diff --git a/framework-bom/framework-bom.gradle b/framework-bom/framework-bom.gradle new file mode 100644 index 0000000..840f205 --- /dev/null +++ b/framework-bom/framework-bom.gradle @@ -0,0 +1,23 @@ +description = "Spring Framework (Bill of Materials)" + +apply plugin: 'java-platform' +apply from: "$rootDir/gradle/publications.gradle" + +group = "org.springframework" + +dependencies { + constraints { + parent.moduleProjects.sort { "$it.name" }.each { + api it + } + } +} + +publishing { + publications { + mavenJava(MavenPublication) { + artifactId = 'spring-framework-bom' + from components.javaPlatform + } + } +} \ No newline at end of file diff --git a/integration-tests/integration-tests.gradle b/integration-tests/integration-tests.gradle new file mode 100644 index 0000000..71e23b9 --- /dev/null +++ b/integration-tests/integration-tests.gradle @@ -0,0 +1,30 @@ +description = "Spring Integration Tests" + +dependencies { + testCompile(project(":spring-aop")) + testCompile(project(":spring-beans")) + testCompile(project(":spring-context")) + testCompile(project(":spring-core")) + testCompile(testFixtures(project(":spring-aop"))) + testCompile(testFixtures(project(":spring-beans"))) + testCompile(testFixtures(project(":spring-core"))) + testCompile(testFixtures(project(":spring-tx"))) + testCompile(project(":spring-expression")) + testCompile(project(":spring-jdbc")) + testCompile(project(":spring-orm")) + testCompile(project(":spring-test")) + testCompile(project(":spring-tx")) + testCompile(project(":spring-web")) + testCompile("javax.inject:javax.inject") + testCompile("javax.resource:javax.resource-api") + testCompile("javax.servlet:javax.servlet-api") + testCompile("org.aspectj:aspectjweaver") + testCompile("org.hsqldb:hsqldb") + testCompile("org.hibernate:hibernate-core") +} + +normalization { + runtimeClasspath { + ignore "META-INF/MANIFEST.MF" + } +} diff --git a/integration-tests/src/test/java/org/springframework/aop/config/AopNamespaceHandlerAdviceOrderIntegrationTests.java b/integration-tests/src/test/java/org/springframework/aop/config/AopNamespaceHandlerAdviceOrderIntegrationTests.java new file mode 100644 index 0000000..da32ff1 --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/aop/config/AopNamespaceHandlerAdviceOrderIntegrationTests.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.config; + +import java.util.ArrayList; +import java.util.List; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Integration tests for advice invocation order for advice configured via the + * AOP namespace. + * + * @author Sam Brannen + * @since 5.2.7 + * @see org.springframework.aop.framework.autoproxy.AspectJAutoProxyAdviceOrderIntegrationTests + */ +class AopNamespaceHandlerAdviceOrderIntegrationTests { + + @Nested + @SpringJUnitConfig(locations = "AopNamespaceHandlerAdviceOrderIntegrationTests-afterFirst.xml") + @DirtiesContext + class AfterAdviceFirstTests { + + @Test + void afterAdviceIsInvokedFirst(@Autowired Echo echo, @Autowired InvocationTrackingAspect aspect) throws Exception { + assertThat(aspect.invocations).isEmpty(); + assertThat(echo.echo(42)).isEqualTo(42); + assertThat(aspect.invocations).containsExactly("around - start", "before", "around - end", "after", "after returning"); + + aspect.invocations.clear(); + assertThatExceptionOfType(Exception.class).isThrownBy(() -> echo.echo(new Exception())); + assertThat(aspect.invocations).containsExactly("around - start", "before", "around - end", "after", "after throwing"); + } + } + + @Nested + @SpringJUnitConfig(locations = "AopNamespaceHandlerAdviceOrderIntegrationTests-afterLast.xml") + @DirtiesContext + class AfterAdviceLastTests { + + @Test + void afterAdviceIsInvokedLast(@Autowired Echo echo, @Autowired InvocationTrackingAspect aspect) throws Exception { + assertThat(aspect.invocations).isEmpty(); + assertThat(echo.echo(42)).isEqualTo(42); + assertThat(aspect.invocations).containsExactly("around - start", "before", "around - end", "after returning", "after"); + + aspect.invocations.clear(); + assertThatExceptionOfType(Exception.class).isThrownBy(() -> echo.echo(new Exception())); + assertThat(aspect.invocations).containsExactly("around - start", "before", "around - end", "after throwing", "after"); + } + } + + + static class Echo { + + Object echo(Object obj) throws Exception { + if (obj instanceof Exception) { + throw (Exception) obj; + } + return obj; + } + } + + static class InvocationTrackingAspect { + + List invocations = new ArrayList<>(); + + Object around(ProceedingJoinPoint joinPoint) throws Throwable { + invocations.add("around - start"); + try { + return joinPoint.proceed(); + } + finally { + invocations.add("around - end"); + } + } + + void before() { + invocations.add("before"); + } + + void afterReturning() { + invocations.add("after returning"); + } + + void afterThrowing() { + invocations.add("after throwing"); + } + + void after() { + invocations.add("after"); + } + } + +} diff --git a/integration-tests/src/test/java/org/springframework/aop/config/AopNamespaceHandlerScopeIntegrationTests.java b/integration-tests/src/test/java/org/springframework/aop/config/AopNamespaceHandlerScopeIntegrationTests.java new file mode 100644 index 0000000..b4c1131 --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/aop/config/AopNamespaceHandlerScopeIntegrationTests.java @@ -0,0 +1,137 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.config; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.Advised; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.testfixture.io.SerializationTestUtils; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for scoped proxy use in conjunction with aop: namespace. + * Deemed an integration test because .web mocks and application contexts are required. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Chris Beams + * @see org.springframework.aop.config.AopNamespaceHandlerTests + */ +@SpringJUnitWebConfig +class AopNamespaceHandlerScopeIntegrationTests { + + @Autowired + ITestBean singletonScoped; + + @Autowired + ITestBean requestScoped; + + @Autowired + ITestBean sessionScoped; + + @Autowired + ITestBean sessionScopedAlias; + + @Autowired + ITestBean testBean; + + + @Test + void testSingletonScoping() throws Exception { + assertThat(AopUtils.isAopProxy(singletonScoped)).as("Should be AOP proxy").isTrue(); + boolean condition = singletonScoped instanceof TestBean; + assertThat(condition).as("Should be target class proxy").isTrue(); + String rob = "Rob Harrop"; + String bram = "Bram Smeets"; + assertThat(singletonScoped.getName()).isEqualTo(rob); + singletonScoped.setName(bram); + assertThat(singletonScoped.getName()).isEqualTo(bram); + ITestBean deserialized = SerializationTestUtils.serializeAndDeserialize(singletonScoped); + assertThat(deserialized.getName()).isEqualTo(bram); + } + + @Test + void testRequestScoping() throws Exception { + MockHttpServletRequest oldRequest = new MockHttpServletRequest(); + MockHttpServletRequest newRequest = new MockHttpServletRequest(); + + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(oldRequest)); + + assertThat(AopUtils.isAopProxy(requestScoped)).as("Should be AOP proxy").isTrue(); + boolean condition = requestScoped instanceof TestBean; + assertThat(condition).as("Should be target class proxy").isTrue(); + + assertThat(AopUtils.isAopProxy(testBean)).as("Should be AOP proxy").isTrue(); + boolean condition1 = testBean instanceof TestBean; + assertThat(condition1).as("Regular bean should be JDK proxy").isFalse(); + + String rob = "Rob Harrop"; + String bram = "Bram Smeets"; + + assertThat(requestScoped.getName()).isEqualTo(rob); + requestScoped.setName(bram); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(newRequest)); + assertThat(requestScoped.getName()).isEqualTo(rob); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(oldRequest)); + assertThat(requestScoped.getName()).isEqualTo(bram); + + assertThat(((Advised) requestScoped).getAdvisors().length > 0).as("Should have advisors").isTrue(); + } + + @Test + void testSessionScoping() throws Exception { + MockHttpSession oldSession = new MockHttpSession(); + MockHttpSession newSession = new MockHttpSession(); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setSession(oldSession); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + + assertThat(AopUtils.isAopProxy(sessionScoped)).as("Should be AOP proxy").isTrue(); + boolean condition1 = sessionScoped instanceof TestBean; + assertThat(condition1).as("Should not be target class proxy").isFalse(); + + assertThat(sessionScopedAlias).isSameAs(sessionScoped); + + assertThat(AopUtils.isAopProxy(testBean)).as("Should be AOP proxy").isTrue(); + boolean condition = testBean instanceof TestBean; + assertThat(condition).as("Regular bean should be JDK proxy").isFalse(); + + String rob = "Rob Harrop"; + String bram = "Bram Smeets"; + + assertThat(sessionScoped.getName()).isEqualTo(rob); + sessionScoped.setName(bram); + request.setSession(newSession); + assertThat(sessionScoped.getName()).isEqualTo(rob); + request.setSession(oldSession); + assertThat(sessionScoped.getName()).isEqualTo(bram); + + assertThat(((Advised) sessionScoped).getAdvisors().length > 0).as("Should have advisors").isTrue(); + } + +} diff --git a/integration-tests/src/test/java/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorIntegrationTests.java b/integration-tests/src/test/java/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorIntegrationTests.java new file mode 100644 index 0000000..cf067e0 --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorIntegrationTests.java @@ -0,0 +1,318 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.autoproxy; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.List; + +import javax.servlet.ServletException; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.support.AopUtils; +import org.springframework.aop.support.StaticMethodMatcherPointcutAdvisor; +import org.springframework.aop.testfixture.advice.CountingBeforeAdvice; +import org.springframework.aop.testfixture.advice.MethodCounter; +import org.springframework.aop.testfixture.interceptor.NopInterceptor; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.lang.Nullable; +import org.springframework.transaction.NoTransactionException; +import org.springframework.transaction.interceptor.TransactionInterceptor; +import org.springframework.transaction.testfixture.CallCountingTransactionManager; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for auto proxy creation by advisor recognition working in + * conjunction with transaction management resources. + * + * @see org.springframework.aop.framework.autoproxy.AdvisorAutoProxyCreatorTests + * + * @author Rod Johnson + * @author Chris Beams + */ +class AdvisorAutoProxyCreatorIntegrationTests { + + private static final Class CLASS = AdvisorAutoProxyCreatorIntegrationTests.class; + private static final String CLASSNAME = CLASS.getSimpleName(); + + private static final String DEFAULT_CONTEXT = CLASSNAME + "-context.xml"; + + private static final String ADVISOR_APC_BEAN_NAME = "aapc"; + private static final String TXMANAGER_BEAN_NAME = "txManager"; + + /** + * Return a bean factory with attributes and EnterpriseServices configured. + */ + protected BeanFactory getBeanFactory() throws IOException { + return new ClassPathXmlApplicationContext(DEFAULT_CONTEXT, CLASS); + } + + @Test + void testDefaultExclusionPrefix() throws Exception { + DefaultAdvisorAutoProxyCreator aapc = (DefaultAdvisorAutoProxyCreator) getBeanFactory().getBean(ADVISOR_APC_BEAN_NAME); + assertThat(aapc.getAdvisorBeanNamePrefix()).isEqualTo((ADVISOR_APC_BEAN_NAME + DefaultAdvisorAutoProxyCreator.SEPARATOR)); + assertThat(aapc.isUsePrefix()).isFalse(); + } + + /** + * If no pointcuts match (no attrs) there should be proxying. + */ + @Test + void testNoProxy() throws Exception { + BeanFactory bf = getBeanFactory(); + Object o = bf.getBean("noSetters"); + assertThat(AopUtils.isAopProxy(o)).isFalse(); + } + + @Test + void testTxIsProxied() throws Exception { + BeanFactory bf = getBeanFactory(); + ITestBean test = (ITestBean) bf.getBean("test"); + assertThat(AopUtils.isAopProxy(test)).isTrue(); + } + + @Test + void testRegexpApplied() throws Exception { + BeanFactory bf = getBeanFactory(); + ITestBean test = (ITestBean) bf.getBean("test"); + MethodCounter counter = (MethodCounter) bf.getBean("countingAdvice"); + assertThat(counter.getCalls()).isEqualTo(0); + test.getName(); + assertThat(counter.getCalls()).isEqualTo(1); + } + + @Test + void testTransactionAttributeOnMethod() throws Exception { + BeanFactory bf = getBeanFactory(); + ITestBean test = (ITestBean) bf.getBean("test"); + + CallCountingTransactionManager txMan = (CallCountingTransactionManager) bf.getBean(TXMANAGER_BEAN_NAME); + OrderedTxCheckAdvisor txc = (OrderedTxCheckAdvisor) bf.getBean("orderedBeforeTransaction"); + assertThat(txc.getCountingBeforeAdvice().getCalls()).isEqualTo(0); + + assertThat(txMan.commits).isEqualTo(0); + assertThat(test.getAge()).as("Initial value was correct").isEqualTo(4); + int newAge = 5; + test.setAge(newAge); + assertThat(txc.getCountingBeforeAdvice().getCalls()).isEqualTo(1); + + assertThat(test.getAge()).as("New value set correctly").isEqualTo(newAge); + assertThat(txMan.commits).as("Transaction counts match").isEqualTo(1); + } + + /** + * Should not roll back on servlet exception. + */ + @Test + void testRollbackRulesOnMethodCauseRollback() throws Exception { + BeanFactory bf = getBeanFactory(); + Rollback rb = (Rollback) bf.getBean("rollback"); + + CallCountingTransactionManager txMan = (CallCountingTransactionManager) bf.getBean(TXMANAGER_BEAN_NAME); + OrderedTxCheckAdvisor txc = (OrderedTxCheckAdvisor) bf.getBean("orderedBeforeTransaction"); + assertThat(txc.getCountingBeforeAdvice().getCalls()).isEqualTo(0); + + assertThat(txMan.commits).isEqualTo(0); + rb.echoException(null); + // Fires only on setters + assertThat(txc.getCountingBeforeAdvice().getCalls()).isEqualTo(0); + assertThat(txMan.commits).as("Transaction counts match").isEqualTo(1); + + assertThat(txMan.rollbacks).isEqualTo(0); + Exception ex = new Exception(); + try { + rb.echoException(ex); + } + catch (Exception actual) { + assertThat(actual).isEqualTo(ex); + } + assertThat(txMan.rollbacks).as("Transaction counts match").isEqualTo(1); + } + + @Test + void testRollbackRulesOnMethodPreventRollback() throws Exception { + BeanFactory bf = getBeanFactory(); + Rollback rb = (Rollback) bf.getBean("rollback"); + + CallCountingTransactionManager txMan = (CallCountingTransactionManager) bf.getBean(TXMANAGER_BEAN_NAME); + + assertThat(txMan.commits).isEqualTo(0); + // Should NOT roll back on ServletException + try { + rb.echoException(new ServletException()); + } + catch (ServletException ex) { + + } + assertThat(txMan.commits).as("Transaction counts match").isEqualTo(1); + } + + @Test + void testProgrammaticRollback() throws Exception { + BeanFactory bf = getBeanFactory(); + + Object bean = bf.getBean(TXMANAGER_BEAN_NAME); + boolean condition = bean instanceof CallCountingTransactionManager; + assertThat(condition).isTrue(); + CallCountingTransactionManager txMan = (CallCountingTransactionManager) bf.getBean(TXMANAGER_BEAN_NAME); + + Rollback rb = (Rollback) bf.getBean("rollback"); + assertThat(txMan.commits).isEqualTo(0); + rb.rollbackOnly(false); + assertThat(txMan.commits).as("Transaction counts match").isEqualTo(1); + assertThat(txMan.rollbacks).isEqualTo(0); + // Will cause rollback only + rb.rollbackOnly(true); + assertThat(txMan.rollbacks).isEqualTo(1); + } + +} + + +@SuppressWarnings("serial") +class NeverMatchAdvisor extends StaticMethodMatcherPointcutAdvisor { + + public NeverMatchAdvisor() { + super(new NopInterceptor()); + } + + /** + * This method is solely to allow us to create a mixture of dependencies in + * the bean definitions. The dependencies don't have any meaning, and don't + * do anything. + */ + public void setDependencies(List l) { + + } + + /** + * @see org.springframework.aop.MethodMatcher#matches(java.lang.reflect.Method, java.lang.Class) + */ + @Override + public boolean matches(Method m, @Nullable Class targetClass) { + return false; + } + +} + + +class NoSetters { + + public void A() { + + } + + public int getB() { + return -1; + } + +} + + +@SuppressWarnings("serial") +class OrderedTxCheckAdvisor extends StaticMethodMatcherPointcutAdvisor implements InitializingBean { + + /** + * Should we insist on the presence of a transaction attribute or refuse to accept one? + */ + private boolean requireTransactionContext = false; + + + public void setRequireTransactionContext(boolean requireTransactionContext) { + this.requireTransactionContext = requireTransactionContext; + } + + public boolean isRequireTransactionContext() { + return requireTransactionContext; + } + + + public CountingBeforeAdvice getCountingBeforeAdvice() { + return (CountingBeforeAdvice) getAdvice(); + } + + @Override + public void afterPropertiesSet() throws Exception { + setAdvice(new TxCountingBeforeAdvice()); + } + + @Override + public boolean matches(Method method, @Nullable Class targetClass) { + return method.getName().startsWith("setAge"); + } + + + private class TxCountingBeforeAdvice extends CountingBeforeAdvice { + + @Override + public void before(Method method, Object[] args, Object target) throws Throwable { + // do transaction checks + if (requireTransactionContext) { + TransactionInterceptor.currentTransactionStatus(); + } + else { + try { + TransactionInterceptor.currentTransactionStatus(); + throw new RuntimeException("Shouldn't have a transaction"); + } + catch (NoTransactionException ex) { + // this is Ok + } + } + super.before(method, args, target); + } + } + +} + + +class Rollback { + + /** + * Inherits transaction attribute. + * Illustrates programmatic rollback. + */ + public void rollbackOnly(boolean rollbackOnly) { + if (rollbackOnly) { + setRollbackOnly(); + } + } + + /** + * Extracted in a protected method to facilitate testing + */ + protected void setRollbackOnly() { + TransactionInterceptor.currentTransactionStatus().setRollbackOnly(); + } + + /** + * @org.springframework.transaction.interceptor.RuleBasedTransaction ( timeout=-1 ) + * @org.springframework.transaction.interceptor.RollbackRule ( "java.lang.Exception" ) + * @org.springframework.transaction.interceptor.NoRollbackRule ( "ServletException" ) + */ + public void echoException(Exception ex) throws Exception { + if (ex != null) { + throw ex; + } + } + +} diff --git a/integration-tests/src/test/java/org/springframework/aop/framework/autoproxy/AspectJAutoProxyAdviceOrderIntegrationTests.java b/integration-tests/src/test/java/org/springframework/aop/framework/autoproxy/AspectJAutoProxyAdviceOrderIntegrationTests.java new file mode 100644 index 0000000..78d4592 --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/aop/framework/autoproxy/AspectJAutoProxyAdviceOrderIntegrationTests.java @@ -0,0 +1,233 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.autoproxy; + +import java.util.ArrayList; +import java.util.List; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.After; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.Pointcut; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Integration tests for advice invocation order for advice configured via + * AspectJ auto-proxy support. + * + * @author Sam Brannen + * @since 5.2.7 + * @see org.springframework.aop.config.AopNamespaceHandlerAdviceOrderIntegrationTests + */ +class AspectJAutoProxyAdviceOrderIntegrationTests { + + /** + * {@link After @After} advice declared as first after method in source code. + */ + @Nested + @SpringJUnitConfig(AfterAdviceFirstConfig.class) + @DirtiesContext + class AfterAdviceFirstTests { + + @Test + void afterAdviceIsInvokedLast(@Autowired Echo echo, @Autowired AfterAdviceFirstAspect aspect) throws Exception { + assertThat(aspect.invocations).isEmpty(); + assertThat(echo.echo(42)).isEqualTo(42); + assertThat(aspect.invocations).containsExactly("around - start", "before", "after returning", "after", "around - end"); + + aspect.invocations.clear(); + assertThatExceptionOfType(Exception.class).isThrownBy( + () -> echo.echo(new Exception())); + assertThat(aspect.invocations).containsExactly("around - start", "before", "after throwing", "after", "around - end"); + } + } + + + /** + * This test class uses {@link AfterAdviceLastAspect} which declares its + * {@link After @After} advice as the last after advice type method + * in its source code. + * + *

On Java versions prior to JDK 7, we would have expected the {@code @After} + * advice method to be invoked before {@code @AfterThrowing} and + * {@code @AfterReturning} advice methods due to the AspectJ precedence + * rules implemented in + * {@link org.springframework.aop.aspectj.autoproxy.AspectJPrecedenceComparator}. + */ + @Nested + @SpringJUnitConfig(AfterAdviceLastConfig.class) + @DirtiesContext + class AfterAdviceLastTests { + + @Test + void afterAdviceIsInvokedLast(@Autowired Echo echo, @Autowired AfterAdviceLastAspect aspect) throws Exception { + assertThat(aspect.invocations).isEmpty(); + assertThat(echo.echo(42)).isEqualTo(42); + assertThat(aspect.invocations).containsExactly("around - start", "before", "after returning", "after", "around - end"); + + aspect.invocations.clear(); + assertThatExceptionOfType(Exception.class).isThrownBy( + () -> echo.echo(new Exception())); + assertThat(aspect.invocations).containsExactly("around - start", "before", "after throwing", "after", "around - end"); + } + } + + + @Configuration + @EnableAspectJAutoProxy(proxyTargetClass = true) + static class AfterAdviceFirstConfig { + + @Bean + AfterAdviceFirstAspect echoAspect() { + return new AfterAdviceFirstAspect(); + } + + @Bean + Echo echo() { + return new Echo(); + } + } + + @Configuration + @EnableAspectJAutoProxy(proxyTargetClass = true) + static class AfterAdviceLastConfig { + + @Bean + AfterAdviceLastAspect echoAspect() { + return new AfterAdviceLastAspect(); + } + + @Bean + Echo echo() { + return new Echo(); + } + } + + static class Echo { + + Object echo(Object obj) throws Exception { + if (obj instanceof Exception) { + throw (Exception) obj; + } + return obj; + } + } + + /** + * {@link After @After} advice declared as first after method in source code. + */ + @Aspect + static class AfterAdviceFirstAspect { + + List invocations = new ArrayList<>(); + + @Pointcut("execution(* echo(*))") + void echo() { + } + + @After("echo()") + void after() { + invocations.add("after"); + } + + @AfterReturning("echo()") + void afterReturning() { + invocations.add("after returning"); + } + + @AfterThrowing("echo()") + void afterThrowing() { + invocations.add("after throwing"); + } + + @Before("echo()") + void before() { + invocations.add("before"); + } + + @Around("echo()") + Object around(ProceedingJoinPoint joinPoint) throws Throwable { + invocations.add("around - start"); + try { + return joinPoint.proceed(); + } + finally { + invocations.add("around - end"); + } + } + } + + /** + * {@link After @After} advice declared as last after method in source code. + */ + @Aspect + static class AfterAdviceLastAspect { + + List invocations = new ArrayList<>(); + + @Pointcut("execution(* echo(*))") + void echo() { + } + + @Around("echo()") + Object around(ProceedingJoinPoint joinPoint) throws Throwable { + invocations.add("around - start"); + try { + return joinPoint.proceed(); + } + finally { + invocations.add("around - end"); + } + } + + @Before("echo()") + void before() { + invocations.add("before"); + } + + @AfterReturning("echo()") + void afterReturning() { + invocations.add("after returning"); + } + + @AfterThrowing("echo()") + void afterThrowing() { + invocations.add("after throwing"); + } + + @After("echo()") + void after() { + invocations.add("after"); + } + } + +} diff --git a/integration-tests/src/test/java/org/springframework/beans/factory/xml/Component.java b/integration-tests/src/test/java/org/springframework/beans/factory/xml/Component.java new file mode 100644 index 0000000..aeb34d2 --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/beans/factory/xml/Component.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import java.util.ArrayList; +import java.util.List; + +public class Component { + + private String name; + private List components = new ArrayList<>(); + + // mmm, there is no setter method for the 'components' + public void addComponent(Component component) { + this.components.add(component); + } + + public List getComponents() { + return components; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentBeanDefinitionParser.java b/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentBeanDefinitionParser.java new file mode 100644 index 0000000..833c856 --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentBeanDefinitionParser.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import java.util.List; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.util.CollectionUtils; +import org.springframework.util.xml.DomUtils; + +public class ComponentBeanDefinitionParser extends AbstractBeanDefinitionParser { + + @Override + protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) { + return parseComponentElement(element); + } + + private static AbstractBeanDefinition parseComponentElement(Element element) { + BeanDefinitionBuilder factory = BeanDefinitionBuilder.rootBeanDefinition(ComponentFactoryBean.class); + factory.addPropertyValue("parent", parseComponent(element)); + + List childElements = DomUtils.getChildElementsByTagName(element, "component"); + if (!CollectionUtils.isEmpty(childElements)) { + parseChildComponents(childElements, factory); + } + + return factory.getBeanDefinition(); + } + + private static BeanDefinition parseComponent(Element element) { + BeanDefinitionBuilder component = BeanDefinitionBuilder.rootBeanDefinition(Component.class); + component.addPropertyValue("name", element.getAttribute("name")); + return component.getBeanDefinition(); + } + + private static void parseChildComponents(List childElements, BeanDefinitionBuilder factory) { + ManagedList children = new ManagedList<>(childElements.size()); + for (Element element : childElements) { + children.add(parseComponentElement(element)); + } + factory.addPropertyValue("children", children); + } + +} diff --git a/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentBeanDefinitionParserTests.java b/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentBeanDefinitionParserTests.java new file mode 100644 index 0000000..fbc3fec --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentBeanDefinitionParserTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import java.util.List; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Costin Leau + */ +@TestInstance(Lifecycle.PER_CLASS) +class ComponentBeanDefinitionParserTests { + + private final DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + + + @BeforeAll + void setUp() throws Exception { + new XmlBeanDefinitionReader(bf).loadBeanDefinitions( + new ClassPathResource("component-config.xml", ComponentBeanDefinitionParserTests.class)); + } + + @AfterAll + void tearDown() { + bf.destroySingletons(); + } + + @Test + void testBionicBasic() { + Component cp = getBionicFamily(); + assertThat("Bionic-1").isEqualTo(cp.getName()); + } + + @Test + void testBionicFirstLevelChildren() { + Component cp = getBionicFamily(); + List components = cp.getComponents(); + assertThat(2).isEqualTo(components.size()); + assertThat("Mother-1").isEqualTo(components.get(0).getName()); + assertThat("Rock-1").isEqualTo(components.get(1).getName()); + } + + @Test + void testBionicSecondLevelChildren() { + Component cp = getBionicFamily(); + List components = cp.getComponents().get(0).getComponents(); + assertThat(2).isEqualTo(components.size()); + assertThat("Karate-1").isEqualTo(components.get(0).getName()); + assertThat("Sport-1").isEqualTo(components.get(1).getName()); + } + + private Component getBionicFamily() { + return bf.getBean("bionic-family", Component.class); + } + +} + diff --git a/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentFactoryBean.java b/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentFactoryBean.java new file mode 100644 index 0000000..ec2479c --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentFactoryBean.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import java.util.List; + +import org.springframework.beans.factory.FactoryBean; + +public class ComponentFactoryBean implements FactoryBean { + + private Component parent; + private List children; + + public void setParent(Component parent) { + this.parent = parent; + } + + public void setChildren(List children) { + this.children = children; + } + + @Override + public Component getObject() throws Exception { + if (this.children != null && this.children.size() > 0) { + for (Component child : children) { + this.parent.addComponent(child); + } + } + return this.parent; + } + + @Override + public Class getObjectType() { + return Component.class; + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentNamespaceHandler.java b/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentNamespaceHandler.java new file mode 100644 index 0000000..abd9867 --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/beans/factory/xml/ComponentNamespaceHandler.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +public class ComponentNamespaceHandler extends NamespaceHandlerSupport { + + @Override + public void init() { + registerBeanDefinitionParser("component", new ComponentBeanDefinitionParser()); + } +} diff --git a/integration-tests/src/test/java/org/springframework/cache/annotation/EnableCachingIntegrationTests.java b/integration-tests/src/test/java/org/springframework/cache/annotation/EnableCachingIntegrationTests.java new file mode 100644 index 0000000..6454829 --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/cache/annotation/EnableCachingIntegrationTests.java @@ -0,0 +1,136 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.annotation; + +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.Advisor; +import org.springframework.aop.framework.Advised; +import org.springframework.aop.support.AopUtils; +import org.springframework.cache.CacheManager; +import org.springframework.cache.interceptor.BeanFactoryCacheOperationSourceAdvisor; +import org.springframework.cache.support.NoOpCacheManager; +import org.springframework.context.annotation.AdviceMode; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Repository; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Integration tests for the @EnableCaching annotation. + * + * @author Chris Beams + * @since 3.1 + */ +@SuppressWarnings("resource") +class EnableCachingIntegrationTests { + + @Test + void repositoryIsClassBasedCacheProxy() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(Config.class, ProxyTargetClassCachingConfig.class); + ctx.refresh(); + + assertCacheProxying(ctx); + assertThat(AopUtils.isCglibProxy(ctx.getBean(FooRepository.class))).isTrue(); + } + + @Test + void repositoryUsesAspectJAdviceMode() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(Config.class, AspectJCacheConfig.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. + assertThatExceptionOfType(Exception.class).isThrownBy( + ctx::refresh) + .withMessageContaining("AspectJCachingConfiguration"); + } + + + private void assertCacheProxying(AnnotationConfigApplicationContext ctx) { + FooRepository repo = ctx.getBean(FooRepository.class); + assertThat(isCacheProxy(repo)).isTrue(); + } + + private boolean isCacheProxy(FooRepository repo) { + if (AopUtils.isAopProxy(repo)) { + for (Advisor advisor : ((Advised)repo).getAdvisors()) { + if (advisor instanceof BeanFactoryCacheOperationSourceAdvisor) { + return true; + } + } + } + return false; + } + + + @Configuration + @EnableCaching(proxyTargetClass=true) + static class ProxyTargetClassCachingConfig { + + @Bean + CacheManager mgr() { + return new NoOpCacheManager(); + } + } + + + @Configuration + static class Config { + + @Bean + FooRepository fooRepository() { + return new DummyFooRepository(); + } + } + + + @Configuration + @EnableCaching(mode=AdviceMode.ASPECTJ) + static class AspectJCacheConfig { + + @Bean + CacheManager cacheManager() { + return new NoOpCacheManager(); + } + } + + + interface FooRepository { + + List findAll(); + } + + + @Repository + static class DummyFooRepository implements FooRepository { + + @Override + @Cacheable("primary") + public List findAll() { + return Collections.emptyList(); + } + } + +} diff --git a/integration-tests/src/test/java/org/springframework/context/annotation/jsr330/ClassPathBeanDefinitionScannerJsr330ScopeIntegrationTests.java b/integration-tests/src/test/java/org/springframework/context/annotation/jsr330/ClassPathBeanDefinitionScannerJsr330ScopeIntegrationTests.java new file mode 100644 index 0000000..bd86cc9 --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/context/annotation/jsr330/ClassPathBeanDefinitionScannerJsr330ScopeIntegrationTests.java @@ -0,0 +1,405 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.jsr330; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.inject.Named; +import javax.inject.Singleton; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.ClassPathBeanDefinitionScanner; +import org.springframework.context.annotation.ScopeMetadata; +import org.springframework.context.annotation.ScopeMetadataResolver; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.context.support.GenericWebApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Mark Fisher + * @author Juergen Hoeller + * @author Chris Beams + */ +class ClassPathBeanDefinitionScannerJsr330ScopeIntegrationTests { + + private static final String DEFAULT_NAME = "default"; + + private static final String MODIFIED_NAME = "modified"; + + private ServletRequestAttributes oldRequestAttributes; + + private ServletRequestAttributes newRequestAttributes; + + private ServletRequestAttributes oldRequestAttributesWithSession; + + private ServletRequestAttributes newRequestAttributesWithSession; + + + @BeforeEach + void setup() { + this.oldRequestAttributes = new ServletRequestAttributes(new MockHttpServletRequest()); + this.newRequestAttributes = new ServletRequestAttributes(new MockHttpServletRequest()); + + MockHttpServletRequest oldRequestWithSession = new MockHttpServletRequest(); + oldRequestWithSession.setSession(new MockHttpSession()); + this.oldRequestAttributesWithSession = new ServletRequestAttributes(oldRequestWithSession); + + MockHttpServletRequest newRequestWithSession = new MockHttpServletRequest(); + newRequestWithSession.setSession(new MockHttpSession()); + this.newRequestAttributesWithSession = new ServletRequestAttributes(newRequestWithSession); + } + + @AfterEach + void reset() { + RequestContextHolder.setRequestAttributes(null); + } + + + @Test + void testPrototype() { + ApplicationContext context = createContext(ScopedProxyMode.NO); + ScopedTestBean bean = (ScopedTestBean) context.getBean("prototype"); + assertThat(bean).isNotNull(); + assertThat(context.isPrototype("prototype")).isTrue(); + assertThat(context.isSingleton("prototype")).isFalse(); + } + + @Test + void testSingletonScopeWithNoProxy() { + RequestContextHolder.setRequestAttributes(oldRequestAttributes); + ApplicationContext context = createContext(ScopedProxyMode.NO); + ScopedTestBean bean = (ScopedTestBean) context.getBean("singleton"); + assertThat(context.isSingleton("singleton")).isTrue(); + assertThat(context.isPrototype("singleton")).isFalse(); + + // should not be a proxy + assertThat(AopUtils.isAopProxy(bean)).isFalse(); + + assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); + bean.setName(MODIFIED_NAME); + + RequestContextHolder.setRequestAttributes(newRequestAttributes); + // not a proxy so this should not have changed + assertThat(bean.getName()).isEqualTo(MODIFIED_NAME); + + // singleton bean, so name should be modified even after lookup + ScopedTestBean bean2 = (ScopedTestBean) context.getBean("singleton"); + assertThat(bean2.getName()).isEqualTo(MODIFIED_NAME); + } + + @Test + void testSingletonScopeIgnoresProxyInterfaces() { + RequestContextHolder.setRequestAttributes(oldRequestAttributes); + ApplicationContext context = createContext(ScopedProxyMode.INTERFACES); + ScopedTestBean bean = (ScopedTestBean) context.getBean("singleton"); + + // should not be a proxy + assertThat(AopUtils.isAopProxy(bean)).isFalse(); + + assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); + bean.setName(MODIFIED_NAME); + + RequestContextHolder.setRequestAttributes(newRequestAttributes); + // not a proxy so this should not have changed + assertThat(bean.getName()).isEqualTo(MODIFIED_NAME); + + // singleton bean, so name should be modified even after lookup + ScopedTestBean bean2 = (ScopedTestBean) context.getBean("singleton"); + assertThat(bean2.getName()).isEqualTo(MODIFIED_NAME); + } + + @Test + void testSingletonScopeIgnoresProxyTargetClass() { + RequestContextHolder.setRequestAttributes(oldRequestAttributes); + ApplicationContext context = createContext(ScopedProxyMode.TARGET_CLASS); + ScopedTestBean bean = (ScopedTestBean) context.getBean("singleton"); + + // should not be a proxy + assertThat(AopUtils.isAopProxy(bean)).isFalse(); + + assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); + bean.setName(MODIFIED_NAME); + + RequestContextHolder.setRequestAttributes(newRequestAttributes); + // not a proxy so this should not have changed + assertThat(bean.getName()).isEqualTo(MODIFIED_NAME); + + // singleton bean, so name should be modified even after lookup + ScopedTestBean bean2 = (ScopedTestBean) context.getBean("singleton"); + assertThat(bean2.getName()).isEqualTo(MODIFIED_NAME); + } + + @Test + void testRequestScopeWithNoProxy() { + RequestContextHolder.setRequestAttributes(oldRequestAttributes); + ApplicationContext context = createContext(ScopedProxyMode.NO); + ScopedTestBean bean = (ScopedTestBean) context.getBean("request"); + + // should not be a proxy + assertThat(AopUtils.isAopProxy(bean)).isFalse(); + + assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); + bean.setName(MODIFIED_NAME); + + RequestContextHolder.setRequestAttributes(newRequestAttributes); + // not a proxy so this should not have changed + assertThat(bean.getName()).isEqualTo(MODIFIED_NAME); + + // but a newly retrieved bean should have the default name + ScopedTestBean bean2 = (ScopedTestBean) context.getBean("request"); + assertThat(bean2.getName()).isEqualTo(DEFAULT_NAME); + } + + @Test + void testRequestScopeWithProxiedInterfaces() { + RequestContextHolder.setRequestAttributes(oldRequestAttributes); + ApplicationContext context = createContext(ScopedProxyMode.INTERFACES); + IScopedTestBean bean = (IScopedTestBean) context.getBean("request"); + + // should be dynamic proxy, implementing both interfaces + assertThat(AopUtils.isJdkDynamicProxy(bean)).isTrue(); + boolean condition = bean instanceof AnotherScopeTestInterface; + assertThat(condition).isTrue(); + + assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); + bean.setName(MODIFIED_NAME); + + RequestContextHolder.setRequestAttributes(newRequestAttributes); + // this is a proxy so it should be reset to default + assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); + + RequestContextHolder.setRequestAttributes(oldRequestAttributes); + assertThat(bean.getName()).isEqualTo(MODIFIED_NAME); + } + + @Test + void testRequestScopeWithProxiedTargetClass() { + RequestContextHolder.setRequestAttributes(oldRequestAttributes); + ApplicationContext context = createContext(ScopedProxyMode.TARGET_CLASS); + IScopedTestBean bean = (IScopedTestBean) context.getBean("request"); + + // should be a class-based proxy + assertThat(AopUtils.isCglibProxy(bean)).isTrue(); + boolean condition = bean instanceof RequestScopedTestBean; + assertThat(condition).isTrue(); + + assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); + bean.setName(MODIFIED_NAME); + + RequestContextHolder.setRequestAttributes(newRequestAttributes); + // this is a proxy so it should be reset to default + assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); + + RequestContextHolder.setRequestAttributes(oldRequestAttributes); + assertThat(bean.getName()).isEqualTo(MODIFIED_NAME); + } + + @Test + void testSessionScopeWithNoProxy() { + RequestContextHolder.setRequestAttributes(oldRequestAttributesWithSession); + ApplicationContext context = createContext(ScopedProxyMode.NO); + ScopedTestBean bean = (ScopedTestBean) context.getBean("session"); + + // should not be a proxy + assertThat(AopUtils.isAopProxy(bean)).isFalse(); + + assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); + bean.setName(MODIFIED_NAME); + + RequestContextHolder.setRequestAttributes(newRequestAttributesWithSession); + // not a proxy so this should not have changed + assertThat(bean.getName()).isEqualTo(MODIFIED_NAME); + + // but a newly retrieved bean should have the default name + ScopedTestBean bean2 = (ScopedTestBean) context.getBean("session"); + assertThat(bean2.getName()).isEqualTo(DEFAULT_NAME); + } + + @Test + void testSessionScopeWithProxiedInterfaces() { + RequestContextHolder.setRequestAttributes(oldRequestAttributesWithSession); + ApplicationContext context = createContext(ScopedProxyMode.INTERFACES); + IScopedTestBean bean = (IScopedTestBean) context.getBean("session"); + + // should be dynamic proxy, implementing both interfaces + assertThat(AopUtils.isJdkDynamicProxy(bean)).isTrue(); + boolean condition = bean instanceof AnotherScopeTestInterface; + assertThat(condition).isTrue(); + + assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); + bean.setName(MODIFIED_NAME); + + RequestContextHolder.setRequestAttributes(newRequestAttributesWithSession); + // this is a proxy so it should be reset to default + assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); + bean.setName(MODIFIED_NAME); + + IScopedTestBean bean2 = (IScopedTestBean) context.getBean("session"); + assertThat(bean2.getName()).isEqualTo(MODIFIED_NAME); + bean2.setName(DEFAULT_NAME); + assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); + + RequestContextHolder.setRequestAttributes(oldRequestAttributesWithSession); + assertThat(bean.getName()).isEqualTo(MODIFIED_NAME); + } + + @Test + void testSessionScopeWithProxiedTargetClass() { + RequestContextHolder.setRequestAttributes(oldRequestAttributesWithSession); + ApplicationContext context = createContext(ScopedProxyMode.TARGET_CLASS); + IScopedTestBean bean = (IScopedTestBean) context.getBean("session"); + + // should be a class-based proxy + assertThat(AopUtils.isCglibProxy(bean)).isTrue(); + boolean condition1 = bean instanceof ScopedTestBean; + assertThat(condition1).isTrue(); + boolean condition = bean instanceof SessionScopedTestBean; + assertThat(condition).isTrue(); + + assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); + bean.setName(MODIFIED_NAME); + + RequestContextHolder.setRequestAttributes(newRequestAttributesWithSession); + // this is a proxy so it should be reset to default + assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); + bean.setName(MODIFIED_NAME); + + IScopedTestBean bean2 = (IScopedTestBean) context.getBean("session"); + assertThat(bean2.getName()).isEqualTo(MODIFIED_NAME); + bean2.setName(DEFAULT_NAME); + assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); + + RequestContextHolder.setRequestAttributes(oldRequestAttributesWithSession); + assertThat(bean.getName()).isEqualTo(MODIFIED_NAME); + } + + + private ApplicationContext createContext(final ScopedProxyMode scopedProxyMode) { + GenericWebApplicationContext context = new GenericWebApplicationContext(); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); + scanner.setIncludeAnnotationConfig(false); + scanner.setScopeMetadataResolver(new ScopeMetadataResolver() { + @Override + public ScopeMetadata resolveScopeMetadata(BeanDefinition definition) { + ScopeMetadata metadata = new ScopeMetadata(); + if (definition instanceof AnnotatedBeanDefinition) { + AnnotatedBeanDefinition annDef = (AnnotatedBeanDefinition) definition; + for (String type : annDef.getMetadata().getAnnotationTypes()) { + if (type.equals(javax.inject.Singleton.class.getName())) { + metadata.setScopeName(BeanDefinition.SCOPE_SINGLETON); + break; + } + else if (annDef.getMetadata().getMetaAnnotationTypes(type).contains(javax.inject.Scope.class.getName())) { + metadata.setScopeName(type.substring(type.length() - 13, type.length() - 6).toLowerCase()); + metadata.setScopedProxyMode(scopedProxyMode); + break; + } + else if (type.startsWith("javax.inject")) { + metadata.setScopeName(BeanDefinition.SCOPE_PROTOTYPE); + } + } + } + return metadata; + } + }); + + // Scan twice in order to find errors in the bean definition compatibility check. + scanner.scan(getClass().getPackage().getName()); + scanner.scan(getClass().getPackage().getName()); + + context.registerAlias("classPathBeanDefinitionScannerJsr330ScopeIntegrationTests.SessionScopedTestBean", "session"); + context.refresh(); + return context; + } + + + public interface IScopedTestBean { + + String getName(); + + void setName(String name); + } + + + public static abstract class ScopedTestBean implements IScopedTestBean { + + private String name = DEFAULT_NAME; + + @Override + public String getName() { return this.name; } + + @Override + public void setName(String name) { this.name = name; } + } + + + @Named("prototype") + public static class PrototypeScopedTestBean extends ScopedTestBean { + } + + + @Named("singleton") + @Singleton + public static class SingletonScopedTestBean extends ScopedTestBean { + } + + + public interface AnotherScopeTestInterface { + } + + + @Named("request") + @RequestScoped + public static class RequestScopedTestBean extends ScopedTestBean implements AnotherScopeTestInterface { + } + + + @Named + @SessionScoped + public static class SessionScopedTestBean extends ScopedTestBean implements AnotherScopeTestInterface { + } + + + @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @javax.inject.Scope + public @interface RequestScoped { + } + + + @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @javax.inject.Scope + public @interface SessionScoped { + } + +} diff --git a/integration-tests/src/test/java/org/springframework/context/annotation/scope/ClassPathBeanDefinitionScannerScopeIntegrationTests.java b/integration-tests/src/test/java/org/springframework/context/annotation/scope/ClassPathBeanDefinitionScannerScopeIntegrationTests.java new file mode 100644 index 0000000..c97840f --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/context/annotation/scope/ClassPathBeanDefinitionScannerScopeIntegrationTests.java @@ -0,0 +1,341 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.scope; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.support.AopUtils; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.ClassPathBeanDefinitionScanner; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.stereotype.Component; +import org.springframework.web.context.annotation.RequestScope; +import org.springframework.web.context.annotation.SessionScope; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.context.support.GenericWebApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.context.annotation.ScopedProxyMode.DEFAULT; +import static org.springframework.context.annotation.ScopedProxyMode.INTERFACES; +import static org.springframework.context.annotation.ScopedProxyMode.NO; +import static org.springframework.context.annotation.ScopedProxyMode.TARGET_CLASS; + +/** + * @author Mark Fisher + * @author Juergen Hoeller + * @author Chris Beams + * @author Sam Brannen + */ +class ClassPathBeanDefinitionScannerScopeIntegrationTests { + + private static final String DEFAULT_NAME = "default"; + private static final String MODIFIED_NAME = "modified"; + + private ServletRequestAttributes oldRequestAttributes = new ServletRequestAttributes(new MockHttpServletRequest()); + private ServletRequestAttributes newRequestAttributes = new ServletRequestAttributes(new MockHttpServletRequest()); + + private ServletRequestAttributes oldRequestAttributesWithSession; + private ServletRequestAttributes newRequestAttributesWithSession; + + + @BeforeEach + void setup() { + MockHttpServletRequest oldRequestWithSession = new MockHttpServletRequest(); + oldRequestWithSession.setSession(new MockHttpSession()); + this.oldRequestAttributesWithSession = new ServletRequestAttributes(oldRequestWithSession); + + MockHttpServletRequest newRequestWithSession = new MockHttpServletRequest(); + newRequestWithSession.setSession(new MockHttpSession()); + this.newRequestAttributesWithSession = new ServletRequestAttributes(newRequestWithSession); + } + + @AfterEach + void reset() { + RequestContextHolder.resetRequestAttributes(); + } + + + @Test + void singletonScopeWithNoProxy() { + RequestContextHolder.setRequestAttributes(oldRequestAttributes); + ApplicationContext context = createContext(NO); + ScopedTestBean bean = (ScopedTestBean) context.getBean("singleton"); + + // should not be a proxy + assertThat(AopUtils.isAopProxy(bean)).isFalse(); + + assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); + bean.setName(MODIFIED_NAME); + + RequestContextHolder.setRequestAttributes(newRequestAttributes); + // not a proxy so this should not have changed + assertThat(bean.getName()).isEqualTo(MODIFIED_NAME); + + // singleton bean, so name should be modified even after lookup + ScopedTestBean bean2 = (ScopedTestBean) context.getBean("singleton"); + assertThat(bean2.getName()).isEqualTo(MODIFIED_NAME); + } + + @Test + void singletonScopeIgnoresProxyInterfaces() { + RequestContextHolder.setRequestAttributes(oldRequestAttributes); + ApplicationContext context = createContext(INTERFACES); + ScopedTestBean bean = (ScopedTestBean) context.getBean("singleton"); + + // should not be a proxy + assertThat(AopUtils.isAopProxy(bean)).isFalse(); + + assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); + bean.setName(MODIFIED_NAME); + + RequestContextHolder.setRequestAttributes(newRequestAttributes); + // not a proxy so this should not have changed + assertThat(bean.getName()).isEqualTo(MODIFIED_NAME); + + // singleton bean, so name should be modified even after lookup + ScopedTestBean bean2 = (ScopedTestBean) context.getBean("singleton"); + assertThat(bean2.getName()).isEqualTo(MODIFIED_NAME); + } + + @Test + void singletonScopeIgnoresProxyTargetClass() { + RequestContextHolder.setRequestAttributes(oldRequestAttributes); + ApplicationContext context = createContext(TARGET_CLASS); + ScopedTestBean bean = (ScopedTestBean) context.getBean("singleton"); + + // should not be a proxy + assertThat(AopUtils.isAopProxy(bean)).isFalse(); + + assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); + bean.setName(MODIFIED_NAME); + + RequestContextHolder.setRequestAttributes(newRequestAttributes); + // not a proxy so this should not have changed + assertThat(bean.getName()).isEqualTo(MODIFIED_NAME); + + // singleton bean, so name should be modified even after lookup + ScopedTestBean bean2 = (ScopedTestBean) context.getBean("singleton"); + assertThat(bean2.getName()).isEqualTo(MODIFIED_NAME); + } + + @Test + void requestScopeWithNoProxy() { + RequestContextHolder.setRequestAttributes(oldRequestAttributes); + ApplicationContext context = createContext(NO); + ScopedTestBean bean = (ScopedTestBean) context.getBean("request"); + + // should not be a proxy + assertThat(AopUtils.isAopProxy(bean)).isFalse(); + + assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); + bean.setName(MODIFIED_NAME); + + RequestContextHolder.setRequestAttributes(newRequestAttributes); + // not a proxy so this should not have changed + assertThat(bean.getName()).isEqualTo(MODIFIED_NAME); + + // but a newly retrieved bean should have the default name + ScopedTestBean bean2 = (ScopedTestBean) context.getBean("request"); + assertThat(bean2.getName()).isEqualTo(DEFAULT_NAME); + } + + @Test + void requestScopeWithProxiedInterfaces() { + RequestContextHolder.setRequestAttributes(oldRequestAttributes); + ApplicationContext context = createContext(INTERFACES); + IScopedTestBean bean = (IScopedTestBean) context.getBean("request"); + + // should be dynamic proxy, implementing both interfaces + assertThat(AopUtils.isJdkDynamicProxy(bean)).isTrue(); + boolean condition = bean instanceof AnotherScopeTestInterface; + assertThat(condition).isTrue(); + + assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); + bean.setName(MODIFIED_NAME); + + RequestContextHolder.setRequestAttributes(newRequestAttributes); + // this is a proxy so it should be reset to default + assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); + + RequestContextHolder.setRequestAttributes(oldRequestAttributes); + assertThat(bean.getName()).isEqualTo(MODIFIED_NAME); + } + + @Test + void requestScopeWithProxiedTargetClass() { + RequestContextHolder.setRequestAttributes(oldRequestAttributes); + ApplicationContext context = createContext(TARGET_CLASS); + IScopedTestBean bean = (IScopedTestBean) context.getBean("request"); + + // should be a class-based proxy + assertThat(AopUtils.isCglibProxy(bean)).isTrue(); + boolean condition = bean instanceof RequestScopedTestBean; + assertThat(condition).isTrue(); + + assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); + bean.setName(MODIFIED_NAME); + + RequestContextHolder.setRequestAttributes(newRequestAttributes); + // this is a proxy so it should be reset to default + assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); + + RequestContextHolder.setRequestAttributes(oldRequestAttributes); + assertThat(bean.getName()).isEqualTo(MODIFIED_NAME); + } + + @Test + void sessionScopeWithNoProxy() { + RequestContextHolder.setRequestAttributes(oldRequestAttributesWithSession); + ApplicationContext context = createContext(NO); + ScopedTestBean bean = (ScopedTestBean) context.getBean("session"); + + // should not be a proxy + assertThat(AopUtils.isAopProxy(bean)).isFalse(); + + assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); + bean.setName(MODIFIED_NAME); + + RequestContextHolder.setRequestAttributes(newRequestAttributesWithSession); + // not a proxy so this should not have changed + assertThat(bean.getName()).isEqualTo(MODIFIED_NAME); + + // but a newly retrieved bean should have the default name + ScopedTestBean bean2 = (ScopedTestBean) context.getBean("session"); + assertThat(bean2.getName()).isEqualTo(DEFAULT_NAME); + } + + @Test + void sessionScopeWithProxiedInterfaces() { + RequestContextHolder.setRequestAttributes(oldRequestAttributesWithSession); + ApplicationContext context = createContext(INTERFACES); + IScopedTestBean bean = (IScopedTestBean) context.getBean("session"); + + // should be dynamic proxy, implementing both interfaces + assertThat(AopUtils.isJdkDynamicProxy(bean)).isTrue(); + boolean condition = bean instanceof AnotherScopeTestInterface; + assertThat(condition).isTrue(); + + assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); + bean.setName(MODIFIED_NAME); + + RequestContextHolder.setRequestAttributes(newRequestAttributesWithSession); + // this is a proxy so it should be reset to default + assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); + bean.setName(MODIFIED_NAME); + + IScopedTestBean bean2 = (IScopedTestBean) context.getBean("session"); + assertThat(bean2.getName()).isEqualTo(MODIFIED_NAME); + bean2.setName(DEFAULT_NAME); + assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); + + RequestContextHolder.setRequestAttributes(oldRequestAttributesWithSession); + assertThat(bean.getName()).isEqualTo(MODIFIED_NAME); + } + + @Test + void sessionScopeWithProxiedTargetClass() { + RequestContextHolder.setRequestAttributes(oldRequestAttributesWithSession); + ApplicationContext context = createContext(TARGET_CLASS); + IScopedTestBean bean = (IScopedTestBean) context.getBean("session"); + + // should be a class-based proxy + assertThat(AopUtils.isCglibProxy(bean)).isTrue(); + boolean condition1 = bean instanceof ScopedTestBean; + assertThat(condition1).isTrue(); + boolean condition = bean instanceof SessionScopedTestBean; + assertThat(condition).isTrue(); + + assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); + bean.setName(MODIFIED_NAME); + + RequestContextHolder.setRequestAttributes(newRequestAttributesWithSession); + // this is a proxy so it should be reset to default + assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); + bean.setName(MODIFIED_NAME); + + IScopedTestBean bean2 = (IScopedTestBean) context.getBean("session"); + assertThat(bean2.getName()).isEqualTo(MODIFIED_NAME); + bean2.setName(DEFAULT_NAME); + assertThat(bean.getName()).isEqualTo(DEFAULT_NAME); + + RequestContextHolder.setRequestAttributes(oldRequestAttributesWithSession); + assertThat(bean.getName()).isEqualTo(MODIFIED_NAME); + } + + + private ApplicationContext createContext(ScopedProxyMode scopedProxyMode) { + GenericWebApplicationContext context = new GenericWebApplicationContext(); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); + scanner.setIncludeAnnotationConfig(false); + scanner.setBeanNameGenerator((definition, registry) -> definition.getScope()); + scanner.setScopedProxyMode(scopedProxyMode); + + // Scan twice in order to find errors in the bean definition compatibility check. + scanner.scan(getClass().getPackage().getName()); + scanner.scan(getClass().getPackage().getName()); + + context.refresh(); + return context; + } + + + interface IScopedTestBean { + + String getName(); + + void setName(String name); + } + + + static abstract class ScopedTestBean implements IScopedTestBean { + + private String name = DEFAULT_NAME; + + @Override + public String getName() { return this.name; } + + @Override + public void setName(String name) { this.name = name; } + } + + + @Component + static class SingletonScopedTestBean extends ScopedTestBean { + } + + + interface AnotherScopeTestInterface { + } + + + @Component + @RequestScope(proxyMode = DEFAULT) + static class RequestScopedTestBean extends ScopedTestBean implements AnotherScopeTestInterface { + } + + + @Component + @SessionScope(proxyMode = DEFAULT) + static class SessionScopedTestBean extends ScopedTestBean implements AnotherScopeTestInterface { + } + +} diff --git a/integration-tests/src/test/java/org/springframework/core/env/EnvironmentSystemIntegrationTests.java b/integration-tests/src/test/java/org/springframework/core/env/EnvironmentSystemIntegrationTests.java new file mode 100644 index 0000000..20e23ec --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/core/env/EnvironmentSystemIntegrationTests.java @@ -0,0 +1,710 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import java.io.File; +import java.io.IOException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.annotation.AnnotatedBeanDefinitionReader; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ClassPathBeanDefinitionScanner; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Profile; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.context.support.FileSystemXmlApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.context.support.GenericXmlApplicationContext; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.core.io.ClassPathResource; +import org.springframework.jca.context.ResourceAdapterApplicationContext; +import org.springframework.jca.support.SimpleBootstrapContext; +import org.springframework.jca.work.SimpleTaskWorkManager; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.mock.env.MockPropertySource; +import org.springframework.mock.web.MockServletConfig; +import org.springframework.mock.web.MockServletContext; +import org.springframework.util.FileCopyUtils; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.AbstractRefreshableWebApplicationContext; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.context.support.GenericWebApplicationContext; +import org.springframework.web.context.support.StandardServletEnvironment; +import org.springframework.web.context.support.StaticWebApplicationContext; +import org.springframework.web.context.support.XmlWebApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.beans.factory.support.BeanDefinitionBuilder.rootBeanDefinition; +import static org.springframework.context.ConfigurableApplicationContext.ENVIRONMENT_BEAN_NAME; +import static org.springframework.core.env.EnvironmentSystemIntegrationTests.Constants.DERIVED_DEV_BEAN_NAME; +import static org.springframework.core.env.EnvironmentSystemIntegrationTests.Constants.DERIVED_DEV_ENV_NAME; +import static org.springframework.core.env.EnvironmentSystemIntegrationTests.Constants.DEV_BEAN_NAME; +import static org.springframework.core.env.EnvironmentSystemIntegrationTests.Constants.DEV_ENV_NAME; +import static org.springframework.core.env.EnvironmentSystemIntegrationTests.Constants.ENVIRONMENT_AWARE_BEAN_NAME; +import static org.springframework.core.env.EnvironmentSystemIntegrationTests.Constants.PROD_BEAN_NAME; +import static org.springframework.core.env.EnvironmentSystemIntegrationTests.Constants.PROD_ENV_NAME; +import static org.springframework.core.env.EnvironmentSystemIntegrationTests.Constants.TRANSITIVE_BEAN_NAME; +import static org.springframework.core.env.EnvironmentSystemIntegrationTests.Constants.XML_PATH; + +/** + * System integration tests for container support of the {@link Environment} API. + * + *

+ * Tests all existing BeanFactory and ApplicationContext implementations to ensure that: + *

    + *
  • a standard environment object is always present + *
  • a custom environment object can be set and retrieved against the factory/context + *
  • the {@link EnvironmentAware} interface is respected + *
  • the environment object is registered with the container as a singleton bean (if an + * ApplicationContext) + *
  • bean definition files (if any, and whether XML or @Configuration) are registered + * conditionally based on environment metadata + *
+ * + * @author Chris Beams + * @author Sam Brannen + * @see org.springframework.context.support.EnvironmentIntegrationTests + */ +@SuppressWarnings("resource") +public class EnvironmentSystemIntegrationTests { + + private final ConfigurableEnvironment prodEnv = new StandardEnvironment(); + + private final ConfigurableEnvironment devEnv = new StandardEnvironment(); + + private final ConfigurableEnvironment prodWebEnv = new StandardServletEnvironment(); + + @BeforeEach + void setUp() { + prodEnv.setActiveProfiles(PROD_ENV_NAME); + devEnv.setActiveProfiles(DEV_ENV_NAME); + prodWebEnv.setActiveProfiles(PROD_ENV_NAME); + } + + @Test + void genericApplicationContext_standardEnv() { + ConfigurableApplicationContext ctx = new GenericApplicationContext(newBeanFactoryWithEnvironmentAwareBean()); + ctx.refresh(); + + assertHasStandardEnvironment(ctx); + assertEnvironmentBeanRegistered(ctx); + assertEnvironmentAwareInvoked(ctx, ctx.getEnvironment()); + } + + @Test + void genericApplicationContext_customEnv() { + GenericApplicationContext ctx = new GenericApplicationContext(newBeanFactoryWithEnvironmentAwareBean()); + ctx.setEnvironment(prodEnv); + ctx.refresh(); + + assertHasEnvironment(ctx, prodEnv); + assertEnvironmentBeanRegistered(ctx); + assertEnvironmentAwareInvoked(ctx, prodEnv); + } + + @Test + void xmlBeanDefinitionReader_inheritsEnvironmentFromEnvironmentCapableBDR() { + GenericApplicationContext ctx = new GenericApplicationContext(); + ctx.setEnvironment(prodEnv); + new XmlBeanDefinitionReader(ctx).loadBeanDefinitions(XML_PATH); + ctx.refresh(); + assertThat(ctx.containsBean(DEV_BEAN_NAME)).isFalse(); + assertThat(ctx.containsBean(PROD_BEAN_NAME)).isTrue(); + } + + @Test + void annotatedBeanDefinitionReader_inheritsEnvironmentFromEnvironmentCapableBDR() { + GenericApplicationContext ctx = new GenericApplicationContext(); + ctx.setEnvironment(prodEnv); + new AnnotatedBeanDefinitionReader(ctx).register(Config.class); + ctx.refresh(); + assertThat(ctx.containsBean(DEV_BEAN_NAME)).isFalse(); + assertThat(ctx.containsBean(PROD_BEAN_NAME)).isTrue(); + } + + @Test + void classPathBeanDefinitionScanner_inheritsEnvironmentFromEnvironmentCapableBDR_scanProfileAnnotatedConfigClasses() { + // it's actually ConfigurationClassPostProcessor's Environment that gets the job done here. + GenericApplicationContext ctx = new GenericApplicationContext(); + ctx.setEnvironment(prodEnv); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(ctx); + scanner.scan("org.springframework.core.env.scan1"); + ctx.refresh(); + assertThat(ctx.containsBean(DEV_BEAN_NAME)).isFalse(); + assertThat(ctx.containsBean(PROD_BEAN_NAME)).isTrue(); + } + + @Test + void classPathBeanDefinitionScanner_inheritsEnvironmentFromEnvironmentCapableBDR_scanProfileAnnotatedComponents() { + GenericApplicationContext ctx = new GenericApplicationContext(); + ctx.setEnvironment(prodEnv); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(ctx); + scanner.scan("org.springframework.core.env.scan2"); + ctx.refresh(); + assertThat(scanner.getEnvironment()).isEqualTo(ctx.getEnvironment()); + assertThat(ctx.containsBean(DEV_BEAN_NAME)).isFalse(); + assertThat(ctx.containsBean(PROD_BEAN_NAME)).isTrue(); + } + + @Test + void genericXmlApplicationContext() { + GenericXmlApplicationContext ctx = new GenericXmlApplicationContext(); + assertHasStandardEnvironment(ctx); + ctx.setEnvironment(prodEnv); + ctx.load(XML_PATH); + ctx.refresh(); + + assertHasEnvironment(ctx, prodEnv); + assertEnvironmentBeanRegistered(ctx); + assertEnvironmentAwareInvoked(ctx, prodEnv); + assertThat(ctx.containsBean(DEV_BEAN_NAME)).isFalse(); + assertThat(ctx.containsBean(PROD_BEAN_NAME)).isTrue(); + } + + @Test + void classPathXmlApplicationContext() { + ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext(XML_PATH); + ctx.setEnvironment(prodEnv); + ctx.refresh(); + + assertEnvironmentBeanRegistered(ctx); + assertHasEnvironment(ctx, prodEnv); + assertEnvironmentAwareInvoked(ctx, ctx.getEnvironment()); + assertThat(ctx.containsBean(DEV_BEAN_NAME)).isFalse(); + assertThat(ctx.containsBean(PROD_BEAN_NAME)).isTrue(); + } + + @Test + void fileSystemXmlApplicationContext() throws IOException { + ClassPathResource xml = new ClassPathResource(XML_PATH); + File tmpFile = File.createTempFile("test", "xml"); + FileCopyUtils.copy(xml.getFile(), tmpFile); + + // strange - FSXAC strips leading '/' unless prefixed with 'file:' + ConfigurableApplicationContext ctx = + new FileSystemXmlApplicationContext(new String[] {"file:" + tmpFile.getPath()}, false); + ctx.setEnvironment(prodEnv); + ctx.refresh(); + assertEnvironmentBeanRegistered(ctx); + assertHasEnvironment(ctx, prodEnv); + assertEnvironmentAwareInvoked(ctx, ctx.getEnvironment()); + assertThat(ctx.containsBean(DEV_BEAN_NAME)).isFalse(); + assertThat(ctx.containsBean(PROD_BEAN_NAME)).isTrue(); + } + + @Test + void annotationConfigApplicationContext_withPojos() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + + assertHasStandardEnvironment(ctx); + ctx.setEnvironment(prodEnv); + + ctx.register(EnvironmentAwareBean.class); + ctx.refresh(); + + assertEnvironmentAwareInvoked(ctx, prodEnv); + } + + @Test + void annotationConfigApplicationContext_withProdEnvAndProdConfigClass() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + + assertHasStandardEnvironment(ctx); + ctx.setEnvironment(prodEnv); + + ctx.register(ProdConfig.class); + ctx.refresh(); + + assertThat(ctx.containsBean(PROD_BEAN_NAME)).isTrue(); + } + + @Test + void annotationConfigApplicationContext_withProdEnvAndDevConfigClass() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + + assertHasStandardEnvironment(ctx); + ctx.setEnvironment(prodEnv); + + ctx.register(DevConfig.class); + ctx.refresh(); + + assertThat(ctx.containsBean(DEV_BEAN_NAME)).isFalse(); + assertThat(ctx.containsBean(TRANSITIVE_BEAN_NAME)).isFalse(); + } + + @Test + void annotationConfigApplicationContext_withDevEnvAndDevConfigClass() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + + assertHasStandardEnvironment(ctx); + ctx.setEnvironment(devEnv); + + ctx.register(DevConfig.class); + ctx.refresh(); + + assertThat(ctx.containsBean(DEV_BEAN_NAME)).isTrue(); + assertThat(ctx.containsBean(TRANSITIVE_BEAN_NAME)).isTrue(); + } + + @Test + void annotationConfigApplicationContext_withImportedConfigClasses() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + + assertHasStandardEnvironment(ctx); + ctx.setEnvironment(prodEnv); + + ctx.register(Config.class); + ctx.refresh(); + + assertEnvironmentAwareInvoked(ctx, prodEnv); + assertThat(ctx.containsBean(PROD_BEAN_NAME)).isTrue(); + assertThat(ctx.containsBean(DEV_BEAN_NAME)).isFalse(); + assertThat(ctx.containsBean(TRANSITIVE_BEAN_NAME)).isFalse(); + } + + @Test + void mostSpecificDerivedClassDrivesEnvironment_withDerivedDevEnvAndDerivedDevConfigClass() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + StandardEnvironment derivedDevEnv = new StandardEnvironment(); + derivedDevEnv.setActiveProfiles(DERIVED_DEV_ENV_NAME); + ctx.setEnvironment(derivedDevEnv); + ctx.register(DerivedDevConfig.class); + ctx.refresh(); + + assertThat(ctx.containsBean(DEV_BEAN_NAME)).isTrue(); + assertThat(ctx.containsBean(DERIVED_DEV_BEAN_NAME)).isTrue(); + assertThat(ctx.containsBean(TRANSITIVE_BEAN_NAME)).isTrue(); + } + + @Test + void mostSpecificDerivedClassDrivesEnvironment_withDevEnvAndDerivedDevConfigClass() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.setEnvironment(devEnv); + ctx.register(DerivedDevConfig.class); + ctx.refresh(); + + assertThat(ctx.containsBean(DEV_BEAN_NAME)).isFalse(); + assertThat(ctx.containsBean(DERIVED_DEV_BEAN_NAME)).isFalse(); + assertThat(ctx.containsBean(TRANSITIVE_BEAN_NAME)).isFalse(); + } + + @Test + void annotationConfigApplicationContext_withProfileExpressionMatchOr() { + testProfileExpression(true, "p3"); + } + + @Test + void annotationConfigApplicationContext_withProfileExpressionMatchAnd() { + testProfileExpression(true, "p1", "p2"); + } + + @Test + void annotationConfigApplicationContext_withProfileExpressionNoMatchAnd() { + testProfileExpression(false, "p1"); + } + + @Test + void annotationConfigApplicationContext_withProfileExpressionNoMatchNone() { + testProfileExpression(false, "p4"); + } + + private void testProfileExpression(boolean expected, String... activeProfiles) { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + StandardEnvironment environment = new StandardEnvironment(); + environment.setActiveProfiles(activeProfiles); + ctx.setEnvironment(environment); + ctx.register(ProfileExpressionConfig.class); + ctx.refresh(); + assertThat(ctx.containsBean("expressionBean")).isEqualTo(expected); + } + + @Test + void webApplicationContext() { + GenericWebApplicationContext ctx = new GenericWebApplicationContext(newBeanFactoryWithEnvironmentAwareBean()); + assertHasStandardServletEnvironment(ctx); + ctx.setEnvironment(prodWebEnv); + ctx.refresh(); + + assertHasEnvironment(ctx, prodWebEnv); + assertEnvironmentBeanRegistered(ctx); + assertEnvironmentAwareInvoked(ctx, prodWebEnv); + } + + @Test + void xmlWebApplicationContext() { + AbstractRefreshableWebApplicationContext ctx = new XmlWebApplicationContext(); + ctx.setConfigLocation("classpath:" + XML_PATH); + ctx.setEnvironment(prodWebEnv); + ctx.refresh(); + + assertHasEnvironment(ctx, prodWebEnv); + assertEnvironmentBeanRegistered(ctx); + assertEnvironmentAwareInvoked(ctx, prodWebEnv); + assertThat(ctx.containsBean(DEV_BEAN_NAME)).isFalse(); + assertThat(ctx.containsBean(PROD_BEAN_NAME)).isTrue(); + } + + @Test + void staticApplicationContext() { + StaticApplicationContext ctx = new StaticApplicationContext(); + + assertHasStandardEnvironment(ctx); + + registerEnvironmentBeanDefinition(ctx); + + ctx.setEnvironment(prodEnv); + ctx.refresh(); + + assertHasEnvironment(ctx, prodEnv); + assertEnvironmentBeanRegistered(ctx); + assertEnvironmentAwareInvoked(ctx, prodEnv); + } + + @Test + void staticWebApplicationContext() { + StaticWebApplicationContext ctx = new StaticWebApplicationContext(); + + assertHasStandardServletEnvironment(ctx); + + registerEnvironmentBeanDefinition(ctx); + + ctx.setEnvironment(prodWebEnv); + ctx.refresh(); + + assertHasEnvironment(ctx, prodWebEnv); + assertEnvironmentBeanRegistered(ctx); + assertEnvironmentAwareInvoked(ctx, prodWebEnv); + } + + @Test + void annotationConfigWebApplicationContext() { + AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext(); + ctx.setEnvironment(prodWebEnv); + ctx.setConfigLocation(EnvironmentAwareBean.class.getName()); + ctx.refresh(); + + assertHasEnvironment(ctx, prodWebEnv); + assertEnvironmentBeanRegistered(ctx); + assertEnvironmentAwareInvoked(ctx, prodWebEnv); + } + + @Test + void registerServletParamPropertySources_AbstractRefreshableWebApplicationContext() { + MockServletContext servletContext = new MockServletContext(); + servletContext.addInitParameter("pCommon", "pCommonContextValue"); + servletContext.addInitParameter("pContext1", "pContext1Value"); + + MockServletConfig servletConfig = new MockServletConfig(servletContext); + servletConfig.addInitParameter("pCommon", "pCommonConfigValue"); + servletConfig.addInitParameter("pConfig1", "pConfig1Value"); + + AbstractRefreshableWebApplicationContext ctx = new AnnotationConfigWebApplicationContext(); + ctx.setConfigLocation(EnvironmentAwareBean.class.getName()); + ctx.setServletConfig(servletConfig); + ctx.refresh(); + + ConfigurableEnvironment environment = ctx.getEnvironment(); + assertThat(environment).isInstanceOf(StandardServletEnvironment.class); + MutablePropertySources propertySources = environment.getPropertySources(); + assertThat(propertySources.contains(StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME)).isTrue(); + assertThat(propertySources.contains(StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME)).isTrue(); + + // ServletConfig gets precedence + assertThat(environment.getProperty("pCommon")).isEqualTo("pCommonConfigValue"); + assertThat(propertySources.precedenceOf(PropertySource.named(StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME))) + .isLessThan(propertySources.precedenceOf(PropertySource.named(StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME))); + + // but all params are available + assertThat(environment.getProperty("pContext1")).isEqualTo("pContext1Value"); + assertThat(environment.getProperty("pConfig1")).isEqualTo("pConfig1Value"); + + // Servlet* PropertySources have precedence over System* PropertySources + assertThat(propertySources.precedenceOf(PropertySource.named(StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME))) + .isLessThan(propertySources.precedenceOf(PropertySource.named(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME))); + + // Replace system properties with a mock property source for convenience + MockPropertySource mockSystemProperties = new MockPropertySource(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME); + mockSystemProperties.setProperty("pCommon", "pCommonSysPropsValue"); + mockSystemProperties.setProperty("pSysProps1", "pSysProps1Value"); + propertySources.replace(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, mockSystemProperties); + + // assert that servletconfig params resolve with higher precedence than sysprops + assertThat(environment.getProperty("pCommon")).isEqualTo("pCommonConfigValue"); + assertThat(environment.getProperty("pSysProps1")).isEqualTo("pSysProps1Value"); + } + + @Test + void registerServletParamPropertySources_GenericWebApplicationContext() { + MockServletContext servletContext = new MockServletContext(); + servletContext.addInitParameter("pCommon", "pCommonContextValue"); + servletContext.addInitParameter("pContext1", "pContext1Value"); + + GenericWebApplicationContext ctx = new GenericWebApplicationContext(); + ctx.setServletContext(servletContext); + ctx.refresh(); + + ConfigurableEnvironment environment = ctx.getEnvironment(); + assertThat(environment).isInstanceOf(StandardServletEnvironment.class); + MutablePropertySources propertySources = environment.getPropertySources(); + assertThat(propertySources.contains(StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME)).isTrue(); + + // ServletContext params are available + assertThat(environment.getProperty("pCommon")).isEqualTo("pCommonContextValue"); + assertThat(environment.getProperty("pContext1")).isEqualTo("pContext1Value"); + + // Servlet* PropertySources have precedence over System* PropertySources + assertThat(propertySources.precedenceOf(PropertySource.named(StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME))) + .isLessThan(propertySources.precedenceOf(PropertySource.named(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME))); + + // Replace system properties with a mock property source for convenience + MockPropertySource mockSystemProperties = new MockPropertySource(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME); + mockSystemProperties.setProperty("pCommon", "pCommonSysPropsValue"); + mockSystemProperties.setProperty("pSysProps1", "pSysProps1Value"); + propertySources.replace(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, mockSystemProperties); + + // assert that servletcontext init params resolve with higher precedence than sysprops + assertThat(environment.getProperty("pCommon")).isEqualTo("pCommonContextValue"); + assertThat(environment.getProperty("pSysProps1")).isEqualTo("pSysProps1Value"); + } + + @Test + void registerServletParamPropertySources_StaticWebApplicationContext() { + MockServletContext servletContext = new MockServletContext(); + servletContext.addInitParameter("pCommon", "pCommonContextValue"); + servletContext.addInitParameter("pContext1", "pContext1Value"); + + MockServletConfig servletConfig = new MockServletConfig(servletContext); + servletConfig.addInitParameter("pCommon", "pCommonConfigValue"); + servletConfig.addInitParameter("pConfig1", "pConfig1Value"); + + StaticWebApplicationContext ctx = new StaticWebApplicationContext(); + ctx.setServletConfig(servletConfig); + ctx.refresh(); + + ConfigurableEnvironment environment = ctx.getEnvironment(); + MutablePropertySources propertySources = environment.getPropertySources(); + assertThat(propertySources.contains(StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME)).isTrue(); + assertThat(propertySources.contains(StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME)).isTrue(); + + // ServletConfig gets precedence + assertThat(environment.getProperty("pCommon")).isEqualTo("pCommonConfigValue"); + assertThat(propertySources.precedenceOf(PropertySource.named(StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME))) + .isLessThan(propertySources.precedenceOf(PropertySource.named(StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME))); + + // but all params are available + assertThat(environment.getProperty("pContext1")).isEqualTo("pContext1Value"); + assertThat(environment.getProperty("pConfig1")).isEqualTo("pConfig1Value"); + + // Servlet* PropertySources have precedence over System* PropertySources + assertThat(propertySources.precedenceOf(PropertySource.named(StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME))) + .isLessThan(propertySources.precedenceOf(PropertySource.named(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME))); + + // Replace system properties with a mock property source for convenience + MockPropertySource mockSystemProperties = new MockPropertySource(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME); + mockSystemProperties.setProperty("pCommon", "pCommonSysPropsValue"); + mockSystemProperties.setProperty("pSysProps1", "pSysProps1Value"); + propertySources.replace(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, mockSystemProperties); + + // assert that servletconfig params resolve with higher precedence than sysprops + assertThat(environment.getProperty("pCommon")).isEqualTo("pCommonConfigValue"); + assertThat(environment.getProperty("pSysProps1")).isEqualTo("pSysProps1Value"); + } + + @Test + void resourceAdapterApplicationContext() { + ResourceAdapterApplicationContext ctx = new ResourceAdapterApplicationContext(new SimpleBootstrapContext(new SimpleTaskWorkManager())); + + assertHasStandardEnvironment(ctx); + + registerEnvironmentBeanDefinition(ctx); + + ctx.setEnvironment(prodEnv); + ctx.refresh(); + + assertHasEnvironment(ctx, prodEnv); + assertEnvironmentBeanRegistered(ctx); + assertEnvironmentAwareInvoked(ctx, prodEnv); + } + + @Test + void abstractApplicationContextValidatesRequiredPropertiesOnRefresh() { + { + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.refresh(); + } + + { + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.getEnvironment().setRequiredProperties("foo", "bar"); + assertThatExceptionOfType(MissingRequiredPropertiesException.class).isThrownBy( + ctx::refresh); + } + + { + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.getEnvironment().setRequiredProperties("foo"); + ctx.setEnvironment(new MockEnvironment().withProperty("foo", "fooValue")); + ctx.refresh(); // should succeed + } + } + + + private DefaultListableBeanFactory newBeanFactoryWithEnvironmentAwareBean() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + registerEnvironmentBeanDefinition(bf); + return bf; + } + + private void registerEnvironmentBeanDefinition(BeanDefinitionRegistry registry) { + registry.registerBeanDefinition(ENVIRONMENT_AWARE_BEAN_NAME, + rootBeanDefinition(EnvironmentAwareBean.class).getBeanDefinition()); + } + + private void assertEnvironmentBeanRegistered( + ConfigurableApplicationContext ctx) { + // ensure environment is registered as a bean + assertThat(ctx.containsBean(ENVIRONMENT_BEAN_NAME)).isTrue(); + } + + private void assertHasStandardEnvironment(ApplicationContext ctx) { + Environment defaultEnv = ctx.getEnvironment(); + assertThat(defaultEnv).isNotNull(); + assertThat(defaultEnv).isInstanceOf(StandardEnvironment.class); + } + + private void assertHasStandardServletEnvironment(WebApplicationContext ctx) { + // ensure a default servlet environment exists + Environment defaultEnv = ctx.getEnvironment(); + assertThat(defaultEnv).isNotNull(); + assertThat(defaultEnv).isInstanceOf(StandardServletEnvironment.class); + } + + private void assertHasEnvironment(ApplicationContext ctx, Environment expectedEnv) { + // ensure the custom environment took + Environment actualEnv = ctx.getEnvironment(); + assertThat(actualEnv).isNotNull(); + assertThat(actualEnv).isEqualTo(expectedEnv); + // ensure environment is registered as a bean + assertThat(ctx.containsBean(ENVIRONMENT_BEAN_NAME)).isTrue(); + } + + private void assertEnvironmentAwareInvoked(ConfigurableApplicationContext ctx, Environment expectedEnv) { + assertThat(ctx.getBean(EnvironmentAwareBean.class).environment).isEqualTo(expectedEnv); + } + + + private static class EnvironmentAwareBean implements EnvironmentAware { + + public Environment environment; + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + } + + + /** + * Mirrors the structure of beans and environment-specific config files in + * EnvironmentSystemIntegrationTests-context.xml + */ + @Configuration + @Import({DevConfig.class, ProdConfig.class}) + static class Config { + @Bean + public EnvironmentAwareBean envAwareBean() { + return new EnvironmentAwareBean(); + } + } + + @Profile(DEV_ENV_NAME) + @Configuration + @Import(TransitiveConfig.class) + static class DevConfig { + @Bean + public Object devBean() { + return new Object(); + } + } + + @Profile(PROD_ENV_NAME) + @Configuration + static class ProdConfig { + @Bean + public Object prodBean() { + return new Object(); + } + } + + @Configuration + static class TransitiveConfig { + @Bean + public Object transitiveBean() { + return new Object(); + } + } + + @Profile(DERIVED_DEV_ENV_NAME) + @Configuration + static class DerivedDevConfig extends DevConfig { + @Bean + public Object derivedDevBean() { + return new Object(); + } + } + + @Profile("(p1 & p2) | p3") + @Configuration + static class ProfileExpressionConfig { + @Bean + public Object expressionBean() { + return new Object(); + } + } + + + /** + * Constants used both locally and in scan* sub-packages + */ + public static class Constants { + + public static final String XML_PATH = "org/springframework/core/env/EnvironmentSystemIntegrationTests-context.xml"; + + public static final String ENVIRONMENT_AWARE_BEAN_NAME = "envAwareBean"; + + public static final String PROD_BEAN_NAME = "prodBean"; + public static final String DEV_BEAN_NAME = "devBean"; + public static final String DERIVED_DEV_BEAN_NAME = "derivedDevBean"; + public static final String TRANSITIVE_BEAN_NAME = "transitiveBean"; + + public static final String PROD_ENV_NAME = "prod"; + public static final String DEV_ENV_NAME = "dev"; + public static final String DERIVED_DEV_ENV_NAME = "derivedDev"; + } + +} diff --git a/integration-tests/src/test/java/org/springframework/core/env/PropertyPlaceholderConfigurerEnvironmentIntegrationTests.java b/integration-tests/src/test/java/org/springframework/core/env/PropertyPlaceholderConfigurerEnvironmentIntegrationTests.java new file mode 100644 index 0000000..248000c --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/core/env/PropertyPlaceholderConfigurerEnvironmentIntegrationTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.support.GenericApplicationContext; + +import static org.springframework.beans.factory.support.BeanDefinitionBuilder.rootBeanDefinition; + +class PropertyPlaceholderConfigurerEnvironmentIntegrationTests { + + @Test + @SuppressWarnings("deprecation") + void test() { + GenericApplicationContext ctx = new GenericApplicationContext(); + ctx.registerBeanDefinition("ppc", + rootBeanDefinition(org.springframework.beans.factory.config.PropertyPlaceholderConfigurer.class) + .addPropertyValue("searchSystemEnvironment", false) + .getBeanDefinition()); + ctx.refresh(); + ctx.getBean("ppc"); + ctx.close(); + } + +} diff --git a/integration-tests/src/test/java/org/springframework/core/env/scan1/Config.java b/integration-tests/src/test/java/org/springframework/core/env/scan1/Config.java new file mode 100644 index 0000000..8e86c30 --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/core/env/scan1/Config.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env.scan1; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import({ DevConfig.class, ProdConfig.class }) +class Config { + +} diff --git a/integration-tests/src/test/java/org/springframework/core/env/scan1/DevConfig.java b/integration-tests/src/test/java/org/springframework/core/env/scan1/DevConfig.java new file mode 100644 index 0000000..d63e797 --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/core/env/scan1/DevConfig.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env.scan1; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +@Profile(org.springframework.core.env.EnvironmentSystemIntegrationTests.Constants.DEV_ENV_NAME) +@Configuration +class DevConfig { + + @Bean + public Object devBean() { + return new Object(); + } + +} diff --git a/integration-tests/src/test/java/org/springframework/core/env/scan1/ProdConfig.java b/integration-tests/src/test/java/org/springframework/core/env/scan1/ProdConfig.java new file mode 100644 index 0000000..eaf7c9a --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/core/env/scan1/ProdConfig.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env.scan1; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +@Profile(org.springframework.core.env.EnvironmentSystemIntegrationTests.Constants.PROD_ENV_NAME) +@Configuration +class ProdConfig { + + @Bean + public Object prodBean() { + return new Object(); + } + +} diff --git a/integration-tests/src/test/java/org/springframework/core/env/scan2/DevBean.java b/integration-tests/src/test/java/org/springframework/core/env/scan2/DevBean.java new file mode 100644 index 0000000..8142b06 --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/core/env/scan2/DevBean.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env.scan2; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Profile(org.springframework.core.env.EnvironmentSystemIntegrationTests.Constants.DEV_ENV_NAME) +@Component(org.springframework.core.env.EnvironmentSystemIntegrationTests.Constants.DEV_BEAN_NAME) +class DevBean { +} diff --git a/integration-tests/src/test/java/org/springframework/core/env/scan2/ProdBean.java b/integration-tests/src/test/java/org/springframework/core/env/scan2/ProdBean.java new file mode 100644 index 0000000..75ee9d5 --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/core/env/scan2/ProdBean.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env.scan2; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Profile(org.springframework.core.env.EnvironmentSystemIntegrationTests.Constants.PROD_ENV_NAME) +@Component(org.springframework.core.env.EnvironmentSystemIntegrationTests.Constants.PROD_BEAN_NAME) +class ProdBean { + +} diff --git a/integration-tests/src/test/java/org/springframework/expression/spel/support/BeanFactoryTypeConverter.java b/integration-tests/src/test/java/org/springframework/expression/spel/support/BeanFactoryTypeConverter.java new file mode 100644 index 0000000..63174d2 --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/expression/spel/support/BeanFactoryTypeConverter.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.support; + +import java.beans.PropertyEditor; + +import org.springframework.beans.BeansException; +import org.springframework.beans.SimpleTypeConverter; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.expression.TypeConverter; + +/** + * Copied from Spring Integration for purposes of reproducing + * {@link Spr7538Tests}. + */ +class BeanFactoryTypeConverter implements TypeConverter, BeanFactoryAware { + + private SimpleTypeConverter delegate = new SimpleTypeConverter(); + + private static ConversionService defaultConversionService; + + private ConversionService conversionService; + + public BeanFactoryTypeConverter() { + synchronized (this) { + if (defaultConversionService == null) { + defaultConversionService = new DefaultConversionService(); + } + } + this.conversionService = defaultConversionService; + } + + public BeanFactoryTypeConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + public void setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + if (beanFactory instanceof ConfigurableBeanFactory) { + Object typeConverter = ((ConfigurableBeanFactory) beanFactory).getTypeConverter(); + if (typeConverter instanceof SimpleTypeConverter) { + delegate = (SimpleTypeConverter) typeConverter; + } + } + } + + public boolean canConvert(Class sourceType, Class targetType) { + if (conversionService.canConvert(sourceType, targetType)) { + return true; + } + if (!String.class.isAssignableFrom(sourceType) && !String.class.isAssignableFrom(targetType)) { + // PropertyEditor cannot convert non-Strings + return false; + } + if (!String.class.isAssignableFrom(sourceType)) { + return delegate.findCustomEditor(sourceType, null) != null || delegate.getDefaultEditor(sourceType) != null; + } + return delegate.findCustomEditor(targetType, null) != null || delegate.getDefaultEditor(targetType) != null; + } + + @Override + public boolean canConvert(TypeDescriptor sourceTypeDescriptor, TypeDescriptor targetTypeDescriptor) { + if (conversionService.canConvert(sourceTypeDescriptor, targetTypeDescriptor)) { + return true; + } + // TODO: what does this mean? This method is not used in SpEL so probably ignorable? + Class sourceType = sourceTypeDescriptor.getObjectType(); + Class targetType = targetTypeDescriptor.getObjectType(); + return canConvert(sourceType, targetType); + } + + @Override + public Object convertValue(Object value, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (targetType.getType() == Void.class || targetType.getType() == Void.TYPE) { + return null; + } + if (conversionService.canConvert(sourceType, targetType)) { + return conversionService.convert(value, sourceType, targetType); + } + if (!String.class.isAssignableFrom(sourceType.getType())) { + PropertyEditor editor = delegate.findCustomEditor(sourceType.getType(), null); + editor.setValue(value); + return editor.getAsText(); + } + return delegate.convertIfNecessary(value, targetType.getType()); + } + +} diff --git a/integration-tests/src/test/java/org/springframework/expression/spel/support/Spr7538Tests.java b/integration-tests/src/test/java/org/springframework/expression/spel/support/Spr7538Tests.java new file mode 100644 index 0000000..372d232 --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/expression/spel/support/Spr7538Tests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.support; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.MethodExecutor; + +class Spr7538Tests { + + @Test + void repro() throws Exception { + AlwaysTrueReleaseStrategy target = new AlwaysTrueReleaseStrategy(); + BeanFactoryTypeConverter converter = new BeanFactoryTypeConverter(); + + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setTypeConverter(converter); + + List arguments = Collections.emptyList(); + + List paramDescriptors = new ArrayList<>(); + Method method = AlwaysTrueReleaseStrategy.class.getMethod("checkCompleteness", List.class); + paramDescriptors.add(new TypeDescriptor(new MethodParameter(method, 0))); + + + List argumentTypes = new ArrayList<>(); + argumentTypes.add(TypeDescriptor.forObject(arguments)); + ReflectiveMethodResolver resolver = new ReflectiveMethodResolver(); + MethodExecutor executor = resolver.resolve(context, target, "checkCompleteness", argumentTypes); + + Object result = executor.execute(context, target, arguments); + System.out.println("Result: " + result); + } + + static class AlwaysTrueReleaseStrategy { + public boolean checkCompleteness(List messages) { + return true; + } + } + + static class Foo{} +} diff --git a/integration-tests/src/test/java/org/springframework/scheduling/annotation/ScheduledAndTransactionalAnnotationIntegrationTests.java b/integration-tests/src/test/java/org/springframework/scheduling/annotation/ScheduledAndTransactionalAnnotationIntegrationTests.java new file mode 100644 index 0000000..16db4fc --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/scheduling/annotation/ScheduledAndTransactionalAnnotationIntegrationTests.java @@ -0,0 +1,250 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.annotation; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.aspectj.lang.annotation.Aspect; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.testfixture.EnabledForTestGroups; +import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor; +import org.springframework.dao.support.PersistenceExceptionTranslator; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.testfixture.CallCountingTransactionManager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.mock; +import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; + +/** + * Integration tests cornering bug SPR-8651, which revealed that @Scheduled methods may + * not work well with beans that have already been proxied for other reasons such + * as @Transactional or @Async processing. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + */ +@SuppressWarnings("resource") +@EnabledForTestGroups(LONG_RUNNING) +class ScheduledAndTransactionalAnnotationIntegrationTests { + + @Test + void failsWhenJdkProxyAndScheduledMethodNotPresentOnInterface() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(Config.class, JdkProxyTxConfig.class, RepoConfigA.class); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(ctx::refresh) + .withCauseInstanceOf(IllegalStateException.class); + } + + @Test + void succeedsWhenSubclassProxyAndScheduledMethodNotPresentOnInterface() throws InterruptedException { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(Config.class, SubclassProxyTxConfig.class, RepoConfigA.class); + ctx.refresh(); + + Thread.sleep(100); // allow @Scheduled method to be called several times + + MyRepository repository = ctx.getBean(MyRepository.class); + CallCountingTransactionManager txManager = ctx.getBean(CallCountingTransactionManager.class); + assertThat(AopUtils.isCglibProxy(repository)).isEqualTo(true); + assertThat(repository.getInvocationCount()).isGreaterThan(0); + assertThat(txManager.commits).isGreaterThan(0); + } + + @Test + void succeedsWhenJdkProxyAndScheduledMethodIsPresentOnInterface() throws InterruptedException { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(Config.class, JdkProxyTxConfig.class, RepoConfigB.class); + ctx.refresh(); + + Thread.sleep(100); // allow @Scheduled method to be called several times + + MyRepositoryWithScheduledMethod repository = ctx.getBean(MyRepositoryWithScheduledMethod.class); + CallCountingTransactionManager txManager = ctx.getBean(CallCountingTransactionManager.class); + assertThat(AopUtils.isJdkDynamicProxy(repository)).isTrue(); + assertThat(repository.getInvocationCount()).isGreaterThan(0); + assertThat(txManager.commits).isGreaterThan(0); + } + + @Test + void withAspectConfig() throws InterruptedException { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(AspectConfig.class, MyRepositoryWithScheduledMethodImpl.class); + ctx.refresh(); + + Thread.sleep(100); // allow @Scheduled method to be called several times + + MyRepositoryWithScheduledMethod repository = ctx.getBean(MyRepositoryWithScheduledMethod.class); + assertThat(AopUtils.isCglibProxy(repository)).isTrue(); + assertThat(repository.getInvocationCount()).isGreaterThan(0); + } + + + @Configuration + @EnableTransactionManagement + static class JdkProxyTxConfig { + } + + + @Configuration + @EnableTransactionManagement(proxyTargetClass = true) + static class SubclassProxyTxConfig { + } + + + @Configuration + static class RepoConfigA { + + @Bean + MyRepository repository() { + return new MyRepositoryImpl(); + } + } + + + @Configuration + static class RepoConfigB { + + @Bean + MyRepositoryWithScheduledMethod repository() { + return new MyRepositoryWithScheduledMethodImpl(); + } + } + + + @Configuration + @EnableScheduling + static class Config { + + @Bean + PlatformTransactionManager txManager() { + return new CallCountingTransactionManager(); + } + + @Bean + PersistenceExceptionTranslator peTranslator() { + return mock(PersistenceExceptionTranslator.class); + } + + @Bean + static PersistenceExceptionTranslationPostProcessor peTranslationPostProcessor() { + return new PersistenceExceptionTranslationPostProcessor(); + } + } + + + @Configuration + @EnableScheduling + static class AspectConfig { + + @Bean + static AnnotationAwareAspectJAutoProxyCreator autoProxyCreator() { + AnnotationAwareAspectJAutoProxyCreator apc = new AnnotationAwareAspectJAutoProxyCreator(); + apc.setProxyTargetClass(true); + return apc; + } + + @Bean + static MyAspect myAspect() { + return new MyAspect(); + } + } + + + @Aspect + public static class MyAspect { + + private final AtomicInteger count = new AtomicInteger(); + + @org.aspectj.lang.annotation.Before("execution(* scheduled())") + public void checkTransaction() { + this.count.incrementAndGet(); + } + } + + + public interface MyRepository { + + int getInvocationCount(); + } + + + @Repository + static class MyRepositoryImpl implements MyRepository { + + private final AtomicInteger count = new AtomicInteger(); + + @Transactional + @Scheduled(fixedDelay = 5) + public void scheduled() { + this.count.incrementAndGet(); + } + + @Override + public int getInvocationCount() { + return this.count.get(); + } + } + + + public interface MyRepositoryWithScheduledMethod { + + int getInvocationCount(); + + void scheduled(); + } + + + @Repository + static class MyRepositoryWithScheduledMethodImpl implements MyRepositoryWithScheduledMethod { + + private final AtomicInteger count = new AtomicInteger(); + + @Autowired(required = false) + private MyAspect myAspect; + + @Override + @Transactional + @Scheduled(fixedDelay = 5) + public void scheduled() { + this.count.incrementAndGet(); + } + + @Override + public int getInvocationCount() { + if (this.myAspect != null) { + assertThat(this.myAspect.count.get()).isEqualTo(this.count.get()); + } + return this.count.get(); + } + } + +} diff --git a/integration-tests/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementIntegrationTests.java b/integration-tests/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementIntegrationTests.java new file mode 100644 index 0000000..8ba4373 --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementIntegrationTests.java @@ -0,0 +1,331 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.transaction.annotation; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.Advised; +import org.springframework.aop.support.AopUtils; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.concurrent.ConcurrentMapCache; +import org.springframework.cache.support.SimpleCacheManager; +import org.springframework.context.annotation.AdviceMode; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportResource; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor; +import org.springframework.transaction.testfixture.CallCountingTransactionManager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Integration tests for the @EnableTransactionManagement annotation. + * + * @author Chris Beams + * @author Sam Brannen + * @since 3.1 + */ +@SuppressWarnings("resource") +class EnableTransactionManagementIntegrationTests { + + @Test + void repositoryIsNotTxProxy() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class); + + assertThat(isTxProxy(ctx.getBean(FooRepository.class))).isFalse(); + } + + @Test + void repositoryIsTxProxy_withDefaultTxManagerName() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class, DefaultTxManagerNameConfig.class); + + assertTxProxying(ctx); + } + + @Test + void repositoryIsTxProxy_withCustomTxManagerName() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class, CustomTxManagerNameConfig.class); + + assertTxProxying(ctx); + } + + @Test + void repositoryIsTxProxy_withNonConventionalTxManagerName_fallsBackToByTypeLookup() { + assertTxProxying(new AnnotationConfigApplicationContext(Config.class, NonConventionalTxManagerNameConfig.class)); + } + + @Test + void repositoryIsClassBasedTxProxy() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class, ProxyTargetClassTxConfig.class); + + assertTxProxying(ctx); + assertThat(AopUtils.isCglibProxy(ctx.getBean(FooRepository.class))).isTrue(); + } + + @Test + void repositoryUsesAspectJAdviceMode() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + 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. + assertThatExceptionOfType(Exception.class) + .isThrownBy(ctx::refresh) + .withMessageContaining("AspectJJtaTransactionManagementConfiguration"); + } + + @Test + void implicitTxManager() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImplicitTxManagerConfig.class); + + FooRepository fooRepository = ctx.getBean(FooRepository.class); + fooRepository.findAll(); + + CallCountingTransactionManager txManager = ctx.getBean(CallCountingTransactionManager.class); + assertThat(txManager.begun).isEqualTo(1); + assertThat(txManager.commits).isEqualTo(1); + assertThat(txManager.rollbacks).isEqualTo(0); + } + + @Test + void explicitTxManager() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ExplicitTxManagerConfig.class); + + FooRepository fooRepository = ctx.getBean(FooRepository.class); + fooRepository.findAll(); + + CallCountingTransactionManager txManager1 = ctx.getBean("txManager1", CallCountingTransactionManager.class); + assertThat(txManager1.begun).isEqualTo(1); + assertThat(txManager1.commits).isEqualTo(1); + assertThat(txManager1.rollbacks).isEqualTo(0); + + CallCountingTransactionManager txManager2 = ctx.getBean("txManager2", CallCountingTransactionManager.class); + assertThat(txManager2.begun).isEqualTo(0); + assertThat(txManager2.commits).isEqualTo(0); + assertThat(txManager2.rollbacks).isEqualTo(0); + } + + @Test + void apcEscalation() { + new AnnotationConfigApplicationContext(EnableTxAndCachingConfig.class); + } + + + private void assertTxProxying(AnnotationConfigApplicationContext ctx) { + FooRepository repo = ctx.getBean(FooRepository.class); + assertThat(isTxProxy(repo)).isTrue(); + // trigger a transaction + repo.findAll(); + } + + private boolean isTxProxy(FooRepository repo) { + if (!AopUtils.isAopProxy(repo)) { + return false; + } + return Arrays.stream(((Advised) repo).getAdvisors()) + .anyMatch(BeanFactoryTransactionAttributeSourceAdvisor.class::isInstance); + } + + + @Configuration + @EnableTransactionManagement + @ImportResource("org/springframework/transaction/annotation/enable-caching.xml") + static class EnableTxAndCachingConfig { + + @Bean + public PlatformTransactionManager txManager() { + return new CallCountingTransactionManager(); + } + + @Bean + public FooRepository fooRepository() { + return new DummyFooRepository(); + } + + @Bean + public CacheManager cacheManager() { + SimpleCacheManager mgr = new SimpleCacheManager(); + ArrayList caches = new ArrayList<>(); + caches.add(new ConcurrentMapCache("")); + mgr.setCaches(caches); + return mgr; + } + } + + + @Configuration + @EnableTransactionManagement + static class ImplicitTxManagerConfig { + + @Bean + public PlatformTransactionManager txManager() { + return new CallCountingTransactionManager(); + } + + @Bean + public FooRepository fooRepository() { + return new DummyFooRepository(); + } + } + + + @Configuration + @EnableTransactionManagement + static class ExplicitTxManagerConfig implements TransactionManagementConfigurer { + + @Bean + public PlatformTransactionManager txManager1() { + return new CallCountingTransactionManager(); + } + + @Bean + public PlatformTransactionManager txManager2() { + return new CallCountingTransactionManager(); + } + + @Override + public PlatformTransactionManager annotationDrivenTransactionManager() { + return txManager1(); + } + + @Bean + public FooRepository fooRepository() { + return new DummyFooRepository(); + } + } + + + @Configuration + @EnableTransactionManagement + static class DefaultTxManagerNameConfig { + + @Bean + PlatformTransactionManager transactionManager(DataSource dataSource) { + return new DataSourceTransactionManager(dataSource); + } + } + + + @Configuration + @EnableTransactionManagement + static class CustomTxManagerNameConfig { + + @Bean + PlatformTransactionManager txManager(DataSource dataSource) { + return new DataSourceTransactionManager(dataSource); + } + } + + + @Configuration + @EnableTransactionManagement + static class NonConventionalTxManagerNameConfig { + + @Bean + PlatformTransactionManager txManager(DataSource dataSource) { + return new DataSourceTransactionManager(dataSource); + } + } + + + @Configuration + @EnableTransactionManagement(proxyTargetClass=true) + static class ProxyTargetClassTxConfig { + + @Bean + PlatformTransactionManager transactionManager(DataSource dataSource) { + return new DataSourceTransactionManager(dataSource); + } + } + + + @Configuration + @EnableTransactionManagement(mode=AdviceMode.ASPECTJ) + static class AspectJTxConfig { + + @Bean + PlatformTransactionManager transactionManager(DataSource dataSource) { + return new DataSourceTransactionManager(dataSource); + } + } + + + @Configuration + static class Config { + + @Bean + FooRepository fooRepository() { + JdbcFooRepository repos = new JdbcFooRepository(); + repos.setDataSource(dataSource()); + return repos; + } + + @Bean + DataSource dataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.HSQL) + .build(); + } + } + + + interface FooRepository { + + List findAll(); + } + + + @Repository + static class JdbcFooRepository implements FooRepository { + + public void setDataSource(DataSource dataSource) { + } + + @Override + @Transactional + public List findAll() { + return Collections.emptyList(); + } + } + + + @Repository + static class DummyFooRepository implements FooRepository { + + @Override + @Transactional + public List findAll() { + return Collections.emptyList(); + } + } + +} diff --git a/integration-tests/src/test/java/org/springframework/transaction/annotation/ProxyAnnotationDiscoveryTests.java b/integration-tests/src/test/java/org/springframework/transaction/annotation/ProxyAnnotationDiscoveryTests.java new file mode 100644 index 0000000..67a61e8 --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/transaction/annotation/ProxyAnnotationDiscoveryTests.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.transaction.annotation; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.support.AopUtils; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests proving that regardless the proxy strategy used (JDK interface-based vs. CGLIB + * subclass-based), discovery of advice-oriented annotations is consistent. + * + * For example, Spring's @Transactional may be declared at the interface or class level, + * and whether interface or subclass proxies are used, the @Transactional annotation must + * be discovered in a consistent fashion. + * + * @author Chris Beams + */ +@SuppressWarnings("resource") +class ProxyAnnotationDiscoveryTests { + + @Test + void annotatedServiceWithoutInterface_PTC_true() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(PTCTrue.class, AnnotatedServiceWithoutInterface.class); + ctx.refresh(); + AnnotatedServiceWithoutInterface s = ctx.getBean(AnnotatedServiceWithoutInterface.class); + assertThat(AopUtils.isCglibProxy(s)).isTrue(); + assertThat(s).isInstanceOf(AnnotatedServiceWithoutInterface.class); + } + + @Test + void annotatedServiceWithoutInterface_PTC_false() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(PTCFalse.class, AnnotatedServiceWithoutInterface.class); + ctx.refresh(); + AnnotatedServiceWithoutInterface s = ctx.getBean(AnnotatedServiceWithoutInterface.class); + assertThat(AopUtils.isCglibProxy(s)).isTrue(); + assertThat(s).isInstanceOf(AnnotatedServiceWithoutInterface.class); + } + + @Test + void nonAnnotatedService_PTC_true() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(PTCTrue.class, AnnotatedServiceImpl.class); + ctx.refresh(); + NonAnnotatedService s = ctx.getBean(NonAnnotatedService.class); + assertThat(AopUtils.isCglibProxy(s)).isTrue(); + assertThat(s).isInstanceOf(AnnotatedServiceImpl.class); + } + + @Test + void nonAnnotatedService_PTC_false() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(PTCFalse.class, AnnotatedServiceImpl.class); + ctx.refresh(); + NonAnnotatedService s = ctx.getBean(NonAnnotatedService.class); + assertThat(AopUtils.isJdkDynamicProxy(s)).isTrue(); + assertThat(s).isNotInstanceOf(AnnotatedServiceImpl.class); + } + + @Test + void annotatedService_PTC_true() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(PTCTrue.class, NonAnnotatedServiceImpl.class); + ctx.refresh(); + AnnotatedService s = ctx.getBean(AnnotatedService.class); + assertThat(AopUtils.isCglibProxy(s)).isTrue(); + assertThat(s).isInstanceOf(NonAnnotatedServiceImpl.class); + } + + @Test + void annotatedService_PTC_false() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(PTCFalse.class, NonAnnotatedServiceImpl.class); + ctx.refresh(); + AnnotatedService s = ctx.getBean(AnnotatedService.class); + assertThat(AopUtils.isJdkDynamicProxy(s)).isTrue(); + assertThat(s).isNotInstanceOf(NonAnnotatedServiceImpl.class); + } +} + +@Configuration +@EnableTransactionManagement(proxyTargetClass=false) +class PTCFalse { } + +@Configuration +@EnableTransactionManagement(proxyTargetClass=true) +class PTCTrue { } + +interface NonAnnotatedService { + void m(); +} + +interface AnnotatedService { + @Transactional void m(); +} + +class NonAnnotatedServiceImpl implements AnnotatedService { + @Override + public void m() { } +} + +class AnnotatedServiceImpl implements NonAnnotatedService { + @Override + @Transactional public void m() { } +} + +class AnnotatedServiceWithoutInterface { + @Transactional public void m() { } +} diff --git a/integration-tests/src/test/resources/log4j2-test.xml b/integration-tests/src/test/resources/log4j2-test.xml new file mode 100644 index 0000000..d0ac5e7 --- /dev/null +++ b/integration-tests/src/test/resources/log4j2-test.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/integration-tests/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerAdviceOrderIntegrationTests-afterFirst.xml b/integration-tests/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerAdviceOrderIntegrationTests-afterFirst.xml new file mode 100644 index 0000000..fd98a0c --- /dev/null +++ b/integration-tests/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerAdviceOrderIntegrationTests-afterFirst.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + diff --git a/integration-tests/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerAdviceOrderIntegrationTests-afterLast.xml b/integration-tests/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerAdviceOrderIntegrationTests-afterLast.xml new file mode 100644 index 0000000..bcab0df --- /dev/null +++ b/integration-tests/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerAdviceOrderIntegrationTests-afterLast.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + diff --git a/integration-tests/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerScopeIntegrationTests-context.xml b/integration-tests/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerScopeIntegrationTests-context.xml new file mode 100644 index 0000000..ab89153 --- /dev/null +++ b/integration-tests/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerScopeIntegrationTests-context.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/integration-tests/src/test/resources/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorIntegrationTests-context.xml b/integration-tests/src/test/resources/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorIntegrationTests-context.xml new file mode 100644 index 0000000..90ea7dc --- /dev/null +++ b/integration-tests/src/test/resources/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorIntegrationTests-context.xml @@ -0,0 +1,107 @@ + + + + + + + + Matches all Advisors in the factory: we don't use a prefix + + + + + + + 9 + false + + + + 11 + true + + + + + true + + + + + + + + + PROPAGATION_REQUIRED + PROPAGATION_REQUIRED + PROPAGATION_REQUIRED,+javax.servlet.ServletException,-java.lang.Exception + + + + + + + + + + + + 10 + + + + + + + + + + + + + + + + + + + + + + + + org.springframework.beans.testfixture.beans.ITestBean.getName + + + + + + 4 + + + + + + + + + + + + + + + + diff --git a/integration-tests/src/test/resources/org/springframework/beans/factory/xml/component-config.xml b/integration-tests/src/test/resources/org/springframework/beans/factory/xml/component-config.xml new file mode 100644 index 0000000..1a649ba --- /dev/null +++ b/integration-tests/src/test/resources/org/springframework/beans/factory/xml/component-config.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/integration-tests/src/test/resources/org/springframework/beans/factory/xml/component.xsd b/integration-tests/src/test/resources/org/springframework/beans/factory/xml/component.xsd new file mode 100644 index 0000000..b98c04f --- /dev/null +++ b/integration-tests/src/test/resources/org/springframework/beans/factory/xml/component.xsd @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/integration-tests/src/test/resources/org/springframework/context/annotation/ltw/ComponentScanningWithLTWTests.xml b/integration-tests/src/test/resources/org/springframework/context/annotation/ltw/ComponentScanningWithLTWTests.xml new file mode 100644 index 0000000..b012d13 --- /dev/null +++ b/integration-tests/src/test/resources/org/springframework/context/annotation/ltw/ComponentScanningWithLTWTests.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + diff --git a/integration-tests/src/test/resources/org/springframework/core/env/EnvironmentSystemIntegrationTests-context-dev.xml b/integration-tests/src/test/resources/org/springframework/core/env/EnvironmentSystemIntegrationTests-context-dev.xml new file mode 100644 index 0000000..f9da4bf --- /dev/null +++ b/integration-tests/src/test/resources/org/springframework/core/env/EnvironmentSystemIntegrationTests-context-dev.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/integration-tests/src/test/resources/org/springframework/core/env/EnvironmentSystemIntegrationTests-context-prod.xml b/integration-tests/src/test/resources/org/springframework/core/env/EnvironmentSystemIntegrationTests-context-prod.xml new file mode 100644 index 0000000..b40902f --- /dev/null +++ b/integration-tests/src/test/resources/org/springframework/core/env/EnvironmentSystemIntegrationTests-context-prod.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/integration-tests/src/test/resources/org/springframework/core/env/EnvironmentSystemIntegrationTests-context.xml b/integration-tests/src/test/resources/org/springframework/core/env/EnvironmentSystemIntegrationTests-context.xml new file mode 100644 index 0000000..dabc2bb --- /dev/null +++ b/integration-tests/src/test/resources/org/springframework/core/env/EnvironmentSystemIntegrationTests-context.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/integration-tests/src/test/resources/org/springframework/transaction/annotation/enable-caching.xml b/integration-tests/src/test/resources/org/springframework/transaction/annotation/enable-caching.xml new file mode 100644 index 0000000..195822b --- /dev/null +++ b/integration-tests/src/test/resources/org/springframework/transaction/annotation/enable-caching.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/integration-tests/src/test/resources/org/springframework/util/testlog4j.properties b/integration-tests/src/test/resources/org/springframework/util/testlog4j.properties new file mode 100644 index 0000000..15d9af5 --- /dev/null +++ b/integration-tests/src/test/resources/org/springframework/util/testlog4j.properties @@ -0,0 +1,2 @@ +log4j.rootCategory=DEBUG, mock +log4j.appender.mock=org.springframework.util.MockLog4jAppender \ No newline at end of file diff --git a/integration-tests/src/test/resources/org/springframework/web/util/testlog4j.properties b/integration-tests/src/test/resources/org/springframework/web/util/testlog4j.properties new file mode 100644 index 0000000..15d9af5 --- /dev/null +++ b/integration-tests/src/test/resources/org/springframework/web/util/testlog4j.properties @@ -0,0 +1,2 @@ +log4j.rootCategory=DEBUG, mock +log4j.appender.mock=org.springframework.util.MockLog4jAppender \ No newline at end of file diff --git a/spring-aop/spring-aop.gradle b/spring-aop/spring-aop.gradle new file mode 100644 index 0000000..73bb378 --- /dev/null +++ b/spring-aop/spring-aop.gradle @@ -0,0 +1,12 @@ +description = "Spring AOP" + +dependencies { + compile(project(":spring-beans")) + compile(project(":spring-core")) + optional("org.aspectj:aspectjweaver") + optional("org.apache.commons:commons-pool2") + optional("com.jamonapi:jamon") + testCompile(testFixtures(project(":spring-beans"))) + testCompile(testFixtures(project(":spring-core"))) + testFixturesImplementation(testFixtures(project(":spring-core"))) +} diff --git a/spring-aop/src/main/java/org/aopalliance/aop/Advice.java b/spring-aop/src/main/java/org/aopalliance/aop/Advice.java new file mode 100644 index 0000000..38f9999 --- /dev/null +++ b/spring-aop/src/main/java/org/aopalliance/aop/Advice.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.aopalliance.aop; + +/** + * Tag interface for Advice. Implementations can be any type + * of advice, such as Interceptors. + * + * @author Rod Johnson + * @version $Id: Advice.java,v 1.1 2004/03/19 17:02:16 johnsonr Exp $ + */ +public interface Advice { + +} diff --git a/spring-aop/src/main/java/org/aopalliance/aop/AspectException.java b/spring-aop/src/main/java/org/aopalliance/aop/AspectException.java new file mode 100644 index 0000000..a91c2ac --- /dev/null +++ b/spring-aop/src/main/java/org/aopalliance/aop/AspectException.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.aopalliance.aop; + +/** + * Superclass for all AOP infrastructure exceptions. + * Unchecked, as such exceptions are fatal and end user + * code shouldn't be forced to catch them. + * + * @author Rod Johnson + * @author Bob Lee + * @author Juergen Hoeller + */ +@SuppressWarnings("serial") +public class AspectException extends RuntimeException { + + /** + * Constructor for AspectException. + * @param message the exception message + */ + public AspectException(String message) { + super(message); + } + + /** + * Constructor for AspectException. + * @param message the exception message + * @param cause the root cause, if any + */ + public AspectException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/spring-aop/src/main/java/org/aopalliance/aop/package-info.java b/spring-aop/src/main/java/org/aopalliance/aop/package-info.java new file mode 100644 index 0000000..add1d41 --- /dev/null +++ b/spring-aop/src/main/java/org/aopalliance/aop/package-info.java @@ -0,0 +1,4 @@ +/** + * The core AOP Alliance advice marker. + */ +package org.aopalliance.aop; diff --git a/spring-aop/src/main/java/org/aopalliance/intercept/ConstructorInterceptor.java b/spring-aop/src/main/java/org/aopalliance/intercept/ConstructorInterceptor.java new file mode 100644 index 0000000..8ac8f65 --- /dev/null +++ b/spring-aop/src/main/java/org/aopalliance/intercept/ConstructorInterceptor.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.aopalliance.intercept; + +import javax.annotation.Nonnull; + +/** + * Intercepts the construction of a new object. + * + *

The user should implement the {@link + * #construct(ConstructorInvocation)} method to modify the original + * behavior. E.g. the following class implements a singleton + * interceptor (allows only one unique instance for the intercepted + * class): + * + *

+ * class DebuggingInterceptor implements ConstructorInterceptor {
+ *   Object instance=null;
+ *
+ *   Object construct(ConstructorInvocation i) throws Throwable {
+ *     if(instance==null) {
+ *       return instance=i.proceed();
+ *     } else {
+ *       throw new Exception("singleton does not allow multiple instance");
+ *     }
+ *   }
+ * }
+ * 
+ * + * @author Rod Johnson + */ +public interface ConstructorInterceptor extends Interceptor { + + /** + * Implement this method to perform extra treatments before and + * after the construction of a new object. Polite implementations + * would certainly like to invoke {@link Joinpoint#proceed()}. + * @param invocation the construction joinpoint + * @return the newly created object, which is also the result of + * the call to {@link Joinpoint#proceed()}; might be replaced by + * the interceptor + * @throws Throwable if the interceptors or the target object + * throws an exception + */ + @Nonnull + Object construct(ConstructorInvocation invocation) throws Throwable; + +} diff --git a/spring-aop/src/main/java/org/aopalliance/intercept/ConstructorInvocation.java b/spring-aop/src/main/java/org/aopalliance/intercept/ConstructorInvocation.java new file mode 100644 index 0000000..7295138 --- /dev/null +++ b/spring-aop/src/main/java/org/aopalliance/intercept/ConstructorInvocation.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.aopalliance.intercept; + +import java.lang.reflect.Constructor; + +import javax.annotation.Nonnull; + +/** + * Description of an invocation to a constructor, given to an + * interceptor upon constructor-call. + * + *

A constructor invocation is a joinpoint and can be intercepted + * by a constructor interceptor. + * + * @author Rod Johnson + * @see ConstructorInterceptor + */ +public interface ConstructorInvocation extends Invocation { + + /** + * Get the constructor being called. + *

This method is a friendly implementation of the + * {@link Joinpoint#getStaticPart()} method (same result). + * @return the constructor being called + */ + @Nonnull + Constructor getConstructor(); + +} diff --git a/spring-aop/src/main/java/org/aopalliance/intercept/Interceptor.java b/spring-aop/src/main/java/org/aopalliance/intercept/Interceptor.java new file mode 100644 index 0000000..918e080 --- /dev/null +++ b/spring-aop/src/main/java/org/aopalliance/intercept/Interceptor.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.aopalliance.intercept; + +import org.aopalliance.aop.Advice; + +/** + * This interface represents a generic interceptor. + * + *

A generic interceptor can intercept runtime events that occur + * within a base program. Those events are materialized by (reified + * in) joinpoints. Runtime joinpoints can be invocations, field + * access, exceptions... + * + *

This interface is not used directly. Use the sub-interfaces + * to intercept specific events. For instance, the following class + * implements some specific interceptors in order to implement a + * debugger: + * + *

+ * class DebuggingInterceptor implements MethodInterceptor,
+ *     ConstructorInterceptor {
+ *
+ *   Object invoke(MethodInvocation i) throws Throwable {
+ *     debug(i.getMethod(), i.getThis(), i.getArgs());
+ *     return i.proceed();
+ *   }
+ *
+ *   Object construct(ConstructorInvocation i) throws Throwable {
+ *     debug(i.getConstructor(), i.getThis(), i.getArgs());
+ *     return i.proceed();
+ *   }
+ *
+ *   void debug(AccessibleObject ao, Object this, Object value) {
+ *     ...
+ *   }
+ * }
+ * 
+ * + * @author Rod Johnson + * @see Joinpoint + */ +public interface Interceptor extends Advice { + +} diff --git a/spring-aop/src/main/java/org/aopalliance/intercept/Invocation.java b/spring-aop/src/main/java/org/aopalliance/intercept/Invocation.java new file mode 100644 index 0000000..96caaef --- /dev/null +++ b/spring-aop/src/main/java/org/aopalliance/intercept/Invocation.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.aopalliance.intercept; + +import javax.annotation.Nonnull; + +/** + * This interface represents an invocation in the program. + * + *

An invocation is a joinpoint and can be intercepted by an + * interceptor. + * + * @author Rod Johnson + */ +public interface Invocation extends Joinpoint { + + /** + * Get the arguments as an array object. + * It is possible to change element values within this + * array to change the arguments. + * @return the argument of the invocation + */ + @Nonnull + Object[] getArguments(); + +} diff --git a/spring-aop/src/main/java/org/aopalliance/intercept/Joinpoint.java b/spring-aop/src/main/java/org/aopalliance/intercept/Joinpoint.java new file mode 100644 index 0000000..780275e --- /dev/null +++ b/spring-aop/src/main/java/org/aopalliance/intercept/Joinpoint.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.aopalliance.intercept; + +import java.lang.reflect.AccessibleObject; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * This interface represents a generic runtime joinpoint (in the AOP + * terminology). + * + *

A runtime joinpoint is an event that occurs on a static + * joinpoint (i.e. a location in a the program). For instance, an + * invocation is the runtime joinpoint on a method (static joinpoint). + * The static part of a given joinpoint can be generically retrieved + * using the {@link #getStaticPart()} method. + * + *

In the context of an interception framework, a runtime joinpoint + * is then the reification of an access to an accessible object (a + * method, a constructor, a field), i.e. the static part of the + * joinpoint. It is passed to the interceptors that are installed on + * the static joinpoint. + * + * @author Rod Johnson + * @see Interceptor + */ +public interface Joinpoint { + + /** + * Proceed to the next interceptor in the chain. + *

The implementation and the semantics of this method depends + * on the actual joinpoint type (see the children interfaces). + * @return see the children interfaces' proceed definition + * @throws Throwable if the joinpoint throws an exception + */ + @Nullable + Object proceed() throws Throwable; + + /** + * Return the object that holds the current joinpoint's static part. + *

For instance, the target object for an invocation. + * @return the object (can be null if the accessible object is static) + */ + @Nullable + Object getThis(); + + /** + * Return the static part of this joinpoint. + *

The static part is an accessible object on which a chain of + * interceptors are installed. + */ + @Nonnull + AccessibleObject getStaticPart(); + +} diff --git a/spring-aop/src/main/java/org/aopalliance/intercept/MethodInterceptor.java b/spring-aop/src/main/java/org/aopalliance/intercept/MethodInterceptor.java new file mode 100644 index 0000000..9188e25 --- /dev/null +++ b/spring-aop/src/main/java/org/aopalliance/intercept/MethodInterceptor.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.aopalliance.intercept; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Intercepts calls on an interface on its way to the target. These + * are nested "on top" of the target. + * + *

The user should implement the {@link #invoke(MethodInvocation)} + * method to modify the original behavior. E.g. the following class + * implements a tracing interceptor (traces all the calls on the + * intercepted method(s)): + * + *

+ * class TracingInterceptor implements MethodInterceptor {
+ *   Object invoke(MethodInvocation i) throws Throwable {
+ *     System.out.println("method "+i.getMethod()+" is called on "+
+ *                        i.getThis()+" with args "+i.getArguments());
+ *     Object ret=i.proceed();
+ *     System.out.println("method "+i.getMethod()+" returns "+ret);
+ *     return ret;
+ *   }
+ * }
+ * 
+ * + * @author Rod Johnson + */ +@FunctionalInterface +public interface MethodInterceptor extends Interceptor { + + /** + * Implement this method to perform extra treatments before and + * after the invocation. Polite implementations would certainly + * like to invoke {@link Joinpoint#proceed()}. + * @param invocation the method invocation joinpoint + * @return the result of the call to {@link Joinpoint#proceed()}; + * might be intercepted by the interceptor + * @throws Throwable if the interceptors or the target object + * throws an exception + */ + @Nullable + Object invoke(@Nonnull MethodInvocation invocation) throws Throwable; + +} diff --git a/spring-aop/src/main/java/org/aopalliance/intercept/MethodInvocation.java b/spring-aop/src/main/java/org/aopalliance/intercept/MethodInvocation.java new file mode 100644 index 0000000..f1f511b --- /dev/null +++ b/spring-aop/src/main/java/org/aopalliance/intercept/MethodInvocation.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.aopalliance.intercept; + +import java.lang.reflect.Method; + +import javax.annotation.Nonnull; + +/** + * Description of an invocation to a method, given to an interceptor + * upon method-call. + * + *

A method invocation is a joinpoint and can be intercepted by a + * method interceptor. + * + * @author Rod Johnson + * @see MethodInterceptor + */ +public interface MethodInvocation extends Invocation { + + /** + * Get the method being called. + *

This method is a friendly implementation of the + * {@link Joinpoint#getStaticPart()} method (same result). + * @return the method being called + */ + @Nonnull + Method getMethod(); + +} diff --git a/spring-aop/src/main/java/org/aopalliance/intercept/package-info.java b/spring-aop/src/main/java/org/aopalliance/intercept/package-info.java new file mode 100644 index 0000000..11ada4f --- /dev/null +++ b/spring-aop/src/main/java/org/aopalliance/intercept/package-info.java @@ -0,0 +1,4 @@ +/** + * The AOP Alliance reflective interception abstraction. + */ +package org.aopalliance.intercept; diff --git a/spring-aop/src/main/java/org/aopalliance/package-info.java b/spring-aop/src/main/java/org/aopalliance/package-info.java new file mode 100644 index 0000000..a525a32 --- /dev/null +++ b/spring-aop/src/main/java/org/aopalliance/package-info.java @@ -0,0 +1,4 @@ +/** + * Spring's variant of the AOP Alliance interfaces. + */ +package org.aopalliance; diff --git a/spring-aop/src/main/java/org/springframework/aop/Advisor.java b/spring-aop/src/main/java/org/springframework/aop/Advisor.java new file mode 100644 index 0000000..b10911a --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/Advisor.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop; + +import org.aopalliance.aop.Advice; + +/** + * Base interface holding AOP advice (action to take at a joinpoint) + * and a filter determining the applicability of the advice (such as + * a pointcut). This interface is not for use by Spring users, but to + * allow for commonality in support for different types of advice. + * + *

Spring AOP is based around around advice delivered via method + * interception, compliant with the AOP Alliance interception API. + * The Advisor interface allows support for different types of advice, + * such as before and after advice, which need not be + * implemented using interception. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +public interface Advisor { + + /** + * Common placeholder for an empty {@code Advice} to be returned from + * {@link #getAdvice()} if no proper advice has been configured (yet). + * @since 5.0 + */ + Advice EMPTY_ADVICE = new Advice() {}; + + + /** + * Return the advice part of this aspect. An advice may be an + * interceptor, a before advice, a throws advice, etc. + * @return the advice that should apply if the pointcut matches + * @see org.aopalliance.intercept.MethodInterceptor + * @see BeforeAdvice + * @see ThrowsAdvice + * @see AfterReturningAdvice + */ + Advice getAdvice(); + + /** + * Return whether this advice is associated with a particular instance + * (for example, creating a mixin) or shared with all instances of + * the advised class obtained from the same Spring bean factory. + *

Note that this method is not currently used by the framework. + * Typical Advisor implementations always return {@code true}. + * Use singleton/prototype bean definitions or appropriate programmatic + * proxy creation to ensure that Advisors have the correct lifecycle model. + * @return whether this advice is associated with a particular target instance + */ + boolean isPerInstance(); + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/AfterAdvice.java b/spring-aop/src/main/java/org/springframework/aop/AfterAdvice.java new file mode 100644 index 0000000..641a701 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/AfterAdvice.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2007 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop; + +import org.aopalliance.aop.Advice; + +/** + * Common marker interface for after advice, + * such as {@link AfterReturningAdvice} and {@link ThrowsAdvice}. + * + * @author Juergen Hoeller + * @since 2.0.3 + * @see BeforeAdvice + */ +public interface AfterAdvice extends Advice { + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/AfterReturningAdvice.java b/spring-aop/src/main/java/org/springframework/aop/AfterReturningAdvice.java new file mode 100644 index 0000000..8c2c5d6 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/AfterReturningAdvice.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop; + +import java.lang.reflect.Method; + +import org.springframework.lang.Nullable; + +/** + * After returning advice is invoked only on normal method return, not if an + * exception is thrown. Such advice can see the return value, but cannot change it. + * + * @author Rod Johnson + * @see MethodBeforeAdvice + * @see ThrowsAdvice + */ +public interface AfterReturningAdvice extends AfterAdvice { + + /** + * Callback after a given method successfully returned. + * @param returnValue the value returned by the method, if any + * @param method the method being invoked + * @param args the arguments to the method + * @param target the target of the method invocation. May be {@code null}. + * @throws Throwable if this object wishes to abort the call. + * Any exception thrown will be returned to the caller if it's + * allowed by the method signature. Otherwise the exception + * will be wrapped as a runtime exception. + */ + void afterReturning(@Nullable Object returnValue, Method method, Object[] args, @Nullable Object target) throws Throwable; + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/AopInvocationException.java b/spring-aop/src/main/java/org/springframework/aop/AopInvocationException.java new file mode 100644 index 0000000..1acee15 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/AopInvocationException.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop; + +import org.springframework.core.NestedRuntimeException; + +/** + * Exception that gets thrown when an AOP invocation failed + * because of misconfiguration or unexpected runtime issues. + * + * @author Juergen Hoeller + * @since 2.0 + */ +@SuppressWarnings("serial") +public class AopInvocationException extends NestedRuntimeException { + + /** + * Constructor for AopInvocationException. + * @param msg the detail message + */ + public AopInvocationException(String msg) { + super(msg); + } + + /** + * Constructor for AopInvocationException. + * @param msg the detail message + * @param cause the root cause + */ + public AopInvocationException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/BeforeAdvice.java b/spring-aop/src/main/java/org/springframework/aop/BeforeAdvice.java new file mode 100644 index 0000000..8012365 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/BeforeAdvice.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2007 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop; + +import org.aopalliance.aop.Advice; + +/** + * Common marker interface for before advice, such as {@link MethodBeforeAdvice}. + * + *

Spring supports only method before advice. Although this is unlikely to change, + * this API is designed to allow field advice in future if desired. + * + * @author Rod Johnson + * @see AfterAdvice + */ +public interface BeforeAdvice extends Advice { + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/ClassFilter.java b/spring-aop/src/main/java/org/springframework/aop/ClassFilter.java new file mode 100644 index 0000000..fa8e206 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/ClassFilter.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop; + +/** + * Filter that restricts matching of a pointcut or introduction to + * a given set of target classes. + * + *

Can be used as part of a {@link Pointcut} or for the entire + * targeting of an {@link IntroductionAdvisor}. + * + *

Concrete implementations of this interface typically should provide proper + * implementations of {@link Object#equals(Object)} and {@link Object#hashCode()} + * in order to allow the filter to be used in caching scenarios — for + * example, in proxies generated by CGLIB. + * + * @author Rod Johnson + * @see Pointcut + * @see MethodMatcher + */ +@FunctionalInterface +public interface ClassFilter { + + /** + * Should the pointcut apply to the given interface or target class? + * @param clazz the candidate target class + * @return whether the advice should apply to the given target class + */ + boolean matches(Class clazz); + + + /** + * Canonical instance of a ClassFilter that matches all classes. + */ + ClassFilter TRUE = TrueClassFilter.INSTANCE; + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/DynamicIntroductionAdvice.java b/spring-aop/src/main/java/org/springframework/aop/DynamicIntroductionAdvice.java new file mode 100644 index 0000000..08c7048 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/DynamicIntroductionAdvice.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop; + +import org.aopalliance.aop.Advice; + +/** + * Subinterface of AOP Alliance Advice that allows additional interfaces + * to be implemented by an Advice, and available via a proxy using that + * interceptor. This is a fundamental AOP concept called introduction. + * + *

Introductions are often mixins, enabling the building of composite + * objects that can achieve many of the goals of multiple inheritance in Java. + * + *

Compared to {qlink IntroductionInfo}, this interface allows an advice to + * implement a range of interfaces that is not necessarily known in advance. + * Thus an {@link IntroductionAdvisor} can be used to specify which interfaces + * will be exposed in an advised object. + * + * @author Rod Johnson + * @since 1.1.1 + * @see IntroductionInfo + * @see IntroductionAdvisor + */ +public interface DynamicIntroductionAdvice extends Advice { + + /** + * Does this introduction advice implement the given interface? + * @param intf the interface to check + * @return whether the advice implements the specified interface + */ + boolean implementsInterface(Class intf); + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/IntroductionAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/IntroductionAdvisor.java new file mode 100644 index 0000000..cd5f52c --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/IntroductionAdvisor.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop; + +/** + * Superinterface for advisors that perform one or more AOP introductions. + * + *

This interface cannot be implemented directly; subinterfaces must + * provide the advice type implementing the introduction. + * + *

Introduction is the implementation of additional interfaces + * (not implemented by a target) via AOP advice. + * + * @author Rod Johnson + * @since 04.04.2003 + * @see IntroductionInterceptor + */ +public interface IntroductionAdvisor extends Advisor, IntroductionInfo { + + /** + * Return the filter determining which target classes this introduction + * should apply to. + *

This represents the class part of a pointcut. Note that method + * matching doesn't make sense to introductions. + * @return the class filter + */ + ClassFilter getClassFilter(); + + /** + * Can the advised interfaces be implemented by the introduction advice? + * Invoked before adding an IntroductionAdvisor. + * @throws IllegalArgumentException if the advised interfaces can't be + * implemented by the introduction advice + */ + void validateInterfaces() throws IllegalArgumentException; + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/IntroductionAwareMethodMatcher.java b/spring-aop/src/main/java/org/springframework/aop/IntroductionAwareMethodMatcher.java new file mode 100644 index 0000000..4185c92 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/IntroductionAwareMethodMatcher.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop; + +import java.lang.reflect.Method; + +/** + * A specialized type of {@link MethodMatcher} that takes into account introductions + * when matching methods. If there are no introductions on the target class, + * a method matcher may be able to optimize matching more effectively for example. + * + * @author Adrian Colyer + * @since 2.0 + */ +public interface IntroductionAwareMethodMatcher extends MethodMatcher { + + /** + * Perform static checking whether the given method matches. This may be invoked + * instead of the 2-arg {@link #matches(java.lang.reflect.Method, Class)} method + * if the caller supports the extended IntroductionAwareMethodMatcher interface. + * @param method the candidate method + * @param targetClass the target class + * @param hasIntroductions {@code true} if the object on whose behalf we are + * asking is the subject on one or more introductions; {@code false} otherwise + * @return whether or not this method matches statically + */ + boolean matches(Method method, Class targetClass, boolean hasIntroductions); + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/IntroductionInfo.java b/spring-aop/src/main/java/org/springframework/aop/IntroductionInfo.java new file mode 100644 index 0000000..534e2db --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/IntroductionInfo.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop; + +/** + * Interface supplying the information necessary to describe an introduction. + * + *

{@link IntroductionAdvisor IntroductionAdvisors} must implement this + * interface. If an {@link org.aopalliance.aop.Advice} implements this, + * it may be used as an introduction without an {@link IntroductionAdvisor}. + * In this case, the advice is self-describing, providing not only the + * necessary behavior, but describing the interfaces it introduces. + * + * @author Rod Johnson + * @since 1.1.1 + */ +public interface IntroductionInfo { + + /** + * Return the additional interfaces introduced by this Advisor or Advice. + * @return the introduced interfaces + */ + Class[] getInterfaces(); + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/IntroductionInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/IntroductionInterceptor.java new file mode 100644 index 0000000..5a8ba21 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/IntroductionInterceptor.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2007 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop; + +import org.aopalliance.intercept.MethodInterceptor; + +/** + * Subinterface of AOP Alliance MethodInterceptor that allows additional interfaces + * to be implemented by the interceptor, and available via a proxy using that + * interceptor. This is a fundamental AOP concept called introduction. + * + *

Introductions are often mixins, enabling the building of composite + * objects that can achieve many of the goals of multiple inheritance in Java. + * + * @author Rod Johnson + * @see DynamicIntroductionAdvice + */ +public interface IntroductionInterceptor extends MethodInterceptor, DynamicIntroductionAdvice { + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/MethodBeforeAdvice.java b/spring-aop/src/main/java/org/springframework/aop/MethodBeforeAdvice.java new file mode 100644 index 0000000..806744d --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/MethodBeforeAdvice.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop; + +import java.lang.reflect.Method; + +import org.springframework.lang.Nullable; + +/** + * Advice invoked before a method is invoked. Such advices cannot + * prevent the method call proceeding, unless they throw a Throwable. + * + * @author Rod Johnson + * @see AfterReturningAdvice + * @see ThrowsAdvice + */ +public interface MethodBeforeAdvice extends BeforeAdvice { + + /** + * Callback before a given method is invoked. + * @param method the method being invoked + * @param args the arguments to the method + * @param target the target of the method invocation. May be {@code null}. + * @throws Throwable if this object wishes to abort the call. + * Any exception thrown will be returned to the caller if it's + * allowed by the method signature. Otherwise the exception + * will be wrapped as a runtime exception. + */ + void before(Method method, Object[] args, @Nullable Object target) throws Throwable; + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/MethodMatcher.java b/spring-aop/src/main/java/org/springframework/aop/MethodMatcher.java new file mode 100644 index 0000000..29127c7 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/MethodMatcher.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop; + +import java.lang.reflect.Method; + +/** + * Part of a {@link Pointcut}: Checks whether the target method is eligible for advice. + * + *

A MethodMatcher may be evaluated statically or at runtime (dynamically). + * Static matching involves method and (possibly) method attributes. Dynamic matching + * also makes arguments for a particular call available, and any effects of running + * previous advice applying to the joinpoint. + * + *

If an implementation returns {@code false} from its {@link #isRuntime()} + * method, evaluation can be performed statically, and the result will be the same + * for all invocations of this method, whatever their arguments. This means that + * if the {@link #isRuntime()} method returns {@code false}, the 3-arg + * {@link #matches(java.lang.reflect.Method, Class, Object[])} method will never be invoked. + * + *

If an implementation returns {@code true} from its 2-arg + * {@link #matches(java.lang.reflect.Method, Class)} method and its {@link #isRuntime()} method + * returns {@code true}, the 3-arg {@link #matches(java.lang.reflect.Method, Class, Object[])} + * method will be invoked immediately before each potential execution of the related advice, + * to decide whether the advice should run. All previous advice, such as earlier interceptors + * in an interceptor chain, will have run, so any state changes they have produced in + * parameters or ThreadLocal state will be available at the time of evaluation. + * + *

Concrete implementations of this interface typically should provide proper + * implementations of {@link Object#equals(Object)} and {@link Object#hashCode()} + * in order to allow the matcher to be used in caching scenarios — for + * example, in proxies generated by CGLIB. + * + * @author Rod Johnson + * @since 11.11.2003 + * @see Pointcut + * @see ClassFilter + */ +public interface MethodMatcher { + + /** + * Perform static checking whether the given method matches. + *

If this returns {@code false} or if the {@link #isRuntime()} + * method returns {@code false}, no runtime check (i.e. no + * {@link #matches(java.lang.reflect.Method, Class, Object[])} call) + * will be made. + * @param method the candidate method + * @param targetClass the target class + * @return whether or not this method matches statically + */ + boolean matches(Method method, Class targetClass); + + /** + * Is this MethodMatcher dynamic, that is, must a final call be made on the + * {@link #matches(java.lang.reflect.Method, Class, Object[])} method at + * runtime even if the 2-arg matches method returns {@code true}? + *

Can be invoked when an AOP proxy is created, and need not be invoked + * again before each method invocation, + * @return whether or not a runtime match via the 3-arg + * {@link #matches(java.lang.reflect.Method, Class, Object[])} method + * is required if static matching passed + */ + boolean isRuntime(); + + /** + * Check whether there a runtime (dynamic) match for this method, + * which must have matched statically. + *

This method is invoked only if the 2-arg matches method returns + * {@code true} for the given method and target class, and if the + * {@link #isRuntime()} method returns {@code true}. Invoked + * immediately before potential running of the advice, after any + * advice earlier in the advice chain has run. + * @param method the candidate method + * @param targetClass the target class + * @param args arguments to the method + * @return whether there's a runtime match + * @see MethodMatcher#matches(Method, Class) + */ + boolean matches(Method method, Class targetClass, Object... args); + + + /** + * Canonical instance that matches all methods. + */ + MethodMatcher TRUE = TrueMethodMatcher.INSTANCE; + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/Pointcut.java b/spring-aop/src/main/java/org/springframework/aop/Pointcut.java new file mode 100644 index 0000000..ffcf92e --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/Pointcut.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop; + +/** + * Core Spring pointcut abstraction. + * + *

A pointcut is composed of a {@link ClassFilter} and a {@link MethodMatcher}. + * Both these basic terms and a Pointcut itself can be combined to build up combinations + * (e.g. through {@link org.springframework.aop.support.ComposablePointcut}). + * + * @author Rod Johnson + * @see ClassFilter + * @see MethodMatcher + * @see org.springframework.aop.support.Pointcuts + * @see org.springframework.aop.support.ClassFilters + * @see org.springframework.aop.support.MethodMatchers + */ +public interface Pointcut { + + /** + * Return the ClassFilter for this pointcut. + * @return the ClassFilter (never {@code null}) + */ + ClassFilter getClassFilter(); + + /** + * Return the MethodMatcher for this pointcut. + * @return the MethodMatcher (never {@code null}) + */ + MethodMatcher getMethodMatcher(); + + + /** + * Canonical Pointcut instance that always matches. + */ + Pointcut TRUE = TruePointcut.INSTANCE; + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/PointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/PointcutAdvisor.java new file mode 100644 index 0000000..69eb504 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/PointcutAdvisor.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop; + +/** + * Superinterface for all Advisors that are driven by a pointcut. + * This covers nearly all advisors except introduction advisors, + * for which method-level matching doesn't apply. + * + * @author Rod Johnson + */ +public interface PointcutAdvisor extends Advisor { + + /** + * Get the Pointcut that drives this advisor. + */ + Pointcut getPointcut(); + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/ProxyMethodInvocation.java b/spring-aop/src/main/java/org/springframework/aop/ProxyMethodInvocation.java new file mode 100644 index 0000000..2cc6376 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/ProxyMethodInvocation.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop; + +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.lang.Nullable; + +/** + * Extension of the AOP Alliance {@link org.aopalliance.intercept.MethodInvocation} + * interface, allowing access to the proxy that the method invocation was made through. + * + *

Useful to be able to substitute return values with the proxy, + * if necessary, for example if the invocation target returned itself. + * + * @author Juergen Hoeller + * @author Adrian Colyer + * @since 1.1.3 + * @see org.springframework.aop.framework.ReflectiveMethodInvocation + * @see org.springframework.aop.support.DelegatingIntroductionInterceptor + */ +public interface ProxyMethodInvocation extends MethodInvocation { + + /** + * Return the proxy that this method invocation was made through. + * @return the original proxy object + */ + Object getProxy(); + + /** + * Create a clone of this object. If cloning is done before {@code proceed()} + * is invoked on this object, {@code proceed()} can be invoked once per clone + * to invoke the joinpoint (and the rest of the advice chain) more than once. + * @return an invocable clone of this invocation. + * {@code proceed()} can be called once per clone. + */ + MethodInvocation invocableClone(); + + /** + * Create a clone of this object. If cloning is done before {@code proceed()} + * is invoked on this object, {@code proceed()} can be invoked once per clone + * to invoke the joinpoint (and the rest of the advice chain) more than once. + * @param arguments the arguments that the cloned invocation is supposed to use, + * overriding the original arguments + * @return an invocable clone of this invocation. + * {@code proceed()} can be called once per clone. + */ + MethodInvocation invocableClone(Object... arguments); + + /** + * Set the arguments to be used on subsequent invocations in the any advice + * in this chain. + * @param arguments the argument array + */ + void setArguments(Object... arguments); + + /** + * Add the specified user attribute with the given value to this invocation. + *

Such attributes are not used within the AOP framework itself. They are + * just kept as part of the invocation object, for use in special interceptors. + * @param key the name of the attribute + * @param value the value of the attribute, or {@code null} to reset it + */ + void setUserAttribute(String key, @Nullable Object value); + + /** + * Return the value of the specified user attribute. + * @param key the name of the attribute + * @return the value of the attribute, or {@code null} if not set + * @see #setUserAttribute + */ + @Nullable + Object getUserAttribute(String key); + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/RawTargetAccess.java b/spring-aop/src/main/java/org/springframework/aop/RawTargetAccess.java new file mode 100644 index 0000000..7a15b34 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/RawTargetAccess.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2007 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop; + +/** + * Marker for AOP proxy interfaces (in particular: introduction interfaces) + * that explicitly intend to return the raw target object (which would normally + * get replaced with the proxy object when returned from a method invocation). + * + *

Note that this is a marker interface in the style of {@link java.io.Serializable}, + * semantically applying to a declared interface rather than to the full class + * of a concrete object. In other words, this marker applies to a particular + * interface only (typically an introduction interface that does not serve + * as the primary interface of an AOP proxy), and hence does not affect + * other interfaces that a concrete AOP proxy may implement. + * + * @author Juergen Hoeller + * @since 2.0.5 + * @see org.springframework.aop.scope.ScopedObject + */ +public interface RawTargetAccess { + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/SpringProxy.java b/spring-aop/src/main/java/org/springframework/aop/SpringProxy.java new file mode 100644 index 0000000..42e44ce --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/SpringProxy.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2007 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop; + +/** + * Marker interface implemented by all AOP proxies. Used to detect + * whether or not objects are Spring-generated proxies. + * + * @author Rob Harrop + * @since 2.0.1 + * @see org.springframework.aop.support.AopUtils#isAopProxy(Object) + */ +public interface SpringProxy { + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/TargetClassAware.java b/spring-aop/src/main/java/org/springframework/aop/TargetClassAware.java new file mode 100644 index 0000000..d518ddb --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/TargetClassAware.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop; + +import org.springframework.lang.Nullable; + +/** + * Minimal interface for exposing the target class behind a proxy. + * + *

Implemented by AOP proxy objects and proxy factories + * (via {@link org.springframework.aop.framework.Advised}) + * as well as by {@link TargetSource TargetSources}. + * + * @author Juergen Hoeller + * @since 2.0.3 + * @see org.springframework.aop.support.AopUtils#getTargetClass(Object) + */ +public interface TargetClassAware { + + /** + * Return the target class behind the implementing object + * (typically a proxy configuration or an actual proxy). + * @return the target Class, or {@code null} if not known + */ + @Nullable + Class getTargetClass(); + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/TargetSource.java b/spring-aop/src/main/java/org/springframework/aop/TargetSource.java new file mode 100644 index 0000000..e894c5c --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/TargetSource.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop; + +import org.springframework.lang.Nullable; + +/** + * A {@code TargetSource} is used to obtain the current "target" of + * an AOP invocation, which will be invoked via reflection if no around + * advice chooses to end the interceptor chain itself. + * + *

If a {@code TargetSource} is "static", it will always return + * the same target, allowing optimizations in the AOP framework. Dynamic + * target sources can support pooling, hot swapping, etc. + * + *

Application developers don't usually need to work with + * {@code TargetSources} directly: this is an AOP framework interface. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +public interface TargetSource extends TargetClassAware { + + /** + * Return the type of targets returned by this {@link TargetSource}. + *

Can return {@code null}, although certain usages of a {@code TargetSource} + * might just work with a predetermined target class. + * @return the type of targets returned by this {@link TargetSource} + */ + @Override + @Nullable + Class getTargetClass(); + + /** + * Will all calls to {@link #getTarget()} return the same object? + *

In that case, there will be no need to invoke {@link #releaseTarget(Object)}, + * and the AOP framework can cache the return value of {@link #getTarget()}. + * @return {@code true} if the target is immutable + * @see #getTarget + */ + boolean isStatic(); + + /** + * Return a target instance. Invoked immediately before the + * AOP framework calls the "target" of an AOP method invocation. + * @return the target object which contains the joinpoint, + * or {@code null} if there is no actual target instance + * @throws Exception if the target object can't be resolved + */ + @Nullable + Object getTarget() throws Exception; + + /** + * Release the given target object obtained from the + * {@link #getTarget()} method, if any. + * @param target object obtained from a call to {@link #getTarget()} + * @throws Exception if the object can't be released + */ + void releaseTarget(Object target) throws Exception; + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/ThrowsAdvice.java b/spring-aop/src/main/java/org/springframework/aop/ThrowsAdvice.java new file mode 100644 index 0000000..ef50fe8 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/ThrowsAdvice.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2008 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop; + +/** + * Tag interface for throws advice. + * + *

There are not any methods on this interface, as methods are invoked by + * reflection. Implementing classes must implement methods of the form: + * + *

void afterThrowing([Method, args, target], ThrowableSubclass);
+ * + *

Some examples of valid methods would be: + * + *

public void afterThrowing(Exception ex)
+ *
public void afterThrowing(RemoteException)
+ *
public void afterThrowing(Method method, Object[] args, Object target, Exception ex)
+ *
public void afterThrowing(Method method, Object[] args, Object target, ServletException ex)
+ * + * The first three arguments are optional, and only useful if we want further + * information about the joinpoint, as in AspectJ after-throwing advice. + * + *

Note: If a throws-advice method throws an exception itself, it will + * override the original exception (i.e. change the exception thrown to the user). + * The overriding exception will typically be a RuntimeException; this is compatible + * with any method signature. However, if a throws-advice method throws a checked + * exception, it will have to match the declared exceptions of the target method + * and is hence to some degree coupled to specific target method signatures. + * Do not throw an undeclared checked exception that is incompatible with + * the target method's signature! + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see AfterReturningAdvice + * @see MethodBeforeAdvice + */ +public interface ThrowsAdvice extends AfterAdvice { + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/TrueClassFilter.java b/spring-aop/src/main/java/org/springframework/aop/TrueClassFilter.java new file mode 100644 index 0000000..b5bd71f --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/TrueClassFilter.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop; + +import java.io.Serializable; + +/** + * Canonical ClassFilter instance that matches all classes. + * + * @author Rod Johnson + */ +@SuppressWarnings("serial") +final class TrueClassFilter implements ClassFilter, Serializable { + + public static final TrueClassFilter INSTANCE = new TrueClassFilter(); + + /** + * Enforce Singleton pattern. + */ + private TrueClassFilter() { + } + + @Override + public boolean matches(Class clazz) { + return true; + } + + /** + * Required to support serialization. Replaces with canonical + * instance on deserialization, protecting Singleton pattern. + * Alternative to overriding {@code equals()}. + */ + private Object readResolve() { + return INSTANCE; + } + + @Override + public String toString() { + return "ClassFilter.TRUE"; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/TrueMethodMatcher.java b/spring-aop/src/main/java/org/springframework/aop/TrueMethodMatcher.java new file mode 100644 index 0000000..6498627 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/TrueMethodMatcher.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop; + +import java.io.Serializable; +import java.lang.reflect.Method; + +/** + * Canonical MethodMatcher instance that matches all methods. + * + * @author Rod Johnson + */ +@SuppressWarnings("serial") +final class TrueMethodMatcher implements MethodMatcher, Serializable { + + public static final TrueMethodMatcher INSTANCE = new TrueMethodMatcher(); + + + /** + * Enforce Singleton pattern. + */ + private TrueMethodMatcher() { + } + + + @Override + public boolean isRuntime() { + return false; + } + + @Override + public boolean matches(Method method, Class targetClass) { + return true; + } + + @Override + public boolean matches(Method method, Class targetClass, Object... args) { + // Should never be invoked as isRuntime returns false. + throw new UnsupportedOperationException(); + } + + + @Override + public String toString() { + return "MethodMatcher.TRUE"; + } + + /** + * Required to support serialization. Replaces with canonical + * instance on deserialization, protecting Singleton pattern. + * Alternative to overriding {@code equals()}. + */ + private Object readResolve() { + return INSTANCE; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/TruePointcut.java b/spring-aop/src/main/java/org/springframework/aop/TruePointcut.java new file mode 100644 index 0000000..f767d75 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/TruePointcut.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop; + +import java.io.Serializable; + +/** + * Canonical Pointcut instance that always matches. + * + * @author Rod Johnson + */ +@SuppressWarnings("serial") +final class TruePointcut implements Pointcut, Serializable { + + public static final TruePointcut INSTANCE = new TruePointcut(); + + /** + * Enforce Singleton pattern. + */ + private TruePointcut() { + } + + @Override + public ClassFilter getClassFilter() { + return ClassFilter.TRUE; + } + + @Override + public MethodMatcher getMethodMatcher() { + return MethodMatcher.TRUE; + } + + /** + * Required to support serialization. Replaces with canonical + * instance on deserialization, protecting Singleton pattern. + * Alternative to overriding {@code equals()}. + */ + private Object readResolve() { + return INSTANCE; + } + + @Override + public String toString() { + return "Pointcut.TRUE"; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java new file mode 100644 index 0000000..8ef1bf1 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java @@ -0,0 +1,735 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; + +import org.aopalliance.aop.Advice; +import org.aopalliance.intercept.MethodInvocation; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.weaver.tools.JoinPointMatch; +import org.aspectj.weaver.tools.PointcutParameter; + +import org.springframework.aop.AopInvocationException; +import org.springframework.aop.MethodMatcher; +import org.springframework.aop.Pointcut; +import org.springframework.aop.ProxyMethodInvocation; +import org.springframework.aop.interceptor.ExposeInvocationInterceptor; +import org.springframework.aop.support.ComposablePointcut; +import org.springframework.aop.support.MethodMatchers; +import org.springframework.aop.support.StaticMethodMatcher; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * Base class for AOP Alliance {@link org.aopalliance.aop.Advice} classes + * wrapping an AspectJ aspect or an AspectJ-annotated advice method. + * + * @author Rod Johnson + * @author Adrian Colyer + * @author Juergen Hoeller + * @author Ramnivas Laddad + * @since 2.0 + */ +@SuppressWarnings("serial") +public abstract class AbstractAspectJAdvice implements Advice, AspectJPrecedenceInformation, Serializable { + + /** + * Key used in ReflectiveMethodInvocation userAttributes map for the current joinpoint. + */ + protected static final String JOIN_POINT_KEY = JoinPoint.class.getName(); + + + /** + * Lazily instantiate joinpoint for the current invocation. + * Requires MethodInvocation to be bound with ExposeInvocationInterceptor. + *

Do not use if access is available to the current ReflectiveMethodInvocation + * (in an around advice). + * @return current AspectJ joinpoint, or through an exception if we're not in a + * Spring AOP invocation. + */ + public static JoinPoint currentJoinPoint() { + MethodInvocation mi = ExposeInvocationInterceptor.currentInvocation(); + if (!(mi instanceof ProxyMethodInvocation)) { + throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi); + } + ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi; + JoinPoint jp = (JoinPoint) pmi.getUserAttribute(JOIN_POINT_KEY); + if (jp == null) { + jp = new MethodInvocationProceedingJoinPoint(pmi); + pmi.setUserAttribute(JOIN_POINT_KEY, jp); + } + return jp; + } + + + private final Class declaringClass; + + private final String methodName; + + private final Class[] parameterTypes; + + protected transient Method aspectJAdviceMethod; + + private final AspectJExpressionPointcut pointcut; + + private final AspectInstanceFactory aspectInstanceFactory; + + /** + * The name of the aspect (ref bean) in which this advice was defined + * (used when determining advice precedence so that we can determine + * whether two pieces of advice come from the same aspect). + */ + private String aspectName = ""; + + /** + * The order of declaration of this advice within the aspect. + */ + private int declarationOrder; + + /** + * This will be non-null if the creator of this advice object knows the argument names + * and sets them explicitly. + */ + @Nullable + private String[] argumentNames; + + /** Non-null if after throwing advice binds the thrown value. */ + @Nullable + private String throwingName; + + /** Non-null if after returning advice binds the return value. */ + @Nullable + private String returningName; + + private Class discoveredReturningType = Object.class; + + private Class discoveredThrowingType = Object.class; + + /** + * Index for thisJoinPoint argument (currently only + * supported at index 0 if present at all). + */ + private int joinPointArgumentIndex = -1; + + /** + * Index for thisJoinPointStaticPart argument (currently only + * supported at index 0 if present at all). + */ + private int joinPointStaticPartArgumentIndex = -1; + + @Nullable + private Map argumentBindings; + + private boolean argumentsIntrospected = false; + + @Nullable + private Type discoveredReturningGenericType; + // Note: Unlike return type, no such generic information is needed for the throwing type, + // since Java doesn't allow exception types to be parameterized. + + + /** + * Create a new AbstractAspectJAdvice for the given advice method. + * @param aspectJAdviceMethod the AspectJ-style advice method + * @param pointcut the AspectJ expression pointcut + * @param aspectInstanceFactory the factory for aspect instances + */ + public AbstractAspectJAdvice( + Method aspectJAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aspectInstanceFactory) { + + Assert.notNull(aspectJAdviceMethod, "Advice method must not be null"); + this.declaringClass = aspectJAdviceMethod.getDeclaringClass(); + this.methodName = aspectJAdviceMethod.getName(); + this.parameterTypes = aspectJAdviceMethod.getParameterTypes(); + this.aspectJAdviceMethod = aspectJAdviceMethod; + this.pointcut = pointcut; + this.aspectInstanceFactory = aspectInstanceFactory; + } + + + /** + * Return the AspectJ-style advice method. + */ + public final Method getAspectJAdviceMethod() { + return this.aspectJAdviceMethod; + } + + /** + * Return the AspectJ expression pointcut. + */ + public final AspectJExpressionPointcut getPointcut() { + calculateArgumentBindings(); + return this.pointcut; + } + + /** + * Build a 'safe' pointcut that excludes the AspectJ advice method itself. + * @return a composable pointcut that builds on the original AspectJ expression pointcut + * @see #getPointcut() + */ + public final Pointcut buildSafePointcut() { + Pointcut pc = getPointcut(); + MethodMatcher safeMethodMatcher = MethodMatchers.intersection( + new AdviceExcludingMethodMatcher(this.aspectJAdviceMethod), pc.getMethodMatcher()); + return new ComposablePointcut(pc.getClassFilter(), safeMethodMatcher); + } + + /** + * Return the factory for aspect instances. + */ + public final AspectInstanceFactory getAspectInstanceFactory() { + return this.aspectInstanceFactory; + } + + /** + * Return the ClassLoader for aspect instances. + */ + @Nullable + public final ClassLoader getAspectClassLoader() { + return this.aspectInstanceFactory.getAspectClassLoader(); + } + + @Override + public int getOrder() { + return this.aspectInstanceFactory.getOrder(); + } + + + /** + * Set the name of the aspect (bean) in which the advice was declared. + */ + public void setAspectName(String name) { + this.aspectName = name; + } + + @Override + public String getAspectName() { + return this.aspectName; + } + + /** + * Set the declaration order of this advice within the aspect. + */ + public void setDeclarationOrder(int order) { + this.declarationOrder = order; + } + + @Override + public int getDeclarationOrder() { + return this.declarationOrder; + } + + /** + * Set by creator of this advice object if the argument names are known. + *

This could be for example because they have been explicitly specified in XML, + * or in an advice annotation. + * @param argNames comma delimited list of arg names + */ + public void setArgumentNames(String argNames) { + String[] tokens = StringUtils.commaDelimitedListToStringArray(argNames); + setArgumentNamesFromStringArray(tokens); + } + + public void setArgumentNamesFromStringArray(String... args) { + this.argumentNames = new String[args.length]; + for (int i = 0; i < args.length; i++) { + this.argumentNames[i] = StringUtils.trimWhitespace(args[i]); + if (!isVariableName(this.argumentNames[i])) { + throw new IllegalArgumentException( + "'argumentNames' property of AbstractAspectJAdvice contains an argument name '" + + this.argumentNames[i] + "' that is not a valid Java identifier"); + } + } + if (this.argumentNames != null) { + if (this.aspectJAdviceMethod.getParameterCount() == this.argumentNames.length + 1) { + // May need to add implicit join point arg name... + Class firstArgType = this.aspectJAdviceMethod.getParameterTypes()[0]; + if (firstArgType == JoinPoint.class || + firstArgType == ProceedingJoinPoint.class || + firstArgType == JoinPoint.StaticPart.class) { + String[] oldNames = this.argumentNames; + this.argumentNames = new String[oldNames.length + 1]; + this.argumentNames[0] = "THIS_JOIN_POINT"; + System.arraycopy(oldNames, 0, this.argumentNames, 1, oldNames.length); + } + } + } + } + + public void setReturningName(String name) { + throw new UnsupportedOperationException("Only afterReturning advice can be used to bind a return value"); + } + + /** + * We need to hold the returning name at this level for argument binding calculations, + * this method allows the afterReturning advice subclass to set the name. + */ + protected void setReturningNameNoCheck(String name) { + // name could be a variable or a type... + if (isVariableName(name)) { + this.returningName = name; + } + else { + // assume a type + try { + this.discoveredReturningType = ClassUtils.forName(name, getAspectClassLoader()); + } + catch (Throwable ex) { + throw new IllegalArgumentException("Returning name '" + name + + "' is neither a valid argument name nor the fully-qualified " + + "name of a Java type on the classpath. Root cause: " + ex); + } + } + } + + protected Class getDiscoveredReturningType() { + return this.discoveredReturningType; + } + + @Nullable + protected Type getDiscoveredReturningGenericType() { + return this.discoveredReturningGenericType; + } + + public void setThrowingName(String name) { + throw new UnsupportedOperationException("Only afterThrowing advice can be used to bind a thrown exception"); + } + + /** + * We need to hold the throwing name at this level for argument binding calculations, + * this method allows the afterThrowing advice subclass to set the name. + */ + protected void setThrowingNameNoCheck(String name) { + // name could be a variable or a type... + if (isVariableName(name)) { + this.throwingName = name; + } + else { + // assume a type + try { + this.discoveredThrowingType = ClassUtils.forName(name, getAspectClassLoader()); + } + catch (Throwable ex) { + throw new IllegalArgumentException("Throwing name '" + name + + "' is neither a valid argument name nor the fully-qualified " + + "name of a Java type on the classpath. Root cause: " + ex); + } + } + } + + protected Class getDiscoveredThrowingType() { + return this.discoveredThrowingType; + } + + private static boolean isVariableName(String name) { + return AspectJProxyUtils.isVariableName(name); + } + + + /** + * Do as much work as we can as part of the set-up so that argument binding + * on subsequent advice invocations can be as fast as possible. + *

If the first argument is of type JoinPoint or ProceedingJoinPoint then we + * pass a JoinPoint in that position (ProceedingJoinPoint for around advice). + *

If the first argument is of type {@code JoinPoint.StaticPart} + * then we pass a {@code JoinPoint.StaticPart} in that position. + *

Remaining arguments have to be bound by pointcut evaluation at + * a given join point. We will get back a map from argument name to + * value. We need to calculate which advice parameter needs to be bound + * to which argument name. There are multiple strategies for determining + * this binding, which are arranged in a ChainOfResponsibility. + */ + public final synchronized void calculateArgumentBindings() { + // The simple case... nothing to bind. + if (this.argumentsIntrospected || this.parameterTypes.length == 0) { + return; + } + + int numUnboundArgs = this.parameterTypes.length; + Class[] parameterTypes = this.aspectJAdviceMethod.getParameterTypes(); + if (maybeBindJoinPoint(parameterTypes[0]) || maybeBindProceedingJoinPoint(parameterTypes[0]) || + maybeBindJoinPointStaticPart(parameterTypes[0])) { + numUnboundArgs--; + } + + if (numUnboundArgs > 0) { + // need to bind arguments by name as returned from the pointcut match + bindArgumentsByName(numUnboundArgs); + } + + this.argumentsIntrospected = true; + } + + private boolean maybeBindJoinPoint(Class candidateParameterType) { + if (JoinPoint.class == candidateParameterType) { + this.joinPointArgumentIndex = 0; + return true; + } + else { + return false; + } + } + + private boolean maybeBindProceedingJoinPoint(Class candidateParameterType) { + if (ProceedingJoinPoint.class == candidateParameterType) { + if (!supportsProceedingJoinPoint()) { + throw new IllegalArgumentException("ProceedingJoinPoint is only supported for around advice"); + } + this.joinPointArgumentIndex = 0; + return true; + } + else { + return false; + } + } + + protected boolean supportsProceedingJoinPoint() { + return false; + } + + private boolean maybeBindJoinPointStaticPart(Class candidateParameterType) { + if (JoinPoint.StaticPart.class == candidateParameterType) { + this.joinPointStaticPartArgumentIndex = 0; + return true; + } + else { + return false; + } + } + + private void bindArgumentsByName(int numArgumentsExpectingToBind) { + if (this.argumentNames == null) { + this.argumentNames = createParameterNameDiscoverer().getParameterNames(this.aspectJAdviceMethod); + } + if (this.argumentNames != null) { + // We have been able to determine the arg names. + bindExplicitArguments(numArgumentsExpectingToBind); + } + else { + throw new IllegalStateException("Advice method [" + this.aspectJAdviceMethod.getName() + "] " + + "requires " + numArgumentsExpectingToBind + " arguments to be bound by name, but " + + "the argument names were not specified and could not be discovered."); + } + } + + /** + * Create a ParameterNameDiscoverer to be used for argument binding. + *

The default implementation creates a {@link DefaultParameterNameDiscoverer} + * and adds a specifically configured {@link AspectJAdviceParameterNameDiscoverer}. + */ + protected ParameterNameDiscoverer createParameterNameDiscoverer() { + // We need to discover them, or if that fails, guess, + // and if we can't guess with 100% accuracy, fail. + DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer(); + AspectJAdviceParameterNameDiscoverer adviceParameterNameDiscoverer = + new AspectJAdviceParameterNameDiscoverer(this.pointcut.getExpression()); + adviceParameterNameDiscoverer.setReturningName(this.returningName); + adviceParameterNameDiscoverer.setThrowingName(this.throwingName); + // Last in chain, so if we're called and we fail, that's bad... + adviceParameterNameDiscoverer.setRaiseExceptions(true); + discoverer.addDiscoverer(adviceParameterNameDiscoverer); + return discoverer; + } + + private void bindExplicitArguments(int numArgumentsLeftToBind) { + Assert.state(this.argumentNames != null, "No argument names available"); + this.argumentBindings = new HashMap<>(); + + int numExpectedArgumentNames = this.aspectJAdviceMethod.getParameterCount(); + if (this.argumentNames.length != numExpectedArgumentNames) { + throw new IllegalStateException("Expecting to find " + numExpectedArgumentNames + + " arguments to bind by name in advice, but actually found " + + this.argumentNames.length + " arguments."); + } + + // So we match in number... + int argumentIndexOffset = this.parameterTypes.length - numArgumentsLeftToBind; + for (int i = argumentIndexOffset; i < this.argumentNames.length; i++) { + this.argumentBindings.put(this.argumentNames[i], i); + } + + // Check that returning and throwing were in the argument names list if + // specified, and find the discovered argument types. + if (this.returningName != null) { + if (!this.argumentBindings.containsKey(this.returningName)) { + throw new IllegalStateException("Returning argument name '" + this.returningName + + "' was not bound in advice arguments"); + } + else { + Integer index = this.argumentBindings.get(this.returningName); + this.discoveredReturningType = this.aspectJAdviceMethod.getParameterTypes()[index]; + this.discoveredReturningGenericType = this.aspectJAdviceMethod.getGenericParameterTypes()[index]; + } + } + if (this.throwingName != null) { + if (!this.argumentBindings.containsKey(this.throwingName)) { + throw new IllegalStateException("Throwing argument name '" + this.throwingName + + "' was not bound in advice arguments"); + } + else { + Integer index = this.argumentBindings.get(this.throwingName); + this.discoveredThrowingType = this.aspectJAdviceMethod.getParameterTypes()[index]; + } + } + + // configure the pointcut expression accordingly. + configurePointcutParameters(this.argumentNames, argumentIndexOffset); + } + + /** + * All parameters from argumentIndexOffset onwards are candidates for + * pointcut parameters - but returning and throwing vars are handled differently + * and must be removed from the list if present. + */ + private void configurePointcutParameters(String[] argumentNames, int argumentIndexOffset) { + int numParametersToRemove = argumentIndexOffset; + if (this.returningName != null) { + numParametersToRemove++; + } + if (this.throwingName != null) { + numParametersToRemove++; + } + String[] pointcutParameterNames = new String[argumentNames.length - numParametersToRemove]; + Class[] pointcutParameterTypes = new Class[pointcutParameterNames.length]; + Class[] methodParameterTypes = this.aspectJAdviceMethod.getParameterTypes(); + + int index = 0; + for (int i = 0; i < argumentNames.length; i++) { + if (i < argumentIndexOffset) { + continue; + } + if (argumentNames[i].equals(this.returningName) || + argumentNames[i].equals(this.throwingName)) { + continue; + } + pointcutParameterNames[index] = argumentNames[i]; + pointcutParameterTypes[index] = methodParameterTypes[i]; + index++; + } + + this.pointcut.setParameterNames(pointcutParameterNames); + this.pointcut.setParameterTypes(pointcutParameterTypes); + } + + /** + * Take the arguments at the method execution join point and output a set of arguments + * to the advice method. + * @param jp the current JoinPoint + * @param jpMatch the join point match that matched this execution join point + * @param returnValue the return value from the method execution (may be null) + * @param ex the exception thrown by the method execution (may be null) + * @return the empty array if there are no arguments + */ + protected Object[] argBinding(JoinPoint jp, @Nullable JoinPointMatch jpMatch, + @Nullable Object returnValue, @Nullable Throwable ex) { + + calculateArgumentBindings(); + + // AMC start + Object[] adviceInvocationArgs = new Object[this.parameterTypes.length]; + int numBound = 0; + + if (this.joinPointArgumentIndex != -1) { + adviceInvocationArgs[this.joinPointArgumentIndex] = jp; + numBound++; + } + else if (this.joinPointStaticPartArgumentIndex != -1) { + adviceInvocationArgs[this.joinPointStaticPartArgumentIndex] = jp.getStaticPart(); + numBound++; + } + + if (!CollectionUtils.isEmpty(this.argumentBindings)) { + // binding from pointcut match + if (jpMatch != null) { + PointcutParameter[] parameterBindings = jpMatch.getParameterBindings(); + for (PointcutParameter parameter : parameterBindings) { + String name = parameter.getName(); + Integer index = this.argumentBindings.get(name); + adviceInvocationArgs[index] = parameter.getBinding(); + numBound++; + } + } + // binding from returning clause + if (this.returningName != null) { + Integer index = this.argumentBindings.get(this.returningName); + adviceInvocationArgs[index] = returnValue; + numBound++; + } + // binding from thrown exception + if (this.throwingName != null) { + Integer index = this.argumentBindings.get(this.throwingName); + adviceInvocationArgs[index] = ex; + numBound++; + } + } + + if (numBound != this.parameterTypes.length) { + throw new IllegalStateException("Required to bind " + this.parameterTypes.length + + " arguments, but only bound " + numBound + " (JoinPointMatch " + + (jpMatch == null ? "was NOT" : "WAS") + " bound in invocation)"); + } + + return adviceInvocationArgs; + } + + + /** + * Invoke the advice method. + * @param jpMatch the JoinPointMatch that matched this execution join point + * @param returnValue the return value from the method execution (may be null) + * @param ex the exception thrown by the method execution (may be null) + * @return the invocation result + * @throws Throwable in case of invocation failure + */ + protected Object invokeAdviceMethod( + @Nullable JoinPointMatch jpMatch, @Nullable Object returnValue, @Nullable Throwable ex) + throws Throwable { + + return invokeAdviceMethodWithGivenArgs(argBinding(getJoinPoint(), jpMatch, returnValue, ex)); + } + + // As above, but in this case we are given the join point. + protected Object invokeAdviceMethod(JoinPoint jp, @Nullable JoinPointMatch jpMatch, + @Nullable Object returnValue, @Nullable Throwable t) throws Throwable { + + return invokeAdviceMethodWithGivenArgs(argBinding(jp, jpMatch, returnValue, t)); + } + + protected Object invokeAdviceMethodWithGivenArgs(Object[] args) throws Throwable { + Object[] actualArgs = args; + if (this.aspectJAdviceMethod.getParameterCount() == 0) { + actualArgs = null; + } + try { + ReflectionUtils.makeAccessible(this.aspectJAdviceMethod); + return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs); + } + catch (IllegalArgumentException ex) { + throw new AopInvocationException("Mismatch on arguments to advice method [" + + this.aspectJAdviceMethod + "]; pointcut expression [" + + this.pointcut.getPointcutExpression() + "]", ex); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + + /** + * Overridden in around advice to return proceeding join point. + */ + protected JoinPoint getJoinPoint() { + return currentJoinPoint(); + } + + /** + * Get the current join point match at the join point we are being dispatched on. + */ + @Nullable + protected JoinPointMatch getJoinPointMatch() { + MethodInvocation mi = ExposeInvocationInterceptor.currentInvocation(); + if (!(mi instanceof ProxyMethodInvocation)) { + throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi); + } + return getJoinPointMatch((ProxyMethodInvocation) mi); + } + + // Note: We can't use JoinPointMatch.getClass().getName() as the key, since + // Spring AOP does all the matching at a join point, and then all the invocations. + // Under this scenario, if we just use JoinPointMatch as the key, then + // 'last man wins' which is not what we want at all. + // Using the expression is guaranteed to be safe, since 2 identical expressions + // are guaranteed to bind in exactly the same way. + @Nullable + protected JoinPointMatch getJoinPointMatch(ProxyMethodInvocation pmi) { + String expression = this.pointcut.getExpression(); + return (expression != null ? (JoinPointMatch) pmi.getUserAttribute(expression) : null); + } + + + @Override + public String toString() { + return getClass().getName() + ": advice method [" + this.aspectJAdviceMethod + "]; " + + "aspect name '" + this.aspectName + "'"; + } + + private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException { + inputStream.defaultReadObject(); + try { + this.aspectJAdviceMethod = this.declaringClass.getMethod(this.methodName, this.parameterTypes); + } + catch (NoSuchMethodException ex) { + throw new IllegalStateException("Failed to find advice method on deserialization", ex); + } + } + + + /** + * MethodMatcher that excludes the specified advice method. + * @see AbstractAspectJAdvice#buildSafePointcut() + */ + private static class AdviceExcludingMethodMatcher extends StaticMethodMatcher { + + private final Method adviceMethod; + + public AdviceExcludingMethodMatcher(Method adviceMethod) { + this.adviceMethod = adviceMethod; + } + + @Override + public boolean matches(Method method, Class targetClass) { + return !this.adviceMethod.equals(method); + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof AdviceExcludingMethodMatcher)) { + return false; + } + AdviceExcludingMethodMatcher otherMm = (AdviceExcludingMethodMatcher) other; + return this.adviceMethod.equals(otherMm.adviceMethod); + } + + @Override + public int hashCode() { + return this.adviceMethod.hashCode(); + } + + @Override + public String toString() { + return getClass().getName() + ": " + this.adviceMethod; + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectInstanceFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectInstanceFactory.java new file mode 100644 index 0000000..4ddf630 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectInstanceFactory.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.springframework.core.Ordered; +import org.springframework.lang.Nullable; + +/** + * Interface implemented to provide an instance of an AspectJ aspect. + * Decouples from Spring's bean factory. + * + *

Extends the {@link org.springframework.core.Ordered} interface + * to express an order value for the underlying aspect in a chain. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.beans.factory.BeanFactory#getBean + */ +public interface AspectInstanceFactory extends Ordered { + + /** + * Create an instance of this factory's aspect. + * @return the aspect instance (never {@code null}) + */ + Object getAspectInstance(); + + /** + * Expose the aspect class loader that this factory uses. + * @return the aspect class loader (or {@code null} for the bootstrap loader) + * @see org.springframework.util.ClassUtils#getDefaultClassLoader() + */ + @Nullable + ClassLoader getAspectClassLoader(); + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverer.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverer.java new file mode 100644 index 0000000..ffcea9d --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverer.java @@ -0,0 +1,790 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.weaver.tools.PointcutParser; +import org.aspectj.weaver.tools.PointcutPrimitive; + +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * {@link ParameterNameDiscoverer} implementation that tries to deduce parameter names + * for an advice method from the pointcut expression, returning, and throwing clauses. + * If an unambiguous interpretation is not available, it returns {@code null}. + * + *

This class interprets arguments in the following way: + *

    + *
  1. If the first parameter of the method is of type {@link JoinPoint} + * or {@link ProceedingJoinPoint}, it is assumed to be for passing + * {@code thisJoinPoint} to the advice, and the parameter name will + * be assigned the value {@code "thisJoinPoint"}.
  2. + *
  3. If the first parameter of the method is of type + * {@code JoinPoint.StaticPart}, it is assumed to be for passing + * {@code "thisJoinPointStaticPart"} to the advice, and the parameter name + * will be assigned the value {@code "thisJoinPointStaticPart"}.
  4. + *
  5. If a {@link #setThrowingName(String) throwingName} has been set, and + * there are no unbound arguments of type {@code Throwable+}, then an + * {@link IllegalArgumentException} is raised. If there is more than one + * unbound argument of type {@code Throwable+}, then an + * {@link AmbiguousBindingException} is raised. If there is exactly one + * unbound argument of type {@code Throwable+}, then the corresponding + * parameter name is assigned the value <throwingName>.
  6. + *
  7. If there remain unbound arguments, then the pointcut expression is + * examined. Let {@code a} be the number of annotation-based pointcut + * expressions (@annotation, @this, @target, @args, + * @within, @withincode) that are used in binding form. Usage in + * binding form has itself to be deduced: if the expression inside the + * pointcut is a single string literal that meets Java variable name + * conventions it is assumed to be a variable name. If {@code a} is + * zero we proceed to the next stage. If {@code a} > 1 then an + * {@code AmbiguousBindingException} is raised. If {@code a} == 1, + * and there are no unbound arguments of type {@code Annotation+}, + * then an {@code IllegalArgumentException} is raised. if there is + * exactly one such argument, then the corresponding parameter name is + * assigned the value from the pointcut expression.
  8. + *
  9. If a returningName has been set, and there are no unbound arguments + * then an {@code IllegalArgumentException} is raised. If there is + * more than one unbound argument then an + * {@code AmbiguousBindingException} is raised. If there is exactly + * one unbound argument then the corresponding parameter name is assigned + * the value <returningName>.
  10. + *
  11. If there remain unbound arguments, then the pointcut expression is + * examined once more for {@code this}, {@code target}, and + * {@code args} pointcut expressions used in the binding form (binding + * forms are deduced as described for the annotation based pointcuts). If + * there remains more than one unbound argument of a primitive type (which + * can only be bound in {@code args}) then an + * {@code AmbiguousBindingException} is raised. If there is exactly + * one argument of a primitive type, then if exactly one {@code args} + * bound variable was found, we assign the corresponding parameter name + * the variable name. If there were no {@code args} bound variables + * found an {@code IllegalStateException} is raised. If there are + * multiple {@code args} bound variables, an + * {@code AmbiguousBindingException} is raised. At this point, if + * there remains more than one unbound argument we raise an + * {@code AmbiguousBindingException}. If there are no unbound arguments + * remaining, we are done. If there is exactly one unbound argument + * remaining, and only one candidate variable name unbound from + * {@code this}, {@code target}, or {@code args}, it is + * assigned as the corresponding parameter name. If there are multiple + * possibilities, an {@code AmbiguousBindingException} is raised.
  12. + *
+ * + *

The behavior on raising an {@code IllegalArgumentException} or + * {@code AmbiguousBindingException} is configurable to allow this discoverer + * to be used as part of a chain-of-responsibility. By default the condition will + * be logged and the {@code getParameterNames(..)} method will simply return + * {@code null}. If the {@link #setRaiseExceptions(boolean) raiseExceptions} + * property is set to {@code true}, the conditions will be thrown as + * {@code IllegalArgumentException} and {@code AmbiguousBindingException}, + * respectively. + * + *

Was that perfectly clear? ;) + * + *

Short version: If an unambiguous binding can be deduced, then it is. + * If the advice requirements cannot possibly be satisfied, then {@code null} + * is returned. By setting the {@link #setRaiseExceptions(boolean) raiseExceptions} + * property to {@code true}, descriptive exceptions will be thrown instead of + * returning {@code null} in the case that the parameter names cannot be discovered. + * + * @author Adrian Colyer + * @author Juergen Hoeller + * @since 2.0 + */ +public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscoverer { + + private static final String THIS_JOIN_POINT = "thisJoinPoint"; + private static final String THIS_JOIN_POINT_STATIC_PART = "thisJoinPointStaticPart"; + + // Steps in the binding algorithm... + private static final int STEP_JOIN_POINT_BINDING = 1; + private static final int STEP_THROWING_BINDING = 2; + private static final int STEP_ANNOTATION_BINDING = 3; + private static final int STEP_RETURNING_BINDING = 4; + private static final int STEP_PRIMITIVE_ARGS_BINDING = 5; + private static final int STEP_THIS_TARGET_ARGS_BINDING = 6; + private static final int STEP_REFERENCE_PCUT_BINDING = 7; + private static final int STEP_FINISHED = 8; + + private static final Set singleValuedAnnotationPcds = new HashSet<>(); + private static final Set nonReferencePointcutTokens = new HashSet<>(); + + + static { + singleValuedAnnotationPcds.add("@this"); + singleValuedAnnotationPcds.add("@target"); + singleValuedAnnotationPcds.add("@within"); + singleValuedAnnotationPcds.add("@withincode"); + singleValuedAnnotationPcds.add("@annotation"); + + Set pointcutPrimitives = PointcutParser.getAllSupportedPointcutPrimitives(); + for (PointcutPrimitive primitive : pointcutPrimitives) { + nonReferencePointcutTokens.add(primitive.getName()); + } + nonReferencePointcutTokens.add("&&"); + nonReferencePointcutTokens.add("!"); + nonReferencePointcutTokens.add("||"); + nonReferencePointcutTokens.add("and"); + nonReferencePointcutTokens.add("or"); + nonReferencePointcutTokens.add("not"); + } + + + /** The pointcut expression associated with the advice, as a simple String. */ + @Nullable + private String pointcutExpression; + + private boolean raiseExceptions; + + /** If the advice is afterReturning, and binds the return value, this is the parameter name used. */ + @Nullable + private String returningName; + + /** If the advice is afterThrowing, and binds the thrown value, this is the parameter name used. */ + @Nullable + private String throwingName; + + private Class[] argumentTypes = new Class[0]; + + private String[] parameterNameBindings = new String[0]; + + private int numberOfRemainingUnboundArguments; + + + /** + * Create a new discoverer that attempts to discover parameter names. + * from the given pointcut expression. + */ + public AspectJAdviceParameterNameDiscoverer(@Nullable String pointcutExpression) { + this.pointcutExpression = pointcutExpression; + } + + + /** + * Indicate whether {@link IllegalArgumentException} and {@link AmbiguousBindingException} + * must be thrown as appropriate in the case of failing to deduce advice parameter names. + * @param raiseExceptions {@code true} if exceptions are to be thrown + */ + public void setRaiseExceptions(boolean raiseExceptions) { + this.raiseExceptions = raiseExceptions; + } + + /** + * If {@code afterReturning} advice binds the return value, the + * returning variable name must be specified. + * @param returningName the name of the returning variable + */ + public void setReturningName(@Nullable String returningName) { + this.returningName = returningName; + } + + /** + * If {@code afterThrowing} advice binds the thrown value, the + * throwing variable name must be specified. + * @param throwingName the name of the throwing variable + */ + public void setThrowingName(@Nullable String throwingName) { + this.throwingName = throwingName; + } + + + /** + * Deduce the parameter names for an advice method. + *

See the {@link AspectJAdviceParameterNameDiscoverer class level javadoc} + * for this class for details of the algorithm used. + * @param method the target {@link Method} + * @return the parameter names + */ + @Override + @Nullable + public String[] getParameterNames(Method method) { + this.argumentTypes = method.getParameterTypes(); + this.numberOfRemainingUnboundArguments = this.argumentTypes.length; + this.parameterNameBindings = new String[this.numberOfRemainingUnboundArguments]; + + int minimumNumberUnboundArgs = 0; + if (this.returningName != null) { + minimumNumberUnboundArgs++; + } + if (this.throwingName != null) { + minimumNumberUnboundArgs++; + } + if (this.numberOfRemainingUnboundArguments < minimumNumberUnboundArgs) { + throw new IllegalStateException( + "Not enough arguments in method to satisfy binding of returning and throwing variables"); + } + + try { + int algorithmicStep = STEP_JOIN_POINT_BINDING; + while ((this.numberOfRemainingUnboundArguments > 0) && algorithmicStep < STEP_FINISHED) { + switch (algorithmicStep++) { + case STEP_JOIN_POINT_BINDING: + if (!maybeBindThisJoinPoint()) { + maybeBindThisJoinPointStaticPart(); + } + break; + case STEP_THROWING_BINDING: + maybeBindThrowingVariable(); + break; + case STEP_ANNOTATION_BINDING: + maybeBindAnnotationsFromPointcutExpression(); + break; + case STEP_RETURNING_BINDING: + maybeBindReturningVariable(); + break; + case STEP_PRIMITIVE_ARGS_BINDING: + maybeBindPrimitiveArgsFromPointcutExpression(); + break; + case STEP_THIS_TARGET_ARGS_BINDING: + maybeBindThisOrTargetOrArgsFromPointcutExpression(); + break; + case STEP_REFERENCE_PCUT_BINDING: + maybeBindReferencePointcutParameter(); + break; + default: + throw new IllegalStateException("Unknown algorithmic step: " + (algorithmicStep - 1)); + } + } + } + catch (AmbiguousBindingException | IllegalArgumentException ex) { + if (this.raiseExceptions) { + throw ex; + } + else { + return null; + } + } + + if (this.numberOfRemainingUnboundArguments == 0) { + return this.parameterNameBindings; + } + else { + if (this.raiseExceptions) { + throw new IllegalStateException("Failed to bind all argument names: " + + this.numberOfRemainingUnboundArguments + " argument(s) could not be bound"); + } + else { + // convention for failing is to return null, allowing participation in a chain of responsibility + return null; + } + } + } + + /** + * An advice method can never be a constructor in Spring. + * @return {@code null} + * @throws UnsupportedOperationException if + * {@link #setRaiseExceptions(boolean) raiseExceptions} has been set to {@code true} + */ + @Override + @Nullable + public String[] getParameterNames(Constructor ctor) { + if (this.raiseExceptions) { + throw new UnsupportedOperationException("An advice method can never be a constructor"); + } + else { + // we return null rather than throw an exception so that we behave well + // in a chain-of-responsibility. + return null; + } + } + + + private void bindParameterName(int index, String name) { + this.parameterNameBindings[index] = name; + this.numberOfRemainingUnboundArguments--; + } + + /** + * If the first parameter is of type JoinPoint or ProceedingJoinPoint,bind "thisJoinPoint" as + * parameter name and return true, else return false. + */ + private boolean maybeBindThisJoinPoint() { + if ((this.argumentTypes[0] == JoinPoint.class) || (this.argumentTypes[0] == ProceedingJoinPoint.class)) { + bindParameterName(0, THIS_JOIN_POINT); + return true; + } + else { + return false; + } + } + + private void maybeBindThisJoinPointStaticPart() { + if (this.argumentTypes[0] == JoinPoint.StaticPart.class) { + bindParameterName(0, THIS_JOIN_POINT_STATIC_PART); + } + } + + /** + * If a throwing name was specified and there is exactly one choice remaining + * (argument that is a subtype of Throwable) then bind it. + */ + private void maybeBindThrowingVariable() { + if (this.throwingName == null) { + return; + } + + // So there is binding work to do... + int throwableIndex = -1; + for (int i = 0; i < this.argumentTypes.length; i++) { + if (isUnbound(i) && isSubtypeOf(Throwable.class, i)) { + if (throwableIndex == -1) { + throwableIndex = i; + } + else { + // Second candidate we've found - ambiguous binding + throw new AmbiguousBindingException("Binding of throwing parameter '" + + this.throwingName + "' is ambiguous: could be bound to argument " + + throwableIndex + " or argument " + i); + } + } + } + + if (throwableIndex == -1) { + throw new IllegalStateException("Binding of throwing parameter '" + this.throwingName + + "' could not be completed as no available arguments are a subtype of Throwable"); + } + else { + bindParameterName(throwableIndex, this.throwingName); + } + } + + /** + * If a returning variable was specified and there is only one choice remaining, bind it. + */ + private void maybeBindReturningVariable() { + if (this.numberOfRemainingUnboundArguments == 0) { + throw new IllegalStateException( + "Algorithm assumes that there must be at least one unbound parameter on entry to this method"); + } + + if (this.returningName != null) { + if (this.numberOfRemainingUnboundArguments > 1) { + throw new AmbiguousBindingException("Binding of returning parameter '" + this.returningName + + "' is ambiguous, there are " + this.numberOfRemainingUnboundArguments + " candidates."); + } + + // We're all set... find the unbound parameter, and bind it. + for (int i = 0; i < this.parameterNameBindings.length; i++) { + if (this.parameterNameBindings[i] == null) { + bindParameterName(i, this.returningName); + break; + } + } + } + } + + + /** + * Parse the string pointcut expression looking for: + * @this, @target, @args, @within, @withincode, @annotation. + * If we find one of these pointcut expressions, try and extract a candidate variable + * name (or variable names, in the case of args). + *

Some more support from AspectJ in doing this exercise would be nice... :) + */ + private void maybeBindAnnotationsFromPointcutExpression() { + List varNames = new ArrayList<>(); + String[] tokens = StringUtils.tokenizeToStringArray(this.pointcutExpression, " "); + for (int i = 0; i < tokens.length; i++) { + String toMatch = tokens[i]; + int firstParenIndex = toMatch.indexOf('('); + if (firstParenIndex != -1) { + toMatch = toMatch.substring(0, firstParenIndex); + } + if (singleValuedAnnotationPcds.contains(toMatch)) { + PointcutBody body = getPointcutBody(tokens, i); + i += body.numTokensConsumed; + String varName = maybeExtractVariableName(body.text); + if (varName != null) { + varNames.add(varName); + } + } + else if (tokens[i].startsWith("@args(") || tokens[i].equals("@args")) { + PointcutBody body = getPointcutBody(tokens, i); + i += body.numTokensConsumed; + maybeExtractVariableNamesFromArgs(body.text, varNames); + } + } + + bindAnnotationsFromVarNames(varNames); + } + + /** + * Match the given list of extracted variable names to argument slots. + */ + private void bindAnnotationsFromVarNames(List varNames) { + if (!varNames.isEmpty()) { + // we have work to do... + int numAnnotationSlots = countNumberOfUnboundAnnotationArguments(); + if (numAnnotationSlots > 1) { + throw new AmbiguousBindingException("Found " + varNames.size() + + " potential annotation variable(s), and " + + numAnnotationSlots + " potential argument slots"); + } + else if (numAnnotationSlots == 1) { + if (varNames.size() == 1) { + // it's a match + findAndBind(Annotation.class, varNames.get(0)); + } + else { + // multiple candidate vars, but only one slot + throw new IllegalArgumentException("Found " + varNames.size() + + " candidate annotation binding variables" + + " but only one potential argument binding slot"); + } + } + else { + // no slots so presume those candidate vars were actually type names + } + } + } + + /* + * If the token starts meets Java identifier conventions, it's in. + */ + @Nullable + private String maybeExtractVariableName(@Nullable String candidateToken) { + if (AspectJProxyUtils.isVariableName(candidateToken)) { + return candidateToken; + } + return null; + } + + /** + * Given an args pointcut body (could be {@code args} or {@code at_args}), + * add any candidate variable names to the given list. + */ + private void maybeExtractVariableNamesFromArgs(@Nullable String argsSpec, List varNames) { + if (argsSpec == null) { + return; + } + String[] tokens = StringUtils.tokenizeToStringArray(argsSpec, ","); + for (int i = 0; i < tokens.length; i++) { + tokens[i] = StringUtils.trimWhitespace(tokens[i]); + String varName = maybeExtractVariableName(tokens[i]); + if (varName != null) { + varNames.add(varName); + } + } + } + + /** + * Parse the string pointcut expression looking for this(), target() and args() expressions. + * If we find one, try and extract a candidate variable name and bind it. + */ + private void maybeBindThisOrTargetOrArgsFromPointcutExpression() { + if (this.numberOfRemainingUnboundArguments > 1) { + throw new AmbiguousBindingException("Still " + this.numberOfRemainingUnboundArguments + + " unbound args at this(),target(),args() binding stage, with no way to determine between them"); + } + + List varNames = new ArrayList<>(); + String[] tokens = StringUtils.tokenizeToStringArray(this.pointcutExpression, " "); + for (int i = 0; i < tokens.length; i++) { + if (tokens[i].equals("this") || + tokens[i].startsWith("this(") || + tokens[i].equals("target") || + tokens[i].startsWith("target(")) { + PointcutBody body = getPointcutBody(tokens, i); + i += body.numTokensConsumed; + String varName = maybeExtractVariableName(body.text); + if (varName != null) { + varNames.add(varName); + } + } + else if (tokens[i].equals("args") || tokens[i].startsWith("args(")) { + PointcutBody body = getPointcutBody(tokens, i); + i += body.numTokensConsumed; + List candidateVarNames = new ArrayList<>(); + maybeExtractVariableNamesFromArgs(body.text, candidateVarNames); + // we may have found some var names that were bound in previous primitive args binding step, + // filter them out... + for (String varName : candidateVarNames) { + if (!alreadyBound(varName)) { + varNames.add(varName); + } + } + } + } + + + if (varNames.size() > 1) { + throw new AmbiguousBindingException("Found " + varNames.size() + + " candidate this(), target() or args() variables but only one unbound argument slot"); + } + else if (varNames.size() == 1) { + for (int j = 0; j < this.parameterNameBindings.length; j++) { + if (isUnbound(j)) { + bindParameterName(j, varNames.get(0)); + break; + } + } + } + // else varNames.size must be 0 and we have nothing to bind. + } + + private void maybeBindReferencePointcutParameter() { + if (this.numberOfRemainingUnboundArguments > 1) { + throw new AmbiguousBindingException("Still " + this.numberOfRemainingUnboundArguments + + " unbound args at reference pointcut binding stage, with no way to determine between them"); + } + + List varNames = new ArrayList<>(); + String[] tokens = StringUtils.tokenizeToStringArray(this.pointcutExpression, " "); + for (int i = 0; i < tokens.length; i++) { + String toMatch = tokens[i]; + if (toMatch.startsWith("!")) { + toMatch = toMatch.substring(1); + } + int firstParenIndex = toMatch.indexOf('('); + if (firstParenIndex != -1) { + toMatch = toMatch.substring(0, firstParenIndex); + } + else { + if (tokens.length < i + 2) { + // no "(" and nothing following + continue; + } + else { + String nextToken = tokens[i + 1]; + if (nextToken.charAt(0) != '(') { + // next token is not "(" either, can't be a pc... + continue; + } + } + + } + + // eat the body + PointcutBody body = getPointcutBody(tokens, i); + i += body.numTokensConsumed; + + if (!nonReferencePointcutTokens.contains(toMatch)) { + // then it could be a reference pointcut + String varName = maybeExtractVariableName(body.text); + if (varName != null) { + varNames.add(varName); + } + } + } + + if (varNames.size() > 1) { + throw new AmbiguousBindingException("Found " + varNames.size() + + " candidate reference pointcut variables but only one unbound argument slot"); + } + else if (varNames.size() == 1) { + for (int j = 0; j < this.parameterNameBindings.length; j++) { + if (isUnbound(j)) { + bindParameterName(j, varNames.get(0)); + break; + } + } + } + // else varNames.size must be 0 and we have nothing to bind. + } + + /* + * We've found the start of a binding pointcut at the given index into the + * token array. Now we need to extract the pointcut body and return it. + */ + private PointcutBody getPointcutBody(String[] tokens, int startIndex) { + int numTokensConsumed = 0; + String currentToken = tokens[startIndex]; + int bodyStart = currentToken.indexOf('('); + if (currentToken.charAt(currentToken.length() - 1) == ')') { + // It's an all in one... get the text between the first (and the last) + return new PointcutBody(0, currentToken.substring(bodyStart + 1, currentToken.length() - 1)); + } + else { + StringBuilder sb = new StringBuilder(); + if (bodyStart >= 0 && bodyStart != (currentToken.length() - 1)) { + sb.append(currentToken.substring(bodyStart + 1)); + sb.append(" "); + } + numTokensConsumed++; + int currentIndex = startIndex + numTokensConsumed; + while (currentIndex < tokens.length) { + if (tokens[currentIndex].equals("(")) { + currentIndex++; + continue; + } + + if (tokens[currentIndex].endsWith(")")) { + sb.append(tokens[currentIndex], 0, tokens[currentIndex].length() - 1); + return new PointcutBody(numTokensConsumed, sb.toString().trim()); + } + + String toAppend = tokens[currentIndex]; + if (toAppend.startsWith("(")) { + toAppend = toAppend.substring(1); + } + sb.append(toAppend); + sb.append(" "); + currentIndex++; + numTokensConsumed++; + } + + } + + // We looked and failed... + return new PointcutBody(numTokensConsumed, null); + } + + /** + * Match up args against unbound arguments of primitive types. + */ + private void maybeBindPrimitiveArgsFromPointcutExpression() { + int numUnboundPrimitives = countNumberOfUnboundPrimitiveArguments(); + if (numUnboundPrimitives > 1) { + throw new AmbiguousBindingException("Found '" + numUnboundPrimitives + + "' unbound primitive arguments with no way to distinguish between them."); + } + if (numUnboundPrimitives == 1) { + // Look for arg variable and bind it if we find exactly one... + List varNames = new ArrayList<>(); + String[] tokens = StringUtils.tokenizeToStringArray(this.pointcutExpression, " "); + for (int i = 0; i < tokens.length; i++) { + if (tokens[i].equals("args") || tokens[i].startsWith("args(")) { + PointcutBody body = getPointcutBody(tokens, i); + i += body.numTokensConsumed; + maybeExtractVariableNamesFromArgs(body.text, varNames); + } + } + if (varNames.size() > 1) { + throw new AmbiguousBindingException("Found " + varNames.size() + + " candidate variable names but only one candidate binding slot when matching primitive args"); + } + else if (varNames.size() == 1) { + // 1 primitive arg, and one candidate... + for (int i = 0; i < this.argumentTypes.length; i++) { + if (isUnbound(i) && this.argumentTypes[i].isPrimitive()) { + bindParameterName(i, varNames.get(0)); + break; + } + } + } + } + } + + /* + * Return true if the parameter name binding for the given parameter + * index has not yet been assigned. + */ + private boolean isUnbound(int i) { + return this.parameterNameBindings[i] == null; + } + + private boolean alreadyBound(String varName) { + for (int i = 0; i < this.parameterNameBindings.length; i++) { + if (!isUnbound(i) && varName.equals(this.parameterNameBindings[i])) { + return true; + } + } + return false; + } + + /* + * Return {@code true} if the given argument type is a subclass + * of the given supertype. + */ + private boolean isSubtypeOf(Class supertype, int argumentNumber) { + return supertype.isAssignableFrom(this.argumentTypes[argumentNumber]); + } + + private int countNumberOfUnboundAnnotationArguments() { + int count = 0; + for (int i = 0; i < this.argumentTypes.length; i++) { + if (isUnbound(i) && isSubtypeOf(Annotation.class, i)) { + count++; + } + } + return count; + } + + private int countNumberOfUnboundPrimitiveArguments() { + int count = 0; + for (int i = 0; i < this.argumentTypes.length; i++) { + if (isUnbound(i) && this.argumentTypes[i].isPrimitive()) { + count++; + } + } + return count; + } + + /* + * Find the argument index with the given type, and bind the given + * {@code varName} in that position. + */ + private void findAndBind(Class argumentType, String varName) { + for (int i = 0; i < this.argumentTypes.length; i++) { + if (isUnbound(i) && isSubtypeOf(argumentType, i)) { + bindParameterName(i, varName); + return; + } + } + throw new IllegalStateException("Expected to find an unbound argument of type '" + + argumentType.getName() + "'"); + } + + + /** + * Simple struct to hold the extracted text from a pointcut body, together + * with the number of tokens consumed in extracting it. + */ + private static class PointcutBody { + + private int numTokensConsumed; + + @Nullable + private String text; + + public PointcutBody(int tokens, @Nullable String text) { + this.numTokensConsumed = tokens; + this.text = text; + } + } + + + /** + * Thrown in response to an ambiguous binding being detected when + * trying to resolve a method's parameter names. + */ + @SuppressWarnings("serial") + public static class AmbiguousBindingException extends RuntimeException { + + /** + * Construct a new AmbiguousBindingException with the specified message. + * @param msg the detail message + */ + public AmbiguousBindingException(String msg) { + super(msg); + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAfterAdvice.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAfterAdvice.java new file mode 100644 index 0000000..a8081b4 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAfterAdvice.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import java.io.Serializable; +import java.lang.reflect.Method; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.aop.AfterAdvice; +import org.springframework.lang.Nullable; + +/** + * Spring AOP advice wrapping an AspectJ after advice method. + * + * @author Rod Johnson + * @since 2.0 + */ +@SuppressWarnings("serial") +public class AspectJAfterAdvice extends AbstractAspectJAdvice + implements MethodInterceptor, AfterAdvice, Serializable { + + public AspectJAfterAdvice( + Method aspectJBeforeAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aif) { + + super(aspectJBeforeAdviceMethod, pointcut, aif); + } + + + @Override + @Nullable + public Object invoke(MethodInvocation mi) throws Throwable { + try { + return mi.proceed(); + } + finally { + invokeAdviceMethod(getJoinPointMatch(), null, null); + } + } + + @Override + public boolean isBeforeAdvice() { + return false; + } + + @Override + public boolean isAfterAdvice() { + return true; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAfterReturningAdvice.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAfterReturningAdvice.java new file mode 100644 index 0000000..48cedab --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAfterReturningAdvice.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.lang.reflect.Type; + +import org.springframework.aop.AfterAdvice; +import org.springframework.aop.AfterReturningAdvice; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.TypeUtils; + +/** + * Spring AOP advice wrapping an AspectJ after-returning advice method. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Ramnivas Laddad + * @since 2.0 + */ +@SuppressWarnings("serial") +public class AspectJAfterReturningAdvice extends AbstractAspectJAdvice + implements AfterReturningAdvice, AfterAdvice, Serializable { + + public AspectJAfterReturningAdvice( + Method aspectJBeforeAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aif) { + + super(aspectJBeforeAdviceMethod, pointcut, aif); + } + + + @Override + public boolean isBeforeAdvice() { + return false; + } + + @Override + public boolean isAfterAdvice() { + return true; + } + + @Override + public void setReturningName(String name) { + setReturningNameNoCheck(name); + } + + @Override + public void afterReturning(@Nullable Object returnValue, Method method, Object[] args, @Nullable Object target) throws Throwable { + if (shouldInvokeOnReturnValueOf(method, returnValue)) { + invokeAdviceMethod(getJoinPointMatch(), returnValue, null); + } + } + + + /** + * Following AspectJ semantics, if a returning clause was specified, then the + * advice is only invoked if the returned value is an instance of the given + * returning type and generic type parameters, if any, match the assignment + * rules. If the returning type is Object, the advice is *always* invoked. + * @param returnValue the return value of the target method + * @return whether to invoke the advice method for the given return value + */ + private boolean shouldInvokeOnReturnValueOf(Method method, @Nullable Object returnValue) { + Class type = getDiscoveredReturningType(); + Type genericType = getDiscoveredReturningGenericType(); + // If we aren't dealing with a raw type, check if generic parameters are assignable. + return (matchesReturnValue(type, method, returnValue) && + (genericType == null || genericType == type || + TypeUtils.isAssignable(genericType, method.getGenericReturnType()))); + } + + /** + * Following AspectJ semantics, if a return value is null (or return type is void), + * then the return type of target method should be used to determine whether advice + * is invoked or not. Also, even if the return type is void, if the type of argument + * declared in the advice method is Object, then the advice must still get invoked. + * @param type the type of argument declared in advice method + * @param method the advice method + * @param returnValue the return value of the target method + * @return whether to invoke the advice method for the given return value and type + */ + private boolean matchesReturnValue(Class type, Method method, @Nullable Object returnValue) { + if (returnValue != null) { + return ClassUtils.isAssignableValue(type, returnValue); + } + else if (Object.class == type && void.class == method.getReturnType()) { + return true; + } + else { + return ClassUtils.isAssignable(type, method.getReturnType()); + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAfterThrowingAdvice.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAfterThrowingAdvice.java new file mode 100644 index 0000000..953658d --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAfterThrowingAdvice.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import java.io.Serializable; +import java.lang.reflect.Method; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.aop.AfterAdvice; +import org.springframework.lang.Nullable; + +/** + * Spring AOP advice wrapping an AspectJ after-throwing advice method. + * + * @author Rod Johnson + * @since 2.0 + */ +@SuppressWarnings("serial") +public class AspectJAfterThrowingAdvice extends AbstractAspectJAdvice + implements MethodInterceptor, AfterAdvice, Serializable { + + public AspectJAfterThrowingAdvice( + Method aspectJBeforeAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aif) { + + super(aspectJBeforeAdviceMethod, pointcut, aif); + } + + + @Override + public boolean isBeforeAdvice() { + return false; + } + + @Override + public boolean isAfterAdvice() { + return true; + } + + @Override + public void setThrowingName(String name) { + setThrowingNameNoCheck(name); + } + + @Override + @Nullable + public Object invoke(MethodInvocation mi) throws Throwable { + try { + return mi.proceed(); + } + catch (Throwable ex) { + if (shouldInvokeOnThrowing(ex)) { + invokeAdviceMethod(getJoinPointMatch(), null, ex); + } + throw ex; + } + } + + /** + * In AspectJ semantics, after throwing advice that specifies a throwing clause + * is only invoked if the thrown exception is a subtype of the given throwing type. + */ + private boolean shouldInvokeOnThrowing(Throwable ex) { + return getDiscoveredThrowingType().isAssignableFrom(ex.getClass()); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAopUtils.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAopUtils.java new file mode 100644 index 0000000..0d23180 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAopUtils.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.aopalliance.aop.Advice; + +import org.springframework.aop.Advisor; +import org.springframework.aop.AfterAdvice; +import org.springframework.aop.BeforeAdvice; +import org.springframework.lang.Nullable; + +/** + * Utility methods for dealing with AspectJ advisors. + * + * @author Adrian Colyer + * @author Juergen Hoeller + * @since 2.0 + */ +public abstract class AspectJAopUtils { + + /** + * Return {@code true} if the advisor is a form of before advice. + */ + public static boolean isBeforeAdvice(Advisor anAdvisor) { + AspectJPrecedenceInformation precedenceInfo = getAspectJPrecedenceInformationFor(anAdvisor); + if (precedenceInfo != null) { + return precedenceInfo.isBeforeAdvice(); + } + return (anAdvisor.getAdvice() instanceof BeforeAdvice); + } + + /** + * Return {@code true} if the advisor is a form of after advice. + */ + public static boolean isAfterAdvice(Advisor anAdvisor) { + AspectJPrecedenceInformation precedenceInfo = getAspectJPrecedenceInformationFor(anAdvisor); + if (precedenceInfo != null) { + return precedenceInfo.isAfterAdvice(); + } + return (anAdvisor.getAdvice() instanceof AfterAdvice); + } + + /** + * Return the AspectJPrecedenceInformation provided by this advisor or its advice. + * If neither the advisor nor the advice have precedence information, this method + * will return {@code null}. + */ + @Nullable + public static AspectJPrecedenceInformation getAspectJPrecedenceInformationFor(Advisor anAdvisor) { + if (anAdvisor instanceof AspectJPrecedenceInformation) { + return (AspectJPrecedenceInformation) anAdvisor; + } + Advice advice = anAdvisor.getAdvice(); + if (advice instanceof AspectJPrecedenceInformation) { + return (AspectJPrecedenceInformation) advice; + } + return null; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAroundAdvice.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAroundAdvice.java new file mode 100644 index 0000000..7a70293 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAroundAdvice.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import java.io.Serializable; +import java.lang.reflect.Method; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.weaver.tools.JoinPointMatch; + +import org.springframework.aop.ProxyMethodInvocation; +import org.springframework.lang.Nullable; + +/** + * Spring AOP around advice (MethodInterceptor) that wraps + * an AspectJ advice method. Exposes ProceedingJoinPoint. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + */ +@SuppressWarnings("serial") +public class AspectJAroundAdvice extends AbstractAspectJAdvice implements MethodInterceptor, Serializable { + + public AspectJAroundAdvice( + Method aspectJAroundAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aif) { + + super(aspectJAroundAdviceMethod, pointcut, aif); + } + + + @Override + public boolean isBeforeAdvice() { + return false; + } + + @Override + public boolean isAfterAdvice() { + return false; + } + + @Override + protected boolean supportsProceedingJoinPoint() { + return true; + } + + @Override + @Nullable + public Object invoke(MethodInvocation mi) throws Throwable { + if (!(mi instanceof ProxyMethodInvocation)) { + throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi); + } + ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi; + ProceedingJoinPoint pjp = lazyGetProceedingJoinPoint(pmi); + JoinPointMatch jpm = getJoinPointMatch(pmi); + return invokeAdviceMethod(pjp, jpm, null, null); + } + + /** + * Return the ProceedingJoinPoint for the current invocation, + * instantiating it lazily if it hasn't been bound to the thread already. + * @param rmi the current Spring AOP ReflectiveMethodInvocation, + * which we'll use for attribute binding + * @return the ProceedingJoinPoint to make available to advice methods + */ + protected ProceedingJoinPoint lazyGetProceedingJoinPoint(ProxyMethodInvocation rmi) { + return new MethodInvocationProceedingJoinPoint(rmi); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java new file mode 100644 index 0000000..1cdef31 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java @@ -0,0 +1,718 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.aspectj.weaver.patterns.NamePattern; +import org.aspectj.weaver.reflect.ReflectionWorld.ReflectionWorldException; +import org.aspectj.weaver.reflect.ShadowMatchImpl; +import org.aspectj.weaver.tools.ContextBasedMatcher; +import org.aspectj.weaver.tools.FuzzyBoolean; +import org.aspectj.weaver.tools.JoinPointMatch; +import org.aspectj.weaver.tools.MatchingContext; +import org.aspectj.weaver.tools.PointcutDesignatorHandler; +import org.aspectj.weaver.tools.PointcutExpression; +import org.aspectj.weaver.tools.PointcutParameter; +import org.aspectj.weaver.tools.PointcutParser; +import org.aspectj.weaver.tools.PointcutPrimitive; +import org.aspectj.weaver.tools.ShadowMatch; + +import org.springframework.aop.ClassFilter; +import org.springframework.aop.IntroductionAwareMethodMatcher; +import org.springframework.aop.MethodMatcher; +import org.springframework.aop.ProxyMethodInvocation; +import org.springframework.aop.framework.autoproxy.ProxyCreationContext; +import org.springframework.aop.interceptor.ExposeInvocationInterceptor; +import org.springframework.aop.support.AbstractExpressionPointcut; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Spring {@link org.springframework.aop.Pointcut} implementation + * that uses the AspectJ weaver to evaluate a pointcut expression. + * + *

The pointcut expression value is an AspectJ expression. This can + * reference other pointcuts and use composition and other operations. + * + *

Naturally, as this is to be processed by Spring AOP's proxy-based model, + * only method execution pointcuts are supported. + * + * @author Rob Harrop + * @author Adrian Colyer + * @author Rod Johnson + * @author Juergen Hoeller + * @author Ramnivas Laddad + * @author Dave Syer + * @since 2.0 + */ +@SuppressWarnings("serial") +public class AspectJExpressionPointcut extends AbstractExpressionPointcut + implements ClassFilter, IntroductionAwareMethodMatcher, BeanFactoryAware { + + private static final Set SUPPORTED_PRIMITIVES = new HashSet<>(); + + static { + SUPPORTED_PRIMITIVES.add(PointcutPrimitive.EXECUTION); + SUPPORTED_PRIMITIVES.add(PointcutPrimitive.ARGS); + SUPPORTED_PRIMITIVES.add(PointcutPrimitive.REFERENCE); + SUPPORTED_PRIMITIVES.add(PointcutPrimitive.THIS); + SUPPORTED_PRIMITIVES.add(PointcutPrimitive.TARGET); + SUPPORTED_PRIMITIVES.add(PointcutPrimitive.WITHIN); + SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_ANNOTATION); + SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_WITHIN); + SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_ARGS); + SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_TARGET); + } + + + private static final Log logger = LogFactory.getLog(AspectJExpressionPointcut.class); + + @Nullable + private Class pointcutDeclarationScope; + + private String[] pointcutParameterNames = new String[0]; + + private Class[] pointcutParameterTypes = new Class[0]; + + @Nullable + private BeanFactory beanFactory; + + @Nullable + private transient ClassLoader pointcutClassLoader; + + @Nullable + private transient PointcutExpression pointcutExpression; + + private transient Map shadowMatchCache = new ConcurrentHashMap<>(32); + + + /** + * Create a new default AspectJExpressionPointcut. + */ + public AspectJExpressionPointcut() { + } + + /** + * Create a new AspectJExpressionPointcut with the given settings. + * @param declarationScope the declaration scope for the pointcut + * @param paramNames the parameter names for the pointcut + * @param paramTypes the parameter types for the pointcut + */ + public AspectJExpressionPointcut(Class declarationScope, String[] paramNames, Class[] paramTypes) { + this.pointcutDeclarationScope = declarationScope; + if (paramNames.length != paramTypes.length) { + throw new IllegalStateException( + "Number of pointcut parameter names must match number of pointcut parameter types"); + } + this.pointcutParameterNames = paramNames; + this.pointcutParameterTypes = paramTypes; + } + + + /** + * Set the declaration scope for the pointcut. + */ + public void setPointcutDeclarationScope(Class pointcutDeclarationScope) { + this.pointcutDeclarationScope = pointcutDeclarationScope; + } + + /** + * Set the parameter names for the pointcut. + */ + public void setParameterNames(String... names) { + this.pointcutParameterNames = names; + } + + /** + * Set the parameter types for the pointcut. + */ + public void setParameterTypes(Class... types) { + this.pointcutParameterTypes = types; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + + @Override + public ClassFilter getClassFilter() { + obtainPointcutExpression(); + return this; + } + + @Override + public MethodMatcher getMethodMatcher() { + obtainPointcutExpression(); + return this; + } + + + /** + * Check whether this pointcut is ready to match, + * lazily building the underlying AspectJ pointcut expression. + */ + private PointcutExpression obtainPointcutExpression() { + if (getExpression() == null) { + throw new IllegalStateException("Must set property 'expression' before attempting to match"); + } + if (this.pointcutExpression == null) { + this.pointcutClassLoader = determinePointcutClassLoader(); + this.pointcutExpression = buildPointcutExpression(this.pointcutClassLoader); + } + return this.pointcutExpression; + } + + /** + * Determine the ClassLoader to use for pointcut evaluation. + */ + @Nullable + private ClassLoader determinePointcutClassLoader() { + if (this.beanFactory instanceof ConfigurableBeanFactory) { + return ((ConfigurableBeanFactory) this.beanFactory).getBeanClassLoader(); + } + if (this.pointcutDeclarationScope != null) { + return this.pointcutDeclarationScope.getClassLoader(); + } + return ClassUtils.getDefaultClassLoader(); + } + + /** + * Build the underlying AspectJ pointcut expression. + */ + private PointcutExpression buildPointcutExpression(@Nullable ClassLoader classLoader) { + PointcutParser parser = initializePointcutParser(classLoader); + PointcutParameter[] pointcutParameters = new PointcutParameter[this.pointcutParameterNames.length]; + for (int i = 0; i < pointcutParameters.length; i++) { + pointcutParameters[i] = parser.createPointcutParameter( + this.pointcutParameterNames[i], this.pointcutParameterTypes[i]); + } + return parser.parsePointcutExpression(replaceBooleanOperators(resolveExpression()), + this.pointcutDeclarationScope, pointcutParameters); + } + + private String resolveExpression() { + String expression = getExpression(); + Assert.state(expression != null, "No expression set"); + return expression; + } + + /** + * Initialize the underlying AspectJ pointcut parser. + */ + private PointcutParser initializePointcutParser(@Nullable ClassLoader classLoader) { + PointcutParser parser = PointcutParser + .getPointcutParserSupportingSpecifiedPrimitivesAndUsingSpecifiedClassLoaderForResolution( + SUPPORTED_PRIMITIVES, classLoader); + parser.registerPointcutDesignatorHandler(new BeanPointcutDesignatorHandler()); + return parser; + } + + + /** + * If a pointcut expression has been specified in XML, the user cannot + * write {@code and} as "&&" (though && will work). + * We also allow {@code and} between two pointcut sub-expressions. + *

This method converts back to {@code &&} for the AspectJ pointcut parser. + */ + private String replaceBooleanOperators(String pcExpr) { + String result = StringUtils.replace(pcExpr, " and ", " && "); + result = StringUtils.replace(result, " or ", " || "); + result = StringUtils.replace(result, " not ", " ! "); + return result; + } + + + /** + * Return the underlying AspectJ pointcut expression. + */ + public PointcutExpression getPointcutExpression() { + return obtainPointcutExpression(); + } + + @Override + public boolean matches(Class targetClass) { + PointcutExpression pointcutExpression = obtainPointcutExpression(); + try { + try { + return pointcutExpression.couldMatchJoinPointsInType(targetClass); + } + catch (ReflectionWorldException ex) { + logger.debug("PointcutExpression matching rejected target class - trying fallback expression", ex); + // Actually this is still a "maybe" - treat the pointcut as dynamic if we don't know enough yet + PointcutExpression fallbackExpression = getFallbackPointcutExpression(targetClass); + if (fallbackExpression != null) { + return fallbackExpression.couldMatchJoinPointsInType(targetClass); + } + } + } + catch (Throwable ex) { + logger.debug("PointcutExpression matching rejected target class", ex); + } + return false; + } + + @Override + public boolean matches(Method method, Class targetClass, boolean hasIntroductions) { + obtainPointcutExpression(); + ShadowMatch shadowMatch = getTargetShadowMatch(method, targetClass); + + // Special handling for this, target, @this, @target, @annotation + // in Spring - we can optimize since we know we have exactly this class, + // and there will never be matching subclass at runtime. + if (shadowMatch.alwaysMatches()) { + return true; + } + else if (shadowMatch.neverMatches()) { + return false; + } + else { + // the maybe case + if (hasIntroductions) { + return true; + } + // A match test returned maybe - if there are any subtype sensitive variables + // involved in the test (this, target, at_this, at_target, at_annotation) then + // we say this is not a match as in Spring there will never be a different + // runtime subtype. + RuntimeTestWalker walker = getRuntimeTestWalker(shadowMatch); + return (!walker.testsSubtypeSensitiveVars() || walker.testTargetInstanceOfResidue(targetClass)); + } + } + + @Override + public boolean matches(Method method, Class targetClass) { + return matches(method, targetClass, false); + } + + @Override + public boolean isRuntime() { + return obtainPointcutExpression().mayNeedDynamicTest(); + } + + @Override + public boolean matches(Method method, Class targetClass, Object... args) { + obtainPointcutExpression(); + ShadowMatch shadowMatch = getTargetShadowMatch(method, targetClass); + + // Bind Spring AOP proxy to AspectJ "this" and Spring AOP target to AspectJ target, + // consistent with return of MethodInvocationProceedingJoinPoint + ProxyMethodInvocation pmi = null; + Object targetObject = null; + Object thisObject = null; + try { + MethodInvocation mi = ExposeInvocationInterceptor.currentInvocation(); + targetObject = mi.getThis(); + if (!(mi instanceof ProxyMethodInvocation)) { + throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi); + } + pmi = (ProxyMethodInvocation) mi; + thisObject = pmi.getProxy(); + } + catch (IllegalStateException ex) { + // No current invocation... + if (logger.isDebugEnabled()) { + logger.debug("Could not access current invocation - matching with limited context: " + ex); + } + } + + try { + JoinPointMatch joinPointMatch = shadowMatch.matchesJoinPoint(thisObject, targetObject, args); + + /* + * Do a final check to see if any this(TYPE) kind of residue match. For + * this purpose, we use the original method's (proxy method's) shadow to + * ensure that 'this' is correctly checked against. Without this check, + * we get incorrect match on this(TYPE) where TYPE matches the target + * type but not 'this' (as would be the case of JDK dynamic proxies). + *

See SPR-2979 for the original bug. + */ + if (pmi != null && thisObject != null) { // there is a current invocation + RuntimeTestWalker originalMethodResidueTest = getRuntimeTestWalker(getShadowMatch(method, method)); + if (!originalMethodResidueTest.testThisInstanceOfResidue(thisObject.getClass())) { + return false; + } + if (joinPointMatch.matches()) { + bindParameters(pmi, joinPointMatch); + } + } + + return joinPointMatch.matches(); + } + catch (Throwable ex) { + if (logger.isDebugEnabled()) { + logger.debug("Failed to evaluate join point for arguments " + Arrays.asList(args) + + " - falling back to non-match", ex); + } + return false; + } + } + + @Nullable + protected String getCurrentProxiedBeanName() { + return ProxyCreationContext.getCurrentProxiedBeanName(); + } + + + /** + * Get a new pointcut expression based on a target class's loader rather than the default. + */ + @Nullable + private PointcutExpression getFallbackPointcutExpression(Class targetClass) { + try { + ClassLoader classLoader = targetClass.getClassLoader(); + if (classLoader != null && classLoader != this.pointcutClassLoader) { + return buildPointcutExpression(classLoader); + } + } + catch (Throwable ex) { + logger.debug("Failed to create fallback PointcutExpression", ex); + } + return null; + } + + private RuntimeTestWalker getRuntimeTestWalker(ShadowMatch shadowMatch) { + if (shadowMatch instanceof DefensiveShadowMatch) { + return new RuntimeTestWalker(((DefensiveShadowMatch) shadowMatch).primary); + } + return new RuntimeTestWalker(shadowMatch); + } + + private void bindParameters(ProxyMethodInvocation invocation, JoinPointMatch jpm) { + // Note: Can't use JoinPointMatch.getClass().getName() as the key, since + // Spring AOP does all the matching at a join point, and then all the invocations + // under this scenario, if we just use JoinPointMatch as the key, then + // 'last man wins' which is not what we want at all. + // Using the expression is guaranteed to be safe, since 2 identical expressions + // are guaranteed to bind in exactly the same way. + invocation.setUserAttribute(resolveExpression(), jpm); + } + + private ShadowMatch getTargetShadowMatch(Method method, Class targetClass) { + Method targetMethod = AopUtils.getMostSpecificMethod(method, targetClass); + if (targetMethod.getDeclaringClass().isInterface()) { + // Try to build the most specific interface possible for inherited methods to be + // considered for sub-interface matches as well, in particular for proxy classes. + // Note: AspectJ is only going to take Method.getDeclaringClass() into account. + Set> ifcs = ClassUtils.getAllInterfacesForClassAsSet(targetClass); + if (ifcs.size() > 1) { + try { + Class compositeInterface = ClassUtils.createCompositeInterface( + ClassUtils.toClassArray(ifcs), targetClass.getClassLoader()); + targetMethod = ClassUtils.getMostSpecificMethod(targetMethod, compositeInterface); + } + catch (IllegalArgumentException ex) { + // Implemented interfaces probably expose conflicting method signatures... + // Proceed with original target method. + } + } + } + return getShadowMatch(targetMethod, method); + } + + private ShadowMatch getShadowMatch(Method targetMethod, Method originalMethod) { + // Avoid lock contention for known Methods through concurrent access... + ShadowMatch shadowMatch = this.shadowMatchCache.get(targetMethod); + if (shadowMatch == null) { + synchronized (this.shadowMatchCache) { + // Not found - now check again with full lock... + PointcutExpression fallbackExpression = null; + shadowMatch = this.shadowMatchCache.get(targetMethod); + if (shadowMatch == null) { + Method methodToMatch = targetMethod; + try { + try { + shadowMatch = obtainPointcutExpression().matchesMethodExecution(methodToMatch); + } + catch (ReflectionWorldException ex) { + // Failed to introspect target method, probably because it has been loaded + // in a special ClassLoader. Let's try the declaring ClassLoader instead... + try { + fallbackExpression = getFallbackPointcutExpression(methodToMatch.getDeclaringClass()); + if (fallbackExpression != null) { + shadowMatch = fallbackExpression.matchesMethodExecution(methodToMatch); + } + } + catch (ReflectionWorldException ex2) { + fallbackExpression = null; + } + } + if (targetMethod != originalMethod && (shadowMatch == null || + (shadowMatch.neverMatches() && Proxy.isProxyClass(targetMethod.getDeclaringClass())))) { + // Fall back to the plain original method in case of no resolvable match or a + // negative match on a proxy class (which doesn't carry any annotations on its + // redeclared methods). + methodToMatch = originalMethod; + try { + shadowMatch = obtainPointcutExpression().matchesMethodExecution(methodToMatch); + } + catch (ReflectionWorldException ex) { + // Could neither introspect the target class nor the proxy class -> + // let's try the original method's declaring class before we give up... + try { + fallbackExpression = getFallbackPointcutExpression(methodToMatch.getDeclaringClass()); + if (fallbackExpression != null) { + shadowMatch = fallbackExpression.matchesMethodExecution(methodToMatch); + } + } + catch (ReflectionWorldException ex2) { + fallbackExpression = null; + } + } + } + } + catch (Throwable ex) { + // Possibly AspectJ 1.8.10 encountering an invalid signature + logger.debug("PointcutExpression matching rejected target method", ex); + fallbackExpression = null; + } + if (shadowMatch == null) { + shadowMatch = new ShadowMatchImpl(org.aspectj.util.FuzzyBoolean.NO, null, null, null); + } + else if (shadowMatch.maybeMatches() && fallbackExpression != null) { + shadowMatch = new DefensiveShadowMatch(shadowMatch, + fallbackExpression.matchesMethodExecution(methodToMatch)); + } + this.shadowMatchCache.put(targetMethod, shadowMatch); + } + } + } + return shadowMatch; + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof AspectJExpressionPointcut)) { + return false; + } + AspectJExpressionPointcut otherPc = (AspectJExpressionPointcut) other; + return ObjectUtils.nullSafeEquals(this.getExpression(), otherPc.getExpression()) && + ObjectUtils.nullSafeEquals(this.pointcutDeclarationScope, otherPc.pointcutDeclarationScope) && + ObjectUtils.nullSafeEquals(this.pointcutParameterNames, otherPc.pointcutParameterNames) && + ObjectUtils.nullSafeEquals(this.pointcutParameterTypes, otherPc.pointcutParameterTypes); + } + + @Override + public int hashCode() { + int hashCode = ObjectUtils.nullSafeHashCode(this.getExpression()); + hashCode = 31 * hashCode + ObjectUtils.nullSafeHashCode(this.pointcutDeclarationScope); + hashCode = 31 * hashCode + ObjectUtils.nullSafeHashCode(this.pointcutParameterNames); + hashCode = 31 * hashCode + ObjectUtils.nullSafeHashCode(this.pointcutParameterTypes); + return hashCode; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("AspectJExpressionPointcut: ("); + for (int i = 0; i < this.pointcutParameterTypes.length; i++) { + sb.append(this.pointcutParameterTypes[i].getName()); + sb.append(" "); + sb.append(this.pointcutParameterNames[i]); + if ((i+1) < this.pointcutParameterTypes.length) { + sb.append(", "); + } + } + sb.append(") "); + if (getExpression() != null) { + sb.append(getExpression()); + } + else { + sb.append(""); + } + return sb.toString(); + } + + //--------------------------------------------------------------------- + // Serialization support + //--------------------------------------------------------------------- + + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + // Rely on default serialization, just initialize state after deserialization. + ois.defaultReadObject(); + + // Initialize transient fields. + // pointcutExpression will be initialized lazily by checkReadyToMatch() + this.shadowMatchCache = new ConcurrentHashMap<>(32); + } + + + /** + * Handler for the Spring-specific {@code bean()} pointcut designator + * extension to AspectJ. + *

This handler must be added to each pointcut object that needs to + * handle the {@code bean()} PCD. Matching context is obtained + * automatically by examining a thread local variable and therefore a matching + * context need not be set on the pointcut. + */ + private class BeanPointcutDesignatorHandler implements PointcutDesignatorHandler { + + private static final String BEAN_DESIGNATOR_NAME = "bean"; + + @Override + public String getDesignatorName() { + return BEAN_DESIGNATOR_NAME; + } + + @Override + public ContextBasedMatcher parse(String expression) { + return new BeanContextMatcher(expression); + } + } + + + /** + * Matcher class for the BeanNamePointcutDesignatorHandler. + *

Dynamic match tests for this matcher always return true, + * since the matching decision is made at the proxy creation time. + * For static match tests, this matcher abstains to allow the overall + * pointcut to match even when negation is used with the bean() pointcut. + */ + private class BeanContextMatcher implements ContextBasedMatcher { + + private final NamePattern expressionPattern; + + public BeanContextMatcher(String expression) { + this.expressionPattern = new NamePattern(expression); + } + + @Override + @SuppressWarnings("rawtypes") + @Deprecated + public boolean couldMatchJoinPointsInType(Class someClass) { + return (contextMatch(someClass) == FuzzyBoolean.YES); + } + + @Override + @SuppressWarnings("rawtypes") + @Deprecated + public boolean couldMatchJoinPointsInType(Class someClass, MatchingContext context) { + return (contextMatch(someClass) == FuzzyBoolean.YES); + } + + @Override + public boolean matchesDynamically(MatchingContext context) { + return true; + } + + @Override + public FuzzyBoolean matchesStatically(MatchingContext context) { + return contextMatch(null); + } + + @Override + public boolean mayNeedDynamicTest() { + return false; + } + + private FuzzyBoolean contextMatch(@Nullable Class targetType) { + String advisedBeanName = getCurrentProxiedBeanName(); + if (advisedBeanName == null) { // no proxy creation in progress + // abstain; can't return YES, since that will make pointcut with negation fail + return FuzzyBoolean.MAYBE; + } + if (BeanFactoryUtils.isGeneratedBeanName(advisedBeanName)) { + return FuzzyBoolean.NO; + } + if (targetType != null) { + boolean isFactory = FactoryBean.class.isAssignableFrom(targetType); + return FuzzyBoolean.fromBoolean( + matchesBean(isFactory ? BeanFactory.FACTORY_BEAN_PREFIX + advisedBeanName : advisedBeanName)); + } + else { + return FuzzyBoolean.fromBoolean(matchesBean(advisedBeanName) || + matchesBean(BeanFactory.FACTORY_BEAN_PREFIX + advisedBeanName)); + } + } + + private boolean matchesBean(String advisedBeanName) { + return BeanFactoryAnnotationUtils.isQualifierMatch( + this.expressionPattern::matches, advisedBeanName, beanFactory); + } + } + + + private static class DefensiveShadowMatch implements ShadowMatch { + + private final ShadowMatch primary; + + private final ShadowMatch other; + + public DefensiveShadowMatch(ShadowMatch primary, ShadowMatch other) { + this.primary = primary; + this.other = other; + } + + @Override + public boolean alwaysMatches() { + return this.primary.alwaysMatches(); + } + + @Override + public boolean maybeMatches() { + return this.primary.maybeMatches(); + } + + @Override + public boolean neverMatches() { + return this.primary.neverMatches(); + } + + @Override + public JoinPointMatch matchesJoinPoint(Object thisObject, Object targetObject, Object[] args) { + try { + return this.primary.matchesJoinPoint(thisObject, targetObject, args); + } + catch (ReflectionWorldException ex) { + return this.other.matchesJoinPoint(thisObject, targetObject, args); + } + } + + @Override + public void setMatchingContext(MatchingContext aMatchContext) { + this.primary.setMatchingContext(aMatchContext); + this.other.setMatchingContext(aMatchContext); + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcutAdvisor.java new file mode 100644 index 0000000..9f4b1e9 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcutAdvisor.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.springframework.aop.Pointcut; +import org.springframework.aop.support.AbstractGenericPointcutAdvisor; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.lang.Nullable; + +/** + * Spring AOP Advisor that can be used for any AspectJ pointcut expression. + * + * @author Rob Harrop + * @since 2.0 + */ +@SuppressWarnings("serial") +public class AspectJExpressionPointcutAdvisor extends AbstractGenericPointcutAdvisor implements BeanFactoryAware { + + private final AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); + + + public void setExpression(@Nullable String expression) { + this.pointcut.setExpression(expression); + } + + @Nullable + public String getExpression() { + return this.pointcut.getExpression(); + } + + public void setLocation(@Nullable String location) { + this.pointcut.setLocation(location); + } + + @Nullable + public String getLocation() { + return this.pointcut.getLocation(); + } + + public void setParameterNames(String... names) { + this.pointcut.setParameterNames(names); + } + + public void setParameterTypes(Class... types) { + this.pointcut.setParameterTypes(types); + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.pointcut.setBeanFactory(beanFactory); + } + + @Override + public Pointcut getPointcut() { + return this.pointcut; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJMethodBeforeAdvice.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJMethodBeforeAdvice.java new file mode 100644 index 0000000..207291c --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJMethodBeforeAdvice.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import java.io.Serializable; +import java.lang.reflect.Method; + +import org.springframework.aop.MethodBeforeAdvice; +import org.springframework.lang.Nullable; + +/** + * Spring AOP advice that wraps an AspectJ before method. + * + * @author Rod Johnson + * @author Adrian Colyer + * @since 2.0 + */ +@SuppressWarnings("serial") +public class AspectJMethodBeforeAdvice extends AbstractAspectJAdvice implements MethodBeforeAdvice, Serializable { + + public AspectJMethodBeforeAdvice( + Method aspectJBeforeAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aif) { + + super(aspectJBeforeAdviceMethod, pointcut, aif); + } + + + @Override + public void before(Method method, Object[] args, @Nullable Object target) throws Throwable { + invokeAdviceMethod(getJoinPointMatch(), null, null); + } + + @Override + public boolean isBeforeAdvice() { + return true; + } + + @Override + public boolean isAfterAdvice() { + return false; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJPointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJPointcutAdvisor.java new file mode 100644 index 0000000..b1a1217 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJPointcutAdvisor.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.aopalliance.aop.Advice; + +import org.springframework.aop.Pointcut; +import org.springframework.aop.PointcutAdvisor; +import org.springframework.core.Ordered; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * AspectJPointcutAdvisor that adapts an {@link AbstractAspectJAdvice} + * to the {@link org.springframework.aop.PointcutAdvisor} interface. + * + * @author Adrian Colyer + * @author Juergen Hoeller + * @since 2.0 + */ +public class AspectJPointcutAdvisor implements PointcutAdvisor, Ordered { + + private final AbstractAspectJAdvice advice; + + private final Pointcut pointcut; + + @Nullable + private Integer order; + + + /** + * Create a new AspectJPointcutAdvisor for the given advice. + * @param advice the AbstractAspectJAdvice to wrap + */ + public AspectJPointcutAdvisor(AbstractAspectJAdvice advice) { + Assert.notNull(advice, "Advice must not be null"); + this.advice = advice; + this.pointcut = advice.buildSafePointcut(); + } + + + public void setOrder(int order) { + this.order = order; + } + + @Override + public int getOrder() { + if (this.order != null) { + return this.order; + } + else { + return this.advice.getOrder(); + } + } + + @Override + public boolean isPerInstance() { + return true; + } + + @Override + public Advice getAdvice() { + return this.advice; + } + + @Override + public Pointcut getPointcut() { + return this.pointcut; + } + + /** + * Return the name of the aspect (bean) in which the advice was declared. + * @since 4.3.15 + * @see AbstractAspectJAdvice#getAspectName() + */ + public String getAspectName() { + return this.advice.getAspectName(); + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof AspectJPointcutAdvisor)) { + return false; + } + AspectJPointcutAdvisor otherAdvisor = (AspectJPointcutAdvisor) other; + return this.advice.equals(otherAdvisor.advice); + } + + @Override + public int hashCode() { + return AspectJPointcutAdvisor.class.hashCode() * 29 + this.advice.hashCode(); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJPrecedenceInformation.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJPrecedenceInformation.java new file mode 100644 index 0000000..88c9468 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJPrecedenceInformation.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.springframework.core.Ordered; + +/** + * Interface to be implemented by types that can supply the information + * needed to sort advice/advisors by AspectJ's precedence rules. + * + * @author Adrian Colyer + * @since 2.0 + * @see org.springframework.aop.aspectj.autoproxy.AspectJPrecedenceComparator + */ +public interface AspectJPrecedenceInformation extends Ordered { + + // Implementation note: + // We need the level of indirection this interface provides as otherwise the + // AspectJPrecedenceComparator must ask an Advisor for its Advice in all cases + // in order to sort advisors. This causes problems with the + // InstantiationModelAwarePointcutAdvisor which needs to delay creating + // its advice for aspects with non-singleton instantiation models. + + /** + * Return the name of the aspect (bean) in which the advice was declared. + */ + String getAspectName(); + + /** + * Return the declaration order of the advice member within the aspect. + */ + int getDeclarationOrder(); + + /** + * Return whether this is a before advice. + */ + boolean isBeforeAdvice(); + + /** + * Return whether this is an after advice. + */ + boolean isAfterAdvice(); + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJProxyUtils.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJProxyUtils.java new file mode 100644 index 0000000..e161007 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJProxyUtils.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import java.util.List; + +import org.springframework.aop.Advisor; +import org.springframework.aop.PointcutAdvisor; +import org.springframework.aop.interceptor.ExposeInvocationInterceptor; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Utility methods for working with AspectJ proxies. + * + * @author Rod Johnson + * @author Ramnivas Laddad + * @author Juergen Hoeller + * @since 2.0 + */ +public abstract class AspectJProxyUtils { + + /** + * Add special advisors if necessary to work with a proxy chain that contains AspectJ advisors: + * concretely, {@link ExposeInvocationInterceptor} at the beginning of the list. + *

This will expose the current Spring AOP invocation (necessary for some AspectJ pointcut + * matching) and make available the current AspectJ JoinPoint. The call will have no effect + * if there are no AspectJ advisors in the advisor chain. + * @param advisors the advisors available + * @return {@code true} if an {@link ExposeInvocationInterceptor} was added to the list, + * otherwise {@code false} + */ + public static boolean makeAdvisorChainAspectJCapableIfNecessary(List advisors) { + // Don't add advisors to an empty list; may indicate that proxying is just not required + if (!advisors.isEmpty()) { + boolean foundAspectJAdvice = false; + for (Advisor advisor : advisors) { + // Be careful not to get the Advice without a guard, as this might eagerly + // instantiate a non-singleton AspectJ aspect... + if (isAspectJAdvice(advisor)) { + foundAspectJAdvice = true; + break; + } + } + if (foundAspectJAdvice && !advisors.contains(ExposeInvocationInterceptor.ADVISOR)) { + advisors.add(0, ExposeInvocationInterceptor.ADVISOR); + return true; + } + } + return false; + } + + /** + * Determine whether the given Advisor contains an AspectJ advice. + * @param advisor the Advisor to check + */ + private static boolean isAspectJAdvice(Advisor advisor) { + return (advisor instanceof InstantiationModelAwarePointcutAdvisor || + advisor.getAdvice() instanceof AbstractAspectJAdvice || + (advisor instanceof PointcutAdvisor && + ((PointcutAdvisor) advisor).getPointcut() instanceof AspectJExpressionPointcut)); + } + + static boolean isVariableName(@Nullable String name) { + if (!StringUtils.hasLength(name)) { + return false; + } + if (!Character.isJavaIdentifierStart(name.charAt(0))) { + return false; + } + for (int i = 1; i < name.length(); i++) { + if (!Character.isJavaIdentifierPart(name.charAt(i))) { + return false; + } + } + return true; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJWeaverMessageHandler.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJWeaverMessageHandler.java new file mode 100644 index 0000000..ed837d8 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJWeaverMessageHandler.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.aspectj.bridge.AbortException; +import org.aspectj.bridge.IMessage; +import org.aspectj.bridge.IMessage.Kind; +import org.aspectj.bridge.IMessageHandler; + +/** + * Implementation of AspectJ's {@link IMessageHandler} interface that + * routes AspectJ weaving messages through the same logging system as the + * regular Spring messages. + * + *

Pass the option... + * + *

-XmessageHandlerClass:org.springframework.aop.aspectj.AspectJWeaverMessageHandler + * + *

to the weaver; for example, specifying the following in a + * "{@code META-INF/aop.xml} file: + * + *

<weaver options="..."/> + * + * @author Adrian Colyer + * @author Juergen Hoeller + * @since 2.0 + */ +public class AspectJWeaverMessageHandler implements IMessageHandler { + + private static final String AJ_ID = "[AspectJ] "; + + private static final Log logger = LogFactory.getLog("AspectJ Weaver"); + + + @Override + public boolean handleMessage(IMessage message) throws AbortException { + Kind messageKind = message.getKind(); + if (messageKind == IMessage.DEBUG) { + if (logger.isDebugEnabled()) { + logger.debug(makeMessageFor(message)); + return true; + } + } + else if (messageKind == IMessage.INFO || messageKind == IMessage.WEAVEINFO) { + if (logger.isInfoEnabled()) { + logger.info(makeMessageFor(message)); + return true; + } + } + else if (messageKind == IMessage.WARNING) { + if (logger.isWarnEnabled()) { + logger.warn(makeMessageFor(message)); + return true; + } + } + else if (messageKind == IMessage.ERROR) { + if (logger.isErrorEnabled()) { + logger.error(makeMessageFor(message)); + return true; + } + } + else if (messageKind == IMessage.ABORT) { + if (logger.isFatalEnabled()) { + logger.fatal(makeMessageFor(message)); + return true; + } + } + return false; + } + + private String makeMessageFor(IMessage aMessage) { + return AJ_ID + aMessage.getMessage(); + } + + @Override + public boolean isIgnoring(Kind messageKind) { + // We want to see everything, and allow configuration of log levels dynamically. + return false; + } + + @Override + public void dontIgnore(Kind messageKind) { + // We weren't ignoring anything anyway... + } + + @Override + public void ignore(Kind kind) { + // We weren't ignoring anything anyway... + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/DeclareParentsAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/DeclareParentsAdvisor.java new file mode 100644 index 0000000..3a4f899 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/DeclareParentsAdvisor.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.aopalliance.aop.Advice; + +import org.springframework.aop.ClassFilter; +import org.springframework.aop.IntroductionAdvisor; +import org.springframework.aop.IntroductionInterceptor; +import org.springframework.aop.support.ClassFilters; +import org.springframework.aop.support.DelegatePerTargetObjectIntroductionInterceptor; +import org.springframework.aop.support.DelegatingIntroductionInterceptor; + +/** + * Introduction advisor delegating to the given object. + * Implements AspectJ annotation-style behavior for the DeclareParents annotation. + * + * @author Rod Johnson + * @author Ramnivas Laddad + * @since 2.0 + */ +public class DeclareParentsAdvisor implements IntroductionAdvisor { + + private final Advice advice; + + private final Class introducedInterface; + + private final ClassFilter typePatternClassFilter; + + + /** + * Create a new advisor for this DeclareParents field. + * @param interfaceType static field defining the introduction + * @param typePattern type pattern the introduction is restricted to + * @param defaultImpl the default implementation class + */ + public DeclareParentsAdvisor(Class interfaceType, String typePattern, Class defaultImpl) { + this(interfaceType, typePattern, + new DelegatePerTargetObjectIntroductionInterceptor(defaultImpl, interfaceType)); + } + + /** + * Create a new advisor for this DeclareParents field. + * @param interfaceType static field defining the introduction + * @param typePattern type pattern the introduction is restricted to + * @param delegateRef the delegate implementation object + */ + public DeclareParentsAdvisor(Class interfaceType, String typePattern, Object delegateRef) { + this(interfaceType, typePattern, new DelegatingIntroductionInterceptor(delegateRef)); + } + + /** + * Private constructor to share common code between impl-based delegate and reference-based delegate + * (cannot use method such as init() to share common code, due the use of final fields). + * @param interfaceType static field defining the introduction + * @param typePattern type pattern the introduction is restricted to + * @param interceptor the delegation advice as {@link IntroductionInterceptor} + */ + private DeclareParentsAdvisor(Class interfaceType, String typePattern, IntroductionInterceptor interceptor) { + this.advice = interceptor; + this.introducedInterface = interfaceType; + + // Excludes methods implemented. + ClassFilter typePatternFilter = new TypePatternClassFilter(typePattern); + ClassFilter exclusion = (clazz -> !this.introducedInterface.isAssignableFrom(clazz)); + this.typePatternClassFilter = ClassFilters.intersection(typePatternFilter, exclusion); + } + + + @Override + public ClassFilter getClassFilter() { + return this.typePatternClassFilter; + } + + @Override + public void validateInterfaces() throws IllegalArgumentException { + // Do nothing + } + + @Override + public boolean isPerInstance() { + return true; + } + + @Override + public Advice getAdvice() { + return this.advice; + } + + @Override + public Class[] getInterfaces() { + return new Class[] {this.introducedInterface}; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/InstantiationModelAwarePointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/InstantiationModelAwarePointcutAdvisor.java new file mode 100644 index 0000000..2f3ddab --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/InstantiationModelAwarePointcutAdvisor.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2006 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.springframework.aop.PointcutAdvisor; + +/** + * Interface to be implemented by Spring AOP Advisors wrapping AspectJ + * aspects that may have a lazy initialization strategy. For example, + * a perThis instantiation model would mean lazy initialization of the advice. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + */ +public interface InstantiationModelAwarePointcutAdvisor extends PointcutAdvisor { + + /** + * Return whether this advisor is lazily initializing its underlying advice. + */ + boolean isLazy(); + + /** + * Return whether this advisor has already instantiated its advice. + */ + boolean isAdviceInstantiated(); + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/MethodInvocationProceedingJoinPoint.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/MethodInvocationProceedingJoinPoint.java new file mode 100644 index 0000000..d25ec2e --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/MethodInvocationProceedingJoinPoint.java @@ -0,0 +1,334 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.Signature; +import org.aspectj.lang.reflect.MethodSignature; +import org.aspectj.lang.reflect.SourceLocation; +import org.aspectj.runtime.internal.AroundClosure; + +import org.springframework.aop.ProxyMethodInvocation; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * An implementation of the AspectJ {@link ProceedingJoinPoint} interface + * wrapping an AOP Alliance {@link org.aopalliance.intercept.MethodInvocation}. + * + *

Note: The {@code getThis()} method returns the current Spring AOP proxy. + * The {@code getTarget()} method returns the current Spring AOP target (which may be + * {@code null} if there is no target instance) as a plain POJO without any advice. + * If you want to call the object and have the advice take effect, use {@code getThis()}. + * A common example is casting the object to an introduced interface in the implementation of + * an introduction. There is no such distinction between target and proxy in AspectJ itself. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Adrian Colyer + * @author Ramnivas Laddad + * @since 2.0 + */ +public class MethodInvocationProceedingJoinPoint implements ProceedingJoinPoint, JoinPoint.StaticPart { + + private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + + private final ProxyMethodInvocation methodInvocation; + + @Nullable + private Object[] args; + + /** Lazily initialized signature object. */ + @Nullable + private Signature signature; + + /** Lazily initialized source location object. */ + @Nullable + private SourceLocation sourceLocation; + + + /** + * Create a new MethodInvocationProceedingJoinPoint, wrapping the given + * Spring ProxyMethodInvocation object. + * @param methodInvocation the Spring ProxyMethodInvocation object + */ + public MethodInvocationProceedingJoinPoint(ProxyMethodInvocation methodInvocation) { + Assert.notNull(methodInvocation, "MethodInvocation must not be null"); + this.methodInvocation = methodInvocation; + } + + + @Override + public void set$AroundClosure(AroundClosure aroundClosure) { + throw new UnsupportedOperationException(); + } + + @Override + @Nullable + public Object proceed() throws Throwable { + return this.methodInvocation.invocableClone().proceed(); + } + + @Override + @Nullable + public Object proceed(Object[] arguments) throws Throwable { + Assert.notNull(arguments, "Argument array passed to proceed cannot be null"); + if (arguments.length != this.methodInvocation.getArguments().length) { + throw new IllegalArgumentException("Expecting " + + this.methodInvocation.getArguments().length + " arguments to proceed, " + + "but was passed " + arguments.length + " arguments"); + } + this.methodInvocation.setArguments(arguments); + return this.methodInvocation.invocableClone(arguments).proceed(); + } + + /** + * Returns the Spring AOP proxy. Cannot be {@code null}. + */ + @Override + public Object getThis() { + return this.methodInvocation.getProxy(); + } + + /** + * Returns the Spring AOP target. May be {@code null} if there is no target. + */ + @Override + @Nullable + public Object getTarget() { + return this.methodInvocation.getThis(); + } + + @Override + public Object[] getArgs() { + if (this.args == null) { + this.args = this.methodInvocation.getArguments().clone(); + } + return this.args; + } + + @Override + public Signature getSignature() { + if (this.signature == null) { + this.signature = new MethodSignatureImpl(); + } + return this.signature; + } + + @Override + public SourceLocation getSourceLocation() { + if (this.sourceLocation == null) { + this.sourceLocation = new SourceLocationImpl(); + } + return this.sourceLocation; + } + + @Override + public String getKind() { + return ProceedingJoinPoint.METHOD_EXECUTION; + } + + @Override + public int getId() { + // TODO: It's just an adapter but returning 0 might still have side effects... + return 0; + } + + @Override + public JoinPoint.StaticPart getStaticPart() { + return this; + } + + @Override + public String toShortString() { + return "execution(" + getSignature().toShortString() + ")"; + } + + @Override + public String toLongString() { + return "execution(" + getSignature().toLongString() + ")"; + } + + @Override + public String toString() { + return "execution(" + getSignature().toString() + ")"; + } + + + /** + * Lazily initialized MethodSignature. + */ + private class MethodSignatureImpl implements MethodSignature { + + @Nullable + private volatile String[] parameterNames; + + @Override + public String getName() { + return methodInvocation.getMethod().getName(); + } + + @Override + public int getModifiers() { + return methodInvocation.getMethod().getModifiers(); + } + + @Override + public Class getDeclaringType() { + return methodInvocation.getMethod().getDeclaringClass(); + } + + @Override + public String getDeclaringTypeName() { + return methodInvocation.getMethod().getDeclaringClass().getName(); + } + + @Override + public Class getReturnType() { + return methodInvocation.getMethod().getReturnType(); + } + + @Override + public Method getMethod() { + return methodInvocation.getMethod(); + } + + @Override + public Class[] getParameterTypes() { + return methodInvocation.getMethod().getParameterTypes(); + } + + @Override + @Nullable + public String[] getParameterNames() { + String[] parameterNames = this.parameterNames; + if (parameterNames == null) { + parameterNames = parameterNameDiscoverer.getParameterNames(getMethod()); + this.parameterNames = parameterNames; + } + return parameterNames; + } + + @Override + public Class[] getExceptionTypes() { + return methodInvocation.getMethod().getExceptionTypes(); + } + + @Override + public String toShortString() { + return toString(false, false, false, false); + } + + @Override + public String toLongString() { + return toString(true, true, true, true); + } + + @Override + public String toString() { + return toString(false, true, false, true); + } + + private String toString(boolean includeModifier, boolean includeReturnTypeAndArgs, + boolean useLongReturnAndArgumentTypeName, boolean useLongTypeName) { + + StringBuilder sb = new StringBuilder(); + if (includeModifier) { + sb.append(Modifier.toString(getModifiers())); + sb.append(" "); + } + if (includeReturnTypeAndArgs) { + appendType(sb, getReturnType(), useLongReturnAndArgumentTypeName); + sb.append(" "); + } + appendType(sb, getDeclaringType(), useLongTypeName); + sb.append("."); + sb.append(getMethod().getName()); + sb.append("("); + Class[] parametersTypes = getParameterTypes(); + appendTypes(sb, parametersTypes, includeReturnTypeAndArgs, useLongReturnAndArgumentTypeName); + sb.append(")"); + return sb.toString(); + } + + private void appendTypes(StringBuilder sb, Class[] types, boolean includeArgs, + boolean useLongReturnAndArgumentTypeName) { + + if (includeArgs) { + for (int size = types.length, i = 0; i < size; i++) { + appendType(sb, types[i], useLongReturnAndArgumentTypeName); + if (i < size - 1) { + sb.append(","); + } + } + } + else { + if (types.length != 0) { + sb.append(".."); + } + } + } + + private void appendType(StringBuilder sb, Class type, boolean useLongTypeName) { + if (type.isArray()) { + appendType(sb, type.getComponentType(), useLongTypeName); + sb.append("[]"); + } + else { + sb.append(useLongTypeName ? type.getName() : type.getSimpleName()); + } + } + } + + + /** + * Lazily initialized SourceLocation. + */ + private class SourceLocationImpl implements SourceLocation { + + @Override + public Class getWithinType() { + if (methodInvocation.getThis() == null) { + throw new UnsupportedOperationException("No source location joinpoint available: target is null"); + } + return methodInvocation.getThis().getClass(); + } + + @Override + public String getFileName() { + throw new UnsupportedOperationException(); + } + + @Override + public int getLine() { + throw new UnsupportedOperationException(); + } + + @Override + @Deprecated + public int getColumn() { + throw new UnsupportedOperationException(); + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/RuntimeTestWalker.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/RuntimeTestWalker.java new file mode 100644 index 0000000..82e13b2 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/RuntimeTestWalker.java @@ -0,0 +1,297 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import java.lang.reflect.Field; + +import org.aspectj.weaver.ReferenceType; +import org.aspectj.weaver.ReferenceTypeDelegate; +import org.aspectj.weaver.ResolvedType; +import org.aspectj.weaver.ast.And; +import org.aspectj.weaver.ast.Call; +import org.aspectj.weaver.ast.FieldGetCall; +import org.aspectj.weaver.ast.HasAnnotation; +import org.aspectj.weaver.ast.ITestVisitor; +import org.aspectj.weaver.ast.Instanceof; +import org.aspectj.weaver.ast.Literal; +import org.aspectj.weaver.ast.Not; +import org.aspectj.weaver.ast.Or; +import org.aspectj.weaver.ast.Test; +import org.aspectj.weaver.internal.tools.MatchingContextBasedTest; +import org.aspectj.weaver.reflect.ReflectionBasedReferenceTypeDelegate; +import org.aspectj.weaver.reflect.ReflectionVar; +import org.aspectj.weaver.reflect.ShadowMatchImpl; +import org.aspectj.weaver.tools.ShadowMatch; + +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * This class encapsulates some AspectJ internal knowledge that should be + * pushed back into the AspectJ project in a future release. + * + *

It relies on implementation specific knowledge in AspectJ to break + * encapsulation and do something AspectJ was not designed to do: query + * the types of runtime tests that will be performed. The code here should + * migrate to {@code ShadowMatch.getVariablesInvolvedInRuntimeTest()} + * or some similar operation. + * + *

See Bug 151593 + * + * @author Adrian Colyer + * @author Ramnivas Laddad + * @since 2.0 + */ +class RuntimeTestWalker { + + private static final Field residualTestField; + + private static final Field varTypeField; + + private static final Field myClassField; + + + static { + try { + residualTestField = ShadowMatchImpl.class.getDeclaredField("residualTest"); + varTypeField = ReflectionVar.class.getDeclaredField("varType"); + myClassField = ReflectionBasedReferenceTypeDelegate.class.getDeclaredField("myClass"); + } + catch (NoSuchFieldException ex) { + throw new IllegalStateException("The version of aspectjtools.jar / aspectjweaver.jar " + + "on the classpath is incompatible with this version of Spring: " + ex); + } + } + + + @Nullable + private final Test runtimeTest; + + + public RuntimeTestWalker(ShadowMatch shadowMatch) { + try { + ReflectionUtils.makeAccessible(residualTestField); + this.runtimeTest = (Test) residualTestField.get(shadowMatch); + } + catch (IllegalAccessException ex) { + throw new IllegalStateException(ex); + } + } + + + /** + * If the test uses any of the this, target, at_this, at_target, and at_annotation vars, + * then it tests subtype sensitive vars. + */ + public boolean testsSubtypeSensitiveVars() { + return (this.runtimeTest != null && + new SubtypeSensitiveVarTypeTestVisitor().testsSubtypeSensitiveVars(this.runtimeTest)); + } + + public boolean testThisInstanceOfResidue(Class thisClass) { + return (this.runtimeTest != null && + new ThisInstanceOfResidueTestVisitor(thisClass).thisInstanceOfMatches(this.runtimeTest)); + } + + public boolean testTargetInstanceOfResidue(Class targetClass) { + return (this.runtimeTest != null && + new TargetInstanceOfResidueTestVisitor(targetClass).targetInstanceOfMatches(this.runtimeTest)); + } + + + private static class TestVisitorAdapter implements ITestVisitor { + + protected static final int THIS_VAR = 0; + protected static final int TARGET_VAR = 1; + protected static final int AT_THIS_VAR = 3; + protected static final int AT_TARGET_VAR = 4; + protected static final int AT_ANNOTATION_VAR = 8; + + @Override + public void visit(And e) { + e.getLeft().accept(this); + e.getRight().accept(this); + } + + @Override + public void visit(Or e) { + e.getLeft().accept(this); + e.getRight().accept(this); + } + + @Override + public void visit(Not e) { + e.getBody().accept(this); + } + + @Override + public void visit(Instanceof i) { + } + + @Override + public void visit(Literal literal) { + } + + @Override + public void visit(Call call) { + } + + @Override + public void visit(FieldGetCall fieldGetCall) { + } + + @Override + public void visit(HasAnnotation hasAnnotation) { + } + + @Override + public void visit(MatchingContextBasedTest matchingContextTest) { + } + + protected int getVarType(ReflectionVar v) { + try { + ReflectionUtils.makeAccessible(varTypeField); + return (Integer) varTypeField.get(v); + } + catch (IllegalAccessException ex) { + throw new IllegalStateException(ex); + } + } + } + + + private abstract static class InstanceOfResidueTestVisitor extends TestVisitorAdapter { + + private final Class matchClass; + + private boolean matches; + + private final int matchVarType; + + public InstanceOfResidueTestVisitor(Class matchClass, boolean defaultMatches, int matchVarType) { + this.matchClass = matchClass; + this.matches = defaultMatches; + this.matchVarType = matchVarType; + } + + public boolean instanceOfMatches(Test test) { + test.accept(this); + return this.matches; + } + + @Override + public void visit(Instanceof i) { + int varType = getVarType((ReflectionVar) i.getVar()); + if (varType != this.matchVarType) { + return; + } + Class typeClass = null; + ResolvedType type = (ResolvedType) i.getType(); + if (type instanceof ReferenceType) { + ReferenceTypeDelegate delegate = ((ReferenceType) type).getDelegate(); + if (delegate instanceof ReflectionBasedReferenceTypeDelegate) { + try { + ReflectionUtils.makeAccessible(myClassField); + typeClass = (Class) myClassField.get(delegate); + } + catch (IllegalAccessException ex) { + throw new IllegalStateException(ex); + } + } + } + try { + // Don't use ResolvedType.isAssignableFrom() as it won't be aware of (Spring) mixins + if (typeClass == null) { + typeClass = ClassUtils.forName(type.getName(), this.matchClass.getClassLoader()); + } + this.matches = typeClass.isAssignableFrom(this.matchClass); + } + catch (ClassNotFoundException ex) { + this.matches = false; + } + } + } + + + /** + * Check if residue of target(TYPE) kind. See SPR-3783 for more details. + */ + private static class TargetInstanceOfResidueTestVisitor extends InstanceOfResidueTestVisitor { + + public TargetInstanceOfResidueTestVisitor(Class targetClass) { + super(targetClass, false, TARGET_VAR); + } + + public boolean targetInstanceOfMatches(Test test) { + return instanceOfMatches(test); + } + } + + + /** + * Check if residue of this(TYPE) kind. See SPR-2979 for more details. + */ + private static class ThisInstanceOfResidueTestVisitor extends InstanceOfResidueTestVisitor { + + public ThisInstanceOfResidueTestVisitor(Class thisClass) { + super(thisClass, true, THIS_VAR); + } + + // TODO: Optimization: Process only if this() specifies a type and not an identifier. + public boolean thisInstanceOfMatches(Test test) { + return instanceOfMatches(test); + } + } + + + private static class SubtypeSensitiveVarTypeTestVisitor extends TestVisitorAdapter { + + private final Object thisObj = new Object(); + + private final Object targetObj = new Object(); + + private final Object[] argsObjs = new Object[0]; + + private boolean testsSubtypeSensitiveVars = false; + + public boolean testsSubtypeSensitiveVars(Test aTest) { + aTest.accept(this); + return this.testsSubtypeSensitiveVars; + } + + @Override + public void visit(Instanceof i) { + ReflectionVar v = (ReflectionVar) i.getVar(); + Object varUnderTest = v.getBindingAtJoinPoint(this.thisObj, this.targetObj, this.argsObjs); + if (varUnderTest == this.thisObj || varUnderTest == this.targetObj) { + this.testsSubtypeSensitiveVars = true; + } + } + + @Override + public void visit(HasAnnotation hasAnn) { + // If you thought things were bad before, now we sink to new levels of horror... + ReflectionVar v = (ReflectionVar) hasAnn.getVar(); + int varType = getVarType(v); + if (varType == AT_THIS_VAR || varType == AT_TARGET_VAR || varType == AT_ANNOTATION_VAR) { + this.testsSubtypeSensitiveVars = true; + } + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/SimpleAspectInstanceFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/SimpleAspectInstanceFactory.java new file mode 100644 index 0000000..f8a674a --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/SimpleAspectInstanceFactory.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import java.lang.reflect.InvocationTargetException; + +import org.springframework.aop.framework.AopConfigException; +import org.springframework.core.Ordered; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +/** + * Implementation of {@link AspectInstanceFactory} that creates a new instance + * of the specified aspect class for every {@link #getAspectInstance()} call. + * + * @author Juergen Hoeller + * @since 2.0.4 + */ +public class SimpleAspectInstanceFactory implements AspectInstanceFactory { + + private final Class aspectClass; + + + /** + * Create a new SimpleAspectInstanceFactory for the given aspect class. + * @param aspectClass the aspect class + */ + public SimpleAspectInstanceFactory(Class aspectClass) { + Assert.notNull(aspectClass, "Aspect class must not be null"); + this.aspectClass = aspectClass; + } + + + /** + * Return the specified aspect class (never {@code null}). + */ + public final Class getAspectClass() { + return this.aspectClass; + } + + @Override + public final Object getAspectInstance() { + try { + return ReflectionUtils.accessibleConstructor(this.aspectClass).newInstance(); + } + catch (NoSuchMethodException ex) { + throw new AopConfigException( + "No default constructor on aspect class: " + this.aspectClass.getName(), ex); + } + catch (InstantiationException ex) { + throw new AopConfigException( + "Unable to instantiate aspect class: " + this.aspectClass.getName(), ex); + } + catch (IllegalAccessException ex) { + throw new AopConfigException( + "Could not access aspect constructor: " + this.aspectClass.getName(), ex); + } + catch (InvocationTargetException ex) { + throw new AopConfigException( + "Failed to invoke aspect constructor: " + this.aspectClass.getName(), ex.getTargetException()); + } + } + + @Override + @Nullable + public ClassLoader getAspectClassLoader() { + return this.aspectClass.getClassLoader(); + } + + /** + * Determine the order for this factory's aspect instance, + * either an instance-specific order expressed through implementing + * the {@link org.springframework.core.Ordered} interface, + * or a fallback order. + * @see org.springframework.core.Ordered + * @see #getOrderForAspectClass + */ + @Override + public int getOrder() { + return getOrderForAspectClass(this.aspectClass); + } + + /** + * Determine a fallback order for the case that the aspect instance + * does not express an instance-specific order through implementing + * the {@link org.springframework.core.Ordered} interface. + *

The default implementation simply returns {@code Ordered.LOWEST_PRECEDENCE}. + * @param aspectClass the aspect class + */ + protected int getOrderForAspectClass(Class aspectClass) { + return Ordered.LOWEST_PRECEDENCE; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/SingletonAspectInstanceFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/SingletonAspectInstanceFactory.java new file mode 100644 index 0000000..8044791 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/SingletonAspectInstanceFactory.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import java.io.Serializable; + +import org.springframework.core.Ordered; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Implementation of {@link AspectInstanceFactory} that is backed by a + * specified singleton object, returning the same instance for every + * {@link #getAspectInstance()} call. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + * @see SimpleAspectInstanceFactory + */ +@SuppressWarnings("serial") +public class SingletonAspectInstanceFactory implements AspectInstanceFactory, Serializable { + + private final Object aspectInstance; + + + /** + * Create a new SingletonAspectInstanceFactory for the given aspect instance. + * @param aspectInstance the singleton aspect instance + */ + public SingletonAspectInstanceFactory(Object aspectInstance) { + Assert.notNull(aspectInstance, "Aspect instance must not be null"); + this.aspectInstance = aspectInstance; + } + + + @Override + public final Object getAspectInstance() { + return this.aspectInstance; + } + + @Override + @Nullable + public ClassLoader getAspectClassLoader() { + return this.aspectInstance.getClass().getClassLoader(); + } + + /** + * Determine the order for this factory's aspect instance, + * either an instance-specific order expressed through implementing + * the {@link org.springframework.core.Ordered} interface, + * or a fallback order. + * @see org.springframework.core.Ordered + * @see #getOrderForAspectClass + */ + @Override + public int getOrder() { + if (this.aspectInstance instanceof Ordered) { + return ((Ordered) this.aspectInstance).getOrder(); + } + return getOrderForAspectClass(this.aspectInstance.getClass()); + } + + /** + * Determine a fallback order for the case that the aspect instance + * does not express an instance-specific order through implementing + * the {@link org.springframework.core.Ordered} interface. + *

The default implementation simply returns {@code Ordered.LOWEST_PRECEDENCE}. + * @param aspectClass the aspect class + */ + protected int getOrderForAspectClass(Class aspectClass) { + return Ordered.LOWEST_PRECEDENCE; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/TypePatternClassFilter.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/TypePatternClassFilter.java new file mode 100644 index 0000000..cf1dcd9 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/TypePatternClassFilter.java @@ -0,0 +1,135 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.aspectj.weaver.tools.PointcutParser; +import org.aspectj.weaver.tools.TypePatternMatcher; + +import org.springframework.aop.ClassFilter; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Spring AOP {@link ClassFilter} implementation using AspectJ type matching. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Sam Brannen + * @since 2.0 + */ +public class TypePatternClassFilter implements ClassFilter { + + private String typePattern = ""; + + @Nullable + private TypePatternMatcher aspectJTypePatternMatcher; + + + /** + * Creates a new instance of the {@link TypePatternClassFilter} class. + *

This is the JavaBean constructor; be sure to set the + * {@link #setTypePattern(String) typePattern} property, else a + * no doubt fatal {@link IllegalStateException} will be thrown + * when the {@link #matches(Class)} method is first invoked. + */ + public TypePatternClassFilter() { + } + + /** + * Create a fully configured {@link TypePatternClassFilter} using the + * given type pattern. + * @param typePattern the type pattern that AspectJ weaver should parse + */ + public TypePatternClassFilter(String typePattern) { + setTypePattern(typePattern); + } + + + /** + * Set the AspectJ type pattern to match. + *

Examples include: + * + * org.springframework.beans.* + * + * This will match any class or interface in the given package. + * + * org.springframework.beans.ITestBean+ + * + * This will match the {@code ITestBean} interface and any class + * that implements it. + *

These conventions are established by AspectJ, not Spring AOP. + * @param typePattern the type pattern that AspectJ weaver should parse + */ + public void setTypePattern(String typePattern) { + Assert.notNull(typePattern, "Type pattern must not be null"); + this.typePattern = typePattern; + this.aspectJTypePatternMatcher = + PointcutParser.getPointcutParserSupportingAllPrimitivesAndUsingContextClassloaderForResolution(). + parseTypePattern(replaceBooleanOperators(typePattern)); + } + + /** + * Return the AspectJ type pattern to match. + */ + public String getTypePattern() { + return this.typePattern; + } + + + /** + * Should the pointcut apply to the given interface or target class? + * @param clazz candidate target class + * @return whether the advice should apply to this candidate target class + * @throws IllegalStateException if no {@link #setTypePattern(String)} has been set + */ + @Override + public boolean matches(Class clazz) { + Assert.state(this.aspectJTypePatternMatcher != null, "No type pattern has been set"); + return this.aspectJTypePatternMatcher.matches(clazz); + } + + /** + * If a type pattern has been specified in XML, the user cannot + * write {@code and} as "&&" (though && will work). + * We also allow {@code and} between two sub-expressions. + *

This method converts back to {@code &&} for the AspectJ pointcut parser. + */ + private String replaceBooleanOperators(String pcExpr) { + String result = StringUtils.replace(pcExpr," and "," && "); + result = StringUtils.replace(result, " or ", " || "); + return StringUtils.replace(result, " not ", " ! "); + } + + @Override + public boolean equals(Object other) { + return (this == other || (other instanceof TypePatternClassFilter && + ObjectUtils.nullSafeEquals(this.typePattern, ((TypePatternClassFilter) other).typePattern))); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.typePattern); + } + + @Override + public String toString() { + return getClass().getName() + ": " + this.typePattern; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactory.java new file mode 100644 index 0000000..e76156b --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactory.java @@ -0,0 +1,285 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.Map; +import java.util.StringTokenizer; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.aspectj.lang.annotation.After; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.AjType; +import org.aspectj.lang.reflect.AjTypeSystem; +import org.aspectj.lang.reflect.PerClauseKind; + +import org.springframework.aop.framework.AopConfigException; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.lang.Nullable; + +/** + * Abstract base class for factories that can create Spring AOP Advisors + * given AspectJ classes from classes honoring the AspectJ 5 annotation syntax. + * + *

This class handles annotation parsing and validation functionality. + * It does not actually generate Spring AOP Advisors, which is deferred to subclasses. + * + * @author Rod Johnson + * @author Adrian Colyer + * @author Juergen Hoeller + * @since 2.0 + */ +public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFactory { + + private static final String AJC_MAGIC = "ajc$"; + + private static final Class[] ASPECTJ_ANNOTATION_CLASSES = new Class[] { + Pointcut.class, Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class}; + + + /** Logger available to subclasses. */ + protected final Log logger = LogFactory.getLog(getClass()); + + protected final ParameterNameDiscoverer parameterNameDiscoverer = new AspectJAnnotationParameterNameDiscoverer(); + + + /** + * We consider something to be an AspectJ aspect suitable for use by the Spring AOP system + * if it has the @Aspect annotation, and was not compiled by ajc. The reason for this latter test + * is that aspects written in the code-style (AspectJ language) also have the annotation present + * when compiled by ajc with the -1.5 flag, yet they cannot be consumed by Spring AOP. + */ + @Override + public boolean isAspect(Class clazz) { + return (hasAspectAnnotation(clazz) && !compiledByAjc(clazz)); + } + + private boolean hasAspectAnnotation(Class clazz) { + return (AnnotationUtils.findAnnotation(clazz, Aspect.class) != null); + } + + /** + * We need to detect this as "code-style" AspectJ aspects should not be + * interpreted by Spring AOP. + */ + private boolean compiledByAjc(Class clazz) { + // The AJTypeSystem goes to great lengths to provide a uniform appearance between code-style and + // annotation-style aspects. Therefore there is no 'clean' way to tell them apart. Here we rely on + // an implementation detail of the AspectJ compiler. + for (Field field : clazz.getDeclaredFields()) { + if (field.getName().startsWith(AJC_MAGIC)) { + return true; + } + } + return false; + } + + @Override + public void validate(Class aspectClass) throws AopConfigException { + // If the parent has the annotation and isn't abstract it's an error + if (aspectClass.getSuperclass().getAnnotation(Aspect.class) != null && + !Modifier.isAbstract(aspectClass.getSuperclass().getModifiers())) { + throw new AopConfigException("[" + aspectClass.getName() + "] cannot extend concrete aspect [" + + aspectClass.getSuperclass().getName() + "]"); + } + + AjType ajType = AjTypeSystem.getAjType(aspectClass); + if (!ajType.isAspect()) { + throw new NotAnAtAspectException(aspectClass); + } + if (ajType.getPerClause().getKind() == PerClauseKind.PERCFLOW) { + throw new AopConfigException(aspectClass.getName() + " uses percflow instantiation model: " + + "This is not supported in Spring AOP."); + } + if (ajType.getPerClause().getKind() == PerClauseKind.PERCFLOWBELOW) { + throw new AopConfigException(aspectClass.getName() + " uses percflowbelow instantiation model: " + + "This is not supported in Spring AOP."); + } + } + + /** + * Find and return the first AspectJ annotation on the given method + * (there should only be one anyway...). + */ + @SuppressWarnings("unchecked") + @Nullable + protected static AspectJAnnotation findAspectJAnnotationOnMethod(Method method) { + for (Class clazz : ASPECTJ_ANNOTATION_CLASSES) { + AspectJAnnotation foundAnnotation = findAnnotation(method, (Class) clazz); + if (foundAnnotation != null) { + return foundAnnotation; + } + } + return null; + } + + @Nullable + private static AspectJAnnotation findAnnotation(Method method, Class toLookFor) { + A result = AnnotationUtils.findAnnotation(method, toLookFor); + if (result != null) { + return new AspectJAnnotation<>(result); + } + else { + return null; + } + } + + + /** + * Enum for AspectJ annotation types. + * @see AspectJAnnotation#getAnnotationType() + */ + protected enum AspectJAnnotationType { + + AtPointcut, AtAround, AtBefore, AtAfter, AtAfterReturning, AtAfterThrowing + } + + + /** + * Class modelling an AspectJ annotation, exposing its type enumeration and + * pointcut String. + * @param the annotation type + */ + protected static class AspectJAnnotation { + + private static final String[] EXPRESSION_ATTRIBUTES = new String[] {"pointcut", "value"}; + + private static Map, AspectJAnnotationType> annotationTypeMap = new HashMap<>(8); + + static { + annotationTypeMap.put(Pointcut.class, AspectJAnnotationType.AtPointcut); + annotationTypeMap.put(Around.class, AspectJAnnotationType.AtAround); + annotationTypeMap.put(Before.class, AspectJAnnotationType.AtBefore); + annotationTypeMap.put(After.class, AspectJAnnotationType.AtAfter); + annotationTypeMap.put(AfterReturning.class, AspectJAnnotationType.AtAfterReturning); + annotationTypeMap.put(AfterThrowing.class, AspectJAnnotationType.AtAfterThrowing); + } + + private final A annotation; + + private final AspectJAnnotationType annotationType; + + private final String pointcutExpression; + + private final String argumentNames; + + public AspectJAnnotation(A annotation) { + this.annotation = annotation; + this.annotationType = determineAnnotationType(annotation); + try { + this.pointcutExpression = resolveExpression(annotation); + Object argNames = AnnotationUtils.getValue(annotation, "argNames"); + this.argumentNames = (argNames instanceof String ? (String) argNames : ""); + } + catch (Exception ex) { + throw new IllegalArgumentException(annotation + " is not a valid AspectJ annotation", ex); + } + } + + private AspectJAnnotationType determineAnnotationType(A annotation) { + AspectJAnnotationType type = annotationTypeMap.get(annotation.annotationType()); + if (type != null) { + return type; + } + throw new IllegalStateException("Unknown annotation type: " + annotation); + } + + private String resolveExpression(A annotation) { + for (String attributeName : EXPRESSION_ATTRIBUTES) { + Object val = AnnotationUtils.getValue(annotation, attributeName); + if (val instanceof String) { + String str = (String) val; + if (!str.isEmpty()) { + return str; + } + } + } + throw new IllegalStateException("Failed to resolve expression: " + annotation); + } + + public AspectJAnnotationType getAnnotationType() { + return this.annotationType; + } + + public A getAnnotation() { + return this.annotation; + } + + public String getPointcutExpression() { + return this.pointcutExpression; + } + + public String getArgumentNames() { + return this.argumentNames; + } + + @Override + public String toString() { + return this.annotation.toString(); + } + } + + + /** + * ParameterNameDiscoverer implementation that analyzes the arg names + * specified at the AspectJ annotation level. + */ + private static class AspectJAnnotationParameterNameDiscoverer implements ParameterNameDiscoverer { + + @Override + @Nullable + public String[] getParameterNames(Method method) { + if (method.getParameterCount() == 0) { + return new String[0]; + } + AspectJAnnotation annotation = findAspectJAnnotationOnMethod(method); + if (annotation == null) { + return null; + } + StringTokenizer nameTokens = new StringTokenizer(annotation.getArgumentNames(), ","); + if (nameTokens.countTokens() > 0) { + String[] names = new String[nameTokens.countTokens()]; + for (int i = 0; i < names.length; i++) { + names[i] = nameTokens.nextToken(); + } + return names; + } + else { + return null; + } + } + + @Override + @Nullable + public String[] getParameterNames(Constructor ctor) { + throw new UnsupportedOperationException("Spring AOP cannot handle constructor advice"); + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AnnotationAwareAspectJAutoProxyCreator.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AnnotationAwareAspectJAutoProxyCreator.java new file mode 100644 index 0000000..45ea498 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AnnotationAwareAspectJAutoProxyCreator.java @@ -0,0 +1,153 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.annotation; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +import org.springframework.aop.Advisor; +import org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * {@link AspectJAwareAdvisorAutoProxyCreator} subclass that processes all AspectJ + * annotation aspects in the current application context, as well as Spring Advisors. + * + *

Any AspectJ annotated classes will automatically be recognized, and their + * advice applied if Spring AOP's proxy-based model is capable of applying it. + * This covers method execution joinpoints. + * + *

If the <aop:include> element is used, only @AspectJ beans with names matched by + * an include pattern will be considered as defining aspects to use for Spring auto-proxying. + * + *

Processing of Spring Advisors follows the rules established in + * {@link org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator}. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.aop.aspectj.annotation.AspectJAdvisorFactory + */ +@SuppressWarnings("serial") +public class AnnotationAwareAspectJAutoProxyCreator extends AspectJAwareAdvisorAutoProxyCreator { + + @Nullable + private List includePatterns; + + @Nullable + private AspectJAdvisorFactory aspectJAdvisorFactory; + + @Nullable + private BeanFactoryAspectJAdvisorsBuilder aspectJAdvisorsBuilder; + + + /** + * Set a list of regex patterns, matching eligible @AspectJ bean names. + *

Default is to consider all @AspectJ beans as eligible. + */ + public void setIncludePatterns(List patterns) { + this.includePatterns = new ArrayList<>(patterns.size()); + for (String patternText : patterns) { + this.includePatterns.add(Pattern.compile(patternText)); + } + } + + public void setAspectJAdvisorFactory(AspectJAdvisorFactory aspectJAdvisorFactory) { + Assert.notNull(aspectJAdvisorFactory, "AspectJAdvisorFactory must not be null"); + this.aspectJAdvisorFactory = aspectJAdvisorFactory; + } + + @Override + protected void initBeanFactory(ConfigurableListableBeanFactory beanFactory) { + super.initBeanFactory(beanFactory); + if (this.aspectJAdvisorFactory == null) { + this.aspectJAdvisorFactory = new ReflectiveAspectJAdvisorFactory(beanFactory); + } + this.aspectJAdvisorsBuilder = + new BeanFactoryAspectJAdvisorsBuilderAdapter(beanFactory, this.aspectJAdvisorFactory); + } + + + @Override + protected List findCandidateAdvisors() { + // Add all the Spring advisors found according to superclass rules. + List advisors = super.findCandidateAdvisors(); + // Build Advisors for all AspectJ aspects in the bean factory. + if (this.aspectJAdvisorsBuilder != null) { + advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors()); + } + return advisors; + } + + @Override + protected boolean isInfrastructureClass(Class beanClass) { + // Previously we setProxyTargetClass(true) in the constructor, but that has too + // broad an impact. Instead we now override isInfrastructureClass to avoid proxying + // aspects. I'm not entirely happy with that as there is no good reason not + // to advise aspects, except that it causes advice invocation to go through a + // proxy, and if the aspect implements e.g the Ordered interface it will be + // proxied by that interface and fail at runtime as the advice method is not + // defined on the interface. We could potentially relax the restriction about + // not advising aspects in the future. + return (super.isInfrastructureClass(beanClass) || + (this.aspectJAdvisorFactory != null && this.aspectJAdvisorFactory.isAspect(beanClass))); + } + + /** + * Check whether the given aspect bean is eligible for auto-proxying. + *

If no <aop:include> elements were used then "includePatterns" will be + * {@code null} and all beans are included. If "includePatterns" is non-null, + * then one of the patterns must match. + */ + protected boolean isEligibleAspectBean(String beanName) { + if (this.includePatterns == null) { + return true; + } + else { + for (Pattern pattern : this.includePatterns) { + if (pattern.matcher(beanName).matches()) { + return true; + } + } + return false; + } + } + + + /** + * Subclass of BeanFactoryAspectJAdvisorsBuilderAdapter that delegates to + * surrounding AnnotationAwareAspectJAutoProxyCreator facilities. + */ + private class BeanFactoryAspectJAdvisorsBuilderAdapter extends BeanFactoryAspectJAdvisorsBuilder { + + public BeanFactoryAspectJAdvisorsBuilderAdapter( + ListableBeanFactory beanFactory, AspectJAdvisorFactory advisorFactory) { + + super(beanFactory, advisorFactory); + } + + @Override + protected boolean isEligibleBean(String beanName) { + return AnnotationAwareAspectJAutoProxyCreator.this.isEligibleAspectBean(beanName); + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorFactory.java new file mode 100644 index 0000000..ddba1f0 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorFactory.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.annotation; + +import java.lang.reflect.Method; +import java.util.List; + +import org.aopalliance.aop.Advice; + +import org.springframework.aop.Advisor; +import org.springframework.aop.aspectj.AspectJExpressionPointcut; +import org.springframework.aop.framework.AopConfigException; +import org.springframework.lang.Nullable; + +/** + * Interface for factories that can create Spring AOP Advisors from classes + * annotated with AspectJ annotation syntax. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + * @see AspectMetadata + * @see org.aspectj.lang.reflect.AjTypeSystem + */ +public interface AspectJAdvisorFactory { + + /** + * Determine whether or not the given class is an aspect, as reported + * by AspectJ's {@link org.aspectj.lang.reflect.AjTypeSystem}. + *

Will simply return {@code false} if the supposed aspect is + * invalid (such as an extension of a concrete aspect class). + * Will return true for some aspects that Spring AOP cannot process, + * such as those with unsupported instantiation models. + * Use the {@link #validate} method to handle these cases if necessary. + * @param clazz the supposed annotation-style AspectJ class + * @return whether or not this class is recognized by AspectJ as an aspect class + */ + boolean isAspect(Class clazz); + + /** + * Is the given class a valid AspectJ aspect class? + * @param aspectClass the supposed AspectJ annotation-style class to validate + * @throws AopConfigException if the class is an invalid aspect + * (which can never be legal) + * @throws NotAnAtAspectException if the class is not an aspect at all + * (which may or may not be legal, depending on the context) + */ + void validate(Class aspectClass) throws AopConfigException; + + /** + * Build Spring AOP Advisors for all annotated At-AspectJ methods + * on the specified aspect instance. + * @param aspectInstanceFactory the aspect instance factory + * (not the aspect instance itself in order to avoid eager instantiation) + * @return a list of advisors for this class + */ + List getAdvisors(MetadataAwareAspectInstanceFactory aspectInstanceFactory); + + /** + * Build a Spring AOP Advisor for the given AspectJ advice method. + * @param candidateAdviceMethod the candidate advice method + * @param aspectInstanceFactory the aspect instance factory + * @param declarationOrder the declaration order within the aspect + * @param aspectName the name of the aspect + * @return {@code null} if the method is not an AspectJ advice method + * or if it is a pointcut that will be used by other advice but will not + * create a Spring advice in its own right + */ + @Nullable + Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aspectInstanceFactory, + int declarationOrder, String aspectName); + + /** + * Build a Spring AOP Advice for the given AspectJ advice method. + * @param candidateAdviceMethod the candidate advice method + * @param expressionPointcut the AspectJ expression pointcut + * @param aspectInstanceFactory the aspect instance factory + * @param declarationOrder the declaration order within the aspect + * @param aspectName the name of the aspect + * @return {@code null} if the method is not an AspectJ advice method + * or if it is a pointcut that will be used by other advice but will not + * create a Spring advice in its own right + * @see org.springframework.aop.aspectj.AspectJAroundAdvice + * @see org.springframework.aop.aspectj.AspectJMethodBeforeAdvice + * @see org.springframework.aop.aspectj.AspectJAfterAdvice + * @see org.springframework.aop.aspectj.AspectJAfterReturningAdvice + * @see org.springframework.aop.aspectj.AspectJAfterThrowingAdvice + */ + @Nullable + Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut expressionPointcut, + MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName); + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJProxyFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJProxyFactory.java new file mode 100644 index 0000000..ae9e41d --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJProxyFactory.java @@ -0,0 +1,198 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.annotation; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.aspectj.lang.reflect.PerClauseKind; + +import org.springframework.aop.Advisor; +import org.springframework.aop.aspectj.AspectJProxyUtils; +import org.springframework.aop.aspectj.SimpleAspectInstanceFactory; +import org.springframework.aop.framework.ProxyCreatorSupport; +import org.springframework.aop.support.AopUtils; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * AspectJ-based proxy factory, allowing for programmatic building + * of proxies which include AspectJ aspects (code style as well + * Java 5 annotation style). + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Ramnivas Laddad + * @since 2.0 + * @see #addAspect(Object) + * @see #addAspect(Class) + * @see #getProxy() + * @see #getProxy(ClassLoader) + * @see org.springframework.aop.framework.ProxyFactory + */ +@SuppressWarnings("serial") +public class AspectJProxyFactory extends ProxyCreatorSupport { + + /** Cache for singleton aspect instances. */ + private static final Map, Object> aspectCache = new ConcurrentHashMap<>(); + + private final AspectJAdvisorFactory aspectFactory = new ReflectiveAspectJAdvisorFactory(); + + + /** + * Create a new AspectJProxyFactory. + */ + public AspectJProxyFactory() { + } + + /** + * Create a new AspectJProxyFactory. + *

Will proxy all interfaces that the given target implements. + * @param target the target object to be proxied + */ + public AspectJProxyFactory(Object target) { + Assert.notNull(target, "Target object must not be null"); + setInterfaces(ClassUtils.getAllInterfaces(target)); + setTarget(target); + } + + /** + * Create a new {@code AspectJProxyFactory}. + * No target, only interfaces. Must add interceptors. + */ + public AspectJProxyFactory(Class... interfaces) { + setInterfaces(interfaces); + } + + + /** + * Add the supplied aspect instance to the chain. The type of the aspect instance + * supplied must be a singleton aspect. True singleton lifecycle is not honoured when + * using this method - the caller is responsible for managing the lifecycle of any + * aspects added in this way. + * @param aspectInstance the AspectJ aspect instance + */ + public void addAspect(Object aspectInstance) { + Class aspectClass = aspectInstance.getClass(); + String aspectName = aspectClass.getName(); + AspectMetadata am = createAspectMetadata(aspectClass, aspectName); + if (am.getAjType().getPerClause().getKind() != PerClauseKind.SINGLETON) { + throw new IllegalArgumentException( + "Aspect class [" + aspectClass.getName() + "] does not define a singleton aspect"); + } + addAdvisorsFromAspectInstanceFactory( + new SingletonMetadataAwareAspectInstanceFactory(aspectInstance, aspectName)); + } + + /** + * Add an aspect of the supplied type to the end of the advice chain. + * @param aspectClass the AspectJ aspect class + */ + public void addAspect(Class aspectClass) { + String aspectName = aspectClass.getName(); + AspectMetadata am = createAspectMetadata(aspectClass, aspectName); + MetadataAwareAspectInstanceFactory instanceFactory = createAspectInstanceFactory(am, aspectClass, aspectName); + addAdvisorsFromAspectInstanceFactory(instanceFactory); + } + + + /** + * Add all {@link Advisor Advisors} from the supplied {@link MetadataAwareAspectInstanceFactory} + * to the current chain. Exposes any special purpose {@link Advisor Advisors} if needed. + * @see AspectJProxyUtils#makeAdvisorChainAspectJCapableIfNecessary(List) + */ + private void addAdvisorsFromAspectInstanceFactory(MetadataAwareAspectInstanceFactory instanceFactory) { + List advisors = this.aspectFactory.getAdvisors(instanceFactory); + Class targetClass = getTargetClass(); + Assert.state(targetClass != null, "Unresolvable target class"); + advisors = AopUtils.findAdvisorsThatCanApply(advisors, targetClass); + AspectJProxyUtils.makeAdvisorChainAspectJCapableIfNecessary(advisors); + AnnotationAwareOrderComparator.sort(advisors); + addAdvisors(advisors); + } + + /** + * Create an {@link AspectMetadata} instance for the supplied aspect type. + */ + private AspectMetadata createAspectMetadata(Class aspectClass, String aspectName) { + AspectMetadata am = new AspectMetadata(aspectClass, aspectName); + if (!am.getAjType().isAspect()) { + throw new IllegalArgumentException("Class [" + aspectClass.getName() + "] is not a valid aspect type"); + } + return am; + } + + /** + * Create a {@link MetadataAwareAspectInstanceFactory} for the supplied aspect type. If the aspect type + * has no per clause, then a {@link SingletonMetadataAwareAspectInstanceFactory} is returned, otherwise + * a {@link PrototypeAspectInstanceFactory} is returned. + */ + private MetadataAwareAspectInstanceFactory createAspectInstanceFactory( + AspectMetadata am, Class aspectClass, String aspectName) { + + MetadataAwareAspectInstanceFactory instanceFactory; + if (am.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) { + // Create a shared aspect instance. + Object instance = getSingletonAspectInstance(aspectClass); + instanceFactory = new SingletonMetadataAwareAspectInstanceFactory(instance, aspectName); + } + else { + // Create a factory for independent aspect instances. + instanceFactory = new SimpleMetadataAwareAspectInstanceFactory(aspectClass, aspectName); + } + return instanceFactory; + } + + /** + * Get the singleton aspect instance for the supplied aspect type. + * An instance is created if one cannot be found in the instance cache. + */ + private Object getSingletonAspectInstance(Class aspectClass) { + return aspectCache.computeIfAbsent(aspectClass, + clazz -> new SimpleAspectInstanceFactory(clazz).getAspectInstance()); + } + + + /** + * Create a new proxy according to the settings in this factory. + *

Can be called repeatedly. Effect will vary if we've added + * or removed interfaces. Can add and remove interceptors. + *

Uses a default class loader: Usually, the thread context class loader + * (if necessary for proxy creation). + * @return the new proxy + */ + @SuppressWarnings("unchecked") + public T getProxy() { + return (T) createAopProxy().getProxy(); + } + + /** + * Create a new proxy according to the settings in this factory. + *

Can be called repeatedly. Effect will vary if we've added + * or removed interfaces. Can add and remove interceptors. + *

Uses the given class loader (if necessary for proxy creation). + * @param classLoader the class loader to create the proxy with + * @return the new proxy + */ + @SuppressWarnings("unchecked") + public T getProxy(ClassLoader classLoader) { + return (T) createAopProxy().getProxy(classLoader); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectMetadata.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectMetadata.java new file mode 100644 index 0000000..048cc60 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectMetadata.java @@ -0,0 +1,194 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.annotation; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; + +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.AjType; +import org.aspectj.lang.reflect.AjTypeSystem; +import org.aspectj.lang.reflect.PerClauseKind; + +import org.springframework.aop.Pointcut; +import org.springframework.aop.aspectj.AspectJExpressionPointcut; +import org.springframework.aop.aspectj.TypePatternClassFilter; +import org.springframework.aop.framework.AopConfigException; +import org.springframework.aop.support.ComposablePointcut; + +/** + * Metadata for an AspectJ aspect class, with an additional Spring AOP pointcut + * for the per clause. + * + *

Uses AspectJ 5 AJType reflection API, enabling us to work with different + * AspectJ instantiation models such as "singleton", "pertarget" and "perthis". + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.aop.aspectj.AspectJExpressionPointcut + */ +@SuppressWarnings("serial") +public class AspectMetadata implements Serializable { + + /** + * The name of this aspect as defined to Spring (the bean name) - + * allows us to determine if two pieces of advice come from the + * same aspect and hence their relative precedence. + */ + private final String aspectName; + + /** + * The aspect class, stored separately for re-resolution of the + * corresponding AjType on deserialization. + */ + private final Class aspectClass; + + /** + * AspectJ reflection information (AspectJ 5 / Java 5 specific). + * Re-resolved on deserialization since it isn't serializable itself. + */ + private transient AjType ajType; + + /** + * Spring AOP pointcut corresponding to the per clause of the + * aspect. Will be the Pointcut.TRUE canonical instance in the + * case of a singleton, otherwise an AspectJExpressionPointcut. + */ + private final Pointcut perClausePointcut; + + + /** + * Create a new AspectMetadata instance for the given aspect class. + * @param aspectClass the aspect class + * @param aspectName the name of the aspect + */ + public AspectMetadata(Class aspectClass, String aspectName) { + this.aspectName = aspectName; + + Class currClass = aspectClass; + AjType ajType = null; + while (currClass != Object.class) { + AjType ajTypeToCheck = AjTypeSystem.getAjType(currClass); + if (ajTypeToCheck.isAspect()) { + ajType = ajTypeToCheck; + break; + } + currClass = currClass.getSuperclass(); + } + if (ajType == null) { + throw new IllegalArgumentException("Class '" + aspectClass.getName() + "' is not an @AspectJ aspect"); + } + if (ajType.getDeclarePrecedence().length > 0) { + throw new IllegalArgumentException("DeclarePrecedence not presently supported in Spring AOP"); + } + this.aspectClass = ajType.getJavaClass(); + this.ajType = ajType; + + switch (this.ajType.getPerClause().getKind()) { + case SINGLETON: + this.perClausePointcut = Pointcut.TRUE; + return; + case PERTARGET: + case PERTHIS: + AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); + ajexp.setLocation(aspectClass.getName()); + ajexp.setExpression(findPerClause(aspectClass)); + ajexp.setPointcutDeclarationScope(aspectClass); + this.perClausePointcut = ajexp; + return; + case PERTYPEWITHIN: + // Works with a type pattern + this.perClausePointcut = new ComposablePointcut(new TypePatternClassFilter(findPerClause(aspectClass))); + return; + default: + throw new AopConfigException( + "PerClause " + ajType.getPerClause().getKind() + " not supported by Spring AOP for " + aspectClass); + } + } + + /** + * Extract contents from String of form {@code pertarget(contents)}. + */ + private String findPerClause(Class aspectClass) { + String str = aspectClass.getAnnotation(Aspect.class).value(); + int beginIndex = str.indexOf('(') + 1; + int endIndex = str.length() - 1; + return str.substring(beginIndex, endIndex); + } + + + /** + * Return AspectJ reflection information. + */ + public AjType getAjType() { + return this.ajType; + } + + /** + * Return the aspect class. + */ + public Class getAspectClass() { + return this.aspectClass; + } + + /** + * Return the aspect name. + */ + public String getAspectName() { + return this.aspectName; + } + + /** + * Return a Spring pointcut expression for a singleton aspect. + * (e.g. {@code Pointcut.TRUE} if it's a singleton). + */ + public Pointcut getPerClausePointcut() { + return this.perClausePointcut; + } + + /** + * Return whether the aspect is defined as "perthis" or "pertarget". + */ + public boolean isPerThisOrPerTarget() { + PerClauseKind kind = getAjType().getPerClause().getKind(); + return (kind == PerClauseKind.PERTARGET || kind == PerClauseKind.PERTHIS); + } + + /** + * Return whether the aspect is defined as "pertypewithin". + */ + public boolean isPerTypeWithin() { + PerClauseKind kind = getAjType().getPerClause().getKind(); + return (kind == PerClauseKind.PERTYPEWITHIN); + } + + /** + * Return whether the aspect needs to be lazily instantiated. + */ + public boolean isLazilyInstantiated() { + return (isPerThisOrPerTarget() || isPerTypeWithin()); + } + + + private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException { + inputStream.defaultReadObject(); + this.ajType = AjTypeSystem.getAjType(this.aspectClass); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectInstanceFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectInstanceFactory.java new file mode 100644 index 0000000..d5fefce --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectInstanceFactory.java @@ -0,0 +1,152 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.annotation; + +import java.io.Serializable; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.OrderUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * {@link org.springframework.aop.aspectj.AspectInstanceFactory} implementation + * backed by a Spring {@link org.springframework.beans.factory.BeanFactory}. + * + *

Note that this may instantiate multiple times if using a prototype, + * which probably won't give the semantics you expect. + * Use a {@link LazySingletonAspectInstanceFactoryDecorator} + * to wrap this to ensure only one new aspect comes back. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.beans.factory.BeanFactory + * @see LazySingletonAspectInstanceFactoryDecorator + */ +@SuppressWarnings("serial") +public class BeanFactoryAspectInstanceFactory implements MetadataAwareAspectInstanceFactory, Serializable { + + private final BeanFactory beanFactory; + + private final String name; + + private final AspectMetadata aspectMetadata; + + + /** + * Create a BeanFactoryAspectInstanceFactory. AspectJ will be called to + * introspect to create AJType metadata using the type returned for the + * given bean name from the BeanFactory. + * @param beanFactory the BeanFactory to obtain instance(s) from + * @param name the name of the bean + */ + public BeanFactoryAspectInstanceFactory(BeanFactory beanFactory, String name) { + this(beanFactory, name, null); + } + + /** + * Create a BeanFactoryAspectInstanceFactory, providing a type that AspectJ should + * introspect to create AJType metadata. Use if the BeanFactory may consider the type + * to be a subclass (as when using CGLIB), and the information should relate to a superclass. + * @param beanFactory the BeanFactory to obtain instance(s) from + * @param name the name of the bean + * @param type the type that should be introspected by AspectJ + * ({@code null} indicates resolution through {@link BeanFactory#getType} via the bean name) + */ + public BeanFactoryAspectInstanceFactory(BeanFactory beanFactory, String name, @Nullable Class type) { + Assert.notNull(beanFactory, "BeanFactory must not be null"); + Assert.notNull(name, "Bean name must not be null"); + this.beanFactory = beanFactory; + this.name = name; + Class resolvedType = type; + if (type == null) { + resolvedType = beanFactory.getType(name); + Assert.notNull(resolvedType, "Unresolvable bean type - explicitly specify the aspect class"); + } + this.aspectMetadata = new AspectMetadata(resolvedType, name); + } + + + @Override + public Object getAspectInstance() { + return this.beanFactory.getBean(this.name); + } + + @Override + @Nullable + public ClassLoader getAspectClassLoader() { + return (this.beanFactory instanceof ConfigurableBeanFactory ? + ((ConfigurableBeanFactory) this.beanFactory).getBeanClassLoader() : + ClassUtils.getDefaultClassLoader()); + } + + @Override + public AspectMetadata getAspectMetadata() { + return this.aspectMetadata; + } + + @Override + @Nullable + public Object getAspectCreationMutex() { + if (this.beanFactory.isSingleton(this.name)) { + // Rely on singleton semantics provided by the factory -> no local lock. + return null; + } + else if (this.beanFactory instanceof ConfigurableBeanFactory) { + // No singleton guarantees from the factory -> let's lock locally but + // reuse the factory's singleton lock, just in case a lazy dependency + // of our advice bean happens to trigger the singleton lock implicitly... + return ((ConfigurableBeanFactory) this.beanFactory).getSingletonMutex(); + } + else { + return this; + } + } + + /** + * Determine the order for this factory's target aspect, either + * an instance-specific order expressed through implementing the + * {@link org.springframework.core.Ordered} interface (only + * checked for singleton beans), or an order expressed through the + * {@link org.springframework.core.annotation.Order} annotation + * at the class level. + * @see org.springframework.core.Ordered + * @see org.springframework.core.annotation.Order + */ + @Override + public int getOrder() { + Class type = this.beanFactory.getType(this.name); + if (type != null) { + if (Ordered.class.isAssignableFrom(type) && this.beanFactory.isSingleton(this.name)) { + return ((Ordered) this.beanFactory.getBean(this.name)).getOrder(); + } + return OrderUtils.getOrder(type, Ordered.LOWEST_PRECEDENCE); + } + return Ordered.LOWEST_PRECEDENCE; + } + + + @Override + public String toString() { + return getClass().getSimpleName() + ": bean name '" + this.name + "'"; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectJAdvisorsBuilder.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectJAdvisorsBuilder.java new file mode 100644 index 0000000..8896f99 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectJAdvisorsBuilder.java @@ -0,0 +1,164 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.annotation; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.aspectj.lang.reflect.PerClauseKind; + +import org.springframework.aop.Advisor; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Helper for retrieving @AspectJ beans from a BeanFactory and building + * Spring Advisors based on them, for use with auto-proxying. + * + * @author Juergen Hoeller + * @since 2.0.2 + * @see AnnotationAwareAspectJAutoProxyCreator + */ +public class BeanFactoryAspectJAdvisorsBuilder { + + private final ListableBeanFactory beanFactory; + + private final AspectJAdvisorFactory advisorFactory; + + @Nullable + private volatile List aspectBeanNames; + + private final Map> advisorsCache = new ConcurrentHashMap<>(); + + private final Map aspectFactoryCache = new ConcurrentHashMap<>(); + + + /** + * Create a new BeanFactoryAspectJAdvisorsBuilder for the given BeanFactory. + * @param beanFactory the ListableBeanFactory to scan + */ + public BeanFactoryAspectJAdvisorsBuilder(ListableBeanFactory beanFactory) { + this(beanFactory, new ReflectiveAspectJAdvisorFactory(beanFactory)); + } + + /** + * Create a new BeanFactoryAspectJAdvisorsBuilder for the given BeanFactory. + * @param beanFactory the ListableBeanFactory to scan + * @param advisorFactory the AspectJAdvisorFactory to build each Advisor with + */ + public BeanFactoryAspectJAdvisorsBuilder(ListableBeanFactory beanFactory, AspectJAdvisorFactory advisorFactory) { + Assert.notNull(beanFactory, "ListableBeanFactory must not be null"); + Assert.notNull(advisorFactory, "AspectJAdvisorFactory must not be null"); + this.beanFactory = beanFactory; + this.advisorFactory = advisorFactory; + } + + + /** + * Look for AspectJ-annotated aspect beans in the current bean factory, + * and return to a list of Spring AOP Advisors representing them. + *

Creates a Spring Advisor for each AspectJ advice method. + * @return the list of {@link org.springframework.aop.Advisor} beans + * @see #isEligibleBean + */ + public List buildAspectJAdvisors() { + List aspectNames = this.aspectBeanNames; + + if (aspectNames == null) { + synchronized (this) { + aspectNames = this.aspectBeanNames; + if (aspectNames == null) { + List advisors = new ArrayList<>(); + aspectNames = new ArrayList<>(); + String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( + this.beanFactory, Object.class, true, false); + for (String beanName : beanNames) { + if (!isEligibleBean(beanName)) { + continue; + } + // We must be careful not to instantiate beans eagerly as in this case they + // would be cached by the Spring container but would not have been weaved. + Class beanType = this.beanFactory.getType(beanName, false); + if (beanType == null) { + 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 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)); + } + } + } + this.aspectBeanNames = aspectNames; + return advisors; + } + } + } + + if (aspectNames.isEmpty()) { + return Collections.emptyList(); + } + List advisors = new ArrayList<>(); + for (String aspectName : aspectNames) { + List cachedAdvisors = this.advisorsCache.get(aspectName); + if (cachedAdvisors != null) { + advisors.addAll(cachedAdvisors); + } + else { + MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName); + advisors.addAll(this.advisorFactory.getAdvisors(factory)); + } + } + return advisors; + } + + /** + * Return whether the aspect bean with the given name is eligible. + * @param beanName the name of the aspect bean + * @return whether the bean is eligible + */ + protected boolean isEligibleBean(String beanName) { + return true; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/InstantiationModelAwarePointcutAdvisorImpl.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/InstantiationModelAwarePointcutAdvisorImpl.java new file mode 100644 index 0000000..7d89175 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/InstantiationModelAwarePointcutAdvisorImpl.java @@ -0,0 +1,304 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.annotation; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.lang.reflect.Method; + +import org.aopalliance.aop.Advice; +import org.aspectj.lang.reflect.PerClauseKind; + +import org.springframework.aop.Pointcut; +import org.springframework.aop.aspectj.AspectJExpressionPointcut; +import org.springframework.aop.aspectj.AspectJPrecedenceInformation; +import org.springframework.aop.aspectj.InstantiationModelAwarePointcutAdvisor; +import org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory.AspectJAnnotation; +import org.springframework.aop.support.DynamicMethodMatcherPointcut; +import org.springframework.aop.support.Pointcuts; +import org.springframework.lang.Nullable; + +/** + * Internal implementation of AspectJPointcutAdvisor. + * Note that there will be one instance of this advisor for each target method. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + */ +@SuppressWarnings("serial") +final class InstantiationModelAwarePointcutAdvisorImpl + implements InstantiationModelAwarePointcutAdvisor, AspectJPrecedenceInformation, Serializable { + + private static final Advice EMPTY_ADVICE = new Advice() {}; + + + private final AspectJExpressionPointcut declaredPointcut; + + private final Class declaringClass; + + private final String methodName; + + private final Class[] parameterTypes; + + private transient Method aspectJAdviceMethod; + + private final AspectJAdvisorFactory aspectJAdvisorFactory; + + private final MetadataAwareAspectInstanceFactory aspectInstanceFactory; + + private final int declarationOrder; + + private final String aspectName; + + private final Pointcut pointcut; + + private final boolean lazy; + + @Nullable + private Advice instantiatedAdvice; + + @Nullable + private Boolean isBeforeAdvice; + + @Nullable + private Boolean isAfterAdvice; + + + public InstantiationModelAwarePointcutAdvisorImpl(AspectJExpressionPointcut declaredPointcut, + Method aspectJAdviceMethod, AspectJAdvisorFactory aspectJAdvisorFactory, + MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) { + + this.declaredPointcut = declaredPointcut; + this.declaringClass = aspectJAdviceMethod.getDeclaringClass(); + this.methodName = aspectJAdviceMethod.getName(); + this.parameterTypes = aspectJAdviceMethod.getParameterTypes(); + this.aspectJAdviceMethod = aspectJAdviceMethod; + this.aspectJAdvisorFactory = aspectJAdvisorFactory; + this.aspectInstanceFactory = aspectInstanceFactory; + this.declarationOrder = declarationOrder; + this.aspectName = aspectName; + + if (aspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) { + // Static part of the pointcut is a lazy type. + Pointcut preInstantiationPointcut = Pointcuts.union( + aspectInstanceFactory.getAspectMetadata().getPerClausePointcut(), this.declaredPointcut); + + // Make it dynamic: must mutate from pre-instantiation to post-instantiation state. + // If it's not a dynamic pointcut, it may be optimized out + // by the Spring AOP infrastructure after the first evaluation. + this.pointcut = new PerTargetInstantiationModelPointcut( + this.declaredPointcut, preInstantiationPointcut, aspectInstanceFactory); + this.lazy = true; + } + else { + // A singleton aspect. + this.pointcut = this.declaredPointcut; + this.lazy = false; + this.instantiatedAdvice = instantiateAdvice(this.declaredPointcut); + } + } + + + /** + * The pointcut for Spring AOP to use. + * Actual behaviour of the pointcut will change depending on the state of the advice. + */ + @Override + public Pointcut getPointcut() { + return this.pointcut; + } + + @Override + public boolean isLazy() { + return this.lazy; + } + + @Override + public synchronized boolean isAdviceInstantiated() { + return (this.instantiatedAdvice != null); + } + + /** + * Lazily instantiate advice if necessary. + */ + @Override + public synchronized Advice getAdvice() { + if (this.instantiatedAdvice == null) { + this.instantiatedAdvice = instantiateAdvice(this.declaredPointcut); + } + return this.instantiatedAdvice; + } + + private Advice instantiateAdvice(AspectJExpressionPointcut pointcut) { + Advice advice = this.aspectJAdvisorFactory.getAdvice(this.aspectJAdviceMethod, pointcut, + this.aspectInstanceFactory, this.declarationOrder, this.aspectName); + return (advice != null ? advice : EMPTY_ADVICE); + } + + /** + * This is only of interest for Spring AOP: AspectJ instantiation semantics + * are much richer. In AspectJ terminology, all a return of {@code true} + * means here is that the aspect is not a SINGLETON. + */ + @Override + public boolean isPerInstance() { + return (getAspectMetadata().getAjType().getPerClause().getKind() != PerClauseKind.SINGLETON); + } + + /** + * Return the AspectJ AspectMetadata for this advisor. + */ + public AspectMetadata getAspectMetadata() { + return this.aspectInstanceFactory.getAspectMetadata(); + } + + public MetadataAwareAspectInstanceFactory getAspectInstanceFactory() { + return this.aspectInstanceFactory; + } + + public AspectJExpressionPointcut getDeclaredPointcut() { + return this.declaredPointcut; + } + + @Override + public int getOrder() { + return this.aspectInstanceFactory.getOrder(); + } + + @Override + public String getAspectName() { + return this.aspectName; + } + + @Override + public int getDeclarationOrder() { + return this.declarationOrder; + } + + @Override + public boolean isBeforeAdvice() { + if (this.isBeforeAdvice == null) { + determineAdviceType(); + } + return this.isBeforeAdvice; + } + + @Override + public boolean isAfterAdvice() { + if (this.isAfterAdvice == null) { + determineAdviceType(); + } + return this.isAfterAdvice; + } + + /** + * Duplicates some logic from getAdvice, but importantly does not force + * creation of the advice. + */ + private void determineAdviceType() { + AspectJAnnotation aspectJAnnotation = + AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(this.aspectJAdviceMethod); + if (aspectJAnnotation == null) { + this.isBeforeAdvice = false; + this.isAfterAdvice = false; + } + else { + switch (aspectJAnnotation.getAnnotationType()) { + case AtPointcut: + case AtAround: + this.isBeforeAdvice = false; + this.isAfterAdvice = false; + break; + case AtBefore: + this.isBeforeAdvice = true; + this.isAfterAdvice = false; + break; + case AtAfter: + case AtAfterReturning: + case AtAfterThrowing: + this.isBeforeAdvice = false; + this.isAfterAdvice = true; + break; + } + } + } + + + private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException { + inputStream.defaultReadObject(); + try { + this.aspectJAdviceMethod = this.declaringClass.getMethod(this.methodName, this.parameterTypes); + } + catch (NoSuchMethodException ex) { + throw new IllegalStateException("Failed to find advice method on deserialization", ex); + } + } + + @Override + public String toString() { + return "InstantiationModelAwarePointcutAdvisor: expression [" + getDeclaredPointcut().getExpression() + + "]; advice method [" + this.aspectJAdviceMethod + "]; perClauseKind=" + + this.aspectInstanceFactory.getAspectMetadata().getAjType().getPerClause().getKind(); + } + + + /** + * Pointcut implementation that changes its behaviour when the advice is instantiated. + * Note that this is a dynamic pointcut; otherwise it might be optimized out + * if it does not at first match statically. + */ + private static final class PerTargetInstantiationModelPointcut extends DynamicMethodMatcherPointcut { + + private final AspectJExpressionPointcut declaredPointcut; + + private final Pointcut preInstantiationPointcut; + + @Nullable + private LazySingletonAspectInstanceFactoryDecorator aspectInstanceFactory; + + public PerTargetInstantiationModelPointcut(AspectJExpressionPointcut declaredPointcut, + Pointcut preInstantiationPointcut, MetadataAwareAspectInstanceFactory aspectInstanceFactory) { + + this.declaredPointcut = declaredPointcut; + this.preInstantiationPointcut = preInstantiationPointcut; + if (aspectInstanceFactory instanceof LazySingletonAspectInstanceFactoryDecorator) { + this.aspectInstanceFactory = (LazySingletonAspectInstanceFactoryDecorator) aspectInstanceFactory; + } + } + + @Override + public boolean matches(Method method, Class targetClass) { + // We're either instantiated and matching on declared pointcut, + // or uninstantiated matching on either pointcut... + return (isAspectMaterialized() && this.declaredPointcut.matches(method, targetClass)) || + this.preInstantiationPointcut.getMethodMatcher().matches(method, targetClass); + } + + @Override + public boolean matches(Method method, Class targetClass, Object... args) { + // This can match only on declared pointcut. + return (isAspectMaterialized() && this.declaredPointcut.matches(method, targetClass)); + } + + private boolean isAspectMaterialized() { + return (this.aspectInstanceFactory == null || this.aspectInstanceFactory.isMaterialized()); + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/LazySingletonAspectInstanceFactoryDecorator.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/LazySingletonAspectInstanceFactoryDecorator.java new file mode 100644 index 0000000..73ba36c --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/LazySingletonAspectInstanceFactoryDecorator.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.annotation; + +import java.io.Serializable; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Decorator to cause a {@link MetadataAwareAspectInstanceFactory} to instantiate only once. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + */ +@SuppressWarnings("serial") +public class LazySingletonAspectInstanceFactoryDecorator implements MetadataAwareAspectInstanceFactory, Serializable { + + private final MetadataAwareAspectInstanceFactory maaif; + + @Nullable + private volatile Object materialized; + + + /** + * Create a new lazily initializing decorator for the given AspectInstanceFactory. + * @param maaif the MetadataAwareAspectInstanceFactory to decorate + */ + public LazySingletonAspectInstanceFactoryDecorator(MetadataAwareAspectInstanceFactory maaif) { + Assert.notNull(maaif, "AspectInstanceFactory must not be null"); + this.maaif = maaif; + } + + + @Override + public Object getAspectInstance() { + Object aspectInstance = this.materialized; + if (aspectInstance == null) { + Object mutex = this.maaif.getAspectCreationMutex(); + if (mutex == null) { + aspectInstance = this.maaif.getAspectInstance(); + this.materialized = aspectInstance; + } + else { + synchronized (mutex) { + aspectInstance = this.materialized; + if (aspectInstance == null) { + aspectInstance = this.maaif.getAspectInstance(); + this.materialized = aspectInstance; + } + } + } + } + return aspectInstance; + } + + public boolean isMaterialized() { + return (this.materialized != null); + } + + @Override + @Nullable + public ClassLoader getAspectClassLoader() { + return this.maaif.getAspectClassLoader(); + } + + @Override + public AspectMetadata getAspectMetadata() { + return this.maaif.getAspectMetadata(); + } + + @Override + @Nullable + public Object getAspectCreationMutex() { + return this.maaif.getAspectCreationMutex(); + } + + @Override + public int getOrder() { + return this.maaif.getOrder(); + } + + + @Override + public String toString() { + return "LazySingletonAspectInstanceFactoryDecorator: decorating " + this.maaif; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/MetadataAwareAspectInstanceFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/MetadataAwareAspectInstanceFactory.java new file mode 100644 index 0000000..5a67f7f --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/MetadataAwareAspectInstanceFactory.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.annotation; + +import org.springframework.aop.aspectj.AspectInstanceFactory; +import org.springframework.lang.Nullable; + +/** + * Subinterface of {@link org.springframework.aop.aspectj.AspectInstanceFactory} + * that returns {@link AspectMetadata} associated with AspectJ-annotated classes. + * + *

Ideally, AspectInstanceFactory would include this method itself, but because + * AspectMetadata uses Java-5-only {@link org.aspectj.lang.reflect.AjType}, + * we need to split out this subinterface. + * + * @author Rod Johnson + * @since 2.0 + * @see AspectMetadata + * @see org.aspectj.lang.reflect.AjType + */ +public interface MetadataAwareAspectInstanceFactory extends AspectInstanceFactory { + + /** + * Return the AspectJ AspectMetadata for this factory's aspect. + * @return the aspect metadata + */ + AspectMetadata getAspectMetadata(); + + /** + * Return the best possible creation mutex for this factory. + * @return the mutex object (may be {@code null} for no mutex to use) + * @since 4.3 + */ + @Nullable + Object getAspectCreationMutex(); + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/NotAnAtAspectException.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/NotAnAtAspectException.java new file mode 100644 index 0000000..7db2a4c --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/NotAnAtAspectException.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.annotation; + +import org.springframework.aop.framework.AopConfigException; + +/** + * Extension of AopConfigException thrown when trying to perform + * an advisor generation operation on a class that is not an + * AspectJ annotation-style aspect. + * + * @author Rod Johnson + * @since 2.0 + */ +@SuppressWarnings("serial") +public class NotAnAtAspectException extends AopConfigException { + + private final Class nonAspectClass; + + + /** + * Create a new NotAnAtAspectException for the given class. + * @param nonAspectClass the offending class + */ + public NotAnAtAspectException(Class nonAspectClass) { + super(nonAspectClass.getName() + " is not an @AspectJ aspect"); + this.nonAspectClass = nonAspectClass; + } + + /** + * Returns the offending class. + */ + public Class getNonAspectClass() { + return this.nonAspectClass; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/PrototypeAspectInstanceFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/PrototypeAspectInstanceFactory.java new file mode 100644 index 0000000..ee29552 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/PrototypeAspectInstanceFactory.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.annotation; + +import java.io.Serializable; + +import org.springframework.beans.factory.BeanFactory; + +/** + * {@link org.springframework.aop.aspectj.AspectInstanceFactory} backed by a + * {@link BeanFactory}-provided prototype, enforcing prototype semantics. + * + *

Note that this may instantiate multiple times, which probably won't give the + * semantics you expect. Use a {@link LazySingletonAspectInstanceFactoryDecorator} + * to wrap this to ensure only one new aspect comes back. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.beans.factory.BeanFactory + * @see LazySingletonAspectInstanceFactoryDecorator + */ +@SuppressWarnings("serial") +public class PrototypeAspectInstanceFactory extends BeanFactoryAspectInstanceFactory implements Serializable { + + /** + * Create a PrototypeAspectInstanceFactory. AspectJ will be called to + * introspect to create AJType metadata using the type returned for the + * given bean name from the BeanFactory. + * @param beanFactory the BeanFactory to obtain instance(s) from + * @param name the name of the bean + */ + public PrototypeAspectInstanceFactory(BeanFactory beanFactory, String name) { + super(beanFactory, name); + if (!beanFactory.isPrototype(name)) { + throw new IllegalArgumentException( + "Cannot use PrototypeAspectInstanceFactory with bean named '" + name + "': not a prototype"); + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java new file mode 100644 index 0000000..c1c10c9 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java @@ -0,0 +1,329 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.annotation; + +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import org.aopalliance.aop.Advice; +import org.aspectj.lang.annotation.After; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.DeclareParents; +import org.aspectj.lang.annotation.Pointcut; + +import org.springframework.aop.Advisor; +import org.springframework.aop.MethodBeforeAdvice; +import org.springframework.aop.aspectj.AbstractAspectJAdvice; +import org.springframework.aop.aspectj.AspectJAfterAdvice; +import org.springframework.aop.aspectj.AspectJAfterReturningAdvice; +import org.springframework.aop.aspectj.AspectJAfterThrowingAdvice; +import org.springframework.aop.aspectj.AspectJAroundAdvice; +import org.springframework.aop.aspectj.AspectJExpressionPointcut; +import org.springframework.aop.aspectj.AspectJMethodBeforeAdvice; +import org.springframework.aop.aspectj.DeclareParentsAdvisor; +import org.springframework.aop.framework.AopConfigException; +import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConvertingComparator; +import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.ReflectionUtils.MethodFilter; +import org.springframework.util.StringUtils; +import org.springframework.util.comparator.InstanceComparator; + +/** + * Factory that can create Spring AOP Advisors given AspectJ classes from + * classes honoring AspectJ's annotation syntax, using reflection to invoke the + * corresponding advice methods. + * + * @author Rod Johnson + * @author Adrian Colyer + * @author Juergen Hoeller + * @author Ramnivas Laddad + * @author Phillip Webb + * @author Sam Brannen + * @since 2.0 + */ +@SuppressWarnings("serial") +public class ReflectiveAspectJAdvisorFactory extends AbstractAspectJAdvisorFactory implements Serializable { + + // Exclude @Pointcut methods + private static final MethodFilter adviceMethodFilter = ReflectionUtils.USER_DECLARED_METHODS + .and(method -> (AnnotationUtils.getAnnotation(method, Pointcut.class) == null)); + + private static final Comparator adviceMethodComparator; + + static { + // Note: although @After is ordered before @AfterReturning and @AfterThrowing, + // an @After advice method will actually be invoked after @AfterReturning and + // @AfterThrowing methods due to the fact that AspectJAfterAdvice.invoke(MethodInvocation) + // invokes proceed() in a `try` block and only invokes the @After advice method + // in a corresponding `finally` block. + Comparator adviceKindComparator = new ConvertingComparator<>( + new InstanceComparator<>( + Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class), + (Converter) method -> { + AspectJAnnotation ann = AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(method); + return (ann != null ? ann.getAnnotation() : null); + }); + Comparator methodNameComparator = new ConvertingComparator<>(Method::getName); + adviceMethodComparator = adviceKindComparator.thenComparing(methodNameComparator); + } + + + @Nullable + private final BeanFactory beanFactory; + + + /** + * Create a new {@code ReflectiveAspectJAdvisorFactory}. + */ + public ReflectiveAspectJAdvisorFactory() { + this(null); + } + + /** + * Create a new {@code ReflectiveAspectJAdvisorFactory}, propagating the given + * {@link BeanFactory} to the created {@link AspectJExpressionPointcut} instances, + * for bean pointcut handling as well as consistent {@link ClassLoader} resolution. + * @param beanFactory the BeanFactory to propagate (may be {@code null}} + * @since 4.3.6 + * @see AspectJExpressionPointcut#setBeanFactory + * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#getBeanClassLoader() + */ + public ReflectiveAspectJAdvisorFactory(@Nullable BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + + @Override + public List getAdvisors(MetadataAwareAspectInstanceFactory aspectInstanceFactory) { + Class aspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass(); + String aspectName = aspectInstanceFactory.getAspectMetadata().getAspectName(); + validate(aspectClass); + + // We need to wrap the MetadataAwareAspectInstanceFactory with a decorator + // so that it will only instantiate once. + MetadataAwareAspectInstanceFactory lazySingletonAspectInstanceFactory = + new LazySingletonAspectInstanceFactoryDecorator(aspectInstanceFactory); + + List advisors = new ArrayList<>(); + for (Method method : getAdvisorMethods(aspectClass)) { + // Prior to Spring Framework 5.2.7, advisors.size() was supplied as the declarationOrderInAspect + // to getAdvisor(...) to represent the "current position" in the declared methods list. + // However, since Java 7 the "current position" is not valid since the JDK no longer + // returns declared methods in the order in which they are declared in the source code. + // Thus, we now hard code the declarationOrderInAspect to 0 for all advice methods + // discovered via reflection in order to support reliable advice ordering across JVM launches. + // Specifically, a value of 0 aligns with the default value used in + // AspectJPrecedenceComparator.getAspectDeclarationOrder(Advisor). + Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, 0, aspectName); + if (advisor != null) { + advisors.add(advisor); + } + } + + // If it's a per target aspect, emit the dummy instantiating aspect. + if (!advisors.isEmpty() && lazySingletonAspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) { + Advisor instantiationAdvisor = new SyntheticInstantiationAdvisor(lazySingletonAspectInstanceFactory); + advisors.add(0, instantiationAdvisor); + } + + // Find introduction fields. + for (Field field : aspectClass.getDeclaredFields()) { + Advisor advisor = getDeclareParentsAdvisor(field); + if (advisor != null) { + advisors.add(advisor); + } + } + + return advisors; + } + + private List getAdvisorMethods(Class aspectClass) { + List methods = new ArrayList<>(); + ReflectionUtils.doWithMethods(aspectClass, methods::add, adviceMethodFilter); + if (methods.size() > 1) { + methods.sort(adviceMethodComparator); + } + return methods; + } + + /** + * Build a {@link org.springframework.aop.aspectj.DeclareParentsAdvisor} + * for the given introduction field. + *

Resulting Advisors will need to be evaluated for targets. + * @param introductionField the field to introspect + * @return the Advisor instance, or {@code null} if not an Advisor + */ + @Nullable + private Advisor getDeclareParentsAdvisor(Field introductionField) { + DeclareParents declareParents = introductionField.getAnnotation(DeclareParents.class); + if (declareParents == null) { + // Not an introduction field + return null; + } + + if (DeclareParents.class == declareParents.defaultImpl()) { + throw new IllegalStateException("'defaultImpl' attribute must be set on DeclareParents"); + } + + return new DeclareParentsAdvisor( + introductionField.getType(), declareParents.value(), declareParents.defaultImpl()); + } + + + @Override + @Nullable + public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aspectInstanceFactory, + int declarationOrderInAspect, String aspectName) { + + validate(aspectInstanceFactory.getAspectMetadata().getAspectClass()); + + AspectJExpressionPointcut expressionPointcut = getPointcut( + candidateAdviceMethod, aspectInstanceFactory.getAspectMetadata().getAspectClass()); + if (expressionPointcut == null) { + return null; + } + + return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod, + this, aspectInstanceFactory, declarationOrderInAspect, aspectName); + } + + @Nullable + private AspectJExpressionPointcut getPointcut(Method candidateAdviceMethod, Class candidateAspectClass) { + AspectJAnnotation aspectJAnnotation = + AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod); + if (aspectJAnnotation == null) { + return null; + } + + AspectJExpressionPointcut ajexp = + new AspectJExpressionPointcut(candidateAspectClass, new String[0], new Class[0]); + ajexp.setExpression(aspectJAnnotation.getPointcutExpression()); + if (this.beanFactory != null) { + ajexp.setBeanFactory(this.beanFactory); + } + return ajexp; + } + + + @Override + @Nullable + public Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut expressionPointcut, + MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) { + + Class candidateAspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass(); + validate(candidateAspectClass); + + AspectJAnnotation aspectJAnnotation = + AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod); + if (aspectJAnnotation == null) { + return null; + } + + // If we get here, we know we have an AspectJ method. + // Check that it's an AspectJ-annotated class + if (!isAspect(candidateAspectClass)) { + throw new AopConfigException("Advice must be declared inside an aspect type: " + + "Offending method '" + candidateAdviceMethod + "' in class [" + + candidateAspectClass.getName() + "]"); + } + + if (logger.isDebugEnabled()) { + logger.debug("Found AspectJ method: " + candidateAdviceMethod); + } + + AbstractAspectJAdvice springAdvice; + + switch (aspectJAnnotation.getAnnotationType()) { + case AtPointcut: + if (logger.isDebugEnabled()) { + logger.debug("Processing pointcut '" + candidateAdviceMethod.getName() + "'"); + } + return null; + case AtAround: + springAdvice = new AspectJAroundAdvice( + candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); + break; + case AtBefore: + springAdvice = new AspectJMethodBeforeAdvice( + candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); + break; + case AtAfter: + springAdvice = new AspectJAfterAdvice( + candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); + break; + case AtAfterReturning: + springAdvice = new AspectJAfterReturningAdvice( + candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); + AfterReturning afterReturningAnnotation = (AfterReturning) aspectJAnnotation.getAnnotation(); + if (StringUtils.hasText(afterReturningAnnotation.returning())) { + springAdvice.setReturningName(afterReturningAnnotation.returning()); + } + break; + case AtAfterThrowing: + springAdvice = new AspectJAfterThrowingAdvice( + candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); + AfterThrowing afterThrowingAnnotation = (AfterThrowing) aspectJAnnotation.getAnnotation(); + if (StringUtils.hasText(afterThrowingAnnotation.throwing())) { + springAdvice.setThrowingName(afterThrowingAnnotation.throwing()); + } + break; + default: + throw new UnsupportedOperationException( + "Unsupported advice type on method: " + candidateAdviceMethod); + } + + // Now to configure the advice... + springAdvice.setAspectName(aspectName); + springAdvice.setDeclarationOrder(declarationOrder); + String[] argNames = this.parameterNameDiscoverer.getParameterNames(candidateAdviceMethod); + if (argNames != null) { + springAdvice.setArgumentNamesFromStringArray(argNames); + } + springAdvice.calculateArgumentBindings(); + + return springAdvice; + } + + + /** + * Synthetic advisor that instantiates the aspect. + * Triggered by per-clause pointcut on non-singleton aspect. + * The advice has no effect. + */ + @SuppressWarnings("serial") + protected static class SyntheticInstantiationAdvisor extends DefaultPointcutAdvisor { + + public SyntheticInstantiationAdvisor(final MetadataAwareAspectInstanceFactory aif) { + super(aif.getAspectMetadata().getPerClausePointcut(), (MethodBeforeAdvice) + (method, args, target) -> aif.getAspectInstance()); + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/SimpleMetadataAwareAspectInstanceFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/SimpleMetadataAwareAspectInstanceFactory.java new file mode 100644 index 0000000..386d791 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/SimpleMetadataAwareAspectInstanceFactory.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.annotation; + +import org.springframework.aop.aspectj.SimpleAspectInstanceFactory; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.OrderUtils; + +/** + * Implementation of {@link MetadataAwareAspectInstanceFactory} that + * creates a new instance of the specified aspect class for every + * {@link #getAspectInstance()} call. + * + * @author Juergen Hoeller + * @since 2.0.4 + */ +public class SimpleMetadataAwareAspectInstanceFactory extends SimpleAspectInstanceFactory + implements MetadataAwareAspectInstanceFactory { + + private final AspectMetadata metadata; + + + /** + * Create a new SimpleMetadataAwareAspectInstanceFactory for the given aspect class. + * @param aspectClass the aspect class + * @param aspectName the aspect name + */ + public SimpleMetadataAwareAspectInstanceFactory(Class aspectClass, String aspectName) { + super(aspectClass); + this.metadata = new AspectMetadata(aspectClass, aspectName); + } + + + @Override + public final AspectMetadata getAspectMetadata() { + return this.metadata; + } + + @Override + public Object getAspectCreationMutex() { + return this; + } + + @Override + protected int getOrderForAspectClass(Class aspectClass) { + return OrderUtils.getOrder(aspectClass, Ordered.LOWEST_PRECEDENCE); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/SingletonMetadataAwareAspectInstanceFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/SingletonMetadataAwareAspectInstanceFactory.java new file mode 100644 index 0000000..4dc30e1 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/SingletonMetadataAwareAspectInstanceFactory.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.annotation; + +import java.io.Serializable; + +import org.springframework.aop.aspectj.SingletonAspectInstanceFactory; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.OrderUtils; + +/** + * Implementation of {@link MetadataAwareAspectInstanceFactory} that is backed + * by a specified singleton object, returning the same instance for every + * {@link #getAspectInstance()} call. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + * @see SimpleMetadataAwareAspectInstanceFactory + */ +@SuppressWarnings("serial") +public class SingletonMetadataAwareAspectInstanceFactory extends SingletonAspectInstanceFactory + implements MetadataAwareAspectInstanceFactory, Serializable { + + private final AspectMetadata metadata; + + + /** + * Create a new SingletonMetadataAwareAspectInstanceFactory for the given aspect. + * @param aspectInstance the singleton aspect instance + * @param aspectName the name of the aspect + */ + public SingletonMetadataAwareAspectInstanceFactory(Object aspectInstance, String aspectName) { + super(aspectInstance); + this.metadata = new AspectMetadata(aspectInstance.getClass(), aspectName); + } + + + @Override + public final AspectMetadata getAspectMetadata() { + return this.metadata; + } + + @Override + public Object getAspectCreationMutex() { + return this; + } + + @Override + protected int getOrderForAspectClass(Class aspectClass) { + return OrderUtils.getOrder(aspectClass, Ordered.LOWEST_PRECEDENCE); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/package-info.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/package-info.java new file mode 100644 index 0000000..b5cf524 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/package-info.java @@ -0,0 +1,11 @@ +/** + * Classes enabling AspectJ 5 @Annotated classes to be used in Spring AOP. + * + *

Normally to be used through an AspectJAutoProxyCreator rather than directly. + */ +@NonNullApi +@NonNullFields +package org.springframework.aop.aspectj.annotation; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/autoproxy/AspectJAwareAdvisorAutoProxyCreator.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/autoproxy/AspectJAwareAdvisorAutoProxyCreator.java new file mode 100644 index 0000000..2d2aabd --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/autoproxy/AspectJAwareAdvisorAutoProxyCreator.java @@ -0,0 +1,162 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.autoproxy; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import org.aopalliance.aop.Advice; +import org.aspectj.util.PartialOrder; +import org.aspectj.util.PartialOrder.PartialComparable; + +import org.springframework.aop.Advisor; +import org.springframework.aop.aspectj.AbstractAspectJAdvice; +import org.springframework.aop.aspectj.AspectJPointcutAdvisor; +import org.springframework.aop.aspectj.AspectJProxyUtils; +import org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator; +import org.springframework.aop.interceptor.ExposeInvocationInterceptor; +import org.springframework.core.Ordered; +import org.springframework.util.ClassUtils; + +/** + * {@link org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator} + * subclass that exposes AspectJ's invocation context and understands AspectJ's rules + * for advice precedence when multiple pieces of advice come from the same aspect. + * + * @author Adrian Colyer + * @author Juergen Hoeller + * @author Ramnivas Laddad + * @since 2.0 + */ +@SuppressWarnings("serial") +public class AspectJAwareAdvisorAutoProxyCreator extends AbstractAdvisorAutoProxyCreator { + + private static final Comparator DEFAULT_PRECEDENCE_COMPARATOR = new AspectJPrecedenceComparator(); + + + /** + * Sort the supplied {@link Advisor} instances according to AspectJ precedence. + *

If two pieces of advice come from the same aspect, they will have the same + * order. Advice from the same aspect is then further ordered according to the + * following rules: + *

+ *

Important: Advisors are sorted in precedence order, from highest + * precedence to lowest. "On the way in" to a join point, the highest precedence + * advisor should run first. "On the way out" of a join point, the highest + * precedence advisor should run last. + */ + @Override + protected List sortAdvisors(List advisors) { + List partiallyComparableAdvisors = new ArrayList<>(advisors.size()); + for (Advisor advisor : advisors) { + partiallyComparableAdvisors.add( + new PartiallyComparableAdvisorHolder(advisor, DEFAULT_PRECEDENCE_COMPARATOR)); + } + List sorted = PartialOrder.sort(partiallyComparableAdvisors); + if (sorted != null) { + List result = new ArrayList<>(advisors.size()); + for (PartiallyComparableAdvisorHolder pcAdvisor : sorted) { + result.add(pcAdvisor.getAdvisor()); + } + return result; + } + else { + return super.sortAdvisors(advisors); + } + } + + /** + * Add an {@link ExposeInvocationInterceptor} to the beginning of the advice chain. + *

This additional advice is needed when using AspectJ pointcut expressions + * and when using AspectJ-style advice. + */ + @Override + protected void extendAdvisors(List candidateAdvisors) { + AspectJProxyUtils.makeAdvisorChainAspectJCapableIfNecessary(candidateAdvisors); + } + + @Override + protected boolean shouldSkip(Class beanClass, String beanName) { + // TODO: Consider optimization by caching the list of the aspect names + List candidateAdvisors = findCandidateAdvisors(); + for (Advisor advisor : candidateAdvisors) { + if (advisor instanceof AspectJPointcutAdvisor && + ((AspectJPointcutAdvisor) advisor).getAspectName().equals(beanName)) { + return true; + } + } + return super.shouldSkip(beanClass, beanName); + } + + + /** + * Implements AspectJ's {@link PartialComparable} interface for defining partial orderings. + */ + private static class PartiallyComparableAdvisorHolder implements PartialComparable { + + private final Advisor advisor; + + private final Comparator comparator; + + public PartiallyComparableAdvisorHolder(Advisor advisor, Comparator comparator) { + this.advisor = advisor; + this.comparator = comparator; + } + + @Override + public int compareTo(Object obj) { + Advisor otherAdvisor = ((PartiallyComparableAdvisorHolder) obj).advisor; + return this.comparator.compare(this.advisor, otherAdvisor); + } + + @Override + public int fallbackCompareTo(Object obj) { + return 0; + } + + public Advisor getAdvisor() { + return this.advisor; + } + + @Override + public String toString() { + Advice advice = this.advisor.getAdvice(); + StringBuilder sb = new StringBuilder(ClassUtils.getShortName(advice.getClass())); + boolean appended = false; + if (this.advisor instanceof Ordered) { + sb.append(": order = ").append(((Ordered) this.advisor).getOrder()); + appended = true; + } + if (advice instanceof AbstractAspectJAdvice) { + sb.append(!appended ? ": " : ", "); + AbstractAspectJAdvice ajAdvice = (AbstractAspectJAdvice) advice; + sb.append("aspect name = "); + sb.append(ajAdvice.getAspectName()); + sb.append(", declaration order = "); + sb.append(ajAdvice.getDeclarationOrder()); + } + return sb.toString(); + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/autoproxy/AspectJPrecedenceComparator.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/autoproxy/AspectJPrecedenceComparator.java new file mode 100644 index 0000000..2d243fa --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/autoproxy/AspectJPrecedenceComparator.java @@ -0,0 +1,147 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.autoproxy; + +import java.util.Comparator; + +import org.springframework.aop.Advisor; +import org.springframework.aop.aspectj.AspectJAopUtils; +import org.springframework.aop.aspectj.AspectJPrecedenceInformation; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.util.Assert; + +/** + * Orders AspectJ advice/advisors by precedence (not invocation order). + * + *

Given two pieces of advice, {@code A} and {@code B}: + *

    + *
  • If {@code A} and {@code B} are defined in different aspects, then the advice + * in the aspect with the lowest order value has the highest precedence.
  • + *
  • If {@code A} and {@code B} are defined in the same aspect, if one of + * {@code A} or {@code B} is a form of after advice, then the advice declared + * last in the aspect has the highest precedence. If neither {@code A} nor {@code B} + * is a form of after advice, then the advice declared first in the aspect + * has the highest precedence.
  • + *
+ * + *

Important: This comparator is used with AspectJ's + * {@link org.aspectj.util.PartialOrder PartialOrder} sorting utility. Thus, unlike + * a normal {@link Comparator}, a return value of {@code 0} from this comparator + * means we don't care about the ordering, not that the two elements must be sorted + * identically. + * + * @author Adrian Colyer + * @author Juergen Hoeller + * @since 2.0 + */ +class AspectJPrecedenceComparator implements Comparator { + + private static final int HIGHER_PRECEDENCE = -1; + + private static final int SAME_PRECEDENCE = 0; + + private static final int LOWER_PRECEDENCE = 1; + + + private final Comparator advisorComparator; + + + /** + * Create a default {@code AspectJPrecedenceComparator}. + */ + public AspectJPrecedenceComparator() { + this.advisorComparator = AnnotationAwareOrderComparator.INSTANCE; + } + + /** + * Create an {@code AspectJPrecedenceComparator}, using the given {@link Comparator} + * for comparing {@link org.springframework.aop.Advisor} instances. + * @param advisorComparator the {@code Comparator} to use for advisors + */ + public AspectJPrecedenceComparator(Comparator advisorComparator) { + Assert.notNull(advisorComparator, "Advisor comparator must not be null"); + this.advisorComparator = advisorComparator; + } + + + @Override + public int compare(Advisor o1, Advisor o2) { + int advisorPrecedence = this.advisorComparator.compare(o1, o2); + if (advisorPrecedence == SAME_PRECEDENCE && declaredInSameAspect(o1, o2)) { + advisorPrecedence = comparePrecedenceWithinAspect(o1, o2); + } + return advisorPrecedence; + } + + private int comparePrecedenceWithinAspect(Advisor advisor1, Advisor advisor2) { + boolean oneOrOtherIsAfterAdvice = + (AspectJAopUtils.isAfterAdvice(advisor1) || AspectJAopUtils.isAfterAdvice(advisor2)); + int adviceDeclarationOrderDelta = getAspectDeclarationOrder(advisor1) - getAspectDeclarationOrder(advisor2); + + if (oneOrOtherIsAfterAdvice) { + // the advice declared last has higher precedence + if (adviceDeclarationOrderDelta < 0) { + // advice1 was declared before advice2 + // so advice1 has lower precedence + return LOWER_PRECEDENCE; + } + else if (adviceDeclarationOrderDelta == 0) { + return SAME_PRECEDENCE; + } + else { + return HIGHER_PRECEDENCE; + } + } + else { + // the advice declared first has higher precedence + if (adviceDeclarationOrderDelta < 0) { + // advice1 was declared before advice2 + // so advice1 has higher precedence + return HIGHER_PRECEDENCE; + } + else if (adviceDeclarationOrderDelta == 0) { + return SAME_PRECEDENCE; + } + else { + return LOWER_PRECEDENCE; + } + } + } + + private boolean declaredInSameAspect(Advisor advisor1, Advisor advisor2) { + return (hasAspectName(advisor1) && hasAspectName(advisor2) && + getAspectName(advisor1).equals(getAspectName(advisor2))); + } + + private boolean hasAspectName(Advisor advisor) { + return (advisor instanceof AspectJPrecedenceInformation || + advisor.getAdvice() instanceof AspectJPrecedenceInformation); + } + + // pre-condition is that hasAspectName returned true + private String getAspectName(Advisor advisor) { + AspectJPrecedenceInformation precedenceInfo = AspectJAopUtils.getAspectJPrecedenceInformationFor(advisor); + Assert.state(precedenceInfo != null, () -> "Unresolvable AspectJPrecedenceInformation for " + advisor); + return precedenceInfo.getAspectName(); + } + + private int getAspectDeclarationOrder(Advisor advisor) { + AspectJPrecedenceInformation precedenceInfo = AspectJAopUtils.getAspectJPrecedenceInformationFor(advisor); + return (precedenceInfo != null ? precedenceInfo.getDeclarationOrder() : 0); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/autoproxy/package-info.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/autoproxy/package-info.java new file mode 100644 index 0000000..d83cd88 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/autoproxy/package-info.java @@ -0,0 +1,10 @@ +/** + * Base classes enabling auto-proxying based on AspectJ. + * Support for AspectJ annotation aspects resides in the "aspectj.annotation" package. + */ +@NonNullApi +@NonNullFields +package org.springframework.aop.aspectj.autoproxy; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/package-info.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/package-info.java new file mode 100644 index 0000000..2ffe8b1 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/package-info.java @@ -0,0 +1,16 @@ +/** + * AspectJ integration package. Includes Spring AOP advice implementations for AspectJ 5 + * annotation-style methods, and an AspectJExpressionPointcut: a Spring AOP Pointcut + * implementation that allows use of the AspectJ pointcut expression language with the Spring AOP + * runtime framework. + * + *

Note that use of this package does not require the use of the {@code ajc} compiler + * or AspectJ load-time weaver. It is intended to enable the use of a valuable subset of AspectJ + * functionality, with consistent semantics, with the proxy-based Spring AOP framework. + */ +@NonNullApi +@NonNullFields +package org.springframework.aop.aspectj; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-aop/src/main/java/org/springframework/aop/config/AbstractInterceptorDrivenBeanDefinitionDecorator.java b/spring-aop/src/main/java/org/springframework/aop/config/AbstractInterceptorDrivenBeanDefinitionDecorator.java new file mode 100644 index 0000000..52a00db --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/config/AbstractInterceptorDrivenBeanDefinitionDecorator.java @@ -0,0 +1,130 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.config; + +import java.util.List; + +import org.w3c.dom.Node; + +import org.springframework.aop.framework.ProxyFactoryBean; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.BeanDefinitionDecorator; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * Base implementation for + * {@link org.springframework.beans.factory.xml.BeanDefinitionDecorator BeanDefinitionDecorators} + * wishing to add an {@link org.aopalliance.intercept.MethodInterceptor interceptor} + * to the resulting bean. + * + *

This base class controls the creation of the {@link ProxyFactoryBean} bean definition + * and wraps the original as an inner-bean definition for the {@code target} property + * of {@link ProxyFactoryBean}. + * + *

Chaining is correctly handled, ensuring that only one {@link ProxyFactoryBean} definition + * is created. If a previous {@link org.springframework.beans.factory.xml.BeanDefinitionDecorator} + * already created the {@link org.springframework.aop.framework.ProxyFactoryBean} then the + * interceptor is simply added to the existing definition. + * + *

Subclasses have only to create the {@code BeanDefinition} to the interceptor that + * they wish to add. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + * @see org.aopalliance.intercept.MethodInterceptor + */ +public abstract class AbstractInterceptorDrivenBeanDefinitionDecorator implements BeanDefinitionDecorator { + + @Override + public final BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder definitionHolder, ParserContext parserContext) { + BeanDefinitionRegistry registry = parserContext.getRegistry(); + + // get the root bean name - will be the name of the generated proxy factory bean + String existingBeanName = definitionHolder.getBeanName(); + BeanDefinition targetDefinition = definitionHolder.getBeanDefinition(); + BeanDefinitionHolder targetHolder = new BeanDefinitionHolder(targetDefinition, existingBeanName + ".TARGET"); + + // delegate to subclass for interceptor definition + BeanDefinition interceptorDefinition = createInterceptorDefinition(node); + + // generate name and register the interceptor + String interceptorName = existingBeanName + '.' + getInterceptorNameSuffix(interceptorDefinition); + BeanDefinitionReaderUtils.registerBeanDefinition( + new BeanDefinitionHolder(interceptorDefinition, interceptorName), registry); + + BeanDefinitionHolder result = definitionHolder; + + if (!isProxyFactoryBeanDefinition(targetDefinition)) { + // create the proxy definition + RootBeanDefinition proxyDefinition = new RootBeanDefinition(); + // create proxy factory bean definition + proxyDefinition.setBeanClass(ProxyFactoryBean.class); + proxyDefinition.setScope(targetDefinition.getScope()); + proxyDefinition.setLazyInit(targetDefinition.isLazyInit()); + // set the target + proxyDefinition.setDecoratedDefinition(targetHolder); + proxyDefinition.getPropertyValues().add("target", targetHolder); + // create the interceptor names list + proxyDefinition.getPropertyValues().add("interceptorNames", new ManagedList()); + // copy autowire settings from original bean definition. + proxyDefinition.setAutowireCandidate(targetDefinition.isAutowireCandidate()); + proxyDefinition.setPrimary(targetDefinition.isPrimary()); + if (targetDefinition instanceof AbstractBeanDefinition) { + proxyDefinition.copyQualifiersFrom((AbstractBeanDefinition) targetDefinition); + } + // wrap it in a BeanDefinitionHolder with bean name + result = new BeanDefinitionHolder(proxyDefinition, existingBeanName); + } + + addInterceptorNameToList(interceptorName, result.getBeanDefinition()); + return result; + } + + @SuppressWarnings("unchecked") + private void addInterceptorNameToList(String interceptorName, BeanDefinition beanDefinition) { + List list = (List) beanDefinition.getPropertyValues().get("interceptorNames"); + Assert.state(list != null, "Missing 'interceptorNames' property"); + list.add(interceptorName); + } + + private boolean isProxyFactoryBeanDefinition(BeanDefinition existingDefinition) { + return ProxyFactoryBean.class.getName().equals(existingDefinition.getBeanClassName()); + } + + protected String getInterceptorNameSuffix(BeanDefinition interceptorDefinition) { + String beanClassName = interceptorDefinition.getBeanClassName(); + return (StringUtils.hasLength(beanClassName) ? + StringUtils.uncapitalize(ClassUtils.getShortName(beanClassName)) : ""); + } + + /** + * Subclasses should implement this method to return the {@code BeanDefinition} + * for the interceptor they wish to apply to the bean being decorated. + */ + protected abstract BeanDefinition createInterceptorDefinition(Node node); + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/config/AdviceEntry.java b/spring-aop/src/main/java/org/springframework/aop/config/AdviceEntry.java new file mode 100644 index 0000000..7d9b2ad --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/config/AdviceEntry.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.config; + +import org.springframework.beans.factory.parsing.ParseState; + +/** + * {@link ParseState} entry representing an advice element. + * + * @author Mark Fisher + * @since 2.0 + */ +public class AdviceEntry implements ParseState.Entry { + + private final String kind; + + + /** + * Create a new {@code AdviceEntry} instance. + * @param kind the kind of advice represented by this entry (before, after, around) + */ + public AdviceEntry(String kind) { + this.kind = kind; + } + + + @Override + public String toString() { + return "Advice (" + this.kind + ")"; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/config/AdvisorComponentDefinition.java b/spring-aop/src/main/java/org/springframework/aop/config/AdvisorComponentDefinition.java new file mode 100644 index 0000000..25c8fa2 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/config/AdvisorComponentDefinition.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.config; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanReference; +import org.springframework.beans.factory.parsing.AbstractComponentDefinition; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * {@link org.springframework.beans.factory.parsing.ComponentDefinition} + * that bridges the gap between the advisor bean definition configured + * by the {@code } tag and the component definition + * infrastructure. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +public class AdvisorComponentDefinition extends AbstractComponentDefinition { + + private final String advisorBeanName; + + private final BeanDefinition advisorDefinition; + + private final String description; + + private final BeanReference[] beanReferences; + + private final BeanDefinition[] beanDefinitions; + + + public AdvisorComponentDefinition(String advisorBeanName, BeanDefinition advisorDefinition) { + this(advisorBeanName, advisorDefinition, null); + } + + public AdvisorComponentDefinition( + String advisorBeanName, BeanDefinition advisorDefinition, @Nullable BeanDefinition pointcutDefinition) { + + Assert.notNull(advisorBeanName, "'advisorBeanName' must not be null"); + Assert.notNull(advisorDefinition, "'advisorDefinition' must not be null"); + this.advisorBeanName = advisorBeanName; + this.advisorDefinition = advisorDefinition; + + MutablePropertyValues pvs = advisorDefinition.getPropertyValues(); + BeanReference adviceReference = (BeanReference) pvs.get("adviceBeanName"); + Assert.state(adviceReference != null, "Missing 'adviceBeanName' property"); + + if (pointcutDefinition != null) { + this.beanReferences = new BeanReference[] {adviceReference}; + this.beanDefinitions = new BeanDefinition[] {advisorDefinition, pointcutDefinition}; + this.description = buildDescription(adviceReference, pointcutDefinition); + } + else { + BeanReference pointcutReference = (BeanReference) pvs.get("pointcut"); + Assert.state(pointcutReference != null, "Missing 'pointcut' property"); + this.beanReferences = new BeanReference[] {adviceReference, pointcutReference}; + this.beanDefinitions = new BeanDefinition[] {advisorDefinition}; + this.description = buildDescription(adviceReference, pointcutReference); + } + } + + private String buildDescription(BeanReference adviceReference, BeanDefinition pointcutDefinition) { + return "Advisor "; + } + + private String buildDescription(BeanReference adviceReference, BeanReference pointcutReference) { + return "Advisor "; + } + + + @Override + public String getName() { + return this.advisorBeanName; + } + + @Override + public String getDescription() { + return this.description; + } + + @Override + public BeanDefinition[] getBeanDefinitions() { + return this.beanDefinitions; + } + + @Override + public BeanReference[] getBeanReferences() { + return this.beanReferences; + } + + @Override + @Nullable + public Object getSource() { + return this.advisorDefinition.getSource(); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/config/AdvisorEntry.java b/spring-aop/src/main/java/org/springframework/aop/config/AdvisorEntry.java new file mode 100644 index 0000000..1a8b45c --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/config/AdvisorEntry.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.config; + +import org.springframework.beans.factory.parsing.ParseState; + +/** + * {@link ParseState} entry representing an advisor. + * + * @author Mark Fisher + * @since 2.0 + */ +public class AdvisorEntry implements ParseState.Entry { + + private final String name; + + + /** + * Create a new {@code AdvisorEntry} instance. + * @param name the bean name of the advisor + */ + public AdvisorEntry(String name) { + this.name = name; + } + + + @Override + public String toString() { + return "Advisor '" + this.name + "'"; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/config/AopConfigUtils.java b/spring-aop/src/main/java/org/springframework/aop/config/AopConfigUtils.java new file mode 100644 index 0000000..1bba8f1 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/config/AopConfigUtils.java @@ -0,0 +1,158 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.config; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator; +import org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator; +import org.springframework.aop.framework.autoproxy.InfrastructureAdvisorAutoProxyCreator; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.Ordered; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Utility class for handling registration of AOP auto-proxy creators. + * + *

Only a single auto-proxy creator should be registered yet multiple concrete + * implementations are available. This class provides a simple escalation protocol, + * allowing a caller to request a particular auto-proxy creator and know that creator, + * or a more capable variant thereof, will be registered as a post-processor. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Mark Fisher + * @since 2.5 + * @see AopNamespaceUtils + */ +public abstract class AopConfigUtils { + + /** + * The bean name of the internally managed auto-proxy creator. + */ + public static final String AUTO_PROXY_CREATOR_BEAN_NAME = + "org.springframework.aop.config.internalAutoProxyCreator"; + + /** + * Stores the auto proxy creator classes in escalation order. + */ + private static final List> APC_PRIORITY_LIST = new ArrayList<>(3); + + static { + // Set up the escalation list... + APC_PRIORITY_LIST.add(InfrastructureAdvisorAutoProxyCreator.class); + APC_PRIORITY_LIST.add(AspectJAwareAdvisorAutoProxyCreator.class); + APC_PRIORITY_LIST.add(AnnotationAwareAspectJAutoProxyCreator.class); + } + + + @Nullable + public static BeanDefinition registerAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry) { + return registerAutoProxyCreatorIfNecessary(registry, null); + } + + @Nullable + public static BeanDefinition registerAutoProxyCreatorIfNecessary( + BeanDefinitionRegistry registry, @Nullable Object source) { + + return registerOrEscalateApcAsRequired(InfrastructureAdvisorAutoProxyCreator.class, registry, source); + } + + @Nullable + public static BeanDefinition registerAspectJAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry) { + return registerAspectJAutoProxyCreatorIfNecessary(registry, null); + } + + @Nullable + public static BeanDefinition registerAspectJAutoProxyCreatorIfNecessary( + BeanDefinitionRegistry registry, @Nullable Object source) { + + return registerOrEscalateApcAsRequired(AspectJAwareAdvisorAutoProxyCreator.class, registry, source); + } + + @Nullable + public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry) { + return registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry, null); + } + + @Nullable + public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary( + BeanDefinitionRegistry registry, @Nullable Object source) { + + return registerOrEscalateApcAsRequired(AnnotationAwareAspectJAutoProxyCreator.class, registry, source); + } + + public static void forceAutoProxyCreatorToUseClassProxying(BeanDefinitionRegistry registry) { + if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { + BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); + definition.getPropertyValues().add("proxyTargetClass", Boolean.TRUE); + } + } + + public static void forceAutoProxyCreatorToExposeProxy(BeanDefinitionRegistry registry) { + if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { + BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); + definition.getPropertyValues().add("exposeProxy", Boolean.TRUE); + } + } + + @Nullable + private static BeanDefinition registerOrEscalateApcAsRequired( + Class cls, BeanDefinitionRegistry registry, @Nullable Object source) { + + Assert.notNull(registry, "BeanDefinitionRegistry must not be null"); + + if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { + BeanDefinition apcDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); + if (!cls.getName().equals(apcDefinition.getBeanClassName())) { + int currentPriority = findPriorityForClass(apcDefinition.getBeanClassName()); + int requiredPriority = findPriorityForClass(cls); + if (currentPriority < requiredPriority) { + apcDefinition.setBeanClassName(cls.getName()); + } + } + return null; + } + + RootBeanDefinition beanDefinition = new RootBeanDefinition(cls); + beanDefinition.setSource(source); + beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE); + beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition); + return beanDefinition; + } + + private static int findPriorityForClass(Class clazz) { + return APC_PRIORITY_LIST.indexOf(clazz); + } + + private static int findPriorityForClass(@Nullable String className) { + for (int i = 0; i < APC_PRIORITY_LIST.size(); i++) { + Class clazz = APC_PRIORITY_LIST.get(i); + if (clazz.getName().equals(className)) { + return i; + } + } + throw new IllegalArgumentException( + "Class name [" + className + "] is not a known auto-proxy creator class"); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/config/AopNamespaceHandler.java b/spring-aop/src/main/java/org/springframework/aop/config/AopNamespaceHandler.java new file mode 100644 index 0000000..fa6cc80 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/config/AopNamespaceHandler.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.config; + +import org.springframework.aop.aspectj.AspectJExpressionPointcut; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.NamespaceHandlerSupport; + +/** + * {@code NamespaceHandler} for the {@code aop} namespace. + * + *

Provides a {@link org.springframework.beans.factory.xml.BeanDefinitionParser} for the + * {@code } tag. A {@code config} tag can include nested + * {@code pointcut}, {@code advisor} and {@code aspect} tags. + * + *

The {@code pointcut} tag allows for creation of named + * {@link AspectJExpressionPointcut} beans using a simple syntax: + *

+ * <aop:pointcut id="getNameCalls" expression="execution(* *..ITestBean.getName(..))"/>
+ * 
+ * + *

Using the {@code advisor} tag you can configure an {@link org.springframework.aop.Advisor} + * and have it applied to all relevant beans in you {@link org.springframework.beans.factory.BeanFactory} + * automatically. The {@code advisor} tag supports both in-line and referenced + * {@link org.springframework.aop.Pointcut Pointcuts}: + * + *

+ * <aop:advisor id="getAgeAdvisor"
+ *     pointcut="execution(* *..ITestBean.getAge(..))"
+ *     advice-ref="getAgeCounter"/>
+ *
+ * <aop:advisor id="getNameAdvisor"
+ *     pointcut-ref="getNameCalls"
+ *     advice-ref="getNameCounter"/>
+ * + * @author Rob Harrop + * @author Adrian Colyer + * @author Juergen Hoeller + * @since 2.0 + */ +public class AopNamespaceHandler extends NamespaceHandlerSupport { + + /** + * Register the {@link BeanDefinitionParser BeanDefinitionParsers} for the + * '{@code config}', '{@code spring-configured}', '{@code aspectj-autoproxy}' + * and '{@code scoped-proxy}' tags. + */ + @Override + public void init() { + // In 2.0 XSD as well as in 2.5+ XSDs + registerBeanDefinitionParser("config", new ConfigBeanDefinitionParser()); + registerBeanDefinitionParser("aspectj-autoproxy", new AspectJAutoProxyBeanDefinitionParser()); + registerBeanDefinitionDecorator("scoped-proxy", new ScopedProxyBeanDefinitionDecorator()); + + // Only in 2.0 XSD: moved to context namespace in 2.5+ + registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser()); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/config/AopNamespaceUtils.java b/spring-aop/src/main/java/org/springframework/aop/config/AopNamespaceUtils.java new file mode 100644 index 0000000..5acb1cc --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/config/AopNamespaceUtils.java @@ -0,0 +1,103 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.lang.Nullable; + +/** + * Utility class for handling registration of auto-proxy creators used internally + * by the '{@code aop}' namespace tags. + * + *

Only a single auto-proxy creator should be registered and multiple configuration + * elements may wish to register different concrete implementations. As such this class + * delegates to {@link AopConfigUtils} which provides a simple escalation protocol. + * Callers may request a particular auto-proxy creator and know that creator, + * or a more capable variant thereof, will be registered as a post-processor. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Mark Fisher + * @since 2.0 + * @see AopConfigUtils + */ +public abstract class AopNamespaceUtils { + + /** + * The {@code proxy-target-class} attribute as found on AOP-related XML tags. + */ + public static final String PROXY_TARGET_CLASS_ATTRIBUTE = "proxy-target-class"; + + /** + * The {@code expose-proxy} attribute as found on AOP-related XML tags. + */ + private static final String EXPOSE_PROXY_ATTRIBUTE = "expose-proxy"; + + + public static void registerAutoProxyCreatorIfNecessary( + ParserContext parserContext, Element sourceElement) { + + BeanDefinition beanDefinition = AopConfigUtils.registerAutoProxyCreatorIfNecessary( + parserContext.getRegistry(), parserContext.extractSource(sourceElement)); + useClassProxyingIfNecessary(parserContext.getRegistry(), sourceElement); + registerComponentIfNecessary(beanDefinition, parserContext); + } + + public static void registerAspectJAutoProxyCreatorIfNecessary( + ParserContext parserContext, Element sourceElement) { + + BeanDefinition beanDefinition = AopConfigUtils.registerAspectJAutoProxyCreatorIfNecessary( + parserContext.getRegistry(), parserContext.extractSource(sourceElement)); + useClassProxyingIfNecessary(parserContext.getRegistry(), sourceElement); + registerComponentIfNecessary(beanDefinition, parserContext); + } + + public static void registerAspectJAnnotationAutoProxyCreatorIfNecessary( + ParserContext parserContext, Element sourceElement) { + + BeanDefinition beanDefinition = AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary( + parserContext.getRegistry(), parserContext.extractSource(sourceElement)); + useClassProxyingIfNecessary(parserContext.getRegistry(), sourceElement); + registerComponentIfNecessary(beanDefinition, parserContext); + } + + private static void useClassProxyingIfNecessary(BeanDefinitionRegistry registry, @Nullable Element sourceElement) { + if (sourceElement != null) { + boolean proxyTargetClass = Boolean.parseBoolean(sourceElement.getAttribute(PROXY_TARGET_CLASS_ATTRIBUTE)); + if (proxyTargetClass) { + AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry); + } + boolean exposeProxy = Boolean.parseBoolean(sourceElement.getAttribute(EXPOSE_PROXY_ATTRIBUTE)); + if (exposeProxy) { + AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry); + } + } + } + + private static void registerComponentIfNecessary(@Nullable BeanDefinition beanDefinition, ParserContext parserContext) { + if (beanDefinition != null) { + parserContext.registerComponent( + new BeanComponentDefinition(beanDefinition, AopConfigUtils.AUTO_PROXY_CREATOR_BEAN_NAME)); + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/config/AspectComponentDefinition.java b/spring-aop/src/main/java/org/springframework/aop/config/AspectComponentDefinition.java new file mode 100644 index 0000000..53d0d78 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/config/AspectComponentDefinition.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.config; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanReference; +import org.springframework.beans.factory.parsing.CompositeComponentDefinition; +import org.springframework.lang.Nullable; + +/** + * {@link org.springframework.beans.factory.parsing.ComponentDefinition} + * that holds an aspect definition, including its nested pointcuts. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + * @see #getNestedComponents() + * @see PointcutComponentDefinition + */ +public class AspectComponentDefinition extends CompositeComponentDefinition { + + private final BeanDefinition[] beanDefinitions; + + private final BeanReference[] beanReferences; + + + public AspectComponentDefinition(String aspectName, @Nullable BeanDefinition[] beanDefinitions, + @Nullable BeanReference[] beanReferences, @Nullable Object source) { + + super(aspectName, source); + this.beanDefinitions = (beanDefinitions != null ? beanDefinitions : new BeanDefinition[0]); + this.beanReferences = (beanReferences != null ? beanReferences : new BeanReference[0]); + } + + + @Override + public BeanDefinition[] getBeanDefinitions() { + return this.beanDefinitions; + } + + @Override + public BeanReference[] getBeanReferences() { + return this.beanReferences; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/config/AspectEntry.java b/spring-aop/src/main/java/org/springframework/aop/config/AspectEntry.java new file mode 100644 index 0000000..2d43600 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/config/AspectEntry.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.config; + +import org.springframework.beans.factory.parsing.ParseState; +import org.springframework.util.StringUtils; + +/** + * {@link ParseState} entry representing an aspect. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @since 2.0 + */ +public class AspectEntry implements ParseState.Entry { + + private final String id; + + private final String ref; + + + /** + * Create a new {@code AspectEntry} instance. + * @param id the id of the aspect element + * @param ref the bean name referenced by this aspect element + */ + public AspectEntry(String id, String ref) { + this.id = id; + this.ref = ref; + } + + + @Override + public String toString() { + return "Aspect: " + (StringUtils.hasLength(this.id) ? "id='" + this.id + "'" : "ref='" + this.ref + "'"); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/config/AspectJAutoProxyBeanDefinitionParser.java b/spring-aop/src/main/java/org/springframework/aop/config/AspectJAutoProxyBeanDefinitionParser.java new file mode 100644 index 0000000..4271bec --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/config/AspectJAutoProxyBeanDefinitionParser.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.config; + +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.TypedStringValue; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.lang.Nullable; + +/** + * {@link BeanDefinitionParser} for the {@code aspectj-autoproxy} tag, + * enabling the automatic application of @AspectJ-style aspects found in + * the {@link org.springframework.beans.factory.BeanFactory}. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +class AspectJAutoProxyBeanDefinitionParser implements BeanDefinitionParser { + + @Override + @Nullable + public BeanDefinition parse(Element element, ParserContext parserContext) { + AopNamespaceUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(parserContext, element); + extendBeanDefinition(element, parserContext); + return null; + } + + private void extendBeanDefinition(Element element, ParserContext parserContext) { + BeanDefinition beanDef = + parserContext.getRegistry().getBeanDefinition(AopConfigUtils.AUTO_PROXY_CREATOR_BEAN_NAME); + if (element.hasChildNodes()) { + addIncludePatterns(element, parserContext, beanDef); + } + } + + private void addIncludePatterns(Element element, ParserContext parserContext, BeanDefinition beanDef) { + ManagedList includePatterns = new ManagedList<>(); + NodeList childNodes = element.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + Node node = childNodes.item(i); + if (node instanceof Element) { + Element includeElement = (Element) node; + TypedStringValue valueHolder = new TypedStringValue(includeElement.getAttribute("name")); + valueHolder.setSource(parserContext.extractSource(includeElement)); + includePatterns.add(valueHolder); + } + } + if (!includePatterns.isEmpty()) { + includePatterns.setSource(parserContext.extractSource(element)); + beanDef.getPropertyValues().add("includePatterns", includePatterns); + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/config/ConfigBeanDefinitionParser.java b/spring-aop/src/main/java/org/springframework/aop/config/ConfigBeanDefinitionParser.java new file mode 100644 index 0000000..de31818 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/config/ConfigBeanDefinitionParser.java @@ -0,0 +1,515 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.config; + +import java.util.ArrayList; +import java.util.List; + +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import org.springframework.aop.aspectj.AspectJAfterAdvice; +import org.springframework.aop.aspectj.AspectJAfterReturningAdvice; +import org.springframework.aop.aspectj.AspectJAfterThrowingAdvice; +import org.springframework.aop.aspectj.AspectJAroundAdvice; +import org.springframework.aop.aspectj.AspectJExpressionPointcut; +import org.springframework.aop.aspectj.AspectJMethodBeforeAdvice; +import org.springframework.aop.aspectj.AspectJPointcutAdvisor; +import org.springframework.aop.aspectj.DeclareParentsAdvisor; +import org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanReference; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.beans.factory.config.RuntimeBeanNameReference; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.parsing.CompositeComponentDefinition; +import org.springframework.beans.factory.parsing.ParseState; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; + +/** + * {@link BeanDefinitionParser} for the {@code } tag. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Adrian Colyer + * @author Mark Fisher + * @author Ramnivas Laddad + * @since 2.0 + */ +class ConfigBeanDefinitionParser implements BeanDefinitionParser { + + private static final String ASPECT = "aspect"; + private static final String EXPRESSION = "expression"; + private static final String ID = "id"; + private static final String POINTCUT = "pointcut"; + private static final String ADVICE_BEAN_NAME = "adviceBeanName"; + private static final String ADVISOR = "advisor"; + private static final String ADVICE_REF = "advice-ref"; + private static final String POINTCUT_REF = "pointcut-ref"; + private static final String REF = "ref"; + private static final String BEFORE = "before"; + private static final String DECLARE_PARENTS = "declare-parents"; + private static final String TYPE_PATTERN = "types-matching"; + private static final String DEFAULT_IMPL = "default-impl"; + private static final String DELEGATE_REF = "delegate-ref"; + private static final String IMPLEMENT_INTERFACE = "implement-interface"; + private static final String AFTER = "after"; + private static final String AFTER_RETURNING_ELEMENT = "after-returning"; + private static final String AFTER_THROWING_ELEMENT = "after-throwing"; + private static final String AROUND = "around"; + private static final String RETURNING = "returning"; + private static final String RETURNING_PROPERTY = "returningName"; + private static final String THROWING = "throwing"; + private static final String THROWING_PROPERTY = "throwingName"; + private static final String ARG_NAMES = "arg-names"; + private static final String ARG_NAMES_PROPERTY = "argumentNames"; + private static final String ASPECT_NAME_PROPERTY = "aspectName"; + private static final String DECLARATION_ORDER_PROPERTY = "declarationOrder"; + private static final String ORDER_PROPERTY = "order"; + private static final int METHOD_INDEX = 0; + private static final int POINTCUT_INDEX = 1; + private static final int ASPECT_INSTANCE_FACTORY_INDEX = 2; + + private ParseState parseState = new ParseState(); + + + @Override + @Nullable + public BeanDefinition parse(Element element, ParserContext parserContext) { + CompositeComponentDefinition compositeDef = + new CompositeComponentDefinition(element.getTagName(), parserContext.extractSource(element)); + parserContext.pushContainingComponent(compositeDef); + + configureAutoProxyCreator(parserContext, element); + + List childElts = DomUtils.getChildElements(element); + for (Element elt: childElts) { + String localName = parserContext.getDelegate().getLocalName(elt); + if (POINTCUT.equals(localName)) { + parsePointcut(elt, parserContext); + } + else if (ADVISOR.equals(localName)) { + parseAdvisor(elt, parserContext); + } + else if (ASPECT.equals(localName)) { + parseAspect(elt, parserContext); + } + } + + parserContext.popAndRegisterContainingComponent(); + return null; + } + + /** + * Configures the auto proxy creator needed to support the {@link BeanDefinition BeanDefinitions} + * created by the '{@code }' tag. Will force class proxying if the + * '{@code proxy-target-class}' attribute is set to '{@code true}'. + * @see AopNamespaceUtils + */ + private void configureAutoProxyCreator(ParserContext parserContext, Element element) { + AopNamespaceUtils.registerAspectJAutoProxyCreatorIfNecessary(parserContext, element); + } + + /** + * Parses the supplied {@code } element and registers the resulting + * {@link org.springframework.aop.Advisor} and any resulting {@link org.springframework.aop.Pointcut} + * with the supplied {@link BeanDefinitionRegistry}. + */ + private void parseAdvisor(Element advisorElement, ParserContext parserContext) { + AbstractBeanDefinition advisorDef = createAdvisorBeanDefinition(advisorElement, parserContext); + String id = advisorElement.getAttribute(ID); + + try { + this.parseState.push(new AdvisorEntry(id)); + String advisorBeanName = id; + if (StringUtils.hasText(advisorBeanName)) { + parserContext.getRegistry().registerBeanDefinition(advisorBeanName, advisorDef); + } + else { + advisorBeanName = parserContext.getReaderContext().registerWithGeneratedName(advisorDef); + } + + Object pointcut = parsePointcutProperty(advisorElement, parserContext); + if (pointcut instanceof BeanDefinition) { + advisorDef.getPropertyValues().add(POINTCUT, pointcut); + parserContext.registerComponent( + new AdvisorComponentDefinition(advisorBeanName, advisorDef, (BeanDefinition) pointcut)); + } + else if (pointcut instanceof String) { + advisorDef.getPropertyValues().add(POINTCUT, new RuntimeBeanReference((String) pointcut)); + parserContext.registerComponent( + new AdvisorComponentDefinition(advisorBeanName, advisorDef)); + } + } + finally { + this.parseState.pop(); + } + } + + /** + * Create a {@link RootBeanDefinition} for the advisor described in the supplied. Does not + * parse any associated '{@code pointcut}' or '{@code pointcut-ref}' attributes. + */ + private AbstractBeanDefinition createAdvisorBeanDefinition(Element advisorElement, ParserContext parserContext) { + RootBeanDefinition advisorDefinition = new RootBeanDefinition(DefaultBeanFactoryPointcutAdvisor.class); + advisorDefinition.setSource(parserContext.extractSource(advisorElement)); + + String adviceRef = advisorElement.getAttribute(ADVICE_REF); + if (!StringUtils.hasText(adviceRef)) { + parserContext.getReaderContext().error( + "'advice-ref' attribute contains empty value.", advisorElement, this.parseState.snapshot()); + } + else { + advisorDefinition.getPropertyValues().add( + ADVICE_BEAN_NAME, new RuntimeBeanNameReference(adviceRef)); + } + + if (advisorElement.hasAttribute(ORDER_PROPERTY)) { + advisorDefinition.getPropertyValues().add( + ORDER_PROPERTY, advisorElement.getAttribute(ORDER_PROPERTY)); + } + + return advisorDefinition; + } + + private void parseAspect(Element aspectElement, ParserContext parserContext) { + String aspectId = aspectElement.getAttribute(ID); + String aspectName = aspectElement.getAttribute(REF); + + try { + this.parseState.push(new AspectEntry(aspectId, aspectName)); + List beanDefinitions = new ArrayList<>(); + List beanReferences = new ArrayList<>(); + + List declareParents = DomUtils.getChildElementsByTagName(aspectElement, DECLARE_PARENTS); + for (int i = METHOD_INDEX; i < declareParents.size(); i++) { + Element declareParentsElement = declareParents.get(i); + beanDefinitions.add(parseDeclareParents(declareParentsElement, parserContext)); + } + + // We have to parse "advice" and all the advice kinds in one loop, to get the + // ordering semantics right. + NodeList nodeList = aspectElement.getChildNodes(); + boolean adviceFoundAlready = false; + for (int i = 0; i < nodeList.getLength(); i++) { + Node node = nodeList.item(i); + if (isAdviceNode(node, parserContext)) { + if (!adviceFoundAlready) { + adviceFoundAlready = true; + if (!StringUtils.hasText(aspectName)) { + parserContext.getReaderContext().error( + " tag needs aspect bean reference via 'ref' attribute when declaring advices.", + aspectElement, this.parseState.snapshot()); + return; + } + beanReferences.add(new RuntimeBeanReference(aspectName)); + } + AbstractBeanDefinition advisorDefinition = parseAdvice( + aspectName, i, aspectElement, (Element) node, parserContext, beanDefinitions, beanReferences); + beanDefinitions.add(advisorDefinition); + } + } + + AspectComponentDefinition aspectComponentDefinition = createAspectComponentDefinition( + aspectElement, aspectId, beanDefinitions, beanReferences, parserContext); + parserContext.pushContainingComponent(aspectComponentDefinition); + + List pointcuts = DomUtils.getChildElementsByTagName(aspectElement, POINTCUT); + for (Element pointcutElement : pointcuts) { + parsePointcut(pointcutElement, parserContext); + } + + parserContext.popAndRegisterContainingComponent(); + } + finally { + this.parseState.pop(); + } + } + + private AspectComponentDefinition createAspectComponentDefinition( + Element aspectElement, String aspectId, List beanDefs, + List beanRefs, ParserContext parserContext) { + + BeanDefinition[] beanDefArray = beanDefs.toArray(new BeanDefinition[0]); + BeanReference[] beanRefArray = beanRefs.toArray(new BeanReference[0]); + Object source = parserContext.extractSource(aspectElement); + return new AspectComponentDefinition(aspectId, beanDefArray, beanRefArray, source); + } + + /** + * Return {@code true} if the supplied node describes an advice type. May be one of: + * '{@code before}', '{@code after}', '{@code after-returning}', + * '{@code after-throwing}' or '{@code around}'. + */ + private boolean isAdviceNode(Node aNode, ParserContext parserContext) { + if (!(aNode instanceof Element)) { + return false; + } + else { + String name = parserContext.getDelegate().getLocalName(aNode); + return (BEFORE.equals(name) || AFTER.equals(name) || AFTER_RETURNING_ELEMENT.equals(name) || + AFTER_THROWING_ELEMENT.equals(name) || AROUND.equals(name)); + } + } + + /** + * Parse a '{@code declare-parents}' element and register the appropriate + * DeclareParentsAdvisor with the BeanDefinitionRegistry encapsulated in the + * supplied ParserContext. + */ + private AbstractBeanDefinition parseDeclareParents(Element declareParentsElement, ParserContext parserContext) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(DeclareParentsAdvisor.class); + builder.addConstructorArgValue(declareParentsElement.getAttribute(IMPLEMENT_INTERFACE)); + builder.addConstructorArgValue(declareParentsElement.getAttribute(TYPE_PATTERN)); + + String defaultImpl = declareParentsElement.getAttribute(DEFAULT_IMPL); + String delegateRef = declareParentsElement.getAttribute(DELEGATE_REF); + + if (StringUtils.hasText(defaultImpl) && !StringUtils.hasText(delegateRef)) { + builder.addConstructorArgValue(defaultImpl); + } + else if (StringUtils.hasText(delegateRef) && !StringUtils.hasText(defaultImpl)) { + builder.addConstructorArgReference(delegateRef); + } + else { + parserContext.getReaderContext().error( + "Exactly one of the " + DEFAULT_IMPL + " or " + DELEGATE_REF + " attributes must be specified", + declareParentsElement, this.parseState.snapshot()); + } + + AbstractBeanDefinition definition = builder.getBeanDefinition(); + definition.setSource(parserContext.extractSource(declareParentsElement)); + parserContext.getReaderContext().registerWithGeneratedName(definition); + return definition; + } + + /** + * Parses one of '{@code before}', '{@code after}', '{@code after-returning}', + * '{@code after-throwing}' or '{@code around}' and registers the resulting + * BeanDefinition with the supplied BeanDefinitionRegistry. + * @return the generated advice RootBeanDefinition + */ + private AbstractBeanDefinition parseAdvice( + String aspectName, int order, Element aspectElement, Element adviceElement, ParserContext parserContext, + List beanDefinitions, List beanReferences) { + + try { + this.parseState.push(new AdviceEntry(parserContext.getDelegate().getLocalName(adviceElement))); + + // create the method factory bean + RootBeanDefinition methodDefinition = new RootBeanDefinition(MethodLocatingFactoryBean.class); + methodDefinition.getPropertyValues().add("targetBeanName", aspectName); + methodDefinition.getPropertyValues().add("methodName", adviceElement.getAttribute("method")); + methodDefinition.setSynthetic(true); + + // create instance factory definition + RootBeanDefinition aspectFactoryDef = + new RootBeanDefinition(SimpleBeanFactoryAwareAspectInstanceFactory.class); + aspectFactoryDef.getPropertyValues().add("aspectBeanName", aspectName); + aspectFactoryDef.setSynthetic(true); + + // register the pointcut + AbstractBeanDefinition adviceDef = createAdviceDefinition( + adviceElement, parserContext, aspectName, order, methodDefinition, aspectFactoryDef, + beanDefinitions, beanReferences); + + // configure the advisor + RootBeanDefinition advisorDefinition = new RootBeanDefinition(AspectJPointcutAdvisor.class); + advisorDefinition.setSource(parserContext.extractSource(adviceElement)); + advisorDefinition.getConstructorArgumentValues().addGenericArgumentValue(adviceDef); + if (aspectElement.hasAttribute(ORDER_PROPERTY)) { + advisorDefinition.getPropertyValues().add( + ORDER_PROPERTY, aspectElement.getAttribute(ORDER_PROPERTY)); + } + + // register the final advisor + parserContext.getReaderContext().registerWithGeneratedName(advisorDefinition); + + return advisorDefinition; + } + finally { + this.parseState.pop(); + } + } + + /** + * Creates the RootBeanDefinition for a POJO advice bean. Also causes pointcut + * parsing to occur so that the pointcut may be associate with the advice bean. + * This same pointcut is also configured as the pointcut for the enclosing + * Advisor definition using the supplied MutablePropertyValues. + */ + private AbstractBeanDefinition createAdviceDefinition( + Element adviceElement, ParserContext parserContext, String aspectName, int order, + RootBeanDefinition methodDef, RootBeanDefinition aspectFactoryDef, + List beanDefinitions, List beanReferences) { + + RootBeanDefinition adviceDefinition = new RootBeanDefinition(getAdviceClass(adviceElement, parserContext)); + adviceDefinition.setSource(parserContext.extractSource(adviceElement)); + + adviceDefinition.getPropertyValues().add(ASPECT_NAME_PROPERTY, aspectName); + adviceDefinition.getPropertyValues().add(DECLARATION_ORDER_PROPERTY, order); + + if (adviceElement.hasAttribute(RETURNING)) { + adviceDefinition.getPropertyValues().add( + RETURNING_PROPERTY, adviceElement.getAttribute(RETURNING)); + } + if (adviceElement.hasAttribute(THROWING)) { + adviceDefinition.getPropertyValues().add( + THROWING_PROPERTY, adviceElement.getAttribute(THROWING)); + } + if (adviceElement.hasAttribute(ARG_NAMES)) { + adviceDefinition.getPropertyValues().add( + ARG_NAMES_PROPERTY, adviceElement.getAttribute(ARG_NAMES)); + } + + ConstructorArgumentValues cav = adviceDefinition.getConstructorArgumentValues(); + cav.addIndexedArgumentValue(METHOD_INDEX, methodDef); + + Object pointcut = parsePointcutProperty(adviceElement, parserContext); + if (pointcut instanceof BeanDefinition) { + cav.addIndexedArgumentValue(POINTCUT_INDEX, pointcut); + beanDefinitions.add((BeanDefinition) pointcut); + } + else if (pointcut instanceof String) { + RuntimeBeanReference pointcutRef = new RuntimeBeanReference((String) pointcut); + cav.addIndexedArgumentValue(POINTCUT_INDEX, pointcutRef); + beanReferences.add(pointcutRef); + } + + cav.addIndexedArgumentValue(ASPECT_INSTANCE_FACTORY_INDEX, aspectFactoryDef); + + return adviceDefinition; + } + + /** + * Gets the advice implementation class corresponding to the supplied {@link Element}. + */ + private Class getAdviceClass(Element adviceElement, ParserContext parserContext) { + String elementName = parserContext.getDelegate().getLocalName(adviceElement); + if (BEFORE.equals(elementName)) { + return AspectJMethodBeforeAdvice.class; + } + else if (AFTER.equals(elementName)) { + return AspectJAfterAdvice.class; + } + else if (AFTER_RETURNING_ELEMENT.equals(elementName)) { + return AspectJAfterReturningAdvice.class; + } + else if (AFTER_THROWING_ELEMENT.equals(elementName)) { + return AspectJAfterThrowingAdvice.class; + } + else if (AROUND.equals(elementName)) { + return AspectJAroundAdvice.class; + } + else { + throw new IllegalArgumentException("Unknown advice kind [" + elementName + "]."); + } + } + + /** + * Parses the supplied {@code } and registers the resulting + * Pointcut with the BeanDefinitionRegistry. + */ + private AbstractBeanDefinition parsePointcut(Element pointcutElement, ParserContext parserContext) { + String id = pointcutElement.getAttribute(ID); + String expression = pointcutElement.getAttribute(EXPRESSION); + + AbstractBeanDefinition pointcutDefinition = null; + + try { + this.parseState.push(new PointcutEntry(id)); + pointcutDefinition = createPointcutDefinition(expression); + pointcutDefinition.setSource(parserContext.extractSource(pointcutElement)); + + String pointcutBeanName = id; + if (StringUtils.hasText(pointcutBeanName)) { + parserContext.getRegistry().registerBeanDefinition(pointcutBeanName, pointcutDefinition); + } + else { + pointcutBeanName = parserContext.getReaderContext().registerWithGeneratedName(pointcutDefinition); + } + + parserContext.registerComponent( + new PointcutComponentDefinition(pointcutBeanName, pointcutDefinition, expression)); + } + finally { + this.parseState.pop(); + } + + return pointcutDefinition; + } + + /** + * Parses the {@code pointcut} or {@code pointcut-ref} attributes of the supplied + * {@link Element} and add a {@code pointcut} property as appropriate. Generates a + * {@link org.springframework.beans.factory.config.BeanDefinition} for the pointcut if necessary + * and returns its bean name, otherwise returns the bean name of the referred pointcut. + */ + @Nullable + private Object parsePointcutProperty(Element element, ParserContext parserContext) { + if (element.hasAttribute(POINTCUT) && element.hasAttribute(POINTCUT_REF)) { + parserContext.getReaderContext().error( + "Cannot define both 'pointcut' and 'pointcut-ref' on tag.", + element, this.parseState.snapshot()); + return null; + } + else if (element.hasAttribute(POINTCUT)) { + // Create a pointcut for the anonymous pc and register it. + String expression = element.getAttribute(POINTCUT); + AbstractBeanDefinition pointcutDefinition = createPointcutDefinition(expression); + pointcutDefinition.setSource(parserContext.extractSource(element)); + return pointcutDefinition; + } + else if (element.hasAttribute(POINTCUT_REF)) { + String pointcutRef = element.getAttribute(POINTCUT_REF); + if (!StringUtils.hasText(pointcutRef)) { + parserContext.getReaderContext().error( + "'pointcut-ref' attribute contains empty value.", element, this.parseState.snapshot()); + return null; + } + return pointcutRef; + } + else { + parserContext.getReaderContext().error( + "Must define one of 'pointcut' or 'pointcut-ref' on tag.", + element, this.parseState.snapshot()); + return null; + } + } + + /** + * Creates a {@link BeanDefinition} for the {@link AspectJExpressionPointcut} class using + * the supplied pointcut expression. + */ + protected AbstractBeanDefinition createPointcutDefinition(String expression) { + RootBeanDefinition beanDefinition = new RootBeanDefinition(AspectJExpressionPointcut.class); + beanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE); + beanDefinition.setSynthetic(true); + beanDefinition.getPropertyValues().add(EXPRESSION, expression); + return beanDefinition; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/config/MethodLocatingFactoryBean.java b/spring-aop/src/main/java/org/springframework/aop/config/MethodLocatingFactoryBean.java new file mode 100644 index 0000000..ebff6ee --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/config/MethodLocatingFactoryBean.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.config; + +import java.lang.reflect.Method; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * {@link FactoryBean} implementation that locates a {@link Method} on a specified bean. + * + * @author Rob Harrop + * @since 2.0 + */ +public class MethodLocatingFactoryBean implements FactoryBean, BeanFactoryAware { + + @Nullable + private String targetBeanName; + + @Nullable + private String methodName; + + @Nullable + private Method method; + + + /** + * Set the name of the bean to locate the {@link Method} on. + *

This property is required. + * @param targetBeanName the name of the bean to locate the {@link Method} on + */ + public void setTargetBeanName(String targetBeanName) { + this.targetBeanName = targetBeanName; + } + + /** + * Set the name of the {@link Method} to locate. + *

This property is required. + * @param methodName the name of the {@link Method} to locate + */ + public void setMethodName(String methodName) { + this.methodName = methodName; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + if (!StringUtils.hasText(this.targetBeanName)) { + throw new IllegalArgumentException("Property 'targetBeanName' is required"); + } + if (!StringUtils.hasText(this.methodName)) { + throw new IllegalArgumentException("Property 'methodName' is required"); + } + + Class beanClass = beanFactory.getType(this.targetBeanName); + if (beanClass == null) { + throw new IllegalArgumentException("Can't determine type of bean with name '" + this.targetBeanName + "'"); + } + this.method = BeanUtils.resolveSignature(this.methodName, beanClass); + + if (this.method == null) { + throw new IllegalArgumentException("Unable to locate method [" + this.methodName + + "] on bean [" + this.targetBeanName + "]"); + } + } + + + @Override + @Nullable + public Method getObject() throws Exception { + return this.method; + } + + @Override + public Class getObjectType() { + return Method.class; + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/config/PointcutComponentDefinition.java b/spring-aop/src/main/java/org/springframework/aop/config/PointcutComponentDefinition.java new file mode 100644 index 0000000..389a5b4 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/config/PointcutComponentDefinition.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.config; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.parsing.AbstractComponentDefinition; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * {@link org.springframework.beans.factory.parsing.ComponentDefinition} + * implementation that holds a pointcut definition. + * + * @author Rob Harrop + * @since 2.0 + */ +public class PointcutComponentDefinition extends AbstractComponentDefinition { + + private final String pointcutBeanName; + + private final BeanDefinition pointcutDefinition; + + private final String description; + + + public PointcutComponentDefinition(String pointcutBeanName, BeanDefinition pointcutDefinition, String expression) { + Assert.notNull(pointcutBeanName, "Bean name must not be null"); + Assert.notNull(pointcutDefinition, "Pointcut definition must not be null"); + Assert.notNull(expression, "Expression must not be null"); + this.pointcutBeanName = pointcutBeanName; + this.pointcutDefinition = pointcutDefinition; + this.description = "Pointcut "; + } + + + @Override + public String getName() { + return this.pointcutBeanName; + } + + @Override + public String getDescription() { + return this.description; + } + + @Override + public BeanDefinition[] getBeanDefinitions() { + return new BeanDefinition[] {this.pointcutDefinition}; + } + + @Override + @Nullable + public Object getSource() { + return this.pointcutDefinition.getSource(); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/config/PointcutEntry.java b/spring-aop/src/main/java/org/springframework/aop/config/PointcutEntry.java new file mode 100644 index 0000000..e6066c5 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/config/PointcutEntry.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.config; + +import org.springframework.beans.factory.parsing.ParseState; + +/** + * {@link ParseState} entry representing a pointcut. + * + * @author Mark Fisher + * @since 2.0 + */ +public class PointcutEntry implements ParseState.Entry { + + private final String name; + + + /** + * Create a new {@code PointcutEntry} instance. + * @param name the bean name of the pointcut + */ + public PointcutEntry(String name) { + this.name = name; + } + + + @Override + public String toString() { + return "Pointcut '" + this.name + "'"; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/config/ScopedProxyBeanDefinitionDecorator.java b/spring-aop/src/main/java/org/springframework/aop/config/ScopedProxyBeanDefinitionDecorator.java new file mode 100644 index 0000000..ca940ec --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/config/ScopedProxyBeanDefinitionDecorator.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.config; + +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import org.springframework.aop.scope.ScopedProxyUtils; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.xml.BeanDefinitionDecorator; +import org.springframework.beans.factory.xml.ParserContext; + +/** + * {@link BeanDefinitionDecorator} responsible for parsing the + * {@code } tag. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Mark Fisher + * @since 2.0 + */ +class ScopedProxyBeanDefinitionDecorator implements BeanDefinitionDecorator { + + private static final String PROXY_TARGET_CLASS = "proxy-target-class"; + + + @Override + public BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder definition, ParserContext parserContext) { + boolean proxyTargetClass = true; + if (node instanceof Element) { + Element ele = (Element) node; + if (ele.hasAttribute(PROXY_TARGET_CLASS)) { + proxyTargetClass = Boolean.parseBoolean(ele.getAttribute(PROXY_TARGET_CLASS)); + } + } + + // Register the original bean definition as it will be referenced by the scoped proxy + // and is relevant for tooling (validation, navigation). + BeanDefinitionHolder holder = + ScopedProxyUtils.createScopedProxy(definition, parserContext.getRegistry(), proxyTargetClass); + String targetBeanName = ScopedProxyUtils.getTargetBeanName(definition.getBeanName()); + parserContext.getReaderContext().fireComponentRegistered( + new BeanComponentDefinition(definition.getBeanDefinition(), targetBeanName)); + return holder; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/config/SimpleBeanFactoryAwareAspectInstanceFactory.java b/spring-aop/src/main/java/org/springframework/aop/config/SimpleBeanFactoryAwareAspectInstanceFactory.java new file mode 100644 index 0000000..3d89993 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/config/SimpleBeanFactoryAwareAspectInstanceFactory.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.config; + +import org.springframework.aop.aspectj.AspectInstanceFactory; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.Ordered; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Implementation of {@link AspectInstanceFactory} that locates the aspect from the + * {@link org.springframework.beans.factory.BeanFactory} using a configured bean name. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +public class SimpleBeanFactoryAwareAspectInstanceFactory implements AspectInstanceFactory, BeanFactoryAware { + + @Nullable + private String aspectBeanName; + + @Nullable + private BeanFactory beanFactory; + + + /** + * Set the name of the aspect bean. This is the bean that is returned when calling + * {@link #getAspectInstance()}. + */ + public void setAspectBeanName(String aspectBeanName) { + this.aspectBeanName = aspectBeanName; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + Assert.notNull(this.aspectBeanName, "'aspectBeanName' is required"); + } + + + /** + * Look up the aspect bean from the {@link BeanFactory} and returns it. + * @see #setAspectBeanName + */ + @Override + public Object getAspectInstance() { + Assert.state(this.beanFactory != null, "No BeanFactory set"); + Assert.state(this.aspectBeanName != null, "No 'aspectBeanName' set"); + return this.beanFactory.getBean(this.aspectBeanName); + } + + @Override + @Nullable + public ClassLoader getAspectClassLoader() { + if (this.beanFactory instanceof ConfigurableBeanFactory) { + return ((ConfigurableBeanFactory) this.beanFactory).getBeanClassLoader(); + } + else { + return ClassUtils.getDefaultClassLoader(); + } + } + + @Override + public int getOrder() { + if (this.beanFactory != null && this.aspectBeanName != null && + this.beanFactory.isSingleton(this.aspectBeanName) && + this.beanFactory.isTypeMatch(this.aspectBeanName, Ordered.class)) { + return ((Ordered) this.beanFactory.getBean(this.aspectBeanName)).getOrder(); + } + return Ordered.LOWEST_PRECEDENCE; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/config/SpringConfiguredBeanDefinitionParser.java b/spring-aop/src/main/java/org/springframework/aop/config/SpringConfiguredBeanDefinitionParser.java new file mode 100644 index 0000000..3a74eca --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/config/SpringConfiguredBeanDefinitionParser.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; + +/** + * {@link BeanDefinitionParser} responsible for parsing the + * {@code } tag. + * + *

NOTE: This is essentially a duplicate of Spring 2.5's + * {@link org.springframework.context.config.SpringConfiguredBeanDefinitionParser} + * for the {@code } tag, mirrored here for compatibility with + * Spring 2.0's {@code } tag (avoiding a direct dependency on the + * context package). + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +class SpringConfiguredBeanDefinitionParser implements BeanDefinitionParser { + + /** + * The bean name of the internally managed bean configurer aspect. + */ + public static final String BEAN_CONFIGURER_ASPECT_BEAN_NAME = + "org.springframework.context.config.internalBeanConfigurerAspect"; + + private static final String BEAN_CONFIGURER_ASPECT_CLASS_NAME = + "org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect"; + + + @Override + public BeanDefinition parse(Element element, ParserContext parserContext) { + if (!parserContext.getRegistry().containsBeanDefinition(BEAN_CONFIGURER_ASPECT_BEAN_NAME)) { + RootBeanDefinition def = new RootBeanDefinition(); + def.setBeanClassName(BEAN_CONFIGURER_ASPECT_CLASS_NAME); + def.setFactoryMethodName("aspectOf"); + def.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + def.setSource(parserContext.extractSource(element)); + parserContext.registerBeanComponent(new BeanComponentDefinition(def, BEAN_CONFIGURER_ASPECT_BEAN_NAME)); + } + return null; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/config/package-info.java b/spring-aop/src/main/java/org/springframework/aop/config/package-info.java new file mode 100644 index 0000000..b0d1010 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/config/package-info.java @@ -0,0 +1,10 @@ +/** + * Support package for declarative AOP configuration, + * with XML schema being the primary configuration format. + */ +@NonNullApi +@NonNullFields +package org.springframework.aop.config; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AbstractAdvisingBeanPostProcessor.java b/spring-aop/src/main/java/org/springframework/aop/framework/AbstractAdvisingBeanPostProcessor.java new file mode 100644 index 0000000..a3a87f2 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AbstractAdvisingBeanPostProcessor.java @@ -0,0 +1,172 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.aop.Advisor; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.lang.Nullable; + +/** + * Base class for {@link BeanPostProcessor} implementations that apply a + * Spring AOP {@link Advisor} to specific beans. + * + * @author Juergen Hoeller + * @since 3.2 + */ +@SuppressWarnings("serial") +public abstract class AbstractAdvisingBeanPostProcessor extends ProxyProcessorSupport implements BeanPostProcessor { + + @Nullable + protected Advisor advisor; + + protected boolean beforeExistingAdvisors = false; + + private final Map, Boolean> eligibleBeans = new ConcurrentHashMap<>(256); + + + /** + * Set whether this post-processor's advisor is supposed to apply before + * existing advisors when encountering a pre-advised object. + *

Default is "false", applying the advisor after existing advisors, i.e. + * as close as possible to the target method. Switch this to "true" in order + * for this post-processor's advisor to wrap existing advisors as well. + *

Note: Check the concrete post-processor's javadoc whether it possibly + * changes this flag by default, depending on the nature of its advisor. + */ + public void setBeforeExistingAdvisors(boolean beforeExistingAdvisors) { + this.beforeExistingAdvisors = beforeExistingAdvisors; + } + + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) { + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + if (this.advisor == null || bean instanceof AopInfrastructureBean) { + // Ignore AOP infrastructure such as scoped proxies. + return bean; + } + + if (bean instanceof Advised) { + Advised advised = (Advised) bean; + if (!advised.isFrozen() && isEligible(AopUtils.getTargetClass(bean))) { + // Add our local Advisor to the existing proxy's Advisor chain... + if (this.beforeExistingAdvisors) { + advised.addAdvisor(0, this.advisor); + } + else { + advised.addAdvisor(this.advisor); + } + return bean; + } + } + + if (isEligible(bean, beanName)) { + ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName); + if (!proxyFactory.isProxyTargetClass()) { + evaluateProxyInterfaces(bean.getClass(), proxyFactory); + } + proxyFactory.addAdvisor(this.advisor); + customizeProxyFactory(proxyFactory); + return proxyFactory.getProxy(getProxyClassLoader()); + } + + // No proxy needed. + return bean; + } + + /** + * Check whether the given bean is eligible for advising with this + * post-processor's {@link Advisor}. + *

Delegates to {@link #isEligible(Class)} for target class checking. + * Can be overridden e.g. to specifically exclude certain beans by name. + *

Note: Only called for regular bean instances but not for existing + * proxy instances which implement {@link Advised} and allow for adding + * the local {@link Advisor} to the existing proxy's {@link Advisor} chain. + * For the latter, {@link #isEligible(Class)} is being called directly, + * with the actual target class behind the existing proxy (as determined + * by {@link AopUtils#getTargetClass(Object)}). + * @param bean the bean instance + * @param beanName the name of the bean + * @see #isEligible(Class) + */ + protected boolean isEligible(Object bean, String beanName) { + return isEligible(bean.getClass()); + } + + /** + * Check whether the given class is eligible for advising with this + * post-processor's {@link Advisor}. + *

Implements caching of {@code canApply} results per bean target class. + * @param targetClass the class to check against + * @see AopUtils#canApply(Advisor, Class) + */ + protected boolean isEligible(Class targetClass) { + Boolean eligible = this.eligibleBeans.get(targetClass); + if (eligible != null) { + return eligible; + } + if (this.advisor == null) { + return false; + } + eligible = AopUtils.canApply(this.advisor, targetClass); + this.eligibleBeans.put(targetClass, eligible); + return eligible; + } + + /** + * Prepare a {@link ProxyFactory} for the given bean. + *

Subclasses may customize the handling of the target instance and in + * particular the exposure of the target class. The default introspection + * of interfaces for non-target-class proxies and the configured advisor + * will be applied afterwards; {@link #customizeProxyFactory} allows for + * late customizations of those parts right before proxy creation. + * @param bean the bean instance to create a proxy for + * @param beanName the corresponding bean name + * @return the ProxyFactory, initialized with this processor's + * {@link ProxyConfig} settings and the specified bean + * @since 4.2.3 + * @see #customizeProxyFactory + */ + protected ProxyFactory prepareProxyFactory(Object bean, String beanName) { + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.copyFrom(this); + proxyFactory.setTarget(bean); + return proxyFactory; + } + + /** + * Subclasses may choose to implement this: for example, + * to change the interfaces exposed. + *

The default implementation is empty. + * @param proxyFactory the ProxyFactory that is already configured with + * target, advisor and interfaces and will be used to create the proxy + * immediately after this method returns + * @since 4.2.3 + * @see #prepareProxyFactory + */ + protected void customizeProxyFactory(ProxyFactory proxyFactory) { + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AbstractSingletonProxyFactoryBean.java b/spring-aop/src/main/java/org/springframework/aop/framework/AbstractSingletonProxyFactoryBean.java new file mode 100644 index 0000000..bb680a3 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AbstractSingletonProxyFactoryBean.java @@ -0,0 +1,255 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import org.springframework.aop.TargetSource; +import org.springframework.aop.framework.adapter.AdvisorAdapterRegistry; +import org.springframework.aop.framework.adapter.GlobalAdvisorAdapterRegistry; +import org.springframework.aop.target.SingletonTargetSource; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.FactoryBeanNotInitializedException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * Convenient superclass for {@link FactoryBean} types that produce singleton-scoped + * proxy objects. + * + *

Manages pre- and post-interceptors (references, rather than + * interceptor names, as in {@link ProxyFactoryBean}) and provides + * consistent interface management. + * + * @author Juergen Hoeller + * @since 2.0 + */ +@SuppressWarnings("serial") +public abstract class AbstractSingletonProxyFactoryBean extends ProxyConfig + implements FactoryBean, BeanClassLoaderAware, InitializingBean { + + @Nullable + private Object target; + + @Nullable + private Class[] proxyInterfaces; + + @Nullable + private Object[] preInterceptors; + + @Nullable + private Object[] postInterceptors; + + /** Default is global AdvisorAdapterRegistry. */ + private AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + @Nullable + private transient ClassLoader proxyClassLoader; + + @Nullable + private Object proxy; + + + /** + * Set the target object, that is, the bean to be wrapped with a transactional proxy. + *

The target may be any object, in which case a SingletonTargetSource will + * be created. If it is a TargetSource, no wrapper TargetSource is created: + * This enables the use of a pooling or prototype TargetSource etc. + * @see org.springframework.aop.TargetSource + * @see org.springframework.aop.target.SingletonTargetSource + * @see org.springframework.aop.target.LazyInitTargetSource + * @see org.springframework.aop.target.PrototypeTargetSource + * @see org.springframework.aop.target.CommonsPool2TargetSource + */ + public void setTarget(Object target) { + this.target = target; + } + + /** + * Specify the set of interfaces being proxied. + *

If not specified (the default), the AOP infrastructure works + * out which interfaces need proxying by analyzing the target, + * proxying all the interfaces that the target object implements. + */ + public void setProxyInterfaces(Class[] proxyInterfaces) { + this.proxyInterfaces = proxyInterfaces; + } + + /** + * Set additional interceptors (or advisors) to be applied before the + * implicit transaction interceptor, e.g. a PerformanceMonitorInterceptor. + *

You may specify any AOP Alliance MethodInterceptors or other + * Spring AOP Advices, as well as Spring AOP Advisors. + * @see org.springframework.aop.interceptor.PerformanceMonitorInterceptor + */ + public void setPreInterceptors(Object[] preInterceptors) { + this.preInterceptors = preInterceptors; + } + + /** + * Set additional interceptors (or advisors) to be applied after the + * implicit transaction interceptor. + *

You may specify any AOP Alliance MethodInterceptors or other + * Spring AOP Advices, as well as Spring AOP Advisors. + */ + public void setPostInterceptors(Object[] postInterceptors) { + this.postInterceptors = postInterceptors; + } + + /** + * Specify the AdvisorAdapterRegistry to use. + * Default is the global AdvisorAdapterRegistry. + * @see org.springframework.aop.framework.adapter.GlobalAdvisorAdapterRegistry + */ + public void setAdvisorAdapterRegistry(AdvisorAdapterRegistry advisorAdapterRegistry) { + this.advisorAdapterRegistry = advisorAdapterRegistry; + } + + /** + * Set the ClassLoader to generate the proxy class in. + *

Default is the bean ClassLoader, i.e. the ClassLoader used by the + * containing BeanFactory for loading all bean classes. This can be + * overridden here for specific proxies. + */ + public void setProxyClassLoader(ClassLoader classLoader) { + this.proxyClassLoader = classLoader; + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + if (this.proxyClassLoader == null) { + this.proxyClassLoader = classLoader; + } + } + + + @Override + public void afterPropertiesSet() { + if (this.target == null) { + throw new IllegalArgumentException("Property 'target' is required"); + } + if (this.target instanceof String) { + throw new IllegalArgumentException("'target' needs to be a bean reference, not a bean name as value"); + } + if (this.proxyClassLoader == null) { + this.proxyClassLoader = ClassUtils.getDefaultClassLoader(); + } + + ProxyFactory proxyFactory = new ProxyFactory(); + + if (this.preInterceptors != null) { + for (Object interceptor : this.preInterceptors) { + proxyFactory.addAdvisor(this.advisorAdapterRegistry.wrap(interceptor)); + } + } + + // Add the main interceptor (typically an Advisor). + proxyFactory.addAdvisor(this.advisorAdapterRegistry.wrap(createMainInterceptor())); + + if (this.postInterceptors != null) { + for (Object interceptor : this.postInterceptors) { + proxyFactory.addAdvisor(this.advisorAdapterRegistry.wrap(interceptor)); + } + } + + proxyFactory.copyFrom(this); + + TargetSource targetSource = createTargetSource(this.target); + proxyFactory.setTargetSource(targetSource); + + if (this.proxyInterfaces != null) { + proxyFactory.setInterfaces(this.proxyInterfaces); + } + else if (!isProxyTargetClass()) { + // Rely on AOP infrastructure to tell us what interfaces to proxy. + Class targetClass = targetSource.getTargetClass(); + if (targetClass != null) { + proxyFactory.setInterfaces(ClassUtils.getAllInterfacesForClass(targetClass, this.proxyClassLoader)); + } + } + + postProcessProxyFactory(proxyFactory); + + this.proxy = proxyFactory.getProxy(this.proxyClassLoader); + } + + /** + * Determine a TargetSource for the given target (or TargetSource). + * @param target the target. If this is an implementation of TargetSource it is + * used as our TargetSource; otherwise it is wrapped in a SingletonTargetSource. + * @return a TargetSource for this object + */ + protected TargetSource createTargetSource(Object target) { + if (target instanceof TargetSource) { + return (TargetSource) target; + } + else { + return new SingletonTargetSource(target); + } + } + + /** + * A hook for subclasses to post-process the {@link ProxyFactory} + * before creating the proxy instance with it. + * @param proxyFactory the AOP ProxyFactory about to be used + * @since 4.2 + */ + protected void postProcessProxyFactory(ProxyFactory proxyFactory) { + } + + + @Override + public Object getObject() { + if (this.proxy == null) { + throw new FactoryBeanNotInitializedException(); + } + return this.proxy; + } + + @Override + @Nullable + public Class getObjectType() { + if (this.proxy != null) { + return this.proxy.getClass(); + } + if (this.proxyInterfaces != null && this.proxyInterfaces.length == 1) { + return this.proxyInterfaces[0]; + } + if (this.target instanceof TargetSource) { + return ((TargetSource) this.target).getTargetClass(); + } + if (this.target != null) { + return this.target.getClass(); + } + return null; + } + + @Override + public final boolean isSingleton() { + return true; + } + + + /** + * Create the "main" interceptor for this proxy factory bean. + * Typically an Advisor, but can also be any type of Advice. + *

Pre-interceptors will be applied before, post-interceptors + * will be applied after this interceptor. + */ + protected abstract Object createMainInterceptor(); + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/Advised.java b/spring-aop/src/main/java/org/springframework/aop/framework/Advised.java new file mode 100644 index 0000000..b956f00 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/Advised.java @@ -0,0 +1,235 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import org.aopalliance.aop.Advice; + +import org.springframework.aop.Advisor; +import org.springframework.aop.TargetClassAware; +import org.springframework.aop.TargetSource; + +/** + * Interface to be implemented by classes that hold the configuration + * of a factory of AOP proxies. This configuration includes the + * Interceptors and other advice, Advisors, and the proxied interfaces. + * + *

Any AOP proxy obtained from Spring can be cast to this interface to + * allow manipulation of its AOP advice. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 13.03.2003 + * @see org.springframework.aop.framework.AdvisedSupport + */ +public interface Advised extends TargetClassAware { + + /** + * Return whether the Advised configuration is frozen, + * in which case no advice changes can be made. + */ + boolean isFrozen(); + + /** + * Are we proxying the full target class instead of specified interfaces? + */ + boolean isProxyTargetClass(); + + /** + * Return the interfaces proxied by the AOP proxy. + *

Will not include the target class, which may also be proxied. + */ + Class[] getProxiedInterfaces(); + + /** + * Determine whether the given interface is proxied. + * @param intf the interface to check + */ + boolean isInterfaceProxied(Class intf); + + /** + * Change the {@code TargetSource} used by this {@code Advised} object. + *

Only works if the configuration isn't {@linkplain #isFrozen frozen}. + * @param targetSource new TargetSource to use + */ + void setTargetSource(TargetSource targetSource); + + /** + * Return the {@code TargetSource} used by this {@code Advised} object. + */ + TargetSource getTargetSource(); + + /** + * Set whether the proxy should be exposed by the AOP framework as a + * {@link ThreadLocal} for retrieval via the {@link AopContext} class. + *

It can be necessary to expose the proxy if an advised object needs + * to invoke a method on itself with advice applied. Otherwise, if an + * advised object invokes a method on {@code this}, no advice will be applied. + *

Default is {@code false}, for optimal performance. + */ + void setExposeProxy(boolean exposeProxy); + + /** + * Return whether the factory should expose the proxy as a {@link ThreadLocal}. + *

It can be necessary to expose the proxy if an advised object needs + * to invoke a method on itself with advice applied. Otherwise, if an + * advised object invokes a method on {@code this}, no advice will be applied. + *

Getting the proxy is analogous to an EJB calling {@code getEJBObject()}. + * @see AopContext + */ + boolean isExposeProxy(); + + /** + * Set whether this proxy configuration is pre-filtered so that it only + * contains applicable advisors (matching this proxy's target class). + *

Default is "false". Set this to "true" if the advisors have been + * pre-filtered already, meaning that the ClassFilter check can be skipped + * when building the actual advisor chain for proxy invocations. + * @see org.springframework.aop.ClassFilter + */ + void setPreFiltered(boolean preFiltered); + + /** + * Return whether this proxy configuration is pre-filtered so that it only + * contains applicable advisors (matching this proxy's target class). + */ + boolean isPreFiltered(); + + /** + * Return the advisors applying to this proxy. + * @return a list of Advisors applying to this proxy (never {@code null}) + */ + Advisor[] getAdvisors(); + + /** + * Return the number of advisors applying to this proxy. + *

The default implementation delegates to {@code getAdvisors().length}. + * @since 5.3.1 + */ + default int getAdvisorCount() { + return getAdvisors().length; + } + + /** + * Add an advisor at the end of the advisor chain. + *

The Advisor may be an {@link org.springframework.aop.IntroductionAdvisor}, + * in which new interfaces will be available when a proxy is next obtained + * from the relevant factory. + * @param advisor the advisor to add to the end of the chain + * @throws AopConfigException in case of invalid advice + */ + void addAdvisor(Advisor advisor) throws AopConfigException; + + /** + * Add an Advisor at the specified position in the chain. + * @param advisor the advisor to add at the specified position in the chain + * @param pos position in chain (0 is head). Must be valid. + * @throws AopConfigException in case of invalid advice + */ + void addAdvisor(int pos, Advisor advisor) throws AopConfigException; + + /** + * Remove the given advisor. + * @param advisor the advisor to remove + * @return {@code true} if the advisor was removed; {@code false} + * if the advisor was not found and hence could not be removed + */ + boolean removeAdvisor(Advisor advisor); + + /** + * Remove the advisor at the given index. + * @param index the index of advisor to remove + * @throws AopConfigException if the index is invalid + */ + void removeAdvisor(int index) throws AopConfigException; + + /** + * Return the index (from 0) of the given advisor, + * or -1 if no such advisor applies to this proxy. + *

The return value of this method can be used to index into the advisors array. + * @param advisor the advisor to search for + * @return index from 0 of this advisor, or -1 if there's no such advisor + */ + int indexOf(Advisor advisor); + + /** + * Replace the given advisor. + *

Note: If the advisor is an {@link org.springframework.aop.IntroductionAdvisor} + * and the replacement is not or implements different interfaces, the proxy will need + * to be re-obtained or the old interfaces won't be supported and the new interface + * won't be implemented. + * @param a the advisor to replace + * @param b the advisor to replace it with + * @return whether it was replaced. If the advisor wasn't found in the + * list of advisors, this method returns {@code false} and does nothing. + * @throws AopConfigException in case of invalid advice + */ + boolean replaceAdvisor(Advisor a, Advisor b) throws AopConfigException; + + /** + * Add the given AOP Alliance advice to the tail of the advice (interceptor) chain. + *

This will be wrapped in a DefaultPointcutAdvisor with a pointcut that always + * applies, and returned from the {@code getAdvisors()} method in this wrapped form. + *

Note that the given advice will apply to all invocations on the proxy, + * even to the {@code toString()} method! Use appropriate advice implementations + * or specify appropriate pointcuts to apply to a narrower set of methods. + * @param advice the advice to add to the tail of the chain + * @throws AopConfigException in case of invalid advice + * @see #addAdvice(int, Advice) + * @see org.springframework.aop.support.DefaultPointcutAdvisor + */ + void addAdvice(Advice advice) throws AopConfigException; + + /** + * Add the given AOP Alliance Advice at the specified position in the advice chain. + *

This will be wrapped in a {@link org.springframework.aop.support.DefaultPointcutAdvisor} + * with a pointcut that always applies, and returned from the {@link #getAdvisors()} + * method in this wrapped form. + *

Note: The given advice will apply to all invocations on the proxy, + * even to the {@code toString()} method! Use appropriate advice implementations + * or specify appropriate pointcuts to apply to a narrower set of methods. + * @param pos index from 0 (head) + * @param advice the advice to add at the specified position in the advice chain + * @throws AopConfigException in case of invalid advice + */ + void addAdvice(int pos, Advice advice) throws AopConfigException; + + /** + * Remove the Advisor containing the given advice. + * @param advice the advice to remove + * @return {@code true} of the advice was found and removed; + * {@code false} if there was no such advice + */ + boolean removeAdvice(Advice advice); + + /** + * Return the index (from 0) of the given AOP Alliance Advice, + * or -1 if no such advice is an advice for this proxy. + *

The return value of this method can be used to index into + * the advisors array. + * @param advice the AOP Alliance advice to search for + * @return index from 0 of this advice, or -1 if there's no such advice + */ + int indexOf(Advice advice); + + /** + * As {@code toString()} will normally be delegated to the target, + * this returns the equivalent for the AOP proxy. + * @return a string description of the proxy configuration + */ + String toProxyConfigString(); + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java b/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java new file mode 100644 index 0000000..5664cf7 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java @@ -0,0 +1,604 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.aopalliance.aop.Advice; + +import org.springframework.aop.Advisor; +import org.springframework.aop.DynamicIntroductionAdvice; +import org.springframework.aop.IntroductionAdvisor; +import org.springframework.aop.IntroductionInfo; +import org.springframework.aop.TargetSource; +import org.springframework.aop.support.DefaultIntroductionAdvisor; +import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.aop.target.EmptyTargetSource; +import org.springframework.aop.target.SingletonTargetSource; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; + +/** + * Base class for AOP proxy configuration managers. + * These are not themselves AOP proxies, but subclasses of this class are + * normally factories from which AOP proxy instances are obtained directly. + * + *

This class frees subclasses of the housekeeping of Advices + * and Advisors, but doesn't actually implement proxy creation + * methods, which are provided by subclasses. + * + *

This class is serializable; subclasses need not be. + * This class is used to hold snapshots of proxies. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see org.springframework.aop.framework.AopProxy + */ +public class AdvisedSupport extends ProxyConfig implements Advised { + + /** use serialVersionUID from Spring 2.0 for interoperability. */ + private static final long serialVersionUID = 2651364800145442165L; + + + /** + * Canonical TargetSource when there's no target, and behavior is + * supplied by the advisors. + */ + public static final TargetSource EMPTY_TARGET_SOURCE = EmptyTargetSource.INSTANCE; + + + /** Package-protected to allow direct access for efficiency. */ + TargetSource targetSource = EMPTY_TARGET_SOURCE; + + /** Whether the Advisors are already filtered for the specific target class. */ + private boolean preFiltered = false; + + /** The AdvisorChainFactory to use. */ + AdvisorChainFactory advisorChainFactory = new DefaultAdvisorChainFactory(); + + /** Cache with Method as key and advisor chain List as value. */ + private transient Map> methodCache; + + /** + * Interfaces to be implemented by the proxy. Held in List to keep the order + * of registration, to create JDK proxy with specified order of interfaces. + */ + private List> interfaces = new ArrayList<>(); + + /** + * List of Advisors. If an Advice is added, it will be wrapped + * in an Advisor before being added to this List. + */ + private List advisors = new ArrayList<>(); + + + /** + * No-arg constructor for use as a JavaBean. + */ + public AdvisedSupport() { + this.methodCache = new ConcurrentHashMap<>(32); + } + + /** + * Create a AdvisedSupport instance with the given parameters. + * @param interfaces the proxied interfaces + */ + public AdvisedSupport(Class... interfaces) { + this(); + setInterfaces(interfaces); + } + + + /** + * Set the given object as target. + * Will create a SingletonTargetSource for the object. + * @see #setTargetSource + * @see org.springframework.aop.target.SingletonTargetSource + */ + public void setTarget(Object target) { + setTargetSource(new SingletonTargetSource(target)); + } + + @Override + public void setTargetSource(@Nullable TargetSource targetSource) { + this.targetSource = (targetSource != null ? targetSource : EMPTY_TARGET_SOURCE); + } + + @Override + public TargetSource getTargetSource() { + return this.targetSource; + } + + /** + * Set a target class to be proxied, indicating that the proxy + * should be castable to the given class. + *

Internally, an {@link org.springframework.aop.target.EmptyTargetSource} + * for the given target class will be used. The kind of proxy needed + * will be determined on actual creation of the proxy. + *

This is a replacement for setting a "targetSource" or "target", + * for the case where we want a proxy based on a target class + * (which can be an interface or a concrete class) without having + * a fully capable TargetSource available. + * @see #setTargetSource + * @see #setTarget + */ + public void setTargetClass(@Nullable Class targetClass) { + this.targetSource = EmptyTargetSource.forClass(targetClass); + } + + @Override + @Nullable + public Class getTargetClass() { + return this.targetSource.getTargetClass(); + } + + @Override + public void setPreFiltered(boolean preFiltered) { + this.preFiltered = preFiltered; + } + + @Override + public boolean isPreFiltered() { + return this.preFiltered; + } + + /** + * Set the advisor chain factory to use. + *

Default is a {@link DefaultAdvisorChainFactory}. + */ + public void setAdvisorChainFactory(AdvisorChainFactory advisorChainFactory) { + Assert.notNull(advisorChainFactory, "AdvisorChainFactory must not be null"); + this.advisorChainFactory = advisorChainFactory; + } + + /** + * Return the advisor chain factory to use (never {@code null}). + */ + public AdvisorChainFactory getAdvisorChainFactory() { + return this.advisorChainFactory; + } + + + /** + * Set the interfaces to be proxied. + */ + public void setInterfaces(Class... interfaces) { + Assert.notNull(interfaces, "Interfaces must not be null"); + this.interfaces.clear(); + for (Class ifc : interfaces) { + addInterface(ifc); + } + } + + /** + * Add a new proxied interface. + * @param intf the additional interface to proxy + */ + public void addInterface(Class intf) { + Assert.notNull(intf, "Interface must not be null"); + if (!intf.isInterface()) { + throw new IllegalArgumentException("[" + intf.getName() + "] is not an interface"); + } + if (!this.interfaces.contains(intf)) { + this.interfaces.add(intf); + adviceChanged(); + } + } + + /** + * Remove a proxied interface. + *

Does nothing if the given interface isn't proxied. + * @param intf the interface to remove from the proxy + * @return {@code true} if the interface was removed; {@code false} + * if the interface was not found and hence could not be removed + */ + public boolean removeInterface(Class intf) { + return this.interfaces.remove(intf); + } + + @Override + public Class[] getProxiedInterfaces() { + return ClassUtils.toClassArray(this.interfaces); + } + + @Override + public boolean isInterfaceProxied(Class intf) { + for (Class proxyIntf : this.interfaces) { + if (intf.isAssignableFrom(proxyIntf)) { + return true; + } + } + return false; + } + + + @Override + public final Advisor[] getAdvisors() { + return this.advisors.toArray(new Advisor[0]); + } + + @Override + public int getAdvisorCount() { + return this.advisors.size(); + } + + @Override + public void addAdvisor(Advisor advisor) { + int pos = this.advisors.size(); + addAdvisor(pos, advisor); + } + + @Override + public void addAdvisor(int pos, Advisor advisor) throws AopConfigException { + if (advisor instanceof IntroductionAdvisor) { + validateIntroductionAdvisor((IntroductionAdvisor) advisor); + } + addAdvisorInternal(pos, advisor); + } + + @Override + public boolean removeAdvisor(Advisor advisor) { + int index = indexOf(advisor); + if (index == -1) { + return false; + } + else { + removeAdvisor(index); + return true; + } + } + + @Override + public void removeAdvisor(int index) throws AopConfigException { + if (isFrozen()) { + throw new AopConfigException("Cannot remove Advisor: Configuration is frozen."); + } + if (index < 0 || index > this.advisors.size() - 1) { + throw new AopConfigException("Advisor index " + index + " is out of bounds: " + + "This configuration only has " + this.advisors.size() + " advisors."); + } + + Advisor advisor = this.advisors.remove(index); + if (advisor instanceof IntroductionAdvisor) { + IntroductionAdvisor ia = (IntroductionAdvisor) advisor; + // We need to remove introduction interfaces. + for (Class ifc : ia.getInterfaces()) { + removeInterface(ifc); + } + } + + adviceChanged(); + } + + @Override + public int indexOf(Advisor advisor) { + Assert.notNull(advisor, "Advisor must not be null"); + return this.advisors.indexOf(advisor); + } + + @Override + public boolean replaceAdvisor(Advisor a, Advisor b) throws AopConfigException { + Assert.notNull(a, "Advisor a must not be null"); + Assert.notNull(b, "Advisor b must not be null"); + int index = indexOf(a); + if (index == -1) { + return false; + } + removeAdvisor(index); + addAdvisor(index, b); + return true; + } + + /** + * Add all of the given advisors to this proxy configuration. + * @param advisors the advisors to register + */ + public void addAdvisors(Advisor... advisors) { + addAdvisors(Arrays.asList(advisors)); + } + + /** + * Add all of the given advisors to this proxy configuration. + * @param advisors the advisors to register + */ + public void addAdvisors(Collection advisors) { + if (isFrozen()) { + throw new AopConfigException("Cannot add advisor: Configuration is frozen."); + } + if (!CollectionUtils.isEmpty(advisors)) { + for (Advisor advisor : advisors) { + if (advisor instanceof IntroductionAdvisor) { + validateIntroductionAdvisor((IntroductionAdvisor) advisor); + } + Assert.notNull(advisor, "Advisor must not be null"); + this.advisors.add(advisor); + } + adviceChanged(); + } + } + + private void validateIntroductionAdvisor(IntroductionAdvisor advisor) { + advisor.validateInterfaces(); + // If the advisor passed validation, we can make the change. + Class[] ifcs = advisor.getInterfaces(); + for (Class ifc : ifcs) { + addInterface(ifc); + } + } + + private void addAdvisorInternal(int pos, Advisor advisor) throws AopConfigException { + Assert.notNull(advisor, "Advisor must not be null"); + if (isFrozen()) { + throw new AopConfigException("Cannot add advisor: Configuration is frozen."); + } + if (pos > this.advisors.size()) { + throw new IllegalArgumentException( + "Illegal position " + pos + " in advisor list with size " + this.advisors.size()); + } + this.advisors.add(pos, advisor); + adviceChanged(); + } + + /** + * Allows uncontrolled access to the {@link List} of {@link Advisor Advisors}. + *

Use with care, and remember to {@link #adviceChanged() fire advice changed events} + * when making any modifications. + */ + protected final List getAdvisorsInternal() { + return this.advisors; + } + + @Override + public void addAdvice(Advice advice) throws AopConfigException { + int pos = this.advisors.size(); + addAdvice(pos, advice); + } + + /** + * Cannot add introductions this way unless the advice implements IntroductionInfo. + */ + @Override + public void addAdvice(int pos, Advice advice) throws AopConfigException { + Assert.notNull(advice, "Advice must not be null"); + if (advice instanceof IntroductionInfo) { + // We don't need an IntroductionAdvisor for this kind of introduction: + // It's fully self-describing. + addAdvisor(pos, new DefaultIntroductionAdvisor(advice, (IntroductionInfo) advice)); + } + else if (advice instanceof DynamicIntroductionAdvice) { + // We need an IntroductionAdvisor for this kind of introduction. + throw new AopConfigException("DynamicIntroductionAdvice may only be added as part of IntroductionAdvisor"); + } + else { + addAdvisor(pos, new DefaultPointcutAdvisor(advice)); + } + } + + @Override + public boolean removeAdvice(Advice advice) throws AopConfigException { + int index = indexOf(advice); + if (index == -1) { + return false; + } + else { + removeAdvisor(index); + return true; + } + } + + @Override + public int indexOf(Advice advice) { + Assert.notNull(advice, "Advice must not be null"); + for (int i = 0; i < this.advisors.size(); i++) { + Advisor advisor = this.advisors.get(i); + if (advisor.getAdvice() == advice) { + return i; + } + } + return -1; + } + + /** + * Is the given advice included in any advisor within this proxy configuration? + * @param advice the advice to check inclusion of + * @return whether this advice instance is included + */ + public boolean adviceIncluded(@Nullable Advice advice) { + if (advice != null) { + for (Advisor advisor : this.advisors) { + if (advisor.getAdvice() == advice) { + return true; + } + } + } + return false; + } + + /** + * Count advices of the given class. + * @param adviceClass the advice class to check + * @return the count of the interceptors of this class or subclasses + */ + public int countAdvicesOfType(@Nullable Class adviceClass) { + int count = 0; + if (adviceClass != null) { + for (Advisor advisor : this.advisors) { + if (adviceClass.isInstance(advisor.getAdvice())) { + count++; + } + } + } + return count; + } + + + /** + * Determine a list of {@link org.aopalliance.intercept.MethodInterceptor} objects + * for the given method, based on this configuration. + * @param method the proxied method + * @param targetClass the target class + * @return a List of MethodInterceptors (may also include InterceptorAndDynamicMethodMatchers) + */ + public List getInterceptorsAndDynamicInterceptionAdvice(Method method, @Nullable Class targetClass) { + MethodCacheKey cacheKey = new MethodCacheKey(method); + List cached = this.methodCache.get(cacheKey); + if (cached == null) { + cached = this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice( + this, method, targetClass); + this.methodCache.put(cacheKey, cached); + } + return cached; + } + + /** + * Invoked when advice has changed. + */ + protected void adviceChanged() { + this.methodCache.clear(); + } + + /** + * Call this method on a new instance created by the no-arg constructor + * to create an independent copy of the configuration from the given object. + * @param other the AdvisedSupport object to copy configuration from + */ + protected void copyConfigurationFrom(AdvisedSupport other) { + copyConfigurationFrom(other, other.targetSource, new ArrayList<>(other.advisors)); + } + + /** + * Copy the AOP configuration from the given AdvisedSupport object, + * but allow substitution of a fresh TargetSource and a given interceptor chain. + * @param other the AdvisedSupport object to take proxy configuration from + * @param targetSource the new TargetSource + * @param advisors the Advisors for the chain + */ + protected void copyConfigurationFrom(AdvisedSupport other, TargetSource targetSource, List advisors) { + copyFrom(other); + this.targetSource = targetSource; + this.advisorChainFactory = other.advisorChainFactory; + this.interfaces = new ArrayList<>(other.interfaces); + for (Advisor advisor : advisors) { + if (advisor instanceof IntroductionAdvisor) { + validateIntroductionAdvisor((IntroductionAdvisor) advisor); + } + Assert.notNull(advisor, "Advisor must not be null"); + this.advisors.add(advisor); + } + adviceChanged(); + } + + /** + * Build a configuration-only copy of this AdvisedSupport, + * replacing the TargetSource. + */ + AdvisedSupport getConfigurationOnlyCopy() { + AdvisedSupport copy = new AdvisedSupport(); + copy.copyFrom(this); + copy.targetSource = EmptyTargetSource.forClass(getTargetClass(), getTargetSource().isStatic()); + copy.advisorChainFactory = this.advisorChainFactory; + copy.interfaces = this.interfaces; + copy.advisors = this.advisors; + return copy; + } + + + //--------------------------------------------------------------------- + // Serialization support + //--------------------------------------------------------------------- + + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + // Rely on default serialization; just initialize state after deserialization. + ois.defaultReadObject(); + + // Initialize transient fields. + this.methodCache = new ConcurrentHashMap<>(32); + } + + @Override + public String toProxyConfigString() { + return toString(); + } + + /** + * For debugging/diagnostic use. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(getClass().getName()); + sb.append(": ").append(this.interfaces.size()).append(" interfaces "); + sb.append(ClassUtils.classNamesToString(this.interfaces)).append("; "); + sb.append(this.advisors.size()).append(" advisors "); + sb.append(this.advisors).append("; "); + sb.append("targetSource [").append(this.targetSource).append("]; "); + sb.append(super.toString()); + return sb.toString(); + } + + + /** + * Simple wrapper class around a Method. Used as the key when + * caching methods, for efficient equals and hashCode comparisons. + */ + private static final class MethodCacheKey implements Comparable { + + private final Method method; + + private final int hashCode; + + public MethodCacheKey(Method method) { + this.method = method; + this.hashCode = method.hashCode(); + } + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof MethodCacheKey && + this.method == ((MethodCacheKey) other).method)); + } + + @Override + public int hashCode() { + return this.hashCode; + } + + @Override + public String toString() { + return this.method.toString(); + } + + @Override + public int compareTo(MethodCacheKey other) { + int result = this.method.getName().compareTo(other.method.getName()); + if (result == 0) { + result = this.method.toString().compareTo(other.method.toString()); + } + return result; + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupportListener.java b/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupportListener.java new file mode 100644 index 0000000..c9f4dd7 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupportListener.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2007 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +/** + * Listener to be registered on {@link ProxyCreatorSupport} objects + * Allows for receiving callbacks on activation and change of advice. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see ProxyCreatorSupport#addListener + */ +public interface AdvisedSupportListener { + + /** + * Invoked when the first proxy is created. + * @param advised the AdvisedSupport object + */ + void activated(AdvisedSupport advised); + + /** + * Invoked when advice is changed after a proxy is created. + * @param advised the AdvisedSupport object + */ + void adviceChanged(AdvisedSupport advised); + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AdvisorChainFactory.java b/spring-aop/src/main/java/org/springframework/aop/framework/AdvisorChainFactory.java new file mode 100644 index 0000000..3d31b8c --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AdvisorChainFactory.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import java.lang.reflect.Method; +import java.util.List; + +import org.springframework.lang.Nullable; + +/** + * Factory interface for advisor chains. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +public interface AdvisorChainFactory { + + /** + * Determine a list of {@link org.aopalliance.intercept.MethodInterceptor} objects + * for the given advisor chain configuration. + * @param config the AOP configuration in the form of an Advised object + * @param method the proxied method + * @param targetClass the target class (may be {@code null} to indicate a proxy without + * target object, in which case the method's declaring class is the next best option) + * @return a List of MethodInterceptors (may also include InterceptorAndDynamicMethodMatchers) + */ + List getInterceptorsAndDynamicInterceptionAdvice(Advised config, Method method, @Nullable Class targetClass); + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AopConfigException.java b/spring-aop/src/main/java/org/springframework/aop/framework/AopConfigException.java new file mode 100644 index 0000000..b58b0dd --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AopConfigException.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import org.springframework.core.NestedRuntimeException; + +/** + * Exception that gets thrown on illegal AOP configuration arguments. + * + * @author Rod Johnson + * @since 13.03.2003 + */ +@SuppressWarnings("serial") +public class AopConfigException extends NestedRuntimeException { + + /** + * Constructor for AopConfigException. + * @param msg the detail message + */ + public AopConfigException(String msg) { + super(msg); + } + + /** + * Constructor for AopConfigException. + * @param msg the detail message + * @param cause the root cause + */ + public AopConfigException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AopContext.java b/spring-aop/src/main/java/org/springframework/aop/framework/AopContext.java new file mode 100644 index 0000000..9653ced --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AopContext.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import org.springframework.core.NamedThreadLocal; +import org.springframework.lang.Nullable; + +/** + * Class containing static methods used to obtain information about the current AOP invocation. + * + *

The {@code currentProxy()} method is usable if the AOP framework is configured to + * expose the current proxy (not the default). It returns the AOP proxy in use. Target objects + * or advice can use this to make advised calls, in the same way as {@code getEJBObject()} + * can be used in EJBs. They can also use it to find advice configuration. + * + *

Spring's AOP framework does not expose proxies by default, as there is a performance cost + * in doing so. + * + *

The functionality in this class might be used by a target object that needed access + * to resources on the invocation. However, this approach should not be used when there is + * a reasonable alternative, as it makes application code dependent on usage under AOP and + * the Spring AOP framework in particular. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 13.03.2003 + */ +public final class AopContext { + + /** + * ThreadLocal holder for AOP proxy associated with this thread. + * Will contain {@code null} unless the "exposeProxy" property on + * the controlling proxy configuration has been set to "true". + * @see ProxyConfig#setExposeProxy + */ + private static final ThreadLocal currentProxy = new NamedThreadLocal<>("Current AOP proxy"); + + + private AopContext() { + } + + + /** + * Try to return the current AOP proxy. This method is usable only if the + * calling method has been invoked via AOP, and the AOP framework has been set + * to expose proxies. Otherwise, this method will throw an IllegalStateException. + * @return the current AOP proxy (never returns {@code null}) + * @throws IllegalStateException if the proxy cannot be found, because the + * method was invoked outside an AOP invocation context, or because the + * AOP framework has not been configured to expose the proxy + */ + public static Object currentProxy() throws IllegalStateException { + Object proxy = currentProxy.get(); + if (proxy == null) { + throw new IllegalStateException( + "Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available, and " + + "ensure that AopContext.currentProxy() is invoked in the same thread as the AOP invocation context."); + } + return proxy; + } + + /** + * Make the given proxy available via the {@code currentProxy()} method. + *

Note that the caller should be careful to keep the old value as appropriate. + * @param proxy the proxy to expose (or {@code null} to reset it) + * @return the old proxy, which may be {@code null} if none was bound + * @see #currentProxy() + */ + @Nullable + static Object setCurrentProxy(@Nullable Object proxy) { + Object old = currentProxy.get(); + if (proxy != null) { + currentProxy.set(proxy); + } + else { + currentProxy.remove(); + } + return old; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AopInfrastructureBean.java b/spring-aop/src/main/java/org/springframework/aop/framework/AopInfrastructureBean.java new file mode 100644 index 0000000..3168337 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AopInfrastructureBean.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2007 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +/** + * Marker interface that indicates a bean that is part of Spring's + * AOP infrastructure. In particular, this implies that any such bean + * is not subject to auto-proxying, even if a pointcut would match. + * + * @author Juergen Hoeller + * @since 2.0.3 + * @see org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator + * @see org.springframework.aop.scope.ScopedProxyFactoryBean + */ +public interface AopInfrastructureBean { + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AopProxy.java b/spring-aop/src/main/java/org/springframework/aop/framework/AopProxy.java new file mode 100644 index 0000000..cc7a3fd --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AopProxy.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import org.springframework.lang.Nullable; + +/** + * Delegate interface for a configured AOP proxy, allowing for the creation + * of actual proxy objects. + * + *

Out-of-the-box implementations are available for JDK dynamic proxies + * and for CGLIB proxies, as applied by {@link DefaultAopProxyFactory}. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see DefaultAopProxyFactory + */ +public interface AopProxy { + + /** + * Create a new proxy object. + *

Uses the AopProxy's default class loader (if necessary for proxy creation): + * usually, the thread context class loader. + * @return the new proxy object (never {@code null}) + * @see Thread#getContextClassLoader() + */ + Object getProxy(); + + /** + * Create a new proxy object. + *

Uses the given class loader (if necessary for proxy creation). + * {@code null} will simply be passed down and thus lead to the low-level + * proxy facility's default, which is usually different from the default chosen + * by the AopProxy implementation's {@link #getProxy()} method. + * @param classLoader the class loader to create the proxy with + * (or {@code null} for the low-level proxy facility's default) + * @return the new proxy object (never {@code null}) + */ + Object getProxy(@Nullable ClassLoader classLoader); + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyFactory.java b/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyFactory.java new file mode 100644 index 0000000..6365ee3 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyFactory.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +/** + * Interface to be implemented by factories that are able to create + * AOP proxies based on {@link AdvisedSupport} configuration objects. + * + *

Proxies should observe the following contract: + *

    + *
  • They should implement all interfaces that the configuration + * indicates should be proxied. + *
  • They should implement the {@link Advised} interface. + *
  • They should implement the equals method to compare proxied + * interfaces, advice, and target. + *
  • They should be serializable if all advisors and target + * are serializable. + *
  • They should be thread-safe if advisors and target + * are thread-safe. + *
+ * + *

Proxies may or may not allow advice changes to be made. + * If they do not permit advice changes (for example, because + * the configuration was frozen) a proxy should throw an + * {@link AopConfigException} on an attempted advice change. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +public interface AopProxyFactory { + + /** + * Create an {@link AopProxy} for the given AOP configuration. + * @param config the AOP configuration in the form of an + * AdvisedSupport object + * @return the corresponding AOP proxy + * @throws AopConfigException if the configuration is invalid + */ + AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException; + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyUtils.java b/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyUtils.java new file mode 100644 index 0000000..5dd1874 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyUtils.java @@ -0,0 +1,249 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Arrays; + +import org.springframework.aop.SpringProxy; +import org.springframework.aop.TargetClassAware; +import org.springframework.aop.TargetSource; +import org.springframework.aop.support.AopUtils; +import org.springframework.aop.target.SingletonTargetSource; +import org.springframework.core.DecoratingProxy; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Utility methods for AOP proxy factories. + * Mainly for internal use within the AOP framework. + * + *

See {@link org.springframework.aop.support.AopUtils} for a collection of + * generic AOP utility methods which do not depend on AOP framework internals. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see org.springframework.aop.support.AopUtils + */ +public abstract class AopProxyUtils { + + /** + * Obtain the singleton target object behind the given proxy, if any. + * @param candidate the (potential) proxy to check + * @return the singleton target object managed in a {@link SingletonTargetSource}, + * or {@code null} in any other case (not a proxy, not an existing singleton target) + * @since 4.3.8 + * @see Advised#getTargetSource() + * @see SingletonTargetSource#getTarget() + */ + @Nullable + public static Object getSingletonTarget(Object candidate) { + if (candidate instanceof Advised) { + TargetSource targetSource = ((Advised) candidate).getTargetSource(); + if (targetSource instanceof SingletonTargetSource) { + return ((SingletonTargetSource) targetSource).getTarget(); + } + } + return null; + } + + /** + * Determine the ultimate target class of the given bean instance, traversing + * not only a top-level proxy but any number of nested proxies as well — + * as long as possible without side effects, that is, just for singleton targets. + * @param candidate the instance to check (might be an AOP proxy) + * @return the ultimate target class (or the plain class of the given + * object as fallback; never {@code null}) + * @see org.springframework.aop.TargetClassAware#getTargetClass() + * @see Advised#getTargetSource() + */ + public static Class ultimateTargetClass(Object candidate) { + Assert.notNull(candidate, "Candidate object must not be null"); + Object current = candidate; + Class result = null; + while (current instanceof TargetClassAware) { + result = ((TargetClassAware) current).getTargetClass(); + current = getSingletonTarget(current); + } + if (result == null) { + result = (AopUtils.isCglibProxy(candidate) ? candidate.getClass().getSuperclass() : candidate.getClass()); + } + return result; + } + + /** + * Determine the complete set of interfaces to proxy for the given AOP configuration. + *

This will always add the {@link Advised} interface unless the AdvisedSupport's + * {@link AdvisedSupport#setOpaque "opaque"} flag is on. Always adds the + * {@link org.springframework.aop.SpringProxy} marker interface. + * @param advised the proxy config + * @return the complete set of interfaces to proxy + * @see SpringProxy + * @see Advised + */ + public static Class[] completeProxiedInterfaces(AdvisedSupport advised) { + return completeProxiedInterfaces(advised, false); + } + + /** + * Determine the complete set of interfaces to proxy for the given AOP configuration. + *

This will always add the {@link Advised} interface unless the AdvisedSupport's + * {@link AdvisedSupport#setOpaque "opaque"} flag is on. Always adds the + * {@link org.springframework.aop.SpringProxy} marker interface. + * @param advised the proxy config + * @param decoratingProxy whether to expose the {@link DecoratingProxy} interface + * @return the complete set of interfaces to proxy + * @since 4.3 + * @see SpringProxy + * @see Advised + * @see DecoratingProxy + */ + static Class[] completeProxiedInterfaces(AdvisedSupport advised, boolean decoratingProxy) { + Class[] specifiedInterfaces = advised.getProxiedInterfaces(); + if (specifiedInterfaces.length == 0) { + // No user-specified interfaces: check whether target class is an interface. + Class targetClass = advised.getTargetClass(); + if (targetClass != null) { + if (targetClass.isInterface()) { + advised.setInterfaces(targetClass); + } + else if (Proxy.isProxyClass(targetClass)) { + advised.setInterfaces(targetClass.getInterfaces()); + } + specifiedInterfaces = advised.getProxiedInterfaces(); + } + } + boolean addSpringProxy = !advised.isInterfaceProxied(SpringProxy.class); + boolean addAdvised = !advised.isOpaque() && !advised.isInterfaceProxied(Advised.class); + boolean addDecoratingProxy = (decoratingProxy && !advised.isInterfaceProxied(DecoratingProxy.class)); + int nonUserIfcCount = 0; + if (addSpringProxy) { + nonUserIfcCount++; + } + if (addAdvised) { + nonUserIfcCount++; + } + if (addDecoratingProxy) { + nonUserIfcCount++; + } + Class[] proxiedInterfaces = new Class[specifiedInterfaces.length + nonUserIfcCount]; + System.arraycopy(specifiedInterfaces, 0, proxiedInterfaces, 0, specifiedInterfaces.length); + int index = specifiedInterfaces.length; + if (addSpringProxy) { + proxiedInterfaces[index] = SpringProxy.class; + index++; + } + if (addAdvised) { + proxiedInterfaces[index] = Advised.class; + index++; + } + if (addDecoratingProxy) { + proxiedInterfaces[index] = DecoratingProxy.class; + } + return proxiedInterfaces; + } + + /** + * Extract the user-specified interfaces that the given proxy implements, + * i.e. all non-Advised interfaces that the proxy implements. + * @param proxy the proxy to analyze (usually a JDK dynamic proxy) + * @return all user-specified interfaces that the proxy implements, + * in the original order (never {@code null} or empty) + * @see Advised + */ + public static Class[] proxiedUserInterfaces(Object proxy) { + Class[] proxyInterfaces = proxy.getClass().getInterfaces(); + int nonUserIfcCount = 0; + if (proxy instanceof SpringProxy) { + nonUserIfcCount++; + } + if (proxy instanceof Advised) { + nonUserIfcCount++; + } + if (proxy instanceof DecoratingProxy) { + nonUserIfcCount++; + } + Class[] userInterfaces = Arrays.copyOf(proxyInterfaces, proxyInterfaces.length - nonUserIfcCount); + Assert.notEmpty(userInterfaces, "JDK proxy must implement one or more interfaces"); + return userInterfaces; + } + + /** + * Check equality of the proxies behind the given AdvisedSupport objects. + * Not the same as equality of the AdvisedSupport objects: + * rather, equality of interfaces, advisors and target sources. + */ + public static boolean equalsInProxy(AdvisedSupport a, AdvisedSupport b) { + return (a == b || + (equalsProxiedInterfaces(a, b) && equalsAdvisors(a, b) && a.getTargetSource().equals(b.getTargetSource()))); + } + + /** + * Check equality of the proxied interfaces behind the given AdvisedSupport objects. + */ + public static boolean equalsProxiedInterfaces(AdvisedSupport a, AdvisedSupport b) { + return Arrays.equals(a.getProxiedInterfaces(), b.getProxiedInterfaces()); + } + + /** + * Check equality of the advisors behind the given AdvisedSupport objects. + */ + public static boolean equalsAdvisors(AdvisedSupport a, AdvisedSupport b) { + return a.getAdvisorCount() == b.getAdvisorCount() && Arrays.equals(a.getAdvisors(), b.getAdvisors()); + } + + + /** + * Adapt the given arguments to the target signature in the given method, + * if necessary: in particular, if a given vararg argument array does not + * match the array type of the declared vararg parameter in the method. + * @param method the target method + * @param arguments the given arguments + * @return a cloned argument array, or the original if no adaptation is needed + * @since 4.2.3 + */ + static Object[] adaptArgumentsIfNecessary(Method method, @Nullable Object[] arguments) { + if (ObjectUtils.isEmpty(arguments)) { + return new Object[0]; + } + if (method.isVarArgs()) { + if (method.getParameterCount() == arguments.length) { + Class[] paramTypes = method.getParameterTypes(); + int varargIndex = paramTypes.length - 1; + Class varargType = paramTypes[varargIndex]; + if (varargType.isArray()) { + Object varargArray = arguments[varargIndex]; + if (varargArray instanceof Object[] && !varargType.isInstance(varargArray)) { + Object[] newArguments = new Object[arguments.length]; + System.arraycopy(arguments, 0, newArguments, 0, varargIndex); + Class targetElementType = varargType.getComponentType(); + int varargLength = Array.getLength(varargArray); + Object newVarargArray = Array.newInstance(targetElementType, varargLength); + System.arraycopy(varargArray, 0, newVarargArray, 0, varargLength); + newArguments[varargIndex] = newVarargArray; + return newArguments; + } + } + } + } + return arguments; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java b/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java new file mode 100644 index 0000000..69fe9a0 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java @@ -0,0 +1,991 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.UndeclaredThrowableException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; + +import org.aopalliance.aop.Advice; +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aop.Advisor; +import org.springframework.aop.AopInvocationException; +import org.springframework.aop.PointcutAdvisor; +import org.springframework.aop.RawTargetAccess; +import org.springframework.aop.TargetSource; +import org.springframework.aop.support.AopUtils; +import org.springframework.cglib.core.ClassLoaderAwareGeneratorStrategy; +import org.springframework.cglib.core.CodeGenerationException; +import org.springframework.cglib.core.SpringNamingPolicy; +import org.springframework.cglib.proxy.Callback; +import org.springframework.cglib.proxy.CallbackFilter; +import org.springframework.cglib.proxy.Dispatcher; +import org.springframework.cglib.proxy.Enhancer; +import org.springframework.cglib.proxy.Factory; +import org.springframework.cglib.proxy.MethodInterceptor; +import org.springframework.cglib.proxy.MethodProxy; +import org.springframework.cglib.proxy.NoOp; +import org.springframework.core.SmartClassLoader; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; + +/** + * CGLIB-based {@link AopProxy} implementation for the Spring AOP framework. + * + *

Objects of this type should be obtained through proxy factories, + * configured by an {@link AdvisedSupport} object. This class is internal + * to Spring's AOP framework and need not be used directly by client code. + * + *

{@link DefaultAopProxyFactory} will automatically create CGLIB-based + * proxies if necessary, for example in case of proxying a target class + * (see the {@link DefaultAopProxyFactory attendant javadoc} for details). + * + *

Proxies created using this class are thread-safe if the underlying + * (target) class is thread-safe. + * + * @author Rod Johnson + * @author Rob Harrop + * @author Juergen Hoeller + * @author Ramnivas Laddad + * @author Chris Beams + * @author Dave Syer + * @see org.springframework.cglib.proxy.Enhancer + * @see AdvisedSupport#setProxyTargetClass + * @see DefaultAopProxyFactory + */ +@SuppressWarnings("serial") +class CglibAopProxy implements AopProxy, Serializable { + + // Constants for CGLIB callback array indices + private static final int AOP_PROXY = 0; + private static final int INVOKE_TARGET = 1; + private static final int NO_OVERRIDE = 2; + private static final int DISPATCH_TARGET = 3; + private static final int DISPATCH_ADVISED = 4; + private static final int INVOKE_EQUALS = 5; + private static final int INVOKE_HASHCODE = 6; + + + /** Logger available to subclasses; static to optimize serialization. */ + protected static final Log logger = LogFactory.getLog(CglibAopProxy.class); + + /** Keeps track of the Classes that we have validated for final methods. */ + private static final Map, Boolean> validatedClasses = new WeakHashMap<>(); + + + /** The configuration used to configure this proxy. */ + protected final AdvisedSupport advised; + + @Nullable + protected Object[] constructorArgs; + + @Nullable + protected Class[] constructorArgTypes; + + /** Dispatcher used for methods on Advised. */ + private final transient AdvisedDispatcher advisedDispatcher; + + private transient Map fixedInterceptorMap = Collections.emptyMap(); + + private transient int fixedInterceptorOffset; + + + /** + * Create a new CglibAopProxy for the given AOP configuration. + * @param config the AOP configuration as AdvisedSupport object + * @throws AopConfigException if the config is invalid. We try to throw an informative + * exception in this case, rather than let a mysterious failure happen later. + */ + public CglibAopProxy(AdvisedSupport config) throws AopConfigException { + Assert.notNull(config, "AdvisedSupport must not be null"); + if (config.getAdvisorCount() == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) { + throw new AopConfigException("No advisors and no TargetSource specified"); + } + this.advised = config; + this.advisedDispatcher = new AdvisedDispatcher(this.advised); + } + + /** + * Set constructor arguments to use for creating the proxy. + * @param constructorArgs the constructor argument values + * @param constructorArgTypes the constructor argument types + */ + public void setConstructorArguments(@Nullable Object[] constructorArgs, @Nullable Class[] constructorArgTypes) { + if (constructorArgs == null || constructorArgTypes == null) { + throw new IllegalArgumentException("Both 'constructorArgs' and 'constructorArgTypes' need to be specified"); + } + if (constructorArgs.length != constructorArgTypes.length) { + throw new IllegalArgumentException("Number of 'constructorArgs' (" + constructorArgs.length + + ") must match number of 'constructorArgTypes' (" + constructorArgTypes.length + ")"); + } + this.constructorArgs = constructorArgs; + this.constructorArgTypes = constructorArgTypes; + } + + + @Override + public Object getProxy() { + return getProxy(null); + } + + @Override + public Object getProxy(@Nullable ClassLoader classLoader) { + if (logger.isTraceEnabled()) { + logger.trace("Creating CGLIB proxy: " + this.advised.getTargetSource()); + } + + try { + Class rootClass = this.advised.getTargetClass(); + Assert.state(rootClass != null, "Target class must be available for creating a CGLIB proxy"); + + Class proxySuperClass = rootClass; + if (rootClass.getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR)) { + proxySuperClass = rootClass.getSuperclass(); + Class[] additionalInterfaces = rootClass.getInterfaces(); + for (Class additionalInterface : additionalInterfaces) { + this.advised.addInterface(additionalInterface); + } + } + + // Validate the class, writing log messages as necessary. + validateClassIfNecessary(proxySuperClass, classLoader); + + // Configure CGLIB Enhancer... + Enhancer enhancer = createEnhancer(); + if (classLoader != null) { + enhancer.setClassLoader(classLoader); + if (classLoader instanceof SmartClassLoader && + ((SmartClassLoader) classLoader).isClassReloadable(proxySuperClass)) { + enhancer.setUseCache(false); + } + } + enhancer.setSuperclass(proxySuperClass); + enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised)); + enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); + enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(classLoader)); + + Callback[] callbacks = getCallbacks(rootClass); + Class[] types = new Class[callbacks.length]; + for (int x = 0; x < types.length; x++) { + types[x] = callbacks[x].getClass(); + } + // fixedInterceptorMap only populated at this point, after getCallbacks call above + enhancer.setCallbackFilter(new ProxyCallbackFilter( + this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset)); + enhancer.setCallbackTypes(types); + + // Generate the proxy class and create a proxy instance. + return createProxyClassAndInstance(enhancer, callbacks); + } + catch (CodeGenerationException | IllegalArgumentException ex) { + throw new AopConfigException("Could not generate CGLIB subclass of " + this.advised.getTargetClass() + + ": Common causes of this problem include using a final class or a non-visible class", + ex); + } + catch (Throwable ex) { + // TargetSource.getTarget() failed + throw new AopConfigException("Unexpected AOP exception", ex); + } + } + + protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) { + enhancer.setInterceptDuringConstruction(false); + enhancer.setCallbacks(callbacks); + return (this.constructorArgs != null && this.constructorArgTypes != null ? + enhancer.create(this.constructorArgTypes, this.constructorArgs) : + enhancer.create()); + } + + /** + * Creates the CGLIB {@link Enhancer}. Subclasses may wish to override this to return a custom + * {@link Enhancer} implementation. + */ + protected Enhancer createEnhancer() { + return new Enhancer(); + } + + /** + * Checks to see whether the supplied {@code Class} has already been validated and + * validates it if not. + */ + private void validateClassIfNecessary(Class proxySuperClass, @Nullable ClassLoader proxyClassLoader) { + if (logger.isWarnEnabled()) { + synchronized (validatedClasses) { + if (!validatedClasses.containsKey(proxySuperClass)) { + doValidateClass(proxySuperClass, proxyClassLoader, + ClassUtils.getAllInterfacesForClassAsSet(proxySuperClass)); + validatedClasses.put(proxySuperClass, Boolean.TRUE); + } + } + } + } + + /** + * Checks for final methods on the given {@code Class}, as well as package-visible + * methods across ClassLoaders, and writes warnings to the log for each one found. + */ + private void doValidateClass(Class proxySuperClass, @Nullable ClassLoader proxyClassLoader, Set> ifcs) { + if (proxySuperClass != Object.class) { + Method[] methods = proxySuperClass.getDeclaredMethods(); + for (Method method : methods) { + int mod = method.getModifiers(); + if (!Modifier.isStatic(mod) && !Modifier.isPrivate(mod)) { + if (Modifier.isFinal(mod)) { + if (logger.isInfoEnabled() && implementsInterface(method, ifcs)) { + logger.info("Unable to proxy interface-implementing method [" + method + "] because " + + "it is marked as final: Consider using interface-based JDK proxies instead!"); + } + if (logger.isDebugEnabled()) { + logger.debug("Final method [" + method + "] cannot get proxied via CGLIB: " + + "Calls to this method will NOT be routed to the target instance and " + + "might lead to NPEs against uninitialized fields in the proxy instance."); + } + } + else if (logger.isDebugEnabled() && !Modifier.isPublic(mod) && !Modifier.isProtected(mod) && + proxyClassLoader != null && proxySuperClass.getClassLoader() != proxyClassLoader) { + logger.debug("Method [" + method + "] is package-visible across different ClassLoaders " + + "and cannot get proxied via CGLIB: Declare this method as public or protected " + + "if you need to support invocations through the proxy."); + } + } + } + doValidateClass(proxySuperClass.getSuperclass(), proxyClassLoader, ifcs); + } + } + + private Callback[] getCallbacks(Class rootClass) throws Exception { + // Parameters used for optimization choices... + boolean exposeProxy = this.advised.isExposeProxy(); + boolean isFrozen = this.advised.isFrozen(); + boolean isStatic = this.advised.getTargetSource().isStatic(); + + // Choose an "aop" interceptor (used for AOP calls). + Callback aopInterceptor = new DynamicAdvisedInterceptor(this.advised); + + // Choose a "straight to target" interceptor. (used for calls that are + // unadvised but can return this). May be required to expose the proxy. + Callback targetInterceptor; + if (exposeProxy) { + targetInterceptor = (isStatic ? + new StaticUnadvisedExposedInterceptor(this.advised.getTargetSource().getTarget()) : + new DynamicUnadvisedExposedInterceptor(this.advised.getTargetSource())); + } + else { + targetInterceptor = (isStatic ? + new StaticUnadvisedInterceptor(this.advised.getTargetSource().getTarget()) : + new DynamicUnadvisedInterceptor(this.advised.getTargetSource())); + } + + // Choose a "direct to target" dispatcher (used for + // unadvised calls to static targets that cannot return this). + Callback targetDispatcher = (isStatic ? + new StaticDispatcher(this.advised.getTargetSource().getTarget()) : new SerializableNoOp()); + + Callback[] mainCallbacks = new Callback[] { + aopInterceptor, // for normal advice + targetInterceptor, // invoke target without considering advice, if optimized + new SerializableNoOp(), // no override for methods mapped to this + targetDispatcher, this.advisedDispatcher, + new EqualsInterceptor(this.advised), + new HashCodeInterceptor(this.advised) + }; + + Callback[] callbacks; + + // If the target is a static one and the advice chain is frozen, + // then we can make some optimizations by sending the AOP calls + // direct to the target using the fixed chain for that method. + if (isStatic && isFrozen) { + Method[] methods = rootClass.getMethods(); + Callback[] fixedCallbacks = new Callback[methods.length]; + this.fixedInterceptorMap = CollectionUtils.newHashMap(methods.length); + + // TODO: small memory optimization here (can skip creation for methods with no advice) + for (int x = 0; x < methods.length; x++) { + Method method = methods[x]; + List chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, rootClass); + fixedCallbacks[x] = new FixedChainStaticTargetInterceptor( + chain, this.advised.getTargetSource().getTarget(), this.advised.getTargetClass()); + this.fixedInterceptorMap.put(method, x); + } + + // Now copy both the callbacks from mainCallbacks + // and fixedCallbacks into the callbacks array. + callbacks = new Callback[mainCallbacks.length + fixedCallbacks.length]; + System.arraycopy(mainCallbacks, 0, callbacks, 0, mainCallbacks.length); + System.arraycopy(fixedCallbacks, 0, callbacks, mainCallbacks.length, fixedCallbacks.length); + this.fixedInterceptorOffset = mainCallbacks.length; + } + else { + callbacks = mainCallbacks; + } + return callbacks; + } + + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof CglibAopProxy && + AopProxyUtils.equalsInProxy(this.advised, ((CglibAopProxy) other).advised))); + } + + @Override + public int hashCode() { + return CglibAopProxy.class.hashCode() * 13 + this.advised.getTargetSource().hashCode(); + } + + + /** + * Check whether the given method is declared on any of the given interfaces. + */ + private static boolean implementsInterface(Method method, Set> ifcs) { + for (Class ifc : ifcs) { + if (ClassUtils.hasMethod(ifc, method)) { + return true; + } + } + return false; + } + + /** + * Process a return value. Wraps a return of {@code this} if necessary to be the + * {@code proxy} and also verifies that {@code null} is not returned as a primitive. + */ + @Nullable + private static Object processReturnType( + Object proxy, @Nullable Object target, Method method, @Nullable Object returnValue) { + + // Massage return value if necessary + if (returnValue != null && returnValue == target && + !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) { + // Special case: it returned "this". Note that we can't help + // if the target sets a reference to itself in another returned object. + returnValue = proxy; + } + Class returnType = method.getReturnType(); + if (returnValue == null && returnType != Void.TYPE && returnType.isPrimitive()) { + throw new AopInvocationException( + "Null return value from advice does not match primitive return type for: " + method); + } + return returnValue; + } + + + /** + * Serializable replacement for CGLIB's NoOp interface. + * Public to allow use elsewhere in the framework. + */ + public static class SerializableNoOp implements NoOp, Serializable { + } + + + /** + * Method interceptor used for static targets with no advice chain. The call + * is passed directly back to the target. Used when the proxy needs to be + * exposed and it can't be determined that the method won't return + * {@code this}. + */ + private static class StaticUnadvisedInterceptor implements MethodInterceptor, Serializable { + + @Nullable + private final Object target; + + public StaticUnadvisedInterceptor(@Nullable Object target) { + this.target = target; + } + + @Override + @Nullable + public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { + Object retVal = methodProxy.invoke(this.target, args); + return processReturnType(proxy, this.target, method, retVal); + } + } + + + /** + * Method interceptor used for static targets with no advice chain, when the + * proxy is to be exposed. + */ + private static class StaticUnadvisedExposedInterceptor implements MethodInterceptor, Serializable { + + @Nullable + private final Object target; + + public StaticUnadvisedExposedInterceptor(@Nullable Object target) { + this.target = target; + } + + @Override + @Nullable + public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { + Object oldProxy = null; + try { + oldProxy = AopContext.setCurrentProxy(proxy); + Object retVal = methodProxy.invoke(this.target, args); + return processReturnType(proxy, this.target, method, retVal); + } + finally { + AopContext.setCurrentProxy(oldProxy); + } + } + } + + + /** + * Interceptor used to invoke a dynamic target without creating a method + * invocation or evaluating an advice chain. (We know there was no advice + * for this method.) + */ + private static class DynamicUnadvisedInterceptor implements MethodInterceptor, Serializable { + + private final TargetSource targetSource; + + public DynamicUnadvisedInterceptor(TargetSource targetSource) { + this.targetSource = targetSource; + } + + @Override + @Nullable + public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { + Object target = this.targetSource.getTarget(); + try { + Object retVal = methodProxy.invoke(target, args); + return processReturnType(proxy, target, method, retVal); + } + finally { + if (target != null) { + this.targetSource.releaseTarget(target); + } + } + } + } + + + /** + * Interceptor for unadvised dynamic targets when the proxy needs exposing. + */ + private static class DynamicUnadvisedExposedInterceptor implements MethodInterceptor, Serializable { + + private final TargetSource targetSource; + + public DynamicUnadvisedExposedInterceptor(TargetSource targetSource) { + this.targetSource = targetSource; + } + + @Override + @Nullable + public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { + Object oldProxy = null; + Object target = this.targetSource.getTarget(); + try { + oldProxy = AopContext.setCurrentProxy(proxy); + Object retVal = methodProxy.invoke(target, args); + return processReturnType(proxy, target, method, retVal); + } + finally { + AopContext.setCurrentProxy(oldProxy); + if (target != null) { + this.targetSource.releaseTarget(target); + } + } + } + } + + + /** + * Dispatcher for a static target. Dispatcher is much faster than + * interceptor. This will be used whenever it can be determined that a + * method definitely does not return "this" + */ + private static class StaticDispatcher implements Dispatcher, Serializable { + + @Nullable + private final Object target; + + public StaticDispatcher(@Nullable Object target) { + this.target = target; + } + + @Override + @Nullable + public Object loadObject() { + return this.target; + } + } + + + /** + * Dispatcher for any methods declared on the Advised class. + */ + private static class AdvisedDispatcher implements Dispatcher, Serializable { + + private final AdvisedSupport advised; + + public AdvisedDispatcher(AdvisedSupport advised) { + this.advised = advised; + } + + @Override + public Object loadObject() { + return this.advised; + } + } + + + /** + * Dispatcher for the {@code equals} method. + * Ensures that the method call is always handled by this class. + */ + private static class EqualsInterceptor implements MethodInterceptor, Serializable { + + private final AdvisedSupport advised; + + public EqualsInterceptor(AdvisedSupport advised) { + this.advised = advised; + } + + @Override + public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) { + Object other = args[0]; + if (proxy == other) { + return true; + } + if (other instanceof Factory) { + Callback callback = ((Factory) other).getCallback(INVOKE_EQUALS); + if (!(callback instanceof EqualsInterceptor)) { + return false; + } + AdvisedSupport otherAdvised = ((EqualsInterceptor) callback).advised; + return AopProxyUtils.equalsInProxy(this.advised, otherAdvised); + } + else { + return false; + } + } + } + + + /** + * Dispatcher for the {@code hashCode} method. + * Ensures that the method call is always handled by this class. + */ + private static class HashCodeInterceptor implements MethodInterceptor, Serializable { + + private final AdvisedSupport advised; + + public HashCodeInterceptor(AdvisedSupport advised) { + this.advised = advised; + } + + @Override + public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) { + return CglibAopProxy.class.hashCode() * 13 + this.advised.getTargetSource().hashCode(); + } + } + + + /** + * Interceptor used specifically for advised methods on a frozen, static proxy. + */ + private static class FixedChainStaticTargetInterceptor implements MethodInterceptor, Serializable { + + private final List adviceChain; + + @Nullable + private final Object target; + + @Nullable + private final Class targetClass; + + public FixedChainStaticTargetInterceptor( + List adviceChain, @Nullable Object target, @Nullable Class targetClass) { + + this.adviceChain = adviceChain; + this.target = target; + this.targetClass = targetClass; + } + + @Override + @Nullable + public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { + MethodInvocation invocation = new CglibMethodInvocation( + proxy, this.target, method, args, this.targetClass, this.adviceChain, methodProxy); + // If we get here, we need to create a MethodInvocation. + Object retVal = invocation.proceed(); + retVal = processReturnType(proxy, this.target, method, retVal); + return retVal; + } + } + + + /** + * General purpose AOP callback. Used when the target is dynamic or when the + * proxy is not frozen. + */ + private static class DynamicAdvisedInterceptor implements MethodInterceptor, Serializable { + + private final AdvisedSupport advised; + + public DynamicAdvisedInterceptor(AdvisedSupport advised) { + this.advised = advised; + } + + @Override + @Nullable + public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { + Object oldProxy = null; + boolean setProxyContext = false; + Object target = null; + TargetSource targetSource = this.advised.getTargetSource(); + try { + if (this.advised.exposeProxy) { + // Make invocation available if necessary. + oldProxy = AopContext.setCurrentProxy(proxy); + setProxyContext = true; + } + // Get as late as possible to minimize the time we "own" the target, in case it comes from a pool... + target = targetSource.getTarget(); + Class targetClass = (target != null ? target.getClass() : null); + List chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); + Object retVal; + // Check whether we only have one InvokerInterceptor: that is, + // no real advice, but just reflective invocation of the target. + if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) { + // We can skip creating a MethodInvocation: just invoke the target directly. + // Note that the final invoker must be an InvokerInterceptor, so we know + // it does nothing but a reflective operation on the target, and no hot + // swapping or fancy proxying. + Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); + retVal = methodProxy.invoke(target, argsToUse); + } + else { + // We need to create a method invocation... + retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed(); + } + retVal = processReturnType(proxy, target, method, retVal); + return retVal; + } + finally { + if (target != null && !targetSource.isStatic()) { + targetSource.releaseTarget(target); + } + if (setProxyContext) { + // Restore old proxy. + AopContext.setCurrentProxy(oldProxy); + } + } + } + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || + (other instanceof DynamicAdvisedInterceptor && + this.advised.equals(((DynamicAdvisedInterceptor) other).advised))); + } + + /** + * CGLIB uses this to drive proxy creation. + */ + @Override + public int hashCode() { + return this.advised.hashCode(); + } + } + + + /** + * Implementation of AOP Alliance MethodInvocation used by this AOP proxy. + */ + private static class CglibMethodInvocation extends ReflectiveMethodInvocation { + + @Nullable + private final MethodProxy methodProxy; + + public CglibMethodInvocation(Object proxy, @Nullable Object target, Method method, + Object[] arguments, @Nullable Class targetClass, + List interceptorsAndDynamicMethodMatchers, MethodProxy methodProxy) { + + super(proxy, target, method, arguments, targetClass, interceptorsAndDynamicMethodMatchers); + + // Only use method proxy for public methods not derived from java.lang.Object + this.methodProxy = (Modifier.isPublic(method.getModifiers()) && + method.getDeclaringClass() != Object.class && !AopUtils.isEqualsMethod(method) && + !AopUtils.isHashCodeMethod(method) && !AopUtils.isToStringMethod(method) ? + methodProxy : null); + } + + @Override + @Nullable + public Object proceed() throws Throwable { + try { + return super.proceed(); + } + catch (RuntimeException ex) { + throw ex; + } + catch (Exception ex) { + if (ReflectionUtils.declaresException(getMethod(), ex.getClass())) { + throw ex; + } + else { + throw new UndeclaredThrowableException(ex); + } + } + } + + /** + * Gives a marginal performance improvement versus using reflection to + * invoke the target when invoking public methods. + */ + @Override + protected Object invokeJoinpoint() throws Throwable { + if (this.methodProxy != null) { + return this.methodProxy.invoke(this.target, this.arguments); + } + else { + return super.invokeJoinpoint(); + } + } + } + + + /** + * CallbackFilter to assign Callbacks to methods. + */ + private static class ProxyCallbackFilter implements CallbackFilter { + + private final AdvisedSupport advised; + + private final Map fixedInterceptorMap; + + private final int fixedInterceptorOffset; + + public ProxyCallbackFilter( + AdvisedSupport advised, Map fixedInterceptorMap, int fixedInterceptorOffset) { + + this.advised = advised; + this.fixedInterceptorMap = fixedInterceptorMap; + this.fixedInterceptorOffset = fixedInterceptorOffset; + } + + /** + * Implementation of CallbackFilter.accept() to return the index of the + * callback we need. + *

The callbacks for each proxy are built up of a set of fixed callbacks + * for general use and then a set of callbacks that are specific to a method + * for use on static targets with a fixed advice chain. + *

The callback used is determined thus: + *

+ *
For exposed proxies
+ *
Exposing the proxy requires code to execute before and after the + * method/chain invocation. This means we must use + * DynamicAdvisedInterceptor, since all other interceptors can avoid the + * need for a try/catch block
+ *
For Object.finalize():
+ *
No override for this method is used.
+ *
For equals():
+ *
The EqualsInterceptor is used to redirect equals() calls to a + * special handler to this proxy.
+ *
For methods on the Advised class:
+ *
the AdvisedDispatcher is used to dispatch the call directly to + * the target
+ *
For advised methods:
+ *
If the target is static and the advice chain is frozen then a + * FixedChainStaticTargetInterceptor specific to the method is used to + * invoke the advice chain. Otherwise a DynamicAdvisedInterceptor is + * used.
+ *
For non-advised methods:
+ *
Where it can be determined that the method will not return {@code this} + * or when {@code ProxyFactory.getExposeProxy()} returns {@code false}, + * then a Dispatcher is used. For static targets, the StaticDispatcher is used; + * and for dynamic targets, a DynamicUnadvisedInterceptor is used. + * If it possible for the method to return {@code this} then a + * StaticUnadvisedInterceptor is used for static targets - the + * DynamicUnadvisedInterceptor already considers this.
+ *
+ */ + @Override + public int accept(Method method) { + if (AopUtils.isFinalizeMethod(method)) { + logger.trace("Found finalize() method - using NO_OVERRIDE"); + return NO_OVERRIDE; + } + if (!this.advised.isOpaque() && method.getDeclaringClass().isInterface() && + method.getDeclaringClass().isAssignableFrom(Advised.class)) { + if (logger.isTraceEnabled()) { + logger.trace("Method is declared on Advised interface: " + method); + } + return DISPATCH_ADVISED; + } + // We must always proxy equals, to direct calls to this. + if (AopUtils.isEqualsMethod(method)) { + if (logger.isTraceEnabled()) { + logger.trace("Found 'equals' method: " + method); + } + return INVOKE_EQUALS; + } + // We must always calculate hashCode based on the proxy. + if (AopUtils.isHashCodeMethod(method)) { + if (logger.isTraceEnabled()) { + logger.trace("Found 'hashCode' method: " + method); + } + return INVOKE_HASHCODE; + } + Class targetClass = this.advised.getTargetClass(); + // Proxy is not yet available, but that shouldn't matter. + List chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); + boolean haveAdvice = !chain.isEmpty(); + boolean exposeProxy = this.advised.isExposeProxy(); + boolean isStatic = this.advised.getTargetSource().isStatic(); + boolean isFrozen = this.advised.isFrozen(); + if (haveAdvice || !isFrozen) { + // If exposing the proxy, then AOP_PROXY must be used. + if (exposeProxy) { + if (logger.isTraceEnabled()) { + logger.trace("Must expose proxy on advised method: " + method); + } + return AOP_PROXY; + } + // Check to see if we have fixed interceptor to serve this method. + // Else use the AOP_PROXY. + if (isStatic && isFrozen && this.fixedInterceptorMap.containsKey(method)) { + if (logger.isTraceEnabled()) { + logger.trace("Method has advice and optimizations are enabled: " + method); + } + // We know that we are optimizing so we can use the FixedStaticChainInterceptors. + int index = this.fixedInterceptorMap.get(method); + return (index + this.fixedInterceptorOffset); + } + else { + if (logger.isTraceEnabled()) { + logger.trace("Unable to apply any optimizations to advised method: " + method); + } + return AOP_PROXY; + } + } + else { + // See if the return type of the method is outside the class hierarchy of the target type. + // If so we know it never needs to have return type massage and can use a dispatcher. + // If the proxy is being exposed, then must use the interceptor the correct one is already + // configured. If the target is not static, then we cannot use a dispatcher because the + // target needs to be explicitly released after the invocation. + if (exposeProxy || !isStatic) { + return INVOKE_TARGET; + } + Class returnType = method.getReturnType(); + if (targetClass != null && returnType.isAssignableFrom(targetClass)) { + if (logger.isTraceEnabled()) { + logger.trace("Method return type is assignable from target type and " + + "may therefore return 'this' - using INVOKE_TARGET: " + method); + } + return INVOKE_TARGET; + } + else { + if (logger.isTraceEnabled()) { + logger.trace("Method return type ensures 'this' cannot be returned - " + + "using DISPATCH_TARGET: " + method); + } + return DISPATCH_TARGET; + } + } + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof ProxyCallbackFilter)) { + return false; + } + ProxyCallbackFilter otherCallbackFilter = (ProxyCallbackFilter) other; + AdvisedSupport otherAdvised = otherCallbackFilter.advised; + if (this.advised.isFrozen() != otherAdvised.isFrozen()) { + return false; + } + if (this.advised.isExposeProxy() != otherAdvised.isExposeProxy()) { + return false; + } + if (this.advised.getTargetSource().isStatic() != otherAdvised.getTargetSource().isStatic()) { + return false; + } + if (!AopProxyUtils.equalsProxiedInterfaces(this.advised, otherAdvised)) { + return false; + } + // Advice instance identity is unimportant to the proxy class: + // All that matters is type and ordering. + if (this.advised.getAdvisorCount() != otherAdvised.getAdvisorCount()) { + return false; + } + Advisor[] thisAdvisors = this.advised.getAdvisors(); + Advisor[] thatAdvisors = otherAdvised.getAdvisors(); + for (int i = 0; i < thisAdvisors.length; i++) { + Advisor thisAdvisor = thisAdvisors[i]; + Advisor thatAdvisor = thatAdvisors[i]; + if (!equalsAdviceClasses(thisAdvisor, thatAdvisor)) { + return false; + } + if (!equalsPointcuts(thisAdvisor, thatAdvisor)) { + return false; + } + } + return true; + } + + private static boolean equalsAdviceClasses(Advisor a, Advisor b) { + return (a.getAdvice().getClass() == b.getAdvice().getClass()); + } + + private static boolean equalsPointcuts(Advisor a, Advisor b) { + // If only one of the advisor (but not both) is PointcutAdvisor, then it is a mismatch. + // Takes care of the situations where an IntroductionAdvisor is used (see SPR-3959). + return (!(a instanceof PointcutAdvisor) || + (b instanceof PointcutAdvisor && + ObjectUtils.nullSafeEquals(((PointcutAdvisor) a).getPointcut(), ((PointcutAdvisor) b).getPointcut()))); + } + + @Override + public int hashCode() { + int hashCode = 0; + Advisor[] advisors = this.advised.getAdvisors(); + for (Advisor advisor : advisors) { + Advice advice = advisor.getAdvice(); + hashCode = 13 * hashCode + advice.getClass().hashCode(); + } + hashCode = 13 * hashCode + (this.advised.isFrozen() ? 1 : 0); + hashCode = 13 * hashCode + (this.advised.isExposeProxy() ? 1 : 0); + hashCode = 13 * hashCode + (this.advised.isOptimize() ? 1 : 0); + hashCode = 13 * hashCode + (this.advised.isOpaque() ? 1 : 0); + return hashCode; + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAdvisorChainFactory.java b/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAdvisorChainFactory.java new file mode 100644 index 0000000..a8f4587 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAdvisorChainFactory.java @@ -0,0 +1,124 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.aopalliance.intercept.Interceptor; +import org.aopalliance.intercept.MethodInterceptor; + +import org.springframework.aop.Advisor; +import org.springframework.aop.IntroductionAdvisor; +import org.springframework.aop.IntroductionAwareMethodMatcher; +import org.springframework.aop.MethodMatcher; +import org.springframework.aop.PointcutAdvisor; +import org.springframework.aop.framework.adapter.AdvisorAdapterRegistry; +import org.springframework.aop.framework.adapter.GlobalAdvisorAdapterRegistry; +import org.springframework.lang.Nullable; + +/** + * A simple but definitive way of working out an advice chain for a Method, + * given an {@link Advised} object. Always rebuilds each advice chain; + * caching can be provided by subclasses. + * + * @author Juergen Hoeller + * @author Rod Johnson + * @author Adrian Colyer + * @since 2.0.3 + */ +@SuppressWarnings("serial") +public class DefaultAdvisorChainFactory implements AdvisorChainFactory, Serializable { + + @Override + public List getInterceptorsAndDynamicInterceptionAdvice( + Advised config, Method method, @Nullable Class targetClass) { + + // This is somewhat tricky... We have to process introductions first, + // but we need to preserve order in the ultimate list. + AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance(); + Advisor[] advisors = config.getAdvisors(); + List interceptorList = new ArrayList<>(advisors.length); + Class actualClass = (targetClass != null ? targetClass : method.getDeclaringClass()); + Boolean hasIntroductions = null; + + for (Advisor advisor : advisors) { + if (advisor instanceof PointcutAdvisor) { + // Add it conditionally. + PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor; + if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) { + MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher(); + boolean match; + if (mm instanceof IntroductionAwareMethodMatcher) { + if (hasIntroductions == null) { + hasIntroductions = hasMatchingIntroductions(advisors, actualClass); + } + match = ((IntroductionAwareMethodMatcher) mm).matches(method, actualClass, hasIntroductions); + } + else { + match = mm.matches(method, actualClass); + } + if (match) { + MethodInterceptor[] interceptors = registry.getInterceptors(advisor); + if (mm.isRuntime()) { + // Creating a new object instance in the getInterceptors() method + // isn't a problem as we normally cache created chains. + for (MethodInterceptor interceptor : interceptors) { + interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm)); + } + } + else { + interceptorList.addAll(Arrays.asList(interceptors)); + } + } + } + } + else if (advisor instanceof IntroductionAdvisor) { + IntroductionAdvisor ia = (IntroductionAdvisor) advisor; + if (config.isPreFiltered() || ia.getClassFilter().matches(actualClass)) { + Interceptor[] interceptors = registry.getInterceptors(advisor); + interceptorList.addAll(Arrays.asList(interceptors)); + } + } + else { + Interceptor[] interceptors = registry.getInterceptors(advisor); + interceptorList.addAll(Arrays.asList(interceptors)); + } + } + + return interceptorList; + } + + /** + * Determine whether the Advisors contain matching introductions. + */ + private static boolean hasMatchingIntroductions(Advisor[] advisors, Class actualClass) { + for (Advisor advisor : advisors) { + if (advisor instanceof IntroductionAdvisor) { + IntroductionAdvisor ia = (IntroductionAdvisor) advisor; + if (ia.getClassFilter().matches(actualClass)) { + return true; + } + } + } + return false; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAopProxyFactory.java b/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAopProxyFactory.java new file mode 100644 index 0000000..d84df2f --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAopProxyFactory.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import java.io.Serializable; +import java.lang.reflect.Proxy; + +import org.springframework.aop.SpringProxy; + +/** + * Default {@link AopProxyFactory} implementation, creating either a CGLIB proxy + * or a JDK dynamic proxy. + * + *

Creates a CGLIB proxy if one the following is true for a given + * {@link AdvisedSupport} instance: + *

    + *
  • the {@code optimize} flag is set + *
  • the {@code proxyTargetClass} flag is set + *
  • no proxy interfaces have been specified + *
+ * + *

In general, specify {@code proxyTargetClass} to enforce a CGLIB proxy, + * or specify one or more interfaces to use a JDK dynamic proxy. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Sebastien Deleuze + * @since 12.03.2004 + * @see AdvisedSupport#setOptimize + * @see AdvisedSupport#setProxyTargetClass + * @see AdvisedSupport#setInterfaces + */ +@SuppressWarnings("serial") +public class DefaultAopProxyFactory implements AopProxyFactory, Serializable { + + /** + * Whether this environment lives within a native image. + * Exposed as a private static field rather than in a {@code NativeImageDetector.inNativeImage()} static method due to https://github.com/oracle/graal/issues/2594. + * @see ImageInfo.java + */ + private static final boolean IN_NATIVE_IMAGE = (System.getProperty("org.graalvm.nativeimage.imagecode") != null); + + + @Override + public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { + if (!IN_NATIVE_IMAGE && + (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config))) { + Class targetClass = config.getTargetClass(); + if (targetClass == null) { + throw new AopConfigException("TargetSource cannot determine target class: " + + "Either an interface or a target is required for proxy creation."); + } + if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { + return new JdkDynamicAopProxy(config); + } + return new ObjenesisCglibAopProxy(config); + } + else { + return new JdkDynamicAopProxy(config); + } + } + + /** + * Determine whether the supplied {@link AdvisedSupport} has only the + * {@link org.springframework.aop.SpringProxy} interface specified + * (or no proxy interfaces specified at all). + */ + private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) { + Class[] ifcs = config.getProxiedInterfaces(); + return (ifcs.length == 0 || (ifcs.length == 1 && SpringProxy.class.isAssignableFrom(ifcs[0]))); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/InterceptorAndDynamicMethodMatcher.java b/spring-aop/src/main/java/org/springframework/aop/framework/InterceptorAndDynamicMethodMatcher.java new file mode 100644 index 0000000..79f5101 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/InterceptorAndDynamicMethodMatcher.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2007 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import org.aopalliance.intercept.MethodInterceptor; + +import org.springframework.aop.MethodMatcher; + +/** + * Internal framework class, combining a MethodInterceptor instance + * with a MethodMatcher for use as an element in the advisor chain. + * + * @author Rod Johnson + */ +class InterceptorAndDynamicMethodMatcher { + + final MethodInterceptor interceptor; + + final MethodMatcher methodMatcher; + + public InterceptorAndDynamicMethodMatcher(MethodInterceptor interceptor, MethodMatcher methodMatcher) { + this.interceptor = interceptor; + this.methodMatcher = methodMatcher; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java b/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java new file mode 100644 index 0000000..fcfaf93 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java @@ -0,0 +1,289 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import java.io.Serializable; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.List; + +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aop.AopInvocationException; +import org.springframework.aop.RawTargetAccess; +import org.springframework.aop.TargetSource; +import org.springframework.aop.support.AopUtils; +import org.springframework.core.DecoratingProxy; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * JDK-based {@link AopProxy} implementation for the Spring AOP framework, + * based on JDK {@link java.lang.reflect.Proxy dynamic proxies}. + * + *

Creates a dynamic proxy, implementing the interfaces exposed by + * the AopProxy. Dynamic proxies cannot be used to proxy methods + * defined in classes, rather than interfaces. + * + *

Objects of this type should be obtained through proxy factories, + * configured by an {@link AdvisedSupport} class. This class is internal + * to Spring's AOP framework and need not be used directly by client code. + * + *

Proxies created using this class will be thread-safe if the + * underlying (target) class is thread-safe. + * + *

Proxies are serializable so long as all Advisors (including Advices + * and Pointcuts) and the TargetSource are serializable. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Rob Harrop + * @author Dave Syer + * @author Sergey Tsypanov + * @see java.lang.reflect.Proxy + * @see AdvisedSupport + * @see ProxyFactory + */ +final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable { + + /** use serialVersionUID from Spring 1.2 for interoperability. */ + private static final long serialVersionUID = 5531744639992436476L; + + + /* + * NOTE: We could avoid the code duplication between this class and the CGLIB + * proxies by refactoring "invoke" into a template method. However, this approach + * adds at least 10% performance overhead versus a copy-paste solution, so we sacrifice + * elegance for performance. (We have a good test suite to ensure that the different + * proxies behave the same :-) + * This way, we can also more easily take advantage of minor optimizations in each class. + */ + + /** We use a static Log to avoid serialization issues. */ + private static final Log logger = LogFactory.getLog(JdkDynamicAopProxy.class); + + /** Config used to configure this proxy. */ + private final AdvisedSupport advised; + + private final Class[] proxiedInterfaces; + + /** + * Is the {@link #equals} method defined on the proxied interfaces? + */ + private boolean equalsDefined; + + /** + * Is the {@link #hashCode} method defined on the proxied interfaces? + */ + private boolean hashCodeDefined; + + + /** + * Construct a new JdkDynamicAopProxy for the given AOP configuration. + * @param config the AOP configuration as AdvisedSupport object + * @throws AopConfigException if the config is invalid. We try to throw an informative + * exception in this case, rather than let a mysterious failure happen later. + */ + public JdkDynamicAopProxy(AdvisedSupport config) throws AopConfigException { + Assert.notNull(config, "AdvisedSupport must not be null"); + if (config.getAdvisorCount() == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) { + throw new AopConfigException("No advisors and no TargetSource specified"); + } + this.advised = config; + this.proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true); + findDefinedEqualsAndHashCodeMethods(this.proxiedInterfaces); + } + + + @Override + public Object getProxy() { + return getProxy(ClassUtils.getDefaultClassLoader()); + } + + @Override + public Object getProxy(@Nullable ClassLoader classLoader) { + if (logger.isTraceEnabled()) { + logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource()); + } + return Proxy.newProxyInstance(classLoader, this.proxiedInterfaces, this); + } + + /** + * Finds any {@link #equals} or {@link #hashCode} method that may be defined + * on the supplied set of interfaces. + * @param proxiedInterfaces the interfaces to introspect + */ + private void findDefinedEqualsAndHashCodeMethods(Class[] proxiedInterfaces) { + for (Class proxiedInterface : proxiedInterfaces) { + Method[] methods = proxiedInterface.getDeclaredMethods(); + for (Method method : methods) { + if (AopUtils.isEqualsMethod(method)) { + this.equalsDefined = true; + } + if (AopUtils.isHashCodeMethod(method)) { + this.hashCodeDefined = true; + } + if (this.equalsDefined && this.hashCodeDefined) { + return; + } + } + } + } + + + /** + * Implementation of {@code InvocationHandler.invoke}. + *

Callers will see exactly the exception thrown by the target, + * unless a hook method throws an exception. + */ + @Override + @Nullable + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + Object oldProxy = null; + boolean setProxyContext = false; + + TargetSource targetSource = this.advised.targetSource; + Object target = null; + + try { + if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) { + // The target does not implement the equals(Object) method itself. + return equals(args[0]); + } + else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) { + // The target does not implement the hashCode() method itself. + return hashCode(); + } + else if (method.getDeclaringClass() == DecoratingProxy.class) { + // There is only getDecoratedClass() declared -> dispatch to proxy config. + return AopProxyUtils.ultimateTargetClass(this.advised); + } + else if (!this.advised.opaque && method.getDeclaringClass().isInterface() && + method.getDeclaringClass().isAssignableFrom(Advised.class)) { + // Service invocations on ProxyConfig with the proxy config... + return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args); + } + + Object retVal; + + if (this.advised.exposeProxy) { + // Make invocation available if necessary. + oldProxy = AopContext.setCurrentProxy(proxy); + setProxyContext = true; + } + + // Get as late as possible to minimize the time we "own" the target, + // in case it comes from a pool. + target = targetSource.getTarget(); + Class targetClass = (target != null ? target.getClass() : null); + + // Get the interception chain for this method. + List chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); + + // Check whether we have any advice. If we don't, we can fallback on direct + // reflective invocation of the target, and avoid creating a MethodInvocation. + if (chain.isEmpty()) { + // We can skip creating a MethodInvocation: just invoke the target directly + // Note that the final invoker must be an InvokerInterceptor so we know it does + // nothing but a reflective operation on the target, and no hot swapping or fancy proxying. + Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); + retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse); + } + else { + // We need to create a method invocation... + MethodInvocation invocation = + new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain); + // Proceed to the joinpoint through the interceptor chain. + retVal = invocation.proceed(); + } + + // Massage return value if necessary. + Class returnType = method.getReturnType(); + if (retVal != null && retVal == target && + returnType != Object.class && returnType.isInstance(proxy) && + !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) { + // Special case: it returned "this" and the return type of the method + // is type-compatible. Note that we can't help if the target sets + // a reference to itself in another returned object. + retVal = proxy; + } + else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) { + throw new AopInvocationException( + "Null return value from advice does not match primitive return type for: " + method); + } + return retVal; + } + finally { + if (target != null && !targetSource.isStatic()) { + // Must have come from TargetSource. + targetSource.releaseTarget(target); + } + if (setProxyContext) { + // Restore old proxy. + AopContext.setCurrentProxy(oldProxy); + } + } + } + + + /** + * Equality means interfaces, advisors and TargetSource are equal. + *

The compared object may be a JdkDynamicAopProxy instance itself + * or a dynamic proxy wrapping a JdkDynamicAopProxy instance. + */ + @Override + public boolean equals(@Nullable Object other) { + if (other == this) { + return true; + } + if (other == null) { + return false; + } + + JdkDynamicAopProxy otherProxy; + if (other instanceof JdkDynamicAopProxy) { + otherProxy = (JdkDynamicAopProxy) other; + } + else if (Proxy.isProxyClass(other.getClass())) { + InvocationHandler ih = Proxy.getInvocationHandler(other); + if (!(ih instanceof JdkDynamicAopProxy)) { + return false; + } + otherProxy = (JdkDynamicAopProxy) ih; + } + else { + // Not a valid comparison... + return false; + } + + // If we get here, otherProxy is the other AopProxy. + return AopProxyUtils.equalsInProxy(this.advised, otherProxy.advised); + } + + /** + * Proxy uses the hash code of the TargetSource. + */ + @Override + public int hashCode() { + return JdkDynamicAopProxy.class.hashCode() * 13 + this.advised.getTargetSource().hashCode(); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/ObjenesisCglibAopProxy.java b/spring-aop/src/main/java/org/springframework/aop/framework/ObjenesisCglibAopProxy.java new file mode 100644 index 0000000..ba31e39 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/ObjenesisCglibAopProxy.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import java.lang.reflect.Constructor; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cglib.proxy.Callback; +import org.springframework.cglib.proxy.Enhancer; +import org.springframework.cglib.proxy.Factory; +import org.springframework.objenesis.SpringObjenesis; +import org.springframework.util.ReflectionUtils; + +/** + * Objenesis-based extension of {@link CglibAopProxy} to create proxy instances + * without invoking the constructor of the class. Used by default as of Spring 4. + * + * @author Oliver Gierke + * @author Juergen Hoeller + * @since 4.0 + */ +@SuppressWarnings("serial") +class ObjenesisCglibAopProxy extends CglibAopProxy { + + private static final Log logger = LogFactory.getLog(ObjenesisCglibAopProxy.class); + + private static final SpringObjenesis objenesis = new SpringObjenesis(); + + + /** + * Create a new ObjenesisCglibAopProxy for the given AOP configuration. + * @param config the AOP configuration as AdvisedSupport object + */ + public ObjenesisCglibAopProxy(AdvisedSupport config) { + super(config); + } + + + @Override + protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) { + Class proxyClass = enhancer.createClass(); + Object proxyInstance = null; + + if (objenesis.isWorthTrying()) { + try { + proxyInstance = objenesis.newInstance(proxyClass, enhancer.getUseCache()); + } + catch (Throwable ex) { + logger.debug("Unable to instantiate proxy using Objenesis, " + + "falling back to regular proxy construction", ex); + } + } + + if (proxyInstance == null) { + // Regular instantiation via default constructor... + try { + Constructor ctor = (this.constructorArgs != null ? + proxyClass.getDeclaredConstructor(this.constructorArgTypes) : + proxyClass.getDeclaredConstructor()); + ReflectionUtils.makeAccessible(ctor); + proxyInstance = (this.constructorArgs != null ? + ctor.newInstance(this.constructorArgs) : ctor.newInstance()); + } + catch (Throwable ex) { + throw new AopConfigException("Unable to instantiate proxy using Objenesis, " + + "and regular proxy instantiation via default constructor fails as well", ex); + } + } + + ((Factory) proxyInstance).setCallbacks(callbacks); + return proxyInstance; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/ProxyConfig.java b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyConfig.java new file mode 100644 index 0000000..38665ca --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyConfig.java @@ -0,0 +1,174 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import java.io.Serializable; + +import org.springframework.util.Assert; + +/** + * Convenience superclass for configuration used in creating proxies, + * to ensure that all proxy creators have consistent properties. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see AdvisedSupport + */ +public class ProxyConfig implements Serializable { + + /** use serialVersionUID from Spring 1.2 for interoperability. */ + private static final long serialVersionUID = -8409359707199703185L; + + + private boolean proxyTargetClass = false; + + private boolean optimize = false; + + boolean opaque = false; + + boolean exposeProxy = false; + + private boolean frozen = false; + + + /** + * Set whether to proxy the target class directly, instead of just proxying + * specific interfaces. Default is "false". + *

Set this to "true" to force proxying for the TargetSource's exposed + * target class. If that target class is an interface, a JDK proxy will be + * created for the given interface. If that target class is any other class, + * a CGLIB proxy will be created for the given class. + *

Note: Depending on the configuration of the concrete proxy factory, + * the proxy-target-class behavior will also be applied if no interfaces + * have been specified (and no interface autodetection is activated). + * @see org.springframework.aop.TargetSource#getTargetClass() + */ + public void setProxyTargetClass(boolean proxyTargetClass) { + this.proxyTargetClass = proxyTargetClass; + } + + /** + * Return whether to proxy the target class directly as well as any interfaces. + */ + public boolean isProxyTargetClass() { + return this.proxyTargetClass; + } + + /** + * Set whether proxies should perform aggressive optimizations. + * The exact meaning of "aggressive optimizations" will differ + * between proxies, but there is usually some tradeoff. + * Default is "false". + *

For example, optimization will usually mean that advice changes won't + * take effect after a proxy has been created. For this reason, optimization + * is disabled by default. An optimize value of "true" may be ignored + * if other settings preclude optimization: for example, if "exposeProxy" + * is set to "true" and that's not compatible with the optimization. + */ + public void setOptimize(boolean optimize) { + this.optimize = optimize; + } + + /** + * Return whether proxies should perform aggressive optimizations. + */ + public boolean isOptimize() { + return this.optimize; + } + + /** + * Set whether proxies created by this configuration should be prevented + * from being cast to {@link Advised} to query proxy status. + *

Default is "false", meaning that any AOP proxy can be cast to + * {@link Advised}. + */ + public void setOpaque(boolean opaque) { + this.opaque = opaque; + } + + /** + * Return whether proxies created by this configuration should be + * prevented from being cast to {@link Advised}. + */ + public boolean isOpaque() { + return this.opaque; + } + + /** + * Set whether the proxy should be exposed by the AOP framework as a + * ThreadLocal for retrieval via the AopContext class. This is useful + * if an advised object needs to call another advised method on itself. + * (If it uses {@code this}, the invocation will not be advised). + *

Default is "false", in order to avoid unnecessary extra interception. + * This means that no guarantees are provided that AopContext access will + * work consistently within any method of the advised object. + */ + public void setExposeProxy(boolean exposeProxy) { + this.exposeProxy = exposeProxy; + } + + /** + * Return whether the AOP proxy will expose the AOP proxy for + * each invocation. + */ + public boolean isExposeProxy() { + return this.exposeProxy; + } + + /** + * Set whether this config should be frozen. + *

When a config is frozen, no advice changes can be made. This is + * useful for optimization, and useful when we don't want callers to + * be able to manipulate configuration after casting to Advised. + */ + public void setFrozen(boolean frozen) { + this.frozen = frozen; + } + + /** + * Return whether the config is frozen, and no advice changes can be made. + */ + public boolean isFrozen() { + return this.frozen; + } + + + /** + * Copy configuration from the other config object. + * @param other object to copy configuration from + */ + public void copyFrom(ProxyConfig other) { + Assert.notNull(other, "Other ProxyConfig object must not be null"); + this.proxyTargetClass = other.proxyTargetClass; + this.optimize = other.optimize; + this.exposeProxy = other.exposeProxy; + this.frozen = other.frozen; + this.opaque = other.opaque; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("proxyTargetClass=").append(this.proxyTargetClass).append("; "); + sb.append("optimize=").append(this.optimize).append("; "); + sb.append("opaque=").append(this.opaque).append("; "); + sb.append("exposeProxy=").append(this.exposeProxy).append("; "); + sb.append("frozen=").append(this.frozen); + return sb.toString(); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/ProxyCreatorSupport.java b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyCreatorSupport.java new file mode 100644 index 0000000..9fa04a3 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyCreatorSupport.java @@ -0,0 +1,142 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.util.Assert; + +/** + * Base class for proxy factories. + * Provides convenient access to a configurable AopProxyFactory. + * + * @author Juergen Hoeller + * @since 2.0.3 + * @see #createAopProxy() + */ +@SuppressWarnings("serial") +public class ProxyCreatorSupport extends AdvisedSupport { + + private AopProxyFactory aopProxyFactory; + + private final List listeners = new ArrayList<>(); + + /** Set to true when the first AOP proxy has been created. */ + private boolean active = false; + + + /** + * Create a new ProxyCreatorSupport instance. + */ + public ProxyCreatorSupport() { + this.aopProxyFactory = new DefaultAopProxyFactory(); + } + + /** + * Create a new ProxyCreatorSupport instance. + * @param aopProxyFactory the AopProxyFactory to use + */ + public ProxyCreatorSupport(AopProxyFactory aopProxyFactory) { + Assert.notNull(aopProxyFactory, "AopProxyFactory must not be null"); + this.aopProxyFactory = aopProxyFactory; + } + + + /** + * Customize the AopProxyFactory, allowing different strategies + * to be dropped in without changing the core framework. + *

Default is {@link DefaultAopProxyFactory}, using dynamic JDK + * proxies or CGLIB proxies based on the requirements. + */ + public void setAopProxyFactory(AopProxyFactory aopProxyFactory) { + Assert.notNull(aopProxyFactory, "AopProxyFactory must not be null"); + this.aopProxyFactory = aopProxyFactory; + } + + /** + * Return the AopProxyFactory that this ProxyConfig uses. + */ + public AopProxyFactory getAopProxyFactory() { + return this.aopProxyFactory; + } + + /** + * Add the given AdvisedSupportListener to this proxy configuration. + * @param listener the listener to register + */ + public void addListener(AdvisedSupportListener listener) { + Assert.notNull(listener, "AdvisedSupportListener must not be null"); + this.listeners.add(listener); + } + + /** + * Remove the given AdvisedSupportListener from this proxy configuration. + * @param listener the listener to deregister + */ + public void removeListener(AdvisedSupportListener listener) { + Assert.notNull(listener, "AdvisedSupportListener must not be null"); + this.listeners.remove(listener); + } + + + /** + * Subclasses should call this to get a new AOP proxy. They should not + * create an AOP proxy with {@code this} as an argument. + */ + protected final synchronized AopProxy createAopProxy() { + if (!this.active) { + activate(); + } + return getAopProxyFactory().createAopProxy(this); + } + + /** + * Activate this proxy configuration. + * @see AdvisedSupportListener#activated + */ + private void activate() { + this.active = true; + for (AdvisedSupportListener listener : this.listeners) { + listener.activated(this); + } + } + + /** + * Propagate advice change event to all AdvisedSupportListeners. + * @see AdvisedSupportListener#adviceChanged + */ + @Override + protected void adviceChanged() { + super.adviceChanged(); + synchronized (this) { + if (this.active) { + for (AdvisedSupportListener listener : this.listeners) { + listener.adviceChanged(this); + } + } + } + } + + /** + * Subclasses can call this to check whether any AOP proxies have been created yet. + */ + protected final synchronized boolean isActive() { + return this.active; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactory.java b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactory.java new file mode 100644 index 0000000..f4b6208 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactory.java @@ -0,0 +1,158 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import org.aopalliance.intercept.Interceptor; + +import org.springframework.aop.TargetSource; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * Factory for AOP proxies for programmatic use, rather than via declarative + * setup in a bean factory. This class provides a simple way of obtaining + * and configuring AOP proxy instances in custom user code. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Rob Harrop + * @since 14.03.2003 + */ +@SuppressWarnings("serial") +public class ProxyFactory extends ProxyCreatorSupport { + + /** + * Create a new ProxyFactory. + */ + public ProxyFactory() { + } + + /** + * Create a new ProxyFactory. + *

Will proxy all interfaces that the given target implements. + * @param target the target object to be proxied + */ + public ProxyFactory(Object target) { + setTarget(target); + setInterfaces(ClassUtils.getAllInterfaces(target)); + } + + /** + * Create a new ProxyFactory. + *

No target, only interfaces. Must add interceptors. + * @param proxyInterfaces the interfaces that the proxy should implement + */ + public ProxyFactory(Class... proxyInterfaces) { + setInterfaces(proxyInterfaces); + } + + /** + * Create a new ProxyFactory for the given interface and interceptor. + *

Convenience method for creating a proxy for a single interceptor, + * assuming that the interceptor handles all calls itself rather than + * delegating to a target, like in the case of remoting proxies. + * @param proxyInterface the interface that the proxy should implement + * @param interceptor the interceptor that the proxy should invoke + */ + public ProxyFactory(Class proxyInterface, Interceptor interceptor) { + addInterface(proxyInterface); + addAdvice(interceptor); + } + + /** + * Create a ProxyFactory for the specified {@code TargetSource}, + * making the proxy implement the specified interface. + * @param proxyInterface the interface that the proxy should implement + * @param targetSource the TargetSource that the proxy should invoke + */ + public ProxyFactory(Class proxyInterface, TargetSource targetSource) { + addInterface(proxyInterface); + setTargetSource(targetSource); + } + + + /** + * Create a new proxy according to the settings in this factory. + *

Can be called repeatedly. Effect will vary if we've added + * or removed interfaces. Can add and remove interceptors. + *

Uses a default class loader: Usually, the thread context class loader + * (if necessary for proxy creation). + * @return the proxy object + */ + public Object getProxy() { + return createAopProxy().getProxy(); + } + + /** + * Create a new proxy according to the settings in this factory. + *

Can be called repeatedly. Effect will vary if we've added + * or removed interfaces. Can add and remove interceptors. + *

Uses the given class loader (if necessary for proxy creation). + * @param classLoader the class loader to create the proxy with + * (or {@code null} for the low-level proxy facility's default) + * @return the proxy object + */ + public Object getProxy(@Nullable ClassLoader classLoader) { + return createAopProxy().getProxy(classLoader); + } + + + /** + * Create a new proxy for the given interface and interceptor. + *

Convenience method for creating a proxy for a single interceptor, + * assuming that the interceptor handles all calls itself rather than + * delegating to a target, like in the case of remoting proxies. + * @param proxyInterface the interface that the proxy should implement + * @param interceptor the interceptor that the proxy should invoke + * @return the proxy object + * @see #ProxyFactory(Class, org.aopalliance.intercept.Interceptor) + */ + @SuppressWarnings("unchecked") + public static T getProxy(Class proxyInterface, Interceptor interceptor) { + return (T) new ProxyFactory(proxyInterface, interceptor).getProxy(); + } + + /** + * Create a proxy for the specified {@code TargetSource}, + * implementing the specified interface. + * @param proxyInterface the interface that the proxy should implement + * @param targetSource the TargetSource that the proxy should invoke + * @return the proxy object + * @see #ProxyFactory(Class, org.springframework.aop.TargetSource) + */ + @SuppressWarnings("unchecked") + public static T getProxy(Class proxyInterface, TargetSource targetSource) { + return (T) new ProxyFactory(proxyInterface, targetSource).getProxy(); + } + + /** + * Create a proxy for the specified {@code TargetSource} that extends + * the target class of the {@code TargetSource}. + * @param targetSource the TargetSource that the proxy should invoke + * @return the proxy object + */ + public static Object getProxy(TargetSource targetSource) { + if (targetSource.getTargetClass() == null) { + throw new IllegalArgumentException("Cannot create class proxy for TargetSource with null target class"); + } + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.setTargetSource(targetSource); + proxyFactory.setProxyTargetClass(true); + return proxyFactory.getProxy(); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactoryBean.java b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactoryBean.java new file mode 100644 index 0000000..6c9efc4 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactoryBean.java @@ -0,0 +1,648 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.aopalliance.aop.Advice; +import org.aopalliance.intercept.Interceptor; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aop.Advisor; +import org.springframework.aop.TargetSource; +import org.springframework.aop.framework.adapter.AdvisorAdapterRegistry; +import org.springframework.aop.framework.adapter.GlobalAdvisorAdapterRegistry; +import org.springframework.aop.framework.adapter.UnknownAdviceTypeException; +import org.springframework.aop.target.SingletonTargetSource; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.FactoryBeanNotInitializedException; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; + +/** + * {@link org.springframework.beans.factory.FactoryBean} implementation that builds an + * AOP proxy based on beans in Spring {@link org.springframework.beans.factory.BeanFactory}. + * + *

{@link org.aopalliance.intercept.MethodInterceptor MethodInterceptors} and + * {@link org.springframework.aop.Advisor Advisors} are identified by a list of bean + * names in the current bean factory, specified through the "interceptorNames" property. + * The last entry in the list can be the name of a target bean or a + * {@link org.springframework.aop.TargetSource}; however, it is normally preferable + * to use the "targetName"/"target"/"targetSource" properties instead. + * + *

Global interceptors and advisors can be added at the factory level. The specified + * ones are expanded in an interceptor list where an "xxx*" entry is included in the + * list, matching the given prefix with the bean names (e.g. "global*" would match + * both "globalBean1" and "globalBean2", "*" all defined interceptors). The matching + * interceptors get applied according to their returned order value, if they implement + * the {@link org.springframework.core.Ordered} interface. + * + *

Creates a JDK proxy when proxy interfaces are given, and a CGLIB proxy for the + * actual target class if not. Note that the latter will only work if the target class + * does not have final methods, as a dynamic subclass will be created at runtime. + * + *

It's possible to cast a proxy obtained from this factory to {@link Advised}, + * or to obtain the ProxyFactoryBean reference and programmatically manipulate it. + * This won't work for existing prototype references, which are independent. However, + * it will work for prototypes subsequently obtained from the factory. Changes to + * interception will work immediately on singletons (including existing references). + * However, to change interfaces or target it's necessary to obtain a new instance + * from the factory. This means that singleton instances obtained from the factory + * do not have the same object identity. However, they do have the same interceptors + * and target, and changing any reference will change all objects. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #setInterceptorNames + * @see #setProxyInterfaces + * @see org.aopalliance.intercept.MethodInterceptor + * @see org.springframework.aop.Advisor + * @see Advised + */ +@SuppressWarnings("serial") +public class ProxyFactoryBean extends ProxyCreatorSupport + implements FactoryBean, BeanClassLoaderAware, BeanFactoryAware { + + /** + * This suffix in a value in an interceptor list indicates to expand globals. + */ + public static final String GLOBAL_SUFFIX = "*"; + + + protected final Log logger = LogFactory.getLog(getClass()); + + @Nullable + private String[] interceptorNames; + + @Nullable + private String targetName; + + private boolean autodetectInterfaces = true; + + private boolean singleton = true; + + private AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + private boolean freezeProxy = false; + + @Nullable + private transient ClassLoader proxyClassLoader = ClassUtils.getDefaultClassLoader(); + + private transient boolean classLoaderConfigured = false; + + @Nullable + private transient BeanFactory beanFactory; + + /** Whether the advisor chain has already been initialized. */ + private boolean advisorChainInitialized = false; + + /** If this is a singleton, the cached singleton proxy instance. */ + @Nullable + private Object singletonInstance; + + + /** + * Set the names of the interfaces we're proxying. If no interface + * is given, a CGLIB for the actual class will be created. + *

This is essentially equivalent to the "setInterfaces" method, + * but mirrors TransactionProxyFactoryBean's "setProxyInterfaces". + * @see #setInterfaces + * @see AbstractSingletonProxyFactoryBean#setProxyInterfaces + */ + public void setProxyInterfaces(Class[] proxyInterfaces) throws ClassNotFoundException { + setInterfaces(proxyInterfaces); + } + + /** + * Set the list of Advice/Advisor bean names. This must always be set + * to use this factory bean in a bean factory. + *

The referenced beans should be of type Interceptor, Advisor or Advice + * The last entry in the list can be the name of any bean in the factory. + * If it's neither an Advice nor an Advisor, a new SingletonTargetSource + * is added to wrap it. Such a target bean cannot be used if the "target" + * or "targetSource" or "targetName" property is set, in which case the + * "interceptorNames" array must contain only Advice/Advisor bean names. + *

NOTE: Specifying a target bean as final name in the "interceptorNames" + * list is deprecated and will be removed in a future Spring version. + * Use the {@link #setTargetName "targetName"} property instead. + * @see org.aopalliance.intercept.MethodInterceptor + * @see org.springframework.aop.Advisor + * @see org.aopalliance.aop.Advice + * @see org.springframework.aop.target.SingletonTargetSource + */ + public void setInterceptorNames(String... interceptorNames) { + this.interceptorNames = interceptorNames; + } + + /** + * Set the name of the target bean. This is an alternative to specifying + * the target name at the end of the "interceptorNames" array. + *

You can also specify a target object or a TargetSource object + * directly, via the "target"/"targetSource" property, respectively. + * @see #setInterceptorNames(String[]) + * @see #setTarget(Object) + * @see #setTargetSource(org.springframework.aop.TargetSource) + */ + public void setTargetName(String targetName) { + this.targetName = targetName; + } + + /** + * Set whether to autodetect proxy interfaces if none specified. + *

Default is "true". Turn this flag off to create a CGLIB + * proxy for the full target class if no interfaces specified. + * @see #setProxyTargetClass + */ + public void setAutodetectInterfaces(boolean autodetectInterfaces) { + this.autodetectInterfaces = autodetectInterfaces; + } + + /** + * Set the value of the singleton property. Governs whether this factory + * should always return the same proxy instance (which implies the same target) + * or whether it should return a new prototype instance, which implies that + * the target and interceptors may be new instances also, if they are obtained + * from prototype bean definitions. This allows for fine control of + * independence/uniqueness in the object graph. + */ + public void setSingleton(boolean singleton) { + this.singleton = singleton; + } + + /** + * Specify the AdvisorAdapterRegistry to use. + * Default is the global AdvisorAdapterRegistry. + * @see org.springframework.aop.framework.adapter.GlobalAdvisorAdapterRegistry + */ + public void setAdvisorAdapterRegistry(AdvisorAdapterRegistry advisorAdapterRegistry) { + this.advisorAdapterRegistry = advisorAdapterRegistry; + } + + @Override + public void setFrozen(boolean frozen) { + this.freezeProxy = frozen; + } + + /** + * Set the ClassLoader to generate the proxy class in. + *

Default is the bean ClassLoader, i.e. the ClassLoader used by the + * containing BeanFactory for loading all bean classes. This can be + * overridden here for specific proxies. + */ + public void setProxyClassLoader(@Nullable ClassLoader classLoader) { + this.proxyClassLoader = classLoader; + this.classLoaderConfigured = (classLoader != null); + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + if (!this.classLoaderConfigured) { + this.proxyClassLoader = classLoader; + } + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + checkInterceptorNames(); + } + + + /** + * Return a proxy. Invoked when clients obtain beans from this factory bean. + * Create an instance of the AOP proxy to be returned by this factory. + * The instance will be cached for a singleton, and create on each call to + * {@code getObject()} for a proxy. + * @return a fresh AOP proxy reflecting the current state of this factory + */ + @Override + @Nullable + public Object getObject() throws BeansException { + initializeAdvisorChain(); + if (isSingleton()) { + return getSingletonInstance(); + } + else { + if (this.targetName == null) { + logger.info("Using non-singleton proxies with singleton targets is often undesirable. " + + "Enable prototype proxies by setting the 'targetName' property."); + } + return newPrototypeInstance(); + } + } + + /** + * Return the type of the proxy. Will check the singleton instance if + * already created, else fall back to the proxy interface (in case of just + * a single one), the target bean type, or the TargetSource's target class. + * @see org.springframework.aop.TargetSource#getTargetClass + */ + @Override + public Class getObjectType() { + synchronized (this) { + if (this.singletonInstance != null) { + return this.singletonInstance.getClass(); + } + } + Class[] ifcs = getProxiedInterfaces(); + if (ifcs.length == 1) { + return ifcs[0]; + } + else if (ifcs.length > 1) { + return createCompositeInterface(ifcs); + } + else if (this.targetName != null && this.beanFactory != null) { + return this.beanFactory.getType(this.targetName); + } + else { + return getTargetClass(); + } + } + + @Override + public boolean isSingleton() { + return this.singleton; + } + + + /** + * Create a composite interface Class for the given interfaces, + * implementing the given interfaces in one single Class. + *

The default implementation builds a JDK proxy class for the + * given interfaces. + * @param interfaces the interfaces to merge + * @return the merged interface as Class + * @see java.lang.reflect.Proxy#getProxyClass + */ + protected Class createCompositeInterface(Class[] interfaces) { + return ClassUtils.createCompositeInterface(interfaces, this.proxyClassLoader); + } + + /** + * Return the singleton instance of this class's proxy object, + * lazily creating it if it hasn't been created already. + * @return the shared singleton proxy + */ + private synchronized Object getSingletonInstance() { + if (this.singletonInstance == null) { + this.targetSource = freshTargetSource(); + if (this.autodetectInterfaces && getProxiedInterfaces().length == 0 && !isProxyTargetClass()) { + // Rely on AOP infrastructure to tell us what interfaces to proxy. + Class targetClass = getTargetClass(); + if (targetClass == null) { + throw new FactoryBeanNotInitializedException("Cannot determine target class for proxy"); + } + setInterfaces(ClassUtils.getAllInterfacesForClass(targetClass, this.proxyClassLoader)); + } + // Initialize the shared singleton instance. + super.setFrozen(this.freezeProxy); + this.singletonInstance = getProxy(createAopProxy()); + } + return this.singletonInstance; + } + + /** + * Create a new prototype instance of this class's created proxy object, + * backed by an independent AdvisedSupport configuration. + * @return a totally independent proxy, whose advice we may manipulate in isolation + */ + private synchronized Object newPrototypeInstance() { + // In the case of a prototype, we need to give the proxy + // an independent instance of the configuration. + // In this case, no proxy will have an instance of this object's configuration, + // but will have an independent copy. + ProxyCreatorSupport copy = new ProxyCreatorSupport(getAopProxyFactory()); + + // The copy needs a fresh advisor chain, and a fresh TargetSource. + TargetSource targetSource = freshTargetSource(); + copy.copyConfigurationFrom(this, targetSource, freshAdvisorChain()); + if (this.autodetectInterfaces && getProxiedInterfaces().length == 0 && !isProxyTargetClass()) { + // Rely on AOP infrastructure to tell us what interfaces to proxy. + Class targetClass = targetSource.getTargetClass(); + if (targetClass != null) { + copy.setInterfaces(ClassUtils.getAllInterfacesForClass(targetClass, this.proxyClassLoader)); + } + } + copy.setFrozen(this.freezeProxy); + + return getProxy(copy.createAopProxy()); + } + + /** + * Return the proxy object to expose. + *

The default implementation uses a {@code getProxy} call with + * the factory's bean class loader. Can be overridden to specify a + * custom class loader. + * @param aopProxy the prepared AopProxy instance to get the proxy from + * @return the proxy object to expose + * @see AopProxy#getProxy(ClassLoader) + */ + protected Object getProxy(AopProxy aopProxy) { + return aopProxy.getProxy(this.proxyClassLoader); + } + + /** + * Check the interceptorNames list whether it contains a target name as final element. + * If found, remove the final name from the list and set it as targetName. + */ + private void checkInterceptorNames() { + if (!ObjectUtils.isEmpty(this.interceptorNames)) { + String finalName = this.interceptorNames[this.interceptorNames.length - 1]; + if (this.targetName == null && this.targetSource == EMPTY_TARGET_SOURCE) { + // The last name in the chain may be an Advisor/Advice or a target/TargetSource. + // Unfortunately we don't know; we must look at type of the bean. + if (!finalName.endsWith(GLOBAL_SUFFIX) && !isNamedBeanAnAdvisorOrAdvice(finalName)) { + // The target isn't an interceptor. + this.targetName = finalName; + if (logger.isDebugEnabled()) { + logger.debug("Bean with name '" + finalName + "' concluding interceptor chain " + + "is not an advisor class: treating it as a target or TargetSource"); + } + this.interceptorNames = Arrays.copyOf(this.interceptorNames, this.interceptorNames.length - 1); + } + } + } + } + + /** + * Look at bean factory metadata to work out whether this bean name, + * which concludes the interceptorNames list, is an Advisor or Advice, + * or may be a target. + * @param beanName bean name to check + * @return {@code true} if it's an Advisor or Advice + */ + private boolean isNamedBeanAnAdvisorOrAdvice(String beanName) { + Assert.state(this.beanFactory != null, "No BeanFactory set"); + Class namedBeanClass = this.beanFactory.getType(beanName); + if (namedBeanClass != null) { + return (Advisor.class.isAssignableFrom(namedBeanClass) || Advice.class.isAssignableFrom(namedBeanClass)); + } + // Treat it as an target bean if we can't tell. + if (logger.isDebugEnabled()) { + logger.debug("Could not determine type of bean with name '" + beanName + + "' - assuming it is neither an Advisor nor an Advice"); + } + return false; + } + + /** + * Create the advisor (interceptor) chain. Advisors that are sourced + * from a BeanFactory will be refreshed each time a new prototype instance + * is added. Interceptors added programmatically through the factory API + * are unaffected by such changes. + */ + private synchronized void initializeAdvisorChain() throws AopConfigException, BeansException { + if (this.advisorChainInitialized) { + return; + } + + if (!ObjectUtils.isEmpty(this.interceptorNames)) { + if (this.beanFactory == null) { + throw new IllegalStateException("No BeanFactory available anymore (probably due to serialization) " + + "- cannot resolve interceptor names " + Arrays.asList(this.interceptorNames)); + } + + // Globals can't be last unless we specified a targetSource using the property... + if (this.interceptorNames[this.interceptorNames.length - 1].endsWith(GLOBAL_SUFFIX) && + this.targetName == null && this.targetSource == EMPTY_TARGET_SOURCE) { + throw new AopConfigException("Target required after globals"); + } + + // Materialize interceptor chain from bean names. + for (String name : this.interceptorNames) { + if (name.endsWith(GLOBAL_SUFFIX)) { + if (!(this.beanFactory instanceof ListableBeanFactory)) { + throw new AopConfigException( + "Can only use global advisors or interceptors with a ListableBeanFactory"); + } + addGlobalAdvisors((ListableBeanFactory) this.beanFactory, + name.substring(0, name.length() - GLOBAL_SUFFIX.length())); + } + + else { + // If we get here, we need to add a named interceptor. + // We must check if it's a singleton or prototype. + Object advice; + if (this.singleton || this.beanFactory.isSingleton(name)) { + // Add the real Advisor/Advice to the chain. + advice = this.beanFactory.getBean(name); + } + else { + // It's a prototype Advice or Advisor: replace with a prototype. + // Avoid unnecessary creation of prototype bean just for advisor chain initialization. + advice = new PrototypePlaceholderAdvisor(name); + } + addAdvisorOnChainCreation(advice); + } + } + } + + this.advisorChainInitialized = true; + } + + + /** + * Return an independent advisor chain. + * We need to do this every time a new prototype instance is returned, + * to return distinct instances of prototype Advisors and Advices. + */ + private List freshAdvisorChain() { + Advisor[] advisors = getAdvisors(); + List freshAdvisors = new ArrayList<>(advisors.length); + for (Advisor advisor : advisors) { + if (advisor instanceof PrototypePlaceholderAdvisor) { + PrototypePlaceholderAdvisor pa = (PrototypePlaceholderAdvisor) advisor; + if (logger.isDebugEnabled()) { + logger.debug("Refreshing bean named '" + pa.getBeanName() + "'"); + } + // Replace the placeholder with a fresh prototype instance resulting from a getBean lookup + if (this.beanFactory == null) { + throw new IllegalStateException("No BeanFactory available anymore (probably due to " + + "serialization) - cannot resolve prototype advisor '" + pa.getBeanName() + "'"); + } + Object bean = this.beanFactory.getBean(pa.getBeanName()); + Advisor refreshedAdvisor = namedBeanToAdvisor(bean); + freshAdvisors.add(refreshedAdvisor); + } + else { + // Add the shared instance. + freshAdvisors.add(advisor); + } + } + return freshAdvisors; + } + + /** + * Add all global interceptors and pointcuts. + */ + private void addGlobalAdvisors(ListableBeanFactory beanFactory, String prefix) { + String[] globalAdvisorNames = + BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, Advisor.class); + String[] globalInterceptorNames = + BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, Interceptor.class); + if (globalAdvisorNames.length > 0 || globalInterceptorNames.length > 0) { + List beans = new ArrayList<>(globalAdvisorNames.length + globalInterceptorNames.length); + for (String name : globalAdvisorNames) { + if (name.startsWith(prefix)) { + beans.add(beanFactory.getBean(name)); + } + } + for (String name : globalInterceptorNames) { + if (name.startsWith(prefix)) { + beans.add(beanFactory.getBean(name)); + } + } + AnnotationAwareOrderComparator.sort(beans); + for (Object bean : beans) { + addAdvisorOnChainCreation(bean); + } + } + } + + /** + * Invoked when advice chain is created. + *

Add the given advice, advisor or object to the interceptor list. + * Because of these three possibilities, we can't type the signature + * more strongly. + * @param next advice, advisor or target object + */ + private void addAdvisorOnChainCreation(Object next) { + // We need to convert to an Advisor if necessary so that our source reference + // matches what we find from superclass interceptors. + addAdvisor(namedBeanToAdvisor(next)); + } + + /** + * Return a TargetSource to use when creating a proxy. If the target was not + * specified at the end of the interceptorNames list, the TargetSource will be + * this class's TargetSource member. Otherwise, we get the target bean and wrap + * it in a TargetSource if necessary. + */ + private TargetSource freshTargetSource() { + if (this.targetName == null) { + // Not refreshing target: bean name not specified in 'interceptorNames' + return this.targetSource; + } + else { + if (this.beanFactory == null) { + throw new IllegalStateException("No BeanFactory available anymore (probably due to serialization) " + + "- cannot resolve target with name '" + this.targetName + "'"); + } + if (logger.isDebugEnabled()) { + logger.debug("Refreshing target with name '" + this.targetName + "'"); + } + Object target = this.beanFactory.getBean(this.targetName); + return (target instanceof TargetSource ? (TargetSource) target : new SingletonTargetSource(target)); + } + } + + /** + * Convert the following object sourced from calling getBean() on a name in the + * interceptorNames array to an Advisor or TargetSource. + */ + private Advisor namedBeanToAdvisor(Object next) { + try { + return this.advisorAdapterRegistry.wrap(next); + } + catch (UnknownAdviceTypeException ex) { + // We expected this to be an Advisor or Advice, + // but it wasn't. This is a configuration error. + throw new AopConfigException("Unknown advisor type " + next.getClass() + + "; can only include Advisor or Advice type beans in interceptorNames chain " + + "except for last entry which may also be target instance or TargetSource", ex); + } + } + + /** + * Blow away and recache singleton on an advice change. + */ + @Override + protected void adviceChanged() { + super.adviceChanged(); + if (this.singleton) { + logger.debug("Advice has changed; re-caching singleton instance"); + synchronized (this) { + this.singletonInstance = null; + } + } + } + + + //--------------------------------------------------------------------- + // Serialization support + //--------------------------------------------------------------------- + + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + // Rely on default serialization; just initialize state after deserialization. + ois.defaultReadObject(); + + // Initialize transient fields. + this.proxyClassLoader = ClassUtils.getDefaultClassLoader(); + } + + + /** + * Used in the interceptor chain where we need to replace a bean with a prototype + * on creating a proxy. + */ + private static class PrototypePlaceholderAdvisor implements Advisor, Serializable { + + private final String beanName; + + private final String message; + + public PrototypePlaceholderAdvisor(String beanName) { + this.beanName = beanName; + this.message = "Placeholder for prototype Advisor/Advice with bean name '" + beanName + "'"; + } + + public String getBeanName() { + return this.beanName; + } + + @Override + public Advice getAdvice() { + throw new UnsupportedOperationException("Cannot invoke methods: " + this.message); + } + + @Override + public boolean isPerInstance() { + throw new UnsupportedOperationException("Cannot invoke methods: " + this.message); + } + + @Override + public String toString() { + return this.message; + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/ProxyProcessorSupport.java b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyProcessorSupport.java new file mode 100644 index 0000000..f58e0be --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyProcessorSupport.java @@ -0,0 +1,152 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import java.io.Closeable; + +import org.springframework.beans.factory.Aware; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.Ordered; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; + +/** + * Base class with common functionality for proxy processors, in particular + * ClassLoader management and the {@link #evaluateProxyInterfaces} algorithm. + * + * @author Juergen Hoeller + * @since 4.1 + * @see AbstractAdvisingBeanPostProcessor + * @see org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator + */ +@SuppressWarnings("serial") +public class ProxyProcessorSupport extends ProxyConfig implements Ordered, BeanClassLoaderAware, AopInfrastructureBean { + + /** + * This should run after all other processors, so that it can just add + * an advisor to existing proxies rather than double-proxy. + */ + private int order = Ordered.LOWEST_PRECEDENCE; + + @Nullable + private ClassLoader proxyClassLoader = ClassUtils.getDefaultClassLoader(); + + private boolean classLoaderConfigured = false; + + + /** + * Set the ordering which will apply to this processor's implementation + * of {@link Ordered}, used when applying multiple processors. + *

The default value is {@code Ordered.LOWEST_PRECEDENCE}, meaning non-ordered. + * @param order the ordering value + */ + public void setOrder(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return this.order; + } + + /** + * Set the ClassLoader to generate the proxy class in. + *

Default is the bean ClassLoader, i.e. the ClassLoader used by the containing + * {@link org.springframework.beans.factory.BeanFactory} for loading all bean classes. + * This can be overridden here for specific proxies. + */ + public void setProxyClassLoader(@Nullable ClassLoader classLoader) { + this.proxyClassLoader = classLoader; + this.classLoaderConfigured = (classLoader != null); + } + + /** + * Return the configured proxy ClassLoader for this processor. + */ + @Nullable + protected ClassLoader getProxyClassLoader() { + return this.proxyClassLoader; + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + if (!this.classLoaderConfigured) { + this.proxyClassLoader = classLoader; + } + } + + + /** + * Check the interfaces on the given bean class and apply them to the {@link ProxyFactory}, + * if appropriate. + *

Calls {@link #isConfigurationCallbackInterface} and {@link #isInternalLanguageInterface} + * to filter for reasonable proxy interfaces, falling back to a target-class proxy otherwise. + * @param beanClass the class of the bean + * @param proxyFactory the ProxyFactory for the bean + */ + protected void evaluateProxyInterfaces(Class beanClass, ProxyFactory proxyFactory) { + Class[] targetInterfaces = ClassUtils.getAllInterfacesForClass(beanClass, getProxyClassLoader()); + boolean hasReasonableProxyInterface = false; + for (Class ifc : targetInterfaces) { + if (!isConfigurationCallbackInterface(ifc) && !isInternalLanguageInterface(ifc) && + ifc.getMethods().length > 0) { + hasReasonableProxyInterface = true; + break; + } + } + if (hasReasonableProxyInterface) { + // Must allow for introductions; can't just set interfaces to the target's interfaces only. + for (Class ifc : targetInterfaces) { + proxyFactory.addInterface(ifc); + } + } + else { + proxyFactory.setProxyTargetClass(true); + } + } + + /** + * Determine whether the given interface is just a container callback and + * therefore not to be considered as a reasonable proxy interface. + *

If no reasonable proxy interface is found for a given bean, it will get + * proxied with its full target class, assuming that as the user's intention. + * @param ifc the interface to check + * @return whether the given interface is just a container callback + */ + protected boolean isConfigurationCallbackInterface(Class ifc) { + return (InitializingBean.class == ifc || DisposableBean.class == ifc || Closeable.class == ifc || + AutoCloseable.class == ifc || ObjectUtils.containsElement(ifc.getInterfaces(), Aware.class)); + } + + /** + * Determine whether the given interface is a well-known internal language interface + * and therefore not to be considered as a reasonable proxy interface. + *

If no reasonable proxy interface is found for a given bean, it will get + * proxied with its full target class, assuming that as the user's intention. + * @param ifc the interface to check + * @return whether the given interface is an internal language interface + */ + protected boolean isInternalLanguageInterface(Class ifc) { + return (ifc.getName().equals("groovy.lang.GroovyObject") || + ifc.getName().endsWith(".cglib.proxy.Factory") || + ifc.getName().endsWith(".bytebuddy.MockAccess")); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/ReflectiveMethodInvocation.java b/spring-aop/src/main/java/org/springframework/aop/framework/ReflectiveMethodInvocation.java new file mode 100644 index 0000000..1db3422 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/ReflectiveMethodInvocation.java @@ -0,0 +1,299 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.aop.ProxyMethodInvocation; +import org.springframework.aop.support.AopUtils; +import org.springframework.core.BridgeMethodResolver; +import org.springframework.lang.Nullable; + +/** + * Spring's implementation of the AOP Alliance + * {@link org.aopalliance.intercept.MethodInvocation} interface, + * implementing the extended + * {@link org.springframework.aop.ProxyMethodInvocation} interface. + * + *

Invokes the target object using reflection. Subclasses can override the + * {@link #invokeJoinpoint()} method to change this behavior, so this is also + * a useful base class for more specialized MethodInvocation implementations. + * + *

It is possible to clone an invocation, to invoke {@link #proceed()} + * repeatedly (once per clone), using the {@link #invocableClone()} method. + * It is also possible to attach custom attributes to the invocation, + * using the {@link #setUserAttribute} / {@link #getUserAttribute} methods. + * + *

NOTE: This class is considered internal and should not be + * directly accessed. The sole reason for it being public is compatibility + * with existing framework integrations (e.g. Pitchfork). For any other + * purposes, use the {@link ProxyMethodInvocation} interface instead. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Adrian Colyer + * @see #invokeJoinpoint + * @see #proceed + * @see #invocableClone + * @see #setUserAttribute + * @see #getUserAttribute + */ +public class ReflectiveMethodInvocation implements ProxyMethodInvocation, Cloneable { + + protected final Object proxy; + + @Nullable + protected final Object target; + + protected final Method method; + + protected Object[] arguments; + + @Nullable + private final Class targetClass; + + /** + * Lazily initialized map of user-specific attributes for this invocation. + */ + @Nullable + private Map userAttributes; + + /** + * List of MethodInterceptor and InterceptorAndDynamicMethodMatcher + * that need dynamic checks. + */ + protected final List interceptorsAndDynamicMethodMatchers; + + /** + * Index from 0 of the current interceptor we're invoking. + * -1 until we invoke: then the current interceptor. + */ + private int currentInterceptorIndex = -1; + + + /** + * Construct a new ReflectiveMethodInvocation with the given arguments. + * @param proxy the proxy object that the invocation was made on + * @param target the target object to invoke + * @param method the method to invoke + * @param arguments the arguments to invoke the method with + * @param targetClass the target class, for MethodMatcher invocations + * @param interceptorsAndDynamicMethodMatchers interceptors that should be applied, + * along with any InterceptorAndDynamicMethodMatchers that need evaluation at runtime. + * MethodMatchers included in this struct must already have been found to have matched + * as far as was possibly statically. Passing an array might be about 10% faster, + * but would complicate the code. And it would work only for static pointcuts. + */ + protected ReflectiveMethodInvocation( + Object proxy, @Nullable Object target, Method method, @Nullable Object[] arguments, + @Nullable Class targetClass, List interceptorsAndDynamicMethodMatchers) { + + this.proxy = proxy; + this.target = target; + this.targetClass = targetClass; + this.method = BridgeMethodResolver.findBridgedMethod(method); + this.arguments = AopProxyUtils.adaptArgumentsIfNecessary(method, arguments); + this.interceptorsAndDynamicMethodMatchers = interceptorsAndDynamicMethodMatchers; + } + + + @Override + public final Object getProxy() { + return this.proxy; + } + + @Override + @Nullable + public final Object getThis() { + return this.target; + } + + @Override + public final AccessibleObject getStaticPart() { + return this.method; + } + + /** + * Return the method invoked on the proxied interface. + * May or may not correspond with a method invoked on an underlying + * implementation of that interface. + */ + @Override + public final Method getMethod() { + return this.method; + } + + @Override + public final Object[] getArguments() { + return this.arguments; + } + + @Override + public void setArguments(Object... arguments) { + this.arguments = arguments; + } + + + @Override + @Nullable + public Object proceed() throws Throwable { + // We start with an index of -1 and increment early. + if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) { + return invokeJoinpoint(); + } + + Object interceptorOrInterceptionAdvice = + this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex); + if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) { + // Evaluate dynamic method matcher here: static part will already have + // been evaluated and found to match. + InterceptorAndDynamicMethodMatcher dm = + (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice; + Class targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass()); + if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) { + return dm.interceptor.invoke(this); + } + else { + // Dynamic matching failed. + // Skip this interceptor and invoke the next in the chain. + return proceed(); + } + } + else { + // It's an interceptor, so we just invoke it: The pointcut will have + // been evaluated statically before this object was constructed. + return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this); + } + } + + /** + * Invoke the joinpoint using reflection. + * Subclasses can override this to use custom invocation. + * @return the return value of the joinpoint + * @throws Throwable if invoking the joinpoint resulted in an exception + */ + @Nullable + protected Object invokeJoinpoint() throws Throwable { + return AopUtils.invokeJoinpointUsingReflection(this.target, this.method, this.arguments); + } + + + /** + * This implementation returns a shallow copy of this invocation object, + * including an independent copy of the original arguments array. + *

We want a shallow copy in this case: We want to use the same interceptor + * chain and other object references, but we want an independent value for the + * current interceptor index. + * @see java.lang.Object#clone() + */ + @Override + public MethodInvocation invocableClone() { + Object[] cloneArguments = this.arguments; + if (this.arguments.length > 0) { + // Build an independent copy of the arguments array. + cloneArguments = this.arguments.clone(); + } + return invocableClone(cloneArguments); + } + + /** + * This implementation returns a shallow copy of this invocation object, + * using the given arguments array for the clone. + *

We want a shallow copy in this case: We want to use the same interceptor + * chain and other object references, but we want an independent value for the + * current interceptor index. + * @see java.lang.Object#clone() + */ + @Override + public MethodInvocation invocableClone(Object... arguments) { + // Force initialization of the user attributes Map, + // for having a shared Map reference in the clone. + if (this.userAttributes == null) { + this.userAttributes = new HashMap<>(); + } + + // Create the MethodInvocation clone. + try { + ReflectiveMethodInvocation clone = (ReflectiveMethodInvocation) clone(); + clone.arguments = arguments; + return clone; + } + catch (CloneNotSupportedException ex) { + throw new IllegalStateException( + "Should be able to clone object of type [" + getClass() + "]: " + ex); + } + } + + + @Override + public void setUserAttribute(String key, @Nullable Object value) { + if (value != null) { + if (this.userAttributes == null) { + this.userAttributes = new HashMap<>(); + } + this.userAttributes.put(key, value); + } + else { + if (this.userAttributes != null) { + this.userAttributes.remove(key); + } + } + } + + @Override + @Nullable + public Object getUserAttribute(String key) { + return (this.userAttributes != null ? this.userAttributes.get(key) : null); + } + + /** + * Return user attributes associated with this invocation. + * This method provides an invocation-bound alternative to a ThreadLocal. + *

This map is initialized lazily and is not used in the AOP framework itself. + * @return any user attributes associated with this invocation + * (never {@code null}) + */ + public Map getUserAttributes() { + if (this.userAttributes == null) { + this.userAttributes = new HashMap<>(); + } + return this.userAttributes; + } + + + @Override + public String toString() { + // Don't do toString on target, it may be proxied. + StringBuilder sb = new StringBuilder("ReflectiveMethodInvocation: "); + sb.append(this.method).append("; "); + if (this.target == null) { + sb.append("target is null"); + } + else { + sb.append("target is of class [").append(this.target.getClass().getName()).append(']'); + } + return sb.toString(); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AdvisorAdapter.java b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AdvisorAdapter.java new file mode 100644 index 0000000..d717bbf --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AdvisorAdapter.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.adapter; + +import org.aopalliance.aop.Advice; +import org.aopalliance.intercept.MethodInterceptor; + +import org.springframework.aop.Advisor; + +/** + * Interface allowing extension to the Spring AOP framework to allow + * handling of new Advisors and Advice types. + * + *

Implementing objects can create AOP Alliance Interceptors from + * custom advice types, enabling these advice types to be used + * in the Spring AOP framework, which uses interception under the covers. + * + *

There is no need for most Spring users to implement this interface; + * do so only if you need to introduce more Advisor or Advice types to Spring. + * + * @author Rod Johnson + */ +public interface AdvisorAdapter { + + /** + * Does this adapter understand this advice object? Is it valid to + * invoke the {@code getInterceptors} method with an Advisor that + * contains this advice as an argument? + * @param advice an Advice such as a BeforeAdvice + * @return whether this adapter understands the given advice object + * @see #getInterceptor(org.springframework.aop.Advisor) + * @see org.springframework.aop.BeforeAdvice + */ + boolean supportsAdvice(Advice advice); + + /** + * Return an AOP Alliance MethodInterceptor exposing the behavior of + * the given advice to an interception-based AOP framework. + *

Don't worry about any Pointcut contained in the Advisor; + * the AOP framework will take care of checking the pointcut. + * @param advisor the Advisor. The supportsAdvice() method must have + * returned true on this object + * @return an AOP Alliance interceptor for this Advisor. There's + * no need to cache instances for efficiency, as the AOP framework + * caches advice chains. + */ + MethodInterceptor getInterceptor(Advisor advisor); + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AdvisorAdapterRegistrationManager.java b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AdvisorAdapterRegistrationManager.java new file mode 100644 index 0000000..c9a4af8 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AdvisorAdapterRegistrationManager.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.adapter; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; + +/** + * BeanPostProcessor that registers {@link AdvisorAdapter} beans in the BeanFactory with + * an {@link AdvisorAdapterRegistry} (by default the {@link GlobalAdvisorAdapterRegistry}). + * + *

The only requirement for it to work is that it needs to be defined + * in application context along with "non-native" Spring AdvisorAdapters + * that need to be "recognized" by Spring's AOP framework. + * + * @author Dmitriy Kopylenko + * @author Juergen Hoeller + * @since 27.02.2004 + * @see #setAdvisorAdapterRegistry + * @see AdvisorAdapter + */ +public class AdvisorAdapterRegistrationManager implements BeanPostProcessor { + + private AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + + /** + * Specify the AdvisorAdapterRegistry to register AdvisorAdapter beans with. + * Default is the global AdvisorAdapterRegistry. + * @see GlobalAdvisorAdapterRegistry + */ + public void setAdvisorAdapterRegistry(AdvisorAdapterRegistry advisorAdapterRegistry) { + this.advisorAdapterRegistry = advisorAdapterRegistry; + } + + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof AdvisorAdapter){ + this.advisorAdapterRegistry.registerAdvisorAdapter((AdvisorAdapter) bean); + } + return bean; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AdvisorAdapterRegistry.java b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AdvisorAdapterRegistry.java new file mode 100644 index 0000000..5a9fb99 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AdvisorAdapterRegistry.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.adapter; + +import org.aopalliance.intercept.MethodInterceptor; + +import org.springframework.aop.Advisor; + +/** + * Interface for registries of Advisor adapters. + * + *

This is an SPI interface, not to be implemented by any Spring user. + * + * @author Rod Johnson + * @author Rob Harrop + */ +public interface AdvisorAdapterRegistry { + + /** + * Return an {@link Advisor} wrapping the given advice. + *

Should by default at least support + * {@link org.aopalliance.intercept.MethodInterceptor}, + * {@link org.springframework.aop.MethodBeforeAdvice}, + * {@link org.springframework.aop.AfterReturningAdvice}, + * {@link org.springframework.aop.ThrowsAdvice}. + * @param advice an object that should be an advice + * @return an Advisor wrapping the given advice (never {@code null}; + * if the advice parameter is an Advisor, it is to be returned as-is) + * @throws UnknownAdviceTypeException if no registered advisor adapter + * can wrap the supposed advice + */ + Advisor wrap(Object advice) throws UnknownAdviceTypeException; + + /** + * Return an array of AOP Alliance MethodInterceptors to allow use of the + * given Advisor in an interception-based framework. + *

Don't worry about the pointcut associated with the {@link Advisor}, if it is + * a {@link org.springframework.aop.PointcutAdvisor}: just return an interceptor. + * @param advisor the Advisor to find an interceptor for + * @return an array of MethodInterceptors to expose this Advisor's behavior + * @throws UnknownAdviceTypeException if the Advisor type is + * not understood by any registered AdvisorAdapter + */ + MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException; + + /** + * Register the given {@link AdvisorAdapter}. Note that it is not necessary to register + * adapters for an AOP Alliance Interceptors or Spring Advices: these must be + * automatically recognized by an {@code AdvisorAdapterRegistry} implementation. + * @param adapter an AdvisorAdapter that understands particular Advisor or Advice types + */ + void registerAdvisorAdapter(AdvisorAdapter adapter); + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AfterReturningAdviceAdapter.java b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AfterReturningAdviceAdapter.java new file mode 100644 index 0000000..ba4b049 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AfterReturningAdviceAdapter.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.adapter; + +import java.io.Serializable; + +import org.aopalliance.aop.Advice; +import org.aopalliance.intercept.MethodInterceptor; + +import org.springframework.aop.Advisor; +import org.springframework.aop.AfterReturningAdvice; + +/** + * Adapter to enable {@link org.springframework.aop.AfterReturningAdvice} + * to be used in the Spring AOP framework. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +@SuppressWarnings("serial") +class AfterReturningAdviceAdapter implements AdvisorAdapter, Serializable { + + @Override + public boolean supportsAdvice(Advice advice) { + return (advice instanceof AfterReturningAdvice); + } + + @Override + public MethodInterceptor getInterceptor(Advisor advisor) { + AfterReturningAdvice advice = (AfterReturningAdvice) advisor.getAdvice(); + return new AfterReturningAdviceInterceptor(advice); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AfterReturningAdviceInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AfterReturningAdviceInterceptor.java new file mode 100644 index 0000000..4ce1c45 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AfterReturningAdviceInterceptor.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.adapter; + +import java.io.Serializable; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.aop.AfterAdvice; +import org.springframework.aop.AfterReturningAdvice; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Interceptor to wrap an {@link org.springframework.aop.AfterReturningAdvice}. + * Used internally by the AOP framework; application developers should not need + * to use this class directly. + * + * @author Rod Johnson + * @see MethodBeforeAdviceInterceptor + * @see ThrowsAdviceInterceptor + */ +@SuppressWarnings("serial") +public class AfterReturningAdviceInterceptor implements MethodInterceptor, AfterAdvice, Serializable { + + private final AfterReturningAdvice advice; + + + /** + * Create a new AfterReturningAdviceInterceptor for the given advice. + * @param advice the AfterReturningAdvice to wrap + */ + public AfterReturningAdviceInterceptor(AfterReturningAdvice advice) { + Assert.notNull(advice, "Advice must not be null"); + this.advice = advice; + } + + + @Override + @Nullable + public Object invoke(MethodInvocation mi) throws Throwable { + Object retVal = mi.proceed(); + this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis()); + return retVal; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/DefaultAdvisorAdapterRegistry.java b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/DefaultAdvisorAdapterRegistry.java new file mode 100644 index 0000000..7f2c661 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/DefaultAdvisorAdapterRegistry.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.adapter; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import org.aopalliance.aop.Advice; +import org.aopalliance.intercept.MethodInterceptor; + +import org.springframework.aop.Advisor; +import org.springframework.aop.support.DefaultPointcutAdvisor; + +/** + * Default implementation of the {@link AdvisorAdapterRegistry} interface. + * Supports {@link org.aopalliance.intercept.MethodInterceptor}, + * {@link org.springframework.aop.MethodBeforeAdvice}, + * {@link org.springframework.aop.AfterReturningAdvice}, + * {@link org.springframework.aop.ThrowsAdvice}. + * + * @author Rod Johnson + * @author Rob Harrop + * @author Juergen Hoeller + */ +@SuppressWarnings("serial") +public class DefaultAdvisorAdapterRegistry implements AdvisorAdapterRegistry, Serializable { + + private final List adapters = new ArrayList<>(3); + + + /** + * Create a new DefaultAdvisorAdapterRegistry, registering well-known adapters. + */ + public DefaultAdvisorAdapterRegistry() { + registerAdvisorAdapter(new MethodBeforeAdviceAdapter()); + registerAdvisorAdapter(new AfterReturningAdviceAdapter()); + registerAdvisorAdapter(new ThrowsAdviceAdapter()); + } + + + @Override + public Advisor wrap(Object adviceObject) throws UnknownAdviceTypeException { + if (adviceObject instanceof Advisor) { + return (Advisor) adviceObject; + } + if (!(adviceObject instanceof Advice)) { + throw new UnknownAdviceTypeException(adviceObject); + } + Advice advice = (Advice) adviceObject; + if (advice instanceof MethodInterceptor) { + // So well-known it doesn't even need an adapter. + return new DefaultPointcutAdvisor(advice); + } + for (AdvisorAdapter adapter : this.adapters) { + // Check that it is supported. + if (adapter.supportsAdvice(advice)) { + return new DefaultPointcutAdvisor(advice); + } + } + throw new UnknownAdviceTypeException(advice); + } + + @Override + public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException { + List interceptors = new ArrayList<>(3); + Advice advice = advisor.getAdvice(); + if (advice instanceof MethodInterceptor) { + interceptors.add((MethodInterceptor) advice); + } + for (AdvisorAdapter adapter : this.adapters) { + if (adapter.supportsAdvice(advice)) { + interceptors.add(adapter.getInterceptor(advisor)); + } + } + if (interceptors.isEmpty()) { + throw new UnknownAdviceTypeException(advisor.getAdvice()); + } + return interceptors.toArray(new MethodInterceptor[0]); + } + + @Override + public void registerAdvisorAdapter(AdvisorAdapter adapter) { + this.adapters.add(adapter); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/GlobalAdvisorAdapterRegistry.java b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/GlobalAdvisorAdapterRegistry.java new file mode 100644 index 0000000..705fe94 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/GlobalAdvisorAdapterRegistry.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.adapter; + +/** + * Singleton to publish a shared DefaultAdvisorAdapterRegistry instance. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Phillip Webb + * @see DefaultAdvisorAdapterRegistry + */ +public final class GlobalAdvisorAdapterRegistry { + + private GlobalAdvisorAdapterRegistry() { + } + + + /** + * Keep track of a single instance so we can return it to classes that request it. + */ + private static AdvisorAdapterRegistry instance = new DefaultAdvisorAdapterRegistry(); + + /** + * Return the singleton {@link DefaultAdvisorAdapterRegistry} instance. + */ + public static AdvisorAdapterRegistry getInstance() { + return instance; + } + + /** + * Reset the singleton {@link DefaultAdvisorAdapterRegistry}, removing any + * {@link AdvisorAdapterRegistry#registerAdvisorAdapter(AdvisorAdapter) registered} + * adapters. + */ + static void reset() { + instance = new DefaultAdvisorAdapterRegistry(); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/MethodBeforeAdviceAdapter.java b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/MethodBeforeAdviceAdapter.java new file mode 100644 index 0000000..7cd7262 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/MethodBeforeAdviceAdapter.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.adapter; + +import java.io.Serializable; + +import org.aopalliance.aop.Advice; +import org.aopalliance.intercept.MethodInterceptor; + +import org.springframework.aop.Advisor; +import org.springframework.aop.MethodBeforeAdvice; + +/** + * Adapter to enable {@link org.springframework.aop.MethodBeforeAdvice} + * to be used in the Spring AOP framework. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +@SuppressWarnings("serial") +class MethodBeforeAdviceAdapter implements AdvisorAdapter, Serializable { + + @Override + public boolean supportsAdvice(Advice advice) { + return (advice instanceof MethodBeforeAdvice); + } + + @Override + public MethodInterceptor getInterceptor(Advisor advisor) { + MethodBeforeAdvice advice = (MethodBeforeAdvice) advisor.getAdvice(); + return new MethodBeforeAdviceInterceptor(advice); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/MethodBeforeAdviceInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/MethodBeforeAdviceInterceptor.java new file mode 100644 index 0000000..09683e0 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/MethodBeforeAdviceInterceptor.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.adapter; + +import java.io.Serializable; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.aop.BeforeAdvice; +import org.springframework.aop.MethodBeforeAdvice; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Interceptor to wrap a {@link MethodBeforeAdvice}. + *

Used internally by the AOP framework; application developers should not + * need to use this class directly. + * + * @author Rod Johnson + * @see AfterReturningAdviceInterceptor + * @see ThrowsAdviceInterceptor + */ +@SuppressWarnings("serial") +public class MethodBeforeAdviceInterceptor implements MethodInterceptor, BeforeAdvice, Serializable { + + private final MethodBeforeAdvice advice; + + + /** + * Create a new MethodBeforeAdviceInterceptor for the given advice. + * @param advice the MethodBeforeAdvice to wrap + */ + public MethodBeforeAdviceInterceptor(MethodBeforeAdvice advice) { + Assert.notNull(advice, "Advice must not be null"); + this.advice = advice; + } + + + @Override + @Nullable + public Object invoke(MethodInvocation mi) throws Throwable { + this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis()); + return mi.proceed(); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/ThrowsAdviceAdapter.java b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/ThrowsAdviceAdapter.java new file mode 100644 index 0000000..dd55721 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/ThrowsAdviceAdapter.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.adapter; + +import java.io.Serializable; + +import org.aopalliance.aop.Advice; +import org.aopalliance.intercept.MethodInterceptor; + +import org.springframework.aop.Advisor; +import org.springframework.aop.ThrowsAdvice; + +/** + * Adapter to enable {@link org.springframework.aop.MethodBeforeAdvice} + * to be used in the Spring AOP framework. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +@SuppressWarnings("serial") +class ThrowsAdviceAdapter implements AdvisorAdapter, Serializable { + + @Override + public boolean supportsAdvice(Advice advice) { + return (advice instanceof ThrowsAdvice); + } + + @Override + public MethodInterceptor getInterceptor(Advisor advisor) { + return new ThrowsAdviceInterceptor(advisor.getAdvice()); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptor.java new file mode 100644 index 0000000..fcca4f0 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptor.java @@ -0,0 +1,162 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.adapter; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aop.AfterAdvice; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Interceptor to wrap an after-throwing advice. + * + *

The signatures on handler methods on the {@code ThrowsAdvice} + * implementation method argument must be of the form:
+ * + * {@code void afterThrowing([Method, args, target], ThrowableSubclass);} + * + *

Only the last argument is required. + * + *

Some examples of valid methods would be: + * + *

public void afterThrowing(Exception ex)
+ *
public void afterThrowing(RemoteException)
+ *
public void afterThrowing(Method method, Object[] args, Object target, Exception ex)
+ *
public void afterThrowing(Method method, Object[] args, Object target, ServletException ex)
+ * + *

This is a framework class that need not be used directly by Spring users. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see MethodBeforeAdviceInterceptor + * @see AfterReturningAdviceInterceptor + */ +public class ThrowsAdviceInterceptor implements MethodInterceptor, AfterAdvice { + + private static final String AFTER_THROWING = "afterThrowing"; + + private static final Log logger = LogFactory.getLog(ThrowsAdviceInterceptor.class); + + + private final Object throwsAdvice; + + /** Methods on throws advice, keyed by exception class. */ + private final Map, Method> exceptionHandlerMap = new HashMap<>(); + + + /** + * Create a new ThrowsAdviceInterceptor for the given ThrowsAdvice. + * @param throwsAdvice the advice object that defines the exception handler methods + * (usually a {@link org.springframework.aop.ThrowsAdvice} implementation) + */ + public ThrowsAdviceInterceptor(Object throwsAdvice) { + Assert.notNull(throwsAdvice, "Advice must not be null"); + this.throwsAdvice = throwsAdvice; + + Method[] methods = throwsAdvice.getClass().getMethods(); + for (Method method : methods) { + if (method.getName().equals(AFTER_THROWING) && + (method.getParameterCount() == 1 || method.getParameterCount() == 4)) { + Class throwableParam = method.getParameterTypes()[method.getParameterCount() - 1]; + if (Throwable.class.isAssignableFrom(throwableParam)) { + // An exception handler to register... + this.exceptionHandlerMap.put(throwableParam, method); + if (logger.isDebugEnabled()) { + logger.debug("Found exception handler method on throws advice: " + method); + } + } + } + } + + if (this.exceptionHandlerMap.isEmpty()) { + throw new IllegalArgumentException( + "At least one handler method must be found in class [" + throwsAdvice.getClass() + "]"); + } + } + + + /** + * Return the number of handler methods in this advice. + */ + public int getHandlerMethodCount() { + return this.exceptionHandlerMap.size(); + } + + + @Override + @Nullable + public Object invoke(MethodInvocation mi) throws Throwable { + try { + return mi.proceed(); + } + catch (Throwable ex) { + Method handlerMethod = getExceptionHandler(ex); + if (handlerMethod != null) { + invokeHandlerMethod(mi, ex, handlerMethod); + } + throw ex; + } + } + + /** + * Determine the exception handle method for the given exception. + * @param exception the exception thrown + * @return a handler for the given exception type, or {@code null} if none found + */ + @Nullable + private Method getExceptionHandler(Throwable exception) { + Class exceptionClass = exception.getClass(); + if (logger.isTraceEnabled()) { + logger.trace("Trying to find handler for exception of type [" + exceptionClass.getName() + "]"); + } + Method handler = this.exceptionHandlerMap.get(exceptionClass); + while (handler == null && exceptionClass != Throwable.class) { + exceptionClass = exceptionClass.getSuperclass(); + handler = this.exceptionHandlerMap.get(exceptionClass); + } + if (handler != null && logger.isTraceEnabled()) { + logger.trace("Found handler for exception of type [" + exceptionClass.getName() + "]: " + handler); + } + return handler; + } + + private void invokeHandlerMethod(MethodInvocation mi, Throwable ex, Method method) throws Throwable { + Object[] handlerArgs; + if (method.getParameterCount() == 1) { + handlerArgs = new Object[] {ex}; + } + else { + handlerArgs = new Object[] {mi.getMethod(), mi.getArguments(), mi.getThis(), ex}; + } + try { + method.invoke(this.throwsAdvice, handlerArgs); + } + catch (InvocationTargetException targetEx) { + throw targetEx.getTargetException(); + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/UnknownAdviceTypeException.java b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/UnknownAdviceTypeException.java new file mode 100644 index 0000000..1f09b8e --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/UnknownAdviceTypeException.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.adapter; + +/** + * Exception thrown when an attempt is made to use an unsupported + * Advisor or Advice type. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see org.aopalliance.aop.Advice + * @see org.springframework.aop.Advisor + */ +@SuppressWarnings("serial") +public class UnknownAdviceTypeException extends IllegalArgumentException { + + /** + * Create a new UnknownAdviceTypeException for the given advice object. + * Will create a message text that says that the object is neither a + * subinterface of Advice nor an Advisor. + * @param advice the advice object of unknown type + */ + public UnknownAdviceTypeException(Object advice) { + super("Advice object [" + advice + "] is neither a supported subinterface of " + + "[org.aopalliance.aop.Advice] nor an [org.springframework.aop.Advisor]"); + } + + /** + * Create a new UnknownAdviceTypeException with the given message. + * @param message the message text + */ + public UnknownAdviceTypeException(String message) { + super(message); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/package-info.java b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/package-info.java new file mode 100644 index 0000000..1925e47 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/package-info.java @@ -0,0 +1,17 @@ +/** + * SPI package allowing Spring AOP framework to handle arbitrary advice types. + * + *

Users who want merely to use the Spring AOP framework, rather than extend + * its capabilities, don't need to concern themselves with this package. + * + *

You may wish to use these adapters to wrap Spring-specific advices, such as MethodBeforeAdvice, + * in MethodInterceptor, to allow their use in another AOP framework supporting the AOP Alliance interfaces. + * + *

These adapters do not depend on any other Spring framework classes to allow such usage. + */ +@NonNullApi +@NonNullFields +package org.springframework.aop.framework.adapter; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAdvisorAutoProxyCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAdvisorAutoProxyCreator.java new file mode 100644 index 0000000..4900c3d --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAdvisorAutoProxyCreator.java @@ -0,0 +1,196 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.autoproxy; + +import java.util.List; + +import org.springframework.aop.Advisor; +import org.springframework.aop.TargetSource; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Generic auto proxy creator that builds AOP proxies for specific beans + * based on detected Advisors for each bean. + * + *

Subclasses may override the {@link #findCandidateAdvisors()} method to + * return a custom list of Advisors applying to any object. Subclasses can + * also override the inherited {@link #shouldSkip} method to exclude certain + * objects from auto-proxying. + * + *

Advisors or advices requiring ordering should be annotated with + * {@link org.springframework.core.annotation.Order @Order} or implement the + * {@link org.springframework.core.Ordered} interface. This class sorts + * advisors using the {@link AnnotationAwareOrderComparator}. Advisors that are + * not annotated with {@code @Order} or don't implement the {@code Ordered} + * interface will be considered as unordered; they will appear at the end of the + * advisor chain in an undefined order. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #findCandidateAdvisors + */ +@SuppressWarnings("serial") +public abstract class AbstractAdvisorAutoProxyCreator extends AbstractAutoProxyCreator { + + @Nullable + private BeanFactoryAdvisorRetrievalHelper advisorRetrievalHelper; + + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + super.setBeanFactory(beanFactory); + if (!(beanFactory instanceof ConfigurableListableBeanFactory)) { + throw new IllegalArgumentException( + "AdvisorAutoProxyCreator requires a ConfigurableListableBeanFactory: " + beanFactory); + } + initBeanFactory((ConfigurableListableBeanFactory) beanFactory); + } + + protected void initBeanFactory(ConfigurableListableBeanFactory beanFactory) { + this.advisorRetrievalHelper = new BeanFactoryAdvisorRetrievalHelperAdapter(beanFactory); + } + + + @Override + @Nullable + protected Object[] getAdvicesAndAdvisorsForBean( + Class beanClass, String beanName, @Nullable TargetSource targetSource) { + + List advisors = findEligibleAdvisors(beanClass, beanName); + if (advisors.isEmpty()) { + return DO_NOT_PROXY; + } + return advisors.toArray(); + } + + /** + * Find all eligible Advisors for auto-proxying this class. + * @param beanClass the clazz to find advisors for + * @param beanName the name of the currently proxied bean + * @return the empty List, not {@code null}, + * if there are no pointcuts or interceptors + * @see #findCandidateAdvisors + * @see #sortAdvisors + * @see #extendAdvisors + */ + protected List findEligibleAdvisors(Class beanClass, String beanName) { + List candidateAdvisors = findCandidateAdvisors(); + List eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName); + extendAdvisors(eligibleAdvisors); + if (!eligibleAdvisors.isEmpty()) { + eligibleAdvisors = sortAdvisors(eligibleAdvisors); + } + return eligibleAdvisors; + } + + /** + * Find all candidate Advisors to use in auto-proxying. + * @return the List of candidate Advisors + */ + protected List findCandidateAdvisors() { + Assert.state(this.advisorRetrievalHelper != null, "No BeanFactoryAdvisorRetrievalHelper available"); + return this.advisorRetrievalHelper.findAdvisorBeans(); + } + + /** + * Search the given candidate Advisors to find all Advisors that + * can apply to the specified bean. + * @param candidateAdvisors the candidate Advisors + * @param beanClass the target's bean class + * @param beanName the target's bean name + * @return the List of applicable Advisors + * @see ProxyCreationContext#getCurrentProxiedBeanName() + */ + protected List findAdvisorsThatCanApply( + List candidateAdvisors, Class beanClass, String beanName) { + + ProxyCreationContext.setCurrentProxiedBeanName(beanName); + try { + return AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass); + } + finally { + ProxyCreationContext.setCurrentProxiedBeanName(null); + } + } + + /** + * Return whether the Advisor bean with the given name is eligible + * for proxying in the first place. + * @param beanName the name of the Advisor bean + * @return whether the bean is eligible + */ + protected boolean isEligibleAdvisorBean(String beanName) { + return true; + } + + /** + * Sort advisors based on ordering. Subclasses may choose to override this + * method to customize the sorting strategy. + * @param advisors the source List of Advisors + * @return the sorted List of Advisors + * @see org.springframework.core.Ordered + * @see org.springframework.core.annotation.Order + * @see org.springframework.core.annotation.AnnotationAwareOrderComparator + */ + protected List sortAdvisors(List advisors) { + AnnotationAwareOrderComparator.sort(advisors); + return advisors; + } + + /** + * Extension hook that subclasses can override to register additional Advisors, + * given the sorted Advisors obtained to date. + *

The default implementation is empty. + *

Typically used to add Advisors that expose contextual information + * required by some of the later advisors. + * @param candidateAdvisors the Advisors that have already been identified as + * applying to a given bean + */ + protected void extendAdvisors(List candidateAdvisors) { + } + + /** + * This auto-proxy creator always returns pre-filtered Advisors. + */ + @Override + protected boolean advisorsPreFiltered() { + return true; + } + + + /** + * Subclass of BeanFactoryAdvisorRetrievalHelper that delegates to + * surrounding AbstractAdvisorAutoProxyCreator facilities. + */ + private class BeanFactoryAdvisorRetrievalHelperAdapter extends BeanFactoryAdvisorRetrievalHelper { + + public BeanFactoryAdvisorRetrievalHelperAdapter(ConfigurableListableBeanFactory beanFactory) { + super(beanFactory); + } + + @Override + protected boolean isEligibleBean(String beanName) { + return AbstractAdvisorAutoProxyCreator.this.isEligibleAdvisorBean(beanName); + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java new file mode 100644 index 0000000..d4ffb04 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java @@ -0,0 +1,580 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.autoproxy; + +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.aopalliance.aop.Advice; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aop.Advisor; +import org.springframework.aop.Pointcut; +import org.springframework.aop.TargetSource; +import org.springframework.aop.framework.AopInfrastructureBean; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.framework.ProxyProcessorSupport; +import org.springframework.aop.framework.adapter.AdvisorAdapterRegistry; +import org.springframework.aop.framework.adapter.GlobalAdvisorAdapterRegistry; +import org.springframework.aop.target.SingletonTargetSource; +import org.springframework.beans.BeansException; +import org.springframework.beans.PropertyValues; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * {@link org.springframework.beans.factory.config.BeanPostProcessor} implementation + * that wraps each eligible bean with an AOP proxy, delegating to specified interceptors + * before invoking the bean itself. + * + *

This class distinguishes between "common" interceptors: shared for all proxies it + * creates, and "specific" interceptors: unique per bean instance. There need not be any + * common interceptors. If there are, they are set using the interceptorNames property. + * As with {@link org.springframework.aop.framework.ProxyFactoryBean}, interceptors names + * in the current factory are used rather than bean references to allow correct handling + * of prototype advisors and interceptors: for example, to support stateful mixins. + * Any advice type is supported for {@link #setInterceptorNames "interceptorNames"} entries. + * + *

Such auto-proxying is particularly useful if there's a large number of beans that + * need to be wrapped with similar proxies, i.e. delegating to the same interceptors. + * Instead of x repetitive proxy definitions for x target beans, you can register + * one single such post processor with the bean factory to achieve the same effect. + * + *

Subclasses can apply any strategy to decide if a bean is to be proxied, e.g. by type, + * by name, by definition details, etc. They can also return additional interceptors that + * should just be applied to the specific bean instance. A simple concrete implementation is + * {@link BeanNameAutoProxyCreator}, identifying the beans to be proxied via given names. + * + *

Any number of {@link TargetSourceCreator} implementations can be used to create + * a custom target source: for example, to pool prototype objects. Auto-proxying will + * occur even if there is no advice, as long as a TargetSourceCreator specifies a custom + * {@link org.springframework.aop.TargetSource}. If there are no TargetSourceCreators set, + * or if none matches, a {@link org.springframework.aop.target.SingletonTargetSource} + * will be used by default to wrap the target bean instance. + * + * @author Juergen Hoeller + * @author Rod Johnson + * @author Rob Harrop + * @since 13.10.2003 + * @see #setInterceptorNames + * @see #getAdvicesAndAdvisorsForBean + * @see BeanNameAutoProxyCreator + * @see DefaultAdvisorAutoProxyCreator + */ +@SuppressWarnings("serial") +public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport + implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware { + + /** + * Convenience constant for subclasses: Return value for "do not proxy". + * @see #getAdvicesAndAdvisorsForBean + */ + @Nullable + protected static final Object[] DO_NOT_PROXY = null; + + /** + * Convenience constant for subclasses: Return value for + * "proxy without additional interceptors, just the common ones". + * @see #getAdvicesAndAdvisorsForBean + */ + protected static final Object[] PROXY_WITHOUT_ADDITIONAL_INTERCEPTORS = new Object[0]; + + + /** Logger available to subclasses. */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** Default is global AdvisorAdapterRegistry. */ + private AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + + /** + * Indicates whether or not the proxy should be frozen. Overridden from super + * to prevent the configuration from becoming frozen too early. + */ + private boolean freezeProxy = false; + + /** Default is no common interceptors. */ + private String[] interceptorNames = new String[0]; + + private boolean applyCommonInterceptorsFirst = true; + + @Nullable + private TargetSourceCreator[] customTargetSourceCreators; + + @Nullable + private BeanFactory beanFactory; + + private final Set targetSourcedBeans = Collections.newSetFromMap(new ConcurrentHashMap<>(16)); + + private final Map earlyProxyReferences = new ConcurrentHashMap<>(16); + + private final Map> proxyTypes = new ConcurrentHashMap<>(16); + + private final Map advisedBeans = new ConcurrentHashMap<>(256); + + + /** + * Set whether or not the proxy should be frozen, preventing advice + * from being added to it once it is created. + *

Overridden from the super class to prevent the proxy configuration + * from being frozen before the proxy is created. + */ + @Override + public void setFrozen(boolean frozen) { + this.freezeProxy = frozen; + } + + @Override + public boolean isFrozen() { + return this.freezeProxy; + } + + /** + * Specify the {@link AdvisorAdapterRegistry} to use. + *

Default is the global {@link AdvisorAdapterRegistry}. + * @see org.springframework.aop.framework.adapter.GlobalAdvisorAdapterRegistry + */ + public void setAdvisorAdapterRegistry(AdvisorAdapterRegistry advisorAdapterRegistry) { + this.advisorAdapterRegistry = advisorAdapterRegistry; + } + + /** + * Set custom {@code TargetSourceCreators} to be applied in this order. + * If the list is empty, or they all return null, a {@link SingletonTargetSource} + * will be created for each bean. + *

Note that TargetSourceCreators will kick in even for target beans + * where no advices or advisors have been found. If a {@code TargetSourceCreator} + * returns a {@link TargetSource} for a specific bean, that bean will be proxied + * in any case. + *

{@code TargetSourceCreators} can only be invoked if this post processor is used + * in a {@link BeanFactory} and its {@link BeanFactoryAware} callback is triggered. + * @param targetSourceCreators the list of {@code TargetSourceCreators}. + * Ordering is significant: The {@code TargetSource} returned from the first matching + * {@code TargetSourceCreator} (that is, the first that returns non-null) will be used. + */ + public void setCustomTargetSourceCreators(TargetSourceCreator... targetSourceCreators) { + this.customTargetSourceCreators = targetSourceCreators; + } + + /** + * Set the common interceptors. These must be bean names in the current factory. + * They can be of any advice or advisor type Spring supports. + *

If this property isn't set, there will be zero common interceptors. + * This is perfectly valid, if "specific" interceptors such as matching + * Advisors are all we want. + */ + public void setInterceptorNames(String... interceptorNames) { + this.interceptorNames = interceptorNames; + } + + /** + * Set whether the common interceptors should be applied before bean-specific ones. + * Default is "true"; else, bean-specific interceptors will get applied first. + */ + public void setApplyCommonInterceptorsFirst(boolean applyCommonInterceptorsFirst) { + this.applyCommonInterceptorsFirst = applyCommonInterceptorsFirst; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + /** + * Return the owning {@link BeanFactory}. + * May be {@code null}, as this post-processor doesn't need to belong to a bean factory. + */ + @Nullable + protected BeanFactory getBeanFactory() { + return this.beanFactory; + } + + + @Override + @Nullable + public Class predictBeanType(Class beanClass, String beanName) { + if (this.proxyTypes.isEmpty()) { + return null; + } + Object cacheKey = getCacheKey(beanClass, beanName); + return this.proxyTypes.get(cacheKey); + } + + @Override + @Nullable + public Constructor[] determineCandidateConstructors(Class beanClass, String beanName) { + return null; + } + + @Override + public Object getEarlyBeanReference(Object bean, String beanName) { + Object cacheKey = getCacheKey(bean.getClass(), beanName); + this.earlyProxyReferences.put(cacheKey, bean); + return wrapIfNecessary(bean, beanName, cacheKey); + } + + @Override + public Object postProcessBeforeInstantiation(Class beanClass, String beanName) { + Object cacheKey = getCacheKey(beanClass, beanName); + + if (!StringUtils.hasLength(beanName) || !this.targetSourcedBeans.contains(beanName)) { + if (this.advisedBeans.containsKey(cacheKey)) { + return null; + } + if (isInfrastructureClass(beanClass) || shouldSkip(beanClass, beanName)) { + this.advisedBeans.put(cacheKey, Boolean.FALSE); + return null; + } + } + + // Create proxy here if we have a custom TargetSource. + // Suppresses unnecessary default instantiation of the target bean: + // The TargetSource will handle target instances in a custom fashion. + TargetSource targetSource = getCustomTargetSource(beanClass, beanName); + if (targetSource != null) { + if (StringUtils.hasLength(beanName)) { + this.targetSourcedBeans.add(beanName); + } + Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource); + Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource); + this.proxyTypes.put(cacheKey, proxy.getClass()); + return proxy; + } + + return null; + } + + @Override + public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) { + return pvs; // skip postProcessPropertyValues + } + + /** + * Create a proxy with the configured interceptors if the bean is + * identified as one to proxy by the subclass. + * @see #getAdvicesAndAdvisorsForBean + */ + @Override + public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) { + if (bean != null) { + Object cacheKey = getCacheKey(bean.getClass(), beanName); + if (this.earlyProxyReferences.remove(cacheKey) != bean) { + return wrapIfNecessary(bean, beanName, cacheKey); + } + } + return bean; + } + + + /** + * Build a cache key for the given bean class and bean name. + *

Note: As of 4.2.3, this implementation does not return a concatenated + * class/name String anymore but rather the most efficient cache key possible: + * a plain bean name, prepended with {@link BeanFactory#FACTORY_BEAN_PREFIX} + * in case of a {@code FactoryBean}; or if no bean name specified, then the + * given bean {@code Class} as-is. + * @param beanClass the bean class + * @param beanName the bean name + * @return the cache key for the given class and name + */ + protected Object getCacheKey(Class beanClass, @Nullable String beanName) { + if (StringUtils.hasLength(beanName)) { + return (FactoryBean.class.isAssignableFrom(beanClass) ? + BeanFactory.FACTORY_BEAN_PREFIX + beanName : beanName); + } + else { + return beanClass; + } + } + + /** + * Wrap the given bean if necessary, i.e. if it is eligible for being proxied. + * @param bean the raw bean instance + * @param beanName the name of the bean + * @param cacheKey the cache key for metadata access + * @return a proxy wrapping the bean, or the raw bean instance as-is + */ + protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { + if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) { + return bean; + } + if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) { + return bean; + } + if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) { + this.advisedBeans.put(cacheKey, Boolean.FALSE); + return bean; + } + + // Create proxy if we have advice. + Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null); + if (specificInterceptors != DO_NOT_PROXY) { + this.advisedBeans.put(cacheKey, Boolean.TRUE); + Object proxy = createProxy( + bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)); + this.proxyTypes.put(cacheKey, proxy.getClass()); + return proxy; + } + + this.advisedBeans.put(cacheKey, Boolean.FALSE); + return bean; + } + + /** + * Return whether the given bean class represents an infrastructure class + * that should never be proxied. + *

The default implementation considers Advices, Advisors and + * AopInfrastructureBeans as infrastructure classes. + * @param beanClass the class of the bean + * @return whether the bean represents an infrastructure class + * @see org.aopalliance.aop.Advice + * @see org.springframework.aop.Advisor + * @see org.springframework.aop.framework.AopInfrastructureBean + * @see #shouldSkip + */ + protected boolean isInfrastructureClass(Class beanClass) { + boolean retVal = Advice.class.isAssignableFrom(beanClass) || + Pointcut.class.isAssignableFrom(beanClass) || + Advisor.class.isAssignableFrom(beanClass) || + AopInfrastructureBean.class.isAssignableFrom(beanClass); + if (retVal && logger.isTraceEnabled()) { + logger.trace("Did not attempt to auto-proxy infrastructure class [" + beanClass.getName() + "]"); + } + return retVal; + } + + /** + * Subclasses should override this method to return {@code true} if the + * given bean should not be considered for auto-proxying by this post-processor. + *

Sometimes we need to be able to avoid this happening, e.g. if it will lead to + * a circular reference or if the existing target instance needs to be preserved. + * This implementation returns {@code false} unless the bean name indicates an + * "original instance" according to {@code AutowireCapableBeanFactory} conventions. + * @param beanClass the class of the bean + * @param beanName the name of the bean + * @return whether to skip the given bean + * @see org.springframework.beans.factory.config.AutowireCapableBeanFactory#ORIGINAL_INSTANCE_SUFFIX + */ + protected boolean shouldSkip(Class beanClass, String beanName) { + return AutoProxyUtils.isOriginalInstance(beanName, beanClass); + } + + /** + * Create a target source for bean instances. Uses any TargetSourceCreators if set. + * Returns {@code null} if no custom TargetSource should be used. + *

This implementation uses the "customTargetSourceCreators" property. + * Subclasses can override this method to use a different mechanism. + * @param beanClass the class of the bean to create a TargetSource for + * @param beanName the name of the bean + * @return a TargetSource for this bean + * @see #setCustomTargetSourceCreators + */ + @Nullable + protected TargetSource getCustomTargetSource(Class beanClass, String beanName) { + // We can't create fancy target sources for directly registered singletons. + if (this.customTargetSourceCreators != null && + this.beanFactory != null && this.beanFactory.containsBean(beanName)) { + for (TargetSourceCreator tsc : this.customTargetSourceCreators) { + TargetSource ts = tsc.getTargetSource(beanClass, beanName); + if (ts != null) { + // Found a matching TargetSource. + if (logger.isTraceEnabled()) { + logger.trace("TargetSourceCreator [" + tsc + + "] found custom TargetSource for bean with name '" + beanName + "'"); + } + return ts; + } + } + } + + // No custom TargetSource found. + return null; + } + + /** + * Create an AOP proxy for the given bean. + * @param beanClass the class of the bean + * @param beanName the name of the bean + * @param specificInterceptors the set of interceptors that is + * specific to this bean (may be empty, but not null) + * @param targetSource the TargetSource for the proxy, + * already pre-configured to access the bean + * @return the AOP proxy for the bean + * @see #buildAdvisors + */ + protected Object createProxy(Class beanClass, @Nullable String beanName, + @Nullable Object[] specificInterceptors, TargetSource targetSource) { + + if (this.beanFactory instanceof ConfigurableListableBeanFactory) { + AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass); + } + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.copyFrom(this); + + if (!proxyFactory.isProxyTargetClass()) { + if (shouldProxyTargetClass(beanClass, beanName)) { + proxyFactory.setProxyTargetClass(true); + } + else { + evaluateProxyInterfaces(beanClass, proxyFactory); + } + } + + Advisor[] advisors = buildAdvisors(beanName, specificInterceptors); + proxyFactory.addAdvisors(advisors); + proxyFactory.setTargetSource(targetSource); + customizeProxyFactory(proxyFactory); + + proxyFactory.setFrozen(this.freezeProxy); + if (advisorsPreFiltered()) { + proxyFactory.setPreFiltered(true); + } + + return proxyFactory.getProxy(getProxyClassLoader()); + } + + /** + * Determine whether the given bean should be proxied with its target class rather than its interfaces. + *

Checks the {@link AutoProxyUtils#PRESERVE_TARGET_CLASS_ATTRIBUTE "preserveTargetClass" attribute} + * of the corresponding bean definition. + * @param beanClass the class of the bean + * @param beanName the name of the bean + * @return whether the given bean should be proxied with its target class + * @see AutoProxyUtils#shouldProxyTargetClass + */ + protected boolean shouldProxyTargetClass(Class beanClass, @Nullable String beanName) { + return (this.beanFactory instanceof ConfigurableListableBeanFactory && + AutoProxyUtils.shouldProxyTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName)); + } + + /** + * Return whether the Advisors returned by the subclass are pre-filtered + * to match the bean's target class already, allowing the ClassFilter check + * to be skipped when building advisors chains for AOP invocations. + *

Default is {@code false}. Subclasses may override this if they + * will always return pre-filtered Advisors. + * @return whether the Advisors are pre-filtered + * @see #getAdvicesAndAdvisorsForBean + * @see org.springframework.aop.framework.Advised#setPreFiltered + */ + protected boolean advisorsPreFiltered() { + return false; + } + + /** + * Determine the advisors for the given bean, including the specific interceptors + * as well as the common interceptor, all adapted to the Advisor interface. + * @param beanName the name of the bean + * @param specificInterceptors the set of interceptors that is + * specific to this bean (may be empty, but not null) + * @return the list of Advisors for the given bean + */ + protected Advisor[] buildAdvisors(@Nullable String beanName, @Nullable Object[] specificInterceptors) { + // Handle prototypes correctly... + Advisor[] commonInterceptors = resolveInterceptorNames(); + + List allInterceptors = new ArrayList<>(); + if (specificInterceptors != null) { + allInterceptors.addAll(Arrays.asList(specificInterceptors)); + if (commonInterceptors.length > 0) { + if (this.applyCommonInterceptorsFirst) { + allInterceptors.addAll(0, Arrays.asList(commonInterceptors)); + } + else { + allInterceptors.addAll(Arrays.asList(commonInterceptors)); + } + } + } + if (logger.isTraceEnabled()) { + int nrOfCommonInterceptors = commonInterceptors.length; + int nrOfSpecificInterceptors = (specificInterceptors != null ? specificInterceptors.length : 0); + logger.trace("Creating implicit proxy for bean '" + beanName + "' with " + nrOfCommonInterceptors + + " common interceptors and " + nrOfSpecificInterceptors + " specific interceptors"); + } + + Advisor[] advisors = new Advisor[allInterceptors.size()]; + for (int i = 0; i < allInterceptors.size(); i++) { + advisors[i] = this.advisorAdapterRegistry.wrap(allInterceptors.get(i)); + } + return advisors; + } + + /** + * Resolves the specified interceptor names to Advisor objects. + * @see #setInterceptorNames + */ + private Advisor[] resolveInterceptorNames() { + BeanFactory bf = this.beanFactory; + ConfigurableBeanFactory cbf = (bf instanceof ConfigurableBeanFactory ? (ConfigurableBeanFactory) bf : null); + List advisors = new ArrayList<>(); + for (String beanName : this.interceptorNames) { + if (cbf == null || !cbf.isCurrentlyInCreation(beanName)) { + Assert.state(bf != null, "BeanFactory required for resolving interceptor names"); + Object next = bf.getBean(beanName); + advisors.add(this.advisorAdapterRegistry.wrap(next)); + } + } + return advisors.toArray(new Advisor[0]); + } + + /** + * Subclasses may choose to implement this: for example, + * to change the interfaces exposed. + *

The default implementation is empty. + * @param proxyFactory a ProxyFactory that is already configured with + * TargetSource and interfaces and will be used to create the proxy + * immediately after this method returns + */ + protected void customizeProxyFactory(ProxyFactory proxyFactory) { + } + + + /** + * Return whether the given bean is to be proxied, what additional + * advices (e.g. AOP Alliance interceptors) and advisors to apply. + * @param beanClass the class of the bean to advise + * @param beanName the name of the bean + * @param customTargetSource the TargetSource returned by the + * {@link #getCustomTargetSource} method: may be ignored. + * Will be {@code null} if no custom target source is in use. + * @return an array of additional interceptors for the particular bean; + * or an empty array if no additional interceptors but just the common ones; + * or {@code null} if no proxy at all, not even with the common interceptors. + * See constants DO_NOT_PROXY and PROXY_WITHOUT_ADDITIONAL_INTERCEPTORS. + * @throws BeansException in case of errors + * @see #DO_NOT_PROXY + * @see #PROXY_WITHOUT_ADDITIONAL_INTERCEPTORS + */ + @Nullable + protected abstract Object[] getAdvicesAndAdvisorsForBean(Class beanClass, String beanName, + @Nullable TargetSource customTargetSource) throws BeansException; + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractBeanFactoryAwareAdvisingPostProcessor.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractBeanFactoryAwareAdvisingPostProcessor.java new file mode 100644 index 0000000..280eb9b --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractBeanFactoryAwareAdvisingPostProcessor.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.autoproxy; + +import org.springframework.aop.framework.AbstractAdvisingBeanPostProcessor; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.lang.Nullable; + +/** + * Extension of {@link AbstractAutoProxyCreator} which implements {@link BeanFactoryAware}, + * adds exposure of the original target class for each proxied bean + * ({@link AutoProxyUtils#ORIGINAL_TARGET_CLASS_ATTRIBUTE}), + * and participates in an externally enforced target-class mode for any given bean + * ({@link AutoProxyUtils#PRESERVE_TARGET_CLASS_ATTRIBUTE}). + * This post-processor is therefore aligned with {@link AbstractAutoProxyCreator}. + * + * @author Juergen Hoeller + * @since 4.2.3 + * @see AutoProxyUtils#shouldProxyTargetClass + * @see AutoProxyUtils#determineTargetClass + */ +@SuppressWarnings("serial") +public abstract class AbstractBeanFactoryAwareAdvisingPostProcessor extends AbstractAdvisingBeanPostProcessor + implements BeanFactoryAware { + + @Nullable + private ConfigurableListableBeanFactory beanFactory; + + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = (beanFactory instanceof ConfigurableListableBeanFactory ? + (ConfigurableListableBeanFactory) beanFactory : null); + } + + @Override + protected ProxyFactory prepareProxyFactory(Object bean, String beanName) { + if (this.beanFactory != null) { + AutoProxyUtils.exposeTargetClass(this.beanFactory, beanName, bean.getClass()); + } + + ProxyFactory proxyFactory = super.prepareProxyFactory(bean, beanName); + if (!proxyFactory.isProxyTargetClass() && this.beanFactory != null && + AutoProxyUtils.shouldProxyTargetClass(this.beanFactory, beanName)) { + proxyFactory.setProxyTargetClass(true); + } + return proxyFactory; + } + + @Override + protected boolean isEligible(Object bean, String beanName) { + return (!AutoProxyUtils.isOriginalInstance(beanName, bean.getClass()) && + super.isEligible(bean, beanName)); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AutoProxyUtils.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AutoProxyUtils.java new file mode 100644 index 0000000..1de9382 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AutoProxyUtils.java @@ -0,0 +1,137 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.autoproxy; + +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.Conventions; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Utilities for auto-proxy aware components. + * Mainly for internal use within the framework. + * + * @author Juergen Hoeller + * @since 2.0.3 + * @see AbstractAutoProxyCreator + */ +public abstract class AutoProxyUtils { + + /** + * Bean definition attribute that may indicate whether a given bean is supposed + * to be proxied with its target class (in case of it getting proxied in the first + * place). The value is {@code Boolean.TRUE} or {@code Boolean.FALSE}. + *

Proxy factories can set this attribute if they built a target class proxy + * for a specific bean, and want to enforce that bean can always be cast + * to its target class (even if AOP advices get applied through auto-proxying). + * @see #shouldProxyTargetClass + */ + public static final String PRESERVE_TARGET_CLASS_ATTRIBUTE = + Conventions.getQualifiedAttributeName(AutoProxyUtils.class, "preserveTargetClass"); + + /** + * Bean definition attribute that indicates the original target class of an + * auto-proxied bean, e.g. to be used for the introspection of annotations + * on the target class behind an interface-based proxy. + * @since 4.2.3 + * @see #determineTargetClass + */ + public static final String ORIGINAL_TARGET_CLASS_ATTRIBUTE = + Conventions.getQualifiedAttributeName(AutoProxyUtils.class, "originalTargetClass"); + + + /** + * Determine whether the given bean should be proxied with its target + * class rather than its interfaces. Checks the + * {@link #PRESERVE_TARGET_CLASS_ATTRIBUTE "preserveTargetClass" attribute} + * of the corresponding bean definition. + * @param beanFactory the containing ConfigurableListableBeanFactory + * @param beanName the name of the bean + * @return whether the given bean should be proxied with its target class + */ + public static boolean shouldProxyTargetClass( + ConfigurableListableBeanFactory beanFactory, @Nullable String beanName) { + + if (beanName != null && beanFactory.containsBeanDefinition(beanName)) { + BeanDefinition bd = beanFactory.getBeanDefinition(beanName); + return Boolean.TRUE.equals(bd.getAttribute(PRESERVE_TARGET_CLASS_ATTRIBUTE)); + } + return false; + } + + /** + * Determine the original target class for the specified bean, if possible, + * otherwise falling back to a regular {@code getType} lookup. + * @param beanFactory the containing ConfigurableListableBeanFactory + * @param beanName the name of the bean + * @return the original target class as stored in the bean definition, if any + * @since 4.2.3 + * @see org.springframework.beans.factory.BeanFactory#getType(String) + */ + @Nullable + public static Class determineTargetClass( + ConfigurableListableBeanFactory beanFactory, @Nullable String beanName) { + + if (beanName == null) { + return null; + } + if (beanFactory.containsBeanDefinition(beanName)) { + BeanDefinition bd = beanFactory.getMergedBeanDefinition(beanName); + Class targetClass = (Class) bd.getAttribute(ORIGINAL_TARGET_CLASS_ATTRIBUTE); + if (targetClass != null) { + return targetClass; + } + } + return beanFactory.getType(beanName); + } + + /** + * Expose the given target class for the specified bean, if possible. + * @param beanFactory the containing ConfigurableListableBeanFactory + * @param beanName the name of the bean + * @param targetClass the corresponding target class + * @since 4.2.3 + */ + static void exposeTargetClass( + ConfigurableListableBeanFactory beanFactory, @Nullable String beanName, Class targetClass) { + + if (beanName != null && beanFactory.containsBeanDefinition(beanName)) { + beanFactory.getMergedBeanDefinition(beanName).setAttribute(ORIGINAL_TARGET_CLASS_ATTRIBUTE, targetClass); + } + } + + /** + * Determine whether the given bean name indicates an "original instance" + * according to {@link AutowireCapableBeanFactory#ORIGINAL_INSTANCE_SUFFIX}, + * skipping any proxy attempts for it. + * @param beanName the name of the bean + * @param beanClass the corresponding bean class + * @since 5.1 + * @see AutowireCapableBeanFactory#ORIGINAL_INSTANCE_SUFFIX + */ + static boolean isOriginalInstance(String beanName, Class beanClass) { + if (!StringUtils.hasLength(beanName) || beanName.length() != + beanClass.getName().length() + AutowireCapableBeanFactory.ORIGINAL_INSTANCE_SUFFIX.length()) { + return false; + } + return (beanName.startsWith(beanClass.getName()) && + beanName.endsWith(AutowireCapableBeanFactory.ORIGINAL_INSTANCE_SUFFIX)); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/BeanFactoryAdvisorRetrievalHelper.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/BeanFactoryAdvisorRetrievalHelper.java new file mode 100644 index 0000000..4542f11 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/BeanFactoryAdvisorRetrievalHelper.java @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.autoproxy; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aop.Advisor; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanCurrentlyInCreationException; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Helper for retrieving standard Spring Advisors from a BeanFactory, + * for use with auto-proxying. + * + * @author Juergen Hoeller + * @since 2.0.2 + * @see AbstractAdvisorAutoProxyCreator + */ +public class BeanFactoryAdvisorRetrievalHelper { + + private static final Log logger = LogFactory.getLog(BeanFactoryAdvisorRetrievalHelper.class); + + private final ConfigurableListableBeanFactory beanFactory; + + @Nullable + private volatile String[] cachedAdvisorBeanNames; + + + /** + * Create a new BeanFactoryAdvisorRetrievalHelper for the given BeanFactory. + * @param beanFactory the ListableBeanFactory to scan + */ + public BeanFactoryAdvisorRetrievalHelper(ConfigurableListableBeanFactory beanFactory) { + Assert.notNull(beanFactory, "ListableBeanFactory must not be null"); + this.beanFactory = beanFactory; + } + + + /** + * Find all eligible Advisor beans in the current bean factory, + * ignoring FactoryBeans and excluding beans that are currently in creation. + * @return the list of {@link org.springframework.aop.Advisor} beans + * @see #isEligibleBean + */ + public List findAdvisorBeans() { + // Determine list of advisor bean names, if not cached already. + String[] advisorNames = this.cachedAdvisorBeanNames; + if (advisorNames == null) { + // Do not initialize FactoryBeans here: We need to leave all regular beans + // uninitialized to let the auto-proxy creator apply to them! + advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( + this.beanFactory, Advisor.class, true, false); + this.cachedAdvisorBeanNames = advisorNames; + } + if (advisorNames.length == 0) { + return new ArrayList<>(); + } + + List advisors = new ArrayList<>(); + for (String name : advisorNames) { + if (isEligibleBean(name)) { + if (this.beanFactory.isCurrentlyInCreation(name)) { + if (logger.isTraceEnabled()) { + logger.trace("Skipping currently created advisor '" + name + "'"); + } + } + else { + try { + advisors.add(this.beanFactory.getBean(name, Advisor.class)); + } + catch (BeanCreationException ex) { + Throwable rootCause = ex.getMostSpecificCause(); + if (rootCause instanceof BeanCurrentlyInCreationException) { + BeanCreationException bce = (BeanCreationException) rootCause; + String bceBeanName = bce.getBeanName(); + if (bceBeanName != null && this.beanFactory.isCurrentlyInCreation(bceBeanName)) { + if (logger.isTraceEnabled()) { + logger.trace("Skipping advisor '" + name + + "' with dependency on currently created bean: " + ex.getMessage()); + } + // Ignore: indicates a reference back to the bean we're trying to advise. + // We want to find advisors other than the currently created bean itself. + continue; + } + } + throw ex; + } + } + } + } + return advisors; + } + + /** + * Determine whether the aspect bean with the given name is eligible. + *

The default implementation always returns {@code true}. + * @param beanName the name of the aspect bean + * @return whether the bean is eligible + */ + protected boolean isEligibleBean(String beanName) { + return true; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreator.java new file mode 100644 index 0000000..8c49d4f --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreator.java @@ -0,0 +1,153 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.autoproxy; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.aop.TargetSource; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.PatternMatchUtils; +import org.springframework.util.StringUtils; + +/** + * Auto proxy creator that identifies beans to proxy via a list of names. + * Checks for direct, "xxx*", and "*xxx" matches. + * + *

For configuration details, see the javadoc of the parent class + * AbstractAutoProxyCreator. Typically, you will specify a list of + * interceptor names to apply to all identified beans, via the + * "interceptorNames" property. + * + * @author Juergen Hoeller + * @author Sam Brannen + * @since 10.10.2003 + * @see #setBeanNames + * @see #isMatch + * @see #setInterceptorNames + * @see AbstractAutoProxyCreator + */ +@SuppressWarnings("serial") +public class BeanNameAutoProxyCreator extends AbstractAutoProxyCreator { + + private static final String[] NO_ALIASES = new String[0]; + + @Nullable + private List beanNames; + + + /** + * Set the names of the beans that should automatically get wrapped with proxies. + * A name can specify a prefix to match by ending with "*", e.g. "myBean,tx*" + * will match the bean named "myBean" and all beans whose name start with "tx". + *

NOTE: In case of a FactoryBean, only the objects created by the + * FactoryBean will get proxied. This default behavior applies as of Spring 2.0. + * If you intend to proxy a FactoryBean instance itself (a rare use case, but + * Spring 1.2's default behavior), specify the bean name of the FactoryBean + * including the factory-bean prefix "&": e.g. "&myFactoryBean". + * @see org.springframework.beans.factory.FactoryBean + * @see org.springframework.beans.factory.BeanFactory#FACTORY_BEAN_PREFIX + */ + public void setBeanNames(String... beanNames) { + Assert.notEmpty(beanNames, "'beanNames' must not be empty"); + this.beanNames = new ArrayList<>(beanNames.length); + for (String mappedName : beanNames) { + this.beanNames.add(StringUtils.trimWhitespace(mappedName)); + } + } + + + /** + * Delegate to {@link AbstractAutoProxyCreator#getCustomTargetSource(Class, String)} + * if the bean name matches one of the names in the configured list of supported + * names, returning {@code null} otherwise. + * @since 5.3 + * @see #setBeanNames(String...) + */ + @Override + protected TargetSource getCustomTargetSource(Class beanClass, String beanName) { + return (isSupportedBeanName(beanClass, beanName) ? + super.getCustomTargetSource(beanClass, beanName) : null); + } + + /** + * Identify as a bean to proxy if the bean name matches one of the names in + * the configured list of supported names. + * @see #setBeanNames(String...) + */ + @Override + @Nullable + protected Object[] getAdvicesAndAdvisorsForBean( + Class beanClass, String beanName, @Nullable TargetSource targetSource) { + + return (isSupportedBeanName(beanClass, beanName) ? + PROXY_WITHOUT_ADDITIONAL_INTERCEPTORS : DO_NOT_PROXY); + } + + /** + * Determine if the bean name for the given bean class matches one of the names + * in the configured list of supported names. + * @param beanClass the class of the bean to advise + * @param beanName the name of the bean + * @return {@code true} if the given bean name is supported + * @see #setBeanNames(String...) + */ + private boolean isSupportedBeanName(Class beanClass, String beanName) { + if (this.beanNames != null) { + boolean isFactoryBean = FactoryBean.class.isAssignableFrom(beanClass); + for (String mappedName : this.beanNames) { + if (isFactoryBean) { + if (!mappedName.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)) { + continue; + } + mappedName = mappedName.substring(BeanFactory.FACTORY_BEAN_PREFIX.length()); + } + if (isMatch(beanName, mappedName)) { + return true; + } + } + + BeanFactory beanFactory = getBeanFactory(); + String[] aliases = (beanFactory != null ? beanFactory.getAliases(beanName) : NO_ALIASES); + for (String alias : aliases) { + for (String mappedName : this.beanNames) { + if (isMatch(alias, mappedName)) { + return true; + } + } + } + } + return false; + } + + /** + * Determine if the given bean name matches the mapped name. + *

The default implementation checks for "xxx*", "*xxx" and "*xxx*" matches, + * as well as direct equality. Can be overridden in subclasses. + * @param beanName the bean name to check + * @param mappedName the name in the configured list of names + * @return if the names match + * @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String) + */ + protected boolean isMatch(String beanName, String mappedName) { + return PatternMatchUtils.simpleMatch(mappedName, beanName); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/DefaultAdvisorAutoProxyCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/DefaultAdvisorAutoProxyCreator.java new file mode 100644 index 0000000..07aff4a --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/DefaultAdvisorAutoProxyCreator.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.autoproxy; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.lang.Nullable; + +/** + * {@code BeanPostProcessor} implementation that creates AOP proxies based on all + * candidate {@code Advisor}s in the current {@code BeanFactory}. This class is + * completely generic; it contains no special code to handle any particular aspects, + * such as pooling aspects. + * + *

It's possible to filter out advisors - for example, to use multiple post processors + * of this type in the same factory - by setting the {@code usePrefix} property to true, + * in which case only advisors beginning with the DefaultAdvisorAutoProxyCreator's bean + * name followed by a dot (like "aapc.") will be used. This default prefix can be changed + * from the bean name by setting the {@code advisorBeanNamePrefix} property. + * The separator (.) will also be used in this case. + * + * @author Rod Johnson + * @author Rob Harrop + */ +@SuppressWarnings("serial") +public class DefaultAdvisorAutoProxyCreator extends AbstractAdvisorAutoProxyCreator implements BeanNameAware { + + /** Separator between prefix and remainder of bean name. */ + public static final String SEPARATOR = "."; + + + private boolean usePrefix = false; + + @Nullable + private String advisorBeanNamePrefix; + + + /** + * Set whether to only include advisors with a certain prefix in the bean name. + *

Default is {@code false}, including all beans of type {@code Advisor}. + * @see #setAdvisorBeanNamePrefix + */ + public void setUsePrefix(boolean usePrefix) { + this.usePrefix = usePrefix; + } + + /** + * Return whether to only include advisors with a certain prefix in the bean name. + */ + public boolean isUsePrefix() { + return this.usePrefix; + } + + /** + * Set the prefix for bean names that will cause them to be included for + * auto-proxying by this object. This prefix should be set to avoid circular + * references. Default value is the bean name of this object + a dot. + * @param advisorBeanNamePrefix the exclusion prefix + */ + public void setAdvisorBeanNamePrefix(@Nullable String advisorBeanNamePrefix) { + this.advisorBeanNamePrefix = advisorBeanNamePrefix; + } + + /** + * Return the prefix for bean names that will cause them to be included + * for auto-proxying by this object. + */ + @Nullable + public String getAdvisorBeanNamePrefix() { + return this.advisorBeanNamePrefix; + } + + @Override + public void setBeanName(String name) { + // If no infrastructure bean name prefix has been set, override it. + if (this.advisorBeanNamePrefix == null) { + this.advisorBeanNamePrefix = name + SEPARATOR; + } + } + + + /** + * Consider {@code Advisor} beans with the specified prefix as eligible, if activated. + * @see #setUsePrefix + * @see #setAdvisorBeanNamePrefix + */ + @Override + protected boolean isEligibleAdvisorBean(String beanName) { + if (!isUsePrefix()) { + return true; + } + String prefix = getAdvisorBeanNamePrefix(); + return (prefix != null && beanName.startsWith(prefix)); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/InfrastructureAdvisorAutoProxyCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/InfrastructureAdvisorAutoProxyCreator.java new file mode 100644 index 0000000..f283920 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/InfrastructureAdvisorAutoProxyCreator.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.autoproxy; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.lang.Nullable; + +/** + * Auto-proxy creator that considers infrastructure Advisor beans only, + * ignoring any application-defined Advisors. + * + * @author Juergen Hoeller + * @since 2.0.7 + */ +@SuppressWarnings("serial") +public class InfrastructureAdvisorAutoProxyCreator extends AbstractAdvisorAutoProxyCreator { + + @Nullable + private ConfigurableListableBeanFactory beanFactory; + + + @Override + protected void initBeanFactory(ConfigurableListableBeanFactory beanFactory) { + super.initBeanFactory(beanFactory); + this.beanFactory = beanFactory; + } + + @Override + protected boolean isEligibleAdvisorBean(String beanName) { + return (this.beanFactory != null && this.beanFactory.containsBeanDefinition(beanName) && + this.beanFactory.getBeanDefinition(beanName).getRole() == BeanDefinition.ROLE_INFRASTRUCTURE); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/ProxyCreationContext.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/ProxyCreationContext.java new file mode 100644 index 0000000..314fcc9 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/ProxyCreationContext.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.autoproxy; + +import org.springframework.core.NamedThreadLocal; +import org.springframework.lang.Nullable; + +/** + * Holder for the current proxy creation context, as exposed by auto-proxy creators + * such as {@link AbstractAdvisorAutoProxyCreator}. + * + * @author Juergen Hoeller + * @author Ramnivas Laddad + * @since 2.5 + */ +public final class ProxyCreationContext { + + /** ThreadLocal holding the current proxied bean name during Advisor matching. */ + private static final ThreadLocal currentProxiedBeanName = + new NamedThreadLocal<>("Name of currently proxied bean"); + + + private ProxyCreationContext() { + } + + + /** + * Return the name of the currently proxied bean instance. + * @return the name of the bean, or {@code null} if none available + */ + @Nullable + public static String getCurrentProxiedBeanName() { + return currentProxiedBeanName.get(); + } + + /** + * Set the name of the currently proxied bean instance. + * @param beanName the name of the bean, or {@code null} to reset it + */ + static void setCurrentProxiedBeanName(@Nullable String beanName) { + if (beanName != null) { + currentProxiedBeanName.set(beanName); + } + else { + currentProxiedBeanName.remove(); + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/TargetSourceCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/TargetSourceCreator.java new file mode 100644 index 0000000..012c060 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/TargetSourceCreator.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.autoproxy; + +import org.springframework.aop.TargetSource; +import org.springframework.lang.Nullable; + +/** + * Implementations can create special target sources, such as pooling target + * sources, for particular beans. For example, they may base their choice + * on attributes, such as a pooling attribute, on the target class. + * + *

AbstractAutoProxyCreator can support a number of TargetSourceCreators, + * which will be applied in order. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +@FunctionalInterface +public interface TargetSourceCreator { + + /** + * Create a special TargetSource for the given bean, if any. + * @param beanClass the class of the bean to create a TargetSource for + * @param beanName the name of the bean + * @return a special TargetSource or {@code null} if this TargetSourceCreator isn't + * interested in the particular bean + */ + @Nullable + TargetSource getTargetSource(Class beanClass, String beanName); + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/package-info.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/package-info.java new file mode 100644 index 0000000..3283121 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/package-info.java @@ -0,0 +1,17 @@ +/** + * Bean post-processors for use in ApplicationContexts to simplify AOP usage + * by automatically creating AOP proxies without the need to use a ProxyFactoryBean. + * + *

The various post-processors in this package need only be added to an ApplicationContext + * (typically in an XML bean definition document) to automatically proxy selected beans. + * + *

NB: Automatic auto-proxying is not supported for BeanFactory implementations, + * as post-processors beans are only automatically detected in application contexts. + * Post-processors can be explicitly registered on a ConfigurableBeanFactory instead. + */ +@NonNullApi +@NonNullFields +package org.springframework.aop.framework.autoproxy; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/package-info.java b/spring-aop/src/main/java/org/springframework/aop/framework/package-info.java new file mode 100644 index 0000000..c05af5d --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/framework/package-info.java @@ -0,0 +1,20 @@ +/** + * Package containing Spring's basic AOP infrastructure, compliant with the + * AOP Alliance interfaces. + * + *

Spring AOP supports proxying interfaces or classes, introductions, and offers + * static and dynamic pointcuts. + * + *

Any Spring AOP proxy can be cast to the ProxyConfig AOP configuration interface + * in this package to add or remove interceptors. + * + *

The ProxyFactoryBean is a convenient way to create AOP proxies in a BeanFactory + * or ApplicationContext. However, proxies can be created programmatically using the + * ProxyFactory class. + */ +@NonNullApi +@NonNullFields +package org.springframework.aop.framework; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/AbstractMonitoringInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/AbstractMonitoringInterceptor.java new file mode 100644 index 0000000..536e6e3 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/AbstractMonitoringInterceptor.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.interceptor; + +import java.lang.reflect.Method; + +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.lang.Nullable; + +/** + * Base class for monitoring interceptors, such as performance monitors. + * Provides configurable "prefix and "suffix" properties that help to + * classify/group performance monitoring results. + * + *

In their {@link #invokeUnderTrace} implementation, subclasses should call the + * {@link #createInvocationTraceName} method to create a name for the given trace, + * including information about the method invocation along with a prefix/suffix. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2.7 + * @see #setPrefix + * @see #setSuffix + * @see #createInvocationTraceName + */ +@SuppressWarnings("serial") +public abstract class AbstractMonitoringInterceptor extends AbstractTraceInterceptor { + + private String prefix = ""; + + private String suffix = ""; + + private boolean logTargetClassInvocation = false; + + + /** + * Set the text that will get appended to the trace data. + *

Default is none. + */ + public void setPrefix(@Nullable String prefix) { + this.prefix = (prefix != null ? prefix : ""); + } + + /** + * Return the text that will get appended to the trace data. + */ + protected String getPrefix() { + return this.prefix; + } + + /** + * Set the text that will get prepended to the trace data. + *

Default is none. + */ + public void setSuffix(@Nullable String suffix) { + this.suffix = (suffix != null ? suffix : ""); + } + + /** + * Return the text that will get prepended to the trace data. + */ + protected String getSuffix() { + return this.suffix; + } + + /** + * Set whether to log the invocation on the target class, if applicable + * (i.e. if the method is actually delegated to the target class). + *

Default is "false", logging the invocation based on the proxy + * interface/class name. + */ + public void setLogTargetClassInvocation(boolean logTargetClassInvocation) { + this.logTargetClassInvocation = logTargetClassInvocation; + } + + + /** + * Create a {@code String} name for the given {@code MethodInvocation} + * that can be used for trace/logging purposes. This name is made up of the + * configured prefix, followed by the fully-qualified name of the method being + * invoked, followed by the configured suffix. + * @see #setPrefix + * @see #setSuffix + */ + protected String createInvocationTraceName(MethodInvocation invocation) { + Method method = invocation.getMethod(); + Class clazz = method.getDeclaringClass(); + if (this.logTargetClassInvocation && clazz.isInstance(invocation.getThis())) { + clazz = invocation.getThis().getClass(); + } + String className = clazz.getName(); + return getPrefix() + className + '.' + method.getName() + getSuffix(); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/AbstractTraceInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/AbstractTraceInterceptor.java new file mode 100644 index 0000000..892cf5c --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/AbstractTraceInterceptor.java @@ -0,0 +1,251 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.interceptor; + +import java.io.Serializable; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aop.support.AopUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Base {@code MethodInterceptor} implementation for tracing. + * + *

By default, log messages are written to the log for the interceptor class, + * not the class which is being intercepted. Setting the {@code useDynamicLogger} + * bean property to {@code true} causes all log messages to be written to + * the {@code Log} for the target class being intercepted. + * + *

Subclasses must implement the {@code invokeUnderTrace} method, which + * is invoked by this class ONLY when a particular invocation SHOULD be traced. + * Subclasses should write to the {@code Log} instance provided. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see #setUseDynamicLogger + * @see #invokeUnderTrace(org.aopalliance.intercept.MethodInvocation, org.apache.commons.logging.Log) + */ +@SuppressWarnings("serial") +public abstract class AbstractTraceInterceptor implements MethodInterceptor, Serializable { + + /** + * The default {@code Log} instance used to write trace messages. + * This instance is mapped to the implementing {@code Class}. + */ + @Nullable + protected transient Log defaultLogger = LogFactory.getLog(getClass()); + + /** + * Indicates whether or not proxy class names should be hidden when using dynamic loggers. + * @see #setUseDynamicLogger + */ + private boolean hideProxyClassNames = false; + + /** + * Indicates whether to pass an exception to the logger. + * @see #writeToLog(Log, String, Throwable) + */ + private boolean logExceptionStackTrace = true; + + + /** + * Set whether to use a dynamic logger or a static logger. + * Default is a static logger for this trace interceptor. + *

Used to determine which {@code Log} instance should be used to write + * log messages for a particular method invocation: a dynamic one for the + * {@code Class} getting called, or a static one for the {@code Class} + * of the trace interceptor. + *

NOTE: Specify either this property or "loggerName", not both. + * @see #getLoggerForInvocation(org.aopalliance.intercept.MethodInvocation) + */ + public void setUseDynamicLogger(boolean useDynamicLogger) { + // Release default logger if it is not being used. + this.defaultLogger = (useDynamicLogger ? null : LogFactory.getLog(getClass())); + } + + /** + * Set the name of the logger to use. The name will be passed to the + * underlying logger implementation through Commons Logging, getting + * interpreted as log category according to the logger's configuration. + *

This can be specified to not log into the category of a class + * (whether this interceptor's class or the class getting called) + * but rather into a specific named category. + *

NOTE: Specify either this property or "useDynamicLogger", not both. + * @see org.apache.commons.logging.LogFactory#getLog(String) + * @see java.util.logging.Logger#getLogger(String) + */ + public void setLoggerName(String loggerName) { + this.defaultLogger = LogFactory.getLog(loggerName); + } + + /** + * Set to "true" to have {@link #setUseDynamicLogger dynamic loggers} hide + * proxy class names wherever possible. Default is "false". + */ + public void setHideProxyClassNames(boolean hideProxyClassNames) { + this.hideProxyClassNames = hideProxyClassNames; + } + + /** + * Set whether to pass an exception to the logger, suggesting inclusion + * of its stack trace into the log. Default is "true"; set this to "false" + * in order to reduce the log output to just the trace message (which may + * include the exception class name and exception message, if applicable). + * @since 4.3.10 + */ + public void setLogExceptionStackTrace(boolean logExceptionStackTrace) { + this.logExceptionStackTrace = logExceptionStackTrace; + } + + + /** + * Determines whether or not logging is enabled for the particular {@code MethodInvocation}. + * If not, the method invocation proceeds as normal, otherwise the method invocation is passed + * to the {@code invokeUnderTrace} method for handling. + * @see #invokeUnderTrace(org.aopalliance.intercept.MethodInvocation, org.apache.commons.logging.Log) + */ + @Override + @Nullable + public Object invoke(MethodInvocation invocation) throws Throwable { + Log logger = getLoggerForInvocation(invocation); + if (isInterceptorEnabled(invocation, logger)) { + return invokeUnderTrace(invocation, logger); + } + else { + return invocation.proceed(); + } + } + + /** + * Return the appropriate {@code Log} instance to use for the given + * {@code MethodInvocation}. If the {@code useDynamicLogger} flag + * is set, the {@code Log} instance will be for the target class of the + * {@code MethodInvocation}, otherwise the {@code Log} will be the + * default static logger. + * @param invocation the {@code MethodInvocation} being traced + * @return the {@code Log} instance to use + * @see #setUseDynamicLogger + */ + protected Log getLoggerForInvocation(MethodInvocation invocation) { + if (this.defaultLogger != null) { + return this.defaultLogger; + } + else { + Object target = invocation.getThis(); + Assert.state(target != null, "Target must not be null"); + return LogFactory.getLog(getClassForLogging(target)); + } + } + + /** + * Determine the class to use for logging purposes. + * @param target the target object to introspect + * @return the target class for the given object + * @see #setHideProxyClassNames + */ + protected Class getClassForLogging(Object target) { + return (this.hideProxyClassNames ? AopUtils.getTargetClass(target) : target.getClass()); + } + + /** + * Determine whether the interceptor should kick in, that is, + * whether the {@code invokeUnderTrace} method should be called. + *

Default behavior is to check whether the given {@code Log} + * instance is enabled. Subclasses can override this to apply the + * interceptor in other cases as well. + * @param invocation the {@code MethodInvocation} being traced + * @param logger the {@code Log} instance to check + * @see #invokeUnderTrace + * @see #isLogEnabled + */ + protected boolean isInterceptorEnabled(MethodInvocation invocation, Log logger) { + return isLogEnabled(logger); + } + + /** + * Determine whether the given {@link Log} instance is enabled. + *

Default is {@code true} when the "trace" level is enabled. + * Subclasses can override this to change the level under which 'tracing' occurs. + * @param logger the {@code Log} instance to check + */ + protected boolean isLogEnabled(Log logger) { + return logger.isTraceEnabled(); + } + + /** + * Write the supplied trace message to the supplied {@code Log} instance. + *

To be called by {@link #invokeUnderTrace} for enter/exit messages. + *

Delegates to {@link #writeToLog(Log, String, Throwable)} as the + * ultimate delegate that controls the underlying logger invocation. + * @since 4.3.10 + * @see #writeToLog(Log, String, Throwable) + */ + protected void writeToLog(Log logger, String message) { + writeToLog(logger, message, null); + } + + /** + * Write the supplied trace message and {@link Throwable} to the + * supplied {@code Log} instance. + *

To be called by {@link #invokeUnderTrace} for enter/exit outcomes, + * potentially including an exception. Note that an exception's stack trace + * won't get logged when {@link #setLogExceptionStackTrace} is "false". + *

By default messages are written at {@code TRACE} level. Subclasses + * can override this method to control which level the message is written + * at, typically also overriding {@link #isLogEnabled} accordingly. + * @since 4.3.10 + * @see #setLogExceptionStackTrace + * @see #isLogEnabled + */ + protected void writeToLog(Log logger, String message, @Nullable Throwable ex) { + if (ex != null && this.logExceptionStackTrace) { + logger.trace(message, ex); + } + else { + logger.trace(message); + } + } + + + /** + * Subclasses must override this method to perform any tracing around the + * supplied {@code MethodInvocation}. Subclasses are responsible for + * ensuring that the {@code MethodInvocation} actually executes by + * calling {@code MethodInvocation.proceed()}. + *

By default, the passed-in {@code Log} instance will have log level + * "trace" enabled. Subclasses do not have to check for this again, unless + * they overwrite the {@code isInterceptorEnabled} method to modify + * the default behavior, and may delegate to {@code writeToLog} for actual + * messages to be written. + * @param logger the {@code Log} to write trace messages to + * @return the result of the call to {@code MethodInvocation.proceed()} + * @throws Throwable if the call to {@code MethodInvocation.proceed()} + * encountered any errors + * @see #isLogEnabled + * @see #writeToLog(Log, String) + * @see #writeToLog(Log, String, Throwable) + */ + @Nullable + protected abstract Object invokeUnderTrace(MethodInvocation invocation, Log logger) throws Throwable; + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java new file mode 100644 index 0000000..8908cab --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java @@ -0,0 +1,323 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.interceptor; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.Future; +import java.util.function.Supplier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; +import org.springframework.core.task.AsyncListenableTaskExecutor; +import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.core.task.TaskExecutor; +import org.springframework.core.task.support.TaskExecutorAdapter; +import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.util.function.SingletonSupplier; + +/** + * Base class for asynchronous method execution aspects, such as + * {@code org.springframework.scheduling.annotation.AnnotationAsyncExecutionInterceptor} + * or {@code org.springframework.scheduling.aspectj.AnnotationAsyncExecutionAspect}. + * + *

Provides support for executor qualification on a method-by-method basis. + * {@code AsyncExecutionAspectSupport} objects must be constructed with a default {@code + * Executor}, but each individual method may further qualify a specific {@code Executor} + * bean to be used when executing it, e.g. through an annotation attribute. + * + * @author Chris Beams + * @author Juergen Hoeller + * @author Stephane Nicoll + * @since 3.1.2 + */ +public abstract class AsyncExecutionAspectSupport implements BeanFactoryAware { + + /** + * The default name of the {@link TaskExecutor} bean to pick up: "taskExecutor". + *

Note that the initial lookup happens by type; this is just the fallback + * in case of multiple executor beans found in the context. + * @since 4.2.6 + */ + public static final String DEFAULT_TASK_EXECUTOR_BEAN_NAME = "taskExecutor"; + + + protected final Log logger = LogFactory.getLog(getClass()); + + private final Map executors = new ConcurrentHashMap<>(16); + + private SingletonSupplier defaultExecutor; + + private SingletonSupplier exceptionHandler; + + @Nullable + private BeanFactory beanFactory; + + + /** + * Create a new instance with a default {@link AsyncUncaughtExceptionHandler}. + * @param defaultExecutor the {@code Executor} (typically a Spring {@code AsyncTaskExecutor} + * or {@link java.util.concurrent.ExecutorService}) to delegate to, unless a more specific + * executor has been requested via a qualifier on the async method, in which case the + * executor will be looked up at invocation time against the enclosing bean factory + */ + public AsyncExecutionAspectSupport(@Nullable Executor defaultExecutor) { + this.defaultExecutor = new SingletonSupplier<>(defaultExecutor, () -> getDefaultExecutor(this.beanFactory)); + this.exceptionHandler = SingletonSupplier.of(SimpleAsyncUncaughtExceptionHandler::new); + } + + /** + * Create a new {@link AsyncExecutionAspectSupport} with the given exception handler. + * @param defaultExecutor the {@code Executor} (typically a Spring {@code AsyncTaskExecutor} + * or {@link java.util.concurrent.ExecutorService}) to delegate to, unless a more specific + * executor has been requested via a qualifier on the async method, in which case the + * executor will be looked up at invocation time against the enclosing bean factory + * @param exceptionHandler the {@link AsyncUncaughtExceptionHandler} to use + */ + public AsyncExecutionAspectSupport(@Nullable Executor defaultExecutor, AsyncUncaughtExceptionHandler exceptionHandler) { + this.defaultExecutor = new SingletonSupplier<>(defaultExecutor, () -> getDefaultExecutor(this.beanFactory)); + this.exceptionHandler = SingletonSupplier.of(exceptionHandler); + } + + + /** + * Configure this aspect with the given executor and exception handler suppliers, + * applying the corresponding default if a supplier is not resolvable. + * @since 5.1 + */ + public void configure(@Nullable Supplier defaultExecutor, + @Nullable Supplier exceptionHandler) { + + this.defaultExecutor = new SingletonSupplier<>(defaultExecutor, () -> getDefaultExecutor(this.beanFactory)); + this.exceptionHandler = new SingletonSupplier<>(exceptionHandler, SimpleAsyncUncaughtExceptionHandler::new); + } + + /** + * Supply the executor to be used when executing async methods. + * @param defaultExecutor the {@code Executor} (typically a Spring {@code AsyncTaskExecutor} + * or {@link java.util.concurrent.ExecutorService}) to delegate to, unless a more specific + * executor has been requested via a qualifier on the async method, in which case the + * executor will be looked up at invocation time against the enclosing bean factory + * @see #getExecutorQualifier(Method) + * @see #setBeanFactory(BeanFactory) + * @see #getDefaultExecutor(BeanFactory) + */ + public void setExecutor(Executor defaultExecutor) { + this.defaultExecutor = SingletonSupplier.of(defaultExecutor); + } + + /** + * Supply the {@link AsyncUncaughtExceptionHandler} to use to handle exceptions + * thrown by invoking asynchronous methods with a {@code void} return type. + */ + public void setExceptionHandler(AsyncUncaughtExceptionHandler exceptionHandler) { + this.exceptionHandler = SingletonSupplier.of(exceptionHandler); + } + + /** + * Set the {@link BeanFactory} to be used when looking up executors by qualifier + * or when relying on the default executor lookup algorithm. + * @see #findQualifiedExecutor(BeanFactory, String) + * @see #getDefaultExecutor(BeanFactory) + */ + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + + /** + * Determine the specific executor to use when executing the given method. + * Should preferably return an {@link AsyncListenableTaskExecutor} implementation. + * @return the executor to use (or {@code null}, but just if no default executor is available) + */ + @Nullable + protected AsyncTaskExecutor determineAsyncExecutor(Method method) { + AsyncTaskExecutor executor = this.executors.get(method); + if (executor == null) { + Executor targetExecutor; + String qualifier = getExecutorQualifier(method); + if (StringUtils.hasLength(qualifier)) { + targetExecutor = findQualifiedExecutor(this.beanFactory, qualifier); + } + else { + targetExecutor = this.defaultExecutor.get(); + } + if (targetExecutor == null) { + return null; + } + executor = (targetExecutor instanceof AsyncListenableTaskExecutor ? + (AsyncListenableTaskExecutor) targetExecutor : new TaskExecutorAdapter(targetExecutor)); + this.executors.put(method, executor); + } + return executor; + } + + /** + * Return the qualifier or bean name of the executor to be used when executing the + * given async method, typically specified in the form of an annotation attribute. + * Returning an empty string or {@code null} indicates that no specific executor has + * been specified and that the {@linkplain #setExecutor(Executor) default executor} + * should be used. + * @param method the method to inspect for executor qualifier metadata + * @return the qualifier if specified, otherwise empty String or {@code null} + * @see #determineAsyncExecutor(Method) + * @see #findQualifiedExecutor(BeanFactory, String) + */ + @Nullable + protected abstract String getExecutorQualifier(Method method); + + /** + * Retrieve a target executor for the given qualifier. + * @param qualifier the qualifier to resolve + * @return the target executor, or {@code null} if none available + * @since 4.2.6 + * @see #getExecutorQualifier(Method) + */ + @Nullable + protected Executor findQualifiedExecutor(@Nullable BeanFactory beanFactory, String qualifier) { + if (beanFactory == null) { + throw new IllegalStateException("BeanFactory must be set on " + getClass().getSimpleName() + + " to access qualified executor '" + qualifier + "'"); + } + return BeanFactoryAnnotationUtils.qualifiedBeanOfType(beanFactory, Executor.class, qualifier); + } + + /** + * Retrieve or build a default executor for this advice instance. + * An executor returned from here will be cached for further use. + *

The default implementation searches for a unique {@link TaskExecutor} bean + * in the context, or for an {@link Executor} bean named "taskExecutor" otherwise. + * If neither of the two is resolvable, this implementation will return {@code null}. + * @param beanFactory the BeanFactory to use for a default executor lookup + * @return the default executor, or {@code null} if none available + * @since 4.2.6 + * @see #findQualifiedExecutor(BeanFactory, String) + * @see #DEFAULT_TASK_EXECUTOR_BEAN_NAME + */ + @Nullable + protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) { + if (beanFactory != null) { + try { + // Search for TaskExecutor bean... not plain Executor since that would + // match with ScheduledExecutorService as well, which is unusable for + // our purposes here. TaskExecutor is more clearly designed for it. + return beanFactory.getBean(TaskExecutor.class); + } + catch (NoUniqueBeanDefinitionException ex) { + logger.debug("Could not find unique TaskExecutor bean", ex); + try { + return beanFactory.getBean(DEFAULT_TASK_EXECUTOR_BEAN_NAME, Executor.class); + } + catch (NoSuchBeanDefinitionException ex2) { + if (logger.isInfoEnabled()) { + logger.info("More than one TaskExecutor bean found within the context, and none is named " + + "'taskExecutor'. Mark one of them as primary or name it 'taskExecutor' (possibly " + + "as an alias) in order to use it for async processing: " + ex.getBeanNamesFound()); + } + } + } + catch (NoSuchBeanDefinitionException ex) { + logger.debug("Could not find default TaskExecutor bean", ex); + try { + return beanFactory.getBean(DEFAULT_TASK_EXECUTOR_BEAN_NAME, Executor.class); + } + catch (NoSuchBeanDefinitionException ex2) { + logger.info("No task executor bean found for async processing: " + + "no bean of type TaskExecutor and no bean named 'taskExecutor' either"); + } + // Giving up -> either using local default executor or none at all... + } + } + return null; + } + + + /** + * Delegate for actually executing the given task with the chosen executor. + * @param task the task to execute + * @param executor the chosen executor + * @param returnType the declared return type (potentially a {@link Future} variant) + * @return the execution result (potentially a corresponding {@link Future} handle) + */ + @Nullable + protected Object doSubmit(Callable task, AsyncTaskExecutor executor, Class returnType) { + if (CompletableFuture.class.isAssignableFrom(returnType)) { + return CompletableFuture.supplyAsync(() -> { + try { + return task.call(); + } + catch (Throwable ex) { + throw new CompletionException(ex); + } + }, executor); + } + else if (ListenableFuture.class.isAssignableFrom(returnType)) { + return ((AsyncListenableTaskExecutor) executor).submitListenable(task); + } + else if (Future.class.isAssignableFrom(returnType)) { + return executor.submit(task); + } + else { + executor.submit(task); + return null; + } + } + + /** + * Handles a fatal error thrown while asynchronously invoking the specified + * {@link Method}. + *

If the return type of the method is a {@link Future} object, the original + * exception can be propagated by just throwing it at the higher level. However, + * for all other cases, the exception will not be transmitted back to the client. + * In that later case, the current {@link AsyncUncaughtExceptionHandler} will be + * used to manage such exception. + * @param ex the exception to handle + * @param method the method that was invoked + * @param params the parameters used to invoke the method + */ + protected void handleError(Throwable ex, Method method, Object... params) throws Exception { + if (Future.class.isAssignableFrom(method.getReturnType())) { + ReflectionUtils.rethrowException(ex); + } + else { + // Could not transmit the exception to the caller with default executor + try { + this.exceptionHandler.obtain().handleUncaughtException(ex, method, params); + } + catch (Throwable ex2) { + logger.warn("Exception handler for async method '" + method.toGenericString() + + "' threw unexpected exception itself", ex2); + } + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java new file mode 100644 index 0000000..8d97ad3 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java @@ -0,0 +1,166 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.interceptor; + +import java.lang.reflect.Method; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Future; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.Ordered; +import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * AOP Alliance {@code MethodInterceptor} that processes method invocations + * asynchronously, using a given {@link org.springframework.core.task.AsyncTaskExecutor}. + * Typically used with the {@link org.springframework.scheduling.annotation.Async} annotation. + * + *

In terms of target method signatures, any parameter types are supported. + * However, the return type is constrained to either {@code void} or + * {@code java.util.concurrent.Future}. In the latter case, the Future handle + * returned from the proxy will be an actual asynchronous Future that can be used + * to track the result of the asynchronous method execution. However, since the + * target method needs to implement the same signature, it will have to return + * a temporary Future handle that just passes the return value through + * (like Spring's {@link org.springframework.scheduling.annotation.AsyncResult} + * or EJB 3.1's {@code javax.ejb.AsyncResult}). + * + *

When the return type is {@code java.util.concurrent.Future}, any exception thrown + * during the execution can be accessed and managed by the caller. With {@code void} + * return type however, such exceptions cannot be transmitted back. In that case an + * {@link AsyncUncaughtExceptionHandler} can be registered to process such exceptions. + * + *

As of Spring 3.1.2 the {@code AnnotationAsyncExecutionInterceptor} subclass is + * preferred for use due to its support for executor qualification in conjunction with + * Spring's {@code @Async} annotation. + * + * @author Juergen Hoeller + * @author Chris Beams + * @author Stephane Nicoll + * @since 3.0 + * @see org.springframework.scheduling.annotation.Async + * @see org.springframework.scheduling.annotation.AsyncAnnotationAdvisor + * @see org.springframework.scheduling.annotation.AnnotationAsyncExecutionInterceptor + */ +public class AsyncExecutionInterceptor extends AsyncExecutionAspectSupport implements MethodInterceptor, Ordered { + + /** + * Create a new instance with a default {@link AsyncUncaughtExceptionHandler}. + * @param defaultExecutor the {@link Executor} (typically a Spring {@link AsyncTaskExecutor} + * or {@link java.util.concurrent.ExecutorService}) to delegate to; + * as of 4.2.6, a local executor for this interceptor will be built otherwise + */ + public AsyncExecutionInterceptor(@Nullable Executor defaultExecutor) { + super(defaultExecutor); + } + + /** + * Create a new {@code AsyncExecutionInterceptor}. + * @param defaultExecutor the {@link Executor} (typically a Spring {@link AsyncTaskExecutor} + * or {@link java.util.concurrent.ExecutorService}) to delegate to; + * as of 4.2.6, a local executor for this interceptor will be built otherwise + * @param exceptionHandler the {@link AsyncUncaughtExceptionHandler} to use + */ + public AsyncExecutionInterceptor(@Nullable Executor defaultExecutor, AsyncUncaughtExceptionHandler exceptionHandler) { + super(defaultExecutor, exceptionHandler); + } + + + /** + * Intercept the given method invocation, submit the actual calling of the method to + * the correct task executor and return immediately to the caller. + * @param invocation the method to intercept and make asynchronous + * @return {@link Future} if the original method returns {@code Future}; {@code null} + * otherwise. + */ + @Override + @Nullable + public Object invoke(final MethodInvocation invocation) throws Throwable { + Class targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null); + Method specificMethod = ClassUtils.getMostSpecificMethod(invocation.getMethod(), targetClass); + final Method userDeclaredMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); + + AsyncTaskExecutor executor = determineAsyncExecutor(userDeclaredMethod); + if (executor == null) { + throw new IllegalStateException( + "No executor specified and no default executor set on AsyncExecutionInterceptor either"); + } + + Callable task = () -> { + try { + Object result = invocation.proceed(); + if (result instanceof Future) { + return ((Future) result).get(); + } + } + catch (ExecutionException ex) { + handleError(ex.getCause(), userDeclaredMethod, invocation.getArguments()); + } + catch (Throwable ex) { + handleError(ex, userDeclaredMethod, invocation.getArguments()); + } + return null; + }; + + return doSubmit(task, executor, invocation.getMethod().getReturnType()); + } + + /** + * This implementation is a no-op for compatibility in Spring 3.1.2. + * Subclasses may override to provide support for extracting qualifier information, + * e.g. via an annotation on the given method. + * @return always {@code null} + * @since 3.1.2 + * @see #determineAsyncExecutor(Method) + */ + @Override + @Nullable + protected String getExecutorQualifier(Method method) { + return null; + } + + /** + * This implementation searches for a unique {@link org.springframework.core.task.TaskExecutor} + * bean in the context, or for an {@link Executor} bean named "taskExecutor" otherwise. + * If neither of the two is resolvable (e.g. if no {@code BeanFactory} was configured at all), + * this implementation falls back to a newly created {@link SimpleAsyncTaskExecutor} instance + * for local use if no default could be found. + * @see #DEFAULT_TASK_EXECUTOR_BEAN_NAME + */ + @Override + @Nullable + protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) { + Executor defaultExecutor = super.getDefaultExecutor(beanFactory); + return (defaultExecutor != null ? defaultExecutor : new SimpleAsyncTaskExecutor()); + } + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncUncaughtExceptionHandler.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncUncaughtExceptionHandler.java new file mode 100644 index 0000000..a3d7d85 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncUncaughtExceptionHandler.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.interceptor; + +import java.lang.reflect.Method; + +/** + * A strategy for handling uncaught exceptions thrown from asynchronous methods. + * + *

An asynchronous method usually returns a {@link java.util.concurrent.Future} + * instance that gives access to the underlying exception. When the method does + * not provide that return type, this handler can be used to manage such + * uncaught exceptions. + * + * @author Stephane Nicoll + * @since 4.1 + */ +@FunctionalInterface +public interface AsyncUncaughtExceptionHandler { + + /** + * Handle the given uncaught exception thrown from an asynchronous method. + * @param ex the exception thrown from the asynchronous method + * @param method the asynchronous method + * @param params the parameters used to invoked the method + */ + void handleUncaughtException(Throwable ex, Method method, Object... params); + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/ConcurrencyThrottleInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/ConcurrencyThrottleInterceptor.java new file mode 100644 index 0000000..dd802ce --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/ConcurrencyThrottleInterceptor.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.interceptor; + +import java.io.Serializable; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.lang.Nullable; +import org.springframework.util.ConcurrencyThrottleSupport; + +/** + * Interceptor that throttles concurrent access, blocking invocations + * if a specified concurrency limit is reached. + * + *

Can be applied to methods of local services that involve heavy use + * of system resources, in a scenario where it is more efficient to + * throttle concurrency for a specific service rather than restricting + * the entire thread pool (e.g. the web container's thread pool). + * + *

The default concurrency limit of this interceptor is 1. + * Specify the "concurrencyLimit" bean property to change this value. + * + * @author Juergen Hoeller + * @since 11.02.2004 + * @see #setConcurrencyLimit + */ +@SuppressWarnings("serial") +public class ConcurrencyThrottleInterceptor extends ConcurrencyThrottleSupport + implements MethodInterceptor, Serializable { + + public ConcurrencyThrottleInterceptor() { + setConcurrencyLimit(1); + } + + @Override + @Nullable + public Object invoke(MethodInvocation methodInvocation) throws Throwable { + beforeAccess(); + try { + return methodInvocation.proceed(); + } + finally { + afterAccess(); + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/CustomizableTraceInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/CustomizableTraceInterceptor.java new file mode 100644 index 0000000..a46c85d --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/CustomizableTraceInterceptor.java @@ -0,0 +1,400 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.interceptor; + +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; + +import org.springframework.core.Constants; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StopWatch; +import org.springframework.util.StringUtils; + +/** + * {@code MethodInterceptor} implementation that allows for highly customizable + * method-level tracing, using placeholders. + * + *

Trace messages are written on method entry, and if the method invocation succeeds + * on method exit. If an invocation results in an exception, then an exception message + * is written. The contents of these trace messages is fully customizable and special + * placeholders are available to allow you to include runtime information in your log + * messages. The placeholders available are: + * + *

    + *
  • {@code $[methodName]} - replaced with the name of the method being invoked
  • + *
  • {@code $[targetClassName]} - replaced with the name of the class that is + * the target of the invocation
  • + *
  • {@code $[targetClassShortName]} - replaced with the short name of the class + * that is the target of the invocation
  • + *
  • {@code $[returnValue]} - replaced with the value returned by the invocation
  • + *
  • {@code $[argumentTypes]} - replaced with a comma-separated list of the + * short class names of the method arguments
  • + *
  • {@code $[arguments]} - replaced with a comma-separated list of the + * {@code String} representation of the method arguments
  • + *
  • {@code $[exception]} - replaced with the {@code String} representation + * of any {@code Throwable} raised during the invocation
  • + *
  • {@code $[invocationTime]} - replaced with the time, in milliseconds, + * taken by the method invocation
  • + *
+ * + *

There are restrictions on which placeholders can be used in which messages: + * see the individual message properties for details on the valid placeholders. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see #setEnterMessage + * @see #setExitMessage + * @see #setExceptionMessage + * @see SimpleTraceInterceptor + */ +@SuppressWarnings("serial") +public class CustomizableTraceInterceptor extends AbstractTraceInterceptor { + + /** + * The {@code $[methodName]} placeholder. + * Replaced with the name of the method being invoked. + */ + public static final String PLACEHOLDER_METHOD_NAME = "$[methodName]"; + + /** + * The {@code $[targetClassName]} placeholder. + * Replaced with the fully-qualified name of the {@code Class} + * of the method invocation target. + */ + public static final String PLACEHOLDER_TARGET_CLASS_NAME = "$[targetClassName]"; + + /** + * The {@code $[targetClassShortName]} placeholder. + * Replaced with the short name of the {@code Class} of the + * method invocation target. + */ + public static final String PLACEHOLDER_TARGET_CLASS_SHORT_NAME = "$[targetClassShortName]"; + + /** + * The {@code $[returnValue]} placeholder. + * Replaced with the {@code String} representation of the value + * returned by the method invocation. + */ + public static final String PLACEHOLDER_RETURN_VALUE = "$[returnValue]"; + + /** + * The {@code $[argumentTypes]} placeholder. + * Replaced with a comma-separated list of the argument types for the + * method invocation. Argument types are written as short class names. + */ + public static final String PLACEHOLDER_ARGUMENT_TYPES = "$[argumentTypes]"; + + /** + * The {@code $[arguments]} placeholder. + * Replaced with a comma separated list of the argument values for the + * method invocation. Relies on the {@code toString()} method of + * each argument type. + */ + public static final String PLACEHOLDER_ARGUMENTS = "$[arguments]"; + + /** + * The {@code $[exception]} placeholder. + * Replaced with the {@code String} representation of any + * {@code Throwable} raised during method invocation. + */ + public static final String PLACEHOLDER_EXCEPTION = "$[exception]"; + + /** + * The {@code $[invocationTime]} placeholder. + * Replaced with the time taken by the invocation (in milliseconds). + */ + public static final String PLACEHOLDER_INVOCATION_TIME = "$[invocationTime]"; + + /** + * The default message used for writing method entry messages. + */ + private static final String DEFAULT_ENTER_MESSAGE = "Entering method '" + + PLACEHOLDER_METHOD_NAME + "' of class [" + PLACEHOLDER_TARGET_CLASS_NAME + "]"; + + /** + * The default message used for writing method exit messages. + */ + private static final String DEFAULT_EXIT_MESSAGE = "Exiting method '" + + PLACEHOLDER_METHOD_NAME + "' of class [" + PLACEHOLDER_TARGET_CLASS_NAME + "]"; + + /** + * The default message used for writing exception messages. + */ + private static final String DEFAULT_EXCEPTION_MESSAGE = "Exception thrown in method '" + + PLACEHOLDER_METHOD_NAME + "' of class [" + PLACEHOLDER_TARGET_CLASS_NAME + "]"; + + /** + * The {@code Pattern} used to match placeholders. + */ + private static final Pattern PATTERN = Pattern.compile("\\$\\[\\p{Alpha}+]"); + + /** + * The {@code Set} of allowed placeholders. + */ + private static final Set ALLOWED_PLACEHOLDERS = + new Constants(CustomizableTraceInterceptor.class).getValues("PLACEHOLDER_"); + + + /** + * The message for method entry. + */ + private String enterMessage = DEFAULT_ENTER_MESSAGE; + + /** + * The message for method exit. + */ + private String exitMessage = DEFAULT_EXIT_MESSAGE; + + /** + * The message for exceptions during method execution. + */ + private String exceptionMessage = DEFAULT_EXCEPTION_MESSAGE; + + + /** + * Set the template used for method entry log messages. + * This template can contain any of the following placeholders: + *
    + *
  • {@code $[targetClassName]}
  • + *
  • {@code $[targetClassShortName]}
  • + *
  • {@code $[argumentTypes]}
  • + *
  • {@code $[arguments]}
  • + *
+ */ + public void setEnterMessage(String enterMessage) throws IllegalArgumentException { + Assert.hasText(enterMessage, "enterMessage must not be empty"); + checkForInvalidPlaceholders(enterMessage); + Assert.doesNotContain(enterMessage, PLACEHOLDER_RETURN_VALUE, + "enterMessage cannot contain placeholder " + PLACEHOLDER_RETURN_VALUE); + Assert.doesNotContain(enterMessage, PLACEHOLDER_EXCEPTION, + "enterMessage cannot contain placeholder " + PLACEHOLDER_EXCEPTION); + Assert.doesNotContain(enterMessage, PLACEHOLDER_INVOCATION_TIME, + "enterMessage cannot contain placeholder " + PLACEHOLDER_INVOCATION_TIME); + this.enterMessage = enterMessage; + } + + /** + * Set the template used for method exit log messages. + * This template can contain any of the following placeholders: + *
    + *
  • {@code $[targetClassName]}
  • + *
  • {@code $[targetClassShortName]}
  • + *
  • {@code $[argumentTypes]}
  • + *
  • {@code $[arguments]}
  • + *
  • {@code $[returnValue]}
  • + *
  • {@code $[invocationTime]}
  • + *
+ */ + public void setExitMessage(String exitMessage) { + Assert.hasText(exitMessage, "exitMessage must not be empty"); + checkForInvalidPlaceholders(exitMessage); + Assert.doesNotContain(exitMessage, PLACEHOLDER_EXCEPTION, + "exitMessage cannot contain placeholder" + PLACEHOLDER_EXCEPTION); + this.exitMessage = exitMessage; + } + + /** + * Set the template used for method exception log messages. + * This template can contain any of the following placeholders: + *
    + *
  • {@code $[targetClassName]}
  • + *
  • {@code $[targetClassShortName]}
  • + *
  • {@code $[argumentTypes]}
  • + *
  • {@code $[arguments]}
  • + *
  • {@code $[exception]}
  • + *
+ */ + public void setExceptionMessage(String exceptionMessage) { + Assert.hasText(exceptionMessage, "exceptionMessage must not be empty"); + checkForInvalidPlaceholders(exceptionMessage); + Assert.doesNotContain(exceptionMessage, PLACEHOLDER_RETURN_VALUE, + "exceptionMessage cannot contain placeholder " + PLACEHOLDER_RETURN_VALUE); + this.exceptionMessage = exceptionMessage; + } + + + /** + * Writes a log message before the invocation based on the value of {@code enterMessage}. + * If the invocation succeeds, then a log message is written on exit based on the value + * {@code exitMessage}. If an exception occurs during invocation, then a message is + * written based on the value of {@code exceptionMessage}. + * @see #setEnterMessage + * @see #setExitMessage + * @see #setExceptionMessage + */ + @Override + protected Object invokeUnderTrace(MethodInvocation invocation, Log logger) throws Throwable { + String name = ClassUtils.getQualifiedMethodName(invocation.getMethod()); + StopWatch stopWatch = new StopWatch(name); + Object returnValue = null; + boolean exitThroughException = false; + try { + stopWatch.start(name); + writeToLog(logger, + replacePlaceholders(this.enterMessage, invocation, null, null, -1)); + returnValue = invocation.proceed(); + return returnValue; + } + catch (Throwable ex) { + if (stopWatch.isRunning()) { + stopWatch.stop(); + } + exitThroughException = true; + writeToLog(logger, replacePlaceholders( + this.exceptionMessage, invocation, null, ex, stopWatch.getTotalTimeMillis()), ex); + throw ex; + } + finally { + if (!exitThroughException) { + if (stopWatch.isRunning()) { + stopWatch.stop(); + } + writeToLog(logger, replacePlaceholders( + this.exitMessage, invocation, returnValue, null, stopWatch.getTotalTimeMillis())); + } + } + } + + /** + * Replace the placeholders in the given message with the supplied values, + * or values derived from those supplied. + * @param message the message template containing the placeholders to be replaced + * @param methodInvocation the {@code MethodInvocation} being logged. + * Used to derive values for all placeholders except {@code $[exception]} + * and {@code $[returnValue]}. + * @param returnValue any value returned by the invocation. + * Used to replace the {@code $[returnValue]} placeholder. May be {@code null}. + * @param throwable any {@code Throwable} raised during the invocation. + * The value of {@code Throwable.toString()} is replaced for the + * {@code $[exception]} placeholder. May be {@code null}. + * @param invocationTime the value to write in place of the + * {@code $[invocationTime]} placeholder + * @return the formatted output to write to the log + */ + protected String replacePlaceholders(String message, MethodInvocation methodInvocation, + @Nullable Object returnValue, @Nullable Throwable throwable, long invocationTime) { + + Matcher matcher = PATTERN.matcher(message); + Object target = methodInvocation.getThis(); + Assert.state(target != null, "Target must not be null"); + + StringBuffer output = new StringBuffer(); + while (matcher.find()) { + String match = matcher.group(); + if (PLACEHOLDER_METHOD_NAME.equals(match)) { + matcher.appendReplacement(output, Matcher.quoteReplacement(methodInvocation.getMethod().getName())); + } + else if (PLACEHOLDER_TARGET_CLASS_NAME.equals(match)) { + String className = getClassForLogging(target).getName(); + matcher.appendReplacement(output, Matcher.quoteReplacement(className)); + } + else if (PLACEHOLDER_TARGET_CLASS_SHORT_NAME.equals(match)) { + String shortName = ClassUtils.getShortName(getClassForLogging(target)); + matcher.appendReplacement(output, Matcher.quoteReplacement(shortName)); + } + else if (PLACEHOLDER_ARGUMENTS.equals(match)) { + matcher.appendReplacement(output, + Matcher.quoteReplacement(StringUtils.arrayToCommaDelimitedString(methodInvocation.getArguments()))); + } + else if (PLACEHOLDER_ARGUMENT_TYPES.equals(match)) { + appendArgumentTypes(methodInvocation, matcher, output); + } + else if (PLACEHOLDER_RETURN_VALUE.equals(match)) { + appendReturnValue(methodInvocation, matcher, output, returnValue); + } + else if (throwable != null && PLACEHOLDER_EXCEPTION.equals(match)) { + matcher.appendReplacement(output, Matcher.quoteReplacement(throwable.toString())); + } + else if (PLACEHOLDER_INVOCATION_TIME.equals(match)) { + matcher.appendReplacement(output, Long.toString(invocationTime)); + } + else { + // Should not happen since placeholders are checked earlier. + throw new IllegalArgumentException("Unknown placeholder [" + match + "]"); + } + } + matcher.appendTail(output); + + return output.toString(); + } + + /** + * Adds the {@code String} representation of the method return value + * to the supplied {@code StringBuffer}. Correctly handles + * {@code null} and {@code void} results. + * @param methodInvocation the {@code MethodInvocation} that returned the value + * @param matcher the {@code Matcher} containing the matched placeholder + * @param output the {@code StringBuffer} to write output to + * @param returnValue the value returned by the method invocation. + */ + private void appendReturnValue( + MethodInvocation methodInvocation, Matcher matcher, StringBuffer output, @Nullable Object returnValue) { + + if (methodInvocation.getMethod().getReturnType() == void.class) { + matcher.appendReplacement(output, "void"); + } + else if (returnValue == null) { + matcher.appendReplacement(output, "null"); + } + else { + matcher.appendReplacement(output, Matcher.quoteReplacement(returnValue.toString())); + } + } + + /** + * Adds a comma-separated list of the short {@code Class} names of the + * method argument types to the output. For example, if a method has signature + * {@code put(java.lang.String, java.lang.Object)} then the value returned + * will be {@code String, Object}. + * @param methodInvocation the {@code MethodInvocation} being logged. + * Arguments will be retrieved from the corresponding {@code Method}. + * @param matcher the {@code Matcher} containing the state of the output + * @param output the {@code StringBuffer} containing the output + */ + private void appendArgumentTypes(MethodInvocation methodInvocation, Matcher matcher, StringBuffer output) { + Class[] argumentTypes = methodInvocation.getMethod().getParameterTypes(); + String[] argumentTypeShortNames = new String[argumentTypes.length]; + for (int i = 0; i < argumentTypeShortNames.length; i++) { + argumentTypeShortNames[i] = ClassUtils.getShortName(argumentTypes[i]); + } + matcher.appendReplacement(output, + Matcher.quoteReplacement(StringUtils.arrayToCommaDelimitedString(argumentTypeShortNames))); + } + + /** + * Checks to see if the supplied {@code String} has any placeholders + * that are not specified as constants on this class and throws an + * {@code IllegalArgumentException} if so. + */ + private void checkForInvalidPlaceholders(String message) throws IllegalArgumentException { + Matcher matcher = PATTERN.matcher(message); + while (matcher.find()) { + String match = matcher.group(); + if (!ALLOWED_PLACEHOLDERS.contains(match)) { + throw new IllegalArgumentException("Placeholder [" + match + "] is not valid"); + } + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/DebugInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/DebugInterceptor.java new file mode 100644 index 0000000..06ea610 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/DebugInterceptor.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.interceptor; + +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.lang.Nullable; + +/** + * AOP Alliance {@code MethodInterceptor} that can be introduced in a chain + * to display verbose information about intercepted invocations to the logger. + * + *

Logs full invocation details on method entry and method exit, + * including invocation arguments and invocation count. This is only + * intended for debugging purposes; use {@code SimpleTraceInterceptor} + * or {@code CustomizableTraceInterceptor} for pure tracing purposes. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see SimpleTraceInterceptor + * @see CustomizableTraceInterceptor + */ +@SuppressWarnings("serial") +public class DebugInterceptor extends SimpleTraceInterceptor { + + private volatile long count; + + + /** + * Create a new DebugInterceptor with a static logger. + */ + public DebugInterceptor() { + } + + /** + * Create a new DebugInterceptor with dynamic or static logger, + * according to the given flag. + * @param useDynamicLogger whether to use a dynamic logger or a static logger + * @see #setUseDynamicLogger + */ + public DebugInterceptor(boolean useDynamicLogger) { + setUseDynamicLogger(useDynamicLogger); + } + + + @Override + @Nullable + public Object invoke(MethodInvocation invocation) throws Throwable { + synchronized (this) { + this.count++; + } + return super.invoke(invocation); + } + + @Override + protected String getInvocationDescription(MethodInvocation invocation) { + return invocation + "; count=" + this.count; + } + + + /** + * Return the number of times this interceptor has been invoked. + */ + public long getCount() { + return this.count; + } + + /** + * Reset the invocation count to zero. + */ + public synchronized void resetCount() { + this.count = 0; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/ExposeBeanNameAdvisors.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/ExposeBeanNameAdvisors.java new file mode 100644 index 0000000..7dfb777 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/ExposeBeanNameAdvisors.java @@ -0,0 +1,155 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.interceptor; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.aop.Advisor; +import org.springframework.aop.ProxyMethodInvocation; +import org.springframework.aop.support.DefaultIntroductionAdvisor; +import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.aop.support.DelegatingIntroductionInterceptor; +import org.springframework.beans.factory.NamedBean; +import org.springframework.lang.Nullable; + +/** + * Convenient methods for creating advisors that may be used when autoproxying beans + * created with the Spring IoC container, binding the bean name to the current + * invocation. May support a {@code bean()} pointcut designator with AspectJ. + * + *

Typically used in Spring auto-proxying, where the bean name is known + * at proxy creation time. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.beans.factory.NamedBean + */ +public abstract class ExposeBeanNameAdvisors { + + /** + * Binding for the bean name of the bean which is currently being invoked + * in the ReflectiveMethodInvocation userAttributes Map. + */ + private static final String BEAN_NAME_ATTRIBUTE = ExposeBeanNameAdvisors.class.getName() + ".BEAN_NAME"; + + + /** + * Find the bean name for the current invocation. Assumes that an ExposeBeanNameAdvisor + * has been included in the interceptor chain, and that the invocation is exposed + * with ExposeInvocationInterceptor. + * @return the bean name (never {@code null}) + * @throws IllegalStateException if the bean name has not been exposed + */ + public static String getBeanName() throws IllegalStateException { + return getBeanName(ExposeInvocationInterceptor.currentInvocation()); + } + + /** + * Find the bean name for the given invocation. Assumes that an ExposeBeanNameAdvisor + * has been included in the interceptor chain. + * @param mi the MethodInvocation that should contain the bean name as an attribute + * @return the bean name (never {@code null}) + * @throws IllegalStateException if the bean name has not been exposed + */ + public static String getBeanName(MethodInvocation mi) throws IllegalStateException { + if (!(mi instanceof ProxyMethodInvocation)) { + throw new IllegalArgumentException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi); + } + ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi; + String beanName = (String) pmi.getUserAttribute(BEAN_NAME_ATTRIBUTE); + if (beanName == null) { + throw new IllegalStateException("Cannot get bean name; not set on MethodInvocation: " + mi); + } + return beanName; + } + + /** + * Create a new advisor that will expose the given bean name, + * with no introduction. + * @param beanName bean name to expose + */ + public static Advisor createAdvisorWithoutIntroduction(String beanName) { + return new DefaultPointcutAdvisor(new ExposeBeanNameInterceptor(beanName)); + } + + /** + * Create a new advisor that will expose the given bean name, introducing + * the NamedBean interface to make the bean name accessible without forcing + * the target object to be aware of this Spring IoC concept. + * @param beanName the bean name to expose + */ + public static Advisor createAdvisorIntroducingNamedBean(String beanName) { + return new DefaultIntroductionAdvisor(new ExposeBeanNameIntroduction(beanName)); + } + + + /** + * Interceptor that exposes the specified bean name as invocation attribute. + */ + private static class ExposeBeanNameInterceptor implements MethodInterceptor { + + private final String beanName; + + public ExposeBeanNameInterceptor(String beanName) { + this.beanName = beanName; + } + + @Override + @Nullable + public Object invoke(MethodInvocation mi) throws Throwable { + if (!(mi instanceof ProxyMethodInvocation)) { + throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi); + } + ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi; + pmi.setUserAttribute(BEAN_NAME_ATTRIBUTE, this.beanName); + return mi.proceed(); + } + } + + + /** + * Introduction that exposes the specified bean name as invocation attribute. + */ + @SuppressWarnings("serial") + private static class ExposeBeanNameIntroduction extends DelegatingIntroductionInterceptor implements NamedBean { + + private final String beanName; + + public ExposeBeanNameIntroduction(String beanName) { + this.beanName = beanName; + } + + @Override + @Nullable + public Object invoke(MethodInvocation mi) throws Throwable { + if (!(mi instanceof ProxyMethodInvocation)) { + throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi); + } + ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi; + pmi.setUserAttribute(BEAN_NAME_ATTRIBUTE, this.beanName); + return super.invoke(mi); + } + + @Override + public String getBeanName() { + return this.beanName; + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/ExposeInvocationInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/ExposeInvocationInterceptor.java new file mode 100644 index 0000000..9822374 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/ExposeInvocationInterceptor.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.interceptor; + +import java.io.Serializable; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.aop.Advisor; +import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.core.NamedThreadLocal; +import org.springframework.core.PriorityOrdered; +import org.springframework.lang.Nullable; + +/** + * Interceptor that exposes the current {@link org.aopalliance.intercept.MethodInvocation} + * as a thread-local object. We occasionally need to do this; for example, when a pointcut + * (e.g. an AspectJ expression pointcut) needs to know the full invocation context. + * + *

Don't use this interceptor unless this is really necessary. Target objects should + * not normally know about Spring AOP, as this creates a dependency on Spring API. + * Target objects should be plain POJOs as far as possible. + * + *

If used, this interceptor will normally be the first in the interceptor chain. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +@SuppressWarnings("serial") +public final class ExposeInvocationInterceptor implements MethodInterceptor, PriorityOrdered, Serializable { + + /** Singleton instance of this class. */ + public static final ExposeInvocationInterceptor INSTANCE = new ExposeInvocationInterceptor(); + + /** + * Singleton advisor for this class. Use in preference to INSTANCE when using + * Spring AOP, as it prevents the need to create a new Advisor to wrap the instance. + */ + public static final Advisor ADVISOR = new DefaultPointcutAdvisor(INSTANCE) { + @Override + public String toString() { + return ExposeInvocationInterceptor.class.getName() +".ADVISOR"; + } + }; + + private static final ThreadLocal invocation = + new NamedThreadLocal<>("Current AOP method invocation"); + + + /** + * Return the AOP Alliance MethodInvocation object associated with the current invocation. + * @return the invocation object associated with the current invocation + * @throws IllegalStateException if there is no AOP invocation in progress, + * or if the ExposeInvocationInterceptor was not added to this interceptor chain + */ + public static MethodInvocation currentInvocation() throws IllegalStateException { + MethodInvocation mi = invocation.get(); + if (mi == null) { + throw new IllegalStateException( + "No MethodInvocation found: Check that an AOP invocation is in progress and that the " + + "ExposeInvocationInterceptor is upfront in the interceptor chain. Specifically, note that " + + "advices with order HIGHEST_PRECEDENCE will execute before ExposeInvocationInterceptor! " + + "In addition, ExposeInvocationInterceptor and ExposeInvocationInterceptor.currentInvocation() " + + "must be invoked from the same thread."); + } + return mi; + } + + + /** + * Ensures that only the canonical instance can be created. + */ + private ExposeInvocationInterceptor() { + } + + @Override + @Nullable + public Object invoke(MethodInvocation mi) throws Throwable { + MethodInvocation oldInvocation = invocation.get(); + invocation.set(mi); + try { + return mi.proceed(); + } + finally { + invocation.set(oldInvocation); + } + } + + @Override + public int getOrder() { + return PriorityOrdered.HIGHEST_PRECEDENCE + 1; + } + + /** + * Required to support serialization. Replaces with canonical instance + * on deserialization, protecting Singleton pattern. + *

Alternative to overriding the {@code equals} method. + */ + private Object readResolve() { + return INSTANCE; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/JamonPerformanceMonitorInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/JamonPerformanceMonitorInterceptor.java new file mode 100644 index 0000000..0476a4f --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/JamonPerformanceMonitorInterceptor.java @@ -0,0 +1,144 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.interceptor; + +import com.jamonapi.MonKey; +import com.jamonapi.MonKeyImp; +import com.jamonapi.Monitor; +import com.jamonapi.MonitorFactory; +import com.jamonapi.utils.Misc; +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; + +/** + * Performance monitor interceptor that uses JAMon library to perform the + * performance measurement on the intercepted method and output the stats. + * In addition, it tracks/counts exceptions thrown by the intercepted method. + * The stack traces can be viewed in the JAMon web application. + * + *

This code is inspired by Thierry Templier's blog. + * + * @author Dmitriy Kopylenko + * @author Juergen Hoeller + * @author Rob Harrop + * @author Steve Souza + * @since 1.1.3 + * @see com.jamonapi.MonitorFactory + * @see PerformanceMonitorInterceptor + */ +@SuppressWarnings("serial") +public class JamonPerformanceMonitorInterceptor extends AbstractMonitoringInterceptor { + + private boolean trackAllInvocations = false; + + + /** + * Create a new JamonPerformanceMonitorInterceptor with a static logger. + */ + public JamonPerformanceMonitorInterceptor() { + } + + /** + * Create a new JamonPerformanceMonitorInterceptor with a dynamic or static logger, + * according to the given flag. + * @param useDynamicLogger whether to use a dynamic logger or a static logger + * @see #setUseDynamicLogger + */ + public JamonPerformanceMonitorInterceptor(boolean useDynamicLogger) { + setUseDynamicLogger(useDynamicLogger); + } + + /** + * Create a new JamonPerformanceMonitorInterceptor with a dynamic or static logger, + * according to the given flag. + * @param useDynamicLogger whether to use a dynamic logger or a static logger + * @param trackAllInvocations whether to track all invocations that go through + * this interceptor, or just invocations with trace logging enabled + * @see #setUseDynamicLogger + */ + public JamonPerformanceMonitorInterceptor(boolean useDynamicLogger, boolean trackAllInvocations) { + setUseDynamicLogger(useDynamicLogger); + setTrackAllInvocations(trackAllInvocations); + } + + + /** + * Set whether to track all invocations that go through this interceptor, + * or just invocations with trace logging enabled. + *

Default is "false": Only invocations with trace logging enabled will + * be monitored. Specify "true" to let JAMon track all invocations, + * gathering statistics even when trace logging is disabled. + */ + public void setTrackAllInvocations(boolean trackAllInvocations) { + this.trackAllInvocations = trackAllInvocations; + } + + + /** + * Always applies the interceptor if the "trackAllInvocations" flag has been set; + * else just kicks in if the log is enabled. + * @see #setTrackAllInvocations + * @see #isLogEnabled + */ + @Override + protected boolean isInterceptorEnabled(MethodInvocation invocation, Log logger) { + return (this.trackAllInvocations || isLogEnabled(logger)); + } + + /** + * Wraps the invocation with a JAMon Monitor and writes the current + * performance statistics to the log (if enabled). + * @see com.jamonapi.MonitorFactory#start + * @see com.jamonapi.Monitor#stop + */ + @Override + protected Object invokeUnderTrace(MethodInvocation invocation, Log logger) throws Throwable { + String name = createInvocationTraceName(invocation); + MonKey key = new MonKeyImp(name, name, "ms."); + + Monitor monitor = MonitorFactory.start(key); + try { + return invocation.proceed(); + } + catch (Throwable ex) { + trackException(key, ex); + throw ex; + } + finally { + monitor.stop(); + if (!this.trackAllInvocations || isLogEnabled(logger)) { + writeToLog(logger, "JAMon performance statistics for method [" + name + "]:\n" + monitor); + } + } + } + + /** + * Count the thrown exception and put the stack trace in the details portion of the key. + * This will allow the stack trace to be viewed in the JAMon web application. + */ + protected void trackException(MonKey key, Throwable ex) { + String stackTrace = "stackTrace=" + Misc.getExceptionTrace(ex); + key.setDetails(stackTrace); + + // Specific exception counter. Example: java.lang.RuntimeException + MonitorFactory.add(new MonKeyImp(ex.getClass().getName(), stackTrace, "Exception"), 1); + + // General exception counter which is a total for all exceptions thrown + MonitorFactory.add(new MonKeyImp(MonitorFactory.EXCEPTIONS_LABEL, stackTrace, "Exception"), 1); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/PerformanceMonitorInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/PerformanceMonitorInterceptor.java new file mode 100644 index 0000000..6a0e531 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/PerformanceMonitorInterceptor.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.interceptor; + +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; + +import org.springframework.util.StopWatch; + +/** + * Simple AOP Alliance {@code MethodInterceptor} for performance monitoring. + * This interceptor has no effect on the intercepted method call. + * + *

Uses a {@code StopWatch} for the actual performance measuring. + * + * @author Rod Johnson + * @author Dmitriy Kopylenko + * @author Rob Harrop + * @see org.springframework.util.StopWatch + * @see JamonPerformanceMonitorInterceptor + */ +@SuppressWarnings("serial") +public class PerformanceMonitorInterceptor extends AbstractMonitoringInterceptor { + + /** + * Create a new PerformanceMonitorInterceptor with a static logger. + */ + public PerformanceMonitorInterceptor() { + } + + /** + * Create a new PerformanceMonitorInterceptor with a dynamic or static logger, + * according to the given flag. + * @param useDynamicLogger whether to use a dynamic logger or a static logger + * @see #setUseDynamicLogger + */ + public PerformanceMonitorInterceptor(boolean useDynamicLogger) { + setUseDynamicLogger(useDynamicLogger); + } + + + @Override + protected Object invokeUnderTrace(MethodInvocation invocation, Log logger) throws Throwable { + String name = createInvocationTraceName(invocation); + StopWatch stopWatch = new StopWatch(name); + stopWatch.start(name); + try { + return invocation.proceed(); + } + finally { + stopWatch.stop(); + writeToLog(logger, stopWatch.shortSummary()); + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/SimpleAsyncUncaughtExceptionHandler.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/SimpleAsyncUncaughtExceptionHandler.java new file mode 100644 index 0000000..d11f0d9 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/SimpleAsyncUncaughtExceptionHandler.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.interceptor; + +import java.lang.reflect.Method; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * A default {@link AsyncUncaughtExceptionHandler} that simply logs the exception. + * + * @author Stephane Nicoll + * @author Juergen Hoeller + * @since 4.1 + */ +public class SimpleAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler { + + private static final Log logger = LogFactory.getLog(SimpleAsyncUncaughtExceptionHandler.class); + + + @Override + public void handleUncaughtException(Throwable ex, Method method, Object... params) { + if (logger.isErrorEnabled()) { + logger.error("Unexpected exception occurred invoking async method: " + method, ex); + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/SimpleTraceInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/SimpleTraceInterceptor.java new file mode 100644 index 0000000..f53fd86 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/SimpleTraceInterceptor.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.interceptor; + +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; + +import org.springframework.util.Assert; + +/** + * Simple AOP Alliance {@code MethodInterceptor} that can be introduced + * in a chain to display verbose trace information about intercepted method + * invocations, with method entry and method exit info. + * + *

Consider using {@code CustomizableTraceInterceptor} for more + * advanced needs. + * + * @author Dmitriy Kopylenko + * @author Juergen Hoeller + * @since 1.2 + * @see CustomizableTraceInterceptor + */ +@SuppressWarnings("serial") +public class SimpleTraceInterceptor extends AbstractTraceInterceptor { + + /** + * Create a new SimpleTraceInterceptor with a static logger. + */ + public SimpleTraceInterceptor() { + } + + /** + * Create a new SimpleTraceInterceptor with dynamic or static logger, + * according to the given flag. + * @param useDynamicLogger whether to use a dynamic logger or a static logger + * @see #setUseDynamicLogger + */ + public SimpleTraceInterceptor(boolean useDynamicLogger) { + setUseDynamicLogger(useDynamicLogger); + } + + + @Override + protected Object invokeUnderTrace(MethodInvocation invocation, Log logger) throws Throwable { + String invocationDescription = getInvocationDescription(invocation); + writeToLog(logger, "Entering " + invocationDescription); + try { + Object rval = invocation.proceed(); + writeToLog(logger, "Exiting " + invocationDescription); + return rval; + } + catch (Throwable ex) { + writeToLog(logger, "Exception thrown in " + invocationDescription, ex); + throw ex; + } + } + + /** + * Return a description for the given method invocation. + * @param invocation the invocation to describe + * @return the description + */ + protected String getInvocationDescription(MethodInvocation invocation) { + Object target = invocation.getThis(); + Assert.state(target != null, "Target must not be null"); + String className = target.getClass().getName(); + return "method '" + invocation.getMethod().getName() + "' of class [" + className + "]"; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/package-info.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/package-info.java new file mode 100644 index 0000000..eb2a05f --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/package-info.java @@ -0,0 +1,11 @@ +/** + * Provides miscellaneous interceptor implementations. + * More specific interceptors can be found in corresponding + * functionality packages, like "transaction" and "orm". + */ +@NonNullApi +@NonNullFields +package org.springframework.aop.interceptor; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-aop/src/main/java/org/springframework/aop/package-info.java b/spring-aop/src/main/java/org/springframework/aop/package-info.java new file mode 100644 index 0000000..2b87bce --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/package-info.java @@ -0,0 +1,25 @@ +/** + * Core Spring AOP interfaces, built on AOP Alliance AOP interoperability interfaces. + * + *

Any AOP Alliance MethodInterceptor is usable in Spring. + * + *
Spring AOP also offers: + *

    + *
  • Introduction support + *
  • A Pointcut abstraction, supporting "static" pointcuts + * (class and method-based) and "dynamic" pointcuts (also considering method arguments). + * There are currently no AOP Alliance interfaces for pointcuts. + *
  • A full range of advice types, including around, before, after returning and throws advice. + *
  • Extensibility allowing arbitrary custom advice types to + * be plugged in without modifying the core framework. + *
+ * + *

Spring AOP can be used programmatically or (preferably) + * integrated with the Spring IoC container. + */ +@NonNullApi +@NonNullFields +package org.springframework.aop; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-aop/src/main/java/org/springframework/aop/scope/DefaultScopedObject.java b/spring-aop/src/main/java/org/springframework/aop/scope/DefaultScopedObject.java new file mode 100644 index 0000000..302cd9e --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/scope/DefaultScopedObject.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.scope; + +import java.io.Serializable; + +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.util.Assert; + +/** + * Default implementation of the {@link ScopedObject} interface. + * + *

Simply delegates the calls to the underlying + * {@link ConfigurableBeanFactory bean factory} + * ({@link ConfigurableBeanFactory#getBean(String)}/ + * {@link ConfigurableBeanFactory#destroyScopedBean(String)}). + * + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.beans.factory.BeanFactory#getBean + * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#destroyScopedBean + */ +@SuppressWarnings("serial") +public class DefaultScopedObject implements ScopedObject, Serializable { + + private final ConfigurableBeanFactory beanFactory; + + private final String targetBeanName; + + + /** + * Creates a new instance of the {@link DefaultScopedObject} class. + * @param beanFactory the {@link ConfigurableBeanFactory} that holds the scoped target object + * @param targetBeanName the name of the target bean + */ + public DefaultScopedObject(ConfigurableBeanFactory beanFactory, String targetBeanName) { + Assert.notNull(beanFactory, "BeanFactory must not be null"); + Assert.hasText(targetBeanName, "'targetBeanName' must not be empty"); + this.beanFactory = beanFactory; + this.targetBeanName = targetBeanName; + } + + + @Override + public Object getTargetObject() { + return this.beanFactory.getBean(this.targetBeanName); + } + + @Override + public void removeFromScope() { + this.beanFactory.destroyScopedBean(this.targetBeanName); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/scope/ScopedObject.java b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedObject.java new file mode 100644 index 0000000..ff5edbb --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedObject.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.scope; + +import org.springframework.aop.RawTargetAccess; + +/** + * An AOP introduction interface for scoped objects. + * + *

Objects created from the {@link ScopedProxyFactoryBean} can be cast + * to this interface, enabling access to the raw target object + * and programmatic removal of the target object. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + * @see ScopedProxyFactoryBean + */ +public interface ScopedObject extends RawTargetAccess { + + /** + * Return the current target object behind this scoped object proxy, + * in its raw form (as stored in the target scope). + *

The raw target object can for example be passed to persistence + * providers which would not be able to handle the scoped proxy object. + * @return the current target object behind this scoped object proxy + */ + Object getTargetObject(); + + /** + * Remove this object from its target scope, for example from + * the backing session. + *

Note that no further calls may be made to the scoped object + * afterwards (at least within the current thread, that is, with + * the exact same target object in the target scope). + */ + void removeFromScope(); + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyFactoryBean.java b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyFactoryBean.java new file mode 100644 index 0000000..281b975 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyFactoryBean.java @@ -0,0 +1,142 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.scope; + +import java.lang.reflect.Modifier; + +import org.springframework.aop.framework.AopInfrastructureBean; +import org.springframework.aop.framework.ProxyConfig; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.support.DelegatingIntroductionInterceptor; +import org.springframework.aop.target.SimpleBeanTargetSource; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.FactoryBeanNotInitializedException; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Convenient proxy factory bean for scoped objects. + * + *

Proxies created using this factory bean are thread-safe singletons + * and may be injected into shared objects, with transparent scoping behavior. + * + *

Proxies returned by this class implement the {@link ScopedObject} interface. + * This presently allows for removing the corresponding object from the scope, + * seamlessly creating a new instance in the scope on next access. + * + *

Please note that the proxies created by this factory are + * class-based proxies by default. This can be customized + * through switching the "proxyTargetClass" property to "false". + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + * @see #setProxyTargetClass + */ +@SuppressWarnings("serial") +public class ScopedProxyFactoryBean extends ProxyConfig + implements FactoryBean, BeanFactoryAware, AopInfrastructureBean { + + /** The TargetSource that manages scoping. */ + private final SimpleBeanTargetSource scopedTargetSource = new SimpleBeanTargetSource(); + + /** The name of the target bean. */ + @Nullable + private String targetBeanName; + + /** The cached singleton proxy. */ + @Nullable + private Object proxy; + + + /** + * Create a new ScopedProxyFactoryBean instance. + */ + public ScopedProxyFactoryBean() { + setProxyTargetClass(true); + } + + + /** + * Set the name of the bean that is to be scoped. + */ + public void setTargetBeanName(String targetBeanName) { + this.targetBeanName = targetBeanName; + this.scopedTargetSource.setTargetBeanName(targetBeanName); + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + if (!(beanFactory instanceof ConfigurableBeanFactory)) { + throw new IllegalStateException("Not running in a ConfigurableBeanFactory: " + beanFactory); + } + ConfigurableBeanFactory cbf = (ConfigurableBeanFactory) beanFactory; + + this.scopedTargetSource.setBeanFactory(beanFactory); + + ProxyFactory pf = new ProxyFactory(); + pf.copyFrom(this); + pf.setTargetSource(this.scopedTargetSource); + + Assert.notNull(this.targetBeanName, "Property 'targetBeanName' is required"); + Class beanType = beanFactory.getType(this.targetBeanName); + if (beanType == null) { + throw new IllegalStateException("Cannot create scoped proxy for bean '" + this.targetBeanName + + "': Target type could not be determined at the time of proxy creation."); + } + if (!isProxyTargetClass() || beanType.isInterface() || Modifier.isPrivate(beanType.getModifiers())) { + pf.setInterfaces(ClassUtils.getAllInterfacesForClass(beanType, cbf.getBeanClassLoader())); + } + + // Add an introduction that implements only the methods on ScopedObject. + ScopedObject scopedObject = new DefaultScopedObject(cbf, this.scopedTargetSource.getTargetBeanName()); + pf.addAdvice(new DelegatingIntroductionInterceptor(scopedObject)); + + // Add the AopInfrastructureBean marker to indicate that the scoped proxy + // itself is not subject to auto-proxying! Only its target bean is. + pf.addInterface(AopInfrastructureBean.class); + + this.proxy = pf.getProxy(cbf.getBeanClassLoader()); + } + + + @Override + public Object getObject() { + if (this.proxy == null) { + throw new FactoryBeanNotInitializedException(); + } + return this.proxy; + } + + @Override + public Class getObjectType() { + if (this.proxy != null) { + return this.proxy.getClass(); + } + return this.scopedTargetSource.getTargetClass(); + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyUtils.java b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyUtils.java new file mode 100644 index 0000000..e1899cf --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyUtils.java @@ -0,0 +1,135 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.scope; + +import org.springframework.aop.framework.autoproxy.AutoProxyUtils; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Utility class for creating a scoped proxy. + * + *

Used by ScopedProxyBeanDefinitionDecorator and ClassPathBeanDefinitionScanner. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @author Rob Harrop + * @author Sam Brannen + * @since 2.5 + */ +public abstract class ScopedProxyUtils { + + private static final String TARGET_NAME_PREFIX = "scopedTarget."; + + private static final int TARGET_NAME_PREFIX_LENGTH = TARGET_NAME_PREFIX.length(); + + + /** + * Generate a scoped proxy for the supplied target bean, registering the target + * bean with an internal name and setting 'targetBeanName' on the scoped proxy. + * @param definition the original bean definition + * @param registry the bean definition registry + * @param proxyTargetClass whether to create a target class proxy + * @return the scoped proxy definition + * @see #getTargetBeanName(String) + * @see #getOriginalBeanName(String) + */ + public static BeanDefinitionHolder createScopedProxy(BeanDefinitionHolder definition, + BeanDefinitionRegistry registry, boolean proxyTargetClass) { + + String originalBeanName = definition.getBeanName(); + BeanDefinition targetDefinition = definition.getBeanDefinition(); + String targetBeanName = getTargetBeanName(originalBeanName); + + // Create a scoped proxy definition for the original bean name, + // "hiding" the target bean in an internal target definition. + RootBeanDefinition proxyDefinition = new RootBeanDefinition(ScopedProxyFactoryBean.class); + proxyDefinition.setDecoratedDefinition(new BeanDefinitionHolder(targetDefinition, targetBeanName)); + proxyDefinition.setOriginatingBeanDefinition(targetDefinition); + proxyDefinition.setSource(definition.getSource()); + proxyDefinition.setRole(targetDefinition.getRole()); + + proxyDefinition.getPropertyValues().add("targetBeanName", targetBeanName); + if (proxyTargetClass) { + targetDefinition.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE); + // ScopedProxyFactoryBean's "proxyTargetClass" default is TRUE, so we don't need to set it explicitly here. + } + else { + proxyDefinition.getPropertyValues().add("proxyTargetClass", Boolean.FALSE); + } + + // Copy autowire settings from original bean definition. + proxyDefinition.setAutowireCandidate(targetDefinition.isAutowireCandidate()); + proxyDefinition.setPrimary(targetDefinition.isPrimary()); + if (targetDefinition instanceof AbstractBeanDefinition) { + proxyDefinition.copyQualifiersFrom((AbstractBeanDefinition) targetDefinition); + } + + // The target bean should be ignored in favor of the scoped proxy. + targetDefinition.setAutowireCandidate(false); + targetDefinition.setPrimary(false); + + // Register the target bean as separate bean in the factory. + registry.registerBeanDefinition(targetBeanName, targetDefinition); + + // Return the scoped proxy definition as primary bean definition + // (potentially an inner bean). + return new BeanDefinitionHolder(proxyDefinition, originalBeanName, definition.getAliases()); + } + + /** + * Generate the bean name that is used within the scoped proxy to reference the target bean. + * @param originalBeanName the original name of bean + * @return the generated bean to be used to reference the target bean + * @see #getOriginalBeanName(String) + */ + public static String getTargetBeanName(String originalBeanName) { + return TARGET_NAME_PREFIX + originalBeanName; + } + + /** + * Get the original bean name for the provided {@linkplain #getTargetBeanName + * target bean name}. + * @param targetBeanName the target bean name for the scoped proxy + * @return the original bean name + * @throws IllegalArgumentException if the supplied bean name does not refer + * to the target of a scoped proxy + * @since 5.1.10 + * @see #getTargetBeanName(String) + * @see #isScopedTarget(String) + */ + public static String getOriginalBeanName(@Nullable String targetBeanName) { + Assert.isTrue(isScopedTarget(targetBeanName), () -> "bean name '" + + targetBeanName + "' does not refer to the target of a scoped proxy"); + return targetBeanName.substring(TARGET_NAME_PREFIX_LENGTH); + } + + /** + * Determine if the {@code beanName} is the name of a bean that references + * the target bean within a scoped proxy. + * @since 4.1.4 + */ + public static boolean isScopedTarget(@Nullable String beanName) { + return (beanName != null && beanName.startsWith(TARGET_NAME_PREFIX)); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/scope/package-info.java b/spring-aop/src/main/java/org/springframework/aop/scope/package-info.java new file mode 100644 index 0000000..443f903 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/scope/package-info.java @@ -0,0 +1,9 @@ +/** + * Support for AOP-based scoping of target objects, with configurable backend. + */ +@NonNullApi +@NonNullFields +package org.springframework.aop.scope; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-aop/src/main/java/org/springframework/aop/support/AbstractBeanFactoryPointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/support/AbstractBeanFactoryPointcutAdvisor.java new file mode 100644 index 0000000..ac69e39 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/AbstractBeanFactoryPointcutAdvisor.java @@ -0,0 +1,161 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import java.io.IOException; +import java.io.ObjectInputStream; + +import org.aopalliance.aop.Advice; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Abstract BeanFactory-based PointcutAdvisor that allows for any Advice + * to be configured as reference to an Advice bean in a BeanFactory. + * + *

Specifying the name of an advice bean instead of the advice object itself + * (if running within a BeanFactory) increases loose coupling at initialization time, + * in order to not initialize the advice object until the pointcut actually matches. + * + * @author Juergen Hoeller + * @since 2.0.2 + * @see #setAdviceBeanName + * @see DefaultBeanFactoryPointcutAdvisor + */ +@SuppressWarnings("serial") +public abstract class AbstractBeanFactoryPointcutAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware { + + @Nullable + private String adviceBeanName; + + @Nullable + private BeanFactory beanFactory; + + @Nullable + private transient volatile Advice advice; + + private transient volatile Object adviceMonitor = new Object(); + + + /** + * Specify the name of the advice bean that this advisor should refer to. + *

An instance of the specified bean will be obtained on first access + * of this advisor's advice. This advisor will only ever obtain at most one + * single instance of the advice bean, caching the instance for the lifetime + * of the advisor. + * @see #getAdvice() + */ + public void setAdviceBeanName(@Nullable String adviceBeanName) { + this.adviceBeanName = adviceBeanName; + } + + /** + * Return the name of the advice bean that this advisor refers to, if any. + */ + @Nullable + public String getAdviceBeanName() { + return this.adviceBeanName; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + resetAdviceMonitor(); + } + + private void resetAdviceMonitor() { + if (this.beanFactory instanceof ConfigurableBeanFactory) { + this.adviceMonitor = ((ConfigurableBeanFactory) this.beanFactory).getSingletonMutex(); + } + else { + this.adviceMonitor = new Object(); + } + } + + /** + * Specify a particular instance of the target advice directly, + * avoiding lazy resolution in {@link #getAdvice()}. + * @since 3.1 + */ + public void setAdvice(Advice advice) { + synchronized (this.adviceMonitor) { + this.advice = advice; + } + } + + @Override + public Advice getAdvice() { + Advice advice = this.advice; + if (advice != null) { + return advice; + } + + Assert.state(this.adviceBeanName != null, "'adviceBeanName' must be specified"); + Assert.state(this.beanFactory != null, "BeanFactory must be set to resolve 'adviceBeanName'"); + + if (this.beanFactory.isSingleton(this.adviceBeanName)) { + // Rely on singleton semantics provided by the factory. + advice = this.beanFactory.getBean(this.adviceBeanName, Advice.class); + this.advice = advice; + return advice; + } + else { + // No singleton guarantees from the factory -> let's lock locally but + // reuse the factory's singleton lock, just in case a lazy dependency + // of our advice bean happens to trigger the singleton lock implicitly... + synchronized (this.adviceMonitor) { + advice = this.advice; + if (advice == null) { + advice = this.beanFactory.getBean(this.adviceBeanName, Advice.class); + this.advice = advice; + } + return advice; + } + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(getClass().getName()); + sb.append(": advice "); + if (this.adviceBeanName != null) { + sb.append("bean '").append(this.adviceBeanName).append("'"); + } + else { + sb.append(this.advice); + } + return sb.toString(); + } + + + //--------------------------------------------------------------------- + // Serialization support + //--------------------------------------------------------------------- + + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + // Rely on default serialization, just initialize state after deserialization. + ois.defaultReadObject(); + + // Initialize transient fields. + resetAdviceMonitor(); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/AbstractExpressionPointcut.java b/spring-aop/src/main/java/org/springframework/aop/support/AbstractExpressionPointcut.java new file mode 100644 index 0000000..5330f2c --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/AbstractExpressionPointcut.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import java.io.Serializable; + +import org.springframework.lang.Nullable; + +/** + * Abstract superclass for expression pointcuts, + * offering location and expression properties. + * + * @author Rod Johnson + * @author Rob Harrop + * @since 2.0 + * @see #setLocation + * @see #setExpression + */ +@SuppressWarnings("serial") +public abstract class AbstractExpressionPointcut implements ExpressionPointcut, Serializable { + + @Nullable + private String location; + + @Nullable + private String expression; + + + /** + * Set the location for debugging. + */ + public void setLocation(@Nullable String location) { + this.location = location; + } + + /** + * Return location information about the pointcut expression + * if available. This is useful in debugging. + * @return location information as a human-readable String, + * or {@code null} if none is available + */ + @Nullable + public String getLocation() { + return this.location; + } + + public void setExpression(@Nullable String expression) { + this.expression = expression; + try { + onSetExpression(expression); + } + catch (IllegalArgumentException ex) { + // Fill in location information if possible. + if (this.location != null) { + throw new IllegalArgumentException("Invalid expression at location [" + this.location + "]: " + ex); + } + else { + throw ex; + } + } + } + + /** + * Called when a new pointcut expression is set. + * The expression should be parsed at this point if possible. + *

This implementation is empty. + * @param expression the expression to set + * @throws IllegalArgumentException if the expression is invalid + * @see #setExpression + */ + protected void onSetExpression(@Nullable String expression) throws IllegalArgumentException { + } + + /** + * Return this pointcut's expression. + */ + @Override + @Nullable + public String getExpression() { + return this.expression; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/AbstractGenericPointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/support/AbstractGenericPointcutAdvisor.java new file mode 100644 index 0000000..6243223 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/AbstractGenericPointcutAdvisor.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import org.aopalliance.aop.Advice; + +/** + * Abstract generic {@link org.springframework.aop.PointcutAdvisor} + * that allows for any {@link Advice} to be configured. + * + * @author Juergen Hoeller + * @since 2.0 + * @see #setAdvice + * @see DefaultPointcutAdvisor + */ +@SuppressWarnings("serial") +public abstract class AbstractGenericPointcutAdvisor extends AbstractPointcutAdvisor { + + private Advice advice = EMPTY_ADVICE; + + + /** + * Specify the advice that this advisor should apply. + */ + public void setAdvice(Advice advice) { + this.advice = advice; + } + + @Override + public Advice getAdvice() { + return this.advice; + } + + + @Override + public String toString() { + return getClass().getName() + ": advice [" + getAdvice() + "]"; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/AbstractPointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/support/AbstractPointcutAdvisor.java new file mode 100644 index 0000000..7b7c95a --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/AbstractPointcutAdvisor.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import java.io.Serializable; + +import org.aopalliance.aop.Advice; + +import org.springframework.aop.PointcutAdvisor; +import org.springframework.core.Ordered; +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +/** + * Abstract base class for {@link org.springframework.aop.PointcutAdvisor} + * implementations. Can be subclassed for returning a specific pointcut/advice + * or a freely configurable pointcut/advice. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 1.1.2 + * @see AbstractGenericPointcutAdvisor + */ +@SuppressWarnings("serial") +public abstract class AbstractPointcutAdvisor implements PointcutAdvisor, Ordered, Serializable { + + @Nullable + private Integer order; + + + public void setOrder(int order) { + this.order = order; + } + + @Override + public int getOrder() { + if (this.order != null) { + return this.order; + } + Advice advice = getAdvice(); + if (advice instanceof Ordered) { + return ((Ordered) advice).getOrder(); + } + return Ordered.LOWEST_PRECEDENCE; + } + + @Override + public boolean isPerInstance() { + return true; + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof PointcutAdvisor)) { + return false; + } + PointcutAdvisor otherAdvisor = (PointcutAdvisor) other; + return (ObjectUtils.nullSafeEquals(getAdvice(), otherAdvisor.getAdvice()) && + ObjectUtils.nullSafeEquals(getPointcut(), otherAdvisor.getPointcut())); + } + + @Override + public int hashCode() { + return PointcutAdvisor.class.hashCode(); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/AbstractRegexpMethodPointcut.java b/spring-aop/src/main/java/org/springframework/aop/support/AbstractRegexpMethodPointcut.java new file mode 100644 index 0000000..a6ca47c --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/AbstractRegexpMethodPointcut.java @@ -0,0 +1,229 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Arrays; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Abstract base regular expression pointcut bean. JavaBean properties are: + *

    + *
  • pattern: regular expression for the fully-qualified method names to match. + * The exact regexp syntax will depend on the subclass (e.g. Perl5 regular expressions) + *
  • patterns: alternative property taking a String array of patterns. + * The result will be the union of these patterns. + *
+ * + *

Note: the regular expressions must be a match. For example, + * {@code .*get.*} will match com.mycom.Foo.getBar(). + * {@code get.*} will not. + * + *

This base class is serializable. Subclasses should declare all fields transient; + * the {@link #initPatternRepresentation} method will be invoked again on deserialization. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Rob Harrop + * @since 1.1 + * @see JdkRegexpMethodPointcut + */ +@SuppressWarnings("serial") +public abstract class AbstractRegexpMethodPointcut extends StaticMethodMatcherPointcut + implements Serializable { + + /** + * Regular expressions to match. + */ + private String[] patterns = new String[0]; + + /** + * Regular expressions not to match. + */ + private String[] excludedPatterns = new String[0]; + + + /** + * Convenience method when we have only a single pattern. + * Use either this method or {@link #setPatterns}, not both. + * @see #setPatterns + */ + public void setPattern(String pattern) { + setPatterns(pattern); + } + + /** + * Set the regular expressions defining methods to match. + * Matching will be the union of all these; if any match, the pointcut matches. + * @see #setPattern + */ + public void setPatterns(String... patterns) { + Assert.notEmpty(patterns, "'patterns' must not be empty"); + this.patterns = new String[patterns.length]; + for (int i = 0; i < patterns.length; i++) { + this.patterns[i] = StringUtils.trimWhitespace(patterns[i]); + } + initPatternRepresentation(this.patterns); + } + + /** + * Return the regular expressions for method matching. + */ + public String[] getPatterns() { + return this.patterns; + } + + /** + * Convenience method when we have only a single exclusion pattern. + * Use either this method or {@link #setExcludedPatterns}, not both. + * @see #setExcludedPatterns + */ + public void setExcludedPattern(String excludedPattern) { + setExcludedPatterns(excludedPattern); + } + + /** + * Set the regular expressions defining methods to match for exclusion. + * Matching will be the union of all these; if any match, the pointcut matches. + * @see #setExcludedPattern + */ + public void setExcludedPatterns(String... excludedPatterns) { + Assert.notEmpty(excludedPatterns, "'excludedPatterns' must not be empty"); + this.excludedPatterns = new String[excludedPatterns.length]; + for (int i = 0; i < excludedPatterns.length; i++) { + this.excludedPatterns[i] = StringUtils.trimWhitespace(excludedPatterns[i]); + } + initExcludedPatternRepresentation(this.excludedPatterns); + } + + /** + * Returns the regular expressions for exclusion matching. + */ + public String[] getExcludedPatterns() { + return this.excludedPatterns; + } + + + /** + * Try to match the regular expression against the fully qualified name + * of the target class as well as against the method's declaring class, + * plus the name of the method. + */ + @Override + public boolean matches(Method method, Class targetClass) { + return (matchesPattern(ClassUtils.getQualifiedMethodName(method, targetClass)) || + (targetClass != method.getDeclaringClass() && + matchesPattern(ClassUtils.getQualifiedMethodName(method, method.getDeclaringClass())))); + } + + /** + * Match the specified candidate against the configured patterns. + * @param signatureString "java.lang.Object.hashCode" style signature + * @return whether the candidate matches at least one of the specified patterns + */ + protected boolean matchesPattern(String signatureString) { + for (int i = 0; i < this.patterns.length; i++) { + boolean matched = matches(signatureString, i); + if (matched) { + for (int j = 0; j < this.excludedPatterns.length; j++) { + boolean excluded = matchesExclusion(signatureString, j); + if (excluded) { + return false; + } + } + return true; + } + } + return false; + } + + + /** + * Subclasses must implement this to initialize regexp pointcuts. + * Can be invoked multiple times. + *

This method will be invoked from the {@link #setPatterns} method, + * and also on deserialization. + * @param patterns the patterns to initialize + * @throws IllegalArgumentException in case of an invalid pattern + */ + protected abstract void initPatternRepresentation(String[] patterns) throws IllegalArgumentException; + + /** + * Subclasses must implement this to initialize regexp pointcuts. + * Can be invoked multiple times. + *

This method will be invoked from the {@link #setExcludedPatterns} method, + * and also on deserialization. + * @param patterns the patterns to initialize + * @throws IllegalArgumentException in case of an invalid pattern + */ + protected abstract void initExcludedPatternRepresentation(String[] patterns) throws IllegalArgumentException; + + /** + * Does the pattern at the given index match the given String? + * @param pattern the {@code String} pattern to match + * @param patternIndex index of pattern (starting from 0) + * @return {@code true} if there is a match, {@code false} otherwise + */ + protected abstract boolean matches(String pattern, int patternIndex); + + /** + * Does the exclusion pattern at the given index match the given String? + * @param pattern the {@code String} pattern to match + * @param patternIndex index of pattern (starting from 0) + * @return {@code true} if there is a match, {@code false} otherwise + */ + protected abstract boolean matchesExclusion(String pattern, int patternIndex); + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof AbstractRegexpMethodPointcut)) { + return false; + } + AbstractRegexpMethodPointcut otherPointcut = (AbstractRegexpMethodPointcut) other; + return (Arrays.equals(this.patterns, otherPointcut.patterns) && + Arrays.equals(this.excludedPatterns, otherPointcut.excludedPatterns)); + } + + @Override + public int hashCode() { + int result = 27; + for (String pattern : this.patterns) { + result = 13 * result + pattern.hashCode(); + } + for (String excludedPattern : this.excludedPatterns) { + result = 13 * result + excludedPattern.hashCode(); + } + return result; + } + + @Override + public String toString() { + return getClass().getName() + ": patterns " + ObjectUtils.nullSafeToString(this.patterns) + + ", excluded patterns " + ObjectUtils.nullSafeToString(this.excludedPatterns); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java b/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java new file mode 100644 index 0000000..8055ec9 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java @@ -0,0 +1,360 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.aop.Advisor; +import org.springframework.aop.AopInvocationException; +import org.springframework.aop.IntroductionAdvisor; +import org.springframework.aop.IntroductionAwareMethodMatcher; +import org.springframework.aop.MethodMatcher; +import org.springframework.aop.Pointcut; +import org.springframework.aop.PointcutAdvisor; +import org.springframework.aop.SpringProxy; +import org.springframework.aop.TargetClassAware; +import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.MethodIntrospector; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * Utility methods for AOP support code. + * + *

Mainly for internal use within Spring's AOP support. + * + *

See {@link org.springframework.aop.framework.AopProxyUtils} for a + * collection of framework-specific AOP utility methods which depend + * on internals of Spring's AOP framework implementation. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Rob Harrop + * @see org.springframework.aop.framework.AopProxyUtils + */ +public abstract class AopUtils { + + /** + * Check whether the given object is a JDK dynamic proxy or a CGLIB proxy. + *

This method additionally checks if the given object is an instance + * of {@link SpringProxy}. + * @param object the object to check + * @see #isJdkDynamicProxy + * @see #isCglibProxy + */ + public static boolean isAopProxy(@Nullable Object object) { + return (object instanceof SpringProxy && (Proxy.isProxyClass(object.getClass()) || + object.getClass().getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR))); + } + + /** + * Check whether the given object is a JDK dynamic proxy. + *

This method goes beyond the implementation of + * {@link Proxy#isProxyClass(Class)} by additionally checking if the + * given object is an instance of {@link SpringProxy}. + * @param object the object to check + * @see java.lang.reflect.Proxy#isProxyClass + */ + public static boolean isJdkDynamicProxy(@Nullable Object object) { + return (object instanceof SpringProxy && Proxy.isProxyClass(object.getClass())); + } + + /** + * Check whether the given object is a CGLIB proxy. + *

This method goes beyond the implementation of + * {@link ClassUtils#isCglibProxy(Object)} by additionally checking if + * the given object is an instance of {@link SpringProxy}. + * @param object the object to check + * @see ClassUtils#isCglibProxy(Object) + */ + public static boolean isCglibProxy(@Nullable Object object) { + return (object instanceof SpringProxy && + object.getClass().getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR)); + } + + /** + * Determine the target class of the given bean instance which might be an AOP proxy. + *

Returns the target class for an AOP proxy or the plain class otherwise. + * @param candidate the instance to check (might be an AOP proxy) + * @return the target class (or the plain class of the given object as fallback; + * never {@code null}) + * @see org.springframework.aop.TargetClassAware#getTargetClass() + * @see org.springframework.aop.framework.AopProxyUtils#ultimateTargetClass(Object) + */ + public static Class getTargetClass(Object candidate) { + Assert.notNull(candidate, "Candidate object must not be null"); + Class result = null; + if (candidate instanceof TargetClassAware) { + result = ((TargetClassAware) candidate).getTargetClass(); + } + if (result == null) { + result = (isCglibProxy(candidate) ? candidate.getClass().getSuperclass() : candidate.getClass()); + } + return result; + } + + /** + * Select an invocable method on the target type: either the given method itself + * if actually exposed on the target type, or otherwise a corresponding method + * on one of the target type's interfaces or on the target type itself. + * @param method the method to check + * @param targetType the target type to search methods on (typically an AOP proxy) + * @return a corresponding invocable method on the target type + * @throws IllegalStateException if the given method is not invocable on the given + * target type (typically due to a proxy mismatch) + * @since 4.3 + * @see MethodIntrospector#selectInvocableMethod(Method, Class) + */ + public static Method selectInvocableMethod(Method method, @Nullable Class targetType) { + if (targetType == null) { + return method; + } + Method methodToUse = MethodIntrospector.selectInvocableMethod(method, targetType); + if (Modifier.isPrivate(methodToUse.getModifiers()) && !Modifier.isStatic(methodToUse.getModifiers()) && + SpringProxy.class.isAssignableFrom(targetType)) { + throw new IllegalStateException(String.format( + "Need to invoke method '%s' found on proxy for target class '%s' but cannot " + + "be delegated to target bean. Switch its visibility to package or protected.", + method.getName(), method.getDeclaringClass().getSimpleName())); + } + return methodToUse; + } + + /** + * Determine whether the given method is an "equals" method. + * @see java.lang.Object#equals + */ + public static boolean isEqualsMethod(@Nullable Method method) { + return ReflectionUtils.isEqualsMethod(method); + } + + /** + * Determine whether the given method is a "hashCode" method. + * @see java.lang.Object#hashCode + */ + public static boolean isHashCodeMethod(@Nullable Method method) { + return ReflectionUtils.isHashCodeMethod(method); + } + + /** + * Determine whether the given method is a "toString" method. + * @see java.lang.Object#toString() + */ + public static boolean isToStringMethod(@Nullable Method method) { + return ReflectionUtils.isToStringMethod(method); + } + + /** + * Determine whether the given method is a "finalize" method. + * @see java.lang.Object#finalize() + */ + public static boolean isFinalizeMethod(@Nullable Method method) { + return (method != null && method.getName().equals("finalize") && + method.getParameterCount() == 0); + } + + /** + * Given a method, which may come from an interface, and a target class used + * in the current AOP invocation, find the corresponding target method if there + * is one. E.g. the method may be {@code IFoo.bar()} and the target class + * may be {@code DefaultFoo}. In this case, the method may be + * {@code DefaultFoo.bar()}. This enables attributes on that method to be found. + *

NOTE: In contrast to {@link org.springframework.util.ClassUtils#getMostSpecificMethod}, + * this method resolves Java 5 bridge methods in order to retrieve attributes + * from the original method definition. + * @param method the method to be invoked, which may come from an interface + * @param targetClass the target class for the current invocation. + * May be {@code null} or may not even implement the method. + * @return the specific target method, or the original method if the + * {@code targetClass} doesn't implement it or is {@code null} + * @see org.springframework.util.ClassUtils#getMostSpecificMethod + */ + public static Method getMostSpecificMethod(Method method, @Nullable Class targetClass) { + Class specificTargetClass = (targetClass != null ? ClassUtils.getUserClass(targetClass) : null); + Method resolvedMethod = ClassUtils.getMostSpecificMethod(method, specificTargetClass); + // If we are dealing with method with generic parameters, find the original method. + return BridgeMethodResolver.findBridgedMethod(resolvedMethod); + } + + /** + * Can the given pointcut apply at all on the given class? + *

This is an important test as it can be used to optimize + * out a pointcut for a class. + * @param pc the static or dynamic pointcut to check + * @param targetClass the class to test + * @return whether the pointcut can apply on any method + */ + public static boolean canApply(Pointcut pc, Class targetClass) { + return canApply(pc, targetClass, false); + } + + /** + * Can the given pointcut apply at all on the given class? + *

This is an important test as it can be used to optimize + * out a pointcut for a class. + * @param pc the static or dynamic pointcut to check + * @param targetClass the class to test + * @param hasIntroductions whether or not the advisor chain + * for this bean includes any introductions + * @return whether the pointcut can apply on any method + */ + public static boolean canApply(Pointcut pc, Class targetClass, boolean hasIntroductions) { + Assert.notNull(pc, "Pointcut must not be null"); + if (!pc.getClassFilter().matches(targetClass)) { + return false; + } + + MethodMatcher methodMatcher = pc.getMethodMatcher(); + if (methodMatcher == MethodMatcher.TRUE) { + // No need to iterate the methods if we're matching any method anyway... + return true; + } + + IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null; + if (methodMatcher instanceof IntroductionAwareMethodMatcher) { + introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher; + } + + Set> classes = new LinkedHashSet<>(); + if (!Proxy.isProxyClass(targetClass)) { + classes.add(ClassUtils.getUserClass(targetClass)); + } + classes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetClass)); + + for (Class clazz : classes) { + Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz); + for (Method method : methods) { + if (introductionAwareMethodMatcher != null ? + introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions) : + methodMatcher.matches(method, targetClass)) { + return true; + } + } + } + + return false; + } + + /** + * Can the given advisor apply at all on the given class? + * This is an important test as it can be used to optimize + * out a advisor for a class. + * @param advisor the advisor to check + * @param targetClass class we're testing + * @return whether the pointcut can apply on any method + */ + public static boolean canApply(Advisor advisor, Class targetClass) { + return canApply(advisor, targetClass, false); + } + + /** + * Can the given advisor apply at all on the given class? + *

This is an important test as it can be used to optimize out a advisor for a class. + * This version also takes into account introductions (for IntroductionAwareMethodMatchers). + * @param advisor the advisor to check + * @param targetClass class we're testing + * @param hasIntroductions whether or not the advisor chain for this bean includes + * any introductions + * @return whether the pointcut can apply on any method + */ + public static boolean canApply(Advisor advisor, Class targetClass, boolean hasIntroductions) { + if (advisor instanceof IntroductionAdvisor) { + return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass); + } + else if (advisor instanceof PointcutAdvisor) { + PointcutAdvisor pca = (PointcutAdvisor) advisor; + return canApply(pca.getPointcut(), targetClass, hasIntroductions); + } + else { + // It doesn't have a pointcut so we assume it applies. + return true; + } + } + + /** + * Determine the sublist of the {@code candidateAdvisors} list + * that is applicable to the given class. + * @param candidateAdvisors the Advisors to evaluate + * @param clazz the target class + * @return sublist of Advisors that can apply to an object of the given class + * (may be the incoming List as-is) + */ + public static List findAdvisorsThatCanApply(List candidateAdvisors, Class clazz) { + if (candidateAdvisors.isEmpty()) { + return candidateAdvisors; + } + List eligibleAdvisors = new ArrayList<>(); + for (Advisor candidate : candidateAdvisors) { + if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) { + eligibleAdvisors.add(candidate); + } + } + boolean hasIntroductions = !eligibleAdvisors.isEmpty(); + for (Advisor candidate : candidateAdvisors) { + if (candidate instanceof IntroductionAdvisor) { + // already processed + continue; + } + if (canApply(candidate, clazz, hasIntroductions)) { + eligibleAdvisors.add(candidate); + } + } + return eligibleAdvisors; + } + + /** + * Invoke the given target via reflection, as part of an AOP method invocation. + * @param target the target object + * @param method the method to invoke + * @param args the arguments for the method + * @return the invocation result, if any + * @throws Throwable if thrown by the target method + * @throws org.springframework.aop.AopInvocationException in case of a reflection error + */ + @Nullable + public static Object invokeJoinpointUsingReflection(@Nullable Object target, Method method, Object[] args) + throws Throwable { + + // Use reflection to invoke the method. + try { + ReflectionUtils.makeAccessible(method); + return method.invoke(target, args); + } + catch (InvocationTargetException ex) { + // Invoked method threw a checked exception. + // We must rethrow it. The client won't see the interceptor. + throw ex.getTargetException(); + } + catch (IllegalArgumentException ex) { + throw new AopInvocationException("AOP configuration seems to be invalid: tried calling method [" + + method + "] on target [" + target + "]", ex); + } + catch (IllegalAccessException ex) { + throw new AopInvocationException("Could not access method [" + method + "]", ex); + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/ClassFilters.java b/spring-aop/src/main/java/org/springframework/aop/support/ClassFilters.java new file mode 100644 index 0000000..6f624ca --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/ClassFilters.java @@ -0,0 +1,170 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import java.io.Serializable; +import java.util.Arrays; + +import org.springframework.aop.ClassFilter; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Static utility methods for composing {@link ClassFilter ClassFilters}. + * + * @author Rod Johnson + * @author Rob Harrop + * @author Juergen Hoeller + * @author Sam Brannen + * @since 11.11.2003 + * @see MethodMatchers + * @see Pointcuts + */ +public abstract class ClassFilters { + + /** + * Match all classes that either (or both) of the given ClassFilters matches. + * @param cf1 the first ClassFilter + * @param cf2 the second ClassFilter + * @return a distinct ClassFilter that matches all classes that either + * of the given ClassFilter matches + */ + public static ClassFilter union(ClassFilter cf1, ClassFilter cf2) { + Assert.notNull(cf1, "First ClassFilter must not be null"); + Assert.notNull(cf2, "Second ClassFilter must not be null"); + return new UnionClassFilter(new ClassFilter[] {cf1, cf2}); + } + + /** + * Match all classes that either (or all) of the given ClassFilters matches. + * @param classFilters the ClassFilters to match + * @return a distinct ClassFilter that matches all classes that either + * of the given ClassFilter matches + */ + public static ClassFilter union(ClassFilter[] classFilters) { + Assert.notEmpty(classFilters, "ClassFilter array must not be empty"); + return new UnionClassFilter(classFilters); + } + + /** + * Match all classes that both of the given ClassFilters match. + * @param cf1 the first ClassFilter + * @param cf2 the second ClassFilter + * @return a distinct ClassFilter that matches all classes that both + * of the given ClassFilter match + */ + public static ClassFilter intersection(ClassFilter cf1, ClassFilter cf2) { + Assert.notNull(cf1, "First ClassFilter must not be null"); + Assert.notNull(cf2, "Second ClassFilter must not be null"); + return new IntersectionClassFilter(new ClassFilter[] {cf1, cf2}); + } + + /** + * Match all classes that all of the given ClassFilters match. + * @param classFilters the ClassFilters to match + * @return a distinct ClassFilter that matches all classes that both + * of the given ClassFilter match + */ + public static ClassFilter intersection(ClassFilter[] classFilters) { + Assert.notEmpty(classFilters, "ClassFilter array must not be empty"); + return new IntersectionClassFilter(classFilters); + } + + + /** + * ClassFilter implementation for a union of the given ClassFilters. + */ + @SuppressWarnings("serial") + private static class UnionClassFilter implements ClassFilter, Serializable { + + private final ClassFilter[] filters; + + UnionClassFilter(ClassFilter[] filters) { + this.filters = filters; + } + + @Override + public boolean matches(Class clazz) { + for (ClassFilter filter : this.filters) { + if (filter.matches(clazz)) { + return true; + } + } + return false; + } + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof UnionClassFilter && + ObjectUtils.nullSafeEquals(this.filters, ((UnionClassFilter) other).filters))); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.filters); + } + + @Override + public String toString() { + return getClass().getName() + ": " + Arrays.toString(this.filters); + } + + } + + + /** + * ClassFilter implementation for an intersection of the given ClassFilters. + */ + @SuppressWarnings("serial") + private static class IntersectionClassFilter implements ClassFilter, Serializable { + + private final ClassFilter[] filters; + + IntersectionClassFilter(ClassFilter[] filters) { + this.filters = filters; + } + + @Override + public boolean matches(Class clazz) { + for (ClassFilter filter : this.filters) { + if (!filter.matches(clazz)) { + return false; + } + } + return true; + } + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof IntersectionClassFilter && + ObjectUtils.nullSafeEquals(this.filters, ((IntersectionClassFilter) other).filters))); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.filters); + } + + @Override + public String toString() { + return getClass().getName() + ": " + Arrays.toString(this.filters); + } + + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/ComposablePointcut.java b/spring-aop/src/main/java/org/springframework/aop/support/ComposablePointcut.java new file mode 100644 index 0000000..4bbcaf3 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/ComposablePointcut.java @@ -0,0 +1,209 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import java.io.Serializable; + +import org.springframework.aop.ClassFilter; +import org.springframework.aop.MethodMatcher; +import org.springframework.aop.Pointcut; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Convenient class for building up pointcuts. + * + *

All methods return {@code ComposablePointcut}, so we can use concise idioms + * like in the following example. + * + *

Pointcut pc = new ComposablePointcut()
+ *                      .union(classFilter)
+ *                      .intersection(methodMatcher)
+ *                      .intersection(pointcut);
+ * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Rob Harrop + * @since 11.11.2003 + * @see Pointcuts + */ +public class ComposablePointcut implements Pointcut, Serializable { + + /** use serialVersionUID from Spring 1.2 for interoperability. */ + private static final long serialVersionUID = -2743223737633663832L; + + private ClassFilter classFilter; + + private MethodMatcher methodMatcher; + + + /** + * Create a default ComposablePointcut, with {@code ClassFilter.TRUE} + * and {@code MethodMatcher.TRUE}. + */ + public ComposablePointcut() { + this.classFilter = ClassFilter.TRUE; + this.methodMatcher = MethodMatcher.TRUE; + } + + /** + * Create a ComposablePointcut based on the given Pointcut. + * @param pointcut the original Pointcut + */ + public ComposablePointcut(Pointcut pointcut) { + Assert.notNull(pointcut, "Pointcut must not be null"); + this.classFilter = pointcut.getClassFilter(); + this.methodMatcher = pointcut.getMethodMatcher(); + } + + /** + * Create a ComposablePointcut for the given ClassFilter, + * with {@code MethodMatcher.TRUE}. + * @param classFilter the ClassFilter to use + */ + public ComposablePointcut(ClassFilter classFilter) { + Assert.notNull(classFilter, "ClassFilter must not be null"); + this.classFilter = classFilter; + this.methodMatcher = MethodMatcher.TRUE; + } + + /** + * Create a ComposablePointcut for the given MethodMatcher, + * with {@code ClassFilter.TRUE}. + * @param methodMatcher the MethodMatcher to use + */ + public ComposablePointcut(MethodMatcher methodMatcher) { + Assert.notNull(methodMatcher, "MethodMatcher must not be null"); + this.classFilter = ClassFilter.TRUE; + this.methodMatcher = methodMatcher; + } + + /** + * Create a ComposablePointcut for the given ClassFilter and MethodMatcher. + * @param classFilter the ClassFilter to use + * @param methodMatcher the MethodMatcher to use + */ + public ComposablePointcut(ClassFilter classFilter, MethodMatcher methodMatcher) { + Assert.notNull(classFilter, "ClassFilter must not be null"); + Assert.notNull(methodMatcher, "MethodMatcher must not be null"); + this.classFilter = classFilter; + this.methodMatcher = methodMatcher; + } + + + /** + * Apply a union with the given ClassFilter. + * @param other the ClassFilter to apply a union with + * @return this composable pointcut (for call chaining) + */ + public ComposablePointcut union(ClassFilter other) { + this.classFilter = ClassFilters.union(this.classFilter, other); + return this; + } + + /** + * Apply an intersection with the given ClassFilter. + * @param other the ClassFilter to apply an intersection with + * @return this composable pointcut (for call chaining) + */ + public ComposablePointcut intersection(ClassFilter other) { + this.classFilter = ClassFilters.intersection(this.classFilter, other); + return this; + } + + /** + * Apply a union with the given MethodMatcher. + * @param other the MethodMatcher to apply a union with + * @return this composable pointcut (for call chaining) + */ + public ComposablePointcut union(MethodMatcher other) { + this.methodMatcher = MethodMatchers.union(this.methodMatcher, other); + return this; + } + + /** + * Apply an intersection with the given MethodMatcher. + * @param other the MethodMatcher to apply an intersection with + * @return this composable pointcut (for call chaining) + */ + public ComposablePointcut intersection(MethodMatcher other) { + this.methodMatcher = MethodMatchers.intersection(this.methodMatcher, other); + return this; + } + + /** + * Apply a union with the given Pointcut. + *

Note that for a Pointcut union, methods will only match if their + * original ClassFilter (from the originating Pointcut) matches as well. + * MethodMatchers and ClassFilters from different Pointcuts will never + * get interleaved with each other. + * @param other the Pointcut to apply a union with + * @return this composable pointcut (for call chaining) + */ + public ComposablePointcut union(Pointcut other) { + this.methodMatcher = MethodMatchers.union( + this.methodMatcher, this.classFilter, other.getMethodMatcher(), other.getClassFilter()); + this.classFilter = ClassFilters.union(this.classFilter, other.getClassFilter()); + return this; + } + + /** + * Apply an intersection with the given Pointcut. + * @param other the Pointcut to apply an intersection with + * @return this composable pointcut (for call chaining) + */ + public ComposablePointcut intersection(Pointcut other) { + this.classFilter = ClassFilters.intersection(this.classFilter, other.getClassFilter()); + this.methodMatcher = MethodMatchers.intersection(this.methodMatcher, other.getMethodMatcher()); + return this; + } + + + @Override + public ClassFilter getClassFilter() { + return this.classFilter; + } + + @Override + public MethodMatcher getMethodMatcher() { + return this.methodMatcher; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof ComposablePointcut)) { + return false; + } + ComposablePointcut otherPointcut = (ComposablePointcut) other; + return (this.classFilter.equals(otherPointcut.classFilter) && + this.methodMatcher.equals(otherPointcut.methodMatcher)); + } + + @Override + public int hashCode() { + return this.classFilter.hashCode() * 37 + this.methodMatcher.hashCode(); + } + + @Override + public String toString() { + return getClass().getName() + ": " + this.classFilter + ", " + this.methodMatcher; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/ControlFlowPointcut.java b/spring-aop/src/main/java/org/springframework/aop/support/ControlFlowPointcut.java new file mode 100644 index 0000000..6adeea0 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/ControlFlowPointcut.java @@ -0,0 +1,152 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.concurrent.atomic.AtomicInteger; + +import org.springframework.aop.ClassFilter; +import org.springframework.aop.MethodMatcher; +import org.springframework.aop.Pointcut; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Pointcut and method matcher for use in simple cflow-style pointcut. + * Note that evaluating such pointcuts is 10-15 times slower than evaluating + * normal pointcuts, but they are useful in some cases. + * + * @author Rod Johnson + * @author Rob Harrop + * @author Juergen Hoeller + * @author Sam Brannen + */ +@SuppressWarnings("serial") +public class ControlFlowPointcut implements Pointcut, ClassFilter, MethodMatcher, Serializable { + + private final Class clazz; + + @Nullable + private final String methodName; + + private final AtomicInteger evaluations = new AtomicInteger(); + + + /** + * Construct a new pointcut that matches all control flows below that class. + * @param clazz the clazz + */ + public ControlFlowPointcut(Class clazz) { + this(clazz, null); + } + + /** + * Construct a new pointcut that matches all calls below the given method + * in the given class. If no method name is given, matches all control flows + * below the given class. + * @param clazz the clazz + * @param methodName the name of the method (may be {@code null}) + */ + public ControlFlowPointcut(Class clazz, @Nullable String methodName) { + Assert.notNull(clazz, "Class must not be null"); + this.clazz = clazz; + this.methodName = methodName; + } + + + /** + * Subclasses can override this for greater filtering (and performance). + */ + @Override + public boolean matches(Class clazz) { + return true; + } + + /** + * Subclasses can override this if it's possible to filter out some candidate classes. + */ + @Override + public boolean matches(Method method, Class targetClass) { + return true; + } + + @Override + public boolean isRuntime() { + return true; + } + + @Override + public boolean matches(Method method, Class targetClass, Object... args) { + this.evaluations.incrementAndGet(); + + for (StackTraceElement element : new Throwable().getStackTrace()) { + if (element.getClassName().equals(this.clazz.getName()) && + (this.methodName == null || element.getMethodName().equals(this.methodName))) { + return true; + } + } + return false; + } + + /** + * It's useful to know how many times we've fired, for optimization. + */ + public int getEvaluations() { + return this.evaluations.get(); + } + + + @Override + public ClassFilter getClassFilter() { + return this; + } + + @Override + public MethodMatcher getMethodMatcher() { + return this; + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof ControlFlowPointcut)) { + return false; + } + ControlFlowPointcut that = (ControlFlowPointcut) other; + return (this.clazz.equals(that.clazz)) && ObjectUtils.nullSafeEquals(this.methodName, that.methodName); + } + + @Override + public int hashCode() { + int code = this.clazz.hashCode(); + if (this.methodName != null) { + code = 37 * code + this.methodName.hashCode(); + } + return code; + } + + @Override + public String toString() { + return getClass().getName() + ": class = " + this.clazz.getName() + "; methodName = " + methodName; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/DefaultBeanFactoryPointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/support/DefaultBeanFactoryPointcutAdvisor.java new file mode 100644 index 0000000..ce68704 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/DefaultBeanFactoryPointcutAdvisor.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import org.springframework.aop.Pointcut; +import org.springframework.lang.Nullable; + +/** + * Concrete BeanFactory-based PointcutAdvisor that allows for any Advice + * to be configured as reference to an Advice bean in the BeanFactory, + * as well as the Pointcut to be configured through a bean property. + * + *

Specifying the name of an advice bean instead of the advice object itself + * (if running within a BeanFactory) increases loose coupling at initialization time, + * in order to not initialize the advice object until the pointcut actually matches. + * + * @author Juergen Hoeller + * @since 2.0.2 + * @see #setPointcut + * @see #setAdviceBeanName + */ +@SuppressWarnings("serial") +public class DefaultBeanFactoryPointcutAdvisor extends AbstractBeanFactoryPointcutAdvisor { + + private Pointcut pointcut = Pointcut.TRUE; + + + /** + * Specify the pointcut targeting the advice. + *

Default is {@code Pointcut.TRUE}. + * @see #setAdviceBeanName + */ + public void setPointcut(@Nullable Pointcut pointcut) { + this.pointcut = (pointcut != null ? pointcut : Pointcut.TRUE); + } + + @Override + public Pointcut getPointcut() { + return this.pointcut; + } + + + @Override + public String toString() { + return getClass().getName() + ": pointcut [" + getPointcut() + "]; advice bean '" + getAdviceBeanName() + "'"; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/DefaultIntroductionAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/support/DefaultIntroductionAdvisor.java new file mode 100644 index 0000000..d76cdd7 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/DefaultIntroductionAdvisor.java @@ -0,0 +1,175 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import java.io.Serializable; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.aopalliance.aop.Advice; + +import org.springframework.aop.ClassFilter; +import org.springframework.aop.DynamicIntroductionAdvice; +import org.springframework.aop.IntroductionAdvisor; +import org.springframework.aop.IntroductionInfo; +import org.springframework.core.Ordered; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Simple {@link org.springframework.aop.IntroductionAdvisor} implementation + * that by default applies to any class. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 11.11.2003 + */ +@SuppressWarnings("serial") +public class DefaultIntroductionAdvisor implements IntroductionAdvisor, ClassFilter, Ordered, Serializable { + + private final Advice advice; + + private final Set> interfaces = new LinkedHashSet<>(); + + private int order = Ordered.LOWEST_PRECEDENCE; + + + /** + * Create a DefaultIntroductionAdvisor for the given advice. + * @param advice the Advice to apply (may implement the + * {@link org.springframework.aop.IntroductionInfo} interface) + * @see #addInterface + */ + public DefaultIntroductionAdvisor(Advice advice) { + this(advice, (advice instanceof IntroductionInfo ? (IntroductionInfo) advice : null)); + } + + /** + * Create a DefaultIntroductionAdvisor for the given advice. + * @param advice the Advice to apply + * @param introductionInfo the IntroductionInfo that describes + * the interface to introduce (may be {@code null}) + */ + public DefaultIntroductionAdvisor(Advice advice, @Nullable IntroductionInfo introductionInfo) { + Assert.notNull(advice, "Advice must not be null"); + this.advice = advice; + if (introductionInfo != null) { + Class[] introducedInterfaces = introductionInfo.getInterfaces(); + if (introducedInterfaces.length == 0) { + throw new IllegalArgumentException("IntroductionAdviceSupport implements no interfaces"); + } + for (Class ifc : introducedInterfaces) { + addInterface(ifc); + } + } + } + + /** + * Create a DefaultIntroductionAdvisor for the given advice. + * @param advice the Advice to apply + * @param ifc the interface to introduce + */ + public DefaultIntroductionAdvisor(DynamicIntroductionAdvice advice, Class ifc) { + Assert.notNull(advice, "Advice must not be null"); + this.advice = advice; + addInterface(ifc); + } + + + /** + * Add the specified interface to the list of interfaces to introduce. + * @param ifc the interface to introduce + */ + public void addInterface(Class ifc) { + Assert.notNull(ifc, "Interface must not be null"); + if (!ifc.isInterface()) { + throw new IllegalArgumentException("Specified class [" + ifc.getName() + "] must be an interface"); + } + this.interfaces.add(ifc); + } + + @Override + public Class[] getInterfaces() { + return ClassUtils.toClassArray(this.interfaces); + } + + @Override + public void validateInterfaces() throws IllegalArgumentException { + for (Class ifc : this.interfaces) { + if (this.advice instanceof DynamicIntroductionAdvice && + !((DynamicIntroductionAdvice) this.advice).implementsInterface(ifc)) { + throw new IllegalArgumentException("DynamicIntroductionAdvice [" + this.advice + "] " + + "does not implement interface [" + ifc.getName() + "] specified for introduction"); + } + } + } + + public void setOrder(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return this.order; + } + + @Override + public Advice getAdvice() { + return this.advice; + } + + @Override + public boolean isPerInstance() { + return true; + } + + @Override + public ClassFilter getClassFilter() { + return this; + } + + @Override + public boolean matches(Class clazz) { + return true; + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof DefaultIntroductionAdvisor)) { + return false; + } + DefaultIntroductionAdvisor otherAdvisor = (DefaultIntroductionAdvisor) other; + return (this.advice.equals(otherAdvisor.advice) && this.interfaces.equals(otherAdvisor.interfaces)); + } + + @Override + public int hashCode() { + return this.advice.hashCode() * 13 + this.interfaces.hashCode(); + } + + @Override + public String toString() { + return getClass().getName() + ": advice [" + this.advice + "]; interfaces " + + ClassUtils.classNamesToString(this.interfaces); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/DefaultPointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/support/DefaultPointcutAdvisor.java new file mode 100644 index 0000000..0f3ded0 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/DefaultPointcutAdvisor.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import java.io.Serializable; + +import org.aopalliance.aop.Advice; + +import org.springframework.aop.Pointcut; +import org.springframework.lang.Nullable; + +/** + * Convenient Pointcut-driven Advisor implementation. + * + *

This is the most commonly used Advisor implementation. It can be used + * with any pointcut and advice type, except for introductions. There is + * normally no need to subclass this class, or to implement custom Advisors. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #setPointcut + * @see #setAdvice + */ +@SuppressWarnings("serial") +public class DefaultPointcutAdvisor extends AbstractGenericPointcutAdvisor implements Serializable { + + private Pointcut pointcut = Pointcut.TRUE; + + + /** + * Create an empty DefaultPointcutAdvisor. + *

Advice must be set before use using setter methods. + * Pointcut will normally be set also, but defaults to {@code Pointcut.TRUE}. + */ + public DefaultPointcutAdvisor() { + } + + /** + * Create a DefaultPointcutAdvisor that matches all methods. + *

{@code Pointcut.TRUE} will be used as Pointcut. + * @param advice the Advice to use + */ + public DefaultPointcutAdvisor(Advice advice) { + this(Pointcut.TRUE, advice); + } + + /** + * Create a DefaultPointcutAdvisor, specifying Pointcut and Advice. + * @param pointcut the Pointcut targeting the Advice + * @param advice the Advice to run when Pointcut matches + */ + public DefaultPointcutAdvisor(Pointcut pointcut, Advice advice) { + this.pointcut = pointcut; + setAdvice(advice); + } + + + /** + * Specify the pointcut targeting the advice. + *

Default is {@code Pointcut.TRUE}. + * @see #setAdvice + */ + public void setPointcut(@Nullable Pointcut pointcut) { + this.pointcut = (pointcut != null ? pointcut : Pointcut.TRUE); + } + + @Override + public Pointcut getPointcut() { + return this.pointcut; + } + + + @Override + public String toString() { + return getClass().getName() + ": pointcut [" + getPointcut() + "]; advice [" + getAdvice() + "]"; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/DelegatePerTargetObjectIntroductionInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/support/DelegatePerTargetObjectIntroductionInterceptor.java new file mode 100644 index 0000000..e55cf0b --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/DelegatePerTargetObjectIntroductionInterceptor.java @@ -0,0 +1,146 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import java.util.Map; +import java.util.WeakHashMap; + +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.aop.DynamicIntroductionAdvice; +import org.springframework.aop.IntroductionInterceptor; +import org.springframework.aop.ProxyMethodInvocation; +import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; + +/** + * Convenient implementation of the + * {@link org.springframework.aop.IntroductionInterceptor} interface. + * + *

This differs from {@link DelegatingIntroductionInterceptor} in that a single + * instance of this class can be used to advise multiple target objects, and each target + * object will have its own delegate (whereas DelegatingIntroductionInterceptor + * shares the same delegate, and hence the same state across all targets). + * + *

The {@code suppressInterface} method can be used to suppress interfaces + * implemented by the delegate class but which should not be introduced to the + * owning AOP proxy. + * + *

An instance of this class is serializable if the delegates are. + * + *

Note: There are some implementation similarities between this class and + * {@link DelegatingIntroductionInterceptor} that suggest a possible refactoring + * to extract a common ancestor class in the future. + * + * @author Adrian Colyer + * @author Juergen Hoeller + * @since 2.0 + * @see #suppressInterface + * @see DelegatingIntroductionInterceptor + */ +@SuppressWarnings("serial") +public class DelegatePerTargetObjectIntroductionInterceptor extends IntroductionInfoSupport + implements IntroductionInterceptor { + + /** + * Hold weak references to keys as we don't want to interfere with garbage collection.. + */ + private final Map delegateMap = new WeakHashMap<>(); + + private Class defaultImplType; + + private Class interfaceType; + + + public DelegatePerTargetObjectIntroductionInterceptor(Class defaultImplType, Class interfaceType) { + this.defaultImplType = defaultImplType; + this.interfaceType = interfaceType; + // Create a new delegate now (but don't store it in the map). + // We do this for two reasons: + // 1) to fail early if there is a problem instantiating delegates + // 2) to populate the interface map once and once only + Object delegate = createNewDelegate(); + implementInterfacesOnObject(delegate); + suppressInterface(IntroductionInterceptor.class); + suppressInterface(DynamicIntroductionAdvice.class); + } + + + /** + * Subclasses may need to override this if they want to perform custom + * behaviour in around advice. However, subclasses should invoke this + * method, which handles introduced interfaces and forwarding to the target. + */ + @Override + @Nullable + public Object invoke(MethodInvocation mi) throws Throwable { + if (isMethodOnIntroducedInterface(mi)) { + Object delegate = getIntroductionDelegateFor(mi.getThis()); + + // Using the following method rather than direct reflection, + // we get correct handling of InvocationTargetException + // if the introduced method throws an exception. + Object retVal = AopUtils.invokeJoinpointUsingReflection(delegate, mi.getMethod(), mi.getArguments()); + + // Massage return value if possible: if the delegate returned itself, + // we really want to return the proxy. + if (retVal == delegate && mi instanceof ProxyMethodInvocation) { + retVal = ((ProxyMethodInvocation) mi).getProxy(); + } + return retVal; + } + + return doProceed(mi); + } + + /** + * Proceed with the supplied {@link org.aopalliance.intercept.MethodInterceptor}. + * Subclasses can override this method to intercept method invocations on the + * target object which is useful when an introduction needs to monitor the object + * that it is introduced into. This method is never called for + * {@link MethodInvocation MethodInvocations} on the introduced interfaces. + */ + @Nullable + protected Object doProceed(MethodInvocation mi) throws Throwable { + // If we get here, just pass the invocation on. + return mi.proceed(); + } + + private Object getIntroductionDelegateFor(@Nullable Object targetObject) { + synchronized (this.delegateMap) { + if (this.delegateMap.containsKey(targetObject)) { + return this.delegateMap.get(targetObject); + } + else { + Object delegate = createNewDelegate(); + this.delegateMap.put(targetObject, delegate); + return delegate; + } + } + } + + private Object createNewDelegate() { + try { + return ReflectionUtils.accessibleConstructor(this.defaultImplType).newInstance(); + } + catch (Throwable ex) { + throw new IllegalArgumentException("Cannot create default implementation for '" + + this.interfaceType.getName() + "' mixin (" + this.defaultImplType.getName() + "): " + ex); + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/DelegatingIntroductionInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/support/DelegatingIntroductionInterceptor.java new file mode 100644 index 0000000..c7c5981 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/DelegatingIntroductionInterceptor.java @@ -0,0 +1,140 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.aop.DynamicIntroductionAdvice; +import org.springframework.aop.IntroductionInterceptor; +import org.springframework.aop.ProxyMethodInvocation; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Convenient implementation of the + * {@link org.springframework.aop.IntroductionInterceptor} interface. + * + *

Subclasses merely need to extend this class and implement the interfaces + * to be introduced themselves. In this case the delegate is the subclass + * instance itself. Alternatively a separate delegate may implement the + * interface, and be set via the delegate bean property. + * + *

Delegates or subclasses may implement any number of interfaces. + * All interfaces except IntroductionInterceptor are picked up from + * the subclass or delegate by default. + * + *

The {@code suppressInterface} method can be used to suppress interfaces + * implemented by the delegate but which should not be introduced to the owning + * AOP proxy. + * + *

An instance of this class is serializable if the delegate is. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 16.11.2003 + * @see #suppressInterface + * @see DelegatePerTargetObjectIntroductionInterceptor + */ +@SuppressWarnings("serial") +public class DelegatingIntroductionInterceptor extends IntroductionInfoSupport + implements IntroductionInterceptor { + + /** + * Object that actually implements the interfaces. + * May be "this" if a subclass implements the introduced interfaces. + */ + @Nullable + private Object delegate; + + + /** + * Construct a new DelegatingIntroductionInterceptor, providing + * a delegate that implements the interfaces to be introduced. + * @param delegate the delegate that implements the introduced interfaces + */ + public DelegatingIntroductionInterceptor(Object delegate) { + init(delegate); + } + + /** + * Construct a new DelegatingIntroductionInterceptor. + * The delegate will be the subclass, which must implement + * additional interfaces. + */ + protected DelegatingIntroductionInterceptor() { + init(this); + } + + + /** + * Both constructors use this init method, as it is impossible to pass + * a "this" reference from one constructor to another. + * @param delegate the delegate object + */ + private void init(Object delegate) { + Assert.notNull(delegate, "Delegate must not be null"); + this.delegate = delegate; + implementInterfacesOnObject(delegate); + + // We don't want to expose the control interface + suppressInterface(IntroductionInterceptor.class); + suppressInterface(DynamicIntroductionAdvice.class); + } + + + /** + * Subclasses may need to override this if they want to perform custom + * behaviour in around advice. However, subclasses should invoke this + * method, which handles introduced interfaces and forwarding to the target. + */ + @Override + @Nullable + public Object invoke(MethodInvocation mi) throws Throwable { + if (isMethodOnIntroducedInterface(mi)) { + // Using the following method rather than direct reflection, we + // get correct handling of InvocationTargetException + // if the introduced method throws an exception. + Object retVal = AopUtils.invokeJoinpointUsingReflection(this.delegate, mi.getMethod(), mi.getArguments()); + + // Massage return value if possible: if the delegate returned itself, + // we really want to return the proxy. + if (retVal == this.delegate && mi instanceof ProxyMethodInvocation) { + Object proxy = ((ProxyMethodInvocation) mi).getProxy(); + if (mi.getMethod().getReturnType().isInstance(proxy)) { + retVal = proxy; + } + } + return retVal; + } + + return doProceed(mi); + } + + /** + * Proceed with the supplied {@link org.aopalliance.intercept.MethodInterceptor}. + * Subclasses can override this method to intercept method invocations on the + * target object which is useful when an introduction needs to monitor the object + * that it is introduced into. This method is never called for + * {@link MethodInvocation MethodInvocations} on the introduced interfaces. + */ + @Nullable + protected Object doProceed(MethodInvocation mi) throws Throwable { + // If we get here, just pass the invocation on. + return mi.proceed(); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/DynamicMethodMatcher.java b/spring-aop/src/main/java/org/springframework/aop/support/DynamicMethodMatcher.java new file mode 100644 index 0000000..d45e1d9 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/DynamicMethodMatcher.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import java.lang.reflect.Method; + +import org.springframework.aop.MethodMatcher; + +/** + * Convenient abstract superclass for dynamic method matchers, + * which do care about arguments at runtime. + * + * @author Rod Johnson + */ +public abstract class DynamicMethodMatcher implements MethodMatcher { + + @Override + public final boolean isRuntime() { + return true; + } + + /** + * Can override to add preconditions for dynamic matching. This implementation + * always returns true. + */ + @Override + public boolean matches(Method method, Class targetClass) { + return true; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/DynamicMethodMatcherPointcut.java b/spring-aop/src/main/java/org/springframework/aop/support/DynamicMethodMatcherPointcut.java new file mode 100644 index 0000000..d259724 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/DynamicMethodMatcherPointcut.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import org.springframework.aop.ClassFilter; +import org.springframework.aop.MethodMatcher; +import org.springframework.aop.Pointcut; + +/** + * Convenient superclass when we want to force subclasses to + * implement MethodMatcher interface, but subclasses + * will want to be pointcuts. The getClassFilter() method can + * be overridden to customize ClassFilter behaviour as well. + * + * @author Rod Johnson + */ +public abstract class DynamicMethodMatcherPointcut extends DynamicMethodMatcher implements Pointcut { + + @Override + public ClassFilter getClassFilter() { + return ClassFilter.TRUE; + } + + @Override + public final MethodMatcher getMethodMatcher() { + return this; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/ExpressionPointcut.java b/spring-aop/src/main/java/org/springframework/aop/support/ExpressionPointcut.java new file mode 100644 index 0000000..99b76e1 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/ExpressionPointcut.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import org.springframework.aop.Pointcut; +import org.springframework.lang.Nullable; + +/** + * Interface to be implemented by pointcuts that use String expressions. + * + * @author Rob Harrop + * @since 2.0 + */ +public interface ExpressionPointcut extends Pointcut { + + /** + * Return the String expression for this pointcut. + */ + @Nullable + String getExpression(); + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/IntroductionInfoSupport.java b/spring-aop/src/main/java/org/springframework/aop/support/IntroductionInfoSupport.java new file mode 100644 index 0000000..1033375 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/IntroductionInfoSupport.java @@ -0,0 +1,124 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.aop.IntroductionInfo; +import org.springframework.util.ClassUtils; + +/** + * Support for implementations of {@link org.springframework.aop.IntroductionInfo}. + * + *

Allows subclasses to conveniently add all interfaces from a given object, + * and to suppress interfaces that should not be added. Also allows for querying + * all introduced interfaces. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +@SuppressWarnings("serial") +public class IntroductionInfoSupport implements IntroductionInfo, Serializable { + + protected final Set> publishedInterfaces = new LinkedHashSet<>(); + + private transient Map rememberedMethods = new ConcurrentHashMap<>(32); + + + /** + * Suppress the specified interface, which may have been autodetected + * due to the delegate implementing it. Call this method to exclude + * internal interfaces from being visible at the proxy level. + *

Does nothing if the interface is not implemented by the delegate. + * @param ifc the interface to suppress + */ + public void suppressInterface(Class ifc) { + this.publishedInterfaces.remove(ifc); + } + + @Override + public Class[] getInterfaces() { + return ClassUtils.toClassArray(this.publishedInterfaces); + } + + /** + * Check whether the specified interfaces is a published introduction interface. + * @param ifc the interface to check + * @return whether the interface is part of this introduction + */ + public boolean implementsInterface(Class ifc) { + for (Class pubIfc : this.publishedInterfaces) { + if (ifc.isInterface() && ifc.isAssignableFrom(pubIfc)) { + return true; + } + } + return false; + } + + /** + * Publish all interfaces that the given delegate implements at the proxy level. + * @param delegate the delegate object + */ + protected void implementInterfacesOnObject(Object delegate) { + this.publishedInterfaces.addAll(ClassUtils.getAllInterfacesAsSet(delegate)); + } + + /** + * Is this method on an introduced interface? + * @param mi the method invocation + * @return whether the invoked method is on an introduced interface + */ + protected final boolean isMethodOnIntroducedInterface(MethodInvocation mi) { + Boolean rememberedResult = this.rememberedMethods.get(mi.getMethod()); + if (rememberedResult != null) { + return rememberedResult; + } + else { + // Work it out and cache it. + boolean result = implementsInterface(mi.getMethod().getDeclaringClass()); + this.rememberedMethods.put(mi.getMethod(), result); + return result; + } + } + + + //--------------------------------------------------------------------- + // Serialization support + //--------------------------------------------------------------------- + + /** + * This method is implemented only to restore the logger. + * We don't make the logger static as that would mean that subclasses + * would use this class's log category. + */ + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + // Rely on default serialization; just initialize state after deserialization. + ois.defaultReadObject(); + // Initialize transient fields. + this.rememberedMethods = new ConcurrentHashMap<>(32); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/JdkRegexpMethodPointcut.java b/spring-aop/src/main/java/org/springframework/aop/support/JdkRegexpMethodPointcut.java new file mode 100644 index 0000000..162b5cb --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/JdkRegexpMethodPointcut.java @@ -0,0 +1,103 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * Regular expression pointcut based on the {@code java.util.regex} package. + * Supports the following JavaBean properties: + *

    + *
  • pattern: regular expression for the fully-qualified method names to match + *
  • patterns: alternative property taking a String array of patterns. The result will + * be the union of these patterns. + *
+ * + *

Note: the regular expressions must be a match. For example, + * {@code .*get.*} will match com.mycom.Foo.getBar(). + * {@code get.*} will not. + * + * @author Dmitriy Kopylenko + * @author Rob Harrop + * @since 1.1 + */ +@SuppressWarnings("serial") +public class JdkRegexpMethodPointcut extends AbstractRegexpMethodPointcut { + + /** + * Compiled form of the patterns. + */ + private Pattern[] compiledPatterns = new Pattern[0]; + + /** + * Compiled form of the exclusion patterns. + */ + private Pattern[] compiledExclusionPatterns = new Pattern[0]; + + + /** + * Initialize {@link Pattern Patterns} from the supplied {@code String[]}. + */ + @Override + protected void initPatternRepresentation(String[] patterns) throws PatternSyntaxException { + this.compiledPatterns = compilePatterns(patterns); + } + + /** + * Initialize exclusion {@link Pattern Patterns} from the supplied {@code String[]}. + */ + @Override + protected void initExcludedPatternRepresentation(String[] excludedPatterns) throws PatternSyntaxException { + this.compiledExclusionPatterns = compilePatterns(excludedPatterns); + } + + /** + * Returns {@code true} if the {@link Pattern} at index {@code patternIndex} + * matches the supplied candidate {@code String}. + */ + @Override + protected boolean matches(String pattern, int patternIndex) { + Matcher matcher = this.compiledPatterns[patternIndex].matcher(pattern); + return matcher.matches(); + } + + /** + * Returns {@code true} if the exclusion {@link Pattern} at index {@code patternIndex} + * matches the supplied candidate {@code String}. + */ + @Override + protected boolean matchesExclusion(String candidate, int patternIndex) { + Matcher matcher = this.compiledExclusionPatterns[patternIndex].matcher(candidate); + return matcher.matches(); + } + + + /** + * Compiles the supplied {@code String[]} into an array of + * {@link Pattern} objects and returns that array. + */ + private Pattern[] compilePatterns(String[] source) throws PatternSyntaxException { + Pattern[] destination = new Pattern[source.length]; + for (int i = 0; i < source.length; i++) { + destination[i] = Pattern.compile(source[i]); + } + return destination; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/MethodMatchers.java b/spring-aop/src/main/java/org/springframework/aop/support/MethodMatchers.java new file mode 100644 index 0000000..23b4d9b --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/MethodMatchers.java @@ -0,0 +1,354 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import java.io.Serializable; +import java.lang.reflect.Method; + +import org.springframework.aop.ClassFilter; +import org.springframework.aop.IntroductionAwareMethodMatcher; +import org.springframework.aop.MethodMatcher; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Static utility methods for composing {@link MethodMatcher MethodMatchers}. + * + *

A MethodMatcher may be evaluated statically (based on method and target + * class) or need further evaluation dynamically (based on arguments at the + * time of method invocation). + * + * @author Rod Johnson + * @author Rob Harrop + * @author Juergen Hoeller + * @author Sam Brannen + * @since 11.11.2003 + * @see ClassFilters + * @see Pointcuts + */ +public abstract class MethodMatchers { + + /** + * Match all methods that either (or both) of the given MethodMatchers matches. + * @param mm1 the first MethodMatcher + * @param mm2 the second MethodMatcher + * @return a distinct MethodMatcher that matches all methods that either + * of the given MethodMatchers matches + */ + public static MethodMatcher union(MethodMatcher mm1, MethodMatcher mm2) { + return (mm1 instanceof IntroductionAwareMethodMatcher || mm2 instanceof IntroductionAwareMethodMatcher ? + new UnionIntroductionAwareMethodMatcher(mm1, mm2) : new UnionMethodMatcher(mm1, mm2)); + } + + /** + * Match all methods that either (or both) of the given MethodMatchers matches. + * @param mm1 the first MethodMatcher + * @param cf1 the corresponding ClassFilter for the first MethodMatcher + * @param mm2 the second MethodMatcher + * @param cf2 the corresponding ClassFilter for the second MethodMatcher + * @return a distinct MethodMatcher that matches all methods that either + * of the given MethodMatchers matches + */ + static MethodMatcher union(MethodMatcher mm1, ClassFilter cf1, MethodMatcher mm2, ClassFilter cf2) { + return (mm1 instanceof IntroductionAwareMethodMatcher || mm2 instanceof IntroductionAwareMethodMatcher ? + new ClassFilterAwareUnionIntroductionAwareMethodMatcher(mm1, cf1, mm2, cf2) : + new ClassFilterAwareUnionMethodMatcher(mm1, cf1, mm2, cf2)); + } + + /** + * Match all methods that both of the given MethodMatchers match. + * @param mm1 the first MethodMatcher + * @param mm2 the second MethodMatcher + * @return a distinct MethodMatcher that matches all methods that both + * of the given MethodMatchers match + */ + public static MethodMatcher intersection(MethodMatcher mm1, MethodMatcher mm2) { + return (mm1 instanceof IntroductionAwareMethodMatcher || mm2 instanceof IntroductionAwareMethodMatcher ? + new IntersectionIntroductionAwareMethodMatcher(mm1, mm2) : new IntersectionMethodMatcher(mm1, mm2)); + } + + /** + * Apply the given MethodMatcher to the given Method, supporting an + * {@link org.springframework.aop.IntroductionAwareMethodMatcher} + * (if applicable). + * @param mm the MethodMatcher to apply (may be an IntroductionAwareMethodMatcher) + * @param method the candidate method + * @param targetClass the target class + * @param hasIntroductions {@code true} if the object on whose behalf we are + * asking is the subject on one or more introductions; {@code false} otherwise + * @return whether or not this method matches statically + */ + public static boolean matches(MethodMatcher mm, Method method, Class targetClass, boolean hasIntroductions) { + Assert.notNull(mm, "MethodMatcher must not be null"); + return (mm instanceof IntroductionAwareMethodMatcher ? + ((IntroductionAwareMethodMatcher) mm).matches(method, targetClass, hasIntroductions) : + mm.matches(method, targetClass)); + } + + + /** + * MethodMatcher implementation for a union of two given MethodMatchers. + */ + @SuppressWarnings("serial") + private static class UnionMethodMatcher implements MethodMatcher, Serializable { + + protected final MethodMatcher mm1; + + protected final MethodMatcher mm2; + + public UnionMethodMatcher(MethodMatcher mm1, MethodMatcher mm2) { + Assert.notNull(mm1, "First MethodMatcher must not be null"); + Assert.notNull(mm2, "Second MethodMatcher must not be null"); + this.mm1 = mm1; + this.mm2 = mm2; + } + + @Override + public boolean matches(Method method, Class targetClass) { + return (matchesClass1(targetClass) && this.mm1.matches(method, targetClass)) || + (matchesClass2(targetClass) && this.mm2.matches(method, targetClass)); + } + + protected boolean matchesClass1(Class targetClass) { + return true; + } + + protected boolean matchesClass2(Class targetClass) { + return true; + } + + @Override + public boolean isRuntime() { + return this.mm1.isRuntime() || this.mm2.isRuntime(); + } + + @Override + public boolean matches(Method method, Class targetClass, Object... args) { + return this.mm1.matches(method, targetClass, args) || this.mm2.matches(method, targetClass, args); + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof UnionMethodMatcher)) { + return false; + } + UnionMethodMatcher that = (UnionMethodMatcher) other; + return (this.mm1.equals(that.mm1) && this.mm2.equals(that.mm2)); + } + + @Override + public int hashCode() { + return 37 * this.mm1.hashCode() + this.mm2.hashCode(); + } + + @Override + public String toString() { + return getClass().getName() + ": " + this.mm1 + ", " + this.mm2; + } + } + + + /** + * MethodMatcher implementation for a union of two given MethodMatchers + * of which at least one is an IntroductionAwareMethodMatcher. + * @since 5.1 + */ + @SuppressWarnings("serial") + private static class UnionIntroductionAwareMethodMatcher extends UnionMethodMatcher + implements IntroductionAwareMethodMatcher { + + public UnionIntroductionAwareMethodMatcher(MethodMatcher mm1, MethodMatcher mm2) { + super(mm1, mm2); + } + + @Override + public boolean matches(Method method, Class targetClass, boolean hasIntroductions) { + return (matchesClass1(targetClass) && MethodMatchers.matches(this.mm1, method, targetClass, hasIntroductions)) || + (matchesClass2(targetClass) && MethodMatchers.matches(this.mm2, method, targetClass, hasIntroductions)); + } + } + + + /** + * MethodMatcher implementation for a union of two given MethodMatchers, + * supporting an associated ClassFilter per MethodMatcher. + */ + @SuppressWarnings("serial") + private static class ClassFilterAwareUnionMethodMatcher extends UnionMethodMatcher { + + private final ClassFilter cf1; + + private final ClassFilter cf2; + + public ClassFilterAwareUnionMethodMatcher(MethodMatcher mm1, ClassFilter cf1, MethodMatcher mm2, ClassFilter cf2) { + super(mm1, mm2); + this.cf1 = cf1; + this.cf2 = cf2; + } + + @Override + protected boolean matchesClass1(Class targetClass) { + return this.cf1.matches(targetClass); + } + + @Override + protected boolean matchesClass2(Class targetClass) { + return this.cf2.matches(targetClass); + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!super.equals(other)) { + return false; + } + ClassFilter otherCf1 = ClassFilter.TRUE; + ClassFilter otherCf2 = ClassFilter.TRUE; + if (other instanceof ClassFilterAwareUnionMethodMatcher) { + ClassFilterAwareUnionMethodMatcher cfa = (ClassFilterAwareUnionMethodMatcher) other; + otherCf1 = cfa.cf1; + otherCf2 = cfa.cf2; + } + return (this.cf1.equals(otherCf1) && this.cf2.equals(otherCf2)); + } + + @Override + public int hashCode() { + // Allow for matching with regular UnionMethodMatcher by providing same hash... + return super.hashCode(); + } + + @Override + public String toString() { + return getClass().getName() + ": " + this.cf1 + ", " + this.mm1 + ", " + this.cf2 + ", " + this.mm2; + } + } + + + /** + * MethodMatcher implementation for a union of two given MethodMatchers + * of which at least one is an IntroductionAwareMethodMatcher, + * supporting an associated ClassFilter per MethodMatcher. + * @since 5.1 + */ + @SuppressWarnings("serial") + private static class ClassFilterAwareUnionIntroductionAwareMethodMatcher extends ClassFilterAwareUnionMethodMatcher + implements IntroductionAwareMethodMatcher { + + public ClassFilterAwareUnionIntroductionAwareMethodMatcher( + MethodMatcher mm1, ClassFilter cf1, MethodMatcher mm2, ClassFilter cf2) { + + super(mm1, cf1, mm2, cf2); + } + + @Override + public boolean matches(Method method, Class targetClass, boolean hasIntroductions) { + return (matchesClass1(targetClass) && MethodMatchers.matches(this.mm1, method, targetClass, hasIntroductions)) || + (matchesClass2(targetClass) && MethodMatchers.matches(this.mm2, method, targetClass, hasIntroductions)); + } + } + + + /** + * MethodMatcher implementation for an intersection of two given MethodMatchers. + */ + @SuppressWarnings("serial") + private static class IntersectionMethodMatcher implements MethodMatcher, Serializable { + + protected final MethodMatcher mm1; + + protected final MethodMatcher mm2; + + public IntersectionMethodMatcher(MethodMatcher mm1, MethodMatcher mm2) { + Assert.notNull(mm1, "First MethodMatcher must not be null"); + Assert.notNull(mm2, "Second MethodMatcher must not be null"); + this.mm1 = mm1; + this.mm2 = mm2; + } + + @Override + public boolean matches(Method method, Class targetClass) { + return (this.mm1.matches(method, targetClass) && this.mm2.matches(method, targetClass)); + } + + @Override + public boolean isRuntime() { + return (this.mm1.isRuntime() || this.mm2.isRuntime()); + } + + @Override + public boolean matches(Method method, Class targetClass, Object... args) { + // Because a dynamic intersection may be composed of a static and dynamic part, + // we must avoid calling the 3-arg matches method on a dynamic matcher, as + // it will probably be an unsupported operation. + boolean aMatches = (this.mm1.isRuntime() ? + this.mm1.matches(method, targetClass, args) : this.mm1.matches(method, targetClass)); + boolean bMatches = (this.mm2.isRuntime() ? + this.mm2.matches(method, targetClass, args) : this.mm2.matches(method, targetClass)); + return aMatches && bMatches; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof IntersectionMethodMatcher)) { + return false; + } + IntersectionMethodMatcher that = (IntersectionMethodMatcher) other; + return (this.mm1.equals(that.mm1) && this.mm2.equals(that.mm2)); + } + + @Override + public int hashCode() { + return 37 * this.mm1.hashCode() + this.mm2.hashCode(); + } + + @Override + public String toString() { + return getClass().getName() + ": " + this.mm1 + ", " + this.mm2; + } + } + + + /** + * MethodMatcher implementation for an intersection of two given MethodMatchers + * of which at least one is an IntroductionAwareMethodMatcher. + * @since 5.1 + */ + @SuppressWarnings("serial") + private static class IntersectionIntroductionAwareMethodMatcher extends IntersectionMethodMatcher + implements IntroductionAwareMethodMatcher { + + public IntersectionIntroductionAwareMethodMatcher(MethodMatcher mm1, MethodMatcher mm2) { + super(mm1, mm2); + } + + @Override + public boolean matches(Method method, Class targetClass, boolean hasIntroductions) { + return (MethodMatchers.matches(this.mm1, method, targetClass, hasIntroductions) && + MethodMatchers.matches(this.mm2, method, targetClass, hasIntroductions)); + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/NameMatchMethodPointcut.java b/spring-aop/src/main/java/org/springframework/aop/support/NameMatchMethodPointcut.java new file mode 100644 index 0000000..d3d7624 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/NameMatchMethodPointcut.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.lang.Nullable; +import org.springframework.util.PatternMatchUtils; + +/** + * Pointcut bean for simple method name matches, as an alternative to regexp patterns. + * + *

Does not handle overloaded methods: all methods with a given name will be eligible. + * + * @author Juergen Hoeller + * @author Rod Johnson + * @author Rob Harrop + * @since 11.02.2004 + * @see #isMatch + */ +@SuppressWarnings("serial") +public class NameMatchMethodPointcut extends StaticMethodMatcherPointcut implements Serializable { + + private List mappedNames = new ArrayList<>(); + + + /** + * Convenience method when we have only a single method name to match. + * Use either this method or {@code setMappedNames}, not both. + * @see #setMappedNames + */ + public void setMappedName(String mappedName) { + setMappedNames(mappedName); + } + + /** + * Set the method names defining methods to match. + * Matching will be the union of all these; if any match, + * the pointcut matches. + */ + public void setMappedNames(String... mappedNames) { + this.mappedNames = new ArrayList<>(Arrays.asList(mappedNames)); + } + + /** + * Add another eligible method name, in addition to those already named. + * Like the set methods, this method is for use when configuring proxies, + * before a proxy is used. + *

NB: This method does not work after the proxy is in + * use, as advice chains will be cached. + * @param name the name of the additional method that will match + * @return this pointcut to allow for multiple additions in one line + */ + public NameMatchMethodPointcut addMethodName(String name) { + this.mappedNames.add(name); + return this; + } + + + @Override + public boolean matches(Method method, Class targetClass) { + for (String mappedName : this.mappedNames) { + if (mappedName.equals(method.getName()) || isMatch(method.getName(), mappedName)) { + return true; + } + } + return false; + } + + /** + * Return if the given method name matches the mapped name. + *

The default implementation checks for "xxx*", "*xxx" and "*xxx*" matches, + * as well as direct equality. Can be overridden in subclasses. + * @param methodName the method name of the class + * @param mappedName the name in the descriptor + * @return if the names match + * @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String) + */ + protected boolean isMatch(String methodName, String mappedName) { + return PatternMatchUtils.simpleMatch(mappedName, methodName); + } + + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof NameMatchMethodPointcut && + this.mappedNames.equals(((NameMatchMethodPointcut) other).mappedNames))); + } + + @Override + public int hashCode() { + return this.mappedNames.hashCode(); + } + + @Override + public String toString() { + return getClass().getName() + ": " + this.mappedNames; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/NameMatchMethodPointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/support/NameMatchMethodPointcutAdvisor.java new file mode 100644 index 0000000..a7efd81 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/NameMatchMethodPointcutAdvisor.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import org.aopalliance.aop.Advice; + +import org.springframework.aop.ClassFilter; +import org.springframework.aop.Pointcut; + +/** + * Convenient class for name-match method pointcuts that hold an Advice, + * making them an Advisor. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @see NameMatchMethodPointcut + */ +@SuppressWarnings("serial") +public class NameMatchMethodPointcutAdvisor extends AbstractGenericPointcutAdvisor { + + private final NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut(); + + + public NameMatchMethodPointcutAdvisor() { + } + + public NameMatchMethodPointcutAdvisor(Advice advice) { + setAdvice(advice); + } + + + /** + * Set the {@link ClassFilter} to use for this pointcut. + * Default is {@link ClassFilter#TRUE}. + * @see NameMatchMethodPointcut#setClassFilter + */ + public void setClassFilter(ClassFilter classFilter) { + this.pointcut.setClassFilter(classFilter); + } + + /** + * Convenience method when we have only a single method name to match. + * Use either this method or {@code setMappedNames}, not both. + * @see #setMappedNames + * @see NameMatchMethodPointcut#setMappedName + */ + public void setMappedName(String mappedName) { + this.pointcut.setMappedName(mappedName); + } + + /** + * Set the method names defining methods to match. + * Matching will be the union of all these; if any match, + * the pointcut matches. + * @see NameMatchMethodPointcut#setMappedNames + */ + public void setMappedNames(String... mappedNames) { + this.pointcut.setMappedNames(mappedNames); + } + + /** + * Add another eligible method name, in addition to those already named. + * Like the set methods, this method is for use when configuring proxies, + * before a proxy is used. + * @param name the name of the additional method that will match + * @return this pointcut to allow for multiple additions in one line + * @see NameMatchMethodPointcut#addMethodName + */ + public NameMatchMethodPointcut addMethodName(String name) { + return this.pointcut.addMethodName(name); + } + + + @Override + public Pointcut getPointcut() { + return this.pointcut; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/Pointcuts.java b/spring-aop/src/main/java/org/springframework/aop/support/Pointcuts.java new file mode 100644 index 0000000..7e2ac45 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/Pointcuts.java @@ -0,0 +1,142 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import java.io.Serializable; +import java.lang.reflect.Method; + +import org.springframework.aop.MethodMatcher; +import org.springframework.aop.Pointcut; +import org.springframework.util.Assert; + +/** + * Pointcut constants for matching getters and setters, + * and static methods useful for manipulating and evaluating pointcuts. + * + *

These methods are particularly useful for composing pointcuts + * using the union and intersection methods. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +public abstract class Pointcuts { + + /** Pointcut matching all bean property setters, in any class. */ + public static final Pointcut SETTERS = SetterPointcut.INSTANCE; + + /** Pointcut matching all bean property getters, in any class. */ + public static final Pointcut GETTERS = GetterPointcut.INSTANCE; + + + /** + * Match all methods that either (or both) of the given pointcuts matches. + * @param pc1 the first Pointcut + * @param pc2 the second Pointcut + * @return a distinct Pointcut that matches all methods that either + * of the given Pointcuts matches + */ + public static Pointcut union(Pointcut pc1, Pointcut pc2) { + return new ComposablePointcut(pc1).union(pc2); + } + + /** + * Match all methods that both the given pointcuts match. + * @param pc1 the first Pointcut + * @param pc2 the second Pointcut + * @return a distinct Pointcut that matches all methods that both + * of the given Pointcuts match + */ + public static Pointcut intersection(Pointcut pc1, Pointcut pc2) { + return new ComposablePointcut(pc1).intersection(pc2); + } + + /** + * Perform the least expensive check for a pointcut match. + * @param pointcut the pointcut to match + * @param method the candidate method + * @param targetClass the target class + * @param args arguments to the method + * @return whether there's a runtime match + */ + public static boolean matches(Pointcut pointcut, Method method, Class targetClass, Object... args) { + Assert.notNull(pointcut, "Pointcut must not be null"); + if (pointcut == Pointcut.TRUE) { + return true; + } + if (pointcut.getClassFilter().matches(targetClass)) { + // Only check if it gets past first hurdle. + MethodMatcher mm = pointcut.getMethodMatcher(); + if (mm.matches(method, targetClass)) { + // We may need additional runtime (argument) check. + return (!mm.isRuntime() || mm.matches(method, targetClass, args)); + } + } + return false; + } + + + /** + * Pointcut implementation that matches bean property setters. + */ + @SuppressWarnings("serial") + private static class SetterPointcut extends StaticMethodMatcherPointcut implements Serializable { + + public static final SetterPointcut INSTANCE = new SetterPointcut(); + + @Override + public boolean matches(Method method, Class targetClass) { + return (method.getName().startsWith("set") && + method.getParameterCount() == 1 && + method.getReturnType() == Void.TYPE); + } + + private Object readResolve() { + return INSTANCE; + } + + @Override + public String toString() { + return "Pointcuts.SETTERS"; + } + } + + + /** + * Pointcut implementation that matches bean property getters. + */ + @SuppressWarnings("serial") + private static class GetterPointcut extends StaticMethodMatcherPointcut implements Serializable { + + public static final GetterPointcut INSTANCE = new GetterPointcut(); + + @Override + public boolean matches(Method method, Class targetClass) { + return (method.getName().startsWith("get") && + method.getParameterCount() == 0); + } + + private Object readResolve() { + return INSTANCE; + } + + @Override + public String toString() { + return "Pointcuts.GETTERS"; + } + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/RegexpMethodPointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/support/RegexpMethodPointcutAdvisor.java new file mode 100644 index 0000000..bc41a9f --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/RegexpMethodPointcutAdvisor.java @@ -0,0 +1,157 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import java.io.Serializable; + +import org.aopalliance.aop.Advice; + +import org.springframework.aop.Pointcut; +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +/** + * Convenient class for regexp method pointcuts that hold an Advice, + * making them an {@link org.springframework.aop.Advisor}. + * + *

Configure this class using the "pattern" and "patterns" + * pass-through properties. These are analogous to the pattern + * and patterns properties of {@link AbstractRegexpMethodPointcut}. + * + *

Can delegate to any {@link AbstractRegexpMethodPointcut} subclass. + * By default, {@link JdkRegexpMethodPointcut} will be used. To choose + * a specific one, override the {@link #createPointcut} method. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #setPattern + * @see #setPatterns + * @see JdkRegexpMethodPointcut + */ +@SuppressWarnings("serial") +public class RegexpMethodPointcutAdvisor extends AbstractGenericPointcutAdvisor { + + @Nullable + private String[] patterns; + + @Nullable + private AbstractRegexpMethodPointcut pointcut; + + private final Object pointcutMonitor = new SerializableMonitor(); + + + /** + * Create an empty RegexpMethodPointcutAdvisor. + * @see #setPattern + * @see #setPatterns + * @see #setAdvice + */ + public RegexpMethodPointcutAdvisor() { + } + + /** + * Create a RegexpMethodPointcutAdvisor for the given advice. + * The pattern still needs to be specified afterwards. + * @param advice the advice to use + * @see #setPattern + * @see #setPatterns + */ + public RegexpMethodPointcutAdvisor(Advice advice) { + setAdvice(advice); + } + + /** + * Create a RegexpMethodPointcutAdvisor for the given advice. + * @param pattern the pattern to use + * @param advice the advice to use + */ + public RegexpMethodPointcutAdvisor(String pattern, Advice advice) { + setPattern(pattern); + setAdvice(advice); + } + + /** + * Create a RegexpMethodPointcutAdvisor for the given advice. + * @param patterns the patterns to use + * @param advice the advice to use + */ + public RegexpMethodPointcutAdvisor(String[] patterns, Advice advice) { + setPatterns(patterns); + setAdvice(advice); + } + + + /** + * Set the regular expression defining methods to match. + *

Use either this method or {@link #setPatterns}, not both. + * @see #setPatterns + */ + public void setPattern(String pattern) { + setPatterns(pattern); + } + + /** + * Set the regular expressions defining methods to match. + * To be passed through to the pointcut implementation. + *

Matching will be the union of all these; if any of the + * patterns matches, the pointcut matches. + * @see AbstractRegexpMethodPointcut#setPatterns + */ + public void setPatterns(String... patterns) { + this.patterns = patterns; + } + + + /** + * Initialize the singleton Pointcut held within this Advisor. + */ + @Override + public Pointcut getPointcut() { + synchronized (this.pointcutMonitor) { + if (this.pointcut == null) { + this.pointcut = createPointcut(); + if (this.patterns != null) { + this.pointcut.setPatterns(this.patterns); + } + } + return this.pointcut; + } + } + + /** + * Create the actual pointcut: By default, a {@link JdkRegexpMethodPointcut} + * will be used. + * @return the Pointcut instance (never {@code null}) + */ + protected AbstractRegexpMethodPointcut createPointcut() { + return new JdkRegexpMethodPointcut(); + } + + @Override + public String toString() { + return getClass().getName() + ": advice [" + getAdvice() + + "], pointcut patterns " + ObjectUtils.nullSafeToString(this.patterns); + } + + + /** + * Empty class used for a serializable monitor object. + */ + private static class SerializableMonitor implements Serializable { + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/RootClassFilter.java b/spring-aop/src/main/java/org/springframework/aop/support/RootClassFilter.java new file mode 100644 index 0000000..3809a30 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/RootClassFilter.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import java.io.Serializable; + +import org.springframework.aop.ClassFilter; +import org.springframework.util.Assert; + +/** + * Simple ClassFilter implementation that passes classes (and optionally subclasses). + * + * @author Rod Johnson + * @author Sam Brannen + */ +@SuppressWarnings("serial") +public class RootClassFilter implements ClassFilter, Serializable { + + private final Class clazz; + + + public RootClassFilter(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + this.clazz = clazz; + } + + + @Override + public boolean matches(Class candidate) { + return this.clazz.isAssignableFrom(candidate); + } + + @Override + public boolean equals(Object other) { + return (this == other || (other instanceof RootClassFilter && + this.clazz.equals(((RootClassFilter) other).clazz))); + } + + @Override + public int hashCode() { + return this.clazz.hashCode(); + } + + @Override + public String toString() { + return getClass().getName() + ": " + this.clazz.getName(); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/StaticMethodMatcher.java b/spring-aop/src/main/java/org/springframework/aop/support/StaticMethodMatcher.java new file mode 100644 index 0000000..482ecfd --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/StaticMethodMatcher.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import java.lang.reflect.Method; + +import org.springframework.aop.MethodMatcher; + +/** + * Convenient abstract superclass for static method matchers, which don't care + * about arguments at runtime. + * + * @author Rod Johnson + */ +public abstract class StaticMethodMatcher implements MethodMatcher { + + @Override + public final boolean isRuntime() { + return false; + } + + @Override + public final boolean matches(Method method, Class targetClass, Object... args) { + // should never be invoked because isRuntime() returns false + throw new UnsupportedOperationException("Illegal MethodMatcher usage"); + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/StaticMethodMatcherPointcut.java b/spring-aop/src/main/java/org/springframework/aop/support/StaticMethodMatcherPointcut.java new file mode 100644 index 0000000..1bae026 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/StaticMethodMatcherPointcut.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import org.springframework.aop.ClassFilter; +import org.springframework.aop.MethodMatcher; +import org.springframework.aop.Pointcut; + +/** + * Convenient superclass when we want to force subclasses to implement the + * {@link MethodMatcher} interface but subclasses will want to be pointcuts. + * + *

The {@link #setClassFilter "classFilter"} property can be set to customize + * {@link ClassFilter} behavior. The default is {@link ClassFilter#TRUE}. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +public abstract class StaticMethodMatcherPointcut extends StaticMethodMatcher implements Pointcut { + + private ClassFilter classFilter = ClassFilter.TRUE; + + + /** + * Set the {@link ClassFilter} to use for this pointcut. + * Default is {@link ClassFilter#TRUE}. + */ + public void setClassFilter(ClassFilter classFilter) { + this.classFilter = classFilter; + } + + @Override + public ClassFilter getClassFilter() { + return this.classFilter; + } + + + @Override + public final MethodMatcher getMethodMatcher() { + return this; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/StaticMethodMatcherPointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/support/StaticMethodMatcherPointcutAdvisor.java new file mode 100644 index 0000000..38dceb3 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/StaticMethodMatcherPointcutAdvisor.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import java.io.Serializable; + +import org.aopalliance.aop.Advice; + +import org.springframework.aop.Pointcut; +import org.springframework.aop.PointcutAdvisor; +import org.springframework.core.Ordered; +import org.springframework.util.Assert; + +/** + * Convenient base class for Advisors that are also static pointcuts. + * Serializable if Advice and subclass are. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +@SuppressWarnings("serial") +public abstract class StaticMethodMatcherPointcutAdvisor extends StaticMethodMatcherPointcut + implements PointcutAdvisor, Ordered, Serializable { + + private Advice advice = EMPTY_ADVICE; + + private int order = Ordered.LOWEST_PRECEDENCE; + + + /** + * Create a new StaticMethodMatcherPointcutAdvisor, + * expecting bean-style configuration. + * @see #setAdvice + */ + public StaticMethodMatcherPointcutAdvisor() { + } + + /** + * Create a new StaticMethodMatcherPointcutAdvisor for the given advice. + * @param advice the Advice to use + */ + public StaticMethodMatcherPointcutAdvisor(Advice advice) { + Assert.notNull(advice, "Advice must not be null"); + this.advice = advice; + } + + + public void setOrder(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return this.order; + } + + public void setAdvice(Advice advice) { + this.advice = advice; + } + + @Override + public Advice getAdvice() { + return this.advice; + } + + @Override + public boolean isPerInstance() { + return true; + } + + @Override + public Pointcut getPointcut() { + return this; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationClassFilter.java b/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationClassFilter.java new file mode 100644 index 0000000..847f1bb --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationClassFilter.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support.annotation; + +import java.lang.annotation.Annotation; + +import org.springframework.aop.ClassFilter; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Simple ClassFilter that looks for a specific Java 5 annotation + * being present on a class. + * + * @author Juergen Hoeller + * @since 2.0 + * @see AnnotationMatchingPointcut + */ +public class AnnotationClassFilter implements ClassFilter { + + private final Class annotationType; + + private final boolean checkInherited; + + + /** + * Create a new AnnotationClassFilter for the given annotation type. + * @param annotationType the annotation type to look for + */ + public AnnotationClassFilter(Class annotationType) { + this(annotationType, false); + } + + /** + * Create a new AnnotationClassFilter for the given annotation type. + * @param annotationType the annotation type to look for + * @param checkInherited whether to also check the superclasses and + * interfaces as well as meta-annotations for the annotation type + * (i.e. whether to use {@link AnnotatedElementUtils#hasAnnotation} + * semantics instead of standard Java {@link Class#isAnnotationPresent}) + */ + public AnnotationClassFilter(Class annotationType, boolean checkInherited) { + Assert.notNull(annotationType, "Annotation type must not be null"); + this.annotationType = annotationType; + this.checkInherited = checkInherited; + } + + + @Override + public boolean matches(Class clazz) { + return (this.checkInherited ? AnnotatedElementUtils.hasAnnotation(clazz, this.annotationType) : + clazz.isAnnotationPresent(this.annotationType)); + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof AnnotationClassFilter)) { + return false; + } + AnnotationClassFilter otherCf = (AnnotationClassFilter) other; + return (this.annotationType.equals(otherCf.annotationType) && this.checkInherited == otherCf.checkInherited); + } + + @Override + public int hashCode() { + return this.annotationType.hashCode(); + } + + @Override + public String toString() { + return getClass().getName() + ": " + this.annotationType; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationMatchingPointcut.java b/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationMatchingPointcut.java new file mode 100644 index 0000000..d734fbc --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationMatchingPointcut.java @@ -0,0 +1,211 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support.annotation; + +import java.lang.annotation.Annotation; + +import org.springframework.aop.ClassFilter; +import org.springframework.aop.MethodMatcher; +import org.springframework.aop.Pointcut; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Simple Pointcut that looks for a specific Java 5 annotation + * being present on a {@link #forClassAnnotation class} or + * {@link #forMethodAnnotation method}. + * + * @author Juergen Hoeller + * @author Sam Brannen + * @since 2.0 + * @see AnnotationClassFilter + * @see AnnotationMethodMatcher + */ +public class AnnotationMatchingPointcut implements Pointcut { + + private final ClassFilter classFilter; + + private final MethodMatcher methodMatcher; + + + /** + * Create a new AnnotationMatchingPointcut for the given annotation type. + * @param classAnnotationType the annotation type to look for at the class level + */ + public AnnotationMatchingPointcut(Class classAnnotationType) { + this(classAnnotationType, false); + } + + /** + * Create a new AnnotationMatchingPointcut for the given annotation type. + * @param classAnnotationType the annotation type to look for at the class level + * @param checkInherited whether to also check the superclasses and interfaces + * as well as meta-annotations for the annotation type + * @see AnnotationClassFilter#AnnotationClassFilter(Class, boolean) + */ + public AnnotationMatchingPointcut(Class classAnnotationType, boolean checkInherited) { + this.classFilter = new AnnotationClassFilter(classAnnotationType, checkInherited); + this.methodMatcher = MethodMatcher.TRUE; + } + + /** + * Create a new AnnotationMatchingPointcut for the given annotation types. + * @param classAnnotationType the annotation type to look for at the class level + * (can be {@code null}) + * @param methodAnnotationType the annotation type to look for at the method level + * (can be {@code null}) + */ + public AnnotationMatchingPointcut(@Nullable Class classAnnotationType, + @Nullable Class methodAnnotationType) { + + this(classAnnotationType, methodAnnotationType, false); + } + + /** + * Create a new AnnotationMatchingPointcut for the given annotation types. + * @param classAnnotationType the annotation type to look for at the class level + * (can be {@code null}) + * @param methodAnnotationType the annotation type to look for at the method level + * (can be {@code null}) + * @param checkInherited whether to also check the superclasses and interfaces + * as well as meta-annotations for the annotation type + * @since 5.0 + * @see AnnotationClassFilter#AnnotationClassFilter(Class, boolean) + * @see AnnotationMethodMatcher#AnnotationMethodMatcher(Class, boolean) + */ + public AnnotationMatchingPointcut(@Nullable Class classAnnotationType, + @Nullable Class methodAnnotationType, boolean checkInherited) { + + Assert.isTrue((classAnnotationType != null || methodAnnotationType != null), + "Either Class annotation type or Method annotation type needs to be specified (or both)"); + + if (classAnnotationType != null) { + this.classFilter = new AnnotationClassFilter(classAnnotationType, checkInherited); + } + else { + this.classFilter = new AnnotationCandidateClassFilter(methodAnnotationType); + } + + if (methodAnnotationType != null) { + this.methodMatcher = new AnnotationMethodMatcher(methodAnnotationType, checkInherited); + } + else { + this.methodMatcher = MethodMatcher.TRUE; + } + } + + + @Override + public ClassFilter getClassFilter() { + return this.classFilter; + } + + @Override + public MethodMatcher getMethodMatcher() { + return this.methodMatcher; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof AnnotationMatchingPointcut)) { + return false; + } + AnnotationMatchingPointcut otherPointcut = (AnnotationMatchingPointcut) other; + return (this.classFilter.equals(otherPointcut.classFilter) && + this.methodMatcher.equals(otherPointcut.methodMatcher)); + } + + @Override + public int hashCode() { + return this.classFilter.hashCode() * 37 + this.methodMatcher.hashCode(); + } + + @Override + public String toString() { + return "AnnotationMatchingPointcut: " + this.classFilter + ", " + this.methodMatcher; + } + + /** + * Factory method for an AnnotationMatchingPointcut that matches + * for the specified annotation at the class level. + * @param annotationType the annotation type to look for at the class level + * @return the corresponding AnnotationMatchingPointcut + */ + public static AnnotationMatchingPointcut forClassAnnotation(Class annotationType) { + Assert.notNull(annotationType, "Annotation type must not be null"); + return new AnnotationMatchingPointcut(annotationType); + } + + /** + * Factory method for an AnnotationMatchingPointcut that matches + * for the specified annotation at the method level. + * @param annotationType the annotation type to look for at the method level + * @return the corresponding AnnotationMatchingPointcut + */ + public static AnnotationMatchingPointcut forMethodAnnotation(Class annotationType) { + Assert.notNull(annotationType, "Annotation type must not be null"); + return new AnnotationMatchingPointcut(null, annotationType); + } + + + /** + * {@link ClassFilter} that delegates to {@link AnnotationUtils#isCandidateClass} + * for filtering classes whose methods are not worth searching to begin with. + * @since 5.2 + */ + private static class AnnotationCandidateClassFilter implements ClassFilter { + + private final Class annotationType; + + AnnotationCandidateClassFilter(Class annotationType) { + this.annotationType = annotationType; + } + + @Override + public boolean matches(Class clazz) { + return AnnotationUtils.isCandidateClass(clazz, this.annotationType); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof AnnotationCandidateClassFilter)) { + return false; + } + AnnotationCandidateClassFilter that = (AnnotationCandidateClassFilter) obj; + return this.annotationType.equals(that.annotationType); + } + + @Override + public int hashCode() { + return this.annotationType.hashCode(); + } + + @Override + public String toString() { + return getClass().getName() + ": " + this.annotationType; + } + + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationMethodMatcher.java b/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationMethodMatcher.java new file mode 100644 index 0000000..720c7b8 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationMethodMatcher.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import org.springframework.aop.support.AopUtils; +import org.springframework.aop.support.StaticMethodMatcher; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Simple MethodMatcher that looks for a specific Java 5 annotation + * being present on a method (checking both the method on the invoked + * interface, if any, and the corresponding method on the target class). + * + * @author Juergen Hoeller + * @author Sam Brannen + * @since 2.0 + * @see AnnotationMatchingPointcut + */ +public class AnnotationMethodMatcher extends StaticMethodMatcher { + + private final Class annotationType; + + private final boolean checkInherited; + + + /** + * Create a new AnnotationClassFilter for the given annotation type. + * @param annotationType the annotation type to look for + */ + public AnnotationMethodMatcher(Class annotationType) { + this(annotationType, false); + } + + /** + * Create a new AnnotationClassFilter for the given annotation type. + * @param annotationType the annotation type to look for + * @param checkInherited whether to also check the superclasses and + * interfaces as well as meta-annotations for the annotation type + * (i.e. whether to use {@link AnnotatedElementUtils#hasAnnotation} + * semantics instead of standard Java {@link Method#isAnnotationPresent}) + * @since 5.0 + */ + public AnnotationMethodMatcher(Class annotationType, boolean checkInherited) { + Assert.notNull(annotationType, "Annotation type must not be null"); + this.annotationType = annotationType; + this.checkInherited = checkInherited; + } + + + + @Override + public boolean matches(Method method, Class targetClass) { + if (matchesMethod(method)) { + return true; + } + // Proxy classes never have annotations on their redeclared methods. + if (Proxy.isProxyClass(targetClass)) { + return false; + } + // The method may be on an interface, so let's check on the target class as well. + Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); + return (specificMethod != method && matchesMethod(specificMethod)); + } + + private boolean matchesMethod(Method method) { + return (this.checkInherited ? AnnotatedElementUtils.hasAnnotation(method, this.annotationType) : + method.isAnnotationPresent(this.annotationType)); + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof AnnotationMethodMatcher)) { + return false; + } + AnnotationMethodMatcher otherMm = (AnnotationMethodMatcher) other; + return (this.annotationType.equals(otherMm.annotationType) && this.checkInherited == otherMm.checkInherited); + } + + @Override + public int hashCode() { + return this.annotationType.hashCode(); + } + + @Override + public String toString() { + return getClass().getName() + ": " + this.annotationType; + } + +} diff --git a/spring-aop/src/main/java/org/springframework/aop/support/annotation/package-info.java b/spring-aop/src/main/java/org/springframework/aop/support/annotation/package-info.java new file mode 100644 index 0000000..a5ec1d4 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/annotation/package-info.java @@ -0,0 +1,9 @@ +/** + * Annotation support for AOP pointcuts. + */ +@NonNullApi +@NonNullFields +package org.springframework.aop.support.annotation; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-aop/src/main/java/org/springframework/aop/support/package-info.java b/spring-aop/src/main/java/org/springframework/aop/support/package-info.java new file mode 100644 index 0000000..a39f2d4 --- /dev/null +++ b/spring-aop/src/main/java/org/springframework/aop/support/package-info.java @@ -0,0 +1,9 @@ +/** + * Convenience classes for using Spring's AOP API. + */ +@NonNullApi +@NonNullFields +package org.springframework.aop.support; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-aop/src/main/resources/org/springframework/aop/config/spring-aop.gif b/spring-aop/src/main/resources/org/springframework/aop/config/spring-aop.gif new file mode 100644 index 0000000..28c4ccf Binary files /dev/null and b/spring-aop/src/main/resources/org/springframework/aop/config/spring-aop.gif differ diff --git a/spring-aop/src/main/resources/org/springframework/aop/config/spring-aop.xsd b/spring-aop/src/main/resources/org/springframework/aop/config/spring-aop.xsd new file mode 100644 index 0000000..0b669c9 --- /dev/null +++ b/spring-aop/src/main/resources/org/springframework/aop/config/spring-aop.xsd @@ -0,0 +1,409 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverAnnotationTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverAnnotationTests.java new file mode 100644 index 0000000..fb64933 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverAnnotationTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.junit.jupiter.api.Test; + +/** + * Additional parameter name discover tests that need Java 5. + * Yes this will re-run the tests from the superclass, but that + * doesn't matter in the grand scheme of things... + * + * @author Adrian Colyer + * @author Chris Beams + */ +public class AspectJAdviceParameterNameDiscoverAnnotationTests extends AspectJAdviceParameterNameDiscovererTests { + + @Test + public void testAnnotationBinding() { + assertParameterNames(getMethod("pjpAndAnAnnotation"), + "execution(* *(..)) && @annotation(ann)", + new String[] {"thisJoinPoint","ann"}); + } + + + public void pjpAndAnAnnotation(ProceedingJoinPoint pjp, MyAnnotation ann) {} + + @interface MyAnnotation {} + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscovererTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscovererTests.java new file mode 100644 index 0000000..bd37f5d --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscovererTests.java @@ -0,0 +1,332 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import java.lang.reflect.Method; + +import org.aspectj.lang.JoinPoint; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.aspectj.AspectJAdviceParameterNameDiscoverer.AmbiguousBindingException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Unit tests for the {@link AspectJAdviceParameterNameDiscoverer} class. + * + *

See also {@link TigerAspectJAdviceParameterNameDiscovererTests} for tests relating to annotations. + * + * @author Adrian Colyer + * @author Chris Beams + */ +public class AspectJAdviceParameterNameDiscovererTests { + + @Test + public void testNoArgs() { + assertParameterNames(getMethod("noArgs"), "execution(* *(..))", new String[0]); + } + + @Test + public void testJoinPointOnly() { + assertParameterNames(getMethod("tjp"), "execution(* *(..))", new String[] {"thisJoinPoint"}); + } + + @Test + public void testJoinPointStaticPartOnly() { + assertParameterNames(getMethod("tjpsp"), "execution(* *(..))", new String[] {"thisJoinPointStaticPart"}); + } + + @Test + public void testTwoJoinPoints() { + assertException(getMethod("twoJoinPoints"), "foo()", IllegalStateException.class, + "Failed to bind all argument names: 1 argument(s) could not be bound"); + } + + @Test + public void testOneThrowable() { + assertParameterNames(getMethod("oneThrowable"), "foo()", null, "ex", new String[] {"ex"}); + } + + @Test + public void testOneJPAndOneThrowable() { + assertParameterNames(getMethod("jpAndOneThrowable"), "foo()", null, "ex", new String[] {"thisJoinPoint", "ex"}); + } + + @Test + public void testOneJPAndTwoThrowables() { + assertException(getMethod("jpAndTwoThrowables"), "foo()", null, "ex", AmbiguousBindingException.class, + "Binding of throwing parameter 'ex' is ambiguous: could be bound to argument 1 or argument 2"); + } + + @Test + public void testThrowableNoCandidates() { + assertException(getMethod("noArgs"), "foo()", null, "ex", IllegalStateException.class, + "Not enough arguments in method to satisfy binding of returning and throwing variables"); + } + + @Test + public void testReturning() { + assertParameterNames(getMethod("oneObject"), "foo()", "obj", null, new String[] {"obj"}); + } + + @Test + public void testAmbiguousReturning() { + assertException(getMethod("twoObjects"), "foo()", "obj", null, AmbiguousBindingException.class, + "Binding of returning parameter 'obj' is ambiguous, there are 2 candidates."); + } + + @Test + public void testReturningNoCandidates() { + assertException(getMethod("noArgs"), "foo()", "obj", null, IllegalStateException.class, + "Not enough arguments in method to satisfy binding of returning and throwing variables"); + } + + @Test + public void testThisBindingOneCandidate() { + assertParameterNames(getMethod("oneObject"), "this(x)", new String[] {"x"}); + } + + @Test + public void testThisBindingWithAlternateTokenizations() { + assertParameterNames(getMethod("oneObject"), "this( x )", new String[] {"x"}); + assertParameterNames(getMethod("oneObject"), "this( x)", new String[] {"x"}); + assertParameterNames(getMethod("oneObject"), "this (x )", new String[] {"x"}); + assertParameterNames(getMethod("oneObject"), "this(x )", new String[] {"x"}); + assertParameterNames(getMethod("oneObject"), "foo() && this(x)", new String[] {"x"}); + } + + @Test + public void testThisBindingTwoCandidates() { + assertException(getMethod("oneObject"), "this(x) || this(y)", AmbiguousBindingException.class, + "Found 2 candidate this(), target() or args() variables but only one unbound argument slot"); + } + + @Test + public void testThisBindingWithBadPointcutExpressions() { + assertException(getMethod("oneObject"), "this(", IllegalStateException.class, + "Failed to bind all argument names: 1 argument(s) could not be bound"); + assertException(getMethod("oneObject"), "this(x && foo()", IllegalStateException.class, + "Failed to bind all argument names: 1 argument(s) could not be bound"); + } + + @Test + public void testTargetBindingOneCandidate() { + assertParameterNames(getMethod("oneObject"), "target(x)", new String[] {"x"}); + } + + @Test + public void testTargetBindingWithAlternateTokenizations() { + assertParameterNames(getMethod("oneObject"), "target( x )", new String[] {"x"}); + assertParameterNames(getMethod("oneObject"), "target( x)", new String[] {"x"}); + assertParameterNames(getMethod("oneObject"), "target (x )", new String[] {"x"}); + assertParameterNames(getMethod("oneObject"), "target(x )", new String[] {"x"}); + assertParameterNames(getMethod("oneObject"), "foo() && target(x)", new String[] {"x"}); + } + + @Test + public void testTargetBindingTwoCandidates() { + assertException(getMethod("oneObject"), "target(x) || target(y)", AmbiguousBindingException.class, + "Found 2 candidate this(), target() or args() variables but only one unbound argument slot"); + } + + @Test + public void testTargetBindingWithBadPointcutExpressions() { + assertException(getMethod("oneObject"), "target(", IllegalStateException.class, + "Failed to bind all argument names: 1 argument(s) could not be bound"); + assertException(getMethod("oneObject"), "target(x && foo()", IllegalStateException.class, + "Failed to bind all argument names: 1 argument(s) could not be bound"); + } + + @Test + public void testArgsBindingOneObject() { + assertParameterNames(getMethod("oneObject"), "args(x)", new String[] {"x"}); + } + + @Test + public void testArgsBindingOneObjectTwoCandidates() { + assertException(getMethod("oneObject"), "args(x,y)", AmbiguousBindingException.class, + "Found 2 candidate this(), target() or args() variables but only one unbound argument slot"); + } + + @Test + public void testAmbiguousArgsBinding() { + assertException(getMethod("twoObjects"), "args(x,y)", AmbiguousBindingException.class, + "Still 2 unbound args at this(),target(),args() binding stage, with no way to determine between them"); + } + + @Test + public void testArgsOnePrimitive() { + assertParameterNames(getMethod("onePrimitive"), "args(count)", new String[] {"count"}); + } + + @Test + public void testArgsOnePrimitiveOneObject() { + assertException(getMethod("oneObjectOnePrimitive"), "args(count,obj)", AmbiguousBindingException.class, + "Found 2 candidate variable names but only one candidate binding slot when matching primitive args"); + } + + @Test + public void testThisAndPrimitive() { + assertParameterNames(getMethod("oneObjectOnePrimitive"), "args(count) && this(obj)", + new String[] {"obj", "count"}); + } + + @Test + public void testTargetAndPrimitive() { + assertParameterNames(getMethod("oneObjectOnePrimitive"), "args(count) && target(obj)", + new String[] {"obj", "count"}); + } + + @Test + public void testThrowingAndPrimitive() { + assertParameterNames(getMethod("oneThrowableOnePrimitive"), "args(count)", null, "ex", + new String[] {"ex", "count"}); + } + + @Test + public void testAllTogetherNow() { + assertParameterNames(getMethod("theBigOne"), "this(foo) && args(x)", null, "ex", + new String[] {"thisJoinPoint", "ex", "x", "foo"}); + } + + @Test + public void testReferenceBinding() { + assertParameterNames(getMethod("onePrimitive"),"somepc(foo)", new String[] {"foo"}); + } + + @Test + public void testReferenceBindingWithAlternateTokenizations() { + assertParameterNames(getMethod("onePrimitive"),"call(bar *) && somepc(foo)", new String[] {"foo"}); + assertParameterNames(getMethod("onePrimitive"),"somepc ( foo )", new String[] {"foo"}); + assertParameterNames(getMethod("onePrimitive"),"somepc( foo)", new String[] {"foo"}); + } + + + protected Method getMethod(String name) { + // Assumes no overloading of test methods... + Method[] candidates = getClass().getMethods(); + for (Method candidate : candidates) { + if (candidate.getName().equals(name)) { + return candidate; + } + } + throw new AssertionError("Bad test specification, no method '" + name + "' found in test class"); + } + + protected void assertParameterNames(Method method, String pointcut, String[] parameterNames) { + assertParameterNames(method, pointcut, null, null, parameterNames); + } + + protected void assertParameterNames( + Method method, String pointcut, String returning, String throwing, String[] parameterNames) { + + assertThat(parameterNames.length).as("bad test specification, must have same number of parameter names as method arguments").isEqualTo(method.getParameterCount()); + + AspectJAdviceParameterNameDiscoverer discoverer = new AspectJAdviceParameterNameDiscoverer(pointcut); + discoverer.setRaiseExceptions(true); + discoverer.setReturningName(returning); + discoverer.setThrowingName(throwing); + String[] discoveredNames = discoverer.getParameterNames(method); + + String formattedExpectedNames = format(parameterNames); + String formattedActualNames = format(discoveredNames); + + assertThat(discoveredNames.length).as("Expecting " + parameterNames.length + " parameter names in return set '" + + formattedExpectedNames + "', but found " + discoveredNames.length + + " '" + formattedActualNames + "'").isEqualTo(parameterNames.length); + + for (int i = 0; i < discoveredNames.length; i++) { + assertThat(discoveredNames[i]).as("Parameter names must never be null").isNotNull(); + assertThat(discoveredNames[i]).as("Expecting parameter " + i + " to be named '" + + parameterNames[i] + "' but was '" + discoveredNames[i] + "'").isEqualTo(parameterNames[i]); + } + } + + protected void assertException(Method method, String pointcut, Class exceptionType, String message) { + assertException(method, pointcut, null, null, exceptionType, message); + } + + protected void assertException(Method method, String pointcut, String returning, + String throwing, Class exceptionType, String message) { + + AspectJAdviceParameterNameDiscoverer discoverer = new AspectJAdviceParameterNameDiscoverer(pointcut); + discoverer.setRaiseExceptions(true); + discoverer.setReturningName(returning); + discoverer.setThrowingName(throwing); + assertThatExceptionOfType(exceptionType).isThrownBy(() -> + discoverer.getParameterNames(method)) + .withMessageContaining(message); + } + + + private static String format(String[] names) { + StringBuilder sb = new StringBuilder(); + sb.append("("); + for (int i = 0; i < names.length; i++) { + sb.append(names[i]); + if ((i + 1) < names.length) { + sb.append(","); + } + } + sb.append(")"); + return sb.toString(); + } + + + // Methods to discover parameter names for + + public void noArgs() { + } + + public void tjp(JoinPoint jp) { + } + + public void tjpsp(JoinPoint.StaticPart tjpsp) { + } + + public void twoJoinPoints(JoinPoint jp1, JoinPoint jp2) { + } + + public void oneThrowable(Exception ex) { + } + + public void jpAndOneThrowable(JoinPoint jp, Exception ex) { + } + + public void jpAndTwoThrowables(JoinPoint jp, Exception ex, Error err) { + } + + public void oneObject(Object x) { + } + + public void twoObjects(Object x, Object y) { + } + + public void onePrimitive(int x) { + } + + public void oneObjectOnePrimitive(Object x, int y) { + } + + public void oneThrowableOnePrimitive(Throwable x, int y) { + } + + public void theBigOne(JoinPoint jp, Throwable x, int y, Object foo) { + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java new file mode 100644 index 0000000..20f3106 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java @@ -0,0 +1,323 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import java.lang.reflect.Method; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.aspectj.weaver.tools.PointcutExpression; +import org.aspectj.weaver.tools.PointcutPrimitive; +import org.aspectj.weaver.tools.UnsupportedPointcutPrimitiveException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.ClassFilter; +import org.springframework.aop.MethodMatcher; +import org.springframework.aop.Pointcut; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.beans.testfixture.beans.IOther; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.beans.testfixture.beans.subpkg.DeepBean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * @author Rob Harrop + * @author Rod Johnson + * @author Chris Beams + */ +public class AspectJExpressionPointcutTests { + + public static final String MATCH_ALL_METHODS = "execution(* *(..))"; + + private Method getAge; + + private Method setAge; + + private Method setSomeNumber; + + + @BeforeEach + public void setUp() throws NoSuchMethodException { + getAge = TestBean.class.getMethod("getAge"); + setAge = TestBean.class.getMethod("setAge", int.class); + setSomeNumber = TestBean.class.getMethod("setSomeNumber", Number.class); + } + + + @Test + public void testMatchExplicit() { + String expression = "execution(int org.springframework.beans.testfixture.beans.TestBean.getAge())"; + + Pointcut pointcut = getPointcut(expression); + ClassFilter classFilter = pointcut.getClassFilter(); + MethodMatcher methodMatcher = pointcut.getMethodMatcher(); + + assertMatchesTestBeanClass(classFilter); + + // not currently testable in a reliable fashion + //assertDoesNotMatchStringClass(classFilter); + + assertThat(methodMatcher.isRuntime()).as("Should not be a runtime match").isFalse(); + assertMatchesGetAge(methodMatcher); + assertThat(methodMatcher.matches(setAge, TestBean.class)).as("Expression should match setAge() method").isFalse(); + } + + @Test + public void testMatchWithTypePattern() throws Exception { + String expression = "execution(* *..TestBean.*Age(..))"; + + Pointcut pointcut = getPointcut(expression); + ClassFilter classFilter = pointcut.getClassFilter(); + MethodMatcher methodMatcher = pointcut.getMethodMatcher(); + + assertMatchesTestBeanClass(classFilter); + + // not currently testable in a reliable fashion + //assertDoesNotMatchStringClass(classFilter); + + assertThat(methodMatcher.isRuntime()).as("Should not be a runtime match").isFalse(); + assertMatchesGetAge(methodMatcher); + assertThat(methodMatcher.matches(setAge, TestBean.class)).as("Expression should match setAge(int) method").isTrue(); + } + + + @Test + public void testThis() throws SecurityException, NoSuchMethodException{ + testThisOrTarget("this"); + } + + @Test + public void testTarget() throws SecurityException, NoSuchMethodException { + testThisOrTarget("target"); + } + + /** + * This and target are equivalent. Really instanceof pointcuts. + * @param which this or target + */ + private void testThisOrTarget(String which) throws SecurityException, NoSuchMethodException { + String matchesTestBean = which + "(org.springframework.beans.testfixture.beans.TestBean)"; + String matchesIOther = which + "(org.springframework.beans.testfixture.beans.IOther)"; + AspectJExpressionPointcut testBeanPc = new AspectJExpressionPointcut(); + testBeanPc.setExpression(matchesTestBean); + + AspectJExpressionPointcut iOtherPc = new AspectJExpressionPointcut(); + iOtherPc.setExpression(matchesIOther); + + assertThat(testBeanPc.matches(TestBean.class)).isTrue(); + assertThat(testBeanPc.matches(getAge, TestBean.class)).isTrue(); + assertThat(iOtherPc.matches(OtherIOther.class.getMethod("absquatulate"), OtherIOther.class)).isTrue(); + assertThat(testBeanPc.matches(OtherIOther.class.getMethod("absquatulate"), OtherIOther.class)).isFalse(); + } + + @Test + public void testWithinRootPackage() throws SecurityException, NoSuchMethodException { + testWithinPackage(false); + } + + @Test + public void testWithinRootAndSubpackages() throws SecurityException, NoSuchMethodException { + testWithinPackage(true); + } + + private void testWithinPackage(boolean matchSubpackages) throws SecurityException, NoSuchMethodException { + String withinBeansPackage = "within(org.springframework.beans.testfixture.beans."; + // Subpackages are matched by ** + if (matchSubpackages) { + withinBeansPackage += "."; + } + withinBeansPackage = withinBeansPackage + "*)"; + AspectJExpressionPointcut withinBeansPc = new AspectJExpressionPointcut(); + withinBeansPc.setExpression(withinBeansPackage); + + assertThat(withinBeansPc.matches(TestBean.class)).isTrue(); + assertThat(withinBeansPc.matches(getAge, TestBean.class)).isTrue(); + assertThat(withinBeansPc.matches(DeepBean.class)).isEqualTo(matchSubpackages); + assertThat(withinBeansPc.matches( + DeepBean.class.getMethod("aMethod", String.class), DeepBean.class)).isEqualTo(matchSubpackages); + assertThat(withinBeansPc.matches(String.class)).isFalse(); + assertThat(withinBeansPc.matches(OtherIOther.class.getMethod("absquatulate"), OtherIOther.class)).isFalse(); + } + + @Test + public void testFriendlyErrorOnNoLocationClassMatching() { + AspectJExpressionPointcut pc = new AspectJExpressionPointcut(); + assertThatIllegalStateException().isThrownBy(() -> + pc.matches(ITestBean.class)) + .withMessageContaining("expression"); + } + + @Test + public void testFriendlyErrorOnNoLocation2ArgMatching() { + AspectJExpressionPointcut pc = new AspectJExpressionPointcut(); + assertThatIllegalStateException().isThrownBy(() -> + pc.matches(getAge, ITestBean.class)) + .withMessageContaining("expression"); + } + + @Test + public void testFriendlyErrorOnNoLocation3ArgMatching() { + AspectJExpressionPointcut pc = new AspectJExpressionPointcut(); + assertThatIllegalStateException().isThrownBy(() -> + pc.matches(getAge, ITestBean.class, (Object[]) null)) + .withMessageContaining("expression"); + } + + + @Test + public void testMatchWithArgs() throws Exception { + String expression = "execution(void org.springframework.beans.testfixture.beans.TestBean.setSomeNumber(Number)) && args(Double)"; + + Pointcut pointcut = getPointcut(expression); + ClassFilter classFilter = pointcut.getClassFilter(); + MethodMatcher methodMatcher = pointcut.getMethodMatcher(); + + assertMatchesTestBeanClass(classFilter); + + // not currently testable in a reliable fashion + //assertDoesNotMatchStringClass(classFilter); + + assertThat(methodMatcher.matches(setSomeNumber, TestBean.class, 12D)).as("Should match with setSomeNumber with Double input").isTrue(); + assertThat(methodMatcher.matches(setSomeNumber, TestBean.class, 11)).as("Should not match setSomeNumber with Integer input").isFalse(); + assertThat(methodMatcher.matches(getAge, TestBean.class)).as("Should not match getAge").isFalse(); + assertThat(methodMatcher.isRuntime()).as("Should be a runtime match").isTrue(); + } + + @Test + public void testSimpleAdvice() { + String expression = "execution(int org.springframework.beans.testfixture.beans.TestBean.getAge())"; + CallCountingInterceptor interceptor = new CallCountingInterceptor(); + TestBean testBean = getAdvisedProxy(expression, interceptor); + + assertThat(interceptor.getCount()).as("Calls should be 0").isEqualTo(0); + testBean.getAge(); + assertThat(interceptor.getCount()).as("Calls should be 1").isEqualTo(1); + testBean.setAge(90); + assertThat(interceptor.getCount()).as("Calls should still be 1").isEqualTo(1); + } + + @Test + public void testDynamicMatchingProxy() { + String expression = "execution(void org.springframework.beans.testfixture.beans.TestBean.setSomeNumber(Number)) && args(Double)"; + CallCountingInterceptor interceptor = new CallCountingInterceptor(); + TestBean testBean = getAdvisedProxy(expression, interceptor); + + assertThat(interceptor.getCount()).as("Calls should be 0").isEqualTo(0); + testBean.setSomeNumber(30D); + assertThat(interceptor.getCount()).as("Calls should be 1").isEqualTo(1); + + testBean.setSomeNumber(90); + assertThat(interceptor.getCount()).as("Calls should be 1").isEqualTo(1); + } + + @Test + public void testInvalidExpression() { + String expression = "execution(void org.springframework.beans.testfixture.beans.TestBean.setSomeNumber(Number) && args(Double)"; + assertThatIllegalArgumentException().isThrownBy( + getPointcut(expression)::getClassFilter); // call to getClassFilter forces resolution + } + + private TestBean getAdvisedProxy(String pointcutExpression, CallCountingInterceptor interceptor) { + TestBean target = new TestBean(); + + Pointcut pointcut = getPointcut(pointcutExpression); + + DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(); + advisor.setAdvice(interceptor); + advisor.setPointcut(pointcut); + + ProxyFactory pf = new ProxyFactory(); + pf.setTarget(target); + pf.addAdvisor(advisor); + + return (TestBean) pf.getProxy(); + } + + private void assertMatchesGetAge(MethodMatcher methodMatcher) { + assertThat(methodMatcher.matches(getAge, TestBean.class)).as("Expression should match getAge() method").isTrue(); + } + + private void assertMatchesTestBeanClass(ClassFilter classFilter) { + assertThat(classFilter.matches(TestBean.class)).as("Expression should match TestBean class").isTrue(); + } + + @Test + public void testWithUnsupportedPointcutPrimitive() { + String expression = "call(int org.springframework.beans.testfixture.beans.TestBean.getAge())"; + assertThatExceptionOfType(UnsupportedPointcutPrimitiveException.class).isThrownBy(() -> + getPointcut(expression).getClassFilter()) // call to getClassFilter forces resolution... + .satisfies(ex -> assertThat(ex.getUnsupportedPrimitive()).isEqualTo(PointcutPrimitive.CALL)); + } + + @Test + public void testAndSubstitution() { + Pointcut pc = getPointcut("execution(* *(..)) and args(String)"); + PointcutExpression expr = ((AspectJExpressionPointcut) pc).getPointcutExpression(); + assertThat(expr.getPointcutExpression()).isEqualTo("execution(* *(..)) && args(String)"); + } + + @Test + public void testMultipleAndSubstitutions() { + Pointcut pc = getPointcut("execution(* *(..)) and args(String) and this(Object)"); + PointcutExpression expr = ((AspectJExpressionPointcut) pc).getPointcutExpression(); + assertThat(expr.getPointcutExpression()).isEqualTo("execution(* *(..)) && args(String) && this(Object)"); + } + + private Pointcut getPointcut(String expression) { + AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); + pointcut.setExpression(expression); + return pointcut; + } + + + public static class OtherIOther implements IOther { + + @Override + public void absquatulate() { + // Empty + } + } + +} + + +class CallCountingInterceptor implements MethodInterceptor { + + private int count; + + @Override + public Object invoke(MethodInvocation methodInvocation) throws Throwable { + count++; + return methodInvocation.proceed(); + } + + public int getCount() { + return count; + } + + public void reset() { + this.count = 0; + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/BeanNamePointcutMatchingTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/BeanNamePointcutMatchingTests.java new file mode 100644 index 0000000..3fd1b1e --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/BeanNamePointcutMatchingTests.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for matching of bean() pointcut designator. + * + * @author Ramnivas Laddad + * @author Chris Beams + */ +public class BeanNamePointcutMatchingTests { + + @Test + public void testMatchingPointcuts() { + assertMatch("someName", "bean(someName)"); + + // Spring bean names are less restrictive compared to AspectJ names (methods, types etc.) + // MVC Controller-kind + assertMatch("someName/someOtherName", "bean(someName/someOtherName)"); + assertMatch("someName/foo/someOtherName", "bean(someName/*/someOtherName)"); + assertMatch("someName/foo/bar/someOtherName", "bean(someName/*/someOtherName)"); + assertMatch("someName/*/**", "bean(someName/*)"); + // JMX-kind + assertMatch("service:name=traceService", "bean(service:name=traceService)"); + assertMatch("service:name=traceService", "bean(service:name=*)"); + assertMatch("service:name=traceService", "bean(*:name=traceService)"); + + // Wildcards + assertMatch("someName", "bean(*someName)"); + assertMatch("someName", "bean(*Name)"); + assertMatch("someName", "bean(*)"); + assertMatch("someName", "bean(someName*)"); + assertMatch("someName", "bean(some*)"); + assertMatch("someName", "bean(some*Name)"); + assertMatch("someName", "bean(*some*Name*)"); + assertMatch("someName", "bean(*s*N*)"); + + // Or, and, not expressions + assertMatch("someName", "bean(someName) || bean(someOtherName)"); + assertMatch("someOtherName", "bean(someName) || bean(someOtherName)"); + + assertMatch("someName", "!bean(someOtherName)"); + + assertMatch("someName", "bean(someName) || !bean(someOtherName)"); + assertMatch("someName", "bean(someName) && !bean(someOtherName)"); + } + + @Test + public void testNonMatchingPointcuts() { + assertMisMatch("someName", "bean(someNamex)"); + assertMisMatch("someName", "bean(someX*Name)"); + + // And, not expressions + assertMisMatch("someName", "bean(someName) && bean(someOtherName)"); + assertMisMatch("someName", "!bean(someName)"); + assertMisMatch("someName", "!bean(someName) && bean(someOtherName)"); + assertMisMatch("someName", "!bean(someName) || bean(someOtherName)"); + } + + + private void assertMatch(String beanName, String pcExpression) { + assertThat(matches(beanName, pcExpression)).as("Unexpected mismatch for bean \"" + beanName + "\" for pcExpression \"" + pcExpression + "\"").isTrue(); + } + + private void assertMisMatch(String beanName, String pcExpression) { + assertThat(matches(beanName, pcExpression)).as("Unexpected match for bean \"" + beanName + "\" for pcExpression \"" + pcExpression + "\"").isFalse(); + } + + private static boolean matches(final String beanName, String pcExpression) { + @SuppressWarnings("serial") + AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut() { + @Override + protected String getCurrentProxiedBeanName() { + return beanName; + } + }; + pointcut.setExpression(pcExpression); + return pointcut.matches(TestBean.class); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/MethodInvocationProceedingJoinPointTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/MethodInvocationProceedingJoinPointTests.java new file mode 100644 index 0000000..4591b74 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/MethodInvocationProceedingJoinPointTests.java @@ -0,0 +1,188 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import java.io.IOException; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.JoinPoint.StaticPart; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.reflect.MethodSignature; +import org.aspectj.lang.reflect.SourceLocation; +import org.aspectj.runtime.reflect.Factory; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.MethodBeforeAdvice; +import org.springframework.aop.framework.AopContext; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.interceptor.ExposeInvocationInterceptor; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * @author Rod Johnson + * @author Chris Beams + * @author Ramnivas Laddad + * @since 2.0 + */ +public class MethodInvocationProceedingJoinPointTests { + + @Test + public void testingBindingWithJoinPoint() { + assertThatIllegalStateException().isThrownBy(AbstractAspectJAdvice::currentJoinPoint); + } + + @Test + public void testingBindingWithProceedingJoinPoint() { + assertThatIllegalStateException().isThrownBy(AbstractAspectJAdvice::currentJoinPoint); + } + + @Test + public void testCanGetMethodSignatureFromJoinPoint() { + final Object raw = new TestBean(); + // Will be set by advice during a method call + final int newAge = 23; + + ProxyFactory pf = new ProxyFactory(raw); + pf.setExposeProxy(true); + pf.addAdvisor(ExposeInvocationInterceptor.ADVISOR); + AtomicInteger depth = new AtomicInteger(); + pf.addAdvice((MethodBeforeAdvice) (method, args, target) -> { + JoinPoint jp = AbstractAspectJAdvice.currentJoinPoint(); + assertThat(jp.toString().contains(method.getName())).as("Method named in toString").isTrue(); + // Ensure that these don't cause problems + jp.toShortString(); + jp.toLongString(); + + assertThat(AbstractAspectJAdvice.currentJoinPoint().getTarget()).isSameAs(target); + assertThat(AopUtils.isAopProxy(AbstractAspectJAdvice.currentJoinPoint().getTarget())).isFalse(); + + ITestBean thisProxy = (ITestBean) AbstractAspectJAdvice.currentJoinPoint().getThis(); + assertThat(AopUtils.isAopProxy(AbstractAspectJAdvice.currentJoinPoint().getThis())).isTrue(); + + assertThat(thisProxy).isNotSameAs(target); + + // Check getting again doesn't cause a problem + assertThat(AbstractAspectJAdvice.currentJoinPoint().getThis()).isSameAs(thisProxy); + + // Try reentrant call--will go through this advice. + // Be sure to increment depth to avoid infinite recursion + if (depth.getAndIncrement() == 0) { + // Check that toString doesn't cause a problem + thisProxy.toString(); + // Change age, so this will be returned by invocation + thisProxy.setAge(newAge); + assertThat(thisProxy.getAge()).isEqualTo(newAge); + } + + assertThat(thisProxy).isSameAs(AopContext.currentProxy()); + assertThat(raw).isSameAs(target); + + assertThat(AbstractAspectJAdvice.currentJoinPoint().getSignature().getName()).isSameAs(method.getName()); + assertThat(AbstractAspectJAdvice.currentJoinPoint().getSignature().getModifiers()).isEqualTo(method.getModifiers()); + + MethodSignature msig = (MethodSignature) AbstractAspectJAdvice.currentJoinPoint().getSignature(); + assertThat(AbstractAspectJAdvice.currentJoinPoint().getSignature()).as("Return same MethodSignature repeatedly").isSameAs(msig); + assertThat(AbstractAspectJAdvice.currentJoinPoint()).as("Return same JoinPoint repeatedly").isSameAs(AbstractAspectJAdvice.currentJoinPoint()); + assertThat(msig.getDeclaringType()).isEqualTo(method.getDeclaringClass()); + assertThat(Arrays.equals(method.getParameterTypes(), msig.getParameterTypes())).isTrue(); + assertThat(msig.getReturnType()).isEqualTo(method.getReturnType()); + assertThat(Arrays.equals(method.getExceptionTypes(), msig.getExceptionTypes())).isTrue(); + msig.toLongString(); + msig.toShortString(); + }); + ITestBean itb = (ITestBean) pf.getProxy(); + // Any call will do + assertThat(itb.getAge()).as("Advice reentrantly set age").isEqualTo(newAge); + } + + @Test + public void testCanGetSourceLocationFromJoinPoint() { + final Object raw = new TestBean(); + ProxyFactory pf = new ProxyFactory(raw); + pf.addAdvisor(ExposeInvocationInterceptor.ADVISOR); + pf.addAdvice((MethodBeforeAdvice) (method, args, target) -> { + SourceLocation sloc = AbstractAspectJAdvice.currentJoinPoint().getSourceLocation(); + assertThat(AbstractAspectJAdvice.currentJoinPoint().getSourceLocation()).as("Same source location must be returned on subsequent requests").isEqualTo(sloc); + assertThat(sloc.getWithinType()).isEqualTo(TestBean.class); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(sloc::getLine); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(sloc::getFileName); + }); + ITestBean itb = (ITestBean) pf.getProxy(); + // Any call will do + itb.getAge(); + } + + @Test + public void testCanGetStaticPartFromJoinPoint() { + final Object raw = new TestBean(); + ProxyFactory pf = new ProxyFactory(raw); + pf.addAdvisor(ExposeInvocationInterceptor.ADVISOR); + pf.addAdvice((MethodBeforeAdvice) (method, args, target) -> { + StaticPart staticPart = AbstractAspectJAdvice.currentJoinPoint().getStaticPart(); + assertThat(AbstractAspectJAdvice.currentJoinPoint().getStaticPart()).as("Same static part must be returned on subsequent requests").isEqualTo(staticPart); + assertThat(staticPart.getKind()).isEqualTo(ProceedingJoinPoint.METHOD_EXECUTION); + assertThat(staticPart.getSignature()).isSameAs(AbstractAspectJAdvice.currentJoinPoint().getSignature()); + assertThat(staticPart.getSourceLocation()).isEqualTo(AbstractAspectJAdvice.currentJoinPoint().getSourceLocation()); + }); + ITestBean itb = (ITestBean) pf.getProxy(); + // Any call will do + itb.getAge(); + } + + @Test + public void toShortAndLongStringFormedCorrectly() throws Exception { + final Object raw = new TestBean(); + ProxyFactory pf = new ProxyFactory(raw); + pf.addAdvisor(ExposeInvocationInterceptor.ADVISOR); + pf.addAdvice((MethodBeforeAdvice) (method, args, target) -> { + // makeEncSJP, although meant for computing the enclosing join point, + // it serves our purpose here + StaticPart aspectJVersionJp = Factory.makeEncSJP(method); + JoinPoint jp = AbstractAspectJAdvice.currentJoinPoint(); + + assertThat(jp.getSignature().toLongString()).isEqualTo(aspectJVersionJp.getSignature().toLongString()); + assertThat(jp.getSignature().toShortString()).isEqualTo(aspectJVersionJp.getSignature().toShortString()); + assertThat(jp.getSignature().toString()).isEqualTo(aspectJVersionJp.getSignature().toString()); + + assertThat(jp.toLongString()).isEqualTo(aspectJVersionJp.toLongString()); + assertThat(jp.toShortString()).isEqualTo(aspectJVersionJp.toShortString()); + assertThat(jp.toString()).isEqualTo(aspectJVersionJp.toString()); + }); + ITestBean itb = (ITestBean) pf.getProxy(); + itb.getAge(); + itb.setName("foo"); + itb.getDoctor(); + itb.getStringArray(); + itb.getSpouse(); + itb.setSpouse(new TestBean()); + try { + itb.unreliableFileOperation(); + } + catch (IOException ex) { + // we don't really care... + } + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/TigerAspectJAdviceParameterNameDiscovererTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/TigerAspectJAdviceParameterNameDiscovererTests.java new file mode 100644 index 0000000..0cc3947 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/TigerAspectJAdviceParameterNameDiscovererTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.aspectj.AspectJAdviceParameterNameDiscoverer.AmbiguousBindingException; + +/** + * Tests just the annotation binding part of {@link AspectJAdviceParameterNameDiscoverer}; + * see supertype for remaining tests. + * + * @author Adrian Colyer + * @author Chris Beams + */ +public class TigerAspectJAdviceParameterNameDiscovererTests extends AspectJAdviceParameterNameDiscovererTests { + + @Test + public void testAtThis() { + assertParameterNames(getMethod("oneAnnotation"),"@this(a)", new String[] {"a"}); + } + + @Test + public void testAtTarget() { + assertParameterNames(getMethod("oneAnnotation"),"@target(a)", new String[] {"a"}); + } + + @Test + public void testAtArgs() { + assertParameterNames(getMethod("oneAnnotation"),"@args(a)", new String[] {"a"}); + } + + @Test + public void testAtWithin() { + assertParameterNames(getMethod("oneAnnotation"),"@within(a)", new String[] {"a"}); + } + + @Test + public void testAtWithincode() { + assertParameterNames(getMethod("oneAnnotation"),"@withincode(a)", new String[] {"a"}); + } + + @Test + public void testAtAnnotation() { + assertParameterNames(getMethod("oneAnnotation"),"@annotation(a)", new String[] {"a"}); + } + + @Test + public void testAmbiguousAnnotationTwoVars() { + assertException(getMethod("twoAnnotations"),"@annotation(a) && @this(x)", AmbiguousBindingException.class, + "Found 2 potential annotation variable(s), and 2 potential argument slots"); + } + + @Test + public void testAmbiguousAnnotationOneVar() { + assertException(getMethod("oneAnnotation"),"@annotation(a) && @this(x)",IllegalArgumentException.class, + "Found 2 candidate annotation binding variables but only one potential argument binding slot"); + } + + @Test + public void testAnnotationMedley() { + assertParameterNames(getMethod("annotationMedley"),"@annotation(a) && args(count) && this(foo)", + null, "ex", new String[] {"ex", "foo", "count", "a"}); + } + + + public void oneAnnotation(MyAnnotation ann) {} + + public void twoAnnotations(MyAnnotation ann, MyAnnotation anotherAnn) {} + + public void annotationMedley(Throwable t, Object foo, int x, MyAnnotation ma) {} + + @interface MyAnnotation {} + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/TigerAspectJExpressionPointcutTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/TigerAspectJExpressionPointcutTests.java new file mode 100644 index 0000000..23d63fb --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/TigerAspectJExpressionPointcutTests.java @@ -0,0 +1,330 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import test.annotation.EmptySpringAnnotation; +import test.annotation.transaction.Tx; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Java 5 specific {@link AspectJExpressionPointcutTests}. + * + * @author Rod Johnson + * @author Chris Beams + */ +public class TigerAspectJExpressionPointcutTests { + + private Method getAge; + + private final Map methodsOnHasGeneric = new HashMap<>(); + + + @BeforeEach + public void setup() throws NoSuchMethodException { + getAge = TestBean.class.getMethod("getAge"); + // Assumes no overloading + for (Method method : HasGeneric.class.getMethods()) { + methodsOnHasGeneric.put(method.getName(), method); + } + } + + + @Test + public void testMatchGenericArgument() { + String expression = "execution(* set*(java.util.List) )"; + AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); + ajexp.setExpression(expression); + + // TODO this will currently map, would be nice for optimization + //assertTrue(ajexp.matches(HasGeneric.class)); + //assertFalse(ajexp.matches(TestBean.class)); + + Method takesGenericList = methodsOnHasGeneric.get("setFriends"); + assertThat(ajexp.matches(takesGenericList, HasGeneric.class)).isTrue(); + assertThat(ajexp.matches(methodsOnHasGeneric.get("setEnemies"), HasGeneric.class)).isTrue(); + assertThat(ajexp.matches(methodsOnHasGeneric.get("setPartners"), HasGeneric.class)).isFalse(); + assertThat(ajexp.matches(methodsOnHasGeneric.get("setPhoneNumbers"), HasGeneric.class)).isFalse(); + + assertThat(ajexp.matches(getAge, TestBean.class)).isFalse(); + } + + @Test + public void testMatchVarargs() throws Exception { + + @SuppressWarnings("unused") + class MyTemplate { + public int queryForInt(String sql, Object... params) { + return 0; + } + } + + String expression = "execution(int *.*(String, Object...))"; + AspectJExpressionPointcut jdbcVarArgs = new AspectJExpressionPointcut(); + jdbcVarArgs.setExpression(expression); + + assertThat(jdbcVarArgs.matches( + MyTemplate.class.getMethod("queryForInt", String.class, Object[].class), + MyTemplate.class)).isTrue(); + + Method takesGenericList = methodsOnHasGeneric.get("setFriends"); + assertThat(jdbcVarArgs.matches(takesGenericList, HasGeneric.class)).isFalse(); + assertThat(jdbcVarArgs.matches(methodsOnHasGeneric.get("setEnemies"), HasGeneric.class)).isFalse(); + assertThat(jdbcVarArgs.matches(methodsOnHasGeneric.get("setPartners"), HasGeneric.class)).isFalse(); + assertThat(jdbcVarArgs.matches(methodsOnHasGeneric.get("setPhoneNumbers"), HasGeneric.class)).isFalse(); + assertThat(jdbcVarArgs.matches(getAge, TestBean.class)).isFalse(); + } + + @Test + public void testMatchAnnotationOnClassWithAtWithin() throws Exception { + String expression = "@within(test.annotation.transaction.Tx)"; + testMatchAnnotationOnClass(expression); + } + + @Test + public void testMatchAnnotationOnClassWithoutBinding() throws Exception { + String expression = "within(@test.annotation.transaction.Tx *)"; + testMatchAnnotationOnClass(expression); + } + + @Test + public void testMatchAnnotationOnClassWithSubpackageWildcard() throws Exception { + String expression = "within(@(test.annotation..*) *)"; + AspectJExpressionPointcut springAnnotatedPc = testMatchAnnotationOnClass(expression); + assertThat(springAnnotatedPc.matches(TestBean.class.getMethod("setName", String.class), TestBean.class)).isFalse(); + assertThat(springAnnotatedPc.matches(SpringAnnotated.class.getMethod("foo"), SpringAnnotated.class)).isTrue(); + + expression = "within(@(test.annotation.transaction..*) *)"; + AspectJExpressionPointcut springTxAnnotatedPc = testMatchAnnotationOnClass(expression); + assertThat(springTxAnnotatedPc.matches(SpringAnnotated.class.getMethod("foo"), SpringAnnotated.class)).isFalse(); + } + + @Test + public void testMatchAnnotationOnClassWithExactPackageWildcard() throws Exception { + String expression = "within(@(test.annotation.transaction.*) *)"; + testMatchAnnotationOnClass(expression); + } + + private AspectJExpressionPointcut testMatchAnnotationOnClass(String expression) throws Exception { + AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); + ajexp.setExpression(expression); + + assertThat(ajexp.matches(getAge, TestBean.class)).isFalse(); + assertThat(ajexp.matches(HasTransactionalAnnotation.class.getMethod("foo"), HasTransactionalAnnotation.class)).isTrue(); + assertThat(ajexp.matches(HasTransactionalAnnotation.class.getMethod("bar", String.class), HasTransactionalAnnotation.class)).isTrue(); + assertThat(ajexp.matches(BeanB.class.getMethod("setName", String.class), BeanB.class)).isTrue(); + assertThat(ajexp.matches(BeanA.class.getMethod("setName", String.class), BeanA.class)).isFalse(); + return ajexp; + } + + @Test + public void testAnnotationOnMethodWithFQN() throws Exception { + String expression = "@annotation(test.annotation.transaction.Tx)"; + AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); + ajexp.setExpression(expression); + + assertThat(ajexp.matches(getAge, TestBean.class)).isFalse(); + assertThat(ajexp.matches(HasTransactionalAnnotation.class.getMethod("foo"), HasTransactionalAnnotation.class)).isFalse(); + assertThat(ajexp.matches(HasTransactionalAnnotation.class.getMethod("bar", String.class), HasTransactionalAnnotation.class)).isFalse(); + assertThat(ajexp.matches(BeanA.class.getMethod("setName", String.class), BeanA.class)).isFalse(); + assertThat(ajexp.matches(BeanA.class.getMethod("getAge"), BeanA.class)).isTrue(); + assertThat(ajexp.matches(BeanA.class.getMethod("setName", String.class), BeanA.class)).isFalse(); + } + + @Test + public void testAnnotationOnCglibProxyMethod() throws Exception { + String expression = "@annotation(test.annotation.transaction.Tx)"; + AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); + ajexp.setExpression(expression); + + ProxyFactory factory = new ProxyFactory(new BeanA()); + factory.setProxyTargetClass(true); + BeanA proxy = (BeanA) factory.getProxy(); + assertThat(ajexp.matches(BeanA.class.getMethod("getAge"), proxy.getClass())).isTrue(); + } + + @Test + public void testAnnotationOnDynamicProxyMethod() throws Exception { + String expression = "@annotation(test.annotation.transaction.Tx)"; + AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); + ajexp.setExpression(expression); + + ProxyFactory factory = new ProxyFactory(new BeanA()); + factory.setProxyTargetClass(false); + IBeanA proxy = (IBeanA) factory.getProxy(); + assertThat(ajexp.matches(IBeanA.class.getMethod("getAge"), proxy.getClass())).isTrue(); + } + + @Test + public void testAnnotationOnMethodWithWildcard() throws Exception { + String expression = "execution(@(test.annotation..*) * *(..))"; + AspectJExpressionPointcut anySpringMethodAnnotation = new AspectJExpressionPointcut(); + anySpringMethodAnnotation.setExpression(expression); + + assertThat(anySpringMethodAnnotation.matches(getAge, TestBean.class)).isFalse(); + assertThat(anySpringMethodAnnotation.matches( + HasTransactionalAnnotation.class.getMethod("foo"), HasTransactionalAnnotation.class)).isFalse(); + assertThat(anySpringMethodAnnotation.matches( + HasTransactionalAnnotation.class.getMethod("bar", String.class), HasTransactionalAnnotation.class)).isFalse(); + assertThat(anySpringMethodAnnotation.matches(BeanA.class.getMethod("setName", String.class), BeanA.class)).isFalse(); + assertThat(anySpringMethodAnnotation.matches(BeanA.class.getMethod("getAge"), BeanA.class)).isTrue(); + assertThat(anySpringMethodAnnotation.matches(BeanA.class.getMethod("setName", String.class), BeanA.class)).isFalse(); + } + + @Test + public void testAnnotationOnMethodArgumentsWithFQN() throws Exception { + String expression = "@args(*, test.annotation.EmptySpringAnnotation))"; + AspectJExpressionPointcut takesSpringAnnotatedArgument2 = new AspectJExpressionPointcut(); + takesSpringAnnotatedArgument2.setExpression(expression); + + assertThat(takesSpringAnnotatedArgument2.matches(getAge, TestBean.class)).isFalse(); + assertThat(takesSpringAnnotatedArgument2.matches( + HasTransactionalAnnotation.class.getMethod("foo"), HasTransactionalAnnotation.class)).isFalse(); + assertThat(takesSpringAnnotatedArgument2.matches( + HasTransactionalAnnotation.class.getMethod("bar", String.class), HasTransactionalAnnotation.class)).isFalse(); + assertThat(takesSpringAnnotatedArgument2.matches(BeanA.class.getMethod("setName", String.class), BeanA.class)).isFalse(); + assertThat(takesSpringAnnotatedArgument2.matches(BeanA.class.getMethod("getAge"), BeanA.class)).isFalse(); + assertThat(takesSpringAnnotatedArgument2.matches(BeanA.class.getMethod("setName", String.class), BeanA.class)).isFalse(); + + assertThat(takesSpringAnnotatedArgument2.matches( + ProcessesSpringAnnotatedParameters.class.getMethod("takesAnnotatedParameters", TestBean.class, SpringAnnotated.class), + ProcessesSpringAnnotatedParameters.class)).isTrue(); + + // True because it maybeMatches with potential argument subtypes + assertThat(takesSpringAnnotatedArgument2.matches( + ProcessesSpringAnnotatedParameters.class.getMethod("takesNoAnnotatedParameters", TestBean.class, BeanA.class), + ProcessesSpringAnnotatedParameters.class)).isTrue(); + + assertThat(takesSpringAnnotatedArgument2.matches( + ProcessesSpringAnnotatedParameters.class.getMethod("takesNoAnnotatedParameters", TestBean.class, BeanA.class), + ProcessesSpringAnnotatedParameters.class, new TestBean(), new BeanA())).isFalse(); + } + + @Test + public void testAnnotationOnMethodArgumentsWithWildcards() throws Exception { + String expression = "execution(* *(*, @(test..*) *))"; + AspectJExpressionPointcut takesSpringAnnotatedArgument2 = new AspectJExpressionPointcut(); + takesSpringAnnotatedArgument2.setExpression(expression); + + assertThat(takesSpringAnnotatedArgument2.matches(getAge, TestBean.class)).isFalse(); + assertThat(takesSpringAnnotatedArgument2.matches( + HasTransactionalAnnotation.class.getMethod("foo"), HasTransactionalAnnotation.class)).isFalse(); + assertThat(takesSpringAnnotatedArgument2.matches( + HasTransactionalAnnotation.class.getMethod("bar", String.class), HasTransactionalAnnotation.class)).isFalse(); + assertThat(takesSpringAnnotatedArgument2.matches(BeanA.class.getMethod("setName", String.class), BeanA.class)).isFalse(); + assertThat(takesSpringAnnotatedArgument2.matches(BeanA.class.getMethod("getAge"), BeanA.class)).isFalse(); + assertThat(takesSpringAnnotatedArgument2.matches(BeanA.class.getMethod("setName", String.class), BeanA.class)).isFalse(); + + assertThat(takesSpringAnnotatedArgument2.matches( + ProcessesSpringAnnotatedParameters.class.getMethod("takesAnnotatedParameters", TestBean.class, SpringAnnotated.class), + ProcessesSpringAnnotatedParameters.class)).isTrue(); + assertThat(takesSpringAnnotatedArgument2.matches( + ProcessesSpringAnnotatedParameters.class.getMethod("takesNoAnnotatedParameters", TestBean.class, BeanA.class), + ProcessesSpringAnnotatedParameters.class)).isFalse(); + } + + + public static class HasGeneric { + + public void setFriends(List friends) { + } + public void setEnemies(List enemies) { + } + public void setPartners(List partners) { + } + public void setPhoneNumbers(List numbers) { + } + } + + + public static class ProcessesSpringAnnotatedParameters { + + public void takesAnnotatedParameters(TestBean tb, SpringAnnotated sa) { + } + + public void takesNoAnnotatedParameters(TestBean tb, BeanA tb3) { + } + } + + + @Tx + public static class HasTransactionalAnnotation { + + public void foo() { + } + public Object bar(String foo) { + throw new UnsupportedOperationException(); + } + } + + + @EmptySpringAnnotation + public static class SpringAnnotated { + + public void foo() { + } + } + + + interface IBeanA { + + @Tx + int getAge(); + } + + + static class BeanA implements IBeanA { + + @SuppressWarnings("unused") + private String name; + + private int age; + + public void setName(String name) { + this.name = name; + } + + @Tx + @Override + public int getAge() { + return age; + } + } + + + @Tx + static class BeanB { + + @SuppressWarnings("unused") + private String name; + + public void setName(String name) { + this.name = name; + } + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/TrickyAspectJPointcutExpressionTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/TrickyAspectJPointcutExpressionTests.java new file mode 100644 index 0000000..7a6c7e9 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/TrickyAspectJPointcutExpressionTests.java @@ -0,0 +1,195 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.Advisor; +import org.springframework.aop.MethodBeforeAdvice; +import org.springframework.aop.ThrowsAdvice; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.core.OverridingClassLoader; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Dave Syer + */ +public class TrickyAspectJPointcutExpressionTests { + + @Test + public void testManualProxyJavaWithUnconditionalPointcut() throws Exception { + TestService target = new TestServiceImpl(); + LogUserAdvice logAdvice = new LogUserAdvice(); + testAdvice(new DefaultPointcutAdvisor(logAdvice), logAdvice, target, "TestServiceImpl"); + } + + @Test + public void testManualProxyJavaWithStaticPointcut() throws Exception { + TestService target = new TestServiceImpl(); + LogUserAdvice logAdvice = new LogUserAdvice(); + AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); + pointcut.setExpression(String.format("execution(* %s.TestService.*(..))", getClass().getName())); + testAdvice(new DefaultPointcutAdvisor(pointcut, logAdvice), logAdvice, target, "TestServiceImpl"); + } + + @Test + public void testManualProxyJavaWithDynamicPointcut() throws Exception { + TestService target = new TestServiceImpl(); + LogUserAdvice logAdvice = new LogUserAdvice(); + AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); + pointcut.setExpression(String.format("@within(%s.Log)", getClass().getName())); + testAdvice(new DefaultPointcutAdvisor(pointcut, logAdvice), logAdvice, target, "TestServiceImpl"); + } + + @Test + public void testManualProxyJavaWithDynamicPointcutAndProxyTargetClass() throws Exception { + TestService target = new TestServiceImpl(); + LogUserAdvice logAdvice = new LogUserAdvice(); + AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); + pointcut.setExpression(String.format("@within(%s.Log)", getClass().getName())); + testAdvice(new DefaultPointcutAdvisor(pointcut, logAdvice), logAdvice, target, "TestServiceImpl", true); + } + + @Test + public void testManualProxyJavaWithStaticPointcutAndTwoClassLoaders() throws Exception { + + LogUserAdvice logAdvice = new LogUserAdvice(); + AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); + pointcut.setExpression(String.format("execution(* %s.TestService.*(..))", getClass().getName())); + + // Test with default class loader first... + testAdvice(new DefaultPointcutAdvisor(pointcut, logAdvice), logAdvice, new TestServiceImpl(), "TestServiceImpl"); + + // Then try again with a different class loader on the target... + SimpleThrowawayClassLoader loader = new SimpleThrowawayClassLoader(TestServiceImpl.class.getClassLoader()); + // Make sure the interface is loaded from the parent class loader + loader.excludeClass(TestService.class.getName()); + loader.excludeClass(TestException.class.getName()); + TestService other = (TestService) loader.loadClass(TestServiceImpl.class.getName()).getDeclaredConstructor().newInstance(); + testAdvice(new DefaultPointcutAdvisor(pointcut, logAdvice), logAdvice, other, "TestServiceImpl"); + } + + private void testAdvice(Advisor advisor, LogUserAdvice logAdvice, TestService target, String message) + throws Exception { + testAdvice(advisor, logAdvice, target, message, false); + } + + private void testAdvice(Advisor advisor, LogUserAdvice logAdvice, TestService target, String message, + boolean proxyTargetClass) throws Exception { + + logAdvice.reset(); + + ProxyFactory factory = new ProxyFactory(target); + factory.setProxyTargetClass(proxyTargetClass); + factory.addAdvisor(advisor); + TestService bean = (TestService) factory.getProxy(); + + assertThat(logAdvice.getCountThrows()).isEqualTo(0); + assertThatExceptionOfType(TestException.class).isThrownBy( + bean::sayHello).withMessageContaining(message); + assertThat(logAdvice.getCountThrows()).isEqualTo(1); + } + + + public static class SimpleThrowawayClassLoader extends OverridingClassLoader { + + /** + * Create a new SimpleThrowawayClassLoader for the given class loader. + * @param parent the ClassLoader to build a throwaway ClassLoader for + */ + public SimpleThrowawayClassLoader(ClassLoader parent) { + super(parent); + } + } + + + @SuppressWarnings("serial") + public static class TestException extends RuntimeException { + + public TestException(String string) { + super(string); + } + } + + + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Inherited + public static @interface Log { + } + + + public static interface TestService { + + public String sayHello(); + } + + + @Log + public static class TestServiceImpl implements TestService { + + @Override + public String sayHello() { + throw new TestException("TestServiceImpl"); + } + } + + + public class LogUserAdvice implements MethodBeforeAdvice, ThrowsAdvice { + + private int countBefore = 0; + + private int countThrows = 0; + + @Override + public void before(Method method, Object[] objects, @Nullable Object o) throws Throwable { + countBefore++; + } + + public void afterThrowing(Exception ex) throws Throwable { + countThrows++; + throw ex; + } + + public int getCountBefore() { + return countBefore; + } + + public int getCountThrows() { + return countThrows; + } + + public void reset() { + countThrows = 0; + countBefore = 0; + } + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/TypePatternClassFilterTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/TypePatternClassFilterTests.java new file mode 100644 index 0000000..4337306 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/TypePatternClassFilterTests.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.testfixture.beans.CountingTestBean; +import org.springframework.beans.testfixture.beans.IOther; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.beans.testfixture.beans.subpkg.DeepBean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Unit tests for the {@link TypePatternClassFilter} class. + * + * @author Rod Johnson + * @author Rick Evans + * @author Chris Beams + * @author Sam Brannen + */ +class TypePatternClassFilterTests { + + @Test + void nullPattern() { + assertThatIllegalArgumentException().isThrownBy(() -> new TypePatternClassFilter(null)); + } + + @Test + void invalidPattern() { + assertThatIllegalArgumentException().isThrownBy(() -> new TypePatternClassFilter("-")); + } + + @Test + void invocationOfMatchesMethodBlowsUpWhenNoTypePatternHasBeenSet() throws Exception { + assertThatIllegalStateException().isThrownBy(() -> new TypePatternClassFilter().matches(String.class)); + } + + @Test + void validPatternMatching() { + TypePatternClassFilter tpcf = new TypePatternClassFilter("org.springframework.beans.testfixture.beans.*"); + + assertThat(tpcf.matches(TestBean.class)).as("Must match: in package").isTrue(); + assertThat(tpcf.matches(ITestBean.class)).as("Must match: in package").isTrue(); + assertThat(tpcf.matches(IOther.class)).as("Must match: in package").isTrue(); + + assertThat(tpcf.matches(DeepBean.class)).as("Must be excluded: in wrong package").isFalse(); + assertThat(tpcf.matches(BeanFactory.class)).as("Must be excluded: in wrong package").isFalse(); + assertThat(tpcf.matches(DefaultListableBeanFactory.class)).as("Must be excluded: in wrong package").isFalse(); + } + + @Test + void subclassMatching() { + TypePatternClassFilter tpcf = new TypePatternClassFilter("org.springframework.beans.testfixture.beans.ITestBean+"); + + assertThat(tpcf.matches(TestBean.class)).as("Must match: in package").isTrue(); + assertThat(tpcf.matches(ITestBean.class)).as("Must match: in package").isTrue(); + assertThat(tpcf.matches(CountingTestBean.class)).as("Must match: in package").isTrue(); + + assertThat(tpcf.matches(IOther.class)).as("Must be excluded: not subclass").isFalse(); + assertThat(tpcf.matches(DefaultListableBeanFactory.class)).as("Must be excluded: not subclass").isFalse(); + } + + @Test + void andOrNotReplacement() { + TypePatternClassFilter tpcf = new TypePatternClassFilter("java.lang.Object or java.lang.String"); + assertThat(tpcf.matches(Number.class)).as("matches Number").isFalse(); + assertThat(tpcf.matches(Object.class)).as("matches Object").isTrue(); + assertThat(tpcf.matches(String.class)).as("matchesString").isTrue(); + + tpcf = new TypePatternClassFilter("java.lang.Number+ and java.lang.Float"); + assertThat(tpcf.matches(Float.class)).as("matches Float").isTrue(); + assertThat(tpcf.matches(Double.class)).as("matches Double").isFalse(); + + tpcf = new TypePatternClassFilter("java.lang.Number+ and not java.lang.Float"); + assertThat(tpcf.matches(Float.class)).as("matches Float").isFalse(); + assertThat(tpcf.matches(Double.class)).as("matches Double").isTrue(); + } + + @Test + void testEquals() { + TypePatternClassFilter filter1 = new TypePatternClassFilter("org.springframework.beans.testfixture.beans.*"); + TypePatternClassFilter filter2 = new TypePatternClassFilter("org.springframework.beans.testfixture.beans.*"); + TypePatternClassFilter filter3 = new TypePatternClassFilter("org.springframework.tests.*"); + + assertThat(filter1).isEqualTo(filter2); + assertThat(filter1).isNotEqualTo(filter3); + } + + @Test + void testHashCode() { + TypePatternClassFilter filter1 = new TypePatternClassFilter("org.springframework.beans.testfixture.beans.*"); + TypePatternClassFilter filter2 = new TypePatternClassFilter("org.springframework.beans.testfixture.beans.*"); + TypePatternClassFilter filter3 = new TypePatternClassFilter("org.springframework.tests.*"); + + assertThat(filter1.hashCode()).isEqualTo(filter2.hashCode()); + assertThat(filter1.hashCode()).isNotEqualTo(filter3.hashCode()); + } + + @Test + void testToString() { + TypePatternClassFilter filter1 = new TypePatternClassFilter("org.springframework.beans.testfixture.beans.*"); + TypePatternClassFilter filter2 = new TypePatternClassFilter("org.springframework.beans.testfixture.beans.*"); + + assertThat(filter1.toString()) + .isEqualTo("org.springframework.aop.aspectj.TypePatternClassFilter: org.springframework.beans.testfixture.beans.*"); + assertThat(filter1.toString()).isEqualTo(filter2.toString()); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java new file mode 100644 index 0000000..612aa61 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java @@ -0,0 +1,1081 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.annotation; + +import java.io.FileNotFoundException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Method; +import java.lang.reflect.UndeclaredThrowableException; +import java.rmi.RemoteException; +import java.util.ArrayList; +import java.util.List; + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.After; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.DeclareParents; +import org.aspectj.lang.annotation.DeclarePrecedence; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import test.aop.DefaultLockable; +import test.aop.Lockable; +import test.aop.PerTargetAspect; +import test.aop.TwoAdviceAspect; + +import org.springframework.aop.Advisor; +import org.springframework.aop.framework.Advised; +import org.springframework.aop.framework.AopConfigException; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.interceptor.ExposeInvocationInterceptor; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.OrderComparator; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.util.ObjectUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Abstract tests for {@link AspectJAdvisorFactory} implementations. + * + *

See subclasses for tests of concrete factories. + * + * @author Rod Johnson + * @author Chris Beams + * @author Phillip Webb + * @author Sam Brannen + */ +abstract class AbstractAspectJAdvisorFactoryTests { + + /** + * To be overridden by concrete test subclasses. + * @return the fixture + */ + protected abstract AspectJAdvisorFactory getFixture(); + + + @Test + void rejectsPerCflowAspect() { + assertThatExceptionOfType(AopConfigException.class).isThrownBy(() -> + getFixture().getAdvisors( + new SingletonMetadataAwareAspectInstanceFactory(new PerCflowAspect(), "someBean"))) + .withMessageContaining("PERCFLOW"); + } + + @Test + void rejectsPerCflowBelowAspect() { + assertThatExceptionOfType(AopConfigException.class).isThrownBy(() -> + getFixture().getAdvisors( + new SingletonMetadataAwareAspectInstanceFactory(new PerCflowBelowAspect(), "someBean"))) + .withMessageContaining("PERCFLOWBELOW"); + } + + @Test + void perTargetAspect() throws SecurityException, NoSuchMethodException { + TestBean target = new TestBean(); + int realAge = 65; + target.setAge(realAge); + TestBean itb = (TestBean) createProxy(target, + getFixture().getAdvisors(new SingletonMetadataAwareAspectInstanceFactory(new PerTargetAspect(), "someBean")), + TestBean.class); + assertThat(itb.getAge()).as("Around advice must NOT apply").isEqualTo(realAge); + + Advised advised = (Advised) itb; + ReflectiveAspectJAdvisorFactory.SyntheticInstantiationAdvisor sia = + (ReflectiveAspectJAdvisorFactory.SyntheticInstantiationAdvisor) advised.getAdvisors()[1]; + assertThat(sia.getPointcut().getMethodMatcher().matches(TestBean.class.getMethod("getSpouse"), null)).isTrue(); + InstantiationModelAwarePointcutAdvisorImpl imapa = (InstantiationModelAwarePointcutAdvisorImpl) advised.getAdvisors()[3]; + LazySingletonAspectInstanceFactoryDecorator maaif = + (LazySingletonAspectInstanceFactoryDecorator) imapa.getAspectInstanceFactory(); + assertThat(maaif.isMaterialized()).isFalse(); + + // Check that the perclause pointcut is valid + assertThat(maaif.getAspectMetadata().getPerClausePointcut().getMethodMatcher().matches(TestBean.class.getMethod("getSpouse"), null)).isTrue(); + assertThat(imapa.getPointcut()).isNotSameAs(imapa.getDeclaredPointcut()); + + // Hit the method in the per clause to instantiate the aspect + itb.getSpouse(); + + assertThat(maaif.isMaterialized()).isTrue(); + + assertThat(itb.getAge()).as("Around advice must apply").isEqualTo(0); + assertThat(itb.getAge()).as("Around advice must apply").isEqualTo(1); + } + + @Test + void multiplePerTargetAspects() throws SecurityException, NoSuchMethodException { + TestBean target = new TestBean(); + int realAge = 65; + target.setAge(realAge); + + List advisors = new ArrayList<>(); + PerTargetAspect aspect1 = new PerTargetAspect(); + aspect1.count = 100; + aspect1.setOrder(10); + advisors.addAll( + getFixture().getAdvisors(new SingletonMetadataAwareAspectInstanceFactory(aspect1, "someBean1"))); + PerTargetAspect aspect2 = new PerTargetAspect(); + aspect2.setOrder(5); + advisors.addAll( + getFixture().getAdvisors(new SingletonMetadataAwareAspectInstanceFactory(aspect2, "someBean2"))); + OrderComparator.sort(advisors); + + TestBean itb = (TestBean) createProxy(target, advisors, TestBean.class); + assertThat(itb.getAge()).as("Around advice must NOT apply").isEqualTo(realAge); + + // Hit the method in the per clause to instantiate the aspect + itb.getSpouse(); + + assertThat(itb.getAge()).as("Around advice must apply").isEqualTo(0); + assertThat(itb.getAge()).as("Around advice must apply").isEqualTo(1); + } + + @Test + void multiplePerTargetAspectsWithOrderAnnotation() throws SecurityException, NoSuchMethodException { + TestBean target = new TestBean(); + int realAge = 65; + target.setAge(realAge); + + List advisors = new ArrayList<>(); + PerTargetAspectWithOrderAnnotation10 aspect1 = new PerTargetAspectWithOrderAnnotation10(); + aspect1.count = 100; + advisors.addAll( + getFixture().getAdvisors(new SingletonMetadataAwareAspectInstanceFactory(aspect1, "someBean1"))); + PerTargetAspectWithOrderAnnotation5 aspect2 = new PerTargetAspectWithOrderAnnotation5(); + advisors.addAll( + getFixture().getAdvisors(new SingletonMetadataAwareAspectInstanceFactory(aspect2, "someBean2"))); + OrderComparator.sort(advisors); + + TestBean itb = (TestBean) createProxy(target, advisors, TestBean.class); + assertThat(itb.getAge()).as("Around advice must NOT apply").isEqualTo(realAge); + + // Hit the method in the per clause to instantiate the aspect + itb.getSpouse(); + + assertThat(itb.getAge()).as("Around advice must apply").isEqualTo(0); + assertThat(itb.getAge()).as("Around advice must apply").isEqualTo(1); + } + + @Test + void perThisAspect() throws SecurityException, NoSuchMethodException { + TestBean target = new TestBean(); + int realAge = 65; + target.setAge(realAge); + TestBean itb = (TestBean) createProxy(target, + getFixture().getAdvisors(new SingletonMetadataAwareAspectInstanceFactory(new PerThisAspect(), "someBean")), + TestBean.class); + assertThat(itb.getAge()).as("Around advice must NOT apply").isEqualTo(realAge); + + Advised advised = (Advised) itb; + // Will be ExposeInvocationInterceptor, synthetic instantiation advisor, 2 method advisors + assertThat(advised.getAdvisors().length).isEqualTo(4); + ReflectiveAspectJAdvisorFactory.SyntheticInstantiationAdvisor sia = + (ReflectiveAspectJAdvisorFactory.SyntheticInstantiationAdvisor) advised.getAdvisors()[1]; + assertThat(sia.getPointcut().getMethodMatcher().matches(TestBean.class.getMethod("getSpouse"), null)).isTrue(); + InstantiationModelAwarePointcutAdvisorImpl imapa = (InstantiationModelAwarePointcutAdvisorImpl) advised.getAdvisors()[2]; + LazySingletonAspectInstanceFactoryDecorator maaif = + (LazySingletonAspectInstanceFactoryDecorator) imapa.getAspectInstanceFactory(); + assertThat(maaif.isMaterialized()).isFalse(); + + // Check that the perclause pointcut is valid + assertThat(maaif.getAspectMetadata().getPerClausePointcut().getMethodMatcher().matches(TestBean.class.getMethod("getSpouse"), null)).isTrue(); + assertThat(imapa.getPointcut()).isNotSameAs(imapa.getDeclaredPointcut()); + + // Hit the method in the per clause to instantiate the aspect + itb.getSpouse(); + + assertThat(maaif.isMaterialized()).isTrue(); + + assertThat(imapa.getDeclaredPointcut().getMethodMatcher().matches(TestBean.class.getMethod("getAge"), null)).isTrue(); + + assertThat(itb.getAge()).as("Around advice must apply").isEqualTo(0); + assertThat(itb.getAge()).as("Around advice must apply").isEqualTo(1); + } + + @Test + void perTypeWithinAspect() throws SecurityException, NoSuchMethodException { + TestBean target = new TestBean(); + int realAge = 65; + target.setAge(realAge); + PerTypeWithinAspectInstanceFactory aif = new PerTypeWithinAspectInstanceFactory(); + TestBean itb = (TestBean) createProxy(target, getFixture().getAdvisors(aif), TestBean.class); + assertThat(aif.getInstantiationCount()).as("No method calls").isEqualTo(0); + assertThat(itb.getAge()).as("Around advice must now apply").isEqualTo(0); + + Advised advised = (Advised) itb; + // Will be ExposeInvocationInterceptor, synthetic instantiation advisor, 2 method advisors + assertThat(advised.getAdvisors().length).isEqualTo(4); + ReflectiveAspectJAdvisorFactory.SyntheticInstantiationAdvisor sia = + (ReflectiveAspectJAdvisorFactory.SyntheticInstantiationAdvisor) advised.getAdvisors()[1]; + assertThat(sia.getPointcut().getMethodMatcher().matches(TestBean.class.getMethod("getSpouse"), null)).isTrue(); + InstantiationModelAwarePointcutAdvisorImpl imapa = (InstantiationModelAwarePointcutAdvisorImpl) advised.getAdvisors()[2]; + LazySingletonAspectInstanceFactoryDecorator maaif = + (LazySingletonAspectInstanceFactoryDecorator) imapa.getAspectInstanceFactory(); + assertThat(maaif.isMaterialized()).isTrue(); + + // Check that the perclause pointcut is valid + assertThat(maaif.getAspectMetadata().getPerClausePointcut().getMethodMatcher().matches(TestBean.class.getMethod("getSpouse"), null)).isTrue(); + assertThat(imapa.getPointcut()).isNotSameAs(imapa.getDeclaredPointcut()); + + // Hit the method in the per clause to instantiate the aspect + itb.getSpouse(); + + assertThat(maaif.isMaterialized()).isTrue(); + + assertThat(imapa.getDeclaredPointcut().getMethodMatcher().matches(TestBean.class.getMethod("getAge"), null)).isTrue(); + + assertThat(itb.getAge()).as("Around advice must still apply").isEqualTo(1); + assertThat(itb.getAge()).as("Around advice must still apply").isEqualTo(2); + + TestBean itb2 = (TestBean) createProxy(target, getFixture().getAdvisors(aif), TestBean.class); + assertThat(aif.getInstantiationCount()).isEqualTo(1); + assertThat(itb2.getAge()).as("Around advice be independent for second instance").isEqualTo(0); + assertThat(aif.getInstantiationCount()).isEqualTo(2); + } + + @Test + void namedPointcutAspectWithFQN() { + namedPointcuts(new NamedPointcutAspectWithFQN()); + } + + @Test + void namedPointcutAspectWithoutFQN() { + namedPointcuts(new NamedPointcutAspectWithoutFQN()); + } + + @Test + void namedPointcutFromAspectLibrary() { + namedPointcuts(new NamedPointcutAspectFromLibrary()); + } + + @Test + void namedPointcutFromAspectLibraryWithBinding() { + TestBean target = new TestBean(); + ITestBean itb = (ITestBean) createProxy(target, + getFixture().getAdvisors(new SingletonMetadataAwareAspectInstanceFactory( + new NamedPointcutAspectFromLibraryWithBinding(), "someBean")), + ITestBean.class); + itb.setAge(10); + assertThat(itb.getAge()).as("Around advice must apply").isEqualTo(20); + assertThat(target.getAge()).isEqualTo(20); + } + + private void namedPointcuts(Object aspectInstance) { + TestBean target = new TestBean(); + int realAge = 65; + target.setAge(realAge); + ITestBean itb = (ITestBean) createProxy(target, + getFixture().getAdvisors(new SingletonMetadataAwareAspectInstanceFactory(aspectInstance, "someBean")), + ITestBean.class); + assertThat(itb.getAge()).as("Around advice must apply").isEqualTo(-1); + assertThat(target.getAge()).isEqualTo(realAge); + } + + @Test + void bindingWithSingleArg() { + TestBean target = new TestBean(); + ITestBean itb = (ITestBean) createProxy(target, + getFixture().getAdvisors( + new SingletonMetadataAwareAspectInstanceFactory(new BindingAspectWithSingleArg(), "someBean")), + ITestBean.class); + itb.setAge(10); + assertThat(itb.getAge()).as("Around advice must apply").isEqualTo(20); + assertThat(target.getAge()).isEqualTo(20); + } + + @Test + void bindingWithMultipleArgsDifferentlyOrdered() { + ManyValuedArgs target = new ManyValuedArgs(); + ManyValuedArgs mva = (ManyValuedArgs) createProxy(target, + getFixture().getAdvisors( + new SingletonMetadataAwareAspectInstanceFactory(new ManyValuedArgs(), "someBean")), + ManyValuedArgs.class); + + String a = "a"; + int b = 12; + int c = 25; + String d = "d"; + StringBuffer e = new StringBuffer("stringbuf"); + String expectedResult = a + b+ c + d + e; + assertThat(mva.mungeArgs(a, b, c, d, e)).isEqualTo(expectedResult); + } + + /** + * In this case the introduction will be made. + */ + @Test + void introductionOnTargetNotImplementingInterface() { + NotLockable notLockableTarget = new NotLockable(); + assertThat(notLockableTarget instanceof Lockable).isFalse(); + NotLockable notLockable1 = (NotLockable) createProxy(notLockableTarget, + getFixture().getAdvisors( + new SingletonMetadataAwareAspectInstanceFactory(new MakeLockable(), "someBean")), + NotLockable.class); + assertThat(notLockable1 instanceof Lockable).isTrue(); + Lockable lockable = (Lockable) notLockable1; + assertThat(lockable.locked()).isFalse(); + lockable.lock(); + assertThat(lockable.locked()).isTrue(); + + NotLockable notLockable2Target = new NotLockable(); + NotLockable notLockable2 = (NotLockable) createProxy(notLockable2Target, + getFixture().getAdvisors( + new SingletonMetadataAwareAspectInstanceFactory(new MakeLockable(), "someBean")), + NotLockable.class); + assertThat(notLockable2 instanceof Lockable).isTrue(); + Lockable lockable2 = (Lockable) notLockable2; + assertThat(lockable2.locked()).isFalse(); + notLockable2.setIntValue(1); + lockable2.lock(); + assertThatIllegalStateException().isThrownBy(() -> + notLockable2.setIntValue(32)); + assertThat(lockable2.locked()).isTrue(); + } + + @Test + void introductionAdvisorExcludedFromTargetImplementingInterface() { + assertThat(AopUtils.findAdvisorsThatCanApply( + getFixture().getAdvisors( + new SingletonMetadataAwareAspectInstanceFactory(new MakeLockable(), "someBean")), + CannotBeUnlocked.class).isEmpty()).isTrue(); + assertThat(AopUtils.findAdvisorsThatCanApply(getFixture().getAdvisors( + new SingletonMetadataAwareAspectInstanceFactory(new MakeLockable(),"someBean")), NotLockable.class).size()).isEqualTo(2); + } + + @Test + void introductionOnTargetImplementingInterface() { + CannotBeUnlocked target = new CannotBeUnlocked(); + Lockable proxy = (Lockable) createProxy(target, + // Ensure that we exclude + AopUtils.findAdvisorsThatCanApply( + getFixture().getAdvisors( + new SingletonMetadataAwareAspectInstanceFactory(new MakeLockable(), "someBean")), + CannotBeUnlocked.class + ), + CannotBeUnlocked.class); + assertThat(proxy).isInstanceOf(Lockable.class); + Lockable lockable = proxy; + assertThat(lockable.locked()).as("Already locked").isTrue(); + lockable.lock(); + assertThat(lockable.locked()).as("Real target ignores locking").isTrue(); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + lockable.unlock()); + } + + @Test + void introductionOnTargetExcludedByTypePattern() { + ArrayList target = new ArrayList<>(); + List proxy = (List) createProxy(target, + AopUtils.findAdvisorsThatCanApply( + getFixture().getAdvisors(new SingletonMetadataAwareAspectInstanceFactory(new MakeLockable(), "someBean")), + List.class + ), + List.class); + assertThat(proxy instanceof Lockable).as("Type pattern must have excluded mixin").isFalse(); + } + + @Test + void introductionBasedOnAnnotationMatch_SPR5307() { + AnnotatedTarget target = new AnnotatedTargetImpl(); + List advisors = getFixture().getAdvisors( + new SingletonMetadataAwareAspectInstanceFactory(new MakeAnnotatedTypeModifiable(), "someBean")); + Object proxy = createProxy(target, advisors, AnnotatedTarget.class); + System.out.println(advisors.get(1)); + assertThat(proxy instanceof Lockable).isTrue(); + Lockable lockable = (Lockable)proxy; + lockable.locked(); + } + + // TODO: Why does this test fail? It hasn't been run before, so it maybe never actually passed... + @Test + @Disabled + void introductionWithArgumentBinding() { + TestBean target = new TestBean(); + + List advisors = getFixture().getAdvisors( + new SingletonMetadataAwareAspectInstanceFactory(new MakeITestBeanModifiable(), "someBean")); + advisors.addAll(getFixture().getAdvisors( + new SingletonMetadataAwareAspectInstanceFactory(new MakeLockable(), "someBean"))); + + Modifiable modifiable = (Modifiable) createProxy(target, advisors, ITestBean.class); + assertThat(modifiable).isInstanceOf(Modifiable.class); + Lockable lockable = (Lockable) modifiable; + assertThat(lockable.locked()).isFalse(); + + ITestBean itb = (ITestBean) modifiable; + assertThat(modifiable.isModified()).isFalse(); + int oldAge = itb.getAge(); + itb.setAge(oldAge + 1); + assertThat(modifiable.isModified()).isTrue(); + modifiable.acceptChanges(); + assertThat(modifiable.isModified()).isFalse(); + itb.setAge(itb.getAge()); + assertThat(modifiable.isModified()).as("Setting same value does not modify").isFalse(); + itb.setName("And now for something completely different"); + assertThat(modifiable.isModified()).isTrue(); + + lockable.lock(); + assertThat(lockable.locked()).isTrue(); + assertThatIllegalStateException().as("Should be locked").isThrownBy(() -> + itb.setName("Else")); + lockable.unlock(); + itb.setName("Tony"); + } + + @Test + void aspectMethodThrowsExceptionLegalOnSignature() { + TestBean target = new TestBean(); + UnsupportedOperationException expectedException = new UnsupportedOperationException(); + List advisors = getFixture().getAdvisors( + new SingletonMetadataAwareAspectInstanceFactory(new ExceptionThrowingAspect(expectedException), "someBean")); + assertThat(advisors.size()).as("One advice method was found").isEqualTo(1); + ITestBean itb = (ITestBean) createProxy(target, advisors, ITestBean.class); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy( + itb::getAge); + } + + // TODO document this behaviour. + // Is it different AspectJ behaviour, at least for checked exceptions? + @Test + void aspectMethodThrowsExceptionIllegalOnSignature() { + TestBean target = new TestBean(); + RemoteException expectedException = new RemoteException(); + List advisors = getFixture().getAdvisors( + new SingletonMetadataAwareAspectInstanceFactory(new ExceptionThrowingAspect(expectedException), "someBean")); + assertThat(advisors.size()).as("One advice method was found").isEqualTo(1); + ITestBean itb = (ITestBean) createProxy(target, advisors, ITestBean.class); + assertThatExceptionOfType(UndeclaredThrowableException.class).isThrownBy( + itb::getAge).withCause(expectedException); + } + + protected Object createProxy(Object target, List advisors, Class... interfaces) { + ProxyFactory pf = new ProxyFactory(target); + if (interfaces.length > 1 || interfaces[0].isInterface()) { + pf.setInterfaces(interfaces); + } + else { + pf.setProxyTargetClass(true); + } + + // Required everywhere we use AspectJ proxies + pf.addAdvice(ExposeInvocationInterceptor.INSTANCE); + pf.addAdvisors(advisors); + + pf.setExposeProxy(true); + return pf.getProxy(); + } + + @Test + void twoAdvicesOnOneAspect() { + TestBean target = new TestBean(); + TwoAdviceAspect twoAdviceAspect = new TwoAdviceAspect(); + List advisors = getFixture().getAdvisors( + new SingletonMetadataAwareAspectInstanceFactory(twoAdviceAspect, "someBean")); + assertThat(advisors.size()).as("Two advice methods found").isEqualTo(2); + ITestBean itb = (ITestBean) createProxy(target, advisors, ITestBean.class); + itb.setName(""); + assertThat(itb.getAge()).isEqualTo(0); + int newAge = 32; + itb.setAge(newAge); + assertThat(itb.getAge()).isEqualTo(1); + } + + @Test + void afterAdviceTypes() throws Exception { + InvocationTrackingAspect aspect = new InvocationTrackingAspect(); + List advisors = getFixture().getAdvisors( + new SingletonMetadataAwareAspectInstanceFactory(aspect, "exceptionHandlingAspect")); + Echo echo = (Echo) createProxy(new Echo(), advisors, Echo.class); + + assertThat(aspect.invocations).isEmpty(); + assertThat(echo.echo(42)).isEqualTo(42); + assertThat(aspect.invocations).containsExactly("around - start", "before", "after returning", "after", "around - end"); + + aspect.invocations.clear(); + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(() -> echo.echo(new FileNotFoundException())); + assertThat(aspect.invocations).containsExactly("around - start", "before", "after throwing", "after", "around - end"); + } + + @Test + void failureWithoutExplicitDeclarePrecedence() { + TestBean target = new TestBean(); + MetadataAwareAspectInstanceFactory aspectInstanceFactory = new SingletonMetadataAwareAspectInstanceFactory( + new NoDeclarePrecedenceShouldFail(), "someBean"); + ITestBean itb = (ITestBean) createProxy(target, + getFixture().getAdvisors(aspectInstanceFactory), ITestBean.class); + itb.getAge(); + } + + @Test + void declarePrecedenceNotSupported() { + TestBean target = new TestBean(); + assertThatIllegalArgumentException().isThrownBy(() -> { + MetadataAwareAspectInstanceFactory aspectInstanceFactory = new SingletonMetadataAwareAspectInstanceFactory( + new DeclarePrecedenceShouldSucceed(), "someBean"); + createProxy(target, getFixture().getAdvisors(aspectInstanceFactory), ITestBean.class); + }); + } + + + @Aspect("percflow(execution(* *(..)))") + static class PerCflowAspect { + } + + + @Aspect("percflowbelow(execution(* *(..)))") + static class PerCflowBelowAspect { + } + + + @Aspect("pertarget(execution(* *.getSpouse()))") + @Order(10) + static class PerTargetAspectWithOrderAnnotation10 { + + int count; + + @Around("execution(int *.getAge())") + int returnCountAsAge() { + return count++; + } + + @Before("execution(void *.set*(int))") + void countSetter() { + ++count; + } + } + + + @Aspect("pertarget(execution(* *.getSpouse()))") + @Order(5) + static class PerTargetAspectWithOrderAnnotation5 { + + int count; + + @Around("execution(int *.getAge())") + int returnCountAsAge() { + return count++; + } + + @Before("execution(void *.set*(int))") + void countSetter() { + ++count; + } + } + + + @Aspect("pertypewithin(org.springframework.beans.testfixture.beans.IOther+)") + static class PerTypeWithinAspect { + + int count; + + @Around("execution(int *.getAge())") + int returnCountAsAge() { + return count++; + } + + @Before("execution(void *.*(..))") + void countAnythingVoid() { + ++count; + } + } + + + private class PerTypeWithinAspectInstanceFactory implements MetadataAwareAspectInstanceFactory { + + private int count; + + int getInstantiationCount() { + return this.count; + } + + @Override + public Object getAspectInstance() { + ++this.count; + return new PerTypeWithinAspect(); + } + + @Override + public ClassLoader getAspectClassLoader() { + return PerTypeWithinAspect.class.getClassLoader(); + } + + @Override + public AspectMetadata getAspectMetadata() { + return new AspectMetadata(PerTypeWithinAspect.class, "perTypeWithin"); + } + + @Override + public Object getAspectCreationMutex() { + return this; + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + } + + + @Aspect + static class NamedPointcutAspectWithFQN { + + @SuppressWarnings("unused") + private ITestBean fieldThatShouldBeIgnoredBySpringAtAspectJProcessing = new TestBean(); + + @Pointcut("execution(* getAge())") + void getAge() { + } + + @Around("org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactoryTests.NamedPointcutAspectWithFQN.getAge()") + int changeReturnValue(ProceedingJoinPoint pjp) { + return -1; + } + } + + + @Aspect + static class NamedPointcutAspectWithoutFQN { + + @Pointcut("execution(* getAge())") + void getAge() { + } + + @Around("getAge()") + int changeReturnValue(ProceedingJoinPoint pjp) { + return -1; + } + } + + + @Aspect + static class NamedPointcutAspectFromLibrary { + + @Around("org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactoryTests.Library.propertyAccess()") + int changeReturnType(ProceedingJoinPoint pjp) { + return -1; + } + + @Around(value="org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactoryTests.Library.integerArgOperation(x)", argNames="x") + void doubleArg(ProceedingJoinPoint pjp, int x) throws Throwable { + pjp.proceed(new Object[] {x*2}); + } + } + + + @Aspect + static class Library { + + @Pointcut("execution(!void get*())") + void propertyAccess() {} + + @Pointcut("execution(* *(..)) && args(i)") + void integerArgOperation(int i) {} + } + + + @Aspect + static class NamedPointcutAspectFromLibraryWithBinding { + + @Around(value="org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactoryTests.Library.integerArgOperation(x)", argNames="x") + void doubleArg(ProceedingJoinPoint pjp, int x) throws Throwable { + pjp.proceed(new Object[] {x*2}); + } + } + + + @Aspect + static class BindingAspectWithSingleArg { + + @Pointcut(value="args(a)", argNames="a") + void setAge(int a) {} + + @Around(value="setAge(age)",argNames="age") + // @ArgNames({"age"}) // AMC needs more work here? ignoring pjp arg... ok?? + // argNames should be supported in Around as it is in Pointcut + void changeReturnType(ProceedingJoinPoint pjp, int age) throws Throwable { + pjp.proceed(new Object[] {age*2}); + } + } + + + @Aspect + static class ManyValuedArgs { + + String mungeArgs(String a, int b, int c, String d, StringBuffer e) { + return a + b + c + d + e; + } + + @Around(value="execution(String mungeArgs(..)) && args(a, b, c, d, e)", argNames="b,c,d,e,a") + String reverseAdvice(ProceedingJoinPoint pjp, int b, int c, String d, StringBuffer e, String a) throws Throwable { + assertThat(pjp.proceed()).isEqualTo(a + b+ c+ d+ e); + return a + b + c + d + e; + } + } + + + @Aspect + static class ExceptionThrowingAspect { + + private final Exception ex; + + ExceptionThrowingAspect(Exception ex) { + this.ex = ex; + } + + @Before("execution(* getAge())") + void throwException() throws Exception { + throw ex; + } + } + + + static class Echo { + + Object echo(Object o) throws Exception { + if (o instanceof Exception) { + throw (Exception) o; + } + return o; + } + } + + + @Aspect + private static class InvocationTrackingAspect { + + List invocations = new ArrayList<>(); + + + @Pointcut("execution(* echo(*))") + void echo() { + } + + @Around("echo()") + Object around(ProceedingJoinPoint joinPoint) throws Throwable { + invocations.add("around - start"); + try { + return joinPoint.proceed(); + } + finally { + invocations.add("around - end"); + } + } + + @Before("echo()") + void before() { + invocations.add("before"); + } + + @AfterReturning("echo()") + void afterReturning() { + invocations.add("after returning"); + } + + @AfterThrowing("echo()") + void afterThrowing() { + invocations.add("after throwing"); + } + + @After("echo()") + void after() { + invocations.add("after"); + } + } + + + @Aspect + static class NoDeclarePrecedenceShouldFail { + + @Pointcut("execution(int *.getAge())") + void getAge() { + } + + @Before("getAge()") + void blowUpButDoesntMatterBecauseAroundAdviceWontLetThisBeInvoked() { + throw new IllegalStateException(); + } + + @Around("getAge()") + int preventExecution(ProceedingJoinPoint pjp) { + return 666; + } + } + + + @Aspect + @DeclarePrecedence("test..*") + static class DeclarePrecedenceShouldSucceed { + + @Pointcut("execution(int *.getAge())") + void getAge() { + } + + @Before("getAge()") + void blowUpButDoesntMatterBecauseAroundAdviceWontLetThisBeInvoked() { + throw new IllegalStateException(); + } + + @Around("getAge()") + int preventExecution(ProceedingJoinPoint pjp) { + return 666; + } + } + +} + + +/** + * Add a DeclareParents field in concrete subclasses, to identify + * the type pattern to apply the introduction to. + * + * @author Rod Johnson + * @since 2.0 + */ +@Aspect +abstract class AbstractMakeModifiable { + + interface MutableModifiable extends Modifiable { + + void markDirty(); + } + + static class ModifiableImpl implements MutableModifiable { + + private boolean modified; + + @Override + public void acceptChanges() { + modified = false; + } + + @Override + public boolean isModified() { + return modified; + } + + @Override + public void markDirty() { + this.modified = true; + } + } + + @Before(value="execution(void set*(*)) && this(modifiable) && args(newValue)", argNames="modifiable,newValue") + void recordModificationIfSetterArgumentDiffersFromOldValue( + JoinPoint jp, MutableModifiable mixin, Object newValue) { + + /* + * We use the mixin to check and, if necessary, change, + * modification status. We need the JoinPoint to get the + * setter method. We use newValue for comparison. + * We try to invoke the getter if possible. + */ + + if (mixin.isModified()) { + // Already changed, don't need to change again + //System.out.println("changed"); + return; + } + + // Find the current raw value, by invoking the corresponding setter + Method correspondingGetter = getGetterFromSetter(((MethodSignature) jp.getSignature()).getMethod()); + boolean modified = true; + if (correspondingGetter != null) { + try { + Object oldValue = correspondingGetter.invoke(jp.getTarget()); + //System.out.println("Old value=" + oldValue + "; new=" + newValue); + modified = !ObjectUtils.nullSafeEquals(oldValue, newValue); + } + catch (Exception ex) { + ex.printStackTrace(); + // Don't sweat on exceptions; assume value was modified + } + } + else { + //System.out.println("cannot get getter for " + jp); + } + if (modified) { + mixin.markDirty(); + } + } + + private Method getGetterFromSetter(Method setter) { + String getterName = setter.getName().replaceFirst("set", "get"); + try { + return setter.getDeclaringClass().getMethod(getterName); + } + catch (NoSuchMethodException ex) { + // must be write only + return null; + } + } + +} + + +/** + * Adds a declare parents pointcut. + * @author Rod Johnson + * @since 2.0 + */ +@Aspect +class MakeITestBeanModifiable extends AbstractMakeModifiable { + + @DeclareParents(value = "org.springframework.beans.testfixture.beans.ITestBean+", + defaultImpl=ModifiableImpl.class) + static MutableModifiable mixin; + +} + + +/** + * Adds a declare parents pointcut - spr5307 + * @author Andy Clement + * @since 3.0 + */ +@Aspect +class MakeAnnotatedTypeModifiable extends AbstractMakeModifiable { + + @DeclareParents(value = "(@org.springframework.aop.aspectj.annotation.Measured *)", + defaultImpl = DefaultLockable.class) + static Lockable mixin; + +} + + +/** + * Demonstrates introductions, AspectJ annotation style. + */ +@Aspect +class MakeLockable { + + @DeclareParents(value = "org.springframework..*", defaultImpl = DefaultLockable.class) + static Lockable mixin; + + @Before(value="execution(void set*(*)) && this(mixin)", argNames="mixin") + void checkNotLocked( Lockable mixin) { + // Can also obtain the mixin (this) this way + //Lockable mixin = (Lockable) jp.getThis(); + if (mixin.locked()) { + throw new IllegalStateException(); + } + } + +} + + +class CannotBeUnlocked implements Lockable, Comparable { + + @Override + public void lock() { + } + + @Override + public void unlock() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean locked() { + return true; + } + + @Override + public int compareTo(Object arg0) { + throw new UnsupportedOperationException(); + } + +} + + +/** + * Used as a mixin. + * + * @author Rod Johnson + */ +interface Modifiable { + + boolean isModified(); + + void acceptChanges(); + +} + + +/** + * Used as a target. + * @author Andy Clement + */ +interface AnnotatedTarget { +} + + +@Measured +class AnnotatedTargetImpl implements AnnotatedTarget { +} + + +@Retention(RetentionPolicy.RUNTIME) +@interface Measured {} + +class NotLockable { + + private int intValue; + + int getIntValue() { + return intValue; + } + + void setIntValue(int intValue) { + this.intValue = intValue; + } + +} + + +@Aspect("perthis(execution(* *.getSpouse()))") +class PerThisAspect { + + int count; + + // Just to check that this doesn't cause problems with introduction processing + @SuppressWarnings("unused") + private ITestBean fieldThatShouldBeIgnoredBySpringAtAspectJProcessing = new TestBean(); + + @Around("execution(int *.getAge())") + int returnCountAsAge() { + return count++; + } + + @Before("execution(void *.set*(int))") + void countSetter() { + ++count; + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/ArgumentBindingTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/ArgumentBindingTests.java new file mode 100644 index 0000000..835f4a4 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/ArgumentBindingTests.java @@ -0,0 +1,132 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Method; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.aspectj.AspectJAdviceParameterNameDiscoverer; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * @author Adrian Colyer + * @author Juergen Hoeller + * @author Chris Beams + */ +public class ArgumentBindingTests { + + @Test + public void testBindingInPointcutUsedByAdvice() { + TestBean tb = new TestBean(); + AspectJProxyFactory proxyFactory = new AspectJProxyFactory(tb); + proxyFactory.addAspect(NamedPointcutWithArgs.class); + + ITestBean proxiedTestBean = proxyFactory.getProxy(); + assertThatIllegalArgumentException().isThrownBy(() -> + proxiedTestBean.setName("Supercalifragalisticexpialidocious")); + } + + @Test + public void testAnnotationArgumentNameBinding() { + TransactionalBean tb = new TransactionalBean(); + AspectJProxyFactory proxyFactory = new AspectJProxyFactory(tb); + proxyFactory.addAspect(PointcutWithAnnotationArgument.class); + + ITransactionalBean proxiedTestBean = proxyFactory.getProxy(); + assertThatIllegalStateException().isThrownBy( + proxiedTestBean::doInTransaction); + } + + @Test + public void testParameterNameDiscoverWithReferencePointcut() throws Exception { + AspectJAdviceParameterNameDiscoverer discoverer = + new AspectJAdviceParameterNameDiscoverer("somepc(formal) && set(* *)"); + discoverer.setRaiseExceptions(true); + Method methodUsedForParameterTypeDiscovery = + getClass().getMethod("methodWithOneParam", String.class); + String[] pnames = discoverer.getParameterNames(methodUsedForParameterTypeDiscovery); + assertThat(pnames.length).as("one parameter name").isEqualTo(1); + assertThat(pnames[0]).isEqualTo("formal"); + } + + + public void methodWithOneParam(String aParam) { + } + + + public interface ITransactionalBean { + + @Transactional + void doInTransaction(); + } + + + public static class TransactionalBean implements ITransactionalBean { + + @Override + @Transactional + public void doInTransaction() { + } + } + +} + +/** + * Represents Spring's Transactional annotation without actually introducing the dependency + */ +@Retention(RetentionPolicy.RUNTIME) +@interface Transactional { +} + + +@Aspect +class PointcutWithAnnotationArgument { + + @Around(value = "execution(* org.springframework..*.*(..)) && @annotation(transaction)") + public Object around(ProceedingJoinPoint pjp, Transactional transaction) throws Throwable { + System.out.println("Invoked with transaction " + transaction); + throw new IllegalStateException(); + } + +} + + +@Aspect +class NamedPointcutWithArgs { + + @Pointcut("execution(* *(..)) && args(s,..)") + public void pointcutWithArgs(String s) {} + + @Around("pointcutWithArgs(aString)") + public Object doAround(ProceedingJoinPoint pjp, String aString) throws Throwable { + System.out.println("got '" + aString + "' at '" + pjp + "'"); + throw new IllegalArgumentException(aString); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectJPointcutAdvisorTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectJPointcutAdvisorTests.java new file mode 100644 index 0000000..806f305 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectJPointcutAdvisorTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.annotation; + +import org.junit.jupiter.api.Test; +import test.aop.PerTargetAspect; + +import org.springframework.aop.Pointcut; +import org.springframework.aop.aspectj.AspectJExpressionPointcut; +import org.springframework.aop.aspectj.AspectJExpressionPointcutTests; +import org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactoryTests.ExceptionThrowingAspect; +import org.springframework.aop.framework.AopConfigException; +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Rod Johnson + * @author Chris Beams + */ +public class AspectJPointcutAdvisorTests { + + private final AspectJAdvisorFactory af = new ReflectiveAspectJAdvisorFactory(); + + + @Test + public void testSingleton() throws SecurityException, NoSuchMethodException { + AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); + ajexp.setExpression(AspectJExpressionPointcutTests.MATCH_ALL_METHODS); + + InstantiationModelAwarePointcutAdvisorImpl ajpa = new InstantiationModelAwarePointcutAdvisorImpl( + ajexp, TestBean.class.getMethod("getAge"), af, + new SingletonMetadataAwareAspectInstanceFactory(new ExceptionThrowingAspect(null), "someBean"), + 1, "someBean"); + + assertThat(ajpa.getAspectMetadata().getPerClausePointcut()).isSameAs(Pointcut.TRUE); + assertThat(ajpa.isPerInstance()).isFalse(); + } + + @Test + public void testPerTarget() throws SecurityException, NoSuchMethodException { + AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(); + ajexp.setExpression(AspectJExpressionPointcutTests.MATCH_ALL_METHODS); + + InstantiationModelAwarePointcutAdvisorImpl ajpa = new InstantiationModelAwarePointcutAdvisorImpl( + ajexp, TestBean.class.getMethod("getAge"), af, + new SingletonMetadataAwareAspectInstanceFactory(new PerTargetAspect(), "someBean"), + 1, "someBean"); + + assertThat(ajpa.getAspectMetadata().getPerClausePointcut()).isNotSameAs(Pointcut.TRUE); + boolean condition = ajpa.getAspectMetadata().getPerClausePointcut() instanceof AspectJExpressionPointcut; + assertThat(condition).isTrue(); + assertThat(ajpa.isPerInstance()).isTrue(); + + assertThat(ajpa.getAspectMetadata().getPerClausePointcut().getClassFilter().matches(TestBean.class)).isTrue(); + assertThat(ajpa.getAspectMetadata().getPerClausePointcut().getMethodMatcher().matches( + TestBean.class.getMethod("getAge"), TestBean.class)).isFalse(); + + assertThat(ajpa.getAspectMetadata().getPerClausePointcut().getMethodMatcher().matches( + TestBean.class.getMethod("getSpouse"), TestBean.class)).isTrue(); + } + + @Test + public void testPerCflowTarget() { + assertThatExceptionOfType(AopConfigException.class).isThrownBy(() -> + testIllegalInstantiationModel(AbstractAspectJAdvisorFactoryTests.PerCflowAspect.class)); + } + + @Test + public void testPerCflowBelowTarget() { + assertThatExceptionOfType(AopConfigException.class).isThrownBy(() -> + testIllegalInstantiationModel(AbstractAspectJAdvisorFactoryTests.PerCflowBelowAspect.class)); + } + + private void testIllegalInstantiationModel(Class c) throws AopConfigException { + new AspectMetadata(c, "someBean"); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectMetadataTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectMetadataTests.java new file mode 100644 index 0000000..637baa2 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectMetadataTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.annotation; + +import org.aspectj.lang.reflect.PerClauseKind; +import org.junit.jupiter.api.Test; +import test.aop.PerTargetAspect; + +import org.springframework.aop.Pointcut; +import org.springframework.aop.aspectj.AspectJExpressionPointcut; +import org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactoryTests.ExceptionThrowingAspect; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @since 2.0 + * @author Rod Johnson + * @author Chris Beams + * @author Sam Brannen + */ +class AspectMetadataTests { + + @Test + void notAnAspect() { + assertThatIllegalArgumentException().isThrownBy(() -> new AspectMetadata(String.class, "someBean")); + } + + @Test + void singletonAspect() { + AspectMetadata am = new AspectMetadata(ExceptionThrowingAspect.class, "someBean"); + assertThat(am.isPerThisOrPerTarget()).isFalse(); + assertThat(am.getPerClausePointcut()).isSameAs(Pointcut.TRUE); + assertThat(am.getAjType().getPerClause().getKind()).isEqualTo(PerClauseKind.SINGLETON); + } + + @Test + void perTargetAspect() { + AspectMetadata am = new AspectMetadata(PerTargetAspect.class, "someBean"); + assertThat(am.isPerThisOrPerTarget()).isTrue(); + assertThat(am.getPerClausePointcut()).isNotSameAs(Pointcut.TRUE); + assertThat(am.getAjType().getPerClause().getKind()).isEqualTo(PerClauseKind.PERTARGET); + assertThat(am.getPerClausePointcut()).isInstanceOf(AspectJExpressionPointcut.class); + assertThat(((AspectJExpressionPointcut) am.getPerClausePointcut()).getExpression()) + .isEqualTo("execution(* *.getSpouse())"); + } + + @Test + void perThisAspect() { + AspectMetadata am = new AspectMetadata(PerThisAspect.class, "someBean"); + assertThat(am.isPerThisOrPerTarget()).isTrue(); + assertThat(am.getPerClausePointcut()).isNotSameAs(Pointcut.TRUE); + assertThat(am.getAjType().getPerClause().getKind()).isEqualTo(PerClauseKind.PERTHIS); + assertThat(am.getPerClausePointcut()).isInstanceOf(AspectJExpressionPointcut.class); + assertThat(((AspectJExpressionPointcut) am.getPerClausePointcut()).getExpression()) + .isEqualTo("execution(* *.getSpouse())"); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectProxyFactoryTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectProxyFactoryTests.java new file mode 100644 index 0000000..a00a27b --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectProxyFactoryTests.java @@ -0,0 +1,234 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.annotation; + +import java.io.Serializable; +import java.util.Arrays; + +import org.apache.commons.logging.LogFactory; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.junit.jupiter.api.Test; +import test.aop.PerThisAspect; + +import org.springframework.core.testfixture.io.SerializationTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Rob Harrop + * @author Juergen Hoeller + * @author Chris Beams + */ +public class AspectProxyFactoryTests { + + @Test + public void testWithNonAspect() { + AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TestBean()); + assertThatIllegalArgumentException().isThrownBy(() -> + proxyFactory.addAspect(TestBean.class)); + } + + @Test + public void testWithSimpleAspect() throws Exception { + TestBean bean = new TestBean(); + bean.setAge(2); + AspectJProxyFactory proxyFactory = new AspectJProxyFactory(bean); + proxyFactory.addAspect(MultiplyReturnValue.class); + ITestBean proxy = proxyFactory.getProxy(); + assertThat(proxy.getAge()).as("Multiplication did not occur").isEqualTo((bean.getAge() * 2)); + } + + @Test + public void testWithPerThisAspect() throws Exception { + TestBean bean1 = new TestBean(); + TestBean bean2 = new TestBean(); + + AspectJProxyFactory pf1 = new AspectJProxyFactory(bean1); + pf1.addAspect(PerThisAspect.class); + + AspectJProxyFactory pf2 = new AspectJProxyFactory(bean2); + pf2.addAspect(PerThisAspect.class); + + ITestBean proxy1 = pf1.getProxy(); + ITestBean proxy2 = pf2.getProxy(); + + assertThat(proxy1.getAge()).isEqualTo(0); + assertThat(proxy1.getAge()).isEqualTo(1); + assertThat(proxy2.getAge()).isEqualTo(0); + assertThat(proxy1.getAge()).isEqualTo(2); + } + + @Test + public void testWithInstanceWithNonAspect() throws Exception { + AspectJProxyFactory pf = new AspectJProxyFactory(); + assertThatIllegalArgumentException().isThrownBy(() -> + pf.addAspect(new TestBean())); + } + + @Test + @SuppressWarnings("unchecked") + public void testSerializable() throws Exception { + AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TestBean()); + proxyFactory.addAspect(LoggingAspectOnVarargs.class); + ITestBean proxy = proxyFactory.getProxy(); + assertThat(proxy.doWithVarargs(MyEnum.A, MyOtherEnum.C)).isTrue(); + ITestBean tb = SerializationTestUtils.serializeAndDeserialize(proxy); + assertThat(tb.doWithVarargs(MyEnum.A, MyOtherEnum.C)).isTrue(); + } + + @Test + public void testWithInstance() throws Exception { + MultiplyReturnValue aspect = new MultiplyReturnValue(); + int multiple = 3; + aspect.setMultiple(multiple); + + TestBean target = new TestBean(); + target.setAge(24); + + AspectJProxyFactory proxyFactory = new AspectJProxyFactory(target); + proxyFactory.addAspect(aspect); + + ITestBean proxy = proxyFactory.getProxy(); + assertThat(proxy.getAge()).isEqualTo((target.getAge() * multiple)); + + ITestBean serializedProxy = SerializationTestUtils.serializeAndDeserialize(proxy); + assertThat(serializedProxy.getAge()).isEqualTo((target.getAge() * multiple)); + } + + @Test + public void testWithNonSingletonAspectInstance() throws Exception { + AspectJProxyFactory pf = new AspectJProxyFactory(); + assertThatIllegalArgumentException().isThrownBy(() -> pf.addAspect(new PerThisAspect())); + } + + @Test // SPR-13328 + @SuppressWarnings("unchecked") + public void testProxiedVarargsWithEnumArray() throws Exception { + AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TestBean()); + proxyFactory.addAspect(LoggingAspectOnVarargs.class); + ITestBean proxy = proxyFactory.getProxy(); + assertThat(proxy.doWithVarargs(MyEnum.A, MyOtherEnum.C)).isTrue(); + } + + @Test // SPR-13328 + @SuppressWarnings("unchecked") + public void testUnproxiedVarargsWithEnumArray() throws Exception { + AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TestBean()); + proxyFactory.addAspect(LoggingAspectOnSetter.class); + ITestBean proxy = proxyFactory.getProxy(); + assertThat(proxy.doWithVarargs(MyEnum.A, MyOtherEnum.C)).isTrue(); + } + + + public interface ITestBean { + + int getAge(); + + @SuppressWarnings("unchecked") + boolean doWithVarargs(V... args); + } + + + @SuppressWarnings("serial") + public static class TestBean implements ITestBean, Serializable { + + private int age; + + @Override + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + @SuppressWarnings("unchecked") + @Override + public boolean doWithVarargs(V... args) { + return true; + } + } + + + public interface MyInterface { + } + + + public enum MyEnum implements MyInterface { + + A, B; + } + + + public enum MyOtherEnum implements MyInterface { + + C, D; + } + + + @Aspect + @SuppressWarnings("serial") + public static class LoggingAspectOnVarargs implements Serializable { + + @Around("execution(* doWithVarargs(*))") + public Object doLog(ProceedingJoinPoint pjp) throws Throwable { + LogFactory.getLog(LoggingAspectOnVarargs.class).debug(Arrays.asList(pjp.getArgs())); + return pjp.proceed(); + } + } + + + @Aspect + public static class LoggingAspectOnSetter { + + @Around("execution(* setAge(*))") + public Object doLog(ProceedingJoinPoint pjp) throws Throwable { + LogFactory.getLog(LoggingAspectOnSetter.class).debug(Arrays.asList(pjp.getArgs())); + return pjp.proceed(); + } + } +} + + +@Aspect +@SuppressWarnings("serial") +class MultiplyReturnValue implements Serializable { + + private int multiple = 2; + + public int invocations; + + public void setMultiple(int multiple) { + this.multiple = multiple; + } + + public int getMultiple() { + return this.multiple; + } + + @Around("execution(int *.getAge())") + public Object doubleReturnValue(ProceedingJoinPoint pjp) throws Throwable { + ++this.invocations; + int result = (Integer) pjp.proceed(); + return result * this.multiple; + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactoryTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactoryTests.java new file mode 100644 index 0000000..d649ea0 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactoryTests.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.annotation; + +/** + * Tests for {@link ReflectiveAspectJAdvisorFactory}. + * + *

Tests are inherited: we only set the test fixture here. + * + * @author Rod Johnson + * @since 2.0 + */ +class ReflectiveAspectJAdvisorFactoryTests extends AbstractAspectJAdvisorFactoryTests { + + @Override + protected AspectJAdvisorFactory getFixture() { + return new ReflectiveAspectJAdvisorFactory(); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJNamespaceHandlerTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJNamespaceHandlerTests.java new file mode 100644 index 0000000..a6ecf37 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJNamespaceHandlerTests.java @@ -0,0 +1,103 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.autoproxy; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.config.AopConfigUtils; +import org.springframework.aop.config.AopNamespaceUtils; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.parsing.PassThroughSourceExtractor; +import org.springframework.beans.factory.parsing.SourceExtractor; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.beans.factory.xml.XmlReaderContext; +import org.springframework.beans.testfixture.beans.CollectingReaderEventListener; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + * @author Chris Beams + */ +public class AspectJNamespaceHandlerTests { + + private ParserContext parserContext; + + private CollectingReaderEventListener readerEventListener = new CollectingReaderEventListener(); + + private BeanDefinitionRegistry registry = new DefaultListableBeanFactory(); + + + @BeforeEach + public void setUp() throws Exception { + SourceExtractor sourceExtractor = new PassThroughSourceExtractor(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this.registry); + XmlReaderContext readerContext = + new XmlReaderContext(null, null, this.readerEventListener, sourceExtractor, reader, null); + this.parserContext = new ParserContext(readerContext, null); + } + + @Test + public void testRegisterAutoProxyCreator() throws Exception { + AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(this.parserContext, null); + assertThat(registry.getBeanDefinitionCount()).as("Incorrect number of definitions registered").isEqualTo(1); + + AopNamespaceUtils.registerAspectJAutoProxyCreatorIfNecessary(this.parserContext, null); + assertThat(registry.getBeanDefinitionCount()).as("Incorrect number of definitions registered").isEqualTo(1); + } + + @Test + public void testRegisterAspectJAutoProxyCreator() throws Exception { + AopNamespaceUtils.registerAspectJAutoProxyCreatorIfNecessary(this.parserContext, null); + assertThat(registry.getBeanDefinitionCount()).as("Incorrect number of definitions registered").isEqualTo(1); + + AopNamespaceUtils.registerAspectJAutoProxyCreatorIfNecessary(this.parserContext, null); + assertThat(registry.getBeanDefinitionCount()).as("Incorrect number of definitions registered").isEqualTo(1); + + BeanDefinition definition = registry.getBeanDefinition(AopConfigUtils.AUTO_PROXY_CREATOR_BEAN_NAME); + assertThat(definition.getBeanClassName()).as("Incorrect APC class").isEqualTo(AspectJAwareAdvisorAutoProxyCreator.class.getName()); + } + + @Test + public void testRegisterAspectJAutoProxyCreatorWithExistingAutoProxyCreator() throws Exception { + AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(this.parserContext, null); + assertThat(registry.getBeanDefinitionCount()).isEqualTo(1); + + AopNamespaceUtils.registerAspectJAutoProxyCreatorIfNecessary(this.parserContext, null); + assertThat(registry.getBeanDefinitionCount()).as("Incorrect definition count").isEqualTo(1); + + BeanDefinition definition = registry.getBeanDefinition(AopConfigUtils.AUTO_PROXY_CREATOR_BEAN_NAME); + assertThat(definition.getBeanClassName()).as("APC class not switched").isEqualTo(AspectJAwareAdvisorAutoProxyCreator.class.getName()); + } + + @Test + public void testRegisterAutoProxyCreatorWhenAspectJAutoProxyCreatorAlreadyExists() throws Exception { + AopNamespaceUtils.registerAspectJAutoProxyCreatorIfNecessary(this.parserContext, null); + assertThat(registry.getBeanDefinitionCount()).isEqualTo(1); + + AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(this.parserContext, null); + assertThat(registry.getBeanDefinitionCount()).as("Incorrect definition count").isEqualTo(1); + + BeanDefinition definition = registry.getBeanDefinition(AopConfigUtils.AUTO_PROXY_CREATOR_BEAN_NAME); + assertThat(definition.getBeanClassName()).as("Incorrect APC class").isEqualTo(AspectJAwareAdvisorAutoProxyCreator.class.getName()); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJPrecedenceComparatorTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJPrecedenceComparatorTests.java new file mode 100644 index 0000000..d391d45 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJPrecedenceComparatorTests.java @@ -0,0 +1,212 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.autoproxy; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.Advisor; +import org.springframework.aop.AfterReturningAdvice; +import org.springframework.aop.BeforeAdvice; +import org.springframework.aop.aspectj.AbstractAspectJAdvice; +import org.springframework.aop.aspectj.AspectJAfterAdvice; +import org.springframework.aop.aspectj.AspectJAfterReturningAdvice; +import org.springframework.aop.aspectj.AspectJAfterThrowingAdvice; +import org.springframework.aop.aspectj.AspectJAroundAdvice; +import org.springframework.aop.aspectj.AspectJExpressionPointcut; +import org.springframework.aop.aspectj.AspectJMethodBeforeAdvice; +import org.springframework.aop.aspectj.AspectJPointcutAdvisor; +import org.springframework.aop.support.DefaultPointcutAdvisor; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Adrian Colyer + * @author Chris Beams + */ +public class AspectJPrecedenceComparatorTests { + + private static final int HIGH_PRECEDENCE_ADVISOR_ORDER = 100; + private static final int LOW_PRECEDENCE_ADVISOR_ORDER = 200; + private static final int EARLY_ADVICE_DECLARATION_ORDER = 5; + private static final int LATE_ADVICE_DECLARATION_ORDER = 10; + + + private AspectJPrecedenceComparator comparator; + + private Method anyOldMethod; + + private AspectJExpressionPointcut anyOldPointcut; + + + @BeforeEach + public void setUp() throws Exception { + this.comparator = new AspectJPrecedenceComparator(); + this.anyOldMethod = getClass().getMethods()[0]; + this.anyOldPointcut = new AspectJExpressionPointcut(); + this.anyOldPointcut.setExpression("execution(* *(..))"); + } + + + @Test + public void testSameAspectNoAfterAdvice() { + Advisor advisor1 = createAspectJBeforeAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someAspect"); + Advisor advisor2 = createAspectJBeforeAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, LATE_ADVICE_DECLARATION_ORDER, "someAspect"); + assertThat(this.comparator.compare(advisor1, advisor2)).as("advisor1 sorted before advisor2").isEqualTo(-1); + + advisor1 = createAspectJBeforeAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, LATE_ADVICE_DECLARATION_ORDER, "someAspect"); + advisor2 = createAspectJAroundAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someAspect"); + assertThat(this.comparator.compare(advisor1, advisor2)).as("advisor2 sorted before advisor1").isEqualTo(1); + } + + @Test + public void testSameAspectAfterAdvice() { + Advisor advisor1 = createAspectJAfterAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someAspect"); + Advisor advisor2 = createAspectJAroundAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, LATE_ADVICE_DECLARATION_ORDER, "someAspect"); + assertThat(this.comparator.compare(advisor1, advisor2)).as("advisor2 sorted before advisor1").isEqualTo(1); + + advisor1 = createAspectJAfterReturningAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, LATE_ADVICE_DECLARATION_ORDER, "someAspect"); + advisor2 = createAspectJAfterThrowingAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someAspect"); + assertThat(this.comparator.compare(advisor1, advisor2)).as("advisor1 sorted before advisor2").isEqualTo(-1); + } + + @Test + public void testSameAspectOneOfEach() { + Advisor advisor1 = createAspectJAfterAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someAspect"); + Advisor advisor2 = createAspectJBeforeAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, LATE_ADVICE_DECLARATION_ORDER, "someAspect"); + assertThat(this.comparator.compare(advisor1, advisor2)).as("advisor1 and advisor2 not comparable").isEqualTo(1); + } + + @Test + public void testSameAdvisorPrecedenceDifferentAspectNoAfterAdvice() { + Advisor advisor1 = createAspectJBeforeAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someAspect"); + Advisor advisor2 = createAspectJBeforeAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, LATE_ADVICE_DECLARATION_ORDER, "someOtherAspect"); + assertThat(this.comparator.compare(advisor1, advisor2)).as("nothing to say about order here").isEqualTo(0); + + advisor1 = createAspectJBeforeAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, LATE_ADVICE_DECLARATION_ORDER, "someAspect"); + advisor2 = createAspectJAroundAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someOtherAspect"); + assertThat(this.comparator.compare(advisor1, advisor2)).as("nothing to say about order here").isEqualTo(0); + } + + @Test + public void testSameAdvisorPrecedenceDifferentAspectAfterAdvice() { + Advisor advisor1 = createAspectJAfterAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someAspect"); + Advisor advisor2 = createAspectJAroundAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, LATE_ADVICE_DECLARATION_ORDER, "someOtherAspect"); + assertThat(this.comparator.compare(advisor1, advisor2)).as("nothing to say about order here").isEqualTo(0); + + advisor1 = createAspectJAfterReturningAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, LATE_ADVICE_DECLARATION_ORDER, "someAspect"); + advisor2 = createAspectJAfterThrowingAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someOtherAspect"); + assertThat(this.comparator.compare(advisor1, advisor2)).as("nothing to say about order here").isEqualTo(0); + } + + @Test + public void testHigherAdvisorPrecedenceNoAfterAdvice() { + Advisor advisor1 = createSpringAOPBeforeAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER); + Advisor advisor2 = createAspectJBeforeAdvice(LOW_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someOtherAspect"); + assertThat(this.comparator.compare(advisor1, advisor2)).as("advisor1 sorted before advisor2").isEqualTo(-1); + + advisor1 = createAspectJBeforeAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, LATE_ADVICE_DECLARATION_ORDER, "someAspect"); + advisor2 = createAspectJAroundAdvice(LOW_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someOtherAspect"); + assertThat(this.comparator.compare(advisor1, advisor2)).as("advisor1 sorted before advisor2").isEqualTo(-1); + } + + @Test + public void testHigherAdvisorPrecedenceAfterAdvice() { + Advisor advisor1 = createAspectJAfterAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someAspect"); + Advisor advisor2 = createAspectJAroundAdvice(LOW_PRECEDENCE_ADVISOR_ORDER, LATE_ADVICE_DECLARATION_ORDER, "someOtherAspect"); + assertThat(this.comparator.compare(advisor1, advisor2)).as("advisor1 sorted before advisor2").isEqualTo(-1); + + advisor1 = createAspectJAfterReturningAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, LATE_ADVICE_DECLARATION_ORDER, "someAspect"); + advisor2 = createAspectJAfterThrowingAdvice(LOW_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someOtherAspect"); + assertThat(this.comparator.compare(advisor1, advisor2)).as("advisor2 sorted after advisor1").isEqualTo(-1); + } + + @Test + public void testLowerAdvisorPrecedenceNoAfterAdvice() { + Advisor advisor1 = createAspectJBeforeAdvice(LOW_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someAspect"); + Advisor advisor2 = createAspectJBeforeAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someOtherAspect"); + assertThat(this.comparator.compare(advisor1, advisor2)).as("advisor1 sorted after advisor2").isEqualTo(1); + + advisor1 = createAspectJBeforeAdvice(LOW_PRECEDENCE_ADVISOR_ORDER, LATE_ADVICE_DECLARATION_ORDER, "someAspect"); + advisor2 = createAspectJAroundAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someOtherAspect"); + assertThat(this.comparator.compare(advisor1, advisor2)).as("advisor1 sorted after advisor2").isEqualTo(1); + } + + @Test + public void testLowerAdvisorPrecedenceAfterAdvice() { + Advisor advisor1 = createAspectJAfterAdvice(LOW_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someAspect"); + Advisor advisor2 = createAspectJAroundAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, LATE_ADVICE_DECLARATION_ORDER, "someOtherAspect"); + assertThat(this.comparator.compare(advisor1, advisor2)).as("advisor1 sorted after advisor2").isEqualTo(1); + + advisor1 = createSpringAOPAfterAdvice(LOW_PRECEDENCE_ADVISOR_ORDER); + advisor2 = createAspectJAfterThrowingAdvice(HIGH_PRECEDENCE_ADVISOR_ORDER, EARLY_ADVICE_DECLARATION_ORDER, "someOtherAspect"); + assertThat(this.comparator.compare(advisor1, advisor2)).as("advisor1 sorted after advisor2").isEqualTo(1); + } + + + private Advisor createAspectJBeforeAdvice(int advisorOrder, int adviceDeclarationOrder, String aspectName) { + AspectJMethodBeforeAdvice advice = new AspectJMethodBeforeAdvice(this.anyOldMethod, this.anyOldPointcut, null); + return createAspectJAdvice(advisorOrder, adviceDeclarationOrder, aspectName, advice); + } + + private Advisor createAspectJAroundAdvice(int advisorOrder, int adviceDeclarationOrder, String aspectName) { + AspectJAroundAdvice advice = new AspectJAroundAdvice(this.anyOldMethod, this.anyOldPointcut, null); + return createAspectJAdvice(advisorOrder, adviceDeclarationOrder, aspectName, advice); + } + + private Advisor createAspectJAfterAdvice(int advisorOrder, int adviceDeclarationOrder, String aspectName) { + AspectJAfterAdvice advice = new AspectJAfterAdvice(this.anyOldMethod, this.anyOldPointcut, null); + return createAspectJAdvice(advisorOrder, adviceDeclarationOrder, aspectName, advice); + } + + private Advisor createAspectJAfterReturningAdvice(int advisorOrder, int adviceDeclarationOrder, String aspectName) { + AspectJAfterReturningAdvice advice = new AspectJAfterReturningAdvice(this.anyOldMethod, this.anyOldPointcut, null); + return createAspectJAdvice(advisorOrder, adviceDeclarationOrder, aspectName, advice); + } + + private Advisor createAspectJAfterThrowingAdvice(int advisorOrder, int adviceDeclarationOrder, String aspectName) { + AspectJAfterThrowingAdvice advice = new AspectJAfterThrowingAdvice(this.anyOldMethod, this.anyOldPointcut, null); + return createAspectJAdvice(advisorOrder, adviceDeclarationOrder, aspectName, advice); + } + + private Advisor createAspectJAdvice(int advisorOrder, int adviceDeclarationOrder, String aspectName, AbstractAspectJAdvice advice) { + advice.setDeclarationOrder(adviceDeclarationOrder); + advice.setAspectName(aspectName); + AspectJPointcutAdvisor advisor = new AspectJPointcutAdvisor(advice); + advisor.setOrder(advisorOrder); + return advisor; + } + + private Advisor createSpringAOPAfterAdvice(int order) { + AfterReturningAdvice advice = (returnValue, method, args, target) -> { + }; + DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(this.anyOldPointcut, advice); + advisor.setOrder(order); + return advisor; + } + + private Advisor createSpringAOPBeforeAdvice(int order) { + BeforeAdvice advice = new BeforeAdvice() { + }; + DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(this.anyOldPointcut, advice); + advisor.setOrder(order); + return advisor; + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/config/AopNamespaceHandlerEventTests.java b/spring-aop/src/test/java/org/springframework/aop/config/AopNamespaceHandlerEventTests.java new file mode 100644 index 0000000..d4c7744 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/config/AopNamespaceHandlerEventTests.java @@ -0,0 +1,200 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.config; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanReference; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.parsing.ComponentDefinition; +import org.springframework.beans.factory.parsing.CompositeComponentDefinition; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.beans.testfixture.beans.CollectingReaderEventListener; +import org.springframework.core.io.Resource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.core.testfixture.io.ResourceTestUtils.qualifiedResource; + +/** + * @author Rob Harrop + * @author Juergen Hoeller + * @author Chris Beams + */ +public class AopNamespaceHandlerEventTests { + + private static final Class CLASS = AopNamespaceHandlerEventTests.class; + + private static final Resource CONTEXT = qualifiedResource(CLASS, "context.xml"); + private static final Resource POINTCUT_EVENTS_CONTEXT = qualifiedResource(CLASS, "pointcutEvents.xml"); + private static final Resource POINTCUT_REF_CONTEXT = qualifiedResource(CLASS, "pointcutRefEvents.xml"); + private static final Resource DIRECT_POINTCUT_EVENTS_CONTEXT = qualifiedResource(CLASS, "directPointcutEvents.xml"); + + private CollectingReaderEventListener eventListener = new CollectingReaderEventListener(); + + private DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + private XmlBeanDefinitionReader reader; + + + @BeforeEach + public void setup() { + this.reader = new XmlBeanDefinitionReader(this.beanFactory); + this.reader.setEventListener(this.eventListener); + } + + + @Test + public void testPointcutEvents() { + this.reader.loadBeanDefinitions(POINTCUT_EVENTS_CONTEXT); + ComponentDefinition[] componentDefinitions = this.eventListener.getComponentDefinitions(); + assertThat(componentDefinitions.length).as("Incorrect number of events fired").isEqualTo(1); + boolean condition = componentDefinitions[0] instanceof CompositeComponentDefinition; + assertThat(condition).as("No holder with nested components").isTrue(); + + CompositeComponentDefinition compositeDef = (CompositeComponentDefinition) componentDefinitions[0]; + assertThat(compositeDef.getName()).isEqualTo("aop:config"); + + ComponentDefinition[] nestedComponentDefs = compositeDef.getNestedComponents(); + assertThat(nestedComponentDefs.length).as("Incorrect number of inner components").isEqualTo(2); + PointcutComponentDefinition pcd = null; + for (ComponentDefinition componentDefinition : nestedComponentDefs) { + if (componentDefinition instanceof PointcutComponentDefinition) { + pcd = (PointcutComponentDefinition) componentDefinition; + break; + } + } + assertThat(pcd).as("PointcutComponentDefinition not found").isNotNull(); + assertThat(pcd.getBeanDefinitions().length).as("Incorrect number of BeanDefinitions").isEqualTo(1); + } + + @Test + public void testAdvisorEventsWithPointcutRef() { + this.reader.loadBeanDefinitions(POINTCUT_REF_CONTEXT); + ComponentDefinition[] componentDefinitions = this.eventListener.getComponentDefinitions(); + assertThat(componentDefinitions.length).as("Incorrect number of events fired").isEqualTo(2); + + boolean condition1 = componentDefinitions[0] instanceof CompositeComponentDefinition; + assertThat(condition1).as("No holder with nested components").isTrue(); + CompositeComponentDefinition compositeDef = (CompositeComponentDefinition) componentDefinitions[0]; + assertThat(compositeDef.getName()).isEqualTo("aop:config"); + + ComponentDefinition[] nestedComponentDefs = compositeDef.getNestedComponents(); + assertThat(nestedComponentDefs.length).as("Incorrect number of inner components").isEqualTo(3); + AdvisorComponentDefinition acd = null; + for (int i = 0; i < nestedComponentDefs.length; i++) { + ComponentDefinition componentDefinition = nestedComponentDefs[i]; + if (componentDefinition instanceof AdvisorComponentDefinition) { + acd = (AdvisorComponentDefinition) componentDefinition; + break; + } + } + assertThat(acd).as("AdvisorComponentDefinition not found").isNotNull(); + assertThat(acd.getBeanDefinitions().length).isEqualTo(1); + assertThat(acd.getBeanReferences().length).isEqualTo(2); + + boolean condition = componentDefinitions[1] instanceof BeanComponentDefinition; + assertThat(condition).as("No advice bean found").isTrue(); + BeanComponentDefinition adviceDef = (BeanComponentDefinition) componentDefinitions[1]; + assertThat(adviceDef.getBeanName()).isEqualTo("countingAdvice"); + } + + @Test + public void testAdvisorEventsWithDirectPointcut() { + this.reader.loadBeanDefinitions(DIRECT_POINTCUT_EVENTS_CONTEXT); + ComponentDefinition[] componentDefinitions = this.eventListener.getComponentDefinitions(); + assertThat(componentDefinitions.length).as("Incorrect number of events fired").isEqualTo(2); + + boolean condition1 = componentDefinitions[0] instanceof CompositeComponentDefinition; + assertThat(condition1).as("No holder with nested components").isTrue(); + CompositeComponentDefinition compositeDef = (CompositeComponentDefinition) componentDefinitions[0]; + assertThat(compositeDef.getName()).isEqualTo("aop:config"); + + ComponentDefinition[] nestedComponentDefs = compositeDef.getNestedComponents(); + assertThat(nestedComponentDefs.length).as("Incorrect number of inner components").isEqualTo(2); + AdvisorComponentDefinition acd = null; + for (int i = 0; i < nestedComponentDefs.length; i++) { + ComponentDefinition componentDefinition = nestedComponentDefs[i]; + if (componentDefinition instanceof AdvisorComponentDefinition) { + acd = (AdvisorComponentDefinition) componentDefinition; + break; + } + } + assertThat(acd).as("AdvisorComponentDefinition not found").isNotNull(); + assertThat(acd.getBeanDefinitions().length).isEqualTo(2); + assertThat(acd.getBeanReferences().length).isEqualTo(1); + + boolean condition = componentDefinitions[1] instanceof BeanComponentDefinition; + assertThat(condition).as("No advice bean found").isTrue(); + BeanComponentDefinition adviceDef = (BeanComponentDefinition) componentDefinitions[1]; + assertThat(adviceDef.getBeanName()).isEqualTo("countingAdvice"); + } + + @Test + public void testAspectEvent() { + this.reader.loadBeanDefinitions(CONTEXT); + ComponentDefinition[] componentDefinitions = this.eventListener.getComponentDefinitions(); + assertThat(componentDefinitions.length).as("Incorrect number of events fired").isEqualTo(5); + + boolean condition = componentDefinitions[0] instanceof CompositeComponentDefinition; + assertThat(condition).as("No holder with nested components").isTrue(); + CompositeComponentDefinition compositeDef = (CompositeComponentDefinition) componentDefinitions[0]; + assertThat(compositeDef.getName()).isEqualTo("aop:config"); + + ComponentDefinition[] nestedComponentDefs = compositeDef.getNestedComponents(); + assertThat(nestedComponentDefs.length).as("Incorrect number of inner components").isEqualTo(2); + AspectComponentDefinition acd = null; + for (ComponentDefinition componentDefinition : nestedComponentDefs) { + if (componentDefinition instanceof AspectComponentDefinition) { + acd = (AspectComponentDefinition) componentDefinition; + break; + } + } + + assertThat(acd).as("AspectComponentDefinition not found").isNotNull(); + BeanDefinition[] beanDefinitions = acd.getBeanDefinitions(); + assertThat(beanDefinitions.length).isEqualTo(5); + BeanReference[] beanReferences = acd.getBeanReferences(); + assertThat(beanReferences.length).isEqualTo(6); + + Set expectedReferences = new HashSet<>(); + expectedReferences.add("pc"); + expectedReferences.add("countingAdvice"); + for (BeanReference beanReference : beanReferences) { + expectedReferences.remove(beanReference.getBeanName()); + } + assertThat(expectedReferences.size()).as("Incorrect references found").isEqualTo(0); + + for (int i = 1; i < componentDefinitions.length; i++) { + boolean condition1 = componentDefinitions[i] instanceof BeanComponentDefinition; + assertThat(condition1).isTrue(); + } + + ComponentDefinition[] nestedComponentDefs2 = acd.getNestedComponents(); + assertThat(nestedComponentDefs2.length).as("Inner PointcutComponentDefinition not found").isEqualTo(1); + boolean condition1 = nestedComponentDefs2[0] instanceof PointcutComponentDefinition; + assertThat(condition1).isTrue(); + PointcutComponentDefinition pcd = (PointcutComponentDefinition) nestedComponentDefs2[0]; + assertThat(pcd.getBeanDefinitions().length).as("Incorrect number of BeanDefinitions").isEqualTo(1); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/config/AopNamespaceHandlerPointcutErrorTests.java b/spring-aop/src/test/java/org/springframework/aop/config/AopNamespaceHandlerPointcutErrorTests.java new file mode 100644 index 0000000..6a01b03 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/config/AopNamespaceHandlerPointcutErrorTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.config; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.core.testfixture.io.ResourceTestUtils.qualifiedResource; + +/** + * @author Mark Fisher + * @author Chris Beams + */ +public class AopNamespaceHandlerPointcutErrorTests { + + @Test + public void testDuplicatePointcutConfig() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> + new XmlBeanDefinitionReader(bf).loadBeanDefinitions( + qualifiedResource(getClass(), "pointcutDuplication.xml"))) + .satisfies(ex -> ex.contains(BeanDefinitionParsingException.class)); + } + + @Test + public void testMissingPointcutConfig() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> + new XmlBeanDefinitionReader(bf).loadBeanDefinitions( + qualifiedResource(getClass(), "pointcutMissing.xml"))) + .satisfies(ex -> ex.contains(BeanDefinitionParsingException.class)); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/config/TopLevelAopTagTests.java b/spring-aop/src/test/java/org/springframework/aop/config/TopLevelAopTagTests.java new file mode 100644 index 0000000..f1ab7f2 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/config/TopLevelAopTagTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.config; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.core.testfixture.io.ResourceTestUtils.qualifiedResource; + +/** + * Tests that the <aop:config/> element can be used as a top level element. + * + * @author Rob Harrop + * @author Chris Beams + */ +public class TopLevelAopTagTests { + + @Test + public void testParse() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(beanFactory).loadBeanDefinitions( + qualifiedResource(TopLevelAopTagTests.class, "context.xml")); + + assertThat(beanFactory.containsBeanDefinition("testPointcut")).isTrue(); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/AopProxyUtilsTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/AopProxyUtilsTests.java new file mode 100644 index 0000000..c847579 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/framework/AopProxyUtilsTests.java @@ -0,0 +1,135 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.SpringProxy; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Rod Johnson + * @author Chris Beams + */ +public class AopProxyUtilsTests { + + @Test + public void testCompleteProxiedInterfacesWorksWithNull() { + AdvisedSupport as = new AdvisedSupport(); + Class[] completedInterfaces = AopProxyUtils.completeProxiedInterfaces(as); + assertThat(completedInterfaces.length).isEqualTo(2); + List ifaces = Arrays.asList(completedInterfaces); + assertThat(ifaces.contains(Advised.class)).isTrue(); + assertThat(ifaces.contains(SpringProxy.class)).isTrue(); + } + + @Test + public void testCompleteProxiedInterfacesWorksWithNullOpaque() { + AdvisedSupport as = new AdvisedSupport(); + as.setOpaque(true); + Class[] completedInterfaces = AopProxyUtils.completeProxiedInterfaces(as); + assertThat(completedInterfaces.length).isEqualTo(1); + } + + @Test + public void testCompleteProxiedInterfacesAdvisedNotIncluded() { + AdvisedSupport as = new AdvisedSupport(); + as.addInterface(ITestBean.class); + as.addInterface(Comparable.class); + Class[] completedInterfaces = AopProxyUtils.completeProxiedInterfaces(as); + assertThat(completedInterfaces.length).isEqualTo(4); + + // Can't assume ordering for others, so use a list + List l = Arrays.asList(completedInterfaces); + assertThat(l.contains(Advised.class)).isTrue(); + assertThat(l.contains(ITestBean.class)).isTrue(); + assertThat(l.contains(Comparable.class)).isTrue(); + } + + @Test + public void testCompleteProxiedInterfacesAdvisedIncluded() { + AdvisedSupport as = new AdvisedSupport(); + as.addInterface(ITestBean.class); + as.addInterface(Comparable.class); + as.addInterface(Advised.class); + Class[] completedInterfaces = AopProxyUtils.completeProxiedInterfaces(as); + assertThat(completedInterfaces.length).isEqualTo(4); + + // Can't assume ordering for others, so use a list + List l = Arrays.asList(completedInterfaces); + assertThat(l.contains(Advised.class)).isTrue(); + assertThat(l.contains(ITestBean.class)).isTrue(); + assertThat(l.contains(Comparable.class)).isTrue(); + } + + @Test + public void testCompleteProxiedInterfacesAdvisedNotIncludedOpaque() { + AdvisedSupport as = new AdvisedSupport(); + as.setOpaque(true); + as.addInterface(ITestBean.class); + as.addInterface(Comparable.class); + Class[] completedInterfaces = AopProxyUtils.completeProxiedInterfaces(as); + assertThat(completedInterfaces.length).isEqualTo(3); + + // Can't assume ordering for others, so use a list + List l = Arrays.asList(completedInterfaces); + assertThat(l.contains(Advised.class)).isFalse(); + assertThat(l.contains(ITestBean.class)).isTrue(); + assertThat(l.contains(Comparable.class)).isTrue(); + } + + @Test + public void testProxiedUserInterfacesWithSingleInterface() { + ProxyFactory pf = new ProxyFactory(); + pf.setTarget(new TestBean()); + pf.addInterface(ITestBean.class); + Object proxy = pf.getProxy(); + Class[] userInterfaces = AopProxyUtils.proxiedUserInterfaces(proxy); + assertThat(userInterfaces.length).isEqualTo(1); + assertThat(userInterfaces[0]).isEqualTo(ITestBean.class); + } + + @Test + public void testProxiedUserInterfacesWithMultipleInterfaces() { + ProxyFactory pf = new ProxyFactory(); + pf.setTarget(new TestBean()); + pf.addInterface(ITestBean.class); + pf.addInterface(Comparable.class); + Object proxy = pf.getProxy(); + Class[] userInterfaces = AopProxyUtils.proxiedUserInterfaces(proxy); + assertThat(userInterfaces.length).isEqualTo(2); + assertThat(userInterfaces[0]).isEqualTo(ITestBean.class); + assertThat(userInterfaces[1]).isEqualTo(Comparable.class); + } + + @Test + public void testProxiedUserInterfacesWithNoInterface() { + Object proxy = Proxy.newProxyInstance(getClass().getClassLoader(), new Class[0], + (proxy1, method, args) -> null); + assertThatIllegalArgumentException().isThrownBy(() -> + AopProxyUtils.proxiedUserInterfaces(proxy)); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/ClassWithConstructor.java b/spring-aop/src/test/java/org/springframework/aop/framework/ClassWithConstructor.java new file mode 100644 index 0000000..dfbd090 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/framework/ClassWithConstructor.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +public class ClassWithConstructor { + + public ClassWithConstructor(Object object) { + + } + + public void method() { + + } +} diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/IntroductionBenchmarkTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/IntroductionBenchmarkTests.java new file mode 100644 index 0000000..d3defcc --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/framework/IntroductionBenchmarkTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.support.DelegatingIntroductionInterceptor; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.util.StopWatch; + +/** + * Benchmarks for introductions. + * + * NOTE: No assertions! + * + * @author Rod Johnson + * @author Chris Beams + * @since 2.0 + */ +public class IntroductionBenchmarkTests { + + private static final int EXPECTED_COMPARE = 13; + + /** Increase this if you want meaningful results! */ + private static final int INVOCATIONS = 100000; + + + @SuppressWarnings("serial") + public static class SimpleCounterIntroduction extends DelegatingIntroductionInterceptor implements Counter { + @Override + public int getCount() { + return EXPECTED_COMPARE; + } + } + + public static interface Counter { + int getCount(); + } + + @Test + public void timeManyInvocations() { + StopWatch sw = new StopWatch(); + + TestBean target = new TestBean(); + ProxyFactory pf = new ProxyFactory(target); + pf.setProxyTargetClass(false); + pf.addAdvice(new SimpleCounterIntroduction()); + ITestBean proxy = (ITestBean) pf.getProxy(); + + Counter counter = (Counter) proxy; + + sw.start(INVOCATIONS + " invocations on proxy, not hitting introduction"); + for (int i = 0; i < INVOCATIONS; i++) { + proxy.getAge(); + } + sw.stop(); + + sw.start(INVOCATIONS + " invocations on proxy, hitting introduction"); + for (int i = 0; i < INVOCATIONS; i++) { + counter.getCount(); + } + sw.stop(); + + sw.start(INVOCATIONS + " invocations on target"); + for (int i = 0; i < INVOCATIONS; i++) { + target.getAge(); + } + sw.stop(); + + System.out.println(sw.prettyPrint()); + } +} diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/MethodInvocationTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/MethodInvocationTests.java new file mode 100644 index 0000000..9109103 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/framework/MethodInvocationTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; + +import org.aopalliance.intercept.MethodInterceptor; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rod Johnson + * @author Chris Beams + * @since 14.03.2003 + */ +class MethodInvocationTests { + + @Test + void testValidInvocation() throws Throwable { + Method method = Object.class.getMethod("hashCode"); + Object proxy = new Object(); + Object returnValue = new Object(); + List interceptors = Collections.singletonList((MethodInterceptor) invocation -> returnValue); + ReflectiveMethodInvocation invocation = new ReflectiveMethodInvocation(proxy, null, method, null, null, interceptors); + Object rv = invocation.proceed(); + assertThat(rv).as("correct response").isSameAs(returnValue); + } + + /** + * toString on target can cause failure. + */ + @Test + void testToStringDoesntHitTarget() throws Throwable { + Object target = new TestBean() { + @Override + public String toString() { + throw new UnsupportedOperationException("toString"); + } + }; + List interceptors = Collections.emptyList(); + + Method m = Object.class.getMethod("hashCode"); + Object proxy = new Object(); + ReflectiveMethodInvocation invocation = new ReflectiveMethodInvocation(proxy, target, m, null, null, interceptors); + + // If it hits target, the test will fail with the UnsupportedOpException + // in the inner class above. + invocation.toString(); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/NullPrimitiveTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/NullPrimitiveTests.java new file mode 100644 index 0000000..047e84b --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/framework/NullPrimitiveTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import org.aopalliance.intercept.MethodInterceptor; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.AopInvocationException; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Test for SPR-4675. A null value returned from around advice is very hard to debug if + * the caller expects a primitive. + * + * @author Dave Syer + */ +public class NullPrimitiveTests { + + interface Foo { + int getValue(); + } + + @Test + public void testNullPrimitiveWithJdkProxy() { + + class SimpleFoo implements Foo { + @Override + public int getValue() { + return 100; + } + } + + SimpleFoo target = new SimpleFoo(); + ProxyFactory factory = new ProxyFactory(target); + factory.addAdvice((MethodInterceptor) invocation -> null); + + Foo foo = (Foo) factory.getProxy(); + + assertThatExceptionOfType(AopInvocationException.class).isThrownBy(() -> + foo.getValue()) + .withMessageContaining("Foo.getValue()"); + } + + public static class Bar { + public int getValue() { + return 100; + } + } + + @Test + public void testNullPrimitiveWithCglibProxy() { + + Bar target = new Bar(); + ProxyFactory factory = new ProxyFactory(target); + factory.addAdvice((MethodInterceptor) invocation -> null); + + Bar bar = (Bar) factory.getProxy(); + + assertThatExceptionOfType(AopInvocationException.class).isThrownBy(() -> + bar.getValue()) + .withMessageContaining("Bar.getValue()"); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/PrototypeTargetTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/PrototypeTargetTests.java new file mode 100644 index 0000000..d58a9b5 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/framework/PrototypeTargetTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.core.io.Resource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.core.testfixture.io.ResourceTestUtils.qualifiedResource; + +/** + * @author Juergen Hoeller + * @author Chris Beams + * @since 03.09.2004 + */ +public class PrototypeTargetTests { + + private static final Resource CONTEXT = qualifiedResource(PrototypeTargetTests.class, "context.xml"); + + + @Test + public void testPrototypeProxyWithPrototypeTarget() { + TestBeanImpl.constructionCount = 0; + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions(CONTEXT); + for (int i = 0; i < 10; i++) { + TestBean tb = (TestBean) bf.getBean("testBeanPrototype"); + tb.doSomething(); + } + TestInterceptor interceptor = (TestInterceptor) bf.getBean("testInterceptor"); + assertThat(TestBeanImpl.constructionCount).isEqualTo(10); + assertThat(interceptor.invocationCount).isEqualTo(10); + } + + @Test + public void testSingletonProxyWithPrototypeTarget() { + TestBeanImpl.constructionCount = 0; + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions(CONTEXT); + for (int i = 0; i < 10; i++) { + TestBean tb = (TestBean) bf.getBean("testBeanSingleton"); + tb.doSomething(); + } + TestInterceptor interceptor = (TestInterceptor) bf.getBean("testInterceptor"); + assertThat(TestBeanImpl.constructionCount).isEqualTo(1); + assertThat(interceptor.invocationCount).isEqualTo(10); + } + + + public interface TestBean { + + void doSomething(); + } + + + public static class TestBeanImpl implements TestBean { + + private static int constructionCount = 0; + + public TestBeanImpl() { + constructionCount++; + } + + @Override + public void doSomething() { + } + } + + + public static class TestInterceptor implements MethodInterceptor { + + private int invocationCount = 0; + + @Override + public Object invoke(MethodInvocation methodInvocation) throws Throwable { + invocationCount++; + return methodInvocation.proceed(); + } + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/ProxyFactoryTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/ProxyFactoryTests.java new file mode 100644 index 0000000..2ae1d63 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/framework/ProxyFactoryTests.java @@ -0,0 +1,391 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import java.util.ArrayList; +import java.util.List; + +import javax.accessibility.Accessible; +import javax.swing.JFrame; +import javax.swing.RootPaneContainer; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.Advisor; +import org.springframework.aop.interceptor.DebugInterceptor; +import org.springframework.aop.support.AopUtils; +import org.springframework.aop.support.DefaultIntroductionAdvisor; +import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.aop.testfixture.advice.CountingBeforeAdvice; +import org.springframework.aop.testfixture.interceptor.NopInterceptor; +import org.springframework.aop.testfixture.interceptor.TimestampIntroductionInterceptor; +import org.springframework.beans.testfixture.beans.IOther; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.annotation.Order; +import org.springframework.core.testfixture.TimeStamped; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Also tests AdvisedSupport and ProxyCreatorSupport superclasses. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Chris Beams + * @since 14.05.2003 + */ +public class ProxyFactoryTests { + + @Test + public void testIndexOfMethods() { + TestBean target = new TestBean(); + ProxyFactory pf = new ProxyFactory(target); + NopInterceptor nop = new NopInterceptor(); + Advisor advisor = new DefaultPointcutAdvisor(new CountingBeforeAdvice()); + Advised advised = (Advised) pf.getProxy(); + // Can use advised and ProxyFactory interchangeably + advised.addAdvice(nop); + pf.addAdvisor(advisor); + assertThat(pf.indexOf(new NopInterceptor())).isEqualTo(-1); + assertThat(pf.indexOf(nop)).isEqualTo(0); + assertThat(pf.indexOf(advisor)).isEqualTo(1); + assertThat(advised.indexOf(new DefaultPointcutAdvisor(null))).isEqualTo(-1); + } + + @Test + public void testRemoveAdvisorByReference() { + TestBean target = new TestBean(); + ProxyFactory pf = new ProxyFactory(target); + NopInterceptor nop = new NopInterceptor(); + CountingBeforeAdvice cba = new CountingBeforeAdvice(); + Advisor advisor = new DefaultPointcutAdvisor(cba); + pf.addAdvice(nop); + pf.addAdvisor(advisor); + ITestBean proxied = (ITestBean) pf.getProxy(); + proxied.setAge(5); + assertThat(cba.getCalls()).isEqualTo(1); + assertThat(nop.getCount()).isEqualTo(1); + assertThat(pf.removeAdvisor(advisor)).isTrue(); + assertThat(proxied.getAge()).isEqualTo(5); + assertThat(cba.getCalls()).isEqualTo(1); + assertThat(nop.getCount()).isEqualTo(2); + assertThat(pf.removeAdvisor(new DefaultPointcutAdvisor(null))).isFalse(); + } + + @Test + public void testRemoveAdvisorByIndex() { + TestBean target = new TestBean(); + ProxyFactory pf = new ProxyFactory(target); + NopInterceptor nop = new NopInterceptor(); + CountingBeforeAdvice cba = new CountingBeforeAdvice(); + Advisor advisor = new DefaultPointcutAdvisor(cba); + pf.addAdvice(nop); + pf.addAdvisor(advisor); + NopInterceptor nop2 = new NopInterceptor(); + pf.addAdvice(nop2); + ITestBean proxied = (ITestBean) pf.getProxy(); + proxied.setAge(5); + assertThat(cba.getCalls()).isEqualTo(1); + assertThat(nop.getCount()).isEqualTo(1); + assertThat(nop2.getCount()).isEqualTo(1); + // Removes counting before advisor + pf.removeAdvisor(1); + assertThat(proxied.getAge()).isEqualTo(5); + assertThat(cba.getCalls()).isEqualTo(1); + assertThat(nop.getCount()).isEqualTo(2); + assertThat(nop2.getCount()).isEqualTo(2); + // Removes Nop1 + pf.removeAdvisor(0); + assertThat(proxied.getAge()).isEqualTo(5); + assertThat(cba.getCalls()).isEqualTo(1); + assertThat(nop.getCount()).isEqualTo(2); + assertThat(nop2.getCount()).isEqualTo(3); + + // Check out of bounds + try { + pf.removeAdvisor(-1); + } + catch (AopConfigException ex) { + // Ok + } + + try { + pf.removeAdvisor(2); + } + catch (AopConfigException ex) { + // Ok + } + + assertThat(proxied.getAge()).isEqualTo(5); + assertThat(nop2.getCount()).isEqualTo(4); + } + + @Test + public void testReplaceAdvisor() { + TestBean target = new TestBean(); + ProxyFactory pf = new ProxyFactory(target); + NopInterceptor nop = new NopInterceptor(); + CountingBeforeAdvice cba1 = new CountingBeforeAdvice(); + CountingBeforeAdvice cba2 = new CountingBeforeAdvice(); + Advisor advisor1 = new DefaultPointcutAdvisor(cba1); + Advisor advisor2 = new DefaultPointcutAdvisor(cba2); + pf.addAdvisor(advisor1); + pf.addAdvice(nop); + ITestBean proxied = (ITestBean) pf.getProxy(); + // Use the type cast feature + // Replace etc methods on advised should be same as on ProxyFactory + Advised advised = (Advised) proxied; + proxied.setAge(5); + assertThat(cba1.getCalls()).isEqualTo(1); + assertThat(cba2.getCalls()).isEqualTo(0); + assertThat(nop.getCount()).isEqualTo(1); + assertThat(advised.replaceAdvisor(new DefaultPointcutAdvisor(new NopInterceptor()), advisor2)).isFalse(); + assertThat(advised.replaceAdvisor(advisor1, advisor2)).isTrue(); + assertThat(pf.getAdvisors()[0]).isEqualTo(advisor2); + assertThat(proxied.getAge()).isEqualTo(5); + assertThat(cba1.getCalls()).isEqualTo(1); + assertThat(nop.getCount()).isEqualTo(2); + assertThat(cba2.getCalls()).isEqualTo(1); + assertThat(pf.replaceAdvisor(new DefaultPointcutAdvisor(null), advisor1)).isFalse(); + } + + @Test + public void testAddRepeatedInterface() { + TimeStamped tst = () -> { + throw new UnsupportedOperationException("getTimeStamp"); + }; + ProxyFactory pf = new ProxyFactory(tst); + // We've already implicitly added this interface. + // This call should be ignored without error + pf.addInterface(TimeStamped.class); + // All cool + assertThat(pf.getProxy()).isInstanceOf(TimeStamped.class); + } + + @Test + public void testGetsAllInterfaces() throws Exception { + // Extend to get new interface + class TestBeanSubclass extends TestBean implements Comparable { + @Override + public int compareTo(Object arg0) { + throw new UnsupportedOperationException("compareTo"); + } + } + TestBeanSubclass raw = new TestBeanSubclass(); + ProxyFactory factory = new ProxyFactory(raw); + //System.out.println("Proxied interfaces are " + StringUtils.arrayToDelimitedString(factory.getProxiedInterfaces(), ",")); + assertThat(factory.getProxiedInterfaces().length).as("Found correct number of interfaces").isEqualTo(5); + ITestBean tb = (ITestBean) factory.getProxy(); + assertThat(tb).as("Picked up secondary interface").isInstanceOf(IOther.class); + raw.setAge(25); + assertThat(tb.getAge() == raw.getAge()).isTrue(); + + long t = 555555L; + TimestampIntroductionInterceptor ti = new TimestampIntroductionInterceptor(t); + + Class[] oldProxiedInterfaces = factory.getProxiedInterfaces(); + + factory.addAdvisor(0, new DefaultIntroductionAdvisor(ti, TimeStamped.class)); + + Class[] newProxiedInterfaces = factory.getProxiedInterfaces(); + assertThat(newProxiedInterfaces.length).as("Advisor proxies one more interface after introduction").isEqualTo(oldProxiedInterfaces.length + 1); + + TimeStamped ts = (TimeStamped) factory.getProxy(); + assertThat(ts.getTimeStamp() == t).isTrue(); + // Shouldn't fail; + ((IOther) ts).absquatulate(); + } + + @Test + public void testInterceptorInclusionMethods() { + class MyInterceptor implements MethodInterceptor { + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + throw new UnsupportedOperationException(); + } + } + + NopInterceptor di = new NopInterceptor(); + NopInterceptor diUnused = new NopInterceptor(); + ProxyFactory factory = new ProxyFactory(new TestBean()); + factory.addAdvice(0, di); + assertThat(factory.getProxy()).isInstanceOf(ITestBean.class); + assertThat(factory.adviceIncluded(di)).isTrue(); + assertThat(!factory.adviceIncluded(diUnused)).isTrue(); + assertThat(factory.countAdvicesOfType(NopInterceptor.class) == 1).isTrue(); + assertThat(factory.countAdvicesOfType(MyInterceptor.class) == 0).isTrue(); + + factory.addAdvice(0, diUnused); + assertThat(factory.adviceIncluded(diUnused)).isTrue(); + assertThat(factory.countAdvicesOfType(NopInterceptor.class) == 2).isTrue(); + } + + /** + * Should see effect immediately on behavior. + */ + @Test + public void testCanAddAndRemoveAspectInterfacesOnSingleton() { + ProxyFactory config = new ProxyFactory(new TestBean()); + + assertThat(config.getProxy() instanceof TimeStamped).as("Shouldn't implement TimeStamped before manipulation").isFalse(); + + long time = 666L; + TimestampIntroductionInterceptor ti = new TimestampIntroductionInterceptor(); + ti.setTime(time); + + // Add to front of interceptor chain + int oldCount = config.getAdvisors().length; + config.addAdvisor(0, new DefaultIntroductionAdvisor(ti, TimeStamped.class)); + + assertThat(config.getAdvisors().length == oldCount + 1).isTrue(); + + TimeStamped ts = (TimeStamped) config.getProxy(); + assertThat(ts.getTimeStamp() == time).isTrue(); + + // Can remove + config.removeAdvice(ti); + + assertThat(config.getAdvisors().length == oldCount).isTrue(); + + assertThatExceptionOfType(RuntimeException.class) + .as("Existing object won't implement this interface any more") + .isThrownBy(ts::getTimeStamp); // Existing reference will fail + + assertThat(config.getProxy() instanceof TimeStamped).as("Should no longer implement TimeStamped").isFalse(); + + // Now check non-effect of removing interceptor that isn't there + config.removeAdvice(new DebugInterceptor()); + + assertThat(config.getAdvisors().length == oldCount).isTrue(); + + ITestBean it = (ITestBean) ts; + DebugInterceptor debugInterceptor = new DebugInterceptor(); + config.addAdvice(0, debugInterceptor); + it.getSpouse(); + assertThat(debugInterceptor.getCount()).isEqualTo(1); + config.removeAdvice(debugInterceptor); + it.getSpouse(); + // not invoked again + assertThat(debugInterceptor.getCount() == 1).isTrue(); + } + + @Test + public void testProxyTargetClassWithInterfaceAsTarget() { + ProxyFactory pf = new ProxyFactory(); + pf.setTargetClass(ITestBean.class); + Object proxy = pf.getProxy(); + assertThat(AopUtils.isJdkDynamicProxy(proxy)).as("Proxy is a JDK proxy").isTrue(); + assertThat(proxy instanceof ITestBean).isTrue(); + assertThat(AopProxyUtils.ultimateTargetClass(proxy)).isEqualTo(ITestBean.class); + + ProxyFactory pf2 = new ProxyFactory(proxy); + Object proxy2 = pf2.getProxy(); + assertThat(AopUtils.isJdkDynamicProxy(proxy2)).as("Proxy is a JDK proxy").isTrue(); + assertThat(proxy2 instanceof ITestBean).isTrue(); + assertThat(AopProxyUtils.ultimateTargetClass(proxy2)).isEqualTo(ITestBean.class); + } + + @Test + public void testProxyTargetClassWithConcreteClassAsTarget() { + ProxyFactory pf = new ProxyFactory(); + pf.setTargetClass(TestBean.class); + Object proxy = pf.getProxy(); + assertThat(AopUtils.isCglibProxy(proxy)).as("Proxy is a CGLIB proxy").isTrue(); + assertThat(proxy instanceof TestBean).isTrue(); + assertThat(AopProxyUtils.ultimateTargetClass(proxy)).isEqualTo(TestBean.class); + + ProxyFactory pf2 = new ProxyFactory(proxy); + pf2.setProxyTargetClass(true); + Object proxy2 = pf2.getProxy(); + assertThat(AopUtils.isCglibProxy(proxy2)).as("Proxy is a CGLIB proxy").isTrue(); + assertThat(proxy2 instanceof TestBean).isTrue(); + assertThat(AopProxyUtils.ultimateTargetClass(proxy2)).isEqualTo(TestBean.class); + } + + @Test + @Disabled("Not implemented yet, see https://jira.springframework.org/browse/SPR-5708") + public void testExclusionOfNonPublicInterfaces() { + JFrame frame = new JFrame(); + ProxyFactory proxyFactory = new ProxyFactory(frame); + Object proxy = proxyFactory.getProxy(); + assertThat(proxy instanceof RootPaneContainer).isTrue(); + assertThat(proxy instanceof Accessible).isTrue(); + } + + @Test + public void testInterfaceProxiesCanBeOrderedThroughAnnotations() { + Object proxy1 = new ProxyFactory(new A()).getProxy(); + Object proxy2 = new ProxyFactory(new B()).getProxy(); + List list = new ArrayList<>(2); + list.add(proxy1); + list.add(proxy2); + AnnotationAwareOrderComparator.sort(list); + assertThat(list.get(0)).isSameAs(proxy2); + assertThat(list.get(1)).isSameAs(proxy1); + } + + @Test + public void testTargetClassProxiesCanBeOrderedThroughAnnotations() { + ProxyFactory pf1 = new ProxyFactory(new A()); + pf1.setProxyTargetClass(true); + ProxyFactory pf2 = new ProxyFactory(new B()); + pf2.setProxyTargetClass(true); + Object proxy1 = pf1.getProxy(); + Object proxy2 = pf2.getProxy(); + List list = new ArrayList<>(2); + list.add(proxy1); + list.add(proxy2); + AnnotationAwareOrderComparator.sort(list); + assertThat(list.get(0)).isSameAs(proxy2); + assertThat(list.get(1)).isSameAs(proxy1); + } + + @Test + public void testInterceptorWithoutJoinpoint() { + final TestBean target = new TestBean("tb"); + ITestBean proxy = ProxyFactory.getProxy(ITestBean.class, (MethodInterceptor) invocation -> { + assertThat(invocation.getThis()).isNull(); + return invocation.getMethod().invoke(target, invocation.getArguments()); + }); + assertThat(proxy.getName()).isEqualTo("tb"); + } + + + @Order(2) + public static class A implements Runnable { + + @Override + public void run() { + } + } + + + @Order(1) + public static class B implements Runnable{ + + @Override + public void run() { + } + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptorTests.java new file mode 100644 index 0000000..2c10602 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptorTests.java @@ -0,0 +1,128 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.adapter; + +import java.io.FileNotFoundException; +import java.rmi.ConnectException; +import java.rmi.RemoteException; + +import org.aopalliance.intercept.MethodInvocation; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.testfixture.advice.MyThrowsHandler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * @author Rod Johnson + * @author Chris Beams + */ +public class ThrowsAdviceInterceptorTests { + + @Test + public void testNoHandlerMethods() { + // should require one handler method at least + assertThatIllegalArgumentException().isThrownBy(() -> + new ThrowsAdviceInterceptor(new Object())); + } + + @Test + public void testNotInvoked() throws Throwable { + MyThrowsHandler th = new MyThrowsHandler(); + ThrowsAdviceInterceptor ti = new ThrowsAdviceInterceptor(th); + Object ret = new Object(); + MethodInvocation mi = mock(MethodInvocation.class); + given(mi.proceed()).willReturn(ret); + assertThat(ti.invoke(mi)).isEqualTo(ret); + assertThat(th.getCalls()).isEqualTo(0); + } + + @Test + public void testNoHandlerMethodForThrowable() throws Throwable { + MyThrowsHandler th = new MyThrowsHandler(); + ThrowsAdviceInterceptor ti = new ThrowsAdviceInterceptor(th); + assertThat(ti.getHandlerMethodCount()).isEqualTo(2); + Exception ex = new Exception(); + MethodInvocation mi = mock(MethodInvocation.class); + given(mi.proceed()).willThrow(ex); + assertThatExceptionOfType(Exception.class).isThrownBy(() -> + ti.invoke(mi)) + .isSameAs(ex); + assertThat(th.getCalls()).isEqualTo(0); + } + + @Test + public void testCorrectHandlerUsed() throws Throwable { + MyThrowsHandler th = new MyThrowsHandler(); + ThrowsAdviceInterceptor ti = new ThrowsAdviceInterceptor(th); + FileNotFoundException ex = new FileNotFoundException(); + MethodInvocation mi = mock(MethodInvocation.class); + given(mi.getMethod()).willReturn(Object.class.getMethod("hashCode")); + given(mi.getThis()).willReturn(new Object()); + given(mi.proceed()).willThrow(ex); + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(() -> + ti.invoke(mi)) + .isSameAs(ex); + assertThat(th.getCalls()).isEqualTo(1); + assertThat(th.getCalls("ioException")).isEqualTo(1); + } + + @Test + public void testCorrectHandlerUsedForSubclass() throws Throwable { + MyThrowsHandler th = new MyThrowsHandler(); + ThrowsAdviceInterceptor ti = new ThrowsAdviceInterceptor(th); + // Extends RemoteException + ConnectException ex = new ConnectException(""); + MethodInvocation mi = mock(MethodInvocation.class); + given(mi.proceed()).willThrow(ex); + assertThatExceptionOfType(ConnectException.class).isThrownBy(() -> + ti.invoke(mi)) + .isSameAs(ex); + assertThat(th.getCalls()).isEqualTo(1); + assertThat(th.getCalls("remoteException")).isEqualTo(1); + } + + @Test + public void testHandlerMethodThrowsException() throws Throwable { + final Throwable t = new Throwable(); + + @SuppressWarnings("serial") + MyThrowsHandler th = new MyThrowsHandler() { + @Override + public void afterThrowing(RemoteException ex) throws Throwable { + super.afterThrowing(ex); + throw t; + } + }; + + ThrowsAdviceInterceptor ti = new ThrowsAdviceInterceptor(th); + // Extends RemoteException + ConnectException ex = new ConnectException(""); + MethodInvocation mi = mock(MethodInvocation.class); + given(mi.proceed()).willThrow(ex); + assertThatExceptionOfType(Throwable.class).isThrownBy(() -> + ti.invoke(mi)) + .isSameAs(t); + assertThat(th.getCalls()).isEqualTo(1); + assertThat(th.getCalls("remoteException")).isEqualTo(1); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/ConcurrencyThrottleInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/ConcurrencyThrottleInterceptorTests.java new file mode 100644 index 0000000..9936573 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/ConcurrencyThrottleInterceptorTests.java @@ -0,0 +1,160 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.interceptor; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.Advised; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.testfixture.beans.DerivedTestBean; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.testfixture.io.SerializationTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @author Chris Beams + * @since 06.04.2004 + */ +public class ConcurrencyThrottleInterceptorTests { + + protected static final Log logger = LogFactory.getLog(ConcurrencyThrottleInterceptorTests.class); + + public static final int NR_OF_THREADS = 100; + + public static final int NR_OF_ITERATIONS = 1000; + + + @Test + public void testSerializable() throws Exception { + DerivedTestBean tb = new DerivedTestBean(); + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.setInterfaces(ITestBean.class); + ConcurrencyThrottleInterceptor cti = new ConcurrencyThrottleInterceptor(); + proxyFactory.addAdvice(cti); + proxyFactory.setTarget(tb); + ITestBean proxy = (ITestBean) proxyFactory.getProxy(); + proxy.getAge(); + + ITestBean serializedProxy = SerializationTestUtils.serializeAndDeserialize(proxy); + Advised advised = (Advised) serializedProxy; + ConcurrencyThrottleInterceptor serializedCti = + (ConcurrencyThrottleInterceptor) advised.getAdvisors()[0].getAdvice(); + assertThat(serializedCti.getConcurrencyLimit()).isEqualTo(cti.getConcurrencyLimit()); + serializedProxy.getAge(); + } + + @Test + public void testMultipleThreadsWithLimit1() { + testMultipleThreads(1); + } + + @Test + public void testMultipleThreadsWithLimit10() { + testMultipleThreads(10); + } + + private void testMultipleThreads(int concurrencyLimit) { + TestBean tb = new TestBean(); + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.setInterfaces(ITestBean.class); + ConcurrencyThrottleInterceptor cti = new ConcurrencyThrottleInterceptor(); + cti.setConcurrencyLimit(concurrencyLimit); + proxyFactory.addAdvice(cti); + proxyFactory.setTarget(tb); + ITestBean proxy = (ITestBean) proxyFactory.getProxy(); + + Thread[] threads = new Thread[NR_OF_THREADS]; + for (int i = 0; i < NR_OF_THREADS; i++) { + threads[i] = new ConcurrencyThread(proxy, null); + threads[i].start(); + } + for (int i = 0; i < NR_OF_THREADS / 10; i++) { + try { + Thread.sleep(5); + } + catch (InterruptedException ex) { + ex.printStackTrace(); + } + threads[i] = new ConcurrencyThread(proxy, + i % 2 == 0 ? new OutOfMemoryError() : new IllegalStateException()); + threads[i].start(); + } + for (int i = 0; i < NR_OF_THREADS; i++) { + try { + threads[i].join(); + } + catch (InterruptedException ex) { + ex.printStackTrace(); + } + } + } + + + private static class ConcurrencyThread extends Thread { + + private ITestBean proxy; + private Throwable ex; + + public ConcurrencyThread(ITestBean proxy, Throwable ex) { + this.proxy = proxy; + this.ex = ex; + } + + @Override + public void run() { + if (this.ex != null) { + try { + this.proxy.exceptional(this.ex); + } + catch (RuntimeException ex) { + if (ex == this.ex) { + logger.debug("Expected exception thrown", ex); + } + else { + // should never happen + ex.printStackTrace(); + } + } + catch (Error err) { + if (err == this.ex) { + logger.debug("Expected exception thrown", err); + } + else { + // should never happen + ex.printStackTrace(); + } + } + catch (Throwable ex) { + // should never happen + ex.printStackTrace(); + } + } + else { + for (int i = 0; i < NR_OF_ITERATIONS; i++) { + this.proxy.getName(); + } + } + logger.debug("finished"); + } + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/CustomizableTraceInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/CustomizableTraceInterceptorTests.java new file mode 100644 index 0000000..ebf44f2 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/CustomizableTraceInterceptorTests.java @@ -0,0 +1,182 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.interceptor; + +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author Rob Harrop + * @author Rick Evans + * @author Juergen Hoeller + * @author Chris Beams + */ +public class CustomizableTraceInterceptorTests { + + @Test + public void testSetEmptyEnterMessage() { + // Must not be able to set empty enter message + assertThatIllegalArgumentException().isThrownBy(() -> + new CustomizableTraceInterceptor().setEnterMessage("")); + } + + @Test + public void testSetEnterMessageWithReturnValuePlaceholder() { + // Must not be able to set enter message with return value placeholder + assertThatIllegalArgumentException().isThrownBy(() -> + new CustomizableTraceInterceptor().setEnterMessage(CustomizableTraceInterceptor.PLACEHOLDER_RETURN_VALUE)); + } + + @Test + public void testSetEnterMessageWithExceptionPlaceholder() { + // Must not be able to set enter message with exception placeholder + assertThatIllegalArgumentException().isThrownBy(() -> + new CustomizableTraceInterceptor().setEnterMessage(CustomizableTraceInterceptor.PLACEHOLDER_EXCEPTION)); + } + + @Test + public void testSetEnterMessageWithInvocationTimePlaceholder() { + // Must not be able to set enter message with invocation time placeholder + assertThatIllegalArgumentException().isThrownBy(() -> + new CustomizableTraceInterceptor().setEnterMessage(CustomizableTraceInterceptor.PLACEHOLDER_INVOCATION_TIME)); + } + + @Test + public void testSetEmptyExitMessage() { + // Must not be able to set empty exit message + assertThatIllegalArgumentException().isThrownBy(() -> + new CustomizableTraceInterceptor().setExitMessage("")); + } + + @Test + public void testSetExitMessageWithExceptionPlaceholder() { + // Must not be able to set exit message with exception placeholder + assertThatIllegalArgumentException().isThrownBy(() -> + new CustomizableTraceInterceptor().setExitMessage(CustomizableTraceInterceptor.PLACEHOLDER_EXCEPTION)); + } + + @Test + public void testSetEmptyExceptionMessage() { + // Must not be able to set empty exception message + assertThatIllegalArgumentException().isThrownBy(() -> + new CustomizableTraceInterceptor().setExceptionMessage("")); + } + + @Test + public void testSetExceptionMethodWithReturnValuePlaceholder() { + // Must not be able to set exception message with return value placeholder + assertThatIllegalArgumentException().isThrownBy(() -> + new CustomizableTraceInterceptor().setExceptionMessage(CustomizableTraceInterceptor.PLACEHOLDER_RETURN_VALUE)); + } + + @Test + public void testSunnyDayPathLogsCorrectly() throws Throwable { + + MethodInvocation methodInvocation = mock(MethodInvocation.class); + given(methodInvocation.getMethod()).willReturn(String.class.getMethod("toString")); + given(methodInvocation.getThis()).willReturn(this); + + Log log = mock(Log.class); + given(log.isTraceEnabled()).willReturn(true); + + CustomizableTraceInterceptor interceptor = new StubCustomizableTraceInterceptor(log); + interceptor.invoke(methodInvocation); + + verify(log, times(2)).trace(anyString()); + } + + @Test + public void testExceptionPathLogsCorrectly() throws Throwable { + + MethodInvocation methodInvocation = mock(MethodInvocation.class); + + IllegalArgumentException exception = new IllegalArgumentException(); + given(methodInvocation.getMethod()).willReturn(String.class.getMethod("toString")); + given(methodInvocation.getThis()).willReturn(this); + given(methodInvocation.proceed()).willThrow(exception); + + Log log = mock(Log.class); + given(log.isTraceEnabled()).willReturn(true); + + CustomizableTraceInterceptor interceptor = new StubCustomizableTraceInterceptor(log); + assertThatIllegalArgumentException().isThrownBy(() -> + interceptor.invoke(methodInvocation)); + + verify(log).trace(anyString()); + verify(log).trace(anyString(), eq(exception)); + } + + @Test + public void testSunnyDayPathLogsCorrectlyWithPrettyMuchAllPlaceholdersMatching() throws Throwable { + + MethodInvocation methodInvocation = mock(MethodInvocation.class); + + given(methodInvocation.getMethod()).willReturn(String.class.getMethod("toString", new Class[0])); + given(methodInvocation.getThis()).willReturn(this); + given(methodInvocation.getArguments()).willReturn(new Object[]{"$ One \\$", 2L}); + given(methodInvocation.proceed()).willReturn("Hello!"); + + Log log = mock(Log.class); + given(log.isTraceEnabled()).willReturn(true); + + CustomizableTraceInterceptor interceptor = new StubCustomizableTraceInterceptor(log); + interceptor.setEnterMessage(new StringBuilder() + .append("Entering the '").append(CustomizableTraceInterceptor.PLACEHOLDER_METHOD_NAME) + .append("' method of the [").append(CustomizableTraceInterceptor.PLACEHOLDER_TARGET_CLASS_NAME) + .append("] class with the following args (").append(CustomizableTraceInterceptor.PLACEHOLDER_ARGUMENTS) + .append(") and arg types (").append(CustomizableTraceInterceptor.PLACEHOLDER_ARGUMENT_TYPES) + .append(").").toString()); + interceptor.setExitMessage(new StringBuilder() + .append("Exiting the '").append(CustomizableTraceInterceptor.PLACEHOLDER_METHOD_NAME) + .append("' method of the [").append(CustomizableTraceInterceptor.PLACEHOLDER_TARGET_CLASS_SHORT_NAME) + .append("] class with the following args (").append(CustomizableTraceInterceptor.PLACEHOLDER_ARGUMENTS) + .append(") and arg types (").append(CustomizableTraceInterceptor.PLACEHOLDER_ARGUMENT_TYPES) + .append("), returning '").append(CustomizableTraceInterceptor.PLACEHOLDER_RETURN_VALUE) + .append("' and taking '").append(CustomizableTraceInterceptor.PLACEHOLDER_INVOCATION_TIME) + .append("' this long.").toString()); + interceptor.invoke(methodInvocation); + + verify(log, times(2)).trace(anyString()); + } + + + @SuppressWarnings("serial") + private static class StubCustomizableTraceInterceptor extends CustomizableTraceInterceptor { + + private final Log log; + + public StubCustomizableTraceInterceptor(Log log) { + super.setUseDynamicLogger(false); + this.log = log; + } + + @Override + protected Log getLoggerForInvocation(MethodInvocation invocation) { + return this.log; + } + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/DebugInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/DebugInterceptorTests.java new file mode 100644 index 0000000..427b9cb --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/DebugInterceptorTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.interceptor; + +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Unit tests for the {@link DebugInterceptor} class. + * + * @author Rick Evans + * @author Chris Beams + */ +public class DebugInterceptorTests { + + @Test + public void testSunnyDayPathLogsCorrectly() throws Throwable { + + MethodInvocation methodInvocation = mock(MethodInvocation.class); + + Log log = mock(Log.class); + given(log.isTraceEnabled()).willReturn(true); + + DebugInterceptor interceptor = new StubDebugInterceptor(log); + interceptor.invoke(methodInvocation); + checkCallCountTotal(interceptor); + + verify(log, times(2)).trace(anyString()); + } + + @Test + public void testExceptionPathStillLogsCorrectly() throws Throwable { + + MethodInvocation methodInvocation = mock(MethodInvocation.class); + + IllegalArgumentException exception = new IllegalArgumentException(); + given(methodInvocation.proceed()).willThrow(exception); + + Log log = mock(Log.class); + given(log.isTraceEnabled()).willReturn(true); + + DebugInterceptor interceptor = new StubDebugInterceptor(log); + assertThatIllegalArgumentException().isThrownBy(() -> + interceptor.invoke(methodInvocation)); + checkCallCountTotal(interceptor); + + verify(log).trace(anyString()); + verify(log).trace(anyString(), eq(exception)); + } + + private void checkCallCountTotal(DebugInterceptor interceptor) { + assertThat(interceptor.getCount()).as("Intercepted call count not being incremented correctly").isEqualTo(1); + } + + + @SuppressWarnings("serial") + private static final class StubDebugInterceptor extends DebugInterceptor { + + private final Log log; + + + public StubDebugInterceptor(Log log) { + super(true); + this.log = log; + } + + @Override + protected Log getLoggerForInvocation(MethodInvocation invocation) { + return log; + } + + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/ExposeBeanNameAdvisorsTests.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/ExposeBeanNameAdvisorsTests.java new file mode 100644 index 0000000..9bd43d1 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/ExposeBeanNameAdvisorsTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.interceptor; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.factory.NamedBean; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rod Johnson + * @author Chris Beams + */ +public class ExposeBeanNameAdvisorsTests { + + private class RequiresBeanNameBoundTestBean extends TestBean { + private final String beanName; + + public RequiresBeanNameBoundTestBean(String beanName) { + this.beanName = beanName; + } + + @Override + public int getAge() { + assertThat(ExposeBeanNameAdvisors.getBeanName()).isEqualTo(beanName); + return super.getAge(); + } + } + + @Test + public void testNoIntroduction() { + String beanName = "foo"; + TestBean target = new RequiresBeanNameBoundTestBean(beanName); + ProxyFactory pf = new ProxyFactory(target); + pf.addAdvisor(ExposeInvocationInterceptor.ADVISOR); + pf.addAdvisor(ExposeBeanNameAdvisors.createAdvisorWithoutIntroduction(beanName)); + ITestBean proxy = (ITestBean) pf.getProxy(); + + boolean condition = proxy instanceof NamedBean; + assertThat(condition).as("No introduction").isFalse(); + // Requires binding + proxy.getAge(); + } + + @Test + public void testWithIntroduction() { + String beanName = "foo"; + TestBean target = new RequiresBeanNameBoundTestBean(beanName); + ProxyFactory pf = new ProxyFactory(target); + pf.addAdvisor(ExposeInvocationInterceptor.ADVISOR); + pf.addAdvisor(ExposeBeanNameAdvisors.createAdvisorIntroducingNamedBean(beanName)); + ITestBean proxy = (ITestBean) pf.getProxy(); + + boolean condition = proxy instanceof NamedBean; + assertThat(condition).as("Introduction was made").isTrue(); + // Requires binding + proxy.getAge(); + + NamedBean nb = (NamedBean) proxy; + assertThat(nb.getBeanName()).as("Name returned correctly").isEqualTo(beanName); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/ExposeInvocationInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/ExposeInvocationInterceptorTests.java new file mode 100644 index 0000000..331dc2b --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/ExposeInvocationInterceptorTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.interceptor; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.beans.testfixture.beans.ITestBean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.core.testfixture.io.ResourceTestUtils.qualifiedResource; + +/** + * Non-XML tests are in AbstractAopProxyTests + * + * @author Rod Johnson + * @author Chris Beams + */ +public class ExposeInvocationInterceptorTests { + + @Test + public void testXmlConfig() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions( + qualifiedResource(ExposeInvocationInterceptorTests.class, "context.xml")); + ITestBean tb = (ITestBean) bf.getBean("proxy"); + String name = "tony"; + tb.setName(name); + // Fires context checks + assertThat(tb.getName()).isEqualTo(name); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/ExposedInvocationTestBean.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/ExposedInvocationTestBean.java new file mode 100644 index 0000000..95e8b7d --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/ExposedInvocationTestBean.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.interceptor; + +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.beans.testfixture.beans.TestBean; + +abstract class ExposedInvocationTestBean extends TestBean { + + @Override + public String getName() { + MethodInvocation invocation = ExposeInvocationInterceptor.currentInvocation(); + assertions(invocation); + return super.getName(); + } + + @Override + public void absquatulate() { + MethodInvocation invocation = ExposeInvocationInterceptor.currentInvocation(); + assertions(invocation); + super.absquatulate(); + } + + protected abstract void assertions(MethodInvocation invocation); +} diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/InvocationCheckExposedInvocationTestBean.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/InvocationCheckExposedInvocationTestBean.java new file mode 100644 index 0000000..05fdd00 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/InvocationCheckExposedInvocationTestBean.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.interceptor; + +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.beans.testfixture.beans.ITestBean; + +import static org.assertj.core.api.Assertions.assertThat; + +class InvocationCheckExposedInvocationTestBean extends ExposedInvocationTestBean { + + @Override + protected void assertions(MethodInvocation invocation) { + assertThat(invocation.getThis() == this).isTrue(); + assertThat(ITestBean.class.isAssignableFrom(invocation.getMethod().getDeclaringClass())).as("Invocation should be on ITestBean: " + invocation.getMethod()).isTrue(); + } +} diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/JamonPerformanceMonitorInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/JamonPerformanceMonitorInterceptorTests.java new file mode 100644 index 0000000..18556d6 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/JamonPerformanceMonitorInterceptorTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.interceptor; + +import com.jamonapi.MonitorFactory; +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * @author Steve Souza + * @since 4.1 + */ +public class JamonPerformanceMonitorInterceptorTests { + + private final JamonPerformanceMonitorInterceptor interceptor = new JamonPerformanceMonitorInterceptor(); + + private final MethodInvocation mi = mock(MethodInvocation.class); + + private final Log log = mock(Log.class); + + + @BeforeEach + public void setUp() { + MonitorFactory.reset(); + } + + @AfterEach + public void tearDown() { + MonitorFactory.reset(); + } + + + @Test + public void testInvokeUnderTraceWithNormalProcessing() throws Throwable { + given(mi.getMethod()).willReturn(String.class.getMethod("toString")); + + interceptor.invokeUnderTrace(mi, log); + + assertThat(MonitorFactory.getNumRows() > 0).as("jamon must track the method being invoked").isTrue(); + assertThat(MonitorFactory.getReport().contains("toString")).as("The jamon report must contain the toString method that was invoked").isTrue(); + } + + @Test + public void testInvokeUnderTraceWithExceptionTracking() throws Throwable { + given(mi.getMethod()).willReturn(String.class.getMethod("toString")); + given(mi.proceed()).willThrow(new IllegalArgumentException()); + + assertThatIllegalArgumentException().isThrownBy(() -> + interceptor.invokeUnderTrace(mi, log)); + + assertThat(MonitorFactory.getNumRows()).as("Monitors must exist for the method invocation and 2 exceptions").isEqualTo(3); + assertThat(MonitorFactory.getReport().contains("toString")).as("The jamon report must contain the toString method that was invoked").isTrue(); + assertThat(MonitorFactory.getReport().contains(MonitorFactory.EXCEPTIONS_LABEL)).as("The jamon report must contain the generic exception: " + MonitorFactory.EXCEPTIONS_LABEL).isTrue(); + assertThat(MonitorFactory.getReport().contains("IllegalArgumentException")).as("The jamon report must contain the specific exception: IllegalArgumentException'").isTrue(); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/PerformanceMonitorInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/PerformanceMonitorInterceptorTests.java new file mode 100644 index 0000000..c4c4f94 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/PerformanceMonitorInterceptorTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.interceptor; + +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author Rob Harrop + * @author Rick Evans + * @author Chris Beams + */ +public class PerformanceMonitorInterceptorTests { + + @Test + public void testSuffixAndPrefixAssignment() { + PerformanceMonitorInterceptor interceptor = new PerformanceMonitorInterceptor(); + + assertThat(interceptor.getPrefix()).isNotNull(); + assertThat(interceptor.getSuffix()).isNotNull(); + + interceptor.setPrefix(null); + interceptor.setSuffix(null); + + assertThat(interceptor.getPrefix()).isNotNull(); + assertThat(interceptor.getSuffix()).isNotNull(); + } + + @Test + public void testSunnyDayPathLogsPerformanceMetricsCorrectly() throws Throwable { + MethodInvocation mi = mock(MethodInvocation.class); + given(mi.getMethod()).willReturn(String.class.getMethod("toString", new Class[0])); + + Log log = mock(Log.class); + + PerformanceMonitorInterceptor interceptor = new PerformanceMonitorInterceptor(true); + interceptor.invokeUnderTrace(mi, log); + + verify(log).trace(anyString()); + } + + @Test + public void testExceptionPathStillLogsPerformanceMetricsCorrectly() throws Throwable { + MethodInvocation mi = mock(MethodInvocation.class); + + given(mi.getMethod()).willReturn(String.class.getMethod("toString", new Class[0])); + given(mi.proceed()).willThrow(new IllegalArgumentException()); + Log log = mock(Log.class); + + PerformanceMonitorInterceptor interceptor = new PerformanceMonitorInterceptor(true); + assertThatIllegalArgumentException().isThrownBy(() -> + interceptor.invokeUnderTrace(mi, log)); + + verify(log).trace(anyString()); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/SimpleTraceInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/SimpleTraceInterceptorTests.java new file mode 100644 index 0000000..f076e31 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/SimpleTraceInterceptorTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.interceptor; + +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Unit tests for the {@link SimpleTraceInterceptor} class. + * + * @author Rick Evans + * @author Chris Beams + */ +public class SimpleTraceInterceptorTests { + + @Test + public void testSunnyDayPathLogsCorrectly() throws Throwable { + MethodInvocation mi = mock(MethodInvocation.class); + given(mi.getMethod()).willReturn(String.class.getMethod("toString")); + given(mi.getThis()).willReturn(this); + + Log log = mock(Log.class); + + SimpleTraceInterceptor interceptor = new SimpleTraceInterceptor(true); + interceptor.invokeUnderTrace(mi, log); + + verify(log, times(2)).trace(anyString()); + } + + @Test + public void testExceptionPathStillLogsCorrectly() throws Throwable { + MethodInvocation mi = mock(MethodInvocation.class); + given(mi.getMethod()).willReturn(String.class.getMethod("toString")); + given(mi.getThis()).willReturn(this); + IllegalArgumentException exception = new IllegalArgumentException(); + given(mi.proceed()).willThrow(exception); + + Log log = mock(Log.class); + + final SimpleTraceInterceptor interceptor = new SimpleTraceInterceptor(true); + assertThatIllegalArgumentException().isThrownBy(() -> + interceptor.invokeUnderTrace(mi, log)); + + verify(log).trace(anyString()); + verify(log).trace(anyString(), eq(exception)); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/scope/DefaultScopedObjectTests.java b/spring-aop/src/test/java/org/springframework/aop/scope/DefaultScopedObjectTests.java new file mode 100644 index 0000000..296da8b --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/scope/DefaultScopedObjectTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.scope; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.ConfigurableBeanFactory; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for the {@link DefaultScopedObject} class. + * + * @author Rick Evans + * @author Chris Beams + */ +public class DefaultScopedObjectTests { + + private static final String GOOD_BEAN_NAME = "foo"; + + + @Test + public void testCtorWithNullBeanFactory() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new DefaultScopedObject(null, GOOD_BEAN_NAME)); + } + + @Test + public void testCtorWithNullTargetBeanName() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + testBadTargetBeanName(null)); + } + + @Test + public void testCtorWithEmptyTargetBeanName() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + testBadTargetBeanName("")); + } + + @Test + public void testCtorWithJustWhitespacedTargetBeanName() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + testBadTargetBeanName(" ")); + } + + private static void testBadTargetBeanName(final String badTargetBeanName) { + ConfigurableBeanFactory factory = mock(ConfigurableBeanFactory.class); + new DefaultScopedObject(factory, badTargetBeanName); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyAutowireTests.java b/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyAutowireTests.java new file mode 100644 index 0000000..a746532 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyAutowireTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.scope; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.core.testfixture.io.ResourceTestUtils.qualifiedResource; + +/** + * @author Mark Fisher + * @author Juergen Hoeller + * @author Chris Beams + */ +public class ScopedProxyAutowireTests { + + @Test + public void testScopedProxyInheritsAutowireCandidateFalse() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions( + qualifiedResource(ScopedProxyAutowireTests.class, "scopedAutowireFalse.xml")); + + assertThat(Arrays.asList(bf.getBeanNamesForType(TestBean.class, false, false)).contains("scoped")).isTrue(); + assertThat(Arrays.asList(bf.getBeanNamesForType(TestBean.class, true, false)).contains("scoped")).isTrue(); + assertThat(bf.containsSingleton("scoped")).isFalse(); + TestBean autowired = (TestBean) bf.getBean("autowired"); + TestBean unscoped = (TestBean) bf.getBean("unscoped"); + assertThat(autowired.getChild()).isSameAs(unscoped); + } + + @Test + public void testScopedProxyReplacesAutowireCandidateTrue() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions( + qualifiedResource(ScopedProxyAutowireTests.class, "scopedAutowireTrue.xml")); + + assertThat(Arrays.asList(bf.getBeanNamesForType(TestBean.class, true, false)).contains("scoped")).isTrue(); + assertThat(Arrays.asList(bf.getBeanNamesForType(TestBean.class, false, false)).contains("scoped")).isTrue(); + assertThat(bf.containsSingleton("scoped")).isFalse(); + TestBean autowired = (TestBean) bf.getBean("autowired"); + TestBean scoped = (TestBean) bf.getBean("scoped"); + assertThat(autowired.getChild()).isSameAs(scoped); + } + + + static class TestBean { + + private TestBean child; + + public void setChild(TestBean child) { + this.child = child; + } + + public TestBean getChild() { + return this.child; + } + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyUtilsTests.java b/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyUtilsTests.java new file mode 100644 index 0000000..b69616f --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyUtilsTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.scope; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Unit tests for {@link ScopedProxyUtils}. + * + * @author Sam Brannen + * @since 5.1.10 + */ +class ScopedProxyUtilsTests { + + @Test + void getTargetBeanNameAndIsScopedTarget() { + String originalBeanName = "myBean"; + String targetBeanName = ScopedProxyUtils.getTargetBeanName(originalBeanName); + + assertThat(targetBeanName).isNotEqualTo(originalBeanName).endsWith(originalBeanName); + assertThat(ScopedProxyUtils.isScopedTarget(targetBeanName)).isTrue(); + assertThat(ScopedProxyUtils.isScopedTarget(originalBeanName)).isFalse(); + } + + @Test + void getOriginalBeanNameAndIsScopedTarget() { + String originalBeanName = "myBean"; + String targetBeanName = ScopedProxyUtils.getTargetBeanName(originalBeanName); + String parsedOriginalBeanName = ScopedProxyUtils.getOriginalBeanName(targetBeanName); + + assertThat(parsedOriginalBeanName).isNotEqualTo(targetBeanName).isEqualTo(originalBeanName); + assertThat(ScopedProxyUtils.isScopedTarget(targetBeanName)).isTrue(); + assertThat(ScopedProxyUtils.isScopedTarget(parsedOriginalBeanName)).isFalse(); + } + + @Test + void getOriginalBeanNameForNullTargetBean() { + assertThatIllegalArgumentException() + .isThrownBy(() -> ScopedProxyUtils.getOriginalBeanName(null)) + .withMessage("bean name 'null' does not refer to the target of a scoped proxy"); + } + + @Test + void getOriginalBeanNameForNonScopedTarget() { + assertThatIllegalArgumentException() + .isThrownBy(() -> ScopedProxyUtils.getOriginalBeanName("myBean")) + .withMessage("bean name 'myBean' does not refer to the target of a scoped proxy"); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/support/AbstractRegexpMethodPointcutTests.java b/spring-aop/src/test/java/org/springframework/aop/support/AbstractRegexpMethodPointcutTests.java new file mode 100644 index 0000000..9970b49 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/support/AbstractRegexpMethodPointcutTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import java.io.IOException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.testfixture.io.SerializationTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rod Johnson + * @author Dmitriy Kopylenko + * @author Chris Beams + */ +public abstract class AbstractRegexpMethodPointcutTests { + + private AbstractRegexpMethodPointcut rpc; + + @BeforeEach + public void setUp() { + rpc = getRegexpMethodPointcut(); + } + + protected abstract AbstractRegexpMethodPointcut getRegexpMethodPointcut(); + + @Test + public void testNoPatternSupplied() throws Exception { + noPatternSuppliedTests(rpc); + } + + @Test + public void testSerializationWithNoPatternSupplied() throws Exception { + rpc = SerializationTestUtils.serializeAndDeserialize(rpc); + noPatternSuppliedTests(rpc); + } + + protected void noPatternSuppliedTests(AbstractRegexpMethodPointcut rpc) throws Exception { + assertThat(rpc.matches(Object.class.getMethod("hashCode"), String.class)).isFalse(); + assertThat(rpc.matches(Object.class.getMethod("wait"), Object.class)).isFalse(); + assertThat(rpc.getPatterns().length).isEqualTo(0); + } + + @Test + public void testExactMatch() throws Exception { + rpc.setPattern("java.lang.Object.hashCode"); + exactMatchTests(rpc); + rpc = SerializationTestUtils.serializeAndDeserialize(rpc); + exactMatchTests(rpc); + } + + protected void exactMatchTests(AbstractRegexpMethodPointcut rpc) throws Exception { + // assumes rpc.setPattern("java.lang.Object.hashCode"); + assertThat(rpc.matches(Object.class.getMethod("hashCode"), String.class)).isTrue(); + assertThat(rpc.matches(Object.class.getMethod("hashCode"), Object.class)).isTrue(); + assertThat(rpc.matches(Object.class.getMethod("wait"), Object.class)).isFalse(); + } + + @Test + public void testSpecificMatch() throws Exception { + rpc.setPattern("java.lang.String.hashCode"); + assertThat(rpc.matches(Object.class.getMethod("hashCode"), String.class)).isTrue(); + assertThat(rpc.matches(Object.class.getMethod("hashCode"), Object.class)).isFalse(); + } + + @Test + public void testWildcard() throws Exception { + rpc.setPattern(".*Object.hashCode"); + assertThat(rpc.matches(Object.class.getMethod("hashCode"), Object.class)).isTrue(); + assertThat(rpc.matches(Object.class.getMethod("wait"), Object.class)).isFalse(); + } + + @Test + public void testWildcardForOneClass() throws Exception { + rpc.setPattern("java.lang.Object.*"); + assertThat(rpc.matches(Object.class.getMethod("hashCode"), String.class)).isTrue(); + assertThat(rpc.matches(Object.class.getMethod("wait"), String.class)).isTrue(); + } + + @Test + public void testMatchesObjectClass() throws Exception { + rpc.setPattern("java.lang.Object.*"); + assertThat(rpc.matches(Exception.class.getMethod("hashCode"), IOException.class)).isTrue(); + // Doesn't match a method from Throwable + assertThat(rpc.matches(Exception.class.getMethod("getMessage"), Exception.class)).isFalse(); + } + + @Test + public void testWithExclusion() throws Exception { + this.rpc.setPattern(".*get.*"); + this.rpc.setExcludedPattern(".*Age.*"); + assertThat(this.rpc.matches(TestBean.class.getMethod("getName"), TestBean.class)).isTrue(); + assertThat(this.rpc.matches(TestBean.class.getMethod("getAge"), TestBean.class)).isFalse(); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/support/AopUtilsTests.java b/spring-aop/src/test/java/org/springframework/aop/support/AopUtilsTests.java new file mode 100644 index 0000000..dc5437e --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/support/AopUtilsTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.ClassFilter; +import org.springframework.aop.MethodMatcher; +import org.springframework.aop.Pointcut; +import org.springframework.aop.interceptor.ExposeInvocationInterceptor; +import org.springframework.aop.target.EmptyTargetSource; +import org.springframework.aop.testfixture.interceptor.NopInterceptor; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.testfixture.io.SerializationTestUtils; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rod Johnson + * @author Chris Beams + */ +public class AopUtilsTests { + + @Test + public void testPointcutCanNeverApply() { + class TestPointcut extends StaticMethodMatcherPointcut { + @Override + public boolean matches(Method method, @Nullable Class clazzy) { + return false; + } + } + + Pointcut no = new TestPointcut(); + assertThat(AopUtils.canApply(no, Object.class)).isFalse(); + } + + @Test + public void testPointcutAlwaysApplies() { + assertThat(AopUtils.canApply(new DefaultPointcutAdvisor(new NopInterceptor()), Object.class)).isTrue(); + assertThat(AopUtils.canApply(new DefaultPointcutAdvisor(new NopInterceptor()), TestBean.class)).isTrue(); + } + + @Test + public void testPointcutAppliesToOneMethodOnObject() { + class TestPointcut extends StaticMethodMatcherPointcut { + @Override + public boolean matches(Method method, @Nullable Class clazz) { + return method.getName().equals("hashCode"); + } + } + + Pointcut pc = new TestPointcut(); + + // will return true if we're not proxying interfaces + assertThat(AopUtils.canApply(pc, Object.class)).isTrue(); + } + + /** + * Test that when we serialize and deserialize various canonical instances + * of AOP classes, they return the same instance, not a new instance + * that's subverted the singleton construction limitation. + */ + @Test + public void testCanonicalFrameworkClassesStillCanonicalOnDeserialization() throws Exception { + assertThat(SerializationTestUtils.serializeAndDeserialize(MethodMatcher.TRUE)).isSameAs(MethodMatcher.TRUE); + assertThat(SerializationTestUtils.serializeAndDeserialize(ClassFilter.TRUE)).isSameAs(ClassFilter.TRUE); + assertThat(SerializationTestUtils.serializeAndDeserialize(Pointcut.TRUE)).isSameAs(Pointcut.TRUE); + assertThat(SerializationTestUtils.serializeAndDeserialize(EmptyTargetSource.INSTANCE)).isSameAs(EmptyTargetSource.INSTANCE); + assertThat(SerializationTestUtils.serializeAndDeserialize(Pointcuts.SETTERS)).isSameAs(Pointcuts.SETTERS); + assertThat(SerializationTestUtils.serializeAndDeserialize(Pointcuts.GETTERS)).isSameAs(Pointcuts.GETTERS); + assertThat(SerializationTestUtils.serializeAndDeserialize(ExposeInvocationInterceptor.INSTANCE)).isSameAs(ExposeInvocationInterceptor.INSTANCE); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/support/ClassFiltersTests.java b/spring-aop/src/test/java/org/springframework/aop/support/ClassFiltersTests.java new file mode 100644 index 0000000..db83a94 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/support/ClassFiltersTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.ClassFilter; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.NestedRuntimeException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link ClassFilters}. + * + * @author Rod Johnson + * @author Chris Beams + * @author Sam Brannen + */ +class ClassFiltersTests { + + private final ClassFilter exceptionFilter = new RootClassFilter(Exception.class); + + private final ClassFilter interfaceFilter = new RootClassFilter(ITestBean.class); + + private final ClassFilter hasRootCauseFilter = new RootClassFilter(NestedRuntimeException.class); + + + @Test + void union() { + assertThat(exceptionFilter.matches(RuntimeException.class)).isTrue(); + assertThat(exceptionFilter.matches(TestBean.class)).isFalse(); + assertThat(interfaceFilter.matches(Exception.class)).isFalse(); + assertThat(interfaceFilter.matches(TestBean.class)).isTrue(); + ClassFilter union = ClassFilters.union(exceptionFilter, interfaceFilter); + assertThat(union.matches(RuntimeException.class)).isTrue(); + assertThat(union.matches(TestBean.class)).isTrue(); + assertThat(union.toString()) + .matches("^.+UnionClassFilter: \\[.+RootClassFilter: .+Exception, .+RootClassFilter: .+TestBean\\]$"); + } + + @Test + void intersection() { + assertThat(exceptionFilter.matches(RuntimeException.class)).isTrue(); + assertThat(hasRootCauseFilter.matches(NestedRuntimeException.class)).isTrue(); + ClassFilter intersection = ClassFilters.intersection(exceptionFilter, hasRootCauseFilter); + assertThat(intersection.matches(RuntimeException.class)).isFalse(); + assertThat(intersection.matches(TestBean.class)).isFalse(); + assertThat(intersection.matches(NestedRuntimeException.class)).isTrue(); + assertThat(intersection.toString()) + .matches("^.+IntersectionClassFilter: \\[.+RootClassFilter: .+Exception, .+RootClassFilter: .+NestedRuntimeException\\]$"); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/support/ClassUtilsTests.java b/spring-aop/src/test/java/org/springframework/aop/support/ClassUtilsTests.java new file mode 100644 index 0000000..6eac64e --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/support/ClassUtilsTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Colin Sampaleanu + * @author Juergen Hoeller + * @author Rob Harrop + * @author Rick Evans + */ +public class ClassUtilsTests { + + @Test + public void getShortNameForCglibClass() { + TestBean tb = new TestBean(); + ProxyFactory pf = new ProxyFactory(); + pf.setTarget(tb); + pf.setProxyTargetClass(true); + TestBean proxy = (TestBean) pf.getProxy(); + String className = ClassUtils.getShortName(proxy.getClass()); + assertThat(className).as("Class name did not match").isEqualTo("TestBean"); + } +} diff --git a/spring-aop/src/test/java/org/springframework/aop/support/ComposablePointcutTests.java b/spring-aop/src/test/java/org/springframework/aop/support/ComposablePointcutTests.java new file mode 100644 index 0000000..87b4082 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/support/ComposablePointcutTests.java @@ -0,0 +1,159 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.ClassFilter; +import org.springframework.aop.MethodMatcher; +import org.springframework.aop.Pointcut; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.NestedRuntimeException; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rod Johnson + * @author Chris Beams + */ +public class ComposablePointcutTests { + + public static MethodMatcher GETTER_METHOD_MATCHER = new StaticMethodMatcher() { + @Override + public boolean matches(Method m, @Nullable Class targetClass) { + return m.getName().startsWith("get"); + } + }; + + public static MethodMatcher GET_AGE_METHOD_MATCHER = new StaticMethodMatcher() { + @Override + public boolean matches(Method m, @Nullable Class targetClass) { + return m.getName().equals("getAge"); + } + }; + + public static MethodMatcher ABSQUATULATE_METHOD_MATCHER = new StaticMethodMatcher() { + @Override + public boolean matches(Method m, @Nullable Class targetClass) { + return m.getName().equals("absquatulate"); + } + }; + + public static MethodMatcher SETTER_METHOD_MATCHER = new StaticMethodMatcher() { + @Override + public boolean matches(Method m, @Nullable Class targetClass) { + return m.getName().startsWith("set"); + } + }; + + + @Test + public void testMatchAll() throws NoSuchMethodException { + Pointcut pc = new ComposablePointcut(); + assertThat(pc.getClassFilter().matches(Object.class)).isTrue(); + assertThat(pc.getMethodMatcher().matches(Object.class.getMethod("hashCode"), Exception.class)).isTrue(); + } + + @Test + public void testFilterByClass() throws NoSuchMethodException { + ComposablePointcut pc = new ComposablePointcut(); + + assertThat(pc.getClassFilter().matches(Object.class)).isTrue(); + + ClassFilter cf = new RootClassFilter(Exception.class); + pc.intersection(cf); + assertThat(pc.getClassFilter().matches(Object.class)).isFalse(); + assertThat(pc.getClassFilter().matches(Exception.class)).isTrue(); + pc.intersection(new RootClassFilter(NestedRuntimeException.class)); + assertThat(pc.getClassFilter().matches(Exception.class)).isFalse(); + assertThat(pc.getClassFilter().matches(NestedRuntimeException.class)).isTrue(); + assertThat(pc.getClassFilter().matches(String.class)).isFalse(); + pc.union(new RootClassFilter(String.class)); + assertThat(pc.getClassFilter().matches(Exception.class)).isFalse(); + assertThat(pc.getClassFilter().matches(String.class)).isTrue(); + assertThat(pc.getClassFilter().matches(NestedRuntimeException.class)).isTrue(); + } + + @Test + public void testUnionMethodMatcher() { + // Matches the getAge() method in any class + ComposablePointcut pc = new ComposablePointcut(ClassFilter.TRUE, GET_AGE_METHOD_MATCHER); + assertThat(Pointcuts.matches(pc, PointcutsTests.TEST_BEAN_ABSQUATULATE, TestBean.class)).isFalse(); + assertThat(Pointcuts.matches(pc, PointcutsTests.TEST_BEAN_GET_AGE, TestBean.class)).isTrue(); + assertThat(Pointcuts.matches(pc, PointcutsTests.TEST_BEAN_GET_NAME, TestBean.class)).isFalse(); + + pc.union(GETTER_METHOD_MATCHER); + // Should now match all getter methods + assertThat(Pointcuts.matches(pc, PointcutsTests.TEST_BEAN_ABSQUATULATE, TestBean.class)).isFalse(); + assertThat(Pointcuts.matches(pc, PointcutsTests.TEST_BEAN_GET_AGE, TestBean.class)).isTrue(); + assertThat(Pointcuts.matches(pc, PointcutsTests.TEST_BEAN_GET_NAME, TestBean.class)).isTrue(); + + pc.union(ABSQUATULATE_METHOD_MATCHER); + // Should now match absquatulate() as well + assertThat(Pointcuts.matches(pc, PointcutsTests.TEST_BEAN_ABSQUATULATE, TestBean.class)).isTrue(); + assertThat(Pointcuts.matches(pc, PointcutsTests.TEST_BEAN_GET_AGE, TestBean.class)).isTrue(); + assertThat(Pointcuts.matches(pc, PointcutsTests.TEST_BEAN_GET_NAME, TestBean.class)).isTrue(); + // But it doesn't match everything + assertThat(Pointcuts.matches(pc, PointcutsTests.TEST_BEAN_SET_AGE, TestBean.class)).isFalse(); + } + + @Test + public void testIntersectionMethodMatcher() { + ComposablePointcut pc = new ComposablePointcut(); + assertThat(pc.getMethodMatcher().matches(PointcutsTests.TEST_BEAN_ABSQUATULATE, TestBean.class)).isTrue(); + assertThat(pc.getMethodMatcher().matches(PointcutsTests.TEST_BEAN_GET_AGE, TestBean.class)).isTrue(); + assertThat(pc.getMethodMatcher().matches(PointcutsTests.TEST_BEAN_GET_NAME, TestBean.class)).isTrue(); + pc.intersection(GETTER_METHOD_MATCHER); + assertThat(pc.getMethodMatcher().matches(PointcutsTests.TEST_BEAN_ABSQUATULATE, TestBean.class)).isFalse(); + assertThat(pc.getMethodMatcher().matches(PointcutsTests.TEST_BEAN_GET_AGE, TestBean.class)).isTrue(); + assertThat(pc.getMethodMatcher().matches(PointcutsTests.TEST_BEAN_GET_NAME, TestBean.class)).isTrue(); + pc.intersection(GET_AGE_METHOD_MATCHER); + // Use the Pointcuts matches method + assertThat(Pointcuts.matches(pc, PointcutsTests.TEST_BEAN_ABSQUATULATE, TestBean.class)).isFalse(); + assertThat(Pointcuts.matches(pc, PointcutsTests.TEST_BEAN_GET_AGE, TestBean.class)).isTrue(); + assertThat(Pointcuts.matches(pc, PointcutsTests.TEST_BEAN_GET_NAME, TestBean.class)).isFalse(); + } + + @Test + public void testEqualsAndHashCode() throws Exception { + ComposablePointcut pc1 = new ComposablePointcut(); + ComposablePointcut pc2 = new ComposablePointcut(); + + assertThat(pc2).isEqualTo(pc1); + assertThat(pc2.hashCode()).isEqualTo(pc1.hashCode()); + + pc1.intersection(GETTER_METHOD_MATCHER); + + assertThat(pc1.equals(pc2)).isFalse(); + assertThat(pc1.hashCode() == pc2.hashCode()).isFalse(); + + pc2.intersection(GETTER_METHOD_MATCHER); + + assertThat(pc2).isEqualTo(pc1); + assertThat(pc2.hashCode()).isEqualTo(pc1.hashCode()); + + pc1.union(GET_AGE_METHOD_MATCHER); + pc2.union(GET_AGE_METHOD_MATCHER); + + assertThat(pc2).isEqualTo(pc1); + assertThat(pc2.hashCode()).isEqualTo(pc1.hashCode()); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/support/ControlFlowPointcutTests.java b/spring-aop/src/test/java/org/springframework/aop/support/ControlFlowPointcutTests.java new file mode 100644 index 0000000..f86c6e7 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/support/ControlFlowPointcutTests.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.Pointcut; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.testfixture.interceptor.NopInterceptor; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rod Johnson + * @author Chris Beams + */ +public class ControlFlowPointcutTests { + + @Test + public void testMatches() { + TestBean target = new TestBean(); + target.setAge(27); + NopInterceptor nop = new NopInterceptor(); + ControlFlowPointcut cflow = new ControlFlowPointcut(One.class, "getAge"); + ProxyFactory pf = new ProxyFactory(target); + ITestBean proxied = (ITestBean) pf.getProxy(); + pf.addAdvisor(new DefaultPointcutAdvisor(cflow, nop)); + + // Not advised, not under One + assertThat(proxied.getAge()).isEqualTo(target.getAge()); + assertThat(nop.getCount()).isEqualTo(0); + + // Will be advised + assertThat(new One().getAge(proxied)).isEqualTo(target.getAge()); + assertThat(nop.getCount()).isEqualTo(1); + + // Won't be advised + assertThat(new One().nomatch(proxied)).isEqualTo(target.getAge()); + assertThat(nop.getCount()).isEqualTo(1); + assertThat(cflow.getEvaluations()).isEqualTo(3); + } + + /** + * Check that we can use a cflow pointcut only in conjunction with + * a static pointcut: e.g. all setter methods that are invoked under + * a particular class. This greatly reduces the number of calls + * to the cflow pointcut, meaning that it's not so prohibitively + * expensive. + */ + @Test + public void testSelectiveApplication() { + TestBean target = new TestBean(); + target.setAge(27); + NopInterceptor nop = new NopInterceptor(); + ControlFlowPointcut cflow = new ControlFlowPointcut(One.class); + Pointcut settersUnderOne = Pointcuts.intersection(Pointcuts.SETTERS, cflow); + ProxyFactory pf = new ProxyFactory(target); + ITestBean proxied = (ITestBean) pf.getProxy(); + pf.addAdvisor(new DefaultPointcutAdvisor(settersUnderOne, nop)); + + // Not advised, not under One + target.setAge(16); + assertThat(nop.getCount()).isEqualTo(0); + + // Not advised; under One but not a setter + assertThat(new One().getAge(proxied)).isEqualTo(16); + assertThat(nop.getCount()).isEqualTo(0); + + // Won't be advised + new One().set(proxied); + assertThat(nop.getCount()).isEqualTo(1); + + // We saved most evaluations + assertThat(cflow.getEvaluations()).isEqualTo(1); + } + + @Test + public void testEqualsAndHashCode() throws Exception { + assertThat(new ControlFlowPointcut(One.class)).isEqualTo(new ControlFlowPointcut(One.class)); + assertThat(new ControlFlowPointcut(One.class, "getAge")).isEqualTo(new ControlFlowPointcut(One.class, "getAge")); + assertThat(new ControlFlowPointcut(One.class, "getAge").equals(new ControlFlowPointcut(One.class))).isFalse(); + assertThat(new ControlFlowPointcut(One.class).hashCode()).isEqualTo(new ControlFlowPointcut(One.class).hashCode()); + assertThat(new ControlFlowPointcut(One.class, "getAge").hashCode()).isEqualTo(new ControlFlowPointcut(One.class, "getAge").hashCode()); + assertThat(new ControlFlowPointcut(One.class, "getAge").hashCode() == new ControlFlowPointcut(One.class).hashCode()).isFalse(); + } + + @Test + public void testToString() { + assertThat(new ControlFlowPointcut(One.class).toString()) + .isEqualTo(ControlFlowPointcut.class.getName() + ": class = " + One.class.getName() + "; methodName = null"); + assertThat(new ControlFlowPointcut(One.class, "getAge").toString()) + .isEqualTo(ControlFlowPointcut.class.getName() + ": class = " + One.class.getName() + "; methodName = getAge"); + } + + public class One { + int getAge(ITestBean proxied) { + return proxied.getAge(); + } + int nomatch(ITestBean proxied) { + return proxied.getAge(); + } + void set(ITestBean proxied) { + proxied.setAge(5); + } + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/support/DelegatingIntroductionInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/support/DelegatingIntroductionInterceptorTests.java new file mode 100644 index 0000000..3e6e08a --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/support/DelegatingIntroductionInterceptorTests.java @@ -0,0 +1,313 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import java.io.Serializable; + +import org.aopalliance.intercept.MethodInterceptor; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.IntroductionAdvisor; +import org.springframework.aop.IntroductionInterceptor; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.testfixture.interceptor.SerializableNopInterceptor; +import org.springframework.beans.testfixture.beans.INestedTestBean; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.NestedTestBean; +import org.springframework.beans.testfixture.beans.Person; +import org.springframework.beans.testfixture.beans.SerializablePerson; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.testfixture.TimeStamped; +import org.springframework.core.testfixture.io.SerializationTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * @author Rod Johnson + * @author Chris Beams + * @since 13.05.2003 + */ +public class DelegatingIntroductionInterceptorTests { + + @Test + public void testNullTarget() throws Exception { + // Shouldn't accept null target + assertThatIllegalArgumentException().isThrownBy(() -> + new DelegatingIntroductionInterceptor(null)); + } + + @Test + public void testIntroductionInterceptorWithDelegation() throws Exception { + TestBean raw = new TestBean(); + assertThat(! (raw instanceof TimeStamped)).isTrue(); + ProxyFactory factory = new ProxyFactory(raw); + + TimeStamped ts = mock(TimeStamped.class); + long timestamp = 111L; + given(ts.getTimeStamp()).willReturn(timestamp); + + factory.addAdvisor(0, new DefaultIntroductionAdvisor(new DelegatingIntroductionInterceptor(ts))); + + TimeStamped tsp = (TimeStamped) factory.getProxy(); + assertThat(tsp.getTimeStamp() == timestamp).isTrue(); + } + + @Test + public void testIntroductionInterceptorWithInterfaceHierarchy() throws Exception { + TestBean raw = new TestBean(); + assertThat(! (raw instanceof SubTimeStamped)).isTrue(); + ProxyFactory factory = new ProxyFactory(raw); + + TimeStamped ts = mock(SubTimeStamped.class); + long timestamp = 111L; + given(ts.getTimeStamp()).willReturn(timestamp); + + factory.addAdvisor(0, new DefaultIntroductionAdvisor(new DelegatingIntroductionInterceptor(ts), SubTimeStamped.class)); + + SubTimeStamped tsp = (SubTimeStamped) factory.getProxy(); + assertThat(tsp.getTimeStamp() == timestamp).isTrue(); + } + + @Test + public void testIntroductionInterceptorWithSuperInterface() throws Exception { + TestBean raw = new TestBean(); + assertThat(! (raw instanceof TimeStamped)).isTrue(); + ProxyFactory factory = new ProxyFactory(raw); + + TimeStamped ts = mock(SubTimeStamped.class); + long timestamp = 111L; + given(ts.getTimeStamp()).willReturn(timestamp); + + factory.addAdvisor(0, new DefaultIntroductionAdvisor(new DelegatingIntroductionInterceptor(ts), TimeStamped.class)); + + TimeStamped tsp = (TimeStamped) factory.getProxy(); + assertThat(!(tsp instanceof SubTimeStamped)).isTrue(); + assertThat(tsp.getTimeStamp() == timestamp).isTrue(); + } + + @Test + public void testAutomaticInterfaceRecognitionInDelegate() throws Exception { + final long t = 1001L; + class Tester implements TimeStamped, ITester { + @Override + public void foo() throws Exception { + } + @Override + public long getTimeStamp() { + return t; + } + } + + DelegatingIntroductionInterceptor ii = new DelegatingIntroductionInterceptor(new Tester()); + + TestBean target = new TestBean(); + + ProxyFactory pf = new ProxyFactory(target); + pf.addAdvisor(0, new DefaultIntroductionAdvisor(ii)); + + //assertTrue(Arrays.binarySearch(pf.getProxiedInterfaces(), TimeStamped.class) != -1); + TimeStamped ts = (TimeStamped) pf.getProxy(); + + assertThat(ts.getTimeStamp() == t).isTrue(); + ((ITester) ts).foo(); + + ((ITestBean) ts).getAge(); + } + + + @Test + public void testAutomaticInterfaceRecognitionInSubclass() throws Exception { + final long t = 1001L; + @SuppressWarnings("serial") + class TestII extends DelegatingIntroductionInterceptor implements TimeStamped, ITester { + @Override + public void foo() throws Exception { + } + @Override + public long getTimeStamp() { + return t; + } + } + + DelegatingIntroductionInterceptor ii = new TestII(); + + TestBean target = new TestBean(); + + ProxyFactory pf = new ProxyFactory(target); + IntroductionAdvisor ia = new DefaultIntroductionAdvisor(ii); + assertThat(ia.isPerInstance()).isTrue(); + pf.addAdvisor(0, ia); + + //assertTrue(Arrays.binarySearch(pf.getProxiedInterfaces(), TimeStamped.class) != -1); + TimeStamped ts = (TimeStamped) pf.getProxy(); + + assertThat(ts).isInstanceOf(TimeStamped.class); + // Shouldn't proxy framework interfaces + assertThat(!(ts instanceof MethodInterceptor)).isTrue(); + assertThat(!(ts instanceof IntroductionInterceptor)).isTrue(); + + assertThat(ts.getTimeStamp() == t).isTrue(); + ((ITester) ts).foo(); + ((ITestBean) ts).getAge(); + + // Test removal + ii.suppressInterface(TimeStamped.class); + // Note that we need to construct a new proxy factory, + // or suppress the interface on the proxy factory + pf = new ProxyFactory(target); + pf.addAdvisor(0, new DefaultIntroductionAdvisor(ii)); + Object o = pf.getProxy(); + assertThat(!(o instanceof TimeStamped)).isTrue(); + } + + @SuppressWarnings("serial") + @Test + public void testIntroductionInterceptorDoesntReplaceToString() throws Exception { + TestBean raw = new TestBean(); + assertThat(! (raw instanceof TimeStamped)).isTrue(); + ProxyFactory factory = new ProxyFactory(raw); + + TimeStamped ts = new SerializableTimeStamped(0); + + factory.addAdvisor(0, new DefaultIntroductionAdvisor(new DelegatingIntroductionInterceptor(ts) { + @Override + public String toString() { + throw new UnsupportedOperationException("Shouldn't be invoked"); + } + })); + + TimeStamped tsp = (TimeStamped) factory.getProxy(); + assertThat(tsp.getTimeStamp()).isEqualTo(0); + + assertThat(tsp.toString()).isEqualTo(raw.toString()); + } + + @Test + public void testDelegateReturnsThisIsMassagedToReturnProxy() { + NestedTestBean target = new NestedTestBean(); + String company = "Interface21"; + target.setCompany(company); + TestBean delegate = new TestBean() { + @Override + public ITestBean getSpouse() { + return this; + } + }; + ProxyFactory pf = new ProxyFactory(target); + pf.addAdvice(new DelegatingIntroductionInterceptor(delegate)); + INestedTestBean proxy = (INestedTestBean) pf.getProxy(); + + assertThat(proxy.getCompany()).isEqualTo(company); + ITestBean introduction = (ITestBean) proxy; + assertThat(introduction.getSpouse()).as("Introduced method returning delegate returns proxy").isSameAs(introduction); + assertThat(AopUtils.isAopProxy(introduction.getSpouse())).as("Introduced method returning delegate returns proxy").isTrue(); + } + + @Test + public void testSerializableDelegatingIntroductionInterceptorSerializable() throws Exception { + SerializablePerson serializableTarget = new SerializablePerson(); + String name = "Tony"; + serializableTarget.setName("Tony"); + + ProxyFactory factory = new ProxyFactory(serializableTarget); + factory.addInterface(Person.class); + long time = 1000; + TimeStamped ts = new SerializableTimeStamped(time); + + factory.addAdvisor(new DefaultIntroductionAdvisor(new DelegatingIntroductionInterceptor(ts))); + factory.addAdvice(new SerializableNopInterceptor()); + + Person p = (Person) factory.getProxy(); + + assertThat(p.getName()).isEqualTo(name); + assertThat(((TimeStamped) p).getTimeStamp()).isEqualTo(time); + + Person p1 = SerializationTestUtils.serializeAndDeserialize(p); + assertThat(p1.getName()).isEqualTo(name); + assertThat(((TimeStamped) p1).getTimeStamp()).isEqualTo(time); + } + + // Test when target implements the interface: should get interceptor by preference. + @Test + public void testIntroductionMasksTargetImplementation() throws Exception { + final long t = 1001L; + @SuppressWarnings("serial") + class TestII extends DelegatingIntroductionInterceptor implements TimeStamped { + @Override + public long getTimeStamp() { + return t; + } + } + + DelegatingIntroductionInterceptor ii = new TestII(); + + // != t + TestBean target = new TargetClass(t + 1); + + ProxyFactory pf = new ProxyFactory(target); + pf.addAdvisor(0, new DefaultIntroductionAdvisor(ii)); + + TimeStamped ts = (TimeStamped) pf.getProxy(); + // From introduction interceptor, not target + assertThat(ts.getTimeStamp() == t).isTrue(); + } + + + @SuppressWarnings("serial") + private static class SerializableTimeStamped implements TimeStamped, Serializable { + + private final long ts; + + public SerializableTimeStamped(long ts) { + this.ts = ts; + } + + @Override + public long getTimeStamp() { + return ts; + } + } + + + public static class TargetClass extends TestBean implements TimeStamped { + + long t; + + public TargetClass(long t) { + this.t = t; + } + + @Override + public long getTimeStamp() { + return t; + } + } + + + public interface ITester { + + void foo() throws Exception; + } + + + private static interface SubTimeStamped extends TimeStamped { + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/support/JdkRegexpMethodPointcutTests.java b/spring-aop/src/test/java/org/springframework/aop/support/JdkRegexpMethodPointcutTests.java new file mode 100644 index 0000000..1e4c139 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/support/JdkRegexpMethodPointcutTests.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +/** + * @author Dmitriy Kopylenko + */ +public class JdkRegexpMethodPointcutTests extends AbstractRegexpMethodPointcutTests { + + @Override + protected AbstractRegexpMethodPointcut getRegexpMethodPointcut() { + return new JdkRegexpMethodPointcut(); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/support/MethodMatchersTests.java b/spring-aop/src/test/java/org/springframework/aop/support/MethodMatchersTests.java new file mode 100644 index 0000000..55a2d7c --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/support/MethodMatchersTests.java @@ -0,0 +1,147 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.MethodMatcher; +import org.springframework.beans.testfixture.beans.IOther; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.testfixture.io.SerializationTestUtils; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @author Chris Beams + */ +public class MethodMatchersTests { + + private final Method EXCEPTION_GETMESSAGE; + + private final Method ITESTBEAN_SETAGE; + + private final Method ITESTBEAN_GETAGE; + + private final Method IOTHER_ABSQUATULATE; + + + public MethodMatchersTests() throws Exception { + EXCEPTION_GETMESSAGE = Exception.class.getMethod("getMessage"); + ITESTBEAN_GETAGE = ITestBean.class.getMethod("getAge"); + ITESTBEAN_SETAGE = ITestBean.class.getMethod("setAge", int.class); + IOTHER_ABSQUATULATE = IOther.class.getMethod("absquatulate"); + } + + + @Test + public void testDefaultMatchesAll() throws Exception { + MethodMatcher defaultMm = MethodMatcher.TRUE; + assertThat(defaultMm.matches(EXCEPTION_GETMESSAGE, Exception.class)).isTrue(); + assertThat(defaultMm.matches(ITESTBEAN_SETAGE, TestBean.class)).isTrue(); + } + + @Test + public void testMethodMatcherTrueSerializable() throws Exception { + assertThat(MethodMatcher.TRUE).isSameAs(SerializationTestUtils.serializeAndDeserialize(MethodMatcher.TRUE)); + } + + @Test + public void testSingle() throws Exception { + MethodMatcher defaultMm = MethodMatcher.TRUE; + assertThat(defaultMm.matches(EXCEPTION_GETMESSAGE, Exception.class)).isTrue(); + assertThat(defaultMm.matches(ITESTBEAN_SETAGE, TestBean.class)).isTrue(); + defaultMm = MethodMatchers.intersection(defaultMm, new StartsWithMatcher("get")); + + assertThat(defaultMm.matches(EXCEPTION_GETMESSAGE, Exception.class)).isTrue(); + assertThat(defaultMm.matches(ITESTBEAN_SETAGE, TestBean.class)).isFalse(); + } + + + @Test + public void testDynamicAndStaticMethodMatcherIntersection() throws Exception { + MethodMatcher mm1 = MethodMatcher.TRUE; + MethodMatcher mm2 = new TestDynamicMethodMatcherWhichMatches(); + MethodMatcher intersection = MethodMatchers.intersection(mm1, mm2); + assertThat(intersection.isRuntime()).as("Intersection is a dynamic matcher").isTrue(); + assertThat(intersection.matches(ITESTBEAN_SETAGE, TestBean.class)).as("2Matched setAge method").isTrue(); + assertThat(intersection.matches(ITESTBEAN_SETAGE, TestBean.class, 5)).as("3Matched setAge method").isTrue(); + // Knock out dynamic part + intersection = MethodMatchers.intersection(intersection, new TestDynamicMethodMatcherWhichDoesNotMatch()); + assertThat(intersection.isRuntime()).as("Intersection is a dynamic matcher").isTrue(); + assertThat(intersection.matches(ITESTBEAN_SETAGE, TestBean.class)).as("2Matched setAge method").isTrue(); + assertThat(intersection.matches(ITESTBEAN_SETAGE, TestBean.class, 5)).as("3 - not Matched setAge method").isFalse(); + } + + @Test + public void testStaticMethodMatcherUnion() throws Exception { + MethodMatcher getterMatcher = new StartsWithMatcher("get"); + MethodMatcher setterMatcher = new StartsWithMatcher("set"); + MethodMatcher union = MethodMatchers.union(getterMatcher, setterMatcher); + + assertThat(union.isRuntime()).as("Union is a static matcher").isFalse(); + assertThat(union.matches(ITESTBEAN_SETAGE, TestBean.class)).as("Matched setAge method").isTrue(); + assertThat(union.matches(ITESTBEAN_GETAGE, TestBean.class)).as("Matched getAge method").isTrue(); + assertThat(union.matches(IOTHER_ABSQUATULATE, TestBean.class)).as("Didn't matched absquatulate method").isFalse(); + } + + @Test + public void testUnionEquals() { + MethodMatcher first = MethodMatchers.union(MethodMatcher.TRUE, MethodMatcher.TRUE); + MethodMatcher second = new ComposablePointcut(MethodMatcher.TRUE).union(new ComposablePointcut(MethodMatcher.TRUE)).getMethodMatcher(); + assertThat(first.equals(second)).isTrue(); + assertThat(second.equals(first)).isTrue(); + } + + + public static class StartsWithMatcher extends StaticMethodMatcher { + + private final String prefix; + + public StartsWithMatcher(String s) { + this.prefix = s; + } + + @Override + public boolean matches(Method m, @Nullable Class targetClass) { + return m.getName().startsWith(prefix); + } + } + + + private static class TestDynamicMethodMatcherWhichMatches extends DynamicMethodMatcher { + + @Override + public boolean matches(Method m, @Nullable Class targetClass, Object... args) { + return true; + } + } + + + private static class TestDynamicMethodMatcherWhichDoesNotMatch extends DynamicMethodMatcher { + + @Override + public boolean matches(Method m, @Nullable Class targetClass, Object... args) { + return false; + } + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/support/NameMatchMethodPointcutTests.java b/spring-aop/src/test/java/org/springframework/aop/support/NameMatchMethodPointcutTests.java new file mode 100644 index 0000000..6c7928a --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/support/NameMatchMethodPointcutTests.java @@ -0,0 +1,139 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.Advised; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.testfixture.interceptor.NopInterceptor; +import org.springframework.aop.testfixture.interceptor.SerializableNopInterceptor; +import org.springframework.beans.testfixture.beans.Person; +import org.springframework.beans.testfixture.beans.SerializablePerson; +import org.springframework.core.testfixture.io.SerializationTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rod Johnson + * @author Chris Beams + */ +public class NameMatchMethodPointcutTests { + + protected NameMatchMethodPointcut pc; + + protected Person proxied; + + protected SerializableNopInterceptor nop; + + + /** + * Create an empty pointcut, populating instance variables. + */ + @BeforeEach + public void setup() { + ProxyFactory pf = new ProxyFactory(new SerializablePerson()); + nop = new SerializableNopInterceptor(); + pc = new NameMatchMethodPointcut(); + pf.addAdvisor(new DefaultPointcutAdvisor(pc, nop)); + proxied = (Person) pf.getProxy(); + } + + + @Test + public void testMatchingOnly() { + // Can't do exact matching through isMatch + assertThat(pc.isMatch("echo", "ech*")).isTrue(); + assertThat(pc.isMatch("setName", "setN*")).isTrue(); + assertThat(pc.isMatch("setName", "set*")).isTrue(); + assertThat(pc.isMatch("getName", "set*")).isFalse(); + assertThat(pc.isMatch("setName", "set")).isFalse(); + assertThat(pc.isMatch("testing", "*ing")).isTrue(); + } + + @Test + public void testEmpty() throws Throwable { + assertThat(nop.getCount()).isEqualTo(0); + proxied.getName(); + proxied.setName(""); + proxied.echo(null); + assertThat(nop.getCount()).isEqualTo(0); + } + + + @Test + public void testMatchOneMethod() throws Throwable { + pc.addMethodName("echo"); + pc.addMethodName("set*"); + assertThat(nop.getCount()).isEqualTo(0); + proxied.getName(); + proxied.getName(); + assertThat(nop.getCount()).isEqualTo(0); + proxied.echo(null); + assertThat(nop.getCount()).isEqualTo(1); + + proxied.setName(""); + assertThat(nop.getCount()).isEqualTo(2); + proxied.setAge(25); + assertThat(proxied.getAge()).isEqualTo(25); + assertThat(nop.getCount()).isEqualTo(3); + } + + @Test + public void testSets() throws Throwable { + pc.setMappedNames("set*", "echo"); + assertThat(nop.getCount()).isEqualTo(0); + proxied.getName(); + proxied.setName(""); + assertThat(nop.getCount()).isEqualTo(1); + proxied.echo(null); + assertThat(nop.getCount()).isEqualTo(2); + } + + @Test + public void testSerializable() throws Throwable { + testSets(); + // Count is now 2 + Person p2 = SerializationTestUtils.serializeAndDeserialize(proxied); + NopInterceptor nop2 = (NopInterceptor) ((Advised) p2).getAdvisors()[0].getAdvice(); + p2.getName(); + assertThat(nop2.getCount()).isEqualTo(2); + p2.echo(null); + assertThat(nop2.getCount()).isEqualTo(3); + } + + @Test + public void testEqualsAndHashCode() { + NameMatchMethodPointcut pc1 = new NameMatchMethodPointcut(); + NameMatchMethodPointcut pc2 = new NameMatchMethodPointcut(); + + String foo = "foo"; + + assertThat(pc2).isEqualTo(pc1); + assertThat(pc2.hashCode()).isEqualTo(pc1.hashCode()); + + pc1.setMappedName(foo); + assertThat(pc1.equals(pc2)).isFalse(); + assertThat(pc1.hashCode() != pc2.hashCode()).isTrue(); + + pc2.setMappedName(foo); + assertThat(pc2).isEqualTo(pc1); + assertThat(pc2.hashCode()).isEqualTo(pc1.hashCode()); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/support/PointcutsTests.java b/spring-aop/src/test/java/org/springframework/aop/support/PointcutsTests.java new file mode 100644 index 0000000..9a6f05e --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/support/PointcutsTests.java @@ -0,0 +1,249 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.ClassFilter; +import org.springframework.aop.Pointcut; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rod Johnson + * @author Chris Beams + */ +public class PointcutsTests { + + public static Method TEST_BEAN_SET_AGE; + public static Method TEST_BEAN_GET_AGE; + public static Method TEST_BEAN_GET_NAME; + public static Method TEST_BEAN_ABSQUATULATE; + + static { + try { + TEST_BEAN_SET_AGE = TestBean.class.getMethod("setAge", int.class); + TEST_BEAN_GET_AGE = TestBean.class.getMethod("getAge"); + TEST_BEAN_GET_NAME = TestBean.class.getMethod("getName"); + TEST_BEAN_ABSQUATULATE = TestBean.class.getMethod("absquatulate"); + } + catch (Exception ex) { + throw new RuntimeException("Shouldn't happen: error in test suite"); + } + } + + /** + * Matches only TestBean class, not subclasses + */ + public static Pointcut allTestBeanMethodsPointcut = new StaticMethodMatcherPointcut() { + @Override + public ClassFilter getClassFilter() { + return type -> type.equals(TestBean.class); + } + + @Override + public boolean matches(Method m, @Nullable Class targetClass) { + return true; + } + }; + + public static Pointcut allClassSetterPointcut = Pointcuts.SETTERS; + + // Subclass used for matching + public static class MyTestBean extends TestBean { + } + + public static Pointcut myTestBeanSetterPointcut = new StaticMethodMatcherPointcut() { + @Override + public ClassFilter getClassFilter() { + return new RootClassFilter(MyTestBean.class); + } + + @Override + public boolean matches(Method m, @Nullable Class targetClass) { + return m.getName().startsWith("set"); + } + }; + + // Will match MyTestBeanSubclass + public static Pointcut myTestBeanGetterPointcut = new StaticMethodMatcherPointcut() { + @Override + public ClassFilter getClassFilter() { + return new RootClassFilter(MyTestBean.class); + } + + @Override + public boolean matches(Method m, @Nullable Class targetClass) { + return m.getName().startsWith("get"); + } + }; + + // Still more specific class + public static class MyTestBeanSubclass extends MyTestBean { + } + + public static Pointcut myTestBeanSubclassGetterPointcut = new StaticMethodMatcherPointcut() { + @Override + public ClassFilter getClassFilter() { + return new RootClassFilter(MyTestBeanSubclass.class); + } + + @Override + public boolean matches(Method m, @Nullable Class targetClass) { + return m.getName().startsWith("get"); + } + }; + + public static Pointcut allClassGetterPointcut = Pointcuts.GETTERS; + + public static Pointcut allClassGetAgePointcut = new NameMatchMethodPointcut().addMethodName("getAge"); + + public static Pointcut allClassGetNamePointcut = new NameMatchMethodPointcut().addMethodName("getName"); + + + @Test + public void testTrue() { + assertThat(Pointcuts.matches(Pointcut.TRUE, TEST_BEAN_SET_AGE, TestBean.class, 6)).isTrue(); + assertThat(Pointcuts.matches(Pointcut.TRUE, TEST_BEAN_GET_AGE, TestBean.class)).isTrue(); + assertThat(Pointcuts.matches(Pointcut.TRUE, TEST_BEAN_ABSQUATULATE, TestBean.class)).isTrue(); + assertThat(Pointcuts.matches(Pointcut.TRUE, TEST_BEAN_SET_AGE, TestBean.class, 6)).isTrue(); + assertThat(Pointcuts.matches(Pointcut.TRUE, TEST_BEAN_GET_AGE, TestBean.class)).isTrue(); + assertThat(Pointcuts.matches(Pointcut.TRUE, TEST_BEAN_ABSQUATULATE, TestBean.class)).isTrue(); + } + + @Test + public void testMatches() { + assertThat(Pointcuts.matches(allClassSetterPointcut, TEST_BEAN_SET_AGE, TestBean.class, 6)).isTrue(); + assertThat(Pointcuts.matches(allClassSetterPointcut, TEST_BEAN_GET_AGE, TestBean.class)).isFalse(); + assertThat(Pointcuts.matches(allClassSetterPointcut, TEST_BEAN_ABSQUATULATE, TestBean.class)).isFalse(); + assertThat(Pointcuts.matches(allClassGetterPointcut, TEST_BEAN_SET_AGE, TestBean.class, 6)).isFalse(); + assertThat(Pointcuts.matches(allClassGetterPointcut, TEST_BEAN_GET_AGE, TestBean.class)).isTrue(); + assertThat(Pointcuts.matches(allClassGetterPointcut, TEST_BEAN_ABSQUATULATE, TestBean.class)).isFalse(); + } + + /** + * Should match all setters and getters on any class + */ + @Test + public void testUnionOfSettersAndGetters() { + Pointcut union = Pointcuts.union(allClassGetterPointcut, allClassSetterPointcut); + assertThat(Pointcuts.matches(union, TEST_BEAN_SET_AGE, TestBean.class, 6)).isTrue(); + assertThat(Pointcuts.matches(union, TEST_BEAN_GET_AGE, TestBean.class)).isTrue(); + assertThat(Pointcuts.matches(union, TEST_BEAN_ABSQUATULATE, TestBean.class)).isFalse(); + } + + @Test + public void testUnionOfSpecificGetters() { + Pointcut union = Pointcuts.union(allClassGetAgePointcut, allClassGetNamePointcut); + assertThat(Pointcuts.matches(union, TEST_BEAN_SET_AGE, TestBean.class, 6)).isFalse(); + assertThat(Pointcuts.matches(union, TEST_BEAN_GET_AGE, TestBean.class)).isTrue(); + assertThat(Pointcuts.matches(allClassGetAgePointcut, TEST_BEAN_GET_NAME, TestBean.class)).isFalse(); + assertThat(Pointcuts.matches(union, TEST_BEAN_GET_NAME, TestBean.class)).isTrue(); + assertThat(Pointcuts.matches(union, TEST_BEAN_ABSQUATULATE, TestBean.class)).isFalse(); + + // Union with all setters + union = Pointcuts.union(union, allClassSetterPointcut); + assertThat(Pointcuts.matches(union, TEST_BEAN_SET_AGE, TestBean.class, 6)).isTrue(); + assertThat(Pointcuts.matches(union, TEST_BEAN_GET_AGE, TestBean.class)).isTrue(); + assertThat(Pointcuts.matches(allClassGetAgePointcut, TEST_BEAN_GET_NAME, TestBean.class)).isFalse(); + assertThat(Pointcuts.matches(union, TEST_BEAN_GET_NAME, TestBean.class)).isTrue(); + assertThat(Pointcuts.matches(union, TEST_BEAN_ABSQUATULATE, TestBean.class)).isFalse(); + + assertThat(Pointcuts.matches(union, TEST_BEAN_SET_AGE, TestBean.class, 6)).isTrue(); + } + + /** + * Tests vertical composition. First pointcut matches all setters. + * Second one matches all getters in the MyTestBean class. TestBean getters shouldn't pass. + */ + @Test + public void testUnionOfAllSettersAndSubclassSetters() { + assertThat(Pointcuts.matches(myTestBeanSetterPointcut, TEST_BEAN_SET_AGE, TestBean.class, 6)).isFalse(); + assertThat(Pointcuts.matches(myTestBeanSetterPointcut, TEST_BEAN_SET_AGE, MyTestBean.class, 6)).isTrue(); + assertThat(Pointcuts.matches(myTestBeanSetterPointcut, TEST_BEAN_GET_AGE, TestBean.class)).isFalse(); + + Pointcut union = Pointcuts.union(myTestBeanSetterPointcut, allClassGetterPointcut); + assertThat(Pointcuts.matches(union, TEST_BEAN_GET_AGE, TestBean.class)).isTrue(); + assertThat(Pointcuts.matches(union, TEST_BEAN_GET_AGE, MyTestBean.class)).isTrue(); + // Still doesn't match superclass setter + assertThat(Pointcuts.matches(union, TEST_BEAN_SET_AGE, MyTestBean.class, 6)).isTrue(); + assertThat(Pointcuts.matches(union, TEST_BEAN_SET_AGE, TestBean.class, 6)).isFalse(); + } + + /** + * Intersection should be MyTestBean getAge() only: + * it's the union of allClassGetAge and subclass getters + */ + @Test + public void testIntersectionOfSpecificGettersAndSubclassGetters() { + assertThat(Pointcuts.matches(allClassGetAgePointcut, TEST_BEAN_GET_AGE, TestBean.class)).isTrue(); + assertThat(Pointcuts.matches(allClassGetAgePointcut, TEST_BEAN_GET_AGE, MyTestBean.class)).isTrue(); + assertThat(Pointcuts.matches(myTestBeanGetterPointcut, TEST_BEAN_GET_NAME, TestBean.class)).isFalse(); + assertThat(Pointcuts.matches(myTestBeanGetterPointcut, TEST_BEAN_GET_AGE, TestBean.class)).isFalse(); + assertThat(Pointcuts.matches(myTestBeanGetterPointcut, TEST_BEAN_GET_NAME, MyTestBean.class)).isTrue(); + assertThat(Pointcuts.matches(myTestBeanGetterPointcut, TEST_BEAN_GET_AGE, MyTestBean.class)).isTrue(); + + Pointcut intersection = Pointcuts.intersection(allClassGetAgePointcut, myTestBeanGetterPointcut); + assertThat(Pointcuts.matches(intersection, TEST_BEAN_GET_NAME, TestBean.class)).isFalse(); + assertThat(Pointcuts.matches(intersection, TEST_BEAN_GET_AGE, TestBean.class)).isFalse(); + assertThat(Pointcuts.matches(intersection, TEST_BEAN_GET_NAME, MyTestBean.class)).isFalse(); + assertThat(Pointcuts.matches(intersection, TEST_BEAN_GET_AGE, MyTestBean.class)).isTrue(); + // Matches subclass of MyTestBean + assertThat(Pointcuts.matches(intersection, TEST_BEAN_GET_NAME, MyTestBeanSubclass.class)).isFalse(); + assertThat(Pointcuts.matches(intersection, TEST_BEAN_GET_AGE, MyTestBeanSubclass.class)).isTrue(); + + // Now intersection with MyTestBeanSubclass getters should eliminate MyTestBean target + intersection = Pointcuts.intersection(intersection, myTestBeanSubclassGetterPointcut); + assertThat(Pointcuts.matches(intersection, TEST_BEAN_GET_NAME, TestBean.class)).isFalse(); + assertThat(Pointcuts.matches(intersection, TEST_BEAN_GET_AGE, TestBean.class)).isFalse(); + assertThat(Pointcuts.matches(intersection, TEST_BEAN_GET_NAME, MyTestBean.class)).isFalse(); + assertThat(Pointcuts.matches(intersection, TEST_BEAN_GET_AGE, MyTestBean.class)).isFalse(); + // Still matches subclass of MyTestBean + assertThat(Pointcuts.matches(intersection, TEST_BEAN_GET_NAME, MyTestBeanSubclass.class)).isFalse(); + assertThat(Pointcuts.matches(intersection, TEST_BEAN_GET_AGE, MyTestBeanSubclass.class)).isTrue(); + + // Now union with all TestBean methods + Pointcut union = Pointcuts.union(intersection, allTestBeanMethodsPointcut); + assertThat(Pointcuts.matches(union, TEST_BEAN_GET_NAME, TestBean.class)).isTrue(); + assertThat(Pointcuts.matches(union, TEST_BEAN_GET_AGE, TestBean.class)).isTrue(); + assertThat(Pointcuts.matches(union, TEST_BEAN_GET_NAME, MyTestBean.class)).isFalse(); + assertThat(Pointcuts.matches(union, TEST_BEAN_GET_AGE, MyTestBean.class)).isFalse(); + // Still matches subclass of MyTestBean + assertThat(Pointcuts.matches(union, TEST_BEAN_GET_NAME, MyTestBeanSubclass.class)).isFalse(); + assertThat(Pointcuts.matches(union, TEST_BEAN_GET_AGE, MyTestBeanSubclass.class)).isTrue(); + + assertThat(Pointcuts.matches(union, TEST_BEAN_ABSQUATULATE, TestBean.class)).isTrue(); + assertThat(Pointcuts.matches(union, TEST_BEAN_ABSQUATULATE, MyTestBean.class)).isFalse(); + } + + + /** + * The intersection of these two pointcuts leaves nothing. + */ + @Test + public void testSimpleIntersection() { + Pointcut intersection = Pointcuts.intersection(allClassGetterPointcut, allClassSetterPointcut); + assertThat(Pointcuts.matches(intersection, TEST_BEAN_SET_AGE, TestBean.class, 6)).isFalse(); + assertThat(Pointcuts.matches(intersection, TEST_BEAN_GET_AGE, TestBean.class)).isFalse(); + assertThat(Pointcuts.matches(intersection, TEST_BEAN_ABSQUATULATE, TestBean.class)).isFalse(); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/support/RegexpMethodPointcutAdvisorIntegrationTests.java b/spring-aop/src/test/java/org/springframework/aop/support/RegexpMethodPointcutAdvisorIntegrationTests.java new file mode 100644 index 0000000..c3b546c --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/support/RegexpMethodPointcutAdvisorIntegrationTests.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.Advised; +import org.springframework.aop.testfixture.interceptor.NopInterceptor; +import org.springframework.aop.testfixture.interceptor.SerializableNopInterceptor; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.Person; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.io.Resource; +import org.springframework.core.testfixture.io.SerializationTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.core.testfixture.io.ResourceTestUtils.qualifiedResource; + +/** + * @author Rod Johnson + * @author Chris Beams + */ +public class RegexpMethodPointcutAdvisorIntegrationTests { + + private static final Resource CONTEXT = + qualifiedResource(RegexpMethodPointcutAdvisorIntegrationTests.class, "context.xml"); + + + @Test + public void testSinglePattern() throws Throwable { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions(CONTEXT); + ITestBean advised = (ITestBean) bf.getBean("settersAdvised"); + // Interceptor behind regexp advisor + NopInterceptor nop = (NopInterceptor) bf.getBean("nopInterceptor"); + assertThat(nop.getCount()).isEqualTo(0); + + int newAge = 12; + // Not advised + advised.exceptional(null); + assertThat(nop.getCount()).isEqualTo(0); + advised.setAge(newAge); + assertThat(advised.getAge()).isEqualTo(newAge); + // Only setter fired + assertThat(nop.getCount()).isEqualTo(1); + } + + @Test + public void testMultiplePatterns() throws Throwable { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions(CONTEXT); + // This is a CGLIB proxy, so we can proxy it to the target class + TestBean advised = (TestBean) bf.getBean("settersAndAbsquatulateAdvised"); + // Interceptor behind regexp advisor + NopInterceptor nop = (NopInterceptor) bf.getBean("nopInterceptor"); + assertThat(nop.getCount()).isEqualTo(0); + + int newAge = 12; + // Not advised + advised.exceptional(null); + assertThat(nop.getCount()).isEqualTo(0); + + // This is proxied + advised.absquatulate(); + assertThat(nop.getCount()).isEqualTo(1); + advised.setAge(newAge); + assertThat(advised.getAge()).isEqualTo(newAge); + // Only setter fired + assertThat(nop.getCount()).isEqualTo(2); + } + + @Test + public void testSerialization() throws Throwable { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions(CONTEXT); + // This is a CGLIB proxy, so we can proxy it to the target class + Person p = (Person) bf.getBean("serializableSettersAdvised"); + // Interceptor behind regexp advisor + NopInterceptor nop = (NopInterceptor) bf.getBean("nopInterceptor"); + assertThat(nop.getCount()).isEqualTo(0); + + int newAge = 12; + // Not advised + assertThat(p.getAge()).isEqualTo(0); + assertThat(nop.getCount()).isEqualTo(0); + + // This is proxied + p.setAge(newAge); + assertThat(nop.getCount()).isEqualTo(1); + p.setAge(newAge); + assertThat(p.getAge()).isEqualTo(newAge); + // Only setter fired + assertThat(nop.getCount()).isEqualTo(2); + + // Serialize and continue... + p = SerializationTestUtils.serializeAndDeserialize(p); + assertThat(p.getAge()).isEqualTo(newAge); + // Remembers count, but we need to get a new reference to nop... + nop = (SerializableNopInterceptor) ((Advised) p).getAdvisors()[0].getAdvice(); + assertThat(nop.getCount()).isEqualTo(2); + assertThat(p.getName()).isEqualTo("serializableSettersAdvised"); + p.setAge(newAge + 1); + assertThat(nop.getCount()).isEqualTo(3); + assertThat(p.getAge()).isEqualTo((newAge + 1)); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/support/RootClassFilterTests.java b/spring-aop/src/test/java/org/springframework/aop/support/RootClassFilterTests.java new file mode 100644 index 0000000..e09344b --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/support/RootClassFilterTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.ClassFilter; +import org.springframework.beans.testfixture.beans.ITestBean; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link RootClassFilter}. + * + * @author Sam Brannen + * @since 5.1.10 + */ +class RootClassFilterTests { + + private final ClassFilter filter1 = new RootClassFilter(Exception.class); + private final ClassFilter filter2 = new RootClassFilter(Exception.class); + private final ClassFilter filter3 = new RootClassFilter(ITestBean.class); + + + @Test + void matches() { + assertThat(filter1.matches(Exception.class)).isTrue(); + assertThat(filter1.matches(RuntimeException.class)).isTrue(); + assertThat(filter1.matches(Error.class)).isFalse(); + } + + @Test + void testEquals() { + assertThat(filter1).isEqualTo(filter2); + assertThat(filter1).isNotEqualTo(filter3); + } + + @Test + void testHashCode() { + assertThat(filter1.hashCode()).isEqualTo(filter2.hashCode()); + assertThat(filter1.hashCode()).isNotEqualTo(filter3.hashCode()); + } + + @Test + void testToString() { + assertThat(filter1.toString()).isEqualTo("org.springframework.aop.support.RootClassFilter: java.lang.Exception"); + assertThat(filter1.toString()).isEqualTo(filter2.toString()); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/support/annotation/AnnotationMatchingPointcutTests.java b/spring-aop/src/test/java/org/springframework/aop/support/annotation/AnnotationMatchingPointcutTests.java new file mode 100644 index 0000000..4ccbc02 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/support/annotation/AnnotationMatchingPointcutTests.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.support.annotation; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.MethodMatcher; +import org.springframework.aop.Pointcut; +import org.springframework.beans.factory.annotation.Qualifier; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link AnnotationMatchingPointcut}. + * + * @author Sam Brannen + * @since 5.1.10 + */ +class AnnotationMatchingPointcutTests { + + @Test + void classLevelPointcuts() { + Pointcut pointcut1 = new AnnotationMatchingPointcut(Qualifier.class, true); + Pointcut pointcut2 = new AnnotationMatchingPointcut(Qualifier.class, true); + Pointcut pointcut3 = new AnnotationMatchingPointcut(Qualifier.class); + + assertThat(pointcut1.getClassFilter().getClass()).isEqualTo(AnnotationClassFilter.class); + assertThat(pointcut2.getClassFilter().getClass()).isEqualTo(AnnotationClassFilter.class); + assertThat(pointcut3.getClassFilter().getClass()).isEqualTo(AnnotationClassFilter.class); + assertThat(pointcut1.getClassFilter().toString()).contains(Qualifier.class.getName()); + + assertThat(pointcut1.getMethodMatcher()).isEqualTo(MethodMatcher.TRUE); + assertThat(pointcut2.getMethodMatcher()).isEqualTo(MethodMatcher.TRUE); + assertThat(pointcut3.getMethodMatcher()).isEqualTo(MethodMatcher.TRUE); + + assertThat(pointcut1).isEqualTo(pointcut2); + assertThat(pointcut1).isNotEqualTo(pointcut3); + assertThat(pointcut1.hashCode()).isEqualTo(pointcut2.hashCode()); + // #1 and #3 have equivalent hash codes even though equals() returns false. + assertThat(pointcut1.hashCode()).isEqualTo(pointcut3.hashCode()); + assertThat(pointcut1.toString()).isEqualTo(pointcut2.toString()); + } + + @Test + void methodLevelPointcuts() { + Pointcut pointcut1 = new AnnotationMatchingPointcut(null, Qualifier.class, true); + Pointcut pointcut2 = new AnnotationMatchingPointcut(null, Qualifier.class, true); + Pointcut pointcut3 = new AnnotationMatchingPointcut(null, Qualifier.class); + + assertThat(pointcut1.getClassFilter().getClass().getSimpleName()).isEqualTo("AnnotationCandidateClassFilter"); + assertThat(pointcut2.getClassFilter().getClass().getSimpleName()).isEqualTo("AnnotationCandidateClassFilter"); + assertThat(pointcut3.getClassFilter().getClass().getSimpleName()).isEqualTo("AnnotationCandidateClassFilter"); + assertThat(pointcut1.getClassFilter().toString()).contains(Qualifier.class.getName()); + + assertThat(pointcut1.getMethodMatcher().getClass()).isEqualTo(AnnotationMethodMatcher.class); + assertThat(pointcut2.getMethodMatcher().getClass()).isEqualTo(AnnotationMethodMatcher.class); + assertThat(pointcut3.getMethodMatcher().getClass()).isEqualTo(AnnotationMethodMatcher.class); + + assertThat(pointcut1).isEqualTo(pointcut2); + assertThat(pointcut1).isNotEqualTo(pointcut3); + assertThat(pointcut1.hashCode()).isEqualTo(pointcut2.hashCode()); + // #1 and #3 have equivalent hash codes even though equals() returns false. + assertThat(pointcut1.hashCode()).isEqualTo(pointcut3.hashCode()); + assertThat(pointcut1.toString()).isEqualTo(pointcut2.toString()); + } + + @Test + void classLevelAndMethodLevelPointcuts() { + Pointcut pointcut1 = new AnnotationMatchingPointcut(Qualifier.class, Qualifier.class, true); + Pointcut pointcut2 = new AnnotationMatchingPointcut(Qualifier.class, Qualifier.class, true); + Pointcut pointcut3 = new AnnotationMatchingPointcut(Qualifier.class, Qualifier.class); + + assertThat(pointcut1.getClassFilter().getClass()).isEqualTo(AnnotationClassFilter.class); + assertThat(pointcut2.getClassFilter().getClass()).isEqualTo(AnnotationClassFilter.class); + assertThat(pointcut3.getClassFilter().getClass()).isEqualTo(AnnotationClassFilter.class); + assertThat(pointcut1.getClassFilter().toString()).contains(Qualifier.class.getName()); + + assertThat(pointcut1.getMethodMatcher().getClass()).isEqualTo(AnnotationMethodMatcher.class); + assertThat(pointcut2.getMethodMatcher().getClass()).isEqualTo(AnnotationMethodMatcher.class); + assertThat(pointcut3.getMethodMatcher().getClass()).isEqualTo(AnnotationMethodMatcher.class); + assertThat(pointcut1.getMethodMatcher().toString()).contains(Qualifier.class.getName()); + + assertThat(pointcut1).isEqualTo(pointcut2); + assertThat(pointcut1).isNotEqualTo(pointcut3); + assertThat(pointcut1.hashCode()).isEqualTo(pointcut2.hashCode()); + // #1 and #3 have equivalent hash codes even though equals() returns false. + assertThat(pointcut1.hashCode()).isEqualTo(pointcut3.hashCode()); + assertThat(pointcut1.toString()).isEqualTo(pointcut2.toString()); + } + +} diff --git a/spring-aop/src/test/java/test/annotation/EmptySpringAnnotation.java b/spring-aop/src/test/java/test/annotation/EmptySpringAnnotation.java new file mode 100644 index 0000000..fcb55a8 --- /dev/null +++ b/spring-aop/src/test/java/test/annotation/EmptySpringAnnotation.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2006 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. + * You may obtain a copy of the License at + * + * https://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 test.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface EmptySpringAnnotation { + +} diff --git a/spring-aop/src/test/java/test/annotation/transaction/Tx.java b/spring-aop/src/test/java/test/annotation/transaction/Tx.java new file mode 100644 index 0000000..bf7c9da --- /dev/null +++ b/spring-aop/src/test/java/test/annotation/transaction/Tx.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2006 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. + * You may obtain a copy of the License at + * + * https://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 test.annotation.transaction; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface Tx { +} diff --git a/spring-aop/src/test/java/test/aop/DefaultLockable.java b/spring-aop/src/test/java/test/aop/DefaultLockable.java new file mode 100644 index 0000000..1e9499d --- /dev/null +++ b/spring-aop/src/test/java/test/aop/DefaultLockable.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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 test.aop; + +/** + * Simple implementation of Lockable interface for use in mixins. + * + * @author Rod Johnson + */ +public class DefaultLockable implements Lockable { + + private boolean locked; + + @Override + public void lock() { + this.locked = true; + } + + @Override + public void unlock() { + this.locked = false; + } + + @Override + public boolean locked() { + return this.locked; + } + +} diff --git a/spring-aop/src/test/java/test/aop/Lockable.java b/spring-aop/src/test/java/test/aop/Lockable.java new file mode 100644 index 0000000..7e9058d --- /dev/null +++ b/spring-aop/src/test/java/test/aop/Lockable.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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 test.aop; + + +/** + * Simple interface to use for mixins + * + * @author Rod Johnson + * + */ +public interface Lockable { + + void lock(); + + void unlock(); + + boolean locked(); +} diff --git a/spring-aop/src/test/java/test/aop/PerTargetAspect.java b/spring-aop/src/test/java/test/aop/PerTargetAspect.java new file mode 100644 index 0000000..4dd5c29 --- /dev/null +++ b/spring-aop/src/test/java/test/aop/PerTargetAspect.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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 test.aop; + +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; + +import org.springframework.core.Ordered; + +@Aspect("pertarget(execution(* *.getSpouse()))") +public class PerTargetAspect implements Ordered { + + public int count; + + private int order = Ordered.LOWEST_PRECEDENCE; + + @Around("execution(int *.getAge())") + public int returnCountAsAge() { + return count++; + } + + @Before("execution(void *.set*(int))") + public void countSetter() { + ++count; + } + + @Override + public int getOrder() { + return this.order; + } + + public void setOrder(int order) { + this.order = order; + } + +} diff --git a/spring-aop/src/test/java/test/aop/PerThisAspect.java b/spring-aop/src/test/java/test/aop/PerThisAspect.java new file mode 100644 index 0000000..f6b9a3d --- /dev/null +++ b/spring-aop/src/test/java/test/aop/PerThisAspect.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2005 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. + * You may obtain a copy of the License at + * + * https://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 test.aop; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; + +@Aspect("perthis(execution(* getAge()))") +public class PerThisAspect { + + private int invocations = 0; + + public int getInvocations() { + return this.invocations; + } + + @Around("execution(* getAge())") + public int changeAge(ProceedingJoinPoint pjp) throws Throwable { + return invocations++; + } + +} diff --git a/spring-aop/src/test/java/test/aop/TwoAdviceAspect.java b/spring-aop/src/test/java/test/aop/TwoAdviceAspect.java new file mode 100644 index 0000000..f77ad9a --- /dev/null +++ b/spring-aop/src/test/java/test/aop/TwoAdviceAspect.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2008 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. + * You may obtain a copy of the License at + * + * https://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 test.aop; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; + +@Aspect +public class TwoAdviceAspect { + private int totalCalls; + + @Around("execution(* getAge())") + public int returnCallCount(ProceedingJoinPoint pjp) throws Exception { + return totalCalls; + } + + @Before("execution(* setAge(int)) && args(newAge)") + public void countSet(int newAge) throws Exception { + ++totalCalls; + } +} diff --git a/spring-aop/src/test/resources/log4j2-test.xml b/spring-aop/src/test/resources/log4j2-test.xml new file mode 100644 index 0000000..521cc44 --- /dev/null +++ b/spring-aop/src/test/resources/log4j2-test.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerEventTests-context.xml b/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerEventTests-context.xml new file mode 100644 index 0000000..64f222c --- /dev/null +++ b/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerEventTests-context.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerEventTests-directPointcutEvents.xml b/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerEventTests-directPointcutEvents.xml new file mode 100644 index 0000000..852f747 --- /dev/null +++ b/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerEventTests-directPointcutEvents.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerEventTests-pointcutEvents.xml b/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerEventTests-pointcutEvents.xml new file mode 100644 index 0000000..ed04cb4 --- /dev/null +++ b/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerEventTests-pointcutEvents.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerEventTests-pointcutRefEvents.xml b/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerEventTests-pointcutRefEvents.xml new file mode 100644 index 0000000..8300b28 --- /dev/null +++ b/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerEventTests-pointcutRefEvents.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerPointcutErrorTests-pointcutDuplication.xml b/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerPointcutErrorTests-pointcutDuplication.xml new file mode 100644 index 0000000..33693b7 --- /dev/null +++ b/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerPointcutErrorTests-pointcutDuplication.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + diff --git a/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerPointcutErrorTests-pointcutMissing.xml b/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerPointcutErrorTests-pointcutMissing.xml new file mode 100644 index 0000000..3d20378 --- /dev/null +++ b/spring-aop/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerPointcutErrorTests-pointcutMissing.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + diff --git a/spring-aop/src/test/resources/org/springframework/aop/config/TopLevelAopTagTests-context.xml b/spring-aop/src/test/resources/org/springframework/aop/config/TopLevelAopTagTests-context.xml new file mode 100644 index 0000000..4a1cd95 --- /dev/null +++ b/spring-aop/src/test/resources/org/springframework/aop/config/TopLevelAopTagTests-context.xml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/spring-aop/src/test/resources/org/springframework/aop/framework/PrototypeTargetTests-context.xml b/spring-aop/src/test/resources/org/springframework/aop/framework/PrototypeTargetTests-context.xml new file mode 100644 index 0000000..89b8d26 --- /dev/null +++ b/spring-aop/src/test/resources/org/springframework/aop/framework/PrototypeTargetTests-context.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + org.springframework.aop.framework.PrototypeTargetTests$TestBean + + + false + + + + testInterceptor + testBeanTarget + + + + + + + org.springframework.aop.framework.PrototypeTargetTests$TestBean + + + true + + + + testInterceptor + testBeanTarget + + + + + diff --git a/spring-aop/src/test/resources/org/springframework/aop/interceptor/ExposeInvocationInterceptorTests-context.xml b/spring-aop/src/test/resources/org/springframework/aop/interceptor/ExposeInvocationInterceptorTests-context.xml new file mode 100644 index 0000000..a5bf0f3 --- /dev/null +++ b/spring-aop/src/test/resources/org/springframework/aop/interceptor/ExposeInvocationInterceptorTests-context.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + org.springframework.aop.interceptor.ExposeInvocationInterceptor + + INSTANCE + + + + + + + + + + exposeInvocation,countingBeforeAdvice,nopInterceptor + + + + diff --git a/spring-aop/src/test/resources/org/springframework/aop/scope/ScopedProxyAutowireTests-scopedAutowireFalse.xml b/spring-aop/src/test/resources/org/springframework/aop/scope/ScopedProxyAutowireTests-scopedAutowireFalse.xml new file mode 100644 index 0000000..57101c4 --- /dev/null +++ b/spring-aop/src/test/resources/org/springframework/aop/scope/ScopedProxyAutowireTests-scopedAutowireFalse.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/spring-aop/src/test/resources/org/springframework/aop/scope/ScopedProxyAutowireTests-scopedAutowireTrue.xml b/spring-aop/src/test/resources/org/springframework/aop/scope/ScopedProxyAutowireTests-scopedAutowireTrue.xml new file mode 100644 index 0000000..b8993d5 --- /dev/null +++ b/spring-aop/src/test/resources/org/springframework/aop/scope/ScopedProxyAutowireTests-scopedAutowireTrue.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/spring-aop/src/test/resources/org/springframework/aop/support/RegexpMethodPointcutAdvisorIntegrationTests-context.xml b/spring-aop/src/test/resources/org/springframework/aop/support/RegexpMethodPointcutAdvisorIntegrationTests-context.xml new file mode 100644 index 0000000..967e42b --- /dev/null +++ b/spring-aop/src/test/resources/org/springframework/aop/support/RegexpMethodPointcutAdvisorIntegrationTests-context.xml @@ -0,0 +1,58 @@ + + + + + + + + custom + 666 + + + + + + + + + .*set.* + + + + + + org.springframework.beans.testfixture.beans.ITestBean + + settersAdvisor + + + + org.springframework.beans.testfixture.beans.Person + + + serializableSettersAdvised + + + settersAdvisor + + + + + + + + .*get.* + .*absquatulate + + + + + + org.springframework.beans.testfixture.beans.ITestBean + + true + + settersAndAbsquatulateAdvisor + + + diff --git a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/CountingAfterReturningAdvice.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/CountingAfterReturningAdvice.java new file mode 100644 index 0000000..8cfab3c --- /dev/null +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/CountingAfterReturningAdvice.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.testfixture.advice; + +import java.lang.reflect.Method; + +import org.springframework.aop.AfterReturningAdvice; + +/** + * Simple before advice example that we can use for counting checks. + * + * @author Rod Johnson + */ +@SuppressWarnings("serial") +public class CountingAfterReturningAdvice extends MethodCounter implements AfterReturningAdvice { + + @Override + public void afterReturning(Object o, Method m, Object[] args, Object target) throws Throwable { + count(m); + } + +} diff --git a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/CountingBeforeAdvice.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/CountingBeforeAdvice.java new file mode 100644 index 0000000..bf931f6 --- /dev/null +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/CountingBeforeAdvice.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.testfixture.advice; + +import java.lang.reflect.Method; + +import org.springframework.aop.MethodBeforeAdvice; + +/** + * Simple before advice example that we can use for counting checks. + * + * @author Rod Johnson + */ +@SuppressWarnings("serial") +public class CountingBeforeAdvice extends MethodCounter implements MethodBeforeAdvice { + + @Override + public void before(Method m, Object[] args, Object target) throws Throwable { + count(m); + } + +} diff --git a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/MethodCounter.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/MethodCounter.java new file mode 100644 index 0000000..384973b --- /dev/null +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/MethodCounter.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.testfixture.advice; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.HashMap; + +/** + * Abstract superclass for counting advices etc. + * + * @author Rod Johnson + * @author Chris Beams + * @author Sam Brannen + */ +@SuppressWarnings("serial") +public class MethodCounter implements Serializable { + + /** Method name --> count, does not understand overloading */ + private HashMap map = new HashMap<>(); + + private int allCount; + + protected void count(Method m) { + count(m.getName()); + } + + protected void count(String methodName) { + map.merge(methodName, 1, (n, m) -> n + 1); + ++allCount; + } + + public int getCalls(String methodName) { + return map.getOrDefault(methodName, 0); + } + + public int getCalls() { + return allCount; + } + + /** + * A bit simplistic: just wants the same class. + * Doesn't worry about counts. + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object other) { + return (other != null && other.getClass() == this.getClass()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + +} diff --git a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/MyThrowsHandler.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/MyThrowsHandler.java new file mode 100644 index 0000000..606101a --- /dev/null +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/MyThrowsHandler.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.testfixture.advice; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.rmi.RemoteException; + +import org.springframework.aop.ThrowsAdvice; + +@SuppressWarnings("serial") +public class MyThrowsHandler extends MethodCounter implements ThrowsAdvice { + + // Full method signature + public void afterThrowing(Method m, Object[] args, Object target, IOException ex) { + count("ioException"); + } + + public void afterThrowing(RemoteException ex) throws Throwable { + count("remoteException"); + } + + /** Not valid, wrong number of arguments */ + public void afterThrowing(Method m, Exception ex) throws Throwable { + throw new UnsupportedOperationException("Shouldn't be called"); + } + +} diff --git a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/TimestampIntroductionAdvisor.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/TimestampIntroductionAdvisor.java new file mode 100644 index 0000000..5321e5d --- /dev/null +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/advice/TimestampIntroductionAdvisor.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.testfixture.advice; + +import org.springframework.aop.support.DefaultIntroductionAdvisor; +import org.springframework.aop.support.DelegatingIntroductionInterceptor; +import org.springframework.aop.testfixture.interceptor.TimestampIntroductionInterceptor; + +/** + * @author Rod Johnson + */ +@SuppressWarnings("serial") +public class TimestampIntroductionAdvisor extends DefaultIntroductionAdvisor { + + public TimestampIntroductionAdvisor() { + super(new DelegatingIntroductionInterceptor(new TimestampIntroductionInterceptor())); + } + +} diff --git a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/interceptor/NopInterceptor.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/interceptor/NopInterceptor.java new file mode 100644 index 0000000..dd44324 --- /dev/null +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/interceptor/NopInterceptor.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.testfixture.interceptor; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +/** + * Trivial interceptor that can be introduced in a chain to display it. + * + * @author Rod Johnson + */ +public class NopInterceptor implements MethodInterceptor { + + private int count; + + + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + increment(); + return invocation.proceed(); + } + + protected void increment() { + this.count++; + } + + public int getCount() { + return this.count; + } + + + @Override + public boolean equals(Object other) { + if (!(other instanceof NopInterceptor)) { + return false; + } + if (this == other) { + return true; + } + return this.count == ((NopInterceptor) other).count; + } + + @Override + public int hashCode() { + return NopInterceptor.class.hashCode(); + } + +} diff --git a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/interceptor/SerializableNopInterceptor.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/interceptor/SerializableNopInterceptor.java new file mode 100644 index 0000000..25d9546 --- /dev/null +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/interceptor/SerializableNopInterceptor.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.testfixture.interceptor; + +import java.io.Serializable; + +/** + * Subclass of NopInterceptor that is serializable and + * can be used to test proxy serialization. + * + * @author Rod Johnson + */ +@SuppressWarnings("serial") +public class SerializableNopInterceptor extends NopInterceptor implements Serializable { + + /** + * We must override this field and the related methods as + * otherwise count won't be serialized from the non-serializable + * NopInterceptor superclass. + */ + private int count; + + @Override + public int getCount() { + return this.count; + } + + @Override + protected void increment() { + ++count; + } + +} diff --git a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/interceptor/TimestampIntroductionInterceptor.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/interceptor/TimestampIntroductionInterceptor.java new file mode 100644 index 0000000..2476135 --- /dev/null +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/interceptor/TimestampIntroductionInterceptor.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.testfixture.interceptor; + +import org.springframework.aop.support.DelegatingIntroductionInterceptor; +import org.springframework.core.testfixture.TimeStamped; + +@SuppressWarnings("serial") +public class TimestampIntroductionInterceptor extends DelegatingIntroductionInterceptor + implements TimeStamped { + + private long ts; + + public TimestampIntroductionInterceptor() { + } + + public TimestampIntroductionInterceptor(long ts) { + this.ts = ts; + } + + public void setTime(long ts) { + this.ts = ts; + } + + @Override + public long getTimeStamp() { + return ts; + } + +} diff --git a/spring-aspects/spring-aspects.gradle b/spring-aspects/spring-aspects.gradle new file mode 100644 index 0000000..12adbfb --- /dev/null +++ b/spring-aspects/spring-aspects.gradle @@ -0,0 +1,37 @@ +description = "Spring Aspects" + +apply plugin: "io.freefair.aspectj" + +sourceSets.main.aspectj.srcDir "src/main/java" +sourceSets.main.java.srcDirs = files() + +sourceSets.test.aspectj.srcDir "src/test/java" +sourceSets.test.java.srcDirs = files() + +aspectj.version = dependencyManagement.managedVersions['org.aspectj:aspectjweaver'] + +dependencies { + compile("org.aspectj:aspectjweaver") + compileOnly("org.aspectj:aspectjrt") + optional(project(":spring-aop")) // for @Async support + optional(project(":spring-beans")) // for @Configurable support + optional(project(":spring-context")) // for @Enable* support + optional(project(":spring-context-support")) // for JavaMail and JSR-107 support + optional(project(":spring-orm")) // for JPA exception translation support + optional(project(":spring-tx")) // for JPA, @Transactional support + optional("javax.cache:cache-api") // for JCache aspect + optional("javax.transaction:javax.transaction-api") // for @javax.transaction.Transactional support + testCompile(project(":spring-core")) // for CodeStyleAspect + testCompile(project(":spring-test")) + testCompile(testFixtures(project(":spring-context"))) + testCompile(testFixtures(project(":spring-context-support"))) + testCompile(testFixtures(project(":spring-core"))) + testCompile(testFixtures(project(":spring-tx"))) + testCompile("javax.mail:javax.mail-api") + testCompileOnly("org.aspectj:aspectjrt") +} + +eclipse.project { + natures += "org.eclipse.ajdt.ui.ajnature" + buildCommands = [new org.gradle.plugins.ide.eclipse.model.BuildCommand("org.eclipse.ajdt.core.ajbuilder")] +} diff --git a/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/AbstractDependencyInjectionAspect.aj b/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/AbstractDependencyInjectionAspect.aj new file mode 100644 index 0000000..94f9f8c --- /dev/null +++ b/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/AbstractDependencyInjectionAspect.aj @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.aspectj; + +import org.aspectj.lang.annotation.SuppressAjWarnings; +import org.aspectj.lang.annotation.control.CodeGenerationHint; + +/** + * Abstract base aspect that can perform Dependency + * Injection on objects, however they may be created. + * + * @author Ramnivas Laddad + * @since 2.5.2 + */ +public abstract aspect AbstractDependencyInjectionAspect { + + private pointcut preConstructionCondition() : + leastSpecificSuperTypeConstruction() && preConstructionConfiguration(); + + private pointcut postConstructionCondition() : + mostSpecificSubTypeConstruction() && !preConstructionConfiguration(); + + /** + * Select least specific super type that is marked for DI + * (so that injection occurs only once with pre-construction injection). + */ + public abstract pointcut leastSpecificSuperTypeConstruction(); + + /** + * Select the most-specific initialization join point + * (most concrete class) for the initialization of an instance. + */ + @CodeGenerationHint(ifNameSuffix="6f1") + public pointcut mostSpecificSubTypeConstruction() : + if (thisJoinPoint.getSignature().getDeclaringType() == thisJoinPoint.getThis().getClass()); + + /** + * Select join points in beans to be configured prior to construction? + * By default, use post-construction injection matching the default in the Configurable annotation. + */ + public pointcut preConstructionConfiguration() : if (false); + + /** + * Select construction join points for objects to inject dependencies. + */ + public abstract pointcut beanConstruction(Object bean); + + /** + * Select deserialization join points for objects to inject dependencies. + */ + public abstract pointcut beanDeserialization(Object bean); + + /** + * Select join points in a configurable bean. + */ + public abstract pointcut inConfigurableBean(); + + + /** + * Pre-construction configuration. + */ + @SuppressAjWarnings("adviceDidNotMatch") + before(Object bean) : + beanConstruction(bean) && preConstructionCondition() && inConfigurableBean() { + configureBean(bean); + } + + /** + * Post-construction configuration. + */ + @SuppressAjWarnings("adviceDidNotMatch") + after(Object bean) returning : + beanConstruction(bean) && postConstructionCondition() && inConfigurableBean() { + configureBean(bean); + } + + /** + * Post-deserialization configuration. + */ + @SuppressAjWarnings("adviceDidNotMatch") + after(Object bean) returning : + beanDeserialization(bean) && inConfigurableBean() { + configureBean(bean); + } + + + /** + * Configure the given bean. + */ + public abstract void configureBean(Object bean); + +} diff --git a/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/AbstractInterfaceDrivenDependencyInjectionAspect.aj b/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/AbstractInterfaceDrivenDependencyInjectionAspect.aj new file mode 100644 index 0000000..6e049a5 --- /dev/null +++ b/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/AbstractInterfaceDrivenDependencyInjectionAspect.aj @@ -0,0 +1,121 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.aspectj; + +import java.io.ObjectStreamException; +import java.io.Serializable; + +/** + * An aspect that injects dependency into any object whose type implements the + * {@link ConfigurableObject} interface. + * + *

This aspect supports injecting into domain objects when they are created + * for the first time as well as upon deserialization. Subaspects need to simply + * provide definition for the configureBean() method. This method may be + * implemented without relying on Spring container if so desired. + * + *

There are two cases that needs to be handled: + *

    + *
  1. Normal object creation via the '{@code new}' operator: this is + * taken care of by advising {@code initialization()} join points.
  2. + *
  3. Object creation through deserialization: since no constructor is + * invoked during deserialization, the aspect needs to advise a method that a + * deserialization mechanism is going to invoke. Ideally, we should not + * require user classes to implement any specific method. This implies that + * we need to introduce the chosen method. We should also handle the cases + * where the chosen method is already implemented in classes (in which case, + * the user's implementation for that method should take precedence over the + * introduced implementation). There are a few choices for the chosen method: + *
      + *
    • readObject(ObjectOutputStream): Java requires that the method must be + * {@code private}

      . Since aspects cannot introduce a private member, + * while preserving its name, this option is ruled out.
    • + *
    • readResolve(): Java doesn't pose any restriction on an access specifier. + * Problem solved! There is one (minor) limitation of this approach in + * that if a user class already has this method, that method must be + * {@code public}. However, this shouldn't be a big burden, since + * use cases that need classes to implement readResolve() (custom enums, + * for example) are unlikely to be marked as @Configurable, and + * in any case asking to make that method {@code public} should not + * pose any undue burden.
    • + *
    + * The minor collaboration needed by user classes (i.e., that the implementation of + * {@code readResolve()}, if any, must be {@code public}) can be lifted as well if we + * were to use an experimental feature in AspectJ - the {@code hasmethod()} PCD.
  4. + *
+ * + *

While having type implement the {@link ConfigurableObject} interface is certainly + * a valid choice, an alternative is to use a 'declare parents' statement another aspect + * (a subaspect of this aspect would be a logical choice) that declares the classes that + * need to be configured by supplying the {@link ConfigurableObject} interface. + * + * @author Ramnivas Laddad + * @since 2.5.2 + */ +public abstract aspect AbstractInterfaceDrivenDependencyInjectionAspect extends AbstractDependencyInjectionAspect { + + /** + * Select initialization join point as object construction + */ + public pointcut beanConstruction(Object bean) : + initialization(ConfigurableObject+.new(..)) && this(bean); + + /** + * Select deserialization join point made available through ITDs for ConfigurableDeserializationSupport + */ + public pointcut beanDeserialization(Object bean) : + execution(Object ConfigurableDeserializationSupport+.readResolve()) && this(bean); + + public pointcut leastSpecificSuperTypeConstruction() : initialization(ConfigurableObject.new(..)); + + + + // Implementation to support re-injecting dependencies once an object is deserialized + + /** + * Declare any class implementing Serializable and ConfigurableObject as also implementing + * ConfigurableDeserializationSupport. This allows us to introduce the {@code readResolve()} + * method and select it with the beanDeserialization() pointcut. + *

Here is an improved version that uses the hasmethod() pointcut and lifts + * even the minor requirement on user classes: + *

+	 * declare parents: ConfigurableObject+ Serializable+
+	 * && !hasmethod(Object readResolve() throws ObjectStreamException)
+	 * implements ConfigurableDeserializationSupport;
+	 * 
+ */ + declare parents: ConfigurableObject+ && Serializable+ implements ConfigurableDeserializationSupport; + + /** + * A marker interface to which the {@code readResolve()} is introduced. + */ + static interface ConfigurableDeserializationSupport extends Serializable { + } + + /** + * Introduce the {@code readResolve()} method so that we can advise its + * execution to configure the object. + *

Note if a method with the same signature already exists in a + * {@code Serializable} class of ConfigurableObject type, + * that implementation will take precedence (a good thing, since we are + * merely interested in an opportunity to detect deserialization.) + */ + public Object ConfigurableDeserializationSupport.readResolve() throws ObjectStreamException { + return this; + } + +} diff --git a/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/AnnotationBeanConfigurerAspect.aj b/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/AnnotationBeanConfigurerAspect.aj new file mode 100644 index 0000000..923e459 --- /dev/null +++ b/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/AnnotationBeanConfigurerAspect.aj @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.aspectj; + +import java.io.Serializable; + +import org.aspectj.lang.annotation.control.CodeGenerationHint; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.AnnotationBeanWiringInfoResolver; +import org.springframework.beans.factory.annotation.Configurable; +import org.springframework.beans.factory.wiring.BeanConfigurerSupport; + +/** + * Concrete aspect that uses the {@link Configurable} annotation to identify + * which classes need autowiring. + * + *

The bean name to look up will be taken from the {@code @Configurable} + * annotation if specified; otherwise, the default bean name to look up will be + * the fully qualified name of the class being configured. + * + * @author Rod Johnson + * @author Ramnivas Laddad + * @author Juergen Hoeller + * @author Adrian Colyer + * @since 2.0 + * @see org.springframework.beans.factory.annotation.Configurable + * @see org.springframework.beans.factory.annotation.AnnotationBeanWiringInfoResolver + */ +public aspect AnnotationBeanConfigurerAspect extends AbstractInterfaceDrivenDependencyInjectionAspect + implements BeanFactoryAware, InitializingBean, DisposableBean { + + private final BeanConfigurerSupport beanConfigurerSupport = new BeanConfigurerSupport(); + + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanConfigurerSupport.setBeanWiringInfoResolver(new AnnotationBeanWiringInfoResolver()); + this.beanConfigurerSupport.setBeanFactory(beanFactory); + } + + @Override + public void afterPropertiesSet() { + this.beanConfigurerSupport.afterPropertiesSet(); + } + + @Override + public void configureBean(Object bean) { + this.beanConfigurerSupport.configureBean(bean); + } + + @Override + public void destroy() { + this.beanConfigurerSupport.destroy(); + } + + + public pointcut inConfigurableBean() : @this(Configurable); + + public pointcut preConstructionConfiguration() : preConstructionConfigurationSupport(*); + + /* + * An intermediary to match preConstructionConfiguration signature (that doesn't expose the annotation object) + */ + @CodeGenerationHint(ifNameSuffix="bb0") + private pointcut preConstructionConfigurationSupport(Configurable c) : @this(c) && if (c.preConstruction()); + + + declare parents: @Configurable * implements ConfigurableObject; + + /* + * This declaration shouldn't be needed, + * except for an AspectJ bug (https://bugs.eclipse.org/bugs/show_bug.cgi?id=214559) + */ + declare parents: @Configurable Serializable+ implements ConfigurableDeserializationSupport; + +} diff --git a/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/ConfigurableObject.java b/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/ConfigurableObject.java new file mode 100644 index 0000000..c670dfc --- /dev/null +++ b/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/ConfigurableObject.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.aspectj; + +/** + * Marker interface for domain object that need DI through aspects. + * + * @author Ramnivas Laddad + * @since 2.5 + */ +public interface ConfigurableObject { + +} diff --git a/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/GenericInterfaceDrivenDependencyInjectionAspect.aj b/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/GenericInterfaceDrivenDependencyInjectionAspect.aj new file mode 100644 index 0000000..867ecff --- /dev/null +++ b/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/GenericInterfaceDrivenDependencyInjectionAspect.aj @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.aspectj; + +/** + * Generic-based dependency injection aspect. + * + *

This aspect allows users to implement efficient, type-safe dependency injection + * without the use of the {@code @Configurable} annotation. + * + *

The subaspect of this aspect doesn't need to include any AOP constructs. For + * example, here is a subaspect that configures the {@code PricingStrategyClient} objects. + * + *

+ * aspect PricingStrategyDependencyInjectionAspect
+ *        extends GenericInterfaceDrivenDependencyInjectionAspect {
+ *     private PricingStrategy pricingStrategy;
+ *
+ *     public void configure(PricingStrategyClient bean) {
+ *         bean.setPricingStrategy(pricingStrategy);
+ *     }
+ *
+ *     public void setPricingStrategy(PricingStrategy pricingStrategy) {
+ *         this.pricingStrategy = pricingStrategy;
+ *     }
+ * }
+ * + * @author Ramnivas Laddad + * @since 3.0 + */ +public abstract aspect GenericInterfaceDrivenDependencyInjectionAspect extends AbstractInterfaceDrivenDependencyInjectionAspect { + + declare parents: I implements ConfigurableObject; + + public pointcut inConfigurableBean() : within(I+); + + @SuppressWarnings("unchecked") + public final void configureBean(Object bean) { + configure((I) bean); + } + + // Unfortunately, erasure used with generics won't allow to use the same named method + protected abstract void configure(I bean); + +} diff --git a/spring-aspects/src/main/java/org/springframework/cache/aspectj/AbstractCacheAspect.aj b/spring-aspects/src/main/java/org/springframework/cache/aspectj/AbstractCacheAspect.aj new file mode 100644 index 0000000..ae6d6a3 --- /dev/null +++ b/spring-aspects/src/main/java/org/springframework/cache/aspectj/AbstractCacheAspect.aj @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.aspectj; + +import java.lang.reflect.Method; + +import org.aspectj.lang.annotation.SuppressAjWarnings; +import org.aspectj.lang.reflect.MethodSignature; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.cache.interceptor.CacheAspectSupport; +import org.springframework.cache.interceptor.CacheOperationInvoker; +import org.springframework.cache.interceptor.CacheOperationSource; + +/** + * Abstract superaspect for AspectJ cache aspects. Concrete subaspects will implement the + * {@link #cacheMethodExecution} pointcut using a strategy such as Java 5 annotations. + * + *

Suitable for use inside or outside the Spring IoC container. Set the + * {@link #setCacheManager cacheManager} property appropriately, allowing use of any cache + * implementation supported by Spring. + * + *

NB: If a method implements an interface that is itself cache annotated, the + * relevant Spring cache definition will not be resolved. + * + * @author Costin Leau + * @author Stephane Nicoll + * @since 3.1 + */ +public abstract aspect AbstractCacheAspect extends CacheAspectSupport implements DisposableBean { + + protected AbstractCacheAspect() { + } + + /** + * Construct object using the given caching metadata retrieval strategy. + * @param cos {@link CacheOperationSource} implementation, retrieving Spring cache + * metadata for each joinpoint. + */ + protected AbstractCacheAspect(CacheOperationSource... cos) { + setCacheOperationSources(cos); + } + + @Override + public void destroy() { + clearMetadataCache(); // An aspect is basically a singleton + } + + @SuppressAjWarnings("adviceDidNotMatch") + Object around(final Object cachedObject) : cacheMethodExecution(cachedObject) { + MethodSignature methodSignature = (MethodSignature) thisJoinPoint.getSignature(); + Method method = methodSignature.getMethod(); + + CacheOperationInvoker aspectJInvoker = new CacheOperationInvoker() { + public Object invoke() { + try { + return proceed(cachedObject); + } + catch (Throwable ex) { + throw new ThrowableWrapper(ex); + } + } + }; + + try { + return execute(aspectJInvoker, thisJoinPoint.getTarget(), method, thisJoinPoint.getArgs()); + } + catch (CacheOperationInvoker.ThrowableWrapper th) { + AnyThrow.throwUnchecked(th.getOriginal()); + return null; // never reached + } + } + + /** + * Concrete subaspects must implement this pointcut, to identify cached methods. + */ + protected abstract pointcut cacheMethodExecution(Object cachedObject); + +} diff --git a/spring-aspects/src/main/java/org/springframework/cache/aspectj/AnnotationCacheAspect.aj b/spring-aspects/src/main/java/org/springframework/cache/aspectj/AnnotationCacheAspect.aj new file mode 100644 index 0000000..671b3a6 --- /dev/null +++ b/spring-aspects/src/main/java/org/springframework/cache/aspectj/AnnotationCacheAspect.aj @@ -0,0 +1,116 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.aspectj; + +import org.springframework.cache.annotation.AnnotationCacheOperationSource; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.annotation.Caching; + +/** + * Concrete AspectJ cache aspect using Spring's @{@link Cacheable} annotation. + * + *

When using this aspect, you must annotate the implementation class (and/or + * methods within that class), not the interface (if any) that the class + * implements. AspectJ follows Java's rule that annotations on interfaces are not + * inherited. + * + *

A {@code @Cacheable} annotation on a class specifies the default caching semantics + * for the execution of any public operation in the class. + * + *

A {@code @Cacheable} annotation on a method within the class overrides the default + * caching semantics given by the class annotation (if present). Any method may be + * annotated (regardless of visibility). Annotating non-public methods directly is the + * only way to get caching demarcation for the execution of such operations. + * + * @author Costin Leau + * @since 3.1 + */ +public aspect AnnotationCacheAspect extends AbstractCacheAspect { + + public AnnotationCacheAspect() { + super(new AnnotationCacheOperationSource(false)); + } + + /** + * Matches the execution of any public method in a type with the @{@link Cacheable} + * annotation, or any subtype of a type with the {@code @Cacheable} annotation. + */ + private pointcut executionOfAnyPublicMethodInAtCacheableType() : + execution(public * ((@Cacheable *)+).*(..)) && within(@Cacheable *); + + /** + * Matches the execution of any public method in a type with the @{@link CacheEvict} + * annotation, or any subtype of a type with the {@code CacheEvict} annotation. + */ + private pointcut executionOfAnyPublicMethodInAtCacheEvictType() : + execution(public * ((@CacheEvict *)+).*(..)) && within(@CacheEvict *); + + /** + * Matches the execution of any public method in a type with the @{@link CachePut} + * annotation, or any subtype of a type with the {@code CachePut} annotation. + */ + private pointcut executionOfAnyPublicMethodInAtCachePutType() : + execution(public * ((@CachePut *)+).*(..)) && within(@CachePut *); + + /** + * Matches the execution of any public method in a type with the @{@link Caching} + * annotation, or any subtype of a type with the {@code Caching} annotation. + */ + private pointcut executionOfAnyPublicMethodInAtCachingType() : + execution(public * ((@Caching *)+).*(..)) && within(@Caching *); + + /** + * Matches the execution of any method with the @{@link Cacheable} annotation. + */ + private pointcut executionOfCacheableMethod() : + execution(@Cacheable * *(..)); + + /** + * Matches the execution of any method with the @{@link CacheEvict} annotation. + */ + private pointcut executionOfCacheEvictMethod() : + execution(@CacheEvict * *(..)); + + /** + * Matches the execution of any method with the @{@link CachePut} annotation. + */ + private pointcut executionOfCachePutMethod() : + execution(@CachePut * *(..)); + + /** + * Matches the execution of any method with the @{@link Caching} annotation. + */ + private pointcut executionOfCachingMethod() : + execution(@Caching * *(..)); + + /** + * Definition of pointcut from super aspect - matched join points will have Spring + * cache management applied. + */ + protected pointcut cacheMethodExecution(Object cachedObject) : + (executionOfAnyPublicMethodInAtCacheableType() + || executionOfAnyPublicMethodInAtCacheEvictType() + || executionOfAnyPublicMethodInAtCachePutType() + || executionOfAnyPublicMethodInAtCachingType() + || executionOfCacheableMethod() + || executionOfCacheEvictMethod() + || executionOfCachePutMethod() + || executionOfCachingMethod()) + && this(cachedObject); +} \ No newline at end of file diff --git a/spring-aspects/src/main/java/org/springframework/cache/aspectj/AnyThrow.java b/spring-aspects/src/main/java/org/springframework/cache/aspectj/AnyThrow.java new file mode 100644 index 0000000..c59bce7 --- /dev/null +++ b/spring-aspects/src/main/java/org/springframework/cache/aspectj/AnyThrow.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.aspectj; + +/** + * Utility to trick the compiler to throw a valid checked + * exceptions within the interceptor. + * + * @author Stephane Nicoll + */ +final class AnyThrow { + + private AnyThrow() { + } + + + static void throwUnchecked(Throwable e) { + AnyThrow.throwAny(e); + } + + @SuppressWarnings("unchecked") + private static void throwAny(Throwable e) throws E { + throw (E) e; + } +} diff --git a/spring-aspects/src/main/java/org/springframework/cache/aspectj/AspectJCachingConfiguration.java b/spring-aspects/src/main/java/org/springframework/cache/aspectj/AspectJCachingConfiguration.java new file mode 100644 index 0000000..e48a0f6 --- /dev/null +++ b/spring-aspects/src/main/java/org/springframework/cache/aspectj/AspectJCachingConfiguration.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.aspectj; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.cache.annotation.AbstractCachingConfiguration; +import org.springframework.cache.config.CacheManagementConfigUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; + +/** + * {@code @Configuration} class that registers the Spring infrastructure beans + * necessary to enable AspectJ-based annotation-driven cache management. + * + * @author Chris Beams + * @author Stephane Nicoll + * @author Juergen Hoeller + * @since 3.1 + * @see org.springframework.cache.annotation.EnableCaching + * @see org.springframework.cache.annotation.CachingConfigurationSelector + */ +@Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +public class AspectJCachingConfiguration extends AbstractCachingConfiguration { + + @Bean(name = CacheManagementConfigUtils.CACHE_ASPECT_BEAN_NAME) + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public AnnotationCacheAspect cacheAspect() { + AnnotationCacheAspect cacheAspect = AnnotationCacheAspect.aspectOf(); + cacheAspect.configure(this.errorHandler, this.keyGenerator, this.cacheResolver, this.cacheManager); + return cacheAspect; + } + +} diff --git a/spring-aspects/src/main/java/org/springframework/cache/aspectj/AspectJJCacheConfiguration.java b/spring-aspects/src/main/java/org/springframework/cache/aspectj/AspectJJCacheConfiguration.java new file mode 100644 index 0000000..3468889 --- /dev/null +++ b/spring-aspects/src/main/java/org/springframework/cache/aspectj/AspectJJCacheConfiguration.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.aspectj; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.cache.config.CacheManagementConfigUtils; +import org.springframework.cache.jcache.config.AbstractJCacheConfiguration; +import org.springframework.cache.jcache.interceptor.JCacheOperationSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; + +/** + * {@code @Configuration} class that registers the Spring infrastructure beans necessary + * to enable AspectJ-based annotation-driven cache management for standard JSR-107 + * annotations. + * + * @author Stephane Nicoll + * @since 4.1 + * @see org.springframework.cache.annotation.EnableCaching + * @see org.springframework.cache.annotation.CachingConfigurationSelector + */ +@Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +public class AspectJJCacheConfiguration extends AbstractJCacheConfiguration { + + @Bean(name = CacheManagementConfigUtils.JCACHE_ASPECT_BEAN_NAME) + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public JCacheCacheAspect cacheAspect(JCacheOperationSource jCacheOperationSource) { + JCacheCacheAspect cacheAspect = JCacheCacheAspect.aspectOf(); + cacheAspect.setCacheOperationSource(jCacheOperationSource); + return cacheAspect; + } + +} diff --git a/spring-aspects/src/main/java/org/springframework/cache/aspectj/JCacheCacheAspect.aj b/spring-aspects/src/main/java/org/springframework/cache/aspectj/JCacheCacheAspect.aj new file mode 100644 index 0000000..ebda3f2 --- /dev/null +++ b/spring-aspects/src/main/java/org/springframework/cache/aspectj/JCacheCacheAspect.aj @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.aspectj; + +import java.lang.reflect.Method; +import javax.cache.annotation.CachePut; +import javax.cache.annotation.CacheRemove; +import javax.cache.annotation.CacheRemoveAll; +import javax.cache.annotation.CacheResult; + +import org.aspectj.lang.annotation.RequiredTypes; +import org.aspectj.lang.annotation.SuppressAjWarnings; +import org.aspectj.lang.reflect.MethodSignature; + +import org.springframework.cache.interceptor.CacheOperationInvoker; +import org.springframework.cache.jcache.interceptor.JCacheAspectSupport; + +/** + * Concrete AspectJ cache aspect using JSR-107 standard annotations. + * + *

When using this aspect, you must annotate the implementation class (and/or + * methods within that class), not the interface (if any) that the class + * implements. AspectJ follows Java's rule that annotations on interfaces are not + * inherited. + * + *

Any method may be annotated (regardless of visibility). Annotating non-public + * methods directly is the only way to get caching demarcation for the execution of + * such operations. + * + * @author Stephane Nicoll + * @since 4.1 + */ +@RequiredTypes({"org.springframework.cache.jcache.interceptor.JCacheAspectSupport", "javax.cache.annotation.CacheResult"}) +public aspect JCacheCacheAspect extends JCacheAspectSupport { + + @SuppressAjWarnings("adviceDidNotMatch") + Object around(final Object cachedObject) : cacheMethodExecution(cachedObject) { + MethodSignature methodSignature = (MethodSignature) thisJoinPoint.getSignature(); + Method method = methodSignature.getMethod(); + + CacheOperationInvoker aspectJInvoker = new CacheOperationInvoker() { + public Object invoke() { + try { + return proceed(cachedObject); + } + catch (Throwable ex) { + throw new ThrowableWrapper(ex); + } + } + + }; + + try { + return execute(aspectJInvoker, thisJoinPoint.getTarget(), method, thisJoinPoint.getArgs()); + } + catch (CacheOperationInvoker.ThrowableWrapper th) { + AnyThrow.throwUnchecked(th.getOriginal()); + return null; // never reached + } + } + + /** + * Definition of pointcut: matched join points will have JSR-107 + * cache management applied. + */ + protected pointcut cacheMethodExecution(Object cachedObject) : + (executionOfCacheResultMethod() + || executionOfCachePutMethod() + || executionOfCacheRemoveMethod() + || executionOfCacheRemoveAllMethod()) + && this(cachedObject); + + /** + * Matches the execution of any method with the @{@link CacheResult} annotation. + */ + private pointcut executionOfCacheResultMethod() : + execution(@CacheResult * *(..)); + + /** + * Matches the execution of any method with the @{@link CachePut} annotation. + */ + private pointcut executionOfCachePutMethod() : + execution(@CachePut * *(..)); + + /** + * Matches the execution of any method with the @{@link CacheRemove} annotation. + */ + private pointcut executionOfCacheRemoveMethod() : + execution(@CacheRemove * *(..)); + + /** + * Matches the execution of any method with the @{@link CacheRemoveAll} annotation. + */ + private pointcut executionOfCacheRemoveAllMethod() : + execution(@CacheRemoveAll * *(..)); + + +} diff --git a/spring-aspects/src/main/java/org/springframework/context/annotation/aspectj/EnableSpringConfigured.java b/spring-aspects/src/main/java/org/springframework/context/annotation/aspectj/EnableSpringConfigured.java new file mode 100644 index 0000000..e26a94f --- /dev/null +++ b/spring-aspects/src/main/java/org/springframework/context/annotation/aspectj/EnableSpringConfigured.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.aspectj; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Import; + +/** + * Signals the current application context to apply dependency injection to + * non-managed classes that are instantiated outside of the Spring bean factory + * (typically classes annotated with the + * {@link org.springframework.beans.factory.annotation.Configurable @Configurable} + * annotation). + * + *

Similar to functionality found in Spring's + * {@code } XML element. Often used in conjunction with + * {@link org.springframework.context.annotation.EnableLoadTimeWeaving @EnableLoadTimeWeaving}. + * + * @author Chris Beams + * @since 3.1 + * @see org.springframework.context.annotation.EnableLoadTimeWeaving + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Import(SpringConfiguredConfiguration.class) +public @interface EnableSpringConfigured { + +} diff --git a/spring-aspects/src/main/java/org/springframework/context/annotation/aspectj/SpringConfiguredConfiguration.java b/spring-aspects/src/main/java/org/springframework/context/annotation/aspectj/SpringConfiguredConfiguration.java new file mode 100644 index 0000000..eb369ca --- /dev/null +++ b/spring-aspects/src/main/java/org/springframework/context/annotation/aspectj/SpringConfiguredConfiguration.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.aspectj; + +import org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; + +/** + * {@code @Configuration} class that registers an {@code AnnotationBeanConfigurerAspect} + * capable of performing dependency injection services for non-Spring managed objects + * annotated with @{@link org.springframework.beans.factory.annotation.Configurable + * Configurable}. + * + *

This configuration class is automatically imported when using the + * {@link EnableSpringConfigured @EnableSpringConfigured} annotation. See + * {@code @EnableSpringConfigured}'s javadoc for complete usage details. + * + * @author Chris Beams + * @since 3.1 + * @see EnableSpringConfigured + */ +@Configuration +public class SpringConfiguredConfiguration { + + /** + * The bean name used for the configurer aspect. + */ + public static final String BEAN_CONFIGURER_ASPECT_BEAN_NAME = + "org.springframework.context.config.internalBeanConfigurerAspect"; + + @Bean(name = BEAN_CONFIGURER_ASPECT_BEAN_NAME) + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public AnnotationBeanConfigurerAspect beanConfigurerAspect() { + return AnnotationBeanConfigurerAspect.aspectOf(); + } + +} diff --git a/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AbstractAsyncExecutionAspect.aj b/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AbstractAsyncExecutionAspect.aj new file mode 100644 index 0000000..eed22f4 --- /dev/null +++ b/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AbstractAsyncExecutionAspect.aj @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.aspectj; + +import java.util.concurrent.Callable; +import java.util.concurrent.Future; + +import org.aspectj.lang.annotation.SuppressAjWarnings; +import org.aspectj.lang.reflect.MethodSignature; + +import org.springframework.aop.interceptor.AsyncExecutionAspectSupport; +import org.springframework.core.task.AsyncTaskExecutor; + +/** + * Abstract aspect that routes selected methods asynchronously. + * + *

This aspect needs to be injected with an implementation of a task-oriented + * {@link java.util.concurrent.Executor} to activate it for a specific thread pool, + * or with a {@link org.springframework.beans.factory.BeanFactory} for default + * executor lookup. Otherwise it will simply delegate all calls synchronously. + * + * @author Ramnivas Laddad + * @author Juergen Hoeller + * @author Chris Beams + * @author Stephane Nicoll + * @since 3.0.5 + * @see #setExecutor + * @see #setBeanFactory + * @see #getDefaultExecutor + */ +public abstract aspect AbstractAsyncExecutionAspect extends AsyncExecutionAspectSupport { + + /** + * Create an {@code AnnotationAsyncExecutionAspect} with a {@code null} + * default executor, which should instead be set via {@code #aspectOf} and + * {@link #setExecutor}. The same applies for {@link #setExceptionHandler}. + */ + public AbstractAsyncExecutionAspect() { + super(null); + } + + + /** + * Apply around advice to methods matching the {@link #asyncMethod()} pointcut, + * submit the actual calling of the method to the correct task executor and return + * immediately to the caller. + * @return {@link Future} if the original method returns {@code Future}; + * {@code null} otherwise + */ + @SuppressAjWarnings("adviceDidNotMatch") + Object around() : asyncMethod() { + final MethodSignature methodSignature = (MethodSignature) thisJoinPointStaticPart.getSignature(); + + AsyncTaskExecutor executor = determineAsyncExecutor(methodSignature.getMethod()); + if (executor == null) { + return proceed(); + } + + Callable task = new Callable() { + public Object call() throws Exception { + try { + Object result = proceed(); + if (result instanceof Future) { + return ((Future) result).get(); + } + } + catch (Throwable ex) { + handleError(ex, methodSignature.getMethod(), thisJoinPoint.getArgs()); + } + return null; + }}; + + return doSubmit(task, executor, methodSignature.getReturnType()); + } + + /** + * Return the set of joinpoints at which async advice should be applied. + */ + public abstract pointcut asyncMethod(); + +} diff --git a/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AnnotationAsyncExecutionAspect.aj b/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AnnotationAsyncExecutionAspect.aj new file mode 100644 index 0000000..46c1b45 --- /dev/null +++ b/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AnnotationAsyncExecutionAspect.aj @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.aspectj; + +import java.lang.reflect.Method; +import java.util.concurrent.Future; + +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.scheduling.annotation.Async; + +/** + * Aspect to route methods based on Spring's {@link Async} annotation. + * + *

This aspect routes methods marked with the {@link Async} annotation as well as methods + * in classes marked with the same. Any method expected to be routed asynchronously must + * return either {@code void}, {@link Future}, or a subtype of {@link Future} (in particular, + * Spring's {@link org.springframework.util.concurrent.ListenableFuture}). This aspect, + * therefore, will produce a compile-time error for methods that violate this constraint + * on the return type. If, however, a class marked with {@code @Async} contains a method + * that violates this constraint, it produces only a warning. + * + *

This aspect needs to be injected with an implementation of a task-oriented + * {@link java.util.concurrent.Executor} to activate it for a specific thread pool, + * or with a {@link org.springframework.beans.factory.BeanFactory} for default + * executor lookup. Otherwise it will simply delegate all calls synchronously. + * + * @author Ramnivas Laddad + * @author Chris Beams + * @since 3.0.5 + * @see #setExecutor + * @see #setBeanFactory + * @see #getDefaultExecutor + */ +public aspect AnnotationAsyncExecutionAspect extends AbstractAsyncExecutionAspect { + + private pointcut asyncMarkedMethod() : execution(@Async (void || Future+) *(..)); + + private pointcut asyncTypeMarkedMethod() : execution((void || Future+) (@Async *).*(..)); + + public pointcut asyncMethod() : asyncMarkedMethod() || asyncTypeMarkedMethod(); + + + /** + * This implementation inspects the given method and its declaring class for the + * {@code @Async} annotation, returning the qualifier value expressed by {@link Async#value()}. + * If {@code @Async} is specified at both the method and class level, the method's + * {@code #value} takes precedence (even if empty string, indicating that the default + * executor should be used preferentially). + * @return the qualifier if specified, otherwise empty string indicating that the + * {@linkplain #setExecutor default executor} should be used + * @see #determineAsyncExecutor(Method) + */ + @Override + protected String getExecutorQualifier(Method method) { + // Maintainer's note: changes made here should also be made in + // AnnotationAsyncExecutionInterceptor#getExecutorQualifier + Async async = AnnotatedElementUtils.findMergedAnnotation(method, Async.class); + if (async == null) { + async = AnnotatedElementUtils.findMergedAnnotation(method.getDeclaringClass(), Async.class); + } + return (async != null ? async.value() : null); + } + + + declare error: + execution(@Async !(void || Future+) *(..)): + "Only methods that return void or Future may have an @Async annotation"; + + declare warning: + execution(!(void || Future+) (@Async *).*(..)): + "Methods in a class marked with @Async that do not return void or Future will be routed synchronously"; + +} diff --git a/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AspectJAsyncConfiguration.java b/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AspectJAsyncConfiguration.java new file mode 100644 index 0000000..c6cdada --- /dev/null +++ b/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/AspectJAsyncConfiguration.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.aspectj; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.scheduling.annotation.AbstractAsyncConfiguration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.config.TaskManagementConfigUtils; + +/** + * {@code @Configuration} class that registers the Spring infrastructure beans necessary + * to enable AspectJ-based asynchronous method execution. + * + * @author Chris Beams + * @author Stephane Nicoll + * @author Juergen Hoeller + * @since 3.1 + * @see EnableAsync + * @see org.springframework.scheduling.annotation.AsyncConfigurationSelector + * @see org.springframework.scheduling.annotation.ProxyAsyncConfiguration + */ +@Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +public class AspectJAsyncConfiguration extends AbstractAsyncConfiguration { + + @Bean(name = TaskManagementConfigUtils.ASYNC_EXECUTION_ASPECT_BEAN_NAME) + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public AnnotationAsyncExecutionAspect asyncAdvisor() { + AnnotationAsyncExecutionAspect asyncAspect = AnnotationAsyncExecutionAspect.aspectOf(); + asyncAspect.configure(this.executor, this.exceptionHandler); + return asyncAspect; + } + +} diff --git a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AbstractTransactionAspect.aj b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AbstractTransactionAspect.aj new file mode 100644 index 0000000..782ca35 --- /dev/null +++ b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AbstractTransactionAspect.aj @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.transaction.aspectj; + +import org.aspectj.lang.annotation.SuppressAjWarnings; +import org.aspectj.lang.reflect.MethodSignature; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.transaction.interceptor.TransactionAspectSupport; +import org.springframework.transaction.interceptor.TransactionAttributeSource; + +/** + * Abstract superaspect for AspectJ transaction aspects. Concrete + * subaspects will implement the {@code transactionalMethodExecution()} + * pointcut using a strategy such as Java 5 annotations. + * + *

Suitable for use inside or outside the Spring IoC container. + * Set the "transactionManager" property appropriately, allowing + * use of any transaction implementation supported by Spring. + * + *

NB: If a method implements an interface that is itself + * transactionally annotated, the relevant Spring transaction attribute + * will not be resolved. This behavior will vary from that of Spring AOP + * if proxying an interface (but not when proxying a class). We recommend that + * transaction annotations should be added to classes, rather than business + * interfaces, as they are an implementation detail rather than a contract + * specification validation. + * + * @author Rod Johnson + * @author Ramnivas Laddad + * @author Juergen Hoeller + * @since 2.0 + */ +public abstract aspect AbstractTransactionAspect extends TransactionAspectSupport implements DisposableBean { + + /** + * Construct the aspect using the given transaction metadata retrieval strategy. + * @param tas TransactionAttributeSource implementation, retrieving Spring + * transaction metadata for each joinpoint. Implement the subclass to pass in + * {@code null} if it is intended to be configured through Setter Injection. + */ + protected AbstractTransactionAspect(TransactionAttributeSource tas) { + setTransactionAttributeSource(tas); + } + + @Override + public void destroy() { + // An aspect is basically a singleton -> cleanup on destruction + clearTransactionManagerCache(); + } + + @SuppressAjWarnings("adviceDidNotMatch") + Object around(final Object txObject): transactionalMethodExecution(txObject) { + MethodSignature methodSignature = (MethodSignature) thisJoinPoint.getSignature(); + // Adapt to TransactionAspectSupport's invokeWithinTransaction... + try { + return invokeWithinTransaction(methodSignature.getMethod(), txObject.getClass(), new InvocationCallback() { + public Object proceedWithInvocation() throws Throwable { + return proceed(txObject); + } + }); + } + catch (RuntimeException | Error ex) { + throw ex; + } + catch (Throwable thr) { + Rethrower.rethrow(thr); + throw new IllegalStateException("Should never get here", thr); + } + } + + /** + * Concrete subaspects must implement this pointcut, to identify + * transactional methods. For each selected joinpoint, TransactionMetadata + * will be retrieved using Spring's TransactionAttributeSource interface. + */ + protected abstract pointcut transactionalMethodExecution(Object txObject); + + + /** + * Ugly but safe workaround: We need to be able to propagate checked exceptions, + * despite AspectJ around advice supporting specifically declared exceptions only. + */ + private static class Rethrower { + + public static void rethrow(final Throwable exception) { + class CheckedExceptionRethrower { + @SuppressWarnings("unchecked") + private void rethrow(Throwable exception) throws T { + throw (T) exception; + } + } + new CheckedExceptionRethrower().rethrow(exception); + } + } + +} diff --git a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AnnotationTransactionAspect.aj b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AnnotationTransactionAspect.aj new file mode 100644 index 0000000..bdaae70 --- /dev/null +++ b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AnnotationTransactionAspect.aj @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.transaction.aspectj; + +import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource; +import org.springframework.transaction.annotation.Transactional; + +/** + * Concrete AspectJ transaction aspect using Spring's + * {@link org.springframework.transaction.annotation.Transactional} annotation. + * + *

When using this aspect, you must annotate the implementation class + * (and/or methods within that class), not the interface (if any) that + * the class implements. AspectJ follows Java's rule that annotations on + * interfaces are not inherited. + * + *

An @Transactional annotation on a class specifies the default transaction + * semantics for the execution of any public operation in the class. + * + *

An @Transactional annotation on a method within the class overrides the + * default transaction semantics given by the class annotation (if present). + * Any method may be annotated (regardless of visibility). Annotating + * non-public methods directly is the only way to get transaction demarcation + * for the execution of such operations. + * + * @author Rod Johnson + * @author Ramnivas Laddad + * @author Adrian Colyer + * @since 2.0 + * @see org.springframework.transaction.annotation.Transactional + */ +public aspect AnnotationTransactionAspect extends AbstractTransactionAspect { + + public AnnotationTransactionAspect() { + super(new AnnotationTransactionAttributeSource(false)); + } + + /** + * Matches the execution of any public method in a type with the Transactional + * annotation, or any subtype of a type with the Transactional annotation. + */ + private pointcut executionOfAnyPublicMethodInAtTransactionalType() : + execution(public * ((@Transactional *)+).*(..)) && within(@Transactional *); + + /** + * Matches the execution of any method with the Transactional annotation. + */ + private pointcut executionOfTransactionalMethod() : + execution(@Transactional * *(..)); + + /** + * Definition of pointcut from super aspect - matched join points + * will have Spring transaction management applied. + */ + protected pointcut transactionalMethodExecution(Object txObject) : + (executionOfAnyPublicMethodInAtTransactionalType() || executionOfTransactionalMethod() ) && this(txObject); + +} diff --git a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJJtaTransactionManagementConfiguration.java b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJJtaTransactionManagementConfiguration.java new file mode 100644 index 0000000..ec733d3 --- /dev/null +++ b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJJtaTransactionManagementConfiguration.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.transaction.aspectj; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.annotation.TransactionManagementConfigurationSelector; +import org.springframework.transaction.config.TransactionManagementConfigUtils; + +/** + * {@code @Configuration} class that registers the Spring infrastructure beans necessary + * to enable AspectJ-based annotation-driven transaction management for the JTA 1.2 + * {@link javax.transaction.Transactional} annotation in addition to Spring's own + * {@link org.springframework.transaction.annotation.Transactional} annotation. + * + * @author Juergen Hoeller + * @since 5.1 + * @see EnableTransactionManagement + * @see TransactionManagementConfigurationSelector + */ +@Configuration +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +public class AspectJJtaTransactionManagementConfiguration extends AspectJTransactionManagementConfiguration { + + @Bean(name = TransactionManagementConfigUtils.JTA_TRANSACTION_ASPECT_BEAN_NAME) + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public JtaAnnotationTransactionAspect jtaTransactionAspect() { + JtaAnnotationTransactionAspect txAspect = JtaAnnotationTransactionAspect.aspectOf(); + if (this.txManager != null) { + txAspect.setTransactionManager(this.txManager); + } + return txAspect; + } + +} diff --git a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJTransactionManagementConfiguration.java b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJTransactionManagementConfiguration.java new file mode 100644 index 0000000..2c99c30 --- /dev/null +++ b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AspectJTransactionManagementConfiguration.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.transaction.aspectj; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.transaction.annotation.AbstractTransactionManagementConfiguration; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.annotation.TransactionManagementConfigurationSelector; +import org.springframework.transaction.config.TransactionManagementConfigUtils; + +/** + * {@code @Configuration} class that registers the Spring infrastructure beans necessary + * to enable AspectJ-based annotation-driven transaction management for Spring's own + * {@link org.springframework.transaction.annotation.Transactional} annotation. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + * @see EnableTransactionManagement + * @see TransactionManagementConfigurationSelector + * @see AspectJJtaTransactionManagementConfiguration + */ +@Configuration +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +public class AspectJTransactionManagementConfiguration extends AbstractTransactionManagementConfiguration { + + @Bean(name = TransactionManagementConfigUtils.TRANSACTION_ASPECT_BEAN_NAME) + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public AnnotationTransactionAspect transactionAspect() { + AnnotationTransactionAspect txAspect = AnnotationTransactionAspect.aspectOf(); + if (this.txManager != null) { + txAspect.setTransactionManager(this.txManager); + } + return txAspect; + } + +} diff --git a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/JtaAnnotationTransactionAspect.aj b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/JtaAnnotationTransactionAspect.aj new file mode 100644 index 0000000..1644ce5 --- /dev/null +++ b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/JtaAnnotationTransactionAspect.aj @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.transaction.aspectj; + +import javax.transaction.Transactional; + +import org.aspectj.lang.annotation.RequiredTypes; + +import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource; + +/** + * Concrete AspectJ transaction aspect using the JTA 1.2 + * {@link javax.transaction.Transactional} annotation. + * + *

When using this aspect, you must annotate the implementation class + * (and/or methods within that class), not the interface (if any) that + * the class implements. AspectJ follows Java's rule that annotations on + * interfaces are not inherited. + * + *

An @Transactional annotation on a class specifies the default transaction + * semantics for the execution of any public operation in the class. + * + *

An @Transactional annotation on a method within the class overrides the + * default transaction semantics given by the class annotation (if present). + * Any method may be annotated (regardless of visibility). Annotating + * non-public methods directly is the only way to get transaction demarcation + * for the execution of such operations. + * + * @author Stephane Nicoll + * @since 4.2 + * @see javax.transaction.Transactional + * @see AnnotationTransactionAspect + */ +@RequiredTypes("javax.transaction.Transactional") +public aspect JtaAnnotationTransactionAspect extends AbstractTransactionAspect { + + public JtaAnnotationTransactionAspect() { + super(new AnnotationTransactionAttributeSource(false)); + } + + /** + * Matches the execution of any public method in a type with the Transactional + * annotation, or any subtype of a type with the Transactional annotation. + */ + private pointcut executionOfAnyPublicMethodInAtTransactionalType() : + execution(public * ((@Transactional *)+).*(..)) && within(@Transactional *); + + /** + * Matches the execution of any method with the Transactional annotation. + */ + private pointcut executionOfTransactionalMethod() : + execution(@Transactional * *(..)); + + /** + * Definition of pointcut from super aspect - matched join points + * will have Spring transaction management applied. + */ + protected pointcut transactionalMethodExecution(Object txObject) : + (executionOfAnyPublicMethodInAtTransactionalType() || executionOfTransactionalMethod() ) && this(txObject); + +} diff --git a/spring-aspects/src/test/java/org/springframework/aop/aspectj/autoproxy/AutoProxyWithCodeStyleAspectsTests.java b/spring-aspects/src/test/java/org/springframework/aop/aspectj/autoproxy/AutoProxyWithCodeStyleAspectsTests.java new file mode 100644 index 0000000..43947fa --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/aop/aspectj/autoproxy/AutoProxyWithCodeStyleAspectsTests.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.autoproxy; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.support.ClassPathXmlApplicationContext; + +/** + * @author Adrian Colyer + */ +public class AutoProxyWithCodeStyleAspectsTests { + + @Test + @SuppressWarnings("resource") + public void noAutoproxyingOfAjcCompiledAspects() { + new ClassPathXmlApplicationContext("org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml"); + } + +} diff --git a/spring-aspects/src/test/java/org/springframework/aop/aspectj/autoproxy/CodeStyleAspect.aj b/spring-aspects/src/test/java/org/springframework/aop/aspectj/autoproxy/CodeStyleAspect.aj new file mode 100644 index 0000000..0ba9e0d --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/aop/aspectj/autoproxy/CodeStyleAspect.aj @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.autoproxy; + +import org.aspectj.lang.annotation.SuppressAjWarnings; + +/** + * @author Adrian Colyer + */ +public aspect CodeStyleAspect { + + @SuppressWarnings("unused") + private String foo; + + pointcut somePC() : call(* someMethod()); + + @SuppressAjWarnings("adviceDidNotMatch") + before() : somePC() { + System.out.println("match"); + } + + public void setFoo(String foo) { + this.foo = foo; + } + +} diff --git a/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/ShouldBeConfiguredBySpring.java b/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/ShouldBeConfiguredBySpring.java new file mode 100644 index 0000000..586d936 --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/ShouldBeConfiguredBySpring.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.aspectj; + +import java.io.Serializable; + +import org.springframework.beans.factory.annotation.Configurable; + +@Configurable("configuredBean") +@SuppressWarnings("serial") +public class ShouldBeConfiguredBySpring implements Serializable { + + private String name; + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + +} diff --git a/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/SpringConfiguredWithAutoProxyingTests.java b/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/SpringConfiguredWithAutoProxyingTests.java new file mode 100644 index 0000000..a02bcad --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/SpringConfiguredWithAutoProxyingTests.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.aspectj; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.support.ClassPathXmlApplicationContext; + +public class SpringConfiguredWithAutoProxyingTests { + + @Test + @SuppressWarnings("resource") + public void springConfiguredAndAutoProxyUsedTogether() { + // instantiation is sufficient to trigger failure if this is going to fail... + new ClassPathXmlApplicationContext("org/springframework/beans/factory/aspectj/springConfigured.xml"); + } + +} diff --git a/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/XmlBeanConfigurerTests.java b/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/XmlBeanConfigurerTests.java new file mode 100644 index 0000000..71e98f6 --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/XmlBeanConfigurerTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.aspectj; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Chris Beams + */ +public class XmlBeanConfigurerTests { + + @Test + public void injection() { + try (ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( + "org/springframework/beans/factory/aspectj/beanConfigurerTests.xml")) { + + ShouldBeConfiguredBySpring myObject = new ShouldBeConfiguredBySpring(); + assertThat(myObject.getName()).isEqualTo("Rod"); + } + } + +} diff --git a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AbstractCacheAnnotationTests.java b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AbstractCacheAnnotationTests.java new file mode 100644 index 0000000..81dcf6b --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AbstractCacheAnnotationTests.java @@ -0,0 +1,882 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.aspectj; + +import java.util.Collection; +import java.util.UUID; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.AopProxyUtils; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.config.AnnotatedClassCacheableService; +import org.springframework.cache.config.CacheableService; +import org.springframework.cache.config.TestEntity; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.testfixture.cache.SomeCustomKeyGenerator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIOException; + +/** + * Copy of the shared {@code AbstractCacheAnnotationTests}: necessary + * due to issues with Gradle test fixtures and AspectJ configuration + * in the Gradle build. + * + *

Abstract cache annotation tests (containing several reusable methods). + * + * @author Costin Leau + * @author Chris Beams + * @author Phillip Webb + * @author Stephane Nicoll + */ +public abstract class AbstractCacheAnnotationTests { + + protected ConfigurableApplicationContext ctx; + + protected CacheableService cs; + + protected CacheableService ccs; + + protected CacheManager cm; + + + /** + * @return a refreshed application context + */ + protected abstract ConfigurableApplicationContext getApplicationContext(); + + + @BeforeEach + public void setup() { + this.ctx = getApplicationContext(); + this.cs = ctx.getBean("service", CacheableService.class); + this.ccs = ctx.getBean("classService", CacheableService.class); + this.cm = ctx.getBean("cacheManager", CacheManager.class); + + Collection cn = this.cm.getCacheNames(); + assertThat(cn).containsOnly("testCache", "secondary", "primary"); + } + + @AfterEach + public void close() { + if (this.ctx != null) { + this.ctx.close(); + } + } + + + protected void testCacheable(CacheableService service) { + Object o1 = new Object(); + + Object r1 = service.cache(o1); + Object r2 = service.cache(o1); + Object r3 = service.cache(o1); + + assertThat(r2).isSameAs(r1); + assertThat(r3).isSameAs(r1); + } + + protected void testCacheableNull(CacheableService service) { + Object o1 = new Object(); + assertThat(this.cm.getCache("testCache").get(o1)).isNull(); + + Object r1 = service.cacheNull(o1); + Object r2 = service.cacheNull(o1); + Object r3 = service.cacheNull(o1); + + assertThat(r2).isSameAs(r1); + assertThat(r3).isSameAs(r1); + + assertThat(this.cm.getCache("testCache")).as("testCache").isNotNull(); + assertThat(this.cm.getCache("testCache").get(o1)).as("cached object").isNotNull(); + assertThat(this.cm.getCache("testCache").get(o1).get()).isEqualTo(r3); + assertThat(r3).as("Cached value should be null").isNull(); + } + + protected void testCacheableSync(CacheableService service) { + Object o1 = new Object(); + + Object r1 = service.cacheSync(o1); + Object r2 = service.cacheSync(o1); + Object r3 = service.cacheSync(o1); + + assertThat(r2).isSameAs(r1); + assertThat(r3).isSameAs(r1); + } + + protected void testCacheableSyncNull(CacheableService service) { + Object o1 = new Object(); + assertThat(this.cm.getCache("testCache").get(o1)).isNull(); + + Object r1 = service.cacheSyncNull(o1); + Object r2 = service.cacheSyncNull(o1); + Object r3 = service.cacheSyncNull(o1); + + assertThat(r2).isSameAs(r1); + assertThat(r3).isSameAs(r1); + + assertThat(this.cm.getCache("testCache").get(o1).get()).isEqualTo(r3); + assertThat(r3).as("Cached value should be null").isNull(); + } + + protected void testEvict(CacheableService service, boolean successExpected) { + Cache cache = this.cm.getCache("testCache"); + + Object o1 = new Object(); + cache.putIfAbsent(o1, -1L); + Object r1 = service.cache(o1); + + service.evict(o1, null); + if (successExpected) { + assertThat(cache.get(o1)).isNull(); + } + else { + assertThat(cache.get(o1)).isNotNull(); + } + + Object r2 = service.cache(o1); + if (successExpected) { + assertThat(r2).isNotSameAs(r1); + } + else { + assertThat(r2).isSameAs(r1); + } + } + + protected void testEvictEarly(CacheableService service) { + Cache cache = this.cm.getCache("testCache"); + + Object o1 = new Object(); + cache.putIfAbsent(o1, -1L); + Object r1 = service.cache(o1); + + try { + service.evictEarly(o1); + } + catch (RuntimeException ex) { + // expected + } + assertThat(cache.get(o1)).isNull(); + + Object r2 = service.cache(o1); + assertThat(r2).isNotSameAs(r1); + } + + protected void testEvictException(CacheableService service) { + Object o1 = new Object(); + Object r1 = service.cache(o1); + + try { + service.evictWithException(o1); + } + catch (RuntimeException ex) { + // expected + } + // exception occurred, eviction skipped, data should still be in the cache + Object r2 = service.cache(o1); + assertThat(r2).isSameAs(r1); + } + + protected void testEvictWithKey(CacheableService service) { + Object o1 = new Object(); + Object r1 = service.cache(o1); + + service.evict(o1, null); + Object r2 = service.cache(o1); + assertThat(r2).isNotSameAs(r1); + } + + protected void testEvictWithKeyEarly(CacheableService service) { + Object o1 = new Object(); + Object r1 = service.cache(o1); + + try { + service.evictEarly(o1); + } + catch (Exception ex) { + // expected + } + Object r2 = service.cache(o1); + assertThat(r2).isNotSameAs(r1); + } + + protected void testEvictAll(CacheableService service, boolean successExpected) { + Cache cache = this.cm.getCache("testCache"); + + Object o1 = new Object(); + Object o2 = new Object(); + cache.putIfAbsent(o1, -1L); + cache.putIfAbsent(o2, -2L); + + Object r1 = service.cache(o1); + Object r2 = service.cache(o2); + assertThat(r2).isNotSameAs(r1); + + service.evictAll(new Object()); + if (successExpected) { + assertThat(cache.get(o1)).isNull(); + assertThat(cache.get(o2)).isNull(); + } + else { + assertThat(cache.get(o1)).isNotNull(); + assertThat(cache.get(o2)).isNotNull(); + } + + Object r3 = service.cache(o1); + Object r4 = service.cache(o2); + if (successExpected) { + assertThat(r3).isNotSameAs(r1); + assertThat(r4).isNotSameAs(r2); + } + else { + assertThat(r3).isSameAs(r1); + assertThat(r4).isSameAs(r2); + } + } + + protected void testEvictAllEarly(CacheableService service) { + Cache cache = this.cm.getCache("testCache"); + + Object o1 = new Object(); + Object o2 = new Object(); + cache.putIfAbsent(o1, -1L); + cache.putIfAbsent(o2, -2L); + + Object r1 = service.cache(o1); + Object r2 = service.cache(o2); + assertThat(r2).isNotSameAs(r1); + + try { + service.evictAllEarly(new Object()); + } + catch (Exception ex) { + // expected + } + assertThat(cache.get(o1)).isNull(); + assertThat(cache.get(o2)).isNull(); + + Object r3 = service.cache(o1); + Object r4 = service.cache(o2); + assertThat(r3).isNotSameAs(r1); + assertThat(r4).isNotSameAs(r2); + } + + protected void testConditionalExpression(CacheableService service) { + Object r1 = service.conditional(4); + Object r2 = service.conditional(4); + + assertThat(r2).isNotSameAs(r1); + + Object r3 = service.conditional(3); + Object r4 = service.conditional(3); + + assertThat(r4).isSameAs(r3); + } + + protected void testConditionalExpressionSync(CacheableService service) { + Object r1 = service.conditionalSync(4); + Object r2 = service.conditionalSync(4); + + assertThat(r2).isNotSameAs(r1); + + Object r3 = service.conditionalSync(3); + Object r4 = service.conditionalSync(3); + + assertThat(r4).isSameAs(r3); + } + + protected void testUnlessExpression(CacheableService service) { + Cache cache = this.cm.getCache("testCache"); + cache.clear(); + service.unless(10); + service.unless(11); + assertThat(cache.get(10).get()).isEqualTo(10L); + assertThat(cache.get(11)).isNull(); + } + + protected void testKeyExpression(CacheableService service) { + Object r1 = service.key(5, 1); + Object r2 = service.key(5, 2); + + assertThat(r2).isSameAs(r1); + + Object r3 = service.key(1, 5); + Object r4 = service.key(2, 5); + + assertThat(r4).isNotSameAs(r3); + } + + protected void testVarArgsKey(CacheableService service) { + Object r1 = service.varArgsKey(1, 2, 3); + Object r2 = service.varArgsKey(1, 2, 3); + + assertThat(r2).isSameAs(r1); + + Object r3 = service.varArgsKey(1, 2, 3); + Object r4 = service.varArgsKey(1, 2); + + assertThat(r4).isNotSameAs(r3); + } + + protected void testNullValue(CacheableService service) { + Object key = new Object(); + assertThat(service.nullValue(key)).isNull(); + int nr = service.nullInvocations().intValue(); + assertThat(service.nullValue(key)).isNull(); + assertThat(service.nullInvocations().intValue()).isEqualTo(nr); + assertThat(service.nullValue(new Object())).isNull(); + assertThat(service.nullInvocations().intValue()).isEqualTo(nr + 1); + } + + protected void testMethodName(CacheableService service, String keyName) { + Object key = new Object(); + Object r1 = service.name(key); + assertThat(service.name(key)).isSameAs(r1); + Cache cache = this.cm.getCache("testCache"); + // assert the method name is used + assertThat(cache.get(keyName)).isNotNull(); + } + + protected void testRootVars(CacheableService service) { + Object key = new Object(); + Object r1 = service.rootVars(key); + assertThat(service.rootVars(key)).isSameAs(r1); + Cache cache = this.cm.getCache("testCache"); + // assert the method name is used + String expectedKey = "rootVarsrootVars" + AopProxyUtils.ultimateTargetClass(service) + service; + assertThat(cache.get(expectedKey)).isNotNull(); + } + + protected void testCheckedThrowable(CacheableService service) { + String arg = UUID.randomUUID().toString(); + assertThatIOException().isThrownBy(() -> + service.throwChecked(arg)) + .withMessage(arg); + } + + protected void testUncheckedThrowable(CacheableService service) { + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + service.throwUnchecked(1L)) + .withMessage("1"); + } + + protected void testCheckedThrowableSync(CacheableService service) { + String arg = UUID.randomUUID().toString(); + assertThatIOException().isThrownBy(() -> + service.throwCheckedSync(arg)) + .withMessage(arg); + } + + protected void testUncheckedThrowableSync(CacheableService service) { + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + service.throwUncheckedSync(1L)) + .withMessage("1"); + } + + protected void testNullArg(CacheableService service) { + Object r1 = service.cache(null); + assertThat(service.cache(null)).isSameAs(r1); + } + + protected void testCacheUpdate(CacheableService service) { + Object o = new Object(); + Cache cache = this.cm.getCache("testCache"); + assertThat(cache.get(o)).isNull(); + Object r1 = service.update(o); + assertThat(cache.get(o).get()).isSameAs(r1); + + o = new Object(); + assertThat(cache.get(o)).isNull(); + Object r2 = service.update(o); + assertThat(cache.get(o).get()).isSameAs(r2); + } + + protected void testConditionalCacheUpdate(CacheableService service) { + int one = 1; + int three = 3; + + Cache cache = this.cm.getCache("testCache"); + assertThat(Integer.parseInt(service.conditionalUpdate(one).toString())).isEqualTo(one); + assertThat(cache.get(one)).isNull(); + + assertThat(Integer.parseInt(service.conditionalUpdate(three).toString())).isEqualTo(three); + assertThat(Integer.parseInt(cache.get(three).get().toString())).isEqualTo(three); + } + + protected void testMultiCache(CacheableService service) { + Object o1 = new Object(); + Object o2 = new Object(); + + Cache primary = this.cm.getCache("primary"); + Cache secondary = this.cm.getCache("secondary"); + + assertThat(primary.get(o1)).isNull(); + assertThat(secondary.get(o1)).isNull(); + Object r1 = service.multiCache(o1); + assertThat(primary.get(o1).get()).isSameAs(r1); + assertThat(secondary.get(o1).get()).isSameAs(r1); + + Object r2 = service.multiCache(o1); + Object r3 = service.multiCache(o1); + + assertThat(r2).isSameAs(r1); + assertThat(r3).isSameAs(r1); + + assertThat(primary.get(o2)).isNull(); + assertThat(secondary.get(o2)).isNull(); + Object r4 = service.multiCache(o2); + assertThat(primary.get(o2).get()).isSameAs(r4); + assertThat(secondary.get(o2).get()).isSameAs(r4); + } + + protected void testMultiEvict(CacheableService service) { + Object o1 = new Object(); + Object o2 = o1.toString() + "A"; + + + Object r1 = service.multiCache(o1); + Object r2 = service.multiCache(o1); + + Cache primary = this.cm.getCache("primary"); + Cache secondary = this.cm.getCache("secondary"); + + primary.put(o2, o2); + assertThat(r2).isSameAs(r1); + assertThat(primary.get(o1).get()).isSameAs(r1); + assertThat(secondary.get(o1).get()).isSameAs(r1); + + service.multiEvict(o1); + assertThat(primary.get(o1)).isNull(); + assertThat(secondary.get(o1)).isNull(); + assertThat(primary.get(o2)).isNull(); + + Object r3 = service.multiCache(o1); + Object r4 = service.multiCache(o1); + assertThat(r3).isNotSameAs(r1); + assertThat(r4).isSameAs(r3); + + assertThat(primary.get(o1).get()).isSameAs(r3); + assertThat(secondary.get(o1).get()).isSameAs(r4); + } + + protected void testMultiPut(CacheableService service) { + Object o = 1; + + Cache primary = this.cm.getCache("primary"); + Cache secondary = this.cm.getCache("secondary"); + + assertThat(primary.get(o)).isNull(); + assertThat(secondary.get(o)).isNull(); + Object r1 = service.multiUpdate(o); + assertThat(primary.get(o).get()).isSameAs(r1); + assertThat(secondary.get(o).get()).isSameAs(r1); + + o = 2; + assertThat(primary.get(o)).isNull(); + assertThat(secondary.get(o)).isNull(); + Object r2 = service.multiUpdate(o); + assertThat(primary.get(o).get()).isSameAs(r2); + assertThat(secondary.get(o).get()).isSameAs(r2); + } + + protected void testPutRefersToResult(CacheableService service) { + Long id = Long.MIN_VALUE; + TestEntity entity = new TestEntity(); + Cache primary = this.cm.getCache("primary"); + assertThat(primary.get(id)).isNull(); + assertThat(entity.getId()).isNull(); + service.putRefersToResult(entity); + assertThat(primary.get(id).get()).isSameAs(entity); + } + + protected void testMultiCacheAndEvict(CacheableService service) { + String methodName = "multiCacheAndEvict"; + + Cache primary = this.cm.getCache("primary"); + Cache secondary = this.cm.getCache("secondary"); + Object key = 1; + + secondary.put(key, key); + + assertThat(secondary.get(methodName)).isNull(); + assertThat(secondary.get(key).get()).isSameAs(key); + + Object r1 = service.multiCacheAndEvict(key); + assertThat(service.multiCacheAndEvict(key)).isSameAs(r1); + + // assert the method name is used + assertThat(primary.get(methodName).get()).isSameAs(r1); + assertThat(secondary.get(methodName)).isNull(); + assertThat(secondary.get(key)).isNull(); + } + + protected void testMultiConditionalCacheAndEvict(CacheableService service) { + Cache primary = this.cm.getCache("primary"); + Cache secondary = this.cm.getCache("secondary"); + Object key = 1; + + secondary.put(key, key); + + assertThat(primary.get(key)).isNull(); + assertThat(secondary.get(key).get()).isSameAs(key); + + Object r1 = service.multiConditionalCacheAndEvict(key); + Object r3 = service.multiConditionalCacheAndEvict(key); + + assertThat(!r1.equals(r3)).isTrue(); + assertThat(primary.get(key)).isNull(); + + Object key2 = 3; + Object r2 = service.multiConditionalCacheAndEvict(key2); + assertThat(service.multiConditionalCacheAndEvict(key2)).isSameAs(r2); + + // assert the method name is used + assertThat(primary.get(key2).get()).isSameAs(r2); + assertThat(secondary.get(key2)).isNull(); + } + + @Test + public void testCacheable() { + testCacheable(this.cs); + } + + @Test + public void testCacheableNull() { + testCacheableNull(this.cs); + } + + @Test + public void testCacheableSync() { + testCacheableSync(this.cs); + } + + @Test + public void testCacheableSyncNull() { + testCacheableSyncNull(this.cs); + } + + @Test + public void testEvict() { + testEvict(this.cs, true); + } + + @Test + public void testEvictEarly() { + testEvictEarly(this.cs); + } + + @Test + public void testEvictWithException() { + testEvictException(this.cs); + } + + @Test + public void testEvictAll() { + testEvictAll(this.cs, true); + } + + @Test + public void testEvictAllEarly() { + testEvictAllEarly(this.cs); + } + + @Test + public void testEvictWithKey() { + testEvictWithKey(this.cs); + } + + @Test + public void testEvictWithKeyEarly() { + testEvictWithKeyEarly(this.cs); + } + + @Test + public void testConditionalExpression() { + testConditionalExpression(this.cs); + } + + @Test + public void testConditionalExpressionSync() { + testConditionalExpressionSync(this.cs); + } + + @Test + public void testUnlessExpression() { + testUnlessExpression(this.cs); + } + + @Test + public void testClassCacheUnlessExpression() { + testUnlessExpression(this.cs); + } + + @Test + public void testKeyExpression() { + testKeyExpression(this.cs); + } + + @Test + public void testVarArgsKey() { + testVarArgsKey(this.cs); + } + + @Test + public void testClassCacheCacheable() { + testCacheable(this.ccs); + } + + @Test + public void testClassCacheEvict() { + testEvict(this.ccs, true); + } + + @Test + public void testClassEvictEarly() { + testEvictEarly(this.ccs); + } + + @Test + public void testClassEvictAll() { + testEvictAll(this.ccs, true); + } + + @Test + public void testClassEvictWithException() { + testEvictException(this.ccs); + } + + @Test + public void testClassCacheEvictWithWKey() { + testEvictWithKey(this.ccs); + } + + @Test + public void testClassEvictWithKeyEarly() { + testEvictWithKeyEarly(this.ccs); + } + + @Test + public void testNullValue() { + testNullValue(this.cs); + } + + @Test + public void testClassNullValue() { + Object key = new Object(); + assertThat(this.ccs.nullValue(key)).isNull(); + int nr = this.ccs.nullInvocations().intValue(); + assertThat(this.ccs.nullValue(key)).isNull(); + assertThat(this.ccs.nullInvocations().intValue()).isEqualTo(nr); + assertThat(this.ccs.nullValue(new Object())).isNull(); + // the check method is also cached + assertThat(this.ccs.nullInvocations().intValue()).isEqualTo(nr); + assertThat(AnnotatedClassCacheableService.nullInvocations.intValue()).isEqualTo(nr + 1); + } + + @Test + public void testMethodName() { + testMethodName(this.cs, "name"); + } + + @Test + public void testClassMethodName() { + testMethodName(this.ccs, "nametestCache"); + } + + @Test + public void testRootVars() { + testRootVars(this.cs); + } + + @Test + public void testClassRootVars() { + testRootVars(this.ccs); + } + + @Test + public void testCustomKeyGenerator() { + Object param = new Object(); + Object r1 = this.cs.customKeyGenerator(param); + assertThat(this.cs.customKeyGenerator(param)).isSameAs(r1); + Cache cache = this.cm.getCache("testCache"); + // Checks that the custom keyGenerator was used + Object expectedKey = SomeCustomKeyGenerator.generateKey("customKeyGenerator", param); + assertThat(cache.get(expectedKey)).isNotNull(); + } + + @Test + public void testUnknownCustomKeyGenerator() { + Object param = new Object(); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> + this.cs.unknownCustomKeyGenerator(param)); + } + + @Test + public void testCustomCacheManager() { + CacheManager customCm = this.ctx.getBean("customCacheManager", CacheManager.class); + Object key = new Object(); + Object r1 = this.cs.customCacheManager(key); + assertThat(this.cs.customCacheManager(key)).isSameAs(r1); + + Cache cache = customCm.getCache("testCache"); + assertThat(cache.get(key)).isNotNull(); + } + + @Test + public void testUnknownCustomCacheManager() { + Object param = new Object(); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> + this.cs.unknownCustomCacheManager(param)); + } + + @Test + public void testNullArg() { + testNullArg(this.cs); + } + + @Test + public void testClassNullArg() { + testNullArg(this.ccs); + } + + @Test + public void testCheckedException() { + testCheckedThrowable(this.cs); + } + + @Test + public void testClassCheckedException() { + testCheckedThrowable(this.ccs); + } + + @Test + public void testCheckedExceptionSync() { + testCheckedThrowableSync(this.cs); + } + + @Test + public void testClassCheckedExceptionSync() { + testCheckedThrowableSync(this.ccs); + } + + @Test + public void testUncheckedException() { + testUncheckedThrowable(this.cs); + } + + @Test + public void testClassUncheckedException() { + testUncheckedThrowable(this.ccs); + } + + @Test + public void testUncheckedExceptionSync() { + testUncheckedThrowableSync(this.cs); + } + + @Test + public void testClassUncheckedExceptionSync() { + testUncheckedThrowableSync(this.ccs); + } + + @Test + public void testUpdate() { + testCacheUpdate(this.cs); + } + + @Test + public void testClassUpdate() { + testCacheUpdate(this.ccs); + } + + @Test + public void testConditionalUpdate() { + testConditionalCacheUpdate(this.cs); + } + + @Test + public void testClassConditionalUpdate() { + testConditionalCacheUpdate(this.ccs); + } + + @Test + public void testMultiCache() { + testMultiCache(this.cs); + } + + @Test + public void testClassMultiCache() { + testMultiCache(this.ccs); + } + + @Test + public void testMultiEvict() { + testMultiEvict(this.cs); + } + + @Test + public void testClassMultiEvict() { + testMultiEvict(this.ccs); + } + + @Test + public void testMultiPut() { + testMultiPut(this.cs); + } + + @Test + public void testClassMultiPut() { + testMultiPut(this.ccs); + } + + @Test + public void testPutRefersToResult() { + testPutRefersToResult(this.cs); + } + + @Test + public void testClassPutRefersToResult() { + testPutRefersToResult(this.ccs); + } + + @Test + public void testMultiCacheAndEvict() { + testMultiCacheAndEvict(this.cs); + } + + @Test + public void testClassMultiCacheAndEvict() { + testMultiCacheAndEvict(this.ccs); + } + + @Test + public void testMultiConditionalCacheAndEvict() { + testMultiConditionalCacheAndEvict(this.cs); + } + + @Test + public void testClassMultiConditionalCacheAndEvict() { + testMultiConditionalCacheAndEvict(this.ccs); + } + +} diff --git a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJCacheAnnotationTests.java b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJCacheAnnotationTests.java new file mode 100644 index 0000000..4601c0e --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJCacheAnnotationTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.aspectj; + +import org.junit.jupiter.api.Test; + +import org.springframework.cache.Cache; +import org.springframework.cache.config.CacheableService; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.GenericXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Costin Leau + */ +public class AspectJCacheAnnotationTests extends AbstractCacheAnnotationTests { + + @Override + protected ConfigurableApplicationContext getApplicationContext() { + return new GenericXmlApplicationContext( + "/org/springframework/cache/config/annotation-cache-aspectj.xml"); + } + + @Test + public void testKeyStrategy() { + AnnotationCacheAspect aspect = ctx.getBean( + "org.springframework.cache.config.internalCacheAspect", AnnotationCacheAspect.class); + assertThat(aspect.getKeyGenerator()).isSameAs(ctx.getBean("keyGenerator")); + } + + @Override + protected void testMultiEvict(CacheableService service) { + Object o1 = new Object(); + + Object r1 = service.multiCache(o1); + Object r2 = service.multiCache(o1); + + Cache primary = cm.getCache("primary"); + Cache secondary = cm.getCache("secondary"); + + assertThat(r2).isSameAs(r1); + assertThat(primary.get(o1).get()).isSameAs(r1); + assertThat(secondary.get(o1).get()).isSameAs(r1); + + service.multiEvict(o1); + assertThat(primary.get(o1)).isNull(); + assertThat(secondary.get(o1)).isNull(); + + Object r3 = service.multiCache(o1); + Object r4 = service.multiCache(o1); + assertThat(r3).isNotSameAs(r1); + assertThat(r4).isSameAs(r3); + + assertThat(primary.get(o1).get()).isSameAs(r3); + assertThat(secondary.get(o1).get()).isSameAs(r4); + } + +} diff --git a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJEnableCachingIsolatedTests.java b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJEnableCachingIsolatedTests.java new file mode 100644 index 0000000..7ca1037 --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJEnableCachingIsolatedTests.java @@ -0,0 +1,285 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.aspectj; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.interceptor.CacheErrorHandler; +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.cache.interceptor.NamedCacheResolver; +import org.springframework.cache.interceptor.SimpleCacheErrorHandler; +import org.springframework.cache.interceptor.SimpleCacheResolver; +import org.springframework.cache.support.NoOpCacheManager; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AdviceMode; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.testfixture.cache.CacheTestUtils; +import org.springframework.context.testfixture.cache.SomeCustomKeyGenerator; +import org.springframework.context.testfixture.cache.SomeKeyGenerator; +import org.springframework.context.testfixture.cache.beans.AnnotatedClassCacheableService; +import org.springframework.context.testfixture.cache.beans.CacheableService; +import org.springframework.context.testfixture.cache.beans.DefaultCacheableService; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Stephane Nicoll + */ +public class AspectJEnableCachingIsolatedTests { + + private ConfigurableApplicationContext ctx; + + + private void load(Class... config) { + this.ctx = new AnnotationConfigApplicationContext(config); + } + + @AfterEach + public void closeContext() { + if (this.ctx != null) { + this.ctx.close(); + } + } + + + @Test + public void testKeyStrategy() { + load(EnableCachingConfig.class); + AnnotationCacheAspect aspect = this.ctx.getBean(AnnotationCacheAspect.class); + assertThat(aspect.getKeyGenerator()).isSameAs(this.ctx.getBean("keyGenerator", KeyGenerator.class)); + } + + @Test + public void testCacheErrorHandler() { + load(EnableCachingConfig.class); + AnnotationCacheAspect aspect = this.ctx.getBean(AnnotationCacheAspect.class); + assertThat(aspect.getErrorHandler()).isSameAs(this.ctx.getBean("errorHandler", CacheErrorHandler.class)); + } + + + // --- local tests ------- + + @Test + public void singleCacheManagerBean() { + load(SingleCacheManagerConfig.class); + } + + @Test + public void multipleCacheManagerBeans() { + try { + load(MultiCacheManagerConfig.class); + } + catch (IllegalStateException ex) { + assertThat(ex.getMessage().contains("bean of type CacheManager")).isTrue(); + } + } + + @Test + public void multipleCacheManagerBeans_implementsCachingConfigurer() { + load(MultiCacheManagerConfigurer.class); // does not throw + } + + @Test + public void multipleCachingConfigurers() { + try { + load(MultiCacheManagerConfigurer.class, EnableCachingConfig.class); + } + catch (BeanCreationException ex) { + Throwable root = ex.getRootCause(); + boolean condition = root instanceof IllegalStateException; + assertThat(condition).isTrue(); + assertThat(ex.getMessage().contains("implementations of CachingConfigurer")).isTrue(); + } + } + + @Test + public void noCacheManagerBeans() { + try { + load(EmptyConfig.class); + } + catch (IllegalStateException ex) { + assertThat(ex.getMessage().contains("no bean of type CacheManager")).isTrue(); + } + } + + @Test + @Disabled("AspectJ has some sort of caching that makes this one fail") + public void emptyConfigSupport() { + load(EmptyConfigSupportConfig.class); + AnnotationCacheAspect aspect = this.ctx.getBean(AnnotationCacheAspect.class); + assertThat(aspect.getCacheResolver()).isNotNull(); + assertThat(aspect.getCacheResolver().getClass()).isEqualTo(SimpleCacheResolver.class); + assertThat(((SimpleCacheResolver) aspect.getCacheResolver()).getCacheManager()).isSameAs(this.ctx.getBean(CacheManager.class)); + } + + @Test + public void bothSetOnlyResolverIsUsed() { + load(FullCachingConfig.class); + + AnnotationCacheAspect aspect = this.ctx.getBean(AnnotationCacheAspect.class); + assertThat(aspect.getCacheResolver()).isSameAs(this.ctx.getBean("cacheResolver")); + assertThat(aspect.getKeyGenerator()).isSameAs(this.ctx.getBean("keyGenerator")); + } + + + @Configuration + @EnableCaching(mode = AdviceMode.ASPECTJ) + static class EnableCachingConfig extends CachingConfigurerSupport { + + @Override + @Bean + public CacheManager cacheManager() { + return CacheTestUtils.createSimpleCacheManager("testCache", "primary", "secondary"); + } + + @Bean + public CacheableService service() { + return new DefaultCacheableService(); + } + + @Bean + public CacheableService classService() { + return new AnnotatedClassCacheableService(); + } + + @Override + @Bean + public KeyGenerator keyGenerator() { + return new SomeKeyGenerator(); + } + + @Override + @Bean + public CacheErrorHandler errorHandler() { + return new SimpleCacheErrorHandler(); + } + + @Bean + public KeyGenerator customKeyGenerator() { + return new SomeCustomKeyGenerator(); + } + + @Bean + public CacheManager customCacheManager() { + return CacheTestUtils.createSimpleCacheManager("testCache"); + } + } + + + @Configuration + @EnableCaching(mode = AdviceMode.ASPECTJ) + static class EmptyConfig { + } + + + @Configuration + @EnableCaching(mode = AdviceMode.ASPECTJ) + static class SingleCacheManagerConfig { + + @Bean + public CacheManager cm1() { + return new NoOpCacheManager(); + } + } + + + @Configuration + @EnableCaching(mode = AdviceMode.ASPECTJ) + static class MultiCacheManagerConfig { + + @Bean + public CacheManager cm1() { + return new NoOpCacheManager(); + } + + @Bean + public CacheManager cm2() { + return new NoOpCacheManager(); + } + } + + + @Configuration + @EnableCaching(mode = AdviceMode.ASPECTJ) + static class MultiCacheManagerConfigurer extends CachingConfigurerSupport { + + @Bean + public CacheManager cm1() { + return new NoOpCacheManager(); + } + + @Bean + public CacheManager cm2() { + return new NoOpCacheManager(); + } + + @Override + public CacheManager cacheManager() { + return cm1(); + } + + @Override + public KeyGenerator keyGenerator() { + return null; + } + } + + + @Configuration + @EnableCaching(mode = AdviceMode.ASPECTJ) + static class EmptyConfigSupportConfig extends CachingConfigurerSupport { + + @Bean + public CacheManager cm() { + return new NoOpCacheManager(); + } + + } + + + @Configuration + @EnableCaching(mode = AdviceMode.ASPECTJ) + static class FullCachingConfig extends CachingConfigurerSupport { + + @Override + @Bean + public CacheManager cacheManager() { + return new NoOpCacheManager(); + } + + @Override + @Bean + public KeyGenerator keyGenerator() { + return new SomeKeyGenerator(); + } + + @Override + @Bean + public CacheResolver cacheResolver() { + return new NamedCacheResolver(cacheManager(), "foo"); + } + } +} diff --git a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJEnableCachingTests.java b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJEnableCachingTests.java new file mode 100644 index 0000000..7e693d6 --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJEnableCachingTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.aspectj; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.config.AnnotatedClassCacheableService; +import org.springframework.cache.config.CacheableService; +import org.springframework.cache.config.DefaultCacheableService; +import org.springframework.cache.interceptor.CacheErrorHandler; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.cache.interceptor.SimpleCacheErrorHandler; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AdviceMode; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.testfixture.cache.CacheTestUtils; +import org.springframework.context.testfixture.cache.SomeCustomKeyGenerator; +import org.springframework.context.testfixture.cache.SomeKeyGenerator; + +/** + * @author Stephane Nicoll + */ +public class AspectJEnableCachingTests extends AbstractCacheAnnotationTests { + + @Override + protected ConfigurableApplicationContext getApplicationContext() { + return new AnnotationConfigApplicationContext(EnableCachingConfig.class); + } + + + @Configuration + @EnableCaching(mode = AdviceMode.ASPECTJ) + static class EnableCachingConfig extends CachingConfigurerSupport { + + @Override + @Bean + public CacheManager cacheManager() { + return CacheTestUtils.createSimpleCacheManager("testCache", "primary", "secondary"); + } + + @Bean + public CacheableService service() { + return new DefaultCacheableService(); + } + + @Bean + public CacheableService classService() { + return new AnnotatedClassCacheableService(); + } + + @Override + @Bean + public KeyGenerator keyGenerator() { + return new SomeKeyGenerator(); + } + + @Override + @Bean + public CacheErrorHandler errorHandler() { + return new SimpleCacheErrorHandler(); + } + + @Bean + public KeyGenerator customKeyGenerator() { + return new SomeCustomKeyGenerator(); + } + + @Bean + public CacheManager customCacheManager() { + return CacheTestUtils.createSimpleCacheManager("testCache"); + } + } + +} diff --git a/spring-aspects/src/test/java/org/springframework/cache/aspectj/JCacheAspectJJavaConfigTests.java b/spring-aspects/src/test/java/org/springframework/cache/aspectj/JCacheAspectJJavaConfigTests.java new file mode 100644 index 0000000..dc27836 --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/cache/aspectj/JCacheAspectJJavaConfigTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.aspectj; + +import java.util.Arrays; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.concurrent.ConcurrentMapCache; +import org.springframework.cache.config.AnnotatedJCacheableService; +import org.springframework.cache.support.SimpleCacheManager; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AdviceMode; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.contextsupport.testfixture.jcache.AbstractJCacheAnnotationTests; + +/** + * @author Stephane Nicoll + */ +public class JCacheAspectJJavaConfigTests extends AbstractJCacheAnnotationTests { + + @Override + protected ApplicationContext getApplicationContext() { + return new AnnotationConfigApplicationContext(EnableCachingConfig.class); + } + + + @Configuration + @EnableCaching(mode = AdviceMode.ASPECTJ) + public static class EnableCachingConfig { + + @Bean + public CacheManager cacheManager() { + SimpleCacheManager cm = new SimpleCacheManager(); + cm.setCaches(Arrays.asList( + defaultCache(), + new ConcurrentMapCache("primary"), + new ConcurrentMapCache("secondary"), + new ConcurrentMapCache("exception"))); + return cm; + } + + @Bean + public AnnotatedJCacheableService cacheableService() { + return new AnnotatedJCacheableService(defaultCache()); + } + + @Bean + public Cache defaultCache() { + return new ConcurrentMapCache("default"); + } + } + +} diff --git a/spring-aspects/src/test/java/org/springframework/cache/aspectj/JCacheAspectJNamespaceConfigTests.java b/spring-aspects/src/test/java/org/springframework/cache/aspectj/JCacheAspectJNamespaceConfigTests.java new file mode 100644 index 0000000..81ba6d0 --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/cache/aspectj/JCacheAspectJNamespaceConfigTests.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.aspectj; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.GenericXmlApplicationContext; +import org.springframework.contextsupport.testfixture.jcache.AbstractJCacheAnnotationTests; + +/** + * @author Stephane Nicoll + */ +public class JCacheAspectJNamespaceConfigTests extends AbstractJCacheAnnotationTests { + + @Override + protected ApplicationContext getApplicationContext() { + return new GenericXmlApplicationContext( + "/org/springframework/cache/config/annotation-jcache-aspectj.xml"); + } + +} diff --git a/spring-aspects/src/test/java/org/springframework/cache/config/AnnotatedClassCacheableService.java b/spring-aspects/src/test/java/org/springframework/cache/config/AnnotatedClassCacheableService.java new file mode 100644 index 0000000..fa494c9 --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/cache/config/AnnotatedClassCacheableService.java @@ -0,0 +1,242 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.config; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicLong; + +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.annotation.Caching; + +/** + * Copy of the shared {@code AbstractCacheAnnotationTests}: necessary + * due to issues with Gradle test fixtures and AspectJ configuration + * in the Gradle build. + * + * @author Costin Leau + * @author Phillip Webb + * @author Stephane Nicoll + */ +@Cacheable("testCache") +public class AnnotatedClassCacheableService implements CacheableService { + + private final AtomicLong counter = new AtomicLong(); + + public static final AtomicLong nullInvocations = new AtomicLong(); + + + @Override + public Object cache(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + public Object cacheNull(Object arg1) { + return null; + } + + @Override + @Cacheable(cacheNames = "testCache", sync = true) + public Object cacheSync(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", sync = true) + public Object cacheSyncNull(Object arg1) { + return null; + } + + @Override + public Object conditional(int field) { + return null; + } + + @Override + public Object conditionalSync(int field) { + return null; + } + + @Override + @Cacheable(cacheNames = "testCache", unless = "#result > 10") + public Object unless(int arg) { + return arg; + } + + @Override + @CacheEvict(cacheNames = "testCache", key = "#p0") + public void evict(Object arg1, Object arg2) { + } + + @Override + @CacheEvict("testCache") + public void evictWithException(Object arg1) { + throw new RuntimeException("exception thrown - evict should NOT occur"); + } + + @Override + @CacheEvict(cacheNames = "testCache", beforeInvocation = true) + public void evictEarly(Object arg1) { + throw new RuntimeException("exception thrown - evict should still occur"); + } + + @Override + @CacheEvict(cacheNames = "testCache", allEntries = true) + public void evictAll(Object arg1) { + } + + @Override + @CacheEvict(cacheNames = "testCache", allEntries = true, beforeInvocation = true) + public void evictAllEarly(Object arg1) { + throw new RuntimeException("exception thrown - evict should still occur"); + } + + @Override + @Cacheable(cacheNames = "testCache", key = "#p0") + public Object key(Object arg1, Object arg2) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable("testCache") + public Object varArgsKey(Object... args) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", key = "#root.methodName + #root.caches[0].name") + public Object name(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", key = "#root.methodName + #root.method.name + #root.targetClass + #root.target") + public Object rootVars(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", keyGenerator = "customKyeGenerator") + public Object customKeyGenerator(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", keyGenerator = "unknownBeanName") + public Object unknownCustomKeyGenerator(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", cacheManager = "customCacheManager") + public Object customCacheManager(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", cacheManager = "unknownBeanName") + public Object unknownCustomCacheManager(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @CachePut("testCache") + public Object update(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @CachePut(cacheNames = "testCache", condition = "#arg.equals(3)") + public Object conditionalUpdate(Object arg) { + return arg; + } + + @Override + public Object nullValue(Object arg1) { + nullInvocations.incrementAndGet(); + return null; + } + + @Override + public Number nullInvocations() { + return nullInvocations.get(); + } + + @Override + public Long throwChecked(Object arg1) throws Exception { + throw new IOException(arg1.toString()); + } + + @Override + public Long throwUnchecked(Object arg1) { + throw new UnsupportedOperationException(arg1.toString()); + } + + @Override + @Cacheable(cacheNames = "testCache", sync = true) + public Object throwCheckedSync(Object arg1) throws Exception { + throw new IOException(arg1.toString()); + } + + @Override + @Cacheable(cacheNames = "testCache", sync = true) + public Object throwUncheckedSync(Object arg1) { + throw new UnsupportedOperationException(arg1.toString()); + } + + // multi annotations + + @Override + @Caching(cacheable = { @Cacheable("primary"), @Cacheable("secondary") }) + public Object multiCache(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames = "secondary", key = "#a0"), @CacheEvict(cacheNames = "primary", key = "#p0 + 'A'") }) + public Object multiEvict(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Caching(cacheable = { @Cacheable(cacheNames = "primary", key = "#root.methodName") }, evict = { @CacheEvict("secondary") }) + public Object multiCacheAndEvict(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Caching(cacheable = { @Cacheable(cacheNames = "primary", condition = "#a0 == 3") }, evict = { @CacheEvict("secondary") }) + public Object multiConditionalCacheAndEvict(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Caching(put = { @CachePut("primary"), @CachePut("secondary") }) + public Object multiUpdate(Object arg1) { + return arg1; + } + + @Override + @CachePut(cacheNames = "primary", key = "#result.id") + public TestEntity putRefersToResult(TestEntity arg1) { + arg1.setId(Long.MIN_VALUE); + return arg1; + } + +} diff --git a/spring-aspects/src/test/java/org/springframework/cache/config/AnnotatedJCacheableService.java b/spring-aspects/src/test/java/org/springframework/cache/config/AnnotatedJCacheableService.java new file mode 100644 index 0000000..0b004f4 --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/cache/config/AnnotatedJCacheableService.java @@ -0,0 +1,220 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.config; + +import java.io.IOException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +import javax.cache.annotation.CacheDefaults; +import javax.cache.annotation.CacheKey; +import javax.cache.annotation.CachePut; +import javax.cache.annotation.CacheRemove; +import javax.cache.annotation.CacheRemoveAll; +import javax.cache.annotation.CacheResult; +import javax.cache.annotation.CacheValue; + +import org.springframework.cache.Cache; +import org.springframework.cache.interceptor.SimpleKeyGenerator; +import org.springframework.contextsupport.testfixture.cache.TestableCacheKeyGenerator; +import org.springframework.contextsupport.testfixture.cache.TestableCacheResolverFactory; +import org.springframework.contextsupport.testfixture.jcache.JCacheableService; + +/** + * Repository sample with a @CacheDefaults annotation + * + *

Note: copy/pasted from its original compilation because it needs to be + * processed by the AspectJ compiler to wave the required aspects. + * + * @author Stephane Nicoll + */ +@CacheDefaults(cacheName = "default") +public class AnnotatedJCacheableService implements JCacheableService { + + private final AtomicLong counter = new AtomicLong(); + private final AtomicLong exceptionCounter = new AtomicLong(); + private final Cache defaultCache; + + public AnnotatedJCacheableService(Cache defaultCache) { + this.defaultCache = defaultCache; + } + + @Override + @CacheResult + public Long cache(String id) { + return counter.getAndIncrement(); + } + + @Override + @CacheResult + public Long cacheNull(String id) { + return null; + } + + @Override + @CacheResult(exceptionCacheName = "exception", nonCachedExceptions = NullPointerException.class) + public Long cacheWithException(@CacheKey String id, boolean matchFilter) { + throwException(matchFilter); + return 0L; // Never reached + } + + @Override + @CacheResult(exceptionCacheName = "exception", nonCachedExceptions = NullPointerException.class) + public Long cacheWithCheckedException(@CacheKey String id, boolean matchFilter) throws IOException { + throwCheckedException(matchFilter); + return 0L; // Never reached + } + + @Override + @CacheResult(skipGet = true) + public Long cacheAlwaysInvoke(String id) { + return counter.getAndIncrement(); + } + + @Override + @CacheResult + public Long cacheWithPartialKey(@CacheKey String id, boolean notUsed) { + return counter.getAndIncrement(); + } + + @Override + @CacheResult(cacheResolverFactory = TestableCacheResolverFactory.class) + public Long cacheWithCustomCacheResolver(String id) { + return counter.getAndIncrement(); + } + + @Override + @CacheResult(cacheKeyGenerator = TestableCacheKeyGenerator.class) + public Long cacheWithCustomKeyGenerator(String id, String anotherId) { + return counter.getAndIncrement(); + } + + @Override + @CachePut + public void put(String id, @CacheValue Object value) { + } + + @Override + @CachePut(cacheFor = UnsupportedOperationException.class) + public void putWithException(@CacheKey String id, @CacheValue Object value, boolean matchFilter) { + throwException(matchFilter); + } + + @Override + @CachePut(afterInvocation = false) + public void earlyPut(String id, @CacheValue Object value) { + Object key = SimpleKeyGenerator.generateKey(id); + Cache.ValueWrapper valueWrapper = defaultCache.get(key); + if (valueWrapper == null) { + throw new AssertionError("Excepted value to be put in cache with key " + key); + } + Object actual = valueWrapper.get(); + if (value != actual) { // instance check on purpose + throw new AssertionError("Wrong value set in cache with key " + key + ". " + + "Expected=" + value + ", but got=" + actual); + } + } + + @Override + @CachePut(afterInvocation = false) + public void earlyPutWithException(@CacheKey String id, @CacheValue Object value, boolean matchFilter) { + throwException(matchFilter); + } + + @Override + @CacheRemove + public void remove(String id) { + } + + @Override + @CacheRemove(noEvictFor = NullPointerException.class) + public void removeWithException(@CacheKey String id, boolean matchFilter) { + throwException(matchFilter); + } + + @Override + @CacheRemove(afterInvocation = false) + public void earlyRemove(String id) { + Object key = SimpleKeyGenerator.generateKey(id); + Cache.ValueWrapper valueWrapper = defaultCache.get(key); + if (valueWrapper != null) { + throw new AssertionError("Value with key " + key + " expected to be already remove from cache"); + } + } + + @Override + @CacheRemove(afterInvocation = false, evictFor = UnsupportedOperationException.class) + public void earlyRemoveWithException(@CacheKey String id, boolean matchFilter) { + throwException(matchFilter); + } + + @Override + @CacheRemoveAll + public void removeAll() { + } + + @Override + @CacheRemoveAll(noEvictFor = NullPointerException.class) + public void removeAllWithException(boolean matchFilter) { + throwException(matchFilter); + } + + @Override + @CacheRemoveAll(afterInvocation = false) + public void earlyRemoveAll() { + ConcurrentHashMap nativeCache = (ConcurrentHashMap) defaultCache.getNativeCache(); + if (!nativeCache.isEmpty()) { + throw new AssertionError("Cache was expected to be empty"); + } + } + + @Override + @CacheRemoveAll(afterInvocation = false, evictFor = UnsupportedOperationException.class) + public void earlyRemoveAllWithException(boolean matchFilter) { + throwException(matchFilter); + } + + @Deprecated + public void noAnnotation() { + } + + @Override + public long exceptionInvocations() { + return exceptionCounter.get(); + } + + private void throwException(boolean matchFilter) { + long count = exceptionCounter.getAndIncrement(); + if (matchFilter) { + throw new UnsupportedOperationException("Expected exception (" + count + ")"); + } + else { + throw new NullPointerException("Expected exception (" + count + ")"); + } + } + + private void throwCheckedException(boolean matchFilter) throws IOException { + long count = exceptionCounter.getAndIncrement(); + if (matchFilter) { + throw new IOException("Expected exception (" + count + ")"); + } + else { + throw new NullPointerException("Expected exception (" + count + ")"); + } + } + +} diff --git a/spring-aspects/src/test/java/org/springframework/cache/config/CacheableService.java b/spring-aspects/src/test/java/org/springframework/cache/config/CacheableService.java new file mode 100644 index 0000000..3ec6212 --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/cache/config/CacheableService.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.config; + +/** + * Copy of the shared {@code CacheableService}: necessary + * due to issues with Gradle test fixtures and AspectJ configuration + * in the Gradle build. + * + *

Basic service interface for caching tests. + * + * @author Costin Leau + * @author Phillip Webb + * @author Stephane Nicoll + */ +public interface CacheableService { + + T cache(Object arg1); + + T cacheNull(Object arg1); + + T cacheSync(Object arg1); + + T cacheSyncNull(Object arg1); + + void evict(Object arg1, Object arg2); + + void evictWithException(Object arg1); + + void evictEarly(Object arg1); + + void evictAll(Object arg1); + + void evictAllEarly(Object arg1); + + T conditional(int field); + + T conditionalSync(int field); + + T unless(int arg); + + T key(Object arg1, Object arg2); + + T varArgsKey(Object... args); + + T name(Object arg1); + + T nullValue(Object arg1); + + T update(Object arg1); + + T conditionalUpdate(Object arg2); + + Number nullInvocations(); + + T rootVars(Object arg1); + + T customKeyGenerator(Object arg1); + + T unknownCustomKeyGenerator(Object arg1); + + T customCacheManager(Object arg1); + + T unknownCustomCacheManager(Object arg1); + + T throwChecked(Object arg1) throws Exception; + + T throwUnchecked(Object arg1); + + T throwCheckedSync(Object arg1) throws Exception; + + T throwUncheckedSync(Object arg1); + + T multiCache(Object arg1); + + T multiEvict(Object arg1); + + T multiCacheAndEvict(Object arg1); + + T multiConditionalCacheAndEvict(Object arg1); + + T multiUpdate(Object arg1); + + TestEntity putRefersToResult(TestEntity arg1); + +} diff --git a/spring-aspects/src/test/java/org/springframework/cache/config/DefaultCacheableService.java b/spring-aspects/src/test/java/org/springframework/cache/config/DefaultCacheableService.java new file mode 100644 index 0000000..47a3a83 --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/cache/config/DefaultCacheableService.java @@ -0,0 +1,250 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.config; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicLong; + +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.annotation.Caching; + +/** + * Copy of the shared {@code DefaultCacheableService}: necessary + * due to issues with Gradle test fixtures and AspectJ configuration + * in the Gradle build. + * + *

Simple cacheable service. + * + * @author Costin Leau + * @author Phillip Webb + * @author Stephane Nicoll + */ +public class DefaultCacheableService implements CacheableService { + + private final AtomicLong counter = new AtomicLong(); + + private final AtomicLong nullInvocations = new AtomicLong(); + + + @Override + @Cacheable("testCache") + public Long cache(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable("testCache") + public Long cacheNull(Object arg1) { + return null; + } + + @Override + @Cacheable(cacheNames = "testCache", sync = true) + public Long cacheSync(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", sync = true) + public Long cacheSyncNull(Object arg1) { + return null; + } + + @Override + @CacheEvict(cacheNames = "testCache", key = "#p0") + public void evict(Object arg1, Object arg2) { + } + + @Override + @CacheEvict("testCache") + public void evictWithException(Object arg1) { + throw new RuntimeException("exception thrown - evict should NOT occur"); + } + + @Override + @CacheEvict(cacheNames = "testCache", beforeInvocation = true) + public void evictEarly(Object arg1) { + throw new RuntimeException("exception thrown - evict should still occur"); + } + + @Override + @CacheEvict(cacheNames = "testCache", allEntries = true) + public void evictAll(Object arg1) { + } + + @Override + @CacheEvict(cacheNames = "testCache", allEntries = true, beforeInvocation = true) + public void evictAllEarly(Object arg1) { + throw new RuntimeException("exception thrown - evict should still occur"); + } + + @Override + @Cacheable(cacheNames = "testCache", condition = "#p0 == 3") + public Long conditional(int classField) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", sync = true, condition = "#p0 == 3") + public Long conditionalSync(int classField) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", unless = "#result > 10") + public Long unless(int arg) { + return (long) arg; + } + + @Override + @Cacheable(cacheNames = "testCache", key = "#p0") + public Long key(Object arg1, Object arg2) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache") + public Long varArgsKey(Object... args) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", key = "#root.methodName") + public Long name(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", key = "#root.methodName + #root.method.name + #root.targetClass + #root.target") + public Long rootVars(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", keyGenerator = "customKeyGenerator") + public Long customKeyGenerator(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", keyGenerator = "unknownBeanName") + public Long unknownCustomKeyGenerator(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", cacheManager = "customCacheManager") + public Long customCacheManager(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", cacheManager = "unknownBeanName") + public Long unknownCustomCacheManager(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @CachePut("testCache") + public Long update(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @CachePut(cacheNames = "testCache", condition = "#arg.equals(3)") + public Long conditionalUpdate(Object arg) { + return Long.valueOf(arg.toString()); + } + + @Override + @Cacheable("testCache") + public Long nullValue(Object arg1) { + this.nullInvocations.incrementAndGet(); + return null; + } + + @Override + public Number nullInvocations() { + return this.nullInvocations.get(); + } + + @Override + @Cacheable("testCache") + public Long throwChecked(Object arg1) throws Exception { + throw new IOException(arg1.toString()); + } + + @Override + @Cacheable("testCache") + public Long throwUnchecked(Object arg1) { + throw new UnsupportedOperationException(arg1.toString()); + } + + @Override + @Cacheable(cacheNames = "testCache", sync = true) + public Long throwCheckedSync(Object arg1) throws Exception { + throw new IOException(arg1.toString()); + } + + @Override + @Cacheable(cacheNames = "testCache", sync = true) + public Long throwUncheckedSync(Object arg1) { + throw new UnsupportedOperationException(arg1.toString()); + } + + // multi annotations + + @Override + @Caching(cacheable = { @Cacheable("primary"), @Cacheable("secondary") }) + public Long multiCache(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames = "secondary", key = "#p0"), @CacheEvict(cacheNames = "primary", key = "#p0 + 'A'") }) + public Long multiEvict(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Caching(cacheable = { @Cacheable(cacheNames = "primary", key = "#root.methodName") }, evict = { @CacheEvict("secondary") }) + public Long multiCacheAndEvict(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Caching(cacheable = { @Cacheable(cacheNames = "primary", condition = "#p0 == 3") }, evict = { @CacheEvict("secondary") }) + public Long multiConditionalCacheAndEvict(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Caching(put = { @CachePut("primary"), @CachePut("secondary") }) + public Long multiUpdate(Object arg1) { + return Long.valueOf(arg1.toString()); + } + + @Override + @CachePut(cacheNames = "primary", key = "#result.id") + public TestEntity putRefersToResult(TestEntity arg1) { + arg1.setId(Long.MIN_VALUE); + return arg1; + } + +} diff --git a/spring-aspects/src/test/java/org/springframework/cache/config/TestEntity.java b/spring-aspects/src/test/java/org/springframework/cache/config/TestEntity.java new file mode 100644 index 0000000..b308741 --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/cache/config/TestEntity.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.config; + +import org.springframework.util.ObjectUtils; + +/** + * Copy of the shared {@code TestEntity}: necessary + * due to issues with Gradle test fixtures and AspectJ configuration + * in the Gradle build. + * + *

Simple test entity for use with caching tests. + * + * @author Michael Plod + */ +public class TestEntity { + + private Long id; + + public Long getId() { + return this.id; + } + + public void setId(Long id) { + this.id = id; + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.id); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null) { + return false; + } + if (obj instanceof TestEntity) { + return ObjectUtils.nullSafeEquals(this.id, ((TestEntity) obj).id); + } + return false; + } +} diff --git a/spring-aspects/src/test/java/org/springframework/context/annotation/aspectj/AnnotationBeanConfigurerTests.java b/spring-aspects/src/test/java/org/springframework/context/annotation/aspectj/AnnotationBeanConfigurerTests.java new file mode 100644 index 0000000..ae781c8 --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/context/annotation/aspectj/AnnotationBeanConfigurerTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.aspectj; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.aspectj.ShouldBeConfiguredBySpring; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests that @EnableSpringConfigured properly registers an + * {@link org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect}, + * just as does {@code }. + * + * @author Chris Beams + * @since 3.1 + */ +public class AnnotationBeanConfigurerTests { + + @Test + public void injection() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class)) { + ShouldBeConfiguredBySpring myObject = new ShouldBeConfiguredBySpring(); + assertThat(myObject.getName()).isEqualTo("Rod"); + } + } + + + @Configuration + @ImportResource("org/springframework/beans/factory/aspectj/beanConfigurerTests-beans.xml") + @EnableSpringConfigured + static class Config { + } + +} diff --git a/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/AnnotationAsyncExecutionAspectTests.java b/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/AnnotationAsyncExecutionAspectTests.java new file mode 100644 index 0000000..624d96a --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/AnnotationAsyncExecutionAspectTests.java @@ -0,0 +1,291 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.aspectj; + +import java.lang.reflect.Method; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.core.testfixture.EnabledForTestGroups; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.AsyncResult; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.concurrent.ListenableFuture; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; + +/** + * Unit tests for {@link AnnotationAsyncExecutionAspect}. + * + * @author Ramnivas Laddad + * @author Stephane Nicoll + */ +@EnabledForTestGroups(LONG_RUNNING) +public class AnnotationAsyncExecutionAspectTests { + + private static final long WAIT_TIME = 1000; //milliseconds + + private final AsyncUncaughtExceptionHandler defaultExceptionHandler = new SimpleAsyncUncaughtExceptionHandler(); + + private CountingExecutor executor; + + + @BeforeEach + public void setUp() { + executor = new CountingExecutor(); + AnnotationAsyncExecutionAspect.aspectOf().setExecutor(executor); + } + + + @Test + public void asyncMethodGetsRoutedAsynchronously() { + ClassWithoutAsyncAnnotation obj = new ClassWithoutAsyncAnnotation(); + obj.incrementAsync(); + executor.waitForCompletion(); + assertThat(obj.counter).isEqualTo(1); + assertThat(executor.submitStartCounter).isEqualTo(1); + assertThat(executor.submitCompleteCounter).isEqualTo(1); + } + + @Test + public void asyncMethodReturningFutureGetsRoutedAsynchronouslyAndReturnsAFuture() throws InterruptedException, ExecutionException { + ClassWithoutAsyncAnnotation obj = new ClassWithoutAsyncAnnotation(); + Future future = obj.incrementReturningAFuture(); + // No need to executor.waitForCompletion() as future.get() will have the same effect + assertThat(future.get().intValue()).isEqualTo(5); + assertThat(obj.counter).isEqualTo(1); + assertThat(executor.submitStartCounter).isEqualTo(1); + assertThat(executor.submitCompleteCounter).isEqualTo(1); + } + + @Test + public void syncMethodGetsRoutedSynchronously() { + ClassWithoutAsyncAnnotation obj = new ClassWithoutAsyncAnnotation(); + obj.increment(); + assertThat(obj.counter).isEqualTo(1); + assertThat(executor.submitStartCounter).isEqualTo(0); + assertThat(executor.submitCompleteCounter).isEqualTo(0); + } + + @Test + public void voidMethodInAsyncClassGetsRoutedAsynchronously() { + ClassWithAsyncAnnotation obj = new ClassWithAsyncAnnotation(); + obj.increment(); + executor.waitForCompletion(); + assertThat(obj.counter).isEqualTo(1); + assertThat(executor.submitStartCounter).isEqualTo(1); + assertThat(executor.submitCompleteCounter).isEqualTo(1); + } + + @Test + public void methodReturningFutureInAsyncClassGetsRoutedAsynchronouslyAndReturnsAFuture() throws InterruptedException, ExecutionException { + ClassWithAsyncAnnotation obj = new ClassWithAsyncAnnotation(); + Future future = obj.incrementReturningAFuture(); + assertThat(future.get().intValue()).isEqualTo(5); + assertThat(obj.counter).isEqualTo(1); + assertThat(executor.submitStartCounter).isEqualTo(1); + assertThat(executor.submitCompleteCounter).isEqualTo(1); + } + + /* + @Test + public void methodReturningNonVoidNonFutureInAsyncClassGetsRoutedSynchronously() { + ClassWithAsyncAnnotation obj = new ClassWithAsyncAnnotation(); + int returnValue = obj.return5(); + assertEquals(5, returnValue); + assertEquals(0, executor.submitStartCounter); + assertEquals(0, executor.submitCompleteCounter); + } + */ + + @Test + public void qualifiedAsyncMethodsAreRoutedToCorrectExecutor() throws InterruptedException, ExecutionException { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition("e1", new RootBeanDefinition(ThreadPoolTaskExecutor.class)); + AnnotationAsyncExecutionAspect.aspectOf().setBeanFactory(beanFactory); + + ClassWithQualifiedAsyncMethods obj = new ClassWithQualifiedAsyncMethods(); + + Future defaultThread = obj.defaultWork(); + assertThat(defaultThread.get()).isNotEqualTo(Thread.currentThread()); + assertThat(defaultThread.get().getName()).doesNotStartWith("e1-"); + + ListenableFuture e1Thread = obj.e1Work(); + assertThat(e1Thread.get().getName()).startsWith("e1-"); + + CompletableFuture e1OtherThread = obj.e1OtherWork(); + assertThat(e1OtherThread.get().getName()).startsWith("e1-"); + } + + @Test + public void exceptionHandlerCalled() { + Method m = ReflectionUtils.findMethod(ClassWithException.class, "failWithVoid"); + TestableAsyncUncaughtExceptionHandler exceptionHandler = new TestableAsyncUncaughtExceptionHandler(); + AnnotationAsyncExecutionAspect.aspectOf().setExceptionHandler(exceptionHandler); + try { + assertThat(exceptionHandler.isCalled()).as("Handler should not have been called").isFalse(); + ClassWithException obj = new ClassWithException(); + obj.failWithVoid(); + exceptionHandler.await(3000); + exceptionHandler.assertCalledWith(m, UnsupportedOperationException.class); + } + finally { + AnnotationAsyncExecutionAspect.aspectOf().setExceptionHandler(defaultExceptionHandler); + } + } + + @Test + public void exceptionHandlerNeverThrowsUnexpectedException() { + Method m = ReflectionUtils.findMethod(ClassWithException.class, "failWithVoid"); + TestableAsyncUncaughtExceptionHandler exceptionHandler = new TestableAsyncUncaughtExceptionHandler(true); + AnnotationAsyncExecutionAspect.aspectOf().setExceptionHandler(exceptionHandler); + try { + assertThat(exceptionHandler.isCalled()).as("Handler should not have been called").isFalse(); + ClassWithException obj = new ClassWithException(); + obj.failWithVoid(); + exceptionHandler.await(3000); + exceptionHandler.assertCalledWith(m, UnsupportedOperationException.class); + } + finally { + AnnotationAsyncExecutionAspect.aspectOf().setExceptionHandler(defaultExceptionHandler); + } + } + + + @SuppressWarnings("serial") + private static class CountingExecutor extends SimpleAsyncTaskExecutor { + + int submitStartCounter; + + int submitCompleteCounter; + + @Override + public Future submit(Callable task) { + submitStartCounter++; + Future future = super.submit(task); + submitCompleteCounter++; + synchronized (this) { + notifyAll(); + } + return future; + } + + public synchronized void waitForCompletion() { + try { + wait(WAIT_TIME); + } + catch (InterruptedException ex) { + throw new AssertionError("Didn't finish the async job in " + WAIT_TIME + " milliseconds"); + } + } + } + + + static class ClassWithoutAsyncAnnotation { + + int counter; + + @Async public void incrementAsync() { + counter++; + } + + public void increment() { + counter++; + } + + @Async public Future incrementReturningAFuture() { + counter++; + return new AsyncResult(5); + } + + /** + * It should raise an error to attach @Async to a method that returns a non-void + * or non-Future. This method must remain commented-out, otherwise there will be a + * compile-time error. Uncomment to manually verify that the compiler produces an + * error message due to the 'declare error' statement in + * {@link AnnotationAsyncExecutionAspect}. + */ +// @Async public int getInt() { +// return 0; +// } + } + + + @Async + static class ClassWithAsyncAnnotation { + + int counter; + + public void increment() { + counter++; + } + + // Manually check that there is a warning from the 'declare warning' statement in + // AnnotationAsyncExecutionAspect + /* + public int return5() { + return 5; + } + */ + + public Future incrementReturningAFuture() { + counter++; + return new AsyncResult(5); + } + } + + + static class ClassWithQualifiedAsyncMethods { + + @Async + public Future defaultWork() { + return new AsyncResult(Thread.currentThread()); + } + + @Async("e1") + public ListenableFuture e1Work() { + return new AsyncResult(Thread.currentThread()); + } + + @Async("e1") + public CompletableFuture e1OtherWork() { + return CompletableFuture.completedFuture(Thread.currentThread()); + } + } + + + static class ClassWithException { + + @Async + public void failWithVoid() { + throw new UnsupportedOperationException("failWithVoid"); + } + } + +} diff --git a/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/AnnotationDrivenBeanDefinitionParserTests.java b/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/AnnotationDrivenBeanDefinitionParserTests.java new file mode 100644 index 0000000..20c22f4 --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/AnnotationDrivenBeanDefinitionParserTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.aspectj; + +import java.util.function.Supplier; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.scheduling.config.TaskManagementConfigUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Stephane Nicoll + */ +public class AnnotationDrivenBeanDefinitionParserTests { + + private ConfigurableApplicationContext context; + + @BeforeEach + public void setup() { + this.context = new ClassPathXmlApplicationContext( + "annotationDrivenContext.xml", AnnotationDrivenBeanDefinitionParserTests.class); + } + + @AfterEach + public void after() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void asyncAspectRegistered() { + assertThat(context.containsBean(TaskManagementConfigUtils.ASYNC_EXECUTION_ASPECT_BEAN_NAME)).isTrue(); + } + + @Test + @SuppressWarnings("rawtypes") + public void asyncPostProcessorExecutorReference() { + Object executor = context.getBean("testExecutor"); + Object aspect = context.getBean(TaskManagementConfigUtils.ASYNC_EXECUTION_ASPECT_BEAN_NAME); + assertThat(((Supplier) new DirectFieldAccessor(aspect).getPropertyValue("defaultExecutor")).get()).isSameAs(executor); + } + + @Test + @SuppressWarnings("rawtypes") + public void asyncPostProcessorExceptionHandlerReference() { + Object exceptionHandler = context.getBean("testExceptionHandler"); + Object aspect = context.getBean(TaskManagementConfigUtils.ASYNC_EXECUTION_ASPECT_BEAN_NAME); + assertThat(((Supplier) new DirectFieldAccessor(aspect).getPropertyValue("exceptionHandler")).get()).isSameAs(exceptionHandler); + } + +} diff --git a/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/TestableAsyncUncaughtExceptionHandler.java b/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/TestableAsyncUncaughtExceptionHandler.java new file mode 100644 index 0000000..616e429 --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/TestableAsyncUncaughtExceptionHandler.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.aspectj; + +import java.lang.reflect.Method; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * A {@link AsyncUncaughtExceptionHandler} implementation used for testing purposes. + * + * @author Stephane Nicoll + */ +class TestableAsyncUncaughtExceptionHandler + implements AsyncUncaughtExceptionHandler { + + private final CountDownLatch latch = new CountDownLatch(1); + + private UncaughtExceptionDescriptor descriptor; + + private final boolean throwUnexpectedException; + + TestableAsyncUncaughtExceptionHandler() { + this(false); + } + + TestableAsyncUncaughtExceptionHandler(boolean throwUnexpectedException) { + this.throwUnexpectedException = throwUnexpectedException; + } + + @Override + public void handleUncaughtException(Throwable ex, Method method, Object... params) { + descriptor = new UncaughtExceptionDescriptor(ex, method); + this.latch.countDown(); + if (throwUnexpectedException) { + throw new IllegalStateException("Test exception"); + } + } + + public boolean isCalled() { + return descriptor != null; + } + + public void assertCalledWith(Method expectedMethod, Class expectedExceptionType) { + assertThat(descriptor).as("Handler not called").isNotNull(); + assertThat(descriptor.ex.getClass()).as("Wrong exception type").isEqualTo(expectedExceptionType); + assertThat(descriptor.method).as("Wrong method").isEqualTo(expectedMethod); + } + + public void await(long timeout) { + try { + this.latch.await(timeout, TimeUnit.MILLISECONDS); + } + catch (Exception ex) { + Thread.currentThread().interrupt(); + } + } + + private static final class UncaughtExceptionDescriptor { + private final Throwable ex; + + private final Method method; + + private UncaughtExceptionDescriptor(Throwable ex, Method method) { + this.ex = ex; + this.method = method; + } + } +} diff --git a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/ClassWithPrivateAnnotatedMember.java b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/ClassWithPrivateAnnotatedMember.java new file mode 100644 index 0000000..7d5b2e6 --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/ClassWithPrivateAnnotatedMember.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.transaction.aspectj; + +import org.springframework.transaction.annotation.Transactional; + +/** + * @author Adrian Colyer + * @since 2.0 + */ +public class ClassWithPrivateAnnotatedMember { + + public void doSomething() { + doInTransaction(); + } + + @Transactional + private void doInTransaction() {} +} diff --git a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/ClassWithProtectedAnnotatedMember.java b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/ClassWithProtectedAnnotatedMember.java new file mode 100644 index 0000000..359eab2 --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/ClassWithProtectedAnnotatedMember.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.transaction.aspectj; + +import org.springframework.transaction.annotation.Transactional; + +/** + * @author Adrian Colyer + * @since 2.0 + */ +public class ClassWithProtectedAnnotatedMember { + + public void doSomething() { + doInTransaction(); + } + + @Transactional + protected void doInTransaction() {} +} diff --git a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/ITransactional.java b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/ITransactional.java new file mode 100644 index 0000000..e553c94 --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/ITransactional.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.transaction.aspectj; + +import org.springframework.transaction.annotation.Transactional; + +@Transactional +public interface ITransactional { + + Object echo(Throwable t) throws Throwable; + +} diff --git a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/JtaTransactionAspectsTests.java b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/JtaTransactionAspectsTests.java new file mode 100644 index 0000000..e0bd918 --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/JtaTransactionAspectsTests.java @@ -0,0 +1,167 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.transaction.aspectj; + +import java.io.IOException; + +import javax.transaction.Transactional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.transaction.testfixture.CallCountingTransactionManager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIOException; + +/** + * @author Stephane Nicoll + */ +@SpringJUnitConfig(JtaTransactionAspectsTests.Config.class) +public class JtaTransactionAspectsTests { + + @Autowired + private CallCountingTransactionManager txManager; + + @BeforeEach + public void setUp() { + this.txManager.clear(); + } + + @Test + public void commitOnAnnotatedPublicMethod() throws Throwable { + assertThat(this.txManager.begun).isEqualTo(0); + new JtaAnnotationPublicAnnotatedMember().echo(null); + assertThat(this.txManager.commits).isEqualTo(1); + } + + @Test + public void matchingRollbackOnApplied() throws Throwable { + assertThat(this.txManager.begun).isEqualTo(0); + InterruptedException test = new InterruptedException(); + assertThatExceptionOfType(InterruptedException.class).isThrownBy(() -> + new JtaAnnotationPublicAnnotatedMember().echo(test)) + .isSameAs(test); + assertThat(this.txManager.rollbacks).isEqualTo(1); + assertThat(this.txManager.commits).isEqualTo(0); + } + + @Test + public void nonMatchingRollbackOnApplied() throws Throwable { + assertThat(this.txManager.begun).isEqualTo(0); + IOException test = new IOException(); + assertThatIOException().isThrownBy(() -> + new JtaAnnotationPublicAnnotatedMember().echo(test)) + .isSameAs(test); + assertThat(this.txManager.commits).isEqualTo(1); + assertThat(this.txManager.rollbacks).isEqualTo(0); + } + + @Test + public void commitOnAnnotatedProtectedMethod() { + assertThat(this.txManager.begun).isEqualTo(0); + new JtaAnnotationProtectedAnnotatedMember().doInTransaction(); + assertThat(this.txManager.commits).isEqualTo(1); + } + + @Test + public void nonAnnotatedMethodCallingProtectedMethod() { + assertThat(this.txManager.begun).isEqualTo(0); + new JtaAnnotationProtectedAnnotatedMember().doSomething(); + assertThat(this.txManager.commits).isEqualTo(1); + } + + @Test + public void commitOnAnnotatedPrivateMethod() { + assertThat(this.txManager.begun).isEqualTo(0); + new JtaAnnotationPrivateAnnotatedMember().doInTransaction(); + assertThat(this.txManager.commits).isEqualTo(1); + } + + @Test + public void nonAnnotatedMethodCallingPrivateMethod() { + assertThat(this.txManager.begun).isEqualTo(0); + new JtaAnnotationPrivateAnnotatedMember().doSomething(); + assertThat(this.txManager.commits).isEqualTo(1); + } + + @Test + public void notTransactional() { + assertThat(this.txManager.begun).isEqualTo(0); + new TransactionAspectTests.NotTransactional().noop(); + assertThat(this.txManager.begun).isEqualTo(0); + } + + + public static class JtaAnnotationPublicAnnotatedMember { + + @Transactional(rollbackOn = InterruptedException.class) + public void echo(Throwable t) throws Throwable { + if (t != null) { + throw t; + } + } + + } + + + protected static class JtaAnnotationProtectedAnnotatedMember { + + public void doSomething() { + doInTransaction(); + } + + @Transactional + protected void doInTransaction() { + } + } + + + protected static class JtaAnnotationPrivateAnnotatedMember { + + public void doSomething() { + doInTransaction(); + } + + @Transactional + private void doInTransaction() { + } + } + + + @Configuration + protected static class Config { + + @Bean + public CallCountingTransactionManager transactionManager() { + return new CallCountingTransactionManager(); + } + + @Bean + public JtaAnnotationTransactionAspect transactionAspect() { + JtaAnnotationTransactionAspect aspect = JtaAnnotationTransactionAspect.aspectOf(); + aspect.setTransactionManager(transactionManager()); + return aspect; + } + } + +} diff --git a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/MethodAnnotationOnClassWithNoInterface.java b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/MethodAnnotationOnClassWithNoInterface.java new file mode 100644 index 0000000..35067b4 --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/MethodAnnotationOnClassWithNoInterface.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.transaction.aspectj; + +import org.springframework.transaction.annotation.Transactional; + +public class MethodAnnotationOnClassWithNoInterface { + + @Transactional(rollbackFor=InterruptedException.class) + public Object echo(Throwable t) throws Throwable { + if (t != null) { + throw t; + } + return t; + } + + public void noTransactionAttribute() { + + } + +} diff --git a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/TransactionAspectTests.java b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/TransactionAspectTests.java new file mode 100644 index 0000000..722c357 --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/TransactionAspectTests.java @@ -0,0 +1,206 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.transaction.aspectj; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.transaction.testfixture.CallCountingTransactionManager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Rod Johnson + * @author Ramnivas Laddad + * @author Juergen Hoeller + * @author Sam Brannen + */ +public class TransactionAspectTests { + + private final CallCountingTransactionManager txManager = new CallCountingTransactionManager(); + + private final TransactionalAnnotationOnlyOnClassWithNoInterface annotationOnlyOnClassWithNoInterface = + new TransactionalAnnotationOnlyOnClassWithNoInterface(); + + private final ClassWithProtectedAnnotatedMember beanWithAnnotatedProtectedMethod = + new ClassWithProtectedAnnotatedMember(); + + private final ClassWithPrivateAnnotatedMember beanWithAnnotatedPrivateMethod = + new ClassWithPrivateAnnotatedMember(); + + private final MethodAnnotationOnClassWithNoInterface methodAnnotationOnly = + new MethodAnnotationOnClassWithNoInterface(); + + + @BeforeEach + public void initContext() { + AnnotationTransactionAspect.aspectOf().setTransactionManager(txManager); + } + + + @Test + public void testCommitOnAnnotatedClass() throws Throwable { + txManager.clear(); + assertThat(txManager.begun).isEqualTo(0); + annotationOnlyOnClassWithNoInterface.echo(null); + assertThat(txManager.commits).isEqualTo(1); + } + + @Test + public void commitOnAnnotatedProtectedMethod() throws Throwable { + txManager.clear(); + assertThat(txManager.begun).isEqualTo(0); + beanWithAnnotatedProtectedMethod.doInTransaction(); + assertThat(txManager.commits).isEqualTo(1); + } + + @Test + public void commitOnAnnotatedPrivateMethod() throws Throwable { + txManager.clear(); + assertThat(txManager.begun).isEqualTo(0); + beanWithAnnotatedPrivateMethod.doSomething(); + assertThat(txManager.commits).isEqualTo(1); + } + + @Test + public void commitOnNonAnnotatedNonPublicMethodInTransactionalType() throws Throwable { + txManager.clear(); + assertThat(txManager.begun).isEqualTo(0); + annotationOnlyOnClassWithNoInterface.nonTransactionalMethod(); + assertThat(txManager.begun).isEqualTo(0); + } + + @Test + public void commitOnAnnotatedMethod() throws Throwable { + txManager.clear(); + assertThat(txManager.begun).isEqualTo(0); + methodAnnotationOnly.echo(null); + assertThat(txManager.commits).isEqualTo(1); + } + + @Test + public void notTransactional() throws Throwable { + txManager.clear(); + assertThat(txManager.begun).isEqualTo(0); + new NotTransactional().noop(); + assertThat(txManager.begun).isEqualTo(0); + } + + @Test + public void defaultCommitOnAnnotatedClass() throws Throwable { + Exception ex = new Exception(); + assertThatExceptionOfType(Exception.class).isThrownBy(() -> + testRollback(() -> annotationOnlyOnClassWithNoInterface.echo(ex), false)) + .isSameAs(ex); + } + + @Test + public void defaultRollbackOnAnnotatedClass() throws Throwable { + RuntimeException ex = new RuntimeException(); + assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> + testRollback(() -> annotationOnlyOnClassWithNoInterface.echo(ex), true)) + .isSameAs(ex); + } + + @Test + public void defaultCommitOnSubclassOfAnnotatedClass() throws Throwable { + Exception ex = new Exception(); + assertThatExceptionOfType(Exception.class).isThrownBy(() -> + testRollback(() -> new SubclassOfClassWithTransactionalAnnotation().echo(ex), false)) + .isSameAs(ex); + } + + @Test + public void defaultCommitOnSubclassOfClassWithTransactionalMethodAnnotated() throws Throwable { + Exception ex = new Exception(); + assertThatExceptionOfType(Exception.class).isThrownBy(() -> + testRollback(() -> new SubclassOfClassWithTransactionalMethodAnnotation().echo(ex), false)) + .isSameAs(ex); + } + + @Test + public void noCommitOnImplementationOfAnnotatedInterface() throws Throwable { + final Exception ex = new Exception(); + testNotTransactional(() -> new ImplementsAnnotatedInterface().echo(ex), ex); + } + + @Test + public void noRollbackOnImplementationOfAnnotatedInterface() throws Throwable { + final Exception rollbackProvokingException = new RuntimeException(); + testNotTransactional(() -> new ImplementsAnnotatedInterface().echo(rollbackProvokingException), + rollbackProvokingException); + } + + + protected void testRollback(TransactionOperationCallback toc, boolean rollback) throws Throwable { + txManager.clear(); + assertThat(txManager.begun).isEqualTo(0); + try { + toc.performTransactionalOperation(); + } + finally { + assertThat(txManager.begun).isEqualTo(1); + long expected1 = rollback ? 0 : 1; + assertThat(txManager.commits).isEqualTo(expected1); + long expected = rollback ? 1 : 0; + assertThat(txManager.rollbacks).isEqualTo(expected); + } + } + + protected void testNotTransactional(TransactionOperationCallback toc, Throwable expected) throws Throwable { + txManager.clear(); + assertThat(txManager.begun).isEqualTo(0); + assertThatExceptionOfType(Throwable.class).isThrownBy( + toc::performTransactionalOperation).isSameAs(expected); + assertThat(txManager.begun).isEqualTo(0); + } + + + private interface TransactionOperationCallback { + + Object performTransactionalOperation() throws Throwable; + } + + + public static class SubclassOfClassWithTransactionalAnnotation extends TransactionalAnnotationOnlyOnClassWithNoInterface { + } + + + public static class SubclassOfClassWithTransactionalMethodAnnotation extends MethodAnnotationOnClassWithNoInterface { + } + + + public static class ImplementsAnnotatedInterface implements ITransactional { + + @Override + public Object echo(Throwable t) throws Throwable { + if (t != null) { + throw t; + } + return t; + } + } + + + public static class NotTransactional { + + public void noop() { + } + } + +} diff --git a/spring-aspects/src/test/java/org/springframework/transaction/aspectj/TransactionalAnnotationOnlyOnClassWithNoInterface.java b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/TransactionalAnnotationOnlyOnClassWithNoInterface.java new file mode 100644 index 0000000..d9eeb7b --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/transaction/aspectj/TransactionalAnnotationOnlyOnClassWithNoInterface.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.transaction.aspectj; + +import org.springframework.transaction.annotation.Transactional; + +@Transactional +public class TransactionalAnnotationOnlyOnClassWithNoInterface { + + public Object echo(Throwable t) throws Throwable { + if (t != null) { + throw t; + } + return t; + } + + void nonTransactionalMethod() { + // no-op + } + +} + diff --git a/spring-aspects/src/test/resources/log4j2-test.xml b/spring-aspects/src/test/resources/log4j2-test.xml new file mode 100644 index 0000000..fb1c94a --- /dev/null +++ b/spring-aspects/src/test/resources/log4j2-test.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/spring-aspects/src/test/resources/org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml b/spring-aspects/src/test/resources/org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml new file mode 100644 index 0000000..6be707b --- /dev/null +++ b/spring-aspects/src/test/resources/org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/spring-aspects/src/test/resources/org/springframework/beans/factory/aspectj/beanConfigurerTests-beans.xml b/spring-aspects/src/test/resources/org/springframework/beans/factory/aspectj/beanConfigurerTests-beans.xml new file mode 100644 index 0000000..c996fa8 --- /dev/null +++ b/spring-aspects/src/test/resources/org/springframework/beans/factory/aspectj/beanConfigurerTests-beans.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/spring-aspects/src/test/resources/org/springframework/beans/factory/aspectj/beanConfigurerTests.xml b/spring-aspects/src/test/resources/org/springframework/beans/factory/aspectj/beanConfigurerTests.xml new file mode 100644 index 0000000..8d9b96b --- /dev/null +++ b/spring-aspects/src/test/resources/org/springframework/beans/factory/aspectj/beanConfigurerTests.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/spring-aspects/src/test/resources/org/springframework/beans/factory/aspectj/springConfigured.xml b/spring-aspects/src/test/resources/org/springframework/beans/factory/aspectj/springConfigured.xml new file mode 100644 index 0000000..6e786b2 --- /dev/null +++ b/spring-aspects/src/test/resources/org/springframework/beans/factory/aspectj/springConfigured.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/spring-aspects/src/test/resources/org/springframework/cache/config/annotation-cache-aspectj.xml b/spring-aspects/src/test/resources/org/springframework/cache/config/annotation-cache-aspectj.xml new file mode 100644 index 0000000..1a283f6 --- /dev/null +++ b/spring-aspects/src/test/resources/org/springframework/cache/config/annotation-cache-aspectj.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-aspects/src/test/resources/org/springframework/cache/config/annotation-jcache-aspectj.xml b/spring-aspects/src/test/resources/org/springframework/cache/config/annotation-jcache-aspectj.xml new file mode 100644 index 0000000..54ddbfd --- /dev/null +++ b/spring-aspects/src/test/resources/org/springframework/cache/config/annotation-jcache-aspectj.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-aspects/src/test/resources/org/springframework/scheduling/aspectj/annotationDrivenContext.xml b/spring-aspects/src/test/resources/org/springframework/scheduling/aspectj/annotationDrivenContext.xml new file mode 100644 index 0000000..61d1d3a --- /dev/null +++ b/spring-aspects/src/test/resources/org/springframework/scheduling/aspectj/annotationDrivenContext.xml @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/spring-aspects/src/test/resources/org/springframework/transaction/aspectj/TransactionAspectTests-context.xml b/spring-aspects/src/test/resources/org/springframework/transaction/aspectj/TransactionAspectTests-context.xml new file mode 100644 index 0000000..fa8ab8c --- /dev/null +++ b/spring-aspects/src/test/resources/org/springframework/transaction/aspectj/TransactionAspectTests-context.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/spring-beans/spring-beans.gradle b/spring-beans/spring-beans.gradle new file mode 100644 index 0000000..73e9942 --- /dev/null +++ b/spring-beans/spring-beans.gradle @@ -0,0 +1,40 @@ +description = "Spring Beans" + +apply plugin: "groovy" +apply plugin: "kotlin" + +dependencies { + compile(project(":spring-core")) + optional("javax.inject:javax.inject") + optional("org.yaml:snakeyaml") + optional("org.codehaus.groovy:groovy-xml") + optional("org.jetbrains.kotlin:kotlin-reflect") + optional("org.jetbrains.kotlin:kotlin-stdlib") + testCompile(testFixtures(project(":spring-core"))) + testCompile("javax.annotation:javax.annotation-api") + testFixturesApi("org.junit.jupiter:junit-jupiter-api") + testFixturesImplementation("org.assertj:assertj-core") +} + +// This module does joint compilation for Java and Groovy code with the compileGroovy task. +sourceSets { + main.groovy.srcDirs += "src/main/java" + main.java.srcDirs = [] +} + +compileGroovy { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + options.compilerArgs += "-Werror" +} + +// This module also builds Kotlin code and the compileKotlin task naturally depends on +// compileJava. We need to redefine dependencies to break task cycles. +tasks.named('compileGroovy') { + // Groovy only needs the declared dependencies (and not the result of Java compilation) + classpath = sourceSets.main.compileClasspath +} +tasks.named('compileKotlin') { + // Kotlin also depends on the result of Groovy compilation + classpath += files(sourceSets.main.groovy.classesDirectory) +} diff --git a/spring-beans/src/jmh/java/org/springframework/beans/AbstractPropertyAccessorBenchmark.java b/spring-beans/src/jmh/java/org/springframework/beans/AbstractPropertyAccessorBenchmark.java new file mode 100644 index 0000000..d407318 --- /dev/null +++ b/spring-beans/src/jmh/java/org/springframework/beans/AbstractPropertyAccessorBenchmark.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; + +import org.springframework.beans.propertyeditors.CustomNumberEditor; +import org.springframework.beans.propertyeditors.StringTrimmerEditor; + +/** + * Benchmark for {@link AbstractPropertyAccessor} use on beans. + * + * @author Brian Clozel + */ +@BenchmarkMode(Mode.Throughput) +public class AbstractPropertyAccessorBenchmark { + + @State(Scope.Benchmark) + public static class BenchmarkState { + + @Param({"DirectFieldAccessor", "BeanWrapper"}) + public String accessor; + + @Param({"none", "stringTrimmer", "numberOnPath", "numberOnNestedPath", "numberOnType"}) + public String customEditor; + + public int[] input; + + public PrimitiveArrayBean target; + + public AbstractPropertyAccessor propertyAccessor; + + @Setup + public void setup() { + this.target = new PrimitiveArrayBean(); + this.input = new int[1024]; + if (this.accessor.equals("DirectFieldAccessor")) { + this.propertyAccessor = new DirectFieldAccessor(this.target); + } + else { + this.propertyAccessor = new BeanWrapperImpl(this.target); + } + switch (this.customEditor) { + case "stringTrimmer": + this.propertyAccessor.registerCustomEditor(String.class, new StringTrimmerEditor(false)); + break; + case "numberOnPath": + this.propertyAccessor.registerCustomEditor(int.class, "array.somePath", new CustomNumberEditor(Integer.class, false)); + break; + case "numberOnNestedPath": + this.propertyAccessor.registerCustomEditor(int.class, "array[0].somePath", new CustomNumberEditor(Integer.class, false)); + break; + case "numberOnType": + this.propertyAccessor.registerCustomEditor(int.class, new CustomNumberEditor(Integer.class, false)); + break; + } + + } + + } + + @Benchmark + public PrimitiveArrayBean setPropertyValue(BenchmarkState state) { + state.propertyAccessor.setPropertyValue("array", state.input); + return state.target; + } + + @SuppressWarnings("unused") + private static class PrimitiveArrayBean { + + private int[] array; + + public int[] getArray() { + return this.array; + } + + public void setArray(int[] array) { + this.array = array; + } + } +} diff --git a/spring-beans/src/jmh/java/org/springframework/beans/factory/ConcurrentBeanFactoryBenchmark.java b/spring-beans/src/jmh/java/org/springframework/beans/factory/ConcurrentBeanFactoryBenchmark.java new file mode 100644 index 0000000..3214290 --- /dev/null +++ b/spring-beans/src/jmh/java/org/springframework/beans/factory/ConcurrentBeanFactoryBenchmark.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.infra.Blackhole; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.beans.propertyeditors.CustomDateEditor; + +import static org.springframework.core.testfixture.io.ResourceTestUtils.qualifiedResource; + +/** + * Benchmark for creating prototype beans in a concurrent fashion. + * This benchmark requires to customize the number of worker threads {@code -t } on the + * CLI when running this particular benchmark to leverage concurrency. + * + * @author Brian Clozel + */ +@BenchmarkMode(Mode.Throughput) +public class ConcurrentBeanFactoryBenchmark { + + @State(Scope.Benchmark) + public static class BenchmarkState { + + public DefaultListableBeanFactory factory; + + @Setup + public void setup() { + this.factory = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(this.factory).loadBeanDefinitions( + qualifiedResource(ConcurrentBeanFactoryBenchmark.class, "context.xml")); + + this.factory.addPropertyEditorRegistrar( + registry -> registry.registerCustomEditor(Date.class, + new CustomDateEditor(new SimpleDateFormat("yyyy/MM/dd"), false))); + } + + } + + @Benchmark + public void concurrentBeanCreation(BenchmarkState state, Blackhole bh) { + bh.consume(state.factory.getBean("bean1")); + bh.consume(state.factory.getBean("bean2")); + } + + + public static class ConcurrentBean { + + private Date date; + + public Date getDate() { + return this.date; + } + + public void setDate(Date date) { + this.date = date; + } + } +} diff --git a/spring-beans/src/jmh/java/org/springframework/beans/factory/DefaultListableBeanFactoryBenchmark.java b/spring-beans/src/jmh/java/org/springframework/beans/factory/DefaultListableBeanFactoryBenchmark.java new file mode 100644 index 0000000..ec828fb --- /dev/null +++ b/spring-beans/src/jmh/java/org/springframework/beans/factory/DefaultListableBeanFactoryBenchmark.java @@ -0,0 +1,142 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.LifecycleBean; +import org.springframework.beans.testfixture.beans.TestBean; + +/** + * Benchmark for retrieving various bean types from the {@link DefaultListableBeanFactory}. + * + * @author Brian Clozel + */ +@BenchmarkMode(Mode.Throughput) +public class DefaultListableBeanFactoryBenchmark { + + public static class Shared { + public DefaultListableBeanFactory beanFactory; + } + + @State(Scope.Benchmark) + public static class PrototypeCreationState extends Shared { + + @Param({"simple", "dependencyCheck", "constructor", "constructorArgument", "properties", "resolvedProperties"}) + public String mode; + + @Setup + public void setup() { + this.beanFactory = new DefaultListableBeanFactory(); + RootBeanDefinition rbd = new RootBeanDefinition(TestBean.class); + + switch (this.mode) { + case "simple": + break; + case "dependencyCheck": + rbd = new RootBeanDefinition(LifecycleBean.class); + rbd.setDependencyCheck(RootBeanDefinition.DEPENDENCY_CHECK_OBJECTS); + this.beanFactory.addBeanPostProcessor(new LifecycleBean.PostProcessor()); + break; + case "constructor": + rbd.getConstructorArgumentValues().addGenericArgumentValue("juergen"); + rbd.getConstructorArgumentValues().addGenericArgumentValue("99"); + break; + case "constructorArgument": + rbd.getConstructorArgumentValues().addGenericArgumentValue(new RuntimeBeanReference("spouse")); + this.beanFactory.registerBeanDefinition("test", rbd); + this.beanFactory.registerBeanDefinition("spouse", new RootBeanDefinition(TestBean.class)); + break; + case "properties": + rbd.getPropertyValues().add("name", "juergen"); + rbd.getPropertyValues().add("age", "99"); + break; + case "resolvedProperties": + rbd.getPropertyValues().add("spouse", new RuntimeBeanReference("spouse")); + this.beanFactory.registerBeanDefinition("spouse", new RootBeanDefinition(TestBean.class)); + break; + } + rbd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + this.beanFactory.registerBeanDefinition("test", rbd); + this.beanFactory.freezeConfiguration(); + } + + } + + @Benchmark + public Object prototypeCreation(PrototypeCreationState state) { + return state.beanFactory.getBean("test"); + } + + @State(Scope.Benchmark) + public static class SingletonLookupState extends Shared { + + @Setup + public void setup() { + this.beanFactory = new DefaultListableBeanFactory(); + this.beanFactory.registerBeanDefinition("test", new RootBeanDefinition(TestBean.class)); + this.beanFactory.freezeConfiguration(); + } + } + + @Benchmark + public Object singletLookup(SingletonLookupState state) { + return state.beanFactory.getBean("test"); + } + + @Benchmark + public Object singletLookupByType(SingletonLookupState state) { + return state.beanFactory.getBean(TestBean.class); + } + + @State(Scope.Benchmark) + public static class SingletonLookupManyBeansState extends Shared { + + @Setup + public void setup() { + this.beanFactory = new DefaultListableBeanFactory(); + this.beanFactory.registerBeanDefinition("test", new RootBeanDefinition(TestBean.class)); + for (int i = 0; i < 1000; i++) { + this.beanFactory.registerBeanDefinition("a" + i, new RootBeanDefinition(A.class)); + } + this.beanFactory.freezeConfiguration(); + } + } + + // See SPR-6870 + @Benchmark + public Object singletLookupByTypeManyBeans(SingletonLookupState state) { + return state.beanFactory.getBean(B.class); + } + + static class A { + } + + static class B { + } + +} diff --git a/spring-beans/src/jmh/resources/org/springframework/beans/factory/ConcurrentBeanFactoryBenchmark-context.xml b/spring-beans/src/jmh/resources/org/springframework/beans/factory/ConcurrentBeanFactoryBenchmark-context.xml new file mode 100644 index 0000000..8dceadf --- /dev/null +++ b/spring-beans/src/jmh/resources/org/springframework/beans/factory/ConcurrentBeanFactoryBenchmark-context.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + diff --git a/spring-beans/src/main/groovy/org/springframework/beans/factory/groovy/GroovyDynamicElementReader.groovy b/spring-beans/src/main/groovy/org/springframework/beans/factory/groovy/GroovyDynamicElementReader.groovy new file mode 100644 index 0000000..d017121 --- /dev/null +++ b/spring-beans/src/main/groovy/org/springframework/beans/factory/groovy/GroovyDynamicElementReader.groovy @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.groovy + +import groovy.xml.StreamingMarkupBuilder +import org.springframework.beans.factory.config.BeanDefinitionHolder +import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate +import org.w3c.dom.Element + +/** + * Used by GroovyBeanDefinitionReader to read a Spring XML namespace expression + * in the Groovy DSL. + * + * @author Jeff Brown + * @author Juergen Hoeller + * @since 4.0 + */ +@groovy.transform.PackageScope +class GroovyDynamicElementReader extends GroovyObjectSupport { + + private final String rootNamespace + + private final Map xmlNamespaces + + private final BeanDefinitionParserDelegate delegate + + private final GroovyBeanDefinitionWrapper beanDefinition + + protected final boolean decorating; + + private boolean callAfterInvocation = true + + + public GroovyDynamicElementReader(String namespace, Map namespaceMap, + BeanDefinitionParserDelegate delegate, GroovyBeanDefinitionWrapper beanDefinition, boolean decorating) { + super(); + this.rootNamespace = namespace + this.xmlNamespaces = namespaceMap + this.delegate = delegate + this.beanDefinition = beanDefinition; + this.decorating = decorating; + } + + + @Override + public Object invokeMethod(String name, Object args) { + if (name.equals("doCall")) { + def callable = args[0] + callable.resolveStrategy = Closure.DELEGATE_FIRST + callable.delegate = this + def result = callable.call() + + if (this.callAfterInvocation) { + afterInvocation() + this.callAfterInvocation = false + } + return result + } + + else { + StreamingMarkupBuilder builder = new StreamingMarkupBuilder(); + def myNamespace = this.rootNamespace + def myNamespaces = this.xmlNamespaces + + def callable = { + for (namespace in myNamespaces) { + mkp.declareNamespace([(namespace.key):namespace.value]) + } + if (args && (args[-1] instanceof Closure)) { + args[-1].resolveStrategy = Closure.DELEGATE_FIRST + args[-1].delegate = builder + } + delegate."$myNamespace"."$name"(*args) + } + + callable.resolveStrategy = Closure.DELEGATE_FIRST + callable.delegate = builder + def writable = builder.bind(callable) + def sw = new StringWriter() + writable.writeTo(sw) + + Element element = this.delegate.readerContext.readDocumentFromString(sw.toString()).documentElement + this.delegate.initDefaults(element) + if (this.decorating) { + BeanDefinitionHolder holder = this.beanDefinition.beanDefinitionHolder; + holder = this.delegate.decorateIfRequired(element, holder, null) + this.beanDefinition.setBeanDefinitionHolder(holder) + } + else { + def beanDefinition = this.delegate.parseCustomElement(element) + if (beanDefinition) { + this.beanDefinition.setBeanDefinition(beanDefinition) + } + } + if (this.callAfterInvocation) { + afterInvocation() + this.callAfterInvocation = false + } + return element + } + } + + /** + * Hook that subclass or anonymous classes can overwrite to implement custom behavior + * after invocation completes. + */ + protected void afterInvocation() { + // NOOP + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java b/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java new file mode 100644 index 0000000..16ab258 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java @@ -0,0 +1,1076 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.beans.PropertyChangeEvent; +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.lang.reflect.UndeclaredThrowableException; +import java.security.PrivilegedActionException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.CollectionFactory; +import org.springframework.core.ResolvableType; +import org.springframework.core.convert.ConversionException; +import org.springframework.core.convert.ConverterNotFoundException; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * A basic {@link ConfigurablePropertyAccessor} that provides the necessary + * infrastructure for all typical use cases. + * + *

This accessor will convert collection and array values to the corresponding + * target collections or arrays, if necessary. Custom property editors that deal + * with collections or arrays can either be written via PropertyEditor's + * {@code setValue}, or against a comma-delimited String via {@code setAsText}, + * as String arrays are converted in such a format if the array itself is not + * assignable. + * + * @author Juergen Hoeller + * @author Stephane Nicoll + * @author Rod Johnson + * @author Rob Harrop + * @since 4.2 + * @see #registerCustomEditor + * @see #setPropertyValues + * @see #setPropertyValue + * @see #getPropertyValue + * @see #getPropertyType + * @see BeanWrapper + * @see PropertyEditorRegistrySupport + */ +public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyAccessor { + + /** + * We'll create a lot of these objects, so we don't want a new logger every time. + */ + private static final Log logger = LogFactory.getLog(AbstractNestablePropertyAccessor.class); + + private int autoGrowCollectionLimit = Integer.MAX_VALUE; + + @Nullable + Object wrappedObject; + + private String nestedPath = ""; + + @Nullable + Object rootObject; + + /** Map with cached nested Accessors: nested path -> Accessor instance. */ + @Nullable + private Map nestedPropertyAccessors; + + + /** + * Create a new empty accessor. Wrapped instance needs to be set afterwards. + * Registers default editors. + * @see #setWrappedInstance + */ + protected AbstractNestablePropertyAccessor() { + this(true); + } + + /** + * Create a new empty accessor. Wrapped instance needs to be set afterwards. + * @param registerDefaultEditors whether to register default editors + * (can be suppressed if the accessor won't need any type conversion) + * @see #setWrappedInstance + */ + protected AbstractNestablePropertyAccessor(boolean registerDefaultEditors) { + if (registerDefaultEditors) { + registerDefaultEditors(); + } + this.typeConverterDelegate = new TypeConverterDelegate(this); + } + + /** + * Create a new accessor for the given object. + * @param object the object wrapped by this accessor + */ + protected AbstractNestablePropertyAccessor(Object object) { + registerDefaultEditors(); + setWrappedInstance(object); + } + + /** + * Create a new accessor, wrapping a new instance of the specified class. + * @param clazz class to instantiate and wrap + */ + protected AbstractNestablePropertyAccessor(Class clazz) { + registerDefaultEditors(); + setWrappedInstance(BeanUtils.instantiateClass(clazz)); + } + + /** + * Create a new accessor for the given object, + * registering a nested path that the object is in. + * @param object the object wrapped by this accessor + * @param nestedPath the nested path of the object + * @param rootObject the root object at the top of the path + */ + protected AbstractNestablePropertyAccessor(Object object, String nestedPath, Object rootObject) { + registerDefaultEditors(); + setWrappedInstance(object, nestedPath, rootObject); + } + + /** + * Create a new accessor for the given object, + * registering a nested path that the object is in. + * @param object the object wrapped by this accessor + * @param nestedPath the nested path of the object + * @param parent the containing accessor (must not be {@code null}) + */ + protected AbstractNestablePropertyAccessor(Object object, String nestedPath, AbstractNestablePropertyAccessor parent) { + setWrappedInstance(object, nestedPath, parent.getWrappedInstance()); + setExtractOldValueForEditor(parent.isExtractOldValueForEditor()); + setAutoGrowNestedPaths(parent.isAutoGrowNestedPaths()); + setAutoGrowCollectionLimit(parent.getAutoGrowCollectionLimit()); + setConversionService(parent.getConversionService()); + } + + + /** + * Specify a limit for array and collection auto-growing. + *

Default is unlimited on a plain accessor. + */ + public void setAutoGrowCollectionLimit(int autoGrowCollectionLimit) { + this.autoGrowCollectionLimit = autoGrowCollectionLimit; + } + + /** + * Return the limit for array and collection auto-growing. + */ + public int getAutoGrowCollectionLimit() { + return this.autoGrowCollectionLimit; + } + + /** + * Switch the target object, replacing the cached introspection results only + * if the class of the new object is different to that of the replaced object. + * @param object the new target object + */ + public void setWrappedInstance(Object object) { + setWrappedInstance(object, "", null); + } + + /** + * Switch the target object, replacing the cached introspection results only + * if the class of the new object is different to that of the replaced object. + * @param object the new target object + * @param nestedPath the nested path of the object + * @param rootObject the root object at the top of the path + */ + public void setWrappedInstance(Object object, @Nullable String nestedPath, @Nullable Object rootObject) { + this.wrappedObject = ObjectUtils.unwrapOptional(object); + Assert.notNull(this.wrappedObject, "Target object must not be null"); + this.nestedPath = (nestedPath != null ? nestedPath : ""); + this.rootObject = (!this.nestedPath.isEmpty() ? rootObject : this.wrappedObject); + this.nestedPropertyAccessors = null; + this.typeConverterDelegate = new TypeConverterDelegate(this, this.wrappedObject); + } + + public final Object getWrappedInstance() { + Assert.state(this.wrappedObject != null, "No wrapped object"); + return this.wrappedObject; + } + + public final Class getWrappedClass() { + return getWrappedInstance().getClass(); + } + + /** + * Return the nested path of the object wrapped by this accessor. + */ + public final String getNestedPath() { + return this.nestedPath; + } + + /** + * Return the root object at the top of the path of this accessor. + * @see #getNestedPath + */ + public final Object getRootInstance() { + Assert.state(this.rootObject != null, "No root object"); + return this.rootObject; + } + + /** + * Return the class of the root object at the top of the path of this accessor. + * @see #getNestedPath + */ + public final Class getRootClass() { + return getRootInstance().getClass(); + } + + @Override + public void setPropertyValue(String propertyName, @Nullable Object value) throws BeansException { + AbstractNestablePropertyAccessor nestedPa; + try { + nestedPa = getPropertyAccessorForPropertyPath(propertyName); + } + catch (NotReadablePropertyException ex) { + throw new NotWritablePropertyException(getRootClass(), this.nestedPath + propertyName, + "Nested property in path '" + propertyName + "' does not exist", ex); + } + PropertyTokenHolder tokens = getPropertyNameTokens(getFinalPath(nestedPa, propertyName)); + nestedPa.setPropertyValue(tokens, new PropertyValue(propertyName, value)); + } + + @Override + public void setPropertyValue(PropertyValue pv) throws BeansException { + PropertyTokenHolder tokens = (PropertyTokenHolder) pv.resolvedTokens; + if (tokens == null) { + String propertyName = pv.getName(); + AbstractNestablePropertyAccessor nestedPa; + try { + nestedPa = getPropertyAccessorForPropertyPath(propertyName); + } + catch (NotReadablePropertyException ex) { + throw new NotWritablePropertyException(getRootClass(), this.nestedPath + propertyName, + "Nested property in path '" + propertyName + "' does not exist", ex); + } + tokens = getPropertyNameTokens(getFinalPath(nestedPa, propertyName)); + if (nestedPa == this) { + pv.getOriginalPropertyValue().resolvedTokens = tokens; + } + nestedPa.setPropertyValue(tokens, pv); + } + else { + setPropertyValue(tokens, pv); + } + } + + protected void setPropertyValue(PropertyTokenHolder tokens, PropertyValue pv) throws BeansException { + if (tokens.keys != null) { + processKeyedProperty(tokens, pv); + } + else { + processLocalProperty(tokens, pv); + } + } + + @SuppressWarnings("unchecked") + private void processKeyedProperty(PropertyTokenHolder tokens, PropertyValue pv) { + Object propValue = getPropertyHoldingValue(tokens); + PropertyHandler ph = getLocalPropertyHandler(tokens.actualName); + if (ph == null) { + throw new InvalidPropertyException( + getRootClass(), this.nestedPath + tokens.actualName, "No property handler found"); + } + Assert.state(tokens.keys != null, "No token keys"); + String lastKey = tokens.keys[tokens.keys.length - 1]; + + if (propValue.getClass().isArray()) { + Class requiredType = propValue.getClass().getComponentType(); + int arrayIndex = Integer.parseInt(lastKey); + Object oldValue = null; + try { + if (isExtractOldValueForEditor() && arrayIndex < Array.getLength(propValue)) { + oldValue = Array.get(propValue, arrayIndex); + } + Object convertedValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(), + requiredType, ph.nested(tokens.keys.length)); + int length = Array.getLength(propValue); + if (arrayIndex >= length && arrayIndex < this.autoGrowCollectionLimit) { + Class componentType = propValue.getClass().getComponentType(); + Object newArray = Array.newInstance(componentType, arrayIndex + 1); + System.arraycopy(propValue, 0, newArray, 0, length); + setPropertyValue(tokens.actualName, newArray); + propValue = getPropertyValue(tokens.actualName); + } + Array.set(propValue, arrayIndex, convertedValue); + } + catch (IndexOutOfBoundsException ex) { + throw new InvalidPropertyException(getRootClass(), this.nestedPath + tokens.canonicalName, + "Invalid array index in property path '" + tokens.canonicalName + "'", ex); + } + } + + else if (propValue instanceof List) { + Class requiredType = ph.getCollectionType(tokens.keys.length); + List list = (List) propValue; + int index = Integer.parseInt(lastKey); + Object oldValue = null; + if (isExtractOldValueForEditor() && index < list.size()) { + oldValue = list.get(index); + } + Object convertedValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(), + requiredType, ph.nested(tokens.keys.length)); + int size = list.size(); + if (index >= size && index < this.autoGrowCollectionLimit) { + for (int i = size; i < index; i++) { + try { + list.add(null); + } + catch (NullPointerException ex) { + throw new InvalidPropertyException(getRootClass(), this.nestedPath + tokens.canonicalName, + "Cannot set element with index " + index + " in List of size " + + size + ", accessed using property path '" + tokens.canonicalName + + "': List does not support filling up gaps with null elements"); + } + } + list.add(convertedValue); + } + else { + try { + list.set(index, convertedValue); + } + catch (IndexOutOfBoundsException ex) { + throw new InvalidPropertyException(getRootClass(), this.nestedPath + tokens.canonicalName, + "Invalid list index in property path '" + tokens.canonicalName + "'", ex); + } + } + } + + else if (propValue instanceof Map) { + Class mapKeyType = ph.getMapKeyType(tokens.keys.length); + Class mapValueType = ph.getMapValueType(tokens.keys.length); + Map map = (Map) propValue; + // IMPORTANT: Do not pass full property name in here - property editors + // must not kick in for map keys but rather only for map values. + TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType); + Object convertedMapKey = convertIfNecessary(null, null, lastKey, mapKeyType, typeDescriptor); + Object oldValue = null; + if (isExtractOldValueForEditor()) { + oldValue = map.get(convertedMapKey); + } + // Pass full property name and old value in here, since we want full + // conversion ability for map values. + Object convertedMapValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(), + mapValueType, ph.nested(tokens.keys.length)); + map.put(convertedMapKey, convertedMapValue); + } + + else { + throw new InvalidPropertyException(getRootClass(), this.nestedPath + tokens.canonicalName, + "Property referenced in indexed property path '" + tokens.canonicalName + + "' is neither an array nor a List nor a Map; returned value was [" + propValue + "]"); + } + } + + private Object getPropertyHoldingValue(PropertyTokenHolder tokens) { + // Apply indexes and map keys: fetch value for all keys but the last one. + Assert.state(tokens.keys != null, "No token keys"); + PropertyTokenHolder getterTokens = new PropertyTokenHolder(tokens.actualName); + getterTokens.canonicalName = tokens.canonicalName; + getterTokens.keys = new String[tokens.keys.length - 1]; + System.arraycopy(tokens.keys, 0, getterTokens.keys, 0, tokens.keys.length - 1); + + Object propValue; + try { + propValue = getPropertyValue(getterTokens); + } + catch (NotReadablePropertyException ex) { + throw new NotWritablePropertyException(getRootClass(), this.nestedPath + tokens.canonicalName, + "Cannot access indexed value in property referenced " + + "in indexed property path '" + tokens.canonicalName + "'", ex); + } + + if (propValue == null) { + // null map value case + if (isAutoGrowNestedPaths()) { + int lastKeyIndex = tokens.canonicalName.lastIndexOf('['); + getterTokens.canonicalName = tokens.canonicalName.substring(0, lastKeyIndex); + propValue = setDefaultValue(getterTokens); + } + else { + throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + tokens.canonicalName, + "Cannot access indexed value in property referenced " + + "in indexed property path '" + tokens.canonicalName + "': returned null"); + } + } + return propValue; + } + + private void processLocalProperty(PropertyTokenHolder tokens, PropertyValue pv) { + PropertyHandler ph = getLocalPropertyHandler(tokens.actualName); + if (ph == null || !ph.isWritable()) { + if (pv.isOptional()) { + if (logger.isDebugEnabled()) { + logger.debug("Ignoring optional value for property '" + tokens.actualName + + "' - property not found on bean class [" + getRootClass().getName() + "]"); + } + return; + } + if (this.suppressNotWritablePropertyException) { + // Optimization for common ignoreUnknown=true scenario since the + // exception would be caught and swallowed higher up anyway... + return; + } + throw createNotWritablePropertyException(tokens.canonicalName); + } + + Object oldValue = null; + try { + Object originalValue = pv.getValue(); + Object valueToApply = originalValue; + if (!Boolean.FALSE.equals(pv.conversionNecessary)) { + if (pv.isConverted()) { + valueToApply = pv.getConvertedValue(); + } + else { + if (isExtractOldValueForEditor() && ph.isReadable()) { + try { + oldValue = ph.getValue(); + } + catch (Exception ex) { + if (ex instanceof PrivilegedActionException) { + ex = ((PrivilegedActionException) ex).getException(); + } + if (logger.isDebugEnabled()) { + logger.debug("Could not read previous value of property '" + + this.nestedPath + tokens.canonicalName + "'", ex); + } + } + } + valueToApply = convertForProperty( + tokens.canonicalName, oldValue, originalValue, ph.toTypeDescriptor()); + } + pv.getOriginalPropertyValue().conversionNecessary = (valueToApply != originalValue); + } + ph.setValue(valueToApply); + } + catch (TypeMismatchException ex) { + throw ex; + } + catch (InvocationTargetException ex) { + PropertyChangeEvent propertyChangeEvent = new PropertyChangeEvent( + getRootInstance(), this.nestedPath + tokens.canonicalName, oldValue, pv.getValue()); + if (ex.getTargetException() instanceof ClassCastException) { + throw new TypeMismatchException(propertyChangeEvent, ph.getPropertyType(), ex.getTargetException()); + } + else { + Throwable cause = ex.getTargetException(); + if (cause instanceof UndeclaredThrowableException) { + // May happen e.g. with Groovy-generated methods + cause = cause.getCause(); + } + throw new MethodInvocationException(propertyChangeEvent, cause); + } + } + catch (Exception ex) { + PropertyChangeEvent pce = new PropertyChangeEvent( + getRootInstance(), this.nestedPath + tokens.canonicalName, oldValue, pv.getValue()); + throw new MethodInvocationException(pce, ex); + } + } + + @Override + @Nullable + public Class getPropertyType(String propertyName) throws BeansException { + try { + PropertyHandler ph = getPropertyHandler(propertyName); + if (ph != null) { + return ph.getPropertyType(); + } + else { + // Maybe an indexed/mapped property... + Object value = getPropertyValue(propertyName); + if (value != null) { + return value.getClass(); + } + // Check to see if there is a custom editor, + // which might give an indication on the desired target type. + Class editorType = guessPropertyTypeFromEditors(propertyName); + if (editorType != null) { + return editorType; + } + } + } + catch (InvalidPropertyException ex) { + // Consider as not determinable. + } + return null; + } + + @Override + @Nullable + public TypeDescriptor getPropertyTypeDescriptor(String propertyName) throws BeansException { + try { + AbstractNestablePropertyAccessor nestedPa = getPropertyAccessorForPropertyPath(propertyName); + String finalPath = getFinalPath(nestedPa, propertyName); + PropertyTokenHolder tokens = getPropertyNameTokens(finalPath); + PropertyHandler ph = nestedPa.getLocalPropertyHandler(tokens.actualName); + if (ph != null) { + if (tokens.keys != null) { + if (ph.isReadable() || ph.isWritable()) { + return ph.nested(tokens.keys.length); + } + } + else { + if (ph.isReadable() || ph.isWritable()) { + return ph.toTypeDescriptor(); + } + } + } + } + catch (InvalidPropertyException ex) { + // Consider as not determinable. + } + return null; + } + + @Override + public boolean isReadableProperty(String propertyName) { + try { + PropertyHandler ph = getPropertyHandler(propertyName); + if (ph != null) { + return ph.isReadable(); + } + else { + // Maybe an indexed/mapped property... + getPropertyValue(propertyName); + return true; + } + } + catch (InvalidPropertyException ex) { + // Cannot be evaluated, so can't be readable. + } + return false; + } + + @Override + public boolean isWritableProperty(String propertyName) { + try { + PropertyHandler ph = getPropertyHandler(propertyName); + if (ph != null) { + return ph.isWritable(); + } + else { + // Maybe an indexed/mapped property... + getPropertyValue(propertyName); + return true; + } + } + catch (InvalidPropertyException ex) { + // Cannot be evaluated, so can't be writable. + } + return false; + } + + @Nullable + private Object convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, + @Nullable Object newValue, @Nullable Class requiredType, @Nullable TypeDescriptor td) + throws TypeMismatchException { + + Assert.state(this.typeConverterDelegate != null, "No TypeConverterDelegate"); + try { + return this.typeConverterDelegate.convertIfNecessary(propertyName, oldValue, newValue, requiredType, td); + } + catch (ConverterNotFoundException | IllegalStateException ex) { + PropertyChangeEvent pce = + new PropertyChangeEvent(getRootInstance(), this.nestedPath + propertyName, oldValue, newValue); + throw new ConversionNotSupportedException(pce, requiredType, ex); + } + catch (ConversionException | IllegalArgumentException ex) { + PropertyChangeEvent pce = + new PropertyChangeEvent(getRootInstance(), this.nestedPath + propertyName, oldValue, newValue); + throw new TypeMismatchException(pce, requiredType, ex); + } + } + + @Nullable + protected Object convertForProperty( + String propertyName, @Nullable Object oldValue, @Nullable Object newValue, TypeDescriptor td) + throws TypeMismatchException { + + return convertIfNecessary(propertyName, oldValue, newValue, td.getType(), td); + } + + @Override + @Nullable + public Object getPropertyValue(String propertyName) throws BeansException { + AbstractNestablePropertyAccessor nestedPa = getPropertyAccessorForPropertyPath(propertyName); + PropertyTokenHolder tokens = getPropertyNameTokens(getFinalPath(nestedPa, propertyName)); + return nestedPa.getPropertyValue(tokens); + } + + @SuppressWarnings("unchecked") + @Nullable + protected Object getPropertyValue(PropertyTokenHolder tokens) throws BeansException { + String propertyName = tokens.canonicalName; + String actualName = tokens.actualName; + PropertyHandler ph = getLocalPropertyHandler(actualName); + if (ph == null || !ph.isReadable()) { + throw new NotReadablePropertyException(getRootClass(), this.nestedPath + propertyName); + } + try { + Object value = ph.getValue(); + if (tokens.keys != null) { + if (value == null) { + if (isAutoGrowNestedPaths()) { + value = setDefaultValue(new PropertyTokenHolder(tokens.actualName)); + } + else { + throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + propertyName, + "Cannot access indexed value of property referenced in indexed " + + "property path '" + propertyName + "': returned null"); + } + } + StringBuilder indexedPropertyName = new StringBuilder(tokens.actualName); + // apply indexes and map keys + for (int i = 0; i < tokens.keys.length; i++) { + String key = tokens.keys[i]; + if (value == null) { + throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + propertyName, + "Cannot access indexed value of property referenced in indexed " + + "property path '" + propertyName + "': returned null"); + } + else if (value.getClass().isArray()) { + int index = Integer.parseInt(key); + value = growArrayIfNecessary(value, index, indexedPropertyName.toString()); + value = Array.get(value, index); + } + else if (value instanceof List) { + int index = Integer.parseInt(key); + List list = (List) value; + growCollectionIfNecessary(list, index, indexedPropertyName.toString(), ph, i + 1); + value = list.get(index); + } + else if (value instanceof Set) { + // Apply index to Iterator in case of a Set. + Set set = (Set) value; + int index = Integer.parseInt(key); + if (index < 0 || index >= set.size()) { + throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, + "Cannot get element with index " + index + " from Set of size " + + set.size() + ", accessed using property path '" + propertyName + "'"); + } + Iterator it = set.iterator(); + for (int j = 0; it.hasNext(); j++) { + Object elem = it.next(); + if (j == index) { + value = elem; + break; + } + } + } + else if (value instanceof Map) { + Map map = (Map) value; + Class mapKeyType = ph.getResolvableType().getNested(i + 1).asMap().resolveGeneric(0); + // IMPORTANT: Do not pass full property name in here - property editors + // must not kick in for map keys but rather only for map values. + TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType); + Object convertedMapKey = convertIfNecessary(null, null, key, mapKeyType, typeDescriptor); + value = map.get(convertedMapKey); + } + else { + throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, + "Property referenced in indexed property path '" + propertyName + + "' is neither an array nor a List nor a Set nor a Map; returned value was [" + value + "]"); + } + indexedPropertyName.append(PROPERTY_KEY_PREFIX).append(key).append(PROPERTY_KEY_SUFFIX); + } + } + return value; + } + catch (IndexOutOfBoundsException ex) { + throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, + "Index of out of bounds in property path '" + propertyName + "'", ex); + } + catch (NumberFormatException | TypeMismatchException ex) { + throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, + "Invalid index in property path '" + propertyName + "'", ex); + } + catch (InvocationTargetException ex) { + throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, + "Getter for property '" + actualName + "' threw exception", ex); + } + catch (Exception ex) { + throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, + "Illegal attempt to get property '" + actualName + "' threw exception", ex); + } + } + + + /** + * Return the {@link PropertyHandler} for the specified {@code propertyName}, navigating + * if necessary. Return {@code null} if not found rather than throwing an exception. + * @param propertyName the property to obtain the descriptor for + * @return the property descriptor for the specified property, + * or {@code null} if not found + * @throws BeansException in case of introspection failure + */ + @Nullable + protected PropertyHandler getPropertyHandler(String propertyName) throws BeansException { + Assert.notNull(propertyName, "Property name must not be null"); + AbstractNestablePropertyAccessor nestedPa = getPropertyAccessorForPropertyPath(propertyName); + return nestedPa.getLocalPropertyHandler(getFinalPath(nestedPa, propertyName)); + } + + /** + * Return a {@link PropertyHandler} for the specified local {@code propertyName}. + * Only used to reach a property available in the current context. + * @param propertyName the name of a local property + * @return the handler for that property, or {@code null} if it has not been found + */ + @Nullable + protected abstract PropertyHandler getLocalPropertyHandler(String propertyName); + + /** + * Create a new nested property accessor instance. + * Can be overridden in subclasses to create a PropertyAccessor subclass. + * @param object the object wrapped by this PropertyAccessor + * @param nestedPath the nested path of the object + * @return the nested PropertyAccessor instance + */ + protected abstract AbstractNestablePropertyAccessor newNestedPropertyAccessor(Object object, String nestedPath); + + /** + * Create a {@link NotWritablePropertyException} for the specified property. + */ + protected abstract NotWritablePropertyException createNotWritablePropertyException(String propertyName); + + + private Object growArrayIfNecessary(Object array, int index, String name) { + if (!isAutoGrowNestedPaths()) { + return array; + } + int length = Array.getLength(array); + if (index >= length && index < this.autoGrowCollectionLimit) { + Class componentType = array.getClass().getComponentType(); + Object newArray = Array.newInstance(componentType, index + 1); + System.arraycopy(array, 0, newArray, 0, length); + for (int i = length; i < Array.getLength(newArray); i++) { + Array.set(newArray, i, newValue(componentType, null, name)); + } + setPropertyValue(name, newArray); + Object defaultValue = getPropertyValue(name); + Assert.state(defaultValue != null, "Default value must not be null"); + return defaultValue; + } + else { + return array; + } + } + + private void growCollectionIfNecessary(Collection collection, int index, String name, + PropertyHandler ph, int nestingLevel) { + + if (!isAutoGrowNestedPaths()) { + return; + } + int size = collection.size(); + if (index >= size && index < this.autoGrowCollectionLimit) { + Class elementType = ph.getResolvableType().getNested(nestingLevel).asCollection().resolveGeneric(); + if (elementType != null) { + for (int i = collection.size(); i < index + 1; i++) { + collection.add(newValue(elementType, null, name)); + } + } + } + } + + /** + * Get the last component of the path. Also works if not nested. + * @param pa property accessor to work on + * @param nestedPath property path we know is nested + * @return last component of the path (the property on the target bean) + */ + protected String getFinalPath(AbstractNestablePropertyAccessor pa, String nestedPath) { + if (pa == this) { + return nestedPath; + } + return nestedPath.substring(PropertyAccessorUtils.getLastNestedPropertySeparatorIndex(nestedPath) + 1); + } + + /** + * Recursively navigate to return a property accessor for the nested property path. + * @param propertyPath property path, which may be nested + * @return a property accessor for the target bean + */ + protected AbstractNestablePropertyAccessor getPropertyAccessorForPropertyPath(String propertyPath) { + int pos = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(propertyPath); + // Handle nested properties recursively. + if (pos > -1) { + String nestedProperty = propertyPath.substring(0, pos); + String nestedPath = propertyPath.substring(pos + 1); + AbstractNestablePropertyAccessor nestedPa = getNestedPropertyAccessor(nestedProperty); + return nestedPa.getPropertyAccessorForPropertyPath(nestedPath); + } + else { + return this; + } + } + + /** + * Retrieve a Property accessor for the given nested property. + * Create a new one if not found in the cache. + *

Note: Caching nested PropertyAccessors is necessary now, + * to keep registered custom editors for nested properties. + * @param nestedProperty property to create the PropertyAccessor for + * @return the PropertyAccessor instance, either cached or newly created + */ + private AbstractNestablePropertyAccessor getNestedPropertyAccessor(String nestedProperty) { + if (this.nestedPropertyAccessors == null) { + this.nestedPropertyAccessors = new HashMap<>(); + } + // Get value of bean property. + PropertyTokenHolder tokens = getPropertyNameTokens(nestedProperty); + String canonicalName = tokens.canonicalName; + Object value = getPropertyValue(tokens); + if (value == null || (value instanceof Optional && !((Optional) value).isPresent())) { + if (isAutoGrowNestedPaths()) { + value = setDefaultValue(tokens); + } + else { + throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + canonicalName); + } + } + + // Lookup cached sub-PropertyAccessor, create new one if not found. + AbstractNestablePropertyAccessor nestedPa = this.nestedPropertyAccessors.get(canonicalName); + if (nestedPa == null || nestedPa.getWrappedInstance() != ObjectUtils.unwrapOptional(value)) { + if (logger.isTraceEnabled()) { + logger.trace("Creating new nested " + getClass().getSimpleName() + " for property '" + canonicalName + "'"); + } + nestedPa = newNestedPropertyAccessor(value, this.nestedPath + canonicalName + NESTED_PROPERTY_SEPARATOR); + // Inherit all type-specific PropertyEditors. + copyDefaultEditorsTo(nestedPa); + copyCustomEditorsTo(nestedPa, canonicalName); + this.nestedPropertyAccessors.put(canonicalName, nestedPa); + } + else { + if (logger.isTraceEnabled()) { + logger.trace("Using cached nested property accessor for property '" + canonicalName + "'"); + } + } + return nestedPa; + } + + private Object setDefaultValue(PropertyTokenHolder tokens) { + PropertyValue pv = createDefaultPropertyValue(tokens); + setPropertyValue(tokens, pv); + Object defaultValue = getPropertyValue(tokens); + Assert.state(defaultValue != null, "Default value must not be null"); + return defaultValue; + } + + private PropertyValue createDefaultPropertyValue(PropertyTokenHolder tokens) { + TypeDescriptor desc = getPropertyTypeDescriptor(tokens.canonicalName); + if (desc == null) { + throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + tokens.canonicalName, + "Could not determine property type for auto-growing a default value"); + } + Object defaultValue = newValue(desc.getType(), desc, tokens.canonicalName); + return new PropertyValue(tokens.canonicalName, defaultValue); + } + + private Object newValue(Class type, @Nullable TypeDescriptor desc, String name) { + try { + if (type.isArray()) { + Class componentType = type.getComponentType(); + // TODO - only handles 2-dimensional arrays + if (componentType.isArray()) { + Object array = Array.newInstance(componentType, 1); + Array.set(array, 0, Array.newInstance(componentType.getComponentType(), 0)); + return array; + } + else { + return Array.newInstance(componentType, 0); + } + } + else if (Collection.class.isAssignableFrom(type)) { + TypeDescriptor elementDesc = (desc != null ? desc.getElementTypeDescriptor() : null); + return CollectionFactory.createCollection(type, (elementDesc != null ? elementDesc.getType() : null), 16); + } + else if (Map.class.isAssignableFrom(type)) { + TypeDescriptor keyDesc = (desc != null ? desc.getMapKeyTypeDescriptor() : null); + return CollectionFactory.createMap(type, (keyDesc != null ? keyDesc.getType() : null), 16); + } + else { + Constructor ctor = type.getDeclaredConstructor(); + if (Modifier.isPrivate(ctor.getModifiers())) { + throw new IllegalAccessException("Auto-growing not allowed with private constructor: " + ctor); + } + return BeanUtils.instantiateClass(ctor); + } + } + catch (Throwable ex) { + throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + name, + "Could not instantiate property type [" + type.getName() + "] to auto-grow nested property path", ex); + } + } + + /** + * Parse the given property name into the corresponding property name tokens. + * @param propertyName the property name to parse + * @return representation of the parsed property tokens + */ + private PropertyTokenHolder getPropertyNameTokens(String propertyName) { + String actualName = null; + List keys = new ArrayList<>(2); + int searchIndex = 0; + while (searchIndex != -1) { + int keyStart = propertyName.indexOf(PROPERTY_KEY_PREFIX, searchIndex); + searchIndex = -1; + if (keyStart != -1) { + int keyEnd = getPropertyNameKeyEnd(propertyName, keyStart + PROPERTY_KEY_PREFIX.length()); + if (keyEnd != -1) { + if (actualName == null) { + actualName = propertyName.substring(0, keyStart); + } + String key = propertyName.substring(keyStart + PROPERTY_KEY_PREFIX.length(), keyEnd); + if (key.length() > 1 && (key.startsWith("'") && key.endsWith("'")) || + (key.startsWith("\"") && key.endsWith("\""))) { + key = key.substring(1, key.length() - 1); + } + keys.add(key); + searchIndex = keyEnd + PROPERTY_KEY_SUFFIX.length(); + } + } + } + PropertyTokenHolder tokens = new PropertyTokenHolder(actualName != null ? actualName : propertyName); + if (!keys.isEmpty()) { + tokens.canonicalName += PROPERTY_KEY_PREFIX + + StringUtils.collectionToDelimitedString(keys, PROPERTY_KEY_SUFFIX + PROPERTY_KEY_PREFIX) + + PROPERTY_KEY_SUFFIX; + tokens.keys = StringUtils.toStringArray(keys); + } + return tokens; + } + + private int getPropertyNameKeyEnd(String propertyName, int startIndex) { + int unclosedPrefixes = 0; + int length = propertyName.length(); + for (int i = startIndex; i < length; i++) { + switch (propertyName.charAt(i)) { + case PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR: + // The property name contains opening prefix(es)... + unclosedPrefixes++; + break; + case PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR: + if (unclosedPrefixes == 0) { + // No unclosed prefix(es) in the property name (left) -> + // this is the suffix we are looking for. + return i; + } + else { + // This suffix does not close the initial prefix but rather + // just one that occurred within the property name. + unclosedPrefixes--; + } + break; + } + } + return -1; + } + + + @Override + public String toString() { + String className = getClass().getName(); + if (this.wrappedObject == null) { + return className + ": no wrapped object set"; + } + return className + ": wrapping object [" + ObjectUtils.identityToString(this.wrappedObject) + ']'; + } + + + /** + * A handler for a specific property. + */ + protected abstract static class PropertyHandler { + + private final Class propertyType; + + private final boolean readable; + + private final boolean writable; + + public PropertyHandler(Class propertyType, boolean readable, boolean writable) { + this.propertyType = propertyType; + this.readable = readable; + this.writable = writable; + } + + public Class getPropertyType() { + return this.propertyType; + } + + public boolean isReadable() { + return this.readable; + } + + public boolean isWritable() { + return this.writable; + } + + public abstract TypeDescriptor toTypeDescriptor(); + + public abstract ResolvableType getResolvableType(); + + @Nullable + public Class getMapKeyType(int nestingLevel) { + return getResolvableType().getNested(nestingLevel).asMap().resolveGeneric(0); + } + + @Nullable + public Class getMapValueType(int nestingLevel) { + return getResolvableType().getNested(nestingLevel).asMap().resolveGeneric(1); + } + + @Nullable + public Class getCollectionType(int nestingLevel) { + return getResolvableType().getNested(nestingLevel).asCollection().resolveGeneric(); + } + + @Nullable + public abstract TypeDescriptor nested(int level); + + @Nullable + public abstract Object getValue() throws Exception; + + public abstract void setValue(@Nullable Object value) throws Exception; + } + + + /** + * Holder class used to store property tokens. + */ + protected static class PropertyTokenHolder { + + public PropertyTokenHolder(String name) { + this.actualName = name; + this.canonicalName = name; + } + + public String actualName; + + public String canonicalName; + + @Nullable + public String[] keys; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/AbstractPropertyAccessor.java b/spring-beans/src/main/java/org/springframework/beans/AbstractPropertyAccessor.java new file mode 100644 index 0000000..1d6b5f4 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/AbstractPropertyAccessor.java @@ -0,0 +1,172 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.springframework.lang.Nullable; + +/** + * Abstract implementation of the {@link PropertyAccessor} interface. + * Provides base implementations of all convenience methods, with the + * implementation of actual property access left to subclasses. + * + * @author Juergen Hoeller + * @author Stephane Nicoll + * @since 2.0 + * @see #getPropertyValue + * @see #setPropertyValue + */ +public abstract class AbstractPropertyAccessor extends TypeConverterSupport implements ConfigurablePropertyAccessor { + + private boolean extractOldValueForEditor = false; + + private boolean autoGrowNestedPaths = false; + + boolean suppressNotWritablePropertyException = false; + + + @Override + public void setExtractOldValueForEditor(boolean extractOldValueForEditor) { + this.extractOldValueForEditor = extractOldValueForEditor; + } + + @Override + public boolean isExtractOldValueForEditor() { + return this.extractOldValueForEditor; + } + + @Override + public void setAutoGrowNestedPaths(boolean autoGrowNestedPaths) { + this.autoGrowNestedPaths = autoGrowNestedPaths; + } + + @Override + public boolean isAutoGrowNestedPaths() { + return this.autoGrowNestedPaths; + } + + + @Override + public void setPropertyValue(PropertyValue pv) throws BeansException { + setPropertyValue(pv.getName(), pv.getValue()); + } + + @Override + public void setPropertyValues(Map map) throws BeansException { + setPropertyValues(new MutablePropertyValues(map)); + } + + @Override + public void setPropertyValues(PropertyValues pvs) throws BeansException { + setPropertyValues(pvs, false, false); + } + + @Override + public void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown) throws BeansException { + setPropertyValues(pvs, ignoreUnknown, false); + } + + @Override + public void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown, boolean ignoreInvalid) + throws BeansException { + + List propertyAccessExceptions = null; + List propertyValues = (pvs instanceof MutablePropertyValues ? + ((MutablePropertyValues) pvs).getPropertyValueList() : Arrays.asList(pvs.getPropertyValues())); + + if (ignoreUnknown) { + this.suppressNotWritablePropertyException = true; + } + try { + for (PropertyValue pv : propertyValues) { + // setPropertyValue may throw any BeansException, which won't be caught + // here, if there is a critical failure such as no matching field. + // We can attempt to deal only with less serious exceptions. + try { + setPropertyValue(pv); + } + catch (NotWritablePropertyException ex) { + if (!ignoreUnknown) { + throw ex; + } + // Otherwise, just ignore it and continue... + } + catch (NullValueInNestedPathException ex) { + if (!ignoreInvalid) { + throw ex; + } + // Otherwise, just ignore it and continue... + } + catch (PropertyAccessException ex) { + if (propertyAccessExceptions == null) { + propertyAccessExceptions = new ArrayList<>(); + } + propertyAccessExceptions.add(ex); + } + } + } + finally { + if (ignoreUnknown) { + this.suppressNotWritablePropertyException = false; + } + } + + // If we encountered individual exceptions, throw the composite exception. + if (propertyAccessExceptions != null) { + PropertyAccessException[] paeArray = propertyAccessExceptions.toArray(new PropertyAccessException[0]); + throw new PropertyBatchUpdateException(paeArray); + } + } + + + // Redefined with public visibility. + @Override + @Nullable + public Class getPropertyType(String propertyPath) { + return null; + } + + /** + * Actually get the value of a property. + * @param propertyName name of the property to get the value of + * @return the value of the property + * @throws InvalidPropertyException if there is no such property or + * if the property isn't readable + * @throws PropertyAccessException if the property was valid but the + * accessor method failed + */ + @Override + @Nullable + public abstract Object getPropertyValue(String propertyName) throws BeansException; + + /** + * Actually set a property value. + * @param propertyName name of the property to set value of + * @param value the new value + * @throws InvalidPropertyException if there is no such property or + * if the property isn't writable + * @throws PropertyAccessException if the property was valid but the + * accessor method failed or a type mismatch occurred + */ + @Override + public abstract void setPropertyValue(String propertyName, @Nullable Object value) throws BeansException; + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanInfoFactory.java b/spring-beans/src/main/java/org/springframework/beans/BeanInfoFactory.java new file mode 100644 index 0000000..3ad632b --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/BeanInfoFactory.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; + +import org.springframework.lang.Nullable; + +/** + * Strategy interface for creating {@link BeanInfo} instances for Spring beans. + * Can be used to plug in custom bean property resolution strategies (e.g. for other + * languages on the JVM) or more efficient {@link BeanInfo} retrieval algorithms. + * + *

BeanInfoFactories are instantiated by the {@link CachedIntrospectionResults}, + * by using the {@link org.springframework.core.io.support.SpringFactoriesLoader} + * utility class. + * + * When a {@link BeanInfo} is to be created, the {@code CachedIntrospectionResults} + * will iterate through the discovered factories, calling {@link #getBeanInfo(Class)} + * on each one. If {@code null} is returned, the next factory will be queried. + * If none of the factories support the class, a standard {@link BeanInfo} will be + * created as a default. + * + *

Note that the {@link org.springframework.core.io.support.SpringFactoriesLoader} + * sorts the {@code BeanInfoFactory} instances by + * {@link org.springframework.core.annotation.Order @Order}, so that ones with a + * higher precedence come first. + * + * @author Arjen Poutsma + * @since 3.2 + * @see CachedIntrospectionResults + * @see org.springframework.core.io.support.SpringFactoriesLoader + */ +public interface BeanInfoFactory { + + /** + * Return the bean info for the given class, if supported. + * @param beanClass the bean class + * @return the BeanInfo, or {@code null} if the given class is not supported + * @throws IntrospectionException in case of exceptions + */ + @Nullable + BeanInfo getBeanInfo(Class beanClass) throws IntrospectionException; + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanInstantiationException.java b/spring-beans/src/main/java/org/springframework/beans/BeanInstantiationException.java new file mode 100644 index 0000000..10bcf80 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/BeanInstantiationException.java @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +import org.springframework.lang.Nullable; + +/** + * Exception thrown when instantiation of a bean failed. + * Carries the offending bean class. + * + * @author Juergen Hoeller + * @since 1.2.8 + */ +@SuppressWarnings("serial") +public class BeanInstantiationException extends FatalBeanException { + + private final Class beanClass; + + @Nullable + private final Constructor constructor; + + @Nullable + private final Method constructingMethod; + + + /** + * Create a new BeanInstantiationException. + * @param beanClass the offending bean class + * @param msg the detail message + */ + public BeanInstantiationException(Class beanClass, String msg) { + this(beanClass, msg, null); + } + + /** + * Create a new BeanInstantiationException. + * @param beanClass the offending bean class + * @param msg the detail message + * @param cause the root cause + */ + public BeanInstantiationException(Class beanClass, String msg, @Nullable Throwable cause) { + super("Failed to instantiate [" + beanClass.getName() + "]: " + msg, cause); + this.beanClass = beanClass; + this.constructor = null; + this.constructingMethod = null; + } + + /** + * Create a new BeanInstantiationException. + * @param constructor the offending constructor + * @param msg the detail message + * @param cause the root cause + * @since 4.3 + */ + public BeanInstantiationException(Constructor constructor, String msg, @Nullable Throwable cause) { + super("Failed to instantiate [" + constructor.getDeclaringClass().getName() + "]: " + msg, cause); + this.beanClass = constructor.getDeclaringClass(); + this.constructor = constructor; + this.constructingMethod = null; + } + + /** + * Create a new BeanInstantiationException. + * @param constructingMethod the delegate for bean construction purposes + * (typically, but not necessarily, a static factory method) + * @param msg the detail message + * @param cause the root cause + * @since 4.3 + */ + public BeanInstantiationException(Method constructingMethod, String msg, @Nullable Throwable cause) { + super("Failed to instantiate [" + constructingMethod.getReturnType().getName() + "]: " + msg, cause); + this.beanClass = constructingMethod.getReturnType(); + this.constructor = null; + this.constructingMethod = constructingMethod; + } + + + /** + * Return the offending bean class (never {@code null}). + * @return the class that was to be instantiated + */ + public Class getBeanClass() { + return this.beanClass; + } + + /** + * Return the offending constructor, if known. + * @return the constructor in use, or {@code null} in case of a + * factory method or in case of default instantiation + * @since 4.3 + */ + @Nullable + public Constructor getConstructor() { + return this.constructor; + } + + /** + * Return the delegate for bean construction purposes, if known. + * @return the method in use (typically a static factory method), + * or {@code null} in case of constructor-based instantiation + * @since 4.3 + */ + @Nullable + public Method getConstructingMethod() { + return this.constructingMethod; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanMetadataAttribute.java b/spring-beans/src/main/java/org/springframework/beans/BeanMetadataAttribute.java new file mode 100644 index 0000000..db6435d --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/BeanMetadataAttribute.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Holder for a key-value style attribute that is part of a bean definition. + * Keeps track of the definition source in addition to the key-value pair. + * + * @author Juergen Hoeller + * @since 2.5 + */ +public class BeanMetadataAttribute implements BeanMetadataElement { + + private final String name; + + @Nullable + private final Object value; + + @Nullable + private Object source; + + + /** + * Create a new AttributeValue instance. + * @param name the name of the attribute (never {@code null}) + * @param value the value of the attribute (possibly before type conversion) + */ + public BeanMetadataAttribute(String name, @Nullable Object value) { + Assert.notNull(name, "Name must not be null"); + this.name = name; + this.value = value; + } + + + /** + * Return the name of the attribute. + */ + public String getName() { + return this.name; + } + + /** + * Return the value of the attribute. + */ + @Nullable + public Object getValue() { + return this.value; + } + + /** + * Set the configuration source {@code Object} for this metadata element. + *

The exact type of the object will depend on the configuration mechanism used. + */ + public void setSource(@Nullable Object source) { + this.source = source; + } + + @Override + @Nullable + public Object getSource() { + return this.source; + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof BeanMetadataAttribute)) { + return false; + } + BeanMetadataAttribute otherMa = (BeanMetadataAttribute) other; + return (this.name.equals(otherMa.name) && + ObjectUtils.nullSafeEquals(this.value, otherMa.value) && + ObjectUtils.nullSafeEquals(this.source, otherMa.source)); + } + + @Override + public int hashCode() { + return this.name.hashCode() * 29 + ObjectUtils.nullSafeHashCode(this.value); + } + + @Override + public String toString() { + return "metadata attribute '" + this.name + "'"; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanMetadataAttributeAccessor.java b/spring-beans/src/main/java/org/springframework/beans/BeanMetadataAttributeAccessor.java new file mode 100644 index 0000000..58409cb --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/BeanMetadataAttributeAccessor.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import org.springframework.core.AttributeAccessorSupport; +import org.springframework.lang.Nullable; + +/** + * Extension of {@link org.springframework.core.AttributeAccessorSupport}, + * holding attributes as {@link BeanMetadataAttribute} objects in order + * to keep track of the definition source. + * + * @author Juergen Hoeller + * @since 2.5 + */ +@SuppressWarnings("serial") +public class BeanMetadataAttributeAccessor extends AttributeAccessorSupport implements BeanMetadataElement { + + @Nullable + private Object source; + + + /** + * Set the configuration source {@code Object} for this metadata element. + *

The exact type of the object will depend on the configuration mechanism used. + */ + public void setSource(@Nullable Object source) { + this.source = source; + } + + @Override + @Nullable + public Object getSource() { + return this.source; + } + + + /** + * Add the given BeanMetadataAttribute to this accessor's set of attributes. + * @param attribute the BeanMetadataAttribute object to register + */ + public void addMetadataAttribute(BeanMetadataAttribute attribute) { + super.setAttribute(attribute.getName(), attribute); + } + + /** + * Look up the given BeanMetadataAttribute in this accessor's set of attributes. + * @param name the name of the attribute + * @return the corresponding BeanMetadataAttribute object, + * or {@code null} if no such attribute defined + */ + @Nullable + public BeanMetadataAttribute getMetadataAttribute(String name) { + return (BeanMetadataAttribute) super.getAttribute(name); + } + + @Override + public void setAttribute(String name, @Nullable Object value) { + super.setAttribute(name, new BeanMetadataAttribute(name, value)); + } + + @Override + @Nullable + public Object getAttribute(String name) { + BeanMetadataAttribute attribute = (BeanMetadataAttribute) super.getAttribute(name); + return (attribute != null ? attribute.getValue() : null); + } + + @Override + @Nullable + public Object removeAttribute(String name) { + BeanMetadataAttribute attribute = (BeanMetadataAttribute) super.removeAttribute(name); + return (attribute != null ? attribute.getValue() : null); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanMetadataElement.java b/spring-beans/src/main/java/org/springframework/beans/BeanMetadataElement.java new file mode 100644 index 0000000..7126c64 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/BeanMetadataElement.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import org.springframework.lang.Nullable; + +/** + * Interface to be implemented by bean metadata elements + * that carry a configuration source object. + * + * @author Juergen Hoeller + * @since 2.0 + */ +public interface BeanMetadataElement { + + /** + * Return the configuration source {@code Object} for this metadata element + * (may be {@code null}). + */ + @Nullable + default Object getSource() { + return null; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java new file mode 100644 index 0000000..3980a24 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java @@ -0,0 +1,860 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.beans.ConstructorProperties; +import java.beans.PropertyDescriptor; +import java.beans.PropertyEditor; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.URI; +import java.net.URL; +import java.time.temporal.Temporal; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import kotlin.jvm.JvmClassMappingKt; +import kotlin.reflect.KFunction; +import kotlin.reflect.KParameter; +import kotlin.reflect.full.KClasses; +import kotlin.reflect.jvm.KCallablesJvm; +import kotlin.reflect.jvm.ReflectJvmMapping; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.KotlinDetector; +import org.springframework.core.MethodParameter; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.core.ResolvableType; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * Static convenience methods for JavaBeans: for instantiating beans, + * checking bean property types, copying bean properties, etc. + * + *

Mainly for internal use within the framework, but to some degree also + * useful for application classes. Consider + * Apache Commons BeanUtils, + * BULL - Bean Utils Light Library, + * or similar third-party frameworks for more comprehensive bean utilities. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Rob Harrop + * @author Sam Brannen + * @author Sebastien Deleuze + */ +public abstract class BeanUtils { + + private static final Log logger = LogFactory.getLog(BeanUtils.class); + + private static final ParameterNameDiscoverer parameterNameDiscoverer = + new DefaultParameterNameDiscoverer(); + + private static final Set> unknownEditorTypes = + Collections.newSetFromMap(new ConcurrentReferenceHashMap<>(64)); + + private static final Map, Object> DEFAULT_TYPE_VALUES; + + static { + Map, Object> values = new HashMap<>(); + values.put(boolean.class, false); + values.put(byte.class, (byte) 0); + values.put(short.class, (short) 0); + values.put(int.class, 0); + values.put(long.class, (long) 0); + DEFAULT_TYPE_VALUES = Collections.unmodifiableMap(values); + } + + + /** + * Convenience method to instantiate a class using its no-arg constructor. + * @param clazz class to instantiate + * @return the new instance + * @throws BeanInstantiationException if the bean cannot be instantiated + * @deprecated as of Spring 5.0, following the deprecation of + * {@link Class#newInstance()} in JDK 9 + * @see Class#newInstance() + */ + @Deprecated + public static T instantiate(Class clazz) throws BeanInstantiationException { + Assert.notNull(clazz, "Class must not be null"); + if (clazz.isInterface()) { + throw new BeanInstantiationException(clazz, "Specified class is an interface"); + } + try { + return clazz.newInstance(); + } + catch (InstantiationException ex) { + throw new BeanInstantiationException(clazz, "Is it an abstract class?", ex); + } + catch (IllegalAccessException ex) { + throw new BeanInstantiationException(clazz, "Is the constructor accessible?", ex); + } + } + + /** + * Instantiate a class using its 'primary' constructor (for Kotlin classes, + * potentially having default arguments declared) or its default constructor + * (for regular Java classes, expecting a standard no-arg setup). + *

Note that this method tries to set the constructor accessible + * if given a non-accessible (that is, non-public) constructor. + * @param clazz the class to instantiate + * @return the new instance + * @throws BeanInstantiationException if the bean cannot be instantiated. + * The cause may notably indicate a {@link NoSuchMethodException} if no + * primary/default constructor was found, a {@link NoClassDefFoundError} + * or other {@link LinkageError} in case of an unresolvable class definition + * (e.g. due to a missing dependency at runtime), or an exception thrown + * from the constructor invocation itself. + * @see Constructor#newInstance + */ + public static T instantiateClass(Class clazz) throws BeanInstantiationException { + Assert.notNull(clazz, "Class must not be null"); + if (clazz.isInterface()) { + throw new BeanInstantiationException(clazz, "Specified class is an interface"); + } + try { + return instantiateClass(clazz.getDeclaredConstructor()); + } + catch (NoSuchMethodException ex) { + Constructor ctor = findPrimaryConstructor(clazz); + if (ctor != null) { + return instantiateClass(ctor); + } + throw new BeanInstantiationException(clazz, "No default constructor found", ex); + } + catch (LinkageError err) { + throw new BeanInstantiationException(clazz, "Unresolvable class definition", err); + } + } + + /** + * Instantiate a class using its no-arg constructor and return the new instance + * as the specified assignable type. + *

Useful in cases where the type of the class to instantiate (clazz) is not + * available, but the type desired (assignableTo) is known. + *

Note that this method tries to set the constructor accessible if given a + * non-accessible (that is, non-public) constructor. + * @param clazz class to instantiate + * @param assignableTo type that clazz must be assignableTo + * @return the new instance + * @throws BeanInstantiationException if the bean cannot be instantiated + * @see Constructor#newInstance + */ + @SuppressWarnings("unchecked") + public static T instantiateClass(Class clazz, Class assignableTo) throws BeanInstantiationException { + Assert.isAssignable(assignableTo, clazz); + return (T) instantiateClass(clazz); + } + + /** + * Convenience method to instantiate a class using the given constructor. + *

Note that this method tries to set the constructor accessible if given a + * non-accessible (that is, non-public) constructor, and supports Kotlin classes + * with optional parameters and default values. + * @param ctor the constructor to instantiate + * @param args the constructor arguments to apply (use {@code null} for an unspecified + * parameter, Kotlin optional parameters and Java primitive types are supported) + * @return the new instance + * @throws BeanInstantiationException if the bean cannot be instantiated + * @see Constructor#newInstance + */ + public static T instantiateClass(Constructor ctor, Object... args) throws BeanInstantiationException { + Assert.notNull(ctor, "Constructor must not be null"); + try { + ReflectionUtils.makeAccessible(ctor); + if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(ctor.getDeclaringClass())) { + return KotlinDelegate.instantiateClass(ctor, args); + } + else { + Class[] parameterTypes = ctor.getParameterTypes(); + Assert.isTrue(args.length <= parameterTypes.length, "Can't specify more arguments than constructor parameters"); + Object[] argsWithDefaultValues = new Object[args.length]; + for (int i = 0 ; i < args.length; i++) { + if (args[i] == null) { + Class parameterType = parameterTypes[i]; + argsWithDefaultValues[i] = (parameterType.isPrimitive() ? DEFAULT_TYPE_VALUES.get(parameterType) : null); + } + else { + argsWithDefaultValues[i] = args[i]; + } + } + return ctor.newInstance(argsWithDefaultValues); + } + } + catch (InstantiationException ex) { + throw new BeanInstantiationException(ctor, "Is it an abstract class?", ex); + } + catch (IllegalAccessException ex) { + throw new BeanInstantiationException(ctor, "Is the constructor accessible?", ex); + } + catch (IllegalArgumentException ex) { + throw new BeanInstantiationException(ctor, "Illegal arguments for constructor", ex); + } + catch (InvocationTargetException ex) { + throw new BeanInstantiationException(ctor, "Constructor threw exception", ex.getTargetException()); + } + } + + /** + * Return a resolvable constructor for the provided class, either a primary constructor + * or single public constructor or simply a default constructor. Callers have to be + * prepared to resolve arguments for the returned constructor's parameters, if any. + * @param clazz the class to check + * @since 5.3 + * @see #findPrimaryConstructor + */ + @SuppressWarnings("unchecked") + public static Constructor getResolvableConstructor(Class clazz) { + Constructor ctor = findPrimaryConstructor(clazz); + if (ctor == null) { + Constructor[] ctors = clazz.getConstructors(); + if (ctors.length == 1) { + ctor = (Constructor) ctors[0]; + } + else { + try { + ctor = clazz.getDeclaredConstructor(); + } + catch (NoSuchMethodException ex) { + throw new IllegalStateException("No primary or single public constructor found for " + + clazz + " - and no default constructor found either"); + } + } + } + return ctor; + } + + /** + * Return the primary constructor of the provided class. For Kotlin classes, this + * returns the Java constructor corresponding to the Kotlin primary constructor + * (as defined in the Kotlin specification). Otherwise, in particular for non-Kotlin + * classes, this simply returns {@code null}. + * @param clazz the class to check + * @since 5.0 + * @see Kotlin docs + */ + @Nullable + public static Constructor findPrimaryConstructor(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(clazz)) { + Constructor kotlinPrimaryConstructor = KotlinDelegate.findPrimaryConstructor(clazz); + if (kotlinPrimaryConstructor != null) { + return kotlinPrimaryConstructor; + } + } + return null; + } + + /** + * Find a method with the given method name and the given parameter types, + * declared on the given class or one of its superclasses. Prefers public methods, + * but will return a protected, package access, or private method too. + *

Checks {@code Class.getMethod} first, falling back to + * {@code findDeclaredMethod}. This allows to find public methods + * without issues even in environments with restricted Java security settings. + * @param clazz the class to check + * @param methodName the name of the method to find + * @param paramTypes the parameter types of the method to find + * @return the Method object, or {@code null} if not found + * @see Class#getMethod + * @see #findDeclaredMethod + */ + @Nullable + public static Method findMethod(Class clazz, String methodName, Class... paramTypes) { + try { + return clazz.getMethod(methodName, paramTypes); + } + catch (NoSuchMethodException ex) { + return findDeclaredMethod(clazz, methodName, paramTypes); + } + } + + /** + * Find a method with the given method name and the given parameter types, + * declared on the given class or one of its superclasses. Will return a public, + * protected, package access, or private method. + *

Checks {@code Class.getDeclaredMethod}, cascading upwards to all superclasses. + * @param clazz the class to check + * @param methodName the name of the method to find + * @param paramTypes the parameter types of the method to find + * @return the Method object, or {@code null} if not found + * @see Class#getDeclaredMethod + */ + @Nullable + public static Method findDeclaredMethod(Class clazz, String methodName, Class... paramTypes) { + try { + return clazz.getDeclaredMethod(methodName, paramTypes); + } + catch (NoSuchMethodException ex) { + if (clazz.getSuperclass() != null) { + return findDeclaredMethod(clazz.getSuperclass(), methodName, paramTypes); + } + return null; + } + } + + /** + * Find a method with the given method name and minimal parameters (best case: none), + * declared on the given class or one of its superclasses. Prefers public methods, + * but will return a protected, package access, or private method too. + *

Checks {@code Class.getMethods} first, falling back to + * {@code findDeclaredMethodWithMinimalParameters}. This allows for finding public + * methods without issues even in environments with restricted Java security settings. + * @param clazz the class to check + * @param methodName the name of the method to find + * @return the Method object, or {@code null} if not found + * @throws IllegalArgumentException if methods of the given name were found but + * could not be resolved to a unique method with minimal parameters + * @see Class#getMethods + * @see #findDeclaredMethodWithMinimalParameters + */ + @Nullable + public static Method findMethodWithMinimalParameters(Class clazz, String methodName) + throws IllegalArgumentException { + + Method targetMethod = findMethodWithMinimalParameters(clazz.getMethods(), methodName); + if (targetMethod == null) { + targetMethod = findDeclaredMethodWithMinimalParameters(clazz, methodName); + } + return targetMethod; + } + + /** + * Find a method with the given method name and minimal parameters (best case: none), + * declared on the given class or one of its superclasses. Will return a public, + * protected, package access, or private method. + *

Checks {@code Class.getDeclaredMethods}, cascading upwards to all superclasses. + * @param clazz the class to check + * @param methodName the name of the method to find + * @return the Method object, or {@code null} if not found + * @throws IllegalArgumentException if methods of the given name were found but + * could not be resolved to a unique method with minimal parameters + * @see Class#getDeclaredMethods + */ + @Nullable + public static Method findDeclaredMethodWithMinimalParameters(Class clazz, String methodName) + throws IllegalArgumentException { + + Method targetMethod = findMethodWithMinimalParameters(clazz.getDeclaredMethods(), methodName); + if (targetMethod == null && clazz.getSuperclass() != null) { + targetMethod = findDeclaredMethodWithMinimalParameters(clazz.getSuperclass(), methodName); + } + return targetMethod; + } + + /** + * Find a method with the given method name and minimal parameters (best case: none) + * in the given list of methods. + * @param methods the methods to check + * @param methodName the name of the method to find + * @return the Method object, or {@code null} if not found + * @throws IllegalArgumentException if methods of the given name were found but + * could not be resolved to a unique method with minimal parameters + */ + @Nullable + public static Method findMethodWithMinimalParameters(Method[] methods, String methodName) + throws IllegalArgumentException { + + Method targetMethod = null; + int numMethodsFoundWithCurrentMinimumArgs = 0; + for (Method method : methods) { + if (method.getName().equals(methodName)) { + int numParams = method.getParameterCount(); + if (targetMethod == null || numParams < targetMethod.getParameterCount()) { + targetMethod = method; + numMethodsFoundWithCurrentMinimumArgs = 1; + } + else if (!method.isBridge() && targetMethod.getParameterCount() == numParams) { + if (targetMethod.isBridge()) { + // Prefer regular method over bridge... + targetMethod = method; + } + else { + // Additional candidate with same length + numMethodsFoundWithCurrentMinimumArgs++; + } + } + } + } + if (numMethodsFoundWithCurrentMinimumArgs > 1) { + throw new IllegalArgumentException("Cannot resolve method '" + methodName + + "' to a unique method. Attempted to resolve to overloaded method with " + + "the least number of parameters but there were " + + numMethodsFoundWithCurrentMinimumArgs + " candidates."); + } + return targetMethod; + } + + /** + * Parse a method signature in the form {@code methodName[([arg_list])]}, + * where {@code arg_list} is an optional, comma-separated list of fully-qualified + * type names, and attempts to resolve that signature against the supplied {@code Class}. + *

When not supplying an argument list ({@code methodName}) the method whose name + * matches and has the least number of parameters will be returned. When supplying an + * argument type list, only the method whose name and argument types match will be returned. + *

Note then that {@code methodName} and {@code methodName()} are not + * resolved in the same way. The signature {@code methodName} means the method called + * {@code methodName} with the least number of arguments, whereas {@code methodName()} + * means the method called {@code methodName} with exactly 0 arguments. + *

If no method can be found, then {@code null} is returned. + * @param signature the method signature as String representation + * @param clazz the class to resolve the method signature against + * @return the resolved Method + * @see #findMethod + * @see #findMethodWithMinimalParameters + */ + @Nullable + public static Method resolveSignature(String signature, Class clazz) { + Assert.hasText(signature, "'signature' must not be empty"); + Assert.notNull(clazz, "Class must not be null"); + int startParen = signature.indexOf('('); + int endParen = signature.indexOf(')'); + if (startParen > -1 && endParen == -1) { + throw new IllegalArgumentException("Invalid method signature '" + signature + + "': expected closing ')' for args list"); + } + else if (startParen == -1 && endParen > -1) { + throw new IllegalArgumentException("Invalid method signature '" + signature + + "': expected opening '(' for args list"); + } + else if (startParen == -1) { + return findMethodWithMinimalParameters(clazz, signature); + } + else { + String methodName = signature.substring(0, startParen); + String[] parameterTypeNames = + StringUtils.commaDelimitedListToStringArray(signature.substring(startParen + 1, endParen)); + Class[] parameterTypes = new Class[parameterTypeNames.length]; + for (int i = 0; i < parameterTypeNames.length; i++) { + String parameterTypeName = parameterTypeNames[i].trim(); + try { + parameterTypes[i] = ClassUtils.forName(parameterTypeName, clazz.getClassLoader()); + } + catch (Throwable ex) { + throw new IllegalArgumentException("Invalid method signature: unable to resolve type [" + + parameterTypeName + "] for argument " + i + ". Root cause: " + ex); + } + } + return findMethod(clazz, methodName, parameterTypes); + } + } + + + /** + * Retrieve the JavaBeans {@code PropertyDescriptor}s of a given class. + * @param clazz the Class to retrieve the PropertyDescriptors for + * @return an array of {@code PropertyDescriptors} for the given class + * @throws BeansException if PropertyDescriptor look fails + */ + public static PropertyDescriptor[] getPropertyDescriptors(Class clazz) throws BeansException { + return CachedIntrospectionResults.forClass(clazz).getPropertyDescriptors(); + } + + /** + * Retrieve the JavaBeans {@code PropertyDescriptors} for the given property. + * @param clazz the Class to retrieve the PropertyDescriptor for + * @param propertyName the name of the property + * @return the corresponding PropertyDescriptor, or {@code null} if none + * @throws BeansException if PropertyDescriptor lookup fails + */ + @Nullable + public static PropertyDescriptor getPropertyDescriptor(Class clazz, String propertyName) throws BeansException { + return CachedIntrospectionResults.forClass(clazz).getPropertyDescriptor(propertyName); + } + + /** + * Find a JavaBeans {@code PropertyDescriptor} for the given method, + * with the method either being the read method or the write method for + * that bean property. + * @param method the method to find a corresponding PropertyDescriptor for, + * introspecting its declaring class + * @return the corresponding PropertyDescriptor, or {@code null} if none + * @throws BeansException if PropertyDescriptor lookup fails + */ + @Nullable + public static PropertyDescriptor findPropertyForMethod(Method method) throws BeansException { + return findPropertyForMethod(method, method.getDeclaringClass()); + } + + /** + * Find a JavaBeans {@code PropertyDescriptor} for the given method, + * with the method either being the read method or the write method for + * that bean property. + * @param method the method to find a corresponding PropertyDescriptor for + * @param clazz the (most specific) class to introspect for descriptors + * @return the corresponding PropertyDescriptor, or {@code null} if none + * @throws BeansException if PropertyDescriptor lookup fails + * @since 3.2.13 + */ + @Nullable + public static PropertyDescriptor findPropertyForMethod(Method method, Class clazz) throws BeansException { + Assert.notNull(method, "Method must not be null"); + PropertyDescriptor[] pds = getPropertyDescriptors(clazz); + for (PropertyDescriptor pd : pds) { + if (method.equals(pd.getReadMethod()) || method.equals(pd.getWriteMethod())) { + return pd; + } + } + return null; + } + + /** + * Find a JavaBeans PropertyEditor following the 'Editor' suffix convention + * (e.g. "mypackage.MyDomainClass" -> "mypackage.MyDomainClassEditor"). + *

Compatible to the standard JavaBeans convention as implemented by + * {@link java.beans.PropertyEditorManager} but isolated from the latter's + * registered default editors for primitive types. + * @param targetType the type to find an editor for + * @return the corresponding editor, or {@code null} if none found + */ + @Nullable + public static PropertyEditor findEditorByConvention(@Nullable Class targetType) { + if (targetType == null || targetType.isArray() || unknownEditorTypes.contains(targetType)) { + return null; + } + ClassLoader cl = targetType.getClassLoader(); + if (cl == null) { + try { + cl = ClassLoader.getSystemClassLoader(); + if (cl == null) { + return null; + } + } + catch (Throwable ex) { + // e.g. AccessControlException on Google App Engine + if (logger.isDebugEnabled()) { + logger.debug("Could not access system ClassLoader: " + ex); + } + return null; + } + } + String targetTypeName = targetType.getName(); + String editorName = targetTypeName + "Editor"; + try { + Class editorClass = cl.loadClass(editorName); + if (!PropertyEditor.class.isAssignableFrom(editorClass)) { + if (logger.isInfoEnabled()) { + logger.info("Editor class [" + editorName + + "] does not implement [java.beans.PropertyEditor] interface"); + } + unknownEditorTypes.add(targetType); + return null; + } + return (PropertyEditor) instantiateClass(editorClass); + } + catch (ClassNotFoundException ex) { + if (logger.isTraceEnabled()) { + logger.trace("No property editor [" + editorName + "] found for type " + + targetTypeName + " according to 'Editor' suffix convention"); + } + unknownEditorTypes.add(targetType); + return null; + } + } + + /** + * Determine the bean property type for the given property from the + * given classes/interfaces, if possible. + * @param propertyName the name of the bean property + * @param beanClasses the classes to check against + * @return the property type, or {@code Object.class} as fallback + */ + public static Class findPropertyType(String propertyName, @Nullable Class... beanClasses) { + if (beanClasses != null) { + for (Class beanClass : beanClasses) { + PropertyDescriptor pd = getPropertyDescriptor(beanClass, propertyName); + if (pd != null) { + return pd.getPropertyType(); + } + } + } + return Object.class; + } + + /** + * Obtain a new MethodParameter object for the write method of the + * specified property. + * @param pd the PropertyDescriptor for the property + * @return a corresponding MethodParameter object + */ + public static MethodParameter getWriteMethodParameter(PropertyDescriptor pd) { + if (pd instanceof GenericTypeAwarePropertyDescriptor) { + return new MethodParameter(((GenericTypeAwarePropertyDescriptor) pd).getWriteMethodParameter()); + } + else { + Method writeMethod = pd.getWriteMethod(); + Assert.state(writeMethod != null, "No write method available"); + return new MethodParameter(writeMethod, 0); + } + } + + /** + * Determine required parameter names for the given constructor, + * considering the JavaBeans {@link ConstructorProperties} annotation + * as well as Spring's {@link DefaultParameterNameDiscoverer}. + * @param ctor the constructor to find parameter names for + * @return the parameter names (matching the constructor's parameter count) + * @throws IllegalStateException if the parameter names are not resolvable + * @since 5.3 + * @see ConstructorProperties + * @see DefaultParameterNameDiscoverer + */ + public static String[] getParameterNames(Constructor ctor) { + ConstructorProperties cp = ctor.getAnnotation(ConstructorProperties.class); + String[] paramNames = (cp != null ? cp.value() : parameterNameDiscoverer.getParameterNames(ctor)); + Assert.state(paramNames != null, () -> "Cannot resolve parameter names for constructor " + ctor); + Assert.state(paramNames.length == ctor.getParameterCount(), + () -> "Invalid number of parameter names: " + paramNames.length + " for constructor " + ctor); + return paramNames; + } + + /** + * Check if the given type represents a "simple" property: a simple value + * type or an array of simple value types. + *

See {@link #isSimpleValueType(Class)} for the definition of simple + * value type. + *

Used to determine properties to check for a "simple" dependency-check. + * @param type the type to check + * @return whether the given type represents a "simple" property + * @see org.springframework.beans.factory.support.RootBeanDefinition#DEPENDENCY_CHECK_SIMPLE + * @see org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#checkDependencies + * @see #isSimpleValueType(Class) + */ + public static boolean isSimpleProperty(Class type) { + Assert.notNull(type, "'type' must not be null"); + return isSimpleValueType(type) || (type.isArray() && isSimpleValueType(type.getComponentType())); + } + + /** + * Check if the given type represents a "simple" value type: a primitive or + * primitive wrapper, an enum, a String or other CharSequence, a Number, a + * Date, a Temporal, a URI, a URL, a Locale, or a Class. + *

{@code Void} and {@code void} are not considered simple value types. + * @param type the type to check + * @return whether the given type represents a "simple" value type + * @see #isSimpleProperty(Class) + */ + public static boolean isSimpleValueType(Class type) { + return (Void.class != type && void.class != type && + (ClassUtils.isPrimitiveOrWrapper(type) || + Enum.class.isAssignableFrom(type) || + CharSequence.class.isAssignableFrom(type) || + Number.class.isAssignableFrom(type) || + Date.class.isAssignableFrom(type) || + Temporal.class.isAssignableFrom(type) || + URI.class == type || + URL.class == type || + Locale.class == type || + Class.class == type)); + } + + + /** + * Copy the property values of the given source bean into the target bean. + *

Note: The source and target classes do not have to match or even be derived + * from each other, as long as the properties match. Any bean properties that the + * source bean exposes but the target bean does not will silently be ignored. + *

This is just a convenience method. For more complex transfer needs, + * consider using a full BeanWrapper. + * @param source the source bean + * @param target the target bean + * @throws BeansException if the copying failed + * @see BeanWrapper + */ + public static void copyProperties(Object source, Object target) throws BeansException { + copyProperties(source, target, null, (String[]) null); + } + + /** + * Copy the property values of the given source bean into the given target bean, + * only setting properties defined in the given "editable" class (or interface). + *

Note: The source and target classes do not have to match or even be derived + * from each other, as long as the properties match. Any bean properties that the + * source bean exposes but the target bean does not will silently be ignored. + *

This is just a convenience method. For more complex transfer needs, + * consider using a full BeanWrapper. + * @param source the source bean + * @param target the target bean + * @param editable the class (or interface) to restrict property setting to + * @throws BeansException if the copying failed + * @see BeanWrapper + */ + public static void copyProperties(Object source, Object target, Class editable) throws BeansException { + copyProperties(source, target, editable, (String[]) null); + } + + /** + * Copy the property values of the given source bean into the given target bean, + * ignoring the given "ignoreProperties". + *

Note: The source and target classes do not have to match or even be derived + * from each other, as long as the properties match. Any bean properties that the + * source bean exposes but the target bean does not will silently be ignored. + *

This is just a convenience method. For more complex transfer needs, + * consider using a full BeanWrapper. + * @param source the source bean + * @param target the target bean + * @param ignoreProperties array of property names to ignore + * @throws BeansException if the copying failed + * @see BeanWrapper + */ + public static void copyProperties(Object source, Object target, String... ignoreProperties) throws BeansException { + copyProperties(source, target, null, ignoreProperties); + } + + /** + * Copy the property values of the given source bean into the given target bean. + *

Note: The source and target classes do not have to match or even be derived + * from each other, as long as the properties match. Any bean properties that the + * source bean exposes but the target bean does not will silently be ignored. + *

As of Spring Framework 5.3, this method honors generic type information + * when matching properties in the source and target objects. + * @param source the source bean + * @param target the target bean + * @param editable the class (or interface) to restrict property setting to + * @param ignoreProperties array of property names to ignore + * @throws BeansException if the copying failed + * @see BeanWrapper + */ + private static void copyProperties(Object source, Object target, @Nullable Class editable, + @Nullable String... ignoreProperties) throws BeansException { + + Assert.notNull(source, "Source must not be null"); + Assert.notNull(target, "Target must not be null"); + + Class actualEditable = target.getClass(); + if (editable != null) { + if (!editable.isInstance(target)) { + throw new IllegalArgumentException("Target class [" + target.getClass().getName() + + "] not assignable to Editable class [" + editable.getName() + "]"); + } + actualEditable = editable; + } + PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable); + List ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null); + + for (PropertyDescriptor targetPd : targetPds) { + Method writeMethod = targetPd.getWriteMethod(); + if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) { + PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName()); + if (sourcePd != null) { + Method readMethod = sourcePd.getReadMethod(); + if (readMethod != null) { + ResolvableType sourceResolvableType = ResolvableType.forMethodReturnType(readMethod); + ResolvableType targetResolvableType = ResolvableType.forMethodParameter(writeMethod, 0); + if (targetResolvableType.isAssignableFrom(sourceResolvableType)) { + try { + if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) { + readMethod.setAccessible(true); + } + Object value = readMethod.invoke(source); + if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) { + writeMethod.setAccessible(true); + } + writeMethod.invoke(target, value); + } + catch (Throwable ex) { + throw new FatalBeanException( + "Could not copy property '" + targetPd.getName() + "' from source to target", ex); + } + } + } + } + } + } + } + + + /** + * Inner class to avoid a hard dependency on Kotlin at runtime. + */ + private static class KotlinDelegate { + + /** + * Retrieve the Java constructor corresponding to the Kotlin primary constructor, if any. + * @param clazz the {@link Class} of the Kotlin class + * @see + * https://kotlinlang.org/docs/reference/classes.html#constructors + */ + @Nullable + public static Constructor findPrimaryConstructor(Class clazz) { + try { + KFunction primaryCtor = KClasses.getPrimaryConstructor(JvmClassMappingKt.getKotlinClass(clazz)); + if (primaryCtor == null) { + return null; + } + Constructor constructor = ReflectJvmMapping.getJavaConstructor(primaryCtor); + if (constructor == null) { + throw new IllegalStateException( + "Failed to find Java constructor for Kotlin primary constructor: " + clazz.getName()); + } + return constructor; + } + catch (UnsupportedOperationException ex) { + return null; + } + } + + /** + * Instantiate a Kotlin class using the provided constructor. + * @param ctor the constructor of the Kotlin class to instantiate + * @param args the constructor arguments to apply + * (use {@code null} for unspecified parameter if needed) + */ + public static T instantiateClass(Constructor ctor, Object... args) + throws IllegalAccessException, InvocationTargetException, InstantiationException { + + KFunction kotlinConstructor = ReflectJvmMapping.getKotlinFunction(ctor); + if (kotlinConstructor == null) { + return ctor.newInstance(args); + } + + if ((!Modifier.isPublic(ctor.getModifiers()) || !Modifier.isPublic(ctor.getDeclaringClass().getModifiers()))) { + KCallablesJvm.setAccessible(kotlinConstructor, true); + } + + List parameters = kotlinConstructor.getParameters(); + Map argParameters = CollectionUtils.newHashMap(parameters.size()); + Assert.isTrue(args.length <= parameters.size(), + "Number of provided arguments should be less of equals than number of constructor parameters"); + for (int i = 0 ; i < args.length ; i++) { + if (!(parameters.get(i).isOptional() && args[i] == null)) { + argParameters.put(parameters.get(i), args[i]); + } + } + return kotlinConstructor.callBy(argParameters); + } + + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanWrapper.java b/spring-beans/src/main/java/org/springframework/beans/BeanWrapper.java new file mode 100644 index 0000000..90ee4f1 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/BeanWrapper.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.beans.PropertyDescriptor; + +/** + * The central interface of Spring's low-level JavaBeans infrastructure. + * + *

Typically not used directly but rather implicitly via a + * {@link org.springframework.beans.factory.BeanFactory} or a + * {@link org.springframework.validation.DataBinder}. + * + *

Provides operations to analyze and manipulate standard JavaBeans: + * the ability to get and set property values (individually or in bulk), + * get property descriptors, and query the readability/writability of properties. + * + *

This interface supports nested properties enabling the setting + * of properties on subproperties to an unlimited depth. + * + *

A BeanWrapper's default for the "extractOldValueForEditor" setting + * is "false", to avoid side effects caused by getter method invocations. + * Turn this to "true" to expose present property values to custom editors. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 13 April 2001 + * @see PropertyAccessor + * @see PropertyEditorRegistry + * @see PropertyAccessorFactory#forBeanPropertyAccess + * @see org.springframework.beans.factory.BeanFactory + * @see org.springframework.validation.BeanPropertyBindingResult + * @see org.springframework.validation.DataBinder#initBeanPropertyAccess() + */ +public interface BeanWrapper extends ConfigurablePropertyAccessor { + + /** + * Specify a limit for array and collection auto-growing. + *

Default is unlimited on a plain BeanWrapper. + * @since 4.1 + */ + void setAutoGrowCollectionLimit(int autoGrowCollectionLimit); + + /** + * Return the limit for array and collection auto-growing. + * @since 4.1 + */ + int getAutoGrowCollectionLimit(); + + /** + * Return the bean instance wrapped by this object. + */ + Object getWrappedInstance(); + + /** + * Return the type of the wrapped bean instance. + */ + Class getWrappedClass(); + + /** + * Obtain the PropertyDescriptors for the wrapped object + * (as determined by standard JavaBeans introspection). + * @return the PropertyDescriptors for the wrapped object + */ + PropertyDescriptor[] getPropertyDescriptors(); + + /** + * Obtain the property descriptor for a specific property + * of the wrapped object. + * @param propertyName the property to obtain the descriptor for + * (may be a nested path, but no indexed/mapped property) + * @return the property descriptor for the specified property + * @throws InvalidPropertyException if there is no such property + */ + PropertyDescriptor getPropertyDescriptor(String propertyName) throws InvalidPropertyException; + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java b/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java new file mode 100644 index 0000000..c2fb1ac --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java @@ -0,0 +1,337 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; +import java.security.AccessControlContext; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; + +import org.springframework.core.ResolvableType; +import org.springframework.core.convert.Property; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; + +/** + * Default {@link BeanWrapper} implementation that should be sufficient + * for all typical use cases. Caches introspection results for efficiency. + * + *

Note: Auto-registers default property editors from the + * {@code org.springframework.beans.propertyeditors} package, which apply + * in addition to the JDK's standard PropertyEditors. Applications can call + * the {@link #registerCustomEditor(Class, java.beans.PropertyEditor)} method + * to register an editor for a particular instance (i.e. they are not shared + * across the application). See the base class + * {@link PropertyEditorRegistrySupport} for details. + * + *

NOTE: As of Spring 2.5, this is - for almost all purposes - an + * internal class. It is just public in order to allow for access from + * other framework packages. For standard application access purposes, use the + * {@link PropertyAccessorFactory#forBeanPropertyAccess} factory method instead. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Rob Harrop + * @author Stephane Nicoll + * @since 15 April 2001 + * @see #registerCustomEditor + * @see #setPropertyValues + * @see #setPropertyValue + * @see #getPropertyValue + * @see #getPropertyType + * @see BeanWrapper + * @see PropertyEditorRegistrySupport + */ +public class BeanWrapperImpl extends AbstractNestablePropertyAccessor implements BeanWrapper { + + /** + * Cached introspections results for this object, to prevent encountering + * the cost of JavaBeans introspection every time. + */ + @Nullable + private CachedIntrospectionResults cachedIntrospectionResults; + + /** + * The security context used for invoking the property methods. + */ + @Nullable + private AccessControlContext acc; + + + /** + * Create a new empty BeanWrapperImpl. Wrapped instance needs to be set afterwards. + * Registers default editors. + * @see #setWrappedInstance + */ + public BeanWrapperImpl() { + this(true); + } + + /** + * Create a new empty BeanWrapperImpl. Wrapped instance needs to be set afterwards. + * @param registerDefaultEditors whether to register default editors + * (can be suppressed if the BeanWrapper won't need any type conversion) + * @see #setWrappedInstance + */ + public BeanWrapperImpl(boolean registerDefaultEditors) { + super(registerDefaultEditors); + } + + /** + * Create a new BeanWrapperImpl for the given object. + * @param object the object wrapped by this BeanWrapper + */ + public BeanWrapperImpl(Object object) { + super(object); + } + + /** + * Create a new BeanWrapperImpl, wrapping a new instance of the specified class. + * @param clazz class to instantiate and wrap + */ + public BeanWrapperImpl(Class clazz) { + super(clazz); + } + + /** + * Create a new BeanWrapperImpl for the given object, + * registering a nested path that the object is in. + * @param object the object wrapped by this BeanWrapper + * @param nestedPath the nested path of the object + * @param rootObject the root object at the top of the path + */ + public BeanWrapperImpl(Object object, String nestedPath, Object rootObject) { + super(object, nestedPath, rootObject); + } + + /** + * Create a new BeanWrapperImpl for the given object, + * registering a nested path that the object is in. + * @param object the object wrapped by this BeanWrapper + * @param nestedPath the nested path of the object + * @param parent the containing BeanWrapper (must not be {@code null}) + */ + private BeanWrapperImpl(Object object, String nestedPath, BeanWrapperImpl parent) { + super(object, nestedPath, parent); + setSecurityContext(parent.acc); + } + + + /** + * Set a bean instance to hold, without any unwrapping of {@link java.util.Optional}. + * @param object the actual target object + * @since 4.3 + * @see #setWrappedInstance(Object) + */ + public void setBeanInstance(Object object) { + this.wrappedObject = object; + this.rootObject = object; + this.typeConverterDelegate = new TypeConverterDelegate(this, this.wrappedObject); + setIntrospectionClass(object.getClass()); + } + + @Override + public void setWrappedInstance(Object object, @Nullable String nestedPath, @Nullable Object rootObject) { + super.setWrappedInstance(object, nestedPath, rootObject); + setIntrospectionClass(getWrappedClass()); + } + + /** + * Set the class to introspect. + * Needs to be called when the target object changes. + * @param clazz the class to introspect + */ + protected void setIntrospectionClass(Class clazz) { + if (this.cachedIntrospectionResults != null && this.cachedIntrospectionResults.getBeanClass() != clazz) { + this.cachedIntrospectionResults = null; + } + } + + /** + * Obtain a lazily initialized CachedIntrospectionResults instance + * for the wrapped object. + */ + private CachedIntrospectionResults getCachedIntrospectionResults() { + if (this.cachedIntrospectionResults == null) { + this.cachedIntrospectionResults = CachedIntrospectionResults.forClass(getWrappedClass()); + } + return this.cachedIntrospectionResults; + } + + /** + * Set the security context used during the invocation of the wrapped instance methods. + * Can be null. + */ + public void setSecurityContext(@Nullable AccessControlContext acc) { + this.acc = acc; + } + + /** + * Return the security context used during the invocation of the wrapped instance methods. + * Can be null. + */ + @Nullable + public AccessControlContext getSecurityContext() { + return this.acc; + } + + + /** + * Convert the given value for the specified property to the latter's type. + *

This method is only intended for optimizations in a BeanFactory. + * Use the {@code convertIfNecessary} methods for programmatic conversion. + * @param value the value to convert + * @param propertyName the target property + * (note that nested or indexed properties are not supported here) + * @return the new value, possibly the result of type conversion + * @throws TypeMismatchException if type conversion failed + */ + @Nullable + public Object convertForProperty(@Nullable Object value, String propertyName) throws TypeMismatchException { + CachedIntrospectionResults cachedIntrospectionResults = getCachedIntrospectionResults(); + PropertyDescriptor pd = cachedIntrospectionResults.getPropertyDescriptor(propertyName); + if (pd == null) { + throw new InvalidPropertyException(getRootClass(), getNestedPath() + propertyName, + "No property '" + propertyName + "' found"); + } + TypeDescriptor td = cachedIntrospectionResults.getTypeDescriptor(pd); + if (td == null) { + td = cachedIntrospectionResults.addTypeDescriptor(pd, new TypeDescriptor(property(pd))); + } + return convertForProperty(propertyName, null, value, td); + } + + private Property property(PropertyDescriptor pd) { + GenericTypeAwarePropertyDescriptor gpd = (GenericTypeAwarePropertyDescriptor) pd; + return new Property(gpd.getBeanClass(), gpd.getReadMethod(), gpd.getWriteMethod(), gpd.getName()); + } + + @Override + @Nullable + protected BeanPropertyHandler getLocalPropertyHandler(String propertyName) { + PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(propertyName); + return (pd != null ? new BeanPropertyHandler(pd) : null); + } + + @Override + protected BeanWrapperImpl newNestedPropertyAccessor(Object object, String nestedPath) { + return new BeanWrapperImpl(object, nestedPath, this); + } + + @Override + protected NotWritablePropertyException createNotWritablePropertyException(String propertyName) { + PropertyMatches matches = PropertyMatches.forProperty(propertyName, getRootClass()); + throw new NotWritablePropertyException(getRootClass(), getNestedPath() + propertyName, + matches.buildErrorMessage(), matches.getPossibleMatches()); + } + + @Override + public PropertyDescriptor[] getPropertyDescriptors() { + return getCachedIntrospectionResults().getPropertyDescriptors(); + } + + @Override + public PropertyDescriptor getPropertyDescriptor(String propertyName) throws InvalidPropertyException { + BeanWrapperImpl nestedBw = (BeanWrapperImpl) getPropertyAccessorForPropertyPath(propertyName); + String finalPath = getFinalPath(nestedBw, propertyName); + PropertyDescriptor pd = nestedBw.getCachedIntrospectionResults().getPropertyDescriptor(finalPath); + if (pd == null) { + throw new InvalidPropertyException(getRootClass(), getNestedPath() + propertyName, + "No property '" + propertyName + "' found"); + } + return pd; + } + + + private class BeanPropertyHandler extends PropertyHandler { + + private final PropertyDescriptor pd; + + public BeanPropertyHandler(PropertyDescriptor pd) { + super(pd.getPropertyType(), pd.getReadMethod() != null, pd.getWriteMethod() != null); + this.pd = pd; + } + + @Override + public ResolvableType getResolvableType() { + return ResolvableType.forMethodReturnType(this.pd.getReadMethod()); + } + + @Override + public TypeDescriptor toTypeDescriptor() { + return new TypeDescriptor(property(this.pd)); + } + + @Override + @Nullable + public TypeDescriptor nested(int level) { + return TypeDescriptor.nested(property(this.pd), level); + } + + @Override + @Nullable + public Object getValue() throws Exception { + Method readMethod = this.pd.getReadMethod(); + if (System.getSecurityManager() != null) { + AccessController.doPrivileged((PrivilegedAction) () -> { + ReflectionUtils.makeAccessible(readMethod); + return null; + }); + try { + return AccessController.doPrivileged((PrivilegedExceptionAction) + () -> readMethod.invoke(getWrappedInstance(), (Object[]) null), acc); + } + catch (PrivilegedActionException pae) { + throw pae.getException(); + } + } + else { + ReflectionUtils.makeAccessible(readMethod); + return readMethod.invoke(getWrappedInstance(), (Object[]) null); + } + } + + @Override + public void setValue(@Nullable Object value) throws Exception { + Method writeMethod = (this.pd instanceof GenericTypeAwarePropertyDescriptor ? + ((GenericTypeAwarePropertyDescriptor) this.pd).getWriteMethodForActualAccess() : + this.pd.getWriteMethod()); + if (System.getSecurityManager() != null) { + AccessController.doPrivileged((PrivilegedAction) () -> { + ReflectionUtils.makeAccessible(writeMethod); + return null; + }); + try { + AccessController.doPrivileged((PrivilegedExceptionAction) + () -> writeMethod.invoke(getWrappedInstance(), value), acc); + } + catch (PrivilegedActionException ex) { + throw ex.getException(); + } + } + else { + ReflectionUtils.makeAccessible(writeMethod); + writeMethod.invoke(getWrappedInstance(), value); + } + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/BeansException.java b/spring-beans/src/main/java/org/springframework/beans/BeansException.java new file mode 100644 index 0000000..f3816a1 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/BeansException.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import org.springframework.core.NestedRuntimeException; +import org.springframework.lang.Nullable; + +/** + * Abstract superclass for all exceptions thrown in the beans package + * and subpackages. + * + *

Note that this is a runtime (unchecked) exception. Beans exceptions + * are usually fatal; there is no reason for them to be checked. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +@SuppressWarnings("serial") +public abstract class BeansException extends NestedRuntimeException { + + /** + * Create a new BeansException with the specified message. + * @param msg the detail message + */ + public BeansException(String msg) { + super(msg); + } + + /** + * Create a new BeansException with the specified message + * and root cause. + * @param msg the detail message + * @param cause the root cause + */ + public BeansException(@Nullable String msg, @Nullable Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java new file mode 100644 index 0000000..7b7a67d --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java @@ -0,0 +1,426 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.SpringProperties; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.StringUtils; + +/** + * Internal class that caches JavaBeans {@link java.beans.PropertyDescriptor} + * information for a Java class. Not intended for direct use by application code. + * + *

Necessary for Spring's own caching of bean descriptors within the application + * {@link ClassLoader}, rather than relying on the JDK's system-wide {@link BeanInfo} + * cache (in order to avoid leaks on individual application shutdown in a shared JVM). + * + *

Information is cached statically, so we don't need to create new + * objects of this class for every JavaBean we manipulate. Hence, this class + * implements the factory design pattern, using a private constructor and + * a static {@link #forClass(Class)} factory method to obtain instances. + * + *

Note that for caching to work effectively, some preconditions need to be met: + * Prefer an arrangement where the Spring jars live in the same ClassLoader as the + * application classes, which allows for clean caching along with the application's + * lifecycle in any case. For a web application, consider declaring a local + * {@link org.springframework.web.util.IntrospectorCleanupListener} in {@code web.xml} + * in case of a multi-ClassLoader layout, which will allow for effective caching as well. + * + *

In case of a non-clean ClassLoader arrangement without a cleanup listener having + * been set up, this class will fall back to a weak-reference-based caching model that + * recreates much-requested entries every time the garbage collector removed them. In + * such a scenario, consider the {@link #IGNORE_BEANINFO_PROPERTY_NAME} system property. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 05 May 2001 + * @see #acceptClassLoader(ClassLoader) + * @see #clearClassLoader(ClassLoader) + * @see #forClass(Class) + */ +public final class CachedIntrospectionResults { + + /** + * System property that instructs Spring to use the {@link Introspector#IGNORE_ALL_BEANINFO} + * mode when calling the JavaBeans {@link Introspector}: "spring.beaninfo.ignore", with a + * value of "true" skipping the search for {@code BeanInfo} classes (typically for scenarios + * where no such classes are being defined for beans in the application in the first place). + *

The default is "false", considering all {@code BeanInfo} metadata classes, like for + * standard {@link Introspector#getBeanInfo(Class)} calls. Consider switching this flag to + * "true" if you experience repeated ClassLoader access for non-existing {@code BeanInfo} + * classes, in case such access is expensive on startup or on lazy loading. + *

Note that such an effect may also indicate a scenario where caching doesn't work + * effectively: Prefer an arrangement where the Spring jars live in the same ClassLoader + * as the application classes, which allows for clean caching along with the application's + * lifecycle in any case. For a web application, consider declaring a local + * {@link org.springframework.web.util.IntrospectorCleanupListener} in {@code web.xml} + * in case of a multi-ClassLoader layout, which will allow for effective caching as well. + * @see Introspector#getBeanInfo(Class, int) + */ + public static final String IGNORE_BEANINFO_PROPERTY_NAME = "spring.beaninfo.ignore"; + + private static final PropertyDescriptor[] EMPTY_PROPERTY_DESCRIPTOR_ARRAY = {}; + + + private static final boolean shouldIntrospectorIgnoreBeaninfoClasses = + SpringProperties.getFlag(IGNORE_BEANINFO_PROPERTY_NAME); + + /** Stores the BeanInfoFactory instances. */ + private static final List beanInfoFactories = SpringFactoriesLoader.loadFactories( + BeanInfoFactory.class, CachedIntrospectionResults.class.getClassLoader()); + + private static final Log logger = LogFactory.getLog(CachedIntrospectionResults.class); + + /** + * Set of ClassLoaders that this CachedIntrospectionResults class will always + * accept classes from, even if the classes do not qualify as cache-safe. + */ + static final Set acceptedClassLoaders = + Collections.newSetFromMap(new ConcurrentHashMap<>(16)); + + /** + * Map keyed by Class containing CachedIntrospectionResults, strongly held. + * This variant is being used for cache-safe bean classes. + */ + static final ConcurrentMap, CachedIntrospectionResults> strongClassCache = + new ConcurrentHashMap<>(64); + + /** + * Map keyed by Class containing CachedIntrospectionResults, softly held. + * This variant is being used for non-cache-safe bean classes. + */ + static final ConcurrentMap, CachedIntrospectionResults> softClassCache = + new ConcurrentReferenceHashMap<>(64); + + + /** + * Accept the given ClassLoader as cache-safe, even if its classes would + * not qualify as cache-safe in this CachedIntrospectionResults class. + *

This configuration method is only relevant in scenarios where the Spring + * classes reside in a 'common' ClassLoader (e.g. the system ClassLoader) + * whose lifecycle is not coupled to the application. In such a scenario, + * CachedIntrospectionResults would by default not cache any of the application's + * classes, since they would create a leak in the common ClassLoader. + *

Any {@code acceptClassLoader} call at application startup should + * be paired with a {@link #clearClassLoader} call at application shutdown. + * @param classLoader the ClassLoader to accept + */ + public static void acceptClassLoader(@Nullable ClassLoader classLoader) { + if (classLoader != null) { + acceptedClassLoaders.add(classLoader); + } + } + + /** + * Clear the introspection cache for the given ClassLoader, removing the + * introspection results for all classes underneath that ClassLoader, and + * removing the ClassLoader (and its children) from the acceptance list. + * @param classLoader the ClassLoader to clear the cache for + */ + public static void clearClassLoader(@Nullable ClassLoader classLoader) { + acceptedClassLoaders.removeIf(registeredLoader -> + isUnderneathClassLoader(registeredLoader, classLoader)); + strongClassCache.keySet().removeIf(beanClass -> + isUnderneathClassLoader(beanClass.getClassLoader(), classLoader)); + softClassCache.keySet().removeIf(beanClass -> + isUnderneathClassLoader(beanClass.getClassLoader(), classLoader)); + } + + /** + * Create CachedIntrospectionResults for the given bean class. + * @param beanClass the bean class to analyze + * @return the corresponding CachedIntrospectionResults + * @throws BeansException in case of introspection failure + */ + static CachedIntrospectionResults forClass(Class beanClass) throws BeansException { + CachedIntrospectionResults results = strongClassCache.get(beanClass); + if (results != null) { + return results; + } + results = softClassCache.get(beanClass); + if (results != null) { + return results; + } + + results = new CachedIntrospectionResults(beanClass); + ConcurrentMap, CachedIntrospectionResults> classCacheToUse; + + if (ClassUtils.isCacheSafe(beanClass, CachedIntrospectionResults.class.getClassLoader()) || + isClassLoaderAccepted(beanClass.getClassLoader())) { + classCacheToUse = strongClassCache; + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Not strongly caching class [" + beanClass.getName() + "] because it is not cache-safe"); + } + classCacheToUse = softClassCache; + } + + CachedIntrospectionResults existing = classCacheToUse.putIfAbsent(beanClass, results); + return (existing != null ? existing : results); + } + + /** + * Check whether this CachedIntrospectionResults class is configured + * to accept the given ClassLoader. + * @param classLoader the ClassLoader to check + * @return whether the given ClassLoader is accepted + * @see #acceptClassLoader + */ + private static boolean isClassLoaderAccepted(ClassLoader classLoader) { + for (ClassLoader acceptedLoader : acceptedClassLoaders) { + if (isUnderneathClassLoader(classLoader, acceptedLoader)) { + return true; + } + } + return false; + } + + /** + * Check whether the given ClassLoader is underneath the given parent, + * that is, whether the parent is within the candidate's hierarchy. + * @param candidate the candidate ClassLoader to check + * @param parent the parent ClassLoader to check for + */ + private static boolean isUnderneathClassLoader(@Nullable ClassLoader candidate, @Nullable ClassLoader parent) { + if (candidate == parent) { + return true; + } + if (candidate == null) { + return false; + } + ClassLoader classLoaderToCheck = candidate; + while (classLoaderToCheck != null) { + classLoaderToCheck = classLoaderToCheck.getParent(); + if (classLoaderToCheck == parent) { + return true; + } + } + return false; + } + + /** + * Retrieve a {@link BeanInfo} descriptor for the given target class. + * @param beanClass the target class to introspect + * @return the resulting {@code BeanInfo} descriptor (never {@code null}) + * @throws IntrospectionException from the underlying {@link Introspector} + */ + private static BeanInfo getBeanInfo(Class beanClass) throws IntrospectionException { + for (BeanInfoFactory beanInfoFactory : beanInfoFactories) { + BeanInfo beanInfo = beanInfoFactory.getBeanInfo(beanClass); + if (beanInfo != null) { + return beanInfo; + } + } + return (shouldIntrospectorIgnoreBeaninfoClasses ? + Introspector.getBeanInfo(beanClass, Introspector.IGNORE_ALL_BEANINFO) : + Introspector.getBeanInfo(beanClass)); + } + + + /** The BeanInfo object for the introspected bean class. */ + private final BeanInfo beanInfo; + + /** PropertyDescriptor objects keyed by property name String. */ + private final Map propertyDescriptors; + + /** TypeDescriptor objects keyed by PropertyDescriptor. */ + private final ConcurrentMap typeDescriptorCache; + + + /** + * Create a new CachedIntrospectionResults instance for the given class. + * @param beanClass the bean class to analyze + * @throws BeansException in case of introspection failure + */ + private CachedIntrospectionResults(Class beanClass) throws BeansException { + try { + if (logger.isTraceEnabled()) { + logger.trace("Getting BeanInfo for class [" + beanClass.getName() + "]"); + } + this.beanInfo = getBeanInfo(beanClass); + + if (logger.isTraceEnabled()) { + logger.trace("Caching PropertyDescriptors for class [" + beanClass.getName() + "]"); + } + this.propertyDescriptors = new LinkedHashMap<>(); + + Set readMethodNames = new HashSet<>(); + + // This call is slow so we do it once. + PropertyDescriptor[] pds = this.beanInfo.getPropertyDescriptors(); + for (PropertyDescriptor pd : pds) { + if (Class.class == beanClass && + ("classLoader".equals(pd.getName()) || "protectionDomain".equals(pd.getName()))) { + // Ignore Class.getClassLoader() and getProtectionDomain() methods - nobody needs to bind to those + continue; + } + if (logger.isTraceEnabled()) { + logger.trace("Found bean property '" + pd.getName() + "'" + + (pd.getPropertyType() != null ? " of type [" + pd.getPropertyType().getName() + "]" : "") + + (pd.getPropertyEditorClass() != null ? + "; editor [" + pd.getPropertyEditorClass().getName() + "]" : "")); + } + pd = buildGenericTypeAwarePropertyDescriptor(beanClass, pd); + this.propertyDescriptors.put(pd.getName(), pd); + Method readMethod = pd.getReadMethod(); + if (readMethod != null) { + readMethodNames.add(readMethod.getName()); + } + } + + // Explicitly check implemented interfaces for setter/getter methods as well, + // in particular for Java 8 default methods... + Class currClass = beanClass; + while (currClass != null && currClass != Object.class) { + introspectInterfaces(beanClass, currClass, readMethodNames); + currClass = currClass.getSuperclass(); + } + + // Check for record-style accessors without prefix: e.g. "lastName()" + // - accessor method directly referring to instance field of same name + // - same convention for component accessors of Java 15 record classes + introspectPlainAccessors(beanClass, readMethodNames); + + this.typeDescriptorCache = new ConcurrentReferenceHashMap<>(); + } + catch (IntrospectionException ex) { + throw new FatalBeanException("Failed to obtain BeanInfo for class [" + beanClass.getName() + "]", ex); + } + } + + private void introspectInterfaces(Class beanClass, Class currClass, Set readMethodNames) + throws IntrospectionException { + + for (Class ifc : currClass.getInterfaces()) { + if (!ClassUtils.isJavaLanguageInterface(ifc)) { + for (PropertyDescriptor pd : getBeanInfo(ifc).getPropertyDescriptors()) { + PropertyDescriptor existingPd = this.propertyDescriptors.get(pd.getName()); + if (existingPd == null || + (existingPd.getReadMethod() == null && pd.getReadMethod() != null)) { + // GenericTypeAwarePropertyDescriptor leniently resolves a set* write method + // against a declared read method, so we prefer read method descriptors here. + pd = buildGenericTypeAwarePropertyDescriptor(beanClass, pd); + this.propertyDescriptors.put(pd.getName(), pd); + Method readMethod = pd.getReadMethod(); + if (readMethod != null) { + readMethodNames.add(readMethod.getName()); + } + } + } + introspectInterfaces(ifc, ifc, readMethodNames); + } + } + } + + private void introspectPlainAccessors(Class beanClass, Set readMethodNames) + throws IntrospectionException { + + for (Method method : beanClass.getMethods()) { + if (!this.propertyDescriptors.containsKey(method.getName()) && + !readMethodNames.contains((method.getName())) && isPlainAccessor(method)) { + this.propertyDescriptors.put(method.getName(), + new GenericTypeAwarePropertyDescriptor(beanClass, method.getName(), method, null, null)); + readMethodNames.add(method.getName()); + } + } + } + + private boolean isPlainAccessor(Method method) { + if (method.getParameterCount() > 0 || method.getReturnType() == void.class || + method.getDeclaringClass() == Object.class || Modifier.isStatic(method.getModifiers())) { + return false; + } + try { + // Accessor method referring to instance field of same name? + method.getDeclaringClass().getDeclaredField(method.getName()); + return true; + } + catch (Exception ex) { + return false; + } + } + + + BeanInfo getBeanInfo() { + return this.beanInfo; + } + + Class getBeanClass() { + return this.beanInfo.getBeanDescriptor().getBeanClass(); + } + + @Nullable + PropertyDescriptor getPropertyDescriptor(String name) { + PropertyDescriptor pd = this.propertyDescriptors.get(name); + if (pd == null && StringUtils.hasLength(name)) { + // Same lenient fallback checking as in Property... + pd = this.propertyDescriptors.get(StringUtils.uncapitalize(name)); + if (pd == null) { + pd = this.propertyDescriptors.get(StringUtils.capitalize(name)); + } + } + return pd; + } + + PropertyDescriptor[] getPropertyDescriptors() { + return this.propertyDescriptors.values().toArray(EMPTY_PROPERTY_DESCRIPTOR_ARRAY); + } + + private PropertyDescriptor buildGenericTypeAwarePropertyDescriptor(Class beanClass, PropertyDescriptor pd) { + try { + return new GenericTypeAwarePropertyDescriptor(beanClass, pd.getName(), pd.getReadMethod(), + pd.getWriteMethod(), pd.getPropertyEditorClass()); + } + catch (IntrospectionException ex) { + throw new FatalBeanException("Failed to re-introspect class [" + beanClass.getName() + "]", ex); + } + } + + TypeDescriptor addTypeDescriptor(PropertyDescriptor pd, TypeDescriptor td) { + TypeDescriptor existing = this.typeDescriptorCache.putIfAbsent(pd, td); + return (existing != null ? existing : td); + } + + @Nullable + TypeDescriptor getTypeDescriptor(PropertyDescriptor pd) { + return this.typeDescriptorCache.get(pd); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/ConfigurablePropertyAccessor.java b/spring-beans/src/main/java/org/springframework/beans/ConfigurablePropertyAccessor.java new file mode 100644 index 0000000..43fc6d2 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/ConfigurablePropertyAccessor.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import org.springframework.core.convert.ConversionService; +import org.springframework.lang.Nullable; + +/** + * Interface that encapsulates configuration methods for a PropertyAccessor. + * Also extends the PropertyEditorRegistry interface, which defines methods + * for PropertyEditor management. + * + *

Serves as base interface for {@link BeanWrapper}. + * + * @author Juergen Hoeller + * @author Stephane Nicoll + * @since 2.0 + * @see BeanWrapper + */ +public interface ConfigurablePropertyAccessor extends PropertyAccessor, PropertyEditorRegistry, TypeConverter { + + /** + * Specify a Spring 3.0 ConversionService to use for converting + * property values, as an alternative to JavaBeans PropertyEditors. + */ + void setConversionService(@Nullable ConversionService conversionService); + + /** + * Return the associated ConversionService, if any. + */ + @Nullable + ConversionService getConversionService(); + + /** + * Set whether to extract the old property value when applying a + * property editor to a new value for a property. + */ + void setExtractOldValueForEditor(boolean extractOldValueForEditor); + + /** + * Return whether to extract the old property value when applying a + * property editor to a new value for a property. + */ + boolean isExtractOldValueForEditor(); + + /** + * Set whether this instance should attempt to "auto-grow" a + * nested path that contains a {@code null} value. + *

If {@code true}, a {@code null} path location will be populated + * with a default object value and traversed instead of resulting in a + * {@link NullValueInNestedPathException}. + *

Default is {@code false} on a plain PropertyAccessor instance. + */ + void setAutoGrowNestedPaths(boolean autoGrowNestedPaths); + + /** + * Return whether "auto-growing" of nested paths has been activated. + */ + boolean isAutoGrowNestedPaths(); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/ConversionNotSupportedException.java b/spring-beans/src/main/java/org/springframework/beans/ConversionNotSupportedException.java new file mode 100644 index 0000000..41c7f95 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/ConversionNotSupportedException.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.beans.PropertyChangeEvent; + +import org.springframework.lang.Nullable; + +/** + * Exception thrown when no suitable editor or converter can be found for a bean property. + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @since 3.0 + */ +@SuppressWarnings("serial") +public class ConversionNotSupportedException extends TypeMismatchException { + + /** + * Create a new ConversionNotSupportedException. + * @param propertyChangeEvent the PropertyChangeEvent that resulted in the problem + * @param requiredType the required target type (or {@code null} if not known) + * @param cause the root cause (may be {@code null}) + */ + public ConversionNotSupportedException(PropertyChangeEvent propertyChangeEvent, + @Nullable Class requiredType, @Nullable Throwable cause) { + super(propertyChangeEvent, requiredType, cause); + } + + /** + * Create a new ConversionNotSupportedException. + * @param value the offending value that couldn't be converted (may be {@code null}) + * @param requiredType the required target type (or {@code null} if not known) + * @param cause the root cause (may be {@code null}) + */ + public ConversionNotSupportedException(@Nullable Object value, @Nullable Class requiredType, @Nullable Throwable cause) { + super(value, requiredType, cause); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/DirectFieldAccessor.java b/spring-beans/src/main/java/org/springframework/beans/DirectFieldAccessor.java new file mode 100644 index 0000000..a98c6eb --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/DirectFieldAccessor.java @@ -0,0 +1,152 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.core.ResolvableType; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; + +/** + * {@link ConfigurablePropertyAccessor} implementation that directly accesses + * instance fields. Allows for direct binding to fields instead of going through + * JavaBean setters. + * + *

As of Spring 4.2, the vast majority of the {@link BeanWrapper} features have + * been merged to {@link AbstractPropertyAccessor}, which means that property + * traversal as well as collections and map access is now supported here as well. + * + *

A DirectFieldAccessor's default for the "extractOldValueForEditor" setting + * is "true", since a field can always be read without side effects. + * + * @author Juergen Hoeller + * @author Stephane Nicoll + * @since 2.0 + * @see #setExtractOldValueForEditor + * @see BeanWrapper + * @see org.springframework.validation.DirectFieldBindingResult + * @see org.springframework.validation.DataBinder#initDirectFieldAccess() + */ +public class DirectFieldAccessor extends AbstractNestablePropertyAccessor { + + private final Map fieldMap = new HashMap<>(); + + + /** + * Create a new DirectFieldAccessor for the given object. + * @param object the object wrapped by this DirectFieldAccessor + */ + public DirectFieldAccessor(Object object) { + super(object); + } + + /** + * Create a new DirectFieldAccessor for the given object, + * registering a nested path that the object is in. + * @param object the object wrapped by this DirectFieldAccessor + * @param nestedPath the nested path of the object + * @param parent the containing DirectFieldAccessor (must not be {@code null}) + */ + protected DirectFieldAccessor(Object object, String nestedPath, DirectFieldAccessor parent) { + super(object, nestedPath, parent); + } + + + @Override + @Nullable + protected FieldPropertyHandler getLocalPropertyHandler(String propertyName) { + FieldPropertyHandler propertyHandler = this.fieldMap.get(propertyName); + if (propertyHandler == null) { + Field field = ReflectionUtils.findField(getWrappedClass(), propertyName); + if (field != null) { + propertyHandler = new FieldPropertyHandler(field); + this.fieldMap.put(propertyName, propertyHandler); + } + } + return propertyHandler; + } + + @Override + protected DirectFieldAccessor newNestedPropertyAccessor(Object object, String nestedPath) { + return new DirectFieldAccessor(object, nestedPath, this); + } + + @Override + protected NotWritablePropertyException createNotWritablePropertyException(String propertyName) { + PropertyMatches matches = PropertyMatches.forField(propertyName, getRootClass()); + throw new NotWritablePropertyException(getRootClass(), getNestedPath() + propertyName, + matches.buildErrorMessage(), matches.getPossibleMatches()); + } + + + private class FieldPropertyHandler extends PropertyHandler { + + private final Field field; + + public FieldPropertyHandler(Field field) { + super(field.getType(), true, true); + this.field = field; + } + + @Override + public TypeDescriptor toTypeDescriptor() { + return new TypeDescriptor(this.field); + } + + @Override + public ResolvableType getResolvableType() { + return ResolvableType.forField(this.field); + } + + @Override + @Nullable + public TypeDescriptor nested(int level) { + return TypeDescriptor.nested(this.field, level); + } + + @Override + @Nullable + public Object getValue() throws Exception { + try { + ReflectionUtils.makeAccessible(this.field); + return this.field.get(getWrappedInstance()); + } + + catch (IllegalAccessException ex) { + throw new InvalidPropertyException(getWrappedClass(), + this.field.getName(), "Field is not accessible", ex); + } + } + + @Override + public void setValue(@Nullable Object value) throws Exception { + try { + ReflectionUtils.makeAccessible(this.field); + this.field.set(getWrappedInstance(), value); + } + catch (IllegalAccessException ex) { + throw new InvalidPropertyException(getWrappedClass(), this.field.getName(), + "Field is not accessible", ex); + } + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.java b/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.java new file mode 100644 index 0000000..21ce57c --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.java @@ -0,0 +1,552 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.awt.Image; +import java.beans.BeanDescriptor; +import java.beans.BeanInfo; +import java.beans.EventSetDescriptor; +import java.beans.IndexedPropertyDescriptor; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.MethodDescriptor; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +/** + * Decorator for a standard {@link BeanInfo} object, e.g. as created by + * {@link Introspector#getBeanInfo(Class)}, designed to discover and register + * static and/or non-void returning setter methods. For example: + * + *

+ * public class Bean {
+ *
+ *     private Foo foo;
+ *
+ *     public Foo getFoo() {
+ *         return this.foo;
+ *     }
+ *
+ *     public Bean setFoo(Foo foo) {
+ *         this.foo = foo;
+ *         return this;
+ *     }
+ * }
+ * + * The standard JavaBeans {@code Introspector} will discover the {@code getFoo} read + * method, but will bypass the {@code #setFoo(Foo)} write method, because its non-void + * returning signature does not comply with the JavaBeans specification. + * {@code ExtendedBeanInfo}, on the other hand, will recognize and include it. This is + * designed to allow APIs with "builder" or method-chaining style setter signatures to be + * used within Spring {@code } XML. {@link #getPropertyDescriptors()} returns all + * existing property descriptors from the wrapped {@code BeanInfo} as well any added for + * non-void returning setters. Both standard ("non-indexed") and + * + * indexed properties are fully supported. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + * @see #ExtendedBeanInfo(BeanInfo) + * @see ExtendedBeanInfoFactory + * @see CachedIntrospectionResults + */ +class ExtendedBeanInfo implements BeanInfo { + + private static final Log logger = LogFactory.getLog(ExtendedBeanInfo.class); + + private final BeanInfo delegate; + + private final Set propertyDescriptors = new TreeSet<>(new PropertyDescriptorComparator()); + + + /** + * Wrap the given {@link BeanInfo} instance; copy all its existing property descriptors + * locally, wrapping each in a custom {@link SimpleIndexedPropertyDescriptor indexed} + * or {@link SimplePropertyDescriptor non-indexed} {@code PropertyDescriptor} + * variant that bypasses default JDK weak/soft reference management; then search + * through its method descriptors to find any non-void returning write methods and + * update or create the corresponding {@link PropertyDescriptor} for each one found. + * @param delegate the wrapped {@code BeanInfo}, which is never modified + * @see #getPropertyDescriptors() + */ + public ExtendedBeanInfo(BeanInfo delegate) { + this.delegate = delegate; + for (PropertyDescriptor pd : delegate.getPropertyDescriptors()) { + try { + this.propertyDescriptors.add(pd instanceof IndexedPropertyDescriptor ? + new SimpleIndexedPropertyDescriptor((IndexedPropertyDescriptor) pd) : + new SimplePropertyDescriptor(pd)); + } + catch (IntrospectionException ex) { + // Probably simply a method that wasn't meant to follow the JavaBeans pattern... + if (logger.isDebugEnabled()) { + logger.debug("Ignoring invalid bean property '" + pd.getName() + "': " + ex.getMessage()); + } + } + } + MethodDescriptor[] methodDescriptors = delegate.getMethodDescriptors(); + if (methodDescriptors != null) { + for (Method method : findCandidateWriteMethods(methodDescriptors)) { + try { + handleCandidateWriteMethod(method); + } + catch (IntrospectionException ex) { + // We're only trying to find candidates, can easily ignore extra ones here... + if (logger.isDebugEnabled()) { + logger.debug("Ignoring candidate write method [" + method + "]: " + ex.getMessage()); + } + } + } + } + } + + + private List findCandidateWriteMethods(MethodDescriptor[] methodDescriptors) { + List matches = new ArrayList<>(); + for (MethodDescriptor methodDescriptor : methodDescriptors) { + Method method = methodDescriptor.getMethod(); + if (isCandidateWriteMethod(method)) { + matches.add(method); + } + } + // Sort non-void returning write methods to guard against the ill effects of + // non-deterministic sorting of methods returned from Class#getDeclaredMethods + // under JDK 7. See https://bugs.java.com/view_bug.do?bug_id=7023180 + matches.sort((m1, m2) -> m2.toString().compareTo(m1.toString())); + return matches; + } + + public static boolean isCandidateWriteMethod(Method method) { + String methodName = method.getName(); + int nParams = method.getParameterCount(); + return (methodName.length() > 3 && methodName.startsWith("set") && Modifier.isPublic(method.getModifiers()) && + (!void.class.isAssignableFrom(method.getReturnType()) || Modifier.isStatic(method.getModifiers())) && + (nParams == 1 || (nParams == 2 && int.class == method.getParameterTypes()[0]))); + } + + private void handleCandidateWriteMethod(Method method) throws IntrospectionException { + int nParams = method.getParameterCount(); + String propertyName = propertyNameFor(method); + Class propertyType = method.getParameterTypes()[nParams - 1]; + PropertyDescriptor existingPd = findExistingPropertyDescriptor(propertyName, propertyType); + if (nParams == 1) { + if (existingPd == null) { + this.propertyDescriptors.add(new SimplePropertyDescriptor(propertyName, null, method)); + } + else { + existingPd.setWriteMethod(method); + } + } + else if (nParams == 2) { + if (existingPd == null) { + this.propertyDescriptors.add( + new SimpleIndexedPropertyDescriptor(propertyName, null, null, null, method)); + } + else if (existingPd instanceof IndexedPropertyDescriptor) { + ((IndexedPropertyDescriptor) existingPd).setIndexedWriteMethod(method); + } + else { + this.propertyDescriptors.remove(existingPd); + this.propertyDescriptors.add(new SimpleIndexedPropertyDescriptor( + propertyName, existingPd.getReadMethod(), existingPd.getWriteMethod(), null, method)); + } + } + else { + throw new IllegalArgumentException("Write method must have exactly 1 or 2 parameters: " + method); + } + } + + @Nullable + private PropertyDescriptor findExistingPropertyDescriptor(String propertyName, Class propertyType) { + for (PropertyDescriptor pd : this.propertyDescriptors) { + final Class candidateType; + final String candidateName = pd.getName(); + if (pd instanceof IndexedPropertyDescriptor) { + IndexedPropertyDescriptor ipd = (IndexedPropertyDescriptor) pd; + candidateType = ipd.getIndexedPropertyType(); + if (candidateName.equals(propertyName) && + (candidateType.equals(propertyType) || candidateType.equals(propertyType.getComponentType()))) { + return pd; + } + } + else { + candidateType = pd.getPropertyType(); + if (candidateName.equals(propertyName) && + (candidateType.equals(propertyType) || propertyType.equals(candidateType.getComponentType()))) { + return pd; + } + } + } + return null; + } + + private String propertyNameFor(Method method) { + return Introspector.decapitalize(method.getName().substring(3)); + } + + + /** + * Return the set of {@link PropertyDescriptor PropertyDescriptors} from the wrapped + * {@link BeanInfo} object as well as {@code PropertyDescriptors} for each non-void + * returning setter method found during construction. + * @see #ExtendedBeanInfo(BeanInfo) + */ + @Override + public PropertyDescriptor[] getPropertyDescriptors() { + return this.propertyDescriptors.toArray(new PropertyDescriptor[0]); + } + + @Override + public BeanInfo[] getAdditionalBeanInfo() { + return this.delegate.getAdditionalBeanInfo(); + } + + @Override + public BeanDescriptor getBeanDescriptor() { + return this.delegate.getBeanDescriptor(); + } + + @Override + public int getDefaultEventIndex() { + return this.delegate.getDefaultEventIndex(); + } + + @Override + public int getDefaultPropertyIndex() { + return this.delegate.getDefaultPropertyIndex(); + } + + @Override + public EventSetDescriptor[] getEventSetDescriptors() { + return this.delegate.getEventSetDescriptors(); + } + + @Override + public Image getIcon(int iconKind) { + return this.delegate.getIcon(iconKind); + } + + @Override + public MethodDescriptor[] getMethodDescriptors() { + return this.delegate.getMethodDescriptors(); + } + + + /** + * A simple {@link PropertyDescriptor}. + */ + static class SimplePropertyDescriptor extends PropertyDescriptor { + + @Nullable + private Method readMethod; + + @Nullable + private Method writeMethod; + + @Nullable + private Class propertyType; + + @Nullable + private Class propertyEditorClass; + + public SimplePropertyDescriptor(PropertyDescriptor original) throws IntrospectionException { + this(original.getName(), original.getReadMethod(), original.getWriteMethod()); + PropertyDescriptorUtils.copyNonMethodProperties(original, this); + } + + public SimplePropertyDescriptor(String propertyName, @Nullable Method readMethod, Method writeMethod) + throws IntrospectionException { + + super(propertyName, null, null); + this.readMethod = readMethod; + this.writeMethod = writeMethod; + this.propertyType = PropertyDescriptorUtils.findPropertyType(readMethod, writeMethod); + } + + @Override + @Nullable + public Method getReadMethod() { + return this.readMethod; + } + + @Override + public void setReadMethod(@Nullable Method readMethod) { + this.readMethod = readMethod; + } + + @Override + @Nullable + public Method getWriteMethod() { + return this.writeMethod; + } + + @Override + public void setWriteMethod(@Nullable Method writeMethod) { + this.writeMethod = writeMethod; + } + + @Override + @Nullable + public Class getPropertyType() { + if (this.propertyType == null) { + try { + this.propertyType = PropertyDescriptorUtils.findPropertyType(this.readMethod, this.writeMethod); + } + catch (IntrospectionException ex) { + // Ignore, as does PropertyDescriptor#getPropertyType + } + } + return this.propertyType; + } + + @Override + @Nullable + public Class getPropertyEditorClass() { + return this.propertyEditorClass; + } + + @Override + public void setPropertyEditorClass(@Nullable Class propertyEditorClass) { + this.propertyEditorClass = propertyEditorClass; + } + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof PropertyDescriptor && + PropertyDescriptorUtils.equals(this, (PropertyDescriptor) other))); + } + + @Override + public int hashCode() { + return (ObjectUtils.nullSafeHashCode(getReadMethod()) * 29 + ObjectUtils.nullSafeHashCode(getWriteMethod())); + } + + @Override + public String toString() { + return String.format("%s[name=%s, propertyType=%s, readMethod=%s, writeMethod=%s]", + getClass().getSimpleName(), getName(), getPropertyType(), this.readMethod, this.writeMethod); + } + } + + + /** + * A simple {@link IndexedPropertyDescriptor}. + */ + static class SimpleIndexedPropertyDescriptor extends IndexedPropertyDescriptor { + + @Nullable + private Method readMethod; + + @Nullable + private Method writeMethod; + + @Nullable + private Class propertyType; + + @Nullable + private Method indexedReadMethod; + + @Nullable + private Method indexedWriteMethod; + + @Nullable + private Class indexedPropertyType; + + @Nullable + private Class propertyEditorClass; + + public SimpleIndexedPropertyDescriptor(IndexedPropertyDescriptor original) throws IntrospectionException { + this(original.getName(), original.getReadMethod(), original.getWriteMethod(), + original.getIndexedReadMethod(), original.getIndexedWriteMethod()); + PropertyDescriptorUtils.copyNonMethodProperties(original, this); + } + + public SimpleIndexedPropertyDescriptor(String propertyName, @Nullable Method readMethod, + @Nullable Method writeMethod, @Nullable Method indexedReadMethod, Method indexedWriteMethod) + throws IntrospectionException { + + super(propertyName, null, null, null, null); + this.readMethod = readMethod; + this.writeMethod = writeMethod; + this.propertyType = PropertyDescriptorUtils.findPropertyType(readMethod, writeMethod); + this.indexedReadMethod = indexedReadMethod; + this.indexedWriteMethod = indexedWriteMethod; + this.indexedPropertyType = PropertyDescriptorUtils.findIndexedPropertyType( + propertyName, this.propertyType, indexedReadMethod, indexedWriteMethod); + } + + @Override + @Nullable + public Method getReadMethod() { + return this.readMethod; + } + + @Override + public void setReadMethod(@Nullable Method readMethod) { + this.readMethod = readMethod; + } + + @Override + @Nullable + public Method getWriteMethod() { + return this.writeMethod; + } + + @Override + public void setWriteMethod(@Nullable Method writeMethod) { + this.writeMethod = writeMethod; + } + + @Override + @Nullable + public Class getPropertyType() { + if (this.propertyType == null) { + try { + this.propertyType = PropertyDescriptorUtils.findPropertyType(this.readMethod, this.writeMethod); + } + catch (IntrospectionException ex) { + // Ignore, as does IndexedPropertyDescriptor#getPropertyType + } + } + return this.propertyType; + } + + @Override + @Nullable + public Method getIndexedReadMethod() { + return this.indexedReadMethod; + } + + @Override + public void setIndexedReadMethod(@Nullable Method indexedReadMethod) throws IntrospectionException { + this.indexedReadMethod = indexedReadMethod; + } + + @Override + @Nullable + public Method getIndexedWriteMethod() { + return this.indexedWriteMethod; + } + + @Override + public void setIndexedWriteMethod(@Nullable Method indexedWriteMethod) throws IntrospectionException { + this.indexedWriteMethod = indexedWriteMethod; + } + + @Override + @Nullable + public Class getIndexedPropertyType() { + if (this.indexedPropertyType == null) { + try { + this.indexedPropertyType = PropertyDescriptorUtils.findIndexedPropertyType( + getName(), getPropertyType(), this.indexedReadMethod, this.indexedWriteMethod); + } + catch (IntrospectionException ex) { + // Ignore, as does IndexedPropertyDescriptor#getIndexedPropertyType + } + } + return this.indexedPropertyType; + } + + @Override + @Nullable + public Class getPropertyEditorClass() { + return this.propertyEditorClass; + } + + @Override + public void setPropertyEditorClass(@Nullable Class propertyEditorClass) { + this.propertyEditorClass = propertyEditorClass; + } + + /* + * See java.beans.IndexedPropertyDescriptor#equals + */ + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof IndexedPropertyDescriptor)) { + return false; + } + IndexedPropertyDescriptor otherPd = (IndexedPropertyDescriptor) other; + return (ObjectUtils.nullSafeEquals(getIndexedReadMethod(), otherPd.getIndexedReadMethod()) && + ObjectUtils.nullSafeEquals(getIndexedWriteMethod(), otherPd.getIndexedWriteMethod()) && + ObjectUtils.nullSafeEquals(getIndexedPropertyType(), otherPd.getIndexedPropertyType()) && + PropertyDescriptorUtils.equals(this, otherPd)); + } + + @Override + public int hashCode() { + int hashCode = ObjectUtils.nullSafeHashCode(getReadMethod()); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getWriteMethod()); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getIndexedReadMethod()); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getIndexedWriteMethod()); + return hashCode; + } + + @Override + public String toString() { + return String.format("%s[name=%s, propertyType=%s, indexedPropertyType=%s, " + + "readMethod=%s, writeMethod=%s, indexedReadMethod=%s, indexedWriteMethod=%s]", + getClass().getSimpleName(), getName(), getPropertyType(), getIndexedPropertyType(), + this.readMethod, this.writeMethod, this.indexedReadMethod, this.indexedWriteMethod); + } + } + + + /** + * Sorts PropertyDescriptor instances alpha-numerically to emulate the behavior of + * {@link java.beans.BeanInfo#getPropertyDescriptors()}. + * @see ExtendedBeanInfo#propertyDescriptors + */ + static class PropertyDescriptorComparator implements Comparator { + + @Override + public int compare(PropertyDescriptor desc1, PropertyDescriptor desc2) { + String left = desc1.getName(); + String right = desc2.getName(); + byte[] leftBytes = left.getBytes(); + byte[] rightBytes = right.getBytes(); + for (int i = 0; i < left.length(); i++) { + if (right.length() == i) { + return 1; + } + int result = leftBytes[i] - rightBytes[i]; + if (result != 0) { + return result; + } + } + return left.length() - right.length(); + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfoFactory.java b/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfoFactory.java new file mode 100644 index 0000000..d7d2b2e --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfoFactory.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.lang.reflect.Method; + +import org.springframework.core.Ordered; +import org.springframework.lang.Nullable; + +/** + * {@link BeanInfoFactory} implementation that evaluates whether bean classes have + * "non-standard" JavaBeans setter methods and are thus candidates for introspection + * by Spring's (package-visible) {@code ExtendedBeanInfo} implementation. + * + *

Ordered at {@link Ordered#LOWEST_PRECEDENCE} to allow other user-defined + * {@link BeanInfoFactory} types to take precedence. + * + * @author Chris Beams + * @since 3.2 + * @see BeanInfoFactory + * @see CachedIntrospectionResults + */ +public class ExtendedBeanInfoFactory implements BeanInfoFactory, Ordered { + + /** + * Return an {@link ExtendedBeanInfo} for the given bean class, if applicable. + */ + @Override + @Nullable + public BeanInfo getBeanInfo(Class beanClass) throws IntrospectionException { + return (supports(beanClass) ? new ExtendedBeanInfo(Introspector.getBeanInfo(beanClass)) : null); + } + + /** + * Return whether the given bean class declares or inherits any non-void + * returning bean property or indexed property setter methods. + */ + private boolean supports(Class beanClass) { + for (Method method : beanClass.getMethods()) { + if (ExtendedBeanInfo.isCandidateWriteMethod(method)) { + return true; + } + } + return false; + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/FatalBeanException.java b/spring-beans/src/main/java/org/springframework/beans/FatalBeanException.java new file mode 100644 index 0000000..7c6e1d9 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/FatalBeanException.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import org.springframework.lang.Nullable; + +/** + * Thrown on an unrecoverable problem encountered in the + * beans packages or sub-packages, e.g. bad class or field. + * + * @author Rod Johnson + */ +@SuppressWarnings("serial") +public class FatalBeanException extends BeansException { + + /** + * Create a new FatalBeanException with the specified message. + * @param msg the detail message + */ + public FatalBeanException(String msg) { + super(msg); + } + + /** + * Create a new FatalBeanException with the specified message + * and root cause. + * @param msg the detail message + * @param cause the root cause + */ + public FatalBeanException(String msg, @Nullable Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/GenericTypeAwarePropertyDescriptor.java b/spring-beans/src/main/java/org/springframework/beans/GenericTypeAwarePropertyDescriptor.java new file mode 100644 index 0000000..603f5aa --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/GenericTypeAwarePropertyDescriptor.java @@ -0,0 +1,186 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.beans.IntrospectionException; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Set; + +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.MethodParameter; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Extension of the standard JavaBeans {@link PropertyDescriptor} class, + * overriding {@code getPropertyType()} such that a generically declared + * type variable will be resolved against the containing bean class. + * + * @author Juergen Hoeller + * @since 2.5.2 + */ +final class GenericTypeAwarePropertyDescriptor extends PropertyDescriptor { + + private final Class beanClass; + + @Nullable + private final Method readMethod; + + @Nullable + private final Method writeMethod; + + @Nullable + private volatile Set ambiguousWriteMethods; + + @Nullable + private MethodParameter writeMethodParameter; + + @Nullable + private Class propertyType; + + @Nullable + private final Class propertyEditorClass; + + + public GenericTypeAwarePropertyDescriptor(Class beanClass, String propertyName, + @Nullable Method readMethod, @Nullable Method writeMethod, + @Nullable Class propertyEditorClass) throws IntrospectionException { + + super(propertyName, null, null); + this.beanClass = beanClass; + + Method readMethodToUse = (readMethod != null ? BridgeMethodResolver.findBridgedMethod(readMethod) : null); + Method writeMethodToUse = (writeMethod != null ? BridgeMethodResolver.findBridgedMethod(writeMethod) : null); + if (writeMethodToUse == null && readMethodToUse != null) { + // Fallback: Original JavaBeans introspection might not have found matching setter + // method due to lack of bridge method resolution, in case of the getter using a + // covariant return type whereas the setter is defined for the concrete property type. + Method candidate = ClassUtils.getMethodIfAvailable( + this.beanClass, "set" + StringUtils.capitalize(getName()), (Class[]) null); + if (candidate != null && candidate.getParameterCount() == 1) { + writeMethodToUse = candidate; + } + } + this.readMethod = readMethodToUse; + this.writeMethod = writeMethodToUse; + + if (this.writeMethod != null) { + if (this.readMethod == null) { + // Write method not matched against read method: potentially ambiguous through + // several overloaded variants, in which case an arbitrary winner has been chosen + // by the JDK's JavaBeans Introspector... + Set ambiguousCandidates = new HashSet<>(); + for (Method method : beanClass.getMethods()) { + if (method.getName().equals(writeMethodToUse.getName()) && + !method.equals(writeMethodToUse) && !method.isBridge() && + method.getParameterCount() == writeMethodToUse.getParameterCount()) { + ambiguousCandidates.add(method); + } + } + if (!ambiguousCandidates.isEmpty()) { + this.ambiguousWriteMethods = ambiguousCandidates; + } + } + this.writeMethodParameter = new MethodParameter(this.writeMethod, 0).withContainingClass(this.beanClass); + } + + if (this.readMethod != null) { + this.propertyType = GenericTypeResolver.resolveReturnType(this.readMethod, this.beanClass); + } + else if (this.writeMethodParameter != null) { + this.propertyType = this.writeMethodParameter.getParameterType(); + } + + this.propertyEditorClass = propertyEditorClass; + } + + + public Class getBeanClass() { + return this.beanClass; + } + + @Override + @Nullable + public Method getReadMethod() { + return this.readMethod; + } + + @Override + @Nullable + public Method getWriteMethod() { + return this.writeMethod; + } + + public Method getWriteMethodForActualAccess() { + Assert.state(this.writeMethod != null, "No write method available"); + Set ambiguousCandidates = this.ambiguousWriteMethods; + if (ambiguousCandidates != null) { + this.ambiguousWriteMethods = null; + LogFactory.getLog(GenericTypeAwarePropertyDescriptor.class).warn("Invalid JavaBean property '" + + getName() + "' being accessed! Ambiguous write methods found next to actually used [" + + this.writeMethod + "]: " + ambiguousCandidates); + } + return this.writeMethod; + } + + public MethodParameter getWriteMethodParameter() { + Assert.state(this.writeMethodParameter != null, "No write method available"); + return this.writeMethodParameter; + } + + @Override + @Nullable + public Class getPropertyType() { + return this.propertyType; + } + + @Override + @Nullable + public Class getPropertyEditorClass() { + return this.propertyEditorClass; + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof GenericTypeAwarePropertyDescriptor)) { + return false; + } + GenericTypeAwarePropertyDescriptor otherPd = (GenericTypeAwarePropertyDescriptor) other; + return (getBeanClass().equals(otherPd.getBeanClass()) && PropertyDescriptorUtils.equals(this, otherPd)); + } + + @Override + public int hashCode() { + int hashCode = getBeanClass().hashCode(); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getReadMethod()); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getWriteMethod()); + return hashCode; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/InvalidPropertyException.java b/spring-beans/src/main/java/org/springframework/beans/InvalidPropertyException.java new file mode 100644 index 0000000..c0d0f50 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/InvalidPropertyException.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import org.springframework.lang.Nullable; + +/** + * Exception thrown when referring to an invalid bean property. + * Carries the offending bean class and property name. + * + * @author Juergen Hoeller + * @since 1.0.2 + */ +@SuppressWarnings("serial") +public class InvalidPropertyException extends FatalBeanException { + + private final Class beanClass; + + private final String propertyName; + + + /** + * Create a new InvalidPropertyException. + * @param beanClass the offending bean class + * @param propertyName the offending property + * @param msg the detail message + */ + public InvalidPropertyException(Class beanClass, String propertyName, String msg) { + this(beanClass, propertyName, msg, null); + } + + /** + * Create a new InvalidPropertyException. + * @param beanClass the offending bean class + * @param propertyName the offending property + * @param msg the detail message + * @param cause the root cause + */ + public InvalidPropertyException(Class beanClass, String propertyName, String msg, @Nullable Throwable cause) { + super("Invalid property '" + propertyName + "' of bean class [" + beanClass.getName() + "]: " + msg, cause); + this.beanClass = beanClass; + this.propertyName = propertyName; + } + + /** + * Return the offending bean class. + */ + public Class getBeanClass() { + return this.beanClass; + } + + /** + * Return the name of the offending property. + */ + public String getPropertyName() { + return this.propertyName; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/Mergeable.java b/spring-beans/src/main/java/org/springframework/beans/Mergeable.java new file mode 100644 index 0000000..41d5052 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/Mergeable.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import org.springframework.lang.Nullable; + +/** + * Interface representing an object whose value set can be merged with + * that of a parent object. + * + * @author Rob Harrop + * @since 2.0 + * @see org.springframework.beans.factory.support.ManagedSet + * @see org.springframework.beans.factory.support.ManagedList + * @see org.springframework.beans.factory.support.ManagedMap + * @see org.springframework.beans.factory.support.ManagedProperties + */ +public interface Mergeable { + + /** + * Is merging enabled for this particular instance? + */ + boolean isMergeEnabled(); + + /** + * Merge the current value set with that of the supplied object. + *

The supplied object is considered the parent, and values in + * the callee's value set must override those of the supplied object. + * @param parent the object to merge with + * @return the result of the merge operation + * @throws IllegalArgumentException if the supplied parent is {@code null} + * @throws IllegalStateException if merging is not enabled for this instance + * (i.e. {@code mergeEnabled} equals {@code false}). + */ + Object merge(@Nullable Object parent); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/MethodInvocationException.java b/spring-beans/src/main/java/org/springframework/beans/MethodInvocationException.java new file mode 100644 index 0000000..8a0af42 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/MethodInvocationException.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.beans.PropertyChangeEvent; + +/** + * Thrown when a bean property getter or setter method throws an exception, + * analogous to an InvocationTargetException. + * + * @author Rod Johnson + */ +@SuppressWarnings("serial") +public class MethodInvocationException extends PropertyAccessException { + + /** + * Error code that a method invocation error will be registered with. + */ + public static final String ERROR_CODE = "methodInvocation"; + + + /** + * Create a new MethodInvocationException. + * @param propertyChangeEvent the PropertyChangeEvent that resulted in an exception + * @param cause the Throwable raised by the invoked method + */ + public MethodInvocationException(PropertyChangeEvent propertyChangeEvent, Throwable cause) { + super(propertyChangeEvent, "Property '" + propertyChangeEvent.getPropertyName() + "' threw exception", cause); + } + + @Override + public String getErrorCode() { + return ERROR_CODE; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/MutablePropertyValues.java b/spring-beans/src/main/java/org/springframework/beans/MutablePropertyValues.java new file mode 100644 index 0000000..97c0a0a --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/MutablePropertyValues.java @@ -0,0 +1,389 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Stream; + +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * The default implementation of the {@link PropertyValues} interface. + * Allows simple manipulation of properties, and provides constructors + * to support deep copy and construction from a Map. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Rob Harrop + * @since 13 May 2001 + */ +@SuppressWarnings("serial") +public class MutablePropertyValues implements PropertyValues, Serializable { + + private final List propertyValueList; + + @Nullable + private Set processedProperties; + + private volatile boolean converted; + + + /** + * Creates a new empty MutablePropertyValues object. + *

Property values can be added with the {@code add} method. + * @see #add(String, Object) + */ + public MutablePropertyValues() { + this.propertyValueList = new ArrayList<>(0); + } + + /** + * Deep copy constructor. Guarantees PropertyValue references + * are independent, although it can't deep copy objects currently + * referenced by individual PropertyValue objects. + * @param original the PropertyValues to copy + * @see #addPropertyValues(PropertyValues) + */ + public MutablePropertyValues(@Nullable PropertyValues original) { + // We can optimize this because it's all new: + // There is no replacement of existing property values. + if (original != null) { + PropertyValue[] pvs = original.getPropertyValues(); + this.propertyValueList = new ArrayList<>(pvs.length); + for (PropertyValue pv : pvs) { + this.propertyValueList.add(new PropertyValue(pv)); + } + } + else { + this.propertyValueList = new ArrayList<>(0); + } + } + + /** + * Construct a new MutablePropertyValues object from a Map. + * @param original a Map with property values keyed by property name Strings + * @see #addPropertyValues(Map) + */ + public MutablePropertyValues(@Nullable Map original) { + // We can optimize this because it's all new: + // There is no replacement of existing property values. + if (original != null) { + this.propertyValueList = new ArrayList<>(original.size()); + original.forEach((attrName, attrValue) -> this.propertyValueList.add( + new PropertyValue(attrName.toString(), attrValue))); + } + else { + this.propertyValueList = new ArrayList<>(0); + } + } + + /** + * Construct a new MutablePropertyValues object using the given List of + * PropertyValue objects as-is. + *

This is a constructor for advanced usage scenarios. + * It is not intended for typical programmatic use. + * @param propertyValueList a List of PropertyValue objects + */ + public MutablePropertyValues(@Nullable List propertyValueList) { + this.propertyValueList = + (propertyValueList != null ? propertyValueList : new ArrayList<>()); + } + + + /** + * Return the underlying List of PropertyValue objects in its raw form. + * The returned List can be modified directly, although this is not recommended. + *

This is an accessor for optimized access to all PropertyValue objects. + * It is not intended for typical programmatic use. + */ + public List getPropertyValueList() { + return this.propertyValueList; + } + + /** + * Return the number of PropertyValue entries in the list. + */ + public int size() { + return this.propertyValueList.size(); + } + + /** + * Copy all given PropertyValues into this object. Guarantees PropertyValue + * references are independent, although it can't deep copy objects currently + * referenced by individual PropertyValue objects. + * @param other the PropertyValues to copy + * @return this in order to allow for adding multiple property values in a chain + */ + public MutablePropertyValues addPropertyValues(@Nullable PropertyValues other) { + if (other != null) { + PropertyValue[] pvs = other.getPropertyValues(); + for (PropertyValue pv : pvs) { + addPropertyValue(new PropertyValue(pv)); + } + } + return this; + } + + /** + * Add all property values from the given Map. + * @param other a Map with property values keyed by property name, + * which must be a String + * @return this in order to allow for adding multiple property values in a chain + */ + public MutablePropertyValues addPropertyValues(@Nullable Map other) { + if (other != null) { + other.forEach((attrName, attrValue) -> addPropertyValue( + new PropertyValue(attrName.toString(), attrValue))); + } + return this; + } + + /** + * Add a PropertyValue object, replacing any existing one for the + * corresponding property or getting merged with it (if applicable). + * @param pv the PropertyValue object to add + * @return this in order to allow for adding multiple property values in a chain + */ + public MutablePropertyValues addPropertyValue(PropertyValue pv) { + for (int i = 0; i < this.propertyValueList.size(); i++) { + PropertyValue currentPv = this.propertyValueList.get(i); + if (currentPv.getName().equals(pv.getName())) { + pv = mergeIfRequired(pv, currentPv); + setPropertyValueAt(pv, i); + return this; + } + } + this.propertyValueList.add(pv); + return this; + } + + /** + * Overloaded version of {@code addPropertyValue} that takes + * a property name and a property value. + *

Note: As of Spring 3.0, we recommend using the more concise + * and chaining-capable variant {@link #add}. + * @param propertyName name of the property + * @param propertyValue value of the property + * @see #addPropertyValue(PropertyValue) + */ + public void addPropertyValue(String propertyName, Object propertyValue) { + addPropertyValue(new PropertyValue(propertyName, propertyValue)); + } + + /** + * Add a PropertyValue object, replacing any existing one for the + * corresponding property or getting merged with it (if applicable). + * @param propertyName name of the property + * @param propertyValue value of the property + * @return this in order to allow for adding multiple property values in a chain + */ + public MutablePropertyValues add(String propertyName, @Nullable Object propertyValue) { + addPropertyValue(new PropertyValue(propertyName, propertyValue)); + return this; + } + + /** + * Modify a PropertyValue object held in this object. + * Indexed from 0. + */ + public void setPropertyValueAt(PropertyValue pv, int i) { + this.propertyValueList.set(i, pv); + } + + /** + * Merges the value of the supplied 'new' {@link PropertyValue} with that of + * the current {@link PropertyValue} if merging is supported and enabled. + * @see Mergeable + */ + private PropertyValue mergeIfRequired(PropertyValue newPv, PropertyValue currentPv) { + Object value = newPv.getValue(); + if (value instanceof Mergeable) { + Mergeable mergeable = (Mergeable) value; + if (mergeable.isMergeEnabled()) { + Object merged = mergeable.merge(currentPv.getValue()); + return new PropertyValue(newPv.getName(), merged); + } + } + return newPv; + } + + /** + * Remove the given PropertyValue, if contained. + * @param pv the PropertyValue to remove + */ + public void removePropertyValue(PropertyValue pv) { + this.propertyValueList.remove(pv); + } + + /** + * Overloaded version of {@code removePropertyValue} that takes a property name. + * @param propertyName name of the property + * @see #removePropertyValue(PropertyValue) + */ + public void removePropertyValue(String propertyName) { + this.propertyValueList.remove(getPropertyValue(propertyName)); + } + + + @Override + public Iterator iterator() { + return Collections.unmodifiableList(this.propertyValueList).iterator(); + } + + @Override + public Spliterator spliterator() { + return Spliterators.spliterator(this.propertyValueList, 0); + } + + @Override + public Stream stream() { + return this.propertyValueList.stream(); + } + + @Override + public PropertyValue[] getPropertyValues() { + return this.propertyValueList.toArray(new PropertyValue[0]); + } + + @Override + @Nullable + public PropertyValue getPropertyValue(String propertyName) { + for (PropertyValue pv : this.propertyValueList) { + if (pv.getName().equals(propertyName)) { + return pv; + } + } + return null; + } + + /** + * Get the raw property value, if any. + * @param propertyName the name to search for + * @return the raw property value, or {@code null} if none found + * @since 4.0 + * @see #getPropertyValue(String) + * @see PropertyValue#getValue() + */ + @Nullable + public Object get(String propertyName) { + PropertyValue pv = getPropertyValue(propertyName); + return (pv != null ? pv.getValue() : null); + } + + @Override + public PropertyValues changesSince(PropertyValues old) { + MutablePropertyValues changes = new MutablePropertyValues(); + if (old == this) { + return changes; + } + + // for each property value in the new set + for (PropertyValue newPv : this.propertyValueList) { + // if there wasn't an old one, add it + PropertyValue pvOld = old.getPropertyValue(newPv.getName()); + if (pvOld == null || !pvOld.equals(newPv)) { + changes.addPropertyValue(newPv); + } + } + return changes; + } + + @Override + public boolean contains(String propertyName) { + return (getPropertyValue(propertyName) != null || + (this.processedProperties != null && this.processedProperties.contains(propertyName))); + } + + @Override + public boolean isEmpty() { + return this.propertyValueList.isEmpty(); + } + + + /** + * Register the specified property as "processed" in the sense + * of some processor calling the corresponding setter method + * outside of the PropertyValue(s) mechanism. + *

This will lead to {@code true} being returned from + * a {@link #contains} call for the specified property. + * @param propertyName the name of the property. + */ + public void registerProcessedProperty(String propertyName) { + if (this.processedProperties == null) { + this.processedProperties = new HashSet<>(4); + } + this.processedProperties.add(propertyName); + } + + /** + * Clear the "processed" registration of the given property, if any. + * @since 3.2.13 + */ + public void clearProcessedProperty(String propertyName) { + if (this.processedProperties != null) { + this.processedProperties.remove(propertyName); + } + } + + /** + * Mark this holder as containing converted values only + * (i.e. no runtime resolution needed anymore). + */ + public void setConverted() { + this.converted = true; + } + + /** + * Return whether this holder contains converted values only ({@code true}), + * or whether the values still need to be converted ({@code false}). + */ + public boolean isConverted() { + return this.converted; + } + + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof MutablePropertyValues && + this.propertyValueList.equals(((MutablePropertyValues) other).propertyValueList))); + } + + @Override + public int hashCode() { + return this.propertyValueList.hashCode(); + } + + @Override + public String toString() { + PropertyValue[] pvs = getPropertyValues(); + if (pvs.length > 0) { + return "PropertyValues: length=" + pvs.length + "; " + StringUtils.arrayToDelimitedString(pvs, "; "); + } + return "PropertyValues: length=0"; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/NotReadablePropertyException.java b/spring-beans/src/main/java/org/springframework/beans/NotReadablePropertyException.java new file mode 100644 index 0000000..52d3bef --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/NotReadablePropertyException.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +/** + * Exception thrown on an attempt to get the value of a property + * that isn't readable, because there's no getter method. + * + * @author Juergen Hoeller + * @since 1.0.2 + */ +@SuppressWarnings("serial") +public class NotReadablePropertyException extends InvalidPropertyException { + + /** + * Create a new NotReadablePropertyException. + * @param beanClass the offending bean class + * @param propertyName the offending property + */ + public NotReadablePropertyException(Class beanClass, String propertyName) { + super(beanClass, propertyName, + "Bean property '" + propertyName + "' is not readable or has an invalid getter method: " + + "Does the return type of the getter match the parameter type of the setter?"); + } + + /** + * Create a new NotReadablePropertyException. + * @param beanClass the offending bean class + * @param propertyName the offending property + * @param msg the detail message + */ + public NotReadablePropertyException(Class beanClass, String propertyName, String msg) { + super(beanClass, propertyName, msg); + } + + /** + * Create a new NotReadablePropertyException. + * @param beanClass the offending bean class + * @param propertyName the offending property + * @param msg the detail message + * @param cause the root cause + * @since 4.0.9 + */ + public NotReadablePropertyException(Class beanClass, String propertyName, String msg, Throwable cause) { + super(beanClass, propertyName, msg, cause); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/NotWritablePropertyException.java b/spring-beans/src/main/java/org/springframework/beans/NotWritablePropertyException.java new file mode 100644 index 0000000..79e017e --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/NotWritablePropertyException.java @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import org.springframework.lang.Nullable; + +/** + * Exception thrown on an attempt to set the value of a property that + * is not writable (typically because there is no setter method). + * + * @author Rod Johnson + * @author Alef Arendsen + * @author Arjen Poutsma + */ +@SuppressWarnings("serial") +public class NotWritablePropertyException extends InvalidPropertyException { + + @Nullable + private final String[] possibleMatches; + + + /** + * Create a new NotWritablePropertyException. + * @param beanClass the offending bean class + * @param propertyName the offending property name + */ + public NotWritablePropertyException(Class beanClass, String propertyName) { + super(beanClass, propertyName, + "Bean property '" + propertyName + "' is not writable or has an invalid setter method: " + + "Does the return type of the getter match the parameter type of the setter?"); + this.possibleMatches = null; + } + + /** + * Create a new NotWritablePropertyException. + * @param beanClass the offending bean class + * @param propertyName the offending property name + * @param msg the detail message + */ + public NotWritablePropertyException(Class beanClass, String propertyName, String msg) { + super(beanClass, propertyName, msg); + this.possibleMatches = null; + } + + /** + * Create a new NotWritablePropertyException. + * @param beanClass the offending bean class + * @param propertyName the offending property name + * @param msg the detail message + * @param cause the root cause + */ + public NotWritablePropertyException(Class beanClass, String propertyName, String msg, Throwable cause) { + super(beanClass, propertyName, msg, cause); + this.possibleMatches = null; + } + + /** + * Create a new NotWritablePropertyException. + * @param beanClass the offending bean class + * @param propertyName the offending property name + * @param msg the detail message + * @param possibleMatches suggestions for actual bean property names + * that closely match the invalid property name + */ + public NotWritablePropertyException(Class beanClass, String propertyName, String msg, String[] possibleMatches) { + super(beanClass, propertyName, msg); + this.possibleMatches = possibleMatches; + } + + + /** + * Return suggestions for actual bean property names that closely match + * the invalid property name, if any. + */ + @Nullable + public String[] getPossibleMatches() { + return this.possibleMatches; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/NullValueInNestedPathException.java b/spring-beans/src/main/java/org/springframework/beans/NullValueInNestedPathException.java new file mode 100644 index 0000000..efb4d3a --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/NullValueInNestedPathException.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +/** + * Exception thrown when navigation of a valid nested property + * path encounters a NullPointerException. + * + *

For example, navigating "spouse.age" could fail because the + * spouse property of the target object has a null value. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +@SuppressWarnings("serial") +public class NullValueInNestedPathException extends InvalidPropertyException { + + /** + * Create a new NullValueInNestedPathException. + * @param beanClass the offending bean class + * @param propertyName the offending property + */ + public NullValueInNestedPathException(Class beanClass, String propertyName) { + super(beanClass, propertyName, "Value of nested property '" + propertyName + "' is null"); + } + + /** + * Create a new NullValueInNestedPathException. + * @param beanClass the offending bean class + * @param propertyName the offending property + * @param msg the detail message + */ + public NullValueInNestedPathException(Class beanClass, String propertyName, String msg) { + super(beanClass, propertyName, msg); + } + + /** + * Create a new NullValueInNestedPathException. + * @param beanClass the offending bean class + * @param propertyName the offending property + * @param msg the detail message + * @param cause the root cause + * @since 4.3.2 + */ + public NullValueInNestedPathException(Class beanClass, String propertyName, String msg, Throwable cause) { + super(beanClass, propertyName, msg, cause); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyAccessException.java b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessException.java new file mode 100644 index 0000000..7789437 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessException.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.beans.PropertyChangeEvent; + +import org.springframework.lang.Nullable; + +/** + * Superclass for exceptions related to a property access, + * such as type mismatch or invocation target exception. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +@SuppressWarnings("serial") +public abstract class PropertyAccessException extends BeansException { + + @Nullable + private final PropertyChangeEvent propertyChangeEvent; + + + /** + * Create a new PropertyAccessException. + * @param propertyChangeEvent the PropertyChangeEvent that resulted in the problem + * @param msg the detail message + * @param cause the root cause + */ + public PropertyAccessException(PropertyChangeEvent propertyChangeEvent, String msg, @Nullable Throwable cause) { + super(msg, cause); + this.propertyChangeEvent = propertyChangeEvent; + } + + /** + * Create a new PropertyAccessException without PropertyChangeEvent. + * @param msg the detail message + * @param cause the root cause + */ + public PropertyAccessException(String msg, @Nullable Throwable cause) { + super(msg, cause); + this.propertyChangeEvent = null; + } + + + /** + * Return the PropertyChangeEvent that resulted in the problem. + *

May be {@code null}; only available if an actual bean property + * was affected. + */ + @Nullable + public PropertyChangeEvent getPropertyChangeEvent() { + return this.propertyChangeEvent; + } + + /** + * Return the name of the affected property, if available. + */ + @Nullable + public String getPropertyName() { + return (this.propertyChangeEvent != null ? this.propertyChangeEvent.getPropertyName() : null); + } + + /** + * Return the affected value that was about to be set, if any. + */ + @Nullable + public Object getValue() { + return (this.propertyChangeEvent != null ? this.propertyChangeEvent.getNewValue() : null); + } + + /** + * Return a corresponding error code for this type of exception. + */ + public abstract String getErrorCode(); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyAccessor.java b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessor.java new file mode 100644 index 0000000..3a417aa --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessor.java @@ -0,0 +1,235 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.util.Map; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.lang.Nullable; + +/** + * Common interface for classes that can access named properties + * (such as bean properties of an object or fields in an object) + * Serves as base interface for {@link BeanWrapper}. + * + * @author Juergen Hoeller + * @since 1.1 + * @see BeanWrapper + * @see PropertyAccessorFactory#forBeanPropertyAccess + * @see PropertyAccessorFactory#forDirectFieldAccess + */ +public interface PropertyAccessor { + + /** + * Path separator for nested properties. + * Follows normal Java conventions: getFoo().getBar() would be "foo.bar". + */ + String NESTED_PROPERTY_SEPARATOR = "."; + + /** + * Path separator for nested properties. + * Follows normal Java conventions: getFoo().getBar() would be "foo.bar". + */ + char NESTED_PROPERTY_SEPARATOR_CHAR = '.'; + + /** + * Marker that indicates the start of a property key for an + * indexed or mapped property like "person.addresses[0]". + */ + String PROPERTY_KEY_PREFIX = "["; + + /** + * Marker that indicates the start of a property key for an + * indexed or mapped property like "person.addresses[0]". + */ + char PROPERTY_KEY_PREFIX_CHAR = '['; + + /** + * Marker that indicates the end of a property key for an + * indexed or mapped property like "person.addresses[0]". + */ + String PROPERTY_KEY_SUFFIX = "]"; + + /** + * Marker that indicates the end of a property key for an + * indexed or mapped property like "person.addresses[0]". + */ + char PROPERTY_KEY_SUFFIX_CHAR = ']'; + + + /** + * Determine whether the specified property is readable. + *

Returns {@code false} if the property doesn't exist. + * @param propertyName the property to check + * (may be a nested path and/or an indexed/mapped property) + * @return whether the property is readable + */ + boolean isReadableProperty(String propertyName); + + /** + * Determine whether the specified property is writable. + *

Returns {@code false} if the property doesn't exist. + * @param propertyName the property to check + * (may be a nested path and/or an indexed/mapped property) + * @return whether the property is writable + */ + boolean isWritableProperty(String propertyName); + + /** + * Determine the property type for the specified property, + * either checking the property descriptor or checking the value + * in case of an indexed or mapped element. + * @param propertyName the property to check + * (may be a nested path and/or an indexed/mapped property) + * @return the property type for the particular property, + * or {@code null} if not determinable + * @throws PropertyAccessException if the property was valid but the + * accessor method failed + */ + @Nullable + Class getPropertyType(String propertyName) throws BeansException; + + /** + * Return a type descriptor for the specified property: + * preferably from the read method, falling back to the write method. + * @param propertyName the property to check + * (may be a nested path and/or an indexed/mapped property) + * @return the property type for the particular property, + * or {@code null} if not determinable + * @throws PropertyAccessException if the property was valid but the + * accessor method failed + */ + @Nullable + TypeDescriptor getPropertyTypeDescriptor(String propertyName) throws BeansException; + + /** + * Get the current value of the specified property. + * @param propertyName the name of the property to get the value of + * (may be a nested path and/or an indexed/mapped property) + * @return the value of the property + * @throws InvalidPropertyException if there is no such property or + * if the property isn't readable + * @throws PropertyAccessException if the property was valid but the + * accessor method failed + */ + @Nullable + Object getPropertyValue(String propertyName) throws BeansException; + + /** + * Set the specified value as current property value. + * @param propertyName the name of the property to set the value of + * (may be a nested path and/or an indexed/mapped property) + * @param value the new value + * @throws InvalidPropertyException if there is no such property or + * if the property isn't writable + * @throws PropertyAccessException if the property was valid but the + * accessor method failed or a type mismatch occurred + */ + void setPropertyValue(String propertyName, @Nullable Object value) throws BeansException; + + /** + * Set the specified value as current property value. + * @param pv an object containing the new property value + * @throws InvalidPropertyException if there is no such property or + * if the property isn't writable + * @throws PropertyAccessException if the property was valid but the + * accessor method failed or a type mismatch occurred + */ + void setPropertyValue(PropertyValue pv) throws BeansException; + + /** + * Perform a batch update from a Map. + *

Bulk updates from PropertyValues are more powerful: This method is + * provided for convenience. Behavior will be identical to that of + * the {@link #setPropertyValues(PropertyValues)} method. + * @param map a Map to take properties from. Contains property value objects, + * keyed by property name + * @throws InvalidPropertyException if there is no such property or + * if the property isn't writable + * @throws PropertyBatchUpdateException if one or more PropertyAccessExceptions + * occurred for specific properties during the batch update. This exception bundles + * all individual PropertyAccessExceptions. All other properties will have been + * successfully updated. + */ + void setPropertyValues(Map map) throws BeansException; + + /** + * The preferred way to perform a batch update. + *

Note that performing a batch update differs from performing a single update, + * in that an implementation of this class will continue to update properties + * if a recoverable error (such as a type mismatch, but not an + * invalid field name or the like) is encountered, throwing a + * {@link PropertyBatchUpdateException} containing all the individual errors. + * This exception can be examined later to see all binding errors. + * Properties that were successfully updated remain changed. + *

Does not allow unknown fields or invalid fields. + * @param pvs a PropertyValues to set on the target object + * @throws InvalidPropertyException if there is no such property or + * if the property isn't writable + * @throws PropertyBatchUpdateException if one or more PropertyAccessExceptions + * occurred for specific properties during the batch update. This exception bundles + * all individual PropertyAccessExceptions. All other properties will have been + * successfully updated. + * @see #setPropertyValues(PropertyValues, boolean, boolean) + */ + void setPropertyValues(PropertyValues pvs) throws BeansException; + + /** + * Perform a batch update with more control over behavior. + *

Note that performing a batch update differs from performing a single update, + * in that an implementation of this class will continue to update properties + * if a recoverable error (such as a type mismatch, but not an + * invalid field name or the like) is encountered, throwing a + * {@link PropertyBatchUpdateException} containing all the individual errors. + * This exception can be examined later to see all binding errors. + * Properties that were successfully updated remain changed. + * @param pvs a PropertyValues to set on the target object + * @param ignoreUnknown should we ignore unknown properties (not found in the bean) + * @throws InvalidPropertyException if there is no such property or + * if the property isn't writable + * @throws PropertyBatchUpdateException if one or more PropertyAccessExceptions + * occurred for specific properties during the batch update. This exception bundles + * all individual PropertyAccessExceptions. All other properties will have been + * successfully updated. + * @see #setPropertyValues(PropertyValues, boolean, boolean) + */ + void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown) + throws BeansException; + + /** + * Perform a batch update with full control over behavior. + *

Note that performing a batch update differs from performing a single update, + * in that an implementation of this class will continue to update properties + * if a recoverable error (such as a type mismatch, but not an + * invalid field name or the like) is encountered, throwing a + * {@link PropertyBatchUpdateException} containing all the individual errors. + * This exception can be examined later to see all binding errors. + * Properties that were successfully updated remain changed. + * @param pvs a PropertyValues to set on the target object + * @param ignoreUnknown should we ignore unknown properties (not found in the bean) + * @param ignoreInvalid should we ignore invalid properties (found but not accessible) + * @throws InvalidPropertyException if there is no such property or + * if the property isn't writable + * @throws PropertyBatchUpdateException if one or more PropertyAccessExceptions + * occurred for specific properties during the batch update. This exception bundles + * all individual PropertyAccessExceptions. All other properties will have been + * successfully updated. + */ + void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown, boolean ignoreInvalid) + throws BeansException; + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyAccessorFactory.java b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessorFactory.java new file mode 100644 index 0000000..78e8ec6 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessorFactory.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +/** + * Simple factory facade for obtaining {@link PropertyAccessor} instances, + * in particular for {@link BeanWrapper} instances. Conceals the actual + * target implementation classes and their extended public signature. + * + * @author Juergen Hoeller + * @since 2.5.2 + */ +public final class PropertyAccessorFactory { + + private PropertyAccessorFactory() { + } + + + /** + * Obtain a BeanWrapper for the given target object, + * accessing properties in JavaBeans style. + * @param target the target object to wrap + * @return the property accessor + * @see BeanWrapperImpl + */ + public static BeanWrapper forBeanPropertyAccess(Object target) { + return new BeanWrapperImpl(target); + } + + /** + * Obtain a PropertyAccessor for the given target object, + * accessing properties in direct field style. + * @param target the target object to wrap + * @return the property accessor + * @see DirectFieldAccessor + */ + public static ConfigurablePropertyAccessor forDirectFieldAccess(Object target) { + return new DirectFieldAccessor(target); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyAccessorUtils.java b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessorUtils.java new file mode 100644 index 0000000..55f50e5 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessorUtils.java @@ -0,0 +1,188 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import org.springframework.lang.Nullable; + +/** + * Utility methods for classes that perform bean property access + * according to the {@link PropertyAccessor} interface. + * + * @author Juergen Hoeller + * @since 1.2.6 + */ +public abstract class PropertyAccessorUtils { + + /** + * Return the actual property name for the given property path. + * @param propertyPath the property path to determine the property name + * for (can include property keys, for example for specifying a map entry) + * @return the actual property name, without any key elements + */ + public static String getPropertyName(String propertyPath) { + int separatorIndex = (propertyPath.endsWith(PropertyAccessor.PROPERTY_KEY_SUFFIX) ? + propertyPath.indexOf(PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR) : -1); + return (separatorIndex != -1 ? propertyPath.substring(0, separatorIndex) : propertyPath); + } + + /** + * Check whether the given property path indicates an indexed or nested property. + * @param propertyPath the property path to check + * @return whether the path indicates an indexed or nested property + */ + public static boolean isNestedOrIndexedProperty(@Nullable String propertyPath) { + if (propertyPath == null) { + return false; + } + for (int i = 0; i < propertyPath.length(); i++) { + char ch = propertyPath.charAt(i); + if (ch == PropertyAccessor.NESTED_PROPERTY_SEPARATOR_CHAR || + ch == PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR) { + return true; + } + } + return false; + } + + /** + * Determine the first nested property separator in the + * given property path, ignoring dots in keys (like "map[my.key]"). + * @param propertyPath the property path to check + * @return the index of the nested property separator, or -1 if none + */ + public static int getFirstNestedPropertySeparatorIndex(String propertyPath) { + return getNestedPropertySeparatorIndex(propertyPath, false); + } + + /** + * Determine the first nested property separator in the + * given property path, ignoring dots in keys (like "map[my.key]"). + * @param propertyPath the property path to check + * @return the index of the nested property separator, or -1 if none + */ + public static int getLastNestedPropertySeparatorIndex(String propertyPath) { + return getNestedPropertySeparatorIndex(propertyPath, true); + } + + /** + * Determine the first (or last) nested property separator in the + * given property path, ignoring dots in keys (like "map[my.key]"). + * @param propertyPath the property path to check + * @param last whether to return the last separator rather than the first + * @return the index of the nested property separator, or -1 if none + */ + private static int getNestedPropertySeparatorIndex(String propertyPath, boolean last) { + boolean inKey = false; + int length = propertyPath.length(); + int i = (last ? length - 1 : 0); + while (last ? i >= 0 : i < length) { + switch (propertyPath.charAt(i)) { + case PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR: + case PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR: + inKey = !inKey; + break; + case PropertyAccessor.NESTED_PROPERTY_SEPARATOR_CHAR: + if (!inKey) { + return i; + } + } + if (last) { + i--; + } + else { + i++; + } + } + return -1; + } + + /** + * Determine whether the given registered path matches the given property path, + * either indicating the property itself or an indexed element of the property. + * @param propertyPath the property path (typically without index) + * @param registeredPath the registered path (potentially with index) + * @return whether the paths match + */ + public static boolean matchesProperty(String registeredPath, String propertyPath) { + if (!registeredPath.startsWith(propertyPath)) { + return false; + } + if (registeredPath.length() == propertyPath.length()) { + return true; + } + if (registeredPath.charAt(propertyPath.length()) != PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR) { + return false; + } + return (registeredPath.indexOf(PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR, propertyPath.length() + 1) == + registeredPath.length() - 1); + } + + /** + * Determine the canonical name for the given property path. + * Removes surrounding quotes from map keys:
+ * {@code map['key']} -> {@code map[key]}
+ * {@code map["key"]} -> {@code map[key]} + * @param propertyName the bean property path + * @return the canonical representation of the property path + */ + public static String canonicalPropertyName(@Nullable String propertyName) { + if (propertyName == null) { + return ""; + } + + StringBuilder sb = new StringBuilder(propertyName); + int searchIndex = 0; + while (searchIndex != -1) { + int keyStart = sb.indexOf(PropertyAccessor.PROPERTY_KEY_PREFIX, searchIndex); + searchIndex = -1; + if (keyStart != -1) { + int keyEnd = sb.indexOf( + PropertyAccessor.PROPERTY_KEY_SUFFIX, keyStart + PropertyAccessor.PROPERTY_KEY_PREFIX.length()); + if (keyEnd != -1) { + String key = sb.substring(keyStart + PropertyAccessor.PROPERTY_KEY_PREFIX.length(), keyEnd); + if ((key.startsWith("'") && key.endsWith("'")) || (key.startsWith("\"") && key.endsWith("\""))) { + sb.delete(keyStart + 1, keyStart + 2); + sb.delete(keyEnd - 2, keyEnd - 1); + keyEnd = keyEnd - 2; + } + searchIndex = keyEnd + PropertyAccessor.PROPERTY_KEY_SUFFIX.length(); + } + } + } + return sb.toString(); + } + + /** + * Determine the canonical names for the given property paths. + * @param propertyNames the bean property paths (as array) + * @return the canonical representation of the property paths + * (as array of the same size) + * @see #canonicalPropertyName(String) + */ + @Nullable + public static String[] canonicalPropertyNames(@Nullable String[] propertyNames) { + if (propertyNames == null) { + return null; + } + String[] result = new String[propertyNames.length]; + for (int i = 0; i < propertyNames.length; i++) { + result[i] = canonicalPropertyName(propertyNames[i]); + } + return result; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyBatchUpdateException.java b/spring-beans/src/main/java/org/springframework/beans/PropertyBatchUpdateException.java new file mode 100644 index 0000000..46491e0 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyBatchUpdateException.java @@ -0,0 +1,148 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.io.PrintStream; +import java.io.PrintWriter; +import java.util.StringJoiner; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Combined exception, composed of individual PropertyAccessException instances. + * An object of this class is created at the beginning of the binding + * process, and errors added to it as necessary. + * + *

The binding process continues when it encounters application-level + * PropertyAccessExceptions, applying those changes that can be applied + * and storing rejected changes in an object of this class. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 18 April 2001 + */ +@SuppressWarnings("serial") +public class PropertyBatchUpdateException extends BeansException { + + /** List of PropertyAccessException objects. */ + private final PropertyAccessException[] propertyAccessExceptions; + + + /** + * Create a new PropertyBatchUpdateException. + * @param propertyAccessExceptions the List of PropertyAccessExceptions + */ + public PropertyBatchUpdateException(PropertyAccessException[] propertyAccessExceptions) { + super(null, null); + Assert.notEmpty(propertyAccessExceptions, "At least 1 PropertyAccessException required"); + this.propertyAccessExceptions = propertyAccessExceptions; + } + + + /** + * If this returns 0, no errors were encountered during binding. + */ + public final int getExceptionCount() { + return this.propertyAccessExceptions.length; + } + + /** + * Return an array of the propertyAccessExceptions stored in this object. + *

Will return the empty array (not {@code null}) if there were no errors. + */ + public final PropertyAccessException[] getPropertyAccessExceptions() { + return this.propertyAccessExceptions; + } + + /** + * Return the exception for this field, or {@code null} if there isn't any. + */ + @Nullable + public PropertyAccessException getPropertyAccessException(String propertyName) { + for (PropertyAccessException pae : this.propertyAccessExceptions) { + if (ObjectUtils.nullSafeEquals(propertyName, pae.getPropertyName())) { + return pae; + } + } + return null; + } + + + @Override + public String getMessage() { + StringJoiner stringJoiner = new StringJoiner("; ", "Failed properties: ", ""); + for (PropertyAccessException exception : this.propertyAccessExceptions) { + stringJoiner.add(exception.getMessage()); + } + return stringJoiner.toString(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(getClass().getName()).append("; nested PropertyAccessExceptions ("); + sb.append(getExceptionCount()).append(") are:"); + for (int i = 0; i < this.propertyAccessExceptions.length; i++) { + sb.append('\n').append("PropertyAccessException ").append(i + 1).append(": "); + sb.append(this.propertyAccessExceptions[i]); + } + return sb.toString(); + } + + @Override + public void printStackTrace(PrintStream ps) { + synchronized (ps) { + ps.println(getClass().getName() + "; nested PropertyAccessException details (" + + getExceptionCount() + ") are:"); + for (int i = 0; i < this.propertyAccessExceptions.length; i++) { + ps.println("PropertyAccessException " + (i + 1) + ":"); + this.propertyAccessExceptions[i].printStackTrace(ps); + } + } + } + + @Override + public void printStackTrace(PrintWriter pw) { + synchronized (pw) { + pw.println(getClass().getName() + "; nested PropertyAccessException details (" + + getExceptionCount() + ") are:"); + for (int i = 0; i < this.propertyAccessExceptions.length; i++) { + pw.println("PropertyAccessException " + (i + 1) + ":"); + this.propertyAccessExceptions[i].printStackTrace(pw); + } + } + } + + @Override + public boolean contains(@Nullable Class exType) { + if (exType == null) { + return false; + } + if (exType.isInstance(this)) { + return true; + } + for (PropertyAccessException pae : this.propertyAccessExceptions) { + if (pae.contains(exType)) { + return true; + } + } + return false; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyDescriptorUtils.java b/spring-beans/src/main/java/org/springframework/beans/PropertyDescriptorUtils.java new file mode 100644 index 0000000..aa99098 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyDescriptorUtils.java @@ -0,0 +1,176 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.beans.IntrospectionException; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; +import java.util.Enumeration; + +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +/** + * Common delegate methods for Spring's internal {@link PropertyDescriptor} implementations. + * + * @author Chris Beams + * @author Juergen Hoeller + */ +abstract class PropertyDescriptorUtils { + + /** + * See {@link java.beans.FeatureDescriptor}. + */ + public static void copyNonMethodProperties(PropertyDescriptor source, PropertyDescriptor target) { + target.setExpert(source.isExpert()); + target.setHidden(source.isHidden()); + target.setPreferred(source.isPreferred()); + target.setName(source.getName()); + target.setShortDescription(source.getShortDescription()); + target.setDisplayName(source.getDisplayName()); + + // Copy all attributes (emulating behavior of private FeatureDescriptor#addTable) + Enumeration keys = source.attributeNames(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + target.setValue(key, source.getValue(key)); + } + + // See java.beans.PropertyDescriptor#PropertyDescriptor(PropertyDescriptor) + target.setPropertyEditorClass(source.getPropertyEditorClass()); + target.setBound(source.isBound()); + target.setConstrained(source.isConstrained()); + } + + /** + * See {@link java.beans.PropertyDescriptor#findPropertyType}. + */ + @Nullable + public static Class findPropertyType(@Nullable Method readMethod, @Nullable Method writeMethod) + throws IntrospectionException { + + Class propertyType = null; + + if (readMethod != null) { + if (readMethod.getParameterCount() != 0) { + throw new IntrospectionException("Bad read method arg count: " + readMethod); + } + propertyType = readMethod.getReturnType(); + if (propertyType == Void.TYPE) { + throw new IntrospectionException("Read method returns void: " + readMethod); + } + } + + if (writeMethod != null) { + Class[] params = writeMethod.getParameterTypes(); + if (params.length != 1) { + throw new IntrospectionException("Bad write method arg count: " + writeMethod); + } + if (propertyType != null) { + if (propertyType.isAssignableFrom(params[0])) { + // Write method's property type potentially more specific + propertyType = params[0]; + } + else if (params[0].isAssignableFrom(propertyType)) { + // Proceed with read method's property type + } + else { + throw new IntrospectionException( + "Type mismatch between read and write methods: " + readMethod + " - " + writeMethod); + } + } + else { + propertyType = params[0]; + } + } + + return propertyType; + } + + /** + * See {@link java.beans.IndexedPropertyDescriptor#findIndexedPropertyType}. + */ + @Nullable + public static Class findIndexedPropertyType(String name, @Nullable Class propertyType, + @Nullable Method indexedReadMethod, @Nullable Method indexedWriteMethod) throws IntrospectionException { + + Class indexedPropertyType = null; + + if (indexedReadMethod != null) { + Class[] params = indexedReadMethod.getParameterTypes(); + if (params.length != 1) { + throw new IntrospectionException("Bad indexed read method arg count: " + indexedReadMethod); + } + if (params[0] != Integer.TYPE) { + throw new IntrospectionException("Non int index to indexed read method: " + indexedReadMethod); + } + indexedPropertyType = indexedReadMethod.getReturnType(); + if (indexedPropertyType == Void.TYPE) { + throw new IntrospectionException("Indexed read method returns void: " + indexedReadMethod); + } + } + + if (indexedWriteMethod != null) { + Class[] params = indexedWriteMethod.getParameterTypes(); + if (params.length != 2) { + throw new IntrospectionException("Bad indexed write method arg count: " + indexedWriteMethod); + } + if (params[0] != Integer.TYPE) { + throw new IntrospectionException("Non int index to indexed write method: " + indexedWriteMethod); + } + if (indexedPropertyType != null) { + if (indexedPropertyType.isAssignableFrom(params[1])) { + // Write method's property type potentially more specific + indexedPropertyType = params[1]; + } + else if (params[1].isAssignableFrom(indexedPropertyType)) { + // Proceed with read method's property type + } + else { + throw new IntrospectionException("Type mismatch between indexed read and write methods: " + + indexedReadMethod + " - " + indexedWriteMethod); + } + } + else { + indexedPropertyType = params[1]; + } + } + + if (propertyType != null && (!propertyType.isArray() || + propertyType.getComponentType() != indexedPropertyType)) { + throw new IntrospectionException("Type mismatch between indexed and non-indexed methods: " + + indexedReadMethod + " - " + indexedWriteMethod); + } + + return indexedPropertyType; + } + + /** + * Compare the given {@code PropertyDescriptors} and return {@code true} if + * they are equivalent, i.e. their read method, write method, property type, + * property editor and flags are equivalent. + * @see java.beans.PropertyDescriptor#equals(Object) + */ + public static boolean equals(PropertyDescriptor pd, PropertyDescriptor otherPd) { + return (ObjectUtils.nullSafeEquals(pd.getReadMethod(), otherPd.getReadMethod()) && + ObjectUtils.nullSafeEquals(pd.getWriteMethod(), otherPd.getWriteMethod()) && + ObjectUtils.nullSafeEquals(pd.getPropertyType(), otherPd.getPropertyType()) && + ObjectUtils.nullSafeEquals(pd.getPropertyEditorClass(), otherPd.getPropertyEditorClass()) && + pd.isBound() == otherPd.isBound() && pd.isConstrained() == otherPd.isConstrained()); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrar.java b/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrar.java new file mode 100644 index 0000000..1d5974c --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrar.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +/** + * Interface for strategies that register custom + * {@link java.beans.PropertyEditor property editors} with a + * {@link org.springframework.beans.PropertyEditorRegistry property editor registry}. + * + *

This is particularly useful when you need to use the same set of + * property editors in several different situations: write a corresponding + * registrar and reuse that in each case. + * + * @author Juergen Hoeller + * @since 1.2.6 + * @see PropertyEditorRegistry + * @see java.beans.PropertyEditor + */ +public interface PropertyEditorRegistrar { + + /** + * Register custom {@link java.beans.PropertyEditor PropertyEditors} with + * the given {@code PropertyEditorRegistry}. + *

The passed-in registry will usually be a {@link BeanWrapper} or a + * {@link org.springframework.validation.DataBinder DataBinder}. + *

It is expected that implementations will create brand new + * {@code PropertyEditors} instances for each invocation of this + * method (since {@code PropertyEditors} are not threadsafe). + * @param registry the {@code PropertyEditorRegistry} to register the + * custom {@code PropertyEditors} with + */ + void registerCustomEditors(PropertyEditorRegistry registry); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistry.java b/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistry.java new file mode 100644 index 0000000..9cbbc55 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistry.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.beans.PropertyEditor; + +import org.springframework.lang.Nullable; + +/** + * Encapsulates methods for registering JavaBeans {@link PropertyEditor PropertyEditors}. + * This is the central interface that a {@link PropertyEditorRegistrar} operates on. + * + *

Extended by {@link BeanWrapper}; implemented by {@link BeanWrapperImpl} + * and {@link org.springframework.validation.DataBinder}. + * + * @author Juergen Hoeller + * @since 1.2.6 + * @see java.beans.PropertyEditor + * @see PropertyEditorRegistrar + * @see BeanWrapper + * @see org.springframework.validation.DataBinder + */ +public interface PropertyEditorRegistry { + + /** + * Register the given custom property editor for all properties of the given type. + * @param requiredType the type of the property + * @param propertyEditor the editor to register + */ + void registerCustomEditor(Class requiredType, PropertyEditor propertyEditor); + + /** + * Register the given custom property editor for the given type and + * property, or for all properties of the given type. + *

If the property path denotes an array or Collection property, + * the editor will get applied either to the array/Collection itself + * (the {@link PropertyEditor} has to create an array or Collection value) or + * to each element (the {@code PropertyEditor} has to create the element type), + * depending on the specified required type. + *

Note: Only one single registered custom editor per property path + * is supported. In the case of a Collection/array, do not register an editor + * for both the Collection/array and each element on the same property. + *

For example, if you wanted to register an editor for "items[n].quantity" + * (for all values n), you would use "items.quantity" as the value of the + * 'propertyPath' argument to this method. + * @param requiredType the type of the property. This may be {@code null} + * if a property is given but should be specified in any case, in particular in + * case of a Collection - making clear whether the editor is supposed to apply + * to the entire Collection itself or to each of its entries. So as a general rule: + * Do not specify {@code null} here in case of a Collection/array! + * @param propertyPath the path of the property (name or nested path), or + * {@code null} if registering an editor for all properties of the given type + * @param propertyEditor editor to register + */ + void registerCustomEditor(@Nullable Class requiredType, @Nullable String propertyPath, PropertyEditor propertyEditor); + + /** + * Find a custom property editor for the given type and property. + * @param requiredType the type of the property (can be {@code null} if a property + * is given but should be specified in any case for consistency checking) + * @param propertyPath the path of the property (name or nested path), or + * {@code null} if looking for an editor for all properties of the given type + * @return the registered editor, or {@code null} if none + */ + @Nullable + PropertyEditor findCustomEditor(@Nullable Class requiredType, @Nullable String propertyPath); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrySupport.java b/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrySupport.java new file mode 100644 index 0000000..d1354e1 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrySupport.java @@ -0,0 +1,575 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.beans.PropertyEditor; +import java.io.File; +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.URI; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Currency; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TimeZone; +import java.util.UUID; +import java.util.regex.Pattern; + +import org.xml.sax.InputSource; + +import org.springframework.beans.propertyeditors.ByteArrayPropertyEditor; +import org.springframework.beans.propertyeditors.CharArrayPropertyEditor; +import org.springframework.beans.propertyeditors.CharacterEditor; +import org.springframework.beans.propertyeditors.CharsetEditor; +import org.springframework.beans.propertyeditors.ClassArrayEditor; +import org.springframework.beans.propertyeditors.ClassEditor; +import org.springframework.beans.propertyeditors.CurrencyEditor; +import org.springframework.beans.propertyeditors.CustomBooleanEditor; +import org.springframework.beans.propertyeditors.CustomCollectionEditor; +import org.springframework.beans.propertyeditors.CustomMapEditor; +import org.springframework.beans.propertyeditors.CustomNumberEditor; +import org.springframework.beans.propertyeditors.FileEditor; +import org.springframework.beans.propertyeditors.InputSourceEditor; +import org.springframework.beans.propertyeditors.InputStreamEditor; +import org.springframework.beans.propertyeditors.LocaleEditor; +import org.springframework.beans.propertyeditors.PathEditor; +import org.springframework.beans.propertyeditors.PatternEditor; +import org.springframework.beans.propertyeditors.PropertiesEditor; +import org.springframework.beans.propertyeditors.ReaderEditor; +import org.springframework.beans.propertyeditors.StringArrayPropertyEditor; +import org.springframework.beans.propertyeditors.TimeZoneEditor; +import org.springframework.beans.propertyeditors.URIEditor; +import org.springframework.beans.propertyeditors.URLEditor; +import org.springframework.beans.propertyeditors.UUIDEditor; +import org.springframework.beans.propertyeditors.ZoneIdEditor; +import org.springframework.core.SpringProperties; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.ResourceArrayPropertyEditor; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * Base implementation of the {@link PropertyEditorRegistry} interface. + * Provides management of default editors and custom editors. + * Mainly serves as base class for {@link BeanWrapperImpl}. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @author Sebastien Deleuze + * @since 1.2.6 + * @see java.beans.PropertyEditorManager + * @see java.beans.PropertyEditorSupport#setAsText + * @see java.beans.PropertyEditorSupport#setValue + */ +public class PropertyEditorRegistrySupport implements PropertyEditorRegistry { + + /** + * Boolean flag controlled by a {@code spring.xml.ignore} system property that instructs Spring to + * ignore XML, i.e. to not initialize the XML-related infrastructure. + *

The default is "false". + */ + private static final boolean shouldIgnoreXml = SpringProperties.getFlag("spring.xml.ignore"); + + + @Nullable + private ConversionService conversionService; + + private boolean defaultEditorsActive = false; + + private boolean configValueEditorsActive = false; + + @Nullable + private Map, PropertyEditor> defaultEditors; + + @Nullable + private Map, PropertyEditor> overriddenDefaultEditors; + + @Nullable + private Map, PropertyEditor> customEditors; + + @Nullable + private Map customEditorsForPath; + + @Nullable + private Map, PropertyEditor> customEditorCache; + + + /** + * Specify a Spring 3.0 ConversionService to use for converting + * property values, as an alternative to JavaBeans PropertyEditors. + */ + public void setConversionService(@Nullable ConversionService conversionService) { + this.conversionService = conversionService; + } + + /** + * Return the associated ConversionService, if any. + */ + @Nullable + public ConversionService getConversionService() { + return this.conversionService; + } + + + //--------------------------------------------------------------------- + // Management of default editors + //--------------------------------------------------------------------- + + /** + * Activate the default editors for this registry instance, + * allowing for lazily registering default editors when needed. + */ + protected void registerDefaultEditors() { + this.defaultEditorsActive = true; + } + + /** + * Activate config value editors which are only intended for configuration purposes, + * such as {@link org.springframework.beans.propertyeditors.StringArrayPropertyEditor}. + *

Those editors are not registered by default simply because they are in + * general inappropriate for data binding purposes. Of course, you may register + * them individually in any case, through {@link #registerCustomEditor}. + */ + public void useConfigValueEditors() { + this.configValueEditorsActive = true; + } + + /** + * Override the default editor for the specified type with the given property editor. + *

Note that this is different from registering a custom editor in that the editor + * semantically still is a default editor. A ConversionService will override such a + * default editor, whereas custom editors usually override the ConversionService. + * @param requiredType the type of the property + * @param propertyEditor the editor to register + * @see #registerCustomEditor(Class, PropertyEditor) + */ + public void overrideDefaultEditor(Class requiredType, PropertyEditor propertyEditor) { + if (this.overriddenDefaultEditors == null) { + this.overriddenDefaultEditors = new HashMap<>(); + } + this.overriddenDefaultEditors.put(requiredType, propertyEditor); + } + + /** + * Retrieve the default editor for the given property type, if any. + *

Lazily registers the default editors, if they are active. + * @param requiredType type of the property + * @return the default editor, or {@code null} if none found + * @see #registerDefaultEditors + */ + @Nullable + public PropertyEditor getDefaultEditor(Class requiredType) { + if (!this.defaultEditorsActive) { + return null; + } + if (this.overriddenDefaultEditors != null) { + PropertyEditor editor = this.overriddenDefaultEditors.get(requiredType); + if (editor != null) { + return editor; + } + } + if (this.defaultEditors == null) { + createDefaultEditors(); + } + return this.defaultEditors.get(requiredType); + } + + /** + * Actually register the default editors for this registry instance. + */ + private void createDefaultEditors() { + this.defaultEditors = new HashMap<>(64); + + // Simple editors, without parameterization capabilities. + // The JDK does not contain a default editor for any of these target types. + this.defaultEditors.put(Charset.class, new CharsetEditor()); + this.defaultEditors.put(Class.class, new ClassEditor()); + this.defaultEditors.put(Class[].class, new ClassArrayEditor()); + this.defaultEditors.put(Currency.class, new CurrencyEditor()); + this.defaultEditors.put(File.class, new FileEditor()); + this.defaultEditors.put(InputStream.class, new InputStreamEditor()); + if (!shouldIgnoreXml) { + this.defaultEditors.put(InputSource.class, new InputSourceEditor()); + } + this.defaultEditors.put(Locale.class, new LocaleEditor()); + this.defaultEditors.put(Path.class, new PathEditor()); + this.defaultEditors.put(Pattern.class, new PatternEditor()); + this.defaultEditors.put(Properties.class, new PropertiesEditor()); + this.defaultEditors.put(Reader.class, new ReaderEditor()); + this.defaultEditors.put(Resource[].class, new ResourceArrayPropertyEditor()); + this.defaultEditors.put(TimeZone.class, new TimeZoneEditor()); + this.defaultEditors.put(URI.class, new URIEditor()); + this.defaultEditors.put(URL.class, new URLEditor()); + this.defaultEditors.put(UUID.class, new UUIDEditor()); + this.defaultEditors.put(ZoneId.class, new ZoneIdEditor()); + + // Default instances of collection editors. + // Can be overridden by registering custom instances of those as custom editors. + this.defaultEditors.put(Collection.class, new CustomCollectionEditor(Collection.class)); + this.defaultEditors.put(Set.class, new CustomCollectionEditor(Set.class)); + this.defaultEditors.put(SortedSet.class, new CustomCollectionEditor(SortedSet.class)); + this.defaultEditors.put(List.class, new CustomCollectionEditor(List.class)); + this.defaultEditors.put(SortedMap.class, new CustomMapEditor(SortedMap.class)); + + // Default editors for primitive arrays. + this.defaultEditors.put(byte[].class, new ByteArrayPropertyEditor()); + this.defaultEditors.put(char[].class, new CharArrayPropertyEditor()); + + // The JDK does not contain a default editor for char! + this.defaultEditors.put(char.class, new CharacterEditor(false)); + this.defaultEditors.put(Character.class, new CharacterEditor(true)); + + // Spring's CustomBooleanEditor accepts more flag values than the JDK's default editor. + this.defaultEditors.put(boolean.class, new CustomBooleanEditor(false)); + this.defaultEditors.put(Boolean.class, new CustomBooleanEditor(true)); + + // The JDK does not contain default editors for number wrapper types! + // Override JDK primitive number editors with our own CustomNumberEditor. + this.defaultEditors.put(byte.class, new CustomNumberEditor(Byte.class, false)); + this.defaultEditors.put(Byte.class, new CustomNumberEditor(Byte.class, true)); + this.defaultEditors.put(short.class, new CustomNumberEditor(Short.class, false)); + this.defaultEditors.put(Short.class, new CustomNumberEditor(Short.class, true)); + this.defaultEditors.put(int.class, new CustomNumberEditor(Integer.class, false)); + this.defaultEditors.put(Integer.class, new CustomNumberEditor(Integer.class, true)); + this.defaultEditors.put(long.class, new CustomNumberEditor(Long.class, false)); + this.defaultEditors.put(Long.class, new CustomNumberEditor(Long.class, true)); + this.defaultEditors.put(float.class, new CustomNumberEditor(Float.class, false)); + this.defaultEditors.put(Float.class, new CustomNumberEditor(Float.class, true)); + this.defaultEditors.put(double.class, new CustomNumberEditor(Double.class, false)); + this.defaultEditors.put(Double.class, new CustomNumberEditor(Double.class, true)); + this.defaultEditors.put(BigDecimal.class, new CustomNumberEditor(BigDecimal.class, true)); + this.defaultEditors.put(BigInteger.class, new CustomNumberEditor(BigInteger.class, true)); + + // Only register config value editors if explicitly requested. + if (this.configValueEditorsActive) { + StringArrayPropertyEditor sae = new StringArrayPropertyEditor(); + this.defaultEditors.put(String[].class, sae); + this.defaultEditors.put(short[].class, sae); + this.defaultEditors.put(int[].class, sae); + this.defaultEditors.put(long[].class, sae); + } + } + + /** + * Copy the default editors registered in this instance to the given target registry. + * @param target the target registry to copy to + */ + protected void copyDefaultEditorsTo(PropertyEditorRegistrySupport target) { + target.defaultEditorsActive = this.defaultEditorsActive; + target.configValueEditorsActive = this.configValueEditorsActive; + target.defaultEditors = this.defaultEditors; + target.overriddenDefaultEditors = this.overriddenDefaultEditors; + } + + + //--------------------------------------------------------------------- + // Management of custom editors + //--------------------------------------------------------------------- + + @Override + public void registerCustomEditor(Class requiredType, PropertyEditor propertyEditor) { + registerCustomEditor(requiredType, null, propertyEditor); + } + + @Override + public void registerCustomEditor(@Nullable Class requiredType, @Nullable String propertyPath, PropertyEditor propertyEditor) { + if (requiredType == null && propertyPath == null) { + throw new IllegalArgumentException("Either requiredType or propertyPath is required"); + } + if (propertyPath != null) { + if (this.customEditorsForPath == null) { + this.customEditorsForPath = new LinkedHashMap<>(16); + } + this.customEditorsForPath.put(propertyPath, new CustomEditorHolder(propertyEditor, requiredType)); + } + else { + if (this.customEditors == null) { + this.customEditors = new LinkedHashMap<>(16); + } + this.customEditors.put(requiredType, propertyEditor); + this.customEditorCache = null; + } + } + + @Override + @Nullable + public PropertyEditor findCustomEditor(@Nullable Class requiredType, @Nullable String propertyPath) { + Class requiredTypeToUse = requiredType; + if (propertyPath != null) { + if (this.customEditorsForPath != null) { + // Check property-specific editor first. + PropertyEditor editor = getCustomEditor(propertyPath, requiredType); + if (editor == null) { + List strippedPaths = new ArrayList<>(); + addStrippedPropertyPaths(strippedPaths, "", propertyPath); + for (Iterator it = strippedPaths.iterator(); it.hasNext() && editor == null;) { + String strippedPath = it.next(); + editor = getCustomEditor(strippedPath, requiredType); + } + } + if (editor != null) { + return editor; + } + } + if (requiredType == null) { + requiredTypeToUse = getPropertyType(propertyPath); + } + } + // No property-specific editor -> check type-specific editor. + return getCustomEditor(requiredTypeToUse); + } + + /** + * Determine whether this registry contains a custom editor + * for the specified array/collection element. + * @param elementType the target type of the element + * (can be {@code null} if not known) + * @param propertyPath the property path (typically of the array/collection; + * can be {@code null} if not known) + * @return whether a matching custom editor has been found + */ + public boolean hasCustomEditorForElement(@Nullable Class elementType, @Nullable String propertyPath) { + if (propertyPath != null && this.customEditorsForPath != null) { + for (Map.Entry entry : this.customEditorsForPath.entrySet()) { + if (PropertyAccessorUtils.matchesProperty(entry.getKey(), propertyPath) && + entry.getValue().getPropertyEditor(elementType) != null) { + return true; + } + } + } + // No property-specific editor -> check type-specific editor. + return (elementType != null && this.customEditors != null && this.customEditors.containsKey(elementType)); + } + + /** + * Determine the property type for the given property path. + *

Called by {@link #findCustomEditor} if no required type has been specified, + * to be able to find a type-specific editor even if just given a property path. + *

The default implementation always returns {@code null}. + * BeanWrapperImpl overrides this with the standard {@code getPropertyType} + * method as defined by the BeanWrapper interface. + * @param propertyPath the property path to determine the type for + * @return the type of the property, or {@code null} if not determinable + * @see BeanWrapper#getPropertyType(String) + */ + @Nullable + protected Class getPropertyType(String propertyPath) { + return null; + } + + /** + * Get custom editor that has been registered for the given property. + * @param propertyName the property path to look for + * @param requiredType the type to look for + * @return the custom editor, or {@code null} if none specific for this property + */ + @Nullable + private PropertyEditor getCustomEditor(String propertyName, @Nullable Class requiredType) { + CustomEditorHolder holder = + (this.customEditorsForPath != null ? this.customEditorsForPath.get(propertyName) : null); + return (holder != null ? holder.getPropertyEditor(requiredType) : null); + } + + /** + * Get custom editor for the given type. If no direct match found, + * try custom editor for superclass (which will in any case be able + * to render a value as String via {@code getAsText}). + * @param requiredType the type to look for + * @return the custom editor, or {@code null} if none found for this type + * @see java.beans.PropertyEditor#getAsText() + */ + @Nullable + private PropertyEditor getCustomEditor(@Nullable Class requiredType) { + if (requiredType == null || this.customEditors == null) { + return null; + } + // Check directly registered editor for type. + PropertyEditor editor = this.customEditors.get(requiredType); + if (editor == null) { + // Check cached editor for type, registered for superclass or interface. + if (this.customEditorCache != null) { + editor = this.customEditorCache.get(requiredType); + } + if (editor == null) { + // Find editor for superclass or interface. + for (Iterator> it = this.customEditors.keySet().iterator(); it.hasNext() && editor == null;) { + Class key = it.next(); + if (key.isAssignableFrom(requiredType)) { + editor = this.customEditors.get(key); + // Cache editor for search type, to avoid the overhead + // of repeated assignable-from checks. + if (this.customEditorCache == null) { + this.customEditorCache = new HashMap<>(); + } + this.customEditorCache.put(requiredType, editor); + } + } + } + } + return editor; + } + + /** + * Guess the property type of the specified property from the registered + * custom editors (provided that they were registered for a specific type). + * @param propertyName the name of the property + * @return the property type, or {@code null} if not determinable + */ + @Nullable + protected Class guessPropertyTypeFromEditors(String propertyName) { + if (this.customEditorsForPath != null) { + CustomEditorHolder editorHolder = this.customEditorsForPath.get(propertyName); + if (editorHolder == null) { + List strippedPaths = new ArrayList<>(); + addStrippedPropertyPaths(strippedPaths, "", propertyName); + for (Iterator it = strippedPaths.iterator(); it.hasNext() && editorHolder == null;) { + String strippedName = it.next(); + editorHolder = this.customEditorsForPath.get(strippedName); + } + } + if (editorHolder != null) { + return editorHolder.getRegisteredType(); + } + } + return null; + } + + /** + * Copy the custom editors registered in this instance to the given target registry. + * @param target the target registry to copy to + * @param nestedProperty the nested property path of the target registry, if any. + * If this is non-null, only editors registered for a path below this nested property + * will be copied. If this is null, all editors will be copied. + */ + protected void copyCustomEditorsTo(PropertyEditorRegistry target, @Nullable String nestedProperty) { + String actualPropertyName = + (nestedProperty != null ? PropertyAccessorUtils.getPropertyName(nestedProperty) : null); + if (this.customEditors != null) { + this.customEditors.forEach(target::registerCustomEditor); + } + if (this.customEditorsForPath != null) { + this.customEditorsForPath.forEach((editorPath, editorHolder) -> { + if (nestedProperty != null) { + int pos = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(editorPath); + if (pos != -1) { + String editorNestedProperty = editorPath.substring(0, pos); + String editorNestedPath = editorPath.substring(pos + 1); + if (editorNestedProperty.equals(nestedProperty) || editorNestedProperty.equals(actualPropertyName)) { + target.registerCustomEditor( + editorHolder.getRegisteredType(), editorNestedPath, editorHolder.getPropertyEditor()); + } + } + } + else { + target.registerCustomEditor( + editorHolder.getRegisteredType(), editorPath, editorHolder.getPropertyEditor()); + } + }); + } + } + + + /** + * Add property paths with all variations of stripped keys and/or indexes. + * Invokes itself recursively with nested paths. + * @param strippedPaths the result list to add to + * @param nestedPath the current nested path + * @param propertyPath the property path to check for keys/indexes to strip + */ + private void addStrippedPropertyPaths(List strippedPaths, String nestedPath, String propertyPath) { + int startIndex = propertyPath.indexOf(PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR); + if (startIndex != -1) { + int endIndex = propertyPath.indexOf(PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR); + if (endIndex != -1) { + String prefix = propertyPath.substring(0, startIndex); + String key = propertyPath.substring(startIndex, endIndex + 1); + String suffix = propertyPath.substring(endIndex + 1); + // Strip the first key. + strippedPaths.add(nestedPath + prefix + suffix); + // Search for further keys to strip, with the first key stripped. + addStrippedPropertyPaths(strippedPaths, nestedPath + prefix, suffix); + // Search for further keys to strip, with the first key not stripped. + addStrippedPropertyPaths(strippedPaths, nestedPath + prefix + key, suffix); + } + } + } + + + /** + * Holder for a registered custom editor with property name. + * Keeps the PropertyEditor itself plus the type it was registered for. + */ + private static final class CustomEditorHolder { + + private final PropertyEditor propertyEditor; + + @Nullable + private final Class registeredType; + + private CustomEditorHolder(PropertyEditor propertyEditor, @Nullable Class registeredType) { + this.propertyEditor = propertyEditor; + this.registeredType = registeredType; + } + + private PropertyEditor getPropertyEditor() { + return this.propertyEditor; + } + + @Nullable + private Class getRegisteredType() { + return this.registeredType; + } + + @Nullable + private PropertyEditor getPropertyEditor(@Nullable Class requiredType) { + // Special case: If no required type specified, which usually only happens for + // Collection elements, or required type is not assignable to registered type, + // which usually only happens for generic properties of type Object - + // then return PropertyEditor if not registered for Collection or array type. + // (If not registered for Collection or array, it is assumed to be intended + // for elements.) + if (this.registeredType == null || + (requiredType != null && + (ClassUtils.isAssignable(this.registeredType, requiredType) || + ClassUtils.isAssignable(requiredType, this.registeredType))) || + (requiredType == null && + (!Collection.class.isAssignableFrom(this.registeredType) && !this.registeredType.isArray()))) { + return this.propertyEditor; + } + else { + return null; + } + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyMatches.java b/spring-beans/src/main/java/org/springframework/beans/PropertyMatches.java new file mode 100644 index 0000000..659f84f --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyMatches.java @@ -0,0 +1,262 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.beans.PropertyDescriptor; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * Helper class for calculating property matches, according to a configurable + * distance. Provide the list of potential matches and an easy way to generate + * an error message. Works for both java bean properties and fields. + * + *

Mainly for use within the framework and in particular the binding facility. + * + * @author Alef Arendsen + * @author Arjen Poutsma + * @author Juergen Hoeller + * @author Stephane Nicoll + * @since 2.0 + * @see #forProperty(String, Class) + * @see #forField(String, Class) + */ +public abstract class PropertyMatches { + + /** Default maximum property distance: 2. */ + public static final int DEFAULT_MAX_DISTANCE = 2; + + + // Static factory methods + + /** + * Create PropertyMatches for the given bean property. + * @param propertyName the name of the property to find possible matches for + * @param beanClass the bean class to search for matches + */ + public static PropertyMatches forProperty(String propertyName, Class beanClass) { + return forProperty(propertyName, beanClass, DEFAULT_MAX_DISTANCE); + } + + /** + * Create PropertyMatches for the given bean property. + * @param propertyName the name of the property to find possible matches for + * @param beanClass the bean class to search for matches + * @param maxDistance the maximum property distance allowed for matches + */ + public static PropertyMatches forProperty(String propertyName, Class beanClass, int maxDistance) { + return new BeanPropertyMatches(propertyName, beanClass, maxDistance); + } + + /** + * Create PropertyMatches for the given field property. + * @param propertyName the name of the field to find possible matches for + * @param beanClass the bean class to search for matches + */ + public static PropertyMatches forField(String propertyName, Class beanClass) { + return forField(propertyName, beanClass, DEFAULT_MAX_DISTANCE); + } + + /** + * Create PropertyMatches for the given field property. + * @param propertyName the name of the field to find possible matches for + * @param beanClass the bean class to search for matches + * @param maxDistance the maximum property distance allowed for matches + */ + public static PropertyMatches forField(String propertyName, Class beanClass, int maxDistance) { + return new FieldPropertyMatches(propertyName, beanClass, maxDistance); + } + + + // Instance state + + private final String propertyName; + + private final String[] possibleMatches; + + + /** + * Create a new PropertyMatches instance for the given property and possible matches. + */ + private PropertyMatches(String propertyName, String[] possibleMatches) { + this.propertyName = propertyName; + this.possibleMatches = possibleMatches; + } + + + /** + * Return the name of the requested property. + */ + public String getPropertyName() { + return this.propertyName; + } + + /** + * Return the calculated possible matches. + */ + public String[] getPossibleMatches() { + return this.possibleMatches; + } + + /** + * Build an error message for the given invalid property name, + * indicating the possible property matches. + */ + public abstract String buildErrorMessage(); + + + // Implementation support for subclasses + + protected void appendHintMessage(StringBuilder msg) { + msg.append("Did you mean "); + for (int i = 0; i < this.possibleMatches.length; i++) { + msg.append('\''); + msg.append(this.possibleMatches[i]); + if (i < this.possibleMatches.length - 2) { + msg.append("', "); + } + else if (i == this.possibleMatches.length - 2) { + msg.append("', or "); + } + } + msg.append("'?"); + } + + /** + * Calculate the distance between the given two Strings + * according to the Levenshtein algorithm. + * @param s1 the first String + * @param s2 the second String + * @return the distance value + */ + private static int calculateStringDistance(String s1, String s2) { + if (s1.isEmpty()) { + return s2.length(); + } + if (s2.isEmpty()) { + return s1.length(); + } + + int[][] d = new int[s1.length() + 1][s2.length() + 1]; + for (int i = 0; i <= s1.length(); i++) { + d[i][0] = i; + } + for (int j = 0; j <= s2.length(); j++) { + d[0][j] = j; + } + + for (int i = 1; i <= s1.length(); i++) { + char c1 = s1.charAt(i - 1); + for (int j = 1; j <= s2.length(); j++) { + int cost; + char c2 = s2.charAt(j - 1); + if (c1 == c2) { + cost = 0; + } + else { + cost = 1; + } + d[i][j] = Math.min(Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1), d[i - 1][j - 1] + cost); + } + } + + return d[s1.length()][s2.length()]; + } + + + // Concrete subclasses + + private static class BeanPropertyMatches extends PropertyMatches { + + public BeanPropertyMatches(String propertyName, Class beanClass, int maxDistance) { + super(propertyName, + calculateMatches(propertyName, BeanUtils.getPropertyDescriptors(beanClass), maxDistance)); + } + + /** + * Generate possible property alternatives for the given property and class. + * Internally uses the {@code getStringDistance} method, which in turn uses + * the Levenshtein algorithm to determine the distance between two Strings. + * @param descriptors the JavaBeans property descriptors to search + * @param maxDistance the maximum distance to accept + */ + private static String[] calculateMatches(String name, PropertyDescriptor[] descriptors, int maxDistance) { + List candidates = new ArrayList<>(); + for (PropertyDescriptor pd : descriptors) { + if (pd.getWriteMethod() != null) { + String possibleAlternative = pd.getName(); + if (calculateStringDistance(name, possibleAlternative) <= maxDistance) { + candidates.add(possibleAlternative); + } + } + } + Collections.sort(candidates); + return StringUtils.toStringArray(candidates); + } + + @Override + public String buildErrorMessage() { + StringBuilder msg = new StringBuilder(160); + msg.append("Bean property '").append(getPropertyName()).append( + "' is not writable or has an invalid setter method. "); + if (!ObjectUtils.isEmpty(getPossibleMatches())) { + appendHintMessage(msg); + } + else { + msg.append("Does the parameter type of the setter match the return type of the getter?"); + } + return msg.toString(); + } + } + + + private static class FieldPropertyMatches extends PropertyMatches { + + public FieldPropertyMatches(String propertyName, Class beanClass, int maxDistance) { + super(propertyName, calculateMatches(propertyName, beanClass, maxDistance)); + } + + private static String[] calculateMatches(final String name, Class clazz, final int maxDistance) { + final List candidates = new ArrayList<>(); + ReflectionUtils.doWithFields(clazz, field -> { + String possibleAlternative = field.getName(); + if (calculateStringDistance(name, possibleAlternative) <= maxDistance) { + candidates.add(possibleAlternative); + } + }); + Collections.sort(candidates); + return StringUtils.toStringArray(candidates); + } + + @Override + public String buildErrorMessage() { + StringBuilder msg = new StringBuilder(80); + msg.append("Bean property '").append(getPropertyName()).append("' has no matching field."); + if (!ObjectUtils.isEmpty(getPossibleMatches())) { + msg.append(' '); + appendHintMessage(msg); + } + return msg.toString(); + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyValue.java b/spring-beans/src/main/java/org/springframework/beans/PropertyValue.java new file mode 100644 index 0000000..16c6bae --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyValue.java @@ -0,0 +1,214 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.io.Serializable; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Object to hold information and value for an individual bean property. + * Using an object here, rather than just storing all properties in + * a map keyed by property name, allows for more flexibility, and the + * ability to handle indexed properties etc in an optimized way. + * + *

Note that the value doesn't need to be the final required type: + * A {@link BeanWrapper} implementation should handle any necessary conversion, + * as this object doesn't know anything about the objects it will be applied to. + * + * @author Rod Johnson + * @author Rob Harrop + * @author Juergen Hoeller + * @since 13 May 2001 + * @see PropertyValues + * @see BeanWrapper + */ +@SuppressWarnings("serial") +public class PropertyValue extends BeanMetadataAttributeAccessor implements Serializable { + + private final String name; + + @Nullable + private final Object value; + + private boolean optional = false; + + private boolean converted = false; + + @Nullable + private Object convertedValue; + + /** Package-visible field that indicates whether conversion is necessary. */ + @Nullable + volatile Boolean conversionNecessary; + + /** Package-visible field for caching the resolved property path tokens. */ + @Nullable + transient volatile Object resolvedTokens; + + + /** + * Create a new PropertyValue instance. + * @param name the name of the property (never {@code null}) + * @param value the value of the property (possibly before type conversion) + */ + public PropertyValue(String name, @Nullable Object value) { + Assert.notNull(name, "Name must not be null"); + this.name = name; + this.value = value; + } + + /** + * Copy constructor. + * @param original the PropertyValue to copy (never {@code null}) + */ + public PropertyValue(PropertyValue original) { + Assert.notNull(original, "Original must not be null"); + this.name = original.getName(); + this.value = original.getValue(); + this.optional = original.isOptional(); + this.converted = original.converted; + this.convertedValue = original.convertedValue; + this.conversionNecessary = original.conversionNecessary; + this.resolvedTokens = original.resolvedTokens; + setSource(original.getSource()); + copyAttributesFrom(original); + } + + /** + * Constructor that exposes a new value for an original value holder. + * The original holder will be exposed as source of the new holder. + * @param original the PropertyValue to link to (never {@code null}) + * @param newValue the new value to apply + */ + public PropertyValue(PropertyValue original, @Nullable Object newValue) { + Assert.notNull(original, "Original must not be null"); + this.name = original.getName(); + this.value = newValue; + this.optional = original.isOptional(); + this.conversionNecessary = original.conversionNecessary; + this.resolvedTokens = original.resolvedTokens; + setSource(original); + copyAttributesFrom(original); + } + + + /** + * Return the name of the property. + */ + public String getName() { + return this.name; + } + + /** + * Return the value of the property. + *

Note that type conversion will not have occurred here. + * It is the responsibility of the BeanWrapper implementation to + * perform type conversion. + */ + @Nullable + public Object getValue() { + return this.value; + } + + /** + * Return the original PropertyValue instance for this value holder. + * @return the original PropertyValue (either a source of this + * value holder or this value holder itself). + */ + public PropertyValue getOriginalPropertyValue() { + PropertyValue original = this; + Object source = getSource(); + while (source instanceof PropertyValue && source != original) { + original = (PropertyValue) source; + source = original.getSource(); + } + return original; + } + + /** + * Set whether this is an optional value, that is, to be ignored + * when no corresponding property exists on the target class. + * @since 3.0 + */ + public void setOptional(boolean optional) { + this.optional = optional; + } + + /** + * Return whether this is an optional value, that is, to be ignored + * when no corresponding property exists on the target class. + * @since 3.0 + */ + public boolean isOptional() { + return this.optional; + } + + /** + * Return whether this holder contains a converted value already ({@code true}), + * or whether the value still needs to be converted ({@code false}). + */ + public synchronized boolean isConverted() { + return this.converted; + } + + /** + * Set the converted value of this property value, + * after processed type conversion. + */ + public synchronized void setConvertedValue(@Nullable Object value) { + this.converted = true; + this.convertedValue = value; + } + + /** + * Return the converted value of this property value, + * after processed type conversion. + */ + @Nullable + public synchronized Object getConvertedValue() { + return this.convertedValue; + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof PropertyValue)) { + return false; + } + PropertyValue otherPv = (PropertyValue) other; + return (this.name.equals(otherPv.name) && + ObjectUtils.nullSafeEquals(this.value, otherPv.value) && + ObjectUtils.nullSafeEquals(getSource(), otherPv.getSource())); + } + + @Override + public int hashCode() { + return this.name.hashCode() * 29 + ObjectUtils.nullSafeHashCode(this.value); + } + + @Override + public String toString() { + return "bean property '" + this.name + "'"; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyValues.java b/spring-beans/src/main/java/org/springframework/beans/PropertyValues.java new file mode 100644 index 0000000..b754a32 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyValues.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.springframework.lang.Nullable; + +/** + * Holder containing one or more {@link PropertyValue} objects, + * typically comprising one update for a specific target bean. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 13 May 2001 + * @see PropertyValue + */ +public interface PropertyValues extends Iterable { + + /** + * Return an {@link Iterator} over the property values. + * @since 5.1 + */ + @Override + default Iterator iterator() { + return Arrays.asList(getPropertyValues()).iterator(); + } + + /** + * Return a {@link Spliterator} over the property values. + * @since 5.1 + */ + @Override + default Spliterator spliterator() { + return Spliterators.spliterator(getPropertyValues(), 0); + } + + /** + * Return a sequential {@link Stream} containing the property values. + * @since 5.1 + */ + default Stream stream() { + return StreamSupport.stream(spliterator(), false); + } + + /** + * Return an array of the PropertyValue objects held in this object. + */ + PropertyValue[] getPropertyValues(); + + /** + * Return the property value with the given name, if any. + * @param propertyName the name to search for + * @return the property value, or {@code null} if none + */ + @Nullable + PropertyValue getPropertyValue(String propertyName); + + /** + * Return the changes since the previous PropertyValues. + * Subclasses should also override {@code equals}. + * @param old the old property values + * @return the updated or new properties. + * Return empty PropertyValues if there are no changes. + * @see Object#equals + */ + PropertyValues changesSince(PropertyValues old); + + /** + * Is there a property value (or other processing entry) for this property? + * @param propertyName the name of the property we're interested in + * @return whether there is a property value for this property + */ + boolean contains(String propertyName); + + /** + * Does this holder not contain any PropertyValue objects at all? + */ + boolean isEmpty(); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyValuesEditor.java b/spring-beans/src/main/java/org/springframework/beans/PropertyValuesEditor.java new file mode 100644 index 0000000..4d112b4 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyValuesEditor.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2007 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.beans.PropertyEditorSupport; +import java.util.Properties; + +import org.springframework.beans.propertyeditors.PropertiesEditor; + +/** + * {@link java.beans.PropertyEditor Editor} for a {@link PropertyValues} object. + * + *

The required format is defined in the {@link java.util.Properties} + * documentation. Each property must be on a new line. + * + *

The present implementation relies on a + * {@link org.springframework.beans.propertyeditors.PropertiesEditor} + * underneath. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +public class PropertyValuesEditor extends PropertyEditorSupport { + + private final PropertiesEditor propertiesEditor = new PropertiesEditor(); + + @Override + public void setAsText(String text) throws IllegalArgumentException { + this.propertiesEditor.setAsText(text); + Properties props = (Properties) this.propertiesEditor.getValue(); + setValue(new MutablePropertyValues(props)); + } + +} + diff --git a/spring-beans/src/main/java/org/springframework/beans/SimpleTypeConverter.java b/spring-beans/src/main/java/org/springframework/beans/SimpleTypeConverter.java new file mode 100644 index 0000000..1313f4e --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/SimpleTypeConverter.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +/** + * Simple implementation of the {@link TypeConverter} interface that does not operate on + * a specific target object. This is an alternative to using a full-blown BeanWrapperImpl + * instance for arbitrary type conversion needs, while using the very same conversion + * algorithm (including delegation to {@link java.beans.PropertyEditor} and + * {@link org.springframework.core.convert.ConversionService}) underneath. + * + *

Note: Due to its reliance on {@link java.beans.PropertyEditor PropertyEditors}, + * SimpleTypeConverter is not thread-safe. Use a separate instance for each thread. + * + * @author Juergen Hoeller + * @since 2.0 + * @see BeanWrapperImpl + */ +public class SimpleTypeConverter extends TypeConverterSupport { + + public SimpleTypeConverter() { + this.typeConverterDelegate = new TypeConverterDelegate(this); + registerDefaultEditors(); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/TypeConverter.java b/spring-beans/src/main/java/org/springframework/beans/TypeConverter.java new file mode 100644 index 0000000..200a350 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/TypeConverter.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.lang.reflect.Field; + +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.lang.Nullable; + +/** + * Interface that defines type conversion methods. Typically (but not necessarily) + * implemented in conjunction with the {@link PropertyEditorRegistry} interface. + * + *

Note: Since TypeConverter implementations are typically based on + * {@link java.beans.PropertyEditor PropertyEditors} which aren't thread-safe, + * TypeConverters themselves are not to be considered as thread-safe either. + * + * @author Juergen Hoeller + * @since 2.0 + * @see SimpleTypeConverter + * @see BeanWrapperImpl + */ +public interface TypeConverter { + + /** + * Convert the value to the required type (if necessary from a String). + *

Conversions from String to any type will typically use the {@code setAsText} + * method of the PropertyEditor class, or a Spring Converter in a ConversionService. + * @param value the value to convert + * @param requiredType the type we must convert to + * (or {@code null} if not known, for example in case of a collection element) + * @return the new value, possibly the result of type conversion + * @throws TypeMismatchException if type conversion failed + * @see java.beans.PropertyEditor#setAsText(String) + * @see java.beans.PropertyEditor#getValue() + * @see org.springframework.core.convert.ConversionService + * @see org.springframework.core.convert.converter.Converter + */ + @Nullable + T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType) throws TypeMismatchException; + + /** + * Convert the value to the required type (if necessary from a String). + *

Conversions from String to any type will typically use the {@code setAsText} + * method of the PropertyEditor class, or a Spring Converter in a ConversionService. + * @param value the value to convert + * @param requiredType the type we must convert to + * (or {@code null} if not known, for example in case of a collection element) + * @param methodParam the method parameter that is the target of the conversion + * (for analysis of generic types; may be {@code null}) + * @return the new value, possibly the result of type conversion + * @throws TypeMismatchException if type conversion failed + * @see java.beans.PropertyEditor#setAsText(String) + * @see java.beans.PropertyEditor#getValue() + * @see org.springframework.core.convert.ConversionService + * @see org.springframework.core.convert.converter.Converter + */ + @Nullable + T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, + @Nullable MethodParameter methodParam) throws TypeMismatchException; + + /** + * Convert the value to the required type (if necessary from a String). + *

Conversions from String to any type will typically use the {@code setAsText} + * method of the PropertyEditor class, or a Spring Converter in a ConversionService. + * @param value the value to convert + * @param requiredType the type we must convert to + * (or {@code null} if not known, for example in case of a collection element) + * @param field the reflective field that is the target of the conversion + * (for analysis of generic types; may be {@code null}) + * @return the new value, possibly the result of type conversion + * @throws TypeMismatchException if type conversion failed + * @see java.beans.PropertyEditor#setAsText(String) + * @see java.beans.PropertyEditor#getValue() + * @see org.springframework.core.convert.ConversionService + * @see org.springframework.core.convert.converter.Converter + */ + @Nullable + T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, @Nullable Field field) + throws TypeMismatchException; + + /** + * Convert the value to the required type (if necessary from a String). + *

Conversions from String to any type will typically use the {@code setAsText} + * method of the PropertyEditor class, or a Spring Converter in a ConversionService. + * @param value the value to convert + * @param requiredType the type we must convert to + * (or {@code null} if not known, for example in case of a collection element) + * @param typeDescriptor the type descriptor to use (may be {@code null})) + * @return the new value, possibly the result of type conversion + * @throws TypeMismatchException if type conversion failed + * @since 5.1.4 + * @see java.beans.PropertyEditor#setAsText(String) + * @see java.beans.PropertyEditor#getValue() + * @see org.springframework.core.convert.ConversionService + * @see org.springframework.core.convert.converter.Converter + */ + @Nullable + default T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, + @Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException { + + throw new UnsupportedOperationException("TypeDescriptor resolution not supported"); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java b/spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java new file mode 100644 index 0000000..38a59ba --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java @@ -0,0 +1,643 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.beans.PropertyEditor; +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Optional; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.CollectionFactory; +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.NumberUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * Internal helper class for converting property values to target types. + * + *

Works on a given {@link PropertyEditorRegistrySupport} instance. + * Used as a delegate by {@link BeanWrapperImpl} and {@link SimpleTypeConverter}. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @author Dave Syer + * @since 2.0 + * @see BeanWrapperImpl + * @see SimpleTypeConverter + */ +class TypeConverterDelegate { + + private static final Log logger = LogFactory.getLog(TypeConverterDelegate.class); + + private final PropertyEditorRegistrySupport propertyEditorRegistry; + + @Nullable + private final Object targetObject; + + + /** + * Create a new TypeConverterDelegate for the given editor registry. + * @param propertyEditorRegistry the editor registry to use + */ + public TypeConverterDelegate(PropertyEditorRegistrySupport propertyEditorRegistry) { + this(propertyEditorRegistry, null); + } + + /** + * Create a new TypeConverterDelegate for the given editor registry and bean instance. + * @param propertyEditorRegistry the editor registry to use + * @param targetObject the target object to work on (as context that can be passed to editors) + */ + public TypeConverterDelegate(PropertyEditorRegistrySupport propertyEditorRegistry, @Nullable Object targetObject) { + this.propertyEditorRegistry = propertyEditorRegistry; + this.targetObject = targetObject; + } + + + /** + * Convert the value to the required type for the specified property. + * @param propertyName name of the property + * @param oldValue the previous value, if available (may be {@code null}) + * @param newValue the proposed new value + * @param requiredType the type we must convert to + * (or {@code null} if not known, for example in case of a collection element) + * @return the new value, possibly the result of type conversion + * @throws IllegalArgumentException if type conversion failed + */ + @Nullable + public T convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, + Object newValue, @Nullable Class requiredType) throws IllegalArgumentException { + + return convertIfNecessary(propertyName, oldValue, newValue, requiredType, TypeDescriptor.valueOf(requiredType)); + } + + /** + * Convert the value to the required type (if necessary from a String), + * for the specified property. + * @param propertyName name of the property + * @param oldValue the previous value, if available (may be {@code null}) + * @param newValue the proposed new value + * @param requiredType the type we must convert to + * (or {@code null} if not known, for example in case of a collection element) + * @param typeDescriptor the descriptor for the target property or field + * @return the new value, possibly the result of type conversion + * @throws IllegalArgumentException if type conversion failed + */ + @SuppressWarnings("unchecked") + @Nullable + public T convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, @Nullable Object newValue, + @Nullable Class requiredType, @Nullable TypeDescriptor typeDescriptor) throws IllegalArgumentException { + + // Custom editor for this type? + PropertyEditor editor = this.propertyEditorRegistry.findCustomEditor(requiredType, propertyName); + + ConversionFailedException conversionAttemptEx = null; + + // No custom editor but custom ConversionService specified? + ConversionService conversionService = this.propertyEditorRegistry.getConversionService(); + if (editor == null && conversionService != null && newValue != null && typeDescriptor != null) { + TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue); + if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) { + try { + return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor); + } + catch (ConversionFailedException ex) { + // fallback to default conversion logic below + conversionAttemptEx = ex; + } + } + } + + Object convertedValue = newValue; + + // Value not of required type? + if (editor != null || (requiredType != null && !ClassUtils.isAssignableValue(requiredType, convertedValue))) { + if (typeDescriptor != null && requiredType != null && Collection.class.isAssignableFrom(requiredType) && + convertedValue instanceof String) { + TypeDescriptor elementTypeDesc = typeDescriptor.getElementTypeDescriptor(); + if (elementTypeDesc != null) { + Class elementType = elementTypeDesc.getType(); + if (Class.class == elementType || Enum.class.isAssignableFrom(elementType)) { + convertedValue = StringUtils.commaDelimitedListToStringArray((String) convertedValue); + } + } + } + if (editor == null) { + editor = findDefaultEditor(requiredType); + } + convertedValue = doConvertValue(oldValue, convertedValue, requiredType, editor); + } + + boolean standardConversion = false; + + if (requiredType != null) { + // Try to apply some standard type conversion rules if appropriate. + + if (convertedValue != null) { + if (Object.class == requiredType) { + return (T) convertedValue; + } + else if (requiredType.isArray()) { + // Array required -> apply appropriate conversion of elements. + if (convertedValue instanceof String && Enum.class.isAssignableFrom(requiredType.getComponentType())) { + convertedValue = StringUtils.commaDelimitedListToStringArray((String) convertedValue); + } + return (T) convertToTypedArray(convertedValue, propertyName, requiredType.getComponentType()); + } + else if (convertedValue instanceof Collection) { + // Convert elements to target type, if determined. + convertedValue = convertToTypedCollection( + (Collection) convertedValue, propertyName, requiredType, typeDescriptor); + standardConversion = true; + } + else if (convertedValue instanceof Map) { + // Convert keys and values to respective target type, if determined. + convertedValue = convertToTypedMap( + (Map) convertedValue, propertyName, requiredType, typeDescriptor); + standardConversion = true; + } + if (convertedValue.getClass().isArray() && Array.getLength(convertedValue) == 1) { + convertedValue = Array.get(convertedValue, 0); + standardConversion = true; + } + if (String.class == requiredType && ClassUtils.isPrimitiveOrWrapper(convertedValue.getClass())) { + // We can stringify any primitive value... + return (T) convertedValue.toString(); + } + else if (convertedValue instanceof String && !requiredType.isInstance(convertedValue)) { + if (conversionAttemptEx == null && !requiredType.isInterface() && !requiredType.isEnum()) { + try { + Constructor strCtor = requiredType.getConstructor(String.class); + return BeanUtils.instantiateClass(strCtor, convertedValue); + } + catch (NoSuchMethodException ex) { + // proceed with field lookup + if (logger.isTraceEnabled()) { + logger.trace("No String constructor found on type [" + requiredType.getName() + "]", ex); + } + } + catch (Exception ex) { + if (logger.isDebugEnabled()) { + logger.debug("Construction via String failed for type [" + requiredType.getName() + "]", ex); + } + } + } + String trimmedValue = ((String) convertedValue).trim(); + if (requiredType.isEnum() && trimmedValue.isEmpty()) { + // It's an empty enum identifier: reset the enum value to null. + return null; + } + convertedValue = attemptToConvertStringToEnum(requiredType, trimmedValue, convertedValue); + standardConversion = true; + } + else if (convertedValue instanceof Number && Number.class.isAssignableFrom(requiredType)) { + convertedValue = NumberUtils.convertNumberToTargetClass( + (Number) convertedValue, (Class) requiredType); + standardConversion = true; + } + } + else { + // convertedValue == null + if (requiredType == Optional.class) { + convertedValue = Optional.empty(); + } + } + + if (!ClassUtils.isAssignableValue(requiredType, convertedValue)) { + if (conversionAttemptEx != null) { + // Original exception from former ConversionService call above... + throw conversionAttemptEx; + } + else if (conversionService != null && typeDescriptor != null) { + // ConversionService not tried before, probably custom editor found + // but editor couldn't produce the required type... + TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue); + if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) { + return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor); + } + } + + // Definitely doesn't match: throw IllegalArgumentException/IllegalStateException + StringBuilder msg = new StringBuilder(); + msg.append("Cannot convert value of type '").append(ClassUtils.getDescriptiveType(newValue)); + msg.append("' to required type '").append(ClassUtils.getQualifiedName(requiredType)).append("'"); + if (propertyName != null) { + msg.append(" for property '").append(propertyName).append("'"); + } + if (editor != null) { + msg.append(": PropertyEditor [").append(editor.getClass().getName()).append( + "] returned inappropriate value of type '").append( + ClassUtils.getDescriptiveType(convertedValue)).append("'"); + throw new IllegalArgumentException(msg.toString()); + } + else { + msg.append(": no matching editors or conversion strategy found"); + throw new IllegalStateException(msg.toString()); + } + } + } + + if (conversionAttemptEx != null) { + if (editor == null && !standardConversion && requiredType != null && Object.class != requiredType) { + throw conversionAttemptEx; + } + logger.debug("Original ConversionService attempt failed - ignored since " + + "PropertyEditor based conversion eventually succeeded", conversionAttemptEx); + } + + return (T) convertedValue; + } + + private Object attemptToConvertStringToEnum(Class requiredType, String trimmedValue, Object currentConvertedValue) { + Object convertedValue = currentConvertedValue; + + if (Enum.class == requiredType && this.targetObject != null) { + // target type is declared as raw enum, treat the trimmed value as .FIELD_NAME + int index = trimmedValue.lastIndexOf('.'); + if (index > - 1) { + String enumType = trimmedValue.substring(0, index); + String fieldName = trimmedValue.substring(index + 1); + ClassLoader cl = this.targetObject.getClass().getClassLoader(); + try { + Class enumValueType = ClassUtils.forName(enumType, cl); + Field enumField = enumValueType.getField(fieldName); + convertedValue = enumField.get(null); + } + catch (ClassNotFoundException ex) { + if (logger.isTraceEnabled()) { + logger.trace("Enum class [" + enumType + "] cannot be loaded", ex); + } + } + catch (Throwable ex) { + if (logger.isTraceEnabled()) { + logger.trace("Field [" + fieldName + "] isn't an enum value for type [" + enumType + "]", ex); + } + } + } + } + + if (convertedValue == currentConvertedValue) { + // Try field lookup as fallback: for JDK 1.5 enum or custom enum + // with values defined as static fields. Resulting value still needs + // to be checked, hence we don't return it right away. + try { + Field enumField = requiredType.getField(trimmedValue); + ReflectionUtils.makeAccessible(enumField); + convertedValue = enumField.get(null); + } + catch (Throwable ex) { + if (logger.isTraceEnabled()) { + logger.trace("Field [" + convertedValue + "] isn't an enum value", ex); + } + } + } + + return convertedValue; + } + /** + * Find a default editor for the given type. + * @param requiredType the type to find an editor for + * @return the corresponding editor, or {@code null} if none + */ + @Nullable + private PropertyEditor findDefaultEditor(@Nullable Class requiredType) { + PropertyEditor editor = null; + if (requiredType != null) { + // No custom editor -> check BeanWrapperImpl's default editors. + editor = this.propertyEditorRegistry.getDefaultEditor(requiredType); + if (editor == null && String.class != requiredType) { + // No BeanWrapper default editor -> check standard JavaBean editor. + editor = BeanUtils.findEditorByConvention(requiredType); + } + } + return editor; + } + + /** + * Convert the value to the required type (if necessary from a String), + * using the given property editor. + * @param oldValue the previous value, if available (may be {@code null}) + * @param newValue the proposed new value + * @param requiredType the type we must convert to + * (or {@code null} if not known, for example in case of a collection element) + * @param editor the PropertyEditor to use + * @return the new value, possibly the result of type conversion + * @throws IllegalArgumentException if type conversion failed + */ + @Nullable + private Object doConvertValue(@Nullable Object oldValue, @Nullable Object newValue, + @Nullable Class requiredType, @Nullable PropertyEditor editor) { + + Object convertedValue = newValue; + + if (editor != null && !(convertedValue instanceof String)) { + // Not a String -> use PropertyEditor's setValue. + // With standard PropertyEditors, this will return the very same object; + // we just want to allow special PropertyEditors to override setValue + // for type conversion from non-String values to the required type. + try { + editor.setValue(convertedValue); + Object newConvertedValue = editor.getValue(); + if (newConvertedValue != convertedValue) { + convertedValue = newConvertedValue; + // Reset PropertyEditor: It already did a proper conversion. + // Don't use it again for a setAsText call. + editor = null; + } + } + catch (Exception ex) { + if (logger.isDebugEnabled()) { + logger.debug("PropertyEditor [" + editor.getClass().getName() + "] does not support setValue call", ex); + } + // Swallow and proceed. + } + } + + Object returnValue = convertedValue; + + if (requiredType != null && !requiredType.isArray() && convertedValue instanceof String[]) { + // Convert String array to a comma-separated String. + // Only applies if no PropertyEditor converted the String array before. + // The CSV String will be passed into a PropertyEditor's setAsText method, if any. + if (logger.isTraceEnabled()) { + logger.trace("Converting String array to comma-delimited String [" + convertedValue + "]"); + } + convertedValue = StringUtils.arrayToCommaDelimitedString((String[]) convertedValue); + } + + if (convertedValue instanceof String) { + if (editor != null) { + // Use PropertyEditor's setAsText in case of a String value. + if (logger.isTraceEnabled()) { + logger.trace("Converting String to [" + requiredType + "] using property editor [" + editor + "]"); + } + String newTextValue = (String) convertedValue; + return doConvertTextValue(oldValue, newTextValue, editor); + } + else if (String.class == requiredType) { + returnValue = convertedValue; + } + } + + return returnValue; + } + + /** + * Convert the given text value using the given property editor. + * @param oldValue the previous value, if available (may be {@code null}) + * @param newTextValue the proposed text value + * @param editor the PropertyEditor to use + * @return the converted value + */ + private Object doConvertTextValue(@Nullable Object oldValue, String newTextValue, PropertyEditor editor) { + try { + editor.setValue(oldValue); + } + catch (Exception ex) { + if (logger.isDebugEnabled()) { + logger.debug("PropertyEditor [" + editor.getClass().getName() + "] does not support setValue call", ex); + } + // Swallow and proceed. + } + editor.setAsText(newTextValue); + return editor.getValue(); + } + + private Object convertToTypedArray(Object input, @Nullable String propertyName, Class componentType) { + if (input instanceof Collection) { + // Convert Collection elements to array elements. + Collection coll = (Collection) input; + Object result = Array.newInstance(componentType, coll.size()); + int i = 0; + for (Iterator it = coll.iterator(); it.hasNext(); i++) { + Object value = convertIfNecessary( + buildIndexedPropertyName(propertyName, i), null, it.next(), componentType); + Array.set(result, i, value); + } + return result; + } + else if (input.getClass().isArray()) { + // Convert array elements, if necessary. + if (componentType.equals(input.getClass().getComponentType()) && + !this.propertyEditorRegistry.hasCustomEditorForElement(componentType, propertyName)) { + return input; + } + int arrayLength = Array.getLength(input); + Object result = Array.newInstance(componentType, arrayLength); + for (int i = 0; i < arrayLength; i++) { + Object value = convertIfNecessary( + buildIndexedPropertyName(propertyName, i), null, Array.get(input, i), componentType); + Array.set(result, i, value); + } + return result; + } + else { + // A plain value: convert it to an array with a single component. + Object result = Array.newInstance(componentType, 1); + Object value = convertIfNecessary( + buildIndexedPropertyName(propertyName, 0), null, input, componentType); + Array.set(result, 0, value); + return result; + } + } + + @SuppressWarnings("unchecked") + private Collection convertToTypedCollection(Collection original, @Nullable String propertyName, + Class requiredType, @Nullable TypeDescriptor typeDescriptor) { + + if (!Collection.class.isAssignableFrom(requiredType)) { + return original; + } + + boolean approximable = CollectionFactory.isApproximableCollectionType(requiredType); + if (!approximable && !canCreateCopy(requiredType)) { + if (logger.isDebugEnabled()) { + logger.debug("Custom Collection type [" + original.getClass().getName() + + "] does not allow for creating a copy - injecting original Collection as-is"); + } + return original; + } + + boolean originalAllowed = requiredType.isInstance(original); + TypeDescriptor elementType = (typeDescriptor != null ? typeDescriptor.getElementTypeDescriptor() : null); + if (elementType == null && originalAllowed && + !this.propertyEditorRegistry.hasCustomEditorForElement(null, propertyName)) { + return original; + } + + Iterator it; + try { + it = original.iterator(); + } + catch (Throwable ex) { + if (logger.isDebugEnabled()) { + logger.debug("Cannot access Collection of type [" + original.getClass().getName() + + "] - injecting original Collection as-is: " + ex); + } + return original; + } + + Collection convertedCopy; + try { + if (approximable) { + convertedCopy = CollectionFactory.createApproximateCollection(original, original.size()); + } + else { + convertedCopy = (Collection) + ReflectionUtils.accessibleConstructor(requiredType).newInstance(); + } + } + catch (Throwable ex) { + if (logger.isDebugEnabled()) { + logger.debug("Cannot create copy of Collection type [" + original.getClass().getName() + + "] - injecting original Collection as-is: " + ex); + } + return original; + } + + for (int i = 0; it.hasNext(); i++) { + Object element = it.next(); + String indexedPropertyName = buildIndexedPropertyName(propertyName, i); + Object convertedElement = convertIfNecessary(indexedPropertyName, null, element, + (elementType != null ? elementType.getType() : null) , elementType); + try { + convertedCopy.add(convertedElement); + } + catch (Throwable ex) { + if (logger.isDebugEnabled()) { + logger.debug("Collection type [" + original.getClass().getName() + + "] seems to be read-only - injecting original Collection as-is: " + ex); + } + return original; + } + originalAllowed = originalAllowed && (element == convertedElement); + } + return (originalAllowed ? original : convertedCopy); + } + + @SuppressWarnings("unchecked") + private Map convertToTypedMap(Map original, @Nullable String propertyName, + Class requiredType, @Nullable TypeDescriptor typeDescriptor) { + + if (!Map.class.isAssignableFrom(requiredType)) { + return original; + } + + boolean approximable = CollectionFactory.isApproximableMapType(requiredType); + if (!approximable && !canCreateCopy(requiredType)) { + if (logger.isDebugEnabled()) { + logger.debug("Custom Map type [" + original.getClass().getName() + + "] does not allow for creating a copy - injecting original Map as-is"); + } + return original; + } + + boolean originalAllowed = requiredType.isInstance(original); + TypeDescriptor keyType = (typeDescriptor != null ? typeDescriptor.getMapKeyTypeDescriptor() : null); + TypeDescriptor valueType = (typeDescriptor != null ? typeDescriptor.getMapValueTypeDescriptor() : null); + if (keyType == null && valueType == null && originalAllowed && + !this.propertyEditorRegistry.hasCustomEditorForElement(null, propertyName)) { + return original; + } + + Iterator it; + try { + it = original.entrySet().iterator(); + } + catch (Throwable ex) { + if (logger.isDebugEnabled()) { + logger.debug("Cannot access Map of type [" + original.getClass().getName() + + "] - injecting original Map as-is: " + ex); + } + return original; + } + + Map convertedCopy; + try { + if (approximable) { + convertedCopy = CollectionFactory.createApproximateMap(original, original.size()); + } + else { + convertedCopy = (Map) + ReflectionUtils.accessibleConstructor(requiredType).newInstance(); + } + } + catch (Throwable ex) { + if (logger.isDebugEnabled()) { + logger.debug("Cannot create copy of Map type [" + original.getClass().getName() + + "] - injecting original Map as-is: " + ex); + } + return original; + } + + while (it.hasNext()) { + Map.Entry entry = (Map.Entry) it.next(); + Object key = entry.getKey(); + Object value = entry.getValue(); + String keyedPropertyName = buildKeyedPropertyName(propertyName, key); + Object convertedKey = convertIfNecessary(keyedPropertyName, null, key, + (keyType != null ? keyType.getType() : null), keyType); + Object convertedValue = convertIfNecessary(keyedPropertyName, null, value, + (valueType!= null ? valueType.getType() : null), valueType); + try { + convertedCopy.put(convertedKey, convertedValue); + } + catch (Throwable ex) { + if (logger.isDebugEnabled()) { + logger.debug("Map type [" + original.getClass().getName() + + "] seems to be read-only - injecting original Map as-is: " + ex); + } + return original; + } + originalAllowed = originalAllowed && (key == convertedKey) && (value == convertedValue); + } + return (originalAllowed ? original : convertedCopy); + } + + @Nullable + private String buildIndexedPropertyName(@Nullable String propertyName, int index) { + return (propertyName != null ? + propertyName + PropertyAccessor.PROPERTY_KEY_PREFIX + index + PropertyAccessor.PROPERTY_KEY_SUFFIX : + null); + } + + @Nullable + private String buildKeyedPropertyName(@Nullable String propertyName, Object key) { + return (propertyName != null ? + propertyName + PropertyAccessor.PROPERTY_KEY_PREFIX + key + PropertyAccessor.PROPERTY_KEY_SUFFIX : + null); + } + + private boolean canCreateCopy(Class requiredType) { + return (!requiredType.isInterface() && !Modifier.isAbstract(requiredType.getModifiers()) && + Modifier.isPublic(requiredType.getModifiers()) && ClassUtils.hasConstructor(requiredType)); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/TypeConverterSupport.java b/spring-beans/src/main/java/org/springframework/beans/TypeConverterSupport.java new file mode 100644 index 0000000..e99c875 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/TypeConverterSupport.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.lang.reflect.Field; + +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionException; +import org.springframework.core.convert.ConverterNotFoundException; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Base implementation of the {@link TypeConverter} interface, using a package-private delegate. + * Mainly serves as base class for {@link BeanWrapperImpl}. + * + * @author Juergen Hoeller + * @since 3.2 + * @see SimpleTypeConverter + */ +public abstract class TypeConverterSupport extends PropertyEditorRegistrySupport implements TypeConverter { + + @Nullable + TypeConverterDelegate typeConverterDelegate; + + + @Override + @Nullable + public T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType) throws TypeMismatchException { + return convertIfNecessary(value, requiredType, TypeDescriptor.valueOf(requiredType)); + } + + @Override + @Nullable + public T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, + @Nullable MethodParameter methodParam) throws TypeMismatchException { + + return convertIfNecessary(value, requiredType, + (methodParam != null ? new TypeDescriptor(methodParam) : TypeDescriptor.valueOf(requiredType))); + } + + @Override + @Nullable + public T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, @Nullable Field field) + throws TypeMismatchException { + + return convertIfNecessary(value, requiredType, + (field != null ? new TypeDescriptor(field) : TypeDescriptor.valueOf(requiredType))); + } + + @Nullable + @Override + public T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, + @Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException { + + Assert.state(this.typeConverterDelegate != null, "No TypeConverterDelegate"); + try { + return this.typeConverterDelegate.convertIfNecessary(null, null, value, requiredType, typeDescriptor); + } + catch (ConverterNotFoundException | IllegalStateException ex) { + throw new ConversionNotSupportedException(value, requiredType, ex); + } + catch (ConversionException | IllegalArgumentException ex) { + throw new TypeMismatchException(value, requiredType, ex); + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/TypeMismatchException.java b/spring-beans/src/main/java/org/springframework/beans/TypeMismatchException.java new file mode 100644 index 0000000..3177c4b --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/TypeMismatchException.java @@ -0,0 +1,151 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.beans.PropertyChangeEvent; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Exception thrown on a type mismatch when trying to set a bean property. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +@SuppressWarnings("serial") +public class TypeMismatchException extends PropertyAccessException { + + /** + * Error code that a type mismatch error will be registered with. + */ + public static final String ERROR_CODE = "typeMismatch"; + + + @Nullable + private String propertyName; + + @Nullable + private transient Object value; + + @Nullable + private Class requiredType; + + + /** + * Create a new {@code TypeMismatchException}. + * @param propertyChangeEvent the PropertyChangeEvent that resulted in the problem + * @param requiredType the required target type + */ + public TypeMismatchException(PropertyChangeEvent propertyChangeEvent, Class requiredType) { + this(propertyChangeEvent, requiredType, null); + } + + /** + * Create a new {@code TypeMismatchException}. + * @param propertyChangeEvent the PropertyChangeEvent that resulted in the problem + * @param requiredType the required target type (or {@code null} if not known) + * @param cause the root cause (may be {@code null}) + */ + public TypeMismatchException(PropertyChangeEvent propertyChangeEvent, @Nullable Class requiredType, + @Nullable Throwable cause) { + + super(propertyChangeEvent, + "Failed to convert property value of type '" + + ClassUtils.getDescriptiveType(propertyChangeEvent.getNewValue()) + "'" + + (requiredType != null ? + " to required type '" + ClassUtils.getQualifiedName(requiredType) + "'" : "") + + (propertyChangeEvent.getPropertyName() != null ? + " for property '" + propertyChangeEvent.getPropertyName() + "'" : ""), + cause); + this.propertyName = propertyChangeEvent.getPropertyName(); + this.value = propertyChangeEvent.getNewValue(); + this.requiredType = requiredType; + } + + /** + * Create a new {@code TypeMismatchException} without a {@code PropertyChangeEvent}. + * @param value the offending value that couldn't be converted (may be {@code null}) + * @param requiredType the required target type (or {@code null} if not known) + * @see #initPropertyName + */ + public TypeMismatchException(@Nullable Object value, @Nullable Class requiredType) { + this(value, requiredType, null); + } + + /** + * Create a new {@code TypeMismatchException} without a {@code PropertyChangeEvent}. + * @param value the offending value that couldn't be converted (may be {@code null}) + * @param requiredType the required target type (or {@code null} if not known) + * @param cause the root cause (may be {@code null}) + * @see #initPropertyName + */ + public TypeMismatchException(@Nullable Object value, @Nullable Class requiredType, @Nullable Throwable cause) { + super("Failed to convert value of type '" + ClassUtils.getDescriptiveType(value) + "'" + + (requiredType != null ? " to required type '" + ClassUtils.getQualifiedName(requiredType) + "'" : ""), + cause); + this.value = value; + this.requiredType = requiredType; + } + + + /** + * Initialize this exception's property name for exposure through {@link #getPropertyName()}, + * as an alternative to having it initialized via a {@link PropertyChangeEvent}. + * @param propertyName the property name to expose + * @since 5.0.4 + * @see #TypeMismatchException(Object, Class) + * @see #TypeMismatchException(Object, Class, Throwable) + */ + public void initPropertyName(String propertyName) { + Assert.state(this.propertyName == null, "Property name already initialized"); + this.propertyName = propertyName; + } + + /** + * Return the name of the affected property, if available. + */ + @Override + @Nullable + public String getPropertyName() { + return this.propertyName; + } + + /** + * Return the offending value (may be {@code null}). + */ + @Override + @Nullable + public Object getValue() { + return this.value; + } + + /** + * Return the required target type, if any. + */ + @Nullable + public Class getRequiredType() { + return this.requiredType; + } + + @Override + public String getErrorCode() { + return ERROR_CODE; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/annotation/AnnotationBeanUtils.java b/spring-beans/src/main/java/org/springframework/beans/annotation/AnnotationBeanUtils.java new file mode 100644 index 0000000..c0b22ad --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/annotation/AnnotationBeanUtils.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.PropertyAccessorFactory; +import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringValueResolver; + +/** + * General utility methods for working with annotations in JavaBeans style. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + * @deprecated as of 5.2, in favor of custom annotation attribute processing + */ +@Deprecated +public abstract class AnnotationBeanUtils { + + /** + * Copy the properties of the supplied {@link Annotation} to the supplied target bean. + * Any properties defined in {@code excludedProperties} will not be copied. + * @param ann the annotation to copy from + * @param bean the bean instance to copy to + * @param excludedProperties the names of excluded properties, if any + * @see org.springframework.beans.BeanWrapper + */ + public static void copyPropertiesToBean(Annotation ann, Object bean, String... excludedProperties) { + copyPropertiesToBean(ann, bean, null, excludedProperties); + } + + /** + * Copy the properties of the supplied {@link Annotation} to the supplied target bean. + * Any properties defined in {@code excludedProperties} will not be copied. + *

A specified value resolver may resolve placeholders in property values, for example. + * @param ann the annotation to copy from + * @param bean the bean instance to copy to + * @param valueResolver a resolve to post-process String property values (may be {@code null}) + * @param excludedProperties the names of excluded properties, if any + * @see org.springframework.beans.BeanWrapper + */ + public static void copyPropertiesToBean(Annotation ann, Object bean, @Nullable StringValueResolver valueResolver, + String... excludedProperties) { + + Set excluded = (excludedProperties.length == 0 ? Collections.emptySet() : + new HashSet<>(Arrays.asList(excludedProperties))); + Method[] annotationProperties = ann.annotationType().getDeclaredMethods(); + BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(bean); + for (Method annotationProperty : annotationProperties) { + String propertyName = annotationProperty.getName(); + if (!excluded.contains(propertyName) && bw.isWritableProperty(propertyName)) { + Object value = ReflectionUtils.invokeMethod(annotationProperty, ann); + if (valueResolver != null && value instanceof String) { + value = valueResolver.resolveStringValue((String) value); + } + bw.setPropertyValue(propertyName, value); + } + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/annotation/package-info.java b/spring-beans/src/main/java/org/springframework/beans/annotation/package-info.java new file mode 100644 index 0000000..d2667da --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/annotation/package-info.java @@ -0,0 +1,9 @@ +/** + * Support package for beans-style handling of Java 5 annotations. + */ +@NonNullApi +@NonNullFields +package org.springframework.beans.annotation; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/Aware.java b/spring-beans/src/main/java/org/springframework/beans/factory/Aware.java new file mode 100644 index 0000000..14ec404 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/Aware.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +/** + * A marker superinterface indicating that a bean is eligible to be notified by the + * Spring container of a particular framework object through a callback-style method. + * The actual method signature is determined by individual subinterfaces but should + * typically consist of just one void-returning method that accepts a single argument. + * + *

Note that merely implementing {@link Aware} provides no default functionality. + * Rather, processing must be done explicitly, for example in a + * {@link org.springframework.beans.factory.config.BeanPostProcessor}. + * Refer to {@link org.springframework.context.support.ApplicationContextAwareProcessor} + * for an example of processing specific {@code *Aware} interface callbacks. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + */ +public interface Aware { + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanClassLoaderAware.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanClassLoaderAware.java new file mode 100644 index 0000000..179781a --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanClassLoaderAware.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +/** + * Callback that allows a bean to be aware of the bean + * {@link ClassLoader class loader}; that is, the class loader used by the + * present bean factory to load bean classes. + * + *

This is mainly intended to be implemented by framework classes which + * have to pick up application classes by name despite themselves potentially + * being loaded from a shared class loader. + * + *

For a list of all bean lifecycle methods, see the + * {@link BeanFactory BeanFactory javadocs}. + * + * @author Juergen Hoeller + * @author Chris Beams + * @since 2.0 + * @see BeanNameAware + * @see BeanFactoryAware + * @see InitializingBean + */ +public interface BeanClassLoaderAware extends Aware { + + /** + * Callback that supplies the bean {@link ClassLoader class loader} to + * a bean instance. + *

Invoked after the population of normal bean properties but + * before an initialization callback such as + * {@link InitializingBean InitializingBean's} + * {@link InitializingBean#afterPropertiesSet()} + * method or a custom init-method. + * @param classLoader the owning class loader + */ + void setBeanClassLoader(ClassLoader classLoader); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanCreationException.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanCreationException.java new file mode 100644 index 0000000..7e7d62d --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanCreationException.java @@ -0,0 +1,216 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +import java.io.PrintStream; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.FatalBeanException; +import org.springframework.core.NestedRuntimeException; +import org.springframework.lang.Nullable; + +/** + * Exception thrown when a BeanFactory encounters an error when + * attempting to create a bean from a bean definition. + * + * @author Juergen Hoeller + */ +@SuppressWarnings("serial") +public class BeanCreationException extends FatalBeanException { + + @Nullable + private final String beanName; + + @Nullable + private final String resourceDescription; + + @Nullable + private List relatedCauses; + + + /** + * Create a new BeanCreationException. + * @param msg the detail message + */ + public BeanCreationException(String msg) { + super(msg); + this.beanName = null; + this.resourceDescription = null; + } + + /** + * Create a new BeanCreationException. + * @param msg the detail message + * @param cause the root cause + */ + public BeanCreationException(String msg, Throwable cause) { + super(msg, cause); + this.beanName = null; + this.resourceDescription = null; + } + + /** + * Create a new BeanCreationException. + * @param beanName the name of the bean requested + * @param msg the detail message + */ + public BeanCreationException(String beanName, String msg) { + super("Error creating bean with name '" + beanName + "': " + msg); + this.beanName = beanName; + this.resourceDescription = null; + } + + /** + * Create a new BeanCreationException. + * @param beanName the name of the bean requested + * @param msg the detail message + * @param cause the root cause + */ + public BeanCreationException(String beanName, String msg, Throwable cause) { + this(beanName, msg); + initCause(cause); + } + + /** + * Create a new BeanCreationException. + * @param resourceDescription description of the resource + * that the bean definition came from + * @param beanName the name of the bean requested + * @param msg the detail message + */ + public BeanCreationException(@Nullable String resourceDescription, @Nullable String beanName, String msg) { + super("Error creating bean with name '" + beanName + "'" + + (resourceDescription != null ? " defined in " + resourceDescription : "") + ": " + msg); + this.resourceDescription = resourceDescription; + this.beanName = beanName; + this.relatedCauses = null; + } + + /** + * Create a new BeanCreationException. + * @param resourceDescription description of the resource + * that the bean definition came from + * @param beanName the name of the bean requested + * @param msg the detail message + * @param cause the root cause + */ + public BeanCreationException(@Nullable String resourceDescription, String beanName, String msg, Throwable cause) { + this(resourceDescription, beanName, msg); + initCause(cause); + } + + + /** + * Return the description of the resource that the bean + * definition came from, if any. + */ + @Nullable + public String getResourceDescription() { + return this.resourceDescription; + } + + /** + * Return the name of the bean requested, if any. + */ + @Nullable + public String getBeanName() { + return this.beanName; + } + + /** + * Add a related cause to this bean creation exception, + * not being a direct cause of the failure but having occurred + * earlier in the creation of the same bean instance. + * @param ex the related cause to add + */ + public void addRelatedCause(Throwable ex) { + if (this.relatedCauses == null) { + this.relatedCauses = new ArrayList<>(); + } + this.relatedCauses.add(ex); + } + + /** + * Return the related causes, if any. + * @return the array of related causes, or {@code null} if none + */ + @Nullable + public Throwable[] getRelatedCauses() { + if (this.relatedCauses == null) { + return null; + } + return this.relatedCauses.toArray(new Throwable[0]); + } + + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(super.toString()); + if (this.relatedCauses != null) { + for (Throwable relatedCause : this.relatedCauses) { + sb.append("\nRelated cause: "); + sb.append(relatedCause); + } + } + return sb.toString(); + } + + @Override + public void printStackTrace(PrintStream ps) { + synchronized (ps) { + super.printStackTrace(ps); + if (this.relatedCauses != null) { + for (Throwable relatedCause : this.relatedCauses) { + ps.println("Related cause:"); + relatedCause.printStackTrace(ps); + } + } + } + } + + @Override + public void printStackTrace(PrintWriter pw) { + synchronized (pw) { + super.printStackTrace(pw); + if (this.relatedCauses != null) { + for (Throwable relatedCause : this.relatedCauses) { + pw.println("Related cause:"); + relatedCause.printStackTrace(pw); + } + } + } + } + + @Override + public boolean contains(@Nullable Class exClass) { + if (super.contains(exClass)) { + return true; + } + if (this.relatedCauses != null) { + for (Throwable relatedCause : this.relatedCauses) { + if (relatedCause instanceof NestedRuntimeException && + ((NestedRuntimeException) relatedCause).contains(exClass)) { + return true; + } + } + } + return false; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanCreationNotAllowedException.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanCreationNotAllowedException.java new file mode 100644 index 0000000..45ff0c4 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanCreationNotAllowedException.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +/** + * Exception thrown in case of a bean being requested despite + * bean creation currently not being allowed (for example, during + * the shutdown phase of a bean factory). + * + * @author Juergen Hoeller + * @since 2.0 + */ +@SuppressWarnings("serial") +public class BeanCreationNotAllowedException extends BeanCreationException { + + /** + * Create a new BeanCreationNotAllowedException. + * @param beanName the name of the bean requested + * @param msg the detail message + */ + public BeanCreationNotAllowedException(String beanName, String msg) { + super(beanName, msg); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanCurrentlyInCreationException.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanCurrentlyInCreationException.java new file mode 100644 index 0000000..4c984fb --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanCurrentlyInCreationException.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +/** + * Exception thrown in case of a reference to a bean that's currently in creation. + * Typically happens when constructor autowiring matches the currently constructed bean. + * + * @author Juergen Hoeller + * @since 1.1 + */ +@SuppressWarnings("serial") +public class BeanCurrentlyInCreationException extends BeanCreationException { + + /** + * Create a new BeanCurrentlyInCreationException, + * with a default error message that indicates a circular reference. + * @param beanName the name of the bean requested + */ + public BeanCurrentlyInCreationException(String beanName) { + super(beanName, + "Requested bean is currently in creation: Is there an unresolvable circular reference?"); + } + + /** + * Create a new BeanCurrentlyInCreationException. + * @param beanName the name of the bean requested + * @param msg the detail message + */ + public BeanCurrentlyInCreationException(String beanName, String msg) { + super(beanName, msg); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanDefinitionStoreException.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanDefinitionStoreException.java new file mode 100644 index 0000000..d807d5f --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanDefinitionStoreException.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +import org.springframework.beans.FatalBeanException; +import org.springframework.lang.Nullable; + +/** + * Exception thrown when a BeanFactory encounters an invalid bean definition: + * e.g. in case of incomplete or contradictory bean metadata. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Rob Harrop + */ +@SuppressWarnings("serial") +public class BeanDefinitionStoreException extends FatalBeanException { + + @Nullable + private final String resourceDescription; + + @Nullable + private final String beanName; + + + /** + * Create a new BeanDefinitionStoreException. + * @param msg the detail message (used as exception message as-is) + */ + public BeanDefinitionStoreException(String msg) { + super(msg); + this.resourceDescription = null; + this.beanName = null; + } + + /** + * Create a new BeanDefinitionStoreException. + * @param msg the detail message (used as exception message as-is) + * @param cause the root cause (may be {@code null}) + */ + public BeanDefinitionStoreException(String msg, @Nullable Throwable cause) { + super(msg, cause); + this.resourceDescription = null; + this.beanName = null; + } + + /** + * Create a new BeanDefinitionStoreException. + * @param resourceDescription description of the resource that the bean definition came from + * @param msg the detail message (used as exception message as-is) + */ + public BeanDefinitionStoreException(@Nullable String resourceDescription, String msg) { + super(msg); + this.resourceDescription = resourceDescription; + this.beanName = null; + } + + /** + * Create a new BeanDefinitionStoreException. + * @param resourceDescription description of the resource that the bean definition came from + * @param msg the detail message (used as exception message as-is) + * @param cause the root cause (may be {@code null}) + */ + public BeanDefinitionStoreException(@Nullable String resourceDescription, String msg, @Nullable Throwable cause) { + super(msg, cause); + this.resourceDescription = resourceDescription; + this.beanName = null; + } + + /** + * Create a new BeanDefinitionStoreException. + * @param resourceDescription description of the resource that the bean definition came from + * @param beanName the name of the bean + * @param msg the detail message (appended to an introductory message that indicates + * the resource and the name of the bean) + */ + public BeanDefinitionStoreException(@Nullable String resourceDescription, String beanName, String msg) { + this(resourceDescription, beanName, msg, null); + } + + /** + * Create a new BeanDefinitionStoreException. + * @param resourceDescription description of the resource that the bean definition came from + * @param beanName the name of the bean + * @param msg the detail message (appended to an introductory message that indicates + * the resource and the name of the bean) + * @param cause the root cause (may be {@code null}) + */ + public BeanDefinitionStoreException( + @Nullable String resourceDescription, String beanName, String msg, @Nullable Throwable cause) { + + super("Invalid bean definition with name '" + beanName + "' defined in " + resourceDescription + ": " + msg, + cause); + this.resourceDescription = resourceDescription; + this.beanName = beanName; + } + + + /** + * Return the description of the resource that the bean definition came from, if available. + */ + @Nullable + public String getResourceDescription() { + return this.resourceDescription; + } + + /** + * Return the name of the bean, if available. + */ + @Nullable + public String getBeanName() { + return this.beanName; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanExpressionException.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanExpressionException.java new file mode 100644 index 0000000..8af52b8 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanExpressionException.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +import org.springframework.beans.FatalBeanException; + +/** + * Exception that indicates an expression evaluation attempt having failed. + * + * @author Juergen Hoeller + * @since 3.0 + */ +@SuppressWarnings("serial") +public class BeanExpressionException extends FatalBeanException { + + /** + * Create a new BeanExpressionException with the specified message. + * @param msg the detail message + */ + public BeanExpressionException(String msg) { + super(msg); + } + + /** + * Create a new BeanExpressionException with the specified message + * and root cause. + * @param msg the detail message + * @param cause the root cause + */ + public BeanExpressionException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java new file mode 100644 index 0000000..f31c29d --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java @@ -0,0 +1,375 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +import org.springframework.beans.BeansException; +import org.springframework.core.ResolvableType; +import org.springframework.lang.Nullable; + +/** + * The root interface for accessing a Spring bean container. + * + *

This is the basic client view of a bean container; + * further interfaces such as {@link ListableBeanFactory} and + * {@link org.springframework.beans.factory.config.ConfigurableBeanFactory} + * are available for specific purposes. + * + *

This interface is implemented by objects that hold a number of bean definitions, + * each uniquely identified by a String name. Depending on the bean definition, + * the factory will return either an independent instance of a contained object + * (the Prototype design pattern), or a single shared instance (a superior + * alternative to the Singleton design pattern, in which the instance is a + * singleton in the scope of the factory). Which type of instance will be returned + * depends on the bean factory configuration: the API is the same. Since Spring + * 2.0, further scopes are available depending on the concrete application + * context (e.g. "request" and "session" scopes in a web environment). + * + *

The point of this approach is that the BeanFactory is a central registry + * of application components, and centralizes configuration of application + * components (no more do individual objects need to read properties files, + * for example). See chapters 4 and 11 of "Expert One-on-One J2EE Design and + * Development" for a discussion of the benefits of this approach. + * + *

Note that it is generally better to rely on Dependency Injection + * ("push" configuration) to configure application objects through setters + * or constructors, rather than use any form of "pull" configuration like a + * BeanFactory lookup. Spring's Dependency Injection functionality is + * implemented using this BeanFactory interface and its subinterfaces. + * + *

Normally a BeanFactory will load bean definitions stored in a configuration + * source (such as an XML document), and use the {@code org.springframework.beans} + * package to configure the beans. However, an implementation could simply return + * Java objects it creates as necessary directly in Java code. There are no + * constraints on how the definitions could be stored: LDAP, RDBMS, XML, + * properties file, etc. Implementations are encouraged to support references + * amongst beans (Dependency Injection). + * + *

In contrast to the methods in {@link ListableBeanFactory}, all of the + * operations in this interface will also check parent factories if this is a + * {@link HierarchicalBeanFactory}. If a bean is not found in this factory instance, + * the immediate parent factory will be asked. Beans in this factory instance + * are supposed to override beans of the same name in any parent factory. + * + *

Bean factory implementations should support the standard bean lifecycle interfaces + * as far as possible. The full set of initialization methods and their standard order is: + *

    + *
  1. BeanNameAware's {@code setBeanName} + *
  2. BeanClassLoaderAware's {@code setBeanClassLoader} + *
  3. BeanFactoryAware's {@code setBeanFactory} + *
  4. EnvironmentAware's {@code setEnvironment} + *
  5. EmbeddedValueResolverAware's {@code setEmbeddedValueResolver} + *
  6. ResourceLoaderAware's {@code setResourceLoader} + * (only applicable when running in an application context) + *
  7. ApplicationEventPublisherAware's {@code setApplicationEventPublisher} + * (only applicable when running in an application context) + *
  8. MessageSourceAware's {@code setMessageSource} + * (only applicable when running in an application context) + *
  9. ApplicationContextAware's {@code setApplicationContext} + * (only applicable when running in an application context) + *
  10. ServletContextAware's {@code setServletContext} + * (only applicable when running in a web application context) + *
  11. {@code postProcessBeforeInitialization} methods of BeanPostProcessors + *
  12. InitializingBean's {@code afterPropertiesSet} + *
  13. a custom init-method definition + *
  14. {@code postProcessAfterInitialization} methods of BeanPostProcessors + *
+ * + *

On shutdown of a bean factory, the following lifecycle methods apply: + *

    + *
  1. {@code postProcessBeforeDestruction} methods of DestructionAwareBeanPostProcessors + *
  2. DisposableBean's {@code destroy} + *
  3. a custom destroy-method definition + *
+ * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Chris Beams + * @since 13 April 2001 + * @see BeanNameAware#setBeanName + * @see BeanClassLoaderAware#setBeanClassLoader + * @see BeanFactoryAware#setBeanFactory + * @see org.springframework.context.ResourceLoaderAware#setResourceLoader + * @see org.springframework.context.ApplicationEventPublisherAware#setApplicationEventPublisher + * @see org.springframework.context.MessageSourceAware#setMessageSource + * @see org.springframework.context.ApplicationContextAware#setApplicationContext + * @see org.springframework.web.context.ServletContextAware#setServletContext + * @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessBeforeInitialization + * @see InitializingBean#afterPropertiesSet + * @see org.springframework.beans.factory.support.RootBeanDefinition#getInitMethodName + * @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization + * @see DisposableBean#destroy + * @see org.springframework.beans.factory.support.RootBeanDefinition#getDestroyMethodName + */ +public interface BeanFactory { + + /** + * Used to dereference a {@link FactoryBean} instance and distinguish it from + * beans created by the FactoryBean. For example, if the bean named + * {@code myJndiObject} is a FactoryBean, getting {@code &myJndiObject} + * will return the factory, not the instance returned by the factory. + */ + String FACTORY_BEAN_PREFIX = "&"; + + + /** + * Return an instance, which may be shared or independent, of the specified bean. + *

This method allows a Spring BeanFactory to be used as a replacement for the + * Singleton or Prototype design pattern. Callers may retain references to + * returned objects in the case of Singleton beans. + *

Translates aliases back to the corresponding canonical bean name. + *

Will ask the parent factory if the bean cannot be found in this factory instance. + * @param name the name of the bean to retrieve + * @return an instance of the bean + * @throws NoSuchBeanDefinitionException if there is no bean with the specified name + * @throws BeansException if the bean could not be obtained + */ + Object getBean(String name) throws BeansException; + + /** + * Return an instance, which may be shared or independent, of the specified bean. + *

Behaves the same as {@link #getBean(String)}, but provides a measure of type + * safety by throwing a BeanNotOfRequiredTypeException if the bean is not of the + * required type. This means that ClassCastException can't be thrown on casting + * the result correctly, as can happen with {@link #getBean(String)}. + *

Translates aliases back to the corresponding canonical bean name. + *

Will ask the parent factory if the bean cannot be found in this factory instance. + * @param name the name of the bean to retrieve + * @param requiredType type the bean must match; can be an interface or superclass + * @return an instance of the bean + * @throws NoSuchBeanDefinitionException if there is no such bean definition + * @throws BeanNotOfRequiredTypeException if the bean is not of the required type + * @throws BeansException if the bean could not be created + */ + T getBean(String name, Class requiredType) throws BeansException; + + /** + * Return an instance, which may be shared or independent, of the specified bean. + *

Allows for specifying explicit constructor arguments / factory method arguments, + * overriding the specified default arguments (if any) in the bean definition. + * @param name the name of the bean to retrieve + * @param args arguments to use when creating a bean instance using explicit arguments + * (only applied when creating a new instance as opposed to retrieving an existing one) + * @return an instance of the bean + * @throws NoSuchBeanDefinitionException if there is no such bean definition + * @throws BeanDefinitionStoreException if arguments have been given but + * the affected bean isn't a prototype + * @throws BeansException if the bean could not be created + * @since 2.5 + */ + Object getBean(String name, Object... args) throws BeansException; + + /** + * Return the bean instance that uniquely matches the given object type, if any. + *

This method goes into {@link ListableBeanFactory} by-type lookup territory + * but may also be translated into a conventional by-name lookup based on the name + * of the given type. For more extensive retrieval operations across sets of beans, + * use {@link ListableBeanFactory} and/or {@link BeanFactoryUtils}. + * @param requiredType type the bean must match; can be an interface or superclass + * @return an instance of the single bean matching the required type + * @throws NoSuchBeanDefinitionException if no bean of the given type was found + * @throws NoUniqueBeanDefinitionException if more than one bean of the given type was found + * @throws BeansException if the bean could not be created + * @since 3.0 + * @see ListableBeanFactory + */ + T getBean(Class requiredType) throws BeansException; + + /** + * Return an instance, which may be shared or independent, of the specified bean. + *

Allows for specifying explicit constructor arguments / factory method arguments, + * overriding the specified default arguments (if any) in the bean definition. + *

This method goes into {@link ListableBeanFactory} by-type lookup territory + * but may also be translated into a conventional by-name lookup based on the name + * of the given type. For more extensive retrieval operations across sets of beans, + * use {@link ListableBeanFactory} and/or {@link BeanFactoryUtils}. + * @param requiredType type the bean must match; can be an interface or superclass + * @param args arguments to use when creating a bean instance using explicit arguments + * (only applied when creating a new instance as opposed to retrieving an existing one) + * @return an instance of the bean + * @throws NoSuchBeanDefinitionException if there is no such bean definition + * @throws BeanDefinitionStoreException if arguments have been given but + * the affected bean isn't a prototype + * @throws BeansException if the bean could not be created + * @since 4.1 + */ + T getBean(Class requiredType, Object... args) throws BeansException; + + /** + * Return a provider for the specified bean, allowing for lazy on-demand retrieval + * of instances, including availability and uniqueness options. + * @param requiredType type the bean must match; can be an interface or superclass + * @return a corresponding provider handle + * @since 5.1 + * @see #getBeanProvider(ResolvableType) + */ + ObjectProvider getBeanProvider(Class requiredType); + + /** + * Return a provider for the specified bean, allowing for lazy on-demand retrieval + * of instances, including availability and uniqueness options. + * @param requiredType type the bean must match; can be a generic type declaration. + * Note that collection types are not supported here, in contrast to reflective + * injection points. For programmatically retrieving a list of beans matching a + * specific type, specify the actual bean type as an argument here and subsequently + * use {@link ObjectProvider#orderedStream()} or its lazy streaming/iteration options. + * @return a corresponding provider handle + * @since 5.1 + * @see ObjectProvider#iterator() + * @see ObjectProvider#stream() + * @see ObjectProvider#orderedStream() + */ + ObjectProvider getBeanProvider(ResolvableType requiredType); + + /** + * Does this bean factory contain a bean definition or externally registered singleton + * instance with the given name? + *

If the given name is an alias, it will be translated back to the corresponding + * canonical bean name. + *

If this factory is hierarchical, will ask any parent factory if the bean cannot + * be found in this factory instance. + *

If a bean definition or singleton instance matching the given name is found, + * this method will return {@code true} whether the named bean definition is concrete + * or abstract, lazy or eager, in scope or not. Therefore, note that a {@code true} + * return value from this method does not necessarily indicate that {@link #getBean} + * will be able to obtain an instance for the same name. + * @param name the name of the bean to query + * @return whether a bean with the given name is present + */ + boolean containsBean(String name); + + /** + * Is this bean a shared singleton? That is, will {@link #getBean} always + * return the same instance? + *

Note: This method returning {@code false} does not clearly indicate + * independent instances. It indicates non-singleton instances, which may correspond + * to a scoped bean as well. Use the {@link #isPrototype} operation to explicitly + * check for independent instances. + *

Translates aliases back to the corresponding canonical bean name. + *

Will ask the parent factory if the bean cannot be found in this factory instance. + * @param name the name of the bean to query + * @return whether this bean corresponds to a singleton instance + * @throws NoSuchBeanDefinitionException if there is no bean with the given name + * @see #getBean + * @see #isPrototype + */ + boolean isSingleton(String name) throws NoSuchBeanDefinitionException; + + /** + * Is this bean a prototype? That is, will {@link #getBean} always return + * independent instances? + *

Note: This method returning {@code false} does not clearly indicate + * a singleton object. It indicates non-independent instances, which may correspond + * to a scoped bean as well. Use the {@link #isSingleton} operation to explicitly + * check for a shared singleton instance. + *

Translates aliases back to the corresponding canonical bean name. + *

Will ask the parent factory if the bean cannot be found in this factory instance. + * @param name the name of the bean to query + * @return whether this bean will always deliver independent instances + * @throws NoSuchBeanDefinitionException if there is no bean with the given name + * @since 2.0.3 + * @see #getBean + * @see #isSingleton + */ + boolean isPrototype(String name) throws NoSuchBeanDefinitionException; + + /** + * Check whether the bean with the given name matches the specified type. + * More specifically, check whether a {@link #getBean} call for the given name + * would return an object that is assignable to the specified target type. + *

Translates aliases back to the corresponding canonical bean name. + *

Will ask the parent factory if the bean cannot be found in this factory instance. + * @param name the name of the bean to query + * @param typeToMatch the type to match against (as a {@code ResolvableType}) + * @return {@code true} if the bean type matches, + * {@code false} if it doesn't match or cannot be determined yet + * @throws NoSuchBeanDefinitionException if there is no bean with the given name + * @since 4.2 + * @see #getBean + * @see #getType + */ + boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException; + + /** + * Check whether the bean with the given name matches the specified type. + * More specifically, check whether a {@link #getBean} call for the given name + * would return an object that is assignable to the specified target type. + *

Translates aliases back to the corresponding canonical bean name. + *

Will ask the parent factory if the bean cannot be found in this factory instance. + * @param name the name of the bean to query + * @param typeToMatch the type to match against (as a {@code Class}) + * @return {@code true} if the bean type matches, + * {@code false} if it doesn't match or cannot be determined yet + * @throws NoSuchBeanDefinitionException if there is no bean with the given name + * @since 2.0.1 + * @see #getBean + * @see #getType + */ + boolean isTypeMatch(String name, Class typeToMatch) throws NoSuchBeanDefinitionException; + + /** + * Determine the type of the bean with the given name. More specifically, + * determine the type of object that {@link #getBean} would return for the given name. + *

For a {@link FactoryBean}, return the type of object that the FactoryBean creates, + * as exposed by {@link FactoryBean#getObjectType()}. This may lead to the initialization + * of a previously uninitialized {@code FactoryBean} (see {@link #getType(String, boolean)}). + *

Translates aliases back to the corresponding canonical bean name. + *

Will ask the parent factory if the bean cannot be found in this factory instance. + * @param name the name of the bean to query + * @return the type of the bean, or {@code null} if not determinable + * @throws NoSuchBeanDefinitionException if there is no bean with the given name + * @since 1.1.2 + * @see #getBean + * @see #isTypeMatch + */ + @Nullable + Class getType(String name) throws NoSuchBeanDefinitionException; + + /** + * Determine the type of the bean with the given name. More specifically, + * determine the type of object that {@link #getBean} would return for the given name. + *

For a {@link FactoryBean}, return the type of object that the FactoryBean creates, + * as exposed by {@link FactoryBean#getObjectType()}. Depending on the + * {@code allowFactoryBeanInit} flag, this may lead to the initialization of a previously + * uninitialized {@code FactoryBean} if no early type information is available. + *

Translates aliases back to the corresponding canonical bean name. + *

Will ask the parent factory if the bean cannot be found in this factory instance. + * @param name the name of the bean to query + * @param allowFactoryBeanInit whether a {@code FactoryBean} may get initialized + * just for the purpose of determining its object type + * @return the type of the bean, or {@code null} if not determinable + * @throws NoSuchBeanDefinitionException if there is no bean with the given name + * @since 5.2 + * @see #getBean + * @see #isTypeMatch + */ + @Nullable + Class getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException; + + /** + * Return the aliases for the given bean name, if any. + *

All of those aliases point to the same bean when used in a {@link #getBean} call. + *

If the given name is an alias, the corresponding original bean name + * and other aliases (if any) will be returned, with the original bean name + * being the first element in the array. + *

Will ask the parent factory if the bean cannot be found in this factory instance. + * @param name the bean name to check for aliases + * @return the aliases, or an empty array if none + * @see #getBean + */ + String[] getAliases(String name); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryAware.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryAware.java new file mode 100644 index 0000000..9812e0b --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryAware.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +import org.springframework.beans.BeansException; + +/** + * Interface to be implemented by beans that wish to be aware of their + * owning {@link BeanFactory}. + * + *

For example, beans can look up collaborating beans via the factory + * (Dependency Lookup). Note that most beans will choose to receive references + * to collaborating beans via corresponding bean properties or constructor + * arguments (Dependency Injection). + * + *

For a list of all bean lifecycle methods, see the + * {@link BeanFactory BeanFactory javadocs}. + * + * @author Rod Johnson + * @author Chris Beams + * @since 11.03.2003 + * @see BeanNameAware + * @see BeanClassLoaderAware + * @see InitializingBean + * @see org.springframework.context.ApplicationContextAware + */ +public interface BeanFactoryAware extends Aware { + + /** + * Callback that supplies the owning factory to a bean instance. + *

Invoked after the population of normal bean properties + * but before an initialization callback such as + * {@link InitializingBean#afterPropertiesSet()} or a custom init-method. + * @param beanFactory owning BeanFactory (never {@code null}). + * The bean can immediately call methods on the factory. + * @throws BeansException in case of initialization errors + * @see BeanInitializationException + */ + void setBeanFactory(BeanFactory beanFactory) throws BeansException; + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java new file mode 100644 index 0000000..17a5d70 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java @@ -0,0 +1,563 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.beans.BeansException; +import org.springframework.core.ResolvableType; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Convenience methods operating on bean factories, in particular + * on the {@link ListableBeanFactory} interface. + * + *

Returns bean counts, bean names or bean instances, + * taking into account the nesting hierarchy of a bean factory + * (which the methods defined on the ListableBeanFactory interface don't, + * in contrast to the methods defined on the BeanFactory interface). + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Chris Beams + * @since 04.07.2003 + */ +public abstract class BeanFactoryUtils { + + /** + * Separator for generated bean names. If a class name or parent name is not + * unique, "#1", "#2" etc will be appended, until the name becomes unique. + */ + public static final String GENERATED_BEAN_NAME_SEPARATOR = "#"; + + /** + * Cache from name with factory bean prefix to stripped name without dereference. + * @since 5.1 + * @see BeanFactory#FACTORY_BEAN_PREFIX + */ + private static final Map transformedBeanNameCache = new ConcurrentHashMap<>(); + + + /** + * Return whether the given name is a factory dereference + * (beginning with the factory dereference prefix). + * @param name the name of the bean + * @return whether the given name is a factory dereference + * @see BeanFactory#FACTORY_BEAN_PREFIX + */ + public static boolean isFactoryDereference(@Nullable String name) { + return (name != null && name.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)); + } + + /** + * Return the actual bean name, stripping out the factory dereference + * prefix (if any, also stripping repeated factory prefixes if found). + * @param name the name of the bean + * @return the transformed name + * @see BeanFactory#FACTORY_BEAN_PREFIX + */ + public static String transformedBeanName(String name) { + Assert.notNull(name, "'name' must not be null"); + if (!name.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)) { + return name; + } + return transformedBeanNameCache.computeIfAbsent(name, beanName -> { + do { + beanName = beanName.substring(BeanFactory.FACTORY_BEAN_PREFIX.length()); + } + while (beanName.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)); + return beanName; + }); + } + + /** + * Return whether the given name is a bean name which has been generated + * by the default naming strategy (containing a "#..." part). + * @param name the name of the bean + * @return whether the given name is a generated bean name + * @see #GENERATED_BEAN_NAME_SEPARATOR + * @see org.springframework.beans.factory.support.BeanDefinitionReaderUtils#generateBeanName + * @see org.springframework.beans.factory.support.DefaultBeanNameGenerator + */ + public static boolean isGeneratedBeanName(@Nullable String name) { + return (name != null && name.contains(GENERATED_BEAN_NAME_SEPARATOR)); + } + + /** + * Extract the "raw" bean name from the given (potentially generated) bean name, + * excluding any "#..." suffixes which might have been added for uniqueness. + * @param name the potentially generated bean name + * @return the raw bean name + * @see #GENERATED_BEAN_NAME_SEPARATOR + */ + public static String originalBeanName(String name) { + Assert.notNull(name, "'name' must not be null"); + int separatorIndex = name.indexOf(GENERATED_BEAN_NAME_SEPARATOR); + return (separatorIndex != -1 ? name.substring(0, separatorIndex) : name); + } + + + // Retrieval of bean names + + /** + * Count all beans in any hierarchy in which this factory participates. + * Includes counts of ancestor bean factories. + *

Beans that are "overridden" (specified in a descendant factory + * with the same name) are only counted once. + * @param lbf the bean factory + * @return count of beans including those defined in ancestor factories + * @see #beanNamesIncludingAncestors + */ + public static int countBeansIncludingAncestors(ListableBeanFactory lbf) { + return beanNamesIncludingAncestors(lbf).length; + } + + /** + * Return all bean names in the factory, including ancestor factories. + * @param lbf the bean factory + * @return the array of matching bean names, or an empty array if none + * @see #beanNamesForTypeIncludingAncestors + */ + public static String[] beanNamesIncludingAncestors(ListableBeanFactory lbf) { + return beanNamesForTypeIncludingAncestors(lbf, Object.class); + } + + /** + * Get all bean names for the given type, including those defined in ancestor + * factories. Will return unique names in case of overridden bean definitions. + *

Does consider objects created by FactoryBeans, which means that FactoryBeans + * will get initialized. If the object created by the FactoryBean doesn't match, + * the raw FactoryBean itself will be matched against the type. + *

This version of {@code beanNamesForTypeIncludingAncestors} automatically + * includes prototypes and FactoryBeans. + * @param lbf the bean factory + * @param type the type that beans must match (as a {@code ResolvableType}) + * @return the array of matching bean names, or an empty array if none + * @since 4.2 + * @see ListableBeanFactory#getBeanNamesForType(ResolvableType) + */ + public static String[] beanNamesForTypeIncludingAncestors(ListableBeanFactory lbf, ResolvableType type) { + Assert.notNull(lbf, "ListableBeanFactory must not be null"); + String[] result = lbf.getBeanNamesForType(type); + if (lbf instanceof HierarchicalBeanFactory) { + HierarchicalBeanFactory hbf = (HierarchicalBeanFactory) lbf; + if (hbf.getParentBeanFactory() instanceof ListableBeanFactory) { + String[] parentResult = beanNamesForTypeIncludingAncestors( + (ListableBeanFactory) hbf.getParentBeanFactory(), type); + result = mergeNamesWithParent(result, parentResult, hbf); + } + } + return result; + } + + /** + * Get all bean names for the given type, including those defined in ancestor + * factories. Will return unique names in case of overridden bean definitions. + *

Does consider objects created by FactoryBeans if the "allowEagerInit" + * flag is set, which means that FactoryBeans will get initialized. If the + * object created by the FactoryBean doesn't match, the raw FactoryBean itself + * will be matched against the type. If "allowEagerInit" is not set, + * only raw FactoryBeans will be checked (which doesn't require initialization + * of each FactoryBean). + * @param lbf the bean factory + * @param type the type that beans must match (as a {@code ResolvableType}) + * @param includeNonSingletons whether to include prototype or scoped beans too + * or just singletons (also applies to FactoryBeans) + * @param allowEagerInit whether to initialize lazy-init singletons and + * objects created by FactoryBeans (or by factory methods with a + * "factory-bean" reference) for the type check. Note that FactoryBeans need to be + * eagerly initialized to determine their type: So be aware that passing in "true" + * for this flag will initialize FactoryBeans and "factory-bean" references. + * @return the array of matching bean names, or an empty array if none + * @since 5.2 + * @see ListableBeanFactory#getBeanNamesForType(ResolvableType, boolean, boolean) + */ + public static String[] beanNamesForTypeIncludingAncestors( + ListableBeanFactory lbf, ResolvableType type, boolean includeNonSingletons, boolean allowEagerInit) { + + Assert.notNull(lbf, "ListableBeanFactory must not be null"); + String[] result = lbf.getBeanNamesForType(type, includeNonSingletons, allowEagerInit); + if (lbf instanceof HierarchicalBeanFactory) { + HierarchicalBeanFactory hbf = (HierarchicalBeanFactory) lbf; + if (hbf.getParentBeanFactory() instanceof ListableBeanFactory) { + String[] parentResult = beanNamesForTypeIncludingAncestors( + (ListableBeanFactory) hbf.getParentBeanFactory(), type, includeNonSingletons, allowEagerInit); + result = mergeNamesWithParent(result, parentResult, hbf); + } + } + return result; + } + + /** + * Get all bean names for the given type, including those defined in ancestor + * factories. Will return unique names in case of overridden bean definitions. + *

Does consider objects created by FactoryBeans, which means that FactoryBeans + * will get initialized. If the object created by the FactoryBean doesn't match, + * the raw FactoryBean itself will be matched against the type. + *

This version of {@code beanNamesForTypeIncludingAncestors} automatically + * includes prototypes and FactoryBeans. + * @param lbf the bean factory + * @param type the type that beans must match (as a {@code Class}) + * @return the array of matching bean names, or an empty array if none + * @see ListableBeanFactory#getBeanNamesForType(Class) + */ + public static String[] beanNamesForTypeIncludingAncestors(ListableBeanFactory lbf, Class type) { + Assert.notNull(lbf, "ListableBeanFactory must not be null"); + String[] result = lbf.getBeanNamesForType(type); + if (lbf instanceof HierarchicalBeanFactory) { + HierarchicalBeanFactory hbf = (HierarchicalBeanFactory) lbf; + if (hbf.getParentBeanFactory() instanceof ListableBeanFactory) { + String[] parentResult = beanNamesForTypeIncludingAncestors( + (ListableBeanFactory) hbf.getParentBeanFactory(), type); + result = mergeNamesWithParent(result, parentResult, hbf); + } + } + return result; + } + + /** + * Get all bean names for the given type, including those defined in ancestor + * factories. Will return unique names in case of overridden bean definitions. + *

Does consider objects created by FactoryBeans if the "allowEagerInit" + * flag is set, which means that FactoryBeans will get initialized. If the + * object created by the FactoryBean doesn't match, the raw FactoryBean itself + * will be matched against the type. If "allowEagerInit" is not set, + * only raw FactoryBeans will be checked (which doesn't require initialization + * of each FactoryBean). + * @param lbf the bean factory + * @param includeNonSingletons whether to include prototype or scoped beans too + * or just singletons (also applies to FactoryBeans) + * @param allowEagerInit whether to initialize lazy-init singletons and + * objects created by FactoryBeans (or by factory methods with a + * "factory-bean" reference) for the type check. Note that FactoryBeans need to be + * eagerly initialized to determine their type: So be aware that passing in "true" + * for this flag will initialize FactoryBeans and "factory-bean" references. + * @param type the type that beans must match + * @return the array of matching bean names, or an empty array if none + * @see ListableBeanFactory#getBeanNamesForType(Class, boolean, boolean) + */ + public static String[] beanNamesForTypeIncludingAncestors( + ListableBeanFactory lbf, Class type, boolean includeNonSingletons, boolean allowEagerInit) { + + Assert.notNull(lbf, "ListableBeanFactory must not be null"); + String[] result = lbf.getBeanNamesForType(type, includeNonSingletons, allowEagerInit); + if (lbf instanceof HierarchicalBeanFactory) { + HierarchicalBeanFactory hbf = (HierarchicalBeanFactory) lbf; + if (hbf.getParentBeanFactory() instanceof ListableBeanFactory) { + String[] parentResult = beanNamesForTypeIncludingAncestors( + (ListableBeanFactory) hbf.getParentBeanFactory(), type, includeNonSingletons, allowEagerInit); + result = mergeNamesWithParent(result, parentResult, hbf); + } + } + return result; + } + + /** + * Get all bean names whose {@code Class} has the supplied {@link Annotation} + * type, including those defined in ancestor factories, without creating any bean + * instances yet. Will return unique names in case of overridden bean definitions. + * @param lbf the bean factory + * @param annotationType the type of annotation to look for + * @return the array of matching bean names, or an empty array if none + * @since 5.0 + * @see ListableBeanFactory#getBeanNamesForAnnotation(Class) + */ + public static String[] beanNamesForAnnotationIncludingAncestors( + ListableBeanFactory lbf, Class annotationType) { + + Assert.notNull(lbf, "ListableBeanFactory must not be null"); + String[] result = lbf.getBeanNamesForAnnotation(annotationType); + if (lbf instanceof HierarchicalBeanFactory) { + HierarchicalBeanFactory hbf = (HierarchicalBeanFactory) lbf; + if (hbf.getParentBeanFactory() instanceof ListableBeanFactory) { + String[] parentResult = beanNamesForAnnotationIncludingAncestors( + (ListableBeanFactory) hbf.getParentBeanFactory(), annotationType); + result = mergeNamesWithParent(result, parentResult, hbf); + } + } + return result; + } + + + // Retrieval of bean instances + + /** + * Return all beans of the given type or subtypes, also picking up beans defined in + * ancestor bean factories if the current bean factory is a HierarchicalBeanFactory. + * The returned Map will only contain beans of this type. + *

Does consider objects created by FactoryBeans, which means that FactoryBeans + * will get initialized. If the object created by the FactoryBean doesn't match, + * the raw FactoryBean itself will be matched against the type. + *

Note: Beans of the same name will take precedence at the 'lowest' factory level, + * i.e. such beans will be returned from the lowest factory that they are being found in, + * hiding corresponding beans in ancestor factories. This feature allows for + * 'replacing' beans by explicitly choosing the same bean name in a child factory; + * the bean in the ancestor factory won't be visible then, not even for by-type lookups. + * @param lbf the bean factory + * @param type type of bean to match + * @return the Map of matching bean instances, or an empty Map if none + * @throws BeansException if a bean could not be created + * @see ListableBeanFactory#getBeansOfType(Class) + */ + public static Map beansOfTypeIncludingAncestors(ListableBeanFactory lbf, Class type) + throws BeansException { + + Assert.notNull(lbf, "ListableBeanFactory must not be null"); + Map result = new LinkedHashMap<>(4); + result.putAll(lbf.getBeansOfType(type)); + if (lbf instanceof HierarchicalBeanFactory) { + HierarchicalBeanFactory hbf = (HierarchicalBeanFactory) lbf; + if (hbf.getParentBeanFactory() instanceof ListableBeanFactory) { + Map parentResult = beansOfTypeIncludingAncestors( + (ListableBeanFactory) hbf.getParentBeanFactory(), type); + parentResult.forEach((beanName, beanInstance) -> { + if (!result.containsKey(beanName) && !hbf.containsLocalBean(beanName)) { + result.put(beanName, beanInstance); + } + }); + } + } + return result; + } + + /** + * Return all beans of the given type or subtypes, also picking up beans defined in + * ancestor bean factories if the current bean factory is a HierarchicalBeanFactory. + * The returned Map will only contain beans of this type. + *

Does consider objects created by FactoryBeans if the "allowEagerInit" flag is set, + * which means that FactoryBeans will get initialized. If the object created by the + * FactoryBean doesn't match, the raw FactoryBean itself will be matched against the + * type. If "allowEagerInit" is not set, only raw FactoryBeans will be checked + * (which doesn't require initialization of each FactoryBean). + *

Note: Beans of the same name will take precedence at the 'lowest' factory level, + * i.e. such beans will be returned from the lowest factory that they are being found in, + * hiding corresponding beans in ancestor factories. This feature allows for + * 'replacing' beans by explicitly choosing the same bean name in a child factory; + * the bean in the ancestor factory won't be visible then, not even for by-type lookups. + * @param lbf the bean factory + * @param type type of bean to match + * @param includeNonSingletons whether to include prototype or scoped beans too + * or just singletons (also applies to FactoryBeans) + * @param allowEagerInit whether to initialize lazy-init singletons and + * objects created by FactoryBeans (or by factory methods with a + * "factory-bean" reference) for the type check. Note that FactoryBeans need to be + * eagerly initialized to determine their type: So be aware that passing in "true" + * for this flag will initialize FactoryBeans and "factory-bean" references. + * @return the Map of matching bean instances, or an empty Map if none + * @throws BeansException if a bean could not be created + * @see ListableBeanFactory#getBeansOfType(Class, boolean, boolean) + */ + public static Map beansOfTypeIncludingAncestors( + ListableBeanFactory lbf, Class type, boolean includeNonSingletons, boolean allowEagerInit) + throws BeansException { + + Assert.notNull(lbf, "ListableBeanFactory must not be null"); + Map result = new LinkedHashMap<>(4); + result.putAll(lbf.getBeansOfType(type, includeNonSingletons, allowEagerInit)); + if (lbf instanceof HierarchicalBeanFactory) { + HierarchicalBeanFactory hbf = (HierarchicalBeanFactory) lbf; + if (hbf.getParentBeanFactory() instanceof ListableBeanFactory) { + Map parentResult = beansOfTypeIncludingAncestors( + (ListableBeanFactory) hbf.getParentBeanFactory(), type, includeNonSingletons, allowEagerInit); + parentResult.forEach((beanName, beanInstance) -> { + if (!result.containsKey(beanName) && !hbf.containsLocalBean(beanName)) { + result.put(beanName, beanInstance); + } + }); + } + } + return result; + } + + /** + * Return a single bean of the given type or subtypes, also picking up beans + * defined in ancestor bean factories if the current bean factory is a + * HierarchicalBeanFactory. Useful convenience method when we expect a + * single bean and don't care about the bean name. + *

Does consider objects created by FactoryBeans, which means that FactoryBeans + * will get initialized. If the object created by the FactoryBean doesn't match, + * the raw FactoryBean itself will be matched against the type. + *

This version of {@code beanOfTypeIncludingAncestors} automatically includes + * prototypes and FactoryBeans. + *

Note: Beans of the same name will take precedence at the 'lowest' factory level, + * i.e. such beans will be returned from the lowest factory that they are being found in, + * hiding corresponding beans in ancestor factories. This feature allows for + * 'replacing' beans by explicitly choosing the same bean name in a child factory; + * the bean in the ancestor factory won't be visible then, not even for by-type lookups. + * @param lbf the bean factory + * @param type type of bean to match + * @return the matching bean instance + * @throws NoSuchBeanDefinitionException if no bean of the given type was found + * @throws NoUniqueBeanDefinitionException if more than one bean of the given type was found + * @throws BeansException if the bean could not be created + * @see #beansOfTypeIncludingAncestors(ListableBeanFactory, Class) + */ + public static T beanOfTypeIncludingAncestors(ListableBeanFactory lbf, Class type) + throws BeansException { + + Map beansOfType = beansOfTypeIncludingAncestors(lbf, type); + return uniqueBean(type, beansOfType); + } + + /** + * Return a single bean of the given type or subtypes, also picking up beans + * defined in ancestor bean factories if the current bean factory is a + * HierarchicalBeanFactory. Useful convenience method when we expect a + * single bean and don't care about the bean name. + *

Does consider objects created by FactoryBeans if the "allowEagerInit" flag is set, + * which means that FactoryBeans will get initialized. If the object created by the + * FactoryBean doesn't match, the raw FactoryBean itself will be matched against the + * type. If "allowEagerInit" is not set, only raw FactoryBeans will be checked + * (which doesn't require initialization of each FactoryBean). + *

Note: Beans of the same name will take precedence at the 'lowest' factory level, + * i.e. such beans will be returned from the lowest factory that they are being found in, + * hiding corresponding beans in ancestor factories. This feature allows for + * 'replacing' beans by explicitly choosing the same bean name in a child factory; + * the bean in the ancestor factory won't be visible then, not even for by-type lookups. + * @param lbf the bean factory + * @param type type of bean to match + * @param includeNonSingletons whether to include prototype or scoped beans too + * or just singletons (also applies to FactoryBeans) + * @param allowEagerInit whether to initialize lazy-init singletons and + * objects created by FactoryBeans (or by factory methods with a + * "factory-bean" reference) for the type check. Note that FactoryBeans need to be + * eagerly initialized to determine their type: So be aware that passing in "true" + * for this flag will initialize FactoryBeans and "factory-bean" references. + * @return the matching bean instance + * @throws NoSuchBeanDefinitionException if no bean of the given type was found + * @throws NoUniqueBeanDefinitionException if more than one bean of the given type was found + * @throws BeansException if the bean could not be created + * @see #beansOfTypeIncludingAncestors(ListableBeanFactory, Class, boolean, boolean) + */ + public static T beanOfTypeIncludingAncestors( + ListableBeanFactory lbf, Class type, boolean includeNonSingletons, boolean allowEagerInit) + throws BeansException { + + Map beansOfType = beansOfTypeIncludingAncestors(lbf, type, includeNonSingletons, allowEagerInit); + return uniqueBean(type, beansOfType); + } + + /** + * Return a single bean of the given type or subtypes, not looking in ancestor + * factories. Useful convenience method when we expect a single bean and + * don't care about the bean name. + *

Does consider objects created by FactoryBeans, which means that FactoryBeans + * will get initialized. If the object created by the FactoryBean doesn't match, + * the raw FactoryBean itself will be matched against the type. + *

This version of {@code beanOfType} automatically includes + * prototypes and FactoryBeans. + * @param lbf the bean factory + * @param type type of bean to match + * @return the matching bean instance + * @throws NoSuchBeanDefinitionException if no bean of the given type was found + * @throws NoUniqueBeanDefinitionException if more than one bean of the given type was found + * @throws BeansException if the bean could not be created + * @see ListableBeanFactory#getBeansOfType(Class) + */ + public static T beanOfType(ListableBeanFactory lbf, Class type) throws BeansException { + Assert.notNull(lbf, "ListableBeanFactory must not be null"); + Map beansOfType = lbf.getBeansOfType(type); + return uniqueBean(type, beansOfType); + } + + /** + * Return a single bean of the given type or subtypes, not looking in ancestor + * factories. Useful convenience method when we expect a single bean and + * don't care about the bean name. + *

Does consider objects created by FactoryBeans if the "allowEagerInit" + * flag is set, which means that FactoryBeans will get initialized. If the + * object created by the FactoryBean doesn't match, the raw FactoryBean itself + * will be matched against the type. If "allowEagerInit" is not set, + * only raw FactoryBeans will be checked (which doesn't require initialization + * of each FactoryBean). + * @param lbf the bean factory + * @param type type of bean to match + * @param includeNonSingletons whether to include prototype or scoped beans too + * or just singletons (also applies to FactoryBeans) + * @param allowEagerInit whether to initialize lazy-init singletons and + * objects created by FactoryBeans (or by factory methods with a + * "factory-bean" reference) for the type check. Note that FactoryBeans need to be + * eagerly initialized to determine their type: So be aware that passing in "true" + * for this flag will initialize FactoryBeans and "factory-bean" references. + * @return the matching bean instance + * @throws NoSuchBeanDefinitionException if no bean of the given type was found + * @throws NoUniqueBeanDefinitionException if more than one bean of the given type was found + * @throws BeansException if the bean could not be created + * @see ListableBeanFactory#getBeansOfType(Class, boolean, boolean) + */ + public static T beanOfType( + ListableBeanFactory lbf, Class type, boolean includeNonSingletons, boolean allowEagerInit) + throws BeansException { + + Assert.notNull(lbf, "ListableBeanFactory must not be null"); + Map beansOfType = lbf.getBeansOfType(type, includeNonSingletons, allowEagerInit); + return uniqueBean(type, beansOfType); + } + + + /** + * Merge the given bean names result with the given parent result. + * @param result the local bean name result + * @param parentResult the parent bean name result (possibly empty) + * @param hbf the local bean factory + * @return the merged result (possibly the local result as-is) + * @since 4.3.15 + */ + private static String[] mergeNamesWithParent(String[] result, String[] parentResult, HierarchicalBeanFactory hbf) { + if (parentResult.length == 0) { + return result; + } + List merged = new ArrayList<>(result.length + parentResult.length); + merged.addAll(Arrays.asList(result)); + for (String beanName : parentResult) { + if (!merged.contains(beanName) && !hbf.containsLocalBean(beanName)) { + merged.add(beanName); + } + } + return StringUtils.toStringArray(merged); + } + + /** + * Extract a unique bean for the given type from the given Map of matching beans. + * @param type type of bean to match + * @param matchingBeans all matching beans found + * @return the unique bean instance + * @throws NoSuchBeanDefinitionException if no bean of the given type was found + * @throws NoUniqueBeanDefinitionException if more than one bean of the given type was found + */ + private static T uniqueBean(Class type, Map matchingBeans) { + int count = matchingBeans.size(); + if (count == 1) { + return matchingBeans.values().iterator().next(); + } + else if (count > 1) { + throw new NoUniqueBeanDefinitionException(type, matchingBeans.keySet()); + } + else { + throw new NoSuchBeanDefinitionException(type); + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanInitializationException.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanInitializationException.java new file mode 100644 index 0000000..e32973f --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanInitializationException.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +import org.springframework.beans.FatalBeanException; + +/** + * Exception that a bean implementation is suggested to throw if its own + * factory-aware initialization code fails. BeansExceptions thrown by + * bean factory methods themselves should simply be propagated as-is. + * + *

Note that {@code afterPropertiesSet()} or a custom "init-method" + * can throw any exception. + * + * @author Juergen Hoeller + * @since 13.11.2003 + * @see BeanFactoryAware#setBeanFactory + * @see InitializingBean#afterPropertiesSet + */ +@SuppressWarnings("serial") +public class BeanInitializationException extends FatalBeanException { + + /** + * Create a new BeanInitializationException with the specified message. + * @param msg the detail message + */ + public BeanInitializationException(String msg) { + super(msg); + } + + /** + * Create a new BeanInitializationException with the specified message + * and root cause. + * @param msg the detail message + * @param cause the root cause + */ + public BeanInitializationException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanIsAbstractException.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanIsAbstractException.java new file mode 100644 index 0000000..69228a4 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanIsAbstractException.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +/** + * Exception thrown when a bean instance has been requested for + * a bean definition which has been marked as abstract. + * + * @author Juergen Hoeller + * @since 1.1 + * @see org.springframework.beans.factory.support.AbstractBeanDefinition#setAbstract + */ +@SuppressWarnings("serial") +public class BeanIsAbstractException extends BeanCreationException { + + /** + * Create a new BeanIsAbstractException. + * @param beanName the name of the bean requested + */ + public BeanIsAbstractException(String beanName) { + super(beanName, "Bean definition is abstract"); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanIsNotAFactoryException.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanIsNotAFactoryException.java new file mode 100644 index 0000000..213bcad --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanIsNotAFactoryException.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +/** + * Exception thrown when a bean is not a factory, but a user tries to get + * at the factory for the given bean name. Whether a bean is a factory is + * determined by whether it implements the FactoryBean interface. + * + * @author Rod Johnson + * @since 10.03.2003 + * @see org.springframework.beans.factory.FactoryBean + */ +@SuppressWarnings("serial") +public class BeanIsNotAFactoryException extends BeanNotOfRequiredTypeException { + + /** + * Create a new BeanIsNotAFactoryException. + * @param name the name of the bean requested + * @param actualType the actual type returned, which did not match + * the expected type + */ + public BeanIsNotAFactoryException(String name, Class actualType) { + super(name, FactoryBean.class, actualType); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanNameAware.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanNameAware.java new file mode 100644 index 0000000..994899c --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanNameAware.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +/** + * Interface to be implemented by beans that want to be aware of their + * bean name in a bean factory. Note that it is not usually recommended + * that an object depends on its bean name, as this represents a potentially + * brittle dependence on external configuration, as well as a possibly + * unnecessary dependence on a Spring API. + * + *

For a list of all bean lifecycle methods, see the + * {@link BeanFactory BeanFactory javadocs}. + * + * @author Juergen Hoeller + * @author Chris Beams + * @since 01.11.2003 + * @see BeanClassLoaderAware + * @see BeanFactoryAware + * @see InitializingBean + */ +public interface BeanNameAware extends Aware { + + /** + * Set the name of the bean in the bean factory that created this bean. + *

Invoked after population of normal bean properties but before an + * init callback such as {@link InitializingBean#afterPropertiesSet()} + * or a custom init-method. + * @param name the name of the bean in the factory. + * Note that this name is the actual bean name used in the factory, which may + * differ from the originally specified name: in particular for inner bean + * names, the actual bean name might have been made unique through appending + * "#..." suffixes. Use the {@link BeanFactoryUtils#originalBeanName(String)} + * method to extract the original bean name (without suffix), if desired. + */ + void setBeanName(String name); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanNotOfRequiredTypeException.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanNotOfRequiredTypeException.java new file mode 100644 index 0000000..1c122e1 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanNotOfRequiredTypeException.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +import org.springframework.beans.BeansException; +import org.springframework.util.ClassUtils; + +/** + * Thrown when a bean doesn't match the expected type. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +@SuppressWarnings("serial") +public class BeanNotOfRequiredTypeException extends BeansException { + + /** The name of the instance that was of the wrong type. */ + private final String beanName; + + /** The required type. */ + private final Class requiredType; + + /** The offending type. */ + private final Class actualType; + + + /** + * Create a new BeanNotOfRequiredTypeException. + * @param beanName the name of the bean requested + * @param requiredType the required type + * @param actualType the actual type returned, which did not match + * the expected type + */ + public BeanNotOfRequiredTypeException(String beanName, Class requiredType, Class actualType) { + super("Bean named '" + beanName + "' is expected to be of type '" + ClassUtils.getQualifiedName(requiredType) + + "' but was actually of type '" + ClassUtils.getQualifiedName(actualType) + "'"); + this.beanName = beanName; + this.requiredType = requiredType; + this.actualType = actualType; + } + + + /** + * Return the name of the instance that was of the wrong type. + */ + public String getBeanName() { + return this.beanName; + } + + /** + * Return the expected type for the bean. + */ + public Class getRequiredType() { + return this.requiredType; + } + + /** + * Return the actual type of the instance found. + */ + public Class getActualType() { + return this.actualType; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/CannotLoadBeanClassException.java b/spring-beans/src/main/java/org/springframework/beans/factory/CannotLoadBeanClassException.java new file mode 100644 index 0000000..fc26cc0 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/CannotLoadBeanClassException.java @@ -0,0 +1,103 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +import org.springframework.beans.FatalBeanException; +import org.springframework.lang.Nullable; + +/** + * Exception thrown when the BeanFactory cannot load the specified class + * of a given bean. + * + * @author Juergen Hoeller + * @since 2.0 + */ +@SuppressWarnings("serial") +public class CannotLoadBeanClassException extends FatalBeanException { + + @Nullable + private final String resourceDescription; + + private final String beanName; + + @Nullable + private final String beanClassName; + + + /** + * Create a new CannotLoadBeanClassException. + * @param resourceDescription description of the resource + * that the bean definition came from + * @param beanName the name of the bean requested + * @param beanClassName the name of the bean class + * @param cause the root cause + */ + public CannotLoadBeanClassException(@Nullable String resourceDescription, String beanName, + @Nullable String beanClassName, ClassNotFoundException cause) { + + super("Cannot find class [" + beanClassName + "] for bean with name '" + beanName + "'" + + (resourceDescription != null ? " defined in " + resourceDescription : ""), cause); + this.resourceDescription = resourceDescription; + this.beanName = beanName; + this.beanClassName = beanClassName; + } + + /** + * Create a new CannotLoadBeanClassException. + * @param resourceDescription description of the resource + * that the bean definition came from + * @param beanName the name of the bean requested + * @param beanClassName the name of the bean class + * @param cause the root cause + */ + public CannotLoadBeanClassException(@Nullable String resourceDescription, String beanName, + @Nullable String beanClassName, LinkageError cause) { + + super("Error loading class [" + beanClassName + "] for bean with name '" + beanName + "'" + + (resourceDescription != null ? " defined in " + resourceDescription : "") + + ": problem with class file or dependent class", cause); + this.resourceDescription = resourceDescription; + this.beanName = beanName; + this.beanClassName = beanClassName; + } + + + /** + * Return the description of the resource that the bean + * definition came from. + */ + @Nullable + public String getResourceDescription() { + return this.resourceDescription; + } + + /** + * Return the name of the bean requested. + */ + public String getBeanName() { + return this.beanName; + } + + /** + * Return the name of the class we were trying to load. + */ + @Nullable + public String getBeanClassName() { + return this.beanClassName; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/DisposableBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/DisposableBean.java new file mode 100644 index 0000000..bb7ea0a --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/DisposableBean.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +/** + * Interface to be implemented by beans that want to release resources on destruction. + * A {@link BeanFactory} will invoke the destroy method on individual destruction of a + * scoped bean. An {@link org.springframework.context.ApplicationContext} is supposed + * to dispose all of its singletons on shutdown, driven by the application lifecycle. + * + *

A Spring-managed bean may also implement Java's {@link AutoCloseable} interface + * for the same purpose. An alternative to implementing an interface is specifying a + * custom destroy method, for example in an XML bean definition. For a list of all + * bean lifecycle methods, see the {@link BeanFactory BeanFactory javadocs}. + * + * @author Juergen Hoeller + * @since 12.08.2003 + * @see InitializingBean + * @see org.springframework.beans.factory.support.RootBeanDefinition#getDestroyMethodName() + * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#destroySingletons() + * @see org.springframework.context.ConfigurableApplicationContext#close() + */ +public interface DisposableBean { + + /** + * Invoked by the containing {@code BeanFactory} on destruction of a bean. + * @throws Exception in case of shutdown errors. Exceptions will get logged + * but not rethrown to allow other beans to release their resources as well. + */ + void destroy() throws Exception; + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/FactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/FactoryBean.java new file mode 100644 index 0000000..224563c --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/FactoryBean.java @@ -0,0 +1,149 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +import org.springframework.lang.Nullable; + +/** + * Interface to be implemented by objects used within a {@link BeanFactory} which + * are themselves factories for individual objects. If a bean implements this + * interface, it is used as a factory for an object to expose, not directly as a + * bean instance that will be exposed itself. + * + *

NB: A bean that implements this interface cannot be used as a normal bean. + * A FactoryBean is defined in a bean style, but the object exposed for bean + * references ({@link #getObject()}) is always the object that it creates. + * + *

FactoryBeans can support singletons and prototypes, and can either create + * objects lazily on demand or eagerly on startup. The {@link SmartFactoryBean} + * interface allows for exposing more fine-grained behavioral metadata. + * + *

This interface is heavily used within the framework itself, for example for + * the AOP {@link org.springframework.aop.framework.ProxyFactoryBean} or the + * {@link org.springframework.jndi.JndiObjectFactoryBean}. It can be used for + * custom components as well; however, this is only common for infrastructure code. + * + *

{@code FactoryBean} is a programmatic contract. Implementations are not + * supposed to rely on annotation-driven injection or other reflective facilities. + * {@link #getObjectType()} {@link #getObject()} invocations may arrive early in the + * bootstrap process, even ahead of any post-processor setup. If you need access to + * other beans, implement {@link BeanFactoryAware} and obtain them programmatically. + * + *

The container is only responsible for managing the lifecycle of the FactoryBean + * instance, not the lifecycle of the objects created by the FactoryBean. Therefore, + * a destroy method on an exposed bean object (such as {@link java.io.Closeable#close()} + * will not be called automatically. Instead, a FactoryBean should implement + * {@link DisposableBean} and delegate any such close call to the underlying object. + * + *

Finally, FactoryBean objects participate in the containing BeanFactory's + * synchronization of bean creation. There is usually no need for internal + * synchronization other than for purposes of lazy initialization within the + * FactoryBean itself (or the like). + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 08.03.2003 + * @param the bean type + * @see org.springframework.beans.factory.BeanFactory + * @see org.springframework.aop.framework.ProxyFactoryBean + * @see org.springframework.jndi.JndiObjectFactoryBean + */ +public interface FactoryBean { + + /** + * The name of an attribute that can be + * {@link org.springframework.core.AttributeAccessor#setAttribute set} on a + * {@link org.springframework.beans.factory.config.BeanDefinition} so that + * factory beans can signal their object type when it can't be deduced from + * the factory bean class. + * @since 5.2 + */ + String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType"; + + + /** + * Return an instance (possibly shared or independent) of the object + * managed by this factory. + *

As with a {@link BeanFactory}, this allows support for both the + * Singleton and Prototype design pattern. + *

If this FactoryBean is not fully initialized yet at the time of + * the call (for example because it is involved in a circular reference), + * throw a corresponding {@link FactoryBeanNotInitializedException}. + *

As of Spring 2.0, FactoryBeans are allowed to return {@code null} + * objects. The factory will consider this as normal value to be used; it + * will not throw a FactoryBeanNotInitializedException in this case anymore. + * FactoryBean implementations are encouraged to throw + * FactoryBeanNotInitializedException themselves now, as appropriate. + * @return an instance of the bean (can be {@code null}) + * @throws Exception in case of creation errors + * @see FactoryBeanNotInitializedException + */ + @Nullable + T getObject() throws Exception; + + /** + * Return the type of object that this FactoryBean creates, + * or {@code null} if not known in advance. + *

This allows one to check for specific types of beans without + * instantiating objects, for example on autowiring. + *

In the case of implementations that are creating a singleton object, + * this method should try to avoid singleton creation as far as possible; + * it should rather estimate the type in advance. + * For prototypes, returning a meaningful type here is advisable too. + *

This method can be called before this FactoryBean has + * been fully initialized. It must not rely on state created during + * initialization; of course, it can still use such state if available. + *

NOTE: Autowiring will simply ignore FactoryBeans that return + * {@code null} here. Therefore it is highly recommended to implement + * this method properly, using the current state of the FactoryBean. + * @return the type of object that this FactoryBean creates, + * or {@code null} if not known at the time of the call + * @see ListableBeanFactory#getBeansOfType + */ + @Nullable + Class getObjectType(); + + /** + * Is the object managed by this factory a singleton? That is, + * will {@link #getObject()} always return the same object + * (a reference that can be cached)? + *

NOTE: If a FactoryBean indicates to hold a singleton object, + * the object returned from {@code getObject()} might get cached + * by the owning BeanFactory. Hence, do not return {@code true} + * unless the FactoryBean always exposes the same reference. + *

The singleton status of the FactoryBean itself will generally + * be provided by the owning BeanFactory; usually, it has to be + * defined as singleton there. + *

NOTE: This method returning {@code false} does not + * necessarily indicate that returned objects are independent instances. + * An implementation of the extended {@link SmartFactoryBean} interface + * may explicitly indicate independent instances through its + * {@link SmartFactoryBean#isPrototype()} method. Plain {@link FactoryBean} + * implementations which do not implement this extended interface are + * simply assumed to always return independent instances if the + * {@code isSingleton()} implementation returns {@code false}. + *

The default implementation returns {@code true}, since a + * {@code FactoryBean} typically manages a singleton instance. + * @return whether the exposed object is a singleton + * @see #getObject() + * @see SmartFactoryBean#isPrototype() + */ + default boolean isSingleton() { + return true; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/FactoryBeanNotInitializedException.java b/spring-beans/src/main/java/org/springframework/beans/factory/FactoryBeanNotInitializedException.java new file mode 100644 index 0000000..520c66b --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/FactoryBeanNotInitializedException.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +import org.springframework.beans.FatalBeanException; + +/** + * Exception to be thrown from a FactoryBean's {@code getObject()} method + * if the bean is not fully initialized yet, for example because it is involved + * in a circular reference. + * + *

Note: A circular reference with a FactoryBean cannot be solved by eagerly + * caching singleton instances like with normal beans. The reason is that + * every FactoryBean needs to be fully initialized before it can + * return the created bean, while only specific normal beans need + * to be initialized - that is, if a collaborating bean actually invokes + * them on initialization instead of just storing the reference. + * + * @author Juergen Hoeller + * @since 30.10.2003 + * @see FactoryBean#getObject() + */ +@SuppressWarnings("serial") +public class FactoryBeanNotInitializedException extends FatalBeanException { + + /** + * Create a new FactoryBeanNotInitializedException with the default message. + */ + public FactoryBeanNotInitializedException() { + super("FactoryBean is not fully initialized yet"); + } + + /** + * Create a new FactoryBeanNotInitializedException with the given message. + * @param msg the detail message + */ + public FactoryBeanNotInitializedException(String msg) { + super(msg); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/HierarchicalBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/HierarchicalBeanFactory.java new file mode 100644 index 0000000..d750443 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/HierarchicalBeanFactory.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +import org.springframework.lang.Nullable; + +/** + * Sub-interface implemented by bean factories that can be part + * of a hierarchy. + * + *

The corresponding {@code setParentBeanFactory} method for bean + * factories that allow setting the parent in a configurable + * fashion can be found in the ConfigurableBeanFactory interface. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 07.07.2003 + * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#setParentBeanFactory + */ +public interface HierarchicalBeanFactory extends BeanFactory { + + /** + * Return the parent bean factory, or {@code null} if there is none. + */ + @Nullable + BeanFactory getParentBeanFactory(); + + /** + * Return whether the local bean factory contains a bean of the given name, + * ignoring beans defined in ancestor contexts. + *

This is an alternative to {@code containsBean}, ignoring a bean + * of the given name from an ancestor bean factory. + * @param name the name of the bean to query + * @return whether a bean with the given name is defined in the local factory + * @see BeanFactory#containsBean + */ + boolean containsLocalBean(String name); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/InitializingBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/InitializingBean.java new file mode 100644 index 0000000..940c2dd --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/InitializingBean.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +/** + * Interface to be implemented by beans that need to react once all their properties + * have been set by a {@link BeanFactory}: e.g. to perform custom initialization, + * or merely to check that all mandatory properties have been set. + * + *

An alternative to implementing {@code InitializingBean} is specifying a custom + * init method, for example in an XML bean definition. For a list of all bean + * lifecycle methods, see the {@link BeanFactory BeanFactory javadocs}. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see DisposableBean + * @see org.springframework.beans.factory.config.BeanDefinition#getPropertyValues() + * @see org.springframework.beans.factory.support.AbstractBeanDefinition#getInitMethodName() + */ +public interface InitializingBean { + + /** + * Invoked by the containing {@code BeanFactory} after it has set all bean properties + * and satisfied {@link BeanFactoryAware}, {@code ApplicationContextAware} etc. + *

This method allows the bean instance to perform validation of its overall + * configuration and final initialization when all bean properties have been set. + * @throws Exception in the event of misconfiguration (such as failure to set an + * essential property) or if initialization fails for any other reason + */ + void afterPropertiesSet() throws Exception; + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/InjectionPoint.java b/spring-beans/src/main/java/org/springframework/beans/factory/InjectionPoint.java new file mode 100644 index 0000000..13e7658 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/InjectionPoint.java @@ -0,0 +1,201 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Field; +import java.lang.reflect.Member; + +import org.springframework.core.MethodParameter; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * A simple descriptor for an injection point, pointing to a method/constructor + * parameter or a field. Exposed by {@link UnsatisfiedDependencyException}. + * Also available as an argument for factory methods, reacting to the + * requesting injection point for building a customized bean instance. + * + * @author Juergen Hoeller + * @since 4.3 + * @see UnsatisfiedDependencyException#getInjectionPoint() + * @see org.springframework.beans.factory.config.DependencyDescriptor + */ +public class InjectionPoint { + + @Nullable + protected MethodParameter methodParameter; + + @Nullable + protected Field field; + + @Nullable + private volatile Annotation[] fieldAnnotations; + + + /** + * Create an injection point descriptor for a method or constructor parameter. + * @param methodParameter the MethodParameter to wrap + */ + public InjectionPoint(MethodParameter methodParameter) { + Assert.notNull(methodParameter, "MethodParameter must not be null"); + this.methodParameter = methodParameter; + } + + /** + * Create an injection point descriptor for a field. + * @param field the field to wrap + */ + public InjectionPoint(Field field) { + Assert.notNull(field, "Field must not be null"); + this.field = field; + } + + /** + * Copy constructor. + * @param original the original descriptor to create a copy from + */ + protected InjectionPoint(InjectionPoint original) { + this.methodParameter = (original.methodParameter != null ? + new MethodParameter(original.methodParameter) : null); + this.field = original.field; + this.fieldAnnotations = original.fieldAnnotations; + } + + /** + * Just available for serialization purposes in subclasses. + */ + protected InjectionPoint() { + } + + + /** + * Return the wrapped MethodParameter, if any. + *

Note: Either MethodParameter or Field is available. + * @return the MethodParameter, or {@code null} if none + */ + @Nullable + public MethodParameter getMethodParameter() { + return this.methodParameter; + } + + /** + * Return the wrapped Field, if any. + *

Note: Either MethodParameter or Field is available. + * @return the Field, or {@code null} if none + */ + @Nullable + public Field getField() { + return this.field; + } + + /** + * Return the wrapped MethodParameter, assuming it is present. + * @return the MethodParameter (never {@code null}) + * @throws IllegalStateException if no MethodParameter is available + * @since 5.0 + */ + protected final MethodParameter obtainMethodParameter() { + Assert.state(this.methodParameter != null, "Neither Field nor MethodParameter"); + return this.methodParameter; + } + + /** + * Obtain the annotations associated with the wrapped field or method/constructor parameter. + */ + public Annotation[] getAnnotations() { + if (this.field != null) { + Annotation[] fieldAnnotations = this.fieldAnnotations; + if (fieldAnnotations == null) { + fieldAnnotations = this.field.getAnnotations(); + this.fieldAnnotations = fieldAnnotations; + } + return fieldAnnotations; + } + else { + return obtainMethodParameter().getParameterAnnotations(); + } + } + + /** + * Retrieve a field/parameter annotation of the given type, if any. + * @param annotationType the annotation type to retrieve + * @return the annotation instance, or {@code null} if none found + * @since 4.3.9 + */ + @Nullable + public A getAnnotation(Class annotationType) { + return (this.field != null ? this.field.getAnnotation(annotationType) : + obtainMethodParameter().getParameterAnnotation(annotationType)); + } + + /** + * Return the type declared by the underlying field or method/constructor parameter, + * indicating the injection type. + */ + public Class getDeclaredType() { + return (this.field != null ? this.field.getType() : obtainMethodParameter().getParameterType()); + } + + /** + * Returns the wrapped member, containing the injection point. + * @return the Field / Method / Constructor as Member + */ + public Member getMember() { + return (this.field != null ? this.field : obtainMethodParameter().getMember()); + } + + /** + * Return the wrapped annotated element. + *

Note: In case of a method/constructor parameter, this exposes + * the annotations declared on the method or constructor itself + * (i.e. at the method/constructor level, not at the parameter level). + * Use {@link #getAnnotations()} to obtain parameter-level annotations in + * such a scenario, transparently with corresponding field annotations. + * @return the Field / Method / Constructor as AnnotatedElement + */ + public AnnotatedElement getAnnotatedElement() { + return (this.field != null ? this.field : obtainMethodParameter().getAnnotatedElement()); + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + InjectionPoint otherPoint = (InjectionPoint) other; + return (ObjectUtils.nullSafeEquals(this.field, otherPoint.field) && + ObjectUtils.nullSafeEquals(this.methodParameter, otherPoint.methodParameter)); + } + + @Override + public int hashCode() { + return (this.field != null ? this.field.hashCode() : ObjectUtils.nullSafeHashCode(this.methodParameter)); + } + + @Override + public String toString() { + return (this.field != null ? "field '" + this.field.getName() + "'" : String.valueOf(this.methodParameter)); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/ListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/ListableBeanFactory.java new file mode 100644 index 0000000..389f19c --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/ListableBeanFactory.java @@ -0,0 +1,361 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +import java.lang.annotation.Annotation; +import java.util.Map; + +import org.springframework.beans.BeansException; +import org.springframework.core.ResolvableType; +import org.springframework.lang.Nullable; + +/** + * Extension of the {@link BeanFactory} interface to be implemented by bean factories + * that can enumerate all their bean instances, rather than attempting bean lookup + * by name one by one as requested by clients. BeanFactory implementations that + * preload all their bean definitions (such as XML-based factories) may implement + * this interface. + * + *

If this is a {@link HierarchicalBeanFactory}, the return values will not + * take any BeanFactory hierarchy into account, but will relate only to the beans + * defined in the current factory. Use the {@link BeanFactoryUtils} helper class + * to consider beans in ancestor factories too. + * + *

The methods in this interface will just respect bean definitions of this factory. + * They will ignore any singleton beans that have been registered by other means like + * {@link org.springframework.beans.factory.config.ConfigurableBeanFactory}'s + * {@code registerSingleton} method, with the exception of + * {@code getBeanNamesForType} and {@code getBeansOfType} which will check + * such manually registered singletons too. Of course, BeanFactory's {@code getBean} + * does allow transparent access to such special beans as well. However, in typical + * scenarios, all beans will be defined by external bean definitions anyway, so most + * applications don't need to worry about this differentiation. + * + *

NOTE: With the exception of {@code getBeanDefinitionCount} + * and {@code containsBeanDefinition}, the methods in this interface + * are not designed for frequent invocation. Implementations may be slow. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 16 April 2001 + * @see HierarchicalBeanFactory + * @see BeanFactoryUtils + */ +public interface ListableBeanFactory extends BeanFactory { + + /** + * Check if this bean factory contains a bean definition with the given name. + *

Does not consider any hierarchy this factory may participate in, + * and ignores any singleton beans that have been registered by + * other means than bean definitions. + * @param beanName the name of the bean to look for + * @return if this bean factory contains a bean definition with the given name + * @see #containsBean + */ + boolean containsBeanDefinition(String beanName); + + /** + * Return the number of beans defined in the factory. + *

Does not consider any hierarchy this factory may participate in, + * and ignores any singleton beans that have been registered by + * other means than bean definitions. + * @return the number of beans defined in the factory + */ + int getBeanDefinitionCount(); + + /** + * Return the names of all beans defined in this factory. + *

Does not consider any hierarchy this factory may participate in, + * and ignores any singleton beans that have been registered by + * other means than bean definitions. + * @return the names of all beans defined in this factory, + * or an empty array if none defined + */ + String[] getBeanDefinitionNames(); + + /** + * Return a provider for the specified bean, allowing for lazy on-demand retrieval + * of instances, including availability and uniqueness options. + * @param requiredType type the bean must match; can be an interface or superclass + * @param allowEagerInit whether stream-based access may initialize lazy-init + * singletons and objects created by FactoryBeans (or by factory methods + * with a "factory-bean" reference) for the type check + * @return a corresponding provider handle + * @since 5.3 + * @see #getBeanProvider(ResolvableType, boolean) + * @see #getBeanProvider(Class) + * @see #getBeansOfType(Class, boolean, boolean) + * @see #getBeanNamesForType(Class, boolean, boolean) + */ + ObjectProvider getBeanProvider(Class requiredType, boolean allowEagerInit); + + /** + * Return a provider for the specified bean, allowing for lazy on-demand retrieval + * of instances, including availability and uniqueness options. + * @param requiredType type the bean must match; can be a generic type declaration. + * Note that collection types are not supported here, in contrast to reflective + * injection points. For programmatically retrieving a list of beans matching a + * specific type, specify the actual bean type as an argument here and subsequently + * use {@link ObjectProvider#orderedStream()} or its lazy streaming/iteration options. + * @param allowEagerInit whether stream-based access may initialize lazy-init + * singletons and objects created by FactoryBeans (or by factory methods + * with a "factory-bean" reference) for the type check + * @return a corresponding provider handle + * @since 5.3 + * @see #getBeanProvider(ResolvableType) + * @see ObjectProvider#iterator() + * @see ObjectProvider#stream() + * @see ObjectProvider#orderedStream() + * @see #getBeanNamesForType(ResolvableType, boolean, boolean) + */ + ObjectProvider getBeanProvider(ResolvableType requiredType, boolean allowEagerInit); + + /** + * Return the names of beans matching the given type (including subclasses), + * judging from either bean definitions or the value of {@code getObjectType} + * in the case of FactoryBeans. + *

NOTE: This method introspects top-level beans only. It does not + * check nested beans which might match the specified type as well. + *

Does consider objects created by FactoryBeans, which means that FactoryBeans + * will get initialized. If the object created by the FactoryBean doesn't match, + * the raw FactoryBean itself will be matched against the type. + *

Does not consider any hierarchy this factory may participate in. + * Use BeanFactoryUtils' {@code beanNamesForTypeIncludingAncestors} + * to include beans in ancestor factories too. + *

Note: Does not ignore singleton beans that have been registered + * by other means than bean definitions. + *

This version of {@code getBeanNamesForType} matches all kinds of beans, + * be it singletons, prototypes, or FactoryBeans. In most implementations, the + * result will be the same as for {@code getBeanNamesForType(type, true, true)}. + *

Bean names returned by this method should always return bean names in the + * order of definition in the backend configuration, as far as possible. + * @param type the generically typed class or interface to match + * @return the names of beans (or objects created by FactoryBeans) matching + * the given object type (including subclasses), or an empty array if none + * @since 4.2 + * @see #isTypeMatch(String, ResolvableType) + * @see FactoryBean#getObjectType + * @see BeanFactoryUtils#beanNamesForTypeIncludingAncestors(ListableBeanFactory, ResolvableType) + */ + String[] getBeanNamesForType(ResolvableType type); + + /** + * Return the names of beans matching the given type (including subclasses), + * judging from either bean definitions or the value of {@code getObjectType} + * in the case of FactoryBeans. + *

NOTE: This method introspects top-level beans only. It does not + * check nested beans which might match the specified type as well. + *

Does consider objects created by FactoryBeans if the "allowEagerInit" flag is set, + * which means that FactoryBeans will get initialized. If the object created by the + * FactoryBean doesn't match, the raw FactoryBean itself will be matched against the + * type. If "allowEagerInit" is not set, only raw FactoryBeans will be checked + * (which doesn't require initialization of each FactoryBean). + *

Does not consider any hierarchy this factory may participate in. + * Use BeanFactoryUtils' {@code beanNamesForTypeIncludingAncestors} + * to include beans in ancestor factories too. + *

Note: Does not ignore singleton beans that have been registered + * by other means than bean definitions. + *

Bean names returned by this method should always return bean names in the + * order of definition in the backend configuration, as far as possible. + * @param type the generically typed class or interface to match + * @param includeNonSingletons whether to include prototype or scoped beans too + * or just singletons (also applies to FactoryBeans) + * @param allowEagerInit whether to initialize lazy-init singletons and + * objects created by FactoryBeans (or by factory methods with a + * "factory-bean" reference) for the type check. Note that FactoryBeans need to be + * eagerly initialized to determine their type: So be aware that passing in "true" + * for this flag will initialize FactoryBeans and "factory-bean" references. + * @return the names of beans (or objects created by FactoryBeans) matching + * the given object type (including subclasses), or an empty array if none + * @since 5.2 + * @see FactoryBean#getObjectType + * @see BeanFactoryUtils#beanNamesForTypeIncludingAncestors(ListableBeanFactory, ResolvableType, boolean, boolean) + */ + String[] getBeanNamesForType(ResolvableType type, boolean includeNonSingletons, boolean allowEagerInit); + + /** + * Return the names of beans matching the given type (including subclasses), + * judging from either bean definitions or the value of {@code getObjectType} + * in the case of FactoryBeans. + *

NOTE: This method introspects top-level beans only. It does not + * check nested beans which might match the specified type as well. + *

Does consider objects created by FactoryBeans, which means that FactoryBeans + * will get initialized. If the object created by the FactoryBean doesn't match, + * the raw FactoryBean itself will be matched against the type. + *

Does not consider any hierarchy this factory may participate in. + * Use BeanFactoryUtils' {@code beanNamesForTypeIncludingAncestors} + * to include beans in ancestor factories too. + *

Note: Does not ignore singleton beans that have been registered + * by other means than bean definitions. + *

This version of {@code getBeanNamesForType} matches all kinds of beans, + * be it singletons, prototypes, or FactoryBeans. In most implementations, the + * result will be the same as for {@code getBeanNamesForType(type, true, true)}. + *

Bean names returned by this method should always return bean names in the + * order of definition in the backend configuration, as far as possible. + * @param type the class or interface to match, or {@code null} for all bean names + * @return the names of beans (or objects created by FactoryBeans) matching + * the given object type (including subclasses), or an empty array if none + * @see FactoryBean#getObjectType + * @see BeanFactoryUtils#beanNamesForTypeIncludingAncestors(ListableBeanFactory, Class) + */ + String[] getBeanNamesForType(@Nullable Class type); + + /** + * Return the names of beans matching the given type (including subclasses), + * judging from either bean definitions or the value of {@code getObjectType} + * in the case of FactoryBeans. + *

NOTE: This method introspects top-level beans only. It does not + * check nested beans which might match the specified type as well. + *

Does consider objects created by FactoryBeans if the "allowEagerInit" flag is set, + * which means that FactoryBeans will get initialized. If the object created by the + * FactoryBean doesn't match, the raw FactoryBean itself will be matched against the + * type. If "allowEagerInit" is not set, only raw FactoryBeans will be checked + * (which doesn't require initialization of each FactoryBean). + *

Does not consider any hierarchy this factory may participate in. + * Use BeanFactoryUtils' {@code beanNamesForTypeIncludingAncestors} + * to include beans in ancestor factories too. + *

Note: Does not ignore singleton beans that have been registered + * by other means than bean definitions. + *

Bean names returned by this method should always return bean names in the + * order of definition in the backend configuration, as far as possible. + * @param type the class or interface to match, or {@code null} for all bean names + * @param includeNonSingletons whether to include prototype or scoped beans too + * or just singletons (also applies to FactoryBeans) + * @param allowEagerInit whether to initialize lazy-init singletons and + * objects created by FactoryBeans (or by factory methods with a + * "factory-bean" reference) for the type check. Note that FactoryBeans need to be + * eagerly initialized to determine their type: So be aware that passing in "true" + * for this flag will initialize FactoryBeans and "factory-bean" references. + * @return the names of beans (or objects created by FactoryBeans) matching + * the given object type (including subclasses), or an empty array if none + * @see FactoryBean#getObjectType + * @see BeanFactoryUtils#beanNamesForTypeIncludingAncestors(ListableBeanFactory, Class, boolean, boolean) + */ + String[] getBeanNamesForType(@Nullable Class type, boolean includeNonSingletons, boolean allowEagerInit); + + /** + * Return the bean instances that match the given object type (including + * subclasses), judging from either bean definitions or the value of + * {@code getObjectType} in the case of FactoryBeans. + *

NOTE: This method introspects top-level beans only. It does not + * check nested beans which might match the specified type as well. + *

Does consider objects created by FactoryBeans, which means that FactoryBeans + * will get initialized. If the object created by the FactoryBean doesn't match, + * the raw FactoryBean itself will be matched against the type. + *

Does not consider any hierarchy this factory may participate in. + * Use BeanFactoryUtils' {@code beansOfTypeIncludingAncestors} + * to include beans in ancestor factories too. + *

Note: Does not ignore singleton beans that have been registered + * by other means than bean definitions. + *

This version of getBeansOfType matches all kinds of beans, be it + * singletons, prototypes, or FactoryBeans. In most implementations, the + * result will be the same as for {@code getBeansOfType(type, true, true)}. + *

The Map returned by this method should always return bean names and + * corresponding bean instances in the order of definition in the + * backend configuration, as far as possible. + * @param type the class or interface to match, or {@code null} for all concrete beans + * @return a Map with the matching beans, containing the bean names as + * keys and the corresponding bean instances as values + * @throws BeansException if a bean could not be created + * @since 1.1.2 + * @see FactoryBean#getObjectType + * @see BeanFactoryUtils#beansOfTypeIncludingAncestors(ListableBeanFactory, Class) + */ + Map getBeansOfType(@Nullable Class type) throws BeansException; + + /** + * Return the bean instances that match the given object type (including + * subclasses), judging from either bean definitions or the value of + * {@code getObjectType} in the case of FactoryBeans. + *

NOTE: This method introspects top-level beans only. It does not + * check nested beans which might match the specified type as well. + *

Does consider objects created by FactoryBeans if the "allowEagerInit" flag is set, + * which means that FactoryBeans will get initialized. If the object created by the + * FactoryBean doesn't match, the raw FactoryBean itself will be matched against the + * type. If "allowEagerInit" is not set, only raw FactoryBeans will be checked + * (which doesn't require initialization of each FactoryBean). + *

Does not consider any hierarchy this factory may participate in. + * Use BeanFactoryUtils' {@code beansOfTypeIncludingAncestors} + * to include beans in ancestor factories too. + *

Note: Does not ignore singleton beans that have been registered + * by other means than bean definitions. + *

The Map returned by this method should always return bean names and + * corresponding bean instances in the order of definition in the + * backend configuration, as far as possible. + * @param type the class or interface to match, or {@code null} for all concrete beans + * @param includeNonSingletons whether to include prototype or scoped beans too + * or just singletons (also applies to FactoryBeans) + * @param allowEagerInit whether to initialize lazy-init singletons and + * objects created by FactoryBeans (or by factory methods with a + * "factory-bean" reference) for the type check. Note that FactoryBeans need to be + * eagerly initialized to determine their type: So be aware that passing in "true" + * for this flag will initialize FactoryBeans and "factory-bean" references. + * @return a Map with the matching beans, containing the bean names as + * keys and the corresponding bean instances as values + * @throws BeansException if a bean could not be created + * @see FactoryBean#getObjectType + * @see BeanFactoryUtils#beansOfTypeIncludingAncestors(ListableBeanFactory, Class, boolean, boolean) + */ + Map getBeansOfType(@Nullable Class type, boolean includeNonSingletons, boolean allowEagerInit) + throws BeansException; + + /** + * Find all names of beans which are annotated with the supplied {@link Annotation} + * type, without creating corresponding bean instances yet. + *

Note that this method considers objects created by FactoryBeans, which means + * that FactoryBeans will get initialized in order to determine their object type. + * @param annotationType the type of annotation to look for + * (at class, interface or factory method level of the specified bean) + * @return the names of all matching beans + * @since 4.0 + * @see #findAnnotationOnBean + */ + String[] getBeanNamesForAnnotation(Class annotationType); + + /** + * Find all beans which are annotated with the supplied {@link Annotation} type, + * returning a Map of bean names with corresponding bean instances. + *

Note that this method considers objects created by FactoryBeans, which means + * that FactoryBeans will get initialized in order to determine their object type. + * @param annotationType the type of annotation to look for + * (at class, interface or factory method level of the specified bean) + * @return a Map with the matching beans, containing the bean names as + * keys and the corresponding bean instances as values + * @throws BeansException if a bean could not be created + * @since 3.0 + * @see #findAnnotationOnBean + */ + Map getBeansWithAnnotation(Class annotationType) throws BeansException; + + /** + * Find an {@link Annotation} of {@code annotationType} on the specified bean, + * traversing its interfaces and super classes if no annotation can be found on + * the given class itself, as well as checking the bean's factory method (if any). + * @param beanName the name of the bean to look for annotations on + * @param annotationType the type of annotation to look for + * (at class, interface or factory method level of the specified bean) + * @return the annotation of the given type if found, or {@code null} otherwise + * @throws NoSuchBeanDefinitionException if there is no bean with the given name + * @since 3.0 + * @see #getBeanNamesForAnnotation + * @see #getBeansWithAnnotation + */ + @Nullable + A findAnnotationOnBean(String beanName, Class annotationType) + throws NoSuchBeanDefinitionException; + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/NamedBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/NamedBean.java new file mode 100644 index 0000000..b3ac111 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/NamedBean.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +/** + * Counterpart of {@link BeanNameAware}. Returns the bean name of an object. + * + *

This interface can be introduced to avoid a brittle dependence on + * bean name in objects used with Spring IoC and Spring AOP. + * + * @author Rod Johnson + * @since 2.0 + * @see BeanNameAware + */ +public interface NamedBean { + + /** + * Return the name of this bean in a Spring bean factory, if known. + */ + String getBeanName(); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/NoSuchBeanDefinitionException.java b/spring-beans/src/main/java/org/springframework/beans/factory/NoSuchBeanDefinitionException.java new file mode 100644 index 0000000..595b40a --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/NoSuchBeanDefinitionException.java @@ -0,0 +1,143 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +import org.springframework.beans.BeansException; +import org.springframework.core.ResolvableType; +import org.springframework.lang.Nullable; + +/** + * Exception thrown when a {@code BeanFactory} is asked for a bean instance for which it + * cannot find a definition. This may point to a non-existing bean, a non-unique bean, + * or a manually registered singleton instance without an associated bean definition. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Stephane Nicoll + * @see BeanFactory#getBean(String) + * @see BeanFactory#getBean(Class) + * @see NoUniqueBeanDefinitionException + */ +@SuppressWarnings("serial") +public class NoSuchBeanDefinitionException extends BeansException { + + @Nullable + private final String beanName; + + @Nullable + private final ResolvableType resolvableType; + + + /** + * Create a new {@code NoSuchBeanDefinitionException}. + * @param name the name of the missing bean + */ + public NoSuchBeanDefinitionException(String name) { + super("No bean named '" + name + "' available"); + this.beanName = name; + this.resolvableType = null; + } + + /** + * Create a new {@code NoSuchBeanDefinitionException}. + * @param name the name of the missing bean + * @param message detailed message describing the problem + */ + public NoSuchBeanDefinitionException(String name, String message) { + super("No bean named '" + name + "' available: " + message); + this.beanName = name; + this.resolvableType = null; + } + + /** + * Create a new {@code NoSuchBeanDefinitionException}. + * @param type required type of the missing bean + */ + public NoSuchBeanDefinitionException(Class type) { + this(ResolvableType.forClass(type)); + } + + /** + * Create a new {@code NoSuchBeanDefinitionException}. + * @param type required type of the missing bean + * @param message detailed message describing the problem + */ + public NoSuchBeanDefinitionException(Class type, String message) { + this(ResolvableType.forClass(type), message); + } + + /** + * Create a new {@code NoSuchBeanDefinitionException}. + * @param type full type declaration of the missing bean + * @since 4.3.4 + */ + public NoSuchBeanDefinitionException(ResolvableType type) { + super("No qualifying bean of type '" + type + "' available"); + this.beanName = null; + this.resolvableType = type; + } + + /** + * Create a new {@code NoSuchBeanDefinitionException}. + * @param type full type declaration of the missing bean + * @param message detailed message describing the problem + * @since 4.3.4 + */ + public NoSuchBeanDefinitionException(ResolvableType type, String message) { + super("No qualifying bean of type '" + type + "' available: " + message); + this.beanName = null; + this.resolvableType = type; + } + + + /** + * Return the name of the missing bean, if it was a lookup by name that failed. + */ + @Nullable + public String getBeanName() { + return this.beanName; + } + + /** + * Return the required type of the missing bean, if it was a lookup by type + * that failed. + */ + @Nullable + public Class getBeanType() { + return (this.resolvableType != null ? this.resolvableType.resolve() : null); + } + + /** + * Return the required {@link ResolvableType} of the missing bean, if it was a lookup + * by type that failed. + * @since 4.3.4 + */ + @Nullable + public ResolvableType getResolvableType() { + return this.resolvableType; + } + + /** + * Return the number of beans found when only one matching bean was expected. + * For a regular NoSuchBeanDefinitionException, this will always be 0. + * @see NoUniqueBeanDefinitionException + */ + public int getNumberOfBeansFound() { + return 0; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/NoUniqueBeanDefinitionException.java b/spring-beans/src/main/java/org/springframework/beans/factory/NoUniqueBeanDefinitionException.java new file mode 100644 index 0000000..5744a73 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/NoUniqueBeanDefinitionException.java @@ -0,0 +1,121 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +import java.util.Arrays; +import java.util.Collection; + +import org.springframework.core.ResolvableType; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Exception thrown when a {@code BeanFactory} is asked for a bean instance for which + * multiple matching candidates have been found when only one matching bean was expected. + * + * @author Juergen Hoeller + * @since 3.2.1 + * @see BeanFactory#getBean(Class) + */ +@SuppressWarnings("serial") +public class NoUniqueBeanDefinitionException extends NoSuchBeanDefinitionException { + + private final int numberOfBeansFound; + + @Nullable + private final Collection beanNamesFound; + + + /** + * Create a new {@code NoUniqueBeanDefinitionException}. + * @param type required type of the non-unique bean + * @param numberOfBeansFound the number of matching beans + * @param message detailed message describing the problem + */ + public NoUniqueBeanDefinitionException(Class type, int numberOfBeansFound, String message) { + super(type, message); + this.numberOfBeansFound = numberOfBeansFound; + this.beanNamesFound = null; + } + + /** + * Create a new {@code NoUniqueBeanDefinitionException}. + * @param type required type of the non-unique bean + * @param beanNamesFound the names of all matching beans (as a Collection) + */ + public NoUniqueBeanDefinitionException(Class type, Collection beanNamesFound) { + super(type, "expected single matching bean but found " + beanNamesFound.size() + ": " + + StringUtils.collectionToCommaDelimitedString(beanNamesFound)); + this.numberOfBeansFound = beanNamesFound.size(); + this.beanNamesFound = beanNamesFound; + } + + /** + * Create a new {@code NoUniqueBeanDefinitionException}. + * @param type required type of the non-unique bean + * @param beanNamesFound the names of all matching beans (as an array) + */ + public NoUniqueBeanDefinitionException(Class type, String... beanNamesFound) { + this(type, Arrays.asList(beanNamesFound)); + } + + /** + * Create a new {@code NoUniqueBeanDefinitionException}. + * @param type required type of the non-unique bean + * @param beanNamesFound the names of all matching beans (as a Collection) + * @since 5.1 + */ + public NoUniqueBeanDefinitionException(ResolvableType type, Collection beanNamesFound) { + super(type, "expected single matching bean but found " + beanNamesFound.size() + ": " + + StringUtils.collectionToCommaDelimitedString(beanNamesFound)); + this.numberOfBeansFound = beanNamesFound.size(); + this.beanNamesFound = beanNamesFound; + } + + /** + * Create a new {@code NoUniqueBeanDefinitionException}. + * @param type required type of the non-unique bean + * @param beanNamesFound the names of all matching beans (as an array) + * @since 5.1 + */ + public NoUniqueBeanDefinitionException(ResolvableType type, String... beanNamesFound) { + this(type, Arrays.asList(beanNamesFound)); + } + + + /** + * Return the number of beans found when only one matching bean was expected. + * For a NoUniqueBeanDefinitionException, this will usually be higher than 1. + * @see #getBeanType() + */ + @Override + public int getNumberOfBeansFound() { + return this.numberOfBeansFound; + } + + /** + * Return the names of all beans found when only one matching bean was expected. + * Note that this may be {@code null} if not specified at construction time. + * @since 4.3 + * @see #getBeanType() + */ + @Nullable + public Collection getBeanNamesFound() { + return this.beanNamesFound; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/ObjectFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/ObjectFactory.java new file mode 100644 index 0000000..9ac3ed4 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/ObjectFactory.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +import org.springframework.beans.BeansException; + +/** + * Defines a factory which can return an Object instance + * (possibly shared or independent) when invoked. + * + *

This interface is typically used to encapsulate a generic factory which + * returns a new instance (prototype) of some target object on each invocation. + * + *

This interface is similar to {@link FactoryBean}, but implementations + * of the latter are normally meant to be defined as SPI instances in a + * {@link BeanFactory}, while implementations of this class are normally meant + * to be fed as an API to other beans (through injection). As such, the + * {@code getObject()} method has different exception handling behavior. + * + * @author Colin Sampaleanu + * @since 1.0.2 + * @param the object type + * @see FactoryBean + */ +@FunctionalInterface +public interface ObjectFactory { + + /** + * Return an instance (possibly shared or independent) + * of the object managed by this factory. + * @return the resulting instance + * @throws BeansException in case of creation errors + */ + T getObject() throws BeansException; + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/ObjectProvider.java b/spring-beans/src/main/java/org/springframework/beans/factory/ObjectProvider.java new file mode 100644 index 0000000..a9dc61e --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/ObjectProvider.java @@ -0,0 +1,179 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +import java.util.Iterator; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.springframework.beans.BeansException; +import org.springframework.lang.Nullable; + +/** + * A variant of {@link ObjectFactory} designed specifically for injection points, + * allowing for programmatic optionality and lenient not-unique handling. + * + *

As of 5.1, this interface extends {@link Iterable} and provides {@link Stream} + * support. It can be therefore be used in {@code for} loops, provides {@link #forEach} + * iteration and allows for collection-style {@link #stream} access. + * + * @author Juergen Hoeller + * @since 4.3 + * @param the object type + * @see BeanFactory#getBeanProvider + * @see org.springframework.beans.factory.annotation.Autowired + */ +public interface ObjectProvider extends ObjectFactory, Iterable { + + /** + * Return an instance (possibly shared or independent) of the object + * managed by this factory. + *

Allows for specifying explicit construction arguments, along the + * lines of {@link BeanFactory#getBean(String, Object...)}. + * @param args arguments to use when creating a corresponding instance + * @return an instance of the bean + * @throws BeansException in case of creation errors + * @see #getObject() + */ + T getObject(Object... args) throws BeansException; + + /** + * Return an instance (possibly shared or independent) of the object + * managed by this factory. + * @return an instance of the bean, or {@code null} if not available + * @throws BeansException in case of creation errors + * @see #getObject() + */ + @Nullable + T getIfAvailable() throws BeansException; + + /** + * Return an instance (possibly shared or independent) of the object + * managed by this factory. + * @param defaultSupplier a callback for supplying a default object + * if none is present in the factory + * @return an instance of the bean, or the supplied default object + * if no such bean is available + * @throws BeansException in case of creation errors + * @since 5.0 + * @see #getIfAvailable() + */ + default T getIfAvailable(Supplier defaultSupplier) throws BeansException { + T dependency = getIfAvailable(); + return (dependency != null ? dependency : defaultSupplier.get()); + } + + /** + * Consume an instance (possibly shared or independent) of the object + * managed by this factory, if available. + * @param dependencyConsumer a callback for processing the target object + * if available (not called otherwise) + * @throws BeansException in case of creation errors + * @since 5.0 + * @see #getIfAvailable() + */ + default void ifAvailable(Consumer dependencyConsumer) throws BeansException { + T dependency = getIfAvailable(); + if (dependency != null) { + dependencyConsumer.accept(dependency); + } + } + + /** + * Return an instance (possibly shared or independent) of the object + * managed by this factory. + * @return an instance of the bean, or {@code null} if not available or + * not unique (i.e. multiple candidates found with none marked as primary) + * @throws BeansException in case of creation errors + * @see #getObject() + */ + @Nullable + T getIfUnique() throws BeansException; + + /** + * Return an instance (possibly shared or independent) of the object + * managed by this factory. + * @param defaultSupplier a callback for supplying a default object + * if no unique candidate is present in the factory + * @return an instance of the bean, or the supplied default object + * if no such bean is available or if it is not unique in the factory + * (i.e. multiple candidates found with none marked as primary) + * @throws BeansException in case of creation errors + * @since 5.0 + * @see #getIfUnique() + */ + default T getIfUnique(Supplier defaultSupplier) throws BeansException { + T dependency = getIfUnique(); + return (dependency != null ? dependency : defaultSupplier.get()); + } + + /** + * Consume an instance (possibly shared or independent) of the object + * managed by this factory, if unique. + * @param dependencyConsumer a callback for processing the target object + * if unique (not called otherwise) + * @throws BeansException in case of creation errors + * @since 5.0 + * @see #getIfAvailable() + */ + default void ifUnique(Consumer dependencyConsumer) throws BeansException { + T dependency = getIfUnique(); + if (dependency != null) { + dependencyConsumer.accept(dependency); + } + } + + /** + * Return an {@link Iterator} over all matching object instances, + * without specific ordering guarantees (but typically in registration order). + * @since 5.1 + * @see #stream() + */ + @Override + default Iterator iterator() { + return stream().iterator(); + } + + /** + * Return a sequential {@link Stream} over all matching object instances, + * without specific ordering guarantees (but typically in registration order). + * @since 5.1 + * @see #iterator() + * @see #orderedStream() + */ + default Stream stream() { + throw new UnsupportedOperationException("Multi element access not supported"); + } + + /** + * Return a sequential {@link Stream} over all matching object instances, + * pre-ordered according to the factory's common order comparator. + *

In a standard Spring application context, this will be ordered + * according to {@link org.springframework.core.Ordered} conventions, + * and in case of annotation-based configuration also considering the + * {@link org.springframework.core.annotation.Order} annotation, + * analogous to multi-element injection points of list/array type. + * @since 5.1 + * @see #stream() + * @see org.springframework.core.OrderComparator + */ + default Stream orderedStream() { + throw new UnsupportedOperationException("Ordered element access not supported"); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/SmartFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/SmartFactoryBean.java new file mode 100644 index 0000000..117f843 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/SmartFactoryBean.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +/** + * Extension of the {@link FactoryBean} interface. Implementations may + * indicate whether they always return independent instances, for the + * case where their {@link #isSingleton()} implementation returning + * {@code false} does not clearly indicate independent instances. + * + *

Plain {@link FactoryBean} implementations which do not implement + * this extended interface are simply assumed to always return independent + * instances if their {@link #isSingleton()} implementation returns + * {@code false}; the exposed object is only accessed on demand. + * + *

NOTE: This interface is a special purpose interface, mainly for + * internal use within the framework and within collaborating frameworks. + * In general, application-provided FactoryBeans should simply implement + * the plain {@link FactoryBean} interface. New methods might be added + * to this extended interface even in point releases. + * + * @author Juergen Hoeller + * @since 2.0.3 + * @param the bean type + * @see #isPrototype() + * @see #isSingleton() + */ +public interface SmartFactoryBean extends FactoryBean { + + /** + * Is the object managed by this factory a prototype? That is, + * will {@link #getObject()} always return an independent instance? + *

The prototype status of the FactoryBean itself will generally + * be provided by the owning {@link BeanFactory}; usually, it has to be + * defined as singleton there. + *

This method is supposed to strictly check for independent instances; + * it should not return {@code true} for scoped objects or other + * kinds of non-singleton, non-independent objects. For this reason, + * this is not simply the inverted form of {@link #isSingleton()}. + *

The default implementation returns {@code false}. + * @return whether the exposed object is a prototype + * @see #getObject() + * @see #isSingleton() + */ + default boolean isPrototype() { + return false; + } + + /** + * Does this FactoryBean expect eager initialization, that is, + * eagerly initialize itself as well as expect eager initialization + * of its singleton object (if any)? + *

A standard FactoryBean is not expected to initialize eagerly: + * Its {@link #getObject()} will only be called for actual access, even + * in case of a singleton object. Returning {@code true} from this + * method suggests that {@link #getObject()} should be called eagerly, + * also applying post-processors eagerly. This may make sense in case + * of a {@link #isSingleton() singleton} object, in particular if + * post-processors expect to be applied on startup. + *

The default implementation returns {@code false}. + * @return whether eager initialization applies + * @see org.springframework.beans.factory.config.ConfigurableListableBeanFactory#preInstantiateSingletons() + */ + default boolean isEagerInit() { + return false; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/SmartInitializingSingleton.java b/spring-beans/src/main/java/org/springframework/beans/factory/SmartInitializingSingleton.java new file mode 100644 index 0000000..3df6363 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/SmartInitializingSingleton.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +/** + * Callback interface triggered at the end of the singleton pre-instantiation phase + * during {@link BeanFactory} bootstrap. This interface can be implemented by + * singleton beans in order to perform some initialization after the regular + * singleton instantiation algorithm, avoiding side effects with accidental early + * initialization (e.g. from {@link ListableBeanFactory#getBeansOfType} calls). + * In that sense, it is an alternative to {@link InitializingBean} which gets + * triggered right at the end of a bean's local construction phase. + * + *

This callback variant is somewhat similar to + * {@link org.springframework.context.event.ContextRefreshedEvent} but doesn't + * require an implementation of {@link org.springframework.context.ApplicationListener}, + * with no need to filter context references across a context hierarchy etc. + * It also implies a more minimal dependency on just the {@code beans} package + * and is being honored by standalone {@link ListableBeanFactory} implementations, + * not just in an {@link org.springframework.context.ApplicationContext} environment. + * + *

NOTE: If you intend to start/manage asynchronous tasks, preferably + * implement {@link org.springframework.context.Lifecycle} instead which offers + * a richer model for runtime management and allows for phased startup/shutdown. + * + * @author Juergen Hoeller + * @since 4.1 + * @see org.springframework.beans.factory.config.ConfigurableListableBeanFactory#preInstantiateSingletons() + */ +public interface SmartInitializingSingleton { + + /** + * Invoked right at the end of the singleton pre-instantiation phase, + * with a guarantee that all regular singleton beans have been created + * already. {@link ListableBeanFactory#getBeansOfType} calls within + * this method won't trigger accidental side effects during bootstrap. + *

NOTE: This callback won't be triggered for singleton beans + * lazily initialized on demand after {@link BeanFactory} bootstrap, + * and not for any other bean scope either. Carefully use it for beans + * with the intended bootstrap semantics only. + */ + void afterSingletonsInstantiated(); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/UnsatisfiedDependencyException.java b/spring-beans/src/main/java/org/springframework/beans/factory/UnsatisfiedDependencyException.java new file mode 100644 index 0000000..8ae820d --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/UnsatisfiedDependencyException.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +import org.springframework.beans.BeansException; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Exception thrown when a bean depends on other beans or simple properties + * that were not specified in the bean factory definition, although + * dependency checking was enabled. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 03.09.2003 + */ +@SuppressWarnings("serial") +public class UnsatisfiedDependencyException extends BeanCreationException { + + @Nullable + private final InjectionPoint injectionPoint; + + + /** + * Create a new UnsatisfiedDependencyException. + * @param resourceDescription description of the resource that the bean definition came from + * @param beanName the name of the bean requested + * @param propertyName the name of the bean property that couldn't be satisfied + * @param msg the detail message + */ + public UnsatisfiedDependencyException( + @Nullable String resourceDescription, @Nullable String beanName, String propertyName, String msg) { + + super(resourceDescription, beanName, + "Unsatisfied dependency expressed through bean property '" + propertyName + "'" + + (StringUtils.hasLength(msg) ? ": " + msg : "")); + this.injectionPoint = null; + } + + /** + * Create a new UnsatisfiedDependencyException. + * @param resourceDescription description of the resource that the bean definition came from + * @param beanName the name of the bean requested + * @param propertyName the name of the bean property that couldn't be satisfied + * @param ex the bean creation exception that indicated the unsatisfied dependency + */ + public UnsatisfiedDependencyException( + @Nullable String resourceDescription, @Nullable String beanName, String propertyName, BeansException ex) { + + this(resourceDescription, beanName, propertyName, ""); + initCause(ex); + } + + /** + * Create a new UnsatisfiedDependencyException. + * @param resourceDescription description of the resource that the bean definition came from + * @param beanName the name of the bean requested + * @param injectionPoint the injection point (field or method/constructor parameter) + * @param msg the detail message + * @since 4.3 + */ + public UnsatisfiedDependencyException( + @Nullable String resourceDescription, @Nullable String beanName, @Nullable InjectionPoint injectionPoint, String msg) { + + super(resourceDescription, beanName, + "Unsatisfied dependency expressed through " + injectionPoint + + (StringUtils.hasLength(msg) ? ": " + msg : "")); + this.injectionPoint = injectionPoint; + } + + /** + * Create a new UnsatisfiedDependencyException. + * @param resourceDescription description of the resource that the bean definition came from + * @param beanName the name of the bean requested + * @param injectionPoint the injection point (field or method/constructor parameter) + * @param ex the bean creation exception that indicated the unsatisfied dependency + * @since 4.3 + */ + public UnsatisfiedDependencyException( + @Nullable String resourceDescription, @Nullable String beanName, @Nullable InjectionPoint injectionPoint, BeansException ex) { + + this(resourceDescription, beanName, injectionPoint, ""); + initCause(ex); + } + + + /** + * Return the injection point (field or method/constructor parameter), if known. + * @since 4.3 + */ + @Nullable + public InjectionPoint getInjectionPoint() { + return this.injectionPoint; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AnnotatedBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AnnotatedBeanDefinition.java new file mode 100644 index 0000000..7d3fc76 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AnnotatedBeanDefinition.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.annotation; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.MethodMetadata; +import org.springframework.lang.Nullable; + +/** + * Extended {@link org.springframework.beans.factory.config.BeanDefinition} + * interface that exposes {@link org.springframework.core.type.AnnotationMetadata} + * about its bean class - without requiring the class to be loaded yet. + * + * @author Juergen Hoeller + * @since 2.5 + * @see AnnotatedGenericBeanDefinition + * @see org.springframework.core.type.AnnotationMetadata + */ +public interface AnnotatedBeanDefinition extends BeanDefinition { + + /** + * Obtain the annotation metadata (as well as basic class metadata) + * for this bean definition's bean class. + * @return the annotation metadata object (never {@code null}) + */ + AnnotationMetadata getMetadata(); + + /** + * Obtain metadata for this bean definition's factory method, if any. + * @return the factory method metadata, or {@code null} if none + * @since 4.1.1 + */ + @Nullable + MethodMetadata getFactoryMethodMetadata(); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AnnotatedGenericBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AnnotatedGenericBeanDefinition.java new file mode 100644 index 0000000..c0cf85a --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AnnotatedGenericBeanDefinition.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.annotation; + +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.MethodMetadata; +import org.springframework.core.type.StandardAnnotationMetadata; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Extension of the {@link org.springframework.beans.factory.support.GenericBeanDefinition} + * class, adding support for annotation metadata exposed through the + * {@link AnnotatedBeanDefinition} interface. + * + *

This GenericBeanDefinition variant is mainly useful for testing code that expects + * to operate on an AnnotatedBeanDefinition, for example strategy implementations + * in Spring's component scanning support (where the default definition class is + * {@link org.springframework.context.annotation.ScannedGenericBeanDefinition}, + * which also implements the AnnotatedBeanDefinition interface). + * + * @author Juergen Hoeller + * @author Chris Beams + * @since 2.5 + * @see AnnotatedBeanDefinition#getMetadata() + * @see org.springframework.core.type.StandardAnnotationMetadata + */ +@SuppressWarnings("serial") +public class AnnotatedGenericBeanDefinition extends GenericBeanDefinition implements AnnotatedBeanDefinition { + + private final AnnotationMetadata metadata; + + @Nullable + private MethodMetadata factoryMethodMetadata; + + + /** + * Create a new AnnotatedGenericBeanDefinition for the given bean class. + * @param beanClass the loaded bean class + */ + public AnnotatedGenericBeanDefinition(Class beanClass) { + setBeanClass(beanClass); + this.metadata = AnnotationMetadata.introspect(beanClass); + } + + /** + * Create a new AnnotatedGenericBeanDefinition for the given annotation metadata, + * allowing for ASM-based processing and avoidance of early loading of the bean class. + * Note that this constructor is functionally equivalent to + * {@link org.springframework.context.annotation.ScannedGenericBeanDefinition + * ScannedGenericBeanDefinition}, however the semantics of the latter indicate that a + * bean was discovered specifically via component-scanning as opposed to other means. + * @param metadata the annotation metadata for the bean class in question + * @since 3.1.1 + */ + public AnnotatedGenericBeanDefinition(AnnotationMetadata metadata) { + Assert.notNull(metadata, "AnnotationMetadata must not be null"); + if (metadata instanceof StandardAnnotationMetadata) { + setBeanClass(((StandardAnnotationMetadata) metadata).getIntrospectedClass()); + } + else { + setBeanClassName(metadata.getClassName()); + } + this.metadata = metadata; + } + + /** + * Create a new AnnotatedGenericBeanDefinition for the given annotation metadata, + * based on an annotated class and a factory method on that class. + * @param metadata the annotation metadata for the bean class in question + * @param factoryMethodMetadata metadata for the selected factory method + * @since 4.1.1 + */ + public AnnotatedGenericBeanDefinition(AnnotationMetadata metadata, MethodMetadata factoryMethodMetadata) { + this(metadata); + Assert.notNull(factoryMethodMetadata, "MethodMetadata must not be null"); + setFactoryMethodName(factoryMethodMetadata.getMethodName()); + this.factoryMethodMetadata = factoryMethodMetadata; + } + + + @Override + public final AnnotationMetadata getMetadata() { + return this.metadata; + } + + @Override + @Nullable + public final MethodMetadata getFactoryMethodMetadata() { + return this.factoryMethodMetadata; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AnnotationBeanWiringInfoResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AnnotationBeanWiringInfoResolver.java new file mode 100644 index 0000000..b355040 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AnnotationBeanWiringInfoResolver.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.annotation; + +import org.springframework.beans.factory.wiring.BeanWiringInfo; +import org.springframework.beans.factory.wiring.BeanWiringInfoResolver; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * {@link org.springframework.beans.factory.wiring.BeanWiringInfoResolver} that + * uses the Configurable annotation to identify which classes need autowiring. + * The bean name to look up will be taken from the {@link Configurable} annotation + * if specified; otherwise the default will be the fully-qualified name of the + * class being configured. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + * @see Configurable + * @see org.springframework.beans.factory.wiring.ClassNameBeanWiringInfoResolver + */ +public class AnnotationBeanWiringInfoResolver implements BeanWiringInfoResolver { + + @Override + @Nullable + public BeanWiringInfo resolveWiringInfo(Object beanInstance) { + Assert.notNull(beanInstance, "Bean instance must not be null"); + Configurable annotation = beanInstance.getClass().getAnnotation(Configurable.class); + return (annotation != null ? buildWiringInfo(beanInstance, annotation) : null); + } + + /** + * Build the {@link BeanWiringInfo} for the given {@link Configurable} annotation. + * @param beanInstance the bean instance + * @param annotation the Configurable annotation found on the bean class + * @return the resolved BeanWiringInfo + */ + protected BeanWiringInfo buildWiringInfo(Object beanInstance, Configurable annotation) { + if (!Autowire.NO.equals(annotation.autowire())) { + // Autowiring by name or by type + return new BeanWiringInfo(annotation.autowire().value(), annotation.dependencyCheck()); + } + else if (!annotation.value().isEmpty()) { + // Explicitly specified bean name for bean definition to take property values from + return new BeanWiringInfo(annotation.value(), false); + } + else { + // Default bean name for bean definition to take property values from + return new BeanWiringInfo(getDefaultBeanName(beanInstance), true); + } + } + + /** + * Determine the default bean name for the specified bean instance. + *

The default implementation returns the superclass name for a CGLIB + * proxy and the name of the plain bean class else. + * @param beanInstance the bean instance to build a default name for + * @return the default bean name to use + * @see org.springframework.util.ClassUtils#getUserClass(Class) + */ + protected String getDefaultBeanName(Object beanInstance) { + return ClassUtils.getUserClass(beanInstance).getName(); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Autowire.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Autowire.java new file mode 100644 index 0000000..27c6921 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Autowire.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2009 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.annotation; + +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; + +/** + * Enumeration determining autowiring status: that is, whether a bean should + * have its dependencies automatically injected by the Spring container using + * setter injection. This is a core concept in Spring DI. + * + *

Available for use in annotation-based configurations, such as for the + * AspectJ AnnotationBeanConfigurer aspect. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.beans.factory.annotation.Configurable + * @see org.springframework.beans.factory.config.AutowireCapableBeanFactory + */ +public enum Autowire { + + /** + * Constant that indicates no autowiring at all. + */ + NO(AutowireCapableBeanFactory.AUTOWIRE_NO), + + /** + * Constant that indicates autowiring bean properties by name. + */ + BY_NAME(AutowireCapableBeanFactory.AUTOWIRE_BY_NAME), + + /** + * Constant that indicates autowiring bean properties by type. + */ + BY_TYPE(AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE); + + + private final int value; + + + Autowire(int value) { + this.value = value; + } + + public int value() { + return this.value; + } + + /** + * Return whether this represents an actual autowiring value. + * @return whether actual autowiring was specified + * (either BY_NAME or BY_TYPE) + */ + public boolean isAutowire() { + return (this == BY_NAME || this == BY_TYPE); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Autowired.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Autowired.java new file mode 100644 index 0000000..242fddb --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Autowired.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a constructor, field, setter method, or config method as to be autowired by + * Spring's dependency injection facilities. This is an alternative to the JSR-330 + * {@link javax.inject.Inject} annotation, adding required-vs-optional semantics. + * + *

Autowired Constructors

+ *

Only one constructor of any given bean class may declare this annotation with the + * {@link #required} attribute set to {@code true}, indicating the constructor + * to autowire when used as a Spring bean. Furthermore, if the {@code required} + * attribute is set to {@code true}, only a single constructor may be annotated + * with {@code @Autowired}. If multiple non-required constructors declare the + * annotation, they will be considered as candidates for autowiring. The constructor + * with the greatest number of dependencies that can be satisfied by matching beans + * in the Spring container will be chosen. If none of the candidates can be satisfied, + * then a primary/default constructor (if present) will be used. Similarly, if a + * class declares multiple constructors but none of them is annotated with + * {@code @Autowired}, then a primary/default constructor (if present) will be used. + * If a class only declares a single constructor to begin with, it will always be used, + * even if not annotated. An annotated constructor does not have to be public. + * + *

Autowired Fields

+ *

Fields are injected right after construction of a bean, before any config methods + * are invoked. Such a config field does not have to be public. + * + *

Autowired Methods

+ *

Config methods may have an arbitrary name and any number of arguments; each of + * those arguments will be autowired with a matching bean in the Spring container. + * Bean property setter methods are effectively just a special case of such a general + * config method. Such config methods do not have to be public. + * + *

Autowired Parameters

+ *

Although {@code @Autowired} can technically be declared on individual method + * or constructor parameters since Spring Framework 5.0, most parts of the + * framework ignore such declarations. The only part of the core Spring Framework + * that actively supports autowired parameters is the JUnit Jupiter support in + * the {@code spring-test} module (see the + * TestContext framework + * reference documentation for details). + * + *

Multiple Arguments and 'required' Semantics

+ *

In the case of a multi-arg constructor or method, the {@link #required} attribute + * is applicable to all arguments. Individual parameters may be declared as Java-8 style + * {@link java.util.Optional} or, as of Spring Framework 5.0, also as {@code @Nullable} + * or a not-null parameter type in Kotlin, overriding the base 'required' semantics. + * + *

Autowiring Arrays, Collections, and Maps

+ *

In case of an array, {@link java.util.Collection}, or {@link java.util.Map} + * dependency type, the container autowires all beans matching the declared value + * type. For such purposes, the map keys must be declared as type {@code String} + * which will be resolved to the corresponding bean names. Such a container-provided + * collection will be ordered, taking into account + * {@link org.springframework.core.Ordered Ordered} and + * {@link org.springframework.core.annotation.Order @Order} values of the target + * components, otherwise following their registration order in the container. + * Alternatively, a single matching target bean may also be a generally typed + * {@code Collection} or {@code Map} itself, getting injected as such. + * + *

Not supported in {@code BeanPostProcessor} or {@code BeanFactoryPostProcessor}

+ *

Note that actual injection is performed through a + * {@link org.springframework.beans.factory.config.BeanPostProcessor + * BeanPostProcessor} which in turn means that you cannot + * use {@code @Autowired} to inject references into + * {@link org.springframework.beans.factory.config.BeanPostProcessor + * BeanPostProcessor} or + * {@link org.springframework.beans.factory.config.BeanFactoryPostProcessor BeanFactoryPostProcessor} + * types. Please consult the javadoc for the {@link AutowiredAnnotationBeanPostProcessor} + * class (which, by default, checks for the presence of this annotation). + * + * @author Juergen Hoeller + * @author Mark Fisher + * @author Sam Brannen + * @since 2.5 + * @see AutowiredAnnotationBeanPostProcessor + * @see Qualifier + * @see Value + */ +@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Autowired { + + /** + * Declares whether the annotated dependency is required. + *

Defaults to {@code true}. + */ + boolean required() default true; + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java new file mode 100644 index 0000000..bd42f89 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java @@ -0,0 +1,799 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.annotation; + +import java.beans.PropertyDescriptor; +import java.lang.annotation.Annotation; +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.PropertyValues; +import org.springframework.beans.TypeConverter; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.InjectionPoint; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.UnsatisfiedDependencyException; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor; +import org.springframework.beans.factory.support.LookupOverride; +import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.MethodParameter; +import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * {@link org.springframework.beans.factory.config.BeanPostProcessor BeanPostProcessor} + * implementation that autowires annotated fields, setter methods, and arbitrary + * config methods. Such members to be injected are detected through annotations: + * by default, Spring's {@link Autowired @Autowired} and {@link Value @Value} + * annotations. + * + *

Also supports JSR-330's {@link javax.inject.Inject @Inject} annotation, + * if available, as a direct alternative to Spring's own {@code @Autowired}. + * + *

Autowired Constructors

+ *

Only one constructor of any given bean class may declare this annotation with + * the 'required' attribute set to {@code true}, indicating the constructor + * to autowire when used as a Spring bean. Furthermore, if the 'required' attribute + * is set to {@code true}, only a single constructor may be annotated with + * {@code @Autowired}. If multiple non-required constructors declare the + * annotation, they will be considered as candidates for autowiring. The constructor + * with the greatest number of dependencies that can be satisfied by matching beans + * in the Spring container will be chosen. If none of the candidates can be satisfied, + * then a primary/default constructor (if present) will be used. If a class only + * declares a single constructor to begin with, it will always be used, even if not + * annotated. An annotated constructor does not have to be public. + * + *

Autowired Fields

+ *

Fields are injected right after construction of a bean, before any + * config methods are invoked. Such a config field does not have to be public. + * + *

Autowired Methods

+ *

Config methods may have an arbitrary name and any number of arguments; each of + * those arguments will be autowired with a matching bean in the Spring container. + * Bean property setter methods are effectively just a special case of such a + * general config method. Config methods do not have to be public. + * + *

Annotation Config vs. XML Config

+ *

A default {@code AutowiredAnnotationBeanPostProcessor} will be registered + * by the "context:annotation-config" and "context:component-scan" XML tags. + * Remove or turn off the default annotation configuration there if you intend + * to specify a custom {@code AutowiredAnnotationBeanPostProcessor} bean definition. + * + *

NOTE: Annotation injection will be performed before XML injection; + * thus the latter configuration will override the former for properties wired through + * both approaches. + * + *

{@literal @}Lookup Methods

+ *

In addition to regular injection points as discussed above, this post-processor + * also handles Spring's {@link Lookup @Lookup} annotation which identifies lookup + * methods to be replaced by the container at runtime. This is essentially a type-safe + * version of {@code getBean(Class, args)} and {@code getBean(String, args)}. + * See {@link Lookup @Lookup's javadoc} for details. + * + * @author Juergen Hoeller + * @author Mark Fisher + * @author Stephane Nicoll + * @author Sebastien Deleuze + * @author Sam Brannen + * @since 2.5 + * @see #setAutowiredAnnotationType + * @see Autowired + * @see Value + */ +public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor, + MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware { + + protected final Log logger = LogFactory.getLog(getClass()); + + private final Set> autowiredAnnotationTypes = new LinkedHashSet<>(4); + + private String requiredParameterName = "required"; + + private boolean requiredParameterValue = true; + + private int order = Ordered.LOWEST_PRECEDENCE - 2; + + @Nullable + private ConfigurableListableBeanFactory beanFactory; + + private final Set lookupMethodsChecked = Collections.newSetFromMap(new ConcurrentHashMap<>(256)); + + private final Map, Constructor[]> candidateConstructorsCache = new ConcurrentHashMap<>(256); + + private final Map injectionMetadataCache = new ConcurrentHashMap<>(256); + + + /** + * Create a new {@code AutowiredAnnotationBeanPostProcessor} for Spring's + * standard {@link Autowired @Autowired} and {@link Value @Value} annotations. + *

Also supports JSR-330's {@link javax.inject.Inject @Inject} annotation, + * if available. + */ + @SuppressWarnings("unchecked") + public AutowiredAnnotationBeanPostProcessor() { + this.autowiredAnnotationTypes.add(Autowired.class); + this.autowiredAnnotationTypes.add(Value.class); + try { + this.autowiredAnnotationTypes.add((Class) + ClassUtils.forName("javax.inject.Inject", AutowiredAnnotationBeanPostProcessor.class.getClassLoader())); + logger.trace("JSR-330 'javax.inject.Inject' annotation found and supported for autowiring"); + } + catch (ClassNotFoundException ex) { + // JSR-330 API not available - simply skip. + } + } + + + /** + * Set the 'autowired' annotation type, to be used on constructors, fields, + * setter methods, and arbitrary config methods. + *

The default autowired annotation types are the Spring-provided + * {@link Autowired @Autowired} and {@link Value @Value} annotations as well + * as JSR-330's {@link javax.inject.Inject @Inject} annotation, if available. + *

This setter property exists so that developers can provide their own + * (non-Spring-specific) annotation type to indicate that a member is supposed + * to be autowired. + */ + public void setAutowiredAnnotationType(Class autowiredAnnotationType) { + Assert.notNull(autowiredAnnotationType, "'autowiredAnnotationType' must not be null"); + this.autowiredAnnotationTypes.clear(); + this.autowiredAnnotationTypes.add(autowiredAnnotationType); + } + + /** + * Set the 'autowired' annotation types, to be used on constructors, fields, + * setter methods, and arbitrary config methods. + *

The default autowired annotation types are the Spring-provided + * {@link Autowired @Autowired} and {@link Value @Value} annotations as well + * as JSR-330's {@link javax.inject.Inject @Inject} annotation, if available. + *

This setter property exists so that developers can provide their own + * (non-Spring-specific) annotation types to indicate that a member is supposed + * to be autowired. + */ + public void setAutowiredAnnotationTypes(Set> autowiredAnnotationTypes) { + Assert.notEmpty(autowiredAnnotationTypes, "'autowiredAnnotationTypes' must not be empty"); + this.autowiredAnnotationTypes.clear(); + this.autowiredAnnotationTypes.addAll(autowiredAnnotationTypes); + } + + /** + * Set the name of an attribute of the annotation that specifies whether it is required. + * @see #setRequiredParameterValue(boolean) + */ + public void setRequiredParameterName(String requiredParameterName) { + this.requiredParameterName = requiredParameterName; + } + + /** + * Set the boolean value that marks a dependency as required. + *

For example if using 'required=true' (the default), this value should be + * {@code true}; but if using 'optional=false', this value should be {@code false}. + * @see #setRequiredParameterName(String) + */ + public void setRequiredParameterValue(boolean requiredParameterValue) { + this.requiredParameterValue = requiredParameterValue; + } + + public void setOrder(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return this.order; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + if (!(beanFactory instanceof ConfigurableListableBeanFactory)) { + throw new IllegalArgumentException( + "AutowiredAnnotationBeanPostProcessor requires a ConfigurableListableBeanFactory: " + beanFactory); + } + this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; + } + + + @Override + public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class beanType, String beanName) { + InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null); + metadata.checkConfigMembers(beanDefinition); + } + + @Override + public void resetBeanDefinition(String beanName) { + this.lookupMethodsChecked.remove(beanName); + this.injectionMetadataCache.remove(beanName); + } + + @Override + @Nullable + public Constructor[] determineCandidateConstructors(Class beanClass, final String beanName) + throws BeanCreationException { + + // Let's check for lookup methods here... + if (!this.lookupMethodsChecked.contains(beanName)) { + if (AnnotationUtils.isCandidateClass(beanClass, Lookup.class)) { + try { + Class targetClass = beanClass; + do { + ReflectionUtils.doWithLocalMethods(targetClass, method -> { + Lookup lookup = method.getAnnotation(Lookup.class); + if (lookup != null) { + Assert.state(this.beanFactory != null, "No BeanFactory available"); + LookupOverride override = new LookupOverride(method, lookup.value()); + try { + RootBeanDefinition mbd = (RootBeanDefinition) + this.beanFactory.getMergedBeanDefinition(beanName); + mbd.getMethodOverrides().addOverride(override); + } + catch (NoSuchBeanDefinitionException ex) { + throw new BeanCreationException(beanName, + "Cannot apply @Lookup to beans without corresponding bean definition"); + } + } + }); + targetClass = targetClass.getSuperclass(); + } + while (targetClass != null && targetClass != Object.class); + + } + catch (IllegalStateException ex) { + throw new BeanCreationException(beanName, "Lookup method resolution failed", ex); + } + } + this.lookupMethodsChecked.add(beanName); + } + + // Quick check on the concurrent map first, with minimal locking. + Constructor[] candidateConstructors = this.candidateConstructorsCache.get(beanClass); + if (candidateConstructors == null) { + // Fully synchronized resolution now... + synchronized (this.candidateConstructorsCache) { + candidateConstructors = this.candidateConstructorsCache.get(beanClass); + if (candidateConstructors == null) { + Constructor[] rawCandidates; + try { + rawCandidates = beanClass.getDeclaredConstructors(); + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, + "Resolution of declared constructors on bean Class [" + beanClass.getName() + + "] from ClassLoader [" + beanClass.getClassLoader() + "] failed", ex); + } + List> candidates = new ArrayList<>(rawCandidates.length); + Constructor requiredConstructor = null; + Constructor defaultConstructor = null; + Constructor primaryConstructor = BeanUtils.findPrimaryConstructor(beanClass); + int nonSyntheticConstructors = 0; + for (Constructor candidate : rawCandidates) { + if (!candidate.isSynthetic()) { + nonSyntheticConstructors++; + } + else if (primaryConstructor != null) { + continue; + } + MergedAnnotation ann = findAutowiredAnnotation(candidate); + if (ann == null) { + Class userClass = ClassUtils.getUserClass(beanClass); + if (userClass != beanClass) { + try { + Constructor superCtor = + userClass.getDeclaredConstructor(candidate.getParameterTypes()); + ann = findAutowiredAnnotation(superCtor); + } + catch (NoSuchMethodException ex) { + // Simply proceed, no equivalent superclass constructor found... + } + } + } + if (ann != null) { + if (requiredConstructor != null) { + throw new BeanCreationException(beanName, + "Invalid autowire-marked constructor: " + candidate + + ". Found constructor with 'required' Autowired annotation already: " + + requiredConstructor); + } + boolean required = determineRequiredStatus(ann); + if (required) { + if (!candidates.isEmpty()) { + throw new BeanCreationException(beanName, + "Invalid autowire-marked constructors: " + candidates + + ". Found constructor with 'required' Autowired annotation: " + + candidate); + } + requiredConstructor = candidate; + } + candidates.add(candidate); + } + else if (candidate.getParameterCount() == 0) { + defaultConstructor = candidate; + } + } + if (!candidates.isEmpty()) { + // Add default constructor to list of optional constructors, as fallback. + if (requiredConstructor == null) { + if (defaultConstructor != null) { + candidates.add(defaultConstructor); + } + else if (candidates.size() == 1 && logger.isInfoEnabled()) { + logger.info("Inconsistent constructor declaration on bean with name '" + beanName + + "': single autowire-marked constructor flagged as optional - " + + "this constructor is effectively required since there is no " + + "default constructor to fall back to: " + candidates.get(0)); + } + } + candidateConstructors = candidates.toArray(new Constructor[0]); + } + else if (rawCandidates.length == 1 && rawCandidates[0].getParameterCount() > 0) { + candidateConstructors = new Constructor[] {rawCandidates[0]}; + } + else if (nonSyntheticConstructors == 2 && primaryConstructor != null && + defaultConstructor != null && !primaryConstructor.equals(defaultConstructor)) { + candidateConstructors = new Constructor[] {primaryConstructor, defaultConstructor}; + } + else if (nonSyntheticConstructors == 1 && primaryConstructor != null) { + candidateConstructors = new Constructor[] {primaryConstructor}; + } + else { + candidateConstructors = new Constructor[0]; + } + this.candidateConstructorsCache.put(beanClass, candidateConstructors); + } + } + } + return (candidateConstructors.length > 0 ? candidateConstructors : null); + } + + @Override + public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) { + InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs); + try { + metadata.inject(bean, beanName, pvs); + } + catch (BeanCreationException ex) { + throw ex; + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex); + } + return pvs; + } + + @Deprecated + @Override + public PropertyValues postProcessPropertyValues( + PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) { + + return postProcessProperties(pvs, bean, beanName); + } + + /** + * 'Native' processing method for direct calls with an arbitrary target instance, + * resolving all of its fields and methods which are annotated with one of the + * configured 'autowired' annotation types. + * @param bean the target instance to process + * @throws BeanCreationException if autowiring failed + * @see #setAutowiredAnnotationTypes(Set) + */ + public void processInjection(Object bean) throws BeanCreationException { + Class clazz = bean.getClass(); + InjectionMetadata metadata = findAutowiringMetadata(clazz.getName(), clazz, null); + try { + metadata.inject(bean, null, null); + } + catch (BeanCreationException ex) { + throw ex; + } + catch (Throwable ex) { + throw new BeanCreationException( + "Injection of autowired dependencies failed for class [" + clazz + "]", ex); + } + } + + + private InjectionMetadata findAutowiringMetadata(String beanName, Class clazz, @Nullable PropertyValues pvs) { + // Fall back to class name as cache key, for backwards compatibility with custom callers. + String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName()); + // Quick check on the concurrent map first, with minimal locking. + InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey); + if (InjectionMetadata.needsRefresh(metadata, clazz)) { + synchronized (this.injectionMetadataCache) { + metadata = this.injectionMetadataCache.get(cacheKey); + if (InjectionMetadata.needsRefresh(metadata, clazz)) { + if (metadata != null) { + metadata.clear(pvs); + } + metadata = buildAutowiringMetadata(clazz); + this.injectionMetadataCache.put(cacheKey, metadata); + } + } + } + return metadata; + } + + private InjectionMetadata buildAutowiringMetadata(final Class clazz) { + if (!AnnotationUtils.isCandidateClass(clazz, this.autowiredAnnotationTypes)) { + return InjectionMetadata.EMPTY; + } + + List elements = new ArrayList<>(); + Class targetClass = clazz; + + do { + final List currElements = new ArrayList<>(); + + ReflectionUtils.doWithLocalFields(targetClass, field -> { + MergedAnnotation ann = findAutowiredAnnotation(field); + if (ann != null) { + if (Modifier.isStatic(field.getModifiers())) { + if (logger.isInfoEnabled()) { + logger.info("Autowired annotation is not supported on static fields: " + field); + } + return; + } + boolean required = determineRequiredStatus(ann); + currElements.add(new AutowiredFieldElement(field, required)); + } + }); + + ReflectionUtils.doWithLocalMethods(targetClass, method -> { + Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method); + if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) { + return; + } + MergedAnnotation ann = findAutowiredAnnotation(bridgedMethod); + if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { + if (Modifier.isStatic(method.getModifiers())) { + if (logger.isInfoEnabled()) { + logger.info("Autowired annotation is not supported on static methods: " + method); + } + return; + } + if (method.getParameterCount() == 0) { + if (logger.isInfoEnabled()) { + logger.info("Autowired annotation should only be used on methods with parameters: " + + method); + } + } + boolean required = determineRequiredStatus(ann); + PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz); + currElements.add(new AutowiredMethodElement(method, required, pd)); + } + }); + + elements.addAll(0, currElements); + targetClass = targetClass.getSuperclass(); + } + while (targetClass != null && targetClass != Object.class); + + return InjectionMetadata.forElements(elements, clazz); + } + + @Nullable + private MergedAnnotation findAutowiredAnnotation(AccessibleObject ao) { + MergedAnnotations annotations = MergedAnnotations.from(ao); + for (Class type : this.autowiredAnnotationTypes) { + MergedAnnotation annotation = annotations.get(type); + if (annotation.isPresent()) { + return annotation; + } + } + return null; + } + + /** + * Determine if the annotated field or method requires its dependency. + *

A 'required' dependency means that autowiring should fail when no beans + * are found. Otherwise, the autowiring process will simply bypass the field + * or method when no beans are found. + * @param ann the Autowired annotation + * @return whether the annotation indicates that a dependency is required + */ + @SuppressWarnings({"deprecation", "cast"}) + protected boolean determineRequiredStatus(MergedAnnotation ann) { + // The following (AnnotationAttributes) cast is required on JDK 9+. + return determineRequiredStatus((AnnotationAttributes) + ann.asMap(mergedAnnotation -> new AnnotationAttributes(mergedAnnotation.getType()))); + } + + /** + * Determine if the annotated field or method requires its dependency. + *

A 'required' dependency means that autowiring should fail when no beans + * are found. Otherwise, the autowiring process will simply bypass the field + * or method when no beans are found. + * @param ann the Autowired annotation + * @return whether the annotation indicates that a dependency is required + * @deprecated since 5.2, in favor of {@link #determineRequiredStatus(MergedAnnotation)} + */ + @Deprecated + protected boolean determineRequiredStatus(AnnotationAttributes ann) { + return (!ann.containsKey(this.requiredParameterName) || + this.requiredParameterValue == ann.getBoolean(this.requiredParameterName)); + } + + /** + * Obtain all beans of the given type as autowire candidates. + * @param type the type of the bean + * @return the target beans, or an empty Collection if no bean of this type is found + * @throws BeansException if bean retrieval failed + */ + protected Map findAutowireCandidates(Class type) throws BeansException { + if (this.beanFactory == null) { + throw new IllegalStateException("No BeanFactory configured - " + + "override the getBeanOfType method or specify the 'beanFactory' property"); + } + return BeanFactoryUtils.beansOfTypeIncludingAncestors(this.beanFactory, type); + } + + /** + * Register the specified bean as dependent on the autowired beans. + */ + private void registerDependentBeans(@Nullable String beanName, Set autowiredBeanNames) { + if (beanName != null) { + for (String autowiredBeanName : autowiredBeanNames) { + if (this.beanFactory != null && this.beanFactory.containsBean(autowiredBeanName)) { + this.beanFactory.registerDependentBean(autowiredBeanName, beanName); + } + if (logger.isTraceEnabled()) { + logger.trace("Autowiring by type from bean name '" + beanName + + "' to bean named '" + autowiredBeanName + "'"); + } + } + } + } + + /** + * Resolve the specified cached method argument or field value. + */ + @Nullable + private Object resolvedCachedArgument(@Nullable String beanName, @Nullable Object cachedArgument) { + if (cachedArgument instanceof DependencyDescriptor) { + DependencyDescriptor descriptor = (DependencyDescriptor) cachedArgument; + Assert.state(this.beanFactory != null, "No BeanFactory available"); + return this.beanFactory.resolveDependency(descriptor, beanName, null, null); + } + else { + return cachedArgument; + } + } + + + /** + * Class representing injection information about an annotated field. + */ + private class AutowiredFieldElement extends InjectionMetadata.InjectedElement { + + private final boolean required; + + private volatile boolean cached; + + @Nullable + private volatile Object cachedFieldValue; + + public AutowiredFieldElement(Field field, boolean required) { + super(field, null); + this.required = required; + } + + @Override + protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable { + Field field = (Field) this.member; + Object value; + if (this.cached) { + value = resolvedCachedArgument(beanName, this.cachedFieldValue); + } + else { + DependencyDescriptor desc = new DependencyDescriptor(field, this.required); + desc.setContainingClass(bean.getClass()); + Set autowiredBeanNames = new LinkedHashSet<>(1); + Assert.state(beanFactory != null, "No BeanFactory available"); + TypeConverter typeConverter = beanFactory.getTypeConverter(); + try { + value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter); + } + catch (BeansException ex) { + throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(field), ex); + } + synchronized (this) { + if (!this.cached) { + Object cachedFieldValue = null; + if (value != null || this.required) { + cachedFieldValue = desc; + registerDependentBeans(beanName, autowiredBeanNames); + if (autowiredBeanNames.size() == 1) { + String autowiredBeanName = autowiredBeanNames.iterator().next(); + if (beanFactory.containsBean(autowiredBeanName) && + beanFactory.isTypeMatch(autowiredBeanName, field.getType())) { + cachedFieldValue = new ShortcutDependencyDescriptor( + desc, autowiredBeanName, field.getType()); + } + } + } + this.cachedFieldValue = cachedFieldValue; + this.cached = true; + } + } + } + if (value != null) { + ReflectionUtils.makeAccessible(field); + field.set(bean, value); + } + } + } + + + /** + * Class representing injection information about an annotated method. + */ + private class AutowiredMethodElement extends InjectionMetadata.InjectedElement { + + private final boolean required; + + private volatile boolean cached; + + @Nullable + private volatile Object[] cachedMethodArguments; + + public AutowiredMethodElement(Method method, boolean required, @Nullable PropertyDescriptor pd) { + super(method, pd); + this.required = required; + } + + @Override + protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable { + if (checkPropertySkipping(pvs)) { + return; + } + Method method = (Method) this.member; + Object[] arguments; + if (this.cached) { + // Shortcut for avoiding synchronization... + arguments = resolveCachedArguments(beanName); + } + else { + int argumentCount = method.getParameterCount(); + arguments = new Object[argumentCount]; + DependencyDescriptor[] descriptors = new DependencyDescriptor[argumentCount]; + Set autowiredBeans = new LinkedHashSet<>(argumentCount); + Assert.state(beanFactory != null, "No BeanFactory available"); + TypeConverter typeConverter = beanFactory.getTypeConverter(); + for (int i = 0; i < arguments.length; i++) { + MethodParameter methodParam = new MethodParameter(method, i); + DependencyDescriptor currDesc = new DependencyDescriptor(methodParam, this.required); + currDesc.setContainingClass(bean.getClass()); + descriptors[i] = currDesc; + try { + Object arg = beanFactory.resolveDependency(currDesc, beanName, autowiredBeans, typeConverter); + if (arg == null && !this.required) { + arguments = null; + break; + } + arguments[i] = arg; + } + catch (BeansException ex) { + throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(methodParam), ex); + } + } + synchronized (this) { + if (!this.cached) { + if (arguments != null) { + DependencyDescriptor[] cachedMethodArguments = Arrays.copyOf(descriptors, arguments.length); + registerDependentBeans(beanName, autowiredBeans); + if (autowiredBeans.size() == argumentCount) { + Iterator it = autowiredBeans.iterator(); + Class[] paramTypes = method.getParameterTypes(); + for (int i = 0; i < paramTypes.length; i++) { + String autowiredBeanName = it.next(); + if (beanFactory.containsBean(autowiredBeanName) && + beanFactory.isTypeMatch(autowiredBeanName, paramTypes[i])) { + cachedMethodArguments[i] = new ShortcutDependencyDescriptor( + descriptors[i], autowiredBeanName, paramTypes[i]); + } + } + } + this.cachedMethodArguments = cachedMethodArguments; + } + else { + this.cachedMethodArguments = null; + } + this.cached = true; + } + } + } + if (arguments != null) { + try { + ReflectionUtils.makeAccessible(method); + method.invoke(bean, arguments); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + } + + @Nullable + private Object[] resolveCachedArguments(@Nullable String beanName) { + Object[] cachedMethodArguments = this.cachedMethodArguments; + if (cachedMethodArguments == null) { + return null; + } + Object[] arguments = new Object[cachedMethodArguments.length]; + for (int i = 0; i < arguments.length; i++) { + arguments[i] = resolvedCachedArgument(beanName, cachedMethodArguments[i]); + } + return arguments; + } + } + + + /** + * DependencyDescriptor variant with a pre-resolved target bean name. + */ + @SuppressWarnings("serial") + private static class ShortcutDependencyDescriptor extends DependencyDescriptor { + + private final String shortcut; + + private final Class requiredType; + + public ShortcutDependencyDescriptor(DependencyDescriptor original, String shortcut, Class requiredType) { + super(original); + this.shortcut = shortcut; + this.requiredType = requiredType; + } + + @Override + public Object resolveShortcut(BeanFactory beanFactory) { + return beanFactory.getBean(this.shortcut, this.requiredType); + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/BeanFactoryAnnotationUtils.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/BeanFactoryAnnotationUtils.java new file mode 100644 index 0000000..58e04c4 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/BeanFactoryAnnotationUtils.java @@ -0,0 +1,205 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.annotation; + +import java.lang.reflect.Method; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Predicate; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.AutowireCandidateQualifier; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Convenience methods performing bean lookups related to Spring-specific annotations, + * for example Spring's {@link Qualifier @Qualifier} annotation. + * + * @author Juergen Hoeller + * @author Chris Beams + * @since 3.1.2 + * @see BeanFactoryUtils + */ +public abstract class BeanFactoryAnnotationUtils { + + /** + * Retrieve all bean of type {@code T} from the given {@code BeanFactory} declaring a + * qualifier (e.g. via {@code } or {@code @Qualifier}) matching the given + * qualifier, or having a bean name matching the given qualifier. + * @param beanFactory the factory to get the target beans from (also searching ancestors) + * @param beanType the type of beans to retrieve + * @param qualifier the qualifier for selecting among all type matches + * @return the matching beans of type {@code T} + * @throws BeansException if any of the matching beans could not be created + * @since 5.1.1 + * @see BeanFactoryUtils#beansOfTypeIncludingAncestors(ListableBeanFactory, Class) + */ + public static Map qualifiedBeansOfType( + ListableBeanFactory beanFactory, Class beanType, String qualifier) throws BeansException { + + String[] candidateBeans = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, beanType); + Map result = new LinkedHashMap<>(4); + for (String beanName : candidateBeans) { + if (isQualifierMatch(qualifier::equals, beanName, beanFactory)) { + result.put(beanName, beanFactory.getBean(beanName, beanType)); + } + } + return result; + } + + /** + * Obtain a bean of type {@code T} from the given {@code BeanFactory} declaring a + * qualifier (e.g. via {@code } or {@code @Qualifier}) matching the given + * qualifier, or having a bean name matching the given qualifier. + * @param beanFactory the factory to get the target bean from (also searching ancestors) + * @param beanType the type of bean to retrieve + * @param qualifier the qualifier for selecting between multiple bean matches + * @return the matching bean of type {@code T} (never {@code null}) + * @throws NoUniqueBeanDefinitionException if multiple matching beans of type {@code T} found + * @throws NoSuchBeanDefinitionException if no matching bean of type {@code T} found + * @throws BeansException if the bean could not be created + * @see BeanFactoryUtils#beanOfTypeIncludingAncestors(ListableBeanFactory, Class) + */ + public static T qualifiedBeanOfType(BeanFactory beanFactory, Class beanType, String qualifier) + throws BeansException { + + Assert.notNull(beanFactory, "BeanFactory must not be null"); + + if (beanFactory instanceof ListableBeanFactory) { + // Full qualifier matching supported. + return qualifiedBeanOfType((ListableBeanFactory) beanFactory, beanType, qualifier); + } + else if (beanFactory.containsBean(qualifier)) { + // Fallback: target bean at least found by bean name. + return beanFactory.getBean(qualifier, beanType); + } + else { + throw new NoSuchBeanDefinitionException(qualifier, "No matching " + beanType.getSimpleName() + + " bean found for bean name '" + qualifier + + "'! (Note: Qualifier matching not supported because given " + + "BeanFactory does not implement ConfigurableListableBeanFactory.)"); + } + } + + /** + * Obtain a bean of type {@code T} from the given {@code BeanFactory} declaring a qualifier + * (e.g. {@code } or {@code @Qualifier}) matching the given qualifier). + * @param bf the factory to get the target bean from + * @param beanType the type of bean to retrieve + * @param qualifier the qualifier for selecting between multiple bean matches + * @return the matching bean of type {@code T} (never {@code null}) + */ + private static T qualifiedBeanOfType(ListableBeanFactory bf, Class beanType, String qualifier) { + String[] candidateBeans = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(bf, beanType); + String matchingBean = null; + for (String beanName : candidateBeans) { + if (isQualifierMatch(qualifier::equals, beanName, bf)) { + if (matchingBean != null) { + throw new NoUniqueBeanDefinitionException(beanType, matchingBean, beanName); + } + matchingBean = beanName; + } + } + if (matchingBean != null) { + return bf.getBean(matchingBean, beanType); + } + else if (bf.containsBean(qualifier)) { + // Fallback: target bean at least found by bean name - probably a manually registered singleton. + return bf.getBean(qualifier, beanType); + } + else { + throw new NoSuchBeanDefinitionException(qualifier, "No matching " + beanType.getSimpleName() + + " bean found for qualifier '" + qualifier + "' - neither qualifier match nor bean name match!"); + } + } + + /** + * Check whether the named bean declares a qualifier of the given name. + * @param qualifier the qualifier to match + * @param beanName the name of the candidate bean + * @param beanFactory the factory from which to retrieve the named bean + * @return {@code true} if either the bean definition (in the XML case) + * or the bean's factory method (in the {@code @Bean} case) defines a matching + * qualifier value (through {@code } or {@code @Qualifier}) + * @since 5.0 + */ + public static boolean isQualifierMatch( + Predicate qualifier, String beanName, @Nullable BeanFactory beanFactory) { + + // Try quick bean name or alias match first... + if (qualifier.test(beanName)) { + return true; + } + if (beanFactory != null) { + for (String alias : beanFactory.getAliases(beanName)) { + if (qualifier.test(alias)) { + return true; + } + } + try { + Class beanType = beanFactory.getType(beanName); + if (beanFactory instanceof ConfigurableBeanFactory) { + BeanDefinition bd = ((ConfigurableBeanFactory) beanFactory).getMergedBeanDefinition(beanName); + // Explicit qualifier metadata on bean definition? (typically in XML definition) + if (bd instanceof AbstractBeanDefinition) { + AbstractBeanDefinition abd = (AbstractBeanDefinition) bd; + AutowireCandidateQualifier candidate = abd.getQualifier(Qualifier.class.getName()); + if (candidate != null) { + Object value = candidate.getAttribute(AutowireCandidateQualifier.VALUE_KEY); + if (value != null && qualifier.test(value.toString())) { + return true; + } + } + } + // Corresponding qualifier on factory method? (typically in configuration class) + if (bd instanceof RootBeanDefinition) { + Method factoryMethod = ((RootBeanDefinition) bd).getResolvedFactoryMethod(); + if (factoryMethod != null) { + Qualifier targetAnnotation = AnnotationUtils.getAnnotation(factoryMethod, Qualifier.class); + if (targetAnnotation != null) { + return qualifier.test(targetAnnotation.value()); + } + } + } + } + // Corresponding qualifier on bean implementation class? (for custom user types) + if (beanType != null) { + Qualifier targetAnnotation = AnnotationUtils.getAnnotation(beanType, Qualifier.class); + if (targetAnnotation != null) { + return qualifier.test(targetAnnotation.value()); + } + } + } + catch (NoSuchBeanDefinitionException ex) { + // Ignore - can't compare qualifiers for a manually registered singleton object + } + } + return false; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Configurable.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Configurable.java new file mode 100644 index 0000000..52705b6 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Configurable.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a class as being eligible for Spring-driven configuration. + * + *

Typically used with the AspectJ {@code AnnotationBeanConfigurerAspect}. + * + * @author Rod Johnson + * @author Rob Harrop + * @author Adrian Colyer + * @author Ramnivas Laddad + * @since 2.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface Configurable { + + /** + * The name of the bean definition that serves as the configuration template. + */ + String value() default ""; + + /** + * Are dependencies to be injected via autowiring? + */ + Autowire autowire() default Autowire.NO; + + /** + * Is dependency checking to be performed for configured objects? + */ + boolean dependencyCheck() default false; + + /** + * Are dependencies to be injected prior to the construction of an object? + */ + boolean preConstruction() default false; + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/CustomAutowireConfigurer.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/CustomAutowireConfigurer.java new file mode 100644 index 0000000..d43329a --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/CustomAutowireConfigurer.java @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.annotation; + +import java.lang.annotation.Annotation; +import java.util.Set; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.core.Ordered; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * A {@link org.springframework.beans.factory.config.BeanFactoryPostProcessor} + * implementation that allows for convenient registration of custom autowire + * qualifier types. + * + *

+ * <bean id="customAutowireConfigurer" class="org.springframework.beans.factory.annotation.CustomAutowireConfigurer">
+ *   <property name="customQualifierTypes">
+ *     <set>
+ *       <value>mypackage.MyQualifier</value>
+ *     </set>
+ *   </property>
+ * </bean>
+ * + * @author Mark Fisher + * @author Juergen Hoeller + * @since 2.5 + * @see org.springframework.beans.factory.annotation.Qualifier + */ +public class CustomAutowireConfigurer implements BeanFactoryPostProcessor, BeanClassLoaderAware, Ordered { + + private int order = Ordered.LOWEST_PRECEDENCE; // default: same as non-Ordered + + @Nullable + private Set customQualifierTypes; + + @Nullable + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + + public void setOrder(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return this.order; + } + + @Override + public void setBeanClassLoader(@Nullable ClassLoader beanClassLoader) { + this.beanClassLoader = beanClassLoader; + } + + /** + * Register custom qualifier annotation types to be considered + * when autowiring beans. Each element of the provided set may + * be either a Class instance or a String representation of the + * fully-qualified class name of the custom annotation. + *

Note that any annotation that is itself annotated with Spring's + * {@link org.springframework.beans.factory.annotation.Qualifier} + * does not require explicit registration. + * @param customQualifierTypes the custom types to register + */ + public void setCustomQualifierTypes(Set customQualifierTypes) { + this.customQualifierTypes = customQualifierTypes; + } + + + @Override + @SuppressWarnings("unchecked") + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + if (this.customQualifierTypes != null) { + if (!(beanFactory instanceof DefaultListableBeanFactory)) { + throw new IllegalStateException( + "CustomAutowireConfigurer needs to operate on a DefaultListableBeanFactory"); + } + DefaultListableBeanFactory dlbf = (DefaultListableBeanFactory) beanFactory; + if (!(dlbf.getAutowireCandidateResolver() instanceof QualifierAnnotationAutowireCandidateResolver)) { + dlbf.setAutowireCandidateResolver(new QualifierAnnotationAutowireCandidateResolver()); + } + QualifierAnnotationAutowireCandidateResolver resolver = + (QualifierAnnotationAutowireCandidateResolver) dlbf.getAutowireCandidateResolver(); + for (Object value : this.customQualifierTypes) { + Class customType = null; + if (value instanceof Class) { + customType = (Class) value; + } + else if (value instanceof String) { + String className = (String) value; + customType = (Class) ClassUtils.resolveClassName(className, this.beanClassLoader); + } + else { + throw new IllegalArgumentException( + "Invalid value [" + value + "] for custom qualifier type: needs to be Class or String."); + } + if (!Annotation.class.isAssignableFrom(customType)) { + throw new IllegalArgumentException( + "Qualifier type [" + customType.getName() + "] needs to be annotation type"); + } + resolver.addQualifierType(customType); + } + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java new file mode 100644 index 0000000..f76a03b --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java @@ -0,0 +1,410 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.annotation; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor; +import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * {@link org.springframework.beans.factory.config.BeanPostProcessor} implementation + * that invokes annotated init and destroy methods. Allows for an annotation + * alternative to Spring's {@link org.springframework.beans.factory.InitializingBean} + * and {@link org.springframework.beans.factory.DisposableBean} callback interfaces. + * + *

The actual annotation types that this post-processor checks for can be + * configured through the {@link #setInitAnnotationType "initAnnotationType"} + * and {@link #setDestroyAnnotationType "destroyAnnotationType"} properties. + * Any custom annotation can be used, since there are no required annotation + * attributes. + * + *

Init and destroy annotations may be applied to methods of any visibility: + * public, package-protected, protected, or private. Multiple such methods + * may be annotated, but it is recommended to only annotate one single + * init method and destroy method, respectively. + * + *

Spring's {@link org.springframework.context.annotation.CommonAnnotationBeanPostProcessor} + * supports the JSR-250 {@link javax.annotation.PostConstruct} and {@link javax.annotation.PreDestroy} + * annotations out of the box, as init annotation and destroy annotation, respectively. + * Furthermore, it also supports the {@link javax.annotation.Resource} annotation + * for annotation-driven injection of named beans. + * + * @author Juergen Hoeller + * @since 2.5 + * @see #setInitAnnotationType + * @see #setDestroyAnnotationType + */ +@SuppressWarnings("serial") +public class InitDestroyAnnotationBeanPostProcessor + implements DestructionAwareBeanPostProcessor, MergedBeanDefinitionPostProcessor, PriorityOrdered, Serializable { + + private final transient LifecycleMetadata emptyLifecycleMetadata = + new LifecycleMetadata(Object.class, Collections.emptyList(), Collections.emptyList()) { + @Override + public void checkConfigMembers(RootBeanDefinition beanDefinition) { + } + @Override + public void invokeInitMethods(Object target, String beanName) { + } + @Override + public void invokeDestroyMethods(Object target, String beanName) { + } + @Override + public boolean hasDestroyMethods() { + return false; + } + }; + + + protected transient Log logger = LogFactory.getLog(getClass()); + + @Nullable + private Class initAnnotationType; + + @Nullable + private Class destroyAnnotationType; + + private int order = Ordered.LOWEST_PRECEDENCE; + + @Nullable + private final transient Map, LifecycleMetadata> lifecycleMetadataCache = new ConcurrentHashMap<>(256); + + + /** + * Specify the init annotation to check for, indicating initialization + * methods to call after configuration of a bean. + *

Any custom annotation can be used, since there are no required + * annotation attributes. There is no default, although a typical choice + * is the JSR-250 {@link javax.annotation.PostConstruct} annotation. + */ + public void setInitAnnotationType(Class initAnnotationType) { + this.initAnnotationType = initAnnotationType; + } + + /** + * Specify the destroy annotation to check for, indicating destruction + * methods to call when the context is shutting down. + *

Any custom annotation can be used, since there are no required + * annotation attributes. There is no default, although a typical choice + * is the JSR-250 {@link javax.annotation.PreDestroy} annotation. + */ + public void setDestroyAnnotationType(Class destroyAnnotationType) { + this.destroyAnnotationType = destroyAnnotationType; + } + + public void setOrder(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return this.order; + } + + + @Override + public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class beanType, String beanName) { + LifecycleMetadata metadata = findLifecycleMetadata(beanType); + metadata.checkConfigMembers(beanDefinition); + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + LifecycleMetadata metadata = findLifecycleMetadata(bean.getClass()); + try { + metadata.invokeInitMethods(bean, beanName); + } + catch (InvocationTargetException ex) { + throw new BeanCreationException(beanName, "Invocation of init method failed", ex.getTargetException()); + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, "Failed to invoke init method", ex); + } + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + + @Override + public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException { + LifecycleMetadata metadata = findLifecycleMetadata(bean.getClass()); + try { + metadata.invokeDestroyMethods(bean, beanName); + } + catch (InvocationTargetException ex) { + String msg = "Destroy method on bean with name '" + beanName + "' threw an exception"; + if (logger.isDebugEnabled()) { + logger.warn(msg, ex.getTargetException()); + } + else { + logger.warn(msg + ": " + ex.getTargetException()); + } + } + catch (Throwable ex) { + logger.warn("Failed to invoke destroy method on bean with name '" + beanName + "'", ex); + } + } + + @Override + public boolean requiresDestruction(Object bean) { + return findLifecycleMetadata(bean.getClass()).hasDestroyMethods(); + } + + + private LifecycleMetadata findLifecycleMetadata(Class clazz) { + if (this.lifecycleMetadataCache == null) { + // Happens after deserialization, during destruction... + return buildLifecycleMetadata(clazz); + } + // Quick check on the concurrent map first, with minimal locking. + LifecycleMetadata metadata = this.lifecycleMetadataCache.get(clazz); + if (metadata == null) { + synchronized (this.lifecycleMetadataCache) { + metadata = this.lifecycleMetadataCache.get(clazz); + if (metadata == null) { + metadata = buildLifecycleMetadata(clazz); + this.lifecycleMetadataCache.put(clazz, metadata); + } + return metadata; + } + } + return metadata; + } + + private LifecycleMetadata buildLifecycleMetadata(final Class clazz) { + if (!AnnotationUtils.isCandidateClass(clazz, Arrays.asList(this.initAnnotationType, this.destroyAnnotationType))) { + return this.emptyLifecycleMetadata; + } + + List initMethods = new ArrayList<>(); + List destroyMethods = new ArrayList<>(); + Class targetClass = clazz; + + do { + final List currInitMethods = new ArrayList<>(); + final List currDestroyMethods = new ArrayList<>(); + + ReflectionUtils.doWithLocalMethods(targetClass, method -> { + if (this.initAnnotationType != null && method.isAnnotationPresent(this.initAnnotationType)) { + LifecycleElement element = new LifecycleElement(method); + currInitMethods.add(element); + if (logger.isTraceEnabled()) { + logger.trace("Found init method on class [" + clazz.getName() + "]: " + method); + } + } + if (this.destroyAnnotationType != null && method.isAnnotationPresent(this.destroyAnnotationType)) { + currDestroyMethods.add(new LifecycleElement(method)); + if (logger.isTraceEnabled()) { + logger.trace("Found destroy method on class [" + clazz.getName() + "]: " + method); + } + } + }); + + initMethods.addAll(0, currInitMethods); + destroyMethods.addAll(currDestroyMethods); + targetClass = targetClass.getSuperclass(); + } + while (targetClass != null && targetClass != Object.class); + + return (initMethods.isEmpty() && destroyMethods.isEmpty() ? this.emptyLifecycleMetadata : + new LifecycleMetadata(clazz, initMethods, destroyMethods)); + } + + + //--------------------------------------------------------------------- + // Serialization support + //--------------------------------------------------------------------- + + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + // Rely on default serialization; just initialize state after deserialization. + ois.defaultReadObject(); + + // Initialize transient fields. + this.logger = LogFactory.getLog(getClass()); + } + + + /** + * Class representing information about annotated init and destroy methods. + */ + private class LifecycleMetadata { + + private final Class targetClass; + + private final Collection initMethods; + + private final Collection destroyMethods; + + @Nullable + private volatile Set checkedInitMethods; + + @Nullable + private volatile Set checkedDestroyMethods; + + public LifecycleMetadata(Class targetClass, Collection initMethods, + Collection destroyMethods) { + + this.targetClass = targetClass; + this.initMethods = initMethods; + this.destroyMethods = destroyMethods; + } + + public void checkConfigMembers(RootBeanDefinition beanDefinition) { + Set checkedInitMethods = new LinkedHashSet<>(this.initMethods.size()); + for (LifecycleElement element : this.initMethods) { + String methodIdentifier = element.getIdentifier(); + if (!beanDefinition.isExternallyManagedInitMethod(methodIdentifier)) { + beanDefinition.registerExternallyManagedInitMethod(methodIdentifier); + checkedInitMethods.add(element); + if (logger.isTraceEnabled()) { + logger.trace("Registered init method on class [" + this.targetClass.getName() + "]: " + element); + } + } + } + Set checkedDestroyMethods = new LinkedHashSet<>(this.destroyMethods.size()); + for (LifecycleElement element : this.destroyMethods) { + String methodIdentifier = element.getIdentifier(); + if (!beanDefinition.isExternallyManagedDestroyMethod(methodIdentifier)) { + beanDefinition.registerExternallyManagedDestroyMethod(methodIdentifier); + checkedDestroyMethods.add(element); + if (logger.isTraceEnabled()) { + logger.trace("Registered destroy method on class [" + this.targetClass.getName() + "]: " + element); + } + } + } + this.checkedInitMethods = checkedInitMethods; + this.checkedDestroyMethods = checkedDestroyMethods; + } + + public void invokeInitMethods(Object target, String beanName) throws Throwable { + Collection checkedInitMethods = this.checkedInitMethods; + Collection initMethodsToIterate = + (checkedInitMethods != null ? checkedInitMethods : this.initMethods); + if (!initMethodsToIterate.isEmpty()) { + for (LifecycleElement element : initMethodsToIterate) { + if (logger.isTraceEnabled()) { + logger.trace("Invoking init method on bean '" + beanName + "': " + element.getMethod()); + } + element.invoke(target); + } + } + } + + public void invokeDestroyMethods(Object target, String beanName) throws Throwable { + Collection checkedDestroyMethods = this.checkedDestroyMethods; + Collection destroyMethodsToUse = + (checkedDestroyMethods != null ? checkedDestroyMethods : this.destroyMethods); + if (!destroyMethodsToUse.isEmpty()) { + for (LifecycleElement element : destroyMethodsToUse) { + if (logger.isTraceEnabled()) { + logger.trace("Invoking destroy method on bean '" + beanName + "': " + element.getMethod()); + } + element.invoke(target); + } + } + } + + public boolean hasDestroyMethods() { + Collection checkedDestroyMethods = this.checkedDestroyMethods; + Collection destroyMethodsToUse = + (checkedDestroyMethods != null ? checkedDestroyMethods : this.destroyMethods); + return !destroyMethodsToUse.isEmpty(); + } + } + + + /** + * Class representing injection information about an annotated method. + */ + private static class LifecycleElement { + + private final Method method; + + private final String identifier; + + public LifecycleElement(Method method) { + if (method.getParameterCount() != 0) { + throw new IllegalStateException("Lifecycle method annotation requires a no-arg method: " + method); + } + this.method = method; + this.identifier = (Modifier.isPrivate(method.getModifiers()) ? + ClassUtils.getQualifiedMethodName(method) : method.getName()); + } + + public Method getMethod() { + return this.method; + } + + public String getIdentifier() { + return this.identifier; + } + + public void invoke(Object target) throws Throwable { + ReflectionUtils.makeAccessible(this.method); + this.method.invoke(target, (Object[]) null); + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof LifecycleElement)) { + return false; + } + LifecycleElement otherElement = (LifecycleElement) other; + return (this.identifier.equals(otherElement.identifier)); + } + + @Override + public int hashCode() { + return this.identifier.hashCode(); + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java new file mode 100644 index 0000000..f7dcb8d --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java @@ -0,0 +1,325 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.annotation; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.PropertyValues; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; + +/** + * Internal class for managing injection metadata. + * Not intended for direct use in applications. + * + *

Used by {@link AutowiredAnnotationBeanPostProcessor}, + * {@link org.springframework.context.annotation.CommonAnnotationBeanPostProcessor} and + * {@link org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor}. + * + * @author Juergen Hoeller + * @since 2.5 + */ +public class InjectionMetadata { + + /** + * An empty {@code InjectionMetadata} instance with no-op callbacks. + * @since 5.2 + */ + public static final InjectionMetadata EMPTY = new InjectionMetadata(Object.class, Collections.emptyList()) { + @Override + protected boolean needsRefresh(Class clazz) { + return false; + } + @Override + public void checkConfigMembers(RootBeanDefinition beanDefinition) { + } + @Override + public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) { + } + @Override + public void clear(@Nullable PropertyValues pvs) { + } + }; + + + private final Class targetClass; + + private final Collection injectedElements; + + @Nullable + private volatile Set checkedElements; + + + /** + * Create a new {@code InjectionMetadata instance}. + *

Preferably use {@link #forElements} for reusing the {@link #EMPTY} + * instance in case of no elements. + * @param targetClass the target class + * @param elements the associated elements to inject + * @see #forElements + */ + public InjectionMetadata(Class targetClass, Collection elements) { + this.targetClass = targetClass; + this.injectedElements = elements; + } + + + /** + * Determine whether this metadata instance needs to be refreshed. + * @param clazz the current target class + * @return {@code true} indicating a refresh, {@code false} otherwise + * @since 5.2.4 + */ + protected boolean needsRefresh(Class clazz) { + return this.targetClass != clazz; + } + + public void checkConfigMembers(RootBeanDefinition beanDefinition) { + Set checkedElements = new LinkedHashSet<>(this.injectedElements.size()); + for (InjectedElement element : this.injectedElements) { + Member member = element.getMember(); + if (!beanDefinition.isExternallyManagedConfigMember(member)) { + beanDefinition.registerExternallyManagedConfigMember(member); + checkedElements.add(element); + } + } + this.checkedElements = checkedElements; + } + + public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable { + Collection checkedElements = this.checkedElements; + Collection elementsToIterate = + (checkedElements != null ? checkedElements : this.injectedElements); + if (!elementsToIterate.isEmpty()) { + for (InjectedElement element : elementsToIterate) { + element.inject(target, beanName, pvs); + } + } + } + + /** + * Clear property skipping for the contained elements. + * @since 3.2.13 + */ + public void clear(@Nullable PropertyValues pvs) { + Collection checkedElements = this.checkedElements; + Collection elementsToIterate = + (checkedElements != null ? checkedElements : this.injectedElements); + if (!elementsToIterate.isEmpty()) { + for (InjectedElement element : elementsToIterate) { + element.clearPropertySkipping(pvs); + } + } + } + + + /** + * Return an {@code InjectionMetadata} instance, possibly for empty elements. + * @param elements the elements to inject (possibly empty) + * @param clazz the target class + * @return a new {@link #InjectionMetadata(Class, Collection)} instance + * @since 5.2 + */ + public static InjectionMetadata forElements(Collection elements, Class clazz) { + return (elements.isEmpty() ? new InjectionMetadata(clazz, Collections.emptyList()) : + new InjectionMetadata(clazz, elements)); + } + + /** + * Check whether the given injection metadata needs to be refreshed. + * @param metadata the existing metadata instance + * @param clazz the current target class + * @return {@code true} indicating a refresh, {@code false} otherwise + * @see #needsRefresh(Class) + */ + public static boolean needsRefresh(@Nullable InjectionMetadata metadata, Class clazz) { + return (metadata == null || metadata.needsRefresh(clazz)); + } + + + /** + * A single injected element. + */ + public abstract static class InjectedElement { + + protected final Member member; + + protected final boolean isField; + + @Nullable + protected final PropertyDescriptor pd; + + @Nullable + protected volatile Boolean skip; + + protected InjectedElement(Member member, @Nullable PropertyDescriptor pd) { + this.member = member; + this.isField = (member instanceof Field); + this.pd = pd; + } + + public final Member getMember() { + return this.member; + } + + protected final Class getResourceType() { + if (this.isField) { + return ((Field) this.member).getType(); + } + else if (this.pd != null) { + return this.pd.getPropertyType(); + } + else { + return ((Method) this.member).getParameterTypes()[0]; + } + } + + protected final void checkResourceType(Class resourceType) { + if (this.isField) { + Class fieldType = ((Field) this.member).getType(); + if (!(resourceType.isAssignableFrom(fieldType) || fieldType.isAssignableFrom(resourceType))) { + throw new IllegalStateException("Specified field type [" + fieldType + + "] is incompatible with resource type [" + resourceType.getName() + "]"); + } + } + else { + Class paramType = + (this.pd != null ? this.pd.getPropertyType() : ((Method) this.member).getParameterTypes()[0]); + if (!(resourceType.isAssignableFrom(paramType) || paramType.isAssignableFrom(resourceType))) { + throw new IllegalStateException("Specified parameter type [" + paramType + + "] is incompatible with resource type [" + resourceType.getName() + "]"); + } + } + } + + /** + * Either this or {@link #getResourceToInject} needs to be overridden. + */ + protected void inject(Object target, @Nullable String requestingBeanName, @Nullable PropertyValues pvs) + throws Throwable { + + if (this.isField) { + Field field = (Field) this.member; + ReflectionUtils.makeAccessible(field); + field.set(target, getResourceToInject(target, requestingBeanName)); + } + else { + if (checkPropertySkipping(pvs)) { + return; + } + try { + Method method = (Method) this.member; + ReflectionUtils.makeAccessible(method); + method.invoke(target, getResourceToInject(target, requestingBeanName)); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + } + + /** + * Check whether this injector's property needs to be skipped due to + * an explicit property value having been specified. Also marks the + * affected property as processed for other processors to ignore it. + */ + protected boolean checkPropertySkipping(@Nullable PropertyValues pvs) { + Boolean skip = this.skip; + if (skip != null) { + return skip; + } + if (pvs == null) { + this.skip = false; + return false; + } + synchronized (pvs) { + skip = this.skip; + if (skip != null) { + return skip; + } + if (this.pd != null) { + if (pvs.contains(this.pd.getName())) { + // Explicit value provided as part of the bean definition. + this.skip = true; + return true; + } + else if (pvs instanceof MutablePropertyValues) { + ((MutablePropertyValues) pvs).registerProcessedProperty(this.pd.getName()); + } + } + this.skip = false; + return false; + } + } + + /** + * Clear property skipping for this element. + * @since 3.2.13 + */ + protected void clearPropertySkipping(@Nullable PropertyValues pvs) { + if (pvs == null) { + return; + } + synchronized (pvs) { + if (Boolean.FALSE.equals(this.skip) && this.pd != null && pvs instanceof MutablePropertyValues) { + ((MutablePropertyValues) pvs).clearProcessedProperty(this.pd.getName()); + } + } + } + + /** + * Either this or {@link #inject} needs to be overridden. + */ + @Nullable + protected Object getResourceToInject(Object target, @Nullable String requestingBeanName) { + return null; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof InjectedElement)) { + return false; + } + InjectedElement otherElement = (InjectedElement) other; + return this.member.equals(otherElement.member); + } + + @Override + public int hashCode() { + return this.member.getClass().hashCode() * 29 + this.member.getName().hashCode(); + } + + @Override + public String toString() { + return getClass().getSimpleName() + " for " + this.member; + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Lookup.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Lookup.java new file mode 100644 index 0000000..0fca4f3 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Lookup.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation that indicates 'lookup' methods, to be overridden by the container + * to redirect them back to the {@link org.springframework.beans.factory.BeanFactory} + * for a {@code getBean} call. This is essentially an annotation-based version of the + * XML {@code lookup-method} attribute, resulting in the same runtime arrangement. + * + *

The resolution of the target bean can either be based on the return type + * ({@code getBean(Class)}) or on a suggested bean name ({@code getBean(String)}), + * in both cases passing the method's arguments to the {@code getBean} call + * for applying them as target factory method arguments or constructor arguments. + * + *

Such lookup methods can have default (stub) implementations that will simply + * get replaced by the container, or they can be declared as abstract - for the + * container to fill them in at runtime. In both cases, the container will generate + * runtime subclasses of the method's containing class via CGLIB, which is why such + * lookup methods can only work on beans that the container instantiates through + * regular constructors: i.e. lookup methods cannot get replaced on beans returned + * from factory methods where we cannot dynamically provide a subclass for them. + * + *

Recommendations for typical Spring configuration scenarios: + * When a concrete class may be needed in certain scenarios, consider providing stub + * implementations of your lookup methods. And please remember that lookup methods + * won't work on beans returned from {@code @Bean} methods in configuration classes; + * you'll have to resort to {@code @Inject Provider} or the like instead. + * + * @author Juergen Hoeller + * @since 4.1 + * @see org.springframework.beans.factory.BeanFactory#getBean(Class, Object...) + * @see org.springframework.beans.factory.BeanFactory#getBean(String, Object...) + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Lookup { + + /** + * This annotation attribute may suggest a target bean name to look up. + * If not specified, the target bean will be resolved based on the + * annotated method's return type declaration. + */ + String value() default ""; + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/ParameterResolutionDelegate.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/ParameterResolutionDelegate.java new file mode 100644 index 0000000..fc212da --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/ParameterResolutionDelegate.java @@ -0,0 +1,171 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Parameter; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Public delegate for resolving autowirable parameters on externally managed + * constructors and methods. + * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 5.2 + * @see #isAutowirable + * @see #resolveDependency + */ +public final class ParameterResolutionDelegate { + + private static final AnnotatedElement EMPTY_ANNOTATED_ELEMENT = new AnnotatedElement() { + @Override + @Nullable + public T getAnnotation(Class annotationClass) { + return null; + } + @Override + public Annotation[] getAnnotations() { + return new Annotation[0]; + } + @Override + public Annotation[] getDeclaredAnnotations() { + return new Annotation[0]; + } + }; + + + private ParameterResolutionDelegate() { + } + + + /** + * Determine if the supplied {@link Parameter} can potentially be + * autowired from an {@link AutowireCapableBeanFactory}. + *

Returns {@code true} if the supplied parameter is annotated or + * meta-annotated with {@link Autowired @Autowired}, + * {@link Qualifier @Qualifier}, or {@link Value @Value}. + *

Note that {@link #resolveDependency} may still be able to resolve the + * dependency for the supplied parameter even if this method returns {@code false}. + * @param parameter the parameter whose dependency should be autowired + * (must not be {@code null}) + * @param parameterIndex the index of the parameter in the constructor or method + * that declares the parameter + * @see #resolveDependency + */ + public static boolean isAutowirable(Parameter parameter, int parameterIndex) { + Assert.notNull(parameter, "Parameter must not be null"); + AnnotatedElement annotatedParameter = getEffectiveAnnotatedParameter(parameter, parameterIndex); + return (AnnotatedElementUtils.hasAnnotation(annotatedParameter, Autowired.class) || + AnnotatedElementUtils.hasAnnotation(annotatedParameter, Qualifier.class) || + AnnotatedElementUtils.hasAnnotation(annotatedParameter, Value.class)); + } + + /** + * Resolve the dependency for the supplied {@link Parameter} from the + * supplied {@link AutowireCapableBeanFactory}. + *

Provides comprehensive autowiring support for individual method parameters + * on par with Spring's dependency injection facilities for autowired fields and + * methods, including support for {@link Autowired @Autowired}, + * {@link Qualifier @Qualifier}, and {@link Value @Value} with support for property + * placeholders and SpEL expressions in {@code @Value} declarations. + *

The dependency is required unless the parameter is annotated or meta-annotated + * with {@link Autowired @Autowired} with the {@link Autowired#required required} + * flag set to {@code false}. + *

If an explicit qualifier is not declared, the name of the parameter + * will be used as the qualifier for resolving ambiguities. + * @param parameter the parameter whose dependency should be resolved (must not be + * {@code null}) + * @param parameterIndex the index of the parameter in the constructor or method + * that declares the parameter + * @param containingClass the concrete class that contains the parameter; this may + * differ from the class that declares the parameter in that it may be a subclass + * thereof, potentially substituting type variables (must not be {@code null}) + * @param beanFactory the {@code AutowireCapableBeanFactory} from which to resolve + * the dependency (must not be {@code null}) + * @return the resolved object, or {@code null} if none found + * @throws BeansException if dependency resolution failed + * @see #isAutowirable + * @see Autowired#required + * @see SynthesizingMethodParameter#forExecutable(Executable, int) + * @see AutowireCapableBeanFactory#resolveDependency(DependencyDescriptor, String) + */ + @Nullable + public static Object resolveDependency( + Parameter parameter, int parameterIndex, Class containingClass, AutowireCapableBeanFactory beanFactory) + throws BeansException { + + Assert.notNull(parameter, "Parameter must not be null"); + Assert.notNull(containingClass, "Containing class must not be null"); + Assert.notNull(beanFactory, "AutowireCapableBeanFactory must not be null"); + + AnnotatedElement annotatedParameter = getEffectiveAnnotatedParameter(parameter, parameterIndex); + Autowired autowired = AnnotatedElementUtils.findMergedAnnotation(annotatedParameter, Autowired.class); + boolean required = (autowired == null || autowired.required()); + + MethodParameter methodParameter = SynthesizingMethodParameter.forExecutable( + parameter.getDeclaringExecutable(), parameterIndex); + DependencyDescriptor descriptor = new DependencyDescriptor(methodParameter, required); + descriptor.setContainingClass(containingClass); + return beanFactory.resolveDependency(descriptor, null); + } + + /** + * Due to a bug in {@code javac} on JDK versions prior to JDK 9, looking up + * annotations directly on a {@link Parameter} will fail for inner class + * constructors. + *

Bug in javac in JDK < 9

+ *

The parameter annotations array in the compiled byte code excludes an entry + * for the implicit enclosing instance parameter for an inner class + * constructor. + *

Workaround

+ *

This method provides a workaround for this off-by-one error by allowing the + * caller to access annotations on the preceding {@link Parameter} object (i.e., + * {@code index - 1}). If the supplied {@code index} is zero, this method returns + * an empty {@code AnnotatedElement}. + *

WARNING

+ *

The {@code AnnotatedElement} returned by this method should never be cast and + * treated as a {@code Parameter} since the metadata (e.g., {@link Parameter#getName()}, + * {@link Parameter#getType()}, etc.) will not match those for the declared parameter + * at the given index in an inner class constructor. + * @return the supplied {@code parameter} or the effective {@code Parameter} + * if the aforementioned bug is in effect + */ + private static AnnotatedElement getEffectiveAnnotatedParameter(Parameter parameter, int index) { + Executable executable = parameter.getDeclaringExecutable(); + if (executable instanceof Constructor && ClassUtils.isInnerClass(executable.getDeclaringClass()) && + executable.getParameterAnnotations().length == executable.getParameterCount() - 1) { + // Bug in javac in JDK <9: annotation array excludes enclosing instance parameter + // for inner classes, so access it with the actual parameter index lowered by 1 + return (index == 0 ? EMPTY_ANNOTATED_ELEMENT : executable.getParameters()[index - 1]); + } + return parameter; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Qualifier.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Qualifier.java new file mode 100644 index 0000000..3fb314f --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Qualifier.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2011 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation may be used on a field or parameter as a qualifier for + * candidate beans when autowiring. It may also be used to annotate other + * custom annotations that can then in turn be used as qualifiers. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @since 2.5 + * @see Autowired + */ +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface Qualifier { + + String value() default ""; + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java new file mode 100644 index 0000000..e4e104b --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java @@ -0,0 +1,386 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.beans.SimpleTypeConverter; +import org.springframework.beans.TypeConverter; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.support.AutowireCandidateQualifier; +import org.springframework.beans.factory.support.AutowireCandidateResolver; +import org.springframework.beans.factory.support.GenericTypeAwareAutowireCandidateResolver; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; + +/** + * {@link AutowireCandidateResolver} implementation that matches bean definition qualifiers + * against {@link Qualifier qualifier annotations} on the field or parameter to be autowired. + * Also supports suggested expression values through a {@link Value value} annotation. + * + *

Also supports JSR-330's {@link javax.inject.Qualifier} annotation, if available. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @author Stephane Nicoll + * @since 2.5 + * @see AutowireCandidateQualifier + * @see Qualifier + * @see Value + */ +public class QualifierAnnotationAutowireCandidateResolver extends GenericTypeAwareAutowireCandidateResolver { + + private final Set> qualifierTypes = new LinkedHashSet<>(2); + + private Class valueAnnotationType = Value.class; + + + /** + * Create a new QualifierAnnotationAutowireCandidateResolver + * for Spring's standard {@link Qualifier} annotation. + *

Also supports JSR-330's {@link javax.inject.Qualifier} annotation, if available. + */ + @SuppressWarnings("unchecked") + public QualifierAnnotationAutowireCandidateResolver() { + this.qualifierTypes.add(Qualifier.class); + try { + this.qualifierTypes.add((Class) ClassUtils.forName("javax.inject.Qualifier", + QualifierAnnotationAutowireCandidateResolver.class.getClassLoader())); + } + catch (ClassNotFoundException ex) { + // JSR-330 API not available - simply skip. + } + } + + /** + * Create a new QualifierAnnotationAutowireCandidateResolver + * for the given qualifier annotation type. + * @param qualifierType the qualifier annotation to look for + */ + public QualifierAnnotationAutowireCandidateResolver(Class qualifierType) { + Assert.notNull(qualifierType, "'qualifierType' must not be null"); + this.qualifierTypes.add(qualifierType); + } + + /** + * Create a new QualifierAnnotationAutowireCandidateResolver + * for the given qualifier annotation types. + * @param qualifierTypes the qualifier annotations to look for + */ + public QualifierAnnotationAutowireCandidateResolver(Set> qualifierTypes) { + Assert.notNull(qualifierTypes, "'qualifierTypes' must not be null"); + this.qualifierTypes.addAll(qualifierTypes); + } + + + /** + * Register the given type to be used as a qualifier when autowiring. + *

This identifies qualifier annotations for direct use (on fields, + * method parameters and constructor parameters) as well as meta + * annotations that in turn identify actual qualifier annotations. + *

This implementation only supports annotations as qualifier types. + * The default is Spring's {@link Qualifier} annotation which serves + * as a qualifier for direct use and also as a meta annotation. + * @param qualifierType the annotation type to register + */ + public void addQualifierType(Class qualifierType) { + this.qualifierTypes.add(qualifierType); + } + + /** + * Set the 'value' annotation type, to be used on fields, method parameters + * and constructor parameters. + *

The default value annotation type is the Spring-provided + * {@link Value} annotation. + *

This setter property exists so that developers can provide their own + * (non-Spring-specific) annotation type to indicate a default value + * expression for a specific argument. + */ + public void setValueAnnotationType(Class valueAnnotationType) { + this.valueAnnotationType = valueAnnotationType; + } + + + /** + * Determine whether the provided bean definition is an autowire candidate. + *

To be considered a candidate the bean's autowire-candidate + * attribute must not have been set to 'false'. Also, if an annotation on + * the field or parameter to be autowired is recognized by this bean factory + * as a qualifier, the bean must 'match' against the annotation as + * well as any attributes it may contain. The bean definition must contain + * the same qualifier or match by meta attributes. A "value" attribute will + * fallback to match against the bean name or an alias if a qualifier or + * attribute does not match. + * @see Qualifier + */ + @Override + public boolean isAutowireCandidate(BeanDefinitionHolder bdHolder, DependencyDescriptor descriptor) { + boolean match = super.isAutowireCandidate(bdHolder, descriptor); + if (match) { + match = checkQualifiers(bdHolder, descriptor.getAnnotations()); + if (match) { + MethodParameter methodParam = descriptor.getMethodParameter(); + if (methodParam != null) { + Method method = methodParam.getMethod(); + if (method == null || void.class == method.getReturnType()) { + match = checkQualifiers(bdHolder, methodParam.getMethodAnnotations()); + } + } + } + } + return match; + } + + /** + * Match the given qualifier annotations against the candidate bean definition. + */ + protected boolean checkQualifiers(BeanDefinitionHolder bdHolder, Annotation[] annotationsToSearch) { + if (ObjectUtils.isEmpty(annotationsToSearch)) { + return true; + } + SimpleTypeConverter typeConverter = new SimpleTypeConverter(); + for (Annotation annotation : annotationsToSearch) { + Class type = annotation.annotationType(); + boolean checkMeta = true; + boolean fallbackToMeta = false; + if (isQualifier(type)) { + if (!checkQualifier(bdHolder, annotation, typeConverter)) { + fallbackToMeta = true; + } + else { + checkMeta = false; + } + } + if (checkMeta) { + boolean foundMeta = false; + for (Annotation metaAnn : type.getAnnotations()) { + Class metaType = metaAnn.annotationType(); + if (isQualifier(metaType)) { + foundMeta = true; + // Only accept fallback match if @Qualifier annotation has a value... + // Otherwise it is just a marker for a custom qualifier annotation. + if ((fallbackToMeta && ObjectUtils.isEmpty(AnnotationUtils.getValue(metaAnn))) || + !checkQualifier(bdHolder, metaAnn, typeConverter)) { + return false; + } + } + } + if (fallbackToMeta && !foundMeta) { + return false; + } + } + } + return true; + } + + /** + * Checks whether the given annotation type is a recognized qualifier type. + */ + protected boolean isQualifier(Class annotationType) { + for (Class qualifierType : this.qualifierTypes) { + if (annotationType.equals(qualifierType) || annotationType.isAnnotationPresent(qualifierType)) { + return true; + } + } + return false; + } + + /** + * Match the given qualifier annotation against the candidate bean definition. + */ + protected boolean checkQualifier( + BeanDefinitionHolder bdHolder, Annotation annotation, TypeConverter typeConverter) { + + Class type = annotation.annotationType(); + RootBeanDefinition bd = (RootBeanDefinition) bdHolder.getBeanDefinition(); + + AutowireCandidateQualifier qualifier = bd.getQualifier(type.getName()); + if (qualifier == null) { + qualifier = bd.getQualifier(ClassUtils.getShortName(type)); + } + if (qualifier == null) { + // First, check annotation on qualified element, if any + Annotation targetAnnotation = getQualifiedElementAnnotation(bd, type); + // Then, check annotation on factory method, if applicable + if (targetAnnotation == null) { + targetAnnotation = getFactoryMethodAnnotation(bd, type); + } + if (targetAnnotation == null) { + RootBeanDefinition dbd = getResolvedDecoratedDefinition(bd); + if (dbd != null) { + targetAnnotation = getFactoryMethodAnnotation(dbd, type); + } + } + if (targetAnnotation == null) { + // Look for matching annotation on the target class + if (getBeanFactory() != null) { + try { + Class beanType = getBeanFactory().getType(bdHolder.getBeanName()); + if (beanType != null) { + targetAnnotation = AnnotationUtils.getAnnotation(ClassUtils.getUserClass(beanType), type); + } + } + catch (NoSuchBeanDefinitionException ex) { + // Not the usual case - simply forget about the type check... + } + } + if (targetAnnotation == null && bd.hasBeanClass()) { + targetAnnotation = AnnotationUtils.getAnnotation(ClassUtils.getUserClass(bd.getBeanClass()), type); + } + } + if (targetAnnotation != null && targetAnnotation.equals(annotation)) { + return true; + } + } + + Map attributes = AnnotationUtils.getAnnotationAttributes(annotation); + if (attributes.isEmpty() && qualifier == null) { + // If no attributes, the qualifier must be present + return false; + } + for (Map.Entry entry : attributes.entrySet()) { + String attributeName = entry.getKey(); + Object expectedValue = entry.getValue(); + Object actualValue = null; + // Check qualifier first + if (qualifier != null) { + actualValue = qualifier.getAttribute(attributeName); + } + if (actualValue == null) { + // Fall back on bean definition attribute + actualValue = bd.getAttribute(attributeName); + } + if (actualValue == null && attributeName.equals(AutowireCandidateQualifier.VALUE_KEY) && + expectedValue instanceof String && bdHolder.matchesName((String) expectedValue)) { + // Fall back on bean name (or alias) match + continue; + } + if (actualValue == null && qualifier != null) { + // Fall back on default, but only if the qualifier is present + actualValue = AnnotationUtils.getDefaultValue(annotation, attributeName); + } + if (actualValue != null) { + actualValue = typeConverter.convertIfNecessary(actualValue, expectedValue.getClass()); + } + if (!expectedValue.equals(actualValue)) { + return false; + } + } + return true; + } + + @Nullable + protected Annotation getQualifiedElementAnnotation(RootBeanDefinition bd, Class type) { + AnnotatedElement qualifiedElement = bd.getQualifiedElement(); + return (qualifiedElement != null ? AnnotationUtils.getAnnotation(qualifiedElement, type) : null); + } + + @Nullable + protected Annotation getFactoryMethodAnnotation(RootBeanDefinition bd, Class type) { + Method resolvedFactoryMethod = bd.getResolvedFactoryMethod(); + return (resolvedFactoryMethod != null ? AnnotationUtils.getAnnotation(resolvedFactoryMethod, type) : null); + } + + + /** + * Determine whether the given dependency declares an autowired annotation, + * checking its required flag. + * @see Autowired#required() + */ + @Override + public boolean isRequired(DependencyDescriptor descriptor) { + if (!super.isRequired(descriptor)) { + return false; + } + Autowired autowired = descriptor.getAnnotation(Autowired.class); + return (autowired == null || autowired.required()); + } + + /** + * Determine whether the given dependency declares a qualifier annotation. + * @see #isQualifier(Class) + * @see Qualifier + */ + @Override + public boolean hasQualifier(DependencyDescriptor descriptor) { + for (Annotation ann : descriptor.getAnnotations()) { + if (isQualifier(ann.annotationType())) { + return true; + } + } + return false; + } + + /** + * Determine whether the given dependency declares a value annotation. + * @see Value + */ + @Override + @Nullable + public Object getSuggestedValue(DependencyDescriptor descriptor) { + Object value = findValue(descriptor.getAnnotations()); + if (value == null) { + MethodParameter methodParam = descriptor.getMethodParameter(); + if (methodParam != null) { + value = findValue(methodParam.getMethodAnnotations()); + } + } + return value; + } + + /** + * Determine a suggested value from any of the given candidate annotations. + */ + @Nullable + protected Object findValue(Annotation[] annotationsToSearch) { + if (annotationsToSearch.length > 0) { // qualifier annotations have to be local + AnnotationAttributes attr = AnnotatedElementUtils.getMergedAnnotationAttributes( + AnnotatedElementUtils.forAnnotations(annotationsToSearch), this.valueAnnotationType); + if (attr != null) { + return extractValue(attr); + } + } + return null; + } + + /** + * Extract the value attribute from the given annotation. + * @since 4.3 + */ + protected Object extractValue(AnnotationAttributes attr) { + Object value = attr.get(AnnotationUtils.VALUE); + if (value == null) { + throw new IllegalStateException("Value annotation must have a value attribute"); + } + return value; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Required.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Required.java new file mode 100644 index 0000000..4993cf2 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Required.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a method (typically a JavaBean setter method) as being 'required': that is, + * the setter method must be configured to be dependency-injected with a value. + * + *

Please do consult the javadoc for the {@link RequiredAnnotationBeanPostProcessor} + * class (which, by default, checks for the presence of this annotation). + * + * @author Rob Harrop + * @since 2.0 + * @see RequiredAnnotationBeanPostProcessor + * @deprecated as of 5.1, in favor of using constructor injection for required settings + * (or a custom {@link org.springframework.beans.factory.InitializingBean} implementation) + */ +@Deprecated +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Required { + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/RequiredAnnotationBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/RequiredAnnotationBeanPostProcessor.java new file mode 100644 index 0000000..9978f97 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/RequiredAnnotationBeanPostProcessor.java @@ -0,0 +1,230 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.annotation; + +import java.beans.PropertyDescriptor; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.beans.PropertyValues; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor; +import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.Conventions; +import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * {@link org.springframework.beans.factory.config.BeanPostProcessor} implementation + * that enforces required JavaBean properties to have been configured. + * Required bean properties are detected through a Java 5 annotation: + * by default, Spring's {@link Required} annotation. + * + *

The motivation for the existence of this BeanPostProcessor is to allow + * developers to annotate the setter properties of their own classes with an + * arbitrary JDK 1.5 annotation to indicate that the container must check + * for the configuration of a dependency injected value. This neatly pushes + * responsibility for such checking onto the container (where it arguably belongs), + * and obviates the need (in part) for a developer to code a method that + * simply checks that all required properties have actually been set. + * + *

Please note that an 'init' method may still need to be implemented (and may + * still be desirable), because all that this class does is enforcing that a + * 'required' property has actually been configured with a value. It does + * not check anything else... In particular, it does not check that a + * configured value is not {@code null}. + * + *

Note: A default RequiredAnnotationBeanPostProcessor will be registered + * by the "context:annotation-config" and "context:component-scan" XML tags. + * Remove or turn off the default annotation configuration there if you intend + * to specify a custom RequiredAnnotationBeanPostProcessor bean definition. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + * @see #setRequiredAnnotationType + * @see Required + * @deprecated as of 5.1, in favor of using constructor injection for required settings + * (or a custom {@link org.springframework.beans.factory.InitializingBean} implementation) + */ +@Deprecated +public class RequiredAnnotationBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor, + MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware { + + /** + * Bean definition attribute that may indicate whether a given bean is supposed + * to be skipped when performing this post-processor's required property check. + * @see #shouldSkip + */ + public static final String SKIP_REQUIRED_CHECK_ATTRIBUTE = + Conventions.getQualifiedAttributeName(RequiredAnnotationBeanPostProcessor.class, "skipRequiredCheck"); + + + private Class requiredAnnotationType = Required.class; + + private int order = Ordered.LOWEST_PRECEDENCE - 1; + + @Nullable + private ConfigurableListableBeanFactory beanFactory; + + /** + * Cache for validated bean names, skipping re-validation for the same bean. + */ + private final Set validatedBeanNames = Collections.newSetFromMap(new ConcurrentHashMap<>(64)); + + + /** + * Set the 'required' annotation type, to be used on bean property + * setter methods. + *

The default required annotation type is the Spring-provided + * {@link Required} annotation. + *

This setter property exists so that developers can provide their own + * (non-Spring-specific) annotation type to indicate that a property value + * is required. + */ + public void setRequiredAnnotationType(Class requiredAnnotationType) { + Assert.notNull(requiredAnnotationType, "'requiredAnnotationType' must not be null"); + this.requiredAnnotationType = requiredAnnotationType; + } + + /** + * Return the 'required' annotation type. + */ + protected Class getRequiredAnnotationType() { + return this.requiredAnnotationType; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + if (beanFactory instanceof ConfigurableListableBeanFactory) { + this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; + } + } + + public void setOrder(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return this.order; + } + + + @Override + public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class beanType, String beanName) { + } + + @Override + public PropertyValues postProcessPropertyValues( + PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) { + + if (!this.validatedBeanNames.contains(beanName)) { + if (!shouldSkip(this.beanFactory, beanName)) { + List invalidProperties = new ArrayList<>(); + for (PropertyDescriptor pd : pds) { + if (isRequiredProperty(pd) && !pvs.contains(pd.getName())) { + invalidProperties.add(pd.getName()); + } + } + if (!invalidProperties.isEmpty()) { + throw new BeanInitializationException(buildExceptionMessage(invalidProperties, beanName)); + } + } + this.validatedBeanNames.add(beanName); + } + return pvs; + } + + /** + * Check whether the given bean definition is not subject to the annotation-based + * required property check as performed by this post-processor. + *

The default implementations check for the presence of the + * {@link #SKIP_REQUIRED_CHECK_ATTRIBUTE} attribute in the bean definition, if any. + * It also suggests skipping in case of a bean definition with a "factory-bean" + * reference set, assuming that instance-based factories pre-populate the bean. + * @param beanFactory the BeanFactory to check against + * @param beanName the name of the bean to check against + * @return {@code true} to skip the bean; {@code false} to process it + */ + protected boolean shouldSkip(@Nullable ConfigurableListableBeanFactory beanFactory, String beanName) { + if (beanFactory == null || !beanFactory.containsBeanDefinition(beanName)) { + return false; + } + BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName); + if (beanDefinition.getFactoryBeanName() != null) { + return true; + } + Object value = beanDefinition.getAttribute(SKIP_REQUIRED_CHECK_ATTRIBUTE); + return (value != null && (Boolean.TRUE.equals(value) || Boolean.parseBoolean(value.toString()))); + } + + /** + * Is the supplied property required to have a value (that is, to be dependency-injected)? + *

This implementation looks for the existence of a + * {@link #setRequiredAnnotationType "required" annotation} + * on the supplied {@link PropertyDescriptor property}. + * @param propertyDescriptor the target PropertyDescriptor (never {@code null}) + * @return {@code true} if the supplied property has been marked as being required; + * {@code false} if not, or if the supplied property does not have a setter method + */ + protected boolean isRequiredProperty(PropertyDescriptor propertyDescriptor) { + Method setter = propertyDescriptor.getWriteMethod(); + return (setter != null && AnnotationUtils.getAnnotation(setter, getRequiredAnnotationType()) != null); + } + + /** + * Build an exception message for the given list of invalid properties. + * @param invalidProperties the list of names of invalid properties + * @param beanName the name of the bean + * @return the exception message + */ + private String buildExceptionMessage(List invalidProperties, String beanName) { + int size = invalidProperties.size(); + StringBuilder sb = new StringBuilder(); + sb.append(size == 1 ? "Property" : "Properties"); + for (int i = 0; i < size; i++) { + String propertyName = invalidProperties.get(i); + if (i > 0) { + if (i == (size - 1)) { + sb.append(" and"); + } + else { + sb.append(","); + } + } + sb.append(" '").append(propertyName).append("'"); + } + sb.append(size == 1 ? " is" : " are"); + sb.append(" required for bean '").append(beanName).append("'"); + return sb.toString(); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Value.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Value.java new file mode 100644 index 0000000..bfa2730 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/Value.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation used at the field or method/constructor parameter level + * that indicates a default value expression for the annotated element. + * + *

Typically used for expression-driven or property-driven dependency injection. + * Also supported for dynamic resolution of handler method arguments — for + * example, in Spring MVC. + * + *

A common use case is to inject values using + * #{systemProperties.myProp} style SpEL (Spring Expression Language) + * expressions. Alternatively, values may be injected using + * ${my.app.myProp} style property placeholders. + * + *

Note that actual processing of the {@code @Value} annotation is performed + * by a {@link org.springframework.beans.factory.config.BeanPostProcessor + * BeanPostProcessor} which in turn means that you cannot use + * {@code @Value} within + * {@link org.springframework.beans.factory.config.BeanPostProcessor + * BeanPostProcessor} or + * {@link org.springframework.beans.factory.config.BeanFactoryPostProcessor BeanFactoryPostProcessor} + * types. Please consult the javadoc for the {@link AutowiredAnnotationBeanPostProcessor} + * class (which, by default, checks for the presence of this annotation). + * + * @author Juergen Hoeller + * @since 3.0 + * @see AutowiredAnnotationBeanPostProcessor + * @see Autowired + * @see org.springframework.beans.factory.config.BeanExpressionResolver + * @see org.springframework.beans.factory.support.AutowireCandidateResolver#getSuggestedValue + */ +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Value { + + /** + * The actual value expression such as #{systemProperties.myProp} + * or property placeholder such as ${my.app.myProp}. + */ + String value(); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/package-info.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/package-info.java new file mode 100644 index 0000000..5a277e0 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/package-info.java @@ -0,0 +1,9 @@ +/** + * Support package for annotation-driven bean configuration. + */ +@NonNullApi +@NonNullFields +package org.springframework.beans.factory.annotation; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/AutowireCapableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/AutowireCapableBeanFactory.java new file mode 100644 index 0000000..e096fbb --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/AutowireCapableBeanFactory.java @@ -0,0 +1,400 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.util.Set; + +import org.springframework.beans.BeansException; +import org.springframework.beans.TypeConverter; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.lang.Nullable; + +/** + * Extension of the {@link org.springframework.beans.factory.BeanFactory} + * interface to be implemented by bean factories that are capable of + * autowiring, provided that they want to expose this functionality for + * existing bean instances. + * + *

This subinterface of BeanFactory is not meant to be used in normal + * application code: stick to {@link org.springframework.beans.factory.BeanFactory} + * or {@link org.springframework.beans.factory.ListableBeanFactory} for + * typical use cases. + * + *

Integration code for other frameworks can leverage this interface to + * wire and populate existing bean instances that Spring does not control + * the lifecycle of. This is particularly useful for WebWork Actions and + * Tapestry Page objects, for example. + * + *

Note that this interface is not implemented by + * {@link org.springframework.context.ApplicationContext} facades, + * as it is hardly ever used by application code. That said, it is available + * from an application context too, accessible through ApplicationContext's + * {@link org.springframework.context.ApplicationContext#getAutowireCapableBeanFactory()} + * method. + * + *

You may also implement the {@link org.springframework.beans.factory.BeanFactoryAware} + * interface, which exposes the internal BeanFactory even when running in an + * ApplicationContext, to get access to an AutowireCapableBeanFactory: + * simply cast the passed-in BeanFactory to AutowireCapableBeanFactory. + * + * @author Juergen Hoeller + * @since 04.12.2003 + * @see org.springframework.beans.factory.BeanFactoryAware + * @see org.springframework.beans.factory.config.ConfigurableListableBeanFactory + * @see org.springframework.context.ApplicationContext#getAutowireCapableBeanFactory() + */ +public interface AutowireCapableBeanFactory extends BeanFactory { + + /** + * Constant that indicates no externally defined autowiring. Note that + * BeanFactoryAware etc and annotation-driven injection will still be applied. + * @see #createBean + * @see #autowire + * @see #autowireBeanProperties + */ + int AUTOWIRE_NO = 0; + + /** + * Constant that indicates autowiring bean properties by name + * (applying to all bean property setters). + * @see #createBean + * @see #autowire + * @see #autowireBeanProperties + */ + int AUTOWIRE_BY_NAME = 1; + + /** + * Constant that indicates autowiring bean properties by type + * (applying to all bean property setters). + * @see #createBean + * @see #autowire + * @see #autowireBeanProperties + */ + int AUTOWIRE_BY_TYPE = 2; + + /** + * Constant that indicates autowiring the greediest constructor that + * can be satisfied (involves resolving the appropriate constructor). + * @see #createBean + * @see #autowire + */ + int AUTOWIRE_CONSTRUCTOR = 3; + + /** + * Constant that indicates determining an appropriate autowire strategy + * through introspection of the bean class. + * @see #createBean + * @see #autowire + * @deprecated as of Spring 3.0: If you are using mixed autowiring strategies, + * prefer annotation-based autowiring for clearer demarcation of autowiring needs. + */ + @Deprecated + int AUTOWIRE_AUTODETECT = 4; + + /** + * Suffix for the "original instance" convention when initializing an existing + * bean instance: to be appended to the fully-qualified bean class name, + * e.g. "com.mypackage.MyClass.ORIGINAL", in order to enforce the given instance + * to be returned, i.e. no proxies etc. + * @since 5.1 + * @see #initializeBean(Object, String) + * @see #applyBeanPostProcessorsBeforeInitialization(Object, String) + * @see #applyBeanPostProcessorsAfterInitialization(Object, String) + */ + String ORIGINAL_INSTANCE_SUFFIX = ".ORIGINAL"; + + + //------------------------------------------------------------------------- + // Typical methods for creating and populating external bean instances + //------------------------------------------------------------------------- + + /** + * Fully create a new bean instance of the given class. + *

Performs full initialization of the bean, including all applicable + * {@link BeanPostProcessor BeanPostProcessors}. + *

Note: This is intended for creating a fresh instance, populating annotated + * fields and methods as well as applying all standard bean initialization callbacks. + * It does not imply traditional by-name or by-type autowiring of properties; + * use {@link #createBean(Class, int, boolean)} for those purposes. + * @param beanClass the class of the bean to create + * @return the new bean instance + * @throws BeansException if instantiation or wiring failed + */ + T createBean(Class beanClass) throws BeansException; + + /** + * Populate the given bean instance through applying after-instantiation callbacks + * and bean property post-processing (e.g. for annotation-driven injection). + *

Note: This is essentially intended for (re-)populating annotated fields and + * methods, either for new instances or for deserialized instances. It does + * not imply traditional by-name or by-type autowiring of properties; + * use {@link #autowireBeanProperties} for those purposes. + * @param existingBean the existing bean instance + * @throws BeansException if wiring failed + */ + void autowireBean(Object existingBean) throws BeansException; + + /** + * Configure the given raw bean: autowiring bean properties, applying + * bean property values, applying factory callbacks such as {@code setBeanName} + * and {@code setBeanFactory}, and also applying all bean post processors + * (including ones which might wrap the given raw bean). + *

This is effectively a superset of what {@link #initializeBean} provides, + * fully applying the configuration specified by the corresponding bean definition. + * Note: This method requires a bean definition for the given name! + * @param existingBean the existing bean instance + * @param beanName the name of the bean, to be passed to it if necessary + * (a bean definition of that name has to be available) + * @return the bean instance to use, either the original or a wrapped one + * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException + * if there is no bean definition with the given name + * @throws BeansException if the initialization failed + * @see #initializeBean + */ + Object configureBean(Object existingBean, String beanName) throws BeansException; + + + //------------------------------------------------------------------------- + // Specialized methods for fine-grained control over the bean lifecycle + //------------------------------------------------------------------------- + + /** + * Fully create a new bean instance of the given class with the specified + * autowire strategy. All constants defined in this interface are supported here. + *

Performs full initialization of the bean, including all applicable + * {@link BeanPostProcessor BeanPostProcessors}. This is effectively a superset + * of what {@link #autowire} provides, adding {@link #initializeBean} behavior. + * @param beanClass the class of the bean to create + * @param autowireMode by name or type, using the constants in this interface + * @param dependencyCheck whether to perform a dependency check for objects + * (not applicable to autowiring a constructor, thus ignored there) + * @return the new bean instance + * @throws BeansException if instantiation or wiring failed + * @see #AUTOWIRE_NO + * @see #AUTOWIRE_BY_NAME + * @see #AUTOWIRE_BY_TYPE + * @see #AUTOWIRE_CONSTRUCTOR + */ + Object createBean(Class beanClass, int autowireMode, boolean dependencyCheck) throws BeansException; + + /** + * Instantiate a new bean instance of the given class with the specified autowire + * strategy. All constants defined in this interface are supported here. + * Can also be invoked with {@code AUTOWIRE_NO} in order to just apply + * before-instantiation callbacks (e.g. for annotation-driven injection). + *

Does not apply standard {@link BeanPostProcessor BeanPostProcessors} + * callbacks or perform any further initialization of the bean. This interface + * offers distinct, fine-grained operations for those purposes, for example + * {@link #initializeBean}. However, {@link InstantiationAwareBeanPostProcessor} + * callbacks are applied, if applicable to the construction of the instance. + * @param beanClass the class of the bean to instantiate + * @param autowireMode by name or type, using the constants in this interface + * @param dependencyCheck whether to perform a dependency check for object + * references in the bean instance (not applicable to autowiring a constructor, + * thus ignored there) + * @return the new bean instance + * @throws BeansException if instantiation or wiring failed + * @see #AUTOWIRE_NO + * @see #AUTOWIRE_BY_NAME + * @see #AUTOWIRE_BY_TYPE + * @see #AUTOWIRE_CONSTRUCTOR + * @see #AUTOWIRE_AUTODETECT + * @see #initializeBean + * @see #applyBeanPostProcessorsBeforeInitialization + * @see #applyBeanPostProcessorsAfterInitialization + */ + Object autowire(Class beanClass, int autowireMode, boolean dependencyCheck) throws BeansException; + + /** + * Autowire the bean properties of the given bean instance by name or type. + * Can also be invoked with {@code AUTOWIRE_NO} in order to just apply + * after-instantiation callbacks (e.g. for annotation-driven injection). + *

Does not apply standard {@link BeanPostProcessor BeanPostProcessors} + * callbacks or perform any further initialization of the bean. This interface + * offers distinct, fine-grained operations for those purposes, for example + * {@link #initializeBean}. However, {@link InstantiationAwareBeanPostProcessor} + * callbacks are applied, if applicable to the configuration of the instance. + * @param existingBean the existing bean instance + * @param autowireMode by name or type, using the constants in this interface + * @param dependencyCheck whether to perform a dependency check for object + * references in the bean instance + * @throws BeansException if wiring failed + * @see #AUTOWIRE_BY_NAME + * @see #AUTOWIRE_BY_TYPE + * @see #AUTOWIRE_NO + */ + void autowireBeanProperties(Object existingBean, int autowireMode, boolean dependencyCheck) + throws BeansException; + + /** + * Apply the property values of the bean definition with the given name to + * the given bean instance. The bean definition can either define a fully + * self-contained bean, reusing its property values, or just property values + * meant to be used for existing bean instances. + *

This method does not autowire bean properties; it just applies + * explicitly defined property values. Use the {@link #autowireBeanProperties} + * method to autowire an existing bean instance. + * Note: This method requires a bean definition for the given name! + *

Does not apply standard {@link BeanPostProcessor BeanPostProcessors} + * callbacks or perform any further initialization of the bean. This interface + * offers distinct, fine-grained operations for those purposes, for example + * {@link #initializeBean}. However, {@link InstantiationAwareBeanPostProcessor} + * callbacks are applied, if applicable to the configuration of the instance. + * @param existingBean the existing bean instance + * @param beanName the name of the bean definition in the bean factory + * (a bean definition of that name has to be available) + * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException + * if there is no bean definition with the given name + * @throws BeansException if applying the property values failed + * @see #autowireBeanProperties + */ + void applyBeanPropertyValues(Object existingBean, String beanName) throws BeansException; + + /** + * Initialize the given raw bean, applying factory callbacks + * such as {@code setBeanName} and {@code setBeanFactory}, + * also applying all bean post processors (including ones which + * might wrap the given raw bean). + *

Note that no bean definition of the given name has to exist + * in the bean factory. The passed-in bean name will simply be used + * for callbacks but not checked against the registered bean definitions. + * @param existingBean the existing bean instance + * @param beanName the name of the bean, to be passed to it if necessary + * (only passed to {@link BeanPostProcessor BeanPostProcessors}; + * can follow the {@link #ORIGINAL_INSTANCE_SUFFIX} convention in order to + * enforce the given instance to be returned, i.e. no proxies etc) + * @return the bean instance to use, either the original or a wrapped one + * @throws BeansException if the initialization failed + * @see #ORIGINAL_INSTANCE_SUFFIX + */ + Object initializeBean(Object existingBean, String beanName) throws BeansException; + + /** + * Apply {@link BeanPostProcessor BeanPostProcessors} to the given existing bean + * instance, invoking their {@code postProcessBeforeInitialization} methods. + * The returned bean instance may be a wrapper around the original. + * @param existingBean the existing bean instance + * @param beanName the name of the bean, to be passed to it if necessary + * (only passed to {@link BeanPostProcessor BeanPostProcessors}; + * can follow the {@link #ORIGINAL_INSTANCE_SUFFIX} convention in order to + * enforce the given instance to be returned, i.e. no proxies etc) + * @return the bean instance to use, either the original or a wrapped one + * @throws BeansException if any post-processing failed + * @see BeanPostProcessor#postProcessBeforeInitialization + * @see #ORIGINAL_INSTANCE_SUFFIX + */ + Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName) + throws BeansException; + + /** + * Apply {@link BeanPostProcessor BeanPostProcessors} to the given existing bean + * instance, invoking their {@code postProcessAfterInitialization} methods. + * The returned bean instance may be a wrapper around the original. + * @param existingBean the existing bean instance + * @param beanName the name of the bean, to be passed to it if necessary + * (only passed to {@link BeanPostProcessor BeanPostProcessors}; + * can follow the {@link #ORIGINAL_INSTANCE_SUFFIX} convention in order to + * enforce the given instance to be returned, i.e. no proxies etc) + * @return the bean instance to use, either the original or a wrapped one + * @throws BeansException if any post-processing failed + * @see BeanPostProcessor#postProcessAfterInitialization + * @see #ORIGINAL_INSTANCE_SUFFIX + */ + Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName) + throws BeansException; + + /** + * Destroy the given bean instance (typically coming from {@link #createBean}), + * applying the {@link org.springframework.beans.factory.DisposableBean} contract as well as + * registered {@link DestructionAwareBeanPostProcessor DestructionAwareBeanPostProcessors}. + *

Any exception that arises during destruction should be caught + * and logged instead of propagated to the caller of this method. + * @param existingBean the bean instance to destroy + */ + void destroyBean(Object existingBean); + + + //------------------------------------------------------------------------- + // Delegate methods for resolving injection points + //------------------------------------------------------------------------- + + /** + * Resolve the bean instance that uniquely matches the given object type, if any, + * including its bean name. + *

This is effectively a variant of {@link #getBean(Class)} which preserves the + * bean name of the matching instance. + * @param requiredType type the bean must match; can be an interface or superclass + * @return the bean name plus bean instance + * @throws NoSuchBeanDefinitionException if no matching bean was found + * @throws NoUniqueBeanDefinitionException if more than one matching bean was found + * @throws BeansException if the bean could not be created + * @since 4.3.3 + * @see #getBean(Class) + */ + NamedBeanHolder resolveNamedBean(Class requiredType) throws BeansException; + + /** + * Resolve a bean instance for the given bean name, providing a dependency descriptor + * for exposure to target factory methods. + *

This is effectively a variant of {@link #getBean(String, Class)} which supports + * factory methods with an {@link org.springframework.beans.factory.InjectionPoint} + * argument. + * @param name the name of the bean to look up + * @param descriptor the dependency descriptor for the requesting injection point + * @return the corresponding bean instance + * @throws NoSuchBeanDefinitionException if there is no bean with the specified name + * @throws BeansException if the bean could not be created + * @since 5.1.5 + * @see #getBean(String, Class) + */ + Object resolveBeanByName(String name, DependencyDescriptor descriptor) throws BeansException; + + /** + * Resolve the specified dependency against the beans defined in this factory. + * @param descriptor the descriptor for the dependency (field/method/constructor) + * @param requestingBeanName the name of the bean which declares the given dependency + * @return the resolved object, or {@code null} if none found + * @throws NoSuchBeanDefinitionException if no matching bean was found + * @throws NoUniqueBeanDefinitionException if more than one matching bean was found + * @throws BeansException if dependency resolution failed for any other reason + * @since 2.5 + * @see #resolveDependency(DependencyDescriptor, String, Set, TypeConverter) + */ + @Nullable + Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName) throws BeansException; + + /** + * Resolve the specified dependency against the beans defined in this factory. + * @param descriptor the descriptor for the dependency (field/method/constructor) + * @param requestingBeanName the name of the bean which declares the given dependency + * @param autowiredBeanNames a Set that all names of autowired beans (used for + * resolving the given dependency) are supposed to be added to + * @param typeConverter the TypeConverter to use for populating arrays and collections + * @return the resolved object, or {@code null} if none found + * @throws NoSuchBeanDefinitionException if no matching bean was found + * @throws NoUniqueBeanDefinitionException if more than one matching bean was found + * @throws BeansException if dependency resolution failed for any other reason + * @since 2.5 + * @see DependencyDescriptor + */ + @Nullable + Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName, + @Nullable Set autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException; + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/AutowiredPropertyMarker.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/AutowiredPropertyMarker.java new file mode 100644 index 0000000..4d4f2c6 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/AutowiredPropertyMarker.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.io.Serializable; + +import org.springframework.lang.Nullable; + +/** + * Simple marker class for an individually autowired property value, to be added + * to {@link BeanDefinition#getPropertyValues()} for a specific bean property. + * + *

At runtime, this will be replaced with a {@link DependencyDescriptor} + * for the corresponding bean property's write method, eventually to be resolved + * through a {@link AutowireCapableBeanFactory#resolveDependency} step. + * + * @author Juergen Hoeller + * @since 5.2 + * @see AutowireCapableBeanFactory#resolveDependency + * @see BeanDefinition#getPropertyValues() + * @see org.springframework.beans.factory.support.BeanDefinitionBuilder#addAutowiredProperty + */ +@SuppressWarnings("serial") +public final class AutowiredPropertyMarker implements Serializable { + + /** + * The canonical instance for the autowired marker value. + */ + public static final Object INSTANCE = new AutowiredPropertyMarker(); + + + private AutowiredPropertyMarker() { + } + + private Object readResolve() { + return INSTANCE; + } + + + @Override + public boolean equals(@Nullable Object obj) { + return (this == obj); + } + + @Override + public int hashCode() { + return AutowiredPropertyMarker.class.hashCode(); + } + + @Override + public String toString() { + return "(autowired)"; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinition.java new file mode 100644 index 0000000..5be39a0 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinition.java @@ -0,0 +1,357 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import org.springframework.beans.BeanMetadataElement; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.core.AttributeAccessor; +import org.springframework.core.ResolvableType; +import org.springframework.lang.Nullable; + +/** + * A BeanDefinition describes a bean instance, which has property values, + * constructor argument values, and further information supplied by + * concrete implementations. + * + *

This is just a minimal interface: The main intention is to allow a + * {@link BeanFactoryPostProcessor} to introspect and modify property values + * and other bean metadata. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @since 19.03.2004 + * @see ConfigurableListableBeanFactory#getBeanDefinition + * @see org.springframework.beans.factory.support.RootBeanDefinition + * @see org.springframework.beans.factory.support.ChildBeanDefinition + */ +public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement { + + /** + * Scope identifier for the standard singleton scope: {@value}. + *

Note that extended bean factories might support further scopes. + * @see #setScope + * @see ConfigurableBeanFactory#SCOPE_SINGLETON + */ + String SCOPE_SINGLETON = ConfigurableBeanFactory.SCOPE_SINGLETON; + + /** + * Scope identifier for the standard prototype scope: {@value}. + *

Note that extended bean factories might support further scopes. + * @see #setScope + * @see ConfigurableBeanFactory#SCOPE_PROTOTYPE + */ + String SCOPE_PROTOTYPE = ConfigurableBeanFactory.SCOPE_PROTOTYPE; + + + /** + * Role hint indicating that a {@code BeanDefinition} is a major part + * of the application. Typically corresponds to a user-defined bean. + */ + int ROLE_APPLICATION = 0; + + /** + * Role hint indicating that a {@code BeanDefinition} is a supporting + * part of some larger configuration, typically an outer + * {@link org.springframework.beans.factory.parsing.ComponentDefinition}. + * {@code SUPPORT} beans are considered important enough to be aware + * of when looking more closely at a particular + * {@link org.springframework.beans.factory.parsing.ComponentDefinition}, + * but not when looking at the overall configuration of an application. + */ + int ROLE_SUPPORT = 1; + + /** + * Role hint indicating that a {@code BeanDefinition} is providing an + * entirely background role and has no relevance to the end-user. This hint is + * used when registering beans that are completely part of the internal workings + * of a {@link org.springframework.beans.factory.parsing.ComponentDefinition}. + */ + int ROLE_INFRASTRUCTURE = 2; + + + // Modifiable attributes + + /** + * Set the name of the parent definition of this bean definition, if any. + */ + void setParentName(@Nullable String parentName); + + /** + * Return the name of the parent definition of this bean definition, if any. + */ + @Nullable + String getParentName(); + + /** + * Specify the bean class name of this bean definition. + *

The class name can be modified during bean factory post-processing, + * typically replacing the original class name with a parsed variant of it. + * @see #setParentName + * @see #setFactoryBeanName + * @see #setFactoryMethodName + */ + void setBeanClassName(@Nullable String beanClassName); + + /** + * Return the current bean class name of this bean definition. + *

Note that this does not have to be the actual class name used at runtime, in + * case of a child definition overriding/inheriting the class name from its parent. + * Also, this may just be the class that a factory method is called on, or it may + * even be empty in case of a factory bean reference that a method is called on. + * Hence, do not consider this to be the definitive bean type at runtime but + * rather only use it for parsing purposes at the individual bean definition level. + * @see #getParentName() + * @see #getFactoryBeanName() + * @see #getFactoryMethodName() + */ + @Nullable + String getBeanClassName(); + + /** + * Override the target scope of this bean, specifying a new scope name. + * @see #SCOPE_SINGLETON + * @see #SCOPE_PROTOTYPE + */ + void setScope(@Nullable String scope); + + /** + * Return the name of the current target scope for this bean, + * or {@code null} if not known yet. + */ + @Nullable + String getScope(); + + /** + * Set whether this bean should be lazily initialized. + *

If {@code false}, the bean will get instantiated on startup by bean + * factories that perform eager initialization of singletons. + */ + void setLazyInit(boolean lazyInit); + + /** + * Return whether this bean should be lazily initialized, i.e. not + * eagerly instantiated on startup. Only applicable to a singleton bean. + */ + boolean isLazyInit(); + + /** + * Set the names of the beans that this bean depends on being initialized. + * The bean factory will guarantee that these beans get initialized first. + */ + void setDependsOn(@Nullable String... dependsOn); + + /** + * Return the bean names that this bean depends on. + */ + @Nullable + String[] getDependsOn(); + + /** + * Set whether this bean is a candidate for getting autowired into some other bean. + *

Note that this flag is designed to only affect type-based autowiring. + * It does not affect explicit references by name, which will get resolved even + * if the specified bean is not marked as an autowire candidate. As a consequence, + * autowiring by name will nevertheless inject a bean if the name matches. + */ + void setAutowireCandidate(boolean autowireCandidate); + + /** + * Return whether this bean is a candidate for getting autowired into some other bean. + */ + boolean isAutowireCandidate(); + + /** + * Set whether this bean is a primary autowire candidate. + *

If this value is {@code true} for exactly one bean among multiple + * matching candidates, it will serve as a tie-breaker. + */ + void setPrimary(boolean primary); + + /** + * Return whether this bean is a primary autowire candidate. + */ + boolean isPrimary(); + + /** + * Specify the factory bean to use, if any. + * This the name of the bean to call the specified factory method on. + * @see #setFactoryMethodName + */ + void setFactoryBeanName(@Nullable String factoryBeanName); + + /** + * Return the factory bean name, if any. + */ + @Nullable + String getFactoryBeanName(); + + /** + * Specify a factory method, if any. This method will be invoked with + * constructor arguments, or with no arguments if none are specified. + * The method will be invoked on the specified factory bean, if any, + * or otherwise as a static method on the local bean class. + * @see #setFactoryBeanName + * @see #setBeanClassName + */ + void setFactoryMethodName(@Nullable String factoryMethodName); + + /** + * Return a factory method, if any. + */ + @Nullable + String getFactoryMethodName(); + + /** + * Return the constructor argument values for this bean. + *

The returned instance can be modified during bean factory post-processing. + * @return the ConstructorArgumentValues object (never {@code null}) + */ + ConstructorArgumentValues getConstructorArgumentValues(); + + /** + * Return if there are constructor argument values defined for this bean. + * @since 5.0.2 + */ + default boolean hasConstructorArgumentValues() { + return !getConstructorArgumentValues().isEmpty(); + } + + /** + * Return the property values to be applied to a new instance of the bean. + *

The returned instance can be modified during bean factory post-processing. + * @return the MutablePropertyValues object (never {@code null}) + */ + MutablePropertyValues getPropertyValues(); + + /** + * Return if there are property values defined for this bean. + * @since 5.0.2 + */ + default boolean hasPropertyValues() { + return !getPropertyValues().isEmpty(); + } + + /** + * Set the name of the initializer method. + * @since 5.1 + */ + void setInitMethodName(@Nullable String initMethodName); + + /** + * Return the name of the initializer method. + * @since 5.1 + */ + @Nullable + String getInitMethodName(); + + /** + * Set the name of the destroy method. + * @since 5.1 + */ + void setDestroyMethodName(@Nullable String destroyMethodName); + + /** + * Return the name of the destroy method. + * @since 5.1 + */ + @Nullable + String getDestroyMethodName(); + + /** + * Set the role hint for this {@code BeanDefinition}. The role hint + * provides the frameworks as well as tools an indication of + * the role and importance of a particular {@code BeanDefinition}. + * @since 5.1 + * @see #ROLE_APPLICATION + * @see #ROLE_SUPPORT + * @see #ROLE_INFRASTRUCTURE + */ + void setRole(int role); + + /** + * Get the role hint for this {@code BeanDefinition}. The role hint + * provides the frameworks as well as tools an indication of + * the role and importance of a particular {@code BeanDefinition}. + * @see #ROLE_APPLICATION + * @see #ROLE_SUPPORT + * @see #ROLE_INFRASTRUCTURE + */ + int getRole(); + + /** + * Set a human-readable description of this bean definition. + * @since 5.1 + */ + void setDescription(@Nullable String description); + + /** + * Return a human-readable description of this bean definition. + */ + @Nullable + String getDescription(); + + + // Read-only attributes + + /** + * Return a resolvable type for this bean definition, + * based on the bean class or other specific metadata. + *

This is typically fully resolved on a runtime-merged bean definition + * but not necessarily on a configuration-time definition instance. + * @return the resolvable type (potentially {@link ResolvableType#NONE}) + * @since 5.2 + * @see ConfigurableBeanFactory#getMergedBeanDefinition + */ + ResolvableType getResolvableType(); + + /** + * Return whether this a Singleton, with a single, shared instance + * returned on all calls. + * @see #SCOPE_SINGLETON + */ + boolean isSingleton(); + + /** + * Return whether this a Prototype, with an independent instance + * returned for each call. + * @since 3.0 + * @see #SCOPE_PROTOTYPE + */ + boolean isPrototype(); + + /** + * Return whether this bean is "abstract", that is, not meant to be instantiated. + */ + boolean isAbstract(); + + /** + * Return a description of the resource that this bean definition + * came from (for the purpose of showing context in case of errors). + */ + @Nullable + String getResourceDescription(); + + /** + * Return the originating BeanDefinition, or {@code null} if none. + *

Allows for retrieving the decorated bean definition, if any. + *

Note that this method returns the immediate originator. Iterate through the + * originator chain to find the original BeanDefinition as defined by the user. + */ + @Nullable + BeanDefinition getOriginatingBeanDefinition(); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinitionCustomizer.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinitionCustomizer.java new file mode 100644 index 0000000..88d22c7 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinitionCustomizer.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +/** + * Callback for customizing a given bean definition. + * Designed for use with a lambda expression or method reference. + * + * @author Juergen Hoeller + * @since 5.0 + * @see org.springframework.beans.factory.support.BeanDefinitionBuilder#applyCustomizers + */ +@FunctionalInterface +public interface BeanDefinitionCustomizer { + + /** + * Customize the given bean definition. + */ + void customize(BeanDefinition bd); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinitionHolder.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinitionHolder.java new file mode 100644 index 0000000..d6767f7 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinitionHolder.java @@ -0,0 +1,188 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import org.springframework.beans.BeanMetadataElement; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Holder for a BeanDefinition with name and aliases. + * Can be registered as a placeholder for an inner bean. + * + *

Can also be used for programmatic registration of inner bean + * definitions. If you don't care about BeanNameAware and the like, + * registering RootBeanDefinition or ChildBeanDefinition is good enough. + * + * @author Juergen Hoeller + * @since 1.0.2 + * @see org.springframework.beans.factory.BeanNameAware + * @see org.springframework.beans.factory.support.RootBeanDefinition + * @see org.springframework.beans.factory.support.ChildBeanDefinition + */ +public class BeanDefinitionHolder implements BeanMetadataElement { + + private final BeanDefinition beanDefinition; + + private final String beanName; + + @Nullable + private final String[] aliases; + + + /** + * Create a new BeanDefinitionHolder. + * @param beanDefinition the BeanDefinition to wrap + * @param beanName the name of the bean, as specified for the bean definition + */ + public BeanDefinitionHolder(BeanDefinition beanDefinition, String beanName) { + this(beanDefinition, beanName, null); + } + + /** + * Create a new BeanDefinitionHolder. + * @param beanDefinition the BeanDefinition to wrap + * @param beanName the name of the bean, as specified for the bean definition + * @param aliases alias names for the bean, or {@code null} if none + */ + public BeanDefinitionHolder(BeanDefinition beanDefinition, String beanName, @Nullable String[] aliases) { + Assert.notNull(beanDefinition, "BeanDefinition must not be null"); + Assert.notNull(beanName, "Bean name must not be null"); + this.beanDefinition = beanDefinition; + this.beanName = beanName; + this.aliases = aliases; + } + + /** + * Copy constructor: Create a new BeanDefinitionHolder with the + * same contents as the given BeanDefinitionHolder instance. + *

Note: The wrapped BeanDefinition reference is taken as-is; + * it is {@code not} deeply copied. + * @param beanDefinitionHolder the BeanDefinitionHolder to copy + */ + public BeanDefinitionHolder(BeanDefinitionHolder beanDefinitionHolder) { + Assert.notNull(beanDefinitionHolder, "BeanDefinitionHolder must not be null"); + this.beanDefinition = beanDefinitionHolder.getBeanDefinition(); + this.beanName = beanDefinitionHolder.getBeanName(); + this.aliases = beanDefinitionHolder.getAliases(); + } + + + /** + * Return the wrapped BeanDefinition. + */ + public BeanDefinition getBeanDefinition() { + return this.beanDefinition; + } + + /** + * Return the primary name of the bean, as specified for the bean definition. + */ + public String getBeanName() { + return this.beanName; + } + + /** + * Return the alias names for the bean, as specified directly for the bean definition. + * @return the array of alias names, or {@code null} if none + */ + @Nullable + public String[] getAliases() { + return this.aliases; + } + + /** + * Expose the bean definition's source object. + * @see BeanDefinition#getSource() + */ + @Override + @Nullable + public Object getSource() { + return this.beanDefinition.getSource(); + } + + /** + * Determine whether the given candidate name matches the bean name + * or the aliases stored in this bean definition. + */ + public boolean matchesName(@Nullable String candidateName) { + return (candidateName != null && (candidateName.equals(this.beanName) || + candidateName.equals(BeanFactoryUtils.transformedBeanName(this.beanName)) || + ObjectUtils.containsElement(this.aliases, candidateName))); + } + + + /** + * Return a friendly, short description for the bean, stating name and aliases. + * @see #getBeanName() + * @see #getAliases() + */ + public String getShortDescription() { + if (this.aliases == null) { + return "Bean definition with name '" + this.beanName + "'"; + } + return "Bean definition with name '" + this.beanName + "' and aliases [" + StringUtils.arrayToCommaDelimitedString(this.aliases) + ']'; + } + + /** + * Return a long description for the bean, including name and aliases + * as well as a description of the contained {@link BeanDefinition}. + * @see #getShortDescription() + * @see #getBeanDefinition() + */ + public String getLongDescription() { + return getShortDescription() + ": " + this.beanDefinition; + } + + /** + * This implementation returns the long description. Can be overridden + * to return the short description or any kind of custom description instead. + * @see #getLongDescription() + * @see #getShortDescription() + */ + @Override + public String toString() { + return getLongDescription(); + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof BeanDefinitionHolder)) { + return false; + } + BeanDefinitionHolder otherHolder = (BeanDefinitionHolder) other; + return this.beanDefinition.equals(otherHolder.beanDefinition) && + this.beanName.equals(otherHolder.beanName) && + ObjectUtils.nullSafeEquals(this.aliases, otherHolder.aliases); + } + + @Override + public int hashCode() { + int hashCode = this.beanDefinition.hashCode(); + hashCode = 29 * hashCode + this.beanName.hashCode(); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.aliases); + return hashCode; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinitionVisitor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinitionVisitor.java new file mode 100644 index 0000000..7b826c5 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinitionVisitor.java @@ -0,0 +1,301 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.PropertyValue; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringValueResolver; + +/** + * Visitor class for traversing {@link BeanDefinition} objects, in particular + * the property values and constructor argument values contained in them, + * resolving bean metadata values. + * + *

Used by {@link PlaceholderConfigurerSupport} to parse all String values + * contained in a BeanDefinition, resolving any placeholders found. + * + * @author Juergen Hoeller + * @author Sam Brannen + * @since 1.2 + * @see BeanDefinition + * @see BeanDefinition#getPropertyValues + * @see BeanDefinition#getConstructorArgumentValues + * @see PlaceholderConfigurerSupport + */ +public class BeanDefinitionVisitor { + + @Nullable + private StringValueResolver valueResolver; + + + /** + * Create a new BeanDefinitionVisitor, applying the specified + * value resolver to all bean metadata values. + * @param valueResolver the StringValueResolver to apply + */ + public BeanDefinitionVisitor(StringValueResolver valueResolver) { + Assert.notNull(valueResolver, "StringValueResolver must not be null"); + this.valueResolver = valueResolver; + } + + /** + * Create a new BeanDefinitionVisitor for subclassing. + * Subclasses need to override the {@link #resolveStringValue} method. + */ + protected BeanDefinitionVisitor() { + } + + + /** + * Traverse the given BeanDefinition object and the MutablePropertyValues + * and ConstructorArgumentValues contained in them. + * @param beanDefinition the BeanDefinition object to traverse + * @see #resolveStringValue(String) + */ + public void visitBeanDefinition(BeanDefinition beanDefinition) { + visitParentName(beanDefinition); + visitBeanClassName(beanDefinition); + visitFactoryBeanName(beanDefinition); + visitFactoryMethodName(beanDefinition); + visitScope(beanDefinition); + if (beanDefinition.hasPropertyValues()) { + visitPropertyValues(beanDefinition.getPropertyValues()); + } + if (beanDefinition.hasConstructorArgumentValues()) { + ConstructorArgumentValues cas = beanDefinition.getConstructorArgumentValues(); + visitIndexedArgumentValues(cas.getIndexedArgumentValues()); + visitGenericArgumentValues(cas.getGenericArgumentValues()); + } + } + + protected void visitParentName(BeanDefinition beanDefinition) { + String parentName = beanDefinition.getParentName(); + if (parentName != null) { + String resolvedName = resolveStringValue(parentName); + if (!parentName.equals(resolvedName)) { + beanDefinition.setParentName(resolvedName); + } + } + } + + protected void visitBeanClassName(BeanDefinition beanDefinition) { + String beanClassName = beanDefinition.getBeanClassName(); + if (beanClassName != null) { + String resolvedName = resolveStringValue(beanClassName); + if (!beanClassName.equals(resolvedName)) { + beanDefinition.setBeanClassName(resolvedName); + } + } + } + + protected void visitFactoryBeanName(BeanDefinition beanDefinition) { + String factoryBeanName = beanDefinition.getFactoryBeanName(); + if (factoryBeanName != null) { + String resolvedName = resolveStringValue(factoryBeanName); + if (!factoryBeanName.equals(resolvedName)) { + beanDefinition.setFactoryBeanName(resolvedName); + } + } + } + + protected void visitFactoryMethodName(BeanDefinition beanDefinition) { + String factoryMethodName = beanDefinition.getFactoryMethodName(); + if (factoryMethodName != null) { + String resolvedName = resolveStringValue(factoryMethodName); + if (!factoryMethodName.equals(resolvedName)) { + beanDefinition.setFactoryMethodName(resolvedName); + } + } + } + + protected void visitScope(BeanDefinition beanDefinition) { + String scope = beanDefinition.getScope(); + if (scope != null) { + String resolvedScope = resolveStringValue(scope); + if (!scope.equals(resolvedScope)) { + beanDefinition.setScope(resolvedScope); + } + } + } + + protected void visitPropertyValues(MutablePropertyValues pvs) { + PropertyValue[] pvArray = pvs.getPropertyValues(); + for (PropertyValue pv : pvArray) { + Object newVal = resolveValue(pv.getValue()); + if (!ObjectUtils.nullSafeEquals(newVal, pv.getValue())) { + pvs.add(pv.getName(), newVal); + } + } + } + + protected void visitIndexedArgumentValues(Map ias) { + for (ConstructorArgumentValues.ValueHolder valueHolder : ias.values()) { + Object newVal = resolveValue(valueHolder.getValue()); + if (!ObjectUtils.nullSafeEquals(newVal, valueHolder.getValue())) { + valueHolder.setValue(newVal); + } + } + } + + protected void visitGenericArgumentValues(List gas) { + for (ConstructorArgumentValues.ValueHolder valueHolder : gas) { + Object newVal = resolveValue(valueHolder.getValue()); + if (!ObjectUtils.nullSafeEquals(newVal, valueHolder.getValue())) { + valueHolder.setValue(newVal); + } + } + } + + @SuppressWarnings("rawtypes") + @Nullable + protected Object resolveValue(@Nullable Object value) { + if (value instanceof BeanDefinition) { + visitBeanDefinition((BeanDefinition) value); + } + else if (value instanceof BeanDefinitionHolder) { + visitBeanDefinition(((BeanDefinitionHolder) value).getBeanDefinition()); + } + else if (value instanceof RuntimeBeanReference) { + RuntimeBeanReference ref = (RuntimeBeanReference) value; + String newBeanName = resolveStringValue(ref.getBeanName()); + if (newBeanName == null) { + return null; + } + if (!newBeanName.equals(ref.getBeanName())) { + return new RuntimeBeanReference(newBeanName); + } + } + else if (value instanceof RuntimeBeanNameReference) { + RuntimeBeanNameReference ref = (RuntimeBeanNameReference) value; + String newBeanName = resolveStringValue(ref.getBeanName()); + if (newBeanName == null) { + return null; + } + if (!newBeanName.equals(ref.getBeanName())) { + return new RuntimeBeanNameReference(newBeanName); + } + } + else if (value instanceof Object[]) { + visitArray((Object[]) value); + } + else if (value instanceof List) { + visitList((List) value); + } + else if (value instanceof Set) { + visitSet((Set) value); + } + else if (value instanceof Map) { + visitMap((Map) value); + } + else if (value instanceof TypedStringValue) { + TypedStringValue typedStringValue = (TypedStringValue) value; + String stringValue = typedStringValue.getValue(); + if (stringValue != null) { + String visitedString = resolveStringValue(stringValue); + typedStringValue.setValue(visitedString); + } + } + else if (value instanceof String) { + return resolveStringValue((String) value); + } + return value; + } + + protected void visitArray(Object[] arrayVal) { + for (int i = 0; i < arrayVal.length; i++) { + Object elem = arrayVal[i]; + Object newVal = resolveValue(elem); + if (!ObjectUtils.nullSafeEquals(newVal, elem)) { + arrayVal[i] = newVal; + } + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + protected void visitList(List listVal) { + for (int i = 0; i < listVal.size(); i++) { + Object elem = listVal.get(i); + Object newVal = resolveValue(elem); + if (!ObjectUtils.nullSafeEquals(newVal, elem)) { + listVal.set(i, newVal); + } + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + protected void visitSet(Set setVal) { + Set newContent = new LinkedHashSet(); + boolean entriesModified = false; + for (Object elem : setVal) { + int elemHash = (elem != null ? elem.hashCode() : 0); + Object newVal = resolveValue(elem); + int newValHash = (newVal != null ? newVal.hashCode() : 0); + newContent.add(newVal); + entriesModified = entriesModified || (newVal != elem || newValHash != elemHash); + } + if (entriesModified) { + setVal.clear(); + setVal.addAll(newContent); + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + protected void visitMap(Map mapVal) { + Map newContent = new LinkedHashMap(); + boolean entriesModified = false; + for (Map.Entry entry : mapVal.entrySet()) { + Object key = entry.getKey(); + int keyHash = (key != null ? key.hashCode() : 0); + Object newKey = resolveValue(key); + int newKeyHash = (newKey != null ? newKey.hashCode() : 0); + Object val = entry.getValue(); + Object newVal = resolveValue(val); + newContent.put(newKey, newVal); + entriesModified = entriesModified || (newVal != val || newKey != key || newKeyHash != keyHash); + } + if (entriesModified) { + mapVal.clear(); + mapVal.putAll(newContent); + } + } + + /** + * Resolve the given String value, for example parsing placeholders. + * @param strVal the original String value + * @return the resolved String value + */ + @Nullable + protected String resolveStringValue(String strVal) { + if (this.valueResolver == null) { + throw new IllegalStateException("No StringValueResolver specified - pass a resolver " + + "object into the constructor or override the 'resolveStringValue' method"); + } + String resolvedValue = this.valueResolver.resolveStringValue(strVal); + // Return original String if not modified. + return (strVal.equals(resolvedValue) ? strVal : resolvedValue); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanExpressionContext.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanExpressionContext.java new file mode 100644 index 0000000..e6e3839 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanExpressionContext.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Context object for evaluating an expression within a bean definition. + * + * @author Juergen Hoeller + * @since 3.0 + */ +public class BeanExpressionContext { + + private final ConfigurableBeanFactory beanFactory; + + @Nullable + private final Scope scope; + + + public BeanExpressionContext(ConfigurableBeanFactory beanFactory, @Nullable Scope scope) { + Assert.notNull(beanFactory, "BeanFactory must not be null"); + this.beanFactory = beanFactory; + this.scope = scope; + } + + public final ConfigurableBeanFactory getBeanFactory() { + return this.beanFactory; + } + + @Nullable + public final Scope getScope() { + return this.scope; + } + + + public boolean containsObject(String key) { + return (this.beanFactory.containsBean(key) || + (this.scope != null && this.scope.resolveContextualObject(key) != null)); + } + + @Nullable + public Object getObject(String key) { + if (this.beanFactory.containsBean(key)) { + return this.beanFactory.getBean(key); + } + else if (this.scope != null) { + return this.scope.resolveContextualObject(key); + } + else { + return null; + } + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof BeanExpressionContext)) { + return false; + } + BeanExpressionContext otherContext = (BeanExpressionContext) other; + return (this.beanFactory == otherContext.beanFactory && this.scope == otherContext.scope); + } + + @Override + public int hashCode() { + return this.beanFactory.hashCode(); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanExpressionResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanExpressionResolver.java new file mode 100644 index 0000000..fc55304 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanExpressionResolver.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import org.springframework.beans.BeansException; +import org.springframework.lang.Nullable; + +/** + * Strategy interface for resolving a value through evaluating it + * as an expression, if applicable. + * + *

A raw {@link org.springframework.beans.factory.BeanFactory} does not + * contain a default implementation of this strategy. However, + * {@link org.springframework.context.ApplicationContext} implementations + * will provide expression support out of the box. + * + * @author Juergen Hoeller + * @since 3.0 + */ +public interface BeanExpressionResolver { + + /** + * Evaluate the given value as an expression, if applicable; + * return the value as-is otherwise. + * @param value the value to check + * @param evalContext the evaluation context + * @return the resolved value (potentially the given value as-is) + * @throws BeansException if evaluation failed + */ + @Nullable + Object evaluate(@Nullable String value, BeanExpressionContext evalContext) throws BeansException; + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanFactoryPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanFactoryPostProcessor.java new file mode 100644 index 0000000..5db855f --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanFactoryPostProcessor.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import org.springframework.beans.BeansException; + +/** + * Factory hook that allows for custom modification of an application context's + * bean definitions, adapting the bean property values of the context's underlying + * bean factory. + * + *

Useful for custom config files targeted at system administrators that + * override bean properties configured in the application context. See + * {@link PropertyResourceConfigurer} and its concrete implementations for + * out-of-the-box solutions that address such configuration needs. + * + *

A {@code BeanFactoryPostProcessor} may interact with and modify bean + * definitions, but never bean instances. Doing so may cause premature bean + * instantiation, violating the container and causing unintended side-effects. + * If bean instance interaction is required, consider implementing + * {@link BeanPostProcessor} instead. + * + *

Registration

+ *

An {@code ApplicationContext} auto-detects {@code BeanFactoryPostProcessor} + * beans in its bean definitions and applies them before any other beans get created. + * A {@code BeanFactoryPostProcessor} may also be registered programmatically + * with a {@code ConfigurableApplicationContext}. + * + *

Ordering

+ *

{@code BeanFactoryPostProcessor} beans that are autodetected in an + * {@code ApplicationContext} will be ordered according to + * {@link org.springframework.core.PriorityOrdered} and + * {@link org.springframework.core.Ordered} semantics. In contrast, + * {@code BeanFactoryPostProcessor} beans that are registered programmatically + * with a {@code ConfigurableApplicationContext} will be applied in the order of + * registration; any ordering semantics expressed through implementing the + * {@code PriorityOrdered} or {@code Ordered} interface will be ignored for + * programmatically registered post-processors. Furthermore, the + * {@link org.springframework.core.annotation.Order @Order} annotation is not + * taken into account for {@code BeanFactoryPostProcessor} beans. + * + * @author Juergen Hoeller + * @author Sam Brannen + * @since 06.07.2003 + * @see BeanPostProcessor + * @see PropertyResourceConfigurer + */ +@FunctionalInterface +public interface BeanFactoryPostProcessor { + + /** + * Modify the application context's internal bean factory after its standard + * initialization. All bean definitions will have been loaded, but no beans + * will have been instantiated yet. This allows for overriding or adding + * properties even to eager-initializing beans. + * @param beanFactory the bean factory used by the application context + * @throws org.springframework.beans.BeansException in case of errors + */ + void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException; + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanPostProcessor.java new file mode 100644 index 0000000..7288aa4 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanPostProcessor.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import org.springframework.beans.BeansException; +import org.springframework.lang.Nullable; + +/** + * Factory hook that allows for custom modification of new bean instances — + * for example, checking for marker interfaces or wrapping beans with proxies. + * + *

Typically, post-processors that populate beans via marker interfaces + * or the like will implement {@link #postProcessBeforeInitialization}, + * while post-processors that wrap beans with proxies will normally + * implement {@link #postProcessAfterInitialization}. + * + *

Registration

+ *

An {@code ApplicationContext} can autodetect {@code BeanPostProcessor} beans + * in its bean definitions and apply those post-processors to any beans subsequently + * created. A plain {@code BeanFactory} allows for programmatic registration of + * post-processors, applying them to all beans created through the bean factory. + * + *

Ordering

+ *

{@code BeanPostProcessor} beans that are autodetected in an + * {@code ApplicationContext} will be ordered according to + * {@link org.springframework.core.PriorityOrdered} and + * {@link org.springframework.core.Ordered} semantics. In contrast, + * {@code BeanPostProcessor} beans that are registered programmatically with a + * {@code BeanFactory} will be applied in the order of registration; any ordering + * semantics expressed through implementing the + * {@code PriorityOrdered} or {@code Ordered} interface will be ignored for + * programmatically registered post-processors. Furthermore, the + * {@link org.springframework.core.annotation.Order @Order} annotation is not + * taken into account for {@code BeanPostProcessor} beans. + * + * @author Juergen Hoeller + * @author Sam Brannen + * @since 10.10.2003 + * @see InstantiationAwareBeanPostProcessor + * @see DestructionAwareBeanPostProcessor + * @see ConfigurableBeanFactory#addBeanPostProcessor + * @see BeanFactoryPostProcessor + */ +public interface BeanPostProcessor { + + /** + * Apply this {@code BeanPostProcessor} to the given new bean instance before any bean + * initialization callbacks (like InitializingBean's {@code afterPropertiesSet} + * or a custom init-method). The bean will already be populated with property values. + * The returned bean instance may be a wrapper around the original. + *

The default implementation returns the given {@code bean} as-is. + * @param bean the new bean instance + * @param beanName the name of the bean + * @return the bean instance to use, either the original or a wrapped one; + * if {@code null}, no subsequent BeanPostProcessors will be invoked + * @throws org.springframework.beans.BeansException in case of errors + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet + */ + @Nullable + default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + + /** + * Apply this {@code BeanPostProcessor} to the given new bean instance after any bean + * initialization callbacks (like InitializingBean's {@code afterPropertiesSet} + * or a custom init-method). The bean will already be populated with property values. + * The returned bean instance may be a wrapper around the original. + *

In case of a FactoryBean, this callback will be invoked for both the FactoryBean + * instance and the objects created by the FactoryBean (as of Spring 2.0). The + * post-processor can decide whether to apply to either the FactoryBean or created + * objects or both through corresponding {@code bean instanceof FactoryBean} checks. + *

This callback will also be invoked after a short-circuiting triggered by a + * {@link InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation} method, + * in contrast to all other {@code BeanPostProcessor} callbacks. + *

The default implementation returns the given {@code bean} as-is. + * @param bean the new bean instance + * @param beanName the name of the bean + * @return the bean instance to use, either the original or a wrapped one; + * if {@code null}, no subsequent BeanPostProcessors will be invoked + * @throws org.springframework.beans.BeansException in case of errors + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet + * @see org.springframework.beans.factory.FactoryBean + */ + @Nullable + default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableBeanFactory.java new file mode 100644 index 0000000..81f4d87 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableBeanFactory.java @@ -0,0 +1,423 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.beans.PropertyEditor; +import java.security.AccessControlContext; + +import org.springframework.beans.PropertyEditorRegistrar; +import org.springframework.beans.PropertyEditorRegistry; +import org.springframework.beans.TypeConverter; +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.HierarchicalBeanFactory; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.metrics.ApplicationStartup; +import org.springframework.lang.Nullable; +import org.springframework.util.StringValueResolver; + +/** + * Configuration interface to be implemented by most bean factories. Provides + * facilities to configure a bean factory, in addition to the bean factory + * client methods in the {@link org.springframework.beans.factory.BeanFactory} + * interface. + * + *

This bean factory interface is not meant to be used in normal application + * code: Stick to {@link org.springframework.beans.factory.BeanFactory} or + * {@link org.springframework.beans.factory.ListableBeanFactory} for typical + * needs. This extended interface is just meant to allow for framework-internal + * plug'n'play and for special access to bean factory configuration methods. + * + * @author Juergen Hoeller + * @since 03.11.2003 + * @see org.springframework.beans.factory.BeanFactory + * @see org.springframework.beans.factory.ListableBeanFactory + * @see ConfigurableListableBeanFactory + */ +public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, SingletonBeanRegistry { + + /** + * Scope identifier for the standard singleton scope: {@value}. + *

Custom scopes can be added via {@code registerScope}. + * @see #registerScope + */ + String SCOPE_SINGLETON = "singleton"; + + /** + * Scope identifier for the standard prototype scope: {@value}. + *

Custom scopes can be added via {@code registerScope}. + * @see #registerScope + */ + String SCOPE_PROTOTYPE = "prototype"; + + + /** + * Set the parent of this bean factory. + *

Note that the parent cannot be changed: It should only be set outside + * a constructor if it isn't available at the time of factory instantiation. + * @param parentBeanFactory the parent BeanFactory + * @throws IllegalStateException if this factory is already associated with + * a parent BeanFactory + * @see #getParentBeanFactory() + */ + void setParentBeanFactory(BeanFactory parentBeanFactory) throws IllegalStateException; + + /** + * Set the class loader to use for loading bean classes. + * Default is the thread context class loader. + *

Note that this class loader will only apply to bean definitions + * that do not carry a resolved bean class yet. This is the case as of + * Spring 2.0 by default: Bean definitions only carry bean class names, + * to be resolved once the factory processes the bean definition. + * @param beanClassLoader the class loader to use, + * or {@code null} to suggest the default class loader + */ + void setBeanClassLoader(@Nullable ClassLoader beanClassLoader); + + /** + * Return this factory's class loader for loading bean classes + * (only {@code null} if even the system ClassLoader isn't accessible). + * @see org.springframework.util.ClassUtils#forName(String, ClassLoader) + */ + @Nullable + ClassLoader getBeanClassLoader(); + + /** + * Specify a temporary ClassLoader to use for type matching purposes. + * Default is none, simply using the standard bean ClassLoader. + *

A temporary ClassLoader is usually just specified if + * load-time weaving is involved, to make sure that actual bean + * classes are loaded as lazily as possible. The temporary loader is + * then removed once the BeanFactory completes its bootstrap phase. + * @since 2.5 + */ + void setTempClassLoader(@Nullable ClassLoader tempClassLoader); + + /** + * Return the temporary ClassLoader to use for type matching purposes, + * if any. + * @since 2.5 + */ + @Nullable + ClassLoader getTempClassLoader(); + + /** + * Set whether to cache bean metadata such as given bean definitions + * (in merged fashion) and resolved bean classes. Default is on. + *

Turn this flag off to enable hot-refreshing of bean definition objects + * and in particular bean classes. If this flag is off, any creation of a bean + * instance will re-query the bean class loader for newly resolved classes. + */ + void setCacheBeanMetadata(boolean cacheBeanMetadata); + + /** + * Return whether to cache bean metadata such as given bean definitions + * (in merged fashion) and resolved bean classes. + */ + boolean isCacheBeanMetadata(); + + /** + * Specify the resolution strategy for expressions in bean definition values. + *

There is no expression support active in a BeanFactory by default. + * An ApplicationContext will typically set a standard expression strategy + * here, supporting "#{...}" expressions in a Unified EL compatible style. + * @since 3.0 + */ + void setBeanExpressionResolver(@Nullable BeanExpressionResolver resolver); + + /** + * Return the resolution strategy for expressions in bean definition values. + * @since 3.0 + */ + @Nullable + BeanExpressionResolver getBeanExpressionResolver(); + + /** + * Specify a Spring 3.0 ConversionService to use for converting + * property values, as an alternative to JavaBeans PropertyEditors. + * @since 3.0 + */ + void setConversionService(@Nullable ConversionService conversionService); + + /** + * Return the associated ConversionService, if any. + * @since 3.0 + */ + @Nullable + ConversionService getConversionService(); + + /** + * Add a PropertyEditorRegistrar to be applied to all bean creation processes. + *

Such a registrar creates new PropertyEditor instances and registers them + * on the given registry, fresh for each bean creation attempt. This avoids + * the need for synchronization on custom editors; hence, it is generally + * preferable to use this method instead of {@link #registerCustomEditor}. + * @param registrar the PropertyEditorRegistrar to register + */ + void addPropertyEditorRegistrar(PropertyEditorRegistrar registrar); + + /** + * Register the given custom property editor for all properties of the + * given type. To be invoked during factory configuration. + *

Note that this method will register a shared custom editor instance; + * access to that instance will be synchronized for thread-safety. It is + * generally preferable to use {@link #addPropertyEditorRegistrar} instead + * of this method, to avoid for the need for synchronization on custom editors. + * @param requiredType type of the property + * @param propertyEditorClass the {@link PropertyEditor} class to register + */ + void registerCustomEditor(Class requiredType, Class propertyEditorClass); + + /** + * Initialize the given PropertyEditorRegistry with the custom editors + * that have been registered with this BeanFactory. + * @param registry the PropertyEditorRegistry to initialize + */ + void copyRegisteredEditorsTo(PropertyEditorRegistry registry); + + /** + * Set a custom type converter that this BeanFactory should use for converting + * bean property values, constructor argument values, etc. + *

This will override the default PropertyEditor mechanism and hence make + * any custom editors or custom editor registrars irrelevant. + * @since 2.5 + * @see #addPropertyEditorRegistrar + * @see #registerCustomEditor + */ + void setTypeConverter(TypeConverter typeConverter); + + /** + * Obtain a type converter as used by this BeanFactory. This may be a fresh + * instance for each call, since TypeConverters are usually not thread-safe. + *

If the default PropertyEditor mechanism is active, the returned + * TypeConverter will be aware of all custom editors that have been registered. + * @since 2.5 + */ + TypeConverter getTypeConverter(); + + /** + * Add a String resolver for embedded values such as annotation attributes. + * @param valueResolver the String resolver to apply to embedded values + * @since 3.0 + */ + void addEmbeddedValueResolver(StringValueResolver valueResolver); + + /** + * Determine whether an embedded value resolver has been registered with this + * bean factory, to be applied through {@link #resolveEmbeddedValue(String)}. + * @since 4.3 + */ + boolean hasEmbeddedValueResolver(); + + /** + * Resolve the given embedded value, e.g. an annotation attribute. + * @param value the value to resolve + * @return the resolved value (may be the original value as-is) + * @since 3.0 + */ + @Nullable + String resolveEmbeddedValue(String value); + + /** + * Add a new BeanPostProcessor that will get applied to beans created + * by this factory. To be invoked during factory configuration. + *

Note: Post-processors submitted here will be applied in the order of + * registration; any ordering semantics expressed through implementing the + * {@link org.springframework.core.Ordered} interface will be ignored. Note + * that autodetected post-processors (e.g. as beans in an ApplicationContext) + * will always be applied after programmatically registered ones. + * @param beanPostProcessor the post-processor to register + */ + void addBeanPostProcessor(BeanPostProcessor beanPostProcessor); + + /** + * Return the current number of registered BeanPostProcessors, if any. + */ + int getBeanPostProcessorCount(); + + /** + * Register the given scope, backed by the given Scope implementation. + * @param scopeName the scope identifier + * @param scope the backing Scope implementation + */ + void registerScope(String scopeName, Scope scope); + + /** + * Return the names of all currently registered scopes. + *

This will only return the names of explicitly registered scopes. + * Built-in scopes such as "singleton" and "prototype" won't be exposed. + * @return the array of scope names, or an empty array if none + * @see #registerScope + */ + String[] getRegisteredScopeNames(); + + /** + * Return the Scope implementation for the given scope name, if any. + *

This will only return explicitly registered scopes. + * Built-in scopes such as "singleton" and "prototype" won't be exposed. + * @param scopeName the name of the scope + * @return the registered Scope implementation, or {@code null} if none + * @see #registerScope + */ + @Nullable + Scope getRegisteredScope(String scopeName); + + /** + * Set the {@code ApplicationStartup} for this bean factory. + *

This allows the application context to record metrics during application startup. + * @param applicationStartup the new application startup + * @since 5.3 + */ + void setApplicationStartup(ApplicationStartup applicationStartup); + + /** + * Return the {@code ApplicationStartup} for this bean factory. + * @since 5.3 + */ + ApplicationStartup getApplicationStartup(); + + /** + * Provides a security access control context relevant to this factory. + * @return the applicable AccessControlContext (never {@code null}) + * @since 3.0 + */ + AccessControlContext getAccessControlContext(); + + /** + * Copy all relevant configuration from the given other factory. + *

Should include all standard configuration settings as well as + * BeanPostProcessors, Scopes, and factory-specific internal settings. + * Should not include any metadata of actual bean definitions, + * such as BeanDefinition objects and bean name aliases. + * @param otherFactory the other BeanFactory to copy from + */ + void copyConfigurationFrom(ConfigurableBeanFactory otherFactory); + + /** + * Given a bean name, create an alias. We typically use this method to + * support names that are illegal within XML ids (used for bean names). + *

Typically invoked during factory configuration, but can also be + * used for runtime registration of aliases. Therefore, a factory + * implementation should synchronize alias access. + * @param beanName the canonical name of the target bean + * @param alias the alias to be registered for the bean + * @throws BeanDefinitionStoreException if the alias is already in use + */ + void registerAlias(String beanName, String alias) throws BeanDefinitionStoreException; + + /** + * Resolve all alias target names and aliases registered in this + * factory, applying the given StringValueResolver to them. + *

The value resolver may for example resolve placeholders + * in target bean names and even in alias names. + * @param valueResolver the StringValueResolver to apply + * @since 2.5 + */ + void resolveAliases(StringValueResolver valueResolver); + + /** + * Return a merged BeanDefinition for the given bean name, + * merging a child bean definition with its parent if necessary. + * Considers bean definitions in ancestor factories as well. + * @param beanName the name of the bean to retrieve the merged definition for + * @return a (potentially merged) BeanDefinition for the given bean + * @throws NoSuchBeanDefinitionException if there is no bean definition with the given name + * @since 2.5 + */ + BeanDefinition getMergedBeanDefinition(String beanName) throws NoSuchBeanDefinitionException; + + /** + * Determine whether the bean with the given name is a FactoryBean. + * @param name the name of the bean to check + * @return whether the bean is a FactoryBean + * ({@code false} means the bean exists but is not a FactoryBean) + * @throws NoSuchBeanDefinitionException if there is no bean with the given name + * @since 2.5 + */ + boolean isFactoryBean(String name) throws NoSuchBeanDefinitionException; + + /** + * Explicitly control the current in-creation status of the specified bean. + * For container-internal use only. + * @param beanName the name of the bean + * @param inCreation whether the bean is currently in creation + * @since 3.1 + */ + void setCurrentlyInCreation(String beanName, boolean inCreation); + + /** + * Determine whether the specified bean is currently in creation. + * @param beanName the name of the bean + * @return whether the bean is currently in creation + * @since 2.5 + */ + boolean isCurrentlyInCreation(String beanName); + + /** + * Register a dependent bean for the given bean, + * to be destroyed before the given bean is destroyed. + * @param beanName the name of the bean + * @param dependentBeanName the name of the dependent bean + * @since 2.5 + */ + void registerDependentBean(String beanName, String dependentBeanName); + + /** + * Return the names of all beans which depend on the specified bean, if any. + * @param beanName the name of the bean + * @return the array of dependent bean names, or an empty array if none + * @since 2.5 + */ + String[] getDependentBeans(String beanName); + + /** + * Return the names of all beans that the specified bean depends on, if any. + * @param beanName the name of the bean + * @return the array of names of beans which the bean depends on, + * or an empty array if none + * @since 2.5 + */ + String[] getDependenciesForBean(String beanName); + + /** + * Destroy the given bean instance (usually a prototype instance + * obtained from this factory) according to its bean definition. + *

Any exception that arises during destruction should be caught + * and logged instead of propagated to the caller of this method. + * @param beanName the name of the bean definition + * @param beanInstance the bean instance to destroy + */ + void destroyBean(String beanName, Object beanInstance); + + /** + * Destroy the specified scoped bean in the current target scope, if any. + *

Any exception that arises during destruction should be caught + * and logged instead of propagated to the caller of this method. + * @param beanName the name of the scoped bean + */ + void destroyScopedBean(String beanName); + + /** + * Destroy all singleton beans in this factory, including inner beans that have + * been registered as disposable. To be called on shutdown of a factory. + *

Any exception that arises during destruction should be caught + * and logged instead of propagated to the caller of this method. + */ + void destroySingletons(); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableListableBeanFactory.java new file mode 100644 index 0000000..3b4c8f5 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableListableBeanFactory.java @@ -0,0 +1,162 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.util.Iterator; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.lang.Nullable; + +/** + * Configuration interface to be implemented by most listable bean factories. + * In addition to {@link ConfigurableBeanFactory}, it provides facilities to + * analyze and modify bean definitions, and to pre-instantiate singletons. + * + *

This subinterface of {@link org.springframework.beans.factory.BeanFactory} + * is not meant to be used in normal application code: Stick to + * {@link org.springframework.beans.factory.BeanFactory} or + * {@link org.springframework.beans.factory.ListableBeanFactory} for typical + * use cases. This interface is just meant to allow for framework-internal + * plug'n'play even when needing access to bean factory configuration methods. + * + * @author Juergen Hoeller + * @since 03.11.2003 + * @see org.springframework.context.support.AbstractApplicationContext#getBeanFactory() + */ +public interface ConfigurableListableBeanFactory + extends ListableBeanFactory, AutowireCapableBeanFactory, ConfigurableBeanFactory { + + /** + * Ignore the given dependency type for autowiring: + * for example, String. Default is none. + * @param type the dependency type to ignore + */ + void ignoreDependencyType(Class type); + + /** + * Ignore the given dependency interface for autowiring. + *

This will typically be used by application contexts to register + * dependencies that are resolved in other ways, like BeanFactory through + * BeanFactoryAware or ApplicationContext through ApplicationContextAware. + *

By default, only the BeanFactoryAware interface is ignored. + * For further types to ignore, invoke this method for each type. + * @param ifc the dependency interface to ignore + * @see org.springframework.beans.factory.BeanFactoryAware + * @see org.springframework.context.ApplicationContextAware + */ + void ignoreDependencyInterface(Class ifc); + + /** + * Register a special dependency type with corresponding autowired value. + *

This is intended for factory/context references that are supposed + * to be autowirable but are not defined as beans in the factory: + * e.g. a dependency of type ApplicationContext resolved to the + * ApplicationContext instance that the bean is living in. + *

Note: There are no such default types registered in a plain BeanFactory, + * not even for the BeanFactory interface itself. + * @param dependencyType the dependency type to register. This will typically + * be a base interface such as BeanFactory, with extensions of it resolved + * as well if declared as an autowiring dependency (e.g. ListableBeanFactory), + * as long as the given value actually implements the extended interface. + * @param autowiredValue the corresponding autowired value. This may also be an + * implementation of the {@link org.springframework.beans.factory.ObjectFactory} + * interface, which allows for lazy resolution of the actual target value. + */ + void registerResolvableDependency(Class dependencyType, @Nullable Object autowiredValue); + + /** + * Determine whether the specified bean qualifies as an autowire candidate, + * to be injected into other beans which declare a dependency of matching type. + *

This method checks ancestor factories as well. + * @param beanName the name of the bean to check + * @param descriptor the descriptor of the dependency to resolve + * @return whether the bean should be considered as autowire candidate + * @throws NoSuchBeanDefinitionException if there is no bean with the given name + */ + boolean isAutowireCandidate(String beanName, DependencyDescriptor descriptor) + throws NoSuchBeanDefinitionException; + + /** + * Return the registered BeanDefinition for the specified bean, allowing access + * to its property values and constructor argument value (which can be + * modified during bean factory post-processing). + *

A returned BeanDefinition object should not be a copy but the original + * definition object as registered in the factory. This means that it should + * be castable to a more specific implementation type, if necessary. + *

NOTE: This method does not consider ancestor factories. + * It is only meant for accessing local bean definitions of this factory. + * @param beanName the name of the bean + * @return the registered BeanDefinition + * @throws NoSuchBeanDefinitionException if there is no bean with the given name + * defined in this factory + */ + BeanDefinition getBeanDefinition(String beanName) throws NoSuchBeanDefinitionException; + + /** + * Return a unified view over all bean names managed by this factory. + *

Includes bean definition names as well as names of manually registered + * singleton instances, with bean definition names consistently coming first, + * analogous to how type/annotation specific retrieval of bean names works. + * @return the composite iterator for the bean names view + * @since 4.1.2 + * @see #containsBeanDefinition + * @see #registerSingleton + * @see #getBeanNamesForType + * @see #getBeanNamesForAnnotation + */ + Iterator getBeanNamesIterator(); + + /** + * Clear the merged bean definition cache, removing entries for beans + * which are not considered eligible for full metadata caching yet. + *

Typically triggered after changes to the original bean definitions, + * e.g. after applying a {@link BeanFactoryPostProcessor}. Note that metadata + * for beans which have already been created at this point will be kept around. + * @since 4.2 + * @see #getBeanDefinition + * @see #getMergedBeanDefinition + */ + void clearMetadataCache(); + + /** + * Freeze all bean definitions, signalling that the registered bean definitions + * will not be modified or post-processed any further. + *

This allows the factory to aggressively cache bean definition metadata. + */ + void freezeConfiguration(); + + /** + * Return whether this factory's bean definitions are frozen, + * i.e. are not supposed to be modified or post-processed any further. + * @return {@code true} if the factory's configuration is considered frozen + */ + boolean isConfigurationFrozen(); + + /** + * Ensure that all non-lazy-init singletons are instantiated, also considering + * {@link org.springframework.beans.factory.FactoryBean FactoryBeans}. + * Typically invoked at the end of factory setup, if desired. + * @throws BeansException if one of the singleton beans could not be created. + * Note: This may have left the factory with some beans already initialized! + * Call {@link #destroySingletons()} for full cleanup in this case. + * @see #destroySingletons() + */ + void preInstantiateSingletons() throws BeansException; + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/ConstructorArgumentValues.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/ConstructorArgumentValues.java new file mode 100644 index 0000000..4fbfb43 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/ConstructorArgumentValues.java @@ -0,0 +1,605 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.beans.BeanMetadataElement; +import org.springframework.beans.Mergeable; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; + +/** + * Holder for constructor argument values, typically as part of a bean definition. + * + *

Supports values for a specific index in the constructor argument list + * as well as for generic argument matches by type. + * + * @author Juergen Hoeller + * @since 09.11.2003 + * @see BeanDefinition#getConstructorArgumentValues + */ +public class ConstructorArgumentValues { + + private final Map indexedArgumentValues = new LinkedHashMap<>(); + + private final List genericArgumentValues = new ArrayList<>(); + + + /** + * Create a new empty ConstructorArgumentValues object. + */ + public ConstructorArgumentValues() { + } + + /** + * Deep copy constructor. + * @param original the ConstructorArgumentValues to copy + */ + public ConstructorArgumentValues(ConstructorArgumentValues original) { + addArgumentValues(original); + } + + + /** + * Copy all given argument values into this object, using separate holder + * instances to keep the values independent from the original object. + *

Note: Identical ValueHolder instances will only be registered once, + * to allow for merging and re-merging of argument value definitions. Distinct + * ValueHolder instances carrying the same content are of course allowed. + */ + public void addArgumentValues(@Nullable ConstructorArgumentValues other) { + if (other != null) { + other.indexedArgumentValues.forEach( + (index, argValue) -> addOrMergeIndexedArgumentValue(index, argValue.copy()) + ); + other.genericArgumentValues.stream() + .filter(valueHolder -> !this.genericArgumentValues.contains(valueHolder)) + .forEach(valueHolder -> addOrMergeGenericArgumentValue(valueHolder.copy())); + } + } + + + /** + * Add an argument value for the given index in the constructor argument list. + * @param index the index in the constructor argument list + * @param value the argument value + */ + public void addIndexedArgumentValue(int index, @Nullable Object value) { + addIndexedArgumentValue(index, new ValueHolder(value)); + } + + /** + * Add an argument value for the given index in the constructor argument list. + * @param index the index in the constructor argument list + * @param value the argument value + * @param type the type of the constructor argument + */ + public void addIndexedArgumentValue(int index, @Nullable Object value, String type) { + addIndexedArgumentValue(index, new ValueHolder(value, type)); + } + + /** + * Add an argument value for the given index in the constructor argument list. + * @param index the index in the constructor argument list + * @param newValue the argument value in the form of a ValueHolder + */ + public void addIndexedArgumentValue(int index, ValueHolder newValue) { + Assert.isTrue(index >= 0, "Index must not be negative"); + Assert.notNull(newValue, "ValueHolder must not be null"); + addOrMergeIndexedArgumentValue(index, newValue); + } + + /** + * Add an argument value for the given index in the constructor argument list, + * merging the new value (typically a collection) with the current value + * if demanded: see {@link org.springframework.beans.Mergeable}. + * @param key the index in the constructor argument list + * @param newValue the argument value in the form of a ValueHolder + */ + private void addOrMergeIndexedArgumentValue(Integer key, ValueHolder newValue) { + ValueHolder currentValue = this.indexedArgumentValues.get(key); + if (currentValue != null && newValue.getValue() instanceof Mergeable) { + Mergeable mergeable = (Mergeable) newValue.getValue(); + if (mergeable.isMergeEnabled()) { + newValue.setValue(mergeable.merge(currentValue.getValue())); + } + } + this.indexedArgumentValues.put(key, newValue); + } + + /** + * Check whether an argument value has been registered for the given index. + * @param index the index in the constructor argument list + */ + public boolean hasIndexedArgumentValue(int index) { + return this.indexedArgumentValues.containsKey(index); + } + + /** + * Get argument value for the given index in the constructor argument list. + * @param index the index in the constructor argument list + * @param requiredType the type to match (can be {@code null} to match + * untyped values only) + * @return the ValueHolder for the argument, or {@code null} if none set + */ + @Nullable + public ValueHolder getIndexedArgumentValue(int index, @Nullable Class requiredType) { + return getIndexedArgumentValue(index, requiredType, null); + } + + /** + * Get argument value for the given index in the constructor argument list. + * @param index the index in the constructor argument list + * @param requiredType the type to match (can be {@code null} to match + * untyped values only) + * @param requiredName the type to match (can be {@code null} to match + * unnamed values only, or empty String to match any name) + * @return the ValueHolder for the argument, or {@code null} if none set + */ + @Nullable + public ValueHolder getIndexedArgumentValue(int index, @Nullable Class requiredType, @Nullable String requiredName) { + Assert.isTrue(index >= 0, "Index must not be negative"); + ValueHolder valueHolder = this.indexedArgumentValues.get(index); + if (valueHolder != null && + (valueHolder.getType() == null || (requiredType != null && + ClassUtils.matchesTypeName(requiredType, valueHolder.getType()))) && + (valueHolder.getName() == null || (requiredName != null && + (requiredName.isEmpty() || requiredName.equals(valueHolder.getName()))))) { + return valueHolder; + } + return null; + } + + /** + * Return the map of indexed argument values. + * @return unmodifiable Map with Integer index as key and ValueHolder as value + * @see ValueHolder + */ + public Map getIndexedArgumentValues() { + return Collections.unmodifiableMap(this.indexedArgumentValues); + } + + + /** + * Add a generic argument value to be matched by type. + *

Note: A single generic argument value will just be used once, + * rather than matched multiple times. + * @param value the argument value + */ + public void addGenericArgumentValue(Object value) { + this.genericArgumentValues.add(new ValueHolder(value)); + } + + /** + * Add a generic argument value to be matched by type. + *

Note: A single generic argument value will just be used once, + * rather than matched multiple times. + * @param value the argument value + * @param type the type of the constructor argument + */ + public void addGenericArgumentValue(Object value, String type) { + this.genericArgumentValues.add(new ValueHolder(value, type)); + } + + /** + * Add a generic argument value to be matched by type or name (if available). + *

Note: A single generic argument value will just be used once, + * rather than matched multiple times. + * @param newValue the argument value in the form of a ValueHolder + *

Note: Identical ValueHolder instances will only be registered once, + * to allow for merging and re-merging of argument value definitions. Distinct + * ValueHolder instances carrying the same content are of course allowed. + */ + public void addGenericArgumentValue(ValueHolder newValue) { + Assert.notNull(newValue, "ValueHolder must not be null"); + if (!this.genericArgumentValues.contains(newValue)) { + addOrMergeGenericArgumentValue(newValue); + } + } + + /** + * Add a generic argument value, merging the new value (typically a collection) + * with the current value if demanded: see {@link org.springframework.beans.Mergeable}. + * @param newValue the argument value in the form of a ValueHolder + */ + private void addOrMergeGenericArgumentValue(ValueHolder newValue) { + if (newValue.getName() != null) { + for (Iterator it = this.genericArgumentValues.iterator(); it.hasNext();) { + ValueHolder currentValue = it.next(); + if (newValue.getName().equals(currentValue.getName())) { + if (newValue.getValue() instanceof Mergeable) { + Mergeable mergeable = (Mergeable) newValue.getValue(); + if (mergeable.isMergeEnabled()) { + newValue.setValue(mergeable.merge(currentValue.getValue())); + } + } + it.remove(); + } + } + } + this.genericArgumentValues.add(newValue); + } + + /** + * Look for a generic argument value that matches the given type. + * @param requiredType the type to match + * @return the ValueHolder for the argument, or {@code null} if none set + */ + @Nullable + public ValueHolder getGenericArgumentValue(Class requiredType) { + return getGenericArgumentValue(requiredType, null, null); + } + + /** + * Look for a generic argument value that matches the given type. + * @param requiredType the type to match + * @param requiredName the name to match + * @return the ValueHolder for the argument, or {@code null} if none set + */ + @Nullable + public ValueHolder getGenericArgumentValue(Class requiredType, String requiredName) { + return getGenericArgumentValue(requiredType, requiredName, null); + } + + /** + * Look for the next generic argument value that matches the given type, + * ignoring argument values that have already been used in the current + * resolution process. + * @param requiredType the type to match (can be {@code null} to find + * an arbitrary next generic argument value) + * @param requiredName the name to match (can be {@code null} to not + * match argument values by name, or empty String to match any name) + * @param usedValueHolders a Set of ValueHolder objects that have already been used + * in the current resolution process and should therefore not be returned again + * @return the ValueHolder for the argument, or {@code null} if none found + */ + @Nullable + public ValueHolder getGenericArgumentValue(@Nullable Class requiredType, @Nullable String requiredName, + @Nullable Set usedValueHolders) { + + for (ValueHolder valueHolder : this.genericArgumentValues) { + if (usedValueHolders != null && usedValueHolders.contains(valueHolder)) { + continue; + } + if (valueHolder.getName() != null && (requiredName == null || + (!requiredName.isEmpty() && !requiredName.equals(valueHolder.getName())))) { + continue; + } + if (valueHolder.getType() != null && (requiredType == null || + !ClassUtils.matchesTypeName(requiredType, valueHolder.getType()))) { + continue; + } + if (requiredType != null && valueHolder.getType() == null && valueHolder.getName() == null && + !ClassUtils.isAssignableValue(requiredType, valueHolder.getValue())) { + continue; + } + return valueHolder; + } + return null; + } + + /** + * Return the list of generic argument values. + * @return unmodifiable List of ValueHolders + * @see ValueHolder + */ + public List getGenericArgumentValues() { + return Collections.unmodifiableList(this.genericArgumentValues); + } + + + /** + * Look for an argument value that either corresponds to the given index + * in the constructor argument list or generically matches by type. + * @param index the index in the constructor argument list + * @param requiredType the parameter type to match + * @return the ValueHolder for the argument, or {@code null} if none set + */ + @Nullable + public ValueHolder getArgumentValue(int index, Class requiredType) { + return getArgumentValue(index, requiredType, null, null); + } + + /** + * Look for an argument value that either corresponds to the given index + * in the constructor argument list or generically matches by type. + * @param index the index in the constructor argument list + * @param requiredType the parameter type to match + * @param requiredName the parameter name to match + * @return the ValueHolder for the argument, or {@code null} if none set + */ + @Nullable + public ValueHolder getArgumentValue(int index, Class requiredType, String requiredName) { + return getArgumentValue(index, requiredType, requiredName, null); + } + + /** + * Look for an argument value that either corresponds to the given index + * in the constructor argument list or generically matches by type. + * @param index the index in the constructor argument list + * @param requiredType the parameter type to match (can be {@code null} + * to find an untyped argument value) + * @param requiredName the parameter name to match (can be {@code null} + * to find an unnamed argument value, or empty String to match any name) + * @param usedValueHolders a Set of ValueHolder objects that have already + * been used in the current resolution process and should therefore not + * be returned again (allowing to return the next generic argument match + * in case of multiple generic argument values of the same type) + * @return the ValueHolder for the argument, or {@code null} if none set + */ + @Nullable + public ValueHolder getArgumentValue(int index, @Nullable Class requiredType, @Nullable String requiredName, @Nullable Set usedValueHolders) { + Assert.isTrue(index >= 0, "Index must not be negative"); + ValueHolder valueHolder = getIndexedArgumentValue(index, requiredType, requiredName); + if (valueHolder == null) { + valueHolder = getGenericArgumentValue(requiredType, requiredName, usedValueHolders); + } + return valueHolder; + } + + /** + * Return the number of argument values held in this instance, + * counting both indexed and generic argument values. + */ + public int getArgumentCount() { + return (this.indexedArgumentValues.size() + this.genericArgumentValues.size()); + } + + /** + * Return if this holder does not contain any argument values, + * neither indexed ones nor generic ones. + */ + public boolean isEmpty() { + return (this.indexedArgumentValues.isEmpty() && this.genericArgumentValues.isEmpty()); + } + + /** + * Clear this holder, removing all argument values. + */ + public void clear() { + this.indexedArgumentValues.clear(); + this.genericArgumentValues.clear(); + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof ConstructorArgumentValues)) { + return false; + } + ConstructorArgumentValues that = (ConstructorArgumentValues) other; + if (this.genericArgumentValues.size() != that.genericArgumentValues.size() || + this.indexedArgumentValues.size() != that.indexedArgumentValues.size()) { + return false; + } + Iterator it1 = this.genericArgumentValues.iterator(); + Iterator it2 = that.genericArgumentValues.iterator(); + while (it1.hasNext() && it2.hasNext()) { + ValueHolder vh1 = it1.next(); + ValueHolder vh2 = it2.next(); + if (!vh1.contentEquals(vh2)) { + return false; + } + } + for (Map.Entry entry : this.indexedArgumentValues.entrySet()) { + ValueHolder vh1 = entry.getValue(); + ValueHolder vh2 = that.indexedArgumentValues.get(entry.getKey()); + if (vh2 == null || !vh1.contentEquals(vh2)) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + int hashCode = 7; + for (ValueHolder valueHolder : this.genericArgumentValues) { + hashCode = 31 * hashCode + valueHolder.contentHashCode(); + } + hashCode = 29 * hashCode; + for (Map.Entry entry : this.indexedArgumentValues.entrySet()) { + hashCode = 31 * hashCode + (entry.getValue().contentHashCode() ^ entry.getKey().hashCode()); + } + return hashCode; + } + + + /** + * Holder for a constructor argument value, with an optional type + * attribute indicating the target type of the actual constructor argument. + */ + public static class ValueHolder implements BeanMetadataElement { + + @Nullable + private Object value; + + @Nullable + private String type; + + @Nullable + private String name; + + @Nullable + private Object source; + + private boolean converted = false; + + @Nullable + private Object convertedValue; + + /** + * Create a new ValueHolder for the given value. + * @param value the argument value + */ + public ValueHolder(@Nullable Object value) { + this.value = value; + } + + /** + * Create a new ValueHolder for the given value and type. + * @param value the argument value + * @param type the type of the constructor argument + */ + public ValueHolder(@Nullable Object value, @Nullable String type) { + this.value = value; + this.type = type; + } + + /** + * Create a new ValueHolder for the given value, type and name. + * @param value the argument value + * @param type the type of the constructor argument + * @param name the name of the constructor argument + */ + public ValueHolder(@Nullable Object value, @Nullable String type, @Nullable String name) { + this.value = value; + this.type = type; + this.name = name; + } + + /** + * Set the value for the constructor argument. + */ + public void setValue(@Nullable Object value) { + this.value = value; + } + + /** + * Return the value for the constructor argument. + */ + @Nullable + public Object getValue() { + return this.value; + } + + /** + * Set the type of the constructor argument. + */ + public void setType(@Nullable String type) { + this.type = type; + } + + /** + * Return the type of the constructor argument. + */ + @Nullable + public String getType() { + return this.type; + } + + /** + * Set the name of the constructor argument. + */ + public void setName(@Nullable String name) { + this.name = name; + } + + /** + * Return the name of the constructor argument. + */ + @Nullable + public String getName() { + return this.name; + } + + /** + * Set the configuration source {@code Object} for this metadata element. + *

The exact type of the object will depend on the configuration mechanism used. + */ + public void setSource(@Nullable Object source) { + this.source = source; + } + + @Override + @Nullable + public Object getSource() { + return this.source; + } + + /** + * Return whether this holder contains a converted value already ({@code true}), + * or whether the value still needs to be converted ({@code false}). + */ + public synchronized boolean isConverted() { + return this.converted; + } + + /** + * Set the converted value of the constructor argument, + * after processed type conversion. + */ + public synchronized void setConvertedValue(@Nullable Object value) { + this.converted = (value != null); + this.convertedValue = value; + } + + /** + * Return the converted value of the constructor argument, + * after processed type conversion. + */ + @Nullable + public synchronized Object getConvertedValue() { + return this.convertedValue; + } + + /** + * Determine whether the content of this ValueHolder is equal + * to the content of the given other ValueHolder. + *

Note that ValueHolder does not implement {@code equals} + * directly, to allow for multiple ValueHolder instances with the + * same content to reside in the same Set. + */ + private boolean contentEquals(ValueHolder other) { + return (this == other || + (ObjectUtils.nullSafeEquals(this.value, other.value) && ObjectUtils.nullSafeEquals(this.type, other.type))); + } + + /** + * Determine whether the hash code of the content of this ValueHolder. + *

Note that ValueHolder does not implement {@code hashCode} + * directly, to allow for multiple ValueHolder instances with the + * same content to reside in the same Set. + */ + private int contentHashCode() { + return ObjectUtils.nullSafeHashCode(this.value) * 29 + ObjectUtils.nullSafeHashCode(this.type); + } + + /** + * Create a copy of this ValueHolder: that is, an independent + * ValueHolder instance with the same contents. + */ + public ValueHolder copy() { + ValueHolder copy = new ValueHolder(this.value, this.type, this.name); + copy.setSource(this.source); + return copy; + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/CustomEditorConfigurer.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/CustomEditorConfigurer.java new file mode 100644 index 0000000..9250915 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/CustomEditorConfigurer.java @@ -0,0 +1,155 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.beans.PropertyEditor; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeansException; +import org.springframework.beans.PropertyEditorRegistrar; +import org.springframework.core.Ordered; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * {@link BeanFactoryPostProcessor} implementation that allows for convenient + * registration of custom {@link PropertyEditor property editors}. + * + *

In case you want to register {@link PropertyEditor} instances, + * the recommended usage as of Spring 2.0 is to use custom + * {@link PropertyEditorRegistrar} implementations that in turn register any + * desired editor instances on a given + * {@link org.springframework.beans.PropertyEditorRegistry registry}. Each + * PropertyEditorRegistrar can register any number of custom editors. + * + *

+ * <bean id="customEditorConfigurer" class="org.springframework.beans.factory.config.CustomEditorConfigurer">
+ *   <property name="propertyEditorRegistrars">
+ *     <list>
+ *       <bean class="mypackage.MyCustomDateEditorRegistrar"/>
+ *       <bean class="mypackage.MyObjectEditorRegistrar"/>
+ *     </list>
+ *   </property>
+ * </bean>
+ * 
+ * + *

+ * It's perfectly fine to register {@link PropertyEditor} classes via + * the {@code customEditors} property. Spring will create fresh instances of + * them for each editing attempt then: + * + *

+ * <bean id="customEditorConfigurer" class="org.springframework.beans.factory.config.CustomEditorConfigurer">
+ *   <property name="customEditors">
+ *     <map>
+ *       <entry key="java.util.Date" value="mypackage.MyCustomDateEditor"/>
+ *       <entry key="mypackage.MyObject" value="mypackage.MyObjectEditor"/>
+ *     </map>
+ *   </property>
+ * </bean>
+ * 
+ * + *

+ * Note, that you shouldn't register {@link PropertyEditor} bean instances via + * the {@code customEditors} property as {@link PropertyEditor PropertyEditors} are stateful + * and the instances will then have to be synchronized for every editing + * attempt. In case you need control over the instantiation process of + * {@link PropertyEditor PropertyEditors}, use a {@link PropertyEditorRegistrar} to register + * them. + * + *

+ * Also supports "java.lang.String[]"-style array class names and primitive + * class names (e.g. "boolean"). Delegates to {@link ClassUtils} for actual + * class name resolution. + * + *

NOTE: Custom property editors registered with this configurer do + * not apply to data binding. Custom editors for data binding need to + * be registered on the {@link org.springframework.validation.DataBinder}: + * Use a common base class or delegate to common PropertyEditorRegistrar + * implementations to reuse editor registration there. + * + * @author Juergen Hoeller + * @since 27.02.2004 + * @see java.beans.PropertyEditor + * @see org.springframework.beans.PropertyEditorRegistrar + * @see ConfigurableBeanFactory#addPropertyEditorRegistrar + * @see ConfigurableBeanFactory#registerCustomEditor + * @see org.springframework.validation.DataBinder#registerCustomEditor + */ +public class CustomEditorConfigurer implements BeanFactoryPostProcessor, Ordered { + + protected final Log logger = LogFactory.getLog(getClass()); + + private int order = Ordered.LOWEST_PRECEDENCE; // default: same as non-Ordered + + @Nullable + private PropertyEditorRegistrar[] propertyEditorRegistrars; + + @Nullable + private Map, Class> customEditors; + + + public void setOrder(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return this.order; + } + + /** + * Specify the {@link PropertyEditorRegistrar PropertyEditorRegistrars} + * to apply to beans defined within the current application context. + *

This allows for sharing {@code PropertyEditorRegistrars} with + * {@link org.springframework.validation.DataBinder DataBinders}, etc. + * Furthermore, it avoids the need for synchronization on custom editors: + * A {@code PropertyEditorRegistrar} will always create fresh editor + * instances for each bean creation attempt. + * @see ConfigurableListableBeanFactory#addPropertyEditorRegistrar + */ + public void setPropertyEditorRegistrars(PropertyEditorRegistrar[] propertyEditorRegistrars) { + this.propertyEditorRegistrars = propertyEditorRegistrars; + } + + /** + * Specify the custom editors to register via a {@link Map}, using the + * class name of the required type as the key and the class name of the + * associated {@link PropertyEditor} as value. + * @see ConfigurableListableBeanFactory#registerCustomEditor + */ + public void setCustomEditors(Map, Class> customEditors) { + this.customEditors = customEditors; + } + + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + if (this.propertyEditorRegistrars != null) { + for (PropertyEditorRegistrar propertyEditorRegistrar : this.propertyEditorRegistrars) { + beanFactory.addPropertyEditorRegistrar(propertyEditorRegistrar); + } + } + if (this.customEditors != null) { + this.customEditors.forEach(beanFactory::registerCustomEditor); + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/CustomScopeConfigurer.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/CustomScopeConfigurer.java new file mode 100644 index 0000000..f292cc9 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/CustomScopeConfigurer.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.core.Ordered; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Simple {@link BeanFactoryPostProcessor} implementation that registers + * custom {@link Scope Scope(s)} with the containing {@link ConfigurableBeanFactory}. + * + *

Will register all of the supplied {@link #setScopes(java.util.Map) scopes} + * with the {@link ConfigurableListableBeanFactory} that is passed to the + * {@link #postProcessBeanFactory(ConfigurableListableBeanFactory)} method. + * + *

This class allows for declarative registration of custom scopes. + * Alternatively, consider implementing a custom {@link BeanFactoryPostProcessor} + * that calls {@link ConfigurableBeanFactory#registerScope} programmatically. + * + * @author Juergen Hoeller + * @author Rick Evans + * @since 2.0 + * @see ConfigurableBeanFactory#registerScope + */ +public class CustomScopeConfigurer implements BeanFactoryPostProcessor, BeanClassLoaderAware, Ordered { + + @Nullable + private Map scopes; + + private int order = Ordered.LOWEST_PRECEDENCE; + + @Nullable + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + + /** + * Specify the custom scopes that are to be registered. + *

The keys indicate the scope names (of type String); each value + * is expected to be the corresponding custom {@link Scope} instance + * or class name. + */ + public void setScopes(Map scopes) { + this.scopes = scopes; + } + + /** + * Add the given scope to this configurer's map of scopes. + * @param scopeName the name of the scope + * @param scope the scope implementation + * @since 4.1.1 + */ + public void addScope(String scopeName, Scope scope) { + if (this.scopes == null) { + this.scopes = new LinkedHashMap<>(1); + } + this.scopes.put(scopeName, scope); + } + + + public void setOrder(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return this.order; + } + + @Override + public void setBeanClassLoader(@Nullable ClassLoader beanClassLoader) { + this.beanClassLoader = beanClassLoader; + } + + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + if (this.scopes != null) { + this.scopes.forEach((scopeKey, value) -> { + if (value instanceof Scope) { + beanFactory.registerScope(scopeKey, (Scope) value); + } + else if (value instanceof Class) { + Class scopeClass = (Class) value; + Assert.isAssignable(Scope.class, scopeClass, "Invalid scope class"); + beanFactory.registerScope(scopeKey, (Scope) BeanUtils.instantiateClass(scopeClass)); + } + else if (value instanceof String) { + Class scopeClass = ClassUtils.resolveClassName((String) value, this.beanClassLoader); + Assert.isAssignable(Scope.class, scopeClass, "Invalid scope class"); + beanFactory.registerScope(scopeKey, (Scope) BeanUtils.instantiateClass(scopeClass)); + } + else { + throw new IllegalArgumentException("Mapped value [" + value + "] for scope key [" + + scopeKey + "] is not an instance of required type [" + Scope.class.getName() + + "] or a corresponding Class or String value indicating a Scope implementation"); + } + }); + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java new file mode 100644 index 0000000..d5fd082 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/DependencyDescriptor.java @@ -0,0 +1,482 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Map; +import java.util.Optional; + +import kotlin.reflect.KProperty; +import kotlin.reflect.jvm.ReflectJvmMapping; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.InjectionPoint; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.core.KotlinDetector; +import org.springframework.core.MethodParameter; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.core.ResolvableType; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +/** + * Descriptor for a specific dependency that is about to be injected. + * Wraps a constructor parameter, a method parameter or a field, + * allowing unified access to their metadata. + * + * @author Juergen Hoeller + * @since 2.5 + */ +@SuppressWarnings("serial") +public class DependencyDescriptor extends InjectionPoint implements Serializable { + + private final Class declaringClass; + + @Nullable + private String methodName; + + @Nullable + private Class[] parameterTypes; + + private int parameterIndex; + + @Nullable + private String fieldName; + + private final boolean required; + + private final boolean eager; + + private int nestingLevel = 1; + + @Nullable + private Class containingClass; + + @Nullable + private transient volatile ResolvableType resolvableType; + + @Nullable + private transient volatile TypeDescriptor typeDescriptor; + + + /** + * Create a new descriptor for a method or constructor parameter. + * Considers the dependency as 'eager'. + * @param methodParameter the MethodParameter to wrap + * @param required whether the dependency is required + */ + public DependencyDescriptor(MethodParameter methodParameter, boolean required) { + this(methodParameter, required, true); + } + + /** + * Create a new descriptor for a method or constructor parameter. + * @param methodParameter the MethodParameter to wrap + * @param required whether the dependency is required + * @param eager whether this dependency is 'eager' in the sense of + * eagerly resolving potential target beans for type matching + */ + public DependencyDescriptor(MethodParameter methodParameter, boolean required, boolean eager) { + super(methodParameter); + + this.declaringClass = methodParameter.getDeclaringClass(); + if (methodParameter.getMethod() != null) { + this.methodName = methodParameter.getMethod().getName(); + } + this.parameterTypes = methodParameter.getExecutable().getParameterTypes(); + this.parameterIndex = methodParameter.getParameterIndex(); + this.containingClass = methodParameter.getContainingClass(); + this.required = required; + this.eager = eager; + } + + /** + * Create a new descriptor for a field. + * Considers the dependency as 'eager'. + * @param field the field to wrap + * @param required whether the dependency is required + */ + public DependencyDescriptor(Field field, boolean required) { + this(field, required, true); + } + + /** + * Create a new descriptor for a field. + * @param field the field to wrap + * @param required whether the dependency is required + * @param eager whether this dependency is 'eager' in the sense of + * eagerly resolving potential target beans for type matching + */ + public DependencyDescriptor(Field field, boolean required, boolean eager) { + super(field); + + this.declaringClass = field.getDeclaringClass(); + this.fieldName = field.getName(); + this.required = required; + this.eager = eager; + } + + /** + * Copy constructor. + * @param original the original descriptor to create a copy from + */ + public DependencyDescriptor(DependencyDescriptor original) { + super(original); + + this.declaringClass = original.declaringClass; + this.methodName = original.methodName; + this.parameterTypes = original.parameterTypes; + this.parameterIndex = original.parameterIndex; + this.fieldName = original.fieldName; + this.containingClass = original.containingClass; + this.required = original.required; + this.eager = original.eager; + this.nestingLevel = original.nestingLevel; + } + + + /** + * Return whether this dependency is required. + *

Optional semantics are derived from Java 8's {@link java.util.Optional}, + * any variant of a parameter-level {@code Nullable} annotation (such as from + * JSR-305 or the FindBugs set of annotations), or a language-level nullable + * type declaration in Kotlin. + */ + public boolean isRequired() { + if (!this.required) { + return false; + } + + if (this.field != null) { + return !(this.field.getType() == Optional.class || hasNullableAnnotation() || + (KotlinDetector.isKotlinReflectPresent() && + KotlinDetector.isKotlinType(this.field.getDeclaringClass()) && + KotlinDelegate.isNullable(this.field))); + } + else { + return !obtainMethodParameter().isOptional(); + } + } + + /** + * Check whether the underlying field is annotated with any variant of a + * {@code Nullable} annotation, e.g. {@code javax.annotation.Nullable} or + * {@code edu.umd.cs.findbugs.annotations.Nullable}. + */ + private boolean hasNullableAnnotation() { + for (Annotation ann : getAnnotations()) { + if ("Nullable".equals(ann.annotationType().getSimpleName())) { + return true; + } + } + return false; + } + + /** + * Return whether this dependency is 'eager' in the sense of + * eagerly resolving potential target beans for type matching. + */ + public boolean isEager() { + return this.eager; + } + + /** + * Resolve the specified not-unique scenario: by default, + * throwing a {@link NoUniqueBeanDefinitionException}. + *

Subclasses may override this to select one of the instances or + * to opt out with no result at all through returning {@code null}. + * @param type the requested bean type + * @param matchingBeans a map of bean names and corresponding bean + * instances which have been pre-selected for the given type + * (qualifiers etc already applied) + * @return a bean instance to proceed with, or {@code null} for none + * @throws BeansException in case of the not-unique scenario being fatal + * @since 5.1 + */ + @Nullable + public Object resolveNotUnique(ResolvableType type, Map matchingBeans) throws BeansException { + throw new NoUniqueBeanDefinitionException(type, matchingBeans.keySet()); + } + + /** + * Resolve the specified not-unique scenario: by default, + * throwing a {@link NoUniqueBeanDefinitionException}. + *

Subclasses may override this to select one of the instances or + * to opt out with no result at all through returning {@code null}. + * @param type the requested bean type + * @param matchingBeans a map of bean names and corresponding bean + * instances which have been pre-selected for the given type + * (qualifiers etc already applied) + * @return a bean instance to proceed with, or {@code null} for none + * @throws BeansException in case of the not-unique scenario being fatal + * @since 4.3 + * @deprecated as of 5.1, in favor of {@link #resolveNotUnique(ResolvableType, Map)} + */ + @Deprecated + @Nullable + public Object resolveNotUnique(Class type, Map matchingBeans) throws BeansException { + throw new NoUniqueBeanDefinitionException(type, matchingBeans.keySet()); + } + + /** + * Resolve a shortcut for this dependency against the given factory, for example + * taking some pre-resolved information into account. + *

The resolution algorithm will first attempt to resolve a shortcut through this + * method before going into the regular type matching algorithm across all beans. + * Subclasses may override this method to improve resolution performance based on + * pre-cached information while still receiving {@link InjectionPoint} exposure etc. + * @param beanFactory the associated factory + * @return the shortcut result if any, or {@code null} if none + * @throws BeansException if the shortcut could not be obtained + * @since 4.3.1 + */ + @Nullable + public Object resolveShortcut(BeanFactory beanFactory) throws BeansException { + return null; + } + + /** + * Resolve the specified bean name, as a candidate result of the matching + * algorithm for this dependency, to a bean instance from the given factory. + *

The default implementation calls {@link BeanFactory#getBean(String)}. + * Subclasses may provide additional arguments or other customizations. + * @param beanName the bean name, as a candidate result for this dependency + * @param requiredType the expected type of the bean (as an assertion) + * @param beanFactory the associated factory + * @return the bean instance (never {@code null}) + * @throws BeansException if the bean could not be obtained + * @since 4.3.2 + * @see BeanFactory#getBean(String) + */ + public Object resolveCandidate(String beanName, Class requiredType, BeanFactory beanFactory) + throws BeansException { + + return beanFactory.getBean(beanName); + } + + + /** + * Increase this descriptor's nesting level. + */ + public void increaseNestingLevel() { + this.nestingLevel++; + this.resolvableType = null; + if (this.methodParameter != null) { + this.methodParameter = this.methodParameter.nested(); + } + } + + /** + * Optionally set the concrete class that contains this dependency. + * This may differ from the class that declares the parameter/field in that + * it may be a subclass thereof, potentially substituting type variables. + * @since 4.0 + */ + public void setContainingClass(Class containingClass) { + this.containingClass = containingClass; + this.resolvableType = null; + if (this.methodParameter != null) { + this.methodParameter = this.methodParameter.withContainingClass(containingClass); + } + } + + /** + * Build a {@link ResolvableType} object for the wrapped parameter/field. + * @since 4.0 + */ + public ResolvableType getResolvableType() { + ResolvableType resolvableType = this.resolvableType; + if (resolvableType == null) { + resolvableType = (this.field != null ? + ResolvableType.forField(this.field, this.nestingLevel, this.containingClass) : + ResolvableType.forMethodParameter(obtainMethodParameter())); + this.resolvableType = resolvableType; + } + return resolvableType; + } + + /** + * Build a {@link TypeDescriptor} object for the wrapped parameter/field. + * @since 5.1.4 + */ + public TypeDescriptor getTypeDescriptor() { + TypeDescriptor typeDescriptor = this.typeDescriptor; + if (typeDescriptor == null) { + typeDescriptor = (this.field != null ? + new TypeDescriptor(getResolvableType(), getDependencyType(), getAnnotations()) : + new TypeDescriptor(obtainMethodParameter())); + this.typeDescriptor = typeDescriptor; + } + return typeDescriptor; + } + + /** + * Return whether a fallback match is allowed. + *

This is {@code false} by default but may be overridden to return {@code true} in order + * to suggest to an {@link org.springframework.beans.factory.support.AutowireCandidateResolver} + * that a fallback match is acceptable as well. + * @since 4.0 + */ + public boolean fallbackMatchAllowed() { + return false; + } + + /** + * Return a variant of this descriptor that is intended for a fallback match. + * @since 4.0 + * @see #fallbackMatchAllowed() + */ + public DependencyDescriptor forFallbackMatch() { + return new DependencyDescriptor(this) { + @Override + public boolean fallbackMatchAllowed() { + return true; + } + }; + } + + /** + * Initialize parameter name discovery for the underlying method parameter, if any. + *

This method does not actually try to retrieve the parameter name at + * this point; it just allows discovery to happen when the application calls + * {@link #getDependencyName()} (if ever). + */ + public void initParameterNameDiscovery(@Nullable ParameterNameDiscoverer parameterNameDiscoverer) { + if (this.methodParameter != null) { + this.methodParameter.initParameterNameDiscovery(parameterNameDiscoverer); + } + } + + /** + * Determine the name of the wrapped parameter/field. + * @return the declared name (may be {@code null} if unresolvable) + */ + @Nullable + public String getDependencyName() { + return (this.field != null ? this.field.getName() : obtainMethodParameter().getParameterName()); + } + + /** + * Determine the declared (non-generic) type of the wrapped parameter/field. + * @return the declared type (never {@code null}) + */ + public Class getDependencyType() { + if (this.field != null) { + if (this.nestingLevel > 1) { + Type type = this.field.getGenericType(); + for (int i = 2; i <= this.nestingLevel; i++) { + if (type instanceof ParameterizedType) { + Type[] args = ((ParameterizedType) type).getActualTypeArguments(); + type = args[args.length - 1]; + } + } + if (type instanceof Class) { + return (Class) type; + } + else if (type instanceof ParameterizedType) { + Type arg = ((ParameterizedType) type).getRawType(); + if (arg instanceof Class) { + return (Class) arg; + } + } + return Object.class; + } + else { + return this.field.getType(); + } + } + else { + return obtainMethodParameter().getNestedParameterType(); + } + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!super.equals(other)) { + return false; + } + DependencyDescriptor otherDesc = (DependencyDescriptor) other; + return (this.required == otherDesc.required && this.eager == otherDesc.eager && + this.nestingLevel == otherDesc.nestingLevel && this.containingClass == otherDesc.containingClass); + } + + @Override + public int hashCode() { + return (31 * super.hashCode() + ObjectUtils.nullSafeHashCode(this.containingClass)); + } + + + //--------------------------------------------------------------------- + // Serialization support + //--------------------------------------------------------------------- + + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + // Rely on default serialization; just initialize state after deserialization. + ois.defaultReadObject(); + + // Restore reflective handles (which are unfortunately not serializable) + try { + if (this.fieldName != null) { + this.field = this.declaringClass.getDeclaredField(this.fieldName); + } + else { + if (this.methodName != null) { + this.methodParameter = new MethodParameter( + this.declaringClass.getDeclaredMethod(this.methodName, this.parameterTypes), this.parameterIndex); + } + else { + this.methodParameter = new MethodParameter( + this.declaringClass.getDeclaredConstructor(this.parameterTypes), this.parameterIndex); + } + for (int i = 1; i < this.nestingLevel; i++) { + this.methodParameter = this.methodParameter.nested(); + } + } + } + catch (Throwable ex) { + throw new IllegalStateException("Could not find original class structure", ex); + } + } + + + /** + * Inner class to avoid a hard dependency on Kotlin at runtime. + */ + private static class KotlinDelegate { + + /** + * Check whether the specified {@link Field} represents a nullable Kotlin type or not. + */ + public static boolean isNullable(Field field) { + KProperty property = ReflectJvmMapping.getKotlinProperty(field); + return (property != null && property.getReturnType().isMarkedNullable()); + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/DestructionAwareBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/DestructionAwareBeanPostProcessor.java new file mode 100644 index 0000000..dd1c542 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/DestructionAwareBeanPostProcessor.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import org.springframework.beans.BeansException; + +/** + * Subinterface of {@link BeanPostProcessor} that adds a before-destruction callback. + * + *

The typical usage will be to invoke custom destruction callbacks on + * specific bean types, matching corresponding initialization callbacks. + * + * @author Juergen Hoeller + * @since 1.0.1 + */ +public interface DestructionAwareBeanPostProcessor extends BeanPostProcessor { + + /** + * Apply this BeanPostProcessor to the given bean instance before its + * destruction, e.g. invoking custom destruction callbacks. + *

Like DisposableBean's {@code destroy} and a custom destroy method, this + * callback will only apply to beans which the container fully manages the + * lifecycle for. This is usually the case for singletons and scoped beans. + * @param bean the bean instance to be destroyed + * @param beanName the name of the bean + * @throws org.springframework.beans.BeansException in case of errors + * @see org.springframework.beans.factory.DisposableBean#destroy() + * @see org.springframework.beans.factory.support.AbstractBeanDefinition#setDestroyMethodName(String) + */ + void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException; + + /** + * Determine whether the given bean instance requires destruction by this + * post-processor. + *

The default implementation returns {@code true}. If a pre-5 implementation + * of {@code DestructionAwareBeanPostProcessor} does not provide a concrete + * implementation of this method, Spring silently assumes {@code true} as well. + * @param bean the bean instance to check + * @return {@code true} if {@link #postProcessBeforeDestruction} is supposed to + * be called for this bean instance eventually, or {@code false} if not needed + * @since 4.3 + */ + default boolean requiresDestruction(Object bean) { + return true; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/EmbeddedValueResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/EmbeddedValueResolver.java new file mode 100644 index 0000000..f38156b --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/EmbeddedValueResolver.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import org.springframework.lang.Nullable; +import org.springframework.util.StringValueResolver; + +/** + * {@link StringValueResolver} adapter for resolving placeholders and + * expressions against a {@link ConfigurableBeanFactory}. + * + *

Note that this adapter resolves expressions as well, in contrast + * to the {@link ConfigurableBeanFactory#resolveEmbeddedValue} method. + * The {@link BeanExpressionContext} used is for the plain bean factory, + * with no scope specified for any contextual objects to access. + * + * @author Juergen Hoeller + * @since 4.3 + * @see ConfigurableBeanFactory#resolveEmbeddedValue(String) + * @see ConfigurableBeanFactory#getBeanExpressionResolver() + * @see BeanExpressionContext + */ +public class EmbeddedValueResolver implements StringValueResolver { + + private final BeanExpressionContext exprContext; + + @Nullable + private final BeanExpressionResolver exprResolver; + + + public EmbeddedValueResolver(ConfigurableBeanFactory beanFactory) { + this.exprContext = new BeanExpressionContext(beanFactory, null); + this.exprResolver = beanFactory.getBeanExpressionResolver(); + } + + + @Override + @Nullable + public String resolveStringValue(String strVal) { + String value = this.exprContext.getBeanFactory().resolveEmbeddedValue(strVal); + if (this.exprResolver != null && value != null) { + Object evaluated = this.exprResolver.evaluate(value, this.exprContext); + value = (evaluated != null ? evaluated.toString() : null); + } + return value; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/FieldRetrievingFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/FieldRetrievingFactoryBean.java new file mode 100644 index 0000000..9f7657f --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/FieldRetrievingFactoryBean.java @@ -0,0 +1,238 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.lang.reflect.Field; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.FactoryBeanNotInitializedException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * {@link FactoryBean} which retrieves a static or non-static field value. + * + *

Typically used for retrieving public static final constants. Usage example: + * + *

+ * // standard definition for exposing a static field, specifying the "staticField" property
+ * <bean id="myField" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">
+ *   <property name="staticField" value="java.sql.Connection.TRANSACTION_SERIALIZABLE"/>
+ * </bean>
+ *
+ * // convenience version that specifies a static field pattern as bean name
+ * <bean id="java.sql.Connection.TRANSACTION_SERIALIZABLE"
+ *       class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean"/>
+ * 
+ * + *

If you are using Spring 2.0, you can also use the following style of configuration for + * public static fields. + * + *

<util:constant static-field="java.sql.Connection.TRANSACTION_SERIALIZABLE"/>
+ * + * @author Juergen Hoeller + * @since 1.1 + * @see #setStaticField + */ +public class FieldRetrievingFactoryBean + implements FactoryBean, BeanNameAware, BeanClassLoaderAware, InitializingBean { + + @Nullable + private Class targetClass; + + @Nullable + private Object targetObject; + + @Nullable + private String targetField; + + @Nullable + private String staticField; + + @Nullable + private String beanName; + + @Nullable + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + // the field we will retrieve + @Nullable + private Field fieldObject; + + + /** + * Set the target class on which the field is defined. + * Only necessary when the target field is static; else, + * a target object needs to be specified anyway. + * @see #setTargetObject + * @see #setTargetField + */ + public void setTargetClass(@Nullable Class targetClass) { + this.targetClass = targetClass; + } + + /** + * Return the target class on which the field is defined. + */ + @Nullable + public Class getTargetClass() { + return this.targetClass; + } + + /** + * Set the target object on which the field is defined. + * Only necessary when the target field is not static; + * else, a target class is sufficient. + * @see #setTargetClass + * @see #setTargetField + */ + public void setTargetObject(@Nullable Object targetObject) { + this.targetObject = targetObject; + } + + /** + * Return the target object on which the field is defined. + */ + @Nullable + public Object getTargetObject() { + return this.targetObject; + } + + /** + * Set the name of the field to be retrieved. + * Refers to either a static field or a non-static field, + * depending on a target object being set. + * @see #setTargetClass + * @see #setTargetObject + */ + public void setTargetField(@Nullable String targetField) { + this.targetField = (targetField != null ? StringUtils.trimAllWhitespace(targetField) : null); + } + + /** + * Return the name of the field to be retrieved. + */ + @Nullable + public String getTargetField() { + return this.targetField; + } + + /** + * Set a fully qualified static field name to retrieve, + * e.g. "example.MyExampleClass.MY_EXAMPLE_FIELD". + * Convenient alternative to specifying targetClass and targetField. + * @see #setTargetClass + * @see #setTargetField + */ + public void setStaticField(String staticField) { + this.staticField = StringUtils.trimAllWhitespace(staticField); + } + + /** + * The bean name of this FieldRetrievingFactoryBean will be interpreted + * as "staticField" pattern, if neither "targetClass" nor "targetObject" + * nor "targetField" have been specified. + * This allows for concise bean definitions with just an id/name. + */ + @Override + public void setBeanName(String beanName) { + this.beanName = StringUtils.trimAllWhitespace(BeanFactoryUtils.originalBeanName(beanName)); + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + + @Override + public void afterPropertiesSet() throws ClassNotFoundException, NoSuchFieldException { + if (this.targetClass != null && this.targetObject != null) { + throw new IllegalArgumentException("Specify either targetClass or targetObject, not both"); + } + + if (this.targetClass == null && this.targetObject == null) { + if (this.targetField != null) { + throw new IllegalArgumentException( + "Specify targetClass or targetObject in combination with targetField"); + } + + // If no other property specified, consider bean name as static field expression. + if (this.staticField == null) { + this.staticField = this.beanName; + Assert.state(this.staticField != null, "No target field specified"); + } + + // Try to parse static field into class and field. + int lastDotIndex = this.staticField.lastIndexOf('.'); + if (lastDotIndex == -1 || lastDotIndex == this.staticField.length()) { + throw new IllegalArgumentException( + "staticField must be a fully qualified class plus static field name: " + + "e.g. 'example.MyExampleClass.MY_EXAMPLE_FIELD'"); + } + String className = this.staticField.substring(0, lastDotIndex); + String fieldName = this.staticField.substring(lastDotIndex + 1); + this.targetClass = ClassUtils.forName(className, this.beanClassLoader); + this.targetField = fieldName; + } + + else if (this.targetField == null) { + // Either targetClass or targetObject specified. + throw new IllegalArgumentException("targetField is required"); + } + + // Try to get the exact method first. + Class targetClass = (this.targetObject != null ? this.targetObject.getClass() : this.targetClass); + this.fieldObject = targetClass.getField(this.targetField); + } + + + @Override + @Nullable + public Object getObject() throws IllegalAccessException { + if (this.fieldObject == null) { + throw new FactoryBeanNotInitializedException(); + } + ReflectionUtils.makeAccessible(this.fieldObject); + if (this.targetObject != null) { + // instance field + return this.fieldObject.get(this.targetObject); + } + else { + // class field + return this.fieldObject.get(null); + } + } + + @Override + public Class getObjectType() { + return (this.fieldObject != null ? this.fieldObject.getType() : null); + } + + @Override + public boolean isSingleton() { + return false; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/InstantiationAwareBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/InstantiationAwareBeanPostProcessor.java new file mode 100644 index 0000000..c4e2112 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/InstantiationAwareBeanPostProcessor.java @@ -0,0 +1,150 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.beans.PropertyDescriptor; + +import org.springframework.beans.BeansException; +import org.springframework.beans.PropertyValues; +import org.springframework.lang.Nullable; + +/** + * Subinterface of {@link BeanPostProcessor} that adds a before-instantiation callback, + * and a callback after instantiation but before explicit properties are set or + * autowiring occurs. + * + *

Typically used to suppress default instantiation for specific target beans, + * for example to create proxies with special TargetSources (pooling targets, + * lazily initializing targets, etc), or to implement additional injection strategies + * such as field injection. + * + *

NOTE: This interface is a special purpose interface, mainly for + * internal use within the framework. It is recommended to implement the plain + * {@link BeanPostProcessor} interface as far as possible, or to derive from + * {@link InstantiationAwareBeanPostProcessorAdapter} in order to be shielded + * from extensions to this interface. + * + * @author Juergen Hoeller + * @author Rod Johnson + * @since 1.2 + * @see org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#setCustomTargetSourceCreators + * @see org.springframework.aop.framework.autoproxy.target.LazyInitTargetSourceCreator + */ +public interface InstantiationAwareBeanPostProcessor extends BeanPostProcessor { + + /** + * Apply this BeanPostProcessor before the target bean gets instantiated. + * The returned bean object may be a proxy to use instead of the target bean, + * effectively suppressing default instantiation of the target bean. + *

If a non-null object is returned by this method, the bean creation process + * will be short-circuited. The only further processing applied is the + * {@link #postProcessAfterInitialization} callback from the configured + * {@link BeanPostProcessor BeanPostProcessors}. + *

This callback will be applied to bean definitions with their bean class, + * as well as to factory-method definitions in which case the returned bean type + * will be passed in here. + *

Post-processors may implement the extended + * {@link SmartInstantiationAwareBeanPostProcessor} interface in order + * to predict the type of the bean object that they are going to return here. + *

The default implementation returns {@code null}. + * @param beanClass the class of the bean to be instantiated + * @param beanName the name of the bean + * @return the bean object to expose instead of a default instance of the target bean, + * or {@code null} to proceed with default instantiation + * @throws org.springframework.beans.BeansException in case of errors + * @see #postProcessAfterInstantiation + * @see org.springframework.beans.factory.support.AbstractBeanDefinition#getBeanClass() + * @see org.springframework.beans.factory.support.AbstractBeanDefinition#getFactoryMethodName() + */ + @Nullable + default Object postProcessBeforeInstantiation(Class beanClass, String beanName) throws BeansException { + return null; + } + + /** + * Perform operations after the bean has been instantiated, via a constructor or factory method, + * but before Spring property population (from explicit properties or autowiring) occurs. + *

This is the ideal callback for performing custom field injection on the given bean + * instance, right before Spring's autowiring kicks in. + *

The default implementation returns {@code true}. + * @param bean the bean instance created, with properties not having been set yet + * @param beanName the name of the bean + * @return {@code true} if properties should be set on the bean; {@code false} + * if property population should be skipped. Normal implementations should return {@code true}. + * Returning {@code false} will also prevent any subsequent InstantiationAwareBeanPostProcessor + * instances being invoked on this bean instance. + * @throws org.springframework.beans.BeansException in case of errors + * @see #postProcessBeforeInstantiation + */ + default boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException { + return true; + } + + /** + * Post-process the given property values before the factory applies them + * to the given bean, without any need for property descriptors. + *

Implementations should return {@code null} (the default) if they provide a custom + * {@link #postProcessPropertyValues} implementation, and {@code pvs} otherwise. + * In a future version of this interface (with {@link #postProcessPropertyValues} removed), + * the default implementation will return the given {@code pvs} as-is directly. + * @param pvs the property values that the factory is about to apply (never {@code null}) + * @param bean the bean instance created, but whose properties have not yet been set + * @param beanName the name of the bean + * @return the actual property values to apply to the given bean (can be the passed-in + * PropertyValues instance), or {@code null} which proceeds with the existing properties + * but specifically continues with a call to {@link #postProcessPropertyValues} + * (requiring initialized {@code PropertyDescriptor}s for the current bean class) + * @throws org.springframework.beans.BeansException in case of errors + * @since 5.1 + * @see #postProcessPropertyValues + */ + @Nullable + default PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) + throws BeansException { + + return null; + } + + /** + * Post-process the given property values before the factory applies them + * to the given bean. Allows for checking whether all dependencies have been + * satisfied, for example based on a "Required" annotation on bean property setters. + *

Also allows for replacing the property values to apply, typically through + * creating a new MutablePropertyValues instance based on the original PropertyValues, + * adding or removing specific values. + *

The default implementation returns the given {@code pvs} as-is. + * @param pvs the property values that the factory is about to apply (never {@code null}) + * @param pds the relevant property descriptors for the target bean (with ignored + * dependency types - which the factory handles specifically - already filtered out) + * @param bean the bean instance created, but whose properties have not yet been set + * @param beanName the name of the bean + * @return the actual property values to apply to the given bean (can be the passed-in + * PropertyValues instance), or {@code null} to skip property population + * @throws org.springframework.beans.BeansException in case of errors + * @see #postProcessProperties + * @see org.springframework.beans.MutablePropertyValues + * @deprecated as of 5.1, in favor of {@link #postProcessProperties(PropertyValues, Object, String)} + */ + @Deprecated + @Nullable + default PropertyValues postProcessPropertyValues( + PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException { + + return pvs; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/InstantiationAwareBeanPostProcessorAdapter.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/InstantiationAwareBeanPostProcessorAdapter.java new file mode 100644 index 0000000..b3c112c --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/InstantiationAwareBeanPostProcessorAdapter.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +/** + * Adapter that implements all methods on {@link SmartInstantiationAwareBeanPostProcessor} + * as no-ops, which will not change normal processing of each bean instantiated + * by the container. Subclasses may override merely those methods that they are + * actually interested in. + * + *

Note that this base class is only recommendable if you actually require + * {@link InstantiationAwareBeanPostProcessor} functionality. If all you need + * is plain {@link BeanPostProcessor} functionality, prefer a straight + * implementation of that (simpler) interface. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + * @deprecated as of 5.3 in favor of implementing {@link InstantiationAwareBeanPostProcessor} + * or {@link SmartInstantiationAwareBeanPostProcessor} directly. + */ +@Deprecated +public abstract class InstantiationAwareBeanPostProcessorAdapter implements SmartInstantiationAwareBeanPostProcessor { + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/ListFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/ListFactoryBean.java new file mode 100644 index 0000000..d9b8921 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/ListFactoryBean.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.TypeConverter; +import org.springframework.core.ResolvableType; +import org.springframework.lang.Nullable; + +/** + * Simple factory for shared List instances. Allows for central setup + * of Lists via the "list" element in XML bean definitions. + * + * @author Juergen Hoeller + * @since 09.12.2003 + * @see SetFactoryBean + * @see MapFactoryBean + */ +public class ListFactoryBean extends AbstractFactoryBean> { + + @Nullable + private List sourceList; + + @SuppressWarnings("rawtypes") + @Nullable + private Class targetListClass; + + + /** + * Set the source List, typically populated via XML "list" elements. + */ + public void setSourceList(List sourceList) { + this.sourceList = sourceList; + } + + /** + * Set the class to use for the target List. Can be populated with a fully + * qualified class name when defined in a Spring application context. + *

Default is a {@code java.util.ArrayList}. + * @see java.util.ArrayList + */ + @SuppressWarnings("rawtypes") + public void setTargetListClass(@Nullable Class targetListClass) { + if (targetListClass == null) { + throw new IllegalArgumentException("'targetListClass' must not be null"); + } + if (!List.class.isAssignableFrom(targetListClass)) { + throw new IllegalArgumentException("'targetListClass' must implement [java.util.List]"); + } + this.targetListClass = targetListClass; + } + + + @Override + @SuppressWarnings("rawtypes") + public Class getObjectType() { + return List.class; + } + + @Override + @SuppressWarnings("unchecked") + protected List createInstance() { + if (this.sourceList == null) { + throw new IllegalArgumentException("'sourceList' is required"); + } + List result = null; + if (this.targetListClass != null) { + result = BeanUtils.instantiateClass(this.targetListClass); + } + else { + result = new ArrayList<>(this.sourceList.size()); + } + Class valueType = null; + if (this.targetListClass != null) { + valueType = ResolvableType.forClass(this.targetListClass).asCollection().resolveGeneric(); + } + if (valueType != null) { + TypeConverter converter = getBeanTypeConverter(); + for (Object elem : this.sourceList) { + result.add(converter.convertIfNecessary(elem, valueType)); + } + } + else { + result.addAll(this.sourceList); + } + return result; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/MapFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/MapFactoryBean.java new file mode 100644 index 0000000..b02673c --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/MapFactoryBean.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.util.Map; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.TypeConverter; +import org.springframework.core.ResolvableType; +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; + +/** + * Simple factory for shared Map instances. Allows for central setup + * of Maps via the "map" element in XML bean definitions. + * + * @author Juergen Hoeller + * @since 09.12.2003 + * @see SetFactoryBean + * @see ListFactoryBean + */ +public class MapFactoryBean extends AbstractFactoryBean> { + + @Nullable + private Map sourceMap; + + @SuppressWarnings("rawtypes") + @Nullable + private Class targetMapClass; + + + /** + * Set the source Map, typically populated via XML "map" elements. + */ + public void setSourceMap(Map sourceMap) { + this.sourceMap = sourceMap; + } + + /** + * Set the class to use for the target Map. Can be populated with a fully + * qualified class name when defined in a Spring application context. + *

Default is a linked HashMap, keeping the registration order. + * @see java.util.LinkedHashMap + */ + @SuppressWarnings("rawtypes") + public void setTargetMapClass(@Nullable Class targetMapClass) { + if (targetMapClass == null) { + throw new IllegalArgumentException("'targetMapClass' must not be null"); + } + if (!Map.class.isAssignableFrom(targetMapClass)) { + throw new IllegalArgumentException("'targetMapClass' must implement [java.util.Map]"); + } + this.targetMapClass = targetMapClass; + } + + + @Override + @SuppressWarnings("rawtypes") + public Class getObjectType() { + return Map.class; + } + + @Override + @SuppressWarnings("unchecked") + protected Map createInstance() { + if (this.sourceMap == null) { + throw new IllegalArgumentException("'sourceMap' is required"); + } + Map result = null; + if (this.targetMapClass != null) { + result = BeanUtils.instantiateClass(this.targetMapClass); + } + else { + result = CollectionUtils.newLinkedHashMap(this.sourceMap.size()); + } + Class keyType = null; + Class valueType = null; + if (this.targetMapClass != null) { + ResolvableType mapType = ResolvableType.forClass(this.targetMapClass).asMap(); + keyType = mapType.resolveGeneric(0); + valueType = mapType.resolveGeneric(1); + } + if (keyType != null || valueType != null) { + TypeConverter converter = getBeanTypeConverter(); + for (Map.Entry entry : this.sourceMap.entrySet()) { + Object convertedKey = converter.convertIfNecessary(entry.getKey(), keyType); + Object convertedValue = converter.convertIfNecessary(entry.getValue(), valueType); + result.put(convertedKey, convertedValue); + } + } + else { + result.putAll(this.sourceMap); + } + return result; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/MethodInvokingBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/MethodInvokingBean.java new file mode 100644 index 0000000..1374c41 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/MethodInvokingBean.java @@ -0,0 +1,136 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.lang.reflect.InvocationTargetException; + +import org.springframework.beans.TypeConverter; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.support.ArgumentConvertingMethodInvoker; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * Simple method invoker bean: just invoking a target method, not expecting a result + * to expose to the container (in contrast to {@link MethodInvokingFactoryBean}). + * + *

This invoker supports any kind of target method. A static method may be specified + * by setting the {@link #setTargetMethod targetMethod} property to a String representing + * the static method name, with {@link #setTargetClass targetClass} specifying the Class + * that the static method is defined on. Alternatively, a target instance method may be + * specified, by setting the {@link #setTargetObject targetObject} property as the target + * object, and the {@link #setTargetMethod targetMethod} property as the name of the + * method to call on that target object. Arguments for the method invocation may be + * specified by setting the {@link #setArguments arguments} property. + * + *

This class depends on {@link #afterPropertiesSet()} being called once + * all properties have been set, as per the InitializingBean contract. + * + *

An example (in an XML based bean factory definition) of a bean definition + * which uses this class to call a static initialization method: + * + *

+ * <bean id="myObject" class="org.springframework.beans.factory.config.MethodInvokingBean">
+ *   <property name="staticMethod" value="com.whatever.MyClass.init"/>
+ * </bean>
+ * + *

An example of calling an instance method to start some server bean: + * + *

+ * <bean id="myStarter" class="org.springframework.beans.factory.config.MethodInvokingBean">
+ *   <property name="targetObject" ref="myServer"/>
+ *   <property name="targetMethod" value="start"/>
+ * </bean>
+ * + * @author Juergen Hoeller + * @since 4.0.3 + * @see MethodInvokingFactoryBean + * @see org.springframework.util.MethodInvoker + */ +public class MethodInvokingBean extends ArgumentConvertingMethodInvoker + implements BeanClassLoaderAware, BeanFactoryAware, InitializingBean { + + @Nullable + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + @Nullable + private ConfigurableBeanFactory beanFactory; + + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + @Override + protected Class resolveClassName(String className) throws ClassNotFoundException { + return ClassUtils.forName(className, this.beanClassLoader); + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + if (beanFactory instanceof ConfigurableBeanFactory) { + this.beanFactory = (ConfigurableBeanFactory) beanFactory; + } + } + + /** + * Obtain the TypeConverter from the BeanFactory that this bean runs in, + * if possible. + * @see ConfigurableBeanFactory#getTypeConverter() + */ + @Override + protected TypeConverter getDefaultTypeConverter() { + if (this.beanFactory != null) { + return this.beanFactory.getTypeConverter(); + } + else { + return super.getDefaultTypeConverter(); + } + } + + + @Override + public void afterPropertiesSet() throws Exception { + prepare(); + invokeWithTargetException(); + } + + /** + * Perform the invocation and convert InvocationTargetException + * into the underlying target exception. + */ + @Nullable + protected Object invokeWithTargetException() throws Exception { + try { + return invoke(); + } + catch (InvocationTargetException ex) { + if (ex.getTargetException() instanceof Exception) { + throw (Exception) ex.getTargetException(); + } + if (ex.getTargetException() instanceof Error) { + throw (Error) ex.getTargetException(); + } + throw ex; + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/NamedBeanHolder.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/NamedBeanHolder.java new file mode 100644 index 0000000..ca73a40 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/NamedBeanHolder.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import org.springframework.beans.factory.NamedBean; +import org.springframework.util.Assert; + +/** + * A simple holder for a given bean name plus bean instance. + * + * @author Juergen Hoeller + * @since 4.3.3 + * @param the bean type + * @see AutowireCapableBeanFactory#resolveNamedBean(Class) + */ +public class NamedBeanHolder implements NamedBean { + + private final String beanName; + + private final T beanInstance; + + + /** + * Create a new holder for the given bean name plus instance. + * @param beanName the name of the bean + * @param beanInstance the corresponding bean instance + */ + public NamedBeanHolder(String beanName, T beanInstance) { + Assert.notNull(beanName, "Bean name must not be null"); + this.beanName = beanName; + this.beanInstance = beanInstance; + } + + + /** + * Return the name of the bean. + */ + @Override + public String getBeanName() { + return this.beanName; + } + + /** + * Return the corresponding bean instance. + */ + public T getBeanInstance() { + return this.beanInstance; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java new file mode 100644 index 0000000..fc29a9e --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java @@ -0,0 +1,240 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.lang.Nullable; +import org.springframework.util.StringValueResolver; + +/** + * Abstract base class for property resource configurers that resolve placeholders + * in bean definition property values. Implementations pull values from a + * properties file or other {@linkplain org.springframework.core.env.PropertySource + * property source} into bean definitions. + * + *

The default placeholder syntax follows the Ant / Log4J / JSP EL style: + * + *

${...}
+ * + * Example XML bean definition: + * + *
+ * <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"/>
+ *   <property name="driverClassName" value="${driver}"/>
+ *   <property name="url" value="jdbc:${dbname}"/>
+ * </bean>
+ * 
+ * + * Example properties file: + * + *
driver=com.mysql.jdbc.Driver
+ * dbname=mysql:mydb
+ * + * Annotated bean definitions may take advantage of property replacement using + * the {@link org.springframework.beans.factory.annotation.Value @Value} annotation: + * + *
@Value("${person.age}")
+ * + * Implementations check simple property values, lists, maps, props, and bean names + * in bean references. Furthermore, placeholder values can also cross-reference + * other placeholders, like: + * + *
rootPath=myrootdir
+ * subPath=${rootPath}/subdir
+ * + * In contrast to {@link PropertyOverrideConfigurer}, subclasses of this type allow + * filling in of explicit placeholders in bean definitions. + * + *

If a configurer cannot resolve a placeholder, a {@link BeanDefinitionStoreException} + * will be thrown. If you want to check against multiple properties files, specify multiple + * resources via the {@link #setLocations locations} property. You can also define multiple + * configurers, each with its own placeholder syntax. Use {@link + * #ignoreUnresolvablePlaceholders} to intentionally suppress throwing an exception if a + * placeholder cannot be resolved. + * + *

Default property values can be defined globally for each configurer instance + * via the {@link #setProperties properties} property, or on a property-by-property basis + * using the default value separator which is {@code ":"} by default and + * customizable via {@link #setValueSeparator(String)}. + * + *

Example XML property with default value: + * + *

+ *   
+ * 
+ * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + * @see PropertyPlaceholderConfigurer + * @see org.springframework.context.support.PropertySourcesPlaceholderConfigurer + */ +public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfigurer + implements BeanNameAware, BeanFactoryAware { + + /** Default placeholder prefix: {@value}. */ + public static final String DEFAULT_PLACEHOLDER_PREFIX = "${"; + + /** Default placeholder suffix: {@value}. */ + public static final String DEFAULT_PLACEHOLDER_SUFFIX = "}"; + + /** Default value separator: {@value}. */ + public static final String DEFAULT_VALUE_SEPARATOR = ":"; + + + /** Defaults to {@value #DEFAULT_PLACEHOLDER_PREFIX}. */ + protected String placeholderPrefix = DEFAULT_PLACEHOLDER_PREFIX; + + /** Defaults to {@value #DEFAULT_PLACEHOLDER_SUFFIX}. */ + protected String placeholderSuffix = DEFAULT_PLACEHOLDER_SUFFIX; + + /** Defaults to {@value #DEFAULT_VALUE_SEPARATOR}. */ + @Nullable + protected String valueSeparator = DEFAULT_VALUE_SEPARATOR; + + protected boolean trimValues = false; + + @Nullable + protected String nullValue; + + protected boolean ignoreUnresolvablePlaceholders = false; + + @Nullable + private String beanName; + + @Nullable + private BeanFactory beanFactory; + + + /** + * Set the prefix that a placeholder string starts with. + * The default is {@value #DEFAULT_PLACEHOLDER_PREFIX}. + */ + public void setPlaceholderPrefix(String placeholderPrefix) { + this.placeholderPrefix = placeholderPrefix; + } + + /** + * Set the suffix that a placeholder string ends with. + * The default is {@value #DEFAULT_PLACEHOLDER_SUFFIX}. + */ + public void setPlaceholderSuffix(String placeholderSuffix) { + this.placeholderSuffix = placeholderSuffix; + } + + /** + * Specify the separating character between the placeholder variable + * and the associated default value, or {@code null} if no such + * special character should be processed as a value separator. + * The default is {@value #DEFAULT_VALUE_SEPARATOR}. + */ + public void setValueSeparator(@Nullable String valueSeparator) { + this.valueSeparator = valueSeparator; + } + + /** + * Specify whether to trim resolved values before applying them, + * removing superfluous whitespace from the beginning and end. + *

Default is {@code false}. + * @since 4.3 + */ + public void setTrimValues(boolean trimValues) { + this.trimValues = trimValues; + } + + /** + * Set a value that should be treated as {@code null} when resolved + * as a placeholder value: e.g. "" (empty String) or "null". + *

Note that this will only apply to full property values, + * not to parts of concatenated values. + *

By default, no such null value is defined. This means that + * there is no way to express {@code null} as a property value + * unless you explicitly map a corresponding value here. + */ + public void setNullValue(String nullValue) { + this.nullValue = nullValue; + } + + /** + * Set whether to ignore unresolvable placeholders. + *

Default is "false": An exception will be thrown if a placeholder fails + * to resolve. Switch this flag to "true" in order to preserve the placeholder + * String as-is in such a case, leaving it up to other placeholder configurers + * to resolve it. + */ + public void setIgnoreUnresolvablePlaceholders(boolean ignoreUnresolvablePlaceholders) { + this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders; + } + + /** + * Only necessary to check that we're not parsing our own bean definition, + * to avoid failing on unresolvable placeholders in properties file locations. + * The latter case can happen with placeholders for system properties in + * resource locations. + * @see #setLocations + * @see org.springframework.core.io.ResourceEditor + */ + @Override + public void setBeanName(String beanName) { + this.beanName = beanName; + } + + /** + * Only necessary to check that we're not parsing our own bean definition, + * to avoid failing on unresolvable placeholders in properties file locations. + * The latter case can happen with placeholders for system properties in + * resource locations. + * @see #setLocations + * @see org.springframework.core.io.ResourceEditor + */ + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + + protected void doProcessProperties(ConfigurableListableBeanFactory beanFactoryToProcess, + StringValueResolver valueResolver) { + + BeanDefinitionVisitor visitor = new BeanDefinitionVisitor(valueResolver); + + String[] beanNames = beanFactoryToProcess.getBeanDefinitionNames(); + for (String curName : beanNames) { + // Check that we're not parsing our own bean definition, + // to avoid failing on unresolvable placeholders in properties file locations. + if (!(curName.equals(this.beanName) && beanFactoryToProcess.equals(this.beanFactory))) { + BeanDefinition bd = beanFactoryToProcess.getBeanDefinition(curName); + try { + visitor.visitBeanDefinition(bd); + } + catch (Exception ex) { + throw new BeanDefinitionStoreException(bd.getResourceDescription(), curName, ex.getMessage(), ex); + } + } + } + + // New in Spring 2.5: resolve placeholders in alias target names and aliases as well. + beanFactoryToProcess.resolveAliases(valueResolver); + + // New in Spring 3.0: resolve placeholders in embedded values such as annotation attributes. + beanFactoryToProcess.addEmbeddedValueResolver(valueResolver); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertiesFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertiesFactoryBean.java new file mode 100644 index 0000000..47a857e --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertiesFactoryBean.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.io.IOException; +import java.util.Properties; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.io.support.PropertiesLoaderSupport; +import org.springframework.lang.Nullable; + +/** + * Allows for making a properties file from a classpath location available + * as Properties instance in a bean factory. Can be used to populate + * any bean property of type Properties via a bean reference. + * + *

Supports loading from a properties file and/or setting local properties + * on this FactoryBean. The created Properties instance will be merged from + * loaded and local values. If neither a location nor local properties are set, + * an exception will be thrown on initialization. + * + *

Can create a singleton or a new object on each request. + * Default is a singleton. + * + * @author Juergen Hoeller + * @see #setLocation + * @see #setProperties + * @see #setLocalOverride + * @see java.util.Properties + */ +public class PropertiesFactoryBean extends PropertiesLoaderSupport + implements FactoryBean, InitializingBean { + + private boolean singleton = true; + + @Nullable + private Properties singletonInstance; + + + /** + * Set whether a shared 'singleton' Properties instance should be + * created, or rather a new Properties instance on each request. + *

Default is "true" (a shared singleton). + */ + public final void setSingleton(boolean singleton) { + this.singleton = singleton; + } + + @Override + public final boolean isSingleton() { + return this.singleton; + } + + + @Override + public final void afterPropertiesSet() throws IOException { + if (this.singleton) { + this.singletonInstance = createProperties(); + } + } + + @Override + @Nullable + public final Properties getObject() throws IOException { + if (this.singleton) { + return this.singletonInstance; + } + else { + return createProperties(); + } + } + + @Override + public Class getObjectType() { + return Properties.class; + } + + + /** + * Template method that subclasses may override to construct the object + * returned by this factory. The default implementation returns the + * plain merged Properties instance. + *

Invoked on initialization of this FactoryBean in case of a + * shared singleton; else, on each {@link #getObject()} call. + * @return the object returned by this factory + * @throws IOException if an exception occurred during properties loading + * @see #mergeProperties() + */ + protected Properties createProperties() throws IOException { + return mergeProperties(); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyOverrideConfigurer.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyOverrideConfigurer.java new file mode 100644 index 0000000..e073d81 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyOverrideConfigurer.java @@ -0,0 +1,171 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.util.Collections; +import java.util.Enumeration; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.beans.BeansException; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.factory.BeanInitializationException; + +/** + * Property resource configurer that overrides bean property values in an application + * context definition. It pushes values from a properties file into bean definitions. + * + *

Configuration lines are expected to be of the following form: + * + *

beanName.property=value
+ * + * Example properties file: + * + *
dataSource.driverClassName=com.mysql.jdbc.Driver
+ * dataSource.url=jdbc:mysql:mydb
+ * + * In contrast to PropertyPlaceholderConfigurer, the original definition can have default + * values or no values at all for such bean properties. If an overriding properties file does + * not have an entry for a certain bean property, the default context definition is used. + * + *

Note that the context definition is not aware of being overridden; + * so this is not immediately obvious when looking at the XML definition file. + * Furthermore, note that specified override values are always literal values; + * they are not translated into bean references. This also applies when the original + * value in the XML bean definition specifies a bean reference. + * + *

In case of multiple PropertyOverrideConfigurers that define different values for + * the same bean property, the last one will win (due to the overriding mechanism). + * + *

Property values can be converted after reading them in, through overriding + * the {@code convertPropertyValue} method. For example, encrypted values + * can be detected and decrypted accordingly before processing them. + * + * @author Juergen Hoeller + * @author Rod Johnson + * @since 12.03.2003 + * @see #convertPropertyValue + * @see PropertyPlaceholderConfigurer + */ +public class PropertyOverrideConfigurer extends PropertyResourceConfigurer { + + /** + * The default bean name separator. + */ + public static final String DEFAULT_BEAN_NAME_SEPARATOR = "."; + + + private String beanNameSeparator = DEFAULT_BEAN_NAME_SEPARATOR; + + private boolean ignoreInvalidKeys = false; + + /** + * Contains names of beans that have overrides. + */ + private final Set beanNames = Collections.newSetFromMap(new ConcurrentHashMap<>(16)); + + + /** + * Set the separator to expect between bean name and property path. + * Default is a dot ("."). + */ + public void setBeanNameSeparator(String beanNameSeparator) { + this.beanNameSeparator = beanNameSeparator; + } + + /** + * Set whether to ignore invalid keys. Default is "false". + *

If you ignore invalid keys, keys that do not follow the 'beanName.property' format + * (or refer to invalid bean names or properties) will just be logged at debug level. + * This allows one to have arbitrary other keys in a properties file. + */ + public void setIgnoreInvalidKeys(boolean ignoreInvalidKeys) { + this.ignoreInvalidKeys = ignoreInvalidKeys; + } + + + @Override + protected void processProperties(ConfigurableListableBeanFactory beanFactory, Properties props) + throws BeansException { + + for (Enumeration names = props.propertyNames(); names.hasMoreElements();) { + String key = (String) names.nextElement(); + try { + processKey(beanFactory, key, props.getProperty(key)); + } + catch (BeansException ex) { + String msg = "Could not process key '" + key + "' in PropertyOverrideConfigurer"; + if (!this.ignoreInvalidKeys) { + throw new BeanInitializationException(msg, ex); + } + if (logger.isDebugEnabled()) { + logger.debug(msg, ex); + } + } + } + } + + /** + * Process the given key as 'beanName.property' entry. + */ + protected void processKey(ConfigurableListableBeanFactory factory, String key, String value) + throws BeansException { + + int separatorIndex = key.indexOf(this.beanNameSeparator); + if (separatorIndex == -1) { + throw new BeanInitializationException("Invalid key '" + key + + "': expected 'beanName" + this.beanNameSeparator + "property'"); + } + String beanName = key.substring(0, separatorIndex); + String beanProperty = key.substring(separatorIndex + 1); + this.beanNames.add(beanName); + applyPropertyValue(factory, beanName, beanProperty, value); + if (logger.isDebugEnabled()) { + logger.debug("Property '" + key + "' set to value [" + value + "]"); + } + } + + /** + * Apply the given property value to the corresponding bean. + */ + protected void applyPropertyValue( + ConfigurableListableBeanFactory factory, String beanName, String property, String value) { + + BeanDefinition bd = factory.getBeanDefinition(beanName); + BeanDefinition bdToUse = bd; + while (bd != null) { + bdToUse = bd; + bd = bd.getOriginatingBeanDefinition(); + } + PropertyValue pv = new PropertyValue(property, value); + pv.setOptional(this.ignoreInvalidKeys); + bdToUse.getPropertyValues().addPropertyValue(pv); + } + + + /** + * Were there overrides for this bean? + * Only valid after processing has occurred at least once. + * @param beanName name of the bean to query status for + * @return whether there were property overrides for the named bean + */ + public boolean hasPropertyOverridesFor(String beanName) { + return this.beanNames.contains(beanName); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPathFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPathFactoryBean.java new file mode 100644 index 0000000..97e9b4f --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPathFactoryBean.java @@ -0,0 +1,242 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.BeansException; +import org.springframework.beans.PropertyAccessorFactory; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * {@link FactoryBean} that evaluates a property path on a given target object. + * + *

The target object can be specified directly or via a bean name. + * + *

Usage examples: + * + *

<!-- target bean to be referenced by name -->
+ * <bean id="tb" class="org.springframework.beans.TestBean" singleton="false">
+ *   <property name="age" value="10"/>
+ *   <property name="spouse">
+ *     <bean class="org.springframework.beans.TestBean">
+ *       <property name="age" value="11"/>
+ *     </bean>
+ *   </property>
+ * </bean>
+ *
+ * <!-- will result in 12, which is the value of property 'age' of the inner bean -->
+ * <bean id="propertyPath1" class="org.springframework.beans.factory.config.PropertyPathFactoryBean">
+ *   <property name="targetObject">
+ *     <bean class="org.springframework.beans.TestBean">
+ *       <property name="age" value="12"/>
+ *     </bean>
+ *   </property>
+ *   <property name="propertyPath" value="age"/>
+ * </bean>
+ *
+ * <!-- will result in 11, which is the value of property 'spouse.age' of bean 'tb' -->
+ * <bean id="propertyPath2" class="org.springframework.beans.factory.config.PropertyPathFactoryBean">
+ *   <property name="targetBeanName" value="tb"/>
+ *   <property name="propertyPath" value="spouse.age"/>
+ * </bean>
+ *
+ * <!-- will result in 10, which is the value of property 'age' of bean 'tb' -->
+ * <bean id="tb.age" class="org.springframework.beans.factory.config.PropertyPathFactoryBean"/>
+ * + *

If you are using Spring 2.0 and XML Schema support in your configuration file(s), + * you can also use the following style of configuration for property path access. + * (See also the appendix entitled 'XML Schema-based configuration' in the Spring + * reference manual for more examples.) + * + *

 <!-- will result in 10, which is the value of property 'age' of bean 'tb' -->
+ * <util:property-path id="name" path="testBean.age"/>
+ * + * Thanks to Matthias Ernst for the suggestion and initial prototype! + * + * @author Juergen Hoeller + * @since 1.1.2 + * @see #setTargetObject + * @see #setTargetBeanName + * @see #setPropertyPath + */ +public class PropertyPathFactoryBean implements FactoryBean, BeanNameAware, BeanFactoryAware { + + private static final Log logger = LogFactory.getLog(PropertyPathFactoryBean.class); + + @Nullable + private BeanWrapper targetBeanWrapper; + + @Nullable + private String targetBeanName; + + @Nullable + private String propertyPath; + + @Nullable + private Class resultType; + + @Nullable + private String beanName; + + @Nullable + private BeanFactory beanFactory; + + + /** + * Specify a target object to apply the property path to. + * Alternatively, specify a target bean name. + * @param targetObject a target object, for example a bean reference + * or an inner bean + * @see #setTargetBeanName + */ + public void setTargetObject(Object targetObject) { + this.targetBeanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(targetObject); + } + + /** + * Specify the name of a target bean to apply the property path to. + * Alternatively, specify a target object directly. + * @param targetBeanName the bean name to be looked up in the + * containing bean factory (e.g. "testBean") + * @see #setTargetObject + */ + public void setTargetBeanName(String targetBeanName) { + this.targetBeanName = StringUtils.trimAllWhitespace(targetBeanName); + } + + /** + * Specify the property path to apply to the target. + * @param propertyPath the property path, potentially nested + * (e.g. "age" or "spouse.age") + */ + public void setPropertyPath(String propertyPath) { + this.propertyPath = StringUtils.trimAllWhitespace(propertyPath); + } + + /** + * Specify the type of the result from evaluating the property path. + *

Note: This is not necessary for directly specified target objects + * or singleton target beans, where the type can be determined through + * introspection. Just specify this in case of a prototype target, + * provided that you need matching by type (for example, for autowiring). + * @param resultType the result type, for example "java.lang.Integer" + */ + public void setResultType(Class resultType) { + this.resultType = resultType; + } + + /** + * The bean name of this PropertyPathFactoryBean will be interpreted + * as "beanName.property" pattern, if neither "targetObject" nor + * "targetBeanName" nor "propertyPath" have been specified. + * This allows for concise bean definitions with just an id/name. + */ + @Override + public void setBeanName(String beanName) { + this.beanName = StringUtils.trimAllWhitespace(BeanFactoryUtils.originalBeanName(beanName)); + } + + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + + if (this.targetBeanWrapper != null && this.targetBeanName != null) { + throw new IllegalArgumentException("Specify either 'targetObject' or 'targetBeanName', not both"); + } + + if (this.targetBeanWrapper == null && this.targetBeanName == null) { + if (this.propertyPath != null) { + throw new IllegalArgumentException( + "Specify 'targetObject' or 'targetBeanName' in combination with 'propertyPath'"); + } + + // No other properties specified: check bean name. + int dotIndex = (this.beanName != null ? this.beanName.indexOf('.') : -1); + if (dotIndex == -1) { + throw new IllegalArgumentException( + "Neither 'targetObject' nor 'targetBeanName' specified, and PropertyPathFactoryBean " + + "bean name '" + this.beanName + "' does not follow 'beanName.property' syntax"); + } + this.targetBeanName = this.beanName.substring(0, dotIndex); + this.propertyPath = this.beanName.substring(dotIndex + 1); + } + + else if (this.propertyPath == null) { + // either targetObject or targetBeanName specified + throw new IllegalArgumentException("'propertyPath' is required"); + } + + if (this.targetBeanWrapper == null && this.beanFactory.isSingleton(this.targetBeanName)) { + // Eagerly fetch singleton target bean, and determine result type. + Object bean = this.beanFactory.getBean(this.targetBeanName); + this.targetBeanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(bean); + this.resultType = this.targetBeanWrapper.getPropertyType(this.propertyPath); + } + } + + + @Override + @Nullable + public Object getObject() throws BeansException { + BeanWrapper target = this.targetBeanWrapper; + if (target != null) { + if (logger.isWarnEnabled() && this.targetBeanName != null && + this.beanFactory instanceof ConfigurableBeanFactory && + ((ConfigurableBeanFactory) this.beanFactory).isCurrentlyInCreation(this.targetBeanName)) { + logger.warn("Target bean '" + this.targetBeanName + "' is still in creation due to a circular " + + "reference - obtained value for property '" + this.propertyPath + "' may be outdated!"); + } + } + else { + // Fetch prototype target bean... + Assert.state(this.beanFactory != null, "No BeanFactory available"); + Assert.state(this.targetBeanName != null, "No target bean name specified"); + Object bean = this.beanFactory.getBean(this.targetBeanName); + target = PropertyAccessorFactory.forBeanPropertyAccess(bean); + } + Assert.state(this.propertyPath != null, "No property path specified"); + return target.getPropertyValue(this.propertyPath); + } + + @Override + public Class getObjectType() { + return this.resultType; + } + + /** + * While this FactoryBean will often be used for singleton targets, + * the invoked getters for the property path might return a new object + * for each call, so we have to assume that we're not returning the + * same object for each {@link #getObject()} call. + */ + @Override + public boolean isSingleton() { + return false; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurer.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurer.java new file mode 100644 index 0000000..130f13e --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurer.java @@ -0,0 +1,255 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.util.Properties; + +import org.springframework.beans.BeansException; +import org.springframework.core.Constants; +import org.springframework.core.SpringProperties; +import org.springframework.core.env.AbstractEnvironment; +import org.springframework.lang.Nullable; +import org.springframework.util.PropertyPlaceholderHelper; +import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; +import org.springframework.util.StringValueResolver; + +/** + * {@link PlaceholderConfigurerSupport} subclass that resolves ${...} placeholders against + * {@link #setLocation local} {@link #setProperties properties} and/or system properties + * and environment variables. + * + *

{@link PropertyPlaceholderConfigurer} is still appropriate for use when: + *

    + *
  • the {@code spring-context} module is not available (i.e., one is using Spring's + * {@code BeanFactory} API as opposed to {@code ApplicationContext}). + *
  • existing configuration makes use of the {@link #setSystemPropertiesMode(int) "systemPropertiesMode"} + * and/or {@link #setSystemPropertiesModeName(String) "systemPropertiesModeName"} properties. + * Users are encouraged to move away from using these settings, and rather configure property + * source search order through the container's {@code Environment}; however, exact preservation + * of functionality may be maintained by continuing to use {@code PropertyPlaceholderConfigurer}. + *
+ * + * @author Juergen Hoeller + * @author Chris Beams + * @since 02.10.2003 + * @see #setSystemPropertiesModeName + * @see PlaceholderConfigurerSupport + * @see PropertyOverrideConfigurer + * @deprecated as of 5.2; use {@code org.springframework.context.support.PropertySourcesPlaceholderConfigurer} + * instead which is more flexible through taking advantage of the {@link org.springframework.core.env.Environment} + * and {@link org.springframework.core.env.PropertySource} mechanisms. + */ +@Deprecated +public class PropertyPlaceholderConfigurer extends PlaceholderConfigurerSupport { + + /** Never check system properties. */ + public static final int SYSTEM_PROPERTIES_MODE_NEVER = 0; + + /** + * Check system properties if not resolvable in the specified properties. + * This is the default. + */ + public static final int SYSTEM_PROPERTIES_MODE_FALLBACK = 1; + + /** + * Check system properties first, before trying the specified properties. + * This allows system properties to override any other property source. + */ + public static final int SYSTEM_PROPERTIES_MODE_OVERRIDE = 2; + + + private static final Constants constants = new Constants(PropertyPlaceholderConfigurer.class); + + private int systemPropertiesMode = SYSTEM_PROPERTIES_MODE_FALLBACK; + + private boolean searchSystemEnvironment = + !SpringProperties.getFlag(AbstractEnvironment.IGNORE_GETENV_PROPERTY_NAME); + + + /** + * Set the system property mode by the name of the corresponding constant, + * e.g. "SYSTEM_PROPERTIES_MODE_OVERRIDE". + * @param constantName name of the constant + * @see #setSystemPropertiesMode + */ + public void setSystemPropertiesModeName(String constantName) throws IllegalArgumentException { + this.systemPropertiesMode = constants.asNumber(constantName).intValue(); + } + + /** + * Set how to check system properties: as fallback, as override, or never. + * For example, will resolve ${user.dir} to the "user.dir" system property. + *

The default is "fallback": If not being able to resolve a placeholder + * with the specified properties, a system property will be tried. + * "override" will check for a system property first, before trying the + * specified properties. "never" will not check system properties at all. + * @see #SYSTEM_PROPERTIES_MODE_NEVER + * @see #SYSTEM_PROPERTIES_MODE_FALLBACK + * @see #SYSTEM_PROPERTIES_MODE_OVERRIDE + * @see #setSystemPropertiesModeName + */ + public void setSystemPropertiesMode(int systemPropertiesMode) { + this.systemPropertiesMode = systemPropertiesMode; + } + + /** + * Set whether to search for a matching system environment variable + * if no matching system property has been found. Only applied when + * "systemPropertyMode" is active (i.e. "fallback" or "override"), right + * after checking JVM system properties. + *

Default is "true". Switch this setting off to never resolve placeholders + * against system environment variables. Note that it is generally recommended + * to pass external values in as JVM system properties: This can easily be + * achieved in a startup script, even for existing environment variables. + * @see #setSystemPropertiesMode + * @see System#getProperty(String) + * @see System#getenv(String) + */ + public void setSearchSystemEnvironment(boolean searchSystemEnvironment) { + this.searchSystemEnvironment = searchSystemEnvironment; + } + + /** + * Resolve the given placeholder using the given properties, performing + * a system properties check according to the given mode. + *

The default implementation delegates to {@code resolvePlaceholder + * (placeholder, props)} before/after the system properties check. + *

Subclasses can override this for custom resolution strategies, + * including customized points for the system properties check. + * @param placeholder the placeholder to resolve + * @param props the merged properties of this configurer + * @param systemPropertiesMode the system properties mode, + * according to the constants in this class + * @return the resolved value, of null if none + * @see #setSystemPropertiesMode + * @see System#getProperty + * @see #resolvePlaceholder(String, java.util.Properties) + */ + @Nullable + protected String resolvePlaceholder(String placeholder, Properties props, int systemPropertiesMode) { + String propVal = null; + if (systemPropertiesMode == SYSTEM_PROPERTIES_MODE_OVERRIDE) { + propVal = resolveSystemProperty(placeholder); + } + if (propVal == null) { + propVal = resolvePlaceholder(placeholder, props); + } + if (propVal == null && systemPropertiesMode == SYSTEM_PROPERTIES_MODE_FALLBACK) { + propVal = resolveSystemProperty(placeholder); + } + return propVal; + } + + /** + * Resolve the given placeholder using the given properties. + * The default implementation simply checks for a corresponding property key. + *

Subclasses can override this for customized placeholder-to-key mappings + * or custom resolution strategies, possibly just using the given properties + * as fallback. + *

Note that system properties will still be checked before respectively + * after this method is invoked, according to the system properties mode. + * @param placeholder the placeholder to resolve + * @param props the merged properties of this configurer + * @return the resolved value, of {@code null} if none + * @see #setSystemPropertiesMode + */ + @Nullable + protected String resolvePlaceholder(String placeholder, Properties props) { + return props.getProperty(placeholder); + } + + /** + * Resolve the given key as JVM system property, and optionally also as + * system environment variable if no matching system property has been found. + * @param key the placeholder to resolve as system property key + * @return the system property value, or {@code null} if not found + * @see #setSearchSystemEnvironment + * @see System#getProperty(String) + * @see System#getenv(String) + */ + @Nullable + protected String resolveSystemProperty(String key) { + try { + String value = System.getProperty(key); + if (value == null && this.searchSystemEnvironment) { + value = System.getenv(key); + } + return value; + } + catch (Throwable ex) { + if (logger.isDebugEnabled()) { + logger.debug("Could not access system property '" + key + "': " + ex); + } + return null; + } + } + + + /** + * Visit each bean definition in the given bean factory and attempt to replace ${...} property + * placeholders with values from the given properties. + */ + @Override + protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, Properties props) + throws BeansException { + + StringValueResolver valueResolver = new PlaceholderResolvingStringValueResolver(props); + doProcessProperties(beanFactoryToProcess, valueResolver); + } + + + private class PlaceholderResolvingStringValueResolver implements StringValueResolver { + + private final PropertyPlaceholderHelper helper; + + private final PlaceholderResolver resolver; + + public PlaceholderResolvingStringValueResolver(Properties props) { + this.helper = new PropertyPlaceholderHelper( + placeholderPrefix, placeholderSuffix, valueSeparator, ignoreUnresolvablePlaceholders); + this.resolver = new PropertyPlaceholderConfigurerResolver(props); + } + + @Override + @Nullable + public String resolveStringValue(String strVal) throws BeansException { + String resolved = this.helper.replacePlaceholders(strVal, this.resolver); + if (trimValues) { + resolved = resolved.trim(); + } + return (resolved.equals(nullValue) ? null : resolved); + } + } + + + private final class PropertyPlaceholderConfigurerResolver implements PlaceholderResolver { + + private final Properties props; + + private PropertyPlaceholderConfigurerResolver(Properties props) { + this.props = props; + } + + @Override + @Nullable + public String resolvePlaceholder(String placeholderName) { + return PropertyPlaceholderConfigurer.this.resolvePlaceholder(placeholderName, + this.props, systemPropertiesMode); + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyResourceConfigurer.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyResourceConfigurer.java new file mode 100644 index 0000000..ad24226 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyResourceConfigurer.java @@ -0,0 +1,154 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.io.IOException; +import java.util.Enumeration; +import java.util.Properties; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; +import org.springframework.core.io.support.PropertiesLoaderSupport; +import org.springframework.util.ObjectUtils; + +/** + * Allows for configuration of individual bean property values from a property resource, + * i.e. a properties file. Useful for custom config files targeted at system + * administrators that override bean properties configured in the application context. + * + *

Two concrete implementations are provided in the distribution: + *

    + *
  • {@link PropertyOverrideConfigurer} for "beanName.property=value" style overriding + * (pushing values from a properties file into bean definitions) + *
  • {@link PropertyPlaceholderConfigurer} for replacing "${...}" placeholders + * (pulling values from a properties file into bean definitions) + *
+ * + *

Property values can be converted after reading them in, through overriding + * the {@link #convertPropertyValue} method. For example, encrypted values + * can be detected and decrypted accordingly before processing them. + * + * @author Juergen Hoeller + * @since 02.10.2003 + * @see PropertyOverrideConfigurer + * @see PropertyPlaceholderConfigurer + */ +public abstract class PropertyResourceConfigurer extends PropertiesLoaderSupport + implements BeanFactoryPostProcessor, PriorityOrdered { + + private int order = Ordered.LOWEST_PRECEDENCE; // default: same as non-Ordered + + + /** + * Set the order value of this object for sorting purposes. + * @see PriorityOrdered + */ + public void setOrder(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return this.order; + } + + + /** + * {@linkplain #mergeProperties Merge}, {@linkplain #convertProperties convert} and + * {@linkplain #processProperties process} properties against the given bean factory. + * @throws BeanInitializationException if any properties cannot be loaded + */ + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + try { + Properties mergedProps = mergeProperties(); + + // Convert the merged properties, if necessary. + convertProperties(mergedProps); + + // Let the subclass process the properties. + processProperties(beanFactory, mergedProps); + } + catch (IOException ex) { + throw new BeanInitializationException("Could not load properties", ex); + } + } + + /** + * Convert the given merged properties, converting property values + * if necessary. The result will then be processed. + *

The default implementation will invoke {@link #convertPropertyValue} + * for each property value, replacing the original with the converted value. + * @param props the Properties to convert + * @see #processProperties + */ + protected void convertProperties(Properties props) { + Enumeration propertyNames = props.propertyNames(); + while (propertyNames.hasMoreElements()) { + String propertyName = (String) propertyNames.nextElement(); + String propertyValue = props.getProperty(propertyName); + String convertedValue = convertProperty(propertyName, propertyValue); + if (!ObjectUtils.nullSafeEquals(propertyValue, convertedValue)) { + props.setProperty(propertyName, convertedValue); + } + } + } + + /** + * Convert the given property from the properties source to the value + * which should be applied. + *

The default implementation calls {@link #convertPropertyValue(String)}. + * @param propertyName the name of the property that the value is defined for + * @param propertyValue the original value from the properties source + * @return the converted value, to be used for processing + * @see #convertPropertyValue(String) + */ + protected String convertProperty(String propertyName, String propertyValue) { + return convertPropertyValue(propertyValue); + } + + /** + * Convert the given property value from the properties source to the value + * which should be applied. + *

The default implementation simply returns the original value. + * Can be overridden in subclasses, for example to detect + * encrypted values and decrypt them accordingly. + * @param originalValue the original value from the properties source + * (properties file or local "properties") + * @return the converted value, to be used for processing + * @see #setProperties + * @see #setLocations + * @see #setLocation + * @see #convertProperty(String, String) + */ + protected String convertPropertyValue(String originalValue) { + return originalValue; + } + + + /** + * Apply the given Properties to the given BeanFactory. + * @param beanFactory the BeanFactory used by the application context + * @param props the Properties to apply + * @throws org.springframework.beans.BeansException in case of errors + */ + protected abstract void processProperties(ConfigurableListableBeanFactory beanFactory, Properties props) + throws BeansException; + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/ProviderCreatingFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/ProviderCreatingFactoryBean.java new file mode 100644 index 0000000..1193329 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/ProviderCreatingFactoryBean.java @@ -0,0 +1,103 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.io.Serializable; + +import javax.inject.Provider; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * A {@link org.springframework.beans.factory.FactoryBean} implementation that + * returns a value which is a JSR-330 {@link javax.inject.Provider} that in turn + * returns a bean sourced from a {@link org.springframework.beans.factory.BeanFactory}. + * + *

This is basically a JSR-330 compliant variant of Spring's good old + * {@link ObjectFactoryCreatingFactoryBean}. It can be used for traditional + * external dependency injection configuration that targets a property or + * constructor argument of type {@code javax.inject.Provider}, as an + * alternative to JSR-330's {@code @Inject} annotation-driven approach. + * + * @author Juergen Hoeller + * @since 3.0.2 + * @see javax.inject.Provider + * @see ObjectFactoryCreatingFactoryBean + */ +public class ProviderCreatingFactoryBean extends AbstractFactoryBean> { + + @Nullable + private String targetBeanName; + + + /** + * Set the name of the target bean. + *

The target does not have to be a non-singleton bean, but realistically + * always will be (because if the target bean were a singleton, then said singleton + * bean could simply be injected straight into the dependent object, thus obviating + * the need for the extra level of indirection afforded by this factory approach). + */ + public void setTargetBeanName(String targetBeanName) { + this.targetBeanName = targetBeanName; + } + + @Override + public void afterPropertiesSet() throws Exception { + Assert.hasText(this.targetBeanName, "Property 'targetBeanName' is required"); + super.afterPropertiesSet(); + } + + + @Override + public Class getObjectType() { + return Provider.class; + } + + @Override + protected Provider createInstance() { + BeanFactory beanFactory = getBeanFactory(); + Assert.state(beanFactory != null, "No BeanFactory available"); + Assert.state(this.targetBeanName != null, "No target bean name specified"); + return new TargetBeanProvider(beanFactory, this.targetBeanName); + } + + + /** + * Independent inner class - for serialization purposes. + */ + @SuppressWarnings("serial") + private static class TargetBeanProvider implements Provider, Serializable { + + private final BeanFactory beanFactory; + + private final String targetBeanName; + + public TargetBeanProvider(BeanFactory beanFactory, String targetBeanName) { + this.beanFactory = beanFactory; + this.targetBeanName = targetBeanName; + } + + @Override + public Object get() throws BeansException { + return this.beanFactory.getBean(this.targetBeanName); + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/RuntimeBeanReference.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/RuntimeBeanReference.java new file mode 100644 index 0000000..11b4f1a --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/RuntimeBeanReference.java @@ -0,0 +1,158 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Immutable placeholder class used for a property value object when it's + * a reference to another bean in the factory, to be resolved at runtime. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see BeanDefinition#getPropertyValues() + * @see org.springframework.beans.factory.BeanFactory#getBean(String) + * @see org.springframework.beans.factory.BeanFactory#getBean(Class) + */ +public class RuntimeBeanReference implements BeanReference { + + private final String beanName; + + @Nullable + private final Class beanType; + + private final boolean toParent; + + @Nullable + private Object source; + + + /** + * Create a new RuntimeBeanReference to the given bean name. + * @param beanName name of the target bean + */ + public RuntimeBeanReference(String beanName) { + this(beanName, false); + } + + /** + * Create a new RuntimeBeanReference to the given bean name, + * with the option to mark it as reference to a bean in the parent factory. + * @param beanName name of the target bean + * @param toParent whether this is an explicit reference to a bean in the + * parent factory + */ + public RuntimeBeanReference(String beanName, boolean toParent) { + Assert.hasText(beanName, "'beanName' must not be empty"); + this.beanName = beanName; + this.beanType = null; + this.toParent = toParent; + } + + /** + * Create a new RuntimeBeanReference to a bean of the given type. + * @param beanType type of the target bean + * @since 5.2 + */ + public RuntimeBeanReference(Class beanType) { + this(beanType, false); + } + + /** + * Create a new RuntimeBeanReference to a bean of the given type, + * with the option to mark it as reference to a bean in the parent factory. + * @param beanType type of the target bean + * @param toParent whether this is an explicit reference to a bean in the + * parent factory + * @since 5.2 + */ + public RuntimeBeanReference(Class beanType, boolean toParent) { + Assert.notNull(beanType, "'beanType' must not be empty"); + this.beanName = beanType.getName(); + this.beanType = beanType; + this.toParent = toParent; + } + + + /** + * Return the requested bean name, or the fully-qualified type name + * in case of by-type resolution. + * @see #getBeanType() + */ + @Override + public String getBeanName() { + return this.beanName; + } + + /** + * Return the requested bean type if resolution by type is demanded. + * @since 5.2 + */ + @Nullable + public Class getBeanType() { + return this.beanType; + } + + /** + * Return whether this is an explicit reference to a bean in the parent factory. + */ + public boolean isToParent() { + return this.toParent; + } + + /** + * Set the configuration source {@code Object} for this metadata element. + *

The exact type of the object will depend on the configuration mechanism used. + */ + public void setSource(@Nullable Object source) { + this.source = source; + } + + @Override + @Nullable + public Object getSource() { + return this.source; + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof RuntimeBeanReference)) { + return false; + } + RuntimeBeanReference that = (RuntimeBeanReference) other; + return (this.beanName.equals(that.beanName) && this.beanType == that.beanType && + this.toParent == that.toParent); + } + + @Override + public int hashCode() { + int result = this.beanName.hashCode(); + result = 29 * result + (this.toParent ? 1 : 0); + return result; + } + + @Override + public String toString() { + return '<' + getBeanName() + '>'; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/Scope.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/Scope.java new file mode 100644 index 0000000..d2054ae --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/Scope.java @@ -0,0 +1,154 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.lang.Nullable; + +/** + * Strategy interface used by a {@link ConfigurableBeanFactory}, + * representing a target scope to hold bean instances in. + * This allows for extending the BeanFactory's standard scopes + * {@link ConfigurableBeanFactory#SCOPE_SINGLETON "singleton"} and + * {@link ConfigurableBeanFactory#SCOPE_PROTOTYPE "prototype"} + * with custom further scopes, registered for a + * {@link ConfigurableBeanFactory#registerScope(String, Scope) specific key}. + * + *

{@link org.springframework.context.ApplicationContext} implementations + * such as a {@link org.springframework.web.context.WebApplicationContext} + * may register additional standard scopes specific to their environment, + * e.g. {@link org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST "request"} + * and {@link org.springframework.web.context.WebApplicationContext#SCOPE_SESSION "session"}, + * based on this Scope SPI. + * + *

Even if its primary use is for extended scopes in a web environment, + * this SPI is completely generic: It provides the ability to get and put + * objects from any underlying storage mechanism, such as an HTTP session + * or a custom conversation mechanism. The name passed into this class's + * {@code get} and {@code remove} methods will identify the + * target object in the current scope. + * + *

{@code Scope} implementations are expected to be thread-safe. + * One {@code Scope} instance can be used with multiple bean factories + * at the same time, if desired (unless it explicitly wants to be aware of + * the containing BeanFactory), with any number of threads accessing + * the {@code Scope} concurrently from any number of factories. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @since 2.0 + * @see ConfigurableBeanFactory#registerScope + * @see CustomScopeConfigurer + * @see org.springframework.aop.scope.ScopedProxyFactoryBean + * @see org.springframework.web.context.request.RequestScope + * @see org.springframework.web.context.request.SessionScope + */ +public interface Scope { + + /** + * Return the object with the given name from the underlying scope, + * {@link org.springframework.beans.factory.ObjectFactory#getObject() creating it} + * if not found in the underlying storage mechanism. + *

This is the central operation of a Scope, and the only operation + * that is absolutely required. + * @param name the name of the object to retrieve + * @param objectFactory the {@link ObjectFactory} to use to create the scoped + * object if it is not present in the underlying storage mechanism + * @return the desired object (never {@code null}) + * @throws IllegalStateException if the underlying scope is not currently active + */ + Object get(String name, ObjectFactory objectFactory); + + /** + * Remove the object with the given {@code name} from the underlying scope. + *

Returns {@code null} if no object was found; otherwise + * returns the removed {@code Object}. + *

Note that an implementation should also remove a registered destruction + * callback for the specified object, if any. It does, however, not + * need to execute a registered destruction callback in this case, + * since the object will be destroyed by the caller (if appropriate). + *

Note: This is an optional operation. Implementations may throw + * {@link UnsupportedOperationException} if they do not support explicitly + * removing an object. + * @param name the name of the object to remove + * @return the removed object, or {@code null} if no object was present + * @throws IllegalStateException if the underlying scope is not currently active + * @see #registerDestructionCallback + */ + @Nullable + Object remove(String name); + + /** + * Register a callback to be executed on destruction of the specified + * object in the scope (or at destruction of the entire scope, if the + * scope does not destroy individual objects but rather only terminates + * in its entirety). + *

Note: This is an optional operation. This method will only + * be called for scoped beans with actual destruction configuration + * (DisposableBean, destroy-method, DestructionAwareBeanPostProcessor). + * Implementations should do their best to execute a given callback + * at the appropriate time. If such a callback is not supported by the + * underlying runtime environment at all, the callback must be + * ignored and a corresponding warning should be logged. + *

Note that 'destruction' refers to automatic destruction of + * the object as part of the scope's own lifecycle, not to the individual + * scoped object having been explicitly removed by the application. + * If a scoped object gets removed via this facade's {@link #remove(String)} + * method, any registered destruction callback should be removed as well, + * assuming that the removed object will be reused or manually destroyed. + * @param name the name of the object to execute the destruction callback for + * @param callback the destruction callback to be executed. + * Note that the passed-in Runnable will never throw an exception, + * so it can safely be executed without an enclosing try-catch block. + * Furthermore, the Runnable will usually be serializable, provided + * that its target object is serializable as well. + * @throws IllegalStateException if the underlying scope is not currently active + * @see org.springframework.beans.factory.DisposableBean + * @see org.springframework.beans.factory.support.AbstractBeanDefinition#getDestroyMethodName() + * @see DestructionAwareBeanPostProcessor + */ + void registerDestructionCallback(String name, Runnable callback); + + /** + * Resolve the contextual object for the given key, if any. + * E.g. the HttpServletRequest object for key "request". + * @param key the contextual key + * @return the corresponding object, or {@code null} if none found + * @throws IllegalStateException if the underlying scope is not currently active + */ + @Nullable + Object resolveContextualObject(String key); + + /** + * Return the conversation ID for the current underlying scope, if any. + *

The exact meaning of the conversation ID depends on the underlying + * storage mechanism. In the case of session-scoped objects, the + * conversation ID would typically be equal to (or derived from) the + * {@link javax.servlet.http.HttpSession#getId() session ID}; in the + * case of a custom conversation that sits within the overall session, + * the specific ID for the current conversation would be appropriate. + *

Note: This is an optional operation. It is perfectly valid to + * return {@code null} in an implementation of this method if the + * underlying storage mechanism has no obvious candidate for such an ID. + * @return the conversation ID, or {@code null} if there is no + * conversation ID for the current scope + * @throws IllegalStateException if the underlying scope is not currently active + */ + @Nullable + String getConversationId(); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/SmartInstantiationAwareBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/SmartInstantiationAwareBeanPostProcessor.java new file mode 100644 index 0000000..d4a6ce7 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/SmartInstantiationAwareBeanPostProcessor.java @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.lang.reflect.Constructor; + +import org.springframework.beans.BeansException; +import org.springframework.lang.Nullable; + +/** + * Extension of the {@link InstantiationAwareBeanPostProcessor} interface, + * adding a callback for predicting the eventual type of a processed bean. + * + *

NOTE: This interface is a special purpose interface, mainly for + * internal use within the framework. In general, application-provided + * post-processors should simply implement the plain {@link BeanPostProcessor} + * interface or derive from the {@link InstantiationAwareBeanPostProcessorAdapter} + * class. New methods might be added to this interface even in point releases. + * + * @author Juergen Hoeller + * @since 2.0.3 + * @see InstantiationAwareBeanPostProcessorAdapter + */ +public interface SmartInstantiationAwareBeanPostProcessor extends InstantiationAwareBeanPostProcessor { + + /** + * Predict the type of the bean to be eventually returned from this + * processor's {@link #postProcessBeforeInstantiation} callback. + *

The default implementation returns {@code null}. + * @param beanClass the raw class of the bean + * @param beanName the name of the bean + * @return the type of the bean, or {@code null} if not predictable + * @throws org.springframework.beans.BeansException in case of errors + */ + @Nullable + default Class predictBeanType(Class beanClass, String beanName) throws BeansException { + return null; + } + + /** + * Determine the candidate constructors to use for the given bean. + *

The default implementation returns {@code null}. + * @param beanClass the raw class of the bean (never {@code null}) + * @param beanName the name of the bean + * @return the candidate constructors, or {@code null} if none specified + * @throws org.springframework.beans.BeansException in case of errors + */ + @Nullable + default Constructor[] determineCandidateConstructors(Class beanClass, String beanName) + throws BeansException { + + return null; + } + + /** + * Obtain a reference for early access to the specified bean, + * typically for the purpose of resolving a circular reference. + *

This callback gives post-processors a chance to expose a wrapper + * early - that is, before the target bean instance is fully initialized. + * The exposed object should be equivalent to the what + * {@link #postProcessBeforeInitialization} / {@link #postProcessAfterInitialization} + * would expose otherwise. Note that the object returned by this method will + * be used as bean reference unless the post-processor returns a different + * wrapper from said post-process callbacks. In other words: Those post-process + * callbacks may either eventually expose the same reference or alternatively + * return the raw bean instance from those subsequent callbacks (if the wrapper + * for the affected bean has been built for a call to this method already, + * it will be exposes as final bean reference by default). + *

The default implementation returns the given {@code bean} as-is. + * @param bean the raw bean instance + * @param beanName the name of the bean + * @return the object to expose as bean reference + * (typically with the passed-in bean instance as default) + * @throws org.springframework.beans.BeansException in case of errors + */ + default Object getEarlyBeanReference(Object bean, String beanName) throws BeansException { + return bean; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlMapFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlMapFactoryBean.java new file mode 100644 index 0000000..bc77a9f --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlMapFactoryBean.java @@ -0,0 +1,144 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.lang.Nullable; + +/** + * Factory for a {@code Map} that reads from a YAML source, preserving the + * YAML-declared value types and their structure. + * + *

YAML is a nice human-readable format for configuration, and it has some + * useful hierarchical properties. It's more or less a superset of JSON, so it + * has a lot of similar features. + * + *

If multiple resources are provided the later ones will override entries in + * the earlier ones hierarchically; that is, all entries with the same nested key + * of type {@code Map} at any depth are merged. For example: + * + *

+ * foo:
+ *   bar:
+ *    one: two
+ * three: four
+ * 
+ * + * plus (later in the list) + * + *
+ * foo:
+ *   bar:
+ *    one: 2
+ * five: six
+ * 
+ * + * results in an effective input of + * + *
+ * foo:
+ *   bar:
+ *    one: 2
+ * three: four
+ * five: six
+ * 
+ * + * Note that the value of "foo" in the first document is not simply replaced + * with the value in the second, but its nested values are merged. + * + *

Requires SnakeYAML 1.18 or higher, as of Spring Framework 5.0.6. + * + * @author Dave Syer + * @author Juergen Hoeller + * @since 4.1 + */ +public class YamlMapFactoryBean extends YamlProcessor implements FactoryBean>, InitializingBean { + + private boolean singleton = true; + + @Nullable + private Map map; + + + /** + * Set if a singleton should be created, or a new object on each request + * otherwise. Default is {@code true} (a singleton). + */ + public void setSingleton(boolean singleton) { + this.singleton = singleton; + } + + @Override + public boolean isSingleton() { + return this.singleton; + } + + @Override + public void afterPropertiesSet() { + if (isSingleton()) { + this.map = createMap(); + } + } + + @Override + @Nullable + public Map getObject() { + return (this.map != null ? this.map : createMap()); + } + + @Override + public Class getObjectType() { + return Map.class; + } + + + /** + * Template method that subclasses may override to construct the object + * returned by this factory. + *

Invoked lazily the first time {@link #getObject()} is invoked in + * case of a shared singleton; else, on each {@link #getObject()} call. + *

The default implementation returns the merged {@code Map} instance. + * @return the object returned by this factory + * @see #process(MatchCallback) + */ + protected Map createMap() { + Map result = new LinkedHashMap<>(); + process((properties, map) -> merge(result, map)); + return result; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private void merge(Map output, Map map) { + map.forEach((key, value) -> { + Object existing = output.get(key); + if (value instanceof Map && existing instanceof Map) { + // Inner cast required by Eclipse IDE. + Map result = new LinkedHashMap<>((Map) existing); + merge(result, (Map) value); + output.put(key, result); + } + else { + output.put(key, value); + } + }); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java new file mode 100644 index 0000000..a7ea8c8 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java @@ -0,0 +1,453 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.io.IOException; +import java.io.Reader; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.reader.UnicodeReader; +import org.yaml.snakeyaml.representer.Representer; + +import org.springframework.core.CollectionFactory; +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Base class for YAML factories. + * + *

Requires SnakeYAML 1.18 or higher, as of Spring Framework 5.0.6. + * + * @author Dave Syer + * @author Juergen Hoeller + * @author Sam Brannen + * @since 4.1 + */ +public abstract class YamlProcessor { + + private final Log logger = LogFactory.getLog(getClass()); + + private ResolutionMethod resolutionMethod = ResolutionMethod.OVERRIDE; + + private Resource[] resources = new Resource[0]; + + private List documentMatchers = Collections.emptyList(); + + private boolean matchDefault = true; + + private Set supportedTypes = Collections.emptySet(); + + + /** + * A map of document matchers allowing callers to selectively use only + * some of the documents in a YAML resource. In YAML documents are + * separated by {@code ---} lines, and each document is converted + * to properties before the match is made. E.g. + *

+	 * environment: dev
+	 * url: https://dev.bar.com
+	 * name: Developer Setup
+	 * ---
+	 * environment: prod
+	 * url:https://foo.bar.com
+	 * name: My Cool App
+	 * 
+ * when mapped with + *
+	 * setDocumentMatchers(properties ->
+	 *     ("prod".equals(properties.getProperty("environment")) ? MatchStatus.FOUND : MatchStatus.NOT_FOUND));
+	 * 
+ * would end up as + *
+	 * environment=prod
+	 * url=https://foo.bar.com
+	 * name=My Cool App
+	 * 
+ */ + public void setDocumentMatchers(DocumentMatcher... matchers) { + this.documentMatchers = Arrays.asList(matchers); + } + + /** + * Flag indicating that a document for which all the + * {@link #setDocumentMatchers(DocumentMatcher...) document matchers} abstain will + * nevertheless match. Default is {@code true}. + */ + public void setMatchDefault(boolean matchDefault) { + this.matchDefault = matchDefault; + } + + /** + * Method to use for resolving resources. Each resource will be converted to a Map, + * so this property is used to decide which map entries to keep in the final output + * from this factory. Default is {@link ResolutionMethod#OVERRIDE}. + */ + public void setResolutionMethod(ResolutionMethod resolutionMethod) { + Assert.notNull(resolutionMethod, "ResolutionMethod must not be null"); + this.resolutionMethod = resolutionMethod; + } + + /** + * Set locations of YAML {@link Resource resources} to be loaded. + * @see ResolutionMethod + */ + public void setResources(Resource... resources) { + this.resources = resources; + } + + /** + * Set the supported types that can be loaded from YAML documents. + *

If no supported types are configured, all types encountered in YAML + * documents will be supported. If an unsupported type is encountered, an + * {@link IllegalStateException} will be thrown when the corresponding YAML + * node is processed. + * @param supportedTypes the supported types, or an empty array to clear the + * supported types + * @since 5.1.16 + * @see #createYaml() + */ + public void setSupportedTypes(Class... supportedTypes) { + if (ObjectUtils.isEmpty(supportedTypes)) { + this.supportedTypes = Collections.emptySet(); + } + else { + Assert.noNullElements(supportedTypes, "'supportedTypes' must not contain null elements"); + this.supportedTypes = Arrays.stream(supportedTypes).map(Class::getName) + .collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet)); + } + } + + /** + * Provide an opportunity for subclasses to process the Yaml parsed from the supplied + * resources. Each resource is parsed in turn and the documents inside checked against + * the {@link #setDocumentMatchers(DocumentMatcher...) matchers}. If a document + * matches it is passed into the callback, along with its representation as Properties. + * Depending on the {@link #setResolutionMethod(ResolutionMethod)} not all of the + * documents will be parsed. + * @param callback a callback to delegate to once matching documents are found + * @see #createYaml() + */ + protected void process(MatchCallback callback) { + Yaml yaml = createYaml(); + for (Resource resource : this.resources) { + boolean found = process(callback, yaml, resource); + if (this.resolutionMethod == ResolutionMethod.FIRST_FOUND && found) { + return; + } + } + } + + /** + * Create the {@link Yaml} instance to use. + *

The default implementation sets the "allowDuplicateKeys" flag to {@code false}, + * enabling built-in duplicate key handling in SnakeYAML 1.18+. + *

As of Spring Framework 5.1.16, if custom {@linkplain #setSupportedTypes + * supported types} have been configured, the default implementation creates + * a {@code Yaml} instance that filters out unsupported types encountered in + * YAML documents. If an unsupported type is encountered, an + * {@link IllegalStateException} will be thrown when the node is processed. + * @see LoaderOptions#setAllowDuplicateKeys(boolean) + */ + protected Yaml createYaml() { + LoaderOptions loaderOptions = new LoaderOptions(); + loaderOptions.setAllowDuplicateKeys(false); + + if (!this.supportedTypes.isEmpty()) { + return new Yaml(new FilteringConstructor(loaderOptions), new Representer(), + new DumperOptions(), loaderOptions); + } + return new Yaml(loaderOptions); + } + + private boolean process(MatchCallback callback, Yaml yaml, Resource resource) { + int count = 0; + try { + if (logger.isDebugEnabled()) { + logger.debug("Loading from YAML: " + resource); + } + try (Reader reader = new UnicodeReader(resource.getInputStream())) { + for (Object object : yaml.loadAll(reader)) { + if (object != null && process(asMap(object), callback)) { + count++; + if (this.resolutionMethod == ResolutionMethod.FIRST_FOUND) { + break; + } + } + } + if (logger.isDebugEnabled()) { + logger.debug("Loaded " + count + " document" + (count > 1 ? "s" : "") + + " from YAML resource: " + resource); + } + } + } + catch (IOException ex) { + handleProcessError(resource, ex); + } + return (count > 0); + } + + private void handleProcessError(Resource resource, IOException ex) { + if (this.resolutionMethod != ResolutionMethod.FIRST_FOUND && + this.resolutionMethod != ResolutionMethod.OVERRIDE_AND_IGNORE) { + throw new IllegalStateException(ex); + } + if (logger.isWarnEnabled()) { + logger.warn("Could not load map from " + resource + ": " + ex.getMessage()); + } + } + + @SuppressWarnings("unchecked") + private Map asMap(Object object) { + // YAML can have numbers as keys + Map result = new LinkedHashMap<>(); + if (!(object instanceof Map)) { + // A document can be a text literal + result.put("document", object); + return result; + } + + Map map = (Map) object; + map.forEach((key, value) -> { + if (value instanceof Map) { + value = asMap(value); + } + if (key instanceof CharSequence) { + result.put(key.toString(), value); + } + else { + // It has to be a map key in this case + result.put("[" + key.toString() + "]", value); + } + }); + return result; + } + + private boolean process(Map map, MatchCallback callback) { + Properties properties = CollectionFactory.createStringAdaptingProperties(); + properties.putAll(getFlattenedMap(map)); + + if (this.documentMatchers.isEmpty()) { + if (logger.isDebugEnabled()) { + logger.debug("Merging document (no matchers set): " + map); + } + callback.process(properties, map); + return true; + } + + MatchStatus result = MatchStatus.ABSTAIN; + for (DocumentMatcher matcher : this.documentMatchers) { + MatchStatus match = matcher.matches(properties); + result = MatchStatus.getMostSpecific(match, result); + if (match == MatchStatus.FOUND) { + if (logger.isDebugEnabled()) { + logger.debug("Matched document with document matcher: " + properties); + } + callback.process(properties, map); + return true; + } + } + + if (result == MatchStatus.ABSTAIN && this.matchDefault) { + if (logger.isDebugEnabled()) { + logger.debug("Matched document with default matcher: " + map); + } + callback.process(properties, map); + return true; + } + + if (logger.isDebugEnabled()) { + logger.debug("Unmatched document: " + map); + } + return false; + } + + /** + * Return a flattened version of the given map, recursively following any nested Map + * or Collection values. Entries from the resulting map retain the same order as the + * source. When called with the Map from a {@link MatchCallback} the result will + * contain the same values as the {@link MatchCallback} Properties. + * @param source the source map + * @return a flattened map + * @since 4.1.3 + */ + protected final Map getFlattenedMap(Map source) { + Map result = new LinkedHashMap<>(); + buildFlattenedMap(result, source, null); + return result; + } + + private void buildFlattenedMap(Map result, Map source, @Nullable String path) { + source.forEach((key, value) -> { + if (StringUtils.hasText(path)) { + if (key.startsWith("[")) { + key = path + key; + } + else { + key = path + '.' + key; + } + } + if (value instanceof String) { + result.put(key, value); + } + else if (value instanceof Map) { + // Need a compound key + @SuppressWarnings("unchecked") + Map map = (Map) value; + buildFlattenedMap(result, map, key); + } + else if (value instanceof Collection) { + // Need a compound key + @SuppressWarnings("unchecked") + Collection collection = (Collection) value; + if (collection.isEmpty()) { + result.put(key, ""); + } + else { + int count = 0; + for (Object object : collection) { + buildFlattenedMap(result, Collections.singletonMap( + "[" + (count++) + "]", object), key); + } + } + } + else { + result.put(key, (value != null ? value : "")); + } + }); + } + + + /** + * Callback interface used to process the YAML parsing results. + */ + @FunctionalInterface + public interface MatchCallback { + + /** + * Process the given representation of the parsing results. + * @param properties the properties to process (as a flattened + * representation with indexed keys in case of a collection or map) + * @param map the result map (preserving the original value structure + * in the YAML document) + */ + void process(Properties properties, Map map); + } + + + /** + * Strategy interface used to test if properties match. + */ + @FunctionalInterface + public interface DocumentMatcher { + + /** + * Test if the given properties match. + * @param properties the properties to test + * @return the status of the match + */ + MatchStatus matches(Properties properties); + } + + + /** + * Status returned from {@link DocumentMatcher#matches(java.util.Properties)}. + */ + public enum MatchStatus { + + /** + * A match was found. + */ + FOUND, + + /** + * No match was found. + */ + NOT_FOUND, + + /** + * The matcher should not be considered. + */ + ABSTAIN; + + /** + * Compare two {@link MatchStatus} items, returning the most specific status. + */ + public static MatchStatus getMostSpecific(MatchStatus a, MatchStatus b) { + return (a.ordinal() < b.ordinal() ? a : b); + } + } + + + /** + * Method to use for resolving resources. + */ + public enum ResolutionMethod { + + /** + * Replace values from earlier in the list. + */ + OVERRIDE, + + /** + * Replace values from earlier in the list, ignoring any failures. + */ + OVERRIDE_AND_IGNORE, + + /** + * Take the first resource in the list that exists and use just that. + */ + FIRST_FOUND + } + + + /** + * {@link Constructor} that supports filtering of unsupported types. + *

If an unsupported type is encountered in a YAML document, an + * {@link IllegalStateException} will be thrown from {@link #getClassForName}. + */ + private class FilteringConstructor extends Constructor { + + FilteringConstructor(LoaderOptions loaderOptions) { + super(loaderOptions); + } + + @Override + protected Class getClassForName(String name) throws ClassNotFoundException { + Assert.state(YamlProcessor.this.supportedTypes.contains(name), + () -> "Unsupported type encountered in YAML document: " + name); + return super.getClassForName(name); + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlPropertiesFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlPropertiesFactoryBean.java new file mode 100644 index 0000000..71088f1 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlPropertiesFactoryBean.java @@ -0,0 +1,139 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.util.Properties; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.CollectionFactory; +import org.springframework.lang.Nullable; + +/** + * Factory for {@link java.util.Properties} that reads from a YAML source, + * exposing a flat structure of String property values. + * + *

YAML is a nice human-readable format for configuration, and it has some + * useful hierarchical properties. It's more or less a superset of JSON, so it + * has a lot of similar features. + * + *

Note: All exposed values are of type {@code String} for access through + * the common {@link Properties#getProperty} method (e.g. in configuration property + * resolution through {@link PropertyResourceConfigurer#setProperties(Properties)}). + * If this is not desirable, use {@link YamlMapFactoryBean} instead. + * + *

The Properties created by this factory have nested paths for hierarchical + * objects, so for instance this YAML + * + *

+ * environments:
+ *   dev:
+ *     url: https://dev.bar.com
+ *     name: Developer Setup
+ *   prod:
+ *     url: https://foo.bar.com
+ *     name: My Cool App
+ * 
+ * + * is transformed into these properties: + * + *
+ * environments.dev.url=https://dev.bar.com
+ * environments.dev.name=Developer Setup
+ * environments.prod.url=https://foo.bar.com
+ * environments.prod.name=My Cool App
+ * 
+ * + * Lists are split as property keys with [] dereferencers, for + * example this YAML: + * + *
+ * servers:
+ * - dev.bar.com
+ * - foo.bar.com
+ * 
+ * + * becomes properties like this: + * + *
+ * servers[0]=dev.bar.com
+ * servers[1]=foo.bar.com
+ * 
+ * + *

Requires SnakeYAML 1.18 or higher, as of Spring Framework 5.0.6. + * + * @author Dave Syer + * @author Stephane Nicoll + * @author Juergen Hoeller + * @since 4.1 + */ +public class YamlPropertiesFactoryBean extends YamlProcessor implements FactoryBean, InitializingBean { + + private boolean singleton = true; + + @Nullable + private Properties properties; + + + /** + * Set if a singleton should be created, or a new object on each request + * otherwise. Default is {@code true} (a singleton). + */ + public void setSingleton(boolean singleton) { + this.singleton = singleton; + } + + @Override + public boolean isSingleton() { + return this.singleton; + } + + @Override + public void afterPropertiesSet() { + if (isSingleton()) { + this.properties = createProperties(); + } + } + + @Override + @Nullable + public Properties getObject() { + return (this.properties != null ? this.properties : createProperties()); + } + + @Override + public Class getObjectType() { + return Properties.class; + } + + + /** + * Template method that subclasses may override to construct the object + * returned by this factory. The default implementation returns a + * properties with the content of all resources. + *

Invoked lazily the first time {@link #getObject()} is invoked in + * case of a shared singleton; else, on each {@link #getObject()} call. + * @return the object returned by this factory + * @see #process(MatchCallback) + */ + protected Properties createProperties() { + Properties result = CollectionFactory.createStringAdaptingProperties(); + process((properties, map) -> result.putAll(properties)); + return result; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/package-info.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/package-info.java new file mode 100644 index 0000000..280e916 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/package-info.java @@ -0,0 +1,9 @@ +/** + * SPI interfaces and configuration-related convenience classes for bean factories. + */ +@NonNullApi +@NonNullFields +package org.springframework.beans.factory.config; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/package-info.java b/spring-beans/src/main/java/org/springframework/beans/factory/package-info.java new file mode 100644 index 0000000..a29b453 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/package-info.java @@ -0,0 +1,17 @@ +/** + * The core package implementing Spring's lightweight Inversion of Control (IoC) container. + * + *

Provides an alternative to the Singleton and Prototype design + * patterns, including a consistent approach to configuration management. + * Builds on the org.springframework.beans package. + * + *

This package and related packages are discussed in Chapter 11 of + * Expert One-On-One J2EE Design and Development + * by Rod Johnson (Wrox, 2002). + */ +@NonNullApi +@NonNullFields +package org.springframework.beans.factory; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/AbstractServiceLoaderBasedFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/AbstractServiceLoaderBasedFactoryBean.java new file mode 100644 index 0000000..974940a --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/AbstractServiceLoaderBasedFactoryBean.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.serviceloader; + +import java.util.ServiceLoader; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.config.AbstractFactoryBean; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Abstract base class for FactoryBeans operating on the + * JDK 1.6 {@link java.util.ServiceLoader} facility. + * + * @author Juergen Hoeller + * @since 2.5 + * @see java.util.ServiceLoader + */ +public abstract class AbstractServiceLoaderBasedFactoryBean extends AbstractFactoryBean + implements BeanClassLoaderAware { + + @Nullable + private Class serviceType; + + @Nullable + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + + /** + * Specify the desired service type (typically the service's public API). + */ + public void setServiceType(@Nullable Class serviceType) { + this.serviceType = serviceType; + } + + /** + * Return the desired service type. + */ + @Nullable + public Class getServiceType() { + return this.serviceType; + } + + @Override + public void setBeanClassLoader(@Nullable ClassLoader beanClassLoader) { + this.beanClassLoader = beanClassLoader; + } + + + /** + * Delegates to {@link #getObjectToExpose(java.util.ServiceLoader)}. + * @return the object to expose + */ + @Override + protected Object createInstance() { + Assert.notNull(getServiceType(), "Property 'serviceType' is required"); + return getObjectToExpose(ServiceLoader.load(getServiceType(), this.beanClassLoader)); + } + + /** + * Determine the actual object to expose for the given ServiceLoader. + *

Left to concrete subclasses. + * @param serviceLoader the ServiceLoader for the configured service class + * @return the object to expose + */ + protected abstract Object getObjectToExpose(ServiceLoader serviceLoader); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/ServiceFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/ServiceFactoryBean.java new file mode 100644 index 0000000..535a537 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/ServiceFactoryBean.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2007 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.serviceloader; + +import java.util.Iterator; +import java.util.ServiceLoader; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.lang.Nullable; + +/** + * {@link org.springframework.beans.factory.FactoryBean} that exposes the + * 'primary' service for the configured service class, obtained through + * the JDK 1.6 {@link java.util.ServiceLoader} facility. + * + * @author Juergen Hoeller + * @since 2.5 + * @see java.util.ServiceLoader + */ +public class ServiceFactoryBean extends AbstractServiceLoaderBasedFactoryBean implements BeanClassLoaderAware { + + @Override + protected Object getObjectToExpose(ServiceLoader serviceLoader) { + Iterator it = serviceLoader.iterator(); + if (!it.hasNext()) { + throw new IllegalStateException( + "ServiceLoader could not find service for type [" + getServiceType() + "]"); + } + return it.next(); + } + + @Override + @Nullable + public Class getObjectType() { + return getServiceType(); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/ServiceListFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/ServiceListFactoryBean.java new file mode 100644 index 0000000..6e97d2f --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/ServiceListFactoryBean.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.serviceloader; + +import java.util.ArrayList; +import java.util.List; +import java.util.ServiceLoader; + +import org.springframework.beans.factory.BeanClassLoaderAware; + +/** + * {@link org.springframework.beans.factory.FactoryBean} that exposes all + * services for the configured service class, represented as a List of service objects, + * obtained through the JDK 1.6 {@link java.util.ServiceLoader} facility. + * + * @author Juergen Hoeller + * @since 2.5 + * @see java.util.ServiceLoader + */ +public class ServiceListFactoryBean extends AbstractServiceLoaderBasedFactoryBean implements BeanClassLoaderAware { + + @Override + protected Object getObjectToExpose(ServiceLoader serviceLoader) { + List result = new ArrayList<>(); + for (Object loaderObject : serviceLoader) { + result.add(loaderObject); + } + return result; + } + + @Override + public Class getObjectType() { + return List.class; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/ServiceLoaderFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/ServiceLoaderFactoryBean.java new file mode 100644 index 0000000..53c40ef --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/ServiceLoaderFactoryBean.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2007 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.serviceloader; + +import java.util.ServiceLoader; + +import org.springframework.beans.factory.BeanClassLoaderAware; + +/** + * {@link org.springframework.beans.factory.FactoryBean} that exposes the + * JDK 1.6 {@link java.util.ServiceLoader} for the configured service class. + * + * @author Juergen Hoeller + * @since 2.5 + * @see java.util.ServiceLoader + */ +public class ServiceLoaderFactoryBean extends AbstractServiceLoaderBasedFactoryBean implements BeanClassLoaderAware { + + @Override + protected Object getObjectToExpose(ServiceLoader serviceLoader) { + return serviceLoader; + } + + @Override + public Class getObjectType() { + return ServiceLoader.class; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/package-info.java b/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/package-info.java new file mode 100644 index 0000000..4e66e56 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/package-info.java @@ -0,0 +1,9 @@ +/** + * Support package for the Java 6 ServiceLoader facility. + */ +@NonNullApi +@NonNullFields +package org.springframework.beans.factory.serviceloader; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java new file mode 100644 index 0000000..c30bc32 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java @@ -0,0 +1,1244 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support; + +import java.lang.reflect.Constructor; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +import org.springframework.beans.BeanMetadataAttributeAccessor; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.core.ResolvableType; +import org.springframework.core.io.DescriptiveResource; +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Base class for concrete, full-fledged {@link BeanDefinition} classes, + * factoring out common properties of {@link GenericBeanDefinition}, + * {@link RootBeanDefinition}, and {@link ChildBeanDefinition}. + * + *

The autowire constants match the ones defined in the + * {@link org.springframework.beans.factory.config.AutowireCapableBeanFactory} + * interface. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Rob Harrop + * @author Mark Fisher + * @see GenericBeanDefinition + * @see RootBeanDefinition + * @see ChildBeanDefinition + */ +@SuppressWarnings("serial") +public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccessor + implements BeanDefinition, Cloneable { + + /** + * Constant for the default scope name: {@code ""}, equivalent to singleton + * status unless overridden from a parent bean definition (if applicable). + */ + public static final String SCOPE_DEFAULT = ""; + + /** + * Constant that indicates no external autowiring at all. + * @see #setAutowireMode + */ + public static final int AUTOWIRE_NO = AutowireCapableBeanFactory.AUTOWIRE_NO; + + /** + * Constant that indicates autowiring bean properties by name. + * @see #setAutowireMode + */ + public static final int AUTOWIRE_BY_NAME = AutowireCapableBeanFactory.AUTOWIRE_BY_NAME; + + /** + * Constant that indicates autowiring bean properties by type. + * @see #setAutowireMode + */ + public static final int AUTOWIRE_BY_TYPE = AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE; + + /** + * Constant that indicates autowiring a constructor. + * @see #setAutowireMode + */ + public static final int AUTOWIRE_CONSTRUCTOR = AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR; + + /** + * Constant that indicates determining an appropriate autowire strategy + * through introspection of the bean class. + * @see #setAutowireMode + * @deprecated as of Spring 3.0: If you are using mixed autowiring strategies, + * use annotation-based autowiring for clearer demarcation of autowiring needs. + */ + @Deprecated + public static final int AUTOWIRE_AUTODETECT = AutowireCapableBeanFactory.AUTOWIRE_AUTODETECT; + + /** + * Constant that indicates no dependency check at all. + * @see #setDependencyCheck + */ + public static final int DEPENDENCY_CHECK_NONE = 0; + + /** + * Constant that indicates dependency checking for object references. + * @see #setDependencyCheck + */ + public static final int DEPENDENCY_CHECK_OBJECTS = 1; + + /** + * Constant that indicates dependency checking for "simple" properties. + * @see #setDependencyCheck + * @see org.springframework.beans.BeanUtils#isSimpleProperty + */ + public static final int DEPENDENCY_CHECK_SIMPLE = 2; + + /** + * Constant that indicates dependency checking for all properties + * (object references as well as "simple" properties). + * @see #setDependencyCheck + */ + public static final int DEPENDENCY_CHECK_ALL = 3; + + /** + * Constant that indicates the container should attempt to infer the + * {@link #setDestroyMethodName destroy method name} for a bean as opposed to + * explicit specification of a method name. The value {@value} is specifically + * designed to include characters otherwise illegal in a method name, ensuring + * no possibility of collisions with legitimately named methods having the same + * name. + *

Currently, the method names detected during destroy method inference + * are "close" and "shutdown", if present on the specific bean class. + */ + public static final String INFER_METHOD = "(inferred)"; + + + @Nullable + private volatile Object beanClass; + + @Nullable + private String scope = SCOPE_DEFAULT; + + private boolean abstractFlag = false; + + @Nullable + private Boolean lazyInit; + + private int autowireMode = AUTOWIRE_NO; + + private int dependencyCheck = DEPENDENCY_CHECK_NONE; + + @Nullable + private String[] dependsOn; + + private boolean autowireCandidate = true; + + private boolean primary = false; + + private final Map qualifiers = new LinkedHashMap<>(); + + @Nullable + private Supplier instanceSupplier; + + private boolean nonPublicAccessAllowed = true; + + private boolean lenientConstructorResolution = true; + + @Nullable + private String factoryBeanName; + + @Nullable + private String factoryMethodName; + + @Nullable + private ConstructorArgumentValues constructorArgumentValues; + + @Nullable + private MutablePropertyValues propertyValues; + + private MethodOverrides methodOverrides = new MethodOverrides(); + + @Nullable + private String initMethodName; + + @Nullable + private String destroyMethodName; + + private boolean enforceInitMethod = true; + + private boolean enforceDestroyMethod = true; + + private boolean synthetic = false; + + private int role = BeanDefinition.ROLE_APPLICATION; + + @Nullable + private String description; + + @Nullable + private Resource resource; + + + /** + * Create a new AbstractBeanDefinition with default settings. + */ + protected AbstractBeanDefinition() { + this(null, null); + } + + /** + * Create a new AbstractBeanDefinition with the given + * constructor argument values and property values. + */ + protected AbstractBeanDefinition(@Nullable ConstructorArgumentValues cargs, @Nullable MutablePropertyValues pvs) { + this.constructorArgumentValues = cargs; + this.propertyValues = pvs; + } + + /** + * Create a new AbstractBeanDefinition as a deep copy of the given + * bean definition. + * @param original the original bean definition to copy from + */ + protected AbstractBeanDefinition(BeanDefinition original) { + setParentName(original.getParentName()); + setBeanClassName(original.getBeanClassName()); + setScope(original.getScope()); + setAbstract(original.isAbstract()); + setFactoryBeanName(original.getFactoryBeanName()); + setFactoryMethodName(original.getFactoryMethodName()); + setRole(original.getRole()); + setSource(original.getSource()); + copyAttributesFrom(original); + + if (original instanceof AbstractBeanDefinition) { + AbstractBeanDefinition originalAbd = (AbstractBeanDefinition) original; + if (originalAbd.hasBeanClass()) { + setBeanClass(originalAbd.getBeanClass()); + } + if (originalAbd.hasConstructorArgumentValues()) { + setConstructorArgumentValues(new ConstructorArgumentValues(original.getConstructorArgumentValues())); + } + if (originalAbd.hasPropertyValues()) { + setPropertyValues(new MutablePropertyValues(original.getPropertyValues())); + } + if (originalAbd.hasMethodOverrides()) { + setMethodOverrides(new MethodOverrides(originalAbd.getMethodOverrides())); + } + Boolean lazyInit = originalAbd.getLazyInit(); + if (lazyInit != null) { + setLazyInit(lazyInit); + } + setAutowireMode(originalAbd.getAutowireMode()); + setDependencyCheck(originalAbd.getDependencyCheck()); + setDependsOn(originalAbd.getDependsOn()); + setAutowireCandidate(originalAbd.isAutowireCandidate()); + setPrimary(originalAbd.isPrimary()); + copyQualifiersFrom(originalAbd); + setInstanceSupplier(originalAbd.getInstanceSupplier()); + setNonPublicAccessAllowed(originalAbd.isNonPublicAccessAllowed()); + setLenientConstructorResolution(originalAbd.isLenientConstructorResolution()); + setInitMethodName(originalAbd.getInitMethodName()); + setEnforceInitMethod(originalAbd.isEnforceInitMethod()); + setDestroyMethodName(originalAbd.getDestroyMethodName()); + setEnforceDestroyMethod(originalAbd.isEnforceDestroyMethod()); + setSynthetic(originalAbd.isSynthetic()); + setResource(originalAbd.getResource()); + } + else { + setConstructorArgumentValues(new ConstructorArgumentValues(original.getConstructorArgumentValues())); + setPropertyValues(new MutablePropertyValues(original.getPropertyValues())); + setLazyInit(original.isLazyInit()); + setResourceDescription(original.getResourceDescription()); + } + } + + + /** + * Override settings in this bean definition (presumably a copied parent + * from a parent-child inheritance relationship) from the given bean + * definition (presumably the child). + *

    + *
  • Will override beanClass if specified in the given bean definition. + *
  • Will always take {@code abstract}, {@code scope}, + * {@code lazyInit}, {@code autowireMode}, {@code dependencyCheck}, + * and {@code dependsOn} from the given bean definition. + *
  • Will add {@code constructorArgumentValues}, {@code propertyValues}, + * {@code methodOverrides} from the given bean definition to existing ones. + *
  • Will override {@code factoryBeanName}, {@code factoryMethodName}, + * {@code initMethodName}, and {@code destroyMethodName} if specified + * in the given bean definition. + *
+ */ + public void overrideFrom(BeanDefinition other) { + if (StringUtils.hasLength(other.getBeanClassName())) { + setBeanClassName(other.getBeanClassName()); + } + if (StringUtils.hasLength(other.getScope())) { + setScope(other.getScope()); + } + setAbstract(other.isAbstract()); + if (StringUtils.hasLength(other.getFactoryBeanName())) { + setFactoryBeanName(other.getFactoryBeanName()); + } + if (StringUtils.hasLength(other.getFactoryMethodName())) { + setFactoryMethodName(other.getFactoryMethodName()); + } + setRole(other.getRole()); + setSource(other.getSource()); + copyAttributesFrom(other); + + if (other instanceof AbstractBeanDefinition) { + AbstractBeanDefinition otherAbd = (AbstractBeanDefinition) other; + if (otherAbd.hasBeanClass()) { + setBeanClass(otherAbd.getBeanClass()); + } + if (otherAbd.hasConstructorArgumentValues()) { + getConstructorArgumentValues().addArgumentValues(other.getConstructorArgumentValues()); + } + if (otherAbd.hasPropertyValues()) { + getPropertyValues().addPropertyValues(other.getPropertyValues()); + } + if (otherAbd.hasMethodOverrides()) { + getMethodOverrides().addOverrides(otherAbd.getMethodOverrides()); + } + Boolean lazyInit = otherAbd.getLazyInit(); + if (lazyInit != null) { + setLazyInit(lazyInit); + } + setAutowireMode(otherAbd.getAutowireMode()); + setDependencyCheck(otherAbd.getDependencyCheck()); + setDependsOn(otherAbd.getDependsOn()); + setAutowireCandidate(otherAbd.isAutowireCandidate()); + setPrimary(otherAbd.isPrimary()); + copyQualifiersFrom(otherAbd); + setInstanceSupplier(otherAbd.getInstanceSupplier()); + setNonPublicAccessAllowed(otherAbd.isNonPublicAccessAllowed()); + setLenientConstructorResolution(otherAbd.isLenientConstructorResolution()); + if (otherAbd.getInitMethodName() != null) { + setInitMethodName(otherAbd.getInitMethodName()); + setEnforceInitMethod(otherAbd.isEnforceInitMethod()); + } + if (otherAbd.getDestroyMethodName() != null) { + setDestroyMethodName(otherAbd.getDestroyMethodName()); + setEnforceDestroyMethod(otherAbd.isEnforceDestroyMethod()); + } + setSynthetic(otherAbd.isSynthetic()); + setResource(otherAbd.getResource()); + } + else { + getConstructorArgumentValues().addArgumentValues(other.getConstructorArgumentValues()); + getPropertyValues().addPropertyValues(other.getPropertyValues()); + setLazyInit(other.isLazyInit()); + setResourceDescription(other.getResourceDescription()); + } + } + + /** + * Apply the provided default values to this bean. + * @param defaults the default settings to apply + * @since 2.5 + */ + public void applyDefaults(BeanDefinitionDefaults defaults) { + Boolean lazyInit = defaults.getLazyInit(); + if (lazyInit != null) { + setLazyInit(lazyInit); + } + setAutowireMode(defaults.getAutowireMode()); + setDependencyCheck(defaults.getDependencyCheck()); + setInitMethodName(defaults.getInitMethodName()); + setEnforceInitMethod(false); + setDestroyMethodName(defaults.getDestroyMethodName()); + setEnforceDestroyMethod(false); + } + + + /** + * Specify the bean class name of this bean definition. + */ + @Override + public void setBeanClassName(@Nullable String beanClassName) { + this.beanClass = beanClassName; + } + + /** + * Return the current bean class name of this bean definition. + */ + @Override + @Nullable + public String getBeanClassName() { + Object beanClassObject = this.beanClass; + if (beanClassObject instanceof Class) { + return ((Class) beanClassObject).getName(); + } + else { + return (String) beanClassObject; + } + } + + /** + * Specify the class for this bean. + * @see #setBeanClassName(String) + */ + public void setBeanClass(@Nullable Class beanClass) { + this.beanClass = beanClass; + } + + /** + * Return the specified class of the bean definition (assuming it is resolved already). + *

NOTE: This is an initial class reference as declared in the bean metadata + * definition, potentially combined with a declared factory method or a + * {@link org.springframework.beans.factory.FactoryBean} which may lead to a different + * runtime type of the bean, or not being set at all in case of an instance-level + * factory method (which is resolved via {@link #getFactoryBeanName()} instead). + * Do not use this for runtime type introspection of arbitrary bean definitions. + * The recommended way to find out about the actual runtime type of a particular bean + * is a {@link org.springframework.beans.factory.BeanFactory#getType} call for the + * specified bean name; this takes all of the above cases into account and returns the + * type of object that a {@link org.springframework.beans.factory.BeanFactory#getBean} + * call is going to return for the same bean name. + * @return the resolved bean class (never {@code null}) + * @throws IllegalStateException if the bean definition does not define a bean class, + * or a specified bean class name has not been resolved into an actual Class yet + * @see #getBeanClassName() + * @see #hasBeanClass() + * @see #setBeanClass(Class) + * @see #resolveBeanClass(ClassLoader) + */ + public Class getBeanClass() throws IllegalStateException { + Object beanClassObject = this.beanClass; + if (beanClassObject == null) { + throw new IllegalStateException("No bean class specified on bean definition"); + } + if (!(beanClassObject instanceof Class)) { + throw new IllegalStateException( + "Bean class name [" + beanClassObject + "] has not been resolved into an actual Class"); + } + return (Class) beanClassObject; + } + + /** + * Return whether this definition specifies a bean class. + * @see #getBeanClass() + * @see #setBeanClass(Class) + * @see #resolveBeanClass(ClassLoader) + */ + public boolean hasBeanClass() { + return (this.beanClass instanceof Class); + } + + /** + * Determine the class of the wrapped bean, resolving it from a + * specified class name if necessary. Will also reload a specified + * Class from its name when called with the bean class already resolved. + * @param classLoader the ClassLoader to use for resolving a (potential) class name + * @return the resolved bean class + * @throws ClassNotFoundException if the class name could be resolved + */ + @Nullable + public Class resolveBeanClass(@Nullable ClassLoader classLoader) throws ClassNotFoundException { + String className = getBeanClassName(); + if (className == null) { + return null; + } + Class resolvedClass = ClassUtils.forName(className, classLoader); + this.beanClass = resolvedClass; + return resolvedClass; + } + + /** + * Return a resolvable type for this bean definition. + *

This implementation delegates to {@link #getBeanClass()}. + * @since 5.2 + */ + @Override + public ResolvableType getResolvableType() { + return (hasBeanClass() ? ResolvableType.forClass(getBeanClass()) : ResolvableType.NONE); + } + + /** + * Set the name of the target scope for the bean. + *

The default is singleton status, although this is only applied once + * a bean definition becomes active in the containing factory. A bean + * definition may eventually inherit its scope from a parent bean definition. + * For this reason, the default scope name is an empty string (i.e., {@code ""}), + * with singleton status being assumed until a resolved scope is set. + * @see #SCOPE_SINGLETON + * @see #SCOPE_PROTOTYPE + */ + @Override + public void setScope(@Nullable String scope) { + this.scope = scope; + } + + /** + * Return the name of the target scope for the bean. + */ + @Override + @Nullable + public String getScope() { + return this.scope; + } + + /** + * Return whether this a Singleton, with a single shared instance + * returned from all calls. + * @see #SCOPE_SINGLETON + */ + @Override + public boolean isSingleton() { + return SCOPE_SINGLETON.equals(this.scope) || SCOPE_DEFAULT.equals(this.scope); + } + + /** + * Return whether this a Prototype, with an independent instance + * returned for each call. + * @see #SCOPE_PROTOTYPE + */ + @Override + public boolean isPrototype() { + return SCOPE_PROTOTYPE.equals(this.scope); + } + + /** + * Set if this bean is "abstract", i.e. not meant to be instantiated itself but + * rather just serving as parent for concrete child bean definitions. + *

Default is "false". Specify true to tell the bean factory to not try to + * instantiate that particular bean in any case. + */ + public void setAbstract(boolean abstractFlag) { + this.abstractFlag = abstractFlag; + } + + /** + * Return whether this bean is "abstract", i.e. not meant to be instantiated + * itself but rather just serving as parent for concrete child bean definitions. + */ + @Override + public boolean isAbstract() { + return this.abstractFlag; + } + + /** + * Set whether this bean should be lazily initialized. + *

If {@code false}, the bean will get instantiated on startup by bean + * factories that perform eager initialization of singletons. + */ + @Override + public void setLazyInit(boolean lazyInit) { + this.lazyInit = lazyInit; + } + + /** + * Return whether this bean should be lazily initialized, i.e. not + * eagerly instantiated on startup. Only applicable to a singleton bean. + * @return whether to apply lazy-init semantics ({@code false} by default) + */ + @Override + public boolean isLazyInit() { + return (this.lazyInit != null && this.lazyInit.booleanValue()); + } + + /** + * Return whether this bean should be lazily initialized, i.e. not + * eagerly instantiated on startup. Only applicable to a singleton bean. + * @return the lazy-init flag if explicitly set, or {@code null} otherwise + * @since 5.2 + */ + @Nullable + public Boolean getLazyInit() { + return this.lazyInit; + } + + /** + * Set the autowire mode. This determines whether any automagical detection + * and setting of bean references will happen. Default is AUTOWIRE_NO + * which means there won't be convention-based autowiring by name or type + * (however, there may still be explicit annotation-driven autowiring). + * @param autowireMode the autowire mode to set. + * Must be one of the constants defined in this class. + * @see #AUTOWIRE_NO + * @see #AUTOWIRE_BY_NAME + * @see #AUTOWIRE_BY_TYPE + * @see #AUTOWIRE_CONSTRUCTOR + * @see #AUTOWIRE_AUTODETECT + */ + public void setAutowireMode(int autowireMode) { + this.autowireMode = autowireMode; + } + + /** + * Return the autowire mode as specified in the bean definition. + */ + public int getAutowireMode() { + return this.autowireMode; + } + + /** + * Return the resolved autowire code, + * (resolving AUTOWIRE_AUTODETECT to AUTOWIRE_CONSTRUCTOR or AUTOWIRE_BY_TYPE). + * @see #AUTOWIRE_AUTODETECT + * @see #AUTOWIRE_CONSTRUCTOR + * @see #AUTOWIRE_BY_TYPE + */ + public int getResolvedAutowireMode() { + if (this.autowireMode == AUTOWIRE_AUTODETECT) { + // Work out whether to apply setter autowiring or constructor autowiring. + // If it has a no-arg constructor it's deemed to be setter autowiring, + // otherwise we'll try constructor autowiring. + Constructor[] constructors = getBeanClass().getConstructors(); + for (Constructor constructor : constructors) { + if (constructor.getParameterCount() == 0) { + return AUTOWIRE_BY_TYPE; + } + } + return AUTOWIRE_CONSTRUCTOR; + } + else { + return this.autowireMode; + } + } + + /** + * Set the dependency check code. + * @param dependencyCheck the code to set. + * Must be one of the four constants defined in this class. + * @see #DEPENDENCY_CHECK_NONE + * @see #DEPENDENCY_CHECK_OBJECTS + * @see #DEPENDENCY_CHECK_SIMPLE + * @see #DEPENDENCY_CHECK_ALL + */ + public void setDependencyCheck(int dependencyCheck) { + this.dependencyCheck = dependencyCheck; + } + + /** + * Return the dependency check code. + */ + public int getDependencyCheck() { + return this.dependencyCheck; + } + + /** + * Set the names of the beans that this bean depends on being initialized. + * The bean factory will guarantee that these beans get initialized first. + *

Note that dependencies are normally expressed through bean properties or + * constructor arguments. This property should just be necessary for other kinds + * of dependencies like statics (*ugh*) or database preparation on startup. + */ + @Override + public void setDependsOn(@Nullable String... dependsOn) { + this.dependsOn = dependsOn; + } + + /** + * Return the bean names that this bean depends on. + */ + @Override + @Nullable + public String[] getDependsOn() { + return this.dependsOn; + } + + /** + * Set whether this bean is a candidate for getting autowired into some other bean. + *

Note that this flag is designed to only affect type-based autowiring. + * It does not affect explicit references by name, which will get resolved even + * if the specified bean is not marked as an autowire candidate. As a consequence, + * autowiring by name will nevertheless inject a bean if the name matches. + * @see #AUTOWIRE_BY_TYPE + * @see #AUTOWIRE_BY_NAME + */ + @Override + public void setAutowireCandidate(boolean autowireCandidate) { + this.autowireCandidate = autowireCandidate; + } + + /** + * Return whether this bean is a candidate for getting autowired into some other bean. + */ + @Override + public boolean isAutowireCandidate() { + return this.autowireCandidate; + } + + /** + * Set whether this bean is a primary autowire candidate. + *

If this value is {@code true} for exactly one bean among multiple + * matching candidates, it will serve as a tie-breaker. + */ + @Override + public void setPrimary(boolean primary) { + this.primary = primary; + } + + /** + * Return whether this bean is a primary autowire candidate. + */ + @Override + public boolean isPrimary() { + return this.primary; + } + + /** + * Register a qualifier to be used for autowire candidate resolution, + * keyed by the qualifier's type name. + * @see AutowireCandidateQualifier#getTypeName() + */ + public void addQualifier(AutowireCandidateQualifier qualifier) { + this.qualifiers.put(qualifier.getTypeName(), qualifier); + } + + /** + * Return whether this bean has the specified qualifier. + */ + public boolean hasQualifier(String typeName) { + return this.qualifiers.containsKey(typeName); + } + + /** + * Return the qualifier mapped to the provided type name. + */ + @Nullable + public AutowireCandidateQualifier getQualifier(String typeName) { + return this.qualifiers.get(typeName); + } + + /** + * Return all registered qualifiers. + * @return the Set of {@link AutowireCandidateQualifier} objects. + */ + public Set getQualifiers() { + return new LinkedHashSet<>(this.qualifiers.values()); + } + + /** + * Copy the qualifiers from the supplied AbstractBeanDefinition to this bean definition. + * @param source the AbstractBeanDefinition to copy from + */ + public void copyQualifiersFrom(AbstractBeanDefinition source) { + Assert.notNull(source, "Source must not be null"); + this.qualifiers.putAll(source.qualifiers); + } + + /** + * Specify a callback for creating an instance of the bean, + * as an alternative to a declaratively specified factory method. + *

If such a callback is set, it will override any other constructor + * or factory method metadata. However, bean property population and + * potential annotation-driven injection will still apply as usual. + * @since 5.0 + * @see #setConstructorArgumentValues(ConstructorArgumentValues) + * @see #setPropertyValues(MutablePropertyValues) + */ + public void setInstanceSupplier(@Nullable Supplier instanceSupplier) { + this.instanceSupplier = instanceSupplier; + } + + /** + * Return a callback for creating an instance of the bean, if any. + * @since 5.0 + */ + @Nullable + public Supplier getInstanceSupplier() { + return this.instanceSupplier; + } + + /** + * Specify whether to allow access to non-public constructors and methods, + * for the case of externalized metadata pointing to those. The default is + * {@code true}; switch this to {@code false} for public access only. + *

This applies to constructor resolution, factory method resolution, + * and also init/destroy methods. Bean property accessors have to be public + * in any case and are not affected by this setting. + *

Note that annotation-driven configuration will still access non-public + * members as far as they have been annotated. This setting applies to + * externalized metadata in this bean definition only. + */ + public void setNonPublicAccessAllowed(boolean nonPublicAccessAllowed) { + this.nonPublicAccessAllowed = nonPublicAccessAllowed; + } + + /** + * Return whether to allow access to non-public constructors and methods. + */ + public boolean isNonPublicAccessAllowed() { + return this.nonPublicAccessAllowed; + } + + /** + * Specify whether to resolve constructors in lenient mode ({@code true}, + * which is the default) or to switch to strict resolution (throwing an exception + * in case of ambiguous constructors that all match when converting the arguments, + * whereas lenient mode would use the one with the 'closest' type matches). + */ + public void setLenientConstructorResolution(boolean lenientConstructorResolution) { + this.lenientConstructorResolution = lenientConstructorResolution; + } + + /** + * Return whether to resolve constructors in lenient mode or in strict mode. + */ + public boolean isLenientConstructorResolution() { + return this.lenientConstructorResolution; + } + + /** + * Specify the factory bean to use, if any. + * This the name of the bean to call the specified factory method on. + * @see #setFactoryMethodName + */ + @Override + public void setFactoryBeanName(@Nullable String factoryBeanName) { + this.factoryBeanName = factoryBeanName; + } + + /** + * Return the factory bean name, if any. + */ + @Override + @Nullable + public String getFactoryBeanName() { + return this.factoryBeanName; + } + + /** + * Specify a factory method, if any. This method will be invoked with + * constructor arguments, or with no arguments if none are specified. + * The method will be invoked on the specified factory bean, if any, + * or otherwise as a static method on the local bean class. + * @see #setFactoryBeanName + * @see #setBeanClassName + */ + @Override + public void setFactoryMethodName(@Nullable String factoryMethodName) { + this.factoryMethodName = factoryMethodName; + } + + /** + * Return a factory method, if any. + */ + @Override + @Nullable + public String getFactoryMethodName() { + return this.factoryMethodName; + } + + /** + * Specify constructor argument values for this bean. + */ + public void setConstructorArgumentValues(ConstructorArgumentValues constructorArgumentValues) { + this.constructorArgumentValues = constructorArgumentValues; + } + + /** + * Return constructor argument values for this bean (never {@code null}). + */ + @Override + public ConstructorArgumentValues getConstructorArgumentValues() { + if (this.constructorArgumentValues == null) { + this.constructorArgumentValues = new ConstructorArgumentValues(); + } + return this.constructorArgumentValues; + } + + /** + * Return if there are constructor argument values defined for this bean. + */ + @Override + public boolean hasConstructorArgumentValues() { + return (this.constructorArgumentValues != null && !this.constructorArgumentValues.isEmpty()); + } + + /** + * Specify property values for this bean, if any. + */ + public void setPropertyValues(MutablePropertyValues propertyValues) { + this.propertyValues = propertyValues; + } + + /** + * Return property values for this bean (never {@code null}). + */ + @Override + public MutablePropertyValues getPropertyValues() { + if (this.propertyValues == null) { + this.propertyValues = new MutablePropertyValues(); + } + return this.propertyValues; + } + + /** + * Return if there are property values values defined for this bean. + * @since 5.0.2 + */ + @Override + public boolean hasPropertyValues() { + return (this.propertyValues != null && !this.propertyValues.isEmpty()); + } + + /** + * Specify method overrides for the bean, if any. + */ + public void setMethodOverrides(MethodOverrides methodOverrides) { + this.methodOverrides = methodOverrides; + } + + /** + * Return information about methods to be overridden by the IoC + * container. This will be empty if there are no method overrides. + *

Never returns {@code null}. + */ + public MethodOverrides getMethodOverrides() { + return this.methodOverrides; + } + + /** + * Return if there are method overrides defined for this bean. + * @since 5.0.2 + */ + public boolean hasMethodOverrides() { + return !this.methodOverrides.isEmpty(); + } + + /** + * Set the name of the initializer method. + *

The default is {@code null} in which case there is no initializer method. + */ + @Override + public void setInitMethodName(@Nullable String initMethodName) { + this.initMethodName = initMethodName; + } + + /** + * Return the name of the initializer method. + */ + @Override + @Nullable + public String getInitMethodName() { + return this.initMethodName; + } + + /** + * Specify whether or not the configured initializer method is the default. + *

The default value is {@code true} for a locally specified init method + * but switched to {@code false} for a shared setting in a defaults section + * (e.g. {@code bean init-method} versus {@code beans default-init-method} + * level in XML) which might not apply to all contained bean definitions. + * @see #setInitMethodName + * @see #applyDefaults + */ + public void setEnforceInitMethod(boolean enforceInitMethod) { + this.enforceInitMethod = enforceInitMethod; + } + + /** + * Indicate whether the configured initializer method is the default. + * @see #getInitMethodName() + */ + public boolean isEnforceInitMethod() { + return this.enforceInitMethod; + } + + /** + * Set the name of the destroy method. + *

The default is {@code null} in which case there is no destroy method. + */ + @Override + public void setDestroyMethodName(@Nullable String destroyMethodName) { + this.destroyMethodName = destroyMethodName; + } + + /** + * Return the name of the destroy method. + */ + @Override + @Nullable + public String getDestroyMethodName() { + return this.destroyMethodName; + } + + /** + * Specify whether or not the configured destroy method is the default. + *

The default value is {@code true} for a locally specified destroy method + * but switched to {@code false} for a shared setting in a defaults section + * (e.g. {@code bean destroy-method} versus {@code beans default-destroy-method} + * level in XML) which might not apply to all contained bean definitions. + * @see #setDestroyMethodName + * @see #applyDefaults + */ + public void setEnforceDestroyMethod(boolean enforceDestroyMethod) { + this.enforceDestroyMethod = enforceDestroyMethod; + } + + /** + * Indicate whether the configured destroy method is the default. + * @see #getDestroyMethodName() + */ + public boolean isEnforceDestroyMethod() { + return this.enforceDestroyMethod; + } + + /** + * Set whether this bean definition is 'synthetic', that is, not defined + * by the application itself (for example, an infrastructure bean such + * as a helper for auto-proxying, created through {@code }). + */ + public void setSynthetic(boolean synthetic) { + this.synthetic = synthetic; + } + + /** + * Return whether this bean definition is 'synthetic', that is, + * not defined by the application itself. + */ + public boolean isSynthetic() { + return this.synthetic; + } + + /** + * Set the role hint for this {@code BeanDefinition}. + */ + @Override + public void setRole(int role) { + this.role = role; + } + + /** + * Return the role hint for this {@code BeanDefinition}. + */ + @Override + public int getRole() { + return this.role; + } + + /** + * Set a human-readable description of this bean definition. + */ + @Override + public void setDescription(@Nullable String description) { + this.description = description; + } + + /** + * Return a human-readable description of this bean definition. + */ + @Override + @Nullable + public String getDescription() { + return this.description; + } + + /** + * Set the resource that this bean definition came from + * (for the purpose of showing context in case of errors). + */ + public void setResource(@Nullable Resource resource) { + this.resource = resource; + } + + /** + * Return the resource that this bean definition came from. + */ + @Nullable + public Resource getResource() { + return this.resource; + } + + /** + * Set a description of the resource that this bean definition + * came from (for the purpose of showing context in case of errors). + */ + public void setResourceDescription(@Nullable String resourceDescription) { + this.resource = (resourceDescription != null ? new DescriptiveResource(resourceDescription) : null); + } + + /** + * Return a description of the resource that this bean definition + * came from (for the purpose of showing context in case of errors). + */ + @Override + @Nullable + public String getResourceDescription() { + return (this.resource != null ? this.resource.getDescription() : null); + } + + /** + * Set the originating (e.g. decorated) BeanDefinition, if any. + */ + public void setOriginatingBeanDefinition(BeanDefinition originatingBd) { + this.resource = new BeanDefinitionResource(originatingBd); + } + + /** + * Return the originating BeanDefinition, or {@code null} if none. + * Allows for retrieving the decorated bean definition, if any. + *

Note that this method returns the immediate originator. Iterate through the + * originator chain to find the original BeanDefinition as defined by the user. + */ + @Override + @Nullable + public BeanDefinition getOriginatingBeanDefinition() { + return (this.resource instanceof BeanDefinitionResource ? + ((BeanDefinitionResource) this.resource).getBeanDefinition() : null); + } + + /** + * Validate this bean definition. + * @throws BeanDefinitionValidationException in case of validation failure + */ + public void validate() throws BeanDefinitionValidationException { + if (hasMethodOverrides() && getFactoryMethodName() != null) { + throw new BeanDefinitionValidationException( + "Cannot combine factory method with container-generated method overrides: " + + "the factory method must create the concrete bean instance."); + } + if (hasBeanClass()) { + prepareMethodOverrides(); + } + } + + /** + * Validate and prepare the method overrides defined for this bean. + * Checks for existence of a method with the specified name. + * @throws BeanDefinitionValidationException in case of validation failure + */ + public void prepareMethodOverrides() throws BeanDefinitionValidationException { + // Check that lookup methods exist and determine their overloaded status. + if (hasMethodOverrides()) { + getMethodOverrides().getOverrides().forEach(this::prepareMethodOverride); + } + } + + /** + * Validate and prepare the given method override. + * Checks for existence of a method with the specified name, + * marking it as not overloaded if none found. + * @param mo the MethodOverride object to validate + * @throws BeanDefinitionValidationException in case of validation failure + */ + protected void prepareMethodOverride(MethodOverride mo) throws BeanDefinitionValidationException { + int count = ClassUtils.getMethodCountForName(getBeanClass(), mo.getMethodName()); + if (count == 0) { + throw new BeanDefinitionValidationException( + "Invalid method override: no method with name '" + mo.getMethodName() + + "' on class [" + getBeanClassName() + "]"); + } + else if (count == 1) { + // Mark override as not overloaded, to avoid the overhead of arg type checking. + mo.setOverloaded(false); + } + } + + + /** + * Public declaration of Object's {@code clone()} method. + * Delegates to {@link #cloneBeanDefinition()}. + * @see Object#clone() + */ + @Override + public Object clone() { + return cloneBeanDefinition(); + } + + /** + * Clone this bean definition. + * To be implemented by concrete subclasses. + * @return the cloned bean definition object + */ + public abstract AbstractBeanDefinition cloneBeanDefinition(); + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof AbstractBeanDefinition)) { + return false; + } + AbstractBeanDefinition that = (AbstractBeanDefinition) other; + return (ObjectUtils.nullSafeEquals(getBeanClassName(), that.getBeanClassName()) && + ObjectUtils.nullSafeEquals(this.scope, that.scope) && + this.abstractFlag == that.abstractFlag && + this.lazyInit == that.lazyInit && + this.autowireMode == that.autowireMode && + this.dependencyCheck == that.dependencyCheck && + Arrays.equals(this.dependsOn, that.dependsOn) && + this.autowireCandidate == that.autowireCandidate && + ObjectUtils.nullSafeEquals(this.qualifiers, that.qualifiers) && + this.primary == that.primary && + this.nonPublicAccessAllowed == that.nonPublicAccessAllowed && + this.lenientConstructorResolution == that.lenientConstructorResolution && + ObjectUtils.nullSafeEquals(this.constructorArgumentValues, that.constructorArgumentValues) && + ObjectUtils.nullSafeEquals(this.propertyValues, that.propertyValues) && + ObjectUtils.nullSafeEquals(this.methodOverrides, that.methodOverrides) && + ObjectUtils.nullSafeEquals(this.factoryBeanName, that.factoryBeanName) && + ObjectUtils.nullSafeEquals(this.factoryMethodName, that.factoryMethodName) && + ObjectUtils.nullSafeEquals(this.initMethodName, that.initMethodName) && + this.enforceInitMethod == that.enforceInitMethod && + ObjectUtils.nullSafeEquals(this.destroyMethodName, that.destroyMethodName) && + this.enforceDestroyMethod == that.enforceDestroyMethod && + this.synthetic == that.synthetic && + this.role == that.role && + super.equals(other)); + } + + @Override + public int hashCode() { + int hashCode = ObjectUtils.nullSafeHashCode(getBeanClassName()); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.scope); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.constructorArgumentValues); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.propertyValues); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.factoryBeanName); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.factoryMethodName); + hashCode = 29 * hashCode + super.hashCode(); + return hashCode; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("class ["); + sb.append(getBeanClassName()).append("]"); + sb.append("; scope=").append(this.scope); + sb.append("; abstract=").append(this.abstractFlag); + sb.append("; lazyInit=").append(this.lazyInit); + sb.append("; autowireMode=").append(this.autowireMode); + sb.append("; dependencyCheck=").append(this.dependencyCheck); + sb.append("; autowireCandidate=").append(this.autowireCandidate); + sb.append("; primary=").append(this.primary); + sb.append("; factoryBeanName=").append(this.factoryBeanName); + sb.append("; factoryMethodName=").append(this.factoryMethodName); + sb.append("; initMethodName=").append(this.initMethodName); + sb.append("; destroyMethodName=").append(this.destroyMethodName); + if (this.resource != null) { + sb.append("; defined in ").append(this.resource.getDescription()); + } + return sb.toString(); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionOverrideException.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionOverrideException.java new file mode 100644 index 0000000..c2c6281 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionOverrideException.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support; + +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.lang.NonNull; + +/** + * Subclass of {@link BeanDefinitionStoreException} indicating an invalid override + * attempt: typically registering a new definition for the same bean name while + * {@link DefaultListableBeanFactory#isAllowBeanDefinitionOverriding()} is {@code false}. + * + * @author Juergen Hoeller + * @since 5.1 + * @see DefaultListableBeanFactory#setAllowBeanDefinitionOverriding + * @see DefaultListableBeanFactory#registerBeanDefinition + */ +@SuppressWarnings("serial") +public class BeanDefinitionOverrideException extends BeanDefinitionStoreException { + + private final BeanDefinition beanDefinition; + + private final BeanDefinition existingDefinition; + + + /** + * Create a new BeanDefinitionOverrideException for the given new and existing definition. + * @param beanName the name of the bean + * @param beanDefinition the newly registered bean definition + * @param existingDefinition the existing bean definition for the same name + */ + public BeanDefinitionOverrideException( + String beanName, BeanDefinition beanDefinition, BeanDefinition existingDefinition) { + + super(beanDefinition.getResourceDescription(), beanName, + "Cannot register bean definition [" + beanDefinition + "] for bean '" + beanName + + "': There is already [" + existingDefinition + "] bound."); + this.beanDefinition = beanDefinition; + this.existingDefinition = existingDefinition; + } + + + /** + * Return the description of the resource that the bean definition came from. + */ + @Override + @NonNull + public String getResourceDescription() { + return String.valueOf(super.getResourceDescription()); + } + + /** + * Return the name of the bean. + */ + @Override + @NonNull + public String getBeanName() { + return String.valueOf(super.getBeanName()); + } + + /** + * Return the newly registered bean definition. + * @see #getBeanName() + */ + public BeanDefinition getBeanDefinition() { + return this.beanDefinition; + } + + /** + * Return the existing bean definition for the same name. + * @see #getBeanName() + */ + public BeanDefinition getExistingDefinition() { + return this.existingDefinition; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReader.java new file mode 100644 index 0000000..a6fedff --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReader.java @@ -0,0 +1,124 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support; + +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.lang.Nullable; + +/** + * Simple interface for bean definition readers. + * Specifies load methods with Resource and String location parameters. + * + *

Concrete bean definition readers can of course add additional + * load and register methods for bean definitions, specific to + * their bean definition format. + * + *

Note that a bean definition reader does not have to implement + * this interface. It only serves as suggestion for bean definition + * readers that want to follow standard naming conventions. + * + * @author Juergen Hoeller + * @since 1.1 + * @see org.springframework.core.io.Resource + */ +public interface BeanDefinitionReader { + + /** + * Return the bean factory to register the bean definitions with. + *

The factory is exposed through the BeanDefinitionRegistry interface, + * encapsulating the methods that are relevant for bean definition handling. + */ + BeanDefinitionRegistry getRegistry(); + + /** + * Return the resource loader to use for resource locations. + * Can be checked for the ResourcePatternResolver interface and cast + * accordingly, for loading multiple resources for a given resource pattern. + *

A {@code null} return value suggests that absolute resource loading + * is not available for this bean definition reader. + *

This is mainly meant to be used for importing further resources + * from within a bean definition resource, for example via the "import" + * tag in XML bean definitions. It is recommended, however, to apply + * such imports relative to the defining resource; only explicit full + * resource locations will trigger absolute resource loading. + *

There is also a {@code loadBeanDefinitions(String)} method available, + * for loading bean definitions from a resource location (or location pattern). + * This is a convenience to avoid explicit ResourceLoader handling. + * @see #loadBeanDefinitions(String) + * @see org.springframework.core.io.support.ResourcePatternResolver + */ + @Nullable + ResourceLoader getResourceLoader(); + + /** + * Return the class loader to use for bean classes. + *

{@code null} suggests to not load bean classes eagerly + * but rather to just register bean definitions with class names, + * with the corresponding Classes to be resolved later (or never). + */ + @Nullable + ClassLoader getBeanClassLoader(); + + /** + * Return the BeanNameGenerator to use for anonymous beans + * (without explicit bean name specified). + */ + BeanNameGenerator getBeanNameGenerator(); + + + /** + * Load bean definitions from the specified resource. + * @param resource the resource descriptor + * @return the number of bean definitions found + * @throws BeanDefinitionStoreException in case of loading or parsing errors + */ + int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException; + + /** + * Load bean definitions from the specified resources. + * @param resources the resource descriptors + * @return the number of bean definitions found + * @throws BeanDefinitionStoreException in case of loading or parsing errors + */ + int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException; + + /** + * Load bean definitions from the specified resource location. + *

The location can also be a location pattern, provided that the + * ResourceLoader of this bean definition reader is a ResourcePatternResolver. + * @param location the resource location, to be loaded with the ResourceLoader + * (or ResourcePatternResolver) of this bean definition reader + * @return the number of bean definitions found + * @throws BeanDefinitionStoreException in case of loading or parsing errors + * @see #getResourceLoader() + * @see #loadBeanDefinitions(org.springframework.core.io.Resource) + * @see #loadBeanDefinitions(org.springframework.core.io.Resource[]) + */ + int loadBeanDefinitions(String location) throws BeanDefinitionStoreException; + + /** + * Load bean definitions from the specified resource locations. + * @param locations the resource locations, to be loaded with the ResourceLoader + * (or ResourcePatternResolver) of this bean definition reader + * @return the number of bean definitions found + * @throws BeanDefinitionStoreException in case of loading or parsing errors + */ + int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException; + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.java new file mode 100644 index 0000000..be9667b --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.java @@ -0,0 +1,481 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.BeansException; +import org.springframework.beans.TypeConverter; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.config.NamedBeanHolder; +import org.springframework.beans.factory.config.RuntimeBeanNameReference; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.config.TypedStringValue; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Helper class for use in bean factory implementations, + * resolving values contained in bean definition objects + * into the actual values applied to the target bean instance. + * + *

Operates on an {@link AbstractBeanFactory} and a plain + * {@link org.springframework.beans.factory.config.BeanDefinition} object. + * Used by {@link AbstractAutowireCapableBeanFactory}. + * + * @author Juergen Hoeller + * @since 1.2 + * @see AbstractAutowireCapableBeanFactory + */ +class BeanDefinitionValueResolver { + + private final AbstractAutowireCapableBeanFactory beanFactory; + + private final String beanName; + + private final BeanDefinition beanDefinition; + + private final TypeConverter typeConverter; + + + /** + * Create a BeanDefinitionValueResolver for the given BeanFactory and BeanDefinition. + * @param beanFactory the BeanFactory to resolve against + * @param beanName the name of the bean that we work on + * @param beanDefinition the BeanDefinition of the bean that we work on + * @param typeConverter the TypeConverter to use for resolving TypedStringValues + */ + public BeanDefinitionValueResolver(AbstractAutowireCapableBeanFactory beanFactory, String beanName, + BeanDefinition beanDefinition, TypeConverter typeConverter) { + + this.beanFactory = beanFactory; + this.beanName = beanName; + this.beanDefinition = beanDefinition; + this.typeConverter = typeConverter; + } + + + /** + * Given a PropertyValue, return a value, resolving any references to other + * beans in the factory if necessary. The value could be: + *

  • A BeanDefinition, which leads to the creation of a corresponding + * new bean instance. Singleton flags and names of such "inner beans" + * are always ignored: Inner beans are anonymous prototypes. + *
  • A RuntimeBeanReference, which must be resolved. + *
  • A ManagedList. This is a special collection that may contain + * RuntimeBeanReferences or Collections that will need to be resolved. + *
  • A ManagedSet. May also contain RuntimeBeanReferences or + * Collections that will need to be resolved. + *
  • A ManagedMap. In this case the value may be a RuntimeBeanReference + * or Collection that will need to be resolved. + *
  • An ordinary object or {@code null}, in which case it's left alone. + * @param argName the name of the argument that the value is defined for + * @param value the value object to resolve + * @return the resolved object + */ + @Nullable + public Object resolveValueIfNecessary(Object argName, @Nullable Object value) { + // We must check each value to see whether it requires a runtime reference + // to another bean to be resolved. + if (value instanceof RuntimeBeanReference) { + RuntimeBeanReference ref = (RuntimeBeanReference) value; + return resolveReference(argName, ref); + } + else if (value instanceof RuntimeBeanNameReference) { + String refName = ((RuntimeBeanNameReference) value).getBeanName(); + refName = String.valueOf(doEvaluate(refName)); + if (!this.beanFactory.containsBean(refName)) { + throw new BeanDefinitionStoreException( + "Invalid bean name '" + refName + "' in bean reference for " + argName); + } + return refName; + } + else if (value instanceof BeanDefinitionHolder) { + // Resolve BeanDefinitionHolder: contains BeanDefinition with name and aliases. + BeanDefinitionHolder bdHolder = (BeanDefinitionHolder) value; + return resolveInnerBean(argName, bdHolder.getBeanName(), bdHolder.getBeanDefinition()); + } + else if (value instanceof BeanDefinition) { + // Resolve plain BeanDefinition, without contained name: use dummy name. + BeanDefinition bd = (BeanDefinition) value; + String innerBeanName = "(inner bean)" + BeanFactoryUtils.GENERATED_BEAN_NAME_SEPARATOR + + ObjectUtils.getIdentityHexString(bd); + return resolveInnerBean(argName, innerBeanName, bd); + } + else if (value instanceof DependencyDescriptor) { + Set autowiredBeanNames = new LinkedHashSet<>(4); + Object result = this.beanFactory.resolveDependency( + (DependencyDescriptor) value, this.beanName, autowiredBeanNames, this.typeConverter); + for (String autowiredBeanName : autowiredBeanNames) { + if (this.beanFactory.containsBean(autowiredBeanName)) { + this.beanFactory.registerDependentBean(autowiredBeanName, this.beanName); + } + } + return result; + } + else if (value instanceof ManagedArray) { + // May need to resolve contained runtime references. + ManagedArray array = (ManagedArray) value; + Class elementType = array.resolvedElementType; + if (elementType == null) { + String elementTypeName = array.getElementTypeName(); + if (StringUtils.hasText(elementTypeName)) { + try { + elementType = ClassUtils.forName(elementTypeName, this.beanFactory.getBeanClassLoader()); + array.resolvedElementType = elementType; + } + catch (Throwable ex) { + // Improve the message by showing the context. + throw new BeanCreationException( + this.beanDefinition.getResourceDescription(), this.beanName, + "Error resolving array type for " + argName, ex); + } + } + else { + elementType = Object.class; + } + } + return resolveManagedArray(argName, (List) value, elementType); + } + else if (value instanceof ManagedList) { + // May need to resolve contained runtime references. + return resolveManagedList(argName, (List) value); + } + else if (value instanceof ManagedSet) { + // May need to resolve contained runtime references. + return resolveManagedSet(argName, (Set) value); + } + else if (value instanceof ManagedMap) { + // May need to resolve contained runtime references. + return resolveManagedMap(argName, (Map) value); + } + else if (value instanceof ManagedProperties) { + Properties original = (Properties) value; + Properties copy = new Properties(); + original.forEach((propKey, propValue) -> { + if (propKey instanceof TypedStringValue) { + propKey = evaluate((TypedStringValue) propKey); + } + if (propValue instanceof TypedStringValue) { + propValue = evaluate((TypedStringValue) propValue); + } + if (propKey == null || propValue == null) { + throw new BeanCreationException( + this.beanDefinition.getResourceDescription(), this.beanName, + "Error converting Properties key/value pair for " + argName + ": resolved to null"); + } + copy.put(propKey, propValue); + }); + return copy; + } + else if (value instanceof TypedStringValue) { + // Convert value to target type here. + TypedStringValue typedStringValue = (TypedStringValue) value; + Object valueObject = evaluate(typedStringValue); + try { + Class resolvedTargetType = resolveTargetType(typedStringValue); + if (resolvedTargetType != null) { + return this.typeConverter.convertIfNecessary(valueObject, resolvedTargetType); + } + else { + return valueObject; + } + } + catch (Throwable ex) { + // Improve the message by showing the context. + throw new BeanCreationException( + this.beanDefinition.getResourceDescription(), this.beanName, + "Error converting typed String value for " + argName, ex); + } + } + else if (value instanceof NullBean) { + return null; + } + else { + return evaluate(value); + } + } + + /** + * Evaluate the given value as an expression, if necessary. + * @param value the candidate value (may be an expression) + * @return the resolved value + */ + @Nullable + protected Object evaluate(TypedStringValue value) { + Object result = doEvaluate(value.getValue()); + if (!ObjectUtils.nullSafeEquals(result, value.getValue())) { + value.setDynamic(); + } + return result; + } + + /** + * Evaluate the given value as an expression, if necessary. + * @param value the original value (may be an expression) + * @return the resolved value if necessary, or the original value + */ + @Nullable + protected Object evaluate(@Nullable Object value) { + if (value instanceof String) { + return doEvaluate((String) value); + } + else if (value instanceof String[]) { + String[] values = (String[]) value; + boolean actuallyResolved = false; + Object[] resolvedValues = new Object[values.length]; + for (int i = 0; i < values.length; i++) { + String originalValue = values[i]; + Object resolvedValue = doEvaluate(originalValue); + if (resolvedValue != originalValue) { + actuallyResolved = true; + } + resolvedValues[i] = resolvedValue; + } + return (actuallyResolved ? resolvedValues : values); + } + else { + return value; + } + } + + /** + * Evaluate the given String value as an expression, if necessary. + * @param value the original value (may be an expression) + * @return the resolved value if necessary, or the original String value + */ + @Nullable + private Object doEvaluate(@Nullable String value) { + return this.beanFactory.evaluateBeanDefinitionString(value, this.beanDefinition); + } + + /** + * Resolve the target type in the given TypedStringValue. + * @param value the TypedStringValue to resolve + * @return the resolved target type (or {@code null} if none specified) + * @throws ClassNotFoundException if the specified type cannot be resolved + * @see TypedStringValue#resolveTargetType + */ + @Nullable + protected Class resolveTargetType(TypedStringValue value) throws ClassNotFoundException { + if (value.hasTargetType()) { + return value.getTargetType(); + } + return value.resolveTargetType(this.beanFactory.getBeanClassLoader()); + } + + /** + * Resolve a reference to another bean in the factory. + */ + @Nullable + private Object resolveReference(Object argName, RuntimeBeanReference ref) { + try { + Object bean; + Class beanType = ref.getBeanType(); + if (ref.isToParent()) { + BeanFactory parent = this.beanFactory.getParentBeanFactory(); + if (parent == null) { + throw new BeanCreationException( + this.beanDefinition.getResourceDescription(), this.beanName, + "Cannot resolve reference to bean " + ref + + " in parent factory: no parent factory available"); + } + if (beanType != null) { + bean = parent.getBean(beanType); + } + else { + bean = parent.getBean(String.valueOf(doEvaluate(ref.getBeanName()))); + } + } + else { + String resolvedName; + if (beanType != null) { + NamedBeanHolder namedBean = this.beanFactory.resolveNamedBean(beanType); + bean = namedBean.getBeanInstance(); + resolvedName = namedBean.getBeanName(); + } + else { + resolvedName = String.valueOf(doEvaluate(ref.getBeanName())); + bean = this.beanFactory.getBean(resolvedName); + } + this.beanFactory.registerDependentBean(resolvedName, this.beanName); + } + if (bean instanceof NullBean) { + bean = null; + } + return bean; + } + catch (BeansException ex) { + throw new BeanCreationException( + this.beanDefinition.getResourceDescription(), this.beanName, + "Cannot resolve reference to bean '" + ref.getBeanName() + "' while setting " + argName, ex); + } + } + + /** + * Resolve an inner bean definition. + * @param argName the name of the argument that the inner bean is defined for + * @param innerBeanName the name of the inner bean + * @param innerBd the bean definition for the inner bean + * @return the resolved inner bean instance + */ + @Nullable + private Object resolveInnerBean(Object argName, String innerBeanName, BeanDefinition innerBd) { + RootBeanDefinition mbd = null; + try { + mbd = this.beanFactory.getMergedBeanDefinition(innerBeanName, innerBd, this.beanDefinition); + // Check given bean name whether it is unique. If not already unique, + // add counter - increasing the counter until the name is unique. + String actualInnerBeanName = innerBeanName; + if (mbd.isSingleton()) { + actualInnerBeanName = adaptInnerBeanName(innerBeanName); + } + this.beanFactory.registerContainedBean(actualInnerBeanName, this.beanName); + // Guarantee initialization of beans that the inner bean depends on. + String[] dependsOn = mbd.getDependsOn(); + if (dependsOn != null) { + for (String dependsOnBean : dependsOn) { + this.beanFactory.registerDependentBean(dependsOnBean, actualInnerBeanName); + this.beanFactory.getBean(dependsOnBean); + } + } + // Actually create the inner bean instance now... + Object innerBean = this.beanFactory.createBean(actualInnerBeanName, mbd, null); + if (innerBean instanceof FactoryBean) { + boolean synthetic = mbd.isSynthetic(); + innerBean = this.beanFactory.getObjectFromFactoryBean( + (FactoryBean) innerBean, actualInnerBeanName, !synthetic); + } + if (innerBean instanceof NullBean) { + innerBean = null; + } + return innerBean; + } + catch (BeansException ex) { + throw new BeanCreationException( + this.beanDefinition.getResourceDescription(), this.beanName, + "Cannot create inner bean '" + innerBeanName + "' " + + (mbd != null && mbd.getBeanClassName() != null ? "of type [" + mbd.getBeanClassName() + "] " : "") + + "while setting " + argName, ex); + } + } + + /** + * Checks the given bean name whether it is unique. If not already unique, + * a counter is added, increasing the counter until the name is unique. + * @param innerBeanName the original name for the inner bean + * @return the adapted name for the inner bean + */ + private String adaptInnerBeanName(String innerBeanName) { + String actualInnerBeanName = innerBeanName; + int counter = 0; + String prefix = innerBeanName + BeanFactoryUtils.GENERATED_BEAN_NAME_SEPARATOR; + while (this.beanFactory.isBeanNameInUse(actualInnerBeanName)) { + counter++; + actualInnerBeanName = prefix + counter; + } + return actualInnerBeanName; + } + + /** + * For each element in the managed array, resolve reference if necessary. + */ + private Object resolveManagedArray(Object argName, List ml, Class elementType) { + Object resolved = Array.newInstance(elementType, ml.size()); + for (int i = 0; i < ml.size(); i++) { + Array.set(resolved, i, resolveValueIfNecessary(new KeyedArgName(argName, i), ml.get(i))); + } + return resolved; + } + + /** + * For each element in the managed list, resolve reference if necessary. + */ + private List resolveManagedList(Object argName, List ml) { + List resolved = new ArrayList<>(ml.size()); + for (int i = 0; i < ml.size(); i++) { + resolved.add(resolveValueIfNecessary(new KeyedArgName(argName, i), ml.get(i))); + } + return resolved; + } + + /** + * For each element in the managed set, resolve reference if necessary. + */ + private Set resolveManagedSet(Object argName, Set ms) { + Set resolved = new LinkedHashSet<>(ms.size()); + int i = 0; + for (Object m : ms) { + resolved.add(resolveValueIfNecessary(new KeyedArgName(argName, i), m)); + i++; + } + return resolved; + } + + /** + * For each element in the managed map, resolve reference if necessary. + */ + private Map resolveManagedMap(Object argName, Map mm) { + Map resolved = CollectionUtils.newLinkedHashMap(mm.size()); + mm.forEach((key, value) -> { + Object resolvedKey = resolveValueIfNecessary(argName, key); + Object resolvedValue = resolveValueIfNecessary(new KeyedArgName(argName, key), value); + resolved.put(resolvedKey, resolvedValue); + }); + return resolved; + } + + + /** + * Holder class used for delayed toString building. + */ + private static class KeyedArgName { + + private final Object argName; + + private final Object key; + + public KeyedArgName(Object argName, Object key) { + this.argName = argName; + this.key = key; + } + + @Override + public String toString() { + return this.argName + " with key " + BeanWrapper.PROPERTY_KEY_PREFIX + + this.key + BeanWrapper.PROPERTY_KEY_SUFFIX; + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ChildBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ChildBeanDefinition.java new file mode 100644 index 0000000..51b981f --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ChildBeanDefinition.java @@ -0,0 +1,180 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +/** + * Bean definition for beans which inherit settings from their parent. + * Child bean definitions have a fixed dependency on a parent bean definition. + * + *

    A child bean definition will inherit constructor argument values, + * property values and method overrides from the parent, with the option + * to add new values. If init method, destroy method and/or static factory + * method are specified, they will override the corresponding parent settings. + * The remaining settings will always be taken from the child definition: + * depends on, autowire mode, dependency check, singleton, lazy init. + * + *

    NOTE: Since Spring 2.5, the preferred way to register bean + * definitions programmatically is the {@link GenericBeanDefinition} class, + * which allows to dynamically define parent dependencies through the + * {@link GenericBeanDefinition#setParentName} method. This effectively + * supersedes the ChildBeanDefinition class for most use cases. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see GenericBeanDefinition + * @see RootBeanDefinition + */ +@SuppressWarnings("serial") +public class ChildBeanDefinition extends AbstractBeanDefinition { + + @Nullable + private String parentName; + + + /** + * Create a new ChildBeanDefinition for the given parent, to be + * configured through its bean properties and configuration methods. + * @param parentName the name of the parent bean + * @see #setBeanClass + * @see #setScope + * @see #setConstructorArgumentValues + * @see #setPropertyValues + */ + public ChildBeanDefinition(String parentName) { + super(); + this.parentName = parentName; + } + + /** + * Create a new ChildBeanDefinition for the given parent. + * @param parentName the name of the parent bean + * @param pvs the additional property values of the child + */ + public ChildBeanDefinition(String parentName, MutablePropertyValues pvs) { + super(null, pvs); + this.parentName = parentName; + } + + /** + * Create a new ChildBeanDefinition for the given parent. + * @param parentName the name of the parent bean + * @param cargs the constructor argument values to apply + * @param pvs the additional property values of the child + */ + public ChildBeanDefinition( + String parentName, ConstructorArgumentValues cargs, MutablePropertyValues pvs) { + + super(cargs, pvs); + this.parentName = parentName; + } + + /** + * Create a new ChildBeanDefinition for the given parent, + * providing constructor arguments and property values. + * @param parentName the name of the parent bean + * @param beanClass the class of the bean to instantiate + * @param cargs the constructor argument values to apply + * @param pvs the property values to apply + */ + public ChildBeanDefinition( + String parentName, Class beanClass, ConstructorArgumentValues cargs, MutablePropertyValues pvs) { + + super(cargs, pvs); + this.parentName = parentName; + setBeanClass(beanClass); + } + + /** + * Create a new ChildBeanDefinition for the given parent, + * providing constructor arguments and property values. + * Takes a bean class name to avoid eager loading of the bean class. + * @param parentName the name of the parent bean + * @param beanClassName the name of the class to instantiate + * @param cargs the constructor argument values to apply + * @param pvs the property values to apply + */ + public ChildBeanDefinition( + String parentName, String beanClassName, ConstructorArgumentValues cargs, MutablePropertyValues pvs) { + + super(cargs, pvs); + this.parentName = parentName; + setBeanClassName(beanClassName); + } + + /** + * Create a new ChildBeanDefinition as deep copy of the given + * bean definition. + * @param original the original bean definition to copy from + */ + public ChildBeanDefinition(ChildBeanDefinition original) { + super(original); + } + + + @Override + public void setParentName(@Nullable String parentName) { + this.parentName = parentName; + } + + @Override + @Nullable + public String getParentName() { + return this.parentName; + } + + @Override + public void validate() throws BeanDefinitionValidationException { + super.validate(); + if (this.parentName == null) { + throw new BeanDefinitionValidationException("'parentName' must be set in ChildBeanDefinition"); + } + } + + + @Override + public AbstractBeanDefinition cloneBeanDefinition() { + return new ChildBeanDefinition(this); + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof ChildBeanDefinition)) { + return false; + } + ChildBeanDefinition that = (ChildBeanDefinition) other; + return (ObjectUtils.nullSafeEquals(this.parentName, that.parentName) && super.equals(other)); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.parentName) * 29 + super.hashCode(); + } + + @Override + public String toString() { + return "Child bean with parent '" + this.parentName + "': " + super.toString(); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/FactoryBeanRegistrySupport.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/FactoryBeanRegistrySupport.java new file mode 100644 index 0000000..fc689a7 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/FactoryBeanRegistrySupport.java @@ -0,0 +1,252 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support; + +import java.security.AccessControlContext; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanCurrentlyInCreationException; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.FactoryBeanNotInitializedException; +import org.springframework.lang.Nullable; + +/** + * Support base class for singleton registries which need to handle + * {@link org.springframework.beans.factory.FactoryBean} instances, + * integrated with {@link DefaultSingletonBeanRegistry}'s singleton management. + * + *

    Serves as base class for {@link AbstractBeanFactory}. + * + * @author Juergen Hoeller + * @since 2.5.1 + */ +public abstract class FactoryBeanRegistrySupport extends DefaultSingletonBeanRegistry { + + /** Cache of singleton objects created by FactoryBeans: FactoryBean name to object. */ + private final Map factoryBeanObjectCache = new ConcurrentHashMap<>(16); + + + /** + * Determine the type for the given FactoryBean. + * @param factoryBean the FactoryBean instance to check + * @return the FactoryBean's object type, + * or {@code null} if the type cannot be determined yet + */ + @Nullable + protected Class getTypeForFactoryBean(FactoryBean factoryBean) { + try { + if (System.getSecurityManager() != null) { + return AccessController.doPrivileged( + (PrivilegedAction>) factoryBean::getObjectType, getAccessControlContext()); + } + else { + return factoryBean.getObjectType(); + } + } + catch (Throwable ex) { + // Thrown from the FactoryBean's getObjectType implementation. + logger.info("FactoryBean threw exception from getObjectType, despite the contract saying " + + "that it should return null if the type of its object cannot be determined yet", ex); + return null; + } + } + + /** + * Obtain an object to expose from the given FactoryBean, if available + * in cached form. Quick check for minimal synchronization. + * @param beanName the name of the bean + * @return the object obtained from the FactoryBean, + * or {@code null} if not available + */ + @Nullable + protected Object getCachedObjectForFactoryBean(String beanName) { + return this.factoryBeanObjectCache.get(beanName); + } + + /** + * Obtain an object to expose from the given FactoryBean. + * @param factory the FactoryBean instance + * @param beanName the name of the bean + * @param shouldPostProcess whether the bean is subject to post-processing + * @return the object obtained from the FactoryBean + * @throws BeanCreationException if FactoryBean object creation failed + * @see org.springframework.beans.factory.FactoryBean#getObject() + */ + protected Object getObjectFromFactoryBean(FactoryBean factory, String beanName, boolean shouldPostProcess) { + if (factory.isSingleton() && containsSingleton(beanName)) { + synchronized (getSingletonMutex()) { + Object object = this.factoryBeanObjectCache.get(beanName); + if (object == null) { + object = doGetObjectFromFactoryBean(factory, beanName); + // Only post-process and store if not put there already during getObject() call above + // (e.g. because of circular reference processing triggered by custom getBean calls) + Object alreadyThere = this.factoryBeanObjectCache.get(beanName); + if (alreadyThere != null) { + object = alreadyThere; + } + else { + if (shouldPostProcess) { + if (isSingletonCurrentlyInCreation(beanName)) { + // Temporarily return non-post-processed object, not storing it yet.. + return object; + } + beforeSingletonCreation(beanName); + try { + object = postProcessObjectFromFactoryBean(object, beanName); + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, + "Post-processing of FactoryBean's singleton object failed", ex); + } + finally { + afterSingletonCreation(beanName); + } + } + if (containsSingleton(beanName)) { + this.factoryBeanObjectCache.put(beanName, object); + } + } + } + return object; + } + } + else { + Object object = doGetObjectFromFactoryBean(factory, beanName); + if (shouldPostProcess) { + try { + object = postProcessObjectFromFactoryBean(object, beanName); + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, "Post-processing of FactoryBean's object failed", ex); + } + } + return object; + } + } + + /** + * Obtain an object to expose from the given FactoryBean. + * @param factory the FactoryBean instance + * @param beanName the name of the bean + * @return the object obtained from the FactoryBean + * @throws BeanCreationException if FactoryBean object creation failed + * @see org.springframework.beans.factory.FactoryBean#getObject() + */ + private Object doGetObjectFromFactoryBean(FactoryBean factory, String beanName) throws BeanCreationException { + Object object; + try { + if (System.getSecurityManager() != null) { + AccessControlContext acc = getAccessControlContext(); + try { + object = AccessController.doPrivileged((PrivilegedExceptionAction) factory::getObject, acc); + } + catch (PrivilegedActionException pae) { + throw pae.getException(); + } + } + else { + object = factory.getObject(); + } + } + catch (FactoryBeanNotInitializedException ex) { + throw new BeanCurrentlyInCreationException(beanName, ex.toString()); + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, "FactoryBean threw exception on object creation", ex); + } + + // Do not accept a null value for a FactoryBean that's not fully + // initialized yet: Many FactoryBeans just return null then. + if (object == null) { + if (isSingletonCurrentlyInCreation(beanName)) { + throw new BeanCurrentlyInCreationException( + beanName, "FactoryBean which is currently in creation returned null from getObject"); + } + object = new NullBean(); + } + return object; + } + + /** + * Post-process the given object that has been obtained from the FactoryBean. + * The resulting object will get exposed for bean references. + *

    The default implementation simply returns the given object as-is. + * Subclasses may override this, for example, to apply post-processors. + * @param object the object obtained from the FactoryBean. + * @param beanName the name of the bean + * @return the object to expose + * @throws org.springframework.beans.BeansException if any post-processing failed + */ + protected Object postProcessObjectFromFactoryBean(Object object, String beanName) throws BeansException { + return object; + } + + /** + * Get a FactoryBean for the given bean if possible. + * @param beanName the name of the bean + * @param beanInstance the corresponding bean instance + * @return the bean instance as FactoryBean + * @throws BeansException if the given bean cannot be exposed as a FactoryBean + */ + protected FactoryBean getFactoryBean(String beanName, Object beanInstance) throws BeansException { + if (!(beanInstance instanceof FactoryBean)) { + throw new BeanCreationException(beanName, + "Bean instance of type [" + beanInstance.getClass() + "] is not a FactoryBean"); + } + return (FactoryBean) beanInstance; + } + + /** + * Overridden to clear the FactoryBean object cache as well. + */ + @Override + protected void removeSingleton(String beanName) { + synchronized (getSingletonMutex()) { + super.removeSingleton(beanName); + this.factoryBeanObjectCache.remove(beanName); + } + } + + /** + * Overridden to clear the FactoryBean object cache as well. + */ + @Override + protected void clearSingletonCache() { + synchronized (getSingletonMutex()) { + super.clearSingletonCache(); + this.factoryBeanObjectCache.clear(); + } + } + + /** + * Return the security context for this bean factory. If a security manager + * is set, interaction with the user code will be executed using the privileged + * of the security context returned by this method. + * @see AccessController#getContext() + */ + protected AccessControlContext getAccessControlContext() { + return AccessController.getContext(); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/InstantiationStrategy.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/InstantiationStrategy.java new file mode 100644 index 0000000..e917958 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/InstantiationStrategy.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.lang.Nullable; + +/** + * Interface responsible for creating instances corresponding to a root bean definition. + * + *

    This is pulled out into a strategy as various approaches are possible, + * including using CGLIB to create subclasses on the fly to support Method Injection. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 1.1 + */ +public interface InstantiationStrategy { + + /** + * Return an instance of the bean with the given name in this factory. + * @param bd the bean definition + * @param beanName the name of the bean when it is created in this context. + * The name can be {@code null} if we are autowiring a bean which doesn't + * belong to the factory. + * @param owner the owning BeanFactory + * @return a bean instance for this bean definition + * @throws BeansException if the instantiation attempt failed + */ + Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) + throws BeansException; + + /** + * Return an instance of the bean with the given name in this factory, + * creating it via the given constructor. + * @param bd the bean definition + * @param beanName the name of the bean when it is created in this context. + * The name can be {@code null} if we are autowiring a bean which doesn't + * belong to the factory. + * @param owner the owning BeanFactory + * @param ctor the constructor to use + * @param args the constructor arguments to apply + * @return a bean instance for this bean definition + * @throws BeansException if the instantiation attempt failed + */ + Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner, + Constructor ctor, Object... args) throws BeansException; + + /** + * Return an instance of the bean with the given name in this factory, + * creating it via the given factory method. + * @param bd the bean definition + * @param beanName the name of the bean when it is created in this context. + * The name can be {@code null} if we are autowiring a bean which doesn't + * belong to the factory. + * @param owner the owning BeanFactory + * @param factoryBean the factory bean instance to call the factory method on, + * or {@code null} in case of a static factory method + * @param factoryMethod the factory method to use + * @param args the factory method arguments to apply + * @return a bean instance for this bean definition + * @throws BeansException if the instantiation attempt failed + */ + Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner, + @Nullable Object factoryBean, Method factoryMethod, Object... args) + throws BeansException; + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedArray.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedArray.java new file mode 100644 index 0000000..89e346b --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedArray.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Tag collection class used to hold managed array elements, which may + * include runtime bean references (to be resolved into bean objects). + * + * @author Juergen Hoeller + * @since 3.0 + */ +@SuppressWarnings("serial") +public class ManagedArray extends ManagedList { + + /** Resolved element type for runtime creation of the target array. */ + @Nullable + volatile Class resolvedElementType; + + + /** + * Create a new managed array placeholder. + * @param elementTypeName the target element type as a class name + * @param size the size of the array + */ + public ManagedArray(String elementTypeName, int size) { + super(size); + Assert.notNull(elementTypeName, "elementTypeName must not be null"); + setElementTypeName(elementTypeName); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverrides.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverrides.java new file mode 100644 index 0000000..a84a15f --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverrides.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support; + +import java.lang.reflect.Method; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +import org.springframework.lang.Nullable; + +/** + * Set of method overrides, determining which, if any, methods on a + * managed object the Spring IoC container will override at runtime. + * + *

    The currently supported {@link MethodOverride} variants are + * {@link LookupOverride} and {@link ReplaceOverride}. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 1.1 + * @see MethodOverride + */ +public class MethodOverrides { + + private final Set overrides = new CopyOnWriteArraySet<>(); + + + /** + * Create new MethodOverrides. + */ + public MethodOverrides() { + } + + /** + * Deep copy constructor. + */ + public MethodOverrides(MethodOverrides other) { + addOverrides(other); + } + + + /** + * Copy all given method overrides into this object. + */ + public void addOverrides(@Nullable MethodOverrides other) { + if (other != null) { + this.overrides.addAll(other.overrides); + } + } + + /** + * Add the given method override. + */ + public void addOverride(MethodOverride override) { + this.overrides.add(override); + } + + /** + * Return all method overrides contained by this object. + * @return a Set of MethodOverride objects + * @see MethodOverride + */ + public Set getOverrides() { + return this.overrides; + } + + /** + * Return whether the set of method overrides is empty. + */ + public boolean isEmpty() { + return this.overrides.isEmpty(); + } + + /** + * Return the override for the given method, if any. + * @param method method to check for overrides for + * @return the method override, or {@code null} if none + */ + @Nullable + public MethodOverride getOverride(Method method) { + MethodOverride match = null; + for (MethodOverride candidate : this.overrides) { + if (candidate.matches(method)) { + match = candidate; + } + } + return match; + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof MethodOverrides)) { + return false; + } + MethodOverrides that = (MethodOverrides) other; + return this.overrides.equals(that.overrides); + } + + @Override + public int hashCode() { + return this.overrides.hashCode(); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/PropertiesBeanDefinitionReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/PropertiesBeanDefinitionReader.java new file mode 100644 index 0000000..617a13b --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/PropertiesBeanDefinitionReader.java @@ -0,0 +1,540 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.ResourceBundle; + +import org.springframework.beans.BeansException; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.PropertyAccessor; +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.CannotLoadBeanClassException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.core.io.support.ResourcePropertiesPersister; +import org.springframework.lang.Nullable; +import org.springframework.util.PropertiesPersister; +import org.springframework.util.StringUtils; + +/** + * Bean definition reader for a simple properties format. + * + *

    Provides bean definition registration methods for Map/Properties and + * ResourceBundle. Typically applied to a DefaultListableBeanFactory. + * + *

    Example: + * + *

    + * employee.(class)=MyClass       // bean is of class MyClass
    + * employee.(abstract)=true       // this bean can't be instantiated directly
    + * employee.group=Insurance       // real property
    + * employee.usesDialUp=false      // real property (potentially overridden)
    + *
    + * salesrep.(parent)=employee     // derives from "employee" bean definition
    + * salesrep.(lazy-init)=true      // lazily initialize this singleton bean
    + * salesrep.manager(ref)=tony     // reference to another bean
    + * salesrep.department=Sales      // real property
    + *
    + * techie.(parent)=employee       // derives from "employee" bean definition
    + * techie.(scope)=prototype       // bean is a prototype (not a shared instance)
    + * techie.manager(ref)=jeff       // reference to another bean
    + * techie.department=Engineering  // real property
    + * techie.usesDialUp=true         // real property (overriding parent value)
    + *
    + * ceo.$0(ref)=secretary          // inject 'secretary' bean as 0th constructor arg
    + * ceo.$1=1000000                 // inject value '1000000' at 1st constructor arg
    + * 
    + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Rob Harrop + * @since 26.11.2003 + * @see DefaultListableBeanFactory + * @deprecated as of 5.3, in favor of Spring's common bean definition formats + * and/or custom reader implementations + */ +@Deprecated +public class PropertiesBeanDefinitionReader extends AbstractBeanDefinitionReader { + + /** + * Value of a T/F attribute that represents true. + * Anything else represents false. Case seNsItive. + */ + public static final String TRUE_VALUE = "true"; + + /** + * Separator between bean name and property name. + * We follow normal Java conventions. + */ + public static final String SEPARATOR = "."; + + /** + * Special key to distinguish {@code owner.(class)=com.myapp.MyClass}. + */ + public static final String CLASS_KEY = "(class)"; + + /** + * Special key to distinguish {@code owner.(parent)=parentBeanName}. + */ + public static final String PARENT_KEY = "(parent)"; + + /** + * Special key to distinguish {@code owner.(scope)=prototype}. + * Default is "true". + */ + public static final String SCOPE_KEY = "(scope)"; + + /** + * Special key to distinguish {@code owner.(singleton)=false}. + * Default is "true". + */ + public static final String SINGLETON_KEY = "(singleton)"; + + /** + * Special key to distinguish {@code owner.(abstract)=true} + * Default is "false". + */ + public static final String ABSTRACT_KEY = "(abstract)"; + + /** + * Special key to distinguish {@code owner.(lazy-init)=true} + * Default is "false". + */ + public static final String LAZY_INIT_KEY = "(lazy-init)"; + + /** + * Property suffix for references to other beans in the current + * BeanFactory: e.g. {@code owner.dog(ref)=fido}. + * Whether this is a reference to a singleton or a prototype + * will depend on the definition of the target bean. + */ + public static final String REF_SUFFIX = "(ref)"; + + /** + * Prefix before values referencing other beans. + */ + public static final String REF_PREFIX = "*"; + + /** + * Prefix used to denote a constructor argument definition. + */ + public static final String CONSTRUCTOR_ARG_PREFIX = "$"; + + + @Nullable + private String defaultParentBean; + + private PropertiesPersister propertiesPersister = ResourcePropertiesPersister.INSTANCE; + + + /** + * Create new PropertiesBeanDefinitionReader for the given bean factory. + * @param registry the BeanFactory to load bean definitions into, + * in the form of a BeanDefinitionRegistry + */ + public PropertiesBeanDefinitionReader(BeanDefinitionRegistry registry) { + super(registry); + } + + + /** + * Set the default parent bean for this bean factory. + * If a child bean definition handled by this factory provides neither + * a parent nor a class attribute, this default value gets used. + *

    Can be used e.g. for view definition files, to define a parent + * with a default view class and common attributes for all views. + * View definitions that define their own parent or carry their own + * class can still override this. + *

    Strictly speaking, the rule that a default parent setting does + * not apply to a bean definition that carries a class is there for + * backwards compatibility reasons. It still matches the typical use case. + */ + public void setDefaultParentBean(@Nullable String defaultParentBean) { + this.defaultParentBean = defaultParentBean; + } + + /** + * Return the default parent bean for this bean factory. + */ + @Nullable + public String getDefaultParentBean() { + return this.defaultParentBean; + } + + /** + * Set the PropertiesPersister to use for parsing properties files. + * The default is ResourcePropertiesPersister. + * @see ResourcePropertiesPersister#INSTANCE + */ + public void setPropertiesPersister(@Nullable PropertiesPersister propertiesPersister) { + this.propertiesPersister = + (propertiesPersister != null ? propertiesPersister : ResourcePropertiesPersister.INSTANCE); + } + + /** + * Return the PropertiesPersister to use for parsing properties files. + */ + public PropertiesPersister getPropertiesPersister() { + return this.propertiesPersister; + } + + + /** + * Load bean definitions from the specified properties file, + * using all property keys (i.e. not filtering by prefix). + * @param resource the resource descriptor for the properties file + * @return the number of bean definitions found + * @throws BeanDefinitionStoreException in case of loading or parsing errors + * @see #loadBeanDefinitions(org.springframework.core.io.Resource, String) + */ + @Override + public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException { + return loadBeanDefinitions(new EncodedResource(resource), null); + } + + /** + * Load bean definitions from the specified properties file. + * @param resource the resource descriptor for the properties file + * @param prefix a filter within the keys in the map: e.g. 'beans.' + * (can be empty or {@code null}) + * @return the number of bean definitions found + * @throws BeanDefinitionStoreException in case of loading or parsing errors + */ + public int loadBeanDefinitions(Resource resource, @Nullable String prefix) throws BeanDefinitionStoreException { + return loadBeanDefinitions(new EncodedResource(resource), prefix); + } + + /** + * Load bean definitions from the specified properties file. + * @param encodedResource the resource descriptor for the properties file, + * allowing to specify an encoding to use for parsing the file + * @return the number of bean definitions found + * @throws BeanDefinitionStoreException in case of loading or parsing errors + */ + public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException { + return loadBeanDefinitions(encodedResource, null); + } + + /** + * Load bean definitions from the specified properties file. + * @param encodedResource the resource descriptor for the properties file, + * allowing to specify an encoding to use for parsing the file + * @param prefix a filter within the keys in the map: e.g. 'beans.' + * (can be empty or {@code null}) + * @return the number of bean definitions found + * @throws BeanDefinitionStoreException in case of loading or parsing errors + */ + public int loadBeanDefinitions(EncodedResource encodedResource, @Nullable String prefix) + throws BeanDefinitionStoreException { + + if (logger.isTraceEnabled()) { + logger.trace("Loading properties bean definitions from " + encodedResource); + } + + Properties props = new Properties(); + try { + try (InputStream is = encodedResource.getResource().getInputStream()) { + if (encodedResource.getEncoding() != null) { + getPropertiesPersister().load(props, new InputStreamReader(is, encodedResource.getEncoding())); + } + else { + getPropertiesPersister().load(props, is); + } + } + + int count = registerBeanDefinitions(props, prefix, encodedResource.getResource().getDescription()); + if (logger.isDebugEnabled()) { + logger.debug("Loaded " + count + " bean definitions from " + encodedResource); + } + return count; + } + catch (IOException ex) { + throw new BeanDefinitionStoreException("Could not parse properties from " + encodedResource.getResource(), ex); + } + } + + /** + * Register bean definitions contained in a resource bundle, + * using all property keys (i.e. not filtering by prefix). + * @param rb the ResourceBundle to load from + * @return the number of bean definitions found + * @throws BeanDefinitionStoreException in case of loading or parsing errors + * @see #registerBeanDefinitions(java.util.ResourceBundle, String) + */ + public int registerBeanDefinitions(ResourceBundle rb) throws BeanDefinitionStoreException { + return registerBeanDefinitions(rb, null); + } + + /** + * Register bean definitions contained in a ResourceBundle. + *

    Similar syntax as for a Map. This method is useful to enable + * standard Java internationalization support. + * @param rb the ResourceBundle to load from + * @param prefix a filter within the keys in the map: e.g. 'beans.' + * (can be empty or {@code null}) + * @return the number of bean definitions found + * @throws BeanDefinitionStoreException in case of loading or parsing errors + */ + public int registerBeanDefinitions(ResourceBundle rb, @Nullable String prefix) throws BeanDefinitionStoreException { + // Simply create a map and call overloaded method. + Map map = new HashMap<>(); + Enumeration keys = rb.getKeys(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + map.put(key, rb.getObject(key)); + } + return registerBeanDefinitions(map, prefix); + } + + + /** + * Register bean definitions contained in a Map, using all property keys (i.e. not + * filtering by prefix). + * @param map a map of {@code name} to {@code property} (String or Object). Property + * values will be strings if coming from a Properties file etc. Property names + * (keys) must be Strings. Class keys must be Strings. + * @return the number of bean definitions found + * @throws BeansException in case of loading or parsing errors + * @see #registerBeanDefinitions(java.util.Map, String, String) + */ + public int registerBeanDefinitions(Map map) throws BeansException { + return registerBeanDefinitions(map, null); + } + + /** + * Register bean definitions contained in a Map. + * Ignore ineligible properties. + * @param map a map of {@code name} to {@code property} (String or Object). Property + * values will be strings if coming from a Properties file etc. Property names + * (keys) must be Strings. Class keys must be Strings. + * @param prefix a filter within the keys in the map: e.g. 'beans.' + * (can be empty or {@code null}) + * @return the number of bean definitions found + * @throws BeansException in case of loading or parsing errors + */ + public int registerBeanDefinitions(Map map, @Nullable String prefix) throws BeansException { + return registerBeanDefinitions(map, prefix, "Map " + map); + } + + /** + * Register bean definitions contained in a Map. + * Ignore ineligible properties. + * @param map a map of {@code name} to {@code property} (String or Object). Property + * values will be strings if coming from a Properties file etc. Property names + * (keys) must be Strings. Class keys must be Strings. + * @param prefix a filter within the keys in the map: e.g. 'beans.' + * (can be empty or {@code null}) + * @param resourceDescription description of the resource that the + * Map came from (for logging purposes) + * @return the number of bean definitions found + * @throws BeansException in case of loading or parsing errors + * @see #registerBeanDefinitions(Map, String) + */ + public int registerBeanDefinitions(Map map, @Nullable String prefix, String resourceDescription) + throws BeansException { + + if (prefix == null) { + prefix = ""; + } + int beanCount = 0; + + for (Object key : map.keySet()) { + if (!(key instanceof String)) { + throw new IllegalArgumentException("Illegal key [" + key + "]: only Strings allowed"); + } + String keyString = (String) key; + if (keyString.startsWith(prefix)) { + // Key is of form: prefix.property + String nameAndProperty = keyString.substring(prefix.length()); + // Find dot before property name, ignoring dots in property keys. + int sepIdx ; + int propKeyIdx = nameAndProperty.indexOf(PropertyAccessor.PROPERTY_KEY_PREFIX); + if (propKeyIdx != -1) { + sepIdx = nameAndProperty.lastIndexOf(SEPARATOR, propKeyIdx); + } + else { + sepIdx = nameAndProperty.lastIndexOf(SEPARATOR); + } + if (sepIdx != -1) { + String beanName = nameAndProperty.substring(0, sepIdx); + if (logger.isTraceEnabled()) { + logger.trace("Found bean name '" + beanName + "'"); + } + if (!getRegistry().containsBeanDefinition(beanName)) { + // If we haven't already registered it... + registerBeanDefinition(beanName, map, prefix + beanName, resourceDescription); + ++beanCount; + } + } + else { + // Ignore it: It wasn't a valid bean name and property, + // although it did start with the required prefix. + if (logger.isDebugEnabled()) { + logger.debug("Invalid bean name and property [" + nameAndProperty + "]"); + } + } + } + } + + return beanCount; + } + + /** + * Get all property values, given a prefix (which will be stripped) + * and add the bean they define to the factory with the given name. + * @param beanName name of the bean to define + * @param map a Map containing string pairs + * @param prefix prefix of each entry, which will be stripped + * @param resourceDescription description of the resource that the + * Map came from (for logging purposes) + * @throws BeansException if the bean definition could not be parsed or registered + */ + protected void registerBeanDefinition(String beanName, Map map, String prefix, String resourceDescription) + throws BeansException { + + String className = null; + String parent = null; + String scope = BeanDefinition.SCOPE_SINGLETON; + boolean isAbstract = false; + boolean lazyInit = false; + + ConstructorArgumentValues cas = new ConstructorArgumentValues(); + MutablePropertyValues pvs = new MutablePropertyValues(); + + String prefixWithSep = prefix + SEPARATOR; + int beginIndex = prefixWithSep.length(); + + for (Map.Entry entry : map.entrySet()) { + String key = StringUtils.trimWhitespace((String) entry.getKey()); + if (key.startsWith(prefixWithSep)) { + String property = key.substring(beginIndex); + if (CLASS_KEY.equals(property)) { + className = StringUtils.trimWhitespace((String) entry.getValue()); + } + else if (PARENT_KEY.equals(property)) { + parent = StringUtils.trimWhitespace((String) entry.getValue()); + } + else if (ABSTRACT_KEY.equals(property)) { + String val = StringUtils.trimWhitespace((String) entry.getValue()); + isAbstract = TRUE_VALUE.equals(val); + } + else if (SCOPE_KEY.equals(property)) { + // Spring 2.0 style + scope = StringUtils.trimWhitespace((String) entry.getValue()); + } + else if (SINGLETON_KEY.equals(property)) { + // Spring 1.2 style + String val = StringUtils.trimWhitespace((String) entry.getValue()); + scope = (!StringUtils.hasLength(val) || TRUE_VALUE.equals(val) ? + BeanDefinition.SCOPE_SINGLETON : BeanDefinition.SCOPE_PROTOTYPE); + } + else if (LAZY_INIT_KEY.equals(property)) { + String val = StringUtils.trimWhitespace((String) entry.getValue()); + lazyInit = TRUE_VALUE.equals(val); + } + else if (property.startsWith(CONSTRUCTOR_ARG_PREFIX)) { + if (property.endsWith(REF_SUFFIX)) { + int index = Integer.parseInt(property.substring(1, property.length() - REF_SUFFIX.length())); + cas.addIndexedArgumentValue(index, new RuntimeBeanReference(entry.getValue().toString())); + } + else { + int index = Integer.parseInt(property.substring(1)); + cas.addIndexedArgumentValue(index, readValue(entry)); + } + } + else if (property.endsWith(REF_SUFFIX)) { + // This isn't a real property, but a reference to another prototype + // Extract property name: property is of form dog(ref) + property = property.substring(0, property.length() - REF_SUFFIX.length()); + String ref = StringUtils.trimWhitespace((String) entry.getValue()); + + // It doesn't matter if the referenced bean hasn't yet been registered: + // this will ensure that the reference is resolved at runtime. + Object val = new RuntimeBeanReference(ref); + pvs.add(property, val); + } + else { + // It's a normal bean property. + pvs.add(property, readValue(entry)); + } + } + } + + if (logger.isTraceEnabled()) { + logger.trace("Registering bean definition for bean name '" + beanName + "' with " + pvs); + } + + // Just use default parent if we're not dealing with the parent itself, + // and if there's no class name specified. The latter has to happen for + // backwards compatibility reasons. + if (parent == null && className == null && !beanName.equals(this.defaultParentBean)) { + parent = this.defaultParentBean; + } + + try { + AbstractBeanDefinition bd = BeanDefinitionReaderUtils.createBeanDefinition( + parent, className, getBeanClassLoader()); + bd.setScope(scope); + bd.setAbstract(isAbstract); + bd.setLazyInit(lazyInit); + bd.setConstructorArgumentValues(cas); + bd.setPropertyValues(pvs); + getRegistry().registerBeanDefinition(beanName, bd); + } + catch (ClassNotFoundException ex) { + throw new CannotLoadBeanClassException(resourceDescription, beanName, className, ex); + } + catch (LinkageError err) { + throw new CannotLoadBeanClassException(resourceDescription, beanName, className, err); + } + } + + /** + * Reads the value of the entry. Correctly interprets bean references for + * values that are prefixed with an asterisk. + */ + private Object readValue(Map.Entry entry) { + Object val = entry.getValue(); + if (val instanceof String) { + String strVal = (String) val; + // If it starts with a reference prefix... + if (strVal.startsWith(REF_PREFIX)) { + // Expand the reference. + String targetName = strVal.substring(1); + if (targetName.startsWith(REF_PREFIX)) { + // Escaped prefix -> use plain value. + val = targetName; + } + else { + val = new RuntimeBeanReference(targetName); + } + } + } + return val; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ReplaceOverride.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ReplaceOverride.java new file mode 100644 index 0000000..1b51bf7 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ReplaceOverride.java @@ -0,0 +1,121 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Extension of MethodOverride that represents an arbitrary + * override of a method by the IoC container. + * + *

    Any non-final method can be overridden, irrespective of its + * parameters and return types. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 1.1 + */ +public class ReplaceOverride extends MethodOverride { + + private final String methodReplacerBeanName; + + private final List typeIdentifiers = new ArrayList<>(); + + + /** + * Construct a new ReplaceOverride. + * @param methodName the name of the method to override + * @param methodReplacerBeanName the bean name of the MethodReplacer + */ + public ReplaceOverride(String methodName, String methodReplacerBeanName) { + super(methodName); + Assert.notNull(methodReplacerBeanName, "Method replacer bean name must not be null"); + this.methodReplacerBeanName = methodReplacerBeanName; + } + + + /** + * Return the name of the bean implementing MethodReplacer. + */ + public String getMethodReplacerBeanName() { + return this.methodReplacerBeanName; + } + + /** + * Add a fragment of a class string, like "Exception" + * or "java.lang.Exc", to identify a parameter type. + * @param identifier a substring of the fully qualified class name + */ + public void addTypeIdentifier(String identifier) { + this.typeIdentifiers.add(identifier); + } + + + @Override + public boolean matches(Method method) { + if (!method.getName().equals(getMethodName())) { + return false; + } + if (!isOverloaded()) { + // Not overloaded: don't worry about arg type matching... + return true; + } + // If we get here, we need to insist on precise argument matching... + if (this.typeIdentifiers.size() != method.getParameterCount()) { + return false; + } + Class[] parameterTypes = method.getParameterTypes(); + for (int i = 0; i < this.typeIdentifiers.size(); i++) { + String identifier = this.typeIdentifiers.get(i); + if (!parameterTypes[i].getName().contains(identifier)) { + return false; + } + } + return true; + } + + + @Override + public boolean equals(@Nullable Object other) { + if (!(other instanceof ReplaceOverride) || !super.equals(other)) { + return false; + } + ReplaceOverride that = (ReplaceOverride) other; + return (ObjectUtils.nullSafeEquals(this.methodReplacerBeanName, that.methodReplacerBeanName) && + ObjectUtils.nullSafeEquals(this.typeIdentifiers, that.typeIdentifiers)); + } + + @Override + public int hashCode() { + int hashCode = super.hashCode(); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.methodReplacerBeanName); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.typeIdentifiers); + return hashCode; + } + + @Override + public String toString() { + return "Replace override for method '" + getMethodName() + "'"; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ScopeNotActiveException.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ScopeNotActiveException.java new file mode 100644 index 0000000..bb7cdda --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ScopeNotActiveException.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support; + +import org.springframework.beans.factory.BeanCreationException; + +/** + * A subclass of {@link BeanCreationException} which indicates that the target scope + * is not active, e.g. in case of request or session scope. + * + * @author Juergen Hoeller + * @since 5.3 + * @see org.springframework.beans.factory.BeanFactory#getBean + * @see org.springframework.beans.factory.config.Scope + * @see AbstractBeanDefinition#setScope + */ +@SuppressWarnings("serial") +public class ScopeNotActiveException extends BeanCreationException { + + /** + * Create a new ScopeNotActiveException. + * @param beanName the name of the bean requested + * @param scopeName the name of the target scope + * @param cause the root cause, typically from {@link org.springframework.beans.factory.config.Scope#get} + */ + public ScopeNotActiveException(String beanName, String scopeName, IllegalStateException cause) { + super(beanName, "Scope '" + scopeName + "' is not active for the current thread; consider " + + "defining a scoped proxy for this bean if you intend to refer to it from a singleton", cause); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleAutowireCandidateResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleAutowireCandidateResolver.java new file mode 100644 index 0000000..f33eeec --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleAutowireCandidateResolver.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support; + +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.lang.Nullable; + +/** + * {@link AutowireCandidateResolver} implementation to use when no annotation + * support is available. This implementation checks the bean definition only. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @since 2.5 + */ +public class SimpleAutowireCandidateResolver implements AutowireCandidateResolver { + + /** + * Shared instance of {@code SimpleAutowireCandidateResolver}. + * @since 5.2.7 + */ + public static final SimpleAutowireCandidateResolver INSTANCE = new SimpleAutowireCandidateResolver(); + + + @Override + public boolean isAutowireCandidate(BeanDefinitionHolder bdHolder, DependencyDescriptor descriptor) { + return bdHolder.getBeanDefinition().isAutowireCandidate(); + } + + @Override + public boolean isRequired(DependencyDescriptor descriptor) { + return descriptor.isRequired(); + } + + @Override + public boolean hasQualifier(DependencyDescriptor descriptor) { + return false; + } + + @Override + @Nullable + public Object getSuggestedValue(DependencyDescriptor descriptor) { + return null; + } + + @Override + @Nullable + public Object getLazyResolutionProxyIfNecessary(DependencyDescriptor descriptor, @Nullable String beanName) { + return null; + } + + /** + * This implementation returns {@code this} as-is. + * @see #INSTANCE + */ + @Override + public AutowireCandidateResolver cloneIfNecessary() { + return this; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleInstantiationStrategy.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleInstantiationStrategy.java new file mode 100644 index 0000000..0b05cf5 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleInstantiationStrategy.java @@ -0,0 +1,189 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.security.PrivilegedExceptionAction; + +import org.springframework.beans.BeanInstantiationException; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * Simple object instantiation strategy for use in a BeanFactory. + * + *

    Does not support Method Injection, although it provides hooks for subclasses + * to override to add Method Injection support, for example by overriding methods. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 1.1 + */ +public class SimpleInstantiationStrategy implements InstantiationStrategy { + + private static final ThreadLocal currentlyInvokedFactoryMethod = new ThreadLocal<>(); + + + /** + * Return the factory method currently being invoked or {@code null} if none. + *

    Allows factory method implementations to determine whether the current + * caller is the container itself as opposed to user code. + */ + @Nullable + public static Method getCurrentlyInvokedFactoryMethod() { + return currentlyInvokedFactoryMethod.get(); + } + + + @Override + public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) { + // Don't override the class with CGLIB if no overrides. + if (!bd.hasMethodOverrides()) { + Constructor constructorToUse; + synchronized (bd.constructorArgumentLock) { + constructorToUse = (Constructor) bd.resolvedConstructorOrFactoryMethod; + if (constructorToUse == null) { + final Class clazz = bd.getBeanClass(); + if (clazz.isInterface()) { + throw new BeanInstantiationException(clazz, "Specified class is an interface"); + } + try { + if (System.getSecurityManager() != null) { + constructorToUse = AccessController.doPrivileged( + (PrivilegedExceptionAction>) clazz::getDeclaredConstructor); + } + else { + constructorToUse = clazz.getDeclaredConstructor(); + } + bd.resolvedConstructorOrFactoryMethod = constructorToUse; + } + catch (Throwable ex) { + throw new BeanInstantiationException(clazz, "No default constructor found", ex); + } + } + } + return BeanUtils.instantiateClass(constructorToUse); + } + else { + // Must generate CGLIB subclass. + return instantiateWithMethodInjection(bd, beanName, owner); + } + } + + /** + * Subclasses can override this method, which is implemented to throw + * UnsupportedOperationException, if they can instantiate an object with + * the Method Injection specified in the given RootBeanDefinition. + * Instantiation should use a no-arg constructor. + */ + protected Object instantiateWithMethodInjection(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) { + throw new UnsupportedOperationException("Method Injection not supported in SimpleInstantiationStrategy"); + } + + @Override + public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner, + final Constructor ctor, Object... args) { + + if (!bd.hasMethodOverrides()) { + if (System.getSecurityManager() != null) { + // use own privileged to change accessibility (when security is on) + AccessController.doPrivileged((PrivilegedAction) () -> { + ReflectionUtils.makeAccessible(ctor); + return null; + }); + } + return BeanUtils.instantiateClass(ctor, args); + } + else { + return instantiateWithMethodInjection(bd, beanName, owner, ctor, args); + } + } + + /** + * Subclasses can override this method, which is implemented to throw + * UnsupportedOperationException, if they can instantiate an object with + * the Method Injection specified in the given RootBeanDefinition. + * Instantiation should use the given constructor and parameters. + */ + protected Object instantiateWithMethodInjection(RootBeanDefinition bd, @Nullable String beanName, + BeanFactory owner, @Nullable Constructor ctor, Object... args) { + + throw new UnsupportedOperationException("Method Injection not supported in SimpleInstantiationStrategy"); + } + + @Override + public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner, + @Nullable Object factoryBean, final Method factoryMethod, Object... args) { + + try { + if (System.getSecurityManager() != null) { + AccessController.doPrivileged((PrivilegedAction) () -> { + ReflectionUtils.makeAccessible(factoryMethod); + return null; + }); + } + else { + ReflectionUtils.makeAccessible(factoryMethod); + } + + Method priorInvokedFactoryMethod = currentlyInvokedFactoryMethod.get(); + try { + currentlyInvokedFactoryMethod.set(factoryMethod); + Object result = factoryMethod.invoke(factoryBean, args); + if (result == null) { + result = new NullBean(); + } + return result; + } + finally { + if (priorInvokedFactoryMethod != null) { + currentlyInvokedFactoryMethod.set(priorInvokedFactoryMethod); + } + else { + currentlyInvokedFactoryMethod.remove(); + } + } + } + catch (IllegalArgumentException ex) { + throw new BeanInstantiationException(factoryMethod, + "Illegal arguments to factory method '" + factoryMethod.getName() + "'; " + + "args: " + StringUtils.arrayToCommaDelimitedString(args), ex); + } + catch (IllegalAccessException ex) { + throw new BeanInstantiationException(factoryMethod, + "Cannot access factory method '" + factoryMethod.getName() + "'; is it public?", ex); + } + catch (InvocationTargetException ex) { + String msg = "Factory method '" + factoryMethod.getName() + "' threw exception"; + if (bd.getFactoryBeanName() != null && owner instanceof ConfigurableBeanFactory && + ((ConfigurableBeanFactory) owner).isCurrentlyInCreation(bd.getFactoryBeanName())) { + msg = "Circular reference involving containing bean '" + bd.getFactoryBeanName() + "' - consider " + + "declaring the factory method as static for independence from its containing instance. " + msg; + } + throw new BeanInstantiationException(factoryMethod, msg, ex.getTargetException()); + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/AbstractBeanDefinitionParser.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/AbstractBeanDefinitionParser.java new file mode 100644 index 0000000..288ad40 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/AbstractBeanDefinitionParser.java @@ -0,0 +1,219 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Abstract {@link BeanDefinitionParser} implementation providing + * a number of convenience methods and a + * {@link AbstractBeanDefinitionParser#parseInternal template method} + * that subclasses must override to provide the actual parsing logic. + * + *

    Use this {@link BeanDefinitionParser} implementation when you want + * to parse some arbitrarily complex XML into one or more + * {@link BeanDefinition BeanDefinitions}. If you just want to parse some + * XML into a single {@code BeanDefinition}, you may wish to consider + * the simpler convenience extensions of this class, namely + * {@link AbstractSingleBeanDefinitionParser} and + * {@link AbstractSimpleBeanDefinitionParser}. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Rick Evans + * @author Dave Syer + * @since 2.0 + */ +public abstract class AbstractBeanDefinitionParser implements BeanDefinitionParser { + + /** Constant for the "id" attribute. */ + public static final String ID_ATTRIBUTE = "id"; + + /** Constant for the "name" attribute. */ + public static final String NAME_ATTRIBUTE = "name"; + + + @Override + @Nullable + public final BeanDefinition parse(Element element, ParserContext parserContext) { + AbstractBeanDefinition definition = parseInternal(element, parserContext); + if (definition != null && !parserContext.isNested()) { + try { + String id = resolveId(element, definition, parserContext); + if (!StringUtils.hasText(id)) { + parserContext.getReaderContext().error( + "Id is required for element '" + parserContext.getDelegate().getLocalName(element) + + "' when used as a top-level tag", element); + } + String[] aliases = null; + if (shouldParseNameAsAliases()) { + String name = element.getAttribute(NAME_ATTRIBUTE); + if (StringUtils.hasLength(name)) { + aliases = StringUtils.trimArrayElements(StringUtils.commaDelimitedListToStringArray(name)); + } + } + BeanDefinitionHolder holder = new BeanDefinitionHolder(definition, id, aliases); + registerBeanDefinition(holder, parserContext.getRegistry()); + if (shouldFireEvents()) { + BeanComponentDefinition componentDefinition = new BeanComponentDefinition(holder); + postProcessComponentDefinition(componentDefinition); + parserContext.registerComponent(componentDefinition); + } + } + catch (BeanDefinitionStoreException ex) { + String msg = ex.getMessage(); + parserContext.getReaderContext().error((msg != null ? msg : ex.toString()), element); + return null; + } + } + return definition; + } + + /** + * Resolve the ID for the supplied {@link BeanDefinition}. + *

    When using {@link #shouldGenerateId generation}, a name is generated automatically. + * Otherwise, the ID is extracted from the "id" attribute, potentially with a + * {@link #shouldGenerateIdAsFallback() fallback} to a generated id. + * @param element the element that the bean definition has been built from + * @param definition the bean definition to be registered + * @param parserContext the object encapsulating the current state of the parsing process; + * provides access to a {@link org.springframework.beans.factory.support.BeanDefinitionRegistry} + * @return the resolved id + * @throws BeanDefinitionStoreException if no unique name could be generated + * for the given bean definition + */ + protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext) + throws BeanDefinitionStoreException { + + if (shouldGenerateId()) { + return parserContext.getReaderContext().generateBeanName(definition); + } + else { + String id = element.getAttribute(ID_ATTRIBUTE); + if (!StringUtils.hasText(id) && shouldGenerateIdAsFallback()) { + id = parserContext.getReaderContext().generateBeanName(definition); + } + return id; + } + } + + /** + * Register the supplied {@link BeanDefinitionHolder bean} with the supplied + * {@link BeanDefinitionRegistry registry}. + *

    Subclasses can override this method to control whether or not the supplied + * {@link BeanDefinitionHolder bean} is actually even registered, or to + * register even more beans. + *

    The default implementation registers the supplied {@link BeanDefinitionHolder bean} + * with the supplied {@link BeanDefinitionRegistry registry} only if the {@code isNested} + * parameter is {@code false}, because one typically does not want inner beans + * to be registered as top level beans. + * @param definition the bean definition to be registered + * @param registry the registry that the bean is to be registered with + * @see BeanDefinitionReaderUtils#registerBeanDefinition(BeanDefinitionHolder, BeanDefinitionRegistry) + */ + protected void registerBeanDefinition(BeanDefinitionHolder definition, BeanDefinitionRegistry registry) { + BeanDefinitionReaderUtils.registerBeanDefinition(definition, registry); + } + + + /** + * Central template method to actually parse the supplied {@link Element} + * into one or more {@link BeanDefinition BeanDefinitions}. + * @param element the element that is to be parsed into one or more {@link BeanDefinition BeanDefinitions} + * @param parserContext the object encapsulating the current state of the parsing process; + * provides access to a {@link org.springframework.beans.factory.support.BeanDefinitionRegistry} + * @return the primary {@link BeanDefinition} resulting from the parsing of the supplied {@link Element} + * @see #parse(org.w3c.dom.Element, ParserContext) + * @see #postProcessComponentDefinition(org.springframework.beans.factory.parsing.BeanComponentDefinition) + */ + @Nullable + protected abstract AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext); + + /** + * Should an ID be generated instead of read from the passed in {@link Element}? + *

    Disabled by default; subclasses can override this to enable ID generation. + * Note that this flag is about always generating an ID; the parser + * won't even check for an "id" attribute in this case. + * @return whether the parser should always generate an id + */ + protected boolean shouldGenerateId() { + return false; + } + + /** + * Should an ID be generated instead if the passed in {@link Element} does not + * specify an "id" attribute explicitly? + *

    Disabled by default; subclasses can override this to enable ID generation + * as fallback: The parser will first check for an "id" attribute in this case, + * only falling back to a generated ID if no value was specified. + * @return whether the parser should generate an id if no id was specified + */ + protected boolean shouldGenerateIdAsFallback() { + return false; + } + + /** + * Determine whether the element's "name" attribute should get parsed as + * bean definition aliases, i.e. alternative bean definition names. + *

    The default implementation returns {@code true}. + * @return whether the parser should evaluate the "name" attribute as aliases + * @since 4.1.5 + */ + protected boolean shouldParseNameAsAliases() { + return true; + } + + /** + * Determine whether this parser is supposed to fire a + * {@link org.springframework.beans.factory.parsing.BeanComponentDefinition} + * event after parsing the bean definition. + *

    This implementation returns {@code true} by default; that is, + * an event will be fired when a bean definition has been completely parsed. + * Override this to return {@code false} in order to suppress the event. + * @return {@code true} in order to fire a component registration event + * after parsing the bean definition; {@code false} to suppress the event + * @see #postProcessComponentDefinition + * @see org.springframework.beans.factory.parsing.ReaderContext#fireComponentRegistered + */ + protected boolean shouldFireEvents() { + return true; + } + + /** + * Hook method called after the primary parsing of a + * {@link BeanComponentDefinition} but before the + * {@link BeanComponentDefinition} has been registered with a + * {@link org.springframework.beans.factory.support.BeanDefinitionRegistry}. + *

    Derived classes can override this method to supply any custom logic that + * is to be executed after all the parsing is finished. + *

    The default implementation is a no-op. + * @param componentDefinition the {@link BeanComponentDefinition} that is to be processed + */ + protected void postProcessComponentDefinition(BeanComponentDefinition componentDefinition) { + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/AbstractSimpleBeanDefinitionParser.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/AbstractSimpleBeanDefinitionParser.java new file mode 100644 index 0000000..015801b --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/AbstractSimpleBeanDefinitionParser.java @@ -0,0 +1,195 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.w3c.dom.Attr; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; + +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.core.Conventions; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Convenient base class for when there exists a one-to-one mapping + * between attribute names on the element that is to be parsed and + * the property names on the {@link Class} being configured. + * + *

    Extend this parser class when you want to create a single + * bean definition from a relatively simple custom XML element. The + * resulting {@code BeanDefinition} will be automatically + * registered with the relevant + * {@link org.springframework.beans.factory.support.BeanDefinitionRegistry}. + * + *

    An example will hopefully make the use of this particular parser + * class immediately clear. Consider the following class definition: + * + *

    public class SimpleCache implements Cache {
    + *
    + *     public void setName(String name) {...}
    + *     public void setTimeout(int timeout) {...}
    + *     public void setEvictionPolicy(EvictionPolicy policy) {...}
    + *
    + *     // remaining class definition elided for clarity...
    + * }
    + * + *

    Then let us assume the following XML tag has been defined to + * permit the easy configuration of instances of the above class; + * + *

    <caching:cache name="..." timeout="..." eviction-policy="..."/>
    + * + *

    All that is required of the Java developer tasked with writing + * the parser to parse the above XML tag into an actual + * {@code SimpleCache} bean definition is the following: + * + *

    public class SimpleCacheBeanDefinitionParser extends AbstractSimpleBeanDefinitionParser {
    + *
    + *     protected Class getBeanClass(Element element) {
    + *         return SimpleCache.class;
    + *     }
    + * }
    + * + *

    Please note that the {@code AbstractSimpleBeanDefinitionParser} + * is limited to populating the created bean definition with property values. + * if you want to parse constructor arguments and nested elements from the + * supplied XML element, then you will have to implement the + * {@link #postProcess(org.springframework.beans.factory.support.BeanDefinitionBuilder, org.w3c.dom.Element)} + * method and do such parsing yourself, or (more likely) subclass the + * {@link AbstractSingleBeanDefinitionParser} or {@link AbstractBeanDefinitionParser} + * classes directly. + * + *

    The process of actually registering the + * {@code SimpleCacheBeanDefinitionParser} with the Spring XML parsing + * infrastructure is described in the Spring Framework reference documentation + * (in one of the appendices). + * + *

    For an example of this parser in action (so to speak), do look at + * the source code for the + * {@link org.springframework.beans.factory.xml.UtilNamespaceHandler.PropertiesBeanDefinitionParser}; + * the observant (and even not so observant) reader will immediately notice that + * there is next to no code in the implementation. The + * {@code PropertiesBeanDefinitionParser} populates a + * {@link org.springframework.beans.factory.config.PropertiesFactoryBean} + * from an XML element that looks like this: + * + *

    <util:properties location="jdbc.properties"/>
    + * + *

    The observant reader will notice that the sole attribute on the + * {@code } element matches the + * {@link org.springframework.beans.factory.config.PropertiesFactoryBean#setLocation(org.springframework.core.io.Resource)} + * method name on the {@code PropertiesFactoryBean} (the general + * usage thus illustrated holds true for any number of attributes). + * All that the {@code PropertiesBeanDefinitionParser} needs + * actually do is supply an implementation of the + * {@link #getBeanClass(org.w3c.dom.Element)} method to return the + * {@code PropertiesFactoryBean} type. + * + * @author Rob Harrop + * @author Rick Evans + * @author Juergen Hoeller + * @since 2.0 + * @see Conventions#attributeNameToPropertyName(String) + */ +public abstract class AbstractSimpleBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { + + /** + * Parse the supplied {@link Element} and populate the supplied + * {@link BeanDefinitionBuilder} as required. + *

    This implementation maps any attributes present on the + * supplied element to {@link org.springframework.beans.PropertyValue} + * instances, and + * {@link BeanDefinitionBuilder#addPropertyValue(String, Object) adds them} + * to the + * {@link org.springframework.beans.factory.config.BeanDefinition builder}. + *

    The {@link #extractPropertyName(String)} method is used to + * reconcile the name of an attribute with the name of a JavaBean + * property. + * @param element the XML element being parsed + * @param builder used to define the {@code BeanDefinition} + * @see #extractPropertyName(String) + */ + @Override + protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + NamedNodeMap attributes = element.getAttributes(); + for (int x = 0; x < attributes.getLength(); x++) { + Attr attribute = (Attr) attributes.item(x); + if (isEligibleAttribute(attribute, parserContext)) { + String propertyName = extractPropertyName(attribute.getLocalName()); + Assert.state(StringUtils.hasText(propertyName), + "Illegal property name returned from 'extractPropertyName(String)': cannot be null or empty."); + builder.addPropertyValue(propertyName, attribute.getValue()); + } + } + postProcess(builder, element); + } + + /** + * Determine whether the given attribute is eligible for being + * turned into a corresponding bean property value. + *

    The default implementation considers any attribute as eligible, + * except for the "id" attribute and namespace declaration attributes. + * @param attribute the XML attribute to check + * @param parserContext the {@code ParserContext} + * @see #isEligibleAttribute(String) + */ + protected boolean isEligibleAttribute(Attr attribute, ParserContext parserContext) { + String fullName = attribute.getName(); + return (!fullName.equals("xmlns") && !fullName.startsWith("xmlns:") && + isEligibleAttribute(parserContext.getDelegate().getLocalName(attribute))); + } + + /** + * Determine whether the given attribute is eligible for being + * turned into a corresponding bean property value. + *

    The default implementation considers any attribute as eligible, + * except for the "id" attribute. + * @param attributeName the attribute name taken straight from the + * XML element being parsed (never {@code null}) + */ + protected boolean isEligibleAttribute(String attributeName) { + return !ID_ATTRIBUTE.equals(attributeName); + } + + /** + * Extract a JavaBean property name from the supplied attribute name. + *

    The default implementation uses the + * {@link Conventions#attributeNameToPropertyName(String)} + * method to perform the extraction. + *

    The name returned must obey the standard JavaBean property name + * conventions. For example for a class with a setter method + * '{@code setBingoHallFavourite(String)}', the name returned had + * better be '{@code bingoHallFavourite}' (with that exact casing). + * @param attributeName the attribute name taken straight from the + * XML element being parsed (never {@code null}) + * @return the extracted JavaBean property name (must never be {@code null}) + */ + protected String extractPropertyName(String attributeName) { + return Conventions.attributeNameToPropertyName(attributeName); + } + + /** + * Hook method that derived classes can implement to inspect/change a + * bean definition after parsing is complete. + *

    The default implementation does nothing. + * @param beanDefinition the parsed (and probably totally defined) bean definition being built + * @param element the XML element that was the source of the bean definition's metadata + */ + protected void postProcess(BeanDefinitionBuilder beanDefinition, Element element) { + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/AbstractSingleBeanDefinitionParser.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/AbstractSingleBeanDefinitionParser.java new file mode 100644 index 0000000..75b7079 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/AbstractSingleBeanDefinitionParser.java @@ -0,0 +1,159 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.lang.Nullable; + +/** + * Base class for those {@link BeanDefinitionParser} implementations that + * need to parse and define just a single {@code BeanDefinition}. + * + *

    Extend this parser class when you want to create a single bean definition + * from an arbitrarily complex XML element. You may wish to consider extending + * the {@link AbstractSimpleBeanDefinitionParser} when you want to create a + * single bean definition from a relatively simple custom XML element. + * + *

    The resulting {@code BeanDefinition} will be automatically registered + * with the {@link org.springframework.beans.factory.support.BeanDefinitionRegistry}. + * Your job simply is to {@link #doParse parse} the custom XML {@link Element} + * into a single {@code BeanDefinition}. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Rick Evans + * @since 2.0 + * @see #getBeanClass + * @see #getBeanClassName + * @see #doParse + */ +public abstract class AbstractSingleBeanDefinitionParser extends AbstractBeanDefinitionParser { + + /** + * Creates a {@link BeanDefinitionBuilder} instance for the + * {@link #getBeanClass bean Class} and passes it to the + * {@link #doParse} strategy method. + * @param element the element that is to be parsed into a single BeanDefinition + * @param parserContext the object encapsulating the current state of the parsing process + * @return the BeanDefinition resulting from the parsing of the supplied {@link Element} + * @throws IllegalStateException if the bean {@link Class} returned from + * {@link #getBeanClass(org.w3c.dom.Element)} is {@code null} + * @see #doParse + */ + @Override + protected final AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(); + String parentName = getParentName(element); + if (parentName != null) { + builder.getRawBeanDefinition().setParentName(parentName); + } + Class beanClass = getBeanClass(element); + if (beanClass != null) { + builder.getRawBeanDefinition().setBeanClass(beanClass); + } + else { + String beanClassName = getBeanClassName(element); + if (beanClassName != null) { + builder.getRawBeanDefinition().setBeanClassName(beanClassName); + } + } + builder.getRawBeanDefinition().setSource(parserContext.extractSource(element)); + BeanDefinition containingBd = parserContext.getContainingBeanDefinition(); + if (containingBd != null) { + // Inner bean definition must receive same scope as containing bean. + builder.setScope(containingBd.getScope()); + } + if (parserContext.isDefaultLazyInit()) { + // Default-lazy-init applies to custom bean definitions as well. + builder.setLazyInit(true); + } + doParse(element, parserContext, builder); + return builder.getBeanDefinition(); + } + + /** + * Determine the name for the parent of the currently parsed bean, + * in case of the current bean being defined as a child bean. + *

    The default implementation returns {@code null}, + * indicating a root bean definition. + * @param element the {@code Element} that is being parsed + * @return the name of the parent bean for the currently parsed bean, + * or {@code null} if none + */ + @Nullable + protected String getParentName(Element element) { + return null; + } + + /** + * Determine the bean class corresponding to the supplied {@link Element}. + *

    Note that, for application classes, it is generally preferable to + * override {@link #getBeanClassName} instead, in order to avoid a direct + * dependence on the bean implementation class. The BeanDefinitionParser + * and its NamespaceHandler can be used within an IDE plugin then, even + * if the application classes are not available on the plugin's classpath. + * @param element the {@code Element} that is being parsed + * @return the {@link Class} of the bean that is being defined via parsing + * the supplied {@code Element}, or {@code null} if none + * @see #getBeanClassName + */ + @Nullable + protected Class getBeanClass(Element element) { + return null; + } + + /** + * Determine the bean class name corresponding to the supplied {@link Element}. + * @param element the {@code Element} that is being parsed + * @return the class name of the bean that is being defined via parsing + * the supplied {@code Element}, or {@code null} if none + * @see #getBeanClass + */ + @Nullable + protected String getBeanClassName(Element element) { + return null; + } + + /** + * Parse the supplied {@link Element} and populate the supplied + * {@link BeanDefinitionBuilder} as required. + *

    The default implementation delegates to the {@code doParse} + * version without ParserContext argument. + * @param element the XML element being parsed + * @param parserContext the object encapsulating the current state of the parsing process + * @param builder used to define the {@code BeanDefinition} + * @see #doParse(Element, BeanDefinitionBuilder) + */ + protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + doParse(element, builder); + } + + /** + * Parse the supplied {@link Element} and populate the supplied + * {@link BeanDefinitionBuilder} as required. + *

    The default implementation does nothing. + * @param element the XML element being parsed + * @param builder used to define the {@code BeanDefinition} + */ + protected void doParse(Element element, BeanDefinitionBuilder builder) { + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionDecorator.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionDecorator.java new file mode 100644 index 0000000..b50d70f --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionDecorator.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.w3c.dom.Node; + +import org.springframework.beans.factory.config.BeanDefinitionHolder; + +/** + * Interface used by the {@link DefaultBeanDefinitionDocumentReader} + * to handle custom, nested (directly under a {@code }) tags. + * + *

    Decoration may also occur based on custom attributes applied to the + * {@code } tag. Implementations are free to turn the metadata in the + * custom tag into as many + * {@link org.springframework.beans.factory.config.BeanDefinition BeanDefinitions} as + * required and to transform the + * {@link org.springframework.beans.factory.config.BeanDefinition} of the enclosing + * {@code } tag, potentially even returning a completely different + * {@link org.springframework.beans.factory.config.BeanDefinition} to replace the + * original. + * + *

    {@link BeanDefinitionDecorator BeanDefinitionDecorators} should be aware that + * they may be part of a chain. In particular, a {@link BeanDefinitionDecorator} should + * be aware that a previous {@link BeanDefinitionDecorator} may have replaced the + * original {@link org.springframework.beans.factory.config.BeanDefinition} with a + * {@link org.springframework.aop.framework.ProxyFactoryBean} definition allowing for + * custom {@link org.aopalliance.intercept.MethodInterceptor interceptors} to be added. + * + *

    {@link BeanDefinitionDecorator BeanDefinitionDecorators} that wish to add an + * interceptor to the enclosing bean should extend + * {@link org.springframework.aop.config.AbstractInterceptorDrivenBeanDefinitionDecorator} + * which handles the chaining ensuring that only one proxy is created and that it + * contains all interceptors from the chain. + * + *

    The parser locates a {@link BeanDefinitionDecorator} from the + * {@link NamespaceHandler} for the namespace in which the custom tag resides. + * + * @author Rob Harrop + * @since 2.0 + * @see NamespaceHandler + * @see BeanDefinitionParser + */ +public interface BeanDefinitionDecorator { + + /** + * Parse the specified {@link Node} (either an element or an attribute) and decorate + * the supplied {@link org.springframework.beans.factory.config.BeanDefinition}, + * returning the decorated definition. + *

    Implementations may choose to return a completely new definition, which will + * replace the original definition in the resulting + * {@link org.springframework.beans.factory.BeanFactory}. + *

    The supplied {@link ParserContext} can be used to register any additional + * beans needed to support the main definition. + */ + BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder definition, ParserContext parserContext); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionDocumentReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionDocumentReader.java new file mode 100644 index 0000000..8bd14ff --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionDocumentReader.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.w3c.dom.Document; + +import org.springframework.beans.factory.BeanDefinitionStoreException; + +/** + * SPI for parsing an XML document that contains Spring bean definitions. + * Used by {@link XmlBeanDefinitionReader} for actually parsing a DOM document. + * + *

    Instantiated per document to parse: implementations can hold + * state in instance variables during the execution of the + * {@code registerBeanDefinitions} method — for example, global + * settings that are defined for all bean definitions in the document. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @since 18.12.2003 + * @see XmlBeanDefinitionReader#setDocumentReaderClass + */ +public interface BeanDefinitionDocumentReader { + + /** + * Read bean definitions from the given DOM document and + * register them with the registry in the given reader context. + * @param doc the DOM document + * @param readerContext the current context of the reader + * (includes the target registry and the resource being parsed) + * @throws BeanDefinitionStoreException in case of parsing errors + */ + void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) + throws BeanDefinitionStoreException; + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParser.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParser.java new file mode 100644 index 0000000..a92f282 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParser.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2011 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.lang.Nullable; + +/** + * Interface used by the {@link DefaultBeanDefinitionDocumentReader} to handle custom, + * top-level (directly under {@code }) tags. + * + *

    Implementations are free to turn the metadata in the custom tag into as many + * {@link BeanDefinition BeanDefinitions} as required. + * + *

    The parser locates a {@link BeanDefinitionParser} from the associated + * {@link NamespaceHandler} for the namespace in which the custom tag resides. + * + * @author Rob Harrop + * @since 2.0 + * @see NamespaceHandler + * @see AbstractBeanDefinitionParser + */ +public interface BeanDefinitionParser { + + /** + * Parse the specified {@link Element} and register the resulting + * {@link BeanDefinition BeanDefinition(s)} with the + * {@link org.springframework.beans.factory.xml.ParserContext#getRegistry() BeanDefinitionRegistry} + * embedded in the supplied {@link ParserContext}. + *

    Implementations must return the primary {@link BeanDefinition} that results + * from the parse if they will ever be used in a nested fashion (for example as + * an inner tag in a {@code } tag). Implementations may return + * {@code null} if they will not be used in a nested fashion. + * @param element the element that is to be parsed into one or more {@link BeanDefinition BeanDefinitions} + * @param parserContext the object encapsulating the current state of the parsing process; + * provides access to a {@link org.springframework.beans.factory.support.BeanDefinitionRegistry} + * @return the primary {@link BeanDefinition} + */ + @Nullable + BeanDefinition parse(Element element, ParserContext parserContext); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParserDelegate.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParserDelegate.java new file mode 100644 index 0000000..6be311e --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParserDelegate.java @@ -0,0 +1,1544 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import org.springframework.beans.BeanMetadataAttribute; +import org.springframework.beans.BeanMetadataAttributeAccessor; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.beans.factory.config.RuntimeBeanNameReference; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.config.TypedStringValue; +import org.springframework.beans.factory.parsing.BeanEntry; +import org.springframework.beans.factory.parsing.ConstructorArgumentEntry; +import org.springframework.beans.factory.parsing.ParseState; +import org.springframework.beans.factory.parsing.PropertyEntry; +import org.springframework.beans.factory.parsing.QualifierEntry; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.AutowireCandidateQualifier; +import org.springframework.beans.factory.support.BeanDefinitionDefaults; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.support.LookupOverride; +import org.springframework.beans.factory.support.ManagedArray; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.support.ManagedMap; +import org.springframework.beans.factory.support.ManagedProperties; +import org.springframework.beans.factory.support.ManagedSet; +import org.springframework.beans.factory.support.MethodOverrides; +import org.springframework.beans.factory.support.ReplaceOverride; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.PatternMatchUtils; +import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; + +/** + * Stateful delegate class used to parse XML bean definitions. + * Intended for use by both the main parser and any extension + * {@link BeanDefinitionParser BeanDefinitionParsers} or + * {@link BeanDefinitionDecorator BeanDefinitionDecorators}. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Rod Johnson + * @author Mark Fisher + * @author Gary Russell + * @since 2.0 + * @see ParserContext + * @see DefaultBeanDefinitionDocumentReader + */ +public class BeanDefinitionParserDelegate { + + public static final String BEANS_NAMESPACE_URI = "http://www.springframework.org/schema/beans"; + + public static final String MULTI_VALUE_ATTRIBUTE_DELIMITERS = ",; "; + + /** + * Value of a T/F attribute that represents true. + * Anything else represents false. + */ + public static final String TRUE_VALUE = "true"; + + public static final String FALSE_VALUE = "false"; + + public static final String DEFAULT_VALUE = "default"; + + public static final String DESCRIPTION_ELEMENT = "description"; + + public static final String AUTOWIRE_NO_VALUE = "no"; + + public static final String AUTOWIRE_BY_NAME_VALUE = "byName"; + + public static final String AUTOWIRE_BY_TYPE_VALUE = "byType"; + + public static final String AUTOWIRE_CONSTRUCTOR_VALUE = "constructor"; + + public static final String AUTOWIRE_AUTODETECT_VALUE = "autodetect"; + + public static final String NAME_ATTRIBUTE = "name"; + + public static final String BEAN_ELEMENT = "bean"; + + public static final String META_ELEMENT = "meta"; + + public static final String ID_ATTRIBUTE = "id"; + + public static final String PARENT_ATTRIBUTE = "parent"; + + public static final String CLASS_ATTRIBUTE = "class"; + + public static final String ABSTRACT_ATTRIBUTE = "abstract"; + + public static final String SCOPE_ATTRIBUTE = "scope"; + + private static final String SINGLETON_ATTRIBUTE = "singleton"; + + public static final String LAZY_INIT_ATTRIBUTE = "lazy-init"; + + public static final String AUTOWIRE_ATTRIBUTE = "autowire"; + + public static final String AUTOWIRE_CANDIDATE_ATTRIBUTE = "autowire-candidate"; + + public static final String PRIMARY_ATTRIBUTE = "primary"; + + public static final String DEPENDS_ON_ATTRIBUTE = "depends-on"; + + public static final String INIT_METHOD_ATTRIBUTE = "init-method"; + + public static final String DESTROY_METHOD_ATTRIBUTE = "destroy-method"; + + public static final String FACTORY_METHOD_ATTRIBUTE = "factory-method"; + + public static final String FACTORY_BEAN_ATTRIBUTE = "factory-bean"; + + public static final String CONSTRUCTOR_ARG_ELEMENT = "constructor-arg"; + + public static final String INDEX_ATTRIBUTE = "index"; + + public static final String TYPE_ATTRIBUTE = "type"; + + public static final String VALUE_TYPE_ATTRIBUTE = "value-type"; + + public static final String KEY_TYPE_ATTRIBUTE = "key-type"; + + public static final String PROPERTY_ELEMENT = "property"; + + public static final String REF_ATTRIBUTE = "ref"; + + public static final String VALUE_ATTRIBUTE = "value"; + + public static final String LOOKUP_METHOD_ELEMENT = "lookup-method"; + + public static final String REPLACED_METHOD_ELEMENT = "replaced-method"; + + public static final String REPLACER_ATTRIBUTE = "replacer"; + + public static final String ARG_TYPE_ELEMENT = "arg-type"; + + public static final String ARG_TYPE_MATCH_ATTRIBUTE = "match"; + + public static final String REF_ELEMENT = "ref"; + + public static final String IDREF_ELEMENT = "idref"; + + public static final String BEAN_REF_ATTRIBUTE = "bean"; + + public static final String PARENT_REF_ATTRIBUTE = "parent"; + + public static final String VALUE_ELEMENT = "value"; + + public static final String NULL_ELEMENT = "null"; + + public static final String ARRAY_ELEMENT = "array"; + + public static final String LIST_ELEMENT = "list"; + + public static final String SET_ELEMENT = "set"; + + public static final String MAP_ELEMENT = "map"; + + public static final String ENTRY_ELEMENT = "entry"; + + public static final String KEY_ELEMENT = "key"; + + public static final String KEY_ATTRIBUTE = "key"; + + public static final String KEY_REF_ATTRIBUTE = "key-ref"; + + public static final String VALUE_REF_ATTRIBUTE = "value-ref"; + + public static final String PROPS_ELEMENT = "props"; + + public static final String PROP_ELEMENT = "prop"; + + public static final String MERGE_ATTRIBUTE = "merge"; + + public static final String QUALIFIER_ELEMENT = "qualifier"; + + public static final String QUALIFIER_ATTRIBUTE_ELEMENT = "attribute"; + + public static final String DEFAULT_LAZY_INIT_ATTRIBUTE = "default-lazy-init"; + + public static final String DEFAULT_MERGE_ATTRIBUTE = "default-merge"; + + public static final String DEFAULT_AUTOWIRE_ATTRIBUTE = "default-autowire"; + + public static final String DEFAULT_AUTOWIRE_CANDIDATES_ATTRIBUTE = "default-autowire-candidates"; + + public static final String DEFAULT_INIT_METHOD_ATTRIBUTE = "default-init-method"; + + public static final String DEFAULT_DESTROY_METHOD_ATTRIBUTE = "default-destroy-method"; + + + protected final Log logger = LogFactory.getLog(getClass()); + + private final XmlReaderContext readerContext; + + private final DocumentDefaultsDefinition defaults = new DocumentDefaultsDefinition(); + + private final ParseState parseState = new ParseState(); + + /** + * Stores all used bean names so we can enforce uniqueness on a per + * beans-element basis. Duplicate bean ids/names may not exist within the + * same level of beans element nesting, but may be duplicated across levels. + */ + private final Set usedNames = new HashSet<>(); + + + /** + * Create a new BeanDefinitionParserDelegate associated with the supplied + * {@link XmlReaderContext}. + */ + public BeanDefinitionParserDelegate(XmlReaderContext readerContext) { + Assert.notNull(readerContext, "XmlReaderContext must not be null"); + this.readerContext = readerContext; + } + + + /** + * Get the {@link XmlReaderContext} associated with this helper instance. + */ + public final XmlReaderContext getReaderContext() { + return this.readerContext; + } + + /** + * Invoke the {@link org.springframework.beans.factory.parsing.SourceExtractor} + * to pull the source metadata from the supplied {@link Element}. + */ + @Nullable + protected Object extractSource(Element ele) { + return this.readerContext.extractSource(ele); + } + + /** + * Report an error with the given message for the given source element. + */ + protected void error(String message, Node source) { + this.readerContext.error(message, source, this.parseState.snapshot()); + } + + /** + * Report an error with the given message for the given source element. + */ + protected void error(String message, Element source) { + this.readerContext.error(message, source, this.parseState.snapshot()); + } + + /** + * Report an error with the given message for the given source element. + */ + protected void error(String message, Element source, Throwable cause) { + this.readerContext.error(message, source, this.parseState.snapshot(), cause); + } + + + /** + * Initialize the default settings assuming a {@code null} parent delegate. + */ + public void initDefaults(Element root) { + initDefaults(root, null); + } + + /** + * Initialize the default lazy-init, autowire, dependency check settings, + * init-method, destroy-method and merge settings. Support nested 'beans' + * element use cases by falling back to the given parent in case the + * defaults are not explicitly set locally. + * @see #populateDefaults(DocumentDefaultsDefinition, DocumentDefaultsDefinition, org.w3c.dom.Element) + * @see #getDefaults() + */ + public void initDefaults(Element root, @Nullable BeanDefinitionParserDelegate parent) { + populateDefaults(this.defaults, (parent != null ? parent.defaults : null), root); + this.readerContext.fireDefaultsRegistered(this.defaults); + } + + /** + * Populate the given DocumentDefaultsDefinition instance with the default lazy-init, + * autowire, dependency check settings, init-method, destroy-method and merge settings. + * Support nested 'beans' element use cases by falling back to {@code parentDefaults} + * in case the defaults are not explicitly set locally. + * @param defaults the defaults to populate + * @param parentDefaults the parent BeanDefinitionParserDelegate (if any) defaults to fall back to + * @param root the root element of the current bean definition document (or nested beans element) + */ + protected void populateDefaults(DocumentDefaultsDefinition defaults, @Nullable DocumentDefaultsDefinition parentDefaults, Element root) { + String lazyInit = root.getAttribute(DEFAULT_LAZY_INIT_ATTRIBUTE); + if (isDefaultValue(lazyInit)) { + // Potentially inherited from outer sections, otherwise falling back to false. + lazyInit = (parentDefaults != null ? parentDefaults.getLazyInit() : FALSE_VALUE); + } + defaults.setLazyInit(lazyInit); + + String merge = root.getAttribute(DEFAULT_MERGE_ATTRIBUTE); + if (isDefaultValue(merge)) { + // Potentially inherited from outer sections, otherwise falling back to false. + merge = (parentDefaults != null ? parentDefaults.getMerge() : FALSE_VALUE); + } + defaults.setMerge(merge); + + String autowire = root.getAttribute(DEFAULT_AUTOWIRE_ATTRIBUTE); + if (isDefaultValue(autowire)) { + // Potentially inherited from outer sections, otherwise falling back to 'no'. + autowire = (parentDefaults != null ? parentDefaults.getAutowire() : AUTOWIRE_NO_VALUE); + } + defaults.setAutowire(autowire); + + if (root.hasAttribute(DEFAULT_AUTOWIRE_CANDIDATES_ATTRIBUTE)) { + defaults.setAutowireCandidates(root.getAttribute(DEFAULT_AUTOWIRE_CANDIDATES_ATTRIBUTE)); + } + else if (parentDefaults != null) { + defaults.setAutowireCandidates(parentDefaults.getAutowireCandidates()); + } + + if (root.hasAttribute(DEFAULT_INIT_METHOD_ATTRIBUTE)) { + defaults.setInitMethod(root.getAttribute(DEFAULT_INIT_METHOD_ATTRIBUTE)); + } + else if (parentDefaults != null) { + defaults.setInitMethod(parentDefaults.getInitMethod()); + } + + if (root.hasAttribute(DEFAULT_DESTROY_METHOD_ATTRIBUTE)) { + defaults.setDestroyMethod(root.getAttribute(DEFAULT_DESTROY_METHOD_ATTRIBUTE)); + } + else if (parentDefaults != null) { + defaults.setDestroyMethod(parentDefaults.getDestroyMethod()); + } + + defaults.setSource(this.readerContext.extractSource(root)); + } + + /** + * Return the defaults definition object. + */ + public DocumentDefaultsDefinition getDefaults() { + return this.defaults; + } + + /** + * Return the default settings for bean definitions as indicated within + * the attributes of the top-level {@code } element. + */ + public BeanDefinitionDefaults getBeanDefinitionDefaults() { + BeanDefinitionDefaults bdd = new BeanDefinitionDefaults(); + bdd.setLazyInit(TRUE_VALUE.equalsIgnoreCase(this.defaults.getLazyInit())); + bdd.setAutowireMode(getAutowireMode(DEFAULT_VALUE)); + bdd.setInitMethodName(this.defaults.getInitMethod()); + bdd.setDestroyMethodName(this.defaults.getDestroyMethod()); + return bdd; + } + + /** + * Return any patterns provided in the 'default-autowire-candidates' + * attribute of the top-level {@code } element. + */ + @Nullable + public String[] getAutowireCandidatePatterns() { + String candidatePattern = this.defaults.getAutowireCandidates(); + return (candidatePattern != null ? StringUtils.commaDelimitedListToStringArray(candidatePattern) : null); + } + + + /** + * Parses the supplied {@code } element. May return {@code null} + * if there were errors during parse. Errors are reported to the + * {@link org.springframework.beans.factory.parsing.ProblemReporter}. + */ + @Nullable + public BeanDefinitionHolder parseBeanDefinitionElement(Element ele) { + return parseBeanDefinitionElement(ele, null); + } + + /** + * Parses the supplied {@code } element. May return {@code null} + * if there were errors during parse. Errors are reported to the + * {@link org.springframework.beans.factory.parsing.ProblemReporter}. + */ + @Nullable + public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) { + String id = ele.getAttribute(ID_ATTRIBUTE); + String nameAttr = ele.getAttribute(NAME_ATTRIBUTE); + + List aliases = new ArrayList<>(); + if (StringUtils.hasLength(nameAttr)) { + String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, MULTI_VALUE_ATTRIBUTE_DELIMITERS); + aliases.addAll(Arrays.asList(nameArr)); + } + + String beanName = id; + if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) { + beanName = aliases.remove(0); + if (logger.isTraceEnabled()) { + logger.trace("No XML 'id' specified - using '" + beanName + + "' as bean name and " + aliases + " as aliases"); + } + } + + if (containingBean == null) { + checkNameUniqueness(beanName, aliases, ele); + } + + AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean); + if (beanDefinition != null) { + if (!StringUtils.hasText(beanName)) { + try { + if (containingBean != null) { + beanName = BeanDefinitionReaderUtils.generateBeanName( + beanDefinition, this.readerContext.getRegistry(), true); + } + else { + beanName = this.readerContext.generateBeanName(beanDefinition); + // Register an alias for the plain bean class name, if still possible, + // if the generator returned the class name plus a suffix. + // This is expected for Spring 1.2/2.0 backwards compatibility. + String beanClassName = beanDefinition.getBeanClassName(); + if (beanClassName != null && + beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() && + !this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) { + aliases.add(beanClassName); + } + } + if (logger.isTraceEnabled()) { + logger.trace("Neither XML 'id' nor 'name' specified - " + + "using generated bean name [" + beanName + "]"); + } + } + catch (Exception ex) { + error(ex.getMessage(), ele); + return null; + } + } + String[] aliasesArray = StringUtils.toStringArray(aliases); + return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray); + } + + return null; + } + + /** + * Validate that the specified bean name and aliases have not been used already + * within the current level of beans element nesting. + */ + protected void checkNameUniqueness(String beanName, List aliases, Element beanElement) { + String foundName = null; + + if (StringUtils.hasText(beanName) && this.usedNames.contains(beanName)) { + foundName = beanName; + } + if (foundName == null) { + foundName = CollectionUtils.findFirstMatch(this.usedNames, aliases); + } + if (foundName != null) { + error("Bean name '" + foundName + "' is already used in this element", beanElement); + } + + this.usedNames.add(beanName); + this.usedNames.addAll(aliases); + } + + /** + * Parse the bean definition itself, without regard to name or aliases. May return + * {@code null} if problems occurred during the parsing of the bean definition. + */ + @Nullable + public AbstractBeanDefinition parseBeanDefinitionElement( + Element ele, String beanName, @Nullable BeanDefinition containingBean) { + + this.parseState.push(new BeanEntry(beanName)); + + String className = null; + if (ele.hasAttribute(CLASS_ATTRIBUTE)) { + className = ele.getAttribute(CLASS_ATTRIBUTE).trim(); + } + String parent = null; + if (ele.hasAttribute(PARENT_ATTRIBUTE)) { + parent = ele.getAttribute(PARENT_ATTRIBUTE); + } + + try { + AbstractBeanDefinition bd = createBeanDefinition(className, parent); + + parseBeanDefinitionAttributes(ele, beanName, containingBean, bd); + bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT)); + + parseMetaElements(ele, bd); + parseLookupOverrideSubElements(ele, bd.getMethodOverrides()); + parseReplacedMethodSubElements(ele, bd.getMethodOverrides()); + + parseConstructorArgElements(ele, bd); + parsePropertyElements(ele, bd); + parseQualifierElements(ele, bd); + + bd.setResource(this.readerContext.getResource()); + bd.setSource(extractSource(ele)); + + return bd; + } + catch (ClassNotFoundException ex) { + error("Bean class [" + className + "] not found", ele, ex); + } + catch (NoClassDefFoundError err) { + error("Class that bean class [" + className + "] depends on not found", ele, err); + } + catch (Throwable ex) { + error("Unexpected failure during bean definition parsing", ele, ex); + } + finally { + this.parseState.pop(); + } + + return null; + } + + /** + * Apply the attributes of the given bean element to the given bean * definition. + * @param ele bean declaration element + * @param beanName bean name + * @param containingBean containing bean definition + * @return a bean definition initialized according to the bean element attributes + */ + public AbstractBeanDefinition parseBeanDefinitionAttributes(Element ele, String beanName, + @Nullable BeanDefinition containingBean, AbstractBeanDefinition bd) { + + if (ele.hasAttribute(SINGLETON_ATTRIBUTE)) { + error("Old 1.x 'singleton' attribute in use - upgrade to 'scope' declaration", ele); + } + else if (ele.hasAttribute(SCOPE_ATTRIBUTE)) { + bd.setScope(ele.getAttribute(SCOPE_ATTRIBUTE)); + } + else if (containingBean != null) { + // Take default from containing bean in case of an inner bean definition. + bd.setScope(containingBean.getScope()); + } + + if (ele.hasAttribute(ABSTRACT_ATTRIBUTE)) { + bd.setAbstract(TRUE_VALUE.equals(ele.getAttribute(ABSTRACT_ATTRIBUTE))); + } + + String lazyInit = ele.getAttribute(LAZY_INIT_ATTRIBUTE); + if (isDefaultValue(lazyInit)) { + lazyInit = this.defaults.getLazyInit(); + } + bd.setLazyInit(TRUE_VALUE.equals(lazyInit)); + + String autowire = ele.getAttribute(AUTOWIRE_ATTRIBUTE); + bd.setAutowireMode(getAutowireMode(autowire)); + + if (ele.hasAttribute(DEPENDS_ON_ATTRIBUTE)) { + String dependsOn = ele.getAttribute(DEPENDS_ON_ATTRIBUTE); + bd.setDependsOn(StringUtils.tokenizeToStringArray(dependsOn, MULTI_VALUE_ATTRIBUTE_DELIMITERS)); + } + + String autowireCandidate = ele.getAttribute(AUTOWIRE_CANDIDATE_ATTRIBUTE); + if (isDefaultValue(autowireCandidate)) { + String candidatePattern = this.defaults.getAutowireCandidates(); + if (candidatePattern != null) { + String[] patterns = StringUtils.commaDelimitedListToStringArray(candidatePattern); + bd.setAutowireCandidate(PatternMatchUtils.simpleMatch(patterns, beanName)); + } + } + else { + bd.setAutowireCandidate(TRUE_VALUE.equals(autowireCandidate)); + } + + if (ele.hasAttribute(PRIMARY_ATTRIBUTE)) { + bd.setPrimary(TRUE_VALUE.equals(ele.getAttribute(PRIMARY_ATTRIBUTE))); + } + + if (ele.hasAttribute(INIT_METHOD_ATTRIBUTE)) { + String initMethodName = ele.getAttribute(INIT_METHOD_ATTRIBUTE); + bd.setInitMethodName(initMethodName); + } + else if (this.defaults.getInitMethod() != null) { + bd.setInitMethodName(this.defaults.getInitMethod()); + bd.setEnforceInitMethod(false); + } + + if (ele.hasAttribute(DESTROY_METHOD_ATTRIBUTE)) { + String destroyMethodName = ele.getAttribute(DESTROY_METHOD_ATTRIBUTE); + bd.setDestroyMethodName(destroyMethodName); + } + else if (this.defaults.getDestroyMethod() != null) { + bd.setDestroyMethodName(this.defaults.getDestroyMethod()); + bd.setEnforceDestroyMethod(false); + } + + if (ele.hasAttribute(FACTORY_METHOD_ATTRIBUTE)) { + bd.setFactoryMethodName(ele.getAttribute(FACTORY_METHOD_ATTRIBUTE)); + } + if (ele.hasAttribute(FACTORY_BEAN_ATTRIBUTE)) { + bd.setFactoryBeanName(ele.getAttribute(FACTORY_BEAN_ATTRIBUTE)); + } + + return bd; + } + + /** + * Create a bean definition for the given class name and parent name. + * @param className the name of the bean class + * @param parentName the name of the bean's parent bean + * @return the newly created bean definition + * @throws ClassNotFoundException if bean class resolution was attempted but failed + */ + protected AbstractBeanDefinition createBeanDefinition(@Nullable String className, @Nullable String parentName) + throws ClassNotFoundException { + + return BeanDefinitionReaderUtils.createBeanDefinition( + parentName, className, this.readerContext.getBeanClassLoader()); + } + + /** + * Parse the meta elements underneath the given element, if any. + */ + public void parseMetaElements(Element ele, BeanMetadataAttributeAccessor attributeAccessor) { + NodeList nl = ele.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (isCandidateElement(node) && nodeNameEquals(node, META_ELEMENT)) { + Element metaElement = (Element) node; + String key = metaElement.getAttribute(KEY_ATTRIBUTE); + String value = metaElement.getAttribute(VALUE_ATTRIBUTE); + BeanMetadataAttribute attribute = new BeanMetadataAttribute(key, value); + attribute.setSource(extractSource(metaElement)); + attributeAccessor.addMetadataAttribute(attribute); + } + } + } + + /** + * Parse the given autowire attribute value into + * {@link AbstractBeanDefinition} autowire constants. + */ + @SuppressWarnings("deprecation") + public int getAutowireMode(String attrValue) { + String attr = attrValue; + if (isDefaultValue(attr)) { + attr = this.defaults.getAutowire(); + } + int autowire = AbstractBeanDefinition.AUTOWIRE_NO; + if (AUTOWIRE_BY_NAME_VALUE.equals(attr)) { + autowire = AbstractBeanDefinition.AUTOWIRE_BY_NAME; + } + else if (AUTOWIRE_BY_TYPE_VALUE.equals(attr)) { + autowire = AbstractBeanDefinition.AUTOWIRE_BY_TYPE; + } + else if (AUTOWIRE_CONSTRUCTOR_VALUE.equals(attr)) { + autowire = AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR; + } + else if (AUTOWIRE_AUTODETECT_VALUE.equals(attr)) { + autowire = AbstractBeanDefinition.AUTOWIRE_AUTODETECT; + } + // Else leave default value. + return autowire; + } + + /** + * Parse constructor-arg sub-elements of the given bean element. + */ + public void parseConstructorArgElements(Element beanEle, BeanDefinition bd) { + NodeList nl = beanEle.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (isCandidateElement(node) && nodeNameEquals(node, CONSTRUCTOR_ARG_ELEMENT)) { + parseConstructorArgElement((Element) node, bd); + } + } + } + + /** + * Parse property sub-elements of the given bean element. + */ + public void parsePropertyElements(Element beanEle, BeanDefinition bd) { + NodeList nl = beanEle.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (isCandidateElement(node) && nodeNameEquals(node, PROPERTY_ELEMENT)) { + parsePropertyElement((Element) node, bd); + } + } + } + + /** + * Parse qualifier sub-elements of the given bean element. + */ + public void parseQualifierElements(Element beanEle, AbstractBeanDefinition bd) { + NodeList nl = beanEle.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (isCandidateElement(node) && nodeNameEquals(node, QUALIFIER_ELEMENT)) { + parseQualifierElement((Element) node, bd); + } + } + } + + /** + * Parse lookup-override sub-elements of the given bean element. + */ + public void parseLookupOverrideSubElements(Element beanEle, MethodOverrides overrides) { + NodeList nl = beanEle.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (isCandidateElement(node) && nodeNameEquals(node, LOOKUP_METHOD_ELEMENT)) { + Element ele = (Element) node; + String methodName = ele.getAttribute(NAME_ATTRIBUTE); + String beanRef = ele.getAttribute(BEAN_ELEMENT); + LookupOverride override = new LookupOverride(methodName, beanRef); + override.setSource(extractSource(ele)); + overrides.addOverride(override); + } + } + } + + /** + * Parse replaced-method sub-elements of the given bean element. + */ + public void parseReplacedMethodSubElements(Element beanEle, MethodOverrides overrides) { + NodeList nl = beanEle.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (isCandidateElement(node) && nodeNameEquals(node, REPLACED_METHOD_ELEMENT)) { + Element replacedMethodEle = (Element) node; + String name = replacedMethodEle.getAttribute(NAME_ATTRIBUTE); + String callback = replacedMethodEle.getAttribute(REPLACER_ATTRIBUTE); + ReplaceOverride replaceOverride = new ReplaceOverride(name, callback); + // Look for arg-type match elements. + List argTypeEles = DomUtils.getChildElementsByTagName(replacedMethodEle, ARG_TYPE_ELEMENT); + for (Element argTypeEle : argTypeEles) { + String match = argTypeEle.getAttribute(ARG_TYPE_MATCH_ATTRIBUTE); + match = (StringUtils.hasText(match) ? match : DomUtils.getTextValue(argTypeEle)); + if (StringUtils.hasText(match)) { + replaceOverride.addTypeIdentifier(match); + } + } + replaceOverride.setSource(extractSource(replacedMethodEle)); + overrides.addOverride(replaceOverride); + } + } + } + + /** + * Parse a constructor-arg element. + */ + public void parseConstructorArgElement(Element ele, BeanDefinition bd) { + String indexAttr = ele.getAttribute(INDEX_ATTRIBUTE); + String typeAttr = ele.getAttribute(TYPE_ATTRIBUTE); + String nameAttr = ele.getAttribute(NAME_ATTRIBUTE); + if (StringUtils.hasLength(indexAttr)) { + try { + int index = Integer.parseInt(indexAttr); + if (index < 0) { + error("'index' cannot be lower than 0", ele); + } + else { + try { + this.parseState.push(new ConstructorArgumentEntry(index)); + Object value = parsePropertyValue(ele, bd, null); + ConstructorArgumentValues.ValueHolder valueHolder = new ConstructorArgumentValues.ValueHolder(value); + if (StringUtils.hasLength(typeAttr)) { + valueHolder.setType(typeAttr); + } + if (StringUtils.hasLength(nameAttr)) { + valueHolder.setName(nameAttr); + } + valueHolder.setSource(extractSource(ele)); + if (bd.getConstructorArgumentValues().hasIndexedArgumentValue(index)) { + error("Ambiguous constructor-arg entries for index " + index, ele); + } + else { + bd.getConstructorArgumentValues().addIndexedArgumentValue(index, valueHolder); + } + } + finally { + this.parseState.pop(); + } + } + } + catch (NumberFormatException ex) { + error("Attribute 'index' of tag 'constructor-arg' must be an integer", ele); + } + } + else { + try { + this.parseState.push(new ConstructorArgumentEntry()); + Object value = parsePropertyValue(ele, bd, null); + ConstructorArgumentValues.ValueHolder valueHolder = new ConstructorArgumentValues.ValueHolder(value); + if (StringUtils.hasLength(typeAttr)) { + valueHolder.setType(typeAttr); + } + if (StringUtils.hasLength(nameAttr)) { + valueHolder.setName(nameAttr); + } + valueHolder.setSource(extractSource(ele)); + bd.getConstructorArgumentValues().addGenericArgumentValue(valueHolder); + } + finally { + this.parseState.pop(); + } + } + } + + /** + * Parse a property element. + */ + public void parsePropertyElement(Element ele, BeanDefinition bd) { + String propertyName = ele.getAttribute(NAME_ATTRIBUTE); + if (!StringUtils.hasLength(propertyName)) { + error("Tag 'property' must have a 'name' attribute", ele); + return; + } + this.parseState.push(new PropertyEntry(propertyName)); + try { + if (bd.getPropertyValues().contains(propertyName)) { + error("Multiple 'property' definitions for property '" + propertyName + "'", ele); + return; + } + Object val = parsePropertyValue(ele, bd, propertyName); + PropertyValue pv = new PropertyValue(propertyName, val); + parseMetaElements(ele, pv); + pv.setSource(extractSource(ele)); + bd.getPropertyValues().addPropertyValue(pv); + } + finally { + this.parseState.pop(); + } + } + + /** + * Parse a qualifier element. + */ + public void parseQualifierElement(Element ele, AbstractBeanDefinition bd) { + String typeName = ele.getAttribute(TYPE_ATTRIBUTE); + if (!StringUtils.hasLength(typeName)) { + error("Tag 'qualifier' must have a 'type' attribute", ele); + return; + } + this.parseState.push(new QualifierEntry(typeName)); + try { + AutowireCandidateQualifier qualifier = new AutowireCandidateQualifier(typeName); + qualifier.setSource(extractSource(ele)); + String value = ele.getAttribute(VALUE_ATTRIBUTE); + if (StringUtils.hasLength(value)) { + qualifier.setAttribute(AutowireCandidateQualifier.VALUE_KEY, value); + } + NodeList nl = ele.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (isCandidateElement(node) && nodeNameEquals(node, QUALIFIER_ATTRIBUTE_ELEMENT)) { + Element attributeEle = (Element) node; + String attributeName = attributeEle.getAttribute(KEY_ATTRIBUTE); + String attributeValue = attributeEle.getAttribute(VALUE_ATTRIBUTE); + if (StringUtils.hasLength(attributeName) && StringUtils.hasLength(attributeValue)) { + BeanMetadataAttribute attribute = new BeanMetadataAttribute(attributeName, attributeValue); + attribute.setSource(extractSource(attributeEle)); + qualifier.addMetadataAttribute(attribute); + } + else { + error("Qualifier 'attribute' tag must have a 'name' and 'value'", attributeEle); + return; + } + } + } + bd.addQualifier(qualifier); + } + finally { + this.parseState.pop(); + } + } + + /** + * Get the value of a property element. May be a list etc. + * Also used for constructor arguments, "propertyName" being null in this case. + */ + @Nullable + public Object parsePropertyValue(Element ele, BeanDefinition bd, @Nullable String propertyName) { + String elementName = (propertyName != null ? + " element for property '" + propertyName + "'" : + " element"); + + // Should only have one child element: ref, value, list, etc. + NodeList nl = ele.getChildNodes(); + Element subElement = null; + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (node instanceof Element && !nodeNameEquals(node, DESCRIPTION_ELEMENT) && + !nodeNameEquals(node, META_ELEMENT)) { + // Child element is what we're looking for. + if (subElement != null) { + error(elementName + " must not contain more than one sub-element", ele); + } + else { + subElement = (Element) node; + } + } + } + + boolean hasRefAttribute = ele.hasAttribute(REF_ATTRIBUTE); + boolean hasValueAttribute = ele.hasAttribute(VALUE_ATTRIBUTE); + if ((hasRefAttribute && hasValueAttribute) || + ((hasRefAttribute || hasValueAttribute) && subElement != null)) { + error(elementName + + " is only allowed to contain either 'ref' attribute OR 'value' attribute OR sub-element", ele); + } + + if (hasRefAttribute) { + String refName = ele.getAttribute(REF_ATTRIBUTE); + if (!StringUtils.hasText(refName)) { + error(elementName + " contains empty 'ref' attribute", ele); + } + RuntimeBeanReference ref = new RuntimeBeanReference(refName); + ref.setSource(extractSource(ele)); + return ref; + } + else if (hasValueAttribute) { + TypedStringValue valueHolder = new TypedStringValue(ele.getAttribute(VALUE_ATTRIBUTE)); + valueHolder.setSource(extractSource(ele)); + return valueHolder; + } + else if (subElement != null) { + return parsePropertySubElement(subElement, bd); + } + else { + // Neither child element nor "ref" or "value" attribute found. + error(elementName + " must specify a ref or value", ele); + return null; + } + } + + /** + * Parse a value, ref or collection sub-element of a property or + * constructor-arg element. + * @param ele subelement of property element; we don't know which yet + * @param bd the current bean definition (if any) + */ + @Nullable + public Object parsePropertySubElement(Element ele, @Nullable BeanDefinition bd) { + return parsePropertySubElement(ele, bd, null); + } + + /** + * Parse a value, ref or collection sub-element of a property or + * constructor-arg element. + * @param ele subelement of property element; we don't know which yet + * @param bd the current bean definition (if any) + * @param defaultValueType the default type (class name) for any + * {@code } tag that might be created + */ + @Nullable + public Object parsePropertySubElement(Element ele, @Nullable BeanDefinition bd, @Nullable String defaultValueType) { + if (!isDefaultNamespace(ele)) { + return parseNestedCustomElement(ele, bd); + } + else if (nodeNameEquals(ele, BEAN_ELEMENT)) { + BeanDefinitionHolder nestedBd = parseBeanDefinitionElement(ele, bd); + if (nestedBd != null) { + nestedBd = decorateBeanDefinitionIfRequired(ele, nestedBd, bd); + } + return nestedBd; + } + else if (nodeNameEquals(ele, REF_ELEMENT)) { + // A generic reference to any name of any bean. + String refName = ele.getAttribute(BEAN_REF_ATTRIBUTE); + boolean toParent = false; + if (!StringUtils.hasLength(refName)) { + // A reference to the id of another bean in a parent context. + refName = ele.getAttribute(PARENT_REF_ATTRIBUTE); + toParent = true; + if (!StringUtils.hasLength(refName)) { + error("'bean' or 'parent' is required for element", ele); + return null; + } + } + if (!StringUtils.hasText(refName)) { + error(" element contains empty target attribute", ele); + return null; + } + RuntimeBeanReference ref = new RuntimeBeanReference(refName, toParent); + ref.setSource(extractSource(ele)); + return ref; + } + else if (nodeNameEquals(ele, IDREF_ELEMENT)) { + return parseIdRefElement(ele); + } + else if (nodeNameEquals(ele, VALUE_ELEMENT)) { + return parseValueElement(ele, defaultValueType); + } + else if (nodeNameEquals(ele, NULL_ELEMENT)) { + // It's a distinguished null value. Let's wrap it in a TypedStringValue + // object in order to preserve the source location. + TypedStringValue nullHolder = new TypedStringValue(null); + nullHolder.setSource(extractSource(ele)); + return nullHolder; + } + else if (nodeNameEquals(ele, ARRAY_ELEMENT)) { + return parseArrayElement(ele, bd); + } + else if (nodeNameEquals(ele, LIST_ELEMENT)) { + return parseListElement(ele, bd); + } + else if (nodeNameEquals(ele, SET_ELEMENT)) { + return parseSetElement(ele, bd); + } + else if (nodeNameEquals(ele, MAP_ELEMENT)) { + return parseMapElement(ele, bd); + } + else if (nodeNameEquals(ele, PROPS_ELEMENT)) { + return parsePropsElement(ele); + } + else { + error("Unknown property sub-element: [" + ele.getNodeName() + "]", ele); + return null; + } + } + + /** + * Return a typed String value Object for the given 'idref' element. + */ + @Nullable + public Object parseIdRefElement(Element ele) { + // A generic reference to any name of any bean. + String refName = ele.getAttribute(BEAN_REF_ATTRIBUTE); + if (!StringUtils.hasLength(refName)) { + error("'bean' is required for element", ele); + return null; + } + if (!StringUtils.hasText(refName)) { + error(" element contains empty target attribute", ele); + return null; + } + RuntimeBeanNameReference ref = new RuntimeBeanNameReference(refName); + ref.setSource(extractSource(ele)); + return ref; + } + + /** + * Return a typed String value Object for the given value element. + */ + public Object parseValueElement(Element ele, @Nullable String defaultTypeName) { + // It's a literal value. + String value = DomUtils.getTextValue(ele); + String specifiedTypeName = ele.getAttribute(TYPE_ATTRIBUTE); + String typeName = specifiedTypeName; + if (!StringUtils.hasText(typeName)) { + typeName = defaultTypeName; + } + try { + TypedStringValue typedValue = buildTypedStringValue(value, typeName); + typedValue.setSource(extractSource(ele)); + typedValue.setSpecifiedTypeName(specifiedTypeName); + return typedValue; + } + catch (ClassNotFoundException ex) { + error("Type class [" + typeName + "] not found for element", ele, ex); + return value; + } + } + + /** + * Build a typed String value Object for the given raw value. + * @see org.springframework.beans.factory.config.TypedStringValue + */ + protected TypedStringValue buildTypedStringValue(String value, @Nullable String targetTypeName) + throws ClassNotFoundException { + + ClassLoader classLoader = this.readerContext.getBeanClassLoader(); + TypedStringValue typedValue; + if (!StringUtils.hasText(targetTypeName)) { + typedValue = new TypedStringValue(value); + } + else if (classLoader != null) { + Class targetType = ClassUtils.forName(targetTypeName, classLoader); + typedValue = new TypedStringValue(value, targetType); + } + else { + typedValue = new TypedStringValue(value, targetTypeName); + } + return typedValue; + } + + /** + * Parse an array element. + */ + public Object parseArrayElement(Element arrayEle, @Nullable BeanDefinition bd) { + String elementType = arrayEle.getAttribute(VALUE_TYPE_ATTRIBUTE); + NodeList nl = arrayEle.getChildNodes(); + ManagedArray target = new ManagedArray(elementType, nl.getLength()); + target.setSource(extractSource(arrayEle)); + target.setElementTypeName(elementType); + target.setMergeEnabled(parseMergeAttribute(arrayEle)); + parseCollectionElements(nl, target, bd, elementType); + return target; + } + + /** + * Parse a list element. + */ + public List parseListElement(Element collectionEle, @Nullable BeanDefinition bd) { + String defaultElementType = collectionEle.getAttribute(VALUE_TYPE_ATTRIBUTE); + NodeList nl = collectionEle.getChildNodes(); + ManagedList target = new ManagedList<>(nl.getLength()); + target.setSource(extractSource(collectionEle)); + target.setElementTypeName(defaultElementType); + target.setMergeEnabled(parseMergeAttribute(collectionEle)); + parseCollectionElements(nl, target, bd, defaultElementType); + return target; + } + + /** + * Parse a set element. + */ + public Set parseSetElement(Element collectionEle, @Nullable BeanDefinition bd) { + String defaultElementType = collectionEle.getAttribute(VALUE_TYPE_ATTRIBUTE); + NodeList nl = collectionEle.getChildNodes(); + ManagedSet target = new ManagedSet<>(nl.getLength()); + target.setSource(extractSource(collectionEle)); + target.setElementTypeName(defaultElementType); + target.setMergeEnabled(parseMergeAttribute(collectionEle)); + parseCollectionElements(nl, target, bd, defaultElementType); + return target; + } + + protected void parseCollectionElements( + NodeList elementNodes, Collection target, @Nullable BeanDefinition bd, String defaultElementType) { + + for (int i = 0; i < elementNodes.getLength(); i++) { + Node node = elementNodes.item(i); + if (node instanceof Element && !nodeNameEquals(node, DESCRIPTION_ELEMENT)) { + target.add(parsePropertySubElement((Element) node, bd, defaultElementType)); + } + } + } + + /** + * Parse a map element. + */ + public Map parseMapElement(Element mapEle, @Nullable BeanDefinition bd) { + String defaultKeyType = mapEle.getAttribute(KEY_TYPE_ATTRIBUTE); + String defaultValueType = mapEle.getAttribute(VALUE_TYPE_ATTRIBUTE); + + List entryEles = DomUtils.getChildElementsByTagName(mapEle, ENTRY_ELEMENT); + ManagedMap map = new ManagedMap<>(entryEles.size()); + map.setSource(extractSource(mapEle)); + map.setKeyTypeName(defaultKeyType); + map.setValueTypeName(defaultValueType); + map.setMergeEnabled(parseMergeAttribute(mapEle)); + + for (Element entryEle : entryEles) { + // Should only have one value child element: ref, value, list, etc. + // Optionally, there might be a key child element. + NodeList entrySubNodes = entryEle.getChildNodes(); + Element keyEle = null; + Element valueEle = null; + for (int j = 0; j < entrySubNodes.getLength(); j++) { + Node node = entrySubNodes.item(j); + if (node instanceof Element) { + Element candidateEle = (Element) node; + if (nodeNameEquals(candidateEle, KEY_ELEMENT)) { + if (keyEle != null) { + error(" element is only allowed to contain one sub-element", entryEle); + } + else { + keyEle = candidateEle; + } + } + else { + // Child element is what we're looking for. + if (nodeNameEquals(candidateEle, DESCRIPTION_ELEMENT)) { + // the element is a -> ignore it + } + else if (valueEle != null) { + error(" element must not contain more than one value sub-element", entryEle); + } + else { + valueEle = candidateEle; + } + } + } + } + + // Extract key from attribute or sub-element. + Object key = null; + boolean hasKeyAttribute = entryEle.hasAttribute(KEY_ATTRIBUTE); + boolean hasKeyRefAttribute = entryEle.hasAttribute(KEY_REF_ATTRIBUTE); + if ((hasKeyAttribute && hasKeyRefAttribute) || + (hasKeyAttribute || hasKeyRefAttribute) && keyEle != null) { + error(" element is only allowed to contain either " + + "a 'key' attribute OR a 'key-ref' attribute OR a sub-element", entryEle); + } + if (hasKeyAttribute) { + key = buildTypedStringValueForMap(entryEle.getAttribute(KEY_ATTRIBUTE), defaultKeyType, entryEle); + } + else if (hasKeyRefAttribute) { + String refName = entryEle.getAttribute(KEY_REF_ATTRIBUTE); + if (!StringUtils.hasText(refName)) { + error(" element contains empty 'key-ref' attribute", entryEle); + } + RuntimeBeanReference ref = new RuntimeBeanReference(refName); + ref.setSource(extractSource(entryEle)); + key = ref; + } + else if (keyEle != null) { + key = parseKeyElement(keyEle, bd, defaultKeyType); + } + else { + error(" element must specify a key", entryEle); + } + + // Extract value from attribute or sub-element. + Object value = null; + boolean hasValueAttribute = entryEle.hasAttribute(VALUE_ATTRIBUTE); + boolean hasValueRefAttribute = entryEle.hasAttribute(VALUE_REF_ATTRIBUTE); + boolean hasValueTypeAttribute = entryEle.hasAttribute(VALUE_TYPE_ATTRIBUTE); + if ((hasValueAttribute && hasValueRefAttribute) || + (hasValueAttribute || hasValueRefAttribute) && valueEle != null) { + error(" element is only allowed to contain either " + + "'value' attribute OR 'value-ref' attribute OR sub-element", entryEle); + } + if ((hasValueTypeAttribute && hasValueRefAttribute) || + (hasValueTypeAttribute && !hasValueAttribute) || + (hasValueTypeAttribute && valueEle != null)) { + error(" element is only allowed to contain a 'value-type' " + + "attribute when it has a 'value' attribute", entryEle); + } + if (hasValueAttribute) { + String valueType = entryEle.getAttribute(VALUE_TYPE_ATTRIBUTE); + if (!StringUtils.hasText(valueType)) { + valueType = defaultValueType; + } + value = buildTypedStringValueForMap(entryEle.getAttribute(VALUE_ATTRIBUTE), valueType, entryEle); + } + else if (hasValueRefAttribute) { + String refName = entryEle.getAttribute(VALUE_REF_ATTRIBUTE); + if (!StringUtils.hasText(refName)) { + error(" element contains empty 'value-ref' attribute", entryEle); + } + RuntimeBeanReference ref = new RuntimeBeanReference(refName); + ref.setSource(extractSource(entryEle)); + value = ref; + } + else if (valueEle != null) { + value = parsePropertySubElement(valueEle, bd, defaultValueType); + } + else { + error(" element must specify a value", entryEle); + } + + // Add final key and value to the Map. + map.put(key, value); + } + + return map; + } + + /** + * Build a typed String value Object for the given raw value. + * @see org.springframework.beans.factory.config.TypedStringValue + */ + protected final Object buildTypedStringValueForMap(String value, String defaultTypeName, Element entryEle) { + try { + TypedStringValue typedValue = buildTypedStringValue(value, defaultTypeName); + typedValue.setSource(extractSource(entryEle)); + return typedValue; + } + catch (ClassNotFoundException ex) { + error("Type class [" + defaultTypeName + "] not found for Map key/value type", entryEle, ex); + return value; + } + } + + /** + * Parse a key sub-element of a map element. + */ + @Nullable + protected Object parseKeyElement(Element keyEle, @Nullable BeanDefinition bd, String defaultKeyTypeName) { + NodeList nl = keyEle.getChildNodes(); + Element subElement = null; + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (node instanceof Element) { + // Child element is what we're looking for. + if (subElement != null) { + error(" element must not contain more than one value sub-element", keyEle); + } + else { + subElement = (Element) node; + } + } + } + if (subElement == null) { + return null; + } + return parsePropertySubElement(subElement, bd, defaultKeyTypeName); + } + + /** + * Parse a props element. + */ + public Properties parsePropsElement(Element propsEle) { + ManagedProperties props = new ManagedProperties(); + props.setSource(extractSource(propsEle)); + props.setMergeEnabled(parseMergeAttribute(propsEle)); + + List propEles = DomUtils.getChildElementsByTagName(propsEle, PROP_ELEMENT); + for (Element propEle : propEles) { + String key = propEle.getAttribute(KEY_ATTRIBUTE); + // Trim the text value to avoid unwanted whitespace + // caused by typical XML formatting. + String value = DomUtils.getTextValue(propEle).trim(); + TypedStringValue keyHolder = new TypedStringValue(key); + keyHolder.setSource(extractSource(propEle)); + TypedStringValue valueHolder = new TypedStringValue(value); + valueHolder.setSource(extractSource(propEle)); + props.put(keyHolder, valueHolder); + } + + return props; + } + + /** + * Parse the merge attribute of a collection element, if any. + */ + public boolean parseMergeAttribute(Element collectionElement) { + String value = collectionElement.getAttribute(MERGE_ATTRIBUTE); + if (isDefaultValue(value)) { + value = this.defaults.getMerge(); + } + return TRUE_VALUE.equals(value); + } + + /** + * Parse a custom element (outside of the default namespace). + * @param ele the element to parse + * @return the resulting bean definition + */ + @Nullable + public BeanDefinition parseCustomElement(Element ele) { + return parseCustomElement(ele, null); + } + + /** + * Parse a custom element (outside of the default namespace). + * @param ele the element to parse + * @param containingBd the containing bean definition (if any) + * @return the resulting bean definition + */ + @Nullable + public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) { + String namespaceUri = getNamespaceURI(ele); + if (namespaceUri == null) { + return null; + } + NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri); + if (handler == null) { + error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele); + return null; + } + return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd)); + } + + /** + * Decorate the given bean definition through a namespace handler, if applicable. + * @param ele the current element + * @param originalDef the current bean definition + * @return the decorated bean definition + */ + public BeanDefinitionHolder decorateBeanDefinitionIfRequired(Element ele, BeanDefinitionHolder originalDef) { + return decorateBeanDefinitionIfRequired(ele, originalDef, null); + } + + /** + * Decorate the given bean definition through a namespace handler, if applicable. + * @param ele the current element + * @param originalDef the current bean definition + * @param containingBd the containing bean definition (if any) + * @return the decorated bean definition + */ + public BeanDefinitionHolder decorateBeanDefinitionIfRequired( + Element ele, BeanDefinitionHolder originalDef, @Nullable BeanDefinition containingBd) { + + BeanDefinitionHolder finalDefinition = originalDef; + + // Decorate based on custom attributes first. + NamedNodeMap attributes = ele.getAttributes(); + for (int i = 0; i < attributes.getLength(); i++) { + Node node = attributes.item(i); + finalDefinition = decorateIfRequired(node, finalDefinition, containingBd); + } + + // Decorate based on custom nested elements. + NodeList children = ele.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node node = children.item(i); + if (node.getNodeType() == Node.ELEMENT_NODE) { + finalDefinition = decorateIfRequired(node, finalDefinition, containingBd); + } + } + return finalDefinition; + } + + /** + * Decorate the given bean definition through a namespace handler, + * if applicable. + * @param node the current child node + * @param originalDef the current bean definition + * @param containingBd the containing bean definition (if any) + * @return the decorated bean definition + */ + public BeanDefinitionHolder decorateIfRequired( + Node node, BeanDefinitionHolder originalDef, @Nullable BeanDefinition containingBd) { + + String namespaceUri = getNamespaceURI(node); + if (namespaceUri != null && !isDefaultNamespace(namespaceUri)) { + NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri); + if (handler != null) { + BeanDefinitionHolder decorated = + handler.decorate(node, originalDef, new ParserContext(this.readerContext, this, containingBd)); + if (decorated != null) { + return decorated; + } + } + else if (namespaceUri.startsWith("http://www.springframework.org/schema/")) { + error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", node); + } + else { + // A custom namespace, not to be handled by Spring - maybe "xml:...". + if (logger.isDebugEnabled()) { + logger.debug("No Spring NamespaceHandler found for XML schema namespace [" + namespaceUri + "]"); + } + } + } + return originalDef; + } + + @Nullable + private BeanDefinitionHolder parseNestedCustomElement(Element ele, @Nullable BeanDefinition containingBd) { + BeanDefinition innerDefinition = parseCustomElement(ele, containingBd); + if (innerDefinition == null) { + error("Incorrect usage of element '" + ele.getNodeName() + "' in a nested manner. " + + "This tag cannot be used nested inside .", ele); + return null; + } + String id = ele.getNodeName() + BeanDefinitionReaderUtils.GENERATED_BEAN_NAME_SEPARATOR + + ObjectUtils.getIdentityHexString(innerDefinition); + if (logger.isTraceEnabled()) { + logger.trace("Using generated bean name [" + id + + "] for nested custom element '" + ele.getNodeName() + "'"); + } + return new BeanDefinitionHolder(innerDefinition, id); + } + + + /** + * Get the namespace URI for the supplied node. + *

    The default implementation uses {@link Node#getNamespaceURI}. + * Subclasses may override the default implementation to provide a + * different namespace identification mechanism. + * @param node the node + */ + @Nullable + public String getNamespaceURI(Node node) { + return node.getNamespaceURI(); + } + + /** + * Get the local name for the supplied {@link Node}. + *

    The default implementation calls {@link Node#getLocalName}. + * Subclasses may override the default implementation to provide a + * different mechanism for getting the local name. + * @param node the {@code Node} + */ + public String getLocalName(Node node) { + return node.getLocalName(); + } + + /** + * Determine whether the name of the supplied node is equal to the supplied name. + *

    The default implementation checks the supplied desired name against both + * {@link Node#getNodeName()} and {@link Node#getLocalName()}. + *

    Subclasses may override the default implementation to provide a different + * mechanism for comparing node names. + * @param node the node to compare + * @param desiredName the name to check for + */ + public boolean nodeNameEquals(Node node, String desiredName) { + return desiredName.equals(node.getNodeName()) || desiredName.equals(getLocalName(node)); + } + + /** + * Determine whether the given URI indicates the default namespace. + */ + public boolean isDefaultNamespace(@Nullable String namespaceUri) { + return !StringUtils.hasLength(namespaceUri) || BEANS_NAMESPACE_URI.equals(namespaceUri); + } + + /** + * Determine whether the given node indicates the default namespace. + */ + public boolean isDefaultNamespace(Node node) { + return isDefaultNamespace(getNamespaceURI(node)); + } + + private boolean isDefaultValue(String value) { + return !StringUtils.hasLength(value) || DEFAULT_VALUE.equals(value); + } + + private boolean isCandidateElement(Node node) { + return (node instanceof Element && (isDefaultNamespace(node) || !isDefaultNamespace(node.getParentNode()))); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeansDtdResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeansDtdResolver.java new file mode 100644 index 0000000..16496d3 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeansDtdResolver.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import java.io.FileNotFoundException; +import java.io.IOException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.xml.sax.EntityResolver; +import org.xml.sax.InputSource; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; + +/** + * {@link EntityResolver} implementation for the Spring beans DTD, + * to load the DTD from the Spring class path (or JAR file). + * + *

    Fetches "spring-beans.dtd" from the class path resource + * "/org/springframework/beans/factory/xml/spring-beans.dtd", + * no matter whether specified as some local URL that includes "spring-beans" + * in the DTD name or as "https://www.springframework.org/dtd/spring-beans-2.0.dtd". + * + * @author Juergen Hoeller + * @author Colin Sampaleanu + * @since 04.06.2003 + * @see ResourceEntityResolver + */ +public class BeansDtdResolver implements EntityResolver { + + private static final String DTD_EXTENSION = ".dtd"; + + private static final String DTD_NAME = "spring-beans"; + + private static final Log logger = LogFactory.getLog(BeansDtdResolver.class); + + + @Override + @Nullable + public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) throws IOException { + if (logger.isTraceEnabled()) { + logger.trace("Trying to resolve XML entity with public ID [" + publicId + + "] and system ID [" + systemId + "]"); + } + + if (systemId != null && systemId.endsWith(DTD_EXTENSION)) { + int lastPathSeparator = systemId.lastIndexOf('/'); + int dtdNameStart = systemId.indexOf(DTD_NAME, lastPathSeparator); + if (dtdNameStart != -1) { + String dtdFile = DTD_NAME + DTD_EXTENSION; + if (logger.isTraceEnabled()) { + logger.trace("Trying to locate [" + dtdFile + "] in Spring jar on classpath"); + } + try { + Resource resource = new ClassPathResource(dtdFile, getClass()); + InputSource source = new InputSource(resource.getInputStream()); + source.setPublicId(publicId); + source.setSystemId(systemId); + if (logger.isTraceEnabled()) { + logger.trace("Found beans DTD [" + systemId + "] in classpath: " + dtdFile); + } + return source; + } + catch (FileNotFoundException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Could not resolve beans DTD [" + systemId + "]: not found in classpath", ex); + } + } + } + } + + // Fall back to the parser's default behavior. + return null; + } + + + @Override + public String toString() { + return "EntityResolver for spring-beans DTD"; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultBeanDefinitionDocumentReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultBeanDefinitionDocumentReader.java new file mode 100644 index 0000000..af5026d --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultBeanDefinitionDocumentReader.java @@ -0,0 +1,349 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.ResourcePatternUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ResourceUtils; +import org.springframework.util.StringUtils; + +/** + * Default implementation of the {@link BeanDefinitionDocumentReader} interface that + * reads bean definitions according to the "spring-beans" DTD and XSD format + * (Spring's default XML bean definition format). + * + *

    The structure, elements, and attribute names of the required XML document + * are hard-coded in this class. (Of course a transform could be run if necessary + * to produce this format). {@code } does not need to be the root + * element of the XML document: this class will parse all bean definition elements + * in the XML file, regardless of the actual root element. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Rob Harrop + * @author Erik Wiersma + * @since 18.12.2003 + */ +public class DefaultBeanDefinitionDocumentReader implements BeanDefinitionDocumentReader { + + public static final String BEAN_ELEMENT = BeanDefinitionParserDelegate.BEAN_ELEMENT; + + public static final String NESTED_BEANS_ELEMENT = "beans"; + + public static final String ALIAS_ELEMENT = "alias"; + + public static final String NAME_ATTRIBUTE = "name"; + + public static final String ALIAS_ATTRIBUTE = "alias"; + + public static final String IMPORT_ELEMENT = "import"; + + public static final String RESOURCE_ATTRIBUTE = "resource"; + + public static final String PROFILE_ATTRIBUTE = "profile"; + + + protected final Log logger = LogFactory.getLog(getClass()); + + @Nullable + private XmlReaderContext readerContext; + + @Nullable + private BeanDefinitionParserDelegate delegate; + + + /** + * This implementation parses bean definitions according to the "spring-beans" XSD + * (or DTD, historically). + *

    Opens a DOM Document; then initializes the default settings + * specified at the {@code } level; then parses the contained bean definitions. + */ + @Override + public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) { + this.readerContext = readerContext; + doRegisterBeanDefinitions(doc.getDocumentElement()); + } + + /** + * Return the descriptor for the XML resource that this parser works on. + */ + protected final XmlReaderContext getReaderContext() { + Assert.state(this.readerContext != null, "No XmlReaderContext available"); + return this.readerContext; + } + + /** + * Invoke the {@link org.springframework.beans.factory.parsing.SourceExtractor} + * to pull the source metadata from the supplied {@link Element}. + */ + @Nullable + protected Object extractSource(Element ele) { + return getReaderContext().extractSource(ele); + } + + + /** + * Register each bean definition within the given root {@code } element. + */ + @SuppressWarnings("deprecation") // for Environment.acceptsProfiles(String...) + protected void doRegisterBeanDefinitions(Element root) { + // Any nested elements will cause recursion in this method. In + // order to propagate and preserve default-* attributes correctly, + // keep track of the current (parent) delegate, which may be null. Create + // the new (child) delegate with a reference to the parent for fallback purposes, + // then ultimately reset this.delegate back to its original (parent) reference. + // this behavior emulates a stack of delegates without actually necessitating one. + BeanDefinitionParserDelegate parent = this.delegate; + this.delegate = createDelegate(getReaderContext(), root, parent); + + if (this.delegate.isDefaultNamespace(root)) { + String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE); + if (StringUtils.hasText(profileSpec)) { + String[] specifiedProfiles = StringUtils.tokenizeToStringArray( + profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS); + // We cannot use Profiles.of(...) since profile expressions are not supported + // in XML config. See SPR-12458 for details. + if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) { + if (logger.isDebugEnabled()) { + logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec + + "] not matching: " + getReaderContext().getResource()); + } + return; + } + } + } + + preProcessXml(root); + parseBeanDefinitions(root, this.delegate); + postProcessXml(root); + + this.delegate = parent; + } + + protected BeanDefinitionParserDelegate createDelegate( + XmlReaderContext readerContext, Element root, @Nullable BeanDefinitionParserDelegate parentDelegate) { + + BeanDefinitionParserDelegate delegate = new BeanDefinitionParserDelegate(readerContext); + delegate.initDefaults(root, parentDelegate); + return delegate; + } + + /** + * Parse the elements at the root level in the document: + * "import", "alias", "bean". + * @param root the DOM root element of the document + */ + protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) { + if (delegate.isDefaultNamespace(root)) { + NodeList nl = root.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (node instanceof Element) { + Element ele = (Element) node; + if (delegate.isDefaultNamespace(ele)) { + parseDefaultElement(ele, delegate); + } + else { + delegate.parseCustomElement(ele); + } + } + } + } + else { + delegate.parseCustomElement(root); + } + } + + private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) { + if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) { + importBeanDefinitionResource(ele); + } + else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) { + processAliasRegistration(ele); + } + else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) { + processBeanDefinition(ele, delegate); + } + else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) { + // recurse + doRegisterBeanDefinitions(ele); + } + } + + /** + * Parse an "import" element and load the bean definitions + * from the given resource into the bean factory. + */ + protected void importBeanDefinitionResource(Element ele) { + String location = ele.getAttribute(RESOURCE_ATTRIBUTE); + if (!StringUtils.hasText(location)) { + getReaderContext().error("Resource location must not be empty", ele); + return; + } + + // Resolve system properties: e.g. "${user.dir}" + location = getReaderContext().getEnvironment().resolveRequiredPlaceholders(location); + + Set actualResources = new LinkedHashSet<>(4); + + // Discover whether the location is an absolute or relative URI + boolean absoluteLocation = false; + try { + absoluteLocation = ResourcePatternUtils.isUrl(location) || ResourceUtils.toURI(location).isAbsolute(); + } + catch (URISyntaxException ex) { + // cannot convert to an URI, considering the location relative + // unless it is the well-known Spring prefix "classpath*:" + } + + // Absolute or relative? + if (absoluteLocation) { + try { + int importCount = getReaderContext().getReader().loadBeanDefinitions(location, actualResources); + if (logger.isTraceEnabled()) { + logger.trace("Imported " + importCount + " bean definitions from URL location [" + location + "]"); + } + } + catch (BeanDefinitionStoreException ex) { + getReaderContext().error( + "Failed to import bean definitions from URL location [" + location + "]", ele, ex); + } + } + else { + // No URL -> considering resource location as relative to the current file. + try { + int importCount; + Resource relativeResource = getReaderContext().getResource().createRelative(location); + if (relativeResource.exists()) { + importCount = getReaderContext().getReader().loadBeanDefinitions(relativeResource); + actualResources.add(relativeResource); + } + else { + String baseLocation = getReaderContext().getResource().getURL().toString(); + importCount = getReaderContext().getReader().loadBeanDefinitions( + StringUtils.applyRelativePath(baseLocation, location), actualResources); + } + if (logger.isTraceEnabled()) { + logger.trace("Imported " + importCount + " bean definitions from relative location [" + location + "]"); + } + } + catch (IOException ex) { + getReaderContext().error("Failed to resolve current resource location", ele, ex); + } + catch (BeanDefinitionStoreException ex) { + getReaderContext().error( + "Failed to import bean definitions from relative location [" + location + "]", ele, ex); + } + } + Resource[] actResArray = actualResources.toArray(new Resource[0]); + getReaderContext().fireImportProcessed(location, actResArray, extractSource(ele)); + } + + /** + * Process the given alias element, registering the alias with the registry. + */ + protected void processAliasRegistration(Element ele) { + String name = ele.getAttribute(NAME_ATTRIBUTE); + String alias = ele.getAttribute(ALIAS_ATTRIBUTE); + boolean valid = true; + if (!StringUtils.hasText(name)) { + getReaderContext().error("Name must not be empty", ele); + valid = false; + } + if (!StringUtils.hasText(alias)) { + getReaderContext().error("Alias must not be empty", ele); + valid = false; + } + if (valid) { + try { + getReaderContext().getRegistry().registerAlias(name, alias); + } + catch (Exception ex) { + getReaderContext().error("Failed to register alias '" + alias + + "' for bean with name '" + name + "'", ele, ex); + } + getReaderContext().fireAliasRegistered(name, alias, extractSource(ele)); + } + } + + /** + * Process the given bean element, parsing the bean definition + * and registering it with the registry. + */ + protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) { + BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele); + if (bdHolder != null) { + bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder); + try { + // Register the final decorated instance. + BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry()); + } + catch (BeanDefinitionStoreException ex) { + getReaderContext().error("Failed to register bean definition with name '" + + bdHolder.getBeanName() + "'", ele, ex); + } + // Send registration event. + getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder)); + } + } + + + /** + * Allow the XML to be extensible by processing any custom element types first, + * before we start to process the bean definitions. This method is a natural + * extension point for any other custom pre-processing of the XML. + *

    The default implementation is empty. Subclasses can override this method to + * convert custom elements into standard Spring bean definitions, for example. + * Implementors have access to the parser's bean definition reader and the + * underlying XML resource, through the corresponding accessors. + * @see #getReaderContext() + */ + protected void preProcessXml(Element root) { + } + + /** + * Allow the XML to be extensible by processing any custom element types last, + * after we finished processing the bean definitions. This method is a natural + * extension point for any other custom post-processing of the XML. + *

    The default implementation is empty. Subclasses can override this method to + * convert custom elements into standard Spring bean definitions, for example. + * Implementors have access to the parser's bean definition reader and the + * underlying XML resource, through the corresponding accessors. + * @see #getReaderContext() + */ + protected void postProcessXml(Element root) { + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultDocumentLoader.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultDocumentLoader.java new file mode 100644 index 0000000..a443e7b --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultDocumentLoader.java @@ -0,0 +1,141 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.w3c.dom.Document; +import org.xml.sax.EntityResolver; +import org.xml.sax.ErrorHandler; +import org.xml.sax.InputSource; + +import org.springframework.lang.Nullable; +import org.springframework.util.xml.XmlValidationModeDetector; + +/** + * Spring's default {@link DocumentLoader} implementation. + * + *

    Simply loads {@link Document documents} using the standard JAXP-configured + * XML parser. If you want to change the {@link DocumentBuilder} that is used to + * load documents, then one strategy is to define a corresponding Java system property + * when starting your JVM. For example, to use the Oracle {@link DocumentBuilder}, + * you might start your application like as follows: + * + *

    java -Djavax.xml.parsers.DocumentBuilderFactory=oracle.xml.jaxp.JXDocumentBuilderFactory MyMainClass
    + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +public class DefaultDocumentLoader implements DocumentLoader { + + /** + * JAXP attribute used to configure the schema language for validation. + */ + private static final String SCHEMA_LANGUAGE_ATTRIBUTE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage"; + + /** + * JAXP attribute value indicating the XSD schema language. + */ + private static final String XSD_SCHEMA_LANGUAGE = "http://www.w3.org/2001/XMLSchema"; + + + private static final Log logger = LogFactory.getLog(DefaultDocumentLoader.class); + + + /** + * Load the {@link Document} at the supplied {@link InputSource} using the standard JAXP-configured + * XML parser. + */ + @Override + public Document loadDocument(InputSource inputSource, EntityResolver entityResolver, + ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception { + + DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware); + if (logger.isTraceEnabled()) { + logger.trace("Using JAXP provider [" + factory.getClass().getName() + "]"); + } + DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler); + return builder.parse(inputSource); + } + + /** + * Create the {@link DocumentBuilderFactory} instance. + * @param validationMode the type of validation: {@link XmlValidationModeDetector#VALIDATION_DTD DTD} + * or {@link XmlValidationModeDetector#VALIDATION_XSD XSD}) + * @param namespaceAware whether the returned factory is to provide support for XML namespaces + * @return the JAXP DocumentBuilderFactory + * @throws ParserConfigurationException if we failed to build a proper DocumentBuilderFactory + */ + protected DocumentBuilderFactory createDocumentBuilderFactory(int validationMode, boolean namespaceAware) + throws ParserConfigurationException { + + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(namespaceAware); + + if (validationMode != XmlValidationModeDetector.VALIDATION_NONE) { + factory.setValidating(true); + if (validationMode == XmlValidationModeDetector.VALIDATION_XSD) { + // Enforce namespace aware for XSD... + factory.setNamespaceAware(true); + try { + factory.setAttribute(SCHEMA_LANGUAGE_ATTRIBUTE, XSD_SCHEMA_LANGUAGE); + } + catch (IllegalArgumentException ex) { + ParserConfigurationException pcex = new ParserConfigurationException( + "Unable to validate using XSD: Your JAXP provider [" + factory + + "] does not support XML Schema. Are you running on Java 1.4 with Apache Crimson? " + + "Upgrade to Apache Xerces (or Java 1.5) for full XSD support."); + pcex.initCause(ex); + throw pcex; + } + } + } + + return factory; + } + + /** + * Create a JAXP DocumentBuilder that this bean definition reader + * will use for parsing XML documents. Can be overridden in subclasses, + * adding further initialization of the builder. + * @param factory the JAXP DocumentBuilderFactory that the DocumentBuilder + * should be created with + * @param entityResolver the SAX EntityResolver to use + * @param errorHandler the SAX ErrorHandler to use + * @return the JAXP DocumentBuilder + * @throws ParserConfigurationException if thrown by JAXP methods + */ + protected DocumentBuilder createDocumentBuilder(DocumentBuilderFactory factory, + @Nullable EntityResolver entityResolver, @Nullable ErrorHandler errorHandler) + throws ParserConfigurationException { + + DocumentBuilder docBuilder = factory.newDocumentBuilder(); + if (entityResolver != null) { + docBuilder.setEntityResolver(entityResolver); + } + if (errorHandler != null) { + docBuilder.setErrorHandler(errorHandler); + } + return docBuilder; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultNamespaceHandlerResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultNamespaceHandlerResolver.java new file mode 100644 index 0000000..6dfda38 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultNamespaceHandlerResolver.java @@ -0,0 +1,188 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import java.io.IOException; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.FatalBeanException; +import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; + +/** + * Default implementation of the {@link NamespaceHandlerResolver} interface. + * Resolves namespace URIs to implementation classes based on the mappings + * contained in mapping file. + * + *

    By default, this implementation looks for the mapping file at + * {@code META-INF/spring.handlers}, but this can be changed using the + * {@link #DefaultNamespaceHandlerResolver(ClassLoader, String)} constructor. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + * @see NamespaceHandler + * @see DefaultBeanDefinitionDocumentReader + */ +public class DefaultNamespaceHandlerResolver implements NamespaceHandlerResolver { + + /** + * The location to look for the mapping files. Can be present in multiple JAR files. + */ + public static final String DEFAULT_HANDLER_MAPPINGS_LOCATION = "META-INF/spring.handlers"; + + + /** Logger available to subclasses. */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** ClassLoader to use for NamespaceHandler classes. */ + @Nullable + private final ClassLoader classLoader; + + /** Resource location to search for. */ + private final String handlerMappingsLocation; + + /** Stores the mappings from namespace URI to NamespaceHandler class name / instance. */ + @Nullable + private volatile Map handlerMappings; + + + /** + * Create a new {@code DefaultNamespaceHandlerResolver} using the + * default mapping file location. + *

    This constructor will result in the thread context ClassLoader being used + * to load resources. + * @see #DEFAULT_HANDLER_MAPPINGS_LOCATION + */ + public DefaultNamespaceHandlerResolver() { + this(null, DEFAULT_HANDLER_MAPPINGS_LOCATION); + } + + /** + * Create a new {@code DefaultNamespaceHandlerResolver} using the + * default mapping file location. + * @param classLoader the {@link ClassLoader} instance used to load mapping resources + * (may be {@code null}, in which case the thread context ClassLoader will be used) + * @see #DEFAULT_HANDLER_MAPPINGS_LOCATION + */ + public DefaultNamespaceHandlerResolver(@Nullable ClassLoader classLoader) { + this(classLoader, DEFAULT_HANDLER_MAPPINGS_LOCATION); + } + + /** + * Create a new {@code DefaultNamespaceHandlerResolver} using the + * supplied mapping file location. + * @param classLoader the {@link ClassLoader} instance used to load mapping resources + * may be {@code null}, in which case the thread context ClassLoader will be used) + * @param handlerMappingsLocation the mapping file location + */ + public DefaultNamespaceHandlerResolver(@Nullable ClassLoader classLoader, String handlerMappingsLocation) { + Assert.notNull(handlerMappingsLocation, "Handler mappings location must not be null"); + this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader()); + this.handlerMappingsLocation = handlerMappingsLocation; + } + + + /** + * Locate the {@link NamespaceHandler} for the supplied namespace URI + * from the configured mappings. + * @param namespaceUri the relevant namespace URI + * @return the located {@link NamespaceHandler}, or {@code null} if none found + */ + @Override + @Nullable + public NamespaceHandler resolve(String namespaceUri) { + Map handlerMappings = getHandlerMappings(); + Object handlerOrClassName = handlerMappings.get(namespaceUri); + if (handlerOrClassName == null) { + return null; + } + else if (handlerOrClassName instanceof NamespaceHandler) { + return (NamespaceHandler) handlerOrClassName; + } + else { + String className = (String) handlerOrClassName; + try { + Class handlerClass = ClassUtils.forName(className, this.classLoader); + if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) { + throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri + + "] does not implement the [" + NamespaceHandler.class.getName() + "] interface"); + } + NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass); + namespaceHandler.init(); + handlerMappings.put(namespaceUri, namespaceHandler); + return namespaceHandler; + } + catch (ClassNotFoundException ex) { + throw new FatalBeanException("Could not find NamespaceHandler class [" + className + + "] for namespace [" + namespaceUri + "]", ex); + } + catch (LinkageError err) { + throw new FatalBeanException("Unresolvable class definition for NamespaceHandler class [" + + className + "] for namespace [" + namespaceUri + "]", err); + } + } + } + + /** + * Load the specified NamespaceHandler mappings lazily. + */ + private Map getHandlerMappings() { + Map handlerMappings = this.handlerMappings; + if (handlerMappings == null) { + synchronized (this) { + handlerMappings = this.handlerMappings; + if (handlerMappings == null) { + if (logger.isTraceEnabled()) { + logger.trace("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]"); + } + try { + Properties mappings = + PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader); + if (logger.isTraceEnabled()) { + logger.trace("Loaded NamespaceHandler mappings: " + mappings); + } + handlerMappings = new ConcurrentHashMap<>(mappings.size()); + CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings); + this.handlerMappings = handlerMappings; + } + catch (IOException ex) { + throw new IllegalStateException( + "Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex); + } + } + } + } + return handlerMappings; + } + + + @Override + public String toString() { + return "NamespaceHandlerResolver using mappings " + getHandlerMappings(); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DelegatingEntityResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DelegatingEntityResolver.java new file mode 100644 index 0000000..1335d04 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DelegatingEntityResolver.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import java.io.IOException; + +import org.xml.sax.EntityResolver; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * {@link EntityResolver} implementation that delegates to a {@link BeansDtdResolver} + * and a {@link PluggableSchemaResolver} for DTDs and XML schemas, respectively. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Rick Evans + * @since 2.0 + * @see BeansDtdResolver + * @see PluggableSchemaResolver + */ +public class DelegatingEntityResolver implements EntityResolver { + + /** Suffix for DTD files. */ + public static final String DTD_SUFFIX = ".dtd"; + + /** Suffix for schema definition files. */ + public static final String XSD_SUFFIX = ".xsd"; + + + private final EntityResolver dtdResolver; + + private final EntityResolver schemaResolver; + + + /** + * Create a new DelegatingEntityResolver that delegates to + * a default {@link BeansDtdResolver} and a default {@link PluggableSchemaResolver}. + *

    Configures the {@link PluggableSchemaResolver} with the supplied + * {@link ClassLoader}. + * @param classLoader the ClassLoader to use for loading + * (can be {@code null}) to use the default ClassLoader) + */ + public DelegatingEntityResolver(@Nullable ClassLoader classLoader) { + this.dtdResolver = new BeansDtdResolver(); + this.schemaResolver = new PluggableSchemaResolver(classLoader); + } + + /** + * Create a new DelegatingEntityResolver that delegates to + * the given {@link EntityResolver EntityResolvers}. + * @param dtdResolver the EntityResolver to resolve DTDs with + * @param schemaResolver the EntityResolver to resolve XML schemas with + */ + public DelegatingEntityResolver(EntityResolver dtdResolver, EntityResolver schemaResolver) { + Assert.notNull(dtdResolver, "'dtdResolver' is required"); + Assert.notNull(schemaResolver, "'schemaResolver' is required"); + this.dtdResolver = dtdResolver; + this.schemaResolver = schemaResolver; + } + + + @Override + @Nullable + public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) + throws SAXException, IOException { + + if (systemId != null) { + if (systemId.endsWith(DTD_SUFFIX)) { + return this.dtdResolver.resolveEntity(publicId, systemId); + } + else if (systemId.endsWith(XSD_SUFFIX)) { + return this.schemaResolver.resolveEntity(publicId, systemId); + } + } + + // Fall back to the parser's default behavior. + return null; + } + + + @Override + public String toString() { + return "EntityResolver delegating " + XSD_SUFFIX + " to " + this.schemaResolver + + " and " + DTD_SUFFIX + " to " + this.dtdResolver; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DocumentDefaultsDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DocumentDefaultsDefinition.java new file mode 100644 index 0000000..d5a2122 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DocumentDefaultsDefinition.java @@ -0,0 +1,160 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.springframework.beans.factory.parsing.DefaultsDefinition; +import org.springframework.lang.Nullable; + +/** + * Simple JavaBean that holds the defaults specified at the {@code } + * level in a standard Spring XML bean definition document: + * {@code default-lazy-init}, {@code default-autowire}, etc. + * + * @author Juergen Hoeller + * @since 2.0.2 + */ +public class DocumentDefaultsDefinition implements DefaultsDefinition { + + @Nullable + private String lazyInit; + + @Nullable + private String merge; + + @Nullable + private String autowire; + + @Nullable + private String autowireCandidates; + + @Nullable + private String initMethod; + + @Nullable + private String destroyMethod; + + @Nullable + private Object source; + + + /** + * Set the default lazy-init flag for the document that's currently parsed. + */ + public void setLazyInit(@Nullable String lazyInit) { + this.lazyInit = lazyInit; + } + + /** + * Return the default lazy-init flag for the document that's currently parsed. + */ + @Nullable + public String getLazyInit() { + return this.lazyInit; + } + + /** + * Set the default merge setting for the document that's currently parsed. + */ + public void setMerge(@Nullable String merge) { + this.merge = merge; + } + + /** + * Return the default merge setting for the document that's currently parsed. + */ + @Nullable + public String getMerge() { + return this.merge; + } + + /** + * Set the default autowire setting for the document that's currently parsed. + */ + public void setAutowire(@Nullable String autowire) { + this.autowire = autowire; + } + + /** + * Return the default autowire setting for the document that's currently parsed. + */ + @Nullable + public String getAutowire() { + return this.autowire; + } + + /** + * Set the default autowire-candidate pattern for the document that's currently parsed. + * Also accepts a comma-separated list of patterns. + */ + public void setAutowireCandidates(@Nullable String autowireCandidates) { + this.autowireCandidates = autowireCandidates; + } + + /** + * Return the default autowire-candidate pattern for the document that's currently parsed. + * May also return a comma-separated list of patterns. + */ + @Nullable + public String getAutowireCandidates() { + return this.autowireCandidates; + } + + /** + * Set the default init-method setting for the document that's currently parsed. + */ + public void setInitMethod(@Nullable String initMethod) { + this.initMethod = initMethod; + } + + /** + * Return the default init-method setting for the document that's currently parsed. + */ + @Nullable + public String getInitMethod() { + return this.initMethod; + } + + /** + * Set the default destroy-method setting for the document that's currently parsed. + */ + public void setDestroyMethod(@Nullable String destroyMethod) { + this.destroyMethod = destroyMethod; + } + + /** + * Return the default destroy-method setting for the document that's currently parsed. + */ + @Nullable + public String getDestroyMethod() { + return this.destroyMethod; + } + + /** + * Set the configuration source {@code Object} for this metadata element. + *

    The exact type of the object will depend on the configuration mechanism used. + */ + public void setSource(@Nullable Object source) { + this.source = source; + } + + @Override + @Nullable + public Object getSource() { + return this.source; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DocumentLoader.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DocumentLoader.java new file mode 100644 index 0000000..816ac63 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DocumentLoader.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.w3c.dom.Document; +import org.xml.sax.EntityResolver; +import org.xml.sax.ErrorHandler; +import org.xml.sax.InputSource; + +/** + * Strategy interface for loading an XML {@link Document}. + * + * @author Rob Harrop + * @since 2.0 + * @see DefaultDocumentLoader + */ +public interface DocumentLoader { + + /** + * Load a {@link Document document} from the supplied {@link InputSource source}. + * @param inputSource the source of the document that is to be loaded + * @param entityResolver the resolver that is to be used to resolve any entities + * @param errorHandler used to report any errors during document loading + * @param validationMode the type of validation + * {@link org.springframework.util.xml.XmlValidationModeDetector#VALIDATION_DTD DTD} + * or {@link org.springframework.util.xml.XmlValidationModeDetector#VALIDATION_XSD XSD}) + * @param namespaceAware {@code true} if support for XML namespaces is to be provided + * @return the loaded {@link Document document} + * @throws Exception if an error occurs + */ + Document loadDocument( + InputSource inputSource, EntityResolver entityResolver, + ErrorHandler errorHandler, int validationMode, boolean namespaceAware) + throws Exception; + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/NamespaceHandler.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/NamespaceHandler.java new file mode 100644 index 0000000..fa061fe --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/NamespaceHandler.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.lang.Nullable; + +/** + * Base interface used by the {@link DefaultBeanDefinitionDocumentReader} + * for handling custom namespaces in a Spring XML configuration file. + * + *

    Implementations are expected to return implementations of the + * {@link BeanDefinitionParser} interface for custom top-level tags and + * implementations of the {@link BeanDefinitionDecorator} interface for + * custom nested tags. + * + *

    The parser will call {@link #parse} when it encounters a custom tag + * directly under the {@code } tags and {@link #decorate} when + * it encounters a custom tag directly under a {@code } tag. + * + *

    Developers writing their own custom element extensions typically will + * not implement this interface directly, but rather make use of the provided + * {@link NamespaceHandlerSupport} class. + * + * @author Rob Harrop + * @author Erik Wiersma + * @since 2.0 + * @see DefaultBeanDefinitionDocumentReader + * @see NamespaceHandlerResolver + */ +public interface NamespaceHandler { + + /** + * Invoked by the {@link DefaultBeanDefinitionDocumentReader} after + * construction but before any custom elements are parsed. + * @see NamespaceHandlerSupport#registerBeanDefinitionParser(String, BeanDefinitionParser) + */ + void init(); + + /** + * Parse the specified {@link Element} and register any resulting + * {@link BeanDefinition BeanDefinitions} with the + * {@link org.springframework.beans.factory.support.BeanDefinitionRegistry} + * that is embedded in the supplied {@link ParserContext}. + *

    Implementations should return the primary {@code BeanDefinition} + * that results from the parse phase if they wish to be used nested + * inside (for example) a {@code } tag. + *

    Implementations may return {@code null} if they will + * not be used in a nested scenario. + * @param element the element that is to be parsed into one or more {@code BeanDefinitions} + * @param parserContext the object encapsulating the current state of the parsing process + * @return the primary {@code BeanDefinition} (can be {@code null} as explained above) + */ + @Nullable + BeanDefinition parse(Element element, ParserContext parserContext); + + /** + * Parse the specified {@link Node} and decorate the supplied + * {@link BeanDefinitionHolder}, returning the decorated definition. + *

    The {@link Node} may be either an {@link org.w3c.dom.Attr} or an + * {@link Element}, depending on whether a custom attribute or element + * is being parsed. + *

    Implementations may choose to return a completely new definition, + * which will replace the original definition in the resulting + * {@link org.springframework.beans.factory.BeanFactory}. + *

    The supplied {@link ParserContext} can be used to register any + * additional beans needed to support the main definition. + * @param source the source element or attribute that is to be parsed + * @param definition the current bean definition + * @param parserContext the object encapsulating the current state of the parsing process + * @return the decorated definition (to be registered in the BeanFactory), + * or simply the original bean definition if no decoration is required. + * A {@code null} value is strictly speaking invalid, but will be leniently + * treated like the case where the original bean definition gets returned. + */ + @Nullable + BeanDefinitionHolder decorate(Node source, BeanDefinitionHolder definition, ParserContext parserContext); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/NamespaceHandlerResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/NamespaceHandlerResolver.java new file mode 100644 index 0000000..2e92b25 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/NamespaceHandlerResolver.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.springframework.lang.Nullable; + +/** + * Used by the {@link org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader} to + * locate a {@link NamespaceHandler} implementation for a particular namespace URI. + * + * @author Rob Harrop + * @since 2.0 + * @see NamespaceHandler + * @see org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader + */ +@FunctionalInterface +public interface NamespaceHandlerResolver { + + /** + * Resolve the namespace URI and return the located {@link NamespaceHandler} + * implementation. + * @param namespaceUri the relevant namespace URI + * @return the located {@link NamespaceHandler} (may be {@code null}) + */ + @Nullable + NamespaceHandler resolve(String namespaceUri); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/NamespaceHandlerSupport.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/NamespaceHandlerSupport.java new file mode 100644 index 0000000..b1eec9b --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/NamespaceHandlerSupport.java @@ -0,0 +1,159 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import java.util.HashMap; +import java.util.Map; + +import org.w3c.dom.Attr; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.lang.Nullable; + +/** + * Support class for implementing custom {@link NamespaceHandler NamespaceHandlers}. + * Parsing and decorating of individual {@link Node Nodes} is done via {@link BeanDefinitionParser} + * and {@link BeanDefinitionDecorator} strategy interfaces, respectively. + * + *

    Provides the {@link #registerBeanDefinitionParser} and {@link #registerBeanDefinitionDecorator} + * methods for registering a {@link BeanDefinitionParser} or {@link BeanDefinitionDecorator} + * to handle a specific element. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + * @see #registerBeanDefinitionParser(String, BeanDefinitionParser) + * @see #registerBeanDefinitionDecorator(String, BeanDefinitionDecorator) + */ +public abstract class NamespaceHandlerSupport implements NamespaceHandler { + + /** + * Stores the {@link BeanDefinitionParser} implementations keyed by the + * local name of the {@link Element Elements} they handle. + */ + private final Map parsers = new HashMap<>(); + + /** + * Stores the {@link BeanDefinitionDecorator} implementations keyed by the + * local name of the {@link Element Elements} they handle. + */ + private final Map decorators = new HashMap<>(); + + /** + * Stores the {@link BeanDefinitionDecorator} implementations keyed by the local + * name of the {@link Attr Attrs} they handle. + */ + private final Map attributeDecorators = new HashMap<>(); + + + /** + * Parses the supplied {@link Element} by delegating to the {@link BeanDefinitionParser} that is + * registered for that {@link Element}. + */ + @Override + @Nullable + public BeanDefinition parse(Element element, ParserContext parserContext) { + BeanDefinitionParser parser = findParserForElement(element, parserContext); + return (parser != null ? parser.parse(element, parserContext) : null); + } + + /** + * Locates the {@link BeanDefinitionParser} from the register implementations using + * the local name of the supplied {@link Element}. + */ + @Nullable + private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) { + String localName = parserContext.getDelegate().getLocalName(element); + BeanDefinitionParser parser = this.parsers.get(localName); + if (parser == null) { + parserContext.getReaderContext().fatal( + "Cannot locate BeanDefinitionParser for element [" + localName + "]", element); + } + return parser; + } + + /** + * Decorates the supplied {@link Node} by delegating to the {@link BeanDefinitionDecorator} that + * is registered to handle that {@link Node}. + */ + @Override + @Nullable + public BeanDefinitionHolder decorate( + Node node, BeanDefinitionHolder definition, ParserContext parserContext) { + + BeanDefinitionDecorator decorator = findDecoratorForNode(node, parserContext); + return (decorator != null ? decorator.decorate(node, definition, parserContext) : null); + } + + /** + * Locates the {@link BeanDefinitionParser} from the register implementations using + * the local name of the supplied {@link Node}. Supports both {@link Element Elements} + * and {@link Attr Attrs}. + */ + @Nullable + private BeanDefinitionDecorator findDecoratorForNode(Node node, ParserContext parserContext) { + BeanDefinitionDecorator decorator = null; + String localName = parserContext.getDelegate().getLocalName(node); + if (node instanceof Element) { + decorator = this.decorators.get(localName); + } + else if (node instanceof Attr) { + decorator = this.attributeDecorators.get(localName); + } + else { + parserContext.getReaderContext().fatal( + "Cannot decorate based on Nodes of type [" + node.getClass().getName() + "]", node); + } + if (decorator == null) { + parserContext.getReaderContext().fatal("Cannot locate BeanDefinitionDecorator for " + + (node instanceof Element ? "element" : "attribute") + " [" + localName + "]", node); + } + return decorator; + } + + + /** + * Subclasses can call this to register the supplied {@link BeanDefinitionParser} to + * handle the specified element. The element name is the local (non-namespace qualified) + * name. + */ + protected final void registerBeanDefinitionParser(String elementName, BeanDefinitionParser parser) { + this.parsers.put(elementName, parser); + } + + /** + * Subclasses can call this to register the supplied {@link BeanDefinitionDecorator} to + * handle the specified element. The element name is the local (non-namespace qualified) + * name. + */ + protected final void registerBeanDefinitionDecorator(String elementName, BeanDefinitionDecorator dec) { + this.decorators.put(elementName, dec); + } + + /** + * Subclasses can call this to register the supplied {@link BeanDefinitionDecorator} to + * handle the specified attribute. The attribute name is the local (non-namespace qualified) + * name. + */ + protected final void registerBeanDefinitionDecoratorForAttribute(String attrName, BeanDefinitionDecorator dec) { + this.attributeDecorators.put(attrName, dec); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/ParserContext.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/ParserContext.java new file mode 100644 index 0000000..2223f78 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/ParserContext.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import java.util.ArrayDeque; +import java.util.Deque; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.parsing.ComponentDefinition; +import org.springframework.beans.factory.parsing.CompositeComponentDefinition; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.lang.Nullable; + +/** + * Context that gets passed along a bean definition parsing process, + * encapsulating all relevant configuration as well as state. + * Nested inside an {@link XmlReaderContext}. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + * @see XmlReaderContext + * @see BeanDefinitionParserDelegate + */ +public final class ParserContext { + + private final XmlReaderContext readerContext; + + private final BeanDefinitionParserDelegate delegate; + + @Nullable + private BeanDefinition containingBeanDefinition; + + private final Deque containingComponents = new ArrayDeque<>(); + + + public ParserContext(XmlReaderContext readerContext, BeanDefinitionParserDelegate delegate) { + this.readerContext = readerContext; + this.delegate = delegate; + } + + public ParserContext(XmlReaderContext readerContext, BeanDefinitionParserDelegate delegate, + @Nullable BeanDefinition containingBeanDefinition) { + + this.readerContext = readerContext; + this.delegate = delegate; + this.containingBeanDefinition = containingBeanDefinition; + } + + + public final XmlReaderContext getReaderContext() { + return this.readerContext; + } + + public final BeanDefinitionRegistry getRegistry() { + return this.readerContext.getRegistry(); + } + + public final BeanDefinitionParserDelegate getDelegate() { + return this.delegate; + } + + @Nullable + public final BeanDefinition getContainingBeanDefinition() { + return this.containingBeanDefinition; + } + + public final boolean isNested() { + return (this.containingBeanDefinition != null); + } + + public boolean isDefaultLazyInit() { + return BeanDefinitionParserDelegate.TRUE_VALUE.equals(this.delegate.getDefaults().getLazyInit()); + } + + @Nullable + public Object extractSource(Object sourceCandidate) { + return this.readerContext.extractSource(sourceCandidate); + } + + @Nullable + public CompositeComponentDefinition getContainingComponent() { + return this.containingComponents.peek(); + } + + public void pushContainingComponent(CompositeComponentDefinition containingComponent) { + this.containingComponents.push(containingComponent); + } + + public CompositeComponentDefinition popContainingComponent() { + return this.containingComponents.pop(); + } + + public void popAndRegisterContainingComponent() { + registerComponent(popContainingComponent()); + } + + public void registerComponent(ComponentDefinition component) { + CompositeComponentDefinition containingComponent = getContainingComponent(); + if (containingComponent != null) { + containingComponent.addNestedComponent(component); + } + else { + this.readerContext.fireComponentRegistered(component); + } + } + + public void registerBeanComponent(BeanComponentDefinition component) { + BeanDefinitionReaderUtils.registerBeanDefinition(component, getRegistry()); + registerComponent(component); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/PluggableSchemaResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/PluggableSchemaResolver.java new file mode 100644 index 0000000..3b1bff1 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/PluggableSchemaResolver.java @@ -0,0 +1,182 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.xml.sax.EntityResolver; +import org.xml.sax.InputSource; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * {@link EntityResolver} implementation that attempts to resolve schema URLs into + * local {@link ClassPathResource classpath resources} using a set of mappings files. + * + *

    By default, this class will look for mapping files in the classpath using the + * pattern: {@code META-INF/spring.schemas} allowing for multiple files to exist on + * the classpath at any one time. + * + *

    The format of {@code META-INF/spring.schemas} is a properties file where each line + * should be of the form {@code systemId=schema-location} where {@code schema-location} + * should also be a schema file in the classpath. Since {@code systemId} is commonly a + * URL, one must be careful to escape any ':' characters which are treated as delimiters + * in properties files. + * + *

    The pattern for the mapping files can be overridden using the + * {@link #PluggableSchemaResolver(ClassLoader, String)} constructor. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +public class PluggableSchemaResolver implements EntityResolver { + + /** + * The location of the file that defines schema mappings. + * Can be present in multiple JAR files. + */ + public static final String DEFAULT_SCHEMA_MAPPINGS_LOCATION = "META-INF/spring.schemas"; + + + private static final Log logger = LogFactory.getLog(PluggableSchemaResolver.class); + + @Nullable + private final ClassLoader classLoader; + + private final String schemaMappingsLocation; + + /** Stores the mapping of schema URL -> local schema path. */ + @Nullable + private volatile Map schemaMappings; + + + /** + * Loads the schema URL -> schema file location mappings using the default + * mapping file pattern "META-INF/spring.schemas". + * @param classLoader the ClassLoader to use for loading + * (can be {@code null}) to use the default ClassLoader) + * @see PropertiesLoaderUtils#loadAllProperties(String, ClassLoader) + */ + public PluggableSchemaResolver(@Nullable ClassLoader classLoader) { + this.classLoader = classLoader; + this.schemaMappingsLocation = DEFAULT_SCHEMA_MAPPINGS_LOCATION; + } + + /** + * Loads the schema URL -> schema file location mappings using the given + * mapping file pattern. + * @param classLoader the ClassLoader to use for loading + * (can be {@code null}) to use the default ClassLoader) + * @param schemaMappingsLocation the location of the file that defines schema mappings + * (must not be empty) + * @see PropertiesLoaderUtils#loadAllProperties(String, ClassLoader) + */ + public PluggableSchemaResolver(@Nullable ClassLoader classLoader, String schemaMappingsLocation) { + Assert.hasText(schemaMappingsLocation, "'schemaMappingsLocation' must not be empty"); + this.classLoader = classLoader; + this.schemaMappingsLocation = schemaMappingsLocation; + } + + + @Override + @Nullable + public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) throws IOException { + if (logger.isTraceEnabled()) { + logger.trace("Trying to resolve XML entity with public id [" + publicId + + "] and system id [" + systemId + "]"); + } + + if (systemId != null) { + String resourceLocation = getSchemaMappings().get(systemId); + if (resourceLocation == null && systemId.startsWith("https:")) { + // Retrieve canonical http schema mapping even for https declaration + resourceLocation = getSchemaMappings().get("http:" + systemId.substring(6)); + } + if (resourceLocation != null) { + Resource resource = new ClassPathResource(resourceLocation, this.classLoader); + try { + InputSource source = new InputSource(resource.getInputStream()); + source.setPublicId(publicId); + source.setSystemId(systemId); + if (logger.isTraceEnabled()) { + logger.trace("Found XML schema [" + systemId + "] in classpath: " + resourceLocation); + } + return source; + } + catch (FileNotFoundException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Could not find XML schema [" + systemId + "]: " + resource, ex); + } + } + } + } + + // Fall back to the parser's default behavior. + return null; + } + + /** + * Load the specified schema mappings lazily. + */ + private Map getSchemaMappings() { + Map schemaMappings = this.schemaMappings; + if (schemaMappings == null) { + synchronized (this) { + schemaMappings = this.schemaMappings; + if (schemaMappings == null) { + if (logger.isTraceEnabled()) { + logger.trace("Loading schema mappings from [" + this.schemaMappingsLocation + "]"); + } + try { + Properties mappings = + PropertiesLoaderUtils.loadAllProperties(this.schemaMappingsLocation, this.classLoader); + if (logger.isTraceEnabled()) { + logger.trace("Loaded schema mappings: " + mappings); + } + schemaMappings = new ConcurrentHashMap<>(mappings.size()); + CollectionUtils.mergePropertiesIntoMap(mappings, schemaMappings); + this.schemaMappings = schemaMappings; + } + catch (IOException ex) { + throw new IllegalStateException( + "Unable to load schema mappings from location [" + this.schemaMappingsLocation + "]", ex); + } + } + } + } + return schemaMappings; + } + + + @Override + public String toString() { + return "EntityResolver using schema mappings " + getSchemaMappings(); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/ResourceEntityResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/ResourceEntityResolver.java new file mode 100644 index 0000000..b74e013 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/ResourceEntityResolver.java @@ -0,0 +1,135 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.net.URLDecoder; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.lang.Nullable; + +/** + * {@code EntityResolver} implementation that tries to resolve entity references + * through a {@link org.springframework.core.io.ResourceLoader} (usually, + * relative to the resource base of an {@code ApplicationContext}), if applicable. + * Extends {@link DelegatingEntityResolver} to also provide DTD and XSD lookup. + * + *

    Allows to use standard XML entities to include XML snippets into an + * application context definition, for example to split a large XML file + * into various modules. The include paths can be relative to the + * application context's resource base as usual, instead of relative + * to the JVM working directory (the XML parser's default). + * + *

    Note: In addition to relative paths, every URL that specifies a + * file in the current system root, i.e. the JVM working directory, + * will be interpreted relative to the application context too. + * + * @author Juergen Hoeller + * @since 31.07.2003 + * @see org.springframework.core.io.ResourceLoader + * @see org.springframework.context.ApplicationContext + */ +public class ResourceEntityResolver extends DelegatingEntityResolver { + + private static final Log logger = LogFactory.getLog(ResourceEntityResolver.class); + + private final ResourceLoader resourceLoader; + + + /** + * Create a ResourceEntityResolver for the specified ResourceLoader + * (usually, an ApplicationContext). + * @param resourceLoader the ResourceLoader (or ApplicationContext) + * to load XML entity includes with + */ + public ResourceEntityResolver(ResourceLoader resourceLoader) { + super(resourceLoader.getClassLoader()); + this.resourceLoader = resourceLoader; + } + + + @Override + @Nullable + public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) + throws SAXException, IOException { + + InputSource source = super.resolveEntity(publicId, systemId); + + if (source == null && systemId != null) { + String resourcePath = null; + try { + String decodedSystemId = URLDecoder.decode(systemId, "UTF-8"); + String givenUrl = new URL(decodedSystemId).toString(); + String systemRootUrl = new File("").toURI().toURL().toString(); + // Try relative to resource base if currently in system root. + if (givenUrl.startsWith(systemRootUrl)) { + resourcePath = givenUrl.substring(systemRootUrl.length()); + } + } + catch (Exception ex) { + // Typically a MalformedURLException or AccessControlException. + if (logger.isDebugEnabled()) { + logger.debug("Could not resolve XML entity [" + systemId + "] against system root URL", ex); + } + // No URL (or no resolvable URL) -> try relative to resource base. + resourcePath = systemId; + } + if (resourcePath != null) { + if (logger.isTraceEnabled()) { + logger.trace("Trying to locate XML entity [" + systemId + "] as resource [" + resourcePath + "]"); + } + Resource resource = this.resourceLoader.getResource(resourcePath); + source = new InputSource(resource.getInputStream()); + source.setPublicId(publicId); + source.setSystemId(systemId); + if (logger.isDebugEnabled()) { + logger.debug("Found XML entity [" + systemId + "]: " + resource); + } + } + else if (systemId.endsWith(DTD_SUFFIX) || systemId.endsWith(XSD_SUFFIX)) { + // External dtd/xsd lookup via https even for canonical http declaration + String url = systemId; + if (url.startsWith("http:")) { + url = "https:" + url.substring(5); + } + try { + source = new InputSource(new URL(url).openStream()); + source.setPublicId(publicId); + source.setSystemId(systemId); + } + catch (IOException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Could not resolve XML entity [" + systemId + "] through URL [" + url + "]", ex); + } + // Fall back to the parser's default behavior. + source = null; + } + } + } + + return source; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/SimpleConstructorNamespaceHandler.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/SimpleConstructorNamespaceHandler.java new file mode 100644 index 0000000..ddffb17 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/SimpleConstructorNamespaceHandler.java @@ -0,0 +1,159 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import java.util.Collection; + +import org.w3c.dom.Attr; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.core.Conventions; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Simple {@code NamespaceHandler} implementation that maps custom + * attributes directly through to bean properties. An important point to note is + * that this {@code NamespaceHandler} does not have a corresponding schema + * since there is no way to know in advance all possible attribute names. + * + *

    An example of the usage of this {@code NamespaceHandler} is shown below: + * + *

    + * <bean id="author" class="..TestBean" c:name="Enescu" c:work-ref="compositions"/>
    + * 
    + * + * Here the '{@code c:name}' corresponds directly to the '{@code name} + * ' argument declared on the constructor of class '{@code TestBean}'. The + * '{@code c:work-ref}' attributes corresponds to the '{@code work}' + * argument and, rather than being the concrete value, it contains the name of + * the bean that will be considered as a parameter. + * + * Note: This implementation supports only named parameters - there is no + * support for indexes or types. Further more, the names are used as hints by + * the container which, by default, does type introspection. + * + * @author Costin Leau + * @since 3.1 + * @see SimplePropertyNamespaceHandler + */ +public class SimpleConstructorNamespaceHandler implements NamespaceHandler { + + private static final String REF_SUFFIX = "-ref"; + + private static final String DELIMITER_PREFIX = "_"; + + + @Override + public void init() { + } + + @Override + @Nullable + public BeanDefinition parse(Element element, ParserContext parserContext) { + parserContext.getReaderContext().error( + "Class [" + getClass().getName() + "] does not support custom elements.", element); + return null; + } + + @Override + public BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder definition, ParserContext parserContext) { + if (node instanceof Attr) { + Attr attr = (Attr) node; + String argName = StringUtils.trimWhitespace(parserContext.getDelegate().getLocalName(attr)); + String argValue = StringUtils.trimWhitespace(attr.getValue()); + + ConstructorArgumentValues cvs = definition.getBeanDefinition().getConstructorArgumentValues(); + boolean ref = false; + + // handle -ref arguments + if (argName.endsWith(REF_SUFFIX)) { + ref = true; + argName = argName.substring(0, argName.length() - REF_SUFFIX.length()); + } + + ValueHolder valueHolder = new ValueHolder(ref ? new RuntimeBeanReference(argValue) : argValue); + valueHolder.setSource(parserContext.getReaderContext().extractSource(attr)); + + // handle "escaped"/"_" arguments + if (argName.startsWith(DELIMITER_PREFIX)) { + String arg = argName.substring(1).trim(); + + // fast default check + if (!StringUtils.hasText(arg)) { + cvs.addGenericArgumentValue(valueHolder); + } + // assume an index otherwise + else { + int index = -1; + try { + index = Integer.parseInt(arg); + } + catch (NumberFormatException ex) { + parserContext.getReaderContext().error( + "Constructor argument '" + argName + "' specifies an invalid integer", attr); + } + if (index < 0) { + parserContext.getReaderContext().error( + "Constructor argument '" + argName + "' specifies a negative index", attr); + } + + if (cvs.hasIndexedArgumentValue(index)) { + parserContext.getReaderContext().error( + "Constructor argument '" + argName + "' with index "+ index+" already defined using ." + + " Only one approach may be used per argument.", attr); + } + + cvs.addIndexedArgumentValue(index, valueHolder); + } + } + // no escaping -> ctr name + else { + String name = Conventions.attributeNameToPropertyName(argName); + if (containsArgWithName(name, cvs)) { + parserContext.getReaderContext().error( + "Constructor argument '" + argName + "' already defined using ." + + " Only one approach may be used per argument.", attr); + } + valueHolder.setName(Conventions.attributeNameToPropertyName(argName)); + cvs.addGenericArgumentValue(valueHolder); + } + } + return definition; + } + + private boolean containsArgWithName(String name, ConstructorArgumentValues cvs) { + return (checkName(name, cvs.getGenericArgumentValues()) || + checkName(name, cvs.getIndexedArgumentValues().values())); + } + + private boolean checkName(String name, Collection values) { + for (ValueHolder holder : values) { + if (name.equals(holder.getName())) { + return true; + } + } + return false; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/SimplePropertyNamespaceHandler.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/SimplePropertyNamespaceHandler.java new file mode 100644 index 0000000..9cecef4 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/SimplePropertyNamespaceHandler.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.w3c.dom.Attr; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.core.Conventions; +import org.springframework.lang.Nullable; + +/** + * Simple {@code NamespaceHandler} implementation that maps custom attributes + * directly through to bean properties. An important point to note is that this + * {@code NamespaceHandler} does not have a corresponding schema since there + * is no way to know in advance all possible attribute names. + * + *

    An example of the usage of this {@code NamespaceHandler} is shown below: + * + *

    + * <bean id="rob" class="..TestBean" p:name="Rob Harrop" p:spouse-ref="sally"/>
    + * + * Here the '{@code p:name}' corresponds directly to the '{@code name}' + * property on class '{@code TestBean}'. The '{@code p:spouse-ref}' + * attributes corresponds to the '{@code spouse}' property and, rather + * than being the concrete value, it contains the name of the bean that will + * be injected into that property. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +public class SimplePropertyNamespaceHandler implements NamespaceHandler { + + private static final String REF_SUFFIX = "-ref"; + + + @Override + public void init() { + } + + @Override + @Nullable + public BeanDefinition parse(Element element, ParserContext parserContext) { + parserContext.getReaderContext().error( + "Class [" + getClass().getName() + "] does not support custom elements.", element); + return null; + } + + @Override + public BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder definition, ParserContext parserContext) { + if (node instanceof Attr) { + Attr attr = (Attr) node; + String propertyName = parserContext.getDelegate().getLocalName(attr); + String propertyValue = attr.getValue(); + MutablePropertyValues pvs = definition.getBeanDefinition().getPropertyValues(); + if (pvs.contains(propertyName)) { + parserContext.getReaderContext().error("Property '" + propertyName + "' is already defined using " + + "both and inline syntax. Only one approach may be used per property.", attr); + } + if (propertyName.endsWith(REF_SUFFIX)) { + propertyName = propertyName.substring(0, propertyName.length() - REF_SUFFIX.length()); + pvs.add(Conventions.attributeNameToPropertyName(propertyName), new RuntimeBeanReference(propertyValue)); + } + else { + pvs.add(Conventions.attributeNameToPropertyName(propertyName), propertyValue); + } + } + return definition; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/UtilNamespaceHandler.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/UtilNamespaceHandler.java new file mode 100644 index 0000000..632ddd0 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/UtilNamespaceHandler.java @@ -0,0 +1,221 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.FieldRetrievingFactoryBean; +import org.springframework.beans.factory.config.ListFactoryBean; +import org.springframework.beans.factory.config.MapFactoryBean; +import org.springframework.beans.factory.config.PropertiesFactoryBean; +import org.springframework.beans.factory.config.PropertyPathFactoryBean; +import org.springframework.beans.factory.config.SetFactoryBean; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.util.StringUtils; + +/** + * {@link NamespaceHandler} for the {@code util} namespace. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +public class UtilNamespaceHandler extends NamespaceHandlerSupport { + + private static final String SCOPE_ATTRIBUTE = "scope"; + + + @Override + public void init() { + registerBeanDefinitionParser("constant", new ConstantBeanDefinitionParser()); + registerBeanDefinitionParser("property-path", new PropertyPathBeanDefinitionParser()); + registerBeanDefinitionParser("list", new ListBeanDefinitionParser()); + registerBeanDefinitionParser("set", new SetBeanDefinitionParser()); + registerBeanDefinitionParser("map", new MapBeanDefinitionParser()); + registerBeanDefinitionParser("properties", new PropertiesBeanDefinitionParser()); + } + + + private static class ConstantBeanDefinitionParser extends AbstractSimpleBeanDefinitionParser { + + @Override + protected Class getBeanClass(Element element) { + return FieldRetrievingFactoryBean.class; + } + + @Override + protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext) { + String id = super.resolveId(element, definition, parserContext); + if (!StringUtils.hasText(id)) { + id = element.getAttribute("static-field"); + } + return id; + } + } + + + private static class PropertyPathBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { + + @Override + protected Class getBeanClass(Element element) { + return PropertyPathFactoryBean.class; + } + + @Override + protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + String path = element.getAttribute("path"); + if (!StringUtils.hasText(path)) { + parserContext.getReaderContext().error("Attribute 'path' must not be empty", element); + return; + } + int dotIndex = path.indexOf('.'); + if (dotIndex == -1) { + parserContext.getReaderContext().error( + "Attribute 'path' must follow pattern 'beanName.propertyName'", element); + return; + } + String beanName = path.substring(0, dotIndex); + String propertyPath = path.substring(dotIndex + 1); + builder.addPropertyValue("targetBeanName", beanName); + builder.addPropertyValue("propertyPath", propertyPath); + } + + @Override + protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext) { + String id = super.resolveId(element, definition, parserContext); + if (!StringUtils.hasText(id)) { + id = element.getAttribute("path"); + } + return id; + } + } + + + private static class ListBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { + + @Override + protected Class getBeanClass(Element element) { + return ListFactoryBean.class; + } + + @Override + protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + List parsedList = parserContext.getDelegate().parseListElement(element, builder.getRawBeanDefinition()); + builder.addPropertyValue("sourceList", parsedList); + + String listClass = element.getAttribute("list-class"); + if (StringUtils.hasText(listClass)) { + builder.addPropertyValue("targetListClass", listClass); + } + + String scope = element.getAttribute(SCOPE_ATTRIBUTE); + if (StringUtils.hasLength(scope)) { + builder.setScope(scope); + } + } + } + + + private static class SetBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { + + @Override + protected Class getBeanClass(Element element) { + return SetFactoryBean.class; + } + + @Override + protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + Set parsedSet = parserContext.getDelegate().parseSetElement(element, builder.getRawBeanDefinition()); + builder.addPropertyValue("sourceSet", parsedSet); + + String setClass = element.getAttribute("set-class"); + if (StringUtils.hasText(setClass)) { + builder.addPropertyValue("targetSetClass", setClass); + } + + String scope = element.getAttribute(SCOPE_ATTRIBUTE); + if (StringUtils.hasLength(scope)) { + builder.setScope(scope); + } + } + } + + + private static class MapBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { + + @Override + protected Class getBeanClass(Element element) { + return MapFactoryBean.class; + } + + @Override + protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + Map parsedMap = parserContext.getDelegate().parseMapElement(element, builder.getRawBeanDefinition()); + builder.addPropertyValue("sourceMap", parsedMap); + + String mapClass = element.getAttribute("map-class"); + if (StringUtils.hasText(mapClass)) { + builder.addPropertyValue("targetMapClass", mapClass); + } + + String scope = element.getAttribute(SCOPE_ATTRIBUTE); + if (StringUtils.hasLength(scope)) { + builder.setScope(scope); + } + } + } + + + private static class PropertiesBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { + + @Override + protected Class getBeanClass(Element element) { + return PropertiesFactoryBean.class; + } + + @Override + protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + Properties parsedProps = parserContext.getDelegate().parsePropsElement(element); + builder.addPropertyValue("properties", parsedProps); + + String location = element.getAttribute("location"); + if (StringUtils.hasLength(location)) { + location = parserContext.getReaderContext().getEnvironment().resolvePlaceholders(location); + String[] locations = StringUtils.commaDelimitedListToStringArray(location); + builder.addPropertyValue("locations", locations); + } + + builder.addPropertyValue("ignoreResourceNotFound", + Boolean.valueOf(element.getAttribute("ignore-resource-not-found"))); + + builder.addPropertyValue("localOverride", + Boolean.valueOf(element.getAttribute("local-override"))); + + String scope = element.getAttribute(SCOPE_ATTRIBUTE); + if (StringUtils.hasLength(scope)) { + builder.setScope(scope); + } + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanDefinitionReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanDefinitionReader.java new file mode 100644 index 0000000..589208a --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanDefinitionReader.java @@ -0,0 +1,554 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashSet; +import java.util.Set; + +import javax.xml.parsers.ParserConfigurationException; + +import org.w3c.dom.Document; +import org.xml.sax.EntityResolver; +import org.xml.sax.ErrorHandler; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.parsing.EmptyReaderEventListener; +import org.springframework.beans.factory.parsing.FailFastProblemReporter; +import org.springframework.beans.factory.parsing.NullSourceExtractor; +import org.springframework.beans.factory.parsing.ProblemReporter; +import org.springframework.beans.factory.parsing.ReaderEventListener; +import org.springframework.beans.factory.parsing.SourceExtractor; +import org.springframework.beans.factory.support.AbstractBeanDefinitionReader; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.core.Constants; +import org.springframework.core.NamedThreadLocal; +import org.springframework.core.io.DescriptiveResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.xml.SimpleSaxErrorHandler; +import org.springframework.util.xml.XmlValidationModeDetector; + +/** + * Bean definition reader for XML bean definitions. + * Delegates the actual XML document reading to an implementation + * of the {@link BeanDefinitionDocumentReader} interface. + * + *

    Typically applied to a + * {@link org.springframework.beans.factory.support.DefaultListableBeanFactory} + * or a {@link org.springframework.context.support.GenericApplicationContext}. + * + *

    This class loads a DOM document and applies the BeanDefinitionDocumentReader to it. + * The document reader will register each bean definition with the given bean factory, + * talking to the latter's implementation of the + * {@link org.springframework.beans.factory.support.BeanDefinitionRegistry} interface. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @author Chris Beams + * @since 26.11.2003 + * @see #setDocumentReaderClass + * @see BeanDefinitionDocumentReader + * @see DefaultBeanDefinitionDocumentReader + * @see BeanDefinitionRegistry + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory + * @see org.springframework.context.support.GenericApplicationContext + */ +public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader { + + /** + * Indicates that the validation should be disabled. + */ + public static final int VALIDATION_NONE = XmlValidationModeDetector.VALIDATION_NONE; + + /** + * Indicates that the validation mode should be detected automatically. + */ + public static final int VALIDATION_AUTO = XmlValidationModeDetector.VALIDATION_AUTO; + + /** + * Indicates that DTD validation should be used. + */ + public static final int VALIDATION_DTD = XmlValidationModeDetector.VALIDATION_DTD; + + /** + * Indicates that XSD validation should be used. + */ + public static final int VALIDATION_XSD = XmlValidationModeDetector.VALIDATION_XSD; + + + /** Constants instance for this class. */ + private static final Constants constants = new Constants(XmlBeanDefinitionReader.class); + + private int validationMode = VALIDATION_AUTO; + + private boolean namespaceAware = false; + + private Class documentReaderClass = + DefaultBeanDefinitionDocumentReader.class; + + private ProblemReporter problemReporter = new FailFastProblemReporter(); + + private ReaderEventListener eventListener = new EmptyReaderEventListener(); + + private SourceExtractor sourceExtractor = new NullSourceExtractor(); + + @Nullable + private NamespaceHandlerResolver namespaceHandlerResolver; + + private DocumentLoader documentLoader = new DefaultDocumentLoader(); + + @Nullable + private EntityResolver entityResolver; + + private ErrorHandler errorHandler = new SimpleSaxErrorHandler(logger); + + private final XmlValidationModeDetector validationModeDetector = new XmlValidationModeDetector(); + + private final ThreadLocal> resourcesCurrentlyBeingLoaded = + new NamedThreadLocal>("XML bean definition resources currently being loaded"){ + @Override + protected Set initialValue() { + return new HashSet<>(4); + } + }; + + + /** + * Create new XmlBeanDefinitionReader for the given bean factory. + * @param registry the BeanFactory to load bean definitions into, + * in the form of a BeanDefinitionRegistry + */ + public XmlBeanDefinitionReader(BeanDefinitionRegistry registry) { + super(registry); + } + + + /** + * Set whether to use XML validation. Default is {@code true}. + *

    This method switches namespace awareness on if validation is turned off, + * in order to still process schema namespaces properly in such a scenario. + * @see #setValidationMode + * @see #setNamespaceAware + */ + public void setValidating(boolean validating) { + this.validationMode = (validating ? VALIDATION_AUTO : VALIDATION_NONE); + this.namespaceAware = !validating; + } + + /** + * Set the validation mode to use by name. Defaults to {@link #VALIDATION_AUTO}. + * @see #setValidationMode + */ + public void setValidationModeName(String validationModeName) { + setValidationMode(constants.asNumber(validationModeName).intValue()); + } + + /** + * Set the validation mode to use. Defaults to {@link #VALIDATION_AUTO}. + *

    Note that this only activates or deactivates validation itself. + * If you are switching validation off for schema files, you might need to + * activate schema namespace support explicitly: see {@link #setNamespaceAware}. + */ + public void setValidationMode(int validationMode) { + this.validationMode = validationMode; + } + + /** + * Return the validation mode to use. + */ + public int getValidationMode() { + return this.validationMode; + } + + /** + * Set whether or not the XML parser should be XML namespace aware. + * Default is "false". + *

    This is typically not needed when schema validation is active. + * However, without validation, this has to be switched to "true" + * in order to properly process schema namespaces. + */ + public void setNamespaceAware(boolean namespaceAware) { + this.namespaceAware = namespaceAware; + } + + /** + * Return whether or not the XML parser should be XML namespace aware. + */ + public boolean isNamespaceAware() { + return this.namespaceAware; + } + + /** + * Specify which {@link org.springframework.beans.factory.parsing.ProblemReporter} to use. + *

    The default implementation is {@link org.springframework.beans.factory.parsing.FailFastProblemReporter} + * which exhibits fail fast behaviour. External tools can provide an alternative implementation + * that collates errors and warnings for display in the tool UI. + */ + public void setProblemReporter(@Nullable ProblemReporter problemReporter) { + this.problemReporter = (problemReporter != null ? problemReporter : new FailFastProblemReporter()); + } + + /** + * Specify which {@link ReaderEventListener} to use. + *

    The default implementation is EmptyReaderEventListener which discards every event notification. + * External tools can provide an alternative implementation to monitor the components being + * registered in the BeanFactory. + */ + public void setEventListener(@Nullable ReaderEventListener eventListener) { + this.eventListener = (eventListener != null ? eventListener : new EmptyReaderEventListener()); + } + + /** + * Specify the {@link SourceExtractor} to use. + *

    The default implementation is {@link NullSourceExtractor} which simply returns {@code null} + * as the source object. This means that - during normal runtime execution - + * no additional source metadata is attached to the bean configuration metadata. + */ + public void setSourceExtractor(@Nullable SourceExtractor sourceExtractor) { + this.sourceExtractor = (sourceExtractor != null ? sourceExtractor : new NullSourceExtractor()); + } + + /** + * Specify the {@link NamespaceHandlerResolver} to use. + *

    If none is specified, a default instance will be created through + * {@link #createDefaultNamespaceHandlerResolver()}. + */ + public void setNamespaceHandlerResolver(@Nullable NamespaceHandlerResolver namespaceHandlerResolver) { + this.namespaceHandlerResolver = namespaceHandlerResolver; + } + + /** + * Specify the {@link DocumentLoader} to use. + *

    The default implementation is {@link DefaultDocumentLoader} + * which loads {@link Document} instances using JAXP. + */ + public void setDocumentLoader(@Nullable DocumentLoader documentLoader) { + this.documentLoader = (documentLoader != null ? documentLoader : new DefaultDocumentLoader()); + } + + /** + * Set a SAX entity resolver to be used for parsing. + *

    By default, {@link ResourceEntityResolver} will be used. Can be overridden + * for custom entity resolution, for example relative to some specific base path. + */ + public void setEntityResolver(@Nullable EntityResolver entityResolver) { + this.entityResolver = entityResolver; + } + + /** + * Return the EntityResolver to use, building a default resolver + * if none specified. + */ + protected EntityResolver getEntityResolver() { + if (this.entityResolver == null) { + // Determine default EntityResolver to use. + ResourceLoader resourceLoader = getResourceLoader(); + if (resourceLoader != null) { + this.entityResolver = new ResourceEntityResolver(resourceLoader); + } + else { + this.entityResolver = new DelegatingEntityResolver(getBeanClassLoader()); + } + } + return this.entityResolver; + } + + /** + * Set an implementation of the {@code org.xml.sax.ErrorHandler} + * interface for custom handling of XML parsing errors and warnings. + *

    If not set, a default SimpleSaxErrorHandler is used that simply + * logs warnings using the logger instance of the view class, + * and rethrows errors to discontinue the XML transformation. + * @see SimpleSaxErrorHandler + */ + public void setErrorHandler(ErrorHandler errorHandler) { + this.errorHandler = errorHandler; + } + + /** + * Specify the {@link BeanDefinitionDocumentReader} implementation to use, + * responsible for the actual reading of the XML bean definition document. + *

    The default is {@link DefaultBeanDefinitionDocumentReader}. + * @param documentReaderClass the desired BeanDefinitionDocumentReader implementation class + */ + public void setDocumentReaderClass(Class documentReaderClass) { + this.documentReaderClass = documentReaderClass; + } + + + /** + * Load bean definitions from the specified XML file. + * @param resource the resource descriptor for the XML file + * @return the number of bean definitions found + * @throws BeanDefinitionStoreException in case of loading or parsing errors + */ + @Override + public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException { + return loadBeanDefinitions(new EncodedResource(resource)); + } + + /** + * Load bean definitions from the specified XML file. + * @param encodedResource the resource descriptor for the XML file, + * allowing to specify an encoding to use for parsing the file + * @return the number of bean definitions found + * @throws BeanDefinitionStoreException in case of loading or parsing errors + */ + public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException { + Assert.notNull(encodedResource, "EncodedResource must not be null"); + if (logger.isTraceEnabled()) { + logger.trace("Loading XML bean definitions from " + encodedResource); + } + + Set currentResources = this.resourcesCurrentlyBeingLoaded.get(); + + if (!currentResources.add(encodedResource)) { + throw new BeanDefinitionStoreException( + "Detected cyclic loading of " + encodedResource + " - check your import definitions!"); + } + + try (InputStream inputStream = encodedResource.getResource().getInputStream()) { + InputSource inputSource = new InputSource(inputStream); + if (encodedResource.getEncoding() != null) { + inputSource.setEncoding(encodedResource.getEncoding()); + } + return doLoadBeanDefinitions(inputSource, encodedResource.getResource()); + } + catch (IOException ex) { + throw new BeanDefinitionStoreException( + "IOException parsing XML document from " + encodedResource.getResource(), ex); + } + finally { + currentResources.remove(encodedResource); + if (currentResources.isEmpty()) { + this.resourcesCurrentlyBeingLoaded.remove(); + } + } + } + + /** + * Load bean definitions from the specified XML file. + * @param inputSource the SAX InputSource to read from + * @return the number of bean definitions found + * @throws BeanDefinitionStoreException in case of loading or parsing errors + */ + public int loadBeanDefinitions(InputSource inputSource) throws BeanDefinitionStoreException { + return loadBeanDefinitions(inputSource, "resource loaded through SAX InputSource"); + } + + /** + * Load bean definitions from the specified XML file. + * @param inputSource the SAX InputSource to read from + * @param resourceDescription a description of the resource + * (can be {@code null} or empty) + * @return the number of bean definitions found + * @throws BeanDefinitionStoreException in case of loading or parsing errors + */ + public int loadBeanDefinitions(InputSource inputSource, @Nullable String resourceDescription) + throws BeanDefinitionStoreException { + + return doLoadBeanDefinitions(inputSource, new DescriptiveResource(resourceDescription)); + } + + + /** + * Actually load bean definitions from the specified XML file. + * @param inputSource the SAX InputSource to read from + * @param resource the resource descriptor for the XML file + * @return the number of bean definitions found + * @throws BeanDefinitionStoreException in case of loading or parsing errors + * @see #doLoadDocument + * @see #registerBeanDefinitions + */ + protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) + throws BeanDefinitionStoreException { + + try { + Document doc = doLoadDocument(inputSource, resource); + int count = registerBeanDefinitions(doc, resource); + if (logger.isDebugEnabled()) { + logger.debug("Loaded " + count + " bean definitions from " + resource); + } + return count; + } + catch (BeanDefinitionStoreException ex) { + throw ex; + } + catch (SAXParseException ex) { + throw new XmlBeanDefinitionStoreException(resource.getDescription(), + "Line " + ex.getLineNumber() + " in XML document from " + resource + " is invalid", ex); + } + catch (SAXException ex) { + throw new XmlBeanDefinitionStoreException(resource.getDescription(), + "XML document from " + resource + " is invalid", ex); + } + catch (ParserConfigurationException ex) { + throw new BeanDefinitionStoreException(resource.getDescription(), + "Parser configuration exception parsing XML from " + resource, ex); + } + catch (IOException ex) { + throw new BeanDefinitionStoreException(resource.getDescription(), + "IOException parsing XML document from " + resource, ex); + } + catch (Throwable ex) { + throw new BeanDefinitionStoreException(resource.getDescription(), + "Unexpected exception parsing XML document from " + resource, ex); + } + } + + /** + * Actually load the specified document using the configured DocumentLoader. + * @param inputSource the SAX InputSource to read from + * @param resource the resource descriptor for the XML file + * @return the DOM Document + * @throws Exception when thrown from the DocumentLoader + * @see #setDocumentLoader + * @see DocumentLoader#loadDocument + */ + protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception { + return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler, + getValidationModeForResource(resource), isNamespaceAware()); + } + + /** + * Determine the validation mode for the specified {@link Resource}. + * If no explicit validation mode has been configured, then the validation + * mode gets {@link #detectValidationMode detected} from the given resource. + *

    Override this method if you would like full control over the validation + * mode, even when something other than {@link #VALIDATION_AUTO} was set. + * @see #detectValidationMode + */ + protected int getValidationModeForResource(Resource resource) { + int validationModeToUse = getValidationMode(); + if (validationModeToUse != VALIDATION_AUTO) { + return validationModeToUse; + } + int detectedMode = detectValidationMode(resource); + if (detectedMode != VALIDATION_AUTO) { + return detectedMode; + } + // Hmm, we didn't get a clear indication... Let's assume XSD, + // since apparently no DTD declaration has been found up until + // detection stopped (before finding the document's root tag). + return VALIDATION_XSD; + } + + /** + * Detect which kind of validation to perform on the XML file identified + * by the supplied {@link Resource}. If the file has a {@code DOCTYPE} + * definition then DTD validation is used otherwise XSD validation is assumed. + *

    Override this method if you would like to customize resolution + * of the {@link #VALIDATION_AUTO} mode. + */ + protected int detectValidationMode(Resource resource) { + if (resource.isOpen()) { + throw new BeanDefinitionStoreException( + "Passed-in Resource [" + resource + "] contains an open stream: " + + "cannot determine validation mode automatically. Either pass in a Resource " + + "that is able to create fresh streams, or explicitly specify the validationMode " + + "on your XmlBeanDefinitionReader instance."); + } + + InputStream inputStream; + try { + inputStream = resource.getInputStream(); + } + catch (IOException ex) { + throw new BeanDefinitionStoreException( + "Unable to determine validation mode for [" + resource + "]: cannot open InputStream. " + + "Did you attempt to load directly from a SAX InputSource without specifying the " + + "validationMode on your XmlBeanDefinitionReader instance?", ex); + } + + try { + return this.validationModeDetector.detectValidationMode(inputStream); + } + catch (IOException ex) { + throw new BeanDefinitionStoreException("Unable to determine validation mode for [" + + resource + "]: an error occurred whilst reading from the InputStream.", ex); + } + } + + /** + * Register the bean definitions contained in the given DOM document. + * Called by {@code loadBeanDefinitions}. + *

    Creates a new instance of the parser class and invokes + * {@code registerBeanDefinitions} on it. + * @param doc the DOM document + * @param resource the resource descriptor (for context information) + * @return the number of bean definitions found + * @throws BeanDefinitionStoreException in case of parsing errors + * @see #loadBeanDefinitions + * @see #setDocumentReaderClass + * @see BeanDefinitionDocumentReader#registerBeanDefinitions + */ + public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException { + BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader(); + int countBefore = getRegistry().getBeanDefinitionCount(); + documentReader.registerBeanDefinitions(doc, createReaderContext(resource)); + return getRegistry().getBeanDefinitionCount() - countBefore; + } + + /** + * Create the {@link BeanDefinitionDocumentReader} to use for actually + * reading bean definitions from an XML document. + *

    The default implementation instantiates the specified "documentReaderClass". + * @see #setDocumentReaderClass + */ + protected BeanDefinitionDocumentReader createBeanDefinitionDocumentReader() { + return BeanUtils.instantiateClass(this.documentReaderClass); + } + + /** + * Create the {@link XmlReaderContext} to pass over to the document reader. + */ + public XmlReaderContext createReaderContext(Resource resource) { + return new XmlReaderContext(resource, this.problemReporter, this.eventListener, + this.sourceExtractor, this, getNamespaceHandlerResolver()); + } + + /** + * Lazily create a default NamespaceHandlerResolver, if not set before. + * @see #createDefaultNamespaceHandlerResolver() + */ + public NamespaceHandlerResolver getNamespaceHandlerResolver() { + if (this.namespaceHandlerResolver == null) { + this.namespaceHandlerResolver = createDefaultNamespaceHandlerResolver(); + } + return this.namespaceHandlerResolver; + } + + /** + * Create the default implementation of {@link NamespaceHandlerResolver} used if none is specified. + *

    The default implementation returns an instance of {@link DefaultNamespaceHandlerResolver}. + * @see DefaultNamespaceHandlerResolver#DefaultNamespaceHandlerResolver(ClassLoader) + */ + protected NamespaceHandlerResolver createDefaultNamespaceHandlerResolver() { + ClassLoader cl = (getResourceLoader() != null ? getResourceLoader().getClassLoader() : getBeanClassLoader()); + return new DefaultNamespaceHandlerResolver(cl); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanDefinitionStoreException.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanDefinitionStoreException.java new file mode 100644 index 0000000..9f55693 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanDefinitionStoreException.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +import org.springframework.beans.factory.BeanDefinitionStoreException; + +/** + * XML-specific BeanDefinitionStoreException subclass that wraps a + * {@link org.xml.sax.SAXException}, typically a {@link org.xml.sax.SAXParseException} + * which contains information about the error location. + * + * @author Juergen Hoeller + * @since 2.0.2 + * @see #getLineNumber() + * @see org.xml.sax.SAXParseException + */ +@SuppressWarnings("serial") +public class XmlBeanDefinitionStoreException extends BeanDefinitionStoreException { + + /** + * Create a new XmlBeanDefinitionStoreException. + * @param resourceDescription description of the resource that the bean definition came from + * @param msg the detail message (used as exception message as-is) + * @param cause the SAXException (typically a SAXParseException) root cause + * @see org.xml.sax.SAXParseException + */ + public XmlBeanDefinitionStoreException(String resourceDescription, String msg, SAXException cause) { + super(resourceDescription, msg, cause); + } + + /** + * Return the line number in the XML resource that failed. + * @return the line number if available (in case of a SAXParseException); -1 else + * @see org.xml.sax.SAXParseException#getLineNumber() + */ + public int getLineNumber() { + Throwable cause = getCause(); + if (cause instanceof SAXParseException) { + return ((SAXParseException) cause).getLineNumber(); + } + return -1; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanFactory.java new file mode 100644 index 0000000..b762a41 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanFactory.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.core.io.Resource; + +/** + * Convenience extension of {@link DefaultListableBeanFactory} that reads bean definitions + * from an XML document. Delegates to {@link XmlBeanDefinitionReader} underneath; effectively + * equivalent to using an XmlBeanDefinitionReader with a DefaultListableBeanFactory. + * + *

    The structure, element and attribute names of the required XML document + * are hard-coded in this class. (Of course a transform could be run if necessary + * to produce this format). "beans" doesn't need to be the root element of the XML + * document: This class will parse all bean definition elements in the XML file. + * + *

    This class registers each bean definition with the {@link DefaultListableBeanFactory} + * superclass, and relies on the latter's implementation of the {@link BeanFactory} interface. + * It supports singletons, prototypes, and references to either of these kinds of bean. + * See {@code "spring-beans-3.x.xsd"} (or historically, {@code "spring-beans-2.0.dtd"}) for + * details on options and configuration style. + * + *

    For advanced needs, consider using a {@link DefaultListableBeanFactory} with + * an {@link XmlBeanDefinitionReader}. The latter allows for reading from multiple XML + * resources and is highly configurable in its actual XML parsing behavior. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Chris Beams + * @since 15 April 2001 + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory + * @see XmlBeanDefinitionReader + * @deprecated as of Spring 3.1 in favor of {@link DefaultListableBeanFactory} and + * {@link XmlBeanDefinitionReader} + */ +@Deprecated +@SuppressWarnings({"serial", "all"}) +public class XmlBeanFactory extends DefaultListableBeanFactory { + + private final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this); + + + /** + * Create a new XmlBeanFactory with the given resource, + * which must be parsable using DOM. + * @param resource the XML resource to load bean definitions from + * @throws BeansException in case of loading or parsing errors + */ + public XmlBeanFactory(Resource resource) throws BeansException { + this(resource, null); + } + + /** + * Create a new XmlBeanFactory with the given input stream, + * which must be parsable using DOM. + * @param resource the XML resource to load bean definitions from + * @param parentBeanFactory parent bean factory + * @throws BeansException in case of loading or parsing errors + */ + public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException { + super(parentBeanFactory); + this.reader.loadBeanDefinitions(resource); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlReaderContext.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlReaderContext.java new file mode 100644 index 0000000..a0ca6d0 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlReaderContext.java @@ -0,0 +1,165 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import java.io.StringReader; + +import org.w3c.dom.Document; +import org.xml.sax.InputSource; + +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.parsing.ProblemReporter; +import org.springframework.beans.factory.parsing.ReaderContext; +import org.springframework.beans.factory.parsing.ReaderEventListener; +import org.springframework.beans.factory.parsing.SourceExtractor; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.core.env.Environment; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.lang.Nullable; + +/** + * Extension of {@link org.springframework.beans.factory.parsing.ReaderContext}, + * specific to use with an {@link XmlBeanDefinitionReader}. Provides access to the + * {@link NamespaceHandlerResolver} configured in the {@link XmlBeanDefinitionReader}. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +public class XmlReaderContext extends ReaderContext { + + private final XmlBeanDefinitionReader reader; + + private final NamespaceHandlerResolver namespaceHandlerResolver; + + + /** + * Construct a new {@code XmlReaderContext}. + * @param resource the XML bean definition resource + * @param problemReporter the problem reporter in use + * @param eventListener the event listener in use + * @param sourceExtractor the source extractor in use + * @param reader the XML bean definition reader in use + * @param namespaceHandlerResolver the XML namespace resolver + */ + public XmlReaderContext( + Resource resource, ProblemReporter problemReporter, + ReaderEventListener eventListener, SourceExtractor sourceExtractor, + XmlBeanDefinitionReader reader, NamespaceHandlerResolver namespaceHandlerResolver) { + + super(resource, problemReporter, eventListener, sourceExtractor); + this.reader = reader; + this.namespaceHandlerResolver = namespaceHandlerResolver; + } + + + /** + * Return the XML bean definition reader in use. + */ + public final XmlBeanDefinitionReader getReader() { + return this.reader; + } + + /** + * Return the bean definition registry to use. + * @see XmlBeanDefinitionReader#XmlBeanDefinitionReader(BeanDefinitionRegistry) + */ + public final BeanDefinitionRegistry getRegistry() { + return this.reader.getRegistry(); + } + + /** + * Return the resource loader to use, if any. + *

    This will be non-null in regular scenarios, + * also allowing access to the resource class loader. + * @see XmlBeanDefinitionReader#setResourceLoader + * @see ResourceLoader#getClassLoader() + */ + @Nullable + public final ResourceLoader getResourceLoader() { + return this.reader.getResourceLoader(); + } + + /** + * Return the bean class loader to use, if any. + *

    Note that this will be null in regular scenarios, + * as an indication to lazily resolve bean classes. + * @see XmlBeanDefinitionReader#setBeanClassLoader + */ + @Nullable + public final ClassLoader getBeanClassLoader() { + return this.reader.getBeanClassLoader(); + } + + /** + * Return the environment to use. + * @see XmlBeanDefinitionReader#setEnvironment + */ + public final Environment getEnvironment() { + return this.reader.getEnvironment(); + } + + /** + * Return the namespace resolver. + * @see XmlBeanDefinitionReader#setNamespaceHandlerResolver + */ + public final NamespaceHandlerResolver getNamespaceHandlerResolver() { + return this.namespaceHandlerResolver; + } + + + // Convenience methods to delegate to + + /** + * Call the bean name generator for the given bean definition. + * @see XmlBeanDefinitionReader#getBeanNameGenerator() + * @see org.springframework.beans.factory.support.BeanNameGenerator#generateBeanName + */ + public String generateBeanName(BeanDefinition beanDefinition) { + return this.reader.getBeanNameGenerator().generateBeanName(beanDefinition, getRegistry()); + } + + /** + * Call the bean name generator for the given bean definition + * and register the bean definition under the generated name. + * @see XmlBeanDefinitionReader#getBeanNameGenerator() + * @see org.springframework.beans.factory.support.BeanNameGenerator#generateBeanName + * @see BeanDefinitionRegistry#registerBeanDefinition + */ + public String registerWithGeneratedName(BeanDefinition beanDefinition) { + String generatedName = generateBeanName(beanDefinition); + getRegistry().registerBeanDefinition(generatedName, beanDefinition); + return generatedName; + } + + /** + * Read an XML document from the given String. + * @see #getReader() + */ + public Document readDocumentFromString(String documentContent) { + InputSource is = new InputSource(new StringReader(documentContent)); + try { + return this.reader.doLoadDocument(is, getResource()); + } + catch (Exception ex) { + throw new BeanDefinitionStoreException("Failed to read XML document", ex); + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/package-info.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/package-info.java new file mode 100644 index 0000000..3dcc0d4 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/package-info.java @@ -0,0 +1,10 @@ +/** + * Contains an abstract XML-based {@code BeanFactory} implementation, + * including a standard "spring-beans" XSD. + */ +@NonNullApi +@NonNullFields +package org.springframework.beans.factory.xml; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-beans/src/main/java/org/springframework/beans/package-info.java b/spring-beans/src/main/java/org/springframework/beans/package-info.java new file mode 100644 index 0000000..1bea8ae --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/package-info.java @@ -0,0 +1,17 @@ +/** + * This package contains interfaces and classes for manipulating Java beans. + * It is used by most other Spring packages. + * + *

    A BeanWrapper object may be used to set and get bean properties, + * singly or in bulk. + * + *

    The classes in this package are discussed in Chapter 11 of + * Expert One-On-One J2EE Design and Development + * by Rod Johnson (Wrox, 2002). + */ +@NonNullApi +@NonNullFields +package org.springframework.beans; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ByteArrayPropertyEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ByteArrayPropertyEditor.java new file mode 100644 index 0000000..14e4c4b --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ByteArrayPropertyEditor.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditorSupport; + +import org.springframework.lang.Nullable; + +/** + * Editor for byte arrays. Strings will simply be converted to + * their corresponding byte representations. + * + * @author Juergen Hoeller + * @since 1.0.1 + * @see java.lang.String#getBytes + */ +public class ByteArrayPropertyEditor extends PropertyEditorSupport { + + @Override + public void setAsText(@Nullable String text) { + setValue(text != null ? text.getBytes() : null); + } + + @Override + public String getAsText() { + byte[] value = (byte[]) getValue(); + return (value != null ? new String(value) : ""); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CharArrayPropertyEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CharArrayPropertyEditor.java new file mode 100644 index 0000000..705d58f --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CharArrayPropertyEditor.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditorSupport; + +import org.springframework.lang.Nullable; + +/** + * Editor for char arrays. Strings will simply be converted to + * their corresponding char representations. + * + * @author Juergen Hoeller + * @since 1.2.8 + * @see String#toCharArray() + */ +public class CharArrayPropertyEditor extends PropertyEditorSupport { + + @Override + public void setAsText(@Nullable String text) { + setValue(text != null ? text.toCharArray() : null); + } + + @Override + public String getAsText() { + char[] value = (char[]) getValue(); + return (value != null ? new String(value) : ""); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CharacterEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CharacterEditor.java new file mode 100644 index 0000000..ec7c7d4 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CharacterEditor.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditorSupport; + +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Editor for a {@link Character}, to populate a property + * of type {@code Character} or {@code char} from a String value. + * + *

    Note that the JDK does not contain a default + * {@link java.beans.PropertyEditor property editor} for {@code char}! + * {@link org.springframework.beans.BeanWrapperImpl} will register this + * editor by default. + * + *

    Also supports conversion from a Unicode character sequence; e.g. + * {@code u0041} ('A'). + * + * @author Juergen Hoeller + * @author Rob Harrop + * @author Rick Evans + * @since 1.2 + * @see Character + * @see org.springframework.beans.BeanWrapperImpl + */ +public class CharacterEditor extends PropertyEditorSupport { + + /** + * The prefix that identifies a string as being a Unicode character sequence. + */ + private static final String UNICODE_PREFIX = "\\u"; + + /** + * The length of a Unicode character sequence. + */ + private static final int UNICODE_LENGTH = 6; + + + private final boolean allowEmpty; + + + /** + * Create a new CharacterEditor instance. + *

    The "allowEmpty" parameter controls whether an empty String is to be + * allowed in parsing, i.e. be interpreted as the {@code null} value when + * {@link #setAsText(String) text is being converted}. If {@code false}, + * an {@link IllegalArgumentException} will be thrown at that time. + * @param allowEmpty if empty strings are to be allowed + */ + public CharacterEditor(boolean allowEmpty) { + this.allowEmpty = allowEmpty; + } + + + @Override + public void setAsText(@Nullable String text) throws IllegalArgumentException { + if (this.allowEmpty && !StringUtils.hasLength(text)) { + // Treat empty String as null value. + setValue(null); + } + else if (text == null) { + throw new IllegalArgumentException("null String cannot be converted to char type"); + } + else if (isUnicodeCharacterSequence(text)) { + setAsUnicode(text); + } + else if (text.length() == 1) { + setValue(text.charAt(0)); + } + else { + throw new IllegalArgumentException("String [" + text + "] with length " + + text.length() + " cannot be converted to char type: neither Unicode nor single character"); + } + } + + @Override + public String getAsText() { + Object value = getValue(); + return (value != null ? value.toString() : ""); + } + + + private boolean isUnicodeCharacterSequence(String sequence) { + return (sequence.startsWith(UNICODE_PREFIX) && sequence.length() == UNICODE_LENGTH); + } + + private void setAsUnicode(String text) { + int code = Integer.parseInt(text.substring(UNICODE_PREFIX.length()), 16); + setValue((char) code); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CharsetEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CharsetEditor.java new file mode 100644 index 0000000..adcb806 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CharsetEditor.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditorSupport; +import java.nio.charset.Charset; + +import org.springframework.util.StringUtils; + +/** + * Editor for {@code java.nio.charset.Charset}, translating charset + * String representations into Charset objects and back. + * + *

    Expects the same syntax as Charset's {@link java.nio.charset.Charset#name()}, + * e.g. {@code UTF-8}, {@code ISO-8859-16}, etc. + * + * @author Arjen Poutsma + * @since 2.5.4 + * @see Charset + */ +public class CharsetEditor extends PropertyEditorSupport { + + @Override + public void setAsText(String text) throws IllegalArgumentException { + if (StringUtils.hasText(text)) { + setValue(Charset.forName(text)); + } + else { + setValue(null); + } + } + + @Override + public String getAsText() { + Charset value = (Charset) getValue(); + return (value != null ? value.name() : ""); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ClassArrayEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ClassArrayEditor.java new file mode 100644 index 0000000..0a2882a --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ClassArrayEditor.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditorSupport; +import java.util.StringJoiner; + +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Property editor for an array of {@link Class Classes}, to enable + * the direct population of a {@code Class[]} property without having to + * use a {@code String} class name property as bridge. + * + *

    Also supports "java.lang.String[]"-style array class names, in contrast + * to the standard {@link Class#forName(String)} method. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +public class ClassArrayEditor extends PropertyEditorSupport { + + @Nullable + private final ClassLoader classLoader; + + + /** + * Create a default {@code ClassEditor}, using the thread + * context {@code ClassLoader}. + */ + public ClassArrayEditor() { + this(null); + } + + /** + * Create a default {@code ClassArrayEditor}, using the given + * {@code ClassLoader}. + * @param classLoader the {@code ClassLoader} to use + * (or pass {@code null} for the thread context {@code ClassLoader}) + */ + public ClassArrayEditor(@Nullable ClassLoader classLoader) { + this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader()); + } + + + @Override + public void setAsText(String text) throws IllegalArgumentException { + if (StringUtils.hasText(text)) { + String[] classNames = StringUtils.commaDelimitedListToStringArray(text); + Class[] classes = new Class[classNames.length]; + for (int i = 0; i < classNames.length; i++) { + String className = classNames[i].trim(); + classes[i] = ClassUtils.resolveClassName(className, this.classLoader); + } + setValue(classes); + } + else { + setValue(null); + } + } + + @Override + public String getAsText() { + Class[] classes = (Class[]) getValue(); + if (ObjectUtils.isEmpty(classes)) { + return ""; + } + StringJoiner sj = new StringJoiner(","); + for (Class klass : classes) { + sj.add(ClassUtils.getQualifiedName(klass)); + } + return sj.toString(); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ClassEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ClassEditor.java new file mode 100644 index 0000000..a68d498 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ClassEditor.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditorSupport; + +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * Property editor for {@link Class java.lang.Class}, to enable the direct + * population of a {@code Class} property without recourse to having to use a + * String class name property as bridge. + * + *

    Also supports "java.lang.String[]"-style array class names, in contrast to the + * standard {@link Class#forName(String)} method. + * + * @author Juergen Hoeller + * @author Rick Evans + * @since 13.05.2003 + * @see Class#forName + * @see org.springframework.util.ClassUtils#forName(String, ClassLoader) + */ +public class ClassEditor extends PropertyEditorSupport { + + @Nullable + private final ClassLoader classLoader; + + + /** + * Create a default ClassEditor, using the thread context ClassLoader. + */ + public ClassEditor() { + this(null); + } + + /** + * Create a default ClassEditor, using the given ClassLoader. + * @param classLoader the ClassLoader to use + * (or {@code null} for the thread context ClassLoader) + */ + public ClassEditor(@Nullable ClassLoader classLoader) { + this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader()); + } + + + @Override + public void setAsText(String text) throws IllegalArgumentException { + if (StringUtils.hasText(text)) { + setValue(ClassUtils.resolveClassName(text.trim(), this.classLoader)); + } + else { + setValue(null); + } + } + + @Override + public String getAsText() { + Class clazz = (Class) getValue(); + if (clazz != null) { + return ClassUtils.getQualifiedName(clazz); + } + else { + return ""; + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CurrencyEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CurrencyEditor.java new file mode 100644 index 0000000..0d044ff --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CurrencyEditor.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditorSupport; +import java.util.Currency; + +/** + * Editor for {@code java.util.Currency}, translating currency codes into Currency + * objects. Exposes the currency code as text representation of a Currency object. + * + * @author Juergen Hoeller + * @since 3.0 + * @see java.util.Currency + */ +public class CurrencyEditor extends PropertyEditorSupport { + + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue(Currency.getInstance(text)); + } + + @Override + public String getAsText() { + Currency value = (Currency) getValue(); + return (value != null ? value.getCurrencyCode() : ""); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomBooleanEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomBooleanEditor.java new file mode 100644 index 0000000..5d71fca --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomBooleanEditor.java @@ -0,0 +1,171 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditorSupport; + +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Property editor for Boolean/boolean properties. + * + *

    This is not meant to be used as system PropertyEditor but rather as + * locale-specific Boolean editor within custom controller code, to parse + * UI-caused boolean strings into boolean properties of beans and check + * them in the UI form. + * + *

    In web MVC code, this editor will typically be registered with + * {@code binder.registerCustomEditor} calls. + * + * @author Juergen Hoeller + * @since 10.06.2003 + * @see org.springframework.validation.DataBinder#registerCustomEditor + */ +public class CustomBooleanEditor extends PropertyEditorSupport { + + /** + * Value of {@code "true"}. + */ + public static final String VALUE_TRUE = "true"; + + /** + * Value of {@code "false"}. + */ + public static final String VALUE_FALSE = "false"; + + /** + * Value of {@code "on"}. + */ + public static final String VALUE_ON = "on"; + + /** + * Value of {@code "off"}. + */ + public static final String VALUE_OFF = "off"; + + /** + * Value of {@code "yes"}. + */ + public static final String VALUE_YES = "yes"; + + /** + * Value of {@code "no"}. + */ + public static final String VALUE_NO = "no"; + + /** + * Value of {@code "1"}. + */ + public static final String VALUE_1 = "1"; + + /** + * Value of {@code "0"}. + */ + public static final String VALUE_0 = "0"; + + + @Nullable + private final String trueString; + + @Nullable + private final String falseString; + + private final boolean allowEmpty; + + + /** + * Create a new CustomBooleanEditor instance, with "true"/"on"/"yes" + * and "false"/"off"/"no" as recognized String values. + *

    The "allowEmpty" parameter states if an empty String should + * be allowed for parsing, i.e. get interpreted as null value. + * Else, an IllegalArgumentException gets thrown in that case. + * @param allowEmpty if empty strings should be allowed + */ + public CustomBooleanEditor(boolean allowEmpty) { + this(null, null, allowEmpty); + } + + /** + * Create a new CustomBooleanEditor instance, + * with configurable String values for true and false. + *

    The "allowEmpty" parameter states if an empty String should + * be allowed for parsing, i.e. get interpreted as null value. + * Else, an IllegalArgumentException gets thrown in that case. + * @param trueString the String value that represents true: + * for example, "true" (VALUE_TRUE), "on" (VALUE_ON), + * "yes" (VALUE_YES) or some custom value + * @param falseString the String value that represents false: + * for example, "false" (VALUE_FALSE), "off" (VALUE_OFF), + * "no" (VALUE_NO) or some custom value + * @param allowEmpty if empty strings should be allowed + * @see #VALUE_TRUE + * @see #VALUE_FALSE + * @see #VALUE_ON + * @see #VALUE_OFF + * @see #VALUE_YES + * @see #VALUE_NO + */ + public CustomBooleanEditor(@Nullable String trueString, @Nullable String falseString, boolean allowEmpty) { + this.trueString = trueString; + this.falseString = falseString; + this.allowEmpty = allowEmpty; + } + + + @Override + public void setAsText(@Nullable String text) throws IllegalArgumentException { + String input = (text != null ? text.trim() : null); + if (this.allowEmpty && !StringUtils.hasLength(input)) { + // Treat empty String as null value. + setValue(null); + } + else if (this.trueString != null && this.trueString.equalsIgnoreCase(input)) { + setValue(Boolean.TRUE); + } + else if (this.falseString != null && this.falseString.equalsIgnoreCase(input)) { + setValue(Boolean.FALSE); + } + else if (this.trueString == null && + (VALUE_TRUE.equalsIgnoreCase(input) || VALUE_ON.equalsIgnoreCase(input) || + VALUE_YES.equalsIgnoreCase(input) || VALUE_1.equals(input))) { + setValue(Boolean.TRUE); + } + else if (this.falseString == null && + (VALUE_FALSE.equalsIgnoreCase(input) || VALUE_OFF.equalsIgnoreCase(input) || + VALUE_NO.equalsIgnoreCase(input) || VALUE_0.equals(input))) { + setValue(Boolean.FALSE); + } + else { + throw new IllegalArgumentException("Invalid boolean value [" + text + "]"); + } + } + + @Override + public String getAsText() { + if (Boolean.TRUE.equals(getValue())) { + return (this.trueString != null ? this.trueString : VALUE_TRUE); + } + else if (Boolean.FALSE.equals(getValue())) { + return (this.falseString != null ? this.falseString : VALUE_FALSE); + } + else { + return ""; + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomCollectionEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomCollectionEditor.java new file mode 100644 index 0000000..bcf3997 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomCollectionEditor.java @@ -0,0 +1,215 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditorSupport; +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +/** + * Property editor for Collections, converting any source Collection + * to a given target Collection type. + * + *

    By default registered for Set, SortedSet and List, + * to automatically convert any given Collection to one of those + * target types if the type does not match the target property. + * + * @author Juergen Hoeller + * @since 1.1.3 + * @see java.util.Collection + * @see java.util.Set + * @see java.util.SortedSet + * @see java.util.List + */ +public class CustomCollectionEditor extends PropertyEditorSupport { + + @SuppressWarnings("rawtypes") + private final Class collectionType; + + private final boolean nullAsEmptyCollection; + + + /** + * Create a new CustomCollectionEditor for the given target type, + * keeping an incoming {@code null} as-is. + * @param collectionType the target type, which needs to be a + * sub-interface of Collection or a concrete Collection class + * @see java.util.Collection + * @see java.util.ArrayList + * @see java.util.TreeSet + * @see java.util.LinkedHashSet + */ + @SuppressWarnings("rawtypes") + public CustomCollectionEditor(Class collectionType) { + this(collectionType, false); + } + + /** + * Create a new CustomCollectionEditor for the given target type. + *

    If the incoming value is of the given type, it will be used as-is. + * If it is a different Collection type or an array, it will be converted + * to a default implementation of the given Collection type. + * If the value is anything else, a target Collection with that single + * value will be created. + *

    The default Collection implementations are: ArrayList for List, + * TreeSet for SortedSet, and LinkedHashSet for Set. + * @param collectionType the target type, which needs to be a + * sub-interface of Collection or a concrete Collection class + * @param nullAsEmptyCollection whether to convert an incoming {@code null} + * value to an empty Collection (of the appropriate type) + * @see java.util.Collection + * @see java.util.ArrayList + * @see java.util.TreeSet + * @see java.util.LinkedHashSet + */ + @SuppressWarnings("rawtypes") + public CustomCollectionEditor(Class collectionType, boolean nullAsEmptyCollection) { + Assert.notNull(collectionType, "Collection type is required"); + if (!Collection.class.isAssignableFrom(collectionType)) { + throw new IllegalArgumentException( + "Collection type [" + collectionType.getName() + "] does not implement [java.util.Collection]"); + } + this.collectionType = collectionType; + this.nullAsEmptyCollection = nullAsEmptyCollection; + } + + + /** + * Convert the given text value to a Collection with a single element. + */ + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue(text); + } + + /** + * Convert the given value to a Collection of the target type. + */ + @Override + public void setValue(@Nullable Object value) { + if (value == null && this.nullAsEmptyCollection) { + super.setValue(createCollection(this.collectionType, 0)); + } + else if (value == null || (this.collectionType.isInstance(value) && !alwaysCreateNewCollection())) { + // Use the source value as-is, as it matches the target type. + super.setValue(value); + } + else if (value instanceof Collection) { + // Convert Collection elements. + Collection source = (Collection) value; + Collection target = createCollection(this.collectionType, source.size()); + for (Object elem : source) { + target.add(convertElement(elem)); + } + super.setValue(target); + } + else if (value.getClass().isArray()) { + // Convert array elements to Collection elements. + int length = Array.getLength(value); + Collection target = createCollection(this.collectionType, length); + for (int i = 0; i < length; i++) { + target.add(convertElement(Array.get(value, i))); + } + super.setValue(target); + } + else { + // A plain value: convert it to a Collection with a single element. + Collection target = createCollection(this.collectionType, 1); + target.add(convertElement(value)); + super.setValue(target); + } + } + + /** + * Create a Collection of the given type, with the given + * initial capacity (if supported by the Collection type). + * @param collectionType a sub-interface of Collection + * @param initialCapacity the initial capacity + * @return the new Collection instance + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + protected Collection createCollection(Class collectionType, int initialCapacity) { + if (!collectionType.isInterface()) { + try { + return ReflectionUtils.accessibleConstructor(collectionType).newInstance(); + } + catch (Throwable ex) { + throw new IllegalArgumentException( + "Could not instantiate collection class: " + collectionType.getName(), ex); + } + } + else if (List.class == collectionType) { + return new ArrayList<>(initialCapacity); + } + else if (SortedSet.class == collectionType) { + return new TreeSet<>(); + } + else { + return new LinkedHashSet<>(initialCapacity); + } + } + + /** + * Return whether to always create a new Collection, + * even if the type of the passed-in Collection already matches. + *

    Default is "false"; can be overridden to enforce creation of a + * new Collection, for example to convert elements in any case. + * @see #convertElement + */ + protected boolean alwaysCreateNewCollection() { + return false; + } + + /** + * Hook to convert each encountered Collection/array element. + * The default implementation simply returns the passed-in element as-is. + *

    Can be overridden to perform conversion of certain elements, + * for example String to Integer if a String array comes in and + * should be converted to a Set of Integer objects. + *

    Only called if actually creating a new Collection! + * This is by default not the case if the type of the passed-in Collection + * already matches. Override {@link #alwaysCreateNewCollection()} to + * enforce creating a new Collection in every case. + * @param element the source element + * @return the element to be used in the target Collection + * @see #alwaysCreateNewCollection() + */ + protected Object convertElement(Object element) { + return element; + } + + + /** + * This implementation returns {@code null} to indicate that + * there is no appropriate text representation. + */ + @Override + @Nullable + public String getAsText() { + return null; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomDateEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomDateEditor.java new file mode 100644 index 0000000..fcc3f82 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomDateEditor.java @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditorSupport; +import java.text.DateFormat; +import java.text.ParseException; +import java.util.Date; + +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Property editor for {@code java.util.Date}, + * supporting a custom {@code java.text.DateFormat}. + * + *

    This is not meant to be used as system PropertyEditor but rather + * as locale-specific date editor within custom controller code, + * parsing user-entered number strings into Date properties of beans + * and rendering them in the UI form. + * + *

    In web MVC code, this editor will typically be registered with + * {@code binder.registerCustomEditor}. + * + * @author Juergen Hoeller + * @since 28.04.2003 + * @see java.util.Date + * @see java.text.DateFormat + * @see org.springframework.validation.DataBinder#registerCustomEditor + */ +public class CustomDateEditor extends PropertyEditorSupport { + + private final DateFormat dateFormat; + + private final boolean allowEmpty; + + private final int exactDateLength; + + + /** + * Create a new CustomDateEditor instance, using the given DateFormat + * for parsing and rendering. + *

    The "allowEmpty" parameter states if an empty String should + * be allowed for parsing, i.e. get interpreted as null value. + * Otherwise, an IllegalArgumentException gets thrown in that case. + * @param dateFormat the DateFormat to use for parsing and rendering + * @param allowEmpty if empty strings should be allowed + */ + public CustomDateEditor(DateFormat dateFormat, boolean allowEmpty) { + this.dateFormat = dateFormat; + this.allowEmpty = allowEmpty; + this.exactDateLength = -1; + } + + /** + * Create a new CustomDateEditor instance, using the given DateFormat + * for parsing and rendering. + *

    The "allowEmpty" parameter states if an empty String should + * be allowed for parsing, i.e. get interpreted as null value. + * Otherwise, an IllegalArgumentException gets thrown in that case. + *

    The "exactDateLength" parameter states that IllegalArgumentException gets + * thrown if the String does not exactly match the length specified. This is useful + * because SimpleDateFormat does not enforce strict parsing of the year part, + * not even with {@code setLenient(false)}. Without an "exactDateLength" + * specified, the "01/01/05" would get parsed to "01/01/0005". However, even + * with an "exactDateLength" specified, prepended zeros in the day or month + * part may still allow for a shorter year part, so consider this as just + * one more assertion that gets you closer to the intended date format. + * @param dateFormat the DateFormat to use for parsing and rendering + * @param allowEmpty if empty strings should be allowed + * @param exactDateLength the exact expected length of the date String + */ + public CustomDateEditor(DateFormat dateFormat, boolean allowEmpty, int exactDateLength) { + this.dateFormat = dateFormat; + this.allowEmpty = allowEmpty; + this.exactDateLength = exactDateLength; + } + + + /** + * Parse the Date from the given text, using the specified DateFormat. + */ + @Override + public void setAsText(@Nullable String text) throws IllegalArgumentException { + if (this.allowEmpty && !StringUtils.hasText(text)) { + // Treat empty String as null value. + setValue(null); + } + else if (text != null && this.exactDateLength >= 0 && text.length() != this.exactDateLength) { + throw new IllegalArgumentException( + "Could not parse date: it is not exactly" + this.exactDateLength + "characters long"); + } + else { + try { + setValue(this.dateFormat.parse(text)); + } + catch (ParseException ex) { + throw new IllegalArgumentException("Could not parse date: " + ex.getMessage(), ex); + } + } + } + + /** + * Format the Date as String, using the specified DateFormat. + */ + @Override + public String getAsText() { + Date value = (Date) getValue(); + return (value != null ? this.dateFormat.format(value) : ""); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomMapEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomMapEditor.java new file mode 100644 index 0000000..a1e5ebd --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomMapEditor.java @@ -0,0 +1,205 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditorSupport; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +/** + * Property editor for Maps, converting any source Map + * to a given target Map type. + * + * @author Juergen Hoeller + * @since 2.0.1 + * @see java.util.Map + * @see java.util.SortedMap + */ +public class CustomMapEditor extends PropertyEditorSupport { + + @SuppressWarnings("rawtypes") + private final Class mapType; + + private final boolean nullAsEmptyMap; + + + /** + * Create a new CustomMapEditor for the given target type, + * keeping an incoming {@code null} as-is. + * @param mapType the target type, which needs to be a + * sub-interface of Map or a concrete Map class + * @see java.util.Map + * @see java.util.HashMap + * @see java.util.TreeMap + * @see java.util.LinkedHashMap + */ + @SuppressWarnings("rawtypes") + public CustomMapEditor(Class mapType) { + this(mapType, false); + } + + /** + * Create a new CustomMapEditor for the given target type. + *

    If the incoming value is of the given type, it will be used as-is. + * If it is a different Map type or an array, it will be converted + * to a default implementation of the given Map type. + * If the value is anything else, a target Map with that single + * value will be created. + *

    The default Map implementations are: TreeMap for SortedMap, + * and LinkedHashMap for Map. + * @param mapType the target type, which needs to be a + * sub-interface of Map or a concrete Map class + * @param nullAsEmptyMap ap whether to convert an incoming {@code null} + * value to an empty Map (of the appropriate type) + * @see java.util.Map + * @see java.util.TreeMap + * @see java.util.LinkedHashMap + */ + @SuppressWarnings("rawtypes") + public CustomMapEditor(Class mapType, boolean nullAsEmptyMap) { + Assert.notNull(mapType, "Map type is required"); + if (!Map.class.isAssignableFrom(mapType)) { + throw new IllegalArgumentException( + "Map type [" + mapType.getName() + "] does not implement [java.util.Map]"); + } + this.mapType = mapType; + this.nullAsEmptyMap = nullAsEmptyMap; + } + + + /** + * Convert the given text value to a Map with a single element. + */ + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue(text); + } + + /** + * Convert the given value to a Map of the target type. + */ + @Override + public void setValue(@Nullable Object value) { + if (value == null && this.nullAsEmptyMap) { + super.setValue(createMap(this.mapType, 0)); + } + else if (value == null || (this.mapType.isInstance(value) && !alwaysCreateNewMap())) { + // Use the source value as-is, as it matches the target type. + super.setValue(value); + } + else if (value instanceof Map) { + // Convert Map elements. + Map source = (Map) value; + Map target = createMap(this.mapType, source.size()); + source.forEach((key, val) -> target.put(convertKey(key), convertValue(val))); + super.setValue(target); + } + else { + throw new IllegalArgumentException("Value cannot be converted to Map: " + value); + } + } + + /** + * Create a Map of the given type, with the given + * initial capacity (if supported by the Map type). + * @param mapType a sub-interface of Map + * @param initialCapacity the initial capacity + * @return the new Map instance + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + protected Map createMap(Class mapType, int initialCapacity) { + if (!mapType.isInterface()) { + try { + return ReflectionUtils.accessibleConstructor(mapType).newInstance(); + } + catch (Throwable ex) { + throw new IllegalArgumentException( + "Could not instantiate map class: " + mapType.getName(), ex); + } + } + else if (SortedMap.class == mapType) { + return new TreeMap<>(); + } + else { + return new LinkedHashMap<>(initialCapacity); + } + } + + /** + * Return whether to always create a new Map, + * even if the type of the passed-in Map already matches. + *

    Default is "false"; can be overridden to enforce creation of a + * new Map, for example to convert elements in any case. + * @see #convertKey + * @see #convertValue + */ + protected boolean alwaysCreateNewMap() { + return false; + } + + /** + * Hook to convert each encountered Map key. + * The default implementation simply returns the passed-in key as-is. + *

    Can be overridden to perform conversion of certain keys, + * for example from String to Integer. + *

    Only called if actually creating a new Map! + * This is by default not the case if the type of the passed-in Map + * already matches. Override {@link #alwaysCreateNewMap()} to + * enforce creating a new Map in every case. + * @param key the source key + * @return the key to be used in the target Map + * @see #alwaysCreateNewMap + */ + protected Object convertKey(Object key) { + return key; + } + + /** + * Hook to convert each encountered Map value. + * The default implementation simply returns the passed-in value as-is. + *

    Can be overridden to perform conversion of certain values, + * for example from String to Integer. + *

    Only called if actually creating a new Map! + * This is by default not the case if the type of the passed-in Map + * already matches. Override {@link #alwaysCreateNewMap()} to + * enforce creating a new Map in every case. + * @param value the source value + * @return the value to be used in the target Map + * @see #alwaysCreateNewMap + */ + protected Object convertValue(Object value) { + return value; + } + + + /** + * This implementation returns {@code null} to indicate that + * there is no appropriate text representation. + */ + @Override + @Nullable + public String getAsText() { + return null; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomNumberEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomNumberEditor.java new file mode 100644 index 0000000..2d72d46 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomNumberEditor.java @@ -0,0 +1,151 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditorSupport; +import java.text.NumberFormat; + +import org.springframework.lang.Nullable; +import org.springframework.util.NumberUtils; +import org.springframework.util.StringUtils; + +/** + * Property editor for any Number subclass such as Short, Integer, Long, + * BigInteger, Float, Double, BigDecimal. Can use a given NumberFormat for + * (locale-specific) parsing and rendering, or alternatively the default + * {@code decode} / {@code valueOf} / {@code toString} methods. + * + *

    This is not meant to be used as system PropertyEditor but rather + * as locale-specific number editor within custom controller code, + * parsing user-entered number strings into Number properties of beans + * and rendering them in the UI form. + * + *

    In web MVC code, this editor will typically be registered with + * {@code binder.registerCustomEditor} calls. + * + * @author Juergen Hoeller + * @since 06.06.2003 + * @see Number + * @see java.text.NumberFormat + * @see org.springframework.validation.DataBinder#registerCustomEditor + */ +public class CustomNumberEditor extends PropertyEditorSupport { + + private final Class numberClass; + + @Nullable + private final NumberFormat numberFormat; + + private final boolean allowEmpty; + + + /** + * Create a new CustomNumberEditor instance, using the default + * {@code valueOf} methods for parsing and {@code toString} + * methods for rendering. + *

    The "allowEmpty" parameter states if an empty String should + * be allowed for parsing, i.e. get interpreted as {@code null} value. + * Else, an IllegalArgumentException gets thrown in that case. + * @param numberClass the Number subclass to generate + * @param allowEmpty if empty strings should be allowed + * @throws IllegalArgumentException if an invalid numberClass has been specified + * @see org.springframework.util.NumberUtils#parseNumber(String, Class) + * @see Integer#valueOf + * @see Integer#toString + */ + public CustomNumberEditor(Class numberClass, boolean allowEmpty) throws IllegalArgumentException { + this(numberClass, null, allowEmpty); + } + + /** + * Create a new CustomNumberEditor instance, using the given NumberFormat + * for parsing and rendering. + *

    The allowEmpty parameter states if an empty String should + * be allowed for parsing, i.e. get interpreted as {@code null} value. + * Else, an IllegalArgumentException gets thrown in that case. + * @param numberClass the Number subclass to generate + * @param numberFormat the NumberFormat to use for parsing and rendering + * @param allowEmpty if empty strings should be allowed + * @throws IllegalArgumentException if an invalid numberClass has been specified + * @see org.springframework.util.NumberUtils#parseNumber(String, Class, java.text.NumberFormat) + * @see java.text.NumberFormat#parse + * @see java.text.NumberFormat#format + */ + public CustomNumberEditor(Class numberClass, + @Nullable NumberFormat numberFormat, boolean allowEmpty) throws IllegalArgumentException { + + if (!Number.class.isAssignableFrom(numberClass)) { + throw new IllegalArgumentException("Property class must be a subclass of Number"); + } + this.numberClass = numberClass; + this.numberFormat = numberFormat; + this.allowEmpty = allowEmpty; + } + + + /** + * Parse the Number from the given text, using the specified NumberFormat. + */ + @Override + public void setAsText(String text) throws IllegalArgumentException { + if (this.allowEmpty && !StringUtils.hasText(text)) { + // Treat empty String as null value. + setValue(null); + } + else if (this.numberFormat != null) { + // Use given NumberFormat for parsing text. + setValue(NumberUtils.parseNumber(text, this.numberClass, this.numberFormat)); + } + else { + // Use default valueOf methods for parsing text. + setValue(NumberUtils.parseNumber(text, this.numberClass)); + } + } + + /** + * Coerce a Number value into the required target class, if necessary. + */ + @Override + public void setValue(@Nullable Object value) { + if (value instanceof Number) { + super.setValue(NumberUtils.convertNumberToTargetClass((Number) value, this.numberClass)); + } + else { + super.setValue(value); + } + } + + /** + * Format the Number as String, using the specified NumberFormat. + */ + @Override + public String getAsText() { + Object value = getValue(); + if (value == null) { + return ""; + } + if (this.numberFormat != null) { + // Use NumberFormat for rendering value. + return this.numberFormat.format(value); + } + else { + // Use toString method for rendering value. + return value.toString(); + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/FileEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/FileEditor.java new file mode 100644 index 0000000..f1b4432 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/FileEditor.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditorSupport; +import java.io.File; +import java.io.IOException; + +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceEditor; +import org.springframework.util.Assert; +import org.springframework.util.ResourceUtils; +import org.springframework.util.StringUtils; + +/** + * Editor for {@code java.io.File}, to directly populate a File property + * from a Spring resource location. + * + *

    Supports Spring-style URL notation: any fully qualified standard URL + * ("file:", "http:", etc) and Spring's special "classpath:" pseudo-URL. + * + *

    NOTE: The behavior of this editor has changed in Spring 2.0. + * Previously, it created a File instance directly from a filename. + * As of Spring 2.0, it takes a standard Spring resource location as input; + * this is consistent with URLEditor and InputStreamEditor now. + * + *

    NOTE: In Spring 2.5 the following modification was made. + * If a file name is specified without a URL prefix or without an absolute path + * then we try to locate the file using standard ResourceLoader semantics. + * If the file was not found, then a File instance is created assuming the file + * name refers to a relative file location. + * + * @author Juergen Hoeller + * @author Thomas Risberg + * @since 09.12.2003 + * @see java.io.File + * @see org.springframework.core.io.ResourceEditor + * @see org.springframework.core.io.ResourceLoader + * @see URLEditor + * @see InputStreamEditor + */ +public class FileEditor extends PropertyEditorSupport { + + private final ResourceEditor resourceEditor; + + + /** + * Create a new FileEditor, using a default ResourceEditor underneath. + */ + public FileEditor() { + this.resourceEditor = new ResourceEditor(); + } + + /** + * Create a new FileEditor, using the given ResourceEditor underneath. + * @param resourceEditor the ResourceEditor to use + */ + public FileEditor(ResourceEditor resourceEditor) { + Assert.notNull(resourceEditor, "ResourceEditor must not be null"); + this.resourceEditor = resourceEditor; + } + + + @Override + public void setAsText(String text) throws IllegalArgumentException { + if (!StringUtils.hasText(text)) { + setValue(null); + return; + } + + // Check whether we got an absolute file path without "file:" prefix. + // For backwards compatibility, we'll consider those as straight file path. + File file = null; + if (!ResourceUtils.isUrl(text)) { + file = new File(text); + if (file.isAbsolute()) { + setValue(file); + return; + } + } + + // Proceed with standard resource location parsing. + this.resourceEditor.setAsText(text); + Resource resource = (Resource) this.resourceEditor.getValue(); + + // If it's a URL or a path pointing to an existing resource, use it as-is. + if (file == null || resource.exists()) { + try { + setValue(resource.getFile()); + } + catch (IOException ex) { + throw new IllegalArgumentException( + "Could not retrieve file for " + resource + ": " + ex.getMessage()); + } + } + else { + // Set a relative File reference and hope for the best. + setValue(file); + } + } + + @Override + public String getAsText() { + File value = (File) getValue(); + return (value != null ? value.getPath() : ""); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/InputSourceEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/InputSourceEditor.java new file mode 100644 index 0000000..27de84f --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/InputSourceEditor.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditorSupport; +import java.io.IOException; + +import org.xml.sax.InputSource; + +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceEditor; +import org.springframework.util.Assert; + +/** + * Editor for {@code org.xml.sax.InputSource}, converting from a + * Spring resource location String to a SAX InputSource object. + * + *

    Supports Spring-style URL notation: any fully qualified standard URL + * ("file:", "http:", etc) and Spring's special "classpath:" pseudo-URL. + * + * @author Juergen Hoeller + * @since 3.0.3 + * @see org.xml.sax.InputSource + * @see org.springframework.core.io.ResourceEditor + * @see org.springframework.core.io.ResourceLoader + * @see URLEditor + * @see FileEditor + */ +public class InputSourceEditor extends PropertyEditorSupport { + + private final ResourceEditor resourceEditor; + + + /** + * Create a new InputSourceEditor, + * using the default ResourceEditor underneath. + */ + public InputSourceEditor() { + this.resourceEditor = new ResourceEditor(); + } + + /** + * Create a new InputSourceEditor, + * using the given ResourceEditor underneath. + * @param resourceEditor the ResourceEditor to use + */ + public InputSourceEditor(ResourceEditor resourceEditor) { + Assert.notNull(resourceEditor, "ResourceEditor must not be null"); + this.resourceEditor = resourceEditor; + } + + + @Override + public void setAsText(String text) throws IllegalArgumentException { + this.resourceEditor.setAsText(text); + Resource resource = (Resource) this.resourceEditor.getValue(); + try { + setValue(resource != null ? new InputSource(resource.getURL().toString()) : null); + } + catch (IOException ex) { + throw new IllegalArgumentException( + "Could not retrieve URL for " + resource + ": " + ex.getMessage()); + } + } + + @Override + public String getAsText() { + InputSource value = (InputSource) getValue(); + return (value != null ? value.getSystemId() : ""); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/InputStreamEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/InputStreamEditor.java new file mode 100644 index 0000000..fca24a5 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/InputStreamEditor.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditorSupport; +import java.io.IOException; + +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceEditor; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * One-way PropertyEditor which can convert from a text String to a + * {@code java.io.InputStream}, interpreting the given String as a + * Spring resource location (e.g. a URL String). + * + *

    Supports Spring-style URL notation: any fully qualified standard URL + * ("file:", "http:", etc.) and Spring's special "classpath:" pseudo-URL. + * + *

    Note that such streams usually do not get closed by Spring itself! + * + * @author Juergen Hoeller + * @since 1.0.1 + * @see java.io.InputStream + * @see org.springframework.core.io.ResourceEditor + * @see org.springframework.core.io.ResourceLoader + * @see URLEditor + * @see FileEditor + */ +public class InputStreamEditor extends PropertyEditorSupport { + + private final ResourceEditor resourceEditor; + + + /** + * Create a new InputStreamEditor, using the default ResourceEditor underneath. + */ + public InputStreamEditor() { + this.resourceEditor = new ResourceEditor(); + } + + /** + * Create a new InputStreamEditor, using the given ResourceEditor underneath. + * @param resourceEditor the ResourceEditor to use + */ + public InputStreamEditor(ResourceEditor resourceEditor) { + Assert.notNull(resourceEditor, "ResourceEditor must not be null"); + this.resourceEditor = resourceEditor; + } + + + @Override + public void setAsText(String text) throws IllegalArgumentException { + this.resourceEditor.setAsText(text); + Resource resource = (Resource) this.resourceEditor.getValue(); + try { + setValue(resource != null ? resource.getInputStream() : null); + } + catch (IOException ex) { + throw new IllegalArgumentException("Failed to retrieve InputStream for " + resource, ex); + } + } + + /** + * This implementation returns {@code null} to indicate that + * there is no appropriate text representation. + */ + @Override + @Nullable + public String getAsText() { + return null; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/LocaleEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/LocaleEditor.java new file mode 100644 index 0000000..29bf43a --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/LocaleEditor.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditorSupport; + +import org.springframework.util.StringUtils; + +/** + * Editor for {@code java.util.Locale}, to directly populate a Locale property. + * + *

    Expects the same syntax as Locale's {@code toString}, i.e. language + + * optionally country + optionally variant, separated by "_" (e.g. "en", "en_US"). + * Also accepts spaces as separators, as alternative to underscores. + * + * @author Juergen Hoeller + * @since 26.05.2003 + * @see java.util.Locale + * @see org.springframework.util.StringUtils#parseLocaleString + */ +public class LocaleEditor extends PropertyEditorSupport { + + @Override + public void setAsText(String text) { + setValue(StringUtils.parseLocaleString(text)); + } + + @Override + public String getAsText() { + Object value = getValue(); + return (value != null ? value.toString() : ""); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java new file mode 100644 index 0000000..f1edae0 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditorSupport; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.FileSystemNotFoundException; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceEditor; +import org.springframework.core.io.ResourceLoader; +import org.springframework.util.Assert; + +/** + * Editor for {@code java.nio.file.Path}, to directly populate a Path + * property instead of using a String property as bridge. + * + *

    Based on {@link Paths#get(URI)}'s resolution algorithm, checking + * registered NIO file system providers, including the default file system + * for "file:..." paths. Also supports Spring-style URL notation: any fully + * qualified standard URL and Spring's special "classpath:" pseudo-URL, as + * well as Spring's context-specific relative file paths. As a fallback, a + * path will be resolved in the file system via {@code Paths#get(String)} + * if no existing context-relative resource could be found. + * + * @author Juergen Hoeller + * @since 4.3.2 + * @see java.nio.file.Path + * @see Paths#get(URI) + * @see ResourceEditor + * @see org.springframework.core.io.ResourceLoader + * @see FileEditor + * @see URLEditor + */ +public class PathEditor extends PropertyEditorSupport { + + private final ResourceEditor resourceEditor; + + + /** + * Create a new PathEditor, using the default ResourceEditor underneath. + */ + public PathEditor() { + this.resourceEditor = new ResourceEditor(); + } + + /** + * Create a new PathEditor, using the given ResourceEditor underneath. + * @param resourceEditor the ResourceEditor to use + */ + public PathEditor(ResourceEditor resourceEditor) { + Assert.notNull(resourceEditor, "ResourceEditor must not be null"); + this.resourceEditor = resourceEditor; + } + + + @Override + public void setAsText(String text) throws IllegalArgumentException { + boolean nioPathCandidate = !text.startsWith(ResourceLoader.CLASSPATH_URL_PREFIX); + if (nioPathCandidate && !text.startsWith("/")) { + try { + URI uri = new URI(text); + if (uri.getScheme() != null) { + nioPathCandidate = false; + // Let's try NIO file system providers via Paths.get(URI) + setValue(Paths.get(uri).normalize()); + return; + } + } + catch (URISyntaxException | FileSystemNotFoundException ex) { + // Not a valid URI (let's try as Spring resource location), + // or a URI scheme not registered for NIO (let's try URL + // protocol handlers via Spring's resource mechanism). + } + } + + this.resourceEditor.setAsText(text); + Resource resource = (Resource) this.resourceEditor.getValue(); + if (resource == null) { + setValue(null); + } + else if (!resource.exists() && nioPathCandidate) { + setValue(Paths.get(text).normalize()); + } + else { + try { + setValue(resource.getFile().toPath()); + } + catch (IOException ex) { + throw new IllegalArgumentException("Failed to retrieve file for " + resource, ex); + } + } + } + + @Override + public String getAsText() { + Path value = (Path) getValue(); + return (value != null ? value.toString() : ""); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PatternEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PatternEditor.java new file mode 100644 index 0000000..03f14d1 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PatternEditor.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditorSupport; +import java.util.regex.Pattern; + +import org.springframework.lang.Nullable; + +/** + * Editor for {@code java.util.regex.Pattern}, to directly populate a Pattern property. + * Expects the same syntax as Pattern's {@code compile} method. + * + * @author Juergen Hoeller + * @since 2.0.1 + * @see java.util.regex.Pattern + * @see java.util.regex.Pattern#compile(String) + */ +public class PatternEditor extends PropertyEditorSupport { + + private final int flags; + + + /** + * Create a new PatternEditor with default settings. + */ + public PatternEditor() { + this.flags = 0; + } + + /** + * Create a new PatternEditor with the given settings. + * @param flags the {@code java.util.regex.Pattern} flags to apply + * @see java.util.regex.Pattern#compile(String, int) + * @see java.util.regex.Pattern#CASE_INSENSITIVE + * @see java.util.regex.Pattern#MULTILINE + * @see java.util.regex.Pattern#DOTALL + * @see java.util.regex.Pattern#UNICODE_CASE + * @see java.util.regex.Pattern#CANON_EQ + */ + public PatternEditor(int flags) { + this.flags = flags; + } + + + @Override + public void setAsText(@Nullable String text) { + setValue(text != null ? Pattern.compile(text, this.flags) : null); + } + + @Override + public String getAsText() { + Pattern value = (Pattern) getValue(); + return (value != null ? value.pattern() : ""); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PropertiesEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PropertiesEditor.java new file mode 100644 index 0000000..048193b --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PropertiesEditor.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditorSupport; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Properties; + +import org.springframework.lang.Nullable; + +/** + * Custom {@link java.beans.PropertyEditor} for {@link Properties} objects. + * + *

    Handles conversion from content {@link String} to {@code Properties} object. + * Also handles {@link Map} to {@code Properties} conversion, for populating + * a {@code Properties} object via XML "map" entries. + * + *

    The required format is defined in the standard {@code Properties} + * documentation. Each property must be on a new line. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see java.util.Properties#load + */ +public class PropertiesEditor extends PropertyEditorSupport { + + /** + * Convert {@link String} into {@link Properties}, considering it as + * properties content. + * @param text the text to be so converted + */ + @Override + public void setAsText(@Nullable String text) throws IllegalArgumentException { + Properties props = new Properties(); + if (text != null) { + try { + // Must use the ISO-8859-1 encoding because Properties.load(stream) expects it. + props.load(new ByteArrayInputStream(text.getBytes(StandardCharsets.ISO_8859_1))); + } + catch (IOException ex) { + // Should never happen. + throw new IllegalArgumentException( + "Failed to parse [" + text + "] into Properties", ex); + } + } + setValue(props); + } + + /** + * Take {@link Properties} as-is; convert {@link Map} into {@code Properties}. + */ + @Override + public void setValue(Object value) { + if (!(value instanceof Properties) && value instanceof Map) { + Properties props = new Properties(); + props.putAll((Map) value); + super.setValue(props); + } + else { + super.setValue(value); + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ReaderEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ReaderEditor.java new file mode 100644 index 0000000..a388932 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ReaderEditor.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditorSupport; +import java.io.IOException; + +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceEditor; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * One-way PropertyEditor which can convert from a text String to a + * {@code java.io.Reader}, interpreting the given String as a Spring + * resource location (e.g. a URL String). + * + *

    Supports Spring-style URL notation: any fully qualified standard URL + * ("file:", "http:", etc.) and Spring's special "classpath:" pseudo-URL. + * + *

    Note that such readers usually do not get closed by Spring itself! + * + * @author Juergen Hoeller + * @since 4.2 + * @see java.io.Reader + * @see org.springframework.core.io.ResourceEditor + * @see org.springframework.core.io.ResourceLoader + * @see InputStreamEditor + */ +public class ReaderEditor extends PropertyEditorSupport { + + private final ResourceEditor resourceEditor; + + + /** + * Create a new ReaderEditor, using the default ResourceEditor underneath. + */ + public ReaderEditor() { + this.resourceEditor = new ResourceEditor(); + } + + /** + * Create a new ReaderEditor, using the given ResourceEditor underneath. + * @param resourceEditor the ResourceEditor to use + */ + public ReaderEditor(ResourceEditor resourceEditor) { + Assert.notNull(resourceEditor, "ResourceEditor must not be null"); + this.resourceEditor = resourceEditor; + } + + + @Override + public void setAsText(String text) throws IllegalArgumentException { + this.resourceEditor.setAsText(text); + Resource resource = (Resource) this.resourceEditor.getValue(); + try { + setValue(resource != null ? new EncodedResource(resource).getReader() : null); + } + catch (IOException ex) { + throw new IllegalArgumentException("Failed to retrieve Reader for " + resource, ex); + } + } + + /** + * This implementation returns {@code null} to indicate that + * there is no appropriate text representation. + */ + @Override + @Nullable + public String getAsText() { + return null; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ResourceBundleEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ResourceBundleEditor.java new file mode 100644 index 0000000..632eba0 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ResourceBundleEditor.java @@ -0,0 +1,103 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditorSupport; +import java.util.Locale; +import java.util.ResourceBundle; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * {@link java.beans.PropertyEditor} implementation for standard JDK + * {@link java.util.ResourceBundle ResourceBundles}. + * + *

    Only supports conversion from a String, but not to a String. + * + * Find below some examples of using this class in a (properly configured) + * Spring container using XML-based metadata: + * + *

     <bean id="errorDialog" class="...">
    + *    <!--
    + *        the 'messages' property is of type java.util.ResourceBundle.
    + *        the 'DialogMessages.properties' file exists at the root of the CLASSPATH
    + *    -->
    + *    <property name="messages" value="DialogMessages"/>
    + * </bean>
    + * + *
     <bean id="errorDialog" class="...">
    + *    <!--
    + *        the 'DialogMessages.properties' file exists in the 'com/messages' package
    + *    -->
    + *    <property name="messages" value="com/messages/DialogMessages"/>
    + * </bean>
    + * + *

    A 'properly configured' Spring {@link org.springframework.context.ApplicationContext container} + * might contain a {@link org.springframework.beans.factory.config.CustomEditorConfigurer} + * definition such that the conversion can be effected transparently: + * + *

     <bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
    + *    <property name="customEditors">
    + *        <map>
    + *            <entry key="java.util.ResourceBundle">
    + *                <bean class="org.springframework.beans.propertyeditors.ResourceBundleEditor"/>
    + *            </entry>
    + *        </map>
    + *    </property>
    + * </bean>
    + * + *

    Please note that this {@link java.beans.PropertyEditor} is not + * registered by default with any of the Spring infrastructure. + * + *

    Thanks to David Leal Valmana for the suggestion and initial prototype. + * + * @author Rick Evans + * @author Juergen Hoeller + * @since 2.0 + */ +public class ResourceBundleEditor extends PropertyEditorSupport { + + /** + * The separator used to distinguish between the base name and the locale + * (if any) when {@link #setAsText(String) converting from a String}. + */ + public static final String BASE_NAME_SEPARATOR = "_"; + + + @Override + public void setAsText(String text) throws IllegalArgumentException { + Assert.hasText(text, "'text' must not be empty"); + String name = text.trim(); + + int separator = name.indexOf(BASE_NAME_SEPARATOR); + if (separator == -1) { + setValue(ResourceBundle.getBundle(name)); + } + else { + // The name potentially contains locale information + String baseName = name.substring(0, separator); + if (!StringUtils.hasText(baseName)) { + throw new IllegalArgumentException("Invalid ResourceBundle name: '" + text + "'"); + } + String localeString = name.substring(separator + 1); + Locale locale = StringUtils.parseLocaleString(localeString); + setValue(locale != null ? ResourceBundle.getBundle(baseName, locale) : ResourceBundle.getBundle(baseName)); + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/StringArrayPropertyEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/StringArrayPropertyEditor.java new file mode 100644 index 0000000..1a7a8cc --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/StringArrayPropertyEditor.java @@ -0,0 +1,147 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditorSupport; + +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Custom {@link java.beans.PropertyEditor} for String arrays. + * + *

    Strings must be in CSV format, with a customizable separator. + * By default values in the result are trimmed of whitespace. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Dave Syer + * @see org.springframework.util.StringUtils#delimitedListToStringArray + * @see org.springframework.util.StringUtils#arrayToDelimitedString + */ +public class StringArrayPropertyEditor extends PropertyEditorSupport { + + /** + * Default separator for splitting a String: a comma (","). + */ + public static final String DEFAULT_SEPARATOR = ","; + + + private final String separator; + + @Nullable + private final String charsToDelete; + + private final boolean emptyArrayAsNull; + + private final boolean trimValues; + + + /** + * Create a new {@code StringArrayPropertyEditor} with the default separator + * (a comma). + *

    An empty text (without elements) will be turned into an empty array. + */ + public StringArrayPropertyEditor() { + this(DEFAULT_SEPARATOR, null, false); + } + + /** + * Create a new {@code StringArrayPropertyEditor} with the given separator. + *

    An empty text (without elements) will be turned into an empty array. + * @param separator the separator to use for splitting a {@link String} + */ + public StringArrayPropertyEditor(String separator) { + this(separator, null, false); + } + + /** + * Create a new {@code StringArrayPropertyEditor} with the given separator. + * @param separator the separator to use for splitting a {@link String} + * @param emptyArrayAsNull {@code true} if an empty String array + * is to be transformed into {@code null} + */ + public StringArrayPropertyEditor(String separator, boolean emptyArrayAsNull) { + this(separator, null, emptyArrayAsNull); + } + + /** + * Create a new {@code StringArrayPropertyEditor} with the given separator. + * @param separator the separator to use for splitting a {@link String} + * @param emptyArrayAsNull {@code true} if an empty String array + * is to be transformed into {@code null} + * @param trimValues {@code true} if the values in the parsed arrays + * are to be trimmed of whitespace (default is true) + */ + public StringArrayPropertyEditor(String separator, boolean emptyArrayAsNull, boolean trimValues) { + this(separator, null, emptyArrayAsNull, trimValues); + } + + /** + * Create a new {@code StringArrayPropertyEditor} with the given separator. + * @param separator the separator to use for splitting a {@link String} + * @param charsToDelete a set of characters to delete, in addition to + * trimming an input String. Useful for deleting unwanted line breaks: + * e.g. "\r\n\f" will delete all new lines and line feeds in a String. + * @param emptyArrayAsNull {@code true} if an empty String array + * is to be transformed into {@code null} + */ + public StringArrayPropertyEditor(String separator, @Nullable String charsToDelete, boolean emptyArrayAsNull) { + this(separator, charsToDelete, emptyArrayAsNull, true); + } + + /** + * Create a new {@code StringArrayPropertyEditor} with the given separator. + * @param separator the separator to use for splitting a {@link String} + * @param charsToDelete a set of characters to delete, in addition to + * trimming an input String. Useful for deleting unwanted line breaks: + * e.g. "\r\n\f" will delete all new lines and line feeds in a String. + * @param emptyArrayAsNull {@code true} if an empty String array + * is to be transformed into {@code null} + * @param trimValues {@code true} if the values in the parsed arrays + * are to be trimmed of whitespace (default is true) + */ + public StringArrayPropertyEditor( + String separator, @Nullable String charsToDelete, boolean emptyArrayAsNull, boolean trimValues) { + + this.separator = separator; + this.charsToDelete = charsToDelete; + this.emptyArrayAsNull = emptyArrayAsNull; + this.trimValues = trimValues; + } + + @Override + public void setAsText(String text) throws IllegalArgumentException { + String[] array = StringUtils.delimitedListToStringArray(text, this.separator, this.charsToDelete); + if (this.emptyArrayAsNull && array.length == 0) { + setValue(null); + } + else { + if (this.trimValues) { + array = StringUtils.trimArrayElements(array); + } + setValue(array); + } + } + + @Override + public String getAsText() { + return StringUtils.arrayToDelimitedString(ObjectUtils.toObjectArray(getValue()), this.separator); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/StringTrimmerEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/StringTrimmerEditor.java new file mode 100644 index 0000000..0fbbfd3 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/StringTrimmerEditor.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditorSupport; + +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Property editor that trims Strings. + * + *

    Optionally allows transforming an empty string into a {@code null} value. + * Needs to be explicitly registered, e.g. for command binding. + * + * @author Juergen Hoeller + * @see org.springframework.validation.DataBinder#registerCustomEditor + */ +public class StringTrimmerEditor extends PropertyEditorSupport { + + @Nullable + private final String charsToDelete; + + private final boolean emptyAsNull; + + + /** + * Create a new StringTrimmerEditor. + * @param emptyAsNull {@code true} if an empty String is to be + * transformed into {@code null} + */ + public StringTrimmerEditor(boolean emptyAsNull) { + this.charsToDelete = null; + this.emptyAsNull = emptyAsNull; + } + + /** + * Create a new StringTrimmerEditor. + * @param charsToDelete a set of characters to delete, in addition to + * trimming an input String. Useful for deleting unwanted line breaks: + * e.g. "\r\n\f" will delete all new lines and line feeds in a String. + * @param emptyAsNull {@code true} if an empty String is to be + * transformed into {@code null} + */ + public StringTrimmerEditor(String charsToDelete, boolean emptyAsNull) { + this.charsToDelete = charsToDelete; + this.emptyAsNull = emptyAsNull; + } + + + @Override + public void setAsText(@Nullable String text) { + if (text == null) { + setValue(null); + } + else { + String value = text.trim(); + if (this.charsToDelete != null) { + value = StringUtils.deleteAny(value, this.charsToDelete); + } + if (this.emptyAsNull && value.isEmpty()) { + setValue(null); + } + else { + setValue(value); + } + } + } + + @Override + public String getAsText() { + Object value = getValue(); + return (value != null ? value.toString() : ""); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/TimeZoneEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/TimeZoneEditor.java new file mode 100644 index 0000000..6b80916 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/TimeZoneEditor.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditorSupport; +import java.util.TimeZone; + +import org.springframework.util.StringUtils; + +/** + * Editor for {@code java.util.TimeZone}, translating timezone IDs into + * {@code TimeZone} objects. Exposes the {@code TimeZone} ID as a text + * representation. + * + * @author Juergen Hoeller + * @author Nicholas Williams + * @since 3.0 + * @see java.util.TimeZone + * @see ZoneIdEditor + */ +public class TimeZoneEditor extends PropertyEditorSupport { + + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue(StringUtils.parseTimeZoneString(text)); + } + + @Override + public String getAsText() { + TimeZone value = (TimeZone) getValue(); + return (value != null ? value.getID() : ""); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/URIEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/URIEditor.java new file mode 100644 index 0000000..344fb5d --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/URIEditor.java @@ -0,0 +1,160 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditorSupport; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.ResourceUtils; +import org.springframework.util.StringUtils; + +/** + * Editor for {@code java.net.URI}, to directly populate a URI property + * instead of using a String property as bridge. + * + *

    Supports Spring-style URI notation: any fully qualified standard URI + * ("file:", "http:", etc) and Spring's special "classpath:" pseudo-URL, + * which will be resolved to a corresponding URI. + * + *

    By default, this editor will encode Strings into URIs. For instance, + * a space will be encoded into {@code %20}. This behavior can be changed + * by calling the {@link #URIEditor(boolean)} constructor. + * + *

    Note: A URI is more relaxed than a URL in that it does not require + * a valid protocol to be specified. Any scheme within a valid URI syntax + * is allowed, even without a matching protocol handler being registered. + * + * @author Juergen Hoeller + * @since 2.0.2 + * @see java.net.URI + * @see URLEditor + */ +public class URIEditor extends PropertyEditorSupport { + + @Nullable + private final ClassLoader classLoader; + + private final boolean encode; + + + + /** + * Create a new, encoding URIEditor, converting "classpath:" locations into + * standard URIs (not trying to resolve them into physical resources). + */ + public URIEditor() { + this(true); + } + + /** + * Create a new URIEditor, converting "classpath:" locations into + * standard URIs (not trying to resolve them into physical resources). + * @param encode indicates whether Strings will be encoded or not + * @since 3.0 + */ + public URIEditor(boolean encode) { + this.classLoader = null; + this.encode = encode; + } + + /** + * Create a new URIEditor, using the given ClassLoader to resolve + * "classpath:" locations into physical resource URLs. + * @param classLoader the ClassLoader to use for resolving "classpath:" locations + * (may be {@code null} to indicate the default ClassLoader) + */ + public URIEditor(@Nullable ClassLoader classLoader) { + this(classLoader, true); + } + + /** + * Create a new URIEditor, using the given ClassLoader to resolve + * "classpath:" locations into physical resource URLs. + * @param classLoader the ClassLoader to use for resolving "classpath:" locations + * (may be {@code null} to indicate the default ClassLoader) + * @param encode indicates whether Strings will be encoded or not + * @since 3.0 + */ + public URIEditor(@Nullable ClassLoader classLoader, boolean encode) { + this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader()); + this.encode = encode; + } + + + @Override + public void setAsText(String text) throws IllegalArgumentException { + if (StringUtils.hasText(text)) { + String uri = text.trim(); + if (this.classLoader != null && uri.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX)) { + ClassPathResource resource = new ClassPathResource( + uri.substring(ResourceUtils.CLASSPATH_URL_PREFIX.length()), this.classLoader); + try { + setValue(resource.getURI()); + } + catch (IOException ex) { + throw new IllegalArgumentException("Could not retrieve URI for " + resource + ": " + ex.getMessage()); + } + } + else { + try { + setValue(createURI(uri)); + } + catch (URISyntaxException ex) { + throw new IllegalArgumentException("Invalid URI syntax: " + ex.getMessage()); + } + } + } + else { + setValue(null); + } + } + + /** + * Create a URI instance for the given user-specified String value. + *

    The default implementation encodes the value into a RFC-2396 compliant URI. + * @param value the value to convert into a URI instance + * @return the URI instance + * @throws java.net.URISyntaxException if URI conversion failed + */ + protected URI createURI(String value) throws URISyntaxException { + int colonIndex = value.indexOf(':'); + if (this.encode && colonIndex != -1) { + int fragmentIndex = value.indexOf('#', colonIndex + 1); + String scheme = value.substring(0, colonIndex); + String ssp = value.substring(colonIndex + 1, (fragmentIndex > 0 ? fragmentIndex : value.length())); + String fragment = (fragmentIndex > 0 ? value.substring(fragmentIndex + 1) : null); + return new URI(scheme, ssp, fragment); + } + else { + // not encoding or the value contains no scheme - fallback to default + return new URI(value); + } + } + + + @Override + public String getAsText() { + URI value = (URI) getValue(); + return (value != null ? value.toString() : ""); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/URLEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/URLEditor.java new file mode 100644 index 0000000..dba2f9c --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/URLEditor.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditorSupport; +import java.io.IOException; +import java.net.URL; + +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceEditor; +import org.springframework.util.Assert; + +/** + * Editor for {@code java.net.URL}, to directly populate a URL property + * instead of using a String property as bridge. + * + *

    Supports Spring-style URL notation: any fully qualified standard URL + * ("file:", "http:", etc) and Spring's special "classpath:" pseudo-URL, + * as well as Spring's context-specific relative file paths. + * + *

    Note: A URL must specify a valid protocol, else it will be rejected + * upfront. However, the target resource does not necessarily have to exist + * at the time of URL creation; this depends on the specific resource type. + * + * @author Juergen Hoeller + * @since 15.12.2003 + * @see java.net.URL + * @see org.springframework.core.io.ResourceEditor + * @see org.springframework.core.io.ResourceLoader + * @see FileEditor + * @see InputStreamEditor + */ +public class URLEditor extends PropertyEditorSupport { + + private final ResourceEditor resourceEditor; + + + /** + * Create a new URLEditor, using a default ResourceEditor underneath. + */ + public URLEditor() { + this.resourceEditor = new ResourceEditor(); + } + + /** + * Create a new URLEditor, using the given ResourceEditor underneath. + * @param resourceEditor the ResourceEditor to use + */ + public URLEditor(ResourceEditor resourceEditor) { + Assert.notNull(resourceEditor, "ResourceEditor must not be null"); + this.resourceEditor = resourceEditor; + } + + + @Override + public void setAsText(String text) throws IllegalArgumentException { + this.resourceEditor.setAsText(text); + Resource resource = (Resource) this.resourceEditor.getValue(); + try { + setValue(resource != null ? resource.getURL() : null); + } + catch (IOException ex) { + throw new IllegalArgumentException("Could not retrieve URL for " + resource + ": " + ex.getMessage()); + } + } + + @Override + public String getAsText() { + URL value = (URL) getValue(); + return (value != null ? value.toExternalForm() : ""); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/UUIDEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/UUIDEditor.java new file mode 100644 index 0000000..895c6cb --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/UUIDEditor.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditorSupport; +import java.util.UUID; + +import org.springframework.util.StringUtils; + +/** + * Editor for {@code java.util.UUID}, translating UUID + * String representations into UUID objects and back. + * + * @author Juergen Hoeller + * @since 3.0.1 + * @see java.util.UUID + */ +public class UUIDEditor extends PropertyEditorSupport { + + @Override + public void setAsText(String text) throws IllegalArgumentException { + if (StringUtils.hasText(text)) { + setValue(UUID.fromString(text.trim())); + } + else { + setValue(null); + } + } + + @Override + public String getAsText() { + UUID value = (UUID) getValue(); + return (value != null ? value.toString() : ""); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ZoneIdEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ZoneIdEditor.java new file mode 100644 index 0000000..912bdd5 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ZoneIdEditor.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditorSupport; +import java.time.ZoneId; + +/** + * Editor for {@code java.time.ZoneId}, translating zone ID Strings into {@code ZoneId} + * objects. Exposes the {@code TimeZone} ID as a text representation. + * + * @author Nicholas Williams + * @since 4.0 + * @see java.time.ZoneId + * @see TimeZoneEditor + */ +public class ZoneIdEditor extends PropertyEditorSupport { + + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue(ZoneId.of(text)); + } + + @Override + public String getAsText() { + ZoneId value = (ZoneId) getValue(); + return (value != null ? value.getId() : ""); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/package-info.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/package-info.java new file mode 100644 index 0000000..ddb64ff --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/package-info.java @@ -0,0 +1,14 @@ +/** + * Properties editors used to convert from String values to object + * types such as java.util.Properties. + * + *

    Some of these editors are registered automatically by BeanWrapperImpl. + * "CustomXxxEditor" classes are intended for manual registration in + * specific binding processes, as they are localized or the like. + */ +@NonNullApi +@NonNullFields +package org.springframework.beans.propertyeditors; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-beans/src/main/java/org/springframework/beans/support/ArgumentConvertingMethodInvoker.java b/spring-beans/src/main/java/org/springframework/beans/support/ArgumentConvertingMethodInvoker.java new file mode 100644 index 0000000..bdd72f2 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/support/ArgumentConvertingMethodInvoker.java @@ -0,0 +1,183 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.support; + +import java.beans.PropertyEditor; +import java.lang.reflect.Method; + +import org.springframework.beans.PropertyEditorRegistry; +import org.springframework.beans.SimpleTypeConverter; +import org.springframework.beans.TypeConverter; +import org.springframework.beans.TypeMismatchException; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.MethodInvoker; +import org.springframework.util.ReflectionUtils; + +/** + * Subclass of {@link MethodInvoker} that tries to convert the given + * arguments for the actual target method via a {@link TypeConverter}. + * + *

    Supports flexible argument conversions, in particular for + * invoking a specific overloaded method. + * + * @author Juergen Hoeller + * @since 1.1 + * @see org.springframework.beans.BeanWrapperImpl#convertIfNecessary + */ +public class ArgumentConvertingMethodInvoker extends MethodInvoker { + + @Nullable + private TypeConverter typeConverter; + + private boolean useDefaultConverter = true; + + + /** + * Set a TypeConverter to use for argument type conversion. + *

    Default is a {@link org.springframework.beans.SimpleTypeConverter}. + * Can be overridden with any TypeConverter implementation, typically + * a pre-configured SimpleTypeConverter or a BeanWrapperImpl instance. + * @see org.springframework.beans.SimpleTypeConverter + * @see org.springframework.beans.BeanWrapperImpl + */ + public void setTypeConverter(@Nullable TypeConverter typeConverter) { + this.typeConverter = typeConverter; + this.useDefaultConverter = (typeConverter == null); + } + + /** + * Return the TypeConverter used for argument type conversion. + *

    Can be cast to {@link org.springframework.beans.PropertyEditorRegistry} + * if direct access to the underlying PropertyEditors is desired + * (provided that the present TypeConverter actually implements the + * PropertyEditorRegistry interface). + */ + @Nullable + public TypeConverter getTypeConverter() { + if (this.typeConverter == null && this.useDefaultConverter) { + this.typeConverter = getDefaultTypeConverter(); + } + return this.typeConverter; + } + + /** + * Obtain the default TypeConverter for this method invoker. + *

    Called if no explicit TypeConverter has been specified. + * The default implementation builds a + * {@link org.springframework.beans.SimpleTypeConverter}. + * Can be overridden in subclasses. + */ + protected TypeConverter getDefaultTypeConverter() { + return new SimpleTypeConverter(); + } + + /** + * Register the given custom property editor for all properties of the given type. + *

    Typically used in conjunction with the default + * {@link org.springframework.beans.SimpleTypeConverter}; will work with any + * TypeConverter that implements the PropertyEditorRegistry interface as well. + * @param requiredType type of the property + * @param propertyEditor editor to register + * @see #setTypeConverter + * @see org.springframework.beans.PropertyEditorRegistry#registerCustomEditor + */ + public void registerCustomEditor(Class requiredType, PropertyEditor propertyEditor) { + TypeConverter converter = getTypeConverter(); + if (!(converter instanceof PropertyEditorRegistry)) { + throw new IllegalStateException( + "TypeConverter does not implement PropertyEditorRegistry interface: " + converter); + } + ((PropertyEditorRegistry) converter).registerCustomEditor(requiredType, propertyEditor); + } + + + /** + * This implementation looks for a method with matching parameter types. + * @see #doFindMatchingMethod + */ + @Override + protected Method findMatchingMethod() { + Method matchingMethod = super.findMatchingMethod(); + // Second pass: look for method where arguments can be converted to parameter types. + if (matchingMethod == null) { + // Interpret argument array as individual method arguments. + matchingMethod = doFindMatchingMethod(getArguments()); + } + if (matchingMethod == null) { + // Interpret argument array as single method argument of array type. + matchingMethod = doFindMatchingMethod(new Object[] {getArguments()}); + } + return matchingMethod; + } + + /** + * Actually find a method with matching parameter type, i.e. where each + * argument value is assignable to the corresponding parameter type. + * @param arguments the argument values to match against method parameters + * @return a matching method, or {@code null} if none + */ + @Nullable + protected Method doFindMatchingMethod(Object[] arguments) { + TypeConverter converter = getTypeConverter(); + if (converter != null) { + String targetMethod = getTargetMethod(); + Method matchingMethod = null; + int argCount = arguments.length; + Class targetClass = getTargetClass(); + Assert.state(targetClass != null, "No target class set"); + Method[] candidates = ReflectionUtils.getAllDeclaredMethods(targetClass); + int minTypeDiffWeight = Integer.MAX_VALUE; + Object[] argumentsToUse = null; + for (Method candidate : candidates) { + if (candidate.getName().equals(targetMethod)) { + // Check if the inspected method has the correct number of parameters. + int parameterCount = candidate.getParameterCount(); + if (parameterCount == argCount) { + Class[] paramTypes = candidate.getParameterTypes(); + Object[] convertedArguments = new Object[argCount]; + boolean match = true; + for (int j = 0; j < argCount && match; j++) { + // Verify that the supplied argument is assignable to the method parameter. + try { + convertedArguments[j] = converter.convertIfNecessary(arguments[j], paramTypes[j]); + } + catch (TypeMismatchException ex) { + // Ignore -> simply doesn't match. + match = false; + } + } + if (match) { + int typeDiffWeight = getTypeDifferenceWeight(paramTypes, convertedArguments); + if (typeDiffWeight < minTypeDiffWeight) { + minTypeDiffWeight = typeDiffWeight; + matchingMethod = candidate; + argumentsToUse = convertedArguments; + } + } + } + } + } + if (matchingMethod != null) { + setArguments(argumentsToUse); + return matchingMethod; + } + } + return null; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/support/MutableSortDefinition.java b/spring-beans/src/main/java/org/springframework/beans/support/MutableSortDefinition.java new file mode 100644 index 0000000..2a428fc --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/support/MutableSortDefinition.java @@ -0,0 +1,179 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.support; + +import java.io.Serializable; + +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Mutable implementation of the {@link SortDefinition} interface. + * Supports toggling the ascending value on setting the same property again. + * + * @author Juergen Hoeller + * @author Jean-Pierre Pawlak + * @since 26.05.2003 + * @see #setToggleAscendingOnProperty + */ +@SuppressWarnings("serial") +public class MutableSortDefinition implements SortDefinition, Serializable { + + private String property = ""; + + private boolean ignoreCase = true; + + private boolean ascending = true; + + private boolean toggleAscendingOnProperty = false; + + + /** + * Create an empty MutableSortDefinition, + * to be populated via its bean properties. + * @see #setProperty + * @see #setIgnoreCase + * @see #setAscending + */ + public MutableSortDefinition() { + } + + /** + * Copy constructor: create a new MutableSortDefinition + * that mirrors the given sort definition. + * @param source the original sort definition + */ + public MutableSortDefinition(SortDefinition source) { + this.property = source.getProperty(); + this.ignoreCase = source.isIgnoreCase(); + this.ascending = source.isAscending(); + } + + /** + * Create a MutableSortDefinition for the given settings. + * @param property the property to compare + * @param ignoreCase whether upper and lower case in String values should be ignored + * @param ascending whether to sort ascending (true) or descending (false) + */ + public MutableSortDefinition(String property, boolean ignoreCase, boolean ascending) { + this.property = property; + this.ignoreCase = ignoreCase; + this.ascending = ascending; + } + + /** + * Create a new MutableSortDefinition. + * @param toggleAscendingOnSameProperty whether to toggle the ascending flag + * if the same property gets set again (that is, {@code setProperty} gets + * called with already set property name again). + */ + public MutableSortDefinition(boolean toggleAscendingOnSameProperty) { + this.toggleAscendingOnProperty = toggleAscendingOnSameProperty; + } + + + /** + * Set the property to compare. + *

    If the property was the same as the current, the sort is reversed if + * "toggleAscendingOnProperty" is activated, else simply ignored. + * @see #setToggleAscendingOnProperty + */ + public void setProperty(String property) { + if (!StringUtils.hasLength(property)) { + this.property = ""; + } + else { + // Implicit toggling of ascending? + if (isToggleAscendingOnProperty()) { + this.ascending = (!property.equals(this.property) || !this.ascending); + } + this.property = property; + } + } + + @Override + public String getProperty() { + return this.property; + } + + /** + * Set whether upper and lower case in String values should be ignored. + */ + public void setIgnoreCase(boolean ignoreCase) { + this.ignoreCase = ignoreCase; + } + + @Override + public boolean isIgnoreCase() { + return this.ignoreCase; + } + + /** + * Set whether to sort ascending (true) or descending (false). + */ + public void setAscending(boolean ascending) { + this.ascending = ascending; + } + + @Override + public boolean isAscending() { + return this.ascending; + } + + /** + * Set whether to toggle the ascending flag if the same property gets set again + * (that is, {@link #setProperty} gets called with already set property name again). + *

    This is particularly useful for parameter binding through a web request, + * where clicking on the field header again might be supposed to trigger a + * resort for the same field but opposite order. + */ + public void setToggleAscendingOnProperty(boolean toggleAscendingOnProperty) { + this.toggleAscendingOnProperty = toggleAscendingOnProperty; + } + + /** + * Return whether to toggle the ascending flag if the same property gets set again + * (that is, {@code setProperty} gets called with already set property name again). + */ + public boolean isToggleAscendingOnProperty() { + return this.toggleAscendingOnProperty; + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof SortDefinition)) { + return false; + } + SortDefinition otherSd = (SortDefinition) other; + return (getProperty().equals(otherSd.getProperty()) && + isAscending() == otherSd.isAscending() && + isIgnoreCase() == otherSd.isIgnoreCase()); + } + + @Override + public int hashCode() { + int hashCode = getProperty().hashCode(); + hashCode = 29 * hashCode + (isIgnoreCase() ? 1 : 0); + hashCode = 29 * hashCode + (isAscending() ? 1 : 0); + return hashCode; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/support/PagedListHolder.java b/spring-beans/src/main/java/org/springframework/beans/support/PagedListHolder.java new file mode 100644 index 0000000..063834e --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/support/PagedListHolder.java @@ -0,0 +1,347 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.support; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * PagedListHolder is a simple state holder for handling lists of objects, + * separating them into pages. Page numbering starts with 0. + * + *

    This is mainly targeted at usage in web UIs. Typically, an instance will be + * instantiated with a list of beans, put into the session, and exported as model. + * The properties can all be set/get programmatically, but the most common way will + * be data binding, i.e. populating the bean from request parameters. The getters + * will mainly be used by the view. + * + *

    Supports sorting the underlying list via a {@link SortDefinition} implementation, + * available as property "sort". By default, a {@link MutableSortDefinition} instance + * will be used, toggling the ascending value on setting the same property again. + * + *

    The data binding names have to be called "pageSize" and "sort.ascending", + * as expected by BeanWrapper. Note that the names and the nesting syntax match + * the respective JSTL EL expressions, like "myModelAttr.pageSize" and + * "myModelAttr.sort.ascending". + * + * @author Juergen Hoeller + * @since 19.05.2003 + * @param the element type + * @see #getPageList() + * @see org.springframework.beans.support.MutableSortDefinition + */ +@SuppressWarnings("serial") +public class PagedListHolder implements Serializable { + + /** + * The default page size. + */ + public static final int DEFAULT_PAGE_SIZE = 10; + + /** + * The default maximum number of page links. + */ + public static final int DEFAULT_MAX_LINKED_PAGES = 10; + + + private List source = Collections.emptyList(); + + @Nullable + private Date refreshDate; + + @Nullable + private SortDefinition sort; + + @Nullable + private SortDefinition sortUsed; + + private int pageSize = DEFAULT_PAGE_SIZE; + + private int page = 0; + + private boolean newPageSet; + + private int maxLinkedPages = DEFAULT_MAX_LINKED_PAGES; + + + /** + * Create a new holder instance. + * You'll need to set a source list to be able to use the holder. + * @see #setSource + */ + public PagedListHolder() { + this(new ArrayList<>(0)); + } + + /** + * Create a new holder instance with the given source list, starting with + * a default sort definition (with "toggleAscendingOnProperty" activated). + * @param source the source List + * @see MutableSortDefinition#setToggleAscendingOnProperty + */ + public PagedListHolder(List source) { + this(source, new MutableSortDefinition(true)); + } + + /** + * Create a new holder instance with the given source list. + * @param source the source List + * @param sort the SortDefinition to start with + */ + public PagedListHolder(List source, SortDefinition sort) { + setSource(source); + setSort(sort); + } + + + /** + * Set the source list for this holder. + */ + public void setSource(List source) { + Assert.notNull(source, "Source List must not be null"); + this.source = source; + this.refreshDate = new Date(); + this.sortUsed = null; + } + + /** + * Return the source list for this holder. + */ + public List getSource() { + return this.source; + } + + /** + * Return the last time the list has been fetched from the source provider. + */ + @Nullable + public Date getRefreshDate() { + return this.refreshDate; + } + + /** + * Set the sort definition for this holder. + * Typically an instance of MutableSortDefinition. + * @see org.springframework.beans.support.MutableSortDefinition + */ + public void setSort(@Nullable SortDefinition sort) { + this.sort = sort; + } + + /** + * Return the sort definition for this holder. + */ + @Nullable + public SortDefinition getSort() { + return this.sort; + } + + /** + * Set the current page size. + * Resets the current page number if changed. + *

    Default value is 10. + */ + public void setPageSize(int pageSize) { + if (pageSize != this.pageSize) { + this.pageSize = pageSize; + if (!this.newPageSet) { + this.page = 0; + } + } + } + + /** + * Return the current page size. + */ + public int getPageSize() { + return this.pageSize; + } + + /** + * Set the current page number. + * Page numbering starts with 0. + */ + public void setPage(int page) { + this.page = page; + this.newPageSet = true; + } + + /** + * Return the current page number. + * Page numbering starts with 0. + */ + public int getPage() { + this.newPageSet = false; + if (this.page >= getPageCount()) { + this.page = getPageCount() - 1; + } + return this.page; + } + + /** + * Set the maximum number of page links to a few pages around the current one. + */ + public void setMaxLinkedPages(int maxLinkedPages) { + this.maxLinkedPages = maxLinkedPages; + } + + /** + * Return the maximum number of page links to a few pages around the current one. + */ + public int getMaxLinkedPages() { + return this.maxLinkedPages; + } + + + /** + * Return the number of pages for the current source list. + */ + public int getPageCount() { + float nrOfPages = (float) getNrOfElements() / getPageSize(); + return (int) ((nrOfPages > (int) nrOfPages || nrOfPages == 0.0) ? nrOfPages + 1 : nrOfPages); + } + + /** + * Return if the current page is the first one. + */ + public boolean isFirstPage() { + return getPage() == 0; + } + + /** + * Return if the current page is the last one. + */ + public boolean isLastPage() { + return getPage() == getPageCount() -1; + } + + /** + * Switch to previous page. + * Will stay on first page if already on first page. + */ + public void previousPage() { + if (!isFirstPage()) { + this.page--; + } + } + + /** + * Switch to next page. + * Will stay on last page if already on last page. + */ + public void nextPage() { + if (!isLastPage()) { + this.page++; + } + } + + /** + * Return the total number of elements in the source list. + */ + public int getNrOfElements() { + return getSource().size(); + } + + /** + * Return the element index of the first element on the current page. + * Element numbering starts with 0. + */ + public int getFirstElementOnPage() { + return (getPageSize() * getPage()); + } + + /** + * Return the element index of the last element on the current page. + * Element numbering starts with 0. + */ + public int getLastElementOnPage() { + int endIndex = getPageSize() * (getPage() + 1); + int size = getNrOfElements(); + return (endIndex > size ? size : endIndex) - 1; + } + + /** + * Return a sub-list representing the current page. + */ + public List getPageList() { + return getSource().subList(getFirstElementOnPage(), getLastElementOnPage() + 1); + } + + /** + * Return the first page to which create a link around the current page. + */ + public int getFirstLinkedPage() { + return Math.max(0, getPage() - (getMaxLinkedPages() / 2)); + } + + /** + * Return the last page to which create a link around the current page. + */ + public int getLastLinkedPage() { + return Math.min(getFirstLinkedPage() + getMaxLinkedPages() - 1, getPageCount() - 1); + } + + + /** + * Resort the list if necessary, i.e. if the current {@code sort} instance + * isn't equal to the backed-up {@code sortUsed} instance. + *

    Calls {@code doSort} to trigger actual sorting. + * @see #doSort + */ + public void resort() { + SortDefinition sort = getSort(); + if (sort != null && !sort.equals(this.sortUsed)) { + this.sortUsed = copySortDefinition(sort); + doSort(getSource(), sort); + setPage(0); + } + } + + /** + * Create a deep copy of the given sort definition, + * for use as state holder to compare a modified sort definition against. + *

    Default implementation creates a MutableSortDefinition instance. + * Can be overridden in subclasses, in particular in case of custom + * extensions to the SortDefinition interface. Is allowed to return + * null, which means that no sort state will be held, triggering + * actual sorting for each {@code resort} call. + * @param sort the current SortDefinition object + * @return a deep copy of the SortDefinition object + * @see MutableSortDefinition#MutableSortDefinition(SortDefinition) + */ + protected SortDefinition copySortDefinition(SortDefinition sort) { + return new MutableSortDefinition(sort); + } + + /** + * Actually perform sorting of the given source list, according to + * the given sort definition. + *

    The default implementation uses Spring's PropertyComparator. + * Can be overridden in subclasses. + * @see PropertyComparator#sort(java.util.List, SortDefinition) + */ + protected void doSort(List source, SortDefinition sort) { + PropertyComparator.sort(source, sort); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/support/PropertyComparator.java b/spring-beans/src/main/java/org/springframework/beans/support/PropertyComparator.java new file mode 100644 index 0000000..43e927f --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/support/PropertyComparator.java @@ -0,0 +1,156 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.support; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeanWrapperImpl; +import org.springframework.beans.BeansException; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * PropertyComparator performs a comparison of two beans, + * evaluating the specified bean property via a BeanWrapper. + * + * @author Juergen Hoeller + * @author Jean-Pierre Pawlak + * @since 19.05.2003 + * @param the type of objects that may be compared by this comparator + * @see org.springframework.beans.BeanWrapper + */ +public class PropertyComparator implements Comparator { + + protected final Log logger = LogFactory.getLog(getClass()); + + private final SortDefinition sortDefinition; + + private final BeanWrapperImpl beanWrapper = new BeanWrapperImpl(false); + + + /** + * Create a new PropertyComparator for the given SortDefinition. + * @see MutableSortDefinition + */ + public PropertyComparator(SortDefinition sortDefinition) { + this.sortDefinition = sortDefinition; + } + + /** + * Create a PropertyComparator for the given settings. + * @param property the property to compare + * @param ignoreCase whether upper and lower case in String values should be ignored + * @param ascending whether to sort ascending (true) or descending (false) + */ + public PropertyComparator(String property, boolean ignoreCase, boolean ascending) { + this.sortDefinition = new MutableSortDefinition(property, ignoreCase, ascending); + } + + /** + * Return the SortDefinition that this comparator uses. + */ + public final SortDefinition getSortDefinition() { + return this.sortDefinition; + } + + + @Override + @SuppressWarnings("unchecked") + public int compare(T o1, T o2) { + Object v1 = getPropertyValue(o1); + Object v2 = getPropertyValue(o2); + if (this.sortDefinition.isIgnoreCase() && (v1 instanceof String) && (v2 instanceof String)) { + v1 = ((String) v1).toLowerCase(); + v2 = ((String) v2).toLowerCase(); + } + + int result; + + // Put an object with null property at the end of the sort result. + try { + if (v1 != null) { + result = (v2 != null ? ((Comparable) v1).compareTo(v2) : -1); + } + else { + result = (v2 != null ? 1 : 0); + } + } + catch (RuntimeException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Could not sort objects [" + o1 + "] and [" + o2 + "]", ex); + } + return 0; + } + + return (this.sortDefinition.isAscending() ? result : -result); + } + + /** + * Get the SortDefinition's property value for the given object. + * @param obj the object to get the property value for + * @return the property value + */ + @Nullable + private Object getPropertyValue(Object obj) { + // If a nested property cannot be read, simply return null + // (similar to JSTL EL). If the property doesn't exist in the + // first place, let the exception through. + try { + this.beanWrapper.setWrappedInstance(obj); + return this.beanWrapper.getPropertyValue(this.sortDefinition.getProperty()); + } + catch (BeansException ex) { + logger.debug("PropertyComparator could not access property - treating as null for sorting", ex); + return null; + } + } + + + /** + * Sort the given List according to the given sort definition. + *

    Note: Contained objects have to provide the given property + * in the form of a bean property, i.e. a getXXX method. + * @param source the input List + * @param sortDefinition the parameters to sort by + * @throws java.lang.IllegalArgumentException in case of a missing propertyName + */ + public static void sort(List source, SortDefinition sortDefinition) throws BeansException { + if (StringUtils.hasText(sortDefinition.getProperty())) { + source.sort(new PropertyComparator<>(sortDefinition)); + } + } + + /** + * Sort the given source according to the given sort definition. + *

    Note: Contained objects have to provide the given property + * in the form of a bean property, i.e. a getXXX method. + * @param source input source + * @param sortDefinition the parameters to sort by + * @throws java.lang.IllegalArgumentException in case of a missing propertyName + */ + public static void sort(Object[] source, SortDefinition sortDefinition) throws BeansException { + if (StringUtils.hasText(sortDefinition.getProperty())) { + Arrays.sort(source, new PropertyComparator<>(sortDefinition)); + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/support/ResourceEditorRegistrar.java b/spring-beans/src/main/java/org/springframework/beans/support/ResourceEditorRegistrar.java new file mode 100644 index 0000000..2865bea --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/support/ResourceEditorRegistrar.java @@ -0,0 +1,136 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.support; + +import java.beans.PropertyEditor; +import java.io.File; +import java.io.InputStream; +import java.io.Reader; +import java.net.URI; +import java.net.URL; +import java.nio.file.Path; + +import org.xml.sax.InputSource; + +import org.springframework.beans.PropertyEditorRegistrar; +import org.springframework.beans.PropertyEditorRegistry; +import org.springframework.beans.PropertyEditorRegistrySupport; +import org.springframework.beans.propertyeditors.ClassArrayEditor; +import org.springframework.beans.propertyeditors.ClassEditor; +import org.springframework.beans.propertyeditors.FileEditor; +import org.springframework.beans.propertyeditors.InputSourceEditor; +import org.springframework.beans.propertyeditors.InputStreamEditor; +import org.springframework.beans.propertyeditors.PathEditor; +import org.springframework.beans.propertyeditors.ReaderEditor; +import org.springframework.beans.propertyeditors.URIEditor; +import org.springframework.beans.propertyeditors.URLEditor; +import org.springframework.core.env.PropertyResolver; +import org.springframework.core.io.ContextResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceEditor; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.ResourceArrayPropertyEditor; +import org.springframework.core.io.support.ResourcePatternResolver; + +/** + * PropertyEditorRegistrar implementation that populates a given + * {@link org.springframework.beans.PropertyEditorRegistry} + * (typically a {@link org.springframework.beans.BeanWrapper} used for bean + * creation within an {@link org.springframework.context.ApplicationContext}) + * with resource editors. Used by + * {@link org.springframework.context.support.AbstractApplicationContext}. + * + * @author Juergen Hoeller + * @author Chris Beams + * @since 2.0 + */ +public class ResourceEditorRegistrar implements PropertyEditorRegistrar { + + private final PropertyResolver propertyResolver; + + private final ResourceLoader resourceLoader; + + + /** + * Create a new ResourceEditorRegistrar for the given {@link ResourceLoader} + * and {@link PropertyResolver}. + * @param resourceLoader the ResourceLoader (or ResourcePatternResolver) + * to create editors for (usually an ApplicationContext) + * @param propertyResolver the PropertyResolver (usually an Environment) + * @see org.springframework.core.env.Environment + * @see org.springframework.core.io.support.ResourcePatternResolver + * @see org.springframework.context.ApplicationContext + */ + public ResourceEditorRegistrar(ResourceLoader resourceLoader, PropertyResolver propertyResolver) { + this.resourceLoader = resourceLoader; + this.propertyResolver = propertyResolver; + } + + + /** + * Populate the given {@code registry} with the following resource editors: + * ResourceEditor, InputStreamEditor, InputSourceEditor, FileEditor, URLEditor, + * URIEditor, ClassEditor, ClassArrayEditor. + *

    If this registrar has been configured with a {@link ResourcePatternResolver}, + * a ResourceArrayPropertyEditor will be registered as well. + * @see org.springframework.core.io.ResourceEditor + * @see org.springframework.beans.propertyeditors.InputStreamEditor + * @see org.springframework.beans.propertyeditors.InputSourceEditor + * @see org.springframework.beans.propertyeditors.FileEditor + * @see org.springframework.beans.propertyeditors.URLEditor + * @see org.springframework.beans.propertyeditors.URIEditor + * @see org.springframework.beans.propertyeditors.ClassEditor + * @see org.springframework.beans.propertyeditors.ClassArrayEditor + * @see org.springframework.core.io.support.ResourceArrayPropertyEditor + */ + @Override + public void registerCustomEditors(PropertyEditorRegistry registry) { + ResourceEditor baseEditor = new ResourceEditor(this.resourceLoader, this.propertyResolver); + doRegisterEditor(registry, Resource.class, baseEditor); + doRegisterEditor(registry, ContextResource.class, baseEditor); + doRegisterEditor(registry, InputStream.class, new InputStreamEditor(baseEditor)); + doRegisterEditor(registry, InputSource.class, new InputSourceEditor(baseEditor)); + doRegisterEditor(registry, File.class, new FileEditor(baseEditor)); + doRegisterEditor(registry, Path.class, new PathEditor(baseEditor)); + doRegisterEditor(registry, Reader.class, new ReaderEditor(baseEditor)); + doRegisterEditor(registry, URL.class, new URLEditor(baseEditor)); + + ClassLoader classLoader = this.resourceLoader.getClassLoader(); + doRegisterEditor(registry, URI.class, new URIEditor(classLoader)); + doRegisterEditor(registry, Class.class, new ClassEditor(classLoader)); + doRegisterEditor(registry, Class[].class, new ClassArrayEditor(classLoader)); + + if (this.resourceLoader instanceof ResourcePatternResolver) { + doRegisterEditor(registry, Resource[].class, + new ResourceArrayPropertyEditor((ResourcePatternResolver) this.resourceLoader, this.propertyResolver)); + } + } + + /** + * Override default editor, if possible (since that's what we really mean to do here); + * otherwise register as a custom editor. + */ + private void doRegisterEditor(PropertyEditorRegistry registry, Class requiredType, PropertyEditor editor) { + if (registry instanceof PropertyEditorRegistrySupport) { + ((PropertyEditorRegistrySupport) registry).overrideDefaultEditor(requiredType, editor); + } + else { + registry.registerCustomEditor(requiredType, editor); + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/support/SortDefinition.java b/spring-beans/src/main/java/org/springframework/beans/support/SortDefinition.java new file mode 100644 index 0000000..e061a6b --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/support/SortDefinition.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.support; + +/** + * Definition for sorting bean instances by a property. + * + * @author Juergen Hoeller + * @since 26.05.2003 + */ +public interface SortDefinition { + + /** + * Return the name of the bean property to compare. + * Can also be a nested bean property path. + */ + String getProperty(); + + /** + * Return whether upper and lower case in String values should be ignored. + */ + boolean isIgnoreCase(); + + /** + * Return whether to sort ascending (true) or descending (false). + */ + boolean isAscending(); + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/support/package-info.java b/spring-beans/src/main/java/org/springframework/beans/support/package-info.java new file mode 100644 index 0000000..326ce25 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/support/package-info.java @@ -0,0 +1,10 @@ +/** + * Classes supporting the org.springframework.beans package, + * such as utility classes for sorting and holding lists of beans. + */ +@NonNullApi +@NonNullFields +package org.springframework.beans.support; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-beans/src/main/kotlin/org/springframework/beans/factory/BeanFactoryExtensions.kt b/spring-beans/src/main/kotlin/org/springframework/beans/factory/BeanFactoryExtensions.kt new file mode 100644 index 0000000..ed6f5a6 --- /dev/null +++ b/spring-beans/src/main/kotlin/org/springframework/beans/factory/BeanFactoryExtensions.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory + +import org.springframework.core.ParameterizedTypeReference +import org.springframework.core.ResolvableType + +/** + * Extension for [BeanFactory.getBean] providing a `getBean()` variant. + * + * @author Sebastien Deleuze + * @since 5.0 + */ +inline fun BeanFactory.getBean(): T = getBean(T::class.java) + +/** + * Extension for [BeanFactory.getBean] providing a `getBean("foo")` variant. + * + * @see BeanFactory.getBean(String, Class) + * @author Sebastien Deleuze + * @since 5.0 + */ +@Suppress("EXTENSION_SHADOWED_BY_MEMBER") +inline fun BeanFactory.getBean(name: String): T = + getBean(name, T::class.java) + +/** + * Extension for [BeanFactory.getBean] providing a `getBean(arg1, arg2)` variant. + * + * @see BeanFactory.getBean(Class, Object...) + * @author Sebastien Deleuze + * @since 5.0 + */ +inline fun BeanFactory.getBean(vararg args:Any): T = + getBean(T::class.java, *args) + +/** + * Extension for [BeanFactory.getBeanProvider] providing a `getBeanProvider()` variant. + * This extension is not subject to type erasure and retains actual generic type arguments. + * + * @see BeanFactory.getBeanProvider(ResolvableType) + * @author Sebastien Deleuze + * @since 5.1 + */ +inline fun BeanFactory.getBeanProvider(): ObjectProvider = + getBeanProvider(ResolvableType.forType((object : ParameterizedTypeReference() {}).type)) + diff --git a/spring-beans/src/main/kotlin/org/springframework/beans/factory/ListableBeanFactoryExtensions.kt b/spring-beans/src/main/kotlin/org/springframework/beans/factory/ListableBeanFactoryExtensions.kt new file mode 100644 index 0000000..174507e --- /dev/null +++ b/spring-beans/src/main/kotlin/org/springframework/beans/factory/ListableBeanFactoryExtensions.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory + +/** + * Extension for [ListableBeanFactory.getBeanNamesForType] providing a + * `getBeanNamesForType()` variant. + * + * @author Sebastien Deleuze + * @since 5.0 + */ +inline fun ListableBeanFactory.getBeanNamesForType(includeNonSingletons: Boolean = true, + allowEagerInit: Boolean = true): Array = + getBeanNamesForType(T::class.java, includeNonSingletons, allowEagerInit) + +/** + * Extension for [ListableBeanFactory.getBeansOfType] providing a `getBeansOfType()` variant. + * + * @author Sebastien Deleuze + * @since 5.0 + */ +inline fun ListableBeanFactory.getBeansOfType(includeNonSingletons: Boolean = true, + allowEagerInit: Boolean = true): Map = + getBeansOfType(T::class.java, includeNonSingletons, allowEagerInit) + +/** + * Extension for [ListableBeanFactory.getBeanNamesForAnnotation] providing a + * `getBeansOfType()` variant. + * + * @author Sebastien Deleuze + * @since 5.0 + */ +inline fun ListableBeanFactory.getBeanNamesForAnnotation(): Array = + getBeanNamesForAnnotation(T::class.java) + +/** + * Extension for [ListableBeanFactory.getBeansWithAnnotation] providing a + * `getBeansWithAnnotation()` variant. + * + * @author Sebastien Deleuze + * @since 5.0 + */ +inline fun ListableBeanFactory.getBeansWithAnnotation(): Map = + getBeansWithAnnotation(T::class.java) + +/** + * Extension for [ListableBeanFactory.findAnnotationOnBean] providing a + * `findAnnotationOnBean("foo")` variant. + * + * @author Sebastien Deleuze + * @since 5.0 + */ +inline fun ListableBeanFactory.findAnnotationOnBean(beanName:String): Annotation? = + findAnnotationOnBean(beanName, T::class.java) + diff --git a/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-beans.dtd b/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-beans.dtd new file mode 100644 index 0000000..42f487c --- /dev/null +++ b/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-beans.dtd @@ -0,0 +1,662 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-beans.gif b/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-beans.gif new file mode 100644 index 0000000..e89df3c Binary files /dev/null and b/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-beans.gif differ diff --git a/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-beans.xsd b/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-beans.xsd new file mode 100644 index 0000000..8532e96 --- /dev/null +++ b/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-beans.xsd @@ -0,0 +1,1201 @@ + + + + + + + + + + + + + + + + + + element. + ]]> + + + + + + + + and other elements, typically the root element in the document. + Allows the definition of default values for all nested bean definitions. May itself + be nested for the purpose of defining a subset of beans with certain default values or + to be registered only when certain profile(s) are active. Any such nested element + must be declared as the last element in the document. + ]]> + + + + + + + + + + + + + + + element should be parsed. Multiple profiles + can be separated by spaces, commas, or semi-colons. + + If one or more of the specified profiles are active at time of parsing, the + element will be parsed, and all of its elements registered, <import> + elements followed, etc. If none of the specified profiles are active at time of + parsing, then the entire element and its contents will be ignored. + + If a profile is prefixed with the NOT operator '!', e.g. + + + + indicates that the element should be parsed if profile "p1" is active or + if profile "p2" is not active. + + Profiles are activated in one of two ways: + Programmatic: + ConfigurableEnvironment#setActiveProfiles(String...) + ConfigurableEnvironment#setDefaultProfiles(String...) + + Properties (typically through -D system properties, environment variables, or + servlet context init params): + spring.profiles.active=p1,p2 + spring.profiles.default=p1,p2 + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + element (or "ref" + attribute). We recommend this in most cases as it makes documentation + more explicit. + + Note that this default mode also allows for annotation-driven autowiring, + if activated. "no" refers to externally driven autowiring only, not + affecting any autowiring demands that the bean class itself expresses. + + 2. "byName" + Autowiring by property name. If a bean of class Cat exposes a "dog" + property, Spring will try to set this to the value of the bean "dog" + in the current container. If there is no matching bean by name, nothing + special happens. + + 3. "byType" + Autowiring if there is exactly one bean of the property type in the + container. If there is more than one, a fatal error is raised, and + you cannot use byType autowiring for that bean. If there is none, + nothing special happens. + + 4. "constructor" + Analogous to "byType" for constructor arguments. If there is not exactly + one bean of the constructor argument type in the bean factory, a fatal + error is raised. + + Note that explicit dependencies, i.e. "property" and "constructor-arg" + elements, always override autowiring. + + Note: This attribute will not be inherited by child bean definitions. + Hence, it needs to be specified per concrete bean definition. It can be + shared through the 'default-autowire' attribute at the 'beans' level + and potentially inherited from outer 'beans' defaults in case of nested + 'beans' sections (e.g. with different profiles). + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + " element. + ]]> + + + + + ..." element. + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ". + ]]> + + + + + ..." element. + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ". + ]]> + + + + + ..." + element. + ]]> + + + + + ". + ]]> + + + + + ..." element. + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-tool.xsd b/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-tool.xsd new file mode 100644 index 0000000..9d84906 --- /dev/null +++ b/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-tool.xsd @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-util.gif b/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-util.gif new file mode 100644 index 0000000..7b44866 Binary files /dev/null and b/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-util.gif differ diff --git a/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-util.xsd b/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-util.xsd new file mode 100644 index 0000000..533a0c3 --- /dev/null +++ b/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-util.xsd @@ -0,0 +1,221 @@ + + + + + + + + + + + Reference a public, static field on a type and expose its value as + a bean. For example <util:constant static-field="java.lang.Integer.MAX_VALUE"/>. + + + + + + + + + + + + Reference a property on a bean (or as a nested value) and expose its values as + a bean. For example <util:property-path path="order.customer.name"/>. + + + + + + + + + + + + Builds a List instance of the specified type, populated with the specified content. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Builds a Set instance of the specified type, populated with the specified content. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Builds a Map instance of the specified type, populated with the specified content. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Loads a Properties instance from the resource location specified by the 'location' attribute. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java b/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java new file mode 100644 index 0000000..0bb5feb --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java @@ -0,0 +1,2129 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.beans.PropertyEditorSupport; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowire; +import org.springframework.beans.propertyeditors.StringArrayPropertyEditor; +import org.springframework.beans.propertyeditors.StringTrimmerEditor; +import org.springframework.beans.support.DerivedFromProtectedBaseBean; +import org.springframework.beans.testfixture.beans.BooleanTestBean; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.IndexedTestBean; +import org.springframework.beans.testfixture.beans.NumberTestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.within; + +/** + * Shared tests for property accessors. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Alef Arendsen + * @author Arjen Poutsma + * @author Chris Beams + * @author Dave Syer + * @author Stephane Nicoll + */ +public abstract class AbstractPropertyAccessorTests { + + protected abstract AbstractPropertyAccessor createAccessor(Object target); + + + @Test + public void createWithNullTarget() { + assertThatIllegalArgumentException().isThrownBy(() -> createAccessor(null)); + } + + @Test + public void isReadableProperty() { + AbstractPropertyAccessor accessor = createAccessor(new Simple("John", 2)); + + assertThat(accessor.isReadableProperty("name")).isTrue(); + } + + @Test + public void isReadablePropertyNotReadable() { + AbstractPropertyAccessor accessor = createAccessor(new NoRead()); + + assertThat(accessor.isReadableProperty("age")).isFalse(); + } + + /** + * Shouldn't throw an exception: should just return false + */ + @Test + public void isReadablePropertyNoSuchProperty() { + AbstractPropertyAccessor accessor = createAccessor(new NoRead()); + + assertThat(accessor.isReadableProperty("xxxxx")).isFalse(); + } + + @Test + public void isReadablePropertyNull() { + AbstractPropertyAccessor accessor = createAccessor(new NoRead()); + + assertThatIllegalArgumentException().isThrownBy(() -> accessor.isReadableProperty(null)); + } + + @Test + public void isWritableProperty() { + AbstractPropertyAccessor accessor = createAccessor(new Simple("John", 2)); + + assertThat(accessor.isWritableProperty("name")).isTrue(); + } + + @Test + public void isWritablePropertyNull() { + AbstractPropertyAccessor accessor = createAccessor(new NoRead()); + + assertThatIllegalArgumentException().isThrownBy(() -> accessor.isWritableProperty(null)); + } + + @Test + public void isWritablePropertyNoSuchProperty() { + AbstractPropertyAccessor accessor = createAccessor(new NoRead()); + + assertThat(accessor.isWritableProperty("xxxxx")).isFalse(); + } + + @Test + public void isReadableWritableForIndexedProperties() { + IndexedTestBean target = new IndexedTestBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + + assertThat(accessor.isReadableProperty("array")).isTrue(); + assertThat(accessor.isReadableProperty("list")).isTrue(); + assertThat(accessor.isReadableProperty("set")).isTrue(); + assertThat(accessor.isReadableProperty("map")).isTrue(); + assertThat(accessor.isReadableProperty("xxx")).isFalse(); + + assertThat(accessor.isWritableProperty("array")).isTrue(); + assertThat(accessor.isWritableProperty("list")).isTrue(); + assertThat(accessor.isWritableProperty("set")).isTrue(); + assertThat(accessor.isWritableProperty("map")).isTrue(); + assertThat(accessor.isWritableProperty("xxx")).isFalse(); + + assertThat(accessor.isReadableProperty("array[0]")).isTrue(); + assertThat(accessor.isReadableProperty("array[0].name")).isTrue(); + assertThat(accessor.isReadableProperty("list[0]")).isTrue(); + assertThat(accessor.isReadableProperty("list[0].name")).isTrue(); + assertThat(accessor.isReadableProperty("set[0]")).isTrue(); + assertThat(accessor.isReadableProperty("set[0].name")).isTrue(); + assertThat(accessor.isReadableProperty("map[key1]")).isTrue(); + assertThat(accessor.isReadableProperty("map[key1].name")).isTrue(); + assertThat(accessor.isReadableProperty("map[key4][0]")).isTrue(); + assertThat(accessor.isReadableProperty("map[key4][0].name")).isTrue(); + assertThat(accessor.isReadableProperty("map[key4][1]")).isTrue(); + assertThat(accessor.isReadableProperty("map[key4][1].name")).isTrue(); + assertThat(accessor.isReadableProperty("array[key1]")).isFalse(); + + assertThat(accessor.isWritableProperty("array[0]")).isTrue(); + assertThat(accessor.isWritableProperty("array[0].name")).isTrue(); + assertThat(accessor.isWritableProperty("list[0]")).isTrue(); + assertThat(accessor.isWritableProperty("list[0].name")).isTrue(); + assertThat(accessor.isWritableProperty("set[0]")).isTrue(); + assertThat(accessor.isWritableProperty("set[0].name")).isTrue(); + assertThat(accessor.isWritableProperty("map[key1]")).isTrue(); + assertThat(accessor.isWritableProperty("map[key1].name")).isTrue(); + assertThat(accessor.isWritableProperty("map[key4][0]")).isTrue(); + assertThat(accessor.isWritableProperty("map[key4][0].name")).isTrue(); + assertThat(accessor.isWritableProperty("map[key4][1]")).isTrue(); + assertThat(accessor.isWritableProperty("map[key4][1].name")).isTrue(); + assertThat(accessor.isWritableProperty("array[key1]")).isFalse(); + } + + @Test + public void getSimpleProperty() { + Simple target = new Simple("John", 2); + AbstractPropertyAccessor accessor = createAccessor(target); + assertThat(accessor.getPropertyValue("name")).isEqualTo("John"); + } + + @Test + public void getNestedProperty() { + Person target = createPerson("John", "London", "UK"); + AbstractPropertyAccessor accessor = createAccessor(target); + assertThat(accessor.getPropertyValue("address.city")).isEqualTo("London"); + } + + @Test + public void getNestedDeepProperty() { + Person target = createPerson("John", "London", "UK"); + AbstractPropertyAccessor accessor = createAccessor(target); + assertThat(accessor.getPropertyValue("address.country.name")).isEqualTo("UK"); + } + + @Test + public void getAnotherNestedDeepProperty() { + ITestBean target = new TestBean("rod", 31); + ITestBean kerry = new TestBean("kerry", 35); + target.setSpouse(kerry); + kerry.setSpouse(target); + AbstractPropertyAccessor accessor = createAccessor(target); + Integer KA = (Integer) accessor.getPropertyValue("spouse.age"); + assertThat(KA == 35).as("kerry is 35").isTrue(); + Integer RA = (Integer) accessor.getPropertyValue("spouse.spouse.age"); + assertThat(RA == 31).as("rod is 31, not" + RA).isTrue(); + ITestBean spousesSpouse = (ITestBean) accessor.getPropertyValue("spouse.spouse"); + assertThat(target == spousesSpouse).as("spousesSpouse = initial point").isTrue(); + } + + @Test + public void getPropertyIntermediatePropertyIsNull() { + Person target = createPerson("John", "London", "UK"); + target.address = null; + AbstractPropertyAccessor accessor = createAccessor(target); + assertThatExceptionOfType(NullValueInNestedPathException.class).isThrownBy(() -> + accessor.getPropertyValue("address.country.name")) + .satisfies(ex -> { + assertThat(ex.getPropertyName()).isEqualTo("address"); + assertThat(ex.getBeanClass()).isEqualTo(Person.class); + }); + } + + @Test + public void getPropertyIntermediatePropertyIsNullWithAutoGrow() { + Person target = createPerson("John", "London", "UK"); + target.address = null; + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.setAutoGrowNestedPaths(true); + + assertThat(accessor.getPropertyValue("address.country.name")).isEqualTo("DefaultCountry"); + } + + @Test + public void getPropertyIntermediateMapEntryIsNullWithAutoGrow() { + Foo target = new Foo(); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.setConversionService(new DefaultConversionService()); + accessor.setAutoGrowNestedPaths(true); + accessor.setPropertyValue("listOfMaps[0]['luckyNumber']", "9"); + assertThat(target.listOfMaps.get(0).get("luckyNumber")).isEqualTo("9"); + } + + @Test + public void getUnknownProperty() { + Simple target = new Simple("John", 2); + AbstractPropertyAccessor accessor = createAccessor(target); + assertThatExceptionOfType(NotReadablePropertyException.class).isThrownBy(() -> + accessor.getPropertyValue("foo")) + .satisfies(ex -> { + assertThat(ex.getBeanClass()).isEqualTo(Simple.class); + assertThat(ex.getPropertyName()).isEqualTo("foo"); + }); + } + + @Test + public void getUnknownNestedProperty() { + Person target = createPerson("John", "London", "UK"); + AbstractPropertyAccessor accessor = createAccessor(target); + + assertThatExceptionOfType(NotReadablePropertyException.class).isThrownBy(() -> + accessor.getPropertyValue("address.bar")); + } + + @Test + public void setSimpleProperty() { + Simple target = new Simple("John", 2); + AbstractPropertyAccessor accessor = createAccessor(target); + + accessor.setPropertyValue("name", "SomeValue"); + + assertThat(target.name).isEqualTo("SomeValue"); + assertThat(target.getName()).isEqualTo("SomeValue"); + } + + @Test + public void setNestedProperty() { + Person target = createPerson("John", "Paris", "FR"); + AbstractPropertyAccessor accessor = createAccessor(target); + + accessor.setPropertyValue("address.city", "London"); + assertThat(target.address.city).isEqualTo("London"); + } + + @Test + public void setNestedPropertyPolymorphic() throws Exception { + ITestBean target = new TestBean("rod", 31); + ITestBean kerry = new Employee(); + + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.setPropertyValue("spouse", kerry); + accessor.setPropertyValue("spouse.age", 35); + accessor.setPropertyValue("spouse.name", "Kerry"); + accessor.setPropertyValue("spouse.company", "Lewisham"); + assertThat(kerry.getName().equals("Kerry")).as("kerry name is Kerry").isTrue(); + + assertThat(target.getSpouse() == kerry).as("nested set worked").isTrue(); + assertThat(kerry.getSpouse() == null).as("no back relation").isTrue(); + accessor.setPropertyValue(new PropertyValue("spouse.spouse", target)); + assertThat(kerry.getSpouse() == target).as("nested set worked").isTrue(); + + AbstractPropertyAccessor kerryAccessor = createAccessor(kerry); + assertThat("Lewisham".equals(kerryAccessor.getPropertyValue("spouse.spouse.spouse.spouse.company"))).as("spouse.spouse.spouse.spouse.company=Lewisham").isTrue(); + } + + @Test + public void setAnotherNestedProperty() throws Exception { + ITestBean target = new TestBean("rod", 31); + ITestBean kerry = new TestBean("kerry", 0); + + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.setPropertyValue("spouse", kerry); + + assertThat(target.getSpouse() == kerry).as("nested set worked").isTrue(); + assertThat(kerry.getSpouse() == null).as("no back relation").isTrue(); + accessor.setPropertyValue(new PropertyValue("spouse.spouse", target)); + assertThat(kerry.getSpouse() == target).as("nested set worked").isTrue(); + assertThat(kerry.getAge() == 0).as("kerry age not set").isTrue(); + accessor.setPropertyValue(new PropertyValue("spouse.age", 35)); + assertThat(kerry.getAge() == 35).as("Set primitive on spouse").isTrue(); + + assertThat(accessor.getPropertyValue("spouse")).isEqualTo(kerry); + assertThat(accessor.getPropertyValue("spouse.spouse")).isEqualTo(target); + } + + @Test + public void setYetAnotherNestedProperties() { + String doctorCompany = ""; + String lawyerCompany = "Dr. Sueem"; + TestBean target = new TestBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.setPropertyValue("doctor.company", doctorCompany); + accessor.setPropertyValue("lawyer.company", lawyerCompany); + assertThat(target.getDoctor().getCompany()).isEqualTo(doctorCompany); + assertThat(target.getLawyer().getCompany()).isEqualTo(lawyerCompany); + } + + @Test + public void setNestedDeepProperty() { + Person target = createPerson("John", "Paris", "FR"); + AbstractPropertyAccessor accessor = createAccessor(target); + + accessor.setPropertyValue("address.country.name", "UK"); + assertThat(target.address.country.name).isEqualTo("UK"); + } + + @Test + public void testErrorMessageOfNestedProperty() { + ITestBean target = new TestBean(); + ITestBean child = new DifferentTestBean(); + child.setName("test"); + target.setSpouse(child); + AbstractPropertyAccessor accessor = createAccessor(target); + try { + accessor.getPropertyValue("spouse.bla"); + } + catch (NotReadablePropertyException ex) { + assertThat(ex.getMessage().contains(TestBean.class.getName())).isTrue(); + } + } + + @Test + public void setPropertyIntermediatePropertyIsNull() { + Person target = createPerson("John", "Paris", "FR"); + target.address.country = null; + AbstractPropertyAccessor accessor = createAccessor(target); + assertThatExceptionOfType(NullValueInNestedPathException.class).isThrownBy(() -> + accessor.setPropertyValue("address.country.name", "UK")) + .satisfies(ex -> { + assertThat(ex.getPropertyName()).isEqualTo("address.country"); + assertThat(ex.getBeanClass()).isEqualTo(Person.class); + }); + assertThat(target.address.country).isNull(); // Not touched + } + + @Test + public void setAnotherPropertyIntermediatePropertyIsNull() throws Exception { + ITestBean target = new TestBean("rod", 31); + AbstractPropertyAccessor accessor = createAccessor(target); + assertThatExceptionOfType(NullValueInNestedPathException.class).isThrownBy(() -> + accessor.setPropertyValue("spouse.age", 31)) + .satisfies(ex -> assertThat(ex.getPropertyName()).isEqualTo("spouse")); + } + + @Test + public void setPropertyIntermediatePropertyIsNullWithAutoGrow() { + Person target = createPerson("John", "Paris", "FR"); + target.address.country = null; + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.setAutoGrowNestedPaths(true); + + accessor.setPropertyValue("address.country.name", "UK"); + assertThat(target.address.country.name).isEqualTo("UK"); + } + + @Test + public void setPropertyIntermediateListIsNullWithAutoGrow() { + Foo target = new Foo(); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.setConversionService(new DefaultConversionService()); + accessor.setAutoGrowNestedPaths(true); + Map map = new HashMap<>(); + map.put("favoriteNumber", "9"); + accessor.setPropertyValue("list[0]", map); + assertThat(target.list.get(0)).isEqualTo(map); + } + + @Test + public void setPropertyIntermediateListIsNullWithNoConversionService() { + Foo target = new Foo(); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.setAutoGrowNestedPaths(true); + accessor.setPropertyValue("listOfMaps[0]['luckyNumber']", "9"); + assertThat(target.listOfMaps.get(0).get("luckyNumber")).isEqualTo("9"); + } + + @Test + public void setPropertyIntermediateListIsNullWithBadConversionService() { + Foo target = new Foo(); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.setConversionService(new GenericConversionService() { + @Override + public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) { + throw new ConversionFailedException(sourceType, targetType, source, null); + } + }); + accessor.setAutoGrowNestedPaths(true); + accessor.setPropertyValue("listOfMaps[0]['luckyNumber']", "9"); + assertThat(target.listOfMaps.get(0).get("luckyNumber")).isEqualTo("9"); + } + + + @Test + public void setEmptyPropertyValues() { + TestBean target = new TestBean(); + int age = 50; + String name = "Tony"; + target.setAge(age); + target.setName(name); + AbstractPropertyAccessor accessor = createAccessor(target); + assertThat(target.getAge() == age).as("age is OK").isTrue(); + assertThat(name.equals(target.getName())).as("name is OK").isTrue(); + accessor.setPropertyValues(new MutablePropertyValues()); + // Check its unchanged + assertThat(target.getAge() == age).as("age is OK").isTrue(); + assertThat(name.equals(target.getName())).as("name is OK").isTrue(); + } + + + @Test + public void setValidPropertyValues() { + TestBean target = new TestBean(); + String newName = "tony"; + int newAge = 65; + String newTouchy = "valid"; + AbstractPropertyAccessor accessor = createAccessor(target); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.addPropertyValue(new PropertyValue("age", newAge)); + pvs.addPropertyValue(new PropertyValue("name", newName)); + pvs.addPropertyValue(new PropertyValue("touchy", newTouchy)); + accessor.setPropertyValues(pvs); + assertThat(target.getName().equals(newName)).as("Name property should have changed").isTrue(); + assertThat(target.getTouchy().equals(newTouchy)).as("Touchy property should have changed").isTrue(); + assertThat(target.getAge() == newAge).as("Age property should have changed").isTrue(); + } + + @Test + public void setIndividualValidPropertyValues() { + TestBean target = new TestBean(); + String newName = "tony"; + int newAge = 65; + String newTouchy = "valid"; + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.setPropertyValue("age", newAge); + accessor.setPropertyValue(new PropertyValue("name", newName)); + accessor.setPropertyValue(new PropertyValue("touchy", newTouchy)); + assertThat(target.getName().equals(newName)).as("Name property should have changed").isTrue(); + assertThat(target.getTouchy().equals(newTouchy)).as("Touchy property should have changed").isTrue(); + assertThat(target.getAge() == newAge).as("Age property should have changed").isTrue(); + } + + @Test + public void setPropertyIsReflectedImmediately() { + TestBean target = new TestBean(); + int newAge = 33; + AbstractPropertyAccessor accessor = createAccessor(target); + target.setAge(newAge); + Object bwAge = accessor.getPropertyValue("age"); + assertThat(bwAge instanceof Integer).as("Age is an integer").isTrue(); + assertThat(bwAge).as("Bean wrapper must pick up changes").isEqualTo(newAge); + } + + @Test + public void setPropertyToNull() { + TestBean target = new TestBean(); + target.setName("Frank"); // we need to change it back + target.setSpouse(target); + AbstractPropertyAccessor accessor = createAccessor(target); + assertThat(target.getName() != null).as("name is not null to start off").isTrue(); + accessor.setPropertyValue("name", null); + assertThat(target.getName() == null).as("name is now null").isTrue(); + // now test with non-string + assertThat(target.getSpouse() != null).as("spouse is not null to start off").isTrue(); + accessor.setPropertyValue("spouse", null); + assertThat(target.getSpouse() == null).as("spouse is now null").isTrue(); + } + + @Test + public void setIndexedPropertyIgnored() { + MutablePropertyValues values = new MutablePropertyValues(); + values.add("toBeIgnored[0]", 42); + AbstractPropertyAccessor accessor = createAccessor(new Object()); + accessor.setPropertyValues(values, true); + } + + @Test + public void setPropertyWithPrimitiveConversion() { + MutablePropertyValues values = new MutablePropertyValues(); + values.add("name", 42); + TestBean target = new TestBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.setPropertyValues(values); + assertThat(target.getName()).isEqualTo("42"); + } + + @Test + public void setPropertyWithCustomEditor() { + MutablePropertyValues values = new MutablePropertyValues(); + values.add("name", Integer.class); + TestBean target = new TestBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.registerCustomEditor(String.class, new PropertyEditorSupport() { + @Override + public void setValue(Object value) { + super.setValue(value.toString()); + } + }); + accessor.setPropertyValues(values); + assertThat(target.getName()).isEqualTo(Integer.class.toString()); + } + + @Test + public void setStringPropertyWithCustomEditor() throws Exception { + TestBean target = new TestBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.registerCustomEditor(String.class, "name", new PropertyEditorSupport() { + @Override + public void setValue(Object value) { + if (value instanceof String[]) { + setValue(StringUtils.arrayToDelimitedString(((String[]) value), "-")); + } + else { + super.setValue(value != null ? value : ""); + } + } + }); + accessor.setPropertyValue("name", new String[] {}); + assertThat(target.getName()).isEqualTo(""); + accessor.setPropertyValue("name", new String[] {"a1", "b2"}); + assertThat(target.getName()).isEqualTo("a1-b2"); + accessor.setPropertyValue("name", null); + assertThat(target.getName()).isEqualTo(""); + } + + @Test + public void setBooleanProperty() { + BooleanTestBean target = new BooleanTestBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + + accessor.setPropertyValue("bool2", "true"); + assertThat(Boolean.TRUE.equals(accessor.getPropertyValue("bool2"))).as("Correct bool2 value").isTrue(); + assertThat(target.getBool2()).as("Correct bool2 value").isTrue(); + + accessor.setPropertyValue("bool2", "false"); + assertThat(Boolean.FALSE.equals(accessor.getPropertyValue("bool2"))).as("Correct bool2 value").isTrue(); + assertThat(!target.getBool2()).as("Correct bool2 value").isTrue(); + } + + @Test + public void setNumberProperties() { + NumberTestBean target = new NumberTestBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.setPropertyValue("short2", "2"); + accessor.setPropertyValue("int2", "8"); + accessor.setPropertyValue("long2", "6"); + accessor.setPropertyValue("bigInteger", "3"); + accessor.setPropertyValue("float2", "8.1"); + accessor.setPropertyValue("double2", "6.1"); + accessor.setPropertyValue("bigDecimal", "4.0"); + assertThat(new Short("2").equals(accessor.getPropertyValue("short2"))).as("Correct short2 value").isTrue(); + assertThat(new Short("2").equals(target.getShort2())).as("Correct short2 value").isTrue(); + assertThat(new Integer("8").equals(accessor.getPropertyValue("int2"))).as("Correct int2 value").isTrue(); + assertThat(new Integer("8").equals(target.getInt2())).as("Correct int2 value").isTrue(); + assertThat(new Long("6").equals(accessor.getPropertyValue("long2"))).as("Correct long2 value").isTrue(); + assertThat(new Long("6").equals(target.getLong2())).as("Correct long2 value").isTrue(); + assertThat(new BigInteger("3").equals(accessor.getPropertyValue("bigInteger"))).as("Correct bigInteger value").isTrue(); + assertThat(new BigInteger("3").equals(target.getBigInteger())).as("Correct bigInteger value").isTrue(); + assertThat(new Float("8.1").equals(accessor.getPropertyValue("float2"))).as("Correct float2 value").isTrue(); + assertThat(new Float("8.1").equals(target.getFloat2())).as("Correct float2 value").isTrue(); + assertThat(new Double("6.1").equals(accessor.getPropertyValue("double2"))).as("Correct double2 value").isTrue(); + assertThat(new Double("6.1").equals(target.getDouble2())).as("Correct double2 value").isTrue(); + assertThat(new BigDecimal("4.0").equals(accessor.getPropertyValue("bigDecimal"))).as("Correct bigDecimal value").isTrue(); + assertThat(new BigDecimal("4.0").equals(target.getBigDecimal())).as("Correct bigDecimal value").isTrue(); + } + + @Test + public void setNumberPropertiesWithCoercion() { + NumberTestBean target = new NumberTestBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.setPropertyValue("short2", 2); + accessor.setPropertyValue("int2", 8L); + accessor.setPropertyValue("long2", new BigInteger("6")); + accessor.setPropertyValue("bigInteger", 3L); + accessor.setPropertyValue("float2", 8.1D); + accessor.setPropertyValue("double2", new BigDecimal(6.1)); + accessor.setPropertyValue("bigDecimal", 4.0F); + assertThat(new Short("2").equals(accessor.getPropertyValue("short2"))).as("Correct short2 value").isTrue(); + assertThat(new Short("2").equals(target.getShort2())).as("Correct short2 value").isTrue(); + assertThat(new Integer("8").equals(accessor.getPropertyValue("int2"))).as("Correct int2 value").isTrue(); + assertThat(new Integer("8").equals(target.getInt2())).as("Correct int2 value").isTrue(); + assertThat(new Long("6").equals(accessor.getPropertyValue("long2"))).as("Correct long2 value").isTrue(); + assertThat(new Long("6").equals(target.getLong2())).as("Correct long2 value").isTrue(); + assertThat(new BigInteger("3").equals(accessor.getPropertyValue("bigInteger"))).as("Correct bigInteger value").isTrue(); + assertThat(new BigInteger("3").equals(target.getBigInteger())).as("Correct bigInteger value").isTrue(); + assertThat(new Float("8.1").equals(accessor.getPropertyValue("float2"))).as("Correct float2 value").isTrue(); + assertThat(new Float("8.1").equals(target.getFloat2())).as("Correct float2 value").isTrue(); + assertThat(new Double("6.1").equals(accessor.getPropertyValue("double2"))).as("Correct double2 value").isTrue(); + assertThat(new Double("6.1").equals(target.getDouble2())).as("Correct double2 value").isTrue(); + assertThat(new BigDecimal("4.0").equals(accessor.getPropertyValue("bigDecimal"))).as("Correct bigDecimal value").isTrue(); + assertThat(new BigDecimal("4.0").equals(target.getBigDecimal())).as("Correct bigDecimal value").isTrue(); + } + + @Test + public void setPrimitiveProperties() { + NumberPropertyBean target = new NumberPropertyBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + + String byteValue = " " + Byte.MAX_VALUE + " "; + String shortValue = " " + Short.MAX_VALUE + " "; + String intValue = " " + Integer.MAX_VALUE + " "; + String longValue = " " + Long.MAX_VALUE + " "; + String floatValue = " " + Float.MAX_VALUE + " "; + String doubleValue = " " + Double.MAX_VALUE + " "; + + accessor.setPropertyValue("myPrimitiveByte", byteValue); + accessor.setPropertyValue("myByte", byteValue); + + accessor.setPropertyValue("myPrimitiveShort", shortValue); + accessor.setPropertyValue("myShort", shortValue); + + accessor.setPropertyValue("myPrimitiveInt", intValue); + accessor.setPropertyValue("myInteger", intValue); + + accessor.setPropertyValue("myPrimitiveLong", longValue); + accessor.setPropertyValue("myLong", longValue); + + accessor.setPropertyValue("myPrimitiveFloat", floatValue); + accessor.setPropertyValue("myFloat", floatValue); + + accessor.setPropertyValue("myPrimitiveDouble", doubleValue); + accessor.setPropertyValue("myDouble", doubleValue); + + assertThat(target.getMyPrimitiveByte()).isEqualTo(Byte.MAX_VALUE); + assertThat(target.getMyByte().byteValue()).isEqualTo(Byte.MAX_VALUE); + + assertThat(target.getMyPrimitiveShort()).isEqualTo(Short.MAX_VALUE); + assertThat(target.getMyShort().shortValue()).isEqualTo(Short.MAX_VALUE); + + assertThat(target.getMyPrimitiveInt()).isEqualTo(Integer.MAX_VALUE); + assertThat(target.getMyInteger().intValue()).isEqualTo(Integer.MAX_VALUE); + + assertThat(target.getMyPrimitiveLong()).isEqualTo(Long.MAX_VALUE); + assertThat(target.getMyLong().longValue()).isEqualTo(Long.MAX_VALUE); + + assertThat((double) target.getMyPrimitiveFloat()).isCloseTo(Float.MAX_VALUE, within(0.001)); + assertThat((double) target.getMyFloat().floatValue()).isCloseTo(Float.MAX_VALUE, within(0.001)); + + assertThat(target.getMyPrimitiveDouble()).isCloseTo(Double.MAX_VALUE, within(0.001)); + assertThat(target.getMyDouble().doubleValue()).isCloseTo(Double.MAX_VALUE, within(0.001)); + } + + @Test + public void setEnumProperty() { + EnumTester target = new EnumTester(); + AbstractPropertyAccessor accessor = createAccessor(target); + + accessor.setPropertyValue("autowire", "BY_NAME"); + assertThat(target.getAutowire()).isEqualTo(Autowire.BY_NAME); + + accessor.setPropertyValue("autowire", " BY_TYPE "); + assertThat(target.getAutowire()).isEqualTo(Autowire.BY_TYPE); + + assertThatExceptionOfType(TypeMismatchException.class).isThrownBy(() -> + accessor.setPropertyValue("autowire", "NHERITED")); + } + + @Test + public void setGenericEnumProperty() { + EnumConsumer target = new EnumConsumer(); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.setPropertyValue("enumValue", TestEnum.class.getName() + ".TEST_VALUE"); + assertThat(target.getEnumValue()).isEqualTo(TestEnum.TEST_VALUE); + } + + @Test + public void setWildcardEnumProperty() { + WildcardEnumConsumer target = new WildcardEnumConsumer(); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.setPropertyValue("enumValue", TestEnum.class.getName() + ".TEST_VALUE"); + assertThat(target.getEnumValue()).isEqualTo(TestEnum.TEST_VALUE); + } + + @Test + public void setPropertiesProperty() throws Exception { + PropsTester target = new PropsTester(); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.setPropertyValue("name", "ptest"); + + // Note format... + String ps = "peace=war\nfreedom=slavery"; + accessor.setPropertyValue("properties", ps); + + assertThat(target.name.equals("ptest")).as("name was set").isTrue(); + assertThat(target.properties != null).as("properties non null").isTrue(); + String freedomVal = target.properties.getProperty("freedom"); + String peaceVal = target.properties.getProperty("peace"); + assertThat(peaceVal.equals("war")).as("peace==war").isTrue(); + assertThat(freedomVal.equals("slavery")).as("Freedom==slavery").isTrue(); + } + + @Test + public void setStringArrayProperty() throws Exception { + PropsTester target = new PropsTester(); + AbstractPropertyAccessor accessor = createAccessor(target); + + accessor.setPropertyValue("stringArray", new String[] {"foo", "fi", "fi", "fum"}); + assertThat(target.stringArray.length == 4).as("stringArray length = 4").isTrue(); + assertThat(target.stringArray[0].equals("foo") && target.stringArray[1].equals("fi") && + target.stringArray[2].equals("fi") && target.stringArray[3].equals("fum")).as("correct values").isTrue(); + + List list = new ArrayList<>(); + list.add("foo"); + list.add("fi"); + list.add("fi"); + list.add("fum"); + accessor.setPropertyValue("stringArray", list); + assertThat(target.stringArray.length == 4).as("stringArray length = 4").isTrue(); + assertThat(target.stringArray[0].equals("foo") && target.stringArray[1].equals("fi") && + target.stringArray[2].equals("fi") && target.stringArray[3].equals("fum")).as("correct values").isTrue(); + + Set set = new HashSet<>(); + set.add("foo"); + set.add("fi"); + set.add("fum"); + accessor.setPropertyValue("stringArray", set); + assertThat(target.stringArray.length == 3).as("stringArray length = 3").isTrue(); + List result = Arrays.asList(target.stringArray); + assertThat(result.contains("foo") && result.contains("fi") && result.contains("fum")).as("correct values").isTrue(); + + accessor.setPropertyValue("stringArray", "one"); + assertThat(target.stringArray.length == 1).as("stringArray length = 1").isTrue(); + assertThat(target.stringArray[0].equals("one")).as("stringArray elt is ok").isTrue(); + + accessor.setPropertyValue("stringArray", null); + assertThat(target.stringArray == null).as("stringArray is null").isTrue(); + } + + @Test + public void setStringArrayPropertyWithCustomStringEditor() throws Exception { + PropsTester target = new PropsTester(); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.registerCustomEditor(String.class, "stringArray", new PropertyEditorSupport() { + @Override + public void setAsText(String text) { + setValue(text.substring(1)); + } + }); + + accessor.setPropertyValue("stringArray", new String[] {"4foo", "7fi", "6fi", "5fum"}); + assertThat(target.stringArray.length == 4).as("stringArray length = 4").isTrue(); + assertThat(target.stringArray[0].equals("foo") && target.stringArray[1].equals("fi") && + target.stringArray[2].equals("fi") && target.stringArray[3].equals("fum")).as("correct values").isTrue(); + + List list = new ArrayList<>(); + list.add("4foo"); + list.add("7fi"); + list.add("6fi"); + list.add("5fum"); + accessor.setPropertyValue("stringArray", list); + assertThat(target.stringArray.length == 4).as("stringArray length = 4").isTrue(); + assertThat(target.stringArray[0].equals("foo") && target.stringArray[1].equals("fi") && + target.stringArray[2].equals("fi") && target.stringArray[3].equals("fum")).as("correct values").isTrue(); + + Set set = new HashSet<>(); + set.add("4foo"); + set.add("7fi"); + set.add("6fum"); + accessor.setPropertyValue("stringArray", set); + assertThat(target.stringArray.length == 3).as("stringArray length = 3").isTrue(); + List result = Arrays.asList(target.stringArray); + assertThat(result.contains("foo") && result.contains("fi") && result.contains("fum")).as("correct values").isTrue(); + + accessor.setPropertyValue("stringArray", "8one"); + assertThat(target.stringArray.length == 1).as("stringArray length = 1").isTrue(); + assertThat(target.stringArray[0].equals("one")).as("correct values").isTrue(); + } + + @Test + public void setStringArrayPropertyWithStringSplitting() throws Exception { + PropsTester target = new PropsTester(); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.useConfigValueEditors(); + accessor.setPropertyValue("stringArray", "a1,b2"); + assertThat(target.stringArray.length == 2).as("stringArray length = 2").isTrue(); + assertThat(target.stringArray[0].equals("a1") && target.stringArray[1].equals("b2")).as("correct values").isTrue(); + } + + @Test + public void setStringArrayPropertyWithCustomStringDelimiter() throws Exception { + PropsTester target = new PropsTester(); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.registerCustomEditor(String[].class, "stringArray", new StringArrayPropertyEditor("-")); + accessor.setPropertyValue("stringArray", "a1-b2"); + assertThat(target.stringArray.length == 2).as("stringArray length = 2").isTrue(); + assertThat(target.stringArray[0].equals("a1") && target.stringArray[1].equals("b2")).as("correct values").isTrue(); + } + + @Test + public void setStringArrayWithAutoGrow() throws Exception { + StringArrayBean target = new StringArrayBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.setAutoGrowNestedPaths(true); + + accessor.setPropertyValue("array[0]", "Test0"); + assertThat(target.getArray().length).isEqualTo(1); + + accessor.setPropertyValue("array[2]", "Test2"); + assertThat(target.getArray().length).isEqualTo(3); + assertThat(target.getArray()[0].equals("Test0") && target.getArray()[1] == null && + target.getArray()[2].equals("Test2")).as("correct values").isTrue(); + } + + @Test + public void setIntArrayProperty() { + PropsTester target = new PropsTester(); + AbstractPropertyAccessor accessor = createAccessor(target); + + accessor.setPropertyValue("intArray", new int[] {4, 5, 2, 3}); + assertThat(target.intArray.length == 4).as("intArray length = 4").isTrue(); + assertThat(target.intArray[0] == 4 && target.intArray[1] == 5 && + target.intArray[2] == 2 && target.intArray[3] == 3).as("correct values").isTrue(); + + accessor.setPropertyValue("intArray", new String[] {"4", "5", "2", "3"}); + assertThat(target.intArray.length == 4).as("intArray length = 4").isTrue(); + assertThat(target.intArray[0] == 4 && target.intArray[1] == 5 && + target.intArray[2] == 2 && target.intArray[3] == 3).as("correct values").isTrue(); + + List list = new ArrayList<>(); + list.add(4); + list.add("5"); + list.add(2); + list.add("3"); + accessor.setPropertyValue("intArray", list); + assertThat(target.intArray.length == 4).as("intArray length = 4").isTrue(); + assertThat(target.intArray[0] == 4 && target.intArray[1] == 5 && + target.intArray[2] == 2 && target.intArray[3] == 3).as("correct values").isTrue(); + + Set set = new HashSet<>(); + set.add("4"); + set.add(5); + set.add("3"); + accessor.setPropertyValue("intArray", set); + assertThat(target.intArray.length == 3).as("intArray length = 3").isTrue(); + List result = new ArrayList<>(); + result.add(target.intArray[0]); + result.add(target.intArray[1]); + result.add(target.intArray[2]); + assertThat(result.contains(4) && result.contains(5) && + result.contains(3)).as("correct values").isTrue(); + + accessor.setPropertyValue("intArray", new Integer[] {1}); + assertThat(target.intArray.length == 1).as("intArray length = 4").isTrue(); + assertThat(target.intArray[0] == 1).as("correct values").isTrue(); + + accessor.setPropertyValue("intArray", 1); + assertThat(target.intArray.length == 1).as("intArray length = 4").isTrue(); + assertThat(target.intArray[0] == 1).as("correct values").isTrue(); + + accessor.setPropertyValue("intArray", new String[] {"1"}); + assertThat(target.intArray.length == 1).as("intArray length = 4").isTrue(); + assertThat(target.intArray[0] == 1).as("correct values").isTrue(); + + accessor.setPropertyValue("intArray", "1"); + assertThat(target.intArray.length == 1).as("intArray length = 4").isTrue(); + assertThat(target.intArray[0] == 1).as("correct values").isTrue(); + } + + @Test + public void setIntArrayPropertyWithCustomEditor() { + PropsTester target = new PropsTester(); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.registerCustomEditor(int.class, new PropertyEditorSupport() { + @Override + public void setAsText(String text) { + setValue(Integer.parseInt(text) + 1); + } + }); + + accessor.setPropertyValue("intArray", new int[] {4, 5, 2, 3}); + assertThat(target.intArray.length == 4).as("intArray length = 4").isTrue(); + assertThat(target.intArray[0] == 4 && target.intArray[1] == 5 && + target.intArray[2] == 2 && target.intArray[3] == 3).as("correct values").isTrue(); + + accessor.setPropertyValue("intArray", new String[] {"3", "4", "1", "2"}); + assertThat(target.intArray.length == 4).as("intArray length = 4").isTrue(); + assertThat(target.intArray[0] == 4 && target.intArray[1] == 5 && + target.intArray[2] == 2 && target.intArray[3] == 3).as("correct values").isTrue(); + + accessor.setPropertyValue("intArray", 1); + assertThat(target.intArray.length == 1).as("intArray length = 4").isTrue(); + assertThat(target.intArray[0] == 1).as("correct values").isTrue(); + + accessor.setPropertyValue("intArray", new String[] {"0"}); + assertThat(target.intArray.length == 1).as("intArray length = 4").isTrue(); + assertThat(target.intArray[0] == 1).as("correct values").isTrue(); + + accessor.setPropertyValue("intArray", "0"); + assertThat(target.intArray.length == 1).as("intArray length = 4").isTrue(); + assertThat(target.intArray[0] == 1).as("correct values").isTrue(); + } + + @Test + public void setIntArrayPropertyWithStringSplitting() throws Exception { + PropsTester target = new PropsTester(); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.useConfigValueEditors(); + accessor.setPropertyValue("intArray", "4,5"); + assertThat(target.intArray.length == 2).as("intArray length = 2").isTrue(); + assertThat(target.intArray[0] == 4 && target.intArray[1] == 5).as("correct values").isTrue(); + } + + @Test + public void setPrimitiveArrayProperty() { + PrimitiveArrayBean target = new PrimitiveArrayBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.setPropertyValue("array", new String[] {"1", "2"}); + assertThat(target.getArray().length).isEqualTo(2); + assertThat(target.getArray()[0]).isEqualTo(1); + assertThat(target.getArray()[1]).isEqualTo(2); + } + + @Test + public void setPrimitiveArrayPropertyLargeMatchingWithSpecificEditor() { + PrimitiveArrayBean target = new PrimitiveArrayBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.registerCustomEditor(int.class, "array", new PropertyEditorSupport() { + @Override + public void setValue(Object value) { + if (value instanceof Integer) { + super.setValue((Integer) value + 1); + } + } + }); + int[] input = new int[1024]; + accessor.setPropertyValue("array", input); + assertThat(target.getArray().length).isEqualTo(1024); + assertThat(target.getArray()[0]).isEqualTo(1); + assertThat(target.getArray()[1]).isEqualTo(1); + } + + @Test + public void setPrimitiveArrayPropertyLargeMatchingWithIndexSpecificEditor() { + PrimitiveArrayBean target = new PrimitiveArrayBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.registerCustomEditor(int.class, "array[1]", new PropertyEditorSupport() { + @Override + public void setValue(Object value) { + if (value instanceof Integer) { + super.setValue((Integer) value + 1); + } + } + }); + int[] input = new int[1024]; + accessor.setPropertyValue("array", input); + assertThat(target.getArray().length).isEqualTo(1024); + assertThat(target.getArray()[0]).isEqualTo(0); + assertThat(target.getArray()[1]).isEqualTo(1); + } + + @Test + public void setPrimitiveArrayPropertyWithAutoGrow() throws Exception { + PrimitiveArrayBean target = new PrimitiveArrayBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.setAutoGrowNestedPaths(true); + + accessor.setPropertyValue("array[0]", 1); + assertThat(target.getArray().length).isEqualTo(1); + + accessor.setPropertyValue("array[2]", 3); + assertThat(target.getArray().length).isEqualTo(3); + assertThat(target.getArray()[0] == 1 && target.getArray()[1] == 0 && + target.getArray()[2] == 3).as("correct values").isTrue(); + } + + @Test + @SuppressWarnings("rawtypes") + public void setGenericArrayProperty() { + SkipReaderStub target = new SkipReaderStub(); + AbstractPropertyAccessor accessor = createAccessor(target); + List values = new ArrayList<>(); + values.add("1"); + values.add("2"); + values.add("3"); + values.add("4"); + accessor.setPropertyValue("items", values); + Object[] result = target.items; + assertThat(result.length).isEqualTo(4); + assertThat(result[0]).isEqualTo("1"); + assertThat(result[1]).isEqualTo("2"); + assertThat(result[2]).isEqualTo("3"); + assertThat(result[3]).isEqualTo("4"); + } + + @Test + public void setArrayPropertyToObject() { + ArrayToObject target = new ArrayToObject(); + AbstractPropertyAccessor accessor = createAccessor(target); + + Object[] array = new Object[] {"1", "2"}; + accessor.setPropertyValue("object", array); + assertThat(target.getObject()).isEqualTo(array); + + array = new Object[] {"1"}; + accessor.setPropertyValue("object", array); + assertThat(target.getObject()).isEqualTo(array); + } + + @Test + public void setCollectionProperty() { + IndexedTestBean target = new IndexedTestBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + Collection coll = new HashSet<>(); + coll.add("coll1"); + accessor.setPropertyValue("collection", coll); + Set set = new HashSet<>(); + set.add("set1"); + accessor.setPropertyValue("set", set); + SortedSet sortedSet = new TreeSet<>(); + sortedSet.add("sortedSet1"); + accessor.setPropertyValue("sortedSet", sortedSet); + List list = new ArrayList<>(); + list.add("list1"); + accessor.setPropertyValue("list", list); + assertThat(target.getCollection()).isSameAs(coll); + assertThat(target.getSet()).isSameAs(set); + assertThat(target.getSortedSet()).isSameAs(sortedSet); + assertThat((List) target.getList()).isSameAs(list); + } + + @SuppressWarnings("unchecked") // list cannot be properly parameterized as it breaks other tests + @Test + public void setCollectionPropertyNonMatchingType() { + IndexedTestBean target = new IndexedTestBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + Collection coll = new ArrayList<>(); + coll.add("coll1"); + accessor.setPropertyValue("collection", coll); + List set = new ArrayList<>(); + set.add("set1"); + accessor.setPropertyValue("set", set); + List sortedSet = new ArrayList<>(); + sortedSet.add("sortedSet1"); + accessor.setPropertyValue("sortedSet", sortedSet); + Set list = new HashSet<>(); + list.add("list1"); + accessor.setPropertyValue("list", list); + assertThat(target.getCollection().size()).isEqualTo(1); + assertThat(target.getCollection().containsAll(coll)).isTrue(); + assertThat(target.getSet().size()).isEqualTo(1); + assertThat(target.getSet().containsAll(set)).isTrue(); + assertThat(target.getSortedSet().size()).isEqualTo(1); + assertThat(target.getSortedSet().containsAll(sortedSet)).isTrue(); + assertThat(target.getList().size()).isEqualTo(1); + assertThat(target.getList().containsAll(list)).isTrue(); + } + + @SuppressWarnings("unchecked") // list cannot be properly parameterized as it breaks other tests + @Test + public void setCollectionPropertyWithArrayValue() { + IndexedTestBean target = new IndexedTestBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + Collection coll = new HashSet<>(); + coll.add("coll1"); + accessor.setPropertyValue("collection", coll.toArray()); + List set = new ArrayList<>(); + set.add("set1"); + accessor.setPropertyValue("set", set.toArray()); + List sortedSet = new ArrayList<>(); + sortedSet.add("sortedSet1"); + accessor.setPropertyValue("sortedSet", sortedSet.toArray()); + Set list = new HashSet<>(); + list.add("list1"); + accessor.setPropertyValue("list", list.toArray()); + assertThat(target.getCollection().size()).isEqualTo(1); + assertThat(target.getCollection().containsAll(coll)).isTrue(); + assertThat(target.getSet().size()).isEqualTo(1); + assertThat(target.getSet().containsAll(set)).isTrue(); + assertThat(target.getSortedSet().size()).isEqualTo(1); + assertThat(target.getSortedSet().containsAll(sortedSet)).isTrue(); + assertThat(target.getList().size()).isEqualTo(1); + assertThat(target.getList().containsAll(list)).isTrue(); + } + + @SuppressWarnings("unchecked") // list cannot be properly parameterized as it breaks other tests + @Test + public void setCollectionPropertyWithIntArrayValue() { + IndexedTestBean target = new IndexedTestBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + Collection coll = new HashSet<>(); + coll.add(0); + accessor.setPropertyValue("collection", new int[] {0}); + List set = new ArrayList<>(); + set.add(1); + accessor.setPropertyValue("set", new int[] {1}); + List sortedSet = new ArrayList<>(); + sortedSet.add(2); + accessor.setPropertyValue("sortedSet", new int[] {2}); + Set list = new HashSet<>(); + list.add(3); + accessor.setPropertyValue("list", new int[] {3}); + assertThat(target.getCollection().size()).isEqualTo(1); + assertThat(target.getCollection().containsAll(coll)).isTrue(); + assertThat(target.getSet().size()).isEqualTo(1); + assertThat(target.getSet().containsAll(set)).isTrue(); + assertThat(target.getSortedSet().size()).isEqualTo(1); + assertThat(target.getSortedSet().containsAll(sortedSet)).isTrue(); + assertThat(target.getList().size()).isEqualTo(1); + assertThat(target.getList().containsAll(list)).isTrue(); + } + + @SuppressWarnings("unchecked") // list cannot be properly parameterized as it breaks other tests + @Test + public void setCollectionPropertyWithIntegerValue() { + IndexedTestBean target = new IndexedTestBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + Collection coll = new HashSet<>(); + coll.add(0); + accessor.setPropertyValue("collection", 0); + List set = new ArrayList<>(); + set.add(1); + accessor.setPropertyValue("set", 1); + List sortedSet = new ArrayList<>(); + sortedSet.add(2); + accessor.setPropertyValue("sortedSet", 2); + Set list = new HashSet<>(); + list.add(3); + accessor.setPropertyValue("list", 3); + assertThat(target.getCollection().size()).isEqualTo(1); + assertThat(target.getCollection().containsAll(coll)).isTrue(); + assertThat(target.getSet().size()).isEqualTo(1); + assertThat(target.getSet().containsAll(set)).isTrue(); + assertThat(target.getSortedSet().size()).isEqualTo(1); + assertThat(target.getSortedSet().containsAll(sortedSet)).isTrue(); + assertThat(target.getList().size()).isEqualTo(1); + assertThat(target.getList().containsAll(list)).isTrue(); + } + + @SuppressWarnings("unchecked") // list cannot be properly parameterized as it breaks other tests + @Test + public void setCollectionPropertyWithStringValue() { + IndexedTestBean target = new IndexedTestBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + List set = new ArrayList<>(); + set.add("set1"); + accessor.setPropertyValue("set", "set1"); + List sortedSet = new ArrayList<>(); + sortedSet.add("sortedSet1"); + accessor.setPropertyValue("sortedSet", "sortedSet1"); + Set list = new HashSet<>(); + list.add("list1"); + accessor.setPropertyValue("list", "list1"); + assertThat(target.getSet().size()).isEqualTo(1); + assertThat(target.getSet().containsAll(set)).isTrue(); + assertThat(target.getSortedSet().size()).isEqualTo(1); + assertThat(target.getSortedSet().containsAll(sortedSet)).isTrue(); + assertThat(target.getList().size()).isEqualTo(1); + assertThat(target.getList().containsAll(list)).isTrue(); + } + + @Test + public void setCollectionPropertyWithStringValueAndCustomEditor() { + IndexedTestBean target = new IndexedTestBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.registerCustomEditor(String.class, "set", new StringTrimmerEditor(false)); + accessor.registerCustomEditor(String.class, "list", new StringTrimmerEditor(false)); + + accessor.setPropertyValue("set", "set1 "); + accessor.setPropertyValue("sortedSet", "sortedSet1"); + accessor.setPropertyValue("list", "list1 "); + assertThat(target.getSet().size()).isEqualTo(1); + assertThat(target.getSet().contains("set1")).isTrue(); + assertThat(target.getSortedSet().size()).isEqualTo(1); + assertThat(target.getSortedSet().contains("sortedSet1")).isTrue(); + assertThat(target.getList().size()).isEqualTo(1); + assertThat(target.getList().contains("list1")).isTrue(); + + accessor.setPropertyValue("list", Collections.singletonList("list1 ")); + assertThat(target.getList().contains("list1")).isTrue(); + } + + @Test + public void setMapProperty() { + IndexedTestBean target = new IndexedTestBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + Map map = new HashMap<>(); + map.put("key", "value"); + accessor.setPropertyValue("map", map); + SortedMap sortedMap = new TreeMap<>(); + map.put("sortedKey", "sortedValue"); + accessor.setPropertyValue("sortedMap", sortedMap); + assertThat((Map) target.getMap()).isSameAs(map); + assertThat((Map) target.getSortedMap()).isSameAs(sortedMap); + } + + @Test + public void setMapPropertyNonMatchingType() { + IndexedTestBean target = new IndexedTestBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + Map map = new TreeMap<>(); + map.put("key", "value"); + accessor.setPropertyValue("map", map); + Map sortedMap = new TreeMap<>(); + sortedMap.put("sortedKey", "sortedValue"); + accessor.setPropertyValue("sortedMap", sortedMap); + assertThat(target.getMap().size()).isEqualTo(1); + assertThat(target.getMap().get("key")).isEqualTo("value"); + assertThat(target.getSortedMap().size()).isEqualTo(1); + assertThat(target.getSortedMap().get("sortedKey")).isEqualTo("sortedValue"); + } + + @Test + public void setMapPropertyWithTypeConversion() { + IndexedTestBean target = new IndexedTestBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.registerCustomEditor(TestBean.class, new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + if (!StringUtils.hasLength(text)) { + throw new IllegalArgumentException(); + } + setValue(new TestBean(text)); + } + }); + + MutablePropertyValues goodValues = new MutablePropertyValues(); + goodValues.add("map[key1]", "rod"); + goodValues.add("map[key2]", "rob"); + accessor.setPropertyValues(goodValues); + assertThat(((TestBean) target.getMap().get("key1")).getName()).isEqualTo("rod"); + assertThat(((TestBean) target.getMap().get("key2")).getName()).isEqualTo("rob"); + + MutablePropertyValues badValues = new MutablePropertyValues(); + badValues.add("map[key1]", "rod"); + badValues.add("map[key2]", ""); + assertThatExceptionOfType(PropertyBatchUpdateException.class).isThrownBy(() -> + accessor.setPropertyValues(badValues)) + .satisfies(ex -> assertThat(ex.getPropertyAccessException("map[key2]")).isInstanceOf(TypeMismatchException.class)); + } + + @Test + public void setMapPropertyWithUnmodifiableMap() { + IndexedTestBean target = new IndexedTestBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.registerCustomEditor(TestBean.class, "map", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + if (!StringUtils.hasLength(text)) { + throw new IllegalArgumentException(); + } + setValue(new TestBean(text)); + } + }); + + Map inputMap = new HashMap<>(); + inputMap.put(1, "rod"); + inputMap.put(2, "rob"); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("map", Collections.unmodifiableMap(inputMap)); + accessor.setPropertyValues(pvs); + assertThat(((TestBean) target.getMap().get(1)).getName()).isEqualTo("rod"); + assertThat(((TestBean) target.getMap().get(2)).getName()).isEqualTo("rob"); + } + + @Test + public void setMapPropertyWithCustomUnmodifiableMap() { + IndexedTestBean target = new IndexedTestBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.registerCustomEditor(TestBean.class, "map", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + if (!StringUtils.hasLength(text)) { + throw new IllegalArgumentException(); + } + setValue(new TestBean(text)); + } + }); + + Map inputMap = new HashMap<>(); + inputMap.put(1, "rod"); + inputMap.put(2, "rob"); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("map", new ReadOnlyMap<>(inputMap)); + accessor.setPropertyValues(pvs); + assertThat(((TestBean) target.getMap().get(1)).getName()).isEqualTo("rod"); + assertThat(((TestBean) target.getMap().get(2)).getName()).isEqualTo("rob"); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) // must work with raw map in this test + @Test + public void setRawMapPropertyWithNoEditorRegistered() { + IndexedTestBean target = new IndexedTestBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + Map inputMap = new HashMap(); + inputMap.put(1, "rod"); + inputMap.put(2, "rob"); + ReadOnlyMap readOnlyMap = new ReadOnlyMap(inputMap); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("map", readOnlyMap); + accessor.setPropertyValues(pvs); + assertThat(target.getMap()).isSameAs(readOnlyMap); + assertThat(readOnlyMap.isAccessed()).isFalse(); + } + + @Test + public void setUnknownProperty() { + Simple target = new Simple("John", 2); + AbstractPropertyAccessor accessor = createAccessor(target); + assertThatExceptionOfType(NotWritablePropertyException.class).isThrownBy(() -> + accessor.setPropertyValue("name1", "value")) + .satisfies(ex -> { + assertThat(ex.getBeanClass()).isEqualTo(Simple.class); + assertThat(ex.getPropertyName()).isEqualTo("name1"); + assertThat(ex.getPossibleMatches()).containsExactly("name"); + }); + } + + @Test + public void setUnknownPropertyWithPossibleMatches() { + Simple target = new Simple("John", 2); + AbstractPropertyAccessor accessor = createAccessor(target); + assertThatExceptionOfType(NotWritablePropertyException.class).isThrownBy(() -> + accessor.setPropertyValue("foo", "value")) + .satisfies(ex -> { + assertThat(ex.getBeanClass()).isEqualTo(Simple.class); + assertThat(ex.getPropertyName()).isEqualTo("foo"); + }); + } + + @Test + public void setUnknownOptionalProperty() { + Simple target = new Simple("John", 2); + AbstractPropertyAccessor accessor = createAccessor(target); + PropertyValue value = new PropertyValue("foo", "value"); + value.setOptional(true); + accessor.setPropertyValue(value); + } + + @Test + public void setPropertyInProtectedBaseBean() { + DerivedFromProtectedBaseBean target = new DerivedFromProtectedBaseBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.setPropertyValue("someProperty", "someValue"); + assertThat(accessor.getPropertyValue("someProperty")).isEqualTo("someValue"); + assertThat(target.getSomeProperty()).isEqualTo("someValue"); + } + + @Test + public void setPropertyTypeMismatch() { + TestBean target = new TestBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + assertThatExceptionOfType(TypeMismatchException.class).isThrownBy(() -> + accessor.setPropertyValue("age", "foobar")); + } + + @Test + public void setEmptyValueForPrimitiveProperty() { + TestBean target = new TestBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + assertThatExceptionOfType(TypeMismatchException.class).isThrownBy(() -> + accessor.setPropertyValue("age", "")); + } + + @Test + public void setUnknownNestedProperty() { + Person target = createPerson("John", "Paris", "FR"); + AbstractPropertyAccessor accessor = createAccessor(target); + + assertThatExceptionOfType(NotWritablePropertyException.class).isThrownBy(() -> + accessor.setPropertyValue("address.bar", "value")); + } + + @Test + public void setPropertyValuesIgnoresInvalidNestedOnRequest() { + ITestBean target = new TestBean(); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.addPropertyValue(new PropertyValue("name", "rod")); + pvs.addPropertyValue(new PropertyValue("graceful.rubbish", "tony")); + pvs.addPropertyValue(new PropertyValue("more.garbage", new Object())); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.setPropertyValues(pvs, true); + assertThat(target.getName().equals("rod")).as("Set valid and ignored invalid").isTrue(); + assertThatExceptionOfType(NotWritablePropertyException.class).isThrownBy(() -> + accessor.setPropertyValues(pvs, false)); // Don't ignore: should fail + } + + @Test + public void getAndSetIndexedProperties() { + IndexedTestBean target = new IndexedTestBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + TestBean tb0 = target.getArray()[0]; + TestBean tb1 = target.getArray()[1]; + TestBean tb2 = ((TestBean) target.getList().get(0)); + TestBean tb3 = ((TestBean) target.getList().get(1)); + TestBean tb6 = ((TestBean) target.getSet().toArray()[0]); + TestBean tb7 = ((TestBean) target.getSet().toArray()[1]); + TestBean tb4 = ((TestBean) target.getMap().get("key1")); + TestBean tb5 = ((TestBean) target.getMap().get("key.3")); + TestBean tb8 = ((TestBean) target.getMap().get("key5[foo]")); + assertThat(tb0.getName()).isEqualTo("name0"); + assertThat(tb1.getName()).isEqualTo("name1"); + assertThat(tb2.getName()).isEqualTo("name2"); + assertThat(tb3.getName()).isEqualTo("name3"); + assertThat(tb6.getName()).isEqualTo("name6"); + assertThat(tb7.getName()).isEqualTo("name7"); + assertThat(tb4.getName()).isEqualTo("name4"); + assertThat(tb5.getName()).isEqualTo("name5"); + assertThat(tb8.getName()).isEqualTo("name8"); + assertThat(accessor.getPropertyValue("array[0].name")).isEqualTo("name0"); + assertThat(accessor.getPropertyValue("array[1].name")).isEqualTo("name1"); + assertThat(accessor.getPropertyValue("list[0].name")).isEqualTo("name2"); + assertThat(accessor.getPropertyValue("list[1].name")).isEqualTo("name3"); + assertThat(accessor.getPropertyValue("set[0].name")).isEqualTo("name6"); + assertThat(accessor.getPropertyValue("set[1].name")).isEqualTo("name7"); + assertThat(accessor.getPropertyValue("map[key1].name")).isEqualTo("name4"); + assertThat(accessor.getPropertyValue("map[key.3].name")).isEqualTo("name5"); + assertThat(accessor.getPropertyValue("map['key1'].name")).isEqualTo("name4"); + assertThat(accessor.getPropertyValue("map[\"key.3\"].name")).isEqualTo("name5"); + assertThat(accessor.getPropertyValue("map[key4][0].name")).isEqualTo("nameX"); + assertThat(accessor.getPropertyValue("map[key4][1].name")).isEqualTo("nameY"); + assertThat(accessor.getPropertyValue("map[key5[foo]].name")).isEqualTo("name8"); + assertThat(accessor.getPropertyValue("map['key5[foo]'].name")).isEqualTo("name8"); + assertThat(accessor.getPropertyValue("map[\"key5[foo]\"].name")).isEqualTo("name8"); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("array[0].name", "name5"); + pvs.add("array[1].name", "name4"); + pvs.add("list[0].name", "name3"); + pvs.add("list[1].name", "name2"); + pvs.add("set[0].name", "name8"); + pvs.add("set[1].name", "name9"); + pvs.add("map[key1].name", "name1"); + pvs.add("map['key.3'].name", "name0"); + pvs.add("map[key4][0].name", "nameA"); + pvs.add("map[key4][1].name", "nameB"); + pvs.add("map[key5[foo]].name", "name10"); + accessor.setPropertyValues(pvs); + assertThat(tb0.getName()).isEqualTo("name5"); + assertThat(tb1.getName()).isEqualTo("name4"); + assertThat(tb2.getName()).isEqualTo("name3"); + assertThat(tb3.getName()).isEqualTo("name2"); + assertThat(tb4.getName()).isEqualTo("name1"); + assertThat(tb5.getName()).isEqualTo("name0"); + assertThat(accessor.getPropertyValue("array[0].name")).isEqualTo("name5"); + assertThat(accessor.getPropertyValue("array[1].name")).isEqualTo("name4"); + assertThat(accessor.getPropertyValue("list[0].name")).isEqualTo("name3"); + assertThat(accessor.getPropertyValue("list[1].name")).isEqualTo("name2"); + assertThat(accessor.getPropertyValue("set[0].name")).isEqualTo("name8"); + assertThat(accessor.getPropertyValue("set[1].name")).isEqualTo("name9"); + assertThat(accessor.getPropertyValue("map[\"key1\"].name")).isEqualTo("name1"); + assertThat(accessor.getPropertyValue("map['key.3'].name")).isEqualTo("name0"); + assertThat(accessor.getPropertyValue("map[key4][0].name")).isEqualTo("nameA"); + assertThat(accessor.getPropertyValue("map[key4][1].name")).isEqualTo("nameB"); + assertThat(accessor.getPropertyValue("map[key5[foo]].name")).isEqualTo("name10"); + } + + @Test + public void getAndSetIndexedPropertiesWithDirectAccess() { + IndexedTestBean target = new IndexedTestBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + TestBean tb0 = target.getArray()[0]; + TestBean tb1 = target.getArray()[1]; + TestBean tb2 = ((TestBean) target.getList().get(0)); + TestBean tb3 = ((TestBean) target.getList().get(1)); + TestBean tb6 = ((TestBean) target.getSet().toArray()[0]); + TestBean tb7 = ((TestBean) target.getSet().toArray()[1]); + TestBean tb4 = ((TestBean) target.getMap().get("key1")); + TestBean tb5 = ((TestBean) target.getMap().get("key2")); + assertThat(accessor.getPropertyValue("array[0]")).isEqualTo(tb0); + assertThat(accessor.getPropertyValue("array[1]")).isEqualTo(tb1); + assertThat(accessor.getPropertyValue("list[0]")).isEqualTo(tb2); + assertThat(accessor.getPropertyValue("list[1]")).isEqualTo(tb3); + assertThat(accessor.getPropertyValue("set[0]")).isEqualTo(tb6); + assertThat(accessor.getPropertyValue("set[1]")).isEqualTo(tb7); + assertThat(accessor.getPropertyValue("map[key1]")).isEqualTo(tb4); + assertThat(accessor.getPropertyValue("map[key2]")).isEqualTo(tb5); + assertThat(accessor.getPropertyValue("map['key1']")).isEqualTo(tb4); + assertThat(accessor.getPropertyValue("map[\"key2\"]")).isEqualTo(tb5); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("array[0]", tb5); + pvs.add("array[1]", tb4); + pvs.add("list[0]", tb3); + pvs.add("list[1]", tb2); + pvs.add("list[2]", tb0); + pvs.add("list[4]", tb1); + pvs.add("map[key1]", tb1); + pvs.add("map['key2']", tb0); + pvs.add("map[key5]", tb4); + pvs.add("map['key9']", tb5); + accessor.setPropertyValues(pvs); + assertThat(target.getArray()[0]).isEqualTo(tb5); + assertThat(target.getArray()[1]).isEqualTo(tb4); + assertThat((target.getList().get(0))).isEqualTo(tb3); + assertThat((target.getList().get(1))).isEqualTo(tb2); + assertThat((target.getList().get(2))).isEqualTo(tb0); + assertThat((target.getList().get(3))).isEqualTo(null); + assertThat((target.getList().get(4))).isEqualTo(tb1); + assertThat((target.getMap().get("key1"))).isEqualTo(tb1); + assertThat((target.getMap().get("key2"))).isEqualTo(tb0); + assertThat((target.getMap().get("key5"))).isEqualTo(tb4); + assertThat((target.getMap().get("key9"))).isEqualTo(tb5); + assertThat(accessor.getPropertyValue("array[0]")).isEqualTo(tb5); + assertThat(accessor.getPropertyValue("array[1]")).isEqualTo(tb4); + assertThat(accessor.getPropertyValue("list[0]")).isEqualTo(tb3); + assertThat(accessor.getPropertyValue("list[1]")).isEqualTo(tb2); + assertThat(accessor.getPropertyValue("list[2]")).isEqualTo(tb0); + assertThat(accessor.getPropertyValue("list[3]")).isEqualTo(null); + assertThat(accessor.getPropertyValue("list[4]")).isEqualTo(tb1); + assertThat(accessor.getPropertyValue("map[\"key1\"]")).isEqualTo(tb1); + assertThat(accessor.getPropertyValue("map['key2']")).isEqualTo(tb0); + assertThat(accessor.getPropertyValue("map[\"key5\"]")).isEqualTo(tb4); + assertThat(accessor.getPropertyValue("map['key9']")).isEqualTo(tb5); + } + + @Test + public void propertyType() { + Person target = createPerson("John", "Paris", "FR"); + AbstractPropertyAccessor accessor = createAccessor(target); + + assertThat(accessor.getPropertyType("address.city")).isEqualTo(String.class); + } + + @Test + public void propertyTypeUnknownProperty() { + Simple target = new Simple("John", 2); + AbstractPropertyAccessor accessor = createAccessor(target); + + assertThat(accessor.getPropertyType("foo")).isNull(); + } + + @Test + public void propertyTypeDescriptor() { + Person target = createPerson("John", "Paris", "FR"); + AbstractPropertyAccessor accessor = createAccessor(target); + + assertThat(accessor.getPropertyTypeDescriptor("address.city")).isNotNull(); + } + + @Test + public void propertyTypeDescriptorUnknownProperty() { + Simple target = new Simple("John", 2); + AbstractPropertyAccessor accessor = createAccessor(target); + + assertThat(accessor.getPropertyTypeDescriptor("foo")).isNull(); + } + + @Test + public void propertyTypeIndexedProperty() { + IndexedTestBean target = new IndexedTestBean(); + AbstractPropertyAccessor accessor = createAccessor(target); + assertThat(accessor.getPropertyType("map[key0]")).isEqualTo(null); + + accessor = createAccessor(target); + accessor.setPropertyValue("map[key0]", "my String"); + assertThat(accessor.getPropertyType("map[key0]")).isEqualTo(String.class); + + accessor = createAccessor(target); + accessor.registerCustomEditor(String.class, "map[key0]", new StringTrimmerEditor(false)); + assertThat(accessor.getPropertyType("map[key0]")).isEqualTo(String.class); + } + + @Test + public void cornerSpr10115() { + Spr10115Bean target = new Spr10115Bean(); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.setPropertyValue("prop1", "val1"); + assertThat(Spr10115Bean.prop1).isEqualTo("val1"); + } + + @Test + public void cornerSpr13837() { + Spr13837Bean target = new Spr13837Bean(); + AbstractPropertyAccessor accessor = createAccessor(target); + accessor.setPropertyValue("something", 42); + assertThat(target.something).isEqualTo(Integer.valueOf(42)); + } + + + private Person createPerson(String name, String city, String country) { + return new Person(name, new Address(city, country)); + } + + + @SuppressWarnings("unused") + private static class Simple { + + private String name; + + private Integer integer; + + private Simple(String name, Integer integer) { + this.name = name; + this.integer = integer; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getInteger() { + return integer; + } + + public void setInteger(Integer integer) { + this.integer = integer; + } + } + + @SuppressWarnings("unused") + private static class Person { + private String name; + + private Address address; + + private Person(String name, Address address) { + this.name = name; + this.address = address; + } + + public Person() { + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Address getAddress() { + return address; + } + + public void setAddress(Address address) { + this.address = address; + } + } + + @SuppressWarnings("unused") + private static class Address { + private String city; + + private Country country; + + private Address(String city, String country) { + this.city = city; + this.country = new Country(country); + } + + public Address() { + this("DefaultCity", "DefaultCountry"); + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public Country getCountry() { + return country; + } + + public void setCountry(Country country) { + this.country = country; + } + } + + @SuppressWarnings("unused") + private static class Country { + private String name; + + public Country(String name) { + this.name = name; + } + + public Country() { + this(null); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + + @SuppressWarnings("unused") + static class NoRead { + + public void setAge(int age) { + } + } + + @SuppressWarnings({ "unused", "rawtypes" }) + private static class Foo { + + private List list; + + private List listOfMaps; + + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } + + public List getListOfMaps() { + return listOfMaps; + } + + public void setListOfMaps(List listOfMaps) { + this.listOfMaps = listOfMaps; + } + } + + + @SuppressWarnings("unused") + private static class EnumTester { + + private Autowire autowire; + + public void setAutowire(Autowire autowire) { + this.autowire = autowire; + } + + public Autowire getAutowire() { + return autowire; + } + } + + @SuppressWarnings("unused") + private static class PropsTester { + + private Properties properties; + + private String name; + + private String[] stringArray; + + private int[] intArray; + + public void setProperties(Properties p) { + properties = p; + } + + public void setName(String name) { + this.name = name; + } + + public void setStringArray(String[] sa) { + this.stringArray = sa; + } + + public void setIntArray(int[] intArray) { + this.intArray = intArray; + } + } + + @SuppressWarnings("unused") + private static class StringArrayBean { + + private String[] array; + + public String[] getArray() { + return array; + } + + public void setArray(String[] array) { + this.array = array; + } + } + + + @SuppressWarnings("unused") + private static class PrimitiveArrayBean { + + private int[] array; + + public int[] getArray() { + return array; + } + + public void setArray(int[] array) { + this.array = array; + } + } + + @SuppressWarnings("unused") + private static class Employee extends TestBean { + + private String company; + + public String getCompany() { + return company; + } + + public void setCompany(String co) { + this.company = co; + } + } + + @SuppressWarnings("unused") + private static class DifferentTestBean extends TestBean { + // class to test naming of beans in an error message + } + + @SuppressWarnings("unused") + private static class NumberPropertyBean { + + private byte myPrimitiveByte; + + private Byte myByte; + + private short myPrimitiveShort; + + private Short myShort; + + private int myPrimitiveInt; + + private Integer myInteger; + + private long myPrimitiveLong; + + private Long myLong; + + private float myPrimitiveFloat; + + private Float myFloat; + + private double myPrimitiveDouble; + + private Double myDouble; + + public byte getMyPrimitiveByte() { + return myPrimitiveByte; + } + + public void setMyPrimitiveByte(byte myPrimitiveByte) { + this.myPrimitiveByte = myPrimitiveByte; + } + + public Byte getMyByte() { + return myByte; + } + + public void setMyByte(Byte myByte) { + this.myByte = myByte; + } + + public short getMyPrimitiveShort() { + return myPrimitiveShort; + } + + public void setMyPrimitiveShort(short myPrimitiveShort) { + this.myPrimitiveShort = myPrimitiveShort; + } + + public Short getMyShort() { + return myShort; + } + + public void setMyShort(Short myShort) { + this.myShort = myShort; + } + + public int getMyPrimitiveInt() { + return myPrimitiveInt; + } + + public void setMyPrimitiveInt(int myPrimitiveInt) { + this.myPrimitiveInt = myPrimitiveInt; + } + + public Integer getMyInteger() { + return myInteger; + } + + public void setMyInteger(Integer myInteger) { + this.myInteger = myInteger; + } + + public long getMyPrimitiveLong() { + return myPrimitiveLong; + } + + public void setMyPrimitiveLong(long myPrimitiveLong) { + this.myPrimitiveLong = myPrimitiveLong; + } + + public Long getMyLong() { + return myLong; + } + + public void setMyLong(Long myLong) { + this.myLong = myLong; + } + + public float getMyPrimitiveFloat() { + return myPrimitiveFloat; + } + + public void setMyPrimitiveFloat(float myPrimitiveFloat) { + this.myPrimitiveFloat = myPrimitiveFloat; + } + + public Float getMyFloat() { + return myFloat; + } + + public void setMyFloat(Float myFloat) { + this.myFloat = myFloat; + } + + public double getMyPrimitiveDouble() { + return myPrimitiveDouble; + } + + public void setMyPrimitiveDouble(double myPrimitiveDouble) { + this.myPrimitiveDouble = myPrimitiveDouble; + } + + public Double getMyDouble() { + return myDouble; + } + + public void setMyDouble(Double myDouble) { + this.myDouble = myDouble; + } + } + + + public static class EnumConsumer { + + private Enum enumValue; + + public Enum getEnumValue() { + return enumValue; + } + + public void setEnumValue(Enum enumValue) { + this.enumValue = enumValue; + } + } + + + public static class WildcardEnumConsumer { + + private Enum enumValue; + + public Enum getEnumValue() { + return enumValue; + } + + public void setEnumValue(Enum enumValue) { + this.enumValue = enumValue; + } + } + + + public enum TestEnum { + + TEST_VALUE + } + + + public static class ArrayToObject { + + private Object object; + + public void setObject(Object object) { + this.object = object; + } + + public Object getObject() { + return object; + } + } + + + public static class SkipReaderStub { + + public T[] items; + + public SkipReaderStub() { + } + + @SuppressWarnings("unchecked") + public SkipReaderStub(T... items) { + this.items = items; + } + + @SuppressWarnings("unchecked") + public void setItems(T... items) { + this.items = items; + } + } + + + static class Spr10115Bean { + + private static String prop1; + + public static void setProp1(String prop1) { + Spr10115Bean.prop1 = prop1; + } + } + + interface Spr13837 { + + Integer getSomething(); + + T setSomething(Integer something); + + } + + static class Spr13837Bean implements Spr13837 { + + protected Integer something; + + @Override + public Integer getSomething() { + return this.something; + } + + @Override + @SuppressWarnings("unchecked") + public Spr13837Bean setSomething(final Integer something) { + this.something = something; + return this; + } + } + + @SuppressWarnings("serial") + public static class ReadOnlyMap extends HashMap { + + private boolean frozen = false; + + private boolean accessed = false; + + public ReadOnlyMap() { + this.frozen = true; + } + + public ReadOnlyMap(Map map) { + super(map); + this.frozen = true; + } + + @Override + public V put(K key, V value) { + if (this.frozen) { + throw new UnsupportedOperationException(); + } + else { + return super.put(key, value); + } + } + + @Override + public Set> entrySet() { + this.accessed = true; + return super.entrySet(); + } + + @Override + public Set keySet() { + this.accessed = true; + return super.keySet(); + } + + @Override + public int size() { + this.accessed = true; + return super.size(); + } + + public boolean isAccessed() { + return this.accessed; + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyValuesTests.java b/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyValuesTests.java new file mode 100644 index 0000000..9f3bd08 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyValuesTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rod Johnson + * @author Chris Beams + */ +public abstract class AbstractPropertyValuesTests { + + /** + * Must contain: forname=Tony surname=Blair age=50 + */ + protected void doTestTony(PropertyValues pvs) { + assertThat(pvs.getPropertyValues().length == 3).as("Contains 3").isTrue(); + assertThat(pvs.contains("forname")).as("Contains forname").isTrue(); + assertThat(pvs.contains("surname")).as("Contains surname").isTrue(); + assertThat(pvs.contains("age")).as("Contains age").isTrue(); + boolean condition1 = !pvs.contains("tory"); + assertThat(condition1).as("Doesn't contain tory").isTrue(); + + PropertyValue[] ps = pvs.getPropertyValues(); + Map m = new HashMap<>(); + m.put("forname", "Tony"); + m.put("surname", "Blair"); + m.put("age", "50"); + for (int i = 0; i < ps.length; i++) { + Object val = m.get(ps[i].getName()); + assertThat(val != null).as("Can't have unexpected value").isTrue(); + boolean condition = val instanceof String; + assertThat(condition).as("Val i string").isTrue(); + assertThat(val.equals(ps[i].getValue())).as("val matches expected").isTrue(); + m.remove(ps[i].getName()); + } + assertThat(m.size() == 0).as("Map size is 0").isTrue(); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java b/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java new file mode 100644 index 0000000..ef1bbf4 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java @@ -0,0 +1,636 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.net.URI; +import java.net.URL; +import java.time.DayOfWeek; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import org.springframework.beans.factory.BeanFactory; +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.core.io.Resource; +import org.springframework.core.io.ResourceEditor; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Unit tests for {@link BeanUtils}. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @author Chris Beams + * @author Sebastien Deleuze + * @author Sam Brannen + * @since 19.05.2003 + */ +class BeanUtilsTests { + + @Test + void instantiateClassGivenInterface() { + assertThatExceptionOfType(FatalBeanException.class).isThrownBy(() -> + BeanUtils.instantiateClass(List.class)); + } + + @Test + void instantiateClassGivenClassWithoutDefaultConstructor() { + assertThatExceptionOfType(FatalBeanException.class).isThrownBy(() -> + BeanUtils.instantiateClass(CustomDateEditor.class)); + } + + @Test // gh-22531 + void instantiateClassWithOptionalNullableType() throws NoSuchMethodException { + Constructor ctor = BeanWithNullableTypes.class.getDeclaredConstructor( + Integer.class, Boolean.class, String.class); + BeanWithNullableTypes bean = BeanUtils.instantiateClass(ctor, null, null, "foo"); + assertThat(bean.getCounter()).isNull(); + assertThat(bean.isFlag()).isNull(); + assertThat(bean.getValue()).isEqualTo("foo"); + } + + @Test // gh-22531 + void instantiateClassWithOptionalPrimitiveType() throws NoSuchMethodException { + Constructor ctor = BeanWithPrimitiveTypes.class.getDeclaredConstructor(int.class, boolean.class, String.class); + BeanWithPrimitiveTypes bean = BeanUtils.instantiateClass(ctor, null, null, "foo"); + assertThat(bean.getCounter()).isEqualTo(0); + assertThat(bean.isFlag()).isEqualTo(false); + assertThat(bean.getValue()).isEqualTo("foo"); + } + + @Test // gh-22531 + void instantiateClassWithMoreArgsThanParameters() throws NoSuchMethodException { + Constructor ctor = BeanWithPrimitiveTypes.class.getDeclaredConstructor(int.class, boolean.class, String.class); + assertThatExceptionOfType(BeanInstantiationException.class).isThrownBy(() -> + BeanUtils.instantiateClass(ctor, null, null, "foo", null)); + } + + @Test + void instantiatePrivateClassWithPrivateConstructor() throws NoSuchMethodException { + Constructor ctor = PrivateBeanWithPrivateConstructor.class.getDeclaredConstructor(); + BeanUtils.instantiateClass(ctor); + } + + @Test + void getPropertyDescriptors() throws Exception { + 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); + } + + @Test + void beanPropertyIsArray() { + PropertyDescriptor[] descriptors = BeanUtils.getPropertyDescriptors(ContainerBean.class); + for (PropertyDescriptor descriptor : descriptors) { + if ("containedBeans".equals(descriptor.getName())) { + assertThat(descriptor.getPropertyType().isArray()).as("Property should be an array").isTrue(); + assertThat(ContainedBean.class).isEqualTo(descriptor.getPropertyType().getComponentType()); + } + } + } + + @Test + void findEditorByConvention() { + assertThat(BeanUtils.findEditorByConvention(Resource.class).getClass()).isEqualTo(ResourceEditor.class); + } + + @Test + void copyProperties() throws Exception { + TestBean tb = new TestBean(); + tb.setName("rod"); + 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(); + 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(); + } + + @Test + void copyPropertiesWithDifferentTypes1() throws Exception { + DerivedTestBean tb = new DerivedTestBean(); + tb.setName("rod"); + 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(); + 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(); + } + + @Test + void copyPropertiesWithDifferentTypes2() throws Exception { + TestBean tb = new TestBean(); + tb.setName("rod"); + 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(); + 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(); + } + + @Test + void copyPropertiesHonorsGenericTypeMatches() { + IntegerListHolder1 integerListHolder1 = new IntegerListHolder1(); + integerListHolder1.getList().add(42); + IntegerListHolder2 integerListHolder2 = new IntegerListHolder2(); + + BeanUtils.copyProperties(integerListHolder1, integerListHolder2); + assertThat(integerListHolder1.getList()).containsOnly(42); + assertThat(integerListHolder2.getList()).containsOnly(42); + } + + @Test + void copyPropertiesDoesNotHonorGenericTypeMismatches() { + IntegerListHolder1 integerListHolder = new IntegerListHolder1(); + integerListHolder.getList().add(42); + LongListHolder longListHolder = new LongListHolder(); + + BeanUtils.copyProperties(integerListHolder, longListHolder); + assertThat(integerListHolder.getList()).containsOnly(42); + assertThat(longListHolder.getList()).isEmpty(); + } + + @Test + void copyPropertiesWithEditable() throws Exception { + TestBean tb = new TestBean(); + assertThat(tb.getName() == null).as("Name empty").isTrue(); + 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(); + + // "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(); + } + + @Test + void copyPropertiesWithIgnore() throws Exception { + TestBean tb = new TestBean(); + assertThat(tb.getName() == null).as("Name empty").isTrue(); + 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(); + + // "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(); + } + + @Test + void copyPropertiesWithIgnoredNonExistingProperty() { + NameAndSpecialProperty source = new NameAndSpecialProperty(); + source.setName("name"); + TestBean target = new TestBean(); + BeanUtils.copyProperties(source, target, "specialProperty"); + assertThat("name").isEqualTo(target.getName()); + } + + @Test + void copyPropertiesWithInvalidProperty() { + InvalidProperty source = new InvalidProperty(); + source.setName("name"); + source.setFlag1(true); + source.setFlag2(true); + InvalidProperty target = new InvalidProperty(); + BeanUtils.copyProperties(source, target); + assertThat(target.getName()).isEqualTo("name"); + assertThat((boolean) target.getFlag1()).isTrue(); + assertThat(target.getFlag2()).isTrue(); + } + + @Test + void resolveSimpleSignature() throws Exception { + Method desiredMethod = MethodSignatureBean.class.getMethod("doSomething"); + assertSignatureEquals(desiredMethod, "doSomething"); + assertSignatureEquals(desiredMethod, "doSomething()"); + } + + @Test + void resolveInvalidSignatureEndParen() { + assertThatIllegalArgumentException().isThrownBy(() -> + BeanUtils.resolveSignature("doSomething(", MethodSignatureBean.class)); + } + + @Test + void resolveInvalidSignatureStartParen() { + assertThatIllegalArgumentException().isThrownBy(() -> + BeanUtils.resolveSignature("doSomething)", MethodSignatureBean.class)); + } + + @Test + void resolveWithAndWithoutArgList() throws Exception { + Method desiredMethod = MethodSignatureBean.class.getMethod("doSomethingElse", String.class, int.class); + assertSignatureEquals(desiredMethod, "doSomethingElse"); + assertThat(BeanUtils.resolveSignature("doSomethingElse()", MethodSignatureBean.class)).isNull(); + } + + @Test + void resolveTypedSignature() throws Exception { + Method desiredMethod = MethodSignatureBean.class.getMethod("doSomethingElse", String.class, int.class); + assertSignatureEquals(desiredMethod, "doSomethingElse(java.lang.String, int)"); + } + + @Test + void resolveOverloadedSignature() throws Exception { + // test resolve with no args + Method desiredMethod = MethodSignatureBean.class.getMethod("overloaded"); + assertSignatureEquals(desiredMethod, "overloaded()"); + + // resolve with single arg + desiredMethod = MethodSignatureBean.class.getMethod("overloaded", String.class); + assertSignatureEquals(desiredMethod, "overloaded(java.lang.String)"); + + // resolve with two args + desiredMethod = MethodSignatureBean.class.getMethod("overloaded", String.class, BeanFactory.class); + assertSignatureEquals(desiredMethod, "overloaded(java.lang.String, org.springframework.beans.factory.BeanFactory)"); + } + + @Test + void resolveSignatureWithArray() throws Exception { + Method desiredMethod = MethodSignatureBean.class.getMethod("doSomethingWithAnArray", String[].class); + assertSignatureEquals(desiredMethod, "doSomethingWithAnArray(java.lang.String[])"); + + desiredMethod = MethodSignatureBean.class.getMethod("doSomethingWithAMultiDimensionalArray", String[][].class); + assertSignatureEquals(desiredMethod, "doSomethingWithAMultiDimensionalArray(java.lang.String[][])"); + } + + @Test + void spr6063() { + PropertyDescriptor[] descrs = BeanUtils.getPropertyDescriptors(Bean.class); + + PropertyDescriptor keyDescr = BeanUtils.getPropertyDescriptor(Bean.class, "value"); + assertThat(keyDescr.getPropertyType()).isEqualTo(String.class); + for (PropertyDescriptor propertyDescriptor : descrs) { + if (propertyDescriptor.getName().equals(keyDescr.getName())) { + assertThat(propertyDescriptor.getPropertyType()).as(propertyDescriptor.getName() + " has unexpected type").isEqualTo(keyDescr.getPropertyType()); + } + } + } + + @ParameterizedTest + @ValueSource(classes = { + boolean.class, char.class, byte.class, short.class, int.class, long.class, float.class, double.class, + Boolean.class, Character.class, Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class, + DayOfWeek.class, String.class, LocalDateTime.class, Date.class, URI.class, URL.class, Locale.class, Class.class + }) + void isSimpleValueType(Class type) { + assertThat(BeanUtils.isSimpleValueType(type)).as("Type [" + type.getName() + "] should be a simple value type").isTrue(); + } + + @ParameterizedTest + @ValueSource(classes = { int[].class, Object.class, List.class, void.class, Void.class }) + void isNotSimpleValueType(Class type) { + assertThat(BeanUtils.isSimpleValueType(type)).as("Type [" + type.getName() + "] should not be a simple value type").isFalse(); + } + + @ParameterizedTest + @ValueSource(classes = { + boolean.class, char.class, byte.class, short.class, int.class, long.class, float.class, double.class, + Boolean.class, Character.class, Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class, + DayOfWeek.class, String.class, LocalDateTime.class, Date.class, URI.class, URL.class, Locale.class, Class.class, + boolean[].class, Boolean[].class, LocalDateTime[].class, Date[].class + }) + void isSimpleProperty(Class type) { + assertThat(BeanUtils.isSimpleProperty(type)).as("Type [" + type.getName() + "] should be a simple property").isTrue(); + } + + @ParameterizedTest + @ValueSource(classes = { Object.class, List.class, void.class, Void.class }) + void isNotSimpleProperty(Class type) { + assertThat(BeanUtils.isSimpleProperty(type)).as("Type [" + type.getName() + "] should not be a simple property").isFalse(); + } + + private void assertSignatureEquals(Method desiredMethod, String signature) { + assertThat(BeanUtils.resolveSignature(signature, MethodSignatureBean.class)).isEqualTo(desiredMethod); + } + + + @SuppressWarnings("unused") + private static class IntegerListHolder1 { + + private List list = new ArrayList<>(); + + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } + } + + @SuppressWarnings("unused") + private static class IntegerListHolder2 { + + private List list = new ArrayList<>(); + + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } + } + + @SuppressWarnings("unused") + private static class LongListHolder { + + private List list = new ArrayList<>(); + + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } + } + + + @SuppressWarnings("unused") + private static class NameAndSpecialProperty { + + private String name; + + private int specialProperty; + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + + public void setSpecialProperty(int specialProperty) { + this.specialProperty = specialProperty; + } + + public int getSpecialProperty() { + return specialProperty; + } + } + + + @SuppressWarnings("unused") + private static class InvalidProperty { + + private String name; + + private String value; + + private boolean flag1; + + private boolean flag2; + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + + public void setValue(int value) { + this.value = Integer.toString(value); + } + + public String getValue() { + return this.value; + } + + public void setFlag1(boolean flag1) { + this.flag1 = flag1; + } + + public Boolean getFlag1() { + return this.flag1; + } + + public void setFlag2(Boolean flag2) { + this.flag2 = flag2; + } + + public boolean getFlag2() { + return this.flag2; + } + } + + + @SuppressWarnings("unused") + private static class ContainerBean { + + private ContainedBean[] containedBeans; + + public ContainedBean[] getContainedBeans() { + return containedBeans; + } + + public void setContainedBeans(ContainedBean[] containedBeans) { + this.containedBeans = containedBeans; + } + } + + + @SuppressWarnings("unused") + private static class ContainedBean { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + + @SuppressWarnings("unused") + private static class MethodSignatureBean { + + public void doSomething() { + } + + public void doSomethingElse(String s, int x) { + } + + public void overloaded() { + } + + public void overloaded(String s) { + } + + public void overloaded(String s, BeanFactory beanFactory) { + } + + public void doSomethingWithAnArray(String[] strings) { + } + + public void doSomethingWithAMultiDimensionalArray(String[][] strings) { + } + } + + + private interface MapEntry { + + K getKey(); + + void setKey(V value); + + V getValue(); + + void setValue(V value); + } + + + private static class Bean implements MapEntry { + + private String key; + + private String value; + + @Override + public String getKey() { + return key; + } + + @Override + public void setKey(String aKey) { + key = aKey; + } + + @Override + public String getValue() { + return value; + } + + @Override + public void setValue(String aValue) { + value = aValue; + } + } + + private static class BeanWithNullableTypes { + + private Integer counter; + + private Boolean flag; + + private String value; + + @SuppressWarnings("unused") + public BeanWithNullableTypes(@Nullable Integer counter, @Nullable Boolean flag, String value) { + this.counter = counter; + this.flag = flag; + this.value = value; + } + + @Nullable + public Integer getCounter() { + return counter; + } + + @Nullable + public Boolean isFlag() { + return flag; + } + + public String getValue() { + return value; + } + } + + private static class BeanWithPrimitiveTypes { + + private int counter; + + private boolean flag; + + private String value; + + @SuppressWarnings("unused") + public BeanWithPrimitiveTypes(int counter, boolean flag, String value) { + this.counter = counter; + this.flag = flag; + this.value = value; + } + + public int getCounter() { + return counter; + } + + public boolean isFlag() { + return flag; + } + + public String getValue() { + return value; + } + } + + private static class PrivateBeanWithPrivateConstructor { + + private PrivateBeanWithPrivateConstructor() { + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperAutoGrowingTests.java b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperAutoGrowingTests.java new file mode 100644 index 0000000..c17e2c7 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperAutoGrowingTests.java @@ -0,0 +1,265 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Keith Donald + * @author Juergen Hoeller + */ +public class BeanWrapperAutoGrowingTests { + + private final Bean bean = new Bean(); + + private final BeanWrapperImpl wrapper = new BeanWrapperImpl(bean); + + + @BeforeEach + public void setUp() { + wrapper.setAutoGrowNestedPaths(true); + } + + + @Test + public void getPropertyValueNullValueInNestedPath() { + assertThat(wrapper.getPropertyValue("nested.prop")).isNull(); + } + + @Test + public void setPropertyValueNullValueInNestedPath() { + wrapper.setPropertyValue("nested.prop", "test"); + assertThat(bean.getNested().getProp()).isEqualTo("test"); + } + + @Test + public void getPropertyValueNullValueInNestedPathNoDefaultConstructor() { + assertThatExceptionOfType(NullValueInNestedPathException.class).isThrownBy(() -> + wrapper.getPropertyValue("nestedNoConstructor.prop")); + } + + @Test + public void getPropertyValueAutoGrowArray() { + assertNotNull(wrapper.getPropertyValue("array[0]")); + assertThat(bean.getArray().length).isEqualTo(1); + assertThat(bean.getArray()[0]).isInstanceOf(Bean.class); + } + + private void assertNotNull(Object propertyValue) { + assertThat(propertyValue).isNotNull(); + } + + + @Test + public void setPropertyValueAutoGrowArray() { + wrapper.setPropertyValue("array[0].prop", "test"); + assertThat(bean.getArray()[0].getProp()).isEqualTo("test"); + } + + @Test + public void getPropertyValueAutoGrowArrayBySeveralElements() { + assertNotNull(wrapper.getPropertyValue("array[4]")); + assertThat(bean.getArray().length).isEqualTo(5); + assertThat(bean.getArray()[0]).isInstanceOf(Bean.class); + assertThat(bean.getArray()[1]).isInstanceOf(Bean.class); + assertThat(bean.getArray()[2]).isInstanceOf(Bean.class); + assertThat(bean.getArray()[3]).isInstanceOf(Bean.class); + assertThat(bean.getArray()[4]).isInstanceOf(Bean.class); + assertNotNull(wrapper.getPropertyValue("array[0]")); + assertNotNull(wrapper.getPropertyValue("array[1]")); + assertNotNull(wrapper.getPropertyValue("array[2]")); + assertNotNull(wrapper.getPropertyValue("array[3]")); + } + + @Test + public void getPropertyValueAutoGrowMultiDimensionalArray() { + assertNotNull(wrapper.getPropertyValue("multiArray[0][0]")); + assertThat(bean.getMultiArray()[0].length).isEqualTo(1); + assertThat(bean.getMultiArray()[0][0]).isInstanceOf(Bean.class); + } + + @Test + public void getPropertyValueAutoGrowList() { + assertNotNull(wrapper.getPropertyValue("list[0]")); + assertThat(bean.getList().size()).isEqualTo(1); + assertThat(bean.getList().get(0)).isInstanceOf(Bean.class); + } + + @Test + public void setPropertyValueAutoGrowList() { + wrapper.setPropertyValue("list[0].prop", "test"); + assertThat(bean.getList().get(0).getProp()).isEqualTo("test"); + } + + @Test + public void getPropertyValueAutoGrowListBySeveralElements() { + assertNotNull(wrapper.getPropertyValue("list[4]")); + assertThat(bean.getList().size()).isEqualTo(5); + assertThat(bean.getList().get(0)).isInstanceOf(Bean.class); + assertThat(bean.getList().get(1)).isInstanceOf(Bean.class); + assertThat(bean.getList().get(2)).isInstanceOf(Bean.class); + assertThat(bean.getList().get(3)).isInstanceOf(Bean.class); + assertThat(bean.getList().get(4)).isInstanceOf(Bean.class); + assertNotNull(wrapper.getPropertyValue("list[0]")); + assertNotNull(wrapper.getPropertyValue("list[1]")); + assertNotNull(wrapper.getPropertyValue("list[2]")); + assertNotNull(wrapper.getPropertyValue("list[3]")); + } + + @Test + public void getPropertyValueAutoGrowListFailsAgainstLimit() { + wrapper.setAutoGrowCollectionLimit(2); + assertThatExceptionOfType(InvalidPropertyException.class).isThrownBy(() -> + assertNotNull(wrapper.getPropertyValue("list[4]"))) + .withRootCauseInstanceOf(IndexOutOfBoundsException.class); + } + + @Test + public void getPropertyValueAutoGrowMultiDimensionalList() { + assertNotNull(wrapper.getPropertyValue("multiList[0][0]")); + assertThat(bean.getMultiList().get(0).size()).isEqualTo(1); + assertThat(bean.getMultiList().get(0).get(0)).isInstanceOf(Bean.class); + } + + @Test + public void getPropertyValueAutoGrowListNotParameterized() { + assertThatExceptionOfType(InvalidPropertyException.class).isThrownBy(() -> + wrapper.getPropertyValue("listNotParameterized[0]")); + } + + @Test + public void setPropertyValueAutoGrowMap() { + wrapper.setPropertyValue("map[A]", new Bean()); + assertThat(bean.getMap().get("A")).isInstanceOf(Bean.class); + } + + @Test + public void setNestedPropertyValueAutoGrowMap() { + wrapper.setPropertyValue("map[A].nested", new Bean()); + assertThat(bean.getMap().get("A").getNested()).isInstanceOf(Bean.class); + } + + + @SuppressWarnings("rawtypes") + public static class Bean { + + private String prop; + + private Bean nested; + + private NestedNoDefaultConstructor nestedNoConstructor; + + private Bean[] array; + + private Bean[][] multiArray; + + private List list; + + private List> multiList; + + private List listNotParameterized; + + private Map map; + + public String getProp() { + return prop; + } + + public void setProp(String prop) { + this.prop = prop; + } + + public Bean getNested() { + return nested; + } + + public void setNested(Bean nested) { + this.nested = nested; + } + + public Bean[] getArray() { + return array; + } + + public void setArray(Bean[] array) { + this.array = array; + } + + public Bean[][] getMultiArray() { + return multiArray; + } + + public void setMultiArray(Bean[][] multiArray) { + this.multiArray = multiArray; + } + + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } + + public List> getMultiList() { + return multiList; + } + + public void setMultiList(List> multiList) { + this.multiList = multiList; + } + + public NestedNoDefaultConstructor getNestedNoConstructor() { + return nestedNoConstructor; + } + + public void setNestedNoConstructor(NestedNoDefaultConstructor nestedNoConstructor) { + this.nestedNoConstructor = nestedNoConstructor; + } + + public List getListNotParameterized() { + return listNotParameterized; + } + + public void setListNotParameterized(List listNotParameterized) { + this.listNotParameterized = listNotParameterized; + } + + public Map getMap() { + return map; + } + + public void setMap(Map map) { + this.map = map; + } + } + + + public static class NestedNoDefaultConstructor { + + private NestedNoDefaultConstructor() { + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperEnumTests.java b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperEnumTests.java new file mode 100644 index 0000000..3a5ac37 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperEnumTests.java @@ -0,0 +1,204 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.testfixture.beans.CustomEnum; +import org.springframework.beans.testfixture.beans.GenericBean; +import org.springframework.core.convert.support.DefaultConversionService; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @author Chris Beams + */ +public class BeanWrapperEnumTests { + + @Test + public void testCustomEnum() { + GenericBean gb = new GenericBean<>(); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setPropertyValue("customEnum", "VALUE_1"); + assertThat(gb.getCustomEnum()).isEqualTo(CustomEnum.VALUE_1); + } + + @Test + public void testCustomEnumWithNull() { + GenericBean gb = new GenericBean<>(); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setPropertyValue("customEnum", null); + assertThat(gb.getCustomEnum()).isEqualTo(null); + } + + @Test + public void testCustomEnumWithEmptyString() { + GenericBean gb = new GenericBean<>(); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setPropertyValue("customEnum", ""); + assertThat(gb.getCustomEnum()).isEqualTo(null); + } + + @Test + public void testCustomEnumArrayWithSingleValue() { + GenericBean gb = new GenericBean<>(); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setPropertyValue("customEnumArray", "VALUE_1"); + assertThat(gb.getCustomEnumArray().length).isEqualTo(1); + assertThat(gb.getCustomEnumArray()[0]).isEqualTo(CustomEnum.VALUE_1); + } + + @Test + public void testCustomEnumArrayWithMultipleValues() { + GenericBean gb = new GenericBean<>(); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setPropertyValue("customEnumArray", new String[] {"VALUE_1", "VALUE_2"}); + assertThat(gb.getCustomEnumArray().length).isEqualTo(2); + assertThat(gb.getCustomEnumArray()[0]).isEqualTo(CustomEnum.VALUE_1); + assertThat(gb.getCustomEnumArray()[1]).isEqualTo(CustomEnum.VALUE_2); + } + + @Test + public void testCustomEnumArrayWithMultipleValuesAsCsv() { + GenericBean gb = new GenericBean<>(); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setPropertyValue("customEnumArray", "VALUE_1,VALUE_2"); + assertThat(gb.getCustomEnumArray().length).isEqualTo(2); + assertThat(gb.getCustomEnumArray()[0]).isEqualTo(CustomEnum.VALUE_1); + assertThat(gb.getCustomEnumArray()[1]).isEqualTo(CustomEnum.VALUE_2); + } + + @Test + public void testCustomEnumSetWithSingleValue() { + GenericBean gb = new GenericBean<>(); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setPropertyValue("customEnumSet", "VALUE_1"); + assertThat(gb.getCustomEnumSet().size()).isEqualTo(1); + assertThat(gb.getCustomEnumSet().contains(CustomEnum.VALUE_1)).isTrue(); + } + + @Test + public void testCustomEnumSetWithMultipleValues() { + GenericBean gb = new GenericBean<>(); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setPropertyValue("customEnumSet", new String[] {"VALUE_1", "VALUE_2"}); + assertThat(gb.getCustomEnumSet().size()).isEqualTo(2); + assertThat(gb.getCustomEnumSet().contains(CustomEnum.VALUE_1)).isTrue(); + assertThat(gb.getCustomEnumSet().contains(CustomEnum.VALUE_2)).isTrue(); + } + + @Test + public void testCustomEnumSetWithMultipleValuesAsCsv() { + GenericBean gb = new GenericBean<>(); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setPropertyValue("customEnumSet", "VALUE_1,VALUE_2"); + assertThat(gb.getCustomEnumSet().size()).isEqualTo(2); + assertThat(gb.getCustomEnumSet().contains(CustomEnum.VALUE_1)).isTrue(); + assertThat(gb.getCustomEnumSet().contains(CustomEnum.VALUE_2)).isTrue(); + } + + @Test + public void testCustomEnumSetWithGetterSetterMismatch() { + GenericBean gb = new GenericBean<>(); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setPropertyValue("customEnumSetMismatch", new String[] {"VALUE_1", "VALUE_2"}); + assertThat(gb.getCustomEnumSet().size()).isEqualTo(2); + assertThat(gb.getCustomEnumSet().contains(CustomEnum.VALUE_1)).isTrue(); + assertThat(gb.getCustomEnumSet().contains(CustomEnum.VALUE_2)).isTrue(); + } + + @Test + public void testStandardEnumSetWithMultipleValues() { + GenericBean gb = new GenericBean<>(); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setConversionService(new DefaultConversionService()); + assertThat(gb.getStandardEnumSet()).isNull(); + bw.setPropertyValue("standardEnumSet", new String[] {"VALUE_1", "VALUE_2"}); + assertThat(gb.getStandardEnumSet().size()).isEqualTo(2); + assertThat(gb.getStandardEnumSet().contains(CustomEnum.VALUE_1)).isTrue(); + assertThat(gb.getStandardEnumSet().contains(CustomEnum.VALUE_2)).isTrue(); + } + + @Test + public void testStandardEnumSetWithAutoGrowing() { + GenericBean gb = new GenericBean<>(); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setAutoGrowNestedPaths(true); + assertThat(gb.getStandardEnumSet()).isNull(); + bw.getPropertyValue("standardEnumSet.class"); + assertThat(gb.getStandardEnumSet().size()).isEqualTo(0); + } + + @Test + public void testStandardEnumMapWithMultipleValues() { + GenericBean gb = new GenericBean<>(); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setConversionService(new DefaultConversionService()); + assertThat(gb.getStandardEnumMap()).isNull(); + Map map = new LinkedHashMap<>(); + map.put("VALUE_1", 1); + map.put("VALUE_2", 2); + bw.setPropertyValue("standardEnumMap", map); + assertThat(gb.getStandardEnumMap().size()).isEqualTo(2); + assertThat(gb.getStandardEnumMap().get(CustomEnum.VALUE_1)).isEqualTo(1); + assertThat(gb.getStandardEnumMap().get(CustomEnum.VALUE_2)).isEqualTo(2); + } + + @Test + public void testStandardEnumMapWithAutoGrowing() { + GenericBean gb = new GenericBean<>(); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setAutoGrowNestedPaths(true); + assertThat(gb.getStandardEnumMap()).isNull(); + bw.setPropertyValue("standardEnumMap[VALUE_1]", 1); + assertThat(gb.getStandardEnumMap().size()).isEqualTo(1); + assertThat(gb.getStandardEnumMap().get(CustomEnum.VALUE_1)).isEqualTo(1); + } + + @Test + public void testNonPublicEnum() { + NonPublicEnumHolder holder = new NonPublicEnumHolder(); + BeanWrapper bw = new BeanWrapperImpl(holder); + bw.setPropertyValue("nonPublicEnum", "VALUE_1"); + assertThat(holder.getNonPublicEnum()).isEqualTo(NonPublicEnum.VALUE_1); + } + + + enum NonPublicEnum { + + VALUE_1, VALUE_2; + } + + + static class NonPublicEnumHolder { + + private NonPublicEnum nonPublicEnum; + + public NonPublicEnum getNonPublicEnum() { + return nonPublicEnum; + } + + public void setNonPublicEnum(NonPublicEnum nonPublicEnum) { + this.nonPublicEnum = nonPublicEnum; + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperGenericsTests.java b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperGenericsTests.java new file mode 100644 index 0000000..be0c2cc --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperGenericsTests.java @@ -0,0 +1,672 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.net.MalformedURLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.propertyeditors.CustomNumberEditor; +import org.springframework.beans.propertyeditors.StringTrimmerEditor; +import org.springframework.beans.testfixture.beans.GenericBean; +import org.springframework.beans.testfixture.beans.GenericIntegerBean; +import org.springframework.beans.testfixture.beans.GenericSetOfIntegerBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.io.UrlResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Juergen Hoeller + * @author Chris Beams + * @since 18.01.2006 + */ +public class BeanWrapperGenericsTests { + + @Test + public void testGenericSet() { + GenericBean gb = new GenericBean<>(); + BeanWrapper bw = new BeanWrapperImpl(gb); + Set input = new HashSet<>(); + input.add("4"); + input.add("5"); + bw.setPropertyValue("integerSet", input); + assertThat(gb.getIntegerSet().contains(4)).isTrue(); + assertThat(gb.getIntegerSet().contains(5)).isTrue(); + } + + @Test + public void testGenericLowerBoundedSet() { + GenericBean gb = new GenericBean<>(); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.registerCustomEditor(Number.class, new CustomNumberEditor(Integer.class, true)); + Set input = new HashSet<>(); + input.add("4"); + input.add("5"); + bw.setPropertyValue("numberSet", input); + assertThat(gb.getNumberSet().contains(4)).isTrue(); + assertThat(gb.getNumberSet().contains(5)).isTrue(); + } + + @Test + public void testGenericSetWithConversionFailure() { + GenericBean gb = new GenericBean<>(); + BeanWrapper bw = new BeanWrapperImpl(gb); + Set input = new HashSet<>(); + input.add(new TestBean()); + assertThatExceptionOfType(TypeMismatchException.class).isThrownBy(() -> + bw.setPropertyValue("integerSet", input)) + .withMessageContaining("java.lang.Integer"); + } + + @Test + public void testGenericList() throws MalformedURLException { + GenericBean gb = new GenericBean<>(); + BeanWrapper bw = new BeanWrapperImpl(gb); + List input = new ArrayList<>(); + input.add("http://localhost:8080"); + input.add("http://localhost:9090"); + bw.setPropertyValue("resourceList", input); + assertThat(gb.getResourceList().get(0)).isEqualTo(new UrlResource("http://localhost:8080")); + assertThat(gb.getResourceList().get(1)).isEqualTo(new UrlResource("http://localhost:9090")); + } + + @Test + public void testGenericListElement() throws MalformedURLException { + GenericBean gb = new GenericBean<>(); + gb.setResourceList(new ArrayList<>()); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setPropertyValue("resourceList[0]", "http://localhost:8080"); + assertThat(gb.getResourceList().get(0)).isEqualTo(new UrlResource("http://localhost:8080")); + } + + @Test + public void testGenericMap() { + GenericBean gb = new GenericBean<>(); + BeanWrapper bw = new BeanWrapperImpl(gb); + Map input = new HashMap<>(); + input.put("4", "5"); + input.put("6", "7"); + bw.setPropertyValue("shortMap", input); + assertThat(gb.getShortMap().get(new Short("4"))).isEqualTo(5); + assertThat(gb.getShortMap().get(new Short("6"))).isEqualTo(7); + } + + @Test + public void testGenericMapElement() { + GenericBean gb = new GenericBean<>(); + gb.setShortMap(new HashMap<>()); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setPropertyValue("shortMap[4]", "5"); + assertThat(bw.getPropertyValue("shortMap[4]")).isEqualTo(5); + assertThat(gb.getShortMap().get(new Short("4"))).isEqualTo(5); + } + + @Test + public void testGenericMapWithKeyType() { + GenericBean gb = new GenericBean<>(); + BeanWrapper bw = new BeanWrapperImpl(gb); + Map input = new HashMap<>(); + input.put("4", "5"); + input.put("6", "7"); + bw.setPropertyValue("longMap", input); + assertThat(gb.getLongMap().get(4L)).isEqualTo("5"); + assertThat(gb.getLongMap().get(6L)).isEqualTo("7"); + } + + @Test + public void testGenericMapElementWithKeyType() { + GenericBean gb = new GenericBean<>(); + gb.setLongMap(new HashMap()); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setPropertyValue("longMap[4]", "5"); + assertThat(gb.getLongMap().get(new Long("4"))).isEqualTo("5"); + assertThat(bw.getPropertyValue("longMap[4]")).isEqualTo("5"); + } + + @Test + public void testGenericMapWithCollectionValue() { + GenericBean gb = new GenericBean<>(); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.registerCustomEditor(Number.class, new CustomNumberEditor(Integer.class, false)); + Map> input = new HashMap<>(); + HashSet value1 = new HashSet<>(); + value1.add(1); + input.put("1", value1); + ArrayList value2 = new ArrayList<>(); + value2.add(Boolean.TRUE); + input.put("2", value2); + bw.setPropertyValue("collectionMap", input); + boolean condition1 = gb.getCollectionMap().get(1) instanceof HashSet; + assertThat(condition1).isTrue(); + boolean condition = gb.getCollectionMap().get(2) instanceof ArrayList; + assertThat(condition).isTrue(); + } + + @Test + public void testGenericMapElementWithCollectionValue() { + GenericBean gb = new GenericBean<>(); + gb.setCollectionMap(new HashMap<>()); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.registerCustomEditor(Number.class, new CustomNumberEditor(Integer.class, false)); + HashSet value1 = new HashSet<>(); + value1.add(1); + bw.setPropertyValue("collectionMap[1]", value1); + boolean condition = gb.getCollectionMap().get(1) instanceof HashSet; + assertThat(condition).isTrue(); + } + + @Test + public void testGenericMapFromProperties() { + GenericBean gb = new GenericBean<>(); + BeanWrapper bw = new BeanWrapperImpl(gb); + Properties input = new Properties(); + input.setProperty("4", "5"); + input.setProperty("6", "7"); + bw.setPropertyValue("shortMap", input); + assertThat(gb.getShortMap().get(new Short("4"))).isEqualTo(5); + assertThat(gb.getShortMap().get(new Short("6"))).isEqualTo(7); + } + + @Test + public void testGenericListOfLists() throws MalformedURLException { + GenericBean gb = new GenericBean<>(); + List> list = new ArrayList<>(); + list.add(new ArrayList<>()); + gb.setListOfLists(list); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setPropertyValue("listOfLists[0][0]", 5); + assertThat(bw.getPropertyValue("listOfLists[0][0]")).isEqualTo(5); + assertThat(gb.getListOfLists().get(0).get(0)).isEqualTo(5); + } + + @Test + public void testGenericListOfListsWithElementConversion() throws MalformedURLException { + GenericBean gb = new GenericBean<>(); + List> list = new ArrayList<>(); + list.add(new ArrayList<>()); + gb.setListOfLists(list); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setPropertyValue("listOfLists[0][0]", "5"); + assertThat(bw.getPropertyValue("listOfLists[0][0]")).isEqualTo(5); + assertThat(gb.getListOfLists().get(0).get(0)).isEqualTo(5); + } + + @Test + public void testGenericListOfArrays() throws MalformedURLException { + GenericBean gb = new GenericBean<>(); + ArrayList list = new ArrayList<>(); + list.add(new String[] {"str1", "str2"}); + gb.setListOfArrays(list); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setPropertyValue("listOfArrays[0][1]", "str3 "); + assertThat(bw.getPropertyValue("listOfArrays[0][1]")).isEqualTo("str3 "); + assertThat(gb.getListOfArrays().get(0)[1]).isEqualTo("str3 "); + } + + @Test + public void testGenericListOfArraysWithElementConversion() throws MalformedURLException { + GenericBean gb = new GenericBean<>(); + ArrayList list = new ArrayList<>(); + list.add(new String[] {"str1", "str2"}); + gb.setListOfArrays(list); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.registerCustomEditor(String.class, new StringTrimmerEditor(false)); + bw.setPropertyValue("listOfArrays[0][1]", "str3 "); + assertThat(bw.getPropertyValue("listOfArrays[0][1]")).isEqualTo("str3"); + assertThat(gb.getListOfArrays().get(0)[1]).isEqualTo("str3"); + } + + @Test + public void testGenericListOfMaps() throws MalformedURLException { + GenericBean gb = new GenericBean<>(); + List> list = new ArrayList<>(); + list.add(new HashMap<>()); + gb.setListOfMaps(list); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setPropertyValue("listOfMaps[0][10]", new Long(5)); + assertThat(bw.getPropertyValue("listOfMaps[0][10]")).isEqualTo(new Long(5)); + assertThat(gb.getListOfMaps().get(0).get(10)).isEqualTo(new Long(5)); + } + + @Test + public void testGenericListOfMapsWithElementConversion() throws MalformedURLException { + GenericBean gb = new GenericBean<>(); + List> list = new ArrayList<>(); + list.add(new HashMap<>()); + gb.setListOfMaps(list); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setPropertyValue("listOfMaps[0][10]", "5"); + assertThat(bw.getPropertyValue("listOfMaps[0][10]")).isEqualTo(new Long(5)); + assertThat(gb.getListOfMaps().get(0).get(10)).isEqualTo(new Long(5)); + } + + @Test + public void testGenericMapOfMaps() throws MalformedURLException { + GenericBean gb = new GenericBean<>(); + Map> map = new HashMap<>(); + map.put("mykey", new HashMap<>()); + gb.setMapOfMaps(map); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setPropertyValue("mapOfMaps[mykey][10]", new Long(5)); + assertThat(bw.getPropertyValue("mapOfMaps[mykey][10]")).isEqualTo(new Long(5)); + assertThat(gb.getMapOfMaps().get("mykey").get(10)).isEqualTo(new Long(5)); + } + + @Test + public void testGenericMapOfMapsWithElementConversion() throws MalformedURLException { + GenericBean gb = new GenericBean<>(); + Map> map = new HashMap<>(); + map.put("mykey", new HashMap<>()); + gb.setMapOfMaps(map); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setPropertyValue("mapOfMaps[mykey][10]", "5"); + assertThat(bw.getPropertyValue("mapOfMaps[mykey][10]")).isEqualTo(new Long(5)); + assertThat(gb.getMapOfMaps().get("mykey").get(10)).isEqualTo(new Long(5)); + } + + @Test + public void testGenericMapOfLists() throws MalformedURLException { + GenericBean gb = new GenericBean<>(); + Map> map = new HashMap<>(); + map.put(1, new ArrayList<>()); + gb.setMapOfLists(map); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setPropertyValue("mapOfLists[1][0]", 5); + assertThat(bw.getPropertyValue("mapOfLists[1][0]")).isEqualTo(5); + assertThat(gb.getMapOfLists().get(1).get(0)).isEqualTo(5); + } + + @Test + public void testGenericMapOfListsWithElementConversion() throws MalformedURLException { + GenericBean gb = new GenericBean<>(); + Map> map = new HashMap<>(); + map.put(1, new ArrayList<>()); + gb.setMapOfLists(map); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setPropertyValue("mapOfLists[1][0]", "5"); + assertThat(bw.getPropertyValue("mapOfLists[1][0]")).isEqualTo(5); + assertThat(gb.getMapOfLists().get(1).get(0)).isEqualTo(5); + } + + @Test + public void testGenericTypeNestingMapOfInteger() throws Exception { + Map map = new HashMap<>(); + map.put("testKey", "100"); + + NestedGenericCollectionBean gb = new NestedGenericCollectionBean(); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setPropertyValue("mapOfInteger", map); + + Object obj = gb.getMapOfInteger().get("testKey"); + boolean condition = obj instanceof Integer; + assertThat(condition).isTrue(); + } + + @Test + public void testGenericTypeNestingMapOfListOfInteger() throws Exception { + Map> map = new HashMap<>(); + List list = Arrays.asList(new String[] {"1", "2", "3"}); + map.put("testKey", list); + + NestedGenericCollectionBean gb = new NestedGenericCollectionBean(); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setPropertyValue("mapOfListOfInteger", map); + + Object obj = gb.getMapOfListOfInteger().get("testKey").get(0); + boolean condition = obj instanceof Integer; + assertThat(condition).isTrue(); + assertThat(((Integer) obj).intValue()).isEqualTo(1); + } + + @Test + public void testGenericTypeNestingListOfMapOfInteger() throws Exception { + List> list = new ArrayList<>(); + Map map = new HashMap<>(); + map.put("testKey", "5"); + list.add(map); + + NestedGenericCollectionBean gb = new NestedGenericCollectionBean(); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setPropertyValue("listOfMapOfInteger", list); + + Object obj = gb.getListOfMapOfInteger().get(0).get("testKey"); + boolean condition = obj instanceof Integer; + assertThat(condition).isTrue(); + assertThat(((Integer) obj).intValue()).isEqualTo(5); + } + + @Test + public void testGenericTypeNestingMapOfListOfListOfInteger() throws Exception { + Map>> map = new HashMap<>(); + List list = Arrays.asList(new String[] {"1", "2", "3"}); + map.put("testKey", Collections.singletonList(list)); + + NestedGenericCollectionBean gb = new NestedGenericCollectionBean(); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setPropertyValue("mapOfListOfListOfInteger", map); + + Object obj = gb.getMapOfListOfListOfInteger().get("testKey").get(0).get(0); + boolean condition = obj instanceof Integer; + assertThat(condition).isTrue(); + assertThat(((Integer) obj).intValue()).isEqualTo(1); + } + + @Test + public void testComplexGenericMap() { + Map, List> inputMap = new HashMap<>(); + List inputKey = new ArrayList<>(); + inputKey.add("1"); + List inputValue = new ArrayList<>(); + inputValue.add("10"); + inputMap.put(inputKey, inputValue); + + ComplexMapHolder holder = new ComplexMapHolder(); + BeanWrapper bw = new BeanWrapperImpl(holder); + bw.setPropertyValue("genericMap", inputMap); + + assertThat(holder.getGenericMap().keySet().iterator().next().get(0)).isEqualTo(1); + assertThat(holder.getGenericMap().values().iterator().next().get(0)).isEqualTo(new Long(10)); + } + + @Test + public void testComplexGenericMapWithCollectionConversion() { + Map, Set> inputMap = new HashMap<>(); + Set inputKey = new HashSet<>(); + inputKey.add("1"); + Set inputValue = new HashSet<>(); + inputValue.add("10"); + inputMap.put(inputKey, inputValue); + + ComplexMapHolder holder = new ComplexMapHolder(); + BeanWrapper bw = new BeanWrapperImpl(holder); + bw.setPropertyValue("genericMap", inputMap); + + assertThat(holder.getGenericMap().keySet().iterator().next().get(0)).isEqualTo(1); + assertThat(holder.getGenericMap().values().iterator().next().get(0)).isEqualTo(new Long(10)); + } + + @Test + public void testComplexGenericIndexedMapEntry() { + List inputValue = new ArrayList<>(); + inputValue.add("10"); + + ComplexMapHolder holder = new ComplexMapHolder(); + BeanWrapper bw = new BeanWrapperImpl(holder); + bw.setPropertyValue("genericIndexedMap[1]", inputValue); + + assertThat(holder.getGenericIndexedMap().keySet().iterator().next()).isEqualTo(1); + assertThat(holder.getGenericIndexedMap().values().iterator().next().get(0)).isEqualTo(new Long(10)); + } + + @Test + public void testComplexGenericIndexedMapEntryWithCollectionConversion() { + Set inputValue = new HashSet<>(); + inputValue.add("10"); + + ComplexMapHolder holder = new ComplexMapHolder(); + BeanWrapper bw = new BeanWrapperImpl(holder); + bw.setPropertyValue("genericIndexedMap[1]", inputValue); + + assertThat(holder.getGenericIndexedMap().keySet().iterator().next()).isEqualTo(1); + assertThat(holder.getGenericIndexedMap().values().iterator().next().get(0)).isEqualTo(new Long(10)); + } + + @Test + public void testComplexDerivedIndexedMapEntry() { + List inputValue = new ArrayList<>(); + inputValue.add("10"); + + ComplexMapHolder holder = new ComplexMapHolder(); + BeanWrapper bw = new BeanWrapperImpl(holder); + bw.setPropertyValue("derivedIndexedMap[1]", inputValue); + + assertThat(holder.getDerivedIndexedMap().keySet().iterator().next()).isEqualTo(1); + assertThat(holder.getDerivedIndexedMap().values().iterator().next().get(0)).isEqualTo(new Long(10)); + } + + @Test + public void testComplexDerivedIndexedMapEntryWithCollectionConversion() { + Set inputValue = new HashSet<>(); + inputValue.add("10"); + + ComplexMapHolder holder = new ComplexMapHolder(); + BeanWrapper bw = new BeanWrapperImpl(holder); + bw.setPropertyValue("derivedIndexedMap[1]", inputValue); + + assertThat(holder.getDerivedIndexedMap().keySet().iterator().next()).isEqualTo(1); + assertThat(holder.getDerivedIndexedMap().values().iterator().next().get(0)).isEqualTo(new Long(10)); + } + + @Test + public void testGenericallyTypedIntegerBean() throws Exception { + GenericIntegerBean gb = new GenericIntegerBean(); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setPropertyValue("genericProperty", "10"); + bw.setPropertyValue("genericListProperty", new String[] {"20", "30"}); + assertThat(gb.getGenericProperty()).isEqualTo(10); + assertThat(gb.getGenericListProperty().get(0)).isEqualTo(20); + assertThat(gb.getGenericListProperty().get(1)).isEqualTo(30); + } + + @Test + public void testGenericallyTypedSetOfIntegerBean() throws Exception { + GenericSetOfIntegerBean gb = new GenericSetOfIntegerBean(); + BeanWrapper bw = new BeanWrapperImpl(gb); + bw.setPropertyValue("genericProperty", "10"); + bw.setPropertyValue("genericListProperty", new String[] {"20", "30"}); + assertThat(gb.getGenericProperty().iterator().next()).isEqualTo(10); + assertThat(gb.getGenericListProperty().get(0).iterator().next()).isEqualTo(20); + assertThat(gb.getGenericListProperty().get(1).iterator().next()).isEqualTo(30); + } + + @Test + public void testSettingGenericPropertyWithReadOnlyInterface() { + Bar bar = new Bar(); + BeanWrapper bw = new BeanWrapperImpl(bar); + bw.setPropertyValue("version", "10"); + assertThat(bar.getVersion()).isEqualTo(new Double(10.0)); + } + + @Test + public void testSettingLongPropertyWithGenericInterface() { + Promotion bean = new Promotion(); + BeanWrapper bw = new BeanWrapperImpl(bean); + bw.setPropertyValue("id", "10"); + assertThat(bean.getId()).isEqualTo(new Long(10)); + } + + @Test + public void testUntypedPropertyWithMapAtRuntime() { + class Holder { + private final D data; + public Holder(D data) { + this.data = data; + } + @SuppressWarnings("unused") + public D getData() { + return this.data; + } + } + + Map data = new HashMap<>(); + data.put("x", "y"); + Holder> context = new Holder<>(data); + + BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(context); + assertThat(bw.getPropertyValue("data['x']")).isEqualTo("y"); + + bw.setPropertyValue("data['message']", "it works!"); + assertThat(data.get("message")).isEqualTo("it works!"); + } + + + private static abstract class BaseGenericCollectionBean { + + public abstract Object getMapOfInteger(); + + public abstract Map> getMapOfListOfInteger(); + + public abstract void setMapOfListOfInteger(Map> mapOfListOfInteger); + } + + + @SuppressWarnings("unused") + private static class NestedGenericCollectionBean extends BaseGenericCollectionBean { + + private Map mapOfInteger; + + private Map> mapOfListOfInteger; + + private List> listOfMapOfInteger; + + private Map>> mapOfListOfListOfInteger; + + @Override + public Map getMapOfInteger() { + return mapOfInteger; + } + + public void setMapOfInteger(Map mapOfInteger) { + this.mapOfInteger = mapOfInteger; + } + + @Override + public Map> getMapOfListOfInteger() { + return mapOfListOfInteger; + } + + @Override + public void setMapOfListOfInteger(Map> mapOfListOfInteger) { + this.mapOfListOfInteger = mapOfListOfInteger; + } + + public List> getListOfMapOfInteger() { + return listOfMapOfInteger; + } + + public void setListOfMapOfInteger(List> listOfMapOfInteger) { + this.listOfMapOfInteger = listOfMapOfInteger; + } + + public Map>> getMapOfListOfListOfInteger() { + return mapOfListOfListOfInteger; + } + + public void setMapOfListOfListOfInteger(Map>> mapOfListOfListOfInteger) { + this.mapOfListOfListOfInteger = mapOfListOfListOfInteger; + } + } + + + @SuppressWarnings("unused") + private static class ComplexMapHolder { + + private Map, List> genericMap; + + private Map> genericIndexedMap = new HashMap<>(); + + private DerivedMap derivedIndexedMap = new DerivedMap(); + + public void setGenericMap(Map, List> genericMap) { + this.genericMap = genericMap; + } + + public Map, List> getGenericMap() { + return genericMap; + } + + public void setGenericIndexedMap(Map> genericIndexedMap) { + this.genericIndexedMap = genericIndexedMap; + } + + public Map> getGenericIndexedMap() { + return genericIndexedMap; + } + + public void setDerivedIndexedMap(DerivedMap derivedIndexedMap) { + this.derivedIndexedMap = derivedIndexedMap; + } + + public DerivedMap getDerivedIndexedMap() { + return derivedIndexedMap; + } + } + + + @SuppressWarnings("serial") + private static class DerivedMap extends HashMap> { + + } + + + public interface Foo { + + Number getVersion(); + } + + + public class Bar implements Foo { + + private double version; + + @Override + public Double getVersion() { + return this.version; + } + + public void setVersion(Double theDouble) { + this.version = theDouble; + } + } + + + public interface ObjectWithId> { + + T getId(); + + void setId(T aId); + } + + + public class Promotion implements ObjectWithId { + + private Long id; + + @Override + public Long getId() { + return id; + } + + @Override + public void setId(Long aId) { + this.id = aId; + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java new file mode 100644 index 0000000..0711189 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/BeanWrapperTests.java @@ -0,0 +1,320 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.util.Collections; +import java.util.Map; +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Specific {@link BeanWrapperImpl} tests. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Alef Arendsen + * @author Arjen Poutsma + * @author Chris Beams + * @author Dave Syer + */ +public class BeanWrapperTests extends AbstractPropertyAccessorTests { + + @Override + protected BeanWrapperImpl createAccessor(Object target) { + return new BeanWrapperImpl(target); + } + + + @Test + public void setterDoesNotCallGetter() { + GetterBean target = new GetterBean(); + BeanWrapper accessor = createAccessor(target); + accessor.setPropertyValue("name", "tom"); + assertThat(target.getAliasedName()).isEqualTo("tom"); + assertThat(accessor.getPropertyValue("aliasedName")).isEqualTo("tom"); + } + + @Test + public void getterSilentlyFailWithOldValueExtraction() { + GetterBean target = new GetterBean(); + BeanWrapper accessor = createAccessor(target); + accessor.setExtractOldValueForEditor(true); // This will call the getter + accessor.setPropertyValue("name", "tom"); + assertThat(target.getAliasedName()).isEqualTo("tom"); + assertThat(accessor.getPropertyValue("aliasedName")).isEqualTo("tom"); + } + + @Test + public void aliasedSetterThroughDefaultMethod() { + GetterBean target = new GetterBean(); + BeanWrapper accessor = createAccessor(target); + accessor.setPropertyValue("aliasedName", "tom"); + assertThat(target.getAliasedName()).isEqualTo("tom"); + assertThat(accessor.getPropertyValue("aliasedName")).isEqualTo("tom"); + } + + @Test + public void setValidAndInvalidPropertyValuesShouldContainExceptionDetails() { + TestBean target = new TestBean(); + String newName = "tony"; + String invalidTouchy = ".valid"; + BeanWrapper accessor = createAccessor(target); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.addPropertyValue(new PropertyValue("age", "foobar")); + pvs.addPropertyValue(new PropertyValue("name", newName)); + pvs.addPropertyValue(new PropertyValue("touchy", invalidTouchy)); + assertThatExceptionOfType(PropertyBatchUpdateException.class).isThrownBy(() -> + accessor.setPropertyValues(pvs)) + .satisfies(ex -> { + assertThat(ex.getExceptionCount()).isEqualTo(2); + assertThat(ex.getPropertyAccessException("touchy").getPropertyChangeEvent() + .getNewValue()).isEqualTo(invalidTouchy); + }); + // Test validly set property matches + assertThat(target.getName().equals(newName)).as("Valid set property must stick").isTrue(); + assertThat(target.getAge() == 0).as("Invalid set property must retain old value").isTrue(); + } + + @Test + public void checkNotWritablePropertyHoldPossibleMatches() { + TestBean target = new TestBean(); + BeanWrapper accessor = createAccessor(target); + assertThatExceptionOfType(NotWritablePropertyException.class).isThrownBy(() -> + accessor.setPropertyValue("ag", "foobar")) + .satisfies(ex -> assertThat(ex.getPossibleMatches()).containsExactly("age")); + } + + @Test // Can't be shared; there is no such thing as a read-only field + public void setReadOnlyMapProperty() { + TypedReadOnlyMap map = new TypedReadOnlyMap(Collections.singletonMap("key", new TestBean())); + TypedReadOnlyMapClient target = new TypedReadOnlyMapClient(); + BeanWrapper accessor = createAccessor(target); + accessor.setPropertyValue("map", map); + } + + @Test + public void notWritablePropertyExceptionContainsAlternativeMatch() { + IntelliBean target = new IntelliBean(); + BeanWrapper bw = createAccessor(target); + try { + bw.setPropertyValue("names", "Alef"); + } + catch (NotWritablePropertyException ex) { + assertThat(ex.getPossibleMatches()).as("Possible matches not determined").isNotNull(); + assertThat(ex.getPossibleMatches().length).as("Invalid amount of alternatives").isEqualTo(1); + } + } + + @Test + public void notWritablePropertyExceptionContainsAlternativeMatches() { + IntelliBean target = new IntelliBean(); + BeanWrapper bw = createAccessor(target); + try { + bw.setPropertyValue("mystring", "Arjen"); + } + catch (NotWritablePropertyException ex) { + assertThat(ex.getPossibleMatches()).as("Possible matches not determined").isNotNull(); + assertThat(ex.getPossibleMatches().length).as("Invalid amount of alternatives").isEqualTo(3); + } + } + + @Override + @Test // Can't be shared: no type mismatch with a field + public void setPropertyTypeMismatch() { + PropertyTypeMismatch target = new PropertyTypeMismatch(); + BeanWrapper accessor = createAccessor(target); + accessor.setPropertyValue("object", "a String"); + assertThat(target.value).isEqualTo("a String"); + assertThat(target.getObject() == 8).isTrue(); + assertThat(accessor.getPropertyValue("object")).isEqualTo(8); + } + + @Test + public void propertyDescriptors() { + TestBean target = new TestBean(); + target.setSpouse(new TestBean()); + BeanWrapper accessor = createAccessor(target); + accessor.setPropertyValue("name", "a"); + accessor.setPropertyValue("spouse.name", "b"); + assertThat(target.getName()).isEqualTo("a"); + assertThat(target.getSpouse().getName()).isEqualTo("b"); + assertThat(accessor.getPropertyValue("name")).isEqualTo("a"); + assertThat(accessor.getPropertyValue("spouse.name")).isEqualTo("b"); + assertThat(accessor.getPropertyDescriptor("name").getPropertyType()).isEqualTo(String.class); + assertThat(accessor.getPropertyDescriptor("spouse.name").getPropertyType()).isEqualTo(String.class); + } + + @Test + @SuppressWarnings("unchecked") + public void getPropertyWithOptional() { + GetterWithOptional target = new GetterWithOptional(); + TestBean tb = new TestBean("x"); + BeanWrapper accessor = createAccessor(target); + + accessor.setPropertyValue("object", tb); + assertThat(target.value).isSameAs(tb); + assertThat(target.getObject().get()).isSameAs(tb); + assertThat(((Optional) accessor.getPropertyValue("object")).get()).isSameAs(tb); + assertThat(target.value.getName()).isEqualTo("x"); + assertThat(target.getObject().get().getName()).isEqualTo("x"); + assertThat(accessor.getPropertyValue("object.name")).isEqualTo("x"); + + accessor.setPropertyValue("object.name", "y"); + assertThat(target.value).isSameAs(tb); + assertThat(target.getObject().get()).isSameAs(tb); + assertThat(((Optional) accessor.getPropertyValue("object")).get()).isSameAs(tb); + assertThat(target.value.getName()).isEqualTo("y"); + assertThat(target.getObject().get().getName()).isEqualTo("y"); + assertThat(accessor.getPropertyValue("object.name")).isEqualTo("y"); + } + + @Test + public void getPropertyWithOptionalAndAutoGrow() { + GetterWithOptional target = new GetterWithOptional(); + BeanWrapper accessor = createAccessor(target); + accessor.setAutoGrowNestedPaths(true); + + accessor.setPropertyValue("object.name", "x"); + assertThat(target.value.getName()).isEqualTo("x"); + assertThat(target.getObject().get().getName()).isEqualTo("x"); + assertThat(accessor.getPropertyValue("object.name")).isEqualTo("x"); + } + + @Test + public void incompletelyQuotedKeyLeadsToPropertyException() { + TestBean target = new TestBean(); + BeanWrapper accessor = createAccessor(target); + assertThatExceptionOfType(NotWritablePropertyException.class).isThrownBy(() -> + accessor.setPropertyValue("[']", "foobar")) + .satisfies(ex -> assertThat(ex.getPossibleMatches()).isNull()); + } + + + private interface BaseProperty { + + default String getAliasedName() { + return getName(); + } + + String getName(); + } + + + @SuppressWarnings("unused") + private interface AliasedProperty extends BaseProperty { + + default void setAliasedName(String name) { + setName(name); + } + + void setName(String name); + } + + + @SuppressWarnings("unused") + private static class GetterBean implements AliasedProperty { + + private String name; + + @Override + public void setName(String name) { + this.name = name; + } + + @Override + public String getName() { + if (this.name == null) { + throw new RuntimeException("name property must be set"); + } + return name; + } + } + + + @SuppressWarnings("unused") + private static class IntelliBean { + + public void setName(String name) { + } + + public void setMyString(String string) { + } + + public void setMyStrings(String string) { + } + + public void setMyStriNg(String string) { + } + + public void setMyStringss(String string) { + } + } + + + @SuppressWarnings("serial") + public static class TypedReadOnlyMap extends ReadOnlyMap { + + public TypedReadOnlyMap() { + } + + public TypedReadOnlyMap(Map map) { + super(map); + } + } + + + public static class TypedReadOnlyMapClient { + + public void setMap(TypedReadOnlyMap map) { + } + } + + + public static class PropertyTypeMismatch { + + public String value; + + public void setObject(String object) { + this.value = object; + } + + public Integer getObject() { + return (this.value != null ? this.value.length() : null); + } + } + + + public static class GetterWithOptional { + + public TestBean value; + + public void setObject(TestBean object) { + this.value = object; + } + + public Optional getObject() { + return Optional.ofNullable(this.value); + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/CachedIntrospectionResultsTests.java b/spring-beans/src/test/java/org/springframework/beans/CachedIntrospectionResultsTests.java new file mode 100644 index 0000000..cba7e49 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/CachedIntrospectionResultsTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.beans.BeanInfo; +import java.beans.PropertyDescriptor; +import java.util.ArrayList; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.OverridingClassLoader; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @author Chris Beams + * @author Arjen Poutsma + */ +public class CachedIntrospectionResultsTests { + + @Test + public void acceptAndClearClassLoader() throws Exception { + BeanWrapper bw = new BeanWrapperImpl(TestBean.class); + assertThat(bw.isWritableProperty("name")).isTrue(); + assertThat(bw.isWritableProperty("age")).isTrue(); + assertThat(CachedIntrospectionResults.strongClassCache.containsKey(TestBean.class)).isTrue(); + + ClassLoader child = new OverridingClassLoader(getClass().getClassLoader()); + Class tbClass = child.loadClass("org.springframework.beans.testfixture.beans.TestBean"); + assertThat(CachedIntrospectionResults.strongClassCache.containsKey(tbClass)).isFalse(); + CachedIntrospectionResults.acceptClassLoader(child); + bw = new BeanWrapperImpl(tbClass); + assertThat(bw.isWritableProperty("name")).isTrue(); + assertThat(bw.isWritableProperty("age")).isTrue(); + assertThat(CachedIntrospectionResults.strongClassCache.containsKey(tbClass)).isTrue(); + CachedIntrospectionResults.clearClassLoader(child); + assertThat(CachedIntrospectionResults.strongClassCache.containsKey(tbClass)).isFalse(); + + assertThat(CachedIntrospectionResults.strongClassCache.containsKey(TestBean.class)).isTrue(); + } + + @Test + public void clearClassLoaderForSystemClassLoader() throws Exception { + BeanUtils.getPropertyDescriptors(ArrayList.class); + assertThat(CachedIntrospectionResults.strongClassCache.containsKey(ArrayList.class)).isTrue(); + CachedIntrospectionResults.clearClassLoader(ArrayList.class.getClassLoader()); + assertThat(CachedIntrospectionResults.strongClassCache.containsKey(ArrayList.class)).isFalse(); + } + + @Test + public void shouldUseExtendedBeanInfoWhenApplicable() throws NoSuchMethodException, SecurityException { + // given a class with a non-void returning setter method + @SuppressWarnings("unused") + class C { + public Object setFoo(String s) { return this; } + public String getFoo() { return null; } + } + + // CachedIntrospectionResults should delegate to ExtendedBeanInfo + CachedIntrospectionResults results = CachedIntrospectionResults.forClass(C.class); + BeanInfo info = results.getBeanInfo(); + PropertyDescriptor pd = null; + for (PropertyDescriptor candidate : info.getPropertyDescriptors()) { + if (candidate.getName().equals("foo")) { + pd = candidate; + } + } + + // resulting in a property descriptor including the non-standard setFoo method + assertThat(pd).isNotNull(); + assertThat(pd.getReadMethod()).isEqualTo(C.class.getMethod("getFoo")); + // No write method found for non-void returning 'setFoo' method. + // Check to see if CachedIntrospectionResults is delegating to ExtendedBeanInfo as expected + assertThat(pd.getWriteMethod()).isEqualTo(C.class.getMethod("setFoo", String.class)); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/ConcurrentBeanWrapperTests.java b/spring-beans/src/test/java/org/springframework/beans/ConcurrentBeanWrapperTests.java new file mode 100644 index 0000000..f1d4912 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/ConcurrentBeanWrapperTests.java @@ -0,0 +1,152 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Properties; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Guillaume Poirier + * @author Juergen Hoeller + * @author Chris Beams + * @since 08.03.2004 + */ +public class ConcurrentBeanWrapperTests { + + private final Log logger = LogFactory.getLog(getClass()); + + private Set set = Collections.synchronizedSet(new HashSet()); + + private Throwable ex = null; + + @Test + public void testSingleThread() { + for (int i = 0; i < 100; i++) { + performSet(); + } + } + + @Test + public void testConcurrent() { + for (int i = 0; i < 10; i++) { + TestRun run = new TestRun(this); + set.add(run); + Thread t = new Thread(run); + t.setDaemon(true); + t.start(); + } + logger.info("Thread creation over, " + set.size() + " still active."); + synchronized (this) { + while (!set.isEmpty() && ex == null) { + try { + wait(); + } + catch (InterruptedException e) { + logger.info(e.toString()); + } + logger.info(set.size() + " threads still active."); + } + } + if (ex != null) { + throw new AssertionError("Unexpected exception", ex); + } + } + + private static void performSet() { + TestBean bean = new TestBean(); + + Properties p = (Properties) System.getProperties().clone(); + + assertThat(p.size() != 0).as("The System properties must not be empty").isTrue(); + + for (Iterator i = p.entrySet().iterator(); i.hasNext();) { + i.next(); + if (Math.random() > 0.9) { + i.remove(); + } + } + + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + try { + p.store(buffer, null); + } + catch (IOException e) { + // ByteArrayOutputStream does not throw + // any IOException + } + String value = new String(buffer.toByteArray()); + + BeanWrapperImpl wrapper = new BeanWrapperImpl(bean); + wrapper.setPropertyValue("properties", value); + assertThat(bean.getProperties()).isEqualTo(p); + } + + + private static class TestRun implements Runnable { + + private ConcurrentBeanWrapperTests test; + + public TestRun(ConcurrentBeanWrapperTests test) { + this.test = test; + } + + @Override + public void run() { + try { + for (int i = 0; i < 100; i++) { + performSet(); + } + } + catch (Throwable e) { + test.ex = e; + } + finally { + synchronized (test) { + test.set.remove(this); + test.notifyAll(); + } + } + } + } + + + @SuppressWarnings("unused") + private static class TestBean { + + private Properties properties; + + public Properties getProperties() { + return properties; + } + + public void setProperties(Properties properties) { + this.properties = properties; + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/DirectFieldAccessorTests.java b/spring-beans/src/test/java/org/springframework/beans/DirectFieldAccessorTests.java new file mode 100644 index 0000000..1ca8ddc --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/DirectFieldAccessorTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Specific {@link DirectFieldAccessor} tests. + * + * @author Jose Luis Martin + * @author Chris Beams + * @author Stephane Nicoll + */ +public class DirectFieldAccessorTests extends AbstractPropertyAccessorTests { + + @Override + protected DirectFieldAccessor createAccessor(Object target) { + return new DirectFieldAccessor(target); + } + + + @Test + public void withShadowedField() { + final StringBuilder sb = new StringBuilder(); + + TestBean target = new TestBean() { + @SuppressWarnings("unused") + StringBuilder name = sb; + }; + + DirectFieldAccessor dfa = createAccessor(target); + assertThat(dfa.getPropertyType("name")).isEqualTo(StringBuilder.class); + assertThat(dfa.getPropertyValue("name")).isEqualTo(sb); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/ExtendedBeanInfoFactoryTests.java b/spring-beans/src/test/java/org/springframework/beans/ExtendedBeanInfoFactoryTests.java new file mode 100644 index 0000000..c4449bc --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/ExtendedBeanInfoFactoryTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.beans.IntrospectionException; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Unit tests for {@link ExtendedBeanInfoTests}. + * + * @author Chris Beams + */ +public class ExtendedBeanInfoFactoryTests { + + private ExtendedBeanInfoFactory factory = new ExtendedBeanInfoFactory(); + + @Test + public void shouldNotSupportClassHavingOnlyVoidReturningSetter() throws IntrospectionException { + @SuppressWarnings("unused") + class C { + public void setFoo(String s) { } + } + assertThat(factory.getBeanInfo(C.class)).isNull(); + } + + @Test + public void shouldSupportClassHavingNonVoidReturningSetter() throws IntrospectionException { + @SuppressWarnings("unused") + class C { + public C setFoo(String s) { return this; } + } + assertThat(factory.getBeanInfo(C.class)).isNotNull(); + } + + @Test + public void shouldSupportClassHavingNonVoidReturningIndexedSetter() throws IntrospectionException { + @SuppressWarnings("unused") + class C { + public C setFoo(int i, String s) { return this; } + } + assertThat(factory.getBeanInfo(C.class)).isNotNull(); + } + + @Test + public void shouldNotSupportClassHavingNonPublicNonVoidReturningIndexedSetter() throws IntrospectionException { + @SuppressWarnings("unused") + class C { + void setBar(String s) { } + } + assertThat(factory.getBeanInfo(C.class)).isNull(); + } + + @Test + public void shouldNotSupportClassHavingNonVoidReturningParameterlessSetter() throws IntrospectionException { + @SuppressWarnings("unused") + class C { + C setBar() { return this; } + } + assertThat(factory.getBeanInfo(C.class)).isNull(); + } + + @Test + public void shouldNotSupportClassHavingNonVoidReturningMethodNamedSet() throws IntrospectionException { + @SuppressWarnings("unused") + class C { + C set(String s) { return this; } + } + assertThat(factory.getBeanInfo(C.class)).isNull(); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/ExtendedBeanInfoTests.java b/spring-beans/src/test/java/org/springframework/beans/ExtendedBeanInfoTests.java new file mode 100644 index 0000000..d987672 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/ExtendedBeanInfoTests.java @@ -0,0 +1,987 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.beans.BeanInfo; +import java.beans.IndexedPropertyDescriptor; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.math.BigDecimal; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Chris Beams + * @author Juergen Hoeller + * @author Sam Brannen + * @since 3.1 + */ +public class ExtendedBeanInfoTests { + + @Test + public void standardReadMethodOnly() throws IntrospectionException { + @SuppressWarnings("unused") class C { + public String getFoo() { return null; } + } + + BeanInfo bi = Introspector.getBeanInfo(C.class); + ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi); + + assertThat(hasReadMethodForProperty(bi, "foo")).isTrue(); + assertThat(hasWriteMethodForProperty(bi, "foo")).isFalse(); + + assertThat(hasReadMethodForProperty(ebi, "foo")).isTrue(); + assertThat(hasWriteMethodForProperty(ebi, "foo")).isFalse(); + } + + @Test + public void standardWriteMethodOnly() throws IntrospectionException { + @SuppressWarnings("unused") class C { + public void setFoo(String f) { } + } + + BeanInfo bi = Introspector.getBeanInfo(C.class); + ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi); + + assertThat(hasReadMethodForProperty(bi, "foo")).isFalse(); + assertThat(hasWriteMethodForProperty(bi, "foo")).isTrue(); + + assertThat(hasReadMethodForProperty(ebi, "foo")).isFalse(); + assertThat(hasWriteMethodForProperty(ebi, "foo")).isTrue(); + } + + @Test + public void standardReadAndWriteMethods() throws IntrospectionException { + @SuppressWarnings("unused") class C { + public void setFoo(String f) { } + public String getFoo() { return null; } + } + + BeanInfo bi = Introspector.getBeanInfo(C.class); + ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi); + + assertThat(hasReadMethodForProperty(bi, "foo")).isTrue(); + assertThat(hasWriteMethodForProperty(bi, "foo")).isTrue(); + + assertThat(hasReadMethodForProperty(ebi, "foo")).isTrue(); + assertThat(hasWriteMethodForProperty(ebi, "foo")).isTrue(); + } + + @Test + public void nonStandardWriteMethodOnly() throws IntrospectionException { + @SuppressWarnings("unused") class C { + public C setFoo(String foo) { return this; } + } + + BeanInfo bi = Introspector.getBeanInfo(C.class); + ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi); + + assertThat(hasReadMethodForProperty(bi, "foo")).isFalse(); + assertThat(hasWriteMethodForProperty(bi, "foo")).isFalse(); + + assertThat(hasReadMethodForProperty(ebi, "foo")).isFalse(); + assertThat(hasWriteMethodForProperty(ebi, "foo")).isTrue(); + } + + @Test + public void standardReadAndNonStandardWriteMethods() throws IntrospectionException { + @SuppressWarnings("unused") class C { + public String getFoo() { return null; } + public C setFoo(String foo) { return this; } + } + + BeanInfo bi = Introspector.getBeanInfo(C.class); + + assertThat(hasReadMethodForProperty(bi, "foo")).isTrue(); + assertThat(hasWriteMethodForProperty(bi, "foo")).isFalse(); + + ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi); + + assertThat(hasReadMethodForProperty(bi, "foo")).isTrue(); + assertThat(hasWriteMethodForProperty(bi, "foo")).isFalse(); + + assertThat(hasReadMethodForProperty(ebi, "foo")).isTrue(); + assertThat(hasWriteMethodForProperty(ebi, "foo")).isTrue(); + } + + @Test + public void standardReadAndNonStandardIndexedWriteMethod() throws IntrospectionException { + @SuppressWarnings("unused") class C { + public String[] getFoo() { return null; } + public C setFoo(int i, String foo) { return this; } + } + + BeanInfo bi = Introspector.getBeanInfo(C.class); + + assertThat(hasReadMethodForProperty(bi, "foo")).isTrue(); + assertThat(hasWriteMethodForProperty(bi, "foo")).isFalse(); + assertThat(hasIndexedWriteMethodForProperty(bi, "foo")).isFalse(); + + BeanInfo ebi = new ExtendedBeanInfo(bi); + + assertThat(hasReadMethodForProperty(ebi, "foo")).isTrue(); + assertThat(hasWriteMethodForProperty(ebi, "foo")).isFalse(); + assertThat(hasIndexedWriteMethodForProperty(ebi, "foo")).isTrue(); + } + + @Test + public void standardReadMethodsAndOverloadedNonStandardWriteMethods() throws Exception { + @SuppressWarnings("unused") class C { + public String getFoo() { return null; } + public C setFoo(String foo) { return this; } + public C setFoo(Number foo) { return this; } + } + + BeanInfo bi = Introspector.getBeanInfo(C.class); + + assertThat(hasReadMethodForProperty(bi, "foo")).isTrue(); + assertThat(hasWriteMethodForProperty(bi, "foo")).isFalse(); + + ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi); + + assertThat(hasReadMethodForProperty(bi, "foo")).isTrue(); + assertThat(hasWriteMethodForProperty(bi, "foo")).isFalse(); + + assertThat(hasReadMethodForProperty(ebi, "foo")).isTrue(); + assertThat(hasWriteMethodForProperty(ebi, "foo")).isTrue(); + + for (PropertyDescriptor pd : ebi.getPropertyDescriptors()) { + if (pd.getName().equals("foo")) { + assertThat(pd.getWriteMethod()).isEqualTo(C.class.getMethod("setFoo", String.class)); + return; + } + } + throw new AssertionError("never matched write method"); + } + + @Test + public void cornerSpr9414() throws IntrospectionException { + @SuppressWarnings("unused") class Parent { + public Number getProperty1() { + return 1; + } + } + class Child extends Parent { + @Override + public Integer getProperty1() { + return 2; + } + } + { // always passes + ExtendedBeanInfo bi = new ExtendedBeanInfo(Introspector.getBeanInfo(Parent.class)); + assertThat(hasReadMethodForProperty(bi, "property1")).isTrue(); + } + { // failed prior to fix for SPR-9414 + ExtendedBeanInfo bi = new ExtendedBeanInfo(Introspector.getBeanInfo(Child.class)); + assertThat(hasReadMethodForProperty(bi, "property1")).isTrue(); + } + } + + @Test + public void cornerSpr9453() throws IntrospectionException { + final class Bean implements Spr9453> { + @Override + public Class getProp() { + return null; + } + } + { // always passes + BeanInfo info = Introspector.getBeanInfo(Bean.class); + assertThat(info.getPropertyDescriptors().length).isEqualTo(2); + } + { // failed prior to fix for SPR-9453 + BeanInfo info = new ExtendedBeanInfo(Introspector.getBeanInfo(Bean.class)); + assertThat(info.getPropertyDescriptors().length).isEqualTo(2); + } + } + + @Test + public void standardReadMethodInSuperclassAndNonStandardWriteMethodInSubclass() throws Exception { + @SuppressWarnings("unused") class B { + public String getFoo() { return null; } + } + @SuppressWarnings("unused") class C extends B { + public C setFoo(String foo) { return this; } + } + + BeanInfo bi = Introspector.getBeanInfo(C.class); + + assertThat(hasReadMethodForProperty(bi, "foo")).isTrue(); + assertThat(hasWriteMethodForProperty(bi, "foo")).isFalse(); + + ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi); + + assertThat(hasReadMethodForProperty(bi, "foo")).isTrue(); + assertThat(hasWriteMethodForProperty(bi, "foo")).isFalse(); + + assertThat(hasReadMethodForProperty(ebi, "foo")).isTrue(); + assertThat(hasWriteMethodForProperty(ebi, "foo")).isTrue(); + } + + @Test + public void standardReadMethodInSuperAndSubclassesAndGenericBuilderStyleNonStandardWriteMethodInSuperAndSubclasses() throws Exception { + abstract class B> { + @SuppressWarnings("unchecked") + protected final This instance = (This) this; + private String foo; + public String getFoo() { return foo; } + public This setFoo(String foo) { + this.foo = foo; + return this.instance; + } + } + + class C extends B { + private int bar = -1; + public int getBar() { return bar; } + public C setBar(int bar) { + this.bar = bar; + return this.instance; + } + } + + C c = new C() + .setFoo("blue") + .setBar(42); + + assertThat(c.getFoo()).isEqualTo("blue"); + assertThat(c.getBar()).isEqualTo(42); + + BeanInfo bi = Introspector.getBeanInfo(C.class); + + assertThat(hasReadMethodForProperty(bi, "foo")).isTrue(); + assertThat(hasWriteMethodForProperty(bi, "foo")).isFalse(); + + assertThat(hasReadMethodForProperty(bi, "bar")).isTrue(); + assertThat(hasWriteMethodForProperty(bi, "bar")).isFalse(); + + BeanInfo ebi = new ExtendedBeanInfo(bi); + + assertThat(hasReadMethodForProperty(bi, "foo")).isTrue(); + assertThat(hasWriteMethodForProperty(bi, "foo")).isFalse(); + + assertThat(hasReadMethodForProperty(bi, "bar")).isTrue(); + assertThat(hasWriteMethodForProperty(bi, "bar")).isFalse(); + + assertThat(hasReadMethodForProperty(ebi, "foo")).isTrue(); + assertThat(hasWriteMethodForProperty(ebi, "foo")).isTrue(); + + assertThat(hasReadMethodForProperty(ebi, "bar")).isTrue(); + assertThat(hasWriteMethodForProperty(ebi, "bar")).isTrue(); + } + + @Test + public void nonPublicStandardReadAndWriteMethods() throws Exception { + @SuppressWarnings("unused") class C { + String getFoo() { return null; } + C setFoo(String foo) { return this; } + } + + BeanInfo bi = Introspector.getBeanInfo(C.class); + BeanInfo ebi = new ExtendedBeanInfo(bi); + + assertThat(hasReadMethodForProperty(bi, "foo")).isFalse(); + assertThat(hasWriteMethodForProperty(bi, "foo")).isFalse(); + + assertThat(hasReadMethodForProperty(ebi, "foo")).isFalse(); + assertThat(hasWriteMethodForProperty(ebi, "foo")).isFalse(); + } + + /** + * {@link ExtendedBeanInfo} should behave exactly like {@link BeanInfo} + * in strange edge cases. + */ + @Test + public void readMethodReturnsSupertypeOfWriteMethodParameter() throws IntrospectionException { + @SuppressWarnings("unused") class C { + public Number getFoo() { return null; } + public void setFoo(Integer foo) { } + } + + BeanInfo bi = Introspector.getBeanInfo(C.class); + BeanInfo ebi = new ExtendedBeanInfo(bi); + + assertThat(hasReadMethodForProperty(bi, "foo")).isTrue(); + assertThat(hasReadMethodForProperty(ebi, "foo")).isTrue(); + assertThat(hasWriteMethodForProperty(ebi, "foo")).isEqualTo(hasWriteMethodForProperty(bi, "foo")); + } + + @Test + public void indexedReadMethodReturnsSupertypeOfIndexedWriteMethodParameter() throws IntrospectionException { + @SuppressWarnings("unused") class C { + public Number getFoos(int index) { return null; } + public void setFoos(int index, Integer foo) { } + } + + BeanInfo bi = Introspector.getBeanInfo(C.class); + BeanInfo ebi = new ExtendedBeanInfo(bi); + + assertThat(hasIndexedReadMethodForProperty(bi, "foos")).isTrue(); + assertThat(hasIndexedReadMethodForProperty(ebi, "foos")).isTrue(); + assertThat(hasIndexedWriteMethodForProperty(ebi, "foos")).isEqualTo(hasIndexedWriteMethodForProperty(bi, "foos")); + } + + /** + * {@link ExtendedBeanInfo} should behave exactly like {@link BeanInfo} + * in strange edge cases. + */ + @Test + public void readMethodReturnsSubtypeOfWriteMethodParameter() throws IntrospectionException { + @SuppressWarnings("unused") class C { + public Integer getFoo() { return null; } + public void setFoo(Number foo) { } + } + + BeanInfo bi = Introspector.getBeanInfo(C.class); + BeanInfo ebi = new ExtendedBeanInfo(bi); + + assertThat(hasReadMethodForProperty(bi, "foo")).isTrue(); + assertThat(hasWriteMethodForProperty(bi, "foo")).isFalse(); + + assertThat(hasReadMethodForProperty(ebi, "foo")).isTrue(); + assertThat(hasWriteMethodForProperty(ebi, "foo")).isFalse(); + } + + @Test + public void indexedReadMethodReturnsSubtypeOfIndexedWriteMethodParameter() throws IntrospectionException { + @SuppressWarnings("unused") class C { + public Integer getFoos(int index) { return null; } + public void setFoo(int index, Number foo) { } + } + + BeanInfo bi = Introspector.getBeanInfo(C.class); + BeanInfo ebi = new ExtendedBeanInfo(bi); + + assertThat(hasIndexedReadMethodForProperty(bi, "foos")).isTrue(); + assertThat(hasIndexedWriteMethodForProperty(bi, "foos")).isFalse(); + + assertThat(hasIndexedReadMethodForProperty(ebi, "foos")).isTrue(); + assertThat(hasIndexedWriteMethodForProperty(ebi, "foos")).isFalse(); + } + + @Test + public void indexedReadMethodOnly() throws IntrospectionException { + @SuppressWarnings("unused") + class C { + // indexed read method + public String getFoos(int i) { return null; } + } + + BeanInfo bi = Introspector.getBeanInfo(C.class); + BeanInfo ebi = new ExtendedBeanInfo(Introspector.getBeanInfo(C.class)); + + assertThat(hasReadMethodForProperty(bi, "foos")).isFalse(); + assertThat(hasIndexedReadMethodForProperty(bi, "foos")).isTrue(); + + assertThat(hasReadMethodForProperty(ebi, "foos")).isFalse(); + assertThat(hasIndexedReadMethodForProperty(ebi, "foos")).isTrue(); + } + + @Test + public void indexedWriteMethodOnly() throws IntrospectionException { + @SuppressWarnings("unused") + class C { + // indexed write method + public void setFoos(int i, String foo) { } + } + + BeanInfo bi = Introspector.getBeanInfo(C.class); + BeanInfo ebi = new ExtendedBeanInfo(Introspector.getBeanInfo(C.class)); + + assertThat(hasWriteMethodForProperty(bi, "foos")).isFalse(); + assertThat(hasIndexedWriteMethodForProperty(bi, "foos")).isTrue(); + + assertThat(hasWriteMethodForProperty(ebi, "foos")).isFalse(); + assertThat(hasIndexedWriteMethodForProperty(ebi, "foos")).isTrue(); + } + + @Test + public void indexedReadAndIndexedWriteMethods() throws IntrospectionException { + @SuppressWarnings("unused") + class C { + // indexed read method + public String getFoos(int i) { return null; } + // indexed write method + public void setFoos(int i, String foo) { } + } + + BeanInfo bi = Introspector.getBeanInfo(C.class); + BeanInfo ebi = new ExtendedBeanInfo(Introspector.getBeanInfo(C.class)); + + assertThat(hasReadMethodForProperty(bi, "foos")).isFalse(); + assertThat(hasIndexedReadMethodForProperty(bi, "foos")).isTrue(); + assertThat(hasWriteMethodForProperty(bi, "foos")).isFalse(); + assertThat(hasIndexedWriteMethodForProperty(bi, "foos")).isTrue(); + + assertThat(hasReadMethodForProperty(ebi, "foos")).isFalse(); + assertThat(hasIndexedReadMethodForProperty(ebi, "foos")).isTrue(); + assertThat(hasWriteMethodForProperty(ebi, "foos")).isFalse(); + assertThat(hasIndexedWriteMethodForProperty(ebi, "foos")).isTrue(); + } + + @Test + public void readAndWriteAndIndexedReadAndIndexedWriteMethods() throws IntrospectionException { + @SuppressWarnings("unused") + class C { + // read method + public String[] getFoos() { return null; } + // indexed read method + public String getFoos(int i) { return null; } + // write method + public void setFoos(String[] foos) { } + // indexed write method + public void setFoos(int i, String foo) { } + } + + BeanInfo bi = Introspector.getBeanInfo(C.class); + BeanInfo ebi = new ExtendedBeanInfo(Introspector.getBeanInfo(C.class)); + + assertThat(hasReadMethodForProperty(bi, "foos")).isTrue(); + assertThat(hasWriteMethodForProperty(bi, "foos")).isTrue(); + assertThat(hasIndexedReadMethodForProperty(bi, "foos")).isTrue(); + assertThat(hasIndexedWriteMethodForProperty(bi, "foos")).isTrue(); + + assertThat(hasReadMethodForProperty(ebi, "foos")).isTrue(); + assertThat(hasWriteMethodForProperty(ebi, "foos")).isTrue(); + assertThat(hasIndexedReadMethodForProperty(ebi, "foos")).isTrue(); + assertThat(hasIndexedWriteMethodForProperty(ebi, "foos")).isTrue(); + } + + @Test + public void indexedReadAndNonStandardIndexedWrite() throws IntrospectionException { + @SuppressWarnings("unused") + class C { + // indexed read method + public String getFoos(int i) { return null; } + // non-standard indexed write method + public C setFoos(int i, String foo) { return this; } + } + + BeanInfo bi = Introspector.getBeanInfo(C.class); + + assertThat(hasIndexedReadMethodForProperty(bi, "foos")).isTrue(); + // interesting! standard Inspector picks up non-void return types on indexed write methods by default + assertThat(hasIndexedWriteMethodForProperty(bi, "foos")).isFalse(); + + BeanInfo ebi = new ExtendedBeanInfo(Introspector.getBeanInfo(C.class)); + + assertThat(hasIndexedReadMethodForProperty(ebi, "foos")).isTrue(); + assertThat(hasIndexedWriteMethodForProperty(ebi, "foos")).isTrue(); + } + + @Test + public void indexedReadAndNonStandardWriteAndNonStandardIndexedWrite() throws IntrospectionException { + @SuppressWarnings("unused") + class C { + // non-standard write method + public C setFoos(String[] foos) { return this; } + // indexed read method + public String getFoos(int i) { return null; } + // non-standard indexed write method + public C setFoos(int i, String foo) { return this; } + } + + BeanInfo bi = Introspector.getBeanInfo(C.class); + + assertThat(hasIndexedReadMethodForProperty(bi, "foos")).isTrue(); + assertThat(hasWriteMethodForProperty(bi, "foos")).isFalse(); + // again as above, standard Inspector picks up non-void return types on indexed write methods by default + assertThat(hasIndexedWriteMethodForProperty(bi, "foos")).isFalse(); + + BeanInfo ebi = new ExtendedBeanInfo(Introspector.getBeanInfo(C.class)); + + assertThat(hasIndexedReadMethodForProperty(bi, "foos")).isTrue(); + assertThat(hasWriteMethodForProperty(bi, "foos")).isFalse(); + assertThat(hasIndexedWriteMethodForProperty(bi, "foos")).isFalse(); + + assertThat(hasIndexedReadMethodForProperty(ebi, "foos")).isTrue(); + assertThat(hasWriteMethodForProperty(ebi, "foos")).isTrue(); + assertThat(hasIndexedWriteMethodForProperty(ebi, "foos")).isTrue(); + } + + @Test + public void cornerSpr9702() throws IntrospectionException { + { // baseline with standard write method + @SuppressWarnings("unused") + class C { + // VOID-RETURNING, NON-INDEXED write method + public void setFoos(String[] foos) { } + // indexed read method + public String getFoos(int i) { return null; } + } + + BeanInfo bi = Introspector.getBeanInfo(C.class); + assertThat(hasReadMethodForProperty(bi, "foos")).isFalse(); + assertThat(hasIndexedReadMethodForProperty(bi, "foos")).isTrue(); + assertThat(hasWriteMethodForProperty(bi, "foos")).isTrue(); + assertThat(hasIndexedWriteMethodForProperty(bi, "foos")).isFalse(); + + BeanInfo ebi = Introspector.getBeanInfo(C.class); + assertThat(hasReadMethodForProperty(ebi, "foos")).isFalse(); + assertThat(hasIndexedReadMethodForProperty(ebi, "foos")).isTrue(); + assertThat(hasWriteMethodForProperty(ebi, "foos")).isTrue(); + assertThat(hasIndexedWriteMethodForProperty(ebi, "foos")).isFalse(); + } + { // variant with non-standard write method + @SuppressWarnings("unused") + class C { + // NON-VOID-RETURNING, NON-INDEXED write method + public C setFoos(String[] foos) { return this; } + // indexed read method + public String getFoos(int i) { return null; } + } + + BeanInfo bi = Introspector.getBeanInfo(C.class); + assertThat(hasReadMethodForProperty(bi, "foos")).isFalse(); + assertThat(hasIndexedReadMethodForProperty(bi, "foos")).isTrue(); + assertThat(hasWriteMethodForProperty(bi, "foos")).isFalse(); + assertThat(hasIndexedWriteMethodForProperty(bi, "foos")).isFalse(); + + BeanInfo ebi = new ExtendedBeanInfo(Introspector.getBeanInfo(C.class)); + assertThat(hasReadMethodForProperty(ebi, "foos")).isFalse(); + assertThat(hasIndexedReadMethodForProperty(ebi, "foos")).isTrue(); + assertThat(hasWriteMethodForProperty(ebi, "foos")).isTrue(); + assertThat(hasIndexedWriteMethodForProperty(ebi, "foos")).isFalse(); + } + } + + /** + * Prior to SPR-10111 (a follow-up fix for SPR-9702), this method would throw an + * IntrospectionException regarding a "type mismatch between indexed and non-indexed + * methods" intermittently (approximately one out of every four times) under JDK 7 + * due to non-deterministic results from {@link Class#getDeclaredMethods()}. + * See https://bugs.java.com/view_bug.do?bug_id=7023180 + * @see #cornerSpr9702() + */ + @Test + public void cornerSpr10111() throws Exception { + new ExtendedBeanInfo(Introspector.getBeanInfo(BigDecimal.class)); + } + + @Test + public void subclassWriteMethodWithCovariantReturnType() throws IntrospectionException { + @SuppressWarnings("unused") class B { + public String getFoo() { return null; } + public Number setFoo(String foo) { return null; } + } + class C extends B { + @Override + public String getFoo() { return null; } + @Override + public Integer setFoo(String foo) { return null; } + } + + BeanInfo bi = Introspector.getBeanInfo(C.class); + + assertThat(hasReadMethodForProperty(bi, "foo")).isTrue(); + assertThat(hasWriteMethodForProperty(bi, "foo")).isFalse(); + + BeanInfo ebi = new ExtendedBeanInfo(bi); + + assertThat(hasReadMethodForProperty(bi, "foo")).isTrue(); + assertThat(hasWriteMethodForProperty(bi, "foo")).isFalse(); + + assertThat(hasReadMethodForProperty(ebi, "foo")).isTrue(); + assertThat(hasWriteMethodForProperty(ebi, "foo")).isTrue(); + + assertThat(ebi.getPropertyDescriptors().length).isEqualTo(bi.getPropertyDescriptors().length); + } + + @Test + public void nonStandardReadMethodAndStandardWriteMethod() throws IntrospectionException { + @SuppressWarnings("unused") class C { + public void getFoo() { } + public void setFoo(String foo) { } + } + + BeanInfo bi = Introspector.getBeanInfo(C.class); + BeanInfo ebi = new ExtendedBeanInfo(bi); + + assertThat(hasReadMethodForProperty(bi, "foo")).isFalse(); + assertThat(hasWriteMethodForProperty(bi, "foo")).isTrue(); + + assertThat(hasReadMethodForProperty(ebi, "foo")).isFalse(); + assertThat(hasWriteMethodForProperty(ebi, "foo")).isTrue(); + } + + /** + * Ensures that an empty string is not passed into a PropertyDescriptor constructor. This + * could occur when handling ArrayList.set(int,Object) + */ + @Test + public void emptyPropertiesIgnored() throws IntrospectionException { + @SuppressWarnings("unused") class C { + public Object set(Object o) { return null; } + public Object set(int i, Object o) { return null; } + } + + BeanInfo bi = Introspector.getBeanInfo(C.class); + BeanInfo ebi = new ExtendedBeanInfo(bi); + + assertThat(ebi.getPropertyDescriptors()).isEqualTo(bi.getPropertyDescriptors()); + } + + @Test + public void overloadedNonStandardWriteMethodsOnly_orderA() throws IntrospectionException, SecurityException, NoSuchMethodException { + @SuppressWarnings("unused") class C { + public Object setFoo(String p) { return new Object(); } + public Object setFoo(int p) { return new Object(); } + } + BeanInfo bi = Introspector.getBeanInfo(C.class); + + assertThat(hasReadMethodForProperty(bi, "foo")).isFalse(); + assertThat(hasWriteMethodForProperty(bi, "foo")).isFalse(); + + BeanInfo ebi = new ExtendedBeanInfo(bi); + + assertThat(hasReadMethodForProperty(bi, "foo")).isFalse(); + assertThat(hasWriteMethodForProperty(bi, "foo")).isFalse(); + + assertThat(hasReadMethodForProperty(ebi, "foo")).isFalse(); + assertThat(hasWriteMethodForProperty(ebi, "foo")).isTrue(); + + for (PropertyDescriptor pd : ebi.getPropertyDescriptors()) { + if (pd.getName().equals("foo")) { + assertThat(pd.getWriteMethod()).isEqualTo(C.class.getMethod("setFoo", String.class)); + return; + } + } + throw new AssertionError("never matched write method"); + } + + @Test + public void overloadedNonStandardWriteMethodsOnly_orderB() throws IntrospectionException, SecurityException, NoSuchMethodException { + @SuppressWarnings("unused") class C { + public Object setFoo(int p) { return new Object(); } + public Object setFoo(String p) { return new Object(); } + } + BeanInfo bi = Introspector.getBeanInfo(C.class); + + assertThat(hasReadMethodForProperty(bi, "foo")).isFalse(); + assertThat(hasWriteMethodForProperty(bi, "foo")).isFalse(); + + BeanInfo ebi = new ExtendedBeanInfo(bi); + + assertThat(hasReadMethodForProperty(bi, "foo")).isFalse(); + assertThat(hasWriteMethodForProperty(bi, "foo")).isFalse(); + + assertThat(hasReadMethodForProperty(ebi, "foo")).isFalse(); + assertThat(hasWriteMethodForProperty(ebi, "foo")).isTrue(); + + for (PropertyDescriptor pd : ebi.getPropertyDescriptors()) { + if (pd.getName().equals("foo")) { + assertThat(pd.getWriteMethod()).isEqualTo(C.class.getMethod("setFoo", String.class)); + return; + } + } + throw new AssertionError("never matched write method"); + } + + /** + * Corners the bug revealed by SPR-8522, in which an (apparently) indexed write method + * without a corresponding indexed read method would fail to be processed correctly by + * ExtendedBeanInfo. The local class C below represents the relevant methods from + * Google's GsonBuilder class. Interestingly, the setDateFormat(int, int) method was + * not actually intended to serve as an indexed write method; it just appears that way. + */ + @Test + public void reproSpr8522() throws IntrospectionException { + @SuppressWarnings("unused") class C { + public Object setDateFormat(String pattern) { return new Object(); } + public Object setDateFormat(int style) { return new Object(); } + public Object setDateFormat(int dateStyle, int timeStyle) { return new Object(); } + } + BeanInfo bi = Introspector.getBeanInfo(C.class); + + assertThat(hasReadMethodForProperty(bi, "dateFormat")).isFalse(); + assertThat(hasWriteMethodForProperty(bi, "dateFormat")).isFalse(); + assertThat(hasIndexedReadMethodForProperty(bi, "dateFormat")).isFalse(); + assertThat(hasIndexedWriteMethodForProperty(bi, "dateFormat")).isFalse(); + + BeanInfo ebi = new ExtendedBeanInfo(bi); + + assertThat(hasReadMethodForProperty(bi, "dateFormat")).isFalse(); + assertThat(hasWriteMethodForProperty(bi, "dateFormat")).isFalse(); + assertThat(hasIndexedReadMethodForProperty(bi, "dateFormat")).isFalse(); + assertThat(hasIndexedWriteMethodForProperty(bi, "dateFormat")).isFalse(); + + assertThat(hasReadMethodForProperty(ebi, "dateFormat")).isFalse(); + assertThat(hasWriteMethodForProperty(ebi, "dateFormat")).isTrue(); + assertThat(hasIndexedReadMethodForProperty(ebi, "dateFormat")).isFalse(); + assertThat(hasIndexedWriteMethodForProperty(ebi, "dateFormat")).isFalse(); + } + + @Test + public void propertyCountsMatch() throws IntrospectionException { + BeanInfo bi = Introspector.getBeanInfo(TestBean.class); + BeanInfo ebi = new ExtendedBeanInfo(bi); + + assertThat(ebi.getPropertyDescriptors().length).isEqualTo(bi.getPropertyDescriptors().length); + } + + @Test + public void propertyCountsWithNonStandardWriteMethod() throws IntrospectionException { + class ExtendedTestBean extends TestBean { + @SuppressWarnings("unused") + public ExtendedTestBean setFoo(String s) { return this; } + } + BeanInfo bi = Introspector.getBeanInfo(ExtendedTestBean.class); + BeanInfo ebi = new ExtendedBeanInfo(bi); + + boolean found = false; + for (PropertyDescriptor pd : ebi.getPropertyDescriptors()) { + if (pd.getName().equals("foo")) { + found = true; + break; + } + } + assertThat(found).isTrue(); + assertThat(ebi.getPropertyDescriptors().length).isEqualTo(bi.getPropertyDescriptors().length+1); + } + + /** + * {@link BeanInfo#getPropertyDescriptors()} returns alphanumerically sorted. + * Test that {@link ExtendedBeanInfo#getPropertyDescriptors()} does the same. + */ + @Test + public void propertyDescriptorOrderIsEqual() throws IntrospectionException { + BeanInfo bi = Introspector.getBeanInfo(TestBean.class); + BeanInfo ebi = new ExtendedBeanInfo(bi); + + for (int i = 0; i < bi.getPropertyDescriptors().length; i++) { + assertThat(ebi.getPropertyDescriptors()[i].getName()).isEqualTo(bi.getPropertyDescriptors()[i].getName()); + } + } + + @Test + public void propertyDescriptorComparator() throws IntrospectionException { + ExtendedBeanInfo.PropertyDescriptorComparator c = new ExtendedBeanInfo.PropertyDescriptorComparator(); + + assertThat(c.compare(new PropertyDescriptor("a", null, null), new PropertyDescriptor("a", null, null))).isEqualTo(0); + assertThat(c.compare(new PropertyDescriptor("abc", null, null), new PropertyDescriptor("abc", null, null))).isEqualTo(0); + assertThat(c.compare(new PropertyDescriptor("a", null, null), new PropertyDescriptor("b", null, null))).isLessThan(0); + assertThat(c.compare(new PropertyDescriptor("b", null, null), new PropertyDescriptor("a", null, null))).isGreaterThan(0); + assertThat(c.compare(new PropertyDescriptor("abc", null, null), new PropertyDescriptor("abd", null, null))).isLessThan(0); + assertThat(c.compare(new PropertyDescriptor("xyz", null, null), new PropertyDescriptor("123", null, null))).isGreaterThan(0); + assertThat(c.compare(new PropertyDescriptor("a", null, null), new PropertyDescriptor("abc", null, null))).isLessThan(0); + assertThat(c.compare(new PropertyDescriptor("abc", null, null), new PropertyDescriptor("a", null, null))).isGreaterThan(0); + assertThat(c.compare(new PropertyDescriptor("abc", null, null), new PropertyDescriptor("b", null, null))).isLessThan(0); + + assertThat(c.compare(new PropertyDescriptor(" ", null, null), new PropertyDescriptor("a", null, null))).isLessThan(0); + assertThat(c.compare(new PropertyDescriptor("1", null, null), new PropertyDescriptor("a", null, null))).isLessThan(0); + assertThat(c.compare(new PropertyDescriptor("a", null, null), new PropertyDescriptor("A", null, null))).isGreaterThan(0); + } + + @Test + public void reproSpr8806() throws IntrospectionException { + // does not throw + Introspector.getBeanInfo(LawLibrary.class); + + // does not throw after the changes introduced in SPR-8806 + new ExtendedBeanInfo(Introspector.getBeanInfo(LawLibrary.class)); + } + + @Test + public void cornerSpr8949() throws IntrospectionException { + class A { + @SuppressWarnings("unused") + public boolean isTargetMethod() { + return false; + } + } + + class B extends A { + @Override + public boolean isTargetMethod() { + return false; + } + } + + BeanInfo bi = Introspector.getBeanInfo(B.class); + + // java.beans.Introspector returns the "wrong" declaring class for overridden read + // methods, which in turn violates expectations in {@link ExtendedBeanInfo} regarding + // method equality. Spring's {@link ClassUtils#getMostSpecificMethod(Method, Class)} + // helps out here, and is now put into use in ExtendedBeanInfo as well. + BeanInfo ebi = new ExtendedBeanInfo(bi); + + assertThat(hasReadMethodForProperty(bi, "targetMethod")).isTrue(); + assertThat(hasWriteMethodForProperty(bi, "targetMethod")).isFalse(); + + assertThat(hasReadMethodForProperty(ebi, "targetMethod")).isTrue(); + assertThat(hasWriteMethodForProperty(ebi, "targetMethod")).isFalse(); + } + + @Test + public void cornerSpr8937AndSpr12582() throws IntrospectionException { + @SuppressWarnings("unused") class A { + public void setAddress(String addr){ } + public void setAddress(int index, String addr) { } + public String getAddress(int index){ return null; } + } + + // Baseline: + BeanInfo bi = Introspector.getBeanInfo(A.class); + boolean hasReadMethod = hasReadMethodForProperty(bi, "address"); + boolean hasWriteMethod = hasWriteMethodForProperty(bi, "address"); + boolean hasIndexedReadMethod = hasIndexedReadMethodForProperty(bi, "address"); + boolean hasIndexedWriteMethod = hasIndexedWriteMethodForProperty(bi, "address"); + + // ExtendedBeanInfo needs to behave exactly like BeanInfo... + BeanInfo ebi = new ExtendedBeanInfo(bi); + assertThat(hasReadMethodForProperty(ebi, "address")).isEqualTo(hasReadMethod); + assertThat(hasWriteMethodForProperty(ebi, "address")).isEqualTo(hasWriteMethod); + assertThat(hasIndexedReadMethodForProperty(ebi, "address")).isEqualTo(hasIndexedReadMethod); + assertThat(hasIndexedWriteMethodForProperty(ebi, "address")).isEqualTo(hasIndexedWriteMethod); + } + + @Test + public void shouldSupportStaticWriteMethod() throws IntrospectionException { + { + BeanInfo bi = Introspector.getBeanInfo(WithStaticWriteMethod.class); + assertThat(hasReadMethodForProperty(bi, "prop1")).isFalse(); + assertThat(hasWriteMethodForProperty(bi, "prop1")).isFalse(); + assertThat(hasIndexedReadMethodForProperty(bi, "prop1")).isFalse(); + assertThat(hasIndexedWriteMethodForProperty(bi, "prop1")).isFalse(); + } + { + BeanInfo bi = new ExtendedBeanInfo(Introspector.getBeanInfo(WithStaticWriteMethod.class)); + assertThat(hasReadMethodForProperty(bi, "prop1")).isFalse(); + assertThat(hasWriteMethodForProperty(bi, "prop1")).isTrue(); + assertThat(hasIndexedReadMethodForProperty(bi, "prop1")).isFalse(); + assertThat(hasIndexedWriteMethodForProperty(bi, "prop1")).isFalse(); + } + } + + @Test // SPR-12434 + public void shouldDetectValidPropertiesAndIgnoreInvalidProperties() throws IntrospectionException { + BeanInfo bi = new ExtendedBeanInfo(Introspector.getBeanInfo(java.awt.Window.class)); + assertThat(hasReadMethodForProperty(bi, "locationByPlatform")).isTrue(); + assertThat(hasWriteMethodForProperty(bi, "locationByPlatform")).isTrue(); + assertThat(hasIndexedReadMethodForProperty(bi, "locationByPlatform")).isFalse(); + assertThat(hasIndexedWriteMethodForProperty(bi, "locationByPlatform")).isFalse(); + } + + + private boolean hasWriteMethodForProperty(BeanInfo beanInfo, String propertyName) { + for (PropertyDescriptor pd : beanInfo.getPropertyDescriptors()) { + if (pd.getName().equals(propertyName)) { + return pd.getWriteMethod() != null; + } + } + return false; + } + + private boolean hasReadMethodForProperty(BeanInfo beanInfo, String propertyName) { + for (PropertyDescriptor pd : beanInfo.getPropertyDescriptors()) { + if (pd.getName().equals(propertyName)) { + return pd.getReadMethod() != null; + } + } + return false; + } + + private boolean hasIndexedWriteMethodForProperty(BeanInfo beanInfo, String propertyName) { + for (PropertyDescriptor pd : beanInfo.getPropertyDescriptors()) { + if (pd.getName().equals(propertyName)) { + if (!(pd instanceof IndexedPropertyDescriptor)) { + return false; + } + return ((IndexedPropertyDescriptor)pd).getIndexedWriteMethod() != null; + } + } + return false; + } + + private boolean hasIndexedReadMethodForProperty(BeanInfo beanInfo, String propertyName) { + for (PropertyDescriptor pd : beanInfo.getPropertyDescriptors()) { + if (pd.getName().equals(propertyName)) { + if (!(pd instanceof IndexedPropertyDescriptor)) { + return false; + } + return ((IndexedPropertyDescriptor)pd).getIndexedReadMethod() != null; + } + } + return false; + } + + + interface Spr9453 { + + T getProp(); + } + + + interface Book { + } + + + interface TextBook extends Book { + } + + + interface LawBook extends TextBook { + } + + + interface BookOperations { + + Book getBook(); + + void setBook(Book book); + } + + + interface TextBookOperations extends BookOperations { + + @Override + TextBook getBook(); + } + + + abstract class Library { + + public Book getBook() { + return null; + } + + public void setBook(Book book) { + } + } + + + class LawLibrary extends Library implements TextBookOperations { + + @Override + public LawBook getBook() { + return null; + } + } + + + static class WithStaticWriteMethod { + + public static void setProp1(String prop1) { + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/MutablePropertyValuesTests.java b/spring-beans/src/test/java/org/springframework/beans/MutablePropertyValuesTests.java new file mode 100644 index 0000000..027811e --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/MutablePropertyValuesTests.java @@ -0,0 +1,147 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.util.Iterator; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link MutablePropertyValues}. + * + * @author Rod Johnson + * @author Chris Beams + * @author Juergen Hoeller + */ +public class MutablePropertyValuesTests extends AbstractPropertyValuesTests { + + @Test + public void testValid() { + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.addPropertyValue(new PropertyValue("forname", "Tony")); + pvs.addPropertyValue(new PropertyValue("surname", "Blair")); + pvs.addPropertyValue(new PropertyValue("age", "50")); + doTestTony(pvs); + + MutablePropertyValues deepCopy = new MutablePropertyValues(pvs); + doTestTony(deepCopy); + deepCopy.setPropertyValueAt(new PropertyValue("name", "Gordon"), 0); + doTestTony(pvs); + assertThat(deepCopy.getPropertyValue("name").getValue()).isEqualTo("Gordon"); + } + + @Test + public void testAddOrOverride() { + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.addPropertyValue(new PropertyValue("forname", "Tony")); + pvs.addPropertyValue(new PropertyValue("surname", "Blair")); + pvs.addPropertyValue(new PropertyValue("age", "50")); + doTestTony(pvs); + PropertyValue addedPv = new PropertyValue("rod", "Rod"); + pvs.addPropertyValue(addedPv); + assertThat(pvs.getPropertyValue("rod").equals(addedPv)).isTrue(); + PropertyValue changedPv = new PropertyValue("forname", "Greg"); + pvs.addPropertyValue(changedPv); + assertThat(pvs.getPropertyValue("forname").equals(changedPv)).isTrue(); + } + + @Test + public void testChangesOnEquals() { + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.addPropertyValue(new PropertyValue("forname", "Tony")); + pvs.addPropertyValue(new PropertyValue("surname", "Blair")); + pvs.addPropertyValue(new PropertyValue("age", "50")); + MutablePropertyValues pvs2 = pvs; + PropertyValues changes = pvs2.changesSince(pvs); + assertThat(changes.getPropertyValues().length == 0).as("changes are empty").isTrue(); + } + + @Test + public void testChangeOfOneField() { + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.addPropertyValue(new PropertyValue("forname", "Tony")); + pvs.addPropertyValue(new PropertyValue("surname", "Blair")); + pvs.addPropertyValue(new PropertyValue("age", "50")); + + MutablePropertyValues pvs2 = new MutablePropertyValues(pvs); + PropertyValues changes = pvs2.changesSince(pvs); + assertThat(changes.getPropertyValues().length == 0).as("changes are empty, not of length " + changes.getPropertyValues().length).isTrue(); + + pvs2.addPropertyValue(new PropertyValue("forname", "Gordon")); + changes = pvs2.changesSince(pvs); + assertThat(changes.getPropertyValues().length).as("1 change").isEqualTo(1); + PropertyValue fn = changes.getPropertyValue("forname"); + assertThat(fn != null).as("change is forname").isTrue(); + assertThat(fn.getValue().equals("Gordon")).as("new value is gordon").isTrue(); + + MutablePropertyValues pvs3 = new MutablePropertyValues(pvs); + changes = pvs3.changesSince(pvs); + assertThat(changes.getPropertyValues().length == 0).as("changes are empty, not of length " + changes.getPropertyValues().length).isTrue(); + + // add new + pvs3.addPropertyValue(new PropertyValue("foo", "bar")); + pvs3.addPropertyValue(new PropertyValue("fi", "fum")); + changes = pvs3.changesSince(pvs); + assertThat(changes.getPropertyValues().length == 2).as("2 change").isTrue(); + fn = changes.getPropertyValue("foo"); + assertThat(fn != null).as("change in foo").isTrue(); + assertThat(fn.getValue().equals("bar")).as("new value is bar").isTrue(); + } + + @Test + public void iteratorContainsPropertyValue() { + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("foo", "bar"); + + Iterator it = pvs.iterator(); + assertThat(it.hasNext()).isTrue(); + PropertyValue pv = it.next(); + assertThat(pv.getName()).isEqualTo("foo"); + assertThat(pv.getValue()).isEqualTo("bar"); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(it::remove); + assertThat(it.hasNext()).isFalse(); + } + + @Test + public void iteratorIsEmptyForEmptyValues() { + MutablePropertyValues pvs = new MutablePropertyValues(); + Iterator it = pvs.iterator(); + assertThat(it.hasNext()).isFalse(); + } + + @Test + public void streamContainsPropertyValue() { + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("foo", "bar"); + + assertThat(pvs.stream()).isNotNull(); + assertThat(pvs.stream().count()).isEqualTo(1L); + assertThat(pvs.stream().anyMatch(pv -> "foo".equals(pv.getName()) && "bar".equals(pv.getValue()))).isTrue(); + assertThat(pvs.stream().anyMatch(pv -> "bar".equals(pv.getName()) && "foo".equals(pv.getValue()))).isFalse(); + } + + @Test + public void streamIsEmptyForEmptyValues() { + MutablePropertyValues pvs = new MutablePropertyValues(); + assertThat(pvs.stream()).isNotNull(); + assertThat(pvs.stream().count()).isEqualTo(0L); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/PropertyAccessorUtilsTests.java b/spring-beans/src/test/java/org/springframework/beans/PropertyAccessorUtilsTests.java new file mode 100644 index 0000000..4613f47 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/PropertyAccessorUtilsTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link PropertyAccessorUtils}. + * + * @author Juergen Hoeller + * @author Chris Beams + */ +public class PropertyAccessorUtilsTests { + + @Test + public void getPropertyName() { + assertThat(PropertyAccessorUtils.getPropertyName("")).isEqualTo(""); + assertThat(PropertyAccessorUtils.getPropertyName("[user]")).isEqualTo(""); + assertThat(PropertyAccessorUtils.getPropertyName("user")).isEqualTo("user"); + } + + @Test + public void isNestedOrIndexedProperty() { + assertThat(PropertyAccessorUtils.isNestedOrIndexedProperty(null)).isFalse(); + assertThat(PropertyAccessorUtils.isNestedOrIndexedProperty("")).isFalse(); + assertThat(PropertyAccessorUtils.isNestedOrIndexedProperty("user")).isFalse(); + + assertThat(PropertyAccessorUtils.isNestedOrIndexedProperty("[user]")).isTrue(); + assertThat(PropertyAccessorUtils.isNestedOrIndexedProperty("user.name")).isTrue(); + } + + @Test + public void getFirstNestedPropertySeparatorIndex() { + assertThat(PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex("[user]")).isEqualTo(-1); + assertThat(PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex("user.name")).isEqualTo(4); + } + + @Test + public void getLastNestedPropertySeparatorIndex() { + assertThat(PropertyAccessorUtils.getLastNestedPropertySeparatorIndex("[user]")).isEqualTo(-1); + assertThat(PropertyAccessorUtils.getLastNestedPropertySeparatorIndex("user.address.street")).isEqualTo(12); + } + + @Test + public void matchesProperty() { + assertThat(PropertyAccessorUtils.matchesProperty("user", "email")).isFalse(); + assertThat(PropertyAccessorUtils.matchesProperty("username", "user")).isFalse(); + assertThat(PropertyAccessorUtils.matchesProperty("admin[user]", "user")).isFalse(); + + assertThat(PropertyAccessorUtils.matchesProperty("user", "user")).isTrue(); + assertThat(PropertyAccessorUtils.matchesProperty("user[name]", "user")).isTrue(); + } + + @Test + public void canonicalPropertyName() { + assertThat(PropertyAccessorUtils.canonicalPropertyName(null)).isEqualTo(""); + assertThat(PropertyAccessorUtils.canonicalPropertyName("map")).isEqualTo("map"); + assertThat(PropertyAccessorUtils.canonicalPropertyName("map[key1]")).isEqualTo("map[key1]"); + assertThat(PropertyAccessorUtils.canonicalPropertyName("map['key1']")).isEqualTo("map[key1]"); + assertThat(PropertyAccessorUtils.canonicalPropertyName("map[\"key1\"]")).isEqualTo("map[key1]"); + assertThat(PropertyAccessorUtils.canonicalPropertyName("map[key1][key2]")).isEqualTo("map[key1][key2]"); + assertThat(PropertyAccessorUtils.canonicalPropertyName("map['key1'][\"key2\"]")).isEqualTo("map[key1][key2]"); + assertThat(PropertyAccessorUtils.canonicalPropertyName("map[key1].name")).isEqualTo("map[key1].name"); + assertThat(PropertyAccessorUtils.canonicalPropertyName("map['key1'].name")).isEqualTo("map[key1].name"); + assertThat(PropertyAccessorUtils.canonicalPropertyName("map[\"key1\"].name")).isEqualTo("map[key1].name"); + } + + @Test + public void canonicalPropertyNames() { + assertThat(PropertyAccessorUtils.canonicalPropertyNames(null)).isNull(); + + String[] original = + new String[] {"map", "map[key1]", "map['key1']", "map[\"key1\"]", "map[key1][key2]", + "map['key1'][\"key2\"]", "map[key1].name", "map['key1'].name", "map[\"key1\"].name"}; + String[] canonical = + new String[] {"map", "map[key1]", "map[key1]", "map[key1]", "map[key1][key2]", + "map[key1][key2]", "map[key1].name", "map[key1].name", "map[key1].name"}; + + assertThat(PropertyAccessorUtils.canonicalPropertyNames(original)).isEqualTo(canonical); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/PropertyMatchesTests.java b/spring-beans/src/test/java/org/springframework/beans/PropertyMatchesTests.java new file mode 100644 index 0000000..09e7257 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/PropertyMatchesTests.java @@ -0,0 +1,198 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + + + + + + +/** + * Tests for {@link PropertyMatches}. + * + * @author Stephane Nicoll + */ +public class PropertyMatchesTests { + + @Test + public void simpleBeanPropertyTypo() { + PropertyMatches matches = PropertyMatches.forProperty("naem", SampleBeanProperties.class); + assertThat(matches.getPossibleMatches()).contains("name"); + } + + @Test + public void complexBeanPropertyTypo() { + PropertyMatches matches = PropertyMatches.forProperty("desriptn", SampleBeanProperties.class); + assertThat(matches.getPossibleMatches()).isEmpty(); + } + + @Test + public void unknownBeanProperty() { + PropertyMatches matches = PropertyMatches.forProperty("unknown", SampleBeanProperties.class); + assertThat(matches.getPossibleMatches()).isEmpty(); + } + + @Test + public void severalMatchesBeanProperty() { + PropertyMatches matches = PropertyMatches.forProperty("counter", SampleBeanProperties.class); + assertThat(matches.getPossibleMatches()).contains("counter1"); + assertThat(matches.getPossibleMatches()).contains("counter2"); + assertThat(matches.getPossibleMatches()).contains("counter3"); + } + + @Test + public void simpleBeanPropertyErrorMessage() { + PropertyMatches matches = PropertyMatches.forProperty("naem", SampleBeanProperties.class); + String msg = matches.buildErrorMessage(); + assertThat(msg).contains("naem"); + assertThat(msg).contains("name"); + assertThat(msg).contains("setter"); + assertThat(msg).doesNotContain("field"); + } + + @Test + public void complexBeanPropertyErrorMessage() { + PropertyMatches matches = PropertyMatches.forProperty("counter", SampleBeanProperties.class); + String msg = matches.buildErrorMessage(); + assertThat(msg).contains("counter"); + assertThat(msg).contains("counter1"); + assertThat(msg).contains("counter2"); + assertThat(msg).contains("counter3"); + } + + @Test + public void simpleFieldPropertyTypo() { + PropertyMatches matches = PropertyMatches.forField("naem", SampleFieldProperties.class); + assertThat(matches.getPossibleMatches()).contains("name"); + } + + @Test + public void complexFieldPropertyTypo() { + PropertyMatches matches = PropertyMatches.forField("desriptn", SampleFieldProperties.class); + assertThat(matches.getPossibleMatches()).isEmpty(); + } + + @Test + public void unknownFieldProperty() { + PropertyMatches matches = PropertyMatches.forField("unknown", SampleFieldProperties.class); + assertThat(matches.getPossibleMatches()).isEmpty(); + } + + @Test + public void severalMatchesFieldProperty() { + PropertyMatches matches = PropertyMatches.forField("counter", SampleFieldProperties.class); + assertThat(matches.getPossibleMatches()).contains("counter1"); + assertThat(matches.getPossibleMatches()).contains("counter2"); + assertThat(matches.getPossibleMatches()).contains("counter3"); + } + + @Test + public void simpleFieldPropertyErrorMessage() { + PropertyMatches matches = PropertyMatches.forField("naem", SampleFieldProperties.class); + String msg = matches.buildErrorMessage(); + assertThat(msg).contains("naem"); + assertThat(msg).contains("name"); + assertThat(msg).contains("field"); + assertThat(msg).doesNotContain("setter"); + } + + @Test + public void complexFieldPropertyErrorMessage() { + PropertyMatches matches = PropertyMatches.forField("counter", SampleFieldProperties.class); + String msg = matches.buildErrorMessage(); + assertThat(msg).contains("counter"); + assertThat(msg).contains("counter1"); + assertThat(msg).contains("counter2"); + assertThat(msg).contains("counter3"); + } + + + @SuppressWarnings("unused") + private static class SampleBeanProperties { + + private String name; + + private String description; + + private int counter1; + + private int counter2; + + private int counter3; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public int getCounter1() { + return counter1; + } + + public void setCounter1(int counter1) { + this.counter1 = counter1; + } + + public int getCounter2() { + return counter2; + } + + public void setCounter2(int counter2) { + this.counter2 = counter2; + } + + public int getCounter3() { + return counter3; + } + + public void setCounter3(int counter3) { + this.counter3 = counter3; + } + } + + + @SuppressWarnings("unused") + private static class SampleFieldProperties { + + private String name; + + private String description; + + private int counter1; + + private int counter2; + + private int counter3; + + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/SimplePropertyDescriptorTests.java b/spring-beans/src/test/java/org/springframework/beans/SimplePropertyDescriptorTests.java new file mode 100644 index 0000000..4a154ef --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/SimplePropertyDescriptorTests.java @@ -0,0 +1,147 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans; + +import java.beans.IndexedPropertyDescriptor; +import java.beans.IntrospectionException; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * @author Chris Beams + * @see ExtendedBeanInfoTests + */ +public class SimplePropertyDescriptorTests { + + @Test + public void toStringOutput() throws IntrospectionException, SecurityException, NoSuchMethodException { + { + Object pd = new ExtendedBeanInfo.SimplePropertyDescriptor("foo", null, null); + assertThat(pd.toString()).contains( + "PropertyDescriptor[name=foo, propertyType=null, readMethod=null"); + } + { + class C { + @SuppressWarnings("unused") + public Object setFoo(String foo) { return null; } + } + Method m = C.class.getMethod("setFoo", String.class); + Object pd = new ExtendedBeanInfo.SimplePropertyDescriptor("foo", null, m); + assertThat(pd.toString()).contains( + "PropertyDescriptor[name=foo", + "propertyType=class java.lang.String", + "readMethod=null, writeMethod=public java.lang.Object"); + } + { + Object pd = new ExtendedBeanInfo.SimpleIndexedPropertyDescriptor("foo", null, null, null, null); + assertThat(pd.toString()).contains( + "PropertyDescriptor[name=foo, propertyType=null, indexedPropertyType=null"); + } + { + class C { + @SuppressWarnings("unused") + public Object setFoo(int i, String foo) { return null; } + } + Method m = C.class.getMethod("setFoo", int.class, String.class); + Object pd = new ExtendedBeanInfo.SimpleIndexedPropertyDescriptor("foo", null, null, null, m); + assertThat(pd.toString()).contains( + "PropertyDescriptor[name=foo, propertyType=null", + "indexedPropertyType=class java.lang.String", + "indexedWriteMethod=public java.lang.Object"); + } + } + + @Test + public void nonIndexedEquality() throws IntrospectionException, SecurityException, NoSuchMethodException { + Object pd1 = new ExtendedBeanInfo.SimplePropertyDescriptor("foo", null, null); + assertThat(pd1).isEqualTo(pd1); + + Object pd2 = new ExtendedBeanInfo.SimplePropertyDescriptor("foo", null, null); + assertThat(pd1).isEqualTo(pd2); + assertThat(pd2).isEqualTo(pd1); + + @SuppressWarnings("unused") + class C { + public Object setFoo(String foo) { return null; } + public String getFoo() { return null; } + } + Method wm1 = C.class.getMethod("setFoo", String.class); + Object pd3 = new ExtendedBeanInfo.SimplePropertyDescriptor("foo", null, wm1); + assertThat(pd1).isNotEqualTo(pd3); + assertThat(pd3).isNotEqualTo(pd1); + + Method rm1 = C.class.getMethod("getFoo"); + Object pd4 = new ExtendedBeanInfo.SimplePropertyDescriptor("foo", rm1, null); + assertThat(pd1).isNotEqualTo(pd4); + assertThat(pd4).isNotEqualTo(pd1); + + Object pd5 = new PropertyDescriptor("foo", null, null); + assertThat(pd1).isEqualTo(pd5); + assertThat(pd5).isEqualTo(pd1); + + Object pd6 = "not a PD"; + assertThat(pd1).isNotEqualTo(pd6); + assertThat(pd6).isNotEqualTo(pd1); + + Object pd7 = null; + assertThat(pd1).isNotEqualTo(pd7); + assertThat(pd7).isNotEqualTo(pd1); + } + + @Test + public void indexedEquality() throws IntrospectionException, SecurityException, NoSuchMethodException { + Object pd1 = new ExtendedBeanInfo.SimpleIndexedPropertyDescriptor("foo", null, null, null, null); + assertThat(pd1).isEqualTo(pd1); + + Object pd2 = new ExtendedBeanInfo.SimpleIndexedPropertyDescriptor("foo", null, null, null, null); + assertThat(pd1).isEqualTo(pd2); + assertThat(pd2).isEqualTo(pd1); + + @SuppressWarnings("unused") + class C { + public Object setFoo(int i, String foo) { return null; } + public String getFoo(int i) { return null; } + } + Method wm1 = C.class.getMethod("setFoo", int.class, String.class); + Object pd3 = new ExtendedBeanInfo.SimpleIndexedPropertyDescriptor("foo", null, null, null, wm1); + assertThat(pd1).isNotEqualTo(pd3); + assertThat(pd3).isNotEqualTo(pd1); + + Method rm1 = C.class.getMethod("getFoo", int.class); + Object pd4 = new ExtendedBeanInfo.SimpleIndexedPropertyDescriptor("foo", null, null, rm1, null); + assertThat(pd1).isNotEqualTo(pd4); + assertThat(pd4).isNotEqualTo(pd1); + + Object pd5 = new IndexedPropertyDescriptor("foo", null, null, null, null); + assertThat(pd1).isEqualTo(pd5); + assertThat(pd5).isEqualTo(pd1); + + Object pd6 = "not a PD"; + assertThat(pd1).isNotEqualTo(pd6); + assertThat(pd6).isNotEqualTo(pd1); + + Object pd7 = null; + assertThat(pd1).isNotEqualTo(pd7); + assertThat(pd7).isNotEqualTo(pd1); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/BeanFactoryUtilsTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/BeanFactoryUtilsTests.java new file mode 100644 index 0000000..e869c9c --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/BeanFactoryUtilsTests.java @@ -0,0 +1,493 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.StaticListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.beans.testfixture.beans.AnnotatedBean; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.IndexedTestBean; +import org.springframework.beans.testfixture.beans.TestAnnotation; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.beans.testfixture.beans.factory.DummyFactory; +import org.springframework.cglib.proxy.NoOp; +import org.springframework.core.annotation.AliasFor; +import org.springframework.core.io.Resource; +import org.springframework.util.ObjectUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.core.testfixture.io.ResourceTestUtils.qualifiedResource; + +/** + * @author Rod Johnson + * @author Juergen Hoeller + * @author Chris Beams + * @author Sam Brannen + * @since 04.07.2003 + */ +public class BeanFactoryUtilsTests { + + private static final Class CLASS = BeanFactoryUtilsTests.class; + private static final Resource ROOT_CONTEXT = qualifiedResource(CLASS, "root.xml"); + private static final Resource MIDDLE_CONTEXT = qualifiedResource(CLASS, "middle.xml"); + private static final Resource LEAF_CONTEXT = qualifiedResource(CLASS, "leaf.xml"); + private static final Resource DEPENDENT_BEANS_CONTEXT = qualifiedResource(CLASS, "dependentBeans.xml"); + + private DefaultListableBeanFactory listableBeanFactory; + + private DefaultListableBeanFactory dependentBeansFactory; + + + @BeforeEach + public void setup() { + // Interesting hierarchical factory to test counts. + + DefaultListableBeanFactory grandParent = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(grandParent).loadBeanDefinitions(ROOT_CONTEXT); + DefaultListableBeanFactory parent = new DefaultListableBeanFactory(grandParent); + new XmlBeanDefinitionReader(parent).loadBeanDefinitions(MIDDLE_CONTEXT); + DefaultListableBeanFactory child = new DefaultListableBeanFactory(parent); + new XmlBeanDefinitionReader(child).loadBeanDefinitions(LEAF_CONTEXT); + + this.dependentBeansFactory = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(this.dependentBeansFactory).loadBeanDefinitions(DEPENDENT_BEANS_CONTEXT); + dependentBeansFactory.preInstantiateSingletons(); + this.listableBeanFactory = child; + } + + + @Test + public void testHierarchicalCountBeansWithNonHierarchicalFactory() { + StaticListableBeanFactory lbf = new StaticListableBeanFactory(); + lbf.addBean("t1", new TestBean()); + lbf.addBean("t2", new TestBean()); + assertThat(BeanFactoryUtils.countBeansIncludingAncestors(lbf) == 2).isTrue(); + } + + /** + * Check that override doesn't count as two separate beans. + */ + @Test + public void testHierarchicalCountBeansWithOverride() { + // Leaf count + assertThat(this.listableBeanFactory.getBeanDefinitionCount() == 1).isTrue(); + // Count minus duplicate + assertThat(BeanFactoryUtils.countBeansIncludingAncestors(this.listableBeanFactory) == 8).as("Should count 8 beans, not " + BeanFactoryUtils.countBeansIncludingAncestors(this.listableBeanFactory)).isTrue(); + } + + @Test + public void testHierarchicalNamesWithNoMatch() { + List names = Arrays.asList( + BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.listableBeanFactory, NoOp.class)); + assertThat(names.size()).isEqualTo(0); + } + + @Test + public void testHierarchicalNamesWithMatchOnlyInRoot() { + List names = Arrays.asList( + BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.listableBeanFactory, IndexedTestBean.class)); + assertThat(names.size()).isEqualTo(1); + assertThat(names.contains("indexedBean")).isTrue(); + // Distinguish from default ListableBeanFactory behavior + assertThat(listableBeanFactory.getBeanNamesForType(IndexedTestBean.class).length == 0).isTrue(); + } + + @Test + public void testGetBeanNamesForTypeWithOverride() { + List names = Arrays.asList( + BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.listableBeanFactory, ITestBean.class)); + // includes 2 TestBeans from FactoryBeans (DummyFactory definitions) + assertThat(names.size()).isEqualTo(4); + assertThat(names.contains("test")).isTrue(); + assertThat(names.contains("test3")).isTrue(); + assertThat(names.contains("testFactory1")).isTrue(); + assertThat(names.contains("testFactory2")).isTrue(); + } + + @Test + public void testNoBeansOfType() { + StaticListableBeanFactory lbf = new StaticListableBeanFactory(); + lbf.addBean("foo", new Object()); + Map beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(lbf, ITestBean.class, true, false); + assertThat(beans.isEmpty()).isTrue(); + } + + @Test + public void testFindsBeansOfTypeWithStaticFactory() { + StaticListableBeanFactory lbf = new StaticListableBeanFactory(); + TestBean t1 = new TestBean(); + TestBean t2 = new TestBean(); + DummyFactory t3 = new DummyFactory(); + DummyFactory t4 = new DummyFactory(); + t4.setSingleton(false); + lbf.addBean("t1", t1); + lbf.addBean("t2", t2); + lbf.addBean("t3", t3); + lbf.addBean("t4", t4); + + Map beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(lbf, ITestBean.class, true, true); + assertThat(beans.size()).isEqualTo(4); + assertThat(beans.get("t1")).isEqualTo(t1); + assertThat(beans.get("t2")).isEqualTo(t2); + assertThat(beans.get("t3")).isEqualTo(t3.getObject()); + boolean condition = beans.get("t4") instanceof TestBean; + assertThat(condition).isTrue(); + + beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(lbf, DummyFactory.class, true, true); + assertThat(beans.size()).isEqualTo(2); + assertThat(beans.get("&t3")).isEqualTo(t3); + assertThat(beans.get("&t4")).isEqualTo(t4); + + beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(lbf, FactoryBean.class, true, true); + assertThat(beans.size()).isEqualTo(2); + assertThat(beans.get("&t3")).isEqualTo(t3); + assertThat(beans.get("&t4")).isEqualTo(t4); + } + + @Test + public void testFindsBeansOfTypeWithDefaultFactory() { + Object test3 = this.listableBeanFactory.getBean("test3"); + Object test = this.listableBeanFactory.getBean("test"); + + TestBean t1 = new TestBean(); + TestBean t2 = new TestBean(); + DummyFactory t3 = new DummyFactory(); + DummyFactory t4 = new DummyFactory(); + t4.setSingleton(false); + this.listableBeanFactory.registerSingleton("t1", t1); + this.listableBeanFactory.registerSingleton("t2", t2); + this.listableBeanFactory.registerSingleton("t3", t3); + this.listableBeanFactory.registerSingleton("t4", t4); + + Map beans = + BeanFactoryUtils.beansOfTypeIncludingAncestors(this.listableBeanFactory, ITestBean.class, true, false); + assertThat(beans.size()).isEqualTo(6); + assertThat(beans.get("test3")).isEqualTo(test3); + assertThat(beans.get("test")).isEqualTo(test); + assertThat(beans.get("t1")).isEqualTo(t1); + assertThat(beans.get("t2")).isEqualTo(t2); + assertThat(beans.get("t3")).isEqualTo(t3.getObject()); + boolean condition2 = beans.get("t4") instanceof TestBean; + assertThat(condition2).isTrue(); + // t3 and t4 are found here as of Spring 2.0, since they are pre-registered + // singleton instances, while testFactory1 and testFactory are *not* found + // because they are FactoryBean definitions that haven't been initialized yet. + + beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(this.listableBeanFactory, ITestBean.class, false, true); + Object testFactory1 = this.listableBeanFactory.getBean("testFactory1"); + assertThat(beans.size()).isEqualTo(5); + assertThat(beans.get("test")).isEqualTo(test); + assertThat(beans.get("testFactory1")).isEqualTo(testFactory1); + assertThat(beans.get("t1")).isEqualTo(t1); + assertThat(beans.get("t2")).isEqualTo(t2); + assertThat(beans.get("t3")).isEqualTo(t3.getObject()); + + beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(this.listableBeanFactory, ITestBean.class, true, true); + assertThat(beans.size()).isEqualTo(8); + assertThat(beans.get("test3")).isEqualTo(test3); + assertThat(beans.get("test")).isEqualTo(test); + assertThat(beans.get("testFactory1")).isEqualTo(testFactory1); + boolean condition1 = beans.get("testFactory2") instanceof TestBean; + assertThat(condition1).isTrue(); + assertThat(beans.get("t1")).isEqualTo(t1); + assertThat(beans.get("t2")).isEqualTo(t2); + assertThat(beans.get("t3")).isEqualTo(t3.getObject()); + boolean condition = beans.get("t4") instanceof TestBean; + assertThat(condition).isTrue(); + + beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(this.listableBeanFactory, DummyFactory.class, true, true); + assertThat(beans.size()).isEqualTo(4); + assertThat(beans.get("&testFactory1")).isEqualTo(this.listableBeanFactory.getBean("&testFactory1")); + assertThat(beans.get("&testFactory2")).isEqualTo(this.listableBeanFactory.getBean("&testFactory2")); + assertThat(beans.get("&t3")).isEqualTo(t3); + assertThat(beans.get("&t4")).isEqualTo(t4); + + beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(this.listableBeanFactory, FactoryBean.class, true, true); + assertThat(beans.size()).isEqualTo(4); + assertThat(beans.get("&testFactory1")).isEqualTo(this.listableBeanFactory.getBean("&testFactory1")); + assertThat(beans.get("&testFactory2")).isEqualTo(this.listableBeanFactory.getBean("&testFactory2")); + assertThat(beans.get("&t3")).isEqualTo(t3); + assertThat(beans.get("&t4")).isEqualTo(t4); + } + + @Test + public void testHierarchicalResolutionWithOverride() { + Object test3 = this.listableBeanFactory.getBean("test3"); + Object test = this.listableBeanFactory.getBean("test"); + + Map beans = + BeanFactoryUtils.beansOfTypeIncludingAncestors(this.listableBeanFactory, ITestBean.class, true, false); + assertThat(beans.size()).isEqualTo(2); + assertThat(beans.get("test3")).isEqualTo(test3); + assertThat(beans.get("test")).isEqualTo(test); + + beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(this.listableBeanFactory, ITestBean.class, false, false); + assertThat(beans.size()).isEqualTo(1); + assertThat(beans.get("test")).isEqualTo(test); + + beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(this.listableBeanFactory, ITestBean.class, false, true); + Object testFactory1 = this.listableBeanFactory.getBean("testFactory1"); + assertThat(beans.size()).isEqualTo(2); + assertThat(beans.get("test")).isEqualTo(test); + assertThat(beans.get("testFactory1")).isEqualTo(testFactory1); + + beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(this.listableBeanFactory, ITestBean.class, true, true); + assertThat(beans.size()).isEqualTo(4); + assertThat(beans.get("test3")).isEqualTo(test3); + assertThat(beans.get("test")).isEqualTo(test); + assertThat(beans.get("testFactory1")).isEqualTo(testFactory1); + boolean condition = beans.get("testFactory2") instanceof TestBean; + assertThat(condition).isTrue(); + + beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(this.listableBeanFactory, DummyFactory.class, true, true); + assertThat(beans.size()).isEqualTo(2); + assertThat(beans.get("&testFactory1")).isEqualTo(this.listableBeanFactory.getBean("&testFactory1")); + assertThat(beans.get("&testFactory2")).isEqualTo(this.listableBeanFactory.getBean("&testFactory2")); + + beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(this.listableBeanFactory, FactoryBean.class, true, true); + assertThat(beans.size()).isEqualTo(2); + assertThat(beans.get("&testFactory1")).isEqualTo(this.listableBeanFactory.getBean("&testFactory1")); + assertThat(beans.get("&testFactory2")).isEqualTo(this.listableBeanFactory.getBean("&testFactory2")); + } + + @Test + public void testHierarchicalNamesForAnnotationWithNoMatch() { + List names = Arrays.asList( + BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(this.listableBeanFactory, Override.class)); + assertThat(names.size()).isEqualTo(0); + } + + @Test + public void testHierarchicalNamesForAnnotationWithMatchOnlyInRoot() { + List names = Arrays.asList( + BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(this.listableBeanFactory, TestAnnotation.class)); + assertThat(names.size()).isEqualTo(1); + assertThat(names.contains("annotatedBean")).isTrue(); + // Distinguish from default ListableBeanFactory behavior + assertThat(listableBeanFactory.getBeanNamesForAnnotation(TestAnnotation.class).length == 0).isTrue(); + } + + @Test + public void testGetBeanNamesForAnnotationWithOverride() { + AnnotatedBean annotatedBean = new AnnotatedBean(); + this.listableBeanFactory.registerSingleton("anotherAnnotatedBean", annotatedBean); + List names = Arrays.asList( + BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(this.listableBeanFactory, TestAnnotation.class)); + assertThat(names.size()).isEqualTo(2); + assertThat(names.contains("annotatedBean")).isTrue(); + assertThat(names.contains("anotherAnnotatedBean")).isTrue(); + } + + @Test + public void testADependencies() { + String[] deps = this.dependentBeansFactory.getDependentBeans("a"); + assertThat(ObjectUtils.isEmpty(deps)).isTrue(); + } + + @Test + public void testBDependencies() { + String[] deps = this.dependentBeansFactory.getDependentBeans("b"); + assertThat(Arrays.equals(new String[] { "c" }, deps)).isTrue(); + } + + @Test + public void testCDependencies() { + String[] deps = this.dependentBeansFactory.getDependentBeans("c"); + assertThat(Arrays.equals(new String[] { "int", "long" }, deps)).isTrue(); + } + + @Test + public void testIntDependencies() { + String[] deps = this.dependentBeansFactory.getDependentBeans("int"); + assertThat(Arrays.equals(new String[] { "buffer" }, deps)).isTrue(); + } + + @Test + public void findAnnotationOnBean() { + this.listableBeanFactory.registerSingleton("controllerAdvice", new ControllerAdviceClass()); + this.listableBeanFactory.registerSingleton("restControllerAdvice", new RestControllerAdviceClass()); + testFindAnnotationOnBean(this.listableBeanFactory); + } + + @Test // gh-25520 + public void findAnnotationOnBeanWithStaticFactory() { + StaticListableBeanFactory lbf = new StaticListableBeanFactory(); + lbf.addBean("controllerAdvice", new ControllerAdviceClass()); + lbf.addBean("restControllerAdvice", new RestControllerAdviceClass()); + testFindAnnotationOnBean(lbf); + } + + private void testFindAnnotationOnBean(ListableBeanFactory lbf) { + assertControllerAdvice(lbf, "controllerAdvice"); + assertControllerAdvice(lbf, "restControllerAdvice"); + } + + private void assertControllerAdvice(ListableBeanFactory lbf, String beanName) { + ControllerAdvice controllerAdvice = lbf.findAnnotationOnBean(beanName, ControllerAdvice.class); + assertThat(controllerAdvice).isNotNull(); + assertThat(controllerAdvice.value()).isEqualTo("com.example"); + assertThat(controllerAdvice.basePackage()).isEqualTo("com.example"); + } + + @Test + public void isSingletonAndIsPrototypeWithStaticFactory() { + StaticListableBeanFactory lbf = new StaticListableBeanFactory(); + TestBean bean = new TestBean(); + DummyFactory fb1 = new DummyFactory(); + DummyFactory fb2 = new DummyFactory(); + fb2.setSingleton(false); + TestBeanSmartFactoryBean sfb1 = new TestBeanSmartFactoryBean(true, true); + TestBeanSmartFactoryBean sfb2 = new TestBeanSmartFactoryBean(true, false); + TestBeanSmartFactoryBean sfb3 = new TestBeanSmartFactoryBean(false, true); + TestBeanSmartFactoryBean sfb4 = new TestBeanSmartFactoryBean(false, false); + lbf.addBean("bean", bean); + lbf.addBean("fb1", fb1); + lbf.addBean("fb2", fb2); + lbf.addBean("sfb1", sfb1); + lbf.addBean("sfb2", sfb2); + lbf.addBean("sfb3", sfb3); + lbf.addBean("sfb4", sfb4); + + Map beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(lbf, ITestBean.class, true, true); + assertThat(beans.get("bean")).isSameAs(bean); + assertThat(beans.get("fb1")).isSameAs(fb1.getObject()); + assertThat(beans.get("fb2")).isInstanceOf(TestBean.class); + assertThat(beans.get("sfb1")).isInstanceOf(TestBean.class); + assertThat(beans.get("sfb2")).isInstanceOf(TestBean.class); + assertThat(beans.get("sfb3")).isInstanceOf(TestBean.class); + assertThat(beans.get("sfb4")).isInstanceOf(TestBean.class); + + assertThat(lbf.getBeanDefinitionCount()).isEqualTo(7); + assertThat(lbf.getBean("bean")).isInstanceOf(TestBean.class); + assertThat(lbf.getBean("&fb1")).isInstanceOf(FactoryBean.class); + assertThat(lbf.getBean("&fb2")).isInstanceOf(FactoryBean.class); + assertThat(lbf.getBean("&sfb1")).isInstanceOf(SmartFactoryBean.class); + assertThat(lbf.getBean("&sfb2")).isInstanceOf(SmartFactoryBean.class); + assertThat(lbf.getBean("&sfb3")).isInstanceOf(SmartFactoryBean.class); + assertThat(lbf.getBean("&sfb4")).isInstanceOf(SmartFactoryBean.class); + + assertThat(lbf.isSingleton("bean")).isTrue(); + assertThat(lbf.isSingleton("fb1")).isTrue(); + assertThat(lbf.isSingleton("fb2")).isTrue(); + assertThat(lbf.isSingleton("sfb1")).isTrue(); + assertThat(lbf.isSingleton("sfb2")).isTrue(); + assertThat(lbf.isSingleton("sfb3")).isTrue(); + assertThat(lbf.isSingleton("sfb4")).isTrue(); + + assertThat(lbf.isSingleton("&fb1")).isTrue(); + assertThat(lbf.isSingleton("&fb2")).isFalse(); + assertThat(lbf.isSingleton("&sfb1")).isTrue(); + assertThat(lbf.isSingleton("&sfb2")).isTrue(); + assertThat(lbf.isSingleton("&sfb3")).isFalse(); + assertThat(lbf.isSingleton("&sfb4")).isFalse(); + + assertThat(lbf.isPrototype("bean")).isFalse(); + assertThat(lbf.isPrototype("fb1")).isFalse(); + assertThat(lbf.isPrototype("fb2")).isFalse(); + assertThat(lbf.isPrototype("sfb1")).isFalse(); + assertThat(lbf.isPrototype("sfb2")).isFalse(); + assertThat(lbf.isPrototype("sfb3")).isFalse(); + assertThat(lbf.isPrototype("sfb4")).isFalse(); + + assertThat(lbf.isPrototype("&fb1")).isFalse(); + assertThat(lbf.isPrototype("&fb2")).isTrue(); + assertThat(lbf.isPrototype("&sfb1")).isTrue(); + assertThat(lbf.isPrototype("&sfb2")).isFalse(); + assertThat(lbf.isPrototype("&sfb3")).isTrue(); + assertThat(lbf.isPrototype("&sfb4")).isTrue(); + } + + + @Retention(RetentionPolicy.RUNTIME) + @interface ControllerAdvice { + + @AliasFor("basePackage") + String value() default ""; + + @AliasFor("value") + String basePackage() default ""; + } + + + @Retention(RetentionPolicy.RUNTIME) + @ControllerAdvice + @interface RestControllerAdvice { + + @AliasFor(annotation = ControllerAdvice.class) + String value() default ""; + + @AliasFor(annotation = ControllerAdvice.class) + String basePackage() default ""; + } + + + @ControllerAdvice("com.example") + static class ControllerAdviceClass { + } + + + @RestControllerAdvice("com.example") + static class RestControllerAdviceClass { + } + + + static class TestBeanSmartFactoryBean implements SmartFactoryBean { + + private final TestBean testBean = new TestBean("enigma", 42); + + private final boolean singleton; + + private final boolean prototype; + + TestBeanSmartFactoryBean(boolean singleton, boolean prototype) { + this.singleton = singleton; + this.prototype = prototype; + } + + @Override + public boolean isSingleton() { + return this.singleton; + } + + @Override + public boolean isPrototype() { + return this.prototype; + } + + @Override + public Class getObjectType() { + return TestBean.class; + } + + public TestBean getObject() { + // We don't really care if the actual instance is a singleton or prototype + // for the tests that use this factory. + return this.testBean; + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java new file mode 100644 index 0000000..13321a7 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java @@ -0,0 +1,3171 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +import java.io.Closeable; +import java.io.Serializable; +import java.lang.reflect.Field; +import java.net.MalformedURLException; +import java.security.AccessControlContext; +import java.security.AccessController; +import java.security.Principal; +import java.security.PrivilegedAction; +import java.text.NumberFormat; +import java.text.ParseException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import javax.annotation.Priority; +import javax.security.auth.Subject; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.BeansException; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.NotWritablePropertyException; +import org.springframework.beans.PropertyEditorRegistrar; +import org.springframework.beans.PropertyEditorRegistry; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.TypeConverter; +import org.springframework.beans.TypeMismatchException; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.beans.factory.config.AutowiredPropertyMarker; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanExpressionContext; +import org.springframework.beans.factory.config.BeanExpressionResolver; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor; +import org.springframework.beans.factory.config.PropertiesFactoryBean; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.config.TypedStringValue; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionOverrideException; +import org.springframework.beans.factory.support.ChildBeanDefinition; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.ConstructorDependenciesBean; +import org.springframework.beans.propertyeditors.CustomNumberEditor; +import org.springframework.beans.testfixture.beans.DependenciesBean; +import org.springframework.beans.testfixture.beans.DerivedTestBean; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.NestedTestBean; +import org.springframework.beans.testfixture.beans.SideEffectBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.beans.testfixture.beans.factory.DummyFactory; +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; +import org.springframework.core.testfixture.io.SerializationTestUtils; +import org.springframework.core.testfixture.security.TestPrincipal; +import org.springframework.lang.Nullable; +import org.springframework.util.StringValueResolver; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +/** + * Tests properties population and autowire behavior. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Rick Evans + * @author Sam Brannen + * @author Chris Beams + * @author Phillip Webb + * @author Stephane Nicoll + */ +class DefaultListableBeanFactoryTests { + + private DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + + + @Test + void unreferencedSingletonWasInstantiated() { + KnowsIfInstantiated.clearInstantiationRecord(); + Properties p = new Properties(); + p.setProperty("x1.(class)", KnowsIfInstantiated.class.getName()); + assertThat(!KnowsIfInstantiated.wasInstantiated()).as("singleton not instantiated").isTrue(); + registerBeanDefinitions(p); + lbf.preInstantiateSingletons(); + assertThat(KnowsIfInstantiated.wasInstantiated()).as("singleton was instantiated").isTrue(); + } + + @Test + void lazyInitialization() { + KnowsIfInstantiated.clearInstantiationRecord(); + Properties p = new Properties(); + p.setProperty("x1.(class)", KnowsIfInstantiated.class.getName()); + p.setProperty("x1.(lazy-init)", "true"); + assertThat(!KnowsIfInstantiated.wasInstantiated()).as("singleton not instantiated").isTrue(); + registerBeanDefinitions(p); + assertThat(!KnowsIfInstantiated.wasInstantiated()).as("singleton not instantiated").isTrue(); + lbf.preInstantiateSingletons(); + + assertThat(!KnowsIfInstantiated.wasInstantiated()).as("singleton not instantiated").isTrue(); + lbf.getBean("x1"); + assertThat(KnowsIfInstantiated.wasInstantiated()).as("singleton was instantiated").isTrue(); + } + + @Test + void factoryBeanDidNotCreatePrototype() { + Properties p = new Properties(); + p.setProperty("x1.(class)", DummyFactory.class.getName()); + // Reset static state + DummyFactory.reset(); + p.setProperty("x1.singleton", "false"); + assertThat(!DummyFactory.wasPrototypeCreated()).as("prototype not instantiated").isTrue(); + registerBeanDefinitions(p); + assertThat(!DummyFactory.wasPrototypeCreated()).as("prototype not instantiated").isTrue(); + assertThat(lbf.getType("x1")).isEqualTo(TestBean.class); + lbf.preInstantiateSingletons(); + + assertThat(!DummyFactory.wasPrototypeCreated()).as("prototype not instantiated").isTrue(); + lbf.getBean("x1"); + assertThat(lbf.getType("x1")).isEqualTo(TestBean.class); + assertThat(lbf.containsBean("x1")).isTrue(); + assertThat(lbf.containsBean("&x1")).isTrue(); + assertThat(DummyFactory.wasPrototypeCreated()).as("prototype was instantiated").isTrue(); + } + + @Test + void prototypeFactoryBeanIgnoredByNonEagerTypeMatching() { + Properties p = new Properties(); + p.setProperty("x1.(class)", DummyFactory.class.getName()); + // Reset static state + DummyFactory.reset(); + p.setProperty("x1.(singleton)", "false"); + p.setProperty("x1.singleton", "false"); + registerBeanDefinitions(p); + + assertThat(!DummyFactory.wasPrototypeCreated()).as("prototype not instantiated").isTrue(); + String[] beanNames = lbf.getBeanNamesForType(TestBean.class, true, false); + assertThat(beanNames.length).isEqualTo(0); + beanNames = lbf.getBeanNamesForAnnotation(SuppressWarnings.class); + assertThat(beanNames.length).isEqualTo(0); + + assertThat(lbf.containsSingleton("x1")).isFalse(); + assertThat(lbf.containsBean("x1")).isTrue(); + assertThat(lbf.containsBean("&x1")).isTrue(); + assertThat(lbf.isSingleton("x1")).isFalse(); + assertThat(lbf.isSingleton("&x1")).isFalse(); + assertThat(lbf.isPrototype("x1")).isTrue(); + assertThat(lbf.isPrototype("&x1")).isTrue(); + assertThat(lbf.isTypeMatch("x1", TestBean.class)).isTrue(); + assertThat(lbf.isTypeMatch("&x1", TestBean.class)).isFalse(); + assertThat(lbf.isTypeMatch("&x1", DummyFactory.class)).isTrue(); + assertThat(lbf.isTypeMatch("&x1", ResolvableType.forClass(DummyFactory.class))).isTrue(); + assertThat(lbf.isTypeMatch("&x1", ResolvableType.forClassWithGenerics(FactoryBean.class, Object.class))).isTrue(); + assertThat(lbf.isTypeMatch("&x1", ResolvableType.forClassWithGenerics(FactoryBean.class, String.class))).isFalse(); + assertThat(lbf.getType("x1")).isEqualTo(TestBean.class); + assertThat(lbf.getType("&x1")).isEqualTo(DummyFactory.class); + assertThat(!DummyFactory.wasPrototypeCreated()).as("prototype not instantiated").isTrue(); + } + + @Test + void singletonFactoryBeanIgnoredByNonEagerTypeMatching() { + Properties p = new Properties(); + p.setProperty("x1.(class)", DummyFactory.class.getName()); + // Reset static state + DummyFactory.reset(); + p.setProperty("x1.(singleton)", "false"); + p.setProperty("x1.singleton", "true"); + registerBeanDefinitions(p); + + assertThat(!DummyFactory.wasPrototypeCreated()).as("prototype not instantiated").isTrue(); + String[] beanNames = lbf.getBeanNamesForType(TestBean.class, true, false); + assertThat(beanNames.length).isEqualTo(0); + beanNames = lbf.getBeanNamesForAnnotation(SuppressWarnings.class); + assertThat(beanNames.length).isEqualTo(0); + + assertThat(lbf.containsSingleton("x1")).isFalse(); + assertThat(lbf.containsBean("x1")).isTrue(); + assertThat(lbf.containsBean("&x1")).isTrue(); + assertThat(lbf.isSingleton("x1")).isFalse(); + assertThat(lbf.isSingleton("&x1")).isFalse(); + assertThat(lbf.isPrototype("x1")).isTrue(); + assertThat(lbf.isPrototype("&x1")).isTrue(); + assertThat(lbf.isTypeMatch("x1", TestBean.class)).isTrue(); + assertThat(lbf.isTypeMatch("&x1", TestBean.class)).isFalse(); + assertThat(lbf.isTypeMatch("&x1", DummyFactory.class)).isTrue(); + assertThat(lbf.isTypeMatch("&x1", ResolvableType.forClass(DummyFactory.class))).isTrue(); + assertThat(lbf.isTypeMatch("&x1", ResolvableType.forClassWithGenerics(FactoryBean.class, Object.class))).isTrue(); + assertThat(lbf.isTypeMatch("&x1", ResolvableType.forClassWithGenerics(FactoryBean.class, String.class))).isFalse(); + assertThat(lbf.getType("x1")).isEqualTo(TestBean.class); + assertThat(lbf.getType("&x1")).isEqualTo(DummyFactory.class); + assertThat(!DummyFactory.wasPrototypeCreated()).as("prototype not instantiated").isTrue(); + } + + @Test + void nonInitializedFactoryBeanIgnoredByNonEagerTypeMatching() { + Properties p = new Properties(); + p.setProperty("x1.(class)", DummyFactory.class.getName()); + // Reset static state + DummyFactory.reset(); + p.setProperty("x1.singleton", "false"); + registerBeanDefinitions(p); + + assertThat(!DummyFactory.wasPrototypeCreated()).as("prototype not instantiated").isTrue(); + String[] beanNames = lbf.getBeanNamesForType(TestBean.class, true, false); + assertThat(beanNames.length).isEqualTo(0); + beanNames = lbf.getBeanNamesForAnnotation(SuppressWarnings.class); + assertThat(beanNames.length).isEqualTo(0); + + assertThat(lbf.containsSingleton("x1")).isFalse(); + assertThat(lbf.containsBean("x1")).isTrue(); + assertThat(lbf.containsBean("&x1")).isTrue(); + assertThat(lbf.isSingleton("x1")).isFalse(); + assertThat(lbf.isSingleton("&x1")).isTrue(); + assertThat(lbf.isPrototype("x1")).isTrue(); + assertThat(lbf.isPrototype("&x1")).isFalse(); + assertThat(lbf.isTypeMatch("x1", TestBean.class)).isTrue(); + assertThat(lbf.isTypeMatch("&x1", TestBean.class)).isFalse(); + assertThat(lbf.isTypeMatch("&x1", DummyFactory.class)).isTrue(); + assertThat(lbf.isTypeMatch("&x1", ResolvableType.forClass(DummyFactory.class))).isTrue(); + assertThat(lbf.isTypeMatch("&x1", ResolvableType.forClassWithGenerics(FactoryBean.class, Object.class))).isTrue(); + assertThat(lbf.isTypeMatch("&x1", ResolvableType.forClassWithGenerics(FactoryBean.class, String.class))).isFalse(); + assertThat(lbf.getType("x1")).isEqualTo(TestBean.class); + assertThat(lbf.getType("&x1")).isEqualTo(DummyFactory.class); + assertThat(!DummyFactory.wasPrototypeCreated()).as("prototype not instantiated").isTrue(); + } + + @Test + void initializedFactoryBeanFoundByNonEagerTypeMatching() { + Properties p = new Properties(); + p.setProperty("x1.(class)", DummyFactory.class.getName()); + // Reset static state + DummyFactory.reset(); + p.setProperty("x1.singleton", "false"); + registerBeanDefinitions(p); + lbf.preInstantiateSingletons(); + + assertThat(!DummyFactory.wasPrototypeCreated()).as("prototype not instantiated").isTrue(); + String[] beanNames = lbf.getBeanNamesForType(TestBean.class, true, false); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("x1"); + assertThat(lbf.containsSingleton("x1")).isTrue(); + assertThat(lbf.containsBean("x1")).isTrue(); + assertThat(lbf.containsBean("&x1")).isTrue(); + assertThat(lbf.containsLocalBean("x1")).isTrue(); + assertThat(lbf.containsLocalBean("&x1")).isTrue(); + assertThat(lbf.isSingleton("x1")).isFalse(); + assertThat(lbf.isSingleton("&x1")).isTrue(); + assertThat(lbf.isPrototype("x1")).isTrue(); + assertThat(lbf.isPrototype("&x1")).isFalse(); + assertThat(lbf.isTypeMatch("x1", TestBean.class)).isTrue(); + assertThat(lbf.isTypeMatch("&x1", TestBean.class)).isFalse(); + assertThat(lbf.isTypeMatch("&x1", DummyFactory.class)).isTrue(); + assertThat(lbf.isTypeMatch("x1", Object.class)).isTrue(); + assertThat(lbf.isTypeMatch("&x1", Object.class)).isTrue(); + assertThat(lbf.getType("x1")).isEqualTo(TestBean.class); + assertThat(lbf.getType("&x1")).isEqualTo(DummyFactory.class); + assertThat(!DummyFactory.wasPrototypeCreated()).as("prototype not instantiated").isTrue(); + + lbf.registerAlias("x1", "x2"); + assertThat(lbf.containsBean("x2")).isTrue(); + assertThat(lbf.containsBean("&x2")).isTrue(); + assertThat(lbf.containsLocalBean("x2")).isTrue(); + assertThat(lbf.containsLocalBean("&x2")).isTrue(); + assertThat(lbf.isSingleton("x2")).isFalse(); + assertThat(lbf.isSingleton("&x2")).isTrue(); + assertThat(lbf.isPrototype("x2")).isTrue(); + assertThat(lbf.isPrototype("&x2")).isFalse(); + assertThat(lbf.isTypeMatch("x2", TestBean.class)).isTrue(); + assertThat(lbf.isTypeMatch("&x2", TestBean.class)).isFalse(); + assertThat(lbf.isTypeMatch("&x2", DummyFactory.class)).isTrue(); + assertThat(lbf.isTypeMatch("x2", Object.class)).isTrue(); + assertThat(lbf.isTypeMatch("&x2", Object.class)).isTrue(); + assertThat(lbf.getType("x2")).isEqualTo(TestBean.class); + assertThat(lbf.getType("&x2")).isEqualTo(DummyFactory.class); + assertThat(lbf.getAliases("x1").length).isEqualTo(1); + assertThat(lbf.getAliases("x1")[0]).isEqualTo("x2"); + assertThat(lbf.getAliases("&x1").length).isEqualTo(1); + assertThat(lbf.getAliases("&x1")[0]).isEqualTo("&x2"); + assertThat(lbf.getAliases("x2").length).isEqualTo(1); + assertThat(lbf.getAliases("x2")[0]).isEqualTo("x1"); + assertThat(lbf.getAliases("&x2").length).isEqualTo(1); + assertThat(lbf.getAliases("&x2")[0]).isEqualTo("&x1"); + } + + @Test + void staticFactoryMethodFoundByNonEagerTypeMatching() { + RootBeanDefinition rbd = new RootBeanDefinition(TestBeanFactory.class); + rbd.setFactoryMethodName("createTestBean"); + lbf.registerBeanDefinition("x1", rbd); + + TestBeanFactory.initialized = false; + String[] beanNames = lbf.getBeanNamesForType(TestBean.class, true, false); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("x1"); + assertThat(lbf.containsSingleton("x1")).isFalse(); + assertThat(lbf.containsBean("x1")).isTrue(); + assertThat(lbf.containsBean("&x1")).isFalse(); + assertThat(lbf.isSingleton("x1")).isTrue(); + assertThat(lbf.isSingleton("&x1")).isFalse(); + assertThat(lbf.isPrototype("x1")).isFalse(); + assertThat(lbf.isPrototype("&x1")).isFalse(); + assertThat(lbf.isTypeMatch("x1", TestBean.class)).isTrue(); + assertThat(lbf.isTypeMatch("&x1", TestBean.class)).isFalse(); + assertThat(lbf.getType("x1")).isEqualTo(TestBean.class); + assertThat(lbf.getType("&x1")).isEqualTo(null); + assertThat(TestBeanFactory.initialized).isFalse(); + } + + @Test + void staticPrototypeFactoryMethodFoundByNonEagerTypeMatching() { + RootBeanDefinition rbd = new RootBeanDefinition(TestBeanFactory.class); + rbd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + rbd.setFactoryMethodName("createTestBean"); + lbf.registerBeanDefinition("x1", rbd); + + TestBeanFactory.initialized = false; + String[] beanNames = lbf.getBeanNamesForType(TestBean.class, true, false); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("x1"); + assertThat(lbf.containsSingleton("x1")).isFalse(); + assertThat(lbf.containsBean("x1")).isTrue(); + assertThat(lbf.containsBean("&x1")).isFalse(); + assertThat(lbf.isSingleton("x1")).isFalse(); + assertThat(lbf.isSingleton("&x1")).isFalse(); + assertThat(lbf.isPrototype("x1")).isTrue(); + assertThat(lbf.isPrototype("&x1")).isFalse(); + assertThat(lbf.isTypeMatch("x1", TestBean.class)).isTrue(); + assertThat(lbf.isTypeMatch("&x1", TestBean.class)).isFalse(); + assertThat(lbf.getType("x1")).isEqualTo(TestBean.class); + assertThat(lbf.getType("&x1")).isEqualTo(null); + assertThat(TestBeanFactory.initialized).isFalse(); + } + + @Test + void nonStaticFactoryMethodFoundByNonEagerTypeMatching() { + RootBeanDefinition factoryBd = new RootBeanDefinition(TestBeanFactory.class); + lbf.registerBeanDefinition("factory", factoryBd); + RootBeanDefinition rbd = new RootBeanDefinition(TestBeanFactory.class); + rbd.setFactoryBeanName("factory"); + rbd.setFactoryMethodName("createTestBeanNonStatic"); + lbf.registerBeanDefinition("x1", rbd); + + TestBeanFactory.initialized = false; + String[] beanNames = lbf.getBeanNamesForType(TestBean.class, true, false); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("x1"); + assertThat(lbf.containsSingleton("x1")).isFalse(); + assertThat(lbf.containsBean("x1")).isTrue(); + assertThat(lbf.containsBean("&x1")).isFalse(); + assertThat(lbf.isSingleton("x1")).isTrue(); + assertThat(lbf.isSingleton("&x1")).isFalse(); + assertThat(lbf.isPrototype("x1")).isFalse(); + assertThat(lbf.isPrototype("&x1")).isFalse(); + assertThat(lbf.isTypeMatch("x1", TestBean.class)).isTrue(); + assertThat(lbf.isTypeMatch("&x1", TestBean.class)).isFalse(); + assertThat(lbf.getType("x1")).isEqualTo(TestBean.class); + assertThat(lbf.getType("&x1")).isEqualTo(null); + assertThat(TestBeanFactory.initialized).isFalse(); + } + + @Test + void nonStaticPrototypeFactoryMethodFoundByNonEagerTypeMatching() { + RootBeanDefinition factoryBd = new RootBeanDefinition(TestBeanFactory.class); + lbf.registerBeanDefinition("factory", factoryBd); + RootBeanDefinition rbd = new RootBeanDefinition(); + rbd.setFactoryBeanName("factory"); + rbd.setFactoryMethodName("createTestBeanNonStatic"); + rbd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + lbf.registerBeanDefinition("x1", rbd); + + TestBeanFactory.initialized = false; + String[] beanNames = lbf.getBeanNamesForType(TestBean.class, true, false); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("x1"); + assertThat(lbf.containsSingleton("x1")).isFalse(); + assertThat(lbf.containsBean("x1")).isTrue(); + assertThat(lbf.containsBean("&x1")).isFalse(); + assertThat(lbf.containsLocalBean("x1")).isTrue(); + assertThat(lbf.containsLocalBean("&x1")).isFalse(); + assertThat(lbf.isSingleton("x1")).isFalse(); + assertThat(lbf.isSingleton("&x1")).isFalse(); + assertThat(lbf.isPrototype("x1")).isTrue(); + assertThat(lbf.isPrototype("&x1")).isFalse(); + assertThat(lbf.isTypeMatch("x1", TestBean.class)).isTrue(); + assertThat(lbf.isTypeMatch("&x1", TestBean.class)).isFalse(); + assertThat(lbf.isTypeMatch("x1", Object.class)).isTrue(); + assertThat(lbf.isTypeMatch("&x1", Object.class)).isFalse(); + assertThat(lbf.getType("x1")).isEqualTo(TestBean.class); + assertThat(lbf.getType("&x1")).isEqualTo(null); + assertThat(TestBeanFactory.initialized).isFalse(); + + lbf.registerAlias("x1", "x2"); + assertThat(lbf.containsBean("x2")).isTrue(); + assertThat(lbf.containsBean("&x2")).isFalse(); + assertThat(lbf.containsLocalBean("x2")).isTrue(); + assertThat(lbf.containsLocalBean("&x2")).isFalse(); + assertThat(lbf.isSingleton("x2")).isFalse(); + assertThat(lbf.isSingleton("&x2")).isFalse(); + assertThat(lbf.isPrototype("x2")).isTrue(); + assertThat(lbf.isPrototype("&x2")).isFalse(); + assertThat(lbf.isTypeMatch("x2", TestBean.class)).isTrue(); + assertThat(lbf.isTypeMatch("&x2", TestBean.class)).isFalse(); + assertThat(lbf.isTypeMatch("x2", Object.class)).isTrue(); + assertThat(lbf.isTypeMatch("&x2", Object.class)).isFalse(); + assertThat(lbf.getType("x2")).isEqualTo(TestBean.class); + assertThat(lbf.getType("&x2")).isEqualTo(null); + assertThat(lbf.getAliases("x1").length).isEqualTo(1); + assertThat(lbf.getAliases("x1")[0]).isEqualTo("x2"); + assertThat(lbf.getAliases("&x1").length).isEqualTo(1); + assertThat(lbf.getAliases("&x1")[0]).isEqualTo("&x2"); + assertThat(lbf.getAliases("x2").length).isEqualTo(1); + assertThat(lbf.getAliases("x2")[0]).isEqualTo("x1"); + assertThat(lbf.getAliases("&x2").length).isEqualTo(1); + assertThat(lbf.getAliases("&x2")[0]).isEqualTo("&x1"); + } + + @Test + void empty() { + ListableBeanFactory lbf = new DefaultListableBeanFactory(); + assertThat(lbf.getBeanDefinitionNames() != null).as("No beans defined --> array != null").isTrue(); + assertThat(lbf.getBeanDefinitionNames().length == 0).as("No beans defined after no arg constructor").isTrue(); + assertThat(lbf.getBeanDefinitionCount() == 0).as("No beans defined after no arg constructor").isTrue(); + } + + @Test + void emptyPropertiesPopulation() { + Properties p = new Properties(); + registerBeanDefinitions(p); + assertThat(lbf.getBeanDefinitionCount() == 0).as("No beans defined after ignorable invalid").isTrue(); + } + + @Test + void harmlessIgnorableRubbish() { + Properties p = new Properties(); + p.setProperty("foo", "bar"); + p.setProperty("qwert", "er"); + registerBeanDefinitions(p, "test"); + assertThat(lbf.getBeanDefinitionCount() == 0).as("No beans defined after harmless ignorable rubbish").isTrue(); + } + + @Test + void propertiesPopulationWithNullPrefix() { + Properties p = new Properties(); + p.setProperty("test.(class)", TestBean.class.getName()); + p.setProperty("test.name", "Tony"); + p.setProperty("test.age", "48"); + int count = registerBeanDefinitions(p); + assertThat(count == 1).as("1 beans registered, not " + count).isTrue(); + singleTestBean(lbf); + } + + @Test + void propertiesPopulationWithPrefix() { + String PREFIX = "beans."; + Properties p = new Properties(); + p.setProperty(PREFIX + "test.(class)", TestBean.class.getName()); + p.setProperty(PREFIX + "test.name", "Tony"); + p.setProperty(PREFIX + "test.age", "0x30"); + int count = registerBeanDefinitions(p, PREFIX); + assertThat(count == 1).as("1 beans registered, not " + count).isTrue(); + singleTestBean(lbf); + } + + @Test + void simpleReference() { + String PREFIX = "beans."; + Properties p = new Properties(); + + p.setProperty(PREFIX + "rod.(class)", TestBean.class.getName()); + p.setProperty(PREFIX + "rod.name", "Rod"); + + p.setProperty(PREFIX + "kerry.(class)", TestBean.class.getName()); + p.setProperty(PREFIX + "kerry.name", "Kerry"); + p.setProperty(PREFIX + "kerry.age", "35"); + p.setProperty(PREFIX + "kerry.spouse(ref)", "rod"); + + int count = registerBeanDefinitions(p, PREFIX); + assertThat(count == 2).as("2 beans registered, not " + count).isTrue(); + + TestBean kerry = lbf.getBean("kerry", TestBean.class); + assertThat("Kerry".equals(kerry.getName())).as("Kerry name is Kerry").isTrue(); + ITestBean spouse = kerry.getSpouse(); + assertThat(spouse != null).as("Kerry spouse is non null").isTrue(); + assertThat("Rod".equals(spouse.getName())).as("Kerry spouse name is Rod").isTrue(); + } + + @Test + void propertiesWithDotsInKey() { + Properties p = new Properties(); + + p.setProperty("tb.(class)", TestBean.class.getName()); + p.setProperty("tb.someMap[my.key]", "my.value"); + + int count = registerBeanDefinitions(p); + assertThat(count == 1).as("1 beans registered, not " + count).isTrue(); + assertThat(lbf.getBeanDefinitionCount()).isEqualTo(1); + + TestBean tb = lbf.getBean("tb", TestBean.class); + assertThat(tb.getSomeMap().get("my.key")).isEqualTo("my.value"); + } + + @Test + void unresolvedReference() { + String PREFIX = "beans."; + Properties p = new Properties(); + + p.setProperty(PREFIX + "kerry.(class)", TestBean.class.getName()); + p.setProperty(PREFIX + "kerry.name", "Kerry"); + p.setProperty(PREFIX + "kerry.age", "35"); + p.setProperty(PREFIX + "kerry.spouse(ref)", "rod"); + + registerBeanDefinitions(p, PREFIX); + assertThatExceptionOfType(BeansException.class).as("unresolved reference").isThrownBy(() -> + lbf.getBean("kerry")); + } + + @Test + void selfReference() { + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("spouse", new RuntimeBeanReference("self")); + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.setPropertyValues(pvs); + lbf.registerBeanDefinition("self", bd); + + TestBean self = (TestBean) lbf.getBean("self"); + assertThat(self.getSpouse()).isEqualTo(self); + } + + @Test + void referenceByName() { + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("doctor", new RuntimeBeanReference("doc")); + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.setPropertyValues(pvs); + lbf.registerBeanDefinition("self", bd); + lbf.registerBeanDefinition("doc", new RootBeanDefinition(NestedTestBean.class)); + + TestBean self = (TestBean) lbf.getBean("self"); + assertThat(self.getDoctor()).isEqualTo(lbf.getBean("doc")); + } + + @Test + void referenceByType() { + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("doctor", new RuntimeBeanReference(NestedTestBean.class)); + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.setPropertyValues(pvs); + lbf.registerBeanDefinition("self", bd); + lbf.registerBeanDefinition("doc", new RootBeanDefinition(NestedTestBean.class)); + + TestBean self = (TestBean) lbf.getBean("self"); + assertThat(self.getDoctor()).isEqualTo(lbf.getBean("doc")); + } + + @Test + void referenceByAutowire() { + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("doctor", AutowiredPropertyMarker.INSTANCE); + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.setPropertyValues(pvs); + lbf.registerBeanDefinition("self", bd); + lbf.registerBeanDefinition("doc", new RootBeanDefinition(NestedTestBean.class)); + + TestBean self = (TestBean) lbf.getBean("self"); + assertThat(self.getDoctor()).isEqualTo(lbf.getBean("doc")); + } + + @Test + void arrayReferenceByName() { + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("stringArray", new RuntimeBeanReference("string")); + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.setPropertyValues(pvs); + lbf.registerBeanDefinition("self", bd); + lbf.registerSingleton("string", "A"); + + TestBean self = (TestBean) lbf.getBean("self"); + assertThat(self.getStringArray()).hasSize(1); + assertThat(self.getStringArray()).contains("A"); + } + + @Test + void arrayReferenceByType() { + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("stringArray", new RuntimeBeanReference(String.class)); + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.setPropertyValues(pvs); + lbf.registerBeanDefinition("self", bd); + lbf.registerSingleton("string", "A"); + + TestBean self = (TestBean) lbf.getBean("self"); + assertThat(self.getStringArray()).hasSize(1); + assertThat(self.getStringArray()).contains("A"); + } + + @Test + void arrayReferenceByAutowire() { + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("stringArray", AutowiredPropertyMarker.INSTANCE); + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.setPropertyValues(pvs); + lbf.registerBeanDefinition("self", bd); + lbf.registerSingleton("string1", "A"); + lbf.registerSingleton("string2", "B"); + + TestBean self = (TestBean) lbf.getBean("self"); + assertThat(self.getStringArray()).hasSize(2); + assertThat(self.getStringArray()).contains("A"); + assertThat(self.getStringArray()).contains("B"); + } + + @Test + void possibleMatches() { + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("ag", "foobar"); + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.setPropertyValues(pvs); + lbf.registerBeanDefinition("tb", bd); + + assertThatExceptionOfType(BeanCreationException.class).as("invalid property").isThrownBy(() -> + lbf.getBean("tb")) + .withCauseInstanceOf(NotWritablePropertyException.class) + .satisfies(ex -> { + NotWritablePropertyException cause = (NotWritablePropertyException) ex.getCause(); + assertThat(cause.getPossibleMatches()).hasSize(1); + assertThat(cause.getPossibleMatches()[0]).isEqualTo("age"); + }); + } + + @Test + void prototype() { + Properties p = new Properties(); + p.setProperty("kerry.(class)", TestBean.class.getName()); + p.setProperty("kerry.age", "35"); + registerBeanDefinitions(p); + TestBean kerry1 = (TestBean) lbf.getBean("kerry"); + TestBean kerry2 = (TestBean) lbf.getBean("kerry"); + assertThat(kerry1 != null).as("Non null").isTrue(); + assertThat(kerry1 == kerry2).as("Singletons equal").isTrue(); + + lbf = new DefaultListableBeanFactory(); + p = new Properties(); + p.setProperty("kerry.(class)", TestBean.class.getName()); + p.setProperty("kerry.(scope)", "prototype"); + p.setProperty("kerry.age", "35"); + registerBeanDefinitions(p); + kerry1 = (TestBean) lbf.getBean("kerry"); + kerry2 = (TestBean) lbf.getBean("kerry"); + assertThat(kerry1 != null).as("Non null").isTrue(); + assertThat(kerry1 != kerry2).as("Prototypes NOT equal").isTrue(); + + lbf = new DefaultListableBeanFactory(); + p = new Properties(); + p.setProperty("kerry.(class)", TestBean.class.getName()); + p.setProperty("kerry.(scope)", "singleton"); + p.setProperty("kerry.age", "35"); + registerBeanDefinitions(p); + kerry1 = (TestBean) lbf.getBean("kerry"); + kerry2 = (TestBean) lbf.getBean("kerry"); + assertThat(kerry1 != null).as("Non null").isTrue(); + assertThat(kerry1 == kerry2).as("Specified singletons equal").isTrue(); + } + + @Test + void prototypeCircleLeadsToException() { + Properties p = new Properties(); + p.setProperty("kerry.(class)", TestBean.class.getName()); + p.setProperty("kerry.(singleton)", "false"); + p.setProperty("kerry.age", "35"); + p.setProperty("kerry.spouse", "*rod"); + p.setProperty("rod.(class)", TestBean.class.getName()); + p.setProperty("rod.(singleton)", "false"); + p.setProperty("rod.age", "34"); + p.setProperty("rod.spouse", "*kerry"); + + registerBeanDefinitions(p); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + lbf.getBean("kerry")) + .satisfies(ex -> assertThat(ex.contains(BeanCurrentlyInCreationException.class)).isTrue()); + } + + @Test + void prototypeExtendsPrototype() { + Properties p = new Properties(); + p.setProperty("wife.(class)", TestBean.class.getName()); + p.setProperty("wife.name", "kerry"); + + p.setProperty("kerry.(parent)", "wife"); + p.setProperty("kerry.age", "35"); + registerBeanDefinitions(p); + TestBean kerry1 = (TestBean) lbf.getBean("kerry"); + TestBean kerry2 = (TestBean) lbf.getBean("kerry"); + assertThat(kerry1.getName()).isEqualTo("kerry"); + assertThat(kerry1).as("Non null").isNotNull(); + assertThat(kerry1 == kerry2).as("Singletons equal").isTrue(); + + lbf = new DefaultListableBeanFactory(); + p = new Properties(); + p.setProperty("wife.(class)", TestBean.class.getName()); + p.setProperty("wife.name", "kerry"); + p.setProperty("wife.(singleton)", "false"); + p.setProperty("kerry.(parent)", "wife"); + p.setProperty("kerry.(singleton)", "false"); + p.setProperty("kerry.age", "35"); + registerBeanDefinitions(p); + assertThat(lbf.isSingleton("kerry")).isFalse(); + kerry1 = (TestBean) lbf.getBean("kerry"); + kerry2 = (TestBean) lbf.getBean("kerry"); + assertThat(kerry1 != null).as("Non null").isTrue(); + assertThat(kerry1 != kerry2).as("Prototypes NOT equal").isTrue(); + + lbf = new DefaultListableBeanFactory(); + p = new Properties(); + p.setProperty("kerry.(class)", TestBean.class.getName()); + p.setProperty("kerry.(singleton)", "true"); + p.setProperty("kerry.age", "35"); + registerBeanDefinitions(p); + kerry1 = (TestBean) lbf.getBean("kerry"); + kerry2 = (TestBean) lbf.getBean("kerry"); + assertThat(kerry1 != null).as("Non null").isTrue(); + assertThat(kerry1 == kerry2).as("Specified singletons equal").isTrue(); + } + + @Test + void canReferenceParentBeanFromChildViaAlias() { + final String EXPECTED_NAME = "Juergen"; + final int EXPECTED_AGE = 41; + + RootBeanDefinition parentDefinition = new RootBeanDefinition(TestBean.class); + parentDefinition.setAbstract(true); + parentDefinition.getPropertyValues().add("name", EXPECTED_NAME); + parentDefinition.getPropertyValues().add("age", EXPECTED_AGE); + + ChildBeanDefinition childDefinition = new ChildBeanDefinition("alias"); + + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerBeanDefinition("parent", parentDefinition); + factory.registerBeanDefinition("child", childDefinition); + factory.registerAlias("parent", "alias"); + + TestBean child = factory.getBean("child", TestBean.class); + assertThat(child.getName()).isEqualTo(EXPECTED_NAME); + assertThat(child.getAge()).isEqualTo(EXPECTED_AGE); + BeanDefinition mergedBeanDefinition1 = factory.getMergedBeanDefinition("child"); + BeanDefinition mergedBeanDefinition2 = factory.getMergedBeanDefinition("child"); + + assertThat(mergedBeanDefinition1).as("Use cached merged bean definition").isSameAs(mergedBeanDefinition2); + } + + @Test + void getTypeWorksAfterParentChildMerging() { + RootBeanDefinition parentDefinition = new RootBeanDefinition(TestBean.class); + ChildBeanDefinition childDefinition = new ChildBeanDefinition("parent", DerivedTestBean.class, null, null); + + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerBeanDefinition("parent", parentDefinition); + factory.registerBeanDefinition("child", childDefinition); + factory.freezeConfiguration(); + + assertThat(factory.getType("parent")).isEqualTo(TestBean.class); + assertThat(factory.getType("child")).isEqualTo(DerivedTestBean.class); + } + + @Test + void nameAlreadyBound() { + Properties p = new Properties(); + p.setProperty("kerry.(class)", TestBean.class.getName()); + p.setProperty("kerry.age", "35"); + registerBeanDefinitions(p); + try { + registerBeanDefinitions(p); + } + catch (BeanDefinitionStoreException ex) { + assertThat(ex.getBeanName()).isEqualTo("kerry"); + // expected + } + } + + private void singleTestBean(ListableBeanFactory lbf) { + assertThat(lbf.getBeanDefinitionCount() == 1).as("1 beans defined").isTrue(); + String[] names = lbf.getBeanDefinitionNames(); + assertThat(names != lbf.getBeanDefinitionNames()).isTrue(); + assertThat(names.length == 1).as("Array length == 1").isTrue(); + assertThat(names[0].equals("test")).as("0th element == test").isTrue(); + TestBean tb = (TestBean) lbf.getBean("test"); + assertThat(tb != null).as("Test is non null").isTrue(); + assertThat("Tony".equals(tb.getName())).as("Test bean name is Tony").isTrue(); + assertThat(tb.getAge() == 48).as("Test bean age is 48").isTrue(); + } + + @Test + void aliasCircle() { + lbf.registerAlias("test", "test2"); + lbf.registerAlias("test2", "test3"); + assertThatIllegalStateException().isThrownBy(() -> + lbf.registerAlias("test3", "test2")); + assertThatIllegalStateException().isThrownBy(() -> + lbf.registerAlias("test3", "test")); + lbf.registerAlias("test", "test3"); + } + + @Test + void aliasChaining() { + lbf.registerBeanDefinition("test", new RootBeanDefinition(NestedTestBean.class)); + lbf.registerAlias("test", "testAlias"); + lbf.registerAlias("testAlias", "testAlias2"); + lbf.registerAlias("testAlias2", "testAlias3"); + Object bean = lbf.getBean("test"); + assertThat(lbf.getBean("testAlias")).isSameAs(bean); + assertThat(lbf.getBean("testAlias2")).isSameAs(bean); + assertThat(lbf.getBean("testAlias3")).isSameAs(bean); + } + + @Test + void beanDefinitionOverriding() { + lbf.registerBeanDefinition("test", new RootBeanDefinition(TestBean.class)); + lbf.registerBeanDefinition("test", new RootBeanDefinition(NestedTestBean.class)); + lbf.registerAlias("otherTest", "test2"); + lbf.registerAlias("test", "test2"); + assertThat(lbf.getBean("test")).isInstanceOf(NestedTestBean.class); + assertThat(lbf.getBean("test2")).isInstanceOf(NestedTestBean.class); + } + + @Test + void beanDefinitionOverridingNotAllowed() { + lbf.setAllowBeanDefinitionOverriding(false); + BeanDefinition oldDef = new RootBeanDefinition(TestBean.class); + BeanDefinition newDef = new RootBeanDefinition(NestedTestBean.class); + lbf.registerBeanDefinition("test", oldDef); + assertThatExceptionOfType(BeanDefinitionOverrideException.class).isThrownBy(() -> + lbf.registerBeanDefinition("test", newDef)) + .satisfies(ex -> { + assertThat(ex.getBeanName()).isEqualTo("test"); + assertThat(ex.getBeanDefinition()).isEqualTo(newDef); + assertThat(ex.getExistingDefinition()).isEqualTo(oldDef); + }); + } + + @Test + void beanDefinitionOverridingWithAlias() { + lbf.registerBeanDefinition("test", new RootBeanDefinition(TestBean.class)); + lbf.registerAlias("test", "testAlias"); + lbf.registerBeanDefinition("test", new RootBeanDefinition(NestedTestBean.class)); + lbf.registerAlias("test", "testAlias"); + assertThat(lbf.getBean("test")).isInstanceOf(NestedTestBean.class); + assertThat(lbf.getBean("testAlias")).isInstanceOf(NestedTestBean.class); + } + + @Test + void beanDefinitionOverridingWithConstructorArgumentMismatch() { + RootBeanDefinition bd1 = new RootBeanDefinition(NestedTestBean.class); + bd1.getConstructorArgumentValues().addIndexedArgumentValue(1, "value1"); + lbf.registerBeanDefinition("test", bd1); + RootBeanDefinition bd2 = new RootBeanDefinition(NestedTestBean.class); + bd2.getConstructorArgumentValues().addIndexedArgumentValue(0, "value0"); + lbf.registerBeanDefinition("test", bd2); + assertThat(lbf.getBean("test")).isInstanceOf(NestedTestBean.class); + assertThat(lbf.getBean("test", NestedTestBean.class).getCompany()).isEqualTo("value0"); + } + + @Test + void beanDefinitionRemoval() { + lbf.setAllowBeanDefinitionOverriding(false); + lbf.registerBeanDefinition("test", new RootBeanDefinition(TestBean.class)); + lbf.registerAlias("test", "test2"); + lbf.preInstantiateSingletons(); + assertThat(lbf.getBean("test")).isInstanceOf(TestBean.class); + assertThat(lbf.getBean("test2")).isInstanceOf(TestBean.class); + lbf.removeBeanDefinition("test"); + lbf.removeAlias("test2"); + lbf.registerBeanDefinition("test", new RootBeanDefinition(NestedTestBean.class)); + lbf.registerAlias("test", "test2"); + assertThat(lbf.getBean("test")).isInstanceOf(NestedTestBean.class); + assertThat(lbf.getBean("test2")).isInstanceOf(NestedTestBean.class); + } + + @Test // gh-23542 + void concurrentBeanDefinitionRemoval() { + final int MAX = 200; + lbf.setAllowBeanDefinitionOverriding(false); + + // Register the bean definitions before invoking preInstantiateSingletons() + // to simulate realistic usage of an ApplicationContext; otherwise, the bean + // factory thinks it's an "empty" factory which causes this test to fail in + // an unrealistic manner. + IntStream.range(0, MAX).forEach(this::registerTestBean); + lbf.preInstantiateSingletons(); + + // This test is considered successful if the following does not result in an exception. + IntStream.range(0, MAX).parallel().forEach(this::removeTestBean); + } + + private void registerTestBean(int i) { + String name = "test" + i; + lbf.registerBeanDefinition(name, new RootBeanDefinition(TestBean.class)); + } + + private void removeTestBean(int i) { + String name = "test" + i; + lbf.removeBeanDefinition(name); + } + + @Test + void beanReferenceWithNewSyntax() { + Properties p = new Properties(); + p.setProperty("r.(class)", TestBean.class.getName()); + p.setProperty("r.name", "rod"); + p.setProperty("k.(class)", TestBean.class.getName()); + p.setProperty("k.name", "kerry"); + p.setProperty("k.spouse", "*r"); + registerBeanDefinitions(p); + TestBean k = (TestBean) lbf.getBean("k"); + TestBean r = (TestBean) lbf.getBean("r"); + assertThat(k.getSpouse() == r).isTrue(); + } + + @Test + void canEscapeBeanReferenceSyntax() { + String name = "*name"; + Properties p = new Properties(); + p.setProperty("r.(class)", TestBean.class.getName()); + p.setProperty("r.name", "*" + name); + registerBeanDefinitions(p); + TestBean r = (TestBean) lbf.getBean("r"); + assertThat(r.getName().equals(name)).isTrue(); + } + + @Test + void customEditor() { + lbf.addPropertyEditorRegistrar(registry -> { + NumberFormat nf = NumberFormat.getInstance(Locale.GERMAN); + registry.registerCustomEditor(Float.class, new CustomNumberEditor(Float.class, nf, true)); + }); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("myFloat", "1,1"); + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.setPropertyValues(pvs); + lbf.registerBeanDefinition("testBean", bd); + TestBean testBean = (TestBean) lbf.getBean("testBean"); + assertThat(testBean.getMyFloat().floatValue() == 1.1f).isTrue(); + } + + @Test + void customConverter() { + GenericConversionService conversionService = new DefaultConversionService(); + conversionService.addConverter(new Converter() { + @Override + public Float convert(String source) { + try { + NumberFormat nf = NumberFormat.getInstance(Locale.GERMAN); + return nf.parse(source).floatValue(); + } + catch (ParseException ex) { + throw new IllegalArgumentException(ex); + } + } + }); + lbf.setConversionService(conversionService); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("myFloat", "1,1"); + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.setPropertyValues(pvs); + lbf.registerBeanDefinition("testBean", bd); + TestBean testBean = (TestBean) lbf.getBean("testBean"); + assertThat(testBean.getMyFloat().floatValue() == 1.1f).isTrue(); + } + + @Test + void customEditorWithBeanReference() { + lbf.addPropertyEditorRegistrar(new PropertyEditorRegistrar() { + @Override + public void registerCustomEditors(PropertyEditorRegistry registry) { + NumberFormat nf = NumberFormat.getInstance(Locale.GERMAN); + registry.registerCustomEditor(Float.class, new CustomNumberEditor(Float.class, nf, true)); + } + }); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("myFloat", new RuntimeBeanReference("myFloat")); + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.setPropertyValues(pvs); + lbf.registerBeanDefinition("testBean", bd); + lbf.registerSingleton("myFloat", "1,1"); + TestBean testBean = (TestBean) lbf.getBean("testBean"); + assertThat(testBean.getMyFloat().floatValue() == 1.1f).isTrue(); + } + + @Test + void customTypeConverter() { + NumberFormat nf = NumberFormat.getInstance(Locale.GERMAN); + lbf.setTypeConverter(new CustomTypeConverter(nf)); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("myFloat", "1,1"); + ConstructorArgumentValues cav = new ConstructorArgumentValues(); + cav.addIndexedArgumentValue(0, "myName"); + cav.addIndexedArgumentValue(1, "myAge"); + lbf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class, cav, pvs)); + TestBean testBean = (TestBean) lbf.getBean("testBean"); + assertThat(testBean.getName()).isEqualTo("myName"); + assertThat(testBean.getAge()).isEqualTo(5); + assertThat(testBean.getMyFloat().floatValue() == 1.1f).isTrue(); + } + + @Test + void customTypeConverterWithBeanReference() { + NumberFormat nf = NumberFormat.getInstance(Locale.GERMAN); + lbf.setTypeConverter(new CustomTypeConverter(nf)); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("myFloat", new RuntimeBeanReference("myFloat")); + ConstructorArgumentValues cav = new ConstructorArgumentValues(); + cav.addIndexedArgumentValue(0, "myName"); + cav.addIndexedArgumentValue(1, "myAge"); + lbf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class, cav, pvs)); + lbf.registerSingleton("myFloat", "1,1"); + TestBean testBean = (TestBean) lbf.getBean("testBean"); + assertThat(testBean.getName()).isEqualTo("myName"); + assertThat(testBean.getAge()).isEqualTo(5); + assertThat(testBean.getMyFloat().floatValue() == 1.1f).isTrue(); + } + + @Test + void registerExistingSingletonWithReference() { + Properties p = new Properties(); + p.setProperty("test.(class)", TestBean.class.getName()); + p.setProperty("test.name", "Tony"); + p.setProperty("test.age", "48"); + p.setProperty("test.spouse(ref)", "singletonObject"); + registerBeanDefinitions(p); + Object singletonObject = new TestBean(); + lbf.registerSingleton("singletonObject", singletonObject); + + assertThat(lbf.isSingleton("singletonObject")).isTrue(); + assertThat(lbf.getType("singletonObject")).isEqualTo(TestBean.class); + TestBean test = (TestBean) lbf.getBean("test"); + assertThat(lbf.getBean("singletonObject")).isEqualTo(singletonObject); + assertThat(test.getSpouse()).isEqualTo(singletonObject); + + Map beansOfType = lbf.getBeansOfType(TestBean.class, false, true); + assertThat(beansOfType.size()).isEqualTo(2); + assertThat(beansOfType.containsValue(test)).isTrue(); + assertThat(beansOfType.containsValue(singletonObject)).isTrue(); + + beansOfType = lbf.getBeansOfType(null, false, true); + assertThat(beansOfType.size()).isEqualTo(2); + + Iterator beanNames = lbf.getBeanNamesIterator(); + assertThat(beanNames.next()).isEqualTo("test"); + assertThat(beanNames.next()).isEqualTo("singletonObject"); + assertThat(beanNames.hasNext()).isFalse(); + + assertThat(lbf.containsSingleton("test")).isTrue(); + assertThat(lbf.containsSingleton("singletonObject")).isTrue(); + assertThat(lbf.containsBeanDefinition("test")).isTrue(); + assertThat(lbf.containsBeanDefinition("singletonObject")).isFalse(); + } + + @Test + void registerExistingSingletonWithNameOverriding() { + Properties p = new Properties(); + p.setProperty("test.(class)", TestBean.class.getName()); + p.setProperty("test.name", "Tony"); + p.setProperty("test.age", "48"); + p.setProperty("test.spouse(ref)", "singletonObject"); + registerBeanDefinitions(p); + lbf.registerBeanDefinition("singletonObject", new RootBeanDefinition(PropertiesFactoryBean.class)); + Object singletonObject = new TestBean(); + lbf.registerSingleton("singletonObject", singletonObject); + lbf.preInstantiateSingletons(); + + assertThat(lbf.isSingleton("singletonObject")).isTrue(); + assertThat(lbf.getType("singletonObject")).isEqualTo(TestBean.class); + TestBean test = (TestBean) lbf.getBean("test"); + assertThat(lbf.getBean("singletonObject")).isEqualTo(singletonObject); + assertThat(test.getSpouse()).isEqualTo(singletonObject); + + Map beansOfType = lbf.getBeansOfType(TestBean.class, false, true); + assertThat(beansOfType.size()).isEqualTo(2); + assertThat(beansOfType.containsValue(test)).isTrue(); + assertThat(beansOfType.containsValue(singletonObject)).isTrue(); + + beansOfType = lbf.getBeansOfType(null, false, true); + + Iterator beanNames = lbf.getBeanNamesIterator(); + assertThat(beanNames.next()).isEqualTo("test"); + assertThat(beanNames.next()).isEqualTo("singletonObject"); + assertThat(beanNames.hasNext()).isFalse(); + assertThat(beansOfType.size()).isEqualTo(2); + + assertThat(lbf.containsSingleton("test")).isTrue(); + assertThat(lbf.containsSingleton("singletonObject")).isTrue(); + assertThat(lbf.containsBeanDefinition("test")).isTrue(); + assertThat(lbf.containsBeanDefinition("singletonObject")).isTrue(); + } + + @Test + void registerExistingSingletonWithAutowire() { + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "Tony"); + pvs.add("age", "48"); + RootBeanDefinition bd = new RootBeanDefinition(DependenciesBean.class); + bd.setPropertyValues(pvs); + bd.setDependencyCheck(RootBeanDefinition.DEPENDENCY_CHECK_OBJECTS); + bd.setAutowireMode(RootBeanDefinition.AUTOWIRE_BY_TYPE); + lbf.registerBeanDefinition("test", bd); + Object singletonObject = new TestBean(); + lbf.registerSingleton("singletonObject", singletonObject); + + assertThat(lbf.containsBean("singletonObject")).isTrue(); + assertThat(lbf.isSingleton("singletonObject")).isTrue(); + assertThat(lbf.getType("singletonObject")).isEqualTo(TestBean.class); + assertThat(lbf.getAliases("singletonObject").length).isEqualTo(0); + DependenciesBean test = (DependenciesBean) lbf.getBean("test"); + assertThat(lbf.getBean("singletonObject")).isEqualTo(singletonObject); + assertThat(test.getSpouse()).isEqualTo(singletonObject); + } + + @Test + void registerExistingSingletonWithAlreadyBound() { + Object singletonObject = new TestBean(); + lbf.registerSingleton("singletonObject", singletonObject); + assertThatIllegalStateException().isThrownBy(() -> + lbf.registerSingleton("singletonObject", singletonObject)); + } + + @Test + void reregisterBeanDefinition() { + RootBeanDefinition bd1 = new RootBeanDefinition(TestBean.class); + bd1.setScope(BeanDefinition.SCOPE_PROTOTYPE); + lbf.registerBeanDefinition("testBean", bd1); + assertThat(lbf.getBean("testBean")).isInstanceOf(TestBean.class); + RootBeanDefinition bd2 = new RootBeanDefinition(NestedTestBean.class); + bd2.setScope(BeanDefinition.SCOPE_PROTOTYPE); + lbf.registerBeanDefinition("testBean", bd2); + assertThat(lbf.getBean("testBean")).isInstanceOf(NestedTestBean.class); + } + + @Test + void arrayPropertyWithAutowiring() throws MalformedURLException { + lbf.registerSingleton("resource1", new UrlResource("http://localhost:8080")); + lbf.registerSingleton("resource2", new UrlResource("http://localhost:9090")); + + RootBeanDefinition rbd = new RootBeanDefinition(ArrayBean.class); + rbd.setAutowireMode(RootBeanDefinition.AUTOWIRE_BY_TYPE); + lbf.registerBeanDefinition("arrayBean", rbd); + ArrayBean ab = (ArrayBean) lbf.getBean("arrayBean"); + + assertThat(ab.getResourceArray()[0]).isEqualTo(new UrlResource("http://localhost:8080")); + assertThat(ab.getResourceArray()[1]).isEqualTo(new UrlResource("http://localhost:9090")); + } + + @Test + void arrayPropertyWithOptionalAutowiring() throws MalformedURLException { + RootBeanDefinition rbd = new RootBeanDefinition(ArrayBean.class); + rbd.setAutowireMode(RootBeanDefinition.AUTOWIRE_BY_TYPE); + lbf.registerBeanDefinition("arrayBean", rbd); + ArrayBean ab = (ArrayBean) lbf.getBean("arrayBean"); + + assertThat(ab.getResourceArray()).isNull(); + } + + @Test + void arrayConstructorWithAutowiring() { + lbf.registerSingleton("integer1",4); + lbf.registerSingleton("integer2", 5); + + RootBeanDefinition rbd = new RootBeanDefinition(ArrayBean.class); + rbd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); + lbf.registerBeanDefinition("arrayBean", rbd); + ArrayBean ab = (ArrayBean) lbf.getBean("arrayBean"); + + assertThat(ab.getIntegerArray()[0]).isEqualTo(4); + assertThat(ab.getIntegerArray()[1]).isEqualTo(5); + } + + @Test + void arrayConstructorWithOptionalAutowiring() { + RootBeanDefinition rbd = new RootBeanDefinition(ArrayBean.class); + rbd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); + lbf.registerBeanDefinition("arrayBean", rbd); + ArrayBean ab = (ArrayBean) lbf.getBean("arrayBean"); + + assertThat(ab.getIntegerArray()).isNull(); + } + + @Test + void doubleArrayConstructorWithAutowiring() throws MalformedURLException { + lbf.registerSingleton("integer1", 4); + lbf.registerSingleton("integer2", 5); + lbf.registerSingleton("resource1", new UrlResource("http://localhost:8080")); + lbf.registerSingleton("resource2", new UrlResource("http://localhost:9090")); + + RootBeanDefinition rbd = new RootBeanDefinition(ArrayBean.class); + rbd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); + lbf.registerBeanDefinition("arrayBean", rbd); + ArrayBean ab = (ArrayBean) lbf.getBean("arrayBean"); + + assertThat(ab.getIntegerArray()[0]).isEqualTo(4); + assertThat(ab.getIntegerArray()[1]).isEqualTo(5); + assertThat(ab.getResourceArray()[0]).isEqualTo(new UrlResource("http://localhost:8080")); + assertThat(ab.getResourceArray()[1]).isEqualTo(new UrlResource("http://localhost:9090")); + } + + @Test + void doubleArrayConstructorWithOptionalAutowiring() throws MalformedURLException { + lbf.registerSingleton("resource1", new UrlResource("http://localhost:8080")); + lbf.registerSingleton("resource2", new UrlResource("http://localhost:9090")); + + RootBeanDefinition rbd = new RootBeanDefinition(ArrayBean.class); + rbd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); + lbf.registerBeanDefinition("arrayBean", rbd); + ArrayBean ab = (ArrayBean) lbf.getBean("arrayBean"); + + assertThat(ab.getIntegerArray()).isNull(); + assertThat(ab.getResourceArray()).isNull(); + } + + @Test + void expressionInStringArray() { + BeanExpressionResolver beanExpressionResolver = mock(BeanExpressionResolver.class); + given(beanExpressionResolver.evaluate(eq("#{foo}"), any(BeanExpressionContext.class))) + .willReturn("classpath:/org/springframework/beans/factory/xml/util.properties"); + lbf.setBeanExpressionResolver(beanExpressionResolver); + + RootBeanDefinition rbd = new RootBeanDefinition(PropertiesFactoryBean.class); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("locations", new String[]{"#{foo}"}); + rbd.setPropertyValues(pvs); + lbf.registerBeanDefinition("myProperties", rbd); + Properties properties = (Properties) lbf.getBean("myProperties"); + assertThat(properties.getProperty("foo")).isEqualTo("bar"); + } + + @Test + void autowireWithNoDependencies() { + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + lbf.registerBeanDefinition("rod", bd); + assertThat(lbf.getBeanDefinitionCount()).isEqualTo(1); + Object registered = lbf.autowire(NoDependencies.class, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, false); + assertThat(lbf.getBeanDefinitionCount()).isEqualTo(1); + assertThat(registered instanceof NoDependencies).isTrue(); + } + + @Test + void autowireWithSatisfiedJavaBeanDependency() { + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "Rod"); + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.setPropertyValues(pvs); + lbf.registerBeanDefinition("rod", bd); + assertThat(lbf.getBeanDefinitionCount()).isEqualTo(1); + // Depends on age, name and spouse (TestBean) + Object registered = lbf.autowire(DependenciesBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true); + assertThat(lbf.getBeanDefinitionCount()).isEqualTo(1); + DependenciesBean kerry = (DependenciesBean) registered; + TestBean rod = (TestBean) lbf.getBean("rod"); + assertThat(kerry.getSpouse()).isSameAs(rod); + } + + @Test + void autowireWithSatisfiedConstructorDependency() { + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "Rod"); + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.setPropertyValues(pvs); + lbf.registerBeanDefinition("rod", bd); + assertThat(lbf.getBeanDefinitionCount()).isEqualTo(1); + Object registered = lbf.autowire(ConstructorDependency.class, AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false); + assertThat(lbf.getBeanDefinitionCount()).isEqualTo(1); + ConstructorDependency kerry = (ConstructorDependency) registered; + TestBean rod = (TestBean) lbf.getBean("rod"); + assertThat(kerry.spouse).isSameAs(rod); + } + + @Test + void autowireWithTwoMatchesForConstructorDependency() { + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + lbf.registerBeanDefinition("rod", bd); + RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); + lbf.registerBeanDefinition("rod2", bd2); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> + lbf.autowire(ConstructorDependency.class, AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false)) + .withMessageContaining("rod") + .withMessageContaining("rod2"); + } + + @Test + void autowireWithUnsatisfiedConstructorDependency() { + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.addPropertyValue(new PropertyValue("name", "Rod")); + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.setPropertyValues(pvs); + lbf.registerBeanDefinition("rod", bd); + assertThat(lbf.getBeanDefinitionCount()).isEqualTo(1); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> + lbf.autowire(UnsatisfiedConstructorDependency.class, AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, true)); + } + + @Test + void autowireConstructor() { + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + lbf.registerBeanDefinition("spouse", bd); + ConstructorDependenciesBean bean = (ConstructorDependenciesBean) + lbf.autowire(ConstructorDependenciesBean.class, AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, true); + Object spouse = lbf.getBean("spouse"); + assertThat(bean.getSpouse1() == spouse).isTrue(); + assertThat(BeanFactoryUtils.beanOfType(lbf, TestBean.class) == spouse).isTrue(); + } + + @Test + void autowireBeanByName() { + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + lbf.registerBeanDefinition("spouse", bd); + DependenciesBean bean = (DependenciesBean) + lbf.autowire(DependenciesBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_NAME, true); + TestBean spouse = (TestBean) lbf.getBean("spouse"); + assertThat(bean.getSpouse()).isEqualTo(spouse); + assertThat(BeanFactoryUtils.beanOfType(lbf, TestBean.class) == spouse).isTrue(); + } + + @Test + void autowireBeanByNameWithDependencyCheck() { + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + lbf.registerBeanDefinition("spous", bd); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> + lbf.autowire(DependenciesBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_NAME, true)); + } + + @Test + void autowireBeanByNameWithNoDependencyCheck() { + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + lbf.registerBeanDefinition("spous", bd); + DependenciesBean bean = (DependenciesBean) + lbf.autowire(DependenciesBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_NAME, false); + assertThat(bean.getSpouse()).isNull(); + } + + @Test + void dependsOnCycle() { + RootBeanDefinition bd1 = new RootBeanDefinition(TestBean.class); + bd1.setDependsOn("tb2"); + lbf.registerBeanDefinition("tb1", bd1); + RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); + bd2.setDependsOn("tb1"); + lbf.registerBeanDefinition("tb2", bd2); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + lbf.preInstantiateSingletons()) + .withMessageContaining("Circular") + .withMessageContaining("'tb2'") + .withMessageContaining("'tb1'"); + } + + @Test + void implicitDependsOnCycle() { + RootBeanDefinition bd1 = new RootBeanDefinition(TestBean.class); + bd1.setDependsOn("tb2"); + lbf.registerBeanDefinition("tb1", bd1); + RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); + bd2.setDependsOn("tb3"); + lbf.registerBeanDefinition("tb2", bd2); + RootBeanDefinition bd3 = new RootBeanDefinition(TestBean.class); + bd3.setDependsOn("tb1"); + lbf.registerBeanDefinition("tb3", bd3); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy( + lbf::preInstantiateSingletons) + .withMessageContaining("Circular") + .withMessageContaining("'tb3'") + .withMessageContaining("'tb1'"); + } + + @Test + void getBeanByTypeWithNoneFound() { + DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> + lbf.getBean(TestBean.class)); + } + + @Test + void getBeanByTypeWithLateRegistration() { + DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> + lbf.getBean(TestBean.class)); + RootBeanDefinition bd1 = new RootBeanDefinition(TestBean.class); + lbf.registerBeanDefinition("bd1", bd1); + TestBean bean = lbf.getBean(TestBean.class); + assertThat(bean.getBeanName()).isEqualTo("bd1"); + } + + @Test + void getBeanByTypeWithLateRegistrationAgainstFrozen() { + DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + lbf.freezeConfiguration(); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> + lbf.getBean(TestBean.class)); + RootBeanDefinition bd1 = new RootBeanDefinition(TestBean.class); + lbf.registerBeanDefinition("bd1", bd1); + TestBean bean = lbf.getBean(TestBean.class); + assertThat(bean.getBeanName()).isEqualTo("bd1"); + } + + @Test + void getBeanByTypeDefinedInParent() { + DefaultListableBeanFactory parent = new DefaultListableBeanFactory(); + RootBeanDefinition bd1 = new RootBeanDefinition(TestBean.class); + parent.registerBeanDefinition("bd1", bd1); + DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(parent); + TestBean bean = lbf.getBean(TestBean.class); + assertThat(bean.getBeanName()).isEqualTo("bd1"); + } + + @Test + void getBeanByTypeWithAmbiguity() { + RootBeanDefinition bd1 = new RootBeanDefinition(TestBean.class); + RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); + lbf.registerBeanDefinition("bd1", bd1); + lbf.registerBeanDefinition("bd2", bd2); + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(() -> + lbf.getBean(TestBean.class)); + } + + @Test + void getBeanByTypeWithPrimary() { + RootBeanDefinition bd1 = new RootBeanDefinition(TestBean.class); + bd1.setLazyInit(true); + RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); + bd2.setPrimary(true); + lbf.registerBeanDefinition("bd1", bd1); + lbf.registerBeanDefinition("bd2", bd2); + TestBean bean = lbf.getBean(TestBean.class); + assertThat(bean.getBeanName()).isEqualTo("bd2"); + assertThat(lbf.containsSingleton("bd1")).isFalse(); + } + + @Test + @SuppressWarnings("rawtypes") + void getFactoryBeanByTypeWithPrimary() { + DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + RootBeanDefinition bd1 = new RootBeanDefinition(NullTestBeanFactoryBean.class); + RootBeanDefinition bd2 = new RootBeanDefinition(NullTestBeanFactoryBean.class); + bd2.setPrimary(true); + lbf.registerBeanDefinition("bd1", bd1); + lbf.registerBeanDefinition("bd2", bd2); + NullTestBeanFactoryBean factoryBeanByType = lbf.getBean(NullTestBeanFactoryBean.class); + NullTestBeanFactoryBean bd1FactoryBean = (NullTestBeanFactoryBean)lbf.getBean("&bd1"); + NullTestBeanFactoryBean bd2FactoryBean = (NullTestBeanFactoryBean)lbf.getBean("&bd2"); + assertThat(factoryBeanByType).isNotNull(); + assertThat(bd1FactoryBean).isNotNull(); + assertThat(bd2FactoryBean).isNotNull(); + assertThat(bd1FactoryBean).isNotEqualTo(factoryBeanByType); + assertThat(bd2FactoryBean).isEqualTo(factoryBeanByType); + } + + @Test + void getBeanByTypeWithMultiplePrimary() { + RootBeanDefinition bd1 = new RootBeanDefinition(TestBean.class); + bd1.setPrimary(true); + RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); + bd2.setPrimary(true); + lbf.registerBeanDefinition("bd1", bd1); + lbf.registerBeanDefinition("bd2", bd2); + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(() -> + lbf.getBean(TestBean.class)) + .withMessageContaining("more than one 'primary'"); + } + + @Test + void getBeanByTypeWithPriority() { + lbf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); + RootBeanDefinition bd1 = new RootBeanDefinition(HighPriorityTestBean.class); + RootBeanDefinition bd2 = new RootBeanDefinition(LowPriorityTestBean.class); + RootBeanDefinition bd3 = new RootBeanDefinition(NullTestBeanFactoryBean.class); + lbf.registerBeanDefinition("bd1", bd1); + lbf.registerBeanDefinition("bd2", bd2); + lbf.registerBeanDefinition("bd3", bd3); + lbf.preInstantiateSingletons(); + TestBean bean = lbf.getBean(TestBean.class); + assertThat(bean.getBeanName()).isEqualTo("bd1"); + } + + @Test + void mapInjectionWithPriority() { + lbf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); + RootBeanDefinition bd1 = new RootBeanDefinition(HighPriorityTestBean.class); + RootBeanDefinition bd2 = new RootBeanDefinition(LowPriorityTestBean.class); + RootBeanDefinition bd3 = new RootBeanDefinition(NullTestBeanFactoryBean.class); + RootBeanDefinition bd4 = new RootBeanDefinition(TestBeanRecipient.class, RootBeanDefinition.AUTOWIRE_CONSTRUCTOR, false); + lbf.registerBeanDefinition("bd1", bd1); + lbf.registerBeanDefinition("bd2", bd2); + lbf.registerBeanDefinition("bd3", bd3); + lbf.registerBeanDefinition("bd4", bd4); + lbf.preInstantiateSingletons(); + TestBean bean = lbf.getBean(TestBeanRecipient.class).testBean; + assertThat(bean.getBeanName()).isEqualTo("bd1"); + } + + @Test + void getBeanByTypeWithMultiplePriority() { + lbf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); + RootBeanDefinition bd1 = new RootBeanDefinition(HighPriorityTestBean.class); + RootBeanDefinition bd2 = new RootBeanDefinition(HighPriorityTestBean.class); + lbf.registerBeanDefinition("bd1", bd1); + lbf.registerBeanDefinition("bd2", bd2); + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(() -> + lbf.getBean(TestBean.class)) + .withMessageContaining("Multiple beans found with the same priority") + .withMessageContaining("5"); // conflicting priority + } + + @Test + void getBeanByTypeWithPriorityAndNullInstance() { + lbf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); + RootBeanDefinition bd1 = new RootBeanDefinition(HighPriorityTestBean.class); + RootBeanDefinition bd2 = new RootBeanDefinition(NullTestBeanFactoryBean.class); + lbf.registerBeanDefinition("bd1", bd1); + lbf.registerBeanDefinition("bd2", bd2); + TestBean bean = lbf.getBean(TestBean.class); + assertThat(bean.getBeanName()).isEqualTo("bd1"); + } + + @Test + void getBeanByTypePrimaryHasPrecedenceOverPriority() { + lbf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); + RootBeanDefinition bd1 = new RootBeanDefinition(HighPriorityTestBean.class); + RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); + bd2.setPrimary(true); + lbf.registerBeanDefinition("bd1", bd1); + lbf.registerBeanDefinition("bd2", bd2); + TestBean bean = lbf.getBean(TestBean.class); + assertThat(bean.getBeanName()).isEqualTo("bd2"); + } + + @Test + void getBeanByTypeFiltersOutNonAutowireCandidates() { + RootBeanDefinition bd1 = new RootBeanDefinition(TestBean.class); + RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); + RootBeanDefinition na1 = new RootBeanDefinition(TestBean.class); + na1.setAutowireCandidate(false); + + lbf.registerBeanDefinition("bd1", bd1); + lbf.registerBeanDefinition("na1", na1); + TestBean actual = lbf.getBean(TestBean.class); // na1 was filtered + assertThat(actual).isSameAs(lbf.getBean("bd1", TestBean.class)); + + lbf.registerBeanDefinition("bd2", bd2); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> + lbf.getBean(TestBean.class)); + } + + @Test + void getBeanByTypeWithNullRequiredType() { + assertThatIllegalArgumentException().isThrownBy(() -> lbf.getBean((Class) null)); + } + + @Test + void getBeanProviderByTypeWithNullRequiredType() { + assertThatIllegalArgumentException().isThrownBy(() -> lbf.getBeanProvider((Class) null)); + } + + @Test + void resolveNamedBeanByTypeWithNullRequiredType() { + assertThatIllegalArgumentException().isThrownBy(() -> lbf.resolveNamedBean((Class) null)); + } + + @Test + void getBeanByTypeInstanceWithNoneFound() { + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> + lbf.getBean(ConstructorDependency.class)); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> + lbf.getBean(ConstructorDependency.class, 42)); + + ObjectProvider provider = lbf.getBeanProvider(ConstructorDependency.class); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy( + provider::getObject); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> + provider.getObject(42)); + assertThat(provider.getIfAvailable()).isNull(); + assertThat(provider.getIfUnique()).isNull(); + } + + @Test + void getBeanByTypeInstanceDefinedInParent() { + DefaultListableBeanFactory parent = new DefaultListableBeanFactory(); + RootBeanDefinition bd1 = createConstructorDependencyBeanDefinition(99); + parent.registerBeanDefinition("bd1", bd1); + DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(parent); + + ConstructorDependency bean = lbf.getBean(ConstructorDependency.class); + assertThat(bean.beanName).isEqualTo("bd1"); + assertThat(bean.spouseAge).isEqualTo(99); + bean = lbf.getBean(ConstructorDependency.class, 42); + assertThat(bean.beanName).isEqualTo("bd1"); + assertThat(bean.spouseAge).isEqualTo(42); + + ObjectProvider provider = lbf.getBeanProvider(ConstructorDependency.class); + bean = provider.getObject(); + assertThat(bean.beanName).isEqualTo("bd1"); + assertThat(bean.spouseAge).isEqualTo(99); + bean = provider.getObject(42); + assertThat(bean.beanName).isEqualTo("bd1"); + assertThat(bean.spouseAge).isEqualTo(42); + bean = provider.getIfAvailable(); + assertThat(bean.beanName).isEqualTo("bd1"); + assertThat(bean.spouseAge).isEqualTo(99); + bean = provider.getIfUnique(); + assertThat(bean.beanName).isEqualTo("bd1"); + assertThat(bean.spouseAge).isEqualTo(99); + } + + @Test + void getBeanByTypeInstanceWithAmbiguity() { + RootBeanDefinition bd1 = createConstructorDependencyBeanDefinition(99); + RootBeanDefinition bd2 = new RootBeanDefinition(ConstructorDependency.class); + bd2.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bd2.getConstructorArgumentValues().addGenericArgumentValue("43"); + lbf.registerBeanDefinition("bd1", bd1); + lbf.registerBeanDefinition("bd2", bd2); + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(() -> + lbf.getBean(ConstructorDependency.class)); + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(() -> + lbf.getBean(ConstructorDependency.class, 42)); + ObjectProvider provider = lbf.getBeanProvider(ConstructorDependency.class); + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy( + provider::getObject); + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(() -> + provider.getObject(42)); + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy( + provider::getIfAvailable); + assertThat(provider.getIfUnique()).isNull(); + + Set resolved = new HashSet<>(); + for (ConstructorDependency instance : provider) { + resolved.add(instance); + } + assertThat(resolved.size()).isEqualTo(2); + assertThat(resolved.contains(lbf.getBean("bd1"))).isTrue(); + assertThat(resolved.contains(lbf.getBean("bd2"))).isTrue(); + + resolved = new HashSet<>(); + provider.forEach(resolved::add); + assertThat(resolved.size()).isEqualTo(2); + assertThat(resolved.contains(lbf.getBean("bd1"))).isTrue(); + assertThat(resolved.contains(lbf.getBean("bd2"))).isTrue(); + + resolved = provider.stream().collect(Collectors.toSet()); + assertThat(resolved.size()).isEqualTo(2); + assertThat(resolved.contains(lbf.getBean("bd1"))).isTrue(); + assertThat(resolved.contains(lbf.getBean("bd2"))).isTrue(); + } + + @Test + void getBeanByTypeInstanceWithPrimary() { + RootBeanDefinition bd1 = createConstructorDependencyBeanDefinition(99); + RootBeanDefinition bd2 = createConstructorDependencyBeanDefinition(43); + bd2.setPrimary(true); + lbf.registerBeanDefinition("bd1", bd1); + lbf.registerBeanDefinition("bd2", bd2); + + ConstructorDependency bean = lbf.getBean(ConstructorDependency.class); + assertThat(bean.beanName).isEqualTo("bd2"); + assertThat(bean.spouseAge).isEqualTo(43); + bean = lbf.getBean(ConstructorDependency.class, 42); + assertThat(bean.beanName).isEqualTo("bd2"); + assertThat(bean.spouseAge).isEqualTo(42); + + ObjectProvider provider = lbf.getBeanProvider(ConstructorDependency.class); + bean = provider.getObject(); + assertThat(bean.beanName).isEqualTo("bd2"); + assertThat(bean.spouseAge).isEqualTo(43); + bean = provider.getObject(42); + assertThat(bean.beanName).isEqualTo("bd2"); + assertThat(bean.spouseAge).isEqualTo(42); + bean = provider.getIfAvailable(); + assertThat(bean.beanName).isEqualTo("bd2"); + assertThat(bean.spouseAge).isEqualTo(43); + bean = provider.getIfUnique(); + assertThat(bean.beanName).isEqualTo("bd2"); + assertThat(bean.spouseAge).isEqualTo(43); + + Set resolved = new HashSet<>(); + for (ConstructorDependency instance : provider) { + resolved.add(instance); + } + assertThat(resolved.size()).isEqualTo(2); + assertThat(resolved.contains(lbf.getBean("bd1"))).isTrue(); + assertThat(resolved.contains(lbf.getBean("bd2"))).isTrue(); + + resolved = new HashSet<>(); + provider.forEach(resolved::add); + assertThat(resolved.size()).isEqualTo(2); + assertThat(resolved.contains(lbf.getBean("bd1"))).isTrue(); + assertThat(resolved.contains(lbf.getBean("bd2"))).isTrue(); + + resolved = provider.stream().collect(Collectors.toSet()); + assertThat(resolved.size()).isEqualTo(2); + assertThat(resolved.contains(lbf.getBean("bd1"))).isTrue(); + assertThat(resolved.contains(lbf.getBean("bd2"))).isTrue(); + } + + @Test + void getBeanByTypeInstanceWithMultiplePrimary() { + RootBeanDefinition bd1 = createConstructorDependencyBeanDefinition(99); + RootBeanDefinition bd2 = createConstructorDependencyBeanDefinition(43); + bd1.setPrimary(true); + bd2.setPrimary(true); + lbf.registerBeanDefinition("bd1", bd1); + lbf.registerBeanDefinition("bd2", bd2); + + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(() -> + lbf.getBean(ConstructorDependency.class, 42)) + .withMessageContaining("more than one 'primary'"); + } + + @Test + void getBeanByTypeInstanceFiltersOutNonAutowireCandidates() { + RootBeanDefinition bd1 = createConstructorDependencyBeanDefinition(99); + RootBeanDefinition bd2 = createConstructorDependencyBeanDefinition(43); + RootBeanDefinition na1 = createConstructorDependencyBeanDefinition(21); + na1.setAutowireCandidate(false); + + lbf.registerBeanDefinition("bd1", bd1); + lbf.registerBeanDefinition("na1", na1); + ConstructorDependency actual = lbf.getBean(ConstructorDependency.class, 42); // na1 was filtered + assertThat(actual.beanName).isEqualTo("bd1"); + + lbf.registerBeanDefinition("bd2", bd2); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> + lbf.getBean(TestBean.class, 67)); + } + + @Test + @SuppressWarnings("rawtypes") + void beanProviderSerialization() throws Exception { + lbf.setSerializationId("test"); + + ObjectProvider provider = lbf.getBeanProvider(ConstructorDependency.class); + ObjectProvider deserialized = SerializationTestUtils.serializeAndDeserialize(provider); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy( + deserialized::getObject); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> + deserialized.getObject(42)); + assertThat(deserialized.getIfAvailable()).isNull(); + assertThat(deserialized.getIfUnique()).isNull(); + } + + @Test + void getBeanWithArgsNotCreatedForFactoryBeanChecking() { + RootBeanDefinition bd1 = new RootBeanDefinition(ConstructorDependency.class); + bd1.setScope(BeanDefinition.SCOPE_PROTOTYPE); + lbf.registerBeanDefinition("bd1", bd1); + RootBeanDefinition bd2 = new RootBeanDefinition(ConstructorDependencyFactoryBean.class); + bd2.setScope(BeanDefinition.SCOPE_PROTOTYPE); + lbf.registerBeanDefinition("bd2", bd2); + + ConstructorDependency bean = lbf.getBean(ConstructorDependency.class, 42); + assertThat(bean.beanName).isEqualTo("bd1"); + assertThat(bean.spouseAge).isEqualTo(42); + + assertThat(lbf.getBeanNamesForType(ConstructorDependency.class).length).isEqualTo(1); + assertThat(lbf.getBeanNamesForType(ConstructorDependencyFactoryBean.class).length).isEqualTo(1); + assertThat(lbf.getBeanNamesForType(ResolvableType.forClassWithGenerics(FactoryBean.class, Object.class)).length).isEqualTo(1); + assertThat(lbf.getBeanNamesForType(ResolvableType.forClassWithGenerics(FactoryBean.class, String.class)).length).isEqualTo(0); + assertThat(lbf.getBeanNamesForType(ResolvableType.forClassWithGenerics(FactoryBean.class, Object.class), true, true).length).isEqualTo(1); + assertThat(lbf.getBeanNamesForType(ResolvableType.forClassWithGenerics(FactoryBean.class, String.class), true, true).length).isEqualTo(0); + } + + private RootBeanDefinition createConstructorDependencyBeanDefinition(int age) { + RootBeanDefinition bd = new RootBeanDefinition(ConstructorDependency.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bd.getConstructorArgumentValues().addGenericArgumentValue(age); + return bd; + } + + @Test + void autowireBeanByType() { + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + lbf.registerBeanDefinition("test", bd); + DependenciesBean bean = (DependenciesBean) + lbf.autowire(DependenciesBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true); + TestBean test = (TestBean) lbf.getBean("test"); + assertThat(bean.getSpouse()).isEqualTo(test); + } + + /** + * Verifies that a dependency on a {@link FactoryBean} can be autowired + * by type, specifically addressing the JIRA issue raised in SPR-4040. + */ + @Test + void autowireBeanWithFactoryBeanByType() { + RootBeanDefinition bd = new RootBeanDefinition(LazyInitFactory.class); + lbf.registerBeanDefinition("factoryBean", bd); + LazyInitFactory factoryBean = (LazyInitFactory) lbf.getBean("&factoryBean"); + assertThat(factoryBean).as("The FactoryBean should have been registered.").isNotNull(); + FactoryBeanDependentBean bean = (FactoryBeanDependentBean) lbf.autowire(FactoryBeanDependentBean.class, + AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true); + assertThat(bean.getFactoryBean()).as("The FactoryBeanDependentBean should have been autowired 'by type' with the LazyInitFactory.").isEqualTo(factoryBean); + } + + @Test + void autowireBeanWithFactoryBeanByTypeWithPrimary() { + DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + RootBeanDefinition bd1 = new RootBeanDefinition(LazyInitFactory.class); + RootBeanDefinition bd2 = new RootBeanDefinition(LazyInitFactory.class); + bd2.setPrimary(true); + lbf.registerBeanDefinition("bd1", bd1); + lbf.registerBeanDefinition("bd2", bd2); + LazyInitFactory bd1FactoryBean = (LazyInitFactory) lbf.getBean("&bd1"); + LazyInitFactory bd2FactoryBean = (LazyInitFactory) lbf.getBean("&bd2"); + assertThat(bd1FactoryBean).isNotNull(); + assertThat(bd2FactoryBean).isNotNull(); + FactoryBeanDependentBean bean = (FactoryBeanDependentBean) lbf.autowire(FactoryBeanDependentBean.class, + AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true); + assertThat(bean.getFactoryBean()).isNotEqualTo(bd1FactoryBean); + assertThat(bean.getFactoryBean()).isEqualTo(bd2FactoryBean); + } + + @Test + void getTypeForAbstractFactoryBean() { + RootBeanDefinition bd = new RootBeanDefinition(FactoryBeanThatShouldntBeCalled.class); + bd.setAbstract(true); + lbf.registerBeanDefinition("factoryBean", bd); + assertThat(lbf.getType("factoryBean")).isNull(); + } + + @Test + void getBeanNamesForTypeBeforeFactoryBeanCreation() { + lbf.registerBeanDefinition("factoryBean", new RootBeanDefinition(FactoryBeanThatShouldntBeCalled.class)); + assertThat(lbf.containsSingleton("factoryBean")).isFalse(); + + String[] beanNames = lbf.getBeanNamesForType(Runnable.class, false, false); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("&factoryBean"); + + beanNames = lbf.getBeanNamesForType(Callable.class, false, false); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("&factoryBean"); + + beanNames = lbf.getBeanNamesForType(RepositoryFactoryInformation.class, false, false); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("&factoryBean"); + + beanNames = lbf.getBeanNamesForType(FactoryBean.class, false, false); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("&factoryBean"); + } + + @Test + void getBeanNamesForTypeAfterFactoryBeanCreation() { + lbf.registerBeanDefinition("factoryBean", new RootBeanDefinition(FactoryBeanThatShouldntBeCalled.class)); + lbf.getBean("&factoryBean"); + + String[] beanNames = lbf.getBeanNamesForType(Runnable.class, false, false); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("&factoryBean"); + + beanNames = lbf.getBeanNamesForType(Callable.class, false, false); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("&factoryBean"); + + beanNames = lbf.getBeanNamesForType(RepositoryFactoryInformation.class, false, false); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("&factoryBean"); + + beanNames = lbf.getBeanNamesForType(FactoryBean.class, false, false); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("&factoryBean"); + } + + /** + * Verifies that a dependency on a {@link FactoryBean} can not + * be autowired by name, as & is an illegal character in + * Java method names. In other words, you can't name a method + * {@code set&FactoryBean(...)}. + */ + @Test + void autowireBeanWithFactoryBeanByName() { + RootBeanDefinition bd = new RootBeanDefinition(LazyInitFactory.class); + lbf.registerBeanDefinition("factoryBean", bd); + LazyInitFactory factoryBean = (LazyInitFactory) lbf.getBean("&factoryBean"); + assertThat(factoryBean).as("The FactoryBean should have been registered.").isNotNull(); + assertThatExceptionOfType(TypeMismatchException.class).isThrownBy(() -> + lbf.autowire(FactoryBeanDependentBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_NAME, true)); + } + + @Test + void autowireBeanByTypeWithTwoMatches() { + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); + lbf.registerBeanDefinition("test", bd); + lbf.registerBeanDefinition("spouse", bd2); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> + lbf.autowire(DependenciesBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true)) + .withMessageContaining("test") + .withMessageContaining("spouse"); + } + + @Test + void autowireBeanByTypeWithDependencyCheck() { + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> + lbf.autowire(DependenciesBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true)); + } + + @Test + void autowireBeanByTypeWithNoDependencyCheck() { + DependenciesBean bean = (DependenciesBean) + lbf.autowire(DependenciesBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, false); + assertThat(bean.getSpouse()).isNull(); + } + + @Test + void autowireBeanByTypeWithTwoMatchesAndOnePrimary() { + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.setPrimary(true); + RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); + lbf.registerBeanDefinition("test", bd); + lbf.registerBeanDefinition("spouse", bd2); + + DependenciesBean bean = (DependenciesBean) + lbf.autowire(DependenciesBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true); + assertThat(bean.getSpouse()).isEqualTo(lbf.getBean("test")); + } + + @Test + void autowireBeanByTypeWithTwoPrimaryCandidates() { + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.setPrimary(true); + RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); + bd2.setPrimary(true); + lbf.registerBeanDefinition("test", bd); + lbf.registerBeanDefinition("spouse", bd2); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> + lbf.autowire(DependenciesBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true)) + .withCauseExactlyInstanceOf(NoUniqueBeanDefinitionException.class); + } + + @Test + void autowireBeanByTypeWithTwoMatchesAndPriority() { + lbf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); + RootBeanDefinition bd = new RootBeanDefinition(HighPriorityTestBean.class); + RootBeanDefinition bd2 = new RootBeanDefinition(LowPriorityTestBean.class); + lbf.registerBeanDefinition("test", bd); + lbf.registerBeanDefinition("spouse", bd2); + + DependenciesBean bean = (DependenciesBean) + lbf.autowire(DependenciesBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true); + assertThat(bean.getSpouse()).isEqualTo(lbf.getBean("test")); + } + + @Test + void autowireBeanByTypeWithIdenticalPriorityCandidates() { + lbf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); + RootBeanDefinition bd = new RootBeanDefinition(HighPriorityTestBean.class); + RootBeanDefinition bd2 = new RootBeanDefinition(HighPriorityTestBean.class); + lbf.registerBeanDefinition("test", bd); + lbf.registerBeanDefinition("spouse", bd2); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> + lbf.autowire(DependenciesBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true)) + .withCauseExactlyInstanceOf(NoUniqueBeanDefinitionException.class) + .withMessageContaining("5"); + } + + @Test + void autowireBeanByTypePrimaryTakesPrecedenceOverPriority() { + lbf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); + RootBeanDefinition bd = new RootBeanDefinition(HighPriorityTestBean.class); + RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); + bd2.setPrimary(true); + lbf.registerBeanDefinition("test", bd); + lbf.registerBeanDefinition("spouse", bd2); + + DependenciesBean bean = (DependenciesBean) + lbf.autowire(DependenciesBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true); + assertThat(bean.getSpouse()).isEqualTo(lbf.getBean("spouse")); + } + + @Test + void autowireExistingBeanByName() { + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + lbf.registerBeanDefinition("spouse", bd); + DependenciesBean existingBean = new DependenciesBean(); + lbf.autowireBeanProperties(existingBean, AutowireCapableBeanFactory.AUTOWIRE_BY_NAME, true); + TestBean spouse = (TestBean) lbf.getBean("spouse"); + assertThat(spouse).isEqualTo(existingBean.getSpouse()); + assertThat(BeanFactoryUtils.beanOfType(lbf, TestBean.class)).isSameAs(spouse); + } + + @Test + void autowireExistingBeanByNameWithDependencyCheck() { + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + lbf.registerBeanDefinition("spous", bd); + DependenciesBean existingBean = new DependenciesBean(); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> + lbf.autowireBeanProperties(existingBean, AutowireCapableBeanFactory.AUTOWIRE_BY_NAME, true)); + } + + @Test + void autowireExistingBeanByNameWithNoDependencyCheck() { + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + lbf.registerBeanDefinition("spous", bd); + DependenciesBean existingBean = new DependenciesBean(); + lbf.autowireBeanProperties(existingBean, AutowireCapableBeanFactory.AUTOWIRE_BY_NAME, false); + assertThat(existingBean.getSpouse()).isNull(); + } + + @Test + void autowireExistingBeanByType() { + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + lbf.registerBeanDefinition("test", bd); + DependenciesBean existingBean = new DependenciesBean(); + lbf.autowireBeanProperties(existingBean, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true); + TestBean test = (TestBean) lbf.getBean("test"); + assertThat(test).isEqualTo(existingBean.getSpouse()); + } + + @Test + void autowireExistingBeanByTypeWithDependencyCheck() { + DependenciesBean existingBean = new DependenciesBean(); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> + lbf.autowireBeanProperties(existingBean, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true)); + } + + @Test + void autowireExistingBeanByTypeWithNoDependencyCheck() { + DependenciesBean existingBean = new DependenciesBean(); + lbf.autowireBeanProperties(existingBean, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, false); + assertThat(existingBean.getSpouse()).isNull(); + } + + @Test + void invalidAutowireMode() { + assertThatIllegalArgumentException().isThrownBy(() -> + lbf.autowireBeanProperties(new TestBean(), AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false)); + } + + @Test + void applyBeanPropertyValues() { + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("age", "99"); + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.setPropertyValues(pvs); + lbf.registerBeanDefinition("test", bd); + TestBean tb = new TestBean(); + assertThat(tb.getAge()).isEqualTo(0); + lbf.applyBeanPropertyValues(tb, "test"); + assertThat(tb.getAge()).isEqualTo(99); + } + + @Test + void applyBeanPropertyValuesWithIncompleteDefinition() { + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("age", "99"); + RootBeanDefinition bd = new RootBeanDefinition(); + bd.setPropertyValues(pvs); + lbf.registerBeanDefinition("test", bd); + TestBean tb = new TestBean(); + assertThat(tb.getAge()).isEqualTo(0); + lbf.applyBeanPropertyValues(tb, "test"); + assertThat(tb.getAge()).isEqualTo(99); + assertThat(tb.getBeanFactory()).isNull(); + assertThat(tb.getSpouse()).isNull(); + } + + @Test + void createBean() { + TestBean tb = lbf.createBean(TestBean.class); + assertThat(tb.getBeanFactory()).isSameAs(lbf); + lbf.destroyBean(tb); + } + + @Test + void createBeanWithDisposableBean() { + DerivedTestBean tb = lbf.createBean(DerivedTestBean.class); + assertThat(tb.getBeanFactory()).isSameAs(lbf); + lbf.destroyBean(tb); + assertThat(tb.wasDestroyed()).isTrue(); + } + + @Test + void configureBean() { + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("age", "99"); + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.setPropertyValues(pvs); + lbf.registerBeanDefinition("test", bd); + TestBean tb = new TestBean(); + assertThat(tb.getAge()).isEqualTo(0); + lbf.configureBean(tb, "test"); + assertThat(tb.getAge()).isEqualTo(99); + assertThat(tb.getBeanFactory()).isSameAs(lbf); + assertThat(tb.getSpouse()).isNull(); + } + + @Test + void configureBeanWithAutowiring() { + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + lbf.registerBeanDefinition("spouse", bd); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("age", "99"); + RootBeanDefinition tbd = new RootBeanDefinition(TestBean.class); + tbd.setAutowireMode(RootBeanDefinition.AUTOWIRE_BY_NAME); + lbf.registerBeanDefinition("test", tbd); + TestBean tb = new TestBean(); + lbf.configureBean(tb, "test"); + assertThat(tb.getBeanFactory()).isSameAs(lbf); + TestBean spouse = (TestBean) lbf.getBean("spouse"); + assertThat(tb.getSpouse()).isEqualTo(spouse); + } + + @Test + void extensiveCircularReference() { + for (int i = 0; i < 1000; i++) { + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.addPropertyValue(new PropertyValue("spouse", new RuntimeBeanReference("bean" + (i < 99 ? i + 1 : 0)))); + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.setPropertyValues(pvs); + lbf.registerBeanDefinition("bean" + i, bd); + } + lbf.preInstantiateSingletons(); + for (int i = 0; i < 1000; i++) { + TestBean bean = (TestBean) lbf.getBean("bean" + i); + TestBean otherBean = (TestBean) lbf.getBean("bean" + (i < 99 ? i + 1 : 0)); + assertThat(bean.getSpouse() == otherBean).isTrue(); + } + } + + @Test + void circularReferenceThroughAutowiring() { + RootBeanDefinition bd = new RootBeanDefinition(ConstructorDependencyBean.class); + bd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); + lbf.registerBeanDefinition("test", bd); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy( + lbf::preInstantiateSingletons); + } + + @Test + void circularReferenceThroughFactoryBeanAutowiring() { + RootBeanDefinition bd = new RootBeanDefinition(ConstructorDependencyFactoryBean.class); + bd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); + lbf.registerBeanDefinition("test", bd); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy( + lbf::preInstantiateSingletons); + } + + @Test + void circularReferenceThroughFactoryBeanTypeCheck() { + RootBeanDefinition bd = new RootBeanDefinition(ConstructorDependencyFactoryBean.class); + bd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); + lbf.registerBeanDefinition("test", bd); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> + lbf.getBeansOfType(String.class)); + } + + @Test + void avoidCircularReferenceThroughAutowiring() { + RootBeanDefinition bd = new RootBeanDefinition(ConstructorDependencyFactoryBean.class); + bd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); + lbf.registerBeanDefinition("test", bd); + RootBeanDefinition bd2 = new RootBeanDefinition(String.class); + bd2.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); + lbf.registerBeanDefinition("string", bd2); + lbf.preInstantiateSingletons(); + } + + @Test + void constructorDependencyWithClassResolution() { + RootBeanDefinition bd = new RootBeanDefinition(ConstructorDependencyWithClassResolution.class); + bd.getConstructorArgumentValues().addGenericArgumentValue("java.lang.String"); + lbf.registerBeanDefinition("test", bd); + lbf.preInstantiateSingletons(); + } + + @Test + void constructorDependencyWithUnresolvableClass() { + RootBeanDefinition bd = new RootBeanDefinition(ConstructorDependencyWithClassResolution.class); + bd.getConstructorArgumentValues().addGenericArgumentValue("java.lang.Strin"); + lbf.registerBeanDefinition("test", bd); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy( + lbf::preInstantiateSingletons); + } + + @Test + void beanDefinitionWithInterface() { + lbf.registerBeanDefinition("test", new RootBeanDefinition(ITestBean.class)); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + lbf.getBean("test")) + .withMessageContaining("interface") + .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("test")); + } + + @Test + void beanDefinitionWithAbstractClass() { + lbf.registerBeanDefinition("test", new RootBeanDefinition(AbstractBeanFactory.class)); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + lbf.getBean("test")) + .withMessageContaining("abstract") + .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("test")); + } + + @Test + void prototypeFactoryBeanNotEagerlyCalled() { + lbf.registerBeanDefinition("test", new RootBeanDefinition(FactoryBeanThatShouldntBeCalled.class)); + lbf.preInstantiateSingletons(); + } + + @Test + void lazyInitFlag() { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + RootBeanDefinition bd1 = new RootBeanDefinition(TestBean.class); + bd1.setLazyInit(true); + factory.registerBeanDefinition("tb1", bd1); + RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); + bd2.setLazyInit(false); + factory.registerBeanDefinition("tb2", bd2); + factory.registerBeanDefinition("tb3", new RootBeanDefinition(TestBean.class)); + + assertThat(((AbstractBeanDefinition) factory.getMergedBeanDefinition("tb1")).getLazyInit()).isEqualTo(Boolean.TRUE); + assertThat(((AbstractBeanDefinition) factory.getMergedBeanDefinition("tb2")).getLazyInit()).isEqualTo(Boolean.FALSE); + assertThat(((AbstractBeanDefinition) factory.getMergedBeanDefinition("tb3")).getLazyInit()).isNull(); + + factory.preInstantiateSingletons(); + assertThat(factory.containsSingleton("tb1")).isFalse(); + assertThat(factory.containsSingleton("tb2")).isTrue(); + assertThat(factory.containsSingleton("tb3")).isTrue(); + } + + @Test + void lazyInitFactory() { + lbf.registerBeanDefinition("test", new RootBeanDefinition(LazyInitFactory.class)); + lbf.preInstantiateSingletons(); + LazyInitFactory factory = (LazyInitFactory) lbf.getBean("&test"); + assertThat(factory.initialized).isFalse(); + } + + @Test + void smartInitFactory() { + lbf.registerBeanDefinition("test", new RootBeanDefinition(EagerInitFactory.class)); + lbf.preInstantiateSingletons(); + EagerInitFactory factory = (EagerInitFactory) lbf.getBean("&test"); + assertThat(factory.initialized).isTrue(); + } + + @Test + void prototypeFactoryBeanNotEagerlyCalledInCaseOfBeanClassName() { + lbf.registerBeanDefinition("test", + new RootBeanDefinition(FactoryBeanThatShouldntBeCalled.class.getName(), null, null)); + lbf.preInstantiateSingletons(); + } + + @Test + void prototypeStringCreatedRepeatedly() { + RootBeanDefinition stringDef = new RootBeanDefinition(String.class); + stringDef.setScope(BeanDefinition.SCOPE_PROTOTYPE); + stringDef.getConstructorArgumentValues().addGenericArgumentValue(new TypedStringValue("value")); + lbf.registerBeanDefinition("string", stringDef); + String val1 = lbf.getBean("string", String.class); + String val2 = lbf.getBean("string", String.class); + assertThat(val1).isEqualTo("value"); + assertThat(val2).isEqualTo("value"); + assertThat(val2).isNotSameAs(val1); + } + + @Test + void prototypeWithArrayConversionForConstructor() { + List list = new ManagedList<>(); + list.add("myName"); + list.add("myBeanName"); + RootBeanDefinition bd = new RootBeanDefinition(DerivedTestBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bd.getConstructorArgumentValues().addGenericArgumentValue(list); + lbf.registerBeanDefinition("test", bd); + DerivedTestBean tb = (DerivedTestBean) lbf.getBean("test"); + assertThat(tb.getName()).isEqualTo("myName"); + assertThat(tb.getBeanName()).isEqualTo("myBeanName"); + DerivedTestBean tb2 = (DerivedTestBean) lbf.getBean("test"); + assertThat(tb != tb2).isTrue(); + assertThat(tb2.getName()).isEqualTo("myName"); + assertThat(tb2.getBeanName()).isEqualTo("myBeanName"); + } + + @Test + void prototypeWithArrayConversionForFactoryMethod() { + List list = new ManagedList<>(); + list.add("myName"); + list.add("myBeanName"); + RootBeanDefinition bd = new RootBeanDefinition(DerivedTestBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bd.setFactoryMethodName("create"); + bd.getConstructorArgumentValues().addGenericArgumentValue(list); + lbf.registerBeanDefinition("test", bd); + DerivedTestBean tb = (DerivedTestBean) lbf.getBean("test"); + assertThat(tb.getName()).isEqualTo("myName"); + assertThat(tb.getBeanName()).isEqualTo("myBeanName"); + DerivedTestBean tb2 = (DerivedTestBean) lbf.getBean("test"); + assertThat(tb != tb2).isTrue(); + assertThat(tb2.getName()).isEqualTo("myName"); + assertThat(tb2.getBeanName()).isEqualTo("myBeanName"); + } + + @Test + void beanPostProcessorWithWrappedObjectAndDisposableBean() { + RootBeanDefinition bd = new RootBeanDefinition(BeanWithDisposableBean.class); + lbf.registerBeanDefinition("test", bd); + lbf.addBeanPostProcessor(new BeanPostProcessor() { + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) { + return new TestBean(); + } + }); + BeanWithDisposableBean.closed = false; + lbf.preInstantiateSingletons(); + lbf.destroySingletons(); + assertThat(BeanWithDisposableBean.closed).as("Destroy method invoked").isTrue(); + } + + @Test + void beanPostProcessorWithWrappedObjectAndCloseable() { + RootBeanDefinition bd = new RootBeanDefinition(BeanWithCloseable.class); + lbf.registerBeanDefinition("test", bd); + lbf.addBeanPostProcessor(new BeanPostProcessor() { + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) { + return new TestBean(); + } + }); + BeanWithDisposableBean.closed = false; + lbf.preInstantiateSingletons(); + lbf.destroySingletons(); + assertThat(BeanWithCloseable.closed).as("Destroy method invoked").isTrue(); + } + + @Test + void beanPostProcessorWithWrappedObjectAndDestroyMethod() { + RootBeanDefinition bd = new RootBeanDefinition(BeanWithDestroyMethod.class); + bd.setDestroyMethodName("close"); + lbf.registerBeanDefinition("test", bd); + lbf.addBeanPostProcessor(new BeanPostProcessor() { + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) { + return new TestBean(); + } + }); + BeanWithDestroyMethod.closeCount = 0; + lbf.preInstantiateSingletons(); + lbf.destroySingletons(); + assertThat(BeanWithDestroyMethod.closeCount).as("Destroy methods invoked").isEqualTo(1); + } + + @Test + void destroyMethodOnInnerBean() { + RootBeanDefinition innerBd = new RootBeanDefinition(BeanWithDestroyMethod.class); + innerBd.setDestroyMethodName("close"); + RootBeanDefinition bd = new RootBeanDefinition(BeanWithDestroyMethod.class); + bd.setDestroyMethodName("close"); + bd.getPropertyValues().add("inner", innerBd); + lbf.registerBeanDefinition("test", bd); + BeanWithDestroyMethod.closeCount = 0; + lbf.preInstantiateSingletons(); + lbf.destroySingletons(); + assertThat(BeanWithDestroyMethod.closeCount).as("Destroy methods invoked").isEqualTo(2); + } + + @Test + void destroyMethodOnInnerBeanAsPrototype() { + RootBeanDefinition innerBd = new RootBeanDefinition(BeanWithDestroyMethod.class); + innerBd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + innerBd.setDestroyMethodName("close"); + RootBeanDefinition bd = new RootBeanDefinition(BeanWithDestroyMethod.class); + bd.setDestroyMethodName("close"); + bd.getPropertyValues().add("inner", innerBd); + lbf.registerBeanDefinition("test", bd); + BeanWithDestroyMethod.closeCount = 0; + lbf.preInstantiateSingletons(); + lbf.destroySingletons(); + assertThat(BeanWithDestroyMethod.closeCount).as("Destroy methods invoked").isEqualTo(1); + } + + @Test + void findTypeOfSingletonFactoryMethodOnBeanInstance() { + findTypeOfPrototypeFactoryMethodOnBeanInstance(true); + } + + @Test + void findTypeOfPrototypeFactoryMethodOnBeanInstance() { + findTypeOfPrototypeFactoryMethodOnBeanInstance(false); + } + + /** + * @param singleton whether the bean created from the factory method on + * the bean instance should be a singleton or prototype. This flag is + * used to allow checking of the new ability in 1.2.4 to determine the type + * of a prototype created from invoking a factory method on a bean instance + * in the factory. + */ + private void findTypeOfPrototypeFactoryMethodOnBeanInstance(boolean singleton) { + String expectedNameFromProperties = "tony"; + String expectedNameFromArgs = "gordon"; + + RootBeanDefinition instanceFactoryDefinition = new RootBeanDefinition(BeanWithFactoryMethod.class); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", expectedNameFromProperties); + instanceFactoryDefinition.setPropertyValues(pvs); + lbf.registerBeanDefinition("factoryBeanInstance", instanceFactoryDefinition); + + RootBeanDefinition factoryMethodDefinitionWithProperties = new RootBeanDefinition(); + factoryMethodDefinitionWithProperties.setFactoryBeanName("factoryBeanInstance"); + factoryMethodDefinitionWithProperties.setFactoryMethodName("create"); + if (!singleton) { + factoryMethodDefinitionWithProperties.setScope(BeanDefinition.SCOPE_PROTOTYPE); + } + lbf.registerBeanDefinition("fmWithProperties", factoryMethodDefinitionWithProperties); + + RootBeanDefinition factoryMethodDefinitionGeneric = new RootBeanDefinition(); + factoryMethodDefinitionGeneric.setFactoryBeanName("factoryBeanInstance"); + factoryMethodDefinitionGeneric.setFactoryMethodName("createGeneric"); + if (!singleton) { + factoryMethodDefinitionGeneric.setScope(BeanDefinition.SCOPE_PROTOTYPE); + } + lbf.registerBeanDefinition("fmGeneric", factoryMethodDefinitionGeneric); + + RootBeanDefinition factoryMethodDefinitionWithArgs = new RootBeanDefinition(); + factoryMethodDefinitionWithArgs.setFactoryBeanName("factoryBeanInstance"); + factoryMethodDefinitionWithArgs.setFactoryMethodName("createWithArgs"); + ConstructorArgumentValues cvals = new ConstructorArgumentValues(); + cvals.addGenericArgumentValue(expectedNameFromArgs); + factoryMethodDefinitionWithArgs.setConstructorArgumentValues(cvals); + if (!singleton) { + factoryMethodDefinitionWithArgs.setScope(BeanDefinition.SCOPE_PROTOTYPE); + } + lbf.registerBeanDefinition("fmWithArgs", factoryMethodDefinitionWithArgs); + + assertThat(lbf.getBeanDefinitionCount()).isEqualTo(4); + List tbNames = Arrays.asList(lbf.getBeanNamesForType(TestBean.class)); + assertThat(tbNames.contains("fmWithProperties")).isTrue(); + assertThat(tbNames.contains("fmWithArgs")).isTrue(); + assertThat(tbNames.size()).isEqualTo(2); + + TestBean tb = (TestBean) lbf.getBean("fmWithProperties"); + TestBean second = (TestBean) lbf.getBean("fmWithProperties"); + if (singleton) { + assertThat(second).isSameAs(tb); + } + else { + assertThat(second).isNotSameAs(tb); + } + assertThat(tb.getName()).isEqualTo(expectedNameFromProperties); + + tb = (TestBean) lbf.getBean("fmGeneric"); + second = (TestBean) lbf.getBean("fmGeneric"); + if (singleton) { + assertThat(second).isSameAs(tb); + } + else { + assertThat(second).isNotSameAs(tb); + } + assertThat(tb.getName()).isEqualTo(expectedNameFromProperties); + + TestBean tb2 = (TestBean) lbf.getBean("fmWithArgs"); + second = (TestBean) lbf.getBean("fmWithArgs"); + if (singleton) { + assertThat(second).isSameAs(tb2); + } + else { + assertThat(second).isNotSameAs(tb2); + } + assertThat(tb2.getName()).isEqualTo(expectedNameFromArgs); + } + + @Test + void scopingBeanToUnregisteredScopeResultsInAnException() { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(TestBean.class); + AbstractBeanDefinition beanDefinition = builder.getBeanDefinition(); + beanDefinition.setScope("he put himself so low could hardly look me in the face"); + + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerBeanDefinition("testBean", beanDefinition); + assertThatIllegalStateException().isThrownBy(() -> + factory.getBean("testBean")); + } + + @Test + void explicitScopeInheritanceForChildBeanDefinitions() { + String theChildScope = "bonanza!"; + + RootBeanDefinition parent = new RootBeanDefinition(); + parent.setScope(BeanDefinition.SCOPE_PROTOTYPE); + + AbstractBeanDefinition child = BeanDefinitionBuilder.childBeanDefinition("parent").getBeanDefinition(); + child.setBeanClass(TestBean.class); + child.setScope(theChildScope); + + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerBeanDefinition("parent", parent); + factory.registerBeanDefinition("child", child); + + AbstractBeanDefinition def = (AbstractBeanDefinition) factory.getBeanDefinition("child"); + assertThat(def.getScope()).as("Child 'scope' not overriding parent scope (it must).").isEqualTo(theChildScope); + } + + @Test + void scopeInheritanceForChildBeanDefinitions() { + String theParentScope = "bonanza!"; + + RootBeanDefinition parent = new RootBeanDefinition(); + parent.setScope(theParentScope); + + AbstractBeanDefinition child = new ChildBeanDefinition("parent"); + child.setBeanClass(TestBean.class); + + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerBeanDefinition("parent", parent); + factory.registerBeanDefinition("child", child); + + BeanDefinition def = factory.getMergedBeanDefinition("child"); + assertThat(def.getScope()).as("Child 'scope' not inherited").isEqualTo(theParentScope); + } + + @Test + void fieldSettingWithInstantiationAwarePostProcessorNoShortCircuit() { + doTestFieldSettingWithInstantiationAwarePostProcessor(false); + } + + @Test + void fieldSettingWithInstantiationAwarePostProcessorWithShortCircuit() { + doTestFieldSettingWithInstantiationAwarePostProcessor(true); + } + + private void doTestFieldSettingWithInstantiationAwarePostProcessor(final boolean skipPropertyPopulation) { + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + int ageSetByPropertyValue = 27; + bd.getPropertyValues().addPropertyValue(new PropertyValue("age", ageSetByPropertyValue)); + lbf.registerBeanDefinition("test", bd); + final String nameSetOnField = "nameSetOnField"; + lbf.addBeanPostProcessor(new InstantiationAwareBeanPostProcessor() { + @Override + public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException { + TestBean tb = (TestBean) bean; + try { + Field f = TestBean.class.getDeclaredField("name"); + f.setAccessible(true); + f.set(tb, nameSetOnField); + return !skipPropertyPopulation; + } + catch (Exception ex) { + throw new AssertionError("Unexpected exception", ex); + } + } + }); + lbf.preInstantiateSingletons(); + TestBean tb = (TestBean) lbf.getBean("test"); + assertThat(tb.getName()).as("Name was set on field by IAPP").isEqualTo(nameSetOnField); + if (!skipPropertyPopulation) { + assertThat(tb.getAge()).as("Property value still set").isEqualTo(ageSetByPropertyValue); + } + else { + assertThat(tb.getAge()).as("Property value was NOT set and still has default value").isEqualTo(0); + } + } + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + void initSecurityAwarePrototypeBean() { + RootBeanDefinition bd = new RootBeanDefinition(TestSecuredBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bd.setInitMethodName("init"); + lbf.registerBeanDefinition("test", bd); + final Subject subject = new Subject(); + subject.getPrincipals().add(new TestPrincipal("user1")); + + TestSecuredBean bean = (TestSecuredBean) Subject.doAsPrivileged(subject, + (PrivilegedAction) () -> lbf.getBean("test"), null); + assertThat(bean).isNotNull(); + assertThat(bean.getUserName()).isEqualTo("user1"); + } + + @Test + void containsBeanReturnsTrueEvenForAbstractBeanDefinition() { + lbf.registerBeanDefinition("abs", BeanDefinitionBuilder + .rootBeanDefinition(TestBean.class).setAbstract(true).getBeanDefinition()); + assertThat(lbf.containsBean("abs")).isEqualTo(true); + assertThat(lbf.containsBean("bogus")).isEqualTo(false); + } + + @Test + void resolveEmbeddedValue() { + StringValueResolver r1 = mock(StringValueResolver.class); + StringValueResolver r2 = mock(StringValueResolver.class); + StringValueResolver r3 = mock(StringValueResolver.class); + lbf.addEmbeddedValueResolver(r1); + lbf.addEmbeddedValueResolver(r2); + lbf.addEmbeddedValueResolver(r3); + given(r1.resolveStringValue("A")).willReturn("B"); + given(r2.resolveStringValue("B")).willReturn(null); + given(r3.resolveStringValue(isNull())).willThrow(new IllegalArgumentException()); + + lbf.resolveEmbeddedValue("A"); + + verify(r1).resolveStringValue("A"); + verify(r2).resolveStringValue("B"); + verify(r3, never()).resolveStringValue(isNull()); + } + + @Test + void populatedJavaUtilOptionalBean() { + RootBeanDefinition bd = new RootBeanDefinition(Optional.class); + bd.setFactoryMethodName("of"); + bd.getConstructorArgumentValues().addGenericArgumentValue("CONTENT"); + lbf.registerBeanDefinition("optionalBean", bd); + assertThat((Optional) lbf.getBean(Optional.class)).isEqualTo(Optional.of("CONTENT")); + } + + @Test + void emptyJavaUtilOptionalBean() { + RootBeanDefinition bd = new RootBeanDefinition(Optional.class); + bd.setFactoryMethodName("empty"); + lbf.registerBeanDefinition("optionalBean", bd); + assertThat((Optional) lbf.getBean(Optional.class)).isSameAs(Optional.empty()); + } + + @Test + void nonPublicEnum() { + RootBeanDefinition bd = new RootBeanDefinition(NonPublicEnumHolder.class); + bd.getConstructorArgumentValues().addGenericArgumentValue("VALUE_1"); + lbf.registerBeanDefinition("holderBean", bd); + NonPublicEnumHolder holder = (NonPublicEnumHolder) lbf.getBean("holderBean"); + assertThat(holder.getNonPublicEnum()).isEqualTo(NonPublicEnum.VALUE_1); + } + + + @SuppressWarnings("deprecation") + private int registerBeanDefinitions(Properties p) { + return (new org.springframework.beans.factory.support.PropertiesBeanDefinitionReader(lbf)).registerBeanDefinitions(p); + } + + @SuppressWarnings("deprecation") + private int registerBeanDefinitions(Properties p, String prefix) { + return (new org.springframework.beans.factory.support.PropertiesBeanDefinitionReader(lbf)).registerBeanDefinitions(p, prefix); + } + + + public static class NoDependencies { + + private NoDependencies() { + } + } + + + public static class ConstructorDependency implements BeanNameAware { + + public TestBean spouse; + + public int spouseAge; + + private String beanName; + + public ConstructorDependency(TestBean spouse) { + this.spouse = spouse; + } + + public ConstructorDependency(int spouseAge) { + this.spouseAge = spouseAge; + } + + @SuppressWarnings("unused") + private ConstructorDependency(TestBean spouse, TestBean otherSpouse) { + throw new IllegalArgumentException("Should never be called"); + } + + @Override + public void setBeanName(String name) { + this.beanName = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ConstructorDependency that = (ConstructorDependency) o; + return spouseAge == that.spouseAge && + Objects.equals(spouse, that.spouse) && + Objects.equals(beanName, that.beanName); + } + + @Override + public int hashCode() { + return Objects.hash(spouse, spouseAge, beanName); + } + } + + + public static class UnsatisfiedConstructorDependency { + + public UnsatisfiedConstructorDependency(TestBean t, SideEffectBean b) { + } + } + + + public static class ConstructorDependencyBean { + + public ConstructorDependencyBean(ConstructorDependencyBean dependency) { + } + } + + + public static class ConstructorDependencyFactoryBean implements FactoryBean { + + public ConstructorDependencyFactoryBean(String dependency) { + } + + @Override + public Object getObject() { + return "test"; + } + + @Override + public Class getObjectType() { + return String.class; + } + + @Override + public boolean isSingleton() { + return true; + } + } + + + public static class ConstructorDependencyWithClassResolution { + + public ConstructorDependencyWithClassResolution(Class clazz) { + } + + public ConstructorDependencyWithClassResolution() { + } + } + + + public static class BeanWithDisposableBean implements DisposableBean { + + private static boolean closed; + + @Override + public void destroy() { + closed = true; + } + } + + + public static class BeanWithCloseable implements Closeable { + + private static boolean closed; + + @Override + public void close() { + closed = true; + } + } + + + public static abstract class BaseClassWithDestroyMethod { + + public abstract BaseClassWithDestroyMethod close(); + } + + + public static class BeanWithDestroyMethod extends BaseClassWithDestroyMethod { + + private static int closeCount = 0; + + @SuppressWarnings("unused") + private BeanWithDestroyMethod inner; + + public void setInner(BeanWithDestroyMethod inner) { + this.inner = inner; + } + + @Override + public BeanWithDestroyMethod close() { + closeCount++; + return this; + } + } + + + public static class BeanWithFactoryMethod { + + private String name; + + public void setName(String name) { + this.name = name; + } + + public TestBean create() { + TestBean tb = new TestBean(); + tb.setName(this.name); + return tb; + } + + public TestBean createWithArgs(String arg) { + TestBean tb = new TestBean(); + tb.setName(arg); + return tb; + } + + public Object createGeneric() { + return create(); + } + } + + + public interface Repository { + } + + + public interface RepositoryFactoryInformation { + } + + + public static abstract class RepositoryFactoryBeanSupport, S, ID extends Serializable> + implements RepositoryFactoryInformation, FactoryBean { + } + + + public static class FactoryBeanThatShouldntBeCalled, S, ID extends Serializable> + extends RepositoryFactoryBeanSupport implements Runnable, Callable { + + @Override + public T getObject() { + throw new IllegalStateException(); + } + + @Override + public Class getObjectType() { + return null; + } + + @Override + public boolean isSingleton() { + return false; + } + + @Override + public void run() { + throw new IllegalStateException(); + } + + @Override + public T call() { + throw new IllegalStateException(); + } + } + + + public static class LazyInitFactory implements FactoryBean { + + public boolean initialized = false; + + @Override + public Object getObject() { + this.initialized = true; + return ""; + } + + @Override + public Class getObjectType() { + return String.class; + } + + @Override + public boolean isSingleton() { + return true; + } + } + + + public static class EagerInitFactory implements SmartFactoryBean { + + public boolean initialized = false; + + @Override + public Object getObject() { + this.initialized = true; + return ""; + } + + @Override + public Class getObjectType() { + return String.class; + } + + @Override + public boolean isSingleton() { + return true; + } + + @Override + public boolean isPrototype() { + return false; + } + + @Override + public boolean isEagerInit() { + return true; + } + } + + + public static class TestBeanFactory { + + public static boolean initialized = false; + + public TestBeanFactory() { + initialized = true; + } + + public static TestBean createTestBean() { + return new TestBean(); + } + + public TestBean createTestBeanNonStatic() { + return new TestBean(); + } + } + + + public static class ArrayBean { + + private Integer[] integerArray; + + private Resource[] resourceArray; + + public ArrayBean() { + } + + public ArrayBean(Integer[] integerArray) { + this.integerArray = integerArray; + } + + public ArrayBean(Integer[] integerArray, Resource[] resourceArray) { + this.integerArray = integerArray; + this.resourceArray = resourceArray; + } + + public Integer[] getIntegerArray() { + return this.integerArray; + } + + public void setResourceArray(Resource[] resourceArray) { + this.resourceArray = resourceArray; + } + + public Resource[] getResourceArray() { + return this.resourceArray; + } + } + + + /** + * Bean with a dependency on a {@link FactoryBean}. + */ + @SuppressWarnings("unused") + private static class FactoryBeanDependentBean { + + private FactoryBean factoryBean; + + public final FactoryBean getFactoryBean() { + return this.factoryBean; + } + + public final void setFactoryBean(final FactoryBean factoryBean) { + this.factoryBean = factoryBean; + } + } + + + private static class CustomTypeConverter implements TypeConverter { + + private final NumberFormat numberFormat; + + public CustomTypeConverter(NumberFormat numberFormat) { + this.numberFormat = numberFormat; + } + + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Object convertIfNecessary(Object value, @Nullable Class requiredType) { + if (value instanceof String && Float.class.isAssignableFrom(requiredType)) { + try { + return new Float(this.numberFormat.parse((String) value).floatValue()); + } + catch (ParseException ex) { + throw new TypeMismatchException(value, requiredType, ex); + } + } + else if (value instanceof String && int.class.isAssignableFrom(requiredType)) { + return 5; + } + else { + return value; + } + } + + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Object convertIfNecessary(Object value, @Nullable Class requiredType, @Nullable MethodParameter methodParam) { + return convertIfNecessary(value, requiredType); + } + + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Object convertIfNecessary(Object value, @Nullable Class requiredType, @Nullable Field field) { + return convertIfNecessary(value, requiredType); + } + } + + + @SuppressWarnings("unused") + private static class TestSecuredBean { + + private String userName; + + void init() { + AccessControlContext acc = AccessController.getContext(); + Subject subject = Subject.getSubject(acc); + if (subject == null) { + return; + } + setNameFromPrincipal(subject.getPrincipals()); + } + + private void setNameFromPrincipal(Set principals) { + if (principals == null) { + return; + } + for (Iterator it = principals.iterator(); it.hasNext();) { + Principal p = it.next(); + this.userName = p.getName(); + return; + } + } + + public String getUserName() { + return this.userName; + } + } + + + @SuppressWarnings("unused") + private static class KnowsIfInstantiated { + + private static boolean instantiated; + + public static void clearInstantiationRecord() { + instantiated = false; + } + + public static boolean wasInstantiated() { + return instantiated; + } + + public KnowsIfInstantiated() { + instantiated = true; + } + + } + + + @Priority(5) + private static class HighPriorityTestBean extends TestBean { + } + + + @Priority(500) + private static class LowPriorityTestBean extends TestBean { + } + + + private static class NullTestBeanFactoryBean implements FactoryBean { + + @Override + public TestBean getObject() { + return null; + } + + @Override + public Class getObjectType() { + return TestBean.class; + } + + @Override + public boolean isSingleton() { + return true; + } + } + + + private static class TestBeanRecipient { + + public TestBean testBean; + + @SuppressWarnings("unused") + public TestBeanRecipient(TestBean testBean) { + this.testBean = testBean; + } + } + + + enum NonPublicEnum { + + VALUE_1, VALUE_2; + } + + + static class NonPublicEnumHolder { + + final NonPublicEnum nonPublicEnum; + + public NonPublicEnumHolder(NonPublicEnum nonPublicEnum) { + this.nonPublicEnum = nonPublicEnum; + } + + public NonPublicEnum getNonPublicEnum() { + return nonPublicEnum; + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/FactoryBeanLookupTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/FactoryBeanLookupTests.java new file mode 100644 index 0000000..9075bb9 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/FactoryBeanLookupTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.AbstractFactoryBean; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Written with the intention of reproducing SPR-7318. + * + * @author Chris Beams + */ +public class FactoryBeanLookupTests { + private BeanFactory beanFactory; + + @BeforeEach + public void setUp() { + beanFactory = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader((BeanDefinitionRegistry) beanFactory).loadBeanDefinitions( + new ClassPathResource("FactoryBeanLookupTests-context.xml", this.getClass())); + } + + @Test + public void factoryBeanLookupByNameDereferencing() { + Object fooFactory = beanFactory.getBean("&fooFactory"); + assertThat(fooFactory).isInstanceOf(FooFactoryBean.class); + } + + @Test + public void factoryBeanLookupByType() { + FooFactoryBean fooFactory = beanFactory.getBean(FooFactoryBean.class); + assertThat(fooFactory).isNotNull(); + } + + @Test + public void factoryBeanLookupByTypeAndNameDereference() { + FooFactoryBean fooFactory = beanFactory.getBean("&fooFactory", FooFactoryBean.class); + assertThat(fooFactory).isNotNull(); + } + + @Test + public void factoryBeanObjectLookupByName() { + Object fooFactory = beanFactory.getBean("fooFactory"); + assertThat(fooFactory).isInstanceOf(Foo.class); + } + + @Test + public void factoryBeanObjectLookupByNameAndType() { + Foo foo = beanFactory.getBean("fooFactory", Foo.class); + assertThat(foo).isNotNull(); + } +} + +class FooFactoryBean extends AbstractFactoryBean { + @Override + protected Foo createInstance() throws Exception { + return new Foo(); + } + + @Override + public Class getObjectType() { + return Foo.class; + } +} + +class Foo { } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/FactoryBeanTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/FactoryBeanTests.java new file mode 100644 index 0000000..9a6f86a --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/FactoryBeanTests.java @@ -0,0 +1,335 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.core.io.Resource; +import org.springframework.core.testfixture.stereotype.Component; +import org.springframework.util.Assert; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.core.testfixture.io.ResourceTestUtils.qualifiedResource; + +/** + * @author Rob Harrop + * @author Juergen Hoeller + * @author Chris Beams + */ +public class FactoryBeanTests { + + private static final Class CLASS = FactoryBeanTests.class; + private static final Resource RETURNS_NULL_CONTEXT = qualifiedResource(CLASS, "returnsNull.xml"); + private static final Resource WITH_AUTOWIRING_CONTEXT = qualifiedResource(CLASS, "withAutowiring.xml"); + private static final Resource ABSTRACT_CONTEXT = qualifiedResource(CLASS, "abstract.xml"); + private static final Resource CIRCULAR_CONTEXT = qualifiedResource(CLASS, "circular.xml"); + + + @Test + public void testFactoryBeanReturnsNull() throws Exception { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(factory).loadBeanDefinitions(RETURNS_NULL_CONTEXT); + + assertThat(factory.getBean("factoryBean").toString()).isEqualTo("null"); + } + + @Test + public void testFactoryBeansWithAutowiring() throws Exception { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(factory).loadBeanDefinitions(WITH_AUTOWIRING_CONTEXT); + + BeanFactoryPostProcessor ppc = (BeanFactoryPostProcessor) factory.getBean("propertyPlaceholderConfigurer"); + ppc.postProcessBeanFactory(factory); + + assertThat(factory.getType("betaFactory")).isNull(); + + Alpha alpha = (Alpha) factory.getBean("alpha"); + Beta beta = (Beta) factory.getBean("beta"); + Gamma gamma = (Gamma) factory.getBean("gamma"); + Gamma gamma2 = (Gamma) factory.getBean("gammaFactory"); + + assertThat(alpha.getBeta()).isSameAs(beta); + assertThat(beta.getGamma()).isSameAs(gamma); + assertThat(beta.getGamma()).isSameAs(gamma2); + assertThat(beta.getName()).isEqualTo("yourName"); + } + + @Test + public void testFactoryBeansWithIntermediateFactoryBeanAutowiringFailure() throws Exception { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(factory).loadBeanDefinitions(WITH_AUTOWIRING_CONTEXT); + + BeanFactoryPostProcessor ppc = (BeanFactoryPostProcessor) factory.getBean("propertyPlaceholderConfigurer"); + ppc.postProcessBeanFactory(factory); + + Beta beta = (Beta) factory.getBean("beta"); + Alpha alpha = (Alpha) factory.getBean("alpha"); + Gamma gamma = (Gamma) factory.getBean("gamma"); + assertThat(alpha.getBeta()).isSameAs(beta); + assertThat(beta.getGamma()).isSameAs(gamma); + } + + @Test + public void testAbstractFactoryBeanViaAnnotation() throws Exception { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(factory).loadBeanDefinitions(ABSTRACT_CONTEXT); + factory.getBeansWithAnnotation(Component.class); + } + + @Test + public void testAbstractFactoryBeanViaType() throws Exception { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(factory).loadBeanDefinitions(ABSTRACT_CONTEXT); + factory.getBeansOfType(AbstractFactoryBean.class); + } + + @Test + public void testCircularReferenceWithPostProcessor() { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(factory).loadBeanDefinitions(CIRCULAR_CONTEXT); + + CountingPostProcessor counter = new CountingPostProcessor(); + factory.addBeanPostProcessor(counter); + + BeanImpl1 impl1 = factory.getBean(BeanImpl1.class); + assertThat(impl1).isNotNull(); + assertThat(impl1.getImpl2()).isNotNull(); + assertThat(impl1.getImpl2()).isNotNull(); + assertThat(impl1.getImpl2().getImpl1()).isSameAs(impl1); + assertThat(counter.getCount("bean1")).isEqualTo(1); + assertThat(counter.getCount("bean2")).isEqualTo(1); + } + + + public static class NullReturningFactoryBean implements FactoryBean { + + @Override + public Object getObject() { + return null; + } + + @Override + public Class getObjectType() { + return null; + } + + @Override + public boolean isSingleton() { + return true; + } + } + + + public static class Alpha implements InitializingBean { + + private Beta beta; + + public void setBeta(Beta beta) { + this.beta = beta; + } + + public Beta getBeta() { + return beta; + } + + @Override + public void afterPropertiesSet() { + Assert.notNull(beta, "'beta' property is required"); + } + } + + + public static class Beta implements InitializingBean { + + private Gamma gamma; + + private String name; + + public void setGamma(Gamma gamma) { + this.gamma = gamma; + } + + public Gamma getGamma() { + return gamma; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public void afterPropertiesSet() { + Assert.notNull(gamma, "'gamma' property is required"); + } + } + + + public static class Gamma { + } + + + @Component + public static class BetaFactoryBean implements FactoryBean { + + public BetaFactoryBean(Alpha alpha) { + } + + private Beta beta; + + public void setBeta(Beta beta) { + this.beta = beta; + } + + @Override + public Object getObject() { + return this.beta; + } + + @Override + public Class getObjectType() { + return null; + } + + @Override + public boolean isSingleton() { + return true; + } + } + + + public abstract static class AbstractFactoryBean implements FactoryBean { + } + + + public static class PassThroughFactoryBean implements FactoryBean, BeanFactoryAware { + + private Class type; + + private String instanceName; + + private BeanFactory beanFactory; + + private T instance; + + public PassThroughFactoryBean(Class type) { + this.type = type; + } + + public void setInstanceName(String instanceName) { + this.instanceName = instanceName; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public T getObject() { + if (instance == null) { + instance = beanFactory.getBean(instanceName, type); + } + return instance; + } + + @Override + public Class getObjectType() { + return type; + } + + @Override + public boolean isSingleton() { + return true; + } + } + + + public static class CountingPostProcessor implements BeanPostProcessor { + + private final Map count = new HashMap<>(); + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) { + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + if (bean instanceof FactoryBean) { + return bean; + } + AtomicInteger c = count.get(beanName); + if (c == null) { + c = new AtomicInteger(); + count.put(beanName, c); + } + c.incrementAndGet(); + return bean; + } + + public int getCount(String beanName) { + AtomicInteger c = count.get(beanName); + if (c != null) { + return c.intValue(); + } + else { + return 0; + } + } + } + + + public static class BeanImpl1 { + + private BeanImpl2 impl2; + + public BeanImpl2 getImpl2() { + return impl2; + } + + public void setImpl2(BeanImpl2 impl2) { + this.impl2 = impl2; + } + } + + + public static class BeanImpl2 { + + private BeanImpl1 impl1; + + public BeanImpl1 getImpl1() { + return impl1; + } + + public void setImpl1(BeanImpl1 impl1) { + this.impl1 = impl1; + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/Spr5475Tests.java b/spring-beans/src/test/java/org/springframework/beans/factory/Spr5475Tests.java new file mode 100644 index 0000000..5b82ef0 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/Spr5475Tests.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.beans.factory.support.BeanDefinitionBuilder.rootBeanDefinition; + +/** + * SPR-5475 exposed the fact that the error message displayed when incorrectly + * invoking a factory method is not instructive to the user and rather misleading. + * + * @author Chris Beams + */ +public class Spr5475Tests { + + @Test + public void noArgFactoryMethodInvokedWithOneArg() { + assertExceptionMessageForMisconfiguredFactoryMethod( + rootBeanDefinition(Foo.class) + .setFactoryMethod("noArgFactory") + .addConstructorArgValue("bogusArg").getBeanDefinition(), + "Error creating bean with name 'foo': No matching factory method found: factory method 'noArgFactory(String)'. " + + "Check that a method with the specified name and arguments exists and that it is static."); + } + + @Test + public void noArgFactoryMethodInvokedWithTwoArgs() { + assertExceptionMessageForMisconfiguredFactoryMethod( + rootBeanDefinition(Foo.class) + .setFactoryMethod("noArgFactory") + .addConstructorArgValue("bogusArg1") + .addConstructorArgValue("bogusArg2".getBytes()).getBeanDefinition(), + "Error creating bean with name 'foo': No matching factory method found: factory method 'noArgFactory(String,byte[])'. " + + "Check that a method with the specified name and arguments exists and that it is static."); + } + + @Test + public void noArgFactoryMethodInvokedWithTwoArgsAndTypesSpecified() { + RootBeanDefinition def = new RootBeanDefinition(Foo.class); + def.setFactoryMethodName("noArgFactory"); + ConstructorArgumentValues cav = new ConstructorArgumentValues(); + cav.addIndexedArgumentValue(0, "bogusArg1", CharSequence.class.getName()); + cav.addIndexedArgumentValue(1, "bogusArg2".getBytes()); + def.setConstructorArgumentValues(cav); + + assertExceptionMessageForMisconfiguredFactoryMethod(def, + "Error creating bean with name 'foo': No matching factory method found: factory method 'noArgFactory(CharSequence,byte[])'. " + + "Check that a method with the specified name and arguments exists and that it is static."); + } + + private void assertExceptionMessageForMisconfiguredFactoryMethod(BeanDefinition bd, String expectedMessage) { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerBeanDefinition("foo", bd); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy( + factory::preInstantiateSingletons) + .withMessageContaining(expectedMessage); + } + + @Test + public void singleArgFactoryMethodInvokedWithNoArgs() { + // calling a factory method that accepts arguments without any arguments emits an exception unlike cases + // where a no-arg factory method is called with arguments. Adding this test just to document the difference + assertExceptionMessageForMisconfiguredFactoryMethod( + rootBeanDefinition(Foo.class). + setFactoryMethod("singleArgFactory").getBeanDefinition(), + "Error creating bean with name 'foo': " + + "Unsatisfied dependency expressed through method 'singleArgFactory' parameter 0: " + + "Ambiguous argument values for parameter of type [java.lang.String] - " + + "did you specify the correct bean references as arguments?"); + } + + + static class Foo { + + static Foo noArgFactory() { + return new Foo(); + } + + static Foo singleArgFactory(String arg) { + return new Foo(); + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AnnotationBeanWiringInfoResolverTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AnnotationBeanWiringInfoResolverTests.java new file mode 100644 index 0000000..4851c13 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AnnotationBeanWiringInfoResolverTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.annotation; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.wiring.BeanWiringInfo; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Rick Evans + * @author Chris Beams + */ +public class AnnotationBeanWiringInfoResolverTests { + + @Test + public void testResolveWiringInfo() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new AnnotationBeanWiringInfoResolver().resolveWiringInfo(null)); + } + + @Test + public void testResolveWiringInfoWithAnInstanceOfANonAnnotatedClass() { + AnnotationBeanWiringInfoResolver resolver = new AnnotationBeanWiringInfoResolver(); + BeanWiringInfo info = resolver.resolveWiringInfo("java.lang.String is not @Configurable"); + assertThat(info).as("Must be returning null for a non-@Configurable class instance").isNull(); + } + + @Test + public void testResolveWiringInfoWithAnInstanceOfAnAnnotatedClass() { + AnnotationBeanWiringInfoResolver resolver = new AnnotationBeanWiringInfoResolver(); + BeanWiringInfo info = resolver.resolveWiringInfo(new Soap()); + assertThat(info).as("Must *not* be returning null for a non-@Configurable class instance").isNotNull(); + } + + @Test + public void testResolveWiringInfoWithAnInstanceOfAnAnnotatedClassWithAutowiringTurnedOffExplicitly() { + AnnotationBeanWiringInfoResolver resolver = new AnnotationBeanWiringInfoResolver(); + BeanWiringInfo info = resolver.resolveWiringInfo(new WirelessSoap()); + assertThat(info).as("Must *not* be returning null for an @Configurable class instance even when autowiring is NO").isNotNull(); + assertThat(info.indicatesAutowiring()).isFalse(); + assertThat(info.getBeanName()).isEqualTo(WirelessSoap.class.getName()); + } + + @Test + public void testResolveWiringInfoWithAnInstanceOfAnAnnotatedClassWithAutowiringTurnedOffExplicitlyAndCustomBeanName() { + AnnotationBeanWiringInfoResolver resolver = new AnnotationBeanWiringInfoResolver(); + BeanWiringInfo info = resolver.resolveWiringInfo(new NamedWirelessSoap()); + assertThat(info).as("Must *not* be returning null for an @Configurable class instance even when autowiring is NO").isNotNull(); + assertThat(info.indicatesAutowiring()).isFalse(); + assertThat(info.getBeanName()).isEqualTo("DerBigStick"); + } + + + @Configurable() + private static class Soap { + } + + + @Configurable(autowire = Autowire.NO) + private static class WirelessSoap { + } + + + @Configurable(autowire = Autowire.NO, value = "DerBigStick") + private static class NamedWirelessSoap { + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java new file mode 100644 index 0000000..0493c7f --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java @@ -0,0 +1,3831 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.annotation; + +import java.io.Serializable; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InjectionPoint; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.UnsatisfiedDependencyException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.TypedStringValue; +import org.springframework.beans.factory.support.AutowireCandidateQualifier; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.IndexedTestBean; +import org.springframework.beans.testfixture.beans.NestedTestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.Ordered; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.annotation.Order; +import org.springframework.core.testfixture.io.SerializationTestUtils; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Juergen Hoeller + * @author Mark Fisher + * @author Sam Brannen + * @author Chris Beams + * @author Stephane Nicoll + */ +public class AutowiredAnnotationBeanPostProcessorTests { + + private DefaultListableBeanFactory bf; + + private AutowiredAnnotationBeanPostProcessor bpp; + + + @BeforeEach + public void setup() { + bf = new DefaultListableBeanFactory(); + bf.registerResolvableDependency(BeanFactory.class, bf); + bpp = new AutowiredAnnotationBeanPostProcessor(); + bpp.setBeanFactory(bf); + bf.addBeanPostProcessor(bpp); + bf.setAutowireCandidateResolver(new QualifierAnnotationAutowireCandidateResolver()); + bf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); + } + + @AfterEach + public void close() { + bf.destroySingletons(); + } + + + @Test + public void testIncompleteBeanDefinition() { + bf.registerBeanDefinition("testBean", new GenericBeanDefinition()); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + bf.getBean("testBean")) + .withRootCauseInstanceOf(IllegalStateException.class); + } + + @Test + public void testResourceInjection() { + RootBeanDefinition bd = new RootBeanDefinition(ResourceInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + + ResourceInjectionBean bean = (ResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + + bean = (ResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + } + + @Test + public void testExtendedResourceInjection() { + RootBeanDefinition bd = new RootBeanDefinition(TypedExtendedResourceInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + NestedTestBean ntb = new NestedTestBean(); + bf.registerSingleton("nestedTestBean", ntb); + + TypedExtendedResourceInjectionBean bean = (TypedExtendedResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBean()).isSameAs(ntb); + assertThat(bean.getBeanFactory()).isSameAs(bf); + + bean = (TypedExtendedResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBean()).isSameAs(ntb); + assertThat(bean.getBeanFactory()).isSameAs(bf); + + String[] depBeans = bf.getDependenciesForBean("annotatedBean"); + assertThat(depBeans.length).isEqualTo(2); + assertThat(depBeans[0]).isEqualTo("testBean"); + assertThat(depBeans[1]).isEqualTo("nestedTestBean"); + } + + @Test + public void testExtendedResourceInjectionWithDestruction() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(TypedExtendedResourceInjectionBean.class)); + bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); + NestedTestBean ntb = new NestedTestBean(); + bf.registerSingleton("nestedTestBean", ntb); + + TestBean tb = bf.getBean("testBean", TestBean.class); + TypedExtendedResourceInjectionBean bean = (TypedExtendedResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBean()).isSameAs(ntb); + assertThat(bean.getBeanFactory()).isSameAs(bf); + + assertThat(bf.getDependenciesForBean("annotatedBean")).isEqualTo(new String[] {"testBean", "nestedTestBean"}); + bf.destroySingleton("testBean"); + assertThat(bf.containsSingleton("testBean")).isFalse(); + assertThat(bf.containsSingleton("annotatedBean")).isFalse(); + assertThat(bean.destroyed).isTrue(); + assertThat(bf.getDependenciesForBean("annotatedBean").length).isSameAs(0); + } + + @Test + public void testExtendedResourceInjectionWithOverriding() { + RootBeanDefinition annotatedBd = new RootBeanDefinition(TypedExtendedResourceInjectionBean.class); + TestBean tb2 = new TestBean(); + annotatedBd.getPropertyValues().add("testBean2", tb2); + bf.registerBeanDefinition("annotatedBean", annotatedBd); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + NestedTestBean ntb = new NestedTestBean(); + bf.registerSingleton("nestedTestBean", ntb); + + TypedExtendedResourceInjectionBean bean = (TypedExtendedResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb2); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBean()).isSameAs(ntb); + assertThat(bean.getBeanFactory()).isSameAs(bf); + } + + @Test + public void testExtendedResourceInjectionWithSkippedOverriddenMethods() { + RootBeanDefinition annotatedBd = new RootBeanDefinition(OverriddenExtendedResourceInjectionBean.class); + bf.registerBeanDefinition("annotatedBean", annotatedBd); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + NestedTestBean ntb = new NestedTestBean(); + bf.registerSingleton("nestedTestBean", ntb); + + OverriddenExtendedResourceInjectionBean bean = (OverriddenExtendedResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isNull(); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBean()).isSameAs(ntb); + assertThat(bean.getBeanFactory()).isNull(); + assertThat(bean.baseInjected).isTrue(); + assertThat(bean.subInjected).isTrue(); + } + + @Test + public void testExtendedResourceInjectionWithDefaultMethod() { + RootBeanDefinition annotatedBd = new RootBeanDefinition(DefaultMethodResourceInjectionBean.class); + bf.registerBeanDefinition("annotatedBean", annotatedBd); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + NestedTestBean ntb = new NestedTestBean(); + bf.registerSingleton("nestedTestBean", ntb); + + DefaultMethodResourceInjectionBean bean = (DefaultMethodResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isNull(); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBean()).isSameAs(ntb); + assertThat(bean.getBeanFactory()).isNull(); + assertThat(bean.baseInjected).isTrue(); + assertThat(bean.subInjected).isTrue(); + } + + @Test + @SuppressWarnings("deprecation") + public void testExtendedResourceInjectionWithAtRequired() { + bf.addBeanPostProcessor(new RequiredAnnotationBeanPostProcessor()); + RootBeanDefinition bd = new RootBeanDefinition(TypedExtendedResourceInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + NestedTestBean ntb = new NestedTestBean(); + bf.registerSingleton("nestedTestBean", ntb); + + TypedExtendedResourceInjectionBean bean = (TypedExtendedResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBean()).isSameAs(ntb); + assertThat(bean.getBeanFactory()).isSameAs(bf); + } + + @Test + public void testOptionalResourceInjection() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(OptionalResourceInjectionBean.class)); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + IndexedTestBean itb = new IndexedTestBean(); + bf.registerSingleton("indexedTestBean", itb); + NestedTestBean ntb1 = new NestedTestBean(); + bf.registerSingleton("nestedTestBean1", ntb1); + NestedTestBean ntb2 = new NestedTestBean(); + bf.registerSingleton("nestedTestBean2", ntb2); + + OptionalResourceInjectionBean bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getIndexedTestBean()).isSameAs(itb); + assertThat(bean.getNestedTestBeans().length).isEqualTo(2); + assertThat(bean.getNestedTestBeans()[0]).isSameAs(ntb1); + assertThat(bean.getNestedTestBeans()[1]).isSameAs(ntb2); + assertThat(bean.nestedTestBeansField.length).isEqualTo(2); + assertThat(bean.nestedTestBeansField[0]).isSameAs(ntb1); + assertThat(bean.nestedTestBeansField[1]).isSameAs(ntb2); + } + + @Test + public void testOptionalCollectionResourceInjection() { + RootBeanDefinition rbd = new RootBeanDefinition(OptionalCollectionResourceInjectionBean.class); + rbd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", rbd); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + IndexedTestBean itb = new IndexedTestBean(); + bf.registerSingleton("indexedTestBean", itb); + NestedTestBean ntb1 = new NestedTestBean(); + bf.registerSingleton("nestedTestBean1", ntb1); + NestedTestBean ntb2 = new NestedTestBean(); + bf.registerSingleton("nestedTestBean2", ntb2); + + // Two calls to verify that caching doesn't break re-creation. + OptionalCollectionResourceInjectionBean bean = (OptionalCollectionResourceInjectionBean) bf.getBean("annotatedBean"); + bean = (OptionalCollectionResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getIndexedTestBean()).isSameAs(itb); + assertThat(bean.getNestedTestBeans().size()).isEqualTo(2); + assertThat(bean.getNestedTestBeans().get(0)).isSameAs(ntb1); + assertThat(bean.getNestedTestBeans().get(1)).isSameAs(ntb2); + assertThat(bean.nestedTestBeansSetter.size()).isEqualTo(2); + assertThat(bean.nestedTestBeansSetter.get(0)).isSameAs(ntb1); + assertThat(bean.nestedTestBeansSetter.get(1)).isSameAs(ntb2); + assertThat(bean.nestedTestBeansField.size()).isEqualTo(2); + assertThat(bean.nestedTestBeansField.get(0)).isSameAs(ntb1); + assertThat(bean.nestedTestBeansField.get(1)).isSameAs(ntb2); + } + + @Test + public void testOptionalCollectionResourceInjectionWithSingleElement() { + RootBeanDefinition rbd = new RootBeanDefinition(OptionalCollectionResourceInjectionBean.class); + rbd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", rbd); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + IndexedTestBean itb = new IndexedTestBean(); + bf.registerSingleton("indexedTestBean", itb); + NestedTestBean ntb1 = new NestedTestBean(); + bf.registerSingleton("nestedTestBean1", ntb1); + + // Two calls to verify that caching doesn't break re-creation. + OptionalCollectionResourceInjectionBean bean = (OptionalCollectionResourceInjectionBean) bf.getBean("annotatedBean"); + bean = (OptionalCollectionResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getIndexedTestBean()).isSameAs(itb); + assertThat(bean.getNestedTestBeans().size()).isEqualTo(1); + assertThat(bean.getNestedTestBeans().get(0)).isSameAs(ntb1); + assertThat(bean.nestedTestBeansSetter.size()).isEqualTo(1); + assertThat(bean.nestedTestBeansSetter.get(0)).isSameAs(ntb1); + assertThat(bean.nestedTestBeansField.size()).isEqualTo(1); + assertThat(bean.nestedTestBeansField.get(0)).isSameAs(ntb1); + } + + @Test + public void testOptionalResourceInjectionWithIncompleteDependencies() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(OptionalResourceInjectionBean.class)); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + + OptionalResourceInjectionBean bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isNull(); + assertThat(bean.getNestedTestBeans()).isNull(); + } + + @Test + public void testOptionalResourceInjectionWithNoDependencies() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(OptionalResourceInjectionBean.class)); + + OptionalResourceInjectionBean bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isNull(); + assertThat(bean.getTestBean2()).isNull(); + assertThat(bean.getTestBean3()).isNull(); + assertThat(bean.getTestBean4()).isNull(); + assertThat(bean.getNestedTestBeans()).isNull(); + } + + @Test + public void testOrderedResourceInjection() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(OptionalResourceInjectionBean.class)); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + IndexedTestBean itb = new IndexedTestBean(); + bf.registerSingleton("indexedTestBean", itb); + OrderedNestedTestBean ntb1 = new OrderedNestedTestBean(); + ntb1.setOrder(2); + bf.registerSingleton("nestedTestBean1", ntb1); + OrderedNestedTestBean ntb2 = new OrderedNestedTestBean(); + ntb2.setOrder(1); + bf.registerSingleton("nestedTestBean2", ntb2); + + OptionalResourceInjectionBean bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getIndexedTestBean()).isSameAs(itb); + assertThat(bean.getNestedTestBeans().length).isEqualTo(2); + assertThat(bean.getNestedTestBeans()[0]).isSameAs(ntb2); + assertThat(bean.getNestedTestBeans()[1]).isSameAs(ntb1); + assertThat(bean.nestedTestBeansField.length).isEqualTo(2); + assertThat(bean.nestedTestBeansField[0]).isSameAs(ntb2); + assertThat(bean.nestedTestBeansField[1]).isSameAs(ntb1); + } + + @Test + public void testAnnotationOrderedResourceInjection() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(OptionalResourceInjectionBean.class)); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + IndexedTestBean itb = new IndexedTestBean(); + bf.registerSingleton("indexedTestBean", itb); + FixedOrder2NestedTestBean ntb1 = new FixedOrder2NestedTestBean(); + bf.registerSingleton("nestedTestBean1", ntb1); + FixedOrder1NestedTestBean ntb2 = new FixedOrder1NestedTestBean(); + bf.registerSingleton("nestedTestBean2", ntb2); + + OptionalResourceInjectionBean bean = (OptionalResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getIndexedTestBean()).isSameAs(itb); + assertThat(bean.getNestedTestBeans().length).isEqualTo(2); + assertThat(bean.getNestedTestBeans()[0]).isSameAs(ntb2); + assertThat(bean.getNestedTestBeans()[1]).isSameAs(ntb1); + assertThat(bean.nestedTestBeansField.length).isEqualTo(2); + assertThat(bean.nestedTestBeansField[0]).isSameAs(ntb2); + assertThat(bean.nestedTestBeansField[1]).isSameAs(ntb1); + } + + @Test + public void testOrderedCollectionResourceInjection() { + RootBeanDefinition rbd = new RootBeanDefinition(OptionalCollectionResourceInjectionBean.class); + rbd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", rbd); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + IndexedTestBean itb = new IndexedTestBean(); + bf.registerSingleton("indexedTestBean", itb); + OrderedNestedTestBean ntb1 = new OrderedNestedTestBean(); + ntb1.setOrder(2); + bf.registerSingleton("nestedTestBean1", ntb1); + OrderedNestedTestBean ntb2 = new OrderedNestedTestBean(); + ntb2.setOrder(1); + bf.registerSingleton("nestedTestBean2", ntb2); + + // Two calls to verify that caching doesn't break re-creation. + OptionalCollectionResourceInjectionBean bean = (OptionalCollectionResourceInjectionBean) bf.getBean("annotatedBean"); + bean = (OptionalCollectionResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getIndexedTestBean()).isSameAs(itb); + assertThat(bean.getNestedTestBeans().size()).isEqualTo(2); + assertThat(bean.getNestedTestBeans().get(0)).isSameAs(ntb2); + assertThat(bean.getNestedTestBeans().get(1)).isSameAs(ntb1); + assertThat(bean.nestedTestBeansSetter.size()).isEqualTo(2); + assertThat(bean.nestedTestBeansSetter.get(0)).isSameAs(ntb2); + assertThat(bean.nestedTestBeansSetter.get(1)).isSameAs(ntb1); + assertThat(bean.nestedTestBeansField.size()).isEqualTo(2); + assertThat(bean.nestedTestBeansField.get(0)).isSameAs(ntb2); + assertThat(bean.nestedTestBeansField.get(1)).isSameAs(ntb1); + } + + @Test + public void testAnnotationOrderedCollectionResourceInjection() { + RootBeanDefinition rbd = new RootBeanDefinition(OptionalCollectionResourceInjectionBean.class); + rbd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", rbd); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + IndexedTestBean itb = new IndexedTestBean(); + bf.registerSingleton("indexedTestBean", itb); + FixedOrder2NestedTestBean ntb1 = new FixedOrder2NestedTestBean(); + bf.registerSingleton("nestedTestBean1", ntb1); + FixedOrder1NestedTestBean ntb2 = new FixedOrder1NestedTestBean(); + bf.registerSingleton("nestedTestBean2", ntb2); + + // Two calls to verify that caching doesn't break re-creation. + OptionalCollectionResourceInjectionBean bean = (OptionalCollectionResourceInjectionBean) bf.getBean("annotatedBean"); + bean = (OptionalCollectionResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getIndexedTestBean()).isSameAs(itb); + assertThat(bean.getNestedTestBeans().size()).isEqualTo(2); + assertThat(bean.getNestedTestBeans().get(0)).isSameAs(ntb2); + assertThat(bean.getNestedTestBeans().get(1)).isSameAs(ntb1); + assertThat(bean.nestedTestBeansSetter.size()).isEqualTo(2); + assertThat(bean.nestedTestBeansSetter.get(0)).isSameAs(ntb2); + assertThat(bean.nestedTestBeansSetter.get(1)).isSameAs(ntb1); + assertThat(bean.nestedTestBeansField.size()).isEqualTo(2); + assertThat(bean.nestedTestBeansField.get(0)).isSameAs(ntb2); + assertThat(bean.nestedTestBeansField.get(1)).isSameAs(ntb1); + } + + @Test + public void testConstructorResourceInjection() { + RootBeanDefinition bd = new RootBeanDefinition(ConstructorResourceInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + NestedTestBean ntb = new NestedTestBean(); + bf.registerSingleton("nestedTestBean", ntb); + + ConstructorResourceInjectionBean bean = (ConstructorResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBean()).isSameAs(ntb); + assertThat(bean.getBeanFactory()).isSameAs(bf); + + bean = (ConstructorResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBean()).isSameAs(ntb); + assertThat(bean.getBeanFactory()).isSameAs(bf); + } + + @Test + public void testConstructorResourceInjectionWithNullFromFactoryBean() { + RootBeanDefinition bd = new RootBeanDefinition(ConstructorResourceInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + bf.registerBeanDefinition("nestedTestBean", new RootBeanDefinition(NullNestedTestBeanFactoryBean.class)); + bf.registerSingleton("nestedTestBean2", new NestedTestBean()); + + ConstructorResourceInjectionBean bean = (ConstructorResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBean()).isNull(); + assertThat(bean.getBeanFactory()).isSameAs(bf); + + bean = (ConstructorResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBean()).isNull(); + assertThat(bean.getBeanFactory()).isSameAs(bf); + } + + @Test + public void testConstructorResourceInjectionWithNullFromFactoryMethod() { + RootBeanDefinition bd = new RootBeanDefinition(ConstructorResourceInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + RootBeanDefinition tb = new RootBeanDefinition(NullFactoryMethods.class); + tb.setFactoryMethodName("createTestBean"); + bf.registerBeanDefinition("testBean", tb); + RootBeanDefinition ntb = new RootBeanDefinition(NullFactoryMethods.class); + ntb.setFactoryMethodName("createNestedTestBean"); + bf.registerBeanDefinition("nestedTestBean", ntb); + bf.registerSingleton("nestedTestBean2", new NestedTestBean()); + + ConstructorResourceInjectionBean bean = (ConstructorResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isNull(); + assertThat(bean.getTestBean2()).isNull(); + assertThat(bean.getTestBean3()).isNull(); + assertThat(bean.getTestBean4()).isNull(); + assertThat(bean.getNestedTestBean()).isNull(); + assertThat(bean.getBeanFactory()).isSameAs(bf); + + bean = (ConstructorResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isNull(); + assertThat(bean.getTestBean2()).isNull(); + assertThat(bean.getTestBean3()).isNull(); + assertThat(bean.getTestBean4()).isNull(); + assertThat(bean.getNestedTestBean()).isNull(); + assertThat(bean.getBeanFactory()).isSameAs(bf); + } + + @Test + public void testConstructorResourceInjectionWithMultipleCandidates() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ConstructorsResourceInjectionBean.class)); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + NestedTestBean ntb1 = new NestedTestBean(); + bf.registerSingleton("nestedTestBean1", ntb1); + NestedTestBean ntb2 = new NestedTestBean(); + bf.registerSingleton("nestedTestBean2", ntb2); + + ConstructorsResourceInjectionBean bean = (ConstructorsResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNull(); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBeans().length).isEqualTo(2); + assertThat(bean.getNestedTestBeans()[0]).isSameAs(ntb1); + assertThat(bean.getNestedTestBeans()[1]).isSameAs(ntb2); + } + + @Test + public void testConstructorResourceInjectionWithNoCandidatesAndNoFallback() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ConstructorWithoutFallbackBean.class)); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> + bf.getBean("annotatedBean")) + .satisfies(methodParameterDeclaredOn(ConstructorWithoutFallbackBean.class)); + } + + @Test + public void testConstructorResourceInjectionWithCollectionAndNullFromFactoryBean() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition( + ConstructorsCollectionResourceInjectionBean.class)); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + bf.registerBeanDefinition("nestedTestBean1", new RootBeanDefinition(NullNestedTestBeanFactoryBean.class)); + NestedTestBean ntb2 = new NestedTestBean(); + bf.registerSingleton("nestedTestBean2", ntb2); + + ConstructorsCollectionResourceInjectionBean bean = (ConstructorsCollectionResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNull(); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBeans().size()).isEqualTo(1); + assertThat(bean.getNestedTestBeans().get(0)).isSameAs(ntb2); + + Map map = bf.getBeansOfType(NestedTestBean.class); + assertThat(map.get("nestedTestBean1")).isNull(); + assertThat(map.get("nestedTestBean2")).isSameAs(ntb2); + } + + @Test + public void testConstructorResourceInjectionWithMultipleCandidatesAsCollection() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition( + ConstructorsCollectionResourceInjectionBean.class)); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + NestedTestBean ntb1 = new NestedTestBean(); + bf.registerSingleton("nestedTestBean1", ntb1); + NestedTestBean ntb2 = new NestedTestBean(); + bf.registerSingleton("nestedTestBean2", ntb2); + + ConstructorsCollectionResourceInjectionBean bean = (ConstructorsCollectionResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNull(); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBeans().size()).isEqualTo(2); + assertThat(bean.getNestedTestBeans().get(0)).isSameAs(ntb1); + assertThat(bean.getNestedTestBeans().get(1)).isSameAs(ntb2); + } + + @Test + public void testConstructorResourceInjectionWithMultipleOrderedCandidates() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ConstructorsResourceInjectionBean.class)); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + FixedOrder2NestedTestBean ntb1 = new FixedOrder2NestedTestBean(); + bf.registerSingleton("nestedTestBean1", ntb1); + FixedOrder1NestedTestBean ntb2 = new FixedOrder1NestedTestBean(); + bf.registerSingleton("nestedTestBean2", ntb2); + + ConstructorsResourceInjectionBean bean = (ConstructorsResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNull(); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBeans().length).isEqualTo(2); + assertThat(bean.getNestedTestBeans()[0]).isSameAs(ntb2); + assertThat(bean.getNestedTestBeans()[1]).isSameAs(ntb1); + } + + @Test + public void testConstructorResourceInjectionWithMultipleCandidatesAsOrderedCollection() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ConstructorsCollectionResourceInjectionBean.class)); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + FixedOrder2NestedTestBean ntb1 = new FixedOrder2NestedTestBean(); + bf.registerSingleton("nestedTestBean1", ntb1); + FixedOrder1NestedTestBean ntb2 = new FixedOrder1NestedTestBean(); + bf.registerSingleton("nestedTestBean2", ntb2); + + ConstructorsCollectionResourceInjectionBean bean = (ConstructorsCollectionResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNull(); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBeans().size()).isEqualTo(2); + assertThat(bean.getNestedTestBeans().get(0)).isSameAs(ntb2); + assertThat(bean.getNestedTestBeans().get(1)).isSameAs(ntb1); + } + + @Test + public void testSingleConstructorInjectionWithMultipleCandidatesAsRequiredVararg() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(SingleConstructorVarargBean.class)); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + FixedOrder2NestedTestBean ntb1 = new FixedOrder2NestedTestBean(); + bf.registerSingleton("nestedTestBean1", ntb1); + FixedOrder1NestedTestBean ntb2 = new FixedOrder1NestedTestBean(); + bf.registerSingleton("nestedTestBean2", ntb2); + + SingleConstructorVarargBean bean = (SingleConstructorVarargBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getNestedTestBeans().size()).isEqualTo(2); + assertThat(bean.getNestedTestBeans().get(0)).isSameAs(ntb2); + assertThat(bean.getNestedTestBeans().get(1)).isSameAs(ntb1); + } + + @Test + public void testSingleConstructorInjectionWithEmptyVararg() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(SingleConstructorVarargBean.class)); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + + SingleConstructorVarargBean bean = (SingleConstructorVarargBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getNestedTestBeans()).isNotNull(); + assertThat(bean.getNestedTestBeans().isEmpty()).isTrue(); + } + + @Test + public void testSingleConstructorInjectionWithMultipleCandidatesAsRequiredCollection() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(SingleConstructorRequiredCollectionBean.class)); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + FixedOrder2NestedTestBean ntb1 = new FixedOrder2NestedTestBean(); + bf.registerSingleton("nestedTestBean1", ntb1); + FixedOrder1NestedTestBean ntb2 = new FixedOrder1NestedTestBean(); + bf.registerSingleton("nestedTestBean2", ntb2); + + SingleConstructorRequiredCollectionBean bean = (SingleConstructorRequiredCollectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getNestedTestBeans().size()).isEqualTo(2); + assertThat(bean.getNestedTestBeans().get(0)).isSameAs(ntb2); + assertThat(bean.getNestedTestBeans().get(1)).isSameAs(ntb1); + } + + @Test + public void testSingleConstructorInjectionWithEmptyCollection() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(SingleConstructorRequiredCollectionBean.class)); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + + SingleConstructorRequiredCollectionBean bean = (SingleConstructorRequiredCollectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getNestedTestBeans()).isNotNull(); + assertThat(bean.getNestedTestBeans().isEmpty()).isTrue(); + } + + @Test + public void testSingleConstructorInjectionWithMultipleCandidatesAsOrderedCollection() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(SingleConstructorOptionalCollectionBean.class)); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + FixedOrder2NestedTestBean ntb1 = new FixedOrder2NestedTestBean(); + bf.registerSingleton("nestedTestBean1", ntb1); + FixedOrder1NestedTestBean ntb2 = new FixedOrder1NestedTestBean(); + bf.registerSingleton("nestedTestBean2", ntb2); + + SingleConstructorOptionalCollectionBean bean = (SingleConstructorOptionalCollectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getNestedTestBeans().size()).isEqualTo(2); + assertThat(bean.getNestedTestBeans().get(0)).isSameAs(ntb2); + assertThat(bean.getNestedTestBeans().get(1)).isSameAs(ntb1); + } + + @Test + public void testSingleConstructorInjectionWithEmptyCollectionAsNull() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(SingleConstructorOptionalCollectionBean.class)); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + + SingleConstructorOptionalCollectionBean bean = (SingleConstructorOptionalCollectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getNestedTestBeans()).isNull(); + } + + @Test + public void testSingleConstructorInjectionWithMissingDependency() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(SingleConstructorOptionalCollectionBean.class)); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> + bf.getBean("annotatedBean")); + } + + @Test + public void testSingleConstructorInjectionWithNullDependency() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(SingleConstructorOptionalCollectionBean.class)); + RootBeanDefinition tb = new RootBeanDefinition(NullFactoryMethods.class); + tb.setFactoryMethodName("createTestBean"); + bf.registerBeanDefinition("testBean", tb); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> + bf.getBean("annotatedBean")); + } + + @Test + public void testConstructorResourceInjectionWithMultipleCandidatesAndFallback() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ConstructorsResourceInjectionBean.class)); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + + ConstructorsResourceInjectionBean bean = (ConstructorsResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isNull(); + } + + @Test + public void testConstructorResourceInjectionWithMultipleCandidatesAndDefaultFallback() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ConstructorsResourceInjectionBean.class)); + + ConstructorsResourceInjectionBean bean = (ConstructorsResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNull(); + assertThat(bean.getTestBean4()).isNull(); + } + + @Test + public void testConstructorInjectionWithMap() { + RootBeanDefinition bd = new RootBeanDefinition(MapConstructorInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + TestBean tb1 = new TestBean("tb1"); + bf.registerSingleton("testBean1", tb1); + RootBeanDefinition tb2 = new RootBeanDefinition(NullFactoryMethods.class); + tb2.setFactoryMethodName("createTestBean"); + bf.registerBeanDefinition("testBean2", tb2); + + MapConstructorInjectionBean bean = (MapConstructorInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBeanMap().size()).isEqualTo(1); + assertThat(bean.getTestBeanMap().get("testBean1")).isSameAs(tb1); + assertThat(bean.getTestBeanMap().get("testBean2")).isNull(); + + bean = (MapConstructorInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBeanMap().size()).isEqualTo(1); + assertThat(bean.getTestBeanMap().get("testBean1")).isSameAs(tb1); + assertThat(bean.getTestBeanMap().get("testBean2")).isNull(); + } + + @Test + public void testFieldInjectionWithMap() { + RootBeanDefinition bd = new RootBeanDefinition(MapFieldInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + TestBean tb1 = new TestBean("tb1"); + TestBean tb2 = new TestBean("tb2"); + bf.registerSingleton("testBean1", tb1); + bf.registerSingleton("testBean2", tb2); + + MapFieldInjectionBean bean = (MapFieldInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBeanMap().size()).isEqualTo(2); + assertThat(bean.getTestBeanMap().keySet().contains("testBean1")).isTrue(); + assertThat(bean.getTestBeanMap().keySet().contains("testBean2")).isTrue(); + assertThat(bean.getTestBeanMap().values().contains(tb1)).isTrue(); + assertThat(bean.getTestBeanMap().values().contains(tb2)).isTrue(); + + bean = (MapFieldInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBeanMap().size()).isEqualTo(2); + assertThat(bean.getTestBeanMap().keySet().contains("testBean1")).isTrue(); + assertThat(bean.getTestBeanMap().keySet().contains("testBean2")).isTrue(); + assertThat(bean.getTestBeanMap().values().contains(tb1)).isTrue(); + assertThat(bean.getTestBeanMap().values().contains(tb2)).isTrue(); + } + + @Test + public void testMethodInjectionWithMap() { + RootBeanDefinition bd = new RootBeanDefinition(MapMethodInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + + MapMethodInjectionBean bean = (MapMethodInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBeanMap().size()).isEqualTo(1); + assertThat(bean.getTestBeanMap().keySet().contains("testBean")).isTrue(); + assertThat(bean.getTestBeanMap().values().contains(tb)).isTrue(); + assertThat(bean.getTestBean()).isSameAs(tb); + + bean = (MapMethodInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBeanMap().size()).isEqualTo(1); + assertThat(bean.getTestBeanMap().keySet().contains("testBean")).isTrue(); + assertThat(bean.getTestBeanMap().values().contains(tb)).isTrue(); + assertThat(bean.getTestBean()).isSameAs(tb); + } + + @Test + public void testMethodInjectionWithMapAndMultipleMatches() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(MapMethodInjectionBean.class)); + bf.registerBeanDefinition("testBean1", new RootBeanDefinition(TestBean.class)); + bf.registerBeanDefinition("testBean2", new RootBeanDefinition(TestBean.class)); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).as("should have failed, more than one bean of type").isThrownBy(() -> + bf.getBean("annotatedBean")) + .satisfies(methodParameterDeclaredOn(MapMethodInjectionBean.class)); + } + + @Test + public void testMethodInjectionWithMapAndMultipleMatchesButOnlyOneAutowireCandidate() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(MapMethodInjectionBean.class)); + bf.registerBeanDefinition("testBean1", new RootBeanDefinition(TestBean.class)); + RootBeanDefinition rbd2 = new RootBeanDefinition(TestBean.class); + rbd2.setAutowireCandidate(false); + bf.registerBeanDefinition("testBean2", rbd2); + + MapMethodInjectionBean bean = (MapMethodInjectionBean) bf.getBean("annotatedBean"); + TestBean tb = (TestBean) bf.getBean("testBean1"); + assertThat(bean.getTestBeanMap().size()).isEqualTo(1); + assertThat(bean.getTestBeanMap().keySet().contains("testBean1")).isTrue(); + assertThat(bean.getTestBeanMap().values().contains(tb)).isTrue(); + assertThat(bean.getTestBean()).isSameAs(tb); + } + + @Test + public void testMethodInjectionWithMapAndNoMatches() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(MapMethodInjectionBean.class)); + + MapMethodInjectionBean bean = (MapMethodInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBeanMap()).isNull(); + assertThat(bean.getTestBean()).isNull(); + } + + @Test + public void testConstructorInjectionWithTypedMapAsBean() { + RootBeanDefinition bd = new RootBeanDefinition(MapConstructorInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + MyTestBeanMap tbm = new MyTestBeanMap(); + tbm.put("testBean1", new TestBean("tb1")); + tbm.put("testBean2", new TestBean("tb2")); + bf.registerSingleton("testBeans", tbm); + bf.registerSingleton("otherMap", new Properties()); + + MapConstructorInjectionBean bean = (MapConstructorInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBeanMap()).isSameAs(tbm); + bean = (MapConstructorInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBeanMap()).isSameAs(tbm); + } + + @Test + public void testConstructorInjectionWithPlainMapAsBean() { + RootBeanDefinition bd = new RootBeanDefinition(MapConstructorInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + RootBeanDefinition tbm = new RootBeanDefinition(CollectionFactoryMethods.class); + tbm.setUniqueFactoryMethodName("testBeanMap"); + bf.registerBeanDefinition("myTestBeanMap", tbm); + bf.registerSingleton("otherMap", new HashMap<>()); + + MapConstructorInjectionBean bean = (MapConstructorInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("myTestBeanMap")); + bean = (MapConstructorInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("myTestBeanMap")); + } + + @Test + public void testConstructorInjectionWithCustomMapAsBean() { + RootBeanDefinition bd = new RootBeanDefinition(CustomMapConstructorInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + RootBeanDefinition tbm = new RootBeanDefinition(CustomCollectionFactoryMethods.class); + tbm.setUniqueFactoryMethodName("testBeanMap"); + bf.registerBeanDefinition("myTestBeanMap", tbm); + bf.registerSingleton("testBean1", new TestBean()); + bf.registerSingleton("testBean2", new TestBean()); + + CustomMapConstructorInjectionBean bean = (CustomMapConstructorInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("myTestBeanMap")); + bean = (CustomMapConstructorInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("myTestBeanMap")); + } + + @Test + public void testConstructorInjectionWithPlainHashMapAsBean() { + RootBeanDefinition bd = new RootBeanDefinition(QualifiedMapConstructorInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + bf.registerBeanDefinition("myTestBeanMap", new RootBeanDefinition(HashMap.class)); + + QualifiedMapConstructorInjectionBean bean = (QualifiedMapConstructorInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("myTestBeanMap")); + bean = (QualifiedMapConstructorInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("myTestBeanMap")); + } + + @Test + public void testConstructorInjectionWithTypedSetAsBean() { + RootBeanDefinition bd = new RootBeanDefinition(SetConstructorInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + MyTestBeanSet tbs = new MyTestBeanSet(); + tbs.add(new TestBean("tb1")); + tbs.add(new TestBean("tb2")); + bf.registerSingleton("testBeans", tbs); + bf.registerSingleton("otherSet", new HashSet<>()); + + SetConstructorInjectionBean bean = (SetConstructorInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBeanSet()).isSameAs(tbs); + bean = (SetConstructorInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBeanSet()).isSameAs(tbs); + } + + @Test + public void testConstructorInjectionWithPlainSetAsBean() { + RootBeanDefinition bd = new RootBeanDefinition(SetConstructorInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + RootBeanDefinition tbs = new RootBeanDefinition(CollectionFactoryMethods.class); + tbs.setUniqueFactoryMethodName("testBeanSet"); + bf.registerBeanDefinition("myTestBeanSet", tbs); + bf.registerSingleton("otherSet", new HashSet<>()); + + SetConstructorInjectionBean bean = (SetConstructorInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBeanSet()).isSameAs(bf.getBean("myTestBeanSet")); + bean = (SetConstructorInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBeanSet()).isSameAs(bf.getBean("myTestBeanSet")); + } + + @Test + public void testConstructorInjectionWithCustomSetAsBean() { + RootBeanDefinition bd = new RootBeanDefinition(CustomSetConstructorInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + RootBeanDefinition tbs = new RootBeanDefinition(CustomCollectionFactoryMethods.class); + tbs.setUniqueFactoryMethodName("testBeanSet"); + bf.registerBeanDefinition("myTestBeanSet", tbs); + + CustomSetConstructorInjectionBean bean = (CustomSetConstructorInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBeanSet()).isSameAs(bf.getBean("myTestBeanSet")); + bean = (CustomSetConstructorInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBeanSet()).isSameAs(bf.getBean("myTestBeanSet")); + } + + @Test + public void testSelfReference() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(SelfInjectionBean.class)); + + SelfInjectionBean bean = (SelfInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.reference).isSameAs(bean); + assertThat(bean.referenceCollection).isNull(); + } + + @Test + public void testSelfReferenceWithOther() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(SelfInjectionBean.class)); + bf.registerBeanDefinition("annotatedBean2", new RootBeanDefinition(SelfInjectionBean.class)); + + SelfInjectionBean bean = (SelfInjectionBean) bf.getBean("annotatedBean"); + SelfInjectionBean bean2 = (SelfInjectionBean) bf.getBean("annotatedBean2"); + assertThat(bean.reference).isSameAs(bean2); + assertThat(bean.referenceCollection.size()).isEqualTo(1); + assertThat(bean.referenceCollection.get(0)).isSameAs(bean2); + } + + @Test + public void testSelfReferenceCollection() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(SelfInjectionCollectionBean.class)); + + SelfInjectionCollectionBean bean = (SelfInjectionCollectionBean) bf.getBean("annotatedBean"); + assertThat(bean.reference).isSameAs(bean); + assertThat(bean.referenceCollection).isNull(); + } + + @Test + public void testSelfReferenceCollectionWithOther() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(SelfInjectionCollectionBean.class)); + bf.registerBeanDefinition("annotatedBean2", new RootBeanDefinition(SelfInjectionCollectionBean.class)); + + SelfInjectionCollectionBean bean = (SelfInjectionCollectionBean) bf.getBean("annotatedBean"); + SelfInjectionCollectionBean bean2 = (SelfInjectionCollectionBean) bf.getBean("annotatedBean2"); + assertThat(bean.reference).isSameAs(bean2); + assertThat(bean2.referenceCollection.size()).isSameAs(1); + assertThat(bean.referenceCollection.get(0)).isSameAs(bean2); + } + + @Test + public void testObjectFactoryFieldInjection() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryFieldInjectionBean.class)); + bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); + + ObjectFactoryFieldInjectionBean bean = (ObjectFactoryFieldInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); + } + + @Test + public void testObjectFactoryConstructorInjection() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryConstructorInjectionBean.class)); + bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); + + ObjectFactoryConstructorInjectionBean bean = (ObjectFactoryConstructorInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); + } + + @Test + public void testObjectFactoryInjectionIntoPrototypeBean() { + RootBeanDefinition annotatedBeanDefinition = new RootBeanDefinition(ObjectFactoryFieldInjectionBean.class); + annotatedBeanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", annotatedBeanDefinition); + bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); + + ObjectFactoryFieldInjectionBean bean = (ObjectFactoryFieldInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); + ObjectFactoryFieldInjectionBean anotherBean = (ObjectFactoryFieldInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean).isNotSameAs(anotherBean); + assertThat(anotherBean.getTestBean()).isSameAs(bf.getBean("testBean")); + } + + @Test + public void testObjectFactoryQualifierInjection() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryQualifierInjectionBean.class)); + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.addQualifier(new AutowireCandidateQualifier(Qualifier.class, "testBean")); + bf.registerBeanDefinition("dependencyBean", bd); + bf.registerBeanDefinition("dependencyBean2", new RootBeanDefinition(TestBean.class)); + + ObjectFactoryQualifierInjectionBean bean = (ObjectFactoryQualifierInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("dependencyBean")); + } + + @Test + public void testObjectFactoryQualifierProviderInjection() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryQualifierInjectionBean.class)); + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.setQualifiedElement(ReflectionUtils.findMethod(getClass(), "testBeanQualifierProvider")); + bf.registerBeanDefinition("dependencyBean", bd); + bf.registerBeanDefinition("dependencyBean2", new RootBeanDefinition(TestBean.class)); + + ObjectFactoryQualifierInjectionBean bean = (ObjectFactoryQualifierInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("dependencyBean")); + } + + @Test + public void testObjectFactorySerialization() throws Exception { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryFieldInjectionBean.class)); + bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); + bf.setSerializationId("test"); + + ObjectFactoryFieldInjectionBean bean = (ObjectFactoryFieldInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); + bean = SerializationTestUtils.serializeAndDeserialize(bean); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); + } + + @Test + public void testObjectProviderInjectionWithPrototype() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectProviderInjectionBean.class)); + RootBeanDefinition tbd = new RootBeanDefinition(TestBean.class); + tbd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("testBean", tbd); + + ObjectProviderInjectionBean bean = (ObjectProviderInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isEqualTo(bf.getBean("testBean")); + assertThat(bean.getTestBean("myName")).isEqualTo(bf.getBean("testBean", "myName")); + assertThat(bean.getOptionalTestBean()).isEqualTo(bf.getBean("testBean")); + assertThat(bean.getOptionalTestBeanWithDefault()).isEqualTo(bf.getBean("testBean")); + assertThat(bean.consumeOptionalTestBean()).isEqualTo(bf.getBean("testBean")); + assertThat(bean.getUniqueTestBean()).isEqualTo(bf.getBean("testBean")); + assertThat(bean.getUniqueTestBeanWithDefault()).isEqualTo(bf.getBean("testBean")); + assertThat(bean.consumeUniqueTestBean()).isEqualTo(bf.getBean("testBean")); + + List testBeans = bean.iterateTestBeans(); + assertThat(testBeans.size()).isEqualTo(1); + assertThat(testBeans.contains(bf.getBean("testBean"))).isTrue(); + testBeans = bean.forEachTestBeans(); + assertThat(testBeans.size()).isEqualTo(1); + assertThat(testBeans.contains(bf.getBean("testBean"))).isTrue(); + testBeans = bean.streamTestBeans(); + assertThat(testBeans.size()).isEqualTo(1); + assertThat(testBeans.contains(bf.getBean("testBean"))).isTrue(); + testBeans = bean.sortedTestBeans(); + assertThat(testBeans.size()).isEqualTo(1); + assertThat(testBeans.contains(bf.getBean("testBean"))).isTrue(); + } + + @Test + public void testObjectProviderInjectionWithSingletonTarget() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectProviderInjectionBean.class)); + bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); + + ObjectProviderInjectionBean bean = (ObjectProviderInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); + assertThat(bean.getOptionalTestBean()).isSameAs(bf.getBean("testBean")); + assertThat(bean.getOptionalTestBeanWithDefault()).isSameAs(bf.getBean("testBean")); + assertThat(bean.consumeOptionalTestBean()).isEqualTo(bf.getBean("testBean")); + assertThat(bean.getUniqueTestBean()).isSameAs(bf.getBean("testBean")); + assertThat(bean.getUniqueTestBeanWithDefault()).isSameAs(bf.getBean("testBean")); + assertThat(bean.consumeUniqueTestBean()).isEqualTo(bf.getBean("testBean")); + + List testBeans = bean.iterateTestBeans(); + assertThat(testBeans.size()).isEqualTo(1); + assertThat(testBeans.contains(bf.getBean("testBean"))).isTrue(); + testBeans = bean.forEachTestBeans(); + assertThat(testBeans.size()).isEqualTo(1); + assertThat(testBeans.contains(bf.getBean("testBean"))).isTrue(); + testBeans = bean.streamTestBeans(); + assertThat(testBeans.size()).isEqualTo(1); + assertThat(testBeans.contains(bf.getBean("testBean"))).isTrue(); + testBeans = bean.sortedTestBeans(); + assertThat(testBeans.size()).isEqualTo(1); + assertThat(testBeans.contains(bf.getBean("testBean"))).isTrue(); + } + + @Test + public void testObjectProviderInjectionWithTargetNotAvailable() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectProviderInjectionBean.class)); + + ObjectProviderInjectionBean bean = (ObjectProviderInjectionBean) bf.getBean("annotatedBean"); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy( + bean::getTestBean); + assertThat(bean.getOptionalTestBean()).isNull(); + assertThat(bean.consumeOptionalTestBean()).isNull(); + assertThat(bean.getOptionalTestBeanWithDefault()).isEqualTo(new TestBean("default")); + assertThat(bean.getUniqueTestBeanWithDefault()).isEqualTo(new TestBean("default")); + assertThat(bean.getUniqueTestBean()).isNull(); + assertThat(bean.consumeUniqueTestBean()).isNull(); + + List testBeans = bean.iterateTestBeans(); + assertThat(testBeans.isEmpty()).isTrue(); + testBeans = bean.forEachTestBeans(); + assertThat(testBeans.isEmpty()).isTrue(); + testBeans = bean.streamTestBeans(); + assertThat(testBeans.isEmpty()).isTrue(); + testBeans = bean.sortedTestBeans(); + assertThat(testBeans.isEmpty()).isTrue(); + } + + @Test + public void testObjectProviderInjectionWithTargetNotUnique() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectProviderInjectionBean.class)); + bf.registerBeanDefinition("testBean1", new RootBeanDefinition(TestBean.class)); + bf.registerBeanDefinition("testBean2", new RootBeanDefinition(TestBean.class)); + + ObjectProviderInjectionBean bean = (ObjectProviderInjectionBean) bf.getBean("annotatedBean"); + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(bean::getTestBean); + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(bean::getOptionalTestBean); + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(bean::consumeOptionalTestBean); + assertThat(bean.getUniqueTestBean()).isNull(); + assertThat(bean.consumeUniqueTestBean()).isNull(); + + List testBeans = bean.iterateTestBeans(); + assertThat(testBeans.size()).isEqualTo(2); + assertThat(testBeans.get(0)).isSameAs(bf.getBean("testBean1")); + assertThat(testBeans.get(1)).isSameAs(bf.getBean("testBean2")); + testBeans = bean.forEachTestBeans(); + assertThat(testBeans.size()).isEqualTo(2); + assertThat(testBeans.get(0)).isSameAs(bf.getBean("testBean1")); + assertThat(testBeans.get(1)).isSameAs(bf.getBean("testBean2")); + testBeans = bean.streamTestBeans(); + assertThat(testBeans.size()).isEqualTo(2); + assertThat(testBeans.get(0)).isSameAs(bf.getBean("testBean1")); + assertThat(testBeans.get(1)).isSameAs(bf.getBean("testBean2")); + testBeans = bean.sortedTestBeans(); + assertThat(testBeans.size()).isEqualTo(2); + assertThat(testBeans.get(0)).isSameAs(bf.getBean("testBean1")); + assertThat(testBeans.get(1)).isSameAs(bf.getBean("testBean2")); + } + + @Test + public void testObjectProviderInjectionWithTargetPrimary() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectProviderInjectionBean.class)); + RootBeanDefinition tb1 = new RootBeanDefinition(TestBeanFactory.class); + tb1.setFactoryMethodName("newTestBean1"); + tb1.setPrimary(true); + bf.registerBeanDefinition("testBean1", tb1); + RootBeanDefinition tb2 = new RootBeanDefinition(TestBeanFactory.class); + tb2.setFactoryMethodName("newTestBean2"); + tb2.setLazyInit(true); + bf.registerBeanDefinition("testBean2", tb2); + + ObjectProviderInjectionBean bean = (ObjectProviderInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean1")); + assertThat(bean.getOptionalTestBean()).isSameAs(bf.getBean("testBean1")); + assertThat(bean.consumeOptionalTestBean()).isSameAs(bf.getBean("testBean1")); + assertThat(bean.getUniqueTestBean()).isSameAs(bf.getBean("testBean1")); + assertThat(bean.consumeUniqueTestBean()).isSameAs(bf.getBean("testBean1")); + assertThat(bf.containsSingleton("testBean2")).isFalse(); + + List testBeans = bean.iterateTestBeans(); + assertThat(testBeans.size()).isEqualTo(2); + assertThat(testBeans.get(0)).isSameAs(bf.getBean("testBean1")); + assertThat(testBeans.get(1)).isSameAs(bf.getBean("testBean2")); + testBeans = bean.forEachTestBeans(); + assertThat(testBeans.size()).isEqualTo(2); + assertThat(testBeans.get(0)).isSameAs(bf.getBean("testBean1")); + assertThat(testBeans.get(1)).isSameAs(bf.getBean("testBean2")); + testBeans = bean.streamTestBeans(); + assertThat(testBeans.size()).isEqualTo(2); + assertThat(testBeans.get(0)).isSameAs(bf.getBean("testBean1")); + assertThat(testBeans.get(1)).isSameAs(bf.getBean("testBean2")); + testBeans = bean.sortedTestBeans(); + assertThat(testBeans.size()).isEqualTo(2); + assertThat(testBeans.get(0)).isSameAs(bf.getBean("testBean2")); + assertThat(testBeans.get(1)).isSameAs(bf.getBean("testBean1")); + } + + @Test + public void testObjectProviderInjectionWithUnresolvedOrderedStream() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectProviderInjectionBean.class)); + RootBeanDefinition tb1 = new RootBeanDefinition(TestBeanFactory.class); + tb1.setFactoryMethodName("newTestBean1"); + tb1.setPrimary(true); + bf.registerBeanDefinition("testBean1", tb1); + RootBeanDefinition tb2 = new RootBeanDefinition(TestBeanFactory.class); + tb2.setFactoryMethodName("newTestBean2"); + tb2.setLazyInit(true); + bf.registerBeanDefinition("testBean2", tb2); + + ObjectProviderInjectionBean bean = (ObjectProviderInjectionBean) bf.getBean("annotatedBean"); + List testBeans = bean.sortedTestBeans(); + assertThat(testBeans.size()).isEqualTo(2); + assertThat(testBeans.get(0)).isSameAs(bf.getBean("testBean2")); + assertThat(testBeans.get(1)).isSameAs(bf.getBean("testBean1")); + } + + @Test + public void testCustomAnnotationRequiredFieldResourceInjection() { + bpp.setAutowiredAnnotationType(MyAutowired.class); + bpp.setRequiredParameterName("optional"); + bpp.setRequiredParameterValue(false); + bf.registerBeanDefinition("customBean", new RootBeanDefinition( + CustomAnnotationRequiredFieldResourceInjectionBean.class)); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + + CustomAnnotationRequiredFieldResourceInjectionBean bean = + (CustomAnnotationRequiredFieldResourceInjectionBean) bf.getBean("customBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + } + + @Test + public void testCustomAnnotationRequiredFieldResourceInjectionFailsWhenNoDependencyFound() { + bpp.setAutowiredAnnotationType(MyAutowired.class); + bpp.setRequiredParameterName("optional"); + bpp.setRequiredParameterValue(false); + bf.registerBeanDefinition("customBean", new RootBeanDefinition( + CustomAnnotationRequiredFieldResourceInjectionBean.class)); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> + bf.getBean("customBean")) + .satisfies(fieldDeclaredOn(CustomAnnotationRequiredFieldResourceInjectionBean.class)); + } + + @Test + public void testCustomAnnotationRequiredFieldResourceInjectionFailsWhenMultipleDependenciesFound() { + bpp.setAutowiredAnnotationType(MyAutowired.class); + bpp.setRequiredParameterName("optional"); + bpp.setRequiredParameterValue(false); + bf.registerBeanDefinition("customBean", new RootBeanDefinition( + CustomAnnotationRequiredFieldResourceInjectionBean.class)); + TestBean tb1 = new TestBean(); + bf.registerSingleton("testBean1", tb1); + TestBean tb2 = new TestBean(); + bf.registerSingleton("testBean2", tb2); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> + bf.getBean("customBean")) + .satisfies(fieldDeclaredOn(CustomAnnotationRequiredFieldResourceInjectionBean.class)); + } + + @Test + public void testCustomAnnotationRequiredMethodResourceInjection() { + bpp.setAutowiredAnnotationType(MyAutowired.class); + bpp.setRequiredParameterName("optional"); + bpp.setRequiredParameterValue(false); + bf.registerBeanDefinition("customBean", new RootBeanDefinition( + CustomAnnotationRequiredMethodResourceInjectionBean.class)); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + + CustomAnnotationRequiredMethodResourceInjectionBean bean = + (CustomAnnotationRequiredMethodResourceInjectionBean) bf.getBean("customBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + } + + @Test + public void testCustomAnnotationRequiredMethodResourceInjectionFailsWhenNoDependencyFound() { + bpp.setAutowiredAnnotationType(MyAutowired.class); + bpp.setRequiredParameterName("optional"); + bpp.setRequiredParameterValue(false); + bf.registerBeanDefinition("customBean", new RootBeanDefinition( + CustomAnnotationRequiredMethodResourceInjectionBean.class)); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> + bf.getBean("customBean")) + .satisfies(methodParameterDeclaredOn(CustomAnnotationRequiredMethodResourceInjectionBean.class)); + } + + @Test + public void testCustomAnnotationRequiredMethodResourceInjectionFailsWhenMultipleDependenciesFound() { + bpp.setAutowiredAnnotationType(MyAutowired.class); + bpp.setRequiredParameterName("optional"); + bpp.setRequiredParameterValue(false); + bf.registerBeanDefinition("customBean", new RootBeanDefinition( + CustomAnnotationRequiredMethodResourceInjectionBean.class)); + TestBean tb1 = new TestBean(); + bf.registerSingleton("testBean1", tb1); + TestBean tb2 = new TestBean(); + bf.registerSingleton("testBean2", tb2); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> + bf.getBean("customBean")) + .satisfies(methodParameterDeclaredOn(CustomAnnotationRequiredMethodResourceInjectionBean.class)); + } + + @Test + public void testCustomAnnotationOptionalFieldResourceInjection() { + bpp.setAutowiredAnnotationType(MyAutowired.class); + bpp.setRequiredParameterName("optional"); + bpp.setRequiredParameterValue(false); + bf.registerBeanDefinition("customBean", new RootBeanDefinition( + CustomAnnotationOptionalFieldResourceInjectionBean.class)); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + + CustomAnnotationOptionalFieldResourceInjectionBean bean = + (CustomAnnotationOptionalFieldResourceInjectionBean) bf.getBean("customBean"); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean()).isNull(); + assertThat(bean.getTestBean2()).isNull(); + } + + @Test + public void testCustomAnnotationOptionalFieldResourceInjectionWhenNoDependencyFound() { + bpp.setAutowiredAnnotationType(MyAutowired.class); + bpp.setRequiredParameterName("optional"); + bpp.setRequiredParameterValue(false); + bf.registerBeanDefinition("customBean", new RootBeanDefinition( + CustomAnnotationOptionalFieldResourceInjectionBean.class)); + + CustomAnnotationOptionalFieldResourceInjectionBean bean = + (CustomAnnotationOptionalFieldResourceInjectionBean) bf.getBean("customBean"); + assertThat(bean.getTestBean3()).isNull(); + assertThat(bean.getTestBean()).isNull(); + assertThat(bean.getTestBean2()).isNull(); + } + + @Test + public void testCustomAnnotationOptionalFieldResourceInjectionWhenMultipleDependenciesFound() { + bpp.setAutowiredAnnotationType(MyAutowired.class); + bpp.setRequiredParameterName("optional"); + bpp.setRequiredParameterValue(false); + bf.registerBeanDefinition("customBean", new RootBeanDefinition( + CustomAnnotationOptionalFieldResourceInjectionBean.class)); + TestBean tb1 = new TestBean(); + bf.registerSingleton("testBean1", tb1); + TestBean tb2 = new TestBean(); + bf.registerSingleton("testBean2", tb2); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> + bf.getBean("customBean")) + .satisfies(fieldDeclaredOn(CustomAnnotationOptionalFieldResourceInjectionBean.class)); + } + + @Test + public void testCustomAnnotationOptionalMethodResourceInjection() { + bpp.setAutowiredAnnotationType(MyAutowired.class); + bpp.setRequiredParameterName("optional"); + bpp.setRequiredParameterValue(false); + bf.registerBeanDefinition("customBean", new RootBeanDefinition( + CustomAnnotationOptionalMethodResourceInjectionBean.class)); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + + CustomAnnotationOptionalMethodResourceInjectionBean bean = + (CustomAnnotationOptionalMethodResourceInjectionBean) bf.getBean("customBean"); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean()).isNull(); + assertThat(bean.getTestBean2()).isNull(); + } + + @Test + public void testCustomAnnotationOptionalMethodResourceInjectionWhenNoDependencyFound() { + bpp.setAutowiredAnnotationType(MyAutowired.class); + bpp.setRequiredParameterName("optional"); + bpp.setRequiredParameterValue(false); + bf.registerBeanDefinition("customBean", new RootBeanDefinition( + CustomAnnotationOptionalMethodResourceInjectionBean.class)); + + CustomAnnotationOptionalMethodResourceInjectionBean bean = + (CustomAnnotationOptionalMethodResourceInjectionBean) bf.getBean("customBean"); + assertThat(bean.getTestBean3()).isNull(); + assertThat(bean.getTestBean()).isNull(); + assertThat(bean.getTestBean2()).isNull(); + } + + @Test + public void testCustomAnnotationOptionalMethodResourceInjectionWhenMultipleDependenciesFound() { + bpp.setAutowiredAnnotationType(MyAutowired.class); + bpp.setRequiredParameterName("optional"); + bpp.setRequiredParameterValue(false); + bf.registerBeanDefinition("customBean", new RootBeanDefinition( + CustomAnnotationOptionalMethodResourceInjectionBean.class)); + TestBean tb1 = new TestBean(); + bf.registerSingleton("testBean1", tb1); + TestBean tb2 = new TestBean(); + bf.registerSingleton("testBean2", tb2); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> + bf.getBean("customBean")) + .satisfies(methodParameterDeclaredOn(CustomAnnotationOptionalMethodResourceInjectionBean.class)); + } + + /** + * Verifies that a dependency on a {@link FactoryBean} can be autowired via + * {@link Autowired @Autowired}, specifically addressing the JIRA issue + * raised in SPR-4040. + */ + @Test + public void testBeanAutowiredWithFactoryBean() { + bf.registerBeanDefinition("factoryBeanDependentBean", new RootBeanDefinition(FactoryBeanDependentBean.class)); + bf.registerSingleton("stringFactoryBean", new StringFactoryBean()); + + final StringFactoryBean factoryBean = (StringFactoryBean) bf.getBean("&stringFactoryBean"); + final FactoryBeanDependentBean bean = (FactoryBeanDependentBean) bf.getBean("factoryBeanDependentBean"); + + assertThat(factoryBean).as("The singleton StringFactoryBean should have been registered.").isNotNull(); + assertThat(bean).as("The factoryBeanDependentBean should have been registered.").isNotNull(); + assertThat(bean.getFactoryBean()).as("The FactoryBeanDependentBean should have been autowired 'by type' with the StringFactoryBean.").isEqualTo(factoryBean); + } + + @Test + public void testGenericsBasedFieldInjection() { + RootBeanDefinition bd = new RootBeanDefinition(RepositoryFieldInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + String sv = "X"; + bf.registerSingleton("stringValue", sv); + Integer iv = 1; + bf.registerSingleton("integerValue", iv); + StringRepository sr = new StringRepository(); + bf.registerSingleton("stringRepo", sr); + IntegerRepository ir = new IntegerRepository(); + bf.registerSingleton("integerRepo", ir); + + RepositoryFieldInjectionBean bean = (RepositoryFieldInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.string).isSameAs(sv); + assertThat(bean.integer).isSameAs(iv); + assertThat(bean.stringArray.length).isSameAs(1); + assertThat(bean.integerArray.length).isSameAs(1); + assertThat(bean.stringArray[0]).isSameAs(sv); + assertThat(bean.integerArray[0]).isSameAs(iv); + assertThat(bean.stringList.size()).isSameAs(1); + assertThat(bean.integerList.size()).isSameAs(1); + assertThat(bean.stringList.get(0)).isSameAs(sv); + assertThat(bean.integerList.get(0)).isSameAs(iv); + assertThat(bean.stringMap.size()).isSameAs(1); + assertThat(bean.integerMap.size()).isSameAs(1); + assertThat(bean.stringMap.get("stringValue")).isSameAs(sv); + assertThat(bean.integerMap.get("integerValue")).isSameAs(iv); + assertThat(bean.stringRepository).isSameAs(sr); + assertThat(bean.integerRepository).isSameAs(ir); + assertThat(bean.stringRepositoryArray.length).isSameAs(1); + assertThat(bean.integerRepositoryArray.length).isSameAs(1); + assertThat(bean.stringRepositoryArray[0]).isSameAs(sr); + assertThat(bean.integerRepositoryArray[0]).isSameAs(ir); + assertThat(bean.stringRepositoryList.size()).isSameAs(1); + assertThat(bean.integerRepositoryList.size()).isSameAs(1); + assertThat(bean.stringRepositoryList.get(0)).isSameAs(sr); + assertThat(bean.integerRepositoryList.get(0)).isSameAs(ir); + assertThat(bean.stringRepositoryMap.size()).isSameAs(1); + assertThat(bean.integerRepositoryMap.size()).isSameAs(1); + assertThat(bean.stringRepositoryMap.get("stringRepo")).isSameAs(sr); + assertThat(bean.integerRepositoryMap.get("integerRepo")).isSameAs(ir); + } + + @Test + public void testGenericsBasedFieldInjectionWithSubstitutedVariables() { + RootBeanDefinition bd = new RootBeanDefinition(RepositoryFieldInjectionBeanWithSubstitutedVariables.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + String sv = "X"; + bf.registerSingleton("stringValue", sv); + Integer iv = 1; + bf.registerSingleton("integerValue", iv); + StringRepository sr = new StringRepository(); + bf.registerSingleton("stringRepo", sr); + IntegerRepository ir = new IntegerRepository(); + bf.registerSingleton("integerRepo", ir); + + RepositoryFieldInjectionBeanWithSubstitutedVariables bean = (RepositoryFieldInjectionBeanWithSubstitutedVariables) bf.getBean("annotatedBean"); + assertThat(bean.string).isSameAs(sv); + assertThat(bean.integer).isSameAs(iv); + assertThat(bean.stringArray.length).isSameAs(1); + assertThat(bean.integerArray.length).isSameAs(1); + assertThat(bean.stringArray[0]).isSameAs(sv); + assertThat(bean.integerArray[0]).isSameAs(iv); + assertThat(bean.stringList.size()).isSameAs(1); + assertThat(bean.integerList.size()).isSameAs(1); + assertThat(bean.stringList.get(0)).isSameAs(sv); + assertThat(bean.integerList.get(0)).isSameAs(iv); + assertThat(bean.stringMap.size()).isSameAs(1); + assertThat(bean.integerMap.size()).isSameAs(1); + assertThat(bean.stringMap.get("stringValue")).isSameAs(sv); + assertThat(bean.integerMap.get("integerValue")).isSameAs(iv); + assertThat(bean.stringRepository).isSameAs(sr); + assertThat(bean.integerRepository).isSameAs(ir); + assertThat(bean.stringRepositoryArray.length).isSameAs(1); + assertThat(bean.integerRepositoryArray.length).isSameAs(1); + assertThat(bean.stringRepositoryArray[0]).isSameAs(sr); + assertThat(bean.integerRepositoryArray[0]).isSameAs(ir); + assertThat(bean.stringRepositoryList.size()).isSameAs(1); + assertThat(bean.integerRepositoryList.size()).isSameAs(1); + assertThat(bean.stringRepositoryList.get(0)).isSameAs(sr); + assertThat(bean.integerRepositoryList.get(0)).isSameAs(ir); + assertThat(bean.stringRepositoryMap.size()).isSameAs(1); + assertThat(bean.integerRepositoryMap.size()).isSameAs(1); + assertThat(bean.stringRepositoryMap.get("stringRepo")).isSameAs(sr); + assertThat(bean.integerRepositoryMap.get("integerRepo")).isSameAs(ir); + } + + @Test + public void testGenericsBasedFieldInjectionWithQualifiers() { + RootBeanDefinition bd = new RootBeanDefinition(RepositoryFieldInjectionBeanWithQualifiers.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + StringRepository sr = new StringRepository(); + bf.registerSingleton("stringRepo", sr); + IntegerRepository ir = new IntegerRepository(); + bf.registerSingleton("integerRepo", ir); + + RepositoryFieldInjectionBeanWithQualifiers bean = (RepositoryFieldInjectionBeanWithQualifiers) bf.getBean("annotatedBean"); + assertThat(bean.stringRepository).isSameAs(sr); + assertThat(bean.integerRepository).isSameAs(ir); + assertThat(bean.stringRepositoryArray.length).isSameAs(1); + assertThat(bean.integerRepositoryArray.length).isSameAs(1); + assertThat(bean.stringRepositoryArray[0]).isSameAs(sr); + assertThat(bean.integerRepositoryArray[0]).isSameAs(ir); + assertThat(bean.stringRepositoryList.size()).isSameAs(1); + assertThat(bean.integerRepositoryList.size()).isSameAs(1); + assertThat(bean.stringRepositoryList.get(0)).isSameAs(sr); + assertThat(bean.integerRepositoryList.get(0)).isSameAs(ir); + assertThat(bean.stringRepositoryMap.size()).isSameAs(1); + assertThat(bean.integerRepositoryMap.size()).isSameAs(1); + assertThat(bean.stringRepositoryMap.get("stringRepo")).isSameAs(sr); + assertThat(bean.integerRepositoryMap.get("integerRepo")).isSameAs(ir); + } + + @Test + public void testGenericsBasedFieldInjectionWithMocks() { + RootBeanDefinition bd = new RootBeanDefinition(RepositoryFieldInjectionBeanWithQualifiers.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + + RootBeanDefinition rbd = new RootBeanDefinition(MocksControl.class); + bf.registerBeanDefinition("mocksControl", rbd); + rbd = new RootBeanDefinition(); + rbd.setFactoryBeanName("mocksControl"); + rbd.setFactoryMethodName("createMock"); + rbd.getConstructorArgumentValues().addGenericArgumentValue(Repository.class); + bf.registerBeanDefinition("stringRepo", rbd); + rbd = new RootBeanDefinition(); + rbd.setFactoryBeanName("mocksControl"); + rbd.setFactoryMethodName("createMock"); + rbd.getConstructorArgumentValues().addGenericArgumentValue(Repository.class); + rbd.setQualifiedElement(ReflectionUtils.findField(getClass(), "integerRepositoryQualifierProvider")); + bf.registerBeanDefinition("integerRepository", rbd); // Bean name not matching qualifier + + RepositoryFieldInjectionBeanWithQualifiers bean = (RepositoryFieldInjectionBeanWithQualifiers) bf.getBean("annotatedBean"); + Repository sr = bf.getBean("stringRepo", Repository.class); + Repository ir = bf.getBean("integerRepository", Repository.class); + assertThat(bean.stringRepository).isSameAs(sr); + assertThat(bean.integerRepository).isSameAs(ir); + assertThat(bean.stringRepositoryArray.length).isSameAs(1); + assertThat(bean.integerRepositoryArray.length).isSameAs(1); + assertThat(bean.stringRepositoryArray[0]).isSameAs(sr); + assertThat(bean.integerRepositoryArray[0]).isSameAs(ir); + assertThat(bean.stringRepositoryList.size()).isSameAs(1); + assertThat(bean.integerRepositoryList.size()).isSameAs(1); + assertThat(bean.stringRepositoryList.get(0)).isSameAs(sr); + assertThat(bean.integerRepositoryList.get(0)).isSameAs(ir); + assertThat(bean.stringRepositoryMap.size()).isSameAs(1); + assertThat(bean.integerRepositoryMap.size()).isSameAs(1); + assertThat(bean.stringRepositoryMap.get("stringRepo")).isSameAs(sr); + assertThat(bean.integerRepositoryMap.get("integerRepository")).isSameAs(ir); + } + + @Test + public void testGenericsBasedFieldInjectionWithSimpleMatch() { + RootBeanDefinition bd = new RootBeanDefinition(RepositoryFieldInjectionBeanWithSimpleMatch.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + + bf.registerSingleton("repo", new StringRepository()); + + RepositoryFieldInjectionBeanWithSimpleMatch bean = (RepositoryFieldInjectionBeanWithSimpleMatch) bf.getBean("annotatedBean"); + Repository repo = bf.getBean("repo", Repository.class); + assertThat(bean.repository).isSameAs(repo); + assertThat(bean.stringRepository).isSameAs(repo); + assertThat(bean.repositoryArray.length).isSameAs(1); + assertThat(bean.stringRepositoryArray.length).isSameAs(1); + assertThat(bean.repositoryArray[0]).isSameAs(repo); + assertThat(bean.stringRepositoryArray[0]).isSameAs(repo); + assertThat(bean.repositoryList.size()).isSameAs(1); + assertThat(bean.stringRepositoryList.size()).isSameAs(1); + assertThat(bean.repositoryList.get(0)).isSameAs(repo); + assertThat(bean.stringRepositoryList.get(0)).isSameAs(repo); + assertThat(bean.repositoryMap.size()).isSameAs(1); + assertThat(bean.stringRepositoryMap.size()).isSameAs(1); + assertThat(bean.repositoryMap.get("repo")).isSameAs(repo); + assertThat(bean.stringRepositoryMap.get("repo")).isSameAs(repo); + + assertThat(bf.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class))).isEqualTo(new String[] {"repo"}); + } + + @Test + public void testGenericsBasedFactoryBeanInjectionWithBeanDefinition() { + RootBeanDefinition bd = new RootBeanDefinition(RepositoryFactoryBeanInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + bf.registerBeanDefinition("repoFactoryBean", new RootBeanDefinition(RepositoryFactoryBean.class)); + + RepositoryFactoryBeanInjectionBean bean = (RepositoryFactoryBeanInjectionBean) bf.getBean("annotatedBean"); + RepositoryFactoryBean repoFactoryBean = bf.getBean("&repoFactoryBean", RepositoryFactoryBean.class); + assertThat(bean.repositoryFactoryBean).isSameAs(repoFactoryBean); + } + + @Test + public void testGenericsBasedFactoryBeanInjectionWithSingletonBean() { + RootBeanDefinition bd = new RootBeanDefinition(RepositoryFactoryBeanInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + bf.registerSingleton("repoFactoryBean", new RepositoryFactoryBean<>()); + + RepositoryFactoryBeanInjectionBean bean = (RepositoryFactoryBeanInjectionBean) bf.getBean("annotatedBean"); + RepositoryFactoryBean repoFactoryBean = bf.getBean("&repoFactoryBean", RepositoryFactoryBean.class); + assertThat(bean.repositoryFactoryBean).isSameAs(repoFactoryBean); + } + + @Test + public void testGenericsBasedFieldInjectionWithSimpleMatchAndMock() { + RootBeanDefinition bd = new RootBeanDefinition(RepositoryFieldInjectionBeanWithSimpleMatch.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + + RootBeanDefinition rbd = new RootBeanDefinition(MocksControl.class); + bf.registerBeanDefinition("mocksControl", rbd); + rbd = new RootBeanDefinition(); + rbd.setFactoryBeanName("mocksControl"); + rbd.setFactoryMethodName("createMock"); + rbd.getConstructorArgumentValues().addGenericArgumentValue(Repository.class); + bf.registerBeanDefinition("repo", rbd); + + RepositoryFieldInjectionBeanWithSimpleMatch bean = (RepositoryFieldInjectionBeanWithSimpleMatch) bf.getBean("annotatedBean"); + Repository repo = bf.getBean("repo", Repository.class); + assertThat(bean.repository).isSameAs(repo); + assertThat(bean.stringRepository).isSameAs(repo); + assertThat(bean.repositoryArray.length).isSameAs(1); + assertThat(bean.stringRepositoryArray.length).isSameAs(1); + assertThat(bean.repositoryArray[0]).isSameAs(repo); + assertThat(bean.stringRepositoryArray[0]).isSameAs(repo); + assertThat(bean.repositoryList.size()).isSameAs(1); + assertThat(bean.stringRepositoryList.size()).isSameAs(1); + assertThat(bean.repositoryList.get(0)).isSameAs(repo); + assertThat(bean.stringRepositoryList.get(0)).isSameAs(repo); + assertThat(bean.repositoryMap.size()).isSameAs(1); + assertThat(bean.stringRepositoryMap.size()).isSameAs(1); + assertThat(bean.repositoryMap.get("repo")).isSameAs(repo); + assertThat(bean.stringRepositoryMap.get("repo")).isSameAs(repo); + } + + @Test + public void testGenericsBasedFieldInjectionWithSimpleMatchAndMockito() { + RootBeanDefinition bd = new RootBeanDefinition(RepositoryFieldInjectionBeanWithSimpleMatch.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + + RootBeanDefinition rbd = new RootBeanDefinition(); + rbd.setBeanClassName(Mockito.class.getName()); + rbd.setFactoryMethodName("mock"); + // TypedStringValue used to be equivalent to an XML-defined argument String + rbd.getConstructorArgumentValues().addGenericArgumentValue(new TypedStringValue(Repository.class.getName())); + bf.registerBeanDefinition("repo", rbd); + + RepositoryFieldInjectionBeanWithSimpleMatch bean = (RepositoryFieldInjectionBeanWithSimpleMatch) bf.getBean("annotatedBean"); + Repository repo = bf.getBean("repo", Repository.class); + assertThat(bean.repository).isSameAs(repo); + assertThat(bean.stringRepository).isSameAs(repo); + assertThat(bean.repositoryArray.length).isSameAs(1); + assertThat(bean.stringRepositoryArray.length).isSameAs(1); + assertThat(bean.repositoryArray[0]).isSameAs(repo); + assertThat(bean.stringRepositoryArray[0]).isSameAs(repo); + assertThat(bean.repositoryList.size()).isSameAs(1); + assertThat(bean.stringRepositoryList.size()).isSameAs(1); + assertThat(bean.repositoryList.get(0)).isSameAs(repo); + assertThat(bean.stringRepositoryList.get(0)).isSameAs(repo); + assertThat(bean.repositoryMap.size()).isSameAs(1); + assertThat(bean.stringRepositoryMap.size()).isSameAs(1); + assertThat(bean.repositoryMap.get("repo")).isSameAs(repo); + assertThat(bean.stringRepositoryMap.get("repo")).isSameAs(repo); + } + + @Test + public void testGenericsBasedMethodInjection() { + RootBeanDefinition bd = new RootBeanDefinition(RepositoryMethodInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + String sv = "X"; + bf.registerSingleton("stringValue", sv); + Integer iv = 1; + bf.registerSingleton("integerValue", iv); + StringRepository sr = new StringRepository(); + bf.registerSingleton("stringRepo", sr); + IntegerRepository ir = new IntegerRepository(); + bf.registerSingleton("integerRepo", ir); + + RepositoryMethodInjectionBean bean = (RepositoryMethodInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.string).isSameAs(sv); + assertThat(bean.integer).isSameAs(iv); + assertThat(bean.stringArray.length).isSameAs(1); + assertThat(bean.integerArray.length).isSameAs(1); + assertThat(bean.stringArray[0]).isSameAs(sv); + assertThat(bean.integerArray[0]).isSameAs(iv); + assertThat(bean.stringList.size()).isSameAs(1); + assertThat(bean.integerList.size()).isSameAs(1); + assertThat(bean.stringList.get(0)).isSameAs(sv); + assertThat(bean.integerList.get(0)).isSameAs(iv); + assertThat(bean.stringMap.size()).isSameAs(1); + assertThat(bean.integerMap.size()).isSameAs(1); + assertThat(bean.stringMap.get("stringValue")).isSameAs(sv); + assertThat(bean.integerMap.get("integerValue")).isSameAs(iv); + assertThat(bean.stringRepository).isSameAs(sr); + assertThat(bean.integerRepository).isSameAs(ir); + assertThat(bean.stringRepositoryArray.length).isSameAs(1); + assertThat(bean.integerRepositoryArray.length).isSameAs(1); + assertThat(bean.stringRepositoryArray[0]).isSameAs(sr); + assertThat(bean.integerRepositoryArray[0]).isSameAs(ir); + assertThat(bean.stringRepositoryList.size()).isSameAs(1); + assertThat(bean.integerRepositoryList.size()).isSameAs(1); + assertThat(bean.stringRepositoryList.get(0)).isSameAs(sr); + assertThat(bean.integerRepositoryList.get(0)).isSameAs(ir); + assertThat(bean.stringRepositoryMap.size()).isSameAs(1); + assertThat(bean.integerRepositoryMap.size()).isSameAs(1); + assertThat(bean.stringRepositoryMap.get("stringRepo")).isSameAs(sr); + assertThat(bean.integerRepositoryMap.get("integerRepo")).isSameAs(ir); + } + + @Test + public void testGenericsBasedMethodInjectionWithSubstitutedVariables() { + RootBeanDefinition bd = new RootBeanDefinition(RepositoryMethodInjectionBeanWithSubstitutedVariables.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + String sv = "X"; + bf.registerSingleton("stringValue", sv); + Integer iv = 1; + bf.registerSingleton("integerValue", iv); + StringRepository sr = new StringRepository(); + bf.registerSingleton("stringRepo", sr); + IntegerRepository ir = new IntegerRepository(); + bf.registerSingleton("integerRepo", ir); + + RepositoryMethodInjectionBeanWithSubstitutedVariables bean = (RepositoryMethodInjectionBeanWithSubstitutedVariables) bf.getBean("annotatedBean"); + assertThat(bean.string).isSameAs(sv); + assertThat(bean.integer).isSameAs(iv); + assertThat(bean.stringArray.length).isSameAs(1); + assertThat(bean.integerArray.length).isSameAs(1); + assertThat(bean.stringArray[0]).isSameAs(sv); + assertThat(bean.integerArray[0]).isSameAs(iv); + assertThat(bean.stringList.size()).isSameAs(1); + assertThat(bean.integerList.size()).isSameAs(1); + assertThat(bean.stringList.get(0)).isSameAs(sv); + assertThat(bean.integerList.get(0)).isSameAs(iv); + assertThat(bean.stringMap.size()).isSameAs(1); + assertThat(bean.integerMap.size()).isSameAs(1); + assertThat(bean.stringMap.get("stringValue")).isSameAs(sv); + assertThat(bean.integerMap.get("integerValue")).isSameAs(iv); + assertThat(bean.stringRepository).isSameAs(sr); + assertThat(bean.integerRepository).isSameAs(ir); + assertThat(bean.stringRepositoryArray.length).isSameAs(1); + assertThat(bean.integerRepositoryArray.length).isSameAs(1); + assertThat(bean.stringRepositoryArray[0]).isSameAs(sr); + assertThat(bean.integerRepositoryArray[0]).isSameAs(ir); + assertThat(bean.stringRepositoryList.size()).isSameAs(1); + assertThat(bean.integerRepositoryList.size()).isSameAs(1); + assertThat(bean.stringRepositoryList.get(0)).isSameAs(sr); + assertThat(bean.integerRepositoryList.get(0)).isSameAs(ir); + assertThat(bean.stringRepositoryMap.size()).isSameAs(1); + assertThat(bean.integerRepositoryMap.size()).isSameAs(1); + assertThat(bean.stringRepositoryMap.get("stringRepo")).isSameAs(sr); + assertThat(bean.integerRepositoryMap.get("integerRepo")).isSameAs(ir); + } + + @Test + public void testGenericsBasedConstructorInjection() { + RootBeanDefinition bd = new RootBeanDefinition(RepositoryConstructorInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + StringRepository sr = new StringRepository(); + bf.registerSingleton("stringRepo", sr); + IntegerRepository ir = new IntegerRepository(); + bf.registerSingleton("integerRepo", ir); + + RepositoryConstructorInjectionBean bean = (RepositoryConstructorInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.stringRepository).isSameAs(sr); + assertThat(bean.integerRepository).isSameAs(ir); + assertThat(bean.stringRepositoryArray.length).isSameAs(1); + assertThat(bean.integerRepositoryArray.length).isSameAs(1); + assertThat(bean.stringRepositoryArray[0]).isSameAs(sr); + assertThat(bean.integerRepositoryArray[0]).isSameAs(ir); + assertThat(bean.stringRepositoryList.size()).isSameAs(1); + assertThat(bean.integerRepositoryList.size()).isSameAs(1); + assertThat(bean.stringRepositoryList.get(0)).isSameAs(sr); + assertThat(bean.integerRepositoryList.get(0)).isSameAs(ir); + assertThat(bean.stringRepositoryMap.size()).isSameAs(1); + assertThat(bean.integerRepositoryMap.size()).isSameAs(1); + assertThat(bean.stringRepositoryMap.get("stringRepo")).isSameAs(sr); + assertThat(bean.integerRepositoryMap.get("integerRepo")).isSameAs(ir); + } + + @Test + @SuppressWarnings("rawtypes") + public void testGenericsBasedConstructorInjectionWithNonTypedTarget() { + RootBeanDefinition bd = new RootBeanDefinition(RepositoryConstructorInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + GenericRepository gr = new GenericRepository(); + bf.registerSingleton("genericRepo", gr); + + RepositoryConstructorInjectionBean bean = (RepositoryConstructorInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.stringRepository).isSameAs(gr); + assertThat(bean.integerRepository).isSameAs(gr); + assertThat(bean.stringRepositoryArray.length).isSameAs(1); + assertThat(bean.integerRepositoryArray.length).isSameAs(1); + assertThat(bean.stringRepositoryArray[0]).isSameAs(gr); + assertThat(bean.integerRepositoryArray[0]).isSameAs(gr); + assertThat(bean.stringRepositoryList.size()).isSameAs(1); + assertThat(bean.integerRepositoryList.size()).isSameAs(1); + assertThat(bean.stringRepositoryList.get(0)).isSameAs(gr); + assertThat(bean.integerRepositoryList.get(0)).isSameAs(gr); + assertThat(bean.stringRepositoryMap.size()).isSameAs(1); + assertThat(bean.integerRepositoryMap.size()).isSameAs(1); + assertThat(bean.stringRepositoryMap.get("genericRepo")).isSameAs(gr); + assertThat(bean.integerRepositoryMap.get("genericRepo")).isSameAs(gr); + } + + @Test + public void testGenericsBasedConstructorInjectionWithNonGenericTarget() { + RootBeanDefinition bd = new RootBeanDefinition(RepositoryConstructorInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + SimpleRepository ngr = new SimpleRepository(); + bf.registerSingleton("simpleRepo", ngr); + + RepositoryConstructorInjectionBean bean = (RepositoryConstructorInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.stringRepository).isSameAs(ngr); + assertThat(bean.integerRepository).isSameAs(ngr); + assertThat(bean.stringRepositoryArray.length).isSameAs(1); + assertThat(bean.integerRepositoryArray.length).isSameAs(1); + assertThat(bean.stringRepositoryArray[0]).isSameAs(ngr); + assertThat(bean.integerRepositoryArray[0]).isSameAs(ngr); + assertThat(bean.stringRepositoryList.size()).isSameAs(1); + assertThat(bean.integerRepositoryList.size()).isSameAs(1); + assertThat(bean.stringRepositoryList.get(0)).isSameAs(ngr); + assertThat(bean.integerRepositoryList.get(0)).isSameAs(ngr); + assertThat(bean.stringRepositoryMap.size()).isSameAs(1); + assertThat(bean.integerRepositoryMap.size()).isSameAs(1); + assertThat(bean.stringRepositoryMap.get("simpleRepo")).isSameAs(ngr); + assertThat(bean.integerRepositoryMap.get("simpleRepo")).isSameAs(ngr); + } + + @Test + @SuppressWarnings("rawtypes") + public void testGenericsBasedConstructorInjectionWithMixedTargets() { + RootBeanDefinition bd = new RootBeanDefinition(RepositoryConstructorInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + StringRepository sr = new StringRepository(); + bf.registerSingleton("stringRepo", sr); + GenericRepository gr = new GenericRepositorySubclass(); + bf.registerSingleton("genericRepo", gr); + + RepositoryConstructorInjectionBean bean = (RepositoryConstructorInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.stringRepository).isSameAs(sr); + assertThat(bean.integerRepository).isSameAs(gr); + assertThat(bean.stringRepositoryArray.length).isSameAs(1); + assertThat(bean.integerRepositoryArray.length).isSameAs(1); + assertThat(bean.stringRepositoryArray[0]).isSameAs(sr); + assertThat(bean.integerRepositoryArray[0]).isSameAs(gr); + assertThat(bean.stringRepositoryList.size()).isSameAs(1); + assertThat(bean.integerRepositoryList.size()).isSameAs(1); + assertThat(bean.stringRepositoryList.get(0)).isSameAs(sr); + assertThat(bean.integerRepositoryList.get(0)).isSameAs(gr); + assertThat(bean.stringRepositoryMap.size()).isSameAs(1); + assertThat(bean.integerRepositoryMap.size()).isSameAs(1); + assertThat(bean.stringRepositoryMap.get("stringRepo")).isSameAs(sr); + assertThat(bean.integerRepositoryMap.get("genericRepo")).isSameAs(gr); + } + + @Test + public void testGenericsBasedConstructorInjectionWithMixedTargetsIncludingNonGeneric() { + RootBeanDefinition bd = new RootBeanDefinition(RepositoryConstructorInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + StringRepository sr = new StringRepository(); + bf.registerSingleton("stringRepo", sr); + SimpleRepository ngr = new SimpleRepositorySubclass(); + bf.registerSingleton("simpleRepo", ngr); + + RepositoryConstructorInjectionBean bean = (RepositoryConstructorInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.stringRepository).isSameAs(sr); + assertThat(bean.integerRepository).isSameAs(ngr); + assertThat(bean.stringRepositoryArray.length).isSameAs(1); + assertThat(bean.integerRepositoryArray.length).isSameAs(1); + assertThat(bean.stringRepositoryArray[0]).isSameAs(sr); + assertThat(bean.integerRepositoryArray[0]).isSameAs(ngr); + assertThat(bean.stringRepositoryList.size()).isSameAs(1); + assertThat(bean.integerRepositoryList.size()).isSameAs(1); + assertThat(bean.stringRepositoryList.get(0)).isSameAs(sr); + assertThat(bean.integerRepositoryList.get(0)).isSameAs(ngr); + assertThat(bean.stringRepositoryMap.size()).isSameAs(1); + assertThat(bean.integerRepositoryMap.size()).isSameAs(1); + assertThat(bean.stringRepositoryMap.get("stringRepo")).isSameAs(sr); + assertThat(bean.integerRepositoryMap.get("simpleRepo")).isSameAs(ngr); + } + + @Test + @SuppressWarnings("rawtypes") + public void testGenericsBasedInjectionIntoMatchingTypeVariable() { + RootBeanDefinition bd = new RootBeanDefinition(GenericInterface1Impl.class); + bd.setFactoryMethodName("create"); + bf.registerBeanDefinition("bean1", bd); + bf.registerBeanDefinition("bean2", new RootBeanDefinition(GenericInterface2Impl.class)); + + GenericInterface1Impl bean1 = (GenericInterface1Impl) bf.getBean("bean1"); + GenericInterface2Impl bean2 = (GenericInterface2Impl) bf.getBean("bean2"); + assertThat(bean1.gi2).isSameAs(bean2); + assertThat(bd.getResolvableType()).isEqualTo(ResolvableType.forClass(GenericInterface1Impl.class)); + } + + @Test + @SuppressWarnings("rawtypes") + public void testGenericsBasedInjectionIntoUnresolvedTypeVariable() { + RootBeanDefinition bd = new RootBeanDefinition(GenericInterface1Impl.class); + bd.setFactoryMethodName("createPlain"); + bf.registerBeanDefinition("bean1", bd); + bf.registerBeanDefinition("bean2", new RootBeanDefinition(GenericInterface2Impl.class)); + + GenericInterface1Impl bean1 = (GenericInterface1Impl) bf.getBean("bean1"); + GenericInterface2Impl bean2 = (GenericInterface2Impl) bf.getBean("bean2"); + assertThat(bean1.gi2).isSameAs(bean2); + assertThat(bd.getResolvableType()).isEqualTo(ResolvableType.forClass(GenericInterface1Impl.class)); + } + + @Test + @SuppressWarnings("rawtypes") + public void testGenericsBasedInjectionIntoTypeVariableSelectingBestMatch() { + RootBeanDefinition bd = new RootBeanDefinition(GenericInterface1Impl.class); + bd.setFactoryMethodName("create"); + bf.registerBeanDefinition("bean1", bd); + bf.registerBeanDefinition("bean2", new RootBeanDefinition(GenericInterface2Impl.class)); + bf.registerBeanDefinition("bean2a", new RootBeanDefinition(ReallyGenericInterface2Impl.class)); + bf.registerBeanDefinition("bean2b", new RootBeanDefinition(PlainGenericInterface2Impl.class)); + + GenericInterface1Impl bean1 = (GenericInterface1Impl) bf.getBean("bean1"); + GenericInterface2Impl bean2 = (GenericInterface2Impl) bf.getBean("bean2"); + assertThat(bean1.gi2).isSameAs(bean2); + assertThat(bf.getBeanNamesForType(ResolvableType.forClassWithGenerics(GenericInterface1.class, String.class))).isEqualTo(new String[] {"bean1"}); + assertThat(bf.getBeanNamesForType(ResolvableType.forClassWithGenerics(GenericInterface2.class, String.class))).isEqualTo(new String[] {"bean2"}); + } + + @Test + @Disabled // SPR-11521 + @SuppressWarnings("rawtypes") + public void testGenericsBasedInjectionIntoTypeVariableSelectingBestMatchAgainstFactoryMethodSignature() { + RootBeanDefinition bd = new RootBeanDefinition(GenericInterface1Impl.class); + bd.setFactoryMethodName("createErased"); + bf.registerBeanDefinition("bean1", bd); + bf.registerBeanDefinition("bean2", new RootBeanDefinition(GenericInterface2Impl.class)); + bf.registerBeanDefinition("bean2a", new RootBeanDefinition(ReallyGenericInterface2Impl.class)); + bf.registerBeanDefinition("bean2b", new RootBeanDefinition(PlainGenericInterface2Impl.class)); + + GenericInterface1Impl bean1 = (GenericInterface1Impl) bf.getBean("bean1"); + GenericInterface2Impl bean2 = (GenericInterface2Impl) bf.getBean("bean2"); + assertThat(bean1.gi2).isSameAs(bean2); + } + + @Test + public void testGenericsBasedInjectionWithBeanDefinitionTargetResolvableType() { + RootBeanDefinition bd1 = new RootBeanDefinition(GenericInterface2Bean.class); + bd1.setTargetType(ResolvableType.forClassWithGenerics(GenericInterface2Bean.class, String.class)); + bf.registerBeanDefinition("bean1", bd1); + RootBeanDefinition bd2 = new RootBeanDefinition(GenericInterface2Bean.class); + bd2.setTargetType(ResolvableType.forClassWithGenerics(GenericInterface2Bean.class, Integer.class)); + bf.registerBeanDefinition("bean2", bd2); + bf.registerBeanDefinition("bean3", new RootBeanDefinition(MultiGenericFieldInjection.class)); + + assertThat(bf.getBean("bean3").toString()).isEqualTo("bean1 a bean2 123"); + assertThat(bd1.getResolvableType()).isEqualTo(ResolvableType.forClassWithGenerics(GenericInterface2Bean.class, String.class)); + assertThat(bd2.getResolvableType()).isEqualTo(ResolvableType.forClassWithGenerics(GenericInterface2Bean.class, Integer.class)); + } + + @Test + public void testCircularTypeReference() { + bf.registerBeanDefinition("bean1", new RootBeanDefinition(StockServiceImpl.class)); + bf.registerBeanDefinition("bean2", new RootBeanDefinition(StockMovementDaoImpl.class)); + bf.registerBeanDefinition("bean3", new RootBeanDefinition(StockMovementImpl.class)); + bf.registerBeanDefinition("bean4", new RootBeanDefinition(StockMovementInstructionImpl.class)); + + StockServiceImpl service = bf.getBean(StockServiceImpl.class); + assertThat(service.stockMovementDao).isSameAs(bf.getBean(StockMovementDaoImpl.class)); + } + + @Test + public void testBridgeMethodHandling() { + bf.registerBeanDefinition("bean1", new RootBeanDefinition(MyCallable.class)); + bf.registerBeanDefinition("bean2", new RootBeanDefinition(SecondCallable.class)); + bf.registerBeanDefinition("bean3", new RootBeanDefinition(FooBar.class)); + assertThat(bf.getBean(FooBar.class)).isNotNull(); + } + + @Test + public void testSingleConstructorWithProvidedArgument() { + RootBeanDefinition bd = new RootBeanDefinition(ProvidedArgumentBean.class); + bd.getConstructorArgumentValues().addGenericArgumentValue(Collections.singletonList("value")); + bf.registerBeanDefinition("beanWithArgs", bd); + assertThat(bf.getBean(ProvidedArgumentBean.class)).isNotNull(); + } + + @Test + public void testAnnotatedDefaultConstructor() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(AnnotatedDefaultConstructorBean.class)); + + assertThat(bf.getBean("annotatedBean")).isNotNull(); + } + + @Test // SPR-15125 + public void testFactoryBeanSelfInjection() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(SelfInjectingFactoryBean.class)); + + SelfInjectingFactoryBean bean = bf.getBean(SelfInjectingFactoryBean.class); + assertThat(bean.testBean).isSameAs(bf.getBean("annotatedBean")); + } + + @Test // SPR-15125 + public void testFactoryBeanSelfInjectionViaFactoryMethod() { + RootBeanDefinition bd = new RootBeanDefinition(SelfInjectingFactoryBean.class); + bd.setFactoryMethodName("create"); + bf.registerBeanDefinition("annotatedBean", bd); + + SelfInjectingFactoryBean bean = bf.getBean(SelfInjectingFactoryBean.class); + assertThat(bean.testBean).isSameAs(bf.getBean("annotatedBean")); + } + + private Consumer methodParameterDeclaredOn( + Class expected) { + return declaredOn( + injectionPoint -> injectionPoint.getMethodParameter().getDeclaringClass(), + expected); + } + + private Consumer fieldDeclaredOn( + Class expected) { + return declaredOn( + injectionPoint -> injectionPoint.getField().getDeclaringClass(), + expected); + } + + private Consumer declaredOn( + Function> declaringClassExtractor, + Class expected) { + return ex -> { + InjectionPoint injectionPoint = ex.getInjectionPoint(); + Class declaringClass = declaringClassExtractor.apply(injectionPoint); + assertThat(declaringClass).isSameAs(expected); + }; + } + + + @Qualifier("testBean") + private void testBeanQualifierProvider() {} + + @Qualifier("integerRepo") + private Repository integerRepositoryQualifierProvider; + + + public static class ResourceInjectionBean { + + @Autowired(required = false) + private TestBean testBean; + + private TestBean testBean2; + + @Autowired + public void setTestBean2(TestBean testBean2) { + if (this.testBean2 != null) { + throw new IllegalStateException("Already called"); + } + this.testBean2 = testBean2; + } + + public TestBean getTestBean() { + return this.testBean; + } + + public TestBean getTestBean2() { + return this.testBean2; + } + } + + + static class NonPublicResourceInjectionBean extends ResourceInjectionBean { + + @Autowired + public final ITestBean testBean3 = null; + + private T nestedTestBean; + + private ITestBean testBean4; + + protected BeanFactory beanFactory; + + public boolean baseInjected = false; + + public NonPublicResourceInjectionBean() { + } + + @Override + @Autowired + @Required + @SuppressWarnings("deprecation") + public void setTestBean2(TestBean testBean2) { + super.setTestBean2(testBean2); + } + + @Autowired + private void inject(ITestBean testBean4, T nestedTestBean) { + this.testBean4 = testBean4; + this.nestedTestBean = nestedTestBean; + } + + @Autowired + private void inject(ITestBean testBean4) { + this.baseInjected = true; + } + + @Autowired + protected void initBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + public ITestBean getTestBean3() { + return this.testBean3; + } + + public ITestBean getTestBean4() { + return this.testBean4; + } + + public T getNestedTestBean() { + return this.nestedTestBean; + } + + public BeanFactory getBeanFactory() { + return this.beanFactory; + } + } + + + public static class TypedExtendedResourceInjectionBean extends NonPublicResourceInjectionBean + implements DisposableBean { + + public boolean destroyed = false; + + @Override + public void destroy() { + this.destroyed = true; + } + } + + + public static class OverriddenExtendedResourceInjectionBean extends NonPublicResourceInjectionBean { + + public boolean subInjected = false; + + @Override + public void setTestBean2(TestBean testBean2) { + super.setTestBean2(testBean2); + } + + @Override + protected void initBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Autowired + private void inject(ITestBean testBean4) { + this.subInjected = true; + } + } + + + public interface InterfaceWithDefaultMethod { + + @Autowired + void setTestBean2(TestBean testBean2); + + @Autowired + default void injectDefault(ITestBean testBean4) { + markSubInjected(); + } + + void markSubInjected(); + } + + + public static class DefaultMethodResourceInjectionBean extends NonPublicResourceInjectionBean + implements InterfaceWithDefaultMethod { + + public boolean subInjected = false; + + @Override + public void setTestBean2(TestBean testBean2) { + super.setTestBean2(testBean2); + } + + @Override + protected void initBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public void markSubInjected() { + subInjected = true; + } + } + + + public static class OptionalResourceInjectionBean extends ResourceInjectionBean { + + @Autowired(required = false) + protected ITestBean testBean3; + + private IndexedTestBean indexedTestBean; + + private NestedTestBean[] nestedTestBeans; + + @Autowired(required = false) + public NestedTestBean[] nestedTestBeansField; + + private ITestBean testBean4; + + @Override + @Autowired(required = false) + public void setTestBean2(TestBean testBean2) { + super.setTestBean2(testBean2); + } + + @Autowired(required = false) + private void inject(ITestBean testBean4, NestedTestBean[] nestedTestBeans, IndexedTestBean indexedTestBean) { + this.testBean4 = testBean4; + this.indexedTestBean = indexedTestBean; + this.nestedTestBeans = nestedTestBeans; + } + + public ITestBean getTestBean3() { + return this.testBean3; + } + + public ITestBean getTestBean4() { + return this.testBean4; + } + + public IndexedTestBean getIndexedTestBean() { + return this.indexedTestBean; + } + + public NestedTestBean[] getNestedTestBeans() { + return this.nestedTestBeans; + } + } + + + public static class OptionalCollectionResourceInjectionBean extends ResourceInjectionBean { + + @Autowired(required = false) + protected ITestBean testBean3; + + private IndexedTestBean indexedTestBean; + + private List nestedTestBeans; + + public List nestedTestBeansSetter; + + @Autowired(required = false) + public List nestedTestBeansField; + + private ITestBean testBean4; + + @Override + @Autowired(required = false) + public void setTestBean2(TestBean testBean2) { + super.setTestBean2(testBean2); + } + + @Autowired(required = false) + private void inject(ITestBean testBean4, List nestedTestBeans, IndexedTestBean indexedTestBean) { + this.testBean4 = testBean4; + this.indexedTestBean = indexedTestBean; + this.nestedTestBeans = nestedTestBeans; + } + + @Autowired(required = false) + public void setNestedTestBeans(List nestedTestBeans) { + this.nestedTestBeansSetter = nestedTestBeans; + } + + public ITestBean getTestBean3() { + return this.testBean3; + } + + public ITestBean getTestBean4() { + return this.testBean4; + } + + public IndexedTestBean getIndexedTestBean() { + return this.indexedTestBean; + } + + public List getNestedTestBeans() { + return this.nestedTestBeans; + } + } + + + public static class ConstructorResourceInjectionBean extends ResourceInjectionBean { + + @Autowired(required = false) + protected ITestBean testBean3; + + private ITestBean testBean4; + + private NestedTestBean nestedTestBean; + + private ConfigurableListableBeanFactory beanFactory; + + public ConstructorResourceInjectionBean() { + throw new UnsupportedOperationException(); + } + + public ConstructorResourceInjectionBean(ITestBean testBean3) { + throw new UnsupportedOperationException(); + } + + @Autowired + public ConstructorResourceInjectionBean(@Autowired(required = false) ITestBean testBean4, + @Autowired(required = false) NestedTestBean nestedTestBean, + ConfigurableListableBeanFactory beanFactory) { + this.testBean4 = testBean4; + this.nestedTestBean = nestedTestBean; + this.beanFactory = beanFactory; + } + + public ConstructorResourceInjectionBean(NestedTestBean nestedTestBean) { + throw new UnsupportedOperationException(); + } + + public ConstructorResourceInjectionBean(ITestBean testBean3, ITestBean testBean4, NestedTestBean nestedTestBean) { + throw new UnsupportedOperationException(); + } + + @Override + @Autowired(required = false) + public void setTestBean2(TestBean testBean2) { + super.setTestBean2(testBean2); + } + + public ITestBean getTestBean3() { + return this.testBean3; + } + + public ITestBean getTestBean4() { + return this.testBean4; + } + + public NestedTestBean getNestedTestBean() { + return this.nestedTestBean; + } + + public ConfigurableListableBeanFactory getBeanFactory() { + return this.beanFactory; + } + } + + + public static class ConstructorsResourceInjectionBean { + + protected ITestBean testBean3; + + private ITestBean testBean4; + + private NestedTestBean[] nestedTestBeans; + + public ConstructorsResourceInjectionBean() { + } + + @Autowired(required = false) + public ConstructorsResourceInjectionBean(ITestBean testBean3) { + this.testBean3 = testBean3; + } + + @Autowired(required = false) + public ConstructorsResourceInjectionBean(ITestBean testBean4, NestedTestBean[] nestedTestBeans) { + this.testBean4 = testBean4; + this.nestedTestBeans = nestedTestBeans; + } + + public ConstructorsResourceInjectionBean(NestedTestBean nestedTestBean) { + throw new UnsupportedOperationException(); + } + + public ConstructorsResourceInjectionBean(ITestBean testBean3, ITestBean testBean4, NestedTestBean nestedTestBean) { + throw new UnsupportedOperationException(); + } + + public ITestBean getTestBean3() { + return this.testBean3; + } + + public ITestBean getTestBean4() { + return this.testBean4; + } + + public NestedTestBean[] getNestedTestBeans() { + return this.nestedTestBeans; + } + } + + + public static class ConstructorWithoutFallbackBean { + + protected ITestBean testBean3; + + @Autowired(required = false) + public ConstructorWithoutFallbackBean(ITestBean testBean3) { + this.testBean3 = testBean3; + } + + public ITestBean getTestBean3() { + return this.testBean3; + } + } + + + public static class ConstructorsCollectionResourceInjectionBean { + + protected ITestBean testBean3; + + private ITestBean testBean4; + + private List nestedTestBeans; + + public ConstructorsCollectionResourceInjectionBean() { + } + + @Autowired(required = false) + public ConstructorsCollectionResourceInjectionBean(ITestBean testBean3) { + this.testBean3 = testBean3; + } + + @Autowired(required = false) + public ConstructorsCollectionResourceInjectionBean(ITestBean testBean4, List nestedTestBeans) { + this.testBean4 = testBean4; + this.nestedTestBeans = nestedTestBeans; + } + + public ConstructorsCollectionResourceInjectionBean(NestedTestBean nestedTestBean) { + throw new UnsupportedOperationException(); + } + + public ConstructorsCollectionResourceInjectionBean(ITestBean testBean3, ITestBean testBean4, + NestedTestBean nestedTestBean) { + throw new UnsupportedOperationException(); + } + + public ITestBean getTestBean3() { + return this.testBean3; + } + + public ITestBean getTestBean4() { + return this.testBean4; + } + + public List getNestedTestBeans() { + return this.nestedTestBeans; + } + } + + + public static class SingleConstructorVarargBean { + + private ITestBean testBean; + + private List nestedTestBeans; + + public SingleConstructorVarargBean(ITestBean testBean, NestedTestBean... nestedTestBeans) { + this.testBean = testBean; + this.nestedTestBeans = Arrays.asList(nestedTestBeans); + } + + public ITestBean getTestBean() { + return this.testBean; + } + + public List getNestedTestBeans() { + return this.nestedTestBeans; + } + } + + + public static class SingleConstructorRequiredCollectionBean { + + private ITestBean testBean; + + private List nestedTestBeans; + + public SingleConstructorRequiredCollectionBean(ITestBean testBean, List nestedTestBeans) { + this.testBean = testBean; + this.nestedTestBeans = nestedTestBeans; + } + + public ITestBean getTestBean() { + return this.testBean; + } + + public List getNestedTestBeans() { + return this.nestedTestBeans; + } + } + + + public static class SingleConstructorOptionalCollectionBean { + + private ITestBean testBean; + + private List nestedTestBeans; + + public SingleConstructorOptionalCollectionBean(ITestBean testBean, + @Autowired(required = false) List nestedTestBeans) { + this.testBean = testBean; + this.nestedTestBeans = nestedTestBeans; + } + + public ITestBean getTestBean() { + return this.testBean; + } + + public List getNestedTestBeans() { + return this.nestedTestBeans; + } + } + + + @SuppressWarnings("serial") + public static class MyTestBeanMap extends LinkedHashMap { + } + + + @SuppressWarnings("serial") + public static class MyTestBeanSet extends LinkedHashSet { + } + + + public static class MapConstructorInjectionBean { + + private Map testBeanMap; + + @Autowired + public MapConstructorInjectionBean(Map testBeanMap) { + this.testBeanMap = testBeanMap; + } + + public Map getTestBeanMap() { + return this.testBeanMap; + } + } + + + public static class QualifiedMapConstructorInjectionBean { + + private Map testBeanMap; + + @Autowired + public QualifiedMapConstructorInjectionBean(@Qualifier("myTestBeanMap") Map testBeanMap) { + this.testBeanMap = testBeanMap; + } + + public Map getTestBeanMap() { + return this.testBeanMap; + } + } + + + public static class SetConstructorInjectionBean { + + private Set testBeanSet; + + @Autowired + public SetConstructorInjectionBean(Set testBeanSet) { + this.testBeanSet = testBeanSet; + } + + public Set getTestBeanSet() { + return this.testBeanSet; + } + } + + + public static class SelfInjectionBean { + + @Autowired + public SelfInjectionBean reference; + + @Autowired(required = false) + public List referenceCollection; + } + + + @SuppressWarnings("serial") + public static class SelfInjectionCollectionBean extends ArrayList { + + @Autowired + public SelfInjectionCollectionBean reference; + + @Autowired(required = false) + public List referenceCollection; + } + + + public static class MapFieldInjectionBean { + + @Autowired + private Map testBeanMap; + + public Map getTestBeanMap() { + return this.testBeanMap; + } + } + + + public static class MapMethodInjectionBean { + + private TestBean testBean; + + private Map testBeanMap; + + @Autowired(required = false) + public void setTestBeanMap(TestBean testBean, Map testBeanMap) { + this.testBean = testBean; + this.testBeanMap = testBeanMap; + } + + public TestBean getTestBean() { + return this.testBean; + } + + public Map getTestBeanMap() { + return this.testBeanMap; + } + } + + + @SuppressWarnings("serial") + public static class ObjectFactoryFieldInjectionBean implements Serializable { + + @Autowired + private ObjectFactory testBeanFactory; + + public TestBean getTestBean() { + return this.testBeanFactory.getObject(); + } + } + + + @SuppressWarnings("serial") + public static class ObjectFactoryConstructorInjectionBean implements Serializable { + + private final ObjectFactory testBeanFactory; + + public ObjectFactoryConstructorInjectionBean(ObjectFactory testBeanFactory) { + this.testBeanFactory = testBeanFactory; + } + + public TestBean getTestBean() { + return this.testBeanFactory.getObject(); + } + } + + + public static class ObjectFactoryQualifierInjectionBean { + + @Autowired + @Qualifier("testBean") + private ObjectFactory testBeanFactory; + + public TestBean getTestBean() { + return (TestBean) this.testBeanFactory.getObject(); + } + } + + + public static class ObjectProviderInjectionBean { + + @Autowired + private ObjectProvider testBeanProvider; + + private TestBean consumedTestBean; + + public TestBean getTestBean() { + return this.testBeanProvider.getObject(); + } + + public TestBean getTestBean(String name) { + return this.testBeanProvider.getObject(name); + } + + public TestBean getOptionalTestBean() { + return this.testBeanProvider.getIfAvailable(); + } + + public TestBean getOptionalTestBeanWithDefault() { + return this.testBeanProvider.getIfAvailable(() -> new TestBean("default")); + } + + public TestBean consumeOptionalTestBean() { + this.testBeanProvider.ifAvailable(tb -> consumedTestBean = tb); + return consumedTestBean; + } + + public TestBean getUniqueTestBean() { + return this.testBeanProvider.getIfUnique(); + } + + public TestBean getUniqueTestBeanWithDefault() { + return this.testBeanProvider.getIfUnique(() -> new TestBean("default")); + } + + public TestBean consumeUniqueTestBean() { + this.testBeanProvider.ifUnique(tb -> consumedTestBean = tb); + return consumedTestBean; + } + + public List iterateTestBeans() { + List resolved = new ArrayList<>(); + for (TestBean tb : this.testBeanProvider) { + resolved.add(tb); + } + return resolved; + } + + public List forEachTestBeans() { + List resolved = new ArrayList<>(); + this.testBeanProvider.forEach(resolved::add); + return resolved; + } + + public List streamTestBeans() { + return this.testBeanProvider.stream().collect(Collectors.toList()); + } + + public List sortedTestBeans() { + return this.testBeanProvider.orderedStream().collect(Collectors.toList()); + } + } + + + public static class CustomAnnotationRequiredFieldResourceInjectionBean { + + @MyAutowired(optional = false) + private TestBean testBean; + + public TestBean getTestBean() { + return this.testBean; + } + } + + + public static class CustomAnnotationRequiredMethodResourceInjectionBean { + + private TestBean testBean; + + @MyAutowired(optional = false) + public void setTestBean(TestBean testBean) { + this.testBean = testBean; + } + + public TestBean getTestBean() { + return this.testBean; + } + } + + + public static class CustomAnnotationOptionalFieldResourceInjectionBean extends ResourceInjectionBean { + + @MyAutowired(optional = true) + private TestBean testBean3; + + public TestBean getTestBean3() { + return this.testBean3; + } + } + + + public static class CustomAnnotationOptionalMethodResourceInjectionBean extends ResourceInjectionBean { + + private TestBean testBean3; + + @MyAutowired(optional = true) + protected void setTestBean3(TestBean testBean3) { + this.testBean3 = testBean3; + } + + public TestBean getTestBean3() { + return this.testBean3; + } + } + + + @Target({ElementType.METHOD, ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + public @interface MyAutowired { + + boolean optional() default false; + } + + + /** + * Bean with a dependency on a {@link FactoryBean}. + */ + private static class FactoryBeanDependentBean { + + @Autowired + private FactoryBean factoryBean; + + public final FactoryBean getFactoryBean() { + return this.factoryBean; + } + } + + + public static class StringFactoryBean implements FactoryBean { + + @Override + public String getObject() throws Exception { + return ""; + } + + @Override + public Class getObjectType() { + return String.class; + } + + @Override + public boolean isSingleton() { + return true; + } + } + + + public static class OrderedNestedTestBean extends NestedTestBean implements Ordered { + + private int order; + + public void setOrder(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return this.order; + } + } + + + @Order(1) + public static class FixedOrder1NestedTestBean extends NestedTestBean { + } + + @Order(2) + public static class FixedOrder2NestedTestBean extends NestedTestBean { + } + + + public interface Repository { + } + + public static class StringRepository implements Repository { + } + + public static class IntegerRepository implements Repository { + } + + public static class GenericRepository implements Repository { + } + + @SuppressWarnings("rawtypes") + public static class GenericRepositorySubclass extends GenericRepository { + } + + @SuppressWarnings("rawtypes") + public static class SimpleRepository implements Repository { + } + + public static class SimpleRepositorySubclass extends SimpleRepository { + } + + + public static class RepositoryFactoryBean implements FactoryBean { + + @Override + public T getObject() { + throw new IllegalStateException(); + } + + @Override + public Class getObjectType() { + return Object.class; + } + + @Override + public boolean isSingleton() { + return false; + } + } + + + public static class RepositoryFieldInjectionBean { + + @Autowired + public String string; + + @Autowired + public Integer integer; + + @Autowired + public String[] stringArray; + + @Autowired + public Integer[] integerArray; + + @Autowired + public List stringList; + + @Autowired + public List integerList; + + @Autowired + public Map stringMap; + + @Autowired + public Map integerMap; + + @Autowired + public Repository stringRepository; + + @Autowired + public Repository integerRepository; + + @Autowired + public Repository[] stringRepositoryArray; + + @Autowired + public Repository[] integerRepositoryArray; + + @Autowired + public List> stringRepositoryList; + + @Autowired + public List> integerRepositoryList; + + @Autowired + public Map> stringRepositoryMap; + + @Autowired + public Map> integerRepositoryMap; + } + + + public static class RepositoryFieldInjectionBeanWithVariables { + + @Autowired + public S string; + + @Autowired + public I integer; + + @Autowired + public S[] stringArray; + + @Autowired + public I[] integerArray; + + @Autowired + public List stringList; + + @Autowired + public List integerList; + + @Autowired + public Map stringMap; + + @Autowired + public Map integerMap; + + @Autowired + public Repository stringRepository; + + @Autowired + public Repository integerRepository; + + @Autowired + public Repository[] stringRepositoryArray; + + @Autowired + public Repository[] integerRepositoryArray; + + @Autowired + public List> stringRepositoryList; + + @Autowired + public List> integerRepositoryList; + + @Autowired + public Map> stringRepositoryMap; + + @Autowired + public Map> integerRepositoryMap; + } + + + public static class RepositoryFieldInjectionBeanWithSubstitutedVariables + extends RepositoryFieldInjectionBeanWithVariables { + } + + + public static class RepositoryFieldInjectionBeanWithQualifiers { + + @Autowired @Qualifier("stringRepo") + public Repository stringRepository; + + @Autowired @Qualifier("integerRepo") + public Repository integerRepository; + + @Autowired @Qualifier("stringRepo") + public Repository[] stringRepositoryArray; + + @Autowired @Qualifier("integerRepo") + public Repository[] integerRepositoryArray; + + @Autowired @Qualifier("stringRepo") + public List> stringRepositoryList; + + @Autowired @Qualifier("integerRepo") + public List> integerRepositoryList; + + @Autowired @Qualifier("stringRepo") + public Map> stringRepositoryMap; + + @Autowired @Qualifier("integerRepo") + public Map> integerRepositoryMap; + } + + + public static class RepositoryFieldInjectionBeanWithSimpleMatch { + + @Autowired + public Repository repository; + + @Autowired + public Repository stringRepository; + + @Autowired + public Repository[] repositoryArray; + + @Autowired + public Repository[] stringRepositoryArray; + + @Autowired + public List> repositoryList; + + @Autowired + public List> stringRepositoryList; + + @Autowired + public Map> repositoryMap; + + @Autowired + public Map> stringRepositoryMap; + } + + + public static class RepositoryFactoryBeanInjectionBean { + + @Autowired + public RepositoryFactoryBean repositoryFactoryBean; + } + + + public static class RepositoryMethodInjectionBean { + + public String string; + + public Integer integer; + + public String[] stringArray; + + public Integer[] integerArray; + + public List stringList; + + public List integerList; + + public Map stringMap; + + public Map integerMap; + + public Repository stringRepository; + + public Repository integerRepository; + + public Repository[] stringRepositoryArray; + + public Repository[] integerRepositoryArray; + + public List> stringRepositoryList; + + public List> integerRepositoryList; + + public Map> stringRepositoryMap; + + public Map> integerRepositoryMap; + + @Autowired + public void setString(String string) { + this.string = string; + } + + @Autowired + public void setInteger(Integer integer) { + this.integer = integer; + } + + @Autowired + public void setStringArray(String[] stringArray) { + this.stringArray = stringArray; + } + + @Autowired + public void setIntegerArray(Integer[] integerArray) { + this.integerArray = integerArray; + } + + @Autowired + public void setStringList(List stringList) { + this.stringList = stringList; + } + + @Autowired + public void setIntegerList(List integerList) { + this.integerList = integerList; + } + + @Autowired + public void setStringMap(Map stringMap) { + this.stringMap = stringMap; + } + + @Autowired + public void setIntegerMap(Map integerMap) { + this.integerMap = integerMap; + } + + @Autowired + public void setStringRepository(Repository stringRepository) { + this.stringRepository = stringRepository; + } + + @Autowired + public void setIntegerRepository(Repository integerRepository) { + this.integerRepository = integerRepository; + } + + @Autowired + public void setStringRepositoryArray(Repository[] stringRepositoryArray) { + this.stringRepositoryArray = stringRepositoryArray; + } + + @Autowired + public void setIntegerRepositoryArray(Repository[] integerRepositoryArray) { + this.integerRepositoryArray = integerRepositoryArray; + } + + @Autowired + public void setStringRepositoryList(List> stringRepositoryList) { + this.stringRepositoryList = stringRepositoryList; + } + + @Autowired + public void setIntegerRepositoryList(List> integerRepositoryList) { + this.integerRepositoryList = integerRepositoryList; + } + + @Autowired + public void setStringRepositoryMap(Map> stringRepositoryMap) { + this.stringRepositoryMap = stringRepositoryMap; + } + + @Autowired + public void setIntegerRepositoryMap(Map> integerRepositoryMap) { + this.integerRepositoryMap = integerRepositoryMap; + } + } + + + public static class RepositoryMethodInjectionBeanWithVariables { + + public S string; + + public I integer; + + public S[] stringArray; + + public I[] integerArray; + + public List stringList; + + public List integerList; + + public Map stringMap; + + public Map integerMap; + + public Repository stringRepository; + + public Repository integerRepository; + + public Repository[] stringRepositoryArray; + + public Repository[] integerRepositoryArray; + + public List> stringRepositoryList; + + public List> integerRepositoryList; + + public Map> stringRepositoryMap; + + public Map> integerRepositoryMap; + + @Autowired + public void setString(S string) { + this.string = string; + } + + @Autowired + public void setInteger(I integer) { + this.integer = integer; + } + + @Autowired + public void setStringArray(S[] stringArray) { + this.stringArray = stringArray; + } + + @Autowired + public void setIntegerArray(I[] integerArray) { + this.integerArray = integerArray; + } + + @Autowired + public void setStringList(List stringList) { + this.stringList = stringList; + } + + @Autowired + public void setIntegerList(List integerList) { + this.integerList = integerList; + } + + @Autowired + public void setStringMap(Map stringMap) { + this.stringMap = stringMap; + } + + @Autowired + public void setIntegerMap(Map integerMap) { + this.integerMap = integerMap; + } + + @Autowired + public void setStringRepository(Repository stringRepository) { + this.stringRepository = stringRepository; + } + + @Autowired + public void setIntegerRepository(Repository integerRepository) { + this.integerRepository = integerRepository; + } + + @Autowired + public void setStringRepositoryArray(Repository[] stringRepositoryArray) { + this.stringRepositoryArray = stringRepositoryArray; + } + + @Autowired + public void setIntegerRepositoryArray(Repository[] integerRepositoryArray) { + this.integerRepositoryArray = integerRepositoryArray; + } + + @Autowired + public void setStringRepositoryList(List> stringRepositoryList) { + this.stringRepositoryList = stringRepositoryList; + } + + @Autowired + public void setIntegerRepositoryList(List> integerRepositoryList) { + this.integerRepositoryList = integerRepositoryList; + } + + @Autowired + public void setStringRepositoryMap(Map> stringRepositoryMap) { + this.stringRepositoryMap = stringRepositoryMap; + } + + @Autowired + public void setIntegerRepositoryMap(Map> integerRepositoryMap) { + this.integerRepositoryMap = integerRepositoryMap; + } + } + + + public static class RepositoryMethodInjectionBeanWithSubstitutedVariables + extends RepositoryMethodInjectionBeanWithVariables { + } + + + public static class RepositoryConstructorInjectionBean { + + public Repository stringRepository; + + public Repository integerRepository; + + public Repository[] stringRepositoryArray; + + public Repository[] integerRepositoryArray; + + public List> stringRepositoryList; + + public List> integerRepositoryList; + + public Map> stringRepositoryMap; + + public Map> integerRepositoryMap; + + @Autowired + public RepositoryConstructorInjectionBean(Repository stringRepository, Repository integerRepository, + Repository[] stringRepositoryArray, Repository[] integerRepositoryArray, + List> stringRepositoryList, List> integerRepositoryList, + Map> stringRepositoryMap, Map> integerRepositoryMap) { + this.stringRepository = stringRepository; + this.integerRepository = integerRepository; + this.stringRepositoryArray = stringRepositoryArray; + this.integerRepositoryArray = integerRepositoryArray; + this.stringRepositoryList = stringRepositoryList; + this.integerRepositoryList = integerRepositoryList; + this.stringRepositoryMap = stringRepositoryMap; + this.integerRepositoryMap = integerRepositoryMap; + } + } + + + /** + * Pseudo-implementation of EasyMock's {@code MocksControl} class. + */ + public static class MocksControl { + + @SuppressWarnings("unchecked") + public T createMock(Class toMock) { + return (T) Proxy.newProxyInstance(AutowiredAnnotationBeanPostProcessorTests.class.getClassLoader(), new Class[] {toMock}, + new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + throw new UnsupportedOperationException("mocked!"); + } + }); + } + } + + + public interface GenericInterface1 { + + String doSomethingGeneric(T o); + } + + + public static class GenericInterface1Impl implements GenericInterface1 { + + @Autowired + private GenericInterface2 gi2; + + @Override + public String doSomethingGeneric(T o) { + return gi2.doSomethingMoreGeneric(o) + "_somethingGeneric_" + o; + } + + public static GenericInterface1 create() { + return new StringGenericInterface1Impl(); + } + + public static GenericInterface1 createErased() { + return new GenericInterface1Impl<>(); + } + + @SuppressWarnings("rawtypes") + public static GenericInterface1 createPlain() { + return new GenericInterface1Impl(); + } + } + + + public static class StringGenericInterface1Impl extends GenericInterface1Impl { + } + + + public interface GenericInterface2 { + + String doSomethingMoreGeneric(K o); + } + + + public static class GenericInterface2Impl implements GenericInterface2 { + + @Override + public String doSomethingMoreGeneric(String o) { + return "somethingMoreGeneric_" + o; + } + } + + + public static class ReallyGenericInterface2Impl implements GenericInterface2 { + + @Override + public String doSomethingMoreGeneric(Object o) { + return "somethingMoreGeneric_" + o; + } + } + + + public static class GenericInterface2Bean implements GenericInterface2, BeanNameAware { + + private String name; + + @Override + public void setBeanName(String name) { + this.name = name; + } + + @Override + public String doSomethingMoreGeneric(K o) { + return this.name + " " + o; + } + } + + + public static class MultiGenericFieldInjection { + + @Autowired + private GenericInterface2 stringBean; + + @Autowired + private GenericInterface2 integerBean; + + @Override + public String toString() { + return this.stringBean.doSomethingMoreGeneric("a") + " " + this.integerBean.doSomethingMoreGeneric(123); + } + } + + + @SuppressWarnings("rawtypes") + public static class PlainGenericInterface2Impl implements GenericInterface2 { + + @Override + public String doSomethingMoreGeneric(Object o) { + return "somethingMoreGeneric_" + o; + } + } + + + @SuppressWarnings("rawtypes") + public interface StockMovement

    { + } + + + @SuppressWarnings("rawtypes") + public interface StockMovementInstruction { + } + + + @SuppressWarnings("rawtypes") + public interface StockMovementDao { + } + + + @SuppressWarnings("rawtypes") + public static class StockMovementImpl

    implements StockMovement

    { + } + + + @SuppressWarnings("rawtypes") + public static class StockMovementInstructionImpl implements StockMovementInstruction { + } + + + @SuppressWarnings("rawtypes") + public static class StockMovementDaoImpl implements StockMovementDao { + } + + + public static class StockServiceImpl { + + @Autowired + @SuppressWarnings("rawtypes") + private StockMovementDao stockMovementDao; + } + + + public static class MyCallable implements Callable { + + @Override + public Thread call() throws Exception { + return null; + } + } + + + public static class SecondCallable implements Callable{ + + @Override + public Thread call() throws Exception { + return null; + } + } + + + public static abstract class Foo> { + + private RT obj; + + protected void setObj(RT obj) { + if (this.obj != null) { + throw new IllegalStateException("Already called"); + } + this.obj = obj; + } + } + + + public static class FooBar extends Foo { + + @Override + @Autowired + public void setObj(MyCallable obj) { + super.setObj(obj); + } + } + + + public static class NullNestedTestBeanFactoryBean implements FactoryBean { + + @Override + public NestedTestBean getObject() { + return null; + } + + @Override + public Class getObjectType() { + return NestedTestBean.class; + } + + @Override + public boolean isSingleton() { + return true; + } + } + + + public static class NullFactoryMethods { + + public static TestBean createTestBean() { + return null; + } + + public static NestedTestBean createNestedTestBean() { + return null; + } + } + + + public static class ProvidedArgumentBean { + + public ProvidedArgumentBean(String[] args) { + } + } + + + public static class CollectionFactoryMethods { + + public static Map testBeanMap() { + Map tbm = new LinkedHashMap<>(); + tbm.put("testBean1", new TestBean("tb1")); + tbm.put("testBean2", new TestBean("tb2")); + return tbm; + } + + public static Set testBeanSet() { + Set tbs = new LinkedHashSet<>(); + tbs.add(new TestBean("tb1")); + tbs.add(new TestBean("tb2")); + return tbs; + } + } + + + public static class CustomCollectionFactoryMethods { + + public static CustomMap testBeanMap() { + CustomMap tbm = new CustomHashMap<>(); + tbm.put("testBean1", new TestBean("tb1")); + tbm.put("testBean2", new TestBean("tb2")); + return tbm; + } + + public static CustomSet testBeanSet() { + CustomSet tbs = new CustomHashSet<>(); + tbs.add(new TestBean("tb1")); + tbs.add(new TestBean("tb2")); + return tbs; + } + } + + + public static class CustomMapConstructorInjectionBean { + + private CustomMap testBeanMap; + + @Autowired + public CustomMapConstructorInjectionBean(CustomMap testBeanMap) { + this.testBeanMap = testBeanMap; + } + + public CustomMap getTestBeanMap() { + return this.testBeanMap; + } + } + + + public static class CustomSetConstructorInjectionBean { + + private CustomSet testBeanSet; + + @Autowired + public CustomSetConstructorInjectionBean(CustomSet testBeanSet) { + this.testBeanSet = testBeanSet; + } + + public CustomSet getTestBeanSet() { + return this.testBeanSet; + } + } + + + public interface CustomMap extends Map { + } + + + @SuppressWarnings("serial") + public static class CustomHashMap extends LinkedHashMap implements CustomMap { + } + + + public interface CustomSet extends Set { + } + + + @SuppressWarnings("serial") + public static class CustomHashSet extends LinkedHashSet implements CustomSet { + } + + + public static class AnnotatedDefaultConstructorBean { + + @Autowired + public AnnotatedDefaultConstructorBean() { + } + } + + + public static class SelfInjectingFactoryBean implements FactoryBean { + + private final TestBean exposedTestBean = new TestBean(); + + @Autowired + TestBean testBean; + + @Override + public TestBean getObject() { + return exposedTestBean; + } + + @Override + public Class getObjectType() { + return TestBean.class; + } + + @Override + public boolean isSingleton() { + return true; + } + + public static SelfInjectingFactoryBean create() { + return new SelfInjectingFactoryBean(); + } + } + + + public static class TestBeanFactory { + + @Order(1) + public static TestBean newTestBean1() { + return new TestBean(); + } + + @Order(0) + public static TestBean newTestBean2() { + return new TestBean(); + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/CustomAutowireConfigurerTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/CustomAutowireConfigurerTests.java new file mode 100644 index 0000000..02a87f6 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/CustomAutowireConfigurerTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.annotation; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.support.AutowireCandidateResolver; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.core.testfixture.io.ResourceTestUtils.qualifiedResource; + +/** + * Unit tests for {@link CustomAutowireConfigurer}. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @author Chris Beams + */ +public class CustomAutowireConfigurerTests { + + @Test + public void testCustomResolver() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions( + qualifiedResource(CustomAutowireConfigurerTests.class, "context.xml")); + + CustomAutowireConfigurer cac = new CustomAutowireConfigurer(); + CustomResolver customResolver = new CustomResolver(); + bf.setAutowireCandidateResolver(customResolver); + cac.postProcessBeanFactory(bf); + TestBean testBean = (TestBean) bf.getBean("testBean"); + assertThat(testBean.getName()).isEqualTo("#1!"); + } + + + public static class TestBean { + + private String name; + + public TestBean(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + } + + + public static class CustomResolver implements AutowireCandidateResolver { + + @Override + public boolean isAutowireCandidate(BeanDefinitionHolder bdHolder, DependencyDescriptor descriptor) { + if (!bdHolder.getBeanDefinition().isAutowireCandidate()) { + return false; + } + if (!bdHolder.getBeanName().matches("[a-z-]+")) { + return false; + } + if (bdHolder.getBeanDefinition().getAttribute("priority").equals("1")) { + return true; + } + return false; + } + + @Override + public Object getSuggestedValue(DependencyDescriptor descriptor) { + return null; + } + + @Override + public Object getLazyResolutionProxyIfNecessary(DependencyDescriptor descriptor, String beanName) { + return null; + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/InjectAnnotationBeanPostProcessorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/InjectAnnotationBeanPostProcessorTests.java new file mode 100644 index 0000000..aa4c1cb --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/InjectAnnotationBeanPostProcessorTests.java @@ -0,0 +1,1269 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.annotation; + +import java.io.Serializable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Provider; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.AutowireCandidateQualifier; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.IndexedTestBean; +import org.springframework.beans.testfixture.beans.NestedTestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.testfixture.io.SerializationTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Unit tests for {@link org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor} + * processing the JSR-330 {@link javax.inject.Inject} annotation. + * + * @author Juergen Hoeller + * @since 3.0 + */ +public class InjectAnnotationBeanPostProcessorTests { + + private DefaultListableBeanFactory bf; + + private AutowiredAnnotationBeanPostProcessor bpp; + + + @BeforeEach + public void setup() { + bf = new DefaultListableBeanFactory(); + bf.registerResolvableDependency(BeanFactory.class, bf); + bpp = new AutowiredAnnotationBeanPostProcessor(); + bpp.setBeanFactory(bf); + bf.addBeanPostProcessor(bpp); + bf.setAutowireCandidateResolver(new QualifierAnnotationAutowireCandidateResolver()); + } + + @AfterEach + public void close() { + bf.destroySingletons(); + } + + + @Test + public void testIncompleteBeanDefinition() { + bf.registerBeanDefinition("testBean", new GenericBeanDefinition()); + try { + bf.getBean("testBean"); + } + catch (BeanCreationException ex) { + boolean condition = ex.getRootCause() instanceof IllegalStateException; + assertThat(condition).isTrue(); + } + } + + @Test + public void testResourceInjection() { + RootBeanDefinition bd = new RootBeanDefinition(ResourceInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + + ResourceInjectionBean bean = (ResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + + bean = (ResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + } + + @Test + public void testExtendedResourceInjection() { + RootBeanDefinition bd = new RootBeanDefinition(TypedExtendedResourceInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + NestedTestBean ntb = new NestedTestBean(); + bf.registerSingleton("nestedTestBean", ntb); + + TypedExtendedResourceInjectionBean bean = (TypedExtendedResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBean()).isSameAs(ntb); + assertThat(bean.getBeanFactory()).isSameAs(bf); + + bean = (TypedExtendedResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBean()).isSameAs(ntb); + assertThat(bean.getBeanFactory()).isSameAs(bf); + } + + @Test + public void testExtendedResourceInjectionWithOverriding() { + RootBeanDefinition annotatedBd = new RootBeanDefinition(TypedExtendedResourceInjectionBean.class); + TestBean tb2 = new TestBean(); + annotatedBd.getPropertyValues().add("testBean2", tb2); + bf.registerBeanDefinition("annotatedBean", annotatedBd); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + NestedTestBean ntb = new NestedTestBean(); + bf.registerSingleton("nestedTestBean", ntb); + + TypedExtendedResourceInjectionBean bean = (TypedExtendedResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb2); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBean()).isSameAs(ntb); + assertThat(bean.getBeanFactory()).isSameAs(bf); + } + + @Test + @SuppressWarnings("deprecation") + public void testExtendedResourceInjectionWithAtRequired() { + bf.addBeanPostProcessor(new RequiredAnnotationBeanPostProcessor()); + RootBeanDefinition bd = new RootBeanDefinition(TypedExtendedResourceInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + NestedTestBean ntb = new NestedTestBean(); + bf.registerSingleton("nestedTestBean", ntb); + + TypedExtendedResourceInjectionBean bean = (TypedExtendedResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBean()).isSameAs(ntb); + assertThat(bean.getBeanFactory()).isSameAs(bf); + } + + @Test + public void testConstructorResourceInjection() { + RootBeanDefinition bd = new RootBeanDefinition(ConstructorResourceInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + NestedTestBean ntb = new NestedTestBean(); + bf.registerSingleton("nestedTestBean", ntb); + + ConstructorResourceInjectionBean bean = (ConstructorResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBean()).isSameAs(ntb); + assertThat(bean.getBeanFactory()).isSameAs(bf); + + bean = (ConstructorResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBean()).isSameAs(ntb); + assertThat(bean.getBeanFactory()).isSameAs(bf); + } + + @Test + public void testConstructorResourceInjectionWithMultipleCandidatesAsCollection() { + bf.registerBeanDefinition("annotatedBean", + new RootBeanDefinition(ConstructorsCollectionResourceInjectionBean.class)); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + NestedTestBean ntb1 = new NestedTestBean(); + bf.registerSingleton("nestedTestBean1", ntb1); + NestedTestBean ntb2 = new NestedTestBean(); + bf.registerSingleton("nestedTestBean2", ntb2); + + ConstructorsCollectionResourceInjectionBean bean = (ConstructorsCollectionResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isNull(); + assertThat(bean.getTestBean4()).isSameAs(tb); + assertThat(bean.getNestedTestBeans().size()).isEqualTo(2); + assertThat(bean.getNestedTestBeans().get(0)).isSameAs(ntb1); + assertThat(bean.getNestedTestBeans().get(1)).isSameAs(ntb2); + } + + @Test + public void testConstructorResourceInjectionWithMultipleCandidatesAndFallback() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ConstructorsResourceInjectionBean.class)); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + + ConstructorsResourceInjectionBean bean = (ConstructorsResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean3()).isSameAs(tb); + assertThat(bean.getTestBean4()).isNull(); + } + + @Test + public void testConstructorInjectionWithMap() { + RootBeanDefinition bd = new RootBeanDefinition(MapConstructorInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + TestBean tb1 = new TestBean(); + TestBean tb2 = new TestBean(); + bf.registerSingleton("testBean1", tb1); + bf.registerSingleton("testBean2", tb1); + + MapConstructorInjectionBean bean = (MapConstructorInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBeanMap().size()).isEqualTo(2); + assertThat(bean.getTestBeanMap().keySet().contains("testBean1")).isTrue(); + assertThat(bean.getTestBeanMap().keySet().contains("testBean2")).isTrue(); + assertThat(bean.getTestBeanMap().values().contains(tb1)).isTrue(); + assertThat(bean.getTestBeanMap().values().contains(tb2)).isTrue(); + + bean = (MapConstructorInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBeanMap().size()).isEqualTo(2); + assertThat(bean.getTestBeanMap().keySet().contains("testBean1")).isTrue(); + assertThat(bean.getTestBeanMap().keySet().contains("testBean2")).isTrue(); + assertThat(bean.getTestBeanMap().values().contains(tb1)).isTrue(); + assertThat(bean.getTestBeanMap().values().contains(tb2)).isTrue(); + } + + @Test + public void testFieldInjectionWithMap() { + RootBeanDefinition bd = new RootBeanDefinition(MapFieldInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + TestBean tb1 = new TestBean(); + TestBean tb2 = new TestBean(); + bf.registerSingleton("testBean1", tb1); + bf.registerSingleton("testBean2", tb1); + + MapFieldInjectionBean bean = (MapFieldInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBeanMap().size()).isEqualTo(2); + assertThat(bean.getTestBeanMap().keySet().contains("testBean1")).isTrue(); + assertThat(bean.getTestBeanMap().keySet().contains("testBean2")).isTrue(); + assertThat(bean.getTestBeanMap().values().contains(tb1)).isTrue(); + assertThat(bean.getTestBeanMap().values().contains(tb2)).isTrue(); + + bean = (MapFieldInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBeanMap().size()).isEqualTo(2); + assertThat(bean.getTestBeanMap().keySet().contains("testBean1")).isTrue(); + assertThat(bean.getTestBeanMap().keySet().contains("testBean2")).isTrue(); + assertThat(bean.getTestBeanMap().values().contains(tb1)).isTrue(); + assertThat(bean.getTestBeanMap().values().contains(tb2)).isTrue(); + } + + @Test + public void testMethodInjectionWithMap() { + RootBeanDefinition bd = new RootBeanDefinition(MapMethodInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + + MapMethodInjectionBean bean = (MapMethodInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBeanMap().size()).isEqualTo(1); + assertThat(bean.getTestBeanMap().keySet().contains("testBean")).isTrue(); + assertThat(bean.getTestBeanMap().values().contains(tb)).isTrue(); + assertThat(bean.getTestBean()).isSameAs(tb); + + bean = (MapMethodInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBeanMap().size()).isEqualTo(1); + assertThat(bean.getTestBeanMap().keySet().contains("testBean")).isTrue(); + assertThat(bean.getTestBeanMap().values().contains(tb)).isTrue(); + assertThat(bean.getTestBean()).isSameAs(tb); + } + + @Test + public void testMethodInjectionWithMapAndMultipleMatches() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(MapMethodInjectionBean.class)); + bf.registerBeanDefinition("testBean1", new RootBeanDefinition(TestBean.class)); + bf.registerBeanDefinition("testBean2", new RootBeanDefinition(TestBean.class)); + assertThatExceptionOfType(BeanCreationException.class).as("should have failed, more than one bean of type").isThrownBy(() -> + bf.getBean("annotatedBean")); + } + + @Test + public void testMethodInjectionWithMapAndMultipleMatchesButOnlyOneAutowireCandidate() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(MapMethodInjectionBean.class)); + bf.registerBeanDefinition("testBean1", new RootBeanDefinition(TestBean.class)); + RootBeanDefinition rbd2 = new RootBeanDefinition(TestBean.class); + rbd2.setAutowireCandidate(false); + bf.registerBeanDefinition("testBean2", rbd2); + + MapMethodInjectionBean bean = (MapMethodInjectionBean) bf.getBean("annotatedBean"); + TestBean tb = (TestBean) bf.getBean("testBean1"); + assertThat(bean.getTestBeanMap().size()).isEqualTo(1); + assertThat(bean.getTestBeanMap().keySet().contains("testBean1")).isTrue(); + assertThat(bean.getTestBeanMap().values().contains(tb)).isTrue(); + assertThat(bean.getTestBean()).isSameAs(tb); + } + + @Test + public void testObjectFactoryInjection() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryQualifierFieldInjectionBean.class)); + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.addQualifier(new AutowireCandidateQualifier(Qualifier.class, "testBean")); + bf.registerBeanDefinition("testBean", bd); + bf.registerBeanDefinition("testBean2", new RootBeanDefinition(TestBean.class)); + + ObjectFactoryQualifierFieldInjectionBean bean = (ObjectFactoryQualifierFieldInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); + } + + @Test + public void testObjectFactoryQualifierInjection() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryQualifierFieldInjectionBean.class)); + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.addQualifier(new AutowireCandidateQualifier(Qualifier.class, "testBean")); + bf.registerBeanDefinition("testBean", bd); + + ObjectFactoryQualifierFieldInjectionBean bean = (ObjectFactoryQualifierFieldInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); + } + + @Test + public void testObjectFactoryFieldInjectionIntoPrototypeBean() { + RootBeanDefinition annotatedBeanDefinition = new RootBeanDefinition(ObjectFactoryQualifierFieldInjectionBean.class); + annotatedBeanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", annotatedBeanDefinition); + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.addQualifier(new AutowireCandidateQualifier(Qualifier.class, "testBean")); + bf.registerBeanDefinition("testBean", bd); + bf.registerBeanDefinition("testBean2", new RootBeanDefinition(TestBean.class)); + + ObjectFactoryQualifierFieldInjectionBean bean = (ObjectFactoryQualifierFieldInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); + ObjectFactoryQualifierFieldInjectionBean anotherBean = (ObjectFactoryQualifierFieldInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean).isNotSameAs(anotherBean); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); + } + + @Test + public void testObjectFactoryMethodInjectionIntoPrototypeBean() { + RootBeanDefinition annotatedBeanDefinition = new RootBeanDefinition(ObjectFactoryQualifierMethodInjectionBean.class); + annotatedBeanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", annotatedBeanDefinition); + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.addQualifier(new AutowireCandidateQualifier(Qualifier.class, "testBean")); + bf.registerBeanDefinition("testBean", bd); + bf.registerBeanDefinition("testBean2", new RootBeanDefinition(TestBean.class)); + + ObjectFactoryQualifierMethodInjectionBean bean = (ObjectFactoryQualifierMethodInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); + ObjectFactoryQualifierMethodInjectionBean anotherBean = (ObjectFactoryQualifierMethodInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean).isNotSameAs(anotherBean); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); + } + + @Test + public void testObjectFactoryWithBeanField() throws Exception { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryFieldInjectionBean.class)); + bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); + bf.setSerializationId("test"); + + ObjectFactoryFieldInjectionBean bean = (ObjectFactoryFieldInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); + bean = SerializationTestUtils.serializeAndDeserialize(bean); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); + } + + @Test + public void testObjectFactoryWithBeanMethod() throws Exception { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryMethodInjectionBean.class)); + bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); + bf.setSerializationId("test"); + + ObjectFactoryMethodInjectionBean bean = (ObjectFactoryMethodInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); + bean = SerializationTestUtils.serializeAndDeserialize(bean); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); + } + + @Test + public void testObjectFactoryWithTypedListField() throws Exception { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryListFieldInjectionBean.class)); + bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); + bf.setSerializationId("test"); + + ObjectFactoryListFieldInjectionBean bean = (ObjectFactoryListFieldInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); + bean = SerializationTestUtils.serializeAndDeserialize(bean); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); + } + + @Test + public void testObjectFactoryWithTypedListMethod() throws Exception { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryListMethodInjectionBean.class)); + bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); + bf.setSerializationId("test"); + + ObjectFactoryListMethodInjectionBean bean = (ObjectFactoryListMethodInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); + bean = SerializationTestUtils.serializeAndDeserialize(bean); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); + } + + @Test + public void testObjectFactoryWithTypedMapField() throws Exception { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryMapFieldInjectionBean.class)); + bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); + bf.setSerializationId("test"); + + ObjectFactoryMapFieldInjectionBean bean = (ObjectFactoryMapFieldInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); + bean = SerializationTestUtils.serializeAndDeserialize(bean); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); + } + + @Test + public void testObjectFactoryWithTypedMapMethod() throws Exception { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryMapMethodInjectionBean.class)); + bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); + bf.setSerializationId("test"); + + ObjectFactoryMapMethodInjectionBean bean = (ObjectFactoryMapMethodInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); + bean = SerializationTestUtils.serializeAndDeserialize(bean); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); + } + + /** + * Verifies that a dependency on a {@link org.springframework.beans.factory.FactoryBean} + * can be autowired via {@link org.springframework.beans.factory.annotation.Autowired @Inject}, + * specifically addressing SPR-4040. + */ + @Test + public void testBeanAutowiredWithFactoryBean() { + bf.registerBeanDefinition("factoryBeanDependentBean", new RootBeanDefinition(FactoryBeanDependentBean.class)); + bf.registerSingleton("stringFactoryBean", new StringFactoryBean()); + + final StringFactoryBean factoryBean = (StringFactoryBean) bf.getBean("&stringFactoryBean"); + final FactoryBeanDependentBean bean = (FactoryBeanDependentBean) bf.getBean("factoryBeanDependentBean"); + + assertThat(factoryBean).as("The singleton StringFactoryBean should have been registered.").isNotNull(); + assertThat(bean).as("The factoryBeanDependentBean should have been registered.").isNotNull(); + assertThat(bean.getFactoryBean()).as("The FactoryBeanDependentBean should have been autowired 'by type' with the StringFactoryBean.").isEqualTo(factoryBean); + } + + @Test + public void testNullableFieldInjectionWithBeanAvailable() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(NullableFieldInjectionBean.class)); + bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); + + NullableFieldInjectionBean bean = (NullableFieldInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); + } + + @Test + public void testNullableFieldInjectionWithBeanNotAvailable() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(NullableFieldInjectionBean.class)); + + NullableFieldInjectionBean bean = (NullableFieldInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isNull(); + } + + @Test + public void testNullableMethodInjectionWithBeanAvailable() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(NullableMethodInjectionBean.class)); + bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); + + NullableMethodInjectionBean bean = (NullableMethodInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean")); + } + + @Test + public void testNullableMethodInjectionWithBeanNotAvailable() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(NullableMethodInjectionBean.class)); + + NullableMethodInjectionBean bean = (NullableMethodInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isNull(); + } + + @Test + public void testOptionalFieldInjectionWithBeanAvailable() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(OptionalFieldInjectionBean.class)); + bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); + + OptionalFieldInjectionBean bean = (OptionalFieldInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean().isPresent()).isTrue(); + assertThat(bean.getTestBean().get()).isSameAs(bf.getBean("testBean")); + } + + @Test + public void testOptionalFieldInjectionWithBeanNotAvailable() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(OptionalFieldInjectionBean.class)); + + OptionalFieldInjectionBean bean = (OptionalFieldInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean().isPresent()).isFalse(); + } + + @Test + public void testOptionalMethodInjectionWithBeanAvailable() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(OptionalMethodInjectionBean.class)); + bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); + + OptionalMethodInjectionBean bean = (OptionalMethodInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean().isPresent()).isTrue(); + assertThat(bean.getTestBean().get()).isSameAs(bf.getBean("testBean")); + } + + @Test + public void testOptionalMethodInjectionWithBeanNotAvailable() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(OptionalMethodInjectionBean.class)); + + OptionalMethodInjectionBean bean = (OptionalMethodInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean().isPresent()).isFalse(); + } + + @Test + public void testOptionalListFieldInjectionWithBeanAvailable() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(OptionalListFieldInjectionBean.class)); + bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); + + OptionalListFieldInjectionBean bean = (OptionalListFieldInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean().isPresent()).isTrue(); + assertThat(bean.getTestBean().get().get(0)).isSameAs(bf.getBean("testBean")); + } + + @Test + public void testOptionalListFieldInjectionWithBeanNotAvailable() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(OptionalListFieldInjectionBean.class)); + + OptionalListFieldInjectionBean bean = (OptionalListFieldInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean().isPresent()).isFalse(); + } + + @Test + public void testOptionalListMethodInjectionWithBeanAvailable() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(OptionalListMethodInjectionBean.class)); + bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); + + OptionalListMethodInjectionBean bean = (OptionalListMethodInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean().isPresent()).isTrue(); + assertThat(bean.getTestBean().get().get(0)).isSameAs(bf.getBean("testBean")); + } + + @Test + public void testOptionalListMethodInjectionWithBeanNotAvailable() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(OptionalListMethodInjectionBean.class)); + + OptionalListMethodInjectionBean bean = (OptionalListMethodInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean().isPresent()).isFalse(); + } + + @Test + public void testProviderOfOptionalFieldInjectionWithBeanAvailable() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ProviderOfOptionalFieldInjectionBean.class)); + bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); + + ProviderOfOptionalFieldInjectionBean bean = (ProviderOfOptionalFieldInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean().isPresent()).isTrue(); + assertThat(bean.getTestBean().get()).isSameAs(bf.getBean("testBean")); + } + + @Test + public void testProviderOfOptionalFieldInjectionWithBeanNotAvailable() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ProviderOfOptionalFieldInjectionBean.class)); + + ProviderOfOptionalFieldInjectionBean bean = (ProviderOfOptionalFieldInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean().isPresent()).isFalse(); + } + + @Test + public void testProviderOfOptionalMethodInjectionWithBeanAvailable() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ProviderOfOptionalMethodInjectionBean.class)); + bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); + + ProviderOfOptionalMethodInjectionBean bean = (ProviderOfOptionalMethodInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean().isPresent()).isTrue(); + assertThat(bean.getTestBean().get()).isSameAs(bf.getBean("testBean")); + } + + @Test + public void testProviderOfOptionalMethodInjectionWithBeanNotAvailable() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ProviderOfOptionalMethodInjectionBean.class)); + + ProviderOfOptionalMethodInjectionBean bean = (ProviderOfOptionalMethodInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean().isPresent()).isFalse(); + } + + @Test + public void testAnnotatedDefaultConstructor() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(AnnotatedDefaultConstructorBean.class)); + + assertThat(bf.getBean("annotatedBean")).isNotNull(); + } + + + public static class ResourceInjectionBean { + + @Inject + private TestBean testBean; + + private TestBean testBean2; + + @Inject + public void setTestBean2(TestBean testBean2) { + if (this.testBean2 != null) { + throw new IllegalStateException("Already called"); + } + this.testBean2 = testBean2; + } + + public TestBean getTestBean() { + return this.testBean; + } + + public TestBean getTestBean2() { + return this.testBean2; + } + } + + + public static class ExtendedResourceInjectionBean extends ResourceInjectionBean { + + @Inject + protected ITestBean testBean3; + + private T nestedTestBean; + + private ITestBean testBean4; + + private BeanFactory beanFactory; + + public ExtendedResourceInjectionBean() { + } + + @Override + @Inject + @Required + @SuppressWarnings("deprecation") + public void setTestBean2(TestBean testBean2) { + super.setTestBean2(testBean2); + } + + @Inject + private void inject(ITestBean testBean4, T nestedTestBean) { + this.testBean4 = testBean4; + this.nestedTestBean = nestedTestBean; + } + + @Inject + protected void initBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + public ITestBean getTestBean3() { + return this.testBean3; + } + + public ITestBean getTestBean4() { + return this.testBean4; + } + + public T getNestedTestBean() { + return this.nestedTestBean; + } + + public BeanFactory getBeanFactory() { + return this.beanFactory; + } + } + + + public static class TypedExtendedResourceInjectionBean extends ExtendedResourceInjectionBean { + } + + + public static class OptionalResourceInjectionBean extends ResourceInjectionBean { + + @Inject + protected ITestBean testBean3; + + private IndexedTestBean indexedTestBean; + + private NestedTestBean[] nestedTestBeans; + + @Inject + public NestedTestBean[] nestedTestBeansField; + + private ITestBean testBean4; + + @Override + @Inject + public void setTestBean2(TestBean testBean2) { + super.setTestBean2(testBean2); + } + + @Inject + private void inject(ITestBean testBean4, NestedTestBean[] nestedTestBeans, IndexedTestBean indexedTestBean) { + this.testBean4 = testBean4; + this.indexedTestBean = indexedTestBean; + this.nestedTestBeans = nestedTestBeans; + } + + public ITestBean getTestBean3() { + return this.testBean3; + } + + public ITestBean getTestBean4() { + return this.testBean4; + } + + public IndexedTestBean getIndexedTestBean() { + return this.indexedTestBean; + } + + public NestedTestBean[] getNestedTestBeans() { + return this.nestedTestBeans; + } + } + + + public static class OptionalCollectionResourceInjectionBean extends ResourceInjectionBean { + + @Inject + protected ITestBean testBean3; + + private IndexedTestBean indexedTestBean; + + private List nestedTestBeans; + + public List nestedTestBeansSetter; + + @Inject + public List nestedTestBeansField; + + private ITestBean testBean4; + + @Override + @Inject + public void setTestBean2(TestBean testBean2) { + super.setTestBean2(testBean2); + } + + @Inject + private void inject(ITestBean testBean4, List nestedTestBeans, IndexedTestBean indexedTestBean) { + this.testBean4 = testBean4; + this.indexedTestBean = indexedTestBean; + this.nestedTestBeans = nestedTestBeans; + } + + @Inject + public void setNestedTestBeans(List nestedTestBeans) { + this.nestedTestBeansSetter = nestedTestBeans; + } + + public ITestBean getTestBean3() { + return this.testBean3; + } + + public ITestBean getTestBean4() { + return this.testBean4; + } + + public IndexedTestBean getIndexedTestBean() { + return this.indexedTestBean; + } + + public List getNestedTestBeans() { + return this.nestedTestBeans; + } + } + + + public static class ConstructorResourceInjectionBean extends ResourceInjectionBean { + + @Inject + protected ITestBean testBean3; + + private ITestBean testBean4; + + private NestedTestBean nestedTestBean; + + private ConfigurableListableBeanFactory beanFactory; + + + public ConstructorResourceInjectionBean() { + throw new UnsupportedOperationException(); + } + + public ConstructorResourceInjectionBean(ITestBean testBean3) { + throw new UnsupportedOperationException(); + } + + @Inject + public ConstructorResourceInjectionBean(ITestBean testBean4, NestedTestBean nestedTestBean, + ConfigurableListableBeanFactory beanFactory) { + this.testBean4 = testBean4; + this.nestedTestBean = nestedTestBean; + this.beanFactory = beanFactory; + } + + public ConstructorResourceInjectionBean(NestedTestBean nestedTestBean) { + throw new UnsupportedOperationException(); + } + + public ConstructorResourceInjectionBean(ITestBean testBean3, ITestBean testBean4, NestedTestBean nestedTestBean) { + throw new UnsupportedOperationException(); + } + + @Override + @Inject + public void setTestBean2(TestBean testBean2) { + super.setTestBean2(testBean2); + } + + public ITestBean getTestBean3() { + return this.testBean3; + } + + public ITestBean getTestBean4() { + return this.testBean4; + } + + public NestedTestBean getNestedTestBean() { + return this.nestedTestBean; + } + + public ConfigurableListableBeanFactory getBeanFactory() { + return this.beanFactory; + } + } + + + public static class ConstructorsResourceInjectionBean { + + protected ITestBean testBean3; + + private ITestBean testBean4; + + private NestedTestBean[] nestedTestBeans; + + public ConstructorsResourceInjectionBean() { + } + + @Inject + public ConstructorsResourceInjectionBean(ITestBean testBean3) { + this.testBean3 = testBean3; + } + + public ConstructorsResourceInjectionBean(ITestBean testBean4, NestedTestBean[] nestedTestBeans) { + this.testBean4 = testBean4; + this.nestedTestBeans = nestedTestBeans; + } + + public ConstructorsResourceInjectionBean(NestedTestBean nestedTestBean) { + throw new UnsupportedOperationException(); + } + + public ConstructorsResourceInjectionBean(ITestBean testBean3, ITestBean testBean4, NestedTestBean nestedTestBean) { + throw new UnsupportedOperationException(); + } + + public ITestBean getTestBean3() { + return this.testBean3; + } + + public ITestBean getTestBean4() { + return this.testBean4; + } + + public NestedTestBean[] getNestedTestBeans() { + return this.nestedTestBeans; + } + } + + + public static class ConstructorsCollectionResourceInjectionBean { + + protected ITestBean testBean3; + + private ITestBean testBean4; + + private List nestedTestBeans; + + public ConstructorsCollectionResourceInjectionBean() { + } + + public ConstructorsCollectionResourceInjectionBean(ITestBean testBean3) { + this.testBean3 = testBean3; + } + + @Inject + public ConstructorsCollectionResourceInjectionBean(ITestBean testBean4, List nestedTestBeans) { + this.testBean4 = testBean4; + this.nestedTestBeans = nestedTestBeans; + } + + public ConstructorsCollectionResourceInjectionBean(NestedTestBean nestedTestBean) { + throw new UnsupportedOperationException(); + } + + public ConstructorsCollectionResourceInjectionBean(ITestBean testBean3, ITestBean testBean4, + NestedTestBean nestedTestBean) { + throw new UnsupportedOperationException(); + } + + public ITestBean getTestBean3() { + return this.testBean3; + } + + public ITestBean getTestBean4() { + return this.testBean4; + } + + public List getNestedTestBeans() { + return this.nestedTestBeans; + } + } + + + public static class MapConstructorInjectionBean { + + private Map testBeanMap; + + @Inject + public MapConstructorInjectionBean(Map testBeanMap) { + this.testBeanMap = testBeanMap; + } + + public Map getTestBeanMap() { + return this.testBeanMap; + } + } + + + public static class MapFieldInjectionBean { + + @Inject + private Map testBeanMap; + + public Map getTestBeanMap() { + return this.testBeanMap; + } + } + + + public static class MapMethodInjectionBean { + + private TestBean testBean; + + private Map testBeanMap; + + @Inject + public void setTestBeanMap(TestBean testBean, Map testBeanMap) { + this.testBean = testBean; + this.testBeanMap = testBeanMap; + } + + public TestBean getTestBean() { + return this.testBean; + } + + public Map getTestBeanMap() { + return this.testBeanMap; + } + } + + + @SuppressWarnings("serial") + public static class ObjectFactoryFieldInjectionBean implements Serializable { + + @Inject + private Provider testBeanFactory; + + public TestBean getTestBean() { + return this.testBeanFactory.get(); + } + } + + + @SuppressWarnings("serial") + public static class ObjectFactoryMethodInjectionBean implements Serializable { + + private Provider testBeanFactory; + + @Inject + public void setTestBeanFactory(Provider testBeanFactory) { + this.testBeanFactory = testBeanFactory; + } + + public TestBean getTestBean() { + return this.testBeanFactory.get(); + } + } + + + public static class ObjectFactoryQualifierFieldInjectionBean { + + @Inject + @Named("testBean") + private Provider testBeanFactory; + + public TestBean getTestBean() { + return (TestBean) this.testBeanFactory.get(); + } + } + + + public static class ObjectFactoryQualifierMethodInjectionBean { + + private Provider testBeanFactory; + + @Inject + @Named("testBean") + public void setTestBeanFactory(Provider testBeanFactory) { + this.testBeanFactory = testBeanFactory; + } + + public TestBean getTestBean() { + return (TestBean) this.testBeanFactory.get(); + } + } + + + @SuppressWarnings("serial") + public static class ObjectFactoryListFieldInjectionBean implements Serializable { + + @Inject + private Provider> testBeanFactory; + + public void setTestBeanFactory(Provider> testBeanFactory) { + this.testBeanFactory = testBeanFactory; + } + + public TestBean getTestBean() { + return this.testBeanFactory.get().get(0); + } + } + + + @SuppressWarnings("serial") + public static class ObjectFactoryListMethodInjectionBean implements Serializable { + + private Provider> testBeanFactory; + + @Inject + public void setTestBeanFactory(Provider> testBeanFactory) { + this.testBeanFactory = testBeanFactory; + } + + public TestBean getTestBean() { + return this.testBeanFactory.get().get(0); + } + } + + + @SuppressWarnings("serial") + public static class ObjectFactoryMapFieldInjectionBean implements Serializable { + + @Inject + private Provider> testBeanFactory; + + public void setTestBeanFactory(Provider> testBeanFactory) { + this.testBeanFactory = testBeanFactory; + } + + public TestBean getTestBean() { + return this.testBeanFactory.get().values().iterator().next(); + } + } + + + @SuppressWarnings("serial") + public static class ObjectFactoryMapMethodInjectionBean implements Serializable { + + private Provider> testBeanFactory; + + @Inject + public void setTestBeanFactory(Provider> testBeanFactory) { + this.testBeanFactory = testBeanFactory; + } + + public TestBean getTestBean() { + return this.testBeanFactory.get().values().iterator().next(); + } + } + + + /** + * Bean with a dependency on a {@link org.springframework.beans.factory.FactoryBean}. + */ + private static class FactoryBeanDependentBean { + + @Inject + private FactoryBean factoryBean; + + public final FactoryBean getFactoryBean() { + return this.factoryBean; + } + } + + + public static class StringFactoryBean implements FactoryBean { + + @Override + public String getObject() { + return ""; + } + + @Override + public Class getObjectType() { + return String.class; + } + + @Override + public boolean isSingleton() { + return true; + } + } + + + @Retention(RetentionPolicy.RUNTIME) + public @interface Nullable {} + + + public static class NullableFieldInjectionBean { + + @Inject @Nullable + private TestBean testBean; + + public TestBean getTestBean() { + return this.testBean; + } + } + + + public static class NullableMethodInjectionBean { + + private TestBean testBean; + + @Inject + public void setTestBean(@Nullable TestBean testBean) { + this.testBean = testBean; + } + + public TestBean getTestBean() { + return this.testBean; + } + } + + + public static class OptionalFieldInjectionBean { + + @Inject + private Optional testBean; + + public Optional getTestBean() { + return this.testBean; + } + } + + + public static class OptionalMethodInjectionBean { + + private Optional testBean; + + @Inject + public void setTestBean(Optional testBean) { + this.testBean = testBean; + } + + public Optional getTestBean() { + return this.testBean; + } + } + + + public static class OptionalListFieldInjectionBean { + + @Inject + private Optional> testBean; + + public Optional> getTestBean() { + return this.testBean; + } + } + + + public static class OptionalListMethodInjectionBean { + + private Optional> testBean; + + @Inject + public void setTestBean(Optional> testBean) { + this.testBean = testBean; + } + + public Optional> getTestBean() { + return this.testBean; + } + } + + + public static class ProviderOfOptionalFieldInjectionBean { + + @Inject + private Provider> testBean; + + public Optional getTestBean() { + return this.testBean.get(); + } + } + + + public static class ProviderOfOptionalMethodInjectionBean { + + private Provider> testBean; + + @Inject + public void setTestBean(Provider> testBean) { + this.testBean = testBean; + } + + public Optional getTestBean() { + return this.testBean.get(); + } + } + + + public static class AnnotatedDefaultConstructorBean { + + @Inject + public AnnotatedDefaultConstructorBean() { + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/LookupAnnotationTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/LookupAnnotationTests.java new file mode 100644 index 0000000..bd30b5b --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/LookupAnnotationTests.java @@ -0,0 +1,150 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.annotation; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Karl Pietrzak + * @author Juergen Hoeller + */ +public class LookupAnnotationTests { + + private DefaultListableBeanFactory beanFactory; + + + @BeforeEach + public void setup() { + beanFactory = new DefaultListableBeanFactory(); + AutowiredAnnotationBeanPostProcessor aabpp = new AutowiredAnnotationBeanPostProcessor(); + aabpp.setBeanFactory(beanFactory); + beanFactory.addBeanPostProcessor(aabpp); + beanFactory.registerBeanDefinition("abstractBean", new RootBeanDefinition(AbstractBean.class)); + beanFactory.registerBeanDefinition("beanConsumer", new RootBeanDefinition(BeanConsumer.class)); + RootBeanDefinition tbd = new RootBeanDefinition(TestBean.class); + tbd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + beanFactory.registerBeanDefinition("testBean", tbd); + } + + + @Test + public void testWithoutConstructorArg() { + AbstractBean bean = (AbstractBean) beanFactory.getBean("abstractBean"); + assertThat(bean).isNotNull(); + Object expected = bean.get(); + assertThat(expected.getClass()).isEqualTo(TestBean.class); + assertThat(beanFactory.getBean(BeanConsumer.class).abstractBean).isSameAs(bean); + } + + @Test + public void testWithOverloadedArg() { + AbstractBean bean = (AbstractBean) beanFactory.getBean("abstractBean"); + assertThat(bean).isNotNull(); + TestBean expected = bean.get("haha"); + assertThat(expected.getClass()).isEqualTo(TestBean.class); + assertThat(expected.getName()).isEqualTo("haha"); + assertThat(beanFactory.getBean(BeanConsumer.class).abstractBean).isSameAs(bean); + } + + @Test + public void testWithOneConstructorArg() { + AbstractBean bean = (AbstractBean) beanFactory.getBean("abstractBean"); + assertThat(bean).isNotNull(); + TestBean expected = bean.getOneArgument("haha"); + assertThat(expected.getClass()).isEqualTo(TestBean.class); + assertThat(expected.getName()).isEqualTo("haha"); + assertThat(beanFactory.getBean(BeanConsumer.class).abstractBean).isSameAs(bean); + } + + @Test + public void testWithTwoConstructorArg() { + AbstractBean bean = (AbstractBean) beanFactory.getBean("abstractBean"); + assertThat(bean).isNotNull(); + TestBean expected = bean.getTwoArguments("haha", 72); + assertThat(expected.getClass()).isEqualTo(TestBean.class); + assertThat(expected.getName()).isEqualTo("haha"); + assertThat(expected.getAge()).isEqualTo(72); + assertThat(beanFactory.getBean(BeanConsumer.class).abstractBean).isSameAs(bean); + } + + @Test + public void testWithThreeArgsShouldFail() { + AbstractBean bean = (AbstractBean) beanFactory.getBean("abstractBean"); + assertThat(bean).isNotNull(); + assertThatExceptionOfType(AbstractMethodError.class).as("TestBean has no three arg constructor").isThrownBy(() -> + bean.getThreeArguments("name", 1, 2)); + assertThat(beanFactory.getBean(BeanConsumer.class).abstractBean).isSameAs(bean); + } + + @Test + public void testWithEarlyInjection() { + AbstractBean bean = beanFactory.getBean("beanConsumer", BeanConsumer.class).abstractBean; + assertThat(bean).isNotNull(); + Object expected = bean.get(); + assertThat(expected.getClass()).isEqualTo(TestBean.class); + assertThat(beanFactory.getBean(BeanConsumer.class).abstractBean).isSameAs(bean); + } + + @Test // gh-25806 + public void testWithNullBean() { + RootBeanDefinition tbd = new RootBeanDefinition(TestBean.class, () -> null); + tbd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + beanFactory.registerBeanDefinition("testBean", tbd); + + AbstractBean bean = beanFactory.getBean("beanConsumer", BeanConsumer.class).abstractBean; + assertThat(bean).isNotNull(); + Object expected = bean.get(); + assertThat(expected).isNull(); + assertThat(beanFactory.getBean(BeanConsumer.class).abstractBean).isSameAs(bean); + } + + + public static abstract class AbstractBean { + + @Lookup("testBean") + public abstract TestBean get(); + + @Lookup + public abstract TestBean get(String name); // overloaded + + @Lookup + public abstract TestBean getOneArgument(String name); + + @Lookup + public abstract TestBean getTwoArguments(String name, int age); + + // no @Lookup annotation + public abstract TestBean getThreeArguments(String name, int age, int anotherArg); + } + + + public static class BeanConsumer { + + @Autowired + AbstractBean abstractBean; + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/ParameterResolutionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/ParameterResolutionTests.java new file mode 100644 index 0000000..9710d29 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/ParameterResolutionTests.java @@ -0,0 +1,167 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.annotation; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link ParameterResolutionDelegate}. + * + * @author Sam Brannen + * @author Juergen Hoeller + * @author Loïc Ledoyen + */ +public class ParameterResolutionTests { + + @Test + public void isAutowirablePreconditions() { + assertThatIllegalArgumentException().isThrownBy(() -> + ParameterResolutionDelegate.isAutowirable(null, 0)) + .withMessageContaining("Parameter must not be null"); + } + + @Test + public void annotatedParametersInMethodAreCandidatesForAutowiring() throws Exception { + Method method = getClass().getDeclaredMethod("autowirableMethod", String.class, String.class, String.class, String.class); + assertAutowirableParameters(method); + } + + @Test + public void annotatedParametersInTopLevelClassConstructorAreCandidatesForAutowiring() throws Exception { + Constructor constructor = AutowirableClass.class.getConstructor(String.class, String.class, String.class, String.class); + assertAutowirableParameters(constructor); + } + + @Test + public void annotatedParametersInInnerClassConstructorAreCandidatesForAutowiring() throws Exception { + Class innerClass = AutowirableClass.InnerAutowirableClass.class; + assertThat(ClassUtils.isInnerClass(innerClass)).isTrue(); + Constructor constructor = innerClass.getConstructor(AutowirableClass.class, String.class, String.class); + assertAutowirableParameters(constructor); + } + + private void assertAutowirableParameters(Executable executable) { + int startIndex = (executable instanceof Constructor) + && ClassUtils.isInnerClass(executable.getDeclaringClass()) ? 1 : 0; + Parameter[] parameters = executable.getParameters(); + for (int parameterIndex = startIndex; parameterIndex < parameters.length; parameterIndex++) { + Parameter parameter = parameters[parameterIndex]; + assertThat(ParameterResolutionDelegate.isAutowirable(parameter, parameterIndex)).as("Parameter " + parameter + " must be autowirable").isTrue(); + } + } + + @Test + public void nonAnnotatedParametersInTopLevelClassConstructorAreNotCandidatesForAutowiring() throws Exception { + Constructor notAutowirableConstructor = AutowirableClass.class.getConstructor(String.class); + + Parameter[] parameters = notAutowirableConstructor.getParameters(); + for (int parameterIndex = 0; parameterIndex < parameters.length; parameterIndex++) { + Parameter parameter = parameters[parameterIndex]; + assertThat(ParameterResolutionDelegate.isAutowirable(parameter, parameterIndex)).as("Parameter " + parameter + " must not be autowirable").isFalse(); + } + } + + @Test + public void resolveDependencyPreconditionsForParameter() { + assertThatIllegalArgumentException().isThrownBy(() -> + ParameterResolutionDelegate.resolveDependency(null, 0, null, mock(AutowireCapableBeanFactory.class))) + .withMessageContaining("Parameter must not be null"); + } + + @Test + public void resolveDependencyPreconditionsForContainingClass() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + ParameterResolutionDelegate.resolveDependency(getParameter(), 0, null, null)) + .withMessageContaining("Containing class must not be null"); + } + + @Test + public void resolveDependencyPreconditionsForBeanFactory() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + ParameterResolutionDelegate.resolveDependency(getParameter(), 0, getClass(), null)) + .withMessageContaining("AutowireCapableBeanFactory must not be null"); + } + + private Parameter getParameter() throws NoSuchMethodException { + Method method = getClass().getDeclaredMethod("autowirableMethod", String.class, String.class, String.class, String.class); + return method.getParameters()[0]; + } + + @Test + public void resolveDependencyForAnnotatedParametersInTopLevelClassConstructor() throws Exception { + Constructor constructor = AutowirableClass.class.getConstructor(String.class, String.class, String.class, String.class); + + AutowireCapableBeanFactory beanFactory = mock(AutowireCapableBeanFactory.class); + // Configure the mocked BeanFactory to return the DependencyDescriptor for convenience and + // to avoid using an ArgumentCaptor. + given(beanFactory.resolveDependency(any(), isNull())).willAnswer(invocation -> invocation.getArgument(0)); + + Parameter[] parameters = constructor.getParameters(); + for (int parameterIndex = 0; parameterIndex < parameters.length; parameterIndex++) { + Parameter parameter = parameters[parameterIndex]; + DependencyDescriptor intermediateDependencyDescriptor = (DependencyDescriptor) ParameterResolutionDelegate.resolveDependency( + parameter, parameterIndex, AutowirableClass.class, beanFactory); + assertThat(intermediateDependencyDescriptor.getAnnotatedElement()).isEqualTo(constructor); + assertThat(intermediateDependencyDescriptor.getMethodParameter().getParameter()).isEqualTo(parameter); + } + } + + + void autowirableMethod( + @Autowired String firstParameter, + @Qualifier("someQualifier") String secondParameter, + @Value("${someValue}") String thirdParameter, + @Autowired(required = false) String fourthParameter) { + } + + + public static class AutowirableClass { + + public AutowirableClass(@Autowired String firstParameter, + @Qualifier("someQualifier") String secondParameter, + @Value("${someValue}") String thirdParameter, + @Autowired(required = false) String fourthParameter) { + } + + public AutowirableClass(String notAutowirableParameter) { + } + + public class InnerAutowirableClass { + + public InnerAutowirableClass(@Autowired String firstParameter, + @Qualifier("someQualifier") String secondParameter) { + } + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/RequiredAnnotationBeanPostProcessorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/RequiredAnnotationBeanPostProcessorTests.java new file mode 100644 index 0000000..a88a756 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/RequiredAnnotationBeanPostProcessorTests.java @@ -0,0 +1,244 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Rob Harrop + * @author Chris Beams + * @since 2.0 + */ +@Deprecated +public class RequiredAnnotationBeanPostProcessorTests { + + @Test + public void testWithRequiredPropertyOmitted() { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + BeanDefinition beanDef = BeanDefinitionBuilder + .genericBeanDefinition(RequiredTestBean.class) + .addPropertyValue("name", "Rob Harrop") + .addPropertyValue("favouriteColour", "Blue") + .addPropertyValue("jobTitle", "Grand Poobah") + .getBeanDefinition(); + factory.registerBeanDefinition("testBean", beanDef); + factory.addBeanPostProcessor(new RequiredAnnotationBeanPostProcessor()); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy( + factory::preInstantiateSingletons) + .withMessageContaining("Property") + .withMessageContaining("age") + .withMessageContaining("testBean"); + } + + @Test + public void testWithThreeRequiredPropertiesOmitted() { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + BeanDefinition beanDef = BeanDefinitionBuilder + .genericBeanDefinition(RequiredTestBean.class) + .addPropertyValue("name", "Rob Harrop") + .getBeanDefinition(); + factory.registerBeanDefinition("testBean", beanDef); + factory.addBeanPostProcessor(new RequiredAnnotationBeanPostProcessor()); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy( + factory::preInstantiateSingletons) + .withMessageContaining("Properties") + .withMessageContaining("age") + .withMessageContaining("favouriteColour") + .withMessageContaining("jobTitle") + .withMessageContaining("testBean"); + } + + @Test + public void testWithAllRequiredPropertiesSpecified() { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + BeanDefinition beanDef = BeanDefinitionBuilder + .genericBeanDefinition(RequiredTestBean.class) + .addPropertyValue("age", "24") + .addPropertyValue("favouriteColour", "Blue") + .addPropertyValue("jobTitle", "Grand Poobah") + .getBeanDefinition(); + factory.registerBeanDefinition("testBean", beanDef); + factory.addBeanPostProcessor(new RequiredAnnotationBeanPostProcessor()); + factory.preInstantiateSingletons(); + RequiredTestBean bean = (RequiredTestBean) factory.getBean("testBean"); + assertThat(bean.getAge()).isEqualTo(24); + assertThat(bean.getFavouriteColour()).isEqualTo("Blue"); + } + + @Test + public void testWithCustomAnnotation() { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + BeanDefinition beanDef = BeanDefinitionBuilder + .genericBeanDefinition(RequiredTestBean.class) + .getBeanDefinition(); + factory.registerBeanDefinition("testBean", beanDef); + RequiredAnnotationBeanPostProcessor rabpp = new RequiredAnnotationBeanPostProcessor(); + rabpp.setRequiredAnnotationType(MyRequired.class); + factory.addBeanPostProcessor(rabpp); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy( + factory::preInstantiateSingletons) + .withMessageContaining("Property") + .withMessageContaining("name") + .withMessageContaining("testBean"); + } + + @Test + public void testWithStaticFactoryMethod() { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + BeanDefinition beanDef = BeanDefinitionBuilder + .genericBeanDefinition(RequiredTestBean.class) + .setFactoryMethod("create") + .addPropertyValue("name", "Rob Harrop") + .addPropertyValue("favouriteColour", "Blue") + .addPropertyValue("jobTitle", "Grand Poobah") + .getBeanDefinition(); + factory.registerBeanDefinition("testBean", beanDef); + factory.addBeanPostProcessor(new RequiredAnnotationBeanPostProcessor()); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy( + factory::preInstantiateSingletons) + .withMessageContaining("Property") + .withMessageContaining("age") + .withMessageContaining("testBean"); + } + + @Test + public void testWithStaticFactoryMethodAndRequiredPropertiesSpecified() { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + BeanDefinition beanDef = BeanDefinitionBuilder + .genericBeanDefinition(RequiredTestBean.class) + .setFactoryMethod("create") + .addPropertyValue("age", "24") + .addPropertyValue("favouriteColour", "Blue") + .addPropertyValue("jobTitle", "Grand Poobah") + .getBeanDefinition(); + factory.registerBeanDefinition("testBean", beanDef); + factory.addBeanPostProcessor(new RequiredAnnotationBeanPostProcessor()); + factory.preInstantiateSingletons(); + RequiredTestBean bean = (RequiredTestBean) factory.getBean("testBean"); + assertThat(bean.getAge()).isEqualTo(24); + assertThat(bean.getFavouriteColour()).isEqualTo("Blue"); + } + + @Test + public void testWithFactoryBean() { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + RootBeanDefinition beanDef = new RootBeanDefinition(RequiredTestBean.class); + beanDef.setFactoryBeanName("testBeanFactory"); + beanDef.setFactoryMethodName("create"); + factory.registerBeanDefinition("testBean", beanDef); + factory.registerBeanDefinition("testBeanFactory", new RootBeanDefinition(RequiredTestBeanFactory.class)); + RequiredAnnotationBeanPostProcessor bpp = new RequiredAnnotationBeanPostProcessor(); + bpp.setBeanFactory(factory); + factory.addBeanPostProcessor(bpp); + factory.preInstantiateSingletons(); + } + + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface MyRequired { + } + + + public static class RequiredTestBean implements BeanNameAware, BeanFactoryAware { + + private String name; + + private int age; + + private String favouriteColour; + + private String jobTitle; + + + public int getAge() { + return age; + } + + @Required + public void setAge(int age) { + this.age = age; + } + + public String getName() { + return name; + } + + @MyRequired + public void setName(String name) { + this.name = name; + } + + public String getFavouriteColour() { + return favouriteColour; + } + + @Required + public void setFavouriteColour(String favouriteColour) { + this.favouriteColour = favouriteColour; + } + + public String getJobTitle() { + return jobTitle; + } + + @Required + public void setJobTitle(String jobTitle) { + this.jobTitle = jobTitle; + } + + @Override + @Required + public void setBeanName(String name) { + } + + @Override + @Required + public void setBeanFactory(BeanFactory beanFactory) { + } + + public static RequiredTestBean create() { + return new RequiredTestBean(); + } + } + + + public static class RequiredTestBeanFactory { + + public RequiredTestBean create() { + return new RequiredTestBean(); + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/CustomEditorConfigurerTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/CustomEditorConfigurerTests.java new file mode 100644 index 0000000..a94425c --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/CustomEditorConfigurerTests.java @@ -0,0 +1,135 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.beans.PropertyEditor; +import java.beans.PropertyEditorSupport; +import java.text.DateFormat; +import java.text.ParseException; +import java.util.Date; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.PropertyEditorRegistrar; +import org.springframework.beans.PropertyEditorRegistry; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.propertyeditors.CustomDateEditor; +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @author Chris Beams + * @since 31.07.2004 + */ +public class CustomEditorConfigurerTests { + + @Test + public void testCustomEditorConfigurerWithPropertyEditorRegistrar() throws ParseException { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + CustomEditorConfigurer cec = new CustomEditorConfigurer(); + final DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, Locale.GERMAN); + cec.setPropertyEditorRegistrars(new PropertyEditorRegistrar[] { + new PropertyEditorRegistrar() { + @Override + public void registerCustomEditors(PropertyEditorRegistry registry) { + registry.registerCustomEditor(Date.class, new CustomDateEditor(df, true)); + } + }}); + cec.postProcessBeanFactory(bf); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("date", "2.12.1975"); + RootBeanDefinition bd1 = new RootBeanDefinition(TestBean.class); + bd1.setPropertyValues(pvs); + bf.registerBeanDefinition("tb1", bd1); + pvs = new MutablePropertyValues(); + pvs.add("someMap[myKey]", new TypedStringValue("2.12.1975", Date.class)); + RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); + bd2.setPropertyValues(pvs); + bf.registerBeanDefinition("tb2", bd2); + + TestBean tb1 = (TestBean) bf.getBean("tb1"); + assertThat(tb1.getDate()).isEqualTo(df.parse("2.12.1975")); + TestBean tb2 = (TestBean) bf.getBean("tb2"); + assertThat(tb2.getSomeMap().get("myKey")).isEqualTo(df.parse("2.12.1975")); + } + + @Test + public void testCustomEditorConfigurerWithEditorAsClass() throws ParseException { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + CustomEditorConfigurer cec = new CustomEditorConfigurer(); + Map, Class> editors = new HashMap<>(); + editors.put(Date.class, MyDateEditor.class); + cec.setCustomEditors(editors); + cec.postProcessBeanFactory(bf); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("date", "2.12.1975"); + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.setPropertyValues(pvs); + bf.registerBeanDefinition("tb", bd); + + TestBean tb = (TestBean) bf.getBean("tb"); + DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, Locale.GERMAN); + assertThat(tb.getDate()).isEqualTo(df.parse("2.12.1975")); + } + + @Test + public void testCustomEditorConfigurerWithRequiredTypeArray() throws ParseException { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + CustomEditorConfigurer cec = new CustomEditorConfigurer(); + Map, Class> editors = new HashMap<>(); + editors.put(String[].class, MyTestEditor.class); + cec.setCustomEditors(editors); + cec.postProcessBeanFactory(bf); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("stringArray", "xxx"); + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.setPropertyValues(pvs); + bf.registerBeanDefinition("tb", bd); + + TestBean tb = (TestBean) bf.getBean("tb"); + assertThat(tb.getStringArray() != null && tb.getStringArray().length == 1).isTrue(); + assertThat(tb.getStringArray()[0]).isEqualTo("test"); + } + + + public static class MyDateEditor extends CustomDateEditor { + + public MyDateEditor() { + super(DateFormat.getDateInstance(DateFormat.SHORT, Locale.GERMAN), true); + } + } + + + public static class MyTestEditor extends PropertyEditorSupport { + + @Override + public void setAsText(String text) { + setValue(new String[] {"test"}); + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/CustomScopeConfigurerTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/CustomScopeConfigurerTests.java new file mode 100644 index 0000000..c302652 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/CustomScopeConfigurerTests.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link CustomScopeConfigurer}. + * + * @author Rick Evans + * @author Juergen Hoeller + * @author Chris Beams + */ +public class CustomScopeConfigurerTests { + + private static final String FOO_SCOPE = "fooScope"; + + private final ConfigurableListableBeanFactory factory = new DefaultListableBeanFactory(); + + + @Test + public void testWithNoScopes() { + CustomScopeConfigurer figurer = new CustomScopeConfigurer(); + figurer.postProcessBeanFactory(factory); + } + + @Test + public void testSunnyDayWithBonaFideScopeInstance() { + Scope scope = mock(Scope.class); + factory.registerScope(FOO_SCOPE, scope); + Map scopes = new HashMap<>(); + scopes.put(FOO_SCOPE, scope); + CustomScopeConfigurer figurer = new CustomScopeConfigurer(); + figurer.setScopes(scopes); + figurer.postProcessBeanFactory(factory); + } + + @Test + public void testSunnyDayWithBonaFideScopeClass() { + Map scopes = new HashMap<>(); + scopes.put(FOO_SCOPE, NoOpScope.class); + CustomScopeConfigurer figurer = new CustomScopeConfigurer(); + figurer.setScopes(scopes); + figurer.postProcessBeanFactory(factory); + boolean condition = factory.getRegisteredScope(FOO_SCOPE) instanceof NoOpScope; + assertThat(condition).isTrue(); + } + + @Test + public void testSunnyDayWithBonaFideScopeClassName() { + Map scopes = new HashMap<>(); + scopes.put(FOO_SCOPE, NoOpScope.class.getName()); + CustomScopeConfigurer figurer = new CustomScopeConfigurer(); + figurer.setScopes(scopes); + figurer.postProcessBeanFactory(factory); + boolean condition = factory.getRegisteredScope(FOO_SCOPE) instanceof NoOpScope; + assertThat(condition).isTrue(); + } + + @Test + public void testWhereScopeMapHasNullScopeValueInEntrySet() { + Map scopes = new HashMap<>(); + scopes.put(FOO_SCOPE, null); + CustomScopeConfigurer figurer = new CustomScopeConfigurer(); + figurer.setScopes(scopes); + assertThatIllegalArgumentException().isThrownBy(() -> + figurer.postProcessBeanFactory(factory)); + } + + @Test + public void testWhereScopeMapHasNonScopeInstanceInEntrySet() { + Map scopes = new HashMap<>(); + scopes.put(FOO_SCOPE, this); // <-- not a valid value... + CustomScopeConfigurer figurer = new CustomScopeConfigurer(); + figurer.setScopes(scopes); + assertThatIllegalArgumentException().isThrownBy(() -> + figurer.postProcessBeanFactory(factory)); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void testWhereScopeMapHasNonStringTypedScopeNameInKeySet() { + Map scopes = new HashMap(); + scopes.put(this, new NoOpScope()); // <-- not a valid value (the key)... + CustomScopeConfigurer figurer = new CustomScopeConfigurer(); + figurer.setScopes(scopes); + assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> + figurer.postProcessBeanFactory(factory)); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/DeprecatedBeanWarnerTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/DeprecatedBeanWarnerTests.java new file mode 100644 index 0000000..2be93f0 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/DeprecatedBeanWarnerTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + */ +public class DeprecatedBeanWarnerTests { + + private String beanName; + + private BeanDefinition beanDefinition; + + + @Test + @SuppressWarnings("deprecation") + public void postProcess() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + BeanDefinition def = new RootBeanDefinition(MyDeprecatedBean.class); + String beanName = "deprecated"; + beanFactory.registerBeanDefinition(beanName, def); + + DeprecatedBeanWarner warner = new MyDeprecatedBeanWarner(); + warner.postProcessBeanFactory(beanFactory); + assertThat(this.beanName).isEqualTo(beanName); + assertThat(this.beanDefinition).isEqualTo(def); + } + + + private class MyDeprecatedBeanWarner extends DeprecatedBeanWarner { + + @Override + protected void logDeprecatedBean(String beanName, Class beanType, BeanDefinition beanDefinition) { + DeprecatedBeanWarnerTests.this.beanName = beanName; + DeprecatedBeanWarnerTests.this.beanDefinition = beanDefinition; + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/FieldRetrievingFactoryBeanTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/FieldRetrievingFactoryBeanTests.java new file mode 100644 index 0000000..b697500 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/FieldRetrievingFactoryBeanTests.java @@ -0,0 +1,140 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.sql.Connection; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.core.testfixture.io.ResourceTestUtils.qualifiedResource; + +/** + * Unit tests for {@link FieldRetrievingFactoryBean}. + * + * @author Juergen Hoeller + * @author Chris Beams + * @since 31.07.2004 + */ +public class FieldRetrievingFactoryBeanTests { + + @Test + public void testStaticField() throws Exception { + FieldRetrievingFactoryBean fr = new FieldRetrievingFactoryBean(); + fr.setStaticField("java.sql.Connection.TRANSACTION_SERIALIZABLE"); + fr.afterPropertiesSet(); + assertThat(fr.getObject()).isEqualTo(Connection.TRANSACTION_SERIALIZABLE); + } + + @Test + public void testStaticFieldWithWhitespace() throws Exception { + FieldRetrievingFactoryBean fr = new FieldRetrievingFactoryBean(); + fr.setStaticField(" java.sql.Connection.TRANSACTION_SERIALIZABLE "); + fr.afterPropertiesSet(); + assertThat(fr.getObject()).isEqualTo(Connection.TRANSACTION_SERIALIZABLE); + } + + @Test + public void testStaticFieldViaClassAndFieldName() throws Exception { + FieldRetrievingFactoryBean fr = new FieldRetrievingFactoryBean(); + fr.setTargetClass(Connection.class); + fr.setTargetField("TRANSACTION_SERIALIZABLE"); + fr.afterPropertiesSet(); + assertThat(fr.getObject()).isEqualTo(Connection.TRANSACTION_SERIALIZABLE); + } + + @Test + public void testNonStaticField() throws Exception { + FieldRetrievingFactoryBean fr = new FieldRetrievingFactoryBean(); + PublicFieldHolder target = new PublicFieldHolder(); + fr.setTargetObject(target); + fr.setTargetField("publicField"); + fr.afterPropertiesSet(); + assertThat(fr.getObject()).isEqualTo(target.publicField); + } + + @Test + public void testNothingButBeanName() throws Exception { + FieldRetrievingFactoryBean fr = new FieldRetrievingFactoryBean(); + fr.setBeanName("java.sql.Connection.TRANSACTION_SERIALIZABLE"); + fr.afterPropertiesSet(); + assertThat(fr.getObject()).isEqualTo(Connection.TRANSACTION_SERIALIZABLE); + } + + @Test + public void testJustTargetField() throws Exception { + FieldRetrievingFactoryBean fr = new FieldRetrievingFactoryBean(); + fr.setTargetField("TRANSACTION_SERIALIZABLE"); + try { + fr.afterPropertiesSet(); + } + catch (IllegalArgumentException expected) { + } + } + + @Test + public void testJustTargetClass() throws Exception { + FieldRetrievingFactoryBean fr = new FieldRetrievingFactoryBean(); + fr.setTargetClass(Connection.class); + try { + fr.afterPropertiesSet(); + } + catch (IllegalArgumentException expected) { + } + } + + @Test + public void testJustTargetObject() throws Exception { + FieldRetrievingFactoryBean fr = new FieldRetrievingFactoryBean(); + fr.setTargetObject(new PublicFieldHolder()); + try { + fr.afterPropertiesSet(); + } + catch (IllegalArgumentException expected) { + } + } + + @Test + public void testWithConstantOnClassWithPackageLevelVisibility() throws Exception { + FieldRetrievingFactoryBean fr = new FieldRetrievingFactoryBean(); + fr.setBeanName("org.springframework.beans.testfixture.beans.PackageLevelVisibleBean.CONSTANT"); + fr.afterPropertiesSet(); + assertThat(fr.getObject()).isEqualTo("Wuby"); + } + + @Test + public void testBeanNameSyntaxWithBeanFactory() throws Exception { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions( + qualifiedResource(FieldRetrievingFactoryBeanTests.class, "context.xml")); + + TestBean testBean = (TestBean) bf.getBean("testBean"); + assertThat(testBean.getSomeIntegerArray()[0]).isEqualTo(Connection.TRANSACTION_SERIALIZABLE); + assertThat(testBean.getSomeIntegerArray()[1]).isEqualTo(Connection.TRANSACTION_SERIALIZABLE); + } + + + private static class PublicFieldHolder { + + public String publicField = "test"; + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/MethodInvokingFactoryBeanTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/MethodInvokingFactoryBeanTests.java new file mode 100644 index 0000000..fae7128 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/MethodInvokingFactoryBeanTests.java @@ -0,0 +1,340 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.propertyeditors.StringTrimmerEditor; +import org.springframework.beans.support.ArgumentConvertingMethodInvoker; +import org.springframework.util.MethodInvoker; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Unit tests for {@link MethodInvokingFactoryBean} and {@link MethodInvokingBean}. + * + * @author Colin Sampaleanu + * @author Juergen Hoeller + * @author Chris Beams + * @since 21.11.2003 + */ +public class MethodInvokingFactoryBeanTests { + + @Test + public void testParameterValidation() throws Exception { + + // assert that only static OR non static are set, but not both or none + MethodInvokingFactoryBean mcfb = new MethodInvokingFactoryBean(); + assertThatIllegalArgumentException().isThrownBy(mcfb::afterPropertiesSet); + + mcfb = new MethodInvokingFactoryBean(); + mcfb.setTargetObject(this); + mcfb.setTargetMethod("whatever"); + assertThatExceptionOfType(NoSuchMethodException.class).isThrownBy(mcfb::afterPropertiesSet); + + // bogus static method + mcfb = new MethodInvokingFactoryBean(); + mcfb.setTargetClass(TestClass1.class); + mcfb.setTargetMethod("some.bogus.Method.name"); + assertThatExceptionOfType(NoSuchMethodException.class).isThrownBy(mcfb::afterPropertiesSet); + + // bogus static method + mcfb = new MethodInvokingFactoryBean(); + mcfb.setTargetClass(TestClass1.class); + mcfb.setTargetMethod("method1"); + assertThatIllegalArgumentException().isThrownBy(mcfb::afterPropertiesSet); + + // missing method + mcfb = new MethodInvokingFactoryBean(); + mcfb.setTargetObject(this); + assertThatIllegalArgumentException().isThrownBy(mcfb::afterPropertiesSet); + + // bogus method + mcfb = new MethodInvokingFactoryBean(); + mcfb.setTargetObject(this); + mcfb.setTargetMethod("bogus"); + assertThatExceptionOfType(NoSuchMethodException.class).isThrownBy(mcfb::afterPropertiesSet); + + // static method + TestClass1._staticField1 = 0; + mcfb = new MethodInvokingFactoryBean(); + mcfb.setTargetClass(TestClass1.class); + mcfb.setTargetMethod("staticMethod1"); + mcfb.afterPropertiesSet(); + + // non-static method + TestClass1 tc1 = new TestClass1(); + mcfb = new MethodInvokingFactoryBean(); + mcfb.setTargetObject(tc1); + mcfb.setTargetMethod("method1"); + mcfb.afterPropertiesSet(); + } + + @Test + public void testGetObjectType() throws Exception { + TestClass1 tc1 = new TestClass1(); + MethodInvokingFactoryBean mcfb = new MethodInvokingFactoryBean(); + mcfb = new MethodInvokingFactoryBean(); + mcfb.setTargetObject(tc1); + mcfb.setTargetMethod("method1"); + mcfb.afterPropertiesSet(); + assertThat(int.class.equals(mcfb.getObjectType())).isTrue(); + + mcfb = new MethodInvokingFactoryBean(); + mcfb.setTargetClass(TestClass1.class); + mcfb.setTargetMethod("voidRetvalMethod"); + mcfb.afterPropertiesSet(); + Class objType = mcfb.getObjectType(); + assertThat(void.class).isSameAs(objType); + + // verify that we can call a method with args that are subtypes of the + // target method arg types + TestClass1._staticField1 = 0; + mcfb = new MethodInvokingFactoryBean(); + mcfb.setTargetClass(TestClass1.class); + mcfb.setTargetMethod("supertypes"); + mcfb.setArguments(new ArrayList<>(), new ArrayList(), "hello"); + mcfb.afterPropertiesSet(); + mcfb.getObjectType(); + + // fail on improper argument types at afterPropertiesSet + mcfb = new MethodInvokingFactoryBean(); + mcfb.registerCustomEditor(String.class, new StringTrimmerEditor(false)); + mcfb.setTargetClass(TestClass1.class); + mcfb.setTargetMethod("supertypes"); + mcfb.setArguments("1", new Object()); + assertThatExceptionOfType(NoSuchMethodException.class).isThrownBy(mcfb::afterPropertiesSet); + } + + @Test + public void testGetObject() throws Exception { + // singleton, non-static + TestClass1 tc1 = new TestClass1(); + MethodInvokingFactoryBean mcfb = new MethodInvokingFactoryBean(); + mcfb.setTargetObject(tc1); + mcfb.setTargetMethod("method1"); + mcfb.afterPropertiesSet(); + Integer i = (Integer) mcfb.getObject(); + assertThat(i.intValue()).isEqualTo(1); + i = (Integer) mcfb.getObject(); + assertThat(i.intValue()).isEqualTo(1); + + // non-singleton, non-static + tc1 = new TestClass1(); + mcfb = new MethodInvokingFactoryBean(); + mcfb.setTargetObject(tc1); + mcfb.setTargetMethod("method1"); + mcfb.setSingleton(false); + mcfb.afterPropertiesSet(); + i = (Integer) mcfb.getObject(); + assertThat(i.intValue()).isEqualTo(1); + i = (Integer) mcfb.getObject(); + assertThat(i.intValue()).isEqualTo(2); + + // singleton, static + TestClass1._staticField1 = 0; + mcfb = new MethodInvokingFactoryBean(); + mcfb.setTargetClass(TestClass1.class); + mcfb.setTargetMethod("staticMethod1"); + mcfb.afterPropertiesSet(); + i = (Integer) mcfb.getObject(); + assertThat(i.intValue()).isEqualTo(1); + i = (Integer) mcfb.getObject(); + assertThat(i.intValue()).isEqualTo(1); + + // non-singleton, static + TestClass1._staticField1 = 0; + mcfb = new MethodInvokingFactoryBean(); + mcfb.setStaticMethod("org.springframework.beans.factory.config.MethodInvokingFactoryBeanTests$TestClass1.staticMethod1"); + mcfb.setSingleton(false); + mcfb.afterPropertiesSet(); + i = (Integer) mcfb.getObject(); + assertThat(i.intValue()).isEqualTo(1); + i = (Integer) mcfb.getObject(); + assertThat(i.intValue()).isEqualTo(2); + + // void return value + mcfb = new MethodInvokingFactoryBean(); + mcfb.setTargetClass(TestClass1.class); + mcfb.setTargetMethod("voidRetvalMethod"); + mcfb.afterPropertiesSet(); + assertThat(mcfb.getObject()).isNull(); + + // now see if we can match methods with arguments that have supertype arguments + mcfb = new MethodInvokingFactoryBean(); + mcfb.setTargetClass(TestClass1.class); + mcfb.setTargetMethod("supertypes"); + mcfb.setArguments(new ArrayList<>(), new ArrayList(), "hello"); + // should pass + mcfb.afterPropertiesSet(); + } + + @Test + public void testArgumentConversion() throws Exception { + MethodInvokingFactoryBean mcfb = new MethodInvokingFactoryBean(); + mcfb.setTargetClass(TestClass1.class); + mcfb.setTargetMethod("supertypes"); + mcfb.setArguments(new ArrayList<>(), new ArrayList(), "hello", "bogus"); + assertThatExceptionOfType(NoSuchMethodException.class).as( + "Matched method with wrong number of args").isThrownBy( + mcfb::afterPropertiesSet); + + mcfb = new MethodInvokingFactoryBean(); + mcfb.setTargetClass(TestClass1.class); + mcfb.setTargetMethod("supertypes"); + mcfb.setArguments(1, new Object()); + assertThatExceptionOfType(NoSuchMethodException.class).as( + "Should have failed on getObject with mismatched argument types").isThrownBy( + mcfb::afterPropertiesSet); + + mcfb = new MethodInvokingFactoryBean(); + mcfb.setTargetClass(TestClass1.class); + mcfb.setTargetMethod("supertypes2"); + mcfb.setArguments(new ArrayList<>(), new ArrayList(), "hello", "bogus"); + mcfb.afterPropertiesSet(); + assertThat(mcfb.getObject()).isEqualTo("hello"); + + mcfb = new MethodInvokingFactoryBean(); + mcfb.setTargetClass(TestClass1.class); + mcfb.setTargetMethod("supertypes2"); + mcfb.setArguments(new ArrayList<>(), new ArrayList(), new Object()); + assertThatExceptionOfType(NoSuchMethodException.class).as( + "Matched method when shouldn't have matched").isThrownBy( + mcfb::afterPropertiesSet); + } + + @Test + public void testInvokeWithNullArgument() throws Exception { + MethodInvoker methodInvoker = new MethodInvoker(); + methodInvoker.setTargetClass(TestClass1.class); + methodInvoker.setTargetMethod("nullArgument"); + methodInvoker.setArguments(new Object[] {null}); + methodInvoker.prepare(); + methodInvoker.invoke(); + } + + @Test + public void testInvokeWithIntArgument() throws Exception { + ArgumentConvertingMethodInvoker methodInvoker = new ArgumentConvertingMethodInvoker(); + methodInvoker.setTargetClass(TestClass1.class); + methodInvoker.setTargetMethod("intArgument"); + methodInvoker.setArguments(5); + methodInvoker.prepare(); + methodInvoker.invoke(); + + methodInvoker = new ArgumentConvertingMethodInvoker(); + methodInvoker.setTargetClass(TestClass1.class); + methodInvoker.setTargetMethod("intArgument"); + methodInvoker.setArguments(5); + methodInvoker.prepare(); + methodInvoker.invoke(); + } + + @Test + public void testInvokeWithIntArguments() throws Exception { + MethodInvokingBean methodInvoker = new MethodInvokingBean(); + methodInvoker.setTargetClass(TestClass1.class); + methodInvoker.setTargetMethod("intArguments"); + methodInvoker.setArguments(new Object[] {new Integer[] {5, 10}}); + methodInvoker.afterPropertiesSet(); + + methodInvoker = new MethodInvokingBean(); + methodInvoker.setTargetClass(TestClass1.class); + methodInvoker.setTargetMethod("intArguments"); + methodInvoker.setArguments(new Object[] {new String[] {"5", "10"}}); + methodInvoker.afterPropertiesSet(); + + methodInvoker = new MethodInvokingBean(); + methodInvoker.setTargetClass(TestClass1.class); + methodInvoker.setTargetMethod("intArguments"); + methodInvoker.setArguments(new Object[] {new Integer[] {5, 10}}); + methodInvoker.afterPropertiesSet(); + + methodInvoker = new MethodInvokingBean(); + methodInvoker.setTargetClass(TestClass1.class); + methodInvoker.setTargetMethod("intArguments"); + methodInvoker.setArguments("5", "10"); + methodInvoker.afterPropertiesSet(); + + methodInvoker = new MethodInvokingBean(); + methodInvoker.setTargetClass(TestClass1.class); + methodInvoker.setTargetMethod("intArguments"); + methodInvoker.setArguments(new Object[] {new Integer[] {5, 10}}); + methodInvoker.afterPropertiesSet(); + + methodInvoker = new MethodInvokingBean(); + methodInvoker.setTargetClass(TestClass1.class); + methodInvoker.setTargetMethod("intArguments"); + methodInvoker.setArguments("5", "10"); + methodInvoker.afterPropertiesSet(); + } + + + public static class TestClass1 { + + public static int _staticField1; + + public int _field1 = 0; + + public int method1() { + return ++_field1; + } + + public static int staticMethod1() { + return ++TestClass1._staticField1; + } + + public static void voidRetvalMethod() { + } + + public static void nullArgument(Object arg) { + } + + public static void intArgument(int arg) { + } + + public static void intArguments(int[] arg) { + } + + public static String supertypes(Collection c, Integer i) { + return i.toString(); + } + + public static String supertypes(Collection c, List l, String s) { + return s; + } + + public static String supertypes2(Collection c, List l, Integer i) { + return i.toString(); + } + + public static String supertypes2(Collection c, List l, String s, Integer i) { + return s; + } + + public static String supertypes2(Collection c, List l, String s, String s2) { + return s; + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/MyDeprecatedBean.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/MyDeprecatedBean.java new file mode 100644 index 0000000..69a9db6 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/MyDeprecatedBean.java @@ -0,0 +1,22 @@ +/* + * Copyright 2002-2010 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +@Deprecated +public class MyDeprecatedBean { + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/ObjectFactoryCreatingFactoryBeanTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/ObjectFactoryCreatingFactoryBeanTests.java new file mode 100644 index 0000000..7357ba9 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/ObjectFactoryCreatingFactoryBeanTests.java @@ -0,0 +1,183 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.util.Date; + +import javax.inject.Provider; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.core.testfixture.io.SerializationTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.core.testfixture.io.ResourceTestUtils.qualifiedResource; + +/** + * @author Colin Sampaleanu + * @author Juergen Hoeller + * @author Rick Evans + * @author Chris Beams + */ +public class ObjectFactoryCreatingFactoryBeanTests { + + private DefaultListableBeanFactory beanFactory; + + + @BeforeEach + public void setup() { + this.beanFactory = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(this.beanFactory).loadBeanDefinitions( + qualifiedResource(ObjectFactoryCreatingFactoryBeanTests.class, "context.xml")); + this.beanFactory.setSerializationId("test"); + } + + @AfterEach + public void close() { + this.beanFactory.setSerializationId(null); + } + + + @Test + public void testFactoryOperation() { + FactoryTestBean testBean = beanFactory.getBean("factoryTestBean", FactoryTestBean.class); + ObjectFactory objectFactory = testBean.getObjectFactory(); + + Date date1 = (Date) objectFactory.getObject(); + Date date2 = (Date) objectFactory.getObject(); + assertThat(date1 != date2).isTrue(); + } + + @Test + public void testFactorySerialization() throws Exception { + FactoryTestBean testBean = beanFactory.getBean("factoryTestBean", FactoryTestBean.class); + ObjectFactory objectFactory = testBean.getObjectFactory(); + + objectFactory = SerializationTestUtils.serializeAndDeserialize(objectFactory); + + Date date1 = (Date) objectFactory.getObject(); + Date date2 = (Date) objectFactory.getObject(); + assertThat(date1 != date2).isTrue(); + } + + @Test + public void testProviderOperation() { + ProviderTestBean testBean = beanFactory.getBean("providerTestBean", ProviderTestBean.class); + Provider provider = testBean.getProvider(); + + Date date1 = (Date) provider.get(); + Date date2 = (Date) provider.get(); + assertThat(date1 != date2).isTrue(); + } + + @Test + public void testProviderSerialization() throws Exception { + ProviderTestBean testBean = beanFactory.getBean("providerTestBean", ProviderTestBean.class); + Provider provider = testBean.getProvider(); + + provider = SerializationTestUtils.serializeAndDeserialize(provider); + + Date date1 = (Date) provider.get(); + Date date2 = (Date) provider.get(); + assertThat(date1 != date2).isTrue(); + } + + @Test + public void testDoesNotComplainWhenTargetBeanNameRefersToSingleton() throws Exception { + final String targetBeanName = "singleton"; + final String expectedSingleton = "Alicia Keys"; + + BeanFactory beanFactory = mock(BeanFactory.class); + given(beanFactory.getBean(targetBeanName)).willReturn(expectedSingleton); + + ObjectFactoryCreatingFactoryBean factory = new ObjectFactoryCreatingFactoryBean(); + factory.setTargetBeanName(targetBeanName); + factory.setBeanFactory(beanFactory); + factory.afterPropertiesSet(); + ObjectFactory objectFactory = factory.getObject(); + Object actualSingleton = objectFactory.getObject(); + assertThat(actualSingleton).isSameAs(expectedSingleton); + } + + @Test + public void testWhenTargetBeanNameIsNull() throws Exception { + assertThatIllegalArgumentException().as( + "'targetBeanName' property not set").isThrownBy( + new ObjectFactoryCreatingFactoryBean()::afterPropertiesSet); + } + + @Test + public void testWhenTargetBeanNameIsEmptyString() throws Exception { + ObjectFactoryCreatingFactoryBean factory = new ObjectFactoryCreatingFactoryBean(); + factory.setTargetBeanName(""); + assertThatIllegalArgumentException().as( + "'targetBeanName' property set to (invalid) empty string").isThrownBy( + factory::afterPropertiesSet); + } + + @Test + public void testWhenTargetBeanNameIsWhitespacedString() throws Exception { + ObjectFactoryCreatingFactoryBean factory = new ObjectFactoryCreatingFactoryBean(); + factory.setTargetBeanName(" \t"); + assertThatIllegalArgumentException().as( + "'targetBeanName' property set to (invalid) only-whitespace string").isThrownBy( + factory::afterPropertiesSet); + } + + @Test + public void testEnsureOFBFBReportsThatItActuallyCreatesObjectFactoryInstances() { + assertThat(new ObjectFactoryCreatingFactoryBean().getObjectType()).as("Must be reporting that it creates ObjectFactory instances (as per class contract).").isEqualTo(ObjectFactory.class); + } + + + public static class FactoryTestBean { + + private ObjectFactory objectFactory; + + public ObjectFactory getObjectFactory() { + return objectFactory; + } + + public void setObjectFactory(ObjectFactory objectFactory) { + this.objectFactory = objectFactory; + } + } + + + public static class ProviderTestBean { + + private Provider provider; + + public Provider getProvider() { + return provider; + } + + public void setProvider(Provider provider) { + this.provider = provider; + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertiesFactoryBeanTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertiesFactoryBeanTests.java new file mode 100644 index 0000000..27a00cb --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertiesFactoryBeanTests.java @@ -0,0 +1,146 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.util.Properties; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.Resource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.core.testfixture.io.ResourceTestUtils.qualifiedResource; + +/** + * Unit tests for {@link PropertiesFactoryBean}. + * + * @author Juergen Hoeller + * @author Chris Beams + * @since 01.11.2003 + */ +public class PropertiesFactoryBeanTests { + + private static final Class CLASS = PropertiesFactoryBeanTests.class; + private static final Resource TEST_PROPS = qualifiedResource(CLASS, "test.properties"); + private static final Resource TEST_PROPS_XML = qualifiedResource(CLASS, "test.properties.xml"); + + @Test + public void testWithPropertiesFile() throws Exception { + PropertiesFactoryBean pfb = new PropertiesFactoryBean(); + pfb.setLocation(TEST_PROPS); + pfb.afterPropertiesSet(); + Properties props = pfb.getObject(); + assertThat(props.getProperty("tb.array[0].age")).isEqualTo("99"); + } + + @Test + public void testWithPropertiesXmlFile() throws Exception { + PropertiesFactoryBean pfb = new PropertiesFactoryBean(); + pfb.setLocation(TEST_PROPS_XML); + pfb.afterPropertiesSet(); + Properties props = pfb.getObject(); + assertThat(props.getProperty("tb.array[0].age")).isEqualTo("99"); + } + + @Test + public void testWithLocalProperties() throws Exception { + PropertiesFactoryBean pfb = new PropertiesFactoryBean(); + Properties localProps = new Properties(); + localProps.setProperty("key2", "value2"); + pfb.setProperties(localProps); + pfb.afterPropertiesSet(); + Properties props = pfb.getObject(); + assertThat(props.getProperty("key2")).isEqualTo("value2"); + } + + @Test + public void testWithPropertiesFileAndLocalProperties() throws Exception { + PropertiesFactoryBean pfb = new PropertiesFactoryBean(); + pfb.setLocation(TEST_PROPS); + Properties localProps = new Properties(); + localProps.setProperty("key2", "value2"); + localProps.setProperty("tb.array[0].age", "0"); + pfb.setProperties(localProps); + pfb.afterPropertiesSet(); + Properties props = pfb.getObject(); + assertThat(props.getProperty("tb.array[0].age")).isEqualTo("99"); + assertThat(props.getProperty("key2")).isEqualTo("value2"); + } + + @Test + public void testWithPropertiesFileAndMultipleLocalProperties() throws Exception { + PropertiesFactoryBean pfb = new PropertiesFactoryBean(); + pfb.setLocation(TEST_PROPS); + + Properties props1 = new Properties(); + props1.setProperty("key2", "value2"); + props1.setProperty("tb.array[0].age", "0"); + + Properties props2 = new Properties(); + props2.setProperty("spring", "framework"); + props2.setProperty("Don", "Mattingly"); + + Properties props3 = new Properties(); + props3.setProperty("spider", "man"); + props3.setProperty("bat", "man"); + + pfb.setPropertiesArray(new Properties[] {props1, props2, props3}); + pfb.afterPropertiesSet(); + + Properties props = pfb.getObject(); + assertThat(props.getProperty("tb.array[0].age")).isEqualTo("99"); + assertThat(props.getProperty("key2")).isEqualTo("value2"); + assertThat(props.getProperty("spring")).isEqualTo("framework"); + assertThat(props.getProperty("Don")).isEqualTo("Mattingly"); + assertThat(props.getProperty("spider")).isEqualTo("man"); + assertThat(props.getProperty("bat")).isEqualTo("man"); + } + + @Test + public void testWithPropertiesFileAndLocalPropertiesAndLocalOverride() throws Exception { + PropertiesFactoryBean pfb = new PropertiesFactoryBean(); + pfb.setLocation(TEST_PROPS); + Properties localProps = new Properties(); + localProps.setProperty("key2", "value2"); + localProps.setProperty("tb.array[0].age", "0"); + pfb.setProperties(localProps); + pfb.setLocalOverride(true); + pfb.afterPropertiesSet(); + Properties props = pfb.getObject(); + assertThat(props.getProperty("tb.array[0].age")).isEqualTo("0"); + assertThat(props.getProperty("key2")).isEqualTo("value2"); + } + + @Test + public void testWithPrototype() throws Exception { + PropertiesFactoryBean pfb = new PropertiesFactoryBean(); + pfb.setSingleton(false); + pfb.setLocation(TEST_PROPS); + Properties localProps = new Properties(); + localProps.setProperty("key2", "value2"); + pfb.setProperties(localProps); + pfb.afterPropertiesSet(); + Properties props = pfb.getObject(); + assertThat(props.getProperty("tb.array[0].age")).isEqualTo("99"); + assertThat(props.getProperty("key2")).isEqualTo("value2"); + Properties newProps = pfb.getObject(); + assertThat(props != newProps).isTrue(); + assertThat(newProps.getProperty("tb.array[0].age")).isEqualTo("99"); + assertThat(newProps.getProperty("key2")).isEqualTo("value2"); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyPathFactoryBeanTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyPathFactoryBeanTests.java new file mode 100644 index 0000000..bc0c7cc --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyPathFactoryBeanTests.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.io.Resource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.core.testfixture.io.ResourceTestUtils.qualifiedResource; + +/** + * Unit tests for {@link PropertyPathFactoryBean}. + * + * @author Juergen Hoeller + * @author Chris Beams + * @since 04.10.2004 + */ +public class PropertyPathFactoryBeanTests { + + private static final Resource CONTEXT = qualifiedResource(PropertyPathFactoryBeanTests.class, "context.xml"); + + + @Test + public void testPropertyPathFactoryBeanWithSingletonResult() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONTEXT); + assertThat(xbf.getBean("propertyPath1")).isEqualTo(12); + assertThat(xbf.getBean("propertyPath2")).isEqualTo(11); + assertThat(xbf.getBean("tb.age")).isEqualTo(10); + assertThat(xbf.getType("otb.spouse")).isEqualTo(ITestBean.class); + Object result1 = xbf.getBean("otb.spouse"); + Object result2 = xbf.getBean("otb.spouse"); + boolean condition = result1 instanceof TestBean; + assertThat(condition).isTrue(); + assertThat(result1 == result2).isTrue(); + assertThat(((TestBean) result1).getAge()).isEqualTo(99); + } + + @Test + public void testPropertyPathFactoryBeanWithPrototypeResult() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONTEXT); + assertThat(xbf.getType("tb.spouse")).isNull(); + assertThat(xbf.getType("propertyPath3")).isEqualTo(TestBean.class); + Object result1 = xbf.getBean("tb.spouse"); + Object result2 = xbf.getBean("propertyPath3"); + Object result3 = xbf.getBean("propertyPath3"); + boolean condition2 = result1 instanceof TestBean; + assertThat(condition2).isTrue(); + boolean condition1 = result2 instanceof TestBean; + assertThat(condition1).isTrue(); + boolean condition = result3 instanceof TestBean; + assertThat(condition).isTrue(); + assertThat(((TestBean) result1).getAge()).isEqualTo(11); + assertThat(((TestBean) result2).getAge()).isEqualTo(11); + assertThat(((TestBean) result3).getAge()).isEqualTo(11); + assertThat(result1 != result2).isTrue(); + assertThat(result1 != result3).isTrue(); + assertThat(result2 != result3).isTrue(); + } + + @Test + public void testPropertyPathFactoryBeanWithNullResult() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONTEXT); + assertThat(xbf.getType("tb.spouse.spouse")).isNull(); + assertThat(xbf.getBean("tb.spouse.spouse").toString()).isEqualTo("null"); + } + + @Test + public void testPropertyPathFactoryBeanAsInnerBean() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONTEXT); + TestBean spouse = (TestBean) xbf.getBean("otb.spouse"); + TestBean tbWithInner = (TestBean) xbf.getBean("tbWithInner"); + assertThat(tbWithInner.getSpouse()).isSameAs(spouse); + boolean condition = !tbWithInner.getFriends().isEmpty(); + assertThat(condition).isTrue(); + assertThat(tbWithInner.getFriends().iterator().next()).isSameAs(spouse); + } + + @Test + public void testPropertyPathFactoryBeanAsNullReference() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONTEXT); + assertThat(xbf.getBean("tbWithNullReference", TestBean.class).getSpouse()).isNull(); + } + + @Test + public void testPropertyPathFactoryBeanAsInnerNull() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONTEXT); + assertThat(xbf.getBean("tbWithInnerNull", TestBean.class).getSpouse()).isNull(); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurerTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurerTests.java new file mode 100644 index 0000000..13d8b13 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyPlaceholderConfigurerTests.java @@ -0,0 +1,250 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.util.Properties; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.beans.factory.support.BeanDefinitionBuilder.genericBeanDefinition; +import static org.springframework.beans.factory.support.BeanDefinitionBuilder.rootBeanDefinition; +import static org.springframework.beans.factory.support.BeanDefinitionReaderUtils.registerWithGeneratedName; + +/** + * Unit tests for {@link PropertyPlaceholderConfigurer}. + * + * @author Chris Beams + */ +@SuppressWarnings("deprecation") +public class PropertyPlaceholderConfigurerTests { + + private static final String P1 = "p1"; + private static final String P1_LOCAL_PROPS_VAL = "p1LocalPropsVal"; + private static final String P1_SYSTEM_PROPS_VAL = "p1SystemPropsVal"; + + private DefaultListableBeanFactory bf; + private PropertyPlaceholderConfigurer ppc; + private Properties ppcProperties; + + private AbstractBeanDefinition p1BeanDef; + + + @BeforeEach + public void setup() { + p1BeanDef = rootBeanDefinition(TestBean.class) + .addPropertyValue("name", "${" + P1 + "}") + .getBeanDefinition(); + + bf = new DefaultListableBeanFactory(); + + ppcProperties = new Properties(); + ppcProperties.setProperty(P1, P1_LOCAL_PROPS_VAL); + System.setProperty(P1, P1_SYSTEM_PROPS_VAL); + ppc = new PropertyPlaceholderConfigurer(); + ppc.setProperties(ppcProperties); + + } + + @AfterEach + public void cleanup() { + System.clearProperty(P1); + } + + + @Test + public void localPropertiesViaResource() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("testBean", + genericBeanDefinition(TestBean.class) + .addPropertyValue("name", "${my.name}") + .getBeanDefinition()); + + PropertyPlaceholderConfigurer pc = new PropertyPlaceholderConfigurer(); + Resource resource = new ClassPathResource("PropertyPlaceholderConfigurerTests.properties", this.getClass()); + pc.setLocation(resource); + pc.postProcessBeanFactory(bf); + } + + @Test + public void resolveFromSystemProperties() { + System.setProperty("otherKey", "systemValue"); + p1BeanDef = rootBeanDefinition(TestBean.class) + .addPropertyValue("name", "${" + P1 + "}") + .addPropertyValue("sex", "${otherKey}") + .getBeanDefinition(); + registerWithGeneratedName(p1BeanDef, bf); + ppc.postProcessBeanFactory(bf); + TestBean bean = bf.getBean(TestBean.class); + assertThat(bean.getName()).isEqualTo(P1_LOCAL_PROPS_VAL); + assertThat(bean.getSex()).isEqualTo("systemValue"); + System.clearProperty("otherKey"); + } + + @Test + public void resolveFromLocalProperties() { + System.clearProperty(P1); + registerWithGeneratedName(p1BeanDef, bf); + ppc.postProcessBeanFactory(bf); + TestBean bean = bf.getBean(TestBean.class); + assertThat(bean.getName()).isEqualTo(P1_LOCAL_PROPS_VAL); + } + + @Test + public void setSystemPropertiesMode_defaultIsFallback() { + registerWithGeneratedName(p1BeanDef, bf); + ppc.postProcessBeanFactory(bf); + TestBean bean = bf.getBean(TestBean.class); + assertThat(bean.getName()).isEqualTo(P1_LOCAL_PROPS_VAL); + } + + @Test + public void setSystemSystemPropertiesMode_toOverride_andResolveFromSystemProperties() { + registerWithGeneratedName(p1BeanDef, bf); + ppc.setSystemPropertiesMode(PropertyPlaceholderConfigurer.SYSTEM_PROPERTIES_MODE_OVERRIDE); + ppc.postProcessBeanFactory(bf); + TestBean bean = bf.getBean(TestBean.class); + assertThat(bean.getName()).isEqualTo(P1_SYSTEM_PROPS_VAL); + } + + @Test + public void setSystemSystemPropertiesMode_toOverride_andSetSearchSystemEnvironment_toFalse() { + registerWithGeneratedName(p1BeanDef, bf); + System.clearProperty(P1); // will now fall all the way back to system environment + ppc.setSearchSystemEnvironment(false); + ppc.setSystemPropertiesMode(PropertyPlaceholderConfigurer.SYSTEM_PROPERTIES_MODE_OVERRIDE); + ppc.postProcessBeanFactory(bf); + TestBean bean = bf.getBean(TestBean.class); + assertThat(bean.getName()).isEqualTo(P1_LOCAL_PROPS_VAL); // has to resort to local props + } + + /** + * Creates a scenario in which two PPCs are configured, each with different + * settings regarding resolving properties from the environment. + */ + @Test + public void twoPlaceholderConfigurers_withConflictingSettings() { + String P2 = "p2"; + String P2_LOCAL_PROPS_VAL = "p2LocalPropsVal"; + String P2_SYSTEM_PROPS_VAL = "p2SystemPropsVal"; + + AbstractBeanDefinition p2BeanDef = rootBeanDefinition(TestBean.class) + .addPropertyValue("name", "${" + P1 + "}") + .addPropertyValue("country", "${" + P2 + "}") + .getBeanDefinition(); + + bf.registerBeanDefinition("p1Bean", p1BeanDef); + bf.registerBeanDefinition("p2Bean", p2BeanDef); + + ppc.setIgnoreUnresolvablePlaceholders(true); + ppc.postProcessBeanFactory(bf); + + System.setProperty(P2, P2_SYSTEM_PROPS_VAL); + Properties ppc2Properties = new Properties(); + ppc2Properties.put(P2, P2_LOCAL_PROPS_VAL); + + PropertyPlaceholderConfigurer ppc2 = new PropertyPlaceholderConfigurer(); + ppc2.setSystemPropertiesMode(PropertyPlaceholderConfigurer.SYSTEM_PROPERTIES_MODE_OVERRIDE); + ppc2.setProperties(ppc2Properties); + + ppc2Properties = new Properties(); + ppc2Properties.setProperty(P2, P2_LOCAL_PROPS_VAL); + ppc2.postProcessBeanFactory(bf); + + TestBean p1Bean = bf.getBean("p1Bean", TestBean.class); + assertThat(p1Bean.getName()).isEqualTo(P1_LOCAL_PROPS_VAL); + + TestBean p2Bean = bf.getBean("p2Bean", TestBean.class); + assertThat(p2Bean.getName()).isEqualTo(P1_LOCAL_PROPS_VAL); + assertThat(p2Bean.getCountry()).isEqualTo(P2_SYSTEM_PROPS_VAL); + + System.clearProperty(P2); + } + + @Test + public void customPlaceholderPrefixAndSuffix() { + PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); + ppc.setPlaceholderPrefix("@<"); + ppc.setPlaceholderSuffix(">"); + + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("testBean", + rootBeanDefinition(TestBean.class) + .addPropertyValue("name", "@") + .addPropertyValue("sex", "${key2}") + .getBeanDefinition()); + + System.setProperty("key1", "systemKey1Value"); + System.setProperty("key2", "systemKey2Value"); + ppc.postProcessBeanFactory(bf); + System.clearProperty("key1"); + System.clearProperty("key2"); + + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("systemKey1Value"); + assertThat(bf.getBean(TestBean.class).getSex()).isEqualTo("${key2}"); + } + + @Test + public void nullValueIsPreserved() { + PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); + ppc.setNullValue("customNull"); + System.setProperty("my.name", "customNull"); + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("testBean", rootBeanDefinition(TestBean.class) + .addPropertyValue("name", "${my.name}") + .getBeanDefinition()); + ppc.postProcessBeanFactory(bf); + assertThat(bf.getBean(TestBean.class).getName()).isNull(); + System.clearProperty("my.name"); + } + + @Test + public void trimValuesIsOffByDefault() { + PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); + System.setProperty("my.name", " myValue "); + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("testBean", rootBeanDefinition(TestBean.class) + .addPropertyValue("name", "${my.name}") + .getBeanDefinition()); + ppc.postProcessBeanFactory(bf); + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo(" myValue "); + System.clearProperty("my.name"); + } + + @Test + public void trimValuesIsApplied() { + PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); + ppc.setTrimValues(true); + System.setProperty("my.name", " myValue "); + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("testBean", rootBeanDefinition(TestBean.class) + .addPropertyValue("name", "${my.name}") + .getBeanDefinition()); + ppc.postProcessBeanFactory(bf); + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("myValue"); + System.clearProperty("my.name"); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyResourceConfigurerTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyResourceConfigurerTests.java new file mode 100644 index 0000000..5dd7686 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/PropertyResourceConfigurerTests.java @@ -0,0 +1,851 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.prefs.AbstractPreferences; +import java.util.prefs.BackingStoreException; +import java.util.prefs.Preferences; +import java.util.prefs.PreferencesFactory; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.ChildBeanDefinition; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.support.ManagedMap; +import org.springframework.beans.factory.support.ManagedSet; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.IndexedTestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.io.Resource; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.beans.factory.support.BeanDefinitionBuilder.genericBeanDefinition; +import static org.springframework.core.testfixture.io.ResourceTestUtils.qualifiedResource; + +/** + * Unit tests for various {@link PropertyResourceConfigurer} implementations including: + * {@link PropertyPlaceholderConfigurer}, {@link PropertyOverrideConfigurer} and + * {@link PreferencesPlaceholderConfigurer}. + * + * @author Juergen Hoeller + * @author Chris Beams + * @author Phillip Webb + * @since 02.10.2003 + * @see PropertyPlaceholderConfigurerTests + */ +@SuppressWarnings("deprecation") +public class PropertyResourceConfigurerTests { + + static { + System.setProperty("java.util.prefs.PreferencesFactory", MockPreferencesFactory.class.getName()); + } + + private static final Class CLASS = PropertyResourceConfigurerTests.class; + private static final Resource TEST_PROPS = qualifiedResource(CLASS, "test.properties"); + private static final Resource XTEST_PROPS = qualifiedResource(CLASS, "xtest.properties"); // does not exist + private static final Resource TEST_PROPS_XML = qualifiedResource(CLASS, "test.properties.xml"); + + private final DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + + + @Test + public void testPropertyOverrideConfigurer() { + BeanDefinition def1 = BeanDefinitionBuilder.genericBeanDefinition(TestBean.class).getBeanDefinition(); + factory.registerBeanDefinition("tb1", def1); + + BeanDefinition def2 = BeanDefinitionBuilder.genericBeanDefinition(TestBean.class).getBeanDefinition(); + factory.registerBeanDefinition("tb2", def2); + + PropertyOverrideConfigurer poc1; + PropertyOverrideConfigurer poc2; + + { + poc1 = new PropertyOverrideConfigurer(); + Properties props = new Properties(); + props.setProperty("tb1.age", "99"); + props.setProperty("tb2.name", "test"); + poc1.setProperties(props); + } + + { + poc2 = new PropertyOverrideConfigurer(); + Properties props = new Properties(); + props.setProperty("tb2.age", "99"); + props.setProperty("tb2.name", "test2"); + poc2.setProperties(props); + } + + // emulate what happens when BFPPs are added to an application context: It's LIFO-based + poc2.postProcessBeanFactory(factory); + poc1.postProcessBeanFactory(factory); + + TestBean tb1 = (TestBean) factory.getBean("tb1"); + TestBean tb2 = (TestBean) factory.getBean("tb2"); + + assertThat(tb1.getAge()).isEqualTo(99); + assertThat(tb2.getAge()).isEqualTo(99); + assertThat(tb1.getName()).isEqualTo(null); + assertThat(tb2.getName()).isEqualTo("test"); + } + + @Test + public void testPropertyOverrideConfigurerWithNestedProperty() { + BeanDefinition def = BeanDefinitionBuilder.genericBeanDefinition(IndexedTestBean.class).getBeanDefinition(); + factory.registerBeanDefinition("tb", def); + + PropertyOverrideConfigurer poc; + poc = new PropertyOverrideConfigurer(); + Properties props = new Properties(); + props.setProperty("tb.array[0].age", "99"); + props.setProperty("tb.list[1].name", "test"); + poc.setProperties(props); + poc.postProcessBeanFactory(factory); + + IndexedTestBean tb = (IndexedTestBean) factory.getBean("tb"); + assertThat(tb.getArray()[0].getAge()).isEqualTo(99); + assertThat(((TestBean) tb.getList().get(1)).getName()).isEqualTo("test"); + } + + @Test + public void testPropertyOverrideConfigurerWithNestedPropertyAndDotInBeanName() { + BeanDefinition def = BeanDefinitionBuilder.genericBeanDefinition(IndexedTestBean.class).getBeanDefinition(); + factory.registerBeanDefinition("my.tb", def); + + PropertyOverrideConfigurer poc; + poc = new PropertyOverrideConfigurer(); + Properties props = new Properties(); + props.setProperty("my.tb_array[0].age", "99"); + props.setProperty("my.tb_list[1].name", "test"); + poc.setProperties(props); + poc.setBeanNameSeparator("_"); + poc.postProcessBeanFactory(factory); + + IndexedTestBean tb = (IndexedTestBean) factory.getBean("my.tb"); + assertThat(tb.getArray()[0].getAge()).isEqualTo(99); + assertThat(((TestBean) tb.getList().get(1)).getName()).isEqualTo("test"); + } + + @Test + public void testPropertyOverrideConfigurerWithNestedMapPropertyAndDotInMapKey() { + BeanDefinition def = BeanDefinitionBuilder.genericBeanDefinition(IndexedTestBean.class).getBeanDefinition(); + factory.registerBeanDefinition("tb", def); + + PropertyOverrideConfigurer poc; + poc = new PropertyOverrideConfigurer(); + Properties props = new Properties(); + props.setProperty("tb.map[key1]", "99"); + props.setProperty("tb.map[key2.ext]", "test"); + poc.setProperties(props); + poc.postProcessBeanFactory(factory); + + IndexedTestBean tb = (IndexedTestBean) factory.getBean("tb"); + assertThat(tb.getMap().get("key1")).isEqualTo("99"); + assertThat(tb.getMap().get("key2.ext")).isEqualTo("test"); + } + + @Test + public void testPropertyOverrideConfigurerWithHeldProperties() { + BeanDefinition def = BeanDefinitionBuilder.genericBeanDefinition(PropertiesHolder.class).getBeanDefinition(); + factory.registerBeanDefinition("tb", def); + + PropertyOverrideConfigurer poc; + poc = new PropertyOverrideConfigurer(); + Properties props = new Properties(); + props.setProperty("tb.heldProperties[mail.smtp.auth]", "true"); + poc.setProperties(props); + poc.postProcessBeanFactory(factory); + + PropertiesHolder tb = (PropertiesHolder) factory.getBean("tb"); + assertThat(tb.getHeldProperties().getProperty("mail.smtp.auth")).isEqualTo("true"); + } + + @Test + public void testPropertyOverrideConfigurerWithPropertiesFile() { + BeanDefinition def = BeanDefinitionBuilder.genericBeanDefinition(IndexedTestBean.class).getBeanDefinition(); + factory.registerBeanDefinition("tb", def); + + PropertyOverrideConfigurer poc = new PropertyOverrideConfigurer(); + poc.setLocation(TEST_PROPS); + poc.postProcessBeanFactory(factory); + + IndexedTestBean tb = (IndexedTestBean) factory.getBean("tb"); + assertThat(tb.getArray()[0].getAge()).isEqualTo(99); + assertThat(((TestBean) tb.getList().get(1)).getName()).isEqualTo("test"); + } + + @Test + public void testPropertyOverrideConfigurerWithInvalidPropertiesFile() { + BeanDefinition def = BeanDefinitionBuilder.genericBeanDefinition(IndexedTestBean.class).getBeanDefinition(); + factory.registerBeanDefinition("tb", def); + + PropertyOverrideConfigurer poc = new PropertyOverrideConfigurer(); + poc.setLocations(TEST_PROPS, XTEST_PROPS); + poc.setIgnoreResourceNotFound(true); + poc.postProcessBeanFactory(factory); + + IndexedTestBean tb = (IndexedTestBean) factory.getBean("tb"); + assertThat(tb.getArray()[0].getAge()).isEqualTo(99); + assertThat(((TestBean) tb.getList().get(1)).getName()).isEqualTo("test"); + } + + @Test + public void testPropertyOverrideConfigurerWithPropertiesXmlFile() { + BeanDefinition def = BeanDefinitionBuilder.genericBeanDefinition(IndexedTestBean.class).getBeanDefinition(); + factory.registerBeanDefinition("tb", def); + + PropertyOverrideConfigurer poc = new PropertyOverrideConfigurer(); + poc.setLocation(TEST_PROPS_XML); + poc.postProcessBeanFactory(factory); + + IndexedTestBean tb = (IndexedTestBean) factory.getBean("tb"); + assertThat(tb.getArray()[0].getAge()).isEqualTo(99); + assertThat(((TestBean) tb.getList().get(1)).getName()).isEqualTo("test"); + } + + @Test + public void testPropertyOverrideConfigurerWithConvertProperties() { + BeanDefinition def = BeanDefinitionBuilder.genericBeanDefinition(IndexedTestBean.class).getBeanDefinition(); + factory.registerBeanDefinition("tb", def); + + ConvertingOverrideConfigurer bfpp = new ConvertingOverrideConfigurer(); + Properties props = new Properties(); + props.setProperty("tb.array[0].name", "99"); + props.setProperty("tb.list[1].name", "test"); + bfpp.setProperties(props); + bfpp.postProcessBeanFactory(factory); + + IndexedTestBean tb = (IndexedTestBean) factory.getBean("tb"); + assertThat(tb.getArray()[0].getName()).isEqualTo("X99"); + assertThat(((TestBean) tb.getList().get(1)).getName()).isEqualTo("Xtest"); + } + + @Test + public void testPropertyOverrideConfigurerWithInvalidKey() { + factory.registerBeanDefinition("tb1", genericBeanDefinition(TestBean.class).getBeanDefinition()); + factory.registerBeanDefinition("tb2", genericBeanDefinition(TestBean.class).getBeanDefinition()); + + { + PropertyOverrideConfigurer poc = new PropertyOverrideConfigurer(); + poc.setIgnoreInvalidKeys(true); + Properties props = new Properties(); + props.setProperty("argh", "hgra"); + props.setProperty("tb2.name", "test"); + props.setProperty("tb2.nam", "test"); + props.setProperty("tb3.name", "test"); + poc.setProperties(props); + poc.postProcessBeanFactory(factory); + assertThat(factory.getBean("tb2", TestBean.class).getName()).isEqualTo("test"); + } + { + PropertyOverrideConfigurer poc = new PropertyOverrideConfigurer(); + Properties props = new Properties(); + props.setProperty("argh", "hgra"); + props.setProperty("tb2.age", "99"); + props.setProperty("tb2.name", "test2"); + poc.setProperties(props); + poc.setOrder(0); // won't actually do anything since we're not processing through an app ctx + try { + poc.postProcessBeanFactory(factory); + } + catch (BeanInitializationException ex) { + // prove that the processor chokes on the invalid key + assertThat(ex.getMessage().toLowerCase().contains("argh")).isTrue(); + } + } + } + + @Test + public void testPropertyOverrideConfigurerWithIgnoreInvalidKeys() { + factory.registerBeanDefinition("tb1", genericBeanDefinition(TestBean.class).getBeanDefinition()); + factory.registerBeanDefinition("tb2", genericBeanDefinition(TestBean.class).getBeanDefinition()); + + { + PropertyOverrideConfigurer poc = new PropertyOverrideConfigurer(); + Properties props = new Properties(); + props.setProperty("tb2.age", "99"); + props.setProperty("tb2.name", "test2"); + poc.setProperties(props); + poc.setOrder(0); // won't actually do anything since we're not processing through an app ctx + poc.postProcessBeanFactory(factory); + } + { + PropertyOverrideConfigurer poc = new PropertyOverrideConfigurer(); + poc.setIgnoreInvalidKeys(true); + Properties props = new Properties(); + props.setProperty("argh", "hgra"); + props.setProperty("tb1.age", "99"); + props.setProperty("tb2.name", "test"); + poc.setProperties(props); + poc.postProcessBeanFactory(factory); + } + + TestBean tb1 = (TestBean) factory.getBean("tb1"); + TestBean tb2 = (TestBean) factory.getBean("tb2"); + assertThat(tb1.getAge()).isEqualTo(99); + assertThat(tb2.getAge()).isEqualTo(99); + assertThat(tb1.getName()).isEqualTo(null); + assertThat(tb2.getName()).isEqualTo("test"); + } + + @Test + public void testPropertyPlaceholderConfigurer() { + doTestPropertyPlaceholderConfigurer(false); + } + + @Test + public void testPropertyPlaceholderConfigurerWithParentChildSeparation() { + doTestPropertyPlaceholderConfigurer(true); + } + + private void doTestPropertyPlaceholderConfigurer(boolean parentChildSeparation) { + Map singletonMap = Collections.singletonMap("myKey", "myValue"); + if (parentChildSeparation) { + MutablePropertyValues pvs1 = new MutablePropertyValues(); + pvs1.add("age", "${age}"); + MutablePropertyValues pvs2 = new MutablePropertyValues(); + pvs2.add("name", "name${var}${var}${"); + pvs2.add("spouse", new RuntimeBeanReference("${ref}")); + pvs2.add("someMap", singletonMap); + RootBeanDefinition parent = new RootBeanDefinition(TestBean.class); + parent.setPropertyValues(pvs1); + ChildBeanDefinition bd = new ChildBeanDefinition("${parent}", pvs2); + factory.registerBeanDefinition("parent1", parent); + factory.registerBeanDefinition("tb1", bd); + } + else { + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("age", "${age}"); + pvs.add("name", "name${var}${var}${"); + pvs.add("spouse", new RuntimeBeanReference("${ref}")); + pvs.add("someMap", singletonMap); + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.setPropertyValues(pvs); + factory.registerBeanDefinition("tb1", bd); + } + + ConstructorArgumentValues cas = new ConstructorArgumentValues(); + cas.addIndexedArgumentValue(1, "${age}"); + cas.addGenericArgumentValue("${var}name${age}"); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("stringArray", new String[] {"${os.name}", "${age}"}); + + List friends = new ManagedList<>(); + friends.add("na${age}me"); + friends.add(new RuntimeBeanReference("${ref}")); + pvs.add("friends", friends); + + Set someSet = new ManagedSet<>(); + someSet.add("na${age}me"); + someSet.add(new RuntimeBeanReference("${ref}")); + someSet.add(new TypedStringValue("${age}", Integer.class)); + pvs.add("someSet", someSet); + + Map someMap = new ManagedMap<>(); + someMap.put(new TypedStringValue("key${age}"), new TypedStringValue("${age}")); + someMap.put(new TypedStringValue("key${age}ref"), new RuntimeBeanReference("${ref}")); + someMap.put("key1", new RuntimeBeanReference("${ref}")); + someMap.put("key2", "${age}name"); + MutablePropertyValues innerPvs = new MutablePropertyValues(); + innerPvs.add("country", "${os.name}"); + RootBeanDefinition innerBd = new RootBeanDefinition(TestBean.class); + innerBd.setPropertyValues(innerPvs); + someMap.put("key3", innerBd); + MutablePropertyValues innerPvs2 = new MutablePropertyValues(innerPvs); + someMap.put("${key4}", new BeanDefinitionHolder(new ChildBeanDefinition("tb1", innerPvs2), "child")); + pvs.add("someMap", someMap); + + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class, cas, pvs); + factory.registerBeanDefinition("tb2", bd); + + PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); + Properties props = new Properties(); + props.setProperty("age", "98"); + props.setProperty("var", "${m}var"); + props.setProperty("ref", "tb2"); + props.setProperty("m", "my"); + props.setProperty("key4", "mykey4"); + props.setProperty("parent", "parent1"); + ppc.setProperties(props); + ppc.postProcessBeanFactory(factory); + + TestBean tb1 = (TestBean) factory.getBean("tb1"); + TestBean tb2 = (TestBean) factory.getBean("tb2"); + assertThat(tb1.getAge()).isEqualTo(98); + assertThat(tb2.getAge()).isEqualTo(98); + assertThat(tb1.getName()).isEqualTo("namemyvarmyvar${"); + assertThat(tb2.getName()).isEqualTo("myvarname98"); + assertThat(tb1.getSpouse()).isEqualTo(tb2); + assertThat(tb1.getSomeMap().size()).isEqualTo(1); + assertThat(tb1.getSomeMap().get("myKey")).isEqualTo("myValue"); + assertThat(tb2.getStringArray().length).isEqualTo(2); + assertThat(tb2.getStringArray()[0]).isEqualTo(System.getProperty("os.name")); + assertThat(tb2.getStringArray()[1]).isEqualTo("98"); + assertThat(tb2.getFriends().size()).isEqualTo(2); + assertThat(tb2.getFriends().iterator().next()).isEqualTo("na98me"); + assertThat(tb2.getFriends().toArray()[1]).isEqualTo(tb2); + assertThat(tb2.getSomeSet().size()).isEqualTo(3); + assertThat(tb2.getSomeSet().contains("na98me")).isTrue(); + assertThat(tb2.getSomeSet().contains(tb2)).isTrue(); + assertThat(tb2.getSomeSet().contains(98)).isTrue(); + assertThat(tb2.getSomeMap().size()).isEqualTo(6); + assertThat(tb2.getSomeMap().get("key98")).isEqualTo("98"); + assertThat(tb2.getSomeMap().get("key98ref")).isEqualTo(tb2); + assertThat(tb2.getSomeMap().get("key1")).isEqualTo(tb2); + assertThat(tb2.getSomeMap().get("key2")).isEqualTo("98name"); + TestBean inner1 = (TestBean) tb2.getSomeMap().get("key3"); + TestBean inner2 = (TestBean) tb2.getSomeMap().get("mykey4"); + assertThat(inner1.getAge()).isEqualTo(0); + assertThat(inner1.getName()).isEqualTo(null); + assertThat(inner1.getCountry()).isEqualTo(System.getProperty("os.name")); + assertThat(inner2.getAge()).isEqualTo(98); + assertThat(inner2.getName()).isEqualTo("namemyvarmyvar${"); + assertThat(inner2.getCountry()).isEqualTo(System.getProperty("os.name")); + } + + @Test + public void testPropertyPlaceholderConfigurerWithSystemPropertyFallback() { + factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) + .addPropertyValue("country", "${os.name}").getBeanDefinition()); + + PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); + ppc.postProcessBeanFactory(factory); + + TestBean tb = (TestBean) factory.getBean("tb"); + assertThat(tb.getCountry()).isEqualTo(System.getProperty("os.name")); + } + + @Test + public void testPropertyPlaceholderConfigurerWithSystemPropertyNotUsed() { + factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) + .addPropertyValue("country", "${os.name}").getBeanDefinition()); + + PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); + Properties props = new Properties(); + props.setProperty("os.name", "myos"); + ppc.setProperties(props); + ppc.postProcessBeanFactory(factory); + + TestBean tb = (TestBean) factory.getBean("tb"); + assertThat(tb.getCountry()).isEqualTo("myos"); + } + + @Test + public void testPropertyPlaceholderConfigurerWithOverridingSystemProperty() { + factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) + .addPropertyValue("country", "${os.name}").getBeanDefinition()); + + PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); + Properties props = new Properties(); + props.put("os.name", "myos"); + ppc.setProperties(props); + ppc.setSystemPropertiesMode(PropertyPlaceholderConfigurer.SYSTEM_PROPERTIES_MODE_OVERRIDE); + ppc.postProcessBeanFactory(factory); + + TestBean tb = (TestBean) factory.getBean("tb"); + assertThat(tb.getCountry()).isEqualTo(System.getProperty("os.name")); + } + + @Test + public void testPropertyPlaceholderConfigurerWithUnresolvableSystemProperty() { + factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) + .addPropertyValue("touchy", "${user.dir}").getBeanDefinition()); + PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); + ppc.setSystemPropertiesMode(PropertyPlaceholderConfigurer.SYSTEM_PROPERTIES_MODE_NEVER); + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> + ppc.postProcessBeanFactory(factory)) + .withMessageContaining("user.dir"); + } + + @Test + public void testPropertyPlaceholderConfigurerWithUnresolvablePlaceholder() { + factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) + .addPropertyValue("name", "${ref}").getBeanDefinition()); + PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> + ppc.postProcessBeanFactory(factory)) + .withMessageContaining("ref"); + } + + @Test + public void testPropertyPlaceholderConfigurerWithIgnoreUnresolvablePlaceholder() { + factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) + .addPropertyValue("name", "${ref}").getBeanDefinition()); + + PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); + ppc.setIgnoreUnresolvablePlaceholders(true); + ppc.postProcessBeanFactory(factory); + + TestBean tb = (TestBean) factory.getBean("tb"); + assertThat(tb.getName()).isEqualTo("${ref}"); + } + + @Test + public void testPropertyPlaceholderConfigurerWithEmptyStringAsNull() { + factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) + .addPropertyValue("name", "").getBeanDefinition()); + + PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); + ppc.setNullValue(""); + ppc.postProcessBeanFactory(factory); + + TestBean tb = (TestBean) factory.getBean("tb"); + assertThat(tb.getName()).isNull(); + } + + @Test + public void testPropertyPlaceholderConfigurerWithEmptyStringInPlaceholderAsNull() { + factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) + .addPropertyValue("name", "${ref}").getBeanDefinition()); + + PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); + ppc.setNullValue(""); + Properties props = new Properties(); + props.put("ref", ""); + ppc.setProperties(props); + ppc.postProcessBeanFactory(factory); + + TestBean tb = (TestBean) factory.getBean("tb"); + assertThat(tb.getName()).isNull(); + } + + @Test + public void testPropertyPlaceholderConfigurerWithNestedPlaceholderInKey() { + factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) + .addPropertyValue("name", "${my${key}key}").getBeanDefinition()); + + PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); + Properties props = new Properties(); + props.put("key", "new"); + props.put("mynewkey", "myname"); + ppc.setProperties(props); + ppc.postProcessBeanFactory(factory); + + TestBean tb = (TestBean) factory.getBean("tb"); + assertThat(tb.getName()).isEqualTo("myname"); + } + + @Test + public void testPropertyPlaceholderConfigurerWithPlaceholderInAlias() { + factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class).getBeanDefinition()); + factory.registerAlias("tb", "${alias}"); + + PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); + Properties props = new Properties(); + props.put("alias", "tb2"); + ppc.setProperties(props); + ppc.postProcessBeanFactory(factory); + + TestBean tb = (TestBean) factory.getBean("tb"); + TestBean tb2 = (TestBean) factory.getBean("tb2"); + assertThat(tb2).isSameAs(tb); + } + + @Test + public void testPropertyPlaceholderConfigurerWithSelfReferencingPlaceholderInAlias() { + factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class).getBeanDefinition()); + factory.registerAlias("tb", "${alias}"); + + PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); + Properties props = new Properties(); + props.put("alias", "tb"); + ppc.setProperties(props); + ppc.postProcessBeanFactory(factory); + + TestBean tb = (TestBean) factory.getBean("tb"); + assertThat(tb).isNotNull(); + assertThat(factory.getAliases("tb").length).isEqualTo(0); + } + + @Test + public void testPropertyPlaceholderConfigurerWithCircularReference() { + factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) + .addPropertyValue("age", "${age}") + .addPropertyValue("name", "name${var}") + .getBeanDefinition()); + + PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); + Properties props = new Properties(); + props.setProperty("age", "99"); + props.setProperty("var", "${m}"); + props.setProperty("m", "${var}"); + ppc.setProperties(props); + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> + ppc.postProcessBeanFactory(factory)); + } + + @Test + public void testPropertyPlaceholderConfigurerWithDefaultProperties() { + factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) + .addPropertyValue("touchy", "${test}").getBeanDefinition()); + + PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); + Properties props = new Properties(); + props.put("test", "mytest"); + ppc.setProperties(props); + ppc.postProcessBeanFactory(factory); + + TestBean tb = (TestBean) factory.getBean("tb"); + assertThat(tb.getTouchy()).isEqualTo("mytest"); + } + + @Test + public void testPropertyPlaceholderConfigurerWithInlineDefault() { + factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) + .addPropertyValue("touchy", "${test:mytest}").getBeanDefinition()); + + PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); + ppc.postProcessBeanFactory(factory); + + TestBean tb = (TestBean) factory.getBean("tb"); + assertThat(tb.getTouchy()).isEqualTo("mytest"); + } + + @Test + public void testPropertyPlaceholderConfigurerWithAliases() { + factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) + .addPropertyValue("touchy", "${test}").getBeanDefinition()); + + factory.registerAlias("tb", "${myAlias}"); + factory.registerAlias("${myTarget}", "alias2"); + + PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); + Properties props = new Properties(); + props.put("test", "mytest"); + props.put("myAlias", "alias"); + props.put("myTarget", "tb"); + ppc.setProperties(props); + ppc.postProcessBeanFactory(factory); + + TestBean tb = (TestBean) factory.getBean("tb"); + assertThat(tb.getTouchy()).isEqualTo("mytest"); + tb = (TestBean) factory.getBean("alias"); + assertThat(tb.getTouchy()).isEqualTo("mytest"); + tb = (TestBean) factory.getBean("alias2"); + assertThat(tb.getTouchy()).isEqualTo("mytest"); + } + + @Test + public void testPreferencesPlaceholderConfigurer() { + factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) + .addPropertyValue("name", "${myName}") + .addPropertyValue("age", "${myAge}") + .addPropertyValue("touchy", "${myTouchy}") + .getBeanDefinition()); + + PreferencesPlaceholderConfigurer ppc = new PreferencesPlaceholderConfigurer(); + Properties props = new Properties(); + props.put("myAge", "99"); + ppc.setProperties(props); + Preferences.systemRoot().put("myName", "myNameValue"); + Preferences.systemRoot().put("myTouchy", "myTouchyValue"); + Preferences.userRoot().put("myTouchy", "myOtherTouchyValue"); + ppc.afterPropertiesSet(); + ppc.postProcessBeanFactory(factory); + + TestBean tb = (TestBean) factory.getBean("tb"); + assertThat(tb.getName()).isEqualTo("myNameValue"); + assertThat(tb.getAge()).isEqualTo(99); + assertThat(tb.getTouchy()).isEqualTo("myOtherTouchyValue"); + Preferences.userRoot().remove("myTouchy"); + Preferences.systemRoot().remove("myTouchy"); + Preferences.systemRoot().remove("myName"); + } + + @Test + public void testPreferencesPlaceholderConfigurerWithCustomTreePaths() { + factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) + .addPropertyValue("name", "${myName}") + .addPropertyValue("age", "${myAge}") + .addPropertyValue("touchy", "${myTouchy}") + .getBeanDefinition()); + + PreferencesPlaceholderConfigurer ppc = new PreferencesPlaceholderConfigurer(); + Properties props = new Properties(); + props.put("myAge", "99"); + ppc.setProperties(props); + ppc.setSystemTreePath("mySystemPath"); + ppc.setUserTreePath("myUserPath"); + Preferences.systemRoot().node("mySystemPath").put("myName", "myNameValue"); + Preferences.systemRoot().node("mySystemPath").put("myTouchy", "myTouchyValue"); + Preferences.userRoot().node("myUserPath").put("myTouchy", "myOtherTouchyValue"); + ppc.afterPropertiesSet(); + ppc.postProcessBeanFactory(factory); + + TestBean tb = (TestBean) factory.getBean("tb"); + assertThat(tb.getName()).isEqualTo("myNameValue"); + assertThat(tb.getAge()).isEqualTo(99); + assertThat(tb.getTouchy()).isEqualTo("myOtherTouchyValue"); + Preferences.userRoot().node("myUserPath").remove("myTouchy"); + Preferences.systemRoot().node("mySystemPath").remove("myTouchy"); + Preferences.systemRoot().node("mySystemPath").remove("myName"); + } + + @Test + public void testPreferencesPlaceholderConfigurerWithPathInPlaceholder() { + factory.registerBeanDefinition("tb", genericBeanDefinition(TestBean.class) + .addPropertyValue("name", "${mypath/myName}") + .addPropertyValue("age", "${myAge}") + .addPropertyValue("touchy", "${myotherpath/myTouchy}") + .getBeanDefinition()); + + PreferencesPlaceholderConfigurer ppc = new PreferencesPlaceholderConfigurer(); + Properties props = new Properties(); + props.put("myAge", "99"); + ppc.setProperties(props); + ppc.setSystemTreePath("mySystemPath"); + ppc.setUserTreePath("myUserPath"); + Preferences.systemRoot().node("mySystemPath").node("mypath").put("myName", "myNameValue"); + Preferences.systemRoot().node("mySystemPath/myotherpath").put("myTouchy", "myTouchyValue"); + Preferences.userRoot().node("myUserPath/myotherpath").put("myTouchy", "myOtherTouchyValue"); + ppc.afterPropertiesSet(); + ppc.postProcessBeanFactory(factory); + + TestBean tb = (TestBean) factory.getBean("tb"); + assertThat(tb.getName()).isEqualTo("myNameValue"); + assertThat(tb.getAge()).isEqualTo(99); + assertThat(tb.getTouchy()).isEqualTo("myOtherTouchyValue"); + Preferences.userRoot().node("myUserPath/myotherpath").remove("myTouchy"); + Preferences.systemRoot().node("mySystemPath/myotherpath").remove("myTouchy"); + Preferences.systemRoot().node("mySystemPath/mypath").remove("myName"); + } + + + static class PropertiesHolder { + + private Properties props = new Properties(); + + public Properties getHeldProperties() { + return props; + } + + public void setHeldProperties(Properties props) { + this.props = props; + } + } + + + private static class ConvertingOverrideConfigurer extends PropertyOverrideConfigurer { + + @Override + protected String convertPropertyValue(String originalValue) { + return "X" + originalValue; + } + } + + + /** + * {@link PreferencesFactory} to create {@link MockPreferences}. + */ + public static class MockPreferencesFactory implements PreferencesFactory { + + private final Preferences userRoot = new MockPreferences(); + + private final Preferences systemRoot = new MockPreferences(); + + @Override + public Preferences systemRoot() { + return this.systemRoot; + } + + @Override + public Preferences userRoot() { + return this.userRoot; + } + } + + + /** + * Mock implementation of {@link Preferences} that behaves the same regardless of the + * underlying operating system and will never throw security exceptions. + */ + public static class MockPreferences extends AbstractPreferences { + + private static Map values = new HashMap<>(); + + private static Map children = new HashMap<>(); + + public MockPreferences() { + super(null, ""); + } + + protected MockPreferences(AbstractPreferences parent, String name) { + super(parent, name); + } + + @Override + protected void putSpi(String key, String value) { + values.put(key, value); + } + + @Override + protected String getSpi(String key) { + return values.get(key); + } + + @Override + protected void removeSpi(String key) { + values.remove(key); + } + + @Override + protected void removeNodeSpi() throws BackingStoreException { + } + + @Override + protected String[] keysSpi() throws BackingStoreException { + return StringUtils.toStringArray(values.keySet()); + } + + @Override + protected String[] childrenNamesSpi() throws BackingStoreException { + return StringUtils.toStringArray(children.keySet()); + } + + @Override + protected AbstractPreferences childSpi(String name) { + AbstractPreferences child = children.get(name); + if (child == null) { + child = new MockPreferences(this, name); + children.put(name, child); + } + return child; + } + + @Override + protected void syncSpi() throws BackingStoreException { + } + + @Override + protected void flushSpi() throws BackingStoreException { + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/ServiceLocatorFactoryBeanTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/ServiceLocatorFactoryBeanTests.java new file mode 100644 index 0000000..87139e3 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/ServiceLocatorFactoryBeanTests.java @@ -0,0 +1,351 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.FatalBeanException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.core.NestedCheckedException; +import org.springframework.core.NestedRuntimeException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.mock; +import static org.springframework.beans.factory.support.BeanDefinitionBuilder.genericBeanDefinition; + +/** + * Unit tests for {@link ServiceLocatorFactoryBean}. + * + * @author Colin Sampaleanu + * @author Rick Evans + * @author Chris Beams + */ +public class ServiceLocatorFactoryBeanTests { + + private DefaultListableBeanFactory bf; + + @BeforeEach + public void setUp() { + bf = new DefaultListableBeanFactory(); + } + + @Test + public void testNoArgGetter() { + bf.registerBeanDefinition("testService", genericBeanDefinition(TestService.class).getBeanDefinition()); + bf.registerBeanDefinition("factory", + genericBeanDefinition(ServiceLocatorFactoryBean.class) + .addPropertyValue("serviceLocatorInterface", TestServiceLocator.class) + .getBeanDefinition()); + + TestServiceLocator factory = (TestServiceLocator) bf.getBean("factory"); + TestService testService = factory.getTestService(); + assertThat(testService).isNotNull(); + } + + @Test + public void testErrorOnTooManyOrTooFew() throws Exception { + bf.registerBeanDefinition("testService", genericBeanDefinition(TestService.class).getBeanDefinition()); + bf.registerBeanDefinition("testServiceInstance2", genericBeanDefinition(TestService.class).getBeanDefinition()); + bf.registerBeanDefinition("factory", + genericBeanDefinition(ServiceLocatorFactoryBean.class) + .addPropertyValue("serviceLocatorInterface", TestServiceLocator.class) + .getBeanDefinition()); + bf.registerBeanDefinition("factory2", + genericBeanDefinition(ServiceLocatorFactoryBean.class) + .addPropertyValue("serviceLocatorInterface", TestServiceLocator2.class) + .getBeanDefinition()); + bf.registerBeanDefinition("factory3", + genericBeanDefinition(ServiceLocatorFactoryBean.class) + .addPropertyValue("serviceLocatorInterface", TestService2Locator.class) + .getBeanDefinition()); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).as("more than one matching type").isThrownBy(() -> + ((TestServiceLocator) bf.getBean("factory")).getTestService()); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).as("more than one matching type").isThrownBy(() -> + ((TestServiceLocator2) bf.getBean("factory2")).getTestService(null)); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).as("no matching types").isThrownBy(() -> + ((TestService2Locator) bf.getBean("factory3")).getTestService()); + } + + @Test + public void testErrorOnTooManyOrTooFewWithCustomServiceLocatorException() { + bf.registerBeanDefinition("testService", genericBeanDefinition(TestService.class).getBeanDefinition()); + bf.registerBeanDefinition("testServiceInstance2", genericBeanDefinition(TestService.class).getBeanDefinition()); + bf.registerBeanDefinition("factory", + genericBeanDefinition(ServiceLocatorFactoryBean.class) + .addPropertyValue("serviceLocatorInterface", TestServiceLocator.class) + .addPropertyValue("serviceLocatorExceptionClass", CustomServiceLocatorException1.class) + .getBeanDefinition()); + bf.registerBeanDefinition("factory2", + genericBeanDefinition(ServiceLocatorFactoryBean.class) + .addPropertyValue("serviceLocatorInterface", TestServiceLocator2.class) + .addPropertyValue("serviceLocatorExceptionClass", CustomServiceLocatorException2.class) + .getBeanDefinition()); + bf.registerBeanDefinition("factory3", + genericBeanDefinition(ServiceLocatorFactoryBean.class) + .addPropertyValue("serviceLocatorInterface", TestService2Locator.class) + .addPropertyValue("serviceLocatorExceptionClass", CustomServiceLocatorException3.class) + .getBeanDefinition()); + assertThatExceptionOfType(CustomServiceLocatorException1.class).as("more than one matching type").isThrownBy(() -> + ((TestServiceLocator) bf.getBean("factory")).getTestService()) + .withCauseInstanceOf(NoSuchBeanDefinitionException.class); + assertThatExceptionOfType(CustomServiceLocatorException2.class).as("more than one matching type").isThrownBy(() -> + ((TestServiceLocator2) bf.getBean("factory2")).getTestService(null)) + .withCauseInstanceOf(NoSuchBeanDefinitionException.class); + assertThatExceptionOfType(CustomServiceLocatorException3.class).as("no matching type").isThrownBy(() -> + ((TestService2Locator) bf.getBean("factory3")).getTestService()); + } + + @Test + public void testStringArgGetter() throws Exception { + bf.registerBeanDefinition("testService", genericBeanDefinition(TestService.class).getBeanDefinition()); + bf.registerBeanDefinition("factory", + genericBeanDefinition(ServiceLocatorFactoryBean.class) + .addPropertyValue("serviceLocatorInterface", TestServiceLocator2.class) + .getBeanDefinition()); + + // test string-arg getter with null id + TestServiceLocator2 factory = (TestServiceLocator2) bf.getBean("factory"); + + @SuppressWarnings("unused") + TestService testBean = factory.getTestService(null); + // now test with explicit id + testBean = factory.getTestService("testService"); + // now verify failure on bad id + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> + factory.getTestService("bogusTestService")); + } + + @Disabled @Test // worked when using an ApplicationContext (see commented), fails when using BeanFactory + public void testCombinedLocatorInterface() { + bf.registerBeanDefinition("testService", genericBeanDefinition(TestService.class).getBeanDefinition()); + bf.registerAlias("testService", "1"); + + bf.registerBeanDefinition("factory", + genericBeanDefinition(ServiceLocatorFactoryBean.class) + .addPropertyValue("serviceLocatorInterface", TestServiceLocator3.class) + .getBeanDefinition()); + +// StaticApplicationContext ctx = new StaticApplicationContext(); +// ctx.registerPrototype("testService", TestService.class, new MutablePropertyValues()); +// ctx.registerAlias("testService", "1"); +// MutablePropertyValues mpv = new MutablePropertyValues(); +// mpv.addPropertyValue("serviceLocatorInterface", TestServiceLocator3.class); +// ctx.registerSingleton("factory", ServiceLocatorFactoryBean.class, mpv); +// ctx.refresh(); + + TestServiceLocator3 factory = (TestServiceLocator3) bf.getBean("factory"); + TestService testBean1 = factory.getTestService(); + TestService testBean2 = factory.getTestService("testService"); + TestService testBean3 = factory.getTestService(1); + TestService testBean4 = factory.someFactoryMethod(); + assertThat(testBean2).isNotSameAs(testBean1); + assertThat(testBean3).isNotSameAs(testBean1); + assertThat(testBean4).isNotSameAs(testBean1); + assertThat(testBean3).isNotSameAs(testBean2); + assertThat(testBean4).isNotSameAs(testBean2); + assertThat(testBean4).isNotSameAs(testBean3); + + assertThat(factory.toString().contains("TestServiceLocator3")).isTrue(); + } + + @Disabled @Test // worked when using an ApplicationContext (see commented), fails when using BeanFactory + public void testServiceMappings() { + bf.registerBeanDefinition("testService1", genericBeanDefinition(TestService.class).getBeanDefinition()); + bf.registerBeanDefinition("testService2", genericBeanDefinition(ExtendedTestService.class).getBeanDefinition()); + bf.registerBeanDefinition("factory", + genericBeanDefinition(ServiceLocatorFactoryBean.class) + .addPropertyValue("serviceLocatorInterface", TestServiceLocator3.class) + .addPropertyValue("serviceMappings", "=testService1\n1=testService1\n2=testService2") + .getBeanDefinition()); + +// StaticApplicationContext ctx = new StaticApplicationContext(); +// ctx.registerPrototype("testService1", TestService.class, new MutablePropertyValues()); +// ctx.registerPrototype("testService2", ExtendedTestService.class, new MutablePropertyValues()); +// MutablePropertyValues mpv = new MutablePropertyValues(); +// mpv.addPropertyValue("serviceLocatorInterface", TestServiceLocator3.class); +// mpv.addPropertyValue("serviceMappings", "=testService1\n1=testService1\n2=testService2"); +// ctx.registerSingleton("factory", ServiceLocatorFactoryBean.class, mpv); +// ctx.refresh(); + + TestServiceLocator3 factory = (TestServiceLocator3) bf.getBean("factory"); + TestService testBean1 = factory.getTestService(); + TestService testBean2 = factory.getTestService("testService1"); + TestService testBean3 = factory.getTestService(1); + TestService testBean4 = factory.getTestService(2); + assertThat(testBean2).isNotSameAs(testBean1); + assertThat(testBean3).isNotSameAs(testBean1); + assertThat(testBean4).isNotSameAs(testBean1); + assertThat(testBean3).isNotSameAs(testBean2); + assertThat(testBean4).isNotSameAs(testBean2); + assertThat(testBean4).isNotSameAs(testBean3); + boolean condition3 = testBean1 instanceof ExtendedTestService; + assertThat(condition3).isFalse(); + boolean condition2 = testBean2 instanceof ExtendedTestService; + assertThat(condition2).isFalse(); + boolean condition1 = testBean3 instanceof ExtendedTestService; + assertThat(condition1).isFalse(); + boolean condition = testBean4 instanceof ExtendedTestService; + assertThat(condition).isTrue(); + } + + @Test + public void testNoServiceLocatorInterfaceSupplied() throws Exception { + assertThatIllegalArgumentException().isThrownBy( + new ServiceLocatorFactoryBean()::afterPropertiesSet); + } + + @Test + public void testWhenServiceLocatorInterfaceIsNotAnInterfaceType() throws Exception { + ServiceLocatorFactoryBean factory = new ServiceLocatorFactoryBean(); + factory.setServiceLocatorInterface(getClass()); + assertThatIllegalArgumentException().isThrownBy( + factory::afterPropertiesSet); + // should throw, bad (non-interface-type) serviceLocator interface supplied + } + + @Test + public void testWhenServiceLocatorExceptionClassToExceptionTypeWithOnlyNoArgCtor() throws Exception { + ServiceLocatorFactoryBean factory = new ServiceLocatorFactoryBean(); + assertThatIllegalArgumentException().isThrownBy(() -> + factory.setServiceLocatorExceptionClass(ExceptionClassWithOnlyZeroArgCtor.class)); + // should throw, bad (invalid-Exception-type) serviceLocatorException class supplied + } + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void testWhenServiceLocatorExceptionClassIsNotAnExceptionSubclass() throws Exception { + ServiceLocatorFactoryBean factory = new ServiceLocatorFactoryBean(); + assertThatIllegalArgumentException().isThrownBy(() -> + factory.setServiceLocatorExceptionClass((Class) getClass())); + // should throw, bad (non-Exception-type) serviceLocatorException class supplied + } + + @Test + public void testWhenServiceLocatorMethodCalledWithTooManyParameters() throws Exception { + ServiceLocatorFactoryBean factory = new ServiceLocatorFactoryBean(); + factory.setServiceLocatorInterface(ServiceLocatorInterfaceWithExtraNonCompliantMethod.class); + factory.afterPropertiesSet(); + ServiceLocatorInterfaceWithExtraNonCompliantMethod locator = (ServiceLocatorInterfaceWithExtraNonCompliantMethod) factory.getObject(); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + locator.getTestService("not", "allowed")); //bad method (too many args, doesn't obey class contract) + } + + @Test + public void testRequiresListableBeanFactoryAndChokesOnAnythingElse() throws Exception { + BeanFactory beanFactory = mock(BeanFactory.class); + try { + ServiceLocatorFactoryBean factory = new ServiceLocatorFactoryBean(); + factory.setBeanFactory(beanFactory); + } + catch (FatalBeanException ex) { + // expected + } + } + + + public static class TestService { + + } + + + public static class ExtendedTestService extends TestService { + + } + + + public static class TestService2 { + + } + + + public static interface TestServiceLocator { + + TestService getTestService(); + } + + + public static interface TestServiceLocator2 { + + TestService getTestService(String id) throws CustomServiceLocatorException2; + } + + + public static interface TestServiceLocator3 { + + TestService getTestService(); + + TestService getTestService(String id); + + TestService getTestService(int id); + + TestService someFactoryMethod(); + } + + + public static interface TestService2Locator { + + TestService2 getTestService() throws CustomServiceLocatorException3; + } + + + public static interface ServiceLocatorInterfaceWithExtraNonCompliantMethod { + + TestService2 getTestService(); + + TestService2 getTestService(String serviceName, String defaultNotAllowedParameter); + } + + + @SuppressWarnings("serial") + public static class CustomServiceLocatorException1 extends NestedRuntimeException { + + public CustomServiceLocatorException1(String message, Throwable cause) { + super(message, cause); + } + } + + + @SuppressWarnings("serial") + public static class CustomServiceLocatorException2 extends NestedCheckedException { + + public CustomServiceLocatorException2(Throwable cause) { + super("", cause); + } + } + + + @SuppressWarnings("serial") + public static class CustomServiceLocatorException3 extends NestedCheckedException { + + public CustomServiceLocatorException3(String message) { + super(message); + } + } + + + @SuppressWarnings("serial") + public static class ExceptionClassWithOnlyZeroArgCtor extends Exception { + + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/SimpleScopeTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/SimpleScopeTests.java new file mode 100644 index 0000000..c66e65e --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/SimpleScopeTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.core.testfixture.io.ResourceTestUtils.qualifiedResource; + +/** + * Simple test to illustrate and verify scope usage. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Chris Beams + */ +public class SimpleScopeTests { + + private DefaultListableBeanFactory beanFactory; + + + @BeforeEach + public void setup() { + beanFactory = new DefaultListableBeanFactory(); + Scope scope = new NoOpScope() { + private int index; + private List objects = new ArrayList<>(); { + objects.add(new TestBean()); + objects.add(new TestBean()); + } + @Override + public Object get(String name, ObjectFactory objectFactory) { + if (index >= objects.size()) { + index = 0; + } + return objects.get(index++); + } + }; + + beanFactory.registerScope("myScope", scope); + + String[] scopeNames = beanFactory.getRegisteredScopeNames(); + assertThat(scopeNames.length).isEqualTo(1); + assertThat(scopeNames[0]).isEqualTo("myScope"); + assertThat(beanFactory.getRegisteredScope("myScope")).isSameAs(scope); + + new XmlBeanDefinitionReader(beanFactory).loadBeanDefinitions( + qualifiedResource(SimpleScopeTests.class, "context.xml")); + } + + + @Test + public void testCanGetScopedObject() { + TestBean tb1 = (TestBean) beanFactory.getBean("usesScope"); + TestBean tb2 = (TestBean) beanFactory.getBean("usesScope"); + assertThat(tb2).isNotSameAs(tb1); + TestBean tb3 = (TestBean) beanFactory.getBean("usesScope"); + assertThat(tb1).isSameAs(tb3); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/TestTypes.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/TestTypes.java new file mode 100644 index 0000000..164718d --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/TestTypes.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import org.springframework.beans.factory.ObjectFactory; + +/** + * Shared test types for this package. + * + * @author Chris Beams + */ +final class TestTypes {} + +/** + * @author Juergen Hoeller + */ +class NoOpScope implements Scope { + + @Override + public Object get(String name, ObjectFactory objectFactory) { + throw new UnsupportedOperationException(); + } + + @Override + public Object remove(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public void registerDestructionCallback(String name, Runnable callback) { + } + + @Override + public Object resolveContextualObject(String key) { + return null; + } + + @Override + public String getConversationId() { + return null; + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlMapFactoryBeanTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlMapFactoryBeanTests.java new file mode 100644 index 0000000..b7e05ed --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlMapFactoryBeanTests.java @@ -0,0 +1,133 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.io.IOException; +import java.io.InputStream; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.yaml.snakeyaml.constructor.DuplicateKeyException; + +import org.springframework.core.io.AbstractResource; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.FileSystemResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link YamlMapFactoryBean}. + * + * @author Dave Syer + * @author Juergen Hoeller + */ +public class YamlMapFactoryBeanTests { + + private final YamlMapFactoryBean factory = new YamlMapFactoryBean(); + + + @Test + public void testSetIgnoreResourceNotFound() { + this.factory.setResolutionMethod(YamlMapFactoryBean.ResolutionMethod.OVERRIDE_AND_IGNORE); + this.factory.setResources(new FileSystemResource("non-exsitent-file.yml")); + assertThat(this.factory.getObject().size()).isEqualTo(0); + } + + @Test + public void testSetBarfOnResourceNotFound() { + assertThatIllegalStateException().isThrownBy(() -> { + this.factory.setResources(new FileSystemResource("non-exsitent-file.yml")); + this.factory.getObject().size(); + }); + } + + @Test + public void testGetObject() { + this.factory.setResources(new ByteArrayResource("foo: bar".getBytes())); + assertThat(this.factory.getObject().size()).isEqualTo(1); + } + + @SuppressWarnings("unchecked") + @Test + public void testOverrideAndRemoveDefaults() { + this.factory.setResources(new ByteArrayResource("foo:\n bar: spam".getBytes()), + new ByteArrayResource("foo:\n spam: bar".getBytes())); + + assertThat(this.factory.getObject().size()).isEqualTo(1); + assertThat(((Map) this.factory.getObject().get("foo")).size()).isEqualTo(2); + } + + @Test + public void testFirstFound() { + this.factory.setResolutionMethod(YamlProcessor.ResolutionMethod.FIRST_FOUND); + this.factory.setResources(new AbstractResource() { + @Override + public String getDescription() { + return "non-existent"; + } + @Override + public InputStream getInputStream() throws IOException { + throw new IOException("planned"); + } + }, new ByteArrayResource("foo:\n spam: bar".getBytes())); + + assertThat(this.factory.getObject().size()).isEqualTo(1); + } + + @Test + public void testMapWithPeriodsInKey() { + this.factory.setResources(new ByteArrayResource("foo:\n ? key1.key2\n : value".getBytes())); + Map map = this.factory.getObject(); + + assertThat(map.size()).isEqualTo(1); + assertThat(map.containsKey("foo")).isTrue(); + Object object = map.get("foo"); + boolean condition = object instanceof LinkedHashMap; + assertThat(condition).isTrue(); + @SuppressWarnings("unchecked") + Map sub = (Map) object; + assertThat(sub.containsKey("key1.key2")).isTrue(); + assertThat(sub.get("key1.key2")).isEqualTo("value"); + } + + @Test + public void testMapWithIntegerValue() { + this.factory.setResources(new ByteArrayResource("foo:\n ? key1.key2\n : 3".getBytes())); + Map map = this.factory.getObject(); + + assertThat(map.size()).isEqualTo(1); + assertThat(map.containsKey("foo")).isTrue(); + Object object = map.get("foo"); + boolean condition = object instanceof LinkedHashMap; + assertThat(condition).isTrue(); + @SuppressWarnings("unchecked") + Map sub = (Map) object; + assertThat(sub.size()).isEqualTo(1); + assertThat(sub.get("key1.key2")).isEqualTo(Integer.valueOf(3)); + } + + @Test + public void testDuplicateKey() { + this.factory.setResources(new ByteArrayResource("mymap:\n foo: bar\nmymap:\n bar: foo".getBytes())); + assertThatExceptionOfType(DuplicateKeyException.class).isThrownBy(() -> + this.factory.getObject().get("mymap")); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java new file mode 100644 index 0000000..60fbd27 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java @@ -0,0 +1,179 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.net.URL; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.yaml.snakeyaml.constructor.ConstructorException; +import org.yaml.snakeyaml.parser.ParserException; +import org.yaml.snakeyaml.scanner.ScannerException; + +import org.springframework.core.io.ByteArrayResource; + +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.entry; + +/** + * Tests for {@link YamlProcessor}. + * + * @author Dave Syer + * @author Juergen Hoeller + * @author Sam Brannen + */ +class YamlProcessorTests { + + private final YamlProcessor processor = new YamlProcessor() {}; + + + @Test + void arrayConvertedToIndexedBeanReference() { + setYaml("foo: bar\nbar: [1,2,3]"); + this.processor.process((properties, map) -> { + assertThat(properties.size()).isEqualTo(4); + assertThat(properties.get("foo")).isEqualTo("bar"); + assertThat(properties.getProperty("foo")).isEqualTo("bar"); + assertThat(properties.get("bar[0]")).isEqualTo(1); + assertThat(properties.getProperty("bar[0]")).isEqualTo("1"); + assertThat(properties.get("bar[1]")).isEqualTo(2); + assertThat(properties.getProperty("bar[1]")).isEqualTo("2"); + assertThat(properties.get("bar[2]")).isEqualTo(3); + assertThat(properties.getProperty("bar[2]")).isEqualTo("3"); + }); + } + + @Test + void stringResource() { + setYaml("foo # a document that is a literal"); + this.processor.process((properties, map) -> assertThat(map.get("document")).isEqualTo("foo")); + } + + @Test + void badDocumentStart() { + setYaml("foo # a document\nbar: baz"); + assertThatExceptionOfType(ParserException.class) + .isThrownBy(() -> this.processor.process((properties, map) -> {})) + .withMessageContaining("line 2, column 1"); + } + + @Test + void badResource() { + setYaml("foo: bar\ncd\nspam:\n foo: baz"); + assertThatExceptionOfType(ScannerException.class) + .isThrownBy(() -> this.processor.process((properties, map) -> {})) + .withMessageContaining("line 3, column 1"); + } + + @Test + void mapConvertedToIndexedBeanReference() { + setYaml("foo: bar\nbar:\n spam: bucket"); + this.processor.process((properties, map) -> { + assertThat(properties.get("bar.spam")).isEqualTo("bucket"); + assertThat(properties).hasSize(2); + }); + } + + @Test + void integerKeyBehaves() { + setYaml("foo: bar\n1: bar"); + this.processor.process((properties, map) -> { + assertThat(properties.get("[1]")).isEqualTo("bar"); + assertThat(properties).hasSize(2); + }); + } + + @Test + void integerDeepKeyBehaves() { + setYaml("foo:\n 1: bar"); + this.processor.process((properties, map) -> { + assertThat(properties.get("foo[1]")).isEqualTo("bar"); + assertThat(properties).hasSize(1); + }); + } + + @Test + @SuppressWarnings("unchecked") + void flattenedMapIsSameAsPropertiesButOrdered() { + setYaml("cat: dog\nfoo: bar\nbar:\n spam: bucket"); + this.processor.process((properties, map) -> { + Map flattenedMap = processor.getFlattenedMap(map); + assertThat(flattenedMap).isInstanceOf(LinkedHashMap.class); + + assertThat(properties).hasSize(3); + assertThat(flattenedMap).hasSize(3); + + assertThat(properties.get("bar.spam")).isEqualTo("bucket"); + assertThat(flattenedMap.get("bar.spam")).isEqualTo("bucket"); + + Map bar = (Map) map.get("bar"); + assertThat(bar.get("spam")).isEqualTo("bucket"); + + List keysFromProperties = properties.keySet().stream().collect(toList()); + List keysFromFlattenedMap = flattenedMap.keySet().stream().collect(toList()); + assertThat(keysFromProperties).containsExactlyInAnyOrderElementsOf(keysFromFlattenedMap); + // Keys in the Properties object are sorted. + assertThat(keysFromProperties).containsExactly("bar.spam", "cat", "foo"); + // But the flattened map retains the order from the input. + assertThat(keysFromFlattenedMap).containsExactly("cat", "foo", "bar.spam"); + }); + } + + @Test + void customTypeSupportedByDefault() throws Exception { + URL url = new URL("https://localhost:9000/"); + setYaml("value: !!java.net.URL [\"" + url + "\"]"); + + this.processor.process((properties, map) -> { + assertThat(properties).containsExactly(entry("value", url)); + assertThat(map).containsExactly(entry("value", url)); + }); + } + + @Test + void customTypesSupportedDueToExplicitConfiguration() throws Exception { + this.processor.setSupportedTypes(URL.class, String.class); + + URL url = new URL("https://localhost:9000/"); + setYaml("value: !!java.net.URL [!!java.lang.String [\"" + url + "\"]]"); + + this.processor.process((properties, map) -> { + assertThat(properties).containsExactly(entry("value", url)); + assertThat(map).containsExactly(entry("value", url)); + }); + } + + @Test + void customTypeNotSupportedDueToExplicitConfiguration() { + this.processor.setSupportedTypes(List.class); + + setYaml("value: !!java.net.URL [\"https://localhost:9000/\"]"); + + assertThatExceptionOfType(ConstructorException.class) + .isThrownBy(() -> this.processor.process((properties, map) -> {})) + .withMessageContaining("Unsupported type encountered in YAML document: java.net.URL"); + } + + private void setYaml(String yaml) { + this.processor.setResources(new ByteArrayResource(yaml.getBytes())); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlPropertiesFactoryBeanTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlPropertiesFactoryBeanTests.java new file mode 100644 index 0000000..c484cba --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlPropertiesFactoryBeanTests.java @@ -0,0 +1,238 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.config; + +import java.util.Map; +import java.util.Properties; + +import org.junit.jupiter.api.Test; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.DuplicateKeyException; +import org.yaml.snakeyaml.scanner.ScannerException; + +import org.springframework.beans.factory.config.YamlProcessor.MatchStatus; +import org.springframework.beans.factory.config.YamlProcessor.ResolutionMethod; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link YamlPropertiesFactoryBean}. + * + * @author Dave Syer + * @author Juergen Hoeller + */ +class YamlPropertiesFactoryBeanTests { + + @Test + void loadResource() { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setResources(new ByteArrayResource("foo: bar\nspam:\n foo: baz".getBytes())); + Properties properties = factory.getObject(); + assertThat(properties.getProperty("foo")).isEqualTo("bar"); + assertThat(properties.getProperty("spam.foo")).isEqualTo("baz"); + } + + @Test + void badResource() { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setResources(new ByteArrayResource("foo: bar\ncd\nspam:\n foo: baz".getBytes())); + assertThatExceptionOfType(ScannerException.class) + .isThrownBy(factory::getObject) + .withMessageContaining("line 3, column 1"); + } + + @Test + void loadResourcesWithOverride() { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setResources( + new ByteArrayResource("foo: bar\nspam:\n foo: baz".getBytes()), + new ByteArrayResource("foo:\n bar: spam".getBytes())); + Properties properties = factory.getObject(); + assertThat(properties.getProperty("foo")).isEqualTo("bar"); + assertThat(properties.getProperty("spam.foo")).isEqualTo("baz"); + assertThat(properties.getProperty("foo.bar")).isEqualTo("spam"); + } + + @Test + void loadResourcesWithInternalOverride() { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setResources(new ByteArrayResource( + "foo: bar\nspam:\n foo: baz\nfoo: bucket".getBytes())); + assertThatExceptionOfType(DuplicateKeyException.class).isThrownBy(factory::getObject); + } + + @Test + void loadResourcesWithNestedInternalOverride() { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setResources(new ByteArrayResource( + "foo:\n bar: spam\n foo: baz\nbreak: it\nfoo: bucket".getBytes())); + assertThatExceptionOfType(DuplicateKeyException.class).isThrownBy(factory::getObject); + } + + @Test + void loadResourceWithMultipleDocuments() { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setResources(new ByteArrayResource( + "foo: bar\nspam: baz\n---\nfoo: bag".getBytes())); + Properties properties = factory.getObject(); + assertThat(properties.getProperty("foo")).isEqualTo("bag"); + assertThat(properties.getProperty("spam")).isEqualTo("baz"); + } + + @Test + void loadResourceWithSelectedDocuments() { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setResources(new ByteArrayResource( + "foo: bar\nspam: baz\n---\nfoo: bag\nspam: bad".getBytes())); + factory.setDocumentMatchers(properties -> ("bag".equals(properties.getProperty("foo")) ? + MatchStatus.FOUND : MatchStatus.NOT_FOUND)); + Properties properties = factory.getObject(); + assertThat(properties.getProperty("foo")).isEqualTo("bag"); + assertThat(properties.getProperty("spam")).isEqualTo("bad"); + } + + @Test + void loadResourceWithDefaultMatch() { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setMatchDefault(true); + factory.setResources(new ByteArrayResource( + "one: two\n---\nfoo: bar\nspam: baz\n---\nfoo: bag\nspam: bad".getBytes())); + factory.setDocumentMatchers(properties -> { + if (!properties.containsKey("foo")) { + return MatchStatus.ABSTAIN; + } + return ("bag".equals(properties.getProperty("foo")) ? + MatchStatus.FOUND : MatchStatus.NOT_FOUND); + }); + Properties properties = factory.getObject(); + assertThat(properties.getProperty("foo")).isEqualTo("bag"); + assertThat(properties.getProperty("spam")).isEqualTo("bad"); + assertThat(properties.getProperty("one")).isEqualTo("two"); + } + + @Test + void loadResourceWithoutDefaultMatch() { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setMatchDefault(false); + factory.setResources(new ByteArrayResource( + "one: two\n---\nfoo: bar\nspam: baz\n---\nfoo: bag\nspam: bad".getBytes())); + factory.setDocumentMatchers(properties -> { + if (!properties.containsKey("foo")) { + return MatchStatus.ABSTAIN; + } + return ("bag".equals(properties.getProperty("foo")) ? + MatchStatus.FOUND : MatchStatus.NOT_FOUND); + }); + Properties properties = factory.getObject(); + assertThat(properties.getProperty("foo")).isEqualTo("bag"); + assertThat(properties.getProperty("spam")).isEqualTo("bad"); + assertThat(properties.getProperty("one")).isNull(); + } + + @Test + void loadResourceWithDefaultMatchSkippingMissedMatch() { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setMatchDefault(true); + factory.setResources(new ByteArrayResource( + "one: two\n---\nfoo: bag\nspam: bad\n---\nfoo: bar\nspam: baz".getBytes())); + factory.setDocumentMatchers(properties -> { + if (!properties.containsKey("foo")) { + return MatchStatus.ABSTAIN; + } + return ("bag".equals(properties.getProperty("foo")) ? + MatchStatus.FOUND : MatchStatus.NOT_FOUND); + }); + Properties properties = factory.getObject(); + assertThat(properties.getProperty("foo")).isEqualTo("bag"); + assertThat(properties.getProperty("spam")).isEqualTo("bad"); + assertThat(properties.getProperty("one")).isEqualTo("two"); + } + + @Test + void loadNonExistentResource() { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setResolutionMethod(ResolutionMethod.OVERRIDE_AND_IGNORE); + factory.setResources(new ClassPathResource("no-such-file.yml")); + Properties properties = factory.getObject(); + assertThat(properties).isEmpty(); + } + + @Test + void loadNull() { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setResources(new ByteArrayResource("foo: bar\nspam:".getBytes())); + Properties properties = factory.getObject(); + assertThat(properties.getProperty("foo")).isEqualTo("bar"); + assertThat(properties.getProperty("spam")).isEqualTo(""); + } + + @Test + void loadEmptyArrayValue() { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setResources(new ByteArrayResource("a: alpha\ntest: []".getBytes())); + Properties properties = factory.getObject(); + assertThat(properties.getProperty("a")).isEqualTo("alpha"); + assertThat(properties.getProperty("test")).isEqualTo(""); + } + + @Test + void loadArrayOfString() { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setResources(new ByteArrayResource("foo:\n- bar\n- baz".getBytes())); + Properties properties = factory.getObject(); + assertThat(properties.getProperty("foo[0]")).isEqualTo("bar"); + assertThat(properties.getProperty("foo[1]")).isEqualTo("baz"); + assertThat(properties.get("foo")).isNull(); + } + + @Test + void loadArrayOfInteger() { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setResources(new ByteArrayResource("foo:\n- 1\n- 2".getBytes())); + Properties properties = factory.getObject(); + assertThat(properties.getProperty("foo[0]")).isEqualTo("1"); + assertThat(properties.getProperty("foo[1]")).isEqualTo("2"); + assertThat(properties.get("foo")).isNull(); + } + + @Test + void loadArrayOfObject() { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setResources(new ByteArrayResource( + "foo:\n- bar:\n spam: crap\n- baz\n- one: two\n three: four".getBytes() + )); + Properties properties = factory.getObject(); + assertThat(properties.getProperty("foo[0].bar.spam")).isEqualTo("crap"); + assertThat(properties.getProperty("foo[1]")).isEqualTo("baz"); + assertThat(properties.getProperty("foo[2].one")).isEqualTo("two"); + assertThat(properties.getProperty("foo[2].three")).isEqualTo("four"); + assertThat(properties.get("foo")).isNull(); + } + + @Test + @SuppressWarnings("unchecked") + void yaml() { + Yaml yaml = new Yaml(); + Map map = yaml.loadAs("foo: bar\nspam:\n foo: baz", Map.class); + assertThat(map.get("foo")).isEqualTo("bar"); + assertThat(((Map) map.get("spam")).get("foo")).isEqualTo("baz"); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/parsing/ConstructorArgumentEntryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/parsing/ConstructorArgumentEntryTests.java new file mode 100644 index 0000000..9e3155d --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/parsing/ConstructorArgumentEntryTests.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.parsing; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Unit tests for {@link ConstructorArgumentEntry}. + * + * @author Rick Evans + * @author Chris Beams + */ +public class ConstructorArgumentEntryTests { + + @Test + public void testCtorBailsOnNegativeCtorIndexArgument() { + assertThatIllegalArgumentException().isThrownBy(() -> + new ConstructorArgumentEntry(-1)); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/parsing/CustomProblemReporterTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/parsing/CustomProblemReporterTests.java new file mode 100644 index 0000000..34abf22 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/parsing/CustomProblemReporterTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.parsing; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.core.testfixture.io.ResourceTestUtils.qualifiedResource; + +/** + * @author Rob Harrop + * @author Chris Beams + * @since 2.0 + */ +public class CustomProblemReporterTests { + + private CollatingProblemReporter problemReporter; + + private DefaultListableBeanFactory beanFactory; + + private XmlBeanDefinitionReader reader; + + + @BeforeEach + public void setup() { + this.problemReporter = new CollatingProblemReporter(); + this.beanFactory = new DefaultListableBeanFactory(); + this.reader = new XmlBeanDefinitionReader(this.beanFactory); + this.reader.setProblemReporter(this.problemReporter); + } + + + @Test + public void testErrorsAreCollated() { + this.reader.loadBeanDefinitions(qualifiedResource(CustomProblemReporterTests.class, "context.xml")); + assertThat(this.problemReporter.getErrors().length).as("Incorrect number of errors collated").isEqualTo(4); + + TestBean bean = (TestBean) this.beanFactory.getBean("validBean"); + assertThat(bean).isNotNull(); + } + + + private static class CollatingProblemReporter implements ProblemReporter { + + private final List errors = new ArrayList<>(); + + private final List warnings = new ArrayList<>(); + + @Override + public void fatal(Problem problem) { + throw new BeanDefinitionParsingException(problem); + } + + @Override + public void error(Problem problem) { + this.errors.add(problem); + } + + public Problem[] getErrors() { + return this.errors.toArray(new Problem[this.errors.size()]); + } + + @Override + public void warning(Problem problem) { + this.warnings.add(problem); + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/parsing/FailFastProblemReporterTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/parsing/FailFastProblemReporterTests.java new file mode 100644 index 0000000..fdd7fae --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/parsing/FailFastProblemReporterTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.parsing; + +import org.apache.commons.logging.Log; +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.DescriptiveResource; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author Rick Evans + * @author Juergen Hoeller + * @author Chris Beams + */ +public class FailFastProblemReporterTests { + + @Test + public void testError() throws Exception { + FailFastProblemReporter reporter = new FailFastProblemReporter(); + assertThatExceptionOfType(BeanDefinitionParsingException.class).isThrownBy(() -> + reporter.error(new Problem("VGER", new Location(new DescriptiveResource("here")), + null, new IllegalArgumentException()))); + } + + @Test + public void testWarn() throws Exception { + Problem problem = new Problem("VGER", new Location(new DescriptiveResource("here")), + null, new IllegalArgumentException()); + + Log log = mock(Log.class); + + FailFastProblemReporter reporter = new FailFastProblemReporter(); + reporter.setLogger(log); + reporter.warning(problem); + + verify(log).warn(any(), isA(IllegalArgumentException.class)); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/parsing/NullSourceExtractorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/parsing/NullSourceExtractorTests.java new file mode 100644 index 0000000..48dd459 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/parsing/NullSourceExtractorTests.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.parsing; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rick Evans + * @author Chris Beams + */ +public class NullSourceExtractorTests { + + @Test + public void testPassThroughContract() throws Exception { + Object source = new Object(); + Object extractedSource = new NullSourceExtractor().extractSource(source, null); + assertThat(extractedSource).as("The contract of NullSourceExtractor states that the extraction *always* return null").isNull(); + } + + @Test + public void testPassThroughContractEvenWithNull() throws Exception { + Object extractedSource = new NullSourceExtractor().extractSource(null, null); + assertThat(extractedSource).as("The contract of NullSourceExtractor states that the extraction *always* return null").isNull(); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/parsing/ParseStateTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/parsing/ParseStateTests.java new file mode 100644 index 0000000..ce6a3aa --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/parsing/ParseStateTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.parsing; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + * @author Chris Beams + * @since 2.0 + */ +public class ParseStateTests { + + @Test + public void testSimple() throws Exception { + MockEntry entry = new MockEntry(); + + ParseState parseState = new ParseState(); + parseState.push(entry); + assertThat(parseState.peek()).as("Incorrect peek value.").isEqualTo(entry); + parseState.pop(); + assertThat(parseState.peek()).as("Should get null on peek()").isNull(); + } + + @Test + public void testNesting() throws Exception { + MockEntry one = new MockEntry(); + MockEntry two = new MockEntry(); + MockEntry three = new MockEntry(); + + ParseState parseState = new ParseState(); + parseState.push(one); + assertThat(parseState.peek()).isEqualTo(one); + parseState.push(two); + assertThat(parseState.peek()).isEqualTo(two); + parseState.push(three); + assertThat(parseState.peek()).isEqualTo(three); + + parseState.pop(); + assertThat(parseState.peek()).isEqualTo(two); + parseState.pop(); + assertThat(parseState.peek()).isEqualTo(one); + } + + @Test + public void testSnapshot() throws Exception { + MockEntry entry = new MockEntry(); + + ParseState original = new ParseState(); + original.push(entry); + + ParseState snapshot = original.snapshot(); + original.push(new MockEntry()); + assertThat(snapshot.peek()).as("Snapshot should not have been modified.").isEqualTo(entry); + } + + + private static class MockEntry implements ParseState.Entry { + + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/parsing/PassThroughSourceExtractorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/parsing/PassThroughSourceExtractorTests.java new file mode 100644 index 0000000..2f6fe19 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/parsing/PassThroughSourceExtractorTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.parsing; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link PassThroughSourceExtractor}. + * + * @author Rick Evans + * @author Chris Beams + */ +public class PassThroughSourceExtractorTests { + + @Test + public void testPassThroughContract() throws Exception { + Object source = new Object(); + Object extractedSource = new PassThroughSourceExtractor().extractSource(source, null); + assertThat(extractedSource).as("The contract of PassThroughSourceExtractor states that the supplied " + + "source object *must* be returned as-is").isSameAs(source); + } + + @Test + public void testPassThroughContractEvenWithNull() throws Exception { + Object extractedSource = new PassThroughSourceExtractor().extractSource(null, null); + assertThat(extractedSource).as("The contract of PassThroughSourceExtractor states that the supplied " + + "source object *must* be returned as-is (even if null)").isNull(); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/parsing/PropertyEntryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/parsing/PropertyEntryTests.java new file mode 100644 index 0000000..084fb1f --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/parsing/PropertyEntryTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.parsing; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Unit tests for {@link PropertyEntry}. + * + * @author Rick Evans + * @author Chris Beams + */ +public class PropertyEntryTests { + + @Test + public void testCtorBailsOnNullPropertyNameArgument() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new PropertyEntry(null)); + } + + @Test + public void testCtorBailsOnEmptyPropertyNameArgument() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new PropertyEntry("")); + } + + @Test + public void testCtorBailsOnWhitespacedPropertyNameArgument() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new PropertyEntry("\t ")); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/serviceloader/ServiceLoaderTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/serviceloader/ServiceLoaderTests.java new file mode 100644 index 0000000..bdf544c --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/serviceloader/ServiceLoaderTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.serviceloader; + +import java.util.List; +import java.util.ServiceLoader; + +import javax.xml.parsers.DocumentBuilderFactory; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * @author Juergen Hoeller + * @author Chris Beams + */ +class ServiceLoaderTests { + + @BeforeAll + static void assumeDocumentBuilderFactoryCanBeLoaded() { + assumeTrue(ServiceLoader.load(DocumentBuilderFactory.class).iterator().hasNext()); + } + + @Test + void testServiceLoaderFactoryBean() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + RootBeanDefinition bd = new RootBeanDefinition(ServiceLoaderFactoryBean.class); + bd.getPropertyValues().add("serviceType", DocumentBuilderFactory.class.getName()); + bf.registerBeanDefinition("service", bd); + ServiceLoader serviceLoader = (ServiceLoader) bf.getBean("service"); + boolean condition = serviceLoader.iterator().next() instanceof DocumentBuilderFactory; + assertThat(condition).isTrue(); + } + + @Test + void testServiceFactoryBean() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + RootBeanDefinition bd = new RootBeanDefinition(ServiceFactoryBean.class); + bd.getPropertyValues().add("serviceType", DocumentBuilderFactory.class.getName()); + bf.registerBeanDefinition("service", bd); + boolean condition = bf.getBean("service") instanceof DocumentBuilderFactory; + assertThat(condition).isTrue(); + } + + @Test + void testServiceListFactoryBean() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + RootBeanDefinition bd = new RootBeanDefinition(ServiceListFactoryBean.class); + bd.getPropertyValues().add("serviceType", DocumentBuilderFactory.class.getName()); + bf.registerBeanDefinition("service", bd); + List serviceList = (List) bf.getBean("service"); + boolean condition = serviceList.get(0) instanceof DocumentBuilderFactory; + assertThat(condition).isTrue(); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/AutowireUtilsTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/AutowireUtilsTests.java new file mode 100644 index 0000000..e7e04d7 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/AutowireUtilsTests.java @@ -0,0 +1,180 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link AutowireUtils}. + * + * @author Juergen Hoeller + * @author Sam Brannen + * @author Loïc Ledoyen + */ +public class AutowireUtilsTests { + + @Test + public void genericMethodReturnTypes() { + Method notParameterized = ReflectionUtils.findMethod(MyTypeWithMethods.class, "notParameterized"); + Object actual = AutowireUtils.resolveReturnTypeForFactoryMethod(notParameterized, new Object[0], getClass().getClassLoader()); + assertThat(actual).isEqualTo(String.class); + + Method notParameterizedWithArguments = ReflectionUtils.findMethod(MyTypeWithMethods.class, "notParameterizedWithArguments", Integer.class, Boolean.class); + assertThat(AutowireUtils.resolveReturnTypeForFactoryMethod(notParameterizedWithArguments, new Object[]{99, true}, getClass().getClassLoader())).isEqualTo(String.class); + + Method createProxy = ReflectionUtils.findMethod(MyTypeWithMethods.class, "createProxy", Object.class); + assertThat(AutowireUtils.resolveReturnTypeForFactoryMethod(createProxy, new Object[]{"foo"}, getClass().getClassLoader())).isEqualTo(String.class); + + Method createNamedProxyWithDifferentTypes = ReflectionUtils.findMethod(MyTypeWithMethods.class, "createNamedProxy", String.class, Object.class); + assertThat(AutowireUtils.resolveReturnTypeForFactoryMethod(createNamedProxyWithDifferentTypes, new Object[]{"enigma", 99L}, getClass().getClassLoader())).isEqualTo(Long.class); + + Method createNamedProxyWithDuplicateTypes = ReflectionUtils.findMethod(MyTypeWithMethods.class, "createNamedProxy", String.class, Object.class); + assertThat(AutowireUtils.resolveReturnTypeForFactoryMethod(createNamedProxyWithDuplicateTypes, new Object[]{"enigma", "foo"}, getClass().getClassLoader())).isEqualTo(String.class); + + Method createMock = ReflectionUtils.findMethod(MyTypeWithMethods.class, "createMock", Class.class); + assertThat(AutowireUtils.resolveReturnTypeForFactoryMethod(createMock, new Object[]{Runnable.class}, getClass().getClassLoader())).isEqualTo(Runnable.class); + assertThat(AutowireUtils.resolveReturnTypeForFactoryMethod(createMock, new Object[]{Runnable.class.getName()}, getClass().getClassLoader())).isEqualTo(Runnable.class); + + Method createNamedMock = ReflectionUtils.findMethod(MyTypeWithMethods.class, "createNamedMock", String.class, Class.class); + assertThat(AutowireUtils.resolveReturnTypeForFactoryMethod(createNamedMock, new Object[]{"foo", Runnable.class}, getClass().getClassLoader())).isEqualTo(Runnable.class); + + Method createVMock = ReflectionUtils.findMethod(MyTypeWithMethods.class, "createVMock", Object.class, Class.class); + assertThat(AutowireUtils.resolveReturnTypeForFactoryMethod(createVMock, new Object[]{"foo", Runnable.class}, getClass().getClassLoader())).isEqualTo(Runnable.class); + + // Ideally we would expect String.class instead of Object.class, but + // resolveReturnTypeForFactoryMethod() does not currently support this form of + // look-up. + Method extractValueFrom = ReflectionUtils.findMethod(MyTypeWithMethods.class, "extractValueFrom", MyInterfaceType.class); + assertThat(AutowireUtils.resolveReturnTypeForFactoryMethod(extractValueFrom, new Object[]{new MySimpleInterfaceType()}, getClass().getClassLoader())).isEqualTo(Object.class); + + // Ideally we would expect Boolean.class instead of Object.class, but this + // information is not available at run-time due to type erasure. + Map map = new HashMap<>(); + map.put(0, false); + map.put(1, true); + Method extractMagicValue = ReflectionUtils.findMethod(MyTypeWithMethods.class, "extractMagicValue", Map.class); + assertThat(AutowireUtils.resolveReturnTypeForFactoryMethod(extractMagicValue, new Object[]{map}, getClass().getClassLoader())).isEqualTo(Object.class); + } + + + public interface MyInterfaceType { + } + + public class MySimpleInterfaceType implements MyInterfaceType { + } + + public static class MyTypeWithMethods { + + /** + * Simulates a factory method that wraps the supplied object in a proxy of the + * same type. + */ + public static T createProxy(T object) { + return null; + } + + /** + * Similar to {@link #createProxy(Object)} but adds an additional argument before + * the argument of type {@code T}. Note that they may potentially be of the same + * time when invoked! + */ + public static T createNamedProxy(String name, T object) { + return null; + } + + /** + * Simulates factory methods found in libraries such as Mockito and EasyMock. + */ + public static MOCK createMock(Class toMock) { + return null; + } + + /** + * Similar to {@link #createMock(Class)} but adds an additional method argument + * before the parameterized argument. + */ + public static T createNamedMock(String name, Class toMock) { + return null; + } + + /** + * Similar to {@link #createNamedMock(String, Class)} but adds an additional + * parameterized type. + */ + public static T createVMock(V name, Class toMock) { + return null; + } + + /** + * Extract some value of the type supported by the interface (i.e., by a concrete, + * non-generic implementation of the interface). + */ + public static T extractValueFrom(MyInterfaceType myInterfaceType) { + return null; + } + + /** + * Extract some magic value from the supplied map. + */ + public static V extractMagicValue(Map map) { + return null; + } + + public MyInterfaceType integer() { + return null; + } + + public MySimpleInterfaceType string() { + return null; + } + + public Object object() { + return null; + } + + @SuppressWarnings("rawtypes") + public MyInterfaceType raw() { + return null; + } + + public String notParameterized() { + return null; + } + + public String notParameterizedWithArguments(Integer x, Boolean b) { + return null; + } + + public void readIntegerInputMessage(MyInterfaceType message) { + } + + public void readIntegerArrayInputMessage(MyInterfaceType[] message) { + } + + public void readGenericArrayInputMessage(T[] message) { + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanDefinitionBuilderTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanDefinitionBuilderTests.java new file mode 100644 index 0000000..ec157df --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanDefinitionBuilderTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rod Johnson + * @author Juergen Hoeller + */ +public class BeanDefinitionBuilderTests { + + @Test + public void beanClassWithSimpleProperty() { + String[] dependsOn = new String[] { "A", "B", "C" }; + BeanDefinitionBuilder bdb = BeanDefinitionBuilder.rootBeanDefinition(TestBean.class); + bdb.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bdb.addPropertyValue("age", "15"); + for (String dependsOnEntry : dependsOn) { + bdb.addDependsOn(dependsOnEntry); + } + + RootBeanDefinition rbd = (RootBeanDefinition) bdb.getBeanDefinition(); + assertThat(rbd.isSingleton()).isFalse(); + assertThat(rbd.getBeanClass()).isEqualTo(TestBean.class); + assertThat(Arrays.equals(dependsOn, rbd.getDependsOn())).as("Depends on was added").isTrue(); + assertThat(rbd.getPropertyValues().contains("age")).isTrue(); + } + + @Test + public void beanClassWithFactoryMethod() { + BeanDefinitionBuilder bdb = BeanDefinitionBuilder.rootBeanDefinition(TestBean.class, "create"); + RootBeanDefinition rbd = (RootBeanDefinition) bdb.getBeanDefinition(); + assertThat(rbd.hasBeanClass()).isTrue(); + assertThat(rbd.getBeanClass()).isEqualTo(TestBean.class); + assertThat(rbd.getFactoryMethodName()).isEqualTo("create"); + } + + @Test + public void beanClassName() { + BeanDefinitionBuilder bdb = BeanDefinitionBuilder.rootBeanDefinition(TestBean.class.getName()); + RootBeanDefinition rbd = (RootBeanDefinition) bdb.getBeanDefinition(); + assertThat(rbd.hasBeanClass()).isFalse(); + assertThat(rbd.getBeanClassName()).isEqualTo(TestBean.class.getName()); + } + + @Test + public void beanClassNameWithFactoryMethod() { + BeanDefinitionBuilder bdb = BeanDefinitionBuilder.rootBeanDefinition(TestBean.class.getName(), "create"); + RootBeanDefinition rbd = (RootBeanDefinition) bdb.getBeanDefinition(); + assertThat(rbd.hasBeanClass()).isFalse(); + assertThat(rbd.getBeanClassName()).isEqualTo(TestBean.class.getName()); + assertThat(rbd.getFactoryMethodName()).isEqualTo("create"); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanDefinitionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanDefinitionTests.java new file mode 100644 index 0000000..88dc51e --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanDefinitionTests.java @@ -0,0 +1,182 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + */ +public class BeanDefinitionTests { + + @Test + public void beanDefinitionEquality() { + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.setAbstract(true); + bd.setLazyInit(true); + bd.setScope("request"); + RootBeanDefinition otherBd = new RootBeanDefinition(TestBean.class); + boolean condition1 = !bd.equals(otherBd); + assertThat(condition1).isTrue(); + boolean condition = !otherBd.equals(bd); + assertThat(condition).isTrue(); + otherBd.setAbstract(true); + otherBd.setLazyInit(true); + otherBd.setScope("request"); + assertThat(bd.equals(otherBd)).isTrue(); + assertThat(otherBd.equals(bd)).isTrue(); + assertThat(bd.hashCode() == otherBd.hashCode()).isTrue(); + } + + @Test + public void beanDefinitionEqualityWithPropertyValues() { + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.getPropertyValues().add("name", "myName"); + bd.getPropertyValues().add("age", "99"); + RootBeanDefinition otherBd = new RootBeanDefinition(TestBean.class); + otherBd.getPropertyValues().add("name", "myName"); + boolean condition3 = !bd.equals(otherBd); + assertThat(condition3).isTrue(); + boolean condition2 = !otherBd.equals(bd); + assertThat(condition2).isTrue(); + otherBd.getPropertyValues().add("age", "11"); + boolean condition1 = !bd.equals(otherBd); + assertThat(condition1).isTrue(); + boolean condition = !otherBd.equals(bd); + assertThat(condition).isTrue(); + otherBd.getPropertyValues().add("age", "99"); + assertThat(bd.equals(otherBd)).isTrue(); + assertThat(otherBd.equals(bd)).isTrue(); + assertThat(bd.hashCode() == otherBd.hashCode()).isTrue(); + } + + @Test + public void beanDefinitionEqualityWithConstructorArguments() { + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.getConstructorArgumentValues().addGenericArgumentValue("test"); + bd.getConstructorArgumentValues().addIndexedArgumentValue(1, 5); + RootBeanDefinition otherBd = new RootBeanDefinition(TestBean.class); + otherBd.getConstructorArgumentValues().addGenericArgumentValue("test"); + boolean condition3 = !bd.equals(otherBd); + assertThat(condition3).isTrue(); + boolean condition2 = !otherBd.equals(bd); + assertThat(condition2).isTrue(); + otherBd.getConstructorArgumentValues().addIndexedArgumentValue(1, 9); + boolean condition1 = !bd.equals(otherBd); + assertThat(condition1).isTrue(); + boolean condition = !otherBd.equals(bd); + assertThat(condition).isTrue(); + otherBd.getConstructorArgumentValues().addIndexedArgumentValue(1, 5); + assertThat(bd.equals(otherBd)).isTrue(); + assertThat(otherBd.equals(bd)).isTrue(); + assertThat(bd.hashCode() == otherBd.hashCode()).isTrue(); + } + + @Test + public void beanDefinitionEqualityWithTypedConstructorArguments() { + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.getConstructorArgumentValues().addGenericArgumentValue("test", "int"); + bd.getConstructorArgumentValues().addIndexedArgumentValue(1, 5, "long"); + RootBeanDefinition otherBd = new RootBeanDefinition(TestBean.class); + otherBd.getConstructorArgumentValues().addGenericArgumentValue("test", "int"); + otherBd.getConstructorArgumentValues().addIndexedArgumentValue(1, 5); + boolean condition3 = !bd.equals(otherBd); + assertThat(condition3).isTrue(); + boolean condition2 = !otherBd.equals(bd); + assertThat(condition2).isTrue(); + otherBd.getConstructorArgumentValues().addIndexedArgumentValue(1, 5, "int"); + boolean condition1 = !bd.equals(otherBd); + assertThat(condition1).isTrue(); + boolean condition = !otherBd.equals(bd); + assertThat(condition).isTrue(); + otherBd.getConstructorArgumentValues().addIndexedArgumentValue(1, 5, "long"); + assertThat(bd.equals(otherBd)).isTrue(); + assertThat(otherBd.equals(bd)).isTrue(); + assertThat(bd.hashCode() == otherBd.hashCode()).isTrue(); + } + + @Test + public void genericBeanDefinitionEquality() { + GenericBeanDefinition bd = new GenericBeanDefinition(); + bd.setParentName("parent"); + bd.setScope("request"); + bd.setAbstract(true); + bd.setLazyInit(true); + GenericBeanDefinition otherBd = new GenericBeanDefinition(); + otherBd.setScope("request"); + otherBd.setAbstract(true); + otherBd.setLazyInit(true); + boolean condition1 = !bd.equals(otherBd); + assertThat(condition1).isTrue(); + boolean condition = !otherBd.equals(bd); + assertThat(condition).isTrue(); + otherBd.setParentName("parent"); + assertThat(bd.equals(otherBd)).isTrue(); + assertThat(otherBd.equals(bd)).isTrue(); + assertThat(bd.hashCode() == otherBd.hashCode()).isTrue(); + } + + @Test + public void beanDefinitionHolderEquality() { + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.setAbstract(true); + bd.setLazyInit(true); + bd.setScope("request"); + BeanDefinitionHolder holder = new BeanDefinitionHolder(bd, "bd"); + RootBeanDefinition otherBd = new RootBeanDefinition(TestBean.class); + boolean condition1 = !bd.equals(otherBd); + assertThat(condition1).isTrue(); + boolean condition = !otherBd.equals(bd); + assertThat(condition).isTrue(); + otherBd.setAbstract(true); + otherBd.setLazyInit(true); + otherBd.setScope("request"); + BeanDefinitionHolder otherHolder = new BeanDefinitionHolder(bd, "bd"); + assertThat(holder.equals(otherHolder)).isTrue(); + assertThat(otherHolder.equals(holder)).isTrue(); + assertThat(holder.hashCode() == otherHolder.hashCode()).isTrue(); + } + + @Test + public void beanDefinitionMerging() { + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.getConstructorArgumentValues().addGenericArgumentValue("test"); + bd.getConstructorArgumentValues().addIndexedArgumentValue(1, 5); + bd.getPropertyValues().add("name", "myName"); + bd.getPropertyValues().add("age", "99"); + bd.setQualifiedElement(getClass()); + + GenericBeanDefinition childBd = new GenericBeanDefinition(); + childBd.setParentName("bd"); + + RootBeanDefinition mergedBd = new RootBeanDefinition(bd); + mergedBd.overrideFrom(childBd); + assertThat(mergedBd.getConstructorArgumentValues().getArgumentCount()).isEqualTo(2); + assertThat(mergedBd.getPropertyValues().size()).isEqualTo(2); + assertThat(mergedBd).isEqualTo(bd); + + mergedBd.getConstructorArgumentValues().getArgumentValue(1, null).setValue(9); + assertThat(bd.getConstructorArgumentValues().getArgumentValue(1, null).getValue()).isEqualTo(5); + assertThat(bd.getQualifiedElement()).isEqualTo(getClass()); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactoryGenericsTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactoryGenericsTests.java new file mode 100644 index 0000000..e3ba7b5 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactoryGenericsTests.java @@ -0,0 +1,1069 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.util.AbstractCollection; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.beans.PropertyEditorRegistrar; +import org.springframework.beans.PropertyEditorRegistry; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.TypedStringValue; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.beans.propertyeditors.CustomNumberEditor; +import org.springframework.beans.testfixture.beans.GenericBean; +import org.springframework.beans.testfixture.beans.GenericIntegerBean; +import org.springframework.beans.testfixture.beans.GenericSetOfIntegerBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.OverridingClassLoader; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.annotation.Order; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.UrlResource; +import org.springframework.core.testfixture.EnabledForTestGroups; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; + +/** + * @author Juergen Hoeller + * @author Chris Beams + * @author Sam Brannen + * @since 20.01.2006 + */ +public class BeanFactoryGenericsTests { + + @Test + public void testGenericSetProperty() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); + + Set input = new HashSet<>(); + input.add("4"); + input.add("5"); + rbd.getPropertyValues().add("integerSet", input); + + bf.registerBeanDefinition("genericBean", rbd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + + assertThat(gb.getIntegerSet().contains(4)).isTrue(); + assertThat(gb.getIntegerSet().contains(5)).isTrue(); + } + + @Test + public void testGenericListProperty() throws Exception { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); + + List input = new ArrayList<>(); + input.add("http://localhost:8080"); + input.add("http://localhost:9090"); + rbd.getPropertyValues().add("resourceList", input); + + bf.registerBeanDefinition("genericBean", rbd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + + assertThat(gb.getResourceList().get(0)).isEqualTo(new UrlResource("http://localhost:8080")); + assertThat(gb.getResourceList().get(1)).isEqualTo(new UrlResource("http://localhost:9090")); + } + + @Test + public void testGenericListPropertyWithAutowiring() throws Exception { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerSingleton("resource1", new UrlResource("http://localhost:8080")); + bf.registerSingleton("resource2", new UrlResource("http://localhost:9090")); + + RootBeanDefinition rbd = new RootBeanDefinition(GenericIntegerBean.class); + rbd.setAutowireMode(RootBeanDefinition.AUTOWIRE_BY_TYPE); + bf.registerBeanDefinition("genericBean", rbd); + GenericIntegerBean gb = (GenericIntegerBean) bf.getBean("genericBean"); + + assertThat(gb.getResourceList().get(0)).isEqualTo(new UrlResource("http://localhost:8080")); + assertThat(gb.getResourceList().get(1)).isEqualTo(new UrlResource("http://localhost:9090")); + } + + @Test + public void testGenericListPropertyWithInvalidElementType() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + RootBeanDefinition rbd = new RootBeanDefinition(GenericIntegerBean.class); + + List input = new ArrayList<>(); + input.add(1); + rbd.getPropertyValues().add("testBeanList", input); + + bf.registerBeanDefinition("genericBean", rbd); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + bf.getBean("genericBean")) + .withMessageContaining("genericBean") + .withMessageContaining("testBeanList[0]") + .withMessageContaining(TestBean.class.getName()) + .withMessageContaining("Integer"); + } + + @Test + public void testGenericListPropertyWithOptionalAutowiring() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + + RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); + rbd.setAutowireMode(RootBeanDefinition.AUTOWIRE_BY_TYPE); + bf.registerBeanDefinition("genericBean", rbd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + + assertThat(gb.getResourceList()).isNull(); + } + + @Test + public void testGenericMapProperty() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); + + Map input = new HashMap<>(); + input.put("4", "5"); + input.put("6", "7"); + rbd.getPropertyValues().add("shortMap", input); + + bf.registerBeanDefinition("genericBean", rbd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + + assertThat(gb.getShortMap().get(new Short("4"))).isEqualTo(5); + assertThat(gb.getShortMap().get(new Short("6"))).isEqualTo(7); + } + + @Test + public void testGenericListOfArraysProperty() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions( + new ClassPathResource("genericBeanTests.xml", getClass())); + GenericBean gb = (GenericBean) bf.getBean("listOfArrays"); + + assertThat(gb.getListOfArrays().size()).isEqualTo(1); + String[] array = gb.getListOfArrays().get(0); + assertThat(array.length).isEqualTo(2); + assertThat(array[0]).isEqualTo("value1"); + assertThat(array[1]).isEqualTo("value2"); + } + + + @Test + public void testGenericSetConstructor() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); + + Set input = new HashSet<>(); + input.add("4"); + input.add("5"); + rbd.getConstructorArgumentValues().addGenericArgumentValue(input); + + bf.registerBeanDefinition("genericBean", rbd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + + assertThat(gb.getIntegerSet().contains(4)).isTrue(); + assertThat(gb.getIntegerSet().contains(5)).isTrue(); + } + + @Test + public void testGenericSetConstructorWithAutowiring() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerSingleton("integer1", 4); + bf.registerSingleton("integer2", 5); + + RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); + rbd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); + bf.registerBeanDefinition("genericBean", rbd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + + assertThat(gb.getIntegerSet().contains(4)).isTrue(); + assertThat(gb.getIntegerSet().contains(5)).isTrue(); + } + + @Test + public void testGenericSetConstructorWithOptionalAutowiring() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + + RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); + rbd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); + bf.registerBeanDefinition("genericBean", rbd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + + assertThat(gb.getIntegerSet()).isNull(); + } + + @Test + public void testGenericSetListConstructor() throws Exception { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); + + Set input = new HashSet<>(); + input.add("4"); + input.add("5"); + List input2 = new ArrayList<>(); + input2.add("http://localhost:8080"); + input2.add("http://localhost:9090"); + rbd.getConstructorArgumentValues().addGenericArgumentValue(input); + rbd.getConstructorArgumentValues().addGenericArgumentValue(input2); + + bf.registerBeanDefinition("genericBean", rbd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + + assertThat(gb.getIntegerSet().contains(4)).isTrue(); + assertThat(gb.getIntegerSet().contains(5)).isTrue(); + assertThat(gb.getResourceList().get(0)).isEqualTo(new UrlResource("http://localhost:8080")); + assertThat(gb.getResourceList().get(1)).isEqualTo(new UrlResource("http://localhost:9090")); + } + + @Test + public void testGenericSetListConstructorWithAutowiring() throws Exception { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerSingleton("integer1", 4); + bf.registerSingleton("integer2", 5); + bf.registerSingleton("resource1", new UrlResource("http://localhost:8080")); + bf.registerSingleton("resource2", new UrlResource("http://localhost:9090")); + + RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); + rbd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); + bf.registerBeanDefinition("genericBean", rbd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + + assertThat(gb.getIntegerSet().contains(4)).isTrue(); + assertThat(gb.getIntegerSet().contains(5)).isTrue(); + assertThat(gb.getResourceList().get(0)).isEqualTo(new UrlResource("http://localhost:8080")); + assertThat(gb.getResourceList().get(1)).isEqualTo(new UrlResource("http://localhost:9090")); + } + + @Test + public void testGenericSetListConstructorWithOptionalAutowiring() throws Exception { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerSingleton("resource1", new UrlResource("http://localhost:8080")); + bf.registerSingleton("resource2", new UrlResource("http://localhost:9090")); + + RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); + rbd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); + bf.registerBeanDefinition("genericBean", rbd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + + assertThat(gb.getIntegerSet()).isNull(); + assertThat(gb.getResourceList()).isNull(); + } + + @Test + public void testGenericSetMapConstructor() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); + + Set input = new HashSet<>(); + input.add("4"); + input.add("5"); + Map input2 = new HashMap<>(); + input2.put("4", "5"); + input2.put("6", "7"); + rbd.getConstructorArgumentValues().addGenericArgumentValue(input); + rbd.getConstructorArgumentValues().addGenericArgumentValue(input2); + + bf.registerBeanDefinition("genericBean", rbd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + + assertThat(gb.getIntegerSet().contains(4)).isTrue(); + assertThat(gb.getIntegerSet().contains(5)).isTrue(); + assertThat(gb.getShortMap().get(new Short("4"))).isEqualTo(5); + assertThat(gb.getShortMap().get(new Short("6"))).isEqualTo(7); + } + + @Test + public void testGenericMapResourceConstructor() throws Exception { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); + + Map input = new HashMap<>(); + input.put("4", "5"); + input.put("6", "7"); + rbd.getConstructorArgumentValues().addGenericArgumentValue(input); + rbd.getConstructorArgumentValues().addGenericArgumentValue("http://localhost:8080"); + + bf.registerBeanDefinition("genericBean", rbd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + + assertThat(gb.getShortMap().get(new Short("4"))).isEqualTo(5); + assertThat(gb.getShortMap().get(new Short("6"))).isEqualTo(7); + assertThat(gb.getResourceList().get(0)).isEqualTo(new UrlResource("http://localhost:8080")); + } + + @Test + public void testGenericMapMapConstructor() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); + + Map input = new HashMap<>(); + input.put("1", "0"); + input.put("2", "3"); + Map input2 = new HashMap<>(); + input2.put("4", "5"); + input2.put("6", "7"); + rbd.getConstructorArgumentValues().addGenericArgumentValue(input); + rbd.getConstructorArgumentValues().addGenericArgumentValue(input2); + + bf.registerBeanDefinition("genericBean", rbd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + + assertThat(gb.getShortMap()).isNotSameAs(gb.getPlainMap()); + assertThat(gb.getPlainMap().size()).isEqualTo(2); + assertThat(gb.getPlainMap().get("1")).isEqualTo("0"); + assertThat(gb.getPlainMap().get("2")).isEqualTo("3"); + assertThat(gb.getShortMap().size()).isEqualTo(2); + assertThat(gb.getShortMap().get(new Short("4"))).isEqualTo(5); + assertThat(gb.getShortMap().get(new Short("6"))).isEqualTo(7); + } + + @Test + public void testGenericMapMapConstructorWithSameRefAndConversion() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); + + Map input = new HashMap<>(); + input.put("1", "0"); + input.put("2", "3"); + rbd.getConstructorArgumentValues().addGenericArgumentValue(input); + rbd.getConstructorArgumentValues().addGenericArgumentValue(input); + + bf.registerBeanDefinition("genericBean", rbd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + + assertThat(gb.getShortMap()).isNotSameAs(gb.getPlainMap()); + assertThat(gb.getPlainMap().size()).isEqualTo(2); + assertThat(gb.getPlainMap().get("1")).isEqualTo("0"); + assertThat(gb.getPlainMap().get("2")).isEqualTo("3"); + assertThat(gb.getShortMap().size()).isEqualTo(2); + assertThat(gb.getShortMap().get(new Short("1"))).isEqualTo(0); + assertThat(gb.getShortMap().get(new Short("2"))).isEqualTo(3); + } + + @Test + public void testGenericMapMapConstructorWithSameRefAndNoConversion() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); + + Map input = new HashMap<>(); + input.put(new Short((short) 1), 0); + input.put(new Short((short) 2), 3); + rbd.getConstructorArgumentValues().addGenericArgumentValue(input); + rbd.getConstructorArgumentValues().addGenericArgumentValue(input); + + bf.registerBeanDefinition("genericBean", rbd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + + assertThat(gb.getShortMap()).isSameAs(gb.getPlainMap()); + assertThat(gb.getShortMap().size()).isEqualTo(2); + assertThat(gb.getShortMap().get(new Short("1"))).isEqualTo(0); + assertThat(gb.getShortMap().get(new Short("2"))).isEqualTo(3); + } + + @Test + public void testGenericMapWithKeyTypeConstructor() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); + + Map input = new HashMap<>(); + input.put("4", "5"); + input.put("6", "7"); + rbd.getConstructorArgumentValues().addGenericArgumentValue(input); + + bf.registerBeanDefinition("genericBean", rbd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + + assertThat(gb.getLongMap().get(4L)).isEqualTo("5"); + assertThat(gb.getLongMap().get(6L)).isEqualTo("7"); + } + + @Test + public void testGenericMapWithCollectionValueConstructor() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.addPropertyEditorRegistrar(new PropertyEditorRegistrar() { + @Override + public void registerCustomEditors(PropertyEditorRegistry registry) { + registry.registerCustomEditor(Number.class, new CustomNumberEditor(Integer.class, false)); + } + }); + RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); + + Map> input = new HashMap<>(); + HashSet value1 = new HashSet<>(); + value1.add(1); + input.put("1", value1); + ArrayList value2 = new ArrayList<>(); + value2.add(Boolean.TRUE); + input.put("2", value2); + rbd.getConstructorArgumentValues().addGenericArgumentValue(Boolean.TRUE); + rbd.getConstructorArgumentValues().addGenericArgumentValue(input); + + bf.registerBeanDefinition("genericBean", rbd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + + boolean condition1 = gb.getCollectionMap().get(1) instanceof HashSet; + assertThat(condition1).isTrue(); + boolean condition = gb.getCollectionMap().get(2) instanceof ArrayList; + assertThat(condition).isTrue(); + } + + + @Test + public void testGenericSetFactoryMethod() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); + rbd.setFactoryMethodName("createInstance"); + + Set input = new HashSet<>(); + input.add("4"); + input.add("5"); + rbd.getConstructorArgumentValues().addGenericArgumentValue(input); + + bf.registerBeanDefinition("genericBean", rbd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + + assertThat(gb.getIntegerSet().contains(4)).isTrue(); + assertThat(gb.getIntegerSet().contains(5)).isTrue(); + } + + @Test + public void testGenericSetListFactoryMethod() throws Exception { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); + rbd.setFactoryMethodName("createInstance"); + + Set input = new HashSet<>(); + input.add("4"); + input.add("5"); + List input2 = new ArrayList<>(); + input2.add("http://localhost:8080"); + input2.add("http://localhost:9090"); + rbd.getConstructorArgumentValues().addGenericArgumentValue(input); + rbd.getConstructorArgumentValues().addGenericArgumentValue(input2); + + bf.registerBeanDefinition("genericBean", rbd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + + assertThat(gb.getIntegerSet().contains(4)).isTrue(); + assertThat(gb.getIntegerSet().contains(5)).isTrue(); + assertThat(gb.getResourceList().get(0)).isEqualTo(new UrlResource("http://localhost:8080")); + assertThat(gb.getResourceList().get(1)).isEqualTo(new UrlResource("http://localhost:9090")); + } + + @Test + public void testGenericSetMapFactoryMethod() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); + rbd.setFactoryMethodName("createInstance"); + + Set input = new HashSet<>(); + input.add("4"); + input.add("5"); + Map input2 = new HashMap<>(); + input2.put("4", "5"); + input2.put("6", "7"); + rbd.getConstructorArgumentValues().addGenericArgumentValue(input); + rbd.getConstructorArgumentValues().addGenericArgumentValue(input2); + + bf.registerBeanDefinition("genericBean", rbd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + + assertThat(gb.getIntegerSet().contains(4)).isTrue(); + assertThat(gb.getIntegerSet().contains(5)).isTrue(); + assertThat(gb.getShortMap().get(new Short("4"))).isEqualTo(5); + assertThat(gb.getShortMap().get(new Short("6"))).isEqualTo(7); + } + + @Test + public void testGenericMapResourceFactoryMethod() throws Exception { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); + rbd.setFactoryMethodName("createInstance"); + + Map input = new HashMap<>(); + input.put("4", "5"); + input.put("6", "7"); + rbd.getConstructorArgumentValues().addGenericArgumentValue(input); + rbd.getConstructorArgumentValues().addGenericArgumentValue("http://localhost:8080"); + + bf.registerBeanDefinition("genericBean", rbd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + + assertThat(gb.getShortMap().get(new Short("4"))).isEqualTo(5); + assertThat(gb.getShortMap().get(new Short("6"))).isEqualTo(7); + assertThat(gb.getResourceList().get(0)).isEqualTo(new UrlResource("http://localhost:8080")); + } + + @Test + public void testGenericMapMapFactoryMethod() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); + rbd.setFactoryMethodName("createInstance"); + + Map input = new HashMap<>(); + input.put("1", "0"); + input.put("2", "3"); + Map input2 = new HashMap<>(); + input2.put("4", "5"); + input2.put("6", "7"); + rbd.getConstructorArgumentValues().addGenericArgumentValue(input); + rbd.getConstructorArgumentValues().addGenericArgumentValue(input2); + + bf.registerBeanDefinition("genericBean", rbd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + + assertThat(gb.getPlainMap().get("1")).isEqualTo("0"); + assertThat(gb.getPlainMap().get("2")).isEqualTo("3"); + assertThat(gb.getShortMap().get(new Short("4"))).isEqualTo(5); + assertThat(gb.getShortMap().get(new Short("6"))).isEqualTo(7); + } + + @Test + public void testGenericMapWithKeyTypeFactoryMethod() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); + rbd.setFactoryMethodName("createInstance"); + + Map input = new HashMap<>(); + input.put("4", "5"); + input.put("6", "7"); + rbd.getConstructorArgumentValues().addGenericArgumentValue(input); + + bf.registerBeanDefinition("genericBean", rbd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + + assertThat(gb.getLongMap().get(new Long("4"))).isEqualTo("5"); + assertThat(gb.getLongMap().get(new Long("6"))).isEqualTo("7"); + } + + @Test + public void testGenericMapWithCollectionValueFactoryMethod() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.addPropertyEditorRegistrar(new PropertyEditorRegistrar() { + @Override + public void registerCustomEditors(PropertyEditorRegistry registry) { + registry.registerCustomEditor(Number.class, new CustomNumberEditor(Integer.class, false)); + } + }); + RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); + rbd.setFactoryMethodName("createInstance"); + + Map> input = new HashMap<>(); + HashSet value1 = new HashSet<>(); + value1.add(1); + input.put("1", value1); + ArrayList value2 = new ArrayList<>(); + value2.add(Boolean.TRUE); + input.put("2", value2); + rbd.getConstructorArgumentValues().addGenericArgumentValue(Boolean.TRUE); + rbd.getConstructorArgumentValues().addGenericArgumentValue(input); + + bf.registerBeanDefinition("genericBean", rbd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + + boolean condition1 = gb.getCollectionMap().get(1) instanceof HashSet; + assertThat(condition1).isTrue(); + boolean condition = gb.getCollectionMap().get(2) instanceof ArrayList; + assertThat(condition).isTrue(); + } + + @Test + public void testGenericListBean() throws Exception { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions( + new ClassPathResource("genericBeanTests.xml", getClass())); + List list = (List) bf.getBean("list"); + assertThat(list.size()).isEqualTo(1); + assertThat(list.get(0)).isEqualTo(new URL("http://localhost:8080")); + } + + @Test + public void testGenericSetBean() throws Exception { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions( + new ClassPathResource("genericBeanTests.xml", getClass())); + Set set = (Set) bf.getBean("set"); + assertThat(set.size()).isEqualTo(1); + assertThat(set.iterator().next()).isEqualTo(new URL("http://localhost:8080")); + } + + @Test + public void testGenericMapBean() throws Exception { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions( + new ClassPathResource("genericBeanTests.xml", getClass())); + Map map = (Map) bf.getBean("map"); + assertThat(map.size()).isEqualTo(1); + assertThat(map.keySet().iterator().next()).isEqualTo(10); + assertThat(map.values().iterator().next()).isEqualTo(new URL("http://localhost:8080")); + } + + @Test + public void testGenericallyTypedIntegerBean() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions( + new ClassPathResource("genericBeanTests.xml", getClass())); + GenericIntegerBean gb = (GenericIntegerBean) bf.getBean("integerBean"); + assertThat(gb.getGenericProperty()).isEqualTo(10); + assertThat(gb.getGenericListProperty().get(0)).isEqualTo(20); + assertThat(gb.getGenericListProperty().get(1)).isEqualTo(30); + } + + @Test + public void testGenericallyTypedSetOfIntegerBean() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions( + new ClassPathResource("genericBeanTests.xml", getClass())); + GenericSetOfIntegerBean gb = (GenericSetOfIntegerBean) bf.getBean("setOfIntegerBean"); + assertThat(gb.getGenericProperty().iterator().next()).isEqualTo(10); + assertThat(gb.getGenericListProperty().get(0).iterator().next()).isEqualTo(20); + assertThat(gb.getGenericListProperty().get(1).iterator().next()).isEqualTo(30); + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + public void testSetBean() throws Exception { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions( + new ClassPathResource("genericBeanTests.xml", getClass())); + UrlSet us = (UrlSet) bf.getBean("setBean"); + assertThat(us.size()).isEqualTo(1); + assertThat(us.iterator().next()).isEqualTo(new URL("https://www.springframework.org")); + } + + /** + * Tests support for parameterized static {@code factory-method} declarations such as + * Mockito's {@code mock()} method which has the following signature. + *
    +	 * {@code
    +	 * public static  T mock(Class classToMock)
    +	 * }
    +	 * 
    + *

    See SPR-9493 + */ + @Test + public void parameterizedStaticFactoryMethod() { + RootBeanDefinition rbd = new RootBeanDefinition(Mockito.class); + rbd.setFactoryMethodName("mock"); + rbd.getConstructorArgumentValues().addGenericArgumentValue(Runnable.class); + + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("mock", rbd); + + assertThat(bf.getType("mock")).isEqualTo(Runnable.class); + assertThat(bf.getType("mock")).isEqualTo(Runnable.class); + Map beans = bf.getBeansOfType(Runnable.class); + assertThat(beans.size()).isEqualTo(1); + } + + /** + * Tests support for parameterized instance {@code factory-method} declarations such + * as EasyMock's {@code IMocksControl.createMock()} method which has the following + * signature. + *

    +	 * {@code
    +	 * public  T createMock(Class toMock)
    +	 * }
    +	 * 
    + *

    See SPR-10411 + */ + @Test + public void parameterizedInstanceFactoryMethod() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + + RootBeanDefinition rbd = new RootBeanDefinition(MocksControl.class); + bf.registerBeanDefinition("mocksControl", rbd); + + rbd = new RootBeanDefinition(); + rbd.setFactoryBeanName("mocksControl"); + rbd.setFactoryMethodName("createMock"); + rbd.getConstructorArgumentValues().addGenericArgumentValue(Runnable.class); + bf.registerBeanDefinition("mock", rbd); + + assertThat(bf.isTypeMatch("mock", Runnable.class)).isTrue(); + assertThat(bf.isTypeMatch("mock", Runnable.class)).isTrue(); + assertThat(bf.getType("mock")).isEqualTo(Runnable.class); + assertThat(bf.getType("mock")).isEqualTo(Runnable.class); + Map beans = bf.getBeansOfType(Runnable.class); + assertThat(beans.size()).isEqualTo(1); + } + + @Test + public void parameterizedInstanceFactoryMethodWithNonResolvedClassName() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + + RootBeanDefinition rbd = new RootBeanDefinition(MocksControl.class); + bf.registerBeanDefinition("mocksControl", rbd); + + rbd = new RootBeanDefinition(); + rbd.setFactoryBeanName("mocksControl"); + rbd.setFactoryMethodName("createMock"); + rbd.getConstructorArgumentValues().addGenericArgumentValue(Runnable.class.getName()); + bf.registerBeanDefinition("mock", rbd); + + assertThat(bf.isTypeMatch("mock", Runnable.class)).isTrue(); + assertThat(bf.isTypeMatch("mock", Runnable.class)).isTrue(); + assertThat(bf.getType("mock")).isEqualTo(Runnable.class); + assertThat(bf.getType("mock")).isEqualTo(Runnable.class); + Map beans = bf.getBeansOfType(Runnable.class); + assertThat(beans.size()).isEqualTo(1); + } + + @Test + public void parameterizedInstanceFactoryMethodWithWrappedClassName() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + + RootBeanDefinition rbd = new RootBeanDefinition(); + rbd.setBeanClassName(Mockito.class.getName()); + rbd.setFactoryMethodName("mock"); + // TypedStringValue used to be equivalent to an XML-defined argument String + rbd.getConstructorArgumentValues().addGenericArgumentValue(new TypedStringValue(Runnable.class.getName())); + bf.registerBeanDefinition("mock", rbd); + + assertThat(bf.isTypeMatch("mock", Runnable.class)).isTrue(); + assertThat(bf.isTypeMatch("mock", Runnable.class)).isTrue(); + assertThat(bf.getType("mock")).isEqualTo(Runnable.class); + assertThat(bf.getType("mock")).isEqualTo(Runnable.class); + Map beans = bf.getBeansOfType(Runnable.class); + assertThat(beans.size()).isEqualTo(1); + } + + @Test + public void parameterizedInstanceFactoryMethodWithInvalidClassName() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + + RootBeanDefinition rbd = new RootBeanDefinition(MocksControl.class); + bf.registerBeanDefinition("mocksControl", rbd); + + rbd = new RootBeanDefinition(); + rbd.setFactoryBeanName("mocksControl"); + rbd.setFactoryMethodName("createMock"); + rbd.getConstructorArgumentValues().addGenericArgumentValue("x"); + bf.registerBeanDefinition("mock", rbd); + + assertThat(bf.isTypeMatch("mock", Runnable.class)).isFalse(); + assertThat(bf.isTypeMatch("mock", Runnable.class)).isFalse(); + assertThat(bf.getType("mock")).isNull(); + assertThat(bf.getType("mock")).isNull(); + Map beans = bf.getBeansOfType(Runnable.class); + assertThat(beans.size()).isEqualTo(0); + } + + @Test + public void parameterizedInstanceFactoryMethodWithIndexedArgument() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + + RootBeanDefinition rbd = new RootBeanDefinition(MocksControl.class); + bf.registerBeanDefinition("mocksControl", rbd); + + rbd = new RootBeanDefinition(); + rbd.setFactoryBeanName("mocksControl"); + rbd.setFactoryMethodName("createMock"); + rbd.getConstructorArgumentValues().addIndexedArgumentValue(0, Runnable.class); + bf.registerBeanDefinition("mock", rbd); + + assertThat(bf.isTypeMatch("mock", Runnable.class)).isTrue(); + assertThat(bf.isTypeMatch("mock", Runnable.class)).isTrue(); + assertThat(bf.getType("mock")).isEqualTo(Runnable.class); + assertThat(bf.getType("mock")).isEqualTo(Runnable.class); + Map beans = bf.getBeansOfType(Runnable.class); + assertThat(beans.size()).isEqualTo(1); + } + + @Test // SPR-16720 + public void parameterizedInstanceFactoryMethodWithTempClassLoader() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.setTempClassLoader(new OverridingClassLoader(getClass().getClassLoader())); + + RootBeanDefinition rbd = new RootBeanDefinition(MocksControl.class); + bf.registerBeanDefinition("mocksControl", rbd); + + rbd = new RootBeanDefinition(); + rbd.setFactoryBeanName("mocksControl"); + rbd.setFactoryMethodName("createMock"); + rbd.getConstructorArgumentValues().addGenericArgumentValue(Runnable.class); + bf.registerBeanDefinition("mock", rbd); + + assertThat(bf.isTypeMatch("mock", Runnable.class)).isTrue(); + assertThat(bf.isTypeMatch("mock", Runnable.class)).isTrue(); + assertThat(bf.getType("mock")).isEqualTo(Runnable.class); + assertThat(bf.getType("mock")).isEqualTo(Runnable.class); + Map beans = bf.getBeansOfType(Runnable.class); + assertThat(beans.size()).isEqualTo(1); + } + + @Test + public void testGenericMatchingWithBeanNameDifferentiation() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.setAutowireCandidateResolver(new GenericTypeAwareAutowireCandidateResolver()); + + bf.registerBeanDefinition("doubleStore", new RootBeanDefinition(NumberStore.class)); + bf.registerBeanDefinition("floatStore", new RootBeanDefinition(NumberStore.class)); + bf.registerBeanDefinition("numberBean", + new RootBeanDefinition(NumberBean.class, RootBeanDefinition.AUTOWIRE_CONSTRUCTOR, false)); + + NumberBean nb = bf.getBean(NumberBean.class); + assertThat(nb.getDoubleStore()).isSameAs(bf.getBean("doubleStore")); + assertThat(nb.getFloatStore()).isSameAs(bf.getBean("floatStore")); + + String[] numberStoreNames = bf.getBeanNamesForType(ResolvableType.forClass(NumberStore.class)); + String[] doubleStoreNames = bf.getBeanNamesForType(ResolvableType.forClassWithGenerics(NumberStore.class, Double.class)); + String[] floatStoreNames = bf.getBeanNamesForType(ResolvableType.forClassWithGenerics(NumberStore.class, Float.class)); + assertThat(numberStoreNames.length).isEqualTo(2); + assertThat(numberStoreNames[0]).isEqualTo("doubleStore"); + assertThat(numberStoreNames[1]).isEqualTo("floatStore"); + assertThat(doubleStoreNames.length).isEqualTo(0); + assertThat(floatStoreNames.length).isEqualTo(0); + } + + @Test + public void testGenericMatchingWithFullTypeDifferentiation() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); + bf.setAutowireCandidateResolver(new GenericTypeAwareAutowireCandidateResolver()); + + RootBeanDefinition bd1 = new RootBeanDefinition(NumberStoreFactory.class); + bd1.setFactoryMethodName("newDoubleStore"); + bf.registerBeanDefinition("store1", bd1); + RootBeanDefinition bd2 = new RootBeanDefinition(NumberStoreFactory.class); + bd2.setFactoryMethodName("newFloatStore"); + bf.registerBeanDefinition("store2", bd2); + bf.registerBeanDefinition("numberBean", + new RootBeanDefinition(NumberBean.class, RootBeanDefinition.AUTOWIRE_CONSTRUCTOR, false)); + + NumberBean nb = bf.getBean(NumberBean.class); + assertThat(nb.getDoubleStore()).isSameAs(bf.getBean("store1")); + assertThat(nb.getFloatStore()).isSameAs(bf.getBean("store2")); + + String[] numberStoreNames = bf.getBeanNamesForType(ResolvableType.forClass(NumberStore.class)); + String[] doubleStoreNames = bf.getBeanNamesForType(ResolvableType.forClassWithGenerics(NumberStore.class, Double.class)); + String[] floatStoreNames = bf.getBeanNamesForType(ResolvableType.forClassWithGenerics(NumberStore.class, Float.class)); + assertThat(numberStoreNames.length).isEqualTo(2); + assertThat(numberStoreNames[0]).isEqualTo("store1"); + assertThat(numberStoreNames[1]).isEqualTo("store2"); + assertThat(doubleStoreNames.length).isEqualTo(1); + assertThat(doubleStoreNames[0]).isEqualTo("store1"); + assertThat(floatStoreNames.length).isEqualTo(1); + assertThat(floatStoreNames[0]).isEqualTo("store2"); + + ObjectProvider> numberStoreProvider = bf.getBeanProvider(ResolvableType.forClass(NumberStore.class)); + ObjectProvider> doubleStoreProvider = bf.getBeanProvider(ResolvableType.forClassWithGenerics(NumberStore.class, Double.class)); + ObjectProvider> floatStoreProvider = bf.getBeanProvider(ResolvableType.forClassWithGenerics(NumberStore.class, Float.class)); + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(numberStoreProvider::getObject); + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(numberStoreProvider::getIfAvailable); + assertThat(numberStoreProvider.getIfUnique()).isNull(); + assertThat(doubleStoreProvider.getObject()).isSameAs(bf.getBean("store1")); + assertThat(doubleStoreProvider.getIfAvailable()).isSameAs(bf.getBean("store1")); + assertThat(doubleStoreProvider.getIfUnique()).isSameAs(bf.getBean("store1")); + assertThat(floatStoreProvider.getObject()).isSameAs(bf.getBean("store2")); + assertThat(floatStoreProvider.getIfAvailable()).isSameAs(bf.getBean("store2")); + assertThat(floatStoreProvider.getIfUnique()).isSameAs(bf.getBean("store2")); + + List> resolved = new ArrayList<>(); + for (NumberStore instance : numberStoreProvider) { + resolved.add(instance); + } + assertThat(resolved.size()).isEqualTo(2); + assertThat(resolved.get(0)).isSameAs(bf.getBean("store1")); + assertThat(resolved.get(1)).isSameAs(bf.getBean("store2")); + + resolved = numberStoreProvider.stream().collect(Collectors.toList()); + assertThat(resolved.size()).isEqualTo(2); + assertThat(resolved.get(0)).isSameAs(bf.getBean("store1")); + assertThat(resolved.get(1)).isSameAs(bf.getBean("store2")); + + resolved = numberStoreProvider.orderedStream().collect(Collectors.toList()); + assertThat(resolved.size()).isEqualTo(2); + assertThat(resolved.get(0)).isSameAs(bf.getBean("store2")); + assertThat(resolved.get(1)).isSameAs(bf.getBean("store1")); + + resolved = new ArrayList<>(); + for (NumberStore instance : doubleStoreProvider) { + resolved.add(instance); + } + assertThat(resolved.size()).isEqualTo(1); + assertThat(resolved.contains(bf.getBean("store1"))).isTrue(); + + resolved = doubleStoreProvider.stream().collect(Collectors.toList()); + assertThat(resolved.size()).isEqualTo(1); + assertThat(resolved.contains(bf.getBean("store1"))).isTrue(); + + resolved = doubleStoreProvider.orderedStream().collect(Collectors.toList()); + assertThat(resolved.size()).isEqualTo(1); + assertThat(resolved.contains(bf.getBean("store1"))).isTrue(); + + resolved = new ArrayList<>(); + for (NumberStore instance : floatStoreProvider) { + resolved.add(instance); + } + assertThat(resolved.size()).isEqualTo(1); + assertThat(resolved.contains(bf.getBean("store2"))).isTrue(); + + resolved = floatStoreProvider.stream().collect(Collectors.toList()); + assertThat(resolved.size()).isEqualTo(1); + assertThat(resolved.contains(bf.getBean("store2"))).isTrue(); + + resolved = floatStoreProvider.orderedStream().collect(Collectors.toList()); + assertThat(resolved.size()).isEqualTo(1); + assertThat(resolved.contains(bf.getBean("store2"))).isTrue(); + } + + @Test + public void testGenericMatchingWithUnresolvedOrderedStream() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); + bf.setAutowireCandidateResolver(new GenericTypeAwareAutowireCandidateResolver()); + + RootBeanDefinition bd1 = new RootBeanDefinition(NumberStoreFactory.class); + bd1.setFactoryMethodName("newDoubleStore"); + bf.registerBeanDefinition("store1", bd1); + RootBeanDefinition bd2 = new RootBeanDefinition(NumberStoreFactory.class); + bd2.setFactoryMethodName("newFloatStore"); + bf.registerBeanDefinition("store2", bd2); + + ObjectProvider> numberStoreProvider = bf.getBeanProvider(ResolvableType.forClass(NumberStore.class)); + List> resolved = numberStoreProvider.orderedStream().collect(Collectors.toList()); + assertThat(resolved.size()).isEqualTo(2); + assertThat(resolved.get(0)).isSameAs(bf.getBean("store2")); + assertThat(resolved.get(1)).isSameAs(bf.getBean("store1")); + } + + + @SuppressWarnings("serial") + public static class NamedUrlList extends ArrayList { + } + + + @SuppressWarnings("serial") + public static class NamedUrlSet extends HashSet { + } + + + @SuppressWarnings("serial") + public static class NamedUrlMap extends HashMap { + } + + + public static class CollectionDependentBean { + + public CollectionDependentBean(NamedUrlList list, NamedUrlSet set, NamedUrlMap map) { + assertThat(list.size()).isEqualTo(1); + assertThat(set.size()).isEqualTo(1); + assertThat(map.size()).isEqualTo(1); + } + } + + + @SuppressWarnings("serial") + public static class UrlSet extends HashSet { + + public UrlSet() { + super(); + } + + public UrlSet(Set urls) { + super(); + } + + public void setUrlNames(Set urlNames) throws MalformedURLException { + for (URI urlName : urlNames) { + add(urlName.toURL()); + } + } + } + + + /** + * Pseudo-implementation of EasyMock's {@code MocksControl} class. + */ + public static class MocksControl { + + @SuppressWarnings("unchecked") + public T createMock(Class toMock) { + return (T) Proxy.newProxyInstance(BeanFactoryGenericsTests.class.getClassLoader(), new Class[] {toMock}, + new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + throw new UnsupportedOperationException("mocked!"); + } + }); + } + } + + + public static class NumberStore { + } + + + public static class DoubleStore extends NumberStore { + } + + + public static class FloatStore extends NumberStore { + } + + + public static class NumberBean { + + private final NumberStore doubleStore; + + private final NumberStore floatStore; + + public NumberBean(NumberStore doubleStore, NumberStore floatStore) { + this.doubleStore = doubleStore; + this.floatStore = floatStore; + } + + public NumberStore getDoubleStore() { + return this.doubleStore; + } + + public NumberStore getFloatStore() { + return this.floatStore; + } + } + + + public static class NumberStoreFactory { + + @Order(1) + public static NumberStore newDoubleStore() { + return new DoubleStore(); + } + + @Order(0) + public static NumberStore newFloatStore() { + return new FloatStore(); + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistryTests.java new file mode 100644 index 0000000..6e3f846 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistryTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.testfixture.beans.DerivedTestBean; +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @author Chris Beams + * @since 04.07.2006 + */ +public class DefaultSingletonBeanRegistryTests { + + @Test + public void testSingletons() { + DefaultSingletonBeanRegistry beanRegistry = new DefaultSingletonBeanRegistry(); + + TestBean tb = new TestBean(); + beanRegistry.registerSingleton("tb", tb); + assertThat(beanRegistry.getSingleton("tb")).isSameAs(tb); + + TestBean tb2 = (TestBean) beanRegistry.getSingleton("tb2", new ObjectFactory() { + @Override + public Object getObject() throws BeansException { + return new TestBean(); + } + }); + assertThat(beanRegistry.getSingleton("tb2")).isSameAs(tb2); + + assertThat(beanRegistry.getSingleton("tb")).isSameAs(tb); + assertThat(beanRegistry.getSingleton("tb2")).isSameAs(tb2); + assertThat(beanRegistry.getSingletonCount()).isEqualTo(2); + String[] names = beanRegistry.getSingletonNames(); + assertThat(names.length).isEqualTo(2); + assertThat(names[0]).isEqualTo("tb"); + assertThat(names[1]).isEqualTo("tb2"); + + beanRegistry.destroySingletons(); + assertThat(beanRegistry.getSingletonCount()).isEqualTo(0); + assertThat(beanRegistry.getSingletonNames().length).isEqualTo(0); + } + + @Test + public void testDisposableBean() { + DefaultSingletonBeanRegistry beanRegistry = new DefaultSingletonBeanRegistry(); + + DerivedTestBean tb = new DerivedTestBean(); + beanRegistry.registerSingleton("tb", tb); + beanRegistry.registerDisposableBean("tb", tb); + assertThat(beanRegistry.getSingleton("tb")).isSameAs(tb); + + assertThat(beanRegistry.getSingleton("tb")).isSameAs(tb); + assertThat(beanRegistry.getSingletonCount()).isEqualTo(1); + String[] names = beanRegistry.getSingletonNames(); + assertThat(names.length).isEqualTo(1); + assertThat(names[0]).isEqualTo("tb"); + assertThat(tb.wasDestroyed()).isFalse(); + + beanRegistry.destroySingletons(); + assertThat(beanRegistry.getSingletonCount()).isEqualTo(0); + assertThat(beanRegistry.getSingletonNames().length).isEqualTo(0); + assertThat(tb.wasDestroyed()).isTrue(); + } + + @Test + public void testDependentRegistration() { + DefaultSingletonBeanRegistry beanRegistry = new DefaultSingletonBeanRegistry(); + + beanRegistry.registerDependentBean("a", "b"); + beanRegistry.registerDependentBean("b", "c"); + beanRegistry.registerDependentBean("c", "b"); + assertThat(beanRegistry.isDependent("a", "b")).isTrue(); + assertThat(beanRegistry.isDependent("b", "c")).isTrue(); + assertThat(beanRegistry.isDependent("c", "b")).isTrue(); + assertThat(beanRegistry.isDependent("a", "c")).isTrue(); + assertThat(beanRegistry.isDependent("c", "a")).isFalse(); + assertThat(beanRegistry.isDependent("b", "a")).isFalse(); + assertThat(beanRegistry.isDependent("a", "a")).isFalse(); + assertThat(beanRegistry.isDependent("b", "b")).isTrue(); + assertThat(beanRegistry.isDependent("c", "c")).isTrue(); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/DefinitionMetadataEqualsHashCodeTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/DefinitionMetadataEqualsHashCodeTests.java new file mode 100644 index 0000000..36c0c4b --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/DefinitionMetadataEqualsHashCodeTests.java @@ -0,0 +1,135 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@code equals()} and {@code hashCode()} in bean definitions. + * + * @author Rob Harrop + * @author Sam Brannen + */ +@SuppressWarnings("serial") +public class DefinitionMetadataEqualsHashCodeTests { + + @Test + public void rootBeanDefinition() { + RootBeanDefinition master = new RootBeanDefinition(TestBean.class); + RootBeanDefinition equal = new RootBeanDefinition(TestBean.class); + RootBeanDefinition notEqual = new RootBeanDefinition(String.class); + RootBeanDefinition subclass = new RootBeanDefinition(TestBean.class) { + }; + setBaseProperties(master); + setBaseProperties(equal); + setBaseProperties(notEqual); + setBaseProperties(subclass); + + assertEqualsAndHashCodeContracts(master, equal, notEqual, subclass); + } + + /** + * @since 3.2.8 + * @see SPR-11420 + */ + @Test + public void rootBeanDefinitionAndMethodOverridesWithDifferentOverloadedValues() { + RootBeanDefinition master = new RootBeanDefinition(TestBean.class); + RootBeanDefinition equal = new RootBeanDefinition(TestBean.class); + + setBaseProperties(master); + setBaseProperties(equal); + + // Simulate AbstractBeanDefinition.validate() which delegates to + // AbstractBeanDefinition.prepareMethodOverrides(): + master.getMethodOverrides().getOverrides().iterator().next().setOverloaded(false); + // But do not simulate validation of the 'equal' bean. As a consequence, a method + // override in 'equal' will be marked as overloaded, but the corresponding + // override in 'master' will not. But... the bean definitions should still be + // considered equal. + + assertThat(equal).as("Should be equal").isEqualTo(master); + assertThat(equal.hashCode()).as("Hash code for equal instances must match").isEqualTo(master.hashCode()); + } + + @Test + public void childBeanDefinition() { + ChildBeanDefinition master = new ChildBeanDefinition("foo"); + ChildBeanDefinition equal = new ChildBeanDefinition("foo"); + ChildBeanDefinition notEqual = new ChildBeanDefinition("bar"); + ChildBeanDefinition subclass = new ChildBeanDefinition("foo") { + }; + setBaseProperties(master); + setBaseProperties(equal); + setBaseProperties(notEqual); + setBaseProperties(subclass); + + assertEqualsAndHashCodeContracts(master, equal, notEqual, subclass); + } + + @Test + public void runtimeBeanReference() { + RuntimeBeanReference master = new RuntimeBeanReference("name"); + RuntimeBeanReference equal = new RuntimeBeanReference("name"); + RuntimeBeanReference notEqual = new RuntimeBeanReference("someOtherName"); + RuntimeBeanReference subclass = new RuntimeBeanReference("name") { + }; + assertEqualsAndHashCodeContracts(master, equal, notEqual, subclass); + } + + private void setBaseProperties(AbstractBeanDefinition definition) { + definition.setAbstract(true); + definition.setAttribute("foo", "bar"); + definition.setAutowireCandidate(false); + definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); + // definition.getConstructorArgumentValues().addGenericArgumentValue("foo"); + definition.setDependencyCheck(AbstractBeanDefinition.DEPENDENCY_CHECK_OBJECTS); + definition.setDependsOn(new String[] { "foo", "bar" }); + definition.setDestroyMethodName("destroy"); + definition.setEnforceDestroyMethod(false); + definition.setEnforceInitMethod(true); + definition.setFactoryBeanName("factoryBean"); + definition.setFactoryMethodName("factoryMethod"); + definition.setInitMethodName("init"); + definition.setLazyInit(true); + definition.getMethodOverrides().addOverride(new LookupOverride("foo", "bar")); + definition.getMethodOverrides().addOverride(new ReplaceOverride("foo", "bar")); + definition.getPropertyValues().add("foo", "bar"); + definition.setResourceDescription("desc"); + definition.setRole(BeanDefinition.ROLE_APPLICATION); + definition.setScope(BeanDefinition.SCOPE_PROTOTYPE); + definition.setSource("foo"); + } + + private void assertEqualsAndHashCodeContracts(Object master, Object equal, Object notEqual, Object subclass) { + assertThat(equal).as("Should be equal").isEqualTo(master); + assertThat(equal.hashCode()).as("Hash code for equal instances should match").isEqualTo(master.hashCode()); + + assertThat(notEqual).as("Should not be equal").isNotEqualTo(master); + assertThat(notEqual.hashCode()).as("Hash code for non-equal instances should not match").isNotEqualTo(master.hashCode()); + + assertThat(subclass).as("Subclass should be equal").isEqualTo(master); + assertThat(subclass.hashCode()).as("Hash code for subclass should match").isEqualTo(master.hashCode()); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/LookupMethodTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/LookupMethodTests.java new file mode 100644 index 0000000..eeb34b6 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/LookupMethodTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Karl Pietrzak + * @author Juergen Hoeller + */ +public class LookupMethodTests { + + private DefaultListableBeanFactory beanFactory; + + + @BeforeEach + public void setUp() { + beanFactory = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory); + reader.loadBeanDefinitions(new ClassPathResource("lookupMethodTests.xml", getClass())); + } + + + @Test + public void testWithoutConstructorArg() { + AbstractBean bean = (AbstractBean) beanFactory.getBean("abstractBean"); + assertThat(bean).isNotNull(); + Object expected = bean.get(); + assertThat(expected.getClass()).isEqualTo(TestBean.class); + } + + @Test + public void testWithOverloadedArg() { + AbstractBean bean = (AbstractBean) beanFactory.getBean("abstractBean"); + assertThat(bean).isNotNull(); + TestBean expected = bean.get("haha"); + assertThat(expected.getClass()).isEqualTo(TestBean.class); + assertThat(expected.getName()).isEqualTo("haha"); + } + + @Test + public void testWithOneConstructorArg() { + AbstractBean bean = (AbstractBean) beanFactory.getBean("abstractBean"); + assertThat(bean).isNotNull(); + TestBean expected = bean.getOneArgument("haha"); + assertThat(expected.getClass()).isEqualTo(TestBean.class); + assertThat(expected.getName()).isEqualTo("haha"); + } + + @Test + public void testWithTwoConstructorArg() { + AbstractBean bean = (AbstractBean) beanFactory.getBean("abstractBean"); + assertThat(bean).isNotNull(); + TestBean expected = bean.getTwoArguments("haha", 72); + assertThat(expected.getClass()).isEqualTo(TestBean.class); + assertThat(expected.getName()).isEqualTo("haha"); + assertThat(expected.getAge()).isEqualTo(72); + } + + @Test + public void testWithThreeArgsShouldFail() { + AbstractBean bean = (AbstractBean) beanFactory.getBean("abstractBean"); + assertThat(bean).isNotNull(); + assertThatExceptionOfType(AbstractMethodError.class).as("does not have a three arg constructor").isThrownBy(() -> + bean.getThreeArguments("name", 1, 2)); + } + + @Test + public void testWithOverriddenLookupMethod() { + AbstractBean bean = (AbstractBean) beanFactory.getBean("extendedBean"); + assertThat(bean).isNotNull(); + TestBean expected = bean.getOneArgument("haha"); + assertThat(expected.getClass()).isEqualTo(TestBean.class); + assertThat(expected.getName()).isEqualTo("haha"); + assertThat(expected.isJedi()).isTrue(); + } + + + public static abstract class AbstractBean { + + public abstract TestBean get(); + + public abstract TestBean get(String name); // overloaded + + public abstract TestBean getOneArgument(String name); + + public abstract TestBean getTwoArguments(String name, int age); + + public abstract TestBean getThreeArguments(String name, int age, int anotherArg); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedListTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedListTests.java new file mode 100644 index 0000000..9bf6576 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedListTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * @author Rick Evans + * @author Juergen Hoeller + * @author Sam Brannen + */ +@SuppressWarnings({ "rawtypes", "unchecked" }) +public class ManagedListTests { + + @Test + public void mergeSunnyDay() { + ManagedList parent = new ManagedList(); + parent.add("one"); + parent.add("two"); + ManagedList child = new ManagedList(); + child.add("three"); + child.setMergeEnabled(true); + List mergedList = child.merge(parent); + assertThat(mergedList.size()).as("merge() obviously did not work.").isEqualTo(3); + } + + @Test + public void mergeWithNullParent() { + ManagedList child = new ManagedList(); + child.add("one"); + child.setMergeEnabled(true); + assertThat(child.merge(null)).isSameAs(child); + } + + @Test + public void mergeNotAllowedWhenMergeNotEnabled() { + ManagedList child = new ManagedList(); + assertThatIllegalStateException().isThrownBy(() -> + child.merge(null)); + } + + @Test + public void mergeWithNonCompatibleParentType() { + ManagedList child = new ManagedList(); + child.add("one"); + child.setMergeEnabled(true); + assertThatIllegalArgumentException().isThrownBy(() -> + child.merge("hello")); + } + + @Test + public void mergeEmptyChild() { + ManagedList parent = new ManagedList(); + parent.add("one"); + parent.add("two"); + ManagedList child = new ManagedList(); + child.setMergeEnabled(true); + List mergedList = child.merge(parent); + assertThat(mergedList.size()).as("merge() obviously did not work.").isEqualTo(2); + } + + @Test + public void mergeChildValuesOverrideTheParents() { + // doesn't make much sense in the context of a list... + ManagedList parent = new ManagedList(); + parent.add("one"); + parent.add("two"); + ManagedList child = new ManagedList(); + child.add("one"); + child.setMergeEnabled(true); + List mergedList = child.merge(parent); + assertThat(mergedList.size()).as("merge() obviously did not work.").isEqualTo(3); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedMapTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedMapTests.java new file mode 100644 index 0000000..1f6dc5e --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedMapTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * @author Rick Evans + * @author Juergen Hoeller + * @author Sam Brannen + */ +@SuppressWarnings({ "rawtypes", "unchecked" }) +public class ManagedMapTests { + + @Test + public void mergeSunnyDay() { + ManagedMap parent = new ManagedMap(); + parent.put("one", "one"); + parent.put("two", "two"); + ManagedMap child = new ManagedMap(); + child.put("three", "three"); + child.setMergeEnabled(true); + Map mergedMap = (Map) child.merge(parent); + assertThat(mergedMap.size()).as("merge() obviously did not work.").isEqualTo(3); + } + + @Test + public void mergeWithNullParent() { + ManagedMap child = new ManagedMap(); + child.setMergeEnabled(true); + assertThat(child.merge(null)).isSameAs(child); + } + + @Test + public void mergeWithNonCompatibleParentType() { + ManagedMap map = new ManagedMap(); + map.setMergeEnabled(true); + assertThatIllegalArgumentException().isThrownBy(() -> + map.merge("hello")); + } + + @Test + public void mergeNotAllowedWhenMergeNotEnabled() { + assertThatIllegalStateException().isThrownBy(() -> + new ManagedMap().merge(null)); + } + + @Test + public void mergeEmptyChild() { + ManagedMap parent = new ManagedMap(); + parent.put("one", "one"); + parent.put("two", "two"); + ManagedMap child = new ManagedMap(); + child.setMergeEnabled(true); + Map mergedMap = (Map) child.merge(parent); + assertThat(mergedMap.size()).as("merge() obviously did not work.").isEqualTo(2); + } + + @Test + public void mergeChildValuesOverrideTheParents() { + ManagedMap parent = new ManagedMap(); + parent.put("one", "one"); + parent.put("two", "two"); + ManagedMap child = new ManagedMap(); + child.put("one", "fork"); + child.setMergeEnabled(true); + Map mergedMap = (Map) child.merge(parent); + // child value for 'one' must override parent value... + assertThat(mergedMap.size()).as("merge() obviously did not work.").isEqualTo(2); + assertThat(mergedMap.get("one")).as("Parent value not being overridden during merge().").isEqualTo("fork"); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedPropertiesTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedPropertiesTests.java new file mode 100644 index 0000000..473679f --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedPropertiesTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * @author Rick Evans + * @author Juergen Hoeller + * @author Sam Brannen + */ +@SuppressWarnings("rawtypes") +public class ManagedPropertiesTests { + + @Test + public void mergeSunnyDay() { + ManagedProperties parent = new ManagedProperties(); + parent.setProperty("one", "one"); + parent.setProperty("two", "two"); + ManagedProperties child = new ManagedProperties(); + child.setProperty("three", "three"); + child.setMergeEnabled(true); + Map mergedMap = (Map) child.merge(parent); + assertThat(mergedMap.size()).as("merge() obviously did not work.").isEqualTo(3); + } + + @Test + public void mergeWithNullParent() { + ManagedProperties child = new ManagedProperties(); + child.setMergeEnabled(true); + assertThat(child.merge(null)).isSameAs(child); + } + + @Test + public void mergeWithNonCompatibleParentType() { + ManagedProperties map = new ManagedProperties(); + map.setMergeEnabled(true); + assertThatIllegalArgumentException().isThrownBy(() -> + map.merge("hello")); + } + + @Test + public void mergeNotAllowedWhenMergeNotEnabled() { + ManagedProperties map = new ManagedProperties(); + assertThatIllegalStateException().isThrownBy(() -> + map.merge(null)); + } + + @Test + public void mergeEmptyChild() { + ManagedProperties parent = new ManagedProperties(); + parent.setProperty("one", "one"); + parent.setProperty("two", "two"); + ManagedProperties child = new ManagedProperties(); + child.setMergeEnabled(true); + Map mergedMap = (Map) child.merge(parent); + assertThat(mergedMap.size()).as("merge() obviously did not work.").isEqualTo(2); + } + + @Test + public void mergeChildValuesOverrideTheParents() { + ManagedProperties parent = new ManagedProperties(); + parent.setProperty("one", "one"); + parent.setProperty("two", "two"); + ManagedProperties child = new ManagedProperties(); + child.setProperty("one", "fork"); + child.setMergeEnabled(true); + Map mergedMap = (Map) child.merge(parent); + // child value for 'one' must override parent value... + assertThat(mergedMap.size()).as("merge() obviously did not work.").isEqualTo(2); + assertThat(mergedMap.get("one")).as("Parent value not being overridden during merge().").isEqualTo("fork"); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedSetTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedSetTests.java new file mode 100644 index 0000000..39c0899 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/ManagedSetTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support; + +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * @author Rick Evans + * @author Juergen Hoeller + * @author Sam Brannen + */ +@SuppressWarnings({ "rawtypes", "unchecked" }) +public class ManagedSetTests { + + @Test + public void mergeSunnyDay() { + ManagedSet parent = new ManagedSet(); + parent.add("one"); + parent.add("two"); + ManagedSet child = new ManagedSet(); + child.add("three"); + child.setMergeEnabled(true); + Set mergedSet = child.merge(parent); + assertThat(mergedSet.size()).as("merge() obviously did not work.").isEqualTo(3); + } + + @Test + public void mergeWithNullParent() { + ManagedSet child = new ManagedSet(); + child.add("one"); + child.setMergeEnabled(true); + assertThat(child.merge(null)).isSameAs(child); + } + + @Test + public void mergeNotAllowedWhenMergeNotEnabled() { + assertThatIllegalStateException().isThrownBy(() -> + new ManagedSet().merge(null)); + } + + @Test + public void mergeWithNonCompatibleParentType() { + ManagedSet child = new ManagedSet(); + child.add("one"); + child.setMergeEnabled(true); + assertThatIllegalArgumentException().isThrownBy(() -> + child.merge("hello")); + } + + @Test + public void mergeEmptyChild() { + ManagedSet parent = new ManagedSet(); + parent.add("one"); + parent.add("two"); + ManagedSet child = new ManagedSet(); + child.setMergeEnabled(true); + Set mergedSet = child.merge(parent); + assertThat(mergedSet.size()).as("merge() obviously did not work.").isEqualTo(2); + } + + @Test + public void mergeChildValuesOverrideTheParents() { + // asserts that the set contract is not violated during a merge() operation... + ManagedSet parent = new ManagedSet(); + parent.add("one"); + parent.add("two"); + ManagedSet child = new ManagedSet(); + child.add("one"); + child.setMergeEnabled(true); + Set mergedSet = child.merge(parent); + assertThat(mergedSet.size()).as("merge() obviously did not work.").isEqualTo(2); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/PropertiesBeanDefinitionReaderTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/PropertiesBeanDefinitionReaderTests.java new file mode 100644 index 0000000..d7df748 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/PropertiesBeanDefinitionReaderTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + */ +@SuppressWarnings("deprecation") +class PropertiesBeanDefinitionReaderTests { + + private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + private final PropertiesBeanDefinitionReader reader = new PropertiesBeanDefinitionReader(this.beanFactory); + + + @Test + void withSimpleConstructorArg() { + this.reader.loadBeanDefinitions(new ClassPathResource("simpleConstructorArg.properties", getClass())); + TestBean bean = (TestBean) this.beanFactory.getBean("testBean"); + assertThat(bean.getName()).isEqualTo("Rob Harrop"); + } + + @Test + void withConstructorArgRef() { + this.reader.loadBeanDefinitions(new ClassPathResource("refConstructorArg.properties", getClass())); + TestBean rob = (TestBean) this.beanFactory.getBean("rob"); + TestBean sally = (TestBean) this.beanFactory.getBean("sally"); + assertThat(rob.getSpouse()).isEqualTo(sally); + } + + @Test + void withMultipleConstructorsArgs() { + this.reader.loadBeanDefinitions(new ClassPathResource("multiConstructorArgs.properties", getClass())); + TestBean bean = (TestBean) this.beanFactory.getBean("testBean"); + assertThat(bean.getName()).isEqualTo("Rob Harrop"); + assertThat(bean.getAge()).isEqualTo(23); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireBeanFactoryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireBeanFactoryTests.java new file mode 100644 index 0000000..17fd92d --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireBeanFactoryTests.java @@ -0,0 +1,250 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.MethodParameter; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Mark Fisher + * @author Juergen Hoeller + */ +public class QualifierAnnotationAutowireBeanFactoryTests { + + private static final String JUERGEN = "juergen"; + + private static final String MARK = "mark"; + + + @Test + public void testAutowireCandidateDefaultWithIrrelevantDescriptor() throws Exception { + DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + ConstructorArgumentValues cavs = new ConstructorArgumentValues(); + cavs.addGenericArgumentValue(JUERGEN); + RootBeanDefinition rbd = new RootBeanDefinition(Person.class, cavs, null); + lbf.registerBeanDefinition(JUERGEN, rbd); + assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isTrue(); + assertThat(lbf.isAutowireCandidate(JUERGEN, + new DependencyDescriptor(Person.class.getDeclaredField("name"), false))).isTrue(); + assertThat(lbf.isAutowireCandidate(JUERGEN, + new DependencyDescriptor(Person.class.getDeclaredField("name"), true))).isTrue(); + } + + @Test + public void testAutowireCandidateExplicitlyFalseWithIrrelevantDescriptor() throws Exception { + DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + ConstructorArgumentValues cavs = new ConstructorArgumentValues(); + cavs.addGenericArgumentValue(JUERGEN); + RootBeanDefinition rbd = new RootBeanDefinition(Person.class, cavs, null); + rbd.setAutowireCandidate(false); + lbf.registerBeanDefinition(JUERGEN, rbd); + assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isFalse(); + assertThat(lbf.isAutowireCandidate(JUERGEN, + new DependencyDescriptor(Person.class.getDeclaredField("name"), false))).isFalse(); + assertThat(lbf.isAutowireCandidate(JUERGEN, + new DependencyDescriptor(Person.class.getDeclaredField("name"), true))).isFalse(); + } + + @Disabled + @Test + public void testAutowireCandidateWithFieldDescriptor() throws Exception { + DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + person1.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); + lbf.registerBeanDefinition(JUERGEN, person1); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + lbf.registerBeanDefinition(MARK, person2); + DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor( + QualifiedTestBean.class.getDeclaredField("qualified"), false); + DependencyDescriptor nonqualifiedDescriptor = new DependencyDescriptor( + QualifiedTestBean.class.getDeclaredField("nonqualified"), false); + assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isTrue(); + assertThat(lbf.isAutowireCandidate(JUERGEN, nonqualifiedDescriptor)).isTrue(); + assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isTrue(); + assertThat(lbf.isAutowireCandidate(MARK, null)).isTrue(); + assertThat(lbf.isAutowireCandidate(MARK, nonqualifiedDescriptor)).isTrue(); + assertThat(lbf.isAutowireCandidate(MARK, qualifiedDescriptor)).isFalse(); + } + + @Test + public void testAutowireCandidateExplicitlyFalseWithFieldDescriptor() throws Exception { + DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + ConstructorArgumentValues cavs = new ConstructorArgumentValues(); + cavs.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person = new RootBeanDefinition(Person.class, cavs, null); + person.setAutowireCandidate(false); + person.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); + lbf.registerBeanDefinition(JUERGEN, person); + DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor( + QualifiedTestBean.class.getDeclaredField("qualified"), false); + DependencyDescriptor nonqualifiedDescriptor = new DependencyDescriptor( + QualifiedTestBean.class.getDeclaredField("nonqualified"), false); + assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isFalse(); + assertThat(lbf.isAutowireCandidate(JUERGEN, nonqualifiedDescriptor)).isFalse(); + assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isFalse(); + } + + @Test + public void testAutowireCandidateWithShortClassName() throws Exception { + DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + ConstructorArgumentValues cavs = new ConstructorArgumentValues(); + cavs.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person = new RootBeanDefinition(Person.class, cavs, null); + person.addQualifier(new AutowireCandidateQualifier(ClassUtils.getShortName(TestQualifier.class))); + lbf.registerBeanDefinition(JUERGEN, person); + DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor( + QualifiedTestBean.class.getDeclaredField("qualified"), false); + DependencyDescriptor nonqualifiedDescriptor = new DependencyDescriptor( + QualifiedTestBean.class.getDeclaredField("nonqualified"), false); + assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isTrue(); + assertThat(lbf.isAutowireCandidate(JUERGEN, nonqualifiedDescriptor)).isTrue(); + assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isTrue(); + } + + @Disabled + @Test + public void testAutowireCandidateWithConstructorDescriptor() throws Exception { + DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + person1.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); + lbf.registerBeanDefinition(JUERGEN, person1); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + lbf.registerBeanDefinition(MARK, person2); + MethodParameter param = new MethodParameter(QualifiedTestBean.class.getDeclaredConstructor(Person.class), 0); + DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor(param, false); + param.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer()); + assertThat(param.getParameterName()).isEqualTo("tpb"); + assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isTrue(); + assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isTrue(); + assertThat(lbf.isAutowireCandidate(MARK, qualifiedDescriptor)).isFalse(); + } + + @Disabled + @Test + public void testAutowireCandidateWithMethodDescriptor() throws Exception { + DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + person1.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); + lbf.registerBeanDefinition(JUERGEN, person1); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + lbf.registerBeanDefinition(MARK, person2); + MethodParameter qualifiedParam = + new MethodParameter(QualifiedTestBean.class.getDeclaredMethod("autowireQualified", Person.class), 0); + MethodParameter nonqualifiedParam = + new MethodParameter(QualifiedTestBean.class.getDeclaredMethod("autowireNonqualified", Person.class), 0); + DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor(qualifiedParam, false); + DependencyDescriptor nonqualifiedDescriptor = new DependencyDescriptor(nonqualifiedParam, false); + qualifiedParam.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer()); + assertThat(qualifiedParam.getParameterName()).isEqualTo("tpb"); + nonqualifiedParam.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer()); + assertThat(nonqualifiedParam.getParameterName()).isEqualTo("tpb"); + assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isTrue(); + assertThat(lbf.isAutowireCandidate(JUERGEN, nonqualifiedDescriptor)).isTrue(); + assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isTrue(); + assertThat(lbf.isAutowireCandidate(MARK, null)).isTrue(); + assertThat(lbf.isAutowireCandidate(MARK, nonqualifiedDescriptor)).isTrue(); + assertThat(lbf.isAutowireCandidate(MARK, qualifiedDescriptor)).isFalse(); + } + + @Test + public void testAutowireCandidateWithMultipleCandidatesDescriptor() throws Exception { + DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + person1.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); + lbf.registerBeanDefinition(JUERGEN, person1); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + person2.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); + lbf.registerBeanDefinition(MARK, person2); + DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor( + new MethodParameter(QualifiedTestBean.class.getDeclaredConstructor(Person.class), 0), + false); + assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isTrue(); + assertThat(lbf.isAutowireCandidate(MARK, qualifiedDescriptor)).isTrue(); + } + + + @SuppressWarnings("unused") + private static class QualifiedTestBean { + + @TestQualifier + private Person qualified; + + private Person nonqualified; + + public QualifiedTestBean(@TestQualifier Person tpb) { + } + + public void autowireQualified(@TestQualifier Person tpb) { + } + + public void autowireNonqualified(Person tpb) { + } + } + + + @SuppressWarnings("unused") + private static class Person { + + private String name; + + public Person(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + } + + + @Target({ElementType.FIELD, ElementType.PARAMETER}) + @Retention(RetentionPolicy.RUNTIME) + @Qualifier + private static @interface TestQualifier { + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/Spr8954Tests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/Spr8954Tests.java new file mode 100644 index 0000000..a1db2e3 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/Spr8954Tests.java @@ -0,0 +1,136 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support; + +import java.util.Arrays; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor; +import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Unit tests for SPR-8954, in which a custom {@link InstantiationAwareBeanPostProcessor} + * forces the predicted type of a FactoryBean, effectively preventing retrieval of the + * bean from calls to #getBeansOfType(FactoryBean.class). The implementation of + * {@link AbstractBeanFactory#isFactoryBean(String, RootBeanDefinition)} now ensures that + * not only the predicted bean type is considered, but also the original bean definition's + * beanClass. + * + * @author Chris Beams + * @author Oliver Gierke + */ +public class Spr8954Tests { + + private DefaultListableBeanFactory bf; + + @BeforeEach + public void setUp() { + bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("foo", new RootBeanDefinition(FooFactoryBean.class)); + bf.addBeanPostProcessor(new PredictingBPP()); + } + + @Test + public void repro() { + assertThat(bf.getBean("foo")).isInstanceOf(Foo.class); + assertThat(bf.getBean("&foo")).isInstanceOf(FooFactoryBean.class); + assertThat(bf.isTypeMatch("&foo", FactoryBean.class)).isTrue(); + + @SuppressWarnings("rawtypes") + Map fbBeans = bf.getBeansOfType(FactoryBean.class); + assertThat(fbBeans).hasSize(1); + assertThat(fbBeans.keySet()).contains("&foo"); + + Map aiBeans = bf.getBeansOfType(AnInterface.class); + assertThat(aiBeans).hasSize(1); + assertThat(aiBeans.keySet()).contains("&foo"); + } + + @Test + public void findsBeansByTypeIfNotInstantiated() { + assertThat(bf.isTypeMatch("&foo", FactoryBean.class)).isTrue(); + + @SuppressWarnings("rawtypes") + Map fbBeans = bf.getBeansOfType(FactoryBean.class); + assertThat(1).isEqualTo(fbBeans.size()); + assertThat("&foo").isEqualTo(fbBeans.keySet().iterator().next()); + + Map aiBeans = bf.getBeansOfType(AnInterface.class); + assertThat(aiBeans).hasSize(1); + assertThat(aiBeans.keySet()).contains("&foo"); + } + + /** + * SPR-10517 + */ + @Test + public void findsFactoryBeanNameByTypeWithoutInstantiation() { + String[] names = bf.getBeanNamesForType(AnInterface.class, false, false); + assertThat(Arrays.asList(names)).contains("&foo"); + + Map beans = bf.getBeansOfType(AnInterface.class, false, false); + assertThat(beans).hasSize(1); + assertThat(beans.keySet()).contains("&foo"); + } + + + static class FooFactoryBean implements FactoryBean, AnInterface { + + @Override + public Foo getObject() throws Exception { + return new Foo(); + } + + @Override + public Class getObjectType() { + return Foo.class; + } + + @Override + public boolean isSingleton() { + return true; + } + } + + interface AnInterface { + } + + static class Foo { + } + + interface PredictedType { + } + + static class PredictedTypeImpl implements PredictedType { + } + + static class PredictingBPP implements SmartInstantiationAwareBeanPostProcessor { + + @Override + public Class predictBeanType(Class beanClass, String beanName) { + return FactoryBean.class.isAssignableFrom(beanClass) ? PredictedType.class : null; + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/security/CallbacksSecurityTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/security/CallbacksSecurityTests.java new file mode 100644 index 0000000..368e0b6 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/security/CallbacksSecurityTests.java @@ -0,0 +1,479 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support.security; + +import java.lang.reflect.Method; +import java.net.URL; +import java.security.AccessControlContext; +import java.security.AccessController; +import java.security.Permissions; +import java.security.Policy; +import java.security.Principal; +import java.security.PrivilegedAction; +import java.security.PrivilegedExceptionAction; +import java.security.ProtectionDomain; +import java.util.PropertyPermission; +import java.util.Set; +import java.util.function.Consumer; + +import javax.security.auth.AuthPermission; +import javax.security.auth.Subject; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.SmartFactoryBean; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.SecurityContextProvider; +import org.springframework.beans.factory.support.security.support.ConstructorBean; +import org.springframework.beans.factory.support.security.support.CustomCallbackBean; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.core.NestedRuntimeException; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.testfixture.security.TestPrincipal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Security test case. Checks whether the container uses its privileges for its + * internal work but does not leak them when touching/calling user code. + * + *

    The first half of the test case checks that permissions are downgraded when + * calling user code while the second half that the caller code permission get + * through and Spring doesn't override the permission stack. + * + * @author Costin Leau + */ +public class CallbacksSecurityTests { + + private DefaultListableBeanFactory beanFactory; + private SecurityContextProvider provider; + + @SuppressWarnings("unused") + private static class NonPrivilegedBean { + + private String expectedName; + public static boolean destroyed = false; + + public NonPrivilegedBean(String expected) { + this.expectedName = expected; + checkCurrentContext(); + } + + public void init() { + checkCurrentContext(); + } + + public void destroy() { + checkCurrentContext(); + destroyed = true; + } + + public void setProperty(Object value) { + checkCurrentContext(); + } + + public Object getProperty() { + checkCurrentContext(); + return null; + } + + public void setListProperty(Object value) { + checkCurrentContext(); + } + + public Object getListProperty() { + checkCurrentContext(); + return null; + } + + private void checkCurrentContext() { + assertThat(getCurrentSubjectName()).isEqualTo(expectedName); + } + } + + @SuppressWarnings("unused") + private static class NonPrivilegedSpringCallbacksBean implements + InitializingBean, DisposableBean, BeanClassLoaderAware, + BeanFactoryAware, BeanNameAware { + + private String expectedName; + public static boolean destroyed = false; + + public NonPrivilegedSpringCallbacksBean(String expected) { + this.expectedName = expected; + checkCurrentContext(); + } + + @Override + public void afterPropertiesSet() { + checkCurrentContext(); + } + + @Override + public void destroy() { + checkCurrentContext(); + destroyed = true; + } + + @Override + public void setBeanName(String name) { + checkCurrentContext(); + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + checkCurrentContext(); + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) + throws BeansException { + checkCurrentContext(); + } + + private void checkCurrentContext() { + assertThat(getCurrentSubjectName()).isEqualTo(expectedName); + } + } + + @SuppressWarnings({ "unused", "rawtypes" }) + private static class NonPrivilegedFactoryBean implements SmartFactoryBean { + private String expectedName; + + public NonPrivilegedFactoryBean(String expected) { + this.expectedName = expected; + checkCurrentContext(); + } + + @Override + public boolean isEagerInit() { + checkCurrentContext(); + return false; + } + + @Override + public boolean isPrototype() { + checkCurrentContext(); + return true; + } + + @Override + public Object getObject() throws Exception { + checkCurrentContext(); + return new Object(); + } + + @Override + public Class getObjectType() { + checkCurrentContext(); + return Object.class; + } + + @Override + public boolean isSingleton() { + checkCurrentContext(); + return false; + } + + private void checkCurrentContext() { + assertThat(getCurrentSubjectName()).isEqualTo(expectedName); + } + } + + @SuppressWarnings("unused") + private static class NonPrivilegedFactory { + + private final String expectedName; + + public NonPrivilegedFactory(String expected) { + this.expectedName = expected; + assertThat(getCurrentSubjectName()).isEqualTo(expectedName); + } + + public static Object makeStaticInstance(String expectedName) { + assertThat(getCurrentSubjectName()).isEqualTo(expectedName); + return new Object(); + } + + public Object makeInstance() { + assertThat(getCurrentSubjectName()).isEqualTo(expectedName); + return new Object(); + } + } + + private static String getCurrentSubjectName() { + final AccessControlContext acc = AccessController.getContext(); + + return AccessController.doPrivileged(new PrivilegedAction() { + + @Override + public String run() { + Subject subject = Subject.getSubject(acc); + if (subject == null) { + return null; + } + + Set principals = subject.getPrincipals(); + + if (principals == null) { + return null; + } + for (Principal p : principals) { + return p.getName(); + } + return null; + } + }); + } + + public CallbacksSecurityTests() { + // setup security + if (System.getSecurityManager() == null) { + Policy policy = Policy.getPolicy(); + URL policyURL = getClass() + .getResource( + "/org/springframework/beans/factory/support/security/policy.all"); + System.setProperty("java.security.policy", policyURL.toString()); + System.setProperty("policy.allowSystemProperty", "true"); + policy.refresh(); + + System.setSecurityManager(new SecurityManager()); + } + } + + @BeforeEach + public void setUp() throws Exception { + + final ProtectionDomain empty = new ProtectionDomain(null, + new Permissions()); + + provider = new SecurityContextProvider() { + private final AccessControlContext acc = new AccessControlContext( + new ProtectionDomain[] { empty }); + + @Override + public AccessControlContext getAccessControlContext() { + return acc; + } + }; + + DefaultResourceLoader drl = new DefaultResourceLoader(); + Resource config = drl + .getResource("/org/springframework/beans/factory/support/security/callbacks.xml"); + beanFactory = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(beanFactory).loadBeanDefinitions(config); + beanFactory.setSecurityContextProvider(provider); + } + + @Test + public void testSecuritySanity() throws Exception { + AccessControlContext acc = provider.getAccessControlContext(); + assertThatExceptionOfType(SecurityException.class).as( + "Acc should not have any permissions").isThrownBy(() -> + acc.checkPermission(new PropertyPermission("*", "read"))); + + CustomCallbackBean bean = new CustomCallbackBean(); + Method method = bean.getClass().getMethod("destroy"); + method.setAccessible(true); + + assertThatExceptionOfType(Exception.class).isThrownBy(() -> + AccessController.doPrivileged((PrivilegedExceptionAction) () -> { + method.invoke(bean); + return null; + }, acc)); + + Class cl = ConstructorBean.class; + assertThatExceptionOfType(Exception.class).isThrownBy(() -> + AccessController.doPrivileged((PrivilegedExceptionAction) () -> + cl.newInstance(), acc)); + } + + @Test + public void testSpringInitBean() throws Exception { + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + beanFactory.getBean("spring-init")) + .withCauseInstanceOf(SecurityException.class); + } + + @Test + public void testCustomInitBean() throws Exception { + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + beanFactory.getBean("custom-init")) + .withCauseInstanceOf(SecurityException.class); + } + + @Test + public void testSpringDestroyBean() throws Exception { + beanFactory.getBean("spring-destroy"); + beanFactory.destroySingletons(); + assertThat(System.getProperty("security.destroy")).isNull(); + } + + @Test + public void testCustomDestroyBean() throws Exception { + beanFactory.getBean("custom-destroy"); + beanFactory.destroySingletons(); + assertThat(System.getProperty("security.destroy")).isNull(); + } + + @Test + public void testCustomFactoryObject() throws Exception { + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + beanFactory.getBean("spring-factory")) + .withCauseInstanceOf(SecurityException.class); + } + + @Test + public void testCustomFactoryType() throws Exception { + assertThat(beanFactory.getType("spring-factory")).isNull(); + assertThat(System.getProperty("factory.object.type")).isNull(); + } + + @Test + public void testCustomStaticFactoryMethod() throws Exception { + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + beanFactory.getBean("custom-static-factory-method")) + .satisfies(mostSpecificCauseOf(SecurityException.class)); + } + + @Test + public void testCustomInstanceFactoryMethod() throws Exception { + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + beanFactory.getBean("custom-factory-method")) + .satisfies(mostSpecificCauseOf(SecurityException.class)); + } + + @Test + public void testTrustedFactoryMethod() throws Exception { + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + beanFactory.getBean("privileged-static-factory-method")) + .satisfies(mostSpecificCauseOf(SecurityException.class)); + } + + @Test + public void testConstructor() throws Exception { + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + beanFactory.getBean("constructor")) + .satisfies(mostSpecificCauseOf(SecurityException.class)); + } + + @Test + public void testContainerPrivileges() throws Exception { + AccessControlContext acc = provider.getAccessControlContext(); + + AccessController.doPrivileged(new PrivilegedExceptionAction() { + + @Override + public Object run() throws Exception { + beanFactory.getBean("working-factory-method"); + beanFactory.getBean("container-execution"); + return null; + } + }, acc); + } + + @Test + public void testPropertyInjection() throws Exception { + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + beanFactory.getBean("property-injection")) + .withMessageContaining("security"); + beanFactory.getBean("working-property-injection"); + } + + @Test + public void testInitSecurityAwarePrototypeBean() { + final DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + BeanDefinitionBuilder bdb = BeanDefinitionBuilder + .genericBeanDefinition(NonPrivilegedBean.class).setScope( + BeanDefinition.SCOPE_PROTOTYPE) + .setInitMethodName("init").setDestroyMethodName("destroy") + .addConstructorArgValue("user1"); + lbf.registerBeanDefinition("test", bdb.getBeanDefinition()); + final Subject subject = new Subject(); + subject.getPrincipals().add(new TestPrincipal("user1")); + + NonPrivilegedBean bean = Subject.doAsPrivileged( + subject, new PrivilegedAction() { + @Override + public NonPrivilegedBean run() { + return lbf.getBean("test", NonPrivilegedBean.class); + } + }, null); + assertThat(bean).isNotNull(); + } + + @Test + public void testTrustedExecution() throws Exception { + beanFactory.setSecurityContextProvider(null); + + Permissions perms = new Permissions(); + perms.add(new AuthPermission("getSubject")); + ProtectionDomain pd = new ProtectionDomain(null, perms); + + new AccessControlContext(new ProtectionDomain[] { pd }); + + final Subject subject = new Subject(); + subject.getPrincipals().add(new TestPrincipal("user1")); + + // request the beans from non-privileged code + Subject.doAsPrivileged(subject, new PrivilegedAction() { + + @Override + public Object run() { + // sanity check + assertThat(getCurrentSubjectName()).isEqualTo("user1"); + assertThat(NonPrivilegedBean.destroyed).isEqualTo(false); + + beanFactory.getBean("trusted-spring-callbacks"); + beanFactory.getBean("trusted-custom-init-destroy"); + // the factory is a prototype - ask for multiple instances + beanFactory.getBean("trusted-spring-factory"); + beanFactory.getBean("trusted-spring-factory"); + beanFactory.getBean("trusted-spring-factory"); + + beanFactory.getBean("trusted-factory-bean"); + beanFactory.getBean("trusted-static-factory-method"); + beanFactory.getBean("trusted-factory-method"); + beanFactory.getBean("trusted-property-injection"); + beanFactory.getBean("trusted-working-property-injection"); + + beanFactory.destroySingletons(); + assertThat(NonPrivilegedBean.destroyed).isEqualTo(true); + return null; + } + }, provider.getAccessControlContext()); + } + + private Consumer mostSpecificCauseOf(Class type) { + return ex -> assertThat(ex.getMostSpecificCause()).isInstanceOf(type); + + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/ConstructorBean.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/ConstructorBean.java new file mode 100644 index 0000000..fc60fc3 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/ConstructorBean.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support.security.support; + +/** + * @author Costin Leau + */ +public class ConstructorBean { + + public ConstructorBean() { + System.getProperties(); + } + + public ConstructorBean(Object obj) { + } +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/CustomCallbackBean.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/CustomCallbackBean.java new file mode 100644 index 0000000..4874306 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/CustomCallbackBean.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support.security.support; + +/** + * @author Costin Leau + */ +public class CustomCallbackBean { + + public void init() { + System.getProperties(); + } + + public void destroy() { + System.setProperty("security.destroy", "true"); + } +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/CustomFactoryBean.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/CustomFactoryBean.java new file mode 100644 index 0000000..4ec3d71 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/CustomFactoryBean.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support.security.support; + +import java.util.Properties; + +import org.springframework.beans.factory.FactoryBean; + +/** + * @author Costin Leau + */ +public class CustomFactoryBean implements FactoryBean { + + @Override + public Properties getObject() throws Exception { + return System.getProperties(); + } + + @Override + public Class getObjectType() { + System.setProperty("factory.object.type", "true"); + return Properties.class; + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/DestroyBean.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/DestroyBean.java new file mode 100644 index 0000000..67005ab --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/DestroyBean.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support.security.support; + +import org.springframework.beans.factory.DisposableBean; + +/** + * @author Costin Leau + */ +public class DestroyBean implements DisposableBean { + + @Override + public void destroy() throws Exception { + System.setProperty("security.destroy", "true"); + } +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/FactoryBean.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/FactoryBean.java new file mode 100644 index 0000000..4f7fb62 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/FactoryBean.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support.security.support; + +/** + * @author Costin Leau + */ +public class FactoryBean { + + public static Object makeStaticInstance() { + System.getProperties(); + return new Object(); + } + + protected static Object protectedStaticInstance() { + return "protectedStaticInstance"; + } + + public Object makeInstance() { + System.getProperties(); + return new Object(); + } +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/InitBean.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/InitBean.java new file mode 100644 index 0000000..3693bb9 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/InitBean.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support.security.support; + +import org.springframework.beans.factory.InitializingBean; + +/** + * @author Costin Leau + */ +public class InitBean implements InitializingBean { + + @Override + public void afterPropertiesSet() throws Exception { + System.getProperties(); + } +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/PropertyBean.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/PropertyBean.java new file mode 100644 index 0000000..5193313 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/security/support/PropertyBean.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support.security.support; + +/** + * @author Costin Leau + */ +public class PropertyBean { + + public void setSecurityProperty(Object property) { + System.getProperties(); + } + + public void setProperty(Object property) { + + } +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/wiring/BeanConfigurerSupportTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/wiring/BeanConfigurerSupportTests.java new file mode 100644 index 0000000..8ff2952 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/wiring/BeanConfigurerSupportTests.java @@ -0,0 +1,127 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.wiring; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author Rick Evans + * @author Juergen Hoeller + * @author Sam Brannen + */ +public class BeanConfigurerSupportTests { + + @Test + public void supplyIncompatibleBeanFactoryImplementation() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new StubBeanConfigurerSupport().setBeanFactory(mock(BeanFactory.class))); + } + + @Test + public void configureBeanDoesNothingIfBeanWiringInfoResolverResolvesToNull() throws Exception { + TestBean beanInstance = new TestBean(); + + BeanWiringInfoResolver resolver = mock(BeanWiringInfoResolver.class); + + BeanConfigurerSupport configurer = new StubBeanConfigurerSupport(); + configurer.setBeanWiringInfoResolver(resolver); + configurer.setBeanFactory(new DefaultListableBeanFactory()); + configurer.configureBean(beanInstance); + verify(resolver).resolveWiringInfo(beanInstance); + assertThat(beanInstance.getName()).isNull(); + } + + @Test + public void configureBeanDoesNothingIfNoBeanFactoryHasBeenSet() throws Exception { + TestBean beanInstance = new TestBean(); + BeanConfigurerSupport configurer = new StubBeanConfigurerSupport(); + configurer.configureBean(beanInstance); + assertThat(beanInstance.getName()).isNull(); + } + + @Test + public void configureBeanReallyDoesDefaultToUsingTheFullyQualifiedClassNameOfTheSuppliedBeanInstance() throws Exception { + TestBean beanInstance = new TestBean(); + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(TestBean.class); + builder.addPropertyValue("name", "Harriet Wheeler"); + + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerBeanDefinition(beanInstance.getClass().getName(), builder.getBeanDefinition()); + + BeanConfigurerSupport configurer = new StubBeanConfigurerSupport(); + configurer.setBeanFactory(factory); + configurer.afterPropertiesSet(); + configurer.configureBean(beanInstance); + assertThat(beanInstance.getName()).as("Bean is evidently not being configured (for some reason)").isEqualTo("Harriet Wheeler"); + } + + @Test + public void configureBeanPerformsAutowiringByNameIfAppropriateBeanWiringInfoResolverIsPluggedIn() throws Exception { + TestBean beanInstance = new TestBean(); + // spouse for autowiring by name... + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(TestBean.class); + builder.addConstructorArgValue("David Gavurin"); + + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerBeanDefinition("spouse", builder.getBeanDefinition()); + + BeanWiringInfoResolver resolver = mock(BeanWiringInfoResolver.class); + given(resolver.resolveWiringInfo(beanInstance)).willReturn(new BeanWiringInfo(BeanWiringInfo.AUTOWIRE_BY_NAME, false)); + + BeanConfigurerSupport configurer = new StubBeanConfigurerSupport(); + configurer.setBeanFactory(factory); + configurer.setBeanWiringInfoResolver(resolver); + configurer.configureBean(beanInstance); + assertThat(beanInstance.getSpouse().getName()).as("Bean is evidently not being configured (for some reason)").isEqualTo("David Gavurin"); + } + + @Test + public void configureBeanPerformsAutowiringByTypeIfAppropriateBeanWiringInfoResolverIsPluggedIn() throws Exception { + TestBean beanInstance = new TestBean(); + // spouse for autowiring by type... + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(TestBean.class); + builder.addConstructorArgValue("David Gavurin"); + + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerBeanDefinition("Mmm, I fancy a salad!", builder.getBeanDefinition()); + + BeanWiringInfoResolver resolver = mock(BeanWiringInfoResolver.class); + given(resolver.resolveWiringInfo(beanInstance)).willReturn(new BeanWiringInfo(BeanWiringInfo.AUTOWIRE_BY_TYPE, false)); + + BeanConfigurerSupport configurer = new StubBeanConfigurerSupport(); + configurer.setBeanFactory(factory); + configurer.setBeanWiringInfoResolver(resolver); + configurer.configureBean(beanInstance); + assertThat(beanInstance.getSpouse().getName()).as("Bean is evidently not being configured (for some reason)").isEqualTo("David Gavurin"); + } + + + private static class StubBeanConfigurerSupport extends BeanConfigurerSupport { + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/wiring/BeanWiringInfoTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/wiring/BeanWiringInfoTests.java new file mode 100644 index 0000000..7919ba0 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/wiring/BeanWiringInfoTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.wiring; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Unit tests for the BeanWiringInfo class. + * + * @author Rick Evans + * @author Sam Brannen + */ +public class BeanWiringInfoTests { + + @Test + public void ctorWithNullBeanName() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new BeanWiringInfo(null)); + } + + @Test + public void ctorWithWhitespacedBeanName() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new BeanWiringInfo(" \t")); + } + + @Test + public void ctorWithEmptyBeanName() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new BeanWiringInfo("")); + } + + @Test + public void ctorWithNegativeIllegalAutowiringValue() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new BeanWiringInfo(-1, true)); + } + + @Test + public void ctorWithPositiveOutOfRangeAutowiringValue() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new BeanWiringInfo(123871, true)); + } + + @Test + public void usingAutowireCtorIndicatesAutowiring() throws Exception { + BeanWiringInfo info = new BeanWiringInfo(BeanWiringInfo.AUTOWIRE_BY_NAME, true); + assertThat(info.indicatesAutowiring()).isTrue(); + } + + @Test + public void usingBeanNameCtorDoesNotIndicateAutowiring() throws Exception { + BeanWiringInfo info = new BeanWiringInfo("fooService"); + assertThat(info.indicatesAutowiring()).isFalse(); + } + + @Test + public void noDependencyCheckValueIsPreserved() throws Exception { + BeanWiringInfo info = new BeanWiringInfo(BeanWiringInfo.AUTOWIRE_BY_NAME, true); + assertThat(info.getDependencyCheck()).isTrue(); + } + + @Test + public void dependencyCheckValueIsPreserved() throws Exception { + BeanWiringInfo info = new BeanWiringInfo(BeanWiringInfo.AUTOWIRE_BY_TYPE, false); + assertThat(info.getDependencyCheck()).isFalse(); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/wiring/ClassNameBeanWiringInfoResolverTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/wiring/ClassNameBeanWiringInfoResolverTests.java new file mode 100644 index 0000000..67cef6f --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/wiring/ClassNameBeanWiringInfoResolverTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.wiring; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Unit tests for the ClassNameBeanWiringInfoResolver class. + * + * @author Rick Evans + */ +public class ClassNameBeanWiringInfoResolverTests { + + @Test + public void resolveWiringInfoWithNullBeanInstance() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new ClassNameBeanWiringInfoResolver().resolveWiringInfo(null)); + } + + @Test + public void resolveWiringInfo() { + ClassNameBeanWiringInfoResolver resolver = new ClassNameBeanWiringInfoResolver(); + Long beanInstance = new Long(1); + BeanWiringInfo info = resolver.resolveWiringInfo(beanInstance); + assertThat(info).isNotNull(); + assertThat(info.getBeanName()).as("Not resolving bean name to the class name of the supplied bean instance as per class contract.").isEqualTo(beanInstance.getClass().getName()); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/AutowireWithExclusionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/AutowireWithExclusionTests.java new file mode 100644 index 0000000..8ea0719 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/AutowireWithExclusionTests.java @@ -0,0 +1,177 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.PropertiesFactoryBean; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + * @author Juergen Hoeller + */ +public class AutowireWithExclusionTests { + + @Test + public void byTypeAutowireWithAutoSelfExclusion() throws Exception { + CountingFactory.reset(); + DefaultListableBeanFactory beanFactory = getBeanFactory("autowire-with-exclusion.xml"); + beanFactory.preInstantiateSingletons(); + TestBean rob = (TestBean) beanFactory.getBean("rob"); + TestBean sally = (TestBean) beanFactory.getBean("sally"); + assertThat(rob.getSpouse()).isEqualTo(sally); + assertThat(CountingFactory.getFactoryBeanInstanceCount()).isEqualTo(1); + } + + @Test + public void byTypeAutowireWithExclusion() throws Exception { + CountingFactory.reset(); + DefaultListableBeanFactory beanFactory = getBeanFactory("autowire-with-exclusion.xml"); + beanFactory.preInstantiateSingletons(); + TestBean rob = (TestBean) beanFactory.getBean("rob"); + assertThat(rob.getSomeProperties().getProperty("name")).isEqualTo("props1"); + assertThat(CountingFactory.getFactoryBeanInstanceCount()).isEqualTo(1); + } + + @Test + public void byTypeAutowireWithExclusionInParentFactory() throws Exception { + CountingFactory.reset(); + DefaultListableBeanFactory parent = getBeanFactory("autowire-with-exclusion.xml"); + parent.preInstantiateSingletons(); + DefaultListableBeanFactory child = new DefaultListableBeanFactory(parent); + RootBeanDefinition robDef = new RootBeanDefinition(TestBean.class); + robDef.setAutowireMode(RootBeanDefinition.AUTOWIRE_BY_TYPE); + robDef.getPropertyValues().add("spouse", new RuntimeBeanReference("sally")); + child.registerBeanDefinition("rob2", robDef); + TestBean rob = (TestBean) child.getBean("rob2"); + assertThat(rob.getSomeProperties().getProperty("name")).isEqualTo("props1"); + assertThat(CountingFactory.getFactoryBeanInstanceCount()).isEqualTo(1); + } + + @Test + public void byTypeAutowireWithPrimaryInParentFactory() throws Exception { + CountingFactory.reset(); + DefaultListableBeanFactory parent = getBeanFactory("autowire-with-exclusion.xml"); + parent.getBeanDefinition("props1").setPrimary(true); + parent.preInstantiateSingletons(); + DefaultListableBeanFactory child = new DefaultListableBeanFactory(parent); + RootBeanDefinition robDef = new RootBeanDefinition(TestBean.class); + robDef.setAutowireMode(RootBeanDefinition.AUTOWIRE_BY_TYPE); + robDef.getPropertyValues().add("spouse", new RuntimeBeanReference("sally")); + child.registerBeanDefinition("rob2", robDef); + RootBeanDefinition propsDef = new RootBeanDefinition(PropertiesFactoryBean.class); + propsDef.getPropertyValues().add("properties", "name=props3"); + child.registerBeanDefinition("props3", propsDef); + TestBean rob = (TestBean) child.getBean("rob2"); + assertThat(rob.getSomeProperties().getProperty("name")).isEqualTo("props1"); + assertThat(CountingFactory.getFactoryBeanInstanceCount()).isEqualTo(1); + } + + @Test + public void byTypeAutowireWithPrimaryOverridingParentFactory() throws Exception { + CountingFactory.reset(); + DefaultListableBeanFactory parent = getBeanFactory("autowire-with-exclusion.xml"); + parent.preInstantiateSingletons(); + DefaultListableBeanFactory child = new DefaultListableBeanFactory(parent); + RootBeanDefinition robDef = new RootBeanDefinition(TestBean.class); + robDef.setAutowireMode(RootBeanDefinition.AUTOWIRE_BY_TYPE); + robDef.getPropertyValues().add("spouse", new RuntimeBeanReference("sally")); + child.registerBeanDefinition("rob2", robDef); + RootBeanDefinition propsDef = new RootBeanDefinition(PropertiesFactoryBean.class); + propsDef.getPropertyValues().add("properties", "name=props3"); + propsDef.setPrimary(true); + child.registerBeanDefinition("props3", propsDef); + TestBean rob = (TestBean) child.getBean("rob2"); + assertThat(rob.getSomeProperties().getProperty("name")).isEqualTo("props3"); + assertThat(CountingFactory.getFactoryBeanInstanceCount()).isEqualTo(1); + } + + @Test + public void byTypeAutowireWithPrimaryInParentAndChild() throws Exception { + CountingFactory.reset(); + DefaultListableBeanFactory parent = getBeanFactory("autowire-with-exclusion.xml"); + parent.getBeanDefinition("props1").setPrimary(true); + parent.preInstantiateSingletons(); + DefaultListableBeanFactory child = new DefaultListableBeanFactory(parent); + RootBeanDefinition robDef = new RootBeanDefinition(TestBean.class); + robDef.setAutowireMode(RootBeanDefinition.AUTOWIRE_BY_TYPE); + robDef.getPropertyValues().add("spouse", new RuntimeBeanReference("sally")); + child.registerBeanDefinition("rob2", robDef); + RootBeanDefinition propsDef = new RootBeanDefinition(PropertiesFactoryBean.class); + propsDef.getPropertyValues().add("properties", "name=props3"); + propsDef.setPrimary(true); + child.registerBeanDefinition("props3", propsDef); + TestBean rob = (TestBean) child.getBean("rob2"); + assertThat(rob.getSomeProperties().getProperty("name")).isEqualTo("props3"); + assertThat(CountingFactory.getFactoryBeanInstanceCount()).isEqualTo(1); + } + + @Test + public void byTypeAutowireWithInclusion() throws Exception { + CountingFactory.reset(); + DefaultListableBeanFactory beanFactory = getBeanFactory("autowire-with-inclusion.xml"); + beanFactory.preInstantiateSingletons(); + TestBean rob = (TestBean) beanFactory.getBean("rob"); + assertThat(rob.getSomeProperties().getProperty("name")).isEqualTo("props1"); + assertThat(CountingFactory.getFactoryBeanInstanceCount()).isEqualTo(1); + } + + @Test + public void byTypeAutowireWithSelectiveInclusion() throws Exception { + CountingFactory.reset(); + DefaultListableBeanFactory beanFactory = getBeanFactory("autowire-with-selective-inclusion.xml"); + beanFactory.preInstantiateSingletons(); + TestBean rob = (TestBean) beanFactory.getBean("rob"); + assertThat(rob.getSomeProperties().getProperty("name")).isEqualTo("props1"); + assertThat(CountingFactory.getFactoryBeanInstanceCount()).isEqualTo(1); + } + + @Test + public void constructorAutowireWithAutoSelfExclusion() throws Exception { + DefaultListableBeanFactory beanFactory = getBeanFactory("autowire-constructor-with-exclusion.xml"); + TestBean rob = (TestBean) beanFactory.getBean("rob"); + TestBean sally = (TestBean) beanFactory.getBean("sally"); + assertThat(rob.getSpouse()).isEqualTo(sally); + TestBean rob2 = (TestBean) beanFactory.getBean("rob"); + assertThat(rob2).isEqualTo(rob); + assertThat(rob2).isNotSameAs(rob); + assertThat(rob2.getSpouse()).isEqualTo(rob.getSpouse()); + assertThat(rob2.getSpouse()).isNotSameAs(rob.getSpouse()); + } + + @Test + public void constructorAutowireWithExclusion() throws Exception { + DefaultListableBeanFactory beanFactory = getBeanFactory("autowire-constructor-with-exclusion.xml"); + TestBean rob = (TestBean) beanFactory.getBean("rob"); + assertThat(rob.getSomeProperties().getProperty("name")).isEqualTo("props1"); + } + + private DefaultListableBeanFactory getBeanFactory(String configPath) { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions( + new ClassPathResource(configPath, getClass())); + return bf; + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/BeanNameGenerationTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/BeanNameGenerationTests.java new file mode 100644 index 0000000..55fef5c --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/BeanNameGenerationTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + * @author Juergen Hoeller + */ +public class BeanNameGenerationTests { + + private DefaultListableBeanFactory beanFactory; + + + @BeforeEach + public void setUp() { + this.beanFactory = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this.beanFactory); + reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_NONE); + reader.loadBeanDefinitions(new ClassPathResource("beanNameGeneration.xml", getClass())); + } + + @Test + public void naming() { + String className = GeneratedNameBean.class.getName(); + + String targetName = className + BeanDefinitionReaderUtils.GENERATED_BEAN_NAME_SEPARATOR + "0"; + GeneratedNameBean topLevel1 = (GeneratedNameBean) beanFactory.getBean(targetName); + assertThat(topLevel1).isNotNull(); + + targetName = className + BeanDefinitionReaderUtils.GENERATED_BEAN_NAME_SEPARATOR + "1"; + GeneratedNameBean topLevel2 = (GeneratedNameBean) beanFactory.getBean(targetName); + assertThat(topLevel2).isNotNull(); + + GeneratedNameBean child1 = topLevel1.getChild(); + assertThat(child1.getBeanName()).isNotNull(); + assertThat(child1.getBeanName().startsWith(className)).isTrue(); + + GeneratedNameBean child2 = topLevel2.getChild(); + assertThat(child2.getBeanName()).isNotNull(); + assertThat(child2.getBeanName().startsWith(className)).isTrue(); + + assertThat(child1.getBeanName().equals(child2.getBeanName())).isFalse(); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/CollectionMergingTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/CollectionMergingTests.java new file mode 100644 index 0000000..04b82ca --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/CollectionMergingTests.java @@ -0,0 +1,207 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.BeanDefinitionReader; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit and integration tests for the collection merging support. + * + * @author Rob Harrop + * @author Rick Evans + */ +@SuppressWarnings("rawtypes") +public class CollectionMergingTests { + + private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + + @BeforeEach + public void setUp() throws Exception { + BeanDefinitionReader reader = new XmlBeanDefinitionReader(this.beanFactory); + reader.loadBeanDefinitions(new ClassPathResource("collectionMerging.xml", getClass())); + } + + @Test + public void mergeList() throws Exception { + TestBean bean = (TestBean) this.beanFactory.getBean("childWithList"); + List list = bean.getSomeList(); + assertThat(list.size()).as("Incorrect size").isEqualTo(3); + assertThat(list.get(0)).isEqualTo("Rob Harrop"); + assertThat(list.get(1)).isEqualTo("Rod Johnson"); + assertThat(list.get(2)).isEqualTo("Juergen Hoeller"); + } + + @Test + public void mergeListWithInnerBeanAsListElement() throws Exception { + TestBean bean = (TestBean) this.beanFactory.getBean("childWithListOfRefs"); + List list = bean.getSomeList(); + assertThat(list).isNotNull(); + assertThat(list.size()).isEqualTo(3); + assertThat(list.get(2)).isNotNull(); + boolean condition = list.get(2) instanceof TestBean; + assertThat(condition).isTrue(); + } + + @Test + public void mergeSet() { + TestBean bean = (TestBean) this.beanFactory.getBean("childWithSet"); + Set set = bean.getSomeSet(); + assertThat(set.size()).as("Incorrect size").isEqualTo(2); + assertThat(set.contains("Rob Harrop")).isTrue(); + assertThat(set.contains("Sally Greenwood")).isTrue(); + } + + @Test + public void mergeSetWithInnerBeanAsSetElement() throws Exception { + TestBean bean = (TestBean) this.beanFactory.getBean("childWithSetOfRefs"); + Set set = bean.getSomeSet(); + assertThat(set).isNotNull(); + assertThat(set.size()).isEqualTo(2); + Iterator it = set.iterator(); + it.next(); + Object o = it.next(); + assertThat(o).isNotNull(); + boolean condition = o instanceof TestBean; + assertThat(condition).isTrue(); + assertThat(((TestBean) o).getName()).isEqualTo("Sally"); + } + + @Test + public void mergeMap() throws Exception { + TestBean bean = (TestBean) this.beanFactory.getBean("childWithMap"); + Map map = bean.getSomeMap(); + assertThat(map.size()).as("Incorrect size").isEqualTo(3); + assertThat(map.get("Rob")).isEqualTo("Sally"); + assertThat(map.get("Rod")).isEqualTo("Kerry"); + assertThat(map.get("Juergen")).isEqualTo("Eva"); + } + + @Test + public void mergeMapWithInnerBeanAsMapEntryValue() throws Exception { + TestBean bean = (TestBean) this.beanFactory.getBean("childWithMapOfRefs"); + Map map = bean.getSomeMap(); + assertThat(map).isNotNull(); + assertThat(map.size()).isEqualTo(2); + assertThat(map.get("Rob")).isNotNull(); + boolean condition = map.get("Rob") instanceof TestBean; + assertThat(condition).isTrue(); + assertThat(((TestBean) map.get("Rob")).getName()).isEqualTo("Sally"); + } + + @Test + public void mergeProperties() throws Exception { + TestBean bean = (TestBean) this.beanFactory.getBean("childWithProps"); + Properties props = bean.getSomeProperties(); + assertThat(props.size()).as("Incorrect size").isEqualTo(3); + assertThat(props.getProperty("Rob")).isEqualTo("Sally"); + assertThat(props.getProperty("Rod")).isEqualTo("Kerry"); + assertThat(props.getProperty("Juergen")).isEqualTo("Eva"); + } + + @Test + public void mergeListInConstructor() throws Exception { + TestBean bean = (TestBean) this.beanFactory.getBean("childWithListInConstructor"); + List list = bean.getSomeList(); + assertThat(list.size()).as("Incorrect size").isEqualTo(3); + assertThat(list.get(0)).isEqualTo("Rob Harrop"); + assertThat(list.get(1)).isEqualTo("Rod Johnson"); + assertThat(list.get(2)).isEqualTo("Juergen Hoeller"); + } + + @Test + public void mergeListWithInnerBeanAsListElementInConstructor() throws Exception { + TestBean bean = (TestBean) this.beanFactory.getBean("childWithListOfRefsInConstructor"); + List list = bean.getSomeList(); + assertThat(list).isNotNull(); + assertThat(list.size()).isEqualTo(3); + assertThat(list.get(2)).isNotNull(); + boolean condition = list.get(2) instanceof TestBean; + assertThat(condition).isTrue(); + } + + @Test + public void mergeSetInConstructor() { + TestBean bean = (TestBean) this.beanFactory.getBean("childWithSetInConstructor"); + Set set = bean.getSomeSet(); + assertThat(set.size()).as("Incorrect size").isEqualTo(2); + assertThat(set.contains("Rob Harrop")).isTrue(); + assertThat(set.contains("Sally Greenwood")).isTrue(); + } + + @Test + public void mergeSetWithInnerBeanAsSetElementInConstructor() throws Exception { + TestBean bean = (TestBean) this.beanFactory.getBean("childWithSetOfRefsInConstructor"); + Set set = bean.getSomeSet(); + assertThat(set).isNotNull(); + assertThat(set.size()).isEqualTo(2); + Iterator it = set.iterator(); + it.next(); + Object o = it.next(); + assertThat(o).isNotNull(); + boolean condition = o instanceof TestBean; + assertThat(condition).isTrue(); + assertThat(((TestBean) o).getName()).isEqualTo("Sally"); + } + + @Test + public void mergeMapInConstructor() throws Exception { + TestBean bean = (TestBean) this.beanFactory.getBean("childWithMapInConstructor"); + Map map = bean.getSomeMap(); + assertThat(map.size()).as("Incorrect size").isEqualTo(3); + assertThat(map.get("Rob")).isEqualTo("Sally"); + assertThat(map.get("Rod")).isEqualTo("Kerry"); + assertThat(map.get("Juergen")).isEqualTo("Eva"); + } + + @Test + public void mergeMapWithInnerBeanAsMapEntryValueInConstructor() throws Exception { + TestBean bean = (TestBean) this.beanFactory.getBean("childWithMapOfRefsInConstructor"); + Map map = bean.getSomeMap(); + assertThat(map).isNotNull(); + assertThat(map.size()).isEqualTo(2); + assertThat(map.get("Rob")).isNotNull(); + boolean condition = map.get("Rob") instanceof TestBean; + assertThat(condition).isTrue(); + assertThat(((TestBean) map.get("Rob")).getName()).isEqualTo("Sally"); + } + + @Test + public void mergePropertiesInConstructor() throws Exception { + TestBean bean = (TestBean) this.beanFactory.getBean("childWithPropsInConstructor"); + Properties props = bean.getSomeProperties(); + assertThat(props.size()).as("Incorrect size").isEqualTo(3); + assertThat(props.getProperty("Rob")).isEqualTo("Sally"); + assertThat(props.getProperty("Rod")).isEqualTo("Kerry"); + assertThat(props.getProperty("Juergen")).isEqualTo("Eva"); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/CollectionsWithDefaultTypesTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/CollectionsWithDefaultTypesTests.java new file mode 100644 index 0000000..8a67fea --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/CollectionsWithDefaultTypesTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + * @author Juergen Hoeller + */ +public class CollectionsWithDefaultTypesTests { + + private final DefaultListableBeanFactory beanFactory; + + public CollectionsWithDefaultTypesTests() { + this.beanFactory = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(this.beanFactory).loadBeanDefinitions( + new ClassPathResource("collectionsWithDefaultTypes.xml", getClass())); + } + + @Test + public void testListHasDefaultType() throws Exception { + TestBean bean = (TestBean) this.beanFactory.getBean("testBean"); + for (Object o : bean.getSomeList()) { + assertThat(o.getClass()).as("Value type is incorrect").isEqualTo(Integer.class); + } + } + + @Test + public void testSetHasDefaultType() throws Exception { + TestBean bean = (TestBean) this.beanFactory.getBean("testBean"); + for (Object o : bean.getSomeSet()) { + assertThat(o.getClass()).as("Value type is incorrect").isEqualTo(Integer.class); + } + } + + @Test + public void testMapHasDefaultKeyAndValueType() throws Exception { + TestBean bean = (TestBean) this.beanFactory.getBean("testBean"); + assertMap(bean.getSomeMap()); + } + + @Test + public void testMapWithNestedElementsHasDefaultKeyAndValueType() throws Exception { + TestBean bean = (TestBean) this.beanFactory.getBean("testBean2"); + assertMap(bean.getSomeMap()); + } + + @SuppressWarnings("rawtypes") + private void assertMap(Map map) { + for (Map.Entry entry : map.entrySet()) { + assertThat(entry.getKey().getClass()).as("Key type is incorrect").isEqualTo(Integer.class); + assertThat(entry.getValue().getClass()).as("Value type is incorrect").isEqualTo(Boolean.class); + } + } + + @Test + @SuppressWarnings("rawtypes") + public void testBuildCollectionFromMixtureOfReferencesAndValues() throws Exception { + MixedCollectionBean jumble = (MixedCollectionBean) this.beanFactory.getBean("jumble"); + assertThat(jumble.getJumble().size() == 3).as("Expected 3 elements, not " + jumble.getJumble().size()).isTrue(); + List l = (List) jumble.getJumble(); + assertThat(l.get(0).equals("literal")).isTrue(); + Integer[] array1 = (Integer[]) l.get(1); + assertThat(array1[0].equals(2)).isTrue(); + assertThat(array1[1].equals(4)).isTrue(); + int[] array2 = (int[]) l.get(2); + assertThat(array2[0] == 3).isTrue(); + assertThat(array2[1] == 5).isTrue(); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/ConstructorDependenciesBean.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/ConstructorDependenciesBean.java new file mode 100644 index 0000000..1d3c628 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/ConstructorDependenciesBean.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import java.io.Serializable; + +import org.springframework.beans.testfixture.beans.IndexedTestBean; +import org.springframework.beans.testfixture.beans.TestBean; + +/** + * Simple bean used to check constructor dependency checking. + * + * @author Juergen Hoeller + * @since 09.11.2003 + */ +@SuppressWarnings("serial") +public class ConstructorDependenciesBean implements Serializable { + + private int age; + + private String name; + + private TestBean spouse1; + + private TestBean spouse2; + + private IndexedTestBean other; + + public ConstructorDependenciesBean(int age) { + this.age = age; + } + + public ConstructorDependenciesBean(String name) { + this.name = name; + } + + public ConstructorDependenciesBean(TestBean spouse1) { + this.spouse1 = spouse1; + } + + public ConstructorDependenciesBean(TestBean spouse1, TestBean spouse2) { + this.spouse1 = spouse1; + this.spouse2 = spouse2; + } + + public ConstructorDependenciesBean(TestBean spouse1, TestBean spouse2, int age) { + this.spouse1 = spouse1; + this.spouse2 = spouse2; + this.age = age; + } + + public ConstructorDependenciesBean(TestBean spouse1, TestBean spouse2, IndexedTestBean other) { + this.spouse1 = spouse1; + this.spouse2 = spouse2; + this.other = other; + } + + public int getAge() { + return age; + } + + public String getName() { + return name; + } + + public TestBean getSpouse1() { + return spouse1; + } + + public TestBean getSpouse2() { + return spouse2; + } + + public IndexedTestBean getOther() { + return other; + } + + public void setAge(int age) { + this.age = age; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/CountingFactory.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/CountingFactory.java new file mode 100644 index 0000000..21ccf61 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/CountingFactory.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.testfixture.beans.TestBean; + +/** + * @author Juergen Hoeller + */ +public class CountingFactory implements FactoryBean { + + private static int factoryBeanInstanceCount = 0; + + + /** + * Clear static state. + */ + public static void reset() { + factoryBeanInstanceCount = 0; + } + + public static int getFactoryBeanInstanceCount() { + return factoryBeanInstanceCount; + } + + + public CountingFactory() { + factoryBeanInstanceCount++; + } + + public void setTestBean(TestBean tb) { + if (tb.getSpouse() == null) { + throw new IllegalStateException("TestBean needs to have spouse"); + } + } + + + @Override + public String getObject() { + return "myString"; + } + + @Override + public Class getObjectType() { + return String.class; + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/DefaultLifecycleMethodsTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/DefaultLifecycleMethodsTests.java new file mode 100644 index 0000000..0c6f7f8 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/DefaultLifecycleMethodsTests.java @@ -0,0 +1,148 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + * @author Juergen Hoeller + */ +public class DefaultLifecycleMethodsTests { + + private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + + @BeforeEach + public void setup() throws Exception { + new XmlBeanDefinitionReader(this.beanFactory).loadBeanDefinitions( + new ClassPathResource("defaultLifecycleMethods.xml", getClass())); + } + + + @Test + public void lifecycleMethodsInvoked() { + LifecycleAwareBean bean = (LifecycleAwareBean) this.beanFactory.getBean("lifecycleAware"); + assertThat(bean.isInitCalled()).as("Bean not initialized").isTrue(); + assertThat(bean.isCustomInitCalled()).as("Custom init method called incorrectly").isFalse(); + assertThat(bean.isDestroyCalled()).as("Bean destroyed too early").isFalse(); + this.beanFactory.destroySingletons(); + assertThat(bean.isDestroyCalled()).as("Bean not destroyed").isTrue(); + assertThat(bean.isCustomDestroyCalled()).as("Custom destroy method called incorrectly").isFalse(); + } + + @Test + public void lifecycleMethodsDisabled() throws Exception { + LifecycleAwareBean bean = (LifecycleAwareBean) this.beanFactory.getBean("lifecycleMethodsDisabled"); + assertThat(bean.isInitCalled()).as("Bean init method called incorrectly").isFalse(); + assertThat(bean.isCustomInitCalled()).as("Custom init method called incorrectly").isFalse(); + this.beanFactory.destroySingletons(); + assertThat(bean.isDestroyCalled()).as("Bean destroy method called incorrectly").isFalse(); + assertThat(bean.isCustomDestroyCalled()).as("Custom destroy method called incorrectly").isFalse(); + } + + @Test + public void ignoreDefaultLifecycleMethods() throws Exception { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource( + "ignoreDefaultLifecycleMethods.xml", getClass())); + bf.preInstantiateSingletons(); + bf.destroySingletons(); + } + + @Test + public void overrideDefaultLifecycleMethods() throws Exception { + LifecycleAwareBean bean = (LifecycleAwareBean) this.beanFactory.getBean("overrideLifecycleMethods"); + assertThat(bean.isInitCalled()).as("Default init method called incorrectly").isFalse(); + assertThat(bean.isCustomInitCalled()).as("Custom init method not called").isTrue(); + this.beanFactory.destroySingletons(); + assertThat(bean.isDestroyCalled()).as("Default destroy method called incorrectly").isFalse(); + assertThat(bean.isCustomDestroyCalled()).as("Custom destroy method not called").isTrue(); + } + + @Test + public void childWithDefaultLifecycleMethods() throws Exception { + LifecycleAwareBean bean = (LifecycleAwareBean) this.beanFactory.getBean("childWithDefaultLifecycleMethods"); + assertThat(bean.isInitCalled()).as("Bean not initialized").isTrue(); + assertThat(bean.isCustomInitCalled()).as("Custom init method called incorrectly").isFalse(); + assertThat(bean.isDestroyCalled()).as("Bean destroyed too early").isFalse(); + this.beanFactory.destroySingletons(); + assertThat(bean.isDestroyCalled()).as("Bean not destroyed").isTrue(); + assertThat(bean.isCustomDestroyCalled()).as("Custom destroy method called incorrectly").isFalse(); + } + + @Test + public void childWithLifecycleMethodsDisabled() throws Exception { + LifecycleAwareBean bean = (LifecycleAwareBean) this.beanFactory.getBean("childWithLifecycleMethodsDisabled"); + assertThat(bean.isInitCalled()).as("Bean init method called incorrectly").isFalse(); + assertThat(bean.isCustomInitCalled()).as("Custom init method called incorrectly").isFalse(); + this.beanFactory.destroySingletons(); + assertThat(bean.isDestroyCalled()).as("Bean destroy method called incorrectly").isFalse(); + assertThat(bean.isCustomDestroyCalled()).as("Custom destroy method called incorrectly").isFalse(); + } + + + public static class LifecycleAwareBean { + + private boolean initCalled; + + private boolean destroyCalled; + + private boolean customInitCalled; + + private boolean customDestroyCalled; + + public void init() { + this.initCalled = true; + } + + public void destroy() { + this.destroyCalled = true; + } + + public void customInit() { + this.customInitCalled = true; + } + + public void customDestroy() { + this.customDestroyCalled = true; + } + + public boolean isInitCalled() { + return initCalled; + } + + public boolean isDestroyCalled() { + return destroyCalled; + } + + public boolean isCustomInitCalled() { + return customInitCalled; + } + + public boolean isCustomDestroyCalled() { + return customDestroyCalled; + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/DelegatingEntityResolverTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/DelegatingEntityResolverTests.java new file mode 100644 index 0000000..6e05b66 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/DelegatingEntityResolverTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.junit.jupiter.api.Test; +import org.xml.sax.EntityResolver; +import org.xml.sax.InputSource; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Unit tests for the {@link DelegatingEntityResolver} class. + * + * @author Rick Evans + * @author Chris Beams + */ +public class DelegatingEntityResolverTests { + + @Test + public void testCtorWhereDtdEntityResolverIsNull() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new DelegatingEntityResolver(null, new NoOpEntityResolver())); + } + + @Test + public void testCtorWhereSchemaEntityResolverIsNull() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new DelegatingEntityResolver(new NoOpEntityResolver(), null)); + } + + @Test + public void testCtorWhereEntityResolversAreBothNull() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new DelegatingEntityResolver(null, null)); + } + + + private static final class NoOpEntityResolver implements EntityResolver { + @Override + public InputSource resolveEntity(String publicId, String systemId) { + return null; + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/DummyReferencer.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/DummyReferencer.java new file mode 100644 index 0000000..10bf54f --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/DummyReferencer.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + + +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.beans.testfixture.beans.factory.DummyFactory; + +/** + * @author Juergen Hoeller + * @since 21.07.2003 + */ +public class DummyReferencer { + + private TestBean testBean1; + + private TestBean testBean2; + + private DummyFactory dummyFactory; + + + public DummyReferencer() { + } + + public DummyReferencer(DummyFactory dummyFactory) { + this.dummyFactory = dummyFactory; + } + + public void setDummyFactory(DummyFactory dummyFactory) { + this.dummyFactory = dummyFactory; + } + + public DummyFactory getDummyFactory() { + return dummyFactory; + } + + public void setTestBean1(TestBean testBean1) { + this.testBean1 = testBean1; + } + + public TestBean getTestBean1() { + return testBean1; + } + + public void setTestBean2(TestBean testBean2) { + this.testBean2 = testBean2; + } + + public TestBean getTestBean2() { + return testBean2; + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/DuplicateBeanIdTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/DuplicateBeanIdTests.java new file mode 100644 index 0000000..c520014 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/DuplicateBeanIdTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + + +/** + * With Spring 3.1, bean id attributes (and all other id attributes across the + * core schemas) are no longer typed as xsd:id, but as xsd:string. This allows + * for using the same bean id within nested <beans> elements. + * + * Duplicate ids *within the same level of nesting* will still be treated as an + * error through the ProblemReporter, as this could never be an intended/valid + * situation. + * + * @author Chris Beams + * @since 3.1 + * @see org.springframework.beans.factory.xml.XmlBeanFactoryTests#withDuplicateName + * @see org.springframework.beans.factory.xml.XmlBeanFactoryTests#withDuplicateNameInAlias + */ +public class DuplicateBeanIdTests { + + @Test + public void duplicateBeanIdsWithinSameNestingLevelRaisesError() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(bf); + assertThatExceptionOfType(Exception.class).as("duplicate ids in same nesting level").isThrownBy(() -> + reader.loadBeanDefinitions(new ClassPathResource("DuplicateBeanIdTests-sameLevel-context.xml", this.getClass()))); + } + + @Test + public void duplicateBeanIdsAcrossNestingLevels() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(bf); + reader.loadBeanDefinitions(new ClassPathResource("DuplicateBeanIdTests-multiLevel-context.xml", this.getClass())); + TestBean testBean = bf.getBean(TestBean.class); // there should be only one + assertThat(testBean.getName()).isEqualTo("nested"); + } +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/EventPublicationTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/EventPublicationTests.java new file mode 100644 index 0000000..29e2561 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/EventPublicationTests.java @@ -0,0 +1,130 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.TypedStringValue; +import org.springframework.beans.factory.parsing.AliasDefinition; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.parsing.ComponentDefinition; +import org.springframework.beans.factory.parsing.ImportDefinition; +import org.springframework.beans.factory.parsing.PassThroughSourceExtractor; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.testfixture.beans.CollectingReaderEventListener; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + * @author Juergen Hoeller + */ +@SuppressWarnings("rawtypes") +public class EventPublicationTests { + + private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + private final CollectingReaderEventListener eventListener = new CollectingReaderEventListener(); + + + + @BeforeEach + public void setUp() throws Exception { + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this.beanFactory); + reader.setEventListener(this.eventListener); + reader.setSourceExtractor(new PassThroughSourceExtractor()); + reader.loadBeanDefinitions(new ClassPathResource("beanEvents.xml", getClass())); + } + + @Test + public void defaultsEventReceived() throws Exception { + List defaultsList = this.eventListener.getDefaults(); + boolean condition2 = !defaultsList.isEmpty(); + assertThat(condition2).isTrue(); + boolean condition1 = defaultsList.get(0) instanceof DocumentDefaultsDefinition; + assertThat(condition1).isTrue(); + DocumentDefaultsDefinition defaults = (DocumentDefaultsDefinition) defaultsList.get(0); + assertThat(defaults.getLazyInit()).isEqualTo("true"); + assertThat(defaults.getAutowire()).isEqualTo("constructor"); + assertThat(defaults.getInitMethod()).isEqualTo("myInit"); + assertThat(defaults.getDestroyMethod()).isEqualTo("myDestroy"); + assertThat(defaults.getMerge()).isEqualTo("true"); + boolean condition = defaults.getSource() instanceof Element; + assertThat(condition).isTrue(); + } + + @Test + public void beanEventReceived() throws Exception { + ComponentDefinition componentDefinition1 = this.eventListener.getComponentDefinition("testBean"); + boolean condition3 = componentDefinition1 instanceof BeanComponentDefinition; + assertThat(condition3).isTrue(); + assertThat(componentDefinition1.getBeanDefinitions().length).isEqualTo(1); + BeanDefinition beanDefinition1 = componentDefinition1.getBeanDefinitions()[0]; + assertThat(beanDefinition1.getConstructorArgumentValues().getGenericArgumentValue(String.class).getValue()).isEqualTo(new TypedStringValue("Rob Harrop")); + assertThat(componentDefinition1.getBeanReferences().length).isEqualTo(1); + assertThat(componentDefinition1.getBeanReferences()[0].getBeanName()).isEqualTo("testBean2"); + assertThat(componentDefinition1.getInnerBeanDefinitions().length).isEqualTo(1); + BeanDefinition innerBd1 = componentDefinition1.getInnerBeanDefinitions()[0]; + assertThat(innerBd1.getConstructorArgumentValues().getGenericArgumentValue(String.class).getValue()).isEqualTo(new TypedStringValue("ACME")); + boolean condition2 = componentDefinition1.getSource() instanceof Element; + assertThat(condition2).isTrue(); + + ComponentDefinition componentDefinition2 = this.eventListener.getComponentDefinition("testBean2"); + boolean condition1 = componentDefinition2 instanceof BeanComponentDefinition; + assertThat(condition1).isTrue(); + assertThat(componentDefinition1.getBeanDefinitions().length).isEqualTo(1); + BeanDefinition beanDefinition2 = componentDefinition2.getBeanDefinitions()[0]; + assertThat(beanDefinition2.getPropertyValues().getPropertyValue("name").getValue()).isEqualTo(new TypedStringValue("Juergen Hoeller")); + assertThat(componentDefinition2.getBeanReferences().length).isEqualTo(0); + assertThat(componentDefinition2.getInnerBeanDefinitions().length).isEqualTo(1); + BeanDefinition innerBd2 = componentDefinition2.getInnerBeanDefinitions()[0]; + assertThat(innerBd2.getPropertyValues().getPropertyValue("name").getValue()).isEqualTo(new TypedStringValue("Eva Schallmeiner")); + boolean condition = componentDefinition2.getSource() instanceof Element; + assertThat(condition).isTrue(); + } + + @Test + public void aliasEventReceived() throws Exception { + List aliases = this.eventListener.getAliases("testBean"); + assertThat(aliases.size()).isEqualTo(2); + AliasDefinition aliasDefinition1 = (AliasDefinition) aliases.get(0); + assertThat(aliasDefinition1.getAlias()).isEqualTo("testBeanAlias1"); + boolean condition1 = aliasDefinition1.getSource() instanceof Element; + assertThat(condition1).isTrue(); + AliasDefinition aliasDefinition2 = (AliasDefinition) aliases.get(1); + assertThat(aliasDefinition2.getAlias()).isEqualTo("testBeanAlias2"); + boolean condition = aliasDefinition2.getSource() instanceof Element; + assertThat(condition).isTrue(); + } + + @Test + public void importEventReceived() throws Exception { + List imports = this.eventListener.getImports(); + assertThat(imports.size()).isEqualTo(1); + ImportDefinition importDefinition = (ImportDefinition) imports.get(0); + assertThat(importDefinition.getImportedResource()).isEqualTo("beanEventsImported.xml"); + boolean condition = importDefinition.getSource() instanceof Element; + assertThat(condition).isTrue(); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/FactoryMethodTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/FactoryMethodTests.java new file mode 100644 index 0000000..d2f26ee --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/FactoryMethodTests.java @@ -0,0 +1,389 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.testfixture.beans.FactoryMethods; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Juergen Hoeller + * @author Chris Beams + */ +public class FactoryMethodTests { + + @Test + public void testFactoryMethodsSingletonOnTargetClass() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); + + TestBean tb = (TestBean) xbf.getBean("defaultTestBean"); + assertThat(tb.getName()).isEqualTo("defaultInstance"); + assertThat(tb.getAge()).isEqualTo(1); + + FactoryMethods fm = (FactoryMethods) xbf.getBean("default"); + assertThat(fm.getNum()).isEqualTo(0); + assertThat(fm.getName()).isEqualTo("default"); + assertThat(fm.getTestBean().getName()).isEqualTo("defaultInstance"); + assertThat(fm.getStringValue()).isEqualTo("setterString"); + + fm = (FactoryMethods) xbf.getBean("testBeanOnly"); + assertThat(fm.getNum()).isEqualTo(0); + assertThat(fm.getName()).isEqualTo("default"); + // This comes from the test bean + assertThat(fm.getTestBean().getName()).isEqualTo("Juergen"); + + fm = (FactoryMethods) xbf.getBean("full"); + assertThat(fm.getNum()).isEqualTo(27); + assertThat(fm.getName()).isEqualTo("gotcha"); + assertThat(fm.getTestBean().getName()).isEqualTo("Juergen"); + + FactoryMethods fm2 = (FactoryMethods) xbf.getBean("full"); + assertThat(fm2).isSameAs(fm); + + xbf.destroySingletons(); + assertThat(tb.wasDestroyed()).isTrue(); + } + + @Test + public void testFactoryMethodsWithInvalidDestroyMethod() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + xbf.getBean("defaultTestBeanWithInvalidDestroyMethod")); + } + + @Test + public void testFactoryMethodsWithNullInstance() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); + + assertThat(xbf.getBean("null").toString()).isEqualTo("null"); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + xbf.getBean("nullWithProperty")); + } + + @Test + public void testFactoryMethodsWithNullValue() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); + + FactoryMethods fm = (FactoryMethods) xbf.getBean("fullWithNull"); + assertThat(fm.getNum()).isEqualTo(27); + assertThat(fm.getName()).isEqualTo(null); + assertThat(fm.getTestBean().getName()).isEqualTo("Juergen"); + + fm = (FactoryMethods) xbf.getBean("fullWithGenericNull"); + assertThat(fm.getNum()).isEqualTo(27); + assertThat(fm.getName()).isEqualTo(null); + assertThat(fm.getTestBean().getName()).isEqualTo("Juergen"); + + fm = (FactoryMethods) xbf.getBean("fullWithNamedNull"); + assertThat(fm.getNum()).isEqualTo(27); + assertThat(fm.getName()).isEqualTo(null); + assertThat(fm.getTestBean().getName()).isEqualTo("Juergen"); + } + + @Test + public void testFactoryMethodsWithAutowire() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); + + FactoryMethods fm = (FactoryMethods) xbf.getBean("fullWithAutowire"); + assertThat(fm.getNum()).isEqualTo(27); + assertThat(fm.getName()).isEqualTo("gotchaAutowired"); + assertThat(fm.getTestBean().getName()).isEqualTo("Juergen"); + } + + @Test + public void testProtectedFactoryMethod() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); + + TestBean tb = (TestBean) xbf.getBean("defaultTestBean.protected"); + assertThat(tb.getAge()).isEqualTo(1); + } + + @Test + public void testPrivateFactoryMethod() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); + + TestBean tb = (TestBean) xbf.getBean("defaultTestBean.private"); + assertThat(tb.getAge()).isEqualTo(1); + } + + @Test + public void testFactoryMethodsPrototypeOnTargetClass() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); + FactoryMethods fm = (FactoryMethods) xbf.getBean("defaultPrototype"); + FactoryMethods fm2 = (FactoryMethods) xbf.getBean("defaultPrototype"); + assertThat(fm.getNum()).isEqualTo(0); + assertThat(fm.getName()).isEqualTo("default"); + assertThat(fm.getTestBean().getName()).isEqualTo("defaultInstance"); + assertThat(fm.getStringValue()).isEqualTo("setterString"); + assertThat(fm2.getNum()).isEqualTo(fm.getNum()); + assertThat(fm2.getStringValue()).isEqualTo(fm.getStringValue()); + // The TestBean is created separately for each bean + assertThat(fm2.getTestBean()).isNotSameAs(fm.getTestBean()); + assertThat(fm2).isNotSameAs(fm); + + fm = (FactoryMethods) xbf.getBean("testBeanOnlyPrototype"); + fm2 = (FactoryMethods) xbf.getBean("testBeanOnlyPrototype"); + assertThat(fm.getNum()).isEqualTo(0); + assertThat(fm.getName()).isEqualTo("default"); + // This comes from the test bean + assertThat(fm.getTestBean().getName()).isEqualTo("Juergen"); + assertThat(fm2.getNum()).isEqualTo(fm.getNum()); + assertThat(fm2.getStringValue()).isEqualTo(fm.getStringValue()); + // The TestBean reference is resolved to a prototype in the factory + assertThat(fm2.getTestBean()).isSameAs(fm.getTestBean()); + assertThat(fm2).isNotSameAs(fm); + + fm = (FactoryMethods) xbf.getBean("fullPrototype"); + fm2 = (FactoryMethods) xbf.getBean("fullPrototype"); + assertThat(fm.getNum()).isEqualTo(27); + assertThat(fm.getName()).isEqualTo("gotcha"); + assertThat(fm.getTestBean().getName()).isEqualTo("Juergen"); + assertThat(fm2.getNum()).isEqualTo(fm.getNum()); + assertThat(fm2.getStringValue()).isEqualTo(fm.getStringValue()); + // The TestBean reference is resolved to a prototype in the factory + assertThat(fm2.getTestBean()).isSameAs(fm.getTestBean()); + assertThat(fm2).isNotSameAs(fm); + } + + /** + * Tests where the static factory method is on a different class. + */ + @Test + public void testFactoryMethodsOnExternalClass() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); + + assertThat(xbf.getType("externalFactoryMethodWithoutArgs")).isEqualTo(TestBean.class); + assertThat(xbf.getType("externalFactoryMethodWithArgs")).isEqualTo(TestBean.class); + String[] names = xbf.getBeanNamesForType(TestBean.class); + assertThat(Arrays.asList(names).contains("externalFactoryMethodWithoutArgs")).isTrue(); + assertThat(Arrays.asList(names).contains("externalFactoryMethodWithArgs")).isTrue(); + + TestBean tb = (TestBean) xbf.getBean("externalFactoryMethodWithoutArgs"); + assertThat(tb.getAge()).isEqualTo(2); + assertThat(tb.getName()).isEqualTo("Tristan"); + tb = (TestBean) xbf.getBean("externalFactoryMethodWithArgs"); + assertThat(tb.getAge()).isEqualTo(33); + assertThat(tb.getName()).isEqualTo("Rod"); + + assertThat(xbf.getType("externalFactoryMethodWithoutArgs")).isEqualTo(TestBean.class); + assertThat(xbf.getType("externalFactoryMethodWithArgs")).isEqualTo(TestBean.class); + names = xbf.getBeanNamesForType(TestBean.class); + assertThat(Arrays.asList(names).contains("externalFactoryMethodWithoutArgs")).isTrue(); + assertThat(Arrays.asList(names).contains("externalFactoryMethodWithArgs")).isTrue(); + } + + @Test + public void testInstanceFactoryMethodWithoutArgs() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); + + InstanceFactory.count = 0; + xbf.preInstantiateSingletons(); + assertThat(InstanceFactory.count).isEqualTo(1); + FactoryMethods fm = (FactoryMethods) xbf.getBean("instanceFactoryMethodWithoutArgs"); + assertThat(fm.getTestBean().getName()).isEqualTo("instanceFactory"); + assertThat(InstanceFactory.count).isEqualTo(1); + FactoryMethods fm2 = (FactoryMethods) xbf.getBean("instanceFactoryMethodWithoutArgs"); + assertThat(fm2.getTestBean().getName()).isEqualTo("instanceFactory"); + assertThat(fm).isSameAs(fm2); + assertThat(InstanceFactory.count).isEqualTo(1); + } + + @Test + public void testFactoryMethodNoMatchingStaticMethod() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); + assertThatExceptionOfType(BeanCreationException.class).as("No static method matched").isThrownBy(() -> + xbf.getBean("noMatchPrototype")); + } + + @Test + public void testNonExistingFactoryMethod() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + xbf.getBean("invalidPrototype")) + .withMessageContaining("nonExisting(TestBean)"); + } + + @Test + public void testFactoryMethodArgumentsForNonExistingMethod() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + xbf.getBean("invalidPrototype", new TestBean())) + .withMessageContaining("nonExisting(TestBean)"); + } + + @Test + public void testCanSpecifyFactoryMethodArgumentsOnFactoryMethodPrototype() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); + TestBean tbArg = new TestBean(); + tbArg.setName("arg1"); + TestBean tbArg2 = new TestBean(); + tbArg2.setName("arg2"); + + FactoryMethods fm1 = (FactoryMethods) xbf.getBean("testBeanOnlyPrototype", tbArg); + assertThat(fm1.getNum()).isEqualTo(0); + assertThat(fm1.getName()).isEqualTo("default"); + // This comes from the test bean + assertThat(fm1.getTestBean().getName()).isEqualTo("arg1"); + + FactoryMethods fm2 = (FactoryMethods) xbf.getBean("testBeanOnlyPrototype", tbArg2); + assertThat(fm2.getTestBean().getName()).isEqualTo("arg2"); + assertThat(fm2.getNum()).isEqualTo(fm1.getNum()); + assertThat("testBeanOnlyPrototypeDISetterString").isEqualTo(fm2.getStringValue()); + assertThat(fm2.getStringValue()).isEqualTo(fm2.getStringValue()); + // The TestBean reference is resolved to a prototype in the factory + assertThat(fm2.getTestBean()).isSameAs(fm2.getTestBean()); + assertThat(fm2).isNotSameAs(fm1); + + FactoryMethods fm3 = (FactoryMethods) xbf.getBean("testBeanOnlyPrototype", tbArg2, 1, "myName"); + assertThat(fm3.getNum()).isEqualTo(1); + assertThat(fm3.getName()).isEqualTo("myName"); + assertThat(fm3.getTestBean().getName()).isEqualTo("arg2"); + + FactoryMethods fm4 = (FactoryMethods) xbf.getBean("testBeanOnlyPrototype", tbArg); + assertThat(fm4.getNum()).isEqualTo(0); + assertThat(fm4.getName()).isEqualTo("default"); + assertThat(fm4.getTestBean().getName()).isEqualTo("arg1"); + } + + @Test + public void testCanSpecifyFactoryMethodArgumentsOnSingleton() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); + + // First getBean call triggers actual creation of the singleton bean + TestBean tb = new TestBean(); + FactoryMethods fm1 = (FactoryMethods) xbf.getBean("testBeanOnly", tb); + assertThat(fm1.getTestBean()).isSameAs(tb); + FactoryMethods fm2 = (FactoryMethods) xbf.getBean("testBeanOnly", new TestBean()); + assertThat(fm2).isSameAs(fm1); + assertThat(fm2.getTestBean()).isSameAs(tb); + } + + @Test + public void testCannotSpecifyFactoryMethodArgumentsOnSingletonAfterCreation() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); + + // First getBean call triggers actual creation of the singleton bean + FactoryMethods fm1 = (FactoryMethods) xbf.getBean("testBeanOnly"); + TestBean tb = fm1.getTestBean(); + FactoryMethods fm2 = (FactoryMethods) xbf.getBean("testBeanOnly", new TestBean()); + assertThat(fm2).isSameAs(fm1); + assertThat(fm2.getTestBean()).isSameAs(tb); + } + + @Test + public void testFactoryMethodWithDifferentReturnType() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); + + // Check that listInstance is not considered a bean of type FactoryMethods. + assertThat(List.class.isAssignableFrom(xbf.getType("listInstance"))).isTrue(); + String[] names = xbf.getBeanNamesForType(FactoryMethods.class); + boolean condition1 = !Arrays.asList(names).contains("listInstance"); + assertThat(condition1).isTrue(); + names = xbf.getBeanNamesForType(List.class); + assertThat(Arrays.asList(names).contains("listInstance")).isTrue(); + + xbf.preInstantiateSingletons(); + assertThat(List.class.isAssignableFrom(xbf.getType("listInstance"))).isTrue(); + names = xbf.getBeanNamesForType(FactoryMethods.class); + boolean condition = !Arrays.asList(names).contains("listInstance"); + assertThat(condition).isTrue(); + names = xbf.getBeanNamesForType(List.class); + assertThat(Arrays.asList(names).contains("listInstance")).isTrue(); + List list = (List) xbf.getBean("listInstance"); + assertThat(list).isEqualTo(Collections.EMPTY_LIST); + } + + @Test + public void testFactoryMethodForJavaMailSession() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.loadBeanDefinitions(new ClassPathResource("factory-methods.xml", getClass())); + + MailSession session = (MailSession) xbf.getBean("javaMailSession"); + assertThat(session.getProperty("mail.smtp.user")).isEqualTo("someuser"); + assertThat(session.getProperty("mail.smtp.password")).isEqualTo("somepw"); + } +} + + +class MailSession { + + private Properties props; + + private MailSession() { + } + + public void setProperties(Properties props) { + this.props = props; + } + + public static MailSession getDefaultInstance(Properties props) { + MailSession session = new MailSession(); + session.setProperties(props); + return session; + } + + public Object getProperty(String key) { + return this.props.get(key); + } +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/GeneratedNameBean.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/GeneratedNameBean.java new file mode 100644 index 0000000..51cb92c --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/GeneratedNameBean.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.springframework.beans.factory.BeanNameAware; + +/** + * @author Rob Harrop + */ +public class GeneratedNameBean implements BeanNameAware { + + private String beanName; + + private GeneratedNameBean child; + + @Override + public void setBeanName(String beanName) { + this.beanName = beanName; + } + + public String getBeanName() { + return beanName; + } + + public void setChild(GeneratedNameBean child) { + this.child = child; + } + + public GeneratedNameBean getChild() { + return child; + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/InstanceFactory.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/InstanceFactory.java new file mode 100644 index 0000000..af7f2f7 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/InstanceFactory.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.springframework.beans.testfixture.beans.FactoryMethods; +import org.springframework.beans.testfixture.beans.TestBean; + +/** + * Test class for Spring's ability to create objects using + * static factory methods, rather than constructors. + * + * @author Rod Johnson + */ +public class InstanceFactory { + + protected static int count = 0; + + private String factoryBeanProperty; + + public InstanceFactory() { + count++; + } + + public void setFactoryBeanProperty(String s) { + this.factoryBeanProperty = s; + } + + public String getFactoryBeanProperty() { + return this.factoryBeanProperty; + } + + public FactoryMethods defaultInstance() { + TestBean tb = new TestBean(); + tb.setName(this.factoryBeanProperty); + return FactoryMethods.newInstance(tb); + } + + /** + * Note that overloaded methods are supported. + */ + public FactoryMethods newInstance(TestBean tb) { + return FactoryMethods.newInstance(tb); + } + + public FactoryMethods newInstance(TestBean tb, int num, String name) { + return FactoryMethods.newInstance(tb, num, name); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/MetadataAttachmentTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/MetadataAttachmentTests.java new file mode 100644 index 0000000..1c79626 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/MetadataAttachmentTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.PropertyValue; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + */ +public class MetadataAttachmentTests { + + private DefaultListableBeanFactory beanFactory; + + + @BeforeEach + public void setUp() throws Exception { + this.beanFactory = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(this.beanFactory).loadBeanDefinitions( + new ClassPathResource("withMeta.xml", getClass())); + } + + @Test + public void metadataAttachment() throws Exception { + BeanDefinition beanDefinition1 = this.beanFactory.getMergedBeanDefinition("testBean1"); + assertThat(beanDefinition1.getAttribute("foo")).isEqualTo("bar"); + } + + @Test + public void metadataIsInherited() throws Exception { + BeanDefinition beanDefinition = this.beanFactory.getMergedBeanDefinition("testBean2"); + assertThat(beanDefinition.getAttribute("foo")).as("Metadata not inherited").isEqualTo("bar"); + assertThat(beanDefinition.getAttribute("abc")).as("Child metdata not attached").isEqualTo("123"); + } + + @Test + public void propertyMetadata() throws Exception { + BeanDefinition beanDefinition = this.beanFactory.getMergedBeanDefinition("testBean3"); + PropertyValue pv = beanDefinition.getPropertyValues().getPropertyValue("name"); + assertThat(pv.getAttribute("surname")).isEqualTo("Harrop"); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/MixedCollectionBean.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/MixedCollectionBean.java new file mode 100644 index 0000000..a3419e4 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/MixedCollectionBean.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2006 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import java.util.Collection; + +/** + * Bean that exposes a simple property that can be set + * to a mix of references and individual values. + * + * @author Rod Johnson + * @since 27.05.2003 + */ +public class MixedCollectionBean { + + private Collection jumble; + + + public void setJumble(Collection jumble) { + this.jumble = jumble; + } + + public Collection getJumble() { + return jumble; + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests.java new file mode 100644 index 0000000..cce3bb3 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests.java @@ -0,0 +1,190 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for propagating enclosing beans element defaults to nested beans elements. + * + * @author Chris Beams + */ +public class NestedBeansElementAttributeRecursionTests { + + @Test + public void defaultLazyInit() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions( + new ClassPathResource("NestedBeansElementAttributeRecursionTests-lazy-context.xml", this.getClass())); + + assertLazyInits(bf); + } + + @Test + public void defaultLazyInitWithNonValidatingParser() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(bf); + xmlBeanDefinitionReader.setValidating(false); + xmlBeanDefinitionReader.loadBeanDefinitions( + new ClassPathResource("NestedBeansElementAttributeRecursionTests-lazy-context.xml", this.getClass())); + + assertLazyInits(bf); + } + + private void assertLazyInits(DefaultListableBeanFactory bf) { + BeanDefinition foo = bf.getBeanDefinition("foo"); + BeanDefinition bar = bf.getBeanDefinition("bar"); + BeanDefinition baz = bf.getBeanDefinition("baz"); + BeanDefinition biz = bf.getBeanDefinition("biz"); + BeanDefinition buz = bf.getBeanDefinition("buz"); + + assertThat(foo.isLazyInit()).isFalse(); + assertThat(bar.isLazyInit()).isTrue(); + assertThat(baz.isLazyInit()).isFalse(); + assertThat(biz.isLazyInit()).isTrue(); + assertThat(buz.isLazyInit()).isTrue(); + } + + @Test + public void defaultMerge() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions( + new ClassPathResource("NestedBeansElementAttributeRecursionTests-merge-context.xml", this.getClass())); + + assertMerge(bf); + } + + @Test + public void defaultMergeWithNonValidatingParser() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(bf); + xmlBeanDefinitionReader.setValidating(false); + xmlBeanDefinitionReader.loadBeanDefinitions( + new ClassPathResource("NestedBeansElementAttributeRecursionTests-merge-context.xml", this.getClass())); + + assertMerge(bf); + } + + @SuppressWarnings("unchecked") + private void assertMerge(DefaultListableBeanFactory bf) { + TestBean topLevel = bf.getBean("topLevelConcreteTestBean", TestBean.class); + // has the concrete child bean values + assertThat((Iterable) topLevel.getSomeList()).contains("charlie", "delta"); + // but does not merge the parent values + assertThat((Iterable) topLevel.getSomeList()).doesNotContain("alpha", "bravo"); + + TestBean firstLevel = bf.getBean("firstLevelNestedTestBean", TestBean.class); + // merges all values + assertThat((Iterable) firstLevel.getSomeList()).contains( + "charlie", "delta", "echo", "foxtrot"); + + TestBean secondLevel = bf.getBean("secondLevelNestedTestBean", TestBean.class); + // merges all values + assertThat((Iterable)secondLevel.getSomeList()).contains( + "charlie", "delta", "echo", "foxtrot", "golf", "hotel"); + } + + @Test + public void defaultAutowireCandidates() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions( + new ClassPathResource("NestedBeansElementAttributeRecursionTests-autowire-candidates-context.xml", this.getClass())); + + assertAutowireCandidates(bf); + } + + @Test + public void defaultAutowireCandidatesWithNonValidatingParser() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(bf); + xmlBeanDefinitionReader.setValidating(false); + xmlBeanDefinitionReader.loadBeanDefinitions( + new ClassPathResource("NestedBeansElementAttributeRecursionTests-autowire-candidates-context.xml", this.getClass())); + + assertAutowireCandidates(bf); + } + + private void assertAutowireCandidates(DefaultListableBeanFactory bf) { + assertThat(bf.getBeanDefinition("fooService").isAutowireCandidate()).isTrue(); + assertThat(bf.getBeanDefinition("fooRepository").isAutowireCandidate()).isTrue(); + assertThat(bf.getBeanDefinition("other").isAutowireCandidate()).isFalse(); + + assertThat(bf.getBeanDefinition("barService").isAutowireCandidate()).isTrue(); + assertThat(bf.getBeanDefinition("fooController").isAutowireCandidate()).isFalse(); + + assertThat(bf.getBeanDefinition("bizRepository").isAutowireCandidate()).isTrue(); + assertThat(bf.getBeanDefinition("bizService").isAutowireCandidate()).isFalse(); + + assertThat(bf.getBeanDefinition("bazService").isAutowireCandidate()).isTrue(); + assertThat(bf.getBeanDefinition("random").isAutowireCandidate()).isFalse(); + assertThat(bf.getBeanDefinition("fooComponent").isAutowireCandidate()).isFalse(); + assertThat(bf.getBeanDefinition("fRepository").isAutowireCandidate()).isFalse(); + + assertThat(bf.getBeanDefinition("aComponent").isAutowireCandidate()).isTrue(); + assertThat(bf.getBeanDefinition("someService").isAutowireCandidate()).isFalse(); + } + + @Test + public void initMethod() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions( + new ClassPathResource("NestedBeansElementAttributeRecursionTests-init-destroy-context.xml", this.getClass())); + + InitDestroyBean beanA = bf.getBean("beanA", InitDestroyBean.class); + InitDestroyBean beanB = bf.getBean("beanB", InitDestroyBean.class); + InitDestroyBean beanC = bf.getBean("beanC", InitDestroyBean.class); + InitDestroyBean beanD = bf.getBean("beanD", InitDestroyBean.class); + + assertThat(beanA.initMethod1Called).isTrue(); + assertThat(beanB.initMethod2Called).isTrue(); + assertThat(beanC.initMethod3Called).isTrue(); + assertThat(beanD.initMethod2Called).isTrue(); + + bf.destroySingletons(); + + assertThat(beanA.destroyMethod1Called).isTrue(); + assertThat(beanB.destroyMethod2Called).isTrue(); + assertThat(beanC.destroyMethod3Called).isTrue(); + assertThat(beanD.destroyMethod2Called).isTrue(); + } + +} + +class InitDestroyBean { + boolean initMethod1Called; + boolean initMethod2Called; + boolean initMethod3Called; + + boolean destroyMethod1Called; + boolean destroyMethod2Called; + boolean destroyMethod3Called; + + void initMethod1() { this.initMethod1Called = true; } + void initMethod2() { this.initMethod2Called = true; } + void initMethod3() { this.initMethod3Called = true; } + + void destroyMethod1() { this.destroyMethod1Called = true; } + void destroyMethod2() { this.destroyMethod2Called = true; } + void destroyMethod3() { this.destroyMethod3Called = true; } +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/NestedBeansElementTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/NestedBeansElementTests.java new file mode 100644 index 0000000..4117c82 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/NestedBeansElementTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Tests for new nested beans element support in Spring XML + * + * @author Chris Beams + */ +public class NestedBeansElementTests { + private final Resource XML = + new ClassPathResource("NestedBeansElementTests-context.xml", this.getClass()); + + @Test + public void getBean_withoutActiveProfile() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions(XML); + + Object foo = bf.getBean("foo"); + assertThat(foo).isInstanceOf(String.class); + } + + @Test + public void getBean_withActiveProfile() { + ConfigurableEnvironment env = new StandardEnvironment(); + env.setActiveProfiles("dev"); + + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(bf); + reader.setEnvironment(env); + reader.loadBeanDefinitions(XML); + + bf.getBean("devOnlyBean"); // should not throw NSBDE + + Object foo = bf.getBean("foo"); + assertThat(foo).isInstanceOf(Integer.class); + + bf.getBean("devOnlyBean"); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests.java new file mode 100644 index 0000000..56795fe --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests.java @@ -0,0 +1,194 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.assertj.core.api.Condition; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests various combinations of profile declarations against various profile + * activation and profile default scenarios. + * + * @author Chris Beams + * @author Sam Brannen + * @since 3.1 + */ +public class ProfileXmlBeanDefinitionTests { + + private static final String PROD_ELIGIBLE_XML = "ProfileXmlBeanDefinitionTests-prodProfile.xml"; + private static final String DEV_ELIGIBLE_XML = "ProfileXmlBeanDefinitionTests-devProfile.xml"; + private static final String NOT_DEV_ELIGIBLE_XML = "ProfileXmlBeanDefinitionTests-notDevProfile.xml"; + private static final String ALL_ELIGIBLE_XML = "ProfileXmlBeanDefinitionTests-noProfile.xml"; + private static final String MULTI_ELIGIBLE_XML = "ProfileXmlBeanDefinitionTests-multiProfile.xml"; + private static final String MULTI_NEGATED_XML = "ProfileXmlBeanDefinitionTests-multiProfileNegated.xml"; + private static final String MULTI_NOT_DEV_ELIGIBLE_XML = "ProfileXmlBeanDefinitionTests-multiProfileNotDev.xml"; + private static final String MULTI_ELIGIBLE_SPACE_DELIMITED_XML = "ProfileXmlBeanDefinitionTests-spaceDelimitedProfile.xml"; + private static final String UNKNOWN_ELIGIBLE_XML = "ProfileXmlBeanDefinitionTests-unknownProfile.xml"; + private static final String DEFAULT_ELIGIBLE_XML = "ProfileXmlBeanDefinitionTests-defaultProfile.xml"; + private static final String CUSTOM_DEFAULT_ELIGIBLE_XML = "ProfileXmlBeanDefinitionTests-customDefaultProfile.xml"; + private static final String DEFAULT_AND_DEV_ELIGIBLE_XML = "ProfileXmlBeanDefinitionTests-defaultAndDevProfile.xml"; + + private static final String PROD_ACTIVE = "prod"; + private static final String DEV_ACTIVE = "dev"; + private static final String NULL_ACTIVE = null; + private static final String UNKNOWN_ACTIVE = "unknown"; + private static final String[] NONE_ACTIVE = new String[0]; + private static final String[] MULTI_ACTIVE = new String[] { PROD_ACTIVE, DEV_ACTIVE }; + + private static final String TARGET_BEAN = "foo"; + + @Test + public void testProfileValidation() { + assertThatIllegalArgumentException().isThrownBy(() -> + beanFactoryFor(PROD_ELIGIBLE_XML, NULL_ACTIVE)); + } + + @Test + public void testProfilePermutations() { + assertThat(beanFactoryFor(PROD_ELIGIBLE_XML, NONE_ACTIVE)).isNot(containingTarget()); + assertThat(beanFactoryFor(PROD_ELIGIBLE_XML, DEV_ACTIVE)).isNot(containingTarget()); + assertThat(beanFactoryFor(PROD_ELIGIBLE_XML, PROD_ACTIVE)).is(containingTarget()); + assertThat(beanFactoryFor(PROD_ELIGIBLE_XML, MULTI_ACTIVE)).is(containingTarget()); + + assertThat(beanFactoryFor(DEV_ELIGIBLE_XML, NONE_ACTIVE)).isNot(containingTarget()); + assertThat(beanFactoryFor(DEV_ELIGIBLE_XML, DEV_ACTIVE)).is(containingTarget()); + assertThat(beanFactoryFor(DEV_ELIGIBLE_XML, PROD_ACTIVE)).isNot(containingTarget()); + assertThat(beanFactoryFor(DEV_ELIGIBLE_XML, MULTI_ACTIVE)).is(containingTarget()); + + assertThat(beanFactoryFor(NOT_DEV_ELIGIBLE_XML, NONE_ACTIVE)).is(containingTarget()); + assertThat(beanFactoryFor(NOT_DEV_ELIGIBLE_XML, DEV_ACTIVE)).isNot(containingTarget()); + assertThat(beanFactoryFor(NOT_DEV_ELIGIBLE_XML, PROD_ACTIVE)).is(containingTarget()); + assertThat(beanFactoryFor(NOT_DEV_ELIGIBLE_XML, MULTI_ACTIVE)).isNot(containingTarget()); + + assertThat(beanFactoryFor(ALL_ELIGIBLE_XML, NONE_ACTIVE)).is(containingTarget()); + assertThat(beanFactoryFor(ALL_ELIGIBLE_XML, DEV_ACTIVE)).is(containingTarget()); + assertThat(beanFactoryFor(ALL_ELIGIBLE_XML, PROD_ACTIVE)).is(containingTarget()); + assertThat(beanFactoryFor(ALL_ELIGIBLE_XML, MULTI_ACTIVE)).is(containingTarget()); + + assertThat(beanFactoryFor(MULTI_ELIGIBLE_XML, NONE_ACTIVE)).isNot(containingTarget()); + assertThat(beanFactoryFor(MULTI_ELIGIBLE_XML, UNKNOWN_ACTIVE)).isNot(containingTarget()); + assertThat(beanFactoryFor(MULTI_ELIGIBLE_XML, DEV_ACTIVE)).is(containingTarget()); + assertThat(beanFactoryFor(MULTI_ELIGIBLE_XML, PROD_ACTIVE)).is(containingTarget()); + assertThat(beanFactoryFor(MULTI_ELIGIBLE_XML, MULTI_ACTIVE)).is(containingTarget()); + + assertThat(beanFactoryFor(MULTI_NEGATED_XML, NONE_ACTIVE)).is(containingTarget()); + assertThat(beanFactoryFor(MULTI_NEGATED_XML, UNKNOWN_ACTIVE)).is(containingTarget()); + assertThat(beanFactoryFor(MULTI_NEGATED_XML, DEV_ACTIVE)).is(containingTarget()); + assertThat(beanFactoryFor(MULTI_NEGATED_XML, PROD_ACTIVE)).is(containingTarget()); + assertThat(beanFactoryFor(MULTI_NEGATED_XML, MULTI_ACTIVE)).isNot(containingTarget()); + + assertThat(beanFactoryFor(MULTI_NOT_DEV_ELIGIBLE_XML, NONE_ACTIVE)).is(containingTarget()); + assertThat(beanFactoryFor(MULTI_NOT_DEV_ELIGIBLE_XML, UNKNOWN_ACTIVE)).is(containingTarget()); + assertThat(beanFactoryFor(MULTI_NOT_DEV_ELIGIBLE_XML, DEV_ACTIVE)).isNot(containingTarget()); + assertThat(beanFactoryFor(MULTI_NOT_DEV_ELIGIBLE_XML, PROD_ACTIVE)).is(containingTarget()); + assertThat(beanFactoryFor(MULTI_NOT_DEV_ELIGIBLE_XML, MULTI_ACTIVE)).is(containingTarget()); + + assertThat(beanFactoryFor(MULTI_ELIGIBLE_SPACE_DELIMITED_XML, NONE_ACTIVE)).isNot(containingTarget()); + assertThat(beanFactoryFor(MULTI_ELIGIBLE_SPACE_DELIMITED_XML, UNKNOWN_ACTIVE)).isNot(containingTarget()); + assertThat(beanFactoryFor(MULTI_ELIGIBLE_SPACE_DELIMITED_XML, DEV_ACTIVE)).is(containingTarget()); + assertThat(beanFactoryFor(MULTI_ELIGIBLE_SPACE_DELIMITED_XML, PROD_ACTIVE)).is(containingTarget()); + assertThat(beanFactoryFor(MULTI_ELIGIBLE_SPACE_DELIMITED_XML, MULTI_ACTIVE)).is(containingTarget()); + + assertThat(beanFactoryFor(UNKNOWN_ELIGIBLE_XML, MULTI_ACTIVE)).isNot(containingTarget()); + } + + @Test + public void testDefaultProfile() { + { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory); + ConfigurableEnvironment env = new StandardEnvironment(); + env.setDefaultProfiles("custom-default"); + reader.setEnvironment(env); + reader.loadBeanDefinitions(new ClassPathResource(DEFAULT_ELIGIBLE_XML, getClass())); + + assertThat(beanFactory).isNot(containingTarget()); + } + { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory); + ConfigurableEnvironment env = new StandardEnvironment(); + env.setDefaultProfiles("custom-default"); + reader.setEnvironment(env); + reader.loadBeanDefinitions(new ClassPathResource(CUSTOM_DEFAULT_ELIGIBLE_XML, getClass())); + + assertThat(beanFactory).is(containingTarget()); + } + } + + @Test + public void testDefaultAndNonDefaultProfile() { + assertThat(beanFactoryFor(DEFAULT_ELIGIBLE_XML, NONE_ACTIVE)).is(containingTarget()); + assertThat(beanFactoryFor(DEFAULT_ELIGIBLE_XML, "other")).isNot(containingTarget()); + + { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory); + ConfigurableEnvironment env = new StandardEnvironment(); + env.setActiveProfiles(DEV_ACTIVE); + env.setDefaultProfiles("default"); + reader.setEnvironment(env); + reader.loadBeanDefinitions(new ClassPathResource(DEFAULT_AND_DEV_ELIGIBLE_XML, getClass())); + assertThat(beanFactory).is(containingTarget()); + } + { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory); + ConfigurableEnvironment env = new StandardEnvironment(); + // env.setActiveProfiles(DEV_ACTIVE); + env.setDefaultProfiles("default"); + reader.setEnvironment(env); + reader.loadBeanDefinitions(new ClassPathResource(DEFAULT_AND_DEV_ELIGIBLE_XML, getClass())); + assertThat(beanFactory).is(containingTarget()); + } + { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory); + ConfigurableEnvironment env = new StandardEnvironment(); + // env.setActiveProfiles(DEV_ACTIVE); + //env.setDefaultProfiles("default"); + reader.setEnvironment(env); + reader.loadBeanDefinitions(new ClassPathResource(DEFAULT_AND_DEV_ELIGIBLE_XML, getClass())); + assertThat(beanFactory).is(containingTarget()); + } + } + + + private BeanDefinitionRegistry beanFactoryFor(String xmlName, String... activeProfiles) { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory); + StandardEnvironment env = new StandardEnvironment(); + env.setActiveProfiles(activeProfiles); + reader.setEnvironment(env); + reader.loadBeanDefinitions(new ClassPathResource(xmlName, getClass())); + return beanFactory; + } + + private Condition containingTarget() { + return new Condition<>(registry -> registry.containsBeanDefinition(TARGET_BEAN), "contains target"); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/ProtectedLifecycleBean.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/ProtectedLifecycleBean.java new file mode 100644 index 0000000..95aca60 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/ProtectedLifecycleBean.java @@ -0,0 +1,168 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.config.BeanPostProcessor; + +/** + * Simple test of BeanFactory initialization and lifecycle callbacks. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +class ProtectedLifecycleBean implements BeanNameAware, BeanFactoryAware, InitializingBean, DisposableBean { + + protected boolean initMethodDeclared = false; + + protected String beanName; + + protected BeanFactory owningFactory; + + protected boolean postProcessedBeforeInit; + + protected boolean inited; + + protected boolean initedViaDeclaredInitMethod; + + protected boolean postProcessedAfterInit; + + protected boolean destroyed; + + + public void setInitMethodDeclared(boolean initMethodDeclared) { + this.initMethodDeclared = initMethodDeclared; + } + + public boolean isInitMethodDeclared() { + return initMethodDeclared; + } + + @Override + public void setBeanName(String name) { + this.beanName = name; + } + + public String getBeanName() { + return beanName; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.owningFactory = beanFactory; + } + + public void postProcessBeforeInit() { + if (this.inited || this.initedViaDeclaredInitMethod) { + throw new RuntimeException("Factory called postProcessBeforeInit after afterPropertiesSet"); + } + if (this.postProcessedBeforeInit) { + throw new RuntimeException("Factory called postProcessBeforeInit twice"); + } + this.postProcessedBeforeInit = true; + } + + @Override + public void afterPropertiesSet() { + if (this.owningFactory == null) { + throw new RuntimeException("Factory didn't call setBeanFactory before afterPropertiesSet on lifecycle bean"); + } + if (!this.postProcessedBeforeInit) { + throw new RuntimeException("Factory didn't call postProcessBeforeInit before afterPropertiesSet on lifecycle bean"); + } + if (this.initedViaDeclaredInitMethod) { + throw new RuntimeException("Factory initialized via declared init method before initializing via afterPropertiesSet"); + } + if (this.inited) { + throw new RuntimeException("Factory called afterPropertiesSet twice"); + } + this.inited = true; + } + + public void declaredInitMethod() { + if (!this.inited) { + throw new RuntimeException("Factory didn't call afterPropertiesSet before declared init method"); + } + + if (this.initedViaDeclaredInitMethod) { + throw new RuntimeException("Factory called declared init method twice"); + } + this.initedViaDeclaredInitMethod = true; + } + + public void postProcessAfterInit() { + if (!this.inited) { + throw new RuntimeException("Factory called postProcessAfterInit before afterPropertiesSet"); + } + if (this.initMethodDeclared && !this.initedViaDeclaredInitMethod) { + throw new RuntimeException("Factory called postProcessAfterInit before calling declared init method"); + } + if (this.postProcessedAfterInit) { + throw new RuntimeException("Factory called postProcessAfterInit twice"); + } + this.postProcessedAfterInit = true; + } + + /** + * Dummy business method that will fail unless the factory + * managed the bean's lifecycle correctly + */ + public void businessMethod() { + if (!this.inited || (this.initMethodDeclared && !this.initedViaDeclaredInitMethod) || + !this.postProcessedAfterInit) { + throw new RuntimeException("Factory didn't initialize lifecycle object correctly"); + } + } + + @Override + public void destroy() { + if (this.destroyed) { + throw new IllegalStateException("Already destroyed"); + } + this.destroyed = true; + } + + public boolean isDestroyed() { + return destroyed; + } + + + public static class PostProcessor implements BeanPostProcessor { + + @Override + public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException { + if (bean instanceof ProtectedLifecycleBean) { + ((ProtectedLifecycleBean) bean).postProcessBeforeInit(); + } + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String name) throws BeansException { + if (bean instanceof ProtectedLifecycleBean) { + ((ProtectedLifecycleBean) bean).postProcessAfterInit(); + } + return bean; + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/SchemaValidationTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/SchemaValidationTests.java new file mode 100644 index 0000000..7019945 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/SchemaValidationTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.junit.jupiter.api.Test; +import org.xml.sax.SAXParseException; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Rob Harrop + */ +public class SchemaValidationTests { + + @Test + public void withAutodetection() throws Exception { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(bf); + assertThatExceptionOfType(BeansException.class).isThrownBy(() -> + reader.loadBeanDefinitions(new ClassPathResource("invalidPerSchema.xml", getClass()))) + .withCauseInstanceOf(SAXParseException.class); + } + + @Test + public void withExplicitValidationMode() throws Exception { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(bf); + reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_XSD); + assertThatExceptionOfType(BeansException.class).isThrownBy(() -> + reader.loadBeanDefinitions(new ClassPathResource("invalidPerSchema.xml", getClass()))) + .withCauseInstanceOf(SAXParseException.class); + } + + @Test + public void loadDefinitions() throws Exception { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(bf); + reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_XSD); + reader.loadBeanDefinitions(new ClassPathResource("schemaValidated.xml", getClass())); + + TestBean foo = (TestBean) bf.getBean("fooBean"); + assertThat(foo.getSpouse()).as("Spouse is null").isNotNull(); + assertThat(foo.getFriends().size()).as("Incorrect number of friends").isEqualTo(2); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/SimpleConstructorNamespaceHandlerTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/SimpleConstructorNamespaceHandlerTests.java new file mode 100644 index 0000000..24a9d43 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/SimpleConstructorNamespaceHandlerTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.testfixture.beans.DummyBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Costin Leau + */ +public class SimpleConstructorNamespaceHandlerTests { + + @Test + public void simpleValue() throws Exception { + DefaultListableBeanFactory beanFactory = createFactory("simpleConstructorNamespaceHandlerTests.xml"); + String name = "simple"; + // beanFactory.getBean("simple1", DummyBean.class); + DummyBean nameValue = beanFactory.getBean(name, DummyBean.class); + assertThat(nameValue.getValue()).isEqualTo("simple"); + } + + @Test + public void simpleRef() throws Exception { + DefaultListableBeanFactory beanFactory = createFactory("simpleConstructorNamespaceHandlerTests.xml"); + String name = "simple-ref"; + // beanFactory.getBean("name-value1", TestBean.class); + DummyBean nameValue = beanFactory.getBean(name, DummyBean.class); + assertThat(nameValue.getValue()).isEqualTo(beanFactory.getBean("name")); + } + + @Test + public void nameValue() throws Exception { + DefaultListableBeanFactory beanFactory = createFactory("simpleConstructorNamespaceHandlerTests.xml"); + String name = "name-value"; + // beanFactory.getBean("name-value1", TestBean.class); + TestBean nameValue = beanFactory.getBean(name, TestBean.class); + assertThat(nameValue.getName()).isEqualTo(name); + assertThat(nameValue.getAge()).isEqualTo(10); + } + + @Test + public void nameRef() throws Exception { + DefaultListableBeanFactory beanFactory = createFactory("simpleConstructorNamespaceHandlerTests.xml"); + TestBean nameValue = beanFactory.getBean("name-value", TestBean.class); + DummyBean nameRef = beanFactory.getBean("name-ref", DummyBean.class); + + assertThat(nameRef.getName()).isEqualTo("some-name"); + assertThat(nameRef.getSpouse()).isEqualTo(nameValue); + } + + @Test + public void typeIndexedValue() throws Exception { + DefaultListableBeanFactory beanFactory = createFactory("simpleConstructorNamespaceHandlerTests.xml"); + DummyBean typeRef = beanFactory.getBean("indexed-value", DummyBean.class); + + assertThat(typeRef.getName()).isEqualTo("at"); + assertThat(typeRef.getValue()).isEqualTo("austria"); + assertThat(typeRef.getAge()).isEqualTo(10); + } + + @Test + public void typeIndexedRef() throws Exception { + DefaultListableBeanFactory beanFactory = createFactory("simpleConstructorNamespaceHandlerTests.xml"); + DummyBean typeRef = beanFactory.getBean("indexed-ref", DummyBean.class); + + assertThat(typeRef.getName()).isEqualTo("some-name"); + assertThat(typeRef.getSpouse()).isEqualTo(beanFactory.getBean("name-value")); + } + + @Test + public void ambiguousConstructor() throws Exception { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> + new XmlBeanDefinitionReader(bf).loadBeanDefinitions( + new ClassPathResource("simpleConstructorNamespaceHandlerTestsWithErrors.xml", getClass()))); + } + + @Test + public void constructorWithNameEndingInRef() throws Exception { + DefaultListableBeanFactory beanFactory = createFactory("simpleConstructorNamespaceHandlerTests.xml"); + DummyBean derivedBean = beanFactory.getBean("beanWithRefConstructorArg", DummyBean.class); + assertThat(derivedBean.getAge()).isEqualTo(10); + assertThat(derivedBean.getName()).isEqualTo("silly name"); + } + + private DefaultListableBeanFactory createFactory(String resourceName) { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions( + new ClassPathResource(resourceName, getClass())); + return bf; + } +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/SimplePropertyNamespaceHandlerTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/SimplePropertyNamespaceHandlerTests.java new file mode 100644 index 0000000..68ef31d --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/SimplePropertyNamespaceHandlerTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Rob Harrop + * @author Juergen Hoeller + * @author Arjen Poutsma + */ +public class SimplePropertyNamespaceHandlerTests { + + @Test + public void simpleBeanConfigured() throws Exception { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(beanFactory).loadBeanDefinitions( + new ClassPathResource("simplePropertyNamespaceHandlerTests.xml", getClass())); + ITestBean rob = (TestBean) beanFactory.getBean("rob"); + ITestBean sally = (TestBean) beanFactory.getBean("sally"); + assertThat(rob.getName()).isEqualTo("Rob Harrop"); + assertThat(rob.getAge()).isEqualTo(24); + assertThat(sally).isEqualTo(rob.getSpouse()); + } + + @Test + public void innerBeanConfigured() throws Exception { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(beanFactory).loadBeanDefinitions( + new ClassPathResource("simplePropertyNamespaceHandlerTests.xml", getClass())); + TestBean sally = (TestBean) beanFactory.getBean("sally2"); + ITestBean rob = sally.getSpouse(); + assertThat(rob.getName()).isEqualTo("Rob Harrop"); + assertThat(rob.getAge()).isEqualTo(24); + assertThat(sally).isEqualTo(rob.getSpouse()); + } + + @Test + public void withPropertyDefinedTwice() throws Exception { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> + new XmlBeanDefinitionReader(beanFactory).loadBeanDefinitions( + new ClassPathResource("simplePropertyNamespaceHandlerTestsWithErrors.xml", getClass()))); + } + + @Test + public void propertyWithNameEndingInRef() throws Exception { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(beanFactory).loadBeanDefinitions( + new ClassPathResource("simplePropertyNamespaceHandlerTests.xml", getClass())); + ITestBean sally = (TestBean) beanFactory.getBean("derivedSally"); + assertThat(sally.getSpouse().getName()).isEqualTo("r"); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/TestBeanCreator.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/TestBeanCreator.java new file mode 100644 index 0000000..091a202 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/TestBeanCreator.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.springframework.beans.testfixture.beans.TestBean; + +/** + * Test class for Spring's ability to create + * objects using static factory methods, rather + * than constructors. + * @author Rod Johnson + */ +public class TestBeanCreator { + + public static TestBean createTestBean(String name, int age) { + TestBean tb = new TestBean(); + tb.setName(name); + tb.setAge(age); + return tb; + } + + public static TestBean createTestBean() { + TestBean tb = new TestBean(); + tb.setName("Tristan"); + tb.setAge(2); + return tb; + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/UtilNamespaceHandlerTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/UtilNamespaceHandlerTests.java new file mode 100644 index 0000000..2260a86 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/UtilNamespaceHandlerTests.java @@ -0,0 +1,396 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.TreeMap; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.FieldRetrievingFactoryBean; +import org.springframework.beans.factory.config.PropertiesFactoryBean; +import org.springframework.beans.factory.parsing.ComponentDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.testfixture.beans.CollectingReaderEventListener; +import org.springframework.beans.testfixture.beans.CustomEnum; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.io.ClassPathResource; +import org.springframework.util.LinkedCaseInsensitiveMap; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + * @author Juergen Hoeller + * @author Mark Fisher + */ +@SuppressWarnings("rawtypes") +public class UtilNamespaceHandlerTests { + + private DefaultListableBeanFactory beanFactory; + + private CollectingReaderEventListener listener = new CollectingReaderEventListener(); + + + @BeforeEach + public void setUp() { + this.beanFactory = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this.beanFactory); + reader.setEventListener(this.listener); + reader.loadBeanDefinitions(new ClassPathResource("testUtilNamespace.xml", getClass())); + } + + + @Test + public void testConstant() { + Integer min = (Integer) this.beanFactory.getBean("min"); + assertThat(min.intValue()).isEqualTo(Integer.MIN_VALUE); + } + + @Test + public void testConstantWithDefaultName() { + Integer max = (Integer) this.beanFactory.getBean("java.lang.Integer.MAX_VALUE"); + assertThat(max.intValue()).isEqualTo(Integer.MAX_VALUE); + } + + @Test + public void testEvents() { + ComponentDefinition propertiesComponent = this.listener.getComponentDefinition("myProperties"); + assertThat(propertiesComponent).as("Event for 'myProperties' not sent").isNotNull(); + AbstractBeanDefinition propertiesBean = (AbstractBeanDefinition) propertiesComponent.getBeanDefinitions()[0]; + assertThat(propertiesBean.getBeanClass()).as("Incorrect BeanDefinition").isEqualTo(PropertiesFactoryBean.class); + + ComponentDefinition constantComponent = this.listener.getComponentDefinition("min"); + assertThat(propertiesComponent).as("Event for 'min' not sent").isNotNull(); + AbstractBeanDefinition constantBean = (AbstractBeanDefinition) constantComponent.getBeanDefinitions()[0]; + assertThat(constantBean.getBeanClass()).as("Incorrect BeanDefinition").isEqualTo(FieldRetrievingFactoryBean.class); + } + + @Test + public void testNestedProperties() { + TestBean bean = (TestBean) this.beanFactory.getBean("testBean"); + Properties props = bean.getSomeProperties(); + assertThat(props.get("foo")).as("Incorrect property value").isEqualTo("bar"); + } + + @Test + public void testPropertyPath() { + String name = (String) this.beanFactory.getBean("name"); + assertThat(name).isEqualTo("Rob Harrop"); + } + + @Test + public void testNestedPropertyPath() { + TestBean bean = (TestBean) this.beanFactory.getBean("testBean"); + assertThat(bean.getName()).isEqualTo("Rob Harrop"); + } + + @Test + public void testSimpleMap() { + Map map = (Map) this.beanFactory.getBean("simpleMap"); + assertThat(map.get("foo")).isEqualTo("bar"); + Map map2 = (Map) this.beanFactory.getBean("simpleMap"); + assertThat(map == map2).isTrue(); + } + + @Test + public void testScopedMap() { + Map map = (Map) this.beanFactory.getBean("scopedMap"); + assertThat(map.get("foo")).isEqualTo("bar"); + Map map2 = (Map) this.beanFactory.getBean("scopedMap"); + assertThat(map2.get("foo")).isEqualTo("bar"); + assertThat(map != map2).isTrue(); + } + + @Test + public void testSimpleList() { + List list = (List) this.beanFactory.getBean("simpleList"); + assertThat(list.get(0)).isEqualTo("Rob Harrop"); + List list2 = (List) this.beanFactory.getBean("simpleList"); + assertThat(list == list2).isTrue(); + } + + @Test + public void testScopedList() { + List list = (List) this.beanFactory.getBean("scopedList"); + assertThat(list.get(0)).isEqualTo("Rob Harrop"); + List list2 = (List) this.beanFactory.getBean("scopedList"); + assertThat(list2.get(0)).isEqualTo("Rob Harrop"); + assertThat(list != list2).isTrue(); + } + + @Test + public void testSimpleSet() { + Set set = (Set) this.beanFactory.getBean("simpleSet"); + assertThat(set.contains("Rob Harrop")).isTrue(); + Set set2 = (Set) this.beanFactory.getBean("simpleSet"); + assertThat(set == set2).isTrue(); + } + + @Test + public void testScopedSet() { + Set set = (Set) this.beanFactory.getBean("scopedSet"); + assertThat(set.contains("Rob Harrop")).isTrue(); + Set set2 = (Set) this.beanFactory.getBean("scopedSet"); + assertThat(set2.contains("Rob Harrop")).isTrue(); + assertThat(set != set2).isTrue(); + } + + @Test + public void testMapWithRef() { + Map map = (Map) this.beanFactory.getBean("mapWithRef"); + boolean condition = map instanceof TreeMap; + assertThat(condition).isTrue(); + assertThat(map.get("bean")).isEqualTo(this.beanFactory.getBean("testBean")); + } + + @Test + public void testMapWithTypes() { + Map map = (Map) this.beanFactory.getBean("mapWithTypes"); + boolean condition = map instanceof LinkedCaseInsensitiveMap; + assertThat(condition).isTrue(); + assertThat(map.get("bean")).isEqualTo(this.beanFactory.getBean("testBean")); + } + + @Test + public void testNestedCollections() { + TestBean bean = (TestBean) this.beanFactory.getBean("nestedCollectionsBean"); + + List list = bean.getSomeList(); + assertThat(list.size()).isEqualTo(1); + assertThat(list.get(0)).isEqualTo("foo"); + + Set set = bean.getSomeSet(); + assertThat(set.size()).isEqualTo(1); + assertThat(set.contains("bar")).isTrue(); + + Map map = bean.getSomeMap(); + assertThat(map.size()).isEqualTo(1); + boolean condition = map.get("foo") instanceof Set; + assertThat(condition).isTrue(); + Set innerSet = (Set) map.get("foo"); + assertThat(innerSet.size()).isEqualTo(1); + assertThat(innerSet.contains("bar")).isTrue(); + + TestBean bean2 = (TestBean) this.beanFactory.getBean("nestedCollectionsBean"); + assertThat(bean2.getSomeList()).isEqualTo(list); + assertThat(bean2.getSomeSet()).isEqualTo(set); + assertThat(bean2.getSomeMap()).isEqualTo(map); + assertThat(list == bean2.getSomeList()).isFalse(); + assertThat(set == bean2.getSomeSet()).isFalse(); + assertThat(map == bean2.getSomeMap()).isFalse(); + } + + @Test + public void testNestedShortcutCollections() { + TestBean bean = (TestBean) this.beanFactory.getBean("nestedShortcutCollections"); + + assertThat(bean.getStringArray().length).isEqualTo(1); + assertThat(bean.getStringArray()[0]).isEqualTo("fooStr"); + + List list = bean.getSomeList(); + assertThat(list.size()).isEqualTo(1); + assertThat(list.get(0)).isEqualTo("foo"); + + Set set = bean.getSomeSet(); + assertThat(set.size()).isEqualTo(1); + assertThat(set.contains("bar")).isTrue(); + + TestBean bean2 = (TestBean) this.beanFactory.getBean("nestedShortcutCollections"); + assertThat(Arrays.equals(bean.getStringArray(), bean2.getStringArray())).isTrue(); + assertThat(bean.getStringArray() == bean2.getStringArray()).isFalse(); + assertThat(bean2.getSomeList()).isEqualTo(list); + assertThat(bean2.getSomeSet()).isEqualTo(set); + assertThat(list == bean2.getSomeList()).isFalse(); + assertThat(set == bean2.getSomeSet()).isFalse(); + } + + @Test + public void testNestedInCollections() { + TestBean bean = (TestBean) this.beanFactory.getBean("nestedCustomTagBean"); + + List list = bean.getSomeList(); + assertThat(list.size()).isEqualTo(1); + assertThat(list.get(0)).isEqualTo(Integer.MIN_VALUE); + + Set set = bean.getSomeSet(); + assertThat(set.size()).isEqualTo(2); + assertThat(set.contains(Thread.State.NEW)).isTrue(); + assertThat(set.contains(Thread.State.RUNNABLE)).isTrue(); + + Map map = bean.getSomeMap(); + assertThat(map.size()).isEqualTo(1); + assertThat(map.get("min")).isEqualTo(CustomEnum.VALUE_1); + + TestBean bean2 = (TestBean) this.beanFactory.getBean("nestedCustomTagBean"); + assertThat(bean2.getSomeList()).isEqualTo(list); + assertThat(bean2.getSomeSet()).isEqualTo(set); + assertThat(bean2.getSomeMap()).isEqualTo(map); + assertThat(list == bean2.getSomeList()).isFalse(); + assertThat(set == bean2.getSomeSet()).isFalse(); + assertThat(map == bean2.getSomeMap()).isFalse(); + } + + @Test + public void testCircularCollections() { + TestBean bean = (TestBean) this.beanFactory.getBean("circularCollectionsBean"); + + List list = bean.getSomeList(); + assertThat(list.size()).isEqualTo(1); + assertThat(list.get(0)).isEqualTo(bean); + + Set set = bean.getSomeSet(); + assertThat(set.size()).isEqualTo(1); + assertThat(set.contains(bean)).isTrue(); + + Map map = bean.getSomeMap(); + assertThat(map.size()).isEqualTo(1); + assertThat(map.get("foo")).isEqualTo(bean); + } + + @Test + public void testCircularCollectionBeansStartingWithList() { + this.beanFactory.getBean("circularList"); + TestBean bean = (TestBean) this.beanFactory.getBean("circularCollectionBeansBean"); + + List list = bean.getSomeList(); + assertThat(Proxy.isProxyClass(list.getClass())).isTrue(); + assertThat(list.size()).isEqualTo(1); + assertThat(list.get(0)).isEqualTo(bean); + + Set set = bean.getSomeSet(); + assertThat(Proxy.isProxyClass(set.getClass())).isFalse(); + assertThat(set.size()).isEqualTo(1); + assertThat(set.contains(bean)).isTrue(); + + Map map = bean.getSomeMap(); + assertThat(Proxy.isProxyClass(map.getClass())).isFalse(); + assertThat(map.size()).isEqualTo(1); + assertThat(map.get("foo")).isEqualTo(bean); + } + + @Test + public void testCircularCollectionBeansStartingWithSet() { + this.beanFactory.getBean("circularSet"); + TestBean bean = (TestBean) this.beanFactory.getBean("circularCollectionBeansBean"); + + List list = bean.getSomeList(); + assertThat(Proxy.isProxyClass(list.getClass())).isFalse(); + assertThat(list.size()).isEqualTo(1); + assertThat(list.get(0)).isEqualTo(bean); + + Set set = bean.getSomeSet(); + assertThat(Proxy.isProxyClass(set.getClass())).isTrue(); + assertThat(set.size()).isEqualTo(1); + assertThat(set.contains(bean)).isTrue(); + + Map map = bean.getSomeMap(); + assertThat(Proxy.isProxyClass(map.getClass())).isFalse(); + assertThat(map.size()).isEqualTo(1); + assertThat(map.get("foo")).isEqualTo(bean); + } + + @Test + public void testCircularCollectionBeansStartingWithMap() { + this.beanFactory.getBean("circularMap"); + TestBean bean = (TestBean) this.beanFactory.getBean("circularCollectionBeansBean"); + + List list = bean.getSomeList(); + assertThat(Proxy.isProxyClass(list.getClass())).isFalse(); + assertThat(list.size()).isEqualTo(1); + assertThat(list.get(0)).isEqualTo(bean); + + Set set = bean.getSomeSet(); + assertThat(Proxy.isProxyClass(set.getClass())).isFalse(); + assertThat(set.size()).isEqualTo(1); + assertThat(set.contains(bean)).isTrue(); + + Map map = bean.getSomeMap(); + assertThat(Proxy.isProxyClass(map.getClass())).isTrue(); + assertThat(map.size()).isEqualTo(1); + assertThat(map.get("foo")).isEqualTo(bean); + } + + @Test + public void testNestedInConstructor() { + TestBean bean = (TestBean) this.beanFactory.getBean("constructedTestBean"); + assertThat(bean.getName()).isEqualTo("Rob Harrop"); + } + + @Test + public void testLoadProperties() { + Properties props = (Properties) this.beanFactory.getBean("myProperties"); + assertThat(props.get("foo")).as("Incorrect property value").isEqualTo("bar"); + assertThat(props.get("foo2")).as("Incorrect property value").isEqualTo(null); + Properties props2 = (Properties) this.beanFactory.getBean("myProperties"); + assertThat(props == props2).isTrue(); + } + + @Test + public void testScopedProperties() { + Properties props = (Properties) this.beanFactory.getBean("myScopedProperties"); + assertThat(props.get("foo")).as("Incorrect property value").isEqualTo("bar"); + assertThat(props.get("foo2")).as("Incorrect property value").isEqualTo(null); + Properties props2 = (Properties) this.beanFactory.getBean("myScopedProperties"); + assertThat(props.get("foo")).as("Incorrect property value").isEqualTo("bar"); + assertThat(props.get("foo2")).as("Incorrect property value").isEqualTo(null); + assertThat(props != props2).isTrue(); + } + + @Test + public void testLocalProperties() { + Properties props = (Properties) this.beanFactory.getBean("myLocalProperties"); + assertThat(props.get("foo")).as("Incorrect property value").isEqualTo(null); + assertThat(props.get("foo2")).as("Incorrect property value").isEqualTo("bar2"); + } + + @Test + public void testMergedProperties() { + Properties props = (Properties) this.beanFactory.getBean("myMergedProperties"); + assertThat(props.get("foo")).as("Incorrect property value").isEqualTo("bar"); + assertThat(props.get("foo2")).as("Incorrect property value").isEqualTo("bar2"); + } + + @Test + public void testLocalOverrideDefault() { + Properties props = (Properties) this.beanFactory.getBean("defaultLocalOverrideProperties"); + assertThat(props.get("foo")).as("Incorrect property value").isEqualTo("bar"); + assertThat(props.get("foo2")).as("Incorrect property value").isEqualTo("local2"); + } + + @Test + public void testLocalOverrideFalse() { + Properties props = (Properties) this.beanFactory.getBean("falseLocalOverrideProperties"); + assertThat(props.get("foo")).as("Incorrect property value").isEqualTo("bar"); + assertThat(props.get("foo2")).as("Incorrect property value").isEqualTo("local2"); + } + + @Test + public void testLocalOverrideTrue() { + Properties props = (Properties) this.beanFactory.getBean("trueLocalOverrideProperties"); + assertThat(props.get("foo")).as("Incorrect property value").isEqualTo("local"); + assertThat(props.get("foo2")).as("Incorrect property value").isEqualTo("local2"); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/XmlBeanCollectionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/XmlBeanCollectionTests.java new file mode 100644 index 0000000..e44f0d9 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/XmlBeanCollectionTests.java @@ -0,0 +1,467 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.config.ListFactoryBean; +import org.springframework.beans.factory.config.MapFactoryBean; +import org.springframework.beans.factory.config.SetFactoryBean; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.testfixture.beans.HasMap; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for collections in XML bean definitions. + * + * @author Juergen Hoeller + * @author Chris Beams + * @since 19.12.2004 + */ +@SuppressWarnings({ "rawtypes", "unchecked" }) +public class XmlBeanCollectionTests { + + private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + + @BeforeEach + public void loadBeans() { + new XmlBeanDefinitionReader(this.beanFactory).loadBeanDefinitions( + new ClassPathResource("collections.xml", getClass())); + } + + + @Test + public void testCollectionFactoryDefaults() throws Exception { + ListFactoryBean listFactory = new ListFactoryBean(); + listFactory.setSourceList(new LinkedList()); + listFactory.afterPropertiesSet(); + boolean condition2 = listFactory.getObject() instanceof ArrayList; + assertThat(condition2).isTrue(); + + SetFactoryBean setFactory = new SetFactoryBean(); + setFactory.setSourceSet(new TreeSet()); + setFactory.afterPropertiesSet(); + boolean condition1 = setFactory.getObject() instanceof LinkedHashSet; + assertThat(condition1).isTrue(); + + MapFactoryBean mapFactory = new MapFactoryBean(); + mapFactory.setSourceMap(new TreeMap()); + mapFactory.afterPropertiesSet(); + boolean condition = mapFactory.getObject() instanceof LinkedHashMap; + assertThat(condition).isTrue(); + } + + @Test + public void testRefSubelement() throws Exception { + //assertTrue("5 beans in reftypes, not " + this.beanFactory.getBeanDefinitionCount(), this.beanFactory.getBeanDefinitionCount() == 5); + TestBean jen = (TestBean) this.beanFactory.getBean("jenny"); + TestBean dave = (TestBean) this.beanFactory.getBean("david"); + assertThat(jen.getSpouse() == dave).isTrue(); + } + + @Test + public void testPropertyWithLiteralValueSubelement() throws Exception { + TestBean verbose = (TestBean) this.beanFactory.getBean("verbose"); + assertThat(verbose.getName().equals("verbose")).isTrue(); + } + + @Test + public void testPropertyWithIdRefLocalAttrSubelement() throws Exception { + TestBean verbose = (TestBean) this.beanFactory.getBean("verbose2"); + assertThat(verbose.getName().equals("verbose")).isTrue(); + } + + @Test + public void testPropertyWithIdRefBeanAttrSubelement() throws Exception { + TestBean verbose = (TestBean) this.beanFactory.getBean("verbose3"); + assertThat(verbose.getName().equals("verbose")).isTrue(); + } + + @Test + public void testRefSubelementsBuildCollection() throws Exception { + TestBean jen = (TestBean) this.beanFactory.getBean("jenny"); + TestBean dave = (TestBean) this.beanFactory.getBean("david"); + TestBean rod = (TestBean) this.beanFactory.getBean("rod"); + + // Must be a list to support ordering + // Our bean doesn't modify the collection: + // of course it could be a different copy in a real object. + Object[] friends = rod.getFriends().toArray(); + assertThat(friends.length == 2).isTrue(); + + assertThat(friends[0] == jen).as("First friend must be jen, not " + friends[0]).isTrue(); + assertThat(friends[1] == dave).isTrue(); + // Should be ordered + } + + @Test + public void testRefSubelementsBuildCollectionWithPrototypes() throws Exception { + TestBean jen = (TestBean) this.beanFactory.getBean("pJenny"); + TestBean dave = (TestBean) this.beanFactory.getBean("pDavid"); + TestBean rod = (TestBean) this.beanFactory.getBean("pRod"); + + Object[] friends = rod.getFriends().toArray(); + assertThat(friends.length == 2).isTrue(); + assertThat(friends[0].toString().equals(jen.toString())).as("First friend must be jen, not " + friends[0]).isTrue(); + assertThat(friends[0] != jen).as("Jen not same instance").isTrue(); + assertThat(friends[1].toString().equals(dave.toString())).isTrue(); + assertThat(friends[1] != dave).as("Dave not same instance").isTrue(); + assertThat(dave.getSpouse().getName()).isEqualTo("Jen"); + + TestBean rod2 = (TestBean) this.beanFactory.getBean("pRod"); + Object[] friends2 = rod2.getFriends().toArray(); + assertThat(friends2.length == 2).isTrue(); + assertThat(friends2[0].toString().equals(jen.toString())).as("First friend must be jen, not " + friends2[0]).isTrue(); + assertThat(friends2[0] != friends[0]).as("Jen not same instance").isTrue(); + assertThat(friends2[1].toString().equals(dave.toString())).isTrue(); + assertThat(friends2[1] != friends[1]).as("Dave not same instance").isTrue(); + } + + @Test + public void testRefSubelementsBuildCollectionFromSingleElement() throws Exception { + TestBean loner = (TestBean) this.beanFactory.getBean("loner"); + TestBean dave = (TestBean) this.beanFactory.getBean("david"); + assertThat(loner.getFriends().size() == 1).isTrue(); + assertThat(loner.getFriends().contains(dave)).isTrue(); + } + + @Test + public void testBuildCollectionFromMixtureOfReferencesAndValues() throws Exception { + MixedCollectionBean jumble = (MixedCollectionBean) this.beanFactory.getBean("jumble"); + assertThat(jumble.getJumble().size() == 5).as("Expected 5 elements, not " + jumble.getJumble().size()).isTrue(); + List l = (List) jumble.getJumble(); + assertThat(l.get(0).equals(this.beanFactory.getBean("david"))).isTrue(); + assertThat(l.get(1).equals("literal")).isTrue(); + assertThat(l.get(2).equals(this.beanFactory.getBean("jenny"))).isTrue(); + assertThat(l.get(3).equals("rod")).isTrue(); + Object[] array = (Object[]) l.get(4); + assertThat(array[0].equals(this.beanFactory.getBean("david"))).isTrue(); + assertThat(array[1].equals("literal2")).isTrue(); + } + + @Test + public void testInvalidBeanNameReference() throws Exception { + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + this.beanFactory.getBean("jumble2")) + .withCauseInstanceOf(BeanDefinitionStoreException.class) + .withMessageContaining("rod2"); + } + + @Test + public void testEmptyMap() throws Exception { + HasMap hasMap = (HasMap) this.beanFactory.getBean("emptyMap"); + assertThat(hasMap.getMap().size() == 0).isTrue(); + } + + @Test + public void testMapWithLiteralsOnly() throws Exception { + HasMap hasMap = (HasMap) this.beanFactory.getBean("literalMap"); + assertThat(hasMap.getMap().size() == 3).isTrue(); + assertThat(hasMap.getMap().get("foo").equals("bar")).isTrue(); + assertThat(hasMap.getMap().get("fi").equals("fum")).isTrue(); + assertThat(hasMap.getMap().get("fa") == null).isTrue(); + } + + @Test + public void testMapWithLiteralsAndReferences() throws Exception { + HasMap hasMap = (HasMap) this.beanFactory.getBean("mixedMap"); + assertThat(hasMap.getMap().size() == 5).isTrue(); + assertThat(hasMap.getMap().get("foo").equals(10)).isTrue(); + TestBean jenny = (TestBean) this.beanFactory.getBean("jenny"); + assertThat(hasMap.getMap().get("jenny") == jenny).isTrue(); + assertThat(hasMap.getMap().get(5).equals("david")).isTrue(); + boolean condition1 = hasMap.getMap().get("bar") instanceof Long; + assertThat(condition1).isTrue(); + assertThat(hasMap.getMap().get("bar").equals(100L)).isTrue(); + boolean condition = hasMap.getMap().get("baz") instanceof Integer; + assertThat(condition).isTrue(); + assertThat(hasMap.getMap().get("baz").equals(200)).isTrue(); + } + + @Test + public void testMapWithLiteralsAndPrototypeReferences() throws Exception { + TestBean jenny = (TestBean) this.beanFactory.getBean("pJenny"); + HasMap hasMap = (HasMap) this.beanFactory.getBean("pMixedMap"); + assertThat(hasMap.getMap().size() == 2).isTrue(); + assertThat(hasMap.getMap().get("foo").equals("bar")).isTrue(); + assertThat(hasMap.getMap().get("jenny").toString().equals(jenny.toString())).isTrue(); + assertThat(hasMap.getMap().get("jenny") != jenny).as("Not same instance").isTrue(); + + HasMap hasMap2 = (HasMap) this.beanFactory.getBean("pMixedMap"); + assertThat(hasMap2.getMap().size() == 2).isTrue(); + assertThat(hasMap2.getMap().get("foo").equals("bar")).isTrue(); + assertThat(hasMap2.getMap().get("jenny").toString().equals(jenny.toString())).isTrue(); + assertThat(hasMap2.getMap().get("jenny") != hasMap.getMap().get("jenny")).as("Not same instance").isTrue(); + } + + @Test + public void testMapWithLiteralsReferencesAndList() throws Exception { + HasMap hasMap = (HasMap) this.beanFactory.getBean("mixedMapWithList"); + assertThat(hasMap.getMap().size() == 4).isTrue(); + assertThat(hasMap.getMap().get(null).equals("bar")).isTrue(); + TestBean jenny = (TestBean) this.beanFactory.getBean("jenny"); + assertThat(hasMap.getMap().get("jenny").equals(jenny)).isTrue(); + + // Check list + List l = (List) hasMap.getMap().get("list"); + assertThat(l).isNotNull(); + assertThat(l.size() == 4).isTrue(); + assertThat(l.get(0).equals("zero")).isTrue(); + assertThat(l.get(3) == null).isTrue(); + + // Check nested map in list + Map m = (Map) l.get(1); + assertThat(m).isNotNull(); + assertThat(m.size() == 2).isTrue(); + assertThat(m.get("fo").equals("bar")).isTrue(); + assertThat(m.get("jen").equals(jenny)).as("Map element 'jenny' should be equal to jenny bean, not " + m.get("jen")).isTrue(); + + // Check nested list in list + l = (List) l.get(2); + assertThat(l).isNotNull(); + assertThat(l.size() == 2).isTrue(); + assertThat(l.get(0).equals(jenny)).isTrue(); + assertThat(l.get(1).equals("ba")).isTrue(); + + // Check nested map + m = (Map) hasMap.getMap().get("map"); + assertThat(m).isNotNull(); + assertThat(m.size() == 2).isTrue(); + assertThat(m.get("foo").equals("bar")).isTrue(); + assertThat(m.get("jenny").equals(jenny)).as("Map element 'jenny' should be equal to jenny bean, not " + m.get("jenny")).isTrue(); + } + + @Test + public void testEmptySet() throws Exception { + HasMap hasMap = (HasMap) this.beanFactory.getBean("emptySet"); + assertThat(hasMap.getSet().size() == 0).isTrue(); + } + + @Test + public void testPopulatedSet() throws Exception { + HasMap hasMap = (HasMap) this.beanFactory.getBean("set"); + assertThat(hasMap.getSet().size() == 3).isTrue(); + assertThat(hasMap.getSet().contains("bar")).isTrue(); + TestBean jenny = (TestBean) this.beanFactory.getBean("jenny"); + assertThat(hasMap.getSet().contains(jenny)).isTrue(); + assertThat(hasMap.getSet().contains(null)).isTrue(); + Iterator it = hasMap.getSet().iterator(); + assertThat(it.next()).isEqualTo("bar"); + assertThat(it.next()).isEqualTo(jenny); + assertThat(it.next()).isEqualTo(null); + } + + @Test + public void testPopulatedConcurrentSet() throws Exception { + HasMap hasMap = (HasMap) this.beanFactory.getBean("concurrentSet"); + assertThat(hasMap.getConcurrentSet().size() == 3).isTrue(); + assertThat(hasMap.getConcurrentSet().contains("bar")).isTrue(); + TestBean jenny = (TestBean) this.beanFactory.getBean("jenny"); + assertThat(hasMap.getConcurrentSet().contains(jenny)).isTrue(); + assertThat(hasMap.getConcurrentSet().contains(null)).isTrue(); + } + + @Test + public void testPopulatedIdentityMap() throws Exception { + HasMap hasMap = (HasMap) this.beanFactory.getBean("identityMap"); + assertThat(hasMap.getIdentityMap().size() == 2).isTrue(); + HashSet set = new HashSet(hasMap.getIdentityMap().keySet()); + assertThat(set.contains("foo")).isTrue(); + assertThat(set.contains("jenny")).isTrue(); + } + + @Test + public void testEmptyProps() throws Exception { + HasMap hasMap = (HasMap) this.beanFactory.getBean("emptyProps"); + assertThat(hasMap.getProps().size() == 0).isTrue(); + assertThat(Properties.class).isEqualTo(hasMap.getProps().getClass()); + } + + @Test + public void testPopulatedProps() throws Exception { + HasMap hasMap = (HasMap) this.beanFactory.getBean("props"); + assertThat(hasMap.getProps().size() == 2).isTrue(); + assertThat(hasMap.getProps().get("foo").equals("bar")).isTrue(); + assertThat(hasMap.getProps().get("2").equals("TWO")).isTrue(); + } + + @Test + public void testObjectArray() throws Exception { + HasMap hasMap = (HasMap) this.beanFactory.getBean("objectArray"); + assertThat(hasMap.getObjectArray().length == 2).isTrue(); + assertThat(hasMap.getObjectArray()[0].equals("one")).isTrue(); + assertThat(hasMap.getObjectArray()[1].equals(this.beanFactory.getBean("jenny"))).isTrue(); + } + + @Test + public void testIntegerArray() throws Exception { + HasMap hasMap = (HasMap) this.beanFactory.getBean("integerArray"); + assertThat(hasMap.getIntegerArray().length == 3).isTrue(); + assertThat(hasMap.getIntegerArray()[0].intValue() == 0).isTrue(); + assertThat(hasMap.getIntegerArray()[1].intValue() == 1).isTrue(); + assertThat(hasMap.getIntegerArray()[2].intValue() == 2).isTrue(); + } + + @Test + public void testClassArray() throws Exception { + HasMap hasMap = (HasMap) this.beanFactory.getBean("classArray"); + assertThat(hasMap.getClassArray().length == 2).isTrue(); + assertThat(hasMap.getClassArray()[0].equals(String.class)).isTrue(); + assertThat(hasMap.getClassArray()[1].equals(Exception.class)).isTrue(); + } + + @Test + public void testClassList() throws Exception { + HasMap hasMap = (HasMap) this.beanFactory.getBean("classList"); + assertThat(hasMap.getClassList().size()== 2).isTrue(); + assertThat(hasMap.getClassList().get(0).equals(String.class)).isTrue(); + assertThat(hasMap.getClassList().get(1).equals(Exception.class)).isTrue(); + } + + @Test + public void testProps() throws Exception { + HasMap hasMap = (HasMap) this.beanFactory.getBean("props"); + assertThat(hasMap.getProps().size()).isEqualTo(2); + assertThat(hasMap.getProps().getProperty("foo")).isEqualTo("bar"); + assertThat(hasMap.getProps().getProperty("2")).isEqualTo("TWO"); + + HasMap hasMap2 = (HasMap) this.beanFactory.getBean("propsViaMap"); + assertThat(hasMap2.getProps().size()).isEqualTo(2); + assertThat(hasMap2.getProps().getProperty("foo")).isEqualTo("bar"); + assertThat(hasMap2.getProps().getProperty("2")).isEqualTo("TWO"); + } + + @Test + public void testListFactory() throws Exception { + List list = (List) this.beanFactory.getBean("listFactory"); + boolean condition = list instanceof LinkedList; + assertThat(condition).isTrue(); + assertThat(list.size() == 2).isTrue(); + assertThat(list.get(0)).isEqualTo("bar"); + assertThat(list.get(1)).isEqualTo("jenny"); + } + + @Test + public void testPrototypeListFactory() throws Exception { + List list = (List) this.beanFactory.getBean("pListFactory"); + boolean condition = list instanceof LinkedList; + assertThat(condition).isTrue(); + assertThat(list.size() == 2).isTrue(); + assertThat(list.get(0)).isEqualTo("bar"); + assertThat(list.get(1)).isEqualTo("jenny"); + } + + @Test + public void testSetFactory() throws Exception { + Set set = (Set) this.beanFactory.getBean("setFactory"); + boolean condition = set instanceof TreeSet; + assertThat(condition).isTrue(); + assertThat(set.size() == 2).isTrue(); + assertThat(set.contains("bar")).isTrue(); + assertThat(set.contains("jenny")).isTrue(); + } + + @Test + public void testPrototypeSetFactory() throws Exception { + Set set = (Set) this.beanFactory.getBean("pSetFactory"); + boolean condition = set instanceof TreeSet; + assertThat(condition).isTrue(); + assertThat(set.size() == 2).isTrue(); + assertThat(set.contains("bar")).isTrue(); + assertThat(set.contains("jenny")).isTrue(); + } + + @Test + public void testMapFactory() throws Exception { + Map map = (Map) this.beanFactory.getBean("mapFactory"); + boolean condition = map instanceof TreeMap; + assertThat(condition).isTrue(); + assertThat(map.size() == 2).isTrue(); + assertThat(map.get("foo")).isEqualTo("bar"); + assertThat(map.get("jen")).isEqualTo("jenny"); + } + + @Test + public void testPrototypeMapFactory() throws Exception { + Map map = (Map) this.beanFactory.getBean("pMapFactory"); + boolean condition = map instanceof TreeMap; + assertThat(condition).isTrue(); + assertThat(map.size() == 2).isTrue(); + assertThat(map.get("foo")).isEqualTo("bar"); + assertThat(map.get("jen")).isEqualTo("jenny"); + } + + @Test + public void testChoiceBetweenSetAndMap() { + MapAndSet sam = (MapAndSet) this.beanFactory.getBean("setAndMap"); + boolean condition = sam.getObject() instanceof Map; + assertThat(condition).as("Didn't choose constructor with Map argument").isTrue(); + Map map = (Map) sam.getObject(); + assertThat(map.size()).isEqualTo(3); + assertThat(map.get("key1")).isEqualTo("val1"); + assertThat(map.get("key2")).isEqualTo("val2"); + assertThat(map.get("key3")).isEqualTo("val3"); + } + + @Test + public void testEnumSetFactory() throws Exception { + Set set = (Set) this.beanFactory.getBean("enumSetFactory"); + assertThat(set.size() == 2).isTrue(); + assertThat(set.contains("ONE")).isTrue(); + assertThat(set.contains("TWO")).isTrue(); + } + + + public static class MapAndSet { + + private Object obj; + + public MapAndSet(Map map) { + this.obj = map; + } + + public MapAndSet(Set set) { + this.obj = set; + } + + public Object getObject() { + return obj; + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/XmlBeanDefinitionReaderTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/XmlBeanDefinitionReaderTests.java new file mode 100644 index 0000000..69768da --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/XmlBeanDefinitionReaderTests.java @@ -0,0 +1,144 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; +import org.xml.sax.InputSource; + +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.SimpleBeanDefinitionRegistry; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.util.ObjectUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Rick Evans + * @author Juergen Hoeller + * @author Sam Brannen + */ +public class XmlBeanDefinitionReaderTests { + + @Test + public void setParserClassSunnyDay() { + SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + new XmlBeanDefinitionReader(registry).setDocumentReaderClass(DefaultBeanDefinitionDocumentReader.class); + } + + @Test + public void withOpenInputStream() { + SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + Resource resource = new InputStreamResource(getClass().getResourceAsStream("test.xml")); + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> + new XmlBeanDefinitionReader(registry).loadBeanDefinitions(resource)); + } + + @Test + public void withOpenInputStreamAndExplicitValidationMode() { + SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + Resource resource = new InputStreamResource(getClass().getResourceAsStream("test.xml")); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(registry); + reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_DTD); + reader.loadBeanDefinitions(resource); + testBeanDefinitions(registry); + } + + @Test + public void withImport() { + SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + Resource resource = new ClassPathResource("import.xml", getClass()); + new XmlBeanDefinitionReader(registry).loadBeanDefinitions(resource); + testBeanDefinitions(registry); + } + + @Test + public void withWildcardImport() { + SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + Resource resource = new ClassPathResource("importPattern.xml", getClass()); + new XmlBeanDefinitionReader(registry).loadBeanDefinitions(resource); + testBeanDefinitions(registry); + } + + @Test + public void withInputSource() { + SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + InputSource resource = new InputSource(getClass().getResourceAsStream("test.xml")); + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> + new XmlBeanDefinitionReader(registry).loadBeanDefinitions(resource)); + } + + @Test + public void withInputSourceAndExplicitValidationMode() { + SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + InputSource resource = new InputSource(getClass().getResourceAsStream("test.xml")); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(registry); + reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_DTD); + reader.loadBeanDefinitions(resource); + testBeanDefinitions(registry); + } + + @Test + public void withFreshInputStream() { + SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + Resource resource = new ClassPathResource("test.xml", getClass()); + new XmlBeanDefinitionReader(registry).loadBeanDefinitions(resource); + testBeanDefinitions(registry); + } + + private void testBeanDefinitions(BeanDefinitionRegistry registry) { + assertThat(registry.getBeanDefinitionCount()).isEqualTo(24); + assertThat(registry.getBeanDefinitionNames().length).isEqualTo(24); + assertThat(Arrays.asList(registry.getBeanDefinitionNames()).contains("rod")).isTrue(); + assertThat(Arrays.asList(registry.getBeanDefinitionNames()).contains("aliased")).isTrue(); + assertThat(registry.containsBeanDefinition("rod")).isTrue(); + assertThat(registry.containsBeanDefinition("aliased")).isTrue(); + assertThat(registry.getBeanDefinition("rod").getBeanClassName()).isEqualTo(TestBean.class.getName()); + assertThat(registry.getBeanDefinition("aliased").getBeanClassName()).isEqualTo(TestBean.class.getName()); + assertThat(registry.isAlias("youralias")).isTrue(); + String[] aliases = registry.getAliases("aliased"); + assertThat(aliases.length).isEqualTo(2); + assertThat(ObjectUtils.containsElement(aliases, "myalias")).isTrue(); + assertThat(ObjectUtils.containsElement(aliases, "youralias")).isTrue(); + } + + @Test + public void dtdValidationAutodetect() { + doTestValidation("validateWithDtd.xml"); + } + + @Test + public void xsdValidationAutodetect() { + doTestValidation("validateWithXsd.xml"); + } + + private void doTestValidation(String resourceName) { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + Resource resource = new ClassPathResource(resourceName, getClass()); + new XmlBeanDefinitionReader(factory).loadBeanDefinitions(resource); + TestBean bean = (TestBean) factory.getBean("testBean"); + assertThat(bean).isNotNull(); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/XmlListableBeanFactoryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/XmlListableBeanFactoryTests.java new file mode 100644 index 0000000..d48b55f --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/XmlListableBeanFactoryTests.java @@ -0,0 +1,258 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.BeansException; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.LifecycleBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.beans.testfixture.beans.factory.DummyFactory; +import org.springframework.beans.testfixture.factory.xml.AbstractListableBeanFactoryTests; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @since 09.11.2003 + */ +@SuppressWarnings({"rawtypes", "unchecked"}) +public class XmlListableBeanFactoryTests extends AbstractListableBeanFactoryTests { + + private DefaultListableBeanFactory parent; + + private DefaultListableBeanFactory factory; + + + @BeforeEach + public void setup() { + parent = new DefaultListableBeanFactory(); + + Map map = new HashMap(); + map.put("name", "Albert"); + RootBeanDefinition bd1 = new RootBeanDefinition(TestBean.class); + bd1.setPropertyValues(new MutablePropertyValues(map)); + parent.registerBeanDefinition("father", bd1); + + map = new HashMap(); + map.put("name", "Roderick"); + RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); + bd2.setPropertyValues(new MutablePropertyValues(map)); + parent.registerBeanDefinition("rod", bd2); + + this.factory = new DefaultListableBeanFactory(parent); + new XmlBeanDefinitionReader(this.factory).loadBeanDefinitions(new ClassPathResource("test.xml", getClass())); + + this.factory.addBeanPostProcessor(new BeanPostProcessor() { + @Override + public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException { + if (bean instanceof TestBean) { + ((TestBean) bean).setPostProcessed(true); + } + if (bean instanceof DummyFactory) { + ((DummyFactory) bean).setPostProcessed(true); + } + return bean; + } + @Override + public Object postProcessAfterInitialization(Object bean, String name) throws BeansException { + return bean; + } + }); + + this.factory.addBeanPostProcessor(new LifecycleBean.PostProcessor()); + this.factory.addBeanPostProcessor(new ProtectedLifecycleBean.PostProcessor()); + // this.factory.preInstantiateSingletons(); + } + + @Override + protected BeanFactory getBeanFactory() { + return factory; + } + + + @Test + @Override + public void count() { + assertCount(24); + } + + @Test + public void beanCount() { + assertTestBeanCount(13); + } + + @Test + public void lifecycleMethods() { + LifecycleBean bean = (LifecycleBean) getBeanFactory().getBean("lifecycle"); + bean.businessMethod(); + } + + @Test + public void protectedLifecycleMethods() { + ProtectedLifecycleBean bean = (ProtectedLifecycleBean) getBeanFactory().getBean("protectedLifecycle"); + bean.businessMethod(); + } + + @Test + public void descriptionButNoProperties() { + TestBean validEmpty = (TestBean) getBeanFactory().getBean("validEmptyWithDescription"); + assertThat(validEmpty.getAge()).isEqualTo(0); + } + + /** + * Test that properties with name as well as id creating an alias up front. + */ + @Test + public void autoAliasing() { + List beanNames = Arrays.asList(getListableBeanFactory().getBeanDefinitionNames()); + + TestBean tb1 = (TestBean) getBeanFactory().getBean("aliased"); + TestBean alias1 = (TestBean) getBeanFactory().getBean("myalias"); + assertThat(tb1 == alias1).isTrue(); + List tb1Aliases = Arrays.asList(getBeanFactory().getAliases("aliased")); + assertThat(tb1Aliases.size()).isEqualTo(2); + assertThat(tb1Aliases.contains("myalias")).isTrue(); + assertThat(tb1Aliases.contains("youralias")).isTrue(); + assertThat(beanNames.contains("aliased")).isTrue(); + assertThat(beanNames.contains("myalias")).isFalse(); + assertThat(beanNames.contains("youralias")).isFalse(); + + TestBean tb2 = (TestBean) getBeanFactory().getBean("multiAliased"); + TestBean alias2 = (TestBean) getBeanFactory().getBean("alias1"); + TestBean alias3 = (TestBean) getBeanFactory().getBean("alias2"); + TestBean alias3a = (TestBean) getBeanFactory().getBean("alias3"); + TestBean alias3b = (TestBean) getBeanFactory().getBean("alias4"); + assertThat(tb2 == alias2).isTrue(); + assertThat(tb2 == alias3).isTrue(); + assertThat(tb2 == alias3a).isTrue(); + assertThat(tb2 == alias3b).isTrue(); + + List tb2Aliases = Arrays.asList(getBeanFactory().getAliases("multiAliased")); + assertThat(tb2Aliases.size()).isEqualTo(4); + assertThat(tb2Aliases.contains("alias1")).isTrue(); + assertThat(tb2Aliases.contains("alias2")).isTrue(); + assertThat(tb2Aliases.contains("alias3")).isTrue(); + assertThat(tb2Aliases.contains("alias4")).isTrue(); + assertThat(beanNames.contains("multiAliased")).isTrue(); + assertThat(beanNames.contains("alias1")).isFalse(); + assertThat(beanNames.contains("alias2")).isFalse(); + assertThat(beanNames.contains("alias3")).isFalse(); + assertThat(beanNames.contains("alias4")).isFalse(); + + TestBean tb3 = (TestBean) getBeanFactory().getBean("aliasWithoutId1"); + TestBean alias4 = (TestBean) getBeanFactory().getBean("aliasWithoutId2"); + TestBean alias5 = (TestBean) getBeanFactory().getBean("aliasWithoutId3"); + assertThat(tb3 == alias4).isTrue(); + assertThat(tb3 == alias5).isTrue(); + List tb3Aliases = Arrays.asList(getBeanFactory().getAliases("aliasWithoutId1")); + assertThat(tb3Aliases.size()).isEqualTo(2); + assertThat(tb3Aliases.contains("aliasWithoutId2")).isTrue(); + assertThat(tb3Aliases.contains("aliasWithoutId3")).isTrue(); + assertThat(beanNames.contains("aliasWithoutId1")).isTrue(); + assertThat(beanNames.contains("aliasWithoutId2")).isFalse(); + assertThat(beanNames.contains("aliasWithoutId3")).isFalse(); + + TestBean tb4 = (TestBean) getBeanFactory().getBean(TestBean.class.getName() + "#0"); + assertThat(tb4.getName()).isEqualTo(null); + + Map drs = getListableBeanFactory().getBeansOfType(DummyReferencer.class, false, false); + assertThat(drs.size()).isEqualTo(5); + assertThat(drs.containsKey(DummyReferencer.class.getName() + "#0")).isTrue(); + assertThat(drs.containsKey(DummyReferencer.class.getName() + "#1")).isTrue(); + assertThat(drs.containsKey(DummyReferencer.class.getName() + "#2")).isTrue(); + } + + @Test + public void factoryNesting() { + ITestBean father = (ITestBean) getBeanFactory().getBean("father"); + assertThat(father != null).as("Bean from root context").isTrue(); + + TestBean rod = (TestBean) getBeanFactory().getBean("rod"); + assertThat("Rod".equals(rod.getName())).as("Bean from child context").isTrue(); + assertThat(rod.getSpouse() == father).as("Bean has external reference").isTrue(); + + rod = (TestBean) parent.getBean("rod"); + assertThat("Roderick".equals(rod.getName())).as("Bean from root context").isTrue(); + } + + @Test + public void factoryReferences() { + DummyFactory factory = (DummyFactory) getBeanFactory().getBean("&singletonFactory"); + + DummyReferencer ref = (DummyReferencer) getBeanFactory().getBean("factoryReferencer"); + assertThat(ref.getTestBean1() == ref.getTestBean2()).isTrue(); + assertThat(ref.getDummyFactory() == factory).isTrue(); + + DummyReferencer ref2 = (DummyReferencer) getBeanFactory().getBean("factoryReferencerWithConstructor"); + assertThat(ref2.getTestBean1() == ref2.getTestBean2()).isTrue(); + assertThat(ref2.getDummyFactory() == factory).isTrue(); + } + + @Test + public void prototypeReferences() { + // check that not broken by circular reference resolution mechanism + DummyReferencer ref1 = (DummyReferencer) getBeanFactory().getBean("prototypeReferencer"); + assertThat(ref1.getTestBean1() != ref1.getTestBean2()).as("Not referencing same bean twice").isTrue(); + DummyReferencer ref2 = (DummyReferencer) getBeanFactory().getBean("prototypeReferencer"); + assertThat(ref1 != ref2).as("Not the same referencer").isTrue(); + assertThat(ref2.getTestBean1() != ref2.getTestBean2()).as("Not referencing same bean twice").isTrue(); + assertThat(ref1.getTestBean1() != ref2.getTestBean1()).as("Not referencing same bean twice").isTrue(); + assertThat(ref1.getTestBean2() != ref2.getTestBean2()).as("Not referencing same bean twice").isTrue(); + assertThat(ref1.getTestBean1() != ref2.getTestBean2()).as("Not referencing same bean twice").isTrue(); + } + + @Test + public void beanPostProcessor() { + TestBean kerry = (TestBean) getBeanFactory().getBean("kerry"); + TestBean kathy = (TestBean) getBeanFactory().getBean("kathy"); + DummyFactory factory = (DummyFactory) getBeanFactory().getBean("&singletonFactory"); + TestBean factoryCreated = (TestBean) getBeanFactory().getBean("singletonFactory"); + assertThat(kerry.isPostProcessed()).isTrue(); + assertThat(kathy.isPostProcessed()).isTrue(); + assertThat(factory.isPostProcessed()).isTrue(); + assertThat(factoryCreated.isPostProcessed()).isTrue(); + } + + @Test + public void emptyValues() { + TestBean rod = (TestBean) getBeanFactory().getBean("rod"); + TestBean kerry = (TestBean) getBeanFactory().getBean("kerry"); + assertThat("".equals(rod.getTouchy())).as("Touchy is empty").isTrue(); + assertThat("".equals(kerry.getTouchy())).as("Touchy is empty").isTrue(); + } + + @Test + public void commentsAndCdataInValue() { + TestBean bean = (TestBean) getBeanFactory().getBean("commentsInValue"); + assertThat(bean.getName()).as("Failed to handle comments and CDATA properly").isEqualTo("this is a "); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/support/DefaultNamespaceHandlerResolverTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/support/DefaultNamespaceHandlerResolverTests.java new file mode 100644 index 0000000..faa4199 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/support/DefaultNamespaceHandlerResolverTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml.support; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.xml.DefaultNamespaceHandlerResolver; +import org.springframework.beans.factory.xml.NamespaceHandler; +import org.springframework.beans.factory.xml.UtilNamespaceHandler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Unit and integration tests for the {@link DefaultNamespaceHandlerResolver} class. + * + * @author Rob Harrop + * @author Rick Evans + */ +public class DefaultNamespaceHandlerResolverTests { + + @Test + public void testResolvedMappedHandler() { + DefaultNamespaceHandlerResolver resolver = new DefaultNamespaceHandlerResolver(getClass().getClassLoader()); + NamespaceHandler handler = resolver.resolve("http://www.springframework.org/schema/util"); + assertThat(handler).as("Handler should not be null.").isNotNull(); + assertThat(handler.getClass()).as("Incorrect handler loaded").isEqualTo(UtilNamespaceHandler.class); + } + + @Test + public void testResolvedMappedHandlerWithNoArgCtor() { + DefaultNamespaceHandlerResolver resolver = new DefaultNamespaceHandlerResolver(); + NamespaceHandler handler = resolver.resolve("http://www.springframework.org/schema/util"); + assertThat(handler).as("Handler should not be null.").isNotNull(); + assertThat(handler.getClass()).as("Incorrect handler loaded").isEqualTo(UtilNamespaceHandler.class); + } + + @Test + public void testNonExistentHandlerClass() { + String mappingPath = "org/springframework/beans/factory/xml/support/nonExistent.properties"; + new DefaultNamespaceHandlerResolver(getClass().getClassLoader(), mappingPath); + } + + @Test + public void testCtorWithNullClassLoaderArgument() { + // simply must not bail... + new DefaultNamespaceHandlerResolver(null); + } + + @Test + public void testCtorWithNullClassLoaderArgumentAndNullMappingLocationArgument() { + assertThatIllegalArgumentException().isThrownBy(() -> + new DefaultNamespaceHandlerResolver(null, null)); + } + + @Test + public void testCtorWithNonExistentMappingLocationArgument() { + // simply must not bail; we don't want non-existent resources to result in an Exception + new DefaultNamespaceHandlerResolver(null, "738trbc bobabloobop871"); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/BeanInfoTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/BeanInfoTests.java new file mode 100644 index 0000000..7b91fc6 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/BeanInfoTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.IntrospectionException; +import java.beans.PropertyDescriptor; +import java.beans.SimpleBeanInfo; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.BeanWrapperImpl; +import org.springframework.beans.FatalBeanException; +import org.springframework.util.Assert; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @since 06.03.2006 + */ +public class BeanInfoTests { + + @Test + public void testComplexObject() { + ValueBean bean = new ValueBean(); + BeanWrapper bw = new BeanWrapperImpl(bean); + Integer value = 1; + + bw.setPropertyValue("value", value); + assertThat(value).as("value not set correctly").isEqualTo(bean.getValue()); + + value = 2; + bw.setPropertyValue("value", value.toString()); + assertThat(value).as("value not converted").isEqualTo(bean.getValue()); + + bw.setPropertyValue("value", null); + assertThat(bean.getValue()).as("value not null").isNull(); + + bw.setPropertyValue("value", ""); + assertThat(bean.getValue()).as("value not converted to null").isNull(); + } + + + public static class ValueBean { + + private Integer value; + + public Integer getValue() { + return value; + } + + public void setValue(Integer value) { + this.value = value; + } + } + + + public static class ValueBeanBeanInfo extends SimpleBeanInfo { + + @Override + public PropertyDescriptor[] getPropertyDescriptors() { + try { + PropertyDescriptor pd = new PropertyDescriptor("value", ValueBean.class); + pd.setPropertyEditorClass(MyNumberEditor.class); + return new PropertyDescriptor[] {pd}; + } + catch (IntrospectionException ex) { + throw new FatalBeanException("Couldn't create PropertyDescriptor", ex); + } + } + } + + + public static class MyNumberEditor extends CustomNumberEditor { + + private Object target; + + public MyNumberEditor() throws IllegalArgumentException { + super(Integer.class, true); + } + + public MyNumberEditor(Object target) throws IllegalArgumentException { + super(Integer.class, true); + this.target = target; + } + + @Override + public void setAsText(String text) throws IllegalArgumentException { + Assert.isTrue(this.target instanceof ValueBean, "Target must be available"); + super.setAsText(text); + } + + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ByteArrayPropertyEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ByteArrayPropertyEditorTests.java new file mode 100644 index 0000000..0ea1102 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ByteArrayPropertyEditorTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditor; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for the {@link ByteArrayPropertyEditor} class. + * + * @author Rick Evans + */ +public class ByteArrayPropertyEditorTests { + + private final PropertyEditor byteEditor = new ByteArrayPropertyEditor(); + + @Test + public void sunnyDaySetAsText() throws Exception { + final String text = "Hideous towns make me throw... up"; + byteEditor.setAsText(text); + + Object value = byteEditor.getValue(); + assertThat(value).isNotNull().isInstanceOf(byte[].class); + byte[] bytes = (byte[]) value; + for (int i = 0; i < text.length(); ++i) { + assertThat(bytes[i]).as("cyte[] differs at index '" + i + "'").isEqualTo((byte) text.charAt(i)); + } + assertThat(byteEditor.getAsText()).isEqualTo(text); + } + + @Test + public void getAsTextReturnsEmptyStringIfValueIsNull() throws Exception { + assertThat(byteEditor.getAsText()).isEqualTo(""); + + byteEditor.setAsText(null); + assertThat(byteEditor.getAsText()).isEqualTo(""); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CharArrayPropertyEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CharArrayPropertyEditorTests.java new file mode 100644 index 0000000..d01f591 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CharArrayPropertyEditorTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditor; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for the {@link CharArrayPropertyEditor} class. + * + * @author Rick Evans + */ +public class CharArrayPropertyEditorTests { + + private final PropertyEditor charEditor = new CharArrayPropertyEditor(); + + @Test + public void sunnyDaySetAsText() throws Exception { + final String text = "Hideous towns make me throw... up"; + charEditor.setAsText(text); + + Object value = charEditor.getValue(); + assertThat(value).isNotNull().isInstanceOf(char[].class); + char[] chars = (char[]) value; + for (int i = 0; i < text.length(); ++i) { + assertThat(chars[i]).as("char[] differs at index '" + i + "'").isEqualTo(text.charAt(i)); + } + assertThat(charEditor.getAsText()).isEqualTo(text); + } + + @Test + public void getAsTextReturnsEmptyStringIfValueIsNull() throws Exception { + assertThat(charEditor.getAsText()).isEqualTo(""); + + charEditor.setAsText(null); + assertThat(charEditor.getAsText()).isEqualTo(""); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomCollectionEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomCollectionEditorTests.java new file mode 100644 index 0000000..ad8d4c7 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomCollectionEditorTests.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Unit tests for the {@link CustomCollectionEditor} class. + * + * @author Rick Evans + * @author Chris Beams + */ +public class CustomCollectionEditorTests { + + @Test + public void testCtorWithNullCollectionType() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new CustomCollectionEditor(null)); + } + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void testCtorWithNonCollectionType() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new CustomCollectionEditor((Class) String.class)); + } + + @Test + public void testWithCollectionTypeThatDoesNotExposeAPublicNoArgCtor() throws Exception { + CustomCollectionEditor editor = new CustomCollectionEditor(CollectionTypeWithNoNoArgCtor.class); + assertThatIllegalArgumentException().isThrownBy(() -> + editor.setValue("1")); + } + + @Test + public void testSunnyDaySetValue() throws Exception { + CustomCollectionEditor editor = new CustomCollectionEditor(ArrayList.class); + editor.setValue(new int[] {0, 1, 2}); + Object value = editor.getValue(); + assertThat(value).isNotNull(); + boolean condition = value instanceof ArrayList; + assertThat(condition).isTrue(); + List list = (List) value; + assertThat(list.size()).as("There must be 3 elements in the converted collection").isEqualTo(3); + assertThat(list.get(0)).isEqualTo(0); + assertThat(list.get(1)).isEqualTo(1); + assertThat(list.get(2)).isEqualTo(2); + } + + @Test + public void testWhenTargetTypeIsExactlyTheCollectionInterfaceUsesFallbackCollectionType() throws Exception { + CustomCollectionEditor editor = new CustomCollectionEditor(Collection.class); + editor.setValue("0, 1, 2"); + Collection value = (Collection) editor.getValue(); + assertThat(value).isNotNull(); + assertThat(value.size()).as("There must be 1 element in the converted collection").isEqualTo(1); + assertThat(value.iterator().next()).isEqualTo("0, 1, 2"); + } + + @Test + public void testSunnyDaySetAsTextYieldsSingleValue() throws Exception { + CustomCollectionEditor editor = new CustomCollectionEditor(ArrayList.class); + editor.setValue("0, 1, 2"); + Object value = editor.getValue(); + assertThat(value).isNotNull(); + boolean condition = value instanceof ArrayList; + assertThat(condition).isTrue(); + List list = (List) value; + assertThat(list.size()).as("There must be 1 element in the converted collection").isEqualTo(1); + assertThat(list.get(0)).isEqualTo("0, 1, 2"); + } + + + @SuppressWarnings({ "serial", "unused" }) + private static final class CollectionTypeWithNoNoArgCtor extends ArrayList { + public CollectionTypeWithNoNoArgCtor(String anArg) { + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomEditorTests.java new file mode 100644 index 0000000..0de98a1 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/CustomEditorTests.java @@ -0,0 +1,1581 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditor; +import java.beans.PropertyEditorSupport; +import java.beans.PropertyVetoException; +import java.io.File; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.text.NumberFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.StringTokenizer; +import java.util.Vector; +import java.util.regex.Pattern; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.BeanWrapperImpl; +import org.springframework.beans.BeansException; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.testfixture.beans.BooleanTestBean; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.IndexedTestBean; +import org.springframework.beans.testfixture.beans.NumberTestBean; +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.within; + +/** + * Unit tests for the various PropertyEditors in Spring. + * + * @author Juergen Hoeller + * @author Rick Evans + * @author Rob Harrop + * @author Arjen Poutsma + * @author Chris Beams + * @since 10.06.2003 + */ +public class CustomEditorTests { + + @Test + public void testComplexObject() { + TestBean tb = new TestBean(); + String newName = "Rod"; + String tbString = "Kerry_34"; + + BeanWrapper bw = new BeanWrapperImpl(tb); + bw.registerCustomEditor(ITestBean.class, new TestBeanEditor()); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.addPropertyValue(new PropertyValue("age", 55)); + pvs.addPropertyValue(new PropertyValue("name", newName)); + pvs.addPropertyValue(new PropertyValue("touchy", "valid")); + pvs.addPropertyValue(new PropertyValue("spouse", tbString)); + bw.setPropertyValues(pvs); + assertThat(tb.getSpouse() != null).as("spouse is non-null").isTrue(); + assertThat(tb.getSpouse().getName().equals("Kerry") && tb.getSpouse().getAge() == 34).as("spouse name is Kerry and age is 34").isTrue(); + } + + @Test + public void testComplexObjectWithOldValueAccess() { + TestBean tb = new TestBean(); + String newName = "Rod"; + String tbString = "Kerry_34"; + + BeanWrapper bw = new BeanWrapperImpl(tb); + bw.setExtractOldValueForEditor(true); + bw.registerCustomEditor(ITestBean.class, new OldValueAccessingTestBeanEditor()); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.addPropertyValue(new PropertyValue("age", 55)); + pvs.addPropertyValue(new PropertyValue("name", newName)); + pvs.addPropertyValue(new PropertyValue("touchy", "valid")); + pvs.addPropertyValue(new PropertyValue("spouse", tbString)); + + bw.setPropertyValues(pvs); + assertThat(tb.getSpouse() != null).as("spouse is non-null").isTrue(); + assertThat(tb.getSpouse().getName().equals("Kerry") && tb.getSpouse().getAge() == 34).as("spouse name is Kerry and age is 34").isTrue(); + ITestBean spouse = tb.getSpouse(); + + bw.setPropertyValues(pvs); + assertThat(tb.getSpouse()).as("Should have remained same object").isSameAs(spouse); + } + + @Test + public void testCustomEditorForSingleProperty() { + TestBean tb = new TestBean(); + BeanWrapper bw = new BeanWrapperImpl(tb); + bw.registerCustomEditor(String.class, "name", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue("prefix" + text); + } + }); + bw.setPropertyValue("name", "value"); + bw.setPropertyValue("touchy", "value"); + assertThat(bw.getPropertyValue("name")).isEqualTo("prefixvalue"); + assertThat(tb.getName()).isEqualTo("prefixvalue"); + assertThat(bw.getPropertyValue("touchy")).isEqualTo("value"); + assertThat(tb.getTouchy()).isEqualTo("value"); + } + + @Test + public void testCustomEditorForAllStringProperties() { + TestBean tb = new TestBean(); + BeanWrapper bw = new BeanWrapperImpl(tb); + bw.registerCustomEditor(String.class, new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue("prefix" + text); + } + }); + bw.setPropertyValue("name", "value"); + bw.setPropertyValue("touchy", "value"); + assertThat(bw.getPropertyValue("name")).isEqualTo("prefixvalue"); + assertThat(tb.getName()).isEqualTo("prefixvalue"); + assertThat(bw.getPropertyValue("touchy")).isEqualTo("prefixvalue"); + assertThat(tb.getTouchy()).isEqualTo("prefixvalue"); + } + + @Test + public void testCustomEditorForSingleNestedProperty() { + TestBean tb = new TestBean(); + tb.setSpouse(new TestBean()); + BeanWrapper bw = new BeanWrapperImpl(tb); + bw.registerCustomEditor(String.class, "spouse.name", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue("prefix" + text); + } + }); + bw.setPropertyValue("spouse.name", "value"); + bw.setPropertyValue("touchy", "value"); + assertThat(bw.getPropertyValue("spouse.name")).isEqualTo("prefixvalue"); + assertThat(tb.getSpouse().getName()).isEqualTo("prefixvalue"); + assertThat(bw.getPropertyValue("touchy")).isEqualTo("value"); + assertThat(tb.getTouchy()).isEqualTo("value"); + } + + @Test + public void testCustomEditorForAllNestedStringProperties() { + TestBean tb = new TestBean(); + tb.setSpouse(new TestBean()); + BeanWrapper bw = new BeanWrapperImpl(tb); + bw.registerCustomEditor(String.class, new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue("prefix" + text); + } + }); + bw.setPropertyValue("spouse.name", "value"); + bw.setPropertyValue("touchy", "value"); + assertThat(bw.getPropertyValue("spouse.name")).isEqualTo("prefixvalue"); + assertThat(tb.getSpouse().getName()).isEqualTo("prefixvalue"); + assertThat(bw.getPropertyValue("touchy")).isEqualTo("prefixvalue"); + assertThat(tb.getTouchy()).isEqualTo("prefixvalue"); + } + + @Test + public void testDefaultBooleanEditorForPrimitiveType() { + BooleanTestBean tb = new BooleanTestBean(); + BeanWrapper bw = new BeanWrapperImpl(tb); + + bw.setPropertyValue("bool1", "true"); + assertThat(Boolean.TRUE.equals(bw.getPropertyValue("bool1"))).as("Correct bool1 value").isTrue(); + assertThat(tb.isBool1()).as("Correct bool1 value").isTrue(); + + bw.setPropertyValue("bool1", "false"); + assertThat(Boolean.FALSE.equals(bw.getPropertyValue("bool1"))).as("Correct bool1 value").isTrue(); + boolean condition4 = !tb.isBool1(); + assertThat(condition4).as("Correct bool1 value").isTrue(); + + bw.setPropertyValue("bool1", " true "); + assertThat(tb.isBool1()).as("Correct bool1 value").isTrue(); + + bw.setPropertyValue("bool1", " false "); + boolean condition3 = !tb.isBool1(); + assertThat(condition3).as("Correct bool1 value").isTrue(); + + bw.setPropertyValue("bool1", "on"); + assertThat(tb.isBool1()).as("Correct bool1 value").isTrue(); + + bw.setPropertyValue("bool1", "off"); + boolean condition2 = !tb.isBool1(); + assertThat(condition2).as("Correct bool1 value").isTrue(); + + bw.setPropertyValue("bool1", "yes"); + assertThat(tb.isBool1()).as("Correct bool1 value").isTrue(); + + bw.setPropertyValue("bool1", "no"); + boolean condition1 = !tb.isBool1(); + assertThat(condition1).as("Correct bool1 value").isTrue(); + + bw.setPropertyValue("bool1", "1"); + assertThat(tb.isBool1()).as("Correct bool1 value").isTrue(); + + bw.setPropertyValue("bool1", "0"); + boolean condition = !tb.isBool1(); + assertThat(condition).as("Correct bool1 value").isTrue(); + + assertThatExceptionOfType(BeansException.class).isThrownBy(() -> + bw.setPropertyValue("bool1", "argh")); + } + + @Test + public void testDefaultBooleanEditorForWrapperType() { + BooleanTestBean tb = new BooleanTestBean(); + BeanWrapper bw = new BeanWrapperImpl(tb); + + bw.setPropertyValue("bool2", "true"); + assertThat(Boolean.TRUE.equals(bw.getPropertyValue("bool2"))).as("Correct bool2 value").isTrue(); + assertThat(tb.getBool2().booleanValue()).as("Correct bool2 value").isTrue(); + + bw.setPropertyValue("bool2", "false"); + assertThat(Boolean.FALSE.equals(bw.getPropertyValue("bool2"))).as("Correct bool2 value").isTrue(); + boolean condition3 = !tb.getBool2().booleanValue(); + assertThat(condition3).as("Correct bool2 value").isTrue(); + + bw.setPropertyValue("bool2", "on"); + assertThat(tb.getBool2().booleanValue()).as("Correct bool2 value").isTrue(); + + bw.setPropertyValue("bool2", "off"); + boolean condition2 = !tb.getBool2().booleanValue(); + assertThat(condition2).as("Correct bool2 value").isTrue(); + + bw.setPropertyValue("bool2", "yes"); + assertThat(tb.getBool2().booleanValue()).as("Correct bool2 value").isTrue(); + + bw.setPropertyValue("bool2", "no"); + boolean condition1 = !tb.getBool2().booleanValue(); + assertThat(condition1).as("Correct bool2 value").isTrue(); + + bw.setPropertyValue("bool2", "1"); + assertThat(tb.getBool2().booleanValue()).as("Correct bool2 value").isTrue(); + + bw.setPropertyValue("bool2", "0"); + boolean condition = !tb.getBool2().booleanValue(); + assertThat(condition).as("Correct bool2 value").isTrue(); + + bw.setPropertyValue("bool2", ""); + assertThat(tb.getBool2()).as("Correct bool2 value").isNull(); + } + + @Test + public void testCustomBooleanEditorWithAllowEmpty() { + BooleanTestBean tb = new BooleanTestBean(); + BeanWrapper bw = new BeanWrapperImpl(tb); + bw.registerCustomEditor(Boolean.class, new CustomBooleanEditor(true)); + + bw.setPropertyValue("bool2", "true"); + assertThat(Boolean.TRUE.equals(bw.getPropertyValue("bool2"))).as("Correct bool2 value").isTrue(); + assertThat(tb.getBool2().booleanValue()).as("Correct bool2 value").isTrue(); + + bw.setPropertyValue("bool2", "false"); + assertThat(Boolean.FALSE.equals(bw.getPropertyValue("bool2"))).as("Correct bool2 value").isTrue(); + boolean condition3 = !tb.getBool2().booleanValue(); + assertThat(condition3).as("Correct bool2 value").isTrue(); + + bw.setPropertyValue("bool2", "on"); + assertThat(tb.getBool2().booleanValue()).as("Correct bool2 value").isTrue(); + + bw.setPropertyValue("bool2", "off"); + boolean condition2 = !tb.getBool2().booleanValue(); + assertThat(condition2).as("Correct bool2 value").isTrue(); + + bw.setPropertyValue("bool2", "yes"); + assertThat(tb.getBool2().booleanValue()).as("Correct bool2 value").isTrue(); + + bw.setPropertyValue("bool2", "no"); + boolean condition1 = !tb.getBool2().booleanValue(); + assertThat(condition1).as("Correct bool2 value").isTrue(); + + bw.setPropertyValue("bool2", "1"); + assertThat(tb.getBool2().booleanValue()).as("Correct bool2 value").isTrue(); + + bw.setPropertyValue("bool2", "0"); + boolean condition = !tb.getBool2().booleanValue(); + assertThat(condition).as("Correct bool2 value").isTrue(); + + bw.setPropertyValue("bool2", ""); + assertThat(bw.getPropertyValue("bool2") == null).as("Correct bool2 value").isTrue(); + assertThat(tb.getBool2() == null).as("Correct bool2 value").isTrue(); + } + + @Test + public void testCustomBooleanEditorWithSpecialTrueAndFalseStrings() throws Exception { + String trueString = "pechorin"; + String falseString = "nash"; + + CustomBooleanEditor editor = new CustomBooleanEditor(trueString, falseString, false); + + editor.setAsText(trueString); + assertThat(((Boolean) editor.getValue()).booleanValue()).isTrue(); + assertThat(editor.getAsText()).isEqualTo(trueString); + editor.setAsText(falseString); + assertThat(((Boolean) editor.getValue()).booleanValue()).isFalse(); + assertThat(editor.getAsText()).isEqualTo(falseString); + + editor.setAsText(trueString.toUpperCase()); + assertThat(((Boolean) editor.getValue()).booleanValue()).isTrue(); + assertThat(editor.getAsText()).isEqualTo(trueString); + editor.setAsText(falseString.toUpperCase()); + assertThat(((Boolean) editor.getValue()).booleanValue()).isFalse(); + assertThat(editor.getAsText()).isEqualTo(falseString); + assertThatIllegalArgumentException().isThrownBy(() -> + editor.setAsText(null)); + } + + @Test + public void testDefaultNumberEditor() { + NumberTestBean tb = new NumberTestBean(); + BeanWrapper bw = new BeanWrapperImpl(tb); + + bw.setPropertyValue("short1", "1"); + bw.setPropertyValue("short2", "2"); + bw.setPropertyValue("int1", "7"); + bw.setPropertyValue("int2", "8"); + bw.setPropertyValue("long1", "5"); + bw.setPropertyValue("long2", "6"); + bw.setPropertyValue("bigInteger", "3"); + bw.setPropertyValue("float1", "7.1"); + bw.setPropertyValue("float2", "8.1"); + bw.setPropertyValue("double1", "5.1"); + bw.setPropertyValue("double2", "6.1"); + bw.setPropertyValue("bigDecimal", "4.5"); + + assertThat(new Short("1").equals(bw.getPropertyValue("short1"))).as("Correct short1 value").isTrue(); + assertThat(tb.getShort1() == 1).as("Correct short1 value").isTrue(); + assertThat(new Short("2").equals(bw.getPropertyValue("short2"))).as("Correct short2 value").isTrue(); + assertThat(new Short("2").equals(tb.getShort2())).as("Correct short2 value").isTrue(); + assertThat(new Integer("7").equals(bw.getPropertyValue("int1"))).as("Correct int1 value").isTrue(); + assertThat(tb.getInt1() == 7).as("Correct int1 value").isTrue(); + assertThat(new Integer("8").equals(bw.getPropertyValue("int2"))).as("Correct int2 value").isTrue(); + assertThat(new Integer("8").equals(tb.getInt2())).as("Correct int2 value").isTrue(); + assertThat(new Long("5").equals(bw.getPropertyValue("long1"))).as("Correct long1 value").isTrue(); + assertThat(tb.getLong1() == 5).as("Correct long1 value").isTrue(); + assertThat(new Long("6").equals(bw.getPropertyValue("long2"))).as("Correct long2 value").isTrue(); + assertThat(new Long("6").equals(tb.getLong2())).as("Correct long2 value").isTrue(); + assertThat(new BigInteger("3").equals(bw.getPropertyValue("bigInteger"))).as("Correct bigInteger value").isTrue(); + assertThat(new BigInteger("3").equals(tb.getBigInteger())).as("Correct bigInteger value").isTrue(); + assertThat(new Float("7.1").equals(bw.getPropertyValue("float1"))).as("Correct float1 value").isTrue(); + assertThat(new Float("7.1").equals(new Float(tb.getFloat1()))).as("Correct float1 value").isTrue(); + assertThat(new Float("8.1").equals(bw.getPropertyValue("float2"))).as("Correct float2 value").isTrue(); + assertThat(new Float("8.1").equals(tb.getFloat2())).as("Correct float2 value").isTrue(); + assertThat(new Double("5.1").equals(bw.getPropertyValue("double1"))).as("Correct double1 value").isTrue(); + assertThat(tb.getDouble1() == 5.1).as("Correct double1 value").isTrue(); + assertThat(new Double("6.1").equals(bw.getPropertyValue("double2"))).as("Correct double2 value").isTrue(); + assertThat(new Double("6.1").equals(tb.getDouble2())).as("Correct double2 value").isTrue(); + assertThat(new BigDecimal("4.5").equals(bw.getPropertyValue("bigDecimal"))).as("Correct bigDecimal value").isTrue(); + assertThat(new BigDecimal("4.5").equals(tb.getBigDecimal())).as("Correct bigDecimal value").isTrue(); + } + + @Test + public void testCustomNumberEditorWithoutAllowEmpty() { + NumberFormat nf = NumberFormat.getNumberInstance(Locale.GERMAN); + NumberTestBean tb = new NumberTestBean(); + BeanWrapper bw = new BeanWrapperImpl(tb); + bw.registerCustomEditor(short.class, new CustomNumberEditor(Short.class, nf, false)); + bw.registerCustomEditor(Short.class, new CustomNumberEditor(Short.class, nf, false)); + bw.registerCustomEditor(int.class, new CustomNumberEditor(Integer.class, nf, false)); + bw.registerCustomEditor(Integer.class, new CustomNumberEditor(Integer.class, nf, false)); + bw.registerCustomEditor(long.class, new CustomNumberEditor(Long.class, nf, false)); + bw.registerCustomEditor(Long.class, new CustomNumberEditor(Long.class, nf, false)); + bw.registerCustomEditor(BigInteger.class, new CustomNumberEditor(BigInteger.class, nf, false)); + bw.registerCustomEditor(float.class, new CustomNumberEditor(Float.class, nf, false)); + bw.registerCustomEditor(Float.class, new CustomNumberEditor(Float.class, nf, false)); + bw.registerCustomEditor(double.class, new CustomNumberEditor(Double.class, nf, false)); + bw.registerCustomEditor(Double.class, new CustomNumberEditor(Double.class, nf, false)); + bw.registerCustomEditor(BigDecimal.class, new CustomNumberEditor(BigDecimal.class, nf, false)); + + bw.setPropertyValue("short1", "1"); + bw.setPropertyValue("short2", "2"); + bw.setPropertyValue("int1", "7"); + bw.setPropertyValue("int2", "8"); + bw.setPropertyValue("long1", "5"); + bw.setPropertyValue("long2", "6"); + bw.setPropertyValue("bigInteger", "3"); + bw.setPropertyValue("float1", "7,1"); + bw.setPropertyValue("float2", "8,1"); + bw.setPropertyValue("double1", "5,1"); + bw.setPropertyValue("double2", "6,1"); + bw.setPropertyValue("bigDecimal", "4,5"); + + assertThat(new Short("1").equals(bw.getPropertyValue("short1"))).as("Correct short1 value").isTrue(); + assertThat(tb.getShort1() == 1).as("Correct short1 value").isTrue(); + assertThat(new Short("2").equals(bw.getPropertyValue("short2"))).as("Correct short2 value").isTrue(); + assertThat(new Short("2").equals(tb.getShort2())).as("Correct short2 value").isTrue(); + assertThat(new Integer("7").equals(bw.getPropertyValue("int1"))).as("Correct int1 value").isTrue(); + assertThat(tb.getInt1() == 7).as("Correct int1 value").isTrue(); + assertThat(new Integer("8").equals(bw.getPropertyValue("int2"))).as("Correct int2 value").isTrue(); + assertThat(new Integer("8").equals(tb.getInt2())).as("Correct int2 value").isTrue(); + assertThat(new Long("5").equals(bw.getPropertyValue("long1"))).as("Correct long1 value").isTrue(); + assertThat(tb.getLong1() == 5).as("Correct long1 value").isTrue(); + assertThat(new Long("6").equals(bw.getPropertyValue("long2"))).as("Correct long2 value").isTrue(); + assertThat(new Long("6").equals(tb.getLong2())).as("Correct long2 value").isTrue(); + assertThat(new BigInteger("3").equals(bw.getPropertyValue("bigInteger"))).as("Correct bigInteger value").isTrue(); + assertThat(new BigInteger("3").equals(tb.getBigInteger())).as("Correct bigInteger value").isTrue(); + assertThat(new Float("7.1").equals(bw.getPropertyValue("float1"))).as("Correct float1 value").isTrue(); + assertThat(new Float("7.1").equals(new Float(tb.getFloat1()))).as("Correct float1 value").isTrue(); + assertThat(new Float("8.1").equals(bw.getPropertyValue("float2"))).as("Correct float2 value").isTrue(); + assertThat(new Float("8.1").equals(tb.getFloat2())).as("Correct float2 value").isTrue(); + assertThat(new Double("5.1").equals(bw.getPropertyValue("double1"))).as("Correct double1 value").isTrue(); + assertThat(tb.getDouble1() == 5.1).as("Correct double1 value").isTrue(); + assertThat(new Double("6.1").equals(bw.getPropertyValue("double2"))).as("Correct double2 value").isTrue(); + assertThat(new Double("6.1").equals(tb.getDouble2())).as("Correct double2 value").isTrue(); + assertThat(new BigDecimal("4.5").equals(bw.getPropertyValue("bigDecimal"))).as("Correct bigDecimal value").isTrue(); + assertThat(new BigDecimal("4.5").equals(tb.getBigDecimal())).as("Correct bigDecimal value").isTrue(); + } + + @Test + public void testCustomNumberEditorWithAllowEmpty() { + NumberFormat nf = NumberFormat.getNumberInstance(Locale.GERMAN); + NumberTestBean tb = new NumberTestBean(); + BeanWrapper bw = new BeanWrapperImpl(tb); + bw.registerCustomEditor(long.class, new CustomNumberEditor(Long.class, nf, true)); + bw.registerCustomEditor(Long.class, new CustomNumberEditor(Long.class, nf, true)); + + bw.setPropertyValue("long1", "5"); + bw.setPropertyValue("long2", "6"); + assertThat(new Long("5").equals(bw.getPropertyValue("long1"))).as("Correct long1 value").isTrue(); + assertThat(tb.getLong1() == 5).as("Correct long1 value").isTrue(); + assertThat(new Long("6").equals(bw.getPropertyValue("long2"))).as("Correct long2 value").isTrue(); + assertThat(new Long("6").equals(tb.getLong2())).as("Correct long2 value").isTrue(); + + bw.setPropertyValue("long2", ""); + assertThat(bw.getPropertyValue("long2") == null).as("Correct long2 value").isTrue(); + assertThat(tb.getLong2() == null).as("Correct long2 value").isTrue(); + assertThatExceptionOfType(BeansException.class).isThrownBy(() -> + bw.setPropertyValue("long1", "")); + assertThat(bw.getPropertyValue("long1")).isEqualTo(5L); + assertThat(tb.getLong1()).isEqualTo(5); + } + + @Test + public void testCustomNumberEditorWithFrenchBigDecimal() throws Exception { + NumberFormat nf = NumberFormat.getNumberInstance(Locale.FRENCH); + NumberTestBean tb = new NumberTestBean(); + BeanWrapper bw = new BeanWrapperImpl(tb); + bw.registerCustomEditor(BigDecimal.class, new CustomNumberEditor(BigDecimal.class, nf, true)); + bw.setPropertyValue("bigDecimal", "1000"); + assertThat(tb.getBigDecimal().floatValue()).isCloseTo(1000.0f, within(0f)); + + bw.setPropertyValue("bigDecimal", "1000,5"); + assertThat(tb.getBigDecimal().floatValue()).isCloseTo(1000.5f, within(0f)); + + bw.setPropertyValue("bigDecimal", "1 000,5"); + assertThat(tb.getBigDecimal().floatValue()).isCloseTo(1000.5f, within(0f)); + + } + + @Test + public void testParseShortGreaterThanMaxValueWithoutNumberFormat() { + CustomNumberEditor editor = new CustomNumberEditor(Short.class, true); + assertThatExceptionOfType(NumberFormatException.class).as("greater than Short.MAX_VALUE + 1").isThrownBy(() -> + editor.setAsText(String.valueOf(Short.MAX_VALUE + 1))); + } + + @Test + public void testByteArrayPropertyEditor() { + PrimitiveArrayBean bean = new PrimitiveArrayBean(); + BeanWrapper bw = new BeanWrapperImpl(bean); + bw.setPropertyValue("byteArray", "myvalue"); + assertThat(new String(bean.getByteArray())).isEqualTo("myvalue"); + } + + @Test + public void testCharArrayPropertyEditor() { + PrimitiveArrayBean bean = new PrimitiveArrayBean(); + BeanWrapper bw = new BeanWrapperImpl(bean); + bw.setPropertyValue("charArray", "myvalue"); + assertThat(new String(bean.getCharArray())).isEqualTo("myvalue"); + } + + @Test + public void testCharacterEditor() { + CharBean cb = new CharBean(); + BeanWrapper bw = new BeanWrapperImpl(cb); + + bw.setPropertyValue("myChar", new Character('c')); + assertThat(cb.getMyChar()).isEqualTo('c'); + + bw.setPropertyValue("myChar", "c"); + assertThat(cb.getMyChar()).isEqualTo('c'); + + bw.setPropertyValue("myChar", "\u0041"); + assertThat(cb.getMyChar()).isEqualTo('A'); + + bw.setPropertyValue("myChar", "\\u0022"); + assertThat(cb.getMyChar()).isEqualTo('"'); + + CharacterEditor editor = new CharacterEditor(false); + editor.setAsText("M"); + assertThat(editor.getAsText()).isEqualTo("M"); + } + + @Test + public void testCharacterEditorWithAllowEmpty() { + CharBean cb = new CharBean(); + BeanWrapper bw = new BeanWrapperImpl(cb); + bw.registerCustomEditor(Character.class, new CharacterEditor(true)); + + bw.setPropertyValue("myCharacter", new Character('c')); + assertThat(cb.getMyCharacter()).isEqualTo(new Character('c')); + + bw.setPropertyValue("myCharacter", "c"); + assertThat(cb.getMyCharacter()).isEqualTo(new Character('c')); + + bw.setPropertyValue("myCharacter", "\u0041"); + assertThat(cb.getMyCharacter()).isEqualTo(new Character('A')); + + bw.setPropertyValue("myCharacter", " "); + assertThat(cb.getMyCharacter()).isEqualTo(new Character(' ')); + + bw.setPropertyValue("myCharacter", ""); + assertThat(cb.getMyCharacter()).isNull(); + } + + @Test + public void testCharacterEditorSetAsTextWithStringLongerThanOneCharacter() throws Exception { + PropertyEditor charEditor = new CharacterEditor(false); + assertThatIllegalArgumentException().isThrownBy(() -> + charEditor.setAsText("ColdWaterCanyon")); + } + + @Test + public void testCharacterEditorGetAsTextReturnsEmptyStringIfValueIsNull() throws Exception { + PropertyEditor charEditor = new CharacterEditor(false); + assertThat(charEditor.getAsText()).isEqualTo(""); + charEditor = new CharacterEditor(true); + charEditor.setAsText(null); + assertThat(charEditor.getAsText()).isEqualTo(""); + charEditor.setAsText(""); + assertThat(charEditor.getAsText()).isEqualTo(""); + charEditor.setAsText(" "); + assertThat(charEditor.getAsText()).isEqualTo(" "); + } + + @Test + public void testCharacterEditorSetAsTextWithNullNotAllowingEmptyAsNull() throws Exception { + PropertyEditor charEditor = new CharacterEditor(false); + assertThatIllegalArgumentException().isThrownBy(() -> + charEditor.setAsText(null)); + } + + @Test + public void testClassEditor() { + PropertyEditor classEditor = new ClassEditor(); + classEditor.setAsText(TestBean.class.getName()); + assertThat(classEditor.getValue()).isEqualTo(TestBean.class); + assertThat(classEditor.getAsText()).isEqualTo(TestBean.class.getName()); + + classEditor.setAsText(null); + assertThat(classEditor.getAsText()).isEqualTo(""); + classEditor.setAsText(""); + assertThat(classEditor.getAsText()).isEqualTo(""); + classEditor.setAsText("\t "); + assertThat(classEditor.getAsText()).isEqualTo(""); + } + + @Test + public void testClassEditorWithNonExistentClass() throws Exception { + PropertyEditor classEditor = new ClassEditor(); + assertThatIllegalArgumentException().isThrownBy(() -> + classEditor.setAsText("hairdresser.on.Fire")); + } + + @Test + public void testClassEditorWithArray() { + PropertyEditor classEditor = new ClassEditor(); + classEditor.setAsText("org.springframework.beans.testfixture.beans.TestBean[]"); + assertThat(classEditor.getValue()).isEqualTo(TestBean[].class); + assertThat(classEditor.getAsText()).isEqualTo("org.springframework.beans.testfixture.beans.TestBean[]"); + } + + /* + * SPR_2165 - ClassEditor is inconsistent with multidimensional arrays + */ + @Test + public void testGetAsTextWithTwoDimensionalArray() throws Exception { + String[][] chessboard = new String[8][8]; + ClassEditor editor = new ClassEditor(); + editor.setValue(chessboard.getClass()); + assertThat(editor.getAsText()).isEqualTo("java.lang.String[][]"); + } + + /* + * SPR_2165 - ClassEditor is inconsistent with multidimensional arrays + */ + @Test + public void testGetAsTextWithRidiculousMultiDimensionalArray() throws Exception { + String[][][][][] ridiculousChessboard = new String[8][4][0][1][3]; + ClassEditor editor = new ClassEditor(); + editor.setValue(ridiculousChessboard.getClass()); + assertThat(editor.getAsText()).isEqualTo("java.lang.String[][][][][]"); + } + + @Test + public void testFileEditor() { + PropertyEditor fileEditor = new FileEditor(); + fileEditor.setAsText("file:myfile.txt"); + assertThat(fileEditor.getValue()).isEqualTo(new File("myfile.txt")); + assertThat(fileEditor.getAsText()).isEqualTo((new File("myfile.txt")).getPath()); + } + + @Test + public void testFileEditorWithRelativePath() { + PropertyEditor fileEditor = new FileEditor(); + try { + fileEditor.setAsText("myfile.txt"); + } + catch (IllegalArgumentException ex) { + // expected: should get resolved as class path resource, + // and there is no such resource in the class path... + } + } + + @Test + public void testFileEditorWithAbsolutePath() { + PropertyEditor fileEditor = new FileEditor(); + // testing on Windows + if (new File("C:/myfile.txt").isAbsolute()) { + fileEditor.setAsText("C:/myfile.txt"); + assertThat(fileEditor.getValue()).isEqualTo(new File("C:/myfile.txt")); + } + // testing on Unix + if (new File("/myfile.txt").isAbsolute()) { + fileEditor.setAsText("/myfile.txt"); + assertThat(fileEditor.getValue()).isEqualTo(new File("/myfile.txt")); + } + } + + @Test + public void testLocaleEditor() { + PropertyEditor localeEditor = new LocaleEditor(); + localeEditor.setAsText("en_CA"); + assertThat(localeEditor.getValue()).isEqualTo(Locale.CANADA); + assertThat(localeEditor.getAsText()).isEqualTo("en_CA"); + + localeEditor = new LocaleEditor(); + assertThat(localeEditor.getAsText()).isEqualTo(""); + } + + @Test + public void testPatternEditor() { + final String REGEX = "a.*"; + + PropertyEditor patternEditor = new PatternEditor(); + patternEditor.setAsText(REGEX); + assertThat(((Pattern) patternEditor.getValue()).pattern()).isEqualTo(Pattern.compile(REGEX).pattern()); + assertThat(patternEditor.getAsText()).isEqualTo(REGEX); + + patternEditor = new PatternEditor(); + assertThat(patternEditor.getAsText()).isEqualTo(""); + + patternEditor = new PatternEditor(); + patternEditor.setAsText(null); + assertThat(patternEditor.getAsText()).isEqualTo(""); + } + + @Test + public void testCustomBooleanEditor() { + CustomBooleanEditor editor = new CustomBooleanEditor(false); + + editor.setAsText("true"); + assertThat(editor.getValue()).isEqualTo(Boolean.TRUE); + assertThat(editor.getAsText()).isEqualTo("true"); + + editor.setAsText("false"); + assertThat(editor.getValue()).isEqualTo(Boolean.FALSE); + assertThat(editor.getAsText()).isEqualTo("false"); + + editor.setValue(null); + assertThat(editor.getValue()).isEqualTo(null); + assertThat(editor.getAsText()).isEqualTo(""); + + assertThatIllegalArgumentException().isThrownBy(() -> + editor.setAsText(null)); + } + + @Test + public void testCustomBooleanEditorWithEmptyAsNull() { + CustomBooleanEditor editor = new CustomBooleanEditor(true); + + editor.setAsText("true"); + assertThat(editor.getValue()).isEqualTo(Boolean.TRUE); + assertThat(editor.getAsText()).isEqualTo("true"); + + editor.setAsText("false"); + assertThat(editor.getValue()).isEqualTo(Boolean.FALSE); + assertThat(editor.getAsText()).isEqualTo("false"); + + editor.setValue(null); + assertThat(editor.getValue()).isEqualTo(null); + assertThat(editor.getAsText()).isEqualTo(""); + } + + @Test + public void testCustomDateEditor() { + CustomDateEditor editor = new CustomDateEditor(null, false); + editor.setValue(null); + assertThat(editor.getValue()).isEqualTo(null); + assertThat(editor.getAsText()).isEqualTo(""); + } + + @Test + public void testCustomDateEditorWithEmptyAsNull() { + CustomDateEditor editor = new CustomDateEditor(null, true); + editor.setValue(null); + assertThat(editor.getValue()).isEqualTo(null); + assertThat(editor.getAsText()).isEqualTo(""); + } + + @Test + public void testCustomDateEditorWithExactDateLength() { + int maxLength = 10; + String validDate = "01/01/2005"; + String invalidDate = "01/01/05"; + + assertThat(validDate.length() == maxLength).isTrue(); + assertThat(invalidDate.length() == maxLength).isFalse(); + + CustomDateEditor editor = new CustomDateEditor(new SimpleDateFormat("MM/dd/yyyy"), true, maxLength); + editor.setAsText(validDate); + assertThatIllegalArgumentException().isThrownBy(() -> + editor.setAsText(invalidDate)) + .withMessageContaining("10"); + } + + @Test + public void testCustomNumberEditor() { + CustomNumberEditor editor = new CustomNumberEditor(Integer.class, false); + editor.setAsText("5"); + assertThat(editor.getValue()).isEqualTo(5); + assertThat(editor.getAsText()).isEqualTo("5"); + editor.setValue(null); + assertThat(editor.getValue()).isEqualTo(null); + assertThat(editor.getAsText()).isEqualTo(""); + } + + @Test + public void testCustomNumberEditorWithHex() { + CustomNumberEditor editor = new CustomNumberEditor(Integer.class, false); + editor.setAsText("0x" + Integer.toHexString(64)); + assertThat(editor.getValue()).isEqualTo(64); + } + + @Test + public void testCustomNumberEditorWithEmptyAsNull() { + CustomNumberEditor editor = new CustomNumberEditor(Integer.class, true); + editor.setAsText("5"); + assertThat(editor.getValue()).isEqualTo(5); + assertThat(editor.getAsText()).isEqualTo("5"); + editor.setAsText(""); + assertThat(editor.getValue()).isEqualTo(null); + assertThat(editor.getAsText()).isEqualTo(""); + editor.setValue(null); + assertThat(editor.getValue()).isEqualTo(null); + assertThat(editor.getAsText()).isEqualTo(""); + } + + @Test + public void testStringTrimmerEditor() { + StringTrimmerEditor editor = new StringTrimmerEditor(false); + editor.setAsText("test"); + assertThat(editor.getValue()).isEqualTo("test"); + assertThat(editor.getAsText()).isEqualTo("test"); + editor.setAsText(" test "); + assertThat(editor.getValue()).isEqualTo("test"); + assertThat(editor.getAsText()).isEqualTo("test"); + editor.setAsText(""); + assertThat(editor.getValue()).isEqualTo(""); + assertThat(editor.getAsText()).isEqualTo(""); + editor.setValue(null); + assertThat(editor.getAsText()).isEqualTo(""); + editor.setAsText(null); + assertThat(editor.getAsText()).isEqualTo(""); + } + + @Test + public void testStringTrimmerEditorWithEmptyAsNull() { + StringTrimmerEditor editor = new StringTrimmerEditor(true); + editor.setAsText("test"); + assertThat(editor.getValue()).isEqualTo("test"); + assertThat(editor.getAsText()).isEqualTo("test"); + editor.setAsText(" test "); + assertThat(editor.getValue()).isEqualTo("test"); + assertThat(editor.getAsText()).isEqualTo("test"); + editor.setAsText(" "); + assertThat(editor.getValue()).isEqualTo(null); + assertThat(editor.getAsText()).isEqualTo(""); + editor.setValue(null); + assertThat(editor.getAsText()).isEqualTo(""); + } + + @Test + public void testStringTrimmerEditorWithCharsToDelete() { + StringTrimmerEditor editor = new StringTrimmerEditor("\r\n\f", false); + editor.setAsText("te\ns\ft"); + assertThat(editor.getValue()).isEqualTo("test"); + assertThat(editor.getAsText()).isEqualTo("test"); + editor.setAsText(" test "); + assertThat(editor.getValue()).isEqualTo("test"); + assertThat(editor.getAsText()).isEqualTo("test"); + editor.setAsText(""); + assertThat(editor.getValue()).isEqualTo(""); + assertThat(editor.getAsText()).isEqualTo(""); + editor.setValue(null); + assertThat(editor.getAsText()).isEqualTo(""); + } + + @Test + public void testStringTrimmerEditorWithCharsToDeleteAndEmptyAsNull() { + StringTrimmerEditor editor = new StringTrimmerEditor("\r\n\f", true); + editor.setAsText("te\ns\ft"); + assertThat(editor.getValue()).isEqualTo("test"); + assertThat(editor.getAsText()).isEqualTo("test"); + editor.setAsText(" test "); + assertThat(editor.getValue()).isEqualTo("test"); + assertThat(editor.getAsText()).isEqualTo("test"); + editor.setAsText(" \n\f "); + assertThat(editor.getValue()).isEqualTo(null); + assertThat(editor.getAsText()).isEqualTo(""); + editor.setValue(null); + assertThat(editor.getAsText()).isEqualTo(""); + } + + @Test + public void testIndexedPropertiesWithCustomEditorForType() { + IndexedTestBean bean = new IndexedTestBean(); + BeanWrapper bw = new BeanWrapperImpl(bean); + bw.registerCustomEditor(String.class, new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue("prefix" + text); + } + }); + TestBean tb0 = bean.getArray()[0]; + TestBean tb1 = bean.getArray()[1]; + TestBean tb2 = ((TestBean) bean.getList().get(0)); + TestBean tb3 = ((TestBean) bean.getList().get(1)); + TestBean tb4 = ((TestBean) bean.getMap().get("key1")); + TestBean tb5 = ((TestBean) bean.getMap().get("key2")); + assertThat(tb0.getName()).isEqualTo("name0"); + assertThat(tb1.getName()).isEqualTo("name1"); + assertThat(tb2.getName()).isEqualTo("name2"); + assertThat(tb3.getName()).isEqualTo("name3"); + assertThat(tb4.getName()).isEqualTo("name4"); + assertThat(tb5.getName()).isEqualTo("name5"); + assertThat(bw.getPropertyValue("array[0].name")).isEqualTo("name0"); + assertThat(bw.getPropertyValue("array[1].name")).isEqualTo("name1"); + assertThat(bw.getPropertyValue("list[0].name")).isEqualTo("name2"); + assertThat(bw.getPropertyValue("list[1].name")).isEqualTo("name3"); + assertThat(bw.getPropertyValue("map[key1].name")).isEqualTo("name4"); + assertThat(bw.getPropertyValue("map[key2].name")).isEqualTo("name5"); + assertThat(bw.getPropertyValue("map['key1'].name")).isEqualTo("name4"); + assertThat(bw.getPropertyValue("map[\"key2\"].name")).isEqualTo("name5"); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("array[0].name", "name5"); + pvs.add("array[1].name", "name4"); + pvs.add("list[0].name", "name3"); + pvs.add("list[1].name", "name2"); + pvs.add("map[key1].name", "name1"); + pvs.add("map['key2'].name", "name0"); + bw.setPropertyValues(pvs); + assertThat(tb0.getName()).isEqualTo("prefixname5"); + assertThat(tb1.getName()).isEqualTo("prefixname4"); + assertThat(tb2.getName()).isEqualTo("prefixname3"); + assertThat(tb3.getName()).isEqualTo("prefixname2"); + assertThat(tb4.getName()).isEqualTo("prefixname1"); + assertThat(tb5.getName()).isEqualTo("prefixname0"); + assertThat(bw.getPropertyValue("array[0].name")).isEqualTo("prefixname5"); + assertThat(bw.getPropertyValue("array[1].name")).isEqualTo("prefixname4"); + assertThat(bw.getPropertyValue("list[0].name")).isEqualTo("prefixname3"); + assertThat(bw.getPropertyValue("list[1].name")).isEqualTo("prefixname2"); + assertThat(bw.getPropertyValue("map[\"key1\"].name")).isEqualTo("prefixname1"); + assertThat(bw.getPropertyValue("map['key2'].name")).isEqualTo("prefixname0"); + } + + @Test + public void testIndexedPropertiesWithCustomEditorForProperty() { + IndexedTestBean bean = new IndexedTestBean(false); + BeanWrapper bw = new BeanWrapperImpl(bean); + bw.registerCustomEditor(String.class, "array.name", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue("array" + text); + } + }); + bw.registerCustomEditor(String.class, "list.name", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue("list" + text); + } + }); + bw.registerCustomEditor(String.class, "map.name", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue("map" + text); + } + }); + bean.populate(); + + TestBean tb0 = bean.getArray()[0]; + TestBean tb1 = bean.getArray()[1]; + TestBean tb2 = ((TestBean) bean.getList().get(0)); + TestBean tb3 = ((TestBean) bean.getList().get(1)); + TestBean tb4 = ((TestBean) bean.getMap().get("key1")); + TestBean tb5 = ((TestBean) bean.getMap().get("key2")); + assertThat(tb0.getName()).isEqualTo("name0"); + assertThat(tb1.getName()).isEqualTo("name1"); + assertThat(tb2.getName()).isEqualTo("name2"); + assertThat(tb3.getName()).isEqualTo("name3"); + assertThat(tb4.getName()).isEqualTo("name4"); + assertThat(tb5.getName()).isEqualTo("name5"); + assertThat(bw.getPropertyValue("array[0].name")).isEqualTo("name0"); + assertThat(bw.getPropertyValue("array[1].name")).isEqualTo("name1"); + assertThat(bw.getPropertyValue("list[0].name")).isEqualTo("name2"); + assertThat(bw.getPropertyValue("list[1].name")).isEqualTo("name3"); + assertThat(bw.getPropertyValue("map[key1].name")).isEqualTo("name4"); + assertThat(bw.getPropertyValue("map[key2].name")).isEqualTo("name5"); + assertThat(bw.getPropertyValue("map['key1'].name")).isEqualTo("name4"); + assertThat(bw.getPropertyValue("map[\"key2\"].name")).isEqualTo("name5"); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("array[0].name", "name5"); + pvs.add("array[1].name", "name4"); + pvs.add("list[0].name", "name3"); + pvs.add("list[1].name", "name2"); + pvs.add("map[key1].name", "name1"); + pvs.add("map['key2'].name", "name0"); + bw.setPropertyValues(pvs); + assertThat(tb0.getName()).isEqualTo("arrayname5"); + assertThat(tb1.getName()).isEqualTo("arrayname4"); + assertThat(tb2.getName()).isEqualTo("listname3"); + assertThat(tb3.getName()).isEqualTo("listname2"); + assertThat(tb4.getName()).isEqualTo("mapname1"); + assertThat(tb5.getName()).isEqualTo("mapname0"); + assertThat(bw.getPropertyValue("array[0].name")).isEqualTo("arrayname5"); + assertThat(bw.getPropertyValue("array[1].name")).isEqualTo("arrayname4"); + assertThat(bw.getPropertyValue("list[0].name")).isEqualTo("listname3"); + assertThat(bw.getPropertyValue("list[1].name")).isEqualTo("listname2"); + assertThat(bw.getPropertyValue("map[\"key1\"].name")).isEqualTo("mapname1"); + assertThat(bw.getPropertyValue("map['key2'].name")).isEqualTo("mapname0"); + } + + @Test + public void testIndexedPropertiesWithIndividualCustomEditorForProperty() { + IndexedTestBean bean = new IndexedTestBean(false); + BeanWrapper bw = new BeanWrapperImpl(bean); + bw.registerCustomEditor(String.class, "array[0].name", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue("array0" + text); + } + }); + bw.registerCustomEditor(String.class, "array[1].name", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue("array1" + text); + } + }); + bw.registerCustomEditor(String.class, "list[0].name", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue("list0" + text); + } + }); + bw.registerCustomEditor(String.class, "list[1].name", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue("list1" + text); + } + }); + bw.registerCustomEditor(String.class, "map[key1].name", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue("mapkey1" + text); + } + }); + bw.registerCustomEditor(String.class, "map[key2].name", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue("mapkey2" + text); + } + }); + bean.populate(); + + TestBean tb0 = bean.getArray()[0]; + TestBean tb1 = bean.getArray()[1]; + TestBean tb2 = ((TestBean) bean.getList().get(0)); + TestBean tb3 = ((TestBean) bean.getList().get(1)); + TestBean tb4 = ((TestBean) bean.getMap().get("key1")); + TestBean tb5 = ((TestBean) bean.getMap().get("key2")); + assertThat(tb0.getName()).isEqualTo("name0"); + assertThat(tb1.getName()).isEqualTo("name1"); + assertThat(tb2.getName()).isEqualTo("name2"); + assertThat(tb3.getName()).isEqualTo("name3"); + assertThat(tb4.getName()).isEqualTo("name4"); + assertThat(tb5.getName()).isEqualTo("name5"); + assertThat(bw.getPropertyValue("array[0].name")).isEqualTo("name0"); + assertThat(bw.getPropertyValue("array[1].name")).isEqualTo("name1"); + assertThat(bw.getPropertyValue("list[0].name")).isEqualTo("name2"); + assertThat(bw.getPropertyValue("list[1].name")).isEqualTo("name3"); + assertThat(bw.getPropertyValue("map[key1].name")).isEqualTo("name4"); + assertThat(bw.getPropertyValue("map[key2].name")).isEqualTo("name5"); + assertThat(bw.getPropertyValue("map['key1'].name")).isEqualTo("name4"); + assertThat(bw.getPropertyValue("map[\"key2\"].name")).isEqualTo("name5"); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("array[0].name", "name5"); + pvs.add("array[1].name", "name4"); + pvs.add("list[0].name", "name3"); + pvs.add("list[1].name", "name2"); + pvs.add("map[key1].name", "name1"); + pvs.add("map['key2'].name", "name0"); + bw.setPropertyValues(pvs); + assertThat(tb0.getName()).isEqualTo("array0name5"); + assertThat(tb1.getName()).isEqualTo("array1name4"); + assertThat(tb2.getName()).isEqualTo("list0name3"); + assertThat(tb3.getName()).isEqualTo("list1name2"); + assertThat(tb4.getName()).isEqualTo("mapkey1name1"); + assertThat(tb5.getName()).isEqualTo("mapkey2name0"); + assertThat(bw.getPropertyValue("array[0].name")).isEqualTo("array0name5"); + assertThat(bw.getPropertyValue("array[1].name")).isEqualTo("array1name4"); + assertThat(bw.getPropertyValue("list[0].name")).isEqualTo("list0name3"); + assertThat(bw.getPropertyValue("list[1].name")).isEqualTo("list1name2"); + assertThat(bw.getPropertyValue("map[\"key1\"].name")).isEqualTo("mapkey1name1"); + assertThat(bw.getPropertyValue("map['key2'].name")).isEqualTo("mapkey2name0"); + } + + @Test + public void testNestedIndexedPropertiesWithCustomEditorForProperty() { + IndexedTestBean bean = new IndexedTestBean(); + TestBean tb0 = bean.getArray()[0]; + TestBean tb1 = bean.getArray()[1]; + TestBean tb2 = ((TestBean) bean.getList().get(0)); + TestBean tb3 = ((TestBean) bean.getList().get(1)); + TestBean tb4 = ((TestBean) bean.getMap().get("key1")); + TestBean tb5 = ((TestBean) bean.getMap().get("key2")); + tb0.setNestedIndexedBean(new IndexedTestBean()); + tb1.setNestedIndexedBean(new IndexedTestBean()); + tb2.setNestedIndexedBean(new IndexedTestBean()); + tb3.setNestedIndexedBean(new IndexedTestBean()); + tb4.setNestedIndexedBean(new IndexedTestBean()); + tb5.setNestedIndexedBean(new IndexedTestBean()); + BeanWrapper bw = new BeanWrapperImpl(bean); + bw.registerCustomEditor(String.class, "array.nestedIndexedBean.array.name", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue("array" + text); + } + + @Override + public String getAsText() { + return ((String) getValue()).substring(5); + } + }); + bw.registerCustomEditor(String.class, "list.nestedIndexedBean.list.name", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue("list" + text); + } + + @Override + public String getAsText() { + return ((String) getValue()).substring(4); + } + }); + bw.registerCustomEditor(String.class, "map.nestedIndexedBean.map.name", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue("map" + text); + } + + @Override + public String getAsText() { + return ((String) getValue()).substring(4); + } + }); + assertThat(tb0.getName()).isEqualTo("name0"); + assertThat(tb1.getName()).isEqualTo("name1"); + assertThat(tb2.getName()).isEqualTo("name2"); + assertThat(tb3.getName()).isEqualTo("name3"); + assertThat(tb4.getName()).isEqualTo("name4"); + assertThat(tb5.getName()).isEqualTo("name5"); + assertThat(bw.getPropertyValue("array[0].nestedIndexedBean.array[0].name")).isEqualTo("name0"); + assertThat(bw.getPropertyValue("array[1].nestedIndexedBean.array[1].name")).isEqualTo("name1"); + assertThat(bw.getPropertyValue("list[0].nestedIndexedBean.list[0].name")).isEqualTo("name2"); + assertThat(bw.getPropertyValue("list[1].nestedIndexedBean.list[1].name")).isEqualTo("name3"); + assertThat(bw.getPropertyValue("map[key1].nestedIndexedBean.map[key1].name")).isEqualTo("name4"); + assertThat(bw.getPropertyValue("map['key2'].nestedIndexedBean.map[\"key2\"].name")).isEqualTo("name5"); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("array[0].nestedIndexedBean.array[0].name", "name5"); + pvs.add("array[1].nestedIndexedBean.array[1].name", "name4"); + pvs.add("list[0].nestedIndexedBean.list[0].name", "name3"); + pvs.add("list[1].nestedIndexedBean.list[1].name", "name2"); + pvs.add("map[key1].nestedIndexedBean.map[\"key1\"].name", "name1"); + pvs.add("map['key2'].nestedIndexedBean.map[key2].name", "name0"); + bw.setPropertyValues(pvs); + assertThat(tb0.getNestedIndexedBean().getArray()[0].getName()).isEqualTo("arrayname5"); + assertThat(tb1.getNestedIndexedBean().getArray()[1].getName()).isEqualTo("arrayname4"); + assertThat(((TestBean) tb2.getNestedIndexedBean().getList().get(0)).getName()).isEqualTo("listname3"); + assertThat(((TestBean) tb3.getNestedIndexedBean().getList().get(1)).getName()).isEqualTo("listname2"); + assertThat(((TestBean) tb4.getNestedIndexedBean().getMap().get("key1")).getName()).isEqualTo("mapname1"); + assertThat(((TestBean) tb5.getNestedIndexedBean().getMap().get("key2")).getName()).isEqualTo("mapname0"); + assertThat(bw.getPropertyValue("array[0].nestedIndexedBean.array[0].name")).isEqualTo("arrayname5"); + assertThat(bw.getPropertyValue("array[1].nestedIndexedBean.array[1].name")).isEqualTo("arrayname4"); + assertThat(bw.getPropertyValue("list[0].nestedIndexedBean.list[0].name")).isEqualTo("listname3"); + assertThat(bw.getPropertyValue("list[1].nestedIndexedBean.list[1].name")).isEqualTo("listname2"); + assertThat(bw.getPropertyValue("map['key1'].nestedIndexedBean.map[key1].name")).isEqualTo("mapname1"); + assertThat(bw.getPropertyValue("map[key2].nestedIndexedBean.map[\"key2\"].name")).isEqualTo("mapname0"); + } + + @Test + public void testNestedIndexedPropertiesWithIndexedCustomEditorForProperty() { + IndexedTestBean bean = new IndexedTestBean(); + TestBean tb0 = bean.getArray()[0]; + TestBean tb1 = bean.getArray()[1]; + TestBean tb2 = ((TestBean) bean.getList().get(0)); + TestBean tb3 = ((TestBean) bean.getList().get(1)); + TestBean tb4 = ((TestBean) bean.getMap().get("key1")); + TestBean tb5 = ((TestBean) bean.getMap().get("key2")); + tb0.setNestedIndexedBean(new IndexedTestBean()); + tb1.setNestedIndexedBean(new IndexedTestBean()); + tb2.setNestedIndexedBean(new IndexedTestBean()); + tb3.setNestedIndexedBean(new IndexedTestBean()); + tb4.setNestedIndexedBean(new IndexedTestBean()); + tb5.setNestedIndexedBean(new IndexedTestBean()); + BeanWrapper bw = new BeanWrapperImpl(bean); + bw.registerCustomEditor(String.class, "array[0].nestedIndexedBean.array[0].name", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue("array" + text); + } + }); + bw.registerCustomEditor(String.class, "list.nestedIndexedBean.list[1].name", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue("list" + text); + } + }); + bw.registerCustomEditor(String.class, "map[key1].nestedIndexedBean.map.name", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue("map" + text); + } + }); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("array[0].nestedIndexedBean.array[0].name", "name5"); + pvs.add("array[1].nestedIndexedBean.array[1].name", "name4"); + pvs.add("list[0].nestedIndexedBean.list[0].name", "name3"); + pvs.add("list[1].nestedIndexedBean.list[1].name", "name2"); + pvs.add("map[key1].nestedIndexedBean.map[\"key1\"].name", "name1"); + pvs.add("map['key2'].nestedIndexedBean.map[key2].name", "name0"); + bw.setPropertyValues(pvs); + assertThat(tb0.getNestedIndexedBean().getArray()[0].getName()).isEqualTo("arrayname5"); + assertThat(tb1.getNestedIndexedBean().getArray()[1].getName()).isEqualTo("name4"); + assertThat(((TestBean) tb2.getNestedIndexedBean().getList().get(0)).getName()).isEqualTo("name3"); + assertThat(((TestBean) tb3.getNestedIndexedBean().getList().get(1)).getName()).isEqualTo("listname2"); + assertThat(((TestBean) tb4.getNestedIndexedBean().getMap().get("key1")).getName()).isEqualTo("mapname1"); + assertThat(((TestBean) tb5.getNestedIndexedBean().getMap().get("key2")).getName()).isEqualTo("name0"); + } + + @Test + public void testIndexedPropertiesWithDirectAccessAndPropertyEditors() { + IndexedTestBean bean = new IndexedTestBean(); + BeanWrapper bw = new BeanWrapperImpl(bean); + bw.registerCustomEditor(TestBean.class, "array", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue(new TestBean("array" + text, 99)); + } + + @Override + public String getAsText() { + return ((TestBean) getValue()).getName(); + } + }); + bw.registerCustomEditor(TestBean.class, "list", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue(new TestBean("list" + text, 99)); + } + + @Override + public String getAsText() { + return ((TestBean) getValue()).getName(); + } + }); + bw.registerCustomEditor(TestBean.class, "map", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue(new TestBean("map" + text, 99)); + } + + @Override + public String getAsText() { + return ((TestBean) getValue()).getName(); + } + }); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("array[0]", "a"); + pvs.add("array[1]", "b"); + pvs.add("list[0]", "c"); + pvs.add("list[1]", "d"); + pvs.add("map[key1]", "e"); + pvs.add("map['key2']", "f"); + bw.setPropertyValues(pvs); + assertThat(bean.getArray()[0].getName()).isEqualTo("arraya"); + assertThat(bean.getArray()[1].getName()).isEqualTo("arrayb"); + assertThat(((TestBean) bean.getList().get(0)).getName()).isEqualTo("listc"); + assertThat(((TestBean) bean.getList().get(1)).getName()).isEqualTo("listd"); + assertThat(((TestBean) bean.getMap().get("key1")).getName()).isEqualTo("mape"); + assertThat(((TestBean) bean.getMap().get("key2")).getName()).isEqualTo("mapf"); + } + + @Test + public void testIndexedPropertiesWithDirectAccessAndSpecificPropertyEditors() { + IndexedTestBean bean = new IndexedTestBean(); + BeanWrapper bw = new BeanWrapperImpl(bean); + bw.registerCustomEditor(TestBean.class, "array[0]", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue(new TestBean("array0" + text, 99)); + } + + @Override + public String getAsText() { + return ((TestBean) getValue()).getName(); + } + }); + bw.registerCustomEditor(TestBean.class, "array[1]", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue(new TestBean("array1" + text, 99)); + } + + @Override + public String getAsText() { + return ((TestBean) getValue()).getName(); + } + }); + bw.registerCustomEditor(TestBean.class, "list[0]", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue(new TestBean("list0" + text, 99)); + } + + @Override + public String getAsText() { + return ((TestBean) getValue()).getName(); + } + }); + bw.registerCustomEditor(TestBean.class, "list[1]", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue(new TestBean("list1" + text, 99)); + } + + @Override + public String getAsText() { + return ((TestBean) getValue()).getName(); + } + }); + bw.registerCustomEditor(TestBean.class, "map[key1]", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue(new TestBean("mapkey1" + text, 99)); + } + + @Override + public String getAsText() { + return ((TestBean) getValue()).getName(); + } + }); + bw.registerCustomEditor(TestBean.class, "map[key2]", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue(new TestBean("mapkey2" + text, 99)); + } + + @Override + public String getAsText() { + return ((TestBean) getValue()).getName(); + } + }); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("array[0]", "a"); + pvs.add("array[1]", "b"); + pvs.add("list[0]", "c"); + pvs.add("list[1]", "d"); + pvs.add("map[key1]", "e"); + pvs.add("map['key2']", "f"); + bw.setPropertyValues(pvs); + assertThat(bean.getArray()[0].getName()).isEqualTo("array0a"); + assertThat(bean.getArray()[1].getName()).isEqualTo("array1b"); + assertThat(((TestBean) bean.getList().get(0)).getName()).isEqualTo("list0c"); + assertThat(((TestBean) bean.getList().get(1)).getName()).isEqualTo("list1d"); + assertThat(((TestBean) bean.getMap().get("key1")).getName()).isEqualTo("mapkey1e"); + assertThat(((TestBean) bean.getMap().get("key2")).getName()).isEqualTo("mapkey2f"); + } + + @Test + public void testIndexedPropertiesWithListPropertyEditor() { + IndexedTestBean bean = new IndexedTestBean(); + BeanWrapper bw = new BeanWrapperImpl(bean); + bw.registerCustomEditor(List.class, "list", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + List result = new ArrayList<>(); + result.add(new TestBean("list" + text, 99)); + setValue(result); + } + }); + bw.setPropertyValue("list", "1"); + assertThat(((TestBean) bean.getList().get(0)).getName()).isEqualTo("list1"); + bw.setPropertyValue("list[0]", "test"); + assertThat(bean.getList().get(0)).isEqualTo("test"); + } + + @Test + public void testConversionToOldCollections() throws PropertyVetoException { + OldCollectionsBean tb = new OldCollectionsBean(); + BeanWrapper bw = new BeanWrapperImpl(tb); + bw.registerCustomEditor(Vector.class, new CustomCollectionEditor(Vector.class)); + bw.registerCustomEditor(Hashtable.class, new CustomMapEditor(Hashtable.class)); + + bw.setPropertyValue("vector", new String[] {"a", "b"}); + assertThat(tb.getVector().size()).isEqualTo(2); + assertThat(tb.getVector().get(0)).isEqualTo("a"); + assertThat(tb.getVector().get(1)).isEqualTo("b"); + + bw.setPropertyValue("hashtable", Collections.singletonMap("foo", "bar")); + assertThat(tb.getHashtable().size()).isEqualTo(1); + assertThat(tb.getHashtable().get("foo")).isEqualTo("bar"); + } + + @Test + public void testUninitializedArrayPropertyWithCustomEditor() { + IndexedTestBean bean = new IndexedTestBean(false); + BeanWrapper bw = new BeanWrapperImpl(bean); + PropertyEditor pe = new CustomNumberEditor(Integer.class, true); + bw.registerCustomEditor(null, "list.age", pe); + TestBean tb = new TestBean(); + bw.setPropertyValue("list", new ArrayList<>()); + bw.setPropertyValue("list[0]", tb); + assertThat(bean.getList().get(0)).isEqualTo(tb); + assertThat(bw.findCustomEditor(int.class, "list.age")).isEqualTo(pe); + assertThat(bw.findCustomEditor(null, "list.age")).isEqualTo(pe); + assertThat(bw.findCustomEditor(int.class, "list[0].age")).isEqualTo(pe); + assertThat(bw.findCustomEditor(null, "list[0].age")).isEqualTo(pe); + } + + @Test + public void testArrayToArrayConversion() throws PropertyVetoException { + IndexedTestBean tb = new IndexedTestBean(); + BeanWrapper bw = new BeanWrapperImpl(tb); + bw.registerCustomEditor(TestBean.class, new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue(new TestBean(text, 99)); + } + }); + bw.setPropertyValue("array", new String[] {"a", "b"}); + assertThat(tb.getArray().length).isEqualTo(2); + assertThat(tb.getArray()[0].getName()).isEqualTo("a"); + assertThat(tb.getArray()[1].getName()).isEqualTo("b"); + } + + @Test + public void testArrayToStringConversion() throws PropertyVetoException { + TestBean tb = new TestBean(); + BeanWrapper bw = new BeanWrapperImpl(tb); + bw.registerCustomEditor(String.class, new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue("-" + text + "-"); + } + }); + bw.setPropertyValue("name", new String[] {"a", "b"}); + assertThat(tb.getName()).isEqualTo("-a,b-"); + } + + @Test + public void testClassArrayEditorSunnyDay() throws Exception { + ClassArrayEditor classArrayEditor = new ClassArrayEditor(); + classArrayEditor.setAsText("java.lang.String,java.util.HashMap"); + Class[] classes = (Class[]) classArrayEditor.getValue(); + assertThat(classes.length).isEqualTo(2); + assertThat(classes[0]).isEqualTo(String.class); + assertThat(classes[1]).isEqualTo(HashMap.class); + assertThat(classArrayEditor.getAsText()).isEqualTo("java.lang.String,java.util.HashMap"); + // ensure setAsText can consume the return value of getAsText + classArrayEditor.setAsText(classArrayEditor.getAsText()); + } + + @Test + public void testClassArrayEditorSunnyDayWithArrayTypes() throws Exception { + ClassArrayEditor classArrayEditor = new ClassArrayEditor(); + classArrayEditor.setAsText("java.lang.String[],java.util.Map[],int[],float[][][]"); + Class[] classes = (Class[]) classArrayEditor.getValue(); + assertThat(classes.length).isEqualTo(4); + assertThat(classes[0]).isEqualTo(String[].class); + assertThat(classes[1]).isEqualTo(Map[].class); + assertThat(classes[2]).isEqualTo(int[].class); + assertThat(classes[3]).isEqualTo(float[][][].class); + assertThat(classArrayEditor.getAsText()).isEqualTo("java.lang.String[],java.util.Map[],int[],float[][][]"); + // ensure setAsText can consume the return value of getAsText + classArrayEditor.setAsText(classArrayEditor.getAsText()); + } + + @Test + public void testClassArrayEditorSetAsTextWithNull() throws Exception { + ClassArrayEditor editor = new ClassArrayEditor(); + editor.setAsText(null); + assertThat(editor.getValue()).isNull(); + assertThat(editor.getAsText()).isEqualTo(""); + } + + @Test + public void testClassArrayEditorSetAsTextWithEmptyString() throws Exception { + ClassArrayEditor editor = new ClassArrayEditor(); + editor.setAsText(""); + assertThat(editor.getValue()).isNull(); + assertThat(editor.getAsText()).isEqualTo(""); + } + + @Test + public void testClassArrayEditorSetAsTextWithWhitespaceString() throws Exception { + ClassArrayEditor editor = new ClassArrayEditor(); + editor.setAsText("\n"); + assertThat(editor.getValue()).isNull(); + assertThat(editor.getAsText()).isEqualTo(""); + } + + @Test + public void testCharsetEditor() throws Exception { + CharsetEditor editor = new CharsetEditor(); + String name = "UTF-8"; + editor.setAsText(name); + Charset charset = Charset.forName(name); + assertThat(editor.getValue()).as("Invalid Charset conversion").isEqualTo(charset); + editor.setValue(charset); + assertThat(editor.getAsText()).as("Invalid Charset conversion").isEqualTo(name); + } + + + private static class TestBeanEditor extends PropertyEditorSupport { + + @Override + public void setAsText(String text) { + TestBean tb = new TestBean(); + StringTokenizer st = new StringTokenizer(text, "_"); + tb.setName(st.nextToken()); + tb.setAge(Integer.parseInt(st.nextToken())); + setValue(tb); + } + } + + + private static class OldValueAccessingTestBeanEditor extends PropertyEditorSupport { + + @Override + public void setAsText(String text) { + TestBean tb = new TestBean(); + StringTokenizer st = new StringTokenizer(text, "_"); + tb.setName(st.nextToken()); + tb.setAge(Integer.parseInt(st.nextToken())); + if (!tb.equals(getValue())) { + setValue(tb); + } + } + } + + + @SuppressWarnings("unused") + private static class PrimitiveArrayBean { + + private byte[] byteArray; + + private char[] charArray; + + public byte[] getByteArray() { + return byteArray; + } + + public void setByteArray(byte[] byteArray) { + this.byteArray = byteArray; + } + + public char[] getCharArray() { + return charArray; + } + + public void setCharArray(char[] charArray) { + this.charArray = charArray; + } + } + + + @SuppressWarnings("unused") + private static class CharBean { + + private char myChar; + + private Character myCharacter; + + public char getMyChar() { + return myChar; + } + + public void setMyChar(char myChar) { + this.myChar = myChar; + } + + public Character getMyCharacter() { + return myCharacter; + } + + public void setMyCharacter(Character myCharacter) { + this.myCharacter = myCharacter; + } + } + + + @SuppressWarnings("unused") + private static class OldCollectionsBean { + + private Vector vector; + + private Hashtable hashtable; + + public Vector getVector() { + return vector; + } + + public void setVector(Vector vector) { + this.vector = vector; + } + + public Hashtable getHashtable() { + return hashtable; + } + + public void setHashtable(Hashtable hashtable) { + this.hashtable = hashtable; + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/FileEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/FileEditorTests.java new file mode 100644 index 0000000..ec5cc0f --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/FileEditorTests.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditor; +import java.io.File; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Thomas Risberg + * @author Chris Beams + * @author Juergen Hoeller + */ +public class FileEditorTests { + + @Test + public void testClasspathFileName() throws Exception { + PropertyEditor fileEditor = new FileEditor(); + fileEditor.setAsText("classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + "/" + + ClassUtils.getShortName(getClass()) + ".class"); + Object value = fileEditor.getValue(); + boolean condition = value instanceof File; + assertThat(condition).isTrue(); + File file = (File) value; + assertThat(file.exists()).isTrue(); + } + + @Test + public void testWithNonExistentResource() throws Exception { + PropertyEditor propertyEditor = new FileEditor(); + assertThatIllegalArgumentException().isThrownBy(() -> + propertyEditor.setAsText("classpath:no_way_this_file_is_found.doc")); + } + + @Test + public void testWithNonExistentFile() throws Exception { + PropertyEditor fileEditor = new FileEditor(); + fileEditor.setAsText("file:no_way_this_file_is_found.doc"); + Object value = fileEditor.getValue(); + boolean condition1 = value instanceof File; + assertThat(condition1).isTrue(); + File file = (File) value; + boolean condition = !file.exists(); + assertThat(condition).isTrue(); + } + + @Test + public void testAbsoluteFileName() throws Exception { + PropertyEditor fileEditor = new FileEditor(); + fileEditor.setAsText("/no_way_this_file_is_found.doc"); + Object value = fileEditor.getValue(); + boolean condition1 = value instanceof File; + assertThat(condition1).isTrue(); + File file = (File) value; + boolean condition = !file.exists(); + assertThat(condition).isTrue(); + } + + @Test + public void testUnqualifiedFileNameFound() throws Exception { + PropertyEditor fileEditor = new FileEditor(); + String fileName = ClassUtils.classPackageAsResourcePath(getClass()) + "/" + + ClassUtils.getShortName(getClass()) + ".class"; + fileEditor.setAsText(fileName); + Object value = fileEditor.getValue(); + boolean condition = value instanceof File; + assertThat(condition).isTrue(); + File file = (File) value; + assertThat(file.exists()).isTrue(); + String absolutePath = file.getAbsolutePath().replace('\\', '/'); + assertThat(absolutePath.endsWith(fileName)).isTrue(); + } + + @Test + public void testUnqualifiedFileNameNotFound() throws Exception { + PropertyEditor fileEditor = new FileEditor(); + String fileName = ClassUtils.classPackageAsResourcePath(getClass()) + "/" + + ClassUtils.getShortName(getClass()) + ".clazz"; + fileEditor.setAsText(fileName); + Object value = fileEditor.getValue(); + boolean condition = value instanceof File; + assertThat(condition).isTrue(); + File file = (File) value; + assertThat(file.exists()).isFalse(); + String absolutePath = file.getAbsolutePath().replace('\\', '/'); + assertThat(absolutePath.endsWith(fileName)).isTrue(); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/InputStreamEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/InputStreamEditorTests.java new file mode 100644 index 0000000..e0fb3aa --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/InputStreamEditorTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.io.InputStream; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Unit tests for the {@link InputStreamEditor} class. + * + * @author Rick Evans + * @author Chris Beams + */ +public class InputStreamEditorTests { + + @Test + public void testCtorWithNullResourceEditor() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new InputStreamEditor(null)); + } + + @Test + public void testSunnyDay() throws Exception { + InputStream stream = null; + try { + String resource = "classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + + "/" + ClassUtils.getShortName(getClass()) + ".class"; + InputStreamEditor editor = new InputStreamEditor(); + editor.setAsText(resource); + Object value = editor.getValue(); + assertThat(value).isNotNull(); + boolean condition = value instanceof InputStream; + assertThat(condition).isTrue(); + stream = (InputStream) value; + assertThat(stream.available() > 0).isTrue(); + } + finally { + if (stream != null) { + stream.close(); + } + } + } + + @Test + public void testWhenResourceDoesNotExist() throws Exception { + InputStreamEditor editor = new InputStreamEditor(); + assertThatIllegalArgumentException().isThrownBy(() -> + editor.setAsText("classpath:bingo!")); + } + + @Test + public void testGetAsTextReturnsNullByDefault() throws Exception { + assertThat(new InputStreamEditor().getAsText()).isNull(); + String resource = "classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + + "/" + ClassUtils.getShortName(getClass()) + ".class"; + InputStreamEditor editor = new InputStreamEditor(); + editor.setAsText(resource); + assertThat(editor.getAsText()).isNull(); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PathEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PathEditorTests.java new file mode 100644 index 0000000..40354fc --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PathEditorTests.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditor; +import java.io.File; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Juergen Hoeller + * @since 4.3.2 + */ +public class PathEditorTests { + + @Test + public void testClasspathPathName() throws Exception { + PropertyEditor pathEditor = new PathEditor(); + pathEditor.setAsText("classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + "/" + + ClassUtils.getShortName(getClass()) + ".class"); + Object value = pathEditor.getValue(); + boolean condition = value instanceof Path; + assertThat(condition).isTrue(); + Path path = (Path) value; + assertThat(path.toFile().exists()).isTrue(); + } + + @Test + public void testWithNonExistentResource() throws Exception { + PropertyEditor propertyEditor = new PathEditor(); + assertThatIllegalArgumentException().isThrownBy(() -> + propertyEditor.setAsText("classpath:/no_way_this_file_is_found.doc")); + } + + @Test + public void testWithNonExistentPath() throws Exception { + PropertyEditor pathEditor = new PathEditor(); + pathEditor.setAsText("file:/no_way_this_file_is_found.doc"); + Object value = pathEditor.getValue(); + boolean condition1 = value instanceof Path; + assertThat(condition1).isTrue(); + Path path = (Path) value; + boolean condition = !path.toFile().exists(); + assertThat(condition).isTrue(); + } + + @Test + public void testAbsolutePath() throws Exception { + PropertyEditor pathEditor = new PathEditor(); + pathEditor.setAsText("/no_way_this_file_is_found.doc"); + Object value = pathEditor.getValue(); + boolean condition1 = value instanceof Path; + assertThat(condition1).isTrue(); + Path path = (Path) value; + boolean condition = !path.toFile().exists(); + assertThat(condition).isTrue(); + } + + @Test + public void testUnqualifiedPathNameFound() throws Exception { + PropertyEditor pathEditor = new PathEditor(); + String fileName = ClassUtils.classPackageAsResourcePath(getClass()) + "/" + + ClassUtils.getShortName(getClass()) + ".class"; + pathEditor.setAsText(fileName); + Object value = pathEditor.getValue(); + boolean condition = value instanceof Path; + assertThat(condition).isTrue(); + Path path = (Path) value; + File file = path.toFile(); + assertThat(file.exists()).isTrue(); + String absolutePath = file.getAbsolutePath(); + if (File.separatorChar == '\\') { + absolutePath = absolutePath.replace('\\', '/'); + } + assertThat(absolutePath.endsWith(fileName)).isTrue(); + } + + @Test + public void testUnqualifiedPathNameNotFound() throws Exception { + PropertyEditor pathEditor = new PathEditor(); + String fileName = ClassUtils.classPackageAsResourcePath(getClass()) + "/" + + ClassUtils.getShortName(getClass()) + ".clazz"; + pathEditor.setAsText(fileName); + Object value = pathEditor.getValue(); + boolean condition = value instanceof Path; + assertThat(condition).isTrue(); + Path path = (Path) value; + File file = path.toFile(); + assertThat(file.exists()).isFalse(); + String absolutePath = file.getAbsolutePath(); + if (File.separatorChar == '\\') { + absolutePath = absolutePath.replace('\\', '/'); + } + assertThat(absolutePath.endsWith(fileName)).isTrue(); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PropertiesEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PropertiesEditorTests.java new file mode 100644 index 0000000..32fb123 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PropertiesEditorTests.java @@ -0,0 +1,172 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test the conversion of Strings to {@link java.util.Properties} objects, + * and other property editors. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Rick Evans + */ +public class PropertiesEditorTests { + + @Test + public void oneProperty() { + String s = "foo=bar"; + PropertiesEditor pe= new PropertiesEditor(); + pe.setAsText(s); + Properties p = (Properties) pe.getValue(); + assertThat(p.entrySet().size() == 1).as("contains one entry").isTrue(); + assertThat(p.get("foo").equals("bar")).as("foo=bar").isTrue(); + } + + @Test + public void twoProperties() { + String s = "foo=bar with whitespace\n" + + "me=mi"; + PropertiesEditor pe= new PropertiesEditor(); + pe.setAsText(s); + Properties p = (Properties) pe.getValue(); + assertThat(p.entrySet().size() == 2).as("contains two entries").isTrue(); + assertThat(p.get("foo").equals("bar with whitespace")).as("foo=bar with whitespace").isTrue(); + assertThat(p.get("me").equals("mi")).as("me=mi").isTrue(); + } + + @Test + public void handlesEqualsInValue() { + String s = "foo=bar\n" + + "me=mi\n" + + "x=y=z"; + PropertiesEditor pe= new PropertiesEditor(); + pe.setAsText(s); + Properties p = (Properties) pe.getValue(); + assertThat(p.entrySet().size() == 3).as("contains two entries").isTrue(); + assertThat(p.get("foo").equals("bar")).as("foo=bar").isTrue(); + assertThat(p.get("me").equals("mi")).as("me=mi").isTrue(); + assertThat(p.get("x").equals("y=z")).as("x='y=z'").isTrue(); + } + + @Test + public void handlesEmptyProperty() { + String s = "foo=bar\nme=mi\nx="; + PropertiesEditor pe= new PropertiesEditor(); + pe.setAsText(s); + Properties p = (Properties) pe.getValue(); + assertThat(p.entrySet().size() == 3).as("contains two entries").isTrue(); + assertThat(p.get("foo").equals("bar")).as("foo=bar").isTrue(); + assertThat(p.get("me").equals("mi")).as("me=mi").isTrue(); + assertThat(p.get("x").equals("")).as("x='y=z'").isTrue(); + } + + @Test + public void handlesEmptyPropertyWithoutEquals() { + String s = "foo\nme=mi\nx=x"; + PropertiesEditor pe= new PropertiesEditor(); + pe.setAsText(s); + Properties p = (Properties) pe.getValue(); + assertThat(p.entrySet().size() == 3).as("contains three entries").isTrue(); + assertThat(p.get("foo").equals("")).as("foo is empty").isTrue(); + assertThat(p.get("me").equals("mi")).as("me=mi").isTrue(); + } + + /** + * Comments begin with # + */ + @Test + public void ignoresCommentLinesAndEmptyLines() { + String s = "#Ignore this comment\n" + + "foo=bar\n" + + "#Another=comment more junk /\n" + + "me=mi\n" + + "x=x\n" + + "\n"; + PropertiesEditor pe= new PropertiesEditor(); + pe.setAsText(s); + Properties p = (Properties) pe.getValue(); + assertThat(p.entrySet().size() == 3).as("contains three entries").isTrue(); + assertThat(p.get("foo").equals("bar")).as("foo is bar").isTrue(); + assertThat(p.get("me").equals("mi")).as("me=mi").isTrue(); + } + + /** + * We'll typically align by indenting with tabs or spaces. + * These should be ignored if at the beginning of a line. + * We must ensure that comment lines beginning with whitespace are + * still ignored: The standard syntax doesn't allow this on JDK 1.3. + */ + @Test + public void ignoresLeadingSpacesAndTabs() { + String s = " #Ignore this comment\n" + + "\t\tfoo=bar\n" + + "\t#Another comment more junk \n" + + " me=mi\n" + + "x=x\n" + + "\n"; + PropertiesEditor pe= new PropertiesEditor(); + pe.setAsText(s); + Properties p = (Properties) pe.getValue(); + assertThat(p.size() == 3).as("contains 3 entries, not " + p.size()).isTrue(); + assertThat(p.get("foo").equals("bar")).as("foo is bar").isTrue(); + assertThat(p.get("me").equals("mi")).as("me=mi").isTrue(); + } + + @Test + public void nullValue() { + PropertiesEditor pe= new PropertiesEditor(); + pe.setAsText(null); + Properties p = (Properties) pe.getValue(); + assertThat(p.size()).isEqualTo(0); + } + + @Test + public void emptyString() { + PropertiesEditor pe = new PropertiesEditor(); + pe.setAsText(""); + Properties p = (Properties) pe.getValue(); + assertThat(p.isEmpty()).as("empty string means empty properties").isTrue(); + } + + @Test + public void usingMapAsValueSource() throws Exception { + Map map = new HashMap<>(); + map.put("one", "1"); + map.put("two", "2"); + map.put("three", "3"); + PropertiesEditor pe = new PropertiesEditor(); + pe.setValue(map); + Object value = pe.getValue(); + assertThat(value).isNotNull(); + boolean condition = value instanceof Properties; + assertThat(condition).isTrue(); + Properties props = (Properties) value; + assertThat(props.size()).isEqualTo(3); + assertThat(props.getProperty("one")).isEqualTo("1"); + assertThat(props.getProperty("two")).isEqualTo("2"); + assertThat(props.getProperty("three")).isEqualTo("3"); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ReaderEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ReaderEditorTests.java new file mode 100644 index 0000000..5573452 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ReaderEditorTests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.io.Reader; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Unit tests for the {@link ReaderEditor} class. + * + * @author Juergen Hoeller + * @since 4.2 + */ +public class ReaderEditorTests { + + @Test + public void testCtorWithNullResourceEditor() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new ReaderEditor(null)); + } + + @Test + public void testSunnyDay() throws Exception { + Reader reader = null; + try { + String resource = "classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + + "/" + ClassUtils.getShortName(getClass()) + ".class"; + ReaderEditor editor = new ReaderEditor(); + editor.setAsText(resource); + Object value = editor.getValue(); + assertThat(value).isNotNull(); + boolean condition = value instanceof Reader; + assertThat(condition).isTrue(); + reader = (Reader) value; + assertThat(reader.ready()).isTrue(); + } + finally { + if (reader != null) { + reader.close(); + } + } + } + + @Test + public void testWhenResourceDoesNotExist() throws Exception { + String resource = "classpath:bingo!"; + ReaderEditor editor = new ReaderEditor(); + assertThatIllegalArgumentException().isThrownBy(() -> + editor.setAsText(resource)); + } + + @Test + public void testGetAsTextReturnsNullByDefault() throws Exception { + assertThat(new ReaderEditor().getAsText()).isNull(); + String resource = "classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + + "/" + ClassUtils.getShortName(getClass()) + ".class"; + ReaderEditor editor = new ReaderEditor(); + editor.setAsText(resource); + assertThat(editor.getAsText()).isNull(); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ResourceBundleEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ResourceBundleEditorTests.java new file mode 100644 index 0000000..3785304 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ResourceBundleEditorTests.java @@ -0,0 +1,132 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.util.ResourceBundle; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Unit tests for the {@link ResourceBundleEditor} class. + * + * @author Rick Evans + * @author Chris Beams + */ +public class ResourceBundleEditorTests { + + private static final String BASE_NAME = ResourceBundleEditorTests.class.getName(); + + private static final String MESSAGE_KEY = "punk"; + + + @Test + public void testSetAsTextWithJustBaseName() throws Exception { + ResourceBundleEditor editor = new ResourceBundleEditor(); + editor.setAsText(BASE_NAME); + Object value = editor.getValue(); + assertThat(value).as("Returned ResourceBundle was null (must not be for valid setAsText(..) call).").isNotNull(); + boolean condition = value instanceof ResourceBundle; + assertThat(condition).as("Returned object was not a ResourceBundle (must be for valid setAsText(..) call).").isTrue(); + ResourceBundle bundle = (ResourceBundle) value; + String string = bundle.getString(MESSAGE_KEY); + assertThat(string).isEqualTo(MESSAGE_KEY); + } + + @Test + public void testSetAsTextWithBaseNameThatEndsInDefaultSeparator() throws Exception { + ResourceBundleEditor editor = new ResourceBundleEditor(); + editor.setAsText(BASE_NAME + "_"); + Object value = editor.getValue(); + assertThat(value).as("Returned ResourceBundle was null (must not be for valid setAsText(..) call).").isNotNull(); + boolean condition = value instanceof ResourceBundle; + assertThat(condition).as("Returned object was not a ResourceBundle (must be for valid setAsText(..) call).").isTrue(); + ResourceBundle bundle = (ResourceBundle) value; + String string = bundle.getString(MESSAGE_KEY); + assertThat(string).isEqualTo(MESSAGE_KEY); + } + + @Test + public void testSetAsTextWithBaseNameAndLanguageCode() throws Exception { + ResourceBundleEditor editor = new ResourceBundleEditor(); + editor.setAsText(BASE_NAME + "Lang" + "_en"); + Object value = editor.getValue(); + assertThat(value).as("Returned ResourceBundle was null (must not be for valid setAsText(..) call).").isNotNull(); + boolean condition = value instanceof ResourceBundle; + assertThat(condition).as("Returned object was not a ResourceBundle (must be for valid setAsText(..) call).").isTrue(); + ResourceBundle bundle = (ResourceBundle) value; + String string = bundle.getString(MESSAGE_KEY); + assertThat(string).isEqualTo("yob"); + } + + @Test + public void testSetAsTextWithBaseNameLanguageAndCountryCode() throws Exception { + ResourceBundleEditor editor = new ResourceBundleEditor(); + editor.setAsText(BASE_NAME + "LangCountry" + "_en_GB"); + Object value = editor.getValue(); + assertThat(value).as("Returned ResourceBundle was null (must not be for valid setAsText(..) call).").isNotNull(); + boolean condition = value instanceof ResourceBundle; + assertThat(condition).as("Returned object was not a ResourceBundle (must be for valid setAsText(..) call).").isTrue(); + ResourceBundle bundle = (ResourceBundle) value; + String string = bundle.getString(MESSAGE_KEY); + assertThat(string).isEqualTo("chav"); + } + + @Test + public void testSetAsTextWithTheKitchenSink() throws Exception { + ResourceBundleEditor editor = new ResourceBundleEditor(); + editor.setAsText(BASE_NAME + "LangCountryDialect" + "_en_GB_GLASGOW"); + Object value = editor.getValue(); + assertThat(value).as("Returned ResourceBundle was null (must not be for valid setAsText(..) call).").isNotNull(); + boolean condition = value instanceof ResourceBundle; + assertThat(condition).as("Returned object was not a ResourceBundle (must be for valid setAsText(..) call).").isTrue(); + ResourceBundle bundle = (ResourceBundle) value; + String string = bundle.getString(MESSAGE_KEY); + assertThat(string).isEqualTo("ned"); + } + + @Test + public void testSetAsTextWithNull() throws Exception { + ResourceBundleEditor editor = new ResourceBundleEditor(); + assertThatIllegalArgumentException().isThrownBy(() -> + editor.setAsText(null)); + } + + @Test + public void testSetAsTextWithEmptyString() throws Exception { + ResourceBundleEditor editor = new ResourceBundleEditor(); + assertThatIllegalArgumentException().isThrownBy(() -> + editor.setAsText("")); + } + + @Test + public void testSetAsTextWithWhiteSpaceString() throws Exception { + ResourceBundleEditor editor = new ResourceBundleEditor(); + assertThatIllegalArgumentException().isThrownBy(() -> + editor.setAsText(" ")); + } + + @Test + public void testSetAsTextWithJustSeparatorString() throws Exception { + ResourceBundleEditor editor = new ResourceBundleEditor(); + assertThatIllegalArgumentException().isThrownBy(() -> + editor.setAsText("_")); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/StringArrayPropertyEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/StringArrayPropertyEditorTests.java new file mode 100644 index 0000000..0583c78 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/StringArrayPropertyEditorTests.java @@ -0,0 +1,103 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rick Evans + * @author Juergen Hoeller + * @author Sam Brannen + */ +class StringArrayPropertyEditorTests { + + @Test + void withDefaultSeparator() { + StringArrayPropertyEditor editor = new StringArrayPropertyEditor(); + editor.setAsText("0,1,2"); + Object value = editor.getValue(); + assertTrimmedElements(value); + assertThat(editor.getAsText()).isEqualTo("0,1,2"); + } + + @Test + void trimByDefault() { + StringArrayPropertyEditor editor = new StringArrayPropertyEditor(); + editor.setAsText(" 0,1 , 2 "); + Object value = editor.getValue(); + assertTrimmedElements(value); + assertThat(editor.getAsText()).isEqualTo("0,1,2"); + } + + @Test + void noTrim() { + StringArrayPropertyEditor editor = new StringArrayPropertyEditor(",", false, false); + editor.setAsText(" 0,1 , 2 "); + Object value = editor.getValue(); + String[] array = (String[]) value; + for (int i = 0; i < array.length; ++i) { + assertThat(array[i].length()).isEqualTo(3); + assertThat(array[i].trim()).isEqualTo(("" + i)); + } + assertThat(editor.getAsText()).isEqualTo(" 0,1 , 2 "); + } + + @Test + void withCustomSeparator() { + StringArrayPropertyEditor editor = new StringArrayPropertyEditor(":"); + editor.setAsText("0:1:2"); + Object value = editor.getValue(); + assertTrimmedElements(value); + assertThat(editor.getAsText()).isEqualTo("0:1:2"); + } + + @Test + void withCharsToDelete() { + StringArrayPropertyEditor editor = new StringArrayPropertyEditor(",", "\r\n", false); + editor.setAsText("0\r,1,\n2"); + Object value = editor.getValue(); + assertTrimmedElements(value); + assertThat(editor.getAsText()).isEqualTo("0,1,2"); + } + + @Test + void withEmptyArray() { + StringArrayPropertyEditor editor = new StringArrayPropertyEditor(); + editor.setAsText(""); + Object value = editor.getValue(); + assertThat(value).isInstanceOf(String[].class); + assertThat((String[]) value).isEmpty(); + } + + @Test + void withEmptyArrayAsNull() { + StringArrayPropertyEditor editor = new StringArrayPropertyEditor(",", true); + editor.setAsText(""); + assertThat(editor.getValue()).isNull(); + } + + private static void assertTrimmedElements(Object value) { + assertThat(value).isInstanceOf(String[].class); + String[] array = (String[]) value; + for (int i = 0; i < array.length; ++i) { + assertThat(array[i]).isEqualTo(("" + i)); + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/URIEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/URIEditorTests.java new file mode 100644 index 0000000..e9896ed --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/URIEditorTests.java @@ -0,0 +1,154 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditor; +import java.net.URI; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @author Arjen Poutsma + */ +public class URIEditorTests { + + @Test + public void standardURI() throws Exception { + doTestURI("mailto:juergen.hoeller@interface21.com"); + } + + @Test + public void withNonExistentResource() throws Exception { + doTestURI("gonna:/freak/in/the/morning/freak/in/the.evening"); + } + + @Test + public void standardURL() throws Exception { + doTestURI("https://www.springframework.org"); + } + + @Test + public void standardURLWithFragment() throws Exception { + doTestURI("https://www.springframework.org#1"); + } + + @Test + public void standardURLWithWhitespace() throws Exception { + PropertyEditor uriEditor = new URIEditor(); + uriEditor.setAsText(" https://www.springframework.org "); + Object value = uriEditor.getValue(); + boolean condition = value instanceof URI; + assertThat(condition).isTrue(); + URI uri = (URI) value; + assertThat(uri.toString()).isEqualTo("https://www.springframework.org"); + } + + @Test + public void classpathURL() throws Exception { + PropertyEditor uriEditor = new URIEditor(getClass().getClassLoader()); + uriEditor.setAsText("classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + + "/" + ClassUtils.getShortName(getClass()) + ".class"); + Object value = uriEditor.getValue(); + boolean condition1 = value instanceof URI; + assertThat(condition1).isTrue(); + URI uri = (URI) value; + assertThat(uriEditor.getAsText()).isEqualTo(uri.toString()); + boolean condition = !uri.getScheme().startsWith("classpath"); + assertThat(condition).isTrue(); + } + + @Test + public void classpathURLWithWhitespace() throws Exception { + PropertyEditor uriEditor = new URIEditor(getClass().getClassLoader()); + uriEditor.setAsText(" classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + + "/" + ClassUtils.getShortName(getClass()) + ".class "); + Object value = uriEditor.getValue(); + boolean condition1 = value instanceof URI; + assertThat(condition1).isTrue(); + URI uri = (URI) value; + assertThat(uriEditor.getAsText()).isEqualTo(uri.toString()); + boolean condition = !uri.getScheme().startsWith("classpath"); + assertThat(condition).isTrue(); + } + + @Test + public void classpathURLAsIs() throws Exception { + PropertyEditor uriEditor = new URIEditor(); + uriEditor.setAsText("classpath:test.txt"); + Object value = uriEditor.getValue(); + boolean condition = value instanceof URI; + assertThat(condition).isTrue(); + URI uri = (URI) value; + assertThat(uriEditor.getAsText()).isEqualTo(uri.toString()); + assertThat(uri.getScheme().startsWith("classpath")).isTrue(); + } + + @Test + public void setAsTextWithNull() throws Exception { + PropertyEditor uriEditor = new URIEditor(); + uriEditor.setAsText(null); + assertThat(uriEditor.getValue()).isNull(); + assertThat(uriEditor.getAsText()).isEqualTo(""); + } + + @Test + public void getAsTextReturnsEmptyStringIfValueNotSet() throws Exception { + PropertyEditor uriEditor = new URIEditor(); + assertThat(uriEditor.getAsText()).isEqualTo(""); + } + + @Test + public void encodeURI() throws Exception { + PropertyEditor uriEditor = new URIEditor(); + uriEditor.setAsText("https://example.com/spaces and \u20AC"); + Object value = uriEditor.getValue(); + boolean condition = value instanceof URI; + assertThat(condition).isTrue(); + URI uri = (URI) value; + assertThat(uriEditor.getAsText()).isEqualTo(uri.toString()); + assertThat(uri.toASCIIString()).isEqualTo("https://example.com/spaces%20and%20%E2%82%AC"); + } + + @Test + public void encodeAlreadyEncodedURI() throws Exception { + PropertyEditor uriEditor = new URIEditor(false); + uriEditor.setAsText("https://example.com/spaces%20and%20%E2%82%AC"); + Object value = uriEditor.getValue(); + boolean condition = value instanceof URI; + assertThat(condition).isTrue(); + URI uri = (URI) value; + assertThat(uriEditor.getAsText()).isEqualTo(uri.toString()); + assertThat(uri.toASCIIString()).isEqualTo("https://example.com/spaces%20and%20%E2%82%AC"); + } + + + private void doTestURI(String uriSpec) { + PropertyEditor uriEditor = new URIEditor(); + uriEditor.setAsText(uriSpec); + Object value = uriEditor.getValue(); + boolean condition = value instanceof URI; + assertThat(condition).isTrue(); + URI uri = (URI) value; + assertThat(uri.toString()).isEqualTo(uriSpec); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/URLEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/URLEditorTests.java new file mode 100644 index 0000000..2d6c4b0 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/URLEditorTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.beans.PropertyEditor; +import java.net.URL; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Rick Evans + * @author Chris Beams + */ +public class URLEditorTests { + + @Test + public void testCtorWithNullResourceEditor() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new URLEditor(null)); + } + + @Test + public void testStandardURI() throws Exception { + PropertyEditor urlEditor = new URLEditor(); + urlEditor.setAsText("mailto:juergen.hoeller@interface21.com"); + Object value = urlEditor.getValue(); + boolean condition = value instanceof URL; + assertThat(condition).isTrue(); + URL url = (URL) value; + assertThat(urlEditor.getAsText()).isEqualTo(url.toExternalForm()); + } + + @Test + public void testStandardURL() throws Exception { + PropertyEditor urlEditor = new URLEditor(); + urlEditor.setAsText("https://www.springframework.org"); + Object value = urlEditor.getValue(); + boolean condition = value instanceof URL; + assertThat(condition).isTrue(); + URL url = (URL) value; + assertThat(urlEditor.getAsText()).isEqualTo(url.toExternalForm()); + } + + @Test + public void testClasspathURL() throws Exception { + PropertyEditor urlEditor = new URLEditor(); + urlEditor.setAsText("classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + + "/" + ClassUtils.getShortName(getClass()) + ".class"); + Object value = urlEditor.getValue(); + boolean condition1 = value instanceof URL; + assertThat(condition1).isTrue(); + URL url = (URL) value; + assertThat(urlEditor.getAsText()).isEqualTo(url.toExternalForm()); + boolean condition = !url.getProtocol().startsWith("classpath"); + assertThat(condition).isTrue(); + } + + @Test + public void testWithNonExistentResource() throws Exception { + PropertyEditor urlEditor = new URLEditor(); + assertThatIllegalArgumentException().isThrownBy(() -> + urlEditor.setAsText("gonna:/freak/in/the/morning/freak/in/the.evening")); + } + + @Test + public void testSetAsTextWithNull() throws Exception { + PropertyEditor urlEditor = new URLEditor(); + urlEditor.setAsText(null); + assertThat(urlEditor.getValue()).isNull(); + assertThat(urlEditor.getAsText()).isEqualTo(""); + } + + @Test + public void testGetAsTextReturnsEmptyStringIfValueNotSet() throws Exception { + PropertyEditor urlEditor = new URLEditor(); + assertThat(urlEditor.getAsText()).isEqualTo(""); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ZoneIdEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ZoneIdEditorTests.java new file mode 100644 index 0000000..7a91623 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/ZoneIdEditorTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.propertyeditors; + +import java.time.ZoneId; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Nicholas Williams + */ +public class ZoneIdEditorTests { + + private final ZoneIdEditor editor = new ZoneIdEditor(); + + @Test + public void americaChicago() { + editor.setAsText("America/Chicago"); + + ZoneId zoneId = (ZoneId) editor.getValue(); + assertThat(zoneId).as("The zone ID should not be null.").isNotNull(); + assertThat(zoneId).as("The zone ID is not correct.").isEqualTo(ZoneId.of("America/Chicago")); + + assertThat(editor.getAsText()).as("The text version is not correct.").isEqualTo("America/Chicago"); + } + + @Test + public void americaLosAngeles() { + editor.setAsText("America/Los_Angeles"); + + ZoneId zoneId = (ZoneId) editor.getValue(); + assertThat(zoneId).as("The zone ID should not be null.").isNotNull(); + assertThat(zoneId).as("The zone ID is not correct.").isEqualTo(ZoneId.of("America/Los_Angeles")); + + assertThat(editor.getAsText()).as("The text version is not correct.").isEqualTo("America/Los_Angeles"); + } + + @Test + public void getNullAsText() { + assertThat(editor.getAsText()).as("The returned value is not correct.").isEqualTo(""); + } + + @Test + public void getValueAsText() { + editor.setValue(ZoneId.of("America/New_York")); + assertThat(editor.getAsText()).as("The text version is not correct.").isEqualTo("America/New_York"); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/support/DerivedFromProtectedBaseBean.java b/spring-beans/src/test/java/org/springframework/beans/support/DerivedFromProtectedBaseBean.java new file mode 100644 index 0000000..ab95a47 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/support/DerivedFromProtectedBaseBean.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.support; + +/** + * @author Juergen Hoeller + * @since 29.07.2004 + */ +public class DerivedFromProtectedBaseBean extends ProtectedBaseBean { + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/support/PagedListHolderTests.java b/spring-beans/src/test/java/org/springframework/beans/support/PagedListHolderTests.java new file mode 100644 index 0000000..0afce64 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/support/PagedListHolderTests.java @@ -0,0 +1,220 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.support; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @author Jean-Pierre PAWLAK + * @author Chris Beams + * @since 20.05.2003 + */ +public class PagedListHolderTests { + + @Test + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void testPagedListHolder() { + TestBean tb1 = new TestBean(); + tb1.setName("eva"); + tb1.setAge(25); + TestBean tb2 = new TestBean(); + tb2.setName("juergen"); + tb2.setAge(99); + TestBean tb3 = new TestBean(); + tb3.setName("Rod"); + tb3.setAge(32); + List tbs = new ArrayList(); + tbs.add(tb1); + tbs.add(tb2); + tbs.add(tb3); + + PagedListHolder holder = new PagedListHolder(tbs); + assertThat(holder.getSource() == tbs).as("Correct source").isTrue(); + assertThat(holder.getNrOfElements() == 3).as("Correct number of elements").isTrue(); + assertThat(holder.getPageCount() == 1).as("Correct number of pages").isTrue(); + assertThat(holder.getPageSize() == PagedListHolder.DEFAULT_PAGE_SIZE).as("Correct page size").isTrue(); + assertThat(holder.getPage() == 0).as("Correct page number").isTrue(); + assertThat(holder.isFirstPage()).as("First page").isTrue(); + assertThat(holder.isLastPage()).as("Last page").isTrue(); + assertThat(holder.getFirstElementOnPage() == 0).as("Correct first element").isTrue(); + assertThat(holder.getLastElementOnPage() == 2).as("Correct first element").isTrue(); + assertThat(holder.getPageList().size() == 3).as("Correct page list size").isTrue(); + assertThat(holder.getPageList().get(0) == tb1).as("Correct page list contents").isTrue(); + assertThat(holder.getPageList().get(1) == tb2).as("Correct page list contents").isTrue(); + assertThat(holder.getPageList().get(2) == tb3).as("Correct page list contents").isTrue(); + + holder.setPageSize(2); + assertThat(holder.getPageCount() == 2).as("Correct number of pages").isTrue(); + assertThat(holder.getPageSize() == 2).as("Correct page size").isTrue(); + assertThat(holder.getPage() == 0).as("Correct page number").isTrue(); + assertThat(holder.isFirstPage()).as("First page").isTrue(); + assertThat(holder.isLastPage()).as("Last page").isFalse(); + assertThat(holder.getFirstElementOnPage() == 0).as("Correct first element").isTrue(); + assertThat(holder.getLastElementOnPage() == 1).as("Correct last element").isTrue(); + assertThat(holder.getPageList().size() == 2).as("Correct page list size").isTrue(); + assertThat(holder.getPageList().get(0) == tb1).as("Correct page list contents").isTrue(); + assertThat(holder.getPageList().get(1) == tb2).as("Correct page list contents").isTrue(); + + holder.setPage(1); + assertThat(holder.getPage() == 1).as("Correct page number").isTrue(); + assertThat(holder.isFirstPage()).as("First page").isFalse(); + assertThat(holder.isLastPage()).as("Last page").isTrue(); + assertThat(holder.getFirstElementOnPage() == 2).as("Correct first element").isTrue(); + assertThat(holder.getLastElementOnPage() == 2).as("Correct last element").isTrue(); + assertThat(holder.getPageList().size() == 1).as("Correct page list size").isTrue(); + assertThat(holder.getPageList().get(0) == tb3).as("Correct page list contents").isTrue(); + + holder.setPageSize(3); + assertThat(holder.getPageCount() == 1).as("Correct number of pages").isTrue(); + assertThat(holder.getPageSize() == 3).as("Correct page size").isTrue(); + assertThat(holder.getPage() == 0).as("Correct page number").isTrue(); + assertThat(holder.isFirstPage()).as("First page").isTrue(); + assertThat(holder.isLastPage()).as("Last page").isTrue(); + assertThat(holder.getFirstElementOnPage() == 0).as("Correct first element").isTrue(); + assertThat(holder.getLastElementOnPage() == 2).as("Correct last element").isTrue(); + + holder.setPage(1); + holder.setPageSize(2); + assertThat(holder.getPageCount() == 2).as("Correct number of pages").isTrue(); + assertThat(holder.getPageSize() == 2).as("Correct page size").isTrue(); + assertThat(holder.getPage() == 1).as("Correct page number").isTrue(); + assertThat(holder.isFirstPage()).as("First page").isFalse(); + assertThat(holder.isLastPage()).as("Last page").isTrue(); + assertThat(holder.getFirstElementOnPage() == 2).as("Correct first element").isTrue(); + assertThat(holder.getLastElementOnPage() == 2).as("Correct last element").isTrue(); + + holder.setPageSize(2); + holder.setPage(1); + ((MutableSortDefinition) holder.getSort()).setProperty("name"); + ((MutableSortDefinition) holder.getSort()).setIgnoreCase(false); + holder.resort(); + assertThat(holder.getSource() == tbs).as("Correct source").isTrue(); + assertThat(holder.getNrOfElements() == 3).as("Correct number of elements").isTrue(); + assertThat(holder.getPageCount() == 2).as("Correct number of pages").isTrue(); + assertThat(holder.getPageSize() == 2).as("Correct page size").isTrue(); + assertThat(holder.getPage() == 0).as("Correct page number").isTrue(); + assertThat(holder.isFirstPage()).as("First page").isTrue(); + assertThat(holder.isLastPage()).as("Last page").isFalse(); + assertThat(holder.getFirstElementOnPage() == 0).as("Correct first element").isTrue(); + assertThat(holder.getLastElementOnPage() == 1).as("Correct last element").isTrue(); + assertThat(holder.getPageList().size() == 2).as("Correct page list size").isTrue(); + assertThat(holder.getPageList().get(0) == tb3).as("Correct page list contents").isTrue(); + assertThat(holder.getPageList().get(1) == tb1).as("Correct page list contents").isTrue(); + + ((MutableSortDefinition) holder.getSort()).setProperty("name"); + holder.resort(); + assertThat(holder.getPageList().get(0) == tb2).as("Correct page list contents").isTrue(); + assertThat(holder.getPageList().get(1) == tb1).as("Correct page list contents").isTrue(); + + ((MutableSortDefinition) holder.getSort()).setProperty("name"); + holder.resort(); + assertThat(holder.getPageList().get(0) == tb3).as("Correct page list contents").isTrue(); + assertThat(holder.getPageList().get(1) == tb1).as("Correct page list contents").isTrue(); + + holder.setPage(1); + assertThat(holder.getPageList().size() == 1).as("Correct page list size").isTrue(); + assertThat(holder.getPageList().get(0) == tb2).as("Correct page list contents").isTrue(); + + ((MutableSortDefinition) holder.getSort()).setProperty("age"); + holder.resort(); + assertThat(holder.getPageList().get(0) == tb1).as("Correct page list contents").isTrue(); + assertThat(holder.getPageList().get(1) == tb3).as("Correct page list contents").isTrue(); + + ((MutableSortDefinition) holder.getSort()).setIgnoreCase(true); + holder.resort(); + assertThat(holder.getPageList().get(0) == tb1).as("Correct page list contents").isTrue(); + assertThat(holder.getPageList().get(1) == tb3).as("Correct page list contents").isTrue(); + + holder.nextPage(); + assertThat(holder.getPage()).isEqualTo(1); + holder.previousPage(); + assertThat(holder.getPage()).isEqualTo(0); + holder.nextPage(); + assertThat(holder.getPage()).isEqualTo(1); + holder.nextPage(); + assertThat(holder.getPage()).isEqualTo(1); + holder.previousPage(); + assertThat(holder.getPage()).isEqualTo(0); + holder.previousPage(); + assertThat(holder.getPage()).isEqualTo(0); + } + + + + public static class MockFilter { + + private String name = ""; + private String age = ""; + private String extendedInfo = ""; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAge() { + return age; + } + + public void setAge(String age) { + this.age = age; + } + + public String getExtendedInfo() { + return extendedInfo; + } + + public void setExtendedInfo(String extendedInfo) { + this.extendedInfo = extendedInfo; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof MockFilter)) return false; + + final MockFilter mockFilter = (MockFilter) o; + + if (!age.equals(mockFilter.age)) return false; + if (!extendedInfo.equals(mockFilter.extendedInfo)) return false; + if (!name.equals(mockFilter.name)) return false; + + return true; + } + + @Override + public int hashCode() { + int result; + result = name.hashCode(); + result = 29 * result + age.hashCode(); + result = 29 * result + extendedInfo.hashCode(); + return result; + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/support/PropertyComparatorTests.java b/spring-beans/src/test/java/org/springframework/beans/support/PropertyComparatorTests.java new file mode 100644 index 0000000..3ca5a5d --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/support/PropertyComparatorTests.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.support; + +import java.util.Comparator; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link PropertyComparator}. + * + * @author Keith Donald + * @author Chris Beams + */ +public class PropertyComparatorTests { + + @Test + public void testPropertyComparator() { + Dog dog = new Dog(); + dog.setNickName("mace"); + + Dog dog2 = new Dog(); + dog2.setNickName("biscy"); + + PropertyComparator c = new PropertyComparator<>("nickName", false, true); + assertThat(c.compare(dog, dog2) > 0).isTrue(); + assertThat(c.compare(dog, dog) == 0).isTrue(); + assertThat(c.compare(dog2, dog) < 0).isTrue(); + } + + @Test + public void testPropertyComparatorNulls() { + Dog dog = new Dog(); + Dog dog2 = new Dog(); + PropertyComparator c = new PropertyComparator<>("nickName", false, true); + assertThat(c.compare(dog, dog2) == 0).isTrue(); + } + + @Test + public void testChainedComparators() { + Comparator c = new PropertyComparator<>("lastName", false, true); + + Dog dog1 = new Dog(); + dog1.setFirstName("macy"); + dog1.setLastName("grayspots"); + + Dog dog2 = new Dog(); + dog2.setFirstName("biscuit"); + dog2.setLastName("grayspots"); + + assertThat(c.compare(dog1, dog2) == 0).isTrue(); + + c = c.thenComparing(new PropertyComparator<>("firstName", false, true)); + assertThat(c.compare(dog1, dog2) > 0).isTrue(); + + dog2.setLastName("konikk dog"); + assertThat(c.compare(dog2, dog1) > 0).isTrue(); + } + + @Test + public void testChainedComparatorsReversed() { + Comparator c = (new PropertyComparator("lastName", false, true)). + thenComparing(new PropertyComparator<>("firstName", false, true)); + + Dog dog1 = new Dog(); + dog1.setFirstName("macy"); + dog1.setLastName("grayspots"); + + Dog dog2 = new Dog(); + dog2.setFirstName("biscuit"); + dog2.setLastName("grayspots"); + + assertThat(c.compare(dog1, dog2) > 0).isTrue(); + c = c.reversed(); + assertThat(c.compare(dog1, dog2) < 0).isTrue(); + } + + + @SuppressWarnings("unused") + private static class Dog implements Comparable { + + private String nickName; + + private String firstName; + + private String lastName; + + public String getNickName() { + return nickName; + } + + public void setNickName(String nickName) { + this.nickName = nickName; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + @Override + public int compareTo(Object o) { + return this.nickName.compareTo(((Dog) o).nickName); + } + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/support/ProtectedBaseBean.java b/spring-beans/src/test/java/org/springframework/beans/support/ProtectedBaseBean.java new file mode 100644 index 0000000..4c5528c --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/support/ProtectedBaseBean.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.support; + +/** + * @author Juergen Hoeller + * @since 29.07.2004 + */ +class ProtectedBaseBean { + + private String someProperty; + + public void setSomeProperty(String someProperty) { + this.someProperty = someProperty; + } + + public String getSomeProperty() { + return someProperty; + } + +} diff --git a/spring-beans/src/test/kotlin/org/springframework/beans/KotlinBeanUtilsTests.kt b/spring-beans/src/test/kotlin/org/springframework/beans/KotlinBeanUtilsTests.kt new file mode 100644 index 0000000..647d320 --- /dev/null +++ b/spring-beans/src/test/kotlin/org/springframework/beans/KotlinBeanUtilsTests.kt @@ -0,0 +1,131 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +/** + * Kotlin tests for {@link BeanUtils}. + * + * @author Sebastien Deleuze + */ +@Suppress("unused", "UNUSED_PARAMETER") +class KotlinBeanUtilsTests { + + @Test + fun `Instantiate immutable class`() { + val constructor = BeanUtils.findPrimaryConstructor(Foo::class.java)!! + val foo = BeanUtils.instantiateClass(constructor, "a", 3) + assertThat(foo.param1).isEqualTo("a") + assertThat(foo.param2).isEqualTo(3) + } + + @Test + fun `Instantiate immutable class with optional parameter and all parameters specified`() { + val constructor = BeanUtils.findPrimaryConstructor(Bar::class.java)!! + val bar = BeanUtils.instantiateClass(constructor, "a", 8) + assertThat(bar.param1).isEqualTo("a") + assertThat(bar.param2).isEqualTo(8) + } + + @Test + fun `Instantiate immutable class with optional parameter and only mandatory parameters specified by position`() { + val constructor = BeanUtils.findPrimaryConstructor(Bar::class.java)!! + val bar = BeanUtils.instantiateClass(constructor, "a") + assertThat(bar.param1).isEqualTo("a") + assertThat(bar.param2).isEqualTo(12) + } + + @Test + fun `Instantiate immutable class with optional parameter specified with null value`() { + val constructor = BeanUtils.findPrimaryConstructor(Bar::class.java)!! + val bar = BeanUtils.instantiateClass(constructor, "a", null) + assertThat(bar.param1).isEqualTo("a") + assertThat(bar.param2).isEqualTo(12) + } + + @Test // gh-22531 + fun `Instantiate immutable class with nullable parameter`() { + val constructor = BeanUtils.findPrimaryConstructor(Qux::class.java)!! + val bar = BeanUtils.instantiateClass(constructor, "a", null) + assertThat(bar.param1).isEqualTo("a") + assertThat(bar.param2).isNull() + } + + @Test // SPR-15851 + fun `Instantiate mutable class with declared constructor and default values for all parameters`() { + val baz = BeanUtils.instantiateClass(Baz::class.java.getDeclaredConstructor()) + assertThat(baz.param1).isEqualTo("a") + assertThat(baz.param2).isEqualTo(12) + } + + @Test + @Suppress("UsePropertyAccessSyntax") + fun `Instantiate class with private constructor`() { + BeanUtils.instantiateClass(PrivateConstructor::class.java.getDeclaredConstructor()) + } + + @Test + fun `Instantiate class with protected constructor`() { + BeanUtils.instantiateClass(ProtectedConstructor::class.java.getDeclaredConstructor()) + } + + @Test + fun `Instantiate private class`() { + BeanUtils.instantiateClass(PrivateClass::class.java.getDeclaredConstructor()) + } + + class Foo(val param1: String, val param2: Int) + + class Bar(val param1: String, val param2: Int = 12) + + class Baz(var param1: String = "a", var param2: Int = 12) + + class Qux(val param1: String, val param2: Int?) + + class TwoConstructorsWithDefaultOne { + + constructor() + + constructor(param1: String) + } + + class TwoConstructorsWithoutDefaultOne { + + constructor(param1: String) + + constructor(param1: String, param2: String) + } + + class OneConstructorWithDefaultOne { + + constructor() + } + + class OneConstructorWithoutDefaultOne { + + constructor(param1: String) + } + + class PrivateConstructor private constructor() + + open class ProtectedConstructor protected constructor() + + private class PrivateClass + +} diff --git a/spring-beans/src/test/kotlin/org/springframework/beans/factory/BeanFactoryExtensionsTests.kt b/spring-beans/src/test/kotlin/org/springframework/beans/factory/BeanFactoryExtensionsTests.kt new file mode 100644 index 0000000..6ba9e5d --- /dev/null +++ b/spring-beans/src/test/kotlin/org/springframework/beans/factory/BeanFactoryExtensionsTests.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory + +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.Test +import org.springframework.core.ResolvableType + +/** + * Mock object based tests for BeanFactory Kotlin extensions. + * + * @author Sebastien Deleuze + */ +class BeanFactoryExtensionsTests { + + val bf = mockk(relaxed = true) + + @Test + fun `getBean with reified type parameters`() { + bf.getBean() + verify { bf.getBean(Foo::class.java) } + } + + @Test + fun `getBean with String and reified type parameters`() { + val name = "foo" + bf.getBean(name) + verify { bf.getBean(name, Foo::class.java) } + } + + @Test + fun `getBean with reified type parameters and varargs`() { + val arg1 = "arg1" + val arg2 = "arg2" + bf.getBean(arg1, arg2) + verify { bf.getBean(Foo::class.java, arg1, arg2) } + } + + @Test + fun `getBeanProvider with reified type parameters`() { + bf.getBeanProvider() + verify { bf.getBeanProvider>(ofType()) } + } + + class Foo +} diff --git a/spring-beans/src/test/kotlin/org/springframework/beans/factory/ListableBeanFactoryExtensionsTests.kt b/spring-beans/src/test/kotlin/org/springframework/beans/factory/ListableBeanFactoryExtensionsTests.kt new file mode 100644 index 0000000..a86a60a --- /dev/null +++ b/spring-beans/src/test/kotlin/org/springframework/beans/factory/ListableBeanFactoryExtensionsTests.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory + +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.Test + +/** + * Mock object based tests for ListableBeanFactory Kotlin extensions + * + * @author Sebastien Deleuze + */ +class ListableBeanFactoryExtensionsTests { + + val lbf = mockk(relaxed = true) + + @Test + fun `getBeanNamesForType with reified type parameters`() { + lbf.getBeanNamesForType() + verify { lbf.getBeanNamesForType(Foo::class.java, true , true) } + } + + @Test + fun `getBeanNamesForType with reified type parameters and Boolean`() { + lbf.getBeanNamesForType(false) + verify { lbf.getBeanNamesForType(Foo::class.java, false , true) } + } + + @Test + fun `getBeanNamesForType with reified type parameters, Boolean and Boolean`() { + lbf.getBeanNamesForType(false, false) + verify { lbf.getBeanNamesForType(Foo::class.java, false , false) } + } + + @Test + fun `getBeansOfType with reified type parameters`() { + lbf.getBeansOfType() + verify { lbf.getBeansOfType(Foo::class.java, true , true) } + } + + @Test + fun `getBeansOfType with reified type parameters and Boolean`() { + lbf.getBeansOfType(false) + verify { lbf.getBeansOfType(Foo::class.java, false , true) } + } + + @Test + fun `getBeansOfType with reified type parameters, Boolean and Boolean`() { + lbf.getBeansOfType(false, false) + verify { lbf.getBeansOfType(Foo::class.java, false , false) } + } + + @Test + fun `getBeanNamesForAnnotation with reified type parameters`() { + lbf.getBeanNamesForAnnotation() + verify { lbf.getBeanNamesForAnnotation(Bar::class.java) } + } + + @Test + fun `getBeansWithAnnotation with reified type parameters`() { + lbf.getBeansWithAnnotation() + verify { lbf.getBeansWithAnnotation(Bar::class.java) } + } + + @Test + fun `findAnnotationOnBean with String and reified type parameters`() { + val name = "bar" + lbf.findAnnotationOnBean(name) + verify { lbf.findAnnotationOnBean(name, Bar::class.java) } + } + + class Foo + + annotation class Bar +} diff --git a/spring-beans/src/test/kotlin/org/springframework/beans/factory/annotation/KotlinAutowiredTests.kt b/spring-beans/src/test/kotlin/org/springframework/beans/factory/annotation/KotlinAutowiredTests.kt new file mode 100644 index 0000000..37d6793 --- /dev/null +++ b/spring-beans/src/test/kotlin/org/springframework/beans/factory/annotation/KotlinAutowiredTests.kt @@ -0,0 +1,279 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.annotation + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.BeanCreationException +import org.springframework.beans.factory.support.DefaultListableBeanFactory +import org.springframework.beans.factory.support.RootBeanDefinition +import org.springframework.beans.testfixture.beans.Colour +import org.springframework.beans.testfixture.beans.TestBean + +/** + * Tests for Kotlin support with [Autowired]. + * + * @author Juergen Hoeller + * @author Sebastien Deleuze + */ +class KotlinAutowiredTests { + + @Test + fun `Autowiring with target`() { + val bf = DefaultListableBeanFactory() + val bpp = AutowiredAnnotationBeanPostProcessor() + bpp.setBeanFactory(bf) + bf.addBeanPostProcessor(bpp) + val bd = RootBeanDefinition(KotlinBean::class.java) + bd.scope = RootBeanDefinition.SCOPE_PROTOTYPE + bf.registerBeanDefinition("annotatedBean", bd) + val tb = TestBean() + bf.registerSingleton("testBean", tb) + + val kb = bf.getBean("annotatedBean", KotlinBean::class.java) + assertThat(kb.injectedFromConstructor).isSameAs(tb) + assertThat(kb.injectedFromMethod).isSameAs(tb) + assertThat(kb.injectedField).isSameAs(tb) + } + + @Test + fun `Autowiring without target`() { + val bf = DefaultListableBeanFactory() + val bpp = AutowiredAnnotationBeanPostProcessor() + bpp.setBeanFactory(bf) + bf.addBeanPostProcessor(bpp) + val bd = RootBeanDefinition(KotlinBean::class.java) + bd.scope = RootBeanDefinition.SCOPE_PROTOTYPE + bf.registerBeanDefinition("annotatedBean", bd) + + val kb = bf.getBean("annotatedBean", KotlinBean::class.java) + assertThat(kb.injectedFromConstructor).isNull() + assertThat(kb.injectedFromMethod).isNull() + assertThat(kb.injectedField).isNull() + } + + @Test // SPR-15847 + fun `Autowiring by primary constructor with mandatory and optional parameters`() { + val bf = DefaultListableBeanFactory() + val bpp = AutowiredAnnotationBeanPostProcessor() + bpp.setBeanFactory(bf) + bf.addBeanPostProcessor(bpp) + val bd = RootBeanDefinition(KotlinBeanWithMandatoryAndOptionalParameters::class.java) + bd.scope = RootBeanDefinition.SCOPE_PROTOTYPE + bf.registerBeanDefinition("bean", bd) + val tb = TestBean() + bf.registerSingleton("testBean", tb) + + val kb = bf.getBean("bean", KotlinBeanWithMandatoryAndOptionalParameters::class.java) + assertThat(kb.injectedFromConstructor).isSameAs(tb) + assertThat(kb.optional).isEqualTo("foo") + assertThat(kb.initializedField).isEqualTo("bar") + } + + @Test + fun `Autowiring by primary constructor with optional parameters`() { + val bf = DefaultListableBeanFactory() + val bpp = AutowiredAnnotationBeanPostProcessor() + bpp.setBeanFactory(bf) + bf.addBeanPostProcessor(bpp) + val bd = RootBeanDefinition(KotlinBeanWithOptionalParameters::class.java) + bd.scope = RootBeanDefinition.SCOPE_PROTOTYPE + bf.registerBeanDefinition("bean", bd) + + val kb = bf.getBean("bean", KotlinBeanWithOptionalParameters::class.java) + assertThat(kb.optional1).isNotNull() + assertThat(kb.optional2).isEqualTo("foo") + assertThat(kb.initializedField).isEqualTo("bar") + } + + @Test // SPR-15847 + fun `Autowiring by annotated primary constructor with optional parameter`() { + val bf = DefaultListableBeanFactory() + val bpp = AutowiredAnnotationBeanPostProcessor() + bpp.setBeanFactory(bf) + bf.addBeanPostProcessor(bpp) + val bd = RootBeanDefinition(KotlinBeanWithOptionalParameterAndExplicitConstructor::class.java) + bd.scope = RootBeanDefinition.SCOPE_PROTOTYPE + bf.registerBeanDefinition("bean", bd) + val tb = TestBean() + bf.registerSingleton("testBean", tb) + + val kb = bf.getBean("bean", KotlinBeanWithOptionalParameterAndExplicitConstructor::class.java) + assertThat(kb.injectedFromConstructor).isSameAs(tb) + assertThat(kb.optional).isEqualTo("foo") + } + + @Test // SPR-15847 + fun `Autowiring by annotated secondary constructor with optional parameter`() { + val bf = DefaultListableBeanFactory() + val bpp = AutowiredAnnotationBeanPostProcessor() + bpp.setBeanFactory(bf) + bf.addBeanPostProcessor(bpp) + val bd = RootBeanDefinition(KotlinBeanWithAutowiredSecondaryConstructor::class.java) + bd.scope = RootBeanDefinition.SCOPE_PROTOTYPE + bf.registerBeanDefinition("bean", bd) + val tb = TestBean() + bf.registerSingleton("testBean", tb) + val colour = Colour.BLUE + bf.registerSingleton("colour", colour) + + val kb = bf.getBean("bean", KotlinBeanWithAutowiredSecondaryConstructor::class.java) + assertThat(kb.injectedFromConstructor).isSameAs(tb) + assertThat(kb.optional).isEqualTo("bar") + assertThat(kb.injectedFromSecondaryConstructor).isSameAs(colour) + } + + @Test // SPR-16012 + fun `Fallback on the default constructor when no autowirable primary constructor is defined`() { + val bf = DefaultListableBeanFactory() + val bpp = AutowiredAnnotationBeanPostProcessor() + bpp.setBeanFactory(bf) + bf.addBeanPostProcessor(bpp) + val bd = RootBeanDefinition(KotlinBeanWithPrimaryAndDefaultConstructors::class.java) + bd.scope = RootBeanDefinition.SCOPE_PROTOTYPE + bf.registerBeanDefinition("bean", bd) + + val kb = bf.getBean("bean", KotlinBeanWithPrimaryAndDefaultConstructors::class.java) + assertThat(kb.testBean).isNotNull() + } + + @Test // SPR-16012 + fun `Instantiation via primary constructor when a default is defined`() { + val bf = DefaultListableBeanFactory() + val bpp = AutowiredAnnotationBeanPostProcessor() + bpp.setBeanFactory(bf) + bf.addBeanPostProcessor(bpp) + val bd = RootBeanDefinition(KotlinBeanWithPrimaryAndDefaultConstructors::class.java) + bd.scope = RootBeanDefinition.SCOPE_PROTOTYPE + bf.registerBeanDefinition("bean", bd) + val tb = TestBean() + bf.registerSingleton("testBean", tb) + + val kb = bf.getBean("bean", KotlinBeanWithPrimaryAndDefaultConstructors::class.java) + assertThat(kb.testBean).isEqualTo(tb) + } + + @Test // SPR-16289 + fun `Instantiation via secondary constructor when a default primary is defined`() { + val bf = DefaultListableBeanFactory() + val bpp = AutowiredAnnotationBeanPostProcessor() + bpp.setBeanFactory(bf) + bf.addBeanPostProcessor(bpp) + val bd = RootBeanDefinition(KotlinBeanWithPrimaryAndSecondaryConstructors::class.java) + bd.scope = RootBeanDefinition.SCOPE_PROTOTYPE + bf.registerBeanDefinition("bean", bd) + + bf.getBean(KotlinBeanWithPrimaryAndSecondaryConstructors::class.java, "foo") + bf.getBean(KotlinBeanWithPrimaryAndSecondaryConstructors::class.java) + } + + @Test // SPR-16022 + fun `No autowiring with primary and secondary non annotated constructors`() { + val bf = DefaultListableBeanFactory() + val bpp = AutowiredAnnotationBeanPostProcessor() + bpp.setBeanFactory(bf) + bf.addBeanPostProcessor(bpp) + val bd = RootBeanDefinition(KotlinBeanWithSecondaryConstructor::class.java) + bd.scope = RootBeanDefinition.SCOPE_PROTOTYPE + bf.registerBeanDefinition("bean", bd) + val tb = TestBean() + bf.registerSingleton("testBean", tb) + val colour = Colour.BLUE + bf.registerSingleton("colour", colour) + + assertThatExceptionOfType(BeanCreationException::class.java).isThrownBy { + bf.getBean("bean", KotlinBeanWithSecondaryConstructor::class.java) + } + } + + + class KotlinBean(val injectedFromConstructor: TestBean?) { + + var injectedFromMethod: TestBean? = null + + @Autowired + var injectedField: TestBean? = null + + @Autowired + fun injectedMethod(p1: TestBean?) { + injectedFromMethod = p1 + } + } + + class KotlinBeanWithMandatoryAndOptionalParameters( + val injectedFromConstructor: TestBean, + val optional: String = "foo" + ) { + var initializedField: String? = null + + init { + initializedField = "bar" + } + } + + class KotlinBeanWithOptionalParameters( + val optional1: TestBean = TestBean(), + val optional2: String = "foo" + ) { + var initializedField: String? = null + + init { + initializedField = "bar" + } + } + + class KotlinBeanWithOptionalParameterAndExplicitConstructor @Autowired constructor( + val optional: String = "foo", + val injectedFromConstructor: TestBean + ) + + class KotlinBeanWithAutowiredSecondaryConstructor( + val optional: String = "foo", + val injectedFromConstructor: TestBean + ) { + @Autowired constructor(injectedFromSecondaryConstructor: Colour, injectedFromConstructor: TestBean, + optional: String = "bar") : this(optional, injectedFromConstructor) { + this.injectedFromSecondaryConstructor = injectedFromSecondaryConstructor + } + + var injectedFromSecondaryConstructor: Colour? = null + } + + @Suppress("unused") + class KotlinBeanWithPrimaryAndDefaultConstructors(val testBean: TestBean) { + constructor() : this(TestBean()) + } + + @Suppress("unused", "UNUSED_PARAMETER") + class KotlinBeanWithPrimaryAndSecondaryConstructors() { + constructor(p: String) : this() + } + + class KotlinBeanWithSecondaryConstructor( + val optional: String = "foo", + val injectedFromConstructor: TestBean + ) { + constructor(injectedFromSecondaryConstructor: Colour, injectedFromConstructor: TestBean, + optional: String = "bar") : this(optional, injectedFromConstructor) { + this.injectedFromSecondaryConstructor = injectedFromSecondaryConstructor + } + + var injectedFromSecondaryConstructor: Colour? = null + } + +} diff --git a/spring-beans/src/test/resources/log4j2-test.xml b/spring-beans/src/test/resources/log4j2-test.xml new file mode 100644 index 0000000..fb1c94a --- /dev/null +++ b/spring-beans/src/test/resources/log4j2-test.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/BeanFactoryUtilsTests-dependentBeans.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/BeanFactoryUtilsTests-dependentBeans.xml new file mode 100644 index 0000000..7742f40 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/BeanFactoryUtilsTests-dependentBeans.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/BeanFactoryUtilsTests-leaf.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/BeanFactoryUtilsTests-leaf.xml new file mode 100644 index 0000000..435dcb0 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/BeanFactoryUtilsTests-leaf.xml @@ -0,0 +1,12 @@ + + + + + + + custom + 25 + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/BeanFactoryUtilsTests-middle.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/BeanFactoryUtilsTests-middle.xml new file mode 100644 index 0000000..4804e44 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/BeanFactoryUtilsTests-middle.xml @@ -0,0 +1,19 @@ + + + + + + + + custom + 666 + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/BeanFactoryUtilsTests-root.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/BeanFactoryUtilsTests-root.xml new file mode 100644 index 0000000..fea62a3 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/BeanFactoryUtilsTests-root.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + custom + 25 + + + + + + false + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/FactoryBeanLookupTests-context.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/FactoryBeanLookupTests-context.xml new file mode 100644 index 0000000..bbd84e2 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/FactoryBeanLookupTests-context.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/FactoryBeanTests-abstract.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/FactoryBeanTests-abstract.xml new file mode 100644 index 0000000..9bd5ca7 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/FactoryBeanTests-abstract.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/FactoryBeanTests-circular.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/FactoryBeanTests-circular.xml new file mode 100644 index 0000000..18fef0b --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/FactoryBeanTests-circular.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/FactoryBeanTests-returnsNull.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/FactoryBeanTests-returnsNull.xml new file mode 100644 index 0000000..a40ed35 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/FactoryBeanTests-returnsNull.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/FactoryBeanTests-withAutowiring.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/FactoryBeanTests-withAutowiring.xml new file mode 100644 index 0000000..90ce215 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/FactoryBeanTests-withAutowiring.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + yourName + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/annotation/CustomAutowireConfigurerTests-context.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/annotation/CustomAutowireConfigurerTests-context.xml new file mode 100644 index 0000000..f3481e4 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/annotation/CustomAutowireConfigurerTests-context.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/config/FieldRetrievingFactoryBeanTests-context.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/config/FieldRetrievingFactoryBeanTests-context.xml new file mode 100644 index 0000000..35c854b --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/config/FieldRetrievingFactoryBeanTests-context.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/config/ObjectFactoryCreatingFactoryBeanTests-context.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/config/ObjectFactoryCreatingFactoryBeanTests-context.xml new file mode 100644 index 0000000..960e6ed --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/config/ObjectFactoryCreatingFactoryBeanTests-context.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/config/PropertiesFactoryBeanTests-test.properties b/spring-beans/src/test/resources/org/springframework/beans/factory/config/PropertiesFactoryBeanTests-test.properties new file mode 100644 index 0000000..9affcba --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/config/PropertiesFactoryBeanTests-test.properties @@ -0,0 +1,2 @@ +tb.array[0].age=99 +tb.list[1].name=test diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/config/PropertiesFactoryBeanTests-test.properties.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/config/PropertiesFactoryBeanTests-test.properties.xml new file mode 100644 index 0000000..e39b872 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/config/PropertiesFactoryBeanTests-test.properties.xml @@ -0,0 +1,9 @@ + + + + + + 99 + test + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/config/PropertyPathFactoryBeanTests-context.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/config/PropertyPathFactoryBeanTests-context.xml new file mode 100644 index 0000000..d9979e3 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/config/PropertyPathFactoryBeanTests-context.xml @@ -0,0 +1,72 @@ + + + + + + + 10 + + + 11 + + + + + + 98 + + + 99 + + + + + + + + 12 + + + age + + + + tb + spouse.age + + + + + + + + + + + + tb + spouse + org.springframework.beans.testfixture.beans.TestBean + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/config/PropertyPlaceholderConfigurerTests.properties b/spring-beans/src/test/resources/org/springframework/beans/factory/config/PropertyPlaceholderConfigurerTests.properties new file mode 100644 index 0000000..b8f6978 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/config/PropertyPlaceholderConfigurerTests.properties @@ -0,0 +1 @@ +my.name=foo \ No newline at end of file diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/config/PropertyResourceConfigurerTests-test.properties b/spring-beans/src/test/resources/org/springframework/beans/factory/config/PropertyResourceConfigurerTests-test.properties new file mode 100644 index 0000000..9affcba --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/config/PropertyResourceConfigurerTests-test.properties @@ -0,0 +1,2 @@ +tb.array[0].age=99 +tb.list[1].name=test diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/config/PropertyResourceConfigurerTests-test.properties.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/config/PropertyResourceConfigurerTests-test.properties.xml new file mode 100644 index 0000000..e39b872 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/config/PropertyResourceConfigurerTests-test.properties.xml @@ -0,0 +1,9 @@ + + + + + + 99 + test + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/config/SimpleScopeTests-context.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/config/SimpleScopeTests-context.xml new file mode 100644 index 0000000..9ac321b --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/config/SimpleScopeTests-context.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/parsing/CustomProblemReporterTests-context.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/parsing/CustomProblemReporterTests-context.xml new file mode 100644 index 0000000..1c6e22b --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/parsing/CustomProblemReporterTests-context.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/support/genericBeanTests.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/support/genericBeanTests.xml new file mode 100644 index 0000000..c610f9c --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/support/genericBeanTests.xml @@ -0,0 +1,70 @@ + + + + + + + + + + value1 + value2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 20 + 30 + + + + + + + + + 20 + 30 + + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/support/lookupMethodTests.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/support/lookupMethodTests.xml new file mode 100644 index 0000000..a04f0e3 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/support/lookupMethodTests.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/support/multiConstructorArgs.properties b/spring-beans/src/test/resources/org/springframework/beans/factory/support/multiConstructorArgs.properties new file mode 100644 index 0000000..9cc82ac --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/support/multiConstructorArgs.properties @@ -0,0 +1,3 @@ +testBean.(class)=org.springframework.beans.testfixture.beans.TestBean +testBean.$0=Rob Harrop +testBean.$1=23 diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/support/refConstructorArg.properties b/spring-beans/src/test/resources/org/springframework/beans/factory/support/refConstructorArg.properties new file mode 100644 index 0000000..b1bb01e --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/support/refConstructorArg.properties @@ -0,0 +1,5 @@ +sally.(class)=org.springframework.beans.testfixture.beans.TestBean +sally.name=Sally + +rob.(class)=org.springframework.beans.testfixture.beans.TestBean +rob.$0(ref)=sally diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/support/security/callbacks.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/support/security/callbacks.xml new file mode 100644 index 0000000..1df2d27 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/support/security/callbacks.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + foo + bar + + + + + \ No newline at end of file diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/support/security/policy.all b/spring-beans/src/test/resources/org/springframework/beans/factory/support/security/policy.all new file mode 100644 index 0000000..de8e185 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/support/security/policy.all @@ -0,0 +1,3 @@ +grant { + permission java.security.AllPermission; +}; \ No newline at end of file diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/support/simpleConstructorArg.properties b/spring-beans/src/test/resources/org/springframework/beans/factory/support/simpleConstructorArg.properties new file mode 100644 index 0000000..bc39b1b --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/support/simpleConstructorArg.properties @@ -0,0 +1,2 @@ +testBean.(class)=org.springframework.beans.testfixture.beans.TestBean +testBean.$0=Rob Harrop diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/DuplicateBeanIdTests-multiLevel-context.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/DuplicateBeanIdTests-multiLevel-context.xml new file mode 100644 index 0000000..0e32679 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/DuplicateBeanIdTests-multiLevel-context.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/DuplicateBeanIdTests-sameLevel-context.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/DuplicateBeanIdTests-sameLevel-context.xml new file mode 100644 index 0000000..40ecbec --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/DuplicateBeanIdTests-sameLevel-context.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests-autowire-candidates-context.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests-autowire-candidates-context.xml new file mode 100644 index 0000000..2749cd2 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests-autowire-candidates-context.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests-autowire-context.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests-autowire-context.xml new file mode 100644 index 0000000..632d035 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests-autowire-context.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests-init-destroy-context.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests-init-destroy-context.xml new file mode 100644 index 0000000..bc03075 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests-init-destroy-context.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests-lazy-context.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests-lazy-context.xml new file mode 100644 index 0000000..78601eb --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests-lazy-context.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests-merge-context.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests-merge-context.xml new file mode 100644 index 0000000..f3453c7 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/NestedBeansElementAttributeRecursionTests-merge-context.xml @@ -0,0 +1,47 @@ + + + + + + + alpha + bravo + + + + + + + + charlie + delta + + + + + + + + + echo + foxtrot + + + + + + + + + golf + hotel + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/NestedBeansElementTests-context.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/NestedBeansElementTests-context.xml new file mode 100644 index 0000000..8a44f25 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/NestedBeansElementTests-context.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-customDefaultProfile.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-customDefaultProfile.xml new file mode 100644 index 0000000..74c2c2f --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-customDefaultProfile.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-defaultAndDevProfile.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-defaultAndDevProfile.xml new file mode 100644 index 0000000..fcb24a4 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-defaultAndDevProfile.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-defaultProfile.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-defaultProfile.xml new file mode 100644 index 0000000..ca8cbf4 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-defaultProfile.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-devProfile.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-devProfile.xml new file mode 100644 index 0000000..7f391e8 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-devProfile.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-multiProfile.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-multiProfile.xml new file mode 100644 index 0000000..e3a36f2 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-multiProfile.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-multiProfileNegated.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-multiProfileNegated.xml new file mode 100644 index 0000000..9c35ae7 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-multiProfileNegated.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-multiProfileNotDev.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-multiProfileNotDev.xml new file mode 100644 index 0000000..f5820c5 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-multiProfileNotDev.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-noProfile.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-noProfile.xml new file mode 100644 index 0000000..49a8c7a --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-noProfile.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-notDevProfile.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-notDevProfile.xml new file mode 100644 index 0000000..9d18522 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-notDevProfile.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-prodProfile.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-prodProfile.xml new file mode 100644 index 0000000..879b23c --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-prodProfile.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-spaceDelimitedProfile.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-spaceDelimitedProfile.xml new file mode 100644 index 0000000..3d50231 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-spaceDelimitedProfile.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-unknownProfile.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-unknownProfile.xml new file mode 100644 index 0000000..0b0dbce --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ProfileXmlBeanDefinitionTests-unknownProfile.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/autowire-constructor-with-exclusion.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/autowire-constructor-with-exclusion.xml new file mode 100644 index 0000000..2b25151 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/autowire-constructor-with-exclusion.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + name=props1 + + + + + + name=props2 + + + + \ No newline at end of file diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/autowire-with-exclusion.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/autowire-with-exclusion.xml new file mode 100644 index 0000000..692cc77 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/autowire-with-exclusion.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + name=props1 + + + + + + name=props2 + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/autowire-with-inclusion.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/autowire-with-inclusion.xml new file mode 100644 index 0000000..f4090d1 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/autowire-with-inclusion.xml @@ -0,0 +1,27 @@ + + + + + + + + + + name=props1 + + + + + + name=props2 + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/autowire-with-selective-inclusion.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/autowire-with-selective-inclusion.xml new file mode 100644 index 0000000..d52ba19 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/autowire-with-selective-inclusion.xml @@ -0,0 +1,33 @@ + + + + + + + + + + name=props1 + + + + + + name=props2 + + + + + + name=someProps + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/beanEvents.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/beanEvents.xml new file mode 100644 index 0000000..2bdea18 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/beanEvents.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/beanEventsImported.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/beanEventsImported.xml new file mode 100644 index 0000000..299c52a --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/beanEventsImported.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/beanNameGeneration.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/beanNameGeneration.xml new file mode 100644 index 0000000..1f22cad --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/beanNameGeneration.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/collectionMerging.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/collectionMerging.xml new file mode 100644 index 0000000..d457647 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/collectionMerging.xml @@ -0,0 +1,205 @@ + + + + + + + + + Rob Harrop + Rod Johnson + + + + + + + + Juergen Hoeller + + + + + + + + + + + + + + + + Rob Harrop + + + + + + + + Sally Greenwood + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Sall + Kerry + + + + + + + + Eva + Sally + + + + + + + + + Rob Harrop + Rod Johnson + + + + + + + + Juergen Hoeller + + + + + + + + + + + + + + + + Rob Harrop + + + + + + + + Sally Greenwood + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Sall + Kerry + + + + + + + + Eva + Sally + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/collections.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/collections.xml new file mode 100644 index 0000000..5683b4f --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/collections.xml @@ -0,0 +1,411 @@ + + + + + Jenny + 30 + + + + + + + + + Simple bean, without any collections. + + + The name of the user + David + + 27 + + + + Rod + 32 + + List of Rod's friends + + + + + + + + + Jenny + 30 + + + + + + + David + 27 + + + + + Rod + 32 + + + + + + + + + + + loner + 26 + + + My List + + + + + + + + + + literal + + + + + literal2 + + + + + + + + + + literal + + + + + + + + + verbose + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 10 + + + + + + + + + + + + + + + + + + + + + + + + + + + A map entry with a description + v1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + bar + + + + + zero + + bar + + + + + ba + + + + + + + bar + + + + + + + + + + + + + + + + + + bar + + + + + + + + + + bar + + + + + + + + + + + + + + + + + + + + + + + + + + + bar + TWO + + + + + + + + + + + + + + + + + one + + + + + + + + + 0 + 1 + 2 + + + + + + + + + + + + + + + + bar + jenny + + + + java.util.LinkedList + + + + + + + bar + jenny + + + + java.util.LinkedList + + + true + + + + + + + bar + jenny + + + + java.util.TreeSet + + + + + + + bar + jenny + + + + java.util.TreeSet + + + true + + + + + + + bar + jenny + + + + java.util.TreeMap + + + + + + + bar + jenny + + + + java.util.TreeMap + + + true + + + + + + + My Map + + + + + + + + + + + My Set + ONE + TWO + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/collectionsWithDefaultTypes.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/collectionsWithDefaultTypes.xml new file mode 100644 index 0000000..a1a1ea3 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/collectionsWithDefaultTypes.xml @@ -0,0 +1,78 @@ + + + + + + + 1 + 2 + 3 + + + + + 1 + 2 + 3 + + + + + + + + + + + + + + + + + + 1 + + true + + + + 2 + + false + + + + 3 + + false + + + + 4 + + true + + + + + + + + + literal + + 2 + 4 + + + 3 + 5 + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/defaultLifecycleMethods.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/defaultLifecycleMethods.xml new file mode 100644 index 0000000..9bd3f13 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/defaultLifecycleMethods.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/factory-methods.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/factory-methods.xml new file mode 100644 index 0000000..93f338b --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/factory-methods.xml @@ -0,0 +1,159 @@ + + + + + + setterString + + + + + + + + + + + + + + + + + + setterString + + + + + 27 + gotcha + + + + + + 27 + + + + + + 27 + + + + + + 27 + + + + + + + + + + + + + + setterString + + + + + testBeanOnlyPrototypeDISetterString + + + + + + + + 27 + gotcha + + + + + + 27 + gotcha + bogus + + + + + + Juergen + + + + + + + + Rod + 33 + + + + + + + + + + + + + + + + + + instanceFactory + + + + + + true + + + someuser + somepw + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ignoreDefaultLifecycleMethods.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ignoreDefaultLifecycleMethods.xml new file mode 100644 index 0000000..25ffd42 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/ignoreDefaultLifecycleMethods.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/import.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/import.xml new file mode 100644 index 0000000..44b1c8f --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/import.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/importPattern.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/importPattern.xml new file mode 100644 index 0000000..329d258 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/importPattern.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/invalidPerSchema.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/invalidPerSchema.xml new file mode 100644 index 0000000..6715273 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/invalidPerSchema.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/schemaValidated.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/schemaValidated.xml new file mode 100644 index 0000000..59b5ef7 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/schemaValidated.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/simpleConstructorNamespaceHandlerTests.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/simpleConstructorNamespaceHandlerTests.xml new file mode 100644 index 0000000..c8adc6b --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/simpleConstructorNamespaceHandlerTests.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/simpleConstructorNamespaceHandlerTestsWithErrors.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/simpleConstructorNamespaceHandlerTestsWithErrors.xml new file mode 100644 index 0000000..3dbfb5f --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/simpleConstructorNamespaceHandlerTestsWithErrors.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/simplePropertyNamespaceHandlerTests.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/simplePropertyNamespaceHandlerTests.xml new file mode 100644 index 0000000..83e74fa --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/simplePropertyNamespaceHandlerTests.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/simplePropertyNamespaceHandlerTestsWithErrors.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/simplePropertyNamespaceHandlerTestsWithErrors.xml new file mode 100644 index 0000000..b64093d --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/simplePropertyNamespaceHandlerTestsWithErrors.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/test.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/test.xml new file mode 100644 index 0000000..7c4c759 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/test.xml @@ -0,0 +1,127 @@ + + + + + + + + I have no properties and I'm happy without them. + + + + + + aliased + + + + + + + + aliased + + + + + + aliased + + + + + + + + + + + + + + Rod + 31 + + + + + + Roderick + + + + + Kerry + 34 + + + + + + Kathy + 28 + + + + + typeMismatch + 34x + + + + + + + + true + + + + true + + + + + + + + false + + + + + + + + + + + + + + + + + + + + + + + listenerVeto + 66 + + + + + + this is a ]]> + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/testUtilNamespace.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/testUtilNamespace.xml new file mode 100644 index 0000000..972aabc --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/testUtilNamespace.xml @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + My scoped Map + + + + + + + + + + + + + Rob Harrop + + + + My scoped List + Rob Harrop + + + + Rob Harrop + + + + My scoped Set + Rob Harrop + + + + + + foo + + + + + bar + + + + + + + bar + + + + + + + + + + + + + + + + + + + + + + + + + + + + min + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + bar2 + + + + bar2 + + + + local + local2 + + + + local + local2 + + + + local + local2 + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/util.properties b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/util.properties new file mode 100644 index 0000000..c9f0304 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/util.properties @@ -0,0 +1 @@ +foo=bar \ No newline at end of file diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/validateWithDtd.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/validateWithDtd.xml new file mode 100644 index 0000000..fbe7861 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/validateWithDtd.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/validateWithXsd.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/validateWithXsd.xml new file mode 100644 index 0000000..1e46734 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/validateWithXsd.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/spring-beans/src/test/resources/org/springframework/beans/factory/xml/withMeta.xml b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/withMeta.xml new file mode 100644 index 0000000..5f73881 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/factory/xml/withMeta.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + diff --git a/spring-beans/src/test/resources/org/springframework/beans/propertyeditors/ResourceBundleEditorTests.properties b/spring-beans/src/test/resources/org/springframework/beans/propertyeditors/ResourceBundleEditorTests.properties new file mode 100644 index 0000000..ecb69cb --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/propertyeditors/ResourceBundleEditorTests.properties @@ -0,0 +1 @@ +punk=punk \ No newline at end of file diff --git a/spring-beans/src/test/resources/org/springframework/beans/propertyeditors/ResourceBundleEditorTestsLangCountryDialect_en_GB_GLASGOW.properties b/spring-beans/src/test/resources/org/springframework/beans/propertyeditors/ResourceBundleEditorTestsLangCountryDialect_en_GB_GLASGOW.properties new file mode 100644 index 0000000..70055b6 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/propertyeditors/ResourceBundleEditorTestsLangCountryDialect_en_GB_GLASGOW.properties @@ -0,0 +1 @@ +punk=ned \ No newline at end of file diff --git a/spring-beans/src/test/resources/org/springframework/beans/propertyeditors/ResourceBundleEditorTestsLangCountry_en_GB.properties b/spring-beans/src/test/resources/org/springframework/beans/propertyeditors/ResourceBundleEditorTestsLangCountry_en_GB.properties new file mode 100644 index 0000000..4b790a4 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/propertyeditors/ResourceBundleEditorTestsLangCountry_en_GB.properties @@ -0,0 +1 @@ +punk=chav \ No newline at end of file diff --git a/spring-beans/src/test/resources/org/springframework/beans/propertyeditors/ResourceBundleEditorTestsLang_en.properties b/spring-beans/src/test/resources/org/springframework/beans/propertyeditors/ResourceBundleEditorTestsLang_en.properties new file mode 100644 index 0000000..de8a7b3 --- /dev/null +++ b/spring-beans/src/test/resources/org/springframework/beans/propertyeditors/ResourceBundleEditorTestsLang_en.properties @@ -0,0 +1 @@ +punk=yob \ No newline at end of file diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/AgeHolder.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/AgeHolder.java new file mode 100644 index 0000000..722cea9 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/AgeHolder.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + +public interface AgeHolder { + + default int age() { + return getAge(); + } + + int getAge(); + + void setAge(int age); + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/AnnotatedBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/AnnotatedBean.java new file mode 100644 index 0000000..985cf64 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/AnnotatedBean.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + +/** + * @author Stephane Nicoll + */ +@TestAnnotation +public class AnnotatedBean { +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/BooleanTestBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/BooleanTestBean.java new file mode 100644 index 0000000..5edbd30 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/BooleanTestBean.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + +/** + * @author Juergen Hoeller + * @author Chris Beams + * @since 10.06.2003 + */ +public class BooleanTestBean { + + private boolean bool1; + + private Boolean bool2; + + public boolean isBool1() { + return bool1; + } + + public void setBool1(boolean bool1) { + this.bool1 = bool1; + } + + public Boolean getBool2() { + return bool2; + } + + public void setBool2(Boolean bool2) { + this.bool2 = bool2; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/CollectingReaderEventListener.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/CollectingReaderEventListener.java new file mode 100644 index 0000000..cd213a4 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/CollectingReaderEventListener.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.parsing.AliasDefinition; +import org.springframework.beans.factory.parsing.ComponentDefinition; +import org.springframework.beans.factory.parsing.DefaultsDefinition; +import org.springframework.beans.factory.parsing.ImportDefinition; +import org.springframework.beans.factory.parsing.ReaderEventListener; + +/** + * @author Rob Harrop + * @author Juergen Hoeller + */ +public class CollectingReaderEventListener implements ReaderEventListener { + + private final List defaults = new ArrayList<>(); + + private final Map componentDefinitions = new LinkedHashMap<>(8); + + private final Map> aliasMap = new LinkedHashMap<>(8); + + private final List imports = new ArrayList<>(); + + + @Override + public void defaultsRegistered(DefaultsDefinition defaultsDefinition) { + this.defaults.add(defaultsDefinition); + } + + public List getDefaults() { + return Collections.unmodifiableList(this.defaults); + } + + @Override + public void componentRegistered(ComponentDefinition componentDefinition) { + this.componentDefinitions.put(componentDefinition.getName(), componentDefinition); + } + + public ComponentDefinition getComponentDefinition(String name) { + return this.componentDefinitions.get(name); + } + + public ComponentDefinition[] getComponentDefinitions() { + Collection collection = this.componentDefinitions.values(); + return collection.toArray(new ComponentDefinition[0]); + } + + @Override + public void aliasRegistered(AliasDefinition aliasDefinition) { + List aliases = this.aliasMap.computeIfAbsent(aliasDefinition.getBeanName(), k -> new ArrayList<>()); + aliases.add(aliasDefinition); + } + + public List getAliases(String beanName) { + List aliases = this.aliasMap.get(beanName); + return (aliases != null ? Collections.unmodifiableList(aliases) : null); + } + + @Override + public void importProcessed(ImportDefinition importDefinition) { + this.imports.add(importDefinition); + } + + public List getImports() { + return Collections.unmodifiableList(this.imports); + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/Colour.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/Colour.java new file mode 100644 index 0000000..5311490 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/Colour.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + +/** + * @author Rob Harrop + * @author Juergen Hoeller + */ +public class Colour { + + public static final Colour RED = new Colour("RED"); + public static final Colour BLUE = new Colour("BLUE"); + public static final Colour GREEN = new Colour("GREEN"); + public static final Colour PURPLE = new Colour("PURPLE"); + + private final String name; + + public Colour(String name) { + this.name = name; + } + + @Override + public String toString() { + return this.name; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/CountingTestBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/CountingTestBean.java new file mode 100644 index 0000000..aa9083a --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/CountingTestBean.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2005 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + + +/** + * @author Juergen Hoeller + * @since 15.03.2005 + */ +public class CountingTestBean extends TestBean { + + public static int count = 0; + + public CountingTestBean() { + count++; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/CustomEnum.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/CustomEnum.java new file mode 100644 index 0000000..de0d721 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/CustomEnum.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + +/** + * @author Juergen Hoeller + */ +public enum CustomEnum { + + VALUE_1, VALUE_2; + + @Override + public String toString() { + return "CustomEnum: " + name(); + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/DependenciesBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/DependenciesBean.java new file mode 100644 index 0000000..f990781 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/DependenciesBean.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; + +/** + * Simple bean used to test dependency checking. + * + * @author Rod Johnson + * @since 04.09.2003 + */ +public class DependenciesBean implements BeanFactoryAware { + + private int age; + + private String name; + + private TestBean spouse; + + private BeanFactory beanFactory; + + + public void setAge(int age) { + this.age = age; + } + + public int getAge() { + return age; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setSpouse(TestBean spouse) { + this.spouse = spouse; + } + + public TestBean getSpouse() { + return spouse; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + public BeanFactory getBeanFactory() { + return beanFactory; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/DerivedTestBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/DerivedTestBean.java new file mode 100644 index 0000000..c0e9c2e --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/DerivedTestBean.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + +import java.io.Serializable; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; + +/** + * @author Juergen Hoeller + * @since 21.08.2003 + */ +@SuppressWarnings("serial") +public class DerivedTestBean extends TestBean implements Serializable, BeanNameAware, DisposableBean { + + private String beanName; + + private boolean initialized; + + private boolean destroyed; + + + public DerivedTestBean() { + } + + public DerivedTestBean(String[] names) { + if (names == null || names.length < 2) { + throw new IllegalArgumentException("Invalid names array"); + } + setName(names[0]); + setBeanName(names[1]); + } + + public static DerivedTestBean create(String[] names) { + return new DerivedTestBean(names); + } + + + @Override + public void setBeanName(String beanName) { + if (this.beanName == null || beanName == null) { + this.beanName = beanName; + } + } + + @Override + public String getBeanName() { + return beanName; + } + + public void setActualSpouse(TestBean spouse) { + setSpouse(spouse); + } + + public void setSpouseRef(String name) { + setSpouse(new TestBean(name)); + } + + @Override + public TestBean getSpouse() { + return (TestBean) super.getSpouse(); + } + + + public void initialize() { + this.initialized = true; + } + + public boolean wasInitialized() { + return initialized; + } + + + @Override + public void destroy() { + this.destroyed = true; + } + + @Override + public boolean wasDestroyed() { + return destroyed; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/DummyBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/DummyBean.java new file mode 100644 index 0000000..cac5cce --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/DummyBean.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + +/** + * @author Costin Leau + */ +public class DummyBean { + + private Object value; + private String name; + private int age; + private TestBean spouse; + + public DummyBean(Object value) { + this.value = value; + } + + public DummyBean(String name, int age) { + this.name = name; + this.age = age; + } + + public DummyBean(int ageRef, String nameRef) { + this.name = nameRef; + this.age = ageRef; + } + + public DummyBean(String name, TestBean spouse) { + this.name = name; + this.spouse = spouse; + } + + public DummyBean(String name, Object value, int age) { + this.name = name; + this.value = value; + this.age = age; + } + + public Object getValue() { + return value; + } + + public String getName() { + return name; + } + + public int getAge() { + return age; + } + + public TestBean getSpouse() { + return spouse; + } +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/DummyFactory.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/DummyFactory.java new file mode 100644 index 0000000..2cf683a --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/DummyFactory.java @@ -0,0 +1,185 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; + +/** + * Simple factory to allow testing of FactoryBean support in AbstractBeanFactory. + * Depending on whether its singleton property is set, it will return a singleton + * or a prototype instance. + * + *

    Implements InitializingBean interface, so we can check that + * factories get this lifecycle callback if they want. + * + * @author Rod Johnson + * @author Chris Beams + * @since 10.03.2003 + */ +public class DummyFactory + implements FactoryBean, BeanNameAware, BeanFactoryAware, InitializingBean, DisposableBean { + + public static final String SINGLETON_NAME = "Factory singleton"; + + private static boolean prototypeCreated; + + /** + * Clear static state. + */ + public static void reset() { + prototypeCreated = false; + } + + + /** + * Default is for factories to return a singleton instance. + */ + private boolean singleton = true; + + private String beanName; + + private AutowireCapableBeanFactory beanFactory; + + private boolean postProcessed; + + private boolean initialized; + + private TestBean testBean; + + private TestBean otherTestBean; + + + public DummyFactory() { + this.testBean = new TestBean(); + this.testBean.setName(SINGLETON_NAME); + this.testBean.setAge(25); + } + + /** + * Return if the bean managed by this factory is a singleton. + * @see FactoryBean#isSingleton() + */ + @Override + public boolean isSingleton() { + return this.singleton; + } + + /** + * Set if the bean managed by this factory is a singleton. + */ + public void setSingleton(boolean singleton) { + this.singleton = singleton; + } + + @Override + public void setBeanName(String beanName) { + this.beanName = beanName; + } + + public String getBeanName() { + return beanName; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = (AutowireCapableBeanFactory) beanFactory; + this.beanFactory.applyBeanPostProcessorsBeforeInitialization(this.testBean, this.beanName); + } + + public BeanFactory getBeanFactory() { + return beanFactory; + } + + public void setPostProcessed(boolean postProcessed) { + this.postProcessed = postProcessed; + } + + public boolean isPostProcessed() { + return postProcessed; + } + + public void setOtherTestBean(TestBean otherTestBean) { + this.otherTestBean = otherTestBean; + this.testBean.setSpouse(otherTestBean); + } + + public TestBean getOtherTestBean() { + return otherTestBean; + } + + @Override + public void afterPropertiesSet() { + if (initialized) { + throw new RuntimeException("Cannot call afterPropertiesSet twice on the one bean"); + } + this.initialized = true; + } + + /** + * Was this initialized by invocation of the + * afterPropertiesSet() method from the InitializingBean interface? + */ + public boolean wasInitialized() { + return initialized; + } + + public static boolean wasPrototypeCreated() { + return prototypeCreated; + } + + + /** + * Return the managed object, supporting both singleton + * and prototype mode. + * @see FactoryBean#getObject() + */ + @Override + public Object getObject() throws BeansException { + if (isSingleton()) { + return this.testBean; + } + else { + TestBean prototype = new TestBean("prototype created at " + System.currentTimeMillis(), 11); + if (this.beanFactory != null) { + this.beanFactory.applyBeanPostProcessorsBeforeInitialization(prototype, this.beanName); + } + prototypeCreated = true; + return prototype; + } + } + + @Override + public Class getObjectType() { + return TestBean.class; + } + + + @Override + public void destroy() { + if (this.testBean != null) { + this.testBean.setName(null); + } + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/Employee.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/Employee.java new file mode 100644 index 0000000..3e9da76 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/Employee.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + +public class Employee extends TestBean { + + private String co; + + public Employee() { + } + + public Employee(String name) { + super(name); + } + + public String getCompany() { + return co; + } + + public void setCompany(String co) { + this.co = co; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/FactoryMethods.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/FactoryMethods.java new file mode 100644 index 0000000..98b2679 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/FactoryMethods.java @@ -0,0 +1,128 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + +import java.util.Collections; +import java.util.List; + +/** + * Test class for Spring's ability to create objects using static + * factory methods, rather than constructors. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +public class FactoryMethods { + + public static FactoryMethods nullInstance() { + return null; + } + + public static FactoryMethods defaultInstance() { + TestBean tb = new TestBean(); + tb.setName("defaultInstance"); + return new FactoryMethods(tb, "default", 0); + } + + /** + * Note that overloaded methods are supported. + */ + public static FactoryMethods newInstance(TestBean tb) { + return new FactoryMethods(tb, "default", 0); + } + + public static FactoryMethods newInstance(TestBean tb, int num, String name) { + if (name == null) { + throw new IllegalStateException("Should never be called with null value"); + } + return new FactoryMethods(tb, name, num); + } + + static ExtendedFactoryMethods newInstance(TestBean tb, int num, Integer something) { + if (something != null) { + throw new IllegalStateException("Should never be called with non-null value"); + } + return new ExtendedFactoryMethods(tb, null, num); + } + + @SuppressWarnings("unused") + private static List listInstance() { + return Collections.EMPTY_LIST; + } + + + private int num = 0; + private String name = "default"; + private TestBean tb; + private String stringValue; + + + /** + * Constructor is private: not for use outside this class, + * even by IoC container. + */ + private FactoryMethods(TestBean tb, String name, int num) { + this.tb = tb; + this.name = name; + this.num = num; + } + + public void setStringValue(String stringValue) { + this.stringValue = stringValue; + } + + public String getStringValue() { + return this.stringValue; + } + + public TestBean getTestBean() { + return this.tb; + } + + protected TestBean protectedGetTestBean() { + return this.tb; + } + + @SuppressWarnings("unused") + private TestBean privateGetTestBean() { + return this.tb; + } + + public int getNum() { + return num; + } + + public String getName() { + return name; + } + + /** + * Set via Setter Injection once instance is created. + */ + public void setName(String name) { + this.name = name; + } + + + public static class ExtendedFactoryMethods extends FactoryMethods { + + ExtendedFactoryMethods(TestBean tb, String name, int num) { + super(tb, name, num); + } + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/GenericBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/GenericBean.java new file mode 100644 index 0000000..2ad5ece --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/GenericBean.java @@ -0,0 +1,327 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.core.io.Resource; + +/** + * @author Juergen Hoeller + */ +public class GenericBean { + + private Set integerSet; + + private Set numberSet; + + private Set testBeanSet; + + private List resourceList; + + private List testBeanList; + + private List> listOfLists; + + private ArrayList listOfArrays; + + private List> listOfMaps; + + private Map plainMap; + + private Map shortMap; + + private HashMap longMap; + + private Map> collectionMap; + + private Map> mapOfMaps; + + private Map> mapOfLists; + + private CustomEnum customEnum; + + private CustomEnum[] customEnumArray; + + private Set customEnumSet; + + private EnumSet standardEnumSet; + + private EnumMap standardEnumMap; + + private T genericProperty; + + private List genericListProperty; + + + public GenericBean() { + } + + public GenericBean(Set integerSet) { + this.integerSet = integerSet; + } + + public GenericBean(Set integerSet, List resourceList) { + this.integerSet = integerSet; + this.resourceList = resourceList; + } + + public GenericBean(HashSet integerSet, Map shortMap) { + this.integerSet = integerSet; + this.shortMap = shortMap; + } + + public GenericBean(Map shortMap, Resource resource) { + this.shortMap = shortMap; + this.resourceList = Collections.singletonList(resource); + } + + public GenericBean(Map plainMap, Map shortMap) { + this.plainMap = plainMap; + this.shortMap = shortMap; + } + + public GenericBean(HashMap longMap) { + this.longMap = longMap; + } + + public GenericBean(boolean someFlag, Map> collectionMap) { + this.collectionMap = collectionMap; + } + + + public Set getIntegerSet() { + return integerSet; + } + + public void setIntegerSet(Set integerSet) { + this.integerSet = integerSet; + } + + public Set getNumberSet() { + return numberSet; + } + + public void setNumberSet(Set numberSet) { + this.numberSet = numberSet; + } + + public Set getTestBeanSet() { + return testBeanSet; + } + + public void setTestBeanSet(Set testBeanSet) { + this.testBeanSet = testBeanSet; + } + + public List getResourceList() { + return resourceList; + } + + public void setResourceList(List resourceList) { + this.resourceList = resourceList; + } + + public List getTestBeanList() { + return testBeanList; + } + + public void setTestBeanList(List testBeanList) { + this.testBeanList = testBeanList; + } + + public List> getListOfLists() { + return listOfLists; + } + + public ArrayList getListOfArrays() { + return listOfArrays; + } + + public void setListOfArrays(ArrayList listOfArrays) { + this.listOfArrays = listOfArrays; + } + + public void setListOfLists(List> listOfLists) { + this.listOfLists = listOfLists; + } + + public List> getListOfMaps() { + return listOfMaps; + } + + public void setListOfMaps(List> listOfMaps) { + this.listOfMaps = listOfMaps; + } + + public Map getPlainMap() { + return plainMap; + } + + public Map getShortMap() { + return shortMap; + } + + public void setShortMap(Map shortMap) { + this.shortMap = shortMap; + } + + public HashMap getLongMap() { + return longMap; + } + + public void setLongMap(HashMap longMap) { + this.longMap = longMap; + } + + public Map> getCollectionMap() { + return collectionMap; + } + + public void setCollectionMap(Map> collectionMap) { + this.collectionMap = collectionMap; + } + + public Map> getMapOfMaps() { + return mapOfMaps; + } + + public void setMapOfMaps(Map> mapOfMaps) { + this.mapOfMaps = mapOfMaps; + } + + public Map> getMapOfLists() { + return mapOfLists; + } + + public void setMapOfLists(Map> mapOfLists) { + this.mapOfLists = mapOfLists; + } + + public T getGenericProperty() { + return genericProperty; + } + + public void setGenericProperty(T genericProperty) { + this.genericProperty = genericProperty; + } + + public List getGenericListProperty() { + return genericListProperty; + } + + public void setGenericListProperty(List genericListProperty) { + this.genericListProperty = genericListProperty; + } + + public CustomEnum getCustomEnum() { + return customEnum; + } + + public void setCustomEnum(CustomEnum customEnum) { + this.customEnum = customEnum; + } + + public CustomEnum[] getCustomEnumArray() { + return customEnumArray; + } + + public void setCustomEnumArray(CustomEnum[] customEnum) { + this.customEnumArray = customEnum; + } + + public Set getCustomEnumSet() { + return customEnumSet; + } + + public void setCustomEnumSet(Set customEnumSet) { + this.customEnumSet = customEnumSet; + } + + public Set getCustomEnumSetMismatch() { + return customEnumSet; + } + + public void setCustomEnumSetMismatch(Set customEnumSet) { + this.customEnumSet = new HashSet<>(customEnumSet.size()); + for (Iterator iterator = customEnumSet.iterator(); iterator.hasNext(); ) { + this.customEnumSet.add(CustomEnum.valueOf(iterator.next())); + } + } + + public EnumSet getStandardEnumSet() { + return standardEnumSet; + } + + public void setStandardEnumSet(EnumSet standardEnumSet) { + this.standardEnumSet = standardEnumSet; + } + + public EnumMap getStandardEnumMap() { + return standardEnumMap; + } + + public void setStandardEnumMap(EnumMap standardEnumMap) { + this.standardEnumMap = standardEnumMap; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static GenericBean createInstance(Set integerSet) { + return new GenericBean(integerSet); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static GenericBean createInstance(Set integerSet, List resourceList) { + return new GenericBean(integerSet, resourceList); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static GenericBean createInstance(HashSet integerSet, Map shortMap) { + return new GenericBean(integerSet, shortMap); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static GenericBean createInstance(Map shortMap, Resource resource) { + return new GenericBean(shortMap, resource); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static GenericBean createInstance(Map map, Map shortMap) { + return new GenericBean(map, shortMap); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static GenericBean createInstance(HashMap longMap) { + return new GenericBean(longMap); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static GenericBean createInstance(boolean someFlag, Map> collectionMap) { + return new GenericBean(someFlag, collectionMap); + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/GenericIntegerBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/GenericIntegerBean.java new file mode 100644 index 0000000..69fd56d --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/GenericIntegerBean.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + + +/** + * @author Juergen Hoeller + */ +public class GenericIntegerBean extends GenericBean { + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/GenericSetOfIntegerBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/GenericSetOfIntegerBean.java new file mode 100644 index 0000000..c0439f9 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/GenericSetOfIntegerBean.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + +import java.util.Set; + +/** + * @author Juergen Hoeller + */ +public class GenericSetOfIntegerBean extends GenericBean> { + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/HasMap.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/HasMap.java new file mode 100644 index 0000000..c874077 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/HasMap.java @@ -0,0 +1,127 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * Bean exposing a map. Used for bean factory tests. + * + * @author Rod Johnson + * @since 05.06.2003 + */ +public class HasMap { + + private Map map; + + private Set set; + + private Properties props; + + private Object[] objectArray; + + private Integer[] intArray; + + private Class[] classArray; + + private List> classList; + + private IdentityHashMap identityMap; + + private CopyOnWriteArraySet concurrentSet; + + private HasMap() { + } + + public Map getMap() { + return map; + } + + public void setMap(Map map) { + this.map = map; + } + + public Set getSet() { + return set; + } + + public void setSet(Set set) { + this.set = set; + } + + public Properties getProps() { + return props; + } + + public void setProps(Properties props) { + this.props = props; + } + + public Object[] getObjectArray() { + return objectArray; + } + + public void setObjectArray(Object[] objectArray) { + this.objectArray = objectArray; + } + + public Integer[] getIntegerArray() { + return intArray; + } + + public void setIntegerArray(Integer[] is) { + intArray = is; + } + + public Class[] getClassArray() { + return classArray; + } + + public void setClassArray(Class[] classArray) { + this.classArray = classArray; + } + + public List> getClassList() { + return classList; + } + + public void setClassList(List> classList) { + this.classList = classList; + } + + public IdentityHashMap getIdentityMap() { + return identityMap; + } + + public void setIdentityMap(IdentityHashMap identityMap) { + this.identityMap = identityMap; + } + + public CopyOnWriteArraySet getConcurrentSet() { + return concurrentSet; + } + + public void setConcurrentSet(CopyOnWriteArraySet concurrentSet) { + this.concurrentSet = concurrentSet; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/INestedTestBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/INestedTestBean.java new file mode 100644 index 0000000..3107e6b --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/INestedTestBean.java @@ -0,0 +1,23 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + +public interface INestedTestBean { + + public String getCompany(); + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/IOther.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/IOther.java new file mode 100644 index 0000000..05059bc --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/IOther.java @@ -0,0 +1,23 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + +public interface IOther { + + void absquatulate(); + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/ITestBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/ITestBean.java new file mode 100644 index 0000000..742b39c --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/ITestBean.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + +import java.io.IOException; + +/** + * Interface used for {@link org.springframework.beans.testfixture.beans.TestBean}. + * + *

    Two methods are the same as on Person, but if this + * extends person it breaks quite a few tests.. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +public interface ITestBean extends AgeHolder { + + String getName(); + + void setName(String name); + + ITestBean getSpouse(); + + void setSpouse(ITestBean spouse); + + ITestBean[] getSpouses(); + + String[] getStringArray(); + + void setStringArray(String[] stringArray); + + Integer[][] getNestedIntegerArray(); + + Integer[] getSomeIntegerArray(); + + void setSomeIntegerArray(Integer[] someIntegerArray); + + void setNestedIntegerArray(Integer[][] nestedIntegerArray); + + int[] getSomeIntArray(); + + void setSomeIntArray(int[] someIntArray); + + int[][] getNestedIntArray(); + + void setNestedIntArray(int[][] someNestedArray); + + /** + * Throws a given (non-null) exception. + */ + void exceptional(Throwable t) throws Throwable; + + Object returnsThis(); + + INestedTestBean getDoctor(); + + INestedTestBean getLawyer(); + + IndexedTestBean getNestedIndexedBean(); + + /** + * Increment the age by one. + * @return the previous age + */ + int haveBirthday(); + + void unreliableFileOperation() throws IOException; + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/IndexedTestBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/IndexedTestBean.java new file mode 100644 index 0000000..02948f7 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/IndexedTestBean.java @@ -0,0 +1,149 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * @author Juergen Hoeller + * @since 11.11.2003 + */ +@SuppressWarnings("rawtypes") +public class IndexedTestBean { + + private TestBean[] array; + + private Collection collection; + + private List list; + + private Set set; + + private SortedSet sortedSet; + + private Map map; + + private SortedMap sortedMap; + + + public IndexedTestBean() { + this(true); + } + + public IndexedTestBean(boolean populate) { + if (populate) { + populate(); + } + } + + @SuppressWarnings("unchecked") + public void populate() { + TestBean tb0 = new TestBean("name0", 0); + TestBean tb1 = new TestBean("name1", 0); + TestBean tb2 = new TestBean("name2", 0); + TestBean tb3 = new TestBean("name3", 0); + TestBean tb4 = new TestBean("name4", 0); + TestBean tb5 = new TestBean("name5", 0); + TestBean tb6 = new TestBean("name6", 0); + TestBean tb7 = new TestBean("name7", 0); + TestBean tb8 = new TestBean("name8", 0); + TestBean tbX = new TestBean("nameX", 0); + TestBean tbY = new TestBean("nameY", 0); + this.array = new TestBean[] {tb0, tb1}; + this.list = new ArrayList<>(); + this.list.add(tb2); + this.list.add(tb3); + this.set = new TreeSet<>(); + this.set.add(tb6); + this.set.add(tb7); + this.map = new HashMap<>(); + this.map.put("key1", tb4); + this.map.put("key2", tb5); + this.map.put("key.3", tb5); + List list = new ArrayList(); + list.add(tbX); + list.add(tbY); + this.map.put("key4", list); + this.map.put("key5[foo]", tb8); + } + + + public TestBean[] getArray() { + return array; + } + + public void setArray(TestBean[] array) { + this.array = array; + } + + public Collection getCollection() { + return collection; + } + + public void setCollection(Collection collection) { + this.collection = collection; + } + + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } + + public Set getSet() { + return set; + } + + public void setSet(Set set) { + this.set = set; + } + + public SortedSet getSortedSet() { + return sortedSet; + } + + public void setSortedSet(SortedSet sortedSet) { + this.sortedSet = sortedSet; + } + + public Map getMap() { + return map; + } + + public void setMap(Map map) { + this.map = map; + } + + public SortedMap getSortedMap() { + return sortedMap; + } + + public void setSortedMap(SortedMap sortedMap) { + this.sortedMap = sortedMap; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/LifecycleBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/LifecycleBean.java new file mode 100644 index 0000000..b2e5402 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/LifecycleBean.java @@ -0,0 +1,170 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.config.BeanPostProcessor; + +/** + * Simple test of BeanFactory initialization and lifecycle callbacks. + * + * @author Rod Johnson + * @author Colin Sampaleanu + * @author Chris Beams + * @since 12.03.2003 + */ +public class LifecycleBean implements BeanNameAware, BeanFactoryAware, InitializingBean, DisposableBean { + + protected boolean initMethodDeclared = false; + + protected String beanName; + + protected BeanFactory owningFactory; + + protected boolean postProcessedBeforeInit; + + protected boolean inited; + + protected boolean initedViaDeclaredInitMethod; + + protected boolean postProcessedAfterInit; + + protected boolean destroyed; + + + public void setInitMethodDeclared(boolean initMethodDeclared) { + this.initMethodDeclared = initMethodDeclared; + } + + public boolean isInitMethodDeclared() { + return initMethodDeclared; + } + + @Override + public void setBeanName(String name) { + this.beanName = name; + } + + public String getBeanName() { + return beanName; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.owningFactory = beanFactory; + } + + public void postProcessBeforeInit() { + if (this.inited || this.initedViaDeclaredInitMethod) { + throw new RuntimeException("Factory called postProcessBeforeInit after afterPropertiesSet"); + } + if (this.postProcessedBeforeInit) { + throw new RuntimeException("Factory called postProcessBeforeInit twice"); + } + this.postProcessedBeforeInit = true; + } + + @Override + public void afterPropertiesSet() { + if (this.owningFactory == null) { + throw new RuntimeException("Factory didn't call setBeanFactory before afterPropertiesSet on lifecycle bean"); + } + if (!this.postProcessedBeforeInit) { + throw new RuntimeException("Factory didn't call postProcessBeforeInit before afterPropertiesSet on lifecycle bean"); + } + if (this.initedViaDeclaredInitMethod) { + throw new RuntimeException("Factory initialized via declared init method before initializing via afterPropertiesSet"); + } + if (this.inited) { + throw new RuntimeException("Factory called afterPropertiesSet twice"); + } + this.inited = true; + } + + public void declaredInitMethod() { + if (!this.inited) { + throw new RuntimeException("Factory didn't call afterPropertiesSet before declared init method"); + } + + if (this.initedViaDeclaredInitMethod) { + throw new RuntimeException("Factory called declared init method twice"); + } + this.initedViaDeclaredInitMethod = true; + } + + public void postProcessAfterInit() { + if (!this.inited) { + throw new RuntimeException("Factory called postProcessAfterInit before afterPropertiesSet"); + } + if (this.initMethodDeclared && !this.initedViaDeclaredInitMethod) { + throw new RuntimeException("Factory called postProcessAfterInit before calling declared init method"); + } + if (this.postProcessedAfterInit) { + throw new RuntimeException("Factory called postProcessAfterInit twice"); + } + this.postProcessedAfterInit = true; + } + + /** + * Dummy business method that will fail unless the factory + * managed the bean's lifecycle correctly + */ + public void businessMethod() { + if (!this.inited || (this.initMethodDeclared && !this.initedViaDeclaredInitMethod) || + !this.postProcessedAfterInit) { + throw new RuntimeException("Factory didn't initialize lifecycle object correctly"); + } + } + + @Override + public void destroy() { + if (this.destroyed) { + throw new IllegalStateException("Already destroyed"); + } + this.destroyed = true; + } + + public boolean isDestroyed() { + return destroyed; + } + + + public static class PostProcessor implements BeanPostProcessor { + + @Override + public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException { + if (bean instanceof LifecycleBean) { + ((LifecycleBean) bean).postProcessBeforeInit(); + } + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String name) throws BeansException { + if (bean instanceof LifecycleBean) { + ((LifecycleBean) bean).postProcessAfterInit(); + } + return bean; + } + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/MustBeInitialized.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/MustBeInitialized.java new file mode 100644 index 0000000..2e53ce2 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/MustBeInitialized.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + +import org.springframework.beans.factory.InitializingBean; + +/** + * Simple test of BeanFactory initialization + * @author Rod Johnson + * @since 12.03.2003 + */ +public class MustBeInitialized implements InitializingBean { + + private boolean inited; + + /** + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + @Override + public void afterPropertiesSet() throws Exception { + this.inited = true; + } + + /** + * Dummy business method that will fail unless the factory + * managed the bean's lifecycle correctly + */ + public void businessMethod() { + if (!this.inited) + throw new RuntimeException("Factory didn't call afterPropertiesSet() on MustBeInitialized object"); + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/NestedTestBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/NestedTestBean.java new file mode 100644 index 0000000..ea26ec0 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/NestedTestBean.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + +/** + * Simple nested test bean used for testing bean factories, AOP framework etc. + * + * @author Trevor D. Cook + * @since 30.09.2003 + */ +public class NestedTestBean implements INestedTestBean { + + private String company = ""; + + public NestedTestBean() { + } + + public NestedTestBean(String company) { + setCompany(company); + } + + public void setCompany(String company) { + this.company = (company != null ? company : ""); + } + + @Override + public String getCompany() { + return company; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof NestedTestBean)) { + return false; + } + NestedTestBean ntb = (NestedTestBean) obj; + return this.company.equals(ntb.company); + } + + @Override + public int hashCode() { + return this.company.hashCode(); + } + + @Override + public String toString() { + return "NestedTestBean: " + this.company; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/NumberTestBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/NumberTestBean.java new file mode 100644 index 0000000..224965b --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/NumberTestBean.java @@ -0,0 +1,143 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * @author Juergen Hoeller + * @since 10.06.2003 + */ +public class NumberTestBean { + + private short short1; + private Short short2; + + private int int1; + private Integer int2; + + private long long1; + private Long long2; + + private BigInteger bigInteger; + + private float float1; + private Float float2; + + private double double1; + private Double double2; + + private BigDecimal bigDecimal; + + public short getShort1() { + return short1; + } + + public void setShort1(short short1) { + this.short1 = short1; + } + + public Short getShort2() { + return short2; + } + + public void setShort2(Short short2) { + this.short2 = short2; + } + + public int getInt1() { + return int1; + } + + public void setInt1(int int1) { + this.int1 = int1; + } + + public Integer getInt2() { + return int2; + } + + public void setInt2(Integer int2) { + this.int2 = int2; + } + + public long getLong1() { + return long1; + } + + public void setLong1(long long1) { + this.long1 = long1; + } + + public Long getLong2() { + return long2; + } + + public void setLong2(Long long2) { + this.long2 = long2; + } + + public BigInteger getBigInteger() { + return bigInteger; + } + + public void setBigInteger(BigInteger bigInteger) { + this.bigInteger = bigInteger; + } + + public float getFloat1() { + return float1; + } + + public void setFloat1(float float1) { + this.float1 = float1; + } + + public Float getFloat2() { + return float2; + } + + public void setFloat2(Float float2) { + this.float2 = float2; + } + + public double getDouble1() { + return double1; + } + + public void setDouble1(double double1) { + this.double1 = double1; + } + + public Double getDouble2() { + return double2; + } + + public void setDouble2(Double double2) { + this.double2 = double2; + } + + public BigDecimal getBigDecimal() { + return bigDecimal; + } + + public void setBigDecimal(BigDecimal bigDecimal) { + this.bigDecimal = bigDecimal; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/PackageLevelVisibleBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/PackageLevelVisibleBean.java new file mode 100644 index 0000000..73a0bd3 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/PackageLevelVisibleBean.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + +/** + * @see org.springframework.beans.factory.config.FieldRetrievingFactoryBeanTests + * + * @author Rick Evans + * @author Chris Beams + */ +class PackageLevelVisibleBean { + + public static final String CONSTANT = "Wuby"; + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/Person.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/Person.java new file mode 100644 index 0000000..d57a8ca --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/Person.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + +/** + * + * @author Rod Johnson + */ +public interface Person { + + String getName(); + + void setName(String name); + + int getAge(); + + void setAge(int i); + + /** + * Test for non-property method matching. If the parameter is a Throwable, it will be + * thrown rather than returned. + */ + Object echo(Object o) throws Throwable; +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/Pet.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/Pet.java new file mode 100644 index 0000000..661eff9 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/Pet.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2006 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + +/** + * @author Rob Harrop + * @since 2.0 + */ +public class Pet { + + private String name; + + public Pet(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return getName(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final Pet pet = (Pet) o; + + if (name != null ? !name.equals(pet.name) : pet.name != null) return false; + + return true; + } + + @Override + public int hashCode() { + return (name != null ? name.hashCode() : 0); + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/SerializablePerson.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/SerializablePerson.java new file mode 100644 index 0000000..6f94369 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/SerializablePerson.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + +import java.io.Serializable; + +import org.springframework.util.ObjectUtils; + +/** + * Serializable implementation of the Person interface. + * + * @author Rod Johnson + */ +@SuppressWarnings("serial") +public class SerializablePerson implements Person, Serializable { + + private String name; + + private int age; + + + @Override + public String getName() { + return name; + } + + @Override + public void setName(String name) { + this.name = name; + } + + @Override + public int getAge() { + return age; + } + + @Override + public void setAge(int age) { + this.age = age; + } + + @Override + public Object echo(Object o) throws Throwable { + if (o instanceof Throwable) { + throw (Throwable) o; + } + return o; + } + + + @Override + public boolean equals(Object other) { + if (!(other instanceof SerializablePerson)) { + return false; + } + SerializablePerson p = (SerializablePerson) other; + return p.age == age && ObjectUtils.nullSafeEquals(name, p.name); + } + + @Override + public int hashCode() { + return SerializablePerson.class.hashCode(); + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/SideEffectBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/SideEffectBean.java new file mode 100644 index 0000000..36911bd --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/SideEffectBean.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + +/** + * Bean that changes state on a business invocation, so that + * we can check whether it's been invoked + * @author Rod Johnson + */ +public class SideEffectBean { + + private int count; + + public void setCount(int count) { + this.count = count; + } + + public int getCount() { + return this.count; + } + + public void doWork() { + ++count; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestAnnotation.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestAnnotation.java new file mode 100644 index 0000000..379c874 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestAnnotation.java @@ -0,0 +1,27 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * @author Stephane Nicoll + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface TestAnnotation { +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBean.java new file mode 100644 index 0000000..ed54d0d --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBean.java @@ -0,0 +1,498 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.util.ObjectUtils; + +/** + * Simple test bean used for testing bean factories, the AOP framework etc. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Stephane Nicoll + * @since 15 April 2001 + */ +public class TestBean implements BeanNameAware, BeanFactoryAware, ITestBean, IOther, Comparable { + + private String beanName; + + private String country; + + private BeanFactory beanFactory; + + private boolean postProcessed; + + private String name; + + private String sex; + + private int age; + + private boolean jedi; + + private ITestBean spouse; + + private String touchy; + + private String[] stringArray; + + private Integer[] someIntegerArray; + + private Integer[][] nestedIntegerArray; + + private int[] someIntArray; + + private int[][] nestedIntArray; + + private Date date = new Date(); + + private Float myFloat = Float.valueOf(0.0f); + + private Collection friends = new ArrayList<>(); + + private Set someSet = new HashSet<>(); + + private Map someMap = new HashMap<>(); + + private List someList = new ArrayList<>(); + + private Properties someProperties = new Properties(); + + private INestedTestBean doctor = new NestedTestBean(); + + private INestedTestBean lawyer = new NestedTestBean(); + + private IndexedTestBean nestedIndexedBean; + + private boolean destroyed; + + private Number someNumber; + + private Colour favouriteColour; + + private Boolean someBoolean; + + private List otherColours; + + private List pets; + + + public TestBean() { + } + + public TestBean(String name) { + this.name = name; + } + + public TestBean(ITestBean spouse) { + this.spouse = spouse; + } + + public TestBean(String name, int age) { + this.name = name; + this.age = age; + } + + public TestBean(ITestBean spouse, Properties someProperties) { + this.spouse = spouse; + this.someProperties = someProperties; + } + + public TestBean(List someList) { + this.someList = someList; + } + + public TestBean(Set someSet) { + this.someSet = someSet; + } + + public TestBean(Map someMap) { + this.someMap = someMap; + } + + public TestBean(Properties someProperties) { + this.someProperties = someProperties; + } + + + @Override + public void setBeanName(String beanName) { + this.beanName = beanName; + } + + public String getBeanName() { + return beanName; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + public BeanFactory getBeanFactory() { + return beanFactory; + } + + public void setPostProcessed(boolean postProcessed) { + this.postProcessed = postProcessed; + } + + public boolean isPostProcessed() { + return postProcessed; + } + + @Override + public String getName() { + return name; + } + + @Override + public void setName(String name) { + this.name = name; + } + + public String getSex() { + return sex; + } + + public void setSex(String sex) { + this.sex = sex; + if (this.name == null) { + this.name = sex; + } + } + + @Override + public int getAge() { + return age; + } + + @Override + public void setAge(int age) { + this.age = age; + } + + public boolean isJedi() { + return jedi; + } + + public void setJedi(boolean jedi) { + this.jedi = jedi; + } + + @Override + public ITestBean getSpouse() { + return this.spouse; + } + + @Override + public void setSpouse(ITestBean spouse) { + this.spouse = spouse; + } + + @Override + public ITestBean[] getSpouses() { + return (spouse != null ? new ITestBean[] {spouse} : null); + } + + public String getTouchy() { + return touchy; + } + + public void setTouchy(String touchy) throws Exception { + if (touchy.indexOf('.') != -1) { + throw new Exception("Can't contain a ."); + } + if (touchy.indexOf(',') != -1) { + throw new NumberFormatException("Number format exception: contains a ,"); + } + this.touchy = touchy; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + @Override + public String[] getStringArray() { + return stringArray; + } + + @Override + public void setStringArray(String[] stringArray) { + this.stringArray = stringArray; + } + + @Override + public Integer[] getSomeIntegerArray() { + return someIntegerArray; + } + + @Override + public void setSomeIntegerArray(Integer[] someIntegerArray) { + this.someIntegerArray = someIntegerArray; + } + + @Override + public Integer[][] getNestedIntegerArray() { + return nestedIntegerArray; + } + + @Override + public void setNestedIntegerArray(Integer[][] nestedIntegerArray) { + this.nestedIntegerArray = nestedIntegerArray; + } + + @Override + public int[] getSomeIntArray() { + return someIntArray; + } + + @Override + public void setSomeIntArray(int[] someIntArray) { + this.someIntArray = someIntArray; + } + + @Override + public int[][] getNestedIntArray() { + return nestedIntArray; + } + + @Override + public void setNestedIntArray(int[][] nestedIntArray) { + this.nestedIntArray = nestedIntArray; + } + + public Date getDate() { + return date; + } + + public void setDate(Date date) { + this.date = date; + } + + public Float getMyFloat() { + return myFloat; + } + + public void setMyFloat(Float myFloat) { + this.myFloat = myFloat; + } + + public Collection getFriends() { + return friends; + } + + public void setFriends(Collection friends) { + this.friends = friends; + } + + public Set getSomeSet() { + return someSet; + } + + public void setSomeSet(Set someSet) { + this.someSet = someSet; + } + + public Map getSomeMap() { + return someMap; + } + + public void setSomeMap(Map someMap) { + this.someMap = someMap; + } + + public List getSomeList() { + return someList; + } + + public void setSomeList(List someList) { + this.someList = someList; + } + + public Properties getSomeProperties() { + return someProperties; + } + + public void setSomeProperties(Properties someProperties) { + this.someProperties = someProperties; + } + + @Override + public INestedTestBean getDoctor() { + return doctor; + } + + public void setDoctor(INestedTestBean doctor) { + this.doctor = doctor; + } + + @Override + public INestedTestBean getLawyer() { + return lawyer; + } + + public void setLawyer(INestedTestBean lawyer) { + this.lawyer = lawyer; + } + + public Number getSomeNumber() { + return someNumber; + } + + public void setSomeNumber(Number someNumber) { + this.someNumber = someNumber; + } + + public Colour getFavouriteColour() { + return favouriteColour; + } + + public void setFavouriteColour(Colour favouriteColour) { + this.favouriteColour = favouriteColour; + } + + public Boolean getSomeBoolean() { + return someBoolean; + } + + public void setSomeBoolean(Boolean someBoolean) { + this.someBoolean = someBoolean; + } + + @Override + public IndexedTestBean getNestedIndexedBean() { + return nestedIndexedBean; + } + + public void setNestedIndexedBean(IndexedTestBean nestedIndexedBean) { + this.nestedIndexedBean = nestedIndexedBean; + } + + public List getOtherColours() { + return otherColours; + } + + public void setOtherColours(List otherColours) { + this.otherColours = otherColours; + } + + public List getPets() { + return pets; + } + + public void setPets(List pets) { + this.pets = pets; + } + + + /** + * @see org.springframework.beans.testfixture.beans.ITestBean#exceptional(Throwable) + */ + @Override + public void exceptional(Throwable t) throws Throwable { + if (t != null) { + throw t; + } + } + + @Override + public void unreliableFileOperation() throws IOException { + throw new IOException(); + } + /** + * @see org.springframework.beans.testfixture.beans.ITestBean#returnsThis() + */ + @Override + public Object returnsThis() { + return this; + } + + /** + * @see org.springframework.beans.testfixture.beans.IOther#absquatulate() + */ + @Override + public void absquatulate() { + } + + @Override + public int haveBirthday() { + return age++; + } + + + public void destroy() { + this.destroyed = true; + } + + public boolean wasDestroyed() { + return destroyed; + } + + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof TestBean)) { + return false; + } + TestBean tb2 = (TestBean) other; + return (ObjectUtils.nullSafeEquals(this.name, tb2.name) && this.age == tb2.age); + } + + @Override + public int hashCode() { + return this.age; + } + + @Override + public int compareTo(Object other) { + if (this.name != null && other instanceof TestBean) { + return this.name.compareTo(((TestBean) other).getName()); + } + else { + return 1; + } + } + + @Override + public String toString() { + return this.name; + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/DummyFactory.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/DummyFactory.java new file mode 100644 index 0000000..9e3d8b6 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/DummyFactory.java @@ -0,0 +1,186 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans.factory; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.beans.testfixture.beans.TestBean; + +/** + * Simple factory to allow testing of FactoryBean support in AbstractBeanFactory. + * Depending on whether its singleton property is set, it will return a singleton + * or a prototype instance. + * + *

    Implements InitializingBean interface, so we can check that + * factories get this lifecycle callback if they want. + * + * @author Rod Johnson + * @author Chris Beams + * @since 10.03.2003 + */ +public class DummyFactory + implements FactoryBean, BeanNameAware, BeanFactoryAware, InitializingBean, DisposableBean { + + public static final String SINGLETON_NAME = "Factory singleton"; + + private static boolean prototypeCreated; + + /** + * Clear static state. + */ + public static void reset() { + prototypeCreated = false; + } + + + /** + * Default is for factories to return a singleton instance. + */ + private boolean singleton = true; + + private String beanName; + + private AutowireCapableBeanFactory beanFactory; + + private boolean postProcessed; + + private boolean initialized; + + private TestBean testBean; + + private TestBean otherTestBean; + + + public DummyFactory() { + this.testBean = new TestBean(); + this.testBean.setName(SINGLETON_NAME); + this.testBean.setAge(25); + } + + /** + * Return if the bean managed by this factory is a singleton. + * @see FactoryBean#isSingleton() + */ + @Override + public boolean isSingleton() { + return this.singleton; + } + + /** + * Set if the bean managed by this factory is a singleton. + */ + public void setSingleton(boolean singleton) { + this.singleton = singleton; + } + + @Override + public void setBeanName(String beanName) { + this.beanName = beanName; + } + + public String getBeanName() { + return beanName; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = (AutowireCapableBeanFactory) beanFactory; + this.beanFactory.applyBeanPostProcessorsBeforeInitialization(this.testBean, this.beanName); + } + + public BeanFactory getBeanFactory() { + return beanFactory; + } + + public void setPostProcessed(boolean postProcessed) { + this.postProcessed = postProcessed; + } + + public boolean isPostProcessed() { + return postProcessed; + } + + public void setOtherTestBean(TestBean otherTestBean) { + this.otherTestBean = otherTestBean; + this.testBean.setSpouse(otherTestBean); + } + + public TestBean getOtherTestBean() { + return otherTestBean; + } + + @Override + public void afterPropertiesSet() { + if (initialized) { + throw new RuntimeException("Cannot call afterPropertiesSet twice on the one bean"); + } + this.initialized = true; + } + + /** + * Was this initialized by invocation of the + * afterPropertiesSet() method from the InitializingBean interface? + */ + public boolean wasInitialized() { + return initialized; + } + + public static boolean wasPrototypeCreated() { + return prototypeCreated; + } + + + /** + * Return the managed object, supporting both singleton + * and prototype mode. + * @see FactoryBean#getObject() + */ + @Override + public Object getObject() throws BeansException { + if (isSingleton()) { + return this.testBean; + } + else { + TestBean prototype = new TestBean("prototype created at " + System.currentTimeMillis(), 11); + if (this.beanFactory != null) { + this.beanFactory.applyBeanPostProcessorsBeforeInitialization(prototype, this.beanName); + } + prototypeCreated = true; + return prototype; + } + } + + @Override + public Class getObjectType() { + return TestBean.class; + } + + + @Override + public void destroy() { + if (this.testBean != null) { + this.testBean.setName(null); + } + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/package-info.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/package-info.java new file mode 100644 index 0000000..14a1870 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/package-info.java @@ -0,0 +1,4 @@ +/** + * General purpose sample beans that can be used with tests. + */ +package org.springframework.beans.testfixture.beans; diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/subpkg/DeepBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/subpkg/DeepBean.java new file mode 100644 index 0000000..50fc891 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/subpkg/DeepBean.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.beans.subpkg; + +/** + * Used for testing pointcut matching. + * + * @see org.springframework.aop.aspectj.AspectJExpressionPointcutTests#testWithinRootAndSubpackages() + * + * @author Chris Beams + */ +public class DeepBean { + public void aMethod(String foo) { + // no-op + } +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/factory/xml/AbstractBeanFactoryTests.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/factory/xml/AbstractBeanFactoryTests.java new file mode 100644 index 0000000..307a9d6 --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/factory/xml/AbstractBeanFactoryTests.java @@ -0,0 +1,295 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.factory.xml; + +import java.beans.PropertyEditorSupport; +import java.util.StringTokenizer; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.BeansException; +import org.springframework.beans.TypeMismatchException; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanIsNotAFactoryException; +import org.springframework.beans.factory.BeanNotOfRequiredTypeException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.testfixture.beans.LifecycleBean; +import org.springframework.beans.testfixture.beans.MustBeInitialized; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.beans.testfixture.beans.factory.DummyFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Subclasses must initialize the bean factory and any other variables they need. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Sam Brannen + */ +public abstract class AbstractBeanFactoryTests { + + protected abstract BeanFactory getBeanFactory(); + + /** + * Roderick bean inherits from rod, overriding name only. + */ + @Test + public void inheritance() { + assertThat(getBeanFactory().containsBean("rod")).isTrue(); + assertThat(getBeanFactory().containsBean("roderick")).isTrue(); + TestBean rod = (TestBean) getBeanFactory().getBean("rod"); + TestBean roderick = (TestBean) getBeanFactory().getBean("roderick"); + assertThat(rod != roderick).as("not == ").isTrue(); + assertThat(rod.getName().equals("Rod")).as("rod.name is Rod").isTrue(); + assertThat(rod.getAge() == 31).as("rod.age is 31").isTrue(); + assertThat(roderick.getName().equals("Roderick")).as("roderick.name is Roderick").isTrue(); + assertThat(roderick.getAge() == rod.getAge()).as("roderick.age was inherited").isTrue(); + } + + @Test + public void getBeanWithNullArg() { + assertThatIllegalArgumentException().isThrownBy(() -> + getBeanFactory().getBean((String) null)); + } + + /** + * Test that InitializingBean objects receive the afterPropertiesSet() callback + */ + @Test + public void initializingBeanCallback() { + MustBeInitialized mbi = (MustBeInitialized) getBeanFactory().getBean("mustBeInitialized"); + // The dummy business method will throw an exception if the + // afterPropertiesSet() callback wasn't invoked + mbi.businessMethod(); + } + + /** + * Test that InitializingBean/BeanFactoryAware/DisposableBean objects receive the + * afterPropertiesSet() callback before BeanFactoryAware callbacks + */ + @Test + public void lifecycleCallbacks() { + LifecycleBean lb = (LifecycleBean) getBeanFactory().getBean("lifecycle"); + assertThat(lb.getBeanName()).isEqualTo("lifecycle"); + // The dummy business method will throw an exception if the + // necessary callbacks weren't invoked in the right order. + lb.businessMethod(); + boolean condition = !lb.isDestroyed(); + assertThat(condition).as("Not destroyed").isTrue(); + } + + @Test + public void findsValidInstance() { + Object o = getBeanFactory().getBean("rod"); + boolean condition = o instanceof TestBean; + assertThat(condition).as("Rod bean is a TestBean").isTrue(); + TestBean rod = (TestBean) o; + assertThat(rod.getName().equals("Rod")).as("rod.name is Rod").isTrue(); + assertThat(rod.getAge() == 31).as("rod.age is 31").isTrue(); + } + + @Test + public void getInstanceByMatchingClass() { + Object o = getBeanFactory().getBean("rod", TestBean.class); + boolean condition = o instanceof TestBean; + assertThat(condition).as("Rod bean is a TestBean").isTrue(); + } + + @Test + public void getInstanceByNonmatchingClass() { + assertThatExceptionOfType(BeanNotOfRequiredTypeException.class).isThrownBy(() -> + getBeanFactory().getBean("rod", BeanFactory.class)) + .satisfies(ex -> { + assertThat(ex.getBeanName()).isEqualTo("rod"); + assertThat(ex.getRequiredType()).isEqualTo(BeanFactory.class); + assertThat(ex.getActualType()).isEqualTo(TestBean.class).isEqualTo(getBeanFactory().getBean("rod").getClass()); + }); + } + + @Test + public void getSharedInstanceByMatchingClass() { + Object o = getBeanFactory().getBean("rod", TestBean.class); + boolean condition = o instanceof TestBean; + assertThat(condition).as("Rod bean is a TestBean").isTrue(); + } + + @Test + public void getSharedInstanceByMatchingClassNoCatch() { + Object o = getBeanFactory().getBean("rod", TestBean.class); + boolean condition = o instanceof TestBean; + assertThat(condition).as("Rod bean is a TestBean").isTrue(); + } + + @Test + public void getSharedInstanceByNonmatchingClass() { + assertThatExceptionOfType(BeanNotOfRequiredTypeException.class).isThrownBy(() -> + getBeanFactory().getBean("rod", BeanFactory.class)) + .satisfies(ex -> { + assertThat(ex.getBeanName()).isEqualTo("rod"); + assertThat(ex.getRequiredType()).isEqualTo(BeanFactory.class); + assertThat(ex.getActualType()).isEqualTo(TestBean.class); + }); + } + + @Test + public void sharedInstancesAreEqual() { + Object o = getBeanFactory().getBean("rod"); + boolean condition1 = o instanceof TestBean; + assertThat(condition1).as("Rod bean1 is a TestBean").isTrue(); + Object o1 = getBeanFactory().getBean("rod"); + boolean condition = o1 instanceof TestBean; + assertThat(condition).as("Rod bean2 is a TestBean").isTrue(); + assertThat(o == o1).as("Object equals applies").isTrue(); + } + + @Test + public void prototypeInstancesAreIndependent() { + TestBean tb1 = (TestBean) getBeanFactory().getBean("kathy"); + TestBean tb2 = (TestBean) getBeanFactory().getBean("kathy"); + assertThat(tb1 != tb2).as("ref equal DOES NOT apply").isTrue(); + assertThat(tb1.equals(tb2)).as("object equal true").isTrue(); + tb1.setAge(1); + tb2.setAge(2); + assertThat(tb1.getAge() == 1).as("1 age independent = 1").isTrue(); + assertThat(tb2.getAge() == 2).as("2 age independent = 2").isTrue(); + boolean condition = !tb1.equals(tb2); + assertThat(condition).as("object equal now false").isTrue(); + } + + @Test + public void notThere() { + assertThat(getBeanFactory().containsBean("Mr Squiggle")).isFalse(); + assertThatExceptionOfType(BeansException.class).isThrownBy(() -> + getBeanFactory().getBean("Mr Squiggle")); + } + + @Test + public void validEmpty() { + Object o = getBeanFactory().getBean("validEmpty"); + boolean condition = o instanceof TestBean; + assertThat(condition).as("validEmpty bean is a TestBean").isTrue(); + TestBean ve = (TestBean) o; + assertThat(ve.getName() == null && ve.getAge() == 0 && ve.getSpouse() == null).as("Valid empty has defaults").isTrue(); + } + + @Test + public void typeMismatch() { + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> getBeanFactory().getBean("typeMismatch")) + .withCauseInstanceOf(TypeMismatchException.class); + } + + @Test + public void grandparentDefinitionFoundInBeanFactory() throws Exception { + TestBean dad = (TestBean) getBeanFactory().getBean("father"); + assertThat(dad.getName().equals("Albert")).as("Dad has correct name").isTrue(); + } + + @Test + public void factorySingleton() throws Exception { + assertThat(getBeanFactory().isSingleton("&singletonFactory")).isTrue(); + assertThat(getBeanFactory().isSingleton("singletonFactory")).isTrue(); + TestBean tb = (TestBean) getBeanFactory().getBean("singletonFactory"); + assertThat(tb.getName().equals(DummyFactory.SINGLETON_NAME)).as("Singleton from factory has correct name, not " + tb.getName()).isTrue(); + DummyFactory factory = (DummyFactory) getBeanFactory().getBean("&singletonFactory"); + TestBean tb2 = (TestBean) getBeanFactory().getBean("singletonFactory"); + assertThat(tb == tb2).as("Singleton references ==").isTrue(); + assertThat(factory.getBeanFactory() != null).as("FactoryBean is BeanFactoryAware").isTrue(); + } + + @Test + public void factoryPrototype() throws Exception { + assertThat(getBeanFactory().isSingleton("&prototypeFactory")).isTrue(); + assertThat(getBeanFactory().isSingleton("prototypeFactory")).isFalse(); + TestBean tb = (TestBean) getBeanFactory().getBean("prototypeFactory"); + boolean condition = !tb.getName().equals(DummyFactory.SINGLETON_NAME); + assertThat(condition).isTrue(); + TestBean tb2 = (TestBean) getBeanFactory().getBean("prototypeFactory"); + assertThat(tb != tb2).as("Prototype references !=").isTrue(); + } + + /** + * Check that we can get the factory bean itself. + * This is only possible if we're dealing with a factory + */ + @Test + public void getFactoryItself() throws Exception { + assertThat(getBeanFactory().getBean("&singletonFactory")).isNotNull(); + } + + /** + * Check that afterPropertiesSet gets called on factory + */ + @Test + public void factoryIsInitialized() throws Exception { + TestBean tb = (TestBean) getBeanFactory().getBean("singletonFactory"); + assertThat(tb).isNotNull(); + DummyFactory factory = (DummyFactory) getBeanFactory().getBean("&singletonFactory"); + assertThat(factory.wasInitialized()).as("Factory was initialized because it implemented InitializingBean").isTrue(); + } + + /** + * It should be illegal to dereference a normal bean as a factory. + */ + @Test + public void rejectsFactoryGetOnNormalBean() { + assertThatExceptionOfType(BeanIsNotAFactoryException.class).isThrownBy(() -> + getBeanFactory().getBean("&rod")); + } + + // TODO: refactor in AbstractBeanFactory (tests for AbstractBeanFactory) + // and rename this class + @Test + public void aliasing() { + BeanFactory bf = getBeanFactory(); + if (!(bf instanceof ConfigurableBeanFactory)) { + return; + } + ConfigurableBeanFactory cbf = (ConfigurableBeanFactory) bf; + + String alias = "rods alias"; + + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> + cbf.getBean(alias)) + .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo(alias)); + + // Create alias + cbf.registerAlias("rod", alias); + Object rod = getBeanFactory().getBean("rod"); + Object aliasRod = getBeanFactory().getBean(alias); + assertThat(rod == aliasRod).isTrue(); + } + + + public static class TestBeanEditor extends PropertyEditorSupport { + + @Override + public void setAsText(String text) { + TestBean tb = new TestBean(); + StringTokenizer st = new StringTokenizer(text, "_"); + tb.setName(st.nextToken()); + tb.setAge(Integer.parseInt(st.nextToken())); + setValue(tb); + } + } + +} diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/factory/xml/AbstractListableBeanFactoryTests.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/factory/xml/AbstractListableBeanFactoryTests.java new file mode 100644 index 0000000..0717f8a --- /dev/null +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/factory/xml/AbstractListableBeanFactoryTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.testfixture.factory.xml; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rod Johnson + * @author Juergen Hoeller + */ +public abstract class AbstractListableBeanFactoryTests extends AbstractBeanFactoryTests { + + /** Subclasses must initialize this */ + protected ListableBeanFactory getListableBeanFactory() { + BeanFactory bf = getBeanFactory(); + if (!(bf instanceof ListableBeanFactory)) { + throw new IllegalStateException("ListableBeanFactory required"); + } + return (ListableBeanFactory) bf; + } + + /** + * Subclasses can override this. + */ + @Test + public void count() { + assertCount(13); + } + + protected final void assertCount(int count) { + String[] defnames = getListableBeanFactory().getBeanDefinitionNames(); + assertThat(defnames.length == count).as("We should have " + count + " beans, not " + defnames.length).isTrue(); + } + + protected void assertTestBeanCount(int count) { + String[] defNames = getListableBeanFactory().getBeanNamesForType(TestBean.class, true, false); + assertThat(defNames.length == count).as("We should have " + count + " beans for class org.springframework.beans.testfixture.beans.TestBean, not " + + defNames.length).isTrue(); + + int countIncludingFactoryBeans = count + 2; + String[] names = getListableBeanFactory().getBeanNamesForType(TestBean.class, true, true); + assertThat(names.length == countIncludingFactoryBeans).as("We should have " + countIncludingFactoryBeans + + " beans for class org.springframework.beans.testfixture.beans.TestBean, not " + names.length).isTrue(); + } + + @Test + public void getDefinitionsForNoSuchClass() { + String[] defnames = getListableBeanFactory().getBeanNamesForType(String.class); + assertThat(defnames.length == 0).as("No string definitions").isTrue(); + } + + /** + * Check that count refers to factory class, not bean class. (We don't know + * what type factories may return, and it may even change over time.) + */ + @Test + public void getCountForFactoryClass() { + assertThat(getListableBeanFactory().getBeanNamesForType(FactoryBean.class).length == 2).as("Should have 2 factories, not " + + getListableBeanFactory().getBeanNamesForType(FactoryBean.class).length).isTrue(); + + assertThat(getListableBeanFactory().getBeanNamesForType(FactoryBean.class).length == 2).as("Should have 2 factories, not " + + getListableBeanFactory().getBeanNamesForType(FactoryBean.class).length).isTrue(); + } + + @Test + public void containsBeanDefinition() { + assertThat(getListableBeanFactory().containsBeanDefinition("rod")).isTrue(); + assertThat(getListableBeanFactory().containsBeanDefinition("roderick")).isTrue(); + } + +} diff --git a/spring-context-indexer/spring-context-indexer.gradle b/spring-context-indexer/spring-context-indexer.gradle new file mode 100644 index 0000000..a9769ad --- /dev/null +++ b/spring-context-indexer/spring-context-indexer.gradle @@ -0,0 +1,9 @@ +description = "Spring Context Indexer" + +dependencies { + testCompile(project(":spring-context")) + testCompile("javax.inject:javax.inject") + testCompile("javax.annotation:javax.annotation-api") + testCompile("javax.transaction:javax.transaction-api") + testCompile("org.eclipse.persistence:javax.persistence") +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/CandidateComponentsIndexer.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/CandidateComponentsIndexer.java new file mode 100644 index 0000000..fcd0d63 --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/CandidateComponentsIndexer.java @@ -0,0 +1,146 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.processor; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import javax.annotation.processing.Completion; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.Processor; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; + +/** + * Annotation {@link Processor} that writes {@link CandidateComponentsMetadata} + * file for spring components. + * + * @author Stephane Nicoll + * @author Juergen Hoeller + * @since 5.0 + */ +public class CandidateComponentsIndexer implements Processor { + + private static final Set TYPE_KINDS = + Collections.unmodifiableSet(EnumSet.of(ElementKind.CLASS, ElementKind.INTERFACE)); + + private MetadataStore metadataStore; + + private MetadataCollector metadataCollector; + + private TypeHelper typeHelper; + + private List stereotypesProviders; + + + @Override + public Set getSupportedOptions() { + return Collections.emptySet(); + } + + @Override + public Set getSupportedAnnotationTypes() { + return Collections.singleton("*"); + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latest(); + } + + @Override + public synchronized void init(ProcessingEnvironment env) { + this.stereotypesProviders = getStereotypesProviders(env); + this.typeHelper = new TypeHelper(env); + this.metadataStore = new MetadataStore(env); + this.metadataCollector = new MetadataCollector(env, this.metadataStore.readMetadata()); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + this.metadataCollector.processing(roundEnv); + roundEnv.getRootElements().forEach(this::processElement); + if (roundEnv.processingOver()) { + writeMetaData(); + } + return false; + } + + @Override + public Iterable getCompletions( + Element element, AnnotationMirror annotation, ExecutableElement member, String userText) { + + return Collections.emptyList(); + } + + + private List getStereotypesProviders(ProcessingEnvironment env) { + List result = new ArrayList<>(); + TypeHelper typeHelper = new TypeHelper(env); + result.add(new IndexedStereotypesProvider(typeHelper)); + result.add(new StandardStereotypesProvider(typeHelper)); + result.add(new PackageInfoStereotypesProvider()); + return result; + } + + private void processElement(Element element) { + addMetadataFor(element); + staticTypesIn(element.getEnclosedElements()).forEach(this::processElement); + } + + private void addMetadataFor(Element element) { + Set stereotypes = new LinkedHashSet<>(); + this.stereotypesProviders.forEach(p -> stereotypes.addAll(p.getStereotypes(element))); + if (!stereotypes.isEmpty()) { + this.metadataCollector.add(new ItemMetadata(this.typeHelper.getType(element), stereotypes)); + } + } + + private void writeMetaData() { + CandidateComponentsMetadata metadata = this.metadataCollector.getMetadata(); + if (!metadata.getItems().isEmpty()) { + try { + this.metadataStore.writeMetadata(metadata); + } + catch (IOException ex) { + throw new IllegalStateException("Failed to write metadata", ex); + } + } + } + + private static List staticTypesIn(Iterable elements) { + List list = new ArrayList<>(); + for (Element element : elements) { + if (TYPE_KINDS.contains(element.getKind()) && element.getModifiers().contains(Modifier.STATIC)) { + list.add((TypeElement) element); + } + } + return list; + } + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/CandidateComponentsMetadata.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/CandidateComponentsMetadata.java new file mode 100644 index 0000000..232207d --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/CandidateComponentsMetadata.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.processor; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Meta-data for candidate components. + * + * @author Stephane Nicoll + * @since 5.0 + */ +class CandidateComponentsMetadata { + + private final List items; + + + public CandidateComponentsMetadata() { + this.items = new ArrayList<>(); + } + + + public void add(ItemMetadata item) { + this.items.add(item); + } + + public List getItems() { + return Collections.unmodifiableList(this.items); + } + + @Override + public String toString() { + return "CandidateComponentsMetadata{" + "items=" + this.items + '}'; + } + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/IndexedStereotypesProvider.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/IndexedStereotypesProvider.java new file mode 100644 index 0000000..c8ef2b7 --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/IndexedStereotypesProvider.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.processor; + +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; + +/** + * A {@link StereotypesProvider} implementation that extracts the stereotypes + * flagged by the {@value INDEXED_ANNOTATION} annotation. This implementation + * honors stereotypes defined this way on meta-annotations. + * + * @author Stephane Nicoll + * @since 5.0 + */ +class IndexedStereotypesProvider implements StereotypesProvider { + + private static final String INDEXED_ANNOTATION = "org.springframework.stereotype.Indexed"; + + private final TypeHelper typeHelper; + + + public IndexedStereotypesProvider(TypeHelper typeHelper) { + this.typeHelper = typeHelper; + } + + + @Override + public Set getStereotypes(Element element) { + Set stereotypes = new LinkedHashSet<>(); + ElementKind kind = element.getKind(); + if (kind != ElementKind.CLASS && kind != ElementKind.INTERFACE) { + return stereotypes; + } + Set seen = new HashSet<>(); + collectStereotypesOnAnnotations(seen, stereotypes, element); + seen = new HashSet<>(); + collectStereotypesOnTypes(seen, stereotypes, element); + return stereotypes; + } + + private void collectStereotypesOnAnnotations(Set seen, Set stereotypes, Element element) { + for (AnnotationMirror annotation : this.typeHelper.getAllAnnotationMirrors(element)) { + Element next = collectStereotypes(seen, stereotypes, element, annotation); + if (next != null) { + collectStereotypesOnAnnotations(seen, stereotypes, next); + } + } + } + + private void collectStereotypesOnTypes(Set seen, Set stereotypes, Element type) { + if (!seen.contains(type)) { + seen.add(type); + if (isAnnotatedWithIndexed(type)) { + stereotypes.add(this.typeHelper.getType(type)); + } + Element superClass = this.typeHelper.getSuperClass(type); + if (superClass != null) { + collectStereotypesOnTypes(seen, stereotypes, superClass); + } + this.typeHelper.getDirectInterfaces(type).forEach( + i -> collectStereotypesOnTypes(seen, stereotypes, i)); + } + } + + private Element collectStereotypes(Set seen, Set stereotypes, Element element, + AnnotationMirror annotation) { + + if (isIndexedAnnotation(annotation)) { + stereotypes.add(this.typeHelper.getType(element)); + } + return getCandidateAnnotationElement(seen, annotation); + } + + private Element getCandidateAnnotationElement(Set seen, AnnotationMirror annotation) { + Element element = annotation.getAnnotationType().asElement(); + if (seen.contains(element)) { + return null; + } + // We need to visit all indexed annotations. + if (!isIndexedAnnotation(annotation)) { + seen.add(element); + } + return (!element.toString().startsWith("java.lang") ? element : null); + } + + private boolean isAnnotatedWithIndexed(Element type) { + for (AnnotationMirror annotation : type.getAnnotationMirrors()) { + if (isIndexedAnnotation(annotation)) { + return true; + } + } + return false; + } + + private boolean isIndexedAnnotation(AnnotationMirror annotation) { + return INDEXED_ANNOTATION.equals(annotation.getAnnotationType().toString()); + } + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/ItemMetadata.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/ItemMetadata.java new file mode 100644 index 0000000..2106326 --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/ItemMetadata.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.processor; + +import java.util.HashSet; +import java.util.Set; + +/** + * Represents one entry in the index. The type defines the identify of the target + * candidate (usually fully qualified name) and the stereotypes are "markers" that can + * be used to retrieve the candidates. A typical use case is the presence of a given + * annotation on the candidate. + * + * @author Stephane Nicoll + * @since 5.0 + */ +class ItemMetadata { + + private final String type; + + private final Set stereotypes; + + + public ItemMetadata(String type, Set stereotypes) { + this.type = type; + this.stereotypes = new HashSet<>(stereotypes); + } + + + public String getType() { + return this.type; + } + + public Set getStereotypes() { + return this.stereotypes; + } + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/MetadataCollector.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/MetadataCollector.java new file mode 100644 index 0000000..c1442e3 --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/MetadataCollector.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.processor; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; + +/** + * Used by {@link CandidateComponentsIndexer} to collect {@link CandidateComponentsMetadata}. + * + * @author Stephane Nicoll + * @since 5.0 + */ +class MetadataCollector { + + private final List metadataItems = new ArrayList<>(); + + private final ProcessingEnvironment processingEnvironment; + + private final CandidateComponentsMetadata previousMetadata; + + private final TypeHelper typeHelper; + + private final Set processedSourceTypes = new HashSet<>(); + + + /** + * Create a new {@code MetadataProcessor} instance. + * @param processingEnvironment the processing environment of the build + * @param previousMetadata any previous metadata or {@code null} + */ + public MetadataCollector(ProcessingEnvironment processingEnvironment, + CandidateComponentsMetadata previousMetadata) { + + this.processingEnvironment = processingEnvironment; + this.previousMetadata = previousMetadata; + this.typeHelper = new TypeHelper(processingEnvironment); + } + + + public void processing(RoundEnvironment roundEnv) { + for (Element element : roundEnv.getRootElements()) { + markAsProcessed(element); + } + } + + private void markAsProcessed(Element element) { + if (element instanceof TypeElement) { + this.processedSourceTypes.add(this.typeHelper.getType(element)); + } + } + + public void add(ItemMetadata metadata) { + this.metadataItems.add(metadata); + } + + public CandidateComponentsMetadata getMetadata() { + CandidateComponentsMetadata metadata = new CandidateComponentsMetadata(); + for (ItemMetadata item : this.metadataItems) { + metadata.add(item); + } + if (this.previousMetadata != null) { + List items = this.previousMetadata.getItems(); + for (ItemMetadata item : items) { + if (shouldBeMerged(item)) { + metadata.add(item); + } + } + } + return metadata; + } + + private boolean shouldBeMerged(ItemMetadata itemMetadata) { + String sourceType = itemMetadata.getType(); + return (sourceType != null && !deletedInCurrentBuild(sourceType) + && !processedInCurrentBuild(sourceType)); + } + + private boolean deletedInCurrentBuild(String sourceType) { + return this.processingEnvironment.getElementUtils() + .getTypeElement(sourceType) == null; + } + + private boolean processedInCurrentBuild(String sourceType) { + return this.processedSourceTypes.contains(sourceType); + } + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/MetadataStore.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/MetadataStore.java new file mode 100644 index 0000000..c00f682 --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/MetadataStore.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.processor; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.tools.FileObject; +import javax.tools.StandardLocation; + +/** + * Store {@link CandidateComponentsMetadata} on the filesystem. + * + * @author Stephane Nicoll + * @since 5.0 + */ +class MetadataStore { + + static final String METADATA_PATH = "META-INF/spring.components"; + + private final ProcessingEnvironment environment; + + + public MetadataStore(ProcessingEnvironment environment) { + this.environment = environment; + } + + + public CandidateComponentsMetadata readMetadata() { + try { + return readMetadata(getMetadataResource().openInputStream()); + } + catch (IOException ex) { + // Failed to read metadata -> ignore. + return null; + } + } + + public void writeMetadata(CandidateComponentsMetadata metadata) throws IOException { + if (!metadata.getItems().isEmpty()) { + try (OutputStream outputStream = createMetadataResource().openOutputStream()) { + PropertiesMarshaller.write(metadata, outputStream); + } + } + } + + + private CandidateComponentsMetadata readMetadata(InputStream in) throws IOException { + try { + return PropertiesMarshaller.read(in); + } + finally { + in.close(); + } + } + + private FileObject getMetadataResource() throws IOException { + return this.environment.getFiler().getResource(StandardLocation.CLASS_OUTPUT, "", METADATA_PATH); + } + + private FileObject createMetadataResource() throws IOException { + return this.environment.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", METADATA_PATH); + } + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/PackageInfoStereotypesProvider.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/PackageInfoStereotypesProvider.java new file mode 100644 index 0000000..d35b437 --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/PackageInfoStereotypesProvider.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.processor; + +import java.util.HashSet; +import java.util.Set; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; + +/** + * A {@link StereotypesProvider} implementation that provides the + * {@value STEREOTYPE} stereotype for each package-info. + * + * @author Stephane Nicoll + * @since 5.0 + */ +class PackageInfoStereotypesProvider implements StereotypesProvider { + + public static final String STEREOTYPE = "package-info"; + + + @Override + public Set getStereotypes(Element element) { + Set stereotypes = new HashSet<>(); + if (element.getKind() == ElementKind.PACKAGE) { + stereotypes.add(STEREOTYPE); + } + return stereotypes; + } + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/PropertiesMarshaller.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/PropertiesMarshaller.java new file mode 100644 index 0000000..84a8d83 --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/PropertiesMarshaller.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.processor; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; + +/** + * Marshaller to write {@link CandidateComponentsMetadata} as properties. + * + * @author Stephane Nicoll + * @author Vedran Pavic + * @since 5.0 + */ +abstract class PropertiesMarshaller { + + public static void write(CandidateComponentsMetadata metadata, OutputStream out) throws IOException { + Properties props = new SortedProperties(true); + metadata.getItems().forEach(m -> props.put(m.getType(), String.join(",", m.getStereotypes()))); + props.store(out, null); + } + + public static CandidateComponentsMetadata read(InputStream in) throws IOException { + CandidateComponentsMetadata result = new CandidateComponentsMetadata(); + Properties props = new Properties(); + props.load(in); + props.forEach((type, value) -> { + Set candidates = new HashSet<>(Arrays.asList(((String) value).split(","))); + result.add(new ItemMetadata((String) type, candidates)); + }); + return result; + } + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/SortedProperties.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/SortedProperties.java new file mode 100644 index 0000000..127a1a2 --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/SortedProperties.java @@ -0,0 +1,156 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.processor; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.StringWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.Set; +import java.util.TreeSet; + +/** + * Specialization of {@link Properties} that sorts properties alphanumerically + * based on their keys. + * + *

    This can be useful when storing the {@link Properties} instance in a + * properties file, since it allows such files to be generated in a repeatable + * manner with consistent ordering of properties. + * + *

    Comments in generated properties files can also be optionally omitted. + * + * @author Sam Brannen + * @since 5.2 + * @see java.util.Properties + */ +@SuppressWarnings("serial") +class SortedProperties extends Properties { + + static final String EOL = System.lineSeparator(); + + private static final Comparator keyComparator = Comparator.comparing(String::valueOf); + + private static final Comparator> entryComparator = Entry.comparingByKey(keyComparator); + + + private final boolean omitComments; + + + /** + * Construct a new {@code SortedProperties} instance that honors the supplied + * {@code omitComments} flag. + * @param omitComments {@code true} if comments should be omitted when + * storing properties in a file + */ + SortedProperties(boolean omitComments) { + this.omitComments = omitComments; + } + + /** + * Construct a new {@code SortedProperties} instance with properties populated + * from the supplied {@link Properties} object and honoring the supplied + * {@code omitComments} flag. + *

    Default properties from the supplied {@code Properties} object will + * not be copied. + * @param properties the {@code Properties} object from which to copy the + * initial properties + * @param omitComments {@code true} if comments should be omitted when + * storing properties in a file + */ + SortedProperties(Properties properties, boolean omitComments) { + this(omitComments); + putAll(properties); + } + + + @Override + public void store(OutputStream out, String comments) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + super.store(baos, (this.omitComments ? null : comments)); + String contents = baos.toString(StandardCharsets.ISO_8859_1.name()); + for (String line : contents.split(EOL)) { + if (!(this.omitComments && line.startsWith("#"))) { + out.write((line + EOL).getBytes(StandardCharsets.ISO_8859_1)); + } + } + } + + @Override + public void store(Writer writer, String comments) throws IOException { + StringWriter stringWriter = new StringWriter(); + super.store(stringWriter, (this.omitComments ? null : comments)); + String contents = stringWriter.toString(); + for (String line : contents.split(EOL)) { + if (!(this.omitComments && line.startsWith("#"))) { + writer.write(line + EOL); + } + } + } + + @Override + public void storeToXML(OutputStream out, String comments) throws IOException { + super.storeToXML(out, (this.omitComments ? null : comments)); + } + + @Override + public void storeToXML(OutputStream out, String comments, String encoding) throws IOException { + super.storeToXML(out, (this.omitComments ? null : comments), encoding); + } + + /** + * Return a sorted enumeration of the keys in this {@link Properties} object. + * @see #keySet() + */ + @Override + public synchronized Enumeration keys() { + return Collections.enumeration(keySet()); + } + + /** + * Return a sorted set of the keys in this {@link Properties} object. + *

    The keys will be converted to strings if necessary using + * {@link String#valueOf(Object)} and sorted alphanumerically according to + * the natural order of strings. + */ + @Override + public Set keySet() { + Set sortedKeys = new TreeSet<>(keyComparator); + sortedKeys.addAll(super.keySet()); + return Collections.synchronizedSet(sortedKeys); + } + + /** + * Return a sorted set of the entries in this {@link Properties} object. + *

    The entries will be sorted based on their keys, and the keys will be + * converted to strings if necessary using {@link String#valueOf(Object)} + * and compared alphanumerically according to the natural order of strings. + */ + @Override + public Set> entrySet() { + Set> sortedEntries = new TreeSet<>(entryComparator); + sortedEntries.addAll(super.entrySet()); + return Collections.synchronizedSet(sortedEntries); + } + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/StandardStereotypesProvider.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/StandardStereotypesProvider.java new file mode 100644 index 0000000..378343c --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/StandardStereotypesProvider.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.processor; + +import java.util.LinkedHashSet; +import java.util.Set; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; + +/** + * A {@link StereotypesProvider} that extract a stereotype for each + * {@code javax.*} annotation placed on a class or interface. + * + * @author Stephane Nicoll + * @since 5.0 + */ +class StandardStereotypesProvider implements StereotypesProvider { + + private final TypeHelper typeHelper; + + + StandardStereotypesProvider(TypeHelper typeHelper) { + this.typeHelper = typeHelper; + } + + + @Override + public Set getStereotypes(Element element) { + Set stereotypes = new LinkedHashSet<>(); + ElementKind kind = element.getKind(); + if (kind != ElementKind.CLASS && kind != ElementKind.INTERFACE) { + return stereotypes; + } + for (AnnotationMirror annotation : this.typeHelper.getAllAnnotationMirrors(element)) { + String type = this.typeHelper.getType(annotation); + if (type.startsWith("javax.")) { + stereotypes.add(type); + } + } + return stereotypes; + } + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/StereotypesProvider.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/StereotypesProvider.java new file mode 100644 index 0000000..e0e5f7a --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/StereotypesProvider.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.processor; + +import java.util.Set; + +import javax.lang.model.element.Element; + +/** + * Provide the list of stereotypes that match an {@link Element}. + * If an element has one more stereotypes, it is referenced in the index + * of candidate components and each stereotype can be queried individually. + * + * @author Stephane Nicoll + * @since 5.0 + */ +interface StereotypesProvider { + + /** + * Return the stereotypes that are present on the given {@link Element}. + * @param element the element to handle + * @return the stereotypes or an empty set if none were found + */ + Set getStereotypes(Element element); + +} diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/TypeHelper.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/TypeHelper.java new file mode 100644 index 0000000..470c039 --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/TypeHelper.java @@ -0,0 +1,124 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.processor; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.QualifiedNameable; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Types; + +/** + * Type utilities. + * + * @author Stephane Nicoll + * @since 5.0 + */ +class TypeHelper { + + private final ProcessingEnvironment env; + + private final Types types; + + + public TypeHelper(ProcessingEnvironment env) { + this.env = env; + this.types = env.getTypeUtils(); + } + + + public String getType(Element element) { + return getType(element != null ? element.asType() : null); + } + + public String getType(AnnotationMirror annotation) { + return getType(annotation != null ? annotation.getAnnotationType() : null); + } + + public String getType(TypeMirror type) { + if (type == null) { + return null; + } + if (type instanceof DeclaredType) { + DeclaredType declaredType = (DeclaredType) type; + Element enclosingElement = declaredType.asElement().getEnclosingElement(); + if (enclosingElement instanceof TypeElement) { + return getQualifiedName(enclosingElement) + "$" + declaredType.asElement().getSimpleName().toString(); + } + else { + return getQualifiedName(declaredType.asElement()); + } + } + return type.toString(); + } + + private String getQualifiedName(Element element) { + if (element instanceof QualifiedNameable) { + return ((QualifiedNameable) element).getQualifiedName().toString(); + } + return element.toString(); + } + + /** + * Return the super class of the specified {@link Element} or null if this + * {@code element} represents {@link Object}. + */ + public Element getSuperClass(Element element) { + List superTypes = this.types.directSupertypes(element.asType()); + if (superTypes.isEmpty()) { + return null; // reached java.lang.Object + } + return this.types.asElement(superTypes.get(0)); + } + + /** + * Return the interfaces that are directly implemented by the + * specified {@link Element} or an empty list if this {@code element} does not + * implement any interface. + */ + public List getDirectInterfaces(Element element) { + List superTypes = this.types.directSupertypes(element.asType()); + List directInterfaces = new ArrayList<>(); + if (superTypes.size() > 1) { // index 0 is the super class + for (int i = 1; i < superTypes.size(); i++) { + Element e = this.types.asElement(superTypes.get(i)); + if (e != null) { + directInterfaces.add(e); + } + } + } + return directInterfaces; + } + + public List getAllAnnotationMirrors(Element e) { + try { + return this.env.getElementUtils().getAllAnnotationMirrors(e); + } + catch (Exception ex) { + // This may fail if one of the annotations is not available. + return Collections.emptyList(); + } + } + +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/processor/CandidateComponentsIndexerTests.java b/spring-context-indexer/src/test/java/org/springframework/context/index/processor/CandidateComponentsIndexerTests.java new file mode 100644 index 0000000..aa1a21d --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/processor/CandidateComponentsIndexerTests.java @@ -0,0 +1,263 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.processor; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Path; + +import javax.annotation.ManagedBean; +import javax.inject.Named; +import javax.persistence.Converter; +import javax.persistence.Embeddable; +import javax.persistence.Entity; +import javax.persistence.MappedSuperclass; +import javax.transaction.Transactional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.context.index.sample.AbstractController; +import org.springframework.context.index.sample.MetaControllerIndexed; +import org.springframework.context.index.sample.SampleComponent; +import org.springframework.context.index.sample.SampleController; +import org.springframework.context.index.sample.SampleEmbedded; +import org.springframework.context.index.sample.SampleMetaController; +import org.springframework.context.index.sample.SampleMetaIndexedController; +import org.springframework.context.index.sample.SampleNonStaticEmbedded; +import org.springframework.context.index.sample.SampleNone; +import org.springframework.context.index.sample.SampleRepository; +import org.springframework.context.index.sample.SampleService; +import org.springframework.context.index.sample.cdi.SampleManagedBean; +import org.springframework.context.index.sample.cdi.SampleNamed; +import org.springframework.context.index.sample.cdi.SampleTransactional; +import org.springframework.context.index.sample.jpa.SampleConverter; +import org.springframework.context.index.sample.jpa.SampleEmbeddable; +import org.springframework.context.index.sample.jpa.SampleEntity; +import org.springframework.context.index.sample.jpa.SampleMappedSuperClass; +import org.springframework.context.index.sample.type.Repo; +import org.springframework.context.index.sample.type.SampleRepo; +import org.springframework.context.index.sample.type.SampleSmartRepo; +import org.springframework.context.index.sample.type.SampleSpecializedRepo; +import org.springframework.context.index.sample.type.SmartRepo; +import org.springframework.context.index.sample.type.SpecializedRepo; +import org.springframework.context.index.test.TestCompiler; +import org.springframework.stereotype.Component; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link CandidateComponentsIndexer}. + * + * @author Stephane Nicoll + * @author Vedran Pavic + * @author Sam Brannen + */ +class CandidateComponentsIndexerTests { + + private TestCompiler compiler; + + + @BeforeEach + void createCompiler(@TempDir Path tempDir) throws IOException { + this.compiler = new TestCompiler(tempDir); + } + + @Test + void noCandidate() { + CandidateComponentsMetadata metadata = compile(SampleNone.class); + assertThat(metadata.getItems()).hasSize(0); + } + + @Test + void noAnnotation() { + CandidateComponentsMetadata metadata = compile(CandidateComponentsIndexerTests.class); + assertThat(metadata.getItems()).hasSize(0); + } + + @Test + void stereotypeComponent() { + testComponent(SampleComponent.class); + } + + @Test + void stereotypeService() { + testComponent(SampleService.class); + } + + @Test + void stereotypeController() { + testComponent(SampleController.class); + } + + @Test + void stereotypeControllerMetaAnnotation() { + testComponent(SampleMetaController.class); + } + + @Test + void stereotypeRepository() { + testSingleComponent(SampleRepository.class, Component.class); + } + + @Test + void stereotypeControllerMetaIndex() { + testSingleComponent(SampleMetaIndexedController.class, Component.class, MetaControllerIndexed.class); + } + + @Test + void stereotypeOnAbstractClass() { + testComponent(AbstractController.class); + } + + @Test + void cdiManagedBean() { + testSingleComponent(SampleManagedBean.class, ManagedBean.class); + } + + @Test + void cdiNamed() { + testSingleComponent(SampleNamed.class, Named.class); + } + + @Test + void cdiTransactional() { + testSingleComponent(SampleTransactional.class, Transactional.class); + } + + @Test + void persistenceEntity() { + testSingleComponent(SampleEntity.class, Entity.class); + } + + @Test + void persistenceMappedSuperClass() { + testSingleComponent(SampleMappedSuperClass.class, MappedSuperclass.class); + } + + @Test + void persistenceEmbeddable() { + testSingleComponent(SampleEmbeddable.class, Embeddable.class); + } + + @Test + void persistenceConverter() { + testSingleComponent(SampleConverter.class, Converter.class); + } + + @Test + void packageInfo() { + CandidateComponentsMetadata metadata = compile("org/springframework/context/index/sample/jpa/package-info"); + assertThat(metadata).has(Metadata.of("org.springframework.context.index.sample.jpa", "package-info")); + } + + @Test + void typeStereotypeFromMetaInterface() { + testSingleComponent(SampleSpecializedRepo.class, Repo.class); + } + + @Test + void typeStereotypeFromInterfaceFromSuperClass() { + testSingleComponent(SampleRepo.class, Repo.class); + } + + @Test + void typeStereotypeFromSeveralInterfaces() { + testSingleComponent(SampleSmartRepo.class, Repo.class, SmartRepo.class); + } + + @Test + void typeStereotypeOnInterface() { + testSingleComponent(SpecializedRepo.class, Repo.class); + } + + @Test + void typeStereotypeOnInterfaceFromSeveralInterfaces() { + testSingleComponent(SmartRepo.class, Repo.class, SmartRepo.class); + } + + @Test + void typeStereotypeOnIndexedInterface() { + testSingleComponent(Repo.class, Repo.class); + } + + @Test + void embeddedCandidatesAreDetected() + throws IOException, ClassNotFoundException { + // Validate nested type structure + String nestedType = "org.springframework.context.index.sample.SampleEmbedded.Another$AnotherPublicCandidate"; + Class type = ClassUtils.forName(nestedType, getClass().getClassLoader()); + assertThat(type).isSameAs(SampleEmbedded.Another.AnotherPublicCandidate.class); + + CandidateComponentsMetadata metadata = compile(SampleEmbedded.class); + assertThat(metadata).has(Metadata.of(SampleEmbedded.PublicCandidate.class, Component.class)); + assertThat(metadata).has(Metadata.of(nestedType, Component.class.getName())); + assertThat(metadata.getItems()).hasSize(2); + } + + @Test + void embeddedNonStaticCandidateAreIgnored() { + CandidateComponentsMetadata metadata = compile(SampleNonStaticEmbedded.class); + assertThat(metadata.getItems()).hasSize(0); + } + + private void testComponent(Class... classes) { + CandidateComponentsMetadata metadata = compile(classes); + for (Class c : classes) { + assertThat(metadata).has(Metadata.of(c, Component.class)); + } + assertThat(metadata.getItems()).hasSize(classes.length); + } + + private void testSingleComponent(Class target, Class... stereotypes) { + CandidateComponentsMetadata metadata = compile(target); + assertThat(metadata).has(Metadata.of(target, stereotypes)); + assertThat(metadata.getItems()).hasSize(1); + } + + private CandidateComponentsMetadata compile(Class... types) { + CandidateComponentsIndexer processor = new CandidateComponentsIndexer(); + this.compiler.getTask(types).call(processor); + return readGeneratedMetadata(this.compiler.getOutputLocation()); + } + + private CandidateComponentsMetadata compile(String... types) { + CandidateComponentsIndexer processor = new CandidateComponentsIndexer(); + this.compiler.getTask(types).call(processor); + return readGeneratedMetadata(this.compiler.getOutputLocation()); + } + + private CandidateComponentsMetadata readGeneratedMetadata(File outputLocation) { + File metadataFile = new File(outputLocation, MetadataStore.METADATA_PATH); + if (metadataFile.isFile()) { + try (FileInputStream fileInputStream = new FileInputStream(metadataFile)) { + CandidateComponentsMetadata metadata = PropertiesMarshaller.read(fileInputStream); + return metadata; + } + catch (IOException ex) { + throw new IllegalStateException("Failed to read metadata from disk", ex); + } + } + else { + return new CandidateComponentsMetadata(); + } + } + +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/processor/Metadata.java b/spring-context-indexer/src/test/java/org/springframework/context/index/processor/Metadata.java new file mode 100644 index 0000000..30ca2ca --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/processor/Metadata.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.processor; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.assertj.core.api.Condition; + +/** + * AssertJ {@link Condition} to help test {@link CandidateComponentsMetadata}. + * + * @author Stephane Nicoll + */ +class Metadata { + + public static Condition of(Class type, Class... stereotypes) { + return of(type.getName(), Arrays.stream(stereotypes).map(Class::getName).collect(Collectors.toList())); + } + + public static Condition of(String type, String... stereotypes) { + return of(type, Arrays.asList(stereotypes)); + } + + public static Condition of(String type, + List stereotypes) { + return new Condition<>(metadata -> { + ItemMetadata itemMetadata = metadata.getItems().stream() + .filter(item -> item.getType().equals(type)) + .findFirst().orElse(null); + return itemMetadata != null && itemMetadata.getStereotypes().size() == stereotypes.size() + && itemMetadata.getStereotypes().containsAll(stereotypes); + }, "Candidates with type %s and stereotypes %s", type, stereotypes); + } + +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/processor/PropertiesMarshallerTests.java b/spring-context-indexer/src/test/java/org/springframework/context/index/processor/PropertiesMarshallerTests.java new file mode 100644 index 0000000..e518841 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/processor/PropertiesMarshallerTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.processor; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashSet; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link PropertiesMarshaller}. + * + * @author Stephane Nicoll + * @author Vedran Pavic + */ +public class PropertiesMarshallerTests { + + @Test + public void readWrite() throws IOException { + CandidateComponentsMetadata metadata = new CandidateComponentsMetadata(); + metadata.add(createItem("com.foo", "first", "second")); + metadata.add(createItem("com.bar", "first")); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + PropertiesMarshaller.write(metadata, outputStream); + CandidateComponentsMetadata readMetadata = PropertiesMarshaller.read( + new ByteArrayInputStream(outputStream.toByteArray())); + assertThat(readMetadata).has(Metadata.of("com.foo", "first", "second")); + assertThat(readMetadata).has(Metadata.of("com.bar", "first")); + assertThat(readMetadata.getItems()).hasSize(2); + } + + @Test + public void metadataIsWrittenDeterministically() throws IOException { + CandidateComponentsMetadata metadata = new CandidateComponentsMetadata(); + metadata.add(createItem("com.b", "type")); + metadata.add(createItem("com.c", "type")); + metadata.add(createItem("com.a", "type")); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + PropertiesMarshaller.write(metadata, outputStream); + String contents = new String(outputStream.toByteArray(), StandardCharsets.ISO_8859_1); + assertThat(contents.split(System.lineSeparator())).containsExactly("com.a=type", "com.b=type", "com.c=type"); + } + + private static ItemMetadata createItem(String type, String... stereotypes) { + return new ItemMetadata(type, new HashSet<>(Arrays.asList(stereotypes))); + } + +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/AbstractController.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/AbstractController.java new file mode 100644 index 0000000..aea4965 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/AbstractController.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.sample; + +import org.springframework.stereotype.Component; + +/** + * Abstract {@link Component} that shouldn't be registered. + * + * @author Stephane Nicoll + */ +@Component +public abstract class AbstractController { + +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/MetaController.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/MetaController.java new file mode 100644 index 0000000..e37a765 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/MetaController.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.sample; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.stereotype.Controller; + +/** + * Sample meta-annotation. + * + * @author Stephane Nicoll + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Controller +public @interface MetaController { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/MetaControllerIndexed.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/MetaControllerIndexed.java new file mode 100644 index 0000000..b67e9c7 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/MetaControllerIndexed.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.sample; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.stereotype.Controller; +import org.springframework.stereotype.Indexed; + +/** + * A test annotation that triggers a dedicated entry in the index. + * + * @author Stephane Nicoll + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Controller +@Indexed +public @interface MetaControllerIndexed { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleComponent.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleComponent.java new file mode 100644 index 0000000..fbf08aa --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleComponent.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.sample; + +import org.springframework.stereotype.Component; + +/** + * Test candidate for {@link Component}. + * + * @author Stephane Nicoll + */ +@Component +public class SampleComponent { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleController.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleController.java new file mode 100644 index 0000000..eed0f8c --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleController.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.sample; + +import org.springframework.stereotype.Controller; + +/** + * Test candidate for {@link Controller}. + * + * @author Stephane Nicoll + */ +@Controller +public class SampleController { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleEmbedded.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleEmbedded.java new file mode 100644 index 0000000..5fd68a1 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleEmbedded.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.sample; + +import org.springframework.stereotype.Component; + +/** + * Test candidate for an embedded {@link Component}. + * + * @author Stephane Nicoll + */ +public class SampleEmbedded { + + @Component + public static class PublicCandidate { + + } + + public static class Another { + + @Component + public static class AnotherPublicCandidate { + + } + } + +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleMetaController.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleMetaController.java new file mode 100644 index 0000000..e474a9c --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleMetaController.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.sample; + +/** + * Test candidate for a {@code Controller} defined using a meta-annotation. + * + * @author Stephane Nicoll + */ +@MetaController +public class SampleMetaController { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleMetaIndexedController.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleMetaIndexedController.java new file mode 100644 index 0000000..1488c1f --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleMetaIndexedController.java @@ -0,0 +1,27 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.sample; + +/** + * Test candidate for a {@code Controller} that adds both the + * {@code Component} and {@code MetaControllerIndexed} stereotypes. + * + * @author Stephane Nicoll + */ +@MetaControllerIndexed +public class SampleMetaIndexedController { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleNonStaticEmbedded.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleNonStaticEmbedded.java new file mode 100644 index 0000000..4e12931 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleNonStaticEmbedded.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.sample; + +import org.springframework.stereotype.Component; + +/** + * Candidate with a inner class that isn't static (and should therefore not be added). + * + * @author Stephane Nicoll + */ +public class SampleNonStaticEmbedded { + + @Component + public class InvalidCandidate { + + } + +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleNone.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleNone.java new file mode 100644 index 0000000..f81de93 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleNone.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.sample; + +import org.springframework.beans.factory.annotation.Qualifier; + +/** + * Candidate with no matching annotation. + * + * @author Stephane Nicoll + */ +@Scope("None") +@Qualifier("None") +public class SampleNone { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleRepository.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleRepository.java new file mode 100644 index 0000000..c850d94 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleRepository.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.sample; + +import org.springframework.stereotype.Repository; + +/** + * Test candidate for {@link Repository}. + * + * @author Stephane Nicoll + */ +@Repository +public class SampleRepository { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleService.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleService.java new file mode 100644 index 0000000..44a3e36 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/SampleService.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.sample; + +import org.springframework.stereotype.Service; + +/** + * Test candidate for {@link Service}. + * + * @author Stephane Nicoll + */ +@Service +public class SampleService { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/Scope.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/Scope.java new file mode 100644 index 0000000..b96de61 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/Scope.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2009 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.sample; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Copy of the {@code @Scope} annotation for testing purposes. + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface Scope { + + String value() default "singleton"; + +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleManagedBean.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleManagedBean.java new file mode 100644 index 0000000..d3bf3dd --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleManagedBean.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.sample.cdi; + +import javax.annotation.ManagedBean; + +/** + * Test candidate for a CDI {@link ManagedBean}. + * + * @author Stephane Nicoll + */ +@ManagedBean +public class SampleManagedBean { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleNamed.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleNamed.java new file mode 100644 index 0000000..20ca034 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleNamed.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.sample.cdi; + +import javax.inject.Named; + +/** + * Test candidate for a CDI {@link Named} bean. + * + * @author Stephane Nicoll + */ +@Named +public class SampleNamed { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleTransactional.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleTransactional.java new file mode 100644 index 0000000..f104d56 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleTransactional.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.sample.cdi; + +import javax.transaction.Transactional; + +/** + * Test candidate for {@link Transactional}. This verifies that the annotation processor + * can process an annotation that declares itself with an annotation that is not on the + * classpath. + * + * @author Vedran Pavic + * @author Stephane Nicoll + */ +@Transactional +public class SampleTransactional { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleConverter.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleConverter.java new file mode 100644 index 0000000..129f090 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleConverter.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.sample.jpa; + +import javax.persistence.Converter; + +/** + * Test candidate for {@link Converter}. + * + * @author Stephane Nicoll + */ +@Converter +public class SampleConverter { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleEmbeddable.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleEmbeddable.java new file mode 100644 index 0000000..7926950 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleEmbeddable.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.sample.jpa; + +import javax.persistence.Embeddable; + +/** + * Test candidate for {@link Embeddable}. + * + * @author Stephane Nicoll + */ +@Embeddable +public class SampleEmbeddable { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleEntity.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleEntity.java new file mode 100644 index 0000000..101c389 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleEntity.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.sample.jpa; + +import javax.persistence.Entity; + +/** + * Test candidate for {@link Entity}. + * + * @author Stephane Nicoll + */ +@Entity +public class SampleEntity { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleMappedSuperClass.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleMappedSuperClass.java new file mode 100644 index 0000000..73737f4 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/SampleMappedSuperClass.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.sample.jpa; + +import javax.persistence.MappedSuperclass; + +/** + * Test candidate for {@link MappedSuperclass}. + * + * @author Stephane Nicoll + */ +@MappedSuperclass +public abstract class SampleMappedSuperClass { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/package-info.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/package-info.java new file mode 100644 index 0000000..17732bb --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/jpa/package-info.java @@ -0,0 +1,6 @@ +/** + * Test candidate for {@code package-info}. + * + * @author Stephane Nicoll + */ +package org.springframework.context.index.sample.jpa; diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/AbstractRepo.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/AbstractRepo.java new file mode 100644 index 0000000..3c9dcba --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/AbstractRepo.java @@ -0,0 +1,23 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.sample.type; + +/** + * @author Stephane Nicoll + */ +public abstract class AbstractRepo implements Repo { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/Repo.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/Repo.java new file mode 100644 index 0000000..e105186 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/Repo.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.sample.type; + +import org.springframework.stereotype.Indexed; + +/** + * A sample interface flagged with {@link Indexed} to indicate that a stereotype + * for all implementations should be added to the index. + * + * @author Stephane Nicoll + */ +@Indexed +public interface Repo { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleEntity.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleEntity.java new file mode 100644 index 0000000..a909a77 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleEntity.java @@ -0,0 +1,23 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.sample.type; + +/** + * @author Stephane Nicoll + */ +public class SampleEntity { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleRepo.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleRepo.java new file mode 100644 index 0000000..61b402c --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleRepo.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.sample.type; + +/** + * A sample that gets its stereotype via an abstract class. + * + * @author Stephane Nicoll + */ +public class SampleRepo extends AbstractRepo { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleSmartRepo.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleSmartRepo.java new file mode 100644 index 0000000..02103cf --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleSmartRepo.java @@ -0,0 +1,27 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.sample.type; + +/** + * A sample that implements both interface used to demonstrate that no + * duplicate stereotypes are generated. + * + * @author Stephane Nicoll + */ +public class SampleSmartRepo + implements SmartRepo, Repo { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleSpecializedRepo.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleSpecializedRepo.java new file mode 100644 index 0000000..7782cf1 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SampleSpecializedRepo.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.sample.type; + +/** + * A sample that does not directly implement the {@link Repo} interface. + * + * @author Stephane Nicoll + */ +public class SampleSpecializedRepo implements SpecializedRepo { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SmartRepo.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SmartRepo.java new file mode 100644 index 0000000..4848fd0 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SmartRepo.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.sample.type; + +import org.springframework.stereotype.Indexed; + +/** + * A {@link Repo} that requires an extra stereotype. + * + * @author Stephane Nicoll + */ +@Indexed +public interface SmartRepo extends Repo { +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SpecializedRepo.java b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SpecializedRepo.java new file mode 100644 index 0000000..b4751e1 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/sample/type/SpecializedRepo.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.sample.type; + +/** + * @author Stephane Nicoll + */ +public interface SpecializedRepo extends Repo { + +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/test/TestCompiler.java b/spring-context-indexer/src/test/java/org/springframework/context/index/test/TestCompiler.java new file mode 100644 index 0000000..ee0eca1 --- /dev/null +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/test/TestCompiler.java @@ -0,0 +1,121 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index.test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; + +import javax.annotation.processing.Processor; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; +import javax.tools.ToolProvider; + +/** + * Wrapper to make the {@link JavaCompiler} easier to use in tests. + * + * @author Stephane Nicoll + * @author Sam Brannen + */ +public class TestCompiler { + + public static final File ORIGINAL_SOURCE_FOLDER = new File("src/test/java"); + + private final JavaCompiler compiler; + + private final StandardJavaFileManager fileManager; + + private final File outputLocation; + + + public TestCompiler(Path tempDir) throws IOException { + this(ToolProvider.getSystemJavaCompiler(), tempDir); + } + + public TestCompiler(JavaCompiler compiler, Path tempDir) throws IOException { + this.compiler = compiler; + this.fileManager = compiler.getStandardFileManager(null, null, null); + this.outputLocation = tempDir.toFile(); + Iterable temp = Collections.singletonList(this.outputLocation); + this.fileManager.setLocation(StandardLocation.CLASS_OUTPUT, temp); + this.fileManager.setLocation(StandardLocation.SOURCE_OUTPUT, temp); + } + + + public TestCompilationTask getTask(Class... types) { + return getTask(Arrays.stream(types).map(Class::getName).toArray(String[]::new)); + } + + public TestCompilationTask getTask(String... types) { + Iterable javaFileObjects = getJavaFileObjects(types); + return getTask(javaFileObjects); + } + + private TestCompilationTask getTask(Iterable javaFileObjects) { + return new TestCompilationTask( + this.compiler.getTask(null, this.fileManager, null, null, null, javaFileObjects)); + } + + public File getOutputLocation() { + return this.outputLocation; + } + + private Iterable getJavaFileObjects(String... types) { + File[] files = new File[types.length]; + for (int i = 0; i < types.length; i++) { + files[i] = getFile(types[i]); + } + return this.fileManager.getJavaFileObjects(files); + } + + private File getFile(String type) { + return new File(getSourceFolder(), sourcePathFor(type)); + } + + private static String sourcePathFor(String type) { + return type.replace(".", "/") + ".java"; + } + + private File getSourceFolder() { + return ORIGINAL_SOURCE_FOLDER; + } + + + /** + * A compilation task. + */ + public static class TestCompilationTask { + + private final JavaCompiler.CompilationTask task; + + public TestCompilationTask(JavaCompiler.CompilationTask task) { + this.task = task; + } + + public void call(Processor... processors) { + this.task.setProcessors(Arrays.asList(processors)); + if (!this.task.call()) { + throw new IllegalStateException("Compilation failed"); + } + } + } + +} diff --git a/spring-context-support/spring-context-support.gradle b/spring-context-support/spring-context-support.gradle new file mode 100644 index 0000000..f8a8631 --- /dev/null +++ b/spring-context-support/spring-context-support.gradle @@ -0,0 +1,32 @@ +description = "Spring Context Support" + +dependencies { + compile(project(":spring-beans")) + compile(project(":spring-context")) + compile(project(":spring-core")) + optional(project(":spring-jdbc")) // for Quartz support + optional(project(":spring-tx")) // for Quartz support + optional("javax.activation:javax.activation-api") + optional("javax.mail:javax.mail-api") + optional("javax.cache:cache-api") + optional("com.github.ben-manes.caffeine:caffeine") + optional("net.sf.ehcache:ehcache") + optional("org.quartz-scheduler:quartz") + optional("org.codehaus.fabric3.api:commonj") + optional("org.freemarker:freemarker") + testCompile(project(":spring-context")) + testCompile(testFixtures(project(":spring-beans"))) + testCompile(testFixtures(project(":spring-context"))) + testCompile(testFixtures(project(":spring-core"))) + testCompile(testFixtures(project(":spring-tx"))) + testCompile("org.hsqldb:hsqldb") + testCompile("org.hibernate:hibernate-validator") + testCompile("javax.annotation:javax.annotation-api") + testRuntime("org.ehcache:jcache") + testRuntime("org.ehcache:ehcache") + testRuntime("org.glassfish:javax.el") + testRuntime("com.sun.mail:javax.mail") + testFixturesApi("org.junit.jupiter:junit-jupiter-api") + testFixturesImplementation("org.assertj:assertj-core") + testFixturesImplementation("org.mockito:mockito-core") +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCache.java b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCache.java new file mode 100644 index 0000000..ef8c3b0 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCache.java @@ -0,0 +1,176 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.caffeine; + +import java.util.concurrent.Callable; +import java.util.function.Function; + +import com.github.benmanes.caffeine.cache.LoadingCache; + +import org.springframework.cache.support.AbstractValueAdaptingCache; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Spring {@link org.springframework.cache.Cache} adapter implementation + * on top of a Caffeine {@link com.github.benmanes.caffeine.cache.Cache} instance. + * + *

    Requires Caffeine 2.1 or higher. + * + * @author Ben Manes + * @author Juergen Hoeller + * @author Stephane Nicoll + * @since 4.3 + * @see CaffeineCacheManager + */ +public class CaffeineCache extends AbstractValueAdaptingCache { + + private final String name; + + private final com.github.benmanes.caffeine.cache.Cache cache; + + + /** + * Create a {@link CaffeineCache} instance with the specified name and the + * given internal {@link com.github.benmanes.caffeine.cache.Cache} to use. + * @param name the name of the cache + * @param cache the backing Caffeine Cache instance + */ + public CaffeineCache(String name, com.github.benmanes.caffeine.cache.Cache cache) { + this(name, cache, true); + } + + /** + * Create a {@link CaffeineCache} instance with the specified name and the + * given internal {@link com.github.benmanes.caffeine.cache.Cache} to use. + * @param name the name of the cache + * @param cache the backing Caffeine Cache instance + * @param allowNullValues whether to accept and convert {@code null} + * values for this cache + */ + public CaffeineCache(String name, com.github.benmanes.caffeine.cache.Cache cache, + boolean allowNullValues) { + + super(allowNullValues); + Assert.notNull(name, "Name must not be null"); + Assert.notNull(cache, "Cache must not be null"); + this.name = name; + this.cache = cache; + } + + + @Override + public final String getName() { + return this.name; + } + + @Override + public final com.github.benmanes.caffeine.cache.Cache getNativeCache() { + return this.cache; + } + + @SuppressWarnings("unchecked") + @Override + @Nullable + public T get(Object key, final Callable valueLoader) { + return (T) fromStoreValue(this.cache.get(key, new LoadFunction(valueLoader))); + } + + @Override + @Nullable + protected Object lookup(Object key) { + if (this.cache instanceof LoadingCache) { + return ((LoadingCache) this.cache).get(key); + } + return this.cache.getIfPresent(key); + } + + @Override + public void put(Object key, @Nullable Object value) { + this.cache.put(key, toStoreValue(value)); + } + + @Override + @Nullable + public ValueWrapper putIfAbsent(Object key, @Nullable final Object value) { + PutIfAbsentFunction callable = new PutIfAbsentFunction(value); + Object result = this.cache.get(key, callable); + return (callable.called ? null : toValueWrapper(result)); + } + + @Override + public void evict(Object key) { + this.cache.invalidate(key); + } + + @Override + public boolean evictIfPresent(Object key) { + return (this.cache.asMap().remove(key) != null); + } + + @Override + public void clear() { + this.cache.invalidateAll(); + } + + @Override + public boolean invalidate() { + boolean notEmpty = !this.cache.asMap().isEmpty(); + this.cache.invalidateAll(); + return notEmpty; + } + + + private class PutIfAbsentFunction implements Function { + + @Nullable + private final Object value; + + private boolean called; + + public PutIfAbsentFunction(@Nullable Object value) { + this.value = value; + } + + @Override + public Object apply(Object key) { + this.called = true; + return toStoreValue(this.value); + } + } + + + private class LoadFunction implements Function { + + private final Callable valueLoader; + + public LoadFunction(Callable valueLoader) { + this.valueLoader = valueLoader; + } + + @Override + public Object apply(Object o) { + try { + return toStoreValue(this.valueLoader.call()); + } + catch (Exception ex) { + throw new ValueRetrievalException(o, this.valueLoader, ex); + } + } + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java new file mode 100644 index 0000000..d5b77cb --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java @@ -0,0 +1,268 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.caffeine; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +import com.github.benmanes.caffeine.cache.CacheLoader; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.CaffeineSpec; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * {@link CacheManager} implementation that lazily builds {@link CaffeineCache} + * instances for each {@link #getCache} request. Also supports a 'static' mode + * where the set of cache names is pre-defined through {@link #setCacheNames}, + * with no dynamic creation of further cache regions at runtime. + * + *

    The configuration of the underlying cache can be fine-tuned through a + * {@link Caffeine} builder or {@link CaffeineSpec}, passed into this + * CacheManager through {@link #setCaffeine}/{@link #setCaffeineSpec}. + * A {@link CaffeineSpec}-compliant expression value can also be applied + * via the {@link #setCacheSpecification "cacheSpecification"} bean property. + * + *

    Requires Caffeine 2.1 or higher. + * + * @author Ben Manes + * @author Juergen Hoeller + * @author Stephane Nicoll + * @author Sam Brannen + * @since 4.3 + * @see CaffeineCache + */ +public class CaffeineCacheManager implements CacheManager { + + private Caffeine cacheBuilder = Caffeine.newBuilder(); + + @Nullable + private CacheLoader cacheLoader; + + private boolean allowNullValues = true; + + private boolean dynamic = true; + + private final Map cacheMap = new ConcurrentHashMap<>(16); + + private final Collection customCacheNames = new CopyOnWriteArrayList<>(); + + + /** + * Construct a dynamic CaffeineCacheManager, + * lazily creating cache instances as they are being requested. + */ + public CaffeineCacheManager() { + } + + /** + * Construct a static CaffeineCacheManager, + * managing caches for the specified cache names only. + */ + public CaffeineCacheManager(String... cacheNames) { + setCacheNames(Arrays.asList(cacheNames)); + } + + + /** + * Specify the set of cache names for this CacheManager's 'static' mode. + *

    The number of caches and their names will be fixed after a call to this method, + * with no creation of further cache regions at runtime. + *

    Calling this with a {@code null} collection argument resets the + * mode to 'dynamic', allowing for further creation of caches again. + */ + public void setCacheNames(@Nullable Collection cacheNames) { + if (cacheNames != null) { + for (String name : cacheNames) { + this.cacheMap.put(name, createCaffeineCache(name)); + } + this.dynamic = false; + } + else { + this.dynamic = true; + } + } + + /** + * Set the Caffeine to use for building each individual + * {@link CaffeineCache} instance. + * @see #createNativeCaffeineCache + * @see com.github.benmanes.caffeine.cache.Caffeine#build() + */ + public void setCaffeine(Caffeine caffeine) { + Assert.notNull(caffeine, "Caffeine must not be null"); + doSetCaffeine(caffeine); + } + + /** + * Set the {@link CaffeineSpec} to use for building each individual + * {@link CaffeineCache} instance. + * @see #createNativeCaffeineCache + * @see com.github.benmanes.caffeine.cache.Caffeine#from(CaffeineSpec) + */ + public void setCaffeineSpec(CaffeineSpec caffeineSpec) { + doSetCaffeine(Caffeine.from(caffeineSpec)); + } + + /** + * Set the Caffeine cache specification String to use for building each + * individual {@link CaffeineCache} instance. The given value needs to + * comply with Caffeine's {@link CaffeineSpec} (see its javadoc). + * @see #createNativeCaffeineCache + * @see com.github.benmanes.caffeine.cache.Caffeine#from(String) + */ + public void setCacheSpecification(String cacheSpecification) { + doSetCaffeine(Caffeine.from(cacheSpecification)); + } + + private void doSetCaffeine(Caffeine cacheBuilder) { + if (!ObjectUtils.nullSafeEquals(this.cacheBuilder, cacheBuilder)) { + this.cacheBuilder = cacheBuilder; + refreshCommonCaches(); + } + } + + /** + * Set the Caffeine CacheLoader to use for building each individual + * {@link CaffeineCache} instance, turning it into a LoadingCache. + * @see #createNativeCaffeineCache + * @see com.github.benmanes.caffeine.cache.Caffeine#build(CacheLoader) + * @see com.github.benmanes.caffeine.cache.LoadingCache + */ + public void setCacheLoader(CacheLoader cacheLoader) { + if (!ObjectUtils.nullSafeEquals(this.cacheLoader, cacheLoader)) { + this.cacheLoader = cacheLoader; + refreshCommonCaches(); + } + } + + /** + * Specify whether to accept and convert {@code null} values for all caches + * in this cache manager. + *

    Default is "true", despite Caffeine itself not supporting {@code null} values. + * An internal holder object will be used to store user-level {@code null}s. + */ + public void setAllowNullValues(boolean allowNullValues) { + if (this.allowNullValues != allowNullValues) { + this.allowNullValues = allowNullValues; + refreshCommonCaches(); + } + } + + /** + * Return whether this cache manager accepts and converts {@code null} values + * for all of its caches. + */ + public boolean isAllowNullValues() { + return this.allowNullValues; + } + + + @Override + public Collection getCacheNames() { + return Collections.unmodifiableSet(this.cacheMap.keySet()); + } + + @Override + @Nullable + public Cache getCache(String name) { + return this.cacheMap.computeIfAbsent(name, cacheName -> + this.dynamic ? createCaffeineCache(cacheName) : null); + } + + + /** + * Register the given native Caffeine Cache instance with this cache manager, + * adapting it to Spring's cache API for exposure through {@link #getCache}. + * Any number of such custom caches may be registered side by side. + *

    This allows for custom settings per cache (as opposed to all caches + * sharing the common settings in the cache manager's configuration) and + * is typically used with the Caffeine builder API: + * {@code registerCustomCache("myCache", Caffeine.newBuilder().maximumSize(10).build())} + *

    Note that any other caches, whether statically specified through + * {@link #setCacheNames} or dynamically built on demand, still operate + * with the common settings in the cache manager's configuration. + * @param name the name of the cache + * @param cache the custom Caffeine Cache instance to register + * @since 5.2.8 + * @see #adaptCaffeineCache + */ + public void registerCustomCache(String name, com.github.benmanes.caffeine.cache.Cache cache) { + this.customCacheNames.add(name); + this.cacheMap.put(name, adaptCaffeineCache(name, cache)); + } + + /** + * Adapt the given new native Caffeine Cache instance to Spring's {@link Cache} + * abstraction for the specified cache name. + * @param name the name of the cache + * @param cache the native Caffeine Cache instance + * @return the Spring CaffeineCache adapter (or a decorator thereof) + * @since 5.2.8 + * @see CaffeineCache + * @see #isAllowNullValues() + */ + protected Cache adaptCaffeineCache(String name, com.github.benmanes.caffeine.cache.Cache cache) { + return new CaffeineCache(name, cache, isAllowNullValues()); + } + + /** + * Build a common {@link CaffeineCache} instance for the specified cache name, + * using the common Caffeine configuration specified on this cache manager. + *

    Delegates to {@link #adaptCaffeineCache} as the adaptation method to + * Spring's cache abstraction (allowing for centralized decoration etc), + * passing in a freshly built native Caffeine Cache instance. + * @param name the name of the cache + * @return the Spring CaffeineCache adapter (or a decorator thereof) + * @see #adaptCaffeineCache + * @see #createNativeCaffeineCache + */ + protected Cache createCaffeineCache(String name) { + return adaptCaffeineCache(name, createNativeCaffeineCache(name)); + } + + /** + * Build a common Caffeine Cache instance for the specified cache name, + * using the common Caffeine configuration specified on this cache manager. + * @param name the name of the cache + * @return the native Caffeine Cache instance + * @see #createCaffeineCache + */ + protected com.github.benmanes.caffeine.cache.Cache createNativeCaffeineCache(String name) { + return (this.cacheLoader != null ? this.cacheBuilder.build(this.cacheLoader) : this.cacheBuilder.build()); + } + + /** + * Recreate the common caches with the current state of this manager. + */ + private void refreshCommonCaches() { + for (Map.Entry entry : this.cacheMap.entrySet()) { + if (!this.customCacheNames.contains(entry.getKey())) { + entry.setValue(createCaffeineCache(entry.getKey())); + } + } + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/caffeine/package-info.java b/spring-context-support/src/main/java/org/springframework/cache/caffeine/package-info.java new file mode 100644 index 0000000..864fb2e --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/caffeine/package-info.java @@ -0,0 +1,11 @@ +/** + * Support classes for the open source cache in + * Caffeine library, + * allowing to set up Caffeine caches within Spring's cache abstraction. + */ +@NonNullApi +@NonNullFields +package org.springframework.cache.caffeine; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheCache.java b/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheCache.java new file mode 100644 index 0000000..4309fa7 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheCache.java @@ -0,0 +1,171 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.ehcache; + +import java.util.concurrent.Callable; + +import net.sf.ehcache.Ehcache; +import net.sf.ehcache.Element; +import net.sf.ehcache.Status; + +import org.springframework.cache.Cache; +import org.springframework.cache.support.SimpleValueWrapper; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * {@link Cache} implementation on top of an {@link Ehcache} instance. + * + * @author Costin Leau + * @author Juergen Hoeller + * @author Stephane Nicoll + * @since 3.1 + * @see EhCacheCacheManager + */ +public class EhCacheCache implements Cache { + + private final Ehcache cache; + + + /** + * Create an {@link EhCacheCache} instance. + * @param ehcache the backing Ehcache instance + */ + public EhCacheCache(Ehcache ehcache) { + Assert.notNull(ehcache, "Ehcache must not be null"); + Status status = ehcache.getStatus(); + if (!Status.STATUS_ALIVE.equals(status)) { + throw new IllegalArgumentException( + "An 'alive' Ehcache is required - current cache is " + status.toString()); + } + this.cache = ehcache; + } + + + @Override + public final String getName() { + return this.cache.getName(); + } + + @Override + public final Ehcache getNativeCache() { + return this.cache; + } + + @Override + @Nullable + public ValueWrapper get(Object key) { + Element element = lookup(key); + return toValueWrapper(element); + } + + @SuppressWarnings("unchecked") + @Override + @Nullable + public T get(Object key, @Nullable Class type) { + Element element = this.cache.get(key); + Object value = (element != null ? element.getObjectValue() : null); + if (value != null && type != null && !type.isInstance(value)) { + throw new IllegalStateException( + "Cached value is not of required type [" + type.getName() + "]: " + value); + } + return (T) value; + } + + @SuppressWarnings("unchecked") + @Override + @Nullable + public T get(Object key, Callable valueLoader) { + Element element = lookup(key); + if (element != null) { + return (T) element.getObjectValue(); + } + else { + this.cache.acquireWriteLockOnKey(key); + try { + element = lookup(key); // one more attempt with the write lock + if (element != null) { + return (T) element.getObjectValue(); + } + else { + return loadValue(key, valueLoader); + } + } + finally { + this.cache.releaseWriteLockOnKey(key); + } + } + } + + private T loadValue(Object key, Callable valueLoader) { + T value; + try { + value = valueLoader.call(); + } + catch (Throwable ex) { + throw new ValueRetrievalException(key, valueLoader, ex); + } + put(key, value); + return value; + } + + @Override + public void put(Object key, @Nullable Object value) { + this.cache.put(new Element(key, value)); + } + + @Override + @Nullable + public ValueWrapper putIfAbsent(Object key, @Nullable Object value) { + Element existingElement = this.cache.putIfAbsent(new Element(key, value)); + return toValueWrapper(existingElement); + } + + @Override + public void evict(Object key) { + this.cache.remove(key); + } + + @Override + public boolean evictIfPresent(Object key) { + return this.cache.remove(key); + } + + @Override + public void clear() { + this.cache.removeAll(); + } + + @Override + public boolean invalidate() { + boolean notEmpty = (this.cache.getSize() > 0); + this.cache.removeAll(); + return notEmpty; + } + + + @Nullable + private Element lookup(Object key) { + return this.cache.get(key); + } + + @Nullable + private ValueWrapper toValueWrapper(@Nullable Element element) { + return (element != null ? new SimpleValueWrapper(element.getObjectValue()) : null); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheCacheManager.java b/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheCacheManager.java new file mode 100644 index 0000000..f3e58a5 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheCacheManager.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.ehcache; + +import java.util.Collection; +import java.util.LinkedHashSet; + +import net.sf.ehcache.Ehcache; +import net.sf.ehcache.Status; + +import org.springframework.cache.Cache; +import org.springframework.cache.transaction.AbstractTransactionSupportingCacheManager; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * CacheManager backed by an EhCache {@link net.sf.ehcache.CacheManager}. + * + * @author Costin Leau + * @author Juergen Hoeller + * @author Stephane Nicoll + * @since 3.1 + * @see EhCacheCache + */ +public class EhCacheCacheManager extends AbstractTransactionSupportingCacheManager { + + @Nullable + private net.sf.ehcache.CacheManager cacheManager; + + + /** + * Create a new EhCacheCacheManager, setting the target EhCache CacheManager + * through the {@link #setCacheManager} bean property. + */ + public EhCacheCacheManager() { + } + + /** + * Create a new EhCacheCacheManager for the given backing EhCache CacheManager. + * @param cacheManager the backing EhCache {@link net.sf.ehcache.CacheManager} + */ + public EhCacheCacheManager(net.sf.ehcache.CacheManager cacheManager) { + this.cacheManager = cacheManager; + } + + + /** + * Set the backing EhCache {@link net.sf.ehcache.CacheManager}. + */ + public void setCacheManager(@Nullable net.sf.ehcache.CacheManager cacheManager) { + this.cacheManager = cacheManager; + } + + /** + * Return the backing EhCache {@link net.sf.ehcache.CacheManager}. + */ + @Nullable + public net.sf.ehcache.CacheManager getCacheManager() { + return this.cacheManager; + } + + @Override + public void afterPropertiesSet() { + if (getCacheManager() == null) { + setCacheManager(EhCacheManagerUtils.buildCacheManager()); + } + super.afterPropertiesSet(); + } + + + @Override + protected Collection loadCaches() { + net.sf.ehcache.CacheManager cacheManager = getCacheManager(); + Assert.state(cacheManager != null, "No CacheManager set"); + + Status status = cacheManager.getStatus(); + if (!Status.STATUS_ALIVE.equals(status)) { + throw new IllegalStateException( + "An 'alive' EhCache CacheManager is required - current cache is " + status.toString()); + } + + String[] names = getCacheManager().getCacheNames(); + Collection caches = new LinkedHashSet<>(names.length); + for (String name : names) { + caches.add(new EhCacheCache(getCacheManager().getEhcache(name))); + } + return caches; + } + + @Override + protected Cache getMissingCache(String name) { + net.sf.ehcache.CacheManager cacheManager = getCacheManager(); + Assert.state(cacheManager != null, "No CacheManager set"); + + // Check the EhCache cache again (in case the cache was added at runtime) + Ehcache ehcache = cacheManager.getEhcache(name); + if (ehcache != null) { + return new EhCacheCache(ehcache); + } + return null; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheFactoryBean.java b/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheFactoryBean.java new file mode 100644 index 0000000..3d4f839 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheFactoryBean.java @@ -0,0 +1,329 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.ehcache; + +import java.util.Set; + +import net.sf.ehcache.Cache; +import net.sf.ehcache.CacheException; +import net.sf.ehcache.CacheManager; +import net.sf.ehcache.Ehcache; +import net.sf.ehcache.bootstrap.BootstrapCacheLoader; +import net.sf.ehcache.config.CacheConfiguration; +import net.sf.ehcache.constructs.blocking.BlockingCache; +import net.sf.ehcache.constructs.blocking.CacheEntryFactory; +import net.sf.ehcache.constructs.blocking.SelfPopulatingCache; +import net.sf.ehcache.constructs.blocking.UpdatingCacheEntryFactory; +import net.sf.ehcache.constructs.blocking.UpdatingSelfPopulatingCache; +import net.sf.ehcache.event.CacheEventListener; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.lang.Nullable; + +/** + * {@link FactoryBean} that creates a named EhCache {@link net.sf.ehcache.Cache} instance + * (or a decorator that implements the {@link net.sf.ehcache.Ehcache} interface), + * representing a cache region within an EhCache {@link net.sf.ehcache.CacheManager}. + * + *

    If the specified named cache is not configured in the cache configuration descriptor, + * this FactoryBean will construct an instance of a Cache with the provided name and the + * specified cache properties and add it to the CacheManager for later retrieval. If some + * or all properties are not set at configuration time, this FactoryBean will use defaults. + * + *

    Note: If the named Cache instance is found, the properties will be ignored and the + * Cache instance will be retrieved from the CacheManager. + * + *

    Note: As of Spring 5.0, Spring's EhCache support requires EhCache 2.10 or higher. + * + * @author Juergen Hoeller + * @author Dmitriy Kopylenko + * @since 1.1.1 + * @see #setCacheManager + * @see EhCacheManagerFactoryBean + * @see net.sf.ehcache.Cache + */ +public class EhCacheFactoryBean extends CacheConfiguration implements FactoryBean, BeanNameAware, InitializingBean { + + protected final Log logger = LogFactory.getLog(getClass()); + + @Nullable + private CacheManager cacheManager; + + private boolean blocking = false; + + @Nullable + private CacheEntryFactory cacheEntryFactory; + + @Nullable + private BootstrapCacheLoader bootstrapCacheLoader; + + @Nullable + private Set cacheEventListeners; + + private boolean disabled = false; + + @Nullable + private String beanName; + + @Nullable + private Ehcache cache; + + + public EhCacheFactoryBean() { + setMaxEntriesLocalHeap(10000); + setMaxEntriesLocalDisk(10000000); + setTimeToLiveSeconds(120); + setTimeToIdleSeconds(120); + } + + + /** + * Set a CacheManager from which to retrieve a named Cache instance. + * By default, {@code CacheManager.getInstance()} will be called. + *

    Note that in particular for persistent caches, it is advisable to + * properly handle the shutdown of the CacheManager: Set up a separate + * EhCacheManagerFactoryBean and pass a reference to this bean property. + *

    A separate EhCacheManagerFactoryBean is also necessary for loading + * EhCache configuration from a non-default config location. + * @see EhCacheManagerFactoryBean + * @see net.sf.ehcache.CacheManager#getInstance + */ + public void setCacheManager(CacheManager cacheManager) { + this.cacheManager = cacheManager; + } + + /** + * Set a name for which to retrieve or create a cache instance. + * Default is the bean name of this EhCacheFactoryBean. + */ + public void setCacheName(String cacheName) { + setName(cacheName); + } + + /** + * Set the time to live. + * @see #setTimeToLiveSeconds(long) + */ + public void setTimeToLive(int timeToLive) { + setTimeToLiveSeconds(timeToLive); + } + + /** + * Set the time to idle. + * @see #setTimeToIdleSeconds(long) + */ + public void setTimeToIdle(int timeToIdle) { + setTimeToIdleSeconds(timeToIdle); + } + + /** + * Set the disk spool buffer size (in MB). + * @see #setDiskSpoolBufferSizeMB(int) + */ + public void setDiskSpoolBufferSize(int diskSpoolBufferSize) { + setDiskSpoolBufferSizeMB(diskSpoolBufferSize); + } + + /** + * Set whether to use a blocking cache that lets read attempts block + * until the requested element is created. + *

    If you intend to build a self-populating blocking cache, + * consider specifying a {@link #setCacheEntryFactory CacheEntryFactory}. + * @see net.sf.ehcache.constructs.blocking.BlockingCache + * @see #setCacheEntryFactory + */ + public void setBlocking(boolean blocking) { + this.blocking = blocking; + } + + /** + * Set an EhCache {@link net.sf.ehcache.constructs.blocking.CacheEntryFactory} + * to use for a self-populating cache. If such a factory is specified, + * the cache will be decorated with EhCache's + * {@link net.sf.ehcache.constructs.blocking.SelfPopulatingCache}. + *

    The specified factory can be of type + * {@link net.sf.ehcache.constructs.blocking.UpdatingCacheEntryFactory}, + * which will lead to the use of an + * {@link net.sf.ehcache.constructs.blocking.UpdatingSelfPopulatingCache}. + *

    Note: Any such self-populating cache is automatically a blocking cache. + * @see net.sf.ehcache.constructs.blocking.SelfPopulatingCache + * @see net.sf.ehcache.constructs.blocking.UpdatingSelfPopulatingCache + * @see net.sf.ehcache.constructs.blocking.UpdatingCacheEntryFactory + */ + public void setCacheEntryFactory(CacheEntryFactory cacheEntryFactory) { + this.cacheEntryFactory = cacheEntryFactory; + } + + /** + * Set an EhCache {@link net.sf.ehcache.bootstrap.BootstrapCacheLoader} + * for this cache, if any. + */ + public void setBootstrapCacheLoader(BootstrapCacheLoader bootstrapCacheLoader) { + this.bootstrapCacheLoader = bootstrapCacheLoader; + } + + /** + * Specify EhCache {@link net.sf.ehcache.event.CacheEventListener cache event listeners} + * to registered with this cache. + */ + public void setCacheEventListeners(Set cacheEventListeners) { + this.cacheEventListeners = cacheEventListeners; + } + + /** + * Set whether this cache should be marked as disabled. + * @see net.sf.ehcache.Cache#setDisabled + */ + public void setDisabled(boolean disabled) { + this.disabled = disabled; + } + + @Override + public void setBeanName(String name) { + this.beanName = name; + } + + + @Override + public void afterPropertiesSet() throws CacheException { + // If no cache name given, use bean name as cache name. + String cacheName = getName(); + if (cacheName == null) { + cacheName = this.beanName; + if (cacheName != null) { + setName(cacheName); + } + } + + // If no CacheManager given, fetch the default. + if (this.cacheManager == null) { + if (logger.isDebugEnabled()) { + logger.debug("Using default EhCache CacheManager for cache region '" + cacheName + "'"); + } + this.cacheManager = CacheManager.getInstance(); + } + + synchronized (this.cacheManager) { + // Fetch cache region: If none with the given name exists, create one on the fly. + Ehcache rawCache; + boolean cacheExists = this.cacheManager.cacheExists(cacheName); + + if (cacheExists) { + if (logger.isDebugEnabled()) { + logger.debug("Using existing EhCache cache region '" + cacheName + "'"); + } + rawCache = this.cacheManager.getEhcache(cacheName); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Creating new EhCache cache region '" + cacheName + "'"); + } + rawCache = createCache(); + rawCache.setBootstrapCacheLoader(this.bootstrapCacheLoader); + } + + if (this.cacheEventListeners != null) { + for (CacheEventListener listener : this.cacheEventListeners) { + rawCache.getCacheEventNotificationService().registerListener(listener); + } + } + + // Needs to happen after listener registration but before setStatisticsEnabled + if (!cacheExists) { + this.cacheManager.addCache(rawCache); + } + + if (this.disabled) { + rawCache.setDisabled(true); + } + + Ehcache decoratedCache = decorateCache(rawCache); + if (decoratedCache != rawCache) { + this.cacheManager.replaceCacheWithDecoratedCache(rawCache, decoratedCache); + } + this.cache = decoratedCache; + } + } + + /** + * Create a raw Cache object based on the configuration of this FactoryBean. + */ + protected Cache createCache() { + return new Cache(this); + } + + /** + * Decorate the given Cache, if necessary. + * @param cache the raw Cache object, based on the configuration of this FactoryBean + * @return the (potentially decorated) cache object to be registered with the CacheManager + */ + protected Ehcache decorateCache(Ehcache cache) { + if (this.cacheEntryFactory != null) { + if (this.cacheEntryFactory instanceof UpdatingCacheEntryFactory) { + return new UpdatingSelfPopulatingCache(cache, (UpdatingCacheEntryFactory) this.cacheEntryFactory); + } + else { + return new SelfPopulatingCache(cache, this.cacheEntryFactory); + } + } + if (this.blocking) { + return new BlockingCache(cache); + } + return cache; + } + + + @Override + @Nullable + public Ehcache getObject() { + return this.cache; + } + + /** + * Predict the particular {@code Ehcache} implementation that will be returned from + * {@link #getObject()} based on logic in {@link #createCache()} and + * {@link #decorateCache(Ehcache)} as orchestrated by {@link #afterPropertiesSet()}. + */ + @Override + public Class getObjectType() { + if (this.cache != null) { + return this.cache.getClass(); + } + if (this.cacheEntryFactory != null) { + if (this.cacheEntryFactory instanceof UpdatingCacheEntryFactory) { + return UpdatingSelfPopulatingCache.class; + } + else { + return SelfPopulatingCache.class; + } + } + if (this.blocking) { + return BlockingCache.class; + } + return Cache.class; + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheManagerFactoryBean.java b/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheManagerFactoryBean.java new file mode 100644 index 0000000..0683419 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheManagerFactoryBean.java @@ -0,0 +1,199 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.ehcache; + +import net.sf.ehcache.CacheException; +import net.sf.ehcache.CacheManager; +import net.sf.ehcache.config.Configuration; +import net.sf.ehcache.config.ConfigurationFactory; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; + +/** + * {@link FactoryBean} that exposes an EhCache {@link net.sf.ehcache.CacheManager} + * instance (independent or shared), configured from a specified config location. + * + *

    If no config location is specified, a CacheManager will be configured from + * "ehcache.xml" in the root of the class path (that is, default EhCache initialization + * - as defined in the EhCache docs - will apply). + * + *

    Setting up a separate EhCacheManagerFactoryBean is also advisable when using + * EhCacheFactoryBean, as it provides a (by default) independent CacheManager instance + * and cares for proper shutdown of the CacheManager. EhCacheManagerFactoryBean is + * also necessary for loading EhCache configuration from a non-default config location. + * + *

    Note: As of Spring 5.0, Spring's EhCache support requires EhCache 2.10 or higher. + * + * @author Juergen Hoeller + * @author Dmitriy Kopylenko + * @since 1.1.1 + * @see #setConfigLocation + * @see #setShared + * @see EhCacheFactoryBean + * @see net.sf.ehcache.CacheManager + */ +public class EhCacheManagerFactoryBean implements FactoryBean, InitializingBean, DisposableBean { + + protected final Log logger = LogFactory.getLog(getClass()); + + @Nullable + private Resource configLocation; + + @Nullable + private String cacheManagerName; + + private boolean acceptExisting = false; + + private boolean shared = false; + + @Nullable + private CacheManager cacheManager; + + private boolean locallyManaged = true; + + + /** + * Set the location of the EhCache config file. A typical value is "/WEB-INF/ehcache.xml". + *

    Default is "ehcache.xml" in the root of the class path, or if not found, + * "ehcache-failsafe.xml" in the EhCache jar (default EhCache initialization). + * @see net.sf.ehcache.CacheManager#create(java.io.InputStream) + * @see net.sf.ehcache.CacheManager#CacheManager(java.io.InputStream) + */ + public void setConfigLocation(Resource configLocation) { + this.configLocation = configLocation; + } + + /** + * Set the name of the EhCache CacheManager (if a specific name is desired). + * @see net.sf.ehcache.config.Configuration#setName(String) + */ + public void setCacheManagerName(String cacheManagerName) { + this.cacheManagerName = cacheManagerName; + } + + /** + * Set whether an existing EhCache CacheManager of the same name will be accepted + * for this EhCacheManagerFactoryBean setup. Default is "false". + *

    Typically used in combination with {@link #setCacheManagerName "cacheManagerName"} + * but will simply work with the default CacheManager name if none specified. + * All references to the same CacheManager name (or the same default) in the + * same ClassLoader space will share the specified CacheManager then. + * @see #setCacheManagerName + * #see #setShared + * @see net.sf.ehcache.CacheManager#getCacheManager(String) + * @see net.sf.ehcache.CacheManager#CacheManager() + */ + public void setAcceptExisting(boolean acceptExisting) { + this.acceptExisting = acceptExisting; + } + + /** + * Set whether the EhCache CacheManager should be shared (as a singleton at the + * ClassLoader level) or independent (typically local within the application). + * Default is "false", creating an independent local instance. + *

    NOTE: This feature allows for sharing this EhCacheManagerFactoryBean's + * CacheManager with any code calling CacheManager.create() in the same + * ClassLoader space, with no need to agree on a specific CacheManager name. + * However, it only supports a single EhCacheManagerFactoryBean involved which will + * control the lifecycle of the underlying CacheManager (in particular, its shutdown). + *

    This flag overrides {@link #setAcceptExisting "acceptExisting"} if both are set, + * since it indicates the 'stronger' mode of sharing. + * @see #setCacheManagerName + * @see #setAcceptExisting + * @see net.sf.ehcache.CacheManager#create() + * @see net.sf.ehcache.CacheManager#CacheManager() + */ + public void setShared(boolean shared) { + this.shared = shared; + } + + + @Override + public void afterPropertiesSet() throws CacheException { + if (logger.isInfoEnabled()) { + logger.info("Initializing EhCache CacheManager" + + (this.cacheManagerName != null ? " '" + this.cacheManagerName + "'" : "")); + } + + Configuration configuration = (this.configLocation != null ? + EhCacheManagerUtils.parseConfiguration(this.configLocation) : ConfigurationFactory.parseConfiguration()); + if (this.cacheManagerName != null) { + configuration.setName(this.cacheManagerName); + } + + if (this.shared) { + // Old-school EhCache singleton sharing... + // No way to find out whether we actually created a new CacheManager + // or just received an existing singleton reference. + this.cacheManager = CacheManager.create(configuration); + } + else if (this.acceptExisting) { + // EhCache 2.5+: Reusing an existing CacheManager of the same name. + // Basically the same code as in CacheManager.getInstance(String), + // just storing whether we're dealing with an existing instance. + synchronized (CacheManager.class) { + this.cacheManager = CacheManager.getCacheManager(this.cacheManagerName); + if (this.cacheManager == null) { + this.cacheManager = new CacheManager(configuration); + } + else { + this.locallyManaged = false; + } + } + } + else { + // Throwing an exception if a CacheManager of the same name exists already... + this.cacheManager = new CacheManager(configuration); + } + } + + + @Override + @Nullable + public CacheManager getObject() { + return this.cacheManager; + } + + @Override + public Class getObjectType() { + return (this.cacheManager != null ? this.cacheManager.getClass() : CacheManager.class); + } + + @Override + public boolean isSingleton() { + return true; + } + + + @Override + public void destroy() { + if (this.cacheManager != null && this.locallyManaged) { + if (logger.isInfoEnabled()) { + logger.info("Shutting down EhCache CacheManager" + + (this.cacheManagerName != null ? " '" + this.cacheManagerName + "'" : "")); + } + this.cacheManager.shutdown(); + } + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheManagerUtils.java b/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheManagerUtils.java new file mode 100644 index 0000000..7d6654a --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheManagerUtils.java @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.ehcache; + +import java.io.IOException; +import java.io.InputStream; + +import net.sf.ehcache.CacheException; +import net.sf.ehcache.CacheManager; +import net.sf.ehcache.config.Configuration; +import net.sf.ehcache.config.ConfigurationFactory; + +import org.springframework.core.io.Resource; + +/** + * Convenient builder methods for EhCache 2.5+ {@link CacheManager} setup, + * providing easy programmatic bootstrapping from a Spring-provided resource. + * This is primarily intended for use within {@code @Bean} methods in a + * Spring configuration class. + * + *

    These methods are a simple alternative to custom {@link CacheManager} setup + * code. For any advanced purposes, consider using {@link #parseConfiguration}, + * customizing the configuration object, and then calling the + * {@link CacheManager#CacheManager(Configuration)} constructor. + * + * @author Juergen Hoeller + * @since 4.1 + */ +public abstract class EhCacheManagerUtils { + + /** + * Build an EhCache {@link CacheManager} from the default configuration. + *

    The CacheManager will be configured from "ehcache.xml" in the root of the class path + * (that is, default EhCache initialization - as defined in the EhCache docs - will apply). + * If no configuration file can be found, a fail-safe fallback configuration will be used. + * @return the new EhCache CacheManager + * @throws CacheException in case of configuration parsing failure + */ + public static CacheManager buildCacheManager() throws CacheException { + return new CacheManager(ConfigurationFactory.parseConfiguration()); + } + + /** + * Build an EhCache {@link CacheManager} from the default configuration. + *

    The CacheManager will be configured from "ehcache.xml" in the root of the class path + * (that is, default EhCache initialization - as defined in the EhCache docs - will apply). + * If no configuration file can be found, a fail-safe fallback configuration will be used. + * @param name the desired name of the cache manager + * @return the new EhCache CacheManager + * @throws CacheException in case of configuration parsing failure + */ + public static CacheManager buildCacheManager(String name) throws CacheException { + Configuration configuration = ConfigurationFactory.parseConfiguration(); + configuration.setName(name); + return new CacheManager(configuration); + } + + /** + * Build an EhCache {@link CacheManager} from the given configuration resource. + * @param configLocation the location of the configuration file (as a Spring resource) + * @return the new EhCache CacheManager + * @throws CacheException in case of configuration parsing failure + */ + public static CacheManager buildCacheManager(Resource configLocation) throws CacheException { + return new CacheManager(parseConfiguration(configLocation)); + } + + /** + * Build an EhCache {@link CacheManager} from the given configuration resource. + * @param name the desired name of the cache manager + * @param configLocation the location of the configuration file (as a Spring resource) + * @return the new EhCache CacheManager + * @throws CacheException in case of configuration parsing failure + */ + public static CacheManager buildCacheManager(String name, Resource configLocation) throws CacheException { + Configuration configuration = parseConfiguration(configLocation); + configuration.setName(name); + return new CacheManager(configuration); + } + + /** + * Parse EhCache configuration from the given resource, for further use with + * custom {@link CacheManager} creation. + * @param configLocation the location of the configuration file (as a Spring resource) + * @return the EhCache Configuration handle + * @throws CacheException in case of configuration parsing failure + * @see CacheManager#CacheManager(Configuration) + * @see CacheManager#create(Configuration) + */ + public static Configuration parseConfiguration(Resource configLocation) throws CacheException { + InputStream is = null; + try { + is = configLocation.getInputStream(); + return ConfigurationFactory.parseConfiguration(is); + } + catch (IOException ex) { + throw new CacheException("Failed to parse EhCache configuration resource", ex); + } + finally { + if (is != null) { + try { + is.close(); + } + catch (IOException ex) { + // ignore + } + } + } + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/ehcache/package-info.java b/spring-context-support/src/main/java/org/springframework/cache/ehcache/package-info.java new file mode 100644 index 0000000..d786a80 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/ehcache/package-info.java @@ -0,0 +1,17 @@ +/** + * Support classes for the open source cache + * EhCache 2.x, + * allowing to set up an EhCache CacheManager and Caches + * as beans in a Spring context. + * + *

    Note: EhCache 3.x lives in a different package namespace + * and is not covered by the traditional support classes here. + * Instead, consider using it through JCache (JSR-107), with + * Spring's support in {@code org.springframework.cache.jcache}. + */ +@NonNullApi +@NonNullFields +package org.springframework.cache.ehcache; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCache.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCache.java new file mode 100644 index 0000000..84d2e3f --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCache.java @@ -0,0 +1,153 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache; + +import java.util.concurrent.Callable; + +import javax.cache.Cache; +import javax.cache.processor.EntryProcessor; +import javax.cache.processor.EntryProcessorException; +import javax.cache.processor.MutableEntry; + +import org.springframework.cache.support.AbstractValueAdaptingCache; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * {@link org.springframework.cache.Cache} implementation on top of a + * {@link Cache javax.cache.Cache} instance. + * + *

    Note: This class has been updated for JCache 1.0, as of Spring 4.0. + * + * @author Juergen Hoeller + * @author Stephane Nicoll + * @since 3.2 + * @see JCacheCacheManager + */ +public class JCacheCache extends AbstractValueAdaptingCache { + + private final Cache cache; + + + /** + * Create a {@code JCacheCache} instance. + * @param jcache backing JCache Cache instance + */ + public JCacheCache(Cache jcache) { + this(jcache, true); + } + + /** + * Create a {@code JCacheCache} instance. + * @param jcache backing JCache Cache instance + * @param allowNullValues whether to accept and convert null values for this cache + */ + public JCacheCache(Cache jcache, boolean allowNullValues) { + super(allowNullValues); + Assert.notNull(jcache, "Cache must not be null"); + this.cache = jcache; + } + + + @Override + public final String getName() { + return this.cache.getName(); + } + + @Override + public final Cache getNativeCache() { + return this.cache; + } + + @Override + @Nullable + protected Object lookup(Object key) { + return this.cache.get(key); + } + + @Override + @Nullable + public T get(Object key, Callable valueLoader) { + try { + return this.cache.invoke(key, new ValueLoaderEntryProcessor(), valueLoader); + } + catch (EntryProcessorException ex) { + throw new ValueRetrievalException(key, valueLoader, ex.getCause()); + } + } + + @Override + public void put(Object key, @Nullable Object value) { + this.cache.put(key, toStoreValue(value)); + } + + @Override + @Nullable + public ValueWrapper putIfAbsent(Object key, @Nullable Object value) { + boolean set = this.cache.putIfAbsent(key, toStoreValue(value)); + return (set ? null : get(key)); + } + + @Override + public void evict(Object key) { + this.cache.remove(key); + } + + @Override + public boolean evictIfPresent(Object key) { + return this.cache.remove(key); + } + + @Override + public void clear() { + this.cache.removeAll(); + } + + @Override + public boolean invalidate() { + boolean notEmpty = this.cache.iterator().hasNext(); + this.cache.removeAll(); + return notEmpty; + } + + + private class ValueLoaderEntryProcessor implements EntryProcessor { + + @SuppressWarnings("unchecked") + @Override + @Nullable + public T process(MutableEntry entry, Object... arguments) throws EntryProcessorException { + Callable valueLoader = (Callable) arguments[0]; + if (entry.exists()) { + return (T) fromStoreValue(entry.getValue()); + } + else { + T value; + try { + value = valueLoader.call(); + } + catch (Exception ex) { + throw new EntryProcessorException("Value loader '" + valueLoader + "' failed " + + "to compute value for key '" + entry.getKey() + "'", ex); + } + entry.setValue(toStoreValue(value)); + return value; + } + } + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCacheManager.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCacheManager.java new file mode 100644 index 0000000..e4feb09 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCacheManager.java @@ -0,0 +1,136 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache; + +import java.util.Collection; +import java.util.LinkedHashSet; + +import javax.cache.CacheManager; +import javax.cache.Caching; + +import org.springframework.cache.Cache; +import org.springframework.cache.transaction.AbstractTransactionSupportingCacheManager; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * {@link org.springframework.cache.CacheManager} implementation + * backed by a JCache {@link CacheManager javax.cache.CacheManager}. + * + *

    Note: This class has been updated for JCache 1.0, as of Spring 4.0. + * + * @author Juergen Hoeller + * @author Stephane Nicoll + * @since 3.2 + * @see JCacheCache + */ +public class JCacheCacheManager extends AbstractTransactionSupportingCacheManager { + + @Nullable + private CacheManager cacheManager; + + private boolean allowNullValues = true; + + + /** + * Create a new {@code JCacheCacheManager} without a backing JCache + * {@link CacheManager javax.cache.CacheManager}. + *

    The backing JCache {@code javax.cache.CacheManager} can be set via the + * {@link #setCacheManager} bean property. + */ + public JCacheCacheManager() { + } + + /** + * Create a new {@code JCacheCacheManager} for the given backing JCache + * {@link CacheManager javax.cache.CacheManager}. + * @param cacheManager the backing JCache {@code javax.cache.CacheManager} + */ + public JCacheCacheManager(CacheManager cacheManager) { + this.cacheManager = cacheManager; + } + + + /** + * Set the backing JCache {@link CacheManager javax.cache.CacheManager}. + */ + public void setCacheManager(@Nullable CacheManager cacheManager) { + this.cacheManager = cacheManager; + } + + /** + * Return the backing JCache {@link CacheManager javax.cache.CacheManager}. + */ + @Nullable + public CacheManager getCacheManager() { + return this.cacheManager; + } + + /** + * Specify whether to accept and convert {@code null} values for all caches + * in this cache manager. + *

    Default is "true", despite JSR-107 itself not supporting {@code null} values. + * An internal holder object will be used to store user-level {@code null}s. + */ + public void setAllowNullValues(boolean allowNullValues) { + this.allowNullValues = allowNullValues; + } + + /** + * Return whether this cache manager accepts and converts {@code null} values + * for all of its caches. + */ + public boolean isAllowNullValues() { + return this.allowNullValues; + } + + @Override + public void afterPropertiesSet() { + if (getCacheManager() == null) { + setCacheManager(Caching.getCachingProvider().getCacheManager()); + } + super.afterPropertiesSet(); + } + + + @Override + protected Collection loadCaches() { + CacheManager cacheManager = getCacheManager(); + Assert.state(cacheManager != null, "No CacheManager set"); + + Collection caches = new LinkedHashSet<>(); + for (String cacheName : cacheManager.getCacheNames()) { + javax.cache.Cache jcache = cacheManager.getCache(cacheName); + caches.add(new JCacheCache(jcache, isAllowNullValues())); + } + return caches; + } + + @Override + protected Cache getMissingCache(String name) { + CacheManager cacheManager = getCacheManager(); + Assert.state(cacheManager != null, "No CacheManager set"); + + // Check the JCache cache again (in case the cache was added at runtime) + javax.cache.Cache jcache = cacheManager.getCache(name); + if (jcache != null) { + return new JCacheCache(jcache, isAllowNullValues()); + } + return null; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheManagerFactoryBean.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheManagerFactoryBean.java new file mode 100644 index 0000000..da3a2e1 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheManagerFactoryBean.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache; + +import java.net.URI; +import java.util.Properties; + +import javax.cache.CacheManager; +import javax.cache.Caching; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.lang.Nullable; + +/** + * {@link FactoryBean} for a JCache {@link CacheManager javax.cache.CacheManager}, + * obtaining a pre-defined {@code CacheManager} by name through the standard + * JCache {@link Caching javax.cache.Caching} class. + * + *

    Note: This class has been updated for JCache 1.0, as of Spring 4.0. + * + * @author Juergen Hoeller + * @since 3.2 + * @see javax.cache.Caching#getCachingProvider() + * @see javax.cache.spi.CachingProvider#getCacheManager() + */ +public class JCacheManagerFactoryBean + implements FactoryBean, BeanClassLoaderAware, InitializingBean, DisposableBean { + + @Nullable + private URI cacheManagerUri; + + @Nullable + private Properties cacheManagerProperties; + + @Nullable + private ClassLoader beanClassLoader; + + @Nullable + private CacheManager cacheManager; + + + /** + * Specify the URI for the desired {@code CacheManager}. + *

    Default is {@code null} (i.e. JCache's default). + */ + public void setCacheManagerUri(@Nullable URI cacheManagerUri) { + this.cacheManagerUri = cacheManagerUri; + } + + /** + * Specify properties for the to-be-created {@code CacheManager}. + *

    Default is {@code null} (i.e. no special properties to apply). + * @see javax.cache.spi.CachingProvider#getCacheManager(URI, ClassLoader, Properties) + */ + public void setCacheManagerProperties(@Nullable Properties cacheManagerProperties) { + this.cacheManagerProperties = cacheManagerProperties; + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + @Override + public void afterPropertiesSet() { + this.cacheManager = Caching.getCachingProvider().getCacheManager( + this.cacheManagerUri, this.beanClassLoader, this.cacheManagerProperties); + } + + + @Override + @Nullable + public CacheManager getObject() { + return this.cacheManager; + } + + @Override + public Class getObjectType() { + return (this.cacheManager != null ? this.cacheManager.getClass() : CacheManager.class); + } + + @Override + public boolean isSingleton() { + return true; + } + + + @Override + public void destroy() { + if (this.cacheManager != null) { + this.cacheManager.close(); + } + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/AbstractJCacheConfiguration.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/AbstractJCacheConfiguration.java new file mode 100644 index 0000000..1cd5ce6 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/AbstractJCacheConfiguration.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.config; + +import java.util.function.Supplier; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.cache.annotation.AbstractCachingConfiguration; +import org.springframework.cache.annotation.CachingConfigurer; +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.jcache.interceptor.DefaultJCacheOperationSource; +import org.springframework.cache.jcache.interceptor.JCacheOperationSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.lang.Nullable; + +/** + * Abstract JSR-107 specific {@code @Configuration} class providing common + * structure for enabling JSR-107 annotation-driven cache management capability. + * + * @author Stephane Nicoll + * @author Juergen Hoeller + * @since 4.1 + * @see JCacheConfigurer + */ +@Configuration(proxyBeanMethods = false) +public abstract class AbstractJCacheConfiguration extends AbstractCachingConfiguration { + + @Nullable + protected Supplier exceptionCacheResolver; + + + @Override + protected void useCachingConfigurer(CachingConfigurer config) { + super.useCachingConfigurer(config); + if (config instanceof JCacheConfigurer) { + this.exceptionCacheResolver = ((JCacheConfigurer) config)::exceptionCacheResolver; + } + } + + @Bean(name = "jCacheOperationSource") + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public JCacheOperationSource cacheOperationSource() { + return new DefaultJCacheOperationSource( + this.cacheManager, this.cacheResolver, this.exceptionCacheResolver, this.keyGenerator); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurer.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurer.java new file mode 100644 index 0000000..989e720 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurer.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.config; + +import org.springframework.cache.annotation.CachingConfigurer; +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.lang.Nullable; + +/** + * Extension of {@link CachingConfigurer} for the JSR-107 implementation. + * + *

    To be implemented by classes annotated with + * {@link org.springframework.cache.annotation.EnableCaching} that wish + * or need to specify explicitly how exception caches are resolved for + * annotation-driven cache management. Consider extending {@link JCacheConfigurerSupport}, + * which provides a stub implementation of all interface methods. + * + *

    See {@link org.springframework.cache.annotation.EnableCaching} for + * general examples and context; see {@link #exceptionCacheResolver()} for + * detailed instructions. + * + * @author Stephane Nicoll + * @since 4.1 + * @see CachingConfigurer + * @see JCacheConfigurerSupport + * @see org.springframework.cache.annotation.EnableCaching + */ +public interface JCacheConfigurer extends CachingConfigurer { + + /** + * Return the {@link CacheResolver} bean to use to resolve exception caches for + * annotation-driven cache management. Implementations must explicitly declare + * {@link org.springframework.context.annotation.Bean @Bean}, e.g. + *

    +	 * @Configuration
    +	 * @EnableCaching
    +	 * public class AppConfig extends JCacheConfigurerSupport {
    +	 *     @Bean // important!
    +	 *     @Override
    +	 *     public CacheResolver exceptionCacheResolver() {
    +	 *         // configure and return CacheResolver instance
    +	 *     }
    +	 *     // ...
    +	 * }
    +	 * 
    + * See {@link org.springframework.cache.annotation.EnableCaching} for more complete examples. + */ + @Nullable + CacheResolver exceptionCacheResolver(); + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurerSupport.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurerSupport.java new file mode 100644 index 0000000..e36c4fb --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurerSupport.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.config; + +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.lang.Nullable; + +/** + * An extension of {@link CachingConfigurerSupport} that also implements + * {@link JCacheConfigurer}. + * + *

    Users of JSR-107 annotations may extend from this class rather than + * implementing from {@link JCacheConfigurer} directly. + * + * @author Stephane Nicoll + * @since 4.1 + * @see JCacheConfigurer + * @see CachingConfigurerSupport + */ +public class JCacheConfigurerSupport extends CachingConfigurerSupport implements JCacheConfigurer { + + @Override + @Nullable + public CacheResolver exceptionCacheResolver() { + return null; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/ProxyJCacheConfiguration.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/ProxyJCacheConfiguration.java new file mode 100644 index 0000000..c999000 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/ProxyJCacheConfiguration.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.config; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.cache.config.CacheManagementConfigUtils; +import org.springframework.cache.jcache.interceptor.BeanFactoryJCacheOperationSourceAdvisor; +import org.springframework.cache.jcache.interceptor.JCacheInterceptor; +import org.springframework.cache.jcache.interceptor.JCacheOperationSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; + +/** + * {@code @Configuration} class that registers the Spring infrastructure beans necessary + * to enable proxy-based annotation-driven JSR-107 cache management. + * + *

    Can safely be used alongside Spring's caching support. + * + * @author Stephane Nicoll + * @author Juergen Hoeller + * @since 4.1 + * @see org.springframework.cache.annotation.EnableCaching + * @see org.springframework.cache.annotation.CachingConfigurationSelector + */ +@Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +public class ProxyJCacheConfiguration extends AbstractJCacheConfiguration { + + @Bean(name = CacheManagementConfigUtils.JCACHE_ADVISOR_BEAN_NAME) + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public BeanFactoryJCacheOperationSourceAdvisor cacheAdvisor( + JCacheOperationSource jCacheOperationSource, JCacheInterceptor jCacheInterceptor) { + + BeanFactoryJCacheOperationSourceAdvisor advisor = new BeanFactoryJCacheOperationSourceAdvisor(); + advisor.setCacheOperationSource(jCacheOperationSource); + advisor.setAdvice(jCacheInterceptor); + if (this.enableCaching != null) { + advisor.setOrder(this.enableCaching.getNumber("order")); + } + return advisor; + } + + @Bean(name = "jCacheInterceptor") + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public JCacheInterceptor cacheInterceptor(JCacheOperationSource jCacheOperationSource) { + JCacheInterceptor interceptor = new JCacheInterceptor(this.errorHandler); + interceptor.setCacheOperationSource(jCacheOperationSource); + return interceptor; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/package-info.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/package-info.java new file mode 100644 index 0000000..9bc89f8 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/package-info.java @@ -0,0 +1,14 @@ +/** + * Support package for declarative JSR-107 caching configuration. Used + * by the regular Spring's caching configuration when it detects the + * JSR-107 API and Spring's JCache implementation. + * + *

    Provide an extension of the {@code CachingConfigurer} that exposes + * the exception cache resolver to use, see {@code JCacheConfigurer}. + */ +@NonNullApi +@NonNullFields +package org.springframework.cache.jcache.config; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractCacheInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractCacheInterceptor.java new file mode 100644 index 0000000..06025fc --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractCacheInterceptor.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.util.Collection; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cache.Cache; +import org.springframework.cache.interceptor.AbstractCacheInvoker; +import org.springframework.cache.interceptor.CacheErrorHandler; +import org.springframework.cache.interceptor.CacheOperationInvocationContext; +import org.springframework.cache.interceptor.CacheOperationInvoker; +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; + +/** + * A base interceptor for JSR-107 cache annotations. + * + * @author Stephane Nicoll + * @since 4.1 + * @param the operation type + * @param the annotation type + */ +@SuppressWarnings("serial") +abstract class AbstractCacheInterceptor, A extends Annotation> + extends AbstractCacheInvoker implements Serializable { + + protected final Log logger = LogFactory.getLog(getClass()); + + + protected AbstractCacheInterceptor(CacheErrorHandler errorHandler) { + super(errorHandler); + } + + + @Nullable + protected abstract Object invoke(CacheOperationInvocationContext context, CacheOperationInvoker invoker) + throws Throwable; + + + /** + * Resolve the cache to use. + * @param context the invocation context + * @return the cache to use (never {@code null}) + */ + protected Cache resolveCache(CacheOperationInvocationContext context) { + Collection caches = context.getOperation().getCacheResolver().resolveCaches(context); + Cache cache = extractFrom(caches); + if (cache == null) { + throw new IllegalStateException("Cache could not have been resolved for " + context.getOperation()); + } + return cache; + } + + /** + * Convert the collection of caches in a single expected element. + *

    Throw an {@link IllegalStateException} if the collection holds more than one element + * @return the single element, or {@code null} if the collection is empty + */ + @Nullable + static Cache extractFrom(Collection caches) { + if (CollectionUtils.isEmpty(caches)) { + return null; + } + else if (caches.size() == 1) { + return caches.iterator().next(); + } + else { + throw new IllegalStateException("Unsupported cache resolution result " + caches + + ": JSR-107 only supports a single cache."); + } + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java new file mode 100644 index 0000000..3edf2a2 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aop.support.AopUtils; +import org.springframework.core.MethodClassKey; +import org.springframework.lang.Nullable; + +/** + * Abstract implementation of {@link JCacheOperationSource} that caches attributes + * for methods and implements a fallback policy: 1. specific target method; + * 2. declaring method. + * + *

    This implementation caches attributes by method after they are first used. + * + * @author Stephane Nicoll + * @author Juergen Hoeller + * @since 4.1 + * @see org.springframework.cache.interceptor.AbstractFallbackCacheOperationSource + */ +public abstract class AbstractFallbackJCacheOperationSource implements JCacheOperationSource { + + /** + * Canonical value held in cache to indicate no caching attribute was + * found for this method and we don't need to look again. + */ + private static final Object NULL_CACHING_ATTRIBUTE = new Object(); + + + protected final Log logger = LogFactory.getLog(getClass()); + + private final Map cache = new ConcurrentHashMap<>(1024); + + + @Override + public JCacheOperation getCacheOperation(Method method, @Nullable Class targetClass) { + MethodClassKey cacheKey = new MethodClassKey(method, targetClass); + Object cached = this.cache.get(cacheKey); + + if (cached != null) { + return (cached != NULL_CACHING_ATTRIBUTE ? (JCacheOperation) cached : null); + } + else { + JCacheOperation operation = computeCacheOperation(method, targetClass); + if (operation != null) { + if (logger.isDebugEnabled()) { + logger.debug("Adding cacheable method '" + method.getName() + "' with operation: " + operation); + } + this.cache.put(cacheKey, operation); + } + else { + this.cache.put(cacheKey, NULL_CACHING_ATTRIBUTE); + } + return operation; + } + } + + @Nullable + private JCacheOperation computeCacheOperation(Method method, @Nullable Class targetClass) { + // Don't allow no-public methods as required. + if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { + return null; + } + + // The method may be on an interface, but we need attributes from the target class. + // If the target class is null, the method will be unchanged. + Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); + + // First try is the method in the target class. + JCacheOperation operation = findCacheOperation(specificMethod, targetClass); + if (operation != null) { + return operation; + } + if (specificMethod != method) { + // Fallback is to look at the original method. + operation = findCacheOperation(method, targetClass); + if (operation != null) { + return operation; + } + } + return null; + } + + + /** + * Subclasses need to implement this to return the caching operation + * for the given method, if any. + * @param method the method to retrieve the operation for + * @param targetType the target class + * @return the cache operation associated with this method + * (or {@code null} if none) + */ + @Nullable + protected abstract JCacheOperation findCacheOperation(Method method, @Nullable Class targetType); + + /** + * Should only public methods be allowed to have caching semantics? + *

    The default implementation returns {@code false}. + */ + protected boolean allowPublicMethodsOnly() { + return false; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractJCacheKeyOperation.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractJCacheKeyOperation.java new file mode 100644 index 0000000..e3ecd6f --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractJCacheKeyOperation.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; + +import javax.cache.annotation.CacheInvocationParameter; +import javax.cache.annotation.CacheMethodDetails; + +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.interceptor.KeyGenerator; + +/** + * A base {@link JCacheOperation} that operates with a key. + * + * @author Stephane Nicoll + * @since 4.1 + * @param the annotation type + */ +abstract class AbstractJCacheKeyOperation extends AbstractJCacheOperation { + + private final KeyGenerator keyGenerator; + + private final List keyParameterDetails; + + + /** + * Create a new instance. + * @param methodDetails the {@link CacheMethodDetails} related to the cached method + * @param cacheResolver the cache resolver to resolve regular caches + * @param keyGenerator the key generator to compute cache keys + */ + protected AbstractJCacheKeyOperation(CacheMethodDetails methodDetails, + CacheResolver cacheResolver, KeyGenerator keyGenerator) { + + super(methodDetails, cacheResolver); + this.keyGenerator = keyGenerator; + this.keyParameterDetails = initializeKeyParameterDetails(this.allParameterDetails); + } + + + /** + * Return the {@link KeyGenerator} to use to compute cache keys. + */ + public KeyGenerator getKeyGenerator() { + return this.keyGenerator; + } + + /** + * Return the {@link CacheInvocationParameter} for the parameters that are to be + * used to compute the key. + *

    Per the spec, if some method parameters are annotated with + * {@link javax.cache.annotation.CacheKey}, only those parameters should be part + * of the key. If none are annotated, all parameters except the parameter annotated + * with {@link javax.cache.annotation.CacheValue} should be part of the key. + *

    The method arguments must match the signature of the related method invocation + * @param values the parameters value for a particular invocation + * @return the {@link CacheInvocationParameter} instances for the parameters to be + * used to compute the key + */ + public CacheInvocationParameter[] getKeyParameters(Object... values) { + List result = new ArrayList<>(); + for (CacheParameterDetail keyParameterDetail : this.keyParameterDetails) { + int parameterPosition = keyParameterDetail.getParameterPosition(); + if (parameterPosition >= values.length) { + throw new IllegalStateException("Values mismatch, key parameter at position " + + parameterPosition + " cannot be matched against " + values.length + " value(s)"); + } + result.add(keyParameterDetail.toCacheInvocationParameter(values[parameterPosition])); + } + return result.toArray(new CacheInvocationParameter[0]); + } + + + private static List initializeKeyParameterDetails(List allParameters) { + List all = new ArrayList<>(); + List annotated = new ArrayList<>(); + for (CacheParameterDetail allParameter : allParameters) { + if (!allParameter.isValue()) { + all.add(allParameter); + } + if (allParameter.isKey()) { + annotated.add(allParameter); + } + } + return (annotated.isEmpty() ? all : annotated); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractJCacheOperation.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractJCacheOperation.java new file mode 100644 index 0000000..d1771b1 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractJCacheOperation.java @@ -0,0 +1,244 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import javax.cache.annotation.CacheInvocationParameter; +import javax.cache.annotation.CacheKey; +import javax.cache.annotation.CacheMethodDetails; +import javax.cache.annotation.CacheValue; + +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.util.Assert; +import org.springframework.util.ExceptionTypeFilter; + +/** + * A base {@link JCacheOperation} implementation. + * + * @author Stephane Nicoll + * @since 4.1 + * @param the annotation type + */ +abstract class AbstractJCacheOperation implements JCacheOperation { + + private final CacheMethodDetails methodDetails; + + private final CacheResolver cacheResolver; + + protected final List allParameterDetails; + + + /** + * Construct a new {@code AbstractJCacheOperation}. + * @param methodDetails the {@link CacheMethodDetails} related to the cached method + * @param cacheResolver the cache resolver to resolve regular caches + */ + protected AbstractJCacheOperation(CacheMethodDetails methodDetails, CacheResolver cacheResolver) { + Assert.notNull(methodDetails, "CacheMethodDetails must not be null"); + Assert.notNull(cacheResolver, "CacheResolver must not be null"); + this.methodDetails = methodDetails; + this.cacheResolver = cacheResolver; + this.allParameterDetails = initializeAllParameterDetails(methodDetails.getMethod()); + } + + private static List initializeAllParameterDetails(Method method) { + int parameterCount = method.getParameterCount(); + List result = new ArrayList<>(parameterCount); + for (int i = 0; i < parameterCount; i++) { + CacheParameterDetail detail = new CacheParameterDetail(method, i); + result.add(detail); + } + return result; + } + + + @Override + public Method getMethod() { + return this.methodDetails.getMethod(); + } + + @Override + public Set getAnnotations() { + return this.methodDetails.getAnnotations(); + } + + @Override + public A getCacheAnnotation() { + return this.methodDetails.getCacheAnnotation(); + } + + @Override + public String getCacheName() { + return this.methodDetails.getCacheName(); + } + + @Override + public Set getCacheNames() { + return Collections.singleton(getCacheName()); + } + + @Override + public CacheResolver getCacheResolver() { + return this.cacheResolver; + } + + @Override + public CacheInvocationParameter[] getAllParameters(Object... values) { + if (this.allParameterDetails.size() != values.length) { + throw new IllegalStateException("Values mismatch, operation has " + + this.allParameterDetails.size() + " parameter(s) but got " + values.length + " value(s)"); + } + List result = new ArrayList<>(); + for (int i = 0; i < this.allParameterDetails.size(); i++) { + result.add(this.allParameterDetails.get(i).toCacheInvocationParameter(values[i])); + } + return result.toArray(new CacheInvocationParameter[0]); + } + + + /** + * Return the {@link ExceptionTypeFilter} to use to filter exceptions thrown while + * invoking the method. + * @see #createExceptionTypeFilter + */ + public abstract ExceptionTypeFilter getExceptionTypeFilter(); + + /** + * Convenience method for subclasses to create a specific {@code ExceptionTypeFilter}. + * @see #getExceptionTypeFilter() + */ + protected ExceptionTypeFilter createExceptionTypeFilter( + Class[] includes, Class[] excludes) { + + return new ExceptionTypeFilter(Arrays.asList(includes), Arrays.asList(excludes), true); + } + + + @Override + public String toString() { + return getOperationDescription().append("]").toString(); + } + + /** + * Return an identifying description for this caching operation. + *

    Available to subclasses, for inclusion in their {@code toString()} result. + */ + protected StringBuilder getOperationDescription() { + StringBuilder result = new StringBuilder(); + result.append(getClass().getSimpleName()); + result.append("["); + result.append(this.methodDetails); + return result; + } + + + /** + * Details for a single cache parameter. + */ + protected static class CacheParameterDetail { + + private final Class rawType; + + private final Set annotations; + + private final int parameterPosition; + + private final boolean isKey; + + private final boolean isValue; + + public CacheParameterDetail(Method method, int parameterPosition) { + this.rawType = method.getParameterTypes()[parameterPosition]; + this.annotations = new LinkedHashSet<>(); + boolean foundKeyAnnotation = false; + boolean foundValueAnnotation = false; + for (Annotation annotation : method.getParameterAnnotations()[parameterPosition]) { + this.annotations.add(annotation); + if (CacheKey.class.isAssignableFrom(annotation.annotationType())) { + foundKeyAnnotation = true; + } + if (CacheValue.class.isAssignableFrom(annotation.annotationType())) { + foundValueAnnotation = true; + } + } + this.parameterPosition = parameterPosition; + this.isKey = foundKeyAnnotation; + this.isValue = foundValueAnnotation; + } + + public int getParameterPosition() { + return this.parameterPosition; + } + + protected boolean isKey() { + return this.isKey; + } + + protected boolean isValue() { + return this.isValue; + } + + public CacheInvocationParameter toCacheInvocationParameter(Object value) { + return new CacheInvocationParameterImpl(this, value); + } + } + + + /** + * A single cache invocation parameter. + */ + protected static class CacheInvocationParameterImpl implements CacheInvocationParameter { + + private final CacheParameterDetail detail; + + private final Object value; + + public CacheInvocationParameterImpl(CacheParameterDetail detail, Object value) { + this.detail = detail; + this.value = value; + } + + @Override + public Class getRawType() { + return this.detail.rawType; + } + + @Override + public Object getValue() { + return this.value; + } + + @Override + public Set getAnnotations() { + return this.detail.annotations; + } + + @Override + public int getParameterPosition() { + return this.detail.parameterPosition; + } + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractKeyCacheInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractKeyCacheInterceptor.java new file mode 100644 index 0000000..1539505 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractKeyCacheInterceptor.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import java.lang.annotation.Annotation; + +import javax.cache.annotation.CacheKeyInvocationContext; + +import org.springframework.cache.interceptor.CacheErrorHandler; +import org.springframework.cache.interceptor.CacheOperationInvocationContext; +import org.springframework.cache.interceptor.KeyGenerator; + +/** + * A base interceptor for JSR-107 key-based cache annotations. + * + * @author Stephane Nicoll + * @since 4.1 + * @param the operation type + * @param the annotation type + */ +@SuppressWarnings("serial") +abstract class AbstractKeyCacheInterceptor, A extends Annotation> + extends AbstractCacheInterceptor { + + protected AbstractKeyCacheInterceptor(CacheErrorHandler errorHandler) { + super(errorHandler); + } + + + /** + * Generate a key for the specified invocation. + * @param context the context of the invocation + * @return the key to use + */ + protected Object generateKey(CacheOperationInvocationContext context) { + KeyGenerator keyGenerator = context.getOperation().getKeyGenerator(); + Object key = keyGenerator.generate(context.getTarget(), context.getMethod(), context.getArgs()); + if (logger.isTraceEnabled()) { + logger.trace("Computed cache key " + key + " for operation " + context.getOperation()); + } + return key; + } + + /** + * Create a {@link CacheKeyInvocationContext} based on the specified invocation. + * @param context the context of the invocation. + * @return the related {@code CacheKeyInvocationContext} + */ + protected CacheKeyInvocationContext createCacheKeyInvocationContext(CacheOperationInvocationContext context) { + return new DefaultCacheKeyInvocationContext<>(context.getOperation(), context.getTarget(), context.getArgs()); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AnnotationJCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AnnotationJCacheOperationSource.java new file mode 100644 index 0000000..b289b14 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AnnotationJCacheOperationSource.java @@ -0,0 +1,254 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import javax.cache.annotation.CacheDefaults; +import javax.cache.annotation.CacheKeyGenerator; +import javax.cache.annotation.CacheMethodDetails; +import javax.cache.annotation.CachePut; +import javax.cache.annotation.CacheRemove; +import javax.cache.annotation.CacheRemoveAll; +import javax.cache.annotation.CacheResolverFactory; +import javax.cache.annotation.CacheResult; + +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Implementation of the {@link JCacheOperationSource} interface that reads + * the JSR-107 {@link CacheResult}, {@link CachePut}, {@link CacheRemove} and + * {@link CacheRemoveAll} annotations. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public abstract class AnnotationJCacheOperationSource extends AbstractFallbackJCacheOperationSource { + + @Override + protected JCacheOperation findCacheOperation(Method method, @Nullable Class targetType) { + CacheResult cacheResult = method.getAnnotation(CacheResult.class); + CachePut cachePut = method.getAnnotation(CachePut.class); + CacheRemove cacheRemove = method.getAnnotation(CacheRemove.class); + CacheRemoveAll cacheRemoveAll = method.getAnnotation(CacheRemoveAll.class); + + int found = countNonNull(cacheResult, cachePut, cacheRemove, cacheRemoveAll); + if (found == 0) { + return null; + } + if (found > 1) { + throw new IllegalStateException("More than one cache annotation found on '" + method + "'"); + } + + CacheDefaults defaults = getCacheDefaults(method, targetType); + if (cacheResult != null) { + return createCacheResultOperation(method, defaults, cacheResult); + } + else if (cachePut != null) { + return createCachePutOperation(method, defaults, cachePut); + } + else if (cacheRemove != null) { + return createCacheRemoveOperation(method, defaults, cacheRemove); + } + else { + return createCacheRemoveAllOperation(method, defaults, cacheRemoveAll); + } + } + + @Nullable + protected CacheDefaults getCacheDefaults(Method method, @Nullable Class targetType) { + CacheDefaults annotation = method.getDeclaringClass().getAnnotation(CacheDefaults.class); + if (annotation != null) { + return annotation; + } + return (targetType != null ? targetType.getAnnotation(CacheDefaults.class) : null); + } + + protected CacheResultOperation createCacheResultOperation(Method method, @Nullable CacheDefaults defaults, CacheResult ann) { + String cacheName = determineCacheName(method, defaults, ann.cacheName()); + CacheResolverFactory cacheResolverFactory = + determineCacheResolverFactory(defaults, ann.cacheResolverFactory()); + KeyGenerator keyGenerator = determineKeyGenerator(defaults, ann.cacheKeyGenerator()); + + CacheMethodDetails methodDetails = createMethodDetails(method, ann, cacheName); + + CacheResolver cacheResolver = getCacheResolver(cacheResolverFactory, methodDetails); + CacheResolver exceptionCacheResolver = null; + final String exceptionCacheName = ann.exceptionCacheName(); + if (StringUtils.hasText(exceptionCacheName)) { + exceptionCacheResolver = getExceptionCacheResolver(cacheResolverFactory, methodDetails); + } + + return new CacheResultOperation(methodDetails, cacheResolver, keyGenerator, exceptionCacheResolver); + } + + protected CachePutOperation createCachePutOperation(Method method, @Nullable CacheDefaults defaults, CachePut ann) { + String cacheName = determineCacheName(method, defaults, ann.cacheName()); + CacheResolverFactory cacheResolverFactory = + determineCacheResolverFactory(defaults, ann.cacheResolverFactory()); + KeyGenerator keyGenerator = determineKeyGenerator(defaults, ann.cacheKeyGenerator()); + + CacheMethodDetails methodDetails = createMethodDetails(method, ann, cacheName); + CacheResolver cacheResolver = getCacheResolver(cacheResolverFactory, methodDetails); + return new CachePutOperation(methodDetails, cacheResolver, keyGenerator); + } + + protected CacheRemoveOperation createCacheRemoveOperation(Method method, @Nullable CacheDefaults defaults, CacheRemove ann) { + String cacheName = determineCacheName(method, defaults, ann.cacheName()); + CacheResolverFactory cacheResolverFactory = + determineCacheResolverFactory(defaults, ann.cacheResolverFactory()); + KeyGenerator keyGenerator = determineKeyGenerator(defaults, ann.cacheKeyGenerator()); + + CacheMethodDetails methodDetails = createMethodDetails(method, ann, cacheName); + CacheResolver cacheResolver = getCacheResolver(cacheResolverFactory, methodDetails); + return new CacheRemoveOperation(methodDetails, cacheResolver, keyGenerator); + } + + protected CacheRemoveAllOperation createCacheRemoveAllOperation(Method method, @Nullable CacheDefaults defaults, CacheRemoveAll ann) { + String cacheName = determineCacheName(method, defaults, ann.cacheName()); + CacheResolverFactory cacheResolverFactory = + determineCacheResolverFactory(defaults, ann.cacheResolverFactory()); + + CacheMethodDetails methodDetails = createMethodDetails(method, ann, cacheName); + CacheResolver cacheResolver = getCacheResolver(cacheResolverFactory, methodDetails); + return new CacheRemoveAllOperation(methodDetails, cacheResolver); + } + + private CacheMethodDetails createMethodDetails(Method method, A annotation, String cacheName) { + return new DefaultCacheMethodDetails<>(method, annotation, cacheName); + } + + protected CacheResolver getCacheResolver( + @Nullable CacheResolverFactory factory, CacheMethodDetails details) { + + if (factory != null) { + javax.cache.annotation.CacheResolver cacheResolver = factory.getCacheResolver(details); + return new CacheResolverAdapter(cacheResolver); + } + else { + return getDefaultCacheResolver(); + } + } + + protected CacheResolver getExceptionCacheResolver( + @Nullable CacheResolverFactory factory, CacheMethodDetails details) { + + if (factory != null) { + javax.cache.annotation.CacheResolver cacheResolver = factory.getExceptionCacheResolver(details); + return new CacheResolverAdapter(cacheResolver); + } + else { + return getDefaultExceptionCacheResolver(); + } + } + + @Nullable + protected CacheResolverFactory determineCacheResolverFactory( + @Nullable CacheDefaults defaults, Class candidate) { + + if (candidate != CacheResolverFactory.class) { + return getBean(candidate); + } + else if (defaults != null && defaults.cacheResolverFactory() != CacheResolverFactory.class) { + return getBean(defaults.cacheResolverFactory()); + } + else { + return null; + } + } + + protected KeyGenerator determineKeyGenerator( + @Nullable CacheDefaults defaults, Class candidate) { + + if (candidate != CacheKeyGenerator.class) { + return new KeyGeneratorAdapter(this, getBean(candidate)); + } + else if (defaults != null && CacheKeyGenerator.class != defaults.cacheKeyGenerator()) { + return new KeyGeneratorAdapter(this, getBean(defaults.cacheKeyGenerator())); + } + else { + return getDefaultKeyGenerator(); + } + } + + protected String determineCacheName(Method method, @Nullable CacheDefaults defaults, String candidate) { + if (StringUtils.hasText(candidate)) { + return candidate; + } + if (defaults != null && StringUtils.hasText(defaults.cacheName())) { + return defaults.cacheName(); + } + return generateDefaultCacheName(method); + } + + /** + * Generate a default cache name for the specified {@link Method}. + * @param method the annotated method + * @return the default cache name, according to JSR-107 + */ + protected String generateDefaultCacheName(Method method) { + Class[] parameterTypes = method.getParameterTypes(); + List parameters = new ArrayList<>(parameterTypes.length); + for (Class parameterType : parameterTypes) { + parameters.add(parameterType.getName()); + } + + return method.getDeclaringClass().getName() + + '.' + method.getName() + + '(' + StringUtils.collectionToCommaDelimitedString(parameters) + ')'; + } + + private int countNonNull(Object... instances) { + int result = 0; + for (Object instance : instances) { + if (instance != null) { + result += 1; + } + } + return result; + } + + + /** + * Locate or create an instance of the specified cache strategy {@code type}. + * @param type the type of the bean to manage + * @return the required bean + */ + protected abstract T getBean(Class type); + + /** + * Return the default {@link CacheResolver} if none is set. + */ + protected abstract CacheResolver getDefaultCacheResolver(); + + /** + * Return the default exception {@link CacheResolver} if none is set. + */ + protected abstract CacheResolver getDefaultExceptionCacheResolver(); + + /** + * Return the default {@link KeyGenerator} if none is set. + */ + protected abstract KeyGenerator getDefaultKeyGenerator(); + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/BeanFactoryJCacheOperationSourceAdvisor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/BeanFactoryJCacheOperationSourceAdvisor.java new file mode 100644 index 0000000..62ae44f --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/BeanFactoryJCacheOperationSourceAdvisor.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import org.springframework.aop.ClassFilter; +import org.springframework.aop.Pointcut; +import org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor; +import org.springframework.lang.Nullable; + +/** + * Advisor driven by a {@link JCacheOperationSource}, used to include a + * cache advice bean for methods that are cacheable. + * + * @author Stephane Nicoll + * @since 4.1 + */ +@SuppressWarnings("serial") +public class BeanFactoryJCacheOperationSourceAdvisor extends AbstractBeanFactoryPointcutAdvisor { + + @Nullable + private JCacheOperationSource cacheOperationSource; + + private final JCacheOperationSourcePointcut pointcut = new JCacheOperationSourcePointcut() { + @Override + protected JCacheOperationSource getCacheOperationSource() { + return cacheOperationSource; + } + }; + + + /** + * Set the cache operation attribute source which is used to find cache + * attributes. This should usually be identical to the source reference + * set on the cache interceptor itself. + */ + public void setCacheOperationSource(JCacheOperationSource cacheOperationSource) { + this.cacheOperationSource = cacheOperationSource; + } + + /** + * Set the {@link org.springframework.aop.ClassFilter} to use for this pointcut. + * Default is {@link org.springframework.aop.ClassFilter#TRUE}. + */ + public void setClassFilter(ClassFilter classFilter) { + this.pointcut.setClassFilter(classFilter); + } + + @Override + public Pointcut getPointcut() { + return this.pointcut; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutInterceptor.java new file mode 100644 index 0000000..d71af32 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutInterceptor.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import javax.cache.annotation.CacheKeyInvocationContext; +import javax.cache.annotation.CachePut; + +import org.springframework.cache.Cache; +import org.springframework.cache.interceptor.CacheErrorHandler; +import org.springframework.cache.interceptor.CacheOperationInvocationContext; +import org.springframework.cache.interceptor.CacheOperationInvoker; + +/** + * Intercept methods annotated with {@link CachePut}. + * + * @author Stephane Nicoll + * @since 4.1 + */ +@SuppressWarnings("serial") +class CachePutInterceptor extends AbstractKeyCacheInterceptor { + + public CachePutInterceptor(CacheErrorHandler errorHandler) { + super(errorHandler); + } + + + @Override + protected Object invoke( + CacheOperationInvocationContext context, CacheOperationInvoker invoker) { + + CachePutOperation operation = context.getOperation(); + CacheKeyInvocationContext invocationContext = createCacheKeyInvocationContext(context); + + boolean earlyPut = operation.isEarlyPut(); + Object value = invocationContext.getValueParameter().getValue(); + if (earlyPut) { + cacheValue(context, value); + } + + try { + Object result = invoker.invoke(); + if (!earlyPut) { + cacheValue(context, value); + } + return result; + } + catch (CacheOperationInvoker.ThrowableWrapper ex) { + Throwable original = ex.getOriginal(); + if (!earlyPut && operation.getExceptionTypeFilter().match(original.getClass())) { + cacheValue(context, value); + } + throw ex; + } + } + + protected void cacheValue(CacheOperationInvocationContext context, Object value) { + Object key = generateKey(context); + Cache cache = resolveCache(context); + doPut(cache, key, value); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutOperation.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutOperation.java new file mode 100644 index 0000000..efbfb65 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutOperation.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import java.lang.reflect.Method; +import java.util.List; + +import javax.cache.annotation.CacheInvocationParameter; +import javax.cache.annotation.CacheMethodDetails; +import javax.cache.annotation.CachePut; + +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.lang.Nullable; +import org.springframework.util.ExceptionTypeFilter; + +/** + * The {@link JCacheOperation} implementation for a {@link CachePut} operation. + * + * @author Stephane Nicoll + * @since 4.1 + * @see CachePut + */ +class CachePutOperation extends AbstractJCacheKeyOperation { + + private final ExceptionTypeFilter exceptionTypeFilter; + + private final CacheParameterDetail valueParameterDetail; + + + public CachePutOperation( + CacheMethodDetails methodDetails, CacheResolver cacheResolver, KeyGenerator keyGenerator) { + + super(methodDetails, cacheResolver, keyGenerator); + + CachePut ann = methodDetails.getCacheAnnotation(); + this.exceptionTypeFilter = createExceptionTypeFilter(ann.cacheFor(), ann.noCacheFor()); + + CacheParameterDetail valueParameterDetail = + initializeValueParameterDetail(methodDetails.getMethod(), this.allParameterDetails); + if (valueParameterDetail == null) { + throw new IllegalArgumentException("No parameter annotated with @CacheValue was found for " + + methodDetails.getMethod()); + } + this.valueParameterDetail = valueParameterDetail; + } + + + @Override + public ExceptionTypeFilter getExceptionTypeFilter() { + return this.exceptionTypeFilter; + } + + /** + * Specify if the cache should be updated before invoking the method. By default, + * the cache is updated after the method invocation. + * @see javax.cache.annotation.CachePut#afterInvocation() + */ + public boolean isEarlyPut() { + return !getCacheAnnotation().afterInvocation(); + } + + /** + * Return the {@link CacheInvocationParameter} for the parameter holding the value + * to cache. + *

    The method arguments must match the signature of the related method invocation + * @param values the parameters value for a particular invocation + * @return the {@link CacheInvocationParameter} instance for the value parameter + */ + public CacheInvocationParameter getValueParameter(Object... values) { + int parameterPosition = this.valueParameterDetail.getParameterPosition(); + if (parameterPosition >= values.length) { + throw new IllegalStateException("Values mismatch, value parameter at position " + + parameterPosition + " cannot be matched against " + values.length + " value(s)"); + } + return this.valueParameterDetail.toCacheInvocationParameter(values[parameterPosition]); + } + + + @Nullable + private static CacheParameterDetail initializeValueParameterDetail( + Method method, List allParameters) { + + CacheParameterDetail result = null; + for (CacheParameterDetail parameter : allParameters) { + if (parameter.isValue()) { + if (result == null) { + result = parameter; + } + else { + throw new IllegalArgumentException("More than one @CacheValue found on " + method + ""); + } + } + } + return result; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllInterceptor.java new file mode 100644 index 0000000..66b583e --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllInterceptor.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import javax.cache.annotation.CacheRemoveAll; + +import org.springframework.cache.Cache; +import org.springframework.cache.interceptor.CacheErrorHandler; +import org.springframework.cache.interceptor.CacheOperationInvocationContext; +import org.springframework.cache.interceptor.CacheOperationInvoker; + +/** + * Intercept methods annotated with {@link CacheRemoveAll}. + * + * @author Stephane Nicoll + * @since 4.1 + */ +@SuppressWarnings("serial") +class CacheRemoveAllInterceptor extends AbstractCacheInterceptor { + + protected CacheRemoveAllInterceptor(CacheErrorHandler errorHandler) { + super(errorHandler); + } + + + @Override + protected Object invoke( + CacheOperationInvocationContext context, CacheOperationInvoker invoker) { + + CacheRemoveAllOperation operation = context.getOperation(); + boolean earlyRemove = operation.isEarlyRemove(); + if (earlyRemove) { + removeAll(context); + } + + try { + Object result = invoker.invoke(); + if (!earlyRemove) { + removeAll(context); + } + return result; + } + catch (CacheOperationInvoker.ThrowableWrapper ex) { + Throwable original = ex.getOriginal(); + if (!earlyRemove && operation.getExceptionTypeFilter().match(original.getClass())) { + removeAll(context); + } + throw ex; + } + } + + protected void removeAll(CacheOperationInvocationContext context) { + Cache cache = resolveCache(context); + if (logger.isTraceEnabled()) { + logger.trace("Invalidating entire cache '" + cache.getName() + "' for operation " + + context.getOperation()); + } + doClear(cache, context.getOperation().isEarlyRemove()); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllOperation.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllOperation.java new file mode 100644 index 0000000..aa57826 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllOperation.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import javax.cache.annotation.CacheMethodDetails; +import javax.cache.annotation.CacheRemoveAll; + +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.util.ExceptionTypeFilter; + +/** + * The {@link JCacheOperation} implementation for a {@link CacheRemoveAll} operation. + * + * @author Stephane Nicoll + * @since 4.1 + * @see CacheRemoveAll + */ +class CacheRemoveAllOperation extends AbstractJCacheOperation { + + private final ExceptionTypeFilter exceptionTypeFilter; + + + public CacheRemoveAllOperation(CacheMethodDetails methodDetails, CacheResolver cacheResolver) { + super(methodDetails, cacheResolver); + CacheRemoveAll ann = methodDetails.getCacheAnnotation(); + this.exceptionTypeFilter = createExceptionTypeFilter(ann.evictFor(), ann.noEvictFor()); + } + + + @Override + public ExceptionTypeFilter getExceptionTypeFilter() { + return this.exceptionTypeFilter; + } + + /** + * Specify if the cache should be cleared before invoking the method. By default, the + * cache is cleared after the method invocation. + * @see javax.cache.annotation.CacheRemoveAll#afterInvocation() + */ + public boolean isEarlyRemove() { + return !getCacheAnnotation().afterInvocation(); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveEntryInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveEntryInterceptor.java new file mode 100644 index 0000000..8485278 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveEntryInterceptor.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import javax.cache.annotation.CacheRemove; + +import org.springframework.cache.Cache; +import org.springframework.cache.interceptor.CacheErrorHandler; +import org.springframework.cache.interceptor.CacheOperationInvocationContext; +import org.springframework.cache.interceptor.CacheOperationInvoker; + +/** + * Intercept methods annotated with {@link CacheRemove}. + * + * @author Stephane Nicoll + * @since 4.1 + */ +@SuppressWarnings("serial") +class CacheRemoveEntryInterceptor extends AbstractKeyCacheInterceptor { + + protected CacheRemoveEntryInterceptor(CacheErrorHandler errorHandler) { + super(errorHandler); + } + + + @Override + protected Object invoke( + CacheOperationInvocationContext context, CacheOperationInvoker invoker) { + + CacheRemoveOperation operation = context.getOperation(); + boolean earlyRemove = operation.isEarlyRemove(); + if (earlyRemove) { + removeValue(context); + } + + try { + Object result = invoker.invoke(); + if (!earlyRemove) { + removeValue(context); + } + return result; + } + catch (CacheOperationInvoker.ThrowableWrapper wrapperException) { + Throwable ex = wrapperException.getOriginal(); + if (!earlyRemove && operation.getExceptionTypeFilter().match(ex.getClass())) { + removeValue(context); + } + throw wrapperException; + } + } + + private void removeValue(CacheOperationInvocationContext context) { + Object key = generateKey(context); + Cache cache = resolveCache(context); + if (logger.isTraceEnabled()) { + logger.trace("Invalidating key [" + key + "] on cache '" + cache.getName() + + "' for operation " + context.getOperation()); + } + doEvict(cache, key, context.getOperation().isEarlyRemove()); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveOperation.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveOperation.java new file mode 100644 index 0000000..5fb9a31 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveOperation.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import javax.cache.annotation.CacheMethodDetails; +import javax.cache.annotation.CacheRemove; + +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.util.ExceptionTypeFilter; + +/** + * The {@link JCacheOperation} implementation for a {@link CacheRemove} operation. + * + * @author Stephane Nicoll + * @since 4.1 + * @see CacheRemove + */ +class CacheRemoveOperation extends AbstractJCacheKeyOperation { + + private final ExceptionTypeFilter exceptionTypeFilter; + + + public CacheRemoveOperation( + CacheMethodDetails methodDetails, CacheResolver cacheResolver, KeyGenerator keyGenerator) { + + super(methodDetails, cacheResolver, keyGenerator); + CacheRemove ann = methodDetails.getCacheAnnotation(); + this.exceptionTypeFilter = createExceptionTypeFilter(ann.evictFor(), ann.noEvictFor()); + } + + + @Override + public ExceptionTypeFilter getExceptionTypeFilter() { + return this.exceptionTypeFilter; + } + + /** + * Specify if the cache entry should be removed before invoking the method. + *

    By default, the cache entry is removed after the method invocation. + * @see javax.cache.annotation.CacheRemove#afterInvocation() + */ + public boolean isEarlyRemove() { + return !getCacheAnnotation().afterInvocation(); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResolverAdapter.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResolverAdapter.java new file mode 100644 index 0000000..88724ad --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResolverAdapter.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import java.util.Collection; +import java.util.Collections; + +import javax.cache.annotation.CacheInvocationContext; + +import org.springframework.cache.Cache; +import org.springframework.cache.interceptor.CacheOperationInvocationContext; +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.jcache.JCacheCache; +import org.springframework.util.Assert; + +/** + * Spring's {@link CacheResolver} implementation that delegates to a standard + * JSR-107 {@link javax.cache.annotation.CacheResolver}. + *

    Used internally to invoke user-based JSR-107 cache resolvers. + * + * @author Stephane Nicoll + * @since 4.1 + */ +class CacheResolverAdapter implements CacheResolver { + + private final javax.cache.annotation.CacheResolver target; + + + /** + * Create a new instance with the JSR-107 cache resolver to invoke. + */ + public CacheResolverAdapter(javax.cache.annotation.CacheResolver target) { + Assert.notNull(target, "JSR-107 CacheResolver is required"); + this.target = target; + } + + + /** + * Return the underlying {@link javax.cache.annotation.CacheResolver} + * that this instance is using. + */ + protected javax.cache.annotation.CacheResolver getTarget() { + return this.target; + } + + @Override + public Collection resolveCaches(CacheOperationInvocationContext context) { + if (!(context instanceof CacheInvocationContext)) { + throw new IllegalStateException("Unexpected context " + context); + } + CacheInvocationContext cacheInvocationContext = (CacheInvocationContext) context; + javax.cache.Cache cache = this.target.resolveCache(cacheInvocationContext); + if (cache == null) { + throw new IllegalStateException("Could not resolve cache for " + context + " using " + this.target); + } + return Collections.singleton(new JCacheCache(cache)); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultInterceptor.java new file mode 100644 index 0000000..ea2f4a0 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultInterceptor.java @@ -0,0 +1,170 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import javax.cache.annotation.CacheResult; + +import org.springframework.cache.Cache; +import org.springframework.cache.interceptor.CacheErrorHandler; +import org.springframework.cache.interceptor.CacheOperationInvocationContext; +import org.springframework.cache.interceptor.CacheOperationInvoker; +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ExceptionTypeFilter; +import org.springframework.util.SerializationUtils; + +/** + * Intercept methods annotated with {@link CacheResult}. + * + * @author Stephane Nicoll + * @since 4.1 + */ +@SuppressWarnings("serial") +class CacheResultInterceptor extends AbstractKeyCacheInterceptor { + + public CacheResultInterceptor(CacheErrorHandler errorHandler) { + super(errorHandler); + } + + + @Override + @Nullable + protected Object invoke( + CacheOperationInvocationContext context, CacheOperationInvoker invoker) { + + CacheResultOperation operation = context.getOperation(); + Object cacheKey = generateKey(context); + + Cache cache = resolveCache(context); + Cache exceptionCache = resolveExceptionCache(context); + + if (!operation.isAlwaysInvoked()) { + Cache.ValueWrapper cachedValue = doGet(cache, cacheKey); + if (cachedValue != null) { + return cachedValue.get(); + } + checkForCachedException(exceptionCache, cacheKey); + } + + try { + Object invocationResult = invoker.invoke(); + doPut(cache, cacheKey, invocationResult); + return invocationResult; + } + catch (CacheOperationInvoker.ThrowableWrapper ex) { + Throwable original = ex.getOriginal(); + cacheException(exceptionCache, operation.getExceptionTypeFilter(), cacheKey, original); + throw ex; + } + } + + /** + * Check for a cached exception. If the exception is found, throw it directly. + */ + protected void checkForCachedException(@Nullable Cache exceptionCache, Object cacheKey) { + if (exceptionCache == null) { + return; + } + Cache.ValueWrapper result = doGet(exceptionCache, cacheKey); + if (result != null) { + Throwable ex = (Throwable) result.get(); + Assert.state(ex != null, "No exception in cache"); + throw rewriteCallStack(ex, getClass().getName(), "invoke"); + } + } + + protected void cacheException(@Nullable Cache exceptionCache, ExceptionTypeFilter filter, Object cacheKey, Throwable ex) { + if (exceptionCache == null) { + return; + } + if (filter.match(ex.getClass())) { + doPut(exceptionCache, cacheKey, ex); + } + } + + @Nullable + private Cache resolveExceptionCache(CacheOperationInvocationContext context) { + CacheResolver exceptionCacheResolver = context.getOperation().getExceptionCacheResolver(); + if (exceptionCacheResolver != null) { + return extractFrom(context.getOperation().getExceptionCacheResolver().resolveCaches(context)); + } + return null; + } + + + /** + * Rewrite the call stack of the specified {@code exception} so that it matches + * the current call stack up to (included) the specified method invocation. + *

    Clone the specified exception. If the exception is not {@code serializable}, + * the original exception is returned. If no common ancestor can be found, returns + * the original exception. + *

    Used to make sure that a cached exception has a valid invocation context. + * @param exception the exception to merge with the current call stack + * @param className the class name of the common ancestor + * @param methodName the method name of the common ancestor + * @return a clone exception with a rewritten call stack composed of the current call + * stack up to (included) the common ancestor specified by the {@code className} and + * {@code methodName} arguments, followed by stack trace elements of the specified + * {@code exception} after the common ancestor. + */ + private static CacheOperationInvoker.ThrowableWrapper rewriteCallStack( + Throwable exception, String className, String methodName) { + + Throwable clone = cloneException(exception); + if (clone == null) { + return new CacheOperationInvoker.ThrowableWrapper(exception); + } + + StackTraceElement[] callStack = new Exception().getStackTrace(); + StackTraceElement[] cachedCallStack = exception.getStackTrace(); + + int index = findCommonAncestorIndex(callStack, className, methodName); + int cachedIndex = findCommonAncestorIndex(cachedCallStack, className, methodName); + if (index == -1 || cachedIndex == -1) { + return new CacheOperationInvoker.ThrowableWrapper(exception); // Cannot find common ancestor + } + StackTraceElement[] result = new StackTraceElement[cachedIndex + callStack.length - index]; + System.arraycopy(cachedCallStack, 0, result, 0, cachedIndex); + System.arraycopy(callStack, index, result, cachedIndex, callStack.length - index); + + clone.setStackTrace(result); + return new CacheOperationInvoker.ThrowableWrapper(clone); + } + + @SuppressWarnings("unchecked") + @Nullable + private static T cloneException(T exception) { + try { + return (T) SerializationUtils.deserialize(SerializationUtils.serialize(exception)); + } + catch (Exception ex) { + return null; // exception parameter cannot be cloned + } + } + + private static int findCommonAncestorIndex(StackTraceElement[] callStack, String className, String methodName) { + for (int i = 0; i < callStack.length; i++) { + StackTraceElement element = callStack[i]; + if (className.equals(element.getClassName()) && methodName.equals(element.getMethodName())) { + return i; + } + } + return -1; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultOperation.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultOperation.java new file mode 100644 index 0000000..3f4eedf --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultOperation.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import javax.cache.annotation.CacheMethodDetails; +import javax.cache.annotation.CacheResult; + +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.lang.Nullable; +import org.springframework.util.ExceptionTypeFilter; +import org.springframework.util.StringUtils; + +/** + * The {@link JCacheOperation} implementation for a {@link CacheResult} operation. + * + * @author Stephane Nicoll + * @since 4.1 + * @see CacheResult + */ +class CacheResultOperation extends AbstractJCacheKeyOperation { + + private final ExceptionTypeFilter exceptionTypeFilter; + + @Nullable + private final CacheResolver exceptionCacheResolver; + + @Nullable + private final String exceptionCacheName; + + + public CacheResultOperation(CacheMethodDetails methodDetails, CacheResolver cacheResolver, + KeyGenerator keyGenerator, @Nullable CacheResolver exceptionCacheResolver) { + + super(methodDetails, cacheResolver, keyGenerator); + + CacheResult ann = methodDetails.getCacheAnnotation(); + this.exceptionTypeFilter = createExceptionTypeFilter(ann.cachedExceptions(), ann.nonCachedExceptions()); + this.exceptionCacheResolver = exceptionCacheResolver; + this.exceptionCacheName = (StringUtils.hasText(ann.exceptionCacheName()) ? ann.exceptionCacheName() : null); + } + + + @Override + public ExceptionTypeFilter getExceptionTypeFilter() { + return this.exceptionTypeFilter; + } + + /** + * Specify if the method should always be invoked regardless of a cache hit. + * By default, the method is only invoked in case of a cache miss. + * @see javax.cache.annotation.CacheResult#skipGet() + */ + public boolean isAlwaysInvoked() { + return getCacheAnnotation().skipGet(); + } + + /** + * Return the {@link CacheResolver} instance to use to resolve the cache to + * use for matching exceptions thrown by this operation. + */ + @Nullable + public CacheResolver getExceptionCacheResolver() { + return this.exceptionCacheResolver; + } + + /** + * Return the name of the cache to cache exceptions, or {@code null} if + * caching exceptions should be disabled. + * @see javax.cache.annotation.CacheResult#exceptionCacheName() + */ + @Nullable + public String getExceptionCacheName() { + return this.exceptionCacheName; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultCacheInvocationContext.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultCacheInvocationContext.java new file mode 100644 index 0000000..35523c2 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultCacheInvocationContext.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Set; + +import javax.cache.annotation.CacheInvocationContext; +import javax.cache.annotation.CacheInvocationParameter; + +import org.springframework.cache.interceptor.CacheOperationInvocationContext; + +/** + * The default {@link CacheOperationInvocationContext} implementation used + * by all interceptors. Also implements {@link CacheInvocationContext} to + * act as a proper bridge when calling JSR-107 {@link javax.cache.annotation.CacheResolver} + * + * @author Stephane Nicoll + * @since 4.1 + * @param the annotation type + */ +class DefaultCacheInvocationContext + implements CacheInvocationContext, CacheOperationInvocationContext> { + + private final JCacheOperation operation; + + private final Object target; + + private final Object[] args; + + private final CacheInvocationParameter[] allParameters; + + + public DefaultCacheInvocationContext(JCacheOperation operation, Object target, Object[] args) { + this.operation = operation; + this.target = target; + this.args = args; + this.allParameters = operation.getAllParameters(args); + } + + + @Override + public JCacheOperation getOperation() { + return this.operation; + } + + @Override + public Method getMethod() { + return this.operation.getMethod(); + } + + @Override + public Object[] getArgs() { + return this.args.clone(); + } + + @Override + public Set getAnnotations() { + return this.operation.getAnnotations(); + } + + @Override + public A getCacheAnnotation() { + return this.operation.getCacheAnnotation(); + } + + @Override + public String getCacheName() { + return this.operation.getCacheName(); + } + + @Override + public Object getTarget() { + return this.target; + } + + @Override + public CacheInvocationParameter[] getAllParameters() { + return this.allParameters.clone(); + } + + @Override + public T unwrap(Class cls) { + throw new IllegalArgumentException("Cannot unwrap to " + cls); + } + + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("CacheInvocationContext{"); + sb.append("operation=").append(this.operation); + sb.append(", target=").append(this.target); + sb.append(", args=").append(Arrays.toString(this.args)); + sb.append(", allParameters=").append(Arrays.toString(this.allParameters)); + sb.append('}'); + return sb.toString(); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultCacheKeyInvocationContext.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultCacheKeyInvocationContext.java new file mode 100644 index 0000000..d1f396e --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultCacheKeyInvocationContext.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import java.lang.annotation.Annotation; + +import javax.cache.annotation.CacheInvocationParameter; +import javax.cache.annotation.CacheKeyInvocationContext; + +import org.springframework.lang.Nullable; + +/** + * The default {@link CacheKeyInvocationContext} implementation. + * + * @author Stephane Nicoll + * @since 4.1 + * @param the annotation type + */ +class DefaultCacheKeyInvocationContext extends DefaultCacheInvocationContext + implements CacheKeyInvocationContext { + + private final CacheInvocationParameter[] keyParameters; + + @Nullable + private final CacheInvocationParameter valueParameter; + + + public DefaultCacheKeyInvocationContext(AbstractJCacheKeyOperation operation, Object target, Object[] args) { + super(operation, target, args); + this.keyParameters = operation.getKeyParameters(args); + if (operation instanceof CachePutOperation) { + this.valueParameter = ((CachePutOperation) operation).getValueParameter(args); + } + else { + this.valueParameter = null; + } + } + + + @Override + public CacheInvocationParameter[] getKeyParameters() { + return this.keyParameters.clone(); + } + + @Override + @Nullable + public CacheInvocationParameter getValueParameter() { + return this.valueParameter; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultCacheMethodDetails.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultCacheMethodDetails.java new file mode 100644 index 0000000..ca4cebf --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultCacheMethodDetails.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import javax.cache.annotation.CacheMethodDetails; + +/** + * The default {@link CacheMethodDetails} implementation. + * + * @author Stephane Nicoll + * @since 4.1 + * @param the annotation type + */ +class DefaultCacheMethodDetails implements CacheMethodDetails { + + private final Method method; + + private final Set annotations; + + private final A cacheAnnotation; + + private final String cacheName; + + + public DefaultCacheMethodDetails(Method method, A cacheAnnotation, String cacheName) { + this.method = method; + this.annotations = Collections.unmodifiableSet( + new LinkedHashSet<>(Arrays.asList(method.getAnnotations()))); + this.cacheAnnotation = cacheAnnotation; + this.cacheName = cacheName; + } + + + @Override + public Method getMethod() { + return this.method; + } + + @Override + public Set getAnnotations() { + return this.annotations; + } + + @Override + public A getCacheAnnotation() { + return this.cacheAnnotation; + } + + @Override + public String getCacheName() { + return this.cacheName; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("CacheMethodDetails["); + sb.append("method=").append(this.method); + sb.append(", cacheAnnotation=").append(this.cacheAnnotation); + sb.append(", cacheName='").append(this.cacheName).append('\''); + sb.append(']'); + return sb.toString(); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultJCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultJCacheOperationSource.java new file mode 100644 index 0000000..c323cf4 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultJCacheOperationSource.java @@ -0,0 +1,253 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import java.util.Collection; +import java.util.function.Supplier; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.interceptor.CacheOperationInvocationContext; +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.cache.interceptor.SimpleCacheResolver; +import org.springframework.cache.interceptor.SimpleKeyGenerator; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.function.SingletonSupplier; +import org.springframework.util.function.SupplierUtils; + +/** + * The default {@link JCacheOperationSource} implementation delegating + * default operations to configurable services with sensible defaults + * when not present. + * + * @author Stephane Nicoll + * @author Juergen Hoeller + * @since 4.1 + */ +public class DefaultJCacheOperationSource extends AnnotationJCacheOperationSource + implements BeanFactoryAware, SmartInitializingSingleton { + + @Nullable + private SingletonSupplier cacheManager; + + @Nullable + private SingletonSupplier cacheResolver; + + @Nullable + private SingletonSupplier exceptionCacheResolver; + + private SingletonSupplier keyGenerator; + + private final SingletonSupplier adaptedKeyGenerator = + SingletonSupplier.of(() -> new KeyGeneratorAdapter(this, getKeyGenerator())); + + @Nullable + private BeanFactory beanFactory; + + + /** + * Construct a new {@code DefaultJCacheOperationSource} with the default key generator. + * @see SimpleKeyGenerator + */ + public DefaultJCacheOperationSource() { + this.keyGenerator = SingletonSupplier.of(SimpleKeyGenerator::new); + } + + /** + * Construct a new {@code DefaultJCacheOperationSource} with the given cache manager, + * cache resolver and key generator suppliers, applying the corresponding default + * if a supplier is not resolvable. + * @since 5.1 + */ + public DefaultJCacheOperationSource( + @Nullable Supplier cacheManager, @Nullable Supplier cacheResolver, + @Nullable Supplier exceptionCacheResolver, @Nullable Supplier keyGenerator) { + + this.cacheManager = SingletonSupplier.ofNullable(cacheManager); + this.cacheResolver = SingletonSupplier.ofNullable(cacheResolver); + this.exceptionCacheResolver = SingletonSupplier.ofNullable(exceptionCacheResolver); + this.keyGenerator = new SingletonSupplier<>(keyGenerator, SimpleKeyGenerator::new); + } + + + /** + * Set the default {@link CacheManager} to use to lookup cache by name. + * Only mandatory if the {@linkplain CacheResolver cache resolver} has not been set. + */ + public void setCacheManager(@Nullable CacheManager cacheManager) { + this.cacheManager = SingletonSupplier.ofNullable(cacheManager); + } + + /** + * Return the specified cache manager to use, if any. + */ + @Nullable + public CacheManager getCacheManager() { + return SupplierUtils.resolve(this.cacheManager); + } + + /** + * Set the {@link CacheResolver} to resolve regular caches. If none is set, a default + * implementation using the specified cache manager will be used. + */ + public void setCacheResolver(@Nullable CacheResolver cacheResolver) { + this.cacheResolver = SingletonSupplier.ofNullable(cacheResolver); + } + + /** + * Return the specified cache resolver to use, if any. + */ + @Nullable + public CacheResolver getCacheResolver() { + return SupplierUtils.resolve(this.cacheResolver); + } + + /** + * Set the {@link CacheResolver} to resolve exception caches. If none is set, a default + * implementation using the specified cache manager will be used. + */ + public void setExceptionCacheResolver(@Nullable CacheResolver exceptionCacheResolver) { + this.exceptionCacheResolver = SingletonSupplier.ofNullable(exceptionCacheResolver); + } + + /** + * Return the specified exception cache resolver to use, if any. + */ + @Nullable + public CacheResolver getExceptionCacheResolver() { + return SupplierUtils.resolve(this.exceptionCacheResolver); + } + + /** + * Set the default {@link KeyGenerator}. If none is set, a {@link SimpleKeyGenerator} + * honoring the JSR-107 {@link javax.cache.annotation.CacheKey} and + * {@link javax.cache.annotation.CacheValue} will be used. + */ + public void setKeyGenerator(KeyGenerator keyGenerator) { + this.keyGenerator = SingletonSupplier.of(keyGenerator); + } + + /** + * Return the specified key generator to use. + */ + public KeyGenerator getKeyGenerator() { + return this.keyGenerator.obtain(); + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + + @Override + public void afterSingletonsInstantiated() { + // Make sure that the cache resolver is initialized. An exception cache resolver is only + // required if the exceptionCacheName attribute is set on an operation. + Assert.notNull(getDefaultCacheResolver(), "Cache resolver should have been initialized"); + } + + + @Override + protected T getBean(Class type) { + Assert.state(this.beanFactory != null, () -> "BeanFactory required for resolution of [" + type + "]"); + try { + return this.beanFactory.getBean(type); + } + catch (NoUniqueBeanDefinitionException ex) { + throw new IllegalStateException("No unique [" + type.getName() + "] bean found in application context - " + + "mark one as primary, or declare a more specific implementation type for your cache", ex); + } + catch (NoSuchBeanDefinitionException ex) { + if (logger.isDebugEnabled()) { + logger.debug("No bean of type [" + type.getName() + "] found in application context", ex); + } + return BeanUtils.instantiateClass(type); + } + } + + protected CacheManager getDefaultCacheManager() { + if (getCacheManager() == null) { + Assert.state(this.beanFactory != null, "BeanFactory required for default CacheManager resolution"); + try { + this.cacheManager = SingletonSupplier.of(this.beanFactory.getBean(CacheManager.class)); + } + catch (NoUniqueBeanDefinitionException ex) { + throw new IllegalStateException("No unique bean of type CacheManager found. "+ + "Mark one as primary or declare a specific CacheManager to use."); + } + catch (NoSuchBeanDefinitionException ex) { + throw new IllegalStateException("No bean of type CacheManager found. Register a CacheManager "+ + "bean or remove the @EnableCaching annotation from your configuration."); + } + } + return getCacheManager(); + } + + @Override + protected CacheResolver getDefaultCacheResolver() { + if (getCacheResolver() == null) { + this.cacheResolver = SingletonSupplier.of(new SimpleCacheResolver(getDefaultCacheManager())); + } + return getCacheResolver(); + } + + @Override + protected CacheResolver getDefaultExceptionCacheResolver() { + if (getExceptionCacheResolver() == null) { + this.exceptionCacheResolver = SingletonSupplier.of(new LazyCacheResolver()); + } + return getExceptionCacheResolver(); + } + + @Override + protected KeyGenerator getDefaultKeyGenerator() { + return this.adaptedKeyGenerator.obtain(); + } + + + /** + * Only resolve the default exception cache resolver when an exception needs to be handled. + *

    A non-JSR-107 setup requires either a {@link CacheManager} or a {@link CacheResolver}. + * If only the latter is specified, it is not possible to extract a default exception + * {@code CacheResolver} from a custom {@code CacheResolver} implementation so we have to + * fall back on the {@code CacheManager}. + *

    This gives this weird situation of a perfectly valid configuration that breaks all + * the sudden because the JCache support is enabled. To avoid this we resolve the default + * exception {@code CacheResolver} as late as possible to avoid such hard requirement + * in other cases. + */ + class LazyCacheResolver implements CacheResolver { + + private final SingletonSupplier cacheResolver = + SingletonSupplier.of(() -> new SimpleExceptionCacheResolver(getDefaultCacheManager())); + + @Override + public Collection resolveCaches(CacheOperationInvocationContext context) { + return this.cacheResolver.obtain().resolveCaches(context); + } + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheAspectSupport.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheAspectSupport.java new file mode 100644 index 0000000..1dbf3c8 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheAspectSupport.java @@ -0,0 +1,188 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aop.framework.AopProxyUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.cache.interceptor.AbstractCacheInvoker; +import org.springframework.cache.interceptor.BasicOperation; +import org.springframework.cache.interceptor.CacheOperationInvocationContext; +import org.springframework.cache.interceptor.CacheOperationInvoker; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Base class for JSR-107 caching aspects, such as the {@link JCacheInterceptor} + * or an AspectJ aspect. + * + *

    Use the Spring caching abstraction for cache-related operations. No JSR-107 + * {@link javax.cache.Cache} or {@link javax.cache.CacheManager} are required to + * process standard JSR-107 cache annotations. + * + *

    The {@link JCacheOperationSource} is used for determining caching operations + * + *

    A cache aspect is serializable if its {@code JCacheOperationSource} is serializable. + * + * @author Stephane Nicoll + * @since 4.1 + * @see org.springframework.cache.interceptor.CacheAspectSupport + * @see KeyGeneratorAdapter + * @see CacheResolverAdapter + */ +public class JCacheAspectSupport extends AbstractCacheInvoker implements InitializingBean { + + protected final Log logger = LogFactory.getLog(getClass()); + + @Nullable + private JCacheOperationSource cacheOperationSource; + + @Nullable + private CacheResultInterceptor cacheResultInterceptor; + + @Nullable + private CachePutInterceptor cachePutInterceptor; + + @Nullable + private CacheRemoveEntryInterceptor cacheRemoveEntryInterceptor; + + @Nullable + private CacheRemoveAllInterceptor cacheRemoveAllInterceptor; + + private boolean initialized = false; + + + /** + * Set the CacheOperationSource for this cache aspect. + */ + public void setCacheOperationSource(JCacheOperationSource cacheOperationSource) { + Assert.notNull(cacheOperationSource, "JCacheOperationSource must not be null"); + this.cacheOperationSource = cacheOperationSource; + } + + /** + * Return the CacheOperationSource for this cache aspect. + */ + public JCacheOperationSource getCacheOperationSource() { + Assert.state(this.cacheOperationSource != null, "The 'cacheOperationSource' property is required: " + + "If there are no cacheable methods, then don't use a cache aspect."); + return this.cacheOperationSource; + } + + @Override + public void afterPropertiesSet() { + getCacheOperationSource(); + + this.cacheResultInterceptor = new CacheResultInterceptor(getErrorHandler()); + this.cachePutInterceptor = new CachePutInterceptor(getErrorHandler()); + this.cacheRemoveEntryInterceptor = new CacheRemoveEntryInterceptor(getErrorHandler()); + this.cacheRemoveAllInterceptor = new CacheRemoveAllInterceptor(getErrorHandler()); + + this.initialized = true; + } + + + @Nullable + protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) { + // Check whether aspect is enabled to cope with cases where the AJ is pulled in automatically + if (this.initialized) { + Class targetClass = AopProxyUtils.ultimateTargetClass(target); + JCacheOperation operation = getCacheOperationSource().getCacheOperation(method, targetClass); + if (operation != null) { + CacheOperationInvocationContext context = + createCacheOperationInvocationContext(target, args, operation); + return execute(context, invoker); + } + } + + return invoker.invoke(); + } + + @SuppressWarnings("unchecked") + private CacheOperationInvocationContext createCacheOperationInvocationContext( + Object target, Object[] args, JCacheOperation operation) { + + return new DefaultCacheInvocationContext<>( + (JCacheOperation) operation, target, args); + } + + @SuppressWarnings("unchecked") + @Nullable + private Object execute(CacheOperationInvocationContext context, CacheOperationInvoker invoker) { + CacheOperationInvoker adapter = new CacheOperationInvokerAdapter(invoker); + BasicOperation operation = context.getOperation(); + + if (operation instanceof CacheResultOperation) { + Assert.state(this.cacheResultInterceptor != null, "No CacheResultInterceptor"); + return this.cacheResultInterceptor.invoke( + (CacheOperationInvocationContext) context, adapter); + } + else if (operation instanceof CachePutOperation) { + Assert.state(this.cachePutInterceptor != null, "No CachePutInterceptor"); + return this.cachePutInterceptor.invoke( + (CacheOperationInvocationContext) context, adapter); + } + else if (operation instanceof CacheRemoveOperation) { + Assert.state(this.cacheRemoveEntryInterceptor != null, "No CacheRemoveEntryInterceptor"); + return this.cacheRemoveEntryInterceptor.invoke( + (CacheOperationInvocationContext) context, adapter); + } + else if (operation instanceof CacheRemoveAllOperation) { + Assert.state(this.cacheRemoveAllInterceptor != null, "No CacheRemoveAllInterceptor"); + return this.cacheRemoveAllInterceptor.invoke( + (CacheOperationInvocationContext) context, adapter); + } + else { + throw new IllegalArgumentException("Cannot handle " + operation); + } + } + + /** + * Execute the underlying operation (typically in case of cache miss) and return + * the result of the invocation. If an exception occurs it will be wrapped in + * a {@code ThrowableWrapper}: the exception can be handled or modified but it + * must be wrapped in a {@code ThrowableWrapper} as well. + * @param invoker the invoker handling the operation being cached + * @return the result of the invocation + * @see CacheOperationInvoker#invoke() + */ + @Nullable + protected Object invokeOperation(CacheOperationInvoker invoker) { + return invoker.invoke(); + } + + + private class CacheOperationInvokerAdapter implements CacheOperationInvoker { + + private final CacheOperationInvoker delegate; + + public CacheOperationInvokerAdapter(CacheOperationInvoker delegate) { + this.delegate = delegate; + } + + @Override + public Object invoke() throws ThrowableWrapper { + return invokeOperation(this.delegate); + } + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheInterceptor.java new file mode 100644 index 0000000..81e65d1 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheInterceptor.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.function.Supplier; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.cache.interceptor.CacheErrorHandler; +import org.springframework.cache.interceptor.CacheOperationInvoker; +import org.springframework.cache.interceptor.SimpleCacheErrorHandler; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.function.SingletonSupplier; + +/** + * AOP Alliance MethodInterceptor for declarative cache + * management using JSR-107 caching annotations. + * + *

    Derives from the {@link JCacheAspectSupport} class which + * contains the integration with Spring's underlying caching API. + * JCacheInterceptor simply calls the relevant superclass method. + * + *

    JCacheInterceptors are thread-safe. + * + * @author Stephane Nicoll + * @author Juergen Hoeller + * @since 4.1 + * @see org.springframework.cache.interceptor.CacheInterceptor + */ +@SuppressWarnings("serial") +public class JCacheInterceptor extends JCacheAspectSupport implements MethodInterceptor, Serializable { + + /** + * Construct a new {@code JCacheInterceptor} with the default error handler. + */ + public JCacheInterceptor() { + } + + /** + * Construct a new {@code JCacheInterceptor} with the given error handler. + * @param errorHandler a supplier for the error handler to use, + * applying the default error handler if the supplier is not resolvable + * @since 5.1 + */ + public JCacheInterceptor(@Nullable Supplier errorHandler) { + this.errorHandler = new SingletonSupplier<>(errorHandler, SimpleCacheErrorHandler::new); + } + + + @Override + @Nullable + public Object invoke(final MethodInvocation invocation) throws Throwable { + Method method = invocation.getMethod(); + + CacheOperationInvoker aopAllianceInvoker = () -> { + try { + return invocation.proceed(); + } + catch (Throwable ex) { + throw new CacheOperationInvoker.ThrowableWrapper(ex); + } + }; + + Object target = invocation.getThis(); + Assert.state(target != null, "Target must not be null"); + try { + return execute(aopAllianceInvoker, target, method, invocation.getArguments()); + } + catch (CacheOperationInvoker.ThrowableWrapper th) { + throw th.getOriginal(); + } + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperation.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperation.java new file mode 100644 index 0000000..00c5ef9 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperation.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import java.lang.annotation.Annotation; + +import javax.cache.annotation.CacheInvocationParameter; +import javax.cache.annotation.CacheMethodDetails; + +import org.springframework.cache.interceptor.BasicOperation; +import org.springframework.cache.interceptor.CacheResolver; + +/** + * Model the base of JSR-107 cache operation through an interface contract. + * + *

    A cache operation can be statically cached as it does not contain any + * runtime operation of a specific cache invocation. + * + * @author Stephane Nicoll + * @since 4.1 + * @param the type of the JSR-107 annotation + */ +public interface JCacheOperation extends BasicOperation, CacheMethodDetails { + + /** + * Return the {@link CacheResolver} instance to use to resolve the cache + * to use for this operation. + */ + CacheResolver getCacheResolver(); + + /** + * Return the {@link CacheInvocationParameter} instances based on the + * specified method arguments. + *

    The method arguments must match the signature of the related method invocation + * @param values the parameters value for a particular invocation + */ + CacheInvocationParameter[] getAllParameters(Object... values); + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSource.java new file mode 100644 index 0000000..445a7ef --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSource.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import java.lang.reflect.Method; + +import org.springframework.lang.Nullable; + +/** + * Interface used by {@link JCacheInterceptor}. Implementations know how to source + * cache operation attributes from standard JSR-107 annotations. + * + * @author Stephane Nicoll + * @since 4.1 + * @see org.springframework.cache.interceptor.CacheOperationSource + */ +public interface JCacheOperationSource { + + /** + * Return the cache operations for this method, or {@code null} + * if the method contains no JSR-107 related metadata. + * @param method the method to introspect + * @param targetClass the target class (may be {@code null}, in which case + * the declaring class of the method must be used) + * @return the cache operation for this method, or {@code null} if none found + */ + @Nullable + JCacheOperation getCacheOperation(Method method, @Nullable Class targetClass); + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java new file mode 100644 index 0000000..4b6d7e0 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import java.io.Serializable; +import java.lang.reflect.Method; + +import org.springframework.aop.support.StaticMethodMatcherPointcut; +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +/** + * A Pointcut that matches if the underlying {@link JCacheOperationSource} + * has an operation for a given method. + * + * @author Stephane Nicoll + * @since 4.1 + */ +@SuppressWarnings("serial") +public abstract class JCacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable { + + @Override + public boolean matches(Method method, Class targetClass) { + JCacheOperationSource cas = getCacheOperationSource(); + return (cas != null && cas.getCacheOperation(method, targetClass) != null); + } + + /** + * Obtain the underlying {@link JCacheOperationSource} (may be {@code null}). + * To be implemented by subclasses. + */ + @Nullable + protected abstract JCacheOperationSource getCacheOperationSource(); + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof JCacheOperationSourcePointcut)) { + return false; + } + JCacheOperationSourcePointcut otherPc = (JCacheOperationSourcePointcut) other; + return ObjectUtils.nullSafeEquals(getCacheOperationSource(), otherPc.getCacheOperationSource()); + } + + @Override + public int hashCode() { + return JCacheOperationSourcePointcut.class.hashCode(); + } + + @Override + public String toString() { + return getClass().getName() + ": " + getCacheOperationSource(); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/KeyGeneratorAdapter.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/KeyGeneratorAdapter.java new file mode 100644 index 0000000..3fcfe9f --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/KeyGeneratorAdapter.java @@ -0,0 +1,128 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import javax.cache.annotation.CacheInvocationParameter; +import javax.cache.annotation.CacheKeyGenerator; +import javax.cache.annotation.CacheKeyInvocationContext; + +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * Spring's {@link KeyGenerator} implementation that either delegates to a standard JSR-107 + * {@link javax.cache.annotation.CacheKeyGenerator}, or wrap a standard {@link KeyGenerator} + * so that only relevant parameters are handled. + * + * @author Stephane Nicoll + * @author Juergen Hoeller + * @since 4.1 + */ +class KeyGeneratorAdapter implements KeyGenerator { + + private final JCacheOperationSource cacheOperationSource; + + @Nullable + private KeyGenerator keyGenerator; + + @Nullable + private CacheKeyGenerator cacheKeyGenerator; + + + /** + * Create an instance with the given {@link KeyGenerator} so that {@link javax.cache.annotation.CacheKey} + * and {@link javax.cache.annotation.CacheValue} are handled according to the spec. + */ + public KeyGeneratorAdapter(JCacheOperationSource cacheOperationSource, KeyGenerator target) { + Assert.notNull(cacheOperationSource, "JCacheOperationSource must not be null"); + Assert.notNull(target, "KeyGenerator must not be null"); + this.cacheOperationSource = cacheOperationSource; + this.keyGenerator = target; + } + + /** + * Create an instance used to wrap the specified {@link javax.cache.annotation.CacheKeyGenerator}. + */ + public KeyGeneratorAdapter(JCacheOperationSource cacheOperationSource, CacheKeyGenerator target) { + Assert.notNull(cacheOperationSource, "JCacheOperationSource must not be null"); + Assert.notNull(target, "CacheKeyGenerator must not be null"); + this.cacheOperationSource = cacheOperationSource; + this.cacheKeyGenerator = target; + } + + + /** + * Return the target key generator to use in the form of either a {@link KeyGenerator} + * or a {@link CacheKeyGenerator}. + */ + public Object getTarget() { + if (this.cacheKeyGenerator != null) { + return this.cacheKeyGenerator; + } + Assert.state(this.keyGenerator != null, "No key generator"); + return this.keyGenerator; + } + + @Override + public Object generate(Object target, Method method, Object... params) { + JCacheOperation operation = this.cacheOperationSource.getCacheOperation(method, target.getClass()); + if (!(operation instanceof AbstractJCacheKeyOperation)) { + throw new IllegalStateException("Invalid operation, should be a key-based operation " + operation); + } + CacheKeyInvocationContext invocationContext = createCacheKeyInvocationContext(target, operation, params); + + if (this.cacheKeyGenerator != null) { + return this.cacheKeyGenerator.generateCacheKey(invocationContext); + } + else { + Assert.state(this.keyGenerator != null, "No key generator"); + return doGenerate(this.keyGenerator, invocationContext); + } + } + + private static Object doGenerate(KeyGenerator keyGenerator, CacheKeyInvocationContext context) { + List parameters = new ArrayList<>(); + for (CacheInvocationParameter param : context.getKeyParameters()) { + Object value = param.getValue(); + if (param.getParameterPosition() == context.getAllParameters().length - 1 && + context.getMethod().isVarArgs()) { + parameters.addAll(CollectionUtils.arrayToList(value)); + } + else { + parameters.add(value); + } + } + return keyGenerator.generate(context.getTarget(), context.getMethod(), parameters.toArray()); + } + + + @SuppressWarnings("unchecked") + private CacheKeyInvocationContext createCacheKeyInvocationContext( + Object target, JCacheOperation operation, Object[] params) { + + AbstractJCacheKeyOperation keyCacheOperation = (AbstractJCacheKeyOperation) operation; + return new DefaultCacheKeyInvocationContext<>(keyCacheOperation, target, params); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleExceptionCacheResolver.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleExceptionCacheResolver.java new file mode 100644 index 0000000..f2091ab --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleExceptionCacheResolver.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import java.util.Collection; +import java.util.Collections; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.interceptor.AbstractCacheResolver; +import org.springframework.cache.interceptor.BasicOperation; +import org.springframework.cache.interceptor.CacheOperationInvocationContext; +import org.springframework.cache.interceptor.CacheResolver; + +/** + * A simple {@link CacheResolver} that resolves the exception cache + * based on a configurable {@link CacheManager} and the name of the + * cache: {@link CacheResultOperation#getExceptionCacheName()}. + * + * @author Stephane Nicoll + * @since 4.1 + * @see CacheResultOperation#getExceptionCacheName() + */ +public class SimpleExceptionCacheResolver extends AbstractCacheResolver { + + public SimpleExceptionCacheResolver(CacheManager cacheManager) { + super(cacheManager); + } + + @Override + protected Collection getCacheNames(CacheOperationInvocationContext context) { + BasicOperation operation = context.getOperation(); + if (!(operation instanceof CacheResultOperation)) { + throw new IllegalStateException("Could not extract exception cache name from " + operation); + } + CacheResultOperation cacheResultOperation = (CacheResultOperation) operation; + String exceptionCacheName = cacheResultOperation.getExceptionCacheName(); + if (exceptionCacheName != null) { + return Collections.singleton(exceptionCacheName); + } + return null; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/package-info.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/package-info.java new file mode 100644 index 0000000..c752c80 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/package-info.java @@ -0,0 +1,15 @@ +/** + * AOP-based solution for declarative caching demarcation using JSR-107 annotations. + * + *

    Strongly based on the infrastructure in org.springframework.cache.interceptor + * that deals with Spring's caching annotations. + * + *

    Builds on the AOP infrastructure in org.springframework.aop.framework. + * Any POJO can be cache-advised with Spring. + */ +@NonNullApi +@NonNullFields +package org.springframework.cache.jcache.interceptor; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/package-info.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/package-info.java new file mode 100644 index 0000000..e8b5c89 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/package-info.java @@ -0,0 +1,12 @@ +/** + * Implementation package for JSR-107 (javax.cache aka "JCache") based caches. + * Provides a {@link org.springframework.cache.CacheManager CacheManager} + * and {@link org.springframework.cache.Cache Cache} implementation for + * use in a Spring context, using a JSR-107 compliant cache provider. + */ +@NonNullApi +@NonNullFields +package org.springframework.cache.jcache; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context-support/src/main/java/org/springframework/cache/transaction/AbstractTransactionSupportingCacheManager.java b/spring-context-support/src/main/java/org/springframework/cache/transaction/AbstractTransactionSupportingCacheManager.java new file mode 100644 index 0000000..a65e0ca --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/transaction/AbstractTransactionSupportingCacheManager.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.transaction; + +import org.springframework.cache.Cache; +import org.springframework.cache.support.AbstractCacheManager; + +/** + * Base class for CacheManager implementations that want to support built-in + * awareness of Spring-managed transactions. This usually needs to be switched + * on explicitly through the {@link #setTransactionAware} bean property. + * + * @author Juergen Hoeller + * @since 3.2 + * @see #setTransactionAware + * @see TransactionAwareCacheDecorator + * @see TransactionAwareCacheManagerProxy + */ +public abstract class AbstractTransactionSupportingCacheManager extends AbstractCacheManager { + + private boolean transactionAware = false; + + + /** + * Set whether this CacheManager should expose transaction-aware Cache objects. + *

    Default is "false". Set this to "true" to synchronize cache put/evict + * operations with ongoing Spring-managed transactions, performing the actual cache + * put/evict operation only in the after-commit phase of a successful transaction. + */ + public void setTransactionAware(boolean transactionAware) { + this.transactionAware = transactionAware; + } + + /** + * Return whether this CacheManager has been configured to be transaction-aware. + */ + public boolean isTransactionAware() { + return this.transactionAware; + } + + + @Override + protected Cache decorateCache(Cache cache) { + return (isTransactionAware() ? new TransactionAwareCacheDecorator(cache) : cache); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/transaction/TransactionAwareCacheDecorator.java b/spring-context-support/src/main/java/org/springframework/cache/transaction/TransactionAwareCacheDecorator.java new file mode 100644 index 0000000..22fcaff --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/transaction/TransactionAwareCacheDecorator.java @@ -0,0 +1,155 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.transaction; + +import java.util.concurrent.Callable; + +import org.springframework.cache.Cache; +import org.springframework.lang.Nullable; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.Assert; + +/** + * Cache decorator which synchronizes its {@link #put}, {@link #evict} and + * {@link #clear} operations with Spring-managed transactions (through Spring's + * {@link TransactionSynchronizationManager}, performing the actual cache + * put/evict/clear operation only in the after-commit phase of a successful + * transaction. If no transaction is active, {@link #put}, {@link #evict} and + * {@link #clear} operations will be performed immediately, as usual. + * + *

    Note: Use of immediate operations such as {@link #putIfAbsent} and + * {@link #evictIfPresent} cannot be deferred to the after-commit phase of a + * running transaction. Use these with care in a transactional environment. + * + * @author Juergen Hoeller + * @author Stephane Nicoll + * @author Stas Volsky + * @since 3.2 + * @see TransactionAwareCacheManagerProxy + */ +public class TransactionAwareCacheDecorator implements Cache { + + private final Cache targetCache; + + + /** + * Create a new TransactionAwareCache for the given target Cache. + * @param targetCache the target Cache to decorate + */ + public TransactionAwareCacheDecorator(Cache targetCache) { + Assert.notNull(targetCache, "Target Cache must not be null"); + this.targetCache = targetCache; + } + + + /** + * Return the target Cache that this Cache should delegate to. + */ + public Cache getTargetCache() { + return this.targetCache; + } + + @Override + public String getName() { + return this.targetCache.getName(); + } + + @Override + public Object getNativeCache() { + return this.targetCache.getNativeCache(); + } + + @Override + @Nullable + public ValueWrapper get(Object key) { + return this.targetCache.get(key); + } + + @Override + public T get(Object key, @Nullable Class type) { + return this.targetCache.get(key, type); + } + + @Override + @Nullable + public T get(Object key, Callable valueLoader) { + return this.targetCache.get(key, valueLoader); + } + + @Override + public void put(final Object key, @Nullable final Object value) { + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + TransactionAwareCacheDecorator.this.targetCache.put(key, value); + } + }); + } + else { + this.targetCache.put(key, value); + } + } + + @Override + @Nullable + public ValueWrapper putIfAbsent(Object key, @Nullable Object value) { + return this.targetCache.putIfAbsent(key, value); + } + + @Override + public void evict(final Object key) { + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + TransactionAwareCacheDecorator.this.targetCache.evict(key); + } + }); + } + else { + this.targetCache.evict(key); + } + } + + @Override + public boolean evictIfPresent(Object key) { + return this.targetCache.evictIfPresent(key); + } + + @Override + public void clear() { + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + targetCache.clear(); + } + }); + } + else { + this.targetCache.clear(); + } + } + + @Override + public boolean invalidate() { + return this.targetCache.invalidate(); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/transaction/TransactionAwareCacheManagerProxy.java b/spring-context-support/src/main/java/org/springframework/cache/transaction/TransactionAwareCacheManagerProxy.java new file mode 100644 index 0000000..d6c5189 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/transaction/TransactionAwareCacheManagerProxy.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.transaction; + +import java.util.Collection; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Proxy for a target {@link CacheManager}, exposing transaction-aware {@link Cache} objects + * which synchronize their {@link Cache#put} operations with Spring-managed transactions + * (through Spring's {@link org.springframework.transaction.support.TransactionSynchronizationManager}, + * performing the actual cache put operation only in the after-commit phase of a successful transaction. + * If no transaction is active, {@link Cache#put} operations will be performed immediately, as usual. + * + * @author Juergen Hoeller + * @since 3.2 + * @see #setTargetCacheManager + * @see TransactionAwareCacheDecorator + * @see org.springframework.transaction.support.TransactionSynchronizationManager + */ +public class TransactionAwareCacheManagerProxy implements CacheManager, InitializingBean { + + @Nullable + private CacheManager targetCacheManager; + + + /** + * Create a new TransactionAwareCacheManagerProxy, setting the target CacheManager + * through the {@link #setTargetCacheManager} bean property. + */ + public TransactionAwareCacheManagerProxy() { + } + + /** + * Create a new TransactionAwareCacheManagerProxy for the given target CacheManager. + * @param targetCacheManager the target CacheManager to proxy + */ + public TransactionAwareCacheManagerProxy(CacheManager targetCacheManager) { + Assert.notNull(targetCacheManager, "Target CacheManager must not be null"); + this.targetCacheManager = targetCacheManager; + } + + + /** + * Set the target CacheManager to proxy. + */ + public void setTargetCacheManager(CacheManager targetCacheManager) { + this.targetCacheManager = targetCacheManager; + } + + @Override + public void afterPropertiesSet() { + if (this.targetCacheManager == null) { + throw new IllegalArgumentException("Property 'targetCacheManager' is required"); + } + } + + + @Override + @Nullable + public Cache getCache(String name) { + Assert.state(this.targetCacheManager != null, "No target CacheManager set"); + Cache targetCache = this.targetCacheManager.getCache(name); + return (targetCache != null ? new TransactionAwareCacheDecorator(targetCache) : null); + } + + @Override + public Collection getCacheNames() { + Assert.state(this.targetCacheManager != null, "No target CacheManager set"); + return this.targetCacheManager.getCacheNames(); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/transaction/package-info.java b/spring-context-support/src/main/java/org/springframework/cache/transaction/package-info.java new file mode 100644 index 0000000..340f019 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/transaction/package-info.java @@ -0,0 +1,10 @@ +/** + * Transaction-aware decorators for the org.springframework.cache package. + * Provides synchronization of put operations with Spring-managed transactions. + */ +@NonNullApi +@NonNullFields +package org.springframework.cache.transaction; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context-support/src/main/java/org/springframework/mail/MailAuthenticationException.java b/spring-context-support/src/main/java/org/springframework/mail/MailAuthenticationException.java new file mode 100644 index 0000000..d4de3d1 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/mail/MailAuthenticationException.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.mail; + +/** + * Exception thrown on failed authentication. + * + * @author Dmitriy Kopylenko + * @author Juergen Hoeller + */ +@SuppressWarnings("serial") +public class MailAuthenticationException extends MailException { + + /** + * Constructor for MailAuthenticationException. + * @param msg message + */ + public MailAuthenticationException(String msg) { + super(msg); + } + + /** + * Constructor for MailAuthenticationException. + * @param msg the detail message + * @param cause the root cause from the mail API in use + */ + public MailAuthenticationException(String msg, Throwable cause) { + super(msg, cause); + } + + /** + * Constructor for MailAuthenticationException. + * @param cause the root cause from the mail API in use + */ + public MailAuthenticationException(Throwable cause) { + super("Authentication failed", cause); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/mail/MailException.java b/spring-context-support/src/main/java/org/springframework/mail/MailException.java new file mode 100644 index 0000000..0761369 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/mail/MailException.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.mail; + +import org.springframework.core.NestedRuntimeException; +import org.springframework.lang.Nullable; + +/** + * Base class for all mail exceptions. + * + * @author Dmitriy Kopylenko + */ +@SuppressWarnings("serial") +public abstract class MailException extends NestedRuntimeException { + + /** + * Constructor for MailException. + * @param msg the detail message + */ + public MailException(String msg) { + super(msg); + } + + /** + * Constructor for MailException. + * @param msg the detail message + * @param cause the root cause from the mail API in use + */ + public MailException(@Nullable String msg, @Nullable Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/mail/MailMessage.java b/spring-context-support/src/main/java/org/springframework/mail/MailMessage.java new file mode 100644 index 0000000..3ec68c4 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/mail/MailMessage.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.mail; + +import java.util.Date; + +/** + * This is a common interface for mail messages, allowing a user to set key + * values required in assembling a mail message, without needing to know if + * the underlying message is a simple text message or a more sophisticated + * MIME message. + * + *

    Implemented by both SimpleMailMessage and MimeMessageHelper, + * to let message population code interact with a simple message or a + * MIME message through a common interface. + * + * @author Juergen Hoeller + * @since 1.1.5 + * @see SimpleMailMessage + * @see org.springframework.mail.javamail.MimeMessageHelper + */ +public interface MailMessage { + + void setFrom(String from) throws MailParseException; + + void setReplyTo(String replyTo) throws MailParseException; + + void setTo(String to) throws MailParseException; + + void setTo(String... to) throws MailParseException; + + void setCc(String cc) throws MailParseException; + + void setCc(String... cc) throws MailParseException; + + void setBcc(String bcc) throws MailParseException; + + void setBcc(String... bcc) throws MailParseException; + + void setSentDate(Date sentDate) throws MailParseException; + + void setSubject(String subject) throws MailParseException; + + void setText(String text) throws MailParseException; + +} diff --git a/spring-context-support/src/main/java/org/springframework/mail/MailParseException.java b/spring-context-support/src/main/java/org/springframework/mail/MailParseException.java new file mode 100644 index 0000000..12290ed --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/mail/MailParseException.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.mail; + +/** + * Exception thrown if illegal message properties are encountered. + * + * @author Dmitriy Kopylenko + * @author Juergen Hoeller + */ +@SuppressWarnings("serial") +public class MailParseException extends MailException { + + /** + * Constructor for MailParseException. + * @param msg the detail message + */ + public MailParseException(String msg) { + super(msg); + } + + /** + * Constructor for MailParseException. + * @param msg the detail message + * @param cause the root cause from the mail API in use + */ + public MailParseException(String msg, Throwable cause) { + super(msg, cause); + } + + /** + * Constructor for MailParseException. + * @param cause the root cause from the mail API in use + */ + public MailParseException(Throwable cause) { + super("Could not parse mail", cause); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/mail/MailPreparationException.java b/spring-context-support/src/main/java/org/springframework/mail/MailPreparationException.java new file mode 100644 index 0000000..cc7901a --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/mail/MailPreparationException.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.mail; + +/** + * Exception to be thrown by user code if a mail cannot be prepared properly, + * for example when a FreeMarker template cannot be rendered for the mail text. + * + * @author Juergen Hoeller + * @since 1.1 + * @see org.springframework.ui.freemarker.FreeMarkerTemplateUtils#processTemplateIntoString + */ +@SuppressWarnings("serial") +public class MailPreparationException extends MailException { + + /** + * Constructor for MailPreparationException. + * @param msg the detail message + */ + public MailPreparationException(String msg) { + super(msg); + } + + /** + * Constructor for MailPreparationException. + * @param msg the detail message + * @param cause the root cause from the mail API in use + */ + public MailPreparationException(String msg, Throwable cause) { + super(msg, cause); + } + + public MailPreparationException(Throwable cause) { + super("Could not prepare mail", cause); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/mail/MailSendException.java b/spring-context-support/src/main/java/org/springframework/mail/MailSendException.java new file mode 100644 index 0000000..693082c --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/mail/MailSendException.java @@ -0,0 +1,199 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.mail; + +import java.io.PrintStream; +import java.io.PrintWriter; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +/** + * Exception thrown when a mail sending error is encountered. + * Can register failed messages with their exceptions. + * + * @author Dmitriy Kopylenko + * @author Juergen Hoeller + */ +@SuppressWarnings("serial") +public class MailSendException extends MailException { + + private final transient Map failedMessages; + + @Nullable + private final Exception[] messageExceptions; + + + /** + * Constructor for MailSendException. + * @param msg the detail message + */ + public MailSendException(String msg) { + this(msg, null); + } + + /** + * Constructor for MailSendException. + * @param msg the detail message + * @param cause the root cause from the mail API in use + */ + public MailSendException(String msg, @Nullable Throwable cause) { + super(msg, cause); + this.failedMessages = new LinkedHashMap<>(); + this.messageExceptions = null; + } + + /** + * Constructor for registration of failed messages, with the + * messages that failed as keys, and the thrown exceptions as values. + *

    The messages should be the same that were originally passed + * to the invoked send method. + * @param msg the detail message + * @param cause the root cause from the mail API in use + * @param failedMessages a Map of failed messages as keys and thrown + * exceptions as values + */ + public MailSendException(@Nullable String msg, @Nullable Throwable cause, Map failedMessages) { + super(msg, cause); + this.failedMessages = new LinkedHashMap<>(failedMessages); + this.messageExceptions = failedMessages.values().toArray(new Exception[0]); + } + + /** + * Constructor for registration of failed messages, with the + * messages that failed as keys, and the thrown exceptions as values. + *

    The messages should be the same that were originally passed + * to the invoked send method. + * @param failedMessages a Map of failed messages as keys and thrown + * exceptions as values + */ + public MailSendException(Map failedMessages) { + this(null, null, failedMessages); + } + + + /** + * Return a Map with the failed messages as keys, and the thrown exceptions + * as values. + *

    Note that a general mail server connection failure will not result + * in failed messages being returned here: A message will only be + * contained here if actually sending it was attempted but failed. + *

    The messages will be the same that were originally passed to the + * invoked send method, that is, SimpleMailMessages in case of using + * the generic MailSender interface. + *

    In case of sending MimeMessage instances via JavaMailSender, + * the messages will be of type MimeMessage. + *

    NOTE: This Map will not be available after serialization. + * Use {@link #getMessageExceptions()} in such a scenario, which will + * be available after serialization as well. + * @return the Map of failed messages as keys and thrown exceptions as values + * @see SimpleMailMessage + * @see javax.mail.internet.MimeMessage + */ + public final Map getFailedMessages() { + return this.failedMessages; + } + + /** + * Return an array with thrown message exceptions. + *

    Note that a general mail server connection failure will not result + * in failed messages being returned here: A message will only be + * contained here if actually sending it was attempted but failed. + * @return the array of thrown message exceptions, + * or an empty array if no failed messages + */ + public final Exception[] getMessageExceptions() { + return (this.messageExceptions != null ? this.messageExceptions : new Exception[0]); + } + + + @Override + @Nullable + public String getMessage() { + if (ObjectUtils.isEmpty(this.messageExceptions)) { + return super.getMessage(); + } + else { + StringBuilder sb = new StringBuilder(); + String baseMessage = super.getMessage(); + if (baseMessage != null) { + sb.append(baseMessage).append(". "); + } + sb.append("Failed messages: "); + for (int i = 0; i < this.messageExceptions.length; i++) { + Exception subEx = this.messageExceptions[i]; + sb.append(subEx.toString()); + if (i < this.messageExceptions.length - 1) { + sb.append("; "); + } + } + return sb.toString(); + } + } + + @Override + public String toString() { + if (ObjectUtils.isEmpty(this.messageExceptions)) { + return super.toString(); + } + else { + StringBuilder sb = new StringBuilder(super.toString()); + sb.append("; message exceptions (").append(this.messageExceptions.length).append(") are:"); + for (int i = 0; i < this.messageExceptions.length; i++) { + Exception subEx = this.messageExceptions[i]; + sb.append('\n').append("Failed message ").append(i + 1).append(": "); + sb.append(subEx); + } + return sb.toString(); + } + } + + @Override + public void printStackTrace(PrintStream ps) { + if (ObjectUtils.isEmpty(this.messageExceptions)) { + super.printStackTrace(ps); + } + else { + ps.println(super.toString() + "; message exception details (" + + this.messageExceptions.length + ") are:"); + for (int i = 0; i < this.messageExceptions.length; i++) { + Exception subEx = this.messageExceptions[i]; + ps.println("Failed message " + (i + 1) + ":"); + subEx.printStackTrace(ps); + } + } + } + + @Override + public void printStackTrace(PrintWriter pw) { + if (ObjectUtils.isEmpty(this.messageExceptions)) { + super.printStackTrace(pw); + } + else { + pw.println(super.toString() + "; message exception details (" + + this.messageExceptions.length + ") are:"); + for (int i = 0; i < this.messageExceptions.length; i++) { + Exception subEx = this.messageExceptions[i]; + pw.println("Failed message " + (i + 1) + ":"); + subEx.printStackTrace(pw); + } + } + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/mail/MailSender.java b/spring-context-support/src/main/java/org/springframework/mail/MailSender.java new file mode 100644 index 0000000..6fd8265 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/mail/MailSender.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.mail; + +/** + * This interface defines a strategy for sending simple mails. Can be + * implemented for a variety of mailing systems due to the simple requirements. + * For richer functionality like MIME messages, consider JavaMailSender. + * + *

    Allows for easy testing of clients, as it does not depend on JavaMail's + * infrastructure classes: no mocking of JavaMail Session or Transport necessary. + * + * @author Dmitriy Kopylenko + * @author Juergen Hoeller + * @since 10.09.2003 + * @see org.springframework.mail.javamail.JavaMailSender + */ +public interface MailSender { + + /** + * Send the given simple mail message. + * @param simpleMessage the message to send + * @throws MailParseException in case of failure when parsing the message + * @throws MailAuthenticationException in case of authentication failure + * @throws MailSendException in case of failure when sending the message + */ + void send(SimpleMailMessage simpleMessage) throws MailException; + + /** + * Send the given array of simple mail messages in batch. + * @param simpleMessages the messages to send + * @throws MailParseException in case of failure when parsing a message + * @throws MailAuthenticationException in case of authentication failure + * @throws MailSendException in case of failure when sending a message + */ + void send(SimpleMailMessage... simpleMessages) throws MailException; + +} diff --git a/spring-context-support/src/main/java/org/springframework/mail/SimpleMailMessage.java b/spring-context-support/src/main/java/org/springframework/mail/SimpleMailMessage.java new file mode 100644 index 0000000..95268a2 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/mail/SimpleMailMessage.java @@ -0,0 +1,282 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.mail; + +import java.io.Serializable; +import java.util.Date; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Models a simple mail message, including data such as the from, to, cc, subject, + * and text fields. + * + *

    Consider {@code JavaMailSender} and JavaMail {@code MimeMessages} for creating + * more sophisticated messages, for example messages with attachments, special + * character encodings, or personal names that accompany mail addresses. + * + * @author Dmitriy Kopylenko + * @author Juergen Hoeller + * @since 10.09.2003 + * @see MailSender + * @see org.springframework.mail.javamail.JavaMailSender + * @see org.springframework.mail.javamail.MimeMessagePreparator + * @see org.springframework.mail.javamail.MimeMessageHelper + * @see org.springframework.mail.javamail.MimeMailMessage + */ +@SuppressWarnings("serial") +public class SimpleMailMessage implements MailMessage, Serializable { + + @Nullable + private String from; + + @Nullable + private String replyTo; + + @Nullable + private String[] to; + + @Nullable + private String[] cc; + + @Nullable + private String[] bcc; + + @Nullable + private Date sentDate; + + @Nullable + private String subject; + + @Nullable + private String text; + + + /** + * Create a new {@code SimpleMailMessage}. + */ + public SimpleMailMessage() { + } + + /** + * Copy constructor for creating a new {@code SimpleMailMessage} from the state + * of an existing {@code SimpleMailMessage} instance. + */ + public SimpleMailMessage(SimpleMailMessage original) { + Assert.notNull(original, "'original' message argument must not be null"); + this.from = original.getFrom(); + this.replyTo = original.getReplyTo(); + this.to = copyOrNull(original.getTo()); + this.cc = copyOrNull(original.getCc()); + this.bcc = copyOrNull(original.getBcc()); + this.sentDate = original.getSentDate(); + this.subject = original.getSubject(); + this.text = original.getText(); + } + + + @Override + public void setFrom(String from) { + this.from = from; + } + + @Nullable + public String getFrom() { + return this.from; + } + + @Override + public void setReplyTo(String replyTo) { + this.replyTo = replyTo; + } + + @Nullable + public String getReplyTo() { + return this.replyTo; + } + + @Override + public void setTo(String to) { + this.to = new String[] {to}; + } + + @Override + public void setTo(String... to) { + this.to = to; + } + + @Nullable + public String[] getTo() { + return this.to; + } + + @Override + public void setCc(String cc) { + this.cc = new String[] {cc}; + } + + @Override + public void setCc(String... cc) { + this.cc = cc; + } + + @Nullable + public String[] getCc() { + return this.cc; + } + + @Override + public void setBcc(String bcc) { + this.bcc = new String[] {bcc}; + } + + @Override + public void setBcc(String... bcc) { + this.bcc = bcc; + } + + @Nullable + public String[] getBcc() { + return this.bcc; + } + + @Override + public void setSentDate(Date sentDate) { + this.sentDate = sentDate; + } + + @Nullable + public Date getSentDate() { + return this.sentDate; + } + + @Override + public void setSubject(String subject) { + this.subject = subject; + } + + @Nullable + public String getSubject() { + return this.subject; + } + + @Override + public void setText(String text) { + this.text = text; + } + + @Nullable + public String getText() { + return this.text; + } + + + /** + * Copy the contents of this message to the given target message. + * @param target the {@code MailMessage} to copy to + */ + public void copyTo(MailMessage target) { + Assert.notNull(target, "'target' MailMessage must not be null"); + if (getFrom() != null) { + target.setFrom(getFrom()); + } + if (getReplyTo() != null) { + target.setReplyTo(getReplyTo()); + } + if (getTo() != null) { + target.setTo(copy(getTo())); + } + if (getCc() != null) { + target.setCc(copy(getCc())); + } + if (getBcc() != null) { + target.setBcc(copy(getBcc())); + } + if (getSentDate() != null) { + target.setSentDate(getSentDate()); + } + if (getSubject() != null) { + target.setSubject(getSubject()); + } + if (getText() != null) { + target.setText(getText()); + } + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof SimpleMailMessage)) { + return false; + } + SimpleMailMessage otherMessage = (SimpleMailMessage) other; + return (ObjectUtils.nullSafeEquals(this.from, otherMessage.from) && + ObjectUtils.nullSafeEquals(this.replyTo, otherMessage.replyTo) && + ObjectUtils.nullSafeEquals(this.to, otherMessage.to) && + ObjectUtils.nullSafeEquals(this.cc, otherMessage.cc) && + ObjectUtils.nullSafeEquals(this.bcc, otherMessage.bcc) && + ObjectUtils.nullSafeEquals(this.sentDate, otherMessage.sentDate) && + ObjectUtils.nullSafeEquals(this.subject, otherMessage.subject) && + ObjectUtils.nullSafeEquals(this.text, otherMessage.text)); + } + + @Override + public int hashCode() { + int hashCode = ObjectUtils.nullSafeHashCode(this.from); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.replyTo); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.to); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.cc); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.bcc); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.sentDate); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.subject); + return hashCode; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("SimpleMailMessage: "); + sb.append("from=").append(this.from).append("; "); + sb.append("replyTo=").append(this.replyTo).append("; "); + sb.append("to=").append(StringUtils.arrayToCommaDelimitedString(this.to)).append("; "); + sb.append("cc=").append(StringUtils.arrayToCommaDelimitedString(this.cc)).append("; "); + sb.append("bcc=").append(StringUtils.arrayToCommaDelimitedString(this.bcc)).append("; "); + sb.append("sentDate=").append(this.sentDate).append("; "); + sb.append("subject=").append(this.subject).append("; "); + sb.append("text=").append(this.text); + return sb.toString(); + } + + + @Nullable + private static String[] copyOrNull(@Nullable String[] state) { + if (state == null) { + return null; + } + return copy(state); + } + + private static String[] copy(String[] state) { + return state.clone(); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/mail/javamail/ConfigurableMimeFileTypeMap.java b/spring-context-support/src/main/java/org/springframework/mail/javamail/ConfigurableMimeFileTypeMap.java new file mode 100644 index 0000000..ff6921b --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/mail/javamail/ConfigurableMimeFileTypeMap.java @@ -0,0 +1,183 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.mail.javamail; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +import javax.activation.FileTypeMap; +import javax.activation.MimetypesFileTypeMap; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; + +/** + * Spring-configurable {@code FileTypeMap} implementation that will read + * MIME type to file extension mappings from a standard JavaMail MIME type + * mapping file, using a standard {@code MimetypesFileTypeMap} underneath. + * + *

    The mapping file should be in the following format, as specified by the + * Java Activation Framework: + * + *

    + * # map text/html to .htm and .html files
    + * text/html  html htm HTML HTM
    + * + * Lines starting with {@code #} are treated as comments and are ignored. All + * other lines are treated as mappings. Each mapping line should contain the MIME + * type as the first entry and then each file extension to map to that MIME type + * as subsequent entries. Each entry is separated by spaces or tabs. + * + *

    By default, the mappings in the {@code mime.types} file located in the + * same package as this class are used, which cover many common file extensions + * (in contrast to the out-of-the-box mappings in {@code activation.jar}). + * This can be overridden using the {@code mappingLocation} property. + * + *

    Additional mappings can be added via the {@code mappings} bean property, + * as lines that follow the {@code mime.types} file format. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see #setMappingLocation + * @see #setMappings + * @see javax.activation.MimetypesFileTypeMap + */ +public class ConfigurableMimeFileTypeMap extends FileTypeMap implements InitializingBean { + + /** + * The {@code Resource} to load the mapping file from. + */ + private Resource mappingLocation = new ClassPathResource("mime.types", getClass()); + + /** + * Used to configure additional mappings. + */ + @Nullable + private String[] mappings; + + /** + * The delegate FileTypeMap, compiled from the mappings in the mapping file + * and the entries in the {@code mappings} property. + */ + @Nullable + private FileTypeMap fileTypeMap; + + + /** + * Specify the {@code Resource} from which mappings are loaded. + *

    Needs to follow the {@code mime.types} file format, as specified + * by the Java Activation Framework, containing lines such as:
    + * {@code text/html html htm HTML HTM} + */ + public void setMappingLocation(Resource mappingLocation) { + this.mappingLocation = mappingLocation; + } + + /** + * Specify additional MIME type mappings as lines that follow the + * {@code mime.types} file format, as specified by the + * Java Activation Framework. For example:
    + * {@code text/html html htm HTML HTM} + */ + public void setMappings(String... mappings) { + this.mappings = mappings; + } + + + /** + * Creates the final merged mapping set. + */ + @Override + public void afterPropertiesSet() { + getFileTypeMap(); + } + + /** + * Return the delegate FileTypeMap, compiled from the mappings in the mapping file + * and the entries in the {@code mappings} property. + * @see #setMappingLocation + * @see #setMappings + * @see #createFileTypeMap + */ + protected final FileTypeMap getFileTypeMap() { + if (this.fileTypeMap == null) { + try { + this.fileTypeMap = createFileTypeMap(this.mappingLocation, this.mappings); + } + catch (IOException ex) { + throw new IllegalStateException( + "Could not load specified MIME type mapping file: " + this.mappingLocation, ex); + } + } + return this.fileTypeMap; + } + + /** + * Compile a {@link FileTypeMap} from the mappings in the given mapping file + * and the given mapping entries. + *

    The default implementation creates an Activation Framework {@link MimetypesFileTypeMap}, + * passing in an InputStream from the mapping resource (if any) and registering + * the mapping lines programmatically. + * @param mappingLocation a {@code mime.types} mapping resource (can be {@code null}) + * @param mappings an array of MIME type mapping lines (can be {@code null}) + * @return the compiled FileTypeMap + * @throws IOException if resource access failed + * @see javax.activation.MimetypesFileTypeMap#MimetypesFileTypeMap(java.io.InputStream) + * @see javax.activation.MimetypesFileTypeMap#addMimeTypes(String) + */ + protected FileTypeMap createFileTypeMap(@Nullable Resource mappingLocation, @Nullable String[] mappings) throws IOException { + MimetypesFileTypeMap fileTypeMap = null; + if (mappingLocation != null) { + try (InputStream is = mappingLocation.getInputStream()) { + fileTypeMap = new MimetypesFileTypeMap(is); + } + } + else { + fileTypeMap = new MimetypesFileTypeMap(); + } + if (mappings != null) { + for (String mapping : mappings) { + fileTypeMap.addMimeTypes(mapping); + } + } + return fileTypeMap; + } + + + /** + * Delegates to the underlying FileTypeMap. + * @see #getFileTypeMap() + */ + @Override + public String getContentType(File file) { + return getFileTypeMap().getContentType(file); + } + + /** + * Delegates to the underlying FileTypeMap. + * @see #getFileTypeMap() + */ + @Override + public String getContentType(String fileName) { + return getFileTypeMap().getContentType(fileName); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/mail/javamail/InternetAddressEditor.java b/spring-context-support/src/main/java/org/springframework/mail/javamail/InternetAddressEditor.java new file mode 100644 index 0000000..7738328 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/mail/javamail/InternetAddressEditor.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.mail.javamail; + +import java.beans.PropertyEditorSupport; + +import javax.mail.internet.AddressException; +import javax.mail.internet.InternetAddress; + +import org.springframework.util.StringUtils; + +/** + * Editor for {@code java.mail.internet.InternetAddress}, + * to directly populate an InternetAddress property. + * + *

    Expects the same syntax as InternetAddress's constructor with + * a String argument. Converts empty Strings into null values. + * + * @author Juergen Hoeller + * @since 1.2.3 + * @see javax.mail.internet.InternetAddress + */ +public class InternetAddressEditor extends PropertyEditorSupport { + + @Override + public void setAsText(String text) throws IllegalArgumentException { + if (StringUtils.hasText(text)) { + try { + setValue(new InternetAddress(text)); + } + catch (AddressException ex) { + throw new IllegalArgumentException("Could not parse mail address: " + ex.getMessage()); + } + } + else { + setValue(null); + } + } + + @Override + public String getAsText() { + InternetAddress value = (InternetAddress) getValue(); + return (value != null ? value.toUnicodeString() : ""); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailSender.java b/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailSender.java new file mode 100644 index 0000000..f810be6 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailSender.java @@ -0,0 +1,143 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.mail.javamail; + +import java.io.InputStream; + +import javax.mail.internet.MimeMessage; + +import org.springframework.mail.MailException; +import org.springframework.mail.MailSender; + +/** + * Extended {@link org.springframework.mail.MailSender} interface for JavaMail, + * supporting MIME messages both as direct arguments and through preparation + * callbacks. Typically used in conjunction with the {@link MimeMessageHelper} + * class for convenient creation of JavaMail {@link MimeMessage MimeMessages}, + * including attachments etc. + * + *

    Clients should talk to the mail sender through this interface if they need + * mail functionality beyond {@link org.springframework.mail.SimpleMailMessage}. + * The production implementation is {@link JavaMailSenderImpl}; for testing, + * mocks can be created based on this interface. Clients will typically receive + * the JavaMailSender reference through dependency injection. + * + *

    The recommended way of using this interface is the {@link MimeMessagePreparator} + * mechanism, possibly using a {@link MimeMessageHelper} for populating the message. + * See {@link MimeMessageHelper MimeMessageHelper's javadoc} for an example. + * + *

    The entire JavaMail {@link javax.mail.Session} management is abstracted + * by the JavaMailSender. Client code should not deal with a Session in any way, + * rather leave the entire JavaMail configuration and resource handling to the + * JavaMailSender implementation. This also increases testability. + * + *

    A JavaMailSender client is not as easy to test as a plain + * {@link org.springframework.mail.MailSender} client, but still straightforward + * compared to traditional JavaMail code: Just let {@link #createMimeMessage()} + * return a plain {@link MimeMessage} created with a + * {@code Session.getInstance(new Properties())} call, and check the passed-in + * messages in your mock implementations of the various {@code send} methods. + * + * @author Juergen Hoeller + * @since 07.10.2003 + * @see javax.mail.internet.MimeMessage + * @see javax.mail.Session + * @see JavaMailSenderImpl + * @see MimeMessagePreparator + * @see MimeMessageHelper + */ +public interface JavaMailSender extends MailSender { + + /** + * Create a new JavaMail MimeMessage for the underlying JavaMail Session + * of this sender. Needs to be called to create MimeMessage instances + * that can be prepared by the client and passed to send(MimeMessage). + * @return the new MimeMessage instance + * @see #send(MimeMessage) + * @see #send(MimeMessage[]) + */ + MimeMessage createMimeMessage(); + + /** + * Create a new JavaMail MimeMessage for the underlying JavaMail Session + * of this sender, using the given input stream as the message source. + * @param contentStream the raw MIME input stream for the message + * @return the new MimeMessage instance + * @throws org.springframework.mail.MailParseException + * in case of message creation failure + */ + MimeMessage createMimeMessage(InputStream contentStream) throws MailException; + + /** + * Send the given JavaMail MIME message. + * The message needs to have been created with {@link #createMimeMessage()}. + * @param mimeMessage message to send + * @throws org.springframework.mail.MailAuthenticationException + * in case of authentication failure + * @throws org.springframework.mail.MailSendException + * in case of failure when sending the message + * @see #createMimeMessage + */ + void send(MimeMessage mimeMessage) throws MailException; + + /** + * Send the given array of JavaMail MIME messages in batch. + * The messages need to have been created with {@link #createMimeMessage()}. + * @param mimeMessages messages to send + * @throws org.springframework.mail.MailAuthenticationException + * in case of authentication failure + * @throws org.springframework.mail.MailSendException + * in case of failure when sending a message + * @see #createMimeMessage + */ + void send(MimeMessage... mimeMessages) throws MailException; + + /** + * Send the JavaMail MIME message prepared by the given MimeMessagePreparator. + *

    Alternative way to prepare MimeMessage instances, instead of + * {@link #createMimeMessage()} and {@link #send(MimeMessage)} calls. + * Takes care of proper exception conversion. + * @param mimeMessagePreparator the preparator to use + * @throws org.springframework.mail.MailPreparationException + * in case of failure when preparing the message + * @throws org.springframework.mail.MailParseException + * in case of failure when parsing the message + * @throws org.springframework.mail.MailAuthenticationException + * in case of authentication failure + * @throws org.springframework.mail.MailSendException + * in case of failure when sending the message + */ + void send(MimeMessagePreparator mimeMessagePreparator) throws MailException; + + /** + * Send the JavaMail MIME messages prepared by the given MimeMessagePreparators. + *

    Alternative way to prepare MimeMessage instances, instead of + * {@link #createMimeMessage()} and {@link #send(MimeMessage[])} calls. + * Takes care of proper exception conversion. + * @param mimeMessagePreparators the preparator to use + * @throws org.springframework.mail.MailPreparationException + * in case of failure when preparing a message + * @throws org.springframework.mail.MailParseException + * in case of failure when parsing a message + * @throws org.springframework.mail.MailAuthenticationException + * in case of authentication failure + * @throws org.springframework.mail.MailSendException + * in case of failure when sending a message + */ + void send(MimeMessagePreparator... mimeMessagePreparators) throws MailException; + +} diff --git a/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailSenderImpl.java b/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailSenderImpl.java new file mode 100644 index 0000000..9286d0e --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailSenderImpl.java @@ -0,0 +1,541 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.mail.javamail; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import javax.activation.FileTypeMap; +import javax.mail.Address; +import javax.mail.AuthenticationFailedException; +import javax.mail.MessagingException; +import javax.mail.NoSuchProviderException; +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.internet.MimeMessage; + +import org.springframework.lang.Nullable; +import org.springframework.mail.MailAuthenticationException; +import org.springframework.mail.MailException; +import org.springframework.mail.MailParseException; +import org.springframework.mail.MailPreparationException; +import org.springframework.mail.MailSendException; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.util.Assert; + +/** + * Production implementation of the {@link JavaMailSender} interface, + * supporting both JavaMail {@link MimeMessage MimeMessages} and Spring + * {@link SimpleMailMessage SimpleMailMessages}. Can also be used as a + * plain {@link org.springframework.mail.MailSender} implementation. + * + *

    Allows for defining all settings locally as bean properties. + * Alternatively, a pre-configured JavaMail {@link javax.mail.Session} can be + * specified, possibly pulled from an application server's JNDI environment. + * + *

    Non-default properties in this object will always override the settings + * in the JavaMail {@code Session}. Note that if overriding all values locally, + * there is no added value in setting a pre-configured {@code Session}. + * + * @author Dmitriy Kopylenko + * @author Juergen Hoeller + * @since 10.09.2003 + * @see javax.mail.internet.MimeMessage + * @see javax.mail.Session + * @see #setSession + * @see #setJavaMailProperties + * @see #setHost + * @see #setPort + * @see #setUsername + * @see #setPassword + */ +public class JavaMailSenderImpl implements JavaMailSender { + + /** The default protocol: 'smtp'. */ + public static final String DEFAULT_PROTOCOL = "smtp"; + + /** The default port: -1. */ + public static final int DEFAULT_PORT = -1; + + private static final String HEADER_MESSAGE_ID = "Message-ID"; + + + private Properties javaMailProperties = new Properties(); + + @Nullable + private Session session; + + @Nullable + private String protocol; + + @Nullable + private String host; + + private int port = DEFAULT_PORT; + + @Nullable + private String username; + + @Nullable + private String password; + + @Nullable + private String defaultEncoding; + + @Nullable + private FileTypeMap defaultFileTypeMap; + + + /** + * Create a new instance of the {@code JavaMailSenderImpl} class. + *

    Initializes the {@link #setDefaultFileTypeMap "defaultFileTypeMap"} + * property with a default {@link ConfigurableMimeFileTypeMap}. + */ + public JavaMailSenderImpl() { + ConfigurableMimeFileTypeMap fileTypeMap = new ConfigurableMimeFileTypeMap(); + fileTypeMap.afterPropertiesSet(); + this.defaultFileTypeMap = fileTypeMap; + } + + + /** + * Set JavaMail properties for the {@code Session}. + *

    A new {@code Session} will be created with those properties. + * Use either this method or {@link #setSession}, but not both. + *

    Non-default properties in this instance will override given + * JavaMail properties. + */ + public void setJavaMailProperties(Properties javaMailProperties) { + this.javaMailProperties = javaMailProperties; + synchronized (this) { + this.session = null; + } + } + + /** + * Allow Map access to the JavaMail properties of this sender, + * with the option to add or override specific entries. + *

    Useful for specifying entries directly, for example via + * "javaMailProperties[mail.smtp.auth]". + */ + public Properties getJavaMailProperties() { + return this.javaMailProperties; + } + + /** + * Set the JavaMail {@code Session}, possibly pulled from JNDI. + *

    Default is a new {@code Session} without defaults, that is + * completely configured via this instance's properties. + *

    If using a pre-configured {@code Session}, non-default properties + * in this instance will override the settings in the {@code Session}. + * @see #setJavaMailProperties + */ + public synchronized void setSession(Session session) { + Assert.notNull(session, "Session must not be null"); + this.session = session; + } + + /** + * Return the JavaMail {@code Session}, + * lazily initializing it if hasn't been specified explicitly. + */ + public synchronized Session getSession() { + if (this.session == null) { + this.session = Session.getInstance(this.javaMailProperties); + } + return this.session; + } + + /** + * Set the mail protocol. Default is "smtp". + */ + public void setProtocol(@Nullable String protocol) { + this.protocol = protocol; + } + + /** + * Return the mail protocol. + */ + @Nullable + public String getProtocol() { + return this.protocol; + } + + /** + * Set the mail server host, typically an SMTP host. + *

    Default is the default host of the underlying JavaMail Session. + */ + public void setHost(@Nullable String host) { + this.host = host; + } + + /** + * Return the mail server host. + */ + @Nullable + public String getHost() { + return this.host; + } + + /** + * Set the mail server port. + *

    Default is {@link #DEFAULT_PORT}, letting JavaMail use the default + * SMTP port (25). + */ + public void setPort(int port) { + this.port = port; + } + + /** + * Return the mail server port. + */ + public int getPort() { + return this.port; + } + + /** + * Set the username for the account at the mail host, if any. + *

    Note that the underlying JavaMail {@code Session} has to be + * configured with the property {@code "mail.smtp.auth"} set to + * {@code true}, else the specified username will not be sent to the + * mail server by the JavaMail runtime. If you are not explicitly passing + * in a {@code Session} to use, simply specify this setting via + * {@link #setJavaMailProperties}. + * @see #setSession + * @see #setPassword + */ + public void setUsername(@Nullable String username) { + this.username = username; + } + + /** + * Return the username for the account at the mail host. + */ + @Nullable + public String getUsername() { + return this.username; + } + + /** + * Set the password for the account at the mail host, if any. + *

    Note that the underlying JavaMail {@code Session} has to be + * configured with the property {@code "mail.smtp.auth"} set to + * {@code true}, else the specified password will not be sent to the + * mail server by the JavaMail runtime. If you are not explicitly passing + * in a {@code Session} to use, simply specify this setting via + * {@link #setJavaMailProperties}. + * @see #setSession + * @see #setUsername + */ + public void setPassword(@Nullable String password) { + this.password = password; + } + + /** + * Return the password for the account at the mail host. + */ + @Nullable + public String getPassword() { + return this.password; + } + + /** + * Set the default encoding to use for {@link MimeMessage MimeMessages} + * created by this instance. + *

    Such an encoding will be auto-detected by {@link MimeMessageHelper}. + */ + public void setDefaultEncoding(@Nullable String defaultEncoding) { + this.defaultEncoding = defaultEncoding; + } + + /** + * Return the default encoding for {@link MimeMessage MimeMessages}, + * or {@code null} if none. + */ + @Nullable + public String getDefaultEncoding() { + return this.defaultEncoding; + } + + /** + * Set the default Java Activation {@link FileTypeMap} to use for + * {@link MimeMessage MimeMessages} created by this instance. + *

    A {@code FileTypeMap} specified here will be autodetected by + * {@link MimeMessageHelper}, avoiding the need to specify the + * {@code FileTypeMap} for each {@code MimeMessageHelper} instance. + *

    For example, you can specify a custom instance of Spring's + * {@link ConfigurableMimeFileTypeMap} here. If not explicitly specified, + * a default {@code ConfigurableMimeFileTypeMap} will be used, containing + * an extended set of MIME type mappings (as defined by the + * {@code mime.types} file contained in the Spring jar). + * @see MimeMessageHelper#setFileTypeMap + */ + public void setDefaultFileTypeMap(@Nullable FileTypeMap defaultFileTypeMap) { + this.defaultFileTypeMap = defaultFileTypeMap; + } + + /** + * Return the default Java Activation {@link FileTypeMap} for + * {@link MimeMessage MimeMessages}, or {@code null} if none. + */ + @Nullable + public FileTypeMap getDefaultFileTypeMap() { + return this.defaultFileTypeMap; + } + + + //--------------------------------------------------------------------- + // Implementation of MailSender + //--------------------------------------------------------------------- + + @Override + public void send(SimpleMailMessage simpleMessage) throws MailException { + send(new SimpleMailMessage[] {simpleMessage}); + } + + @Override + public void send(SimpleMailMessage... simpleMessages) throws MailException { + List mimeMessages = new ArrayList<>(simpleMessages.length); + for (SimpleMailMessage simpleMessage : simpleMessages) { + MimeMailMessage message = new MimeMailMessage(createMimeMessage()); + simpleMessage.copyTo(message); + mimeMessages.add(message.getMimeMessage()); + } + doSend(mimeMessages.toArray(new MimeMessage[0]), simpleMessages); + } + + + //--------------------------------------------------------------------- + // Implementation of JavaMailSender + //--------------------------------------------------------------------- + + /** + * This implementation creates a SmartMimeMessage, holding the specified + * default encoding and default FileTypeMap. This special defaults-carrying + * message will be autodetected by {@link MimeMessageHelper}, which will use + * the carried encoding and FileTypeMap unless explicitly overridden. + * @see #setDefaultEncoding + * @see #setDefaultFileTypeMap + */ + @Override + public MimeMessage createMimeMessage() { + return new SmartMimeMessage(getSession(), getDefaultEncoding(), getDefaultFileTypeMap()); + } + + @Override + public MimeMessage createMimeMessage(InputStream contentStream) throws MailException { + try { + return new MimeMessage(getSession(), contentStream); + } + catch (Exception ex) { + throw new MailParseException("Could not parse raw MIME content", ex); + } + } + + @Override + public void send(MimeMessage mimeMessage) throws MailException { + send(new MimeMessage[] {mimeMessage}); + } + + @Override + public void send(MimeMessage... mimeMessages) throws MailException { + doSend(mimeMessages, null); + } + + @Override + public void send(MimeMessagePreparator mimeMessagePreparator) throws MailException { + send(new MimeMessagePreparator[] {mimeMessagePreparator}); + } + + @Override + public void send(MimeMessagePreparator... mimeMessagePreparators) throws MailException { + try { + List mimeMessages = new ArrayList<>(mimeMessagePreparators.length); + for (MimeMessagePreparator preparator : mimeMessagePreparators) { + MimeMessage mimeMessage = createMimeMessage(); + preparator.prepare(mimeMessage); + mimeMessages.add(mimeMessage); + } + send(mimeMessages.toArray(new MimeMessage[0])); + } + catch (MailException ex) { + throw ex; + } + catch (MessagingException ex) { + throw new MailParseException(ex); + } + catch (Exception ex) { + throw new MailPreparationException(ex); + } + } + + /** + * Validate that this instance can connect to the server that it is configured + * for. Throws a {@link MessagingException} if the connection attempt failed. + */ + public void testConnection() throws MessagingException { + Transport transport = null; + try { + transport = connectTransport(); + } + finally { + if (transport != null) { + transport.close(); + } + } + } + + /** + * Actually send the given array of MimeMessages via JavaMail. + * @param mimeMessages the MimeMessage objects to send + * @param originalMessages corresponding original message objects + * that the MimeMessages have been created from (with same array + * length and indices as the "mimeMessages" array), if any + * @throws org.springframework.mail.MailAuthenticationException + * in case of authentication failure + * @throws org.springframework.mail.MailSendException + * in case of failure when sending a message + */ + protected void doSend(MimeMessage[] mimeMessages, @Nullable Object[] originalMessages) throws MailException { + Map failedMessages = new LinkedHashMap<>(); + Transport transport = null; + + try { + for (int i = 0; i < mimeMessages.length; i++) { + + // Check transport connection first... + if (transport == null || !transport.isConnected()) { + if (transport != null) { + try { + transport.close(); + } + catch (Exception ex) { + // Ignore - we're reconnecting anyway + } + transport = null; + } + try { + transport = connectTransport(); + } + catch (AuthenticationFailedException ex) { + throw new MailAuthenticationException(ex); + } + catch (Exception ex) { + // Effectively, all remaining messages failed... + for (int j = i; j < mimeMessages.length; j++) { + Object original = (originalMessages != null ? originalMessages[j] : mimeMessages[j]); + failedMessages.put(original, ex); + } + throw new MailSendException("Mail server connection failed", ex, failedMessages); + } + } + + // Send message via current transport... + MimeMessage mimeMessage = mimeMessages[i]; + try { + if (mimeMessage.getSentDate() == null) { + mimeMessage.setSentDate(new Date()); + } + String messageId = mimeMessage.getMessageID(); + mimeMessage.saveChanges(); + if (messageId != null) { + // Preserve explicitly specified message id... + mimeMessage.setHeader(HEADER_MESSAGE_ID, messageId); + } + Address[] addresses = mimeMessage.getAllRecipients(); + transport.sendMessage(mimeMessage, (addresses != null ? addresses : new Address[0])); + } + catch (Exception ex) { + Object original = (originalMessages != null ? originalMessages[i] : mimeMessage); + failedMessages.put(original, ex); + } + } + } + finally { + try { + if (transport != null) { + transport.close(); + } + } + catch (Exception ex) { + if (!failedMessages.isEmpty()) { + throw new MailSendException("Failed to close server connection after message failures", ex, + failedMessages); + } + else { + throw new MailSendException("Failed to close server connection after message sending", ex); + } + } + } + + if (!failedMessages.isEmpty()) { + throw new MailSendException(failedMessages); + } + } + + /** + * Obtain and connect a Transport from the underlying JavaMail Session, + * passing in the specified host, port, username, and password. + * @return the connected Transport object + * @throws MessagingException if the connect attempt failed + * @since 4.1.2 + * @see #getTransport + * @see #getHost() + * @see #getPort() + * @see #getUsername() + * @see #getPassword() + */ + protected Transport connectTransport() throws MessagingException { + String username = getUsername(); + String password = getPassword(); + if ("".equals(username)) { // probably from a placeholder + username = null; + if ("".equals(password)) { // in conjunction with "" username, this means no password to use + password = null; + } + } + + Transport transport = getTransport(getSession()); + transport.connect(getHost(), getPort(), username, password); + return transport; + } + + /** + * Obtain a Transport object from the given JavaMail Session, + * using the configured protocol. + *

    Can be overridden in subclasses, e.g. to return a mock Transport object. + * @see javax.mail.Session#getTransport(String) + * @see #getSession() + * @see #getProtocol() + */ + protected Transport getTransport(Session session) throws NoSuchProviderException { + String protocol = getProtocol(); + if (protocol == null) { + protocol = session.getProperty("mail.transport.protocol"); + if (protocol == null) { + protocol = DEFAULT_PROTOCOL; + } + } + return session.getTransport(protocol); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/mail/javamail/MimeMailMessage.java b/spring-context-support/src/main/java/org/springframework/mail/javamail/MimeMailMessage.java new file mode 100644 index 0000000..e62690a --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/mail/javamail/MimeMailMessage.java @@ -0,0 +1,186 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.mail.javamail; + +import java.util.Date; + +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; + +import org.springframework.mail.MailMessage; +import org.springframework.mail.MailParseException; + +/** + * Implementation of the MailMessage interface for a JavaMail MIME message, + * to let message population code interact with a simple message or a MIME + * message through a common interface. + * + *

    Uses a MimeMessageHelper underneath. Can either be created with a + * MimeMessageHelper instance or with a JavaMail MimeMessage instance. + * + * @author Juergen Hoeller + * @since 1.1.5 + * @see MimeMessageHelper + * @see javax.mail.internet.MimeMessage + */ +public class MimeMailMessage implements MailMessage { + + private final MimeMessageHelper helper; + + + /** + * Create a new MimeMailMessage based on the given MimeMessageHelper. + * @param mimeMessageHelper the MimeMessageHelper + */ + public MimeMailMessage(MimeMessageHelper mimeMessageHelper) { + this.helper = mimeMessageHelper; + } + + /** + * Create a new MimeMailMessage based on the given JavaMail MimeMessage. + * @param mimeMessage the JavaMail MimeMessage + */ + public MimeMailMessage(MimeMessage mimeMessage) { + this.helper = new MimeMessageHelper(mimeMessage); + } + + /** + * Return the MimeMessageHelper that this MimeMailMessage is based on. + */ + public final MimeMessageHelper getMimeMessageHelper() { + return this.helper; + } + + /** + * Return the JavaMail MimeMessage that this MimeMailMessage is based on. + */ + public final MimeMessage getMimeMessage() { + return this.helper.getMimeMessage(); + } + + + @Override + public void setFrom(String from) throws MailParseException { + try { + this.helper.setFrom(from); + } + catch (MessagingException ex) { + throw new MailParseException(ex); + } + } + + @Override + public void setReplyTo(String replyTo) throws MailParseException { + try { + this.helper.setReplyTo(replyTo); + } + catch (MessagingException ex) { + throw new MailParseException(ex); + } + } + + @Override + public void setTo(String to) throws MailParseException { + try { + this.helper.setTo(to); + } + catch (MessagingException ex) { + throw new MailParseException(ex); + } + } + + @Override + public void setTo(String... to) throws MailParseException { + try { + this.helper.setTo(to); + } + catch (MessagingException ex) { + throw new MailParseException(ex); + } + } + + @Override + public void setCc(String cc) throws MailParseException { + try { + this.helper.setCc(cc); + } + catch (MessagingException ex) { + throw new MailParseException(ex); + } + } + + @Override + public void setCc(String... cc) throws MailParseException { + try { + this.helper.setCc(cc); + } + catch (MessagingException ex) { + throw new MailParseException(ex); + } + } + + @Override + public void setBcc(String bcc) throws MailParseException { + try { + this.helper.setBcc(bcc); + } + catch (MessagingException ex) { + throw new MailParseException(ex); + } + } + + @Override + public void setBcc(String... bcc) throws MailParseException { + try { + this.helper.setBcc(bcc); + } + catch (MessagingException ex) { + throw new MailParseException(ex); + } + } + + @Override + public void setSentDate(Date sentDate) throws MailParseException { + try { + this.helper.setSentDate(sentDate); + } + catch (MessagingException ex) { + throw new MailParseException(ex); + } + } + + @Override + public void setSubject(String subject) throws MailParseException { + try { + this.helper.setSubject(subject); + } + catch (MessagingException ex) { + throw new MailParseException(ex); + } + } + + @Override + public void setText(String text) throws MailParseException { + try { + this.helper.setText(text); + } + catch (MessagingException ex) { + throw new MailParseException(ex); + } + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/mail/javamail/MimeMessageHelper.java b/spring-context-support/src/main/java/org/springframework/mail/javamail/MimeMessageHelper.java new file mode 100644 index 0000000..f795390 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/mail/javamail/MimeMessageHelper.java @@ -0,0 +1,1137 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.mail.javamail; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.util.Date; + +import javax.activation.DataHandler; +import javax.activation.DataSource; +import javax.activation.FileDataSource; +import javax.activation.FileTypeMap; +import javax.mail.BodyPart; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.internet.AddressException; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; +import javax.mail.internet.MimePart; +import javax.mail.internet.MimeUtility; + +import org.springframework.core.io.InputStreamSource; +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Helper class for populating a {@link javax.mail.internet.MimeMessage}. + * + *

    Mirrors the simple setters of {@link org.springframework.mail.SimpleMailMessage}, + * directly applying the values to the underlying MimeMessage. Allows for defining + * a character encoding for the entire message, automatically applied by all methods + * of this helper class. + * + *

    Offers support for HTML text content, inline elements such as images, and typical + * mail attachments. Also supports personal names that accompany mail addresses. Note that + * advanced settings can still be applied directly to the underlying MimeMessage object! + * + *

    Typically used in {@link MimeMessagePreparator} implementations or + * {@link JavaMailSender} client code: simply instantiating it as a MimeMessage wrapper, + * invoking setters on the wrapper, using the underlying MimeMessage for mail sending. + * Also used internally by {@link JavaMailSenderImpl}. + * + *

    Sample code for an HTML mail with an inline image and a PDF attachment: + * + *

    + * mailSender.send(new MimeMessagePreparator() {
    + *   public void prepare(MimeMessage mimeMessage) throws MessagingException {
    + *     MimeMessageHelper message = new MimeMessageHelper(mimeMessage, true, "UTF-8");
    + *     message.setFrom("me@mail.com");
    + *     message.setTo("you@mail.com");
    + *     message.setSubject("my subject");
    + *     message.setText("my text <img src='cid:myLogo'>", true);
    + *     message.addInline("myLogo", new ClassPathResource("img/mylogo.gif"));
    + *     message.addAttachment("myDocument.pdf", new ClassPathResource("doc/myDocument.pdf"));
    + *   }
    + * });
    + * + * Consider using {@link MimeMailMessage} (which implements the common + * {@link org.springframework.mail.MailMessage} interface, just like + * {@link org.springframework.mail.SimpleMailMessage}) on top of this helper, + * in order to let message population code interact with a simple message + * or a MIME message through a common interface. + * + *

    Warning regarding multipart mails: Simple MIME messages that + * just contain HTML text but no inline elements or attachments will work on + * more or less any email client that is capable of HTML rendering. However, + * inline elements and attachments are still a major compatibility issue + * between email clients: It's virtually impossible to get inline elements + * and attachments working across Microsoft Outlook, Lotus Notes and Mac Mail. + * Consider choosing a specific multipart mode for your needs: The javadoc + * on the MULTIPART_MODE constants contains more detailed information. + * + * @author Juergen Hoeller + * @since 19.01.2004 + * @see #setText(String, boolean) + * @see #setText(String, String) + * @see #addInline(String, org.springframework.core.io.Resource) + * @see #addAttachment(String, org.springframework.core.io.InputStreamSource) + * @see #MULTIPART_MODE_MIXED_RELATED + * @see #MULTIPART_MODE_RELATED + * @see #getMimeMessage() + * @see JavaMailSender + */ +public class MimeMessageHelper { + + /** + * Constant indicating a non-multipart message. + */ + public static final int MULTIPART_MODE_NO = 0; + + /** + * Constant indicating a multipart message with a single root multipart + * element of type "mixed". Texts, inline elements and attachements + * will all get added to that root element. + *

    This was Spring 1.0's default behavior. It is known to work properly + * on Outlook. However, other mail clients tend to misinterpret inline + * elements as attachments and/or show attachments inline as well. + */ + public static final int MULTIPART_MODE_MIXED = 1; + + /** + * Constant indicating a multipart message with a single root multipart + * element of type "related". Texts, inline elements and attachements + * will all get added to that root element. + *

    This was the default behavior from Spring 1.1 up to 1.2 final. + * This is the "Microsoft multipart mode", as natively sent by Outlook. + * It is known to work properly on Outlook, Outlook Express, Yahoo Mail, and + * to a large degree also on Mac Mail (with an additional attachment listed + * for an inline element, despite the inline element also shown inline). + * Does not work properly on Lotus Notes (attachments won't be shown there). + */ + public static final int MULTIPART_MODE_RELATED = 2; + + /** + * Constant indicating a multipart message with a root multipart element + * "mixed" plus a nested multipart element of type "related". Texts and + * inline elements will get added to the nested "related" element, + * while attachments will get added to the "mixed" root element. + *

    This is the default since Spring 1.2.1. This is arguably the most correct + * MIME structure, according to the MIME spec: It is known to work properly + * on Outlook, Outlook Express, Yahoo Mail, and Lotus Notes. Does not work + * properly on Mac Mail. If you target Mac Mail or experience issues with + * specific mails on Outlook, consider using MULTIPART_MODE_RELATED instead. + */ + public static final int MULTIPART_MODE_MIXED_RELATED = 3; + + + private static final String MULTIPART_SUBTYPE_MIXED = "mixed"; + + private static final String MULTIPART_SUBTYPE_RELATED = "related"; + + private static final String MULTIPART_SUBTYPE_ALTERNATIVE = "alternative"; + + private static final String CONTENT_TYPE_ALTERNATIVE = "text/alternative"; + + private static final String CONTENT_TYPE_HTML = "text/html"; + + private static final String CONTENT_TYPE_CHARSET_SUFFIX = ";charset="; + + private static final String HEADER_PRIORITY = "X-Priority"; + + + private final MimeMessage mimeMessage; + + @Nullable + private MimeMultipart rootMimeMultipart; + + @Nullable + private MimeMultipart mimeMultipart; + + @Nullable + private final String encoding; + + private FileTypeMap fileTypeMap; + + private boolean encodeFilenames = false; + + private boolean validateAddresses = false; + + + /** + * Create a new MimeMessageHelper for the given MimeMessage, + * assuming a simple text message (no multipart content, + * i.e. no alternative texts and no inline elements or attachments). + *

    The character encoding for the message will be taken from + * the passed-in MimeMessage object, if carried there. Else, + * JavaMail's default encoding will be used. + * @param mimeMessage the mime message to work on + * @see #MimeMessageHelper(javax.mail.internet.MimeMessage, boolean) + * @see #getDefaultEncoding(javax.mail.internet.MimeMessage) + * @see JavaMailSenderImpl#setDefaultEncoding + */ + public MimeMessageHelper(MimeMessage mimeMessage) { + this(mimeMessage, null); + } + + /** + * Create a new MimeMessageHelper for the given MimeMessage, + * assuming a simple text message (no multipart content, + * i.e. no alternative texts and no inline elements or attachments). + * @param mimeMessage the mime message to work on + * @param encoding the character encoding to use for the message + * @see #MimeMessageHelper(javax.mail.internet.MimeMessage, boolean) + */ + public MimeMessageHelper(MimeMessage mimeMessage, @Nullable String encoding) { + this.mimeMessage = mimeMessage; + this.encoding = (encoding != null ? encoding : getDefaultEncoding(mimeMessage)); + this.fileTypeMap = getDefaultFileTypeMap(mimeMessage); + } + + /** + * Create a new MimeMessageHelper for the given MimeMessage, + * in multipart mode (supporting alternative texts, inline + * elements and attachments) if requested. + *

    Consider using the MimeMessageHelper constructor that + * takes a multipartMode argument to choose a specific multipart + * mode other than MULTIPART_MODE_MIXED_RELATED. + *

    The character encoding for the message will be taken from + * the passed-in MimeMessage object, if carried there. Else, + * JavaMail's default encoding will be used. + * @param mimeMessage the mime message to work on + * @param multipart whether to create a multipart message that + * supports alternative texts, inline elements and attachments + * (corresponds to MULTIPART_MODE_MIXED_RELATED) + * @throws MessagingException if multipart creation failed + * @see #MimeMessageHelper(javax.mail.internet.MimeMessage, int) + * @see #getDefaultEncoding(javax.mail.internet.MimeMessage) + * @see JavaMailSenderImpl#setDefaultEncoding + */ + public MimeMessageHelper(MimeMessage mimeMessage, boolean multipart) throws MessagingException { + this(mimeMessage, multipart, null); + } + + /** + * Create a new MimeMessageHelper for the given MimeMessage, + * in multipart mode (supporting alternative texts, inline + * elements and attachments) if requested. + *

    Consider using the MimeMessageHelper constructor that + * takes a multipartMode argument to choose a specific multipart + * mode other than MULTIPART_MODE_MIXED_RELATED. + * @param mimeMessage the mime message to work on + * @param multipart whether to create a multipart message that + * supports alternative texts, inline elements and attachments + * (corresponds to MULTIPART_MODE_MIXED_RELATED) + * @param encoding the character encoding to use for the message + * @throws MessagingException if multipart creation failed + * @see #MimeMessageHelper(javax.mail.internet.MimeMessage, int, String) + */ + public MimeMessageHelper(MimeMessage mimeMessage, boolean multipart, @Nullable String encoding) + throws MessagingException { + + this(mimeMessage, (multipart ? MULTIPART_MODE_MIXED_RELATED : MULTIPART_MODE_NO), encoding); + } + + /** + * Create a new MimeMessageHelper for the given MimeMessage, + * in multipart mode (supporting alternative texts, inline + * elements and attachments) if requested. + *

    The character encoding for the message will be taken from + * the passed-in MimeMessage object, if carried there. Else, + * JavaMail's default encoding will be used. + * @param mimeMessage the mime message to work on + * @param multipartMode which kind of multipart message to create + * (MIXED, RELATED, MIXED_RELATED, or NO) + * @throws MessagingException if multipart creation failed + * @see #MULTIPART_MODE_NO + * @see #MULTIPART_MODE_MIXED + * @see #MULTIPART_MODE_RELATED + * @see #MULTIPART_MODE_MIXED_RELATED + * @see #getDefaultEncoding(javax.mail.internet.MimeMessage) + * @see JavaMailSenderImpl#setDefaultEncoding + */ + public MimeMessageHelper(MimeMessage mimeMessage, int multipartMode) throws MessagingException { + this(mimeMessage, multipartMode, null); + } + + /** + * Create a new MimeMessageHelper for the given MimeMessage, + * in multipart mode (supporting alternative texts, inline + * elements and attachments) if requested. + * @param mimeMessage the mime message to work on + * @param multipartMode which kind of multipart message to create + * (MIXED, RELATED, MIXED_RELATED, or NO) + * @param encoding the character encoding to use for the message + * @throws MessagingException if multipart creation failed + * @see #MULTIPART_MODE_NO + * @see #MULTIPART_MODE_MIXED + * @see #MULTIPART_MODE_RELATED + * @see #MULTIPART_MODE_MIXED_RELATED + */ + public MimeMessageHelper(MimeMessage mimeMessage, int multipartMode, @Nullable String encoding) + throws MessagingException { + + this.mimeMessage = mimeMessage; + createMimeMultiparts(mimeMessage, multipartMode); + this.encoding = (encoding != null ? encoding : getDefaultEncoding(mimeMessage)); + this.fileTypeMap = getDefaultFileTypeMap(mimeMessage); + } + + + /** + * Return the underlying MimeMessage object. + */ + public final MimeMessage getMimeMessage() { + return this.mimeMessage; + } + + + /** + * Determine the MimeMultipart objects to use, which will be used + * to store attachments on the one hand and text(s) and inline elements + * on the other hand. + *

    Texts and inline elements can either be stored in the root element + * itself (MULTIPART_MODE_MIXED, MULTIPART_MODE_RELATED) or in a nested element + * rather than the root element directly (MULTIPART_MODE_MIXED_RELATED). + *

    By default, the root MimeMultipart element will be of type "mixed" + * (MULTIPART_MODE_MIXED) or "related" (MULTIPART_MODE_RELATED). + * The main multipart element will either be added as nested element of + * type "related" (MULTIPART_MODE_MIXED_RELATED) or be identical to the root + * element itself (MULTIPART_MODE_MIXED, MULTIPART_MODE_RELATED). + * @param mimeMessage the MimeMessage object to add the root MimeMultipart + * object to + * @param multipartMode the multipart mode, as passed into the constructor + * (MIXED, RELATED, MIXED_RELATED, or NO) + * @throws MessagingException if multipart creation failed + * @see #setMimeMultiparts + * @see #MULTIPART_MODE_NO + * @see #MULTIPART_MODE_MIXED + * @see #MULTIPART_MODE_RELATED + * @see #MULTIPART_MODE_MIXED_RELATED + */ + protected void createMimeMultiparts(MimeMessage mimeMessage, int multipartMode) throws MessagingException { + switch (multipartMode) { + case MULTIPART_MODE_NO: + setMimeMultiparts(null, null); + break; + case MULTIPART_MODE_MIXED: + MimeMultipart mixedMultipart = new MimeMultipart(MULTIPART_SUBTYPE_MIXED); + mimeMessage.setContent(mixedMultipart); + setMimeMultiparts(mixedMultipart, mixedMultipart); + break; + case MULTIPART_MODE_RELATED: + MimeMultipart relatedMultipart = new MimeMultipart(MULTIPART_SUBTYPE_RELATED); + mimeMessage.setContent(relatedMultipart); + setMimeMultiparts(relatedMultipart, relatedMultipart); + break; + case MULTIPART_MODE_MIXED_RELATED: + MimeMultipart rootMixedMultipart = new MimeMultipart(MULTIPART_SUBTYPE_MIXED); + mimeMessage.setContent(rootMixedMultipart); + MimeMultipart nestedRelatedMultipart = new MimeMultipart(MULTIPART_SUBTYPE_RELATED); + MimeBodyPart relatedBodyPart = new MimeBodyPart(); + relatedBodyPart.setContent(nestedRelatedMultipart); + rootMixedMultipart.addBodyPart(relatedBodyPart); + setMimeMultiparts(rootMixedMultipart, nestedRelatedMultipart); + break; + default: + throw new IllegalArgumentException("Only multipart modes MIXED_RELATED, RELATED and NO supported"); + } + } + + /** + * Set the given MimeMultipart objects for use by this MimeMessageHelper. + * @param root the root MimeMultipart object, which attachments will be added to; + * or {@code null} to indicate no multipart at all + * @param main the main MimeMultipart object, which text(s) and inline elements + * will be added to (can be the same as the root multipart object, or an element + * nested underneath the root multipart element) + */ + protected final void setMimeMultiparts(@Nullable MimeMultipart root, @Nullable MimeMultipart main) { + this.rootMimeMultipart = root; + this.mimeMultipart = main; + } + + /** + * Return whether this helper is in multipart mode, + * i.e. whether it holds a multipart message. + * @see #MimeMessageHelper(MimeMessage, boolean) + */ + public final boolean isMultipart() { + return (this.rootMimeMultipart != null); + } + + /** + * Return the root MIME "multipart/mixed" object, if any. + * Can be used to manually add attachments. + *

    This will be the direct content of the MimeMessage, + * in case of a multipart mail. + * @throws IllegalStateException if this helper is not in multipart mode + * @see #isMultipart + * @see #getMimeMessage + * @see javax.mail.internet.MimeMultipart#addBodyPart + */ + public final MimeMultipart getRootMimeMultipart() throws IllegalStateException { + if (this.rootMimeMultipart == null) { + throw new IllegalStateException("Not in multipart mode - " + + "create an appropriate MimeMessageHelper via a constructor that takes a 'multipart' flag " + + "if you need to set alternative texts or add inline elements or attachments."); + } + return this.rootMimeMultipart; + } + + /** + * Return the underlying MIME "multipart/related" object, if any. + * Can be used to manually add body parts, inline elements, etc. + *

    This will be nested within the root MimeMultipart, + * in case of a multipart mail. + * @throws IllegalStateException if this helper is not in multipart mode + * @see #isMultipart + * @see #getRootMimeMultipart + * @see javax.mail.internet.MimeMultipart#addBodyPart + */ + public final MimeMultipart getMimeMultipart() throws IllegalStateException { + if (this.mimeMultipart == null) { + throw new IllegalStateException("Not in multipart mode - " + + "create an appropriate MimeMessageHelper via a constructor that takes a 'multipart' flag " + + "if you need to set alternative texts or add inline elements or attachments."); + } + return this.mimeMultipart; + } + + + /** + * Determine the default encoding for the given MimeMessage. + * @param mimeMessage the passed-in MimeMessage + * @return the default encoding associated with the MimeMessage, + * or {@code null} if none found + */ + @Nullable + protected String getDefaultEncoding(MimeMessage mimeMessage) { + if (mimeMessage instanceof SmartMimeMessage) { + return ((SmartMimeMessage) mimeMessage).getDefaultEncoding(); + } + return null; + } + + /** + * Return the specific character encoding used for this message, if any. + */ + @Nullable + public String getEncoding() { + return this.encoding; + } + + /** + * Determine the default Java Activation FileTypeMap for the given MimeMessage. + * @param mimeMessage the passed-in MimeMessage + * @return the default FileTypeMap associated with the MimeMessage, + * or a default ConfigurableMimeFileTypeMap if none found for the message + * @see ConfigurableMimeFileTypeMap + */ + protected FileTypeMap getDefaultFileTypeMap(MimeMessage mimeMessage) { + if (mimeMessage instanceof SmartMimeMessage) { + FileTypeMap fileTypeMap = ((SmartMimeMessage) mimeMessage).getDefaultFileTypeMap(); + if (fileTypeMap != null) { + return fileTypeMap; + } + } + ConfigurableMimeFileTypeMap fileTypeMap = new ConfigurableMimeFileTypeMap(); + fileTypeMap.afterPropertiesSet(); + return fileTypeMap; + } + + /** + * Set the Java Activation Framework {@code FileTypeMap} to use + * for determining the content type of inline content and attachments + * that get added to the message. + *

    The default is the {@code FileTypeMap} that the underlying + * MimeMessage carries, if any, or the Activation Framework's default + * {@code FileTypeMap} instance else. + * @see #addInline + * @see #addAttachment + * @see #getDefaultFileTypeMap(javax.mail.internet.MimeMessage) + * @see JavaMailSenderImpl#setDefaultFileTypeMap + * @see javax.activation.FileTypeMap#getDefaultFileTypeMap + * @see ConfigurableMimeFileTypeMap + */ + public void setFileTypeMap(@Nullable FileTypeMap fileTypeMap) { + this.fileTypeMap = (fileTypeMap != null ? fileTypeMap : getDefaultFileTypeMap(getMimeMessage())); + } + + /** + * Return the {@code FileTypeMap} used by this MimeMessageHelper. + * @see #setFileTypeMap + */ + public FileTypeMap getFileTypeMap() { + return this.fileTypeMap; + } + + + /** + * Set whether to encode attachment filenames passed to this helper's + * {@code #addAttachment} methods. + *

    The default is {@code false} for standard MIME behavior; turn this to + * {@code true} for compatibility with older email clients. On a related note, + * check out JavaMail's {@code mail.mime.encodefilename} system property. + *

    NOTE: The default changed to {@code false} in 5.3, in favor of + * JavaMail's standard {@code mail.mime.encodefilename} system property. + * @since 5.2.9 + * @see #addAttachment(String, DataSource) + * @see MimeBodyPart#setFileName(String) + */ + public void setEncodeFilenames(boolean encodeFilenames) { + this.encodeFilenames = encodeFilenames; + } + + /** + * Return whether to encode attachment filenames passed to this helper's + * {@code #addAttachment} methods. + * @since 5.2.9 + * @see #setEncodeFilenames + */ + public boolean isEncodeFilenames() { + return this.encodeFilenames; + } + + /** + * Set whether to validate all addresses which get passed to this helper. + *

    The default is {@code false}. + * @see #validateAddress + */ + public void setValidateAddresses(boolean validateAddresses) { + this.validateAddresses = validateAddresses; + } + + /** + * Return whether this helper will validate all addresses passed to it. + * @see #setValidateAddresses + */ + public boolean isValidateAddresses() { + return this.validateAddresses; + } + + /** + * Validate the given mail address. + * Called by all of MimeMessageHelper's address setters and adders. + *

    The default implementation invokes {@link InternetAddress#validate()}, + * provided that address validation is activated for the helper instance. + * @param address the address to validate + * @throws AddressException if validation failed + * @see #isValidateAddresses() + * @see javax.mail.internet.InternetAddress#validate() + */ + protected void validateAddress(InternetAddress address) throws AddressException { + if (isValidateAddresses()) { + address.validate(); + } + } + + /** + * Validate all given mail addresses. + *

    The default implementation simply delegates to {@link #validateAddress} + * for each address. + * @param addresses the addresses to validate + * @throws AddressException if validation failed + * @see #validateAddress(InternetAddress) + */ + protected void validateAddresses(InternetAddress[] addresses) throws AddressException { + for (InternetAddress address : addresses) { + validateAddress(address); + } + } + + + public void setFrom(InternetAddress from) throws MessagingException { + Assert.notNull(from, "From address must not be null"); + validateAddress(from); + this.mimeMessage.setFrom(from); + } + + public void setFrom(String from) throws MessagingException { + Assert.notNull(from, "From address must not be null"); + setFrom(parseAddress(from)); + } + + public void setFrom(String from, String personal) throws MessagingException, UnsupportedEncodingException { + Assert.notNull(from, "From address must not be null"); + setFrom(getEncoding() != null ? + new InternetAddress(from, personal, getEncoding()) : new InternetAddress(from, personal)); + } + + public void setReplyTo(InternetAddress replyTo) throws MessagingException { + Assert.notNull(replyTo, "Reply-to address must not be null"); + validateAddress(replyTo); + this.mimeMessage.setReplyTo(new InternetAddress[] {replyTo}); + } + + public void setReplyTo(String replyTo) throws MessagingException { + Assert.notNull(replyTo, "Reply-to address must not be null"); + setReplyTo(parseAddress(replyTo)); + } + + public void setReplyTo(String replyTo, String personal) throws MessagingException, UnsupportedEncodingException { + Assert.notNull(replyTo, "Reply-to address must not be null"); + InternetAddress replyToAddress = (getEncoding() != null) ? + new InternetAddress(replyTo, personal, getEncoding()) : new InternetAddress(replyTo, personal); + setReplyTo(replyToAddress); + } + + + public void setTo(InternetAddress to) throws MessagingException { + Assert.notNull(to, "To address must not be null"); + validateAddress(to); + this.mimeMessage.setRecipient(Message.RecipientType.TO, to); + } + + public void setTo(InternetAddress[] to) throws MessagingException { + Assert.notNull(to, "To address array must not be null"); + validateAddresses(to); + this.mimeMessage.setRecipients(Message.RecipientType.TO, to); + } + + public void setTo(String to) throws MessagingException { + Assert.notNull(to, "To address must not be null"); + setTo(parseAddress(to)); + } + + public void setTo(String[] to) throws MessagingException { + Assert.notNull(to, "To address array must not be null"); + InternetAddress[] addresses = new InternetAddress[to.length]; + for (int i = 0; i < to.length; i++) { + addresses[i] = parseAddress(to[i]); + } + setTo(addresses); + } + + public void addTo(InternetAddress to) throws MessagingException { + Assert.notNull(to, "To address must not be null"); + validateAddress(to); + this.mimeMessage.addRecipient(Message.RecipientType.TO, to); + } + + public void addTo(String to) throws MessagingException { + Assert.notNull(to, "To address must not be null"); + addTo(parseAddress(to)); + } + + public void addTo(String to, String personal) throws MessagingException, UnsupportedEncodingException { + Assert.notNull(to, "To address must not be null"); + addTo(getEncoding() != null ? + new InternetAddress(to, personal, getEncoding()) : + new InternetAddress(to, personal)); + } + + + public void setCc(InternetAddress cc) throws MessagingException { + Assert.notNull(cc, "Cc address must not be null"); + validateAddress(cc); + this.mimeMessage.setRecipient(Message.RecipientType.CC, cc); + } + + public void setCc(InternetAddress[] cc) throws MessagingException { + Assert.notNull(cc, "Cc address array must not be null"); + validateAddresses(cc); + this.mimeMessage.setRecipients(Message.RecipientType.CC, cc); + } + + public void setCc(String cc) throws MessagingException { + Assert.notNull(cc, "Cc address must not be null"); + setCc(parseAddress(cc)); + } + + public void setCc(String[] cc) throws MessagingException { + Assert.notNull(cc, "Cc address array must not be null"); + InternetAddress[] addresses = new InternetAddress[cc.length]; + for (int i = 0; i < cc.length; i++) { + addresses[i] = parseAddress(cc[i]); + } + setCc(addresses); + } + + public void addCc(InternetAddress cc) throws MessagingException { + Assert.notNull(cc, "Cc address must not be null"); + validateAddress(cc); + this.mimeMessage.addRecipient(Message.RecipientType.CC, cc); + } + + public void addCc(String cc) throws MessagingException { + Assert.notNull(cc, "Cc address must not be null"); + addCc(parseAddress(cc)); + } + + public void addCc(String cc, String personal) throws MessagingException, UnsupportedEncodingException { + Assert.notNull(cc, "Cc address must not be null"); + addCc(getEncoding() != null ? + new InternetAddress(cc, personal, getEncoding()) : + new InternetAddress(cc, personal)); + } + + + public void setBcc(InternetAddress bcc) throws MessagingException { + Assert.notNull(bcc, "Bcc address must not be null"); + validateAddress(bcc); + this.mimeMessage.setRecipient(Message.RecipientType.BCC, bcc); + } + + public void setBcc(InternetAddress[] bcc) throws MessagingException { + Assert.notNull(bcc, "Bcc address array must not be null"); + validateAddresses(bcc); + this.mimeMessage.setRecipients(Message.RecipientType.BCC, bcc); + } + + public void setBcc(String bcc) throws MessagingException { + Assert.notNull(bcc, "Bcc address must not be null"); + setBcc(parseAddress(bcc)); + } + + public void setBcc(String[] bcc) throws MessagingException { + Assert.notNull(bcc, "Bcc address array must not be null"); + InternetAddress[] addresses = new InternetAddress[bcc.length]; + for (int i = 0; i < bcc.length; i++) { + addresses[i] = parseAddress(bcc[i]); + } + setBcc(addresses); + } + + public void addBcc(InternetAddress bcc) throws MessagingException { + Assert.notNull(bcc, "Bcc address must not be null"); + validateAddress(bcc); + this.mimeMessage.addRecipient(Message.RecipientType.BCC, bcc); + } + + public void addBcc(String bcc) throws MessagingException { + Assert.notNull(bcc, "Bcc address must not be null"); + addBcc(parseAddress(bcc)); + } + + public void addBcc(String bcc, String personal) throws MessagingException, UnsupportedEncodingException { + Assert.notNull(bcc, "Bcc address must not be null"); + addBcc(getEncoding() != null ? + new InternetAddress(bcc, personal, getEncoding()) : + new InternetAddress(bcc, personal)); + } + + private InternetAddress parseAddress(String address) throws MessagingException { + InternetAddress[] parsed = InternetAddress.parse(address); + if (parsed.length != 1) { + throw new AddressException("Illegal address", address); + } + InternetAddress raw = parsed[0]; + try { + return (getEncoding() != null ? + new InternetAddress(raw.getAddress(), raw.getPersonal(), getEncoding()) : raw); + } + catch (UnsupportedEncodingException ex) { + throw new MessagingException("Failed to parse embedded personal name to correct encoding", ex); + } + } + + + /** + * Set the priority ("X-Priority" header) of the message. + * @param priority the priority value; + * typically between 1 (highest) and 5 (lowest) + * @throws MessagingException in case of errors + */ + public void setPriority(int priority) throws MessagingException { + this.mimeMessage.setHeader(HEADER_PRIORITY, Integer.toString(priority)); + } + + /** + * Set the sent-date of the message. + * @param sentDate the date to set (never {@code null}) + * @throws MessagingException in case of errors + */ + public void setSentDate(Date sentDate) throws MessagingException { + Assert.notNull(sentDate, "Sent date must not be null"); + this.mimeMessage.setSentDate(sentDate); + } + + /** + * Set the subject of the message, using the correct encoding. + * @param subject the subject text + * @throws MessagingException in case of errors + */ + public void setSubject(String subject) throws MessagingException { + Assert.notNull(subject, "Subject must not be null"); + if (getEncoding() != null) { + this.mimeMessage.setSubject(subject, getEncoding()); + } + else { + this.mimeMessage.setSubject(subject); + } + } + + + /** + * Set the given text directly as content in non-multipart mode + * or as default body part in multipart mode. + * Always applies the default content type "text/plain". + *

    NOTE: Invoke {@link #addInline} after {@code setText}; + * else, mail readers might not be able to resolve inline references correctly. + * @param text the text for the message + * @throws MessagingException in case of errors + */ + public void setText(String text) throws MessagingException { + setText(text, false); + } + + /** + * Set the given text directly as content in non-multipart mode + * or as default body part in multipart mode. + * The "html" flag determines the content type to apply. + *

    NOTE: Invoke {@link #addInline} after {@code setText}; + * else, mail readers might not be able to resolve inline references correctly. + * @param text the text for the message + * @param html whether to apply content type "text/html" for an + * HTML mail, using default content type ("text/plain") else + * @throws MessagingException in case of errors + */ + public void setText(String text, boolean html) throws MessagingException { + Assert.notNull(text, "Text must not be null"); + MimePart partToUse; + if (isMultipart()) { + partToUse = getMainPart(); + } + else { + partToUse = this.mimeMessage; + } + if (html) { + setHtmlTextToMimePart(partToUse, text); + } + else { + setPlainTextToMimePart(partToUse, text); + } + } + + /** + * Set the given plain text and HTML text as alternatives, offering + * both options to the email client. Requires multipart mode. + *

    NOTE: Invoke {@link #addInline} after {@code setText}; + * else, mail readers might not be able to resolve inline references correctly. + * @param plainText the plain text for the message + * @param htmlText the HTML text for the message + * @throws MessagingException in case of errors + */ + public void setText(String plainText, String htmlText) throws MessagingException { + Assert.notNull(plainText, "Plain text must not be null"); + Assert.notNull(htmlText, "HTML text must not be null"); + + MimeMultipart messageBody = new MimeMultipart(MULTIPART_SUBTYPE_ALTERNATIVE); + getMainPart().setContent(messageBody, CONTENT_TYPE_ALTERNATIVE); + + // Create the plain text part of the message. + MimeBodyPart plainTextPart = new MimeBodyPart(); + setPlainTextToMimePart(plainTextPart, plainText); + messageBody.addBodyPart(plainTextPart); + + // Create the HTML text part of the message. + MimeBodyPart htmlTextPart = new MimeBodyPart(); + setHtmlTextToMimePart(htmlTextPart, htmlText); + messageBody.addBodyPart(htmlTextPart); + } + + private MimeBodyPart getMainPart() throws MessagingException { + MimeMultipart mimeMultipart = getMimeMultipart(); + MimeBodyPart bodyPart = null; + for (int i = 0; i < mimeMultipart.getCount(); i++) { + BodyPart bp = mimeMultipart.getBodyPart(i); + if (bp.getFileName() == null) { + bodyPart = (MimeBodyPart) bp; + } + } + if (bodyPart == null) { + MimeBodyPart mimeBodyPart = new MimeBodyPart(); + mimeMultipart.addBodyPart(mimeBodyPart); + bodyPart = mimeBodyPart; + } + return bodyPart; + } + + private void setPlainTextToMimePart(MimePart mimePart, String text) throws MessagingException { + if (getEncoding() != null) { + mimePart.setText(text, getEncoding()); + } + else { + mimePart.setText(text); + } + } + + private void setHtmlTextToMimePart(MimePart mimePart, String text) throws MessagingException { + if (getEncoding() != null) { + mimePart.setContent(text, CONTENT_TYPE_HTML + CONTENT_TYPE_CHARSET_SUFFIX + getEncoding()); + } + else { + mimePart.setContent(text, CONTENT_TYPE_HTML); + } + } + + + /** + * Add an inline element to the MimeMessage, taking the content from a + * {@code javax.activation.DataSource}. + *

    Note that the InputStream returned by the DataSource implementation + * needs to be a fresh one on each call, as JavaMail will invoke + * {@code getInputStream()} multiple times. + *

    NOTE: Invoke {@code addInline} after {@link #setText}; + * else, mail readers might not be able to resolve inline references correctly. + * @param contentId the content ID to use. Will end up as "Content-ID" header + * in the body part, surrounded by angle brackets: e.g. "myId" -> "<myId>". + * Can be referenced in HTML source via src="cid:myId" expressions. + * @param dataSource the {@code javax.activation.DataSource} to take + * the content from, determining the InputStream and the content type + * @throws MessagingException in case of errors + * @see #addInline(String, java.io.File) + * @see #addInline(String, org.springframework.core.io.Resource) + */ + public void addInline(String contentId, DataSource dataSource) throws MessagingException { + Assert.notNull(contentId, "Content ID must not be null"); + Assert.notNull(dataSource, "DataSource must not be null"); + MimeBodyPart mimeBodyPart = new MimeBodyPart(); + mimeBodyPart.setDisposition(MimeBodyPart.INLINE); + mimeBodyPart.setContentID("<" + contentId + ">"); + mimeBodyPart.setDataHandler(new DataHandler(dataSource)); + getMimeMultipart().addBodyPart(mimeBodyPart); + } + + /** + * Add an inline element to the MimeMessage, taking the content from a + * {@code java.io.File}. + *

    The content type will be determined by the name of the given + * content file. Do not use this for temporary files with arbitrary + * filenames (possibly ending in ".tmp" or the like)! + *

    NOTE: Invoke {@code addInline} after {@link #setText}; + * else, mail readers might not be able to resolve inline references correctly. + * @param contentId the content ID to use. Will end up as "Content-ID" header + * in the body part, surrounded by angle brackets: e.g. "myId" -> "<myId>". + * Can be referenced in HTML source via src="cid:myId" expressions. + * @param file the File resource to take the content from + * @throws MessagingException in case of errors + * @see #setText + * @see #addInline(String, org.springframework.core.io.Resource) + * @see #addInline(String, javax.activation.DataSource) + */ + public void addInline(String contentId, File file) throws MessagingException { + Assert.notNull(file, "File must not be null"); + FileDataSource dataSource = new FileDataSource(file); + dataSource.setFileTypeMap(getFileTypeMap()); + addInline(contentId, dataSource); + } + + /** + * Add an inline element to the MimeMessage, taking the content from a + * {@code org.springframework.core.io.Resource}. + *

    The content type will be determined by the name of the given + * content file. Do not use this for temporary files with arbitrary + * filenames (possibly ending in ".tmp" or the like)! + *

    Note that the InputStream returned by the Resource implementation + * needs to be a fresh one on each call, as JavaMail will invoke + * {@code getInputStream()} multiple times. + *

    NOTE: Invoke {@code addInline} after {@link #setText}; + * else, mail readers might not be able to resolve inline references correctly. + * @param contentId the content ID to use. Will end up as "Content-ID" header + * in the body part, surrounded by angle brackets: e.g. "myId" -> "<myId>". + * Can be referenced in HTML source via src="cid:myId" expressions. + * @param resource the resource to take the content from + * @throws MessagingException in case of errors + * @see #setText + * @see #addInline(String, java.io.File) + * @see #addInline(String, javax.activation.DataSource) + */ + public void addInline(String contentId, Resource resource) throws MessagingException { + Assert.notNull(resource, "Resource must not be null"); + String contentType = getFileTypeMap().getContentType(resource.getFilename()); + addInline(contentId, resource, contentType); + } + + /** + * Add an inline element to the MimeMessage, taking the content from an + * {@code org.springframework.core.InputStreamResource}, and + * specifying the content type explicitly. + *

    You can determine the content type for any given filename via a Java + * Activation Framework's FileTypeMap, for example the one held by this helper. + *

    Note that the InputStream returned by the InputStreamSource implementation + * needs to be a fresh one on each call, as JavaMail will invoke + * {@code getInputStream()} multiple times. + *

    NOTE: Invoke {@code addInline} after {@code setText}; + * else, mail readers might not be able to resolve inline references correctly. + * @param contentId the content ID to use. Will end up as "Content-ID" header + * in the body part, surrounded by angle brackets: e.g. "myId" -> "<myId>". + * Can be referenced in HTML source via src="cid:myId" expressions. + * @param inputStreamSource the resource to take the content from + * @param contentType the content type to use for the element + * @throws MessagingException in case of errors + * @see #setText + * @see #getFileTypeMap + * @see #addInline(String, org.springframework.core.io.Resource) + * @see #addInline(String, javax.activation.DataSource) + */ + public void addInline(String contentId, InputStreamSource inputStreamSource, String contentType) + throws MessagingException { + + Assert.notNull(inputStreamSource, "InputStreamSource must not be null"); + if (inputStreamSource instanceof Resource && ((Resource) inputStreamSource).isOpen()) { + throw new IllegalArgumentException( + "Passed-in Resource contains an open stream: invalid argument. " + + "JavaMail requires an InputStreamSource that creates a fresh stream for every call."); + } + DataSource dataSource = createDataSource(inputStreamSource, contentType, "inline"); + addInline(contentId, dataSource); + } + + /** + * Add an attachment to the MimeMessage, taking the content from a + * {@code javax.activation.DataSource}. + *

    Note that the InputStream returned by the DataSource implementation + * needs to be a fresh one on each call, as JavaMail will invoke + * {@code getInputStream()} multiple times. + * @param attachmentFilename the name of the attachment as it will + * appear in the mail (the content type will be determined by this) + * @param dataSource the {@code javax.activation.DataSource} to take + * the content from, determining the InputStream and the content type + * @throws MessagingException in case of errors + * @see #addAttachment(String, org.springframework.core.io.InputStreamSource) + * @see #addAttachment(String, java.io.File) + */ + public void addAttachment(String attachmentFilename, DataSource dataSource) throws MessagingException { + Assert.notNull(attachmentFilename, "Attachment filename must not be null"); + Assert.notNull(dataSource, "DataSource must not be null"); + try { + MimeBodyPart mimeBodyPart = new MimeBodyPart(); + mimeBodyPart.setDisposition(MimeBodyPart.ATTACHMENT); + mimeBodyPart.setFileName(isEncodeFilenames() ? + MimeUtility.encodeText(attachmentFilename) : attachmentFilename); + mimeBodyPart.setDataHandler(new DataHandler(dataSource)); + getRootMimeMultipart().addBodyPart(mimeBodyPart); + } + catch (UnsupportedEncodingException ex) { + throw new MessagingException("Failed to encode attachment filename", ex); + } + } + + /** + * Add an attachment to the MimeMessage, taking the content from a + * {@code java.io.File}. + *

    The content type will be determined by the name of the given + * content file. Do not use this for temporary files with arbitrary + * filenames (possibly ending in ".tmp" or the like)! + * @param attachmentFilename the name of the attachment as it will + * appear in the mail + * @param file the File resource to take the content from + * @throws MessagingException in case of errors + * @see #addAttachment(String, org.springframework.core.io.InputStreamSource) + * @see #addAttachment(String, javax.activation.DataSource) + */ + public void addAttachment(String attachmentFilename, File file) throws MessagingException { + Assert.notNull(file, "File must not be null"); + FileDataSource dataSource = new FileDataSource(file); + dataSource.setFileTypeMap(getFileTypeMap()); + addAttachment(attachmentFilename, dataSource); + } + + /** + * Add an attachment to the MimeMessage, taking the content from an + * {@code org.springframework.core.io.InputStreamResource}. + *

    The content type will be determined by the given filename for + * the attachment. Thus, any content source will be fine, including + * temporary files with arbitrary filenames. + *

    Note that the InputStream returned by the InputStreamSource + * implementation needs to be a fresh one on each call, as + * JavaMail will invoke {@code getInputStream()} multiple times. + * @param attachmentFilename the name of the attachment as it will + * appear in the mail + * @param inputStreamSource the resource to take the content from + * (all of Spring's Resource implementations can be passed in here) + * @throws MessagingException in case of errors + * @see #addAttachment(String, java.io.File) + * @see #addAttachment(String, javax.activation.DataSource) + * @see org.springframework.core.io.Resource + */ + public void addAttachment(String attachmentFilename, InputStreamSource inputStreamSource) + throws MessagingException { + + String contentType = getFileTypeMap().getContentType(attachmentFilename); + addAttachment(attachmentFilename, inputStreamSource, contentType); + } + + /** + * Add an attachment to the MimeMessage, taking the content from an + * {@code org.springframework.core.io.InputStreamResource}. + *

    Note that the InputStream returned by the InputStreamSource + * implementation needs to be a fresh one on each call, as + * JavaMail will invoke {@code getInputStream()} multiple times. + * @param attachmentFilename the name of the attachment as it will + * appear in the mail + * @param inputStreamSource the resource to take the content from + * (all of Spring's Resource implementations can be passed in here) + * @param contentType the content type to use for the element + * @throws MessagingException in case of errors + * @see #addAttachment(String, java.io.File) + * @see #addAttachment(String, javax.activation.DataSource) + * @see org.springframework.core.io.Resource + */ + public void addAttachment( + String attachmentFilename, InputStreamSource inputStreamSource, String contentType) + throws MessagingException { + + Assert.notNull(inputStreamSource, "InputStreamSource must not be null"); + if (inputStreamSource instanceof Resource && ((Resource) inputStreamSource).isOpen()) { + throw new IllegalArgumentException( + "Passed-in Resource contains an open stream: invalid argument. " + + "JavaMail requires an InputStreamSource that creates a fresh stream for every call."); + } + DataSource dataSource = createDataSource(inputStreamSource, contentType, attachmentFilename); + addAttachment(attachmentFilename, dataSource); + } + + /** + * Create an Activation Framework DataSource for the given InputStreamSource. + * @param inputStreamSource the InputStreamSource (typically a Spring Resource) + * @param contentType the content type + * @param name the name of the DataSource + * @return the Activation Framework DataSource + */ + protected DataSource createDataSource( + final InputStreamSource inputStreamSource, final String contentType, final String name) { + + return new DataSource() { + @Override + public InputStream getInputStream() throws IOException { + return inputStreamSource.getInputStream(); + } + @Override + public OutputStream getOutputStream() { + throw new UnsupportedOperationException("Read-only javax.activation.DataSource"); + } + @Override + public String getContentType() { + return contentType; + } + @Override + public String getName() { + return name; + } + }; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/mail/javamail/MimeMessagePreparator.java b/spring-context-support/src/main/java/org/springframework/mail/javamail/MimeMessagePreparator.java new file mode 100644 index 0000000..5973d17 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/mail/javamail/MimeMessagePreparator.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.mail.javamail; + +import javax.mail.internet.MimeMessage; + +/** + * Callback interface for the preparation of JavaMail MIME messages. + * + *

    The corresponding {@code send} methods of {@link JavaMailSender} + * will take care of the actual creation of a {@link MimeMessage} instance, + * and of proper exception conversion. + * + *

    It is often convenient to use a {@link MimeMessageHelper} for populating + * the passed-in MimeMessage, in particular when working with attachments or + * special character encodings. + * See {@link MimeMessageHelper MimeMessageHelper's javadoc} for an example. + * + * @author Juergen Hoeller + * @since 07.10.2003 + * @see JavaMailSender#send(MimeMessagePreparator) + * @see JavaMailSender#send(MimeMessagePreparator[]) + * @see MimeMessageHelper + */ +@FunctionalInterface +public interface MimeMessagePreparator { + + /** + * Prepare the given new MimeMessage instance. + * @param mimeMessage the message to prepare + * @throws javax.mail.MessagingException passing any exceptions thrown by MimeMessage + * methods through for automatic conversion to the MailException hierarchy + * @throws java.io.IOException passing any exceptions thrown by MimeMessage methods + * through for automatic conversion to the MailException hierarchy + * @throws Exception if mail preparation failed, for example when a + * FreeMarker template cannot be rendered for the mail text + */ + void prepare(MimeMessage mimeMessage) throws Exception; + +} diff --git a/spring-context-support/src/main/java/org/springframework/mail/javamail/SmartMimeMessage.java b/spring-context-support/src/main/java/org/springframework/mail/javamail/SmartMimeMessage.java new file mode 100644 index 0000000..2fa44c9 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/mail/javamail/SmartMimeMessage.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.mail.javamail; + +import javax.activation.FileTypeMap; +import javax.mail.Session; +import javax.mail.internet.MimeMessage; + +import org.springframework.lang.Nullable; + +/** + * Special subclass of the standard JavaMail {@link MimeMessage}, carrying a + * default encoding to be used when populating the message and a default Java + * Activation {@link FileTypeMap} to be used for resolving attachment types. + * + *

    Created by {@link JavaMailSenderImpl} in case of a specified default encoding + * and/or default FileTypeMap. Autodetected by {@link MimeMessageHelper}, which + * will use the carried encoding and FileTypeMap unless explicitly overridden. + * + * @author Juergen Hoeller + * @since 1.2 + * @see JavaMailSenderImpl#createMimeMessage() + * @see MimeMessageHelper#getDefaultEncoding(javax.mail.internet.MimeMessage) + * @see MimeMessageHelper#getDefaultFileTypeMap(javax.mail.internet.MimeMessage) + */ +class SmartMimeMessage extends MimeMessage { + + @Nullable + private final String defaultEncoding; + + @Nullable + private final FileTypeMap defaultFileTypeMap; + + + /** + * Create a new SmartMimeMessage. + * @param session the JavaMail Session to create the message for + * @param defaultEncoding the default encoding, or {@code null} if none + * @param defaultFileTypeMap the default FileTypeMap, or {@code null} if none + */ + public SmartMimeMessage( + Session session, @Nullable String defaultEncoding, @Nullable FileTypeMap defaultFileTypeMap) { + + super(session); + this.defaultEncoding = defaultEncoding; + this.defaultFileTypeMap = defaultFileTypeMap; + } + + + /** + * Return the default encoding of this message, or {@code null} if none. + */ + @Nullable + public final String getDefaultEncoding() { + return this.defaultEncoding; + } + + /** + * Return the default FileTypeMap of this message, or {@code null} if none. + */ + @Nullable + public final FileTypeMap getDefaultFileTypeMap() { + return this.defaultFileTypeMap; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/mail/javamail/package-info.java b/spring-context-support/src/main/java/org/springframework/mail/javamail/package-info.java new file mode 100644 index 0000000..e511448 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/mail/javamail/package-info.java @@ -0,0 +1,11 @@ +/** + * JavaMail support for Spring's mail infrastructure. + * Provides an extended JavaMailSender interface and a MimeMessageHelper + * class for convenient population of a JavaMail MimeMessage. + */ +@NonNullApi +@NonNullFields +package org.springframework.mail.javamail; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context-support/src/main/java/org/springframework/mail/package-info.java b/spring-context-support/src/main/java/org/springframework/mail/package-info.java new file mode 100644 index 0000000..a5d452d --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/mail/package-info.java @@ -0,0 +1,10 @@ +/** + * Spring's generic mail infrastructure. + * Concrete implementations are provided in the subpackages. + */ +@NonNullApi +@NonNullFields +package org.springframework.mail; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/commonj/DelegatingTimerListener.java b/spring-context-support/src/main/java/org/springframework/scheduling/commonj/DelegatingTimerListener.java new file mode 100644 index 0000000..d6d493e --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/scheduling/commonj/DelegatingTimerListener.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.commonj; + +import commonj.timers.Timer; +import commonj.timers.TimerListener; + +import org.springframework.util.Assert; + +/** + * Simple TimerListener adapter that delegates to a given Runnable. + * + * @author Juergen Hoeller + * @since 2.0 + * @see commonj.timers.TimerListener + * @see java.lang.Runnable + * @deprecated as of 5.1, in favor of EE 7's + * {@link org.springframework.scheduling.concurrent.DefaultManagedTaskScheduler} + */ +@Deprecated +public class DelegatingTimerListener implements TimerListener { + + private final Runnable runnable; + + + /** + * Create a new DelegatingTimerListener. + * @param runnable the Runnable implementation to delegate to + */ + public DelegatingTimerListener(Runnable runnable) { + Assert.notNull(runnable, "Runnable is required"); + this.runnable = runnable; + } + + + /** + * Delegates execution to the underlying Runnable. + */ + @Override + public void timerExpired(Timer timer) { + this.runnable.run(); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/commonj/DelegatingWork.java b/spring-context-support/src/main/java/org/springframework/scheduling/commonj/DelegatingWork.java new file mode 100644 index 0000000..9f7eb91 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/scheduling/commonj/DelegatingWork.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.commonj; + +import commonj.work.Work; + +import org.springframework.scheduling.SchedulingAwareRunnable; +import org.springframework.util.Assert; + +/** + * Simple Work adapter that delegates to a given Runnable. + * + * @author Juergen Hoeller + * @since 2.0 + * @deprecated as of 5.1, in favor of EE 7's + * {@link org.springframework.scheduling.concurrent.DefaultManagedTaskExecutor} + */ +@Deprecated +public class DelegatingWork implements Work { + + private final Runnable delegate; + + + /** + * Create a new DelegatingWork. + * @param delegate the Runnable implementation to delegate to + * (may be a SchedulingAwareRunnable for extended support) + * @see org.springframework.scheduling.SchedulingAwareRunnable + * @see #isDaemon() + */ + public DelegatingWork(Runnable delegate) { + Assert.notNull(delegate, "Delegate must not be null"); + this.delegate = delegate; + } + + /** + * Return the wrapped Runnable implementation. + */ + public final Runnable getDelegate() { + return this.delegate; + } + + + /** + * Delegates execution to the underlying Runnable. + */ + @Override + public void run() { + this.delegate.run(); + } + + /** + * This implementation delegates to + * {@link org.springframework.scheduling.SchedulingAwareRunnable#isLongLived()}, + * if available. + */ + @Override + public boolean isDaemon() { + return (this.delegate instanceof SchedulingAwareRunnable && + ((SchedulingAwareRunnable) this.delegate).isLongLived()); + } + + /** + * This implementation is empty, since we expect the Runnable + * to terminate based on some specific shutdown signal. + */ + @Override + public void release() { + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/commonj/ScheduledTimerListener.java b/spring-context-support/src/main/java/org/springframework/scheduling/commonj/ScheduledTimerListener.java new file mode 100644 index 0000000..52ca0af --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/scheduling/commonj/ScheduledTimerListener.java @@ -0,0 +1,229 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.commonj; + +import commonj.timers.TimerListener; + +import org.springframework.lang.Nullable; + +/** + * JavaBean that describes a scheduled TimerListener, consisting of + * the TimerListener itself (or a Runnable to create a TimerListener for) + * and a delay plus period. Period needs to be specified; + * there is no point in a default for it. + * + *

    The CommonJ TimerManager does not offer more sophisticated scheduling + * options such as cron expressions. Consider using Quartz for such + * advanced needs. + * + *

    Note that the TimerManager uses a TimerListener instance that is + * shared between repeated executions, in contrast to Quartz which + * instantiates a new Job for each execution. + * + * @author Juergen Hoeller + * @since 2.0 + * @deprecated as of 5.1, in favor of EE 7's + * {@link org.springframework.scheduling.concurrent.DefaultManagedTaskScheduler} + */ +@Deprecated +public class ScheduledTimerListener { + + @Nullable + private TimerListener timerListener; + + private long delay = 0; + + private long period = -1; + + private boolean fixedRate = false; + + + /** + * Create a new ScheduledTimerListener, + * to be populated via bean properties. + * @see #setTimerListener + * @see #setDelay + * @see #setPeriod + * @see #setFixedRate + */ + public ScheduledTimerListener() { + } + + /** + * Create a new ScheduledTimerListener, with default + * one-time execution without delay. + * @param timerListener the TimerListener to schedule + */ + public ScheduledTimerListener(TimerListener timerListener) { + this.timerListener = timerListener; + } + + /** + * Create a new ScheduledTimerListener, with default + * one-time execution with the given delay. + * @param timerListener the TimerListener to schedule + * @param delay the delay before starting the task for the first time (ms) + */ + public ScheduledTimerListener(TimerListener timerListener, long delay) { + this.timerListener = timerListener; + this.delay = delay; + } + + /** + * Create a new ScheduledTimerListener. + * @param timerListener the TimerListener to schedule + * @param delay the delay before starting the task for the first time (ms) + * @param period the period between repeated task executions (ms) + * @param fixedRate whether to schedule as fixed-rate execution + */ + public ScheduledTimerListener(TimerListener timerListener, long delay, long period, boolean fixedRate) { + this.timerListener = timerListener; + this.delay = delay; + this.period = period; + this.fixedRate = fixedRate; + } + + /** + * Create a new ScheduledTimerListener, with default + * one-time execution without delay. + * @param timerTask the Runnable to schedule as TimerListener + */ + public ScheduledTimerListener(Runnable timerTask) { + setRunnable(timerTask); + } + + /** + * Create a new ScheduledTimerListener, with default + * one-time execution with the given delay. + * @param timerTask the Runnable to schedule as TimerListener + * @param delay the delay before starting the task for the first time (ms) + */ + public ScheduledTimerListener(Runnable timerTask, long delay) { + setRunnable(timerTask); + this.delay = delay; + } + + /** + * Create a new ScheduledTimerListener. + * @param timerTask the Runnable to schedule as TimerListener + * @param delay the delay before starting the task for the first time (ms) + * @param period the period between repeated task executions (ms) + * @param fixedRate whether to schedule as fixed-rate execution + */ + public ScheduledTimerListener(Runnable timerTask, long delay, long period, boolean fixedRate) { + setRunnable(timerTask); + this.delay = delay; + this.period = period; + this.fixedRate = fixedRate; + } + + + /** + * Set the Runnable to schedule as TimerListener. + * @see DelegatingTimerListener + */ + public void setRunnable(Runnable timerTask) { + this.timerListener = new DelegatingTimerListener(timerTask); + } + + /** + * Set the TimerListener to schedule. + */ + public void setTimerListener(@Nullable TimerListener timerListener) { + this.timerListener = timerListener; + } + + /** + * Return the TimerListener to schedule. + */ + @Nullable + public TimerListener getTimerListener() { + return this.timerListener; + } + + /** + * Set the delay before starting the task for the first time, + * in milliseconds. Default is 0, immediately starting the + * task after successful scheduling. + *

    If the "firstTime" property is specified, this property will be ignored. + * Specify one or the other, not both. + */ + public void setDelay(long delay) { + this.delay = delay; + } + + /** + * Return the delay before starting the job for the first time. + */ + public long getDelay() { + return this.delay; + } + + /** + * Set the period between repeated task executions, in milliseconds. + *

    Default is -1, leading to one-time execution. In case of zero or a + * positive value, the task will be executed repeatedly, with the given + * interval in-between executions. + *

    Note that the semantics of the period value vary between fixed-rate + * and fixed-delay execution. + *

    Note: A period of 0 (for example as fixed delay) is + * supported, because the CommonJ specification defines this as a legal value. + * Hence a value of 0 will result in immediate re-execution after a job has + * finished (not in one-time execution like with {@code java.util.Timer}). + * @see #setFixedRate + * @see #isOneTimeTask() + * @see commonj.timers.TimerManager#schedule(commonj.timers.TimerListener, long, long) + */ + public void setPeriod(long period) { + this.period = period; + } + + /** + * Return the period between repeated task executions. + */ + public long getPeriod() { + return this.period; + } + + /** + * Is this task only ever going to execute once? + * @return {@code true} if this task is only ever going to execute once + * @see #getPeriod() + */ + public boolean isOneTimeTask() { + return (this.period < 0); + } + + /** + * Set whether to schedule as fixed-rate execution, rather than + * fixed-delay execution. Default is "false", i.e. fixed delay. + *

    See TimerManager javadoc for details on those execution modes. + * @see commonj.timers.TimerManager#schedule(commonj.timers.TimerListener, long, long) + * @see commonj.timers.TimerManager#scheduleAtFixedRate(commonj.timers.TimerListener, long, long) + */ + public void setFixedRate(boolean fixedRate) { + this.fixedRate = fixedRate; + } + + /** + * Return whether to schedule as fixed-rate execution. + */ + public boolean isFixedRate() { + return this.fixedRate; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/commonj/TimerManagerAccessor.java b/spring-context-support/src/main/java/org/springframework/scheduling/commonj/TimerManagerAccessor.java new file mode 100644 index 0000000..b5c0e15 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/scheduling/commonj/TimerManagerAccessor.java @@ -0,0 +1,192 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.commonj; + +import javax.naming.NamingException; + +import commonj.timers.TimerManager; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.Lifecycle; +import org.springframework.jndi.JndiLocatorSupport; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Base class for classes that are accessing a CommonJ {@link commonj.timers.TimerManager} + * Defines common configuration settings and common lifecycle handling. + * + * @author Juergen Hoeller + * @since 3.0 + * @see commonj.timers.TimerManager + * @deprecated as of 5.1, in favor of EE 7's + * {@link org.springframework.scheduling.concurrent.DefaultManagedTaskScheduler} + */ +@Deprecated +public abstract class TimerManagerAccessor extends JndiLocatorSupport + implements InitializingBean, DisposableBean, Lifecycle { + + @Nullable + private TimerManager timerManager; + + @Nullable + private String timerManagerName; + + private boolean shared = false; + + + /** + * Specify the CommonJ TimerManager to delegate to. + *

    Note that the given TimerManager's lifecycle will be managed + * by this FactoryBean. + *

    Alternatively (and typically), you can specify the JNDI name + * of the target TimerManager. + * @see #setTimerManagerName + */ + public void setTimerManager(TimerManager timerManager) { + this.timerManager = timerManager; + } + + /** + * Set the JNDI name of the CommonJ TimerManager. + *

    This can either be a fully qualified JNDI name, or the JNDI name relative + * to the current environment naming context if "resourceRef" is set to "true". + * @see #setTimerManager + * @see #setResourceRef + */ + public void setTimerManagerName(String timerManagerName) { + this.timerManagerName = timerManagerName; + } + + /** + * Specify whether the TimerManager obtained by this FactoryBean + * is a shared instance ("true") or an independent instance ("false"). + * The lifecycle of the former is supposed to be managed by the application + * server, while the lifecycle of the latter is up to the application. + *

    Default is "false", i.e. managing an independent TimerManager instance. + * This is what the CommonJ specification suggests that application servers + * are supposed to offer via JNDI lookups, typically declared as a + * {@code resource-ref} of type {@code commonj.timers.TimerManager} + * in {@code web.xml}, with {@code res-sharing-scope} set to 'Unshareable'. + *

    Switch this flag to "true" if you are obtaining a shared TimerManager, + * typically through specifying the JNDI location of a TimerManager that + * has been explicitly declared as 'Shareable'. Note that WebLogic's + * cluster-aware Job Scheduler is a shared TimerManager too. + *

    The sole difference between this FactoryBean being in shared or + * non-shared mode is that it will only attempt to suspend / resume / stop + * the underlying TimerManager in case of an independent (non-shared) instance. + * This only affects the {@link org.springframework.context.Lifecycle} support + * as well as application context shutdown. + * @see #stop() + * @see #start() + * @see #destroy() + * @see commonj.timers.TimerManager + */ + public void setShared(boolean shared) { + this.shared = shared; + } + + + @Override + public void afterPropertiesSet() throws NamingException { + if (this.timerManager == null) { + if (this.timerManagerName == null) { + throw new IllegalArgumentException("Either 'timerManager' or 'timerManagerName' must be specified"); + } + this.timerManager = lookup(this.timerManagerName, TimerManager.class); + } + } + + /** + * Return the configured TimerManager, if any. + * @return the TimerManager, or {@code null} if not available + */ + @Nullable + protected final TimerManager getTimerManager() { + return this.timerManager; + } + + /** + * Obtain the TimerManager for actual use. + * @return the TimerManager (never {@code null}) + * @throws IllegalStateException in case of no TimerManager set + * @since 5.0 + */ + protected TimerManager obtainTimerManager() { + Assert.notNull(this.timerManager, "No TimerManager set"); + return this.timerManager; + } + + + //--------------------------------------------------------------------- + // Implementation of Lifecycle interface + //--------------------------------------------------------------------- + + /** + * Resumes the underlying TimerManager (if not shared). + * @see commonj.timers.TimerManager#resume() + */ + @Override + public void start() { + if (!this.shared) { + obtainTimerManager().resume(); + } + } + + /** + * Suspends the underlying TimerManager (if not shared). + * @see commonj.timers.TimerManager#suspend() + */ + @Override + public void stop() { + if (!this.shared) { + obtainTimerManager().suspend(); + } + } + + /** + * Considers the underlying TimerManager as running if it is + * neither suspending nor stopping. + * @see commonj.timers.TimerManager#isSuspending() + * @see commonj.timers.TimerManager#isStopping() + */ + @Override + public boolean isRunning() { + TimerManager tm = obtainTimerManager(); + return (!tm.isSuspending() && !tm.isStopping()); + } + + + //--------------------------------------------------------------------- + // Implementation of DisposableBean interface + //--------------------------------------------------------------------- + + /** + * Stops the underlying TimerManager (if not shared). + * @see commonj.timers.TimerManager#stop() + */ + @Override + public void destroy() { + // Stop the entire TimerManager, if necessary. + if (this.timerManager != null && !this.shared) { + // May return early, but at least we already cancelled all known Timers. + this.timerManager.stop(); + } + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/commonj/TimerManagerFactoryBean.java b/spring-context-support/src/main/java/org/springframework/scheduling/commonj/TimerManagerFactoryBean.java new file mode 100644 index 0000000..1d6db6c --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/scheduling/commonj/TimerManagerFactoryBean.java @@ -0,0 +1,165 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.commonj; + +import java.util.ArrayList; +import java.util.List; + +import javax.naming.NamingException; + +import commonj.timers.Timer; +import commonj.timers.TimerManager; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.Lifecycle; +import org.springframework.lang.Nullable; + +/** + * {@link org.springframework.beans.factory.FactoryBean} that retrieves a + * CommonJ {@link commonj.timers.TimerManager} and exposes it for bean references. + * + *

    This is the central convenience class for setting up a + * CommonJ TimerManager in a Spring context. + * + *

    Allows for registration of ScheduledTimerListeners. This is the main + * purpose of this class; the TimerManager itself could also be fetched + * from JNDI via {@link org.springframework.jndi.JndiObjectFactoryBean}. + * In scenarios that just require static registration of tasks at startup, + * there is no need to access the TimerManager itself in application code. + * + *

    Note that the TimerManager uses a TimerListener instance that is + * shared between repeated executions, in contrast to Quartz which + * instantiates a new Job for each execution. + * + * @author Juergen Hoeller + * @since 2.0 + * @see ScheduledTimerListener + * @see commonj.timers.TimerManager + * @see commonj.timers.TimerListener + * @deprecated as of 5.1, in favor of EE 7's + * {@link org.springframework.scheduling.concurrent.DefaultManagedTaskScheduler} + */ +@Deprecated +public class TimerManagerFactoryBean extends TimerManagerAccessor + implements FactoryBean, InitializingBean, DisposableBean, Lifecycle { + + @Nullable + private ScheduledTimerListener[] scheduledTimerListeners; + + @Nullable + private List timers; + + + /** + * Register a list of ScheduledTimerListener objects with the TimerManager + * that this FactoryBean creates. Depending on each ScheduledTimerListener's settings, + * it will be registered via one of TimerManager's schedule methods. + * @see commonj.timers.TimerManager#schedule(commonj.timers.TimerListener, long) + * @see commonj.timers.TimerManager#schedule(commonj.timers.TimerListener, long, long) + * @see commonj.timers.TimerManager#scheduleAtFixedRate(commonj.timers.TimerListener, long, long) + */ + public void setScheduledTimerListeners(ScheduledTimerListener[] scheduledTimerListeners) { + this.scheduledTimerListeners = scheduledTimerListeners; + } + + + //--------------------------------------------------------------------- + // Implementation of InitializingBean interface + //--------------------------------------------------------------------- + + @Override + public void afterPropertiesSet() throws NamingException { + super.afterPropertiesSet(); + + if (this.scheduledTimerListeners != null) { + this.timers = new ArrayList<>(this.scheduledTimerListeners.length); + TimerManager timerManager = obtainTimerManager(); + for (ScheduledTimerListener scheduledTask : this.scheduledTimerListeners) { + Timer timer; + if (scheduledTask.isOneTimeTask()) { + timer = timerManager.schedule(scheduledTask.getTimerListener(), scheduledTask.getDelay()); + } + else { + if (scheduledTask.isFixedRate()) { + timer = timerManager.scheduleAtFixedRate( + scheduledTask.getTimerListener(), scheduledTask.getDelay(), scheduledTask.getPeriod()); + } + else { + timer = timerManager.schedule( + scheduledTask.getTimerListener(), scheduledTask.getDelay(), scheduledTask.getPeriod()); + } + } + this.timers.add(timer); + } + } + } + + + //--------------------------------------------------------------------- + // Implementation of FactoryBean interface + //--------------------------------------------------------------------- + + @Override + @Nullable + public TimerManager getObject() { + return getTimerManager(); + } + + @Override + public Class getObjectType() { + TimerManager timerManager = getTimerManager(); + return (timerManager != null ? timerManager.getClass() : TimerManager.class); + } + + @Override + public boolean isSingleton() { + return true; + } + + + //--------------------------------------------------------------------- + // Implementation of DisposableBean interface + //--------------------------------------------------------------------- + + /** + * Cancels all statically registered Timers on shutdown, + * and stops the underlying TimerManager (if not shared). + * @see commonj.timers.Timer#cancel() + * @see commonj.timers.TimerManager#stop() + */ + @Override + public void destroy() { + // Cancel all registered timers. + if (this.timers != null) { + for (Timer timer : this.timers) { + try { + timer.cancel(); + } + catch (Throwable ex) { + logger.debug("Could not cancel CommonJ Timer", ex); + } + } + this.timers.clear(); + } + + // Stop the TimerManager itself. + super.destroy(); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/commonj/TimerManagerTaskScheduler.java b/spring-context-support/src/main/java/org/springframework/scheduling/commonj/TimerManagerTaskScheduler.java new file mode 100644 index 0000000..97ea5b8 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/scheduling/commonj/TimerManagerTaskScheduler.java @@ -0,0 +1,201 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.commonj; + +import java.util.Date; +import java.util.concurrent.Delayed; +import java.util.concurrent.FutureTask; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import commonj.timers.Timer; +import commonj.timers.TimerListener; + +import org.springframework.lang.Nullable; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.Trigger; +import org.springframework.scheduling.support.SimpleTriggerContext; +import org.springframework.scheduling.support.TaskUtils; +import org.springframework.util.Assert; +import org.springframework.util.ErrorHandler; + +/** + * Implementation of Spring's {@link TaskScheduler} interface, wrapping + * a CommonJ {@link commonj.timers.TimerManager}. + * + * @author Juergen Hoeller + * @author Mark Fisher + * @since 3.0 + * @deprecated as of 5.1, in favor of EE 7's + * {@link org.springframework.scheduling.concurrent.DefaultManagedTaskScheduler} + */ +@Deprecated +public class TimerManagerTaskScheduler extends TimerManagerAccessor implements TaskScheduler { + + @Nullable + private volatile ErrorHandler errorHandler; + + + /** + * Provide an {@link ErrorHandler} strategy. + */ + public void setErrorHandler(ErrorHandler errorHandler) { + this.errorHandler = errorHandler; + } + + + @Override + @Nullable + public ScheduledFuture schedule(Runnable task, Trigger trigger) { + return new ReschedulingTimerListener(errorHandlingTask(task, true), trigger).schedule(); + } + + @Override + public ScheduledFuture schedule(Runnable task, Date startTime) { + TimerScheduledFuture futureTask = new TimerScheduledFuture(errorHandlingTask(task, false)); + Timer timer = obtainTimerManager().schedule(futureTask, startTime); + futureTask.setTimer(timer); + return futureTask; + } + + @Override + public ScheduledFuture scheduleAtFixedRate(Runnable task, Date startTime, long period) { + TimerScheduledFuture futureTask = new TimerScheduledFuture(errorHandlingTask(task, true)); + Timer timer = obtainTimerManager().scheduleAtFixedRate(futureTask, startTime, period); + futureTask.setTimer(timer); + return futureTask; + } + + @Override + public ScheduledFuture scheduleAtFixedRate(Runnable task, long period) { + TimerScheduledFuture futureTask = new TimerScheduledFuture(errorHandlingTask(task, true)); + Timer timer = obtainTimerManager().scheduleAtFixedRate(futureTask, 0, period); + futureTask.setTimer(timer); + return futureTask; + } + + @Override + public ScheduledFuture scheduleWithFixedDelay(Runnable task, Date startTime, long delay) { + TimerScheduledFuture futureTask = new TimerScheduledFuture(errorHandlingTask(task, true)); + Timer timer = obtainTimerManager().schedule(futureTask, startTime, delay); + futureTask.setTimer(timer); + return futureTask; + } + + @Override + public ScheduledFuture scheduleWithFixedDelay(Runnable task, long delay) { + TimerScheduledFuture futureTask = new TimerScheduledFuture(errorHandlingTask(task, true)); + Timer timer = obtainTimerManager().schedule(futureTask, 0, delay); + futureTask.setTimer(timer); + return futureTask; + } + + private Runnable errorHandlingTask(Runnable delegate, boolean isRepeatingTask) { + return TaskUtils.decorateTaskWithErrorHandler(delegate, this.errorHandler, isRepeatingTask); + } + + + /** + * ScheduledFuture adapter that wraps a CommonJ Timer. + */ + private static class TimerScheduledFuture extends FutureTask implements TimerListener, ScheduledFuture { + + @Nullable + protected transient Timer timer; + + protected transient boolean cancelled = false; + + public TimerScheduledFuture(Runnable runnable) { + super(runnable, null); + } + + public void setTimer(Timer timer) { + this.timer = timer; + } + + @Override + public void timerExpired(Timer timer) { + runAndReset(); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + boolean result = super.cancel(mayInterruptIfRunning); + if (this.timer != null) { + this.timer.cancel(); + } + this.cancelled = true; + return result; + } + + @Override + public long getDelay(TimeUnit unit) { + Assert.state(this.timer != null, "No Timer available"); + return unit.convert(this.timer.getScheduledExecutionTime() - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + + @Override + public int compareTo(Delayed other) { + if (this == other) { + return 0; + } + long diff = getDelay(TimeUnit.MILLISECONDS) - other.getDelay(TimeUnit.MILLISECONDS); + return (diff == 0 ? 0 : ((diff < 0) ? -1 : 1)); + } + } + + + /** + * ScheduledFuture adapter for trigger-based rescheduling. + */ + private class ReschedulingTimerListener extends TimerScheduledFuture { + + private final Trigger trigger; + + private final SimpleTriggerContext triggerContext = new SimpleTriggerContext(); + + private volatile Date scheduledExecutionTime = new Date(); + + public ReschedulingTimerListener(Runnable runnable, Trigger trigger) { + super(runnable); + this.trigger = trigger; + } + + @Nullable + public ScheduledFuture schedule() { + Date nextExecutionTime = this.trigger.nextExecutionTime(this.triggerContext); + if (nextExecutionTime == null) { + return null; + } + this.scheduledExecutionTime = nextExecutionTime; + setTimer(obtainTimerManager().schedule(this, this.scheduledExecutionTime)); + return this; + } + + @Override + public void timerExpired(Timer timer) { + Date actualExecutionTime = new Date(); + super.timerExpired(timer); + Date completionTime = new Date(); + this.triggerContext.update(this.scheduledExecutionTime, actualExecutionTime, completionTime); + if (!this.cancelled) { + schedule(); + } + } + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/commonj/WorkManagerTaskExecutor.java b/spring-context-support/src/main/java/org/springframework/scheduling/commonj/WorkManagerTaskExecutor.java new file mode 100644 index 0000000..a9adcc8 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/scheduling/commonj/WorkManagerTaskExecutor.java @@ -0,0 +1,234 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.commonj; + +import java.util.Collection; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; + +import javax.naming.NamingException; + +import commonj.work.Work; +import commonj.work.WorkException; +import commonj.work.WorkItem; +import commonj.work.WorkListener; +import commonj.work.WorkManager; +import commonj.work.WorkRejectedException; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.task.AsyncListenableTaskExecutor; +import org.springframework.core.task.TaskDecorator; +import org.springframework.core.task.TaskRejectedException; +import org.springframework.jndi.JndiLocatorSupport; +import org.springframework.lang.Nullable; +import org.springframework.scheduling.SchedulingException; +import org.springframework.scheduling.SchedulingTaskExecutor; +import org.springframework.util.Assert; +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.util.concurrent.ListenableFutureTask; + +/** + * TaskExecutor implementation that delegates to a CommonJ WorkManager, + * implementing the {@link commonj.work.WorkManager} interface, + * which either needs to be specified as reference or through the JNDI name. + * + *

    This is the central convenience class for setting up a + * CommonJ WorkManager in a Spring context. + * + *

    Also implements the CommonJ WorkManager interface itself, delegating all + * calls to the target WorkManager. Hence, a caller can choose whether it wants + * to talk to this executor through the Spring TaskExecutor interface or the + * CommonJ WorkManager interface. + * + *

    The CommonJ WorkManager will usually be retrieved from the application + * server's JNDI environment, as defined in the server's management console. + * + *

    Note: On EE 7/8 compliant versions of WebLogic and WebSphere, a + * {@link org.springframework.scheduling.concurrent.DefaultManagedTaskExecutor} + * should be preferred, following JSR-236 support in Java EE 7/8. + * + * @author Juergen Hoeller + * @since 2.0 + * @deprecated as of 5.1, in favor of the EE 7/8 based + * {@link org.springframework.scheduling.concurrent.DefaultManagedTaskExecutor} + */ +@Deprecated +public class WorkManagerTaskExecutor extends JndiLocatorSupport + implements AsyncListenableTaskExecutor, SchedulingTaskExecutor, WorkManager, InitializingBean { + + @Nullable + private WorkManager workManager; + + @Nullable + private String workManagerName; + + @Nullable + private WorkListener workListener; + + @Nullable + private TaskDecorator taskDecorator; + + + /** + * Specify the CommonJ WorkManager to delegate to. + *

    Alternatively, you can also specify the JNDI name of the target WorkManager. + * @see #setWorkManagerName + */ + public void setWorkManager(WorkManager workManager) { + this.workManager = workManager; + } + + /** + * Set the JNDI name of the CommonJ WorkManager. + *

    This can either be a fully qualified JNDI name, or the JNDI name relative + * to the current environment naming context if "resourceRef" is set to "true". + * @see #setWorkManager + * @see #setResourceRef + */ + public void setWorkManagerName(String workManagerName) { + this.workManagerName = workManagerName; + } + + /** + * Specify a CommonJ WorkListener to apply, if any. + *

    This shared WorkListener instance will be passed on to the + * WorkManager by all {@link #execute} calls on this TaskExecutor. + */ + public void setWorkListener(WorkListener workListener) { + this.workListener = workListener; + } + + /** + * Specify a custom {@link TaskDecorator} to be applied to any {@link Runnable} + * about to be executed. + *

    Note that such a decorator is not necessarily being applied to the + * user-supplied {@code Runnable}/{@code Callable} but rather to the actual + * execution callback (which may be a wrapper around the user-supplied task). + *

    The primary use case is to set some execution context around the task's + * invocation, or to provide some monitoring/statistics for task execution. + *

    NOTE: Exception handling in {@code TaskDecorator} implementations + * is limited to plain {@code Runnable} execution via {@code execute} calls. + * In case of {@code #submit} calls, the exposed {@code Runnable} will be a + * {@code FutureTask} which does not propagate any exceptions; you might + * have to cast it and call {@code Future#get} to evaluate exceptions. + * @since 4.3 + */ + public void setTaskDecorator(TaskDecorator taskDecorator) { + this.taskDecorator = taskDecorator; + } + + @Override + public void afterPropertiesSet() throws NamingException { + if (this.workManager == null) { + if (this.workManagerName == null) { + throw new IllegalArgumentException("Either 'workManager' or 'workManagerName' must be specified"); + } + this.workManager = lookup(this.workManagerName, WorkManager.class); + } + } + + private WorkManager obtainWorkManager() { + Assert.state(this.workManager != null, "No WorkManager specified"); + return this.workManager; + } + + + //------------------------------------------------------------------------- + // Implementation of the Spring SchedulingTaskExecutor interface + //------------------------------------------------------------------------- + + @Override + public void execute(Runnable task) { + Work work = new DelegatingWork(this.taskDecorator != null ? this.taskDecorator.decorate(task) : task); + try { + if (this.workListener != null) { + obtainWorkManager().schedule(work, this.workListener); + } + else { + obtainWorkManager().schedule(work); + } + } + catch (WorkRejectedException ex) { + throw new TaskRejectedException("CommonJ WorkManager did not accept task: " + task, ex); + } + catch (WorkException ex) { + throw new SchedulingException("Could not schedule task on CommonJ WorkManager", ex); + } + } + + @Override + public void execute(Runnable task, long startTimeout) { + execute(task); + } + + @Override + public Future submit(Runnable task) { + FutureTask future = new FutureTask<>(task, null); + execute(future); + return future; + } + + @Override + public Future submit(Callable task) { + FutureTask future = new FutureTask<>(task); + execute(future); + return future; + } + + @Override + public ListenableFuture submitListenable(Runnable task) { + ListenableFutureTask future = new ListenableFutureTask<>(task, null); + execute(future); + return future; + } + + @Override + public ListenableFuture submitListenable(Callable task) { + ListenableFutureTask future = new ListenableFutureTask<>(task); + execute(future); + return future; + } + + + //------------------------------------------------------------------------- + // Implementation of the CommonJ WorkManager interface + //------------------------------------------------------------------------- + + @Override + public WorkItem schedule(Work work) throws WorkException, IllegalArgumentException { + return obtainWorkManager().schedule(work); + } + + @Override + public WorkItem schedule(Work work, WorkListener workListener) throws WorkException { + return obtainWorkManager().schedule(work, workListener); + } + + @Override + @SuppressWarnings("rawtypes") + public boolean waitForAll(Collection workItems, long timeout) throws InterruptedException { + return obtainWorkManager().waitForAll(workItems, timeout); + } + + @Override + @SuppressWarnings("rawtypes") + public Collection waitForAny(Collection workItems, long timeout) throws InterruptedException { + return obtainWorkManager().waitForAny(workItems, timeout); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/commonj/package-info.java b/spring-context-support/src/main/java/org/springframework/scheduling/commonj/package-info.java new file mode 100644 index 0000000..ca0fbea --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/scheduling/commonj/package-info.java @@ -0,0 +1,10 @@ +/** + * Convenience classes for scheduling based on the CommonJ WorkManager/TimerManager + * facility, as supported by IBM WebSphere 6.0+ and BEA WebLogic 9.0+. + */ +@NonNullApi +@NonNullFields +package org.springframework.scheduling.commonj; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/AdaptableJobFactory.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/AdaptableJobFactory.java new file mode 100644 index 0000000..33898c1 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/AdaptableJobFactory.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.quartz; + +import org.quartz.Job; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.spi.JobFactory; +import org.quartz.spi.TriggerFiredBundle; + +import org.springframework.util.ReflectionUtils; + +/** + * {@link JobFactory} implementation that supports {@link java.lang.Runnable} + * objects as well as standard Quartz {@link org.quartz.Job} instances. + * + *

    Compatible with Quartz 2.1.4 and higher, as of Spring 4.1. + * + * @author Juergen Hoeller + * @since 2.0 + * @see DelegatingJob + * @see #adaptJob(Object) + */ +public class AdaptableJobFactory implements JobFactory { + + @Override + public Job newJob(TriggerFiredBundle bundle, Scheduler scheduler) throws SchedulerException { + try { + Object jobObject = createJobInstance(bundle); + return adaptJob(jobObject); + } + catch (Throwable ex) { + throw new SchedulerException("Job instantiation failed", ex); + } + } + + /** + * Create an instance of the specified job class. + *

    Can be overridden to post-process the job instance. + * @param bundle the TriggerFiredBundle from which the JobDetail + * and other info relating to the trigger firing can be obtained + * @return the job instance + * @throws Exception if job instantiation failed + */ + protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception { + Class jobClass = bundle.getJobDetail().getJobClass(); + return ReflectionUtils.accessibleConstructor(jobClass).newInstance(); + } + + /** + * Adapt the given job object to the Quartz Job interface. + *

    The default implementation supports straight Quartz Jobs + * as well as Runnables, which get wrapped in a DelegatingJob. + * @param jobObject the original instance of the specified job class + * @return the adapted Quartz Job instance + * @throws Exception if the given job could not be adapted + * @see DelegatingJob + */ + protected Job adaptJob(Object jobObject) throws Exception { + if (jobObject instanceof Job) { + return (Job) jobObject; + } + else if (jobObject instanceof Runnable) { + return new DelegatingJob((Runnable) jobObject); + } + else { + throw new IllegalArgumentException( + "Unable to execute job class [" + jobObject.getClass().getName() + + "]: only [org.quartz.Job] and [java.lang.Runnable] supported."); + } + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/CronTriggerFactoryBean.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/CronTriggerFactoryBean.java new file mode 100644 index 0000000..9c98851 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/CronTriggerFactoryBean.java @@ -0,0 +1,284 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.quartz; + +import java.text.ParseException; +import java.util.Date; +import java.util.Map; +import java.util.TimeZone; + +import org.quartz.CronTrigger; +import org.quartz.JobDataMap; +import org.quartz.JobDetail; +import org.quartz.Scheduler; +import org.quartz.impl.triggers.CronTriggerImpl; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.Constants; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * A Spring {@link FactoryBean} for creating a Quartz {@link org.quartz.CronTrigger} + * instance, supporting bean-style usage for trigger configuration. + * + *

    {@code CronTrigger(Impl)} itself is already a JavaBean but lacks sensible defaults. + * This class uses the Spring bean name as job name, the Quartz default group ("DEFAULT") + * as job group, the current time as start time, and indefinite repetition, if not specified. + * + *

    This class will also register the trigger with the job name and group of + * a given {@link org.quartz.JobDetail}. This allows {@link SchedulerFactoryBean} + * to automatically register a trigger for the corresponding JobDetail, + * instead of registering the JobDetail separately. + * + * @author Juergen Hoeller + * @since 3.1 + * @see #setName + * @see #setGroup + * @see #setStartDelay + * @see #setJobDetail + * @see SchedulerFactoryBean#setTriggers + * @see SchedulerFactoryBean#setJobDetails + */ +public class CronTriggerFactoryBean implements FactoryBean, BeanNameAware, InitializingBean { + + /** Constants for the CronTrigger class. */ + private static final Constants constants = new Constants(CronTrigger.class); + + + @Nullable + private String name; + + @Nullable + private String group; + + @Nullable + private JobDetail jobDetail; + + private JobDataMap jobDataMap = new JobDataMap(); + + @Nullable + private Date startTime; + + private long startDelay = 0; + + @Nullable + private String cronExpression; + + @Nullable + private TimeZone timeZone; + + @Nullable + private String calendarName; + + private int priority; + + private int misfireInstruction; + + @Nullable + private String description; + + @Nullable + private String beanName; + + @Nullable + private CronTrigger cronTrigger; + + + /** + * Specify the trigger's name. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Specify the trigger's group. + */ + public void setGroup(String group) { + this.group = group; + } + + /** + * Set the JobDetail that this trigger should be associated with. + */ + public void setJobDetail(JobDetail jobDetail) { + this.jobDetail = jobDetail; + } + + /** + * Set the trigger's JobDataMap. + * @see #setJobDataAsMap + */ + public void setJobDataMap(JobDataMap jobDataMap) { + this.jobDataMap = jobDataMap; + } + + /** + * Return the trigger's JobDataMap. + */ + public JobDataMap getJobDataMap() { + return this.jobDataMap; + } + + /** + * Register objects in the JobDataMap via a given Map. + *

    These objects will be available to this Trigger only, + * in contrast to objects in the JobDetail's data map. + * @param jobDataAsMap a Map with String keys and any objects as values + * (for example Spring-managed beans) + */ + public void setJobDataAsMap(Map jobDataAsMap) { + this.jobDataMap.putAll(jobDataAsMap); + } + + /** + * Set a specific start time for the trigger. + *

    Note that a dynamically computed {@link #setStartDelay} specification + * overrides a static timestamp set here. + */ + public void setStartTime(Date startTime) { + this.startTime = startTime; + } + + /** + * Set the start delay in milliseconds. + *

    The start delay is added to the current system time (when the bean starts) + * to control the start time of the trigger. + */ + public void setStartDelay(long startDelay) { + Assert.isTrue(startDelay >= 0, "Start delay cannot be negative"); + this.startDelay = startDelay; + } + + /** + * Specify the cron expression for this trigger. + */ + public void setCronExpression(String cronExpression) { + this.cronExpression = cronExpression; + } + + /** + * Specify the time zone for this trigger's cron expression. + */ + public void setTimeZone(TimeZone timeZone) { + this.timeZone = timeZone; + } + + /** + * Associate a specific calendar with this cron trigger. + */ + public void setCalendarName(String calendarName) { + this.calendarName = calendarName; + } + + /** + * Specify the priority of this trigger. + */ + public void setPriority(int priority) { + this.priority = priority; + } + + /** + * Specify a misfire instruction for this trigger. + */ + public void setMisfireInstruction(int misfireInstruction) { + this.misfireInstruction = misfireInstruction; + } + + /** + * Set the misfire instruction via the name of the corresponding + * constant in the {@link org.quartz.CronTrigger} class. + * Default is {@code MISFIRE_INSTRUCTION_SMART_POLICY}. + * @see org.quartz.CronTrigger#MISFIRE_INSTRUCTION_FIRE_ONCE_NOW + * @see org.quartz.CronTrigger#MISFIRE_INSTRUCTION_DO_NOTHING + * @see org.quartz.Trigger#MISFIRE_INSTRUCTION_SMART_POLICY + */ + public void setMisfireInstructionName(String constantName) { + this.misfireInstruction = constants.asNumber(constantName).intValue(); + } + + /** + * Associate a textual description with this trigger. + */ + public void setDescription(String description) { + this.description = description; + } + + @Override + public void setBeanName(String beanName) { + this.beanName = beanName; + } + + + @Override + public void afterPropertiesSet() throws ParseException { + Assert.notNull(this.cronExpression, "Property 'cronExpression' is required"); + + if (this.name == null) { + this.name = this.beanName; + } + if (this.group == null) { + this.group = Scheduler.DEFAULT_GROUP; + } + if (this.jobDetail != null) { + this.jobDataMap.put("jobDetail", this.jobDetail); + } + if (this.startDelay > 0 || this.startTime == null) { + this.startTime = new Date(System.currentTimeMillis() + this.startDelay); + } + if (this.timeZone == null) { + this.timeZone = TimeZone.getDefault(); + } + + CronTriggerImpl cti = new CronTriggerImpl(); + cti.setName(this.name != null ? this.name : toString()); + cti.setGroup(this.group); + if (this.jobDetail != null) { + cti.setJobKey(this.jobDetail.getKey()); + } + cti.setJobDataMap(this.jobDataMap); + cti.setStartTime(this.startTime); + cti.setCronExpression(this.cronExpression); + cti.setTimeZone(this.timeZone); + cti.setCalendarName(this.calendarName); + cti.setPriority(this.priority); + cti.setMisfireInstruction(this.misfireInstruction); + cti.setDescription(this.description); + this.cronTrigger = cti; + } + + + @Override + @Nullable + public CronTrigger getObject() { + return this.cronTrigger; + } + + @Override + public Class getObjectType() { + return CronTrigger.class; + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/DelegatingJob.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/DelegatingJob.java new file mode 100644 index 0000000..80cfeaa --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/DelegatingJob.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.quartz; + +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; + +import org.springframework.util.Assert; + +/** + * Simple Quartz {@link org.quartz.Job} adapter that delegates to a + * given {@link java.lang.Runnable} instance. + * + *

    Typically used in combination with property injection on the + * Runnable instance, receiving parameters from the Quartz JobDataMap + * that way instead of via the JobExecutionContext. + * + * @author Juergen Hoeller + * @since 2.0 + * @see SpringBeanJobFactory + * @see org.quartz.Job#execute(org.quartz.JobExecutionContext) + */ +public class DelegatingJob implements Job { + + private final Runnable delegate; + + + /** + * Create a new DelegatingJob. + * @param delegate the Runnable implementation to delegate to + */ + public DelegatingJob(Runnable delegate) { + Assert.notNull(delegate, "Delegate must not be null"); + this.delegate = delegate; + } + + /** + * Return the wrapped Runnable implementation. + */ + public final Runnable getDelegate() { + return this.delegate; + } + + + /** + * Delegates execution to the underlying Runnable. + */ + @Override + public void execute(JobExecutionContext context) throws JobExecutionException { + this.delegate.run(); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/JobDetailFactoryBean.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/JobDetailFactoryBean.java new file mode 100644 index 0000000..78210ab --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/JobDetailFactoryBean.java @@ -0,0 +1,236 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.quartz; + +import java.util.Map; + +import org.quartz.Job; +import org.quartz.JobDataMap; +import org.quartz.JobDetail; +import org.quartz.Scheduler; +import org.quartz.impl.JobDetailImpl; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * A Spring {@link FactoryBean} for creating a Quartz {@link org.quartz.JobDetail} + * instance, supporting bean-style usage for JobDetail configuration. + * + *

    {@code JobDetail(Impl)} itself is already a JavaBean but lacks + * sensible defaults. This class uses the Spring bean name as job name, + * and the Quartz default group ("DEFAULT") as job group if not specified. + * + * @author Juergen Hoeller + * @since 3.1 + * @see #setName + * @see #setGroup + * @see org.springframework.beans.factory.BeanNameAware + * @see org.quartz.Scheduler#DEFAULT_GROUP + */ +public class JobDetailFactoryBean + implements FactoryBean, BeanNameAware, ApplicationContextAware, InitializingBean { + + @Nullable + private String name; + + @Nullable + private String group; + + @Nullable + private Class jobClass; + + private JobDataMap jobDataMap = new JobDataMap(); + + private boolean durability = false; + + private boolean requestsRecovery = false; + + @Nullable + private String description; + + @Nullable + private String beanName; + + @Nullable + private ApplicationContext applicationContext; + + @Nullable + private String applicationContextJobDataKey; + + @Nullable + private JobDetail jobDetail; + + + /** + * Specify the job's name. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Specify the job's group. + */ + public void setGroup(String group) { + this.group = group; + } + + /** + * Specify the job's implementation class. + */ + public void setJobClass(Class jobClass) { + this.jobClass = jobClass; + } + + /** + * Set the job's JobDataMap. + * @see #setJobDataAsMap + */ + public void setJobDataMap(JobDataMap jobDataMap) { + this.jobDataMap = jobDataMap; + } + + /** + * Return the job's JobDataMap. + */ + public JobDataMap getJobDataMap() { + return this.jobDataMap; + } + + /** + * Register objects in the JobDataMap via a given Map. + *

    These objects will be available to this Job only, + * in contrast to objects in the SchedulerContext. + *

    Note: When using persistent Jobs whose JobDetail will be kept in the + * database, do not put Spring-managed beans or an ApplicationContext + * reference into the JobDataMap but rather into the SchedulerContext. + * @param jobDataAsMap a Map with String keys and any objects as values + * (for example Spring-managed beans) + * @see org.springframework.scheduling.quartz.SchedulerFactoryBean#setSchedulerContextAsMap + */ + public void setJobDataAsMap(Map jobDataAsMap) { + getJobDataMap().putAll(jobDataAsMap); + } + + /** + * Specify the job's durability, i.e. whether it should remain stored + * in the job store even if no triggers point to it anymore. + */ + public void setDurability(boolean durability) { + this.durability = durability; + } + + /** + * Set the recovery flag for this job, i.e. whether or not the job should + * get re-executed if a 'recovery' or 'fail-over' situation is encountered. + */ + public void setRequestsRecovery(boolean requestsRecovery) { + this.requestsRecovery = requestsRecovery; + } + + /** + * Set a textual description for this job. + */ + public void setDescription(String description) { + this.description = description; + } + + @Override + public void setBeanName(String beanName) { + this.beanName = beanName; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + /** + * Set the key of an ApplicationContext reference to expose in the JobDataMap, + * for example "applicationContext". Default is none. + * Only applicable when running in a Spring ApplicationContext. + *

    In case of a QuartzJobBean, the reference will be applied to the Job + * instance as bean property. An "applicationContext" attribute will correspond + * to a "setApplicationContext" method in that scenario. + *

    Note that BeanFactory callback interfaces like ApplicationContextAware + * are not automatically applied to Quartz Job instances, because Quartz + * itself is responsible for the lifecycle of its Jobs. + *

    Note: When using persistent job stores where JobDetail contents will + * be kept in the database, do not put an ApplicationContext reference into + * the JobDataMap but rather into the SchedulerContext. + * @see org.springframework.scheduling.quartz.SchedulerFactoryBean#setApplicationContextSchedulerContextKey + * @see org.springframework.context.ApplicationContext + */ + public void setApplicationContextJobDataKey(String applicationContextJobDataKey) { + this.applicationContextJobDataKey = applicationContextJobDataKey; + } + + + @Override + public void afterPropertiesSet() { + Assert.notNull(this.jobClass, "Property 'jobClass' is required"); + + if (this.name == null) { + this.name = this.beanName; + } + if (this.group == null) { + this.group = Scheduler.DEFAULT_GROUP; + } + if (this.applicationContextJobDataKey != null) { + if (this.applicationContext == null) { + throw new IllegalStateException( + "JobDetailBean needs to be set up in an ApplicationContext " + + "to be able to handle an 'applicationContextJobDataKey'"); + } + getJobDataMap().put(this.applicationContextJobDataKey, this.applicationContext); + } + + JobDetailImpl jdi = new JobDetailImpl(); + jdi.setName(this.name != null ? this.name : toString()); + jdi.setGroup(this.group); + jdi.setJobClass(this.jobClass); + jdi.setJobDataMap(this.jobDataMap); + jdi.setDurability(this.durability); + jdi.setRequestsRecovery(this.requestsRecovery); + jdi.setDescription(this.description); + this.jobDetail = jdi; + } + + + @Override + @Nullable + public JobDetail getObject() { + return this.jobDetail; + } + + @Override + public Class getObjectType() { + return JobDetail.class; + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/JobMethodInvocationFailedException.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/JobMethodInvocationFailedException.java new file mode 100644 index 0000000..8fd115e --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/JobMethodInvocationFailedException.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.quartz; + +import org.springframework.core.NestedRuntimeException; +import org.springframework.util.MethodInvoker; + +/** + * Unchecked exception that wraps an exception thrown from a target method. + * Propagated to the Quartz scheduler from a Job that reflectively invokes + * an arbitrary target method. + * + * @author Juergen Hoeller + * @since 2.5.3 + * @see MethodInvokingJobDetailFactoryBean + */ +@SuppressWarnings("serial") +public class JobMethodInvocationFailedException extends NestedRuntimeException { + + /** + * Constructor for JobMethodInvocationFailedException. + * @param methodInvoker the MethodInvoker used for reflective invocation + * @param cause the root cause (as thrown from the target method) + */ + public JobMethodInvocationFailedException(MethodInvoker methodInvoker, Throwable cause) { + super("Invocation of method '" + methodInvoker.getTargetMethod() + + "' on target class [" + methodInvoker.getTargetClass() + "] failed", cause); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalDataSourceJobStore.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalDataSourceJobStore.java new file mode 100644 index 0000000..e0b12f4 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalDataSourceJobStore.java @@ -0,0 +1,173 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.quartz; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.quartz.SchedulerConfigException; +import org.quartz.impl.jdbcjobstore.JobStoreCMT; +import org.quartz.impl.jdbcjobstore.SimpleSemaphore; +import org.quartz.spi.ClassLoadHelper; +import org.quartz.spi.SchedulerSignaler; +import org.quartz.utils.ConnectionProvider; +import org.quartz.utils.DBConnectionManager; + +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.jdbc.support.MetaDataAccessException; +import org.springframework.lang.Nullable; + +/** + * Subclass of Quartz's {@link JobStoreCMT} class that delegates to a Spring-managed + * {@link DataSource} instead of using a Quartz-managed JDBC connection pool. + * This JobStore will be used if SchedulerFactoryBean's "dataSource" property is set. + * + *

    Supports both transactional and non-transactional DataSource access. + * With a non-XA DataSource and local Spring transactions, a single DataSource + * argument is sufficient. In case of an XA DataSource and global JTA transactions, + * SchedulerFactoryBean's "nonTransactionalDataSource" property should be set, + * passing in a non-XA DataSource that will not participate in global transactions. + * + *

    Operations performed by this JobStore will properly participate in any + * kind of Spring-managed transaction, as it uses Spring's DataSourceUtils + * connection handling methods that are aware of a current transaction. + * + *

    Note that all Quartz Scheduler operations that affect the persistent + * job store should usually be performed within active transactions, + * as they assume to get proper locks etc. + * + * @author Juergen Hoeller + * @since 1.1 + * @see SchedulerFactoryBean#setDataSource + * @see SchedulerFactoryBean#setNonTransactionalDataSource + * @see org.springframework.jdbc.datasource.DataSourceUtils#doGetConnection + * @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection + */ +@SuppressWarnings("unchecked") // due to a warning in Quartz 2.2's JobStoreCMT +public class LocalDataSourceJobStore extends JobStoreCMT { + + /** + * Name used for the transactional ConnectionProvider for Quartz. + * This provider will delegate to the local Spring-managed DataSource. + * @see org.quartz.utils.DBConnectionManager#addConnectionProvider + * @see SchedulerFactoryBean#setDataSource + */ + public static final String TX_DATA_SOURCE_PREFIX = "springTxDataSource."; + + /** + * Name used for the non-transactional ConnectionProvider for Quartz. + * This provider will delegate to the local Spring-managed DataSource. + * @see org.quartz.utils.DBConnectionManager#addConnectionProvider + * @see SchedulerFactoryBean#setDataSource + */ + public static final String NON_TX_DATA_SOURCE_PREFIX = "springNonTxDataSource."; + + + @Nullable + private DataSource dataSource; + + + @Override + public void initialize(ClassLoadHelper loadHelper, SchedulerSignaler signaler) throws SchedulerConfigException { + // Absolutely needs thread-bound DataSource to initialize. + this.dataSource = SchedulerFactoryBean.getConfigTimeDataSource(); + if (this.dataSource == null) { + throw new SchedulerConfigException("No local DataSource found for configuration - " + + "'dataSource' property must be set on SchedulerFactoryBean"); + } + + // Configure transactional connection settings for Quartz. + setDataSource(TX_DATA_SOURCE_PREFIX + getInstanceName()); + setDontSetAutoCommitFalse(true); + + // Register transactional ConnectionProvider for Quartz. + DBConnectionManager.getInstance().addConnectionProvider( + TX_DATA_SOURCE_PREFIX + getInstanceName(), + new ConnectionProvider() { + @Override + public Connection getConnection() throws SQLException { + // Return a transactional Connection, if any. + return DataSourceUtils.doGetConnection(dataSource); + } + @Override + public void shutdown() { + // Do nothing - a Spring-managed DataSource has its own lifecycle. + } + @Override + public void initialize() { + // Do nothing - a Spring-managed DataSource has its own lifecycle. + } + } + ); + + // Non-transactional DataSource is optional: fall back to default + // DataSource if not explicitly specified. + DataSource nonTxDataSource = SchedulerFactoryBean.getConfigTimeNonTransactionalDataSource(); + final DataSource nonTxDataSourceToUse = (nonTxDataSource != null ? nonTxDataSource : this.dataSource); + + // Configure non-transactional connection settings for Quartz. + setNonManagedTXDataSource(NON_TX_DATA_SOURCE_PREFIX + getInstanceName()); + + // Register non-transactional ConnectionProvider for Quartz. + DBConnectionManager.getInstance().addConnectionProvider( + NON_TX_DATA_SOURCE_PREFIX + getInstanceName(), + new ConnectionProvider() { + @Override + public Connection getConnection() throws SQLException { + // Always return a non-transactional Connection. + return nonTxDataSourceToUse.getConnection(); + } + @Override + public void shutdown() { + // Do nothing - a Spring-managed DataSource has its own lifecycle. + } + @Override + public void initialize() { + // Do nothing - a Spring-managed DataSource has its own lifecycle. + } + } + ); + + // No, if HSQL is the platform, we really don't want to use locks... + try { + String productName = JdbcUtils.extractDatabaseMetaData(this.dataSource, + DatabaseMetaData::getDatabaseProductName); + productName = JdbcUtils.commonDatabaseName(productName); + if (productName != null && productName.toLowerCase().contains("hsql")) { + setUseDBLocks(false); + setLockHandler(new SimpleSemaphore()); + } + } + catch (MetaDataAccessException ex) { + logWarnIfNonZero(1, "Could not detect database type. Assuming locks can be taken."); + } + + super.initialize(loadHelper, signaler); + + } + + @Override + protected void closeConnection(Connection con) { + // Will work for transactional and non-transactional connections. + DataSourceUtils.releaseConnection(con, this.dataSource); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalTaskExecutorThreadPool.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalTaskExecutorThreadPool.java new file mode 100644 index 0000000..42af660 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalTaskExecutorThreadPool.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.quartz; + +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.quartz.SchedulerConfigException; +import org.quartz.spi.ThreadPool; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Quartz {@link ThreadPool} adapter that delegates to a Spring-managed + * {@link Executor} instance, specified on {@link SchedulerFactoryBean}. + * + * @author Juergen Hoeller + * @since 2.0 + * @see SchedulerFactoryBean#setTaskExecutor + */ +public class LocalTaskExecutorThreadPool implements ThreadPool { + + /** Logger available to subclasses. */ + protected final Log logger = LogFactory.getLog(getClass()); + + @Nullable + private Executor taskExecutor; + + + @Override + public void setInstanceId(String schedInstId) { + } + + @Override + public void setInstanceName(String schedName) { + } + + + @Override + public void initialize() throws SchedulerConfigException { + // Absolutely needs thread-bound Executor to initialize. + this.taskExecutor = SchedulerFactoryBean.getConfigTimeTaskExecutor(); + if (this.taskExecutor == null) { + throw new SchedulerConfigException("No local Executor found for configuration - " + + "'taskExecutor' property must be set on SchedulerFactoryBean"); + } + } + + @Override + public void shutdown(boolean waitForJobsToComplete) { + } + + @Override + public int getPoolSize() { + return -1; + } + + + @Override + public boolean runInThread(Runnable runnable) { + Assert.state(this.taskExecutor != null, "No TaskExecutor available"); + try { + this.taskExecutor.execute(runnable); + return true; + } + catch (RejectedExecutionException ex) { + logger.error("Task has been rejected by TaskExecutor", ex); + return false; + } + } + + @Override + public int blockForAvailableThreads() { + // The present implementation always returns 1, making Quartz + // always schedule any tasks that it feels like scheduling. + // This could be made smarter for specific TaskExecutors, + // for example calling {@code getMaximumPoolSize() - getActiveCount()} + // on a {@code java.util.concurrent.ThreadPoolExecutor}. + return 1; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/MethodInvokingJobDetailFactoryBean.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/MethodInvokingJobDetailFactoryBean.java new file mode 100644 index 0000000..f4658c7 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/MethodInvokingJobDetailFactoryBean.java @@ -0,0 +1,300 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.quartz; + +import java.lang.reflect.InvocationTargetException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.quartz.DisallowConcurrentExecution; +import org.quartz.Job; +import org.quartz.JobDetail; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.PersistJobDataAfterExecution; +import org.quartz.Scheduler; +import org.quartz.impl.JobDetailImpl; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.support.ArgumentConvertingMethodInvoker; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.MethodInvoker; + +/** + * {@link org.springframework.beans.factory.FactoryBean} that exposes a + * {@link org.quartz.JobDetail} object which delegates job execution to a + * specified (static or non-static) method. Avoids the need for implementing + * a one-line Quartz Job that just invokes an existing service method on a + * Spring-managed target bean. + * + *

    Inherits common configuration properties from the {@link MethodInvoker} + * base class, such as {@link #setTargetObject "targetObject"} and + * {@link #setTargetMethod "targetMethod"}, adding support for lookup of the target + * bean by name through the {@link #setTargetBeanName "targetBeanName"} property + * (as alternative to specifying a "targetObject" directly, allowing for + * non-singleton target objects). + * + *

    Supports both concurrently running jobs and non-currently running + * jobs through the "concurrent" property. Jobs created by this + * MethodInvokingJobDetailFactoryBean are by default volatile and durable + * (according to Quartz terminology). + * + *

    NOTE: JobDetails created via this FactoryBean are not + * serializable and thus not suitable for persistent job stores. + * You need to implement your own Quartz Job as a thin wrapper for each case + * where you want a persistent job to delegate to a specific service method. + * + *

    Compatible with Quartz 2.1.4 and higher, as of Spring 4.1. + * + * @author Juergen Hoeller + * @author Alef Arendsen + * @since 18.02.2004 + * @see #setTargetBeanName + * @see #setTargetObject + * @see #setTargetMethod + * @see #setConcurrent + */ +public class MethodInvokingJobDetailFactoryBean extends ArgumentConvertingMethodInvoker + implements FactoryBean, BeanNameAware, BeanClassLoaderAware, BeanFactoryAware, InitializingBean { + + @Nullable + private String name; + + private String group = Scheduler.DEFAULT_GROUP; + + private boolean concurrent = true; + + @Nullable + private String targetBeanName; + + @Nullable + private String beanName; + + @Nullable + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + @Nullable + private BeanFactory beanFactory; + + @Nullable + private JobDetail jobDetail; + + + /** + * Set the name of the job. + *

    Default is the bean name of this FactoryBean. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Set the group of the job. + *

    Default is the default group of the Scheduler. + * @see org.quartz.Scheduler#DEFAULT_GROUP + */ + public void setGroup(String group) { + this.group = group; + } + + /** + * Specify whether or not multiple jobs should be run in a concurrent fashion. + * The behavior when one does not want concurrent jobs to be executed is + * realized through adding the {@code @PersistJobDataAfterExecution} and + * {@code @DisallowConcurrentExecution} markers. + * More information on stateful versus stateless jobs can be found + * here. + *

    The default setting is to run jobs concurrently. + */ + public void setConcurrent(boolean concurrent) { + this.concurrent = concurrent; + } + + /** + * Set the name of the target bean in the Spring BeanFactory. + *

    This is an alternative to specifying {@link #setTargetObject "targetObject"}, + * allowing for non-singleton beans to be invoked. Note that specified + * "targetObject" and {@link #setTargetClass "targetClass"} values will + * override the corresponding effect of this "targetBeanName" setting + * (i.e. statically pre-define the bean type or even the bean object). + */ + public void setTargetBeanName(String targetBeanName) { + this.targetBeanName = targetBeanName; + } + + @Override + public void setBeanName(String beanName) { + this.beanName = beanName; + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + protected Class resolveClassName(String className) throws ClassNotFoundException { + return ClassUtils.forName(className, this.beanClassLoader); + } + + + @Override + public void afterPropertiesSet() throws ClassNotFoundException, NoSuchMethodException { + prepare(); + + // Use specific name if given, else fall back to bean name. + String name = (this.name != null ? this.name : this.beanName); + + // Consider the concurrent flag to choose between stateful and stateless job. + Class jobClass = (this.concurrent ? MethodInvokingJob.class : StatefulMethodInvokingJob.class); + + // Build JobDetail instance. + JobDetailImpl jdi = new JobDetailImpl(); + jdi.setName(name != null ? name : toString()); + jdi.setGroup(this.group); + jdi.setJobClass(jobClass); + jdi.setDurability(true); + jdi.getJobDataMap().put("methodInvoker", this); + this.jobDetail = jdi; + + postProcessJobDetail(this.jobDetail); + } + + /** + * Callback for post-processing the JobDetail to be exposed by this FactoryBean. + *

    The default implementation is empty. Can be overridden in subclasses. + * @param jobDetail the JobDetail prepared by this FactoryBean + */ + protected void postProcessJobDetail(JobDetail jobDetail) { + } + + + /** + * Overridden to support the {@link #setTargetBeanName "targetBeanName"} feature. + */ + @Override + public Class getTargetClass() { + Class targetClass = super.getTargetClass(); + if (targetClass == null && this.targetBeanName != null) { + Assert.state(this.beanFactory != null, "BeanFactory must be set when using 'targetBeanName'"); + targetClass = this.beanFactory.getType(this.targetBeanName); + } + return targetClass; + } + + /** + * Overridden to support the {@link #setTargetBeanName "targetBeanName"} feature. + */ + @Override + public Object getTargetObject() { + Object targetObject = super.getTargetObject(); + if (targetObject == null && this.targetBeanName != null) { + Assert.state(this.beanFactory != null, "BeanFactory must be set when using 'targetBeanName'"); + targetObject = this.beanFactory.getBean(this.targetBeanName); + } + return targetObject; + } + + + @Override + @Nullable + public JobDetail getObject() { + return this.jobDetail; + } + + @Override + public Class getObjectType() { + return (this.jobDetail != null ? this.jobDetail.getClass() : JobDetail.class); + } + + @Override + public boolean isSingleton() { + return true; + } + + + /** + * Quartz Job implementation that invokes a specified method. + * Automatically applied by MethodInvokingJobDetailFactoryBean. + */ + public static class MethodInvokingJob extends QuartzJobBean { + + protected static final Log logger = LogFactory.getLog(MethodInvokingJob.class); + + @Nullable + private MethodInvoker methodInvoker; + + /** + * Set the MethodInvoker to use. + */ + public void setMethodInvoker(MethodInvoker methodInvoker) { + this.methodInvoker = methodInvoker; + } + + /** + * Invoke the method via the MethodInvoker. + */ + @Override + protected void executeInternal(JobExecutionContext context) throws JobExecutionException { + Assert.state(this.methodInvoker != null, "No MethodInvoker set"); + try { + context.setResult(this.methodInvoker.invoke()); + } + catch (InvocationTargetException ex) { + if (ex.getTargetException() instanceof JobExecutionException) { + // -> JobExecutionException, to be logged at info level by Quartz + throw (JobExecutionException) ex.getTargetException(); + } + else { + // -> "unhandled exception", to be logged at error level by Quartz + throw new JobMethodInvocationFailedException(this.methodInvoker, ex.getTargetException()); + } + } + catch (Exception ex) { + // -> "unhandled exception", to be logged at error level by Quartz + throw new JobMethodInvocationFailedException(this.methodInvoker, ex); + } + } + } + + + /** + * Extension of the MethodInvokingJob, implementing the StatefulJob interface. + * Quartz checks whether or not jobs are stateful and if so, + * won't let jobs interfere with each other. + */ + @PersistJobDataAfterExecution + @DisallowConcurrentExecution + public static class StatefulMethodInvokingJob extends MethodInvokingJob { + + // No implementation, just an addition of the tag interface StatefulJob + // in order to allow stateful method invoking jobs. + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/QuartzJobBean.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/QuartzJobBean.java new file mode 100644 index 0000000..c63934d --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/QuartzJobBean.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.quartz; + +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.SchedulerException; + +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.PropertyAccessorFactory; + +/** + * Simple implementation of the Quartz Job interface, applying the + * passed-in JobDataMap and also the SchedulerContext as bean property + * values. This is appropriate because a new Job instance will be created + * for each execution. JobDataMap entries will override SchedulerContext + * entries with the same keys. + * + *

    For example, let's assume that the JobDataMap contains a key + * "myParam" with value "5": The Job implementation can then expose + * a bean property "myParam" of type int to receive such a value, + * i.e. a method "setMyParam(int)". This will also work for complex + * types like business objects etc. + * + *

    Note that the preferred way to apply dependency injection + * to Job instances is via a JobFactory: that is, to specify + * {@link SpringBeanJobFactory} as Quartz JobFactory (typically via + * {@link SchedulerFactoryBean#setJobFactory} SchedulerFactoryBean's "jobFactory" property}). + * This allows to implement dependency-injected Quartz Jobs without + * a dependency on Spring base classes. + * + * @author Juergen Hoeller + * @since 18.02.2004 + * @see org.quartz.JobExecutionContext#getMergedJobDataMap() + * @see org.quartz.Scheduler#getContext() + * @see SchedulerFactoryBean#setSchedulerContextAsMap + * @see SpringBeanJobFactory + * @see SchedulerFactoryBean#setJobFactory + */ +public abstract class QuartzJobBean implements Job { + + /** + * This implementation applies the passed-in job data map as bean property + * values, and delegates to {@code executeInternal} afterwards. + * @see #executeInternal + */ + @Override + public final void execute(JobExecutionContext context) throws JobExecutionException { + try { + BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.addPropertyValues(context.getScheduler().getContext()); + pvs.addPropertyValues(context.getMergedJobDataMap()); + bw.setPropertyValues(pvs, true); + } + catch (SchedulerException ex) { + throw new JobExecutionException(ex); + } + executeInternal(context); + } + + /** + * Execute the actual job. The job data map will already have been + * applied as bean property values by execute. The contract is + * exactly the same as for the standard Quartz execute method. + * @see #execute + */ + protected abstract void executeInternal(JobExecutionContext context) throws JobExecutionException; + +} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/ResourceLoaderClassLoadHelper.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/ResourceLoaderClassLoadHelper.java new file mode 100644 index 0000000..996a598 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/ResourceLoaderClassLoadHelper.java @@ -0,0 +1,140 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.quartz; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.quartz.spi.ClassLoadHelper; + +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Wrapper that adapts from the Quartz {@link ClassLoadHelper} interface + * onto Spring's {@link ResourceLoader} interface. Used by default when + * the SchedulerFactoryBean runs in a Spring ApplicationContext. + * + * @author Juergen Hoeller + * @since 2.5.5 + * @see SchedulerFactoryBean#setApplicationContext + */ +public class ResourceLoaderClassLoadHelper implements ClassLoadHelper { + + protected static final Log logger = LogFactory.getLog(ResourceLoaderClassLoadHelper.class); + + @Nullable + private ResourceLoader resourceLoader; + + + /** + * Create a new ResourceLoaderClassLoadHelper for the default + * ResourceLoader. + * @see SchedulerFactoryBean#getConfigTimeResourceLoader() + */ + public ResourceLoaderClassLoadHelper() { + } + + /** + * Create a new ResourceLoaderClassLoadHelper for the given ResourceLoader. + * @param resourceLoader the ResourceLoader to delegate to + */ + public ResourceLoaderClassLoadHelper(@Nullable ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + + @Override + public void initialize() { + if (this.resourceLoader == null) { + this.resourceLoader = SchedulerFactoryBean.getConfigTimeResourceLoader(); + if (this.resourceLoader == null) { + this.resourceLoader = new DefaultResourceLoader(); + } + } + } + + @Override + public Class loadClass(String name) throws ClassNotFoundException { + Assert.state(this.resourceLoader != null, "ResourceLoaderClassLoadHelper not initialized"); + return ClassUtils.forName(name, this.resourceLoader.getClassLoader()); + } + + @SuppressWarnings("unchecked") + @Override + public Class loadClass(String name, Class clazz) throws ClassNotFoundException { + return (Class) loadClass(name); + } + + @Override + @Nullable + public URL getResource(String name) { + Assert.state(this.resourceLoader != null, "ResourceLoaderClassLoadHelper not initialized"); + Resource resource = this.resourceLoader.getResource(name); + if (resource.exists()) { + try { + return resource.getURL(); + } + catch (IOException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Could not load " + resource); + } + return null; + } + } + else { + return getClassLoader().getResource(name); + } + } + + @Override + @Nullable + public InputStream getResourceAsStream(String name) { + Assert.state(this.resourceLoader != null, "ResourceLoaderClassLoadHelper not initialized"); + Resource resource = this.resourceLoader.getResource(name); + if (resource.exists()) { + try { + return resource.getInputStream(); + } + catch (IOException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Could not load " + resource); + } + return null; + } + } + else { + return getClassLoader().getResourceAsStream(name); + } + } + + @Override + public ClassLoader getClassLoader() { + Assert.state(this.resourceLoader != null, "ResourceLoaderClassLoadHelper not initialized"); + ClassLoader classLoader = this.resourceLoader.getClassLoader(); + Assert.state(classLoader != null, "No ClassLoader"); + return classLoader; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessor.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessor.java new file mode 100644 index 0000000..de60cbb --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessor.java @@ -0,0 +1,375 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.quartz; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.quartz.Calendar; +import org.quartz.JobDetail; +import org.quartz.JobListener; +import org.quartz.ListenerManager; +import org.quartz.ObjectAlreadyExistsException; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.SchedulerListener; +import org.quartz.Trigger; +import org.quartz.TriggerListener; +import org.quartz.spi.ClassLoadHelper; +import org.quartz.xml.XMLSchedulingDataProcessor; + +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.io.ResourceLoader; +import org.springframework.lang.Nullable; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionStatus; + +/** + * Common base class for accessing a Quartz Scheduler, i.e. for registering jobs, + * triggers and listeners on a {@link org.quartz.Scheduler} instance. + * + *

    For concrete usage, check out the {@link SchedulerFactoryBean} and + * {@link SchedulerAccessorBean} classes. + * + *

    Compatible with Quartz 2.1.4 and higher, as of Spring 4.1. + * + * @author Juergen Hoeller + * @author Stephane Nicoll + * @since 2.5.6 + */ +public abstract class SchedulerAccessor implements ResourceLoaderAware { + + protected final Log logger = LogFactory.getLog(getClass()); + + private boolean overwriteExistingJobs = false; + + @Nullable + private String[] jobSchedulingDataLocations; + + @Nullable + private List jobDetails; + + @Nullable + private Map calendars; + + @Nullable + private List triggers; + + @Nullable + private SchedulerListener[] schedulerListeners; + + @Nullable + private JobListener[] globalJobListeners; + + @Nullable + private TriggerListener[] globalTriggerListeners; + + @Nullable + private PlatformTransactionManager transactionManager; + + @Nullable + protected ResourceLoader resourceLoader; + + + /** + * Set whether any jobs defined on this SchedulerFactoryBean should overwrite + * existing job definitions. Default is "false", to not overwrite already + * registered jobs that have been read in from a persistent job store. + */ + public void setOverwriteExistingJobs(boolean overwriteExistingJobs) { + this.overwriteExistingJobs = overwriteExistingJobs; + } + + /** + * Set the location of a Quartz job definition XML file that follows the + * "job_scheduling_data_1_5" XSD or better. Can be specified to automatically + * register jobs that are defined in such a file, possibly in addition + * to jobs defined directly on this SchedulerFactoryBean. + * @see org.quartz.xml.XMLSchedulingDataProcessor + */ + public void setJobSchedulingDataLocation(String jobSchedulingDataLocation) { + this.jobSchedulingDataLocations = new String[] {jobSchedulingDataLocation}; + } + + /** + * Set the locations of Quartz job definition XML files that follow the + * "job_scheduling_data_1_5" XSD or better. Can be specified to automatically + * register jobs that are defined in such files, possibly in addition + * to jobs defined directly on this SchedulerFactoryBean. + * @see org.quartz.xml.XMLSchedulingDataProcessor + */ + public void setJobSchedulingDataLocations(String... jobSchedulingDataLocations) { + this.jobSchedulingDataLocations = jobSchedulingDataLocations; + } + + /** + * Register a list of JobDetail objects with the Scheduler that + * this FactoryBean creates, to be referenced by Triggers. + *

    This is not necessary when a Trigger determines the JobDetail + * itself: In this case, the JobDetail will be implicitly registered + * in combination with the Trigger. + * @see #setTriggers + * @see org.quartz.JobDetail + */ + public void setJobDetails(JobDetail... jobDetails) { + // Use modifiable ArrayList here, to allow for further adding of + // JobDetail objects during autodetection of JobDetail-aware Triggers. + this.jobDetails = new ArrayList<>(Arrays.asList(jobDetails)); + } + + /** + * Register a list of Quartz Calendar objects with the Scheduler + * that this FactoryBean creates, to be referenced by Triggers. + * @param calendars a Map with calendar names as keys as Calendar + * objects as values + * @see org.quartz.Calendar + */ + public void setCalendars(Map calendars) { + this.calendars = calendars; + } + + /** + * Register a list of Trigger objects with the Scheduler that + * this FactoryBean creates. + *

    If the Trigger determines the corresponding JobDetail itself, + * the job will be automatically registered with the Scheduler. + * Else, the respective JobDetail needs to be registered via the + * "jobDetails" property of this FactoryBean. + * @see #setJobDetails + * @see org.quartz.JobDetail + */ + public void setTriggers(Trigger... triggers) { + this.triggers = Arrays.asList(triggers); + } + + /** + * Specify Quartz SchedulerListeners to be registered with the Scheduler. + */ + public void setSchedulerListeners(SchedulerListener... schedulerListeners) { + this.schedulerListeners = schedulerListeners; + } + + /** + * Specify global Quartz JobListeners to be registered with the Scheduler. + * Such JobListeners will apply to all Jobs in the Scheduler. + */ + public void setGlobalJobListeners(JobListener... globalJobListeners) { + this.globalJobListeners = globalJobListeners; + } + + /** + * Specify global Quartz TriggerListeners to be registered with the Scheduler. + * Such TriggerListeners will apply to all Triggers in the Scheduler. + */ + public void setGlobalTriggerListeners(TriggerListener... globalTriggerListeners) { + this.globalTriggerListeners = globalTriggerListeners; + } + + /** + * Set the transaction manager to be used for registering jobs and triggers + * that are defined by this SchedulerFactoryBean. Default is none; setting + * this only makes sense when specifying a DataSource for the Scheduler. + */ + public void setTransactionManager(PlatformTransactionManager transactionManager) { + this.transactionManager = transactionManager; + } + + @Override + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + + /** + * Register jobs and triggers (within a transaction, if possible). + */ + protected void registerJobsAndTriggers() throws SchedulerException { + TransactionStatus transactionStatus = null; + if (this.transactionManager != null) { + transactionStatus = this.transactionManager.getTransaction(TransactionDefinition.withDefaults()); + } + + try { + if (this.jobSchedulingDataLocations != null) { + ClassLoadHelper clh = new ResourceLoaderClassLoadHelper(this.resourceLoader); + clh.initialize(); + XMLSchedulingDataProcessor dataProcessor = new XMLSchedulingDataProcessor(clh); + for (String location : this.jobSchedulingDataLocations) { + dataProcessor.processFileAndScheduleJobs(location, getScheduler()); + } + } + + // Register JobDetails. + if (this.jobDetails != null) { + for (JobDetail jobDetail : this.jobDetails) { + addJobToScheduler(jobDetail); + } + } + else { + // Create empty list for easier checks when registering triggers. + this.jobDetails = new ArrayList<>(); + } + + // Register Calendars. + if (this.calendars != null) { + for (String calendarName : this.calendars.keySet()) { + Calendar calendar = this.calendars.get(calendarName); + getScheduler().addCalendar(calendarName, calendar, true, true); + } + } + + // Register Triggers. + if (this.triggers != null) { + for (Trigger trigger : this.triggers) { + addTriggerToScheduler(trigger); + } + } + } + + catch (Throwable ex) { + if (transactionStatus != null) { + try { + this.transactionManager.rollback(transactionStatus); + } + catch (TransactionException tex) { + logger.error("Job registration exception overridden by rollback exception", ex); + throw tex; + } + } + if (ex instanceof SchedulerException) { + throw (SchedulerException) ex; + } + if (ex instanceof Exception) { + throw new SchedulerException("Registration of jobs and triggers failed: " + ex.getMessage(), ex); + } + throw new SchedulerException("Registration of jobs and triggers failed: " + ex.getMessage()); + } + + if (transactionStatus != null) { + this.transactionManager.commit(transactionStatus); + } + } + + /** + * Add the given job to the Scheduler, if it doesn't already exist. + * Overwrites the job in any case if "overwriteExistingJobs" is set. + * @param jobDetail the job to add + * @return {@code true} if the job was actually added, + * {@code false} if it already existed before + * @see #setOverwriteExistingJobs + */ + private boolean addJobToScheduler(JobDetail jobDetail) throws SchedulerException { + if (this.overwriteExistingJobs || getScheduler().getJobDetail(jobDetail.getKey()) == null) { + getScheduler().addJob(jobDetail, true); + return true; + } + else { + return false; + } + } + + /** + * Add the given trigger to the Scheduler, if it doesn't already exist. + * Overwrites the trigger in any case if "overwriteExistingJobs" is set. + * @param trigger the trigger to add + * @return {@code true} if the trigger was actually added, + * {@code false} if it already existed before + * @see #setOverwriteExistingJobs + */ + private boolean addTriggerToScheduler(Trigger trigger) throws SchedulerException { + boolean triggerExists = (getScheduler().getTrigger(trigger.getKey()) != null); + if (triggerExists && !this.overwriteExistingJobs) { + return false; + } + + // Check if the Trigger is aware of an associated JobDetail. + JobDetail jobDetail = (JobDetail) trigger.getJobDataMap().remove("jobDetail"); + if (triggerExists) { + if (jobDetail != null && this.jobDetails != null && + !this.jobDetails.contains(jobDetail) && addJobToScheduler(jobDetail)) { + this.jobDetails.add(jobDetail); + } + try { + getScheduler().rescheduleJob(trigger.getKey(), trigger); + } + catch (ObjectAlreadyExistsException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Unexpectedly encountered existing trigger on rescheduling, assumably due to " + + "cluster race condition: " + ex.getMessage() + " - can safely be ignored"); + } + } + } + else { + try { + if (jobDetail != null && this.jobDetails != null && !this.jobDetails.contains(jobDetail) && + (this.overwriteExistingJobs || getScheduler().getJobDetail(jobDetail.getKey()) == null)) { + getScheduler().scheduleJob(jobDetail, trigger); + this.jobDetails.add(jobDetail); + } + else { + getScheduler().scheduleJob(trigger); + } + } + catch (ObjectAlreadyExistsException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Unexpectedly encountered existing trigger on job scheduling, assumably due to " + + "cluster race condition: " + ex.getMessage() + " - can safely be ignored"); + } + if (this.overwriteExistingJobs) { + getScheduler().rescheduleJob(trigger.getKey(), trigger); + } + } + } + return true; + } + + /** + * Register all specified listeners with the Scheduler. + */ + protected void registerListeners() throws SchedulerException { + ListenerManager listenerManager = getScheduler().getListenerManager(); + if (this.schedulerListeners != null) { + for (SchedulerListener listener : this.schedulerListeners) { + listenerManager.addSchedulerListener(listener); + } + } + if (this.globalJobListeners != null) { + for (JobListener listener : this.globalJobListeners) { + listenerManager.addJobListener(listener); + } + } + if (this.globalTriggerListeners != null) { + for (TriggerListener listener : this.globalTriggerListeners) { + listenerManager.addTriggerListener(listener); + } + } + } + + + /** + * Template method that determines the Scheduler to operate on. + * To be implemented by subclasses. + */ + protected abstract Scheduler getScheduler(); + +} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessorBean.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessorBean.java new file mode 100644 index 0000000..42a80e2 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessorBean.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.quartz; + +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.impl.SchedulerRepository; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Spring bean-style class for accessing a Quartz Scheduler, i.e. for registering jobs, + * triggers and listeners on a given {@link org.quartz.Scheduler} instance. + * + *

    Compatible with Quartz 2.1.4 and higher, as of Spring 4.1. + * + * @author Juergen Hoeller + * @since 2.5.6 + * @see #setScheduler + * @see #setSchedulerName + */ +public class SchedulerAccessorBean extends SchedulerAccessor implements BeanFactoryAware, InitializingBean { + + @Nullable + private String schedulerName; + + @Nullable + private Scheduler scheduler; + + @Nullable + private BeanFactory beanFactory; + + + /** + * Specify the Quartz {@link Scheduler} to operate on via its scheduler name in the Spring + * application context or also in the Quartz {@link org.quartz.impl.SchedulerRepository}. + *

    Schedulers can be registered in the repository through custom bootstrapping, + * e.g. via the {@link org.quartz.impl.StdSchedulerFactory} or + * {@link org.quartz.impl.DirectSchedulerFactory} factory classes. + * However, in general, it's preferable to use Spring's {@link SchedulerFactoryBean} + * which includes the job/trigger/listener capabilities of this accessor as well. + *

    If not specified, this accessor will try to retrieve a default {@link Scheduler} + * bean from the containing application context. + */ + public void setSchedulerName(String schedulerName) { + this.schedulerName = schedulerName; + } + + /** + * Specify the Quartz {@link Scheduler} instance to operate on. + *

    If not specified, this accessor will try to retrieve a default {@link Scheduler} + * bean from the containing application context. + */ + public void setScheduler(Scheduler scheduler) { + this.scheduler = scheduler; + } + + /** + * Return the Quartz Scheduler instance that this accessor operates on. + */ + @Override + public Scheduler getScheduler() { + Assert.state(this.scheduler != null, "No Scheduler set"); + return this.scheduler; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + + @Override + public void afterPropertiesSet() throws SchedulerException { + if (this.scheduler == null) { + this.scheduler = (this.schedulerName != null ? findScheduler(this.schedulerName) : findDefaultScheduler()); + } + registerListeners(); + registerJobsAndTriggers(); + } + + protected Scheduler findScheduler(String schedulerName) throws SchedulerException { + if (this.beanFactory instanceof ListableBeanFactory) { + ListableBeanFactory lbf = (ListableBeanFactory) this.beanFactory; + String[] beanNames = lbf.getBeanNamesForType(Scheduler.class); + for (String beanName : beanNames) { + Scheduler schedulerBean = (Scheduler) lbf.getBean(beanName); + if (schedulerName.equals(schedulerBean.getSchedulerName())) { + return schedulerBean; + } + } + } + Scheduler schedulerInRepo = SchedulerRepository.getInstance().lookup(schedulerName); + if (schedulerInRepo == null) { + throw new IllegalStateException("No Scheduler named '" + schedulerName + "' found"); + } + return schedulerInRepo; + } + + protected Scheduler findDefaultScheduler() { + if (this.beanFactory != null) { + return this.beanFactory.getBean(Scheduler.class); + } + else { + throw new IllegalStateException( + "No Scheduler specified, and cannot find a default Scheduler without a BeanFactory"); + } + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerContextAware.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerContextAware.java new file mode 100644 index 0000000..ae32172 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerContextAware.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2011 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.quartz; + +import org.quartz.SchedulerContext; + +import org.springframework.beans.factory.Aware; + +/** + * Callback interface to be implemented by Spring-managed + * Quartz artifacts that need access to the SchedulerContext + * (without having natural access to it). + * + *

    Currently only supported for custom JobFactory implementations + * that are passed in via Spring's SchedulerFactoryBean. + * + * @author Juergen Hoeller + * @author Chris Beams + * @since 2.0 + * @see org.quartz.spi.JobFactory + * @see SchedulerFactoryBean#setJobFactory + */ +public interface SchedulerContextAware extends Aware { + + /** + * Set the SchedulerContext of the current Quartz Scheduler. + * @see org.quartz.Scheduler#getContext() + */ + void setSchedulerContext(SchedulerContext schedulerContext); + +} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java new file mode 100644 index 0000000..50ef456 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java @@ -0,0 +1,850 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.quartz; + +import java.io.IOException; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +import javax.sql.DataSource; + +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.SchedulerFactory; +import org.quartz.impl.RemoteScheduler; +import org.quartz.impl.SchedulerRepository; +import org.quartz.impl.StdSchedulerFactory; +import org.quartz.simpl.SimpleThreadPool; +import org.quartz.spi.JobFactory; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.SmartLifecycle; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.lang.Nullable; +import org.springframework.scheduling.SchedulingException; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * {@link FactoryBean} that creates and configures a Quartz {@link org.quartz.Scheduler}, + * manages its lifecycle as part of the Spring application context, and exposes the + * Scheduler as bean reference for dependency injection. + * + *

    Allows registration of JobDetails, Calendars and Triggers, automatically + * starting the scheduler on initialization and shutting it down on destruction. + * In scenarios that just require static registration of jobs at startup, there + * is no need to access the Scheduler instance itself in application code. + * + *

    For dynamic registration of jobs at runtime, use a bean reference to + * this SchedulerFactoryBean to get direct access to the Quartz Scheduler + * ({@code org.quartz.Scheduler}). This allows you to create new jobs + * and triggers, and also to control and monitor the entire Scheduler. + * + *

    Note that Quartz instantiates a new Job for each execution, in + * contrast to Timer which uses a TimerTask instance that is shared + * between repeated executions. Just JobDetail descriptors are shared. + * + *

    When using persistent jobs, it is strongly recommended to perform all + * operations on the Scheduler within Spring-managed (or plain JTA) transactions. + * Else, database locking will not properly work and might even break. + * (See {@link #setDataSource setDataSource} javadoc for details.) + * + *

    The preferred way to achieve transactional execution is to demarcate + * declarative transactions at the business facade level, which will + * automatically apply to Scheduler operations performed within those scopes. + * Alternatively, you may add transactional advice for the Scheduler itself. + * + *

    Compatible with Quartz 2.1.4 and higher, as of Spring 4.1. + * + * @author Juergen Hoeller + * @since 18.02.2004 + * @see #setDataSource + * @see org.quartz.Scheduler + * @see org.quartz.SchedulerFactory + * @see org.quartz.impl.StdSchedulerFactory + * @see org.springframework.transaction.interceptor.TransactionProxyFactoryBean + */ +public class SchedulerFactoryBean extends SchedulerAccessor implements FactoryBean, + BeanNameAware, ApplicationContextAware, InitializingBean, DisposableBean, SmartLifecycle { + + /** + * The thread count property. + */ + public static final String PROP_THREAD_COUNT = "org.quartz.threadPool.threadCount"; + + /** + * The default thread count. + */ + public static final int DEFAULT_THREAD_COUNT = 10; + + + private static final ThreadLocal configTimeResourceLoaderHolder = new ThreadLocal<>(); + + private static final ThreadLocal configTimeTaskExecutorHolder = new ThreadLocal<>(); + + private static final ThreadLocal configTimeDataSourceHolder = new ThreadLocal<>(); + + private static final ThreadLocal configTimeNonTransactionalDataSourceHolder = new ThreadLocal<>(); + + + /** + * Return the {@link ResourceLoader} for the currently configured Quartz Scheduler, + * to be used by {@link ResourceLoaderClassLoadHelper}. + *

    This instance will be set before initialization of the corresponding Scheduler, + * and reset immediately afterwards. It is thus only available during configuration. + * @see #setApplicationContext + * @see ResourceLoaderClassLoadHelper + */ + @Nullable + public static ResourceLoader getConfigTimeResourceLoader() { + return configTimeResourceLoaderHolder.get(); + } + + /** + * Return the {@link Executor} for the currently configured Quartz Scheduler, + * to be used by {@link LocalTaskExecutorThreadPool}. + *

    This instance will be set before initialization of the corresponding Scheduler, + * and reset immediately afterwards. It is thus only available during configuration. + * @since 2.0 + * @see #setTaskExecutor + * @see LocalTaskExecutorThreadPool + */ + @Nullable + public static Executor getConfigTimeTaskExecutor() { + return configTimeTaskExecutorHolder.get(); + } + + /** + * Return the {@link DataSource} for the currently configured Quartz Scheduler, + * to be used by {@link LocalDataSourceJobStore}. + *

    This instance will be set before initialization of the corresponding Scheduler, + * and reset immediately afterwards. It is thus only available during configuration. + * @since 1.1 + * @see #setDataSource + * @see LocalDataSourceJobStore + */ + @Nullable + public static DataSource getConfigTimeDataSource() { + return configTimeDataSourceHolder.get(); + } + + /** + * Return the non-transactional {@link DataSource} for the currently configured + * Quartz Scheduler, to be used by {@link LocalDataSourceJobStore}. + *

    This instance will be set before initialization of the corresponding Scheduler, + * and reset immediately afterwards. It is thus only available during configuration. + * @since 1.1 + * @see #setNonTransactionalDataSource + * @see LocalDataSourceJobStore + */ + @Nullable + public static DataSource getConfigTimeNonTransactionalDataSource() { + return configTimeNonTransactionalDataSourceHolder.get(); + } + + + @Nullable + private SchedulerFactory schedulerFactory; + + private Class schedulerFactoryClass = StdSchedulerFactory.class; + + @Nullable + private String schedulerName; + + @Nullable + private Resource configLocation; + + @Nullable + private Properties quartzProperties; + + @Nullable + private Executor taskExecutor; + + @Nullable + private DataSource dataSource; + + @Nullable + private DataSource nonTransactionalDataSource; + + @Nullable + private Map schedulerContextMap; + + @Nullable + private String applicationContextSchedulerContextKey; + + @Nullable + private JobFactory jobFactory; + + private boolean jobFactorySet = false; + + private boolean autoStartup = true; + + private int startupDelay = 0; + + private int phase = DEFAULT_PHASE; + + private boolean exposeSchedulerInRepository = false; + + private boolean waitForJobsToCompleteOnShutdown = false; + + @Nullable + private String beanName; + + @Nullable + private ApplicationContext applicationContext; + + @Nullable + private Scheduler scheduler; + + + /** + * Set an external Quartz {@link SchedulerFactory} instance to use. + *

    Default is an internal {@link StdSchedulerFactory} instance. If this method is + * called, it overrides any class specified through {@link #setSchedulerFactoryClass} + * as well as any settings specified through {@link #setConfigLocation}, + * {@link #setQuartzProperties}, {@link #setTaskExecutor} or {@link #setDataSource}. + *

    NOTE: With an externally provided {@code SchedulerFactory} instance, + * local settings such as {@link #setConfigLocation} or {@link #setQuartzProperties} + * will be ignored here in {@code SchedulerFactoryBean}, expecting the external + * {@code SchedulerFactory} instance to get initialized on its own. + * @since 4.3.15 + * @see #setSchedulerFactoryClass + */ + public void setSchedulerFactory(SchedulerFactory schedulerFactory) { + this.schedulerFactory = schedulerFactory; + } + + /** + * Set the Quartz {@link SchedulerFactory} implementation to use. + *

    Default is the {@link StdSchedulerFactory} class, reading in the standard + * {@code quartz.properties} from {@code quartz.jar}. For applying custom Quartz + * properties, specify {@link #setConfigLocation "configLocation"} and/or + * {@link #setQuartzProperties "quartzProperties"} etc on this local + * {@code SchedulerFactoryBean} instance. + * @see org.quartz.impl.StdSchedulerFactory + * @see #setConfigLocation + * @see #setQuartzProperties + * @see #setTaskExecutor + * @see #setDataSource + */ + public void setSchedulerFactoryClass(Class schedulerFactoryClass) { + this.schedulerFactoryClass = schedulerFactoryClass; + } + + /** + * Set the name of the Scheduler to create via the SchedulerFactory, as an + * alternative to the {@code org.quartz.scheduler.instanceName} property. + *

    If not specified, the name will be taken from Quartz properties + * ({@code org.quartz.scheduler.instanceName}), or from the declared + * {@code SchedulerFactoryBean} bean name as a fallback. + * @see #setBeanName + * @see StdSchedulerFactory#PROP_SCHED_INSTANCE_NAME + * @see org.quartz.SchedulerFactory#getScheduler() + * @see org.quartz.SchedulerFactory#getScheduler(String) + */ + public void setSchedulerName(String schedulerName) { + this.schedulerName = schedulerName; + } + + /** + * Set the location of the Quartz properties config file, for example + * as classpath resource "classpath:quartz.properties". + *

    Note: Can be omitted when all necessary properties are specified + * locally via this bean, or when relying on Quartz' default configuration. + * @see #setQuartzProperties + */ + public void setConfigLocation(Resource configLocation) { + this.configLocation = configLocation; + } + + /** + * Set Quartz properties, like "org.quartz.threadPool.class". + *

    Can be used to override values in a Quartz properties config file, + * or to specify all necessary properties locally. + * @see #setConfigLocation + */ + public void setQuartzProperties(Properties quartzProperties) { + this.quartzProperties = quartzProperties; + } + + /** + * Set a Spring-managed {@link Executor} to use as Quartz backend. + * Exposed as thread pool through the Quartz SPI. + *

    Can be used to assign a local JDK ThreadPoolExecutor or a CommonJ + * WorkManager as Quartz backend, to avoid Quartz's manual thread creation. + *

    By default, a Quartz SimpleThreadPool will be used, configured through + * the corresponding Quartz properties. + * @since 2.0 + * @see #setQuartzProperties + * @see LocalTaskExecutorThreadPool + * @see org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor + * @see org.springframework.scheduling.concurrent.DefaultManagedTaskExecutor + */ + public void setTaskExecutor(Executor taskExecutor) { + this.taskExecutor = taskExecutor; + } + + /** + * Set the default {@link DataSource} to be used by the Scheduler. + * If set, this will override corresponding settings in Quartz properties. + *

    Note: If this is set, the Quartz settings should not define + * a job store "dataSource" to avoid meaningless double configuration. + *

    A Spring-specific subclass of Quartz' JobStoreCMT will be used. + * It is therefore strongly recommended to perform all operations on + * the Scheduler within Spring-managed (or plain JTA) transactions. + * Else, database locking will not properly work and might even break + * (e.g. if trying to obtain a lock on Oracle without a transaction). + *

    Supports both transactional and non-transactional DataSource access. + * With a non-XA DataSource and local Spring transactions, a single DataSource + * argument is sufficient. In case of an XA DataSource and global JTA transactions, + * SchedulerFactoryBean's "nonTransactionalDataSource" property should be set, + * passing in a non-XA DataSource that will not participate in global transactions. + * @since 1.1 + * @see #setNonTransactionalDataSource + * @see #setQuartzProperties + * @see #setTransactionManager + * @see LocalDataSourceJobStore + */ + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + } + + /** + * Set the {@link DataSource} to be used for non-transactional access. + *

    This is only necessary if the default DataSource is an XA DataSource that will + * always participate in transactions: A non-XA version of that DataSource should + * be specified as "nonTransactionalDataSource" in such a scenario. + *

    This is not relevant with a local DataSource instance and Spring transactions. + * Specifying a single default DataSource as "dataSource" is sufficient there. + * @since 1.1 + * @see #setDataSource + * @see LocalDataSourceJobStore + */ + public void setNonTransactionalDataSource(DataSource nonTransactionalDataSource) { + this.nonTransactionalDataSource = nonTransactionalDataSource; + } + + /** + * Register objects in the Scheduler context via a given Map. + * These objects will be available to any Job that runs in this Scheduler. + *

    Note: When using persistent Jobs whose JobDetail will be kept in the + * database, do not put Spring-managed beans or an ApplicationContext + * reference into the JobDataMap but rather into the SchedulerContext. + * @param schedulerContextAsMap a Map with String keys and any objects as + * values (for example Spring-managed beans) + * @see JobDetailFactoryBean#setJobDataAsMap + */ + public void setSchedulerContextAsMap(Map schedulerContextAsMap) { + this.schedulerContextMap = schedulerContextAsMap; + } + + /** + * Set the key of an {@link ApplicationContext} reference to expose in the + * SchedulerContext, for example "applicationContext". Default is none. + * Only applicable when running in a Spring ApplicationContext. + *

    Note: When using persistent Jobs whose JobDetail will be kept in the + * database, do not put an ApplicationContext reference into the JobDataMap + * but rather into the SchedulerContext. + *

    In case of a QuartzJobBean, the reference will be applied to the Job + * instance as bean property. An "applicationContext" attribute will + * correspond to a "setApplicationContext" method in that scenario. + *

    Note that BeanFactory callback interfaces like ApplicationContextAware + * are not automatically applied to Quartz Job instances, because Quartz + * itself is responsible for the lifecycle of its Jobs. + * @see JobDetailFactoryBean#setApplicationContextJobDataKey + * @see org.springframework.context.ApplicationContext + */ + public void setApplicationContextSchedulerContextKey(String applicationContextSchedulerContextKey) { + this.applicationContextSchedulerContextKey = applicationContextSchedulerContextKey; + } + + /** + * Set the Quartz {@link JobFactory} to use for this Scheduler. + *

    Default is Spring's {@link AdaptableJobFactory}, which supports + * {@link java.lang.Runnable} objects as well as standard Quartz + * {@link org.quartz.Job} instances. Note that this default only applies + * to a local Scheduler, not to a RemoteScheduler (where setting + * a custom JobFactory is not supported by Quartz). + *

    Specify an instance of Spring's {@link SpringBeanJobFactory} here + * (typically as an inner bean definition) to automatically populate a job's + * bean properties from the specified job data map and scheduler context. + * @since 2.0 + * @see AdaptableJobFactory + * @see SpringBeanJobFactory + */ + public void setJobFactory(JobFactory jobFactory) { + this.jobFactory = jobFactory; + this.jobFactorySet = true; + } + + /** + * Set whether to automatically start the scheduler after initialization. + *

    Default is "true"; set this to "false" to allow for manual startup. + */ + public void setAutoStartup(boolean autoStartup) { + this.autoStartup = autoStartup; + } + + /** + * Return whether this scheduler is configured for auto-startup. If "true", + * the scheduler will start after the context is refreshed and after the + * start delay, if any. + */ + @Override + public boolean isAutoStartup() { + return this.autoStartup; + } + + /** + * Specify the phase in which this scheduler should be started and stopped. + * The startup order proceeds from lowest to highest, and the shutdown order + * is the reverse of that. By default this value is {@code Integer.MAX_VALUE} + * meaning that this scheduler starts as late as possible and stops as soon + * as possible. + * @since 3.0 + */ + public void setPhase(int phase) { + this.phase = phase; + } + + /** + * Return the phase in which this scheduler will be started and stopped. + */ + @Override + public int getPhase() { + return this.phase; + } + + /** + * Set the number of seconds to wait after initialization before + * starting the scheduler asynchronously. Default is 0, meaning + * immediate synchronous startup on initialization of this bean. + *

    Setting this to 10 or 20 seconds makes sense if no jobs + * should be run before the entire application has started up. + */ + public void setStartupDelay(int startupDelay) { + this.startupDelay = startupDelay; + } + + /** + * Set whether to expose the Spring-managed {@link Scheduler} instance in the + * Quartz {@link SchedulerRepository}. Default is "false", since the Spring-managed + * Scheduler is usually exclusively intended for access within the Spring context. + *

    Switch this flag to "true" in order to expose the Scheduler globally. + * This is not recommended unless you have an existing Spring application that + * relies on this behavior. Note that such global exposure was the accidental + * default in earlier Spring versions; this has been fixed as of Spring 2.5.6. + */ + public void setExposeSchedulerInRepository(boolean exposeSchedulerInRepository) { + this.exposeSchedulerInRepository = exposeSchedulerInRepository; + } + + /** + * Set whether to wait for running jobs to complete on shutdown. + *

    Default is "false". Switch this to "true" if you prefer + * fully completed jobs at the expense of a longer shutdown phase. + * @see org.quartz.Scheduler#shutdown(boolean) + */ + public void setWaitForJobsToCompleteOnShutdown(boolean waitForJobsToCompleteOnShutdown) { + this.waitForJobsToCompleteOnShutdown = waitForJobsToCompleteOnShutdown; + } + + @Override + public void setBeanName(String name) { + this.beanName = name; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + + //--------------------------------------------------------------------- + // Implementation of InitializingBean interface + //--------------------------------------------------------------------- + + @Override + public void afterPropertiesSet() throws Exception { + if (this.dataSource == null && this.nonTransactionalDataSource != null) { + this.dataSource = this.nonTransactionalDataSource; + } + + if (this.applicationContext != null && this.resourceLoader == null) { + this.resourceLoader = this.applicationContext; + } + + // Initialize the Scheduler instance... + this.scheduler = prepareScheduler(prepareSchedulerFactory()); + try { + registerListeners(); + registerJobsAndTriggers(); + } + catch (Exception ex) { + try { + this.scheduler.shutdown(true); + } + catch (Exception ex2) { + logger.debug("Scheduler shutdown exception after registration failure", ex2); + } + throw ex; + } + } + + + /** + * Create a SchedulerFactory if necessary and apply locally defined Quartz properties to it. + * @return the initialized SchedulerFactory + */ + private SchedulerFactory prepareSchedulerFactory() throws SchedulerException, IOException { + SchedulerFactory schedulerFactory = this.schedulerFactory; + if (schedulerFactory == null) { + // Create local SchedulerFactory instance (typically a StdSchedulerFactory) + schedulerFactory = BeanUtils.instantiateClass(this.schedulerFactoryClass); + if (schedulerFactory instanceof StdSchedulerFactory) { + initSchedulerFactory((StdSchedulerFactory) schedulerFactory); + } + else if (this.configLocation != null || this.quartzProperties != null || + this.taskExecutor != null || this.dataSource != null) { + throw new IllegalArgumentException( + "StdSchedulerFactory required for applying Quartz properties: " + schedulerFactory); + } + // Otherwise, no local settings to be applied via StdSchedulerFactory.initialize(Properties) + } + // Otherwise, assume that externally provided factory has been initialized with appropriate settings + return schedulerFactory; + } + + /** + * Initialize the given SchedulerFactory, applying locally defined Quartz properties to it. + * @param schedulerFactory the SchedulerFactory to initialize + */ + private void initSchedulerFactory(StdSchedulerFactory schedulerFactory) throws SchedulerException, IOException { + Properties mergedProps = new Properties(); + if (this.resourceLoader != null) { + mergedProps.setProperty(StdSchedulerFactory.PROP_SCHED_CLASS_LOAD_HELPER_CLASS, + ResourceLoaderClassLoadHelper.class.getName()); + } + + if (this.taskExecutor != null) { + mergedProps.setProperty(StdSchedulerFactory.PROP_THREAD_POOL_CLASS, + LocalTaskExecutorThreadPool.class.getName()); + } + else { + // Set necessary default properties here, as Quartz will not apply + // its default configuration when explicitly given properties. + mergedProps.setProperty(StdSchedulerFactory.PROP_THREAD_POOL_CLASS, SimpleThreadPool.class.getName()); + mergedProps.setProperty(PROP_THREAD_COUNT, Integer.toString(DEFAULT_THREAD_COUNT)); + } + + if (this.configLocation != null) { + if (logger.isDebugEnabled()) { + logger.debug("Loading Quartz config from [" + this.configLocation + "]"); + } + PropertiesLoaderUtils.fillProperties(mergedProps, this.configLocation); + } + + CollectionUtils.mergePropertiesIntoMap(this.quartzProperties, mergedProps); + if (this.dataSource != null) { + mergedProps.setProperty(StdSchedulerFactory.PROP_JOB_STORE_CLASS, LocalDataSourceJobStore.class.getName()); + } + + // Determine scheduler name across local settings and Quartz properties... + if (this.schedulerName != null) { + mergedProps.setProperty(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME, this.schedulerName); + } + else { + String nameProp = mergedProps.getProperty(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME); + if (nameProp != null) { + this.schedulerName = nameProp; + } + else if (this.beanName != null) { + mergedProps.setProperty(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME, this.beanName); + this.schedulerName = this.beanName; + } + } + + schedulerFactory.initialize(mergedProps); + } + + private Scheduler prepareScheduler(SchedulerFactory schedulerFactory) throws SchedulerException { + if (this.resourceLoader != null) { + // Make given ResourceLoader available for SchedulerFactory configuration. + configTimeResourceLoaderHolder.set(this.resourceLoader); + } + if (this.taskExecutor != null) { + // Make given TaskExecutor available for SchedulerFactory configuration. + configTimeTaskExecutorHolder.set(this.taskExecutor); + } + if (this.dataSource != null) { + // Make given DataSource available for SchedulerFactory configuration. + configTimeDataSourceHolder.set(this.dataSource); + } + if (this.nonTransactionalDataSource != null) { + // Make given non-transactional DataSource available for SchedulerFactory configuration. + configTimeNonTransactionalDataSourceHolder.set(this.nonTransactionalDataSource); + } + + // Get Scheduler instance from SchedulerFactory. + try { + Scheduler scheduler = createScheduler(schedulerFactory, this.schedulerName); + populateSchedulerContext(scheduler); + + if (!this.jobFactorySet && !(scheduler instanceof RemoteScheduler)) { + // Use AdaptableJobFactory as default for a local Scheduler, unless when + // explicitly given a null value through the "jobFactory" bean property. + this.jobFactory = new AdaptableJobFactory(); + } + if (this.jobFactory != null) { + if (this.applicationContext != null && this.jobFactory instanceof ApplicationContextAware) { + ((ApplicationContextAware) this.jobFactory).setApplicationContext(this.applicationContext); + } + if (this.jobFactory instanceof SchedulerContextAware) { + ((SchedulerContextAware) this.jobFactory).setSchedulerContext(scheduler.getContext()); + } + scheduler.setJobFactory(this.jobFactory); + } + return scheduler; + } + + finally { + if (this.resourceLoader != null) { + configTimeResourceLoaderHolder.remove(); + } + if (this.taskExecutor != null) { + configTimeTaskExecutorHolder.remove(); + } + if (this.dataSource != null) { + configTimeDataSourceHolder.remove(); + } + if (this.nonTransactionalDataSource != null) { + configTimeNonTransactionalDataSourceHolder.remove(); + } + } + } + + /** + * Create the Scheduler instance for the given factory and scheduler name. + * Called by {@link #afterPropertiesSet}. + *

    The default implementation invokes SchedulerFactory's {@code getScheduler} + * method. Can be overridden for custom Scheduler creation. + * @param schedulerFactory the factory to create the Scheduler with + * @param schedulerName the name of the scheduler to create + * @return the Scheduler instance + * @throws SchedulerException if thrown by Quartz methods + * @see #afterPropertiesSet + * @see org.quartz.SchedulerFactory#getScheduler + */ + protected Scheduler createScheduler(SchedulerFactory schedulerFactory, @Nullable String schedulerName) + throws SchedulerException { + + // Override thread context ClassLoader to work around naive Quartz ClassLoadHelper loading. + Thread currentThread = Thread.currentThread(); + ClassLoader threadContextClassLoader = currentThread.getContextClassLoader(); + boolean overrideClassLoader = (this.resourceLoader != null && + this.resourceLoader.getClassLoader() != threadContextClassLoader); + if (overrideClassLoader) { + currentThread.setContextClassLoader(this.resourceLoader.getClassLoader()); + } + try { + SchedulerRepository repository = SchedulerRepository.getInstance(); + synchronized (repository) { + Scheduler existingScheduler = (schedulerName != null ? repository.lookup(schedulerName) : null); + Scheduler newScheduler = schedulerFactory.getScheduler(); + if (newScheduler == existingScheduler) { + throw new IllegalStateException("Active Scheduler of name '" + schedulerName + "' already registered " + + "in Quartz SchedulerRepository. Cannot create a new Spring-managed Scheduler of the same name!"); + } + if (!this.exposeSchedulerInRepository) { + // Need to remove it in this case, since Quartz shares the Scheduler instance by default! + SchedulerRepository.getInstance().remove(newScheduler.getSchedulerName()); + } + return newScheduler; + } + } + finally { + if (overrideClassLoader) { + // Reset original thread context ClassLoader. + currentThread.setContextClassLoader(threadContextClassLoader); + } + } + } + + /** + * Expose the specified context attributes and/or the current + * ApplicationContext in the Quartz SchedulerContext. + */ + private void populateSchedulerContext(Scheduler scheduler) throws SchedulerException { + // Put specified objects into Scheduler context. + if (this.schedulerContextMap != null) { + scheduler.getContext().putAll(this.schedulerContextMap); + } + + // Register ApplicationContext in Scheduler context. + if (this.applicationContextSchedulerContextKey != null) { + if (this.applicationContext == null) { + throw new IllegalStateException( + "SchedulerFactoryBean needs to be set up in an ApplicationContext " + + "to be able to handle an 'applicationContextSchedulerContextKey'"); + } + scheduler.getContext().put(this.applicationContextSchedulerContextKey, this.applicationContext); + } + } + + + /** + * Start the Quartz Scheduler, respecting the "startupDelay" setting. + * @param scheduler the Scheduler to start + * @param startupDelay the number of seconds to wait before starting + * the Scheduler asynchronously + */ + protected void startScheduler(final Scheduler scheduler, final int startupDelay) throws SchedulerException { + if (startupDelay <= 0) { + logger.info("Starting Quartz Scheduler now"); + scheduler.start(); + } + else { + if (logger.isInfoEnabled()) { + logger.info("Will start Quartz Scheduler [" + scheduler.getSchedulerName() + + "] in " + startupDelay + " seconds"); + } + // Not using the Quartz startDelayed method since we explicitly want a daemon + // thread here, not keeping the JVM alive in case of all other threads ending. + Thread schedulerThread = new Thread() { + @Override + public void run() { + try { + TimeUnit.SECONDS.sleep(startupDelay); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + // simply proceed + } + if (logger.isInfoEnabled()) { + logger.info("Starting Quartz Scheduler now, after delay of " + startupDelay + " seconds"); + } + try { + scheduler.start(); + } + catch (SchedulerException ex) { + throw new SchedulingException("Could not start Quartz Scheduler after delay", ex); + } + } + }; + schedulerThread.setName("Quartz Scheduler [" + scheduler.getSchedulerName() + "]"); + schedulerThread.setDaemon(true); + schedulerThread.start(); + } + } + + + //--------------------------------------------------------------------- + // Implementation of FactoryBean interface + //--------------------------------------------------------------------- + + @Override + public Scheduler getScheduler() { + Assert.state(this.scheduler != null, "No Scheduler set"); + return this.scheduler; + } + + @Override + @Nullable + public Scheduler getObject() { + return this.scheduler; + } + + @Override + public Class getObjectType() { + return (this.scheduler != null ? this.scheduler.getClass() : Scheduler.class); + } + + @Override + public boolean isSingleton() { + return true; + } + + + //--------------------------------------------------------------------- + // Implementation of SmartLifecycle interface + //--------------------------------------------------------------------- + + @Override + public void start() throws SchedulingException { + if (this.scheduler != null) { + try { + startScheduler(this.scheduler, this.startupDelay); + } + catch (SchedulerException ex) { + throw new SchedulingException("Could not start Quartz Scheduler", ex); + } + } + } + + @Override + public void stop() throws SchedulingException { + if (this.scheduler != null) { + try { + this.scheduler.standby(); + } + catch (SchedulerException ex) { + throw new SchedulingException("Could not stop Quartz Scheduler", ex); + } + } + } + + @Override + public boolean isRunning() throws SchedulingException { + if (this.scheduler != null) { + try { + return !this.scheduler.isInStandbyMode(); + } + catch (SchedulerException ex) { + return false; + } + } + return false; + } + + + //--------------------------------------------------------------------- + // Implementation of DisposableBean interface + //--------------------------------------------------------------------- + + /** + * Shut down the Quartz scheduler on bean factory shutdown, + * stopping all scheduled jobs. + */ + @Override + public void destroy() throws SchedulerException { + if (this.scheduler != null) { + logger.info("Shutting down Quartz Scheduler"); + this.scheduler.shutdown(this.waitForJobsToCompleteOnShutdown); + } + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleThreadPoolTaskExecutor.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleThreadPoolTaskExecutor.java new file mode 100644 index 0000000..3553496 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleThreadPoolTaskExecutor.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.quartz; + +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; + +import org.quartz.SchedulerConfigException; +import org.quartz.simpl.SimpleThreadPool; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.task.AsyncListenableTaskExecutor; +import org.springframework.scheduling.SchedulingException; +import org.springframework.scheduling.SchedulingTaskExecutor; +import org.springframework.util.Assert; +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.util.concurrent.ListenableFutureTask; + +/** + * Subclass of Quartz's SimpleThreadPool that implements Spring's + * {@link org.springframework.core.task.TaskExecutor} interface + * and listens to Spring lifecycle callbacks. + * + *

    Can be shared between a Quartz Scheduler (specified as "taskExecutor") + * and other TaskExecutor users, or even used completely independent of + * a Quartz Scheduler (as plain TaskExecutor backend). + * + * @author Juergen Hoeller + * @since 2.0 + * @see org.quartz.simpl.SimpleThreadPool + * @see org.springframework.core.task.TaskExecutor + * @see SchedulerFactoryBean#setTaskExecutor + */ +public class SimpleThreadPoolTaskExecutor extends SimpleThreadPool + implements AsyncListenableTaskExecutor, SchedulingTaskExecutor, InitializingBean, DisposableBean { + + private boolean waitForJobsToCompleteOnShutdown = false; + + + /** + * Set whether to wait for running jobs to complete on shutdown. + * Default is "false". + * @see org.quartz.simpl.SimpleThreadPool#shutdown(boolean) + */ + public void setWaitForJobsToCompleteOnShutdown(boolean waitForJobsToCompleteOnShutdown) { + this.waitForJobsToCompleteOnShutdown = waitForJobsToCompleteOnShutdown; + } + + @Override + public void afterPropertiesSet() throws SchedulerConfigException { + initialize(); + } + + + @Override + public void execute(Runnable task) { + Assert.notNull(task, "Runnable must not be null"); + if (!runInThread(task)) { + throw new SchedulingException("Quartz SimpleThreadPool already shut down"); + } + } + + @Override + public void execute(Runnable task, long startTimeout) { + execute(task); + } + + @Override + public Future submit(Runnable task) { + FutureTask future = new FutureTask<>(task, null); + execute(future); + return future; + } + + @Override + public Future submit(Callable task) { + FutureTask future = new FutureTask<>(task); + execute(future); + return future; + } + + @Override + public ListenableFuture submitListenable(Runnable task) { + ListenableFutureTask future = new ListenableFutureTask<>(task, null); + execute(future); + return future; + } + + @Override + public ListenableFuture submitListenable(Callable task) { + ListenableFutureTask future = new ListenableFutureTask<>(task); + execute(future); + return future; + } + + + @Override + public void destroy() { + shutdown(this.waitForJobsToCompleteOnShutdown); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleTriggerFactoryBean.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleTriggerFactoryBean.java new file mode 100644 index 0000000..3cd25be --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleTriggerFactoryBean.java @@ -0,0 +1,269 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.quartz; + +import java.util.Date; +import java.util.Map; + +import org.quartz.JobDataMap; +import org.quartz.JobDetail; +import org.quartz.Scheduler; +import org.quartz.SimpleTrigger; +import org.quartz.impl.triggers.SimpleTriggerImpl; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.Constants; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * A Spring {@link FactoryBean} for creating a Quartz {@link org.quartz.SimpleTrigger} + * instance, supporting bean-style usage for trigger configuration. + * + *

    {@code SimpleTrigger(Impl)} itself is already a JavaBean but lacks sensible defaults. + * This class uses the Spring bean name as job name, the Quartz default group ("DEFAULT") + * as job group, the current time as start time, and indefinite repetition, if not specified. + * + *

    This class will also register the trigger with the job name and group of + * a given {@link org.quartz.JobDetail}. This allows {@link SchedulerFactoryBean} + * to automatically register a trigger for the corresponding JobDetail, + * instead of registering the JobDetail separately. + * + * @author Juergen Hoeller + * @since 3.1 + * @see #setName + * @see #setGroup + * @see #setStartDelay + * @see #setJobDetail + * @see SchedulerFactoryBean#setTriggers + * @see SchedulerFactoryBean#setJobDetails + */ +public class SimpleTriggerFactoryBean implements FactoryBean, BeanNameAware, InitializingBean { + + /** Constants for the SimpleTrigger class. */ + private static final Constants constants = new Constants(SimpleTrigger.class); + + + @Nullable + private String name; + + @Nullable + private String group; + + @Nullable + private JobDetail jobDetail; + + private JobDataMap jobDataMap = new JobDataMap(); + + @Nullable + private Date startTime; + + private long startDelay; + + private long repeatInterval; + + private int repeatCount = -1; + + private int priority; + + private int misfireInstruction; + + @Nullable + private String description; + + @Nullable + private String beanName; + + @Nullable + private SimpleTrigger simpleTrigger; + + + /** + * Specify the trigger's name. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Specify the trigger's group. + */ + public void setGroup(String group) { + this.group = group; + } + + /** + * Set the JobDetail that this trigger should be associated with. + */ + public void setJobDetail(JobDetail jobDetail) { + this.jobDetail = jobDetail; + } + + /** + * Set the trigger's JobDataMap. + * @see #setJobDataAsMap + */ + public void setJobDataMap(JobDataMap jobDataMap) { + this.jobDataMap = jobDataMap; + } + + /** + * Return the trigger's JobDataMap. + */ + public JobDataMap getJobDataMap() { + return this.jobDataMap; + } + + /** + * Register objects in the JobDataMap via a given Map. + *

    These objects will be available to this Trigger only, + * in contrast to objects in the JobDetail's data map. + * @param jobDataAsMap a Map with String keys and any objects as values + * (for example Spring-managed beans) + */ + public void setJobDataAsMap(Map jobDataAsMap) { + this.jobDataMap.putAll(jobDataAsMap); + } + + /** + * Set a specific start time for the trigger. + *

    Note that a dynamically computed {@link #setStartDelay} specification + * overrides a static timestamp set here. + */ + public void setStartTime(Date startTime) { + this.startTime = startTime; + } + + /** + * Set the start delay in milliseconds. + *

    The start delay is added to the current system time (when the bean starts) + * to control the start time of the trigger. + * @see #setStartTime + */ + public void setStartDelay(long startDelay) { + Assert.isTrue(startDelay >= 0, "Start delay cannot be negative"); + this.startDelay = startDelay; + } + + /** + * Specify the interval between execution times of this trigger. + */ + public void setRepeatInterval(long repeatInterval) { + this.repeatInterval = repeatInterval; + } + + /** + * Specify the number of times this trigger is supposed to fire. + *

    Default is to repeat indefinitely. + */ + public void setRepeatCount(int repeatCount) { + this.repeatCount = repeatCount; + } + + /** + * Specify the priority of this trigger. + */ + public void setPriority(int priority) { + this.priority = priority; + } + + /** + * Specify a misfire instruction for this trigger. + */ + public void setMisfireInstruction(int misfireInstruction) { + this.misfireInstruction = misfireInstruction; + } + + /** + * Set the misfire instruction via the name of the corresponding + * constant in the {@link org.quartz.SimpleTrigger} class. + * Default is {@code MISFIRE_INSTRUCTION_SMART_POLICY}. + * @see org.quartz.SimpleTrigger#MISFIRE_INSTRUCTION_FIRE_NOW + * @see org.quartz.SimpleTrigger#MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT + * @see org.quartz.SimpleTrigger#MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT + * @see org.quartz.SimpleTrigger#MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT + * @see org.quartz.SimpleTrigger#MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT + * @see org.quartz.Trigger#MISFIRE_INSTRUCTION_SMART_POLICY + */ + public void setMisfireInstructionName(String constantName) { + this.misfireInstruction = constants.asNumber(constantName).intValue(); + } + + /** + * Associate a textual description with this trigger. + */ + public void setDescription(String description) { + this.description = description; + } + + @Override + public void setBeanName(String beanName) { + this.beanName = beanName; + } + + + @Override + public void afterPropertiesSet() { + if (this.name == null) { + this.name = this.beanName; + } + if (this.group == null) { + this.group = Scheduler.DEFAULT_GROUP; + } + if (this.jobDetail != null) { + this.jobDataMap.put("jobDetail", this.jobDetail); + } + if (this.startDelay > 0 || this.startTime == null) { + this.startTime = new Date(System.currentTimeMillis() + this.startDelay); + } + + SimpleTriggerImpl sti = new SimpleTriggerImpl(); + sti.setName(this.name != null ? this.name : toString()); + sti.setGroup(this.group); + if (this.jobDetail != null) { + sti.setJobKey(this.jobDetail.getKey()); + } + sti.setJobDataMap(this.jobDataMap); + sti.setStartTime(this.startTime); + sti.setRepeatInterval(this.repeatInterval); + sti.setRepeatCount(this.repeatCount); + sti.setPriority(this.priority); + sti.setMisfireInstruction(this.misfireInstruction); + sti.setDescription(this.description); + this.simpleTrigger = sti; + } + + + @Override + @Nullable + public SimpleTrigger getObject() { + return this.simpleTrigger; + } + + @Override + public Class getObjectType() { + return SimpleTrigger.class; + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SpringBeanJobFactory.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SpringBeanJobFactory.java new file mode 100644 index 0000000..3c6369f --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SpringBeanJobFactory.java @@ -0,0 +1,130 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.quartz; + +import org.quartz.SchedulerContext; +import org.quartz.spi.TriggerFiredBundle; + +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.PropertyAccessorFactory; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.lang.Nullable; + +/** + * Subclass of {@link AdaptableJobFactory} that also supports Spring-style + * dependency injection on bean properties. This is essentially the direct + * equivalent of Spring's {@link QuartzJobBean} in the shape of a Quartz + * {@link org.quartz.spi.JobFactory}. + * + *

    Applies scheduler context, job data map and trigger data map entries + * as bean property values. If no matching bean property is found, the entry + * is by default simply ignored. This is analogous to QuartzJobBean's behavior. + * + *

    Compatible with Quartz 2.1.4 and higher, as of Spring 4.1. + * + * @author Juergen Hoeller + * @since 2.0 + * @see SchedulerFactoryBean#setJobFactory + * @see QuartzJobBean + */ +public class SpringBeanJobFactory extends AdaptableJobFactory + implements ApplicationContextAware, SchedulerContextAware { + + @Nullable + private String[] ignoredUnknownProperties; + + @Nullable + private ApplicationContext applicationContext; + + @Nullable + private SchedulerContext schedulerContext; + + + /** + * Specify the unknown properties (not found in the bean) that should be ignored. + *

    Default is {@code null}, indicating that all unknown properties + * should be ignored. Specify an empty array to throw an exception in case + * of any unknown properties, or a list of property names that should be + * ignored if there is no corresponding property found on the particular + * job class (all other unknown properties will still trigger an exception). + */ + public void setIgnoredUnknownProperties(String... ignoredUnknownProperties) { + this.ignoredUnknownProperties = ignoredUnknownProperties; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Override + public void setSchedulerContext(SchedulerContext schedulerContext) { + this.schedulerContext = schedulerContext; + } + + + /** + * Create the job instance, populating it with property values taken + * from the scheduler context, job data map and trigger data map. + */ + @Override + protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception { + Object job = (this.applicationContext != null ? + this.applicationContext.getAutowireCapableBeanFactory().createBean( + bundle.getJobDetail().getJobClass(), AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false) : + super.createJobInstance(bundle)); + + if (isEligibleForPropertyPopulation(job)) { + BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(job); + MutablePropertyValues pvs = new MutablePropertyValues(); + if (this.schedulerContext != null) { + pvs.addPropertyValues(this.schedulerContext); + } + pvs.addPropertyValues(bundle.getJobDetail().getJobDataMap()); + pvs.addPropertyValues(bundle.getTrigger().getJobDataMap()); + if (this.ignoredUnknownProperties != null) { + for (String propName : this.ignoredUnknownProperties) { + if (pvs.contains(propName) && !bw.isWritableProperty(propName)) { + pvs.removePropertyValue(propName); + } + } + bw.setPropertyValues(pvs); + } + else { + bw.setPropertyValues(pvs, true); + } + } + + return job; + } + + /** + * Return whether the given job object is eligible for having + * its bean properties populated. + *

    The default implementation ignores {@link QuartzJobBean} instances, + * which will inject bean properties themselves. + * @param jobObject the job object to introspect + * @see QuartzJobBean + */ + protected boolean isEligibleForPropertyPopulation(Object jobObject) { + return (!(jobObject instanceof QuartzJobBean)); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/package-info.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/package-info.java new file mode 100644 index 0000000..6ca38a3 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/package-info.java @@ -0,0 +1,13 @@ +/** + * Support classes for the open source scheduler + * Quartz, + * allowing to set up Quartz Schedulers, JobDetails and + * Triggers as beans in a Spring context. Also provides + * convenience classes for implementing Quartz Jobs. + */ +@NonNullApi +@NonNullFields +package org.springframework.scheduling.quartz; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactory.java b/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactory.java new file mode 100644 index 0000000..2b29d05 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactory.java @@ -0,0 +1,417 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ui.freemarker; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import freemarker.cache.FileTemplateLoader; +import freemarker.cache.MultiTemplateLoader; +import freemarker.cache.TemplateLoader; +import freemarker.template.Configuration; +import freemarker.template.SimpleHash; +import freemarker.template.TemplateException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; + +/** + * Factory that configures a FreeMarker Configuration. Can be used standalone, but + * typically you will either use FreeMarkerConfigurationFactoryBean for preparing a + * Configuration as bean reference, or FreeMarkerConfigurer for web views. + * + *

    The optional "configLocation" property sets the location of a FreeMarker + * properties file, within the current application. FreeMarker properties can be + * overridden via "freemarkerSettings". All of these properties will be set by + * calling FreeMarker's {@code Configuration.setSettings()} method and are + * subject to constraints set by FreeMarker. + * + *

    The "freemarkerVariables" property can be used to specify a Map of + * shared variables that will be applied to the Configuration via the + * {@code setAllSharedVariables()} method. Like {@code setSettings()}, + * these entries are subject to FreeMarker constraints. + * + *

    The simplest way to use this class is to specify a "templateLoaderPath"; + * FreeMarker does not need any further configuration then. + * + *

    Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher. + * + * @author Darren Davison + * @author Juergen Hoeller + * @since 03.03.2004 + * @see #setConfigLocation + * @see #setFreemarkerSettings + * @see #setFreemarkerVariables + * @see #setTemplateLoaderPath + * @see #createConfiguration + * @see FreeMarkerConfigurationFactoryBean + * @see org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer + * @see freemarker.template.Configuration + */ +public class FreeMarkerConfigurationFactory { + + protected final Log logger = LogFactory.getLog(getClass()); + + @Nullable + private Resource configLocation; + + @Nullable + private Properties freemarkerSettings; + + @Nullable + private Map freemarkerVariables; + + @Nullable + private String defaultEncoding; + + private final List templateLoaders = new ArrayList<>(); + + @Nullable + private List preTemplateLoaders; + + @Nullable + private List postTemplateLoaders; + + @Nullable + private String[] templateLoaderPaths; + + private ResourceLoader resourceLoader = new DefaultResourceLoader(); + + private boolean preferFileSystemAccess = true; + + + /** + * Set the location of the FreeMarker config file. + * Alternatively, you can specify all setting locally. + * @see #setFreemarkerSettings + * @see #setTemplateLoaderPath + */ + public void setConfigLocation(Resource resource) { + this.configLocation = resource; + } + + /** + * Set properties that contain well-known FreeMarker keys which will be + * passed to FreeMarker's {@code Configuration.setSettings} method. + * @see freemarker.template.Configuration#setSettings + */ + public void setFreemarkerSettings(Properties settings) { + this.freemarkerSettings = settings; + } + + /** + * Set a Map that contains well-known FreeMarker objects which will be passed + * to FreeMarker's {@code Configuration.setAllSharedVariables()} method. + * @see freemarker.template.Configuration#setAllSharedVariables + */ + public void setFreemarkerVariables(Map variables) { + this.freemarkerVariables = variables; + } + + /** + * Set the default encoding for the FreeMarker configuration. + * If not specified, FreeMarker will use the platform file encoding. + *

    Used for template rendering unless there is an explicit encoding specified + * for the rendering process (for example, on Spring's FreeMarkerView). + * @see freemarker.template.Configuration#setDefaultEncoding + * @see org.springframework.web.servlet.view.freemarker.FreeMarkerView#setEncoding + */ + public void setDefaultEncoding(String defaultEncoding) { + this.defaultEncoding = defaultEncoding; + } + + /** + * Set a List of {@code TemplateLoader}s that will be used to search + * for templates. For example, one or more custom loaders such as database + * loaders could be configured and injected here. + *

    The {@link TemplateLoader TemplateLoaders} specified here will be + * registered before the default template loaders that this factory + * registers (such as loaders for specified "templateLoaderPaths" or any + * loaders registered in {@link #postProcessTemplateLoaders}). + * @see #setTemplateLoaderPaths + * @see #postProcessTemplateLoaders + */ + public void setPreTemplateLoaders(TemplateLoader... preTemplateLoaders) { + this.preTemplateLoaders = Arrays.asList(preTemplateLoaders); + } + + /** + * Set a List of {@code TemplateLoader}s that will be used to search + * for templates. For example, one or more custom loaders such as database + * loaders can be configured. + *

    The {@link TemplateLoader TemplateLoaders} specified here will be + * registered after the default template loaders that this factory + * registers (such as loaders for specified "templateLoaderPaths" or any + * loaders registered in {@link #postProcessTemplateLoaders}). + * @see #setTemplateLoaderPaths + * @see #postProcessTemplateLoaders + */ + public void setPostTemplateLoaders(TemplateLoader... postTemplateLoaders) { + this.postTemplateLoaders = Arrays.asList(postTemplateLoaders); + } + + /** + * Set the Freemarker template loader path via a Spring resource location. + * See the "templateLoaderPaths" property for details on path handling. + * @see #setTemplateLoaderPaths + */ + public void setTemplateLoaderPath(String templateLoaderPath) { + this.templateLoaderPaths = new String[] {templateLoaderPath}; + } + + /** + * Set multiple Freemarker template loader paths via Spring resource locations. + *

    When populated via a String, standard URLs like "file:" and "classpath:" + * pseudo URLs are supported, as understood by ResourceEditor. Allows for + * relative paths when running in an ApplicationContext. + *

    Will define a path for the default FreeMarker template loader. + * If a specified resource cannot be resolved to a {@code java.io.File}, + * a generic SpringTemplateLoader will be used, without modification detection. + *

    To enforce the use of SpringTemplateLoader, i.e. to not resolve a path + * as file system resource in any case, turn off the "preferFileSystemAccess" + * flag. See the latter's javadoc for details. + *

    If you wish to specify your own list of TemplateLoaders, do not set this + * property and instead use {@code setTemplateLoaders(List templateLoaders)} + * @see org.springframework.core.io.ResourceEditor + * @see org.springframework.context.ApplicationContext#getResource + * @see freemarker.template.Configuration#setDirectoryForTemplateLoading + * @see SpringTemplateLoader + */ + public void setTemplateLoaderPaths(String... templateLoaderPaths) { + this.templateLoaderPaths = templateLoaderPaths; + } + + /** + * Set the Spring ResourceLoader to use for loading FreeMarker template files. + * The default is DefaultResourceLoader. Will get overridden by the + * ApplicationContext if running in a context. + * @see org.springframework.core.io.DefaultResourceLoader + */ + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + /** + * Return the Spring ResourceLoader to use for loading FreeMarker template files. + */ + protected ResourceLoader getResourceLoader() { + return this.resourceLoader; + } + + /** + * Set whether to prefer file system access for template loading. + * File system access enables hot detection of template changes. + *

    If this is enabled, FreeMarkerConfigurationFactory will try to resolve + * the specified "templateLoaderPath" as file system resource (which will work + * for expanded class path resources and ServletContext resources too). + *

    Default is "true". Turn this off to always load via SpringTemplateLoader + * (i.e. as stream, without hot detection of template changes), which might + * be necessary if some of your templates reside in an expanded classes + * directory while others reside in jar files. + * @see #setTemplateLoaderPath + */ + public void setPreferFileSystemAccess(boolean preferFileSystemAccess) { + this.preferFileSystemAccess = preferFileSystemAccess; + } + + /** + * Return whether to prefer file system access for template loading. + */ + protected boolean isPreferFileSystemAccess() { + return this.preferFileSystemAccess; + } + + + /** + * Prepare the FreeMarker Configuration and return it. + * @return the FreeMarker Configuration object + * @throws IOException if the config file wasn't found + * @throws TemplateException on FreeMarker initialization failure + */ + public Configuration createConfiguration() throws IOException, TemplateException { + Configuration config = newConfiguration(); + Properties props = new Properties(); + + // Load config file if specified. + if (this.configLocation != null) { + if (logger.isDebugEnabled()) { + logger.debug("Loading FreeMarker configuration from " + this.configLocation); + } + PropertiesLoaderUtils.fillProperties(props, this.configLocation); + } + + // Merge local properties if specified. + if (this.freemarkerSettings != null) { + props.putAll(this.freemarkerSettings); + } + + // FreeMarker will only accept known keys in its setSettings and + // setAllSharedVariables methods. + if (!props.isEmpty()) { + config.setSettings(props); + } + + if (!CollectionUtils.isEmpty(this.freemarkerVariables)) { + config.setAllSharedVariables(new SimpleHash(this.freemarkerVariables, config.getObjectWrapper())); + } + + if (this.defaultEncoding != null) { + config.setDefaultEncoding(this.defaultEncoding); + } + + List templateLoaders = new ArrayList<>(this.templateLoaders); + + // Register template loaders that are supposed to kick in early. + if (this.preTemplateLoaders != null) { + templateLoaders.addAll(this.preTemplateLoaders); + } + + // Register default template loaders. + if (this.templateLoaderPaths != null) { + for (String path : this.templateLoaderPaths) { + templateLoaders.add(getTemplateLoaderForPath(path)); + } + } + postProcessTemplateLoaders(templateLoaders); + + // Register template loaders that are supposed to kick in late. + if (this.postTemplateLoaders != null) { + templateLoaders.addAll(this.postTemplateLoaders); + } + + TemplateLoader loader = getAggregateTemplateLoader(templateLoaders); + if (loader != null) { + config.setTemplateLoader(loader); + } + + postProcessConfiguration(config); + return config; + } + + /** + * Return a new Configuration object. Subclasses can override this for custom + * initialization (e.g. specifying a FreeMarker compatibility level which is a + * new feature in FreeMarker 2.3.21), or for using a mock object for testing. + *

    Called by {@code createConfiguration()}. + * @return the Configuration object + * @throws IOException if a config file wasn't found + * @throws TemplateException on FreeMarker initialization failure + * @see #createConfiguration() + */ + protected Configuration newConfiguration() throws IOException, TemplateException { + return new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS); + } + + /** + * Determine a FreeMarker TemplateLoader for the given path. + *

    Default implementation creates either a FileTemplateLoader or + * a SpringTemplateLoader. + * @param templateLoaderPath the path to load templates from + * @return an appropriate TemplateLoader + * @see freemarker.cache.FileTemplateLoader + * @see SpringTemplateLoader + */ + protected TemplateLoader getTemplateLoaderForPath(String templateLoaderPath) { + if (isPreferFileSystemAccess()) { + // Try to load via the file system, fall back to SpringTemplateLoader + // (for hot detection of template changes, if possible). + try { + Resource path = getResourceLoader().getResource(templateLoaderPath); + File file = path.getFile(); // will fail if not resolvable in the file system + if (logger.isDebugEnabled()) { + logger.debug( + "Template loader path [" + path + "] resolved to file path [" + file.getAbsolutePath() + "]"); + } + return new FileTemplateLoader(file); + } + catch (Exception ex) { + if (logger.isDebugEnabled()) { + logger.debug("Cannot resolve template loader path [" + templateLoaderPath + + "] to [java.io.File]: using SpringTemplateLoader as fallback", ex); + } + return new SpringTemplateLoader(getResourceLoader(), templateLoaderPath); + } + } + else { + // Always load via SpringTemplateLoader (without hot detection of template changes). + logger.debug("File system access not preferred: using SpringTemplateLoader"); + return new SpringTemplateLoader(getResourceLoader(), templateLoaderPath); + } + } + + /** + * To be overridden by subclasses that want to register custom + * TemplateLoader instances after this factory created its default + * template loaders. + *

    Called by {@code createConfiguration()}. Note that specified + * "postTemplateLoaders" will be registered after any loaders + * registered by this callback; as a consequence, they are not + * included in the given List. + * @param templateLoaders the current List of TemplateLoader instances, + * to be modified by a subclass + * @see #createConfiguration() + * @see #setPostTemplateLoaders + */ + protected void postProcessTemplateLoaders(List templateLoaders) { + } + + /** + * Return a TemplateLoader based on the given TemplateLoader list. + * If more than one TemplateLoader has been registered, a FreeMarker + * MultiTemplateLoader needs to be created. + * @param templateLoaders the final List of TemplateLoader instances + * @return the aggregate TemplateLoader + */ + @Nullable + protected TemplateLoader getAggregateTemplateLoader(List templateLoaders) { + switch (templateLoaders.size()) { + case 0: + logger.debug("No FreeMarker TemplateLoaders specified"); + return null; + case 1: + return templateLoaders.get(0); + default: + TemplateLoader[] loaders = templateLoaders.toArray(new TemplateLoader[0]); + return new MultiTemplateLoader(loaders); + } + } + + /** + * To be overridden by subclasses that want to perform custom + * post-processing of the Configuration object after this factory + * performed its default initialization. + *

    Called by {@code createConfiguration()}. + * @param config the current Configuration object + * @throws IOException if a config file wasn't found + * @throws TemplateException on FreeMarker initialization failure + * @see #createConfiguration() + */ + protected void postProcessConfiguration(Configuration config) throws IOException, TemplateException { + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBean.java b/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBean.java new file mode 100644 index 0000000..5639ac0 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBean.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ui.freemarker; + +import java.io.IOException; + +import freemarker.template.Configuration; +import freemarker.template.TemplateException; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.lang.Nullable; + +/** + * Factory bean that creates a FreeMarker Configuration and provides it as + * bean reference. This bean is intended for any kind of usage of FreeMarker + * in application code, e.g. for generating email content. For web views, + * FreeMarkerConfigurer is used to set up a FreeMarkerConfigurationFactory. + * + * The simplest way to use this class is to specify just a "templateLoaderPath"; + * you do not need any further configuration then. For example, in a web + * application context: + * + *

     <bean id="freemarkerConfiguration" class="org.springframework.ui.freemarker.FreeMarkerConfigurationFactoryBean">
    + *   <property name="templateLoaderPath" value="/WEB-INF/freemarker/"/>
    + * </bean>
    + + * See the base class FreeMarkerConfigurationFactory for configuration details. + * + *

    Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher. + * + * @author Darren Davison + * @since 03.03.2004 + * @see #setConfigLocation + * @see #setFreemarkerSettings + * @see #setTemplateLoaderPath + * @see org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer + */ +public class FreeMarkerConfigurationFactoryBean extends FreeMarkerConfigurationFactory + implements FactoryBean, InitializingBean, ResourceLoaderAware { + + @Nullable + private Configuration configuration; + + + @Override + public void afterPropertiesSet() throws IOException, TemplateException { + this.configuration = createConfiguration(); + } + + + @Override + @Nullable + public Configuration getObject() { + return this.configuration; + } + + @Override + public Class getObjectType() { + return Configuration.class; + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerTemplateUtils.java b/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerTemplateUtils.java new file mode 100644 index 0000000..e1ca06b --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerTemplateUtils.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ui.freemarker; + +import java.io.IOException; +import java.io.StringWriter; + +import freemarker.template.Template; +import freemarker.template.TemplateException; + +/** + * Utility class for working with FreeMarker. + * Provides convenience methods to process a FreeMarker template with a model. + * + * @author Juergen Hoeller + * @since 14.03.2004 + */ +public abstract class FreeMarkerTemplateUtils { + + /** + * Process the specified FreeMarker template with the given model and write + * the result to the given Writer. + *

    When using this method to prepare a text for a mail to be sent with Spring's + * mail support, consider wrapping IO/TemplateException in MailPreparationException. + * @param model the model object, typically a Map that contains model names + * as keys and model objects as values + * @return the result as String + * @throws IOException if the template wasn't found or couldn't be read + * @throws freemarker.template.TemplateException if rendering failed + * @see org.springframework.mail.MailPreparationException + */ + public static String processTemplateIntoString(Template template, Object model) + throws IOException, TemplateException { + + StringWriter result = new StringWriter(1024); + template.process(model, result); + return result.toString(); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/ui/freemarker/SpringTemplateLoader.java b/spring-context-support/src/main/java/org/springframework/ui/freemarker/SpringTemplateLoader.java new file mode 100644 index 0000000..d4140aa --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/ui/freemarker/SpringTemplateLoader.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ui.freemarker; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; + +import freemarker.cache.TemplateLoader; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.lang.Nullable; + +/** + * FreeMarker {@link TemplateLoader} adapter that loads via a Spring {@link ResourceLoader}. + * Used by {@link FreeMarkerConfigurationFactory} for any resource loader path that cannot + * be resolved to a {@link java.io.File}. + * + * @author Juergen Hoeller + * @since 14.03.2004 + * @see FreeMarkerConfigurationFactory#setTemplateLoaderPath + * @see freemarker.template.Configuration#setDirectoryForTemplateLoading + */ +public class SpringTemplateLoader implements TemplateLoader { + + protected final Log logger = LogFactory.getLog(getClass()); + + private final ResourceLoader resourceLoader; + + private final String templateLoaderPath; + + + /** + * Create a new SpringTemplateLoader. + * @param resourceLoader the Spring ResourceLoader to use + * @param templateLoaderPath the template loader path to use + */ + public SpringTemplateLoader(ResourceLoader resourceLoader, String templateLoaderPath) { + this.resourceLoader = resourceLoader; + if (!templateLoaderPath.endsWith("/")) { + templateLoaderPath += "/"; + } + this.templateLoaderPath = templateLoaderPath; + if (logger.isDebugEnabled()) { + logger.debug("SpringTemplateLoader for FreeMarker: using resource loader [" + this.resourceLoader + + "] and template loader path [" + this.templateLoaderPath + "]"); + } + } + + + @Override + @Nullable + public Object findTemplateSource(String name) throws IOException { + if (logger.isDebugEnabled()) { + logger.debug("Looking for FreeMarker template with name [" + name + "]"); + } + Resource resource = this.resourceLoader.getResource(this.templateLoaderPath + name); + return (resource.exists() ? resource : null); + } + + @Override + public Reader getReader(Object templateSource, String encoding) throws IOException { + Resource resource = (Resource) templateSource; + try { + return new InputStreamReader(resource.getInputStream(), encoding); + } + catch (IOException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Could not find FreeMarker template: " + resource); + } + throw ex; + } + } + + @Override + public long getLastModified(Object templateSource) { + Resource resource = (Resource) templateSource; + try { + return resource.lastModified(); + } + catch (IOException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Could not obtain last-modified timestamp for FreeMarker template in " + + resource + ": " + ex); + } + return -1; + } + } + + @Override + public void closeTemplateSource(Object templateSource) throws IOException { + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/ui/freemarker/package-info.java b/spring-context-support/src/main/java/org/springframework/ui/freemarker/package-info.java new file mode 100644 index 0000000..492946e --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/ui/freemarker/package-info.java @@ -0,0 +1,11 @@ +/** + * Support classes for setting up + * FreeMarker + * within a Spring application context. + */ +@NonNullApi +@NonNullFields +package org.springframework.ui.freemarker; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context-support/src/main/resources/org/springframework/mail/javamail/mime.types b/spring-context-support/src/main/resources/org/springframework/mail/javamail/mime.types new file mode 100644 index 0000000..b03625f --- /dev/null +++ b/spring-context-support/src/main/resources/org/springframework/mail/javamail/mime.types @@ -0,0 +1,326 @@ +################################################################################ +# Copyright 2002-2019 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. +# You may obtain a copy of the License at +# +# https://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. +################################################################################ + +################################################################################ +# +# Defaults for the Java Activation Framework (revised). +# +# Modified extensions registered in this file: +# text/plain java c c++ cpp pl cc h +# image/png png +# image/svg+xml svg +# +################################################################################ + +text/html html htm HTML HTM +text/plain txt text TXT TEXT java c c++ cpp pl cc h +image/gif gif GIF +image/ief ief +image/jpeg jpeg jpg jpe JPG +image/tiff tiff tif +image/x-xwindowdump xwd +application/postscript ai eps ps +application/rtf rtf +application/x-tex tex +application/x-texinfo texinfo texi +application/x-troff t tr roff +audio/basic au +audio/midi midi mid +audio/x-aifc aifc +audio/x-aiff aif aiff +audio/x-mpeg mpeg mpg +audio/x-wav wav +video/mpeg mpeg mpg mpe +video/quicktime qt mov +video/x-msvideo avi + +################################################################################ +# +# Additional file types adapted from +# http://sites.utoronto.ca/webdocs/HTMLdocs/Book/Book-3ed/appb/mimetype.html +# kindly re-licensed to Apache Software License 2.0 by Ian Graham. +# +################################################################################ + +# TEXT TYPES + +text/x-speech talk +text/css css +text/csv csv + +# IMAGE TYPES + +# X-Windows bitmap (b/w) +image/x-xbitmap xbm +# X-Windows pixelmap (8-bit color) +image/x-xpixmap xpm +# Portable Network Graphics +image/png png +# Scalable Vector Graphics +image/svg+xml svg +# Image Exchange Format (RFC 1314) +image/ief ief +# RGB +image/rgb rgb +# Group III Fax (RFC 1494) +image/g3fax g3f +# X Windowdump format +image/x-xwindowdump xwd +# Macintosh PICT format +image/x-pict pict +# PPM (UNIX PPM package) +image/x-portable-pixmap ppm +# PGM (UNIX PPM package) +image/x-portable-graymap pgm +# PBM (UNIX PPM package) +image/x-portable-bitmap pbm +# PNM (UNIX PPM package) +image/x-portable-anymap pnm +# Microsoft Windows bitmap +image/x-ms-bmp bmp +# CMU raster +image/x-cmu-raster ras +# Kodak Photo-CD +image/x-photo-cd pcd +# Computer Graphics Metafile +image/cgm cgm +# CALS Type 1 or 2 +image/x-cals mil cal +# Fractal Image Format (Iterated Systems) +image/fif fif +# QuickSilver active image (Micrografx) +image/x-mgx-dsf dsf +# CMX vector image (Corel) +image/x-cmx cmx +# Wavelet-compressed (Summus) +image/wavelet wi +# AutoCad Drawing (SoftSource) +image/vnd.dwg dwg +# AutoCad DXF file (SoftSource) +image/vnd.dxf dxf +# Simple Vector Format (SoftSource) +image/vnd.svf svf + +# AUDIO/VOICE/MUSIC RELATED TYPES + +# """basic""audio - 8-bit u-law PCM" +audio/basic au snd +# Macintosh audio format (AIpple) +audio/x-aiff aif aiff aifc +# Microsoft audio +audio/x-wav wav +# MPEG audio +audio/x-mpeg mpa abs mpega +# MPEG-2 audio +audio/x-mpeg-2 mp2a mpa2 +# compressed speech (Echo Speech Corp.) +audio/echospeech es +# Toolvox speech audio (Voxware) +audio/voxware vox +# RapidTransit compressed audio (Fast Man) +application/fastman lcc +# Realaudio (Progressive Networks) +application/x-pn-realaudio ra ram +# MIDI music data +x-music/x-midi mmid +# Koan music data (SSeyo) +application/vnd.koan skp +# Speech synthesis data (MVP Solutions) +text/x-speech talk + +# VIDEO TYPES + +# MPEG video +video/mpeg mpeg mpg mpe +# MPEG-2 video +video/mpeg-2 mpv2 mp2v +# Macintosh Quicktime +video/quicktime qt mov +# Microsoft video +video/x-msvideo avi +# SGI Movie format +video/x-sgi-movie movie +# VDOlive streaming video (VDOnet) +video/vdo vdo +# Vivo streaming video (Vivo software) +video/vnd.vivo viv + +# SPECIAL HTTP/WEB APPLICATION TYPES + +# Proxy autoconfiguration (Netscape browsers) +application/x-ns-proxy-autoconfig pac +# Netscape Cooltalk chat data (Netscape) +x-conference/x-cooltalk ice + +# TEXT-RELATED + +# PostScript +application/postscript ai eps ps +# Microsoft Rich Text Format +application/rtf rtf +# Adobe Acrobat PDF +application/pdf pdf +# Maker Interchange Format (FrameMaker) +application/vnd.mif mif +# Troff document +application/x-troff t tr roff +# Troff document with MAN macros +application/x-troff-man man +# Troff document with ME macros +application/x-troff-me me +# Troff document with MS macros +application/x-troff-ms ms +# LaTeX document +application/x-latex latex +# Tex/LateX document +application/x-tex tex +# GNU TexInfo document +application/x-texinfo texinfo texi +# TeX dvi format +application/x-dvi dvi +# MS word document +application/msword doc DOC +# Office Document Architecture +application/oda oda +# Envoy Document +application/envoy evy + +# ARCHIVE/COMPRESSED ARCHIVES + +# Gnu tar format +application/x-gtar gtar +# 4.3BSD tar format +application/x-tar tar +# POSIX tar format +application/x-ustar ustar +# Old CPIO format +application/x-bcpio bcpio +# POSIX CPIO format +application/x-cpio cpio +# UNIX sh shell archive +application/x-shar shar +# DOS/PC - Pkzipped archive +application/zip zip +# Macintosh Binhexed archive +application/mac-binhex40 hqx +# Macintosh Stuffit Archive +application/x-stuffit sit sea +# Fractal Image Format +application/fractals fif +# "Binary UUencoded" +application/octet-stream bin uu +# PC executable +application/octet-stream exe +# "WAIS ""sources""" +application/x-wais-source src wsrc +# NCSA HDF data format +application/hdf hdf + +# DOWNLOADABLE PROGRAM/SCRIPTS + +# Javascript program +text/javascript js ls mocha +# UNIX bourne shell program +application/x-sh sh +# UNIX c-shell program +application/x-csh csh +# Perl program +application/x-perl pl +# Tcl (Tool Control Language) program +application/x-tcl tcl + +# ANIMATION/MULTIMEDIA + +# FutureSplash vector animation (FutureWave) +application/futuresplash spl +# mBED multimedia data (mBED) +application/mbedlet mbd +# PowerMedia multimedia (RadMedia) +application/x-rad-powermedia rad + +# PRESENTATION + +# PowerPoint presentation (Microsoft) +application/mspowerpoint ppz +# ASAP WordPower (Software Publishing Corp.) +application/x-asap asp +# Astound Web Player multimedia data (GoldDisk) +application/astound asn + +# SPECIAL EMBEDDED OBJECT + +# OLE script e.g. Visual Basic (Ncompass) +application/x-olescript axs +# OLE Object (Microsoft/NCompass) +application/x-oleobject ods +# OpenScape OLE/OCX objects (Business@Web) +x-form/x-openscape opp +# Visual Basic objects (Amara) +application/x-webbasic wba +# Specialized data entry forms (Alpha Software) +application/x-alpha-form frm +# client-server objects (Wayfarer Communications) +x-script/x-wfxclient wfx + +# GENERAL APPLICATIONS + +# Undefined binary data (often executable progs) +application/octet-stream exe com +# Pointcast news data (Pointcast) +application/x-pcn pcn +# Excel spreadsheet (Microsoft) +application/vnd.ms-excel xls +# PowerPoint (Microsoft) +application/vnd.ms-powerpoint ppt +# Microsoft Project (Microsoft) +application/vnd.ms-project mpp +# SourceView document (Dataware Electronics) +application/vnd.svd svd +# Net Install - software install (20/20 Software) +application/x-net-install ins +# Carbon Copy - remote control/access (Microcom) +application/ccv ccv +# Spreadsheets (Visual Components) +workbook/formulaone vts + +# 2D/3D DATA/VIRTUAL REALITY TYPES + +# VRML data file +x-world/x-vrml wrl vrml +# WIRL - VRML data (VREAM) +x-world/x-vream vrw +# Play3D 3d scene data (Play3D) +application/x-p3d p3d +# Viscape Interactive 3d world data (Superscape) +x-world/x-svr svr +# WebActive 3d data (Plastic Thought) +x-world/x-wvr wvr +# QuickDraw3D scene data (Apple) +x-world/x-3dmf 3dmf + +# SCIENTIFIC/MATH/CAD TYPES + +# Mathematica notebook +application/mathematica ma +# Computational meshes for numerical simulations +x-model/x-mesh msh +# Vis5D 5-dimensional data +application/vis5d v5d +# IGES models -- CAD/CAM (CGM) data +application/iges igs +# Autocad WHIP vector drawings +drawing/x-dwf dwf + diff --git a/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java new file mode 100644 index 0000000..e90a3ec --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java @@ -0,0 +1,214 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.caffeine; + +import com.github.benmanes.caffeine.cache.CacheLoader; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.CaffeineSpec; +import org.junit.jupiter.api.Test; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.mock; + +/** + * @author Ben Manes + * @author Juergen Hoeller + * @author Stephane Nicoll + */ +public class CaffeineCacheManagerTests { + + @Test + public void testDynamicMode() { + CacheManager cm = new CaffeineCacheManager(); + Cache cache1 = cm.getCache("c1"); + boolean condition2 = cache1 instanceof CaffeineCache; + assertThat(condition2).isTrue(); + Cache cache1again = cm.getCache("c1"); + assertThat(cache1).isSameAs(cache1again); + Cache cache2 = cm.getCache("c2"); + boolean condition1 = cache2 instanceof CaffeineCache; + assertThat(condition1).isTrue(); + Cache cache2again = cm.getCache("c2"); + assertThat(cache2).isSameAs(cache2again); + Cache cache3 = cm.getCache("c3"); + boolean condition = cache3 instanceof CaffeineCache; + assertThat(condition).isTrue(); + Cache cache3again = cm.getCache("c3"); + assertThat(cache3).isSameAs(cache3again); + + cache1.put("key1", "value1"); + assertThat(cache1.get("key1").get()).isEqualTo("value1"); + cache1.put("key2", 2); + assertThat(cache1.get("key2").get()).isEqualTo(2); + cache1.put("key3", null); + assertThat(cache1.get("key3").get()).isNull(); + cache1.evict("key3"); + assertThat(cache1.get("key3")).isNull(); + } + + @Test + public void testStaticMode() { + CaffeineCacheManager cm = new CaffeineCacheManager("c1", "c2"); + Cache cache1 = cm.getCache("c1"); + boolean condition3 = cache1 instanceof CaffeineCache; + assertThat(condition3).isTrue(); + Cache cache1again = cm.getCache("c1"); + assertThat(cache1).isSameAs(cache1again); + Cache cache2 = cm.getCache("c2"); + boolean condition2 = cache2 instanceof CaffeineCache; + assertThat(condition2).isTrue(); + Cache cache2again = cm.getCache("c2"); + assertThat(cache2).isSameAs(cache2again); + Cache cache3 = cm.getCache("c3"); + assertThat(cache3).isNull(); + + cache1.put("key1", "value1"); + assertThat(cache1.get("key1").get()).isEqualTo("value1"); + cache1.put("key2", 2); + assertThat(cache1.get("key2").get()).isEqualTo(2); + cache1.put("key3", null); + assertThat(cache1.get("key3").get()).isNull(); + cache1.evict("key3"); + assertThat(cache1.get("key3")).isNull(); + + cm.setAllowNullValues(false); + Cache cache1x = cm.getCache("c1"); + boolean condition1 = cache1x instanceof CaffeineCache; + assertThat(condition1).isTrue(); + assertThat(cache1x != cache1).isTrue(); + Cache cache2x = cm.getCache("c2"); + boolean condition = cache2x instanceof CaffeineCache; + assertThat(condition).isTrue(); + assertThat(cache2x != cache2).isTrue(); + Cache cache3x = cm.getCache("c3"); + assertThat(cache3x).isNull(); + + cache1x.put("key1", "value1"); + assertThat(cache1x.get("key1").get()).isEqualTo("value1"); + cache1x.put("key2", 2); + assertThat(cache1x.get("key2").get()).isEqualTo(2); + + cm.setAllowNullValues(true); + Cache cache1y = cm.getCache("c1"); + + cache1y.put("key3", null); + assertThat(cache1y.get("key3").get()).isNull(); + cache1y.evict("key3"); + assertThat(cache1y.get("key3")).isNull(); + } + + @Test + public void changeCaffeineRecreateCache() { + CaffeineCacheManager cm = new CaffeineCacheManager("c1"); + Cache cache1 = cm.getCache("c1"); + + Caffeine caffeine = Caffeine.newBuilder().maximumSize(10); + cm.setCaffeine(caffeine); + Cache cache1x = cm.getCache("c1"); + assertThat(cache1x != cache1).isTrue(); + + cm.setCaffeine(caffeine); // Set same instance + Cache cache1xx = cm.getCache("c1"); + assertThat(cache1xx).isSameAs(cache1x); + } + + @Test + public void changeCaffeineSpecRecreateCache() { + CaffeineCacheManager cm = new CaffeineCacheManager("c1"); + Cache cache1 = cm.getCache("c1"); + + cm.setCaffeineSpec(CaffeineSpec.parse("maximumSize=10")); + Cache cache1x = cm.getCache("c1"); + assertThat(cache1x != cache1).isTrue(); + } + + @Test + public void changeCacheSpecificationRecreateCache() { + CaffeineCacheManager cm = new CaffeineCacheManager("c1"); + Cache cache1 = cm.getCache("c1"); + + cm.setCacheSpecification("maximumSize=10"); + Cache cache1x = cm.getCache("c1"); + assertThat(cache1x != cache1).isTrue(); + } + + @Test + public void changeCacheLoaderRecreateCache() { + CaffeineCacheManager cm = new CaffeineCacheManager("c1"); + Cache cache1 = cm.getCache("c1"); + + @SuppressWarnings("unchecked") + CacheLoader loader = mock(CacheLoader.class); + + cm.setCacheLoader(loader); + Cache cache1x = cm.getCache("c1"); + assertThat(cache1x != cache1).isTrue(); + + cm.setCacheLoader(loader); // Set same instance + Cache cache1xx = cm.getCache("c1"); + assertThat(cache1xx).isSameAs(cache1x); + } + + @Test + public void setCacheNameNullRestoreDynamicMode() { + CaffeineCacheManager cm = new CaffeineCacheManager("c1"); + assertThat(cm.getCache("someCache")).isNull(); + cm.setCacheNames(null); + assertThat(cm.getCache("someCache")).isNotNull(); + } + + @Test + public void cacheLoaderUseLoadingCache() { + CaffeineCacheManager cm = new CaffeineCacheManager("c1"); + cm.setCacheLoader(new CacheLoader() { + @Override + public Object load(Object key) throws Exception { + if ("ping".equals(key)) { + return "pong"; + } + throw new IllegalArgumentException("I only know ping"); + } + }); + Cache cache1 = cm.getCache("c1"); + Cache.ValueWrapper value = cache1.get("ping"); + assertThat(value).isNotNull(); + assertThat(value.get()).isEqualTo("pong"); + + assertThatIllegalArgumentException().isThrownBy(() -> assertThat(cache1.get("foo")).isNull()) + .withMessageContaining("I only know ping"); + } + + @Test + public void customCacheRegistration() { + CaffeineCacheManager cm = new CaffeineCacheManager("c1"); + com.github.benmanes.caffeine.cache.Cache nc = Caffeine.newBuilder().build(); + cm.registerCustomCache("c2", nc); + + Cache cache1 = cm.getCache("c1"); + Cache cache2 = cm.getCache("c2"); + assertThat(nc == cache2.getNativeCache()).isTrue(); + + cm.setCaffeine(Caffeine.newBuilder().maximumSize(10)); + assertThat(cm.getCache("c1") != cache1).isTrue(); + assertThat(cm.getCache("c2") == cache2).isTrue(); + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheTests.java b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheTests.java new file mode 100644 index 0000000..2157203 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.caffeine; + +import com.github.benmanes.caffeine.cache.Caffeine; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.cache.Cache; +import org.springframework.cache.Cache.ValueWrapper; +import org.springframework.context.testfixture.cache.AbstractValueAdaptingCacheTests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link CaffeineCache}. + * + * @author Ben Manes + * @author Stephane Nicoll + */ +class CaffeineCacheTests extends AbstractValueAdaptingCacheTests { + + private com.github.benmanes.caffeine.cache.Cache nativeCache; + + private CaffeineCache cache; + + private CaffeineCache cacheNoNull; + + @BeforeEach + void setUp() { + nativeCache = Caffeine.newBuilder().build(); + cache = new CaffeineCache(CACHE_NAME, nativeCache); + com.github.benmanes.caffeine.cache.Cache nativeCacheNoNull + = Caffeine.newBuilder().build(); + cacheNoNull = new CaffeineCache(CACHE_NAME_NO_NULL, nativeCacheNoNull, false); + } + + @Override + protected CaffeineCache getCache() { + return getCache(true); + } + + @Override + protected CaffeineCache getCache(boolean allowNull) { + return allowNull ? this.cache : this.cacheNoNull; + } + + @Override + protected Object getNativeCache() { + return nativeCache; + } + + @Test + void testLoadingCacheGet() { + Object value = new Object(); + CaffeineCache loadingCache = new CaffeineCache(CACHE_NAME, Caffeine.newBuilder() + .build(key -> value)); + ValueWrapper valueWrapper = loadingCache.get(new Object()); + assertThat(valueWrapper).isNotNull(); + assertThat(valueWrapper.get()).isEqualTo(value); + } + + @Test + void testLoadingCacheGetWithType() { + String value = "value"; + CaffeineCache loadingCache = new CaffeineCache(CACHE_NAME, Caffeine.newBuilder() + .build(key -> value)); + String valueWrapper = loadingCache.get(new Object(), String.class); + assertThat(valueWrapper).isNotNull(); + assertThat(valueWrapper).isEqualTo(value); + } + + @Test + void testLoadingCacheGetWithWrongType() { + String value = "value"; + CaffeineCache loadingCache = new CaffeineCache(CACHE_NAME, Caffeine.newBuilder() + .build(key -> value)); + assertThatIllegalStateException().isThrownBy(() -> loadingCache.get(new Object(), Long.class)); + } + + @Test + void testPutIfAbsentNullValue() { + CaffeineCache cache = getCache(); + + Object key = new Object(); + Object value = null; + + assertThat(cache.get(key)).isNull(); + assertThat(cache.putIfAbsent(key, value)).isNull(); + assertThat(cache.get(key).get()).isEqualTo(value); + Cache.ValueWrapper wrapper = cache.putIfAbsent(key, "anotherValue"); + // A value is set but is 'null' + assertThat(wrapper).isNotNull(); + assertThat(wrapper.get()).isEqualTo(null); + // not changed + assertThat(cache.get(key).get()).isEqualTo(value); + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/ehcache/EhCacheCacheManagerTests.java b/spring-context-support/src/test/java/org/springframework/cache/ehcache/EhCacheCacheManagerTests.java new file mode 100644 index 0000000..fd33777 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/ehcache/EhCacheCacheManagerTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.ehcache; + +import net.sf.ehcache.CacheManager; +import net.sf.ehcache.config.CacheConfiguration; +import net.sf.ehcache.config.Configuration; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +import org.springframework.cache.transaction.AbstractTransactionSupportingCacheManagerTests; + +/** + * @author Stephane Nicoll + */ +public class EhCacheCacheManagerTests extends AbstractTransactionSupportingCacheManagerTests { + + private CacheManager nativeCacheManager; + + private EhCacheCacheManager cacheManager; + + private EhCacheCacheManager transactionalCacheManager; + + + @BeforeEach + public void setup() { + nativeCacheManager = new CacheManager(new Configuration().name("EhCacheCacheManagerTests") + .defaultCache(new CacheConfiguration("default", 100))); + addNativeCache(CACHE_NAME); + + cacheManager = new EhCacheCacheManager(nativeCacheManager); + cacheManager.setTransactionAware(false); + cacheManager.afterPropertiesSet(); + + transactionalCacheManager = new EhCacheCacheManager(nativeCacheManager); + transactionalCacheManager.setTransactionAware(true); + transactionalCacheManager.afterPropertiesSet(); + } + + @AfterEach + public void shutdown() { + nativeCacheManager.shutdown(); + } + + + @Override + protected EhCacheCacheManager getCacheManager(boolean transactionAware) { + if (transactionAware) { + return transactionalCacheManager; + } + else { + return cacheManager; + } + } + + @Override + protected Class getCacheType() { + return EhCacheCache.class; + } + + @Override + protected void addNativeCache(String cacheName) { + nativeCacheManager.addCache(cacheName); + } + + @Override + protected void removeNativeCache(String cacheName) { + nativeCacheManager.removeCache(cacheName); + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/ehcache/EhCacheCacheTests.java b/spring-context-support/src/test/java/org/springframework/cache/ehcache/EhCacheCacheTests.java new file mode 100644 index 0000000..b9bb1cc --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/ehcache/EhCacheCacheTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.ehcache; + +import net.sf.ehcache.CacheManager; +import net.sf.ehcache.Ehcache; +import net.sf.ehcache.Element; +import net.sf.ehcache.config.CacheConfiguration; +import net.sf.ehcache.config.Configuration; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.context.testfixture.cache.AbstractCacheTests; +import org.springframework.core.testfixture.EnabledForTestGroups; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; + +/** + * @author Costin Leau + * @author Stephane Nicoll + * @author Juergen Hoeller + */ +public class EhCacheCacheTests extends AbstractCacheTests { + + private CacheManager cacheManager; + + private Ehcache nativeCache; + + private EhCacheCache cache; + + + @BeforeEach + public void setup() { + cacheManager = new CacheManager(new Configuration().name("EhCacheCacheTests") + .defaultCache(new CacheConfiguration("default", 100))); + nativeCache = new net.sf.ehcache.Cache(new CacheConfiguration(CACHE_NAME, 100)); + cacheManager.addCache(nativeCache); + + cache = new EhCacheCache(nativeCache); + } + + @AfterEach + public void shutdown() { + cacheManager.shutdown(); + } + + + @Override + protected EhCacheCache getCache() { + return cache; + } + + @Override + protected Ehcache getNativeCache() { + return nativeCache; + } + + + @Test + @EnabledForTestGroups(LONG_RUNNING) + public void testExpiredElements() throws Exception { + String key = "brancusi"; + String value = "constantin"; + Element brancusi = new Element(key, value); + // ttl = 10s + brancusi.setTimeToLive(3); + nativeCache.put(brancusi); + + assertThat(cache.get(key).get()).isEqualTo(value); + // wait for the entry to expire + Thread.sleep(5 * 1000); + assertThat(cache.get(key)).isNull(); + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/ehcache/EhCacheSupportTests.java b/spring-context-support/src/test/java/org/springframework/cache/ehcache/EhCacheSupportTests.java new file mode 100644 index 0000000..c663b0f --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/ehcache/EhCacheSupportTests.java @@ -0,0 +1,287 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.ehcache; + +import net.sf.ehcache.Cache; +import net.sf.ehcache.CacheException; +import net.sf.ehcache.CacheManager; +import net.sf.ehcache.Ehcache; +import net.sf.ehcache.config.CacheConfiguration; +import net.sf.ehcache.constructs.blocking.BlockingCache; +import net.sf.ehcache.constructs.blocking.SelfPopulatingCache; +import net.sf.ehcache.constructs.blocking.UpdatingCacheEntryFactory; +import net.sf.ehcache.constructs.blocking.UpdatingSelfPopulatingCache; +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Juergen Hoeller + * @author Dmitriy Kopylenko + * @since 27.09.2004 + */ +public class EhCacheSupportTests { + + @Test + public void testBlankCacheManager() { + EhCacheManagerFactoryBean cacheManagerFb = new EhCacheManagerFactoryBean(); + cacheManagerFb.setCacheManagerName("myCacheManager"); + assertThat(cacheManagerFb.getObjectType()).isEqualTo(CacheManager.class); + assertThat(cacheManagerFb.isSingleton()).as("Singleton property").isTrue(); + cacheManagerFb.afterPropertiesSet(); + try { + CacheManager cm = cacheManagerFb.getObject(); + assertThat(cm.getCacheNames().length == 0).as("Loaded CacheManager with no caches").isTrue(); + Cache myCache1 = cm.getCache("myCache1"); + assertThat(myCache1 == null).as("No myCache1 defined").isTrue(); + } + finally { + cacheManagerFb.destroy(); + } + } + + @Test + public void testCacheManagerConflict() { + EhCacheManagerFactoryBean cacheManagerFb = new EhCacheManagerFactoryBean(); + try { + cacheManagerFb.setCacheManagerName("myCacheManager"); + assertThat(cacheManagerFb.getObjectType()).isEqualTo(CacheManager.class); + assertThat(cacheManagerFb.isSingleton()).as("Singleton property").isTrue(); + cacheManagerFb.afterPropertiesSet(); + CacheManager cm = cacheManagerFb.getObject(); + assertThat(cm.getCacheNames().length == 0).as("Loaded CacheManager with no caches").isTrue(); + Cache myCache1 = cm.getCache("myCache1"); + assertThat(myCache1 == null).as("No myCache1 defined").isTrue(); + + EhCacheManagerFactoryBean cacheManagerFb2 = new EhCacheManagerFactoryBean(); + cacheManagerFb2.setCacheManagerName("myCacheManager"); + assertThatExceptionOfType(CacheException.class).as("because of naming conflict").isThrownBy( + cacheManagerFb2::afterPropertiesSet); + } + finally { + cacheManagerFb.destroy(); + } + } + + @Test + public void testAcceptExistingCacheManager() { + EhCacheManagerFactoryBean cacheManagerFb = new EhCacheManagerFactoryBean(); + cacheManagerFb.setCacheManagerName("myCacheManager"); + assertThat(cacheManagerFb.getObjectType()).isEqualTo(CacheManager.class); + assertThat(cacheManagerFb.isSingleton()).as("Singleton property").isTrue(); + cacheManagerFb.afterPropertiesSet(); + try { + CacheManager cm = cacheManagerFb.getObject(); + assertThat(cm.getCacheNames().length == 0).as("Loaded CacheManager with no caches").isTrue(); + Cache myCache1 = cm.getCache("myCache1"); + assertThat(myCache1 == null).as("No myCache1 defined").isTrue(); + + EhCacheManagerFactoryBean cacheManagerFb2 = new EhCacheManagerFactoryBean(); + cacheManagerFb2.setCacheManagerName("myCacheManager"); + cacheManagerFb2.setAcceptExisting(true); + cacheManagerFb2.afterPropertiesSet(); + CacheManager cm2 = cacheManagerFb2.getObject(); + assertThat(cm2).isSameAs(cm); + cacheManagerFb2.destroy(); + } + finally { + cacheManagerFb.destroy(); + } + } + + public void testCacheManagerFromConfigFile() { + EhCacheManagerFactoryBean cacheManagerFb = new EhCacheManagerFactoryBean(); + cacheManagerFb.setConfigLocation(new ClassPathResource("testEhcache.xml", getClass())); + cacheManagerFb.setCacheManagerName("myCacheManager"); + cacheManagerFb.afterPropertiesSet(); + try { + CacheManager cm = cacheManagerFb.getObject(); + assertThat(cm.getCacheNames().length == 1).as("Correct number of caches loaded").isTrue(); + Cache myCache1 = cm.getCache("myCache1"); + assertThat(myCache1.getCacheConfiguration().isEternal()).as("myCache1 is not eternal").isFalse(); + assertThat(myCache1.getCacheConfiguration().getMaxEntriesLocalHeap() == 300).as("myCache1.maxElements == 300").isTrue(); + } + finally { + cacheManagerFb.destroy(); + } + } + + @Test + public void testEhCacheFactoryBeanWithDefaultCacheManager() { + doTestEhCacheFactoryBean(false); + } + + @Test + public void testEhCacheFactoryBeanWithExplicitCacheManager() { + doTestEhCacheFactoryBean(true); + } + + private void doTestEhCacheFactoryBean(boolean useCacheManagerFb) { + Cache cache; + EhCacheManagerFactoryBean cacheManagerFb = null; + boolean cacheManagerFbInitialized = false; + try { + EhCacheFactoryBean cacheFb = new EhCacheFactoryBean(); + Class objectType = cacheFb.getObjectType(); + assertThat(Ehcache.class.isAssignableFrom(objectType)).isTrue(); + assertThat(cacheFb.isSingleton()).as("Singleton property").isTrue(); + if (useCacheManagerFb) { + cacheManagerFb = new EhCacheManagerFactoryBean(); + cacheManagerFb.setConfigLocation(new ClassPathResource("testEhcache.xml", getClass())); + cacheManagerFb.setCacheManagerName("cache"); + cacheManagerFb.afterPropertiesSet(); + cacheManagerFbInitialized = true; + cacheFb.setCacheManager(cacheManagerFb.getObject()); + } + + cacheFb.setCacheName("myCache1"); + cacheFb.afterPropertiesSet(); + cache = (Cache) cacheFb.getObject(); + Class objectType2 = cacheFb.getObjectType(); + assertThat(objectType2).isSameAs(objectType); + CacheConfiguration config = cache.getCacheConfiguration(); + assertThat(cache.getName()).isEqualTo("myCache1"); + if (useCacheManagerFb){ + assertThat(config.getMaxEntriesLocalHeap()).as("myCache1.maxElements").isEqualTo(300); + } + else { + assertThat(config.getMaxEntriesLocalHeap()).as("myCache1.maxElements").isEqualTo(10000); + } + + // Cache region is not defined. Should create one with default properties. + cacheFb = new EhCacheFactoryBean(); + if (useCacheManagerFb) { + cacheFb.setCacheManager(cacheManagerFb.getObject()); + } + cacheFb.setCacheName("undefinedCache"); + cacheFb.afterPropertiesSet(); + cache = (Cache) cacheFb.getObject(); + config = cache.getCacheConfiguration(); + assertThat(cache.getName()).isEqualTo("undefinedCache"); + assertThat(config.getMaxEntriesLocalHeap() == 10000).as("default maxElements is correct").isTrue(); + assertThat(config.isEternal()).as("default eternal is correct").isFalse(); + assertThat(config.getTimeToLiveSeconds() == 120).as("default timeToLive is correct").isTrue(); + assertThat(config.getTimeToIdleSeconds() == 120).as("default timeToIdle is correct").isTrue(); + assertThat(config.getDiskExpiryThreadIntervalSeconds() == 120).as("default diskExpiryThreadIntervalSeconds is correct").isTrue(); + + // overriding the default properties + cacheFb = new EhCacheFactoryBean(); + if (useCacheManagerFb) { + cacheFb.setCacheManager(cacheManagerFb.getObject()); + } + cacheFb.setBeanName("undefinedCache2"); + cacheFb.setMaxEntriesLocalHeap(5); + cacheFb.setTimeToLive(8); + cacheFb.setTimeToIdle(7); + cacheFb.setDiskExpiryThreadIntervalSeconds(10); + cacheFb.afterPropertiesSet(); + cache = (Cache) cacheFb.getObject(); + config = cache.getCacheConfiguration(); + + assertThat(cache.getName()).isEqualTo("undefinedCache2"); + assertThat(config.getMaxEntriesLocalHeap() == 5).as("overridden maxElements is correct").isTrue(); + assertThat(config.getTimeToLiveSeconds() == 8).as("default timeToLive is correct").isTrue(); + assertThat(config.getTimeToIdleSeconds() == 7).as("default timeToIdle is correct").isTrue(); + assertThat(config.getDiskExpiryThreadIntervalSeconds() == 10).as("overridden diskExpiryThreadIntervalSeconds is correct").isTrue(); + } + finally { + if (cacheManagerFbInitialized) { + cacheManagerFb.destroy(); + } + else { + CacheManager.getInstance().shutdown(); + } + } + } + + @Test + public void testEhCacheFactoryBeanWithBlockingCache() { + EhCacheManagerFactoryBean cacheManagerFb = new EhCacheManagerFactoryBean(); + cacheManagerFb.afterPropertiesSet(); + try { + CacheManager cm = cacheManagerFb.getObject(); + EhCacheFactoryBean cacheFb = new EhCacheFactoryBean(); + cacheFb.setCacheManager(cm); + cacheFb.setCacheName("myCache1"); + cacheFb.setBlocking(true); + assertThat(BlockingCache.class).isEqualTo(cacheFb.getObjectType()); + cacheFb.afterPropertiesSet(); + Ehcache myCache1 = cm.getEhcache("myCache1"); + boolean condition = myCache1 instanceof BlockingCache; + assertThat(condition).isTrue(); + } + finally { + cacheManagerFb.destroy(); + } + } + + @Test + public void testEhCacheFactoryBeanWithSelfPopulatingCache() { + EhCacheManagerFactoryBean cacheManagerFb = new EhCacheManagerFactoryBean(); + cacheManagerFb.afterPropertiesSet(); + try { + CacheManager cm = cacheManagerFb.getObject(); + EhCacheFactoryBean cacheFb = new EhCacheFactoryBean(); + cacheFb.setCacheManager(cm); + cacheFb.setCacheName("myCache1"); + cacheFb.setCacheEntryFactory(key -> key); + assertThat(SelfPopulatingCache.class).isEqualTo(cacheFb.getObjectType()); + cacheFb.afterPropertiesSet(); + Ehcache myCache1 = cm.getEhcache("myCache1"); + boolean condition = myCache1 instanceof SelfPopulatingCache; + assertThat(condition).isTrue(); + assertThat(myCache1.get("myKey1").getObjectValue()).isEqualTo("myKey1"); + } + finally { + cacheManagerFb.destroy(); + } + } + + @Test + public void testEhCacheFactoryBeanWithUpdatingSelfPopulatingCache() { + EhCacheManagerFactoryBean cacheManagerFb = new EhCacheManagerFactoryBean(); + cacheManagerFb.afterPropertiesSet(); + try { + CacheManager cm = cacheManagerFb.getObject(); + EhCacheFactoryBean cacheFb = new EhCacheFactoryBean(); + cacheFb.setCacheManager(cm); + cacheFb.setCacheName("myCache1"); + cacheFb.setCacheEntryFactory(new UpdatingCacheEntryFactory() { + @Override + public Object createEntry(Object key) { + return key; + } + @Override + public void updateEntryValue(Object key, Object value) { + } + }); + assertThat(UpdatingSelfPopulatingCache.class).isEqualTo(cacheFb.getObjectType()); + cacheFb.afterPropertiesSet(); + Ehcache myCache1 = cm.getEhcache("myCache1"); + boolean condition = myCache1 instanceof UpdatingSelfPopulatingCache; + assertThat(condition).isTrue(); + assertThat(myCache1.get("myKey1").getObjectValue()).isEqualTo("myKey1"); + } + finally { + cacheManagerFb.destroy(); + } + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/AbstractJCacheTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/AbstractJCacheTests.java new file mode 100644 index 0000000..0d7df53 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/AbstractJCacheTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInfo; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.concurrent.ConcurrentMapCache; +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.cache.interceptor.SimpleCacheResolver; +import org.springframework.cache.interceptor.SimpleKeyGenerator; +import org.springframework.cache.jcache.interceptor.SimpleExceptionCacheResolver; +import org.springframework.cache.support.SimpleCacheManager; + +/** + * @author Stephane Nicoll + * @author Sam Brannen + */ +public abstract class AbstractJCacheTests { + + protected String cacheName; + + + @BeforeEach + void trackCacheName(TestInfo testInfo) { + this.cacheName = testInfo.getTestMethod().get().getName(); + } + + + protected final CacheManager cacheManager = createSimpleCacheManager("default", "simpleCache"); + + protected final CacheResolver defaultCacheResolver = new SimpleCacheResolver(cacheManager); + + protected final CacheResolver defaultExceptionCacheResolver = new SimpleExceptionCacheResolver(cacheManager); + + protected final KeyGenerator defaultKeyGenerator = new SimpleKeyGenerator(); + + protected static CacheManager createSimpleCacheManager(String... cacheNames) { + SimpleCacheManager result = new SimpleCacheManager(); + List caches = new ArrayList<>(); + for (String cacheName : cacheNames) { + caches.add(new ConcurrentMapCache(cacheName)); + } + result.setCaches(caches); + result.afterPropertiesSet(); + return result; + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheCacheManagerTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheCacheManagerTests.java new file mode 100644 index 0000000..1184942 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheCacheManagerTests.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache; + +import java.util.ArrayList; +import java.util.List; + +import javax.cache.Cache; +import javax.cache.CacheManager; + +import org.junit.jupiter.api.BeforeEach; + +import org.springframework.cache.transaction.AbstractTransactionSupportingCacheManagerTests; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * @author Stephane Nicoll + */ +public class JCacheCacheManagerTests extends AbstractTransactionSupportingCacheManagerTests { + + private CacheManagerMock cacheManagerMock; + + private JCacheCacheManager cacheManager; + + private JCacheCacheManager transactionalCacheManager; + + + @BeforeEach + public void setupOnce() { + cacheManagerMock = new CacheManagerMock(); + cacheManagerMock.addCache(CACHE_NAME); + + cacheManager = new JCacheCacheManager(cacheManagerMock.getCacheManager()); + cacheManager.setTransactionAware(false); + cacheManager.afterPropertiesSet(); + + transactionalCacheManager = new JCacheCacheManager(cacheManagerMock.getCacheManager()); + transactionalCacheManager.setTransactionAware(true); + transactionalCacheManager.afterPropertiesSet(); + } + + + @Override + protected JCacheCacheManager getCacheManager(boolean transactionAware) { + if (transactionAware) { + return transactionalCacheManager; + } + else { + return cacheManager; + } + } + + @Override + protected Class getCacheType() { + return JCacheCache.class; + } + + @Override + protected void addNativeCache(String cacheName) { + cacheManagerMock.addCache(cacheName); + } + + @Override + protected void removeNativeCache(String cacheName) { + cacheManagerMock.removeCache(cacheName); + } + + + private static class CacheManagerMock { + + private final List cacheNames; + + private final CacheManager cacheManager; + + private CacheManagerMock() { + this.cacheNames = new ArrayList<>(); + this.cacheManager = mock(CacheManager.class); + given(cacheManager.getCacheNames()).willReturn(cacheNames); + } + + private CacheManager getCacheManager() { + return cacheManager; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void addCache(String name) { + cacheNames.add(name); + Cache cache = mock(Cache.class); + given(cache.getName()).willReturn(name); + given(cacheManager.getCache(name)).willReturn(cache); + } + + public void removeCache(String name) { + cacheNames.remove(name); + given(cacheManager.getCache(name)).willReturn(null); + } + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCache3AnnotationTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCache3AnnotationTests.java new file mode 100644 index 0000000..105e4e6 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCache3AnnotationTests.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache; + +import javax.cache.Caching; +import javax.cache.spi.CachingProvider; + +/** + * Just here to be run against EHCache 3, whereas the original JCacheEhCacheAnnotationTests + * runs against EhCache 2.x with the EhCache-JCache add-on. + * + * @author Juergen Hoeller + */ +public class JCacheEhCache3AnnotationTests extends JCacheEhCacheAnnotationTests { + + @Override + protected CachingProvider getCachingProvider() { + return Caching.getCachingProvider("org.ehcache.jsr107.EhcacheCachingProvider"); + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCache3ApiTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCache3ApiTests.java new file mode 100644 index 0000000..32a2585 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCache3ApiTests.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache; + +import javax.cache.Caching; +import javax.cache.spi.CachingProvider; + +/** + * Just here to be run against EHCache 3, whereas the original JCacheEhCacheAnnotationTests + * runs against EhCache 2.x with the EhCache-JCache add-on. + * + * @author Stephane Nicoll + */ +public class JCacheEhCache3ApiTests extends JCacheEhCacheApiTests { + + @Override + protected CachingProvider getCachingProvider() { + return Caching.getCachingProvider("org.ehcache.jsr107.EhcacheCachingProvider"); + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheAnnotationTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheAnnotationTests.java new file mode 100644 index 0000000..188cc29 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheAnnotationTests.java @@ -0,0 +1,153 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache; + +import javax.cache.CacheManager; +import javax.cache.Caching; +import javax.cache.configuration.MutableConfiguration; +import javax.cache.spi.CachingProvider; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.cache.interceptor.SimpleKeyGenerator; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.testfixture.cache.AbstractCacheAnnotationTests; +import org.springframework.context.testfixture.cache.SomeCustomKeyGenerator; +import org.springframework.context.testfixture.cache.beans.AnnotatedClassCacheableService; +import org.springframework.context.testfixture.cache.beans.CacheableService; +import org.springframework.context.testfixture.cache.beans.DefaultCacheableService; +import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.transaction.testfixture.CallCountingTransactionManager; + +/** + * @author Stephane Nicoll + * @author Juergen Hoeller + */ +public class JCacheEhCacheAnnotationTests extends AbstractCacheAnnotationTests { + + private final TransactionTemplate txTemplate = new TransactionTemplate(new CallCountingTransactionManager()); + + private CacheManager jCacheManager; + + + @Override + protected ConfigurableApplicationContext getApplicationContext() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.getBeanFactory().registerSingleton("cachingProvider", getCachingProvider()); + context.register(EnableCachingConfig.class); + context.refresh(); + jCacheManager = context.getBean("jCacheManager", CacheManager.class); + return context; + } + + protected CachingProvider getCachingProvider() { + return Caching.getCachingProvider("org.ehcache.jcache.JCacheCachingProvider"); + } + + @AfterEach + public void shutdown() { + if (jCacheManager != null) { + jCacheManager.close(); + } + } + + + @Override + @Test + @Disabled("Multi cache manager support to be added") + public void testCustomCacheManager() { + } + + @Test + public void testEvictWithTransaction() { + txTemplate.executeWithoutResult(s -> testEvict(this.cs, false)); + } + + @Test + public void testEvictEarlyWithTransaction() { + txTemplate.executeWithoutResult(s -> testEvictEarly(this.cs)); + } + + @Test + public void testEvictAllWithTransaction() { + txTemplate.executeWithoutResult(s -> testEvictAll(this.cs, false)); + } + + @Test + public void testEvictAllEarlyWithTransaction() { + txTemplate.executeWithoutResult(s -> testEvictAllEarly(this.cs)); + } + + + @Configuration + @EnableCaching + static class EnableCachingConfig extends CachingConfigurerSupport { + + @Autowired + CachingProvider cachingProvider; + + @Override + @Bean + public org.springframework.cache.CacheManager cacheManager() { + JCacheCacheManager cm = new JCacheCacheManager(jCacheManager()); + cm.setTransactionAware(true); + return cm; + } + + @Bean + public CacheManager jCacheManager() { + CacheManager cacheManager = this.cachingProvider.getCacheManager(); + MutableConfiguration mutableConfiguration = new MutableConfiguration<>(); + mutableConfiguration.setStoreByValue(false); // otherwise value has to be Serializable + cacheManager.createCache("testCache", mutableConfiguration); + cacheManager.createCache("primary", mutableConfiguration); + cacheManager.createCache("secondary", mutableConfiguration); + return cacheManager; + } + + @Bean + public CacheableService service() { + return new DefaultCacheableService(); + } + + @Bean + public CacheableService classService() { + return new AnnotatedClassCacheableService(); + } + + @Override + @Bean + public KeyGenerator keyGenerator() { + return new SimpleKeyGenerator(); + } + + @Bean + public KeyGenerator customKeyGenerator() { + return new SomeCustomKeyGenerator(); + } + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheApiTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheApiTests.java new file mode 100644 index 0000000..b826e2a --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheApiTests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache; + +import javax.cache.Cache; +import javax.cache.CacheManager; +import javax.cache.Caching; +import javax.cache.configuration.MutableConfiguration; +import javax.cache.spi.CachingProvider; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +import org.springframework.context.testfixture.cache.AbstractValueAdaptingCacheTests; + +/** + * @author Stephane Nicoll + */ +public class JCacheEhCacheApiTests extends AbstractValueAdaptingCacheTests { + + private CacheManager cacheManager; + + private Cache nativeCache; + + private JCacheCache cache; + + private JCacheCache cacheNoNull; + + + @BeforeEach + public void setup() { + this.cacheManager = getCachingProvider().getCacheManager(); + this.cacheManager.createCache(CACHE_NAME, new MutableConfiguration<>()); + this.cacheManager.createCache(CACHE_NAME_NO_NULL, new MutableConfiguration<>()); + this.nativeCache = this.cacheManager.getCache(CACHE_NAME); + this.cache = new JCacheCache(this.nativeCache); + Cache nativeCacheNoNull = + this.cacheManager.getCache(CACHE_NAME_NO_NULL); + this.cacheNoNull = new JCacheCache(nativeCacheNoNull, false); + } + + protected CachingProvider getCachingProvider() { + return Caching.getCachingProvider("org.ehcache.jcache.JCacheCachingProvider"); + } + + @AfterEach + public void shutdown() { + if (this.cacheManager != null) { + this.cacheManager.close(); + } + } + + @Override + protected JCacheCache getCache() { + return getCache(true); + } + + @Override + protected JCacheCache getCache(boolean allowNull) { + return allowNull ? this.cache : this.cacheNoNull; + } + + @Override + protected Object getNativeCache() { + return this.nativeCache; + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheCustomInterceptorTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheCustomInterceptorTests.java new file mode 100644 index 0000000..6d5714d --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheCustomInterceptorTests.java @@ -0,0 +1,157 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.config; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Map; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.concurrent.ConcurrentMapCache; +import org.springframework.cache.interceptor.CacheOperationInvoker; +import org.springframework.cache.jcache.interceptor.AnnotatedJCacheableService; +import org.springframework.cache.jcache.interceptor.JCacheInterceptor; +import org.springframework.cache.jcache.interceptor.JCacheOperationSource; +import org.springframework.cache.support.SimpleCacheManager; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.contextsupport.testfixture.jcache.JCacheableService; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Stephane Nicoll + */ +public class JCacheCustomInterceptorTests { + + protected ConfigurableApplicationContext ctx; + + protected JCacheableService cs; + + protected Cache exceptionCache; + + + @BeforeEach + public void setup() { + ctx = new AnnotationConfigApplicationContext(EnableCachingConfig.class); + cs = ctx.getBean("service", JCacheableService.class); + exceptionCache = ctx.getBean("exceptionCache", Cache.class); + } + + @AfterEach + public void tearDown() { + if (ctx != null) { + ctx.close(); + } + } + + + @Test + public void onlyOneInterceptorIsAvailable() { + Map interceptors = ctx.getBeansOfType(JCacheInterceptor.class); + assertThat(interceptors.size()).as("Only one interceptor should be defined").isEqualTo(1); + JCacheInterceptor interceptor = interceptors.values().iterator().next(); + assertThat(interceptor.getClass()).as("Custom interceptor not defined").isEqualTo(TestCacheInterceptor.class); + } + + @Test + public void customInterceptorAppliesWithRuntimeException() { + Object o = cs.cacheWithException("id", true); + // See TestCacheInterceptor + assertThat(o).isEqualTo(55L); + } + + @Test + public void customInterceptorAppliesWithCheckedException() { + assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> + cs.cacheWithCheckedException("id", true)) + .withCauseExactlyInstanceOf(IOException.class); + } + + + @Configuration + @EnableCaching + static class EnableCachingConfig { + + @Bean + public CacheManager cacheManager() { + SimpleCacheManager cm = new SimpleCacheManager(); + cm.setCaches(Arrays.asList( + defaultCache(), + exceptionCache())); + return cm; + } + + @Bean + public JCacheableService service() { + return new AnnotatedJCacheableService(defaultCache()); + } + + @Bean + public Cache defaultCache() { + return new ConcurrentMapCache("default"); + } + + @Bean + public Cache exceptionCache() { + return new ConcurrentMapCache("exception"); + } + + @Bean + public JCacheInterceptor jCacheInterceptor(JCacheOperationSource cacheOperationSource) { + JCacheInterceptor cacheInterceptor = new TestCacheInterceptor(); + cacheInterceptor.setCacheOperationSource(cacheOperationSource); + return cacheInterceptor; + } + } + + + /** + * A test {@link org.springframework.cache.interceptor.CacheInterceptor} that handles special exception + * types. + */ + @SuppressWarnings("serial") + static class TestCacheInterceptor extends JCacheInterceptor { + + @Override + protected Object invokeOperation(CacheOperationInvoker invoker) { + try { + return super.invokeOperation(invoker); + } + catch (CacheOperationInvoker.ThrowableWrapper e) { + Throwable original = e.getOriginal(); + if (original.getClass() == UnsupportedOperationException.class) { + return 55L; + } + else { + throw new CacheOperationInvoker.ThrowableWrapper( + new RuntimeException("wrapping original", original)); + } + } + } + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheJavaConfigTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheJavaConfigTests.java new file mode 100644 index 0000000..5c12ace --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheJavaConfigTests.java @@ -0,0 +1,237 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.config; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.concurrent.ConcurrentMapCache; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.cache.interceptor.CacheErrorHandler; +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.cache.interceptor.NamedCacheResolver; +import org.springframework.cache.interceptor.SimpleCacheErrorHandler; +import org.springframework.cache.interceptor.SimpleCacheResolver; +import org.springframework.cache.interceptor.SimpleKeyGenerator; +import org.springframework.cache.jcache.interceptor.AnnotatedJCacheableService; +import org.springframework.cache.jcache.interceptor.DefaultJCacheOperationSource; +import org.springframework.cache.jcache.interceptor.JCacheInterceptor; +import org.springframework.cache.support.NoOpCacheManager; +import org.springframework.cache.support.SimpleCacheManager; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.testfixture.cache.SomeKeyGenerator; +import org.springframework.contextsupport.testfixture.jcache.AbstractJCacheAnnotationTests; +import org.springframework.contextsupport.testfixture.jcache.JCacheableService; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * @author Stephane Nicoll + */ +public class JCacheJavaConfigTests extends AbstractJCacheAnnotationTests { + + @Override + protected ApplicationContext getApplicationContext() { + return new AnnotationConfigApplicationContext(EnableCachingConfig.class); + } + + + @Test + public void fullCachingConfig() throws Exception { + AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(FullCachingConfig.class); + + DefaultJCacheOperationSource cos = context.getBean(DefaultJCacheOperationSource.class); + assertThat(cos.getKeyGenerator()).isSameAs(context.getBean(KeyGenerator.class)); + assertThat(cos.getCacheResolver()).isSameAs(context.getBean("cacheResolver", CacheResolver.class)); + assertThat(cos.getExceptionCacheResolver()).isSameAs(context.getBean("exceptionCacheResolver", CacheResolver.class)); + JCacheInterceptor interceptor = context.getBean(JCacheInterceptor.class); + assertThat(interceptor.getErrorHandler()).isSameAs(context.getBean("errorHandler", CacheErrorHandler.class)); + context.close(); + } + + @Test + public void emptyConfigSupport() { + ConfigurableApplicationContext context = + new AnnotationConfigApplicationContext(EmptyConfigSupportConfig.class); + + DefaultJCacheOperationSource cos = context.getBean(DefaultJCacheOperationSource.class); + assertThat(cos.getCacheResolver()).isNotNull(); + assertThat(cos.getCacheResolver().getClass()).isEqualTo(SimpleCacheResolver.class); + assertThat(((SimpleCacheResolver) cos.getCacheResolver()).getCacheManager()).isSameAs(context.getBean(CacheManager.class)); + assertThat(cos.getExceptionCacheResolver()).isNull(); + context.close(); + } + + @Test + public void bothSetOnlyResolverIsUsed() { + ConfigurableApplicationContext context = + new AnnotationConfigApplicationContext(FullCachingConfigSupport.class); + + DefaultJCacheOperationSource cos = context.getBean(DefaultJCacheOperationSource.class); + assertThat(cos.getCacheResolver()).isSameAs(context.getBean("cacheResolver")); + assertThat(cos.getKeyGenerator()).isSameAs(context.getBean("keyGenerator")); + assertThat(cos.getExceptionCacheResolver()).isSameAs(context.getBean("exceptionCacheResolver")); + context.close(); + } + + @Test + public void exceptionCacheResolverLazilyRequired() { + try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(NoExceptionCacheResolverConfig.class)) { + DefaultJCacheOperationSource cos = context.getBean(DefaultJCacheOperationSource.class); + assertThat(cos.getCacheResolver()).isSameAs(context.getBean("cacheResolver")); + + JCacheableService service = context.getBean(JCacheableService.class); + service.cache("id"); + + // This call requires the cache manager to be set + assertThatIllegalStateException().isThrownBy(() -> + service.cacheWithException("test", false)); + } + } + + + @Configuration + @EnableCaching + public static class EnableCachingConfig { + + @Bean + public CacheManager cacheManager() { + SimpleCacheManager cm = new SimpleCacheManager(); + cm.setCaches(Arrays.asList( + defaultCache(), + new ConcurrentMapCache("primary"), + new ConcurrentMapCache("secondary"), + new ConcurrentMapCache("exception"))); + return cm; + } + + @Bean + public JCacheableService cacheableService() { + return new AnnotatedJCacheableService(defaultCache()); + } + + @Bean + public Cache defaultCache() { + return new ConcurrentMapCache("default"); + } + } + + + @Configuration + @EnableCaching + public static class FullCachingConfig implements JCacheConfigurer { + + @Override + @Bean + public CacheManager cacheManager() { + return new NoOpCacheManager(); + } + + @Override + @Bean + public KeyGenerator keyGenerator() { + return new SimpleKeyGenerator(); + } + + @Override + @Bean + public CacheErrorHandler errorHandler() { + return new SimpleCacheErrorHandler(); + } + + @Override + @Bean + public CacheResolver cacheResolver() { + return new SimpleCacheResolver(cacheManager()); + } + + @Override + @Bean + public CacheResolver exceptionCacheResolver() { + return new SimpleCacheResolver(cacheManager()); + } + } + + + @Configuration + @EnableCaching + public static class EmptyConfigSupportConfig extends JCacheConfigurerSupport { + @Bean + public CacheManager cm() { + return new NoOpCacheManager(); + } + } + + + @Configuration + @EnableCaching + static class FullCachingConfigSupport extends JCacheConfigurerSupport { + + @Override + @Bean + public CacheManager cacheManager() { + return new NoOpCacheManager(); + } + + @Override + @Bean + public KeyGenerator keyGenerator() { + return new SomeKeyGenerator(); + } + + @Override + @Bean + public CacheResolver cacheResolver() { + return new NamedCacheResolver(cacheManager(), "foo"); + } + + @Override + @Bean + public CacheResolver exceptionCacheResolver() { + return new NamedCacheResolver(cacheManager(), "exception"); + } + } + + + @Configuration + @EnableCaching + static class NoExceptionCacheResolverConfig extends JCacheConfigurerSupport { + + @Override + @Bean + public CacheResolver cacheResolver() { + return new NamedCacheResolver(new ConcurrentMapCacheManager(), "default"); + } + + @Bean + public JCacheableService cacheableService() { + return new AnnotatedJCacheableService(new ConcurrentMapCache("default")); + } + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheNamespaceDrivenTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheNamespaceDrivenTests.java new file mode 100644 index 0000000..262689f --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheNamespaceDrivenTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.config; + +import org.junit.jupiter.api.Test; + +import org.springframework.cache.interceptor.CacheErrorHandler; +import org.springframework.cache.jcache.interceptor.DefaultJCacheOperationSource; +import org.springframework.cache.jcache.interceptor.JCacheInterceptor; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.GenericXmlApplicationContext; +import org.springframework.contextsupport.testfixture.jcache.AbstractJCacheAnnotationTests; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Stephane Nicoll + */ +public class JCacheNamespaceDrivenTests extends AbstractJCacheAnnotationTests { + + @Override + protected ApplicationContext getApplicationContext() { + return new GenericXmlApplicationContext( + "/org/springframework/cache/jcache/config/jCacheNamespaceDriven.xml"); + } + + @Test + public void cacheResolver() { + ConfigurableApplicationContext context = new GenericXmlApplicationContext( + "/org/springframework/cache/jcache/config/jCacheNamespaceDriven-resolver.xml"); + + DefaultJCacheOperationSource ci = context.getBean(DefaultJCacheOperationSource.class); + assertThat(ci.getCacheResolver()).isSameAs(context.getBean("cacheResolver")); + context.close(); + } + + @Test + public void testCacheErrorHandler() { + JCacheInterceptor ci = ctx.getBean(JCacheInterceptor.class); + assertThat(ci.getErrorHandler()).isSameAs(ctx.getBean("errorHandler", CacheErrorHandler.class)); + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheStandaloneConfigTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheStandaloneConfigTests.java new file mode 100644 index 0000000..f07e521 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheStandaloneConfigTests.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.config; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.GenericXmlApplicationContext; +import org.springframework.contextsupport.testfixture.jcache.AbstractJCacheAnnotationTests; + +/** + * @author Stephane Nicoll + */ +public class JCacheStandaloneConfigTests extends AbstractJCacheAnnotationTests { + + @Override + protected ApplicationContext getApplicationContext() { + return new GenericXmlApplicationContext( + "/org/springframework/cache/jcache/config/jCacheStandaloneConfig.xml"); + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AbstractCacheOperationTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AbstractCacheOperationTests.java new file mode 100644 index 0000000..c7a0cfc --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AbstractCacheOperationTests.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +import javax.cache.annotation.CacheInvocationParameter; +import javax.cache.annotation.CacheMethodDetails; + +import org.junit.jupiter.api.Test; + +import org.springframework.cache.jcache.AbstractJCacheTests; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Stephane Nicoll + */ +public abstract class AbstractCacheOperationTests> extends AbstractJCacheTests { + + protected final SampleObject sampleInstance = new SampleObject(); + + protected abstract O createSimpleOperation(); + + + @Test + public void simple() { + O operation = createSimpleOperation(); + assertThat(operation.getCacheName()).as("Wrong cache name").isEqualTo("simpleCache"); + assertThat(operation.getAnnotations().size()).as("Unexpected number of annotation on " + operation.getMethod()).isEqualTo(1); + assertThat(operation.getAnnotations().iterator().next()).as("Wrong method annotation").isEqualTo(operation.getCacheAnnotation()); + + assertThat(operation.getCacheResolver()).as("cache resolver should be set").isNotNull(); + } + + protected void assertCacheInvocationParameter(CacheInvocationParameter actual, Class targetType, + Object value, int position) { + assertThat(actual.getRawType()).as("wrong parameter type for " + actual).isEqualTo(targetType); + assertThat(actual.getValue()).as("wrong parameter value for " + actual).isEqualTo(value); + assertThat(actual.getParameterPosition()).as("wrong parameter position for " + actual).isEqualTo(position); + } + + protected CacheMethodDetails create(Class annotationType, + Class targetType, String methodName, + Class... parameterTypes) { + Method method = ReflectionUtils.findMethod(targetType, methodName, parameterTypes); + Assert.notNull(method, "requested method '" + methodName + "'does not exist"); + A cacheAnnotation = method.getAnnotation(annotationType); + return new DefaultCacheMethodDetails<>(method, cacheAnnotation, getCacheName(cacheAnnotation)); + } + + private static String getCacheName(Annotation annotation) { + Object cacheName = AnnotationUtils.getValue(annotation, "cacheName"); + return cacheName != null ? cacheName.toString() : "test"; + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AnnotatedJCacheableService.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AnnotatedJCacheableService.java new file mode 100644 index 0000000..e5cfb12 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AnnotatedJCacheableService.java @@ -0,0 +1,219 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import java.io.IOException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +import javax.cache.annotation.CacheDefaults; +import javax.cache.annotation.CacheKey; +import javax.cache.annotation.CachePut; +import javax.cache.annotation.CacheRemove; +import javax.cache.annotation.CacheRemoveAll; +import javax.cache.annotation.CacheResult; +import javax.cache.annotation.CacheValue; + +import org.springframework.cache.Cache; +import org.springframework.cache.interceptor.SimpleKeyGenerator; +import org.springframework.contextsupport.testfixture.cache.TestableCacheKeyGenerator; +import org.springframework.contextsupport.testfixture.cache.TestableCacheResolverFactory; +import org.springframework.contextsupport.testfixture.jcache.JCacheableService; + +/** + * Repository sample with a @CacheDefaults annotation + * + * @author Stephane Nicoll + */ +@CacheDefaults(cacheName = "default") +public class AnnotatedJCacheableService implements JCacheableService { + + private final AtomicLong counter = new AtomicLong(); + + private final AtomicLong exceptionCounter = new AtomicLong(); + + private final Cache defaultCache; + + public AnnotatedJCacheableService(Cache defaultCache) { + this.defaultCache = defaultCache; + } + + @Override + @CacheResult + public Long cache(String id) { + return counter.getAndIncrement(); + } + + @Override + @CacheResult + public Long cacheNull(String id) { + return null; + } + + @Override + @CacheResult(exceptionCacheName = "exception", nonCachedExceptions = NullPointerException.class) + public Long cacheWithException(@CacheKey String id, boolean matchFilter) { + throwException(matchFilter); + return 0L; // Never reached + } + + @Override + @CacheResult(exceptionCacheName = "exception", nonCachedExceptions = NullPointerException.class) + public Long cacheWithCheckedException(@CacheKey String id, boolean matchFilter) throws IOException { + throwCheckedException(matchFilter); + return 0L; // Never reached + } + + @Override + @CacheResult(skipGet = true) + public Long cacheAlwaysInvoke(String id) { + return counter.getAndIncrement(); + } + + @Override + @CacheResult + public Long cacheWithPartialKey(@CacheKey String id, boolean notUsed) { + return counter.getAndIncrement(); + } + + @Override + @CacheResult(cacheResolverFactory = TestableCacheResolverFactory.class) + public Long cacheWithCustomCacheResolver(String id) { + return counter.getAndIncrement(); + } + + @Override + @CacheResult(cacheKeyGenerator = TestableCacheKeyGenerator.class) + public Long cacheWithCustomKeyGenerator(String id, String anotherId) { + return counter.getAndIncrement(); + } + + @Override + @CachePut + public void put(String id, @CacheValue Object value) { + } + + @Override + @CachePut(cacheFor = UnsupportedOperationException.class) + public void putWithException(@CacheKey String id, @CacheValue Object value, boolean matchFilter) { + throwException(matchFilter); + } + + @Override + @CachePut(afterInvocation = false) + public void earlyPut(String id, @CacheValue Object value) { + Object key = SimpleKeyGenerator.generateKey(id); + Cache.ValueWrapper valueWrapper = defaultCache.get(key); + if (valueWrapper == null) { + throw new AssertionError("Excepted value to be put in cache with key " + key); + } + Object actual = valueWrapper.get(); + if (value != actual) { // instance check on purpose + throw new AssertionError("Wrong value set in cache with key " + key + ". " + + "Expected=" + value + ", but got=" + actual); + } + } + + @Override + @CachePut(afterInvocation = false) + public void earlyPutWithException(@CacheKey String id, @CacheValue Object value, boolean matchFilter) { + throwException(matchFilter); + } + + @Override + @CacheRemove + public void remove(String id) { + } + + @Override + @CacheRemove(noEvictFor = NullPointerException.class) + public void removeWithException(@CacheKey String id, boolean matchFilter) { + throwException(matchFilter); + } + + @Override + @CacheRemove(afterInvocation = false) + public void earlyRemove(String id) { + Object key = SimpleKeyGenerator.generateKey(id); + Cache.ValueWrapper valueWrapper = defaultCache.get(key); + if (valueWrapper != null) { + throw new AssertionError("Value with key " + key + " expected to be already remove from cache"); + } + } + + @Override + @CacheRemove(afterInvocation = false, evictFor = UnsupportedOperationException.class) + public void earlyRemoveWithException(@CacheKey String id, boolean matchFilter) { + throwException(matchFilter); + } + + @Override + @CacheRemoveAll + public void removeAll() { + } + + @Override + @CacheRemoveAll(noEvictFor = NullPointerException.class) + public void removeAllWithException(boolean matchFilter) { + throwException(matchFilter); + } + + @Override + @CacheRemoveAll(afterInvocation = false) + public void earlyRemoveAll() { + ConcurrentHashMap nativeCache = (ConcurrentHashMap) defaultCache.getNativeCache(); + if (!nativeCache.isEmpty()) { + throw new AssertionError("Cache was expected to be empty"); + } + } + + @Override + @CacheRemoveAll(afterInvocation = false, evictFor = UnsupportedOperationException.class) + public void earlyRemoveAllWithException(boolean matchFilter) { + throwException(matchFilter); + } + + @Deprecated + public void noAnnotation() { + } + + @Override + public long exceptionInvocations() { + return exceptionCounter.get(); + } + + private void throwException(boolean matchFilter) { + long count = exceptionCounter.getAndIncrement(); + if (matchFilter) { + throw new UnsupportedOperationException("Expected exception (" + count + ")"); + } + else { + throw new NullPointerException("Expected exception (" + count + ")"); + } + } + + private void throwCheckedException(boolean matchFilter) throws IOException { + long count = exceptionCounter.getAndIncrement(); + if (matchFilter) { + throw new IOException("Expected exception (" + count + ")"); + } + else { + throw new NullPointerException("Expected exception (" + count + ")"); + } + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AnnotationCacheOperationSourceTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AnnotationCacheOperationSourceTests.java new file mode 100644 index 0000000..bcc9276 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AnnotationCacheOperationSourceTests.java @@ -0,0 +1,272 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import java.lang.reflect.Method; +import java.util.Comparator; + +import javax.cache.annotation.CacheDefaults; +import javax.cache.annotation.CacheKeyGenerator; +import javax.cache.annotation.CacheRemove; +import javax.cache.annotation.CacheRemoveAll; +import javax.cache.annotation.CacheResult; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.cache.jcache.AbstractJCacheTests; +import org.springframework.contextsupport.testfixture.cache.TestableCacheKeyGenerator; +import org.springframework.contextsupport.testfixture.cache.TestableCacheResolver; +import org.springframework.contextsupport.testfixture.cache.TestableCacheResolverFactory; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * @author Stephane Nicoll + */ +public class AnnotationCacheOperationSourceTests extends AbstractJCacheTests { + + private final DefaultJCacheOperationSource source = new DefaultJCacheOperationSource(); + + private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + + @BeforeEach + public void setup() { + source.setCacheResolver(defaultCacheResolver); + source.setExceptionCacheResolver(defaultExceptionCacheResolver); + source.setKeyGenerator(defaultKeyGenerator); + source.setBeanFactory(beanFactory); + } + + + @Test + public void cache() { + CacheResultOperation op = getDefaultCacheOperation(CacheResultOperation.class, String.class); + assertDefaults(op); + assertThat(op.getExceptionCacheResolver()).as("Exception caching not enabled so resolver should not be set").isNull(); + } + + @Test + public void cacheWithException() { + CacheResultOperation op = getDefaultCacheOperation(CacheResultOperation.class, String.class, boolean.class); + assertDefaults(op); + assertThat(op.getExceptionCacheResolver()).isEqualTo(defaultExceptionCacheResolver); + assertThat(op.getExceptionCacheName()).isEqualTo("exception"); + } + + @Test + public void put() { + CachePutOperation op = getDefaultCacheOperation(CachePutOperation.class, String.class, Object.class); + assertDefaults(op); + } + + @Test + public void remove() { + CacheRemoveOperation op = getDefaultCacheOperation(CacheRemoveOperation.class, String.class); + assertDefaults(op); + } + + @Test + public void removeAll() { + CacheRemoveAllOperation op = getDefaultCacheOperation(CacheRemoveAllOperation.class); + assertThat(op.getCacheResolver()).isEqualTo(defaultCacheResolver); + } + + @Test + public void noAnnotation() { + assertThat(getCacheOperation(AnnotatedJCacheableService.class, this.cacheName)).isNull(); + } + + @Test + public void multiAnnotations() { + assertThatIllegalStateException().isThrownBy(() -> getCacheOperation(InvalidCases.class, this.cacheName)); + } + + @Test + public void defaultCacheNameWithCandidate() { + Method method = ReflectionUtils.findMethod(Object.class, "toString"); + assertThat(source.determineCacheName(method, null, "foo")).isEqualTo("foo"); + } + + @Test + public void defaultCacheNameWithDefaults() { + Method method = ReflectionUtils.findMethod(Object.class, "toString"); + CacheDefaults mock = mock(CacheDefaults.class); + given(mock.cacheName()).willReturn(""); + assertThat(source.determineCacheName(method, mock, "")).isEqualTo("java.lang.Object.toString()"); + } + + @Test + public void defaultCacheNameNoDefaults() { + Method method = ReflectionUtils.findMethod(Object.class, "toString"); + assertThat(source.determineCacheName(method, null, "")).isEqualTo("java.lang.Object.toString()"); + } + + @Test + public void defaultCacheNameWithParameters() { + Method method = ReflectionUtils.findMethod(Comparator.class, "compare", Object.class, Object.class); + assertThat(source.determineCacheName(method, null, "")).isEqualTo("java.util.Comparator.compare(java.lang.Object,java.lang.Object)"); + } + + @Test + public void customCacheResolver() { + CacheResultOperation operation = + getCacheOperation(CacheResultOperation.class, CustomService.class, this.cacheName, Long.class); + assertJCacheResolver(operation.getCacheResolver(), TestableCacheResolver.class); + assertJCacheResolver(operation.getExceptionCacheResolver(), null); + assertThat(operation.getKeyGenerator().getClass()).isEqualTo(KeyGeneratorAdapter.class); + assertThat(((KeyGeneratorAdapter) operation.getKeyGenerator()).getTarget()).isEqualTo(defaultKeyGenerator); + } + + @Test + public void customKeyGenerator() { + CacheResultOperation operation = + getCacheOperation(CacheResultOperation.class, CustomService.class, this.cacheName, Long.class); + assertThat(operation.getCacheResolver()).isEqualTo(defaultCacheResolver); + assertThat(operation.getExceptionCacheResolver()).isNull(); + assertCacheKeyGenerator(operation.getKeyGenerator(), TestableCacheKeyGenerator.class); + } + + @Test + public void customKeyGeneratorSpringBean() { + TestableCacheKeyGenerator bean = new TestableCacheKeyGenerator(); + beanFactory.registerSingleton("fooBar", bean); + CacheResultOperation operation = + getCacheOperation(CacheResultOperation.class, CustomService.class, this.cacheName, Long.class); + assertThat(operation.getCacheResolver()).isEqualTo(defaultCacheResolver); + assertThat(operation.getExceptionCacheResolver()).isNull(); + KeyGeneratorAdapter adapter = (KeyGeneratorAdapter) operation.getKeyGenerator(); + // take bean from context + assertThat(adapter.getTarget()).isSameAs(bean); + } + + @Test + public void customKeyGeneratorAndCacheResolver() { + CacheResultOperation operation = getCacheOperation(CacheResultOperation.class, + CustomServiceWithDefaults.class, this.cacheName, Long.class); + assertJCacheResolver(operation.getCacheResolver(), TestableCacheResolver.class); + assertJCacheResolver(operation.getExceptionCacheResolver(), null); + assertCacheKeyGenerator(operation.getKeyGenerator(), TestableCacheKeyGenerator.class); + } + + @Test + public void customKeyGeneratorAndCacheResolverWithExceptionName() { + CacheResultOperation operation = getCacheOperation(CacheResultOperation.class, + CustomServiceWithDefaults.class, this.cacheName, Long.class); + assertJCacheResolver(operation.getCacheResolver(), TestableCacheResolver.class); + assertJCacheResolver(operation.getExceptionCacheResolver(), TestableCacheResolver.class); + assertCacheKeyGenerator(operation.getKeyGenerator(), TestableCacheKeyGenerator.class); + } + + private void assertDefaults(AbstractJCacheKeyOperation operation) { + assertThat(operation.getCacheResolver()).isEqualTo(defaultCacheResolver); + assertThat(operation.getKeyGenerator().getClass()).isEqualTo(KeyGeneratorAdapter.class); + assertThat(((KeyGeneratorAdapter) operation.getKeyGenerator()).getTarget()).isEqualTo(defaultKeyGenerator); + } + + protected > T getDefaultCacheOperation(Class operationType, Class... parameterTypes) { + return getCacheOperation(operationType, AnnotatedJCacheableService.class, this.cacheName, parameterTypes); + } + + protected > T getCacheOperation( + Class operationType, Class targetType, String methodName, Class... parameterTypes) { + + JCacheOperation result = getCacheOperation(targetType, methodName, parameterTypes); + assertThat(result).isNotNull(); + assertThat(result.getClass()).isEqualTo(operationType); + return operationType.cast(result); + } + + private JCacheOperation getCacheOperation(Class targetType, String methodName, Class... parameterTypes) { + Method method = ReflectionUtils.findMethod(targetType, methodName, parameterTypes); + Assert.notNull(method, "requested method '" + methodName + "'does not exist"); + return source.getCacheOperation(method, targetType); + } + + private void assertJCacheResolver(CacheResolver actual, + Class expectedTargetType) { + + if (expectedTargetType == null) { + assertThat(actual).isNull(); + } + else { + assertThat(actual.getClass()).as("Wrong cache resolver implementation").isEqualTo(CacheResolverAdapter.class); + CacheResolverAdapter adapter = (CacheResolverAdapter) actual; + assertThat(adapter.getTarget().getClass()).as("Wrong target JCache implementation").isEqualTo(expectedTargetType); + } + } + + private void assertCacheKeyGenerator(KeyGenerator actual, + Class expectedTargetType) { + assertThat(actual.getClass()).as("Wrong cache resolver implementation").isEqualTo(KeyGeneratorAdapter.class); + KeyGeneratorAdapter adapter = (KeyGeneratorAdapter) actual; + assertThat(adapter.getTarget().getClass()).as("Wrong target CacheKeyGenerator implementation").isEqualTo(expectedTargetType); + } + + + static class CustomService { + + @CacheResult(cacheKeyGenerator = TestableCacheKeyGenerator.class) + public Object customKeyGenerator(Long id) { + return null; + } + + @CacheResult(cacheKeyGenerator = TestableCacheKeyGenerator.class) + public Object customKeyGeneratorSpringBean(Long id) { + return null; + } + + @CacheResult(cacheResolverFactory = TestableCacheResolverFactory.class) + public Object customCacheResolver(Long id) { + return null; + } + } + + + @CacheDefaults(cacheResolverFactory = TestableCacheResolverFactory.class, cacheKeyGenerator = TestableCacheKeyGenerator.class) + static class CustomServiceWithDefaults { + + @CacheResult + public Object customKeyGeneratorAndCacheResolver(Long id) { + return null; + } + + @CacheResult(exceptionCacheName = "exception") + public Object customKeyGeneratorAndCacheResolverWithExceptionName(Long id) { + return null; + } + } + + + static class InvalidCases { + + @CacheRemove + @CacheRemoveAll + public void multiAnnotations() { + } + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CachePutOperationTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CachePutOperationTests.java new file mode 100644 index 0000000..534094c --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CachePutOperationTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import java.io.IOException; + +import javax.cache.annotation.CacheInvocationParameter; +import javax.cache.annotation.CacheMethodDetails; +import javax.cache.annotation.CachePut; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * @author Stephane Nicoll + */ +public class CachePutOperationTests extends AbstractCacheOperationTests { + + @Override + protected CachePutOperation createSimpleOperation() { + CacheMethodDetails methodDetails = create(CachePut.class, + SampleObject.class, "simplePut", Long.class, SampleObject.class); + return createDefaultOperation(methodDetails); + } + + @Test + public void simplePut() { + CachePutOperation operation = createSimpleOperation(); + + CacheInvocationParameter[] allParameters = operation.getAllParameters(2L, sampleInstance); + assertThat(allParameters.length).isEqualTo(2); + assertCacheInvocationParameter(allParameters[0], Long.class, 2L, 0); + assertCacheInvocationParameter(allParameters[1], SampleObject.class, sampleInstance, 1); + + CacheInvocationParameter valueParameter = operation.getValueParameter(2L, sampleInstance); + assertThat(valueParameter).isNotNull(); + assertCacheInvocationParameter(valueParameter, SampleObject.class, sampleInstance, 1); + } + + @Test + public void noCacheValue() { + CacheMethodDetails methodDetails = create(CachePut.class, + SampleObject.class, "noCacheValue", Long.class); + + assertThatIllegalArgumentException().isThrownBy(() -> + createDefaultOperation(methodDetails)); + } + + @Test + public void multiCacheValues() { + CacheMethodDetails methodDetails = create(CachePut.class, + SampleObject.class, "multiCacheValues", Long.class, SampleObject.class, SampleObject.class); + + assertThatIllegalArgumentException().isThrownBy(() -> + createDefaultOperation(methodDetails)); + } + + @Test + public void invokeWithWrongParameters() { + CachePutOperation operation = createSimpleOperation(); + + assertThatIllegalStateException().isThrownBy(() -> + operation.getValueParameter(2L)); + } + + @Test + public void fullPutConfig() { + CacheMethodDetails methodDetails = create(CachePut.class, + SampleObject.class, "fullPutConfig", Long.class, SampleObject.class); + CachePutOperation operation = createDefaultOperation(methodDetails); + assertThat(operation.isEarlyPut()).isTrue(); + assertThat(operation.getExceptionTypeFilter()).isNotNull(); + assertThat(operation.getExceptionTypeFilter().match(IOException.class)).isTrue(); + assertThat(operation.getExceptionTypeFilter().match(NullPointerException.class)).isFalse(); + } + + private CachePutOperation createDefaultOperation(CacheMethodDetails methodDetails) { + return new CachePutOperation(methodDetails, defaultCacheResolver, defaultKeyGenerator); + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllOperationTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllOperationTests.java new file mode 100644 index 0000000..7327a40 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllOperationTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import javax.cache.annotation.CacheInvocationParameter; +import javax.cache.annotation.CacheMethodDetails; +import javax.cache.annotation.CacheRemoveAll; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Stephane Nicoll + */ +public class CacheRemoveAllOperationTests extends AbstractCacheOperationTests { + + @Override + protected CacheRemoveAllOperation createSimpleOperation() { + CacheMethodDetails methodDetails = create(CacheRemoveAll.class, + SampleObject.class, "simpleRemoveAll"); + + return new CacheRemoveAllOperation(methodDetails, defaultCacheResolver); + } + + @Test + public void simpleRemoveAll() { + CacheRemoveAllOperation operation = createSimpleOperation(); + + CacheInvocationParameter[] allParameters = operation.getAllParameters(); + assertThat(allParameters.length).isEqualTo(0); + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheRemoveOperationTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheRemoveOperationTests.java new file mode 100644 index 0000000..b8ae959 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheRemoveOperationTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import javax.cache.annotation.CacheInvocationParameter; +import javax.cache.annotation.CacheMethodDetails; +import javax.cache.annotation.CacheRemove; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Stephane Nicoll + */ +public class CacheRemoveOperationTests extends AbstractCacheOperationTests { + + @Override + protected CacheRemoveOperation createSimpleOperation() { + CacheMethodDetails methodDetails = create(CacheRemove.class, + SampleObject.class, "simpleRemove", Long.class); + + return new CacheRemoveOperation(methodDetails, defaultCacheResolver, defaultKeyGenerator); + } + + @Test + public void simpleRemove() { + CacheRemoveOperation operation = createSimpleOperation(); + + CacheInvocationParameter[] allParameters = operation.getAllParameters(2L); + assertThat(allParameters.length).isEqualTo(1); + assertCacheInvocationParameter(allParameters[0], Long.class, 2L, 0); + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheResolverAdapterTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheResolverAdapterTests.java new file mode 100644 index 0000000..1b1e914 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheResolverAdapterTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Collection; + +import javax.cache.annotation.CacheInvocationContext; +import javax.cache.annotation.CacheMethodDetails; +import javax.cache.annotation.CacheResolver; +import javax.cache.annotation.CacheResult; + +import org.junit.jupiter.api.Test; + +import org.springframework.cache.Cache; +import org.springframework.cache.jcache.AbstractJCacheTests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * @author Stephane Nicoll + */ +public class CacheResolverAdapterTests extends AbstractJCacheTests { + + @Test + public void resolveSimpleCache() throws Exception { + DefaultCacheInvocationContext dummyContext = createDummyContext(); + CacheResolverAdapter adapter = new CacheResolverAdapter(getCacheResolver(dummyContext, "testCache")); + Collection caches = adapter.resolveCaches(dummyContext); + assertThat(caches).isNotNull(); + assertThat(caches.size()).isEqualTo(1); + assertThat(caches.iterator().next().getName()).isEqualTo("testCache"); + } + + @Test + public void resolveUnknownCache() throws Exception { + DefaultCacheInvocationContext dummyContext = createDummyContext(); + CacheResolverAdapter adapter = new CacheResolverAdapter(getCacheResolver(dummyContext, null)); + + assertThatIllegalStateException().isThrownBy(() -> + adapter.resolveCaches(dummyContext)); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected CacheResolver getCacheResolver(CacheInvocationContext context, String cacheName) { + CacheResolver cacheResolver = mock(CacheResolver.class); + javax.cache.Cache cache; + if (cacheName == null) { + cache = null; + } + else { + cache = mock(javax.cache.Cache.class); + given(cache.getName()).willReturn(cacheName); + } + given(cacheResolver.resolveCache(context)).willReturn(cache); + return cacheResolver; + } + + protected DefaultCacheInvocationContext createDummyContext() throws Exception { + Method method = Sample.class.getMethod("get", String.class); + CacheResult cacheAnnotation = method.getAnnotation(CacheResult.class); + CacheMethodDetails methodDetails = + new DefaultCacheMethodDetails<>(method, cacheAnnotation, "test"); + CacheResultOperation operation = new CacheResultOperation(methodDetails, + defaultCacheResolver, defaultKeyGenerator, defaultExceptionCacheResolver); + return new DefaultCacheInvocationContext<>(operation, new Sample(), new Object[] {"id"}); + } + + + static class Sample { + + @CacheResult + public Object get(String id) { + return null; + } + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheResultOperationTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheResultOperationTests.java new file mode 100644 index 0000000..aff8f3e --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheResultOperationTests.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.util.Set; + +import javax.cache.annotation.CacheInvocationParameter; +import javax.cache.annotation.CacheKey; +import javax.cache.annotation.CacheMethodDetails; +import javax.cache.annotation.CacheResult; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Value; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * @author Stephane Nicoll + */ +public class CacheResultOperationTests extends AbstractCacheOperationTests { + + @Override + protected CacheResultOperation createSimpleOperation() { + CacheMethodDetails methodDetails = create(CacheResult.class, + SampleObject.class, "simpleGet", Long.class); + + return new CacheResultOperation(methodDetails, defaultCacheResolver, defaultKeyGenerator, + defaultExceptionCacheResolver); + } + + @Test + public void simpleGet() { + CacheResultOperation operation = createSimpleOperation(); + + assertThat(operation.getKeyGenerator()).isNotNull(); + assertThat(operation.getExceptionCacheResolver()).isNotNull(); + + assertThat(operation.getExceptionCacheName()).isNull(); + assertThat(operation.getExceptionCacheResolver()).isEqualTo(defaultExceptionCacheResolver); + + CacheInvocationParameter[] allParameters = operation.getAllParameters(2L); + assertThat(allParameters.length).isEqualTo(1); + assertCacheInvocationParameter(allParameters[0], Long.class, 2L, 0); + + CacheInvocationParameter[] keyParameters = operation.getKeyParameters(2L); + assertThat(keyParameters.length).isEqualTo(1); + assertCacheInvocationParameter(keyParameters[0], Long.class, 2L, 0); + } + + @Test + public void multiParameterKey() { + CacheMethodDetails methodDetails = create(CacheResult.class, + SampleObject.class, "multiKeysGet", Long.class, Boolean.class, String.class); + CacheResultOperation operation = createDefaultOperation(methodDetails); + + CacheInvocationParameter[] keyParameters = operation.getKeyParameters(3L, Boolean.TRUE, "Foo"); + assertThat(keyParameters.length).isEqualTo(2); + assertCacheInvocationParameter(keyParameters[0], Long.class, 3L, 0); + assertCacheInvocationParameter(keyParameters[1], String.class, "Foo", 2); + } + + @Test + public void invokeWithWrongParameters() { + CacheMethodDetails methodDetails = create(CacheResult.class, + SampleObject.class, "anotherSimpleGet", String.class, Long.class); + CacheResultOperation operation = createDefaultOperation(methodDetails); + + // missing one argument + assertThatIllegalStateException().isThrownBy(() -> + operation.getAllParameters("bar")); + } + + @Test + public void tooManyKeyValues() { + CacheMethodDetails methodDetails = create(CacheResult.class, + SampleObject.class, "anotherSimpleGet", String.class, Long.class); + CacheResultOperation operation = createDefaultOperation(methodDetails); + + // missing one argument + assertThatIllegalStateException().isThrownBy(() -> + operation.getKeyParameters("bar")); + } + + @Test + public void annotatedGet() { + CacheMethodDetails methodDetails = create(CacheResult.class, + SampleObject.class, "annotatedGet", Long.class, String.class); + CacheResultOperation operation = createDefaultOperation(methodDetails); + CacheInvocationParameter[] parameters = operation.getAllParameters(2L, "foo"); + + Set firstParameterAnnotations = parameters[0].getAnnotations(); + assertThat(firstParameterAnnotations.size()).isEqualTo(1); + assertThat(firstParameterAnnotations.iterator().next().annotationType()).isEqualTo(CacheKey.class); + + Set secondParameterAnnotations = parameters[1].getAnnotations(); + assertThat(secondParameterAnnotations.size()).isEqualTo(1); + assertThat(secondParameterAnnotations.iterator().next().annotationType()).isEqualTo(Value.class); + } + + @Test + public void fullGetConfig() { + CacheMethodDetails methodDetails = create(CacheResult.class, + SampleObject.class, "fullGetConfig", Long.class); + CacheResultOperation operation = createDefaultOperation(methodDetails); + assertThat(operation.isAlwaysInvoked()).isTrue(); + assertThat(operation.getExceptionTypeFilter()).isNotNull(); + assertThat(operation.getExceptionTypeFilter().match(IOException.class)).isTrue(); + assertThat(operation.getExceptionTypeFilter().match(NullPointerException.class)).isFalse(); + } + + private CacheResultOperation createDefaultOperation(CacheMethodDetails methodDetails) { + return new CacheResultOperation(methodDetails, + defaultCacheResolver, defaultKeyGenerator, defaultCacheResolver); + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheErrorHandlerTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheErrorHandlerTests.java new file mode 100644 index 0000000..3b341a0 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheErrorHandlerTests.java @@ -0,0 +1,211 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicLong; + +import javax.cache.annotation.CacheDefaults; +import javax.cache.annotation.CachePut; +import javax.cache.annotation.CacheRemove; +import javax.cache.annotation.CacheRemoveAll; +import javax.cache.annotation.CacheResult; +import javax.cache.annotation.CacheValue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.interceptor.CacheErrorHandler; +import org.springframework.cache.interceptor.SimpleKeyGenerator; +import org.springframework.cache.jcache.config.JCacheConfigurerSupport; +import org.springframework.cache.support.SimpleCacheManager; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author Stephane Nicoll + */ +public class JCacheErrorHandlerTests { + + private Cache cache; + + private Cache errorCache; + + private CacheErrorHandler errorHandler; + + private SimpleService simpleService; + + + @BeforeEach + public void setup() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class); + this.cache = context.getBean("mockCache", Cache.class); + this.errorCache = context.getBean("mockErrorCache", Cache.class); + this.errorHandler = context.getBean(CacheErrorHandler.class); + this.simpleService = context.getBean(SimpleService.class); + context.close(); + } + + + @Test + public void getFail() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on get"); + Object key = SimpleKeyGenerator.generateKey(0L); + willThrow(exception).given(this.cache).get(key); + + this.simpleService.get(0L); + verify(this.errorHandler).handleCacheGetError(exception, this.cache, key); + } + + @Test + public void getPutNewElementFail() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on put"); + Object key = SimpleKeyGenerator.generateKey(0L); + given(this.cache.get(key)).willReturn(null); + willThrow(exception).given(this.cache).put(key, 0L); + + this.simpleService.get(0L); + verify(this.errorHandler).handleCachePutError(exception, this.cache, key, 0L); + } + + @Test + public void getFailPutExceptionFail() { + UnsupportedOperationException exceptionOnPut = new UnsupportedOperationException("Test exception on put"); + Object key = SimpleKeyGenerator.generateKey(0L); + given(this.cache.get(key)).willReturn(null); + willThrow(exceptionOnPut).given(this.errorCache).put(key, SimpleService.TEST_EXCEPTION); + + try { + this.simpleService.getFail(0L); + } + catch (IllegalStateException ex) { + assertThat(ex.getMessage()).isEqualTo("Test exception"); + } + verify(this.errorHandler).handleCachePutError( + exceptionOnPut, this.errorCache, key, SimpleService.TEST_EXCEPTION); + } + + @Test + public void putFail() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on put"); + Object key = SimpleKeyGenerator.generateKey(0L); + willThrow(exception).given(this.cache).put(key, 234L); + + this.simpleService.put(0L, 234L); + verify(this.errorHandler).handleCachePutError(exception, this.cache, key, 234L); + } + + @Test + public void evictFail() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on evict"); + Object key = SimpleKeyGenerator.generateKey(0L); + willThrow(exception).given(this.cache).evict(key); + + this.simpleService.evict(0L); + verify(this.errorHandler).handleCacheEvictError(exception, this.cache, key); + } + + @Test + public void clearFail() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on evict"); + willThrow(exception).given(this.cache).clear(); + + this.simpleService.clear(); + verify(this.errorHandler).handleCacheClearError(exception, this.cache); + } + + + @Configuration + @EnableCaching + static class Config extends JCacheConfigurerSupport { + + @Bean + @Override + public CacheManager cacheManager() { + SimpleCacheManager cacheManager = new SimpleCacheManager(); + cacheManager.setCaches(Arrays.asList(mockCache(), mockErrorCache())); + return cacheManager; + } + + @Bean + @Override + public CacheErrorHandler errorHandler() { + return mock(CacheErrorHandler.class); + } + + @Bean + public SimpleService simpleService() { + return new SimpleService(); + } + + @Bean + public Cache mockCache() { + Cache cache = mock(Cache.class); + given(cache.getName()).willReturn("test"); + return cache; + } + + @Bean + public Cache mockErrorCache() { + Cache cache = mock(Cache.class); + given(cache.getName()).willReturn("error"); + return cache; + } + } + + + @CacheDefaults(cacheName = "test") + public static class SimpleService { + + private static final IllegalStateException TEST_EXCEPTION = new IllegalStateException("Test exception"); + + private AtomicLong counter = new AtomicLong(); + + @CacheResult + public Object get(long id) { + return this.counter.getAndIncrement(); + } + + @CacheResult(exceptionCacheName = "error") + public Object getFail(long id) { + throw TEST_EXCEPTION; + } + + @CachePut + public void put(long id, @CacheValue Object object) { + } + + @CacheRemove + public void evict(long id) { + } + + @CacheRemoveAll + public void clear() { + } + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheInterceptorTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheInterceptorTests.java new file mode 100644 index 0000000..0c6671e --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheInterceptorTests.java @@ -0,0 +1,131 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.StaticListableBeanFactory; +import org.springframework.cache.CacheManager; +import org.springframework.cache.interceptor.CacheOperationInvoker; +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.cache.interceptor.NamedCacheResolver; +import org.springframework.cache.jcache.AbstractJCacheTests; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * @author Stephane Nicoll + */ +public class JCacheInterceptorTests extends AbstractJCacheTests { + + private final CacheOperationInvoker dummyInvoker = new DummyInvoker(null); + + @Test + public void severalCachesNotSupported() { + JCacheInterceptor interceptor = createInterceptor(createOperationSource( + cacheManager, new NamedCacheResolver(cacheManager, "default", "simpleCache"), + defaultExceptionCacheResolver, defaultKeyGenerator)); + + AnnotatedJCacheableService service = new AnnotatedJCacheableService(cacheManager.getCache("default")); + Method m = ReflectionUtils.findMethod(AnnotatedJCacheableService.class, "cache", String.class); + + assertThatIllegalStateException().isThrownBy(() -> + interceptor.execute(dummyInvoker, service, m, new Object[] {"myId"})) + .withMessageContaining("JSR-107 only supports a single cache"); + } + + @Test + public void noCacheCouldBeResolved() { + JCacheInterceptor interceptor = createInterceptor(createOperationSource( + cacheManager, new NamedCacheResolver(cacheManager), // Returns empty list + defaultExceptionCacheResolver, defaultKeyGenerator)); + + AnnotatedJCacheableService service = new AnnotatedJCacheableService(cacheManager.getCache("default")); + Method m = ReflectionUtils.findMethod(AnnotatedJCacheableService.class, "cache", String.class); + assertThatIllegalStateException().isThrownBy(() -> + interceptor.execute(dummyInvoker, service, m, new Object[] {"myId"})) + .withMessageContaining("Cache could not have been resolved for"); + } + + @Test + public void cacheManagerMandatoryIfCacheResolverNotSet() { + assertThatIllegalStateException().isThrownBy(() -> + createOperationSource(null, null, null, defaultKeyGenerator)); + } + + @Test + public void cacheManagerOptionalIfCacheResolversSet() { + createOperationSource(null, defaultCacheResolver, defaultExceptionCacheResolver, defaultKeyGenerator); + } + + @Test + public void cacheResultReturnsProperType() throws Throwable { + JCacheInterceptor interceptor = createInterceptor(createOperationSource( + cacheManager, defaultCacheResolver, defaultExceptionCacheResolver, defaultKeyGenerator)); + + AnnotatedJCacheableService service = new AnnotatedJCacheableService(cacheManager.getCache("default")); + Method method = ReflectionUtils.findMethod(AnnotatedJCacheableService.class, "cache", String.class); + + CacheOperationInvoker invoker = new DummyInvoker(0L); + Object execute = interceptor.execute(invoker, service, method, new Object[] {"myId"}); + assertThat(execute).as("result cannot be null.").isNotNull(); + assertThat(execute.getClass()).as("Wrong result type").isEqualTo(Long.class); + assertThat(execute).as("Wrong result").isEqualTo(0L); + } + + protected JCacheOperationSource createOperationSource(CacheManager cacheManager, + CacheResolver cacheResolver, CacheResolver exceptionCacheResolver, KeyGenerator keyGenerator) { + + DefaultJCacheOperationSource source = new DefaultJCacheOperationSource(); + source.setCacheManager(cacheManager); + source.setCacheResolver(cacheResolver); + source.setExceptionCacheResolver(exceptionCacheResolver); + source.setKeyGenerator(keyGenerator); + source.setBeanFactory(new StaticListableBeanFactory()); + source.afterSingletonsInstantiated(); + return source; + } + + + protected JCacheInterceptor createInterceptor(JCacheOperationSource source) { + JCacheInterceptor interceptor = new JCacheInterceptor(); + interceptor.setCacheOperationSource(source); + interceptor.afterPropertiesSet(); + return interceptor; + } + + + private static class DummyInvoker implements CacheOperationInvoker { + + private final Object result; + + private DummyInvoker(Object result) { + this.result = result; + } + + @Override + public Object invoke() throws ThrowableWrapper { + return result; + } + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheKeyGeneratorTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheKeyGeneratorTests.java new file mode 100644 index 0000000..dd54977 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheKeyGeneratorTests.java @@ -0,0 +1,158 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicLong; + +import javax.cache.annotation.CacheDefaults; +import javax.cache.annotation.CacheKey; +import javax.cache.annotation.CacheResult; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.cache.interceptor.SimpleKey; +import org.springframework.cache.interceptor.SimpleKeyGenerator; +import org.springframework.cache.jcache.config.JCacheConfigurerSupport; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Stephane Nicoll + */ +public class JCacheKeyGeneratorTests { + + private TestKeyGenerator keyGenerator; + + private SimpleService simpleService; + + private Cache cache; + + @BeforeEach + public void setup() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class); + this.keyGenerator = context.getBean(TestKeyGenerator.class); + this.simpleService = context.getBean(SimpleService.class); + this.cache = context.getBean(CacheManager.class).getCache("test"); + context.close(); + } + + @Test + public void getSimple() { + this.keyGenerator.expect(1L); + Object first = this.simpleService.get(1L); + Object second = this.simpleService.get(1L); + assertThat(second).isSameAs(first); + + Object key = new SimpleKey(1L); + assertThat(cache.get(key).get()).isEqualTo(first); + } + + @Test + public void getFlattenVararg() { + this.keyGenerator.expect(1L, "foo", "bar"); + Object first = this.simpleService.get(1L, "foo", "bar"); + Object second = this.simpleService.get(1L, "foo", "bar"); + assertThat(second).isSameAs(first); + + Object key = new SimpleKey(1L, "foo", "bar"); + assertThat(cache.get(key).get()).isEqualTo(first); + } + + @Test + public void getFiltered() { + this.keyGenerator.expect(1L); + Object first = this.simpleService.getFiltered(1L, "foo", "bar"); + Object second = this.simpleService.getFiltered(1L, "foo", "bar"); + assertThat(second).isSameAs(first); + + Object key = new SimpleKey(1L); + assertThat(cache.get(key).get()).isEqualTo(first); + } + + + @Configuration + @EnableCaching + static class Config extends JCacheConfigurerSupport { + + @Bean + @Override + public CacheManager cacheManager() { + return new ConcurrentMapCacheManager(); + } + + @Bean + @Override + public KeyGenerator keyGenerator() { + return new TestKeyGenerator(); + } + + @Bean + public SimpleService simpleService() { + return new SimpleService(); + } + + } + + @CacheDefaults(cacheName = "test") + public static class SimpleService { + private AtomicLong counter = new AtomicLong(); + + @CacheResult + public Object get(long id) { + return counter.getAndIncrement(); + } + + @CacheResult + public Object get(long id, String... items) { + return counter.getAndIncrement(); + } + + @CacheResult + public Object getFiltered(@CacheKey long id, String... items) { + return counter.getAndIncrement(); + } + + } + + + private static class TestKeyGenerator extends SimpleKeyGenerator { + + private Object[] expectedParams; + + private void expect(Object... params) { + this.expectedParams = params; + } + + @Override + public Object generate(Object target, Method method, Object... params) { + assertThat(Arrays.equals(expectedParams, params)).as("Unexpected parameters: expected: " + + Arrays.toString(this.expectedParams) + " but got: " + Arrays.toString(params)).isTrue(); + return new SimpleKey(params); + } + } +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/SampleObject.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/SampleObject.java new file mode 100644 index 0000000..f0de3c0 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/SampleObject.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.jcache.interceptor; + +import javax.cache.annotation.CacheKey; +import javax.cache.annotation.CachePut; +import javax.cache.annotation.CacheRemove; +import javax.cache.annotation.CacheRemoveAll; +import javax.cache.annotation.CacheResult; +import javax.cache.annotation.CacheValue; + +import org.springframework.beans.factory.annotation.Value; + +/** + * @author Stephane Nicoll + */ +class SampleObject { + + // Simple + + @CacheResult(cacheName = "simpleCache") + public SampleObject simpleGet(Long id) { + return null; + } + + @CachePut(cacheName = "simpleCache") + public void simplePut(Long id, @CacheValue SampleObject instance) { + } + + @CacheRemove(cacheName = "simpleCache") + public void simpleRemove(Long id) { + } + + @CacheRemoveAll(cacheName = "simpleCache") + public void simpleRemoveAll() { + } + + @CacheResult(cacheName = "testSimple") + public SampleObject anotherSimpleGet(String foo, Long bar) { + return null; + } + + // @CacheKey + + @CacheResult + public SampleObject multiKeysGet(@CacheKey Long id, Boolean notUsed, + @CacheKey String domain) { + return null; + } + + // @CacheValue + + @CachePut(cacheName = "simpleCache") + public void noCacheValue(Long id) { + } + + @CachePut(cacheName = "simpleCache") + public void multiCacheValues(Long id, @CacheValue SampleObject instance, + @CacheValue SampleObject anotherInstance) { + } + + // Parameter annotation + + @CacheResult(cacheName = "simpleCache") + public SampleObject annotatedGet(@CacheKey Long id, @Value("${foo}") String foo) { + return null; + } + + // Full config + + @CacheResult(cacheName = "simpleCache", skipGet = true, + cachedExceptions = Exception.class, nonCachedExceptions = RuntimeException.class) + public SampleObject fullGetConfig(@CacheKey Long id) { + return null; + } + + @CachePut(cacheName = "simpleCache", afterInvocation = false, + cacheFor = Exception.class, noCacheFor = RuntimeException.class) + public void fullPutConfig(@CacheKey Long id, @CacheValue SampleObject instance) { + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/transaction/AbstractTransactionSupportingCacheManagerTests.java b/spring-context-support/src/test/java/org/springframework/cache/transaction/AbstractTransactionSupportingCacheManagerTests.java new file mode 100644 index 0000000..cf0487e --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/transaction/AbstractTransactionSupportingCacheManagerTests.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.transaction; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Shared tests for {@link CacheManager} that inherit from + * {@link AbstractTransactionSupportingCacheManager}. + * + * @author Stephane Nicoll + * @author Sam Brannen + */ +public abstract class AbstractTransactionSupportingCacheManagerTests { + + public static final String CACHE_NAME = "testCacheManager"; + + + protected String cacheName; + + @BeforeEach + void trackCacheName(TestInfo testInfo) { + this.cacheName = testInfo.getTestMethod().get().getName(); + } + + + /** + * Returns the {@link CacheManager} to use. + * @param transactionAware if the requested cache manager should be aware + * of the transaction + * @return the cache manager to use + * @see org.springframework.cache.transaction.AbstractTransactionSupportingCacheManager#setTransactionAware + */ + protected abstract T getCacheManager(boolean transactionAware); + + /** + * Returns the expected concrete type of the cache. + */ + protected abstract Class getCacheType(); + + /** + * Adds a cache with the specified name to the native manager. + */ + protected abstract void addNativeCache(String cacheName); + + /** + * Removes the cache with the specified name from the native manager. + */ + protected abstract void removeNativeCache(String cacheName); + + + @Test + public void getOnExistingCache() { + assertThat(getCacheManager(false).getCache(CACHE_NAME)).isInstanceOf(getCacheType()); + } + + @Test + public void getOnNewCache() { + T cacheManager = getCacheManager(false); + addNativeCache(this.cacheName); + assertThat(cacheManager.getCacheNames().contains(this.cacheName)).isFalse(); + try { + assertThat(cacheManager.getCache(this.cacheName)).isInstanceOf(getCacheType()); + assertThat(cacheManager.getCacheNames().contains(this.cacheName)).isTrue(); + } + finally { + removeNativeCache(this.cacheName); + } + } + + @Test + public void getOnUnknownCache() { + T cacheManager = getCacheManager(false); + assertThat(cacheManager.getCacheNames().contains(this.cacheName)).isFalse(); + assertThat(cacheManager.getCache(this.cacheName)).isNull(); + } + + @Test + public void getTransactionalOnExistingCache() { + assertThat(getCacheManager(true).getCache(CACHE_NAME)) + .isInstanceOf(TransactionAwareCacheDecorator.class); + } + + @Test + public void getTransactionalOnNewCache() { + T cacheManager = getCacheManager(true); + assertThat(cacheManager.getCacheNames().contains(this.cacheName)).isFalse(); + addNativeCache(this.cacheName); + try { + assertThat(cacheManager.getCache(this.cacheName)) + .isInstanceOf(TransactionAwareCacheDecorator.class); + assertThat(cacheManager.getCacheNames().contains(this.cacheName)).isTrue(); + } + finally { + removeNativeCache(this.cacheName); + } + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/transaction/TransactionAwareCacheDecoratorTests.java b/spring-context-support/src/test/java/org/springframework/cache/transaction/TransactionAwareCacheDecoratorTests.java new file mode 100644 index 0000000..66369ed --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/transaction/TransactionAwareCacheDecoratorTests.java @@ -0,0 +1,224 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.transaction; + +import org.junit.jupiter.api.Test; + +import org.springframework.cache.Cache; +import org.springframework.cache.concurrent.ConcurrentMapCache; +import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.transaction.testfixture.CallCountingTransactionManager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Stephane Nicoll + * @author Juergen Hoeller + */ +public class TransactionAwareCacheDecoratorTests { + + private final TransactionTemplate txTemplate = new TransactionTemplate(new CallCountingTransactionManager()); + + + @Test + public void createWithNullTarget() { + assertThatIllegalArgumentException().isThrownBy(() -> new TransactionAwareCacheDecorator(null)); + } + + @Test + public void getTargetCache() { + Cache target = new ConcurrentMapCache("testCache"); + TransactionAwareCacheDecorator cache = new TransactionAwareCacheDecorator(target); + assertThat(cache.getTargetCache()).isSameAs(target); + } + + @Test + public void regularOperationsOnTarget() { + Cache target = new ConcurrentMapCache("testCache"); + Cache cache = new TransactionAwareCacheDecorator(target); + assertThat(cache.getName()).isEqualTo(target.getName()); + assertThat(cache.getNativeCache()).isEqualTo(target.getNativeCache()); + + Object key = new Object(); + target.put(key, "123"); + assertThat(cache.get(key).get()).isEqualTo("123"); + assertThat(cache.get(key, String.class)).isEqualTo("123"); + + cache.clear(); + assertThat(target.get(key)).isNull(); + } + + @Test + public void putNonTransactional() { + Cache target = new ConcurrentMapCache("testCache"); + Cache cache = new TransactionAwareCacheDecorator(target); + + Object key = new Object(); + cache.put(key, "123"); + assertThat(target.get(key, String.class)).isEqualTo("123"); + } + + @Test + public void putTransactional() { + Cache target = new ConcurrentMapCache("testCache"); + Cache cache = new TransactionAwareCacheDecorator(target); + Object key = new Object(); + + txTemplate.executeWithoutResult(s -> { + cache.put(key, "123"); + assertThat(target.get(key)).isNull(); + }); + + assertThat(target.get(key, String.class)).isEqualTo("123"); + } + + @Test + public void putIfAbsentNonTransactional() { + Cache target = new ConcurrentMapCache("testCache"); + Cache cache = new TransactionAwareCacheDecorator(target); + + Object key = new Object(); + assertThat(cache.putIfAbsent(key, "123")).isNull(); + assertThat(target.get(key, String.class)).isEqualTo("123"); + assertThat(cache.putIfAbsent(key, "456").get()).isEqualTo("123"); + // unchanged + assertThat(target.get(key, String.class)).isEqualTo("123"); + } + + @Test + public void putIfAbsentTransactional() { // no transactional support for putIfAbsent + Cache target = new ConcurrentMapCache("testCache"); + Cache cache = new TransactionAwareCacheDecorator(target); + Object key = new Object(); + + txTemplate.executeWithoutResult(s -> { + assertThat(cache.putIfAbsent(key, "123")).isNull(); + assertThat(target.get(key, String.class)).isEqualTo("123"); + assertThat(cache.putIfAbsent(key, "456").get()).isEqualTo("123"); + // unchanged + assertThat(target.get(key, String.class)).isEqualTo("123"); + }); + + assertThat(target.get(key, String.class)).isEqualTo("123"); + } + + @Test + public void evictNonTransactional() { + Cache target = new ConcurrentMapCache("testCache"); + Cache cache = new TransactionAwareCacheDecorator(target); + Object key = new Object(); + cache.put(key, "123"); + + cache.evict(key); + assertThat(target.get(key)).isNull(); + } + + @Test + public void evictTransactional() { + Cache target = new ConcurrentMapCache("testCache"); + Cache cache = new TransactionAwareCacheDecorator(target); + Object key = new Object(); + cache.put(key, "123"); + + txTemplate.executeWithoutResult(s -> { + cache.evict(key); + assertThat(target.get(key, String.class)).isEqualTo("123"); + }); + + assertThat(target.get(key)).isNull(); + } + + @Test + public void evictIfPresentNonTransactional() { + Cache target = new ConcurrentMapCache("testCache"); + Cache cache = new TransactionAwareCacheDecorator(target); + Object key = new Object(); + cache.put(key, "123"); + + cache.evictIfPresent(key); + assertThat(target.get(key)).isNull(); + } + + @Test + public void evictIfPresentTransactional() { // no transactional support for evictIfPresent + Cache target = new ConcurrentMapCache("testCache"); + Cache cache = new TransactionAwareCacheDecorator(target); + Object key = new Object(); + cache.put(key, "123"); + + txTemplate.executeWithoutResult(s -> { + cache.evictIfPresent(key); + assertThat(target.get(key)).isNull(); + }); + + assertThat(target.get(key)).isNull(); + } + + @Test + public void clearNonTransactional() { + Cache target = new ConcurrentMapCache("testCache"); + Cache cache = new TransactionAwareCacheDecorator(target); + Object key = new Object(); + cache.put(key, "123"); + + cache.clear(); + assertThat(target.get(key)).isNull(); + } + + @Test + public void clearTransactional() { + Cache target = new ConcurrentMapCache("testCache"); + Cache cache = new TransactionAwareCacheDecorator(target); + Object key = new Object(); + cache.put(key, "123"); + + txTemplate.executeWithoutResult(s -> { + cache.clear(); + assertThat(target.get(key, String.class)).isEqualTo("123"); + }); + + assertThat(target.get(key)).isNull(); + } + + @Test + public void invalidateNonTransactional() { + Cache target = new ConcurrentMapCache("testCache"); + Cache cache = new TransactionAwareCacheDecorator(target); + Object key = new Object(); + cache.put(key, "123"); + + cache.invalidate(); + assertThat(target.get(key)).isNull(); + } + + @Test + public void invalidateTransactional() { // no transactional support for invalidate + Cache target = new ConcurrentMapCache("testCache"); + Cache cache = new TransactionAwareCacheDecorator(target); + Object key = new Object(); + cache.put(key, "123"); + + txTemplate.executeWithoutResult(s -> { + cache.invalidate(); + assertThat(target.get(key)).isNull(); + }); + + assertThat(target.get(key)).isNull(); + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/mail/SimpleMailMessageTests.java b/spring-context-support/src/test/java/org/springframework/mail/SimpleMailMessageTests.java new file mode 100644 index 0000000..5312152 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/mail/SimpleMailMessageTests.java @@ -0,0 +1,173 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.mail; + +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Dmitriy Kopylenko + * @author Juergen Hoeller + * @author Rick Evans + * @author Chris Beams + * @since 10.09.2003 + */ +public class SimpleMailMessageTests { + + @Test + public void testSimpleMessageCopyCtor() { + SimpleMailMessage message = new SimpleMailMessage(); + message.setFrom("me@mail.org"); + message.setTo("you@mail.org"); + + SimpleMailMessage messageCopy = new SimpleMailMessage(message); + assertThat(messageCopy.getFrom()).isEqualTo("me@mail.org"); + assertThat(messageCopy.getTo()[0]).isEqualTo("you@mail.org"); + + message.setReplyTo("reply@mail.org"); + message.setCc(new String[]{"he@mail.org", "she@mail.org"}); + message.setBcc(new String[]{"us@mail.org", "them@mail.org"}); + Date sentDate = new Date(); + message.setSentDate(sentDate); + message.setSubject("my subject"); + message.setText("my text"); + + assertThat(message.getFrom()).isEqualTo("me@mail.org"); + assertThat(message.getReplyTo()).isEqualTo("reply@mail.org"); + assertThat(message.getTo()[0]).isEqualTo("you@mail.org"); + List ccs = Arrays.asList(message.getCc()); + assertThat(ccs.contains("he@mail.org")).isTrue(); + assertThat(ccs.contains("she@mail.org")).isTrue(); + List bccs = Arrays.asList(message.getBcc()); + assertThat(bccs.contains("us@mail.org")).isTrue(); + assertThat(bccs.contains("them@mail.org")).isTrue(); + assertThat(message.getSentDate()).isEqualTo(sentDate); + assertThat(message.getSubject()).isEqualTo("my subject"); + assertThat(message.getText()).isEqualTo("my text"); + + messageCopy = new SimpleMailMessage(message); + assertThat(messageCopy.getFrom()).isEqualTo("me@mail.org"); + assertThat(messageCopy.getReplyTo()).isEqualTo("reply@mail.org"); + assertThat(messageCopy.getTo()[0]).isEqualTo("you@mail.org"); + ccs = Arrays.asList(messageCopy.getCc()); + assertThat(ccs.contains("he@mail.org")).isTrue(); + assertThat(ccs.contains("she@mail.org")).isTrue(); + bccs = Arrays.asList(message.getBcc()); + assertThat(bccs.contains("us@mail.org")).isTrue(); + assertThat(bccs.contains("them@mail.org")).isTrue(); + assertThat(messageCopy.getSentDate()).isEqualTo(sentDate); + assertThat(messageCopy.getSubject()).isEqualTo("my subject"); + assertThat(messageCopy.getText()).isEqualTo("my text"); + } + + @Test + public void testDeepCopyOfStringArrayTypedFieldsOnCopyCtor() throws Exception { + + SimpleMailMessage original = new SimpleMailMessage(); + original.setTo(new String[]{"fiona@mail.org", "apple@mail.org"}); + original.setCc(new String[]{"he@mail.org", "she@mail.org"}); + original.setBcc(new String[]{"us@mail.org", "them@mail.org"}); + + SimpleMailMessage copy = new SimpleMailMessage(original); + + original.getTo()[0] = "mmm@mmm.org"; + original.getCc()[0] = "mmm@mmm.org"; + original.getBcc()[0] = "mmm@mmm.org"; + + assertThat(copy.getTo()[0]).isEqualTo("fiona@mail.org"); + assertThat(copy.getCc()[0]).isEqualTo("he@mail.org"); + assertThat(copy.getBcc()[0]).isEqualTo("us@mail.org"); + } + + /** + * Tests that two equal SimpleMailMessages have equal hash codes. + */ + @Test + public final void testHashCode() { + SimpleMailMessage message1 = new SimpleMailMessage(); + message1.setFrom("from@somewhere"); + message1.setReplyTo("replyTo@somewhere"); + message1.setTo("to@somewhere"); + message1.setCc("cc@somewhere"); + message1.setBcc("bcc@somewhere"); + message1.setSentDate(new Date()); + message1.setSubject("subject"); + message1.setText("text"); + + // Copy the message + SimpleMailMessage message2 = new SimpleMailMessage(message1); + + assertThat(message2).isEqualTo(message1); + assertThat(message2.hashCode()).isEqualTo(message1.hashCode()); + } + + public final void testEqualsObject() { + SimpleMailMessage message1; + SimpleMailMessage message2; + + // Same object is equal + message1 = new SimpleMailMessage(); + message2 = message1; + assertThat(message1.equals(message2)).isTrue(); + + // Null object is not equal + message1 = new SimpleMailMessage(); + message2 = null; + boolean condition1 = !(message1.equals(message2)); + assertThat(condition1).isTrue(); + + // Different class is not equal + boolean condition = !(message1.equals(new Object())); + assertThat(condition).isTrue(); + + // Equal values are equal + message1 = new SimpleMailMessage(); + message2 = new SimpleMailMessage(); + assertThat(message1.equals(message2)).isTrue(); + + message1 = new SimpleMailMessage(); + message1.setFrom("from@somewhere"); + message1.setReplyTo("replyTo@somewhere"); + message1.setTo("to@somewhere"); + message1.setCc("cc@somewhere"); + message1.setBcc("bcc@somewhere"); + message1.setSentDate(new Date()); + message1.setSubject("subject"); + message1.setText("text"); + message2 = new SimpleMailMessage(message1); + assertThat(message1.equals(message2)).isTrue(); + } + + @Test + public void testCopyCtorChokesOnNullOriginalMessage() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new SimpleMailMessage(null)); + } + + @Test + public void testCopyToChokesOnNullTargetMessage() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new SimpleMailMessage().copyTo(null)); + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/mail/javamail/ConfigurableMimeFileTypeMapTests.java b/spring-context-support/src/test/java/org/springframework/mail/javamail/ConfigurableMimeFileTypeMapTests.java new file mode 100644 index 0000000..ebbf493 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/mail/javamail/ConfigurableMimeFileTypeMapTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.mail.javamail; + +import java.io.File; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + * @author Juergen Hoeller + */ +public class ConfigurableMimeFileTypeMapTests { + + @Test + public void againstDefaultConfiguration() throws Exception { + ConfigurableMimeFileTypeMap ftm = new ConfigurableMimeFileTypeMap(); + ftm.afterPropertiesSet(); + + assertThat(ftm.getContentType("foobar.HTM")).as("Invalid content type for HTM").isEqualTo("text/html"); + assertThat(ftm.getContentType("foobar.html")).as("Invalid content type for html").isEqualTo("text/html"); + assertThat(ftm.getContentType("foobar.c++")).as("Invalid content type for c++").isEqualTo("text/plain"); + assertThat(ftm.getContentType("foobar.svf")).as("Invalid content type for svf").isEqualTo("image/vnd.svf"); + assertThat(ftm.getContentType("foobar.dsf")).as("Invalid content type for dsf").isEqualTo("image/x-mgx-dsf"); + assertThat(ftm.getContentType("foobar.foo")).as("Invalid default content type").isEqualTo("application/octet-stream"); + } + + @Test + public void againstDefaultConfigurationWithFilePath() throws Exception { + ConfigurableMimeFileTypeMap ftm = new ConfigurableMimeFileTypeMap(); + assertThat(ftm.getContentType(new File("/tmp/foobar.HTM"))).as("Invalid content type for HTM").isEqualTo("text/html"); + } + + @Test + public void withAdditionalMappings() throws Exception { + ConfigurableMimeFileTypeMap ftm = new ConfigurableMimeFileTypeMap(); + ftm.setMappings(new String[] {"foo/bar HTM foo", "foo/cpp c++"}); + ftm.afterPropertiesSet(); + + assertThat(ftm.getContentType("foobar.HTM")).as("Invalid content type for HTM - override didn't work").isEqualTo("foo/bar"); + assertThat(ftm.getContentType("foobar.c++")).as("Invalid content type for c++ - override didn't work").isEqualTo("foo/cpp"); + assertThat(ftm.getContentType("bar.foo")).as("Invalid content type for foo - new mapping didn't work").isEqualTo("foo/bar"); + } + + @Test + public void withCustomMappingLocation() throws Exception { + Resource resource = new ClassPathResource("test.mime.types", getClass()); + + ConfigurableMimeFileTypeMap ftm = new ConfigurableMimeFileTypeMap(); + ftm.setMappingLocation(resource); + ftm.afterPropertiesSet(); + + assertThat(ftm.getContentType("foobar.foo")).as("Invalid content type for foo").isEqualTo("text/foo"); + assertThat(ftm.getContentType("foobar.bar")).as("Invalid content type for bar").isEqualTo("text/bar"); + assertThat(ftm.getContentType("foobar.fimg")).as("Invalid content type for fimg").isEqualTo("image/foo"); + assertThat(ftm.getContentType("foobar.bimg")).as("Invalid content type for bimg").isEqualTo("image/bar"); + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/mail/javamail/InternetAddressEditorTests.java b/spring-context-support/src/test/java/org/springframework/mail/javamail/InternetAddressEditorTests.java new file mode 100644 index 0000000..15a6fd3 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/mail/javamail/InternetAddressEditorTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.mail.javamail; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Brian Hanafee + * @author Sam Brannen + * @since 09.07.2005 + */ +public class InternetAddressEditorTests { + + private static final String EMPTY = ""; + private static final String SIMPLE = "nobody@nowhere.com"; + private static final String BAD = "("; + + private final InternetAddressEditor editor = new InternetAddressEditor(); + + + @Test + public void uninitialized() { + assertThat(editor.getAsText()).as("Uninitialized editor did not return empty value string").isEqualTo(EMPTY); + } + + @Test + public void setNull() { + editor.setAsText(null); + assertThat(editor.getAsText()).as("Setting null did not result in empty value string").isEqualTo(EMPTY); + } + + @Test + public void setEmpty() { + editor.setAsText(EMPTY); + assertThat(editor.getAsText()).as("Setting empty string did not result in empty value string").isEqualTo(EMPTY); + } + + @Test + public void allWhitespace() { + editor.setAsText(" "); + assertThat(editor.getAsText()).as("All whitespace was not recognized").isEqualTo(EMPTY); + } + + @Test + public void simpleGoodAddress() { + editor.setAsText(SIMPLE); + assertThat(editor.getAsText()).as("Simple email address failed").isEqualTo(SIMPLE); + } + + @Test + public void excessWhitespace() { + editor.setAsText(" " + SIMPLE + " "); + assertThat(editor.getAsText()).as("Whitespace was not stripped").isEqualTo(SIMPLE); + } + + @Test + public void simpleBadAddress() { + assertThatIllegalArgumentException().isThrownBy(() -> + editor.setAsText(BAD)); + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/mail/javamail/JavaMailSenderTests.java b/spring-context-support/src/test/java/org/springframework/mail/javamail/JavaMailSenderTests.java new file mode 100644 index 0000000..3bd467b --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/mail/javamail/JavaMailSenderTests.java @@ -0,0 +1,597 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.mail.javamail; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.Properties; + +import javax.activation.FileTypeMap; +import javax.mail.Address; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.NoSuchProviderException; +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.URLName; +import javax.mail.internet.AddressException; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; + +import org.junit.jupiter.api.Test; + +import org.springframework.mail.MailParseException; +import org.springframework.mail.MailSendException; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.util.ObjectUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.entry; + +/** + * @author Juergen Hoeller + * @author Stephane Nicoll + * @since 09.10.2004 + */ +public class JavaMailSenderTests { + + @Test + public void javaMailSenderWithSimpleMessage() throws MessagingException, IOException { + MockJavaMailSender sender = new MockJavaMailSender(); + sender.setHost("host"); + sender.setPort(30); + sender.setUsername("username"); + sender.setPassword("password"); + + SimpleMailMessage simpleMessage = new SimpleMailMessage(); + simpleMessage.setFrom("me@mail.org"); + simpleMessage.setReplyTo("reply@mail.org"); + simpleMessage.setTo("you@mail.org"); + simpleMessage.setCc("he@mail.org", "she@mail.org"); + simpleMessage.setBcc("us@mail.org", "them@mail.org"); + Date sentDate = new GregorianCalendar(2004, 1, 1).getTime(); + simpleMessage.setSentDate(sentDate); + simpleMessage.setSubject("my subject"); + simpleMessage.setText("my text"); + sender.send(simpleMessage); + + assertThat(sender.transport.getConnectedHost()).isEqualTo("host"); + assertThat(sender.transport.getConnectedPort()).isEqualTo(30); + assertThat(sender.transport.getConnectedUsername()).isEqualTo("username"); + assertThat(sender.transport.getConnectedPassword()).isEqualTo("password"); + assertThat(sender.transport.isCloseCalled()).isTrue(); + + assertThat(sender.transport.getSentMessages().size()).isEqualTo(1); + MimeMessage sentMessage = sender.transport.getSentMessage(0); + List

    froms = Arrays.asList(sentMessage.getFrom()); + assertThat(froms.size()).isEqualTo(1); + assertThat(((InternetAddress) froms.get(0)).getAddress()).isEqualTo("me@mail.org"); + List
    replyTos = Arrays.asList(sentMessage.getReplyTo()); + assertThat(((InternetAddress) replyTos.get(0)).getAddress()).isEqualTo("reply@mail.org"); + List
    tos = Arrays.asList(sentMessage.getRecipients(Message.RecipientType.TO)); + assertThat(tos.size()).isEqualTo(1); + assertThat(((InternetAddress) tos.get(0)).getAddress()).isEqualTo("you@mail.org"); + List
    ccs = Arrays.asList(sentMessage.getRecipients(Message.RecipientType.CC)); + assertThat(ccs.size()).isEqualTo(2); + assertThat(((InternetAddress) ccs.get(0)).getAddress()).isEqualTo("he@mail.org"); + assertThat(((InternetAddress) ccs.get(1)).getAddress()).isEqualTo("she@mail.org"); + List
    bccs = Arrays.asList(sentMessage.getRecipients(Message.RecipientType.BCC)); + assertThat(bccs.size()).isEqualTo(2); + assertThat(((InternetAddress) bccs.get(0)).getAddress()).isEqualTo("us@mail.org"); + assertThat(((InternetAddress) bccs.get(1)).getAddress()).isEqualTo("them@mail.org"); + assertThat(sentMessage.getSentDate().getTime()).isEqualTo(sentDate.getTime()); + assertThat(sentMessage.getSubject()).isEqualTo("my subject"); + assertThat(sentMessage.getContent()).isEqualTo("my text"); + } + + @Test + public void javaMailSenderWithSimpleMessages() throws MessagingException { + MockJavaMailSender sender = new MockJavaMailSender(); + sender.setHost("host"); + sender.setUsername("username"); + sender.setPassword("password"); + + SimpleMailMessage simpleMessage1 = new SimpleMailMessage(); + simpleMessage1.setTo("he@mail.org"); + SimpleMailMessage simpleMessage2 = new SimpleMailMessage(); + simpleMessage2.setTo("she@mail.org"); + sender.send(simpleMessage1, simpleMessage2); + + assertThat(sender.transport.getConnectedHost()).isEqualTo("host"); + assertThat(sender.transport.getConnectedUsername()).isEqualTo("username"); + assertThat(sender.transport.getConnectedPassword()).isEqualTo("password"); + assertThat(sender.transport.isCloseCalled()).isTrue(); + + assertThat(sender.transport.getSentMessages().size()).isEqualTo(2); + MimeMessage sentMessage1 = sender.transport.getSentMessage(0); + List
    tos1 = Arrays.asList(sentMessage1.getRecipients(Message.RecipientType.TO)); + assertThat(tos1.size()).isEqualTo(1); + assertThat(((InternetAddress) tos1.get(0)).getAddress()).isEqualTo("he@mail.org"); + MimeMessage sentMessage2 = sender.transport.getSentMessage(1); + List
    tos2 = Arrays.asList(sentMessage2.getRecipients(Message.RecipientType.TO)); + assertThat(tos2.size()).isEqualTo(1); + assertThat(((InternetAddress) tos2.get(0)).getAddress()).isEqualTo("she@mail.org"); + } + + @Test + public void javaMailSenderWithMimeMessage() throws MessagingException { + MockJavaMailSender sender = new MockJavaMailSender(); + sender.setHost("host"); + sender.setUsername("username"); + sender.setPassword("password"); + + MimeMessage mimeMessage = sender.createMimeMessage(); + mimeMessage.setRecipient(Message.RecipientType.TO, new InternetAddress("you@mail.org")); + sender.send(mimeMessage); + + assertThat(sender.transport.getConnectedHost()).isEqualTo("host"); + assertThat(sender.transport.getConnectedUsername()).isEqualTo("username"); + assertThat(sender.transport.getConnectedPassword()).isEqualTo("password"); + assertThat(sender.transport.isCloseCalled()).isTrue(); + assertThat(sender.transport.getSentMessages().size()).isEqualTo(1); + assertThat(sender.transport.getSentMessage(0)).isEqualTo(mimeMessage); + } + + @Test + public void javaMailSenderWithMimeMessages() throws MessagingException { + MockJavaMailSender sender = new MockJavaMailSender(); + sender.setHost("host"); + sender.setUsername("username"); + sender.setPassword("password"); + + MimeMessage mimeMessage1 = sender.createMimeMessage(); + mimeMessage1.setRecipient(Message.RecipientType.TO, new InternetAddress("he@mail.org")); + MimeMessage mimeMessage2 = sender.createMimeMessage(); + mimeMessage2.setRecipient(Message.RecipientType.TO, new InternetAddress("she@mail.org")); + sender.send(mimeMessage1, mimeMessage2); + + assertThat(sender.transport.getConnectedHost()).isEqualTo("host"); + assertThat(sender.transport.getConnectedUsername()).isEqualTo("username"); + assertThat(sender.transport.getConnectedPassword()).isEqualTo("password"); + assertThat(sender.transport.isCloseCalled()).isTrue(); + assertThat(sender.transport.getSentMessages().size()).isEqualTo(2); + assertThat(sender.transport.getSentMessage(0)).isEqualTo(mimeMessage1); + assertThat(sender.transport.getSentMessage(1)).isEqualTo(mimeMessage2); + } + + @Test + public void javaMailSenderWithMimeMessagePreparator() { + MockJavaMailSender sender = new MockJavaMailSender(); + sender.setHost("host"); + sender.setUsername("username"); + sender.setPassword("password"); + + final List messages = new ArrayList<>(); + + MimeMessagePreparator preparator = new MimeMessagePreparator() { + @Override + public void prepare(MimeMessage mimeMessage) throws MessagingException { + mimeMessage.setRecipient(Message.RecipientType.TO, new InternetAddress("you@mail.org")); + messages.add(mimeMessage); + } + }; + sender.send(preparator); + + assertThat(sender.transport.getConnectedHost()).isEqualTo("host"); + assertThat(sender.transport.getConnectedUsername()).isEqualTo("username"); + assertThat(sender.transport.getConnectedPassword()).isEqualTo("password"); + assertThat(sender.transport.isCloseCalled()).isTrue(); + assertThat(sender.transport.getSentMessages().size()).isEqualTo(1); + assertThat(sender.transport.getSentMessage(0)).isEqualTo(messages.get(0)); + } + + @Test + public void javaMailSenderWithMimeMessagePreparators() { + MockJavaMailSender sender = new MockJavaMailSender(); + sender.setHost("host"); + sender.setUsername("username"); + sender.setPassword("password"); + + final List messages = new ArrayList<>(); + + MimeMessagePreparator preparator1 = new MimeMessagePreparator() { + @Override + public void prepare(MimeMessage mimeMessage) throws MessagingException { + mimeMessage.setRecipient(Message.RecipientType.TO, new InternetAddress("he@mail.org")); + messages.add(mimeMessage); + } + }; + MimeMessagePreparator preparator2 = new MimeMessagePreparator() { + @Override + public void prepare(MimeMessage mimeMessage) throws MessagingException { + mimeMessage.setRecipient(Message.RecipientType.TO, new InternetAddress("she@mail.org")); + messages.add(mimeMessage); + } + }; + sender.send(preparator1, preparator2); + + assertThat(sender.transport.getConnectedHost()).isEqualTo("host"); + assertThat(sender.transport.getConnectedUsername()).isEqualTo("username"); + assertThat(sender.transport.getConnectedPassword()).isEqualTo("password"); + assertThat(sender.transport.isCloseCalled()).isTrue(); + assertThat(sender.transport.getSentMessages().size()).isEqualTo(2); + assertThat(sender.transport.getSentMessage(0)).isEqualTo(messages.get(0)); + assertThat(sender.transport.getSentMessage(1)).isEqualTo(messages.get(1)); + } + + @Test + public void javaMailSenderWithMimeMessageHelper() throws MessagingException { + MockJavaMailSender sender = new MockJavaMailSender(); + sender.setHost("host"); + sender.setUsername("username"); + sender.setPassword("password"); + + MimeMessageHelper message = new MimeMessageHelper(sender.createMimeMessage()); + assertThat(message.getEncoding()).isNull(); + boolean condition = message.getFileTypeMap() instanceof ConfigurableMimeFileTypeMap; + assertThat(condition).isTrue(); + + message.setTo("you@mail.org"); + sender.send(message.getMimeMessage()); + + assertThat(sender.transport.getConnectedHost()).isEqualTo("host"); + assertThat(sender.transport.getConnectedUsername()).isEqualTo("username"); + assertThat(sender.transport.getConnectedPassword()).isEqualTo("password"); + assertThat(sender.transport.isCloseCalled()).isTrue(); + assertThat(sender.transport.getSentMessages().size()).isEqualTo(1); + assertThat(sender.transport.getSentMessage(0)).isEqualTo(message.getMimeMessage()); + } + + @Test + public void javaMailSenderWithMimeMessageHelperAndSpecificEncoding() throws MessagingException { + MockJavaMailSender sender = new MockJavaMailSender(); + sender.setHost("host"); + sender.setUsername("username"); + sender.setPassword("password"); + + MimeMessageHelper message = new MimeMessageHelper(sender.createMimeMessage(), "UTF-8"); + assertThat(message.getEncoding()).isEqualTo("UTF-8"); + FileTypeMap fileTypeMap = new ConfigurableMimeFileTypeMap(); + message.setFileTypeMap(fileTypeMap); + assertThat(message.getFileTypeMap()).isEqualTo(fileTypeMap); + + message.setTo("you@mail.org"); + sender.send(message.getMimeMessage()); + + assertThat(sender.transport.getConnectedHost()).isEqualTo("host"); + assertThat(sender.transport.getConnectedUsername()).isEqualTo("username"); + assertThat(sender.transport.getConnectedPassword()).isEqualTo("password"); + assertThat(sender.transport.isCloseCalled()).isTrue(); + assertThat(sender.transport.getSentMessages().size()).isEqualTo(1); + assertThat(sender.transport.getSentMessage(0)).isEqualTo(message.getMimeMessage()); + } + + @Test + public void javaMailSenderWithMimeMessageHelperAndDefaultEncoding() throws MessagingException { + MockJavaMailSender sender = new MockJavaMailSender(); + sender.setHost("host"); + sender.setUsername("username"); + sender.setPassword("password"); + sender.setDefaultEncoding("UTF-8"); + + FileTypeMap fileTypeMap = new ConfigurableMimeFileTypeMap(); + sender.setDefaultFileTypeMap(fileTypeMap); + MimeMessageHelper message = new MimeMessageHelper(sender.createMimeMessage()); + assertThat(message.getEncoding()).isEqualTo("UTF-8"); + assertThat(message.getFileTypeMap()).isEqualTo(fileTypeMap); + + message.setTo("you@mail.org"); + sender.send(message.getMimeMessage()); + + assertThat(sender.transport.getConnectedHost()).isEqualTo("host"); + assertThat(sender.transport.getConnectedUsername()).isEqualTo("username"); + assertThat(sender.transport.getConnectedPassword()).isEqualTo("password"); + assertThat(sender.transport.isCloseCalled()).isTrue(); + assertThat(sender.transport.getSentMessages().size()).isEqualTo(1); + assertThat(sender.transport.getSentMessage(0)).isEqualTo(message.getMimeMessage()); + } + + @Test + public void javaMailSenderWithParseExceptionOnSimpleMessage() { + MockJavaMailSender sender = new MockJavaMailSender(); + SimpleMailMessage simpleMessage = new SimpleMailMessage(); + simpleMessage.setFrom(""); + try { + sender.send(simpleMessage); + } + catch (MailParseException ex) { + // expected + boolean condition = ex.getCause() instanceof AddressException; + assertThat(condition).isTrue(); + } + } + + @Test + public void javaMailSenderWithParseExceptionOnMimeMessagePreparator() { + MockJavaMailSender sender = new MockJavaMailSender(); + MimeMessagePreparator preparator = new MimeMessagePreparator() { + @Override + public void prepare(MimeMessage mimeMessage) throws MessagingException { + mimeMessage.setFrom(new InternetAddress("")); + } + }; + try { + sender.send(preparator); + } + catch (MailParseException ex) { + // expected + boolean condition = ex.getCause() instanceof AddressException; + assertThat(condition).isTrue(); + } + } + + @Test + public void javaMailSenderWithCustomSession() throws MessagingException { + final Session session = Session.getInstance(new Properties()); + MockJavaMailSender sender = new MockJavaMailSender() { + @Override + protected Transport getTransport(Session sess) throws NoSuchProviderException { + assertThat(sess).isEqualTo(session); + return super.getTransport(sess); + } + }; + sender.setSession(session); + sender.setHost("host"); + sender.setUsername("username"); + sender.setPassword("password"); + + MimeMessage mimeMessage = sender.createMimeMessage(); + mimeMessage.setSubject("custom"); + mimeMessage.setRecipient(Message.RecipientType.TO, new InternetAddress("you@mail.org")); + mimeMessage.setSentDate(new GregorianCalendar(2005, 3, 1).getTime()); + sender.send(mimeMessage); + + assertThat(sender.transport.getConnectedHost()).isEqualTo("host"); + assertThat(sender.transport.getConnectedUsername()).isEqualTo("username"); + assertThat(sender.transport.getConnectedPassword()).isEqualTo("password"); + assertThat(sender.transport.isCloseCalled()).isTrue(); + assertThat(sender.transport.getSentMessages().size()).isEqualTo(1); + assertThat(sender.transport.getSentMessage(0)).isEqualTo(mimeMessage); + } + + @Test + public void javaMailProperties() throws MessagingException { + Properties props = new Properties(); + props.setProperty("bogusKey", "bogusValue"); + MockJavaMailSender sender = new MockJavaMailSender() { + @Override + protected Transport getTransport(Session sess) throws NoSuchProviderException { + assertThat(sess.getProperty("bogusKey")).isEqualTo("bogusValue"); + return super.getTransport(sess); + } + }; + sender.setJavaMailProperties(props); + sender.setHost("host"); + sender.setUsername("username"); + sender.setPassword("password"); + + MimeMessage mimeMessage = sender.createMimeMessage(); + mimeMessage.setRecipient(Message.RecipientType.TO, new InternetAddress("you@mail.org")); + sender.send(mimeMessage); + + assertThat(sender.transport.getConnectedHost()).isEqualTo("host"); + assertThat(sender.transport.getConnectedUsername()).isEqualTo("username"); + assertThat(sender.transport.getConnectedPassword()).isEqualTo("password"); + assertThat(sender.transport.isCloseCalled()).isTrue(); + assertThat(sender.transport.getSentMessages().size()).isEqualTo(1); + assertThat(sender.transport.getSentMessage(0)).isEqualTo(mimeMessage); + } + + @Test + public void failedMailServerConnect() { + MockJavaMailSender sender = new MockJavaMailSender(); + sender.setHost(null); + sender.setUsername("username"); + sender.setPassword("password"); + SimpleMailMessage simpleMessage1 = new SimpleMailMessage(); + assertThatExceptionOfType(MailSendException.class).isThrownBy(() -> + sender.send(simpleMessage1)) + .satisfies(ex -> assertThat(ex.getFailedMessages()).containsExactly(entry(simpleMessage1, (Exception) ex.getCause()))); + } + + @Test + public void failedMailServerClose() { + MockJavaMailSender sender = new MockJavaMailSender(); + sender.setHost(""); + sender.setUsername("username"); + sender.setPassword("password"); + SimpleMailMessage simpleMessage1 = new SimpleMailMessage(); + assertThatExceptionOfType(MailSendException.class).isThrownBy(() -> + sender.send(simpleMessage1)) + .satisfies(ex -> assertThat(ex.getFailedMessages()).isEmpty()); + } + + @Test + public void failedSimpleMessage() throws MessagingException { + MockJavaMailSender sender = new MockJavaMailSender(); + sender.setHost("host"); + sender.setUsername("username"); + sender.setPassword("password"); + + SimpleMailMessage simpleMessage1 = new SimpleMailMessage(); + simpleMessage1.setTo("he@mail.org"); + simpleMessage1.setSubject("fail"); + SimpleMailMessage simpleMessage2 = new SimpleMailMessage(); + simpleMessage2.setTo("she@mail.org"); + + try { + sender.send(simpleMessage1, simpleMessage2); + } + catch (MailSendException ex) { + ex.printStackTrace(); + assertThat(sender.transport.getConnectedHost()).isEqualTo("host"); + assertThat(sender.transport.getConnectedUsername()).isEqualTo("username"); + assertThat(sender.transport.getConnectedPassword()).isEqualTo("password"); + assertThat(sender.transport.isCloseCalled()).isTrue(); + assertThat(sender.transport.getSentMessages().size()).isEqualTo(1); + assertThat(sender.transport.getSentMessage(0).getAllRecipients()[0]).isEqualTo(new InternetAddress("she@mail.org")); + assertThat(ex.getFailedMessages().size()).isEqualTo(1); + assertThat(ex.getFailedMessages().keySet().iterator().next()).isEqualTo(simpleMessage1); + Object subEx = ex.getFailedMessages().values().iterator().next(); + boolean condition = subEx instanceof MessagingException; + assertThat(condition).isTrue(); + assertThat(((MessagingException) subEx).getMessage()).isEqualTo("failed"); + } + } + + @Test + public void failedMimeMessage() throws MessagingException { + MockJavaMailSender sender = new MockJavaMailSender(); + sender.setHost("host"); + sender.setUsername("username"); + sender.setPassword("password"); + + MimeMessage mimeMessage1 = sender.createMimeMessage(); + mimeMessage1.setRecipient(Message.RecipientType.TO, new InternetAddress("he@mail.org")); + mimeMessage1.setSubject("fail"); + MimeMessage mimeMessage2 = sender.createMimeMessage(); + mimeMessage2.setRecipient(Message.RecipientType.TO, new InternetAddress("she@mail.org")); + + try { + sender.send(mimeMessage1, mimeMessage2); + } + catch (MailSendException ex) { + ex.printStackTrace(); + assertThat(sender.transport.getConnectedHost()).isEqualTo("host"); + assertThat(sender.transport.getConnectedUsername()).isEqualTo("username"); + assertThat(sender.transport.getConnectedPassword()).isEqualTo("password"); + assertThat(sender.transport.isCloseCalled()).isTrue(); + assertThat(sender.transport.getSentMessages().size()).isEqualTo(1); + assertThat(sender.transport.getSentMessage(0)).isEqualTo(mimeMessage2); + assertThat(ex.getFailedMessages().size()).isEqualTo(1); + assertThat(ex.getFailedMessages().keySet().iterator().next()).isEqualTo(mimeMessage1); + Object subEx = ex.getFailedMessages().values().iterator().next(); + boolean condition = subEx instanceof MessagingException; + assertThat(condition).isTrue(); + assertThat(((MessagingException) subEx).getMessage()).isEqualTo("failed"); + } + } + + @Test + public void testConnection() throws MessagingException { + MockJavaMailSender sender = new MockJavaMailSender(); + sender.setHost("host"); + sender.testConnection(); + } + + @Test + public void testConnectionWithFailure() throws MessagingException { + MockJavaMailSender sender = new MockJavaMailSender(); + sender.setHost(null); + assertThatExceptionOfType(MessagingException.class).isThrownBy( + sender::testConnection); + } + + + private static class MockJavaMailSender extends JavaMailSenderImpl { + + private MockTransport transport; + + @Override + protected Transport getTransport(Session session) throws NoSuchProviderException { + this.transport = new MockTransport(session, null); + return transport; + } + } + + + private static class MockTransport extends Transport { + + private String connectedHost = null; + private int connectedPort = -2; + private String connectedUsername = null; + private String connectedPassword = null; + private boolean closeCalled = false; + private List sentMessages = new ArrayList<>(); + + private MockTransport(Session session, URLName urlName) { + super(session, urlName); + } + + public String getConnectedHost() { + return connectedHost; + } + + public int getConnectedPort() { + return connectedPort; + } + + public String getConnectedUsername() { + return connectedUsername; + } + + public String getConnectedPassword() { + return connectedPassword; + } + + public boolean isCloseCalled() { + return closeCalled; + } + + public List getSentMessages() { + return sentMessages; + } + + public MimeMessage getSentMessage(int index) { + return (MimeMessage) this.sentMessages.get(index); + } + + @Override + public void connect(String host, int port, String username, String password) throws MessagingException { + if (host == null) { + throw new MessagingException("no host"); + } + this.connectedHost = host; + this.connectedPort = port; + this.connectedUsername = username; + this.connectedPassword = password; + setConnected(true); + } + + @Override + public synchronized void close() throws MessagingException { + if ("".equals(connectedHost)) { + throw new MessagingException("close failure"); + } + this.closeCalled = true; + } + + @Override + public void sendMessage(Message message, Address[] addresses) throws MessagingException { + if ("fail".equals(message.getSubject())) { + throw new MessagingException("failed"); + } + if (addresses == null || (message.getAllRecipients() == null ? addresses.length > 0 : + !ObjectUtils.nullSafeEquals(addresses, message.getAllRecipients()))) { + throw new MessagingException("addresses not correct"); + } + if (message.getSentDate() == null) { + throw new MessagingException("No sentDate specified"); + } + if (message.getSubject() != null && message.getSubject().contains("custom")) { + assertThat(message.getSentDate()).isEqualTo(new GregorianCalendar(2005, 3, 1).getTime()); + } + this.sentMessages.add(message); + } + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/CronTriggerFactoryBeanTests.java b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/CronTriggerFactoryBeanTests.java new file mode 100644 index 0000000..79dc044 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/CronTriggerFactoryBeanTests.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.quartz; + +import java.text.ParseException; + +import org.junit.jupiter.api.Test; +import org.quartz.CronTrigger; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Stephane Nicoll + */ +public class CronTriggerFactoryBeanTests { + + @Test + public void createWithoutJobDetail() throws ParseException { + CronTriggerFactoryBean factory = new CronTriggerFactoryBean(); + factory.setName("myTrigger"); + factory.setCronExpression("0 15 10 ? * *"); + factory.afterPropertiesSet(); + CronTrigger trigger = factory.getObject(); + assertThat(trigger.getCronExpression()).isEqualTo("0 15 10 ? * *"); + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSchedulerLifecycleTests.java b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSchedulerLifecycleTests.java new file mode 100644 index 0000000..c38def8 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSchedulerLifecycleTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.quartz; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.util.StopWatch; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Mark Fisher + * @since 3.0 + */ +public class QuartzSchedulerLifecycleTests { + + @Test // SPR-6354 + public void destroyLazyInitSchedulerWithDefaultShutdownOrderDoesNotHang() { + ConfigurableApplicationContext context = + new ClassPathXmlApplicationContext("quartzSchedulerLifecycleTests.xml", getClass()); + assertThat(context.getBean("lazyInitSchedulerWithDefaultShutdownOrder")).isNotNull(); + StopWatch sw = new StopWatch(); + sw.start("lazyScheduler"); + context.close(); + sw.stop(); + assertThat(sw.getTotalTimeMillis() < 500).as("Quartz Scheduler with lazy-init is hanging on destruction: " + + sw.getTotalTimeMillis()).isTrue(); + } + + @Test // SPR-6354 + public void destroyLazyInitSchedulerWithCustomShutdownOrderDoesNotHang() { + ConfigurableApplicationContext context = + new ClassPathXmlApplicationContext("quartzSchedulerLifecycleTests.xml", getClass()); + assertThat(context.getBean("lazyInitSchedulerWithCustomShutdownOrder")).isNotNull(); + StopWatch sw = new StopWatch(); + sw.start("lazyScheduler"); + context.close(); + sw.stop(); + assertThat(sw.getTotalTimeMillis() < 500).as("Quartz Scheduler with lazy-init is hanging on destruction: " + + sw.getTotalTimeMillis()).isTrue(); + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java new file mode 100644 index 0000000..6c7d5b8 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java @@ -0,0 +1,464 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.quartz; + +import java.util.HashMap; +import java.util.Map; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.Scheduler; +import org.quartz.SchedulerContext; +import org.quartz.SchedulerFactory; +import org.quartz.impl.JobDetailImpl; +import org.quartz.impl.SchedulerRepository; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.core.task.TaskExecutor; +import org.springframework.core.testfixture.EnabledForTestGroups; +import org.springframework.jdbc.core.JdbcTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; + +/** + * @author Juergen Hoeller + * @author Alef Arendsen + * @author Rob Harrop + * @author Dave Syer + * @author Mark Fisher + * @author Sam Brannen + * @since 20.02.2004 + */ +public class QuartzSupportTests { + + @Test + public void schedulerFactoryBeanWithApplicationContext() throws Exception { + TestBean tb = new TestBean("tb", 99); + StaticApplicationContext ac = new StaticApplicationContext(); + + final Scheduler scheduler = mock(Scheduler.class); + SchedulerContext schedulerContext = new SchedulerContext(); + given(scheduler.getContext()).willReturn(schedulerContext); + + SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean() { + @Override + protected Scheduler createScheduler(SchedulerFactory schedulerFactory, String schedulerName) { + return scheduler; + } + }; + schedulerFactoryBean.setJobFactory(null); + Map schedulerContextMap = new HashMap<>(); + schedulerContextMap.put("testBean", tb); + schedulerFactoryBean.setSchedulerContextAsMap(schedulerContextMap); + schedulerFactoryBean.setApplicationContext(ac); + schedulerFactoryBean.setApplicationContextSchedulerContextKey("appCtx"); + try { + schedulerFactoryBean.afterPropertiesSet(); + schedulerFactoryBean.start(); + Scheduler returnedScheduler = schedulerFactoryBean.getObject(); + assertThat(returnedScheduler.getContext().get("testBean")).isEqualTo(tb); + assertThat(returnedScheduler.getContext().get("appCtx")).isEqualTo(ac); + } + finally { + schedulerFactoryBean.destroy(); + } + + verify(scheduler).start(); + verify(scheduler).shutdown(false); + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + public void schedulerWithTaskExecutor() throws Exception { + CountingTaskExecutor taskExecutor = new CountingTaskExecutor(); + DummyJob.count = 0; + + JobDetailImpl jobDetail = new JobDetailImpl(); + jobDetail.setDurability(true); + jobDetail.setJobClass(DummyJob.class); + jobDetail.setName("myJob"); + + SimpleTriggerFactoryBean trigger = new SimpleTriggerFactoryBean(); + trigger.setName("myTrigger"); + trigger.setJobDetail(jobDetail); + trigger.setStartDelay(1); + trigger.setRepeatInterval(500); + trigger.setRepeatCount(1); + trigger.afterPropertiesSet(); + + SchedulerFactoryBean bean = new SchedulerFactoryBean(); + bean.setTaskExecutor(taskExecutor); + bean.setTriggers(trigger.getObject()); + bean.setJobDetails(jobDetail); + bean.afterPropertiesSet(); + bean.start(); + + Thread.sleep(500); + assertThat(DummyJob.count > 0).as("DummyJob should have been executed at least once.").isTrue(); + assertThat(taskExecutor.count).isEqualTo(DummyJob.count); + + bean.destroy(); + } + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void jobDetailWithRunnableInsteadOfJob() { + JobDetailImpl jobDetail = new JobDetailImpl(); + assertThatIllegalArgumentException().isThrownBy(() -> + jobDetail.setJobClass((Class) DummyRunnable.class)); + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + public void schedulerWithQuartzJobBean() throws Exception { + DummyJob.param = 0; + DummyJob.count = 0; + + JobDetailImpl jobDetail = new JobDetailImpl(); + jobDetail.setDurability(true); + jobDetail.setJobClass(DummyJobBean.class); + jobDetail.setName("myJob"); + jobDetail.getJobDataMap().put("param", "10"); + + SimpleTriggerFactoryBean trigger = new SimpleTriggerFactoryBean(); + trigger.setName("myTrigger"); + trigger.setJobDetail(jobDetail); + trigger.setStartDelay(1); + trigger.setRepeatInterval(500); + trigger.setRepeatCount(1); + trigger.afterPropertiesSet(); + + SchedulerFactoryBean bean = new SchedulerFactoryBean(); + bean.setTriggers(trigger.getObject()); + bean.setJobDetails(jobDetail); + bean.afterPropertiesSet(); + bean.start(); + + Thread.sleep(500); + assertThat(DummyJobBean.param).isEqualTo(10); + assertThat(DummyJobBean.count > 0).isTrue(); + + bean.destroy(); + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + public void schedulerWithSpringBeanJobFactory() throws Exception { + DummyJob.param = 0; + DummyJob.count = 0; + + JobDetailImpl jobDetail = new JobDetailImpl(); + jobDetail.setDurability(true); + jobDetail.setJobClass(DummyJob.class); + jobDetail.setName("myJob"); + jobDetail.getJobDataMap().put("param", "10"); + jobDetail.getJobDataMap().put("ignoredParam", "10"); + + SimpleTriggerFactoryBean trigger = new SimpleTriggerFactoryBean(); + trigger.setName("myTrigger"); + trigger.setJobDetail(jobDetail); + trigger.setStartDelay(1); + trigger.setRepeatInterval(500); + trigger.setRepeatCount(1); + trigger.afterPropertiesSet(); + + SchedulerFactoryBean bean = new SchedulerFactoryBean(); + bean.setJobFactory(new SpringBeanJobFactory()); + bean.setTriggers(trigger.getObject()); + bean.setJobDetails(jobDetail); + bean.afterPropertiesSet(); + bean.start(); + + Thread.sleep(500); + assertThat(DummyJob.param).isEqualTo(10); + assertThat(DummyJob.count > 0).as("DummyJob should have been executed at least once.").isTrue(); + + bean.destroy(); + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + public void schedulerWithSpringBeanJobFactoryAndParamMismatchNotIgnored() throws Exception { + DummyJob.param = 0; + DummyJob.count = 0; + + JobDetailImpl jobDetail = new JobDetailImpl(); + jobDetail.setDurability(true); + jobDetail.setJobClass(DummyJob.class); + jobDetail.setName("myJob"); + jobDetail.getJobDataMap().put("para", "10"); + jobDetail.getJobDataMap().put("ignoredParam", "10"); + + SimpleTriggerFactoryBean trigger = new SimpleTriggerFactoryBean(); + trigger.setName("myTrigger"); + trigger.setJobDetail(jobDetail); + trigger.setStartDelay(1); + trigger.setRepeatInterval(500); + trigger.setRepeatCount(1); + trigger.afterPropertiesSet(); + + SchedulerFactoryBean bean = new SchedulerFactoryBean(); + SpringBeanJobFactory jobFactory = new SpringBeanJobFactory(); + jobFactory.setIgnoredUnknownProperties("ignoredParam"); + bean.setJobFactory(jobFactory); + bean.setTriggers(trigger.getObject()); + bean.setJobDetails(jobDetail); + bean.afterPropertiesSet(); + + Thread.sleep(500); + assertThat(DummyJob.param).isEqualTo(0); + assertThat(DummyJob.count == 0).isTrue(); + + bean.destroy(); + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + public void schedulerWithSpringBeanJobFactoryAndQuartzJobBean() throws Exception { + DummyJobBean.param = 0; + DummyJobBean.count = 0; + + JobDetailImpl jobDetail = new JobDetailImpl(); + jobDetail.setDurability(true); + jobDetail.setJobClass(DummyJobBean.class); + jobDetail.setName("myJob"); + jobDetail.getJobDataMap().put("param", "10"); + + SimpleTriggerFactoryBean trigger = new SimpleTriggerFactoryBean(); + trigger.setName("myTrigger"); + trigger.setJobDetail(jobDetail); + trigger.setStartDelay(1); + trigger.setRepeatInterval(500); + trigger.setRepeatCount(1); + trigger.afterPropertiesSet(); + + SchedulerFactoryBean bean = new SchedulerFactoryBean(); + bean.setJobFactory(new SpringBeanJobFactory()); + bean.setTriggers(trigger.getObject()); + bean.setJobDetails(jobDetail); + bean.afterPropertiesSet(); + bean.start(); + + Thread.sleep(500); + assertThat(DummyJobBean.param).isEqualTo(10); + assertThat(DummyJobBean.count > 0).isTrue(); + + bean.destroy(); + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + public void schedulerWithSpringBeanJobFactoryAndJobSchedulingData() throws Exception { + DummyJob.param = 0; + DummyJob.count = 0; + + SchedulerFactoryBean bean = new SchedulerFactoryBean(); + bean.setJobFactory(new SpringBeanJobFactory()); + bean.setJobSchedulingDataLocation("org/springframework/scheduling/quartz/job-scheduling-data.xml"); + bean.afterPropertiesSet(); + bean.start(); + + Thread.sleep(500); + assertThat(DummyJob.param).isEqualTo(10); + assertThat(DummyJob.count > 0).as("DummyJob should have been executed at least once.").isTrue(); + + bean.destroy(); + } + + @Test // SPR-772 + public void multipleSchedulers() throws Exception { + try (ClassPathXmlApplicationContext ctx = context("multipleSchedulers.xml")) { + Scheduler scheduler1 = (Scheduler) ctx.getBean("scheduler1"); + Scheduler scheduler2 = (Scheduler) ctx.getBean("scheduler2"); + assertThat(scheduler2).isNotSameAs(scheduler1); + assertThat(scheduler1.getSchedulerName()).isEqualTo("quartz1"); + assertThat(scheduler2.getSchedulerName()).isEqualTo("quartz2"); + } + } + + @Test // SPR-16884 + public void multipleSchedulersWithQuartzProperties() throws Exception { + try (ClassPathXmlApplicationContext ctx = context("multipleSchedulersWithQuartzProperties.xml")) { + Scheduler scheduler1 = (Scheduler) ctx.getBean("scheduler1"); + Scheduler scheduler2 = (Scheduler) ctx.getBean("scheduler2"); + assertThat(scheduler2).isNotSameAs(scheduler1); + assertThat(scheduler1.getSchedulerName()).isEqualTo("quartz1"); + assertThat(scheduler2.getSchedulerName()).isEqualTo("quartz2"); + } + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + public void twoAnonymousMethodInvokingJobDetailFactoryBeans() throws Exception { + Thread.sleep(3000); + try (ClassPathXmlApplicationContext ctx = context("multipleAnonymousMethodInvokingJobDetailFB.xml")) { + QuartzTestBean exportService = (QuartzTestBean) ctx.getBean("exportService"); + QuartzTestBean importService = (QuartzTestBean) ctx.getBean("importService"); + + assertThat(exportService.getImportCount()).as("doImport called exportService").isEqualTo(0); + assertThat(exportService.getExportCount()).as("doExport not called on exportService").isEqualTo(2); + assertThat(importService.getImportCount()).as("doImport not called on importService").isEqualTo(2); + assertThat(importService.getExportCount()).as("doExport called on importService").isEqualTo(0); + } + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + public void schedulerAccessorBean() throws Exception { + Thread.sleep(3000); + try (ClassPathXmlApplicationContext ctx = context("schedulerAccessorBean.xml")) { + QuartzTestBean exportService = (QuartzTestBean) ctx.getBean("exportService"); + QuartzTestBean importService = (QuartzTestBean) ctx.getBean("importService"); + + assertThat(exportService.getImportCount()).as("doImport called exportService").isEqualTo(0); + assertThat(exportService.getExportCount()).as("doExport not called on exportService").isEqualTo(2); + assertThat(importService.getImportCount()).as("doImport not called on importService").isEqualTo(2); + assertThat(importService.getExportCount()).as("doExport called on importService").isEqualTo(0); + } + } + + @Test + @SuppressWarnings("resource") + public void schedulerAutoStartsOnContextRefreshedEventByDefault() throws Exception { + StaticApplicationContext context = new StaticApplicationContext(); + context.registerBeanDefinition("scheduler", new RootBeanDefinition(SchedulerFactoryBean.class)); + Scheduler bean = context.getBean("scheduler", Scheduler.class); + assertThat(bean.isStarted()).isFalse(); + context.refresh(); + assertThat(bean.isStarted()).isTrue(); + } + + @Test + @SuppressWarnings("resource") + public void schedulerAutoStartupFalse() throws Exception { + StaticApplicationContext context = new StaticApplicationContext(); + BeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(SchedulerFactoryBean.class) + .addPropertyValue("autoStartup", false).getBeanDefinition(); + context.registerBeanDefinition("scheduler", beanDefinition); + Scheduler bean = context.getBean("scheduler", Scheduler.class); + assertThat(bean.isStarted()).isFalse(); + context.refresh(); + assertThat(bean.isStarted()).isFalse(); + } + + @Test + public void schedulerRepositoryExposure() throws Exception { + try (ClassPathXmlApplicationContext ctx = context("schedulerRepositoryExposure.xml")) { + assertThat(ctx.getBean("scheduler")).isSameAs(SchedulerRepository.getInstance().lookup("myScheduler")); + } + } + + /** + * SPR-6038: detect HSQL and stop illegal locks being taken. + * TODO: Against Quartz 2.2, this test's job doesn't actually execute anymore... + */ + @Test + public void schedulerWithHsqlDataSource() throws Exception { + DummyJob.param = 0; + DummyJob.count = 0; + + try (ClassPathXmlApplicationContext ctx = context("databasePersistence.xml")) { + JdbcTemplate jdbcTemplate = new JdbcTemplate(ctx.getBean(DataSource.class)); + assertThat(jdbcTemplate.queryForList("SELECT * FROM qrtz_triggers").isEmpty()).as("No triggers were persisted").isFalse(); + + /* + Thread.sleep(3000); + assertTrue("DummyJob should have been executed at least once.", DummyJob.count > 0); + */ + } + } + + private ClassPathXmlApplicationContext context(String path) { + return new ClassPathXmlApplicationContext(path, getClass()); + } + + + public static class CountingTaskExecutor implements TaskExecutor { + + private int count; + + @Override + public void execute(Runnable task) { + this.count++; + task.run(); + } + } + + + public static class DummyJob implements Job { + + private static int param; + + private static int count; + + public void setParam(int value) { + if (param > 0) { + throw new IllegalStateException("Param already set"); + } + param = value; + } + + @Override + public synchronized void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { + count++; + } + } + + + public static class DummyJobBean extends QuartzJobBean { + + private static int param; + + private static int count; + + public void setParam(int value) { + if (param > 0) { + throw new IllegalStateException("Param already set"); + } + param = value; + } + + @Override + protected synchronized void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException { + count++; + } + } + + + public static class DummyRunnable implements Runnable { + + @Override + public void run() { + /* no-op */ + } + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzTestBean.java b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzTestBean.java new file mode 100644 index 0000000..77539f0 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzTestBean.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2006 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.quartz; + +/** + * @author Rob Harrop + */ +public class QuartzTestBean { + + private int importCount; + + private int exportCount; + + + public void doImport() { + ++importCount; + } + + public void doExport() { + ++exportCount; + } + + public int getImportCount() { + return importCount; + } + + public int getExportCount() { + return exportCount; + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/SimpleTriggerFactoryBeanTests.java b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/SimpleTriggerFactoryBeanTests.java new file mode 100644 index 0000000..6db0bcc --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/SimpleTriggerFactoryBeanTests.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.quartz; + +import java.text.ParseException; + +import org.junit.jupiter.api.Test; +import org.quartz.SimpleTrigger; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Stephane Nicoll + */ +public class SimpleTriggerFactoryBeanTests { + + @Test + public void createWithoutJobDetail() throws ParseException { + SimpleTriggerFactoryBean factory = new SimpleTriggerFactoryBean(); + factory.setName("myTrigger"); + factory.setRepeatCount(5); + factory.setRepeatInterval(1000L); + factory.afterPropertiesSet(); + SimpleTrigger trigger = factory.getObject(); + assertThat(trigger.getRepeatCount()).isEqualTo(5); + assertThat(trigger.getRepeatInterval()).isEqualTo(1000L); + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBeanTests.java b/spring-context-support/src/test/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBeanTests.java new file mode 100644 index 0000000..c0a07e4 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBeanTests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ui.freemarker; + +import java.util.HashMap; +import java.util.Properties; + +import freemarker.template.Configuration; +import freemarker.template.Template; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIOException; + +/** + * @author Juergen Hoeller + * @author Issam El-atif + * @author Sam Brannen + */ +public class FreeMarkerConfigurationFactoryBeanTests { + + private final FreeMarkerConfigurationFactoryBean fcfb = new FreeMarkerConfigurationFactoryBean(); + + @Test + public void freeMarkerConfigurationFactoryBeanWithConfigLocation() throws Exception { + fcfb.setConfigLocation(new FileSystemResource("myprops.properties")); + Properties props = new Properties(); + props.setProperty("myprop", "/mydir"); + fcfb.setFreemarkerSettings(props); + assertThatIOException().isThrownBy(fcfb::afterPropertiesSet); + } + + @Test + public void freeMarkerConfigurationFactoryBeanWithResourceLoaderPath() throws Exception { + fcfb.setTemplateLoaderPath("file:/mydir"); + fcfb.afterPropertiesSet(); + Configuration cfg = fcfb.getObject(); + assertThat(cfg.getTemplateLoader()).isInstanceOf(SpringTemplateLoader.class); + } + + @Test + @SuppressWarnings("rawtypes") + public void freeMarkerConfigurationFactoryBeanWithNonFileResourceLoaderPath() throws Exception { + fcfb.setTemplateLoaderPath("file:/mydir"); + Properties settings = new Properties(); + settings.setProperty("localized_lookup", "false"); + fcfb.setFreemarkerSettings(settings); + fcfb.setResourceLoader(new ResourceLoader() { + @Override + public Resource getResource(String location) { + if (!("file:/mydir".equals(location) || "file:/mydir/test".equals(location))) { + throw new IllegalArgumentException(location); + } + return new ByteArrayResource("test".getBytes(), "test"); + } + @Override + public ClassLoader getClassLoader() { + return getClass().getClassLoader(); + } + }); + fcfb.afterPropertiesSet(); + assertThat(fcfb.getObject()).isInstanceOf(Configuration.class); + Configuration fc = fcfb.getObject(); + Template ft = fc.getTemplate("test"); + assertThat(FreeMarkerTemplateUtils.processTemplateIntoString(ft, new HashMap())).isEqualTo("test"); + } + + @Test // SPR-12448 + public void freeMarkerConfigurationAsBean() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + RootBeanDefinition loaderDef = new RootBeanDefinition(SpringTemplateLoader.class); + loaderDef.getConstructorArgumentValues().addGenericArgumentValue(new DefaultResourceLoader()); + loaderDef.getConstructorArgumentValues().addGenericArgumentValue("/freemarker"); + RootBeanDefinition configDef = new RootBeanDefinition(Configuration.class); + configDef.getPropertyValues().add("templateLoader", loaderDef); + beanFactory.registerBeanDefinition("freeMarkerConfig", configDef); + assertThat(beanFactory.getBean(Configuration.class)).isNotNull(); + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/BeanValidationPostProcessorTests.java b/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/BeanValidationPostProcessorTests.java new file mode 100644 index 0000000..c5da1e4 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/BeanValidationPostProcessorTests.java @@ -0,0 +1,155 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation.beanvalidation2; + +import javax.annotation.PostConstruct; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.annotation.CommonAnnotationBeanPostProcessor; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.validation.beanvalidation.BeanValidationPostProcessor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Juergen Hoeller + */ +public class BeanValidationPostProcessorTests { + + @Test + public void testNotNullConstraint() { + GenericApplicationContext ac = new GenericApplicationContext(); + ac.registerBeanDefinition("bvpp", new RootBeanDefinition(BeanValidationPostProcessor.class)); + ac.registerBeanDefinition("capp", new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class)); + ac.registerBeanDefinition("bean", new RootBeanDefinition(NotNullConstrainedBean.class)); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(ac::refresh) + .havingRootCause() + .withMessageContainingAll("testBean", "invalid"); + ac.close(); + } + + @Test + public void testNotNullConstraintSatisfied() { + GenericApplicationContext ac = new GenericApplicationContext(); + ac.registerBeanDefinition("bvpp", new RootBeanDefinition(BeanValidationPostProcessor.class)); + ac.registerBeanDefinition("capp", new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class)); + RootBeanDefinition bd = new RootBeanDefinition(NotNullConstrainedBean.class); + bd.getPropertyValues().add("testBean", new TestBean()); + ac.registerBeanDefinition("bean", bd); + ac.refresh(); + ac.close(); + } + + @Test + public void testNotNullConstraintAfterInitialization() { + GenericApplicationContext ac = new GenericApplicationContext(); + RootBeanDefinition bvpp = new RootBeanDefinition(BeanValidationPostProcessor.class); + bvpp.getPropertyValues().add("afterInitialization", true); + ac.registerBeanDefinition("bvpp", bvpp); + ac.registerBeanDefinition("capp", new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class)); + ac.registerBeanDefinition("bean", new RootBeanDefinition(AfterInitConstraintBean.class)); + ac.refresh(); + ac.close(); + } + + @Test + public void testSizeConstraint() { + GenericApplicationContext ac = new GenericApplicationContext(); + ac.registerBeanDefinition("bvpp", new RootBeanDefinition(BeanValidationPostProcessor.class)); + RootBeanDefinition bd = new RootBeanDefinition(NotNullConstrainedBean.class); + bd.getPropertyValues().add("testBean", new TestBean()); + bd.getPropertyValues().add("stringValue", "s"); + ac.registerBeanDefinition("bean", bd); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(ac::refresh) + .havingRootCause() + .withMessageContainingAll("stringValue", "invalid"); + ac.close(); + } + + @Test + public void testSizeConstraintSatisfied() { + GenericApplicationContext ac = new GenericApplicationContext(); + ac.registerBeanDefinition("bvpp", new RootBeanDefinition(BeanValidationPostProcessor.class)); + RootBeanDefinition bd = new RootBeanDefinition(NotNullConstrainedBean.class); + bd.getPropertyValues().add("testBean", new TestBean()); + bd.getPropertyValues().add("stringValue", "ss"); + ac.registerBeanDefinition("bean", bd); + ac.refresh(); + ac.close(); + } + + + public static class NotNullConstrainedBean { + + @NotNull + private TestBean testBean; + + @Size(min = 2) + private String stringValue; + + public TestBean getTestBean() { + return testBean; + } + + public void setTestBean(TestBean testBean) { + this.testBean = testBean; + } + + public String getStringValue() { + return stringValue; + } + + public void setStringValue(String stringValue) { + this.stringValue = stringValue; + } + + @PostConstruct + public void init() { + assertThat(this.testBean).as("Shouldn't be here after constraint checking").isNotNull(); + } + } + + + public static class AfterInitConstraintBean { + + @NotNull + private TestBean testBean; + + public TestBean getTestBean() { + return testBean; + } + + public void setTestBean(TestBean testBean) { + this.testBean = testBean; + } + + @PostConstruct + public void init() { + this.testBean = new TestBean(); + } + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/MethodValidationTests.java b/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/MethodValidationTests.java new file mode 100644 index 0000000..763bc15 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/MethodValidationTests.java @@ -0,0 +1,218 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation.beanvalidation2; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.validation.ValidationException; +import javax.validation.Validator; +import javax.validation.constraints.Max; +import javax.validation.constraints.NotNull; +import javax.validation.groups.Default; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.AsyncAnnotationAdvisor; +import org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor; +import org.springframework.validation.annotation.Validated; +import org.springframework.validation.beanvalidation.CustomValidatorBean; +import org.springframework.validation.beanvalidation.MethodValidationInterceptor; +import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Juergen Hoeller + */ +public class MethodValidationTests { + + @Test + @SuppressWarnings("unchecked") + public void testMethodValidationInterceptor() { + MyValidBean bean = new MyValidBean(); + ProxyFactory proxyFactory = new ProxyFactory(bean); + proxyFactory.addAdvice(new MethodValidationInterceptor()); + proxyFactory.addAdvisor(new AsyncAnnotationAdvisor()); + doTestProxyValidation((MyValidInterface) proxyFactory.getProxy()); + } + + @Test + @SuppressWarnings("unchecked") + public void testMethodValidationPostProcessor() { + StaticApplicationContext ac = new StaticApplicationContext(); + ac.registerSingleton("mvpp", MethodValidationPostProcessor.class); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("beforeExistingAdvisors", false); + ac.registerSingleton("aapp", AsyncAnnotationBeanPostProcessor.class, pvs); + ac.registerSingleton("bean", MyValidBean.class); + ac.refresh(); + doTestProxyValidation(ac.getBean("bean", MyValidInterface.class)); + ac.close(); + } + + private void doTestProxyValidation(MyValidInterface proxy) { + assertThat(proxy.myValidMethod("value", 5)).isNotNull(); + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> + proxy.myValidMethod("value", 15)); + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> + proxy.myValidMethod(null, 5)); + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> + proxy.myValidMethod("value", 0)); + proxy.myValidAsyncMethod("value", 5); + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> + proxy.myValidAsyncMethod("value", 15)); + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> + proxy.myValidAsyncMethod(null, 5)); + assertThat(proxy.myGenericMethod("myValue")).isEqualTo("myValue"); + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> + proxy.myGenericMethod(null)); + } + + @Test + public void testLazyValidatorForMethodValidation() { + @SuppressWarnings("resource") + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext( + LazyMethodValidationConfig.class, CustomValidatorBean.class, + MyValidBean.class, MyValidFactoryBean.class); + ctx.getBeansOfType(MyValidInterface.class).values().forEach(bean -> bean.myValidMethod("value", 5)); + } + + @Test + public void testLazyValidatorForMethodValidationWithProxyTargetClass() { + @SuppressWarnings("resource") + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext( + LazyMethodValidationConfigWithProxyTargetClass.class, CustomValidatorBean.class, + MyValidBean.class, MyValidFactoryBean.class); + ctx.getBeansOfType(MyValidInterface.class).values().forEach(bean -> bean.myValidMethod("value", 5)); + } + + + @MyStereotype + public static class MyValidBean implements MyValidInterface { + + @Override + public Object myValidMethod(String arg1, int arg2) { + return (arg2 == 0 ? null : "value"); + } + + @Override + public void myValidAsyncMethod(String arg1, int arg2) { + } + + @Override + public String myGenericMethod(String value) { + return value; + } + } + + + @MyStereotype + public static class MyValidFactoryBean implements FactoryBean, MyValidInterface { + + @Override + public String getObject() { + return null; + } + + @Override + public Class getObjectType() { + return String.class; + } + + @Override + public Object myValidMethod(String arg1, int arg2) { + return (arg2 == 0 ? null : "value"); + } + + @Override + public void myValidAsyncMethod(String arg1, int arg2) { + } + + @Override + public String myGenericMethod(String value) { + return value; + } + } + + + public interface MyValidInterface { + + @NotNull Object myValidMethod(@NotNull(groups = MyGroup.class) String arg1, @Max(10) int arg2); + + @MyValid + @Async void myValidAsyncMethod(@NotNull(groups = OtherGroup.class) String arg1, @Max(10) int arg2); + + T myGenericMethod(@NotNull T value); + } + + + public interface MyGroup { + } + + + public interface OtherGroup { + } + + + @Validated({MyGroup.class, Default.class}) + @Retention(RetentionPolicy.RUNTIME) + public @interface MyStereotype { + } + + + @Validated({OtherGroup.class, Default.class}) + @Retention(RetentionPolicy.RUNTIME) + public @interface MyValid { + } + + + @Configuration + public static class LazyMethodValidationConfig { + + @Bean + public static MethodValidationPostProcessor methodValidationPostProcessor(@Lazy Validator validator) { + MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor(); + postProcessor.setValidator(validator); + return postProcessor; + } + } + + + @Configuration + public static class LazyMethodValidationConfigWithProxyTargetClass { + + @Bean + public static MethodValidationPostProcessor methodValidationPostProcessor(@Lazy Validator validator) { + MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor(); + postProcessor.setValidator(validator); + postProcessor.setProxyTargetClass(true); + return postProcessor; + } + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/SpringValidatorAdapterTests.java b/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/SpringValidatorAdapterTests.java new file mode 100644 index 0000000..813111a --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/SpringValidatorAdapterTests.java @@ -0,0 +1,563 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation.beanvalidation2; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import javax.validation.Constraint; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import javax.validation.ConstraintViolation; +import javax.validation.Payload; +import javax.validation.Valid; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.BeanWrapperImpl; +import org.springframework.context.support.StaticMessageSource; +import org.springframework.core.testfixture.io.SerializationTestUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.FieldError; +import org.springframework.validation.beanvalidation.SpringValidatorAdapter; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Kazuki Shimizu + * @author Juergen Hoeller + */ +public class SpringValidatorAdapterTests { + + private final Validator nativeValidator = Validation.buildDefaultValidatorFactory().getValidator(); + + private final SpringValidatorAdapter validatorAdapter = new SpringValidatorAdapter(nativeValidator); + + private final StaticMessageSource messageSource = new StaticMessageSource(); + + + @BeforeEach + public void setupSpringValidatorAdapter() { + messageSource.addMessage("Size", Locale.ENGLISH, "Size of {0} must be between {2} and {1}"); + messageSource.addMessage("Same", Locale.ENGLISH, "{2} must be same value as {1}"); + messageSource.addMessage("password", Locale.ENGLISH, "Password"); + messageSource.addMessage("confirmPassword", Locale.ENGLISH, "Password(Confirm)"); + } + + + @Test + public void testUnwrap() { + Validator nativeValidator = validatorAdapter.unwrap(Validator.class); + assertThat(nativeValidator).isSameAs(this.nativeValidator); + } + + @Test // SPR-13406 + public void testNoStringArgumentValue() throws Exception { + TestBean testBean = new TestBean(); + testBean.setPassword("pass"); + testBean.setConfirmPassword("pass"); + + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(testBean, "testBean"); + validatorAdapter.validate(testBean, errors); + + assertThat(errors.getFieldErrorCount("password")).isEqualTo(1); + assertThat(errors.getFieldValue("password")).isEqualTo("pass"); + FieldError error = errors.getFieldError("password"); + assertThat(error).isNotNull(); + assertThat(messageSource.getMessage(error, Locale.ENGLISH)).isEqualTo("Size of Password must be between 8 and 128"); + assertThat(error.contains(ConstraintViolation.class)).isTrue(); + assertThat(error.unwrap(ConstraintViolation.class).getPropertyPath().toString()).isEqualTo("password"); + assertThat(SerializationTestUtils.serializeAndDeserialize(error.toString())).isEqualTo(error.toString()); + } + + @Test // SPR-13406 + public void testApplyMessageSourceResolvableToStringArgumentValueWithResolvedLogicalFieldName() throws Exception { + TestBean testBean = new TestBean(); + testBean.setPassword("password"); + testBean.setConfirmPassword("PASSWORD"); + + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(testBean, "testBean"); + validatorAdapter.validate(testBean, errors); + + assertThat(errors.getFieldErrorCount("password")).isEqualTo(1); + assertThat(errors.getFieldValue("password")).isEqualTo("password"); + FieldError error = errors.getFieldError("password"); + assertThat(error).isNotNull(); + assertThat(messageSource.getMessage(error, Locale.ENGLISH)).isEqualTo("Password must be same value as Password(Confirm)"); + assertThat(error.contains(ConstraintViolation.class)).isTrue(); + assertThat(error.unwrap(ConstraintViolation.class).getPropertyPath().toString()).isEqualTo("password"); + assertThat(SerializationTestUtils.serializeAndDeserialize(error.toString())).isEqualTo(error.toString()); + } + + @Test // SPR-13406 + public void testApplyMessageSourceResolvableToStringArgumentValueWithUnresolvedLogicalFieldName() { + TestBean testBean = new TestBean(); + testBean.setEmail("test@example.com"); + testBean.setConfirmEmail("TEST@EXAMPLE.IO"); + + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(testBean, "testBean"); + validatorAdapter.validate(testBean, errors); + + assertThat(errors.getFieldErrorCount("email")).isEqualTo(1); + assertThat(errors.getFieldValue("email")).isEqualTo("test@example.com"); + assertThat(errors.getFieldErrorCount("confirmEmail")).isEqualTo(1); + FieldError error1 = errors.getFieldError("email"); + FieldError error2 = errors.getFieldError("confirmEmail"); + assertThat(error1).isNotNull(); + assertThat(error2).isNotNull(); + assertThat(messageSource.getMessage(error1, Locale.ENGLISH)).isEqualTo("email must be same value as confirmEmail"); + assertThat(messageSource.getMessage(error2, Locale.ENGLISH)).isEqualTo("Email required"); + assertThat(error1.contains(ConstraintViolation.class)).isTrue(); + assertThat(error1.unwrap(ConstraintViolation.class).getPropertyPath().toString()).isEqualTo("email"); + assertThat(error2.contains(ConstraintViolation.class)).isTrue(); + assertThat(error2.unwrap(ConstraintViolation.class).getPropertyPath().toString()).isEqualTo("confirmEmail"); + } + + @Test // SPR-15123 + public void testApplyMessageSourceResolvableToStringArgumentValueWithAlwaysUseMessageFormat() { + messageSource.setAlwaysUseMessageFormat(true); + + TestBean testBean = new TestBean(); + testBean.setEmail("test@example.com"); + testBean.setConfirmEmail("TEST@EXAMPLE.IO"); + + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(testBean, "testBean"); + validatorAdapter.validate(testBean, errors); + + assertThat(errors.getFieldErrorCount("email")).isEqualTo(1); + assertThat(errors.getFieldValue("email")).isEqualTo("test@example.com"); + assertThat(errors.getFieldErrorCount("confirmEmail")).isEqualTo(1); + FieldError error1 = errors.getFieldError("email"); + FieldError error2 = errors.getFieldError("confirmEmail"); + assertThat(error1).isNotNull(); + assertThat(error2).isNotNull(); + assertThat(messageSource.getMessage(error1, Locale.ENGLISH)).isEqualTo("email must be same value as confirmEmail"); + assertThat(messageSource.getMessage(error2, Locale.ENGLISH)).isEqualTo("Email required"); + assertThat(error1.contains(ConstraintViolation.class)).isTrue(); + assertThat(error1.unwrap(ConstraintViolation.class).getPropertyPath().toString()).isEqualTo("email"); + assertThat(error2.contains(ConstraintViolation.class)).isTrue(); + assertThat(error2.unwrap(ConstraintViolation.class).getPropertyPath().toString()).isEqualTo("confirmEmail"); + } + + @Test + public void testPatternMessage() { + TestBean testBean = new TestBean(); + testBean.setEmail("X"); + testBean.setConfirmEmail("X"); + + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(testBean, "testBean"); + validatorAdapter.validate(testBean, errors); + + assertThat(errors.getFieldErrorCount("email")).isEqualTo(1); + assertThat(errors.getFieldValue("email")).isEqualTo("X"); + FieldError error = errors.getFieldError("email"); + assertThat(error).isNotNull(); + assertThat(messageSource.getMessage(error, Locale.ENGLISH)).contains("[\\w.'-]{1,}@[\\w.'-]{1,}"); + assertThat(error.contains(ConstraintViolation.class)).isTrue(); + assertThat(error.unwrap(ConstraintViolation.class).getPropertyPath().toString()).isEqualTo("email"); + } + + @Test // SPR-16177 + public void testWithList() { + Parent parent = new Parent(); + parent.setName("Parent whit list"); + parent.getChildList().addAll(createChildren(parent)); + + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(parent, "parent"); + validatorAdapter.validate(parent, errors); + + assertThat(errors.getErrorCount() > 0).isTrue(); + } + + @Test // SPR-16177 + public void testWithSet() { + Parent parent = new Parent(); + parent.setName("Parent with set"); + parent.getChildSet().addAll(createChildren(parent)); + + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(parent, "parent"); + validatorAdapter.validate(parent, errors); + + assertThat(errors.getErrorCount() > 0).isTrue(); + } + + private List createChildren(Parent parent) { + Child child1 = new Child(); + child1.setName("Child1"); + child1.setAge(null); + child1.setParent(parent); + + Child child2 = new Child(); + child2.setName(null); + child2.setAge(17); + child2.setParent(parent); + + return Arrays.asList(child1, child2); + } + + @Test // SPR-15839 + public void testListElementConstraint() { + BeanWithListElementConstraint bean = new BeanWithListElementConstraint(); + bean.setProperty(Arrays.asList("no", "element", "can", "be", null)); + + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(bean, "bean"); + validatorAdapter.validate(bean, errors); + + assertThat(errors.getFieldErrorCount("property[4]")).isEqualTo(1); + assertThat(errors.getFieldValue("property[4]")).isNull(); + } + + @Test // SPR-15839 + public void testMapValueConstraint() { + Map property = new HashMap<>(); + property.put("no value can be", null); + + BeanWithMapEntryConstraint bean = new BeanWithMapEntryConstraint(); + bean.setProperty(property); + + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(bean, "bean"); + validatorAdapter.validate(bean, errors); + + assertThat(errors.getFieldErrorCount("property[no value can be]")).isEqualTo(1); + assertThat(errors.getFieldValue("property[no value can be]")).isNull(); + } + + @Test // SPR-15839 + public void testMapEntryConstraint() { + Map property = new HashMap<>(); + property.put(null, null); + + BeanWithMapEntryConstraint bean = new BeanWithMapEntryConstraint(); + bean.setProperty(property); + + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(bean, "bean"); + validatorAdapter.validate(bean, errors); + + assertThat(errors.hasFieldErrors("property[]")).isTrue(); + assertThat(errors.getFieldValue("property[]")).isNull(); + } + + + @Same(field = "password", comparingField = "confirmPassword") + @Same(field = "email", comparingField = "confirmEmail") + static class TestBean { + + @Size(min = 8, max = 128) + private String password; + + private String confirmPassword; + + @Pattern(regexp = "[\\w.'-]{1,}@[\\w.'-]{1,}") + private String email; + + @Pattern(regexp = "[\\p{L} -]*", message = "Email required") + private String confirmEmail; + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getConfirmPassword() { + return confirmPassword; + } + + public void setConfirmPassword(String confirmPassword) { + this.confirmPassword = confirmPassword; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getConfirmEmail() { + return confirmEmail; + } + + public void setConfirmEmail(String confirmEmail) { + this.confirmEmail = confirmEmail; + } + } + + + @Documented + @Constraint(validatedBy = {SameValidator.class}) + @Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @Repeatable(SameGroup.class) + @interface Same { + + String message() default "{org.springframework.validation.beanvalidation.Same.message}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + String field(); + + String comparingField(); + + @Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @interface List { + Same[] value(); + } + } + + + @Documented + @Inherited + @Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @interface SameGroup { + + Same[] value(); + } + + + public static class SameValidator implements ConstraintValidator { + + private String field; + + private String comparingField; + + private String message; + + @Override + public void initialize(Same constraintAnnotation) { + field = constraintAnnotation.field(); + comparingField = constraintAnnotation.comparingField(); + message = constraintAnnotation.message(); + } + + @Override + public boolean isValid(Object value, ConstraintValidatorContext context) { + BeanWrapper beanWrapper = new BeanWrapperImpl(value); + Object fieldValue = beanWrapper.getPropertyValue(field); + Object comparingFieldValue = beanWrapper.getPropertyValue(comparingField); + boolean matched = ObjectUtils.nullSafeEquals(fieldValue, comparingFieldValue); + if (matched) { + return true; + } + else { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(message) + .addPropertyNode(field) + .addConstraintViolation(); + return false; + } + } + } + + + public static class Parent { + + private Integer id; + + @NotNull + private String name; + + @Valid + private Set childSet = new LinkedHashSet<>(); + + @Valid + private List childList = new ArrayList<>(); + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Set getChildSet() { + return childSet; + } + + public void setChildSet(Set childSet) { + this.childSet = childSet; + } + + public List getChildList() { + return childList; + } + + public void setChildList(List childList) { + this.childList = childList; + } + } + + + @AnythingValid + public static class Child { + + private Integer id; + + @NotNull + private String name; + + @NotNull + private Integer age; + + @NotNull + private Parent parent; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + + public Parent getParent() { + return parent; + } + + public void setParent(Parent parent) { + this.parent = parent; + } + } + + + @Constraint(validatedBy = AnythingValidator.class) + @Retention(RetentionPolicy.RUNTIME) + public @interface AnythingValid { + + String message() default "{AnythingValid.message}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + } + + + public static class AnythingValidator implements ConstraintValidator { + + private static final String ID = "id"; + + @Override + public void initialize(AnythingValid constraintAnnotation) { + } + + @Override + public boolean isValid(Object value, ConstraintValidatorContext context) { + List fieldsErrors = new ArrayList<>(); + Arrays.asList(value.getClass().getDeclaredFields()).forEach(field -> { + field.setAccessible(true); + try { + if (!field.getName().equals(ID) && field.get(value) == null) { + fieldsErrors.add(field); + context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()) + .addPropertyNode(field.getName()) + .addConstraintViolation(); + } + } + catch (IllegalAccessException ex) { + throw new IllegalStateException(ex); + } + }); + return fieldsErrors.isEmpty(); + } + } + + + public class BeanWithListElementConstraint { + + @Valid + private List<@NotNull String> property; + + public List getProperty() { + return property; + } + + public void setProperty(List property) { + this.property = property; + } + } + + + public class BeanWithMapEntryConstraint { + + @Valid + private Map<@NotNull String, @NotNull String> property; + + public Map getProperty() { + return property; + } + + public void setProperty(Map property) { + this.property = property; + } + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/ValidatorFactoryTests.java b/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/ValidatorFactoryTests.java new file mode 100644 index 0000000..a383ae5 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/ValidatorFactoryTests.java @@ -0,0 +1,505 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation.beanvalidation2; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import javax.validation.Constraint; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import javax.validation.ConstraintViolation; +import javax.validation.Payload; +import javax.validation.Valid; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; +import javax.validation.constraints.NotNull; + +import org.hibernate.validator.HibernateValidator; +import org.hibernate.validator.HibernateValidatorFactory; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.env.Environment; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.Errors; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + */ +@SuppressWarnings("resource") +public class ValidatorFactoryTests { + + @Test + @SuppressWarnings("cast") + public void testSimpleValidation() { + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.afterPropertiesSet(); + + ValidPerson person = new ValidPerson(); + Set> result = validator.validate(person); + assertThat(result.size()).isEqualTo(2); + for (ConstraintViolation cv : result) { + String path = cv.getPropertyPath().toString(); + assertThat(path).matches(actual -> "name".equals(actual) || "address.street".equals(actual)); + assertThat(cv.getConstraintDescriptor().getAnnotation()).isInstanceOf(NotNull.class); + } + + Validator nativeValidator = validator.unwrap(Validator.class); + assertThat(nativeValidator.getClass().getName().startsWith("org.hibernate")).isTrue(); + assertThat(validator.unwrap(ValidatorFactory.class) instanceof HibernateValidatorFactory).isTrue(); + assertThat(validator.unwrap(HibernateValidatorFactory.class) instanceof HibernateValidatorFactory).isTrue(); + + validator.destroy(); + } + + @Test + @SuppressWarnings("cast") + public void testSimpleValidationWithCustomProvider() { + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.setProviderClass(HibernateValidator.class); + validator.afterPropertiesSet(); + + ValidPerson person = new ValidPerson(); + Set> result = validator.validate(person); + assertThat(result.size()).isEqualTo(2); + for (ConstraintViolation cv : result) { + String path = cv.getPropertyPath().toString(); + assertThat(path).matches(actual -> "name".equals(actual) || "address.street".equals(actual)); + assertThat(cv.getConstraintDescriptor().getAnnotation()).isInstanceOf(NotNull.class); + } + + Validator nativeValidator = validator.unwrap(Validator.class); + assertThat(nativeValidator.getClass().getName().startsWith("org.hibernate")).isTrue(); + assertThat(validator.unwrap(ValidatorFactory.class) instanceof HibernateValidatorFactory).isTrue(); + assertThat(validator.unwrap(HibernateValidatorFactory.class) instanceof HibernateValidatorFactory).isTrue(); + + validator.destroy(); + } + + @Test + public void testSimpleValidationWithClassLevel() { + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.afterPropertiesSet(); + + ValidPerson person = new ValidPerson(); + person.setName("Juergen"); + person.getAddress().setStreet("Juergen's Street"); + Set> result = validator.validate(person); + assertThat(result.size()).isEqualTo(1); + Iterator> iterator = result.iterator(); + ConstraintViolation cv = iterator.next(); + assertThat(cv.getPropertyPath().toString()).isEqualTo(""); + assertThat(cv.getConstraintDescriptor().getAnnotation() instanceof NameAddressValid).isTrue(); + } + + @Test + public void testSpringValidationFieldType() { + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.afterPropertiesSet(); + + ValidPerson person = new ValidPerson(); + person.setName("Phil"); + person.getAddress().setStreet("Phil's Street"); + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(person, "person"); + validator.validate(person, errors); + assertThat(errors.getErrorCount()).isEqualTo(1); + assertThat(errors.getFieldError("address").getRejectedValue()).isInstanceOf(ValidAddress.class); + } + + @Test + public void testSpringValidation() { + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.afterPropertiesSet(); + + ValidPerson person = new ValidPerson(); + BeanPropertyBindingResult result = new BeanPropertyBindingResult(person, "person"); + validator.validate(person, result); + assertThat(result.getErrorCount()).isEqualTo(2); + FieldError fieldError = result.getFieldError("name"); + assertThat(fieldError.getField()).isEqualTo("name"); + List errorCodes = Arrays.asList(fieldError.getCodes()); + assertThat(errorCodes.size()).isEqualTo(4); + assertThat(errorCodes.contains("NotNull.person.name")).isTrue(); + assertThat(errorCodes.contains("NotNull.name")).isTrue(); + assertThat(errorCodes.contains("NotNull.java.lang.String")).isTrue(); + assertThat(errorCodes.contains("NotNull")).isTrue(); + fieldError = result.getFieldError("address.street"); + assertThat(fieldError.getField()).isEqualTo("address.street"); + errorCodes = Arrays.asList(fieldError.getCodes()); + assertThat(errorCodes.size()).isEqualTo(5); + assertThat(errorCodes.contains("NotNull.person.address.street")).isTrue(); + assertThat(errorCodes.contains("NotNull.address.street")).isTrue(); + assertThat(errorCodes.contains("NotNull.street")).isTrue(); + assertThat(errorCodes.contains("NotNull.java.lang.String")).isTrue(); + assertThat(errorCodes.contains("NotNull")).isTrue(); + } + + @Test + public void testSpringValidationWithClassLevel() { + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.afterPropertiesSet(); + + ValidPerson person = new ValidPerson(); + person.setName("Juergen"); + person.getAddress().setStreet("Juergen's Street"); + BeanPropertyBindingResult result = new BeanPropertyBindingResult(person, "person"); + validator.validate(person, result); + assertThat(result.getErrorCount()).isEqualTo(1); + ObjectError globalError = result.getGlobalError(); + List errorCodes = Arrays.asList(globalError.getCodes()); + assertThat(errorCodes.size()).isEqualTo(2); + assertThat(errorCodes.contains("NameAddressValid.person")).isTrue(); + assertThat(errorCodes.contains("NameAddressValid")).isTrue(); + } + + @Test + public void testSpringValidationWithAutowiredValidator() { + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext( + LocalValidatorFactoryBean.class); + LocalValidatorFactoryBean validator = ctx.getBean(LocalValidatorFactoryBean.class); + + ValidPerson person = new ValidPerson(); + person.expectsAutowiredValidator = true; + person.setName("Juergen"); + person.getAddress().setStreet("Juergen's Street"); + BeanPropertyBindingResult result = new BeanPropertyBindingResult(person, "person"); + validator.validate(person, result); + assertThat(result.getErrorCount()).isEqualTo(1); + ObjectError globalError = result.getGlobalError(); + List errorCodes = Arrays.asList(globalError.getCodes()); + assertThat(errorCodes.size()).isEqualTo(2); + assertThat(errorCodes.contains("NameAddressValid.person")).isTrue(); + assertThat(errorCodes.contains("NameAddressValid")).isTrue(); + ctx.close(); + } + + @Test + public void testSpringValidationWithErrorInListElement() { + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.afterPropertiesSet(); + + ValidPerson person = new ValidPerson(); + person.getAddressList().add(new ValidAddress()); + BeanPropertyBindingResult result = new BeanPropertyBindingResult(person, "person"); + validator.validate(person, result); + assertThat(result.getErrorCount()).isEqualTo(3); + FieldError fieldError = result.getFieldError("name"); + assertThat(fieldError.getField()).isEqualTo("name"); + fieldError = result.getFieldError("address.street"); + assertThat(fieldError.getField()).isEqualTo("address.street"); + fieldError = result.getFieldError("addressList[0].street"); + assertThat(fieldError.getField()).isEqualTo("addressList[0].street"); + } + + @Test + public void testSpringValidationWithErrorInSetElement() { + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.afterPropertiesSet(); + + ValidPerson person = new ValidPerson(); + person.getAddressSet().add(new ValidAddress()); + BeanPropertyBindingResult result = new BeanPropertyBindingResult(person, "person"); + validator.validate(person, result); + assertThat(result.getErrorCount()).isEqualTo(3); + FieldError fieldError = result.getFieldError("name"); + assertThat(fieldError.getField()).isEqualTo("name"); + fieldError = result.getFieldError("address.street"); + assertThat(fieldError.getField()).isEqualTo("address.street"); + fieldError = result.getFieldError("addressSet[].street"); + assertThat(fieldError.getField()).isEqualTo("addressSet[].street"); + } + + @Test + public void testInnerBeanValidation() { + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.afterPropertiesSet(); + + MainBean mainBean = new MainBean(); + Errors errors = new BeanPropertyBindingResult(mainBean, "mainBean"); + validator.validate(mainBean, errors); + Object rejected = errors.getFieldValue("inner.value"); + assertThat(rejected).isNull(); + } + + @Test + public void testValidationWithOptionalField() { + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.afterPropertiesSet(); + + MainBeanWithOptional mainBean = new MainBeanWithOptional(); + Errors errors = new BeanPropertyBindingResult(mainBean, "mainBean"); + validator.validate(mainBean, errors); + Object rejected = errors.getFieldValue("inner.value"); + assertThat(rejected).isNull(); + } + + @Test + public void testListValidation() { + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.afterPropertiesSet(); + + ListContainer listContainer = new ListContainer(); + listContainer.addString("A"); + listContainer.addString("X"); + + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(listContainer, "listContainer"); + errors.initConversion(new DefaultConversionService()); + validator.validate(listContainer, errors); + + FieldError fieldError = errors.getFieldError("list[1]"); + assertThat(fieldError).isNotNull(); + assertThat(fieldError.getRejectedValue()).isEqualTo("X"); + assertThat(errors.getFieldValue("list[1]")).isEqualTo("X"); + } + + + @NameAddressValid + public static class ValidPerson { + + @NotNull + private String name; + + @Valid + private ValidAddress address = new ValidAddress(); + + @Valid + private List addressList = new ArrayList<>(); + + @Valid + private Set addressSet = new LinkedHashSet<>(); + + public boolean expectsAutowiredValidator = false; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public ValidAddress getAddress() { + return address; + } + + public void setAddress(ValidAddress address) { + this.address = address; + } + + public List getAddressList() { + return addressList; + } + + public void setAddressList(List addressList) { + this.addressList = addressList; + } + + public Set getAddressSet() { + return addressSet; + } + + public void setAddressSet(Set addressSet) { + this.addressSet = addressSet; + } + } + + + public static class ValidAddress { + + @NotNull + private String street; + + public String getStreet() { + return street; + } + + public void setStreet(String street) { + this.street = street; + } + } + + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Constraint(validatedBy = NameAddressValidator.class) + public @interface NameAddressValid { + + String message() default "Street must not contain name"; + + Class[] groups() default {}; + + Class[] payload() default {}; + } + + + public static class NameAddressValidator implements ConstraintValidator { + + @Autowired + private Environment environment; + + @Override + public void initialize(NameAddressValid constraintAnnotation) { + } + + @Override + public boolean isValid(ValidPerson value, ConstraintValidatorContext context) { + if (value.expectsAutowiredValidator) { + assertThat(this.environment).isNotNull(); + } + boolean valid = (value.name == null || !value.address.street.contains(value.name)); + if (!valid && "Phil".equals(value.name)) { + context.buildConstraintViolationWithTemplate( + context.getDefaultConstraintMessageTemplate()).addPropertyNode("address").addConstraintViolation().disableDefaultConstraintViolation(); + } + return valid; + } + } + + + public static class MainBean { + + @InnerValid + private InnerBean inner = new InnerBean(); + + public InnerBean getInner() { + return inner; + } + } + + + public static class MainBeanWithOptional { + + @InnerValid + private InnerBean inner = new InnerBean(); + + public Optional getInner() { + return Optional.ofNullable(inner); + } + } + + + public static class InnerBean { + + private String value; + + public String getValue() { + return value; + } + public void setValue(String value) { + this.value = value; + } + } + + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @Constraint(validatedBy=InnerValidator.class) + public static @interface InnerValid { + + String message() default "NOT VALID"; + + Class[] groups() default { }; + + Class[] payload() default {}; + } + + + public static class InnerValidator implements ConstraintValidator { + + @Override + public void initialize(InnerValid constraintAnnotation) { + } + + @Override + public boolean isValid(InnerBean bean, ConstraintValidatorContext context) { + context.disableDefaultConstraintViolation(); + if (bean.getValue() == null) { + context.buildConstraintViolationWithTemplate("NULL").addPropertyNode("value").addConstraintViolation(); + return false; + } + return true; + } + } + + + public static class ListContainer { + + @NotXList + private List list = new ArrayList<>(); + + public void addString(String value) { + list.add(value); + } + + public List getList() { + return list; + } + } + + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @Constraint(validatedBy = NotXListValidator.class) + public @interface NotXList { + + String message() default "Should not be X"; + + Class[] groups() default {}; + + Class[] payload() default {}; + } + + + public static class NotXListValidator implements ConstraintValidator> { + + @Override + public void initialize(NotXList constraintAnnotation) { + } + + @Override + public boolean isValid(List list, ConstraintValidatorContext context) { + context.disableDefaultConstraintViolation(); + boolean valid = true; + for (int i = 0; i < list.size(); i++) { + if ("X".equals(list.get(i))) { + context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()).addBeanNode().inIterable().atIndex(i).addConstraintViolation(); + valid = false; + } + } + return valid; + } + } + +} diff --git a/spring-context-support/src/test/resources/log4j2-test.xml b/spring-context-support/src/test/resources/log4j2-test.xml new file mode 100644 index 0000000..9f3f348 --- /dev/null +++ b/spring-context-support/src/test/resources/log4j2-test.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/spring-context-support/src/test/resources/org/springframework/cache/ehcache/testEhcache.xml b/spring-context-support/src/test/resources/org/springframework/cache/ehcache/testEhcache.xml new file mode 100644 index 0000000..77e0033 --- /dev/null +++ b/spring-context-support/src/test/resources/org/springframework/cache/ehcache/testEhcache.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + diff --git a/spring-context-support/src/test/resources/org/springframework/cache/jcache/config/jCacheNamespaceDriven-resolver.xml b/spring-context-support/src/test/resources/org/springframework/cache/jcache/config/jCacheNamespaceDriven-resolver.xml new file mode 100644 index 0000000..4f76dd6 --- /dev/null +++ b/spring-context-support/src/test/resources/org/springframework/cache/jcache/config/jCacheNamespaceDriven-resolver.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-context-support/src/test/resources/org/springframework/cache/jcache/config/jCacheNamespaceDriven.xml b/spring-context-support/src/test/resources/org/springframework/cache/jcache/config/jCacheNamespaceDriven.xml new file mode 100644 index 0000000..b487b40 --- /dev/null +++ b/spring-context-support/src/test/resources/org/springframework/cache/jcache/config/jCacheNamespaceDriven.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-context-support/src/test/resources/org/springframework/cache/jcache/config/jCacheStandaloneConfig.xml b/spring-context-support/src/test/resources/org/springframework/cache/jcache/config/jCacheStandaloneConfig.xml new file mode 100644 index 0000000..428ea09 --- /dev/null +++ b/spring-context-support/src/test/resources/org/springframework/cache/jcache/config/jCacheStandaloneConfig.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context-support/src/test/resources/org/springframework/mail/javamail/test.mime.types b/spring-context-support/src/test/resources/org/springframework/mail/javamail/test.mime.types new file mode 100644 index 0000000..f54f4d7 --- /dev/null +++ b/spring-context-support/src/test/resources/org/springframework/mail/javamail/test.mime.types @@ -0,0 +1,4 @@ +text/foo foo +text/bar bar +image/foo fimg +image/bar bimg \ No newline at end of file diff --git a/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/databasePersistence.xml b/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/databasePersistence.xml new file mode 100644 index 0000000..9b7b97c --- /dev/null +++ b/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/databasePersistence.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/job-scheduling-data.xml b/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/job-scheduling-data.xml new file mode 100644 index 0000000..b437445 --- /dev/null +++ b/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/job-scheduling-data.xml @@ -0,0 +1,30 @@ + + + + + myJob + myGroup + org.springframework.scheduling.quartz.QuartzSupportTests$DummyJob + + + param + 10 + + + + + + myTrigger + myGroup + myJob + myGroup + 1 + 500 + + + + + diff --git a/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/multipleAnonymousMethodInvokingJobDetailFB.xml b/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/multipleAnonymousMethodInvokingJobDetailFB.xml new file mode 100644 index 0000000..e45ccd1 --- /dev/null +++ b/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/multipleAnonymousMethodInvokingJobDetailFB.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/multipleSchedulers.xml b/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/multipleSchedulers.xml new file mode 100644 index 0000000..df9af20 --- /dev/null +++ b/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/multipleSchedulers.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/multipleSchedulersWithQuartzProperties.xml b/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/multipleSchedulersWithQuartzProperties.xml new file mode 100644 index 0000000..e00dc29 --- /dev/null +++ b/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/multipleSchedulersWithQuartzProperties.xml @@ -0,0 +1,22 @@ + + + + + + + + + quartz1 + + + + + + + + quartz2 + + + + + diff --git a/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/quartz-hsql.sql b/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/quartz-hsql.sql new file mode 100644 index 0000000..57e05e8 --- /dev/null +++ b/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/quartz-hsql.sql @@ -0,0 +1,164 @@ +-- +-- In your Quartz properties file, you'll need to set +-- org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.HSQLDBDelegate +-- +-- Column lengths are only suggestions. For names, groups, use at least 40 chars. +-- for blobs (VARBINARY) use a size that is sure to meet the needs of the amount of data +-- you place in job data maps, etc.. +-- + +DROP TABLE qrtz_locks IF EXISTS; +DROP TABLE qrtz_scheduler_state IF EXISTS; +DROP TABLE qrtz_fired_triggers IF EXISTS; +DROP TABLE qrtz_paused_trigger_grps IF EXISTS; +DROP TABLE qrtz_calendars IF EXISTS; +DROP TABLE qrtz_blob_triggers IF EXISTS; +DROP TABLE qrtz_cron_triggers IF EXISTS; +DROP TABLE qrtz_simple_triggers IF EXISTS; +DROP TABLE qrtz_simprop_triggers IF EXISTS; +DROP TABLE qrtz_triggers IF EXISTS; +DROP TABLE qrtz_job_details IF EXISTS; + +CREATE TABLE qrtz_job_details +( +SCHED_NAME VARCHAR(120) NOT NULL, +JOB_NAME VARCHAR(200) NOT NULL, +JOB_GROUP VARCHAR(200) NOT NULL, +DESCRIPTION VARCHAR(250) NULL, +JOB_CLASS_NAME VARCHAR(250) NOT NULL, +IS_DURABLE BOOLEAN NOT NULL, +IS_NONCONCURRENT BOOLEAN NOT NULL, +IS_UPDATE_DATA BOOLEAN NOT NULL, +REQUESTS_RECOVERY BOOLEAN NOT NULL, +JOB_DATA VARBINARY(16000) NULL, +PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) +); + +CREATE TABLE qrtz_triggers +( +SCHED_NAME VARCHAR(120) NOT NULL, +TRIGGER_NAME VARCHAR(200) NOT NULL, +TRIGGER_GROUP VARCHAR(200) NOT NULL, +JOB_NAME VARCHAR(200) NOT NULL, +JOB_GROUP VARCHAR(200) NOT NULL, +DESCRIPTION VARCHAR(250) NULL, +NEXT_FIRE_TIME NUMERIC(13) NULL, +PREV_FIRE_TIME NUMERIC(13) NULL, +PRIORITY INTEGER NULL, +TRIGGER_STATE VARCHAR(16) NOT NULL, +TRIGGER_TYPE VARCHAR(8) NOT NULL, +START_TIME NUMERIC(13) NOT NULL, +END_TIME NUMERIC(13) NULL, +CALENDAR_NAME VARCHAR(200) NULL, +MISFIRE_INSTR NUMERIC(2) NULL, +JOB_DATA VARBINARY(16000) NULL, +PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), +FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) +REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP) +); + +CREATE TABLE qrtz_simple_triggers +( +SCHED_NAME VARCHAR(120) NOT NULL, +TRIGGER_NAME VARCHAR(200) NOT NULL, +TRIGGER_GROUP VARCHAR(200) NOT NULL, +REPEAT_COUNT NUMERIC(7) NOT NULL, +REPEAT_INTERVAL NUMERIC(12) NOT NULL, +TIMES_TRIGGERED NUMERIC(10) NOT NULL, +PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), +FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) +REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) +); + +CREATE TABLE qrtz_cron_triggers +( +SCHED_NAME VARCHAR(120) NOT NULL, +TRIGGER_NAME VARCHAR(200) NOT NULL, +TRIGGER_GROUP VARCHAR(200) NOT NULL, +CRON_EXPRESSION VARCHAR(120) NOT NULL, +TIME_ZONE_ID VARCHAR(80), +PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), +FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) +REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) +); + +CREATE TABLE qrtz_simprop_triggers + ( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR(200) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + STR_PROP_1 VARCHAR(512) NULL, + STR_PROP_2 VARCHAR(512) NULL, + STR_PROP_3 VARCHAR(512) NULL, + INT_PROP_1 NUMERIC(9) NULL, + INT_PROP_2 NUMERIC(9) NULL, + LONG_PROP_1 NUMERIC(13) NULL, + LONG_PROP_2 NUMERIC(13) NULL, + DEC_PROP_1 NUMERIC(13,4) NULL, + DEC_PROP_2 NUMERIC(13,4) NULL, + BOOL_PROP_1 BOOLEAN NULL, + BOOL_PROP_2 BOOLEAN NULL, + PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), + FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) + REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) +); + +CREATE TABLE qrtz_blob_triggers +( +SCHED_NAME VARCHAR(120) NOT NULL, +TRIGGER_NAME VARCHAR(200) NOT NULL, +TRIGGER_GROUP VARCHAR(200) NOT NULL, +BLOB_DATA VARBINARY(16000) NULL, +PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), +FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) +REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) +); + +CREATE TABLE qrtz_calendars +( +SCHED_NAME VARCHAR(120) NOT NULL, +CALENDAR_NAME VARCHAR(200) NOT NULL, +CALENDAR VARBINARY(16000) NOT NULL, +PRIMARY KEY (SCHED_NAME,CALENDAR_NAME) +); + +CREATE TABLE qrtz_paused_trigger_grps +( +SCHED_NAME VARCHAR(120) NOT NULL, +TRIGGER_GROUP VARCHAR(200) NOT NULL, +PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP) +); + +CREATE TABLE qrtz_fired_triggers +( +SCHED_NAME VARCHAR(120) NOT NULL, +ENTRY_ID VARCHAR(95) NOT NULL, +TRIGGER_NAME VARCHAR(200) NOT NULL, +TRIGGER_GROUP VARCHAR(200) NOT NULL, +INSTANCE_NAME VARCHAR(200) NOT NULL, +FIRED_TIME NUMERIC(13) NOT NULL, +PRIORITY INTEGER NOT NULL, +STATE VARCHAR(16) NOT NULL, +JOB_NAME VARCHAR(200) NULL, +JOB_GROUP VARCHAR(200) NULL, +IS_NONCONCURRENT BOOLEAN NULL, +REQUESTS_RECOVERY BOOLEAN NULL, +PRIMARY KEY (SCHED_NAME,ENTRY_ID) +); + +CREATE TABLE qrtz_scheduler_state +( +SCHED_NAME VARCHAR(120) NOT NULL, +INSTANCE_NAME VARCHAR(200) NOT NULL, +LAST_CHECKIN_TIME NUMERIC(13) NOT NULL, +CHECKIN_INTERVAL NUMERIC(13) NOT NULL, +PRIMARY KEY (SCHED_NAME,INSTANCE_NAME) +); + +CREATE TABLE qrtz_locks +( +SCHED_NAME VARCHAR(120) NOT NULL, +LOCK_NAME VARCHAR(40) NOT NULL, +PRIMARY KEY (SCHED_NAME,LOCK_NAME) +); + diff --git a/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/quartzSchedulerLifecycleTests.xml b/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/quartzSchedulerLifecycleTests.xml new file mode 100644 index 0000000..7e7dfb1 --- /dev/null +++ b/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/quartzSchedulerLifecycleTests.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + diff --git a/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/schedulerAccessorBean.xml b/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/schedulerAccessorBean.xml new file mode 100644 index 0000000..0ba9c4a --- /dev/null +++ b/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/schedulerAccessorBean.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/schedulerRepositoryExposure.xml b/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/schedulerRepositoryExposure.xml new file mode 100644 index 0000000..97952cd --- /dev/null +++ b/spring-context-support/src/test/resources/org/springframework/scheduling/quartz/schedulerRepositoryExposure.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context-support/src/testFixtures/java/org/springframework/contextsupport/testfixture/cache/TestableCacheKeyGenerator.java b/spring-context-support/src/testFixtures/java/org/springframework/contextsupport/testfixture/cache/TestableCacheKeyGenerator.java new file mode 100644 index 0000000..9bb225b --- /dev/null +++ b/spring-context-support/src/testFixtures/java/org/springframework/contextsupport/testfixture/cache/TestableCacheKeyGenerator.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.contextsupport.testfixture.cache; + +import java.lang.annotation.Annotation; + +import javax.cache.annotation.CacheKeyGenerator; +import javax.cache.annotation.CacheKeyInvocationContext; +import javax.cache.annotation.GeneratedCacheKey; + +import org.springframework.cache.interceptor.SimpleKey; + +/** + * A simple test key generator that only takes the first key arguments into + * account. To be used with a multi parameters key to validate it has been + * used properly. + * + * @author Stephane Nicoll + */ +public class TestableCacheKeyGenerator implements CacheKeyGenerator { + + @Override + public GeneratedCacheKey generateCacheKey(CacheKeyInvocationContext context) { + return new SimpleGeneratedCacheKey(context.getKeyParameters()[0]); + } + + + @SuppressWarnings("serial") + private static class SimpleGeneratedCacheKey extends SimpleKey implements GeneratedCacheKey { + + public SimpleGeneratedCacheKey(Object... elements) { + super(elements); + } + + } + +} diff --git a/spring-context-support/src/testFixtures/java/org/springframework/contextsupport/testfixture/cache/TestableCacheResolver.java b/spring-context-support/src/testFixtures/java/org/springframework/contextsupport/testfixture/cache/TestableCacheResolver.java new file mode 100644 index 0000000..04567b1 --- /dev/null +++ b/spring-context-support/src/testFixtures/java/org/springframework/contextsupport/testfixture/cache/TestableCacheResolver.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.contextsupport.testfixture.cache; + +import java.lang.annotation.Annotation; + +import javax.cache.Cache; +import javax.cache.annotation.CacheInvocationContext; +import javax.cache.annotation.CacheResolver; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * @author Stephane Nicoll + */ +public class TestableCacheResolver implements CacheResolver { + + @Override + public Cache resolveCache(CacheInvocationContext cacheInvocationContext) { + String cacheName = cacheInvocationContext.getCacheName(); + @SuppressWarnings("unchecked") + Cache mock = mock(Cache.class); + given(mock.getName()).willReturn(cacheName); + return mock; + } + +} diff --git a/spring-context-support/src/testFixtures/java/org/springframework/contextsupport/testfixture/cache/TestableCacheResolverFactory.java b/spring-context-support/src/testFixtures/java/org/springframework/contextsupport/testfixture/cache/TestableCacheResolverFactory.java new file mode 100644 index 0000000..5849090 --- /dev/null +++ b/spring-context-support/src/testFixtures/java/org/springframework/contextsupport/testfixture/cache/TestableCacheResolverFactory.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.contextsupport.testfixture.cache; + +import java.lang.annotation.Annotation; + +import javax.cache.annotation.CacheMethodDetails; +import javax.cache.annotation.CacheResolver; +import javax.cache.annotation.CacheResolverFactory; +import javax.cache.annotation.CacheResult; + +/** + * @author Stephane Nicoll + */ +public class TestableCacheResolverFactory implements CacheResolverFactory { + + @Override + public CacheResolver getCacheResolver(CacheMethodDetails cacheMethodDetails) { + return new TestableCacheResolver(); + } + + @Override + public CacheResolver getExceptionCacheResolver(CacheMethodDetails cacheMethodDetails) { + return new TestableCacheResolver(); + } + +} diff --git a/spring-context-support/src/testFixtures/java/org/springframework/contextsupport/testfixture/jcache/AbstractJCacheAnnotationTests.java b/spring-context-support/src/testFixtures/java/org/springframework/contextsupport/testfixture/jcache/AbstractJCacheAnnotationTests.java new file mode 100644 index 0000000..db5bb03 --- /dev/null +++ b/spring-context-support/src/testFixtures/java/org/springframework/contextsupport/testfixture/jcache/AbstractJCacheAnnotationTests.java @@ -0,0 +1,474 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.contextsupport.testfixture.jcache; + +import java.io.IOException; +import java.util.concurrent.ConcurrentHashMap; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.interceptor.SimpleKeyGenerator; +import org.springframework.context.ApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIOException; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +/** + * @author Stephane Nicoll + */ +public abstract class AbstractJCacheAnnotationTests { + + public static final String DEFAULT_CACHE = "default"; + + public static final String EXCEPTION_CACHE = "exception"; + + + protected String keyItem; + + protected ApplicationContext ctx; + + private JCacheableService service; + + private CacheManager cacheManager; + + protected abstract ApplicationContext getApplicationContext(); + + @BeforeEach + public void setUp(TestInfo testInfo) { + this.keyItem = testInfo.getTestMethod().get().getName(); + this.ctx = getApplicationContext(); + this.service = this.ctx.getBean(JCacheableService.class); + this.cacheManager = this.ctx.getBean("cacheManager", CacheManager.class); + } + + @Test + public void cache() { + Object first = service.cache(this.keyItem); + Object second = service.cache(this.keyItem); + assertThat(second).isSameAs(first); + } + + @Test + public void cacheNull() { + Cache cache = getCache(DEFAULT_CACHE); + + assertThat(cache.get(this.keyItem)).isNull(); + + Object first = service.cacheNull(this.keyItem); + Object second = service.cacheNull(this.keyItem); + assertThat(second).isSameAs(first); + + Cache.ValueWrapper wrapper = cache.get(this.keyItem); + assertThat(wrapper).isNotNull(); + assertThat(wrapper.get()).isSameAs(first); + assertThat(wrapper.get()).as("Cached value should be null").isNull(); + } + + @Test + public void cacheException() { + Cache cache = getCache(EXCEPTION_CACHE); + + Object key = createKey(this.keyItem); + assertThat(cache.get(key)).isNull(); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + service.cacheWithException(this.keyItem, true)); + + Cache.ValueWrapper result = cache.get(key); + assertThat(result).isNotNull(); + assertThat(result.get().getClass()).isEqualTo(UnsupportedOperationException.class); + } + + @Test + public void cacheExceptionVetoed() { + Cache cache = getCache(EXCEPTION_CACHE); + + Object key = createKey(this.keyItem); + assertThat(cache.get(key)).isNull(); + + assertThatNullPointerException().isThrownBy(() -> + service.cacheWithException(this.keyItem, false)); + assertThat(cache.get(key)).isNull(); + } + + @Test + public void cacheCheckedException() { + Cache cache = getCache(EXCEPTION_CACHE); + + Object key = createKey(this.keyItem); + assertThat(cache.get(key)).isNull(); + assertThatIOException().isThrownBy(() -> + service.cacheWithCheckedException(this.keyItem, true)); + + Cache.ValueWrapper result = cache.get(key); + assertThat(result).isNotNull(); + assertThat(result.get().getClass()).isEqualTo(IOException.class); + } + + + @SuppressWarnings("ThrowableResultOfMethodCallIgnored") + @Test + public void cacheExceptionRewriteCallStack() { + long ref = service.exceptionInvocations(); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + service.cacheWithException(this.keyItem, true)) + .satisfies(first -> { + // Sanity check, this particular call has called the service + // First call should not have been cached + assertThat(service.exceptionInvocations()).isEqualTo(ref + 1); + + UnsupportedOperationException second = methodInCallStack(this.keyItem); + // Sanity check, this particular call has *not* called the service + // Second call should have been cached + assertThat(service.exceptionInvocations()).isEqualTo(ref + 1); + + assertThat(first).hasCause(second.getCause()); + assertThat(first).hasMessage(second.getMessage()); + // Original stack must not contain any reference to methodInCallStack + assertThat(contain(first, AbstractJCacheAnnotationTests.class.getName(), "methodInCallStack")).isFalse(); + assertThat(contain(second, AbstractJCacheAnnotationTests.class.getName(), "methodInCallStack")).isTrue(); + }); + } + + @Test + public void cacheAlwaysInvoke() { + Object first = service.cacheAlwaysInvoke(this.keyItem); + Object second = service.cacheAlwaysInvoke(this.keyItem); + assertThat(second).isNotSameAs(first); + } + + @Test + public void cacheWithPartialKey() { + Object first = service.cacheWithPartialKey(this.keyItem, true); + Object second = service.cacheWithPartialKey(this.keyItem, false); + // second argument not used, see config + assertThat(second).isSameAs(first); + } + + @Test + public void cacheWithCustomCacheResolver() { + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(this.keyItem); + service.cacheWithCustomCacheResolver(this.keyItem); + + // Cache in mock cache + assertThat(cache.get(key)).isNull(); + } + + @Test + public void cacheWithCustomKeyGenerator() { + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(this.keyItem); + service.cacheWithCustomKeyGenerator(this.keyItem, "ignored"); + + assertThat(cache.get(key)).isNull(); + } + + @Test + public void put() { + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(this.keyItem); + Object value = new Object(); + assertThat(cache.get(key)).isNull(); + + service.put(this.keyItem, value); + + Cache.ValueWrapper result = cache.get(key); + assertThat(result).isNotNull(); + assertThat(result.get()).isEqualTo(value); + } + + @Test + public void putWithException() { + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(this.keyItem); + Object value = new Object(); + assertThat(cache.get(key)).isNull(); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + service.putWithException(this.keyItem, value, true)); + + Cache.ValueWrapper result = cache.get(key); + assertThat(result).isNotNull(); + assertThat(result.get()).isEqualTo(value); + } + + @Test + public void putWithExceptionVetoPut() { + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(this.keyItem); + Object value = new Object(); + assertThat(cache.get(key)).isNull(); + + assertThatNullPointerException().isThrownBy(() -> + service.putWithException(this.keyItem, value, false)); + assertThat(cache.get(key)).isNull(); + } + + @Test + public void earlyPut() { + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(this.keyItem); + Object value = new Object(); + assertThat(cache.get(key)).isNull(); + + service.earlyPut(this.keyItem, value); + + Cache.ValueWrapper result = cache.get(key); + assertThat(result).isNotNull(); + assertThat(result.get()).isEqualTo(value); + } + + @Test + public void earlyPutWithException() { + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(this.keyItem); + Object value = new Object(); + assertThat(cache.get(key)).isNull(); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + service.earlyPutWithException(this.keyItem, value, true)); + + Cache.ValueWrapper result = cache.get(key); + assertThat(result).isNotNull(); + assertThat(result.get()).isEqualTo(value); + } + + @Test + public void earlyPutWithExceptionVetoPut() { + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(this.keyItem); + Object value = new Object(); + assertThat(cache.get(key)).isNull(); + assertThatNullPointerException().isThrownBy(() -> + service.earlyPutWithException(this.keyItem, value, false)); + // This will be cached anyway as the earlyPut has updated the cache before + Cache.ValueWrapper result = cache.get(key); + assertThat(result).isNotNull(); + assertThat(result.get()).isEqualTo(value); + } + + @Test + public void remove() { + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(this.keyItem); + Object value = new Object(); + cache.put(key, value); + + service.remove(this.keyItem); + + assertThat(cache.get(key)).isNull(); + } + + @Test + public void removeWithException() { + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(this.keyItem); + Object value = new Object(); + cache.put(key, value); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + service.removeWithException(this.keyItem, true)); + + assertThat(cache.get(key)).isNull(); + } + + @Test + public void removeWithExceptionVetoRemove() { + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(this.keyItem); + Object value = new Object(); + cache.put(key, value); + + assertThatNullPointerException().isThrownBy(() -> + service.removeWithException(this.keyItem, false)); + Cache.ValueWrapper wrapper = cache.get(key); + assertThat(wrapper).isNotNull(); + assertThat(wrapper.get()).isEqualTo(value); + } + + @Test + public void earlyRemove() { + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(this.keyItem); + Object value = new Object(); + cache.put(key, value); + + service.earlyRemove(this.keyItem); + + assertThat(cache.get(key)).isNull(); + } + + @Test + public void earlyRemoveWithException() { + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(this.keyItem); + Object value = new Object(); + cache.put(key, value); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + service.earlyRemoveWithException(this.keyItem, true)); + assertThat(cache.get(key)).isNull(); + } + + @Test + public void earlyRemoveWithExceptionVetoRemove() { + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(this.keyItem); + Object value = new Object(); + cache.put(key, value); + + assertThatNullPointerException().isThrownBy(() -> + service.earlyRemoveWithException(this.keyItem, false)); + // This will be remove anyway as the earlyRemove has removed the cache before + assertThat(cache.get(key)).isNull(); + } + + @Test + public void removeAll() { + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(this.keyItem); + cache.put(key, new Object()); + + service.removeAll(); + + assertThat(isEmpty(cache)).isTrue(); + } + + @Test + public void removeAllWithException() { + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(this.keyItem); + cache.put(key, new Object()); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + service.removeAllWithException(true)); + + assertThat(isEmpty(cache)).isTrue(); + } + + @Test + public void removeAllWithExceptionVetoRemove() { + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(this.keyItem); + cache.put(key, new Object()); + + assertThatNullPointerException().isThrownBy(() -> + service.removeAllWithException(false)); + assertThat(cache.get(key)).isNotNull(); + } + + @Test + public void earlyRemoveAll() { + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(this.keyItem); + cache.put(key, new Object()); + + service.earlyRemoveAll(); + + assertThat(isEmpty(cache)).isTrue(); + } + + @Test + public void earlyRemoveAllWithException() { + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(this.keyItem); + cache.put(key, new Object()); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + service.earlyRemoveAllWithException(true)); + assertThat(isEmpty(cache)).isTrue(); + } + + @Test + public void earlyRemoveAllWithExceptionVetoRemove() { + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(this.keyItem); + cache.put(key, new Object()); + + assertThatNullPointerException().isThrownBy(() -> + service.earlyRemoveAllWithException(false)); + // This will be remove anyway as the earlyRemove has removed the cache before + assertThat(isEmpty(cache)).isTrue(); + } + + protected boolean isEmpty(Cache cache) { + ConcurrentHashMap nativeCache = (ConcurrentHashMap) cache.getNativeCache(); + return nativeCache.isEmpty(); + } + + + private Object createKey(Object... params) { + return SimpleKeyGenerator.generateKey(params); + } + + private Cache getCache(String name) { + Cache cache = cacheManager.getCache(name); + assertThat(cache).as("required cache " + name + " does not exist").isNotNull(); + return cache; + } + + /** + * The only purpose of this method is to invoke a particular method on the + * service so that the call stack is different. + */ + private UnsupportedOperationException methodInCallStack(String keyItem) { + try { + service.cacheWithException(keyItem, true); + throw new IllegalStateException("Should have thrown an exception"); + } + catch (UnsupportedOperationException e) { + return e; + } + } + + private boolean contain(Throwable t, String className, String methodName) { + for (StackTraceElement element : t.getStackTrace()) { + if (className.equals(element.getClassName()) && methodName.equals(element.getMethodName())) { + return true; + } + } + return false; + } + +} diff --git a/spring-context-support/src/testFixtures/java/org/springframework/contextsupport/testfixture/jcache/JCacheableService.java b/spring-context-support/src/testFixtures/java/org/springframework/contextsupport/testfixture/jcache/JCacheableService.java new file mode 100644 index 0000000..db36d11 --- /dev/null +++ b/spring-context-support/src/testFixtures/java/org/springframework/contextsupport/testfixture/jcache/JCacheableService.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.contextsupport.testfixture.jcache; + +import java.io.IOException; + +/** + * @author Stephane Nicoll + */ +public interface JCacheableService { + + T cache(String id); + + T cacheNull(String id); + + T cacheWithException(String id, boolean matchFilter); + + T cacheWithCheckedException(String id, boolean matchFilter) throws IOException; + + T cacheAlwaysInvoke(String id); + + T cacheWithPartialKey(String id, boolean notUsed); + + T cacheWithCustomCacheResolver(String id); + + T cacheWithCustomKeyGenerator(String id, String anotherId); + + void put(String id, Object value); + + void putWithException(String id, Object value, boolean matchFilter); + + void earlyPut(String id, Object value); + + void earlyPutWithException(String id, Object value, boolean matchFilter); + + void remove(String id); + + void removeWithException(String id, boolean matchFilter); + + void earlyRemove(String id); + + void earlyRemoveWithException(String id, boolean matchFilter); + + void removeAll(); + + void removeAllWithException(boolean matchFilter); + + void earlyRemoveAll(); + + void earlyRemoveAllWithException(boolean matchFilter); + + long exceptionInvocations(); + +} diff --git a/spring-context/spring-context.gradle b/spring-context/spring-context.gradle new file mode 100644 index 0000000..0b06a7c --- /dev/null +++ b/spring-context/spring-context.gradle @@ -0,0 +1,49 @@ +description = "Spring Context" + +apply plugin: "groovy" +apply plugin: "kotlin" + +dependencies { + compile(project(":spring-aop")) + compile(project(":spring-beans")) + compile(project(":spring-core")) + compile(project(":spring-expression")) + optional(project(":spring-instrument")) + optional("javax.annotation:javax.annotation-api") + optional("javax.ejb:javax.ejb-api") + optional("javax.enterprise.concurrent:javax.enterprise.concurrent-api") + optional("javax.inject:javax.inject") + optional("javax.interceptor:javax.interceptor-api") + optional("javax.money:money-api") + // Overriding 2.0.1.Final due to Bean Validation 1.1 compatibility in LocalValidatorFactoryBean + optional("javax.validation:validation-api:1.1.0.Final") + optional("javax.xml.ws:jaxws-api") + optional("org.aspectj:aspectjweaver") + optional("org.codehaus.groovy:groovy") + optional("org.apache-extras.beanshell:bsh") + optional("joda-time:joda-time") + optional("org.hibernate:hibernate-validator:5.4.3.Final") + optional("org.jetbrains.kotlin:kotlin-reflect") + optional("org.jetbrains.kotlin:kotlin-stdlib") + optional("org.reactivestreams:reactive-streams") + testCompile(testFixtures(project(":spring-aop"))) + testCompile(testFixtures(project(":spring-beans"))) + testCompile(testFixtures(project(":spring-core"))) + testCompile("io.projectreactor:reactor-core") + testCompile("org.codehaus.groovy:groovy-jsr223") + testCompile("org.codehaus.groovy:groovy-test") + testCompile("org.codehaus.groovy:groovy-xml") + testCompile("org.apache.commons:commons-pool2") + testCompile("javax.inject:javax.inject-tck") + testCompile("org.awaitility:awaitility") + testRuntime("javax.xml.bind:jaxb-api") + testRuntime("org.glassfish:javax.el") + // Substitute for javax.management:jmxremote_optional:1.0.1_04 (not available on Maven Central) + testRuntime("org.glassfish.external:opendmk_jmxremote_optional_jar") + testRuntime("org.javamoney:moneta") + testRuntime("org.junit.vintage:junit-vintage-engine") // for @Inject TCK + testFixturesApi("org.junit.jupiter:junit-jupiter-api") + testFixturesImplementation(testFixtures(project(":spring-beans"))) + testFixturesImplementation("com.google.code.findbugs:jsr305") + testFixturesImplementation("org.assertj:assertj-core") +} diff --git a/spring-context/src/jmh/java/org/springframework/context/annotation/AnnotationProcessorBenchmark.java b/spring-context/src/jmh/java/org/springframework/context/annotation/AnnotationProcessorBenchmark.java new file mode 100644 index 0000000..c00cf0d --- /dev/null +++ b/spring-context/src/jmh/java/org/springframework/context/annotation/AnnotationProcessorBenchmark.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import javax.annotation.Resource; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.support.GenericApplicationContext; + +/** + * Benchmark for bean annotation processing with various annotations. + * @author Brian Clozel + */ +@BenchmarkMode(Mode.Throughput) +public class AnnotationProcessorBenchmark { + + @State(Scope.Benchmark) + public static class BenchmarkState { + + public GenericApplicationContext context; + + @Param({"ResourceAnnotatedTestBean", "AutowiredAnnotatedTestBean"}) + public String testBeanClass; + + @Param({"true", "false"}) + public boolean overridden; + + @Setup + public void setup() { + RootBeanDefinition rbd; + this.context = new GenericApplicationContext(); + AnnotationConfigUtils.registerAnnotationConfigProcessors(this.context); + this.context.refresh(); + if (this.testBeanClass.equals("ResourceAnnotatedTestBean")) { + rbd = new RootBeanDefinition(ResourceAnnotatedTestBean.class); + } + else { + rbd = new RootBeanDefinition(AutowiredAnnotatedTestBean.class); + } + rbd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + if (this.overridden) { + rbd.getPropertyValues().add("spouse", new RuntimeBeanReference("spouse")); + } + this.context.registerBeanDefinition("test", rbd); + this.context.registerBeanDefinition("spouse", new RootBeanDefinition(TestBean.class)); + } + } + + @Benchmark + public ITestBean prototypeCreation(BenchmarkState state) { + TestBean tb = state.context.getBean("test", TestBean.class); + return tb.getSpouse(); + } + + + private static class ResourceAnnotatedTestBean extends org.springframework.beans.testfixture.beans.TestBean { + + @Override + @Resource + @SuppressWarnings("deprecation") + @org.springframework.beans.factory.annotation.Required + public void setSpouse(ITestBean spouse) { + super.setSpouse(spouse); + } + } + + private static class AutowiredAnnotatedTestBean extends TestBean { + + @Override + @Autowired + @SuppressWarnings("deprecation") + @org.springframework.beans.factory.annotation.Required + public void setSpouse(ITestBean spouse) { + super.setSpouse(spouse); + } + } + +} diff --git a/spring-context/src/jmh/java/org/springframework/context/expression/ApplicationContextExpressionBenchmark.java b/spring-context/src/jmh/java/org/springframework/context/expression/ApplicationContextExpressionBenchmark.java new file mode 100644 index 0000000..51a568c --- /dev/null +++ b/spring-context/src/jmh/java/org/springframework/context/expression/ApplicationContextExpressionBenchmark.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.expression; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.infra.Blackhole; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.support.GenericApplicationContext; + +/** + * Benchmark for application context expressions resolution during prototype bean creation. + * @author Brian Clozel + */ +@BenchmarkMode(Mode.Throughput) +public class ApplicationContextExpressionBenchmark { + + @State(Scope.Benchmark) + public static class BenchmarkState { + + public GenericApplicationContext context; + + @Setup + public void setup() { + System.getProperties().put("name", "juergen"); + System.getProperties().put("country", "UK"); + this.context = new GenericApplicationContext(); + RootBeanDefinition rbd = new RootBeanDefinition(TestBean.class); + rbd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + rbd.getConstructorArgumentValues().addGenericArgumentValue("#{systemProperties.name}"); + rbd.getPropertyValues().add("country", "#{systemProperties.country}"); + this.context.registerBeanDefinition("test", rbd); + this.context.refresh(); + } + + @TearDown + public void teardown() { + System.getProperties().remove("country"); + System.getProperties().remove("name"); + } + } + + @Benchmark + public void prototypeCreationWithSystemProperties(BenchmarkState state, Blackhole bh) { + TestBean tb = (TestBean) state.context.getBean("test"); + bh.consume(tb.getName()); + bh.consume(tb.getCountry()); + } +} diff --git a/spring-context/src/main/java/org/springframework/cache/Cache.java b/spring-context/src/main/java/org/springframework/cache/Cache.java new file mode 100644 index 0000000..dea4505 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/Cache.java @@ -0,0 +1,246 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache; + +import java.util.concurrent.Callable; + +import org.springframework.lang.Nullable; + +/** + * Interface that defines common cache operations. + * + * Note: Due to the generic use of caching, it is recommended that + * implementations allow storage of null values (for example to + * cache methods that return {@code null}). + * + * @author Costin Leau + * @author Juergen Hoeller + * @author Stephane Nicoll + * @since 3.1 + */ +public interface Cache { + + /** + * Return the cache name. + */ + String getName(); + + /** + * Return the underlying native cache provider. + */ + Object getNativeCache(); + + /** + * Return the value to which this cache maps the specified key. + *

    Returns {@code null} if the cache contains no mapping for this key; + * otherwise, the cached value (which may be {@code null} itself) will + * be returned in a {@link ValueWrapper}. + * @param key the key whose associated value is to be returned + * @return the value to which this cache maps the specified key, + * contained within a {@link ValueWrapper} which may also hold + * a cached {@code null} value. A straight {@code null} being + * returned means that the cache contains no mapping for this key. + * @see #get(Object, Class) + * @see #get(Object, Callable) + */ + @Nullable + ValueWrapper get(Object key); + + /** + * Return the value to which this cache maps the specified key, + * generically specifying a type that return value will be cast to. + *

    Note: This variant of {@code get} does not allow for differentiating + * between a cached {@code null} value and no cache entry found at all. + * Use the standard {@link #get(Object)} variant for that purpose instead. + * @param key the key whose associated value is to be returned + * @param type the required type of the returned value (may be + * {@code null} to bypass a type check; in case of a {@code null} + * value found in the cache, the specified type is irrelevant) + * @return the value to which this cache maps the specified key + * (which may be {@code null} itself), or also {@code null} if + * the cache contains no mapping for this key + * @throws IllegalStateException if a cache entry has been found + * but failed to match the specified type + * @since 4.0 + * @see #get(Object) + */ + @Nullable + T get(Object key, @Nullable Class type); + + /** + * Return the value to which this cache maps the specified key, obtaining + * that value from {@code valueLoader} if necessary. This method provides + * a simple substitute for the conventional "if cached, return; otherwise + * create, cache and return" pattern. + *

    If possible, implementations should ensure that the loading operation + * is synchronized so that the specified {@code valueLoader} is only called + * once in case of concurrent access on the same key. + *

    If the {@code valueLoader} throws an exception, it is wrapped in + * a {@link ValueRetrievalException} + * @param key the key whose associated value is to be returned + * @return the value to which this cache maps the specified key + * @throws ValueRetrievalException if the {@code valueLoader} throws an exception + * @since 4.3 + * @see #get(Object) + */ + @Nullable + T get(Object key, Callable valueLoader); + + /** + * Associate the specified value with the specified key in this cache. + *

    If the cache previously contained a mapping for this key, the old + * value is replaced by the specified value. + *

    Actual registration may be performed in an asynchronous or deferred + * fashion, with subsequent lookups possibly not seeing the entry yet. + * This may for example be the case with transactional cache decorators. + * Use {@link #putIfAbsent} for guaranteed immediate registration. + * @param key the key with which the specified value is to be associated + * @param value the value to be associated with the specified key + * @see #putIfAbsent(Object, Object) + */ + void put(Object key, @Nullable Object value); + + /** + * Atomically associate the specified value with the specified key in this cache + * if it is not set already. + *

    This is equivalent to: + *

    
    +	 * ValueWrapper existingValue = cache.get(key);
    +	 * if (existingValue == null) {
    +	 *     cache.put(key, value);
    +	 * }
    +	 * return existingValue;
    +	 * 
    + * except that the action is performed atomically. While all out-of-the-box + * {@link CacheManager} implementations are able to perform the put atomically, + * the operation may also be implemented in two steps, e.g. with a check for + * presence and a subsequent put, in a non-atomic way. Check the documentation + * of the native cache implementation that you are using for more details. + *

    The default implementation delegates to {@link #get(Object)} and + * {@link #put(Object, Object)} along the lines of the code snippet above. + * @param key the key with which the specified value is to be associated + * @param value the value to be associated with the specified key + * @return the value to which this cache maps the specified key (which may be + * {@code null} itself), or also {@code null} if the cache did not contain any + * mapping for that key prior to this call. Returning {@code null} is therefore + * an indicator that the given {@code value} has been associated with the key. + * @since 4.1 + * @see #put(Object, Object) + */ + @Nullable + default ValueWrapper putIfAbsent(Object key, @Nullable Object value) { + ValueWrapper existingValue = get(key); + if (existingValue == null) { + put(key, value); + } + return existingValue; + } + + /** + * Evict the mapping for this key from this cache if it is present. + *

    Actual eviction may be performed in an asynchronous or deferred + * fashion, with subsequent lookups possibly still seeing the entry. + * This may for example be the case with transactional cache decorators. + * Use {@link #evictIfPresent} for guaranteed immediate removal. + * @param key the key whose mapping is to be removed from the cache + * @see #evictIfPresent(Object) + */ + void evict(Object key); + + /** + * Evict the mapping for this key from this cache if it is present, + * expecting the key to be immediately invisible for subsequent lookups. + *

    The default implementation delegates to {@link #evict(Object)}, + * returning {@code false} for not-determined prior presence of the key. + * Cache providers and in particular cache decorators are encouraged + * to perform immediate eviction if possible (e.g. in case of generally + * deferred cache operations within a transaction) and to reliably + * determine prior presence of the given key. + * @param key the key whose mapping is to be removed from the cache + * @return {@code true} if the cache was known to have a mapping for + * this key before, {@code false} if it did not (or if prior presence + * could not be determined) + * @since 5.2 + * @see #evict(Object) + */ + default boolean evictIfPresent(Object key) { + evict(key); + return false; + } + + /** + * Clear the cache through removing all mappings. + *

    Actual clearing may be performed in an asynchronous or deferred + * fashion, with subsequent lookups possibly still seeing the entries. + * This may for example be the case with transactional cache decorators. + * Use {@link #invalidate()} for guaranteed immediate removal of entries. + * @see #invalidate() + */ + void clear(); + + /** + * Invalidate the cache through removing all mappings, expecting all + * entries to be immediately invisible for subsequent lookups. + * @return {@code true} if the cache was known to have mappings before, + * {@code false} if it did not (or if prior presence of entries could + * not be determined) + * @since 5.2 + * @see #clear() + */ + default boolean invalidate() { + clear(); + return false; + } + + + /** + * A (wrapper) object representing a cache value. + */ + @FunctionalInterface + interface ValueWrapper { + + /** + * Return the actual value in the cache. + */ + @Nullable + Object get(); + } + + + /** + * Wrapper exception to be thrown from {@link #get(Object, Callable)} + * in case of the value loader callback failing with an exception. + * @since 4.3 + */ + @SuppressWarnings("serial") + class ValueRetrievalException extends RuntimeException { + + @Nullable + private final Object key; + + public ValueRetrievalException(@Nullable Object key, Callable loader, Throwable ex) { + super(String.format("Value for key '%s' could not be loaded using '%s'", key, loader), ex); + this.key = key; + } + + @Nullable + public Object getKey() { + return this.key; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/CacheManager.java b/spring-context/src/main/java/org/springframework/cache/CacheManager.java new file mode 100644 index 0000000..833715c --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/CacheManager.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache; + +import java.util.Collection; + +import org.springframework.lang.Nullable; + +/** + * Spring's central cache manager SPI. + * + *

    Allows for retrieving named {@link Cache} regions. + * + * @author Costin Leau + * @author Sam Brannen + * @since 3.1 + */ +public interface CacheManager { + + /** + * Get the cache associated with the given name. + *

    Note that the cache may be lazily created at runtime if the + * native provider supports it. + * @param name the cache identifier (must not be {@code null}) + * @return the associated cache, or {@code null} if such a cache + * does not exist or could be not created + */ + @Nullable + Cache getCache(String name); + + /** + * Get a collection of the cache names known by this manager. + * @return the names of all caches known by the cache manager + */ + Collection getCacheNames(); + +} diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/AbstractCachingConfiguration.java b/spring-context/src/main/java/org/springframework/cache/annotation/AbstractCachingConfiguration.java new file mode 100644 index 0000000..c49d8d2 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/annotation/AbstractCachingConfiguration.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.annotation; + +import java.util.Collection; +import java.util.function.Supplier; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.CacheManager; +import org.springframework.cache.interceptor.CacheErrorHandler; +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportAware; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; + +/** + * Abstract base {@code @Configuration} class providing common structure + * for enabling Spring's annotation-driven cache management capability. + * + * @author Chris Beams + * @author Stephane Nicoll + * @author Juergen Hoeller + * @since 3.1 + * @see EnableCaching + */ +@Configuration(proxyBeanMethods = false) +public abstract class AbstractCachingConfiguration implements ImportAware { + + @Nullable + protected AnnotationAttributes enableCaching; + + @Nullable + protected Supplier cacheManager; + + @Nullable + protected Supplier cacheResolver; + + @Nullable + protected Supplier keyGenerator; + + @Nullable + protected Supplier errorHandler; + + + @Override + public void setImportMetadata(AnnotationMetadata importMetadata) { + this.enableCaching = AnnotationAttributes.fromMap( + importMetadata.getAnnotationAttributes(EnableCaching.class.getName(), false)); + if (this.enableCaching == null) { + throw new IllegalArgumentException( + "@EnableCaching is not present on importing class " + importMetadata.getClassName()); + } + } + + @Autowired(required = false) + void setConfigurers(Collection configurers) { + if (CollectionUtils.isEmpty(configurers)) { + return; + } + if (configurers.size() > 1) { + throw new IllegalStateException(configurers.size() + " implementations of " + + "CachingConfigurer were found when only 1 was expected. " + + "Refactor the configuration such that CachingConfigurer is " + + "implemented only once or not at all."); + } + CachingConfigurer configurer = configurers.iterator().next(); + useCachingConfigurer(configurer); + } + + /** + * Extract the configuration from the nominated {@link CachingConfigurer}. + */ + protected void useCachingConfigurer(CachingConfigurer config) { + this.cacheManager = config::cacheManager; + this.cacheResolver = config::cacheResolver; + this.keyGenerator = config::keyGenerator; + this.errorHandler = config::errorHandler; + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/AnnotationCacheOperationSource.java b/spring-context/src/main/java/org/springframework/cache/annotation/AnnotationCacheOperationSource.java new file mode 100644 index 0000000..5d34d0e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/annotation/AnnotationCacheOperationSource.java @@ -0,0 +1,201 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.annotation; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.cache.interceptor.AbstractFallbackCacheOperationSource; +import org.springframework.cache.interceptor.CacheOperation; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Implementation of the {@link org.springframework.cache.interceptor.CacheOperationSource + * CacheOperationSource} interface for working with caching metadata in annotation format. + * + *

    This class reads Spring's {@link Cacheable}, {@link CachePut} and {@link CacheEvict} + * annotations and exposes corresponding caching operation definition to Spring's cache + * infrastructure. This class may also serve as base class for a custom + * {@code CacheOperationSource}. + * + * @author Costin Leau + * @author Juergen Hoeller + * @author Stephane Nicoll + * @since 3.1 + */ +@SuppressWarnings("serial") +public class AnnotationCacheOperationSource extends AbstractFallbackCacheOperationSource implements Serializable { + + private final boolean publicMethodsOnly; + + private final Set annotationParsers; + + + /** + * Create a default AnnotationCacheOperationSource, supporting public methods + * that carry the {@code Cacheable} and {@code CacheEvict} annotations. + */ + public AnnotationCacheOperationSource() { + this(true); + } + + /** + * Create a default {@code AnnotationCacheOperationSource}, supporting public methods + * that carry the {@code Cacheable} and {@code CacheEvict} annotations. + * @param publicMethodsOnly whether to support only annotated public methods + * typically for use with proxy-based AOP), or protected/private methods as well + * (typically used with AspectJ class weaving) + */ + public AnnotationCacheOperationSource(boolean publicMethodsOnly) { + this.publicMethodsOnly = publicMethodsOnly; + this.annotationParsers = Collections.singleton(new SpringCacheAnnotationParser()); + } + + /** + * Create a custom AnnotationCacheOperationSource. + * @param annotationParser the CacheAnnotationParser to use + */ + public AnnotationCacheOperationSource(CacheAnnotationParser annotationParser) { + this.publicMethodsOnly = true; + Assert.notNull(annotationParser, "CacheAnnotationParser must not be null"); + this.annotationParsers = Collections.singleton(annotationParser); + } + + /** + * Create a custom AnnotationCacheOperationSource. + * @param annotationParsers the CacheAnnotationParser to use + */ + public AnnotationCacheOperationSource(CacheAnnotationParser... annotationParsers) { + this.publicMethodsOnly = true; + Assert.notEmpty(annotationParsers, "At least one CacheAnnotationParser needs to be specified"); + this.annotationParsers = new LinkedHashSet<>(Arrays.asList(annotationParsers)); + } + + /** + * Create a custom AnnotationCacheOperationSource. + * @param annotationParsers the CacheAnnotationParser to use + */ + public AnnotationCacheOperationSource(Set annotationParsers) { + this.publicMethodsOnly = true; + Assert.notEmpty(annotationParsers, "At least one CacheAnnotationParser needs to be specified"); + this.annotationParsers = annotationParsers; + } + + + @Override + public boolean isCandidateClass(Class targetClass) { + for (CacheAnnotationParser parser : this.annotationParsers) { + if (parser.isCandidateClass(targetClass)) { + return true; + } + } + return false; + } + + @Override + @Nullable + protected Collection findCacheOperations(Class clazz) { + return determineCacheOperations(parser -> parser.parseCacheAnnotations(clazz)); + } + + @Override + @Nullable + protected Collection findCacheOperations(Method method) { + return determineCacheOperations(parser -> parser.parseCacheAnnotations(method)); + } + + /** + * Determine the cache operation(s) for the given {@link CacheOperationProvider}. + *

    This implementation delegates to configured + * {@link CacheAnnotationParser CacheAnnotationParsers} + * for parsing known annotations into Spring's metadata attribute class. + *

    Can be overridden to support custom annotations that carry caching metadata. + * @param provider the cache operation provider to use + * @return the configured caching operations, or {@code null} if none found + */ + @Nullable + protected Collection determineCacheOperations(CacheOperationProvider provider) { + Collection ops = null; + for (CacheAnnotationParser parser : this.annotationParsers) { + Collection annOps = provider.getCacheOperations(parser); + if (annOps != null) { + if (ops == null) { + ops = annOps; + } + else { + Collection combined = new ArrayList<>(ops.size() + annOps.size()); + combined.addAll(ops); + combined.addAll(annOps); + ops = combined; + } + } + } + return ops; + } + + /** + * By default, only public methods can be made cacheable. + */ + @Override + protected boolean allowPublicMethodsOnly() { + return this.publicMethodsOnly; + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof AnnotationCacheOperationSource)) { + return false; + } + AnnotationCacheOperationSource otherCos = (AnnotationCacheOperationSource) other; + return (this.annotationParsers.equals(otherCos.annotationParsers) && + this.publicMethodsOnly == otherCos.publicMethodsOnly); + } + + @Override + public int hashCode() { + return this.annotationParsers.hashCode(); + } + + + /** + * Callback interface providing {@link CacheOperation} instance(s) based on + * a given {@link CacheAnnotationParser}. + */ + @FunctionalInterface + protected interface CacheOperationProvider { + + /** + * Return the {@link CacheOperation} instance(s) provided by the specified parser. + * @param parser the parser to use + * @return the cache operations, or {@code null} if none found + */ + @Nullable + Collection getCacheOperations(CacheAnnotationParser parser); + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CacheAnnotationParser.java b/spring-context/src/main/java/org/springframework/cache/annotation/CacheAnnotationParser.java new file mode 100644 index 0000000..10231a1 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CacheAnnotationParser.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.annotation; + +import java.lang.reflect.Method; +import java.util.Collection; + +import org.springframework.cache.interceptor.CacheOperation; +import org.springframework.lang.Nullable; + +/** + * Strategy interface for parsing known caching annotation types. + * {@link AnnotationCacheOperationSource} delegates to such parsers + * for supporting specific annotation types such as Spring's own + * {@link Cacheable}, {@link CachePut} and{@link CacheEvict}. + * + * @author Costin Leau + * @author Stephane Nicoll + * @author Juergen Hoeller + * @since 3.1 + * @see AnnotationCacheOperationSource + * @see SpringCacheAnnotationParser + */ +public interface CacheAnnotationParser { + + /** + * Determine whether the given class is a candidate for cache operations + * in the annotation format of this {@code CacheAnnotationParser}. + *

    If this method returns {@code false}, the methods on the given class + * will not get traversed for {@code #parseCacheAnnotations} introspection. + * Returning {@code false} is therefore an optimization for non-affected + * classes, whereas {@code true} simply means that the class needs to get + * fully introspected for each method on the given class individually. + * @param targetClass the class to introspect + * @return {@code false} if the class is known to have no cache operation + * annotations at class or method level; {@code true} otherwise. The default + * implementation returns {@code true}, leading to regular introspection. + * @since 5.2 + */ + default boolean isCandidateClass(Class targetClass) { + return true; + } + + /** + * Parse the cache definition for the given class, + * based on an annotation type understood by this parser. + *

    This essentially parses a known cache annotation into Spring's metadata + * attribute class. Returns {@code null} if the class is not cacheable. + * @param type the annotated class + * @return the configured caching operation, or {@code null} if none found + * @see AnnotationCacheOperationSource#findCacheOperations(Class) + */ + @Nullable + Collection parseCacheAnnotations(Class type); + + /** + * Parse the cache definition for the given method, + * based on an annotation type understood by this parser. + *

    This essentially parses a known cache annotation into Spring's metadata + * attribute class. Returns {@code null} if the method is not cacheable. + * @param method the annotated method + * @return the configured caching operation, or {@code null} if none found + * @see AnnotationCacheOperationSource#findCacheOperations(Method) + */ + @Nullable + Collection parseCacheAnnotations(Method method); + +} diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CacheConfig.java b/spring-context/src/main/java/org/springframework/cache/annotation/CacheConfig.java new file mode 100644 index 0000000..234f353 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CacheConfig.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * {@code @CacheConfig} provides a mechanism for sharing common cache-related + * settings at the class level. + * + *

    When this annotation is present on a given class, it provides a set + * of default settings for any cache operation defined in that class. + * + * @author Stephane Nicoll + * @author Sam Brannen + * @since 4.1 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface CacheConfig { + + /** + * Names of the default caches to consider for caching operations defined + * in the annotated class. + *

    If none is set at the operation level, these are used instead of the default. + *

    May be used to determine the target cache (or caches), matching the + * qualifier value or the bean names of a specific bean definition. + */ + String[] cacheNames() default {}; + + /** + * The bean name of the default {@link org.springframework.cache.interceptor.KeyGenerator} to + * use for the class. + *

    If none is set at the operation level, this one is used instead of the default. + *

    The key generator is mutually exclusive with the use of a custom key. When such key is + * defined for the operation, the value of this key generator is ignored. + */ + String keyGenerator() default ""; + + /** + * The bean name of the custom {@link org.springframework.cache.CacheManager} to use to + * create a default {@link org.springframework.cache.interceptor.CacheResolver} if none + * is set already. + *

    If no resolver and no cache manager are set at the operation level, and no cache + * resolver is set via {@link #cacheResolver}, this one is used instead of the default. + * @see org.springframework.cache.interceptor.SimpleCacheResolver + */ + String cacheManager() default ""; + + /** + * The bean name of the custom {@link org.springframework.cache.interceptor.CacheResolver} to use. + *

    If no resolver and no cache manager are set at the operation level, this one is used + * instead of the default. + */ + String cacheResolver() default ""; + +} diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CacheEvict.java b/spring-context/src/main/java/org/springframework/cache/annotation/CacheEvict.java new file mode 100644 index 0000000..5aa3978 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CacheEvict.java @@ -0,0 +1,150 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * Annotation indicating that a method (or all methods on a class) triggers a + * {@link org.springframework.cache.Cache#evict(Object) cache evict} operation. + * + *

    This annotation may be used as a meta-annotation to create custom + * composed annotations with attribute overrides. + * + * @author Costin Leau + * @author Stephane Nicoll + * @author Sam Brannen + * @since 3.1 + * @see CacheConfig + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface CacheEvict { + + /** + * Alias for {@link #cacheNames}. + */ + @AliasFor("cacheNames") + String[] value() default {}; + + /** + * Names of the caches to use for the cache eviction operation. + *

    Names may be used to determine the target cache (or caches), matching + * the qualifier value or bean name of a specific bean definition. + * @since 4.2 + * @see #value + * @see CacheConfig#cacheNames + */ + @AliasFor("value") + String[] cacheNames() default {}; + + /** + * Spring Expression Language (SpEL) expression for computing the key dynamically. + *

    Default is {@code ""}, meaning all method parameters are considered as a key, + * unless a custom {@link #keyGenerator} has been set. + *

    The SpEL expression evaluates against a dedicated context that provides the + * following meta-data: + *

      + *
    • {@code #result} for a reference to the result of the method invocation, which + * can only be used if {@link #beforeInvocation()} is {@code false}. For supported + * wrappers such as {@code Optional}, {@code #result} refers to the actual object, + * not the wrapper
    • + *
    • {@code #root.method}, {@code #root.target}, and {@code #root.caches} for + * references to the {@link java.lang.reflect.Method method}, target object, and + * affected cache(s) respectively.
    • + *
    • Shortcuts for the method name ({@code #root.methodName}) and target class + * ({@code #root.targetClass}) are also available. + *
    • Method arguments can be accessed by index. For instance the second argument + * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments + * can also be accessed by name if that information is available.
    • + *
    + */ + String key() default ""; + + /** + * The bean name of the custom {@link org.springframework.cache.interceptor.KeyGenerator} + * to use. + *

    Mutually exclusive with the {@link #key} attribute. + * @see CacheConfig#keyGenerator + */ + String keyGenerator() default ""; + + /** + * The bean name of the custom {@link org.springframework.cache.CacheManager} to use to + * create a default {@link org.springframework.cache.interceptor.CacheResolver} if none + * is set already. + *

    Mutually exclusive with the {@link #cacheResolver} attribute. + * @see org.springframework.cache.interceptor.SimpleCacheResolver + * @see CacheConfig#cacheManager + */ + String cacheManager() default ""; + + /** + * The bean name of the custom {@link org.springframework.cache.interceptor.CacheResolver} + * to use. + * @see CacheConfig#cacheResolver + */ + String cacheResolver() default ""; + + /** + * Spring Expression Language (SpEL) expression used for making the cache + * eviction operation conditional. + *

    Default is {@code ""}, meaning the cache eviction is always performed. + *

    The SpEL expression evaluates against a dedicated context that provides the + * following meta-data: + *

      + *
    • {@code #root.method}, {@code #root.target}, and {@code #root.caches} for + * references to the {@link java.lang.reflect.Method method}, target object, and + * affected cache(s) respectively.
    • + *
    • Shortcuts for the method name ({@code #root.methodName}) and target class + * ({@code #root.targetClass}) are also available. + *
    • Method arguments can be accessed by index. For instance the second argument + * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments + * can also be accessed by name if that information is available.
    • + *
    + */ + String condition() default ""; + + /** + * Whether all the entries inside the cache(s) are removed. + *

    By default, only the value under the associated key is removed. + *

    Note that setting this parameter to {@code true} and specifying a + * {@link #key} is not allowed. + */ + boolean allEntries() default false; + + /** + * Whether the eviction should occur before the method is invoked. + *

    Setting this attribute to {@code true}, causes the eviction to + * occur irrespective of the method outcome (i.e., whether it threw an + * exception or not). + *

    Defaults to {@code false}, meaning that the cache eviction operation + * will occur after the advised method is invoked successfully (i.e. + * only if the invocation did not throw an exception). + */ + boolean beforeInvocation() default false; + +} diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CachePut.java b/spring-context/src/main/java/org/springframework/cache/annotation/CachePut.java new file mode 100644 index 0000000..8743df7 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CachePut.java @@ -0,0 +1,161 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * Annotation indicating that a method (or all methods on a class) triggers a + * {@link org.springframework.cache.Cache#put(Object, Object) cache put} operation. + * + *

    In contrast to the {@link Cacheable @Cacheable} annotation, this annotation + * does not cause the advised method to be skipped. Rather, it always causes the + * method to be invoked and its result to be stored in the associated cache. Note + * that Java8's {@code Optional} return types are automatically handled and its + * content is stored in the cache if present. + * + *

    This annotation may be used as a meta-annotation to create custom + * composed annotations with attribute overrides. + * + * @author Costin Leau + * @author Phillip Webb + * @author Stephane Nicoll + * @author Sam Brannen + * @since 3.1 + * @see CacheConfig + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface CachePut { + + /** + * Alias for {@link #cacheNames}. + */ + @AliasFor("cacheNames") + String[] value() default {}; + + /** + * Names of the caches to use for the cache put operation. + *

    Names may be used to determine the target cache (or caches), matching + * the qualifier value or bean name of a specific bean definition. + * @since 4.2 + * @see #value + * @see CacheConfig#cacheNames + */ + @AliasFor("value") + String[] cacheNames() default {}; + + /** + * Spring Expression Language (SpEL) expression for computing the key dynamically. + *

    Default is {@code ""}, meaning all method parameters are considered as a key, + * unless a custom {@link #keyGenerator} has been set. + *

    The SpEL expression evaluates against a dedicated context that provides the + * following meta-data: + *

      + *
    • {@code #result} for a reference to the result of the method invocation. For + * supported wrappers such as {@code Optional}, {@code #result} refers to the actual + * object, not the wrapper
    • + *
    • {@code #root.method}, {@code #root.target}, and {@code #root.caches} for + * references to the {@link java.lang.reflect.Method method}, target object, and + * affected cache(s) respectively.
    • + *
    • Shortcuts for the method name ({@code #root.methodName}) and target class + * ({@code #root.targetClass}) are also available. + *
    • Method arguments can be accessed by index. For instance the second argument + * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments + * can also be accessed by name if that information is available.
    • + *
    + */ + String key() default ""; + + /** + * The bean name of the custom {@link org.springframework.cache.interceptor.KeyGenerator} + * to use. + *

    Mutually exclusive with the {@link #key} attribute. + * @see CacheConfig#keyGenerator + */ + String keyGenerator() default ""; + + /** + * The bean name of the custom {@link org.springframework.cache.CacheManager} to use to + * create a default {@link org.springframework.cache.interceptor.CacheResolver} if none + * is set already. + *

    Mutually exclusive with the {@link #cacheResolver} attribute. + * @see org.springframework.cache.interceptor.SimpleCacheResolver + * @see CacheConfig#cacheManager + */ + String cacheManager() default ""; + + /** + * The bean name of the custom {@link org.springframework.cache.interceptor.CacheResolver} + * to use. + * @see CacheConfig#cacheResolver + */ + String cacheResolver() default ""; + + /** + * Spring Expression Language (SpEL) expression used for making the cache + * put operation conditional. + *

    Default is {@code ""}, meaning the method result is always cached. + *

    The SpEL expression evaluates against a dedicated context that provides the + * following meta-data: + *

      + *
    • {@code #root.method}, {@code #root.target}, and {@code #root.caches} for + * references to the {@link java.lang.reflect.Method method}, target object, and + * affected cache(s) respectively.
    • + *
    • Shortcuts for the method name ({@code #root.methodName}) and target class + * ({@code #root.targetClass}) are also available. + *
    • Method arguments can be accessed by index. For instance the second argument + * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments + * can also be accessed by name if that information is available.
    • + *
    + */ + String condition() default ""; + + /** + * Spring Expression Language (SpEL) expression used to veto the cache put operation. + *

    Unlike {@link #condition}, this expression is evaluated after the method + * has been called and can therefore refer to the {@code result}. + *

    Default is {@code ""}, meaning that caching is never vetoed. + *

    The SpEL expression evaluates against a dedicated context that provides the + * following meta-data: + *

      + *
    • {@code #result} for a reference to the result of the method invocation. For + * supported wrappers such as {@code Optional}, {@code #result} refers to the actual + * object, not the wrapper
    • + *
    • {@code #root.method}, {@code #root.target}, and {@code #root.caches} for + * references to the {@link java.lang.reflect.Method method}, target object, and + * affected cache(s) respectively.
    • + *
    • Shortcuts for the method name ({@code #root.methodName}) and target class + * ({@code #root.targetClass}) are also available. + *
    • Method arguments can be accessed by index. For instance the second argument + * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments + * can also be accessed by name if that information is available.
    • + *
    + * @since 3.2 + */ + String unless() default ""; + +} diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java b/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java new file mode 100644 index 0000000..b93cf93 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java @@ -0,0 +1,182 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.Callable; + +import org.springframework.core.annotation.AliasFor; + +/** + * Annotation indicating that the result of invoking a method (or all methods + * in a class) can be cached. + * + *

    Each time an advised method is invoked, caching behavior will be applied, + * checking whether the method has been already invoked for the given arguments. + * A sensible default simply uses the method parameters to compute the key, but + * a SpEL expression can be provided via the {@link #key} attribute, or a custom + * {@link org.springframework.cache.interceptor.KeyGenerator} implementation can + * replace the default one (see {@link #keyGenerator}). + * + *

    If no value is found in the cache for the computed key, the target method + * will be invoked and the returned value stored in the associated cache. Note + * that Java8's {@code Optional} return types are automatically handled and its + * content is stored in the cache if present. + * + *

    This annotation may be used as a meta-annotation to create custom + * composed annotations with attribute overrides. + * + * @author Costin Leau + * @author Phillip Webb + * @author Stephane Nicoll + * @author Sam Brannen + * @since 3.1 + * @see CacheConfig + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface Cacheable { + + /** + * Alias for {@link #cacheNames}. + */ + @AliasFor("cacheNames") + String[] value() default {}; + + /** + * Names of the caches in which method invocation results are stored. + *

    Names may be used to determine the target cache (or caches), matching + * the qualifier value or bean name of a specific bean definition. + * @since 4.2 + * @see #value + * @see CacheConfig#cacheNames + */ + @AliasFor("value") + String[] cacheNames() default {}; + + /** + * Spring Expression Language (SpEL) expression for computing the key dynamically. + *

    Default is {@code ""}, meaning all method parameters are considered as a key, + * unless a custom {@link #keyGenerator} has been configured. + *

    The SpEL expression evaluates against a dedicated context that provides the + * following meta-data: + *

      + *
    • {@code #root.method}, {@code #root.target}, and {@code #root.caches} for + * references to the {@link java.lang.reflect.Method method}, target object, and + * affected cache(s) respectively.
    • + *
    • Shortcuts for the method name ({@code #root.methodName}) and target class + * ({@code #root.targetClass}) are also available. + *
    • Method arguments can be accessed by index. For instance the second argument + * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments + * can also be accessed by name if that information is available.
    • + *
    + */ + String key() default ""; + + /** + * The bean name of the custom {@link org.springframework.cache.interceptor.KeyGenerator} + * to use. + *

    Mutually exclusive with the {@link #key} attribute. + * @see CacheConfig#keyGenerator + */ + String keyGenerator() default ""; + + /** + * The bean name of the custom {@link org.springframework.cache.CacheManager} to use to + * create a default {@link org.springframework.cache.interceptor.CacheResolver} if none + * is set already. + *

    Mutually exclusive with the {@link #cacheResolver} attribute. + * @see org.springframework.cache.interceptor.SimpleCacheResolver + * @see CacheConfig#cacheManager + */ + String cacheManager() default ""; + + /** + * The bean name of the custom {@link org.springframework.cache.interceptor.CacheResolver} + * to use. + * @see CacheConfig#cacheResolver + */ + String cacheResolver() default ""; + + /** + * Spring Expression Language (SpEL) expression used for making the method + * caching conditional. + *

    Default is {@code ""}, meaning the method result is always cached. + *

    The SpEL expression evaluates against a dedicated context that provides the + * following meta-data: + *

      + *
    • {@code #root.method}, {@code #root.target}, and {@code #root.caches} for + * references to the {@link java.lang.reflect.Method method}, target object, and + * affected cache(s) respectively.
    • + *
    • Shortcuts for the method name ({@code #root.methodName}) and target class + * ({@code #root.targetClass}) are also available. + *
    • Method arguments can be accessed by index. For instance the second argument + * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments + * can also be accessed by name if that information is available.
    • + *
    + */ + String condition() default ""; + + /** + * Spring Expression Language (SpEL) expression used to veto method caching. + *

    Unlike {@link #condition}, this expression is evaluated after the method + * has been called and can therefore refer to the {@code result}. + *

    Default is {@code ""}, meaning that caching is never vetoed. + *

    The SpEL expression evaluates against a dedicated context that provides the + * following meta-data: + *

      + *
    • {@code #result} for a reference to the result of the method invocation. For + * supported wrappers such as {@code Optional}, {@code #result} refers to the actual + * object, not the wrapper
    • + *
    • {@code #root.method}, {@code #root.target}, and {@code #root.caches} for + * references to the {@link java.lang.reflect.Method method}, target object, and + * affected cache(s) respectively.
    • + *
    • Shortcuts for the method name ({@code #root.methodName}) and target class + * ({@code #root.targetClass}) are also available. + *
    • Method arguments can be accessed by index. For instance the second argument + * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments + * can also be accessed by name if that information is available.
    • + *
    + * @since 3.2 + */ + String unless() default ""; + + /** + * Synchronize the invocation of the underlying method if several threads are + * attempting to load a value for the same key. The synchronization leads to + * a couple of limitations: + *
      + *
    1. {@link #unless()} is not supported
    2. + *
    3. Only one cache may be specified
    4. + *
    5. No other cache-related operation can be combined
    6. + *
    + * This is effectively a hint and the actual cache provider that you are + * using may not support it in a synchronized fashion. Check your provider + * documentation for more details on the actual semantics. + * @since 4.3 + * @see org.springframework.cache.Cache#get(Object, Callable) + */ + boolean sync() default false; + +} diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/Caching.java b/spring-context/src/main/java/org/springframework/cache/annotation/Caching.java new file mode 100644 index 0000000..7deb26a --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/annotation/Caching.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Group annotation for multiple cache annotations (of different or the same type). + * + *

    This annotation may be used as a meta-annotation to create custom + * composed annotations with attribute overrides. + * + * @author Costin Leau + * @author Chris Beams + * @since 3.1 + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface Caching { + + Cacheable[] cacheable() default {}; + + CachePut[] put() default {}; + + CacheEvict[] evict() default {}; + +} diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurationSelector.java b/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurationSelector.java new file mode 100644 index 0000000..3c55c95 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurationSelector.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.annotation; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.context.annotation.AdviceMode; +import org.springframework.context.annotation.AdviceModeImportSelector; +import org.springframework.context.annotation.AutoProxyRegistrar; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * Selects which implementation of {@link AbstractCachingConfiguration} should + * be used based on the value of {@link EnableCaching#mode} on the importing + * {@code @Configuration} class. + * + *

    Detects the presence of JSR-107 and enables JCache support accordingly. + * + * @author Chris Beams + * @author Stephane Nicoll + * @since 3.1 + * @see EnableCaching + * @see ProxyCachingConfiguration + */ +public class CachingConfigurationSelector extends AdviceModeImportSelector { + + private static final String PROXY_JCACHE_CONFIGURATION_CLASS = + "org.springframework.cache.jcache.config.ProxyJCacheConfiguration"; + + private static final String CACHE_ASPECT_CONFIGURATION_CLASS_NAME = + "org.springframework.cache.aspectj.AspectJCachingConfiguration"; + + private static final String JCACHE_ASPECT_CONFIGURATION_CLASS_NAME = + "org.springframework.cache.aspectj.AspectJJCacheConfiguration"; + + + private static final boolean jsr107Present; + + private static final boolean jcacheImplPresent; + + static { + ClassLoader classLoader = CachingConfigurationSelector.class.getClassLoader(); + jsr107Present = ClassUtils.isPresent("javax.cache.Cache", classLoader); + jcacheImplPresent = ClassUtils.isPresent(PROXY_JCACHE_CONFIGURATION_CLASS, classLoader); + } + + + /** + * Returns {@link ProxyCachingConfiguration} or {@code AspectJCachingConfiguration} + * for {@code PROXY} and {@code ASPECTJ} values of {@link EnableCaching#mode()}, + * respectively. Potentially includes corresponding JCache configuration as well. + */ + @Override + public String[] selectImports(AdviceMode adviceMode) { + switch (adviceMode) { + case PROXY: + return getProxyImports(); + case ASPECTJ: + return getAspectJImports(); + default: + return null; + } + } + + /** + * Return the imports to use if the {@link AdviceMode} is set to {@link AdviceMode#PROXY}. + *

    Take care of adding the necessary JSR-107 import if it is available. + */ + private String[] getProxyImports() { + List result = new ArrayList<>(3); + result.add(AutoProxyRegistrar.class.getName()); + result.add(ProxyCachingConfiguration.class.getName()); + if (jsr107Present && jcacheImplPresent) { + result.add(PROXY_JCACHE_CONFIGURATION_CLASS); + } + return StringUtils.toStringArray(result); + } + + /** + * Return the imports to use if the {@link AdviceMode} is set to {@link AdviceMode#ASPECTJ}. + *

    Take care of adding the necessary JSR-107 import if it is available. + */ + private String[] getAspectJImports() { + List result = new ArrayList<>(2); + result.add(CACHE_ASPECT_CONFIGURATION_CLASS_NAME); + if (jsr107Present && jcacheImplPresent) { + result.add(JCACHE_ASPECT_CONFIGURATION_CLASS_NAME); + } + return StringUtils.toStringArray(result); + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurer.java b/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurer.java new file mode 100644 index 0000000..b7a609e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurer.java @@ -0,0 +1,138 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.annotation; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.interceptor.CacheErrorHandler; +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.lang.Nullable; + +/** + * Interface to be implemented by @{@link org.springframework.context.annotation.Configuration + * Configuration} classes annotated with @{@link EnableCaching} that wish or need to + * specify explicitly how caches are resolved and how keys are generated for annotation-driven + * cache management. Consider extending {@link CachingConfigurerSupport}, which provides a + * stub implementation of all interface methods. + * + *

    See @{@link EnableCaching} for general examples and context; see + * {@link #cacheManager()}, {@link #cacheResolver()} and {@link #keyGenerator()} + * for detailed instructions. + * + * @author Chris Beams + * @author Stephane Nicoll + * @since 3.1 + * @see EnableCaching + * @see CachingConfigurerSupport + */ +public interface CachingConfigurer { + + /** + * Return the cache manager bean to use for annotation-driven cache + * management. A default {@link CacheResolver} will be initialized + * behind the scenes with this cache manager. For more fine-grained + * management of the cache resolution, consider setting the + * {@link CacheResolver} directly. + *

    Implementations must explicitly declare + * {@link org.springframework.context.annotation.Bean @Bean}, e.g. + *

    +	 * @Configuration
    +	 * @EnableCaching
    +	 * public class AppConfig extends CachingConfigurerSupport {
    +	 *     @Bean // important!
    +	 *     @Override
    +	 *     public CacheManager cacheManager() {
    +	 *         // configure and return CacheManager instance
    +	 *     }
    +	 *     // ...
    +	 * }
    +	 * 
    + * See @{@link EnableCaching} for more complete examples. + */ + @Nullable + CacheManager cacheManager(); + + /** + * Return the {@link CacheResolver} bean to use to resolve regular caches for + * annotation-driven cache management. This is an alternative and more powerful + * option of specifying the {@link CacheManager} to use. + *

    If both a {@link #cacheManager()} and {@code #cacheResolver()} are set, + * the cache manager is ignored. + *

    Implementations must explicitly declare + * {@link org.springframework.context.annotation.Bean @Bean}, e.g. + *

    +	 * @Configuration
    +	 * @EnableCaching
    +	 * public class AppConfig extends CachingConfigurerSupport {
    +	 *     @Bean // important!
    +	 *     @Override
    +	 *     public CacheResolver cacheResolver() {
    +	 *         // configure and return CacheResolver instance
    +	 *     }
    +	 *     // ...
    +	 * }
    +	 * 
    + * See {@link EnableCaching} for more complete examples. + */ + @Nullable + CacheResolver cacheResolver(); + + /** + * Return the key generator bean to use for annotation-driven cache management. + * Implementations must explicitly declare + * {@link org.springframework.context.annotation.Bean @Bean}, e.g. + *
    +	 * @Configuration
    +	 * @EnableCaching
    +	 * public class AppConfig extends CachingConfigurerSupport {
    +	 *     @Bean // important!
    +	 *     @Override
    +	 *     public KeyGenerator keyGenerator() {
    +	 *         // configure and return KeyGenerator instance
    +	 *     }
    +	 *     // ...
    +	 * }
    +	 * 
    + * See @{@link EnableCaching} for more complete examples. + */ + @Nullable + KeyGenerator keyGenerator(); + + /** + * Return the {@link CacheErrorHandler} to use to handle cache-related errors. + *

    By default,{@link org.springframework.cache.interceptor.SimpleCacheErrorHandler} + * is used and simply throws the exception back at the client. + *

    Implementations must explicitly declare + * {@link org.springframework.context.annotation.Bean @Bean}, e.g. + *

    +	 * @Configuration
    +	 * @EnableCaching
    +	 * public class AppConfig extends CachingConfigurerSupport {
    +	 *     @Bean // important!
    +	 *     @Override
    +	 *     public CacheErrorHandler errorHandler() {
    +	 *         // configure and return CacheErrorHandler instance
    +	 *     }
    +	 *     // ...
    +	 * }
    +	 * 
    + * See @{@link EnableCaching} for more complete examples. + */ + @Nullable + CacheErrorHandler errorHandler(); + +} diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurerSupport.java b/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurerSupport.java new file mode 100644 index 0000000..f077742 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurerSupport.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.annotation; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.interceptor.CacheErrorHandler; +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.lang.Nullable; + +/** + * An implementation of {@link CachingConfigurer} with empty methods allowing + * sub-classes to override only the methods they're interested in. + * + * @author Stephane Nicoll + * @since 4.1 + * @see CachingConfigurer + */ +public class CachingConfigurerSupport implements CachingConfigurer { + + @Override + @Nullable + public CacheManager cacheManager() { + return null; + } + + @Override + @Nullable + public CacheResolver cacheResolver() { + return null; + } + + @Override + @Nullable + public KeyGenerator keyGenerator() { + return null; + } + + @Override + @Nullable + public CacheErrorHandler errorHandler() { + return null; + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/EnableCaching.java b/spring-context/src/main/java/org/springframework/cache/annotation/EnableCaching.java new file mode 100644 index 0000000..8594802 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/annotation/EnableCaching.java @@ -0,0 +1,207 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.AdviceMode; +import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; + +/** + * Enables Spring's annotation-driven cache management capability, similar to the + * support found in Spring's {@code } XML namespace. To be used together + * with @{@link org.springframework.context.annotation.Configuration Configuration} + * classes as follows: + * + *
    + * @Configuration
    + * @EnableCaching
    + * public class AppConfig {
    + *
    + *     @Bean
    + *     public MyService myService() {
    + *         // configure and return a class having @Cacheable methods
    + *         return new MyService();
    + *     }
    + *
    + *     @Bean
    + *     public CacheManager cacheManager() {
    + *         // configure and return an implementation of Spring's CacheManager SPI
    + *         SimpleCacheManager cacheManager = new SimpleCacheManager();
    + *         cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache("default")));
    + *         return cacheManager;
    + *     }
    + * }
    + * + *

    For reference, the example above can be compared to the following Spring XML + * configuration: + * + *

    + * <beans>
    + *
    + *     <cache:annotation-driven/>
    + *
    + *     <bean id="myService" class="com.foo.MyService"/>
    + *
    + *     <bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
    + *         <property name="caches">
    + *             <set>
    + *                 <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean">
    + *                     <property name="name" value="default"/>
    + *                 </bean>
    + *             </set>
    + *         </property>
    + *     </bean>
    + *
    + * </beans>
    + * 
    + * + * In both of the scenarios above, {@code @EnableCaching} and {@code + * } are responsible for registering the necessary Spring + * components that power annotation-driven cache management, such as the + * {@link org.springframework.cache.interceptor.CacheInterceptor CacheInterceptor} and the + * proxy- or AspectJ-based advice that weaves the interceptor into the call stack when + * {@link org.springframework.cache.annotation.Cacheable @Cacheable} methods are invoked. + * + *

    If the JSR-107 API and Spring's JCache implementation are present, the necessary + * components to manage standard cache annotations are also registered. This creates the + * proxy- or AspectJ-based advice that weaves the interceptor into the call stack when + * methods annotated with {@code CacheResult}, {@code CachePut}, {@code CacheRemove} or + * {@code CacheRemoveAll} are invoked. + * + *

    A bean of type {@link org.springframework.cache.CacheManager CacheManager} + * must be registered, as there is no reasonable default that the framework can + * use as a convention. And whereas the {@code } element assumes + * a bean named "cacheManager", {@code @EnableCaching} searches for a cache + * manager bean by type. Therefore, naming of the cache manager bean method is + * not significant. + * + *

    For those that wish to establish a more direct relationship between + * {@code @EnableCaching} and the exact cache manager bean to be used, + * the {@link CachingConfigurer} callback interface may be implemented. + * Notice the {@code @Override}-annotated methods below: + * + *

    + * @Configuration
    + * @EnableCaching
    + * public class AppConfig extends CachingConfigurerSupport {
    + *
    + *     @Bean
    + *     public MyService myService() {
    + *         // configure and return a class having @Cacheable methods
    + *         return new MyService();
    + *     }
    + *
    + *     @Bean
    + *     @Override
    + *     public CacheManager cacheManager() {
    + *         // configure and return an implementation of Spring's CacheManager SPI
    + *         SimpleCacheManager cacheManager = new SimpleCacheManager();
    + *         cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache("default")));
    + *         return cacheManager;
    + *     }
    + *
    + *     @Bean
    + *     @Override
    + *     public KeyGenerator keyGenerator() {
    + *         // configure and return an implementation of Spring's KeyGenerator SPI
    + *         return new MyKeyGenerator();
    + *     }
    + * }
    + * + * This approach may be desirable simply because it is more explicit, or it may be + * necessary in order to distinguish between two {@code CacheManager} beans present in the + * same container. + * + *

    Notice also the {@code keyGenerator} method in the example above. This allows for + * customizing the strategy for cache key generation, per Spring's {@link + * org.springframework.cache.interceptor.KeyGenerator KeyGenerator} SPI. Normally, + * {@code @EnableCaching} will configure Spring's + * {@link org.springframework.cache.interceptor.SimpleKeyGenerator SimpleKeyGenerator} + * for this purpose, but when implementing {@code CachingConfigurer}, a key generator + * must be provided explicitly. Return {@code null} or {@code new SimpleKeyGenerator()} + * from this method if no customization is necessary. + * + *

    {@link CachingConfigurer} offers additional customization options: it is recommended + * to extend from {@link org.springframework.cache.annotation.CachingConfigurerSupport + * CachingConfigurerSupport} that provides a default implementation for all methods which + * can be useful if you do not need to customize everything. See {@link CachingConfigurer} + * Javadoc for further details. + * + *

    The {@link #mode} attribute controls how advice is applied: If the mode is + * {@link AdviceMode#PROXY} (the default), then the other attributes control the behavior + * of the proxying. Please note that proxy mode allows for interception of calls through + * the proxy only; local calls within the same class cannot get intercepted that way. + * + *

    Note that if the {@linkplain #mode} is set to {@link AdviceMode#ASPECTJ}, then the + * value of the {@link #proxyTargetClass} attribute will be ignored. Note also that in + * this case the {@code spring-aspects} module JAR must be present on the classpath, with + * compile-time weaving or load-time weaving applying the aspect to the affected classes. + * There is no proxy involved in such a scenario; local calls will be intercepted as well. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + * @see CachingConfigurer + * @see CachingConfigurationSelector + * @see ProxyCachingConfiguration + * @see org.springframework.cache.aspectj.AspectJCachingConfiguration + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Import(CachingConfigurationSelector.class) +public @interface EnableCaching { + + /** + * Indicate whether subclass-based (CGLIB) proxies are to be created as opposed + * to standard Java interface-based proxies. The default is {@code false}. + * Applicable only if {@link #mode()} is set to {@link AdviceMode#PROXY}. + *

    Note that setting this attribute to {@code true} will affect all + * Spring-managed beans requiring proxying, not just those marked with {@code @Cacheable}. + * For example, other beans marked with Spring's {@code @Transactional} annotation will + * be upgraded to subclass proxying at the same time. This approach has no negative + * impact in practice unless one is explicitly expecting one type of proxy vs another, + * e.g. in tests. + */ + boolean proxyTargetClass() default false; + + /** + * Indicate how caching advice should be applied. + *

    The default is {@link AdviceMode#PROXY}. + * Please note that proxy mode allows for interception of calls through the proxy + * only. Local calls within the same class cannot get intercepted that way; + * a caching annotation on such a method within a local call will be ignored + * since Spring's interceptor does not even kick in for such a runtime scenario. + * For a more advanced mode of interception, consider switching this to + * {@link AdviceMode#ASPECTJ}. + */ + AdviceMode mode() default AdviceMode.PROXY; + + /** + * Indicate the ordering of the execution of the caching advisor + * when multiple advices are applied at a specific joinpoint. + *

    The default is {@link Ordered#LOWEST_PRECEDENCE}. + */ + int order() default Ordered.LOWEST_PRECEDENCE; + +} diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/ProxyCachingConfiguration.java b/spring-context/src/main/java/org/springframework/cache/annotation/ProxyCachingConfiguration.java new file mode 100644 index 0000000..b7d3568 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/annotation/ProxyCachingConfiguration.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.annotation; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.cache.config.CacheManagementConfigUtils; +import org.springframework.cache.interceptor.BeanFactoryCacheOperationSourceAdvisor; +import org.springframework.cache.interceptor.CacheInterceptor; +import org.springframework.cache.interceptor.CacheOperationSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; + +/** + * {@code @Configuration} class that registers the Spring infrastructure beans necessary + * to enable proxy-based annotation-driven cache management. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + * @see EnableCaching + * @see CachingConfigurationSelector + */ +@Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +public class ProxyCachingConfiguration extends AbstractCachingConfiguration { + + @Bean(name = CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME) + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public BeanFactoryCacheOperationSourceAdvisor cacheAdvisor( + CacheOperationSource cacheOperationSource, CacheInterceptor cacheInterceptor) { + + BeanFactoryCacheOperationSourceAdvisor advisor = new BeanFactoryCacheOperationSourceAdvisor(); + advisor.setCacheOperationSource(cacheOperationSource); + advisor.setAdvice(cacheInterceptor); + if (this.enableCaching != null) { + advisor.setOrder(this.enableCaching.getNumber("order")); + } + return advisor; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public CacheOperationSource cacheOperationSource() { + return new AnnotationCacheOperationSource(); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public CacheInterceptor cacheInterceptor(CacheOperationSource cacheOperationSource) { + CacheInterceptor interceptor = new CacheInterceptor(); + interceptor.configure(this.errorHandler, this.keyGenerator, this.cacheResolver, this.cacheManager); + interceptor.setCacheOperationSource(cacheOperationSource); + return interceptor; + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/SpringCacheAnnotationParser.java b/spring-context/src/main/java/org/springframework/cache/annotation/SpringCacheAnnotationParser.java new file mode 100644 index 0000000..223a348 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/annotation/SpringCacheAnnotationParser.java @@ -0,0 +1,296 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.annotation; + +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.cache.interceptor.CacheEvictOperation; +import org.springframework.cache.interceptor.CacheOperation; +import org.springframework.cache.interceptor.CachePutOperation; +import org.springframework.cache.interceptor.CacheableOperation; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Strategy implementation for parsing Spring's {@link Caching}, {@link Cacheable}, + * {@link CacheEvict}, and {@link CachePut} annotations. + * + * @author Costin Leau + * @author Juergen Hoeller + * @author Chris Beams + * @author Phillip Webb + * @author Stephane Nicoll + * @author Sam Brannen + * @since 3.1 + */ +@SuppressWarnings("serial") +public class SpringCacheAnnotationParser implements CacheAnnotationParser, Serializable { + + private static final Set> CACHE_OPERATION_ANNOTATIONS = new LinkedHashSet<>(8); + + static { + CACHE_OPERATION_ANNOTATIONS.add(Cacheable.class); + CACHE_OPERATION_ANNOTATIONS.add(CacheEvict.class); + CACHE_OPERATION_ANNOTATIONS.add(CachePut.class); + CACHE_OPERATION_ANNOTATIONS.add(Caching.class); + } + + + @Override + public boolean isCandidateClass(Class targetClass) { + return AnnotationUtils.isCandidateClass(targetClass, CACHE_OPERATION_ANNOTATIONS); + } + + @Override + @Nullable + public Collection parseCacheAnnotations(Class type) { + DefaultCacheConfig defaultConfig = new DefaultCacheConfig(type); + return parseCacheAnnotations(defaultConfig, type); + } + + @Override + @Nullable + public Collection parseCacheAnnotations(Method method) { + DefaultCacheConfig defaultConfig = new DefaultCacheConfig(method.getDeclaringClass()); + return parseCacheAnnotations(defaultConfig, method); + } + + @Nullable + private Collection parseCacheAnnotations(DefaultCacheConfig cachingConfig, AnnotatedElement ae) { + Collection ops = parseCacheAnnotations(cachingConfig, ae, false); + if (ops != null && ops.size() > 1) { + // More than one operation found -> local declarations override interface-declared ones... + Collection localOps = parseCacheAnnotations(cachingConfig, ae, true); + if (localOps != null) { + return localOps; + } + } + return ops; + } + + @Nullable + private Collection parseCacheAnnotations( + DefaultCacheConfig cachingConfig, AnnotatedElement ae, boolean localOnly) { + + Collection anns = (localOnly ? + AnnotatedElementUtils.getAllMergedAnnotations(ae, CACHE_OPERATION_ANNOTATIONS) : + AnnotatedElementUtils.findAllMergedAnnotations(ae, CACHE_OPERATION_ANNOTATIONS)); + if (anns.isEmpty()) { + return null; + } + + final Collection ops = new ArrayList<>(1); + anns.stream().filter(ann -> ann instanceof Cacheable).forEach( + ann -> ops.add(parseCacheableAnnotation(ae, cachingConfig, (Cacheable) ann))); + anns.stream().filter(ann -> ann instanceof CacheEvict).forEach( + ann -> ops.add(parseEvictAnnotation(ae, cachingConfig, (CacheEvict) ann))); + anns.stream().filter(ann -> ann instanceof CachePut).forEach( + ann -> ops.add(parsePutAnnotation(ae, cachingConfig, (CachePut) ann))); + anns.stream().filter(ann -> ann instanceof Caching).forEach( + ann -> parseCachingAnnotation(ae, cachingConfig, (Caching) ann, ops)); + return ops; + } + + private CacheableOperation parseCacheableAnnotation( + AnnotatedElement ae, DefaultCacheConfig defaultConfig, Cacheable cacheable) { + + CacheableOperation.Builder builder = new CacheableOperation.Builder(); + + builder.setName(ae.toString()); + builder.setCacheNames(cacheable.cacheNames()); + builder.setCondition(cacheable.condition()); + builder.setUnless(cacheable.unless()); + builder.setKey(cacheable.key()); + builder.setKeyGenerator(cacheable.keyGenerator()); + builder.setCacheManager(cacheable.cacheManager()); + builder.setCacheResolver(cacheable.cacheResolver()); + builder.setSync(cacheable.sync()); + + defaultConfig.applyDefault(builder); + CacheableOperation op = builder.build(); + validateCacheOperation(ae, op); + + return op; + } + + private CacheEvictOperation parseEvictAnnotation( + AnnotatedElement ae, DefaultCacheConfig defaultConfig, CacheEvict cacheEvict) { + + CacheEvictOperation.Builder builder = new CacheEvictOperation.Builder(); + + builder.setName(ae.toString()); + builder.setCacheNames(cacheEvict.cacheNames()); + builder.setCondition(cacheEvict.condition()); + builder.setKey(cacheEvict.key()); + builder.setKeyGenerator(cacheEvict.keyGenerator()); + builder.setCacheManager(cacheEvict.cacheManager()); + builder.setCacheResolver(cacheEvict.cacheResolver()); + builder.setCacheWide(cacheEvict.allEntries()); + builder.setBeforeInvocation(cacheEvict.beforeInvocation()); + + defaultConfig.applyDefault(builder); + CacheEvictOperation op = builder.build(); + validateCacheOperation(ae, op); + + return op; + } + + private CacheOperation parsePutAnnotation( + AnnotatedElement ae, DefaultCacheConfig defaultConfig, CachePut cachePut) { + + CachePutOperation.Builder builder = new CachePutOperation.Builder(); + + builder.setName(ae.toString()); + builder.setCacheNames(cachePut.cacheNames()); + builder.setCondition(cachePut.condition()); + builder.setUnless(cachePut.unless()); + builder.setKey(cachePut.key()); + builder.setKeyGenerator(cachePut.keyGenerator()); + builder.setCacheManager(cachePut.cacheManager()); + builder.setCacheResolver(cachePut.cacheResolver()); + + defaultConfig.applyDefault(builder); + CachePutOperation op = builder.build(); + validateCacheOperation(ae, op); + + return op; + } + + private void parseCachingAnnotation( + AnnotatedElement ae, DefaultCacheConfig defaultConfig, Caching caching, Collection ops) { + + Cacheable[] cacheables = caching.cacheable(); + for (Cacheable cacheable : cacheables) { + ops.add(parseCacheableAnnotation(ae, defaultConfig, cacheable)); + } + CacheEvict[] cacheEvicts = caching.evict(); + for (CacheEvict cacheEvict : cacheEvicts) { + ops.add(parseEvictAnnotation(ae, defaultConfig, cacheEvict)); + } + CachePut[] cachePuts = caching.put(); + for (CachePut cachePut : cachePuts) { + ops.add(parsePutAnnotation(ae, defaultConfig, cachePut)); + } + } + + + /** + * Validates the specified {@link CacheOperation}. + *

    Throws an {@link IllegalStateException} if the state of the operation is + * invalid. As there might be multiple sources for default values, this ensure + * that the operation is in a proper state before being returned. + * @param ae the annotated element of the cache operation + * @param operation the {@link CacheOperation} to validate + */ + private void validateCacheOperation(AnnotatedElement ae, CacheOperation operation) { + if (StringUtils.hasText(operation.getKey()) && StringUtils.hasText(operation.getKeyGenerator())) { + throw new IllegalStateException("Invalid cache annotation configuration on '" + + ae.toString() + "'. Both 'key' and 'keyGenerator' attributes have been set. " + + "These attributes are mutually exclusive: either set the SpEL expression used to" + + "compute the key at runtime or set the name of the KeyGenerator bean to use."); + } + if (StringUtils.hasText(operation.getCacheManager()) && StringUtils.hasText(operation.getCacheResolver())) { + throw new IllegalStateException("Invalid cache annotation configuration on '" + + ae.toString() + "'. Both 'cacheManager' and 'cacheResolver' attributes have been set. " + + "These attributes are mutually exclusive: the cache manager is used to configure a" + + "default cache resolver if none is set. If a cache resolver is set, the cache manager" + + "won't be used."); + } + } + + @Override + public boolean equals(@Nullable Object other) { + return (other instanceof SpringCacheAnnotationParser); + } + + @Override + public int hashCode() { + return SpringCacheAnnotationParser.class.hashCode(); + } + + + /** + * Provides default settings for a given set of cache operations. + */ + private static class DefaultCacheConfig { + + private final Class target; + + @Nullable + private String[] cacheNames; + + @Nullable + private String keyGenerator; + + @Nullable + private String cacheManager; + + @Nullable + private String cacheResolver; + + private boolean initialized = false; + + public DefaultCacheConfig(Class target) { + this.target = target; + } + + /** + * Apply the defaults to the specified {@link CacheOperation.Builder}. + * @param builder the operation builder to update + */ + public void applyDefault(CacheOperation.Builder builder) { + if (!this.initialized) { + CacheConfig annotation = AnnotatedElementUtils.findMergedAnnotation(this.target, CacheConfig.class); + if (annotation != null) { + this.cacheNames = annotation.cacheNames(); + this.keyGenerator = annotation.keyGenerator(); + this.cacheManager = annotation.cacheManager(); + this.cacheResolver = annotation.cacheResolver(); + } + this.initialized = true; + } + + if (builder.getCacheNames().isEmpty() && this.cacheNames != null) { + builder.setCacheNames(this.cacheNames); + } + if (!StringUtils.hasText(builder.getKey()) && !StringUtils.hasText(builder.getKeyGenerator()) && + StringUtils.hasText(this.keyGenerator)) { + builder.setKeyGenerator(this.keyGenerator); + } + + if (StringUtils.hasText(builder.getCacheManager()) || StringUtils.hasText(builder.getCacheResolver())) { + // One of these is set so we should not inherit anything + } + else if (StringUtils.hasText(this.cacheResolver)) { + builder.setCacheResolver(this.cacheResolver); + } + else if (StringUtils.hasText(this.cacheManager)) { + builder.setCacheManager(this.cacheManager); + } + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/package-info.java b/spring-context/src/main/java/org/springframework/cache/annotation/package-info.java new file mode 100644 index 0000000..0a53f33 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/annotation/package-info.java @@ -0,0 +1,11 @@ +/** + * Annotations and supporting classes for declarative cache management. + * Hooked into Spring's cache interception infrastructure via + * {@link org.springframework.cache.interceptor.CacheOperationSource}. + */ +@NonNullApi +@NonNullFields +package org.springframework.cache.annotation; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCache.java b/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCache.java new file mode 100644 index 0000000..1a17605 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCache.java @@ -0,0 +1,218 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.concurrent; + +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.springframework.cache.support.AbstractValueAdaptingCache; +import org.springframework.core.serializer.support.SerializationDelegate; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Simple {@link org.springframework.cache.Cache} implementation based on the + * core JDK {@code java.util.concurrent} package. + * + *

    Useful for testing or simple caching scenarios, typically in combination + * with {@link org.springframework.cache.support.SimpleCacheManager} or + * dynamically through {@link ConcurrentMapCacheManager}. + * + *

    Note: As {@link ConcurrentHashMap} (the default implementation used) + * does not allow for {@code null} values to be stored, this class will replace + * them with a predefined internal object. This behavior can be changed through the + * {@link #ConcurrentMapCache(String, ConcurrentMap, boolean)} constructor. + * + * @author Costin Leau + * @author Juergen Hoeller + * @author Stephane Nicoll + * @since 3.1 + * @see ConcurrentMapCacheManager + */ +public class ConcurrentMapCache extends AbstractValueAdaptingCache { + + private final String name; + + private final ConcurrentMap store; + + @Nullable + private final SerializationDelegate serialization; + + + /** + * Create a new ConcurrentMapCache with the specified name. + * @param name the name of the cache + */ + public ConcurrentMapCache(String name) { + this(name, new ConcurrentHashMap<>(256), true); + } + + /** + * Create a new ConcurrentMapCache with the specified name. + * @param name the name of the cache + * @param allowNullValues whether to accept and convert {@code null} + * values for this cache + */ + public ConcurrentMapCache(String name, boolean allowNullValues) { + this(name, new ConcurrentHashMap<>(256), allowNullValues); + } + + /** + * Create a new ConcurrentMapCache with the specified name and the + * given internal {@link ConcurrentMap} to use. + * @param name the name of the cache + * @param store the ConcurrentMap to use as an internal store + * @param allowNullValues whether to allow {@code null} values + * (adapting them to an internal null holder value) + */ + public ConcurrentMapCache(String name, ConcurrentMap store, boolean allowNullValues) { + this(name, store, allowNullValues, null); + } + + /** + * Create a new ConcurrentMapCache with the specified name and the + * given internal {@link ConcurrentMap} to use. If the + * {@link SerializationDelegate} is specified, + * {@link #isStoreByValue() store-by-value} is enabled + * @param name the name of the cache + * @param store the ConcurrentMap to use as an internal store + * @param allowNullValues whether to allow {@code null} values + * (adapting them to an internal null holder value) + * @param serialization the {@link SerializationDelegate} to use + * to serialize cache entry or {@code null} to store the reference + * @since 4.3 + */ + protected ConcurrentMapCache(String name, ConcurrentMap store, + boolean allowNullValues, @Nullable SerializationDelegate serialization) { + + super(allowNullValues); + Assert.notNull(name, "Name must not be null"); + Assert.notNull(store, "Store must not be null"); + this.name = name; + this.store = store; + this.serialization = serialization; + } + + + /** + * Return whether this cache stores a copy of each entry ({@code true}) or + * a reference ({@code false}, default). If store by value is enabled, each + * entry in the cache must be serializable. + * @since 4.3 + */ + public final boolean isStoreByValue() { + return (this.serialization != null); + } + + @Override + public final String getName() { + return this.name; + } + + @Override + public final ConcurrentMap getNativeCache() { + return this.store; + } + + @Override + @Nullable + protected Object lookup(Object key) { + return this.store.get(key); + } + + @SuppressWarnings("unchecked") + @Override + @Nullable + public T get(Object key, Callable valueLoader) { + return (T) fromStoreValue(this.store.computeIfAbsent(key, k -> { + try { + return toStoreValue(valueLoader.call()); + } + catch (Throwable ex) { + throw new ValueRetrievalException(key, valueLoader, ex); + } + })); + } + + @Override + public void put(Object key, @Nullable Object value) { + this.store.put(key, toStoreValue(value)); + } + + @Override + @Nullable + public ValueWrapper putIfAbsent(Object key, @Nullable Object value) { + Object existing = this.store.putIfAbsent(key, toStoreValue(value)); + return toValueWrapper(existing); + } + + @Override + public void evict(Object key) { + this.store.remove(key); + } + + @Override + public boolean evictIfPresent(Object key) { + return (this.store.remove(key) != null); + } + + @Override + public void clear() { + this.store.clear(); + } + + @Override + public boolean invalidate() { + boolean notEmpty = !this.store.isEmpty(); + this.store.clear(); + return notEmpty; + } + + @Override + protected Object toStoreValue(@Nullable Object userValue) { + Object storeValue = super.toStoreValue(userValue); + if (this.serialization != null) { + try { + return this.serialization.serializeToByteArray(storeValue); + } + catch (Throwable ex) { + throw new IllegalArgumentException("Failed to serialize cache value '" + userValue + + "'. Does it implement Serializable?", ex); + } + } + else { + return storeValue; + } + } + + @Override + protected Object fromStoreValue(@Nullable Object storeValue) { + if (storeValue != null && this.serialization != null) { + try { + return super.fromStoreValue(this.serialization.deserializeFromByteArray((byte[]) storeValue)); + } + catch (Throwable ex) { + throw new IllegalArgumentException("Failed to deserialize cache value '" + storeValue + "'", ex); + } + } + else { + return super.fromStoreValue(storeValue); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheFactoryBean.java b/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheFactoryBean.java new file mode 100644 index 0000000..b559e10 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheFactoryBean.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.concurrent; + +import java.util.concurrent.ConcurrentMap; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * {@link FactoryBean} for easy configuration of a {@link ConcurrentMapCache} + * when used within a Spring container. Can be configured through bean properties; + * uses the assigned Spring bean name as the default cache name. + * + *

    Useful for testing or simple caching scenarios, typically in combination + * with {@link org.springframework.cache.support.SimpleCacheManager} or + * dynamically through {@link ConcurrentMapCacheManager}. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 3.1 + */ +public class ConcurrentMapCacheFactoryBean + implements FactoryBean, BeanNameAware, InitializingBean { + + private String name = ""; + + @Nullable + private ConcurrentMap store; + + private boolean allowNullValues = true; + + @Nullable + private ConcurrentMapCache cache; + + + /** + * Specify the name of the cache. + *

    Default is "" (empty String). + */ + public void setName(String name) { + this.name = name; + } + + /** + * Specify the ConcurrentMap to use as an internal store + * (possibly pre-populated). + *

    Default is a standard {@link java.util.concurrent.ConcurrentHashMap}. + */ + public void setStore(ConcurrentMap store) { + this.store = store; + } + + /** + * Set whether to allow {@code null} values + * (adapting them to an internal null holder value). + *

    Default is "true". + */ + public void setAllowNullValues(boolean allowNullValues) { + this.allowNullValues = allowNullValues; + } + + @Override + public void setBeanName(String beanName) { + if (!StringUtils.hasLength(this.name)) { + setName(beanName); + } + } + + @Override + public void afterPropertiesSet() { + this.cache = (this.store != null ? new ConcurrentMapCache(this.name, this.store, this.allowNullValues) : + new ConcurrentMapCache(this.name, this.allowNullValues)); + } + + + @Override + @Nullable + public ConcurrentMapCache getObject() { + return this.cache; + } + + @Override + public Class getObjectType() { + return ConcurrentMapCache.class; + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java b/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java new file mode 100644 index 0000000..aea9132 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java @@ -0,0 +1,196 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.concurrent; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.core.serializer.support.SerializationDelegate; +import org.springframework.lang.Nullable; + +/** + * {@link CacheManager} implementation that lazily builds {@link ConcurrentMapCache} + * instances for each {@link #getCache} request. Also supports a 'static' mode where + * the set of cache names is pre-defined through {@link #setCacheNames}, with no + * dynamic creation of further cache regions at runtime. + * + *

    Note: This is by no means a sophisticated CacheManager; it comes with no + * cache configuration options. However, it may be useful for testing or simple + * caching scenarios. For advanced local caching needs, consider + * {@link org.springframework.cache.jcache.JCacheCacheManager}, + * {@link org.springframework.cache.ehcache.EhCacheCacheManager}, + * {@link org.springframework.cache.caffeine.CaffeineCacheManager}. + * + * @author Juergen Hoeller + * @since 3.1 + * @see ConcurrentMapCache + */ +public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderAware { + + private final ConcurrentMap cacheMap = new ConcurrentHashMap<>(16); + + private boolean dynamic = true; + + private boolean allowNullValues = true; + + private boolean storeByValue = false; + + @Nullable + private SerializationDelegate serialization; + + + /** + * Construct a dynamic ConcurrentMapCacheManager, + * lazily creating cache instances as they are being requested. + */ + public ConcurrentMapCacheManager() { + } + + /** + * Construct a static ConcurrentMapCacheManager, + * managing caches for the specified cache names only. + */ + public ConcurrentMapCacheManager(String... cacheNames) { + setCacheNames(Arrays.asList(cacheNames)); + } + + + /** + * Specify the set of cache names for this CacheManager's 'static' mode. + *

    The number of caches and their names will be fixed after a call to this method, + * with no creation of further cache regions at runtime. + *

    Calling this with a {@code null} collection argument resets the + * mode to 'dynamic', allowing for further creation of caches again. + */ + public void setCacheNames(@Nullable Collection cacheNames) { + if (cacheNames != null) { + for (String name : cacheNames) { + this.cacheMap.put(name, createConcurrentMapCache(name)); + } + this.dynamic = false; + } + else { + this.dynamic = true; + } + } + + /** + * Specify whether to accept and convert {@code null} values for all caches + * in this cache manager. + *

    Default is "true", despite ConcurrentHashMap itself not supporting {@code null} + * values. An internal holder object will be used to store user-level {@code null}s. + *

    Note: A change of the null-value setting will reset all existing caches, + * if any, to reconfigure them with the new null-value requirement. + */ + public void setAllowNullValues(boolean allowNullValues) { + if (allowNullValues != this.allowNullValues) { + this.allowNullValues = allowNullValues; + // Need to recreate all Cache instances with the new null-value configuration... + recreateCaches(); + } + } + + /** + * Return whether this cache manager accepts and converts {@code null} values + * for all of its caches. + */ + public boolean isAllowNullValues() { + return this.allowNullValues; + } + + /** + * Specify whether this cache manager stores a copy of each entry ({@code true} + * or the reference ({@code false} for all of its caches. + *

    Default is "false" so that the value itself is stored and no serializable + * contract is required on cached values. + *

    Note: A change of the store-by-value setting will reset all existing caches, + * if any, to reconfigure them with the new store-by-value requirement. + * @since 4.3 + */ + public void setStoreByValue(boolean storeByValue) { + if (storeByValue != this.storeByValue) { + this.storeByValue = storeByValue; + // Need to recreate all Cache instances with the new store-by-value configuration... + recreateCaches(); + } + } + + /** + * Return whether this cache manager stores a copy of each entry or + * a reference for all its caches. If store by value is enabled, any + * cache entry must be serializable. + * @since 4.3 + */ + public boolean isStoreByValue() { + return this.storeByValue; + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.serialization = new SerializationDelegate(classLoader); + // Need to recreate all Cache instances with new ClassLoader in store-by-value mode... + if (isStoreByValue()) { + recreateCaches(); + } + } + + + @Override + public Collection getCacheNames() { + return Collections.unmodifiableSet(this.cacheMap.keySet()); + } + + @Override + @Nullable + public Cache getCache(String name) { + Cache cache = this.cacheMap.get(name); + if (cache == null && this.dynamic) { + synchronized (this.cacheMap) { + cache = this.cacheMap.get(name); + if (cache == null) { + cache = createConcurrentMapCache(name); + this.cacheMap.put(name, cache); + } + } + } + return cache; + } + + private void recreateCaches() { + for (Map.Entry entry : this.cacheMap.entrySet()) { + entry.setValue(createConcurrentMapCache(entry.getKey())); + } + } + + /** + * Create a new ConcurrentMapCache instance for the specified cache name. + * @param name the name of the cache + * @return the ConcurrentMapCache (or a decorator thereof) + */ + protected Cache createConcurrentMapCache(String name) { + SerializationDelegate actualSerialization = (isStoreByValue() ? this.serialization : null); + return new ConcurrentMapCache(name, new ConcurrentHashMap<>(256), isAllowNullValues(), actualSerialization); + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/concurrent/package-info.java b/spring-context/src/main/java/org/springframework/cache/concurrent/package-info.java new file mode 100644 index 0000000..5c5feb5 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/concurrent/package-info.java @@ -0,0 +1,12 @@ +/** + * Implementation package for {@code java.util.concurrent} based caches. + * Provides a {@link org.springframework.cache.CacheManager CacheManager} + * and {@link org.springframework.cache.Cache Cache} implementation for + * use in a Spring context, using a JDK based thread pool at runtime. + */ +@NonNullApi +@NonNullFields +package org.springframework.cache.concurrent; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/cache/config/AnnotationDrivenCacheBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/cache/config/AnnotationDrivenCacheBeanDefinitionParser.java new file mode 100644 index 0000000..41c0bbc --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/config/AnnotationDrivenCacheBeanDefinitionParser.java @@ -0,0 +1,274 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.config; + +import org.w3c.dom.Element; + +import org.springframework.aop.config.AopNamespaceUtils; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.parsing.CompositeComponentDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.cache.interceptor.BeanFactoryCacheOperationSourceAdvisor; +import org.springframework.cache.interceptor.CacheInterceptor; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * {@link org.springframework.beans.factory.xml.BeanDefinitionParser} + * implementation that allows users to easily configure all the + * infrastructure beans required to enable annotation-driven cache + * demarcation. + * + *

    By default, all proxies are created as JDK proxies. This may cause + * some problems if you are injecting objects as concrete classes rather + * than interfaces. To overcome this restriction you can set the + * '{@code proxy-target-class}' attribute to '{@code true}', which will + * result in class-based proxies being created. + * + *

    If the JSR-107 API and Spring's JCache implementation are present, + * the necessary infrastructure beans required to handle methods annotated + * with {@code CacheResult}, {@code CachePut}, {@code CacheRemove} or + * {@code CacheRemoveAll} are also registered. + * + * @author Costin Leau + * @author Stephane Nicoll + * @since 3.1 + */ +class AnnotationDrivenCacheBeanDefinitionParser implements BeanDefinitionParser { + + private static final String CACHE_ASPECT_CLASS_NAME = + "org.springframework.cache.aspectj.AnnotationCacheAspect"; + + private static final String JCACHE_ASPECT_CLASS_NAME = + "org.springframework.cache.aspectj.JCacheCacheAspect"; + + private static final boolean jsr107Present; + + private static final boolean jcacheImplPresent; + + static { + ClassLoader classLoader = AnnotationDrivenCacheBeanDefinitionParser.class.getClassLoader(); + jsr107Present = ClassUtils.isPresent("javax.cache.Cache", classLoader); + jcacheImplPresent = ClassUtils.isPresent( + "org.springframework.cache.jcache.interceptor.DefaultJCacheOperationSource", classLoader); + } + + + /** + * Parses the '{@code }' tag. Will + * {@link AopNamespaceUtils#registerAutoProxyCreatorIfNecessary + * register an AutoProxyCreator} with the container as necessary. + */ + @Override + @Nullable + public BeanDefinition parse(Element element, ParserContext parserContext) { + String mode = element.getAttribute("mode"); + if ("aspectj".equals(mode)) { + // mode="aspectj" + registerCacheAspect(element, parserContext); + } + else { + // mode="proxy" + registerCacheAdvisor(element, parserContext); + } + + return null; + } + + private void registerCacheAspect(Element element, ParserContext parserContext) { + SpringCachingConfigurer.registerCacheAspect(element, parserContext); + if (jsr107Present && jcacheImplPresent) { + JCacheCachingConfigurer.registerCacheAspect(element, parserContext); + } + } + + private void registerCacheAdvisor(Element element, ParserContext parserContext) { + AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(parserContext, element); + SpringCachingConfigurer.registerCacheAdvisor(element, parserContext); + if (jsr107Present && jcacheImplPresent) { + JCacheCachingConfigurer.registerCacheAdvisor(element, parserContext); + } + } + + /** + * Parse the cache resolution strategy to use. If a 'cache-resolver' attribute + * is set, it is injected. Otherwise the 'cache-manager' is set. If {@code setBoth} + * is {@code true}, both service are actually injected. + */ + private static void parseCacheResolution(Element element, BeanDefinition def, boolean setBoth) { + String name = element.getAttribute("cache-resolver"); + boolean hasText = StringUtils.hasText(name); + if (hasText) { + def.getPropertyValues().add("cacheResolver", new RuntimeBeanReference(name.trim())); + } + if (!hasText || setBoth) { + def.getPropertyValues().add("cacheManager", + new RuntimeBeanReference(CacheNamespaceHandler.extractCacheManager(element))); + } + } + + private static void parseErrorHandler(Element element, BeanDefinition def) { + String name = element.getAttribute("error-handler"); + if (StringUtils.hasText(name)) { + def.getPropertyValues().add("errorHandler", new RuntimeBeanReference(name.trim())); + } + } + + + /** + * Configure the necessary infrastructure to support the Spring's caching annotations. + */ + private static class SpringCachingConfigurer { + + private static void registerCacheAdvisor(Element element, ParserContext parserContext) { + if (!parserContext.getRegistry().containsBeanDefinition(CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME)) { + Object eleSource = parserContext.extractSource(element); + + // Create the CacheOperationSource definition. + RootBeanDefinition sourceDef = new RootBeanDefinition("org.springframework.cache.annotation.AnnotationCacheOperationSource"); + sourceDef.setSource(eleSource); + sourceDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + String sourceName = parserContext.getReaderContext().registerWithGeneratedName(sourceDef); + + // Create the CacheInterceptor definition. + RootBeanDefinition interceptorDef = new RootBeanDefinition(CacheInterceptor.class); + interceptorDef.setSource(eleSource); + interceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + parseCacheResolution(element, interceptorDef, false); + parseErrorHandler(element, interceptorDef); + CacheNamespaceHandler.parseKeyGenerator(element, interceptorDef); + interceptorDef.getPropertyValues().add("cacheOperationSources", new RuntimeBeanReference(sourceName)); + String interceptorName = parserContext.getReaderContext().registerWithGeneratedName(interceptorDef); + + // Create the CacheAdvisor definition. + RootBeanDefinition advisorDef = new RootBeanDefinition(BeanFactoryCacheOperationSourceAdvisor.class); + advisorDef.setSource(eleSource); + advisorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + advisorDef.getPropertyValues().add("cacheOperationSource", new RuntimeBeanReference(sourceName)); + advisorDef.getPropertyValues().add("adviceBeanName", interceptorName); + if (element.hasAttribute("order")) { + advisorDef.getPropertyValues().add("order", element.getAttribute("order")); + } + parserContext.getRegistry().registerBeanDefinition(CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME, advisorDef); + + CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), eleSource); + compositeDef.addNestedComponent(new BeanComponentDefinition(sourceDef, sourceName)); + compositeDef.addNestedComponent(new BeanComponentDefinition(interceptorDef, interceptorName)); + compositeDef.addNestedComponent(new BeanComponentDefinition(advisorDef, CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME)); + parserContext.registerComponent(compositeDef); + } + } + + /** + * Registers a cache aspect. + *

    +		 * <bean id="cacheAspect" class="org.springframework.cache.aspectj.AnnotationCacheAspect" factory-method="aspectOf">
    +		 *   <property name="cacheManager" ref="cacheManager"/>
    +		 *   <property name="keyGenerator" ref="keyGenerator"/>
    +		 * </bean>
    +		 * 
    + */ + private static void registerCacheAspect(Element element, ParserContext parserContext) { + if (!parserContext.getRegistry().containsBeanDefinition(CacheManagementConfigUtils.CACHE_ASPECT_BEAN_NAME)) { + RootBeanDefinition def = new RootBeanDefinition(); + def.setBeanClassName(CACHE_ASPECT_CLASS_NAME); + def.setFactoryMethodName("aspectOf"); + parseCacheResolution(element, def, false); + CacheNamespaceHandler.parseKeyGenerator(element, def); + parserContext.registerBeanComponent(new BeanComponentDefinition(def, CacheManagementConfigUtils.CACHE_ASPECT_BEAN_NAME)); + } + } + } + + + /** + * Configure the necessary infrastructure to support the standard JSR-107 caching annotations. + */ + private static class JCacheCachingConfigurer { + + private static void registerCacheAdvisor(Element element, ParserContext parserContext) { + if (!parserContext.getRegistry().containsBeanDefinition(CacheManagementConfigUtils.JCACHE_ADVISOR_BEAN_NAME)) { + Object source = parserContext.extractSource(element); + + // Create the CacheOperationSource definition. + BeanDefinition sourceDef = createJCacheOperationSourceBeanDefinition(element, source); + String sourceName = parserContext.getReaderContext().registerWithGeneratedName(sourceDef); + + // Create the CacheInterceptor definition. + RootBeanDefinition interceptorDef = + new RootBeanDefinition("org.springframework.cache.jcache.interceptor.JCacheInterceptor"); + interceptorDef.setSource(source); + interceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + interceptorDef.getPropertyValues().add("cacheOperationSource", new RuntimeBeanReference(sourceName)); + parseErrorHandler(element, interceptorDef); + String interceptorName = parserContext.getReaderContext().registerWithGeneratedName(interceptorDef); + + // Create the CacheAdvisor definition. + RootBeanDefinition advisorDef = new RootBeanDefinition( + "org.springframework.cache.jcache.interceptor.BeanFactoryJCacheOperationSourceAdvisor"); + advisorDef.setSource(source); + advisorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + advisorDef.getPropertyValues().add("cacheOperationSource", new RuntimeBeanReference(sourceName)); + advisorDef.getPropertyValues().add("adviceBeanName", interceptorName); + if (element.hasAttribute("order")) { + advisorDef.getPropertyValues().add("order", element.getAttribute("order")); + } + parserContext.getRegistry().registerBeanDefinition(CacheManagementConfigUtils.JCACHE_ADVISOR_BEAN_NAME, advisorDef); + + CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), source); + compositeDef.addNestedComponent(new BeanComponentDefinition(sourceDef, sourceName)); + compositeDef.addNestedComponent(new BeanComponentDefinition(interceptorDef, interceptorName)); + compositeDef.addNestedComponent(new BeanComponentDefinition(advisorDef, CacheManagementConfigUtils.JCACHE_ADVISOR_BEAN_NAME)); + parserContext.registerComponent(compositeDef); + } + } + + private static void registerCacheAspect(Element element, ParserContext parserContext) { + if (!parserContext.getRegistry().containsBeanDefinition(CacheManagementConfigUtils.JCACHE_ASPECT_BEAN_NAME)) { + Object eleSource = parserContext.extractSource(element); + RootBeanDefinition def = new RootBeanDefinition(); + def.setBeanClassName(JCACHE_ASPECT_CLASS_NAME); + def.setFactoryMethodName("aspectOf"); + BeanDefinition sourceDef = createJCacheOperationSourceBeanDefinition(element, eleSource); + String sourceName = + parserContext.getReaderContext().registerWithGeneratedName(sourceDef); + def.getPropertyValues().add("cacheOperationSource", new RuntimeBeanReference(sourceName)); + + parserContext.registerBeanComponent(new BeanComponentDefinition(sourceDef, sourceName)); + parserContext.registerBeanComponent(new BeanComponentDefinition(def, CacheManagementConfigUtils.JCACHE_ASPECT_BEAN_NAME)); + } + } + + private static RootBeanDefinition createJCacheOperationSourceBeanDefinition(Element element, @Nullable Object eleSource) { + RootBeanDefinition sourceDef = + new RootBeanDefinition("org.springframework.cache.jcache.interceptor.DefaultJCacheOperationSource"); + sourceDef.setSource(eleSource); + sourceDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + // JSR-107 support should create an exception cache resolver with the cache manager + // and there is no way to set that exception cache resolver from the namespace + parseCacheResolution(element, sourceDef, true); + CacheNamespaceHandler.parseKeyGenerator(element, sourceDef); + return sourceDef; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/config/CacheAdviceParser.java b/spring-context/src/main/java/org/springframework/cache/config/CacheAdviceParser.java new file mode 100644 index 0000000..2d75593 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/config/CacheAdviceParser.java @@ -0,0 +1,248 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.config; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.TypedStringValue; +import org.springframework.beans.factory.parsing.ReaderContext; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.support.ManagedMap; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.cache.interceptor.CacheEvictOperation; +import org.springframework.cache.interceptor.CacheInterceptor; +import org.springframework.cache.interceptor.CacheOperation; +import org.springframework.cache.interceptor.CachePutOperation; +import org.springframework.cache.interceptor.CacheableOperation; +import org.springframework.cache.interceptor.NameMatchCacheOperationSource; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; + +/** + * {@link org.springframework.beans.factory.xml.BeanDefinitionParser + * BeanDefinitionParser} for the {@code } tag. + * + * @author Costin Leau + * @author Phillip Webb + * @author Stephane Nicoll + */ +class CacheAdviceParser extends AbstractSingleBeanDefinitionParser { + + private static final String CACHEABLE_ELEMENT = "cacheable"; + + private static final String CACHE_EVICT_ELEMENT = "cache-evict"; + + private static final String CACHE_PUT_ELEMENT = "cache-put"; + + private static final String METHOD_ATTRIBUTE = "method"; + + private static final String DEFS_ELEMENT = "caching"; + + + @Override + protected Class getBeanClass(Element element) { + return CacheInterceptor.class; + } + + @Override + protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + builder.addPropertyReference("cacheManager", CacheNamespaceHandler.extractCacheManager(element)); + CacheNamespaceHandler.parseKeyGenerator(element, builder.getBeanDefinition()); + + List cacheDefs = DomUtils.getChildElementsByTagName(element, DEFS_ELEMENT); + if (!cacheDefs.isEmpty()) { + // Using attributes source. + List attributeSourceDefinitions = parseDefinitionsSources(cacheDefs, parserContext); + builder.addPropertyValue("cacheOperationSources", attributeSourceDefinitions); + } + else { + // Assume annotations source. + builder.addPropertyValue("cacheOperationSources", + new RootBeanDefinition("org.springframework.cache.annotation.AnnotationCacheOperationSource")); + } + } + + private List parseDefinitionsSources(List definitions, ParserContext parserContext) { + ManagedList defs = new ManagedList<>(definitions.size()); + + // extract default param for the definition + for (Element element : definitions) { + defs.add(parseDefinitionSource(element, parserContext)); + } + + return defs; + } + + private RootBeanDefinition parseDefinitionSource(Element definition, ParserContext parserContext) { + Props prop = new Props(definition); + // add cacheable first + + ManagedMap> cacheOpMap = new ManagedMap<>(); + cacheOpMap.setSource(parserContext.extractSource(definition)); + + List cacheableCacheMethods = DomUtils.getChildElementsByTagName(definition, CACHEABLE_ELEMENT); + + for (Element opElement : cacheableCacheMethods) { + String name = prop.merge(opElement, parserContext.getReaderContext()); + TypedStringValue nameHolder = new TypedStringValue(name); + nameHolder.setSource(parserContext.extractSource(opElement)); + CacheableOperation.Builder builder = prop.merge(opElement, + parserContext.getReaderContext(), new CacheableOperation.Builder()); + builder.setUnless(getAttributeValue(opElement, "unless", "")); + builder.setSync(Boolean.parseBoolean(getAttributeValue(opElement, "sync", "false"))); + + Collection col = cacheOpMap.computeIfAbsent(nameHolder, k -> new ArrayList<>(2)); + col.add(builder.build()); + } + + List evictCacheMethods = DomUtils.getChildElementsByTagName(definition, CACHE_EVICT_ELEMENT); + + for (Element opElement : evictCacheMethods) { + String name = prop.merge(opElement, parserContext.getReaderContext()); + TypedStringValue nameHolder = new TypedStringValue(name); + nameHolder.setSource(parserContext.extractSource(opElement)); + CacheEvictOperation.Builder builder = prop.merge(opElement, + parserContext.getReaderContext(), new CacheEvictOperation.Builder()); + + String wide = opElement.getAttribute("all-entries"); + if (StringUtils.hasText(wide)) { + builder.setCacheWide(Boolean.parseBoolean(wide.trim())); + } + + String after = opElement.getAttribute("before-invocation"); + if (StringUtils.hasText(after)) { + builder.setBeforeInvocation(Boolean.parseBoolean(after.trim())); + } + + Collection col = cacheOpMap.computeIfAbsent(nameHolder, k -> new ArrayList<>(2)); + col.add(builder.build()); + } + + List putCacheMethods = DomUtils.getChildElementsByTagName(definition, CACHE_PUT_ELEMENT); + + for (Element opElement : putCacheMethods) { + String name = prop.merge(opElement, parserContext.getReaderContext()); + TypedStringValue nameHolder = new TypedStringValue(name); + nameHolder.setSource(parserContext.extractSource(opElement)); + CachePutOperation.Builder builder = prop.merge(opElement, + parserContext.getReaderContext(), new CachePutOperation.Builder()); + builder.setUnless(getAttributeValue(opElement, "unless", "")); + + Collection col = cacheOpMap.computeIfAbsent(nameHolder, k -> new ArrayList<>(2)); + col.add(builder.build()); + } + + RootBeanDefinition attributeSourceDefinition = new RootBeanDefinition(NameMatchCacheOperationSource.class); + attributeSourceDefinition.setSource(parserContext.extractSource(definition)); + attributeSourceDefinition.getPropertyValues().add("nameMap", cacheOpMap); + return attributeSourceDefinition; + } + + + private static String getAttributeValue(Element element, String attributeName, String defaultValue) { + String attribute = element.getAttribute(attributeName); + if (StringUtils.hasText(attribute)) { + return attribute.trim(); + } + return defaultValue; + } + + + /** + * Simple, reusable class used for overriding defaults. + */ + private static class Props { + + private String key; + + private String keyGenerator; + + private String cacheManager; + + private String condition; + + private String method; + + @Nullable + private String[] caches; + + Props(Element root) { + String defaultCache = root.getAttribute("cache"); + this.key = root.getAttribute("key"); + this.keyGenerator = root.getAttribute("key-generator"); + this.cacheManager = root.getAttribute("cache-manager"); + this.condition = root.getAttribute("condition"); + this.method = root.getAttribute(METHOD_ATTRIBUTE); + + if (StringUtils.hasText(defaultCache)) { + this.caches = StringUtils.commaDelimitedListToStringArray(defaultCache.trim()); + } + } + + T merge(Element element, ReaderContext readerCtx, T builder) { + String cache = element.getAttribute("cache"); + + // sanity check + String[] localCaches = this.caches; + if (StringUtils.hasText(cache)) { + localCaches = StringUtils.commaDelimitedListToStringArray(cache.trim()); + } + if (localCaches != null) { + builder.setCacheNames(localCaches); + } + else { + readerCtx.error("No cache specified for " + element.getNodeName(), element); + } + + builder.setKey(getAttributeValue(element, "key", this.key)); + builder.setKeyGenerator(getAttributeValue(element, "key-generator", this.keyGenerator)); + builder.setCacheManager(getAttributeValue(element, "cache-manager", this.cacheManager)); + builder.setCondition(getAttributeValue(element, "condition", this.condition)); + + if (StringUtils.hasText(builder.getKey()) && StringUtils.hasText(builder.getKeyGenerator())) { + throw new IllegalStateException("Invalid cache advice configuration on '" + + element.toString() + "'. Both 'key' and 'keyGenerator' attributes have been set. " + + "These attributes are mutually exclusive: either set the SpEL expression used to" + + "compute the key at runtime or set the name of the KeyGenerator bean to use."); + } + + return builder; + } + + @Nullable + String merge(Element element, ReaderContext readerCtx) { + String method = element.getAttribute(METHOD_ATTRIBUTE); + if (StringUtils.hasText(method)) { + return method.trim(); + } + if (StringUtils.hasText(this.method)) { + return this.method; + } + readerCtx.error("No method specified for " + element.getNodeName(), element); + return null; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/config/CacheManagementConfigUtils.java b/spring-context/src/main/java/org/springframework/cache/config/CacheManagementConfigUtils.java new file mode 100644 index 0000000..957880e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/config/CacheManagementConfigUtils.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.config; + +/** + * Configuration constants for internal sharing across subpackages. + * + * @author Juergen Hoeller + * @since 4.1 + */ +public abstract class CacheManagementConfigUtils { + + /** + * The name of the cache advisor bean. + */ + public static final String CACHE_ADVISOR_BEAN_NAME = + "org.springframework.cache.config.internalCacheAdvisor"; + + /** + * The name of the cache aspect bean. + */ + public static final String CACHE_ASPECT_BEAN_NAME = + "org.springframework.cache.config.internalCacheAspect"; + + /** + * The name of the JCache advisor bean. + */ + public static final String JCACHE_ADVISOR_BEAN_NAME = + "org.springframework.cache.config.internalJCacheAdvisor"; + + /** + * The name of the JCache advisor bean. + */ + public static final String JCACHE_ASPECT_BEAN_NAME = + "org.springframework.cache.config.internalJCacheAspect"; + +} diff --git a/spring-context/src/main/java/org/springframework/cache/config/CacheNamespaceHandler.java b/spring-context/src/main/java/org/springframework/cache/config/CacheNamespaceHandler.java new file mode 100644 index 0000000..a2a1d72 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/config/CacheNamespaceHandler.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.xml.NamespaceHandlerSupport; +import org.springframework.util.StringUtils; + +/** + * {@code NamespaceHandler} allowing for the configuration of declarative + * cache management using either XML or using annotations. + * + *

    This namespace handler is the central piece of functionality in the + * Spring cache management facilities. + * + * @author Costin Leau + * @since 3.1 + */ +public class CacheNamespaceHandler extends NamespaceHandlerSupport { + + static final String CACHE_MANAGER_ATTRIBUTE = "cache-manager"; + + static final String DEFAULT_CACHE_MANAGER_BEAN_NAME = "cacheManager"; + + + static String extractCacheManager(Element element) { + return (element.hasAttribute(CacheNamespaceHandler.CACHE_MANAGER_ATTRIBUTE) ? + element.getAttribute(CacheNamespaceHandler.CACHE_MANAGER_ATTRIBUTE) : + CacheNamespaceHandler.DEFAULT_CACHE_MANAGER_BEAN_NAME); + } + + static BeanDefinition parseKeyGenerator(Element element, BeanDefinition def) { + String name = element.getAttribute("key-generator"); + if (StringUtils.hasText(name)) { + def.getPropertyValues().add("keyGenerator", new RuntimeBeanReference(name.trim())); + } + return def; + } + + + @Override + public void init() { + registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenCacheBeanDefinitionParser()); + registerBeanDefinitionParser("advice", new CacheAdviceParser()); + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/config/package-info.java b/spring-context/src/main/java/org/springframework/cache/config/package-info.java new file mode 100644 index 0000000..b263056 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/config/package-info.java @@ -0,0 +1,12 @@ +/** + * Support package for declarative caching configuration, with XML + * schema being the primary configuration format. See {@link + * org.springframework.cache.annotation.EnableCaching EnableCaching} + * for details on code-based configuration without XML. + */ +@NonNullApi +@NonNullFields +package org.springframework.cache.config; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheInvoker.java b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheInvoker.java new file mode 100644 index 0000000..4710f8e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheInvoker.java @@ -0,0 +1,130 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import org.springframework.cache.Cache; +import org.springframework.lang.Nullable; +import org.springframework.util.function.SingletonSupplier; + +/** + * A base component for invoking {@link Cache} operations and using a + * configurable {@link CacheErrorHandler} when an exception occurs. + * + * @author Stephane Nicoll + * @author Juergen Hoeller + * @since 4.1 + * @see org.springframework.cache.interceptor.CacheErrorHandler + */ +public abstract class AbstractCacheInvoker { + + protected SingletonSupplier errorHandler; + + + protected AbstractCacheInvoker() { + this.errorHandler = SingletonSupplier.of(SimpleCacheErrorHandler::new); + } + + protected AbstractCacheInvoker(CacheErrorHandler errorHandler) { + this.errorHandler = SingletonSupplier.of(errorHandler); + } + + + /** + * Set the {@link CacheErrorHandler} instance to use to handle errors + * thrown by the cache provider. By default, a {@link SimpleCacheErrorHandler} + * is used who throws any exception as is. + */ + public void setErrorHandler(CacheErrorHandler errorHandler) { + this.errorHandler = SingletonSupplier.of(errorHandler); + } + + /** + * Return the {@link CacheErrorHandler} to use. + */ + public CacheErrorHandler getErrorHandler() { + return this.errorHandler.obtain(); + } + + + /** + * Execute {@link Cache#get(Object)} on the specified {@link Cache} and + * invoke the error handler if an exception occurs. Return {@code null} + * if the handler does not throw any exception, which simulates a cache + * miss in case of error. + * @see Cache#get(Object) + */ + @Nullable + protected Cache.ValueWrapper doGet(Cache cache, Object key) { + try { + return cache.get(key); + } + catch (RuntimeException ex) { + getErrorHandler().handleCacheGetError(ex, cache, key); + return null; // If the exception is handled, return a cache miss + } + } + + /** + * Execute {@link Cache#put(Object, Object)} on the specified {@link Cache} + * and invoke the error handler if an exception occurs. + */ + protected void doPut(Cache cache, Object key, @Nullable Object result) { + try { + cache.put(key, result); + } + catch (RuntimeException ex) { + getErrorHandler().handleCachePutError(ex, cache, key, result); + } + } + + /** + * Execute {@link Cache#evict(Object)}/{@link Cache#evictIfPresent(Object)} on the + * specified {@link Cache} and invoke the error handler if an exception occurs. + */ + protected void doEvict(Cache cache, Object key, boolean immediate) { + try { + if (immediate) { + cache.evictIfPresent(key); + } + else { + cache.evict(key); + } + } + catch (RuntimeException ex) { + getErrorHandler().handleCacheEvictError(ex, cache, key); + } + } + + /** + * Execute {@link Cache#clear()} on the specified {@link Cache} and + * invoke the error handler if an exception occurs. + */ + protected void doClear(Cache cache, boolean immediate) { + try { + if (immediate) { + cache.invalidate(); + } + else { + cache.clear(); + } + } + catch (RuntimeException ex) { + getErrorHandler().handleCacheClearError(ex, cache); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheResolver.java b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheResolver.java new file mode 100644 index 0000000..926a44a --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheResolver.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * A base {@link CacheResolver} implementation that requires the concrete + * implementation to provide the collection of cache name(s) based on the + * invocation context. + * + * @author Stephane Nicoll + * @author Juergen Hoeller + * @since 4.1 + */ +public abstract class AbstractCacheResolver implements CacheResolver, InitializingBean { + + @Nullable + private CacheManager cacheManager; + + + /** + * Construct a new {@code AbstractCacheResolver}. + * @see #setCacheManager + */ + protected AbstractCacheResolver() { + } + + /** + * Construct a new {@code AbstractCacheResolver} for the given {@link CacheManager}. + * @param cacheManager the CacheManager to use + */ + protected AbstractCacheResolver(CacheManager cacheManager) { + this.cacheManager = cacheManager; + } + + + /** + * Set the {@link CacheManager} that this instance should use. + */ + public void setCacheManager(CacheManager cacheManager) { + this.cacheManager = cacheManager; + } + + /** + * Return the {@link CacheManager} that this instance uses. + */ + public CacheManager getCacheManager() { + Assert.state(this.cacheManager != null, "No CacheManager set"); + return this.cacheManager; + } + + @Override + public void afterPropertiesSet() { + Assert.notNull(this.cacheManager, "CacheManager is required"); + } + + + @Override + public Collection resolveCaches(CacheOperationInvocationContext context) { + Collection cacheNames = getCacheNames(context); + if (cacheNames == null) { + return Collections.emptyList(); + } + Collection result = new ArrayList<>(cacheNames.size()); + for (String cacheName : cacheNames) { + Cache cache = getCacheManager().getCache(cacheName); + if (cache == null) { + throw new IllegalArgumentException("Cannot find cache named '" + + cacheName + "' for " + context.getOperation()); + } + result.add(cache); + } + return result; + } + + /** + * Provide the name of the cache(s) to resolve against the current cache manager. + *

    It is acceptable to return {@code null} to indicate that no cache could + * be resolved for this invocation. + * @param context the context of the particular invocation + * @return the cache name(s) to resolve, or {@code null} if no cache should be resolved + */ + @Nullable + protected abstract Collection getCacheNames(CacheOperationInvocationContext context); + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java new file mode 100644 index 0000000..8de5288 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java @@ -0,0 +1,191 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aop.support.AopUtils; +import org.springframework.core.MethodClassKey; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * Abstract implementation of {@link CacheOperation} that caches attributes + * for methods and implements a fallback policy: 1. specific target method; + * 2. target class; 3. declaring method; 4. declaring class/interface. + * + *

    Defaults to using the target class's caching attribute if none is + * associated with the target method. Any caching attribute associated with + * the target method completely overrides a class caching attribute. + * If none found on the target class, the interface that the invoked method + * has been called through (in case of a JDK proxy) will be checked. + * + *

    This implementation caches attributes by method after they are first + * used. If it is ever desirable to allow dynamic changing of cacheable + * attributes (which is very unlikely), caching could be made configurable. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 3.1 + */ +public abstract class AbstractFallbackCacheOperationSource implements CacheOperationSource { + + /** + * Canonical value held in cache to indicate no caching attribute was + * found for this method and we don't need to look again. + */ + private static final Collection NULL_CACHING_ATTRIBUTE = Collections.emptyList(); + + + /** + * Logger available to subclasses. + *

    As this base class is not marked Serializable, the logger will be recreated + * after serialization - provided that the concrete subclass is Serializable. + */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** + * Cache of CacheOperations, keyed by method on a specific target class. + *

    As this base class is not marked Serializable, the cache will be recreated + * after serialization - provided that the concrete subclass is Serializable. + */ + private final Map> attributeCache = new ConcurrentHashMap<>(1024); + + + /** + * Determine the caching attribute for this method invocation. + *

    Defaults to the class's caching attribute if no method attribute is found. + * @param method the method for the current invocation (never {@code null}) + * @param targetClass the target class for this invocation (may be {@code null}) + * @return {@link CacheOperation} for this method, or {@code null} if the method + * is not cacheable + */ + @Override + @Nullable + public Collection getCacheOperations(Method method, @Nullable Class targetClass) { + if (method.getDeclaringClass() == Object.class) { + return null; + } + + Object cacheKey = getCacheKey(method, targetClass); + Collection cached = this.attributeCache.get(cacheKey); + + if (cached != null) { + return (cached != NULL_CACHING_ATTRIBUTE ? cached : null); + } + else { + Collection cacheOps = computeCacheOperations(method, targetClass); + if (cacheOps != null) { + if (logger.isTraceEnabled()) { + logger.trace("Adding cacheable method '" + method.getName() + "' with attribute: " + cacheOps); + } + this.attributeCache.put(cacheKey, cacheOps); + } + else { + this.attributeCache.put(cacheKey, NULL_CACHING_ATTRIBUTE); + } + return cacheOps; + } + } + + /** + * Determine a cache key for the given method and target class. + *

    Must not produce same key for overloaded methods. + * Must produce same key for different instances of the same method. + * @param method the method (never {@code null}) + * @param targetClass the target class (may be {@code null}) + * @return the cache key (never {@code null}) + */ + protected Object getCacheKey(Method method, @Nullable Class targetClass) { + return new MethodClassKey(method, targetClass); + } + + @Nullable + private Collection computeCacheOperations(Method method, @Nullable Class targetClass) { + // Don't allow no-public methods as required. + if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { + return null; + } + + // The method may be on an interface, but we need attributes from the target class. + // If the target class is null, the method will be unchanged. + Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); + + // First try is the method in the target class. + Collection opDef = findCacheOperations(specificMethod); + if (opDef != null) { + return opDef; + } + + // Second try is the caching operation on the target class. + opDef = findCacheOperations(specificMethod.getDeclaringClass()); + if (opDef != null && ClassUtils.isUserLevelMethod(method)) { + return opDef; + } + + if (specificMethod != method) { + // Fallback is to look at the original method. + opDef = findCacheOperations(method); + if (opDef != null) { + return opDef; + } + // Last fallback is the class of the original method. + opDef = findCacheOperations(method.getDeclaringClass()); + if (opDef != null && ClassUtils.isUserLevelMethod(method)) { + return opDef; + } + } + + return null; + } + + + /** + * Subclasses need to implement this to return the caching attribute for the + * given class, if any. + * @param clazz the class to retrieve the attribute for + * @return all caching attribute associated with this class, or {@code null} if none + */ + @Nullable + protected abstract Collection findCacheOperations(Class clazz); + + /** + * Subclasses need to implement this to return the caching attribute for the + * given method, if any. + * @param method the method to retrieve the attribute for + * @return all caching attribute associated with this method, or {@code null} if none + */ + @Nullable + protected abstract Collection findCacheOperations(Method method); + + /** + * Should only public methods be allowed to have caching semantics? + *

    The default implementation returns {@code false}. + */ + protected boolean allowPublicMethodsOnly() { + return false; + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/BasicOperation.java b/spring-context/src/main/java/org/springframework/cache/interceptor/BasicOperation.java new file mode 100644 index 0000000..d17c551 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/BasicOperation.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import java.util.Set; + +/** + * The base interface that all cache operations must implement. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public interface BasicOperation { + + /** + * Return the cache name(s) associated with the operation. + */ + Set getCacheNames(); + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/BeanFactoryCacheOperationSourceAdvisor.java b/spring-context/src/main/java/org/springframework/cache/interceptor/BeanFactoryCacheOperationSourceAdvisor.java new file mode 100644 index 0000000..efd0cf7 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/BeanFactoryCacheOperationSourceAdvisor.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import org.springframework.aop.ClassFilter; +import org.springframework.aop.Pointcut; +import org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor; +import org.springframework.lang.Nullable; + +/** + * Advisor driven by a {@link CacheOperationSource}, used to include a + * cache advice bean for methods that are cacheable. + * + * @author Costin Leau + * @since 3.1 + */ +@SuppressWarnings("serial") +public class BeanFactoryCacheOperationSourceAdvisor extends AbstractBeanFactoryPointcutAdvisor { + + @Nullable + private CacheOperationSource cacheOperationSource; + + private final CacheOperationSourcePointcut pointcut = new CacheOperationSourcePointcut() { + @Override + @Nullable + protected CacheOperationSource getCacheOperationSource() { + return cacheOperationSource; + } + }; + + + /** + * Set the cache operation attribute source which is used to find cache + * attributes. This should usually be identical to the source reference + * set on the cache interceptor itself. + */ + public void setCacheOperationSource(CacheOperationSource cacheOperationSource) { + this.cacheOperationSource = cacheOperationSource; + } + + /** + * Set the {@link ClassFilter} to use for this pointcut. + * Default is {@link ClassFilter#TRUE}. + */ + public void setClassFilter(ClassFilter classFilter) { + this.pointcut.setClassFilter(classFilter); + } + + @Override + public Pointcut getPointcut() { + return this.pointcut; + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java new file mode 100644 index 0000000..4d7d856 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java @@ -0,0 +1,897 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aop.framework.AopProxyUtils; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.context.expression.AnnotatedElementKey; +import org.springframework.core.BridgeMethodResolver; +import org.springframework.expression.EvaluationContext; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; +import org.springframework.util.function.SingletonSupplier; +import org.springframework.util.function.SupplierUtils; + +/** + * Base class for caching aspects, such as the {@link CacheInterceptor} or an + * AspectJ aspect. + * + *

    This enables the underlying Spring caching infrastructure to be used easily + * to implement an aspect for any aspect system. + * + *

    Subclasses are responsible for calling relevant methods in the correct order. + * + *

    Uses the Strategy design pattern. A {@link CacheOperationSource} is + * used for determining caching operations, a {@link KeyGenerator} will build the + * cache keys, and a {@link CacheResolver} will resolve the actual cache(s) to use. + * + *

    Note: A cache aspect is serializable but does not perform any actual caching + * after deserialization. + * + * @author Costin Leau + * @author Juergen Hoeller + * @author Chris Beams + * @author Phillip Webb + * @author Sam Brannen + * @author Stephane Nicoll + * @since 3.1 + */ +public abstract class CacheAspectSupport extends AbstractCacheInvoker + implements BeanFactoryAware, InitializingBean, SmartInitializingSingleton { + + protected final Log logger = LogFactory.getLog(getClass()); + + private final Map metadataCache = new ConcurrentHashMap<>(1024); + + private final CacheOperationExpressionEvaluator evaluator = new CacheOperationExpressionEvaluator(); + + @Nullable + private CacheOperationSource cacheOperationSource; + + private SingletonSupplier keyGenerator = SingletonSupplier.of(SimpleKeyGenerator::new); + + @Nullable + private SingletonSupplier cacheResolver; + + @Nullable + private BeanFactory beanFactory; + + private boolean initialized = false; + + + /** + * Configure this aspect with the given error handler, key generator and cache resolver/manager + * suppliers, applying the corresponding default if a supplier is not resolvable. + * @since 5.1 + */ + public void configure( + @Nullable Supplier errorHandler, @Nullable Supplier keyGenerator, + @Nullable Supplier cacheResolver, @Nullable Supplier cacheManager) { + + this.errorHandler = new SingletonSupplier<>(errorHandler, SimpleCacheErrorHandler::new); + this.keyGenerator = new SingletonSupplier<>(keyGenerator, SimpleKeyGenerator::new); + this.cacheResolver = new SingletonSupplier<>(cacheResolver, + () -> SimpleCacheResolver.of(SupplierUtils.resolve(cacheManager))); + } + + + /** + * Set one or more cache operation sources which are used to find the cache + * attributes. If more than one source is provided, they will be aggregated + * using a {@link CompositeCacheOperationSource}. + * @see #setCacheOperationSource + */ + public void setCacheOperationSources(CacheOperationSource... cacheOperationSources) { + Assert.notEmpty(cacheOperationSources, "At least 1 CacheOperationSource needs to be specified"); + this.cacheOperationSource = (cacheOperationSources.length > 1 ? + new CompositeCacheOperationSource(cacheOperationSources) : cacheOperationSources[0]); + } + + /** + * Set the CacheOperationSource for this cache aspect. + * @since 5.1 + * @see #setCacheOperationSources + */ + public void setCacheOperationSource(@Nullable CacheOperationSource cacheOperationSource) { + this.cacheOperationSource = cacheOperationSource; + } + + /** + * Return the CacheOperationSource for this cache aspect. + */ + @Nullable + public CacheOperationSource getCacheOperationSource() { + return this.cacheOperationSource; + } + + /** + * Set the default {@link KeyGenerator} that this cache aspect should delegate to + * if no specific key generator has been set for the operation. + *

    The default is a {@link SimpleKeyGenerator}. + */ + public void setKeyGenerator(KeyGenerator keyGenerator) { + this.keyGenerator = SingletonSupplier.of(keyGenerator); + } + + /** + * Return the default {@link KeyGenerator} that this cache aspect delegates to. + */ + public KeyGenerator getKeyGenerator() { + return this.keyGenerator.obtain(); + } + + /** + * Set the default {@link CacheResolver} that this cache aspect should delegate + * to if no specific cache resolver has been set for the operation. + *

    The default resolver resolves the caches against their names and the + * default cache manager. + * @see #setCacheManager + * @see SimpleCacheResolver + */ + public void setCacheResolver(@Nullable CacheResolver cacheResolver) { + this.cacheResolver = SingletonSupplier.ofNullable(cacheResolver); + } + + /** + * Return the default {@link CacheResolver} that this cache aspect delegates to. + */ + @Nullable + public CacheResolver getCacheResolver() { + return SupplierUtils.resolve(this.cacheResolver); + } + + /** + * Set the {@link CacheManager} to use to create a default {@link CacheResolver}. + * Replace the current {@link CacheResolver}, if any. + * @see #setCacheResolver + * @see SimpleCacheResolver + */ + public void setCacheManager(CacheManager cacheManager) { + this.cacheResolver = SingletonSupplier.of(new SimpleCacheResolver(cacheManager)); + } + + /** + * Set the containing {@link BeanFactory} for {@link CacheManager} and other + * service lookups. + * @since 4.3 + */ + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + + @Override + public void afterPropertiesSet() { + Assert.state(getCacheOperationSource() != null, "The 'cacheOperationSources' property is required: " + + "If there are no cacheable methods, then don't use a cache aspect."); + } + + @Override + public void afterSingletonsInstantiated() { + if (getCacheResolver() == null) { + // Lazily initialize cache resolver via default cache manager... + Assert.state(this.beanFactory != null, "CacheResolver or BeanFactory must be set on cache aspect"); + try { + setCacheManager(this.beanFactory.getBean(CacheManager.class)); + } + catch (NoUniqueBeanDefinitionException ex) { + throw new IllegalStateException("No CacheResolver specified, and no unique bean of type " + + "CacheManager found. Mark one as primary or declare a specific CacheManager to use."); + } + catch (NoSuchBeanDefinitionException ex) { + throw new IllegalStateException("No CacheResolver specified, and no bean of type CacheManager found. " + + "Register a CacheManager bean or remove the @EnableCaching annotation from your configuration."); + } + } + this.initialized = true; + } + + + /** + * Convenience method to return a String representation of this Method + * for use in logging. Can be overridden in subclasses to provide a + * different identifier for the given method. + * @param method the method we're interested in + * @param targetClass class the method is on + * @return log message identifying this method + * @see org.springframework.util.ClassUtils#getQualifiedMethodName + */ + protected String methodIdentification(Method method, Class targetClass) { + Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass); + return ClassUtils.getQualifiedMethodName(specificMethod); + } + + protected Collection getCaches( + CacheOperationInvocationContext context, CacheResolver cacheResolver) { + + Collection caches = cacheResolver.resolveCaches(context); + if (caches.isEmpty()) { + throw new IllegalStateException("No cache could be resolved for '" + + context.getOperation() + "' using resolver '" + cacheResolver + + "'. At least one cache should be provided per cache operation."); + } + return caches; + } + + protected CacheOperationContext getOperationContext( + CacheOperation operation, Method method, Object[] args, Object target, Class targetClass) { + + CacheOperationMetadata metadata = getCacheOperationMetadata(operation, method, targetClass); + return new CacheOperationContext(metadata, args, target); + } + + /** + * Return the {@link CacheOperationMetadata} for the specified operation. + *

    Resolve the {@link CacheResolver} and the {@link KeyGenerator} to be + * used for the operation. + * @param operation the operation + * @param method the method on which the operation is invoked + * @param targetClass the target type + * @return the resolved metadata for the operation + */ + protected CacheOperationMetadata getCacheOperationMetadata( + CacheOperation operation, Method method, Class targetClass) { + + CacheOperationCacheKey cacheKey = new CacheOperationCacheKey(operation, method, targetClass); + CacheOperationMetadata metadata = this.metadataCache.get(cacheKey); + if (metadata == null) { + KeyGenerator operationKeyGenerator; + if (StringUtils.hasText(operation.getKeyGenerator())) { + operationKeyGenerator = getBean(operation.getKeyGenerator(), KeyGenerator.class); + } + else { + operationKeyGenerator = getKeyGenerator(); + } + CacheResolver operationCacheResolver; + if (StringUtils.hasText(operation.getCacheResolver())) { + operationCacheResolver = getBean(operation.getCacheResolver(), CacheResolver.class); + } + else if (StringUtils.hasText(operation.getCacheManager())) { + CacheManager cacheManager = getBean(operation.getCacheManager(), CacheManager.class); + operationCacheResolver = new SimpleCacheResolver(cacheManager); + } + else { + operationCacheResolver = getCacheResolver(); + Assert.state(operationCacheResolver != null, "No CacheResolver/CacheManager set"); + } + metadata = new CacheOperationMetadata(operation, method, targetClass, + operationKeyGenerator, operationCacheResolver); + this.metadataCache.put(cacheKey, metadata); + } + return metadata; + } + + /** + * Return a bean with the specified name and type. Used to resolve services that + * are referenced by name in a {@link CacheOperation}. + * @param beanName the name of the bean, as defined by the operation + * @param expectedType type for the bean + * @return the bean matching that name + * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException if such bean does not exist + * @see CacheOperation#getKeyGenerator() + * @see CacheOperation#getCacheManager() + * @see CacheOperation#getCacheResolver() + */ + protected T getBean(String beanName, Class expectedType) { + if (this.beanFactory == null) { + throw new IllegalStateException( + "BeanFactory must be set on cache aspect for " + expectedType.getSimpleName() + " retrieval"); + } + return BeanFactoryAnnotationUtils.qualifiedBeanOfType(this.beanFactory, expectedType, beanName); + } + + /** + * Clear the cached metadata. + */ + protected void clearMetadataCache() { + this.metadataCache.clear(); + this.evaluator.clear(); + } + + @Nullable + protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) { + // Check whether aspect is enabled (to cope with cases where the AJ is pulled in automatically) + if (this.initialized) { + Class targetClass = getTargetClass(target); + CacheOperationSource cacheOperationSource = getCacheOperationSource(); + if (cacheOperationSource != null) { + Collection operations = cacheOperationSource.getCacheOperations(method, targetClass); + if (!CollectionUtils.isEmpty(operations)) { + return execute(invoker, method, + new CacheOperationContexts(operations, method, args, target, targetClass)); + } + } + } + + return invoker.invoke(); + } + + /** + * Execute the underlying operation (typically in case of cache miss) and return + * the result of the invocation. If an exception occurs it will be wrapped in a + * {@link CacheOperationInvoker.ThrowableWrapper}: the exception can be handled + * or modified but it must be wrapped in a + * {@link CacheOperationInvoker.ThrowableWrapper} as well. + * @param invoker the invoker handling the operation being cached + * @return the result of the invocation + * @see CacheOperationInvoker#invoke() + */ + @Nullable + protected Object invokeOperation(CacheOperationInvoker invoker) { + return invoker.invoke(); + } + + private Class getTargetClass(Object target) { + return AopProxyUtils.ultimateTargetClass(target); + } + + @Nullable + private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) { + // Special handling of synchronized invocation + if (contexts.isSynchronized()) { + CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next(); + if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) { + Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT); + Cache cache = context.getCaches().iterator().next(); + try { + return wrapCacheValue(method, handleSynchronizedGet(invoker, key, cache)); + } + catch (Cache.ValueRetrievalException ex) { + // Directly propagate ThrowableWrapper from the invoker, + // or potentially also an IllegalArgumentException etc. + ReflectionUtils.rethrowRuntimeException(ex.getCause()); + } + } + else { + // No caching required, only call the underlying method + return invokeOperation(invoker); + } + } + + + // Process any early evictions + processCacheEvicts(contexts.get(CacheEvictOperation.class), true, + CacheOperationExpressionEvaluator.NO_RESULT); + + // Check if we have a cached item matching the conditions + Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class)); + + // Collect puts from any @Cacheable miss, if no cached item is found + List cachePutRequests = new ArrayList<>(); + if (cacheHit == null) { + collectPutRequests(contexts.get(CacheableOperation.class), + CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests); + } + + Object cacheValue; + Object returnValue; + + if (cacheHit != null && !hasCachePut(contexts)) { + // If there are no put requests, just use the cache hit + cacheValue = cacheHit.get(); + returnValue = wrapCacheValue(method, cacheValue); + } + else { + // Invoke the method if we don't have a cache hit + returnValue = invokeOperation(invoker); + cacheValue = unwrapReturnValue(returnValue); + } + + // Collect any explicit @CachePuts + collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests); + + // Process any collected put requests, either from @CachePut or a @Cacheable miss + for (CachePutRequest cachePutRequest : cachePutRequests) { + cachePutRequest.apply(cacheValue); + } + + // Process any late evictions + processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue); + + return returnValue; + } + + @Nullable + private Object handleSynchronizedGet(CacheOperationInvoker invoker, Object key, Cache cache) { + InvocationAwareResult invocationResult = new InvocationAwareResult(); + Object result = cache.get(key, () -> { + invocationResult.invoked = true; + if (logger.isTraceEnabled()) { + logger.trace("No cache entry for key '" + key + "' in cache " + cache.getName()); + } + return unwrapReturnValue(invokeOperation(invoker)); + }); + if (!invocationResult.invoked && logger.isTraceEnabled()) { + logger.trace("Cache entry for key '" + key + "' found in cache '" + cache.getName() + "'"); + } + return result; + } + + @Nullable + private Object wrapCacheValue(Method method, @Nullable Object cacheValue) { + if (method.getReturnType() == Optional.class && + (cacheValue == null || cacheValue.getClass() != Optional.class)) { + return Optional.ofNullable(cacheValue); + } + return cacheValue; + } + + @Nullable + private Object unwrapReturnValue(@Nullable Object returnValue) { + return ObjectUtils.unwrapOptional(returnValue); + } + + private boolean hasCachePut(CacheOperationContexts contexts) { + // Evaluate the conditions *without* the result object because we don't have it yet... + Collection cachePutContexts = contexts.get(CachePutOperation.class); + Collection excluded = new ArrayList<>(); + for (CacheOperationContext context : cachePutContexts) { + try { + if (!context.isConditionPassing(CacheOperationExpressionEvaluator.RESULT_UNAVAILABLE)) { + excluded.add(context); + } + } + catch (VariableNotAvailableException ex) { + // Ignoring failure due to missing result, consider the cache put has to proceed + } + } + // Check if all puts have been excluded by condition + return (cachePutContexts.size() != excluded.size()); + } + + private void processCacheEvicts( + Collection contexts, boolean beforeInvocation, @Nullable Object result) { + + for (CacheOperationContext context : contexts) { + CacheEvictOperation operation = (CacheEvictOperation) context.metadata.operation; + if (beforeInvocation == operation.isBeforeInvocation() && isConditionPassing(context, result)) { + performCacheEvict(context, operation, result); + } + } + } + + private void performCacheEvict( + CacheOperationContext context, CacheEvictOperation operation, @Nullable Object result) { + + Object key = null; + for (Cache cache : context.getCaches()) { + if (operation.isCacheWide()) { + logInvalidating(context, operation, null); + doClear(cache, operation.isBeforeInvocation()); + } + else { + if (key == null) { + key = generateKey(context, result); + } + logInvalidating(context, operation, key); + doEvict(cache, key, operation.isBeforeInvocation()); + } + } + } + + private void logInvalidating(CacheOperationContext context, CacheEvictOperation operation, @Nullable Object key) { + if (logger.isTraceEnabled()) { + logger.trace("Invalidating " + (key != null ? "cache key [" + key + "]" : "entire cache") + + " for operation " + operation + " on method " + context.metadata.method); + } + } + + /** + * Find a cached item only for {@link CacheableOperation} that passes the condition. + * @param contexts the cacheable operations + * @return a {@link Cache.ValueWrapper} holding the cached item, + * or {@code null} if none is found + */ + @Nullable + private Cache.ValueWrapper findCachedItem(Collection contexts) { + Object result = CacheOperationExpressionEvaluator.NO_RESULT; + for (CacheOperationContext context : contexts) { + if (isConditionPassing(context, result)) { + Object key = generateKey(context, result); + Cache.ValueWrapper cached = findInCaches(context, key); + if (cached != null) { + return cached; + } + else { + if (logger.isTraceEnabled()) { + logger.trace("No cache entry for key '" + key + "' in cache(s) " + context.getCacheNames()); + } + } + } + } + return null; + } + + /** + * Collect the {@link CachePutRequest} for all {@link CacheOperation} using + * the specified result item. + * @param contexts the contexts to handle + * @param result the result item (never {@code null}) + * @param putRequests the collection to update + */ + private void collectPutRequests(Collection contexts, + @Nullable Object result, Collection putRequests) { + + for (CacheOperationContext context : contexts) { + if (isConditionPassing(context, result)) { + Object key = generateKey(context, result); + putRequests.add(new CachePutRequest(context, key)); + } + } + } + + @Nullable + private Cache.ValueWrapper findInCaches(CacheOperationContext context, Object key) { + for (Cache cache : context.getCaches()) { + Cache.ValueWrapper wrapper = doGet(cache, key); + if (wrapper != null) { + if (logger.isTraceEnabled()) { + logger.trace("Cache entry for key '" + key + "' found in cache '" + cache.getName() + "'"); + } + return wrapper; + } + } + return null; + } + + private boolean isConditionPassing(CacheOperationContext context, @Nullable Object result) { + boolean passing = context.isConditionPassing(result); + if (!passing && logger.isTraceEnabled()) { + logger.trace("Cache condition failed on method " + context.metadata.method + + " for operation " + context.metadata.operation); + } + return passing; + } + + private Object generateKey(CacheOperationContext context, @Nullable Object result) { + Object key = context.generateKey(result); + if (key == null) { + throw new IllegalArgumentException("Null key returned for cache operation (maybe you are " + + "using named params on classes without debug info?) " + context.metadata.operation); + } + if (logger.isTraceEnabled()) { + logger.trace("Computed cache key '" + key + "' for operation " + context.metadata.operation); + } + return key; + } + + + private class CacheOperationContexts { + + private final MultiValueMap, CacheOperationContext> contexts; + + private final boolean sync; + + public CacheOperationContexts(Collection operations, Method method, + Object[] args, Object target, Class targetClass) { + + this.contexts = new LinkedMultiValueMap<>(operations.size()); + for (CacheOperation op : operations) { + this.contexts.add(op.getClass(), getOperationContext(op, method, args, target, targetClass)); + } + this.sync = determineSyncFlag(method); + } + + public Collection get(Class operationClass) { + Collection result = this.contexts.get(operationClass); + return (result != null ? result : Collections.emptyList()); + } + + public boolean isSynchronized() { + return this.sync; + } + + private boolean determineSyncFlag(Method method) { + List cacheOperationContexts = this.contexts.get(CacheableOperation.class); + if (cacheOperationContexts == null) { // no @Cacheable operation at all + return false; + } + boolean syncEnabled = false; + for (CacheOperationContext cacheOperationContext : cacheOperationContexts) { + if (((CacheableOperation) cacheOperationContext.getOperation()).isSync()) { + syncEnabled = true; + break; + } + } + if (syncEnabled) { + if (this.contexts.size() > 1) { + throw new IllegalStateException( + "@Cacheable(sync=true) cannot be combined with other cache operations on '" + method + "'"); + } + if (cacheOperationContexts.size() > 1) { + throw new IllegalStateException( + "Only one @Cacheable(sync=true) entry is allowed on '" + method + "'"); + } + CacheOperationContext cacheOperationContext = cacheOperationContexts.iterator().next(); + CacheableOperation operation = (CacheableOperation) cacheOperationContext.getOperation(); + if (cacheOperationContext.getCaches().size() > 1) { + throw new IllegalStateException( + "@Cacheable(sync=true) only allows a single cache on '" + operation + "'"); + } + if (StringUtils.hasText(operation.getUnless())) { + throw new IllegalStateException( + "@Cacheable(sync=true) does not support unless attribute on '" + operation + "'"); + } + return true; + } + return false; + } + } + + + /** + * Metadata of a cache operation that does not depend on a particular invocation + * which makes it a good candidate for caching. + */ + protected static class CacheOperationMetadata { + + private final CacheOperation operation; + + private final Method method; + + private final Class targetClass; + + private final Method targetMethod; + + private final AnnotatedElementKey methodKey; + + private final KeyGenerator keyGenerator; + + private final CacheResolver cacheResolver; + + public CacheOperationMetadata(CacheOperation operation, Method method, Class targetClass, + KeyGenerator keyGenerator, CacheResolver cacheResolver) { + + this.operation = operation; + this.method = BridgeMethodResolver.findBridgedMethod(method); + this.targetClass = targetClass; + this.targetMethod = (!Proxy.isProxyClass(targetClass) ? + AopUtils.getMostSpecificMethod(method, targetClass) : this.method); + this.methodKey = new AnnotatedElementKey(this.targetMethod, targetClass); + this.keyGenerator = keyGenerator; + this.cacheResolver = cacheResolver; + } + } + + + /** + * A {@link CacheOperationInvocationContext} context for a {@link CacheOperation}. + */ + protected class CacheOperationContext implements CacheOperationInvocationContext { + + private final CacheOperationMetadata metadata; + + private final Object[] args; + + private final Object target; + + private final Collection caches; + + private final Collection cacheNames; + + @Nullable + private Boolean conditionPassing; + + public CacheOperationContext(CacheOperationMetadata metadata, Object[] args, Object target) { + this.metadata = metadata; + this.args = extractArgs(metadata.method, args); + this.target = target; + this.caches = CacheAspectSupport.this.getCaches(this, metadata.cacheResolver); + this.cacheNames = createCacheNames(this.caches); + } + + @Override + public CacheOperation getOperation() { + return this.metadata.operation; + } + + @Override + public Object getTarget() { + return this.target; + } + + @Override + public Method getMethod() { + return this.metadata.method; + } + + @Override + public Object[] getArgs() { + return this.args; + } + + private Object[] extractArgs(Method method, Object[] args) { + if (!method.isVarArgs()) { + return args; + } + Object[] varArgs = ObjectUtils.toObjectArray(args[args.length - 1]); + Object[] combinedArgs = new Object[args.length - 1 + varArgs.length]; + System.arraycopy(args, 0, combinedArgs, 0, args.length - 1); + System.arraycopy(varArgs, 0, combinedArgs, args.length - 1, varArgs.length); + return combinedArgs; + } + + protected boolean isConditionPassing(@Nullable Object result) { + if (this.conditionPassing == null) { + if (StringUtils.hasText(this.metadata.operation.getCondition())) { + EvaluationContext evaluationContext = createEvaluationContext(result); + this.conditionPassing = evaluator.condition(this.metadata.operation.getCondition(), + this.metadata.methodKey, evaluationContext); + } + else { + this.conditionPassing = true; + } + } + return this.conditionPassing; + } + + protected boolean canPutToCache(@Nullable Object value) { + String unless = ""; + if (this.metadata.operation instanceof CacheableOperation) { + unless = ((CacheableOperation) this.metadata.operation).getUnless(); + } + else if (this.metadata.operation instanceof CachePutOperation) { + unless = ((CachePutOperation) this.metadata.operation).getUnless(); + } + if (StringUtils.hasText(unless)) { + EvaluationContext evaluationContext = createEvaluationContext(value); + return !evaluator.unless(unless, this.metadata.methodKey, evaluationContext); + } + return true; + } + + /** + * Compute the key for the given caching operation. + */ + @Nullable + protected Object generateKey(@Nullable Object result) { + if (StringUtils.hasText(this.metadata.operation.getKey())) { + EvaluationContext evaluationContext = createEvaluationContext(result); + return evaluator.key(this.metadata.operation.getKey(), this.metadata.methodKey, evaluationContext); + } + return this.metadata.keyGenerator.generate(this.target, this.metadata.method, this.args); + } + + private EvaluationContext createEvaluationContext(@Nullable Object result) { + return evaluator.createEvaluationContext(this.caches, this.metadata.method, this.args, + this.target, this.metadata.targetClass, this.metadata.targetMethod, result, beanFactory); + } + + protected Collection getCaches() { + return this.caches; + } + + protected Collection getCacheNames() { + return this.cacheNames; + } + + private Collection createCacheNames(Collection caches) { + Collection names = new ArrayList<>(); + for (Cache cache : caches) { + names.add(cache.getName()); + } + return names; + } + } + + + private class CachePutRequest { + + private final CacheOperationContext context; + + private final Object key; + + public CachePutRequest(CacheOperationContext context, Object key) { + this.context = context; + this.key = key; + } + + public void apply(@Nullable Object result) { + if (this.context.canPutToCache(result)) { + for (Cache cache : this.context.getCaches()) { + doPut(cache, this.key, result); + } + } + } + } + + + private static final class CacheOperationCacheKey implements Comparable { + + private final CacheOperation cacheOperation; + + private final AnnotatedElementKey methodCacheKey; + + private CacheOperationCacheKey(CacheOperation cacheOperation, Method method, Class targetClass) { + this.cacheOperation = cacheOperation; + this.methodCacheKey = new AnnotatedElementKey(method, targetClass); + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof CacheOperationCacheKey)) { + return false; + } + CacheOperationCacheKey otherKey = (CacheOperationCacheKey) other; + return (this.cacheOperation.equals(otherKey.cacheOperation) && + this.methodCacheKey.equals(otherKey.methodCacheKey)); + } + + @Override + public int hashCode() { + return (this.cacheOperation.hashCode() * 31 + this.methodCacheKey.hashCode()); + } + + @Override + public String toString() { + return this.cacheOperation + " on " + this.methodCacheKey; + } + + @Override + public int compareTo(CacheOperationCacheKey other) { + int result = this.cacheOperation.getName().compareTo(other.cacheOperation.getName()); + if (result == 0) { + result = this.methodCacheKey.compareTo(other.methodCacheKey); + } + return result; + } + } + + /** + * Internal holder class for recording that a cache method was invoked. + */ + private static class InvocationAwareResult { + + boolean invoked; + + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheErrorHandler.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheErrorHandler.java new file mode 100644 index 0000000..a31ad42 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheErrorHandler.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import org.springframework.cache.Cache; +import org.springframework.lang.Nullable; + +/** + * A strategy for handling cache-related errors. In most cases, any + * exception thrown by the provider should simply be thrown back at + * the client but, in some circumstances, the infrastructure may need + * to handle cache-provider exceptions in a different way. + * + *

    Typically, failing to retrieve an object from the cache with + * a given id can be transparently managed as a cache miss by not + * throwing back such exception. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public interface CacheErrorHandler { + + /** + * Handle the given runtime exception thrown by the cache provider when + * retrieving an item with the specified {@code key}, possibly + * rethrowing it as a fatal exception. + * @param exception the exception thrown by the cache provider + * @param cache the cache + * @param key the key used to get the item + * @see Cache#get(Object) + */ + void handleCacheGetError(RuntimeException exception, Cache cache, Object key); + + /** + * Handle the given runtime exception thrown by the cache provider when + * updating an item with the specified {@code key} and {@code value}, + * possibly rethrowing it as a fatal exception. + * @param exception the exception thrown by the cache provider + * @param cache the cache + * @param key the key used to update the item + * @param value the value to associate with the key + * @see Cache#put(Object, Object) + */ + void handleCachePutError(RuntimeException exception, Cache cache, Object key, @Nullable Object value); + + /** + * Handle the given runtime exception thrown by the cache provider when + * clearing an item with the specified {@code key}, possibly rethrowing + * it as a fatal exception. + * @param exception the exception thrown by the cache provider + * @param cache the cache + * @param key the key used to clear the item + */ + void handleCacheEvictError(RuntimeException exception, Cache cache, Object key); + + /** + * Handle the given runtime exception thrown by the cache provider when + * clearing the specified {@link Cache}, possibly rethrowing it as a + * fatal exception. + * @param exception the exception thrown by the cache provider + * @param cache the cache to clear + */ + void handleCacheClearError(RuntimeException exception, Cache cache); + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvaluationContext.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvaluationContext.java new file mode 100644 index 0000000..bb6d6ee --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvaluationContext.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.context.expression.MethodBasedEvaluationContext; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.lang.Nullable; + +/** + * Cache specific evaluation context that adds a method parameters as SpEL + * variables, in a lazy manner. The lazy nature eliminates unneeded + * parsing of classes byte code for parameter discovery. + * + *

    Also define a set of "unavailable variables" (i.e. variables that should + * lead to an exception right the way when they are accessed). This can be useful + * to verify a condition does not match even when not all potential variables + * are present. + * + *

    To limit the creation of objects, an ugly constructor is used + * (rather then a dedicated 'closure'-like class for deferred execution). + * + * @author Costin Leau + * @author Stephane Nicoll + * @author Juergen Hoeller + * @since 3.1 + */ +class CacheEvaluationContext extends MethodBasedEvaluationContext { + + private final Set unavailableVariables = new HashSet<>(1); + + + CacheEvaluationContext(Object rootObject, Method method, Object[] arguments, + ParameterNameDiscoverer parameterNameDiscoverer) { + + super(rootObject, method, arguments, parameterNameDiscoverer); + } + + + /** + * Add the specified variable name as unavailable for that context. + * Any expression trying to access this variable should lead to an exception. + *

    This permits the validation of expressions that could potentially a + * variable even when such variable isn't available yet. Any expression + * trying to use that variable should therefore fail to evaluate. + */ + public void addUnavailableVariable(String name) { + this.unavailableVariables.add(name); + } + + + /** + * Load the param information only when needed. + */ + @Override + @Nullable + public Object lookupVariable(String name) { + if (this.unavailableVariables.contains(name)) { + throw new VariableNotAvailableException(name); + } + return super.lookupVariable(name); + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvictOperation.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvictOperation.java new file mode 100644 index 0000000..7e72078 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvictOperation.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +/** + * Class describing a cache 'evict' operation. + * + * @author Costin Leau + * @author Marcin Kamionowski + * @since 3.1 + */ +public class CacheEvictOperation extends CacheOperation { + + private final boolean cacheWide; + + private final boolean beforeInvocation; + + + /** + * Create a new {@link CacheEvictOperation} instance from the given builder. + * @since 4.3 + */ + public CacheEvictOperation(CacheEvictOperation.Builder b) { + super(b); + this.cacheWide = b.cacheWide; + this.beforeInvocation = b.beforeInvocation; + } + + + public boolean isCacheWide() { + return this.cacheWide; + } + + public boolean isBeforeInvocation() { + return this.beforeInvocation; + } + + + /** + * A builder that can be used to create a {@link CacheEvictOperation}. + * @since 4.3 + */ + public static class Builder extends CacheOperation.Builder { + + private boolean cacheWide = false; + + private boolean beforeInvocation = false; + + public void setCacheWide(boolean cacheWide) { + this.cacheWide = cacheWide; + } + + public void setBeforeInvocation(boolean beforeInvocation) { + this.beforeInvocation = beforeInvocation; + } + + @Override + protected StringBuilder getOperationDescription() { + StringBuilder sb = super.getOperationDescription(); + sb.append(","); + sb.append(this.cacheWide); + sb.append(","); + sb.append(this.beforeInvocation); + return sb; + } + + @Override + public CacheEvictOperation build() { + return new CacheEvictOperation(this); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheExpressionRootObject.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheExpressionRootObject.java new file mode 100644 index 0000000..32f55cf --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheExpressionRootObject.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import java.lang.reflect.Method; +import java.util.Collection; + +import org.springframework.cache.Cache; + +/** + * Class describing the root object used during the expression evaluation. + * + * @author Costin Leau + * @author Sam Brannen + * @since 3.1 + */ +class CacheExpressionRootObject { + + private final Collection caches; + + private final Method method; + + private final Object[] args; + + private final Object target; + + private final Class targetClass; + + + public CacheExpressionRootObject( + Collection caches, Method method, Object[] args, Object target, Class targetClass) { + + this.method = method; + this.target = target; + this.targetClass = targetClass; + this.args = args; + this.caches = caches; + } + + + public Collection getCaches() { + return this.caches; + } + + public Method getMethod() { + return this.method; + } + + public String getMethodName() { + return this.method.getName(); + } + + public Object[] getArgs() { + return this.args; + } + + public Object getTarget() { + return this.target; + } + + public Class getTargetClass() { + return this.targetClass; + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheInterceptor.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheInterceptor.java new file mode 100644 index 0000000..2488fa4 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheInterceptor.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import java.io.Serializable; +import java.lang.reflect.Method; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * AOP Alliance MethodInterceptor for declarative cache + * management using the common Spring caching infrastructure + * ({@link org.springframework.cache.Cache}). + * + *

    Derives from the {@link CacheAspectSupport} class which + * contains the integration with Spring's underlying caching API. + * CacheInterceptor simply calls the relevant superclass methods + * in the correct order. + * + *

    CacheInterceptors are thread-safe. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 3.1 + */ +@SuppressWarnings("serial") +public class CacheInterceptor extends CacheAspectSupport implements MethodInterceptor, Serializable { + + @Override + @Nullable + public Object invoke(final MethodInvocation invocation) throws Throwable { + Method method = invocation.getMethod(); + + CacheOperationInvoker aopAllianceInvoker = () -> { + try { + return invocation.proceed(); + } + catch (Throwable ex) { + throw new CacheOperationInvoker.ThrowableWrapper(ex); + } + }; + + Object target = invocation.getThis(); + Assert.state(target != null, "Target must not be null"); + try { + return execute(aopAllianceInvoker, target, method, invocation.getArguments()); + } + catch (CacheOperationInvoker.ThrowableWrapper th) { + throw th.getOriginal(); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperation.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperation.java new file mode 100644 index 0000000..69f7305 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperation.java @@ -0,0 +1,232 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Base class for cache operations. + * + * @author Costin Leau + * @author Stephane Nicoll + * @author Marcin Kamionowski + * @since 3.1 + */ +public abstract class CacheOperation implements BasicOperation { + + private final String name; + + private final Set cacheNames; + + private final String key; + + private final String keyGenerator; + + private final String cacheManager; + + private final String cacheResolver; + + private final String condition; + + private final String toString; + + + /** + * Create a new {@link CacheOperation} instance from the given builder. + * @since 4.3 + */ + protected CacheOperation(Builder b) { + this.name = b.name; + this.cacheNames = b.cacheNames; + this.key = b.key; + this.keyGenerator = b.keyGenerator; + this.cacheManager = b.cacheManager; + this.cacheResolver = b.cacheResolver; + this.condition = b.condition; + this.toString = b.getOperationDescription().toString(); + } + + + public String getName() { + return this.name; + } + + @Override + public Set getCacheNames() { + return this.cacheNames; + } + + public String getKey() { + return this.key; + } + + public String getKeyGenerator() { + return this.keyGenerator; + } + + public String getCacheManager() { + return this.cacheManager; + } + + public String getCacheResolver() { + return this.cacheResolver; + } + + public String getCondition() { + return this.condition; + } + + + /** + * This implementation compares the {@code toString()} results. + * @see #toString() + */ + @Override + public boolean equals(@Nullable Object other) { + return (other instanceof CacheOperation && toString().equals(other.toString())); + } + + /** + * This implementation returns {@code toString()}'s hash code. + * @see #toString() + */ + @Override + public int hashCode() { + return toString().hashCode(); + } + + /** + * Return an identifying description for this cache operation. + *

    Returned value is produced by calling {@link Builder#getOperationDescription()} + * during object construction. This method is used in {@link #hashCode} and + * {@link #equals}. + * @see Builder#getOperationDescription() + */ + @Override + public final String toString() { + return this.toString; + } + + + /** + * Base class for builders that can be used to create a {@link CacheOperation}. + * @since 4.3 + */ + public abstract static class Builder { + + private String name = ""; + + private Set cacheNames = Collections.emptySet(); + + private String key = ""; + + private String keyGenerator = ""; + + private String cacheManager = ""; + + private String cacheResolver = ""; + + private String condition = ""; + + public void setName(String name) { + Assert.hasText(name, "Name must not be empty"); + this.name = name; + } + + public void setCacheName(String cacheName) { + Assert.hasText(cacheName, "Cache name must not be empty"); + this.cacheNames = Collections.singleton(cacheName); + } + + public void setCacheNames(String... cacheNames) { + this.cacheNames = new LinkedHashSet<>(cacheNames.length); + for (String cacheName : cacheNames) { + Assert.hasText(cacheName, "Cache name must be non-empty if specified"); + this.cacheNames.add(cacheName); + } + } + + public Set getCacheNames() { + return this.cacheNames; + } + + public void setKey(String key) { + Assert.notNull(key, "Key must not be null"); + this.key = key; + } + + public String getKey() { + return this.key; + } + + public String getKeyGenerator() { + return this.keyGenerator; + } + + public String getCacheManager() { + return this.cacheManager; + } + + public String getCacheResolver() { + return this.cacheResolver; + } + + public void setKeyGenerator(String keyGenerator) { + Assert.notNull(keyGenerator, "KeyGenerator name must not be null"); + this.keyGenerator = keyGenerator; + } + + public void setCacheManager(String cacheManager) { + Assert.notNull(cacheManager, "CacheManager name must not be null"); + this.cacheManager = cacheManager; + } + + public void setCacheResolver(String cacheResolver) { + Assert.notNull(cacheResolver, "CacheResolver name must not be null"); + this.cacheResolver = cacheResolver; + } + + public void setCondition(String condition) { + Assert.notNull(condition, "Condition must not be null"); + this.condition = condition; + } + + /** + * Return an identifying description for this caching operation. + *

    Available to subclasses, for inclusion in their {@code toString()} result. + */ + protected StringBuilder getOperationDescription() { + StringBuilder result = new StringBuilder(getClass().getSimpleName()); + result.append("[").append(this.name); + result.append("] caches=").append(this.cacheNames); + result.append(" | key='").append(this.key); + result.append("' | keyGenerator='").append(this.keyGenerator); + result.append("' | cacheManager='").append(this.cacheManager); + result.append("' | cacheResolver='").append(this.cacheResolver); + result.append("' | condition='").append(this.condition).append("'"); + return result; + } + + public abstract CacheOperation build(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationExpressionEvaluator.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationExpressionEvaluator.java new file mode 100644 index 0000000..82892d0 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationExpressionEvaluator.java @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.cache.Cache; +import org.springframework.context.expression.AnnotatedElementKey; +import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.context.expression.CachedExpressionEvaluator; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.lang.Nullable; + +/** + * Utility class handling the SpEL expression parsing. + * Meant to be used as a reusable, thread-safe component. + * + *

    Performs internal caching for performance reasons + * using {@link AnnotatedElementKey}. + * + * @author Costin Leau + * @author Phillip Webb + * @author Sam Brannen + * @author Stephane Nicoll + * @since 3.1 + */ +class CacheOperationExpressionEvaluator extends CachedExpressionEvaluator { + + /** + * Indicate that there is no result variable. + */ + public static final Object NO_RESULT = new Object(); + + /** + * Indicate that the result variable cannot be used at all. + */ + public static final Object RESULT_UNAVAILABLE = new Object(); + + /** + * The name of the variable holding the result object. + */ + public static final String RESULT_VARIABLE = "result"; + + + private final Map keyCache = new ConcurrentHashMap<>(64); + + private final Map conditionCache = new ConcurrentHashMap<>(64); + + private final Map unlessCache = new ConcurrentHashMap<>(64); + + + /** + * Create an {@link EvaluationContext}. + * @param caches the current caches + * @param method the method + * @param args the method arguments + * @param target the target object + * @param targetClass the target class + * @param result the return value (can be {@code null}) or + * {@link #NO_RESULT} if there is no return at this time + * @return the evaluation context + */ + public EvaluationContext createEvaluationContext(Collection caches, + Method method, Object[] args, Object target, Class targetClass, Method targetMethod, + @Nullable Object result, @Nullable BeanFactory beanFactory) { + + CacheExpressionRootObject rootObject = new CacheExpressionRootObject( + caches, method, args, target, targetClass); + CacheEvaluationContext evaluationContext = new CacheEvaluationContext( + rootObject, targetMethod, args, getParameterNameDiscoverer()); + if (result == RESULT_UNAVAILABLE) { + evaluationContext.addUnavailableVariable(RESULT_VARIABLE); + } + else if (result != NO_RESULT) { + evaluationContext.setVariable(RESULT_VARIABLE, result); + } + if (beanFactory != null) { + evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory)); + } + return evaluationContext; + } + + @Nullable + public Object key(String keyExpression, AnnotatedElementKey methodKey, EvaluationContext evalContext) { + return getExpression(this.keyCache, methodKey, keyExpression).getValue(evalContext); + } + + public boolean condition(String conditionExpression, AnnotatedElementKey methodKey, EvaluationContext evalContext) { + return (Boolean.TRUE.equals(getExpression(this.conditionCache, methodKey, conditionExpression).getValue( + evalContext, Boolean.class))); + } + + public boolean unless(String unlessExpression, AnnotatedElementKey methodKey, EvaluationContext evalContext) { + return (Boolean.TRUE.equals(getExpression(this.unlessCache, methodKey, unlessExpression).getValue( + evalContext, Boolean.class))); + } + + /** + * Clear all caches. + */ + void clear() { + this.keyCache.clear(); + this.conditionCache.clear(); + this.unlessCache.clear(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvocationContext.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvocationContext.java new file mode 100644 index 0000000..c459a09 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvocationContext.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import java.lang.reflect.Method; + +/** + * Representation of the context of the invocation of a cache operation. + * + *

    The cache operation is static and independent of a particular invocation; + * this interface gathers the operation and a particular invocation. + * + * @author Stephane Nicoll + * @since 4.1 + * @param the operation type + */ +public interface CacheOperationInvocationContext { + + /** + * Return the cache operation. + */ + O getOperation(); + + /** + * Return the target instance on which the method was invoked. + */ + Object getTarget(); + + /** + * Return the method which was invoked. + */ + Method getMethod(); + + /** + * Return the argument list used to invoke the method. + */ + Object[] getArgs(); + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java new file mode 100644 index 0000000..cfaab08 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import org.springframework.lang.Nullable; + +/** + * Abstract the invocation of a cache operation. + * + *

    Does not provide a way to transmit checked exceptions but + * provide a special exception that should be used to wrap any + * exception that was thrown by the underlying invocation. + * Callers are expected to handle this issue type specifically. + * + * @author Stephane Nicoll + * @since 4.1 + */ +@FunctionalInterface +public interface CacheOperationInvoker { + + /** + * Invoke the cache operation defined by this instance. Wraps any exception + * that is thrown during the invocation in a {@link ThrowableWrapper}. + * @return the result of the operation + * @throws ThrowableWrapper if an error occurred while invoking the operation + */ + @Nullable + Object invoke() throws ThrowableWrapper; + + + /** + * Wrap any exception thrown while invoking {@link #invoke()}. + */ + @SuppressWarnings("serial") + class ThrowableWrapper extends RuntimeException { + + private final Throwable original; + + public ThrowableWrapper(Throwable original) { + super(original.getMessage(), original); + this.original = original; + } + + public Throwable getOriginal() { + return this.original; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSource.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSource.java new file mode 100644 index 0000000..02a9b4f --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSource.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import java.lang.reflect.Method; +import java.util.Collection; + +import org.springframework.lang.Nullable; + +/** + * Interface used by {@link CacheInterceptor}. Implementations know how to source + * cache operation attributes, whether from configuration, metadata attributes at + * source level, or elsewhere. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 3.1 + */ +public interface CacheOperationSource { + + /** + * Determine whether the given class is a candidate for cache operations + * in the metadata format of this {@code CacheOperationSource}. + *

    If this method returns {@code false}, the methods on the given class + * will not get traversed for {@link #getCacheOperations} introspection. + * Returning {@code false} is therefore an optimization for non-affected + * classes, whereas {@code true} simply means that the class needs to get + * fully introspected for each method on the given class individually. + * @param targetClass the class to introspect + * @return {@code false} if the class is known to have no cache operation + * metadata at class or method level; {@code true} otherwise. The default + * implementation returns {@code true}, leading to regular introspection. + * @since 5.2 + */ + default boolean isCandidateClass(Class targetClass) { + return true; + } + + /** + * Return the collection of cache operations for this method, + * or {@code null} if the method contains no cacheable annotations. + * @param method the method to introspect + * @param targetClass the target class (may be {@code null}, in which case + * the declaring class of the method must be used) + * @return all cache operations for this method, or {@code null} if none found + */ + @Nullable + Collection getCacheOperations(Method method, @Nullable Class targetClass); + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSourcePointcut.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSourcePointcut.java new file mode 100644 index 0000000..5cf286f --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSourcePointcut.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import java.io.Serializable; +import java.lang.reflect.Method; + +import org.springframework.aop.ClassFilter; +import org.springframework.aop.support.StaticMethodMatcherPointcut; +import org.springframework.cache.CacheManager; +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; + +/** + * A Pointcut that matches if the underlying {@link CacheOperationSource} + * has an attribute for a given method. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 3.1 + */ +@SuppressWarnings("serial") +abstract class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable { + + protected CacheOperationSourcePointcut() { + setClassFilter(new CacheOperationSourceClassFilter()); + } + + + @Override + public boolean matches(Method method, Class targetClass) { + CacheOperationSource cas = getCacheOperationSource(); + return (cas != null && !CollectionUtils.isEmpty(cas.getCacheOperations(method, targetClass))); + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof CacheOperationSourcePointcut)) { + return false; + } + CacheOperationSourcePointcut otherPc = (CacheOperationSourcePointcut) other; + return ObjectUtils.nullSafeEquals(getCacheOperationSource(), otherPc.getCacheOperationSource()); + } + + @Override + public int hashCode() { + return CacheOperationSourcePointcut.class.hashCode(); + } + + @Override + public String toString() { + return getClass().getName() + ": " + getCacheOperationSource(); + } + + + /** + * Obtain the underlying {@link CacheOperationSource} (may be {@code null}). + * To be implemented by subclasses. + */ + @Nullable + protected abstract CacheOperationSource getCacheOperationSource(); + + + /** + * {@link ClassFilter} that delegates to {@link CacheOperationSource#isCandidateClass} + * for filtering classes whose methods are not worth searching to begin with. + */ + private class CacheOperationSourceClassFilter implements ClassFilter { + + @Override + public boolean matches(Class clazz) { + if (CacheManager.class.isAssignableFrom(clazz)) { + return false; + } + CacheOperationSource cas = getCacheOperationSource(); + return (cas == null || cas.isCandidateClass(clazz)); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheProxyFactoryBean.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheProxyFactoryBean.java new file mode 100644 index 0000000..9cf0f1b --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheProxyFactoryBean.java @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import org.springframework.aop.Pointcut; +import org.springframework.aop.framework.AbstractSingletonProxyFactoryBean; +import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.cache.CacheManager; + +/** + * Proxy factory bean for simplified declarative caching handling. + * This is a convenient alternative to a standard AOP + * {@link org.springframework.aop.framework.ProxyFactoryBean} + * with a separate {@link CacheInterceptor} definition. + * + *

    This class is designed to facilitate declarative cache demarcation: namely, wrapping + * a singleton target object with a caching proxy, proxying all the interfaces that the + * target implements. Exists primarily for third-party framework integration. + * Users should favor the {@code cache:} XML namespace + * {@link org.springframework.cache.annotation.Cacheable @Cacheable} annotation. + * See the + * declarative annotation-based caching + * section of the Spring reference documentation for more information. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 3.1 + * @see org.springframework.aop.framework.ProxyFactoryBean + * @see CacheInterceptor + */ +@SuppressWarnings("serial") +public class CacheProxyFactoryBean extends AbstractSingletonProxyFactoryBean + implements BeanFactoryAware, SmartInitializingSingleton { + + private final CacheInterceptor cacheInterceptor = new CacheInterceptor(); + + private Pointcut pointcut = Pointcut.TRUE; + + + /** + * Set one or more sources to find cache operations. + * @see CacheInterceptor#setCacheOperationSources + */ + public void setCacheOperationSources(CacheOperationSource... cacheOperationSources) { + this.cacheInterceptor.setCacheOperationSources(cacheOperationSources); + } + + /** + * Set the default {@link KeyGenerator} that this cache aspect should delegate to + * if no specific key generator has been set for the operation. + *

    The default is a {@link SimpleKeyGenerator}. + * @since 5.0.3 + * @see CacheInterceptor#setKeyGenerator + */ + public void setKeyGenerator(KeyGenerator keyGenerator) { + this.cacheInterceptor.setKeyGenerator(keyGenerator); + } + + /** + * Set the default {@link CacheResolver} that this cache aspect should delegate + * to if no specific cache resolver has been set for the operation. + *

    The default resolver resolves the caches against their names and the + * default cache manager. + * @since 5.0.3 + * @see CacheInterceptor#setCacheResolver + */ + public void setCacheResolver(CacheResolver cacheResolver) { + this.cacheInterceptor.setCacheResolver(cacheResolver); + } + + /** + * Set the {@link CacheManager} to use to create a default {@link CacheResolver}. + * Replace the current {@link CacheResolver}, if any. + * @since 5.0.3 + * @see CacheInterceptor#setCacheManager + */ + public void setCacheManager(CacheManager cacheManager) { + this.cacheInterceptor.setCacheManager(cacheManager); + } + + /** + * Set a pointcut, i.e. a bean that triggers conditional invocation of the + * {@link CacheInterceptor} depending on the method and attributes passed. + *

    Note: Additional interceptors are always invoked. + * @see #setPreInterceptors + * @see #setPostInterceptors + */ + public void setPointcut(Pointcut pointcut) { + this.pointcut = pointcut; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.cacheInterceptor.setBeanFactory(beanFactory); + } + + @Override + public void afterSingletonsInstantiated() { + this.cacheInterceptor.afterSingletonsInstantiated(); + } + + + @Override + protected Object createMainInterceptor() { + this.cacheInterceptor.afterPropertiesSet(); + return new DefaultPointcutAdvisor(this.pointcut, this.cacheInterceptor); + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CachePutOperation.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CachePutOperation.java new file mode 100644 index 0000000..d5b39ee --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CachePutOperation.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import org.springframework.lang.Nullable; + +/** + * Class describing a cache 'put' operation. + * + * @author Costin Leau + * @author Phillip Webb + * @author Marcin Kamionowski + * @since 3.1 + */ +public class CachePutOperation extends CacheOperation { + + @Nullable + private final String unless; + + + /** + * Create a new {@link CachePutOperation} instance from the given builder. + * @since 4.3 + */ + public CachePutOperation(CachePutOperation.Builder b) { + super(b); + this.unless = b.unless; + } + + + @Nullable + public String getUnless() { + return this.unless; + } + + + /** + * A builder that can be used to create a {@link CachePutOperation}. + * @since 4.3 + */ + public static class Builder extends CacheOperation.Builder { + + @Nullable + private String unless; + + public void setUnless(String unless) { + this.unless = unless; + } + + @Override + protected StringBuilder getOperationDescription() { + StringBuilder sb = super.getOperationDescription(); + sb.append(" | unless='"); + sb.append(this.unless); + sb.append("'"); + return sb; + } + + @Override + public CachePutOperation build() { + return new CachePutOperation(this); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheResolver.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheResolver.java new file mode 100644 index 0000000..4153f90 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheResolver.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import java.util.Collection; + +import org.springframework.cache.Cache; + +/** + * Determine the {@link Cache} instance(s) to use for an intercepted method invocation. + * + *

    Implementations must be thread-safe. + * + * @author Stephane Nicoll + * @since 4.1 + */ +@FunctionalInterface +public interface CacheResolver { + + /** + * Return the cache(s) to use for the specified invocation. + * @param context the context of the particular invocation + * @return the cache(s) to use (never {@code null}) + * @throws IllegalStateException if cache resolution failed + */ + Collection resolveCaches(CacheOperationInvocationContext context); + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheableOperation.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheableOperation.java new file mode 100644 index 0000000..922407d --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheableOperation.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import org.springframework.lang.Nullable; + +/** + * Class describing a cache 'cacheable' operation. + * + * @author Costin Leau + * @author Phillip Webb + * @author Marcin Kamionowski + * @since 3.1 + */ +public class CacheableOperation extends CacheOperation { + + @Nullable + private final String unless; + + private final boolean sync; + + + /** + * Create a new {@link CacheableOperation} instance from the given builder. + * @since 4.3 + */ + public CacheableOperation(CacheableOperation.Builder b) { + super(b); + this.unless = b.unless; + this.sync = b.sync; + } + + + @Nullable + public String getUnless() { + return this.unless; + } + + public boolean isSync() { + return this.sync; + } + + + /** + * A builder that can be used to create a {@link CacheableOperation}. + * @since 4.3 + */ + public static class Builder extends CacheOperation.Builder { + + @Nullable + private String unless; + + private boolean sync; + + public void setUnless(String unless) { + this.unless = unless; + } + + public void setSync(boolean sync) { + this.sync = sync; + } + + @Override + protected StringBuilder getOperationDescription() { + StringBuilder sb = super.getOperationDescription(); + sb.append(" | unless='"); + sb.append(this.unless); + sb.append("'"); + sb.append(" | sync='"); + sb.append(this.sync); + sb.append("'"); + return sb; + } + + @Override + public CacheableOperation build() { + return new CacheableOperation(this); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CompositeCacheOperationSource.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CompositeCacheOperationSource.java new file mode 100644 index 0000000..da95337 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CompositeCacheOperationSource.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Composite {@link CacheOperationSource} implementation that iterates + * over a given array of {@code CacheOperationSource} instances. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 3.1 + */ +@SuppressWarnings("serial") +public class CompositeCacheOperationSource implements CacheOperationSource, Serializable { + + private final CacheOperationSource[] cacheOperationSources; + + + /** + * Create a new CompositeCacheOperationSource for the given sources. + * @param cacheOperationSources the CacheOperationSource instances to combine + */ + public CompositeCacheOperationSource(CacheOperationSource... cacheOperationSources) { + Assert.notEmpty(cacheOperationSources, "CacheOperationSource array must not be empty"); + this.cacheOperationSources = cacheOperationSources; + } + + /** + * Return the {@code CacheOperationSource} instances that this + * {@code CompositeCacheOperationSource} combines. + */ + public final CacheOperationSource[] getCacheOperationSources() { + return this.cacheOperationSources; + } + + + @Override + public boolean isCandidateClass(Class targetClass) { + for (CacheOperationSource source : this.cacheOperationSources) { + if (source.isCandidateClass(targetClass)) { + return true; + } + } + return false; + } + + @Override + @Nullable + public Collection getCacheOperations(Method method, @Nullable Class targetClass) { + Collection ops = null; + for (CacheOperationSource source : this.cacheOperationSources) { + Collection cacheOperations = source.getCacheOperations(method, targetClass); + if (cacheOperations != null) { + if (ops == null) { + ops = new ArrayList<>(); + } + ops.addAll(cacheOperations); + } + } + return ops; + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/KeyGenerator.java b/spring-context/src/main/java/org/springframework/cache/interceptor/KeyGenerator.java new file mode 100644 index 0000000..2d99e49 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/KeyGenerator.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import java.lang.reflect.Method; + +/** + * Cache key generator. Used for creating a key based on the given method + * (used as context) and its parameters. + * + * @author Costin Leau + * @author Chris Beams + * @author Phillip Webb + * @since 3.1 + */ +@FunctionalInterface +public interface KeyGenerator { + + /** + * Generate a key for the given method and its parameters. + * @param target the target instance + * @param method the method being called + * @param params the method parameters (with any var-args expanded) + * @return a generated key + */ + Object generate(Object target, Method method, Object... params); + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/NameMatchCacheOperationSource.java b/spring-context/src/main/java/org/springframework/cache/interceptor/NameMatchCacheOperationSource.java new file mode 100644 index 0000000..ac23ae7 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/NameMatchCacheOperationSource.java @@ -0,0 +1,133 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; +import org.springframework.util.PatternMatchUtils; + +/** + * Simple {@link CacheOperationSource} implementation that allows attributes to be matched + * by registered name. + * + * @author Costin Leau + * @since 3.1 + */ +@SuppressWarnings("serial") +public class NameMatchCacheOperationSource implements CacheOperationSource, Serializable { + + /** + * Logger available to subclasses. + *

    Static for optimal serialization. + */ + protected static final Log logger = LogFactory.getLog(NameMatchCacheOperationSource.class); + + + /** Keys are method names; values are TransactionAttributes. */ + private Map> nameMap = new LinkedHashMap<>(); + + + /** + * Set a name/attribute map, consisting of method names + * (e.g. "myMethod") and CacheOperation instances + * (or Strings to be converted to CacheOperation instances). + * @see CacheOperation + */ + public void setNameMap(Map> nameMap) { + nameMap.forEach(this::addCacheMethod); + } + + /** + * Add an attribute for a cacheable method. + *

    Method names can be exact matches, or of the pattern "xxx*", + * "*xxx" or "*xxx*" for matching multiple methods. + * @param methodName the name of the method + * @param ops operation associated with the method + */ + public void addCacheMethod(String methodName, Collection ops) { + if (logger.isDebugEnabled()) { + logger.debug("Adding method [" + methodName + "] with cache operations [" + ops + "]"); + } + this.nameMap.put(methodName, ops); + } + + @Override + @Nullable + public Collection getCacheOperations(Method method, @Nullable Class targetClass) { + // look for direct name match + String methodName = method.getName(); + Collection ops = this.nameMap.get(methodName); + + if (ops == null) { + // Look for most specific name match. + String bestNameMatch = null; + for (String mappedName : this.nameMap.keySet()) { + if (isMatch(methodName, mappedName) + && (bestNameMatch == null || bestNameMatch.length() <= mappedName.length())) { + ops = this.nameMap.get(mappedName); + bestNameMatch = mappedName; + } + } + } + + return ops; + } + + /** + * Return if the given method name matches the mapped name. + *

    The default implementation checks for "xxx*", "*xxx" and "*xxx*" matches, + * as well as direct equality. Can be overridden in subclasses. + * @param methodName the method name of the class + * @param mappedName the name in the descriptor + * @return if the names match + * @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String) + */ + protected boolean isMatch(String methodName, String mappedName) { + return PatternMatchUtils.simpleMatch(mappedName, methodName); + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof NameMatchCacheOperationSource)) { + return false; + } + NameMatchCacheOperationSource otherTas = (NameMatchCacheOperationSource) other; + return ObjectUtils.nullSafeEquals(this.nameMap, otherTas.nameMap); + } + + @Override + public int hashCode() { + return NameMatchCacheOperationSource.class.hashCode(); + } + + @Override + public String toString() { + return getClass().getName() + ": " + this.nameMap; + } +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/NamedCacheResolver.java b/spring-context/src/main/java/org/springframework/cache/interceptor/NamedCacheResolver.java new file mode 100644 index 0000000..767c712 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/NamedCacheResolver.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; + +import org.springframework.cache.CacheManager; +import org.springframework.lang.Nullable; + +/** + * A {@link CacheResolver} that forces the resolution to a configurable + * collection of name(s) against a given {@link CacheManager}. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public class NamedCacheResolver extends AbstractCacheResolver { + + @Nullable + private Collection cacheNames; + + + public NamedCacheResolver() { + } + + public NamedCacheResolver(CacheManager cacheManager, String... cacheNames) { + super(cacheManager); + this.cacheNames = new ArrayList<>(Arrays.asList(cacheNames)); + } + + + /** + * Set the cache name(s) that this resolver should use. + */ + public void setCacheNames(Collection cacheNames) { + this.cacheNames = cacheNames; + } + + @Override + protected Collection getCacheNames(CacheOperationInvocationContext context) { + return this.cacheNames; + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleCacheErrorHandler.java b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleCacheErrorHandler.java new file mode 100644 index 0000000..99ba252 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleCacheErrorHandler.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import org.springframework.cache.Cache; +import org.springframework.lang.Nullable; + +/** + * A simple {@link CacheErrorHandler} that does not handle the + * exception at all, simply throwing it back at the client. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public class SimpleCacheErrorHandler implements CacheErrorHandler { + + @Override + public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) { + throw exception; + } + + @Override + public void handleCachePutError(RuntimeException exception, Cache cache, Object key, @Nullable Object value) { + throw exception; + } + + @Override + public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) { + throw exception; + } + + @Override + public void handleCacheClearError(RuntimeException exception, Cache cache) { + throw exception; + } +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleCacheResolver.java b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleCacheResolver.java new file mode 100644 index 0000000..3c22f3e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleCacheResolver.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import java.util.Collection; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.lang.Nullable; + +/** + * A simple {@link CacheResolver} that resolves the {@link Cache} instance(s) + * based on a configurable {@link CacheManager} and the name of the + * cache(s) as provided by {@link BasicOperation#getCacheNames() getCacheNames()}. + * + * @author Stephane Nicoll + * @author Juergen Hoeller + * @since 4.1 + * @see BasicOperation#getCacheNames() + */ +public class SimpleCacheResolver extends AbstractCacheResolver { + + /** + * Construct a new {@code SimpleCacheResolver}. + * @see #setCacheManager + */ + public SimpleCacheResolver() { + } + + /** + * Construct a new {@code SimpleCacheResolver} for the given {@link CacheManager}. + * @param cacheManager the CacheManager to use + */ + public SimpleCacheResolver(CacheManager cacheManager) { + super(cacheManager); + } + + + @Override + protected Collection getCacheNames(CacheOperationInvocationContext context) { + return context.getOperation().getCacheNames(); + } + + + /** + * Return a {@code SimpleCacheResolver} for the given {@link CacheManager}. + * @param cacheManager the CacheManager (potentially {@code null}) + * @return the SimpleCacheResolver ({@code null} if the CacheManager was {@code null}) + * @since 5.1 + */ + @Nullable + static SimpleCacheResolver of(@Nullable CacheManager cacheManager) { + return (cacheManager != null ? new SimpleCacheResolver(cacheManager) : null); + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKey.java b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKey.java new file mode 100644 index 0000000..e6a2556 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKey.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.util.Arrays; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * A simple key as returned from the {@link SimpleKeyGenerator}. + * + * @author Phillip Webb + * @author Juergen Hoeller + * @since 4.0 + * @see SimpleKeyGenerator + */ +@SuppressWarnings("serial") +public class SimpleKey implements Serializable { + + /** + * An empty key. + */ + public static final SimpleKey EMPTY = new SimpleKey(); + + + private final Object[] params; + + // Effectively final, just re-calculated on deserialization + private transient int hashCode; + + + /** + * Create a new {@link SimpleKey} instance. + * @param elements the elements of the key + */ + public SimpleKey(Object... elements) { + Assert.notNull(elements, "Elements must not be null"); + this.params = elements.clone(); + // Pre-calculate hashCode field + this.hashCode = Arrays.deepHashCode(this.params); + } + + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || + (other instanceof SimpleKey && Arrays.deepEquals(this.params, ((SimpleKey) other).params))); + } + + @Override + public final int hashCode() { + // Expose pre-calculated hashCode field + return this.hashCode; + } + + @Override + public String toString() { + return getClass().getSimpleName() + " [" + StringUtils.arrayToCommaDelimitedString(this.params) + "]"; + } + + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + ois.defaultReadObject(); + // Re-calculate hashCode field on deserialization + this.hashCode = Arrays.deepHashCode(this.params); + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKeyGenerator.java b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKeyGenerator.java new file mode 100644 index 0000000..f443ea9 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKeyGenerator.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import java.lang.reflect.Method; + +/** + * Simple key generator. Returns the parameter itself if a single non-null + * value is given, otherwise returns a {@link SimpleKey} of the parameters. + * + *

    No collisions will occur with the keys generated by this class. + * The returned {@link SimpleKey} object can be safely used with a + * {@link org.springframework.cache.concurrent.ConcurrentMapCache}, however, + * might not be suitable for all {@link org.springframework.cache.Cache} + * implementations. + * + * @author Phillip Webb + * @author Juergen Hoeller + * @since 4.0 + * @see SimpleKey + * @see org.springframework.cache.annotation.CachingConfigurer + */ +public class SimpleKeyGenerator implements KeyGenerator { + + @Override + public Object generate(Object target, Method method, Object... params) { + return generateKey(params); + } + + /** + * Generate a key based on the specified parameters. + */ + public static Object generateKey(Object... params) { + if (params.length == 0) { + return SimpleKey.EMPTY; + } + if (params.length == 1) { + Object param = params[0]; + if (param != null && !param.getClass().isArray()) { + return param; + } + } + return new SimpleKey(params); + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/VariableNotAvailableException.java b/spring-context/src/main/java/org/springframework/cache/interceptor/VariableNotAvailableException.java new file mode 100644 index 0000000..b4ce3c2 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/VariableNotAvailableException.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import org.springframework.expression.EvaluationException; + +/** + * A specific {@link EvaluationException} to mention that a given variable + * used in the expression is not available in the context. + * + * @author Stephane Nicoll + * @since 4.0.6 + */ +@SuppressWarnings("serial") +class VariableNotAvailableException extends EvaluationException { + + private final String name; + + + public VariableNotAvailableException(String name) { + super("Variable not available"); + this.name = name; + } + + + public final String getName() { + return this.name; + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/package-info.java b/spring-context/src/main/java/org/springframework/cache/interceptor/package-info.java new file mode 100644 index 0000000..97810d2 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/package-info.java @@ -0,0 +1,11 @@ +/** + * AOP-based solution for declarative caching demarcation. + * Builds on the AOP infrastructure in org.springframework.aop.framework. + * Any POJO can be cache-advised with Spring. + */ +@NonNullApi +@NonNullFields +package org.springframework.cache.interceptor; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/cache/package-info.java b/spring-context/src/main/java/org/springframework/cache/package-info.java new file mode 100644 index 0000000..dbb69ea --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/package-info.java @@ -0,0 +1,10 @@ +/** + * Spring's generic cache abstraction. + * Concrete implementations are provided in the subpackages. + */ +@NonNullApi +@NonNullFields +package org.springframework.cache; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/cache/support/AbstractCacheManager.java b/spring-context/src/main/java/org/springframework/cache/support/AbstractCacheManager.java new file mode 100644 index 0000000..50516d0 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/support/AbstractCacheManager.java @@ -0,0 +1,194 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.support; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.lang.Nullable; + +/** + * Abstract base class implementing the common {@link CacheManager} methods. + * Useful for 'static' environments where the backing caches do not change. + * + * @author Costin Leau + * @author Juergen Hoeller + * @author Stephane Nicoll + * @since 3.1 + */ +public abstract class AbstractCacheManager implements CacheManager, InitializingBean { + + private final ConcurrentMap cacheMap = new ConcurrentHashMap<>(16); + + private volatile Set cacheNames = Collections.emptySet(); + + + // Early cache initialization on startup + + @Override + public void afterPropertiesSet() { + initializeCaches(); + } + + /** + * Initialize the static configuration of caches. + *

    Triggered on startup through {@link #afterPropertiesSet()}; + * can also be called to re-initialize at runtime. + * @since 4.2.2 + * @see #loadCaches() + */ + public void initializeCaches() { + Collection caches = loadCaches(); + + synchronized (this.cacheMap) { + this.cacheNames = Collections.emptySet(); + this.cacheMap.clear(); + Set cacheNames = new LinkedHashSet<>(caches.size()); + for (Cache cache : caches) { + String name = cache.getName(); + this.cacheMap.put(name, decorateCache(cache)); + cacheNames.add(name); + } + this.cacheNames = Collections.unmodifiableSet(cacheNames); + } + } + + /** + * Load the initial caches for this cache manager. + *

    Called by {@link #afterPropertiesSet()} on startup. + * The returned collection may be empty but must not be {@code null}. + */ + protected abstract Collection loadCaches(); + + + // Lazy cache initialization on access + + @Override + @Nullable + public Cache getCache(String name) { + // Quick check for existing cache... + Cache cache = this.cacheMap.get(name); + if (cache != null) { + return cache; + } + + // The provider may support on-demand cache creation... + Cache missingCache = getMissingCache(name); + if (missingCache != null) { + // Fully synchronize now for missing cache registration + synchronized (this.cacheMap) { + cache = this.cacheMap.get(name); + if (cache == null) { + cache = decorateCache(missingCache); + this.cacheMap.put(name, cache); + updateCacheNames(name); + } + } + } + return cache; + } + + @Override + public Collection getCacheNames() { + return this.cacheNames; + } + + + // Common cache initialization delegates for subclasses + + /** + * Check for a registered cache of the given name. + * In contrast to {@link #getCache(String)}, this method does not trigger + * the lazy creation of missing caches via {@link #getMissingCache(String)}. + * @param name the cache identifier (must not be {@code null}) + * @return the associated Cache instance, or {@code null} if none found + * @since 4.1 + * @see #getCache(String) + * @see #getMissingCache(String) + */ + @Nullable + protected final Cache lookupCache(String name) { + return this.cacheMap.get(name); + } + + /** + * Dynamically register an additional Cache with this manager. + * @param cache the Cache to register + * @deprecated as of Spring 4.3, in favor of {@link #getMissingCache(String)} + */ + @Deprecated + protected final void addCache(Cache cache) { + String name = cache.getName(); + synchronized (this.cacheMap) { + if (this.cacheMap.put(name, decorateCache(cache)) == null) { + updateCacheNames(name); + } + } + } + + /** + * Update the exposed {@link #cacheNames} set with the given name. + *

    This will always be called within a full {@link #cacheMap} lock + * and effectively behaves like a {@code CopyOnWriteArraySet} with + * preserved order but exposed as an unmodifiable reference. + * @param name the name of the cache to be added + */ + private void updateCacheNames(String name) { + Set cacheNames = new LinkedHashSet<>(this.cacheNames); + cacheNames.add(name); + this.cacheNames = Collections.unmodifiableSet(cacheNames); + } + + + // Overridable template methods for cache initialization + + /** + * Decorate the given Cache object if necessary. + * @param cache the Cache object to be added to this CacheManager + * @return the decorated Cache object to be used instead, + * or simply the passed-in Cache object by default + */ + protected Cache decorateCache(Cache cache) { + return cache; + } + + /** + * Return a missing cache with the specified {@code name}, or {@code null} if + * such a cache does not exist or could not be created on demand. + *

    Caches may be lazily created at runtime if the native provider supports it. + * If a lookup by name does not yield any result, an {@code AbstractCacheManager} + * subclass gets a chance to register such a cache at runtime. The returned cache + * will be automatically added to this cache manager. + * @param name the name of the cache to retrieve + * @return the missing cache, or {@code null} if no such cache exists or could be + * created on demand + * @since 4.1 + * @see #getCache(String) + */ + @Nullable + protected Cache getMissingCache(String name) { + return null; + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/support/AbstractValueAdaptingCache.java b/spring-context/src/main/java/org/springframework/cache/support/AbstractValueAdaptingCache.java new file mode 100644 index 0000000..28beb6d --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/support/AbstractValueAdaptingCache.java @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.support; + +import org.springframework.cache.Cache; +import org.springframework.lang.Nullable; + +/** + * Common base class for {@link Cache} implementations that need to adapt + * {@code null} values (and potentially other such special values) before + * passing them on to the underlying store. + * + *

    Transparently replaces given {@code null} user values with an internal + * {@link NullValue#INSTANCE}, if configured to support {@code null} values + * (as indicated by {@link #isAllowNullValues()}. + * + * @author Juergen Hoeller + * @since 4.2.2 + */ +public abstract class AbstractValueAdaptingCache implements Cache { + + private final boolean allowNullValues; + + + /** + * Create an {@code AbstractValueAdaptingCache} with the given setting. + * @param allowNullValues whether to allow for {@code null} values + */ + protected AbstractValueAdaptingCache(boolean allowNullValues) { + this.allowNullValues = allowNullValues; + } + + + /** + * Return whether {@code null} values are allowed in this cache. + */ + public final boolean isAllowNullValues() { + return this.allowNullValues; + } + + @Override + @Nullable + public ValueWrapper get(Object key) { + return toValueWrapper(lookup(key)); + } + + @Override + @SuppressWarnings("unchecked") + @Nullable + public T get(Object key, @Nullable Class type) { + Object value = fromStoreValue(lookup(key)); + if (value != null && type != null && !type.isInstance(value)) { + throw new IllegalStateException( + "Cached value is not of required type [" + type.getName() + "]: " + value); + } + return (T) value; + } + + /** + * Perform an actual lookup in the underlying store. + * @param key the key whose associated value is to be returned + * @return the raw store value for the key, or {@code null} if none + */ + @Nullable + protected abstract Object lookup(Object key); + + + /** + * Convert the given value from the internal store to a user value + * returned from the get method (adapting {@code null}). + * @param storeValue the store value + * @return the value to return to the user + */ + @Nullable + protected Object fromStoreValue(@Nullable Object storeValue) { + if (this.allowNullValues && storeValue == NullValue.INSTANCE) { + return null; + } + return storeValue; + } + + /** + * Convert the given user value, as passed into the put method, + * to a value in the internal store (adapting {@code null}). + * @param userValue the given user value + * @return the value to store + */ + protected Object toStoreValue(@Nullable Object userValue) { + if (userValue == null) { + if (this.allowNullValues) { + return NullValue.INSTANCE; + } + throw new IllegalArgumentException( + "Cache '" + getName() + "' is configured to not allow null values but null was provided"); + } + return userValue; + } + + /** + * Wrap the given store value with a {@link SimpleValueWrapper}, also going + * through {@link #fromStoreValue} conversion. Useful for {@link #get(Object)} + * and {@link #putIfAbsent(Object, Object)} implementations. + * @param storeValue the original value + * @return the wrapped value + */ + @Nullable + protected Cache.ValueWrapper toValueWrapper(@Nullable Object storeValue) { + return (storeValue != null ? new SimpleValueWrapper(fromStoreValue(storeValue)) : null); + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/support/CompositeCacheManager.java b/spring-context/src/main/java/org/springframework/cache/support/CompositeCacheManager.java new file mode 100644 index 0000000..25f0bf1 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/support/CompositeCacheManager.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.support; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.lang.Nullable; + +/** + * Composite {@link CacheManager} implementation that iterates over + * a given collection of delegate {@link CacheManager} instances. + * + *

    Allows {@link NoOpCacheManager} to be automatically added to the end of + * the list for handling cache declarations without a backing store. Otherwise, + * any custom {@link CacheManager} may play that role of the last delegate as + * well, lazily creating cache regions for any requested name. + * + *

    Note: Regular CacheManagers that this composite manager delegates to need + * to return {@code null} from {@link #getCache(String)} if they are unaware of + * the specified cache name, allowing for iteration to the next delegate in line. + * However, most {@link CacheManager} implementations fall back to lazy creation + * of named caches once requested; check out the specific configuration details + * for a 'static' mode with fixed cache names, if available. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 3.1 + * @see #setFallbackToNoOpCache + * @see org.springframework.cache.concurrent.ConcurrentMapCacheManager#setCacheNames + */ +public class CompositeCacheManager implements CacheManager, InitializingBean { + + private final List cacheManagers = new ArrayList<>(); + + private boolean fallbackToNoOpCache = false; + + + /** + * Construct an empty CompositeCacheManager, with delegate CacheManagers to + * be added via the {@link #setCacheManagers "cacheManagers"} property. + */ + public CompositeCacheManager() { + } + + /** + * Construct a CompositeCacheManager from the given delegate CacheManagers. + * @param cacheManagers the CacheManagers to delegate to + */ + public CompositeCacheManager(CacheManager... cacheManagers) { + setCacheManagers(Arrays.asList(cacheManagers)); + } + + + /** + * Specify the CacheManagers to delegate to. + */ + public void setCacheManagers(Collection cacheManagers) { + this.cacheManagers.addAll(cacheManagers); + } + + /** + * Indicate whether a {@link NoOpCacheManager} should be added at the end of the delegate list. + * In this case, any {@code getCache} requests not handled by the configured CacheManagers will + * be automatically handled by the {@link NoOpCacheManager} (and hence never return {@code null}). + */ + public void setFallbackToNoOpCache(boolean fallbackToNoOpCache) { + this.fallbackToNoOpCache = fallbackToNoOpCache; + } + + @Override + public void afterPropertiesSet() { + if (this.fallbackToNoOpCache) { + this.cacheManagers.add(new NoOpCacheManager()); + } + } + + + @Override + @Nullable + public Cache getCache(String name) { + for (CacheManager cacheManager : this.cacheManagers) { + Cache cache = cacheManager.getCache(name); + if (cache != null) { + return cache; + } + } + return null; + } + + @Override + public Collection getCacheNames() { + Set names = new LinkedHashSet<>(); + for (CacheManager manager : this.cacheManagers) { + names.addAll(manager.getCacheNames()); + } + return Collections.unmodifiableSet(names); + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/support/NoOpCache.java b/spring-context/src/main/java/org/springframework/cache/support/NoOpCache.java new file mode 100644 index 0000000..6c814ff --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/support/NoOpCache.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.support; + +import java.util.concurrent.Callable; + +import org.springframework.cache.Cache; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * A no operation {@link Cache} implementation suitable for disabling caching. + * + *

    Will simply accept any items into the cache not actually storing them. + * + * @author Costin Leau + * @author Stephane Nicoll + * @since 4.3.4 + * @see NoOpCacheManager + */ +public class NoOpCache implements Cache { + + private final String name; + + + /** + * Create a {@link NoOpCache} instance with the specified name. + * @param name the name of the cache + */ + public NoOpCache(String name) { + Assert.notNull(name, "Cache name must not be null"); + this.name = name; + } + + + @Override + public String getName() { + return this.name; + } + + @Override + public Object getNativeCache() { + return this; + } + + @Override + @Nullable + public ValueWrapper get(Object key) { + return null; + } + + @Override + @Nullable + public T get(Object key, @Nullable Class type) { + return null; + } + + @Override + @Nullable + public T get(Object key, Callable valueLoader) { + try { + return valueLoader.call(); + } + catch (Exception ex) { + throw new ValueRetrievalException(key, valueLoader, ex); + } + } + + @Override + public void put(Object key, @Nullable Object value) { + } + + @Override + @Nullable + public ValueWrapper putIfAbsent(Object key, @Nullable Object value) { + return null; + } + + @Override + public void evict(Object key) { + } + + @Override + public boolean evictIfPresent(Object key) { + return false; + } + + @Override + public void clear() { + } + + @Override + public boolean invalidate() { + return false; + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/support/NoOpCacheManager.java b/spring-context/src/main/java/org/springframework/cache/support/NoOpCacheManager.java new file mode 100644 index 0000000..0b3137b --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/support/NoOpCacheManager.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.support; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.lang.Nullable; + +/** + * A basic, no operation {@link CacheManager} implementation suitable + * for disabling caching, typically used for backing cache declarations + * without an actual backing store. + * + *

    Will simply accept any items into the cache not actually storing them. + * + * @author Costin Leau + * @author Stephane Nicoll + * @since 3.1 + * @see NoOpCache + */ +public class NoOpCacheManager implements CacheManager { + + private final ConcurrentMap caches = new ConcurrentHashMap<>(16); + + private final Set cacheNames = new LinkedHashSet<>(16); + + + /** + * This implementation always returns a {@link Cache} implementation that will not store items. + * Additionally, the request cache will be remembered by the manager for consistency. + */ + @Override + @Nullable + public Cache getCache(String name) { + Cache cache = this.caches.get(name); + if (cache == null) { + this.caches.computeIfAbsent(name, key -> new NoOpCache(name)); + synchronized (this.cacheNames) { + this.cacheNames.add(name); + } + } + + return this.caches.get(name); + } + + /** + * This implementation returns the name of the caches previously requested. + */ + @Override + public Collection getCacheNames() { + synchronized (this.cacheNames) { + return Collections.unmodifiableSet(this.cacheNames); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/support/NullValue.java b/spring-context/src/main/java/org/springframework/cache/support/NullValue.java new file mode 100644 index 0000000..18c26d3 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/support/NullValue.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.support; + +import java.io.Serializable; + +import org.springframework.lang.Nullable; + +/** + * Simple serializable class that serves as a {@code null} replacement + * for cache stores which otherwise do not support {@code null} values. + * + * @author Juergen Hoeller + * @since 4.2.2 + * @see AbstractValueAdaptingCache + */ +public final class NullValue implements Serializable { + + /** + * The canonical representation of a {@code null} replacement, as used by the + * default implementation of {@link AbstractValueAdaptingCache#toStoreValue}/ + * {@link AbstractValueAdaptingCache#fromStoreValue}. + * @since 4.3.10 + */ + public static final Object INSTANCE = new NullValue(); + + private static final long serialVersionUID = 1L; + + + private NullValue() { + } + + private Object readResolve() { + return INSTANCE; + } + + + @Override + public boolean equals(@Nullable Object obj) { + return (this == obj || obj == null); + } + + @Override + public int hashCode() { + return NullValue.class.hashCode(); + } + + @Override + public String toString() { + return "null"; + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/support/SimpleCacheManager.java b/spring-context/src/main/java/org/springframework/cache/support/SimpleCacheManager.java new file mode 100644 index 0000000..c130f8f --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/support/SimpleCacheManager.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.support; + +import java.util.Collection; +import java.util.Collections; + +import org.springframework.cache.Cache; + +/** + * Simple cache manager working against a given collection of caches. + * Useful for testing or simple caching declarations. + *

    + * When using this implementation directly, i.e. not via a regular + * bean registration, {@link #initializeCaches()} should be invoked + * to initialize its internal state once the + * {@linkplain #setCaches(Collection) caches have been provided}. + * + * @author Costin Leau + * @since 3.1 + */ +public class SimpleCacheManager extends AbstractCacheManager { + + private Collection caches = Collections.emptySet(); + + + /** + * Specify the collection of Cache instances to use for this CacheManager. + * @see #initializeCaches() + */ + public void setCaches(Collection caches) { + this.caches = caches; + } + + @Override + protected Collection loadCaches() { + return this.caches; + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/support/SimpleValueWrapper.java b/spring-context/src/main/java/org/springframework/cache/support/SimpleValueWrapper.java new file mode 100644 index 0000000..700936a --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/support/SimpleValueWrapper.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.support; + +import org.springframework.cache.Cache.ValueWrapper; +import org.springframework.lang.Nullable; + +/** + * Straightforward implementation of {@link org.springframework.cache.Cache.ValueWrapper}, + * simply holding the value as given at construction and returning it from {@link #get()}. + * + * @author Costin Leau + * @since 3.1 + */ +public class SimpleValueWrapper implements ValueWrapper { + + @Nullable + private final Object value; + + + /** + * Create a new SimpleValueWrapper instance for exposing the given value. + * @param value the value to expose (may be {@code null}) + */ + public SimpleValueWrapper(@Nullable Object value) { + this.value = value; + } + + + /** + * Simply returns the value as given at construction time. + */ + @Override + @Nullable + public Object get() { + return this.value; + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/support/package-info.java b/spring-context/src/main/java/org/springframework/cache/support/package-info.java new file mode 100644 index 0000000..8e82da0 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/support/package-info.java @@ -0,0 +1,10 @@ +/** + * Support classes for the org.springframework.cache package. + * Provides abstract classes for cache managers and caches. + */ +@NonNullApi +@NonNullFields +package org.springframework.cache.support; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/context/ApplicationContext.java b/spring-context/src/main/java/org/springframework/context/ApplicationContext.java new file mode 100644 index 0000000..232bd62 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/ApplicationContext.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context; + +import org.springframework.beans.factory.HierarchicalBeanFactory; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.core.env.EnvironmentCapable; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.lang.Nullable; + +/** + * Central interface to provide configuration for an application. + * This is read-only while the application is running, but may be + * reloaded if the implementation supports this. + * + *

    An ApplicationContext provides: + *

      + *
    • Bean factory methods for accessing application components. + * Inherited from {@link org.springframework.beans.factory.ListableBeanFactory}. + *
    • The ability to load file resources in a generic fashion. + * Inherited from the {@link org.springframework.core.io.ResourceLoader} interface. + *
    • The ability to publish events to registered listeners. + * Inherited from the {@link ApplicationEventPublisher} interface. + *
    • The ability to resolve messages, supporting internationalization. + * Inherited from the {@link MessageSource} interface. + *
    • Inheritance from a parent context. Definitions in a descendant context + * will always take priority. This means, for example, that a single parent + * context can be used by an entire web application, while each servlet has + * its own child context that is independent of that of any other servlet. + *
    + * + *

    In addition to standard {@link org.springframework.beans.factory.BeanFactory} + * lifecycle capabilities, ApplicationContext implementations detect and invoke + * {@link ApplicationContextAware} beans as well as {@link ResourceLoaderAware}, + * {@link ApplicationEventPublisherAware} and {@link MessageSourceAware} beans. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see ConfigurableApplicationContext + * @see org.springframework.beans.factory.BeanFactory + * @see org.springframework.core.io.ResourceLoader + */ +public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory, + MessageSource, ApplicationEventPublisher, ResourcePatternResolver { + + /** + * Return the unique id of this application context. + * @return the unique id of the context, or {@code null} if none + */ + @Nullable + String getId(); + + /** + * Return a name for the deployed application that this context belongs to. + * @return a name for the deployed application, or the empty String by default + */ + String getApplicationName(); + + /** + * Return a friendly name for this context. + * @return a display name for this context (never {@code null}) + */ + String getDisplayName(); + + /** + * Return the timestamp when this context was first loaded. + * @return the timestamp (ms) when this context was first loaded + */ + long getStartupDate(); + + /** + * Return the parent context, or {@code null} if there is no parent + * and this is the root of the context hierarchy. + * @return the parent context, or {@code null} if there is no parent + */ + @Nullable + ApplicationContext getParent(); + + /** + * Expose AutowireCapableBeanFactory functionality for this context. + *

    This is not typically used by application code, except for the purpose of + * initializing bean instances that live outside of the application context, + * applying the Spring bean lifecycle (fully or partly) to them. + *

    Alternatively, the internal BeanFactory exposed by the + * {@link ConfigurableApplicationContext} interface offers access to the + * {@link AutowireCapableBeanFactory} interface too. The present method mainly + * serves as a convenient, specific facility on the ApplicationContext interface. + *

    NOTE: As of 4.2, this method will consistently throw IllegalStateException + * after the application context has been closed. In current Spring Framework + * versions, only refreshable application contexts behave that way; as of 4.2, + * all application context implementations will be required to comply. + * @return the AutowireCapableBeanFactory for this context + * @throws IllegalStateException if the context does not support the + * {@link AutowireCapableBeanFactory} interface, or does not hold an + * autowire-capable bean factory yet (e.g. if {@code refresh()} has + * never been called), or if the context has been closed already + * @see ConfigurableApplicationContext#refresh() + * @see ConfigurableApplicationContext#getBeanFactory() + */ + AutowireCapableBeanFactory getAutowireCapableBeanFactory() throws IllegalStateException; + +} diff --git a/spring-context/src/main/java/org/springframework/context/ApplicationContextAware.java b/spring-context/src/main/java/org/springframework/context/ApplicationContextAware.java new file mode 100644 index 0000000..e4d0fb1 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/ApplicationContextAware.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.Aware; + +/** + * Interface to be implemented by any object that wishes to be notified + * of the {@link ApplicationContext} that it runs in. + * + *

    Implementing this interface makes sense for example when an object + * requires access to a set of collaborating beans. Note that configuration + * via bean references is preferable to implementing this interface just + * for bean lookup purposes. + * + *

    This interface can also be implemented if an object needs access to file + * resources, i.e. wants to call {@code getResource}, wants to publish + * an application event, or requires access to the MessageSource. However, + * it is preferable to implement the more specific {@link ResourceLoaderAware}, + * {@link ApplicationEventPublisherAware} or {@link MessageSourceAware} interface + * in such a specific scenario. + * + *

    Note that file resource dependencies can also be exposed as bean properties + * of type {@link org.springframework.core.io.Resource}, populated via Strings + * with automatic type conversion by the bean factory. This removes the need + * for implementing any callback interface just for the purpose of accessing + * a specific file resource. + * + *

    {@link org.springframework.context.support.ApplicationObjectSupport} is a + * convenience base class for application objects, implementing this interface. + * + *

    For a list of all bean lifecycle methods, see the + * {@link org.springframework.beans.factory.BeanFactory BeanFactory javadocs}. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Chris Beams + * @see ResourceLoaderAware + * @see ApplicationEventPublisherAware + * @see MessageSourceAware + * @see org.springframework.context.support.ApplicationObjectSupport + * @see org.springframework.beans.factory.BeanFactoryAware + */ +public interface ApplicationContextAware extends Aware { + + /** + * Set the ApplicationContext that this object runs in. + * Normally this call will be used to initialize the object. + *

    Invoked after population of normal bean properties but before an init callback such + * as {@link org.springframework.beans.factory.InitializingBean#afterPropertiesSet()} + * or a custom init-method. Invoked after {@link ResourceLoaderAware#setResourceLoader}, + * {@link ApplicationEventPublisherAware#setApplicationEventPublisher} and + * {@link MessageSourceAware}, if applicable. + * @param applicationContext the ApplicationContext object to be used by this object + * @throws ApplicationContextException in case of context initialization errors + * @throws BeansException if thrown by application context methods + * @see org.springframework.beans.factory.BeanInitializationException + */ + void setApplicationContext(ApplicationContext applicationContext) throws BeansException; + +} diff --git a/spring-context/src/main/java/org/springframework/context/ApplicationContextException.java b/spring-context/src/main/java/org/springframework/context/ApplicationContextException.java new file mode 100644 index 0000000..e8fe8db --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/ApplicationContextException.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context; + +import org.springframework.beans.FatalBeanException; + +/** + * Exception thrown during application context initialization. + * + * @author Rod Johnson + */ +@SuppressWarnings("serial") +public class ApplicationContextException extends FatalBeanException { + + /** + * Create a new {@code ApplicationContextException} + * with the specified detail message and no root cause. + * @param msg the detail message + */ + public ApplicationContextException(String msg) { + super(msg); + } + + /** + * Create a new {@code ApplicationContextException} + * with the specified detail message and the given root cause. + * @param msg the detail message + * @param cause the root cause + */ + public ApplicationContextException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/ApplicationContextInitializer.java b/spring-context/src/main/java/org/springframework/context/ApplicationContextInitializer.java new file mode 100644 index 0000000..42e750f --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/ApplicationContextInitializer.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context; + +/** + * Callback interface for initializing a Spring {@link ConfigurableApplicationContext} + * prior to being {@linkplain ConfigurableApplicationContext#refresh() refreshed}. + * + *

    Typically used within web applications that require some programmatic initialization + * of the application context. For example, registering property sources or activating + * profiles against the {@linkplain ConfigurableApplicationContext#getEnvironment() + * context's environment}. See {@code ContextLoader} and {@code FrameworkServlet} support + * for declaring a "contextInitializerClasses" context-param and init-param, respectively. + * + *

    {@code ApplicationContextInitializer} processors are encouraged to detect + * whether Spring's {@link org.springframework.core.Ordered Ordered} interface has been + * implemented or if the {@link org.springframework.core.annotation.Order @Order} + * annotation is present and to sort instances accordingly if so prior to invocation. + * + * @author Chris Beams + * @since 3.1 + * @param the application context type + * @see org.springframework.web.context.ContextLoader#customizeContext + * @see org.springframework.web.context.ContextLoader#CONTEXT_INITIALIZER_CLASSES_PARAM + * @see org.springframework.web.servlet.FrameworkServlet#setContextInitializerClasses + * @see org.springframework.web.servlet.FrameworkServlet#applyInitializers + */ +@FunctionalInterface +public interface ApplicationContextInitializer { + + /** + * Initialize the given application context. + * @param applicationContext the application to configure + */ + void initialize(C applicationContext); + +} diff --git a/spring-context/src/main/java/org/springframework/context/ApplicationEvent.java b/spring-context/src/main/java/org/springframework/context/ApplicationEvent.java new file mode 100644 index 0000000..5906c59 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/ApplicationEvent.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context; + +import java.util.EventObject; + +/** + * Class to be extended by all application events. Abstract as it + * doesn't make sense for generic events to be published directly. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see org.springframework.context.ApplicationListener + * @see org.springframework.context.event.EventListener + */ +public abstract class ApplicationEvent extends EventObject { + + /** use serialVersionUID from Spring 1.2 for interoperability. */ + private static final long serialVersionUID = 7099057708183571937L; + + /** System time when the event happened. */ + private final long timestamp; + + + /** + * Create a new {@code ApplicationEvent}. + * @param source the object on which the event initially occurred or with + * which the event is associated (never {@code null}) + */ + public ApplicationEvent(Object source) { + super(source); + this.timestamp = System.currentTimeMillis(); + } + + + /** + * Return the system time in milliseconds when the event occurred. + */ + public final long getTimestamp() { + return this.timestamp; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/ApplicationEventPublisher.java b/spring-context/src/main/java/org/springframework/context/ApplicationEventPublisher.java new file mode 100644 index 0000000..779b73c --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/ApplicationEventPublisher.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context; + +/** + * Interface that encapsulates event publication functionality. + * + *

    Serves as a super-interface for {@link ApplicationContext}. + * + * @author Juergen Hoeller + * @author Stephane Nicoll + * @since 1.1.1 + * @see ApplicationContext + * @see ApplicationEventPublisherAware + * @see org.springframework.context.ApplicationEvent + * @see org.springframework.context.event.ApplicationEventMulticaster + * @see org.springframework.context.event.EventPublicationInterceptor + */ +@FunctionalInterface +public interface ApplicationEventPublisher { + + /** + * Notify all matching listeners registered with this + * application of an application event. Events may be framework events + * (such as ContextRefreshedEvent) or application-specific events. + *

    Such an event publication step is effectively a hand-off to the + * multicaster and does not imply synchronous/asynchronous execution + * or even immediate execution at all. Event listeners are encouraged + * to be as efficient as possible, individually using asynchronous + * execution for longer-running and potentially blocking operations. + * @param event the event to publish + * @see #publishEvent(Object) + * @see org.springframework.context.event.ContextRefreshedEvent + * @see org.springframework.context.event.ContextClosedEvent + */ + default void publishEvent(ApplicationEvent event) { + publishEvent((Object) event); + } + + /** + * Notify all matching listeners registered with this + * application of an event. + *

    If the specified {@code event} is not an {@link ApplicationEvent}, + * it is wrapped in a {@link PayloadApplicationEvent}. + *

    Such an event publication step is effectively a hand-off to the + * multicaster and does not imply synchronous/asynchronous execution + * or even immediate execution at all. Event listeners are encouraged + * to be as efficient as possible, individually using asynchronous + * execution for longer-running and potentially blocking operations. + * @param event the event to publish + * @since 4.2 + * @see #publishEvent(ApplicationEvent) + * @see PayloadApplicationEvent + */ + void publishEvent(Object event); + +} diff --git a/spring-context/src/main/java/org/springframework/context/ApplicationEventPublisherAware.java b/spring-context/src/main/java/org/springframework/context/ApplicationEventPublisherAware.java new file mode 100644 index 0000000..3a13745 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/ApplicationEventPublisherAware.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2011 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context; + +import org.springframework.beans.factory.Aware; + +/** + * Interface to be implemented by any object that wishes to be notified + * of the ApplicationEventPublisher (typically the ApplicationContext) + * that it runs in. + * + * @author Juergen Hoeller + * @author Chris Beams + * @since 1.1.1 + * @see ApplicationContextAware + */ +public interface ApplicationEventPublisherAware extends Aware { + + /** + * Set the ApplicationEventPublisher that this object runs in. + *

    Invoked after population of normal bean properties but before an init + * callback like InitializingBean's afterPropertiesSet or a custom init-method. + * Invoked before ApplicationContextAware's setApplicationContext. + * @param applicationEventPublisher event publisher to be used by this object + */ + void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher); + +} diff --git a/spring-context/src/main/java/org/springframework/context/ApplicationListener.java b/spring-context/src/main/java/org/springframework/context/ApplicationListener.java new file mode 100644 index 0000000..3194f52 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/ApplicationListener.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context; + +import java.util.EventListener; +import java.util.function.Consumer; + +/** + * Interface to be implemented by application event listeners. + * + *

    Based on the standard {@code java.util.EventListener} interface + * for the Observer design pattern. + * + *

    As of Spring 3.0, an {@code ApplicationListener} can generically declare + * the event type that it is interested in. When registered with a Spring + * {@code ApplicationContext}, events will be filtered accordingly, with the + * listener getting invoked for matching event objects only. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @param the specific {@code ApplicationEvent} subclass to listen to + * @see org.springframework.context.ApplicationEvent + * @see org.springframework.context.event.ApplicationEventMulticaster + * @see org.springframework.context.event.EventListener + */ +@FunctionalInterface +public interface ApplicationListener extends EventListener { + + /** + * Handle an application event. + * @param event the event to respond to + */ + void onApplicationEvent(E event); + + + /** + * Create a new {@code ApplicationListener} for the given payload consumer. + * @param consumer the event payload consumer + * @param the type of the event payload + * @return a corresponding {@code ApplicationListener} instance + * @since 5.3 + * @see PayloadApplicationEvent + */ + static ApplicationListener> forPayload(Consumer consumer) { + return event -> consumer.accept(event.getPayload()); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/ApplicationStartupAware.java b/spring-context/src/main/java/org/springframework/context/ApplicationStartupAware.java new file mode 100644 index 0000000..b18cf6c --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/ApplicationStartupAware.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context; + +import org.springframework.beans.factory.Aware; +import org.springframework.core.metrics.ApplicationStartup; + +/** + * Interface to be implemented by any object that wishes to be notified + * of the {@link ApplicationStartup} that it runs with. + * + * @author Brian Clozel + * @since 5.3 + * @see ApplicationContextAware + */ +public interface ApplicationStartupAware extends Aware { + + /** + * Set the ApplicationStartup that this object runs with. + *

    Invoked after population of normal bean properties but before an init + * callback like InitializingBean's afterPropertiesSet or a custom init-method. + * Invoked before ApplicationContextAware's setApplicationContext. + * @param applicationStartup application startup to be used by this object + */ + void setApplicationStartup(ApplicationStartup applicationStartup); + +} diff --git a/spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java b/spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java new file mode 100644 index 0000000..29e0daf --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java @@ -0,0 +1,257 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context; + +import java.io.Closeable; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ProtocolResolver; +import org.springframework.core.metrics.ApplicationStartup; +import org.springframework.lang.Nullable; + +/** + * SPI interface to be implemented by most if not all application contexts. + * Provides facilities to configure an application context in addition + * to the application context client methods in the + * {@link org.springframework.context.ApplicationContext} interface. + * + *

    Configuration and lifecycle methods are encapsulated here to avoid + * making them obvious to ApplicationContext client code. The present + * methods should only be used by startup and shutdown code. + * + * @author Juergen Hoeller + * @author Chris Beams + * @author Sam Brannen + * @since 03.11.2003 + */ +public interface ConfigurableApplicationContext extends ApplicationContext, Lifecycle, Closeable { + + /** + * Any number of these characters are considered delimiters between + * multiple context config paths in a single String value. + * @see org.springframework.context.support.AbstractXmlApplicationContext#setConfigLocation + * @see org.springframework.web.context.ContextLoader#CONFIG_LOCATION_PARAM + * @see org.springframework.web.servlet.FrameworkServlet#setContextConfigLocation + */ + String CONFIG_LOCATION_DELIMITERS = ",; \t\n"; + + /** + * Name of the ConversionService bean in the factory. + * If none is supplied, default conversion rules apply. + * @since 3.0 + * @see org.springframework.core.convert.ConversionService + */ + String CONVERSION_SERVICE_BEAN_NAME = "conversionService"; + + /** + * Name of the LoadTimeWeaver bean in the factory. If such a bean is supplied, + * the context will use a temporary ClassLoader for type matching, in order + * to allow the LoadTimeWeaver to process all actual bean classes. + * @since 2.5 + * @see org.springframework.instrument.classloading.LoadTimeWeaver + */ + String LOAD_TIME_WEAVER_BEAN_NAME = "loadTimeWeaver"; + + /** + * Name of the {@link Environment} bean in the factory. + * @since 3.1 + */ + String ENVIRONMENT_BEAN_NAME = "environment"; + + /** + * Name of the System properties bean in the factory. + * @see java.lang.System#getProperties() + */ + String SYSTEM_PROPERTIES_BEAN_NAME = "systemProperties"; + + /** + * Name of the System environment bean in the factory. + * @see java.lang.System#getenv() + */ + String SYSTEM_ENVIRONMENT_BEAN_NAME = "systemEnvironment"; + + /** + * Name of the {@link ApplicationStartup} bean in the factory. + * @since 5.3 + */ + String APPLICATION_STARTUP_BEAN_NAME = "applicationStartup"; + + /** + * {@link Thread#getName() Name} of the {@linkplain #registerShutdownHook() + * shutdown hook} thread: {@value}. + * @since 5.2 + * @see #registerShutdownHook() + */ + String SHUTDOWN_HOOK_THREAD_NAME = "SpringContextShutdownHook"; + + + /** + * Set the unique id of this application context. + * @since 3.0 + */ + void setId(String id); + + /** + * Set the parent of this application context. + *

    Note that the parent shouldn't be changed: It should only be set outside + * a constructor if it isn't available when an object of this class is created, + * for example in case of WebApplicationContext setup. + * @param parent the parent context + * @see org.springframework.web.context.ConfigurableWebApplicationContext + */ + void setParent(@Nullable ApplicationContext parent); + + /** + * Set the {@code Environment} for this application context. + * @param environment the new environment + * @since 3.1 + */ + void setEnvironment(ConfigurableEnvironment environment); + + /** + * Return the {@code Environment} for this application context in configurable + * form, allowing for further customization. + * @since 3.1 + */ + @Override + ConfigurableEnvironment getEnvironment(); + + /** + * Set the {@link ApplicationStartup} for this application context. + *

    This allows the application context to record metrics + * during startup. + * @param applicationStartup the new context event factory + * @since 5.3 + */ + void setApplicationStartup(ApplicationStartup applicationStartup); + + /** + * Return the {@link ApplicationStartup} for this application context. + * @since 5.3 + */ + ApplicationStartup getApplicationStartup(); + + /** + * Add a new BeanFactoryPostProcessor that will get applied to the internal + * bean factory of this application context on refresh, before any of the + * bean definitions get evaluated. To be invoked during context configuration. + * @param postProcessor the factory processor to register + */ + void addBeanFactoryPostProcessor(BeanFactoryPostProcessor postProcessor); + + /** + * Add a new ApplicationListener that will be notified on context events + * such as context refresh and context shutdown. + *

    Note that any ApplicationListener registered here will be applied + * on refresh if the context is not active yet, or on the fly with the + * current event multicaster in case of a context that is already active. + * @param listener the ApplicationListener to register + * @see org.springframework.context.event.ContextRefreshedEvent + * @see org.springframework.context.event.ContextClosedEvent + */ + void addApplicationListener(ApplicationListener listener); + + /** + * Specify the ClassLoader to load class path resources and bean classes with. + *

    This context class loader will be passed to the internal bean factory. + * @since 5.2.7 + * @see org.springframework.core.io.DefaultResourceLoader#DefaultResourceLoader(ClassLoader) + * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#setBeanClassLoader + */ + void setClassLoader(ClassLoader classLoader); + + /** + * Register the given protocol resolver with this application context, + * allowing for additional resource protocols to be handled. + *

    Any such resolver will be invoked ahead of this context's standard + * resolution rules. It may therefore also override any default rules. + * @since 4.3 + */ + void addProtocolResolver(ProtocolResolver resolver); + + /** + * Load or refresh the persistent representation of the configuration, which + * might be from Java-based configuration, an XML file, a properties file, a + * relational database schema, or some other format. + *

    As this is a startup method, it should destroy already created singletons + * if it fails, to avoid dangling resources. In other words, after invocation + * of this method, either all or no singletons at all should be instantiated. + * @throws BeansException if the bean factory could not be initialized + * @throws IllegalStateException if already initialized and multiple refresh + * attempts are not supported + */ + void refresh() throws BeansException, IllegalStateException; + + /** + * Register a shutdown hook with the JVM runtime, closing this context + * on JVM shutdown unless it has already been closed at that time. + *

    This method can be called multiple times. Only one shutdown hook + * (at max) will be registered for each context instance. + *

    As of Spring Framework 5.2, the {@linkplain Thread#getName() name} of + * the shutdown hook thread should be {@link #SHUTDOWN_HOOK_THREAD_NAME}. + * @see java.lang.Runtime#addShutdownHook + * @see #close() + */ + void registerShutdownHook(); + + /** + * Close this application context, releasing all resources and locks that the + * implementation might hold. This includes destroying all cached singleton beans. + *

    Note: Does not invoke {@code close} on a parent context; + * parent contexts have their own, independent lifecycle. + *

    This method can be called multiple times without side effects: Subsequent + * {@code close} calls on an already closed context will be ignored. + */ + @Override + void close(); + + /** + * Determine whether this application context is active, that is, + * whether it has been refreshed at least once and has not been closed yet. + * @return whether the context is still active + * @see #refresh() + * @see #close() + * @see #getBeanFactory() + */ + boolean isActive(); + + /** + * Return the internal bean factory of this application context. + * Can be used to access specific functionality of the underlying factory. + *

    Note: Do not use this to post-process the bean factory; singletons + * will already have been instantiated before. Use a BeanFactoryPostProcessor + * to intercept the BeanFactory setup process before beans get touched. + *

    Generally, this internal factory will only be accessible while the context + * is active, that is, in-between {@link #refresh()} and {@link #close()}. + * The {@link #isActive()} flag can be used to check whether the context + * is in an appropriate state. + * @return the underlying bean factory + * @throws IllegalStateException if the context does not hold an internal + * bean factory (usually if {@link #refresh()} hasn't been called yet or + * if {@link #close()} has already been called) + * @see #isActive() + * @see #refresh() + * @see #close() + * @see #addBeanFactoryPostProcessor + */ + ConfigurableListableBeanFactory getBeanFactory() throws IllegalStateException; + +} diff --git a/spring-context/src/main/java/org/springframework/context/EmbeddedValueResolverAware.java b/spring-context/src/main/java/org/springframework/context/EmbeddedValueResolverAware.java new file mode 100644 index 0000000..4f102be --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/EmbeddedValueResolverAware.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context; + +import org.springframework.beans.factory.Aware; +import org.springframework.util.StringValueResolver; + +/** + * Interface to be implemented by any object that wishes to be notified of a + * {@code StringValueResolver} for the resolution of embedded definition values. + * + *

    This is an alternative to a full ConfigurableBeanFactory dependency via the + * {@code ApplicationContextAware}/{@code BeanFactoryAware} interfaces. + * + * @author Juergen Hoeller + * @author Chris Beams + * @since 3.0.3 + * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#resolveEmbeddedValue(String) + * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#getBeanExpressionResolver() + * @see org.springframework.beans.factory.config.EmbeddedValueResolver + */ +public interface EmbeddedValueResolverAware extends Aware { + + /** + * Set the StringValueResolver to use for resolving embedded definition values. + */ + void setEmbeddedValueResolver(StringValueResolver resolver); + +} diff --git a/spring-context/src/main/java/org/springframework/context/EnvironmentAware.java b/spring-context/src/main/java/org/springframework/context/EnvironmentAware.java new file mode 100644 index 0000000..5cd680a --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/EnvironmentAware.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context; + +import org.springframework.beans.factory.Aware; +import org.springframework.core.env.Environment; + +/** + * Interface to be implemented by any bean that wishes to be notified + * of the {@link Environment} that it runs in. + * + * @author Chris Beams + * @since 3.1 + * @see org.springframework.core.env.EnvironmentCapable + */ +public interface EnvironmentAware extends Aware { + + /** + * Set the {@code Environment} that this component runs in. + */ + void setEnvironment(Environment environment); + +} diff --git a/spring-context/src/main/java/org/springframework/context/HierarchicalMessageSource.java b/spring-context/src/main/java/org/springframework/context/HierarchicalMessageSource.java new file mode 100644 index 0000000..2949a99 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/HierarchicalMessageSource.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context; + +import org.springframework.lang.Nullable; + +/** + * Sub-interface of MessageSource to be implemented by objects that + * can resolve messages hierarchically. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +public interface HierarchicalMessageSource extends MessageSource { + + /** + * Set the parent that will be used to try to resolve messages + * that this object can't resolve. + * @param parent the parent MessageSource that will be used to + * resolve messages that this object can't resolve. + * May be {@code null}, in which case no further resolution is possible. + */ + void setParentMessageSource(@Nullable MessageSource parent); + + /** + * Return the parent of this MessageSource, or {@code null} if none. + */ + @Nullable + MessageSource getParentMessageSource(); + +} diff --git a/spring-context/src/main/java/org/springframework/context/Lifecycle.java b/spring-context/src/main/java/org/springframework/context/Lifecycle.java new file mode 100644 index 0000000..99ba521 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/Lifecycle.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context; + +/** + * A common interface defining methods for start/stop lifecycle control. + * The typical use case for this is to control asynchronous processing. + * NOTE: This interface does not imply specific auto-startup semantics. + * Consider implementing {@link SmartLifecycle} for that purpose. + * + *

    Can be implemented by both components (typically a Spring bean defined in a + * Spring context) and containers (typically a Spring {@link ApplicationContext} + * itself). Containers will propagate start/stop signals to all components that + * apply within each container, e.g. for a stop/restart scenario at runtime. + * + *

    Can be used for direct invocations or for management operations via JMX. + * In the latter case, the {@link org.springframework.jmx.export.MBeanExporter} + * will typically be defined with an + * {@link org.springframework.jmx.export.assembler.InterfaceBasedMBeanInfoAssembler}, + * restricting the visibility of activity-controlled components to the Lifecycle + * interface. + * + *

    Note that the present {@code Lifecycle} interface is only supported on + * top-level singleton beans. On any other component, the {@code Lifecycle} + * interface will remain undetected and hence ignored. Also, note that the extended + * {@link SmartLifecycle} interface provides sophisticated integration with the + * application context's startup and shutdown phases. + * + * @author Juergen Hoeller + * @since 2.0 + * @see SmartLifecycle + * @see ConfigurableApplicationContext + * @see org.springframework.jms.listener.AbstractMessageListenerContainer + * @see org.springframework.scheduling.quartz.SchedulerFactoryBean + */ +public interface Lifecycle { + + /** + * Start this component. + *

    Should not throw an exception if the component is already running. + *

    In the case of a container, this will propagate the start signal to all + * components that apply. + * @see SmartLifecycle#isAutoStartup() + */ + void start(); + + /** + * Stop this component, typically in a synchronous fashion, such that the component is + * fully stopped upon return of this method. Consider implementing {@link SmartLifecycle} + * and its {@code stop(Runnable)} variant when asynchronous stop behavior is necessary. + *

    Note that this stop notification is not guaranteed to come before destruction: + * On regular shutdown, {@code Lifecycle} beans will first receive a stop notification + * before the general destruction callbacks are being propagated; however, on hot + * refresh during a context's lifetime or on aborted refresh attempts, a given bean's + * destroy method will be called without any consideration of stop signals upfront. + *

    Should not throw an exception if the component is not running (not started yet). + *

    In the case of a container, this will propagate the stop signal to all components + * that apply. + * @see SmartLifecycle#stop(Runnable) + * @see org.springframework.beans.factory.DisposableBean#destroy() + */ + void stop(); + + /** + * Check whether this component is currently running. + *

    In the case of a container, this will return {@code true} only if all + * components that apply are currently running. + * @return whether the component is currently running + */ + boolean isRunning(); + +} diff --git a/spring-context/src/main/java/org/springframework/context/LifecycleProcessor.java b/spring-context/src/main/java/org/springframework/context/LifecycleProcessor.java new file mode 100644 index 0000000..6be0e0e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/LifecycleProcessor.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2009 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context; + +/** + * Strategy interface for processing Lifecycle beans within the ApplicationContext. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @since 3.0 + */ +public interface LifecycleProcessor extends Lifecycle { + + /** + * Notification of context refresh, e.g. for auto-starting components. + */ + void onRefresh(); + + /** + * Notification of context close phase, e.g. for auto-stopping components. + */ + void onClose(); + +} diff --git a/spring-context/src/main/java/org/springframework/context/MessageSource.java b/spring-context/src/main/java/org/springframework/context/MessageSource.java new file mode 100644 index 0000000..54f5ea8 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/MessageSource.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context; + +import java.util.Locale; + +import org.springframework.lang.Nullable; + +/** + * Strategy interface for resolving messages, with support for the parameterization + * and internationalization of such messages. + * + *

    Spring provides two out-of-the-box implementations for production: + *

      + *
    • {@link org.springframework.context.support.ResourceBundleMessageSource}: built + * on top of the standard {@link java.util.ResourceBundle}, sharing its limitations. + *
    • {@link org.springframework.context.support.ReloadableResourceBundleMessageSource}: + * highly configurable, in particular with respect to reloading message definitions. + *
    + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see org.springframework.context.support.ResourceBundleMessageSource + * @see org.springframework.context.support.ReloadableResourceBundleMessageSource + */ +public interface MessageSource { + + /** + * Try to resolve the message. Return default message if no message was found. + * @param code the message code to look up, e.g. 'calculator.noRateSet'. + * MessageSource users are encouraged to base message names on qualified class + * or package names, avoiding potential conflicts and ensuring maximum clarity. + * @param args an array of arguments that will be filled in for params within + * the message (params look like "{0}", "{1,date}", "{2,time}" within a message), + * or {@code null} if none + * @param defaultMessage a default message to return if the lookup fails + * @param locale the locale in which to do the lookup + * @return the resolved message if the lookup was successful, otherwise + * the default message passed as a parameter (which may be {@code null}) + * @see #getMessage(MessageSourceResolvable, Locale) + * @see java.text.MessageFormat + */ + @Nullable + String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale); + + /** + * Try to resolve the message. Treat as an error if the message can't be found. + * @param code the message code to look up, e.g. 'calculator.noRateSet'. + * MessageSource users are encouraged to base message names on qualified class + * or package names, avoiding potential conflicts and ensuring maximum clarity. + * @param args an array of arguments that will be filled in for params within + * the message (params look like "{0}", "{1,date}", "{2,time}" within a message), + * or {@code null} if none + * @param locale the locale in which to do the lookup + * @return the resolved message (never {@code null}) + * @throws NoSuchMessageException if no corresponding message was found + * @see #getMessage(MessageSourceResolvable, Locale) + * @see java.text.MessageFormat + */ + String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException; + + /** + * Try to resolve the message using all the attributes contained within the + * {@code MessageSourceResolvable} argument that was passed in. + *

    NOTE: We must throw a {@code NoSuchMessageException} on this method + * since at the time of calling this method we aren't able to determine if the + * {@code defaultMessage} property of the resolvable is {@code null} or not. + * @param resolvable the value object storing attributes required to resolve a message + * (may include a default message) + * @param locale the locale in which to do the lookup + * @return the resolved message (never {@code null} since even a + * {@code MessageSourceResolvable}-provided default message needs to be non-null) + * @throws NoSuchMessageException if no corresponding message was found + * (and no default message was provided by the {@code MessageSourceResolvable}) + * @see MessageSourceResolvable#getCodes() + * @see MessageSourceResolvable#getArguments() + * @see MessageSourceResolvable#getDefaultMessage() + * @see java.text.MessageFormat + */ + String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException; + +} diff --git a/spring-context/src/main/java/org/springframework/context/MessageSourceAware.java b/spring-context/src/main/java/org/springframework/context/MessageSourceAware.java new file mode 100644 index 0000000..504dc6a --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/MessageSourceAware.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context; + +import org.springframework.beans.factory.Aware; + +/** + * Interface to be implemented by any object that wishes to be notified + * of the MessageSource (typically the ApplicationContext) that it runs in. + * + *

    Note that the MessageSource can usually also be passed on as bean + * reference (to arbitrary bean properties or constructor arguments), because + * it is defined as bean with name "messageSource" in the application context. + * + * @author Juergen Hoeller + * @author Chris Beams + * @since 1.1.1 + * @see ApplicationContextAware + */ +public interface MessageSourceAware extends Aware { + + /** + * Set the MessageSource that this object runs in. + *

    Invoked after population of normal bean properties but before an init + * callback like InitializingBean's afterPropertiesSet or a custom init-method. + * Invoked before ApplicationContextAware's setApplicationContext. + * @param messageSource message source to be used by this object + */ + void setMessageSource(MessageSource messageSource); + +} diff --git a/spring-context/src/main/java/org/springframework/context/MessageSourceResolvable.java b/spring-context/src/main/java/org/springframework/context/MessageSourceResolvable.java new file mode 100644 index 0000000..6908b85 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/MessageSourceResolvable.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context; + +import org.springframework.lang.Nullable; + +/** + * Interface for objects that are suitable for message resolution in a + * {@link MessageSource}. + * + *

    Spring's own validation error classes implement this interface. + * + * @author Juergen Hoeller + * @see MessageSource#getMessage(MessageSourceResolvable, java.util.Locale) + * @see org.springframework.validation.ObjectError + * @see org.springframework.validation.FieldError + */ +@FunctionalInterface +public interface MessageSourceResolvable { + + /** + * Return the codes to be used to resolve this message, in the order that + * they should get tried. The last code will therefore be the default one. + * @return a String array of codes which are associated with this message + */ + @Nullable + String[] getCodes(); + + /** + * Return the array of arguments to be used to resolve this message. + *

    The default implementation simply returns {@code null}. + * @return an array of objects to be used as parameters to replace + * placeholders within the message text + * @see java.text.MessageFormat + */ + @Nullable + default Object[] getArguments() { + return null; + } + + /** + * Return the default message to be used to resolve this message. + *

    The default implementation simply returns {@code null}. + * Note that the default message may be identical to the primary + * message code ({@link #getCodes()}), which effectively enforces + * {@link org.springframework.context.support.AbstractMessageSource#setUseCodeAsDefaultMessage} + * for this particular message. + * @return the default message, or {@code null} if no default + */ + @Nullable + default String getDefaultMessage() { + return null; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/NoSuchMessageException.java b/spring-context/src/main/java/org/springframework/context/NoSuchMessageException.java new file mode 100644 index 0000000..a3db0f0 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/NoSuchMessageException.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context; + +import java.util.Locale; + +/** + * Exception thrown when a message can't be resolved. + * + * @author Rod Johnson + */ +@SuppressWarnings("serial") +public class NoSuchMessageException extends RuntimeException { + + /** + * Create a new exception. + * @param code the code that could not be resolved for given locale + * @param locale the locale that was used to search for the code within + */ + public NoSuchMessageException(String code, Locale locale) { + super("No message found under code '" + code + "' for locale '" + locale + "'."); + } + + /** + * Create a new exception. + * @param code the code that could not be resolved for given locale + */ + public NoSuchMessageException(String code) { + super("No message found under code '" + code + "' for locale '" + Locale.getDefault() + "'."); + } + +} + diff --git a/spring-context/src/main/java/org/springframework/context/PayloadApplicationEvent.java b/spring-context/src/main/java/org/springframework/context/PayloadApplicationEvent.java new file mode 100644 index 0000000..38633b9 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/PayloadApplicationEvent.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context; + +import org.springframework.core.ResolvableType; +import org.springframework.core.ResolvableTypeProvider; +import org.springframework.util.Assert; + +/** + * An {@link ApplicationEvent} that carries an arbitrary payload. + * + *

    Mainly intended for internal use within the framework. + * + * @author Stephane Nicoll + * @since 4.2 + * @param the payload type of the event + */ +@SuppressWarnings("serial") +public class PayloadApplicationEvent extends ApplicationEvent implements ResolvableTypeProvider { + + private final T payload; + + + /** + * Create a new PayloadApplicationEvent. + * @param source the object on which the event initially occurred (never {@code null}) + * @param payload the payload object (never {@code null}) + */ + public PayloadApplicationEvent(Object source, T payload) { + super(source); + Assert.notNull(payload, "Payload must not be null"); + this.payload = payload; + } + + + @Override + public ResolvableType getResolvableType() { + return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getPayload())); + } + + /** + * Return the payload of the event. + */ + public T getPayload() { + return this.payload; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/Phased.java b/spring-context/src/main/java/org/springframework/context/Phased.java new file mode 100644 index 0000000..fd5910e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/Phased.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context; + +/** + * Interface for objects that may participate in a phased + * process such as lifecycle management. + * + * @author Mark Fisher + * @since 3.0 + * @see SmartLifecycle + */ +public interface Phased { + + /** + * Return the phase value of this object. + */ + int getPhase(); + +} diff --git a/spring-context/src/main/java/org/springframework/context/ResourceLoaderAware.java b/spring-context/src/main/java/org/springframework/context/ResourceLoaderAware.java new file mode 100644 index 0000000..4e8568a --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/ResourceLoaderAware.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context; + +import org.springframework.beans.factory.Aware; +import org.springframework.core.io.ResourceLoader; + +/** + * Interface to be implemented by any object that wishes to be notified of the + * {@link ResourceLoader} (typically the ApplicationContext) that it runs in. + * This is an alternative to a full {@link ApplicationContext} dependency via + * the {@link org.springframework.context.ApplicationContextAware} interface. + * + *

    Note that {@link org.springframework.core.io.Resource} dependencies can also + * be exposed as bean properties of type {@code Resource}, populated via Strings + * with automatic type conversion by the bean factory. This removes the need for + * implementing any callback interface just for the purpose of accessing a + * specific file resource. + * + *

    You typically need a {@link ResourceLoader} when your application object has to + * access a variety of file resources whose names are calculated. A good strategy is + * to make the object use a {@link org.springframework.core.io.DefaultResourceLoader} + * but still implement {@code ResourceLoaderAware} to allow for overriding when + * running in an {@code ApplicationContext}. See + * {@link org.springframework.context.support.ReloadableResourceBundleMessageSource} + * for an example. + * + *

    A passed-in {@code ResourceLoader} can also be checked for the + * {@link org.springframework.core.io.support.ResourcePatternResolver} interface + * and cast accordingly, in order to resolve resource patterns into arrays of + * {@code Resource} objects. This will always work when running in an ApplicationContext + * (since the context interface extends the ResourcePatternResolver interface). Use a + * {@link org.springframework.core.io.support.PathMatchingResourcePatternResolver} as + * default; see also the {@code ResourcePatternUtils.getResourcePatternResolver} method. + * + *

    As an alternative to a {@code ResourcePatternResolver} dependency, consider + * exposing bean properties of type {@code Resource} array, populated via pattern + * Strings with automatic type conversion by the bean factory at binding time. + * + * @author Juergen Hoeller + * @author Chris Beams + * @since 10.03.2004 + * @see ApplicationContextAware + * @see org.springframework.core.io.Resource + * @see org.springframework.core.io.ResourceLoader + * @see org.springframework.core.io.support.ResourcePatternResolver + */ +public interface ResourceLoaderAware extends Aware { + + /** + * Set the ResourceLoader that this object runs in. + *

    This might be a ResourcePatternResolver, which can be checked + * through {@code instanceof ResourcePatternResolver}. See also the + * {@code ResourcePatternUtils.getResourcePatternResolver} method. + *

    Invoked after population of normal bean properties but before an init callback + * like InitializingBean's {@code afterPropertiesSet} or a custom init-method. + * Invoked before ApplicationContextAware's {@code setApplicationContext}. + * @param resourceLoader the ResourceLoader object to be used by this object + * @see org.springframework.core.io.support.ResourcePatternResolver + * @see org.springframework.core.io.support.ResourcePatternUtils#getResourcePatternResolver + */ + void setResourceLoader(ResourceLoader resourceLoader); + +} diff --git a/spring-context/src/main/java/org/springframework/context/SmartLifecycle.java b/spring-context/src/main/java/org/springframework/context/SmartLifecycle.java new file mode 100644 index 0000000..06f98a4 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/SmartLifecycle.java @@ -0,0 +1,136 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context; + +/** + * An extension of the {@link Lifecycle} interface for those objects that require + * to be started upon {@code ApplicationContext} refresh and/or shutdown in a + * particular order. + * + *

    The {@link #isAutoStartup()} return value indicates whether this object should + * be started at the time of a context refresh. The callback-accepting + * {@link #stop(Runnable)} method is useful for objects that have an asynchronous + * shutdown process. Any implementation of this interface must invoke the + * callback's {@code run()} method upon shutdown completion to avoid unnecessary + * delays in the overall {@code ApplicationContext} shutdown. + * + *

    This interface extends {@link Phased}, and the {@link #getPhase()} method's + * return value indicates the phase within which this {@code Lifecycle} component + * should be started and stopped. The startup process begins with the lowest + * phase value and ends with the highest phase value ({@code Integer.MIN_VALUE} + * is the lowest possible, and {@code Integer.MAX_VALUE} is the highest possible). + * The shutdown process will apply the reverse order. Any components with the + * same value will be arbitrarily ordered within the same phase. + * + *

    Example: if component B depends on component A having already started, + * then component A should have a lower phase value than component B. During + * the shutdown process, component B would be stopped before component A. + * + *

    Any explicit "depends-on" relationship will take precedence over the phase + * order such that the dependent bean always starts after its dependency and + * always stops before its dependency. + * + *

    Any {@code Lifecycle} components within the context that do not also + * implement {@code SmartLifecycle} will be treated as if they have a phase + * value of {@code 0}. This allows a {@code SmartLifecycle} component to start + * before those {@code Lifecycle} components if the {@code SmartLifecycle} + * component has a negative phase value, or the {@code SmartLifecycle} component + * may start after those {@code Lifecycle} components if the {@code SmartLifecycle} + * component has a positive phase value. + * + *

    Note that, due to the auto-startup support in {@code SmartLifecycle}, a + * {@code SmartLifecycle} bean instance will usually get initialized on startup + * of the application context in any case. As a consequence, the bean definition + * lazy-init flag has very limited actual effect on {@code SmartLifecycle} beans. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @author Sam Brannen + * @since 3.0 + * @see LifecycleProcessor + * @see ConfigurableApplicationContext + */ +public interface SmartLifecycle extends Lifecycle, Phased { + + /** + * The default phase for {@code SmartLifecycle}: {@code Integer.MAX_VALUE}. + *

    This is different from the common phase {@code 0} associated with regular + * {@link Lifecycle} implementations, putting the typically auto-started + * {@code SmartLifecycle} beans into a later startup phase and an earlier + * shutdown phase. + * @since 5.1 + * @see #getPhase() + * @see org.springframework.context.support.DefaultLifecycleProcessor#getPhase(Lifecycle) + */ + int DEFAULT_PHASE = Integer.MAX_VALUE; + + + /** + * Returns {@code true} if this {@code Lifecycle} component should get + * started automatically by the container at the time that the containing + * {@link ApplicationContext} gets refreshed. + *

    A value of {@code false} indicates that the component is intended to + * be started through an explicit {@link #start()} call instead, analogous + * to a plain {@link Lifecycle} implementation. + *

    The default implementation returns {@code true}. + * @see #start() + * @see #getPhase() + * @see LifecycleProcessor#onRefresh() + * @see ConfigurableApplicationContext#refresh() + */ + default boolean isAutoStartup() { + return true; + } + + /** + * Indicates that a Lifecycle component must stop if it is currently running. + *

    The provided callback is used by the {@link LifecycleProcessor} to support + * an ordered, and potentially concurrent, shutdown of all components having a + * common shutdown order value. The callback must be executed after + * the {@code SmartLifecycle} component does indeed stop. + *

    The {@link LifecycleProcessor} will call only this variant of the + * {@code stop} method; i.e. {@link Lifecycle#stop()} will not be called for + * {@code SmartLifecycle} implementations unless explicitly delegated to within + * the implementation of this method. + *

    The default implementation delegates to {@link #stop()} and immediately + * triggers the given callback in the calling thread. Note that there is no + * synchronization between the two, so custom implementations may at least + * want to put the same steps within their common lifecycle monitor (if any). + * @see #stop() + * @see #getPhase() + */ + default void stop(Runnable callback) { + stop(); + callback.run(); + } + + /** + * Return the phase that this lifecycle object is supposed to run in. + *

    The default implementation returns {@link #DEFAULT_PHASE} in order to + * let {@code stop()} callbacks execute after regular {@code Lifecycle} + * implementations. + * @see #isAutoStartup() + * @see #start() + * @see #stop(Runnable) + * @see org.springframework.context.support.DefaultLifecycleProcessor#getPhase(Lifecycle) + */ + @Override + default int getPhase() { + return DEFAULT_PHASE; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AdviceMode.java b/spring-context/src/main/java/org/springframework/context/annotation/AdviceMode.java new file mode 100644 index 0000000..d4a86dc --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/AdviceMode.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +/** + * Enumeration used to determine whether JDK proxy-based or + * AspectJ weaving-based advice should be applied. + * + * @author Chris Beams + * @since 3.1 + * @see org.springframework.scheduling.annotation.EnableAsync#mode() + * @see org.springframework.scheduling.annotation.AsyncConfigurationSelector#selectImports + * @see org.springframework.transaction.annotation.EnableTransactionManagement#mode() + */ +public enum AdviceMode { + + /** + * JDK proxy-based advice. + */ + PROXY, + + /** + * AspectJ weaving-based advice. + */ + ASPECTJ + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AdviceModeImportSelector.java b/spring-context/src/main/java/org/springframework/context/annotation/AdviceModeImportSelector.java new file mode 100644 index 0000000..bc2def9 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/AdviceModeImportSelector.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.Annotation; + +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Convenient base class for {@link ImportSelector} implementations that select imports + * based on an {@link AdviceMode} value from an annotation (such as the {@code @Enable*} + * annotations). + * + * @author Chris Beams + * @since 3.1 + * @param annotation containing {@linkplain #getAdviceModeAttributeName() AdviceMode attribute} + */ +public abstract class AdviceModeImportSelector implements ImportSelector { + + /** + * The default advice mode attribute name. + */ + public static final String DEFAULT_ADVICE_MODE_ATTRIBUTE_NAME = "mode"; + + + /** + * The name of the {@link AdviceMode} attribute for the annotation specified by the + * generic type {@code A}. The default is {@value #DEFAULT_ADVICE_MODE_ATTRIBUTE_NAME}, + * but subclasses may override in order to customize. + */ + protected String getAdviceModeAttributeName() { + return DEFAULT_ADVICE_MODE_ATTRIBUTE_NAME; + } + + /** + * This implementation resolves the type of annotation from generic metadata and + * validates that (a) the annotation is in fact present on the importing + * {@code @Configuration} class and (b) that the given annotation has an + * {@linkplain #getAdviceModeAttributeName() advice mode attribute} of type + * {@link AdviceMode}. + *

    The {@link #selectImports(AdviceMode)} method is then invoked, allowing the + * concrete implementation to choose imports in a safe and convenient fashion. + * @throws IllegalArgumentException if expected annotation {@code A} is not present + * on the importing {@code @Configuration} class or if {@link #selectImports(AdviceMode)} + * returns {@code null} + */ + @Override + public final String[] selectImports(AnnotationMetadata importingClassMetadata) { + Class annType = GenericTypeResolver.resolveTypeArgument(getClass(), AdviceModeImportSelector.class); + Assert.state(annType != null, "Unresolvable type argument for AdviceModeImportSelector"); + + AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(importingClassMetadata, annType); + if (attributes == null) { + throw new IllegalArgumentException(String.format( + "@%s is not present on importing class '%s' as expected", + annType.getSimpleName(), importingClassMetadata.getClassName())); + } + + AdviceMode adviceMode = attributes.getEnum(getAdviceModeAttributeName()); + String[] imports = selectImports(adviceMode); + if (imports == null) { + throw new IllegalArgumentException("Unknown AdviceMode: " + adviceMode); + } + return imports; + } + + /** + * Determine which classes should be imported based on the given {@code AdviceMode}. + *

    Returning {@code null} from this method indicates that the {@code AdviceMode} + * could not be handled or was unknown and that an {@code IllegalArgumentException} + * should be thrown. + * @param adviceMode the value of the {@linkplain #getAdviceModeAttributeName() + * advice mode attribute} for the annotation specified via generics. + * @return array containing classes to import (empty array if none; + * {@code null} if the given {@code AdviceMode} is unknown) + */ + @Nullable + protected abstract String[] selectImports(AdviceMode adviceMode); + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java new file mode 100644 index 0000000..c752938 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java @@ -0,0 +1,301 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.Annotation; +import java.util.function.Supplier; + +import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionCustomizer; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.support.AutowireCandidateQualifier; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.core.env.Environment; +import org.springframework.core.env.EnvironmentCapable; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Convenient adapter for programmatic registration of bean classes. + * + *

    This is an alternative to {@link ClassPathBeanDefinitionScanner}, applying + * the same resolution of annotations but for explicitly registered classes only. + * + * @author Juergen Hoeller + * @author Chris Beams + * @author Sam Brannen + * @author Phillip Webb + * @since 3.0 + * @see AnnotationConfigApplicationContext#register + */ +public class AnnotatedBeanDefinitionReader { + + private final BeanDefinitionRegistry registry; + + private BeanNameGenerator beanNameGenerator = AnnotationBeanNameGenerator.INSTANCE; + + private ScopeMetadataResolver scopeMetadataResolver = new AnnotationScopeMetadataResolver(); + + private ConditionEvaluator conditionEvaluator; + + + /** + * Create a new {@code AnnotatedBeanDefinitionReader} for the given registry. + *

    If the registry is {@link EnvironmentCapable}, e.g. is an {@code ApplicationContext}, + * the {@link Environment} will be inherited, otherwise a new + * {@link StandardEnvironment} will be created and used. + * @param registry the {@code BeanFactory} to load bean definitions into, + * in the form of a {@code BeanDefinitionRegistry} + * @see #AnnotatedBeanDefinitionReader(BeanDefinitionRegistry, Environment) + * @see #setEnvironment(Environment) + */ + public AnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry) { + this(registry, getOrCreateEnvironment(registry)); + } + + /** + * Create a new {@code AnnotatedBeanDefinitionReader} for the given registry, + * using the given {@link Environment}. + * @param registry the {@code BeanFactory} to load bean definitions into, + * in the form of a {@code BeanDefinitionRegistry} + * @param environment the {@code Environment} to use when evaluating bean definition + * profiles. + * @since 3.1 + */ + public AnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry, Environment environment) { + Assert.notNull(registry, "BeanDefinitionRegistry must not be null"); + Assert.notNull(environment, "Environment must not be null"); + this.registry = registry; + this.conditionEvaluator = new ConditionEvaluator(registry, environment, null); + AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry); + } + + + /** + * Get the BeanDefinitionRegistry that this reader operates on. + */ + public final BeanDefinitionRegistry getRegistry() { + return this.registry; + } + + /** + * Set the {@code Environment} to use when evaluating whether + * {@link Conditional @Conditional}-annotated component classes should be registered. + *

    The default is a {@link StandardEnvironment}. + * @see #registerBean(Class, String, Class...) + */ + public void setEnvironment(Environment environment) { + this.conditionEvaluator = new ConditionEvaluator(this.registry, environment, null); + } + + /** + * Set the {@code BeanNameGenerator} to use for detected bean classes. + *

    The default is a {@link AnnotationBeanNameGenerator}. + */ + public void setBeanNameGenerator(@Nullable BeanNameGenerator beanNameGenerator) { + this.beanNameGenerator = + (beanNameGenerator != null ? beanNameGenerator : AnnotationBeanNameGenerator.INSTANCE); + } + + /** + * Set the {@code ScopeMetadataResolver} to use for registered component classes. + *

    The default is an {@link AnnotationScopeMetadataResolver}. + */ + public void setScopeMetadataResolver(@Nullable ScopeMetadataResolver scopeMetadataResolver) { + this.scopeMetadataResolver = + (scopeMetadataResolver != null ? scopeMetadataResolver : new AnnotationScopeMetadataResolver()); + } + + + /** + * Register one or more component classes to be processed. + *

    Calls to {@code register} are idempotent; adding the same + * component class more than once has no additional effect. + * @param componentClasses one or more component classes, + * e.g. {@link Configuration @Configuration} classes + */ + public void register(Class... componentClasses) { + for (Class componentClass : componentClasses) { + registerBean(componentClass); + } + } + + /** + * Register a bean from the given bean class, deriving its metadata from + * class-declared annotations. + * @param beanClass the class of the bean + */ + public void registerBean(Class beanClass) { + doRegisterBean(beanClass, null, null, null, null); + } + + /** + * Register a bean from the given bean class, deriving its metadata from + * class-declared annotations. + * @param beanClass the class of the bean + * @param name an explicit name for the bean + * (or {@code null} for generating a default bean name) + * @since 5.2 + */ + public void registerBean(Class beanClass, @Nullable String name) { + doRegisterBean(beanClass, name, null, null, null); + } + + /** + * Register a bean from the given bean class, deriving its metadata from + * class-declared annotations. + * @param beanClass the class of the bean + * @param qualifiers specific qualifier annotations to consider, + * in addition to qualifiers at the bean class level + */ + @SuppressWarnings("unchecked") + public void registerBean(Class beanClass, Class... qualifiers) { + doRegisterBean(beanClass, null, qualifiers, null, null); + } + + /** + * Register a bean from the given bean class, deriving its metadata from + * class-declared annotations. + * @param beanClass the class of the bean + * @param name an explicit name for the bean + * (or {@code null} for generating a default bean name) + * @param qualifiers specific qualifier annotations to consider, + * in addition to qualifiers at the bean class level + */ + @SuppressWarnings("unchecked") + public void registerBean(Class beanClass, @Nullable String name, + Class... qualifiers) { + + doRegisterBean(beanClass, name, qualifiers, null, null); + } + + /** + * Register a bean from the given bean class, deriving its metadata from + * class-declared annotations, using the given supplier for obtaining a new + * instance (possibly declared as a lambda expression or method reference). + * @param beanClass the class of the bean + * @param supplier a callback for creating an instance of the bean + * (may be {@code null}) + * @since 5.0 + */ + public void registerBean(Class beanClass, @Nullable Supplier supplier) { + doRegisterBean(beanClass, null, null, supplier, null); + } + + /** + * Register a bean from the given bean class, deriving its metadata from + * class-declared annotations, using the given supplier for obtaining a new + * instance (possibly declared as a lambda expression or method reference). + * @param beanClass the class of the bean + * @param name an explicit name for the bean + * (or {@code null} for generating a default bean name) + * @param supplier a callback for creating an instance of the bean + * (may be {@code null}) + * @since 5.0 + */ + public void registerBean(Class beanClass, @Nullable String name, @Nullable Supplier supplier) { + doRegisterBean(beanClass, name, null, supplier, null); + } + + /** + * Register a bean from the given bean class, deriving its metadata from + * class-declared annotations. + * @param beanClass the class of the bean + * @param name an explicit name for the bean + * (or {@code null} for generating a default bean name) + * @param supplier a callback for creating an instance of the bean + * (may be {@code null}) + * @param customizers one or more callbacks for customizing the factory's + * {@link BeanDefinition}, e.g. setting a lazy-init or primary flag + * @since 5.2 + */ + public void registerBean(Class beanClass, @Nullable String name, @Nullable Supplier supplier, + BeanDefinitionCustomizer... customizers) { + + doRegisterBean(beanClass, name, null, supplier, customizers); + } + + /** + * Register a bean from the given bean class, deriving its metadata from + * class-declared annotations. + * @param beanClass the class of the bean + * @param name an explicit name for the bean + * @param qualifiers specific qualifier annotations to consider, if any, + * in addition to qualifiers at the bean class level + * @param supplier a callback for creating an instance of the bean + * (may be {@code null}) + * @param customizers one or more callbacks for customizing the factory's + * {@link BeanDefinition}, e.g. setting a lazy-init or primary flag + * @since 5.0 + */ + private void doRegisterBean(Class beanClass, @Nullable String name, + @Nullable Class[] qualifiers, @Nullable Supplier supplier, + @Nullable BeanDefinitionCustomizer[] customizers) { + + AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(beanClass); + if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) { + return; + } + + abd.setInstanceSupplier(supplier); + ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd); + abd.setScope(scopeMetadata.getScopeName()); + String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry)); + + AnnotationConfigUtils.processCommonDefinitionAnnotations(abd); + if (qualifiers != null) { + for (Class qualifier : qualifiers) { + if (Primary.class == qualifier) { + abd.setPrimary(true); + } + else if (Lazy.class == qualifier) { + abd.setLazyInit(true); + } + else { + abd.addQualifier(new AutowireCandidateQualifier(qualifier)); + } + } + } + if (customizers != null) { + for (BeanDefinitionCustomizer customizer : customizers) { + customizer.customize(abd); + } + } + + BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName); + definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry); + BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry); + } + + + /** + * Get the Environment from the given registry if possible, otherwise return a new + * StandardEnvironment. + */ + private static Environment getOrCreateEnvironment(BeanDefinitionRegistry registry) { + Assert.notNull(registry, "BeanDefinitionRegistry must not be null"); + if (registry instanceof EnvironmentCapable) { + return ((EnvironmentCapable) registry).getEnvironment(); + } + return new StandardEnvironment(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java new file mode 100644 index 0000000..a589a92 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java @@ -0,0 +1,173 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.beans.Introspector; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * {@link BeanNameGenerator} implementation for bean classes annotated with the + * {@link org.springframework.stereotype.Component @Component} annotation or + * with another annotation that is itself annotated with {@code @Component} as a + * meta-annotation. For example, Spring's stereotype annotations (such as + * {@link org.springframework.stereotype.Repository @Repository}) are + * themselves annotated with {@code @Component}. + * + *

    Also supports Java EE 6's {@link javax.annotation.ManagedBean} and + * JSR-330's {@link javax.inject.Named} annotations, if available. Note that + * Spring component annotations always override such standard annotations. + * + *

    If the annotation's value doesn't indicate a bean name, an appropriate + * name will be built based on the short name of the class (with the first + * letter lower-cased). For example: + * + *

    com.xyz.FooServiceImpl -> fooServiceImpl
    + * + * @author Juergen Hoeller + * @author Mark Fisher + * @since 2.5 + * @see org.springframework.stereotype.Component#value() + * @see org.springframework.stereotype.Repository#value() + * @see org.springframework.stereotype.Service#value() + * @see org.springframework.stereotype.Controller#value() + * @see javax.inject.Named#value() + * @see FullyQualifiedAnnotationBeanNameGenerator + */ +public class AnnotationBeanNameGenerator implements BeanNameGenerator { + + /** + * A convenient constant for a default {@code AnnotationBeanNameGenerator} instance, + * as used for component scanning purposes. + * @since 5.2 + */ + public static final AnnotationBeanNameGenerator INSTANCE = new AnnotationBeanNameGenerator(); + + private static final String COMPONENT_ANNOTATION_CLASSNAME = "org.springframework.stereotype.Component"; + + private final Map> metaAnnotationTypesCache = new ConcurrentHashMap<>(); + + + @Override + public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) { + if (definition instanceof AnnotatedBeanDefinition) { + String beanName = determineBeanNameFromAnnotation((AnnotatedBeanDefinition) definition); + if (StringUtils.hasText(beanName)) { + // Explicit bean name found. + return beanName; + } + } + // Fallback: generate a unique default bean name. + return buildDefaultBeanName(definition, registry); + } + + /** + * Derive a bean name from one of the annotations on the class. + * @param annotatedDef the annotation-aware bean definition + * @return the bean name, or {@code null} if none is found + */ + @Nullable + protected String determineBeanNameFromAnnotation(AnnotatedBeanDefinition annotatedDef) { + AnnotationMetadata amd = annotatedDef.getMetadata(); + Set types = amd.getAnnotationTypes(); + String beanName = null; + for (String type : types) { + AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(amd, type); + if (attributes != null) { + Set metaTypes = this.metaAnnotationTypesCache.computeIfAbsent(type, key -> { + Set result = amd.getMetaAnnotationTypes(key); + return (result.isEmpty() ? Collections.emptySet() : result); + }); + if (isStereotypeWithNameValue(type, metaTypes, attributes)) { + Object value = attributes.get("value"); + if (value instanceof String) { + String strVal = (String) value; + if (StringUtils.hasLength(strVal)) { + if (beanName != null && !strVal.equals(beanName)) { + throw new IllegalStateException("Stereotype annotations suggest inconsistent " + + "component names: '" + beanName + "' versus '" + strVal + "'"); + } + beanName = strVal; + } + } + } + } + } + return beanName; + } + + /** + * Check whether the given annotation is a stereotype that is allowed + * to suggest a component name through its annotation {@code value()}. + * @param annotationType the name of the annotation class to check + * @param metaAnnotationTypes the names of meta-annotations on the given annotation + * @param attributes the map of attributes for the given annotation + * @return whether the annotation qualifies as a stereotype with component name + */ + protected boolean isStereotypeWithNameValue(String annotationType, + Set metaAnnotationTypes, @Nullable Map attributes) { + + boolean isStereotype = annotationType.equals(COMPONENT_ANNOTATION_CLASSNAME) || + metaAnnotationTypes.contains(COMPONENT_ANNOTATION_CLASSNAME) || + annotationType.equals("javax.annotation.ManagedBean") || + annotationType.equals("javax.inject.Named"); + + return (isStereotype && attributes != null && attributes.containsKey("value")); + } + + /** + * Derive a default bean name from the given bean definition. + *

    The default implementation delegates to {@link #buildDefaultBeanName(BeanDefinition)}. + * @param definition the bean definition to build a bean name for + * @param registry the registry that the given bean definition is being registered with + * @return the default bean name (never {@code null}) + */ + protected String buildDefaultBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) { + return buildDefaultBeanName(definition); + } + + /** + * Derive a default bean name from the given bean definition. + *

    The default implementation simply builds a decapitalized version + * of the short class name: e.g. "mypackage.MyJdbcDao" -> "myJdbcDao". + *

    Note that inner classes will thus have names of the form + * "outerClassName.InnerClassName", which because of the period in the + * name may be an issue if you are autowiring by name. + * @param definition the bean definition to build a bean name for + * @return the default bean name (never {@code null}) + */ + protected String buildDefaultBeanName(BeanDefinition definition) { + String beanClassName = definition.getBeanClassName(); + Assert.state(beanClassName != null, "No bean class name set"); + String shortClassName = ClassUtils.getShortName(beanClassName); + return Introspector.decapitalize(shortClassName); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigApplicationContext.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigApplicationContext.java new file mode 100644 index 0000000..86ea5fe --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigApplicationContext.java @@ -0,0 +1,201 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.util.Arrays; +import java.util.function.Supplier; + +import org.springframework.beans.factory.config.BeanDefinitionCustomizer; +import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.metrics.StartupStep; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Standalone application context, accepting component classes as input — + * in particular {@link Configuration @Configuration}-annotated classes, but also plain + * {@link org.springframework.stereotype.Component @Component} types and JSR-330 compliant + * classes using {@code javax.inject} annotations. + * + *

    Allows for registering classes one by one using {@link #register(Class...)} + * as well as for classpath scanning using {@link #scan(String...)}. + * + *

    In case of multiple {@code @Configuration} classes, {@link Bean @Bean} methods + * defined in later classes will override those defined in earlier classes. This can + * be leveraged to deliberately override certain bean definitions via an extra + * {@code @Configuration} class. + * + *

    See {@link Configuration @Configuration}'s javadoc for usage examples. + * + * @author Juergen Hoeller + * @author Chris Beams + * @since 3.0 + * @see #register + * @see #scan + * @see AnnotatedBeanDefinitionReader + * @see ClassPathBeanDefinitionScanner + * @see org.springframework.context.support.GenericXmlApplicationContext + */ +public class AnnotationConfigApplicationContext extends GenericApplicationContext implements AnnotationConfigRegistry { + + private final AnnotatedBeanDefinitionReader reader; + + private final ClassPathBeanDefinitionScanner scanner; + + + /** + * Create a new AnnotationConfigApplicationContext that needs to be populated + * through {@link #register} calls and then manually {@linkplain #refresh refreshed}. + */ + public AnnotationConfigApplicationContext() { + StartupStep createAnnotatedBeanDefReader = this.getApplicationStartup().start("spring.context.annotated-bean-reader.create"); + this.reader = new AnnotatedBeanDefinitionReader(this); + createAnnotatedBeanDefReader.end(); + this.scanner = new ClassPathBeanDefinitionScanner(this); + } + + /** + * Create a new AnnotationConfigApplicationContext with the given DefaultListableBeanFactory. + * @param beanFactory the DefaultListableBeanFactory instance to use for this context + */ + public AnnotationConfigApplicationContext(DefaultListableBeanFactory beanFactory) { + super(beanFactory); + this.reader = new AnnotatedBeanDefinitionReader(this); + this.scanner = new ClassPathBeanDefinitionScanner(this); + } + + /** + * Create a new AnnotationConfigApplicationContext, deriving bean definitions + * from the given component classes and automatically refreshing the context. + * @param componentClasses one or more component classes — for example, + * {@link Configuration @Configuration} classes + */ + public AnnotationConfigApplicationContext(Class... componentClasses) { + this(); + register(componentClasses); + refresh(); + } + + /** + * Create a new AnnotationConfigApplicationContext, scanning for components + * in the given packages, registering bean definitions for those components, + * and automatically refreshing the context. + * @param basePackages the packages to scan for component classes + */ + public AnnotationConfigApplicationContext(String... basePackages) { + this(); + scan(basePackages); + refresh(); + } + + + /** + * Propagate the given custom {@code Environment} to the underlying + * {@link AnnotatedBeanDefinitionReader} and {@link ClassPathBeanDefinitionScanner}. + */ + @Override + public void setEnvironment(ConfigurableEnvironment environment) { + super.setEnvironment(environment); + this.reader.setEnvironment(environment); + this.scanner.setEnvironment(environment); + } + + /** + * Provide a custom {@link BeanNameGenerator} for use with {@link AnnotatedBeanDefinitionReader} + * and/or {@link ClassPathBeanDefinitionScanner}, if any. + *

    Default is {@link AnnotationBeanNameGenerator}. + *

    Any call to this method must occur prior to calls to {@link #register(Class...)} + * and/or {@link #scan(String...)}. + * @see AnnotatedBeanDefinitionReader#setBeanNameGenerator + * @see ClassPathBeanDefinitionScanner#setBeanNameGenerator + * @see AnnotationBeanNameGenerator + * @see FullyQualifiedAnnotationBeanNameGenerator + */ + public void setBeanNameGenerator(BeanNameGenerator beanNameGenerator) { + this.reader.setBeanNameGenerator(beanNameGenerator); + this.scanner.setBeanNameGenerator(beanNameGenerator); + getBeanFactory().registerSingleton( + AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR, beanNameGenerator); + } + + /** + * Set the {@link ScopeMetadataResolver} to use for registered component classes. + *

    The default is an {@link AnnotationScopeMetadataResolver}. + *

    Any call to this method must occur prior to calls to {@link #register(Class...)} + * and/or {@link #scan(String...)}. + */ + public void setScopeMetadataResolver(ScopeMetadataResolver scopeMetadataResolver) { + this.reader.setScopeMetadataResolver(scopeMetadataResolver); + this.scanner.setScopeMetadataResolver(scopeMetadataResolver); + } + + + //--------------------------------------------------------------------- + // Implementation of AnnotationConfigRegistry + //--------------------------------------------------------------------- + + /** + * Register one or more component classes to be processed. + *

    Note that {@link #refresh()} must be called in order for the context + * to fully process the new classes. + * @param componentClasses one or more component classes — for example, + * {@link Configuration @Configuration} classes + * @see #scan(String...) + * @see #refresh() + */ + @Override + public void register(Class... componentClasses) { + Assert.notEmpty(componentClasses, "At least one component class must be specified"); + StartupStep registerComponentClass = this.getApplicationStartup().start("spring.context.component-classes.register") + .tag("classes", () -> Arrays.toString(componentClasses)); + this.reader.register(componentClasses); + registerComponentClass.end(); + } + + /** + * Perform a scan within the specified base packages. + *

    Note that {@link #refresh()} must be called in order for the context + * to fully process the new classes. + * @param basePackages the packages to scan for component classes + * @see #register(Class...) + * @see #refresh() + */ + @Override + public void scan(String... basePackages) { + Assert.notEmpty(basePackages, "At least one base package must be specified"); + StartupStep scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan") + .tag("packages", () -> Arrays.toString(basePackages)); + this.scanner.scan(basePackages); + scanPackages.end(); + } + + + //--------------------------------------------------------------------- + // Adapt superclass registerBean calls to AnnotatedBeanDefinitionReader + //--------------------------------------------------------------------- + + @Override + public void registerBean(@Nullable String beanName, Class beanClass, + @Nullable Supplier supplier, BeanDefinitionCustomizer... customizers) { + + this.reader.registerBean(beanClass, beanName, supplier, customizers); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigBeanDefinitionParser.java new file mode 100644 index 0000000..878d545 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigBeanDefinitionParser.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.util.Set; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.parsing.CompositeComponentDefinition; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.lang.Nullable; + +/** + * Parser for the <context:annotation-config/> element. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @author Christian Dupuis + * @since 2.5 + * @see AnnotationConfigUtils + */ +public class AnnotationConfigBeanDefinitionParser implements BeanDefinitionParser { + + @Override + @Nullable + public BeanDefinition parse(Element element, ParserContext parserContext) { + Object source = parserContext.extractSource(element); + + // Obtain bean definitions for all relevant BeanPostProcessors. + Set processorDefinitions = + AnnotationConfigUtils.registerAnnotationConfigProcessors(parserContext.getRegistry(), source); + + // Register component for the surrounding element. + CompositeComponentDefinition compDefinition = new CompositeComponentDefinition(element.getTagName(), source); + parserContext.pushContainingComponent(compDefinition); + + // Nest the concrete beans in the surrounding component. + for (BeanDefinitionHolder processorDefinition : processorDefinitions) { + parserContext.registerComponent(new BeanComponentDefinition(processorDefinition)); + } + + // Finally register the composite component. + parserContext.popAndRegisterContainingComponent(); + + return null; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigRegistry.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigRegistry.java new file mode 100644 index 0000000..4535537 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigRegistry.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +/** + * Common interface for annotation config application contexts, + * defining {@link #register} and {@link #scan} methods. + * + * @author Juergen Hoeller + * @since 4.1 + */ +public interface AnnotationConfigRegistry { + + /** + * Register one or more component classes to be processed. + *

    Calls to {@code register} are idempotent; adding the same + * component class more than once has no additional effect. + * @param componentClasses one or more component classes, + * e.g. {@link Configuration @Configuration} classes + */ + void register(Class... componentClasses); + + /** + * Perform a scan within the specified base packages. + * @param basePackages the packages to scan for component classes + */ + void scan(String... basePackages); + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java new file mode 100644 index 0000000..91b1719 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java @@ -0,0 +1,323 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.event.DefaultEventListenerFactory; +import org.springframework.context.event.EventListenerMethodProcessor; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * Utility class that allows for convenient registration of common + * {@link org.springframework.beans.factory.config.BeanPostProcessor} and + * {@link org.springframework.beans.factory.config.BeanFactoryPostProcessor} + * definitions for annotation-based configuration. Also registers a common + * {@link org.springframework.beans.factory.support.AutowireCandidateResolver}. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @author Chris Beams + * @author Phillip Webb + * @author Stephane Nicoll + * @since 2.5 + * @see ContextAnnotationAutowireCandidateResolver + * @see ConfigurationClassPostProcessor + * @see CommonAnnotationBeanPostProcessor + * @see org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor + * @see org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor + */ +public abstract class AnnotationConfigUtils { + + /** + * The bean name of the internally managed Configuration annotation processor. + */ + public static final String CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME = + "org.springframework.context.annotation.internalConfigurationAnnotationProcessor"; + + /** + * The bean name of the internally managed BeanNameGenerator for use when processing + * {@link Configuration} classes. Set by {@link AnnotationConfigApplicationContext} + * and {@code AnnotationConfigWebApplicationContext} during bootstrap in order to make + * any custom name generation strategy available to the underlying + * {@link ConfigurationClassPostProcessor}. + * @since 3.1.1 + */ + public static final String CONFIGURATION_BEAN_NAME_GENERATOR = + "org.springframework.context.annotation.internalConfigurationBeanNameGenerator"; + + /** + * The bean name of the internally managed Autowired annotation processor. + */ + public static final String AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME = + "org.springframework.context.annotation.internalAutowiredAnnotationProcessor"; + + /** + * The bean name of the internally managed Required annotation processor. + * @deprecated as of 5.1, since no Required processor is registered by default anymore + */ + @Deprecated + public static final String REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME = + "org.springframework.context.annotation.internalRequiredAnnotationProcessor"; + + /** + * The bean name of the internally managed JSR-250 annotation processor. + */ + public static final String COMMON_ANNOTATION_PROCESSOR_BEAN_NAME = + "org.springframework.context.annotation.internalCommonAnnotationProcessor"; + + /** + * The bean name of the internally managed JPA annotation processor. + */ + public static final String PERSISTENCE_ANNOTATION_PROCESSOR_BEAN_NAME = + "org.springframework.context.annotation.internalPersistenceAnnotationProcessor"; + + private static final String PERSISTENCE_ANNOTATION_PROCESSOR_CLASS_NAME = + "org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor"; + + /** + * The bean name of the internally managed @EventListener annotation processor. + */ + public static final String EVENT_LISTENER_PROCESSOR_BEAN_NAME = + "org.springframework.context.event.internalEventListenerProcessor"; + + /** + * The bean name of the internally managed EventListenerFactory. + */ + public static final String EVENT_LISTENER_FACTORY_BEAN_NAME = + "org.springframework.context.event.internalEventListenerFactory"; + + private static final boolean jsr250Present; + + private static final boolean jpaPresent; + + static { + ClassLoader classLoader = AnnotationConfigUtils.class.getClassLoader(); + jsr250Present = ClassUtils.isPresent("javax.annotation.Resource", classLoader); + jpaPresent = ClassUtils.isPresent("javax.persistence.EntityManagerFactory", classLoader) && + ClassUtils.isPresent(PERSISTENCE_ANNOTATION_PROCESSOR_CLASS_NAME, classLoader); + } + + + /** + * Register all relevant annotation post processors in the given registry. + * @param registry the registry to operate on + */ + public static void registerAnnotationConfigProcessors(BeanDefinitionRegistry registry) { + registerAnnotationConfigProcessors(registry, null); + } + + /** + * Register all relevant annotation post processors in the given registry. + * @param registry the registry to operate on + * @param source the configuration source element (already extracted) + * that this registration was triggered from. May be {@code null}. + * @return a Set of BeanDefinitionHolders, containing all bean definitions + * that have actually been registered by this call + */ + public static Set registerAnnotationConfigProcessors( + BeanDefinitionRegistry registry, @Nullable Object source) { + + DefaultListableBeanFactory beanFactory = unwrapDefaultListableBeanFactory(registry); + if (beanFactory != null) { + if (!(beanFactory.getDependencyComparator() instanceof AnnotationAwareOrderComparator)) { + beanFactory.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); + } + if (!(beanFactory.getAutowireCandidateResolver() instanceof ContextAnnotationAutowireCandidateResolver)) { + beanFactory.setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver()); + } + } + + Set beanDefs = new LinkedHashSet<>(8); + + if (!registry.containsBeanDefinition(CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)) { + RootBeanDefinition def = new RootBeanDefinition(ConfigurationClassPostProcessor.class); + def.setSource(source); + beanDefs.add(registerPostProcessor(registry, def, CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)); + } + + if (!registry.containsBeanDefinition(AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)) { + RootBeanDefinition def = new RootBeanDefinition(AutowiredAnnotationBeanPostProcessor.class); + def.setSource(source); + beanDefs.add(registerPostProcessor(registry, def, AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)); + } + + // Check for JSR-250 support, and if present add the CommonAnnotationBeanPostProcessor. + if (jsr250Present && !registry.containsBeanDefinition(COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)) { + RootBeanDefinition def = new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class); + def.setSource(source); + beanDefs.add(registerPostProcessor(registry, def, COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)); + } + + // Check for JPA support, and if present add the PersistenceAnnotationBeanPostProcessor. + if (jpaPresent && !registry.containsBeanDefinition(PERSISTENCE_ANNOTATION_PROCESSOR_BEAN_NAME)) { + RootBeanDefinition def = new RootBeanDefinition(); + try { + def.setBeanClass(ClassUtils.forName(PERSISTENCE_ANNOTATION_PROCESSOR_CLASS_NAME, + AnnotationConfigUtils.class.getClassLoader())); + } + catch (ClassNotFoundException ex) { + throw new IllegalStateException( + "Cannot load optional framework class: " + PERSISTENCE_ANNOTATION_PROCESSOR_CLASS_NAME, ex); + } + def.setSource(source); + beanDefs.add(registerPostProcessor(registry, def, PERSISTENCE_ANNOTATION_PROCESSOR_BEAN_NAME)); + } + + if (!registry.containsBeanDefinition(EVENT_LISTENER_PROCESSOR_BEAN_NAME)) { + RootBeanDefinition def = new RootBeanDefinition(EventListenerMethodProcessor.class); + def.setSource(source); + beanDefs.add(registerPostProcessor(registry, def, EVENT_LISTENER_PROCESSOR_BEAN_NAME)); + } + + if (!registry.containsBeanDefinition(EVENT_LISTENER_FACTORY_BEAN_NAME)) { + RootBeanDefinition def = new RootBeanDefinition(DefaultEventListenerFactory.class); + def.setSource(source); + beanDefs.add(registerPostProcessor(registry, def, EVENT_LISTENER_FACTORY_BEAN_NAME)); + } + + return beanDefs; + } + + private static BeanDefinitionHolder registerPostProcessor( + BeanDefinitionRegistry registry, RootBeanDefinition definition, String beanName) { + + definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registry.registerBeanDefinition(beanName, definition); + return new BeanDefinitionHolder(definition, beanName); + } + + @Nullable + private static DefaultListableBeanFactory unwrapDefaultListableBeanFactory(BeanDefinitionRegistry registry) { + if (registry instanceof DefaultListableBeanFactory) { + return (DefaultListableBeanFactory) registry; + } + else if (registry instanceof GenericApplicationContext) { + return ((GenericApplicationContext) registry).getDefaultListableBeanFactory(); + } + else { + return null; + } + } + + public static void processCommonDefinitionAnnotations(AnnotatedBeanDefinition abd) { + processCommonDefinitionAnnotations(abd, abd.getMetadata()); + } + + static void processCommonDefinitionAnnotations(AnnotatedBeanDefinition abd, AnnotatedTypeMetadata metadata) { + AnnotationAttributes lazy = attributesFor(metadata, Lazy.class); + if (lazy != null) { + abd.setLazyInit(lazy.getBoolean("value")); + } + else if (abd.getMetadata() != metadata) { + lazy = attributesFor(abd.getMetadata(), Lazy.class); + if (lazy != null) { + abd.setLazyInit(lazy.getBoolean("value")); + } + } + + if (metadata.isAnnotated(Primary.class.getName())) { + abd.setPrimary(true); + } + AnnotationAttributes dependsOn = attributesFor(metadata, DependsOn.class); + if (dependsOn != null) { + abd.setDependsOn(dependsOn.getStringArray("value")); + } + + AnnotationAttributes role = attributesFor(metadata, Role.class); + if (role != null) { + abd.setRole(role.getNumber("value").intValue()); + } + AnnotationAttributes description = attributesFor(metadata, Description.class); + if (description != null) { + abd.setDescription(description.getString("value")); + } + } + + static BeanDefinitionHolder applyScopedProxyMode( + ScopeMetadata metadata, BeanDefinitionHolder definition, BeanDefinitionRegistry registry) { + + ScopedProxyMode scopedProxyMode = metadata.getScopedProxyMode(); + if (scopedProxyMode.equals(ScopedProxyMode.NO)) { + return definition; + } + boolean proxyTargetClass = scopedProxyMode.equals(ScopedProxyMode.TARGET_CLASS); + return ScopedProxyCreator.createScopedProxy(definition, registry, proxyTargetClass); + } + + @Nullable + static AnnotationAttributes attributesFor(AnnotatedTypeMetadata metadata, Class annotationClass) { + return attributesFor(metadata, annotationClass.getName()); + } + + @Nullable + static AnnotationAttributes attributesFor(AnnotatedTypeMetadata metadata, String annotationClassName) { + return AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(annotationClassName, false)); + } + + static Set attributesForRepeatable(AnnotationMetadata metadata, + Class containerClass, Class annotationClass) { + + return attributesForRepeatable(metadata, containerClass.getName(), annotationClass.getName()); + } + + @SuppressWarnings("unchecked") + static Set attributesForRepeatable( + AnnotationMetadata metadata, String containerClassName, String annotationClassName) { + + Set result = new LinkedHashSet<>(); + + // Direct annotation present? + addAttributesIfNotNull(result, metadata.getAnnotationAttributes(annotationClassName, false)); + + // Container annotation present? + Map container = metadata.getAnnotationAttributes(containerClassName, false); + if (container != null && container.containsKey("value")) { + for (Map containedAttributes : (Map[]) container.get("value")) { + addAttributesIfNotNull(result, containedAttributes); + } + } + + // Return merged result + return Collections.unmodifiableSet(result); + } + + private static void addAttributesIfNotNull( + Set result, @Nullable Map attributes) { + + if (attributes != null) { + result.add(AnnotationAttributes.fromMap(attributes)); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationScopeMetadataResolver.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationScopeMetadataResolver.java new file mode 100644 index 0000000..fec4fb3 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationScopeMetadataResolver.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.Annotation; + +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.util.Assert; + +/** + * A {@link ScopeMetadataResolver} implementation that by default checks for + * the presence of Spring's {@link Scope @Scope} annotation on the bean class. + * + *

    The exact type of annotation that is checked for is configurable via + * {@link #setScopeAnnotationType(Class)}. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @author Sam Brannen + * @since 2.5 + * @see org.springframework.context.annotation.Scope + */ +public class AnnotationScopeMetadataResolver implements ScopeMetadataResolver { + + private final ScopedProxyMode defaultProxyMode; + + protected Class scopeAnnotationType = Scope.class; + + + /** + * Construct a new {@code AnnotationScopeMetadataResolver}. + * @see #AnnotationScopeMetadataResolver(ScopedProxyMode) + * @see ScopedProxyMode#NO + */ + public AnnotationScopeMetadataResolver() { + this.defaultProxyMode = ScopedProxyMode.NO; + } + + /** + * Construct a new {@code AnnotationScopeMetadataResolver} using the + * supplied default {@link ScopedProxyMode}. + * @param defaultProxyMode the default scoped-proxy mode + */ + public AnnotationScopeMetadataResolver(ScopedProxyMode defaultProxyMode) { + Assert.notNull(defaultProxyMode, "'defaultProxyMode' must not be null"); + this.defaultProxyMode = defaultProxyMode; + } + + + /** + * Set the type of annotation that is checked for by this + * {@code AnnotationScopeMetadataResolver}. + * @param scopeAnnotationType the target annotation type + */ + public void setScopeAnnotationType(Class scopeAnnotationType) { + Assert.notNull(scopeAnnotationType, "'scopeAnnotationType' must not be null"); + this.scopeAnnotationType = scopeAnnotationType; + } + + + @Override + public ScopeMetadata resolveScopeMetadata(BeanDefinition definition) { + ScopeMetadata metadata = new ScopeMetadata(); + if (definition instanceof AnnotatedBeanDefinition) { + AnnotatedBeanDefinition annDef = (AnnotatedBeanDefinition) definition; + AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor( + annDef.getMetadata(), this.scopeAnnotationType); + if (attributes != null) { + metadata.setScopeName(attributes.getString("value")); + ScopedProxyMode proxyMode = attributes.getEnum("proxyMode"); + if (proxyMode == ScopedProxyMode.DEFAULT) { + proxyMode = this.defaultProxyMode; + } + metadata.setScopedProxyMode(proxyMode); + } + } + return metadata; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AspectJAutoProxyRegistrar.java b/spring-context/src/main/java/org/springframework/context/annotation/AspectJAutoProxyRegistrar.java new file mode 100644 index 0000000..2d18c06 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/AspectJAutoProxyRegistrar.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.springframework.aop.config.AopConfigUtils; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; + +/** + * Registers an {@link org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator + * AnnotationAwareAspectJAutoProxyCreator} against the current {@link BeanDefinitionRegistry} + * as appropriate based on a given @{@link EnableAspectJAutoProxy} annotation. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + * @see EnableAspectJAutoProxy + */ +class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar { + + /** + * Register, escalate, and configure the AspectJ auto proxy creator based on the value + * of the @{@link EnableAspectJAutoProxy#proxyTargetClass()} attribute on the importing + * {@code @Configuration} class. + */ + @Override + public void registerBeanDefinitions( + AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + + AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry); + + AnnotationAttributes enableAspectJAutoProxy = + AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class); + if (enableAspectJAutoProxy != null) { + if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) { + AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry); + } + if (enableAspectJAutoProxy.getBoolean("exposeProxy")) { + AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry); + } + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AutoProxyRegistrar.java b/spring-context/src/main/java/org/springframework/context/annotation/AutoProxyRegistrar.java new file mode 100644 index 0000000..2e5fa3d --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/AutoProxyRegistrar.java @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aop.config.AopConfigUtils; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; + +/** + * Registers an auto proxy creator against the current {@link BeanDefinitionRegistry} + * as appropriate based on an {@code @Enable*} annotation having {@code mode} and + * {@code proxyTargetClass} attributes set to the correct values. + * + * @author Chris Beams + * @since 3.1 + * @see org.springframework.cache.annotation.EnableCaching + * @see org.springframework.transaction.annotation.EnableTransactionManagement + */ +public class AutoProxyRegistrar implements ImportBeanDefinitionRegistrar { + + private final Log logger = LogFactory.getLog(getClass()); + + /** + * Register, escalate, and configure the standard auto proxy creator (APC) against the + * given registry. Works by finding the nearest annotation declared on the importing + * {@code @Configuration} class that has both {@code mode} and {@code proxyTargetClass} + * attributes. If {@code mode} is set to {@code PROXY}, the APC is registered; if + * {@code proxyTargetClass} is set to {@code true}, then the APC is forced to use + * subclass (CGLIB) proxying. + *

    Several {@code @Enable*} annotations expose both {@code mode} and + * {@code proxyTargetClass} attributes. It is important to note that most of these + * capabilities end up sharing a {@linkplain AopConfigUtils#AUTO_PROXY_CREATOR_BEAN_NAME + * single APC}. For this reason, this implementation doesn't "care" exactly which + * annotation it finds -- as long as it exposes the right {@code mode} and + * {@code proxyTargetClass} attributes, the APC can be registered and configured all + * the same. + */ + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + boolean candidateFound = false; + Set annTypes = importingClassMetadata.getAnnotationTypes(); + for (String annType : annTypes) { + AnnotationAttributes candidate = AnnotationConfigUtils.attributesFor(importingClassMetadata, annType); + if (candidate == null) { + continue; + } + Object mode = candidate.get("mode"); + Object proxyTargetClass = candidate.get("proxyTargetClass"); + if (mode != null && proxyTargetClass != null && AdviceMode.class == mode.getClass() && + Boolean.class == proxyTargetClass.getClass()) { + candidateFound = true; + if (mode == AdviceMode.PROXY) { + AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry); + if ((Boolean) proxyTargetClass) { + AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry); + return; + } + } + } + } + if (!candidateFound && logger.isInfoEnabled()) { + String name = getClass().getSimpleName(); + logger.info(String.format("%s was imported but no annotations were found " + + "having both 'mode' and 'proxyTargetClass' attributes of type " + + "AdviceMode and boolean respectively. This means that auto proxy " + + "creator registration and configuration may not have occurred as " + + "intended, and components may not be proxied as expected. Check to " + + "ensure that %s has been @Import'ed on the same class where these " + + "annotations are declared; otherwise remove the import of %s " + + "altogether.", name, name, name)); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Bean.java b/spring-context/src/main/java/org/springframework/context/annotation/Bean.java new file mode 100644 index 0000000..a723b02 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/Bean.java @@ -0,0 +1,304 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.beans.factory.annotation.Autowire; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.core.annotation.AliasFor; + +/** + * Indicates that a method produces a bean to be managed by the Spring container. + * + *

    Overview

    + * + *

    The names and semantics of the attributes to this annotation are intentionally + * similar to those of the {@code } element in the Spring XML schema. For + * example: + * + *

    + *     @Bean
    + *     public MyBean myBean() {
    + *         // instantiate and configure MyBean obj
    + *         return obj;
    + *     }
    + * 
    + * + *

    Bean Names

    + * + *

    While a {@link #name} attribute is available, the default strategy for + * determining the name of a bean is to use the name of the {@code @Bean} method. + * This is convenient and intuitive, but if explicit naming is desired, the + * {@code name} attribute (or its alias {@code value}) may be used. Also note + * that {@code name} accepts an array of Strings, allowing for multiple names + * (i.e. a primary bean name plus one or more aliases) for a single bean. + * + *

    + *     @Bean({"b1", "b2"}) // bean available as 'b1' and 'b2', but not 'myBean'
    + *     public MyBean myBean() {
    + *         // instantiate and configure MyBean obj
    + *         return obj;
    + *     }
    + * 
    + * + *

    Profile, Scope, Lazy, DependsOn, Primary, Order

    + * + *

    Note that the {@code @Bean} annotation does not provide attributes for profile, + * scope, lazy, depends-on or primary. Rather, it should be used in conjunction with + * {@link Scope @Scope}, {@link Lazy @Lazy}, {@link DependsOn @DependsOn} and + * {@link Primary @Primary} annotations to declare those semantics. For example: + * + *

    + *     @Bean
    + *     @Profile("production")
    + *     @Scope("prototype")
    + *     public MyBean myBean() {
    + *         // instantiate and configure MyBean obj
    + *         return obj;
    + *     }
    + * 
    + * + * The semantics of the above-mentioned annotations match their use at the component + * class level: {@code @Profile} allows for selective inclusion of certain beans. + * {@code @Scope} changes the bean's scope from singleton to the specified scope. + * {@code @Lazy} only has an actual effect in case of the default singleton scope. + * {@code @DependsOn} enforces the creation of specific other beans before this + * bean will be created, in addition to any dependencies that the bean expressed + * through direct references, which is typically helpful for singleton startup. + * {@code @Primary} is a mechanism to resolve ambiguity at the injection point level + * if a single target component needs to be injected but several beans match by type. + * + *

    Additionally, {@code @Bean} methods may also declare qualifier annotations + * and {@link org.springframework.core.annotation.Order @Order} values, to be + * taken into account during injection point resolution just like corresponding + * annotations on the corresponding component classes but potentially being very + * individual per bean definition (in case of multiple definitions with the same + * bean class). Qualifiers narrow the set of candidates after the initial type match; + * order values determine the order of resolved elements in case of collection + * injection points (with several target beans matching by type and qualifier). + * + *

    NOTE: {@code @Order} values may influence priorities at injection points, + * but please be aware that they do not influence singleton startup order which is an + * orthogonal concern determined by dependency relationships and {@code @DependsOn} + * declarations as mentioned above. Also, {@link javax.annotation.Priority} is not + * available at this level since it cannot be declared on methods; its semantics can + * be modeled through {@code @Order} values in combination with {@code @Primary} on + * a single bean per type. + * + *

    {@code @Bean} Methods in {@code @Configuration} Classes

    + * + *

    Typically, {@code @Bean} methods are declared within {@code @Configuration} + * classes. In this case, bean methods may reference other {@code @Bean} methods in the + * same class by calling them directly. This ensures that references between beans + * are strongly typed and navigable. Such so-called 'inter-bean references' are + * guaranteed to respect scoping and AOP semantics, just like {@code getBean()} lookups + * would. These are the semantics known from the original 'Spring JavaConfig' project + * which require CGLIB subclassing of each such configuration class at runtime. As a + * consequence, {@code @Configuration} classes and their factory methods must not be + * marked as final or private in this mode. For example: + * + *

    + * @Configuration
    + * public class AppConfig {
    + *
    + *     @Bean
    + *     public FooService fooService() {
    + *         return new FooService(fooRepository());
    + *     }
    + *
    + *     @Bean
    + *     public FooRepository fooRepository() {
    + *         return new JdbcFooRepository(dataSource());
    + *     }
    + *
    + *     // ...
    + * }
    + * + *

    {@code @Bean} Lite Mode

    + * + *

    {@code @Bean} methods may also be declared within classes that are not + * annotated with {@code @Configuration}. For example, bean methods may be declared + * in a {@code @Component} class or even in a plain old class. In such cases, + * a {@code @Bean} method will get processed in a so-called 'lite' mode. + * + *

    Bean methods in lite mode will be treated as plain factory + * methods by the container (similar to {@code factory-method} declarations + * in XML), with scoping and lifecycle callbacks properly applied. The containing + * class remains unmodified in this case, and there are no unusual constraints for + * the containing class or the factory methods. + * + *

    In contrast to the semantics for bean methods in {@code @Configuration} classes, + * 'inter-bean references' are not supported in lite mode. Instead, + * when one {@code @Bean}-method invokes another {@code @Bean}-method in lite + * mode, the invocation is a standard Java method invocation; Spring does not intercept + * the invocation via a CGLIB proxy. This is analogous to inter-{@code @Transactional} + * method calls where in proxy mode, Spring does not intercept the invocation — + * Spring does so only in AspectJ mode. + * + *

    For example: + * + *

    + * @Component
    + * public class Calculator {
    + *     public int sum(int a, int b) {
    + *         return a+b;
    + *     }
    + *
    + *     @Bean
    + *     public MyBean myBean() {
    + *         return new MyBean();
    + *     }
    + * }
    + * + *

    Bootstrapping

    + * + *

    See the @{@link Configuration} javadoc for further details including how to bootstrap + * the container using {@link AnnotationConfigApplicationContext} and friends. + * + *

    {@code BeanFactoryPostProcessor}-returning {@code @Bean} methods

    + * + *

    Special consideration must be taken for {@code @Bean} methods that return Spring + * {@link org.springframework.beans.factory.config.BeanFactoryPostProcessor BeanFactoryPostProcessor} + * ({@code BFPP}) types. Because {@code BFPP} objects must be instantiated very early in the + * container lifecycle, they can interfere with processing of annotations such as {@code @Autowired}, + * {@code @Value}, and {@code @PostConstruct} within {@code @Configuration} classes. To avoid these + * lifecycle issues, mark {@code BFPP}-returning {@code @Bean} methods as {@code static}. For example: + * + *

    + *     @Bean
    + *     public static PropertySourcesPlaceholderConfigurer pspc() {
    + *         // instantiate, configure and return pspc ...
    + *     }
    + * 
    + * + * By marking this method as {@code static}, it can be invoked without causing instantiation of its + * declaring {@code @Configuration} class, thus avoiding the above-mentioned lifecycle conflicts. + * Note however that {@code static} {@code @Bean} methods will not be enhanced for scoping and AOP + * semantics as mentioned above. This works out in {@code BFPP} cases, as they are not typically + * referenced by other {@code @Bean} methods. As a reminder, an INFO-level log message will be + * issued for any non-static {@code @Bean} methods having a return type assignable to + * {@code BeanFactoryPostProcessor}. + * + * @author Rod Johnson + * @author Costin Leau + * @author Chris Beams + * @author Juergen Hoeller + * @author Sam Brannen + * @since 3.0 + * @see Configuration + * @see Scope + * @see DependsOn + * @see Lazy + * @see Primary + * @see org.springframework.stereotype.Component + * @see org.springframework.beans.factory.annotation.Autowired + * @see org.springframework.beans.factory.annotation.Value + */ +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Bean { + + /** + * Alias for {@link #name}. + *

    Intended to be used when no other attributes are needed, for example: + * {@code @Bean("customBeanName")}. + * @since 4.3.3 + * @see #name + */ + @AliasFor("name") + String[] value() default {}; + + /** + * The name of this bean, or if several names, a primary bean name plus aliases. + *

    If left unspecified, the name of the bean is the name of the annotated method. + * If specified, the method name is ignored. + *

    The bean name and aliases may also be configured via the {@link #value} + * attribute if no other attributes are declared. + * @see #value + */ + @AliasFor("value") + String[] name() default {}; + + /** + * Are dependencies to be injected via convention-based autowiring by name or type? + *

    Note that this autowire mode is just about externally driven autowiring based + * on bean property setter methods by convention, analogous to XML bean definitions. + *

    The default mode does allow for annotation-driven autowiring. "no" refers to + * externally driven autowiring only, not affecting any autowiring demands that the + * bean class itself expresses through annotations. + * @see Autowire#BY_NAME + * @see Autowire#BY_TYPE + * @deprecated as of 5.1, since {@code @Bean} factory method argument resolution and + * {@code @Autowired} processing supersede name/type-based bean property injection + */ + @Deprecated + Autowire autowire() default Autowire.NO; + + /** + * Is this bean a candidate for getting autowired into some other bean? + *

    Default is {@code true}; set this to {@code false} for internal delegates + * that are not meant to get in the way of beans of the same type in other places. + * @since 5.1 + */ + boolean autowireCandidate() default true; + + /** + * The optional name of a method to call on the bean instance during initialization. + * Not commonly used, given that the method may be called programmatically directly + * within the body of a Bean-annotated method. + *

    The default value is {@code ""}, indicating no init method to be called. + * @see org.springframework.beans.factory.InitializingBean + * @see org.springframework.context.ConfigurableApplicationContext#refresh() + */ + String initMethod() default ""; + + /** + * The optional name of a method to call on the bean instance upon closing the + * application context, for example a {@code close()} method on a JDBC + * {@code DataSource} implementation, or a Hibernate {@code SessionFactory} object. + * The method must have no arguments but may throw any exception. + *

    As a convenience to the user, the container will attempt to infer a destroy + * method against an object returned from the {@code @Bean} method. For example, given + * an {@code @Bean} method returning an Apache Commons DBCP {@code BasicDataSource}, + * the container will notice the {@code close()} method available on that object and + * automatically register it as the {@code destroyMethod}. This 'destroy method + * inference' is currently limited to detecting only public, no-arg methods named + * 'close' or 'shutdown'. The method may be declared at any level of the inheritance + * hierarchy and will be detected regardless of the return type of the {@code @Bean} + * method (i.e., detection occurs reflectively against the bean instance itself at + * creation time). + *

    To disable destroy method inference for a particular {@code @Bean}, specify an + * empty string as the value, e.g. {@code @Bean(destroyMethod="")}. Note that the + * {@link org.springframework.beans.factory.DisposableBean} callback interface will + * nevertheless get detected and the corresponding destroy method invoked: In other + * words, {@code destroyMethod=""} only affects custom close/shutdown methods and + * {@link java.io.Closeable}/{@link java.lang.AutoCloseable} declared close methods. + *

    Note: Only invoked on beans whose lifecycle is under the full control of the + * factory, which is always the case for singletons but not guaranteed for any + * other scope. + * @see org.springframework.beans.factory.DisposableBean + * @see org.springframework.context.ConfigurableApplicationContext#close() + */ + String destroyMethod() default AbstractBeanDefinition.INFER_METHOD; + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/BeanAnnotationHelper.java b/spring-context/src/main/java/org/springframework/context/annotation/BeanAnnotationHelper.java new file mode 100644 index 0000000..3a6f5d5 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/BeanAnnotationHelper.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.reflect.Method; +import java.util.Map; + +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.util.ConcurrentReferenceHashMap; + +/** + * Utilities for processing {@link Bean}-annotated methods. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + */ +abstract class BeanAnnotationHelper { + + private static final Map beanNameCache = new ConcurrentReferenceHashMap<>(); + + private static final Map scopedProxyCache = new ConcurrentReferenceHashMap<>(); + + + public static boolean isBeanAnnotated(Method method) { + return AnnotatedElementUtils.hasAnnotation(method, Bean.class); + } + + public static String determineBeanNameFor(Method beanMethod) { + String beanName = beanNameCache.get(beanMethod); + if (beanName == null) { + // By default, the bean name is the name of the @Bean-annotated method + beanName = beanMethod.getName(); + // Check to see if the user has explicitly set a custom bean name... + AnnotationAttributes bean = + AnnotatedElementUtils.findMergedAnnotationAttributes(beanMethod, Bean.class, false, false); + if (bean != null) { + String[] names = bean.getStringArray("name"); + if (names.length > 0) { + beanName = names[0]; + } + } + beanNameCache.put(beanMethod, beanName); + } + return beanName; + } + + public static boolean isScopedProxy(Method beanMethod) { + Boolean scopedProxy = scopedProxyCache.get(beanMethod); + if (scopedProxy == null) { + AnnotationAttributes scope = + AnnotatedElementUtils.findMergedAnnotationAttributes(beanMethod, Scope.class, false, false); + scopedProxy = (scope != null && scope.getEnum("proxyMode") != ScopedProxyMode.NO); + scopedProxyCache.put(beanMethod, scopedProxy); + } + return scopedProxy; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/BeanMethod.java b/spring-context/src/main/java/org/springframework/context/annotation/BeanMethod.java new file mode 100644 index 0000000..850f08c --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/BeanMethod.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.springframework.beans.factory.parsing.Problem; +import org.springframework.beans.factory.parsing.ProblemReporter; +import org.springframework.core.type.MethodMetadata; + +/** + * Represents a {@link Configuration @Configuration} class method marked with the + * {@link Bean @Bean} annotation. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.0 + * @see ConfigurationClass + * @see ConfigurationClassParser + * @see ConfigurationClassBeanDefinitionReader + */ +final class BeanMethod extends ConfigurationMethod { + + public BeanMethod(MethodMetadata metadata, ConfigurationClass configurationClass) { + super(metadata, configurationClass); + } + + @Override + public void validate(ProblemReporter problemReporter) { + if (getMetadata().isStatic()) { + // static @Bean methods have no constraints to validate -> return immediately + return; + } + + if (this.configurationClass.getMetadata().isAnnotated(Configuration.class.getName())) { + if (!getMetadata().isOverridable()) { + // instance @Bean methods within @Configuration classes must be overridable to accommodate CGLIB + problemReporter.error(new NonOverridableMethodError()); + } + } + } + + + private class NonOverridableMethodError extends Problem { + + public NonOverridableMethodError() { + super(String.format("@Bean method '%s' must not be private or final; change the method's modifiers to continue", + getMetadata().getMethodName()), getResourceLocation()); + } + } +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java new file mode 100644 index 0000000..65cbb9b --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java @@ -0,0 +1,382 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionDefaults; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.core.env.Environment; +import org.springframework.core.env.EnvironmentCapable; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.ResourceLoader; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.PatternMatchUtils; + +/** + * A bean definition scanner that detects bean candidates on the classpath, + * registering corresponding bean definitions with a given registry ({@code BeanFactory} + * or {@code ApplicationContext}). + * + *

    Candidate classes are detected through configurable type filters. The + * default filters include classes that are annotated with Spring's + * {@link org.springframework.stereotype.Component @Component}, + * {@link org.springframework.stereotype.Repository @Repository}, + * {@link org.springframework.stereotype.Service @Service}, or + * {@link org.springframework.stereotype.Controller @Controller} stereotype. + * + *

    Also supports Java EE 6's {@link javax.annotation.ManagedBean} and + * JSR-330's {@link javax.inject.Named} annotations, if available. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @author Chris Beams + * @since 2.5 + * @see AnnotationConfigApplicationContext#scan + * @see org.springframework.stereotype.Component + * @see org.springframework.stereotype.Repository + * @see org.springframework.stereotype.Service + * @see org.springframework.stereotype.Controller + */ +public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateComponentProvider { + + private final BeanDefinitionRegistry registry; + + private BeanDefinitionDefaults beanDefinitionDefaults = new BeanDefinitionDefaults(); + + @Nullable + private String[] autowireCandidatePatterns; + + private BeanNameGenerator beanNameGenerator = AnnotationBeanNameGenerator.INSTANCE; + + private ScopeMetadataResolver scopeMetadataResolver = new AnnotationScopeMetadataResolver(); + + private boolean includeAnnotationConfig = true; + + + /** + * Create a new {@code ClassPathBeanDefinitionScanner} for the given bean factory. + * @param registry the {@code BeanFactory} to load bean definitions into, in the form + * of a {@code BeanDefinitionRegistry} + */ + public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry) { + this(registry, true); + } + + /** + * Create a new {@code ClassPathBeanDefinitionScanner} for the given bean factory. + *

    If the passed-in bean factory does not only implement the + * {@code BeanDefinitionRegistry} interface but also the {@code ResourceLoader} + * interface, it will be used as default {@code ResourceLoader} as well. This will + * usually be the case for {@link org.springframework.context.ApplicationContext} + * implementations. + *

    If given a plain {@code BeanDefinitionRegistry}, the default {@code ResourceLoader} + * will be a {@link org.springframework.core.io.support.PathMatchingResourcePatternResolver}. + *

    If the passed-in bean factory also implements {@link EnvironmentCapable} its + * environment will be used by this reader. Otherwise, the reader will initialize and + * use a {@link org.springframework.core.env.StandardEnvironment}. All + * {@code ApplicationContext} implementations are {@code EnvironmentCapable}, while + * normal {@code BeanFactory} implementations are not. + * @param registry the {@code BeanFactory} to load bean definitions into, in the form + * of a {@code BeanDefinitionRegistry} + * @param useDefaultFilters whether to include the default filters for the + * {@link org.springframework.stereotype.Component @Component}, + * {@link org.springframework.stereotype.Repository @Repository}, + * {@link org.springframework.stereotype.Service @Service}, and + * {@link org.springframework.stereotype.Controller @Controller} stereotype annotations + * @see #setResourceLoader + * @see #setEnvironment + */ + public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters) { + this(registry, useDefaultFilters, getOrCreateEnvironment(registry)); + } + + /** + * Create a new {@code ClassPathBeanDefinitionScanner} for the given bean factory and + * using the given {@link Environment} when evaluating bean definition profile metadata. + *

    If the passed-in bean factory does not only implement the {@code + * BeanDefinitionRegistry} interface but also the {@link ResourceLoader} interface, it + * will be used as default {@code ResourceLoader} as well. This will usually be the + * case for {@link org.springframework.context.ApplicationContext} implementations. + *

    If given a plain {@code BeanDefinitionRegistry}, the default {@code ResourceLoader} + * will be a {@link org.springframework.core.io.support.PathMatchingResourcePatternResolver}. + * @param registry the {@code BeanFactory} to load bean definitions into, in the form + * of a {@code BeanDefinitionRegistry} + * @param useDefaultFilters whether to include the default filters for the + * {@link org.springframework.stereotype.Component @Component}, + * {@link org.springframework.stereotype.Repository @Repository}, + * {@link org.springframework.stereotype.Service @Service}, and + * {@link org.springframework.stereotype.Controller @Controller} stereotype annotations + * @param environment the Spring {@link Environment} to use when evaluating bean + * definition profile metadata + * @since 3.1 + * @see #setResourceLoader + */ + public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters, + Environment environment) { + + this(registry, useDefaultFilters, environment, + (registry instanceof ResourceLoader ? (ResourceLoader) registry : null)); + } + + /** + * Create a new {@code ClassPathBeanDefinitionScanner} for the given bean factory and + * using the given {@link Environment} when evaluating bean definition profile metadata. + * @param registry the {@code BeanFactory} to load bean definitions into, in the form + * of a {@code BeanDefinitionRegistry} + * @param useDefaultFilters whether to include the default filters for the + * {@link org.springframework.stereotype.Component @Component}, + * {@link org.springframework.stereotype.Repository @Repository}, + * {@link org.springframework.stereotype.Service @Service}, and + * {@link org.springframework.stereotype.Controller @Controller} stereotype annotations + * @param environment the Spring {@link Environment} to use when evaluating bean + * definition profile metadata + * @param resourceLoader the {@link ResourceLoader} to use + * @since 4.3.6 + */ + public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters, + Environment environment, @Nullable ResourceLoader resourceLoader) { + + Assert.notNull(registry, "BeanDefinitionRegistry must not be null"); + this.registry = registry; + + if (useDefaultFilters) { + registerDefaultFilters(); + } + setEnvironment(environment); + setResourceLoader(resourceLoader); + } + + + /** + * Return the BeanDefinitionRegistry that this scanner operates on. + */ + @Override + public final BeanDefinitionRegistry getRegistry() { + return this.registry; + } + + /** + * Set the defaults to use for detected beans. + * @see BeanDefinitionDefaults + */ + public void setBeanDefinitionDefaults(@Nullable BeanDefinitionDefaults beanDefinitionDefaults) { + this.beanDefinitionDefaults = + (beanDefinitionDefaults != null ? beanDefinitionDefaults : new BeanDefinitionDefaults()); + } + + /** + * Return the defaults to use for detected beans (never {@code null}). + * @since 4.1 + */ + public BeanDefinitionDefaults getBeanDefinitionDefaults() { + return this.beanDefinitionDefaults; + } + + /** + * Set the name-matching patterns for determining autowire candidates. + * @param autowireCandidatePatterns the patterns to match against + */ + public void setAutowireCandidatePatterns(@Nullable String... autowireCandidatePatterns) { + this.autowireCandidatePatterns = autowireCandidatePatterns; + } + + /** + * Set the BeanNameGenerator to use for detected bean classes. + *

    Default is a {@link AnnotationBeanNameGenerator}. + */ + public void setBeanNameGenerator(@Nullable BeanNameGenerator beanNameGenerator) { + this.beanNameGenerator = + (beanNameGenerator != null ? beanNameGenerator : AnnotationBeanNameGenerator.INSTANCE); + } + + /** + * Set the ScopeMetadataResolver to use for detected bean classes. + * Note that this will override any custom "scopedProxyMode" setting. + *

    The default is an {@link AnnotationScopeMetadataResolver}. + * @see #setScopedProxyMode + */ + public void setScopeMetadataResolver(@Nullable ScopeMetadataResolver scopeMetadataResolver) { + this.scopeMetadataResolver = + (scopeMetadataResolver != null ? scopeMetadataResolver : new AnnotationScopeMetadataResolver()); + } + + /** + * Specify the proxy behavior for non-singleton scoped beans. + * Note that this will override any custom "scopeMetadataResolver" setting. + *

    The default is {@link ScopedProxyMode#NO}. + * @see #setScopeMetadataResolver + */ + public void setScopedProxyMode(ScopedProxyMode scopedProxyMode) { + this.scopeMetadataResolver = new AnnotationScopeMetadataResolver(scopedProxyMode); + } + + /** + * Specify whether to register annotation config post-processors. + *

    The default is to register the post-processors. Turn this off + * to be able to ignore the annotations or to process them differently. + */ + public void setIncludeAnnotationConfig(boolean includeAnnotationConfig) { + this.includeAnnotationConfig = includeAnnotationConfig; + } + + + /** + * Perform a scan within the specified base packages. + * @param basePackages the packages to check for annotated classes + * @return number of beans registered + */ + public int scan(String... basePackages) { + int beanCountAtScanStart = this.registry.getBeanDefinitionCount(); + + doScan(basePackages); + + // Register annotation config processors, if necessary. + if (this.includeAnnotationConfig) { + AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry); + } + + return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart); + } + + /** + * Perform a scan within the specified base packages, + * returning the registered bean definitions. + *

    This method does not register an annotation config processor + * but rather leaves this up to the caller. + * @param basePackages the packages to check for annotated classes + * @return set of beans registered if any for tooling registration purposes (never {@code null}) + */ + protected Set doScan(String... basePackages) { + Assert.notEmpty(basePackages, "At least one base package must be specified"); + Set beanDefinitions = new LinkedHashSet<>(); + for (String basePackage : basePackages) { + Set candidates = findCandidateComponents(basePackage); + for (BeanDefinition candidate : candidates) { + ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate); + candidate.setScope(scopeMetadata.getScopeName()); + String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry); + if (candidate instanceof AbstractBeanDefinition) { + postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName); + } + if (candidate instanceof AnnotatedBeanDefinition) { + AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate); + } + if (checkCandidate(beanName, candidate)) { + BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName); + definitionHolder = + AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry); + beanDefinitions.add(definitionHolder); + registerBeanDefinition(definitionHolder, this.registry); + } + } + } + return beanDefinitions; + } + + /** + * Apply further settings to the given bean definition, + * beyond the contents retrieved from scanning the component class. + * @param beanDefinition the scanned bean definition + * @param beanName the generated bean name for the given bean + */ + protected void postProcessBeanDefinition(AbstractBeanDefinition beanDefinition, String beanName) { + beanDefinition.applyDefaults(this.beanDefinitionDefaults); + if (this.autowireCandidatePatterns != null) { + beanDefinition.setAutowireCandidate(PatternMatchUtils.simpleMatch(this.autowireCandidatePatterns, beanName)); + } + } + + /** + * Register the specified bean with the given registry. + *

    Can be overridden in subclasses, e.g. to adapt the registration + * process or to register further bean definitions for each scanned bean. + * @param definitionHolder the bean definition plus bean name for the bean + * @param registry the BeanDefinitionRegistry to register the bean with + */ + protected void registerBeanDefinition(BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) { + BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry); + } + + + /** + * Check the given candidate's bean name, determining whether the corresponding + * bean definition needs to be registered or conflicts with an existing definition. + * @param beanName the suggested name for the bean + * @param beanDefinition the corresponding bean definition + * @return {@code true} if the bean can be registered as-is; + * {@code false} if it should be skipped because there is an + * existing, compatible bean definition for the specified name + * @throws ConflictingBeanDefinitionException if an existing, incompatible + * bean definition has been found for the specified name + */ + protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) throws IllegalStateException { + if (!this.registry.containsBeanDefinition(beanName)) { + return true; + } + BeanDefinition existingDef = this.registry.getBeanDefinition(beanName); + BeanDefinition originatingDef = existingDef.getOriginatingBeanDefinition(); + if (originatingDef != null) { + existingDef = originatingDef; + } + if (isCompatible(beanDefinition, existingDef)) { + return false; + } + throw new ConflictingBeanDefinitionException("Annotation-specified bean name '" + beanName + + "' for bean class [" + beanDefinition.getBeanClassName() + "] conflicts with existing, " + + "non-compatible bean definition of same name and class [" + existingDef.getBeanClassName() + "]"); + } + + /** + * Determine whether the given new bean definition is compatible with + * the given existing bean definition. + *

    The default implementation considers them as compatible when the existing + * bean definition comes from the same source or from a non-scanning source. + * @param newDefinition the new bean definition, originated from scanning + * @param existingDefinition the existing bean definition, potentially an + * explicitly defined one or a previously generated one from scanning + * @return whether the definitions are considered as compatible, with the + * new definition to be skipped in favor of the existing definition + */ + protected boolean isCompatible(BeanDefinition newDefinition, BeanDefinition existingDefinition) { + return (!(existingDefinition instanceof ScannedGenericBeanDefinition) || // explicitly registered overriding bean + (newDefinition.getSource() != null && newDefinition.getSource().equals(existingDefinition.getSource())) || // scanned same file twice + newDefinition.equals(existingDefinition)); // scanned equivalent class twice + } + + + /** + * Get the Environment from the given registry if possible, otherwise return a new + * StandardEnvironment. + */ + private static Environment getOrCreateEnvironment(BeanDefinitionRegistry registry) { + Assert.notNull(registry, "BeanDefinitionRegistry must not be null"); + if (registry instanceof EnvironmentCapable) { + return ((EnvironmentCapable) registry).getEnvironment(); + } + return new StandardEnvironment(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java new file mode 100644 index 0000000..05ea7a5 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java @@ -0,0 +1,542 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.annotation.Lookup; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.context.index.CandidateComponentsIndex; +import org.springframework.context.index.CandidateComponentsIndexLoader; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.env.Environment; +import org.springframework.core.env.EnvironmentCapable; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternUtils; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.classreading.CachingMetadataReaderFactory; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.core.type.filter.AssignableTypeFilter; +import org.springframework.core.type.filter.TypeFilter; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Controller; +import org.springframework.stereotype.Indexed; +import org.springframework.stereotype.Repository; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * A component provider that provides candidate components from a base package. Can + * use {@link CandidateComponentsIndex the index} if it is available of scans the + * classpath otherwise. Candidate components are identified by applying exclude and + * include filters. {@link AnnotationTypeFilter}, {@link AssignableTypeFilter} include + * filters on an annotation/superclass that are annotated with {@link Indexed} are + * supported: if any other include filter is specified, the index is ignored and + * classpath scanning is used instead. + * + *

    This implementation is based on Spring's + * {@link org.springframework.core.type.classreading.MetadataReader MetadataReader} + * facility, backed by an ASM {@link org.springframework.asm.ClassReader ClassReader}. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @author Ramnivas Laddad + * @author Chris Beams + * @author Stephane Nicoll + * @since 2.5 + * @see org.springframework.core.type.classreading.MetadataReaderFactory + * @see org.springframework.core.type.AnnotationMetadata + * @see ScannedGenericBeanDefinition + * @see CandidateComponentsIndex + */ +public class ClassPathScanningCandidateComponentProvider implements EnvironmentCapable, ResourceLoaderAware { + + static final String DEFAULT_RESOURCE_PATTERN = "**/*.class"; + + + protected final Log logger = LogFactory.getLog(getClass()); + + private String resourcePattern = DEFAULT_RESOURCE_PATTERN; + + private final List includeFilters = new ArrayList<>(); + + private final List excludeFilters = new ArrayList<>(); + + @Nullable + private Environment environment; + + @Nullable + private ConditionEvaluator conditionEvaluator; + + @Nullable + private ResourcePatternResolver resourcePatternResolver; + + @Nullable + private MetadataReaderFactory metadataReaderFactory; + + @Nullable + private CandidateComponentsIndex componentsIndex; + + + /** + * Protected constructor for flexible subclass initialization. + * @since 4.3.6 + */ + protected ClassPathScanningCandidateComponentProvider() { + } + + /** + * Create a ClassPathScanningCandidateComponentProvider with a {@link StandardEnvironment}. + * @param useDefaultFilters whether to register the default filters for the + * {@link Component @Component}, {@link Repository @Repository}, + * {@link Service @Service}, and {@link Controller @Controller} + * stereotype annotations + * @see #registerDefaultFilters() + */ + public ClassPathScanningCandidateComponentProvider(boolean useDefaultFilters) { + this(useDefaultFilters, new StandardEnvironment()); + } + + /** + * Create a ClassPathScanningCandidateComponentProvider with the given {@link Environment}. + * @param useDefaultFilters whether to register the default filters for the + * {@link Component @Component}, {@link Repository @Repository}, + * {@link Service @Service}, and {@link Controller @Controller} + * stereotype annotations + * @param environment the Environment to use + * @see #registerDefaultFilters() + */ + public ClassPathScanningCandidateComponentProvider(boolean useDefaultFilters, Environment environment) { + if (useDefaultFilters) { + registerDefaultFilters(); + } + setEnvironment(environment); + setResourceLoader(null); + } + + + /** + * Set the resource pattern to use when scanning the classpath. + * This value will be appended to each base package name. + * @see #findCandidateComponents(String) + * @see #DEFAULT_RESOURCE_PATTERN + */ + public void setResourcePattern(String resourcePattern) { + Assert.notNull(resourcePattern, "'resourcePattern' must not be null"); + this.resourcePattern = resourcePattern; + } + + /** + * Add an include type filter to the end of the inclusion list. + */ + public void addIncludeFilter(TypeFilter includeFilter) { + this.includeFilters.add(includeFilter); + } + + /** + * Add an exclude type filter to the front of the exclusion list. + */ + public void addExcludeFilter(TypeFilter excludeFilter) { + this.excludeFilters.add(0, excludeFilter); + } + + /** + * Reset the configured type filters. + * @param useDefaultFilters whether to re-register the default filters for + * the {@link Component @Component}, {@link Repository @Repository}, + * {@link Service @Service}, and {@link Controller @Controller} + * stereotype annotations + * @see #registerDefaultFilters() + */ + public void resetFilters(boolean useDefaultFilters) { + this.includeFilters.clear(); + this.excludeFilters.clear(); + if (useDefaultFilters) { + registerDefaultFilters(); + } + } + + /** + * Register the default filter for {@link Component @Component}. + *

    This will implicitly register all annotations that have the + * {@link Component @Component} meta-annotation including the + * {@link Repository @Repository}, {@link Service @Service}, and + * {@link Controller @Controller} stereotype annotations. + *

    Also supports Java EE 6's {@link javax.annotation.ManagedBean} and + * JSR-330's {@link javax.inject.Named} annotations, if available. + * + */ + @SuppressWarnings("unchecked") + protected void registerDefaultFilters() { + this.includeFilters.add(new AnnotationTypeFilter(Component.class)); + ClassLoader cl = ClassPathScanningCandidateComponentProvider.class.getClassLoader(); + try { + this.includeFilters.add(new AnnotationTypeFilter( + ((Class) ClassUtils.forName("javax.annotation.ManagedBean", cl)), false)); + logger.trace("JSR-250 'javax.annotation.ManagedBean' found and supported for component scanning"); + } + catch (ClassNotFoundException ex) { + // JSR-250 1.1 API (as included in Java EE 6) not available - simply skip. + } + try { + this.includeFilters.add(new AnnotationTypeFilter( + ((Class) ClassUtils.forName("javax.inject.Named", cl)), false)); + logger.trace("JSR-330 'javax.inject.Named' annotation found and supported for component scanning"); + } + catch (ClassNotFoundException ex) { + // JSR-330 API not available - simply skip. + } + } + + /** + * Set the Environment to use when resolving placeholders and evaluating + * {@link Conditional @Conditional}-annotated component classes. + *

    The default is a {@link StandardEnvironment}. + * @param environment the Environment to use + */ + public void setEnvironment(Environment environment) { + Assert.notNull(environment, "Environment must not be null"); + this.environment = environment; + this.conditionEvaluator = null; + } + + @Override + public final Environment getEnvironment() { + if (this.environment == null) { + this.environment = new StandardEnvironment(); + } + return this.environment; + } + + /** + * Return the {@link BeanDefinitionRegistry} used by this scanner, if any. + */ + @Nullable + protected BeanDefinitionRegistry getRegistry() { + return null; + } + + /** + * Set the {@link ResourceLoader} to use for resource locations. + * This will typically be a {@link ResourcePatternResolver} implementation. + *

    Default is a {@code PathMatchingResourcePatternResolver}, also capable of + * resource pattern resolving through the {@code ResourcePatternResolver} interface. + * @see org.springframework.core.io.support.ResourcePatternResolver + * @see org.springframework.core.io.support.PathMatchingResourcePatternResolver + */ + @Override + public void setResourceLoader(@Nullable ResourceLoader resourceLoader) { + this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader); + this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader); + this.componentsIndex = CandidateComponentsIndexLoader.loadIndex(this.resourcePatternResolver.getClassLoader()); + } + + /** + * Return the ResourceLoader that this component provider uses. + */ + public final ResourceLoader getResourceLoader() { + return getResourcePatternResolver(); + } + + private ResourcePatternResolver getResourcePatternResolver() { + if (this.resourcePatternResolver == null) { + this.resourcePatternResolver = new PathMatchingResourcePatternResolver(); + } + return this.resourcePatternResolver; + } + + /** + * Set the {@link MetadataReaderFactory} to use. + *

    Default is a {@link CachingMetadataReaderFactory} for the specified + * {@linkplain #setResourceLoader resource loader}. + *

    Call this setter method after {@link #setResourceLoader} in order + * for the given MetadataReaderFactory to override the default factory. + */ + public void setMetadataReaderFactory(MetadataReaderFactory metadataReaderFactory) { + this.metadataReaderFactory = metadataReaderFactory; + } + + /** + * Return the MetadataReaderFactory used by this component provider. + */ + public final MetadataReaderFactory getMetadataReaderFactory() { + if (this.metadataReaderFactory == null) { + this.metadataReaderFactory = new CachingMetadataReaderFactory(); + } + return this.metadataReaderFactory; + } + + + /** + * Scan the class path for candidate components. + * @param basePackage the package to check for annotated classes + * @return a corresponding Set of autodetected bean definitions + */ + public Set findCandidateComponents(String basePackage) { + if (this.componentsIndex != null && indexSupportsIncludeFilters()) { + return addCandidateComponentsFromIndex(this.componentsIndex, basePackage); + } + else { + return scanCandidateComponents(basePackage); + } + } + + /** + * Determine if the index can be used by this instance. + * @return {@code true} if the index is available and the configuration of this + * instance is supported by it, {@code false} otherwise + * @since 5.0 + */ + private boolean indexSupportsIncludeFilters() { + for (TypeFilter includeFilter : this.includeFilters) { + if (!indexSupportsIncludeFilter(includeFilter)) { + return false; + } + } + return true; + } + + /** + * Determine if the specified include {@link TypeFilter} is supported by the index. + * @param filter the filter to check + * @return whether the index supports this include filter + * @since 5.0 + * @see #extractStereotype(TypeFilter) + */ + private boolean indexSupportsIncludeFilter(TypeFilter filter) { + if (filter instanceof AnnotationTypeFilter) { + Class annotation = ((AnnotationTypeFilter) filter).getAnnotationType(); + return (AnnotationUtils.isAnnotationDeclaredLocally(Indexed.class, annotation) || + annotation.getName().startsWith("javax.")); + } + if (filter instanceof AssignableTypeFilter) { + Class target = ((AssignableTypeFilter) filter).getTargetType(); + return AnnotationUtils.isAnnotationDeclaredLocally(Indexed.class, target); + } + return false; + } + + /** + * Extract the stereotype to use for the specified compatible filter. + * @param filter the filter to handle + * @return the stereotype in the index matching this filter + * @since 5.0 + * @see #indexSupportsIncludeFilter(TypeFilter) + */ + @Nullable + private String extractStereotype(TypeFilter filter) { + if (filter instanceof AnnotationTypeFilter) { + return ((AnnotationTypeFilter) filter).getAnnotationType().getName(); + } + if (filter instanceof AssignableTypeFilter) { + return ((AssignableTypeFilter) filter).getTargetType().getName(); + } + return null; + } + + private Set addCandidateComponentsFromIndex(CandidateComponentsIndex index, String basePackage) { + Set candidates = new LinkedHashSet<>(); + try { + Set types = new HashSet<>(); + for (TypeFilter filter : this.includeFilters) { + String stereotype = extractStereotype(filter); + if (stereotype == null) { + throw new IllegalArgumentException("Failed to extract stereotype from " + filter); + } + types.addAll(index.getCandidateTypes(basePackage, stereotype)); + } + boolean traceEnabled = logger.isTraceEnabled(); + boolean debugEnabled = logger.isDebugEnabled(); + for (String type : types) { + MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(type); + if (isCandidateComponent(metadataReader)) { + ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader); + sbd.setSource(metadataReader.getResource()); + if (isCandidateComponent(sbd)) { + if (debugEnabled) { + logger.debug("Using candidate component class from index: " + type); + } + candidates.add(sbd); + } + else { + if (debugEnabled) { + logger.debug("Ignored because not a concrete top-level class: " + type); + } + } + } + else { + if (traceEnabled) { + logger.trace("Ignored because matching an exclude filter: " + type); + } + } + } + } + catch (IOException ex) { + throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex); + } + return candidates; + } + + private Set scanCandidateComponents(String basePackage) { + Set candidates = new LinkedHashSet<>(); + try { + String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + + resolveBasePackage(basePackage) + '/' + this.resourcePattern; + Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath); + boolean traceEnabled = logger.isTraceEnabled(); + boolean debugEnabled = logger.isDebugEnabled(); + for (Resource resource : resources) { + if (traceEnabled) { + logger.trace("Scanning " + resource); + } + if (resource.isReadable()) { + try { + MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource); + if (isCandidateComponent(metadataReader)) { + ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader); + sbd.setSource(resource); + if (isCandidateComponent(sbd)) { + if (debugEnabled) { + logger.debug("Identified candidate component class: " + resource); + } + candidates.add(sbd); + } + else { + if (debugEnabled) { + logger.debug("Ignored because not a concrete top-level class: " + resource); + } + } + } + else { + if (traceEnabled) { + logger.trace("Ignored because not matching any filter: " + resource); + } + } + } + catch (Throwable ex) { + throw new BeanDefinitionStoreException( + "Failed to read candidate component class: " + resource, ex); + } + } + else { + if (traceEnabled) { + logger.trace("Ignored because not readable: " + resource); + } + } + } + } + catch (IOException ex) { + throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex); + } + return candidates; + } + + + /** + * Resolve the specified base package into a pattern specification for + * the package search path. + *

    The default implementation resolves placeholders against system properties, + * and converts a "."-based package path to a "/"-based resource path. + * @param basePackage the base package as specified by the user + * @return the pattern specification to be used for package searching + */ + protected String resolveBasePackage(String basePackage) { + return ClassUtils.convertClassNameToResourcePath(getEnvironment().resolveRequiredPlaceholders(basePackage)); + } + + /** + * Determine whether the given class does not match any exclude filter + * and does match at least one include filter. + * @param metadataReader the ASM ClassReader for the class + * @return whether the class qualifies as a candidate component + */ + protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException { + for (TypeFilter tf : this.excludeFilters) { + if (tf.match(metadataReader, getMetadataReaderFactory())) { + return false; + } + } + for (TypeFilter tf : this.includeFilters) { + if (tf.match(metadataReader, getMetadataReaderFactory())) { + return isConditionMatch(metadataReader); + } + } + return false; + } + + /** + * Determine whether the given class is a candidate component based on any + * {@code @Conditional} annotations. + * @param metadataReader the ASM ClassReader for the class + * @return whether the class qualifies as a candidate component + */ + private boolean isConditionMatch(MetadataReader metadataReader) { + if (this.conditionEvaluator == null) { + this.conditionEvaluator = + new ConditionEvaluator(getRegistry(), this.environment, this.resourcePatternResolver); + } + return !this.conditionEvaluator.shouldSkip(metadataReader.getAnnotationMetadata()); + } + + /** + * Determine whether the given bean definition qualifies as candidate. + *

    The default implementation checks whether the class is not an interface + * and not dependent on an enclosing class. + *

    Can be overridden in subclasses. + * @param beanDefinition the bean definition to check + * @return whether the bean definition qualifies as a candidate component + */ + protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { + AnnotationMetadata metadata = beanDefinition.getMetadata(); + return (metadata.isIndependent() && (metadata.isConcrete() || + (metadata.isAbstract() && metadata.hasAnnotatedMethods(Lookup.class.getName())))); + } + + + /** + * Clear the local metadata cache, if any, removing all cached class metadata. + */ + public void clearCache() { + if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) { + // Clear cache in externally provided MetadataReaderFactory; this is a no-op + // for a shared cache since it'll be cleared by the ApplicationContext. + ((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache(); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java new file mode 100644 index 0000000..d9acb72 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java @@ -0,0 +1,820 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +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.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.annotation.Resource; +import javax.ejb.EJB; +import javax.xml.namespace.QName; +import javax.xml.ws.Service; +import javax.xml.ws.WebServiceClient; +import javax.xml.ws.WebServiceRef; + +import org.springframework.aop.TargetSource; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.PropertyValues; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor; +import org.springframework.beans.factory.annotation.InjectionMetadata; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.config.EmbeddedValueResolver; +import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.MethodParameter; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.jndi.support.SimpleJndiBeanFactory; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; +import org.springframework.util.StringValueResolver; + +/** + * {@link org.springframework.beans.factory.config.BeanPostProcessor} implementation + * that supports common Java annotations out of the box, in particular the JSR-250 + * annotations in the {@code javax.annotation} package. These common Java + * annotations are supported in many Java EE 5 technologies (e.g. JSF 1.2), + * as well as in Java 6's JAX-WS. + * + *

    This post-processor includes support for the {@link javax.annotation.PostConstruct} + * and {@link javax.annotation.PreDestroy} annotations - as init annotation + * and destroy annotation, respectively - through inheriting from + * {@link InitDestroyAnnotationBeanPostProcessor} with pre-configured annotation types. + * + *

    The central element is the {@link javax.annotation.Resource} annotation + * for annotation-driven injection of named beans, by default from the containing + * Spring BeanFactory, with only {@code mappedName} references resolved in JNDI. + * The {@link #setAlwaysUseJndiLookup "alwaysUseJndiLookup" flag} enforces JNDI lookups + * equivalent to standard Java EE 5 resource injection for {@code name} references + * and default names as well. The target beans can be simple POJOs, with no special + * requirements other than the type having to match. + * + *

    The JAX-WS {@link javax.xml.ws.WebServiceRef} annotation is supported too, + * analogous to {@link javax.annotation.Resource} but with the capability of creating + * specific JAX-WS service endpoints. This may either point to an explicitly defined + * resource by name or operate on a locally specified JAX-WS service class. Finally, + * this post-processor also supports the EJB 3 {@link javax.ejb.EJB} annotation, + * analogous to {@link javax.annotation.Resource} as well, with the capability to + * specify both a local bean name and a global JNDI name for fallback retrieval. + * The target beans can be plain POJOs as well as EJB 3 Session Beans in this case. + * + *

    The common annotations supported by this post-processor are available in + * Java 6 (JDK 1.6) as well as in Java EE 5/6 (which provides a standalone jar for + * its common annotations as well, allowing for use in any Java 5 based application). + * + *

    For default usage, resolving resource names as Spring bean names, + * simply define the following in your application context: + * + *

    + * <bean class="org.springframework.context.annotation.CommonAnnotationBeanPostProcessor"/>
    + * + * For direct JNDI access, resolving resource names as JNDI resource references + * within the Java EE application's "java:comp/env/" namespace, use the following: + * + *
    + * <bean class="org.springframework.context.annotation.CommonAnnotationBeanPostProcessor">
    + *   <property name="alwaysUseJndiLookup" value="true"/>
    + * </bean>
    + * + * {@code mappedName} references will always be resolved in JNDI, + * allowing for global JNDI names (including "java:" prefix) as well. The + * "alwaysUseJndiLookup" flag just affects {@code name} references and + * default names (inferred from the field name / property name). + * + *

    NOTE: A default CommonAnnotationBeanPostProcessor will be registered + * by the "context:annotation-config" and "context:component-scan" XML tags. + * Remove or turn off the default annotation configuration there if you intend + * to specify a custom CommonAnnotationBeanPostProcessor bean definition! + *

    NOTE: Annotation injection will be performed before XML injection; thus + * the latter configuration will override the former for properties wired through + * both approaches. + * + * @author Juergen Hoeller + * @author Sam Brannen + * @since 2.5 + * @see #setAlwaysUseJndiLookup + * @see #setResourceFactory + * @see org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor + * @see org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor + */ +@SuppressWarnings("serial") +public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBeanPostProcessor + implements InstantiationAwareBeanPostProcessor, BeanFactoryAware, Serializable { + + @Nullable + private static final Class webServiceRefClass; + + @Nullable + private static final Class ejbClass; + + private static final Set> resourceAnnotationTypes = new LinkedHashSet<>(4); + + static { + webServiceRefClass = loadAnnotationType("javax.xml.ws.WebServiceRef"); + ejbClass = loadAnnotationType("javax.ejb.EJB"); + + resourceAnnotationTypes.add(Resource.class); + if (webServiceRefClass != null) { + resourceAnnotationTypes.add(webServiceRefClass); + } + if (ejbClass != null) { + resourceAnnotationTypes.add(ejbClass); + } + } + + + private final Set ignoredResourceTypes = new HashSet<>(1); + + private boolean fallbackToDefaultTypeMatch = true; + + private boolean alwaysUseJndiLookup = false; + + private transient BeanFactory jndiFactory = new SimpleJndiBeanFactory(); + + @Nullable + private transient BeanFactory resourceFactory; + + @Nullable + private transient BeanFactory beanFactory; + + @Nullable + private transient StringValueResolver embeddedValueResolver; + + private final transient Map injectionMetadataCache = new ConcurrentHashMap<>(256); + + + /** + * Create a new CommonAnnotationBeanPostProcessor, + * with the init and destroy annotation types set to + * {@link javax.annotation.PostConstruct} and {@link javax.annotation.PreDestroy}, + * respectively. + */ + public CommonAnnotationBeanPostProcessor() { + setOrder(Ordered.LOWEST_PRECEDENCE - 3); + setInitAnnotationType(PostConstruct.class); + setDestroyAnnotationType(PreDestroy.class); + ignoreResourceType("javax.xml.ws.WebServiceContext"); + } + + + /** + * Ignore the given resource type when resolving {@code @Resource} + * annotations. + *

    By default, the {@code javax.xml.ws.WebServiceContext} interface + * will be ignored, since it will be resolved by the JAX-WS runtime. + * @param resourceType the resource type to ignore + */ + public void ignoreResourceType(String resourceType) { + Assert.notNull(resourceType, "Ignored resource type must not be null"); + this.ignoredResourceTypes.add(resourceType); + } + + /** + * Set whether to allow a fallback to a type match if no explicit name has been + * specified. The default name (i.e. the field name or bean property name) will + * still be checked first; if a bean of that name exists, it will be taken. + * However, if no bean of that name exists, a by-type resolution of the + * dependency will be attempted if this flag is "true". + *

    Default is "true". Switch this flag to "false" in order to enforce a + * by-name lookup in all cases, throwing an exception in case of no name match. + * @see org.springframework.beans.factory.config.AutowireCapableBeanFactory#resolveDependency + */ + public void setFallbackToDefaultTypeMatch(boolean fallbackToDefaultTypeMatch) { + this.fallbackToDefaultTypeMatch = fallbackToDefaultTypeMatch; + } + + /** + * Set whether to always use JNDI lookups equivalent to standard Java EE 5 resource + * injection, even for {@code name} attributes and default names. + *

    Default is "false": Resource names are used for Spring bean lookups in the + * containing BeanFactory; only {@code mappedName} attributes point directly + * into JNDI. Switch this flag to "true" for enforcing Java EE style JNDI lookups + * in any case, even for {@code name} attributes and default names. + * @see #setJndiFactory + * @see #setResourceFactory + */ + public void setAlwaysUseJndiLookup(boolean alwaysUseJndiLookup) { + this.alwaysUseJndiLookup = alwaysUseJndiLookup; + } + + /** + * Specify the factory for objects to be injected into {@code @Resource} / + * {@code @WebServiceRef} / {@code @EJB} annotated fields and setter methods, + * for {@code mappedName} attributes that point directly into JNDI. + * This factory will also be used if "alwaysUseJndiLookup" is set to "true" in order + * to enforce JNDI lookups even for {@code name} attributes and default names. + *

    The default is a {@link org.springframework.jndi.support.SimpleJndiBeanFactory} + * for JNDI lookup behavior equivalent to standard Java EE 5 resource injection. + * @see #setResourceFactory + * @see #setAlwaysUseJndiLookup + */ + public void setJndiFactory(BeanFactory jndiFactory) { + Assert.notNull(jndiFactory, "BeanFactory must not be null"); + this.jndiFactory = jndiFactory; + } + + /** + * Specify the factory for objects to be injected into {@code @Resource} / + * {@code @WebServiceRef} / {@code @EJB} annotated fields and setter methods, + * for {@code name} attributes and default names. + *

    The default is the BeanFactory that this post-processor is defined in, + * if any, looking up resource names as Spring bean names. Specify the resource + * factory explicitly for programmatic usage of this post-processor. + *

    Specifying Spring's {@link org.springframework.jndi.support.SimpleJndiBeanFactory} + * leads to JNDI lookup behavior equivalent to standard Java EE 5 resource injection, + * even for {@code name} attributes and default names. This is the same behavior + * that the "alwaysUseJndiLookup" flag enables. + * @see #setAlwaysUseJndiLookup + */ + public void setResourceFactory(BeanFactory resourceFactory) { + Assert.notNull(resourceFactory, "BeanFactory must not be null"); + this.resourceFactory = resourceFactory; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + Assert.notNull(beanFactory, "BeanFactory must not be null"); + this.beanFactory = beanFactory; + if (this.resourceFactory == null) { + this.resourceFactory = beanFactory; + } + if (beanFactory instanceof ConfigurableBeanFactory) { + this.embeddedValueResolver = new EmbeddedValueResolver((ConfigurableBeanFactory) beanFactory); + } + } + + + @Override + public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class beanType, String beanName) { + super.postProcessMergedBeanDefinition(beanDefinition, beanType, beanName); + InjectionMetadata metadata = findResourceMetadata(beanName, beanType, null); + metadata.checkConfigMembers(beanDefinition); + } + + @Override + public void resetBeanDefinition(String beanName) { + this.injectionMetadataCache.remove(beanName); + } + + @Override + public Object postProcessBeforeInstantiation(Class beanClass, String beanName) { + return null; + } + + @Override + public boolean postProcessAfterInstantiation(Object bean, String beanName) { + return true; + } + + @Override + public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) { + InjectionMetadata metadata = findResourceMetadata(beanName, bean.getClass(), pvs); + try { + metadata.inject(bean, beanName, pvs); + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, "Injection of resource dependencies failed", ex); + } + return pvs; + } + + @Deprecated + @Override + public PropertyValues postProcessPropertyValues( + PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) { + + return postProcessProperties(pvs, bean, beanName); + } + + + private InjectionMetadata findResourceMetadata(String beanName, final Class clazz, @Nullable PropertyValues pvs) { + // Fall back to class name as cache key, for backwards compatibility with custom callers. + String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName()); + // Quick check on the concurrent map first, with minimal locking. + InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey); + if (InjectionMetadata.needsRefresh(metadata, clazz)) { + synchronized (this.injectionMetadataCache) { + metadata = this.injectionMetadataCache.get(cacheKey); + if (InjectionMetadata.needsRefresh(metadata, clazz)) { + if (metadata != null) { + metadata.clear(pvs); + } + metadata = buildResourceMetadata(clazz); + this.injectionMetadataCache.put(cacheKey, metadata); + } + } + } + return metadata; + } + + private InjectionMetadata buildResourceMetadata(final Class clazz) { + if (!AnnotationUtils.isCandidateClass(clazz, resourceAnnotationTypes)) { + return InjectionMetadata.EMPTY; + } + + List elements = new ArrayList<>(); + Class targetClass = clazz; + + do { + final List currElements = new ArrayList<>(); + + ReflectionUtils.doWithLocalFields(targetClass, field -> { + if (webServiceRefClass != null && field.isAnnotationPresent(webServiceRefClass)) { + if (Modifier.isStatic(field.getModifiers())) { + throw new IllegalStateException("@WebServiceRef annotation is not supported on static fields"); + } + currElements.add(new WebServiceRefElement(field, field, null)); + } + else if (ejbClass != null && field.isAnnotationPresent(ejbClass)) { + if (Modifier.isStatic(field.getModifiers())) { + throw new IllegalStateException("@EJB annotation is not supported on static fields"); + } + currElements.add(new EjbRefElement(field, field, null)); + } + else if (field.isAnnotationPresent(Resource.class)) { + if (Modifier.isStatic(field.getModifiers())) { + throw new IllegalStateException("@Resource annotation is not supported on static fields"); + } + if (!this.ignoredResourceTypes.contains(field.getType().getName())) { + currElements.add(new ResourceElement(field, field, null)); + } + } + }); + + ReflectionUtils.doWithLocalMethods(targetClass, method -> { + Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method); + if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) { + return; + } + if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { + if (webServiceRefClass != null && bridgedMethod.isAnnotationPresent(webServiceRefClass)) { + if (Modifier.isStatic(method.getModifiers())) { + throw new IllegalStateException("@WebServiceRef annotation is not supported on static methods"); + } + if (method.getParameterCount() != 1) { + throw new IllegalStateException("@WebServiceRef annotation requires a single-arg method: " + method); + } + PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz); + currElements.add(new WebServiceRefElement(method, bridgedMethod, pd)); + } + else if (ejbClass != null && bridgedMethod.isAnnotationPresent(ejbClass)) { + if (Modifier.isStatic(method.getModifiers())) { + throw new IllegalStateException("@EJB annotation is not supported on static methods"); + } + if (method.getParameterCount() != 1) { + throw new IllegalStateException("@EJB annotation requires a single-arg method: " + method); + } + PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz); + currElements.add(new EjbRefElement(method, bridgedMethod, pd)); + } + else if (bridgedMethod.isAnnotationPresent(Resource.class)) { + if (Modifier.isStatic(method.getModifiers())) { + throw new IllegalStateException("@Resource annotation is not supported on static methods"); + } + Class[] paramTypes = method.getParameterTypes(); + if (paramTypes.length != 1) { + throw new IllegalStateException("@Resource annotation requires a single-arg method: " + method); + } + if (!this.ignoredResourceTypes.contains(paramTypes[0].getName())) { + PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz); + currElements.add(new ResourceElement(method, bridgedMethod, pd)); + } + } + } + }); + + elements.addAll(0, currElements); + targetClass = targetClass.getSuperclass(); + } + while (targetClass != null && targetClass != Object.class); + + return InjectionMetadata.forElements(elements, clazz); + } + + /** + * Obtain a lazily resolving resource proxy for the given name and type, + * delegating to {@link #getResource} on demand once a method call comes in. + * @param element the descriptor for the annotated field/method + * @param requestingBeanName the name of the requesting bean + * @return the resource object (never {@code null}) + * @since 4.2 + * @see #getResource + * @see Lazy + */ + protected Object buildLazyResourceProxy(final LookupElement element, final @Nullable String requestingBeanName) { + TargetSource ts = new TargetSource() { + @Override + public Class getTargetClass() { + return element.lookupType; + } + @Override + public boolean isStatic() { + return false; + } + @Override + public Object getTarget() { + return getResource(element, requestingBeanName); + } + @Override + public void releaseTarget(Object target) { + } + }; + ProxyFactory pf = new ProxyFactory(); + pf.setTargetSource(ts); + if (element.lookupType.isInterface()) { + pf.addInterface(element.lookupType); + } + ClassLoader classLoader = (this.beanFactory instanceof ConfigurableBeanFactory ? + ((ConfigurableBeanFactory) this.beanFactory).getBeanClassLoader() : null); + return pf.getProxy(classLoader); + } + + /** + * Obtain the resource object for the given name and type. + * @param element the descriptor for the annotated field/method + * @param requestingBeanName the name of the requesting bean + * @return the resource object (never {@code null}) + * @throws NoSuchBeanDefinitionException if no corresponding target resource found + */ + protected Object getResource(LookupElement element, @Nullable String requestingBeanName) + throws NoSuchBeanDefinitionException { + + if (StringUtils.hasLength(element.mappedName)) { + return this.jndiFactory.getBean(element.mappedName, element.lookupType); + } + if (this.alwaysUseJndiLookup) { + return this.jndiFactory.getBean(element.name, element.lookupType); + } + if (this.resourceFactory == null) { + throw new NoSuchBeanDefinitionException(element.lookupType, + "No resource factory configured - specify the 'resourceFactory' property"); + } + return autowireResource(this.resourceFactory, element, requestingBeanName); + } + + /** + * Obtain a resource object for the given name and type through autowiring + * based on the given factory. + * @param factory the factory to autowire against + * @param element the descriptor for the annotated field/method + * @param requestingBeanName the name of the requesting bean + * @return the resource object (never {@code null}) + * @throws NoSuchBeanDefinitionException if no corresponding target resource found + */ + protected Object autowireResource(BeanFactory factory, LookupElement element, @Nullable String requestingBeanName) + throws NoSuchBeanDefinitionException { + + Object resource; + Set autowiredBeanNames; + String name = element.name; + + if (factory instanceof AutowireCapableBeanFactory) { + AutowireCapableBeanFactory beanFactory = (AutowireCapableBeanFactory) factory; + DependencyDescriptor descriptor = element.getDependencyDescriptor(); + if (this.fallbackToDefaultTypeMatch && element.isDefaultName && !factory.containsBean(name)) { + autowiredBeanNames = new LinkedHashSet<>(); + resource = beanFactory.resolveDependency(descriptor, requestingBeanName, autowiredBeanNames, null); + if (resource == null) { + throw new NoSuchBeanDefinitionException(element.getLookupType(), "No resolvable resource object"); + } + } + else { + resource = beanFactory.resolveBeanByName(name, descriptor); + autowiredBeanNames = Collections.singleton(name); + } + } + else { + resource = factory.getBean(name, element.lookupType); + autowiredBeanNames = Collections.singleton(name); + } + + if (factory instanceof ConfigurableBeanFactory) { + ConfigurableBeanFactory beanFactory = (ConfigurableBeanFactory) factory; + for (String autowiredBeanName : autowiredBeanNames) { + if (requestingBeanName != null && beanFactory.containsBean(autowiredBeanName)) { + beanFactory.registerDependentBean(autowiredBeanName, requestingBeanName); + } + } + } + + return resource; + } + + + @SuppressWarnings("unchecked") + @Nullable + private static Class loadAnnotationType(String name) { + try { + return (Class) + ClassUtils.forName(name, CommonAnnotationBeanPostProcessor.class.getClassLoader()); + } + catch (ClassNotFoundException ex) { + return null; + } + } + + + /** + * Class representing generic injection information about an annotated field + * or setter method, supporting @Resource and related annotations. + */ + protected abstract static class LookupElement extends InjectionMetadata.InjectedElement { + + protected String name = ""; + + protected boolean isDefaultName = false; + + protected Class lookupType = Object.class; + + @Nullable + protected String mappedName; + + public LookupElement(Member member, @Nullable PropertyDescriptor pd) { + super(member, pd); + } + + /** + * Return the resource name for the lookup. + */ + public final String getName() { + return this.name; + } + + /** + * Return the desired type for the lookup. + */ + public final Class getLookupType() { + return this.lookupType; + } + + /** + * Build a DependencyDescriptor for the underlying field/method. + */ + public final DependencyDescriptor getDependencyDescriptor() { + if (this.isField) { + return new LookupDependencyDescriptor((Field) this.member, this.lookupType); + } + else { + return new LookupDependencyDescriptor((Method) this.member, this.lookupType); + } + } + } + + + /** + * Class representing injection information about an annotated field + * or setter method, supporting the @Resource annotation. + */ + private class ResourceElement extends LookupElement { + + private final boolean lazyLookup; + + public ResourceElement(Member member, AnnotatedElement ae, @Nullable PropertyDescriptor pd) { + super(member, pd); + Resource resource = ae.getAnnotation(Resource.class); + String resourceName = resource.name(); + Class resourceType = resource.type(); + this.isDefaultName = !StringUtils.hasLength(resourceName); + if (this.isDefaultName) { + resourceName = this.member.getName(); + if (this.member instanceof Method && resourceName.startsWith("set") && resourceName.length() > 3) { + resourceName = Introspector.decapitalize(resourceName.substring(3)); + } + } + else if (embeddedValueResolver != null) { + resourceName = embeddedValueResolver.resolveStringValue(resourceName); + } + if (Object.class != resourceType) { + checkResourceType(resourceType); + } + else { + // No resource type specified... check field/method. + resourceType = getResourceType(); + } + this.name = (resourceName != null ? resourceName : ""); + this.lookupType = resourceType; + String lookupValue = resource.lookup(); + this.mappedName = (StringUtils.hasLength(lookupValue) ? lookupValue : resource.mappedName()); + Lazy lazy = ae.getAnnotation(Lazy.class); + this.lazyLookup = (lazy != null && lazy.value()); + } + + @Override + protected Object getResourceToInject(Object target, @Nullable String requestingBeanName) { + return (this.lazyLookup ? buildLazyResourceProxy(this, requestingBeanName) : + getResource(this, requestingBeanName)); + } + } + + + /** + * Class representing injection information about an annotated field + * or setter method, supporting the @WebServiceRef annotation. + */ + private class WebServiceRefElement extends LookupElement { + + private final Class elementType; + + private final String wsdlLocation; + + public WebServiceRefElement(Member member, AnnotatedElement ae, @Nullable PropertyDescriptor pd) { + super(member, pd); + WebServiceRef resource = ae.getAnnotation(WebServiceRef.class); + String resourceName = resource.name(); + Class resourceType = resource.type(); + this.isDefaultName = !StringUtils.hasLength(resourceName); + if (this.isDefaultName) { + resourceName = this.member.getName(); + if (this.member instanceof Method && resourceName.startsWith("set") && resourceName.length() > 3) { + resourceName = Introspector.decapitalize(resourceName.substring(3)); + } + } + if (Object.class != resourceType) { + checkResourceType(resourceType); + } + else { + // No resource type specified... check field/method. + resourceType = getResourceType(); + } + this.name = resourceName; + this.elementType = resourceType; + if (Service.class.isAssignableFrom(resourceType)) { + this.lookupType = resourceType; + } + else { + this.lookupType = resource.value(); + } + this.mappedName = resource.mappedName(); + this.wsdlLocation = resource.wsdlLocation(); + } + + @Override + protected Object getResourceToInject(Object target, @Nullable String requestingBeanName) { + Service service; + try { + service = (Service) getResource(this, requestingBeanName); + } + catch (NoSuchBeanDefinitionException notFound) { + // Service to be created through generated class. + if (Service.class == this.lookupType) { + throw new IllegalStateException("No resource with name '" + this.name + "' found in context, " + + "and no specific JAX-WS Service subclass specified. The typical solution is to either specify " + + "a LocalJaxWsServiceFactoryBean with the given name or to specify the (generated) Service " + + "subclass as @WebServiceRef(...) value."); + } + if (StringUtils.hasLength(this.wsdlLocation)) { + try { + Constructor ctor = this.lookupType.getConstructor(URL.class, QName.class); + WebServiceClient clientAnn = this.lookupType.getAnnotation(WebServiceClient.class); + if (clientAnn == null) { + throw new IllegalStateException("JAX-WS Service class [" + this.lookupType.getName() + + "] does not carry a WebServiceClient annotation"); + } + service = (Service) BeanUtils.instantiateClass(ctor, + new URL(this.wsdlLocation), new QName(clientAnn.targetNamespace(), clientAnn.name())); + } + catch (NoSuchMethodException ex) { + throw new IllegalStateException("JAX-WS Service class [" + this.lookupType.getName() + + "] does not have a (URL, QName) constructor. Cannot apply specified WSDL location [" + + this.wsdlLocation + "]."); + } + catch (MalformedURLException ex) { + throw new IllegalArgumentException( + "Specified WSDL location [" + this.wsdlLocation + "] isn't a valid URL"); + } + } + else { + service = (Service) BeanUtils.instantiateClass(this.lookupType); + } + } + return service.getPort(this.elementType); + } + } + + + /** + * Class representing injection information about an annotated field + * or setter method, supporting the @EJB annotation. + */ + private class EjbRefElement extends LookupElement { + + private final String beanName; + + public EjbRefElement(Member member, AnnotatedElement ae, @Nullable PropertyDescriptor pd) { + super(member, pd); + EJB resource = ae.getAnnotation(EJB.class); + String resourceBeanName = resource.beanName(); + String resourceName = resource.name(); + this.isDefaultName = !StringUtils.hasLength(resourceName); + if (this.isDefaultName) { + resourceName = this.member.getName(); + if (this.member instanceof Method && resourceName.startsWith("set") && resourceName.length() > 3) { + resourceName = Introspector.decapitalize(resourceName.substring(3)); + } + } + Class resourceType = resource.beanInterface(); + if (Object.class != resourceType) { + checkResourceType(resourceType); + } + else { + // No resource type specified... check field/method. + resourceType = getResourceType(); + } + this.beanName = resourceBeanName; + this.name = resourceName; + this.lookupType = resourceType; + this.mappedName = resource.mappedName(); + } + + @Override + protected Object getResourceToInject(Object target, @Nullable String requestingBeanName) { + if (StringUtils.hasLength(this.beanName)) { + if (beanFactory != null && beanFactory.containsBean(this.beanName)) { + // Local match found for explicitly specified local bean name. + Object bean = beanFactory.getBean(this.beanName, this.lookupType); + if (requestingBeanName != null && beanFactory instanceof ConfigurableBeanFactory) { + ((ConfigurableBeanFactory) beanFactory).registerDependentBean(this.beanName, requestingBeanName); + } + return bean; + } + else if (this.isDefaultName && !StringUtils.hasLength(this.mappedName)) { + throw new NoSuchBeanDefinitionException(this.beanName, + "Cannot resolve 'beanName' in local BeanFactory. Consider specifying a general 'name' value instead."); + } + } + // JNDI name lookup - may still go to a local BeanFactory. + return getResource(this, requestingBeanName); + } + } + + + /** + * Extension of the DependencyDescriptor class, + * overriding the dependency type with the specified resource type. + */ + private static class LookupDependencyDescriptor extends DependencyDescriptor { + + private final Class lookupType; + + public LookupDependencyDescriptor(Field field, Class lookupType) { + super(field, true); + this.lookupType = lookupType; + } + + public LookupDependencyDescriptor(Method method, Class lookupType) { + super(new MethodParameter(method, 0), true); + this.lookupType = lookupType; + } + + @Override + public Class getDependencyType() { + return this.lookupType; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScan.java b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScan.java new file mode 100644 index 0000000..ffa1f1a --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScan.java @@ -0,0 +1,226 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.core.annotation.AliasFor; +import org.springframework.core.type.filter.TypeFilter; + +/** + * Configures component scanning directives for use with @{@link Configuration} classes. + * Provides support parallel with Spring XML's {@code } element. + * + *

    Either {@link #basePackageClasses} or {@link #basePackages} (or its alias + * {@link #value}) may be specified to define specific packages to scan. If specific + * packages are not defined, scanning will occur from the package of the + * class that declares this annotation. + * + *

    Note that the {@code } element has an + * {@code annotation-config} attribute; however, this annotation does not. This is because + * in almost all cases when using {@code @ComponentScan}, default annotation config + * processing (e.g. processing {@code @Autowired} and friends) is assumed. Furthermore, + * when using {@link AnnotationConfigApplicationContext}, annotation config processors are + * always registered, meaning that any attempt to disable them at the + * {@code @ComponentScan} level would be ignored. + * + *

    See {@link Configuration @Configuration}'s Javadoc for usage examples. + * + * @author Chris Beams + * @author Juergen Hoeller + * @author Sam Brannen + * @since 3.1 + * @see Configuration + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Documented +@Repeatable(ComponentScans.class) +public @interface ComponentScan { + + /** + * Alias for {@link #basePackages}. + *

    Allows for more concise annotation declarations if no other attributes + * are needed — for example, {@code @ComponentScan("org.my.pkg")} + * instead of {@code @ComponentScan(basePackages = "org.my.pkg")}. + */ + @AliasFor("basePackages") + String[] value() default {}; + + /** + * Base packages to scan for annotated components. + *

    {@link #value} is an alias for (and mutually exclusive with) this + * attribute. + *

    Use {@link #basePackageClasses} for a type-safe alternative to + * String-based package names. + */ + @AliasFor("value") + String[] basePackages() default {}; + + /** + * Type-safe alternative to {@link #basePackages} for specifying the packages + * to scan for annotated components. The package of each class specified will be scanned. + *

    Consider creating a special no-op marker class or interface in each package + * that serves no purpose other than being referenced by this attribute. + */ + Class[] basePackageClasses() default {}; + + /** + * The {@link BeanNameGenerator} class to be used for naming detected components + * within the Spring container. + *

    The default value of the {@link BeanNameGenerator} interface itself indicates + * that the scanner used to process this {@code @ComponentScan} annotation should + * use its inherited bean name generator, e.g. the default + * {@link AnnotationBeanNameGenerator} or any custom instance supplied to the + * application context at bootstrap time. + * @see AnnotationConfigApplicationContext#setBeanNameGenerator(BeanNameGenerator) + * @see AnnotationBeanNameGenerator + * @see FullyQualifiedAnnotationBeanNameGenerator + */ + Class nameGenerator() default BeanNameGenerator.class; + + /** + * The {@link ScopeMetadataResolver} to be used for resolving the scope of detected components. + */ + Class scopeResolver() default AnnotationScopeMetadataResolver.class; + + /** + * Indicates whether proxies should be generated for detected components, which may be + * necessary when using scopes in a proxy-style fashion. + *

    The default is defer to the default behavior of the component scanner used to + * execute the actual scan. + *

    Note that setting this attribute overrides any value set for {@link #scopeResolver}. + * @see ClassPathBeanDefinitionScanner#setScopedProxyMode(ScopedProxyMode) + */ + ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT; + + /** + * Controls the class files eligible for component detection. + *

    Consider use of {@link #includeFilters} and {@link #excludeFilters} + * for a more flexible approach. + */ + String resourcePattern() default ClassPathScanningCandidateComponentProvider.DEFAULT_RESOURCE_PATTERN; + + /** + * Indicates whether automatic detection of classes annotated with {@code @Component} + * {@code @Repository}, {@code @Service}, or {@code @Controller} should be enabled. + */ + boolean useDefaultFilters() default true; + + /** + * Specifies which types are eligible for component scanning. + *

    Further narrows the set of candidate components from everything in {@link #basePackages} + * to everything in the base packages that matches the given filter or filters. + *

    Note that these filters will be applied in addition to the default filters, if specified. + * Any type under the specified base packages which matches a given filter will be included, + * even if it does not match the default filters (i.e. is not annotated with {@code @Component}). + * @see #resourcePattern() + * @see #useDefaultFilters() + */ + Filter[] includeFilters() default {}; + + /** + * Specifies which types are not eligible for component scanning. + * @see #resourcePattern + */ + Filter[] excludeFilters() default {}; + + /** + * Specify whether scanned beans should be registered for lazy initialization. + *

    Default is {@code false}; switch this to {@code true} when desired. + * @since 4.1 + */ + boolean lazyInit() default false; + + + /** + * Declares the type filter to be used as an {@linkplain ComponentScan#includeFilters + * include filter} or {@linkplain ComponentScan#excludeFilters exclude filter}. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target({}) + @interface Filter { + + /** + * The type of filter to use. + *

    Default is {@link FilterType#ANNOTATION}. + * @see #classes + * @see #pattern + */ + FilterType type() default FilterType.ANNOTATION; + + /** + * Alias for {@link #classes}. + * @see #classes + */ + @AliasFor("classes") + Class[] value() default {}; + + /** + * The class or classes to use as the filter. + *

    The following table explains how the classes will be interpreted + * based on the configured value of the {@link #type} attribute. + * + * + * + * + * + * + * + * + *
    {@code FilterType}Class Interpreted As
    {@link FilterType#ANNOTATION ANNOTATION}the annotation itself
    {@link FilterType#ASSIGNABLE_TYPE ASSIGNABLE_TYPE}the type that detected components should be assignable to
    {@link FilterType#CUSTOM CUSTOM}an implementation of {@link TypeFilter}
    + *

    When multiple classes are specified, OR logic is applied + * — for example, "include types annotated with {@code @Foo} OR {@code @Bar}". + *

    Custom {@link TypeFilter TypeFilters} may optionally implement any of the + * following {@link org.springframework.beans.factory.Aware Aware} interfaces, and + * their respective methods will be called prior to {@link TypeFilter#match match}: + *

      + *
    • {@link org.springframework.context.EnvironmentAware EnvironmentAware}
    • + *
    • {@link org.springframework.beans.factory.BeanFactoryAware BeanFactoryAware} + *
    • {@link org.springframework.beans.factory.BeanClassLoaderAware BeanClassLoaderAware} + *
    • {@link org.springframework.context.ResourceLoaderAware ResourceLoaderAware} + *
    + *

    Specifying zero classes is permitted but will have no effect on component + * scanning. + * @since 4.2 + * @see #value + * @see #type + */ + @AliasFor("value") + Class[] classes() default {}; + + /** + * The pattern (or patterns) to use for the filter, as an alternative + * to specifying a Class {@link #value}. + *

    If {@link #type} is set to {@link FilterType#ASPECTJ ASPECTJ}, + * this is an AspectJ type pattern expression. If {@link #type} is + * set to {@link FilterType#REGEX REGEX}, this is a regex pattern + * for the fully-qualified class names to match. + * @see #type + * @see #classes + */ + String[] pattern() default {}; + + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java new file mode 100644 index 0000000..9057586 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java @@ -0,0 +1,180 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.filter.AbstractTypeHierarchyTraversingFilter; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.core.type.filter.AspectJTypeFilter; +import org.springframework.core.type.filter.AssignableTypeFilter; +import org.springframework.core.type.filter.RegexPatternTypeFilter; +import org.springframework.core.type.filter.TypeFilter; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * Parser for the @{@link ComponentScan} annotation. + * + * @author Chris Beams + * @author Juergen Hoeller + * @author Sam Brannen + * @since 3.1 + * @see ClassPathBeanDefinitionScanner#scan(String...) + * @see ComponentScanBeanDefinitionParser + */ +class ComponentScanAnnotationParser { + + private final Environment environment; + + private final ResourceLoader resourceLoader; + + private final BeanNameGenerator beanNameGenerator; + + private final BeanDefinitionRegistry registry; + + + public ComponentScanAnnotationParser(Environment environment, ResourceLoader resourceLoader, + BeanNameGenerator beanNameGenerator, BeanDefinitionRegistry registry) { + + this.environment = environment; + this.resourceLoader = resourceLoader; + this.beanNameGenerator = beanNameGenerator; + this.registry = registry; + } + + + public Set parse(AnnotationAttributes componentScan, final String declaringClass) { + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry, + componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader); + + Class generatorClass = componentScan.getClass("nameGenerator"); + boolean useInheritedGenerator = (BeanNameGenerator.class == generatorClass); + scanner.setBeanNameGenerator(useInheritedGenerator ? this.beanNameGenerator : + BeanUtils.instantiateClass(generatorClass)); + + ScopedProxyMode scopedProxyMode = componentScan.getEnum("scopedProxy"); + if (scopedProxyMode != ScopedProxyMode.DEFAULT) { + scanner.setScopedProxyMode(scopedProxyMode); + } + else { + Class resolverClass = componentScan.getClass("scopeResolver"); + scanner.setScopeMetadataResolver(BeanUtils.instantiateClass(resolverClass)); + } + + scanner.setResourcePattern(componentScan.getString("resourcePattern")); + + for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) { + for (TypeFilter typeFilter : typeFiltersFor(filter)) { + scanner.addIncludeFilter(typeFilter); + } + } + for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) { + for (TypeFilter typeFilter : typeFiltersFor(filter)) { + scanner.addExcludeFilter(typeFilter); + } + } + + boolean lazyInit = componentScan.getBoolean("lazyInit"); + if (lazyInit) { + scanner.getBeanDefinitionDefaults().setLazyInit(true); + } + + Set basePackages = new LinkedHashSet<>(); + String[] basePackagesArray = componentScan.getStringArray("basePackages"); + for (String pkg : basePackagesArray) { + String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg), + ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); + Collections.addAll(basePackages, tokenized); + } + for (Class clazz : componentScan.getClassArray("basePackageClasses")) { + basePackages.add(ClassUtils.getPackageName(clazz)); + } + if (basePackages.isEmpty()) { + basePackages.add(ClassUtils.getPackageName(declaringClass)); + } + + scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) { + @Override + protected boolean matchClassName(String className) { + return declaringClass.equals(className); + } + }); + return scanner.doScan(StringUtils.toStringArray(basePackages)); + } + + private List typeFiltersFor(AnnotationAttributes filterAttributes) { + List typeFilters = new ArrayList<>(); + FilterType filterType = filterAttributes.getEnum("type"); + + for (Class filterClass : filterAttributes.getClassArray("classes")) { + switch (filterType) { + case ANNOTATION: + Assert.isAssignable(Annotation.class, filterClass, + "@ComponentScan ANNOTATION type filter requires an annotation type"); + @SuppressWarnings("unchecked") + Class annotationType = (Class) filterClass; + typeFilters.add(new AnnotationTypeFilter(annotationType)); + break; + case ASSIGNABLE_TYPE: + typeFilters.add(new AssignableTypeFilter(filterClass)); + break; + case CUSTOM: + Assert.isAssignable(TypeFilter.class, filterClass, + "@ComponentScan CUSTOM type filter requires a TypeFilter implementation"); + + TypeFilter filter = ParserStrategyUtils.instantiateClass(filterClass, TypeFilter.class, + this.environment, this.resourceLoader, this.registry); + typeFilters.add(filter); + break; + default: + throw new IllegalArgumentException("Filter type not supported with Class value: " + filterType); + } + } + + for (String expression : filterAttributes.getStringArray("pattern")) { + switch (filterType) { + case ASPECTJ: + typeFilters.add(new AspectJTypeFilter(expression, this.resourceLoader.getClassLoader())); + break; + case REGEX: + typeFilters.add(new RegexPatternTypeFilter(Pattern.compile(expression))); + break; + default: + throw new IllegalArgumentException("Filter type not supported with String pattern: " + filterType); + } + } + + return typeFilters; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanBeanDefinitionParser.java new file mode 100644 index 0000000..535717e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanBeanDefinitionParser.java @@ -0,0 +1,285 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.Annotation; +import java.util.Set; +import java.util.regex.Pattern; + +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.parsing.CompositeComponentDefinition; +import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.beans.factory.xml.XmlReaderContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.core.type.filter.AspectJTypeFilter; +import org.springframework.core.type.filter.AssignableTypeFilter; +import org.springframework.core.type.filter.RegexPatternTypeFilter; +import org.springframework.core.type.filter.TypeFilter; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * Parser for the {@code } element. + * + * @author Mark Fisher + * @author Ramnivas Laddad + * @author Juergen Hoeller + * @since 2.5 + */ +public class ComponentScanBeanDefinitionParser implements BeanDefinitionParser { + + private static final String BASE_PACKAGE_ATTRIBUTE = "base-package"; + + private static final String RESOURCE_PATTERN_ATTRIBUTE = "resource-pattern"; + + private static final String USE_DEFAULT_FILTERS_ATTRIBUTE = "use-default-filters"; + + private static final String ANNOTATION_CONFIG_ATTRIBUTE = "annotation-config"; + + private static final String NAME_GENERATOR_ATTRIBUTE = "name-generator"; + + private static final String SCOPE_RESOLVER_ATTRIBUTE = "scope-resolver"; + + private static final String SCOPED_PROXY_ATTRIBUTE = "scoped-proxy"; + + private static final String EXCLUDE_FILTER_ELEMENT = "exclude-filter"; + + private static final String INCLUDE_FILTER_ELEMENT = "include-filter"; + + private static final String FILTER_TYPE_ATTRIBUTE = "type"; + + private static final String FILTER_EXPRESSION_ATTRIBUTE = "expression"; + + + @Override + @Nullable + public BeanDefinition parse(Element element, ParserContext parserContext) { + String basePackage = element.getAttribute(BASE_PACKAGE_ATTRIBUTE); + basePackage = parserContext.getReaderContext().getEnvironment().resolvePlaceholders(basePackage); + String[] basePackages = StringUtils.tokenizeToStringArray(basePackage, + ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); + + // Actually scan for bean definitions and register them. + ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element); + Set beanDefinitions = scanner.doScan(basePackages); + registerComponents(parserContext.getReaderContext(), beanDefinitions, element); + + return null; + } + + protected ClassPathBeanDefinitionScanner configureScanner(ParserContext parserContext, Element element) { + boolean useDefaultFilters = true; + if (element.hasAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE)) { + useDefaultFilters = Boolean.parseBoolean(element.getAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE)); + } + + // Delegate bean definition registration to scanner class. + ClassPathBeanDefinitionScanner scanner = createScanner(parserContext.getReaderContext(), useDefaultFilters); + scanner.setBeanDefinitionDefaults(parserContext.getDelegate().getBeanDefinitionDefaults()); + scanner.setAutowireCandidatePatterns(parserContext.getDelegate().getAutowireCandidatePatterns()); + + if (element.hasAttribute(RESOURCE_PATTERN_ATTRIBUTE)) { + scanner.setResourcePattern(element.getAttribute(RESOURCE_PATTERN_ATTRIBUTE)); + } + + try { + parseBeanNameGenerator(element, scanner); + } + catch (Exception ex) { + parserContext.getReaderContext().error(ex.getMessage(), parserContext.extractSource(element), ex.getCause()); + } + + try { + parseScope(element, scanner); + } + catch (Exception ex) { + parserContext.getReaderContext().error(ex.getMessage(), parserContext.extractSource(element), ex.getCause()); + } + + parseTypeFilters(element, scanner, parserContext); + + return scanner; + } + + protected ClassPathBeanDefinitionScanner createScanner(XmlReaderContext readerContext, boolean useDefaultFilters) { + return new ClassPathBeanDefinitionScanner(readerContext.getRegistry(), useDefaultFilters, + readerContext.getEnvironment(), readerContext.getResourceLoader()); + } + + protected void registerComponents( + XmlReaderContext readerContext, Set beanDefinitions, Element element) { + + Object source = readerContext.extractSource(element); + CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), source); + + for (BeanDefinitionHolder beanDefHolder : beanDefinitions) { + compositeDef.addNestedComponent(new BeanComponentDefinition(beanDefHolder)); + } + + // Register annotation config processors, if necessary. + boolean annotationConfig = true; + if (element.hasAttribute(ANNOTATION_CONFIG_ATTRIBUTE)) { + annotationConfig = Boolean.parseBoolean(element.getAttribute(ANNOTATION_CONFIG_ATTRIBUTE)); + } + if (annotationConfig) { + Set processorDefinitions = + AnnotationConfigUtils.registerAnnotationConfigProcessors(readerContext.getRegistry(), source); + for (BeanDefinitionHolder processorDefinition : processorDefinitions) { + compositeDef.addNestedComponent(new BeanComponentDefinition(processorDefinition)); + } + } + + readerContext.fireComponentRegistered(compositeDef); + } + + protected void parseBeanNameGenerator(Element element, ClassPathBeanDefinitionScanner scanner) { + if (element.hasAttribute(NAME_GENERATOR_ATTRIBUTE)) { + BeanNameGenerator beanNameGenerator = (BeanNameGenerator) instantiateUserDefinedStrategy( + element.getAttribute(NAME_GENERATOR_ATTRIBUTE), BeanNameGenerator.class, + scanner.getResourceLoader().getClassLoader()); + scanner.setBeanNameGenerator(beanNameGenerator); + } + } + + protected void parseScope(Element element, ClassPathBeanDefinitionScanner scanner) { + // Register ScopeMetadataResolver if class name provided. + if (element.hasAttribute(SCOPE_RESOLVER_ATTRIBUTE)) { + if (element.hasAttribute(SCOPED_PROXY_ATTRIBUTE)) { + throw new IllegalArgumentException( + "Cannot define both 'scope-resolver' and 'scoped-proxy' on tag"); + } + ScopeMetadataResolver scopeMetadataResolver = (ScopeMetadataResolver) instantiateUserDefinedStrategy( + element.getAttribute(SCOPE_RESOLVER_ATTRIBUTE), ScopeMetadataResolver.class, + scanner.getResourceLoader().getClassLoader()); + scanner.setScopeMetadataResolver(scopeMetadataResolver); + } + + if (element.hasAttribute(SCOPED_PROXY_ATTRIBUTE)) { + String mode = element.getAttribute(SCOPED_PROXY_ATTRIBUTE); + if ("targetClass".equals(mode)) { + scanner.setScopedProxyMode(ScopedProxyMode.TARGET_CLASS); + } + else if ("interfaces".equals(mode)) { + scanner.setScopedProxyMode(ScopedProxyMode.INTERFACES); + } + else if ("no".equals(mode)) { + scanner.setScopedProxyMode(ScopedProxyMode.NO); + } + else { + throw new IllegalArgumentException("scoped-proxy only supports 'no', 'interfaces' and 'targetClass'"); + } + } + } + + protected void parseTypeFilters(Element element, ClassPathBeanDefinitionScanner scanner, ParserContext parserContext) { + // Parse exclude and include filter elements. + ClassLoader classLoader = scanner.getResourceLoader().getClassLoader(); + NodeList nodeList = element.getChildNodes(); + for (int i = 0; i < nodeList.getLength(); i++) { + Node node = nodeList.item(i); + if (node.getNodeType() == Node.ELEMENT_NODE) { + String localName = parserContext.getDelegate().getLocalName(node); + try { + if (INCLUDE_FILTER_ELEMENT.equals(localName)) { + TypeFilter typeFilter = createTypeFilter((Element) node, classLoader, parserContext); + scanner.addIncludeFilter(typeFilter); + } + else if (EXCLUDE_FILTER_ELEMENT.equals(localName)) { + TypeFilter typeFilter = createTypeFilter((Element) node, classLoader, parserContext); + scanner.addExcludeFilter(typeFilter); + } + } + catch (ClassNotFoundException ex) { + parserContext.getReaderContext().warning( + "Ignoring non-present type filter class: " + ex, parserContext.extractSource(element)); + } + catch (Exception ex) { + parserContext.getReaderContext().error( + ex.getMessage(), parserContext.extractSource(element), ex.getCause()); + } + } + } + } + + @SuppressWarnings("unchecked") + protected TypeFilter createTypeFilter(Element element, @Nullable ClassLoader classLoader, + ParserContext parserContext) throws ClassNotFoundException { + + String filterType = element.getAttribute(FILTER_TYPE_ATTRIBUTE); + String expression = element.getAttribute(FILTER_EXPRESSION_ATTRIBUTE); + expression = parserContext.getReaderContext().getEnvironment().resolvePlaceholders(expression); + if ("annotation".equals(filterType)) { + return new AnnotationTypeFilter((Class) ClassUtils.forName(expression, classLoader)); + } + else if ("assignable".equals(filterType)) { + return new AssignableTypeFilter(ClassUtils.forName(expression, classLoader)); + } + else if ("aspectj".equals(filterType)) { + return new AspectJTypeFilter(expression, classLoader); + } + else if ("regex".equals(filterType)) { + return new RegexPatternTypeFilter(Pattern.compile(expression)); + } + else if ("custom".equals(filterType)) { + Class filterClass = ClassUtils.forName(expression, classLoader); + if (!TypeFilter.class.isAssignableFrom(filterClass)) { + throw new IllegalArgumentException( + "Class is not assignable to [" + TypeFilter.class.getName() + "]: " + expression); + } + return (TypeFilter) BeanUtils.instantiateClass(filterClass); + } + else { + throw new IllegalArgumentException("Unsupported filter type: " + filterType); + } + } + + @SuppressWarnings("unchecked") + private Object instantiateUserDefinedStrategy( + String className, Class strategyType, @Nullable ClassLoader classLoader) { + + Object result; + try { + result = ReflectionUtils.accessibleConstructor(ClassUtils.forName(className, classLoader)).newInstance(); + } + catch (ClassNotFoundException ex) { + throw new IllegalArgumentException("Class [" + className + "] for strategy [" + + strategyType.getName() + "] not found", ex); + } + catch (Throwable ex) { + throw new IllegalArgumentException("Unable to instantiate class [" + className + "] for strategy [" + + strategyType.getName() + "]: a zero-argument constructor is required", ex); + } + + if (!strategyType.isAssignableFrom(result.getClass())) { + throw new IllegalArgumentException("Provided class name must be an implementation of " + strategyType); + } + return result; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScans.java b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScans.java new file mode 100644 index 0000000..cf1e033 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScans.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Container annotation that aggregates several {@link ComponentScan} annotations. + * + *

    Can be used natively, declaring several nested {@link ComponentScan} annotations. + * Can also be used in conjunction with Java 8's support for repeatable annotations, + * where {@link ComponentScan} can simply be declared several times on the same method, + * implicitly generating this container annotation. + * + * @author Juergen Hoeller + * @since 4.3 + * @see ComponentScan + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Documented +public @interface ComponentScans { + + ComponentScan[] value(); + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Condition.java b/spring-context/src/main/java/org/springframework/context/annotation/Condition.java new file mode 100644 index 0000000..c38fffc --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/Condition.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.core.type.AnnotatedTypeMetadata; + +/** + * A single {@code condition} that must be {@linkplain #matches matched} in order + * for a component to be registered. + * + *

    Conditions are checked immediately before the bean-definition is due to be + * registered and are free to veto registration based on any criteria that can + * be determined at that point. + * + *

    Conditions must follow the same restrictions as {@link BeanFactoryPostProcessor} + * and take care to never interact with bean instances. For more fine-grained control + * of conditions that interact with {@code @Configuration} beans consider implementing + * the {@link ConfigurationCondition} interface. + * + * @author Phillip Webb + * @since 4.0 + * @see ConfigurationCondition + * @see Conditional + * @see ConditionContext + */ +@FunctionalInterface +public interface Condition { + + /** + * Determine if the condition matches. + * @param context the condition context + * @param metadata the metadata of the {@link org.springframework.core.type.AnnotationMetadata class} + * or {@link org.springframework.core.type.MethodMetadata method} being checked + * @return {@code true} if the condition matches and the component can be registered, + * or {@code false} to veto the annotated component's registration + */ + boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata); + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConditionContext.java b/spring-context/src/main/java/org/springframework/context/annotation/ConditionContext.java new file mode 100644 index 0000000..e28dfda --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConditionContext.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ResourceLoader; +import org.springframework.lang.Nullable; + +/** + * Context information for use by {@link Condition} implementations. + * + * @author Phillip Webb + * @author Juergen Hoeller + * @since 4.0 + */ +public interface ConditionContext { + + /** + * Return the {@link BeanDefinitionRegistry} that will hold the bean definition + * should the condition match. + * @throws IllegalStateException if no registry is available (which is unusual: + * only the case with a plain {@link ClassPathScanningCandidateComponentProvider}) + */ + BeanDefinitionRegistry getRegistry(); + + /** + * Return the {@link ConfigurableListableBeanFactory} that will hold the bean + * definition should the condition match, or {@code null} if the bean factory is + * not available (or not downcastable to {@code ConfigurableListableBeanFactory}). + */ + @Nullable + ConfigurableListableBeanFactory getBeanFactory(); + + /** + * Return the {@link Environment} for which the current application is running. + */ + Environment getEnvironment(); + + /** + * Return the {@link ResourceLoader} currently being used. + */ + ResourceLoader getResourceLoader(); + + /** + * Return the {@link ClassLoader} that should be used to load additional classes + * (only {@code null} if even the system ClassLoader isn't accessible). + * @see org.springframework.util.ClassUtils#forName(String, ClassLoader) + */ + @Nullable + ClassLoader getClassLoader(); + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConditionEvaluator.java b/spring-context/src/main/java/org/springframework/context/annotation/ConditionEvaluator.java new file mode 100644 index 0000000..10436f0 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConditionEvaluator.java @@ -0,0 +1,227 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.ConfigurationCondition.ConfigurationPhase; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.env.Environment; +import org.springframework.core.env.EnvironmentCapable; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.MultiValueMap; + +/** + * Internal class used to evaluate {@link Conditional} annotations. + * + * @author Phillip Webb + * @author Juergen Hoeller + * @since 4.0 + */ +class ConditionEvaluator { + + private final ConditionContextImpl context; + + + /** + * Create a new {@link ConditionEvaluator} instance. + */ + public ConditionEvaluator(@Nullable BeanDefinitionRegistry registry, + @Nullable Environment environment, @Nullable ResourceLoader resourceLoader) { + + this.context = new ConditionContextImpl(registry, environment, resourceLoader); + } + + + /** + * Determine if an item should be skipped based on {@code @Conditional} annotations. + * The {@link ConfigurationPhase} will be deduced from the type of item (i.e. a + * {@code @Configuration} class will be {@link ConfigurationPhase#PARSE_CONFIGURATION}) + * @param metadata the meta data + * @return if the item should be skipped + */ + public boolean shouldSkip(AnnotatedTypeMetadata metadata) { + return shouldSkip(metadata, null); + } + + /** + * Determine if an item should be skipped based on {@code @Conditional} annotations. + * @param metadata the meta data + * @param phase the phase of the call + * @return if the item should be skipped + */ + public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) { + if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) { + return false; + } + + if (phase == null) { + if (metadata instanceof AnnotationMetadata && + ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) { + return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION); + } + return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN); + } + + List conditions = new ArrayList<>(); + for (String[] conditionClasses : getConditionClasses(metadata)) { + for (String conditionClass : conditionClasses) { + Condition condition = getCondition(conditionClass, this.context.getClassLoader()); + conditions.add(condition); + } + } + + AnnotationAwareOrderComparator.sort(conditions); + + for (Condition condition : conditions) { + ConfigurationPhase requiredPhase = null; + if (condition instanceof ConfigurationCondition) { + requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase(); + } + if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) { + return true; + } + } + + return false; + } + + @SuppressWarnings("unchecked") + private List getConditionClasses(AnnotatedTypeMetadata metadata) { + MultiValueMap attributes = metadata.getAllAnnotationAttributes(Conditional.class.getName(), true); + Object values = (attributes != null ? attributes.get("value") : null); + return (List) (values != null ? values : Collections.emptyList()); + } + + private Condition getCondition(String conditionClassName, @Nullable ClassLoader classloader) { + Class conditionClass = ClassUtils.resolveClassName(conditionClassName, classloader); + return (Condition) BeanUtils.instantiateClass(conditionClass); + } + + + /** + * Implementation of a {@link ConditionContext}. + */ + private static class ConditionContextImpl implements ConditionContext { + + @Nullable + private final BeanDefinitionRegistry registry; + + @Nullable + private final ConfigurableListableBeanFactory beanFactory; + + private final Environment environment; + + private final ResourceLoader resourceLoader; + + @Nullable + private final ClassLoader classLoader; + + public ConditionContextImpl(@Nullable BeanDefinitionRegistry registry, + @Nullable Environment environment, @Nullable ResourceLoader resourceLoader) { + + this.registry = registry; + this.beanFactory = deduceBeanFactory(registry); + this.environment = (environment != null ? environment : deduceEnvironment(registry)); + this.resourceLoader = (resourceLoader != null ? resourceLoader : deduceResourceLoader(registry)); + this.classLoader = deduceClassLoader(resourceLoader, this.beanFactory); + } + + @Nullable + private ConfigurableListableBeanFactory deduceBeanFactory(@Nullable BeanDefinitionRegistry source) { + if (source instanceof ConfigurableListableBeanFactory) { + return (ConfigurableListableBeanFactory) source; + } + if (source instanceof ConfigurableApplicationContext) { + return (((ConfigurableApplicationContext) source).getBeanFactory()); + } + return null; + } + + private Environment deduceEnvironment(@Nullable BeanDefinitionRegistry source) { + if (source instanceof EnvironmentCapable) { + return ((EnvironmentCapable) source).getEnvironment(); + } + return new StandardEnvironment(); + } + + private ResourceLoader deduceResourceLoader(@Nullable BeanDefinitionRegistry source) { + if (source instanceof ResourceLoader) { + return (ResourceLoader) source; + } + return new DefaultResourceLoader(); + } + + @Nullable + private ClassLoader deduceClassLoader(@Nullable ResourceLoader resourceLoader, + @Nullable ConfigurableListableBeanFactory beanFactory) { + + if (resourceLoader != null) { + ClassLoader classLoader = resourceLoader.getClassLoader(); + if (classLoader != null) { + return classLoader; + } + } + if (beanFactory != null) { + return beanFactory.getBeanClassLoader(); + } + return ClassUtils.getDefaultClassLoader(); + } + + @Override + public BeanDefinitionRegistry getRegistry() { + Assert.state(this.registry != null, "No BeanDefinitionRegistry available"); + return this.registry; + } + + @Override + @Nullable + public ConfigurableListableBeanFactory getBeanFactory() { + return this.beanFactory; + } + + @Override + public Environment getEnvironment() { + return this.environment; + } + + @Override + public ResourceLoader getResourceLoader() { + return this.resourceLoader; + } + + @Override + @Nullable + public ClassLoader getClassLoader() { + return this.classLoader; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Conditional.java b/spring-context/src/main/java/org/springframework/context/annotation/Conditional.java new file mode 100644 index 0000000..80e5f5c --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/Conditional.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that a component is only eligible for registration when all + * {@linkplain #value specified conditions} match. + * + *

    A condition is any state that can be determined programmatically + * before the bean definition is due to be registered (see {@link Condition} for details). + * + *

    The {@code @Conditional} annotation may be used in any of the following ways: + *

      + *
    • as a type-level annotation on any class directly or indirectly annotated with + * {@code @Component}, including {@link Configuration @Configuration} classes
    • + *
    • as a meta-annotation, for the purpose of composing custom stereotype + * annotations
    • + *
    • as a method-level annotation on any {@link Bean @Bean} method
    • + *
    + * + *

    If a {@code @Configuration} class is marked with {@code @Conditional}, + * all of the {@code @Bean} methods, {@link Import @Import} annotations, and + * {@link ComponentScan @ComponentScan} annotations associated with that + * class will be subject to the conditions. + * + *

    NOTE: Inheritance of {@code @Conditional} annotations + * is not supported; any conditions from superclasses or from overridden + * methods will not be considered. In order to enforce these semantics, + * {@code @Conditional} itself is not declared as + * {@link java.lang.annotation.Inherited @Inherited}; furthermore, any + * custom composed annotation that is meta-annotated with + * {@code @Conditional} must not be declared as {@code @Inherited}. + * + * @author Phillip Webb + * @author Sam Brannen + * @since 4.0 + * @see Condition + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Conditional { + + /** + * All {@link Condition} classes that must {@linkplain Condition#matches match} + * in order for the component to be registered. + */ + Class[] value(); + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Configuration.java b/spring-context/src/main/java/org/springframework/context/annotation/Configuration.java new file mode 100644 index 0000000..725999d --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/Configuration.java @@ -0,0 +1,463 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.annotation.AliasFor; +import org.springframework.stereotype.Component; + +/** + * Indicates that a class declares one or more {@link Bean @Bean} methods and + * may be processed by the Spring container to generate bean definitions and + * service requests for those beans at runtime, for example: + * + *

    + * @Configuration
    + * public class AppConfig {
    + *
    + *     @Bean
    + *     public MyBean myBean() {
    + *         // instantiate, configure and return bean ...
    + *     }
    + * }
    + * + *

    Bootstrapping {@code @Configuration} classes

    + * + *

    Via {@code AnnotationConfigApplicationContext}

    + * + *

    {@code @Configuration} classes are typically bootstrapped using either + * {@link AnnotationConfigApplicationContext} or its web-capable variant, + * {@link org.springframework.web.context.support.AnnotationConfigWebApplicationContext + * AnnotationConfigWebApplicationContext}. A simple example with the former follows: + * + *

    + * AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    + * ctx.register(AppConfig.class);
    + * ctx.refresh();
    + * MyBean myBean = ctx.getBean(MyBean.class);
    + * // use myBean ...
    + * 
    + * + *

    See the {@link AnnotationConfigApplicationContext} javadocs for further details, and see + * {@link org.springframework.web.context.support.AnnotationConfigWebApplicationContext + * AnnotationConfigWebApplicationContext} for web configuration instructions in a + * {@code Servlet} container. + * + *

    Via Spring {@code } XML

    + * + *

    As an alternative to registering {@code @Configuration} classes directly against an + * {@code AnnotationConfigApplicationContext}, {@code @Configuration} classes may be + * declared as normal {@code } definitions within Spring XML files: + * + *

    + * <beans>
    + *    <context:annotation-config/>
    + *    <bean class="com.acme.AppConfig"/>
    + * </beans>
    + * 
    + * + *

    In the example above, {@code } is required in order to + * enable {@link ConfigurationClassPostProcessor} and other annotation-related + * post processors that facilitate handling {@code @Configuration} classes. + * + *

    Via component scanning

    + * + *

    {@code @Configuration} is meta-annotated with {@link Component @Component}, therefore + * {@code @Configuration} classes are candidates for component scanning (typically using + * Spring XML's {@code } element) and therefore may also take + * advantage of {@link Autowired @Autowired}/{@link javax.inject.Inject @Inject} + * like any regular {@code @Component}. In particular, if a single constructor is present + * autowiring semantics will be applied transparently for that constructor: + * + *

    + * @Configuration
    + * public class AppConfig {
    + *
    + *     private final SomeBean someBean;
    + *
    + *     public AppConfig(SomeBean someBean) {
    + *         this.someBean = someBean;
    + *     }
    + *
    + *     // @Bean definition using "SomeBean"
    + *
    + * }
    + * + *

    {@code @Configuration} classes may not only be bootstrapped using + * component scanning, but may also themselves configure component scanning using + * the {@link ComponentScan @ComponentScan} annotation: + * + *

    + * @Configuration
    + * @ComponentScan("com.acme.app.services")
    + * public class AppConfig {
    + *     // various @Bean definitions ...
    + * }
    + * + *

    See the {@link ComponentScan @ComponentScan} javadocs for details. + * + *

    Working with externalized values

    + * + *

    Using the {@code Environment} API

    + * + *

    Externalized values may be looked up by injecting the Spring + * {@link org.springframework.core.env.Environment} into a {@code @Configuration} + * class — for example, using the {@code @Autowired} annotation: + * + *

    + * @Configuration
    + * public class AppConfig {
    + *
    + *     @Autowired Environment env;
    + *
    + *     @Bean
    + *     public MyBean myBean() {
    + *         MyBean myBean = new MyBean();
    + *         myBean.setName(env.getProperty("bean.name"));
    + *         return myBean;
    + *     }
    + * }
    + * + *

    Properties resolved through the {@code Environment} reside in one or more "property + * source" objects, and {@code @Configuration} classes may contribute property sources to + * the {@code Environment} object using the {@link PropertySource @PropertySource} + * annotation: + * + *

    + * @Configuration
    + * @PropertySource("classpath:/com/acme/app.properties")
    + * public class AppConfig {
    + *
    + *     @Inject Environment env;
    + *
    + *     @Bean
    + *     public MyBean myBean() {
    + *         return new MyBean(env.getProperty("bean.name"));
    + *     }
    + * }
    + * + *

    See the {@link org.springframework.core.env.Environment Environment} + * and {@link PropertySource @PropertySource} javadocs for further details. + * + *

    Using the {@code @Value} annotation

    + * + *

    Externalized values may be injected into {@code @Configuration} classes using + * the {@link Value @Value} annotation: + * + *

    + * @Configuration
    + * @PropertySource("classpath:/com/acme/app.properties")
    + * public class AppConfig {
    + *
    + *     @Value("${bean.name}") String beanName;
    + *
    + *     @Bean
    + *     public MyBean myBean() {
    + *         return new MyBean(beanName);
    + *     }
    + * }
    + * + *

    This approach is often used in conjunction with Spring's + * {@link org.springframework.context.support.PropertySourcesPlaceholderConfigurer + * PropertySourcesPlaceholderConfigurer} that can be enabled automatically + * in XML configuration via {@code } or explicitly + * in a {@code @Configuration} class via a dedicated {@code static} {@code @Bean} method + * (see "a note on BeanFactoryPostProcessor-returning {@code @Bean} methods" of + * {@link Bean @Bean}'s javadocs for details). Note, however, that explicit registration + * of a {@code PropertySourcesPlaceholderConfigurer} via a {@code static} {@code @Bean} + * method is typically only required if you need to customize configuration such as the + * placeholder syntax, etc. Specifically, if no bean post-processor (such as a + * {@code PropertySourcesPlaceholderConfigurer}) has registered an embedded value + * resolver for the {@code ApplicationContext}, Spring will register a default + * embedded value resolver which resolves placeholders against property sources + * registered in the {@code Environment}. See the section below on composing + * {@code @Configuration} classes with Spring XML using {@code @ImportResource}; see + * the {@link Value @Value} javadocs; and see the {@link Bean @Bean} javadocs for details + * on working with {@code BeanFactoryPostProcessor} types such as + * {@code PropertySourcesPlaceholderConfigurer}. + * + *

    Composing {@code @Configuration} classes

    + * + *

    With the {@code @Import} annotation

    + * + *

    {@code @Configuration} classes may be composed using the {@link Import @Import} annotation, + * similar to the way that {@code } works in Spring XML. Because + * {@code @Configuration} objects are managed as Spring beans within the container, + * imported configurations may be injected — for example, via constructor injection: + * + *

    + * @Configuration
    + * public class DatabaseConfig {
    + *
    + *     @Bean
    + *     public DataSource dataSource() {
    + *         // instantiate, configure and return DataSource
    + *     }
    + * }
    + *
    + * @Configuration
    + * @Import(DatabaseConfig.class)
    + * public class AppConfig {
    + *
    + *     private final DatabaseConfig dataConfig;
    + *
    + *     public AppConfig(DatabaseConfig dataConfig) {
    + *         this.dataConfig = dataConfig;
    + *     }
    + *
    + *     @Bean
    + *     public MyBean myBean() {
    + *         // reference the dataSource() bean method
    + *         return new MyBean(dataConfig.dataSource());
    + *     }
    + * }
    + * + *

    Now both {@code AppConfig} and the imported {@code DatabaseConfig} can be bootstrapped + * by registering only {@code AppConfig} against the Spring context: + * + *

    + * new AnnotationConfigApplicationContext(AppConfig.class);
    + * + *

    With the {@code @Profile} annotation

    + * + *

    {@code @Configuration} classes may be marked with the {@link Profile @Profile} annotation to + * indicate they should be processed only if a given profile or profiles are active: + * + *

    + * @Profile("development")
    + * @Configuration
    + * public class EmbeddedDatabaseConfig {
    + *
    + *     @Bean
    + *     public DataSource dataSource() {
    + *         // instantiate, configure and return embedded DataSource
    + *     }
    + * }
    + *
    + * @Profile("production")
    + * @Configuration
    + * public class ProductionDatabaseConfig {
    + *
    + *     @Bean
    + *     public DataSource dataSource() {
    + *         // instantiate, configure and return production DataSource
    + *     }
    + * }
    + * + *

    Alternatively, you may also declare profile conditions at the {@code @Bean} method level + * — for example, for alternative bean variants within the same configuration class: + * + *

    + * @Configuration
    + * public class ProfileDatabaseConfig {
    + *
    + *     @Bean("dataSource")
    + *     @Profile("development")
    + *     public DataSource embeddedDatabase() { ... }
    + *
    + *     @Bean("dataSource")
    + *     @Profile("production")
    + *     public DataSource productionDatabase() { ... }
    + * }
    + * + *

    See the {@link Profile @Profile} and {@link org.springframework.core.env.Environment} + * javadocs for further details. + * + *

    With Spring XML using the {@code @ImportResource} annotation

    + * + *

    As mentioned above, {@code @Configuration} classes may be declared as regular Spring + * {@code } definitions within Spring XML files. It is also possible to + * import Spring XML configuration files into {@code @Configuration} classes using + * the {@link ImportResource @ImportResource} annotation. Bean definitions imported from + * XML can be injected — for example, using the {@code @Inject} annotation: + * + *

    + * @Configuration
    + * @ImportResource("classpath:/com/acme/database-config.xml")
    + * public class AppConfig {
    + *
    + *     @Inject DataSource dataSource; // from XML
    + *
    + *     @Bean
    + *     public MyBean myBean() {
    + *         // inject the XML-defined dataSource bean
    + *         return new MyBean(this.dataSource);
    + *     }
    + * }
    + * + *

    With nested {@code @Configuration} classes

    + * + *

    {@code @Configuration} classes may be nested within one another as follows: + * + *

    + * @Configuration
    + * public class AppConfig {
    + *
    + *     @Inject DataSource dataSource;
    + *
    + *     @Bean
    + *     public MyBean myBean() {
    + *         return new MyBean(dataSource);
    + *     }
    + *
    + *     @Configuration
    + *     static class DatabaseConfig {
    + *         @Bean
    + *         DataSource dataSource() {
    + *             return new EmbeddedDatabaseBuilder().build();
    + *         }
    + *     }
    + * }
    + * + *

    When bootstrapping such an arrangement, only {@code AppConfig} need be registered + * against the application context. By virtue of being a nested {@code @Configuration} + * class, {@code DatabaseConfig} will be registered automatically. This avoids + * the need to use an {@code @Import} annotation when the relationship between + * {@code AppConfig} and {@code DatabaseConfig} is already implicitly clear. + * + *

    Note also that nested {@code @Configuration} classes can be used to good effect + * with the {@code @Profile} annotation to provide two options of the same bean to the + * enclosing {@code @Configuration} class. + * + *

    Configuring lazy initialization

    + * + *

    By default, {@code @Bean} methods will be eagerly instantiated at container + * bootstrap time. To avoid this, {@code @Configuration} may be used in conjunction with + * the {@link Lazy @Lazy} annotation to indicate that all {@code @Bean} methods declared + * within the class are by default lazily initialized. Note that {@code @Lazy} may be used + * on individual {@code @Bean} methods as well. + * + *

    Testing support for {@code @Configuration} classes

    + * + *

    The Spring TestContext framework available in the {@code spring-test} module + * provides the {@code @ContextConfiguration} annotation which can accept an array of + * component class references — typically {@code @Configuration} or + * {@code @Component} classes. + * + *

    + * @RunWith(SpringRunner.class)
    + * @ContextConfiguration(classes = {AppConfig.class, DatabaseConfig.class})
    + * public class MyTests {
    + *
    + *     @Autowired MyBean myBean;
    + *
    + *     @Autowired DataSource dataSource;
    + *
    + *     @Test
    + *     public void test() {
    + *         // assertions against myBean ...
    + *     }
    + * }
    + * + *

    See the + * TestContext framework + * reference documentation for details. + * + *

    Enabling built-in Spring features using {@code @Enable} annotations

    + * + *

    Spring features such as asynchronous method execution, scheduled task execution, + * annotation driven transaction management, and even Spring MVC can be enabled and + * configured from {@code @Configuration} classes using their respective "{@code @Enable}" + * annotations. See + * {@link org.springframework.scheduling.annotation.EnableAsync @EnableAsync}, + * {@link org.springframework.scheduling.annotation.EnableScheduling @EnableScheduling}, + * {@link org.springframework.transaction.annotation.EnableTransactionManagement @EnableTransactionManagement}, + * {@link org.springframework.context.annotation.EnableAspectJAutoProxy @EnableAspectJAutoProxy}, + * and {@link org.springframework.web.servlet.config.annotation.EnableWebMvc @EnableWebMvc} + * for details. + * + *

    Constraints when authoring {@code @Configuration} classes

    + * + *
      + *
    • Configuration classes must be provided as classes (i.e. not as instances returned + * from factory methods), allowing for runtime enhancements through a generated subclass. + *
    • Configuration classes must be non-final (allowing for subclasses at runtime), + * unless the {@link #proxyBeanMethods() proxyBeanMethods} flag is set to {@code false} + * in which case no runtime-generated subclass is necessary. + *
    • Configuration classes must be non-local (i.e. may not be declared within a method). + *
    • Any nested configuration classes must be declared as {@code static}. + *
    • {@code @Bean} methods may not in turn create further configuration classes + * (any such instances will be treated as regular beans, with their configuration + * annotations remaining undetected). + *
    + * + * @author Rod Johnson + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.0 + * @see Bean + * @see Profile + * @see Import + * @see ImportResource + * @see ComponentScan + * @see Lazy + * @see PropertySource + * @see AnnotationConfigApplicationContext + * @see ConfigurationClassPostProcessor + * @see org.springframework.core.env.Environment + * @see org.springframework.test.context.ContextConfiguration + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface Configuration { + + /** + * Explicitly specify the name of the Spring bean definition associated with the + * {@code @Configuration} class. If left unspecified (the common case), a bean + * name will be automatically generated. + *

    The custom name applies only if the {@code @Configuration} class is picked + * up via component scanning or supplied directly to an + * {@link AnnotationConfigApplicationContext}. If the {@code @Configuration} class + * is registered as a traditional XML bean definition, the name/id of the bean + * element will take precedence. + * @return the explicit component name, if any (or empty String otherwise) + * @see AnnotationBeanNameGenerator + */ + @AliasFor(annotation = Component.class) + String value() default ""; + + /** + * Specify whether {@code @Bean} methods should get proxied in order to enforce + * bean lifecycle behavior, e.g. to return shared singleton bean instances even + * in case of direct {@code @Bean} method calls in user code. This feature + * requires method interception, implemented through a runtime-generated CGLIB + * subclass which comes with limitations such as the configuration class and + * its methods not being allowed to declare {@code final}. + *

    The default is {@code true}, allowing for 'inter-bean references' via direct + * method calls within the configuration class as well as for external calls to + * this configuration's {@code @Bean} methods, e.g. from another configuration class. + * If this is not needed since each of this particular configuration's {@code @Bean} + * methods is self-contained and designed as a plain factory method for container use, + * switch this flag to {@code false} in order to avoid CGLIB subclass processing. + *

    Turning off bean method interception effectively processes {@code @Bean} + * methods individually like when declared on non-{@code @Configuration} classes, + * a.k.a. "@Bean Lite Mode" (see {@link Bean @Bean's javadoc}). It is therefore + * behaviorally equivalent to removing the {@code @Configuration} stereotype. + * @since 5.2 + */ + boolean proxyBeanMethods() default true; + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java new file mode 100644 index 0000000..0b02ead --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java @@ -0,0 +1,253 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.beans.factory.parsing.Location; +import org.springframework.beans.factory.parsing.Problem; +import org.springframework.beans.factory.parsing.ProblemReporter; +import org.springframework.beans.factory.support.BeanDefinitionReader; +import org.springframework.core.io.DescriptiveResource; +import org.springframework.core.io.Resource; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Represents a user-defined {@link Configuration @Configuration} class. + * Includes a set of {@link Bean} methods, including all such methods + * defined in the ancestry of the class, in a 'flattened-out' manner. + * + * @author Chris Beams + * @author Juergen Hoeller + * @author Phillip Webb + * @since 3.0 + * @see BeanMethod + * @see ConfigurationClassParser + */ +final class ConfigurationClass { + + private final AnnotationMetadata metadata; + + private final Resource resource; + + @Nullable + private String beanName; + + private final Set importedBy = new LinkedHashSet<>(1); + + private final Set beanMethods = new LinkedHashSet<>(); + + private final Map> importedResources = + new LinkedHashMap<>(); + + private final Map importBeanDefinitionRegistrars = + new LinkedHashMap<>(); + + final Set skippedBeanMethods = new HashSet<>(); + + + /** + * Create a new {@link ConfigurationClass} with the given name. + * @param metadataReader reader used to parse the underlying {@link Class} + * @param beanName must not be {@code null} + * @see ConfigurationClass#ConfigurationClass(Class, ConfigurationClass) + */ + public ConfigurationClass(MetadataReader metadataReader, String beanName) { + Assert.notNull(beanName, "Bean name must not be null"); + this.metadata = metadataReader.getAnnotationMetadata(); + this.resource = metadataReader.getResource(); + this.beanName = beanName; + } + + /** + * Create a new {@link ConfigurationClass} representing a class that was imported + * using the {@link Import} annotation or automatically processed as a nested + * configuration class (if importedBy is not {@code null}). + * @param metadataReader reader used to parse the underlying {@link Class} + * @param importedBy the configuration class importing this one or {@code null} + * @since 3.1.1 + */ + public ConfigurationClass(MetadataReader metadataReader, @Nullable ConfigurationClass importedBy) { + this.metadata = metadataReader.getAnnotationMetadata(); + this.resource = metadataReader.getResource(); + this.importedBy.add(importedBy); + } + + /** + * Create a new {@link ConfigurationClass} with the given name. + * @param clazz the underlying {@link Class} to represent + * @param beanName name of the {@code @Configuration} class bean + * @see ConfigurationClass#ConfigurationClass(Class, ConfigurationClass) + */ + public ConfigurationClass(Class clazz, String beanName) { + Assert.notNull(beanName, "Bean name must not be null"); + this.metadata = AnnotationMetadata.introspect(clazz); + this.resource = new DescriptiveResource(clazz.getName()); + this.beanName = beanName; + } + + /** + * Create a new {@link ConfigurationClass} representing a class that was imported + * using the {@link Import} annotation or automatically processed as a nested + * configuration class (if imported is {@code true}). + * @param clazz the underlying {@link Class} to represent + * @param importedBy the configuration class importing this one (or {@code null}) + * @since 3.1.1 + */ + public ConfigurationClass(Class clazz, @Nullable ConfigurationClass importedBy) { + this.metadata = AnnotationMetadata.introspect(clazz); + this.resource = new DescriptiveResource(clazz.getName()); + this.importedBy.add(importedBy); + } + + /** + * Create a new {@link ConfigurationClass} with the given name. + * @param metadata the metadata for the underlying class to represent + * @param beanName name of the {@code @Configuration} class bean + * @see ConfigurationClass#ConfigurationClass(Class, ConfigurationClass) + */ + public ConfigurationClass(AnnotationMetadata metadata, String beanName) { + Assert.notNull(beanName, "Bean name must not be null"); + this.metadata = metadata; + this.resource = new DescriptiveResource(metadata.getClassName()); + this.beanName = beanName; + } + + + public AnnotationMetadata getMetadata() { + return this.metadata; + } + + public Resource getResource() { + return this.resource; + } + + public String getSimpleName() { + return ClassUtils.getShortName(getMetadata().getClassName()); + } + + public void setBeanName(String beanName) { + this.beanName = beanName; + } + + @Nullable + public String getBeanName() { + return this.beanName; + } + + /** + * Return whether this configuration class was registered via @{@link Import} or + * automatically registered due to being nested within another configuration class. + * @since 3.1.1 + * @see #getImportedBy() + */ + public boolean isImported() { + return !this.importedBy.isEmpty(); + } + + /** + * Merge the imported-by declarations from the given configuration class into this one. + * @since 4.0.5 + */ + public void mergeImportedBy(ConfigurationClass otherConfigClass) { + this.importedBy.addAll(otherConfigClass.importedBy); + } + + /** + * Return the configuration classes that imported this class, + * or an empty Set if this configuration was not imported. + * @since 4.0.5 + * @see #isImported() + */ + public Set getImportedBy() { + return this.importedBy; + } + + public void addBeanMethod(BeanMethod method) { + this.beanMethods.add(method); + } + + public Set getBeanMethods() { + return this.beanMethods; + } + + public void addImportedResource(String importedResource, Class readerClass) { + this.importedResources.put(importedResource, readerClass); + } + + public void addImportBeanDefinitionRegistrar(ImportBeanDefinitionRegistrar registrar, AnnotationMetadata importingClassMetadata) { + this.importBeanDefinitionRegistrars.put(registrar, importingClassMetadata); + } + + public Map getImportBeanDefinitionRegistrars() { + return this.importBeanDefinitionRegistrars; + } + + public Map> getImportedResources() { + return this.importedResources; + } + + public void validate(ProblemReporter problemReporter) { + // A configuration class may not be final (CGLIB limitation) unless it declares proxyBeanMethods=false + Map attributes = this.metadata.getAnnotationAttributes(Configuration.class.getName()); + if (attributes != null && (Boolean) attributes.get("proxyBeanMethods")) { + if (this.metadata.isFinal()) { + problemReporter.error(new FinalConfigurationProblem()); + } + for (BeanMethod beanMethod : this.beanMethods) { + beanMethod.validate(problemReporter); + } + } + } + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof ConfigurationClass && + getMetadata().getClassName().equals(((ConfigurationClass) other).getMetadata().getClassName()))); + } + + @Override + public int hashCode() { + return getMetadata().getClassName().hashCode(); + } + + @Override + public String toString() { + return "ConfigurationClass: beanName '" + this.beanName + "', " + this.resource; + } + + + /** + * Configuration classes must be non-final to accommodate CGLIB subclassing. + */ + private class FinalConfigurationProblem extends Problem { + + public FinalConfigurationProblem() { + super(String.format("@Configuration class '%s' may not be final. Remove the final modifier to continue.", + getSimpleName()), new Location(getResource(), getMetadata())); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java new file mode 100644 index 0000000..427e2be --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java @@ -0,0 +1,497 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition; +import org.springframework.beans.factory.annotation.Autowire; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader; +import org.springframework.beans.factory.parsing.SourceExtractor; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinitionReader; +import org.springframework.beans.factory.support.BeanDefinitionReader; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.context.annotation.ConfigurationCondition.ConfigurationPhase; +import org.springframework.core.SpringProperties; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.env.Environment; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.MethodMetadata; +import org.springframework.core.type.StandardAnnotationMetadata; +import org.springframework.core.type.StandardMethodMetadata; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Reads a given fully-populated set of ConfigurationClass instances, registering bean + * definitions with the given {@link BeanDefinitionRegistry} based on its contents. + * + *

    This class was modeled after the {@link BeanDefinitionReader} hierarchy, but does + * not implement/extend any of its artifacts as a set of configuration classes is not a + * {@link Resource}. + * + * @author Chris Beams + * @author Juergen Hoeller + * @author Phillip Webb + * @author Sam Brannen + * @author Sebastien Deleuze + * @since 3.0 + * @see ConfigurationClassParser + */ +class ConfigurationClassBeanDefinitionReader { + + private static final Log logger = LogFactory.getLog(ConfigurationClassBeanDefinitionReader.class); + + private static final ScopeMetadataResolver scopeMetadataResolver = new AnnotationScopeMetadataResolver(); + + /** + * Boolean flag controlled by a {@code spring.xml.ignore} system property that instructs Spring to + * ignore XML, i.e. to not initialize the XML-related infrastructure. + *

    The default is "false". + */ + private static final boolean shouldIgnoreXml = SpringProperties.getFlag("spring.xml.ignore"); + + private final BeanDefinitionRegistry registry; + + private final SourceExtractor sourceExtractor; + + private final ResourceLoader resourceLoader; + + private final Environment environment; + + private final BeanNameGenerator importBeanNameGenerator; + + private final ImportRegistry importRegistry; + + private final ConditionEvaluator conditionEvaluator; + + + /** + * Create a new {@link ConfigurationClassBeanDefinitionReader} instance + * that will be used to populate the given {@link BeanDefinitionRegistry}. + */ + ConfigurationClassBeanDefinitionReader(BeanDefinitionRegistry registry, SourceExtractor sourceExtractor, + ResourceLoader resourceLoader, Environment environment, BeanNameGenerator importBeanNameGenerator, + ImportRegistry importRegistry) { + + this.registry = registry; + this.sourceExtractor = sourceExtractor; + this.resourceLoader = resourceLoader; + this.environment = environment; + this.importBeanNameGenerator = importBeanNameGenerator; + this.importRegistry = importRegistry; + this.conditionEvaluator = new ConditionEvaluator(registry, environment, resourceLoader); + } + + + /** + * Read {@code configurationModel}, registering bean definitions + * with the registry based on its contents. + */ + public void loadBeanDefinitions(Set configurationModel) { + TrackedConditionEvaluator trackedConditionEvaluator = new TrackedConditionEvaluator(); + for (ConfigurationClass configClass : configurationModel) { + loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator); + } + } + + /** + * Read a particular {@link ConfigurationClass}, registering bean definitions + * for the class itself and all of its {@link Bean} methods. + */ + private void loadBeanDefinitionsForConfigurationClass( + ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) { + + if (trackedConditionEvaluator.shouldSkip(configClass)) { + String beanName = configClass.getBeanName(); + if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) { + this.registry.removeBeanDefinition(beanName); + } + this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName()); + return; + } + + if (configClass.isImported()) { + registerBeanDefinitionForImportedConfigurationClass(configClass); + } + for (BeanMethod beanMethod : configClass.getBeanMethods()) { + loadBeanDefinitionsForBeanMethod(beanMethod); + } + + loadBeanDefinitionsFromImportedResources(configClass.getImportedResources()); + loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars()); + } + + /** + * Register the {@link Configuration} class itself as a bean definition. + */ + private void registerBeanDefinitionForImportedConfigurationClass(ConfigurationClass configClass) { + AnnotationMetadata metadata = configClass.getMetadata(); + AnnotatedGenericBeanDefinition configBeanDef = new AnnotatedGenericBeanDefinition(metadata); + + ScopeMetadata scopeMetadata = scopeMetadataResolver.resolveScopeMetadata(configBeanDef); + configBeanDef.setScope(scopeMetadata.getScopeName()); + String configBeanName = this.importBeanNameGenerator.generateBeanName(configBeanDef, this.registry); + AnnotationConfigUtils.processCommonDefinitionAnnotations(configBeanDef, metadata); + + BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(configBeanDef, configBeanName); + definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry); + this.registry.registerBeanDefinition(definitionHolder.getBeanName(), definitionHolder.getBeanDefinition()); + configClass.setBeanName(configBeanName); + + if (logger.isTraceEnabled()) { + logger.trace("Registered bean definition for imported class '" + configBeanName + "'"); + } + } + + /** + * Read the given {@link BeanMethod}, registering bean definitions + * with the BeanDefinitionRegistry based on its contents. + */ + @SuppressWarnings("deprecation") // for RequiredAnnotationBeanPostProcessor.SKIP_REQUIRED_CHECK_ATTRIBUTE + private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) { + ConfigurationClass configClass = beanMethod.getConfigurationClass(); + MethodMetadata metadata = beanMethod.getMetadata(); + String methodName = metadata.getMethodName(); + + // Do we need to mark the bean as skipped by its condition? + if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) { + configClass.skippedBeanMethods.add(methodName); + return; + } + if (configClass.skippedBeanMethods.contains(methodName)) { + return; + } + + AnnotationAttributes bean = AnnotationConfigUtils.attributesFor(metadata, Bean.class); + Assert.state(bean != null, "No @Bean annotation attributes"); + + // Consider name and any aliases + List names = new ArrayList<>(Arrays.asList(bean.getStringArray("name"))); + String beanName = (!names.isEmpty() ? names.remove(0) : methodName); + + // Register aliases even when overridden + for (String alias : names) { + this.registry.registerAlias(beanName, alias); + } + + // Has this effectively been overridden before (e.g. via XML)? + if (isOverriddenByExistingDefinition(beanMethod, beanName)) { + if (beanName.equals(beanMethod.getConfigurationClass().getBeanName())) { + throw new BeanDefinitionStoreException(beanMethod.getConfigurationClass().getResource().getDescription(), + beanName, "Bean name derived from @Bean method '" + beanMethod.getMetadata().getMethodName() + + "' clashes with bean name for containing configuration class; please make those names unique!"); + } + return; + } + + ConfigurationClassBeanDefinition beanDef = new ConfigurationClassBeanDefinition(configClass, metadata, beanName); + beanDef.setSource(this.sourceExtractor.extractSource(metadata, configClass.getResource())); + + if (metadata.isStatic()) { + // static @Bean method + if (configClass.getMetadata() instanceof StandardAnnotationMetadata) { + beanDef.setBeanClass(((StandardAnnotationMetadata) configClass.getMetadata()).getIntrospectedClass()); + } + else { + beanDef.setBeanClassName(configClass.getMetadata().getClassName()); + } + beanDef.setUniqueFactoryMethodName(methodName); + } + else { + // instance @Bean method + beanDef.setFactoryBeanName(configClass.getBeanName()); + beanDef.setUniqueFactoryMethodName(methodName); + } + + if (metadata instanceof StandardMethodMetadata) { + beanDef.setResolvedFactoryMethod(((StandardMethodMetadata) metadata).getIntrospectedMethod()); + } + + beanDef.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR); + beanDef.setAttribute(org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor. + SKIP_REQUIRED_CHECK_ATTRIBUTE, Boolean.TRUE); + + AnnotationConfigUtils.processCommonDefinitionAnnotations(beanDef, metadata); + + Autowire autowire = bean.getEnum("autowire"); + if (autowire.isAutowire()) { + beanDef.setAutowireMode(autowire.value()); + } + + boolean autowireCandidate = bean.getBoolean("autowireCandidate"); + if (!autowireCandidate) { + beanDef.setAutowireCandidate(false); + } + + String initMethodName = bean.getString("initMethod"); + if (StringUtils.hasText(initMethodName)) { + beanDef.setInitMethodName(initMethodName); + } + + String destroyMethodName = bean.getString("destroyMethod"); + beanDef.setDestroyMethodName(destroyMethodName); + + // Consider scoping + ScopedProxyMode proxyMode = ScopedProxyMode.NO; + AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(metadata, Scope.class); + if (attributes != null) { + beanDef.setScope(attributes.getString("value")); + proxyMode = attributes.getEnum("proxyMode"); + if (proxyMode == ScopedProxyMode.DEFAULT) { + proxyMode = ScopedProxyMode.NO; + } + } + + // Replace the original bean definition with the target one, if necessary + BeanDefinition beanDefToRegister = beanDef; + if (proxyMode != ScopedProxyMode.NO) { + BeanDefinitionHolder proxyDef = ScopedProxyCreator.createScopedProxy( + new BeanDefinitionHolder(beanDef, beanName), this.registry, + proxyMode == ScopedProxyMode.TARGET_CLASS); + beanDefToRegister = new ConfigurationClassBeanDefinition( + (RootBeanDefinition) proxyDef.getBeanDefinition(), configClass, metadata, beanName); + } + + if (logger.isTraceEnabled()) { + logger.trace(String.format("Registering bean definition for @Bean method %s.%s()", + configClass.getMetadata().getClassName(), beanName)); + } + this.registry.registerBeanDefinition(beanName, beanDefToRegister); + } + + protected boolean isOverriddenByExistingDefinition(BeanMethod beanMethod, String beanName) { + if (!this.registry.containsBeanDefinition(beanName)) { + return false; + } + BeanDefinition existingBeanDef = this.registry.getBeanDefinition(beanName); + + // Is the existing bean definition one that was created from a configuration class? + // -> allow the current bean method to override, since both are at second-pass level. + // However, if the bean method is an overloaded case on the same configuration class, + // preserve the existing bean definition. + if (existingBeanDef instanceof ConfigurationClassBeanDefinition) { + ConfigurationClassBeanDefinition ccbd = (ConfigurationClassBeanDefinition) existingBeanDef; + if (ccbd.getMetadata().getClassName().equals( + beanMethod.getConfigurationClass().getMetadata().getClassName())) { + if (ccbd.getFactoryMethodMetadata().getMethodName().equals(ccbd.getFactoryMethodName())) { + ccbd.setNonUniqueFactoryMethodName(ccbd.getFactoryMethodMetadata().getMethodName()); + } + return true; + } + else { + return false; + } + } + + // A bean definition resulting from a component scan can be silently overridden + // by an @Bean method, as of 4.2... + if (existingBeanDef instanceof ScannedGenericBeanDefinition) { + return false; + } + + // Has the existing bean definition bean marked as a framework-generated bean? + // -> allow the current bean method to override it, since it is application-level + if (existingBeanDef.getRole() > BeanDefinition.ROLE_APPLICATION) { + return false; + } + + // At this point, it's a top-level override (probably XML), just having been parsed + // before configuration class processing kicks in... + if (this.registry instanceof DefaultListableBeanFactory && + !((DefaultListableBeanFactory) this.registry).isAllowBeanDefinitionOverriding()) { + throw new BeanDefinitionStoreException(beanMethod.getConfigurationClass().getResource().getDescription(), + beanName, "@Bean definition illegally overridden by existing bean definition: " + existingBeanDef); + } + if (logger.isDebugEnabled()) { + logger.debug(String.format("Skipping bean definition for %s: a definition for bean '%s' " + + "already exists. This top-level bean definition is considered as an override.", + beanMethod, beanName)); + } + return true; + } + + private void loadBeanDefinitionsFromImportedResources( + Map> importedResources) { + + Map, BeanDefinitionReader> readerInstanceCache = new HashMap<>(); + + importedResources.forEach((resource, readerClass) -> { + // Default reader selection necessary? + if (BeanDefinitionReader.class == readerClass) { + if (StringUtils.endsWithIgnoreCase(resource, ".groovy")) { + // When clearly asking for Groovy, that's what they'll get... + readerClass = GroovyBeanDefinitionReader.class; + } + else if (shouldIgnoreXml) { + throw new UnsupportedOperationException("XML support disabled"); + } + else { + // Primarily ".xml" files but for any other extension as well + readerClass = XmlBeanDefinitionReader.class; + } + } + + BeanDefinitionReader reader = readerInstanceCache.get(readerClass); + if (reader == null) { + try { + // Instantiate the specified BeanDefinitionReader + reader = readerClass.getConstructor(BeanDefinitionRegistry.class).newInstance(this.registry); + // Delegate the current ResourceLoader to it if possible + if (reader instanceof AbstractBeanDefinitionReader) { + AbstractBeanDefinitionReader abdr = ((AbstractBeanDefinitionReader) reader); + abdr.setResourceLoader(this.resourceLoader); + abdr.setEnvironment(this.environment); + } + readerInstanceCache.put(readerClass, reader); + } + catch (Throwable ex) { + throw new IllegalStateException( + "Could not instantiate BeanDefinitionReader class [" + readerClass.getName() + "]"); + } + } + + // TODO SPR-6310: qualify relative path locations as done in AbstractContextLoader.modifyLocations + reader.loadBeanDefinitions(resource); + }); + } + + private void loadBeanDefinitionsFromRegistrars(Map registrars) { + registrars.forEach((registrar, metadata) -> + registrar.registerBeanDefinitions(metadata, this.registry, this.importBeanNameGenerator)); + } + + + /** + * {@link RootBeanDefinition} marker subclass used to signify that a bean definition + * was created from a configuration class as opposed to any other configuration source. + * Used in bean overriding cases where it's necessary to determine whether the bean + * definition was created externally. + */ + @SuppressWarnings("serial") + private static class ConfigurationClassBeanDefinition extends RootBeanDefinition implements AnnotatedBeanDefinition { + + private final AnnotationMetadata annotationMetadata; + + private final MethodMetadata factoryMethodMetadata; + + private final String derivedBeanName; + + public ConfigurationClassBeanDefinition( + ConfigurationClass configClass, MethodMetadata beanMethodMetadata, String derivedBeanName) { + + this.annotationMetadata = configClass.getMetadata(); + this.factoryMethodMetadata = beanMethodMetadata; + this.derivedBeanName = derivedBeanName; + setResource(configClass.getResource()); + setLenientConstructorResolution(false); + } + + public ConfigurationClassBeanDefinition(RootBeanDefinition original, + ConfigurationClass configClass, MethodMetadata beanMethodMetadata, String derivedBeanName) { + super(original); + this.annotationMetadata = configClass.getMetadata(); + this.factoryMethodMetadata = beanMethodMetadata; + this.derivedBeanName = derivedBeanName; + } + + private ConfigurationClassBeanDefinition(ConfigurationClassBeanDefinition original) { + super(original); + this.annotationMetadata = original.annotationMetadata; + this.factoryMethodMetadata = original.factoryMethodMetadata; + this.derivedBeanName = original.derivedBeanName; + } + + @Override + public AnnotationMetadata getMetadata() { + return this.annotationMetadata; + } + + @Override + @NonNull + public MethodMetadata getFactoryMethodMetadata() { + return this.factoryMethodMetadata; + } + + @Override + public boolean isFactoryMethod(Method candidate) { + return (super.isFactoryMethod(candidate) && BeanAnnotationHelper.isBeanAnnotated(candidate) && + BeanAnnotationHelper.determineBeanNameFor(candidate).equals(this.derivedBeanName)); + } + + @Override + public ConfigurationClassBeanDefinition cloneBeanDefinition() { + return new ConfigurationClassBeanDefinition(this); + } + } + + + /** + * Evaluate {@code @Conditional} annotations, tracking results and taking into + * account 'imported by'. + */ + private class TrackedConditionEvaluator { + + private final Map skipped = new HashMap<>(); + + public boolean shouldSkip(ConfigurationClass configClass) { + Boolean skip = this.skipped.get(configClass); + if (skip == null) { + if (configClass.isImported()) { + boolean allSkipped = true; + for (ConfigurationClass importedBy : configClass.getImportedBy()) { + if (!shouldSkip(importedBy)) { + allSkipped = false; + break; + } + } + if (allSkipped) { + // The config classes that imported this one were all skipped, therefore we are skipped... + skip = true; + } + } + if (skip == null) { + skip = conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN); + } + this.skipped.put(configClass, skip); + } + return skip; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java new file mode 100644 index 0000000..1707952 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java @@ -0,0 +1,549 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; +import java.util.Arrays; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aop.scope.ScopedProxyFactoryBean; +import org.springframework.asm.Type; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.support.SimpleInstantiationStrategy; +import org.springframework.cglib.core.ClassGenerator; +import org.springframework.cglib.core.ClassLoaderAwareGeneratorStrategy; +import org.springframework.cglib.core.Constants; +import org.springframework.cglib.core.SpringNamingPolicy; +import org.springframework.cglib.proxy.Callback; +import org.springframework.cglib.proxy.CallbackFilter; +import org.springframework.cglib.proxy.Enhancer; +import org.springframework.cglib.proxy.Factory; +import org.springframework.cglib.proxy.MethodInterceptor; +import org.springframework.cglib.proxy.MethodProxy; +import org.springframework.cglib.proxy.NoOp; +import org.springframework.cglib.transform.ClassEmitterTransformer; +import org.springframework.cglib.transform.TransformingClassGenerator; +import org.springframework.lang.Nullable; +import org.springframework.objenesis.ObjenesisException; +import org.springframework.objenesis.SpringObjenesis; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; + +/** + * Enhances {@link Configuration} classes by generating a CGLIB subclass which + * interacts with the Spring container to respect bean scoping semantics for + * {@code @Bean} methods. Each such {@code @Bean} method will be overridden in + * the generated subclass, only delegating to the actual {@code @Bean} method + * implementation if the container actually requests the construction of a new + * instance. Otherwise, a call to such an {@code @Bean} method serves as a + * reference back to the container, obtaining the corresponding bean by name. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.0 + * @see #enhance + * @see ConfigurationClassPostProcessor + */ +class ConfigurationClassEnhancer { + + // The callbacks to use. Note that these callbacks must be stateless. + private static final Callback[] CALLBACKS = new Callback[] { + new BeanMethodInterceptor(), + new BeanFactoryAwareMethodInterceptor(), + NoOp.INSTANCE + }; + + private static final ConditionalCallbackFilter CALLBACK_FILTER = new ConditionalCallbackFilter(CALLBACKS); + + private static final String BEAN_FACTORY_FIELD = "$$beanFactory"; + + + private static final Log logger = LogFactory.getLog(ConfigurationClassEnhancer.class); + + private static final SpringObjenesis objenesis = new SpringObjenesis(); + + + /** + * Loads the specified class and generates a CGLIB subclass of it equipped with + * container-aware callbacks capable of respecting scoping and other bean semantics. + * @return the enhanced subclass + */ + public Class enhance(Class configClass, @Nullable ClassLoader classLoader) { + if (EnhancedConfiguration.class.isAssignableFrom(configClass)) { + if (logger.isDebugEnabled()) { + logger.debug(String.format("Ignoring request to enhance %s as it has " + + "already been enhanced. This usually indicates that more than one " + + "ConfigurationClassPostProcessor has been registered (e.g. via " + + "). This is harmless, but you may " + + "want check your configuration and remove one CCPP if possible", + configClass.getName())); + } + return configClass; + } + Class enhancedClass = createClass(newEnhancer(configClass, classLoader)); + if (logger.isTraceEnabled()) { + logger.trace(String.format("Successfully enhanced %s; enhanced class name is: %s", + configClass.getName(), enhancedClass.getName())); + } + return enhancedClass; + } + + /** + * Creates a new CGLIB {@link Enhancer} instance. + */ + private Enhancer newEnhancer(Class configSuperClass, @Nullable ClassLoader classLoader) { + Enhancer enhancer = new Enhancer(); + enhancer.setSuperclass(configSuperClass); + enhancer.setInterfaces(new Class[] {EnhancedConfiguration.class}); + enhancer.setUseFactory(false); + enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); + enhancer.setStrategy(new BeanFactoryAwareGeneratorStrategy(classLoader)); + enhancer.setCallbackFilter(CALLBACK_FILTER); + enhancer.setCallbackTypes(CALLBACK_FILTER.getCallbackTypes()); + return enhancer; + } + + /** + * Uses enhancer to generate a subclass of superclass, + * ensuring that callbacks are registered for the new subclass. + */ + private Class createClass(Enhancer enhancer) { + Class subclass = enhancer.createClass(); + // Registering callbacks statically (as opposed to thread-local) + // is critical for usage in an OSGi environment (SPR-5932)... + Enhancer.registerStaticCallbacks(subclass, CALLBACKS); + return subclass; + } + + + /** + * Marker interface to be implemented by all @Configuration CGLIB subclasses. + * Facilitates idempotent behavior for {@link ConfigurationClassEnhancer#enhance} + * through checking to see if candidate classes are already assignable to it, e.g. + * have already been enhanced. + *

    Also extends {@link BeanFactoryAware}, as all enhanced {@code @Configuration} + * classes require access to the {@link BeanFactory} that created them. + *

    Note that this interface is intended for framework-internal use only, however + * must remain public in order to allow access to subclasses generated from other + * packages (i.e. user code). + */ + public interface EnhancedConfiguration extends BeanFactoryAware { + } + + + /** + * Conditional {@link Callback}. + * @see ConditionalCallbackFilter + */ + private interface ConditionalCallback extends Callback { + + boolean isMatch(Method candidateMethod); + } + + + /** + * A {@link CallbackFilter} that works by interrogating {@link Callback Callbacks} in the order + * that they are defined via {@link ConditionalCallback}. + */ + private static class ConditionalCallbackFilter implements CallbackFilter { + + private final Callback[] callbacks; + + private final Class[] callbackTypes; + + public ConditionalCallbackFilter(Callback[] callbacks) { + this.callbacks = callbacks; + this.callbackTypes = new Class[callbacks.length]; + for (int i = 0; i < callbacks.length; i++) { + this.callbackTypes[i] = callbacks[i].getClass(); + } + } + + @Override + public int accept(Method method) { + for (int i = 0; i < this.callbacks.length; i++) { + Callback callback = this.callbacks[i]; + if (!(callback instanceof ConditionalCallback) || ((ConditionalCallback) callback).isMatch(method)) { + return i; + } + } + throw new IllegalStateException("No callback available for method " + method.getName()); + } + + public Class[] getCallbackTypes() { + return this.callbackTypes; + } + } + + + /** + * Custom extension of CGLIB's DefaultGeneratorStrategy, introducing a {@link BeanFactory} field. + * Also exposes the application ClassLoader as thread context ClassLoader for the time of + * class generation (in order for ASM to pick it up when doing common superclass resolution). + */ + private static class BeanFactoryAwareGeneratorStrategy extends + ClassLoaderAwareGeneratorStrategy { + + public BeanFactoryAwareGeneratorStrategy(@Nullable ClassLoader classLoader) { + super(classLoader); + } + + @Override + protected ClassGenerator transform(ClassGenerator cg) throws Exception { + ClassEmitterTransformer transformer = new ClassEmitterTransformer() { + @Override + public void end_class() { + declare_field(Constants.ACC_PUBLIC, BEAN_FACTORY_FIELD, Type.getType(BeanFactory.class), null); + super.end_class(); + } + }; + return new TransformingClassGenerator(cg, transformer); + } + + } + + + /** + * Intercepts the invocation of any {@link BeanFactoryAware#setBeanFactory(BeanFactory)} on + * {@code @Configuration} class instances for the purpose of recording the {@link BeanFactory}. + * @see EnhancedConfiguration + */ + private static class BeanFactoryAwareMethodInterceptor implements MethodInterceptor, ConditionalCallback { + + @Override + @Nullable + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + Field field = ReflectionUtils.findField(obj.getClass(), BEAN_FACTORY_FIELD); + Assert.state(field != null, "Unable to find generated BeanFactory field"); + field.set(obj, args[0]); + + // Does the actual (non-CGLIB) superclass implement BeanFactoryAware? + // If so, call its setBeanFactory() method. If not, just exit. + if (BeanFactoryAware.class.isAssignableFrom(ClassUtils.getUserClass(obj.getClass().getSuperclass()))) { + return proxy.invokeSuper(obj, args); + } + return null; + } + + @Override + public boolean isMatch(Method candidateMethod) { + return isSetBeanFactory(candidateMethod); + } + + public static boolean isSetBeanFactory(Method candidateMethod) { + return (candidateMethod.getName().equals("setBeanFactory") && + candidateMethod.getParameterCount() == 1 && + BeanFactory.class == candidateMethod.getParameterTypes()[0] && + BeanFactoryAware.class.isAssignableFrom(candidateMethod.getDeclaringClass())); + } + } + + + /** + * Intercepts the invocation of any {@link Bean}-annotated methods in order to ensure proper + * handling of bean semantics such as scoping and AOP proxying. + * @see Bean + * @see ConfigurationClassEnhancer + */ + private static class BeanMethodInterceptor implements MethodInterceptor, ConditionalCallback { + + /** + * Enhance a {@link Bean @Bean} method to check the supplied BeanFactory for the + * existence of this bean object. + * @throws Throwable as a catch-all for any exception that may be thrown when invoking the + * super implementation of the proxied method i.e., the actual {@code @Bean} method + */ + @Override + @Nullable + public Object intercept(Object enhancedConfigInstance, Method beanMethod, Object[] beanMethodArgs, + MethodProxy cglibMethodProxy) throws Throwable { + + ConfigurableBeanFactory beanFactory = getBeanFactory(enhancedConfigInstance); + String beanName = BeanAnnotationHelper.determineBeanNameFor(beanMethod); + + // Determine whether this bean is a scoped-proxy + if (BeanAnnotationHelper.isScopedProxy(beanMethod)) { + String scopedBeanName = ScopedProxyCreator.getTargetBeanName(beanName); + if (beanFactory.isCurrentlyInCreation(scopedBeanName)) { + beanName = scopedBeanName; + } + } + + // To handle the case of an inter-bean method reference, we must explicitly check the + // container for already cached instances. + + // First, check to see if the requested bean is a FactoryBean. If so, create a subclass + // proxy that intercepts calls to getObject() and returns any cached bean instance. + // This ensures that the semantics of calling a FactoryBean from within @Bean methods + // is the same as that of referring to a FactoryBean within XML. See SPR-6602. + if (factoryContainsBean(beanFactory, BeanFactory.FACTORY_BEAN_PREFIX + beanName) && + factoryContainsBean(beanFactory, beanName)) { + Object factoryBean = beanFactory.getBean(BeanFactory.FACTORY_BEAN_PREFIX + beanName); + if (factoryBean instanceof ScopedProxyFactoryBean) { + // Scoped proxy factory beans are a special case and should not be further proxied + } + else { + // It is a candidate FactoryBean - go ahead with enhancement + return enhanceFactoryBean(factoryBean, beanMethod.getReturnType(), beanFactory, beanName); + } + } + + if (isCurrentlyInvokedFactoryMethod(beanMethod)) { + // The factory is calling the bean method in order to instantiate and register the bean + // (i.e. via a getBean() call) -> invoke the super implementation of the method to actually + // create the bean instance. + if (logger.isInfoEnabled() && + BeanFactoryPostProcessor.class.isAssignableFrom(beanMethod.getReturnType())) { + logger.info(String.format("@Bean method %s.%s is non-static and returns an object " + + "assignable to Spring's BeanFactoryPostProcessor interface. This will " + + "result in a failure to process annotations such as @Autowired, " + + "@Resource and @PostConstruct within the method's declaring " + + "@Configuration class. Add the 'static' modifier to this method to avoid " + + "these container lifecycle issues; see @Bean javadoc for complete details.", + beanMethod.getDeclaringClass().getSimpleName(), beanMethod.getName())); + } + return cglibMethodProxy.invokeSuper(enhancedConfigInstance, beanMethodArgs); + } + + return resolveBeanReference(beanMethod, beanMethodArgs, beanFactory, beanName); + } + + private Object resolveBeanReference(Method beanMethod, Object[] beanMethodArgs, + ConfigurableBeanFactory beanFactory, String beanName) { + + // The user (i.e. not the factory) is requesting this bean through a call to + // the bean method, direct or indirect. The bean may have already been marked + // as 'in creation' in certain autowiring scenarios; if so, temporarily set + // the in-creation status to false in order to avoid an exception. + boolean alreadyInCreation = beanFactory.isCurrentlyInCreation(beanName); + try { + if (alreadyInCreation) { + beanFactory.setCurrentlyInCreation(beanName, false); + } + boolean useArgs = !ObjectUtils.isEmpty(beanMethodArgs); + if (useArgs && beanFactory.isSingleton(beanName)) { + // Stubbed null arguments just for reference purposes, + // expecting them to be autowired for regular singleton references? + // A safe assumption since @Bean singleton arguments cannot be optional... + for (Object arg : beanMethodArgs) { + if (arg == null) { + useArgs = false; + break; + } + } + } + Object beanInstance = (useArgs ? beanFactory.getBean(beanName, beanMethodArgs) : + beanFactory.getBean(beanName)); + if (!ClassUtils.isAssignableValue(beanMethod.getReturnType(), beanInstance)) { + // Detect package-protected NullBean instance through equals(null) check + if (beanInstance.equals(null)) { + if (logger.isDebugEnabled()) { + logger.debug(String.format("@Bean method %s.%s called as bean reference " + + "for type [%s] returned null bean; resolving to null value.", + beanMethod.getDeclaringClass().getSimpleName(), beanMethod.getName(), + beanMethod.getReturnType().getName())); + } + beanInstance = null; + } + else { + String msg = String.format("@Bean method %s.%s called as bean reference " + + "for type [%s] but overridden by non-compatible bean instance of type [%s].", + beanMethod.getDeclaringClass().getSimpleName(), beanMethod.getName(), + beanMethod.getReturnType().getName(), beanInstance.getClass().getName()); + try { + BeanDefinition beanDefinition = beanFactory.getMergedBeanDefinition(beanName); + msg += " Overriding bean of same name declared in: " + beanDefinition.getResourceDescription(); + } + catch (NoSuchBeanDefinitionException ex) { + // Ignore - simply no detailed message then. + } + throw new IllegalStateException(msg); + } + } + Method currentlyInvoked = SimpleInstantiationStrategy.getCurrentlyInvokedFactoryMethod(); + if (currentlyInvoked != null) { + String outerBeanName = BeanAnnotationHelper.determineBeanNameFor(currentlyInvoked); + beanFactory.registerDependentBean(beanName, outerBeanName); + } + return beanInstance; + } + finally { + if (alreadyInCreation) { + beanFactory.setCurrentlyInCreation(beanName, true); + } + } + } + + @Override + public boolean isMatch(Method candidateMethod) { + return (candidateMethod.getDeclaringClass() != Object.class && + !BeanFactoryAwareMethodInterceptor.isSetBeanFactory(candidateMethod) && + BeanAnnotationHelper.isBeanAnnotated(candidateMethod)); + } + + private ConfigurableBeanFactory getBeanFactory(Object enhancedConfigInstance) { + Field field = ReflectionUtils.findField(enhancedConfigInstance.getClass(), BEAN_FACTORY_FIELD); + Assert.state(field != null, "Unable to find generated bean factory field"); + Object beanFactory = ReflectionUtils.getField(field, enhancedConfigInstance); + Assert.state(beanFactory != null, "BeanFactory has not been injected into @Configuration class"); + Assert.state(beanFactory instanceof ConfigurableBeanFactory, + "Injected BeanFactory is not a ConfigurableBeanFactory"); + return (ConfigurableBeanFactory) beanFactory; + } + + /** + * Check the BeanFactory to see whether the bean named beanName already + * exists. Accounts for the fact that the requested bean may be "in creation", i.e.: + * we're in the middle of servicing the initial request for this bean. From an enhanced + * factory method's perspective, this means that the bean does not actually yet exist, + * and that it is now our job to create it for the first time by executing the logic + * in the corresponding factory method. + *

    Said another way, this check repurposes + * {@link ConfigurableBeanFactory#isCurrentlyInCreation(String)} to determine whether + * the container is calling this method or the user is calling this method. + * @param beanName name of bean to check for + * @return whether beanName already exists in the factory + */ + private boolean factoryContainsBean(ConfigurableBeanFactory beanFactory, String beanName) { + return (beanFactory.containsBean(beanName) && !beanFactory.isCurrentlyInCreation(beanName)); + } + + /** + * Check whether the given method corresponds to the container's currently invoked + * factory method. Compares method name and parameter types only in order to work + * around a potential problem with covariant return types (currently only known + * to happen on Groovy classes). + */ + private boolean isCurrentlyInvokedFactoryMethod(Method method) { + Method currentlyInvoked = SimpleInstantiationStrategy.getCurrentlyInvokedFactoryMethod(); + return (currentlyInvoked != null && method.getName().equals(currentlyInvoked.getName()) && + Arrays.equals(method.getParameterTypes(), currentlyInvoked.getParameterTypes())); + } + + /** + * Create a subclass proxy that intercepts calls to getObject(), delegating to the current BeanFactory + * instead of creating a new instance. These proxies are created only when calling a FactoryBean from + * within a Bean method, allowing for proper scoping semantics even when working against the FactoryBean + * instance directly. If a FactoryBean instance is fetched through the container via &-dereferencing, + * it will not be proxied. This too is aligned with the way XML configuration works. + */ + private Object enhanceFactoryBean(final Object factoryBean, Class exposedType, + final ConfigurableBeanFactory beanFactory, final String beanName) { + + try { + Class clazz = factoryBean.getClass(); + boolean finalClass = Modifier.isFinal(clazz.getModifiers()); + boolean finalMethod = Modifier.isFinal(clazz.getMethod("getObject").getModifiers()); + if (finalClass || finalMethod) { + if (exposedType.isInterface()) { + if (logger.isTraceEnabled()) { + logger.trace("Creating interface proxy for FactoryBean '" + beanName + "' of type [" + + clazz.getName() + "] for use within another @Bean method because its " + + (finalClass ? "implementation class" : "getObject() method") + + " is final: Otherwise a getObject() call would not be routed to the factory."); + } + return createInterfaceProxyForFactoryBean(factoryBean, exposedType, beanFactory, beanName); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Unable to proxy FactoryBean '" + beanName + "' of type [" + + clazz.getName() + "] for use within another @Bean method because its " + + (finalClass ? "implementation class" : "getObject() method") + + " is final: A getObject() call will NOT be routed to the factory. " + + "Consider declaring the return type as a FactoryBean interface."); + } + return factoryBean; + } + } + } + catch (NoSuchMethodException ex) { + // No getObject() method -> shouldn't happen, but as long as nobody is trying to call it... + } + + return createCglibProxyForFactoryBean(factoryBean, beanFactory, beanName); + } + + private Object createInterfaceProxyForFactoryBean(final Object factoryBean, Class interfaceType, + final ConfigurableBeanFactory beanFactory, final String beanName) { + + return Proxy.newProxyInstance( + factoryBean.getClass().getClassLoader(), new Class[] {interfaceType}, + (proxy, method, args) -> { + if (method.getName().equals("getObject") && args == null) { + return beanFactory.getBean(beanName); + } + return ReflectionUtils.invokeMethod(method, factoryBean, args); + }); + } + + private Object createCglibProxyForFactoryBean(final Object factoryBean, + final ConfigurableBeanFactory beanFactory, final String beanName) { + + Enhancer enhancer = new Enhancer(); + enhancer.setSuperclass(factoryBean.getClass()); + enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); + enhancer.setCallbackType(MethodInterceptor.class); + + // Ideally create enhanced FactoryBean proxy without constructor side effects, + // analogous to AOP proxy creation in ObjenesisCglibAopProxy... + Class fbClass = enhancer.createClass(); + Object fbProxy = null; + + if (objenesis.isWorthTrying()) { + try { + fbProxy = objenesis.newInstance(fbClass, enhancer.getUseCache()); + } + catch (ObjenesisException ex) { + logger.debug("Unable to instantiate enhanced FactoryBean using Objenesis, " + + "falling back to regular construction", ex); + } + } + + if (fbProxy == null) { + try { + fbProxy = ReflectionUtils.accessibleConstructor(fbClass).newInstance(); + } + catch (Throwable ex) { + throw new IllegalStateException("Unable to instantiate enhanced FactoryBean using Objenesis, " + + "and regular FactoryBean instantiation via default constructor fails as well", ex); + } + } + + ((Factory) fbProxy).setCallback(0, (MethodInterceptor) (obj, method, args, proxy) -> { + if (method.getName().equals("getObject") && args.length == 0) { + return beanFactory.getBean(beanName); + } + return proxy.invoke(factoryBean, args); + }); + + return fbProxy; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java new file mode 100644 index 0000000..4230786 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java @@ -0,0 +1,1125 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Deque; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringJoiner; +import java.util.function.Predicate; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.parsing.Location; +import org.springframework.beans.factory.parsing.Problem; +import org.springframework.beans.factory.parsing.ProblemReporter; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionReader; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.context.annotation.ConfigurationCondition.ConfigurationPhase; +import org.springframework.context.annotation.DeferredImportSelector.Group; +import org.springframework.core.NestedIOException; +import org.springframework.core.OrderComparator; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.env.CompositePropertySource; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.DefaultPropertySourceFactory; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.core.io.support.PropertySourceFactory; +import org.springframework.core.io.support.ResourcePropertySource; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.MethodMetadata; +import org.springframework.core.type.StandardAnnotationMetadata; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.AssignableTypeFilter; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; + +/** + * Parses a {@link Configuration} class definition, populating a collection of + * {@link ConfigurationClass} objects (parsing a single Configuration class may result in + * any number of ConfigurationClass objects because one Configuration class may import + * another using the {@link Import} annotation). + * + *

    This class helps separate the concern of parsing the structure of a Configuration + * class from the concern of registering BeanDefinition objects based on the content of + * that model (with the exception of {@code @ComponentScan} annotations which need to be + * registered immediately). + * + *

    This ASM-based implementation avoids reflection and eager class loading in order to + * interoperate effectively with lazy class loading in a Spring ApplicationContext. + * + * @author Chris Beams + * @author Juergen Hoeller + * @author Phillip Webb + * @author Sam Brannen + * @author Stephane Nicoll + * @since 3.0 + * @see ConfigurationClassBeanDefinitionReader + */ +class ConfigurationClassParser { + + private static final PropertySourceFactory DEFAULT_PROPERTY_SOURCE_FACTORY = new DefaultPropertySourceFactory(); + + private static final Predicate DEFAULT_EXCLUSION_FILTER = className -> + (className.startsWith("java.lang.annotation.") || className.startsWith("org.springframework.stereotype.")); + + private static final Comparator DEFERRED_IMPORT_COMPARATOR = + (o1, o2) -> AnnotationAwareOrderComparator.INSTANCE.compare(o1.getImportSelector(), o2.getImportSelector()); + + + private final Log logger = LogFactory.getLog(getClass()); + + private final MetadataReaderFactory metadataReaderFactory; + + private final ProblemReporter problemReporter; + + private final Environment environment; + + private final ResourceLoader resourceLoader; + + private final BeanDefinitionRegistry registry; + + private final ComponentScanAnnotationParser componentScanParser; + + private final ConditionEvaluator conditionEvaluator; + + private final Map configurationClasses = new LinkedHashMap<>(); + + private final Map knownSuperclasses = new HashMap<>(); + + private final List propertySourceNames = new ArrayList<>(); + + private final ImportStack importStack = new ImportStack(); + + private final DeferredImportSelectorHandler deferredImportSelectorHandler = new DeferredImportSelectorHandler(); + + private final SourceClass objectSourceClass = new SourceClass(Object.class); + + + /** + * Create a new {@link ConfigurationClassParser} instance that will be used + * to populate the set of configuration classes. + */ + public ConfigurationClassParser(MetadataReaderFactory metadataReaderFactory, + ProblemReporter problemReporter, Environment environment, ResourceLoader resourceLoader, + BeanNameGenerator componentScanBeanNameGenerator, BeanDefinitionRegistry registry) { + + this.metadataReaderFactory = metadataReaderFactory; + this.problemReporter = problemReporter; + this.environment = environment; + this.resourceLoader = resourceLoader; + this.registry = registry; + this.componentScanParser = new ComponentScanAnnotationParser( + environment, resourceLoader, componentScanBeanNameGenerator, registry); + this.conditionEvaluator = new ConditionEvaluator(registry, environment, resourceLoader); + } + + + public void parse(Set configCandidates) { + for (BeanDefinitionHolder holder : configCandidates) { + BeanDefinition bd = holder.getBeanDefinition(); + try { + if (bd instanceof AnnotatedBeanDefinition) { + parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName()); + } + else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) { + parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName()); + } + else { + parse(bd.getBeanClassName(), holder.getBeanName()); + } + } + catch (BeanDefinitionStoreException ex) { + throw ex; + } + catch (Throwable ex) { + throw new BeanDefinitionStoreException( + "Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex); + } + } + + this.deferredImportSelectorHandler.process(); + } + + protected final void parse(@Nullable String className, String beanName) throws IOException { + Assert.notNull(className, "No bean class name for configuration class bean definition"); + MetadataReader reader = this.metadataReaderFactory.getMetadataReader(className); + processConfigurationClass(new ConfigurationClass(reader, beanName), DEFAULT_EXCLUSION_FILTER); + } + + protected final void parse(Class clazz, String beanName) throws IOException { + processConfigurationClass(new ConfigurationClass(clazz, beanName), DEFAULT_EXCLUSION_FILTER); + } + + protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException { + processConfigurationClass(new ConfigurationClass(metadata, beanName), DEFAULT_EXCLUSION_FILTER); + } + + /** + * Validate each {@link ConfigurationClass} object. + * @see ConfigurationClass#validate + */ + public void validate() { + for (ConfigurationClass configClass : this.configurationClasses.keySet()) { + configClass.validate(this.problemReporter); + } + } + + public Set getConfigurationClasses() { + return this.configurationClasses.keySet(); + } + + + protected void processConfigurationClass(ConfigurationClass configClass, Predicate filter) throws IOException { + if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) { + return; + } + + ConfigurationClass existingClass = this.configurationClasses.get(configClass); + if (existingClass != null) { + if (configClass.isImported()) { + if (existingClass.isImported()) { + existingClass.mergeImportedBy(configClass); + } + // Otherwise ignore new imported config class; existing non-imported class overrides it. + return; + } + else { + // Explicit bean definition found, probably replacing an import. + // Let's remove the old one and go with the new one. + this.configurationClasses.remove(configClass); + this.knownSuperclasses.values().removeIf(configClass::equals); + } + } + + // Recursively process the configuration class and its superclass hierarchy. + SourceClass sourceClass = asSourceClass(configClass, filter); + do { + sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter); + } + while (sourceClass != null); + + this.configurationClasses.put(configClass, configClass); + } + + /** + * Apply processing and build a complete {@link ConfigurationClass} by reading the + * annotations, members and methods from the source class. This method can be called + * multiple times as relevant sources are discovered. + * @param configClass the configuration class being build + * @param sourceClass a source class + * @return the superclass, or {@code null} if none found or previously processed + */ + @Nullable + protected final SourceClass doProcessConfigurationClass( + ConfigurationClass configClass, SourceClass sourceClass, Predicate filter) + throws IOException { + + if (configClass.getMetadata().isAnnotated(Component.class.getName())) { + // Recursively process any member (nested) classes first + processMemberClasses(configClass, sourceClass, filter); + } + + // Process any @PropertySource annotations + for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable( + sourceClass.getMetadata(), PropertySources.class, + org.springframework.context.annotation.PropertySource.class)) { + if (this.environment instanceof ConfigurableEnvironment) { + processPropertySource(propertySource); + } + else { + logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() + + "]. Reason: Environment must implement ConfigurableEnvironment"); + } + } + + // Process any @ComponentScan annotations + Set componentScans = AnnotationConfigUtils.attributesForRepeatable( + sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class); + if (!componentScans.isEmpty() && + !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) { + for (AnnotationAttributes componentScan : componentScans) { + // The config class is annotated with @ComponentScan -> perform the scan immediately + Set scannedBeanDefinitions = + this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName()); + // Check the set of scanned definitions for any further config classes and parse recursively if needed + for (BeanDefinitionHolder holder : scannedBeanDefinitions) { + BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition(); + if (bdCand == null) { + bdCand = holder.getBeanDefinition(); + } + if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) { + parse(bdCand.getBeanClassName(), holder.getBeanName()); + } + } + } + } + + // Process any @Import annotations + processImports(configClass, sourceClass, getImports(sourceClass), filter, true); + + // Process any @ImportResource annotations + AnnotationAttributes importResource = + AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class); + if (importResource != null) { + String[] resources = importResource.getStringArray("locations"); + Class readerClass = importResource.getClass("reader"); + for (String resource : resources) { + String resolvedResource = this.environment.resolveRequiredPlaceholders(resource); + configClass.addImportedResource(resolvedResource, readerClass); + } + } + + // Process individual @Bean methods + Set beanMethods = retrieveBeanMethodMetadata(sourceClass); + for (MethodMetadata methodMetadata : beanMethods) { + configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass)); + } + + // Process default methods on interfaces + processInterfaces(configClass, sourceClass); + + // Process superclass, if any + if (sourceClass.getMetadata().hasSuperClass()) { + String superclass = sourceClass.getMetadata().getSuperClassName(); + if (superclass != null && !superclass.startsWith("java") && + !this.knownSuperclasses.containsKey(superclass)) { + this.knownSuperclasses.put(superclass, configClass); + // Superclass found, return its annotation metadata and recurse + return sourceClass.getSuperClass(); + } + } + + // No superclass -> processing is complete + return null; + } + + /** + * Register member (nested) classes that happen to be configuration classes themselves. + */ + private void processMemberClasses(ConfigurationClass configClass, SourceClass sourceClass, + Predicate filter) throws IOException { + + Collection memberClasses = sourceClass.getMemberClasses(); + if (!memberClasses.isEmpty()) { + List candidates = new ArrayList<>(memberClasses.size()); + for (SourceClass memberClass : memberClasses) { + if (ConfigurationClassUtils.isConfigurationCandidate(memberClass.getMetadata()) && + !memberClass.getMetadata().getClassName().equals(configClass.getMetadata().getClassName())) { + candidates.add(memberClass); + } + } + OrderComparator.sort(candidates); + for (SourceClass candidate : candidates) { + if (this.importStack.contains(configClass)) { + this.problemReporter.error(new CircularImportProblem(configClass, this.importStack)); + } + else { + this.importStack.push(configClass); + try { + processConfigurationClass(candidate.asConfigClass(configClass), filter); + } + finally { + this.importStack.pop(); + } + } + } + } + } + + /** + * Register default methods on interfaces implemented by the configuration class. + */ + private void processInterfaces(ConfigurationClass configClass, SourceClass sourceClass) throws IOException { + for (SourceClass ifc : sourceClass.getInterfaces()) { + Set beanMethods = retrieveBeanMethodMetadata(ifc); + for (MethodMetadata methodMetadata : beanMethods) { + if (!methodMetadata.isAbstract()) { + // A default method or other concrete method on a Java 8+ interface... + configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass)); + } + } + processInterfaces(configClass, ifc); + } + } + + /** + * Retrieve the metadata for all @Bean methods. + */ + private Set retrieveBeanMethodMetadata(SourceClass sourceClass) { + AnnotationMetadata original = sourceClass.getMetadata(); + Set beanMethods = original.getAnnotatedMethods(Bean.class.getName()); + if (beanMethods.size() > 1 && original instanceof StandardAnnotationMetadata) { + // Try reading the class file via ASM for deterministic declaration order... + // Unfortunately, the JVM's standard reflection returns methods in arbitrary + // order, even between different runs of the same application on the same JVM. + try { + AnnotationMetadata asm = + this.metadataReaderFactory.getMetadataReader(original.getClassName()).getAnnotationMetadata(); + Set asmMethods = asm.getAnnotatedMethods(Bean.class.getName()); + if (asmMethods.size() >= beanMethods.size()) { + Set selectedMethods = new LinkedHashSet<>(asmMethods.size()); + for (MethodMetadata asmMethod : asmMethods) { + for (MethodMetadata beanMethod : beanMethods) { + if (beanMethod.getMethodName().equals(asmMethod.getMethodName())) { + selectedMethods.add(beanMethod); + break; + } + } + } + if (selectedMethods.size() == beanMethods.size()) { + // All reflection-detected methods found in ASM method set -> proceed + beanMethods = selectedMethods; + } + } + } + catch (IOException ex) { + logger.debug("Failed to read class file via ASM for determining @Bean method order", ex); + // No worries, let's continue with the reflection metadata we started with... + } + } + return beanMethods; + } + + + /** + * Process the given @PropertySource annotation metadata. + * @param propertySource metadata for the @PropertySource annotation found + * @throws IOException if loading a property source failed + */ + private void processPropertySource(AnnotationAttributes propertySource) throws IOException { + String name = propertySource.getString("name"); + if (!StringUtils.hasLength(name)) { + name = null; + } + String encoding = propertySource.getString("encoding"); + if (!StringUtils.hasLength(encoding)) { + encoding = null; + } + String[] locations = propertySource.getStringArray("value"); + Assert.isTrue(locations.length > 0, "At least one @PropertySource(value) location is required"); + boolean ignoreResourceNotFound = propertySource.getBoolean("ignoreResourceNotFound"); + + Class factoryClass = propertySource.getClass("factory"); + PropertySourceFactory factory = (factoryClass == PropertySourceFactory.class ? + DEFAULT_PROPERTY_SOURCE_FACTORY : BeanUtils.instantiateClass(factoryClass)); + + for (String location : locations) { + try { + String resolvedLocation = this.environment.resolveRequiredPlaceholders(location); + Resource resource = this.resourceLoader.getResource(resolvedLocation); + addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding))); + } + catch (IllegalArgumentException | FileNotFoundException | UnknownHostException | SocketException ex) { + // Placeholders not resolvable or resource not found when trying to open it + if (ignoreResourceNotFound) { + if (logger.isInfoEnabled()) { + logger.info("Properties location [" + location + "] not resolvable: " + ex.getMessage()); + } + } + else { + throw ex; + } + } + } + } + + private void addPropertySource(PropertySource propertySource) { + String name = propertySource.getName(); + MutablePropertySources propertySources = ((ConfigurableEnvironment) this.environment).getPropertySources(); + + if (this.propertySourceNames.contains(name)) { + // We've already added a version, we need to extend it + PropertySource existing = propertySources.get(name); + if (existing != null) { + PropertySource newSource = (propertySource instanceof ResourcePropertySource ? + ((ResourcePropertySource) propertySource).withResourceName() : propertySource); + if (existing instanceof CompositePropertySource) { + ((CompositePropertySource) existing).addFirstPropertySource(newSource); + } + else { + if (existing instanceof ResourcePropertySource) { + existing = ((ResourcePropertySource) existing).withResourceName(); + } + CompositePropertySource composite = new CompositePropertySource(name); + composite.addPropertySource(newSource); + composite.addPropertySource(existing); + propertySources.replace(name, composite); + } + return; + } + } + + if (this.propertySourceNames.isEmpty()) { + propertySources.addLast(propertySource); + } + else { + String firstProcessed = this.propertySourceNames.get(this.propertySourceNames.size() - 1); + propertySources.addBefore(firstProcessed, propertySource); + } + this.propertySourceNames.add(name); + } + + + /** + * Returns {@code @Import} class, considering all meta-annotations. + */ + private Set getImports(SourceClass sourceClass) throws IOException { + Set imports = new LinkedHashSet<>(); + Set visited = new LinkedHashSet<>(); + collectImports(sourceClass, imports, visited); + return imports; + } + + /** + * Recursively collect all declared {@code @Import} values. Unlike most + * meta-annotations it is valid to have several {@code @Import}s declared with + * different values; the usual process of returning values from the first + * meta-annotation on a class is not sufficient. + *

    For example, it is common for a {@code @Configuration} class to declare direct + * {@code @Import}s in addition to meta-imports originating from an {@code @Enable} + * annotation. + * @param sourceClass the class to search + * @param imports the imports collected so far + * @param visited used to track visited classes to prevent infinite recursion + * @throws IOException if there is any problem reading metadata from the named class + */ + private void collectImports(SourceClass sourceClass, Set imports, Set visited) + throws IOException { + + if (visited.add(sourceClass)) { + for (SourceClass annotation : sourceClass.getAnnotations()) { + String annName = annotation.getMetadata().getClassName(); + if (!annName.equals(Import.class.getName())) { + collectImports(annotation, imports, visited); + } + } + imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value")); + } + } + + private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass, + Collection importCandidates, Predicate exclusionFilter, + boolean checkForCircularImports) { + + if (importCandidates.isEmpty()) { + return; + } + + if (checkForCircularImports && isChainedImportOnStack(configClass)) { + this.problemReporter.error(new CircularImportProblem(configClass, this.importStack)); + } + else { + this.importStack.push(configClass); + try { + for (SourceClass candidate : importCandidates) { + if (candidate.isAssignable(ImportSelector.class)) { + // Candidate class is an ImportSelector -> delegate to it to determine imports + Class candidateClass = candidate.loadClass(); + ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class, + this.environment, this.resourceLoader, this.registry); + Predicate selectorFilter = selector.getExclusionFilter(); + if (selectorFilter != null) { + exclusionFilter = exclusionFilter.or(selectorFilter); + } + if (selector instanceof DeferredImportSelector) { + this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector); + } + else { + String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata()); + Collection importSourceClasses = asSourceClasses(importClassNames, exclusionFilter); + processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false); + } + } + else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) { + // Candidate class is an ImportBeanDefinitionRegistrar -> + // delegate to it to register additional bean definitions + Class candidateClass = candidate.loadClass(); + ImportBeanDefinitionRegistrar registrar = + ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class, + this.environment, this.resourceLoader, this.registry); + configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata()); + } + else { + // Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar -> + // process it as an @Configuration class + this.importStack.registerImport( + currentSourceClass.getMetadata(), candidate.getMetadata().getClassName()); + processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter); + } + } + } + catch (BeanDefinitionStoreException ex) { + throw ex; + } + catch (Throwable ex) { + throw new BeanDefinitionStoreException( + "Failed to process import candidates for configuration class [" + + configClass.getMetadata().getClassName() + "]", ex); + } + finally { + this.importStack.pop(); + } + } + } + + private boolean isChainedImportOnStack(ConfigurationClass configClass) { + if (this.importStack.contains(configClass)) { + String configClassName = configClass.getMetadata().getClassName(); + AnnotationMetadata importingClass = this.importStack.getImportingClassFor(configClassName); + while (importingClass != null) { + if (configClassName.equals(importingClass.getClassName())) { + return true; + } + importingClass = this.importStack.getImportingClassFor(importingClass.getClassName()); + } + } + return false; + } + + ImportRegistry getImportRegistry() { + return this.importStack; + } + + + /** + * Factory method to obtain a {@link SourceClass} from a {@link ConfigurationClass}. + */ + private SourceClass asSourceClass(ConfigurationClass configurationClass, Predicate filter) throws IOException { + AnnotationMetadata metadata = configurationClass.getMetadata(); + if (metadata instanceof StandardAnnotationMetadata) { + return asSourceClass(((StandardAnnotationMetadata) metadata).getIntrospectedClass(), filter); + } + return asSourceClass(metadata.getClassName(), filter); + } + + /** + * Factory method to obtain a {@link SourceClass} from a {@link Class}. + */ + SourceClass asSourceClass(@Nullable Class classType, Predicate filter) throws IOException { + if (classType == null || filter.test(classType.getName())) { + return this.objectSourceClass; + } + try { + // Sanity test that we can reflectively read annotations, + // including Class attributes; if not -> fall back to ASM + for (Annotation ann : classType.getDeclaredAnnotations()) { + AnnotationUtils.validateAnnotation(ann); + } + return new SourceClass(classType); + } + catch (Throwable ex) { + // Enforce ASM via class name resolution + return asSourceClass(classType.getName(), filter); + } + } + + /** + * Factory method to obtain a {@link SourceClass} collection from class names. + */ + private Collection asSourceClasses(String[] classNames, Predicate filter) throws IOException { + List annotatedClasses = new ArrayList<>(classNames.length); + for (String className : classNames) { + annotatedClasses.add(asSourceClass(className, filter)); + } + return annotatedClasses; + } + + /** + * Factory method to obtain a {@link SourceClass} from a class name. + */ + SourceClass asSourceClass(@Nullable String className, Predicate filter) throws IOException { + if (className == null || filter.test(className)) { + return this.objectSourceClass; + } + if (className.startsWith("java")) { + // Never use ASM for core java types + try { + return new SourceClass(ClassUtils.forName(className, this.resourceLoader.getClassLoader())); + } + catch (ClassNotFoundException ex) { + throw new NestedIOException("Failed to load class [" + className + "]", ex); + } + } + return new SourceClass(this.metadataReaderFactory.getMetadataReader(className)); + } + + + @SuppressWarnings("serial") + private static class ImportStack extends ArrayDeque implements ImportRegistry { + + private final MultiValueMap imports = new LinkedMultiValueMap<>(); + + public void registerImport(AnnotationMetadata importingClass, String importedClass) { + this.imports.add(importedClass, importingClass); + } + + @Override + @Nullable + public AnnotationMetadata getImportingClassFor(String importedClass) { + return CollectionUtils.lastElement(this.imports.get(importedClass)); + } + + @Override + public void removeImportingClass(String importingClass) { + for (List list : this.imports.values()) { + for (Iterator iterator = list.iterator(); iterator.hasNext();) { + if (iterator.next().getClassName().equals(importingClass)) { + iterator.remove(); + break; + } + } + } + } + + /** + * Given a stack containing (in order) + *

      + *
    • com.acme.Foo
    • + *
    • com.acme.Bar
    • + *
    • com.acme.Baz
    • + *
    + * return "[Foo->Bar->Baz]". + */ + @Override + public String toString() { + StringJoiner joiner = new StringJoiner("->", "[", "]"); + for (ConfigurationClass configurationClass : this) { + joiner.add(configurationClass.getSimpleName()); + } + return joiner.toString(); + } + } + + + private class DeferredImportSelectorHandler { + + @Nullable + private List deferredImportSelectors = new ArrayList<>(); + + /** + * Handle the specified {@link DeferredImportSelector}. If deferred import + * selectors are being collected, this registers this instance to the list. If + * they are being processed, the {@link DeferredImportSelector} is also processed + * immediately according to its {@link DeferredImportSelector.Group}. + * @param configClass the source configuration class + * @param importSelector the selector to handle + */ + public void handle(ConfigurationClass configClass, DeferredImportSelector importSelector) { + DeferredImportSelectorHolder holder = new DeferredImportSelectorHolder(configClass, importSelector); + if (this.deferredImportSelectors == null) { + DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler(); + handler.register(holder); + handler.processGroupImports(); + } + else { + this.deferredImportSelectors.add(holder); + } + } + + public void process() { + List deferredImports = this.deferredImportSelectors; + this.deferredImportSelectors = null; + try { + if (deferredImports != null) { + DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler(); + deferredImports.sort(DEFERRED_IMPORT_COMPARATOR); + deferredImports.forEach(handler::register); + handler.processGroupImports(); + } + } + finally { + this.deferredImportSelectors = new ArrayList<>(); + } + } + } + + + private class DeferredImportSelectorGroupingHandler { + + private final Map groupings = new LinkedHashMap<>(); + + private final Map configurationClasses = new HashMap<>(); + + public void register(DeferredImportSelectorHolder deferredImport) { + Class group = deferredImport.getImportSelector().getImportGroup(); + DeferredImportSelectorGrouping grouping = this.groupings.computeIfAbsent( + (group != null ? group : deferredImport), + key -> new DeferredImportSelectorGrouping(createGroup(group))); + grouping.add(deferredImport); + this.configurationClasses.put(deferredImport.getConfigurationClass().getMetadata(), + deferredImport.getConfigurationClass()); + } + + public void processGroupImports() { + for (DeferredImportSelectorGrouping grouping : this.groupings.values()) { + Predicate exclusionFilter = grouping.getCandidateFilter(); + grouping.getImports().forEach(entry -> { + ConfigurationClass configurationClass = this.configurationClasses.get(entry.getMetadata()); + try { + processImports(configurationClass, asSourceClass(configurationClass, exclusionFilter), + Collections.singleton(asSourceClass(entry.getImportClassName(), exclusionFilter)), + exclusionFilter, false); + } + catch (BeanDefinitionStoreException ex) { + throw ex; + } + catch (Throwable ex) { + throw new BeanDefinitionStoreException( + "Failed to process import candidates for configuration class [" + + configurationClass.getMetadata().getClassName() + "]", ex); + } + }); + } + } + + private Group createGroup(@Nullable Class type) { + Class effectiveType = (type != null ? type : DefaultDeferredImportSelectorGroup.class); + return ParserStrategyUtils.instantiateClass(effectiveType, Group.class, + ConfigurationClassParser.this.environment, + ConfigurationClassParser.this.resourceLoader, + ConfigurationClassParser.this.registry); + } + } + + + private static class DeferredImportSelectorHolder { + + private final ConfigurationClass configurationClass; + + private final DeferredImportSelector importSelector; + + public DeferredImportSelectorHolder(ConfigurationClass configClass, DeferredImportSelector selector) { + this.configurationClass = configClass; + this.importSelector = selector; + } + + public ConfigurationClass getConfigurationClass() { + return this.configurationClass; + } + + public DeferredImportSelector getImportSelector() { + return this.importSelector; + } + } + + + private static class DeferredImportSelectorGrouping { + + private final DeferredImportSelector.Group group; + + private final List deferredImports = new ArrayList<>(); + + DeferredImportSelectorGrouping(Group group) { + this.group = group; + } + + public void add(DeferredImportSelectorHolder deferredImport) { + this.deferredImports.add(deferredImport); + } + + /** + * Return the imports defined by the group. + * @return each import with its associated configuration class + */ + public Iterable getImports() { + for (DeferredImportSelectorHolder deferredImport : this.deferredImports) { + this.group.process(deferredImport.getConfigurationClass().getMetadata(), + deferredImport.getImportSelector()); + } + return this.group.selectImports(); + } + + public Predicate getCandidateFilter() { + Predicate mergedFilter = DEFAULT_EXCLUSION_FILTER; + for (DeferredImportSelectorHolder deferredImport : this.deferredImports) { + Predicate selectorFilter = deferredImport.getImportSelector().getExclusionFilter(); + if (selectorFilter != null) { + mergedFilter = mergedFilter.or(selectorFilter); + } + } + return mergedFilter; + } + } + + + private static class DefaultDeferredImportSelectorGroup implements Group { + + private final List imports = new ArrayList<>(); + + @Override + public void process(AnnotationMetadata metadata, DeferredImportSelector selector) { + for (String importClassName : selector.selectImports(metadata)) { + this.imports.add(new Entry(metadata, importClassName)); + } + } + + @Override + public Iterable selectImports() { + return this.imports; + } + } + + + /** + * Simple wrapper that allows annotated source classes to be dealt with + * in a uniform manner, regardless of how they are loaded. + */ + private class SourceClass implements Ordered { + + private final Object source; // Class or MetadataReader + + private final AnnotationMetadata metadata; + + public SourceClass(Object source) { + this.source = source; + if (source instanceof Class) { + this.metadata = AnnotationMetadata.introspect((Class) source); + } + else { + this.metadata = ((MetadataReader) source).getAnnotationMetadata(); + } + } + + public final AnnotationMetadata getMetadata() { + return this.metadata; + } + + @Override + public int getOrder() { + Integer order = ConfigurationClassUtils.getOrder(this.metadata); + return (order != null ? order : Ordered.LOWEST_PRECEDENCE); + } + + public Class loadClass() throws ClassNotFoundException { + if (this.source instanceof Class) { + return (Class) this.source; + } + String className = ((MetadataReader) this.source).getClassMetadata().getClassName(); + return ClassUtils.forName(className, resourceLoader.getClassLoader()); + } + + public boolean isAssignable(Class clazz) throws IOException { + if (this.source instanceof Class) { + return clazz.isAssignableFrom((Class) this.source); + } + return new AssignableTypeFilter(clazz).match((MetadataReader) this.source, metadataReaderFactory); + } + + public ConfigurationClass asConfigClass(ConfigurationClass importedBy) { + if (this.source instanceof Class) { + return new ConfigurationClass((Class) this.source, importedBy); + } + return new ConfigurationClass((MetadataReader) this.source, importedBy); + } + + public Collection getMemberClasses() throws IOException { + Object sourceToProcess = this.source; + if (sourceToProcess instanceof Class) { + Class sourceClass = (Class) sourceToProcess; + try { + Class[] declaredClasses = sourceClass.getDeclaredClasses(); + List members = new ArrayList<>(declaredClasses.length); + for (Class declaredClass : declaredClasses) { + members.add(asSourceClass(declaredClass, DEFAULT_EXCLUSION_FILTER)); + } + return members; + } + catch (NoClassDefFoundError err) { + // getDeclaredClasses() failed because of non-resolvable dependencies + // -> fall back to ASM below + sourceToProcess = metadataReaderFactory.getMetadataReader(sourceClass.getName()); + } + } + + // ASM-based resolution - safe for non-resolvable classes as well + MetadataReader sourceReader = (MetadataReader) sourceToProcess; + String[] memberClassNames = sourceReader.getClassMetadata().getMemberClassNames(); + List members = new ArrayList<>(memberClassNames.length); + for (String memberClassName : memberClassNames) { + try { + members.add(asSourceClass(memberClassName, DEFAULT_EXCLUSION_FILTER)); + } + catch (IOException ex) { + // Let's skip it if it's not resolvable - we're just looking for candidates + if (logger.isDebugEnabled()) { + logger.debug("Failed to resolve member class [" + memberClassName + + "] - not considering it as a configuration class candidate"); + } + } + } + return members; + } + + public SourceClass getSuperClass() throws IOException { + if (this.source instanceof Class) { + return asSourceClass(((Class) this.source).getSuperclass(), DEFAULT_EXCLUSION_FILTER); + } + return asSourceClass( + ((MetadataReader) this.source).getClassMetadata().getSuperClassName(), DEFAULT_EXCLUSION_FILTER); + } + + public Set getInterfaces() throws IOException { + Set result = new LinkedHashSet<>(); + if (this.source instanceof Class) { + Class sourceClass = (Class) this.source; + for (Class ifcClass : sourceClass.getInterfaces()) { + result.add(asSourceClass(ifcClass, DEFAULT_EXCLUSION_FILTER)); + } + } + else { + for (String className : this.metadata.getInterfaceNames()) { + result.add(asSourceClass(className, DEFAULT_EXCLUSION_FILTER)); + } + } + return result; + } + + public Set getAnnotations() { + Set result = new LinkedHashSet<>(); + if (this.source instanceof Class) { + Class sourceClass = (Class) this.source; + for (Annotation ann : sourceClass.getDeclaredAnnotations()) { + Class annType = ann.annotationType(); + if (!annType.getName().startsWith("java")) { + try { + result.add(asSourceClass(annType, DEFAULT_EXCLUSION_FILTER)); + } + catch (Throwable ex) { + // An annotation not present on the classpath is being ignored + // by the JVM's class loading -> ignore here as well. + } + } + } + } + else { + for (String className : this.metadata.getAnnotationTypes()) { + if (!className.startsWith("java")) { + try { + result.add(getRelated(className)); + } + catch (Throwable ex) { + // An annotation not present on the classpath is being ignored + // by the JVM's class loading -> ignore here as well. + } + } + } + } + return result; + } + + public Collection getAnnotationAttributes(String annType, String attribute) throws IOException { + Map annotationAttributes = this.metadata.getAnnotationAttributes(annType, true); + if (annotationAttributes == null || !annotationAttributes.containsKey(attribute)) { + return Collections.emptySet(); + } + String[] classNames = (String[]) annotationAttributes.get(attribute); + Set result = new LinkedHashSet<>(); + for (String className : classNames) { + result.add(getRelated(className)); + } + return result; + } + + private SourceClass getRelated(String className) throws IOException { + if (this.source instanceof Class) { + try { + Class clazz = ClassUtils.forName(className, ((Class) this.source).getClassLoader()); + return asSourceClass(clazz, DEFAULT_EXCLUSION_FILTER); + } + catch (ClassNotFoundException ex) { + // Ignore -> fall back to ASM next, except for core java types. + if (className.startsWith("java")) { + throw new NestedIOException("Failed to load class [" + className + "]", ex); + } + return new SourceClass(metadataReaderFactory.getMetadataReader(className)); + } + } + return asSourceClass(className, DEFAULT_EXCLUSION_FILTER); + } + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof SourceClass && + this.metadata.getClassName().equals(((SourceClass) other).metadata.getClassName()))); + } + + @Override + public int hashCode() { + return this.metadata.getClassName().hashCode(); + } + + @Override + public String toString() { + return this.metadata.getClassName(); + } + } + + + /** + * {@link Problem} registered upon detection of a circular {@link Import}. + */ + private static class CircularImportProblem extends Problem { + + public CircularImportProblem(ConfigurationClass attemptedImport, Deque importStack) { + super(String.format("A circular @Import has been detected: " + + "Illegal attempt by @Configuration class '%s' to import class '%s' as '%s' is " + + "already present in the current import stack %s", importStack.element().getSimpleName(), + attemptedImport.getSimpleName(), attemptedImport.getSimpleName(), importStack), + new Location(importStack.element().getResource(), attemptedImport.getMetadata())); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java new file mode 100644 index 0000000..449c48f --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java @@ -0,0 +1,487 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aop.framework.autoproxy.AutoProxyUtils; +import org.springframework.beans.PropertyValues; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor; +import org.springframework.beans.factory.config.SingletonBeanRegistry; +import org.springframework.beans.factory.parsing.FailFastProblemReporter; +import org.springframework.beans.factory.parsing.PassThroughSourceExtractor; +import org.springframework.beans.factory.parsing.ProblemReporter; +import org.springframework.beans.factory.parsing.SourceExtractor; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.context.ApplicationStartupAware; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.context.annotation.ConfigurationClassEnhancer.EnhancedConfiguration; +import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; +import org.springframework.core.env.Environment; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.metrics.ApplicationStartup; +import org.springframework.core.metrics.StartupStep; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.MethodMetadata; +import org.springframework.core.type.classreading.CachingMetadataReaderFactory; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * {@link BeanFactoryPostProcessor} used for bootstrapping processing of + * {@link Configuration @Configuration} classes. + * + *

    Registered by default when using {@code } or + * {@code }. Otherwise, may be declared manually as + * with any other BeanFactoryPostProcessor. + * + *

    This post processor is priority-ordered as it is important that any + * {@link Bean} methods declared in {@code @Configuration} classes have + * their corresponding bean definitions registered before any other + * {@link BeanFactoryPostProcessor} executes. + * + * @author Chris Beams + * @author Juergen Hoeller + * @author Phillip Webb + * @since 3.0 + */ +public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor, + PriorityOrdered, ResourceLoaderAware, ApplicationStartupAware, BeanClassLoaderAware, EnvironmentAware { + + /** + * A {@code BeanNameGenerator} using fully qualified class names as default bean names. + *

    This default for configuration-level import purposes may be overridden through + * {@link #setBeanNameGenerator}. Note that the default for component scanning purposes + * is a plain {@link AnnotationBeanNameGenerator#INSTANCE}, unless overridden through + * {@link #setBeanNameGenerator} with a unified user-level bean name generator. + * @since 5.2 + * @see #setBeanNameGenerator + */ + public static final AnnotationBeanNameGenerator IMPORT_BEAN_NAME_GENERATOR = + FullyQualifiedAnnotationBeanNameGenerator.INSTANCE; + + private static final String IMPORT_REGISTRY_BEAN_NAME = + ConfigurationClassPostProcessor.class.getName() + ".importRegistry"; + + /** + * Whether this environment lives within a native image. + * Exposed as a private static field rather than in a {@code NativeImageDetector.inNativeImage()} static method due to https://github.com/oracle/graal/issues/2594. + * @see ImageInfo.java + */ + private static final boolean IN_NATIVE_IMAGE = (System.getProperty("org.graalvm.nativeimage.imagecode") != null); + + + private final Log logger = LogFactory.getLog(getClass()); + + private SourceExtractor sourceExtractor = new PassThroughSourceExtractor(); + + private ProblemReporter problemReporter = new FailFastProblemReporter(); + + @Nullable + private Environment environment; + + private ResourceLoader resourceLoader = new DefaultResourceLoader(); + + @Nullable + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + private MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(); + + private boolean setMetadataReaderFactoryCalled = false; + + private final Set registriesPostProcessed = new HashSet<>(); + + private final Set factoriesPostProcessed = new HashSet<>(); + + @Nullable + private ConfigurationClassBeanDefinitionReader reader; + + private boolean localBeanNameGeneratorSet = false; + + /* Using short class names as default bean names by default. */ + private BeanNameGenerator componentScanBeanNameGenerator = AnnotationBeanNameGenerator.INSTANCE; + + /* Using fully qualified class names as default bean names by default. */ + private BeanNameGenerator importBeanNameGenerator = IMPORT_BEAN_NAME_GENERATOR; + + private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT; + + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; // within PriorityOrdered + } + + /** + * Set the {@link SourceExtractor} to use for generated bean definitions + * that correspond to {@link Bean} factory methods. + */ + public void setSourceExtractor(@Nullable SourceExtractor sourceExtractor) { + this.sourceExtractor = (sourceExtractor != null ? sourceExtractor : new PassThroughSourceExtractor()); + } + + /** + * Set the {@link ProblemReporter} to use. + *

    Used to register any problems detected with {@link Configuration} or {@link Bean} + * declarations. For instance, an @Bean method marked as {@code final} is illegal + * and would be reported as a problem. Defaults to {@link FailFastProblemReporter}. + */ + public void setProblemReporter(@Nullable ProblemReporter problemReporter) { + this.problemReporter = (problemReporter != null ? problemReporter : new FailFastProblemReporter()); + } + + /** + * Set the {@link MetadataReaderFactory} to use. + *

    Default is a {@link CachingMetadataReaderFactory} for the specified + * {@linkplain #setBeanClassLoader bean class loader}. + */ + public void setMetadataReaderFactory(MetadataReaderFactory metadataReaderFactory) { + Assert.notNull(metadataReaderFactory, "MetadataReaderFactory must not be null"); + this.metadataReaderFactory = metadataReaderFactory; + this.setMetadataReaderFactoryCalled = true; + } + + /** + * Set the {@link BeanNameGenerator} to be used when triggering component scanning + * from {@link Configuration} classes and when registering {@link Import}'ed + * configuration classes. The default is a standard {@link AnnotationBeanNameGenerator} + * for scanned components (compatible with the default in {@link ClassPathBeanDefinitionScanner}) + * and a variant thereof for imported configuration classes (using unique fully-qualified + * class names instead of standard component overriding). + *

    Note that this strategy does not apply to {@link Bean} methods. + *

    This setter is typically only appropriate when configuring the post-processor as a + * standalone bean definition in XML, e.g. not using the dedicated {@code AnnotationConfig*} + * application contexts or the {@code } element. Any bean name + * generator specified against the application context will take precedence over any set here. + * @since 3.1.1 + * @see AnnotationConfigApplicationContext#setBeanNameGenerator(BeanNameGenerator) + * @see AnnotationConfigUtils#CONFIGURATION_BEAN_NAME_GENERATOR + */ + public void setBeanNameGenerator(BeanNameGenerator beanNameGenerator) { + Assert.notNull(beanNameGenerator, "BeanNameGenerator must not be null"); + this.localBeanNameGeneratorSet = true; + this.componentScanBeanNameGenerator = beanNameGenerator; + this.importBeanNameGenerator = beanNameGenerator; + } + + @Override + public void setEnvironment(Environment environment) { + Assert.notNull(environment, "Environment must not be null"); + this.environment = environment; + } + + @Override + public void setResourceLoader(ResourceLoader resourceLoader) { + Assert.notNull(resourceLoader, "ResourceLoader must not be null"); + this.resourceLoader = resourceLoader; + if (!this.setMetadataReaderFactoryCalled) { + this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader); + } + } + + @Override + public void setBeanClassLoader(ClassLoader beanClassLoader) { + this.beanClassLoader = beanClassLoader; + if (!this.setMetadataReaderFactoryCalled) { + this.metadataReaderFactory = new CachingMetadataReaderFactory(beanClassLoader); + } + } + + @Override + public void setApplicationStartup(ApplicationStartup applicationStartup) { + this.applicationStartup = applicationStartup; + } + + /** + * Derive further bean definitions from the configuration classes in the registry. + */ + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { + int registryId = System.identityHashCode(registry); + if (this.registriesPostProcessed.contains(registryId)) { + throw new IllegalStateException( + "postProcessBeanDefinitionRegistry already called on this post-processor against " + registry); + } + if (this.factoriesPostProcessed.contains(registryId)) { + throw new IllegalStateException( + "postProcessBeanFactory already called on this post-processor against " + registry); + } + this.registriesPostProcessed.add(registryId); + + processConfigBeanDefinitions(registry); + } + + /** + * Prepare the Configuration classes for servicing bean requests at runtime + * by replacing them with CGLIB-enhanced subclasses. + */ + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { + int factoryId = System.identityHashCode(beanFactory); + if (this.factoriesPostProcessed.contains(factoryId)) { + throw new IllegalStateException( + "postProcessBeanFactory already called on this post-processor against " + beanFactory); + } + this.factoriesPostProcessed.add(factoryId); + if (!this.registriesPostProcessed.contains(factoryId)) { + // BeanDefinitionRegistryPostProcessor hook apparently not supported... + // Simply call processConfigurationClasses lazily at this point then. + processConfigBeanDefinitions((BeanDefinitionRegistry) beanFactory); + } + + enhanceConfigurationClasses(beanFactory); + beanFactory.addBeanPostProcessor(new ImportAwareBeanPostProcessor(beanFactory)); + } + + /** + * Build and validate a configuration model based on the registry of + * {@link Configuration} classes. + */ + public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) { + List configCandidates = new ArrayList<>(); + String[] candidateNames = registry.getBeanDefinitionNames(); + + for (String beanName : candidateNames) { + BeanDefinition beanDef = registry.getBeanDefinition(beanName); + if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) { + if (logger.isDebugEnabled()) { + logger.debug("Bean definition has already been processed as a configuration class: " + beanDef); + } + } + else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) { + configCandidates.add(new BeanDefinitionHolder(beanDef, beanName)); + } + } + + // Return immediately if no @Configuration classes were found + if (configCandidates.isEmpty()) { + return; + } + + // Sort by previously determined @Order value, if applicable + configCandidates.sort((bd1, bd2) -> { + int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition()); + int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition()); + return Integer.compare(i1, i2); + }); + + // Detect any custom bean name generation strategy supplied through the enclosing application context + SingletonBeanRegistry sbr = null; + if (registry instanceof SingletonBeanRegistry) { + sbr = (SingletonBeanRegistry) registry; + if (!this.localBeanNameGeneratorSet) { + BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton( + AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR); + if (generator != null) { + this.componentScanBeanNameGenerator = generator; + this.importBeanNameGenerator = generator; + } + } + } + + if (this.environment == null) { + this.environment = new StandardEnvironment(); + } + + // Parse each @Configuration class + ConfigurationClassParser parser = new ConfigurationClassParser( + this.metadataReaderFactory, this.problemReporter, this.environment, + this.resourceLoader, this.componentScanBeanNameGenerator, registry); + + Set candidates = new LinkedHashSet<>(configCandidates); + Set alreadyParsed = new HashSet<>(configCandidates.size()); + do { + StartupStep processConfig = this.applicationStartup.start("spring.context.config-classes.parse"); + parser.parse(candidates); + parser.validate(); + + Set configClasses = new LinkedHashSet<>(parser.getConfigurationClasses()); + configClasses.removeAll(alreadyParsed); + + // Read the model and create bean definitions based on its content + if (this.reader == null) { + this.reader = new ConfigurationClassBeanDefinitionReader( + registry, this.sourceExtractor, this.resourceLoader, this.environment, + this.importBeanNameGenerator, parser.getImportRegistry()); + } + this.reader.loadBeanDefinitions(configClasses); + alreadyParsed.addAll(configClasses); + processConfig.tag("classCount", () -> String.valueOf(configClasses.size())).end(); + + candidates.clear(); + if (registry.getBeanDefinitionCount() > candidateNames.length) { + String[] newCandidateNames = registry.getBeanDefinitionNames(); + Set oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames)); + Set alreadyParsedClasses = new HashSet<>(); + for (ConfigurationClass configurationClass : alreadyParsed) { + alreadyParsedClasses.add(configurationClass.getMetadata().getClassName()); + } + for (String candidateName : newCandidateNames) { + if (!oldCandidateNames.contains(candidateName)) { + BeanDefinition bd = registry.getBeanDefinition(candidateName); + if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) && + !alreadyParsedClasses.contains(bd.getBeanClassName())) { + candidates.add(new BeanDefinitionHolder(bd, candidateName)); + } + } + } + candidateNames = newCandidateNames; + } + } + while (!candidates.isEmpty()); + + // Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes + if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) { + sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry()); + } + + if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) { + // Clear cache in externally provided MetadataReaderFactory; this is a no-op + // for a shared cache since it'll be cleared by the ApplicationContext. + ((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache(); + } + } + + /** + * Post-processes a BeanFactory in search of Configuration class BeanDefinitions; + * any candidates are then enhanced by a {@link ConfigurationClassEnhancer}. + * Candidate status is determined by BeanDefinition attribute metadata. + * @see ConfigurationClassEnhancer + */ + public void enhanceConfigurationClasses(ConfigurableListableBeanFactory beanFactory) { + StartupStep enhanceConfigClasses = this.applicationStartup.start("spring.context.config-classes.enhance"); + Map configBeanDefs = new LinkedHashMap<>(); + for (String beanName : beanFactory.getBeanDefinitionNames()) { + BeanDefinition beanDef = beanFactory.getBeanDefinition(beanName); + Object configClassAttr = beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE); + MethodMetadata methodMetadata = null; + if (beanDef instanceof AnnotatedBeanDefinition) { + methodMetadata = ((AnnotatedBeanDefinition) beanDef).getFactoryMethodMetadata(); + } + if ((configClassAttr != null || methodMetadata != null) && beanDef instanceof AbstractBeanDefinition) { + // Configuration class (full or lite) or a configuration-derived @Bean method + // -> resolve bean class at this point... + AbstractBeanDefinition abd = (AbstractBeanDefinition) beanDef; + if (!abd.hasBeanClass()) { + try { + abd.resolveBeanClass(this.beanClassLoader); + } + catch (Throwable ex) { + throw new IllegalStateException( + "Cannot load configuration class: " + beanDef.getBeanClassName(), ex); + } + } + } + if (ConfigurationClassUtils.CONFIGURATION_CLASS_FULL.equals(configClassAttr)) { + if (!(beanDef instanceof AbstractBeanDefinition)) { + throw new BeanDefinitionStoreException("Cannot enhance @Configuration bean definition '" + + beanName + "' since it is not stored in an AbstractBeanDefinition subclass"); + } + else if (logger.isInfoEnabled() && beanFactory.containsSingleton(beanName)) { + logger.info("Cannot enhance @Configuration bean definition '" + beanName + + "' since its singleton instance has been created too early. The typical cause " + + "is a non-static @Bean method with a BeanDefinitionRegistryPostProcessor " + + "return type: Consider declaring such methods as 'static'."); + } + configBeanDefs.put(beanName, (AbstractBeanDefinition) beanDef); + } + } + if (configBeanDefs.isEmpty() || IN_NATIVE_IMAGE) { + // nothing to enhance -> return immediately + enhanceConfigClasses.end(); + return; + } + + ConfigurationClassEnhancer enhancer = new ConfigurationClassEnhancer(); + for (Map.Entry entry : configBeanDefs.entrySet()) { + AbstractBeanDefinition beanDef = entry.getValue(); + // If a @Configuration class gets proxied, always proxy the target class + beanDef.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE); + // Set enhanced subclass of the user-specified bean class + Class configClass = beanDef.getBeanClass(); + Class enhancedClass = enhancer.enhance(configClass, this.beanClassLoader); + if (configClass != enhancedClass) { + if (logger.isTraceEnabled()) { + logger.trace(String.format("Replacing bean definition '%s' existing class '%s' with " + + "enhanced class '%s'", entry.getKey(), configClass.getName(), enhancedClass.getName())); + } + beanDef.setBeanClass(enhancedClass); + } + } + enhanceConfigClasses.tag("classCount", () -> String.valueOf(configBeanDefs.keySet().size())).end(); + } + + + private static class ImportAwareBeanPostProcessor implements InstantiationAwareBeanPostProcessor { + + private final BeanFactory beanFactory; + + public ImportAwareBeanPostProcessor(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public PropertyValues postProcessProperties(@Nullable PropertyValues pvs, Object bean, String beanName) { + // Inject the BeanFactory before AutowiredAnnotationBeanPostProcessor's + // postProcessProperties method attempts to autowire other configuration beans. + if (bean instanceof EnhancedConfiguration) { + ((EnhancedConfiguration) bean).setBeanFactory(this.beanFactory); + } + return pvs; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) { + if (bean instanceof ImportAware) { + ImportRegistry ir = this.beanFactory.getBean(IMPORT_REGISTRY_BEAN_NAME, ImportRegistry.class); + AnnotationMetadata importingClass = ir.getImportingClassFor(ClassUtils.getUserClass(bean).getName()); + if (importingClass != null) { + ((ImportAware) bean).setImportMetadata(importingClass); + } + } + return bean; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassUtils.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassUtils.java new file mode 100644 index 0000000..3758084 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassUtils.java @@ -0,0 +1,202 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aop.framework.AopInfrastructureBean; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.context.event.EventListenerFactory; +import org.springframework.core.Conventions; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.Order; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; + +/** + * Utilities for identifying {@link Configuration} classes. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + */ +abstract class ConfigurationClassUtils { + + public static final String CONFIGURATION_CLASS_FULL = "full"; + + public static final String CONFIGURATION_CLASS_LITE = "lite"; + + public static final String CONFIGURATION_CLASS_ATTRIBUTE = + Conventions.getQualifiedAttributeName(ConfigurationClassPostProcessor.class, "configurationClass"); + + private static final String ORDER_ATTRIBUTE = + Conventions.getQualifiedAttributeName(ConfigurationClassPostProcessor.class, "order"); + + + private static final Log logger = LogFactory.getLog(ConfigurationClassUtils.class); + + private static final Set candidateIndicators = new HashSet<>(8); + + static { + candidateIndicators.add(Component.class.getName()); + candidateIndicators.add(ComponentScan.class.getName()); + candidateIndicators.add(Import.class.getName()); + candidateIndicators.add(ImportResource.class.getName()); + } + + + /** + * Check whether the given bean definition is a candidate for a configuration class + * (or a nested component class declared within a configuration/component class, + * to be auto-registered as well), and mark it accordingly. + * @param beanDef the bean definition to check + * @param metadataReaderFactory the current factory in use by the caller + * @return whether the candidate qualifies as (any kind of) configuration class + */ + public static boolean checkConfigurationClassCandidate( + BeanDefinition beanDef, MetadataReaderFactory metadataReaderFactory) { + + String className = beanDef.getBeanClassName(); + if (className == null || beanDef.getFactoryMethodName() != null) { + return false; + } + + AnnotationMetadata metadata; + if (beanDef instanceof AnnotatedBeanDefinition && + className.equals(((AnnotatedBeanDefinition) beanDef).getMetadata().getClassName())) { + // Can reuse the pre-parsed metadata from the given BeanDefinition... + metadata = ((AnnotatedBeanDefinition) beanDef).getMetadata(); + } + else if (beanDef instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) beanDef).hasBeanClass()) { + // Check already loaded Class if present... + // since we possibly can't even load the class file for this Class. + Class beanClass = ((AbstractBeanDefinition) beanDef).getBeanClass(); + if (BeanFactoryPostProcessor.class.isAssignableFrom(beanClass) || + BeanPostProcessor.class.isAssignableFrom(beanClass) || + AopInfrastructureBean.class.isAssignableFrom(beanClass) || + EventListenerFactory.class.isAssignableFrom(beanClass)) { + return false; + } + metadata = AnnotationMetadata.introspect(beanClass); + } + else { + try { + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(className); + metadata = metadataReader.getAnnotationMetadata(); + } + catch (IOException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Could not find class file for introspecting configuration annotations: " + + className, ex); + } + return false; + } + } + + Map config = metadata.getAnnotationAttributes(Configuration.class.getName()); + if (config != null && !Boolean.FALSE.equals(config.get("proxyBeanMethods"))) { + beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL); + } + else if (config != null || isConfigurationCandidate(metadata)) { + beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE); + } + else { + return false; + } + + // It's a full or lite configuration candidate... Let's determine the order value, if any. + Integer order = getOrder(metadata); + if (order != null) { + beanDef.setAttribute(ORDER_ATTRIBUTE, order); + } + + return true; + } + + /** + * Check the given metadata for a configuration class candidate + * (or nested component class declared within a configuration/component class). + * @param metadata the metadata of the annotated class + * @return {@code true} if the given class is to be registered for + * configuration class processing; {@code false} otherwise + */ + public static boolean isConfigurationCandidate(AnnotationMetadata metadata) { + // Do not consider an interface or an annotation... + if (metadata.isInterface()) { + return false; + } + + // Any of the typical annotations found? + for (String indicator : candidateIndicators) { + if (metadata.isAnnotated(indicator)) { + return true; + } + } + + // Finally, let's look for @Bean methods... + try { + return metadata.hasAnnotatedMethods(Bean.class.getName()); + } + catch (Throwable ex) { + if (logger.isDebugEnabled()) { + logger.debug("Failed to introspect @Bean methods on class [" + metadata.getClassName() + "]: " + ex); + } + return false; + } + } + + /** + * Determine the order for the given configuration class metadata. + * @param metadata the metadata of the annotated class + * @return the {@code @Order} annotation value on the configuration class, + * or {@code Ordered.LOWEST_PRECEDENCE} if none declared + * @since 5.0 + */ + @Nullable + public static Integer getOrder(AnnotationMetadata metadata) { + Map orderAttributes = metadata.getAnnotationAttributes(Order.class.getName()); + return (orderAttributes != null ? ((Integer) orderAttributes.get(AnnotationUtils.VALUE)) : null); + } + + /** + * Determine the order for the given configuration class bean definition, + * as set by {@link #checkConfigurationClassCandidate}. + * @param beanDef the bean definition to check + * @return the {@link Order @Order} annotation value on the configuration class, + * or {@link Ordered#LOWEST_PRECEDENCE} if none declared + * @since 4.2 + */ + public static int getOrder(BeanDefinition beanDef) { + Integer order = (Integer) beanDef.getAttribute(ORDER_ATTRIBUTE); + return (order != null ? order : Ordered.LOWEST_PRECEDENCE); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationCondition.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationCondition.java new file mode 100644 index 0000000..e14e030 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationCondition.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +/** + * A {@link Condition} that offers more fine-grained control when used with + * {@code @Configuration}. Allows certain conditions to adapt when they match + * based on the configuration phase. For example, a condition that checks if a bean + * has already been registered might choose to only be evaluated during the + * {@link ConfigurationPhase#REGISTER_BEAN REGISTER_BEAN} {@link ConfigurationPhase}. + * + * @author Phillip Webb + * @since 4.0 + * @see Configuration + */ +public interface ConfigurationCondition extends Condition { + + /** + * Return the {@link ConfigurationPhase} in which the condition should be evaluated. + */ + ConfigurationPhase getConfigurationPhase(); + + + /** + * The various configuration phases where the condition could be evaluated. + */ + enum ConfigurationPhase { + + /** + * The {@link Condition} should be evaluated as a {@code @Configuration} + * class is being parsed. + *

    If the condition does not match at this point, the {@code @Configuration} + * class will not be added. + */ + PARSE_CONFIGURATION, + + /** + * The {@link Condition} should be evaluated when adding a regular + * (non {@code @Configuration}) bean. The condition will not prevent + * {@code @Configuration} classes from being added. + *

    At the time that the condition is evaluated, all {@code @Configuration} + * classes will have been parsed. + */ + REGISTER_BEAN + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationMethod.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationMethod.java new file mode 100644 index 0000000..cf641db --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationMethod.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.springframework.beans.factory.parsing.Location; +import org.springframework.beans.factory.parsing.ProblemReporter; +import org.springframework.core.type.MethodMetadata; + +/** + * Base class for a {@link Configuration @Configuration} class method. + * + * @author Chris Beams + * @since 3.1 + */ +abstract class ConfigurationMethod { + + protected final MethodMetadata metadata; + + protected final ConfigurationClass configurationClass; + + + public ConfigurationMethod(MethodMetadata metadata, ConfigurationClass configurationClass) { + this.metadata = metadata; + this.configurationClass = configurationClass; + } + + + public MethodMetadata getMetadata() { + return this.metadata; + } + + public ConfigurationClass getConfigurationClass() { + return this.configurationClass; + } + + public Location getResourceLocation() { + return new Location(this.configurationClass.getResource(), this.metadata); + } + + String getFullyQualifiedMethodName() { + return this.metadata.getDeclaringClassName() + "#" + this.metadata.getMethodName(); + } + + static String getShortMethodName(String fullyQualifiedMethodName) { + return fullyQualifiedMethodName.substring(fullyQualifiedMethodName.indexOf('#') + 1); + } + + public void validate(ProblemReporter problemReporter) { + } + + + @Override + public String toString() { + return String.format("[%s:name=%s,declaringClass=%s]", + getClass().getSimpleName(), getMetadata().getMethodName(), getMetadata().getDeclaringClassName()); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConflictingBeanDefinitionException.java b/spring-context/src/main/java/org/springframework/context/annotation/ConflictingBeanDefinitionException.java new file mode 100644 index 0000000..a313c42 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConflictingBeanDefinitionException.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2011 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +/** + * Marker subclass of {@link IllegalStateException}, allowing for explicit + * catch clauses in calling code. + * + * @author Chris Beams + * @since 3.1 + */ +@SuppressWarnings("serial") +class ConflictingBeanDefinitionException extends IllegalStateException { + + public ConflictingBeanDefinitionException(String message) { + super(message); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ContextAnnotationAutowireCandidateResolver.java b/spring-context/src/main/java/org/springframework/context/annotation/ContextAnnotationAutowireCandidateResolver.java new file mode 100644 index 0000000..93999dd --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ContextAnnotationAutowireCandidateResolver.java @@ -0,0 +1,133 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.aop.TargetSource; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.QualifierAnnotationAutowireCandidateResolver; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Complete implementation of the + * {@link org.springframework.beans.factory.support.AutowireCandidateResolver} strategy + * interface, providing support for qualifier annotations as well as for lazy resolution + * driven by the {@link Lazy} annotation in the {@code context.annotation} package. + * + * @author Juergen Hoeller + * @since 4.0 + */ +public class ContextAnnotationAutowireCandidateResolver extends QualifierAnnotationAutowireCandidateResolver { + + @Override + @Nullable + public Object getLazyResolutionProxyIfNecessary(DependencyDescriptor descriptor, @Nullable String beanName) { + return (isLazy(descriptor) ? buildLazyResolutionProxy(descriptor, beanName) : null); + } + + protected boolean isLazy(DependencyDescriptor descriptor) { + for (Annotation ann : descriptor.getAnnotations()) { + Lazy lazy = AnnotationUtils.getAnnotation(ann, Lazy.class); + if (lazy != null && lazy.value()) { + return true; + } + } + MethodParameter methodParam = descriptor.getMethodParameter(); + if (methodParam != null) { + Method method = methodParam.getMethod(); + if (method == null || void.class == method.getReturnType()) { + Lazy lazy = AnnotationUtils.getAnnotation(methodParam.getAnnotatedElement(), Lazy.class); + if (lazy != null && lazy.value()) { + return true; + } + } + } + return false; + } + + protected Object buildLazyResolutionProxy(final DependencyDescriptor descriptor, final @Nullable String beanName) { + BeanFactory beanFactory = getBeanFactory(); + Assert.state(beanFactory instanceof DefaultListableBeanFactory, + "BeanFactory needs to be a DefaultListableBeanFactory"); + final DefaultListableBeanFactory dlbf = (DefaultListableBeanFactory) beanFactory; + + TargetSource ts = new TargetSource() { + @Override + public Class getTargetClass() { + return descriptor.getDependencyType(); + } + @Override + public boolean isStatic() { + return false; + } + @Override + public Object getTarget() { + Set autowiredBeanNames = (beanName != null ? new LinkedHashSet<>(1) : null); + Object target = dlbf.doResolveDependency(descriptor, beanName, autowiredBeanNames, null); + if (target == null) { + Class type = getTargetClass(); + if (Map.class == type) { + return Collections.emptyMap(); + } + else if (List.class == type) { + return Collections.emptyList(); + } + else if (Set.class == type || Collection.class == type) { + return Collections.emptySet(); + } + throw new NoSuchBeanDefinitionException(descriptor.getResolvableType(), + "Optional dependency not present for lazy injection point"); + } + if (autowiredBeanNames != null) { + for (String autowiredBeanName : autowiredBeanNames) { + if (dlbf.containsBean(autowiredBeanName)) { + dlbf.registerDependentBean(autowiredBeanName, beanName); + } + } + } + return target; + } + @Override + public void releaseTarget(Object target) { + } + }; + + ProxyFactory pf = new ProxyFactory(); + pf.setTargetSource(ts); + Class dependencyType = descriptor.getDependencyType(); + if (dependencyType.isInterface()) { + pf.addInterface(dependencyType); + } + return pf.getProxy(dlbf.getBeanClassLoader()); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/DeferredImportSelector.java b/spring-context/src/main/java/org/springframework/context/annotation/DeferredImportSelector.java new file mode 100644 index 0000000..dedb068 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/DeferredImportSelector.java @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.lang.Nullable; + +/** + * A variation of {@link ImportSelector} that runs after all {@code @Configuration} beans + * have been processed. This type of selector can be particularly useful when the selected + * imports are {@code @Conditional}. + * + *

    Implementations can also extend the {@link org.springframework.core.Ordered} + * interface or use the {@link org.springframework.core.annotation.Order} annotation to + * indicate a precedence against other {@link DeferredImportSelector DeferredImportSelectors}. + * + *

    Implementations may also provide an {@link #getImportGroup() import group} which + * can provide additional sorting and filtering logic across different selectors. + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 4.0 + */ +public interface DeferredImportSelector extends ImportSelector { + + /** + * Return a specific import group. + *

    The default implementations return {@code null} for no grouping required. + * @return the import group class, or {@code null} if none + * @since 5.0 + */ + @Nullable + default Class getImportGroup() { + return null; + } + + + /** + * Interface used to group results from different import selectors. + * @since 5.0 + */ + interface Group { + + /** + * Process the {@link AnnotationMetadata} of the importing @{@link Configuration} + * class using the specified {@link DeferredImportSelector}. + */ + void process(AnnotationMetadata metadata, DeferredImportSelector selector); + + /** + * Return the {@link Entry entries} of which class(es) should be imported + * for this group. + */ + Iterable selectImports(); + + + /** + * An entry that holds the {@link AnnotationMetadata} of the importing + * {@link Configuration} class and the class name to import. + */ + class Entry { + + private final AnnotationMetadata metadata; + + private final String importClassName; + + public Entry(AnnotationMetadata metadata, String importClassName) { + this.metadata = metadata; + this.importClassName = importClassName; + } + + /** + * Return the {@link AnnotationMetadata} of the importing + * {@link Configuration} class. + */ + public AnnotationMetadata getMetadata() { + return this.metadata; + } + + /** + * Return the fully qualified name of the class to import. + */ + public String getImportClassName() { + return this.importClassName; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + Entry entry = (Entry) other; + return (this.metadata.equals(entry.metadata) && this.importClassName.equals(entry.importClassName)); + } + + @Override + public int hashCode() { + return (this.metadata.hashCode() * 31 + this.importClassName.hashCode()); + } + + @Override + public String toString() { + return this.importClassName; + } + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/DependsOn.java b/spring-context/src/main/java/org/springframework/context/annotation/DependsOn.java new file mode 100644 index 0000000..217c25c --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/DependsOn.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Beans on which the current bean depends. Any beans specified are guaranteed to be + * created by the container before this bean. Used infrequently in cases where a bean + * does not explicitly depend on another through properties or constructor arguments, + * but rather depends on the side effects of another bean's initialization. + * + *

    A depends-on declaration can specify both an initialization-time dependency and, + * in the case of singleton beans only, a corresponding destruction-time dependency. + * Dependent beans that define a depends-on relationship with a given bean are destroyed + * first, prior to the given bean itself being destroyed. Thus, a depends-on declaration + * can also control shutdown order. + * + *

    May be used on any class directly or indirectly annotated with + * {@link org.springframework.stereotype.Component} or on methods annotated + * with {@link Bean}. + * + *

    Using {@link DependsOn} at the class level has no effect unless component-scanning + * is being used. If a {@link DependsOn}-annotated class is declared via XML, + * {@link DependsOn} annotation metadata is ignored, and + * {@code } is respected instead. + * + * @author Juergen Hoeller + * @since 3.0 + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DependsOn { + + String[] value() default {}; + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Description.java b/spring-context/src/main/java/org/springframework/context/annotation/Description.java new file mode 100644 index 0000000..f7da0dc --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/Description.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Adds a textual description to bean definitions derived from + * {@link org.springframework.stereotype.Component} or {@link Bean}. + * + * @author Juergen Hoeller + * @since 4.0 + * @see org.springframework.beans.factory.config.BeanDefinition#getDescription() + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Description { + + /** + * The textual description to associate with the bean definition. + */ + String value(); + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/EnableAspectJAutoProxy.java b/spring-context/src/main/java/org/springframework/context/annotation/EnableAspectJAutoProxy.java new file mode 100644 index 0000000..8e26463 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/EnableAspectJAutoProxy.java @@ -0,0 +1,139 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Enables support for handling components marked with AspectJ's {@code @Aspect} annotation, + * similar to functionality found in Spring's {@code } XML element. + * To be used on @{@link Configuration} classes as follows: + * + *

    + * @Configuration
    + * @EnableAspectJAutoProxy
    + * public class AppConfig {
    + *
    + *     @Bean
    + *     public FooService fooService() {
    + *         return new FooService();
    + *     }
    + *
    + *     @Bean
    + *     public MyAspect myAspect() {
    + *         return new MyAspect();
    + *     }
    + * }
    + * + * Where {@code FooService} is a typical POJO component and {@code MyAspect} is an + * {@code @Aspect}-style aspect: + * + *
    + * public class FooService {
    + *
    + *     // various methods
    + * }
    + * + *
    + * @Aspect
    + * public class MyAspect {
    + *
    + *     @Before("execution(* FooService+.*(..))")
    + *     public void advice() {
    + *         // advise FooService methods as appropriate
    + *     }
    + * }
    + * + * In the scenario above, {@code @EnableAspectJAutoProxy} ensures that {@code MyAspect} + * will be properly processed and that {@code FooService} will be proxied mixing in the + * advice that it contributes. + * + *

    Users can control the type of proxy that gets created for {@code FooService} using + * the {@link #proxyTargetClass()} attribute. The following enables CGLIB-style 'subclass' + * proxies as opposed to the default interface-based JDK proxy approach. + * + *

    + * @Configuration
    + * @EnableAspectJAutoProxy(proxyTargetClass=true)
    + * public class AppConfig {
    + *     // ...
    + * }
    + * + *

    Note that {@code @Aspect} beans may be component-scanned like any other. + * Simply mark the aspect with both {@code @Aspect} and {@code @Component}: + * + *

    + * package com.foo;
    + *
    + * @Component
    + * public class FooService { ... }
    + *
    + * @Aspect
    + * @Component
    + * public class MyAspect { ... }
    + * + * Then use the @{@link ComponentScan} annotation to pick both up: + * + *
    + * @Configuration
    + * @ComponentScan("com.foo")
    + * @EnableAspectJAutoProxy
    + * public class AppConfig {
    + *
    + *     // no explicit @Bean definitions required
    + * }
    + * + * Note: {@code @EnableAspectJAutoProxy} applies to its local application context only, + * allowing for selective proxying of beans at different levels. Please redeclare + * {@code @EnableAspectJAutoProxy} in each individual context, e.g. the common root web + * application context and any separate {@code DispatcherServlet} application contexts, + * if you need to apply its behavior at multiple levels. + * + *

    This feature requires the presence of {@code aspectjweaver} on the classpath. + * While that dependency is optional for {@code spring-aop} in general, it is required + * for {@code @EnableAspectJAutoProxy} and its underlying facilities. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + * @see org.aspectj.lang.annotation.Aspect + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Import(AspectJAutoProxyRegistrar.class) +public @interface EnableAspectJAutoProxy { + + /** + * Indicate whether subclass-based (CGLIB) proxies are to be created as opposed + * to standard Java interface-based proxies. The default is {@code false}. + */ + boolean proxyTargetClass() default false; + + /** + * Indicate that the proxy should be exposed by the AOP framework as a {@code ThreadLocal} + * for retrieval via the {@link org.springframework.aop.framework.AopContext} class. + * Off by default, i.e. no guarantees that {@code AopContext} access will work. + * @since 4.3.1 + */ + boolean exposeProxy() default false; + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/EnableLoadTimeWeaving.java b/spring-context/src/main/java/org/springframework/context/annotation/EnableLoadTimeWeaving.java new file mode 100644 index 0000000..e31c02d --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/EnableLoadTimeWeaving.java @@ -0,0 +1,171 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.weaving.DefaultContextLoadTimeWeaver; +import org.springframework.instrument.classloading.LoadTimeWeaver; + +/** + * Activates a Spring {@link LoadTimeWeaver} for this application context, available as + * a bean with the name "loadTimeWeaver", similar to the {@code } + * element in Spring XML. + * + *

    To be used on @{@link org.springframework.context.annotation.Configuration Configuration} classes; + * the simplest possible example of which follows: + * + *

    + * @Configuration
    + * @EnableLoadTimeWeaving
    + * public class AppConfig {
    + *
    + *     // application-specific @Bean definitions ...
    + * }
    + * + * The example above is equivalent to the following Spring XML configuration: + * + *
    + * <beans>
    + *
    + *     <context:load-time-weaver/>
    + *
    + *     <!-- application-specific <bean> definitions -->
    + *
    + * </beans>
    + * 
    + * + *

    The {@code LoadTimeWeaverAware} interface

    + * Any bean that implements the {@link + * org.springframework.context.weaving.LoadTimeWeaverAware LoadTimeWeaverAware} interface + * will then receive the {@code LoadTimeWeaver} reference automatically; for example, + * Spring's JPA bootstrap support. + * + *

    Customizing the {@code LoadTimeWeaver}

    + * The default weaver is determined automatically: see {@link DefaultContextLoadTimeWeaver}. + * + *

    To customize the weaver used, the {@code @Configuration} class annotated with + * {@code @EnableLoadTimeWeaving} may also implement the {@link LoadTimeWeavingConfigurer} + * interface and return a custom {@code LoadTimeWeaver} instance through the + * {@code #getLoadTimeWeaver} method: + * + *

    + * @Configuration
    + * @EnableLoadTimeWeaving
    + * public class AppConfig implements LoadTimeWeavingConfigurer {
    + *
    + *     @Override
    + *     public LoadTimeWeaver getLoadTimeWeaver() {
    + *         MyLoadTimeWeaver ltw = new MyLoadTimeWeaver();
    + *         ltw.addClassTransformer(myClassFileTransformer);
    + *         // ...
    + *         return ltw;
    + *     }
    + * }
    + * + *

    The example above can be compared to the following Spring XML configuration: + * + *

    + * <beans>
    + *
    + *     <context:load-time-weaver weaverClass="com.acme.MyLoadTimeWeaver"/>
    + *
    + * </beans>
    + * 
    + * + *

    The code example differs from the XML example in that it actually instantiates the + * {@code MyLoadTimeWeaver} type, meaning that it can also configure the instance, e.g. + * calling the {@code #addClassTransformer} method. This demonstrates how the code-based + * configuration approach is more flexible through direct programmatic access. + * + *

    Enabling AspectJ-based weaving

    + * AspectJ load-time weaving may be enabled with the {@link #aspectjWeaving()} + * attribute, which will cause the {@linkplain + * org.aspectj.weaver.loadtime.ClassPreProcessorAgentAdapter AspectJ class transformer} to + * be registered through {@link LoadTimeWeaver#addTransformer}. AspectJ weaving will be + * activated by default if a "META-INF/aop.xml" resource is present on the classpath. + * Example: + * + *
    + * @Configuration
    + * @EnableLoadTimeWeaving(aspectjWeaving=ENABLED)
    + * public class AppConfig {
    + * }
    + * + *

    The example above can be compared to the following Spring XML configuration: + * + *

    + * <beans>
    + *
    + *     <context:load-time-weaver aspectj-weaving="on"/>
    + *
    + * </beans>
    + * 
    + * + *

    The two examples are equivalent with one significant exception: in the XML case, + * the functionality of {@code } is implicitly enabled when + * {@code aspectj-weaving} is "on". This does not occur when using + * {@code @EnableLoadTimeWeaving(aspectjWeaving=ENABLED)}. Instead you must explicitly add + * {@code @EnableSpringConfigured} (included in the {@code spring-aspects} module) + * + * @author Chris Beams + * @since 3.1 + * @see LoadTimeWeaver + * @see DefaultContextLoadTimeWeaver + * @see org.aspectj.weaver.loadtime.ClassPreProcessorAgentAdapter + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Import(LoadTimeWeavingConfiguration.class) +public @interface EnableLoadTimeWeaving { + + /** + * Whether AspectJ weaving should be enabled. + */ + AspectJWeaving aspectjWeaving() default AspectJWeaving.AUTODETECT; + + + /** + * AspectJ weaving enablement options. + */ + enum AspectJWeaving { + + /** + * Switches on Spring-based AspectJ load-time weaving. + */ + ENABLED, + + /** + * Switches off Spring-based AspectJ load-time weaving (even if a + * "META-INF/aop.xml" resource is present on the classpath). + */ + DISABLED, + + /** + * Switches on AspectJ load-time weaving if a "META-INF/aop.xml" resource + * is present in the classpath. If there is no such resource, then AspectJ + * load-time weaving will be switched off. + */ + AUTODETECT; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/EnableMBeanExport.java b/spring-context/src/main/java/org/springframework/context/annotation/EnableMBeanExport.java new file mode 100644 index 0000000..ff919d3 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/EnableMBeanExport.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.jmx.export.annotation.AnnotationMBeanExporter; +import org.springframework.jmx.support.RegistrationPolicy; + +/** + * Enables default exporting of all standard {@code MBean}s from the Spring context, as + * well as well all {@code @ManagedResource} annotated beans. + * + *

    The resulting {@link org.springframework.jmx.export.MBeanExporter MBeanExporter} + * bean is defined under the name "mbeanExporter". Alternatively, consider defining a + * custom {@link AnnotationMBeanExporter} bean explicitly. + * + *

    This annotation is modeled after and functionally equivalent to Spring XML's + * {@code } element. + * + * @author Phillip Webb + * @since 3.2 + * @see MBeanExportConfiguration + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Import(MBeanExportConfiguration.class) +public @interface EnableMBeanExport { + + /** + * The default domain to use when generating JMX ObjectNames. + */ + String defaultDomain() default ""; + + /** + * The bean name of the MBeanServer to which MBeans should be exported. Default is to + * use the platform's default MBeanServer. + */ + String server() default ""; + + /** + * The policy to use when attempting to register an MBean under an + * {@link javax.management.ObjectName} that already exists. Defaults to + * {@link RegistrationPolicy#FAIL_ON_EXISTING}. + */ + RegistrationPolicy registration() default RegistrationPolicy.FAIL_ON_EXISTING; +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/FilterType.java b/spring-context/src/main/java/org/springframework/context/annotation/FilterType.java new file mode 100644 index 0000000..36466ff --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/FilterType.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +/** + * Enumeration of the type filters that may be used in conjunction with + * {@link ComponentScan @ComponentScan}. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @author Chris Beams + * @since 2.5 + * @see ComponentScan + * @see ComponentScan#includeFilters() + * @see ComponentScan#excludeFilters() + * @see org.springframework.core.type.filter.TypeFilter + */ +public enum FilterType { + + /** + * Filter candidates marked with a given annotation. + * @see org.springframework.core.type.filter.AnnotationTypeFilter + */ + ANNOTATION, + + /** + * Filter candidates assignable to a given type. + * @see org.springframework.core.type.filter.AssignableTypeFilter + */ + ASSIGNABLE_TYPE, + + /** + * Filter candidates matching a given AspectJ type pattern expression. + * @see org.springframework.core.type.filter.AspectJTypeFilter + */ + ASPECTJ, + + /** + * Filter candidates matching a given regex pattern. + * @see org.springframework.core.type.filter.RegexPatternTypeFilter + */ + REGEX, + + /** Filter candidates using a given custom + * {@link org.springframework.core.type.filter.TypeFilter} implementation. + */ + CUSTOM + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/FullyQualifiedAnnotationBeanNameGenerator.java b/spring-context/src/main/java/org/springframework/context/annotation/FullyQualifiedAnnotationBeanNameGenerator.java new file mode 100644 index 0000000..d0d9b86 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/FullyQualifiedAnnotationBeanNameGenerator.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.util.Assert; + +/** + * An extension of {@code AnnotationBeanNameGenerator} that uses the fully qualified + * class name as the default bean name if an explicit bean name is not supplied via + * a supported type-level annotation such as {@code @Component} (see + * {@link AnnotationBeanNameGenerator} for details on supported annotations). + * + *

    Favor this bean naming strategy over {@code AnnotationBeanNameGenerator} if + * you run into naming conflicts due to multiple autodetected components having the + * same non-qualified class name (i.e., classes with identical names but residing in + * different packages). + * + *

    Note that an instance of this class is used by default for configuration-level + * import purposes; whereas, the default for component scanning purposes is a plain + * {@code AnnotationBeanNameGenerator}. + * + * @author Juergen Hoeller + * @author Sam Brannen + * @since 5.2.3 + * @see org.springframework.beans.factory.support.DefaultBeanNameGenerator + * @see AnnotationBeanNameGenerator + * @see ConfigurationClassPostProcessor#IMPORT_BEAN_NAME_GENERATOR + */ +public class FullyQualifiedAnnotationBeanNameGenerator extends AnnotationBeanNameGenerator { + + /** + * A convenient constant for a default {@code FullyQualifiedAnnotationBeanNameGenerator} + * instance, as used for configuration-level import purposes. + * @since 5.2.11 + */ + public static final FullyQualifiedAnnotationBeanNameGenerator INSTANCE = + new FullyQualifiedAnnotationBeanNameGenerator(); + + + @Override + protected String buildDefaultBeanName(BeanDefinition definition) { + String beanClassName = definition.getBeanClassName(); + Assert.state(beanClassName != null, "No bean class name set"); + return beanClassName; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Import.java b/spring-context/src/main/java/org/springframework/context/annotation/Import.java new file mode 100644 index 0000000..9c905d0 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/Import.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates one or more component classes to import — typically + * {@link Configuration @Configuration} classes. + * + *

    Provides functionality equivalent to the {@code } element in Spring XML. + * Allows for importing {@code @Configuration} classes, {@link ImportSelector} and + * {@link ImportBeanDefinitionRegistrar} implementations, as well as regular component + * classes (as of 4.2; analogous to {@link AnnotationConfigApplicationContext#register}). + * + *

    {@code @Bean} definitions declared in imported {@code @Configuration} classes should be + * accessed by using {@link org.springframework.beans.factory.annotation.Autowired @Autowired} + * injection. Either the bean itself can be autowired, or the configuration class instance + * declaring the bean can be autowired. The latter approach allows for explicit, IDE-friendly + * navigation between {@code @Configuration} class methods. + * + *

    May be declared at the class level or as a meta-annotation. + * + *

    If XML or other non-{@code @Configuration} bean definition resources need to be + * imported, use the {@link ImportResource @ImportResource} annotation instead. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.0 + * @see Configuration + * @see ImportSelector + * @see ImportBeanDefinitionRegistrar + * @see ImportResource + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Import { + + /** + * {@link Configuration @Configuration}, {@link ImportSelector}, + * {@link ImportBeanDefinitionRegistrar}, or regular component classes to import. + */ + Class[] value(); + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ImportAware.java b/spring-context/src/main/java/org/springframework/context/annotation/ImportAware.java new file mode 100644 index 0000000..c4bdd42 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ImportAware.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2011 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.springframework.beans.factory.Aware; +import org.springframework.core.type.AnnotationMetadata; + +/** + * Interface to be implemented by any @{@link Configuration} class that wishes + * to be injected with the {@link AnnotationMetadata} of the @{@code Configuration} + * class that imported it. Useful in conjunction with annotations that + * use @{@link Import} as a meta-annotation. + * + * @author Chris Beams + * @since 3.1 + */ +public interface ImportAware extends Aware { + + /** + * Set the annotation metadata of the importing @{@code Configuration} class. + */ + void setImportMetadata(AnnotationMetadata importMetadata); + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ImportBeanDefinitionRegistrar.java b/spring-context/src/main/java/org/springframework/context/annotation/ImportBeanDefinitionRegistrar.java new file mode 100644 index 0000000..68fdeda --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ImportBeanDefinitionRegistrar.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.core.type.AnnotationMetadata; + +/** + * Interface to be implemented by types that register additional bean definitions when + * processing @{@link Configuration} classes. Useful when operating at the bean definition + * level (as opposed to {@code @Bean} method/instance level) is desired or necessary. + * + *

    Along with {@code @Configuration} and {@link ImportSelector}, classes of this type + * may be provided to the @{@link Import} annotation (or may also be returned from an + * {@code ImportSelector}). + * + *

    An {@link ImportBeanDefinitionRegistrar} may implement any of the following + * {@link org.springframework.beans.factory.Aware Aware} interfaces, and their respective + * methods will be called prior to {@link #registerBeanDefinitions}: + *

      + *
    • {@link org.springframework.context.EnvironmentAware EnvironmentAware}
    • + *
    • {@link org.springframework.beans.factory.BeanFactoryAware BeanFactoryAware} + *
    • {@link org.springframework.beans.factory.BeanClassLoaderAware BeanClassLoaderAware} + *
    • {@link org.springframework.context.ResourceLoaderAware ResourceLoaderAware} + *
    + * + *

    Alternatively, the class may provide a single constructor with one or more of + * the following supported parameter types: + *

      + *
    • {@link org.springframework.core.env.Environment Environment}
    • + *
    • {@link org.springframework.beans.factory.BeanFactory BeanFactory}
    • + *
    • {@link java.lang.ClassLoader ClassLoader}
    • + *
    • {@link org.springframework.core.io.ResourceLoader ResourceLoader}
    • + *
    + * + *

    See implementations and associated unit tests for usage examples. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + * @see Import + * @see ImportSelector + * @see Configuration + */ +public interface ImportBeanDefinitionRegistrar { + + /** + * Register bean definitions as necessary based on the given annotation metadata of + * the importing {@code @Configuration} class. + *

    Note that {@link BeanDefinitionRegistryPostProcessor} types may not be + * registered here, due to lifecycle constraints related to {@code @Configuration} + * class processing. + *

    The default implementation delegates to + * {@link #registerBeanDefinitions(AnnotationMetadata, BeanDefinitionRegistry)}. + * @param importingClassMetadata annotation metadata of the importing class + * @param registry current bean definition registry + * @param importBeanNameGenerator the bean name generator strategy for imported beans: + * {@link ConfigurationClassPostProcessor#IMPORT_BEAN_NAME_GENERATOR} by default, or a + * user-provided one if {@link ConfigurationClassPostProcessor#setBeanNameGenerator} + * has been set. In the latter case, the passed-in strategy will be the same used for + * component scanning in the containing application context (otherwise, the default + * component-scan naming strategy is {@link AnnotationBeanNameGenerator#INSTANCE}). + * @since 5.2 + * @see ConfigurationClassPostProcessor#IMPORT_BEAN_NAME_GENERATOR + * @see ConfigurationClassPostProcessor#setBeanNameGenerator + */ + default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, + BeanNameGenerator importBeanNameGenerator) { + + registerBeanDefinitions(importingClassMetadata, registry); + } + + /** + * Register bean definitions as necessary based on the given annotation metadata of + * the importing {@code @Configuration} class. + *

    Note that {@link BeanDefinitionRegistryPostProcessor} types may not be + * registered here, due to lifecycle constraints related to {@code @Configuration} + * class processing. + *

    The default implementation is empty. + * @param importingClassMetadata annotation metadata of the importing class + * @param registry current bean definition registry + */ + default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ImportRegistry.java b/spring-context/src/main/java/org/springframework/context/annotation/ImportRegistry.java new file mode 100644 index 0000000..779bbc3 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ImportRegistry.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.lang.Nullable; + +/** + * Registry of imported class {@link AnnotationMetadata}. + * + * @author Juergen Hoeller + * @author Phillip Webb + */ +interface ImportRegistry { + + @Nullable + AnnotationMetadata getImportingClassFor(String importedClass); + + void removeImportingClass(String importingClass); + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ImportResource.java b/spring-context/src/main/java/org/springframework/context/annotation/ImportResource.java new file mode 100644 index 0000000..da1cb97 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ImportResource.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.beans.factory.support.BeanDefinitionReader; +import org.springframework.core.annotation.AliasFor; + +/** + * Indicates one or more resources containing bean definitions to import. + * + *

    Like {@link Import @Import}, this annotation provides functionality similar to + * the {@code } element in Spring XML. It is typically used when designing + * {@link Configuration @Configuration} classes to be bootstrapped by an + * {@link AnnotationConfigApplicationContext}, but where some XML functionality such + * as namespaces is still necessary. + * + *

    By default, arguments to the {@link #value} attribute will be processed using a + * {@link org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader GroovyBeanDefinitionReader} + * if ending in {@code ".groovy"}; otherwise, an + * {@link org.springframework.beans.factory.xml.XmlBeanDefinitionReader XmlBeanDefinitionReader} + * will be used to parse Spring {@code } XML files. Optionally, the {@link #reader} + * attribute may be declared, allowing the user to choose a custom {@link BeanDefinitionReader} + * implementation. + * + * @author Chris Beams + * @author Juergen Hoeller + * @author Sam Brannen + * @since 3.0 + * @see Configuration + * @see Import + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Documented +public @interface ImportResource { + + /** + * Alias for {@link #locations}. + * @see #locations + * @see #reader + */ + @AliasFor("locations") + String[] value() default {}; + + /** + * Resource locations from which to import. + *

    Supports resource-loading prefixes such as {@code classpath:}, + * {@code file:}, etc. + *

    Consult the Javadoc for {@link #reader} for details on how resources + * will be processed. + * @since 4.2 + * @see #value + * @see #reader + */ + @AliasFor("value") + String[] locations() default {}; + + /** + * {@link BeanDefinitionReader} implementation to use when processing + * resources specified via the {@link #value} attribute. + *

    By default, the reader will be adapted to the resource path specified: + * {@code ".groovy"} files will be processed with a + * {@link org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader GroovyBeanDefinitionReader}; + * whereas, all other resources will be processed with an + * {@link org.springframework.beans.factory.xml.XmlBeanDefinitionReader XmlBeanDefinitionReader}. + * @see #value + */ + Class reader() default BeanDefinitionReader.class; + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ImportSelector.java b/spring-context/src/main/java/org/springframework/context/annotation/ImportSelector.java new file mode 100644 index 0000000..7ae3423 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ImportSelector.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.util.function.Predicate; + +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.lang.Nullable; + +/** + * Interface to be implemented by types that determine which @{@link Configuration} + * class(es) should be imported based on a given selection criteria, usually one or + * more annotation attributes. + * + *

    An {@link ImportSelector} may implement any of the following + * {@link org.springframework.beans.factory.Aware Aware} interfaces, + * and their respective methods will be called prior to {@link #selectImports}: + *

      + *
    • {@link org.springframework.context.EnvironmentAware EnvironmentAware}
    • + *
    • {@link org.springframework.beans.factory.BeanFactoryAware BeanFactoryAware}
    • + *
    • {@link org.springframework.beans.factory.BeanClassLoaderAware BeanClassLoaderAware}
    • + *
    • {@link org.springframework.context.ResourceLoaderAware ResourceLoaderAware}
    • + *
    + * + *

    Alternatively, the class may provide a single constructor with one or more of + * the following supported parameter types: + *

      + *
    • {@link org.springframework.core.env.Environment Environment}
    • + *
    • {@link org.springframework.beans.factory.BeanFactory BeanFactory}
    • + *
    • {@link java.lang.ClassLoader ClassLoader}
    • + *
    • {@link org.springframework.core.io.ResourceLoader ResourceLoader}
    • + *
    + * + *

    {@code ImportSelector} implementations are usually processed in the same way + * as regular {@code @Import} annotations, however, it is also possible to defer + * selection of imports until all {@code @Configuration} classes have been processed + * (see {@link DeferredImportSelector} for details). + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + * @see DeferredImportSelector + * @see Import + * @see ImportBeanDefinitionRegistrar + * @see Configuration + */ +public interface ImportSelector { + + /** + * Select and return the names of which class(es) should be imported based on + * the {@link AnnotationMetadata} of the importing @{@link Configuration} class. + * @return the class names, or an empty array if none + */ + String[] selectImports(AnnotationMetadata importingClassMetadata); + + /** + * Return a predicate for excluding classes from the import candidates, to be + * transitively applied to all classes found through this selector's imports. + *

    If this predicate returns {@code true} for a given fully-qualified + * class name, said class will not be considered as an imported configuration + * class, bypassing class file loading as well as metadata introspection. + * @return the filter predicate for fully-qualified candidate class names + * of transitively imported configuration classes, or {@code null} if none + * @since 5.2.4 + */ + @Nullable + default Predicate getExclusionFilter() { + return null; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Jsr330ScopeMetadataResolver.java b/spring-context/src/main/java/org/springframework/context/annotation/Jsr330ScopeMetadataResolver.java new file mode 100644 index 0000000..d9e033a --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/Jsr330ScopeMetadataResolver.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.lang.Nullable; + +/** + * Simple {@link ScopeMetadataResolver} implementation that follows JSR-330 scoping rules: + * defaulting to prototype scope unless {@link javax.inject.Singleton} is present. + * + *

    This scope resolver can be used with {@link ClassPathBeanDefinitionScanner} and + * {@link AnnotatedBeanDefinitionReader} for standard JSR-330 compliance. However, + * in practice, you will typically use Spring's rich default scoping instead - or extend + * this resolver with custom scoping annotations that point to extended Spring scopes. + * + * @author Juergen Hoeller + * @since 3.0 + * @see #registerScope + * @see #resolveScopeName + * @see ClassPathBeanDefinitionScanner#setScopeMetadataResolver + * @see AnnotatedBeanDefinitionReader#setScopeMetadataResolver + */ +public class Jsr330ScopeMetadataResolver implements ScopeMetadataResolver { + + private final Map scopeMap = new HashMap<>(); + + + public Jsr330ScopeMetadataResolver() { + registerScope("javax.inject.Singleton", BeanDefinition.SCOPE_SINGLETON); + } + + + /** + * Register an extended JSR-330 scope annotation, mapping it onto a + * specific Spring scope by name. + * @param annotationType the JSR-330 annotation type as a Class + * @param scopeName the Spring scope name + */ + public final void registerScope(Class annotationType, String scopeName) { + this.scopeMap.put(annotationType.getName(), scopeName); + } + + /** + * Register an extended JSR-330 scope annotation, mapping it onto a + * specific Spring scope by name. + * @param annotationType the JSR-330 annotation type by name + * @param scopeName the Spring scope name + */ + public final void registerScope(String annotationType, String scopeName) { + this.scopeMap.put(annotationType, scopeName); + } + + /** + * Resolve the given annotation type into a named Spring scope. + *

    The default implementation simply checks against registered scopes. + * Can be overridden for custom mapping rules, e.g. naming conventions. + * @param annotationType the JSR-330 annotation type + * @return the Spring scope name + */ + @Nullable + protected String resolveScopeName(String annotationType) { + return this.scopeMap.get(annotationType); + } + + + @Override + public ScopeMetadata resolveScopeMetadata(BeanDefinition definition) { + ScopeMetadata metadata = new ScopeMetadata(); + metadata.setScopeName(BeanDefinition.SCOPE_PROTOTYPE); + if (definition instanceof AnnotatedBeanDefinition) { + AnnotatedBeanDefinition annDef = (AnnotatedBeanDefinition) definition; + Set annTypes = annDef.getMetadata().getAnnotationTypes(); + String found = null; + for (String annType : annTypes) { + Set metaAnns = annDef.getMetadata().getMetaAnnotationTypes(annType); + if (metaAnns.contains("javax.inject.Scope")) { + if (found != null) { + throw new IllegalStateException("Found ambiguous scope annotations on bean class [" + + definition.getBeanClassName() + "]: " + found + ", " + annType); + } + found = annType; + String scopeName = resolveScopeName(annType); + if (scopeName == null) { + throw new IllegalStateException( + "Unsupported scope annotation - not mapped onto Spring scope name: " + annType); + } + metadata.setScopeName(scopeName); + } + } + } + return metadata; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Lazy.java b/spring-context/src/main/java/org/springframework/context/annotation/Lazy.java new file mode 100644 index 0000000..9d04a9d --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/Lazy.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates whether a bean is to be lazily initialized. + * + *

    May be used on any class directly or indirectly annotated with {@link + * org.springframework.stereotype.Component @Component} or on methods annotated with + * {@link Bean @Bean}. + * + *

    If this annotation is not present on a {@code @Component} or {@code @Bean} definition, + * eager initialization will occur. If present and set to {@code true}, the {@code @Bean} or + * {@code @Component} will not be initialized until referenced by another bean or explicitly + * retrieved from the enclosing {@link org.springframework.beans.factory.BeanFactory + * BeanFactory}. If present and set to {@code false}, the bean will be instantiated on + * startup by bean factories that perform eager initialization of singletons. + * + *

    If Lazy is present on a {@link Configuration @Configuration} class, this + * indicates that all {@code @Bean} methods within that {@code @Configuration} + * should be lazily initialized. If {@code @Lazy} is present and false on a {@code @Bean} + * method within a {@code @Lazy}-annotated {@code @Configuration} class, this indicates + * overriding the 'default lazy' behavior and that the bean should be eagerly initialized. + * + *

    In addition to its role for component initialization, this annotation may also be placed + * on injection points marked with {@link org.springframework.beans.factory.annotation.Autowired} + * or {@link javax.inject.Inject}: In that context, it leads to the creation of a + * lazy-resolution proxy for all affected dependencies, as an alternative to using + * {@link org.springframework.beans.factory.ObjectFactory} or {@link javax.inject.Provider}. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.0 + * @see Primary + * @see Bean + * @see Configuration + * @see org.springframework.stereotype.Component + */ +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Lazy { + + /** + * Whether lazy initialization should occur. + */ + boolean value() default true; + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/LoadTimeWeavingConfiguration.java b/spring-context/src/main/java/org/springframework/context/annotation/LoadTimeWeavingConfiguration.java new file mode 100644 index 0000000..477de75 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/LoadTimeWeavingConfiguration.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.EnableLoadTimeWeaving.AspectJWeaving; +import org.springframework.context.weaving.AspectJWeavingEnabler; +import org.springframework.context.weaving.DefaultContextLoadTimeWeaver; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.instrument.classloading.LoadTimeWeaver; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * {@code @Configuration} class that registers a {@link LoadTimeWeaver} bean. + * + *

    This configuration class is automatically imported when using the + * {@link EnableLoadTimeWeaving} annotation. See {@code @EnableLoadTimeWeaving} + * javadoc for complete usage details. + * + * @author Chris Beams + * @since 3.1 + * @see LoadTimeWeavingConfigurer + * @see ConfigurableApplicationContext#LOAD_TIME_WEAVER_BEAN_NAME + */ +@Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +public class LoadTimeWeavingConfiguration implements ImportAware, BeanClassLoaderAware { + + @Nullable + private AnnotationAttributes enableLTW; + + @Nullable + private LoadTimeWeavingConfigurer ltwConfigurer; + + @Nullable + private ClassLoader beanClassLoader; + + + @Override + public void setImportMetadata(AnnotationMetadata importMetadata) { + this.enableLTW = AnnotationConfigUtils.attributesFor(importMetadata, EnableLoadTimeWeaving.class); + if (this.enableLTW == null) { + throw new IllegalArgumentException( + "@EnableLoadTimeWeaving is not present on importing class " + importMetadata.getClassName()); + } + } + + @Autowired(required = false) + public void setLoadTimeWeavingConfigurer(LoadTimeWeavingConfigurer ltwConfigurer) { + this.ltwConfigurer = ltwConfigurer; + } + + @Override + public void setBeanClassLoader(ClassLoader beanClassLoader) { + this.beanClassLoader = beanClassLoader; + } + + + @Bean(name = ConfigurableApplicationContext.LOAD_TIME_WEAVER_BEAN_NAME) + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public LoadTimeWeaver loadTimeWeaver() { + Assert.state(this.beanClassLoader != null, "No ClassLoader set"); + LoadTimeWeaver loadTimeWeaver = null; + + if (this.ltwConfigurer != null) { + // The user has provided a custom LoadTimeWeaver instance + loadTimeWeaver = this.ltwConfigurer.getLoadTimeWeaver(); + } + + if (loadTimeWeaver == null) { + // No custom LoadTimeWeaver provided -> fall back to the default + loadTimeWeaver = new DefaultContextLoadTimeWeaver(this.beanClassLoader); + } + + if (this.enableLTW != null) { + AspectJWeaving aspectJWeaving = this.enableLTW.getEnum("aspectjWeaving"); + switch (aspectJWeaving) { + case DISABLED: + // AJ weaving is disabled -> do nothing + break; + case AUTODETECT: + if (this.beanClassLoader.getResource(AspectJWeavingEnabler.ASPECTJ_AOP_XML_RESOURCE) == null) { + // No aop.xml present on the classpath -> treat as 'disabled' + break; + } + // aop.xml is present on the classpath -> enable + AspectJWeavingEnabler.enableAspectJWeaving(loadTimeWeaver, this.beanClassLoader); + break; + case ENABLED: + AspectJWeavingEnabler.enableAspectJWeaving(loadTimeWeaver, this.beanClassLoader); + break; + } + } + + return loadTimeWeaver; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/LoadTimeWeavingConfigurer.java b/spring-context/src/main/java/org/springframework/context/annotation/LoadTimeWeavingConfigurer.java new file mode 100644 index 0000000..ca0a9d8 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/LoadTimeWeavingConfigurer.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.springframework.instrument.classloading.LoadTimeWeaver; + +/** + * Interface to be implemented by + * {@link org.springframework.context.annotation.Configuration @Configuration} + * classes annotated with {@link EnableLoadTimeWeaving @EnableLoadTimeWeaving} that wish to + * customize the {@link LoadTimeWeaver} instance to be used. + * + *

    See {@link org.springframework.scheduling.annotation.EnableAsync @EnableAsync} + * for usage examples and information on how a default {@code LoadTimeWeaver} + * is selected when this interface is not used. + * + * @author Chris Beams + * @since 3.1 + * @see LoadTimeWeavingConfiguration + * @see EnableLoadTimeWeaving + */ +public interface LoadTimeWeavingConfigurer { + + /** + * Create, configure and return the {@code LoadTimeWeaver} instance to be used. Note + * that it is unnecessary to annotate this method with {@code @Bean}, because the + * object returned will automatically be registered as a bean by + * {@link LoadTimeWeavingConfiguration#loadTimeWeaver()} + */ + LoadTimeWeaver getLoadTimeWeaver(); + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/MBeanExportConfiguration.java b/spring-context/src/main/java/org/springframework/context/annotation/MBeanExportConfiguration.java new file mode 100644 index 0000000..ea4f5b6 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/MBeanExportConfiguration.java @@ -0,0 +1,189 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.util.Map; + +import javax.management.MBeanServer; +import javax.naming.NamingException; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.env.Environment; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.jmx.MBeanServerNotFoundException; +import org.springframework.jmx.export.annotation.AnnotationMBeanExporter; +import org.springframework.jmx.support.RegistrationPolicy; +import org.springframework.jmx.support.WebSphereMBeanServerFactoryBean; +import org.springframework.jndi.JndiLocatorDelegate; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * {@code @Configuration} class that registers a {@link AnnotationMBeanExporter} bean. + * + *

    This configuration class is automatically imported when using the + * {@link EnableMBeanExport} annotation. See its javadoc for complete usage details. + * + * @author Phillip Webb + * @author Chris Beams + * @since 3.2 + * @see EnableMBeanExport + */ +@Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +public class MBeanExportConfiguration implements ImportAware, EnvironmentAware, BeanFactoryAware { + + private static final String MBEAN_EXPORTER_BEAN_NAME = "mbeanExporter"; + + @Nullable + private AnnotationAttributes enableMBeanExport; + + @Nullable + private Environment environment; + + @Nullable + private BeanFactory beanFactory; + + + @Override + public void setImportMetadata(AnnotationMetadata importMetadata) { + Map map = importMetadata.getAnnotationAttributes(EnableMBeanExport.class.getName()); + this.enableMBeanExport = AnnotationAttributes.fromMap(map); + if (this.enableMBeanExport == null) { + throw new IllegalArgumentException( + "@EnableMBeanExport is not present on importing class " + importMetadata.getClassName()); + } + } + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + + @Bean(name = MBEAN_EXPORTER_BEAN_NAME) + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public AnnotationMBeanExporter mbeanExporter() { + AnnotationMBeanExporter exporter = new AnnotationMBeanExporter(); + Assert.state(this.enableMBeanExport != null, "No EnableMBeanExport annotation found"); + setupDomain(exporter, this.enableMBeanExport); + setupServer(exporter, this.enableMBeanExport); + setupRegistrationPolicy(exporter, this.enableMBeanExport); + return exporter; + } + + private void setupDomain(AnnotationMBeanExporter exporter, AnnotationAttributes enableMBeanExport) { + String defaultDomain = enableMBeanExport.getString("defaultDomain"); + if (StringUtils.hasLength(defaultDomain) && this.environment != null) { + defaultDomain = this.environment.resolvePlaceholders(defaultDomain); + } + if (StringUtils.hasText(defaultDomain)) { + exporter.setDefaultDomain(defaultDomain); + } + } + + private void setupServer(AnnotationMBeanExporter exporter, AnnotationAttributes enableMBeanExport) { + String server = enableMBeanExport.getString("server"); + if (StringUtils.hasLength(server) && this.environment != null) { + server = this.environment.resolvePlaceholders(server); + } + if (StringUtils.hasText(server)) { + Assert.state(this.beanFactory != null, "No BeanFactory set"); + exporter.setServer(this.beanFactory.getBean(server, MBeanServer.class)); + } + else { + SpecificPlatform specificPlatform = SpecificPlatform.get(); + if (specificPlatform != null) { + MBeanServer mbeanServer = specificPlatform.getMBeanServer(); + if (mbeanServer != null) { + exporter.setServer(mbeanServer); + } + } + } + } + + private void setupRegistrationPolicy(AnnotationMBeanExporter exporter, AnnotationAttributes enableMBeanExport) { + RegistrationPolicy registrationPolicy = enableMBeanExport.getEnum("registration"); + exporter.setRegistrationPolicy(registrationPolicy); + } + + + /** + * Specific platforms that might need custom MBean handling. + */ + public enum SpecificPlatform { + + /** + * Weblogic. + */ + WEBLOGIC("weblogic.management.Helper") { + @Override + public MBeanServer getMBeanServer() { + try { + return new JndiLocatorDelegate().lookup("java:comp/env/jmx/runtime", MBeanServer.class); + } + catch (NamingException ex) { + throw new MBeanServerNotFoundException("Failed to retrieve WebLogic MBeanServer from JNDI", ex); + } + } + }, + + /** + * Websphere. + */ + WEBSPHERE("com.ibm.websphere.management.AdminServiceFactory") { + @Override + public MBeanServer getMBeanServer() { + WebSphereMBeanServerFactoryBean fb = new WebSphereMBeanServerFactoryBean(); + fb.afterPropertiesSet(); + return fb.getObject(); + } + }; + + private final String identifyingClass; + + SpecificPlatform(String identifyingClass) { + this.identifyingClass = identifyingClass; + } + + @Nullable + public abstract MBeanServer getMBeanServer(); + + @Nullable + public static SpecificPlatform get() { + ClassLoader classLoader = MBeanExportConfiguration.class.getClassLoader(); + for (SpecificPlatform environment : values()) { + if (ClassUtils.isPresent(environment.identifyingClass, classLoader)) { + return environment; + } + } + return null; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ParserStrategyUtils.java b/spring-context/src/main/java/org/springframework/context/annotation/ParserStrategyUtils.java new file mode 100644 index 0000000..0aa2965 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ParserStrategyUtils.java @@ -0,0 +1,140 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.reflect.Constructor; + +import org.springframework.beans.BeanInstantiationException; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.Aware; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ResourceLoader; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Common delegate code for the handling of parser strategies, e.g. + * {@code TypeFilter}, {@code ImportSelector}, {@code ImportBeanDefinitionRegistrar} + * + * @author Juergen Hoeller + * @author Phillip Webb + * @since 4.3.3 + */ +abstract class ParserStrategyUtils { + + /** + * Instantiate a class using an appropriate constructor and return the new + * instance as the specified assignable type. The returned instance will + * have {@link BeanClassLoaderAware}, {@link BeanFactoryAware}, + * {@link EnvironmentAware}, and {@link ResourceLoaderAware} contracts + * invoked if they are implemented by the given object. + * @since 5.2 + */ + @SuppressWarnings("unchecked") + static T instantiateClass(Class clazz, Class assignableTo, Environment environment, + ResourceLoader resourceLoader, BeanDefinitionRegistry registry) { + + Assert.notNull(clazz, "Class must not be null"); + Assert.isAssignable(assignableTo, clazz); + if (clazz.isInterface()) { + throw new BeanInstantiationException(clazz, "Specified class is an interface"); + } + ClassLoader classLoader = (registry instanceof ConfigurableBeanFactory ? + ((ConfigurableBeanFactory) registry).getBeanClassLoader() : resourceLoader.getClassLoader()); + T instance = (T) createInstance(clazz, environment, resourceLoader, registry, classLoader); + ParserStrategyUtils.invokeAwareMethods(instance, environment, resourceLoader, registry, classLoader); + return instance; + } + + private static Object createInstance(Class clazz, Environment environment, + ResourceLoader resourceLoader, BeanDefinitionRegistry registry, + @Nullable ClassLoader classLoader) { + + Constructor[] constructors = clazz.getDeclaredConstructors(); + if (constructors.length == 1 && constructors[0].getParameterCount() > 0) { + try { + Constructor constructor = constructors[0]; + Object[] args = resolveArgs(constructor.getParameterTypes(), + environment, resourceLoader, registry, classLoader); + return BeanUtils.instantiateClass(constructor, args); + } + catch (Exception ex) { + throw new BeanInstantiationException(clazz, "No suitable constructor found", ex); + } + } + return BeanUtils.instantiateClass(clazz); + } + + private static Object[] resolveArgs(Class[] parameterTypes, + Environment environment, ResourceLoader resourceLoader, + BeanDefinitionRegistry registry, @Nullable ClassLoader classLoader) { + + Object[] parameters = new Object[parameterTypes.length]; + for (int i = 0; i < parameterTypes.length; i++) { + parameters[i] = resolveParameter(parameterTypes[i], environment, + resourceLoader, registry, classLoader); + } + return parameters; + } + + @Nullable + private static Object resolveParameter(Class parameterType, + Environment environment, ResourceLoader resourceLoader, + BeanDefinitionRegistry registry, @Nullable ClassLoader classLoader) { + + if (parameterType == Environment.class) { + return environment; + } + if (parameterType == ResourceLoader.class) { + return resourceLoader; + } + if (parameterType == BeanFactory.class) { + return (registry instanceof BeanFactory ? registry : null); + } + if (parameterType == ClassLoader.class) { + return classLoader; + } + throw new IllegalStateException("Illegal method parameter type: " + parameterType.getName()); + } + + private static void invokeAwareMethods(Object parserStrategyBean, Environment environment, + ResourceLoader resourceLoader, BeanDefinitionRegistry registry, @Nullable ClassLoader classLoader) { + + if (parserStrategyBean instanceof Aware) { + if (parserStrategyBean instanceof BeanClassLoaderAware && classLoader != null) { + ((BeanClassLoaderAware) parserStrategyBean).setBeanClassLoader(classLoader); + } + if (parserStrategyBean instanceof BeanFactoryAware && registry instanceof BeanFactory) { + ((BeanFactoryAware) parserStrategyBean).setBeanFactory((BeanFactory) registry); + } + if (parserStrategyBean instanceof EnvironmentAware) { + ((EnvironmentAware) parserStrategyBean).setEnvironment(environment); + } + if (parserStrategyBean instanceof ResourceLoaderAware) { + ((ResourceLoaderAware) parserStrategyBean).setResourceLoader(resourceLoader); + } + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Primary.java b/spring-context/src/main/java/org/springframework/context/annotation/Primary.java new file mode 100644 index 0000000..3832996 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/Primary.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that a bean should be given preference when multiple candidates + * are qualified to autowire a single-valued dependency. If exactly one + * 'primary' bean exists among the candidates, it will be the autowired value. + * + *

    This annotation is semantically equivalent to the {@code } element's + * {@code primary} attribute in Spring XML. + * + *

    May be used on any class directly or indirectly annotated with + * {@code @Component} or on methods annotated with @{@link Bean}. + * + *

    Example

    + *
    + * @Component
    + * public class FooService {
    + *
    + *     private FooRepository fooRepository;
    + *
    + *     @Autowired
    + *     public FooService(FooRepository fooRepository) {
    + *         this.fooRepository = fooRepository;
    + *     }
    + * }
    + *
    + * @Component
    + * public class JdbcFooRepository extends FooRepository {
    + *
    + *     public JdbcFooRepository(DataSource dataSource) {
    + *         // ...
    + *     }
    + * }
    + *
    + * @Primary
    + * @Component
    + * public class HibernateFooRepository extends FooRepository {
    + *
    + *     public HibernateFooRepository(SessionFactory sessionFactory) {
    + *         // ...
    + *     }
    + * }
    + * 
    + * + *

    Because {@code HibernateFooRepository} is marked with {@code @Primary}, + * it will be injected preferentially over the jdbc-based variant assuming both + * are present as beans within the same Spring application context, which is + * often the case when component-scanning is applied liberally. + * + *

    Note that using {@code @Primary} at the class level has no effect unless + * component-scanning is being used. If a {@code @Primary}-annotated class is + * declared via XML, {@code @Primary} annotation metadata is ignored, and + * {@code } is respected instead. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.0 + * @see Lazy + * @see Bean + * @see ComponentScan + * @see org.springframework.stereotype.Component + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Primary { + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Profile.java b/spring-context/src/main/java/org/springframework/context/annotation/Profile.java new file mode 100644 index 0000000..2dae204 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/Profile.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.env.AbstractEnvironment; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Profiles; + +/** + * Indicates that a component is eligible for registration when one or more + * {@linkplain #value specified profiles} are active. + * + *

    A profile is a named logical grouping that may be activated + * programmatically via {@link ConfigurableEnvironment#setActiveProfiles} or declaratively + * by setting the {@link AbstractEnvironment#ACTIVE_PROFILES_PROPERTY_NAME + * spring.profiles.active} property as a JVM system property, as an + * environment variable, or as a Servlet context parameter in {@code web.xml} + * for web applications. Profiles may also be activated declaratively in + * integration tests via the {@code @ActiveProfiles} annotation. + * + *

    The {@code @Profile} annotation may be used in any of the following ways: + *

      + *
    • as a type-level annotation on any class directly or indirectly annotated with + * {@code @Component}, including {@link Configuration @Configuration} classes
    • + *
    • as a meta-annotation, for the purpose of composing custom stereotype annotations
    • + *
    • as a method-level annotation on any {@link Bean @Bean} method
    • + *
    + * + *

    If a {@code @Configuration} class is marked with {@code @Profile}, all of the + * {@code @Bean} methods and {@link Import @Import} annotations associated with that class + * will be bypassed unless one or more of the specified profiles are active. A profile + * string may contain a simple profile name (for example {@code "p1"}) or a profile + * expression. A profile expression allows for more complicated profile logic to be + * expressed, for example {@code "p1 & p2"}. See {@link Profiles#of(String...)} for more + * details about supported formats. + * + *

    This is analogous to the behavior in Spring XML: if the {@code profile} attribute of + * the {@code beans} element is supplied e.g., {@code }, the + * {@code beans} element will not be parsed unless at least profile 'p1' or 'p2' has been + * activated. Likewise, if a {@code @Component} or {@code @Configuration} class is marked + * with {@code @Profile({"p1", "p2"})}, that class will not be registered or processed unless + * at least profile 'p1' or 'p2' has been activated. + * + *

    If a given profile is prefixed with the NOT operator ({@code !}), the annotated + * component will be registered if the profile is not active — for example, + * given {@code @Profile({"p1", "!p2"})}, registration will occur if profile 'p1' is active + * or if profile 'p2' is not active. + * + *

    If the {@code @Profile} annotation is omitted, registration will occur regardless + * of which (if any) profiles are active. + * + *

    NOTE: With {@code @Profile} on {@code @Bean} methods, a special scenario may + * apply: In the case of overloaded {@code @Bean} methods of the same Java method name + * (analogous to constructor overloading), an {@code @Profile} condition needs to be + * consistently declared on all overloaded methods. If the conditions are inconsistent, + * only the condition on the first declaration among the overloaded methods will matter. + * {@code @Profile} can therefore not be used to select an overloaded method with a + * particular argument signature over another; resolution between all factory methods + * for the same bean follows Spring's constructor resolution algorithm at creation time. + * Use distinct Java method names pointing to the same {@link Bean#name bean name} + * if you'd like to define alternative beans with different profile conditions; + * see {@code ProfileDatabaseConfig} in {@link Configuration @Configuration}'s javadoc. + * + *

    When defining Spring beans via XML, the {@code "profile"} attribute of the + * {@code } element may be used. See the documentation in the + * {@code spring-beans} XSD (version 3.1 or greater) for details. + * + * @author Chris Beams + * @author Phillip Webb + * @author Sam Brannen + * @since 3.1 + * @see ConfigurableEnvironment#setActiveProfiles + * @see ConfigurableEnvironment#setDefaultProfiles + * @see AbstractEnvironment#ACTIVE_PROFILES_PROPERTY_NAME + * @see AbstractEnvironment#DEFAULT_PROFILES_PROPERTY_NAME + * @see Conditional + * @see org.springframework.test.context.ActiveProfiles + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Conditional(ProfileCondition.class) +public @interface Profile { + + /** + * The set of profiles for which the annotated component should be registered. + */ + String[] value(); + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ProfileCondition.java b/spring-context/src/main/java/org/springframework/context/annotation/ProfileCondition.java new file mode 100644 index 0000000..deedc4c --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ProfileCondition.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.springframework.core.env.Profiles; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.util.MultiValueMap; + +/** + * {@link Condition} that matches based on the value of a {@link Profile @Profile} + * annotation. + * + * @author Chris Beams + * @author Phillip Webb + * @author Juergen Hoeller + * @since 4.0 + */ +class ProfileCondition implements Condition { + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + MultiValueMap attrs = metadata.getAllAnnotationAttributes(Profile.class.getName()); + if (attrs != null) { + for (Object value : attrs.get("value")) { + if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) { + return true; + } + } + return false; + } + return true; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/PropertySource.java b/spring-context/src/main/java/org/springframework/context/annotation/PropertySource.java new file mode 100644 index 0000000..3d917f6 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/PropertySource.java @@ -0,0 +1,224 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.io.support.PropertySourceFactory; + +/** + * Annotation providing a convenient and declarative mechanism for adding a + * {@link org.springframework.core.env.PropertySource PropertySource} to Spring's + * {@link org.springframework.core.env.Environment Environment}. To be used in + * conjunction with @{@link Configuration} classes. + * + *

    Example usage

    + * + *

    Given a file {@code app.properties} containing the key/value pair + * {@code testbean.name=myTestBean}, the following {@code @Configuration} class + * uses {@code @PropertySource} to contribute {@code app.properties} to the + * {@code Environment}'s set of {@code PropertySources}. + * + *

    + * @Configuration
    + * @PropertySource("classpath:/com/myco/app.properties")
    + * public class AppConfig {
    + *
    + *     @Autowired
    + *     Environment env;
    + *
    + *     @Bean
    + *     public TestBean testBean() {
    + *         TestBean testBean = new TestBean();
    + *         testBean.setName(env.getProperty("testbean.name"));
    + *         return testBean;
    + *     }
    + * }
    + * + *

    Notice that the {@code Environment} object is + * {@link org.springframework.beans.factory.annotation.Autowired @Autowired} into the + * configuration class and then used when populating the {@code TestBean} object. Given + * the configuration above, a call to {@code testBean.getName()} will return "myTestBean". + * + *

    Resolving ${...} placeholders in {@code } and {@code @Value} annotations

    + * + *

    In order to resolve ${...} placeholders in {@code } definitions or {@code @Value} + * annotations using properties from a {@code PropertySource}, you must ensure that an + * appropriate embedded value resolver is registered in the {@code BeanFactory} + * used by the {@code ApplicationContext}. This happens automatically when using + * {@code } in XML. When using {@code @Configuration} classes + * this can be achieved by explicitly registering a {@code PropertySourcesPlaceholderConfigurer} + * via a {@code static} {@code @Bean} method. Note, however, that explicit registration + * of a {@code PropertySourcesPlaceholderConfigurer} via a {@code static} {@code @Bean} + * method is typically only required if you need to customize configuration such as the + * placeholder syntax, etc. See the "Working with externalized values" section of + * {@link Configuration @Configuration}'s javadocs and "a note on + * BeanFactoryPostProcessor-returning {@code @Bean} methods" of {@link Bean @Bean}'s + * javadocs for details and examples. + * + *

    Resolving ${...} placeholders within {@code @PropertySource} resource locations

    + * + *

    Any ${...} placeholders present in a {@code @PropertySource} {@linkplain #value() + * resource location} will be resolved against the set of property sources already + * registered against the environment. For example: + * + *

    + * @Configuration
    + * @PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties")
    + * public class AppConfig {
    + *
    + *     @Autowired
    + *     Environment env;
    + *
    + *     @Bean
    + *     public TestBean testBean() {
    + *         TestBean testBean = new TestBean();
    + *         testBean.setName(env.getProperty("testbean.name"));
    + *         return testBean;
    + *     }
    + * }
    + * + *

    Assuming that "my.placeholder" is present in one of the property sources already + * registered — for example, system properties or environment variables — + * the placeholder will be resolved to the corresponding value. If not, then "default/path" + * will be used as a default. Expressing a default value (delimited by colon ":") is + * optional. If no default is specified and a property cannot be resolved, an {@code + * IllegalArgumentException} will be thrown. + * + *

    A note on property overriding with {@code @PropertySource}

    + * + *

    In cases where a given property key exists in more than one {@code .properties} + * file, the last {@code @PropertySource} annotation processed will 'win' and override + * any previous key with the same name. + * + *

    For example, given two properties files {@code a.properties} and + * {@code b.properties}, consider the following two configuration classes + * that reference them with {@code @PropertySource} annotations: + * + *

    + * @Configuration
    + * @PropertySource("classpath:/com/myco/a.properties")
    + * public class ConfigA { }
    + *
    + * @Configuration
    + * @PropertySource("classpath:/com/myco/b.properties")
    + * public class ConfigB { }
    + * 
    + * + *

    The override ordering depends on the order in which these classes are registered + * with the application context. + * + *

    + * AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    + * ctx.register(ConfigA.class);
    + * ctx.register(ConfigB.class);
    + * ctx.refresh();
    + * 
    + * + *

    In the scenario above, the properties in {@code b.properties} will override any + * duplicates that exist in {@code a.properties}, because {@code ConfigB} was registered + * last. + * + *

    In certain situations, it may not be possible or practical to tightly control + * property source ordering when using {@code @PropertySource} annotations. For example, + * if the {@code @Configuration} classes above were registered via component-scanning, + * the ordering is difficult to predict. In such cases — and if overriding is important + * — it is recommended that the user fall back to using the programmatic + * {@code PropertySource} API. See {@link org.springframework.core.env.ConfigurableEnvironment + * ConfigurableEnvironment} and {@link org.springframework.core.env.MutablePropertySources + * MutablePropertySources} javadocs for details. + * + *

    NOTE: This annotation is repeatable according to Java 8 conventions. + * However, all such {@code @PropertySource} annotations need to be declared at the same + * level: either directly on the configuration class or as meta-annotations on the + * same custom annotation. Mixing direct annotations and meta-annotations is not + * recommended since direct annotations will effectively override meta-annotations. + * + * @author Chris Beams + * @author Juergen Hoeller + * @author Phillip Webb + * @author Sam Brannen + * @since 3.1 + * @see PropertySources + * @see Configuration + * @see org.springframework.core.env.PropertySource + * @see org.springframework.core.env.ConfigurableEnvironment#getPropertySources() + * @see org.springframework.core.env.MutablePropertySources + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Repeatable(PropertySources.class) +public @interface PropertySource { + + /** + * Indicate the name of this property source. If omitted, the {@link #factory} + * will generate a name based on the underlying resource (in the case of + * {@link org.springframework.core.io.support.DefaultPropertySourceFactory}: + * derived from the resource description through a corresponding name-less + * {@link org.springframework.core.io.support.ResourcePropertySource} constructor). + * @see org.springframework.core.env.PropertySource#getName() + * @see org.springframework.core.io.Resource#getDescription() + */ + String name() default ""; + + /** + * Indicate the resource location(s) of the properties file to be loaded. + *

    Both traditional and XML-based properties file formats are supported + * — for example, {@code "classpath:/com/myco/app.properties"} + * or {@code "file:/path/to/file.xml"}. + *

    Resource location wildcards (e.g. **/*.properties) are not permitted; + * each location must evaluate to exactly one {@code .properties} or {@code .xml} + * resource. + *

    ${...} placeholders will be resolved against any/all property sources already + * registered with the {@code Environment}. See {@linkplain PropertySource above} + * for examples. + *

    Each location will be added to the enclosing {@code Environment} as its own + * property source, and in the order declared. + */ + String[] value(); + + /** + * Indicate if a failure to find a {@link #value property resource} should be + * ignored. + *

    {@code true} is appropriate if the properties file is completely optional. + *

    Default is {@code false}. + * @since 4.0 + */ + boolean ignoreResourceNotFound() default false; + + /** + * A specific character encoding for the given resources, e.g. "UTF-8". + * @since 4.3 + */ + String encoding() default ""; + + /** + * Specify a custom {@link PropertySourceFactory}, if any. + *

    By default, a default factory for standard resource files will be used. + * @since 4.3 + * @see org.springframework.core.io.support.DefaultPropertySourceFactory + * @see org.springframework.core.io.support.ResourcePropertySource + */ + Class factory() default PropertySourceFactory.class; + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/PropertySources.java b/spring-context/src/main/java/org/springframework/context/annotation/PropertySources.java new file mode 100644 index 0000000..1a21ef9 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/PropertySources.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Container annotation that aggregates several {@link PropertySource} annotations. + * + *

    Can be used natively, declaring several nested {@link PropertySource} annotations. + * Can also be used in conjunction with Java 8's support for repeatable annotations, + * where {@link PropertySource} can simply be declared several times on the same + * {@linkplain ElementType#TYPE type}, implicitly generating this container annotation. + * + * @author Phillip Webb + * @since 4.0 + * @see PropertySource + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface PropertySources { + + PropertySource[] value(); + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Role.java b/spring-context/src/main/java/org/springframework/context/annotation/Role.java new file mode 100644 index 0000000..20d2175 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/Role.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.beans.factory.config.BeanDefinition; + +/** + * Indicates the 'role' hint for a given bean. + * + *

    May be used on any class directly or indirectly annotated with + * {@link org.springframework.stereotype.Component} or on methods + * annotated with {@link Bean}. + * + *

    If this annotation is not present on a Component or Bean definition, + * the default value of {@link BeanDefinition#ROLE_APPLICATION} will apply. + * + *

    If Role is present on a {@link Configuration @Configuration} class, + * this indicates the role of the configuration class bean definition and + * does not cascade to all @{@code Bean} methods defined within. This behavior + * is different than that of the @{@link Lazy} annotation, for example. + * + * @author Chris Beams + * @since 3.1 + * @see BeanDefinition#ROLE_APPLICATION + * @see BeanDefinition#ROLE_INFRASTRUCTURE + * @see BeanDefinition#ROLE_SUPPORT + * @see Bean + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Role { + + /** + * Set the role hint for the associated bean. + * @see BeanDefinition#ROLE_APPLICATION + * @see BeanDefinition#ROLE_INFRASTRUCTURE + * @see BeanDefinition#ROLE_SUPPORT + */ + int value(); + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ScannedGenericBeanDefinition.java b/spring-context/src/main/java/org/springframework/context/annotation/ScannedGenericBeanDefinition.java new file mode 100644 index 0000000..26e603b --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ScannedGenericBeanDefinition.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.MethodMetadata; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Extension of the {@link org.springframework.beans.factory.support.GenericBeanDefinition} + * class, based on an ASM ClassReader, with support for annotation metadata exposed + * through the {@link AnnotatedBeanDefinition} interface. + * + *

    This class does not load the bean {@code Class} early. + * It rather retrieves all relevant metadata from the ".class" file itself, + * parsed with the ASM ClassReader. It is functionally equivalent to + * {@link AnnotatedGenericBeanDefinition#AnnotatedGenericBeanDefinition(AnnotationMetadata)} + * but distinguishes by type beans that have been scanned vs those that have + * been otherwise registered or detected by other means. + * + * @author Juergen Hoeller + * @author Chris Beams + * @since 2.5 + * @see #getMetadata() + * @see #getBeanClassName() + * @see org.springframework.core.type.classreading.MetadataReaderFactory + * @see AnnotatedGenericBeanDefinition + */ +@SuppressWarnings("serial") +public class ScannedGenericBeanDefinition extends GenericBeanDefinition implements AnnotatedBeanDefinition { + + private final AnnotationMetadata metadata; + + + /** + * Create a new ScannedGenericBeanDefinition for the class that the + * given MetadataReader describes. + * @param metadataReader the MetadataReader for the scanned target class + */ + public ScannedGenericBeanDefinition(MetadataReader metadataReader) { + Assert.notNull(metadataReader, "MetadataReader must not be null"); + this.metadata = metadataReader.getAnnotationMetadata(); + setBeanClassName(this.metadata.getClassName()); + setResource(metadataReader.getResource()); + } + + + @Override + public final AnnotationMetadata getMetadata() { + return this.metadata; + } + + @Override + @Nullable + public MethodMetadata getFactoryMethodMetadata() { + return null; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Scope.java b/spring-context/src/main/java/org/springframework/context/annotation/Scope.java new file mode 100644 index 0000000..6433688 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/Scope.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.annotation.AliasFor; + +/** + * When used as a type-level annotation in conjunction with + * {@link org.springframework.stereotype.Component @Component}, + * {@code @Scope} indicates the name of a scope to use for instances of + * the annotated type. + * + *

    When used as a method-level annotation in conjunction with + * {@link Bean @Bean}, {@code @Scope} indicates the name of a scope to use + * for the instance returned from the method. + * + *

    NOTE: {@code @Scope} annotations are only introspected on the + * concrete bean class (for annotated components) or the factory method + * (for {@code @Bean} methods). In contrast to XML bean definitions, + * there is no notion of bean definition inheritance, and inheritance + * hierarchies at the class level are irrelevant for metadata purposes. + * + *

    In this context, scope means the lifecycle of an instance, + * such as {@code singleton}, {@code prototype}, and so forth. Scopes + * provided out of the box in Spring may be referred to using the + * {@code SCOPE_*} constants available in the {@link ConfigurableBeanFactory} + * and {@code WebApplicationContext} interfaces. + * + *

    To register additional custom scopes, see + * {@link org.springframework.beans.factory.config.CustomScopeConfigurer + * CustomScopeConfigurer}. + * + * @author Mark Fisher + * @author Chris Beams + * @author Sam Brannen + * @since 2.5 + * @see org.springframework.stereotype.Component + * @see org.springframework.context.annotation.Bean + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Scope { + + /** + * Alias for {@link #scopeName}. + * @see #scopeName + */ + @AliasFor("scopeName") + String value() default ""; + + /** + * Specifies the name of the scope to use for the annotated component/bean. + *

    Defaults to an empty string ({@code ""}) which implies + * {@link ConfigurableBeanFactory#SCOPE_SINGLETON SCOPE_SINGLETON}. + * @since 4.2 + * @see ConfigurableBeanFactory#SCOPE_PROTOTYPE + * @see ConfigurableBeanFactory#SCOPE_SINGLETON + * @see org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST + * @see org.springframework.web.context.WebApplicationContext#SCOPE_SESSION + * @see #value + */ + @AliasFor("value") + String scopeName() default ""; + + /** + * Specifies whether a component should be configured as a scoped proxy + * and if so, whether the proxy should be interface-based or subclass-based. + *

    Defaults to {@link ScopedProxyMode#DEFAULT}, which typically indicates + * that no scoped proxy should be created unless a different default + * has been configured at the component-scan instruction level. + *

    Analogous to {@code } support in Spring XML. + * @see ScopedProxyMode + */ + ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT; + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ScopeMetadata.java b/spring-context/src/main/java/org/springframework/context/annotation/ScopeMetadata.java new file mode 100644 index 0000000..f3ed5b1 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ScopeMetadata.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.util.Assert; + +/** + * Describes scope characteristics for a Spring-managed bean including the scope + * name and the scoped-proxy behavior. + * + *

    The default scope is "singleton", and the default is to not create + * scoped-proxies. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @since 2.5 + * @see ScopeMetadataResolver + * @see ScopedProxyMode + */ +public class ScopeMetadata { + + private String scopeName = BeanDefinition.SCOPE_SINGLETON; + + private ScopedProxyMode scopedProxyMode = ScopedProxyMode.NO; + + + /** + * Set the name of the scope. + */ + public void setScopeName(String scopeName) { + Assert.notNull(scopeName, "'scopeName' must not be null"); + this.scopeName = scopeName; + } + + /** + * Get the name of the scope. + */ + public String getScopeName() { + return this.scopeName; + } + + /** + * Set the proxy-mode to be applied to the scoped instance. + */ + public void setScopedProxyMode(ScopedProxyMode scopedProxyMode) { + Assert.notNull(scopedProxyMode, "'scopedProxyMode' must not be null"); + this.scopedProxyMode = scopedProxyMode; + } + + /** + * Get the proxy-mode to be applied to the scoped instance. + */ + public ScopedProxyMode getScopedProxyMode() { + return this.scopedProxyMode; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ScopeMetadataResolver.java b/spring-context/src/main/java/org/springframework/context/annotation/ScopeMetadataResolver.java new file mode 100644 index 0000000..c030fbd --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ScopeMetadataResolver.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.springframework.beans.factory.config.BeanDefinition; + +/** + * Strategy interface for resolving the scope of bean definitions. + * + * @author Mark Fisher + * @since 2.5 + * @see org.springframework.context.annotation.Scope + */ +@FunctionalInterface +public interface ScopeMetadataResolver { + + /** + * Resolve the {@link ScopeMetadata} appropriate to the supplied + * bean {@code definition}. + *

    Implementations can of course use any strategy they like to + * determine the scope metadata, but some implementations that spring + * immediately to mind might be to use source level annotations + * present on {@link BeanDefinition#getBeanClassName() the class} of the + * supplied {@code definition}, or to use metadata present in the + * {@link BeanDefinition#attributeNames()} of the supplied {@code definition}. + * @param definition the target bean definition + * @return the relevant scope metadata; never {@code null} + */ + ScopeMetadata resolveScopeMetadata(BeanDefinition definition); + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ScopedProxyCreator.java b/spring-context/src/main/java/org/springframework/context/annotation/ScopedProxyCreator.java new file mode 100644 index 0000000..1ff6f35 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ScopedProxyCreator.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.springframework.aop.scope.ScopedProxyUtils; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; + +/** + * Delegate factory class used to just introduce an AOP framework dependency + * when actually creating a scoped proxy. + * + * @author Juergen Hoeller + * @since 3.0 + * @see org.springframework.aop.scope.ScopedProxyUtils#createScopedProxy + */ +final class ScopedProxyCreator { + + private ScopedProxyCreator() { + } + + + public static BeanDefinitionHolder createScopedProxy( + BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry, boolean proxyTargetClass) { + + return ScopedProxyUtils.createScopedProxy(definitionHolder, registry, proxyTargetClass); + } + + public static String getTargetBeanName(String originalBeanName) { + return ScopedProxyUtils.getTargetBeanName(originalBeanName); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ScopedProxyMode.java b/spring-context/src/main/java/org/springframework/context/annotation/ScopedProxyMode.java new file mode 100644 index 0000000..08e7fd8 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ScopedProxyMode.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +/** + * Enumerates the various scoped-proxy options. + * + *

    For a more complete discussion of exactly what a scoped proxy is, see the + * section of the Spring reference documentation entitled 'Scoped beans as + * dependencies'. + * + * @author Mark Fisher + * @since 2.5 + * @see ScopeMetadata + */ +public enum ScopedProxyMode { + + /** + * Default typically equals {@link #NO}, unless a different default + * has been configured at the component-scan instruction level. + */ + DEFAULT, + + /** + * Do not create a scoped proxy. + *

    This proxy-mode is not typically useful when used with a + * non-singleton scoped instance, which should favor the use of the + * {@link #INTERFACES} or {@link #TARGET_CLASS} proxy-modes instead if it + * is to be used as a dependency. + */ + NO, + + /** + * Create a JDK dynamic proxy implementing all interfaces exposed by + * the class of the target object. + */ + INTERFACES, + + /** + * Create a class-based proxy (uses CGLIB). + */ + TARGET_CLASS + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/package-info.java b/spring-context/src/main/java/org/springframework/context/annotation/package-info.java new file mode 100644 index 0000000..d40f541 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/package-info.java @@ -0,0 +1,11 @@ +/** + * Annotation support for the Application Context, including JSR-250 "common" + * annotations, component-scanning, and Java-based metadata for creating + * Spring-managed objects. + */ +@NonNullApi +@NonNullFields +package org.springframework.context.annotation; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/context/config/AbstractPropertyLoadingBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/context/config/AbstractPropertyLoadingBeanDefinitionParser.java new file mode 100644 index 0000000..270a955 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/config/AbstractPropertyLoadingBeanDefinitionParser.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.util.StringUtils; + +/** + * Abstract parser for <context:property-.../> elements. + * + * @author Juergen Hoeller + * @author Arjen Poutsma + * @author Dave Syer + * @since 2.5.2 + */ +abstract class AbstractPropertyLoadingBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { + + @Override + protected boolean shouldGenerateId() { + return true; + } + + @Override + protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + String location = element.getAttribute("location"); + if (StringUtils.hasLength(location)) { + location = parserContext.getReaderContext().getEnvironment().resolvePlaceholders(location); + String[] locations = StringUtils.commaDelimitedListToStringArray(location); + builder.addPropertyValue("locations", locations); + } + + String propertiesRef = element.getAttribute("properties-ref"); + if (StringUtils.hasLength(propertiesRef)) { + builder.addPropertyReference("properties", propertiesRef); + } + + String fileEncoding = element.getAttribute("file-encoding"); + if (StringUtils.hasLength(fileEncoding)) { + builder.addPropertyValue("fileEncoding", fileEncoding); + } + + String order = element.getAttribute("order"); + if (StringUtils.hasLength(order)) { + builder.addPropertyValue("order", Integer.valueOf(order)); + } + + builder.addPropertyValue("ignoreResourceNotFound", + Boolean.valueOf(element.getAttribute("ignore-resource-not-found"))); + + builder.addPropertyValue("localOverride", + Boolean.valueOf(element.getAttribute("local-override"))); + + builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/config/ContextNamespaceHandler.java b/spring-context/src/main/java/org/springframework/context/config/ContextNamespaceHandler.java new file mode 100644 index 0000000..e68c8a5 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/config/ContextNamespaceHandler.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.config; + +import org.springframework.beans.factory.xml.NamespaceHandlerSupport; +import org.springframework.context.annotation.AnnotationConfigBeanDefinitionParser; +import org.springframework.context.annotation.ComponentScanBeanDefinitionParser; + +/** + * {@link org.springframework.beans.factory.xml.NamespaceHandler} + * for the '{@code context}' namespace. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @since 2.5 + */ +public class ContextNamespaceHandler extends NamespaceHandlerSupport { + + @Override + public void init() { + registerBeanDefinitionParser("property-placeholder", new PropertyPlaceholderBeanDefinitionParser()); + registerBeanDefinitionParser("property-override", new PropertyOverrideBeanDefinitionParser()); + registerBeanDefinitionParser("annotation-config", new AnnotationConfigBeanDefinitionParser()); + registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser()); + registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser()); + registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser()); + registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser()); + registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser()); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/config/LoadTimeWeaverBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/context/config/LoadTimeWeaverBeanDefinitionParser.java new file mode 100644 index 0000000..e21b5fe --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/config/LoadTimeWeaverBeanDefinitionParser.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.weaving.AspectJWeavingEnabler; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * Parser for the <context:load-time-weaver/> element. + * + * @author Juergen Hoeller + * @since 2.5 + */ +class LoadTimeWeaverBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { + + /** + * The bean name of the internally managed AspectJ weaving enabler. + * @since 4.3.1 + */ + public static final String ASPECTJ_WEAVING_ENABLER_BEAN_NAME = + "org.springframework.context.config.internalAspectJWeavingEnabler"; + + private static final String ASPECTJ_WEAVING_ENABLER_CLASS_NAME = + "org.springframework.context.weaving.AspectJWeavingEnabler"; + + private static final String DEFAULT_LOAD_TIME_WEAVER_CLASS_NAME = + "org.springframework.context.weaving.DefaultContextLoadTimeWeaver"; + + private static final String WEAVER_CLASS_ATTRIBUTE = "weaver-class"; + + private static final String ASPECTJ_WEAVING_ATTRIBUTE = "aspectj-weaving"; + + + @Override + protected String getBeanClassName(Element element) { + if (element.hasAttribute(WEAVER_CLASS_ATTRIBUTE)) { + return element.getAttribute(WEAVER_CLASS_ATTRIBUTE); + } + return DEFAULT_LOAD_TIME_WEAVER_CLASS_NAME; + } + + @Override + protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext) { + return ConfigurableApplicationContext.LOAD_TIME_WEAVER_BEAN_NAME; + } + + @Override + protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + + if (isAspectJWeavingEnabled(element.getAttribute(ASPECTJ_WEAVING_ATTRIBUTE), parserContext)) { + if (!parserContext.getRegistry().containsBeanDefinition(ASPECTJ_WEAVING_ENABLER_BEAN_NAME)) { + RootBeanDefinition def = new RootBeanDefinition(ASPECTJ_WEAVING_ENABLER_CLASS_NAME); + parserContext.registerBeanComponent( + new BeanComponentDefinition(def, ASPECTJ_WEAVING_ENABLER_BEAN_NAME)); + } + + if (isBeanConfigurerAspectEnabled(parserContext.getReaderContext().getBeanClassLoader())) { + new SpringConfiguredBeanDefinitionParser().parse(element, parserContext); + } + } + } + + protected boolean isAspectJWeavingEnabled(String value, ParserContext parserContext) { + if ("on".equals(value)) { + return true; + } + else if ("off".equals(value)) { + return false; + } + else { + // Determine default... + ClassLoader cl = parserContext.getReaderContext().getBeanClassLoader(); + return (cl != null && cl.getResource(AspectJWeavingEnabler.ASPECTJ_AOP_XML_RESOURCE) != null); + } + } + + protected boolean isBeanConfigurerAspectEnabled(@Nullable ClassLoader beanClassLoader) { + return ClassUtils.isPresent(SpringConfiguredBeanDefinitionParser.BEAN_CONFIGURER_ASPECT_CLASS_NAME, + beanClassLoader); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/config/MBeanExportBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/context/config/MBeanExportBeanDefinitionParser.java new file mode 100644 index 0000000..e1bba29 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/config/MBeanExportBeanDefinitionParser.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.jmx.export.annotation.AnnotationMBeanExporter; +import org.springframework.jmx.support.RegistrationPolicy; +import org.springframework.util.StringUtils; + +/** + * Parser for the <context:mbean-export/> element. + * + *

    Registers an instance of + * {@link org.springframework.jmx.export.annotation.AnnotationMBeanExporter} + * within the context. + * + * @author Juergen Hoeller + * @author Mark Fisher + * @since 2.5 + * @see org.springframework.jmx.export.annotation.AnnotationMBeanExporter + */ +class MBeanExportBeanDefinitionParser extends AbstractBeanDefinitionParser { + + private static final String MBEAN_EXPORTER_BEAN_NAME = "mbeanExporter"; + + private static final String DEFAULT_DOMAIN_ATTRIBUTE = "default-domain"; + + private static final String SERVER_ATTRIBUTE = "server"; + + private static final String REGISTRATION_ATTRIBUTE = "registration"; + + private static final String REGISTRATION_IGNORE_EXISTING = "ignoreExisting"; + + private static final String REGISTRATION_REPLACE_EXISTING = "replaceExisting"; + + + @Override + protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext) { + return MBEAN_EXPORTER_BEAN_NAME; + } + + @Override + protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(AnnotationMBeanExporter.class); + + // Mark as infrastructure bean and attach source location. + builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + builder.getRawBeanDefinition().setSource(parserContext.extractSource(element)); + + String defaultDomain = element.getAttribute(DEFAULT_DOMAIN_ATTRIBUTE); + if (StringUtils.hasText(defaultDomain)) { + builder.addPropertyValue("defaultDomain", defaultDomain); + } + + String serverBeanName = element.getAttribute(SERVER_ATTRIBUTE); + if (StringUtils.hasText(serverBeanName)) { + builder.addPropertyReference("server", serverBeanName); + } + else { + AbstractBeanDefinition specialServer = MBeanServerBeanDefinitionParser.findServerForSpecialEnvironment(); + if (specialServer != null) { + builder.addPropertyValue("server", specialServer); + } + } + + String registration = element.getAttribute(REGISTRATION_ATTRIBUTE); + RegistrationPolicy registrationPolicy = RegistrationPolicy.FAIL_ON_EXISTING; + if (REGISTRATION_IGNORE_EXISTING.equals(registration)) { + registrationPolicy = RegistrationPolicy.IGNORE_EXISTING; + } + else if (REGISTRATION_REPLACE_EXISTING.equals(registration)) { + registrationPolicy = RegistrationPolicy.REPLACE_EXISTING; + } + builder.addPropertyValue("registrationPolicy", registrationPolicy); + + return builder.getBeanDefinition(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/config/MBeanServerBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/context/config/MBeanServerBeanDefinitionParser.java new file mode 100644 index 0000000..78062b6 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/config/MBeanServerBeanDefinitionParser.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.jmx.support.MBeanServerFactoryBean; +import org.springframework.jmx.support.WebSphereMBeanServerFactoryBean; +import org.springframework.jndi.JndiObjectFactoryBean; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * Parser for the <context:mbean-server/> element. + * + *

    Registers an instance of + * {@link org.springframework.jmx.export.annotation.AnnotationMBeanExporter} + * within the context. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @since 2.5 + * @see org.springframework.jmx.export.annotation.AnnotationMBeanExporter + */ +class MBeanServerBeanDefinitionParser extends AbstractBeanDefinitionParser { + + private static final String MBEAN_SERVER_BEAN_NAME = "mbeanServer"; + + private static final String AGENT_ID_ATTRIBUTE = "agent-id"; + + + private static final boolean weblogicPresent; + + private static final boolean webspherePresent; + + static { + ClassLoader classLoader = MBeanServerBeanDefinitionParser.class.getClassLoader(); + weblogicPresent = ClassUtils.isPresent("weblogic.management.Helper", classLoader); + webspherePresent = ClassUtils.isPresent("com.ibm.websphere.management.AdminServiceFactory", classLoader); + } + + + @Override + protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext) { + String id = element.getAttribute(ID_ATTRIBUTE); + return (StringUtils.hasText(id) ? id : MBEAN_SERVER_BEAN_NAME); + } + + @Override + protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) { + String agentId = element.getAttribute(AGENT_ID_ATTRIBUTE); + if (StringUtils.hasText(agentId)) { + RootBeanDefinition bd = new RootBeanDefinition(MBeanServerFactoryBean.class); + bd.getPropertyValues().add("agentId", agentId); + return bd; + } + AbstractBeanDefinition specialServer = findServerForSpecialEnvironment(); + if (specialServer != null) { + return specialServer; + } + RootBeanDefinition bd = new RootBeanDefinition(MBeanServerFactoryBean.class); + bd.getPropertyValues().add("locateExistingServerIfPossible", Boolean.TRUE); + + // Mark as infrastructure bean and attach source location. + bd.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + bd.setSource(parserContext.extractSource(element)); + return bd; + } + + @Nullable + static AbstractBeanDefinition findServerForSpecialEnvironment() { + if (weblogicPresent) { + RootBeanDefinition bd = new RootBeanDefinition(JndiObjectFactoryBean.class); + bd.getPropertyValues().add("jndiName", "java:comp/env/jmx/runtime"); + return bd; + } + else if (webspherePresent) { + return new RootBeanDefinition(WebSphereMBeanServerFactoryBean.class); + } + else { + return null; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/config/PropertyOverrideBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/context/config/PropertyOverrideBeanDefinitionParser.java new file mode 100644 index 0000000..a491b3d --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/config/PropertyOverrideBeanDefinitionParser.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.PropertyOverrideConfigurer; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.ParserContext; + +/** + * Parser for the <context:property-override/> element. + * + * @author Juergen Hoeller + * @author Dave Syer + * @since 2.5.2 + */ +class PropertyOverrideBeanDefinitionParser extends AbstractPropertyLoadingBeanDefinitionParser { + + @Override + protected Class getBeanClass(Element element) { + return PropertyOverrideConfigurer.class; + } + + @Override + protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + super.doParse(element, parserContext, builder); + + builder.addPropertyValue("ignoreInvalidKeys", + Boolean.valueOf(element.getAttribute("ignore-unresolvable"))); + + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/config/PropertyPlaceholderBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/context/config/PropertyPlaceholderBeanDefinitionParser.java new file mode 100644 index 0000000..b2bd33a --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/config/PropertyPlaceholderBeanDefinitionParser.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.util.StringUtils; + +/** + * Parser for the {@code } element. + * + * @author Juergen Hoeller + * @author Dave Syer + * @author Chris Beams + * @since 2.5 + */ +class PropertyPlaceholderBeanDefinitionParser extends AbstractPropertyLoadingBeanDefinitionParser { + + private static final String SYSTEM_PROPERTIES_MODE_ATTRIBUTE = "system-properties-mode"; + + private static final String SYSTEM_PROPERTIES_MODE_DEFAULT = "ENVIRONMENT"; + + + @Override + @SuppressWarnings("deprecation") + protected Class getBeanClass(Element element) { + // As of Spring 3.1, the default value of system-properties-mode has changed from + // 'FALLBACK' to 'ENVIRONMENT'. This latter value indicates that resolution of + // placeholders against system properties is a function of the Environment and + // its current set of PropertySources. + if (SYSTEM_PROPERTIES_MODE_DEFAULT.equals(element.getAttribute(SYSTEM_PROPERTIES_MODE_ATTRIBUTE))) { + return PropertySourcesPlaceholderConfigurer.class; + } + + // The user has explicitly specified a value for system-properties-mode: revert to + // PropertyPlaceholderConfigurer to ensure backward compatibility with 3.0 and earlier. + // This is deprecated; to be removed along with PropertyPlaceholderConfigurer itself. + return org.springframework.beans.factory.config.PropertyPlaceholderConfigurer.class; + } + + @Override + protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + super.doParse(element, parserContext, builder); + + builder.addPropertyValue("ignoreUnresolvablePlaceholders", + Boolean.valueOf(element.getAttribute("ignore-unresolvable"))); + + String systemPropertiesModeName = element.getAttribute(SYSTEM_PROPERTIES_MODE_ATTRIBUTE); + if (StringUtils.hasLength(systemPropertiesModeName) && + !systemPropertiesModeName.equals(SYSTEM_PROPERTIES_MODE_DEFAULT)) { + builder.addPropertyValue("systemPropertiesModeName", "SYSTEM_PROPERTIES_MODE_" + systemPropertiesModeName); + } + + if (element.hasAttribute("value-separator")) { + builder.addPropertyValue("valueSeparator", element.getAttribute("value-separator")); + } + if (element.hasAttribute("trim-values")) { + builder.addPropertyValue("trimValues", element.getAttribute("trim-values")); + } + if (element.hasAttribute("null-value")) { + builder.addPropertyValue("nullValue", element.getAttribute("null-value")); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/config/SpringConfiguredBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/context/config/SpringConfiguredBeanDefinitionParser.java new file mode 100644 index 0000000..c83eb85 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/config/SpringConfiguredBeanDefinitionParser.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; + +/** + * {@link BeanDefinitionParser} responsible for parsing the + * {@code } tag. + * + * @author Juergen Hoeller + * @since 2.5 + */ +class SpringConfiguredBeanDefinitionParser implements BeanDefinitionParser { + + /** + * The bean name of the internally managed bean configurer aspect. + */ + public static final String BEAN_CONFIGURER_ASPECT_BEAN_NAME = + "org.springframework.context.config.internalBeanConfigurerAspect"; + + static final String BEAN_CONFIGURER_ASPECT_CLASS_NAME = + "org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect"; + + + @Override + public BeanDefinition parse(Element element, ParserContext parserContext) { + if (!parserContext.getRegistry().containsBeanDefinition(BEAN_CONFIGURER_ASPECT_BEAN_NAME)) { + RootBeanDefinition def = new RootBeanDefinition(); + def.setBeanClassName(BEAN_CONFIGURER_ASPECT_CLASS_NAME); + def.setFactoryMethodName("aspectOf"); + def.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + def.setSource(parserContext.extractSource(element)); + parserContext.registerBeanComponent(new BeanComponentDefinition(def, BEAN_CONFIGURER_ASPECT_BEAN_NAME)); + } + return null; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/config/package-info.java b/spring-context/src/main/java/org/springframework/context/config/package-info.java new file mode 100644 index 0000000..08b96ec --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/config/package-info.java @@ -0,0 +1,10 @@ +/** + * Support package for advanced application context configuration, + * with XML schema being the primary configuration format. + */ +@NonNullApi +@NonNullFields +package org.springframework.context.config; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java b/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java new file mode 100644 index 0000000..9f0fd1d --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java @@ -0,0 +1,500 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.aop.framework.AopProxyUtils; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; + +/** + * Abstract implementation of the {@link ApplicationEventMulticaster} interface, + * providing the basic listener registration facility. + * + *

    Doesn't permit multiple instances of the same listener by default, + * as it keeps listeners in a linked Set. The collection class used to hold + * ApplicationListener objects can be overridden through the "collectionClass" + * bean property. + * + *

    Implementing ApplicationEventMulticaster's actual {@link #multicastEvent} method + * is left to subclasses. {@link SimpleApplicationEventMulticaster} simply multicasts + * all events to all registered listeners, invoking them in the calling thread. + * Alternative implementations could be more sophisticated in those respects. + * + * @author Juergen Hoeller + * @author Stephane Nicoll + * @since 1.2.3 + * @see #getApplicationListeners(ApplicationEvent, ResolvableType) + * @see SimpleApplicationEventMulticaster + */ +public abstract class AbstractApplicationEventMulticaster + implements ApplicationEventMulticaster, BeanClassLoaderAware, BeanFactoryAware { + + private final DefaultListenerRetriever defaultRetriever = new DefaultListenerRetriever(); + + final Map retrieverCache = new ConcurrentHashMap<>(64); + + @Nullable + private ClassLoader beanClassLoader; + + @Nullable + private ConfigurableBeanFactory beanFactory; + + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + if (!(beanFactory instanceof ConfigurableBeanFactory)) { + throw new IllegalStateException("Not running in a ConfigurableBeanFactory: " + beanFactory); + } + this.beanFactory = (ConfigurableBeanFactory) beanFactory; + if (this.beanClassLoader == null) { + this.beanClassLoader = this.beanFactory.getBeanClassLoader(); + } + } + + private ConfigurableBeanFactory getBeanFactory() { + if (this.beanFactory == null) { + throw new IllegalStateException("ApplicationEventMulticaster cannot retrieve listener beans " + + "because it is not associated with a BeanFactory"); + } + return this.beanFactory; + } + + + @Override + public void addApplicationListener(ApplicationListener listener) { + synchronized (this.defaultRetriever) { + // Explicitly remove target for a proxy, if registered already, + // in order to avoid double invocations of the same listener. + Object singletonTarget = AopProxyUtils.getSingletonTarget(listener); + if (singletonTarget instanceof ApplicationListener) { + this.defaultRetriever.applicationListeners.remove(singletonTarget); + } + this.defaultRetriever.applicationListeners.add(listener); + this.retrieverCache.clear(); + } + } + + @Override + public void addApplicationListenerBean(String listenerBeanName) { + synchronized (this.defaultRetriever) { + this.defaultRetriever.applicationListenerBeans.add(listenerBeanName); + this.retrieverCache.clear(); + } + } + + @Override + public void removeApplicationListener(ApplicationListener listener) { + synchronized (this.defaultRetriever) { + this.defaultRetriever.applicationListeners.remove(listener); + this.retrieverCache.clear(); + } + } + + @Override + public void removeApplicationListenerBean(String listenerBeanName) { + synchronized (this.defaultRetriever) { + this.defaultRetriever.applicationListenerBeans.remove(listenerBeanName); + this.retrieverCache.clear(); + } + } + + @Override + public void removeAllListeners() { + synchronized (this.defaultRetriever) { + this.defaultRetriever.applicationListeners.clear(); + this.defaultRetriever.applicationListenerBeans.clear(); + this.retrieverCache.clear(); + } + } + + + /** + * Return a Collection containing all ApplicationListeners. + * @return a Collection of ApplicationListeners + * @see org.springframework.context.ApplicationListener + */ + protected Collection> getApplicationListeners() { + synchronized (this.defaultRetriever) { + return this.defaultRetriever.getApplicationListeners(); + } + } + + /** + * Return a Collection of ApplicationListeners matching the given + * event type. Non-matching listeners get excluded early. + * @param event the event to be propagated. Allows for excluding + * non-matching listeners early, based on cached matching information. + * @param eventType the event type + * @return a Collection of ApplicationListeners + * @see org.springframework.context.ApplicationListener + */ + protected Collection> getApplicationListeners( + ApplicationEvent event, ResolvableType eventType) { + + Object source = event.getSource(); + Class sourceType = (source != null ? source.getClass() : null); + ListenerCacheKey cacheKey = new ListenerCacheKey(eventType, sourceType); + + // Potential new retriever to populate + CachedListenerRetriever newRetriever = null; + + // Quick check for existing entry on ConcurrentHashMap + CachedListenerRetriever existingRetriever = this.retrieverCache.get(cacheKey); + if (existingRetriever == null) { + // Caching a new ListenerRetriever if possible + if (this.beanClassLoader == null || + (ClassUtils.isCacheSafe(event.getClass(), this.beanClassLoader) && + (sourceType == null || ClassUtils.isCacheSafe(sourceType, this.beanClassLoader)))) { + newRetriever = new CachedListenerRetriever(); + existingRetriever = this.retrieverCache.putIfAbsent(cacheKey, newRetriever); + if (existingRetriever != null) { + newRetriever = null; // no need to populate it in retrieveApplicationListeners + } + } + } + + if (existingRetriever != null) { + Collection> result = existingRetriever.getApplicationListeners(); + if (result != null) { + return result; + } + // If result is null, the existing retriever is not fully populated yet by another thread. + // Proceed like caching wasn't possible for this current local attempt. + } + + return retrieveApplicationListeners(eventType, sourceType, newRetriever); + } + + /** + * Actually retrieve the application listeners for the given event and source type. + * @param eventType the event type + * @param sourceType the event source type + * @param retriever the ListenerRetriever, if supposed to populate one (for caching purposes) + * @return the pre-filtered list of application listeners for the given event and source type + */ + private Collection> retrieveApplicationListeners( + ResolvableType eventType, @Nullable Class sourceType, @Nullable CachedListenerRetriever retriever) { + + List> allListeners = new ArrayList<>(); + Set> filteredListeners = (retriever != null ? new LinkedHashSet<>() : null); + Set filteredListenerBeans = (retriever != null ? new LinkedHashSet<>() : null); + + Set> listeners; + Set listenerBeans; + synchronized (this.defaultRetriever) { + listeners = new LinkedHashSet<>(this.defaultRetriever.applicationListeners); + listenerBeans = new LinkedHashSet<>(this.defaultRetriever.applicationListenerBeans); + } + + // Add programmatically registered listeners, including ones coming + // from ApplicationListenerDetector (singleton beans and inner beans). + for (ApplicationListener listener : listeners) { + if (supportsEvent(listener, eventType, sourceType)) { + if (retriever != null) { + filteredListeners.add(listener); + } + allListeners.add(listener); + } + } + + // Add listeners by bean name, potentially overlapping with programmatically + // registered listeners above - but here potentially with additional metadata. + if (!listenerBeans.isEmpty()) { + ConfigurableBeanFactory beanFactory = getBeanFactory(); + for (String listenerBeanName : listenerBeans) { + try { + if (supportsEvent(beanFactory, listenerBeanName, eventType)) { + ApplicationListener listener = + beanFactory.getBean(listenerBeanName, ApplicationListener.class); + if (!allListeners.contains(listener) && supportsEvent(listener, eventType, sourceType)) { + if (retriever != null) { + if (beanFactory.isSingleton(listenerBeanName)) { + filteredListeners.add(listener); + } + else { + filteredListenerBeans.add(listenerBeanName); + } + } + allListeners.add(listener); + } + } + else { + // Remove non-matching listeners that originally came from + // ApplicationListenerDetector, possibly ruled out by additional + // BeanDefinition metadata (e.g. factory method generics) above. + Object listener = beanFactory.getSingleton(listenerBeanName); + if (retriever != null) { + filteredListeners.remove(listener); + } + allListeners.remove(listener); + } + } + catch (NoSuchBeanDefinitionException ex) { + // Singleton listener instance (without backing bean definition) disappeared - + // probably in the middle of the destruction phase + } + } + } + + AnnotationAwareOrderComparator.sort(allListeners); + if (retriever != null) { + if (filteredListenerBeans.isEmpty()) { + retriever.applicationListeners = new LinkedHashSet<>(allListeners); + retriever.applicationListenerBeans = filteredListenerBeans; + } + else { + retriever.applicationListeners = filteredListeners; + retriever.applicationListenerBeans = filteredListenerBeans; + } + } + return allListeners; + } + + /** + * Filter a bean-defined listener early through checking its generically declared + * event type before trying to instantiate it. + *

    If this method returns {@code true} for a given listener as a first pass, + * the listener instance will get retrieved and fully evaluated through a + * {@link #supportsEvent(ApplicationListener, ResolvableType, Class)} call afterwards. + * @param beanFactory the BeanFactory that contains the listener beans + * @param listenerBeanName the name of the bean in the BeanFactory + * @param eventType the event type to check + * @return whether the given listener should be included in the candidates + * for the given event type + * @see #supportsEvent(Class, ResolvableType) + * @see #supportsEvent(ApplicationListener, ResolvableType, Class) + */ + private boolean supportsEvent( + ConfigurableBeanFactory beanFactory, String listenerBeanName, ResolvableType eventType) { + + Class listenerType = beanFactory.getType(listenerBeanName); + if (listenerType == null || GenericApplicationListener.class.isAssignableFrom(listenerType) || + SmartApplicationListener.class.isAssignableFrom(listenerType)) { + return true; + } + if (!supportsEvent(listenerType, eventType)) { + return false; + } + try { + BeanDefinition bd = beanFactory.getMergedBeanDefinition(listenerBeanName); + ResolvableType genericEventType = bd.getResolvableType().as(ApplicationListener.class).getGeneric(); + return (genericEventType == ResolvableType.NONE || genericEventType.isAssignableFrom(eventType)); + } + catch (NoSuchBeanDefinitionException ex) { + // Ignore - no need to check resolvable type for manually registered singleton + return true; + } + } + + /** + * Filter a listener early through checking its generically declared event + * type before trying to instantiate it. + *

    If this method returns {@code true} for a given listener as a first pass, + * the listener instance will get retrieved and fully evaluated through a + * {@link #supportsEvent(ApplicationListener, ResolvableType, Class)} call afterwards. + * @param listenerType the listener's type as determined by the BeanFactory + * @param eventType the event type to check + * @return whether the given listener should be included in the candidates + * for the given event type + */ + protected boolean supportsEvent(Class listenerType, ResolvableType eventType) { + ResolvableType declaredEventType = GenericApplicationListenerAdapter.resolveDeclaredEventType(listenerType); + return (declaredEventType == null || declaredEventType.isAssignableFrom(eventType)); + } + + /** + * Determine whether the given listener supports the given event. + *

    The default implementation detects the {@link SmartApplicationListener} + * and {@link GenericApplicationListener} interfaces. In case of a standard + * {@link ApplicationListener}, a {@link GenericApplicationListenerAdapter} + * will be used to introspect the generically declared type of the target listener. + * @param listener the target listener to check + * @param eventType the event type to check against + * @param sourceType the source type to check against + * @return whether the given listener should be included in the candidates + * for the given event type + */ + protected boolean supportsEvent( + ApplicationListener listener, ResolvableType eventType, @Nullable Class sourceType) { + + GenericApplicationListener smartListener = (listener instanceof GenericApplicationListener ? + (GenericApplicationListener) listener : new GenericApplicationListenerAdapter(listener)); + return (smartListener.supportsEventType(eventType) && smartListener.supportsSourceType(sourceType)); + } + + + /** + * Cache key for ListenerRetrievers, based on event type and source type. + */ + private static final class ListenerCacheKey implements Comparable { + + private final ResolvableType eventType; + + @Nullable + private final Class sourceType; + + public ListenerCacheKey(ResolvableType eventType, @Nullable Class sourceType) { + Assert.notNull(eventType, "Event type must not be null"); + this.eventType = eventType; + this.sourceType = sourceType; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof ListenerCacheKey)) { + return false; + } + ListenerCacheKey otherKey = (ListenerCacheKey) other; + return (this.eventType.equals(otherKey.eventType) && + ObjectUtils.nullSafeEquals(this.sourceType, otherKey.sourceType)); + } + + @Override + public int hashCode() { + return this.eventType.hashCode() * 29 + ObjectUtils.nullSafeHashCode(this.sourceType); + } + + @Override + public String toString() { + return "ListenerCacheKey [eventType = " + this.eventType + ", sourceType = " + this.sourceType + "]"; + } + + @Override + public int compareTo(ListenerCacheKey other) { + int result = this.eventType.toString().compareTo(other.eventType.toString()); + if (result == 0) { + if (this.sourceType == null) { + return (other.sourceType == null ? 0 : -1); + } + if (other.sourceType == null) { + return 1; + } + result = this.sourceType.getName().compareTo(other.sourceType.getName()); + } + return result; + } + } + + + /** + * Helper class that encapsulates a specific set of target listeners, + * allowing for efficient retrieval of pre-filtered listeners. + *

    An instance of this helper gets cached per event type and source type. + */ + private class CachedListenerRetriever { + + @Nullable + public volatile Set> applicationListeners; + + @Nullable + public volatile Set applicationListenerBeans; + + @Nullable + public Collection> getApplicationListeners() { + Set> applicationListeners = this.applicationListeners; + Set applicationListenerBeans = this.applicationListenerBeans; + if (applicationListeners == null || applicationListenerBeans == null) { + // Not fully populated yet + return null; + } + + List> allListeners = new ArrayList<>( + applicationListeners.size() + applicationListenerBeans.size()); + allListeners.addAll(applicationListeners); + if (!applicationListenerBeans.isEmpty()) { + BeanFactory beanFactory = getBeanFactory(); + for (String listenerBeanName : applicationListenerBeans) { + try { + allListeners.add(beanFactory.getBean(listenerBeanName, ApplicationListener.class)); + } + catch (NoSuchBeanDefinitionException ex) { + // Singleton listener instance (without backing bean definition) disappeared - + // probably in the middle of the destruction phase + } + } + } + if (!applicationListenerBeans.isEmpty()) { + AnnotationAwareOrderComparator.sort(allListeners); + } + return allListeners; + } + } + + + /** + * Helper class that encapsulates a general set of target listeners. + */ + private class DefaultListenerRetriever { + + public final Set> applicationListeners = new LinkedHashSet<>(); + + public final Set applicationListenerBeans = new LinkedHashSet<>(); + + public Collection> getApplicationListeners() { + List> allListeners = new ArrayList<>( + this.applicationListeners.size() + this.applicationListenerBeans.size()); + allListeners.addAll(this.applicationListeners); + if (!this.applicationListenerBeans.isEmpty()) { + BeanFactory beanFactory = getBeanFactory(); + for (String listenerBeanName : this.applicationListenerBeans) { + try { + ApplicationListener listener = + beanFactory.getBean(listenerBeanName, ApplicationListener.class); + if (!allListeners.contains(listener)) { + allListeners.add(listener); + } + } + catch (NoSuchBeanDefinitionException ex) { + // Singleton listener instance (without backing bean definition) disappeared - + // probably in the middle of the destruction phase + } + } + } + AnnotationAwareOrderComparator.sort(allListeners); + return allListeners; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/event/ApplicationContextEvent.java b/spring-context/src/main/java/org/springframework/context/event/ApplicationContextEvent.java new file mode 100644 index 0000000..fab9067 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/ApplicationContextEvent.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; + +/** + * Base class for events raised for an {@code ApplicationContext}. + * + * @author Juergen Hoeller + * @since 2.5 + */ +@SuppressWarnings("serial") +public abstract class ApplicationContextEvent extends ApplicationEvent { + + /** + * Create a new ContextStartedEvent. + * @param source the {@code ApplicationContext} that the event is raised for + * (must not be {@code null}) + */ + public ApplicationContextEvent(ApplicationContext source) { + super(source); + } + + /** + * Get the {@code ApplicationContext} that the event was raised for. + */ + public final ApplicationContext getApplicationContext() { + return (ApplicationContext) getSource(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/event/ApplicationEventMulticaster.java b/spring-context/src/main/java/org/springframework/context/event/ApplicationEventMulticaster.java new file mode 100644 index 0000000..5810d8b --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/ApplicationEventMulticaster.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event; + +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.core.ResolvableType; +import org.springframework.lang.Nullable; + +/** + * Interface to be implemented by objects that can manage a number of + * {@link ApplicationListener} objects and publish events to them. + * + *

    An {@link org.springframework.context.ApplicationEventPublisher}, typically + * a Spring {@link org.springframework.context.ApplicationContext}, can use an + * {@code ApplicationEventMulticaster} as a delegate for actually publishing events. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Stephane Nicoll + * @see ApplicationListener + */ +public interface ApplicationEventMulticaster { + + /** + * Add a listener to be notified of all events. + * @param listener the listener to add + */ + void addApplicationListener(ApplicationListener listener); + + /** + * Add a listener bean to be notified of all events. + * @param listenerBeanName the name of the listener bean to add + */ + void addApplicationListenerBean(String listenerBeanName); + + /** + * Remove a listener from the notification list. + * @param listener the listener to remove + */ + void removeApplicationListener(ApplicationListener listener); + + /** + * Remove a listener bean from the notification list. + * @param listenerBeanName the name of the listener bean to remove + */ + void removeApplicationListenerBean(String listenerBeanName); + + /** + * Remove all listeners registered with this multicaster. + *

    After a remove call, the multicaster will perform no action + * on event notification until new listeners are registered. + */ + void removeAllListeners(); + + /** + * Multicast the given application event to appropriate listeners. + *

    Consider using {@link #multicastEvent(ApplicationEvent, ResolvableType)} + * if possible as it provides better support for generics-based events. + * @param event the event to multicast + */ + void multicastEvent(ApplicationEvent event); + + /** + * Multicast the given application event to appropriate listeners. + *

    If the {@code eventType} is {@code null}, a default type is built + * based on the {@code event} instance. + * @param event the event to multicast + * @param eventType the type of event (can be {@code null}) + * @since 4.2 + */ + void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType); + +} diff --git a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java new file mode 100644 index 0000000..02f4dc8 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java @@ -0,0 +1,474 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.lang.reflect.UndeclaredThrowableException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletionStage; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import org.springframework.aop.support.AopUtils; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.PayloadApplicationEvent; +import org.springframework.context.expression.AnnotatedElementKey; +import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.Ordered; +import org.springframework.core.ReactiveAdapter; +import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.Order; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; +import org.springframework.util.concurrent.ListenableFuture; + +/** + * {@link GenericApplicationListener} adapter that delegates the processing of + * an event to an {@link EventListener} annotated method. + * + *

    Delegates to {@link #processEvent(ApplicationEvent)} to give subclasses + * a chance to deviate from the default. Unwraps the content of a + * {@link PayloadApplicationEvent} if necessary to allow a method declaration + * to define any arbitrary event type. If a condition is defined, it is + * evaluated prior to invoking the underlying method. + * + * @author Stephane Nicoll + * @author Juergen Hoeller + * @author Sam Brannen + * @since 4.2 + */ +public class ApplicationListenerMethodAdapter implements GenericApplicationListener { + + private static final boolean reactiveStreamsPresent = ClassUtils.isPresent( + "org.reactivestreams.Publisher", ApplicationListenerMethodAdapter.class.getClassLoader()); + + + protected final Log logger = LogFactory.getLog(getClass()); + + private final String beanName; + + private final Method method; + + private final Method targetMethod; + + private final AnnotatedElementKey methodKey; + + private final List declaredEventTypes; + + @Nullable + private final String condition; + + private final int order; + + @Nullable + private ApplicationContext applicationContext; + + @Nullable + private EventExpressionEvaluator evaluator; + + + /** + * Construct a new ApplicationListenerMethodAdapter. + * @param beanName the name of the bean to invoke the listener method on + * @param targetClass the target class that the method is declared on + * @param method the listener method to invoke + */ + public ApplicationListenerMethodAdapter(String beanName, Class targetClass, Method method) { + this.beanName = beanName; + this.method = BridgeMethodResolver.findBridgedMethod(method); + this.targetMethod = (!Proxy.isProxyClass(targetClass) ? + AopUtils.getMostSpecificMethod(method, targetClass) : this.method); + this.methodKey = new AnnotatedElementKey(this.targetMethod, targetClass); + + EventListener ann = AnnotatedElementUtils.findMergedAnnotation(this.targetMethod, EventListener.class); + this.declaredEventTypes = resolveDeclaredEventTypes(method, ann); + this.condition = (ann != null ? ann.condition() : null); + this.order = resolveOrder(this.targetMethod); + } + + private static List resolveDeclaredEventTypes(Method method, @Nullable EventListener ann) { + int count = method.getParameterCount(); + if (count > 1) { + throw new IllegalStateException( + "Maximum one parameter is allowed for event listener method: " + method); + } + + if (ann != null) { + Class[] classes = ann.classes(); + if (classes.length > 0) { + List types = new ArrayList<>(classes.length); + for (Class eventType : classes) { + types.add(ResolvableType.forClass(eventType)); + } + return types; + } + } + + if (count == 0) { + throw new IllegalStateException( + "Event parameter is mandatory for event listener method: " + method); + } + return Collections.singletonList(ResolvableType.forMethodParameter(method, 0)); + } + + private static int resolveOrder(Method method) { + Order ann = AnnotatedElementUtils.findMergedAnnotation(method, Order.class); + return (ann != null ? ann.value() : Ordered.LOWEST_PRECEDENCE); + } + + + /** + * Initialize this instance. + */ + void init(ApplicationContext applicationContext, @Nullable EventExpressionEvaluator evaluator) { + this.applicationContext = applicationContext; + this.evaluator = evaluator; + } + + + @Override + public void onApplicationEvent(ApplicationEvent event) { + processEvent(event); + } + + @Override + public boolean supportsEventType(ResolvableType eventType) { + for (ResolvableType declaredEventType : this.declaredEventTypes) { + if (declaredEventType.isAssignableFrom(eventType)) { + return true; + } + if (PayloadApplicationEvent.class.isAssignableFrom(eventType.toClass())) { + ResolvableType payloadType = eventType.as(PayloadApplicationEvent.class).getGeneric(); + if (declaredEventType.isAssignableFrom(payloadType)) { + return true; + } + } + } + return eventType.hasUnresolvableGenerics(); + } + + @Override + public boolean supportsSourceType(@Nullable Class sourceType) { + return true; + } + + @Override + public int getOrder() { + return this.order; + } + + + /** + * Process the specified {@link ApplicationEvent}, checking if the condition + * matches and handling a non-null result, if any. + */ + public void processEvent(ApplicationEvent event) { + Object[] args = resolveArguments(event); + if (shouldHandle(event, args)) { + Object result = doInvoke(args); + if (result != null) { + handleResult(result); + } + else { + logger.trace("No result object given - no result to handle"); + } + } + } + + /** + * Resolve the method arguments to use for the specified {@link ApplicationEvent}. + *

    These arguments will be used to invoke the method handled by this instance. + * Can return {@code null} to indicate that no suitable arguments could be resolved + * and therefore the method should not be invoked at all for the specified event. + */ + @Nullable + protected Object[] resolveArguments(ApplicationEvent event) { + ResolvableType declaredEventType = getResolvableType(event); + if (declaredEventType == null) { + return null; + } + if (this.method.getParameterCount() == 0) { + return new Object[0]; + } + Class declaredEventClass = declaredEventType.toClass(); + if (!ApplicationEvent.class.isAssignableFrom(declaredEventClass) && + event instanceof PayloadApplicationEvent) { + Object payload = ((PayloadApplicationEvent) event).getPayload(); + if (declaredEventClass.isInstance(payload)) { + return new Object[] {payload}; + } + } + return new Object[] {event}; + } + + protected void handleResult(Object result) { + if (reactiveStreamsPresent && new ReactiveResultHandler().subscribeToPublisher(result)) { + if (logger.isTraceEnabled()) { + logger.trace("Adapted to reactive result: " + result); + } + } + else if (result instanceof CompletionStage) { + ((CompletionStage) result).whenComplete((event, ex) -> { + if (ex != null) { + handleAsyncError(ex); + } + else if (event != null) { + publishEvent(event); + } + }); + } + else if (result instanceof ListenableFuture) { + ((ListenableFuture) result).addCallback(this::publishEvents, this::handleAsyncError); + } + else { + publishEvents(result); + } + } + + private void publishEvents(Object result) { + if (result.getClass().isArray()) { + Object[] events = ObjectUtils.toObjectArray(result); + for (Object event : events) { + publishEvent(event); + } + } + else if (result instanceof Collection) { + Collection events = (Collection) result; + for (Object event : events) { + publishEvent(event); + } + } + else { + publishEvent(result); + } + } + + private void publishEvent(@Nullable Object event) { + if (event != null) { + Assert.notNull(this.applicationContext, "ApplicationContext must not be null"); + this.applicationContext.publishEvent(event); + } + } + + protected void handleAsyncError(Throwable t) { + logger.error("Unexpected error occurred in asynchronous listener", t); + } + + private boolean shouldHandle(ApplicationEvent event, @Nullable Object[] args) { + if (args == null) { + return false; + } + String condition = getCondition(); + if (StringUtils.hasText(condition)) { + Assert.notNull(this.evaluator, "EventExpressionEvaluator must not be null"); + return this.evaluator.condition( + condition, event, this.targetMethod, this.methodKey, args, this.applicationContext); + } + return true; + } + + /** + * Invoke the event listener method with the given argument values. + */ + @Nullable + protected Object doInvoke(Object... args) { + Object bean = getTargetBean(); + // Detect package-protected NullBean instance through equals(null) check + if (bean.equals(null)) { + return null; + } + + ReflectionUtils.makeAccessible(this.method); + try { + return this.method.invoke(bean, args); + } + catch (IllegalArgumentException ex) { + assertTargetBean(this.method, bean, args); + throw new IllegalStateException(getInvocationErrorMessage(bean, ex.getMessage(), args), ex); + } + catch (IllegalAccessException ex) { + throw new IllegalStateException(getInvocationErrorMessage(bean, ex.getMessage(), args), ex); + } + catch (InvocationTargetException ex) { + // Throw underlying exception + Throwable targetException = ex.getTargetException(); + if (targetException instanceof RuntimeException) { + throw (RuntimeException) targetException; + } + else { + String msg = getInvocationErrorMessage(bean, "Failed to invoke event listener method", args); + throw new UndeclaredThrowableException(targetException, msg); + } + } + } + + /** + * Return the target bean instance to use. + */ + protected Object getTargetBean() { + Assert.notNull(this.applicationContext, "ApplicationContext must no be null"); + return this.applicationContext.getBean(this.beanName); + } + + /** + * Return the target listener method. + * @since 5.3 + */ + protected Method getTargetMethod() { + return this.targetMethod; + } + + /** + * Return the condition to use. + *

    Matches the {@code condition} attribute of the {@link EventListener} + * annotation or any matching attribute on a composed annotation that + * is meta-annotated with {@code @EventListener}. + */ + @Nullable + protected String getCondition() { + return this.condition; + } + + /** + * Add additional details such as the bean type and method signature to + * the given error message. + * @param message error message to append the HandlerMethod details to + */ + protected String getDetailedErrorMessage(Object bean, String message) { + StringBuilder sb = new StringBuilder(message).append("\n"); + sb.append("HandlerMethod details: \n"); + sb.append("Bean [").append(bean.getClass().getName()).append("]\n"); + sb.append("Method [").append(this.method.toGenericString()).append("]\n"); + return sb.toString(); + } + + /** + * Assert that the target bean class is an instance of the class where the given + * method is declared. In some cases the actual bean instance at event- + * processing time may be a JDK dynamic proxy (lazy initialization, prototype + * beans, and others). Event listener beans that require proxying should prefer + * class-based proxy mechanisms. + */ + private void assertTargetBean(Method method, Object targetBean, Object[] args) { + Class methodDeclaringClass = method.getDeclaringClass(); + Class targetBeanClass = targetBean.getClass(); + if (!methodDeclaringClass.isAssignableFrom(targetBeanClass)) { + String msg = "The event listener method class '" + methodDeclaringClass.getName() + + "' is not an instance of the actual bean class '" + + targetBeanClass.getName() + "'. If the bean requires proxying " + + "(e.g. due to @Transactional), please use class-based proxying."; + throw new IllegalStateException(getInvocationErrorMessage(targetBean, msg, args)); + } + } + + private String getInvocationErrorMessage(Object bean, String message, Object[] resolvedArgs) { + StringBuilder sb = new StringBuilder(getDetailedErrorMessage(bean, message)); + sb.append("Resolved arguments: \n"); + for (int i = 0; i < resolvedArgs.length; i++) { + sb.append("[").append(i).append("] "); + if (resolvedArgs[i] == null) { + sb.append("[null] \n"); + } + else { + sb.append("[type=").append(resolvedArgs[i].getClass().getName()).append("] "); + sb.append("[value=").append(resolvedArgs[i]).append("]\n"); + } + } + return sb.toString(); + } + + @Nullable + private ResolvableType getResolvableType(ApplicationEvent event) { + ResolvableType payloadType = null; + if (event instanceof PayloadApplicationEvent) { + PayloadApplicationEvent payloadEvent = (PayloadApplicationEvent) event; + ResolvableType eventType = payloadEvent.getResolvableType(); + if (eventType != null) { + payloadType = eventType.as(PayloadApplicationEvent.class).getGeneric(); + } + } + for (ResolvableType declaredEventType : this.declaredEventTypes) { + Class eventClass = declaredEventType.toClass(); + if (!ApplicationEvent.class.isAssignableFrom(eventClass) && + payloadType != null && declaredEventType.isAssignableFrom(payloadType)) { + return declaredEventType; + } + if (eventClass.isInstance(event)) { + return declaredEventType; + } + } + return null; + } + + + @Override + public String toString() { + return this.method.toGenericString(); + } + + + private class ReactiveResultHandler { + + public boolean subscribeToPublisher(Object result) { + ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(result.getClass()); + if (adapter != null) { + adapter.toPublisher(result).subscribe(new EventPublicationSubscriber()); + return true; + } + return false; + } + } + + + private class EventPublicationSubscriber implements Subscriber { + + @Override + public void onSubscribe(Subscription s) { + s.request(Integer.MAX_VALUE); + } + + @Override + public void onNext(Object o) { + publishEvents(o); + } + + @Override + public void onError(Throwable t) { + handleAsyncError(t); + } + + @Override + public void onComplete() { + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/event/ContextClosedEvent.java b/spring-context/src/main/java/org/springframework/context/event/ContextClosedEvent.java new file mode 100644 index 0000000..900bf30 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/ContextClosedEvent.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event; + +import org.springframework.context.ApplicationContext; + +/** + * Event raised when an {@code ApplicationContext} gets closed. + * + * @author Juergen Hoeller + * @since 12.08.2003 + * @see ContextRefreshedEvent + */ +@SuppressWarnings("serial") +public class ContextClosedEvent extends ApplicationContextEvent { + + /** + * Creates a new ContextClosedEvent. + * @param source the {@code ApplicationContext} that has been closed + * (must not be {@code null}) + */ + public ContextClosedEvent(ApplicationContext source) { + super(source); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/event/ContextRefreshedEvent.java b/spring-context/src/main/java/org/springframework/context/event/ContextRefreshedEvent.java new file mode 100644 index 0000000..27c657a --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/ContextRefreshedEvent.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event; + +import org.springframework.context.ApplicationContext; + +/** + * Event raised when an {@code ApplicationContext} gets initialized or refreshed. + * + * @author Juergen Hoeller + * @since 04.03.2003 + * @see ContextClosedEvent + */ +@SuppressWarnings("serial") +public class ContextRefreshedEvent extends ApplicationContextEvent { + + /** + * Create a new ContextRefreshedEvent. + * @param source the {@code ApplicationContext} that has been initialized + * or refreshed (must not be {@code null}) + */ + public ContextRefreshedEvent(ApplicationContext source) { + super(source); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/event/ContextStartedEvent.java b/spring-context/src/main/java/org/springframework/context/event/ContextStartedEvent.java new file mode 100644 index 0000000..bfd615d --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/ContextStartedEvent.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event; + +import org.springframework.context.ApplicationContext; + +/** + * Event raised when an {@code ApplicationContext} gets started. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @since 2.5 + * @see ContextStoppedEvent + */ +@SuppressWarnings("serial") +public class ContextStartedEvent extends ApplicationContextEvent { + + /** + * Create a new ContextStartedEvent. + * @param source the {@code ApplicationContext} that has been started + * (must not be {@code null}) + */ + public ContextStartedEvent(ApplicationContext source) { + super(source); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/event/ContextStoppedEvent.java b/spring-context/src/main/java/org/springframework/context/event/ContextStoppedEvent.java new file mode 100644 index 0000000..4a156b2 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/ContextStoppedEvent.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event; + +import org.springframework.context.ApplicationContext; + +/** + * Event raised when an {@code ApplicationContext} gets stopped. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @since 2.5 + * @see ContextStartedEvent + */ +@SuppressWarnings("serial") +public class ContextStoppedEvent extends ApplicationContextEvent { + + /** + * Create a new ContextStoppedEvent. + * @param source the {@code ApplicationContext} that has been stopped + * (must not be {@code null}) + */ + public ContextStoppedEvent(ApplicationContext source) { + super(source); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/event/DefaultEventListenerFactory.java b/spring-context/src/main/java/org/springframework/context/event/DefaultEventListenerFactory.java new file mode 100644 index 0000000..72d2988 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/DefaultEventListenerFactory.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event; + +import java.lang.reflect.Method; + +import org.springframework.context.ApplicationListener; +import org.springframework.core.Ordered; + +/** + * Default {@link EventListenerFactory} implementation that supports the + * regular {@link EventListener} annotation. + * + *

    Used as "catch-all" implementation by default. + * + * @author Stephane Nicoll + * @since 4.2 + * @see ApplicationListenerMethodAdapter + */ +public class DefaultEventListenerFactory implements EventListenerFactory, Ordered { + + private int order = LOWEST_PRECEDENCE; + + + public void setOrder(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return this.order; + } + + + @Override + public boolean supportsMethod(Method method) { + return true; + } + + @Override + public ApplicationListener createApplicationListener(String beanName, Class type, Method method) { + return new ApplicationListenerMethodAdapter(beanName, type, method); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/event/EventExpressionEvaluator.java b/spring-context/src/main/java/org/springframework/context/event/EventExpressionEvaluator.java new file mode 100644 index 0000000..ed1974f --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/EventExpressionEvaluator.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.expression.AnnotatedElementKey; +import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.context.expression.CachedExpressionEvaluator; +import org.springframework.context.expression.MethodBasedEvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.lang.Nullable; + +/** + * Utility class for handling SpEL expression parsing for application events. + *

    Meant to be used as a reusable, thread-safe component. + * + * @author Stephane Nicoll + * @since 4.2 + * @see CachedExpressionEvaluator + */ +class EventExpressionEvaluator extends CachedExpressionEvaluator { + + private final Map conditionCache = new ConcurrentHashMap<>(64); + + + /** + * Determine if the condition defined by the specified expression evaluates + * to {@code true}. + */ + public boolean condition(String conditionExpression, ApplicationEvent event, Method targetMethod, + AnnotatedElementKey methodKey, Object[] args, @Nullable BeanFactory beanFactory) { + + EventExpressionRootObject root = new EventExpressionRootObject(event, args); + MethodBasedEvaluationContext evaluationContext = new MethodBasedEvaluationContext( + root, targetMethod, args, getParameterNameDiscoverer()); + if (beanFactory != null) { + evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory)); + } + + return (Boolean.TRUE.equals(getExpression(this.conditionCache, methodKey, conditionExpression).getValue( + evaluationContext, Boolean.class))); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/event/EventExpressionRootObject.java b/spring-context/src/main/java/org/springframework/context/event/EventExpressionRootObject.java new file mode 100644 index 0000000..1d9ba77 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/EventExpressionRootObject.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event; + +import org.springframework.context.ApplicationEvent; + +/** + * Root object used during event listener expression evaluation. + * + * @author Stephane Nicoll + * @since 4.2 + */ +class EventExpressionRootObject { + + private final ApplicationEvent event; + + private final Object[] args; + + public EventExpressionRootObject(ApplicationEvent event, Object[] args) { + this.event = event; + this.args = args; + } + + public ApplicationEvent getEvent() { + return this.event; + } + + public Object[] getArgs() { + return this.args; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/event/EventListener.java b/spring-context/src/main/java/org/springframework/context/event/EventListener.java new file mode 100644 index 0000000..4d1930e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/EventListener.java @@ -0,0 +1,131 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.ApplicationEvent; +import org.springframework.core.annotation.AliasFor; + +/** + * Annotation that marks a method as a listener for application events. + * + *

    If an annotated method supports a single event type, the method may + * declare a single parameter that reflects the event type to listen to. + * If an annotated method supports multiple event types, this annotation + * may refer to one or more supported event types using the {@code classes} + * attribute. See the {@link #classes} javadoc for further details. + * + *

    Events can be {@link ApplicationEvent} instances as well as arbitrary + * objects. + * + *

    Processing of {@code @EventListener} annotations is performed via + * the internal {@link EventListenerMethodProcessor} bean which gets + * registered automatically when using Java config or manually via the + * {@code } or {@code } + * element when using XML config. + * + *

    Annotated methods may have a non-{@code void} return type. When they + * do, the result of the method invocation is sent as a new event. If the + * return type is either an array or a collection, each element is sent + * as a new individual event. + * + *

    This annotation may be used as a meta-annotation to create custom + * composed annotations. + * + *

    Exception Handling

    + *

    While it is possible for an event listener to declare that it + * throws arbitrary exception types, any checked exceptions thrown + * from an event listener will be wrapped in an + * {@link java.lang.reflect.UndeclaredThrowableException UndeclaredThrowableException} + * since the event publisher can only handle runtime exceptions. + * + *

    Asynchronous Listeners

    + *

    If you want a particular listener to process events asynchronously, you + * can use Spring's {@link org.springframework.scheduling.annotation.Async @Async} + * support, but be aware of the following limitations when using asynchronous events. + * + *

      + *
    • If an asynchronous event listener throws an exception, it is not propagated + * to the caller. See {@link org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler + * AsyncUncaughtExceptionHandler} for more details.
    • + *
    • Asynchronous event listener methods cannot publish a subsequent event by returning a + * value. If you need to publish another event as the result of the processing, inject an + * {@link org.springframework.context.ApplicationEventPublisher ApplicationEventPublisher} + * to publish the event manually.
    • + *
    + * + *

    Ordering Listeners

    + *

    It is also possible to define the order in which listeners for a + * certain event are to be invoked. To do so, add Spring's common + * {@link org.springframework.core.annotation.Order @Order} annotation + * alongside this event listener annotation. + * + * @author Stephane Nicoll + * @author Sam Brannen + * @since 4.2 + * @see EventListenerMethodProcessor + */ +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface EventListener { + + /** + * Alias for {@link #classes}. + */ + @AliasFor("classes") + Class[] value() default {}; + + /** + * The event classes that this listener handles. + *

    If this attribute is specified with a single value, the + * annotated method may optionally accept a single parameter. + * However, if this attribute is specified with multiple values, + * the annotated method must not declare any parameters. + */ + @AliasFor("value") + Class[] classes() default {}; + + /** + * Spring Expression Language (SpEL) expression used for making the event + * handling conditional. + *

    The event will be handled if the expression evaluates to boolean + * {@code true} or one of the following strings: {@code "true"}, {@code "on"}, + * {@code "yes"}, or {@code "1"}. + *

    The default expression is {@code ""}, meaning the event is always handled. + *

    The SpEL expression will be evaluated against a dedicated context that + * provides the following metadata: + *

      + *
    • {@code #root.event} or {@code event} for references to the + * {@link ApplicationEvent}
    • + *
    • {@code #root.args} or {@code args} for references to the method + * arguments array
    • + *
    • Method arguments can be accessed by index. For example, the first + * argument can be accessed via {@code #root.args[0]}, {@code args[0]}, + * {@code #a0}, or {@code #p0}.
    • + *
    • Method arguments can be accessed by name (with a preceding hash tag) + * if parameter names are available in the compiled byte code.
    • + *
    + */ + String condition() default ""; + +} diff --git a/spring-context/src/main/java/org/springframework/context/event/EventListenerFactory.java b/spring-context/src/main/java/org/springframework/context/event/EventListenerFactory.java new file mode 100644 index 0000000..1f8e9e9 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/EventListenerFactory.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event; + +import java.lang.reflect.Method; + +import org.springframework.context.ApplicationListener; + +/** + * Strategy interface for creating {@link ApplicationListener} for methods + * annotated with {@link EventListener}. + * + * @author Stephane Nicoll + * @since 4.2 + */ +public interface EventListenerFactory { + + /** + * Specify if this factory supports the specified {@link Method}. + * @param method an {@link EventListener} annotated method + * @return {@code true} if this factory supports the specified method + */ + boolean supportsMethod(Method method); + + /** + * Create an {@link ApplicationListener} for the specified method. + * @param beanName the name of the bean + * @param type the target type of the instance + * @param method the {@link EventListener} annotated method + * @return an application listener, suitable to invoke the specified method + */ + ApplicationListener createApplicationListener(String beanName, Class type, Method method); + +} diff --git a/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java b/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java new file mode 100644 index 0000000..ad89b9e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java @@ -0,0 +1,228 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aop.framework.autoproxy.AutoProxyUtils; +import org.springframework.aop.scope.ScopedObject; +import org.springframework.aop.scope.ScopedProxyUtils; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.MethodIntrospector; +import org.springframework.core.SpringProperties; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; + +/** + * Registers {@link EventListener} methods as individual {@link ApplicationListener} instances. + * Implements {@link BeanFactoryPostProcessor} (as of 5.1) primarily for early retrieval, + * avoiding AOP checks for this processor bean and its {@link EventListenerFactory} delegates. + * + * @author Stephane Nicoll + * @author Juergen Hoeller + * @author Sebastien Deleuze + * @since 4.2 + * @see EventListenerFactory + * @see DefaultEventListenerFactory + */ +public class EventListenerMethodProcessor + implements SmartInitializingSingleton, ApplicationContextAware, BeanFactoryPostProcessor { + + /** + * Boolean flag controlled by a {@code spring.spel.ignore} system property that instructs Spring to + * ignore SpEL, i.e. to not initialize the SpEL infrastructure. + *

    The default is "false". + */ + private static final boolean shouldIgnoreSpel = SpringProperties.getFlag("spring.spel.ignore"); + + + protected final Log logger = LogFactory.getLog(getClass()); + + @Nullable + private ConfigurableApplicationContext applicationContext; + + @Nullable + private ConfigurableListableBeanFactory beanFactory; + + @Nullable + private List eventListenerFactories; + + @Nullable + private final EventExpressionEvaluator evaluator; + + private final Set> nonAnnotatedClasses = Collections.newSetFromMap(new ConcurrentHashMap<>(64)); + + + public EventListenerMethodProcessor() { + if (shouldIgnoreSpel) { + this.evaluator = null; + } + else { + this.evaluator = new EventExpressionEvaluator(); + } + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + Assert.isTrue(applicationContext instanceof ConfigurableApplicationContext, + "ApplicationContext does not implement ConfigurableApplicationContext"); + this.applicationContext = (ConfigurableApplicationContext) applicationContext; + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { + this.beanFactory = beanFactory; + + Map beans = beanFactory.getBeansOfType(EventListenerFactory.class, false, false); + List factories = new ArrayList<>(beans.values()); + AnnotationAwareOrderComparator.sort(factories); + this.eventListenerFactories = factories; + } + + + @Override + public void afterSingletonsInstantiated() { + ConfigurableListableBeanFactory beanFactory = this.beanFactory; + Assert.state(this.beanFactory != null, "No ConfigurableListableBeanFactory set"); + String[] beanNames = beanFactory.getBeanNamesForType(Object.class); + for (String beanName : beanNames) { + if (!ScopedProxyUtils.isScopedTarget(beanName)) { + Class type = null; + try { + type = AutoProxyUtils.determineTargetClass(beanFactory, beanName); + } + catch (Throwable ex) { + // An unresolvable bean type, probably from a lazy bean - let's ignore it. + if (logger.isDebugEnabled()) { + logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex); + } + } + if (type != null) { + if (ScopedObject.class.isAssignableFrom(type)) { + try { + Class targetClass = AutoProxyUtils.determineTargetClass( + beanFactory, ScopedProxyUtils.getTargetBeanName(beanName)); + if (targetClass != null) { + type = targetClass; + } + } + catch (Throwable ex) { + // An invalid scoped proxy arrangement - let's ignore it. + if (logger.isDebugEnabled()) { + logger.debug("Could not resolve target bean for scoped proxy '" + beanName + "'", ex); + } + } + } + try { + processBean(beanName, type); + } + catch (Throwable ex) { + throw new BeanInitializationException("Failed to process @EventListener " + + "annotation on bean with name '" + beanName + "'", ex); + } + } + } + } + } + + private void processBean(final String beanName, final Class targetType) { + if (!this.nonAnnotatedClasses.contains(targetType) && + AnnotationUtils.isCandidateClass(targetType, EventListener.class) && + !isSpringContainerClass(targetType)) { + + Map annotatedMethods = null; + try { + annotatedMethods = MethodIntrospector.selectMethods(targetType, + (MethodIntrospector.MetadataLookup) method -> + AnnotatedElementUtils.findMergedAnnotation(method, EventListener.class)); + } + catch (Throwable ex) { + // An unresolvable type in a method signature, probably from a lazy bean - let's ignore it. + if (logger.isDebugEnabled()) { + logger.debug("Could not resolve methods for bean with name '" + beanName + "'", ex); + } + } + + if (CollectionUtils.isEmpty(annotatedMethods)) { + this.nonAnnotatedClasses.add(targetType); + if (logger.isTraceEnabled()) { + logger.trace("No @EventListener annotations found on bean class: " + targetType.getName()); + } + } + else { + // Non-empty set of methods + ConfigurableApplicationContext context = this.applicationContext; + Assert.state(context != null, "No ApplicationContext set"); + List factories = this.eventListenerFactories; + Assert.state(factories != null, "EventListenerFactory List not initialized"); + for (Method method : annotatedMethods.keySet()) { + for (EventListenerFactory factory : factories) { + if (factory.supportsMethod(method)) { + Method methodToUse = AopUtils.selectInvocableMethod(method, context.getType(beanName)); + ApplicationListener applicationListener = + factory.createApplicationListener(beanName, targetType, methodToUse); + if (applicationListener instanceof ApplicationListenerMethodAdapter) { + ((ApplicationListenerMethodAdapter) applicationListener).init(context, this.evaluator); + } + context.addApplicationListener(applicationListener); + break; + } + } + } + if (logger.isDebugEnabled()) { + logger.debug(annotatedMethods.size() + " @EventListener methods processed on bean '" + + beanName + "': " + annotatedMethods); + } + } + } + } + + /** + * Determine whether the given class is an {@code org.springframework} + * bean class that is not annotated as a user or test {@link Component}... + * which indicates that there is no {@link EventListener} to be found there. + * @since 5.1 + */ + private static boolean isSpringContainerClass(Class clazz) { + return (clazz.getName().startsWith("org.springframework.") && + !AnnotatedElementUtils.isAnnotated(ClassUtils.getUserClass(clazz), Component.class)); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/event/EventPublicationInterceptor.java b/spring-context/src/main/java/org/springframework/context/event/EventPublicationInterceptor.java new file mode 100644 index 0000000..ef5c2e6 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/EventPublicationInterceptor.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event; + +import java.lang.reflect.Constructor; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * {@link MethodInterceptor Interceptor} that publishes an + * {@code ApplicationEvent} to all {@code ApplicationListeners} + * registered with an {@code ApplicationEventPublisher} after each + * successful method invocation. + * + *

    Note that this interceptor is only capable of publishing stateless + * events configured via the + * {@link #setApplicationEventClass "applicationEventClass"} property. + * + * @author Dmitriy Kopylenko + * @author Juergen Hoeller + * @author Rick Evans + * @see #setApplicationEventClass + * @see org.springframework.context.ApplicationEvent + * @see org.springframework.context.ApplicationListener + * @see org.springframework.context.ApplicationEventPublisher + * @see org.springframework.context.ApplicationContext + */ +public class EventPublicationInterceptor + implements MethodInterceptor, ApplicationEventPublisherAware, InitializingBean { + + @Nullable + private Constructor applicationEventClassConstructor; + + @Nullable + private ApplicationEventPublisher applicationEventPublisher; + + + /** + * Set the application event class to publish. + *

    The event class must have a constructor with a single + * {@code Object} argument for the event source. The interceptor + * will pass in the invoked object. + * @throws IllegalArgumentException if the supplied {@code Class} is + * {@code null} or if it is not an {@code ApplicationEvent} subclass or + * if it does not expose a constructor that takes a single {@code Object} argument + */ + public void setApplicationEventClass(Class applicationEventClass) { + if (ApplicationEvent.class == applicationEventClass || + !ApplicationEvent.class.isAssignableFrom(applicationEventClass)) { + throw new IllegalArgumentException("'applicationEventClass' needs to extend ApplicationEvent"); + } + try { + this.applicationEventClassConstructor = applicationEventClass.getConstructor(Object.class); + } + catch (NoSuchMethodException ex) { + throw new IllegalArgumentException("ApplicationEvent class [" + + applicationEventClass.getName() + "] does not have the required Object constructor: " + ex); + } + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } + + @Override + public void afterPropertiesSet() throws Exception { + if (this.applicationEventClassConstructor == null) { + throw new IllegalArgumentException("Property 'applicationEventClass' is required"); + } + } + + + @Override + @Nullable + public Object invoke(MethodInvocation invocation) throws Throwable { + Object retVal = invocation.proceed(); + + Assert.state(this.applicationEventClassConstructor != null, "No ApplicationEvent class set"); + ApplicationEvent event = (ApplicationEvent) + this.applicationEventClassConstructor.newInstance(invocation.getThis()); + + Assert.state(this.applicationEventPublisher != null, "No ApplicationEventPublisher available"); + this.applicationEventPublisher.publishEvent(event); + + return retVal; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListener.java b/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListener.java new file mode 100644 index 0000000..0df8d3a --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListener.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event; + +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.core.Ordered; +import org.springframework.core.ResolvableType; +import org.springframework.lang.Nullable; + +/** + * Extended variant of the standard {@link ApplicationListener} interface, + * exposing further metadata such as the supported event and source type. + * + *

    As of Spring Framework 4.2, this interface supersedes the Class-based + * {@link SmartApplicationListener} with full handling of generic event types. + * + * @author Stephane Nicoll + * @since 4.2 + * @see SmartApplicationListener + * @see GenericApplicationListenerAdapter + */ +public interface GenericApplicationListener extends ApplicationListener, Ordered { + + /** + * Determine whether this listener actually supports the given event type. + * @param eventType the event type (never {@code null}) + */ + boolean supportsEventType(ResolvableType eventType); + + /** + * Determine whether this listener actually supports the given source type. + *

    The default implementation always returns {@code true}. + * @param sourceType the source type, or {@code null} if no source + */ + default boolean supportsSourceType(@Nullable Class sourceType) { + return true; + } + + /** + * Determine this listener's order in a set of listeners for the same event. + *

    The default implementation returns {@link #LOWEST_PRECEDENCE}. + */ + @Override + default int getOrder() { + return LOWEST_PRECEDENCE; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListenerAdapter.java b/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListenerAdapter.java new file mode 100644 index 0000000..b80d035 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListenerAdapter.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event; + +import java.util.Map; + +import org.springframework.aop.support.AopUtils; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.core.Ordered; +import org.springframework.core.ResolvableType; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ConcurrentReferenceHashMap; + +/** + * {@link GenericApplicationListener} adapter that determines supported event types + * through introspecting the generically declared type of the target listener. + * + * @author Juergen Hoeller + * @author Stephane Nicoll + * @since 3.0 + * @see org.springframework.context.ApplicationListener#onApplicationEvent + */ +public class GenericApplicationListenerAdapter implements GenericApplicationListener, SmartApplicationListener { + + private static final Map, ResolvableType> eventTypeCache = new ConcurrentReferenceHashMap<>(); + + + private final ApplicationListener delegate; + + @Nullable + private final ResolvableType declaredEventType; + + + /** + * Create a new GenericApplicationListener for the given delegate. + * @param delegate the delegate listener to be invoked + */ + @SuppressWarnings("unchecked") + public GenericApplicationListenerAdapter(ApplicationListener delegate) { + Assert.notNull(delegate, "Delegate listener must not be null"); + this.delegate = (ApplicationListener) delegate; + this.declaredEventType = resolveDeclaredEventType(this.delegate); + } + + + @Override + public void onApplicationEvent(ApplicationEvent event) { + this.delegate.onApplicationEvent(event); + } + + @Override + @SuppressWarnings("unchecked") + public boolean supportsEventType(ResolvableType eventType) { + if (this.delegate instanceof SmartApplicationListener) { + Class eventClass = (Class) eventType.resolve(); + return (eventClass != null && ((SmartApplicationListener) this.delegate).supportsEventType(eventClass)); + } + else { + return (this.declaredEventType == null || this.declaredEventType.isAssignableFrom(eventType)); + } + } + + @Override + public boolean supportsEventType(Class eventType) { + return supportsEventType(ResolvableType.forClass(eventType)); + } + + @Override + public boolean supportsSourceType(@Nullable Class sourceType) { + return !(this.delegate instanceof SmartApplicationListener) || + ((SmartApplicationListener) this.delegate).supportsSourceType(sourceType); + } + + @Override + public int getOrder() { + return (this.delegate instanceof Ordered ? ((Ordered) this.delegate).getOrder() : Ordered.LOWEST_PRECEDENCE); + } + + + @Nullable + private static ResolvableType resolveDeclaredEventType(ApplicationListener listener) { + ResolvableType declaredEventType = resolveDeclaredEventType(listener.getClass()); + if (declaredEventType == null || declaredEventType.isAssignableFrom(ApplicationEvent.class)) { + Class targetClass = AopUtils.getTargetClass(listener); + if (targetClass != listener.getClass()) { + declaredEventType = resolveDeclaredEventType(targetClass); + } + } + return declaredEventType; + } + + @Nullable + static ResolvableType resolveDeclaredEventType(Class listenerType) { + ResolvableType eventType = eventTypeCache.get(listenerType); + if (eventType == null) { + eventType = ResolvableType.forClass(listenerType).as(ApplicationListener.class).getGeneric(); + eventTypeCache.put(listenerType, eventType); + } + return (eventType != ResolvableType.NONE ? eventType : null); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java b/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java new file mode 100644 index 0000000..2ca8e8d --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java @@ -0,0 +1,208 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event; + +import java.util.concurrent.Executor; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.core.ResolvableType; +import org.springframework.lang.Nullable; +import org.springframework.util.ErrorHandler; + +/** + * Simple implementation of the {@link ApplicationEventMulticaster} interface. + * + *

    Multicasts all events to all registered listeners, leaving it up to + * the listeners to ignore events that they are not interested in. + * Listeners will usually perform corresponding {@code instanceof} + * checks on the passed-in event object. + * + *

    By default, all listeners are invoked in the calling thread. + * This allows the danger of a rogue listener blocking the entire application, + * but adds minimal overhead. Specify an alternative task executor to have + * listeners executed in different threads, for example from a thread pool. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Stephane Nicoll + * @author Brian Clozel + * @see #setTaskExecutor + */ +public class SimpleApplicationEventMulticaster extends AbstractApplicationEventMulticaster { + + @Nullable + private Executor taskExecutor; + + @Nullable + private ErrorHandler errorHandler; + + + /** + * Create a new SimpleApplicationEventMulticaster. + */ + public SimpleApplicationEventMulticaster() { + } + + /** + * Create a new SimpleApplicationEventMulticaster for the given BeanFactory. + */ + public SimpleApplicationEventMulticaster(BeanFactory beanFactory) { + setBeanFactory(beanFactory); + } + + + /** + * Set a custom executor (typically a {@link org.springframework.core.task.TaskExecutor}) + * to invoke each listener with. + *

    Default is equivalent to {@link org.springframework.core.task.SyncTaskExecutor}, + * executing all listeners synchronously in the calling thread. + *

    Consider specifying an asynchronous task executor here to not block the + * caller until all listeners have been executed. However, note that asynchronous + * execution will not participate in the caller's thread context (class loader, + * transaction association) unless the TaskExecutor explicitly supports this. + * @see org.springframework.core.task.SyncTaskExecutor + * @see org.springframework.core.task.SimpleAsyncTaskExecutor + */ + public void setTaskExecutor(@Nullable Executor taskExecutor) { + this.taskExecutor = taskExecutor; + } + + /** + * Return the current task executor for this multicaster. + */ + @Nullable + protected Executor getTaskExecutor() { + return this.taskExecutor; + } + + /** + * Set the {@link ErrorHandler} to invoke in case an exception is thrown + * from a listener. + *

    Default is none, with a listener exception stopping the current + * multicast and getting propagated to the publisher of the current event. + * If a {@linkplain #setTaskExecutor task executor} is specified, each + * individual listener exception will get propagated to the executor but + * won't necessarily stop execution of other listeners. + *

    Consider setting an {@link ErrorHandler} implementation that catches + * and logs exceptions (a la + * {@link org.springframework.scheduling.support.TaskUtils#LOG_AND_SUPPRESS_ERROR_HANDLER}) + * or an implementation that logs exceptions while nevertheless propagating them + * (e.g. {@link org.springframework.scheduling.support.TaskUtils#LOG_AND_PROPAGATE_ERROR_HANDLER}). + * @since 4.1 + */ + public void setErrorHandler(@Nullable ErrorHandler errorHandler) { + this.errorHandler = errorHandler; + } + + /** + * Return the current error handler for this multicaster. + * @since 4.1 + */ + @Nullable + protected ErrorHandler getErrorHandler() { + return this.errorHandler; + } + + @Override + public void multicastEvent(ApplicationEvent event) { + multicastEvent(event, resolveDefaultEventType(event)); + } + + @Override + public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) { + ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event)); + Executor executor = getTaskExecutor(); + for (ApplicationListener listener : getApplicationListeners(event, type)) { + if (executor != null) { + executor.execute(() -> invokeListener(listener, event)); + } + else { + invokeListener(listener, event); + } + } + } + + private ResolvableType resolveDefaultEventType(ApplicationEvent event) { + return ResolvableType.forInstance(event); + } + + /** + * Invoke the given listener with the given event. + * @param listener the ApplicationListener to invoke + * @param event the current event to propagate + * @since 4.1 + */ + protected void invokeListener(ApplicationListener listener, ApplicationEvent event) { + ErrorHandler errorHandler = getErrorHandler(); + if (errorHandler != null) { + try { + doInvokeListener(listener, event); + } + catch (Throwable err) { + errorHandler.handleError(err); + } + } + else { + doInvokeListener(listener, event); + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) { + try { + listener.onApplicationEvent(event); + } + catch (ClassCastException ex) { + String msg = ex.getMessage(); + if (msg == null || matchesClassCastMessage(msg, event.getClass())) { + // Possibly a lambda-defined listener which we could not resolve the generic event type for + // -> let's suppress the exception and just log a debug message. + Log logger = LogFactory.getLog(getClass()); + if (logger.isTraceEnabled()) { + logger.trace("Non-matching event type for listener: " + listener, ex); + } + } + else { + throw ex; + } + } + } + + private boolean matchesClassCastMessage(String classCastMessage, Class eventClass) { + // On Java 8, the message starts with the class name: "java.lang.String cannot be cast..." + if (classCastMessage.startsWith(eventClass.getName())) { + return true; + } + // On Java 11, the message starts with "class ..." a.k.a. Class.toString() + if (classCastMessage.startsWith(eventClass.toString())) { + return true; + } + // On Java 9, the message used to contain the module name: "java.base/java.lang.String cannot be cast..." + int moduleSeparatorIndex = classCastMessage.indexOf('/'); + if (moduleSeparatorIndex != -1 && classCastMessage.startsWith(eventClass.getName(), moduleSeparatorIndex + 1)) { + return true; + } + // Assuming an unrelated class cast failure... + return false; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/event/SmartApplicationListener.java b/spring-context/src/main/java/org/springframework/context/event/SmartApplicationListener.java new file mode 100644 index 0000000..548b67f --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/SmartApplicationListener.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event; + +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.core.Ordered; +import org.springframework.lang.Nullable; + +/** + * Extended variant of the standard {@link ApplicationListener} interface, + * exposing further metadata such as the supported event and source type. + * + *

    For full introspection of generic event types, consider implementing + * the {@link GenericApplicationListener} interface instead. + * + * @author Juergen Hoeller + * @since 3.0 + * @see GenericApplicationListener + * @see GenericApplicationListenerAdapter + */ +public interface SmartApplicationListener extends ApplicationListener, Ordered { + + /** + * Determine whether this listener actually supports the given event type. + * @param eventType the event type (never {@code null}) + */ + boolean supportsEventType(Class eventType); + + /** + * Determine whether this listener actually supports the given source type. + *

    The default implementation always returns {@code true}. + * @param sourceType the source type, or {@code null} if no source + */ + default boolean supportsSourceType(@Nullable Class sourceType) { + return true; + } + + /** + * Determine this listener's order in a set of listeners for the same event. + *

    The default implementation returns {@link #LOWEST_PRECEDENCE}. + */ + @Override + default int getOrder() { + return LOWEST_PRECEDENCE; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/event/SourceFilteringListener.java b/spring-context/src/main/java/org/springframework/context/event/SourceFilteringListener.java new file mode 100644 index 0000000..c6e52df --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/SourceFilteringListener.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event; + +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.core.Ordered; +import org.springframework.core.ResolvableType; +import org.springframework.lang.Nullable; + +/** + * {@link org.springframework.context.ApplicationListener} decorator that filters + * events from a specified event source, invoking its delegate listener for + * matching {@link org.springframework.context.ApplicationEvent} objects only. + * + *

    Can also be used as base class, overriding the {@link #onApplicationEventInternal} + * method instead of specifying a delegate listener. + * + * @author Juergen Hoeller + * @author Stephane Nicoll + * @since 2.0.5 + */ +public class SourceFilteringListener implements GenericApplicationListener, SmartApplicationListener { + + private final Object source; + + @Nullable + private GenericApplicationListener delegate; + + + /** + * Create a SourceFilteringListener for the given event source. + * @param source the event source that this listener filters for, + * only processing events from this source + * @param delegate the delegate listener to invoke with event + * from the specified source + */ + public SourceFilteringListener(Object source, ApplicationListener delegate) { + this.source = source; + this.delegate = (delegate instanceof GenericApplicationListener ? + (GenericApplicationListener) delegate : new GenericApplicationListenerAdapter(delegate)); + } + + /** + * Create a SourceFilteringListener for the given event source, + * expecting subclasses to override the {@link #onApplicationEventInternal} + * method (instead of specifying a delegate listener). + * @param source the event source that this listener filters for, + * only processing events from this source + */ + protected SourceFilteringListener(Object source) { + this.source = source; + } + + + @Override + public void onApplicationEvent(ApplicationEvent event) { + if (event.getSource() == this.source) { + onApplicationEventInternal(event); + } + } + + @Override + public boolean supportsEventType(ResolvableType eventType) { + return (this.delegate == null || this.delegate.supportsEventType(eventType)); + } + + @Override + public boolean supportsEventType(Class eventType) { + return supportsEventType(ResolvableType.forType(eventType)); + } + + @Override + public boolean supportsSourceType(@Nullable Class sourceType) { + return (sourceType != null && sourceType.isInstance(this.source)); + } + + @Override + public int getOrder() { + return (this.delegate != null ? this.delegate.getOrder() : Ordered.LOWEST_PRECEDENCE); + } + + + /** + * Actually process the event, after having filtered according to the + * desired event source already. + *

    The default implementation invokes the specified delegate, if any. + * @param event the event to process (matching the specified source) + */ + protected void onApplicationEventInternal(ApplicationEvent event) { + if (this.delegate == null) { + throw new IllegalStateException( + "Must specify a delegate object or override the onApplicationEventInternal method"); + } + this.delegate.onApplicationEvent(event); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/event/package-info.java b/spring-context/src/main/java/org/springframework/context/event/package-info.java new file mode 100644 index 0000000..79cccd7 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/package-info.java @@ -0,0 +1,10 @@ +/** + * Support classes for application events, like standard context events. + * To be supported by all major application context implementations. + */ +@NonNullApi +@NonNullFields +package org.springframework.context.event; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/context/expression/AnnotatedElementKey.java b/spring-context/src/main/java/org/springframework/context/expression/AnnotatedElementKey.java new file mode 100644 index 0000000..582e823 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/expression/AnnotatedElementKey.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.expression; + +import java.lang.reflect.AnnotatedElement; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Represent an {@link AnnotatedElement} on a particular {@link Class} + * and is suitable as a key. + * + * @author Costin Leau + * @author Stephane Nicoll + * @since 4.2 + * @see CachedExpressionEvaluator + */ +public final class AnnotatedElementKey implements Comparable { + + private final AnnotatedElement element; + + @Nullable + private final Class targetClass; + + + /** + * Create a new instance with the specified {@link AnnotatedElement} and + * optional target {@link Class}. + */ + public AnnotatedElementKey(AnnotatedElement element, @Nullable Class targetClass) { + Assert.notNull(element, "AnnotatedElement must not be null"); + this.element = element; + this.targetClass = targetClass; + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof AnnotatedElementKey)) { + return false; + } + AnnotatedElementKey otherKey = (AnnotatedElementKey) other; + return (this.element.equals(otherKey.element) && + ObjectUtils.nullSafeEquals(this.targetClass, otherKey.targetClass)); + } + + @Override + public int hashCode() { + return this.element.hashCode() + (this.targetClass != null ? this.targetClass.hashCode() * 29 : 0); + } + + @Override + public String toString() { + return this.element + (this.targetClass != null ? " on " + this.targetClass : ""); + } + + @Override + public int compareTo(AnnotatedElementKey other) { + int result = this.element.toString().compareTo(other.element.toString()); + if (result == 0 && this.targetClass != null) { + if (other.targetClass == null) { + return 1; + } + result = this.targetClass.getName().compareTo(other.targetClass.getName()); + } + return result; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/expression/BeanExpressionContextAccessor.java b/spring-context/src/main/java/org/springframework/context/expression/BeanExpressionContextAccessor.java new file mode 100644 index 0000000..8672542 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/expression/BeanExpressionContextAccessor.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.expression; + +import org.springframework.beans.factory.config.BeanExpressionContext; +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.TypedValue; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * EL property accessor that knows how to traverse the beans and contextual objects + * of a Spring {@link org.springframework.beans.factory.config.BeanExpressionContext}. + * + * @author Juergen Hoeller + * @author Andy Clement + * @since 3.0 + */ +public class BeanExpressionContextAccessor implements PropertyAccessor { + + @Override + public boolean canRead(EvaluationContext context, @Nullable Object target, String name) throws AccessException { + return (target instanceof BeanExpressionContext && ((BeanExpressionContext) target).containsObject(name)); + } + + @Override + public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException { + Assert.state(target instanceof BeanExpressionContext, "Target must be of type BeanExpressionContext"); + return new TypedValue(((BeanExpressionContext) target).getObject(name)); + } + + @Override + public boolean canWrite(EvaluationContext context, @Nullable Object target, String name) throws AccessException { + return false; + } + + @Override + public void write(EvaluationContext context, @Nullable Object target, String name, @Nullable Object newValue) + throws AccessException { + + throw new AccessException("Beans in a BeanFactory are read-only"); + } + + @Override + public Class[] getSpecificTargetClasses() { + return new Class[] {BeanExpressionContext.class}; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/expression/BeanFactoryAccessor.java b/spring-context/src/main/java/org/springframework/context/expression/BeanFactoryAccessor.java new file mode 100644 index 0000000..58bf711 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/expression/BeanFactoryAccessor.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.expression; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.TypedValue; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * EL property accessor that knows how to traverse the beans of a + * Spring {@link org.springframework.beans.factory.BeanFactory}. + * + * @author Juergen Hoeller + * @author Andy Clement + * @since 3.0 + */ +public class BeanFactoryAccessor implements PropertyAccessor { + + @Override + public Class[] getSpecificTargetClasses() { + return new Class[] {BeanFactory.class}; + } + + @Override + public boolean canRead(EvaluationContext context, @Nullable Object target, String name) throws AccessException { + return (target instanceof BeanFactory && ((BeanFactory) target).containsBean(name)); + } + + @Override + public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException { + Assert.state(target instanceof BeanFactory, "Target must be of type BeanFactory"); + return new TypedValue(((BeanFactory) target).getBean(name)); + } + + @Override + public boolean canWrite(EvaluationContext context, @Nullable Object target, String name) throws AccessException { + return false; + } + + @Override + public void write(EvaluationContext context, @Nullable Object target, String name, @Nullable Object newValue) + throws AccessException { + + throw new AccessException("Beans in a BeanFactory are read-only"); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/expression/BeanFactoryResolver.java b/spring-context/src/main/java/org/springframework/context/expression/BeanFactoryResolver.java new file mode 100644 index 0000000..4dfaa20 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/expression/BeanFactoryResolver.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.expression; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.expression.AccessException; +import org.springframework.expression.BeanResolver; +import org.springframework.expression.EvaluationContext; +import org.springframework.util.Assert; + +/** + * EL bean resolver that operates against a Spring + * {@link org.springframework.beans.factory.BeanFactory}. + * + * @author Juergen Hoeller + * @since 3.0.4 + */ +public class BeanFactoryResolver implements BeanResolver { + + private final BeanFactory beanFactory; + + + /** + * Create a new {@link BeanFactoryResolver} for the given factory. + * @param beanFactory the {@link BeanFactory} to resolve bean names against + */ + public BeanFactoryResolver(BeanFactory beanFactory) { + Assert.notNull(beanFactory, "BeanFactory must not be null"); + this.beanFactory = beanFactory; + } + + + @Override + public Object resolve(EvaluationContext context, String beanName) throws AccessException { + try { + return this.beanFactory.getBean(beanName); + } + catch (BeansException ex) { + throw new AccessException("Could not resolve bean reference against BeanFactory", ex); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/expression/CachedExpressionEvaluator.java b/spring-context/src/main/java/org/springframework/context/expression/CachedExpressionEvaluator.java new file mode 100644 index 0000000..7b868fc --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/expression/CachedExpressionEvaluator.java @@ -0,0 +1,149 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.expression; + +import java.util.Map; + +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Shared utility class used to evaluate and cache SpEL expressions that + * are defined on {@link java.lang.reflect.AnnotatedElement}. + * + * @author Stephane Nicoll + * @since 4.2 + * @see AnnotatedElementKey + */ +public abstract class CachedExpressionEvaluator { + + private final SpelExpressionParser parser; + + private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + + + /** + * Create a new instance with the specified {@link SpelExpressionParser}. + */ + protected CachedExpressionEvaluator(SpelExpressionParser parser) { + Assert.notNull(parser, "SpelExpressionParser must not be null"); + this.parser = parser; + } + + /** + * Create a new instance with a default {@link SpelExpressionParser}. + */ + protected CachedExpressionEvaluator() { + this(new SpelExpressionParser()); + } + + + /** + * Return the {@link SpelExpressionParser} to use. + */ + protected SpelExpressionParser getParser() { + return this.parser; + } + + /** + * Return a shared parameter name discoverer which caches data internally. + * @since 4.3 + */ + protected ParameterNameDiscoverer getParameterNameDiscoverer() { + return this.parameterNameDiscoverer; + } + + + /** + * Return the {@link Expression} for the specified SpEL value + *

    Parse the expression if it hasn't been already. + * @param cache the cache to use + * @param elementKey the element on which the expression is defined + * @param expression the expression to parse + */ + protected Expression getExpression(Map cache, + AnnotatedElementKey elementKey, String expression) { + + ExpressionKey expressionKey = createKey(elementKey, expression); + Expression expr = cache.get(expressionKey); + if (expr == null) { + expr = getParser().parseExpression(expression); + cache.put(expressionKey, expr); + } + return expr; + } + + private ExpressionKey createKey(AnnotatedElementKey elementKey, String expression) { + return new ExpressionKey(elementKey, expression); + } + + + /** + * An expression key. + */ + protected static class ExpressionKey implements Comparable { + + private final AnnotatedElementKey element; + + private final String expression; + + protected ExpressionKey(AnnotatedElementKey element, String expression) { + Assert.notNull(element, "AnnotatedElementKey must not be null"); + Assert.notNull(expression, "Expression must not be null"); + this.element = element; + this.expression = expression; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof ExpressionKey)) { + return false; + } + ExpressionKey otherKey = (ExpressionKey) other; + return (this.element.equals(otherKey.element) && + ObjectUtils.nullSafeEquals(this.expression, otherKey.expression)); + } + + @Override + public int hashCode() { + return this.element.hashCode() * 29 + this.expression.hashCode(); + } + + @Override + public String toString() { + return this.element + " with expression \"" + this.expression + "\""; + } + + @Override + public int compareTo(ExpressionKey other) { + int result = this.element.toString().compareTo(other.element.toString()); + if (result == 0) { + result = this.expression.compareTo(other.expression); + } + return result; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/expression/EnvironmentAccessor.java b/spring-context/src/main/java/org/springframework/context/expression/EnvironmentAccessor.java new file mode 100644 index 0000000..8e5dd92 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/expression/EnvironmentAccessor.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.expression; + +import org.springframework.core.env.Environment; +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.TypedValue; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Read-only EL property accessor that knows how to retrieve keys + * of a Spring {@link Environment} instance. + * + * @author Chris Beams + * @since 3.1 + */ +public class EnvironmentAccessor implements PropertyAccessor { + + @Override + public Class[] getSpecificTargetClasses() { + return new Class[] {Environment.class}; + } + + /** + * Can read any {@link Environment}, thus always returns true. + * @return true + */ + @Override + public boolean canRead(EvaluationContext context, @Nullable Object target, String name) throws AccessException { + return true; + } + + /** + * Access the given target object by resolving the given property name against the given target + * environment. + */ + @Override + public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException { + Assert.state(target instanceof Environment, "Target must be of type Environment"); + return new TypedValue(((Environment) target).getProperty(name)); + } + + /** + * Read-only: returns {@code false}. + */ + @Override + public boolean canWrite(EvaluationContext context, @Nullable Object target, String name) throws AccessException { + return false; + } + + /** + * Read-only: no-op. + */ + @Override + public void write(EvaluationContext context, @Nullable Object target, String name, @Nullable Object newValue) + throws AccessException { + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/expression/MapAccessor.java b/spring-context/src/main/java/org/springframework/context/expression/MapAccessor.java new file mode 100644 index 0000000..5acc804 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/expression/MapAccessor.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.expression; + +import java.util.Map; + +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.CompilablePropertyAccessor; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * EL property accessor that knows how to traverse the keys + * of a standard {@link java.util.Map}. + * + * @author Juergen Hoeller + * @author Andy Clement + * @since 3.0 + */ +public class MapAccessor implements CompilablePropertyAccessor { + + @Override + public Class[] getSpecificTargetClasses() { + return new Class[] {Map.class}; + } + + @Override + public boolean canRead(EvaluationContext context, @Nullable Object target, String name) throws AccessException { + return (target instanceof Map && ((Map) target).containsKey(name)); + } + + @Override + public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException { + Assert.state(target instanceof Map, "Target must be of type Map"); + Map map = (Map) target; + Object value = map.get(name); + if (value == null && !map.containsKey(name)) { + throw new MapAccessException(name); + } + return new TypedValue(value); + } + + @Override + public boolean canWrite(EvaluationContext context, @Nullable Object target, String name) throws AccessException { + return true; + } + + @Override + @SuppressWarnings("unchecked") + public void write(EvaluationContext context, @Nullable Object target, String name, @Nullable Object newValue) + throws AccessException { + + Assert.state(target instanceof Map, "Target must be a Map"); + Map map = (Map) target; + map.put(name, newValue); + } + + @Override + public boolean isCompilable() { + return true; + } + + @Override + public Class getPropertyType() { + return Object.class; + } + + @Override + public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) { + String descriptor = cf.lastDescriptor(); + if (descriptor == null || !descriptor.equals("Ljava/util/Map")) { + if (descriptor == null) { + cf.loadTarget(mv); + } + CodeFlow.insertCheckCast(mv, "Ljava/util/Map"); + } + mv.visitLdcInsn(propertyName); + mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "get","(Ljava/lang/Object;)Ljava/lang/Object;",true); + } + + + /** + * Exception thrown from {@code read} in order to reset a cached + * PropertyAccessor, allowing other accessors to have a try. + */ + @SuppressWarnings("serial") + private static class MapAccessException extends AccessException { + + private final String key; + + public MapAccessException(String key) { + super(""); + this.key = key; + } + + @Override + public String getMessage() { + return "Map does not contain a value for key '" + this.key + "'"; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/expression/MethodBasedEvaluationContext.java b/spring-context/src/main/java/org/springframework/context/expression/MethodBasedEvaluationContext.java new file mode 100644 index 0000000..11d29e3 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/expression/MethodBasedEvaluationContext.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.expression; + +import java.lang.reflect.Method; +import java.util.Arrays; + +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +/** + * A method-based {@link org.springframework.expression.EvaluationContext} that + * provides explicit support for method-based invocations. + * + *

    Expose the actual method arguments using the following aliases: + *

      + *
    1. pX where X is the index of the argument (p0 for the first argument)
    2. + *
    3. aX where X is the index of the argument (a1 for the second argument)
    4. + *
    5. the name of the parameter as discovered by a configurable {@link ParameterNameDiscoverer}
    6. + *
    + * + * @author Stephane Nicoll + * @author Juergen Hoeller + * @since 4.2 + */ +public class MethodBasedEvaluationContext extends StandardEvaluationContext { + + private final Method method; + + private final Object[] arguments; + + private final ParameterNameDiscoverer parameterNameDiscoverer; + + private boolean argumentsLoaded = false; + + + public MethodBasedEvaluationContext(Object rootObject, Method method, Object[] arguments, + ParameterNameDiscoverer parameterNameDiscoverer) { + + super(rootObject); + this.method = method; + this.arguments = arguments; + this.parameterNameDiscoverer = parameterNameDiscoverer; + } + + + @Override + @Nullable + public Object lookupVariable(String name) { + Object variable = super.lookupVariable(name); + if (variable != null) { + return variable; + } + if (!this.argumentsLoaded) { + lazyLoadArguments(); + this.argumentsLoaded = true; + variable = super.lookupVariable(name); + } + return variable; + } + + /** + * Load the param information only when needed. + */ + protected void lazyLoadArguments() { + // Shortcut if no args need to be loaded + if (ObjectUtils.isEmpty(this.arguments)) { + return; + } + + // Expose indexed variables as well as parameter names (if discoverable) + String[] paramNames = this.parameterNameDiscoverer.getParameterNames(this.method); + int paramCount = (paramNames != null ? paramNames.length : this.method.getParameterCount()); + int argsCount = this.arguments.length; + + for (int i = 0; i < paramCount; i++) { + Object value = null; + if (argsCount > paramCount && i == paramCount - 1) { + // Expose remaining arguments as vararg array for last parameter + value = Arrays.copyOfRange(this.arguments, i, argsCount); + } + else if (argsCount > i) { + // Actual argument found - otherwise left as null + value = this.arguments[i]; + } + setVariable("a" + i, value); + setVariable("p" + i, value); + if (paramNames != null && paramNames[i] != null) { + setVariable(paramNames[i], value); + } + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/expression/StandardBeanExpressionResolver.java b/spring-context/src/main/java/org/springframework/context/expression/StandardBeanExpressionResolver.java new file mode 100644 index 0000000..c2a0235 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/expression/StandardBeanExpressionResolver.java @@ -0,0 +1,180 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.expression; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanExpressionException; +import org.springframework.beans.factory.config.BeanExpressionContext; +import org.springframework.beans.factory.config.BeanExpressionResolver; +import org.springframework.core.convert.ConversionService; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.ParserContext; +import org.springframework.expression.spel.SpelParserConfiguration; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.expression.spel.support.StandardTypeConverter; +import org.springframework.expression.spel.support.StandardTypeLocator; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Standard implementation of the + * {@link org.springframework.beans.factory.config.BeanExpressionResolver} + * interface, parsing and evaluating Spring EL using Spring's expression module. + * + *

    All beans in the containing {@code BeanFactory} are made available as + * predefined variables with their common bean name, including standard context + * beans such as "environment", "systemProperties" and "systemEnvironment". + * + * @author Juergen Hoeller + * @since 3.0 + * @see BeanExpressionContext#getBeanFactory() + * @see org.springframework.expression.ExpressionParser + * @see org.springframework.expression.spel.standard.SpelExpressionParser + * @see org.springframework.expression.spel.support.StandardEvaluationContext + */ +public class StandardBeanExpressionResolver implements BeanExpressionResolver { + + /** Default expression prefix: "#{". */ + public static final String DEFAULT_EXPRESSION_PREFIX = "#{"; + + /** Default expression suffix: "}". */ + public static final String DEFAULT_EXPRESSION_SUFFIX = "}"; + + + private String expressionPrefix = DEFAULT_EXPRESSION_PREFIX; + + private String expressionSuffix = DEFAULT_EXPRESSION_SUFFIX; + + private ExpressionParser expressionParser; + + private final Map expressionCache = new ConcurrentHashMap<>(256); + + private final Map evaluationCache = new ConcurrentHashMap<>(8); + + private final ParserContext beanExpressionParserContext = new ParserContext() { + @Override + public boolean isTemplate() { + return true; + } + @Override + public String getExpressionPrefix() { + return expressionPrefix; + } + @Override + public String getExpressionSuffix() { + return expressionSuffix; + } + }; + + + /** + * Create a new {@code StandardBeanExpressionResolver} with default settings. + */ + public StandardBeanExpressionResolver() { + this.expressionParser = new SpelExpressionParser(); + } + + /** + * Create a new {@code StandardBeanExpressionResolver} with the given bean class loader, + * using it as the basis for expression compilation. + * @param beanClassLoader the factory's bean class loader + */ + public StandardBeanExpressionResolver(@Nullable ClassLoader beanClassLoader) { + this.expressionParser = new SpelExpressionParser(new SpelParserConfiguration(null, beanClassLoader)); + } + + + /** + * Set the prefix that an expression string starts with. + * The default is "#{". + * @see #DEFAULT_EXPRESSION_PREFIX + */ + public void setExpressionPrefix(String expressionPrefix) { + Assert.hasText(expressionPrefix, "Expression prefix must not be empty"); + this.expressionPrefix = expressionPrefix; + } + + /** + * Set the suffix that an expression string ends with. + * The default is "}". + * @see #DEFAULT_EXPRESSION_SUFFIX + */ + public void setExpressionSuffix(String expressionSuffix) { + Assert.hasText(expressionSuffix, "Expression suffix must not be empty"); + this.expressionSuffix = expressionSuffix; + } + + /** + * Specify the EL parser to use for expression parsing. + *

    Default is a {@link org.springframework.expression.spel.standard.SpelExpressionParser}, + * compatible with standard Unified EL style expression syntax. + */ + public void setExpressionParser(ExpressionParser expressionParser) { + Assert.notNull(expressionParser, "ExpressionParser must not be null"); + this.expressionParser = expressionParser; + } + + + @Override + @Nullable + public Object evaluate(@Nullable String value, BeanExpressionContext evalContext) throws BeansException { + if (!StringUtils.hasLength(value)) { + return value; + } + try { + Expression expr = this.expressionCache.get(value); + if (expr == null) { + expr = this.expressionParser.parseExpression(value, this.beanExpressionParserContext); + this.expressionCache.put(value, expr); + } + StandardEvaluationContext sec = this.evaluationCache.get(evalContext); + if (sec == null) { + sec = new StandardEvaluationContext(evalContext); + sec.addPropertyAccessor(new BeanExpressionContextAccessor()); + sec.addPropertyAccessor(new BeanFactoryAccessor()); + sec.addPropertyAccessor(new MapAccessor()); + sec.addPropertyAccessor(new EnvironmentAccessor()); + sec.setBeanResolver(new BeanFactoryResolver(evalContext.getBeanFactory())); + sec.setTypeLocator(new StandardTypeLocator(evalContext.getBeanFactory().getBeanClassLoader())); + ConversionService conversionService = evalContext.getBeanFactory().getConversionService(); + if (conversionService != null) { + sec.setTypeConverter(new StandardTypeConverter(conversionService)); + } + customizeEvaluationContext(sec); + this.evaluationCache.put(evalContext, sec); + } + return expr.getValue(sec); + } + catch (Throwable ex) { + throw new BeanExpressionException("Expression parsing failed", ex); + } + } + + /** + * Template method for customizing the expression evaluation context. + *

    The default implementation is empty. + */ + protected void customizeEvaluationContext(StandardEvaluationContext evalContext) { + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/expression/package-info.java b/spring-context/src/main/java/org/springframework/context/expression/package-info.java new file mode 100644 index 0000000..f08c49a --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/expression/package-info.java @@ -0,0 +1,9 @@ +/** + * Expression parsing support within a Spring application context. + */ +@NonNullApi +@NonNullFields +package org.springframework.context.expression; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/context/i18n/LocaleContext.java b/spring-context/src/main/java/org/springframework/context/i18n/LocaleContext.java new file mode 100644 index 0000000..f10305c --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/i18n/LocaleContext.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.i18n; + +import java.util.Locale; + +import org.springframework.lang.Nullable; + +/** + * Strategy interface for determining the current Locale. + * + *

    A LocaleContext instance can be associated with a thread + * via the LocaleContextHolder class. + * + * @author Juergen Hoeller + * @since 1.2 + * @see LocaleContextHolder#getLocale() + * @see TimeZoneAwareLocaleContext + */ +public interface LocaleContext { + + /** + * Return the current Locale, which can be fixed or determined dynamically, + * depending on the implementation strategy. + * @return the current Locale, or {@code null} if no specific Locale associated + */ + @Nullable + Locale getLocale(); + +} diff --git a/spring-context/src/main/java/org/springframework/context/i18n/LocaleContextHolder.java b/spring-context/src/main/java/org/springframework/context/i18n/LocaleContextHolder.java new file mode 100644 index 0000000..ee0b7cb --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/i18n/LocaleContextHolder.java @@ -0,0 +1,336 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.i18n; + +import java.util.Locale; +import java.util.TimeZone; + +import org.springframework.core.NamedInheritableThreadLocal; +import org.springframework.core.NamedThreadLocal; +import org.springframework.lang.Nullable; + +/** + * Simple holder class that associates a LocaleContext instance + * with the current thread. The LocaleContext will be inherited + * by any child threads spawned by the current thread if the + * {@code inheritable} flag is set to {@code true}. + * + *

    Used as a central holder for the current Locale in Spring, + * wherever necessary: for example, in MessageSourceAccessor. + * DispatcherServlet automatically exposes its current Locale here. + * Other applications can expose theirs too, to make classes like + * MessageSourceAccessor automatically use that Locale. + * + * @author Juergen Hoeller + * @author Nicholas Williams + * @since 1.2 + * @see LocaleContext + * @see org.springframework.context.support.MessageSourceAccessor + * @see org.springframework.web.servlet.DispatcherServlet + */ +public final class LocaleContextHolder { + + private static final ThreadLocal localeContextHolder = + new NamedThreadLocal<>("LocaleContext"); + + private static final ThreadLocal inheritableLocaleContextHolder = + new NamedInheritableThreadLocal<>("LocaleContext"); + + // Shared default locale at the framework level + @Nullable + private static Locale defaultLocale; + + // Shared default time zone at the framework level + @Nullable + private static TimeZone defaultTimeZone; + + + private LocaleContextHolder() { + } + + + /** + * Reset the LocaleContext for the current thread. + */ + public static void resetLocaleContext() { + localeContextHolder.remove(); + inheritableLocaleContextHolder.remove(); + } + + /** + * Associate the given LocaleContext with the current thread, + * not exposing it as inheritable for child threads. + *

    The given LocaleContext may be a {@link TimeZoneAwareLocaleContext}, + * containing a locale with associated time zone information. + * @param localeContext the current LocaleContext, + * or {@code null} to reset the thread-bound context + * @see SimpleLocaleContext + * @see SimpleTimeZoneAwareLocaleContext + */ + public static void setLocaleContext(@Nullable LocaleContext localeContext) { + setLocaleContext(localeContext, false); + } + + /** + * Associate the given LocaleContext with the current thread. + *

    The given LocaleContext may be a {@link TimeZoneAwareLocaleContext}, + * containing a locale with associated time zone information. + * @param localeContext the current LocaleContext, + * or {@code null} to reset the thread-bound context + * @param inheritable whether to expose the LocaleContext as inheritable + * for child threads (using an {@link InheritableThreadLocal}) + * @see SimpleLocaleContext + * @see SimpleTimeZoneAwareLocaleContext + */ + public static void setLocaleContext(@Nullable LocaleContext localeContext, boolean inheritable) { + if (localeContext == null) { + resetLocaleContext(); + } + else { + if (inheritable) { + inheritableLocaleContextHolder.set(localeContext); + localeContextHolder.remove(); + } + else { + localeContextHolder.set(localeContext); + inheritableLocaleContextHolder.remove(); + } + } + } + + /** + * Return the LocaleContext associated with the current thread, if any. + * @return the current LocaleContext, or {@code null} if none + */ + @Nullable + public static LocaleContext getLocaleContext() { + LocaleContext localeContext = localeContextHolder.get(); + if (localeContext == null) { + localeContext = inheritableLocaleContextHolder.get(); + } + return localeContext; + } + + /** + * Associate the given Locale with the current thread, + * preserving any TimeZone that may have been set already. + *

    Will implicitly create a LocaleContext for the given Locale, + * not exposing it as inheritable for child threads. + * @param locale the current Locale, or {@code null} to reset + * the locale part of thread-bound context + * @see #setTimeZone(TimeZone) + * @see SimpleLocaleContext#SimpleLocaleContext(Locale) + */ + public static void setLocale(@Nullable Locale locale) { + setLocale(locale, false); + } + + /** + * Associate the given Locale with the current thread, + * preserving any TimeZone that may have been set already. + *

    Will implicitly create a LocaleContext for the given Locale. + * @param locale the current Locale, or {@code null} to reset + * the locale part of thread-bound context + * @param inheritable whether to expose the LocaleContext as inheritable + * for child threads (using an {@link InheritableThreadLocal}) + * @see #setTimeZone(TimeZone, boolean) + * @see SimpleLocaleContext#SimpleLocaleContext(Locale) + */ + public static void setLocale(@Nullable Locale locale, boolean inheritable) { + LocaleContext localeContext = getLocaleContext(); + TimeZone timeZone = (localeContext instanceof TimeZoneAwareLocaleContext ? + ((TimeZoneAwareLocaleContext) localeContext).getTimeZone() : null); + if (timeZone != null) { + localeContext = new SimpleTimeZoneAwareLocaleContext(locale, timeZone); + } + else if (locale != null) { + localeContext = new SimpleLocaleContext(locale); + } + else { + localeContext = null; + } + setLocaleContext(localeContext, inheritable); + } + + /** + * Set a shared default locale at the framework level, + * as an alternative to the JVM-wide default locale. + *

    NOTE: This can be useful to set an application-level + * default locale which differs from the JVM-wide default locale. + * However, this requires each such application to operate against + * locally deployed Spring Framework jars. Do not deploy Spring + * as a shared library at the server level in such a scenario! + * @param locale the default locale (or {@code null} for none, + * letting lookups fall back to {@link Locale#getDefault()}) + * @since 4.3.5 + * @see #getLocale() + * @see Locale#getDefault() + */ + public static void setDefaultLocale(@Nullable Locale locale) { + LocaleContextHolder.defaultLocale = locale; + } + + /** + * Return the Locale associated with the current thread, if any, + * or the system default Locale otherwise. This is effectively a + * replacement for {@link java.util.Locale#getDefault()}, + * able to optionally respect a user-level Locale setting. + *

    Note: This method has a fallback to the shared default Locale, + * either at the framework level or at the JVM-wide system level. + * If you'd like to check for the raw LocaleContext content + * (which may indicate no specific locale through {@code null}, use + * {@link #getLocaleContext()} and call {@link LocaleContext#getLocale()} + * @return the current Locale, or the system default Locale if no + * specific Locale has been associated with the current thread + * @see #getLocaleContext() + * @see LocaleContext#getLocale() + * @see #setDefaultLocale(Locale) + * @see java.util.Locale#getDefault() + */ + public static Locale getLocale() { + return getLocale(getLocaleContext()); + } + + /** + * Return the Locale associated with the given user context, if any, + * or the system default Locale otherwise. This is effectively a + * replacement for {@link java.util.Locale#getDefault()}, + * able to optionally respect a user-level Locale setting. + * @param localeContext the user-level locale context to check + * @return the current Locale, or the system default Locale if no + * specific Locale has been associated with the current thread + * @since 5.0 + * @see #getLocale() + * @see LocaleContext#getLocale() + * @see #setDefaultLocale(Locale) + * @see java.util.Locale#getDefault() + */ + public static Locale getLocale(@Nullable LocaleContext localeContext) { + if (localeContext != null) { + Locale locale = localeContext.getLocale(); + if (locale != null) { + return locale; + } + } + return (defaultLocale != null ? defaultLocale : Locale.getDefault()); + } + + /** + * Associate the given TimeZone with the current thread, + * preserving any Locale that may have been set already. + *

    Will implicitly create a LocaleContext for the given Locale, + * not exposing it as inheritable for child threads. + * @param timeZone the current TimeZone, or {@code null} to reset + * the time zone part of the thread-bound context + * @see #setLocale(Locale) + * @see SimpleTimeZoneAwareLocaleContext#SimpleTimeZoneAwareLocaleContext(Locale, TimeZone) + */ + public static void setTimeZone(@Nullable TimeZone timeZone) { + setTimeZone(timeZone, false); + } + + /** + * Associate the given TimeZone with the current thread, + * preserving any Locale that may have been set already. + *

    Will implicitly create a LocaleContext for the given Locale. + * @param timeZone the current TimeZone, or {@code null} to reset + * the time zone part of the thread-bound context + * @param inheritable whether to expose the LocaleContext as inheritable + * for child threads (using an {@link InheritableThreadLocal}) + * @see #setLocale(Locale, boolean) + * @see SimpleTimeZoneAwareLocaleContext#SimpleTimeZoneAwareLocaleContext(Locale, TimeZone) + */ + public static void setTimeZone(@Nullable TimeZone timeZone, boolean inheritable) { + LocaleContext localeContext = getLocaleContext(); + Locale locale = (localeContext != null ? localeContext.getLocale() : null); + if (timeZone != null) { + localeContext = new SimpleTimeZoneAwareLocaleContext(locale, timeZone); + } + else if (locale != null) { + localeContext = new SimpleLocaleContext(locale); + } + else { + localeContext = null; + } + setLocaleContext(localeContext, inheritable); + } + + /** + * Set a shared default time zone at the framework level, + * as an alternative to the JVM-wide default time zone. + *

    NOTE: This can be useful to set an application-level + * default time zone which differs from the JVM-wide default time zone. + * However, this requires each such application to operate against + * locally deployed Spring Framework jars. Do not deploy Spring + * as a shared library at the server level in such a scenario! + * @param timeZone the default time zone (or {@code null} for none, + * letting lookups fall back to {@link TimeZone#getDefault()}) + * @since 4.3.5 + * @see #getTimeZone() + * @see TimeZone#getDefault() + */ + public static void setDefaultTimeZone(@Nullable TimeZone timeZone) { + defaultTimeZone = timeZone; + } + + /** + * Return the TimeZone associated with the current thread, if any, + * or the system default TimeZone otherwise. This is effectively a + * replacement for {@link java.util.TimeZone#getDefault()}, + * able to optionally respect a user-level TimeZone setting. + *

    Note: This method has a fallback to the shared default TimeZone, + * either at the framework level or at the JVM-wide system level. + * If you'd like to check for the raw LocaleContext content + * (which may indicate no specific time zone through {@code null}, use + * {@link #getLocaleContext()} and call {@link TimeZoneAwareLocaleContext#getTimeZone()} + * after downcasting to {@link TimeZoneAwareLocaleContext}. + * @return the current TimeZone, or the system default TimeZone if no + * specific TimeZone has been associated with the current thread + * @see #getLocaleContext() + * @see TimeZoneAwareLocaleContext#getTimeZone() + * @see #setDefaultTimeZone(TimeZone) + * @see java.util.TimeZone#getDefault() + */ + public static TimeZone getTimeZone() { + return getTimeZone(getLocaleContext()); + } + + /** + * Return the TimeZone associated with the given user context, if any, + * or the system default TimeZone otherwise. This is effectively a + * replacement for {@link java.util.TimeZone#getDefault()}, + * able to optionally respect a user-level TimeZone setting. + * @param localeContext the user-level locale context to check + * @return the current TimeZone, or the system default TimeZone if no + * specific TimeZone has been associated with the current thread + * @since 5.0 + * @see #getTimeZone() + * @see TimeZoneAwareLocaleContext#getTimeZone() + * @see #setDefaultTimeZone(TimeZone) + * @see java.util.TimeZone#getDefault() + */ + public static TimeZone getTimeZone(@Nullable LocaleContext localeContext) { + if (localeContext instanceof TimeZoneAwareLocaleContext) { + TimeZone timeZone = ((TimeZoneAwareLocaleContext) localeContext).getTimeZone(); + if (timeZone != null) { + return timeZone; + } + } + return (defaultTimeZone != null ? defaultTimeZone : TimeZone.getDefault()); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/i18n/SimpleLocaleContext.java b/spring-context/src/main/java/org/springframework/context/i18n/SimpleLocaleContext.java new file mode 100644 index 0000000..aaec779 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/i18n/SimpleLocaleContext.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.i18n; + +import java.util.Locale; + +import org.springframework.lang.Nullable; + +/** + * Simple implementation of the {@link LocaleContext} interface, + * always returning a specified {@code Locale}. + * + * @author Juergen Hoeller + * @since 1.2 + * @see LocaleContextHolder#setLocaleContext + * @see LocaleContextHolder#getLocale() + * @see SimpleTimeZoneAwareLocaleContext + */ +public class SimpleLocaleContext implements LocaleContext { + + @Nullable + private final Locale locale; + + + /** + * Create a new SimpleLocaleContext that exposes the specified Locale. + * Every {@link #getLocale()} call will return this Locale. + * @param locale the Locale to expose, or {@code null} for no specific one + */ + public SimpleLocaleContext(@Nullable Locale locale) { + this.locale = locale; + } + + @Override + @Nullable + public Locale getLocale() { + return this.locale; + } + + @Override + public String toString() { + return (this.locale != null ? this.locale.toString() : "-"); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/i18n/SimpleTimeZoneAwareLocaleContext.java b/spring-context/src/main/java/org/springframework/context/i18n/SimpleTimeZoneAwareLocaleContext.java new file mode 100644 index 0000000..0ce008d --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/i18n/SimpleTimeZoneAwareLocaleContext.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.i18n; + +import java.util.Locale; +import java.util.TimeZone; + +import org.springframework.lang.Nullable; + +/** + * Simple implementation of the {@link TimeZoneAwareLocaleContext} interface, + * always returning a specified {@code Locale} and {@code TimeZone}. + * + *

    Note: Prefer the use of {@link SimpleLocaleContext} when only setting + * a Locale but no TimeZone. + * + * @author Juergen Hoeller + * @author Nicholas Williams + * @since 4.0 + * @see LocaleContextHolder#setLocaleContext + * @see LocaleContextHolder#getTimeZone() + */ +public class SimpleTimeZoneAwareLocaleContext extends SimpleLocaleContext implements TimeZoneAwareLocaleContext { + + @Nullable + private final TimeZone timeZone; + + + /** + * Create a new SimpleTimeZoneAwareLocaleContext that exposes the specified + * Locale and TimeZone. Every {@link #getLocale()} call will return the given + * Locale, and every {@link #getTimeZone()} call will return the given TimeZone. + * @param locale the Locale to expose + * @param timeZone the TimeZone to expose + */ + public SimpleTimeZoneAwareLocaleContext(@Nullable Locale locale, @Nullable TimeZone timeZone) { + super(locale); + this.timeZone = timeZone; + } + + + @Override + @Nullable + public TimeZone getTimeZone() { + return this.timeZone; + } + + @Override + public String toString() { + return super.toString() + " " + (this.timeZone != null ? this.timeZone.toString() : "-"); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/i18n/TimeZoneAwareLocaleContext.java b/spring-context/src/main/java/org/springframework/context/i18n/TimeZoneAwareLocaleContext.java new file mode 100644 index 0000000..ab93b39 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/i18n/TimeZoneAwareLocaleContext.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.i18n; + +import java.util.TimeZone; + +import org.springframework.lang.Nullable; + +/** + * Extension of {@link LocaleContext}, adding awareness of the current time zone. + * + *

    Having this variant of LocaleContext set to {@link LocaleContextHolder} means + * that some TimeZone-aware infrastructure has been configured, even if it may not + * be able to produce a non-null TimeZone at the moment. + * + * @author Juergen Hoeller + * @author Nicholas Williams + * @since 4.0 + * @see LocaleContextHolder#getTimeZone() + */ +public interface TimeZoneAwareLocaleContext extends LocaleContext { + + /** + * Return the current TimeZone, which can be fixed or determined dynamically, + * depending on the implementation strategy. + * @return the current TimeZone, or {@code null} if no specific TimeZone associated + */ + @Nullable + TimeZone getTimeZone(); + +} diff --git a/spring-context/src/main/java/org/springframework/context/i18n/package-info.java b/spring-context/src/main/java/org/springframework/context/i18n/package-info.java new file mode 100644 index 0000000..d7eba0b --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/i18n/package-info.java @@ -0,0 +1,10 @@ +/** + * Abstraction for determining the current Locale, + * plus global holder that exposes a thread-bound Locale. + */ +@NonNullApi +@NonNullFields +package org.springframework.context.i18n; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndex.java b/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndex.java new file mode 100644 index 0000000..5d9c0be --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndex.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index; + +import java.util.Collections; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.util.AntPathMatcher; +import org.springframework.util.ClassUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * Provide access to the candidates that are defined in {@code META-INF/spring.components}. + * + *

    An arbitrary number of stereotypes can be registered (and queried) on the index: a + * typical example is the fully qualified name of an annotation that flags the class for + * a certain use case. The following call returns all the {@code @Component} + * candidate types for the {@code com.example} package (and its sub-packages): + *

    + * Set<String> candidates = index.getCandidateTypes(
    + *         "com.example", "org.springframework.stereotype.Component");
    + * 
    + * + *

    The {@code type} is usually the fully qualified name of a class, though this is + * not a rule. Similarly, the {@code stereotype} is usually the fully qualified name of + * a target type but it can be any marker really. + * + * @author Stephane Nicoll + * @since 5.0 + */ +public class CandidateComponentsIndex { + + private static final AntPathMatcher pathMatcher = new AntPathMatcher("."); + + private final MultiValueMap index; + + + CandidateComponentsIndex(List content) { + this.index = parseIndex(content); + } + + private static MultiValueMap parseIndex(List content) { + MultiValueMap index = new LinkedMultiValueMap<>(); + for (Properties entry : content) { + entry.forEach((type, values) -> { + String[] stereotypes = ((String) values).split(","); + for (String stereotype : stereotypes) { + index.add(stereotype, new Entry((String) type)); + } + }); + } + return index; + } + + + /** + * Return the candidate types that are associated with the specified stereotype. + * @param basePackage the package to check for candidates + * @param stereotype the stereotype to use + * @return the candidate types associated with the specified {@code stereotype} + * or an empty set if none has been found for the specified {@code basePackage} + */ + public Set getCandidateTypes(String basePackage, String stereotype) { + List candidates = this.index.get(stereotype); + if (candidates != null) { + return candidates.parallelStream() + .filter(t -> t.match(basePackage)) + .map(t -> t.type) + .collect(Collectors.toSet()); + } + return Collections.emptySet(); + } + + + private static class Entry { + + private final String type; + + private final String packageName; + + Entry(String type) { + this.type = type; + this.packageName = ClassUtils.getPackageName(type); + } + + public boolean match(String basePackage) { + if (pathMatcher.isPattern(basePackage)) { + return pathMatcher.match(basePackage, this.packageName); + } + else { + return this.type.startsWith(basePackage); + } + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndexLoader.java b/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndexLoader.java new file mode 100644 index 0000000..fcca537 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndexLoader.java @@ -0,0 +1,121 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.ConcurrentMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.SpringProperties; +import org.springframework.core.io.UrlResource; +import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.ConcurrentReferenceHashMap; + +/** + * Candidate components index loading mechanism for internal use within the framework. + * + * @author Stephane Nicoll + * @since 5.0 + */ +public final class CandidateComponentsIndexLoader { + + /** + * The location to look for components. + *

    Can be present in multiple JAR files. + */ + public static final String COMPONENTS_RESOURCE_LOCATION = "META-INF/spring.components"; + + /** + * System property that instructs Spring to ignore the index, i.e. + * to always return {@code null} from {@link #loadIndex(ClassLoader)}. + *

    The default is "false", allowing for regular use of the index. Switching this + * flag to {@code true} fulfills a corner case scenario when an index is partially + * available for some libraries (or use cases) but couldn't be built for the whole + * application. In this case, the application context fallbacks to a regular + * classpath arrangement (i.e. as no index was present at all). + */ + public static final String IGNORE_INDEX = "spring.index.ignore"; + + + private static final boolean shouldIgnoreIndex = SpringProperties.getFlag(IGNORE_INDEX); + + private static final Log logger = LogFactory.getLog(CandidateComponentsIndexLoader.class); + + private static final ConcurrentMap cache = + new ConcurrentReferenceHashMap<>(); + + + private CandidateComponentsIndexLoader() { + } + + + /** + * Load and instantiate the {@link CandidateComponentsIndex} from + * {@value #COMPONENTS_RESOURCE_LOCATION}, using the given class loader. If no + * index is available, return {@code null}. + * @param classLoader the ClassLoader to use for loading (can be {@code null} to use the default) + * @return the index to use or {@code null} if no index was found + * @throws IllegalArgumentException if any module index cannot + * be loaded or if an error occurs while creating {@link CandidateComponentsIndex} + */ + @Nullable + public static CandidateComponentsIndex loadIndex(@Nullable ClassLoader classLoader) { + ClassLoader classLoaderToUse = classLoader; + if (classLoaderToUse == null) { + classLoaderToUse = CandidateComponentsIndexLoader.class.getClassLoader(); + } + return cache.computeIfAbsent(classLoaderToUse, CandidateComponentsIndexLoader::doLoadIndex); + } + + @Nullable + private static CandidateComponentsIndex doLoadIndex(ClassLoader classLoader) { + if (shouldIgnoreIndex) { + return null; + } + + try { + Enumeration urls = classLoader.getResources(COMPONENTS_RESOURCE_LOCATION); + if (!urls.hasMoreElements()) { + return null; + } + List result = new ArrayList<>(); + while (urls.hasMoreElements()) { + URL url = urls.nextElement(); + Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url)); + result.add(properties); + } + if (logger.isDebugEnabled()) { + logger.debug("Loaded " + result.size() + "] index(es)"); + } + int totalCount = result.stream().mapToInt(Properties::size).sum(); + return (totalCount > 0 ? new CandidateComponentsIndex(result) : null); + } + catch (IOException ex) { + throw new IllegalStateException("Unable to load indexes from location [" + + COMPONENTS_RESOURCE_LOCATION + "]", ex); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/index/package-info.java b/spring-context/src/main/java/org/springframework/context/index/package-info.java new file mode 100644 index 0000000..e07328e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/index/package-info.java @@ -0,0 +1,9 @@ +/** + * Support package for reading and managing the components index. + */ +@NonNullApi +@NonNullFields +package org.springframework.context.index; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/context/package-info.java b/spring-context/src/main/java/org/springframework/context/package-info.java new file mode 100644 index 0000000..9aae0c2 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/package-info.java @@ -0,0 +1,18 @@ +/** + * This package builds on the beans package to add support for + * message sources and for the Observer design pattern, and the + * ability for application objects to obtain resources using a + * consistent API. + * + *

    There is no necessity for Spring applications to depend + * on ApplicationContext or even BeanFactory functionality + * explicitly. One of the strengths of the Spring architecture + * is that application objects can often be configured without + * any dependency on Spring-specific APIs. + */ +@NonNullApi +@NonNullFields +package org.springframework.context; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java new file mode 100644 index 0000000..e87edab --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java @@ -0,0 +1,1496 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeansException; +import org.springframework.beans.CachedIntrospectionResults; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.support.ResourceEditorRegistrar; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.EmbeddedValueResolverAware; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.HierarchicalMessageSource; +import org.springframework.context.LifecycleProcessor; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceAware; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.NoSuchMessageException; +import org.springframework.context.PayloadApplicationEvent; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.context.event.ApplicationEventMulticaster; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.ContextStartedEvent; +import org.springframework.context.event.ContextStoppedEvent; +import org.springframework.context.event.SimpleApplicationEventMulticaster; +import org.springframework.context.expression.StandardBeanExpressionResolver; +import org.springframework.context.weaving.LoadTimeWeaverAware; +import org.springframework.context.weaving.LoadTimeWeaverAwareProcessor; +import org.springframework.core.ResolvableType; +import org.springframework.core.SpringProperties; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.core.metrics.ApplicationStartup; +import org.springframework.core.metrics.StartupStep; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; + +/** + * Abstract implementation of the {@link org.springframework.context.ApplicationContext} + * interface. Doesn't mandate the type of storage used for configuration; simply + * implements common context functionality. Uses the Template Method design pattern, + * requiring concrete subclasses to implement abstract methods. + * + *

    In contrast to a plain BeanFactory, an ApplicationContext is supposed + * to detect special beans defined in its internal bean factory: + * Therefore, this class automatically registers + * {@link org.springframework.beans.factory.config.BeanFactoryPostProcessor BeanFactoryPostProcessors}, + * {@link org.springframework.beans.factory.config.BeanPostProcessor BeanPostProcessors}, + * and {@link org.springframework.context.ApplicationListener ApplicationListeners} + * which are defined as beans in the context. + * + *

    A {@link org.springframework.context.MessageSource} may also be supplied + * as a bean in the context, with the name "messageSource"; otherwise, message + * resolution is delegated to the parent context. Furthermore, a multicaster + * for application events can be supplied as an "applicationEventMulticaster" bean + * of type {@link org.springframework.context.event.ApplicationEventMulticaster} + * in the context; otherwise, a default multicaster of type + * {@link org.springframework.context.event.SimpleApplicationEventMulticaster} will be used. + * + *

    Implements resource loading by extending + * {@link org.springframework.core.io.DefaultResourceLoader}. + * Consequently treats non-URL resource paths as class path resources + * (supporting full class path resource names that include the package path, + * e.g. "mypackage/myresource.dat"), unless the {@link #getResourceByPath} + * method is overridden in a subclass. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Mark Fisher + * @author Stephane Nicoll + * @author Sam Brannen + * @author Sebastien Deleuze + * @author Brian Clozel + * @since January 21, 2001 + * @see #refreshBeanFactory + * @see #getBeanFactory + * @see org.springframework.beans.factory.config.BeanFactoryPostProcessor + * @see org.springframework.beans.factory.config.BeanPostProcessor + * @see org.springframework.context.event.ApplicationEventMulticaster + * @see org.springframework.context.ApplicationListener + * @see org.springframework.context.MessageSource + */ +public abstract class AbstractApplicationContext extends DefaultResourceLoader + implements ConfigurableApplicationContext { + + /** + * Name of the MessageSource bean in the factory. + * If none is supplied, message resolution is delegated to the parent. + * @see MessageSource + */ + public static final String MESSAGE_SOURCE_BEAN_NAME = "messageSource"; + + /** + * Name of the LifecycleProcessor bean in the factory. + * If none is supplied, a DefaultLifecycleProcessor is used. + * @see org.springframework.context.LifecycleProcessor + * @see org.springframework.context.support.DefaultLifecycleProcessor + */ + public static final String LIFECYCLE_PROCESSOR_BEAN_NAME = "lifecycleProcessor"; + + /** + * Name of the ApplicationEventMulticaster bean in the factory. + * If none is supplied, a default SimpleApplicationEventMulticaster is used. + * @see org.springframework.context.event.ApplicationEventMulticaster + * @see org.springframework.context.event.SimpleApplicationEventMulticaster + */ + public static final String APPLICATION_EVENT_MULTICASTER_BEAN_NAME = "applicationEventMulticaster"; + + /** + * Boolean flag controlled by a {@code spring.spel.ignore} system property that instructs Spring to + * ignore SpEL, i.e. to not initialize the SpEL infrastructure. + *

    The default is "false". + */ + private static final boolean shouldIgnoreSpel = SpringProperties.getFlag("spring.spel.ignore"); + + /** + * Whether this environment lives within a native image. + * Exposed as a private static field rather than in a {@code NativeImageDetector.inNativeImage()} static method due to https://github.com/oracle/graal/issues/2594. + * @see ImageInfo.java + */ + private static final boolean IN_NATIVE_IMAGE = (System.getProperty("org.graalvm.nativeimage.imagecode") != null); + + + static { + // Eagerly load the ContextClosedEvent class to avoid weird classloader issues + // on application shutdown in WebLogic 8.1. (Reported by Dustin Woods.) + ContextClosedEvent.class.getName(); + } + + + /** Logger used by this class. Available to subclasses. */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** Unique id for this context, if any. */ + private String id = ObjectUtils.identityToString(this); + + /** Display name. */ + private String displayName = ObjectUtils.identityToString(this); + + /** Parent context. */ + @Nullable + private ApplicationContext parent; + + /** Environment used by this context. */ + @Nullable + private ConfigurableEnvironment environment; + + /** BeanFactoryPostProcessors to apply on refresh. */ + private final List beanFactoryPostProcessors = new ArrayList<>(); + + /** System time in milliseconds when this context started. */ + private long startupDate; + + /** Flag that indicates whether this context is currently active. */ + private final AtomicBoolean active = new AtomicBoolean(); + + /** Flag that indicates whether this context has been closed already. */ + private final AtomicBoolean closed = new AtomicBoolean(); + + /** Synchronization monitor for the "refresh" and "destroy". */ + private final Object startupShutdownMonitor = new Object(); + + /** Reference to the JVM shutdown hook, if registered. */ + @Nullable + private Thread shutdownHook; + + /** ResourcePatternResolver used by this context. */ + private ResourcePatternResolver resourcePatternResolver; + + /** LifecycleProcessor for managing the lifecycle of beans within this context. */ + @Nullable + private LifecycleProcessor lifecycleProcessor; + + /** MessageSource we delegate our implementation of this interface to. */ + @Nullable + private MessageSource messageSource; + + /** Helper class used in event publishing. */ + @Nullable + private ApplicationEventMulticaster applicationEventMulticaster; + + /** Application startup metrics. **/ + private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT; + + /** Statically specified listeners. */ + private final Set> applicationListeners = new LinkedHashSet<>(); + + /** Local listeners registered before refresh. */ + @Nullable + private Set> earlyApplicationListeners; + + /** ApplicationEvents published before the multicaster setup. */ + @Nullable + private Set earlyApplicationEvents; + + + /** + * Create a new AbstractApplicationContext with no parent. + */ + public AbstractApplicationContext() { + this.resourcePatternResolver = getResourcePatternResolver(); + } + + /** + * Create a new AbstractApplicationContext with the given parent context. + * @param parent the parent context + */ + public AbstractApplicationContext(@Nullable ApplicationContext parent) { + this(); + setParent(parent); + } + + + //--------------------------------------------------------------------- + // Implementation of ApplicationContext interface + //--------------------------------------------------------------------- + + /** + * Set the unique id of this application context. + *

    Default is the object id of the context instance, or the name + * of the context bean if the context is itself defined as a bean. + * @param id the unique id of the context + */ + @Override + public void setId(String id) { + this.id = id; + } + + @Override + public String getId() { + return this.id; + } + + @Override + public String getApplicationName() { + return ""; + } + + /** + * Set a friendly name for this context. + * Typically done during initialization of concrete context implementations. + *

    Default is the object id of the context instance. + */ + public void setDisplayName(String displayName) { + Assert.hasLength(displayName, "Display name must not be empty"); + this.displayName = displayName; + } + + /** + * Return a friendly name for this context. + * @return a display name for this context (never {@code null}) + */ + @Override + public String getDisplayName() { + return this.displayName; + } + + /** + * Return the parent context, or {@code null} if there is no parent + * (that is, this context is the root of the context hierarchy). + */ + @Override + @Nullable + public ApplicationContext getParent() { + return this.parent; + } + + /** + * Set the {@code Environment} for this application context. + *

    Default value is determined by {@link #createEnvironment()}. Replacing the + * default with this method is one option but configuration through {@link + * #getEnvironment()} should also be considered. In either case, such modifications + * should be performed before {@link #refresh()}. + * @see org.springframework.context.support.AbstractApplicationContext#createEnvironment + */ + @Override + public void setEnvironment(ConfigurableEnvironment environment) { + this.environment = environment; + } + + /** + * Return the {@code Environment} for this application context in configurable + * form, allowing for further customization. + *

    If none specified, a default environment will be initialized via + * {@link #createEnvironment()}. + */ + @Override + public ConfigurableEnvironment getEnvironment() { + if (this.environment == null) { + this.environment = createEnvironment(); + } + return this.environment; + } + + /** + * Create and return a new {@link StandardEnvironment}. + *

    Subclasses may override this method in order to supply + * a custom {@link ConfigurableEnvironment} implementation. + */ + protected ConfigurableEnvironment createEnvironment() { + return new StandardEnvironment(); + } + + /** + * Return this context's internal bean factory as AutowireCapableBeanFactory, + * if already available. + * @see #getBeanFactory() + */ + @Override + public AutowireCapableBeanFactory getAutowireCapableBeanFactory() throws IllegalStateException { + return getBeanFactory(); + } + + /** + * Return the timestamp (ms) when this context was first loaded. + */ + @Override + public long getStartupDate() { + return this.startupDate; + } + + /** + * Publish the given event to all listeners. + *

    Note: Listeners get initialized after the MessageSource, to be able + * to access it within listener implementations. Thus, MessageSource + * implementations cannot publish events. + * @param event the event to publish (may be application-specific or a + * standard framework event) + */ + @Override + public void publishEvent(ApplicationEvent event) { + publishEvent(event, null); + } + + /** + * Publish the given event to all listeners. + *

    Note: Listeners get initialized after the MessageSource, to be able + * to access it within listener implementations. Thus, MessageSource + * implementations cannot publish events. + * @param event the event to publish (may be an {@link ApplicationEvent} + * or a payload object to be turned into a {@link PayloadApplicationEvent}) + */ + @Override + public void publishEvent(Object event) { + publishEvent(event, null); + } + + /** + * Publish the given event to all listeners. + * @param event the event to publish (may be an {@link ApplicationEvent} + * or a payload object to be turned into a {@link PayloadApplicationEvent}) + * @param eventType the resolved event type, if known + * @since 4.2 + */ + protected void publishEvent(Object event, @Nullable ResolvableType eventType) { + Assert.notNull(event, "Event must not be null"); + + // Decorate event as an ApplicationEvent if necessary + ApplicationEvent applicationEvent; + if (event instanceof ApplicationEvent) { + applicationEvent = (ApplicationEvent) event; + } + else { + applicationEvent = new PayloadApplicationEvent<>(this, event); + if (eventType == null) { + eventType = ((PayloadApplicationEvent) applicationEvent).getResolvableType(); + } + } + + // Multicast right now if possible - or lazily once the multicaster is initialized + if (this.earlyApplicationEvents != null) { + this.earlyApplicationEvents.add(applicationEvent); + } + else { + getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType); + } + + // Publish event via parent context as well... + if (this.parent != null) { + if (this.parent instanceof AbstractApplicationContext) { + ((AbstractApplicationContext) this.parent).publishEvent(event, eventType); + } + else { + this.parent.publishEvent(event); + } + } + } + + /** + * Return the internal ApplicationEventMulticaster used by the context. + * @return the internal ApplicationEventMulticaster (never {@code null}) + * @throws IllegalStateException if the context has not been initialized yet + */ + ApplicationEventMulticaster getApplicationEventMulticaster() throws IllegalStateException { + if (this.applicationEventMulticaster == null) { + throw new IllegalStateException("ApplicationEventMulticaster not initialized - " + + "call 'refresh' before multicasting events via the context: " + this); + } + return this.applicationEventMulticaster; + } + + @Override + public void setApplicationStartup(ApplicationStartup applicationStartup) { + Assert.notNull(applicationStartup, "applicationStartup should not be null"); + this.applicationStartup = applicationStartup; + } + + @Override + public ApplicationStartup getApplicationStartup() { + return this.applicationStartup; + } + + /** + * Return the internal LifecycleProcessor used by the context. + * @return the internal LifecycleProcessor (never {@code null}) + * @throws IllegalStateException if the context has not been initialized yet + */ + LifecycleProcessor getLifecycleProcessor() throws IllegalStateException { + if (this.lifecycleProcessor == null) { + throw new IllegalStateException("LifecycleProcessor not initialized - " + + "call 'refresh' before invoking lifecycle methods via the context: " + this); + } + return this.lifecycleProcessor; + } + + /** + * Return the ResourcePatternResolver to use for resolving location patterns + * into Resource instances. Default is a + * {@link org.springframework.core.io.support.PathMatchingResourcePatternResolver}, + * supporting Ant-style location patterns. + *

    Can be overridden in subclasses, for extended resolution strategies, + * for example in a web environment. + *

    Do not call this when needing to resolve a location pattern. + * Call the context's {@code getResources} method instead, which + * will delegate to the ResourcePatternResolver. + * @return the ResourcePatternResolver for this context + * @see #getResources + * @see org.springframework.core.io.support.PathMatchingResourcePatternResolver + */ + protected ResourcePatternResolver getResourcePatternResolver() { + return new PathMatchingResourcePatternResolver(this); + } + + + //--------------------------------------------------------------------- + // Implementation of ConfigurableApplicationContext interface + //--------------------------------------------------------------------- + + /** + * Set the parent of this application context. + *

    The parent {@linkplain ApplicationContext#getEnvironment() environment} is + * {@linkplain ConfigurableEnvironment#merge(ConfigurableEnvironment) merged} with + * this (child) application context environment if the parent is non-{@code null} and + * its environment is an instance of {@link ConfigurableEnvironment}. + * @see ConfigurableEnvironment#merge(ConfigurableEnvironment) + */ + @Override + public void setParent(@Nullable ApplicationContext parent) { + this.parent = parent; + if (parent != null) { + Environment parentEnvironment = parent.getEnvironment(); + if (parentEnvironment instanceof ConfigurableEnvironment) { + getEnvironment().merge((ConfigurableEnvironment) parentEnvironment); + } + } + } + + @Override + public void addBeanFactoryPostProcessor(BeanFactoryPostProcessor postProcessor) { + Assert.notNull(postProcessor, "BeanFactoryPostProcessor must not be null"); + this.beanFactoryPostProcessors.add(postProcessor); + } + + /** + * Return the list of BeanFactoryPostProcessors that will get applied + * to the internal BeanFactory. + */ + public List getBeanFactoryPostProcessors() { + return this.beanFactoryPostProcessors; + } + + @Override + public void addApplicationListener(ApplicationListener listener) { + Assert.notNull(listener, "ApplicationListener must not be null"); + if (this.applicationEventMulticaster != null) { + this.applicationEventMulticaster.addApplicationListener(listener); + } + this.applicationListeners.add(listener); + } + + /** + * Return the list of statically specified ApplicationListeners. + */ + public Collection> getApplicationListeners() { + return this.applicationListeners; + } + + @Override + public void refresh() throws BeansException, IllegalStateException { + synchronized (this.startupShutdownMonitor) { + StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh"); + + // Prepare this context for refreshing. + prepareRefresh(); + + // Tell the subclass to refresh the internal bean factory. + ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); + + // Prepare the bean factory for use in this context. + prepareBeanFactory(beanFactory); + + try { + // Allows post-processing of the bean factory in context subclasses. + postProcessBeanFactory(beanFactory); + + StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process"); + // Invoke factory processors registered as beans in the context. + invokeBeanFactoryPostProcessors(beanFactory); + + // Register bean processors that intercept bean creation. + registerBeanPostProcessors(beanFactory); + beanPostProcess.end(); + + // Initialize message source for this context. + initMessageSource(); + + // Initialize event multicaster for this context. + initApplicationEventMulticaster(); + + // Initialize other special beans in specific context subclasses. + onRefresh(); + + // Check for listener beans and register them. + registerListeners(); + + // Instantiate all remaining (non-lazy-init) singletons. + finishBeanFactoryInitialization(beanFactory); + + // Last step: publish corresponding event. + finishRefresh(); + } + + catch (BeansException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Exception encountered during context initialization - " + + "cancelling refresh attempt: " + ex); + } + + // Destroy already created singletons to avoid dangling resources. + destroyBeans(); + + // Reset 'active' flag. + cancelRefresh(ex); + + // Propagate exception to caller. + throw ex; + } + + finally { + // Reset common introspection caches in Spring's core, since we + // might not ever need metadata for singleton beans anymore... + resetCommonCaches(); + contextRefresh.end(); + } + } + } + + /** + * Prepare this context for refreshing, setting its startup date and + * active flag as well as performing any initialization of property sources. + */ + protected void prepareRefresh() { + // Switch to active. + this.startupDate = System.currentTimeMillis(); + this.closed.set(false); + this.active.set(true); + + if (logger.isDebugEnabled()) { + if (logger.isTraceEnabled()) { + logger.trace("Refreshing " + this); + } + else { + logger.debug("Refreshing " + getDisplayName()); + } + } + + // Initialize any placeholder property sources in the context environment. + initPropertySources(); + + // Validate that all properties marked as required are resolvable: + // see ConfigurablePropertyResolver#setRequiredProperties + getEnvironment().validateRequiredProperties(); + + // Store pre-refresh ApplicationListeners... + if (this.earlyApplicationListeners == null) { + this.earlyApplicationListeners = new LinkedHashSet<>(this.applicationListeners); + } + else { + // Reset local application listeners to pre-refresh state. + this.applicationListeners.clear(); + this.applicationListeners.addAll(this.earlyApplicationListeners); + } + + // Allow for the collection of early ApplicationEvents, + // to be published once the multicaster is available... + this.earlyApplicationEvents = new LinkedHashSet<>(); + } + + /** + *

    Replace any stub property sources with actual instances. + * @see org.springframework.core.env.PropertySource.StubPropertySource + * @see org.springframework.web.context.support.WebApplicationContextUtils#initServletPropertySources + */ + protected void initPropertySources() { + // For subclasses: do nothing by default. + } + + /** + * Tell the subclass to refresh the internal bean factory. + * @return the fresh BeanFactory instance + * @see #refreshBeanFactory() + * @see #getBeanFactory() + */ + protected ConfigurableListableBeanFactory obtainFreshBeanFactory() { + refreshBeanFactory(); + return getBeanFactory(); + } + + /** + * Configure the factory's standard context characteristics, + * such as the context's ClassLoader and post-processors. + * @param beanFactory the BeanFactory to configure + */ + protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) { + // Tell the internal bean factory to use the context's class loader etc. + beanFactory.setBeanClassLoader(getClassLoader()); + if (!shouldIgnoreSpel) { + beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader())); + } + beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment())); + + // Configure the bean factory with context callbacks. + beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this)); + beanFactory.ignoreDependencyInterface(EnvironmentAware.class); + beanFactory.ignoreDependencyInterface(EmbeddedValueResolverAware.class); + beanFactory.ignoreDependencyInterface(ResourceLoaderAware.class); + beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class); + beanFactory.ignoreDependencyInterface(MessageSourceAware.class); + beanFactory.ignoreDependencyInterface(ApplicationContextAware.class); + beanFactory.ignoreDependencyInterface(ApplicationStartup.class); + + // BeanFactory interface not registered as resolvable type in a plain factory. + // MessageSource registered (and found for autowiring) as a bean. + beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory); + beanFactory.registerResolvableDependency(ResourceLoader.class, this); + beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this); + beanFactory.registerResolvableDependency(ApplicationContext.class, this); + + // Register early post-processor for detecting inner beans as ApplicationListeners. + beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this)); + + // Detect a LoadTimeWeaver and prepare for weaving, if found. + if (!IN_NATIVE_IMAGE && beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) { + beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory)); + // Set a temporary ClassLoader for type matching. + beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader())); + } + + // Register default environment beans. + if (!beanFactory.containsLocalBean(ENVIRONMENT_BEAN_NAME)) { + beanFactory.registerSingleton(ENVIRONMENT_BEAN_NAME, getEnvironment()); + } + if (!beanFactory.containsLocalBean(SYSTEM_PROPERTIES_BEAN_NAME)) { + beanFactory.registerSingleton(SYSTEM_PROPERTIES_BEAN_NAME, getEnvironment().getSystemProperties()); + } + if (!beanFactory.containsLocalBean(SYSTEM_ENVIRONMENT_BEAN_NAME)) { + beanFactory.registerSingleton(SYSTEM_ENVIRONMENT_BEAN_NAME, getEnvironment().getSystemEnvironment()); + } + if (!beanFactory.containsLocalBean(APPLICATION_STARTUP_BEAN_NAME)) { + beanFactory.registerSingleton(APPLICATION_STARTUP_BEAN_NAME, getApplicationStartup()); + } + } + + /** + * Modify the application context's internal bean factory after its standard + * initialization. All bean definitions will have been loaded, but no beans + * will have been instantiated yet. This allows for registering special + * BeanPostProcessors etc in certain ApplicationContext implementations. + * @param beanFactory the bean factory used by the application context + */ + protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { + } + + /** + * Instantiate and invoke all registered BeanFactoryPostProcessor beans, + * respecting explicit order if given. + *

    Must be called before singleton instantiation. + */ + protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) { + PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors()); + + // Detect a LoadTimeWeaver and prepare for weaving, if found in the meantime + // (e.g. through an @Bean method registered by ConfigurationClassPostProcessor) + if (!IN_NATIVE_IMAGE && beanFactory.getTempClassLoader() == null && beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) { + beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory)); + beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader())); + } + } + + /** + * Instantiate and register all BeanPostProcessor beans, + * respecting explicit order if given. + *

    Must be called before any instantiation of application beans. + */ + protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFactory) { + PostProcessorRegistrationDelegate.registerBeanPostProcessors(beanFactory, this); + } + + /** + * Initialize the MessageSource. + * Use parent's if none defined in this context. + */ + protected void initMessageSource() { + ConfigurableListableBeanFactory beanFactory = getBeanFactory(); + if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME)) { + this.messageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, MessageSource.class); + // Make MessageSource aware of parent MessageSource. + if (this.parent != null && this.messageSource instanceof HierarchicalMessageSource) { + HierarchicalMessageSource hms = (HierarchicalMessageSource) this.messageSource; + if (hms.getParentMessageSource() == null) { + // Only set parent context as parent MessageSource if no parent MessageSource + // registered already. + hms.setParentMessageSource(getInternalParentMessageSource()); + } + } + if (logger.isTraceEnabled()) { + logger.trace("Using MessageSource [" + this.messageSource + "]"); + } + } + else { + // Use empty MessageSource to be able to accept getMessage calls. + DelegatingMessageSource dms = new DelegatingMessageSource(); + dms.setParentMessageSource(getInternalParentMessageSource()); + this.messageSource = dms; + beanFactory.registerSingleton(MESSAGE_SOURCE_BEAN_NAME, this.messageSource); + if (logger.isTraceEnabled()) { + logger.trace("No '" + MESSAGE_SOURCE_BEAN_NAME + "' bean, using [" + this.messageSource + "]"); + } + } + } + + /** + * Initialize the ApplicationEventMulticaster. + * Uses SimpleApplicationEventMulticaster if none defined in the context. + * @see org.springframework.context.event.SimpleApplicationEventMulticaster + */ + protected void initApplicationEventMulticaster() { + ConfigurableListableBeanFactory beanFactory = getBeanFactory(); + if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) { + this.applicationEventMulticaster = + beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class); + if (logger.isTraceEnabled()) { + logger.trace("Using ApplicationEventMulticaster [" + this.applicationEventMulticaster + "]"); + } + } + else { + this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory); + beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster); + if (logger.isTraceEnabled()) { + logger.trace("No '" + APPLICATION_EVENT_MULTICASTER_BEAN_NAME + "' bean, using " + + "[" + this.applicationEventMulticaster.getClass().getSimpleName() + "]"); + } + } + } + + /** + * Initialize the LifecycleProcessor. + * Uses DefaultLifecycleProcessor if none defined in the context. + * @see org.springframework.context.support.DefaultLifecycleProcessor + */ + protected void initLifecycleProcessor() { + ConfigurableListableBeanFactory beanFactory = getBeanFactory(); + if (beanFactory.containsLocalBean(LIFECYCLE_PROCESSOR_BEAN_NAME)) { + this.lifecycleProcessor = + beanFactory.getBean(LIFECYCLE_PROCESSOR_BEAN_NAME, LifecycleProcessor.class); + if (logger.isTraceEnabled()) { + logger.trace("Using LifecycleProcessor [" + this.lifecycleProcessor + "]"); + } + } + else { + DefaultLifecycleProcessor defaultProcessor = new DefaultLifecycleProcessor(); + defaultProcessor.setBeanFactory(beanFactory); + this.lifecycleProcessor = defaultProcessor; + beanFactory.registerSingleton(LIFECYCLE_PROCESSOR_BEAN_NAME, this.lifecycleProcessor); + if (logger.isTraceEnabled()) { + logger.trace("No '" + LIFECYCLE_PROCESSOR_BEAN_NAME + "' bean, using " + + "[" + this.lifecycleProcessor.getClass().getSimpleName() + "]"); + } + } + } + + /** + * Template method which can be overridden to add context-specific refresh work. + * Called on initialization of special beans, before instantiation of singletons. + *

    This implementation is empty. + * @throws BeansException in case of errors + * @see #refresh() + */ + protected void onRefresh() throws BeansException { + // For subclasses: do nothing by default. + } + + /** + * Add beans that implement ApplicationListener as listeners. + * Doesn't affect other listeners, which can be added without being beans. + */ + protected void registerListeners() { + // Register statically specified listeners first. + for (ApplicationListener listener : getApplicationListeners()) { + getApplicationEventMulticaster().addApplicationListener(listener); + } + + // Do not initialize FactoryBeans here: We need to leave all regular beans + // uninitialized to let post-processors apply to them! + String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false); + for (String listenerBeanName : listenerBeanNames) { + getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName); + } + + // Publish early application events now that we finally have a multicaster... + Set earlyEventsToProcess = this.earlyApplicationEvents; + this.earlyApplicationEvents = null; + if (!CollectionUtils.isEmpty(earlyEventsToProcess)) { + for (ApplicationEvent earlyEvent : earlyEventsToProcess) { + getApplicationEventMulticaster().multicastEvent(earlyEvent); + } + } + } + + /** + * Finish the initialization of this context's bean factory, + * initializing all remaining singleton beans. + */ + protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) { + // Initialize conversion service for this context. + if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) && + beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) { + beanFactory.setConversionService( + beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)); + } + + // Register a default embedded value resolver if no bean post-processor + // (such as a PropertyPlaceholderConfigurer bean) registered any before: + // at this point, primarily for resolution in annotation attribute values. + if (!beanFactory.hasEmbeddedValueResolver()) { + beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal)); + } + + // Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early. + String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false); + for (String weaverAwareName : weaverAwareNames) { + getBean(weaverAwareName); + } + + // Stop using the temporary ClassLoader for type matching. + beanFactory.setTempClassLoader(null); + + // Allow for caching all bean definition metadata, not expecting further changes. + beanFactory.freezeConfiguration(); + + // Instantiate all remaining (non-lazy-init) singletons. + beanFactory.preInstantiateSingletons(); + } + + /** + * Finish the refresh of this context, invoking the LifecycleProcessor's + * onRefresh() method and publishing the + * {@link org.springframework.context.event.ContextRefreshedEvent}. + */ + @SuppressWarnings("deprecation") + protected void finishRefresh() { + // Clear context-level resource caches (such as ASM metadata from scanning). + clearResourceCaches(); + + // Initialize lifecycle processor for this context. + initLifecycleProcessor(); + + // Propagate refresh to lifecycle processor first. + getLifecycleProcessor().onRefresh(); + + // Publish the final event. + publishEvent(new ContextRefreshedEvent(this)); + + // Participate in LiveBeansView MBean, if active. + if (!IN_NATIVE_IMAGE) { + LiveBeansView.registerApplicationContext(this); + } + } + + /** + * Cancel this context's refresh attempt, resetting the {@code active} flag + * after an exception got thrown. + * @param ex the exception that led to the cancellation + */ + protected void cancelRefresh(BeansException ex) { + this.active.set(false); + } + + /** + * Reset Spring's common reflection metadata caches, in particular the + * {@link ReflectionUtils}, {@link AnnotationUtils}, {@link ResolvableType} + * and {@link CachedIntrospectionResults} caches. + * @since 4.2 + * @see ReflectionUtils#clearCache() + * @see AnnotationUtils#clearCache() + * @see ResolvableType#clearCache() + * @see CachedIntrospectionResults#clearClassLoader(ClassLoader) + */ + protected void resetCommonCaches() { + ReflectionUtils.clearCache(); + AnnotationUtils.clearCache(); + ResolvableType.clearCache(); + CachedIntrospectionResults.clearClassLoader(getClassLoader()); + } + + + /** + * Register a shutdown hook {@linkplain Thread#getName() named} + * {@code SpringContextShutdownHook} with the JVM runtime, closing this + * context on JVM shutdown unless it has already been closed at that time. + *

    Delegates to {@code doClose()} for the actual closing procedure. + * @see Runtime#addShutdownHook + * @see ConfigurableApplicationContext#SHUTDOWN_HOOK_THREAD_NAME + * @see #close() + * @see #doClose() + */ + @Override + public void registerShutdownHook() { + if (this.shutdownHook == null) { + // No shutdown hook registered yet. + this.shutdownHook = new Thread(SHUTDOWN_HOOK_THREAD_NAME) { + @Override + public void run() { + synchronized (startupShutdownMonitor) { + doClose(); + } + } + }; + Runtime.getRuntime().addShutdownHook(this.shutdownHook); + } + } + + /** + * Callback for destruction of this instance, originally attached + * to a {@code DisposableBean} implementation (not anymore in 5.0). + *

    The {@link #close()} method is the native way to shut down + * an ApplicationContext, which this method simply delegates to. + * @deprecated as of Spring Framework 5.0, in favor of {@link #close()} + */ + @Deprecated + public void destroy() { + close(); + } + + /** + * Close this application context, destroying all beans in its bean factory. + *

    Delegates to {@code doClose()} for the actual closing procedure. + * Also removes a JVM shutdown hook, if registered, as it's not needed anymore. + * @see #doClose() + * @see #registerShutdownHook() + */ + @Override + public void close() { + synchronized (this.startupShutdownMonitor) { + doClose(); + // If we registered a JVM shutdown hook, we don't need it anymore now: + // We've already explicitly closed the context. + if (this.shutdownHook != null) { + try { + Runtime.getRuntime().removeShutdownHook(this.shutdownHook); + } + catch (IllegalStateException ex) { + // ignore - VM is already shutting down + } + } + } + } + + /** + * Actually performs context closing: publishes a ContextClosedEvent and + * destroys the singletons in the bean factory of this application context. + *

    Called by both {@code close()} and a JVM shutdown hook, if any. + * @see org.springframework.context.event.ContextClosedEvent + * @see #destroyBeans() + * @see #close() + * @see #registerShutdownHook() + */ + @SuppressWarnings("deprecation") + protected void doClose() { + // Check whether an actual close attempt is necessary... + if (this.active.get() && this.closed.compareAndSet(false, true)) { + if (logger.isDebugEnabled()) { + logger.debug("Closing " + this); + } + + if (!IN_NATIVE_IMAGE) { + LiveBeansView.unregisterApplicationContext(this); + } + + try { + // Publish shutdown event. + publishEvent(new ContextClosedEvent(this)); + } + catch (Throwable ex) { + logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex); + } + + // Stop all Lifecycle beans, to avoid delays during individual destruction. + if (this.lifecycleProcessor != null) { + try { + this.lifecycleProcessor.onClose(); + } + catch (Throwable ex) { + logger.warn("Exception thrown from LifecycleProcessor on context close", ex); + } + } + + // Destroy all cached singletons in the context's BeanFactory. + destroyBeans(); + + // Close the state of this context itself. + closeBeanFactory(); + + // Let subclasses do some final clean-up if they wish... + onClose(); + + // Reset local application listeners to pre-refresh state. + if (this.earlyApplicationListeners != null) { + this.applicationListeners.clear(); + this.applicationListeners.addAll(this.earlyApplicationListeners); + } + + // Switch to inactive. + this.active.set(false); + } + } + + /** + * Template method for destroying all beans that this context manages. + * The default implementation destroy all cached singletons in this context, + * invoking {@code DisposableBean.destroy()} and/or the specified + * "destroy-method". + *

    Can be overridden to add context-specific bean destruction steps + * right before or right after standard singleton destruction, + * while the context's BeanFactory is still active. + * @see #getBeanFactory() + * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#destroySingletons() + */ + protected void destroyBeans() { + getBeanFactory().destroySingletons(); + } + + /** + * Template method which can be overridden to add context-specific shutdown work. + * The default implementation is empty. + *

    Called at the end of {@link #doClose}'s shutdown procedure, after + * this context's BeanFactory has been closed. If custom shutdown logic + * needs to execute while the BeanFactory is still active, override + * the {@link #destroyBeans()} method instead. + */ + protected void onClose() { + // For subclasses: do nothing by default. + } + + @Override + public boolean isActive() { + return this.active.get(); + } + + /** + * Assert that this context's BeanFactory is currently active, + * throwing an {@link IllegalStateException} if it isn't. + *

    Invoked by all {@link BeanFactory} delegation methods that depend + * on an active context, i.e. in particular all bean accessor methods. + *

    The default implementation checks the {@link #isActive() 'active'} status + * of this context overall. May be overridden for more specific checks, or for a + * no-op if {@link #getBeanFactory()} itself throws an exception in such a case. + */ + protected void assertBeanFactoryActive() { + if (!this.active.get()) { + if (this.closed.get()) { + throw new IllegalStateException(getDisplayName() + " has been closed already"); + } + else { + throw new IllegalStateException(getDisplayName() + " has not been refreshed yet"); + } + } + } + + + //--------------------------------------------------------------------- + // Implementation of BeanFactory interface + //--------------------------------------------------------------------- + + @Override + public Object getBean(String name) throws BeansException { + assertBeanFactoryActive(); + return getBeanFactory().getBean(name); + } + + @Override + public T getBean(String name, Class requiredType) throws BeansException { + assertBeanFactoryActive(); + return getBeanFactory().getBean(name, requiredType); + } + + @Override + public Object getBean(String name, Object... args) throws BeansException { + assertBeanFactoryActive(); + return getBeanFactory().getBean(name, args); + } + + @Override + public T getBean(Class requiredType) throws BeansException { + assertBeanFactoryActive(); + return getBeanFactory().getBean(requiredType); + } + + @Override + public T getBean(Class requiredType, Object... args) throws BeansException { + assertBeanFactoryActive(); + return getBeanFactory().getBean(requiredType, args); + } + + @Override + public ObjectProvider getBeanProvider(Class requiredType) { + assertBeanFactoryActive(); + return getBeanFactory().getBeanProvider(requiredType); + } + + @Override + public ObjectProvider getBeanProvider(ResolvableType requiredType) { + assertBeanFactoryActive(); + return getBeanFactory().getBeanProvider(requiredType); + } + + @Override + public boolean containsBean(String name) { + return getBeanFactory().containsBean(name); + } + + @Override + public boolean isSingleton(String name) throws NoSuchBeanDefinitionException { + assertBeanFactoryActive(); + return getBeanFactory().isSingleton(name); + } + + @Override + public boolean isPrototype(String name) throws NoSuchBeanDefinitionException { + assertBeanFactoryActive(); + return getBeanFactory().isPrototype(name); + } + + @Override + public boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException { + assertBeanFactoryActive(); + return getBeanFactory().isTypeMatch(name, typeToMatch); + } + + @Override + public boolean isTypeMatch(String name, Class typeToMatch) throws NoSuchBeanDefinitionException { + assertBeanFactoryActive(); + return getBeanFactory().isTypeMatch(name, typeToMatch); + } + + @Override + @Nullable + public Class getType(String name) throws NoSuchBeanDefinitionException { + assertBeanFactoryActive(); + return getBeanFactory().getType(name); + } + + @Override + @Nullable + public Class getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException { + assertBeanFactoryActive(); + return getBeanFactory().getType(name, allowFactoryBeanInit); + } + + @Override + public String[] getAliases(String name) { + return getBeanFactory().getAliases(name); + } + + + //--------------------------------------------------------------------- + // Implementation of ListableBeanFactory interface + //--------------------------------------------------------------------- + + @Override + public boolean containsBeanDefinition(String beanName) { + return getBeanFactory().containsBeanDefinition(beanName); + } + + @Override + public int getBeanDefinitionCount() { + return getBeanFactory().getBeanDefinitionCount(); + } + + @Override + public String[] getBeanDefinitionNames() { + return getBeanFactory().getBeanDefinitionNames(); + } + + @Override + public ObjectProvider getBeanProvider(Class requiredType, boolean allowEagerInit) { + assertBeanFactoryActive(); + return getBeanFactory().getBeanProvider(requiredType, allowEagerInit); + } + + @Override + public ObjectProvider getBeanProvider(ResolvableType requiredType, boolean allowEagerInit) { + assertBeanFactoryActive(); + return getBeanFactory().getBeanProvider(requiredType, allowEagerInit); + } + + @Override + public String[] getBeanNamesForType(ResolvableType type) { + assertBeanFactoryActive(); + return getBeanFactory().getBeanNamesForType(type); + } + + @Override + public String[] getBeanNamesForType(ResolvableType type, boolean includeNonSingletons, boolean allowEagerInit) { + assertBeanFactoryActive(); + return getBeanFactory().getBeanNamesForType(type, includeNonSingletons, allowEagerInit); + } + + @Override + public String[] getBeanNamesForType(@Nullable Class type) { + assertBeanFactoryActive(); + return getBeanFactory().getBeanNamesForType(type); + } + + @Override + public String[] getBeanNamesForType(@Nullable Class type, boolean includeNonSingletons, boolean allowEagerInit) { + assertBeanFactoryActive(); + return getBeanFactory().getBeanNamesForType(type, includeNonSingletons, allowEagerInit); + } + + @Override + public Map getBeansOfType(@Nullable Class type) throws BeansException { + assertBeanFactoryActive(); + return getBeanFactory().getBeansOfType(type); + } + + @Override + public Map getBeansOfType(@Nullable Class type, boolean includeNonSingletons, boolean allowEagerInit) + throws BeansException { + + assertBeanFactoryActive(); + return getBeanFactory().getBeansOfType(type, includeNonSingletons, allowEagerInit); + } + + @Override + public String[] getBeanNamesForAnnotation(Class annotationType) { + assertBeanFactoryActive(); + return getBeanFactory().getBeanNamesForAnnotation(annotationType); + } + + @Override + public Map getBeansWithAnnotation(Class annotationType) + throws BeansException { + + assertBeanFactoryActive(); + return getBeanFactory().getBeansWithAnnotation(annotationType); + } + + @Override + @Nullable + public A findAnnotationOnBean(String beanName, Class annotationType) + throws NoSuchBeanDefinitionException { + + assertBeanFactoryActive(); + return getBeanFactory().findAnnotationOnBean(beanName, annotationType); + } + + + //--------------------------------------------------------------------- + // Implementation of HierarchicalBeanFactory interface + //--------------------------------------------------------------------- + + @Override + @Nullable + public BeanFactory getParentBeanFactory() { + return getParent(); + } + + @Override + public boolean containsLocalBean(String name) { + return getBeanFactory().containsLocalBean(name); + } + + /** + * Return the internal bean factory of the parent context if it implements + * ConfigurableApplicationContext; else, return the parent context itself. + * @see org.springframework.context.ConfigurableApplicationContext#getBeanFactory + */ + @Nullable + protected BeanFactory getInternalParentBeanFactory() { + return (getParent() instanceof ConfigurableApplicationContext ? + ((ConfigurableApplicationContext) getParent()).getBeanFactory() : getParent()); + } + + + //--------------------------------------------------------------------- + // Implementation of MessageSource interface + //--------------------------------------------------------------------- + + @Override + public String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) { + return getMessageSource().getMessage(code, args, defaultMessage, locale); + } + + @Override + public String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException { + return getMessageSource().getMessage(code, args, locale); + } + + @Override + public String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException { + return getMessageSource().getMessage(resolvable, locale); + } + + /** + * Return the internal MessageSource used by the context. + * @return the internal MessageSource (never {@code null}) + * @throws IllegalStateException if the context has not been initialized yet + */ + private MessageSource getMessageSource() throws IllegalStateException { + if (this.messageSource == null) { + throw new IllegalStateException("MessageSource not initialized - " + + "call 'refresh' before accessing messages via the context: " + this); + } + return this.messageSource; + } + + /** + * Return the internal message source of the parent context if it is an + * AbstractApplicationContext too; else, return the parent context itself. + */ + @Nullable + protected MessageSource getInternalParentMessageSource() { + return (getParent() instanceof AbstractApplicationContext ? + ((AbstractApplicationContext) getParent()).messageSource : getParent()); + } + + + //--------------------------------------------------------------------- + // Implementation of ResourcePatternResolver interface + //--------------------------------------------------------------------- + + @Override + public Resource[] getResources(String locationPattern) throws IOException { + return this.resourcePatternResolver.getResources(locationPattern); + } + + + //--------------------------------------------------------------------- + // Implementation of Lifecycle interface + //--------------------------------------------------------------------- + + @Override + public void start() { + getLifecycleProcessor().start(); + publishEvent(new ContextStartedEvent(this)); + } + + @Override + public void stop() { + getLifecycleProcessor().stop(); + publishEvent(new ContextStoppedEvent(this)); + } + + @Override + public boolean isRunning() { + return (this.lifecycleProcessor != null && this.lifecycleProcessor.isRunning()); + } + + + //--------------------------------------------------------------------- + // Abstract methods that must be implemented by subclasses + //--------------------------------------------------------------------- + + /** + * Subclasses must implement this method to perform the actual configuration load. + * The method is invoked by {@link #refresh()} before any other initialization work. + *

    A subclass will either create a new bean factory and hold a reference to it, + * or return a single BeanFactory instance that it holds. In the latter case, it will + * usually throw an IllegalStateException if refreshing the context more than once. + * @throws BeansException if initialization of the bean factory failed + * @throws IllegalStateException if already initialized and multiple refresh + * attempts are not supported + */ + protected abstract void refreshBeanFactory() throws BeansException, IllegalStateException; + + /** + * Subclasses must implement this method to release their internal bean factory. + * This method gets invoked by {@link #close()} after all other shutdown work. + *

    Should never throw an exception but rather log shutdown failures. + */ + protected abstract void closeBeanFactory(); + + /** + * Subclasses must return their internal bean factory here. They should implement the + * lookup efficiently, so that it can be called repeatedly without a performance penalty. + *

    Note: Subclasses should check whether the context is still active before + * returning the internal bean factory. The internal factory should generally be + * considered unavailable once the context has been closed. + * @return this application context's internal bean factory (never {@code null}) + * @throws IllegalStateException if the context does not hold an internal bean factory yet + * (usually if {@link #refresh()} has never been called) or if the context has been + * closed already + * @see #refreshBeanFactory() + * @see #closeBeanFactory() + */ + @Override + public abstract ConfigurableListableBeanFactory getBeanFactory() throws IllegalStateException; + + + /** + * Return information about this context. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(getDisplayName()); + sb.append(", started on ").append(new Date(getStartupDate())); + ApplicationContext parent = getParent(); + if (parent != null) { + sb.append(", parent: ").append(parent.getDisplayName()); + } + return sb.toString(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/AbstractMessageSource.java new file mode 100644 index 0000000..df4dbbf --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractMessageSource.java @@ -0,0 +1,393 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Properties; + +import org.springframework.context.HierarchicalMessageSource; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.NoSuchMessageException; +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +/** + * Abstract implementation of the {@link HierarchicalMessageSource} interface, + * implementing common handling of message variants, making it easy + * to implement a specific strategy for a concrete MessageSource. + * + *

    Subclasses must implement the abstract {@link #resolveCode} + * method. For efficient resolution of messages without arguments, the + * {@link #resolveCodeWithoutArguments} method should be overridden + * as well, resolving messages without a MessageFormat being involved. + * + *

    Note: By default, message texts are only parsed through + * MessageFormat if arguments have been passed in for the message. In case + * of no arguments, message texts will be returned as-is. As a consequence, + * you should only use MessageFormat escaping for messages with actual + * arguments, and keep all other messages unescaped. If you prefer to + * escape all messages, set the "alwaysUseMessageFormat" flag to "true". + * + *

    Supports not only MessageSourceResolvables as primary messages + * but also resolution of message arguments that are in turn + * MessageSourceResolvables themselves. + * + *

    This class does not implement caching of messages per code, thus + * subclasses can dynamically change messages over time. Subclasses are + * encouraged to cache their messages in a modification-aware fashion, + * allowing for hot deployment of updated messages. + * + * @author Juergen Hoeller + * @author Rod Johnson + * @see #resolveCode(String, java.util.Locale) + * @see #resolveCodeWithoutArguments(String, java.util.Locale) + * @see #setAlwaysUseMessageFormat + * @see java.text.MessageFormat + */ +public abstract class AbstractMessageSource extends MessageSourceSupport implements HierarchicalMessageSource { + + @Nullable + private MessageSource parentMessageSource; + + @Nullable + private Properties commonMessages; + + private boolean useCodeAsDefaultMessage = false; + + + @Override + public void setParentMessageSource(@Nullable MessageSource parent) { + this.parentMessageSource = parent; + } + + @Override + @Nullable + public MessageSource getParentMessageSource() { + return this.parentMessageSource; + } + + /** + * Specify locale-independent common messages, with the message code as key + * and the full message String (may contain argument placeholders) as value. + *

    May also link to an externally defined Properties object, e.g. defined + * through a {@link org.springframework.beans.factory.config.PropertiesFactoryBean}. + */ + public void setCommonMessages(@Nullable Properties commonMessages) { + this.commonMessages = commonMessages; + } + + /** + * Return a Properties object defining locale-independent common messages, if any. + */ + @Nullable + protected Properties getCommonMessages() { + return this.commonMessages; + } + + /** + * Set whether to use the message code as default message instead of + * throwing a NoSuchMessageException. Useful for development and debugging. + * Default is "false". + *

    Note: In case of a MessageSourceResolvable with multiple codes + * (like a FieldError) and a MessageSource that has a parent MessageSource, + * do not activate "useCodeAsDefaultMessage" in the parent: + * Else, you'll get the first code returned as message by the parent, + * without attempts to check further codes. + *

    To be able to work with "useCodeAsDefaultMessage" turned on in the parent, + * AbstractMessageSource and AbstractApplicationContext contain special checks + * to delegate to the internal {@link #getMessageInternal} method if available. + * In general, it is recommended to just use "useCodeAsDefaultMessage" during + * development and not rely on it in production in the first place, though. + * @see #getMessage(String, Object[], Locale) + * @see org.springframework.validation.FieldError + */ + public void setUseCodeAsDefaultMessage(boolean useCodeAsDefaultMessage) { + this.useCodeAsDefaultMessage = useCodeAsDefaultMessage; + } + + /** + * Return whether to use the message code as default message instead of + * throwing a NoSuchMessageException. Useful for development and debugging. + * Default is "false". + *

    Alternatively, consider overriding the {@link #getDefaultMessage} + * method to return a custom fallback message for an unresolvable code. + * @see #getDefaultMessage(String) + */ + protected boolean isUseCodeAsDefaultMessage() { + return this.useCodeAsDefaultMessage; + } + + + @Override + public final String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) { + String msg = getMessageInternal(code, args, locale); + if (msg != null) { + return msg; + } + if (defaultMessage == null) { + return getDefaultMessage(code); + } + return renderDefaultMessage(defaultMessage, args, locale); + } + + @Override + public final String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException { + String msg = getMessageInternal(code, args, locale); + if (msg != null) { + return msg; + } + String fallback = getDefaultMessage(code); + if (fallback != null) { + return fallback; + } + throw new NoSuchMessageException(code, locale); + } + + @Override + public final String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException { + String[] codes = resolvable.getCodes(); + if (codes != null) { + for (String code : codes) { + String message = getMessageInternal(code, resolvable.getArguments(), locale); + if (message != null) { + return message; + } + } + } + String defaultMessage = getDefaultMessage(resolvable, locale); + if (defaultMessage != null) { + return defaultMessage; + } + throw new NoSuchMessageException(!ObjectUtils.isEmpty(codes) ? codes[codes.length - 1] : "", locale); + } + + + /** + * Resolve the given code and arguments as message in the given Locale, + * returning {@code null} if not found. Does not fall back to + * the code as default message. Invoked by {@code getMessage} methods. + * @param code the code to lookup up, such as 'calculator.noRateSet' + * @param args array of arguments that will be filled in for params + * within the message + * @param locale the locale in which to do the lookup + * @return the resolved message, or {@code null} if not found + * @see #getMessage(String, Object[], String, Locale) + * @see #getMessage(String, Object[], Locale) + * @see #getMessage(MessageSourceResolvable, Locale) + * @see #setUseCodeAsDefaultMessage + */ + @Nullable + protected String getMessageInternal(@Nullable String code, @Nullable Object[] args, @Nullable Locale locale) { + if (code == null) { + return null; + } + if (locale == null) { + locale = Locale.getDefault(); + } + Object[] argsToUse = args; + + if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) { + // Optimized resolution: no arguments to apply, + // therefore no MessageFormat needs to be involved. + // Note that the default implementation still uses MessageFormat; + // this can be overridden in specific subclasses. + String message = resolveCodeWithoutArguments(code, locale); + if (message != null) { + return message; + } + } + + else { + // Resolve arguments eagerly, for the case where the message + // is defined in a parent MessageSource but resolvable arguments + // are defined in the child MessageSource. + argsToUse = resolveArguments(args, locale); + + MessageFormat messageFormat = resolveCode(code, locale); + if (messageFormat != null) { + synchronized (messageFormat) { + return messageFormat.format(argsToUse); + } + } + } + + // Check locale-independent common messages for the given message code. + Properties commonMessages = getCommonMessages(); + if (commonMessages != null) { + String commonMessage = commonMessages.getProperty(code); + if (commonMessage != null) { + return formatMessage(commonMessage, args, locale); + } + } + + // Not found -> check parent, if any. + return getMessageFromParent(code, argsToUse, locale); + } + + /** + * Try to retrieve the given message from the parent {@code MessageSource}, if any. + * @param code the code to lookup up, such as 'calculator.noRateSet' + * @param args array of arguments that will be filled in for params + * within the message + * @param locale the locale in which to do the lookup + * @return the resolved message, or {@code null} if not found + * @see #getParentMessageSource() + */ + @Nullable + protected String getMessageFromParent(String code, @Nullable Object[] args, Locale locale) { + MessageSource parent = getParentMessageSource(); + if (parent != null) { + if (parent instanceof AbstractMessageSource) { + // Call internal method to avoid getting the default code back + // in case of "useCodeAsDefaultMessage" being activated. + return ((AbstractMessageSource) parent).getMessageInternal(code, args, locale); + } + else { + // Check parent MessageSource, returning null if not found there. + // Covers custom MessageSource impls and DelegatingMessageSource. + return parent.getMessage(code, args, null, locale); + } + } + // Not found in parent either. + return null; + } + + /** + * Get a default message for the given {@code MessageSourceResolvable}. + *

    This implementation fully renders the default message if available, + * or just returns the plain default message {@code String} if the primary + * message code is being used as a default message. + * @param resolvable the value object to resolve a default message for + * @param locale the current locale + * @return the default message, or {@code null} if none + * @since 4.3.6 + * @see #renderDefaultMessage(String, Object[], Locale) + * @see #getDefaultMessage(String) + */ + @Nullable + protected String getDefaultMessage(MessageSourceResolvable resolvable, Locale locale) { + String defaultMessage = resolvable.getDefaultMessage(); + String[] codes = resolvable.getCodes(); + if (defaultMessage != null) { + if (resolvable instanceof DefaultMessageSourceResolvable && + !((DefaultMessageSourceResolvable) resolvable).shouldRenderDefaultMessage()) { + // Given default message does not contain any argument placeholders + // (and isn't escaped for alwaysUseMessageFormat either) -> return as-is. + return defaultMessage; + } + if (!ObjectUtils.isEmpty(codes) && defaultMessage.equals(codes[0])) { + // Never format a code-as-default-message, even with alwaysUseMessageFormat=true + return defaultMessage; + } + return renderDefaultMessage(defaultMessage, resolvable.getArguments(), locale); + } + return (!ObjectUtils.isEmpty(codes) ? getDefaultMessage(codes[0]) : null); + } + + /** + * Return a fallback default message for the given code, if any. + *

    Default is to return the code itself if "useCodeAsDefaultMessage" is activated, + * or return no fallback else. In case of no fallback, the caller will usually + * receive a {@code NoSuchMessageException} from {@code getMessage}. + * @param code the message code that we couldn't resolve + * and that we didn't receive an explicit default message for + * @return the default message to use, or {@code null} if none + * @see #setUseCodeAsDefaultMessage + */ + @Nullable + protected String getDefaultMessage(String code) { + if (isUseCodeAsDefaultMessage()) { + return code; + } + return null; + } + + + /** + * Searches through the given array of objects, finds any MessageSourceResolvable + * objects and resolves them. + *

    Allows for messages to have MessageSourceResolvables as arguments. + * @param args array of arguments for a message + * @param locale the locale to resolve through + * @return an array of arguments with any MessageSourceResolvables resolved + */ + @Override + protected Object[] resolveArguments(@Nullable Object[] args, Locale locale) { + if (ObjectUtils.isEmpty(args)) { + return super.resolveArguments(args, locale); + } + List resolvedArgs = new ArrayList<>(args.length); + for (Object arg : args) { + if (arg instanceof MessageSourceResolvable) { + resolvedArgs.add(getMessage((MessageSourceResolvable) arg, locale)); + } + else { + resolvedArgs.add(arg); + } + } + return resolvedArgs.toArray(); + } + + /** + * Subclasses can override this method to resolve a message without arguments + * in an optimized fashion, i.e. to resolve without involving a MessageFormat. + *

    The default implementation does use MessageFormat, through + * delegating to the {@link #resolveCode} method. Subclasses are encouraged + * to replace this with optimized resolution. + *

    Unfortunately, {@code java.text.MessageFormat} is not implemented + * in an efficient fashion. In particular, it does not detect that a message + * pattern doesn't contain argument placeholders in the first place. Therefore, + * it is advisable to circumvent MessageFormat for messages without arguments. + * @param code the code of the message to resolve + * @param locale the locale to resolve the code for + * (subclasses are encouraged to support internationalization) + * @return the message String, or {@code null} if not found + * @see #resolveCode + * @see java.text.MessageFormat + */ + @Nullable + protected String resolveCodeWithoutArguments(String code, Locale locale) { + MessageFormat messageFormat = resolveCode(code, locale); + if (messageFormat != null) { + synchronized (messageFormat) { + return messageFormat.format(new Object[0]); + } + } + return null; + } + + /** + * Subclasses must implement this method to resolve a message. + *

    Returns a MessageFormat instance rather than a message String, + * to allow for appropriate caching of MessageFormats in subclasses. + *

    Subclasses are encouraged to provide optimized resolution + * for messages without arguments, not involving MessageFormat. + * See the {@link #resolveCodeWithoutArguments} javadoc for details. + * @param code the code of the message to resolve + * @param locale the locale to resolve the code for + * (subclasses are encouraged to support internationalization) + * @return the MessageFormat for the message, or {@code null} if not found + * @see #resolveCodeWithoutArguments(String, java.util.Locale) + */ + @Nullable + protected abstract MessageFormat resolveCode(String code, Locale locale); + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java new file mode 100644 index 0000000..9c87844 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java @@ -0,0 +1,235 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.io.IOException; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextException; +import org.springframework.lang.Nullable; + +/** + * Base class for {@link org.springframework.context.ApplicationContext} + * implementations which are supposed to support multiple calls to {@link #refresh()}, + * creating a new internal bean factory instance every time. + * Typically (but not necessarily), such a context will be driven by + * a set of config locations to load bean definitions from. + * + *

    The only method to be implemented by subclasses is {@link #loadBeanDefinitions}, + * which gets invoked on each refresh. A concrete implementation is supposed to load + * bean definitions into the given + * {@link org.springframework.beans.factory.support.DefaultListableBeanFactory}, + * typically delegating to one or more specific bean definition readers. + * + *

    Note that there is a similar base class for WebApplicationContexts. + * {@link org.springframework.web.context.support.AbstractRefreshableWebApplicationContext} + * provides the same subclassing strategy, but additionally pre-implements + * all context functionality for web environments. There is also a + * pre-defined way to receive config locations for a web context. + * + *

    Concrete standalone subclasses of this base class, reading in a + * specific bean definition format, are {@link ClassPathXmlApplicationContext} + * and {@link FileSystemXmlApplicationContext}, which both derive from the + * common {@link AbstractXmlApplicationContext} base class; + * {@link org.springframework.context.annotation.AnnotationConfigApplicationContext} + * supports {@code @Configuration}-annotated classes as a source of bean definitions. + * + * @author Juergen Hoeller + * @author Chris Beams + * @since 1.1.3 + * @see #loadBeanDefinitions + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory + * @see org.springframework.web.context.support.AbstractRefreshableWebApplicationContext + * @see AbstractXmlApplicationContext + * @see ClassPathXmlApplicationContext + * @see FileSystemXmlApplicationContext + * @see org.springframework.context.annotation.AnnotationConfigApplicationContext + */ +public abstract class AbstractRefreshableApplicationContext extends AbstractApplicationContext { + + @Nullable + private Boolean allowBeanDefinitionOverriding; + + @Nullable + private Boolean allowCircularReferences; + + /** Bean factory for this context. */ + @Nullable + private volatile DefaultListableBeanFactory beanFactory; + + + /** + * Create a new AbstractRefreshableApplicationContext with no parent. + */ + public AbstractRefreshableApplicationContext() { + } + + /** + * Create a new AbstractRefreshableApplicationContext with the given parent context. + * @param parent the parent context + */ + public AbstractRefreshableApplicationContext(@Nullable ApplicationContext parent) { + super(parent); + } + + + /** + * Set whether it should be allowed to override bean definitions by registering + * a different definition with the same name, automatically replacing the former. + * If not, an exception will be thrown. Default is "true". + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#setAllowBeanDefinitionOverriding + */ + public void setAllowBeanDefinitionOverriding(boolean allowBeanDefinitionOverriding) { + this.allowBeanDefinitionOverriding = allowBeanDefinitionOverriding; + } + + /** + * Set whether to allow circular references between beans - and automatically + * try to resolve them. + *

    Default is "true". Turn this off to throw an exception when encountering + * a circular reference, disallowing them completely. + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#setAllowCircularReferences + */ + public void setAllowCircularReferences(boolean allowCircularReferences) { + this.allowCircularReferences = allowCircularReferences; + } + + + /** + * This implementation performs an actual refresh of this context's underlying + * bean factory, shutting down the previous bean factory (if any) and + * initializing a fresh bean factory for the next phase of the context's lifecycle. + */ + @Override + protected final void refreshBeanFactory() throws BeansException { + if (hasBeanFactory()) { + destroyBeans(); + closeBeanFactory(); + } + try { + DefaultListableBeanFactory beanFactory = createBeanFactory(); + beanFactory.setSerializationId(getId()); + customizeBeanFactory(beanFactory); + loadBeanDefinitions(beanFactory); + this.beanFactory = beanFactory; + } + catch (IOException ex) { + throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex); + } + } + + @Override + protected void cancelRefresh(BeansException ex) { + DefaultListableBeanFactory beanFactory = this.beanFactory; + if (beanFactory != null) { + beanFactory.setSerializationId(null); + } + super.cancelRefresh(ex); + } + + @Override + protected final void closeBeanFactory() { + DefaultListableBeanFactory beanFactory = this.beanFactory; + if (beanFactory != null) { + beanFactory.setSerializationId(null); + this.beanFactory = null; + } + } + + /** + * Determine whether this context currently holds a bean factory, + * i.e. has been refreshed at least once and not been closed yet. + */ + protected final boolean hasBeanFactory() { + return (this.beanFactory != null); + } + + @Override + public final ConfigurableListableBeanFactory getBeanFactory() { + DefaultListableBeanFactory beanFactory = this.beanFactory; + if (beanFactory == null) { + throw new IllegalStateException("BeanFactory not initialized or already closed - " + + "call 'refresh' before accessing beans via the ApplicationContext"); + } + return beanFactory; + } + + /** + * Overridden to turn it into a no-op: With AbstractRefreshableApplicationContext, + * {@link #getBeanFactory()} serves a strong assertion for an active context anyway. + */ + @Override + protected void assertBeanFactoryActive() { + } + + /** + * Create an internal bean factory for this context. + * Called for each {@link #refresh()} attempt. + *

    The default implementation creates a + * {@link org.springframework.beans.factory.support.DefaultListableBeanFactory} + * with the {@linkplain #getInternalParentBeanFactory() internal bean factory} of this + * context's parent as parent bean factory. Can be overridden in subclasses, + * for example to customize DefaultListableBeanFactory's settings. + * @return the bean factory for this context + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#setAllowBeanDefinitionOverriding + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#setAllowEagerClassLoading + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#setAllowCircularReferences + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#setAllowRawInjectionDespiteWrapping + */ + protected DefaultListableBeanFactory createBeanFactory() { + return new DefaultListableBeanFactory(getInternalParentBeanFactory()); + } + + /** + * Customize the internal bean factory used by this context. + * Called for each {@link #refresh()} attempt. + *

    The default implementation applies this context's + * {@linkplain #setAllowBeanDefinitionOverriding "allowBeanDefinitionOverriding"} + * and {@linkplain #setAllowCircularReferences "allowCircularReferences"} settings, + * if specified. Can be overridden in subclasses to customize any of + * {@link DefaultListableBeanFactory}'s settings. + * @param beanFactory the newly created bean factory for this context + * @see DefaultListableBeanFactory#setAllowBeanDefinitionOverriding + * @see DefaultListableBeanFactory#setAllowCircularReferences + * @see DefaultListableBeanFactory#setAllowRawInjectionDespiteWrapping + * @see DefaultListableBeanFactory#setAllowEagerClassLoading + */ + protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory) { + if (this.allowBeanDefinitionOverriding != null) { + beanFactory.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding); + } + if (this.allowCircularReferences != null) { + beanFactory.setAllowCircularReferences(this.allowCircularReferences); + } + } + + /** + * Load bean definitions into the given bean factory, typically through + * delegating to one or more bean definition readers. + * @param beanFactory the bean factory to load bean definitions into + * @throws BeansException if parsing of the bean definitions failed + * @throws IOException if loading of bean definition files failed + * @see org.springframework.beans.factory.support.PropertiesBeanDefinitionReader + * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader + */ + protected abstract void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) + throws BeansException, IOException; + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableConfigApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableConfigApplicationContext.java new file mode 100644 index 0000000..901e715 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableConfigApplicationContext.java @@ -0,0 +1,158 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * {@link AbstractRefreshableApplicationContext} subclass that adds common handling + * of specified config locations. Serves as base class for XML-based application + * context implementations such as {@link ClassPathXmlApplicationContext} and + * {@link FileSystemXmlApplicationContext}, as well as + * {@link org.springframework.web.context.support.XmlWebApplicationContext}. + * + * @author Juergen Hoeller + * @since 2.5.2 + * @see #setConfigLocation + * @see #setConfigLocations + * @see #getDefaultConfigLocations + */ +public abstract class AbstractRefreshableConfigApplicationContext extends AbstractRefreshableApplicationContext + implements BeanNameAware, InitializingBean { + + @Nullable + private String[] configLocations; + + private boolean setIdCalled = false; + + + /** + * Create a new AbstractRefreshableConfigApplicationContext with no parent. + */ + public AbstractRefreshableConfigApplicationContext() { + } + + /** + * Create a new AbstractRefreshableConfigApplicationContext with the given parent context. + * @param parent the parent context + */ + public AbstractRefreshableConfigApplicationContext(@Nullable ApplicationContext parent) { + super(parent); + } + + + /** + * Set the config locations for this application context in init-param style, + * i.e. with distinct locations separated by commas, semicolons or whitespace. + *

    If not set, the implementation may use a default as appropriate. + */ + public void setConfigLocation(String location) { + setConfigLocations(StringUtils.tokenizeToStringArray(location, CONFIG_LOCATION_DELIMITERS)); + } + + /** + * Set the config locations for this application context. + *

    If not set, the implementation may use a default as appropriate. + */ + public void setConfigLocations(@Nullable String... locations) { + if (locations != null) { + Assert.noNullElements(locations, "Config locations must not be null"); + this.configLocations = new String[locations.length]; + for (int i = 0; i < locations.length; i++) { + this.configLocations[i] = resolvePath(locations[i]).trim(); + } + } + else { + this.configLocations = null; + } + } + + /** + * Return an array of resource locations, referring to the XML bean definition + * files that this context should be built with. Can also include location + * patterns, which will get resolved via a ResourcePatternResolver. + *

    The default implementation returns {@code null}. Subclasses can override + * this to provide a set of resource locations to load bean definitions from. + * @return an array of resource locations, or {@code null} if none + * @see #getResources + * @see #getResourcePatternResolver + */ + @Nullable + protected String[] getConfigLocations() { + return (this.configLocations != null ? this.configLocations : getDefaultConfigLocations()); + } + + /** + * Return the default config locations to use, for the case where no + * explicit config locations have been specified. + *

    The default implementation returns {@code null}, + * requiring explicit config locations. + * @return an array of default config locations, if any + * @see #setConfigLocations + */ + @Nullable + protected String[] getDefaultConfigLocations() { + return null; + } + + /** + * Resolve the given path, replacing placeholders with corresponding + * environment property values if necessary. Applied to config locations. + * @param path the original file path + * @return the resolved file path + * @see org.springframework.core.env.Environment#resolveRequiredPlaceholders(String) + */ + protected String resolvePath(String path) { + return getEnvironment().resolveRequiredPlaceholders(path); + } + + + @Override + public void setId(String id) { + super.setId(id); + this.setIdCalled = true; + } + + /** + * Sets the id of this context to the bean name by default, + * for cases where the context instance is itself defined as a bean. + */ + @Override + public void setBeanName(String name) { + if (!this.setIdCalled) { + super.setId(name); + setDisplayName("ApplicationContext '" + name + "'"); + } + } + + /** + * Triggers {@link #refresh()} if not refreshed in the concrete context's + * constructor already. + */ + @Override + public void afterPropertiesSet() { + if (!isActive()) { + refresh(); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractResourceBasedMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/AbstractResourceBasedMessageSource.java new file mode 100644 index 0000000..37ffa85 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractResourceBasedMessageSource.java @@ -0,0 +1,254 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Set; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Abstract base class for {@code MessageSource} implementations based on + * resource bundle conventions, such as {@link ResourceBundleMessageSource} + * and {@link ReloadableResourceBundleMessageSource}. Provides common + * configuration methods and corresponding semantic definitions. + * + * @author Juergen Hoeller + * @since 4.3 + * @see ResourceBundleMessageSource + * @see ReloadableResourceBundleMessageSource + */ +public abstract class AbstractResourceBasedMessageSource extends AbstractMessageSource { + + private final Set basenameSet = new LinkedHashSet<>(4); + + @Nullable + private String defaultEncoding; + + private boolean fallbackToSystemLocale = true; + + @Nullable + private Locale defaultLocale; + + private long cacheMillis = -1; + + + /** + * Set a single basename, following the basic ResourceBundle convention + * of not specifying file extension or language codes. The resource location + * format is up to the specific {@code MessageSource} implementation. + *

    Regular and XMl properties files are supported: e.g. "messages" will find + * a "messages.properties", "messages_en.properties" etc arrangement as well + * as "messages.xml", "messages_en.xml" etc. + * @param basename the single basename + * @see #setBasenames + * @see org.springframework.core.io.ResourceEditor + * @see java.util.ResourceBundle + */ + public void setBasename(String basename) { + setBasenames(basename); + } + + /** + * Set an array of basenames, each following the basic ResourceBundle convention + * of not specifying file extension or language codes. The resource location + * format is up to the specific {@code MessageSource} implementation. + *

    Regular and XMl properties files are supported: e.g. "messages" will find + * a "messages.properties", "messages_en.properties" etc arrangement as well + * as "messages.xml", "messages_en.xml" etc. + *

    The associated resource bundles will be checked sequentially when resolving + * a message code. Note that message definitions in a previous resource + * bundle will override ones in a later bundle, due to the sequential lookup. + *

    Note: In contrast to {@link #addBasenames}, this replaces existing entries + * with the given names and can therefore also be used to reset the configuration. + * @param basenames an array of basenames + * @see #setBasename + * @see java.util.ResourceBundle + */ + public void setBasenames(String... basenames) { + this.basenameSet.clear(); + addBasenames(basenames); + } + + /** + * Add the specified basenames to the existing basename configuration. + *

    Note: If a given basename already exists, the position of its entry + * will remain as in the original set. New entries will be added at the + * end of the list, to be searched after existing basenames. + * @since 4.3 + * @see #setBasenames + * @see java.util.ResourceBundle + */ + public void addBasenames(String... basenames) { + if (!ObjectUtils.isEmpty(basenames)) { + for (String basename : basenames) { + Assert.hasText(basename, "Basename must not be empty"); + this.basenameSet.add(basename.trim()); + } + } + } + + /** + * Return this {@code MessageSource}'s basename set, containing entries + * in the order of registration. + *

    Calling code may introspect this set as well as add or remove entries. + * @since 4.3 + * @see #addBasenames + */ + public Set getBasenameSet() { + return this.basenameSet; + } + + /** + * Set the default charset to use for parsing properties files. + * Used if no file-specific charset is specified for a file. + *

    The effective default is the {@code java.util.Properties} + * default encoding: ISO-8859-1. A {@code null} value indicates + * the platform default encoding. + *

    Only applies to classic properties files, not to XML files. + * @param defaultEncoding the default charset + */ + public void setDefaultEncoding(@Nullable String defaultEncoding) { + this.defaultEncoding = defaultEncoding; + } + + /** + * Return the default charset to use for parsing properties files, if any. + * @since 4.3 + */ + @Nullable + protected String getDefaultEncoding() { + return this.defaultEncoding; + } + + /** + * Set whether to fall back to the system Locale if no files for a specific + * Locale have been found. Default is "true"; if this is turned off, the only + * fallback will be the default file (e.g. "messages.properties" for + * basename "messages"). + *

    Falling back to the system Locale is the default behavior of + * {@code java.util.ResourceBundle}. However, this is often not desirable + * in an application server environment, where the system Locale is not relevant + * to the application at all: set this flag to "false" in such a scenario. + * @see #setDefaultLocale + */ + public void setFallbackToSystemLocale(boolean fallbackToSystemLocale) { + this.fallbackToSystemLocale = fallbackToSystemLocale; + } + + /** + * Return whether to fall back to the system Locale if no files for a specific + * Locale have been found. + * @since 4.3 + * @deprecated as of 5.2.2, in favor of {@link #getDefaultLocale()} + */ + @Deprecated + protected boolean isFallbackToSystemLocale() { + return this.fallbackToSystemLocale; + } + + /** + * Specify a default Locale to fall back to, as an alternative to falling back + * to the system Locale. + *

    Default is to fall back to the system Locale. You may override this with + * a locally specified default Locale here, or enforce no fallback locale at all + * through disabling {@link #setFallbackToSystemLocale "fallbackToSystemLocale"}. + * @since 5.2.2 + * @see #setFallbackToSystemLocale + * @see #getDefaultLocale() + */ + public void setDefaultLocale(@Nullable Locale defaultLocale) { + this.defaultLocale = defaultLocale; + } + + /** + * Determine a default Locale to fall back to: either a locally specified default + * Locale or the system Locale, or {@code null} for no fallback locale at all. + * @since 5.2.2 + * @see #setDefaultLocale + * @see #setFallbackToSystemLocale + * @see Locale#getDefault() + */ + @Nullable + protected Locale getDefaultLocale() { + if (this.defaultLocale != null) { + return this.defaultLocale; + } + if (this.fallbackToSystemLocale) { + return Locale.getDefault(); + } + return null; + } + + /** + * Set the number of seconds to cache loaded properties files. + *

      + *
    • Default is "-1", indicating to cache forever (matching the default behavior + * of {@code java.util.ResourceBundle}). Note that this constant follows Spring + * conventions, not {@link java.util.ResourceBundle.Control#getTimeToLive}. + *
    • A positive number will cache loaded properties files for the given + * number of seconds. This is essentially the interval between refresh checks. + * Note that a refresh attempt will first check the last-modified timestamp + * of the file before actually reloading it; so if files don't change, this + * interval can be set rather low, as refresh attempts will not actually reload. + *
    • A value of "0" will check the last-modified timestamp of the file on + * every message access. Do not use this in a production environment! + *
    + *

    Note that depending on your ClassLoader, expiration might not work reliably + * since the ClassLoader may hold on to a cached version of the bundle file. + * Prefer {@link ReloadableResourceBundleMessageSource} over + * {@link ResourceBundleMessageSource} in such a scenario, in combination with + * a non-classpath location. + */ + public void setCacheSeconds(int cacheSeconds) { + this.cacheMillis = cacheSeconds * 1000L; + } + + /** + * Set the number of milliseconds to cache loaded properties files. + * Note that it is common to set seconds instead: {@link #setCacheSeconds}. + *

      + *
    • Default is "-1", indicating to cache forever (matching the default behavior + * of {@code java.util.ResourceBundle}). Note that this constant follows Spring + * conventions, not {@link java.util.ResourceBundle.Control#getTimeToLive}. + *
    • A positive number will cache loaded properties files for the given + * number of milliseconds. This is essentially the interval between refresh checks. + * Note that a refresh attempt will first check the last-modified timestamp + * of the file before actually reloading it; so if files don't change, this + * interval can be set rather low, as refresh attempts will not actually reload. + *
    • A value of "0" will check the last-modified timestamp of the file on + * every message access. Do not use this in a production environment! + *
    + * @since 4.3 + * @see #setCacheSeconds + */ + public void setCacheMillis(long cacheMillis) { + this.cacheMillis = cacheMillis; + } + + /** + * Return the number of milliseconds to cache loaded properties files. + * @since 4.3 + */ + protected long getCacheMillis() { + return this.cacheMillis; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractXmlApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/AbstractXmlApplicationContext.java new file mode 100644 index 0000000..85cb225 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractXmlApplicationContext.java @@ -0,0 +1,145 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.io.IOException; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.ResourceEntityResolver; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.context.ApplicationContext; +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; + +/** + * Convenient base class for {@link org.springframework.context.ApplicationContext} + * implementations, drawing configuration from XML documents containing bean definitions + * understood by an {@link org.springframework.beans.factory.xml.XmlBeanDefinitionReader}. + * + *

    Subclasses just have to implement the {@link #getConfigResources} and/or + * the {@link #getConfigLocations} method. Furthermore, they might override + * the {@link #getResourceByPath} hook to interpret relative paths in an + * environment-specific fashion, and/or {@link #getResourcePatternResolver} + * for extended pattern resolution. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #getConfigResources + * @see #getConfigLocations + * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader + */ +public abstract class AbstractXmlApplicationContext extends AbstractRefreshableConfigApplicationContext { + + private boolean validating = true; + + + /** + * Create a new AbstractXmlApplicationContext with no parent. + */ + public AbstractXmlApplicationContext() { + } + + /** + * Create a new AbstractXmlApplicationContext with the given parent context. + * @param parent the parent context + */ + public AbstractXmlApplicationContext(@Nullable ApplicationContext parent) { + super(parent); + } + + + /** + * Set whether to use XML validation. Default is {@code true}. + */ + public void setValidating(boolean validating) { + this.validating = validating; + } + + + /** + * Loads the bean definitions via an XmlBeanDefinitionReader. + * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader + * @see #initBeanDefinitionReader + * @see #loadBeanDefinitions + */ + @Override + protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException { + // Create a new XmlBeanDefinitionReader for the given BeanFactory. + XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory); + + // Configure the bean definition reader with this context's + // resource loading environment. + beanDefinitionReader.setEnvironment(this.getEnvironment()); + beanDefinitionReader.setResourceLoader(this); + beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this)); + + // Allow a subclass to provide custom initialization of the reader, + // then proceed with actually loading the bean definitions. + initBeanDefinitionReader(beanDefinitionReader); + loadBeanDefinitions(beanDefinitionReader); + } + + /** + * Initialize the bean definition reader used for loading the bean + * definitions of this context. Default implementation is empty. + *

    Can be overridden in subclasses, e.g. for turning off XML validation + * or using a different XmlBeanDefinitionParser implementation. + * @param reader the bean definition reader used by this context + * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader#setDocumentReaderClass + */ + protected void initBeanDefinitionReader(XmlBeanDefinitionReader reader) { + reader.setValidating(this.validating); + } + + /** + * Load the bean definitions with the given XmlBeanDefinitionReader. + *

    The lifecycle of the bean factory is handled by the {@link #refreshBeanFactory} + * method; hence this method is just supposed to load and/or register bean definitions. + * @param reader the XmlBeanDefinitionReader to use + * @throws BeansException in case of bean registration errors + * @throws IOException if the required XML document isn't found + * @see #refreshBeanFactory + * @see #getConfigLocations + * @see #getResources + * @see #getResourcePatternResolver + */ + protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException { + Resource[] configResources = getConfigResources(); + if (configResources != null) { + reader.loadBeanDefinitions(configResources); + } + String[] configLocations = getConfigLocations(); + if (configLocations != null) { + reader.loadBeanDefinitions(configLocations); + } + } + + /** + * Return an array of Resource objects, referring to the XML bean definition + * files that this context should be built with. + *

    The default implementation returns {@code null}. Subclasses can override + * this to provide pre-built Resource objects rather than location Strings. + * @return an array of Resource objects, or {@code null} if none + * @see #getConfigLocations() + */ + @Nullable + protected Resource[] getConfigResources() { + return null; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/ApplicationContextAwareProcessor.java b/spring-context/src/main/java/org/springframework/context/support/ApplicationContextAwareProcessor.java new file mode 100644 index 0000000..a58bb63 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/ApplicationContextAwareProcessor.java @@ -0,0 +1,132 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.security.AccessControlContext; +import java.security.AccessController; +import java.security.PrivilegedAction; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.config.EmbeddedValueResolver; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.context.ApplicationStartupAware; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.EmbeddedValueResolverAware; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.MessageSourceAware; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.lang.Nullable; +import org.springframework.util.StringValueResolver; + +/** + * {@link BeanPostProcessor} implementation that supplies the {@code ApplicationContext}, + * {@link org.springframework.core.env.Environment Environment}, or + * {@link StringValueResolver} for the {@code ApplicationContext} to beans that + * implement the {@link EnvironmentAware}, {@link EmbeddedValueResolverAware}, + * {@link ResourceLoaderAware}, {@link ApplicationEventPublisherAware}, + * {@link MessageSourceAware}, and/or {@link ApplicationContextAware} interfaces. + * + *

    Implemented interfaces are satisfied in the order in which they are + * mentioned above. + * + *

    Application contexts will automatically register this with their + * underlying bean factory. Applications do not use this directly. + * + * @author Juergen Hoeller + * @author Costin Leau + * @author Chris Beams + * @since 10.10.2003 + * @see org.springframework.context.EnvironmentAware + * @see org.springframework.context.EmbeddedValueResolverAware + * @see org.springframework.context.ResourceLoaderAware + * @see org.springframework.context.ApplicationEventPublisherAware + * @see org.springframework.context.MessageSourceAware + * @see org.springframework.context.ApplicationContextAware + * @see org.springframework.context.support.AbstractApplicationContext#refresh() + */ +class ApplicationContextAwareProcessor implements BeanPostProcessor { + + private final ConfigurableApplicationContext applicationContext; + + private final StringValueResolver embeddedValueResolver; + + + /** + * Create a new ApplicationContextAwareProcessor for the given context. + */ + public ApplicationContextAwareProcessor(ConfigurableApplicationContext applicationContext) { + this.applicationContext = applicationContext; + this.embeddedValueResolver = new EmbeddedValueResolver(applicationContext.getBeanFactory()); + } + + + @Override + @Nullable + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (!(bean instanceof EnvironmentAware || bean instanceof EmbeddedValueResolverAware || + bean instanceof ResourceLoaderAware || bean instanceof ApplicationEventPublisherAware || + bean instanceof MessageSourceAware || bean instanceof ApplicationContextAware || + bean instanceof ApplicationStartupAware)) { + return bean; + } + + AccessControlContext acc = null; + + if (System.getSecurityManager() != null) { + acc = this.applicationContext.getBeanFactory().getAccessControlContext(); + } + + if (acc != null) { + AccessController.doPrivileged((PrivilegedAction) () -> { + invokeAwareInterfaces(bean); + return null; + }, acc); + } + else { + invokeAwareInterfaces(bean); + } + + return bean; + } + + private void invokeAwareInterfaces(Object bean) { + if (bean instanceof EnvironmentAware) { + ((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment()); + } + if (bean instanceof EmbeddedValueResolverAware) { + ((EmbeddedValueResolverAware) bean).setEmbeddedValueResolver(this.embeddedValueResolver); + } + if (bean instanceof ResourceLoaderAware) { + ((ResourceLoaderAware) bean).setResourceLoader(this.applicationContext); + } + if (bean instanceof ApplicationEventPublisherAware) { + ((ApplicationEventPublisherAware) bean).setApplicationEventPublisher(this.applicationContext); + } + if (bean instanceof MessageSourceAware) { + ((MessageSourceAware) bean).setMessageSource(this.applicationContext); + } + if (bean instanceof ApplicationStartupAware) { + ((ApplicationStartupAware) bean).setApplicationStartup(this.applicationContext.getApplicationStartup()); + } + if (bean instanceof ApplicationContextAware) { + ((ApplicationContextAware) bean).setApplicationContext(this.applicationContext); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/ApplicationListenerDetector.java b/spring-context/src/main/java/org/springframework/context/support/ApplicationListenerDetector.java new file mode 100644 index 0000000..d519b98 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/ApplicationListenerDetector.java @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor; +import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ApplicationEventMulticaster; +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +/** + * {@code BeanPostProcessor} that detects beans which implement the {@code ApplicationListener} + * interface. This catches beans that can't reliably be detected by {@code getBeanNamesForType} + * and related operations which only work against top-level beans. + * + *

    With standard Java serialization, this post-processor won't get serialized as part of + * {@code DisposableBeanAdapter} to begin with. However, with alternative serialization + * mechanisms, {@code DisposableBeanAdapter.writeReplace} might not get used at all, so we + * defensively mark this post-processor's field state as {@code transient}. + * + * @author Juergen Hoeller + * @since 4.3.4 + */ +class ApplicationListenerDetector implements DestructionAwareBeanPostProcessor, MergedBeanDefinitionPostProcessor { + + private static final Log logger = LogFactory.getLog(ApplicationListenerDetector.class); + + private final transient AbstractApplicationContext applicationContext; + + private final transient Map singletonNames = new ConcurrentHashMap<>(256); + + + public ApplicationListenerDetector(AbstractApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + + @Override + public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class beanType, String beanName) { + if (ApplicationListener.class.isAssignableFrom(beanType)) { + this.singletonNames.put(beanName, beanDefinition.isSingleton()); + } + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) { + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + if (bean instanceof ApplicationListener) { + // potentially not detected as a listener by getBeanNamesForType retrieval + Boolean flag = this.singletonNames.get(beanName); + if (Boolean.TRUE.equals(flag)) { + // singleton bean (top-level or inner): register on the fly + this.applicationContext.addApplicationListener((ApplicationListener) bean); + } + else if (Boolean.FALSE.equals(flag)) { + if (logger.isWarnEnabled() && !this.applicationContext.containsBean(beanName)) { + // inner bean with other scope - can't reliably process events + logger.warn("Inner bean '" + beanName + "' implements ApplicationListener interface " + + "but is not reachable for event multicasting by its containing ApplicationContext " + + "because it does not have singleton scope. Only top-level listener beans are allowed " + + "to be of non-singleton scope."); + } + this.singletonNames.remove(beanName); + } + } + return bean; + } + + @Override + public void postProcessBeforeDestruction(Object bean, String beanName) { + if (bean instanceof ApplicationListener) { + try { + ApplicationEventMulticaster multicaster = this.applicationContext.getApplicationEventMulticaster(); + multicaster.removeApplicationListener((ApplicationListener) bean); + multicaster.removeApplicationListenerBean(beanName); + } + catch (IllegalStateException ex) { + // ApplicationEventMulticaster not initialized yet - no need to remove a listener + } + } + } + + @Override + public boolean requiresDestruction(Object bean) { + return (bean instanceof ApplicationListener); + } + + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof ApplicationListenerDetector && + this.applicationContext == ((ApplicationListenerDetector) other).applicationContext)); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.applicationContext); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/ApplicationObjectSupport.java b/spring-context/src/main/java/org/springframework/context/support/ApplicationObjectSupport.java new file mode 100644 index 0000000..a251932 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/ApplicationObjectSupport.java @@ -0,0 +1,178 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationContextException; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Convenient superclass for application objects that want to be aware of + * the application context, e.g. for custom lookup of collaborating beans + * or for context-specific resource access. It saves the application + * context reference and provides an initialization callback method. + * Furthermore, it offers numerous convenience methods for message lookup. + * + *

    There is no requirement to subclass this class: It just makes things + * a little easier if you need access to the context, e.g. for access to + * file resources or to the message source. Note that many application + * objects do not need to be aware of the application context at all, + * as they can receive collaborating beans via bean references. + * + *

    Many framework classes are derived from this class, particularly + * within the web support. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see org.springframework.web.context.support.WebApplicationObjectSupport + */ +public abstract class ApplicationObjectSupport implements ApplicationContextAware { + + /** Logger that is available to subclasses. */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** ApplicationContext this object runs in. */ + @Nullable + private ApplicationContext applicationContext; + + /** MessageSourceAccessor for easy message access. */ + @Nullable + private MessageSourceAccessor messageSourceAccessor; + + + @Override + public final void setApplicationContext(@Nullable ApplicationContext context) throws BeansException { + if (context == null && !isContextRequired()) { + // Reset internal context state. + this.applicationContext = null; + this.messageSourceAccessor = null; + } + else if (this.applicationContext == null) { + // Initialize with passed-in context. + if (!requiredContextClass().isInstance(context)) { + throw new ApplicationContextException( + "Invalid application context: needs to be of type [" + requiredContextClass().getName() + "]"); + } + this.applicationContext = context; + this.messageSourceAccessor = new MessageSourceAccessor(context); + initApplicationContext(context); + } + else { + // Ignore reinitialization if same context passed in. + if (this.applicationContext != context) { + throw new ApplicationContextException( + "Cannot reinitialize with different application context: current one is [" + + this.applicationContext + "], passed-in one is [" + context + "]"); + } + } + } + + /** + * Determine whether this application object needs to run in an ApplicationContext. + *

    Default is "false". Can be overridden to enforce running in a context + * (i.e. to throw IllegalStateException on accessors if outside a context). + * @see #getApplicationContext + * @see #getMessageSourceAccessor + */ + protected boolean isContextRequired() { + return false; + } + + /** + * Determine the context class that any context passed to + * {@code setApplicationContext} must be an instance of. + * Can be overridden in subclasses. + * @see #setApplicationContext + */ + protected Class requiredContextClass() { + return ApplicationContext.class; + } + + /** + * Subclasses can override this for custom initialization behavior. + * Gets called by {@code setApplicationContext} after setting the context instance. + *

    Note: Does not get called on re-initialization of the context + * but rather just on first initialization of this object's context reference. + *

    The default implementation calls the overloaded {@link #initApplicationContext()} + * method without ApplicationContext reference. + * @param context the containing ApplicationContext + * @throws ApplicationContextException in case of initialization errors + * @throws BeansException if thrown by ApplicationContext methods + * @see #setApplicationContext + */ + protected void initApplicationContext(ApplicationContext context) throws BeansException { + initApplicationContext(); + } + + /** + * Subclasses can override this for custom initialization behavior. + *

    The default implementation is empty. Called by + * {@link #initApplicationContext(org.springframework.context.ApplicationContext)}. + * @throws ApplicationContextException in case of initialization errors + * @throws BeansException if thrown by ApplicationContext methods + * @see #setApplicationContext + */ + protected void initApplicationContext() throws BeansException { + } + + + /** + * Return the ApplicationContext that this object is associated with. + * @throws IllegalStateException if not running in an ApplicationContext + */ + @Nullable + public final ApplicationContext getApplicationContext() throws IllegalStateException { + if (this.applicationContext == null && isContextRequired()) { + throw new IllegalStateException( + "ApplicationObjectSupport instance [" + this + "] does not run in an ApplicationContext"); + } + return this.applicationContext; + } + + /** + * Obtain the ApplicationContext for actual use. + * @return the ApplicationContext (never {@code null}) + * @throws IllegalStateException in case of no ApplicationContext set + * @since 5.0 + */ + protected final ApplicationContext obtainApplicationContext() { + ApplicationContext applicationContext = getApplicationContext(); + Assert.state(applicationContext != null, "No ApplicationContext"); + return applicationContext; + } + + /** + * Return a MessageSourceAccessor for the application context + * used by this object, for easy message access. + * @throws IllegalStateException if not running in an ApplicationContext + */ + @Nullable + protected final MessageSourceAccessor getMessageSourceAccessor() throws IllegalStateException { + if (this.messageSourceAccessor == null && isContextRequired()) { + throw new IllegalStateException( + "ApplicationObjectSupport instance [" + this + "] does not run in an ApplicationContext"); + } + return this.messageSourceAccessor; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/ClassPathXmlApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/ClassPathXmlApplicationContext.java new file mode 100644 index 0000000..9b72875 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/ClassPathXmlApplicationContext.java @@ -0,0 +1,212 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Standalone XML application context, taking the context definition files + * from the class path, interpreting plain paths as class path resource names + * that include the package path (e.g. "mypackage/myresource.txt"). Useful for + * test harnesses as well as for application contexts embedded within JARs. + * + *

    The config location defaults can be overridden via {@link #getConfigLocations}, + * Config locations can either denote concrete files like "/myfiles/context.xml" + * or Ant-style patterns like "/myfiles/*-context.xml" (see the + * {@link org.springframework.util.AntPathMatcher} javadoc for pattern details). + * + *

    Note: In case of multiple config locations, later bean definitions will + * override ones defined in earlier loaded files. This can be leveraged to + * deliberately override certain bean definitions via an extra XML file. + * + *

    This is a simple, one-stop shop convenience ApplicationContext. + * Consider using the {@link GenericApplicationContext} class in combination + * with an {@link org.springframework.beans.factory.xml.XmlBeanDefinitionReader} + * for more flexible context setup. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #getResource + * @see #getResourceByPath + * @see GenericApplicationContext + */ +public class ClassPathXmlApplicationContext extends AbstractXmlApplicationContext { + + @Nullable + private Resource[] configResources; + + + /** + * Create a new ClassPathXmlApplicationContext for bean-style configuration. + * @see #setConfigLocation + * @see #setConfigLocations + * @see #afterPropertiesSet() + */ + public ClassPathXmlApplicationContext() { + } + + /** + * Create a new ClassPathXmlApplicationContext for bean-style configuration. + * @param parent the parent context + * @see #setConfigLocation + * @see #setConfigLocations + * @see #afterPropertiesSet() + */ + public ClassPathXmlApplicationContext(ApplicationContext parent) { + super(parent); + } + + /** + * Create a new ClassPathXmlApplicationContext, loading the definitions + * from the given XML file and automatically refreshing the context. + * @param configLocation resource location + * @throws BeansException if context creation failed + */ + public ClassPathXmlApplicationContext(String configLocation) throws BeansException { + this(new String[] {configLocation}, true, null); + } + + /** + * Create a new ClassPathXmlApplicationContext, loading the definitions + * from the given XML files and automatically refreshing the context. + * @param configLocations array of resource locations + * @throws BeansException if context creation failed + */ + public ClassPathXmlApplicationContext(String... configLocations) throws BeansException { + this(configLocations, true, null); + } + + /** + * Create a new ClassPathXmlApplicationContext with the given parent, + * loading the definitions from the given XML files and automatically + * refreshing the context. + * @param configLocations array of resource locations + * @param parent the parent context + * @throws BeansException if context creation failed + */ + public ClassPathXmlApplicationContext(String[] configLocations, @Nullable ApplicationContext parent) + throws BeansException { + + this(configLocations, true, parent); + } + + /** + * Create a new ClassPathXmlApplicationContext, loading the definitions + * from the given XML files. + * @param configLocations array of resource locations + * @param refresh whether to automatically refresh the context, + * loading all bean definitions and creating all singletons. + * Alternatively, call refresh manually after further configuring the context. + * @throws BeansException if context creation failed + * @see #refresh() + */ + public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh) throws BeansException { + this(configLocations, refresh, null); + } + + /** + * Create a new ClassPathXmlApplicationContext with the given parent, + * loading the definitions from the given XML files. + * @param configLocations array of resource locations + * @param refresh whether to automatically refresh the context, + * loading all bean definitions and creating all singletons. + * Alternatively, call refresh manually after further configuring the context. + * @param parent the parent context + * @throws BeansException if context creation failed + * @see #refresh() + */ + public ClassPathXmlApplicationContext( + String[] configLocations, boolean refresh, @Nullable ApplicationContext parent) + throws BeansException { + + super(parent); + setConfigLocations(configLocations); + if (refresh) { + refresh(); + } + } + + + /** + * Create a new ClassPathXmlApplicationContext, loading the definitions + * from the given XML file and automatically refreshing the context. + *

    This is a convenience method to load class path resources relative to a + * given Class. For full flexibility, consider using a GenericApplicationContext + * with an XmlBeanDefinitionReader and a ClassPathResource argument. + * @param path relative (or absolute) path within the class path + * @param clazz the class to load resources with (basis for the given paths) + * @throws BeansException if context creation failed + * @see org.springframework.core.io.ClassPathResource#ClassPathResource(String, Class) + * @see org.springframework.context.support.GenericApplicationContext + * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader + */ + public ClassPathXmlApplicationContext(String path, Class clazz) throws BeansException { + this(new String[] {path}, clazz); + } + + /** + * Create a new ClassPathXmlApplicationContext, loading the definitions + * from the given XML files and automatically refreshing the context. + * @param paths array of relative (or absolute) paths within the class path + * @param clazz the class to load resources with (basis for the given paths) + * @throws BeansException if context creation failed + * @see org.springframework.core.io.ClassPathResource#ClassPathResource(String, Class) + * @see org.springframework.context.support.GenericApplicationContext + * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader + */ + public ClassPathXmlApplicationContext(String[] paths, Class clazz) throws BeansException { + this(paths, clazz, null); + } + + /** + * Create a new ClassPathXmlApplicationContext with the given parent, + * loading the definitions from the given XML files and automatically + * refreshing the context. + * @param paths array of relative (or absolute) paths within the class path + * @param clazz the class to load resources with (basis for the given paths) + * @param parent the parent context + * @throws BeansException if context creation failed + * @see org.springframework.core.io.ClassPathResource#ClassPathResource(String, Class) + * @see org.springframework.context.support.GenericApplicationContext + * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader + */ + public ClassPathXmlApplicationContext(String[] paths, Class clazz, @Nullable ApplicationContext parent) + throws BeansException { + + super(parent); + Assert.notNull(paths, "Path array must not be null"); + Assert.notNull(clazz, "Class argument must not be null"); + this.configResources = new Resource[paths.length]; + for (int i = 0; i < paths.length; i++) { + this.configResources[i] = new ClassPathResource(paths[i], clazz); + } + refresh(); + } + + + @Override + @Nullable + protected Resource[] getConfigResources() { + return this.configResources; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/ContextTypeMatchClassLoader.java b/spring-context/src/main/java/org/springframework/context/support/ContextTypeMatchClassLoader.java new file mode 100644 index 0000000..30e44b5 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/ContextTypeMatchClassLoader.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.core.DecoratingClassLoader; +import org.springframework.core.OverridingClassLoader; +import org.springframework.core.SmartClassLoader; +import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; + +/** + * Special variant of an overriding ClassLoader, used for temporary type + * matching in {@link AbstractApplicationContext}. Redefines classes from + * a cached byte array for every {@code loadClass} call in order to + * pick up recently loaded types in the parent ClassLoader. + * + * @author Juergen Hoeller + * @since 2.5 + * @see AbstractApplicationContext + * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#setTempClassLoader + */ +class ContextTypeMatchClassLoader extends DecoratingClassLoader implements SmartClassLoader { + + static { + ClassLoader.registerAsParallelCapable(); + } + + + private static Method findLoadedClassMethod; + + static { + try { + findLoadedClassMethod = ClassLoader.class.getDeclaredMethod("findLoadedClass", String.class); + } + catch (NoSuchMethodException ex) { + throw new IllegalStateException("Invalid [java.lang.ClassLoader] class: no 'findLoadedClass' method defined!"); + } + } + + + /** Cache for byte array per class name. */ + private final Map bytesCache = new ConcurrentHashMap<>(256); + + + public ContextTypeMatchClassLoader(@Nullable ClassLoader parent) { + super(parent); + } + + @Override + public Class loadClass(String name) throws ClassNotFoundException { + return new ContextOverridingClassLoader(getParent()).loadClass(name); + } + + @Override + public boolean isClassReloadable(Class clazz) { + return (clazz.getClassLoader() instanceof ContextOverridingClassLoader); + } + + + /** + * ClassLoader to be created for each loaded class. + * Caches class file content but redefines class for each call. + */ + private class ContextOverridingClassLoader extends OverridingClassLoader { + + public ContextOverridingClassLoader(ClassLoader parent) { + super(parent); + } + + @Override + protected boolean isEligibleForOverriding(String className) { + if (isExcluded(className) || ContextTypeMatchClassLoader.this.isExcluded(className)) { + return false; + } + ReflectionUtils.makeAccessible(findLoadedClassMethod); + ClassLoader parent = getParent(); + while (parent != null) { + if (ReflectionUtils.invokeMethod(findLoadedClassMethod, parent, className) != null) { + return false; + } + parent = parent.getParent(); + } + return true; + } + + @Override + protected Class loadClassForOverriding(String name) throws ClassNotFoundException { + byte[] bytes = bytesCache.get(name); + if (bytes == null) { + bytes = loadBytesForClass(name); + if (bytes != null) { + bytesCache.put(name, bytes); + } + else { + return null; + } + } + return defineClass(name, bytes, 0, bytes.length); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/ConversionServiceFactoryBean.java b/spring-context/src/main/java/org/springframework/context/support/ConversionServiceFactoryBean.java new file mode 100644 index 0000000..f04f33d --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/ConversionServiceFactoryBean.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.util.Set; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.ConversionServiceFactory; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.lang.Nullable; + +/** + * A factory providing convenient access to a ConversionService configured with + * converters appropriate for most environments. Set the + * {@link #setConverters "converters"} property to supplement the default converters. + * + *

    This implementation creates a {@link DefaultConversionService}. + * Subclasses may override {@link #createConversionService()} in order to return + * a {@link GenericConversionService} instance of their choosing. + * + *

    Like all {@code FactoryBean} implementations, this class is suitable for + * use when configuring a Spring application context using Spring {@code } + * XML. When configuring the container with + * {@link org.springframework.context.annotation.Configuration @Configuration} + * classes, simply instantiate, configure and return the appropriate + * {@code ConversionService} object from a {@link + * org.springframework.context.annotation.Bean @Bean} method. + * + * @author Keith Donald + * @author Juergen Hoeller + * @author Chris Beams + * @since 3.0 + */ +public class ConversionServiceFactoryBean implements FactoryBean, InitializingBean { + + @Nullable + private Set converters; + + @Nullable + private GenericConversionService conversionService; + + + /** + * Configure the set of custom converter objects that should be added: + * implementing {@link org.springframework.core.convert.converter.Converter}, + * {@link org.springframework.core.convert.converter.ConverterFactory}, + * or {@link org.springframework.core.convert.converter.GenericConverter}. + */ + public void setConverters(Set converters) { + this.converters = converters; + } + + @Override + public void afterPropertiesSet() { + this.conversionService = createConversionService(); + ConversionServiceFactory.registerConverters(this.converters, this.conversionService); + } + + /** + * Create the ConversionService instance returned by this factory bean. + *

    Creates a simple {@link GenericConversionService} instance by default. + * Subclasses may override to customize the ConversionService instance that + * gets created. + */ + protected GenericConversionService createConversionService() { + return new DefaultConversionService(); + } + + + // implementing FactoryBean + + @Override + @Nullable + public ConversionService getObject() { + return this.conversionService; + } + + @Override + public Class getObjectType() { + return GenericConversionService.class; + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java b/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java new file mode 100644 index 0000000..a269d5a --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java @@ -0,0 +1,417 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ApplicationContextException; +import org.springframework.context.Lifecycle; +import org.springframework.context.LifecycleProcessor; +import org.springframework.context.Phased; +import org.springframework.context.SmartLifecycle; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Default implementation of the {@link LifecycleProcessor} strategy. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @since 3.0 + */ +public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactoryAware { + + private final Log logger = LogFactory.getLog(getClass()); + + private volatile long timeoutPerShutdownPhase = 30000; + + private volatile boolean running; + + @Nullable + private volatile ConfigurableListableBeanFactory beanFactory; + + + /** + * Specify the maximum time allotted in milliseconds for the shutdown of + * any phase (group of SmartLifecycle beans with the same 'phase' value). + *

    The default value is 30 seconds. + */ + public void setTimeoutPerShutdownPhase(long timeoutPerShutdownPhase) { + this.timeoutPerShutdownPhase = timeoutPerShutdownPhase; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + if (!(beanFactory instanceof ConfigurableListableBeanFactory)) { + throw new IllegalArgumentException( + "DefaultLifecycleProcessor requires a ConfigurableListableBeanFactory: " + beanFactory); + } + this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; + } + + private ConfigurableListableBeanFactory getBeanFactory() { + ConfigurableListableBeanFactory beanFactory = this.beanFactory; + Assert.state(beanFactory != null, "No BeanFactory available"); + return beanFactory; + } + + + // Lifecycle implementation + + /** + * Start all registered beans that implement {@link Lifecycle} and are not + * already running. Any bean that implements {@link SmartLifecycle} will be + * started within its 'phase', and all phases will be ordered from lowest to + * highest value. All beans that do not implement {@link SmartLifecycle} will be + * started in the default phase 0. A bean declared as a dependency of another bean + * will be started before the dependent bean regardless of the declared phase. + */ + @Override + public void start() { + startBeans(false); + this.running = true; + } + + /** + * Stop all registered beans that implement {@link Lifecycle} and are + * currently running. Any bean that implements {@link SmartLifecycle} will be + * stopped within its 'phase', and all phases will be ordered from highest to + * lowest value. All beans that do not implement {@link SmartLifecycle} will be + * stopped in the default phase 0. A bean declared as dependent on another bean + * will be stopped before the dependency bean regardless of the declared phase. + */ + @Override + public void stop() { + stopBeans(); + this.running = false; + } + + @Override + public void onRefresh() { + startBeans(true); + this.running = true; + } + + @Override + public void onClose() { + stopBeans(); + this.running = false; + } + + @Override + public boolean isRunning() { + return this.running; + } + + + // Internal helpers + + private void startBeans(boolean autoStartupOnly) { + Map lifecycleBeans = getLifecycleBeans(); + Map phases = new TreeMap<>(); + + 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) + ).add(beanName, bean); + } + }); + if (!phases.isEmpty()) { + phases.values().forEach(LifecycleGroup::start); + } + } + + /** + * Start the specified bean as part of the given set of Lifecycle beans, + * making sure that any beans that it depends on are started first. + * @param lifecycleBeans a Map with bean name as key and Lifecycle instance as value + * @param beanName the name of the bean to start + */ + private void doStart(Map lifecycleBeans, String beanName, boolean autoStartupOnly) { + Lifecycle bean = lifecycleBeans.remove(beanName); + if (bean != null && bean != this) { + String[] dependenciesForBean = getBeanFactory().getDependenciesForBean(beanName); + for (String dependency : dependenciesForBean) { + doStart(lifecycleBeans, dependency, autoStartupOnly); + } + if (!bean.isRunning() && + (!autoStartupOnly || !(bean instanceof SmartLifecycle) || ((SmartLifecycle) bean).isAutoStartup())) { + if (logger.isTraceEnabled()) { + logger.trace("Starting bean '" + beanName + "' of type [" + bean.getClass().getName() + "]"); + } + try { + bean.start(); + } + catch (Throwable ex) { + throw new ApplicationContextException("Failed to start bean '" + beanName + "'", ex); + } + if (logger.isDebugEnabled()) { + logger.debug("Successfully started bean '" + beanName + "'"); + } + } + } + } + + private void stopBeans() { + Map lifecycleBeans = getLifecycleBeans(); + Map phases = new HashMap<>(); + lifecycleBeans.forEach((beanName, bean) -> { + int shutdownPhase = getPhase(bean); + LifecycleGroup group = phases.get(shutdownPhase); + if (group == null) { + group = new LifecycleGroup(shutdownPhase, this.timeoutPerShutdownPhase, lifecycleBeans, false); + phases.put(shutdownPhase, group); + } + group.add(beanName, bean); + }); + if (!phases.isEmpty()) { + List keys = new ArrayList<>(phases.keySet()); + keys.sort(Collections.reverseOrder()); + for (Integer key : keys) { + phases.get(key).stop(); + } + } + } + + /** + * Stop the specified bean as part of the given set of Lifecycle beans, + * making sure that any beans that depends on it are stopped first. + * @param lifecycleBeans a Map with bean name as key and Lifecycle instance as value + * @param beanName the name of the bean to stop + */ + private void doStop(Map lifecycleBeans, final String beanName, + final CountDownLatch latch, final Set countDownBeanNames) { + + Lifecycle bean = lifecycleBeans.remove(beanName); + if (bean != null) { + String[] dependentBeans = getBeanFactory().getDependentBeans(beanName); + for (String dependentBean : dependentBeans) { + doStop(lifecycleBeans, dependentBean, latch, countDownBeanNames); + } + try { + if (bean.isRunning()) { + if (bean instanceof SmartLifecycle) { + if (logger.isTraceEnabled()) { + logger.trace("Asking bean '" + beanName + "' of type [" + + bean.getClass().getName() + "] to stop"); + } + countDownBeanNames.add(beanName); + ((SmartLifecycle) bean).stop(() -> { + latch.countDown(); + countDownBeanNames.remove(beanName); + if (logger.isDebugEnabled()) { + logger.debug("Bean '" + beanName + "' completed its stop procedure"); + } + }); + } + else { + if (logger.isTraceEnabled()) { + logger.trace("Stopping bean '" + beanName + "' of type [" + + bean.getClass().getName() + "]"); + } + bean.stop(); + if (logger.isDebugEnabled()) { + logger.debug("Successfully stopped bean '" + beanName + "'"); + } + } + } + else if (bean instanceof SmartLifecycle) { + // Don't wait for beans that aren't running... + latch.countDown(); + } + } + catch (Throwable ex) { + if (logger.isWarnEnabled()) { + logger.warn("Failed to stop bean '" + beanName + "'", ex); + } + } + } + } + + + // overridable hooks + + /** + * Retrieve all applicable Lifecycle beans: all singletons that have already been created, + * as well as all SmartLifecycle beans (even if they are marked as lazy-init). + * @return the Map of applicable beans, with bean names as keys and bean instances as values + */ + protected Map getLifecycleBeans() { + ConfigurableListableBeanFactory beanFactory = getBeanFactory(); + Map beans = new LinkedHashMap<>(); + String[] beanNames = beanFactory.getBeanNamesForType(Lifecycle.class, false, false); + for (String beanName : beanNames) { + String beanNameToRegister = BeanFactoryUtils.transformedBeanName(beanName); + boolean isFactoryBean = beanFactory.isFactoryBean(beanNameToRegister); + String beanNameToCheck = (isFactoryBean ? BeanFactory.FACTORY_BEAN_PREFIX + beanName : beanName); + if ((beanFactory.containsSingleton(beanNameToRegister) && + (!isFactoryBean || matchesBeanType(Lifecycle.class, beanNameToCheck, beanFactory))) || + matchesBeanType(SmartLifecycle.class, beanNameToCheck, beanFactory)) { + Object bean = beanFactory.getBean(beanNameToCheck); + if (bean != this && bean instanceof Lifecycle) { + beans.put(beanNameToRegister, (Lifecycle) bean); + } + } + } + return beans; + } + + private boolean matchesBeanType(Class targetType, String beanName, BeanFactory beanFactory) { + Class beanType = beanFactory.getType(beanName); + return (beanType != null && targetType.isAssignableFrom(beanType)); + } + + /** + * Determine the lifecycle phase of the given bean. + *

    The default implementation checks for the {@link Phased} interface, using + * a default of 0 otherwise. Can be overridden to apply other/further policies. + * @param bean the bean to introspect + * @return the phase (an integer value) + * @see Phased#getPhase() + * @see SmartLifecycle + */ + protected int getPhase(Lifecycle bean) { + return (bean instanceof Phased ? ((Phased) bean).getPhase() : 0); + } + + + /** + * 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). + */ + private class LifecycleGroup { + + private final int phase; + + private final long timeout; + + private final Map lifecycleBeans; + + private final boolean autoStartupOnly; + + private final List members = new ArrayList<>(); + + private int smartMemberCount; + + public LifecycleGroup( + int phase, long timeout, Map lifecycleBeans, boolean autoStartupOnly) { + + this.phase = phase; + this.timeout = timeout; + this.lifecycleBeans = lifecycleBeans; + this.autoStartupOnly = autoStartupOnly; + } + + public void add(String name, Lifecycle bean) { + this.members.add(new LifecycleGroupMember(name, bean)); + if (bean instanceof SmartLifecycle) { + this.smartMemberCount++; + } + } + + public void start() { + if (this.members.isEmpty()) { + return; + } + if (logger.isDebugEnabled()) { + logger.debug("Starting beans in phase " + this.phase); + } + Collections.sort(this.members); + for (LifecycleGroupMember member : this.members) { + doStart(this.lifecycleBeans, member.name, this.autoStartupOnly); + } + } + + public void stop() { + if (this.members.isEmpty()) { + return; + } + if (logger.isDebugEnabled()) { + logger.debug("Stopping beans in phase " + this.phase); + } + this.members.sort(Collections.reverseOrder()); + CountDownLatch latch = new CountDownLatch(this.smartMemberCount); + Set countDownBeanNames = Collections.synchronizedSet(new LinkedHashSet<>()); + Set lifecycleBeanNames = new HashSet<>(this.lifecycleBeans.keySet()); + for (LifecycleGroupMember member : this.members) { + if (lifecycleBeanNames.contains(member.name)) { + doStop(this.lifecycleBeans, member.name, latch, countDownBeanNames); + } + else if (member.bean instanceof SmartLifecycle) { + // Already removed: must have been a dependent bean from another phase + latch.countDown(); + } + } + try { + latch.await(this.timeout, TimeUnit.MILLISECONDS); + if (latch.getCount() > 0 && !countDownBeanNames.isEmpty() && logger.isInfoEnabled()) { + logger.info("Failed to shut down " + countDownBeanNames.size() + " bean" + + (countDownBeanNames.size() > 1 ? "s" : "") + " with phase value " + + this.phase + " within timeout of " + this.timeout + "ms: " + countDownBeanNames); + } + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + + + /** + * Adapts the Comparable interface onto the lifecycle phase model. + */ + private class LifecycleGroupMember implements Comparable { + + private final String name; + + private final Lifecycle bean; + + LifecycleGroupMember(String name, Lifecycle bean) { + this.name = name; + this.bean = bean; + } + + @Override + public int compareTo(LifecycleGroupMember other) { + int thisPhase = getPhase(this.bean); + int otherPhase = getPhase(other.bean); + return Integer.compare(thisPhase, otherPhase); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/DefaultMessageSourceResolvable.java b/spring-context/src/main/java/org/springframework/context/support/DefaultMessageSourceResolvable.java new file mode 100644 index 0000000..3342044 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/DefaultMessageSourceResolvable.java @@ -0,0 +1,194 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.io.Serializable; + +import org.springframework.context.MessageSourceResolvable; +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Spring's default implementation of the {@link MessageSourceResolvable} interface. + * Offers an easy way to store all the necessary values needed to resolve + * a message via a {@link org.springframework.context.MessageSource}. + * + * @author Juergen Hoeller + * @since 13.02.2004 + * @see org.springframework.context.MessageSource#getMessage(MessageSourceResolvable, java.util.Locale) + */ +@SuppressWarnings("serial") +public class DefaultMessageSourceResolvable implements MessageSourceResolvable, Serializable { + + @Nullable + private final String[] codes; + + @Nullable + private final Object[] arguments; + + @Nullable + private final String defaultMessage; + + + /** + * Create a new DefaultMessageSourceResolvable. + * @param code the code to be used to resolve this message + */ + public DefaultMessageSourceResolvable(String code) { + this(new String[] {code}, null, null); + } + + /** + * Create a new DefaultMessageSourceResolvable. + * @param codes the codes to be used to resolve this message + */ + public DefaultMessageSourceResolvable(String[] codes) { + this(codes, null, null); + } + + /** + * Create a new DefaultMessageSourceResolvable. + * @param codes the codes to be used to resolve this message + * @param defaultMessage the default message to be used to resolve this message + */ + public DefaultMessageSourceResolvable(String[] codes, String defaultMessage) { + this(codes, null, defaultMessage); + } + + /** + * Create a new DefaultMessageSourceResolvable. + * @param codes the codes to be used to resolve this message + * @param arguments the array of arguments to be used to resolve this message + */ + public DefaultMessageSourceResolvable(String[] codes, Object[] arguments) { + this(codes, arguments, null); + } + + /** + * Create a new DefaultMessageSourceResolvable. + * @param codes the codes to be used to resolve this message + * @param arguments the array of arguments to be used to resolve this message + * @param defaultMessage the default message to be used to resolve this message + */ + public DefaultMessageSourceResolvable( + @Nullable String[] codes, @Nullable Object[] arguments, @Nullable String defaultMessage) { + + this.codes = codes; + this.arguments = arguments; + this.defaultMessage = defaultMessage; + } + + /** + * Copy constructor: Create a new instance from another resolvable. + * @param resolvable the resolvable to copy from + */ + public DefaultMessageSourceResolvable(MessageSourceResolvable resolvable) { + this(resolvable.getCodes(), resolvable.getArguments(), resolvable.getDefaultMessage()); + } + + + /** + * Return the default code of this resolvable, that is, + * the last one in the codes array. + */ + @Nullable + public String getCode() { + return (this.codes != null && this.codes.length > 0 ? this.codes[this.codes.length - 1] : null); + } + + @Override + @Nullable + public String[] getCodes() { + return this.codes; + } + + @Override + @Nullable + public Object[] getArguments() { + return this.arguments; + } + + @Override + @Nullable + public String getDefaultMessage() { + return this.defaultMessage; + } + + /** + * Indicate whether the specified default message needs to be rendered for + * substituting placeholders and/or {@link java.text.MessageFormat} escaping. + * @return {@code true} if the default message may contain argument placeholders; + * {@code false} if it definitely does not contain placeholders or custom escaping + * and can therefore be simply exposed as-is + * @since 5.1.7 + * @see #getDefaultMessage() + * @see #getArguments() + * @see AbstractMessageSource#renderDefaultMessage + */ + public boolean shouldRenderDefaultMessage() { + return true; + } + + + /** + * Build a default String representation for this MessageSourceResolvable: + * including codes, arguments, and default message. + */ + protected final String resolvableToString() { + StringBuilder result = new StringBuilder(64); + result.append("codes [").append(StringUtils.arrayToDelimitedString(this.codes, ",")); + result.append("]; arguments [").append(StringUtils.arrayToDelimitedString(this.arguments, ",")); + result.append("]; default message [").append(this.defaultMessage).append(']'); + return result.toString(); + } + + /** + * The default implementation exposes the attributes of this MessageSourceResolvable. + *

    To be overridden in more specific subclasses, potentially including the + * resolvable content through {@code resolvableToString()}. + * @see #resolvableToString() + */ + @Override + public String toString() { + return getClass().getName() + ": " + resolvableToString(); + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof MessageSourceResolvable)) { + return false; + } + MessageSourceResolvable otherResolvable = (MessageSourceResolvable) other; + return (ObjectUtils.nullSafeEquals(getCodes(), otherResolvable.getCodes()) && + ObjectUtils.nullSafeEquals(getArguments(), otherResolvable.getArguments()) && + ObjectUtils.nullSafeEquals(getDefaultMessage(), otherResolvable.getDefaultMessage())); + } + + @Override + public int hashCode() { + int hashCode = ObjectUtils.nullSafeHashCode(getCodes()); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getArguments()); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getDefaultMessage()); + return hashCode; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/DelegatingMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/DelegatingMessageSource.java new file mode 100644 index 0000000..f592996 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/DelegatingMessageSource.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.util.Locale; + +import org.springframework.context.HierarchicalMessageSource; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.NoSuchMessageException; +import org.springframework.lang.Nullable; + +/** + * Empty {@link MessageSource} that delegates all calls to the parent MessageSource. + * If no parent is available, it simply won't resolve any message. + * + *

    Used as placeholder by AbstractApplicationContext, if the context doesn't + * define its own MessageSource. Not intended for direct use in applications. + * + * @author Juergen Hoeller + * @since 1.1.5 + * @see AbstractApplicationContext + */ +public class DelegatingMessageSource extends MessageSourceSupport implements HierarchicalMessageSource { + + @Nullable + private MessageSource parentMessageSource; + + + @Override + public void setParentMessageSource(@Nullable MessageSource parent) { + this.parentMessageSource = parent; + } + + @Override + @Nullable + public MessageSource getParentMessageSource() { + return this.parentMessageSource; + } + + + @Override + @Nullable + public String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) { + if (this.parentMessageSource != null) { + return this.parentMessageSource.getMessage(code, args, defaultMessage, locale); + } + else if (defaultMessage != null) { + return renderDefaultMessage(defaultMessage, args, locale); + } + else { + return null; + } + } + + @Override + public String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException { + if (this.parentMessageSource != null) { + return this.parentMessageSource.getMessage(code, args, locale); + } + else { + throw new NoSuchMessageException(code, locale); + } + } + + @Override + public String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException { + if (this.parentMessageSource != null) { + return this.parentMessageSource.getMessage(resolvable, locale); + } + else { + if (resolvable.getDefaultMessage() != null) { + return renderDefaultMessage(resolvable.getDefaultMessage(), resolvable.getArguments(), locale); + } + String[] codes = resolvable.getCodes(); + String code = (codes != null && codes.length > 0 ? codes[0] : ""); + throw new NoSuchMessageException(code, locale); + } + } + + + @Override + public String toString() { + return this.parentMessageSource != null ? this.parentMessageSource.toString() : "Empty MessageSource"; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/EmbeddedValueResolutionSupport.java b/spring-context/src/main/java/org/springframework/context/support/EmbeddedValueResolutionSupport.java new file mode 100644 index 0000000..dc85006 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/EmbeddedValueResolutionSupport.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import org.springframework.context.EmbeddedValueResolverAware; +import org.springframework.lang.Nullable; +import org.springframework.util.StringValueResolver; + +/** + * Convenient base class for components with a need for embedded value resolution + * (i.e. {@link org.springframework.context.EmbeddedValueResolverAware} consumers). + * + * @author Juergen Hoeller + * @since 4.1 + */ +public class EmbeddedValueResolutionSupport implements EmbeddedValueResolverAware { + + @Nullable + private StringValueResolver embeddedValueResolver; + + + @Override + public void setEmbeddedValueResolver(StringValueResolver resolver) { + this.embeddedValueResolver = resolver; + } + + /** + * Resolve the given embedded value through this instance's {@link StringValueResolver}. + * @param value the value to resolve + * @return the resolved value, or always the original value if no resolver is available + * @see #setEmbeddedValueResolver + */ + @Nullable + protected String resolveEmbeddedValue(String value) { + return (this.embeddedValueResolver != null ? this.embeddedValueResolver.resolveStringValue(value) : value); + } + + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/FileSystemXmlApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/FileSystemXmlApplicationContext.java new file mode 100644 index 0000000..e6abe2b --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/FileSystemXmlApplicationContext.java @@ -0,0 +1,164 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; + +/** + * Standalone XML application context, taking the context definition files + * from the file system or from URLs, interpreting plain paths as relative + * file system locations (e.g. "mydir/myfile.txt"). Useful for test harnesses + * as well as for standalone environments. + * + *

    NOTE: Plain paths will always be interpreted as relative + * to the current VM working directory, even if they start with a slash. + * (This is consistent with the semantics in a Servlet container.) + * Use an explicit "file:" prefix to enforce an absolute file path. + * + *

    The config location defaults can be overridden via {@link #getConfigLocations}, + * Config locations can either denote concrete files like "/myfiles/context.xml" + * or Ant-style patterns like "/myfiles/*-context.xml" (see the + * {@link org.springframework.util.AntPathMatcher} javadoc for pattern details). + * + *

    Note: In case of multiple config locations, later bean definitions will + * override ones defined in earlier loaded files. This can be leveraged to + * deliberately override certain bean definitions via an extra XML file. + * + *

    This is a simple, one-stop shop convenience ApplicationContext. + * Consider using the {@link GenericApplicationContext} class in combination + * with an {@link org.springframework.beans.factory.xml.XmlBeanDefinitionReader} + * for more flexible context setup. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #getResource + * @see #getResourceByPath + * @see GenericApplicationContext + */ +public class FileSystemXmlApplicationContext extends AbstractXmlApplicationContext { + + /** + * Create a new FileSystemXmlApplicationContext for bean-style configuration. + * @see #setConfigLocation + * @see #setConfigLocations + * @see #afterPropertiesSet() + */ + public FileSystemXmlApplicationContext() { + } + + /** + * Create a new FileSystemXmlApplicationContext for bean-style configuration. + * @param parent the parent context + * @see #setConfigLocation + * @see #setConfigLocations + * @see #afterPropertiesSet() + */ + public FileSystemXmlApplicationContext(ApplicationContext parent) { + super(parent); + } + + /** + * Create a new FileSystemXmlApplicationContext, loading the definitions + * from the given XML file and automatically refreshing the context. + * @param configLocation file path + * @throws BeansException if context creation failed + */ + public FileSystemXmlApplicationContext(String configLocation) throws BeansException { + this(new String[] {configLocation}, true, null); + } + + /** + * Create a new FileSystemXmlApplicationContext, loading the definitions + * from the given XML files and automatically refreshing the context. + * @param configLocations array of file paths + * @throws BeansException if context creation failed + */ + public FileSystemXmlApplicationContext(String... configLocations) throws BeansException { + this(configLocations, true, null); + } + + /** + * Create a new FileSystemXmlApplicationContext with the given parent, + * loading the definitions from the given XML files and automatically + * refreshing the context. + * @param configLocations array of file paths + * @param parent the parent context + * @throws BeansException if context creation failed + */ + public FileSystemXmlApplicationContext(String[] configLocations, ApplicationContext parent) throws BeansException { + this(configLocations, true, parent); + } + + /** + * Create a new FileSystemXmlApplicationContext, loading the definitions + * from the given XML files. + * @param configLocations array of file paths + * @param refresh whether to automatically refresh the context, + * loading all bean definitions and creating all singletons. + * Alternatively, call refresh manually after further configuring the context. + * @throws BeansException if context creation failed + * @see #refresh() + */ + public FileSystemXmlApplicationContext(String[] configLocations, boolean refresh) throws BeansException { + this(configLocations, refresh, null); + } + + /** + * Create a new FileSystemXmlApplicationContext with the given parent, + * loading the definitions from the given XML files. + * @param configLocations array of file paths + * @param refresh whether to automatically refresh the context, + * loading all bean definitions and creating all singletons. + * Alternatively, call refresh manually after further configuring the context. + * @param parent the parent context + * @throws BeansException if context creation failed + * @see #refresh() + */ + public FileSystemXmlApplicationContext( + String[] configLocations, boolean refresh, @Nullable ApplicationContext parent) + throws BeansException { + + super(parent); + setConfigLocations(configLocations); + if (refresh) { + refresh(); + } + } + + + /** + * Resolve resource paths as file system paths. + *

    Note: Even if a given path starts with a slash, it will get + * interpreted as relative to the current VM working directory. + * This is consistent with the semantics in a Servlet container. + * @param path the path to the resource + * @return the Resource handle + * @see org.springframework.web.context.support.XmlWebApplicationContext#getResourceByPath + */ + @Override + protected Resource getResourceByPath(String path) { + if (path.startsWith("/")) { + path = path.substring(1); + } + return new FileSystemResource(path); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java new file mode 100644 index 0000000..accdb15 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java @@ -0,0 +1,518 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionCustomizer; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.ApplicationContext; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.core.metrics.ApplicationStartup; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Generic ApplicationContext implementation that holds a single internal + * {@link org.springframework.beans.factory.support.DefaultListableBeanFactory} + * instance and does not assume a specific bean definition format. Implements + * the {@link org.springframework.beans.factory.support.BeanDefinitionRegistry} + * interface in order to allow for applying any bean definition readers to it. + * + *

    Typical usage is to register a variety of bean definitions via the + * {@link org.springframework.beans.factory.support.BeanDefinitionRegistry} + * interface and then call {@link #refresh()} to initialize those beans + * with application context semantics (handling + * {@link org.springframework.context.ApplicationContextAware}, auto-detecting + * {@link org.springframework.beans.factory.config.BeanFactoryPostProcessor BeanFactoryPostProcessors}, + * etc). + * + *

    In contrast to other ApplicationContext implementations that create a new + * internal BeanFactory instance for each refresh, the internal BeanFactory of + * this context is available right from the start, to be able to register bean + * definitions on it. {@link #refresh()} may only be called once. + * + *

    Usage example: + * + *

    + * GenericApplicationContext ctx = new GenericApplicationContext();
    + * XmlBeanDefinitionReader xmlReader = new XmlBeanDefinitionReader(ctx);
    + * xmlReader.loadBeanDefinitions(new ClassPathResource("applicationContext.xml"));
    + * PropertiesBeanDefinitionReader propReader = new PropertiesBeanDefinitionReader(ctx);
    + * propReader.loadBeanDefinitions(new ClassPathResource("otherBeans.properties"));
    + * ctx.refresh();
    + *
    + * MyBean myBean = (MyBean) ctx.getBean("myBean");
    + * ...
    + * + * For the typical case of XML bean definitions, simply use + * {@link ClassPathXmlApplicationContext} or {@link FileSystemXmlApplicationContext}, + * which are easier to set up - but less flexible, since you can just use standard + * resource locations for XML bean definitions, rather than mixing arbitrary bean + * definition formats. The equivalent in a web environment is + * {@link org.springframework.web.context.support.XmlWebApplicationContext}. + * + *

    For custom application context implementations that are supposed to read + * special bean definition formats in a refreshable manner, consider deriving + * from the {@link AbstractRefreshableApplicationContext} base class. + * + * @author Juergen Hoeller + * @author Chris Beams + * @since 1.1.2 + * @see #registerBeanDefinition + * @see #refresh() + * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader + * @see org.springframework.beans.factory.support.PropertiesBeanDefinitionReader + */ +public class GenericApplicationContext extends AbstractApplicationContext implements BeanDefinitionRegistry { + + private final DefaultListableBeanFactory beanFactory; + + @Nullable + private ResourceLoader resourceLoader; + + private boolean customClassLoader = false; + + private final AtomicBoolean refreshed = new AtomicBoolean(); + + + /** + * Create a new GenericApplicationContext. + * @see #registerBeanDefinition + * @see #refresh + */ + public GenericApplicationContext() { + this.beanFactory = new DefaultListableBeanFactory(); + } + + /** + * Create a new GenericApplicationContext with the given DefaultListableBeanFactory. + * @param beanFactory the DefaultListableBeanFactory instance to use for this context + * @see #registerBeanDefinition + * @see #refresh + */ + public GenericApplicationContext(DefaultListableBeanFactory beanFactory) { + Assert.notNull(beanFactory, "BeanFactory must not be null"); + this.beanFactory = beanFactory; + } + + /** + * Create a new GenericApplicationContext with the given parent. + * @param parent the parent application context + * @see #registerBeanDefinition + * @see #refresh + */ + public GenericApplicationContext(@Nullable ApplicationContext parent) { + this(); + setParent(parent); + } + + /** + * Create a new GenericApplicationContext with the given DefaultListableBeanFactory. + * @param beanFactory the DefaultListableBeanFactory instance to use for this context + * @param parent the parent application context + * @see #registerBeanDefinition + * @see #refresh + */ + public GenericApplicationContext(DefaultListableBeanFactory beanFactory, ApplicationContext parent) { + this(beanFactory); + setParent(parent); + } + + + /** + * Set the parent of this application context, also setting + * the parent of the internal BeanFactory accordingly. + * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#setParentBeanFactory + */ + @Override + public void setParent(@Nullable ApplicationContext parent) { + super.setParent(parent); + this.beanFactory.setParentBeanFactory(getInternalParentBeanFactory()); + } + + @Override + public void setApplicationStartup(ApplicationStartup applicationStartup) { + super.setApplicationStartup(applicationStartup); + this.beanFactory.setApplicationStartup(applicationStartup); + } + + /** + * Set whether it should be allowed to override bean definitions by registering + * a different definition with the same name, automatically replacing the former. + * If not, an exception will be thrown. Default is "true". + * @since 3.0 + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#setAllowBeanDefinitionOverriding + */ + public void setAllowBeanDefinitionOverriding(boolean allowBeanDefinitionOverriding) { + this.beanFactory.setAllowBeanDefinitionOverriding(allowBeanDefinitionOverriding); + } + + /** + * Set whether to allow circular references between beans - and automatically + * try to resolve them. + *

    Default is "true". Turn this off to throw an exception when encountering + * a circular reference, disallowing them completely. + * @since 3.0 + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#setAllowCircularReferences + */ + public void setAllowCircularReferences(boolean allowCircularReferences) { + this.beanFactory.setAllowCircularReferences(allowCircularReferences); + } + + /** + * Set a ResourceLoader to use for this context. If set, the context will + * delegate all {@code getResource} calls to the given ResourceLoader. + * If not set, default resource loading will apply. + *

    The main reason to specify a custom ResourceLoader is to resolve + * resource paths (without URL prefix) in a specific fashion. + * The default behavior is to resolve such paths as class path locations. + * To resolve resource paths as file system locations, specify a + * FileSystemResourceLoader here. + *

    You can also pass in a full ResourcePatternResolver, which will + * be autodetected by the context and used for {@code getResources} + * calls as well. Else, default resource pattern matching will apply. + * @see #getResource + * @see org.springframework.core.io.DefaultResourceLoader + * @see org.springframework.core.io.FileSystemResourceLoader + * @see org.springframework.core.io.support.ResourcePatternResolver + * @see #getResources + */ + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + + //--------------------------------------------------------------------- + // ResourceLoader / ResourcePatternResolver override if necessary + //--------------------------------------------------------------------- + + /** + * This implementation delegates to this context's ResourceLoader if set, + * falling back to the default superclass behavior else. + * @see #setResourceLoader + */ + @Override + public Resource getResource(String location) { + if (this.resourceLoader != null) { + return this.resourceLoader.getResource(location); + } + return super.getResource(location); + } + + /** + * This implementation delegates to this context's ResourceLoader if it + * implements the ResourcePatternResolver interface, falling back to the + * default superclass behavior else. + * @see #setResourceLoader + */ + @Override + public Resource[] getResources(String locationPattern) throws IOException { + if (this.resourceLoader instanceof ResourcePatternResolver) { + return ((ResourcePatternResolver) this.resourceLoader).getResources(locationPattern); + } + return super.getResources(locationPattern); + } + + @Override + public void setClassLoader(@Nullable ClassLoader classLoader) { + super.setClassLoader(classLoader); + this.customClassLoader = true; + } + + @Override + @Nullable + public ClassLoader getClassLoader() { + if (this.resourceLoader != null && !this.customClassLoader) { + return this.resourceLoader.getClassLoader(); + } + return super.getClassLoader(); + } + + + //--------------------------------------------------------------------- + // Implementations of AbstractApplicationContext's template methods + //--------------------------------------------------------------------- + + /** + * Do nothing: We hold a single internal BeanFactory and rely on callers + * to register beans through our public methods (or the BeanFactory's). + * @see #registerBeanDefinition + */ + @Override + protected final void refreshBeanFactory() throws IllegalStateException { + if (!this.refreshed.compareAndSet(false, true)) { + throw new IllegalStateException( + "GenericApplicationContext does not support multiple refresh attempts: just call 'refresh' once"); + } + this.beanFactory.setSerializationId(getId()); + } + + @Override + protected void cancelRefresh(BeansException ex) { + this.beanFactory.setSerializationId(null); + super.cancelRefresh(ex); + } + + /** + * Not much to do: We hold a single internal BeanFactory that will never + * get released. + */ + @Override + protected final void closeBeanFactory() { + this.beanFactory.setSerializationId(null); + } + + /** + * Return the single internal BeanFactory held by this context + * (as ConfigurableListableBeanFactory). + */ + @Override + public final ConfigurableListableBeanFactory getBeanFactory() { + return this.beanFactory; + } + + /** + * Return the underlying bean factory of this context, + * available for registering bean definitions. + *

    NOTE: You need to call {@link #refresh()} to initialize the + * bean factory and its contained beans with application context semantics + * (autodetecting BeanFactoryPostProcessors, etc). + * @return the internal bean factory (as DefaultListableBeanFactory) + */ + public final DefaultListableBeanFactory getDefaultListableBeanFactory() { + return this.beanFactory; + } + + @Override + public AutowireCapableBeanFactory getAutowireCapableBeanFactory() throws IllegalStateException { + assertBeanFactoryActive(); + return this.beanFactory; + } + + + //--------------------------------------------------------------------- + // Implementation of BeanDefinitionRegistry + //--------------------------------------------------------------------- + + @Override + public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) + throws BeanDefinitionStoreException { + + this.beanFactory.registerBeanDefinition(beanName, beanDefinition); + } + + @Override + public void removeBeanDefinition(String beanName) throws NoSuchBeanDefinitionException { + this.beanFactory.removeBeanDefinition(beanName); + } + + @Override + public BeanDefinition getBeanDefinition(String beanName) throws NoSuchBeanDefinitionException { + return this.beanFactory.getBeanDefinition(beanName); + } + + @Override + public boolean isBeanNameInUse(String beanName) { + return this.beanFactory.isBeanNameInUse(beanName); + } + + @Override + public void registerAlias(String beanName, String alias) { + this.beanFactory.registerAlias(beanName, alias); + } + + @Override + public void removeAlias(String alias) { + this.beanFactory.removeAlias(alias); + } + + @Override + public boolean isAlias(String beanName) { + return this.beanFactory.isAlias(beanName); + } + + + //--------------------------------------------------------------------- + // Convenient methods for registering individual beans + //--------------------------------------------------------------------- + + /** + * Register a bean from the given bean class, optionally providing explicit + * constructor arguments for consideration in the autowiring process. + * @param beanClass the class of the bean + * @param constructorArgs custom argument values to be fed into Spring's + * constructor resolution algorithm, resolving either all arguments or just + * specific ones, with the rest to be resolved through regular autowiring + * (may be {@code null} or empty) + * @since 5.2 (since 5.0 on the AnnotationConfigApplicationContext subclass) + */ + public void registerBean(Class beanClass, Object... constructorArgs) { + registerBean(null, beanClass, constructorArgs); + } + + /** + * Register a bean from the given bean class, optionally providing explicit + * constructor arguments for consideration in the autowiring process. + * @param beanName the name of the bean (may be {@code null}) + * @param beanClass the class of the bean + * @param constructorArgs custom argument values to be fed into Spring's + * constructor resolution algorithm, resolving either all arguments or just + * specific ones, with the rest to be resolved through regular autowiring + * (may be {@code null} or empty) + * @since 5.2 (since 5.0 on the AnnotationConfigApplicationContext subclass) + */ + public void registerBean(@Nullable String beanName, Class beanClass, Object... constructorArgs) { + registerBean(beanName, beanClass, (Supplier) null, + bd -> { + for (Object arg : constructorArgs) { + bd.getConstructorArgumentValues().addGenericArgumentValue(arg); + } + }); + } + + /** + * Register a bean from the given bean class, optionally customizing its + * bean definition metadata (typically declared as a lambda expression). + * @param beanClass the class of the bean (resolving a public constructor + * to be autowired, possibly simply the default constructor) + * @param customizers one or more callbacks for customizing the factory's + * {@link BeanDefinition}, e.g. setting a lazy-init or primary flag + * @since 5.0 + * @see #registerBean(String, Class, Supplier, BeanDefinitionCustomizer...) + */ + public final void registerBean(Class beanClass, BeanDefinitionCustomizer... customizers) { + registerBean(null, beanClass, null, customizers); + } + + /** + * Register a bean from the given bean class, optionally customizing its + * bean definition metadata (typically declared as a lambda expression). + * @param beanName the name of the bean (may be {@code null}) + * @param beanClass the class of the bean (resolving a public constructor + * to be autowired, possibly simply the default constructor) + * @param customizers one or more callbacks for customizing the factory's + * {@link BeanDefinition}, e.g. setting a lazy-init or primary flag + * @since 5.0 + * @see #registerBean(String, Class, Supplier, BeanDefinitionCustomizer...) + */ + public final void registerBean( + @Nullable String beanName, Class beanClass, BeanDefinitionCustomizer... customizers) { + + registerBean(beanName, beanClass, null, customizers); + } + + /** + * Register a bean from the given bean class, using the given supplier for + * obtaining a new instance (typically declared as a lambda expression or + * method reference), optionally customizing its bean definition metadata + * (again typically declared as a lambda expression). + * @param beanClass the class of the bean + * @param supplier a callback for creating an instance of the bean + * @param customizers one or more callbacks for customizing the factory's + * {@link BeanDefinition}, e.g. setting a lazy-init or primary flag + * @since 5.0 + * @see #registerBean(String, Class, Supplier, BeanDefinitionCustomizer...) + */ + public final void registerBean( + Class beanClass, Supplier supplier, BeanDefinitionCustomizer... customizers) { + + registerBean(null, beanClass, supplier, customizers); + } + + /** + * Register a bean from the given bean class, using the given supplier for + * obtaining a new instance (typically declared as a lambda expression or + * method reference), optionally customizing its bean definition metadata + * (again typically declared as a lambda expression). + *

    This method can be overridden to adapt the registration mechanism for + * all {@code registerBean} methods (since they all delegate to this one). + * @param beanName the name of the bean (may be {@code null}) + * @param beanClass the class of the bean + * @param supplier a callback for creating an instance of the bean (in case + * of {@code null}, resolving a public constructor to be autowired instead) + * @param customizers one or more callbacks for customizing the factory's + * {@link BeanDefinition}, e.g. setting a lazy-init or primary flag + * @since 5.0 + */ + public void registerBean(@Nullable String beanName, Class beanClass, + @Nullable Supplier supplier, BeanDefinitionCustomizer... customizers) { + + ClassDerivedBeanDefinition beanDefinition = new ClassDerivedBeanDefinition(beanClass); + if (supplier != null) { + beanDefinition.setInstanceSupplier(supplier); + } + for (BeanDefinitionCustomizer customizer : customizers) { + customizer.customize(beanDefinition); + } + + String nameToUse = (beanName != null ? beanName : beanClass.getName()); + registerBeanDefinition(nameToUse, beanDefinition); + } + + + /** + * {@link RootBeanDefinition} marker subclass for {@code #registerBean} based + * registrations with flexible autowiring for public constructors. + */ + @SuppressWarnings("serial") + private static class ClassDerivedBeanDefinition extends RootBeanDefinition { + + public ClassDerivedBeanDefinition(Class beanClass) { + super(beanClass); + } + + public ClassDerivedBeanDefinition(ClassDerivedBeanDefinition original) { + super(original); + } + + @Override + @Nullable + public Constructor[] getPreferredConstructors() { + Class clazz = getBeanClass(); + Constructor primaryCtor = BeanUtils.findPrimaryConstructor(clazz); + if (primaryCtor != null) { + return new Constructor[] {primaryCtor}; + } + Constructor[] publicCtors = clazz.getConstructors(); + if (publicCtors.length > 0) { + return publicCtors; + } + return null; + } + + @Override + public RootBeanDefinition cloneBeanDefinition() { + return new ClassDerivedBeanDefinition(this); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/GenericGroovyApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/GenericGroovyApplicationContext.java new file mode 100644 index 0000000..8e1f74b --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/GenericGroovyApplicationContext.java @@ -0,0 +1,265 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import groovy.lang.GroovyObject; +import groovy.lang.GroovySystem; +import groovy.lang.MetaClass; + +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.BeanWrapperImpl; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; + +/** + * An {@link org.springframework.context.ApplicationContext} implementation that extends + * {@link GenericApplicationContext} and implements {@link GroovyObject} such that beans + * can be retrieved with the dot de-reference syntax instead of using {@link #getBean}. + * + *

    Consider this as the equivalent of {@link GenericXmlApplicationContext} for + * Groovy bean definitions, or even an upgrade thereof since it seamlessly understands + * XML bean definition files as well. The main difference is that, within a Groovy + * script, the context can be used with an inline bean definition closure as follows: + * + *

    + * import org.hibernate.SessionFactory
    + * import org.apache.commons.dbcp.BasicDataSource
    + *
    + * def context = new GenericGroovyApplicationContext()
    + * context.reader.beans {
    + *     dataSource(BasicDataSource) {                  // <--- invokeMethod
    + *         driverClassName = "org.hsqldb.jdbcDriver"
    + *         url = "jdbc:hsqldb:mem:grailsDB"
    + *         username = "sa"                            // <-- setProperty
    + *         password = ""
    + *         settings = [mynew:"setting"]
    + *     }
    + *     sessionFactory(SessionFactory) {
    + *         dataSource = dataSource                    // <-- getProperty for retrieving references
    + *     }
    + *     myService(MyService) {
    + *         nestedBean = { AnotherBean bean ->         // <-- setProperty with closure for nested bean
    + *             dataSource = dataSource
    + *         }
    + *     }
    + * }
    + * context.refresh()
    + * 
    + * + *

    Alternatively, load a Groovy bean definition script like the following + * from an external resource (e.g. an "applicationContext.groovy" file): + * + *

    + * import org.hibernate.SessionFactory
    + * import org.apache.commons.dbcp.BasicDataSource
    + *
    + * beans {
    + *     dataSource(BasicDataSource) {
    + *         driverClassName = "org.hsqldb.jdbcDriver"
    + *         url = "jdbc:hsqldb:mem:grailsDB"
    + *         username = "sa"
    + *         password = ""
    + *         settings = [mynew:"setting"]
    + *     }
    + *     sessionFactory(SessionFactory) {
    + *         dataSource = dataSource
    + *     }
    + *     myService(MyService) {
    + *         nestedBean = { AnotherBean bean ->
    + *             dataSource = dataSource
    + *         }
    + *     }
    + * }
    + * 
    + * + *

    With the following Java code creating the {@code GenericGroovyApplicationContext} + * (potentially using Ant-style '*'/'**' location patterns): + * + *

    + * GenericGroovyApplicationContext context = new GenericGroovyApplicationContext();
    + * context.load("org/myapp/applicationContext.groovy");
    + * context.refresh();
    + * 
    + * + *

    Or even more concise, provided that no extra configuration is needed: + * + *

    + * ApplicationContext context = new GenericGroovyApplicationContext("org/myapp/applicationContext.groovy");
    + * 
    + * + *

    This application context also understands XML bean definition files, + * allowing for seamless mixing and matching with Groovy bean definition files. + * ".xml" files will be parsed as XML content; all other kinds of resources will + * be parsed as Groovy scripts. + * + * @author Juergen Hoeller + * @author Jeff Brown + * @since 4.0 + * @see org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader + */ +public class GenericGroovyApplicationContext extends GenericApplicationContext implements GroovyObject { + + private final GroovyBeanDefinitionReader reader = new GroovyBeanDefinitionReader(this); + + private final BeanWrapper contextWrapper = new BeanWrapperImpl(this); + + private MetaClass metaClass = GroovySystem.getMetaClassRegistry().getMetaClass(getClass()); + + + /** + * Create a new GenericGroovyApplicationContext that needs to be + * {@link #load loaded} and then manually {@link #refresh refreshed}. + */ + public GenericGroovyApplicationContext() { + } + + /** + * Create a new GenericGroovyApplicationContext, loading bean definitions + * from the given resources and automatically refreshing the context. + * @param resources the resources to load from + */ + public GenericGroovyApplicationContext(Resource... resources) { + load(resources); + refresh(); + } + + /** + * Create a new GenericGroovyApplicationContext, loading bean definitions + * from the given resource locations and automatically refreshing the context. + * @param resourceLocations the resources to load from + */ + public GenericGroovyApplicationContext(String... resourceLocations) { + load(resourceLocations); + refresh(); + } + + /** + * Create a new GenericGroovyApplicationContext, loading bean definitions + * from the given resource locations and automatically refreshing the context. + * @param relativeClass class whose package will be used as a prefix when + * loading each specified resource name + * @param resourceNames relatively-qualified names of resources to load + */ + public GenericGroovyApplicationContext(Class relativeClass, String... resourceNames) { + load(relativeClass, resourceNames); + refresh(); + } + + + /** + * Exposes the underlying {@link GroovyBeanDefinitionReader} for convenient access + * to the {@code loadBeanDefinition} methods on it as well as the ability + * to specify an inline Groovy bean definition closure. + * @see GroovyBeanDefinitionReader#loadBeanDefinitions(org.springframework.core.io.Resource...) + * @see GroovyBeanDefinitionReader#loadBeanDefinitions(String...) + */ + public final GroovyBeanDefinitionReader getReader() { + return this.reader; + } + + /** + * Delegates the given environment to underlying {@link GroovyBeanDefinitionReader}. + * Should be called before any call to {@code #load}. + */ + @Override + public void setEnvironment(ConfigurableEnvironment environment) { + super.setEnvironment(environment); + this.reader.setEnvironment(getEnvironment()); + } + + /** + * Load bean definitions from the given Groovy scripts or XML files. + *

    Note that ".xml" files will be parsed as XML content; all other kinds + * of resources will be parsed as Groovy scripts. + * @param resources one or more resources to load from + */ + public void load(Resource... resources) { + this.reader.loadBeanDefinitions(resources); + } + + /** + * Load bean definitions from the given Groovy scripts or XML files. + *

    Note that ".xml" files will be parsed as XML content; all other kinds + * of resources will be parsed as Groovy scripts. + * @param resourceLocations one or more resource locations to load from + */ + public void load(String... resourceLocations) { + this.reader.loadBeanDefinitions(resourceLocations); + } + + /** + * Load bean definitions from the given Groovy scripts or XML files. + *

    Note that ".xml" files will be parsed as XML content; all other kinds + * of resources will be parsed as Groovy scripts. + * @param relativeClass class whose package will be used as a prefix when + * loading each specified resource name + * @param resourceNames relatively-qualified names of resources to load + */ + public void load(Class relativeClass, String... resourceNames) { + Resource[] resources = new Resource[resourceNames.length]; + for (int i = 0; i < resourceNames.length; i++) { + resources[i] = new ClassPathResource(resourceNames[i], relativeClass); + } + load(resources); + } + + + // Implementation of the GroovyObject interface + + @Override + public void setMetaClass(MetaClass metaClass) { + this.metaClass = metaClass; + } + + @Override + public MetaClass getMetaClass() { + return this.metaClass; + } + + @Override + public Object invokeMethod(String name, Object args) { + return this.metaClass.invokeMethod(this, name, args); + } + + @Override + public void setProperty(String property, Object newValue) { + if (newValue instanceof BeanDefinition) { + registerBeanDefinition(property, (BeanDefinition) newValue); + } + else { + this.metaClass.setProperty(this, property, newValue); + } + } + + @Override + @Nullable + public Object getProperty(String property) { + if (containsBean(property)) { + return getBean(property); + } + else if (this.contextWrapper.isReadableProperty(property)) { + return this.contextWrapper.getPropertyValue(property); + } + throw new NoSuchBeanDefinitionException(property); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/GenericXmlApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/GenericXmlApplicationContext.java new file mode 100644 index 0000000..80c8fd8 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/GenericXmlApplicationContext.java @@ -0,0 +1,147 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; + +/** + * Convenient application context with built-in XML support. + * This is a flexible alternative to {@link ClassPathXmlApplicationContext} + * and {@link FileSystemXmlApplicationContext}, to be configured via setters, + * with an eventual {@link #refresh()} call activating the context. + * + *

    In case of multiple configuration files, bean definitions in later files + * will override those defined in earlier files. This can be leveraged to + * intentionally override certain bean definitions via an extra configuration + * file appended to the list. + * + * @author Juergen Hoeller + * @author Chris Beams + * @since 3.0 + * @see #load + * @see XmlBeanDefinitionReader + * @see org.springframework.context.annotation.AnnotationConfigApplicationContext + */ +public class GenericXmlApplicationContext extends GenericApplicationContext { + + private final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this); + + + /** + * Create a new GenericXmlApplicationContext that needs to be + * {@link #load loaded} and then manually {@link #refresh refreshed}. + */ + public GenericXmlApplicationContext() { + } + + /** + * Create a new GenericXmlApplicationContext, loading bean definitions + * from the given resources and automatically refreshing the context. + * @param resources the resources to load from + */ + public GenericXmlApplicationContext(Resource... resources) { + load(resources); + refresh(); + } + + /** + * Create a new GenericXmlApplicationContext, loading bean definitions + * from the given resource locations and automatically refreshing the context. + * @param resourceLocations the resources to load from + */ + public GenericXmlApplicationContext(String... resourceLocations) { + load(resourceLocations); + refresh(); + } + + /** + * Create a new GenericXmlApplicationContext, loading bean definitions + * from the given resource locations and automatically refreshing the context. + * @param relativeClass class whose package will be used as a prefix when + * loading each specified resource name + * @param resourceNames relatively-qualified names of resources to load + */ + public GenericXmlApplicationContext(Class relativeClass, String... resourceNames) { + load(relativeClass, resourceNames); + refresh(); + } + + + /** + * Exposes the underlying {@link XmlBeanDefinitionReader} for additional + * configuration facilities and {@code loadBeanDefinition} variations. + */ + public final XmlBeanDefinitionReader getReader() { + return this.reader; + } + + /** + * Set whether to use XML validation. Default is {@code true}. + */ + public void setValidating(boolean validating) { + this.reader.setValidating(validating); + } + + /** + * Delegates the given environment to underlying {@link XmlBeanDefinitionReader}. + * Should be called before any call to {@code #load}. + */ + @Override + public void setEnvironment(ConfigurableEnvironment environment) { + super.setEnvironment(environment); + this.reader.setEnvironment(getEnvironment()); + } + + + //--------------------------------------------------------------------- + // Convenient methods for loading XML bean definition files + //--------------------------------------------------------------------- + + /** + * Load bean definitions from the given XML resources. + * @param resources one or more resources to load from + */ + public void load(Resource... resources) { + this.reader.loadBeanDefinitions(resources); + } + + /** + * Load bean definitions from the given XML resources. + * @param resourceLocations one or more resource locations to load from + */ + public void load(String... resourceLocations) { + this.reader.loadBeanDefinitions(resourceLocations); + } + + /** + * Load bean definitions from the given XML resources. + * @param relativeClass class whose package will be used as a prefix when + * loading each specified resource name + * @param resourceNames relatively-qualified names of resources to load + */ + public void load(Class relativeClass, String... resourceNames) { + Resource[] resources = new Resource[resourceNames.length]; + for (int i = 0; i < resourceNames.length; i++) { + resources[i] = new ClassPathResource(resourceNames[i], relativeClass); + } + this.load(resources); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/LiveBeansView.java b/spring-context/src/main/java/org/springframework/context/support/LiveBeansView.java new file mode 100644 index 0000000..668710f --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/LiveBeansView.java @@ -0,0 +1,270 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.lang.management.ManagementFactory; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Set; + +import javax.management.MBeanServer; +import javax.management.ObjectName; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationContextException; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Adapter for live beans view exposure, building a snapshot of current beans + * and their dependencies from either a local {@code ApplicationContext} (with a + * local {@code LiveBeansView} bean definition) or all registered ApplicationContexts + * (driven by the {@value #MBEAN_DOMAIN_PROPERTY_NAME} environment property). + * + * @author Juergen Hoeller + * @author Stephane Nicoll + * @since 3.2 + * @see #getSnapshotAsJson() + * @see org.springframework.web.context.support.LiveBeansViewServlet + * @deprecated as of 5.3, in favor of using Spring Boot actuators for such needs + */ +@Deprecated +public class LiveBeansView implements LiveBeansViewMBean, ApplicationContextAware { + + /** + * The "MBean Domain" property name. + */ + public static final String MBEAN_DOMAIN_PROPERTY_NAME = "spring.liveBeansView.mbeanDomain"; + + /** + * The MBean application key. + */ + public static final String MBEAN_APPLICATION_KEY = "application"; + + private static final Set applicationContexts = new LinkedHashSet<>(); + + @Nullable + private static String applicationName; + + + static void registerApplicationContext(ConfigurableApplicationContext applicationContext) { + String mbeanDomain = applicationContext.getEnvironment().getProperty(MBEAN_DOMAIN_PROPERTY_NAME); + if (mbeanDomain != null) { + synchronized (applicationContexts) { + if (applicationContexts.isEmpty()) { + try { + MBeanServer server = ManagementFactory.getPlatformMBeanServer(); + applicationName = applicationContext.getApplicationName(); + server.registerMBean(new LiveBeansView(), + new ObjectName(mbeanDomain, MBEAN_APPLICATION_KEY, applicationName)); + } + catch (Throwable ex) { + throw new ApplicationContextException("Failed to register LiveBeansView MBean", ex); + } + } + applicationContexts.add(applicationContext); + } + } + } + + static void unregisterApplicationContext(ConfigurableApplicationContext applicationContext) { + synchronized (applicationContexts) { + if (applicationContexts.remove(applicationContext) && applicationContexts.isEmpty()) { + try { + MBeanServer server = ManagementFactory.getPlatformMBeanServer(); + String mbeanDomain = applicationContext.getEnvironment().getProperty(MBEAN_DOMAIN_PROPERTY_NAME); + if (mbeanDomain != null) { + server.unregisterMBean(new ObjectName(mbeanDomain, MBEAN_APPLICATION_KEY, applicationName)); + } + } + catch (Throwable ex) { + throw new ApplicationContextException("Failed to unregister LiveBeansView MBean", ex); + } + finally { + applicationName = null; + } + } + } + } + + + @Nullable + private ConfigurableApplicationContext applicationContext; + + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + Assert.isTrue(applicationContext instanceof ConfigurableApplicationContext, + "ApplicationContext does not implement ConfigurableApplicationContext"); + this.applicationContext = (ConfigurableApplicationContext) applicationContext; + } + + + /** + * Generate a JSON snapshot of current beans and their dependencies, + * finding all active ApplicationContexts through {@link #findApplicationContexts()}, + * then delegating to {@link #generateJson(java.util.Set)}. + */ + @Override + public String getSnapshotAsJson() { + Set contexts; + if (this.applicationContext != null) { + contexts = Collections.singleton(this.applicationContext); + } + else { + contexts = findApplicationContexts(); + } + return generateJson(contexts); + } + + /** + * Find all applicable ApplicationContexts for the current application. + *

    Called if no specific ApplicationContext has been set for this LiveBeansView. + * @return the set of ApplicationContexts + */ + protected Set findApplicationContexts() { + synchronized (applicationContexts) { + return new LinkedHashSet<>(applicationContexts); + } + } + + /** + * Actually generate a JSON snapshot of the beans in the given ApplicationContexts. + *

    This implementation doesn't use any JSON parsing libraries in order to avoid + * third-party library dependencies. It produces an array of context description + * objects, each containing a context and parent attribute as well as a beans + * attribute with nested bean description objects. Each bean object contains a + * bean, scope, type and resource attribute, as well as a dependencies attribute + * with a nested array of bean names that the present bean depends on. + * @param contexts the set of ApplicationContexts + * @return the JSON document + */ + protected String generateJson(Set contexts) { + StringBuilder result = new StringBuilder("[\n"); + for (Iterator it = contexts.iterator(); it.hasNext();) { + ConfigurableApplicationContext context = it.next(); + result.append("{\n\"context\": \"").append(context.getId()).append("\",\n"); + if (context.getParent() != null) { + result.append("\"parent\": \"").append(context.getParent().getId()).append("\",\n"); + } + else { + result.append("\"parent\": null,\n"); + } + result.append("\"beans\": [\n"); + ConfigurableListableBeanFactory bf = context.getBeanFactory(); + String[] beanNames = bf.getBeanDefinitionNames(); + boolean elementAppended = false; + for (String beanName : beanNames) { + BeanDefinition bd = bf.getBeanDefinition(beanName); + if (isBeanEligible(beanName, bd, bf)) { + if (elementAppended) { + result.append(",\n"); + } + result.append("{\n\"bean\": \"").append(beanName).append("\",\n"); + result.append("\"aliases\": "); + appendArray(result, bf.getAliases(beanName)); + result.append(",\n"); + String scope = bd.getScope(); + if (!StringUtils.hasText(scope)) { + scope = BeanDefinition.SCOPE_SINGLETON; + } + result.append("\"scope\": \"").append(scope).append("\",\n"); + Class beanType = bf.getType(beanName); + if (beanType != null) { + result.append("\"type\": \"").append(beanType.getName()).append("\",\n"); + } + else { + result.append("\"type\": null,\n"); + } + result.append("\"resource\": \"").append(getEscapedResourceDescription(bd)).append("\",\n"); + result.append("\"dependencies\": "); + appendArray(result, bf.getDependenciesForBean(beanName)); + result.append("\n}"); + elementAppended = true; + } + } + result.append("]\n"); + result.append("}"); + if (it.hasNext()) { + result.append(",\n"); + } + } + result.append("]"); + return result.toString(); + } + + /** + * Determine whether the specified bean is eligible for inclusion in the + * LiveBeansView JSON snapshot. + * @param beanName the name of the bean + * @param bd the corresponding bean definition + * @param bf the containing bean factory + * @return {@code true} if the bean is to be included; {@code false} otherwise + */ + protected boolean isBeanEligible(String beanName, BeanDefinition bd, ConfigurableBeanFactory bf) { + return (bd.getRole() != BeanDefinition.ROLE_INFRASTRUCTURE && + (!bd.isLazyInit() || bf.containsSingleton(beanName))); + } + + /** + * Determine a resource description for the given bean definition and + * apply basic JSON escaping (backslashes, double quotes) to it. + * @param bd the bean definition to build the resource description for + * @return the JSON-escaped resource description + */ + @Nullable + protected String getEscapedResourceDescription(BeanDefinition bd) { + String resourceDescription = bd.getResourceDescription(); + if (resourceDescription == null) { + return null; + } + StringBuilder result = new StringBuilder(resourceDescription.length() + 16); + for (int i = 0; i < resourceDescription.length(); i++) { + char character = resourceDescription.charAt(i); + if (character == '\\') { + result.append('/'); + } + else if (character == '"') { + result.append("\\").append('"'); + } + else { + result.append(character); + } + } + return result.toString(); + } + + private void appendArray(StringBuilder result, String[] arr) { + result.append('['); + if (arr.length > 0) { + result.append('\"'); + } + result.append(StringUtils.arrayToDelimitedString(arr, "\", \"")); + if (arr.length > 0) { + result.append('\"'); + } + result.append(']'); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/LiveBeansViewMBean.java b/spring-context/src/main/java/org/springframework/context/support/LiveBeansViewMBean.java new file mode 100644 index 0000000..b8e0ba0 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/LiveBeansViewMBean.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +/** + * MBean operation interface for the {@link LiveBeansView} feature. + * + * @author Juergen Hoeller + * @since 3.2 + * @deprecated as of 5.3, in favor of using Spring Boot actuators for such needs + */ +@Deprecated +public interface LiveBeansViewMBean { + + /** + * Generate a JSON snapshot of current beans and their dependencies. + */ + String getSnapshotAsJson(); + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/MessageSourceAccessor.java b/spring-context/src/main/java/org/springframework/context/support/MessageSourceAccessor.java new file mode 100644 index 0000000..6c36abd --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/MessageSourceAccessor.java @@ -0,0 +1,195 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.util.Locale; + +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.NoSuchMessageException; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.lang.Nullable; + +/** + * Helper class for easy access to messages from a MessageSource, + * providing various overloaded getMessage methods. + * + *

    Available from ApplicationObjectSupport, but also reusable + * as a standalone helper to delegate to in application objects. + * + * @author Juergen Hoeller + * @since 23.10.2003 + * @see ApplicationObjectSupport#getMessageSourceAccessor + */ +public class MessageSourceAccessor { + + private final MessageSource messageSource; + + @Nullable + private final Locale defaultLocale; + + + /** + * Create a new MessageSourceAccessor, using LocaleContextHolder's locale + * as default locale. + * @param messageSource the MessageSource to wrap + * @see org.springframework.context.i18n.LocaleContextHolder#getLocale() + */ + public MessageSourceAccessor(MessageSource messageSource) { + this.messageSource = messageSource; + this.defaultLocale = null; + } + + /** + * Create a new MessageSourceAccessor, using the given default locale. + * @param messageSource the MessageSource to wrap + * @param defaultLocale the default locale to use for message access + */ + public MessageSourceAccessor(MessageSource messageSource, Locale defaultLocale) { + this.messageSource = messageSource; + this.defaultLocale = defaultLocale; + } + + + /** + * Return the default locale to use if no explicit locale has been given. + *

    The default implementation returns the default locale passed into the + * corresponding constructor, or LocaleContextHolder's locale as fallback. + * Can be overridden in subclasses. + * @see #MessageSourceAccessor(org.springframework.context.MessageSource, java.util.Locale) + * @see org.springframework.context.i18n.LocaleContextHolder#getLocale() + */ + protected Locale getDefaultLocale() { + return (this.defaultLocale != null ? this.defaultLocale : LocaleContextHolder.getLocale()); + } + + /** + * Retrieve the message for the given code and the default Locale. + * @param code the code of the message + * @param defaultMessage the String to return if the lookup fails + * @return the message + */ + public String getMessage(String code, String defaultMessage) { + String msg = this.messageSource.getMessage(code, null, defaultMessage, getDefaultLocale()); + return (msg != null ? msg : ""); + } + + /** + * Retrieve the message for the given code and the given Locale. + * @param code the code of the message + * @param defaultMessage the String to return if the lookup fails + * @param locale the Locale in which to do lookup + * @return the message + */ + public String getMessage(String code, String defaultMessage, Locale locale) { + String msg = this.messageSource.getMessage(code, null, defaultMessage, locale); + return (msg != null ? msg : ""); + } + + /** + * Retrieve the message for the given code and the default Locale. + * @param code the code of the message + * @param args arguments for the message, or {@code null} if none + * @param defaultMessage the String to return if the lookup fails + * @return the message + */ + public String getMessage(String code, @Nullable Object[] args, String defaultMessage) { + String msg = this.messageSource.getMessage(code, args, defaultMessage, getDefaultLocale()); + return (msg != null ? msg : ""); + } + + /** + * Retrieve the message for the given code and the given Locale. + * @param code the code of the message + * @param args arguments for the message, or {@code null} if none + * @param defaultMessage the String to return if the lookup fails + * @param locale the Locale in which to do lookup + * @return the message + */ + public String getMessage(String code, @Nullable Object[] args, String defaultMessage, Locale locale) { + String msg = this.messageSource.getMessage(code, args, defaultMessage, locale); + return (msg != null ? msg : ""); + } + + /** + * Retrieve the message for the given code and the default Locale. + * @param code the code of the message + * @return the message + * @throws org.springframework.context.NoSuchMessageException if not found + */ + public String getMessage(String code) throws NoSuchMessageException { + return this.messageSource.getMessage(code, null, getDefaultLocale()); + } + + /** + * Retrieve the message for the given code and the given Locale. + * @param code the code of the message + * @param locale the Locale in which to do lookup + * @return the message + * @throws org.springframework.context.NoSuchMessageException if not found + */ + public String getMessage(String code, Locale locale) throws NoSuchMessageException { + return this.messageSource.getMessage(code, null, locale); + } + + /** + * Retrieve the message for the given code and the default Locale. + * @param code the code of the message + * @param args arguments for the message, or {@code null} if none + * @return the message + * @throws org.springframework.context.NoSuchMessageException if not found + */ + public String getMessage(String code, @Nullable Object[] args) throws NoSuchMessageException { + return this.messageSource.getMessage(code, args, getDefaultLocale()); + } + + /** + * Retrieve the message for the given code and the given Locale. + * @param code the code of the message + * @param args arguments for the message, or {@code null} if none + * @param locale the Locale in which to do lookup + * @return the message + * @throws org.springframework.context.NoSuchMessageException if not found + */ + public String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException { + return this.messageSource.getMessage(code, args, locale); + } + + /** + * Retrieve the given MessageSourceResolvable (e.g. an ObjectError instance) + * in the default Locale. + * @param resolvable the MessageSourceResolvable + * @return the message + * @throws org.springframework.context.NoSuchMessageException if not found + */ + public String getMessage(MessageSourceResolvable resolvable) throws NoSuchMessageException { + return this.messageSource.getMessage(resolvable, getDefaultLocale()); + } + + /** + * Retrieve the given MessageSourceResolvable (e.g. an ObjectError instance) + * in the given Locale. + * @param resolvable the MessageSourceResolvable + * @param locale the Locale in which to do lookup + * @return the message + * @throws org.springframework.context.NoSuchMessageException if not found + */ + public String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException { + return this.messageSource.getMessage(resolvable, locale); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/MessageSourceResourceBundle.java b/spring-context/src/main/java/org/springframework/context/support/MessageSourceResourceBundle.java new file mode 100644 index 0000000..b860a38 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/MessageSourceResourceBundle.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.util.Enumeration; +import java.util.Locale; +import java.util.ResourceBundle; + +import org.springframework.context.MessageSource; +import org.springframework.context.NoSuchMessageException; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Helper class that allows for accessing a Spring + * {@link org.springframework.context.MessageSource} as a {@link java.util.ResourceBundle}. + * Used for example to expose a Spring MessageSource to JSTL web views. + * + * @author Juergen Hoeller + * @since 27.02.2003 + * @see org.springframework.context.MessageSource + * @see java.util.ResourceBundle + * @see org.springframework.web.servlet.support.JstlUtils#exposeLocalizationContext + */ +public class MessageSourceResourceBundle extends ResourceBundle { + + private final MessageSource messageSource; + + private final Locale locale; + + + /** + * Create a new MessageSourceResourceBundle for the given MessageSource and Locale. + * @param source the MessageSource to retrieve messages from + * @param locale the Locale to retrieve messages for + */ + public MessageSourceResourceBundle(MessageSource source, Locale locale) { + Assert.notNull(source, "MessageSource must not be null"); + this.messageSource = source; + this.locale = locale; + } + + /** + * Create a new MessageSourceResourceBundle for the given MessageSource and Locale. + * @param source the MessageSource to retrieve messages from + * @param locale the Locale to retrieve messages for + * @param parent the parent ResourceBundle to delegate to if no local message found + */ + public MessageSourceResourceBundle(MessageSource source, Locale locale, ResourceBundle parent) { + this(source, locale); + setParent(parent); + } + + + /** + * This implementation resolves the code in the MessageSource. + * Returns {@code null} if the message could not be resolved. + */ + @Override + @Nullable + protected Object handleGetObject(String key) { + try { + return this.messageSource.getMessage(key, null, this.locale); + } + catch (NoSuchMessageException ex) { + return null; + } + } + + /** + * This implementation checks whether the target MessageSource can resolve + * a message for the given key, translating {@code NoSuchMessageException} + * accordingly. In contrast to ResourceBundle's default implementation in + * JDK 1.6, this does not rely on the capability to enumerate message keys. + */ + @Override + public boolean containsKey(String key) { + try { + this.messageSource.getMessage(key, null, this.locale); + return true; + } + catch (NoSuchMessageException ex) { + return false; + } + } + + /** + * This implementation throws {@code UnsupportedOperationException}, + * as a MessageSource does not allow for enumerating the defined message codes. + */ + @Override + public Enumeration getKeys() { + throw new UnsupportedOperationException("MessageSourceResourceBundle does not support enumerating its keys"); + } + + /** + * This implementation exposes the specified Locale for introspection + * through the standard {@code ResourceBundle.getLocale()} method. + */ + @Override + public Locale getLocale() { + return this.locale; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/MessageSourceSupport.java b/spring-context/src/main/java/org/springframework/context/support/MessageSourceSupport.java new file mode 100644 index 0000000..c8e24fa --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/MessageSourceSupport.java @@ -0,0 +1,174 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +/** + * Base class for message source implementations, providing support infrastructure + * such as {@link java.text.MessageFormat} handling but not implementing concrete + * methods defined in the {@link org.springframework.context.MessageSource}. + * + *

    {@link AbstractMessageSource} derives from this class, providing concrete + * {@code getMessage} implementations that delegate to a central template + * method for message code resolution. + * + * @author Juergen Hoeller + * @since 2.5.5 + */ +public abstract class MessageSourceSupport { + + private static final MessageFormat INVALID_MESSAGE_FORMAT = new MessageFormat(""); + + /** Logger available to subclasses. */ + protected final Log logger = LogFactory.getLog(getClass()); + + private boolean alwaysUseMessageFormat = false; + + /** + * Cache to hold already generated MessageFormats per message. + * Used for passed-in default messages. MessageFormats for resolved + * codes are cached on a specific basis in subclasses. + */ + private final Map> messageFormatsPerMessage = new HashMap<>(); + + + /** + * Set whether to always apply the {@code MessageFormat} rules, + * parsing even messages without arguments. + *

    Default is "false": Messages without arguments are by default + * returned as-is, without parsing them through MessageFormat. + * Set this to "true" to enforce MessageFormat for all messages, + * expecting all message texts to be written with MessageFormat escaping. + *

    For example, MessageFormat expects a single quote to be escaped + * as "''". If your message texts are all written with such escaping, + * even when not defining argument placeholders, you need to set this + * flag to "true". Else, only message texts with actual arguments + * are supposed to be written with MessageFormat escaping. + * @see java.text.MessageFormat + */ + public void setAlwaysUseMessageFormat(boolean alwaysUseMessageFormat) { + this.alwaysUseMessageFormat = alwaysUseMessageFormat; + } + + /** + * Return whether to always apply the MessageFormat rules, parsing even + * messages without arguments. + */ + protected boolean isAlwaysUseMessageFormat() { + return this.alwaysUseMessageFormat; + } + + + /** + * Render the given default message String. The default message is + * passed in as specified by the caller and can be rendered into + * a fully formatted default message shown to the user. + *

    The default implementation passes the String to {@code formatMessage}, + * resolving any argument placeholders found in them. Subclasses may override + * this method to plug in custom processing of default messages. + * @param defaultMessage the passed-in default message String + * @param args array of arguments that will be filled in for params within + * the message, or {@code null} if none. + * @param locale the Locale used for formatting + * @return the rendered default message (with resolved arguments) + * @see #formatMessage(String, Object[], java.util.Locale) + */ + protected String renderDefaultMessage(String defaultMessage, @Nullable Object[] args, Locale locale) { + return formatMessage(defaultMessage, args, locale); + } + + /** + * Format the given message String, using cached MessageFormats. + * By default invoked for passed-in default messages, to resolve + * any argument placeholders found in them. + * @param msg the message to format + * @param args array of arguments that will be filled in for params within + * the message, or {@code null} if none + * @param locale the Locale used for formatting + * @return the formatted message (with resolved arguments) + */ + protected String formatMessage(String msg, @Nullable Object[] args, Locale locale) { + if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) { + return msg; + } + MessageFormat messageFormat = null; + synchronized (this.messageFormatsPerMessage) { + Map messageFormatsPerLocale = this.messageFormatsPerMessage.get(msg); + if (messageFormatsPerLocale != null) { + messageFormat = messageFormatsPerLocale.get(locale); + } + else { + messageFormatsPerLocale = new HashMap<>(); + this.messageFormatsPerMessage.put(msg, messageFormatsPerLocale); + } + if (messageFormat == null) { + try { + messageFormat = createMessageFormat(msg, locale); + } + catch (IllegalArgumentException ex) { + // Invalid message format - probably not intended for formatting, + // rather using a message structure with no arguments involved... + if (isAlwaysUseMessageFormat()) { + throw ex; + } + // Silently proceed with raw message if format not enforced... + messageFormat = INVALID_MESSAGE_FORMAT; + } + messageFormatsPerLocale.put(locale, messageFormat); + } + } + if (messageFormat == INVALID_MESSAGE_FORMAT) { + return msg; + } + synchronized (messageFormat) { + return messageFormat.format(resolveArguments(args, locale)); + } + } + + /** + * Create a MessageFormat for the given message and Locale. + * @param msg the message to create a MessageFormat for + * @param locale the Locale to create a MessageFormat for + * @return the MessageFormat instance + */ + protected MessageFormat createMessageFormat(String msg, Locale locale) { + return new MessageFormat(msg, locale); + } + + /** + * Template method for resolving argument objects. + *

    The default implementation simply returns the given argument array as-is. + * Can be overridden in subclasses in order to resolve special argument types. + * @param args the original argument array + * @param locale the Locale to resolve against + * @return the resolved argument array + */ + protected Object[] resolveArguments(@Nullable Object[] args, Locale locale) { + return (args != null ? args : new Object[0]); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/PostProcessorRegistrationDelegate.java b/spring-context/src/main/java/org/springframework/context/support/PostProcessorRegistrationDelegate.java new file mode 100644 index 0000000..f74909a --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/PostProcessorRegistrationDelegate.java @@ -0,0 +1,367 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.AbstractBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.OrderComparator; +import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; +import org.springframework.core.metrics.ApplicationStartup; +import org.springframework.core.metrics.StartupStep; +import org.springframework.lang.Nullable; + +/** + * Delegate for AbstractApplicationContext's post-processor handling. + * + * @author Juergen Hoeller + * @author Sam Brannen + * @since 4.0 + */ +final class PostProcessorRegistrationDelegate { + + private PostProcessorRegistrationDelegate() { + } + + + public static void invokeBeanFactoryPostProcessors( + ConfigurableListableBeanFactory beanFactory, List beanFactoryPostProcessors) { + + // Invoke BeanDefinitionRegistryPostProcessors first, if any. + Set processedBeans = new HashSet<>(); + + if (beanFactory instanceof BeanDefinitionRegistry) { + BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; + List regularPostProcessors = new ArrayList<>(); + List registryProcessors = new ArrayList<>(); + + for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors) { + if (postProcessor instanceof BeanDefinitionRegistryPostProcessor) { + BeanDefinitionRegistryPostProcessor registryProcessor = + (BeanDefinitionRegistryPostProcessor) postProcessor; + registryProcessor.postProcessBeanDefinitionRegistry(registry); + registryProcessors.add(registryProcessor); + } + else { + regularPostProcessors.add(postProcessor); + } + } + + // Do not initialize FactoryBeans here: We need to leave all regular beans + // uninitialized to let the bean factory post-processors apply to them! + // Separate between BeanDefinitionRegistryPostProcessors that implement + // PriorityOrdered, Ordered, and the rest. + List currentRegistryProcessors = new ArrayList<>(); + + // First, invoke the BeanDefinitionRegistryPostProcessors that implement PriorityOrdered. + String[] postProcessorNames = + beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false); + for (String ppName : postProcessorNames) { + if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) { + currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class)); + processedBeans.add(ppName); + } + } + sortPostProcessors(currentRegistryProcessors, beanFactory); + registryProcessors.addAll(currentRegistryProcessors); + invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup()); + currentRegistryProcessors.clear(); + + // Next, invoke the BeanDefinitionRegistryPostProcessors that implement Ordered. + postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false); + for (String ppName : postProcessorNames) { + if (!processedBeans.contains(ppName) && beanFactory.isTypeMatch(ppName, Ordered.class)) { + currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class)); + processedBeans.add(ppName); + } + } + sortPostProcessors(currentRegistryProcessors, beanFactory); + registryProcessors.addAll(currentRegistryProcessors); + invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup()); + currentRegistryProcessors.clear(); + + // Finally, invoke all other BeanDefinitionRegistryPostProcessors until no further ones appear. + boolean reiterate = true; + while (reiterate) { + reiterate = false; + postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false); + for (String ppName : postProcessorNames) { + if (!processedBeans.contains(ppName)) { + currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class)); + processedBeans.add(ppName); + reiterate = true; + } + } + sortPostProcessors(currentRegistryProcessors, beanFactory); + registryProcessors.addAll(currentRegistryProcessors); + invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup()); + currentRegistryProcessors.clear(); + } + + // Now, invoke the postProcessBeanFactory callback of all processors handled so far. + invokeBeanFactoryPostProcessors(registryProcessors, beanFactory); + invokeBeanFactoryPostProcessors(regularPostProcessors, beanFactory); + } + + else { + // Invoke factory processors registered with the context instance. + invokeBeanFactoryPostProcessors(beanFactoryPostProcessors, beanFactory); + } + + // Do not initialize FactoryBeans here: We need to leave all regular beans + // uninitialized to let the bean factory post-processors apply to them! + String[] postProcessorNames = + beanFactory.getBeanNamesForType(BeanFactoryPostProcessor.class, true, false); + + // Separate between BeanFactoryPostProcessors that implement PriorityOrdered, + // Ordered, and the rest. + List priorityOrderedPostProcessors = new ArrayList<>(); + List orderedPostProcessorNames = new ArrayList<>(); + List nonOrderedPostProcessorNames = new ArrayList<>(); + for (String ppName : postProcessorNames) { + if (processedBeans.contains(ppName)) { + // skip - already processed in first phase above + } + else if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) { + priorityOrderedPostProcessors.add(beanFactory.getBean(ppName, BeanFactoryPostProcessor.class)); + } + else if (beanFactory.isTypeMatch(ppName, Ordered.class)) { + orderedPostProcessorNames.add(ppName); + } + else { + nonOrderedPostProcessorNames.add(ppName); + } + } + + // First, invoke the BeanFactoryPostProcessors that implement PriorityOrdered. + sortPostProcessors(priorityOrderedPostProcessors, beanFactory); + invokeBeanFactoryPostProcessors(priorityOrderedPostProcessors, beanFactory); + + // Next, invoke the BeanFactoryPostProcessors that implement Ordered. + List orderedPostProcessors = new ArrayList<>(orderedPostProcessorNames.size()); + for (String postProcessorName : orderedPostProcessorNames) { + orderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class)); + } + sortPostProcessors(orderedPostProcessors, beanFactory); + invokeBeanFactoryPostProcessors(orderedPostProcessors, beanFactory); + + // Finally, invoke all other BeanFactoryPostProcessors. + List nonOrderedPostProcessors = new ArrayList<>(nonOrderedPostProcessorNames.size()); + for (String postProcessorName : nonOrderedPostProcessorNames) { + nonOrderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class)); + } + invokeBeanFactoryPostProcessors(nonOrderedPostProcessors, beanFactory); + + // Clear cached merged bean definitions since the post-processors might have + // modified the original metadata, e.g. replacing placeholders in values... + beanFactory.clearMetadataCache(); + } + + public static void registerBeanPostProcessors( + ConfigurableListableBeanFactory beanFactory, AbstractApplicationContext applicationContext) { + + String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanPostProcessor.class, true, false); + + // Register BeanPostProcessorChecker that logs an info message when + // a bean is created during BeanPostProcessor instantiation, i.e. when + // a bean is not eligible for getting processed by all BeanPostProcessors. + int beanProcessorTargetCount = beanFactory.getBeanPostProcessorCount() + 1 + postProcessorNames.length; + beanFactory.addBeanPostProcessor(new BeanPostProcessorChecker(beanFactory, beanProcessorTargetCount)); + + // Separate between BeanPostProcessors that implement PriorityOrdered, + // Ordered, and the rest. + List priorityOrderedPostProcessors = new ArrayList<>(); + List internalPostProcessors = new ArrayList<>(); + List orderedPostProcessorNames = new ArrayList<>(); + List nonOrderedPostProcessorNames = new ArrayList<>(); + for (String ppName : postProcessorNames) { + if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) { + BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class); + priorityOrderedPostProcessors.add(pp); + if (pp instanceof MergedBeanDefinitionPostProcessor) { + internalPostProcessors.add(pp); + } + } + else if (beanFactory.isTypeMatch(ppName, Ordered.class)) { + orderedPostProcessorNames.add(ppName); + } + else { + nonOrderedPostProcessorNames.add(ppName); + } + } + + // First, register the BeanPostProcessors that implement PriorityOrdered. + sortPostProcessors(priorityOrderedPostProcessors, beanFactory); + registerBeanPostProcessors(beanFactory, priorityOrderedPostProcessors); + + // Next, register the BeanPostProcessors that implement Ordered. + List orderedPostProcessors = new ArrayList<>(orderedPostProcessorNames.size()); + for (String ppName : orderedPostProcessorNames) { + BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class); + orderedPostProcessors.add(pp); + if (pp instanceof MergedBeanDefinitionPostProcessor) { + internalPostProcessors.add(pp); + } + } + sortPostProcessors(orderedPostProcessors, beanFactory); + registerBeanPostProcessors(beanFactory, orderedPostProcessors); + + // Now, register all regular BeanPostProcessors. + List nonOrderedPostProcessors = new ArrayList<>(nonOrderedPostProcessorNames.size()); + for (String ppName : nonOrderedPostProcessorNames) { + BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class); + nonOrderedPostProcessors.add(pp); + if (pp instanceof MergedBeanDefinitionPostProcessor) { + internalPostProcessors.add(pp); + } + } + registerBeanPostProcessors(beanFactory, nonOrderedPostProcessors); + + // Finally, re-register all internal BeanPostProcessors. + sortPostProcessors(internalPostProcessors, beanFactory); + registerBeanPostProcessors(beanFactory, internalPostProcessors); + + // Re-register post-processor for detecting inner beans as ApplicationListeners, + // moving it to the end of the processor chain (for picking up proxies etc). + beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(applicationContext)); + } + + private static void sortPostProcessors(List postProcessors, ConfigurableListableBeanFactory beanFactory) { + // Nothing to sort? + if (postProcessors.size() <= 1) { + return; + } + Comparator comparatorToUse = null; + if (beanFactory instanceof DefaultListableBeanFactory) { + comparatorToUse = ((DefaultListableBeanFactory) beanFactory).getDependencyComparator(); + } + if (comparatorToUse == null) { + comparatorToUse = OrderComparator.INSTANCE; + } + postProcessors.sort(comparatorToUse); + } + + /** + * Invoke the given BeanDefinitionRegistryPostProcessor beans. + */ + private static void invokeBeanDefinitionRegistryPostProcessors( + Collection postProcessors, BeanDefinitionRegistry registry, ApplicationStartup applicationStartup) { + + for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) { + StartupStep postProcessBeanDefRegistry = applicationStartup.start("spring.context.beandef-registry.post-process") + .tag("postProcessor", postProcessor::toString); + postProcessor.postProcessBeanDefinitionRegistry(registry); + postProcessBeanDefRegistry.end(); + } + } + + /** + * Invoke the given BeanFactoryPostProcessor beans. + */ + private static void invokeBeanFactoryPostProcessors( + Collection postProcessors, ConfigurableListableBeanFactory beanFactory) { + + for (BeanFactoryPostProcessor postProcessor : postProcessors) { + StartupStep postProcessBeanFactory = beanFactory.getApplicationStartup().start("spring.context.bean-factory.post-process") + .tag("postProcessor", postProcessor::toString); + postProcessor.postProcessBeanFactory(beanFactory); + postProcessBeanFactory.end(); + } + } + + /** + * Register the given BeanPostProcessor beans. + */ + private static void registerBeanPostProcessors( + ConfigurableListableBeanFactory beanFactory, List postProcessors) { + + if (beanFactory instanceof AbstractBeanFactory) { + // Bulk addition is more efficient against our CopyOnWriteArrayList there + ((AbstractBeanFactory) beanFactory).addBeanPostProcessors(postProcessors); + } + else { + for (BeanPostProcessor postProcessor : postProcessors) { + beanFactory.addBeanPostProcessor(postProcessor); + } + } + } + + + /** + * BeanPostProcessor that logs an info message when a bean is created during + * BeanPostProcessor instantiation, i.e. when a bean is not eligible for + * getting processed by all BeanPostProcessors. + */ + private static final class BeanPostProcessorChecker implements BeanPostProcessor { + + private static final Log logger = LogFactory.getLog(BeanPostProcessorChecker.class); + + private final ConfigurableListableBeanFactory beanFactory; + + private final int beanPostProcessorTargetCount; + + public BeanPostProcessorChecker(ConfigurableListableBeanFactory beanFactory, int beanPostProcessorTargetCount) { + this.beanFactory = beanFactory; + this.beanPostProcessorTargetCount = beanPostProcessorTargetCount; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) { + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + if (!(bean instanceof BeanPostProcessor) && !isInfrastructureBean(beanName) && + this.beanFactory.getBeanPostProcessorCount() < this.beanPostProcessorTargetCount) { + if (logger.isInfoEnabled()) { + logger.info("Bean '" + beanName + "' of type [" + bean.getClass().getName() + + "] is not eligible for getting processed by all BeanPostProcessors " + + "(for example: not eligible for auto-proxying)"); + } + } + return bean; + } + + private boolean isInfrastructureBean(@Nullable String beanName) { + if (beanName != null && this.beanFactory.containsBeanDefinition(beanName)) { + BeanDefinition bd = this.beanFactory.getBeanDefinition(beanName); + return (bd.getRole() == RootBeanDefinition.ROLE_INFRASTRUCTURE); + } + return false; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurer.java b/spring-context/src/main/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurer.java new file mode 100644 index 0000000..abbfec0 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurer.java @@ -0,0 +1,211 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.io.IOException; +import java.util.Properties; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.PlaceholderConfigurerSupport; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.ConfigurablePropertyResolver; +import org.springframework.core.env.Environment; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertiesPropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.core.env.PropertySources; +import org.springframework.core.env.PropertySourcesPropertyResolver; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringValueResolver; + +/** + * Specialization of {@link PlaceholderConfigurerSupport} that resolves ${...} placeholders + * within bean definition property values and {@code @Value} annotations against the current + * Spring {@link Environment} and its set of {@link PropertySources}. + * + *

    This class is designed as a general replacement for {@code PropertyPlaceholderConfigurer}. + * It is used by default to support the {@code property-placeholder} element in working against + * the spring-context-3.1 or higher XSD; whereas, spring-context versions <= 3.0 default to + * {@code PropertyPlaceholderConfigurer} to ensure backward compatibility. See the spring-context + * XSD documentation for complete details. + * + *

    Any local properties (e.g. those added via {@link #setProperties}, {@link #setLocations} + * et al.) are added as a {@code PropertySource}. Search precedence of local properties is + * based on the value of the {@link #setLocalOverride localOverride} property, which is by + * default {@code false} meaning that local properties are to be searched last, after all + * environment property sources. + * + *

    See {@link org.springframework.core.env.ConfigurableEnvironment} and related javadocs + * for details on manipulating environment property sources. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + * @see org.springframework.core.env.ConfigurableEnvironment + * @see org.springframework.beans.factory.config.PlaceholderConfigurerSupport + * @see org.springframework.beans.factory.config.PropertyPlaceholderConfigurer + */ +public class PropertySourcesPlaceholderConfigurer extends PlaceholderConfigurerSupport implements EnvironmentAware { + + /** + * {@value} is the name given to the {@link PropertySource} for the set of + * {@linkplain #mergeProperties() merged properties} supplied to this configurer. + */ + public static final String LOCAL_PROPERTIES_PROPERTY_SOURCE_NAME = "localProperties"; + + /** + * {@value} is the name given to the {@link PropertySource} that wraps the + * {@linkplain #setEnvironment environment} supplied to this configurer. + */ + public static final String ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME = "environmentProperties"; + + + @Nullable + private MutablePropertySources propertySources; + + @Nullable + private PropertySources appliedPropertySources; + + @Nullable + private Environment environment; + + + /** + * Customize the set of {@link PropertySources} to be used by this configurer. + *

    Setting this property indicates that environment property sources and + * local properties should be ignored. + * @see #postProcessBeanFactory + */ + public void setPropertySources(PropertySources propertySources) { + this.propertySources = new MutablePropertySources(propertySources); + } + + /** + * {@code PropertySources} from the given {@link Environment} + * will be searched when replacing ${...} placeholders. + * @see #setPropertySources + * @see #postProcessBeanFactory + */ + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + + /** + * Processing occurs by replacing ${...} placeholders in bean definitions by resolving each + * against this configurer's set of {@link PropertySources}, which includes: + *

      + *
    • all {@linkplain org.springframework.core.env.ConfigurableEnvironment#getPropertySources + * environment property sources}, if an {@code Environment} {@linkplain #setEnvironment is present} + *
    • {@linkplain #mergeProperties merged local properties}, if {@linkplain #setLocation any} + * {@linkplain #setLocations have} {@linkplain #setProperties been} + * {@linkplain #setPropertiesArray specified} + *
    • any property sources set by calling {@link #setPropertySources} + *
    + *

    If {@link #setPropertySources} is called, environment and local properties will be + * ignored. This method is designed to give the user fine-grained control over property + * sources, and once set, the configurer makes no assumptions about adding additional sources. + */ + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + if (this.propertySources == null) { + this.propertySources = new MutablePropertySources(); + if (this.environment != null) { + this.propertySources.addLast( + new PropertySource(ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME, this.environment) { + @Override + @Nullable + public String getProperty(String key) { + return this.source.getProperty(key); + } + } + ); + } + try { + PropertySource localPropertySource = + new PropertiesPropertySource(LOCAL_PROPERTIES_PROPERTY_SOURCE_NAME, mergeProperties()); + if (this.localOverride) { + this.propertySources.addFirst(localPropertySource); + } + else { + this.propertySources.addLast(localPropertySource); + } + } + catch (IOException ex) { + throw new BeanInitializationException("Could not load properties", ex); + } + } + + processProperties(beanFactory, new PropertySourcesPropertyResolver(this.propertySources)); + this.appliedPropertySources = this.propertySources; + } + + /** + * Visit each bean definition in the given bean factory and attempt to replace ${...} property + * placeholders with values from the given properties. + */ + protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, + final ConfigurablePropertyResolver propertyResolver) throws BeansException { + + propertyResolver.setPlaceholderPrefix(this.placeholderPrefix); + propertyResolver.setPlaceholderSuffix(this.placeholderSuffix); + propertyResolver.setValueSeparator(this.valueSeparator); + + StringValueResolver valueResolver = strVal -> { + String resolved = (this.ignoreUnresolvablePlaceholders ? + propertyResolver.resolvePlaceholders(strVal) : + propertyResolver.resolveRequiredPlaceholders(strVal)); + if (this.trimValues) { + resolved = resolved.trim(); + } + return (resolved.equals(this.nullValue) ? null : resolved); + }; + + doProcessProperties(beanFactoryToProcess, valueResolver); + } + + /** + * Implemented for compatibility with + * {@link org.springframework.beans.factory.config.PlaceholderConfigurerSupport}. + * @deprecated in favor of + * {@link #processProperties(ConfigurableListableBeanFactory, ConfigurablePropertyResolver)} + * @throws UnsupportedOperationException in this implementation + */ + @Override + @Deprecated + protected void processProperties(ConfigurableListableBeanFactory beanFactory, Properties props) { + throw new UnsupportedOperationException( + "Call processProperties(ConfigurableListableBeanFactory, ConfigurablePropertyResolver) instead"); + } + + /** + * Return the property sources that were actually applied during + * {@link #postProcessBeanFactory(ConfigurableListableBeanFactory) post-processing}. + * @return the property sources that were applied + * @throws IllegalStateException if the property sources have not yet been applied + * @since 4.0 + */ + public PropertySources getAppliedPropertySources() throws IllegalStateException { + Assert.state(this.appliedPropertySources != null, "PropertySources have not yet been applied"); + return this.appliedPropertySources; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java new file mode 100644 index 0000000..e7f78f8 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java @@ -0,0 +1,635 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.ReentrantLock; + +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.ResourcePropertiesPersister; +import org.springframework.lang.Nullable; +import org.springframework.util.PropertiesPersister; +import org.springframework.util.StringUtils; + +/** + * Spring-specific {@link org.springframework.context.MessageSource} implementation + * that accesses resource bundles using specified basenames, participating in the + * Spring {@link org.springframework.context.ApplicationContext}'s resource loading. + * + *

    In contrast to the JDK-based {@link ResourceBundleMessageSource}, this class uses + * {@link java.util.Properties} instances as its custom data structure for messages, + * loading them via a {@link org.springframework.util.PropertiesPersister} strategy + * from Spring {@link Resource} handles. This strategy is not only capable of + * reloading files based on timestamp changes, but also of loading properties files + * with a specific character encoding. It will detect XML property files as well. + * + *

    Note that the basenames set as {@link #setBasenames "basenames"} property + * are treated in a slightly different fashion than the "basenames" property of + * {@link ResourceBundleMessageSource}. It follows the basic ResourceBundle rule of not + * specifying file extension or language codes, but can refer to any Spring resource + * location (instead of being restricted to classpath resources). With a "classpath:" + * prefix, resources can still be loaded from the classpath, but "cacheSeconds" values + * other than "-1" (caching forever) might not work reliably in this case. + * + *

    For a typical web application, message files could be placed in {@code WEB-INF}: + * e.g. a "WEB-INF/messages" basename would find a "WEB-INF/messages.properties", + * "WEB-INF/messages_en.properties" etc arrangement as well as "WEB-INF/messages.xml", + * "WEB-INF/messages_en.xml" etc. Note that message definitions in a previous + * resource bundle will override ones in a later bundle, due to sequential lookup. + + *

    This MessageSource can easily be used outside of an + * {@link org.springframework.context.ApplicationContext}: it will use a + * {@link org.springframework.core.io.DefaultResourceLoader} as default, + * simply getting overridden with the ApplicationContext's resource loader + * if running in a context. It does not have any other specific dependencies. + * + *

    Thanks to Thomas Achleitner for providing the initial implementation of + * this message source! + * + * @author Juergen Hoeller + * @see #setCacheSeconds + * @see #setBasenames + * @see #setDefaultEncoding + * @see #setFileEncodings + * @see #setPropertiesPersister + * @see #setResourceLoader + * @see ResourcePropertiesPersister + * @see org.springframework.core.io.DefaultResourceLoader + * @see ResourceBundleMessageSource + * @see java.util.ResourceBundle + */ +public class ReloadableResourceBundleMessageSource extends AbstractResourceBasedMessageSource + implements ResourceLoaderAware { + + private static final String PROPERTIES_SUFFIX = ".properties"; + + private static final String XML_SUFFIX = ".xml"; + + + @Nullable + private Properties fileEncodings; + + private boolean concurrentRefresh = true; + + private PropertiesPersister propertiesPersister = ResourcePropertiesPersister.INSTANCE; + + private ResourceLoader resourceLoader = new DefaultResourceLoader(); + + // Cache to hold filename lists per Locale + private final ConcurrentMap>> cachedFilenames = new ConcurrentHashMap<>(); + + // Cache to hold already loaded properties per filename + private final ConcurrentMap cachedProperties = new ConcurrentHashMap<>(); + + // Cache to hold already loaded properties per filename + private final ConcurrentMap cachedMergedProperties = new ConcurrentHashMap<>(); + + + /** + * Set per-file charsets to use for parsing properties files. + *

    Only applies to classic properties files, not to XML files. + * @param fileEncodings a Properties with filenames as keys and charset + * names as values. Filenames have to match the basename syntax, + * with optional locale-specific components: e.g. "WEB-INF/messages" + * or "WEB-INF/messages_en". + * @see #setBasenames + * @see org.springframework.util.PropertiesPersister#load + */ + public void setFileEncodings(Properties fileEncodings) { + this.fileEncodings = fileEncodings; + } + + /** + * Specify whether to allow for concurrent refresh behavior, i.e. one thread + * locked in a refresh attempt for a specific cached properties file whereas + * other threads keep returning the old properties for the time being, until + * the refresh attempt has completed. + *

    Default is "true": this behavior is new as of Spring Framework 4.1, + * minimizing contention between threads. If you prefer the old behavior, + * i.e. to fully block on refresh, switch this flag to "false". + * @since 4.1 + * @see #setCacheSeconds + */ + public void setConcurrentRefresh(boolean concurrentRefresh) { + this.concurrentRefresh = concurrentRefresh; + } + + /** + * Set the PropertiesPersister to use for parsing properties files. + *

    The default is ResourcePropertiesPersister. + * @see ResourcePropertiesPersister#INSTANCE + */ + public void setPropertiesPersister(@Nullable PropertiesPersister propertiesPersister) { + this.propertiesPersister = + (propertiesPersister != null ? propertiesPersister : ResourcePropertiesPersister.INSTANCE); + } + + /** + * Set the ResourceLoader to use for loading bundle properties files. + *

    The default is a DefaultResourceLoader. Will get overridden by the + * ApplicationContext if running in a context, as it implements the + * ResourceLoaderAware interface. Can be manually overridden when + * running outside of an ApplicationContext. + * @see org.springframework.core.io.DefaultResourceLoader + * @see org.springframework.context.ResourceLoaderAware + */ + @Override + public void setResourceLoader(@Nullable ResourceLoader resourceLoader) { + this.resourceLoader = (resourceLoader != null ? resourceLoader : new DefaultResourceLoader()); + } + + + /** + * Resolves the given message code as key in the retrieved bundle files, + * returning the value found in the bundle as-is (without MessageFormat parsing). + */ + @Override + protected String resolveCodeWithoutArguments(String code, Locale locale) { + if (getCacheMillis() < 0) { + PropertiesHolder propHolder = getMergedProperties(locale); + String result = propHolder.getProperty(code); + if (result != null) { + return result; + } + } + else { + for (String basename : getBasenameSet()) { + List filenames = calculateAllFilenames(basename, locale); + for (String filename : filenames) { + PropertiesHolder propHolder = getProperties(filename); + String result = propHolder.getProperty(code); + if (result != null) { + return result; + } + } + } + } + return null; + } + + /** + * Resolves the given message code as key in the retrieved bundle files, + * using a cached MessageFormat instance per message code. + */ + @Override + @Nullable + protected MessageFormat resolveCode(String code, Locale locale) { + if (getCacheMillis() < 0) { + PropertiesHolder propHolder = getMergedProperties(locale); + MessageFormat result = propHolder.getMessageFormat(code, locale); + if (result != null) { + return result; + } + } + else { + for (String basename : getBasenameSet()) { + List filenames = calculateAllFilenames(basename, locale); + for (String filename : filenames) { + PropertiesHolder propHolder = getProperties(filename); + MessageFormat result = propHolder.getMessageFormat(code, locale); + if (result != null) { + return result; + } + } + } + } + return null; + } + + + /** + * Get a PropertiesHolder that contains the actually visible properties + * for a Locale, after merging all specified resource bundles. + * Either fetches the holder from the cache or freshly loads it. + *

    Only used when caching resource bundle contents forever, i.e. + * with cacheSeconds < 0. Therefore, merged properties are always + * cached forever. + */ + protected PropertiesHolder getMergedProperties(Locale locale) { + PropertiesHolder mergedHolder = this.cachedMergedProperties.get(locale); + if (mergedHolder != null) { + return mergedHolder; + } + + Properties mergedProps = newProperties(); + long latestTimestamp = -1; + String[] basenames = StringUtils.toStringArray(getBasenameSet()); + for (int i = basenames.length - 1; i >= 0; i--) { + List filenames = calculateAllFilenames(basenames[i], locale); + for (int j = filenames.size() - 1; j >= 0; j--) { + String filename = filenames.get(j); + PropertiesHolder propHolder = getProperties(filename); + if (propHolder.getProperties() != null) { + mergedProps.putAll(propHolder.getProperties()); + if (propHolder.getFileTimestamp() > latestTimestamp) { + latestTimestamp = propHolder.getFileTimestamp(); + } + } + } + } + + mergedHolder = new PropertiesHolder(mergedProps, latestTimestamp); + PropertiesHolder existing = this.cachedMergedProperties.putIfAbsent(locale, mergedHolder); + if (existing != null) { + mergedHolder = existing; + } + return mergedHolder; + } + + /** + * Calculate all filenames for the given bundle basename and Locale. + * Will calculate filenames for the given Locale, the system Locale + * (if applicable), and the default file. + * @param basename the basename of the bundle + * @param locale the locale + * @return the List of filenames to check + * @see #setFallbackToSystemLocale + * @see #calculateFilenamesForLocale + */ + protected List calculateAllFilenames(String basename, Locale locale) { + Map> localeMap = this.cachedFilenames.get(basename); + if (localeMap != null) { + List filenames = localeMap.get(locale); + if (filenames != null) { + return filenames; + } + } + + // Filenames for given Locale + List filenames = new ArrayList<>(7); + filenames.addAll(calculateFilenamesForLocale(basename, locale)); + + // Filenames for default Locale, if any + Locale defaultLocale = getDefaultLocale(); + if (defaultLocale != null && !defaultLocale.equals(locale)) { + List fallbackFilenames = calculateFilenamesForLocale(basename, defaultLocale); + for (String fallbackFilename : fallbackFilenames) { + if (!filenames.contains(fallbackFilename)) { + // Entry for fallback locale that isn't already in filenames list. + filenames.add(fallbackFilename); + } + } + } + + // Filename for default bundle file + filenames.add(basename); + + if (localeMap == null) { + localeMap = new ConcurrentHashMap<>(); + Map> existing = this.cachedFilenames.putIfAbsent(basename, localeMap); + if (existing != null) { + localeMap = existing; + } + } + localeMap.put(locale, filenames); + return filenames; + } + + /** + * Calculate the filenames for the given bundle basename and Locale, + * appending language code, country code, and variant code. + * E.g.: basename "messages", Locale "de_AT_oo" -> "messages_de_AT_OO", + * "messages_de_AT", "messages_de". + *

    Follows the rules defined by {@link java.util.Locale#toString()}. + * @param basename the basename of the bundle + * @param locale the locale + * @return the List of filenames to check + */ + protected List calculateFilenamesForLocale(String basename, Locale locale) { + List result = new ArrayList<>(3); + String language = locale.getLanguage(); + String country = locale.getCountry(); + String variant = locale.getVariant(); + StringBuilder temp = new StringBuilder(basename); + + temp.append('_'); + if (language.length() > 0) { + temp.append(language); + result.add(0, temp.toString()); + } + + temp.append('_'); + if (country.length() > 0) { + temp.append(country); + result.add(0, temp.toString()); + } + + if (variant.length() > 0 && (language.length() > 0 || country.length() > 0)) { + temp.append('_').append(variant); + result.add(0, temp.toString()); + } + + return result; + } + + + /** + * Get a PropertiesHolder for the given filename, either from the + * cache or freshly loaded. + * @param filename the bundle filename (basename + Locale) + * @return the current PropertiesHolder for the bundle + */ + protected PropertiesHolder getProperties(String filename) { + PropertiesHolder propHolder = this.cachedProperties.get(filename); + long originalTimestamp = -2; + + if (propHolder != null) { + originalTimestamp = propHolder.getRefreshTimestamp(); + if (originalTimestamp == -1 || originalTimestamp > System.currentTimeMillis() - getCacheMillis()) { + // Up to date + return propHolder; + } + } + else { + propHolder = new PropertiesHolder(); + PropertiesHolder existingHolder = this.cachedProperties.putIfAbsent(filename, propHolder); + if (existingHolder != null) { + propHolder = existingHolder; + } + } + + // At this point, we need to refresh... + if (this.concurrentRefresh && propHolder.getRefreshTimestamp() >= 0) { + // A populated but stale holder -> could keep using it. + if (!propHolder.refreshLock.tryLock()) { + // Getting refreshed by another thread already -> + // let's return the existing properties for the time being. + return propHolder; + } + } + else { + propHolder.refreshLock.lock(); + } + try { + PropertiesHolder existingHolder = this.cachedProperties.get(filename); + if (existingHolder != null && existingHolder.getRefreshTimestamp() > originalTimestamp) { + return existingHolder; + } + return refreshProperties(filename, propHolder); + } + finally { + propHolder.refreshLock.unlock(); + } + } + + /** + * Refresh the PropertiesHolder for the given bundle filename. + * The holder can be {@code null} if not cached before, or a timed-out cache entry + * (potentially getting re-validated against the current last-modified timestamp). + * @param filename the bundle filename (basename + Locale) + * @param propHolder the current PropertiesHolder for the bundle + */ + protected PropertiesHolder refreshProperties(String filename, @Nullable PropertiesHolder propHolder) { + long refreshTimestamp = (getCacheMillis() < 0 ? -1 : System.currentTimeMillis()); + + Resource resource = this.resourceLoader.getResource(filename + PROPERTIES_SUFFIX); + if (!resource.exists()) { + resource = this.resourceLoader.getResource(filename + XML_SUFFIX); + } + + if (resource.exists()) { + long fileTimestamp = -1; + if (getCacheMillis() >= 0) { + // Last-modified timestamp of file will just be read if caching with timeout. + try { + fileTimestamp = resource.lastModified(); + if (propHolder != null && propHolder.getFileTimestamp() == fileTimestamp) { + if (logger.isDebugEnabled()) { + logger.debug("Re-caching properties for filename [" + filename + "] - file hasn't been modified"); + } + propHolder.setRefreshTimestamp(refreshTimestamp); + return propHolder; + } + } + catch (IOException ex) { + // Probably a class path resource: cache it forever. + if (logger.isDebugEnabled()) { + logger.debug(resource + " could not be resolved in the file system - assuming that it hasn't changed", ex); + } + fileTimestamp = -1; + } + } + try { + Properties props = loadProperties(resource, filename); + propHolder = new PropertiesHolder(props, fileTimestamp); + } + catch (IOException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Could not parse properties file [" + resource.getFilename() + "]", ex); + } + // Empty holder representing "not valid". + propHolder = new PropertiesHolder(); + } + } + + else { + // Resource does not exist. + if (logger.isDebugEnabled()) { + logger.debug("No properties file found for [" + filename + "] - neither plain properties nor XML"); + } + // Empty holder representing "not found". + propHolder = new PropertiesHolder(); + } + + propHolder.setRefreshTimestamp(refreshTimestamp); + this.cachedProperties.put(filename, propHolder); + return propHolder; + } + + /** + * Load the properties from the given resource. + * @param resource the resource to load from + * @param filename the original bundle filename (basename + Locale) + * @return the populated Properties instance + * @throws IOException if properties loading failed + */ + protected Properties loadProperties(Resource resource, String filename) throws IOException { + Properties props = newProperties(); + try (InputStream is = resource.getInputStream()) { + String resourceFilename = resource.getFilename(); + if (resourceFilename != null && resourceFilename.endsWith(XML_SUFFIX)) { + if (logger.isDebugEnabled()) { + logger.debug("Loading properties [" + resource.getFilename() + "]"); + } + this.propertiesPersister.loadFromXml(props, is); + } + else { + String encoding = null; + if (this.fileEncodings != null) { + encoding = this.fileEncodings.getProperty(filename); + } + if (encoding == null) { + encoding = getDefaultEncoding(); + } + if (encoding != null) { + if (logger.isDebugEnabled()) { + logger.debug("Loading properties [" + resource.getFilename() + "] with encoding '" + encoding + "'"); + } + this.propertiesPersister.load(props, new InputStreamReader(is, encoding)); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Loading properties [" + resource.getFilename() + "]"); + } + this.propertiesPersister.load(props, is); + } + } + return props; + } + } + + /** + * Template method for creating a plain new {@link Properties} instance. + * The default implementation simply calls {@link Properties#Properties()}. + *

    Allows for returning a custom {@link Properties} extension in subclasses. + * Overriding methods should just instantiate a custom {@link Properties} subclass, + * with no further initialization or population to be performed at that point. + * @return a plain Properties instance + * @since 4.2 + */ + protected Properties newProperties() { + return new Properties(); + } + + + /** + * Clear the resource bundle cache. + * Subsequent resolve calls will lead to reloading of the properties files. + */ + public void clearCache() { + logger.debug("Clearing entire resource bundle cache"); + this.cachedProperties.clear(); + this.cachedMergedProperties.clear(); + } + + /** + * Clear the resource bundle caches of this MessageSource and all its ancestors. + * @see #clearCache + */ + public void clearCacheIncludingAncestors() { + clearCache(); + if (getParentMessageSource() instanceof ReloadableResourceBundleMessageSource) { + ((ReloadableResourceBundleMessageSource) getParentMessageSource()).clearCacheIncludingAncestors(); + } + } + + + @Override + public String toString() { + return getClass().getName() + ": basenames=" + getBasenameSet(); + } + + + /** + * PropertiesHolder for caching. + * Stores the last-modified timestamp of the source file for efficient + * change detection, and the timestamp of the last refresh attempt + * (updated every time the cache entry gets re-validated). + */ + protected class PropertiesHolder { + + @Nullable + private final Properties properties; + + private final long fileTimestamp; + + private volatile long refreshTimestamp = -2; + + private final ReentrantLock refreshLock = new ReentrantLock(); + + /** Cache to hold already generated MessageFormats per message code. */ + private final ConcurrentMap> cachedMessageFormats = + new ConcurrentHashMap<>(); + + public PropertiesHolder() { + this.properties = null; + this.fileTimestamp = -1; + } + + public PropertiesHolder(Properties properties, long fileTimestamp) { + this.properties = properties; + this.fileTimestamp = fileTimestamp; + } + + @Nullable + public Properties getProperties() { + return this.properties; + } + + public long getFileTimestamp() { + return this.fileTimestamp; + } + + public void setRefreshTimestamp(long refreshTimestamp) { + this.refreshTimestamp = refreshTimestamp; + } + + public long getRefreshTimestamp() { + return this.refreshTimestamp; + } + + @Nullable + public String getProperty(String code) { + if (this.properties == null) { + return null; + } + return this.properties.getProperty(code); + } + + @Nullable + public MessageFormat getMessageFormat(String code, Locale locale) { + if (this.properties == null) { + return null; + } + Map localeMap = this.cachedMessageFormats.get(code); + if (localeMap != null) { + MessageFormat result = localeMap.get(locale); + if (result != null) { + return result; + } + } + String msg = this.properties.getProperty(code); + if (msg != null) { + if (localeMap == null) { + localeMap = new ConcurrentHashMap<>(); + Map existing = this.cachedMessageFormats.putIfAbsent(code, localeMap); + if (existing != null) { + localeMap = existing; + } + } + MessageFormat result = createMessageFormat(msg, locale); + localeMap.put(locale, result); + return result; + } + return null; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/ResourceBundleMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/ResourceBundleMessageSource.java new file mode 100644 index 0000000..f78120b --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/ResourceBundleMessageSource.java @@ -0,0 +1,476 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URL; +import java.net.URLConnection; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.text.MessageFormat; +import java.util.Locale; +import java.util.Map; +import java.util.MissingResourceException; +import java.util.PropertyResourceBundle; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * {@link org.springframework.context.MessageSource} implementation that + * accesses resource bundles using specified basenames. This class relies + * on the underlying JDK's {@link java.util.ResourceBundle} implementation, + * in combination with the JDK's standard message parsing provided by + * {@link java.text.MessageFormat}. + * + *

    This MessageSource caches both the accessed ResourceBundle instances and + * the generated MessageFormats for each message. It also implements rendering of + * no-arg messages without MessageFormat, as supported by the AbstractMessageSource + * base class. The caching provided by this MessageSource is significantly faster + * than the built-in caching of the {@code java.util.ResourceBundle} class. + * + *

    The basenames follow {@link java.util.ResourceBundle} conventions: essentially, + * a fully-qualified classpath location. If it doesn't contain a package qualifier + * (such as {@code org.mypackage}), it will be resolved from the classpath root. + * Note that the JDK's standard ResourceBundle treats dots as package separators: + * This means that "test.theme" is effectively equivalent to "test/theme". + * + *

    On the classpath, bundle resources will be read with the locally configured + * {@link #setDefaultEncoding encoding}: by default, ISO-8859-1; consider switching + * this to UTF-8, or to {@code null} for the platform default encoding. On the JDK 9+ + * module path where locally provided {@code ResourceBundle.Control} handles are not + * supported, this MessageSource always falls back to {@link ResourceBundle#getBundle} + * retrieval with the platform default encoding: UTF-8 with a ISO-8859-1 fallback on + * JDK 9+ (configurable through the "java.util.PropertyResourceBundle.encoding" system + * property). Note that {@link #loadBundle(Reader)}/{@link #loadBundle(InputStream)} + * won't be called in this case either, effectively ignoring overrides in subclasses. + * Consider implementing a JDK 9 {@code java.util.spi.ResourceBundleProvider} instead. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Qimiao Chen + * @see #setBasenames + * @see ReloadableResourceBundleMessageSource + * @see java.util.ResourceBundle + * @see java.text.MessageFormat + */ +public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSource implements BeanClassLoaderAware { + + @Nullable + private ClassLoader bundleClassLoader; + + @Nullable + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + /** + * Cache to hold loaded ResourceBundles. + * This Map is keyed with the bundle basename, which holds a Map that is + * keyed with the Locale and in turn holds the ResourceBundle instances. + * This allows for very efficient hash lookups, significantly faster + * than the ResourceBundle class's own cache. + */ + private final Map> cachedResourceBundles = + new ConcurrentHashMap<>(); + + /** + * Cache to hold already generated MessageFormats. + * This Map is keyed with the ResourceBundle, which holds a Map that is + * keyed with the message code, which in turn holds a Map that is keyed + * with the Locale and holds the MessageFormat values. This allows for + * very efficient hash lookups without concatenated keys. + * @see #getMessageFormat + */ + private final Map>> cachedBundleMessageFormats = + new ConcurrentHashMap<>(); + + @Nullable + private volatile MessageSourceControl control = new MessageSourceControl(); + + + public ResourceBundleMessageSource() { + setDefaultEncoding("ISO-8859-1"); + } + + + /** + * Set the ClassLoader to load resource bundles with. + *

    Default is the containing BeanFactory's + * {@link org.springframework.beans.factory.BeanClassLoaderAware bean ClassLoader}, + * or the default ClassLoader determined by + * {@link org.springframework.util.ClassUtils#getDefaultClassLoader()} + * if not running within a BeanFactory. + */ + public void setBundleClassLoader(ClassLoader classLoader) { + this.bundleClassLoader = classLoader; + } + + /** + * Return the ClassLoader to load resource bundles with. + *

    Default is the containing BeanFactory's bean ClassLoader. + * @see #setBundleClassLoader + */ + @Nullable + protected ClassLoader getBundleClassLoader() { + return (this.bundleClassLoader != null ? this.bundleClassLoader : this.beanClassLoader); + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + + /** + * Resolves the given message code as key in the registered resource bundles, + * returning the value found in the bundle as-is (without MessageFormat parsing). + */ + @Override + protected String resolveCodeWithoutArguments(String code, Locale locale) { + Set basenames = getBasenameSet(); + for (String basename : basenames) { + ResourceBundle bundle = getResourceBundle(basename, locale); + if (bundle != null) { + String result = getStringOrNull(bundle, code); + if (result != null) { + return result; + } + } + } + return null; + } + + /** + * Resolves the given message code as key in the registered resource bundles, + * using a cached MessageFormat instance per message code. + */ + @Override + @Nullable + protected MessageFormat resolveCode(String code, Locale locale) { + Set basenames = getBasenameSet(); + for (String basename : basenames) { + ResourceBundle bundle = getResourceBundle(basename, locale); + if (bundle != null) { + MessageFormat messageFormat = getMessageFormat(bundle, code, locale); + if (messageFormat != null) { + return messageFormat; + } + } + } + return null; + } + + + /** + * Return a ResourceBundle for the given basename and Locale, + * fetching already generated ResourceBundle from the cache. + * @param basename the basename of the ResourceBundle + * @param locale the Locale to find the ResourceBundle for + * @return the resulting ResourceBundle, or {@code null} if none + * found for the given basename and Locale + */ + @Nullable + protected ResourceBundle getResourceBundle(String basename, Locale locale) { + if (getCacheMillis() >= 0) { + // Fresh ResourceBundle.getBundle call in order to let ResourceBundle + // do its native caching, at the expense of more extensive lookup steps. + return doGetBundle(basename, locale); + } + else { + // Cache forever: prefer locale cache over repeated getBundle calls. + Map localeMap = this.cachedResourceBundles.get(basename); + if (localeMap != null) { + ResourceBundle bundle = localeMap.get(locale); + if (bundle != null) { + return bundle; + } + } + try { + ResourceBundle bundle = doGetBundle(basename, locale); + if (localeMap == null) { + localeMap = this.cachedResourceBundles.computeIfAbsent(basename, bn -> new ConcurrentHashMap<>()); + } + localeMap.put(locale, bundle); + return bundle; + } + catch (MissingResourceException ex) { + if (logger.isWarnEnabled()) { + logger.warn("ResourceBundle [" + basename + "] not found for MessageSource: " + ex.getMessage()); + } + // Assume bundle not found + // -> do NOT throw the exception to allow for checking parent message source. + return null; + } + } + } + + /** + * Obtain the resource bundle for the given basename and Locale. + * @param basename the basename to look for + * @param locale the Locale to look for + * @return the corresponding ResourceBundle + * @throws MissingResourceException if no matching bundle could be found + * @see java.util.ResourceBundle#getBundle(String, Locale, ClassLoader) + * @see #getBundleClassLoader() + */ + protected ResourceBundle doGetBundle(String basename, Locale locale) throws MissingResourceException { + ClassLoader classLoader = getBundleClassLoader(); + Assert.state(classLoader != null, "No bundle ClassLoader set"); + + MessageSourceControl control = this.control; + if (control != null) { + try { + return ResourceBundle.getBundle(basename, locale, classLoader, control); + } + catch (UnsupportedOperationException ex) { + // Probably in a Jigsaw environment on JDK 9+ + this.control = null; + String encoding = getDefaultEncoding(); + if (encoding != null && logger.isInfoEnabled()) { + logger.info("ResourceBundleMessageSource is configured to read resources with encoding '" + + encoding + "' but ResourceBundle.Control not supported in current system environment: " + + ex.getMessage() + " - falling back to plain ResourceBundle.getBundle retrieval with the " + + "platform default encoding. Consider setting the 'defaultEncoding' property to 'null' " + + "for participating in the platform default and therefore avoiding this log message."); + } + } + } + + // Fallback: plain getBundle lookup without Control handle + return ResourceBundle.getBundle(basename, locale, classLoader); + } + + /** + * Load a property-based resource bundle from the given reader. + *

    This will be called in case of a {@link #setDefaultEncoding "defaultEncoding"}, + * including {@link ResourceBundleMessageSource}'s default ISO-8859-1 encoding. + * Note that this method can only be called with a {@code ResourceBundle.Control}: + * When running on the JDK 9+ module path where such control handles are not + * supported, any overrides in custom subclasses will effectively get ignored. + *

    The default implementation returns a {@link PropertyResourceBundle}. + * @param reader the reader for the target resource + * @return the fully loaded bundle + * @throws IOException in case of I/O failure + * @since 4.2 + * @see #loadBundle(InputStream) + * @see PropertyResourceBundle#PropertyResourceBundle(Reader) + */ + protected ResourceBundle loadBundle(Reader reader) throws IOException { + return new PropertyResourceBundle(reader); + } + + /** + * Load a property-based resource bundle from the given input stream, + * picking up the default properties encoding on JDK 9+. + *

    This will only be called with {@link #setDefaultEncoding "defaultEncoding"} + * set to {@code null}, explicitly enforcing the platform default encoding + * (which is UTF-8 with a ISO-8859-1 fallback on JDK 9+ but configurable + * through the "java.util.PropertyResourceBundle.encoding" system property). + * Note that this method can only be called with a {@code ResourceBundle.Control}: + * When running on the JDK 9+ module path where such control handles are not + * supported, any overrides in custom subclasses will effectively get ignored. + *

    The default implementation returns a {@link PropertyResourceBundle}. + * @param inputStream the input stream for the target resource + * @return the fully loaded bundle + * @throws IOException in case of I/O failure + * @since 5.1 + * @see #loadBundle(Reader) + * @see PropertyResourceBundle#PropertyResourceBundle(InputStream) + */ + protected ResourceBundle loadBundle(InputStream inputStream) throws IOException { + return new PropertyResourceBundle(inputStream); + } + + /** + * Return a MessageFormat for the given bundle and code, + * fetching already generated MessageFormats from the cache. + * @param bundle the ResourceBundle to work on + * @param code the message code to retrieve + * @param locale the Locale to use to build the MessageFormat + * @return the resulting MessageFormat, or {@code null} if no message + * defined for the given code + * @throws MissingResourceException if thrown by the ResourceBundle + */ + @Nullable + protected MessageFormat getMessageFormat(ResourceBundle bundle, String code, Locale locale) + throws MissingResourceException { + + Map> codeMap = this.cachedBundleMessageFormats.get(bundle); + Map localeMap = null; + if (codeMap != null) { + localeMap = codeMap.get(code); + if (localeMap != null) { + MessageFormat result = localeMap.get(locale); + if (result != null) { + return result; + } + } + } + + String msg = getStringOrNull(bundle, code); + if (msg != null) { + if (codeMap == null) { + codeMap = this.cachedBundleMessageFormats.computeIfAbsent(bundle, b -> new ConcurrentHashMap<>()); + } + if (localeMap == null) { + localeMap = codeMap.computeIfAbsent(code, c -> new ConcurrentHashMap<>()); + } + MessageFormat result = createMessageFormat(msg, locale); + localeMap.put(locale, result); + return result; + } + + return null; + } + + /** + * Efficiently retrieve the String value for the specified key, + * or return {@code null} if not found. + *

    As of 4.2, the default implementation checks {@code containsKey} + * before it attempts to call {@code getString} (which would require + * catching {@code MissingResourceException} for key not found). + *

    Can be overridden in subclasses. + * @param bundle the ResourceBundle to perform the lookup in + * @param key the key to look up + * @return the associated value, or {@code null} if none + * @since 4.2 + * @see ResourceBundle#getString(String) + * @see ResourceBundle#containsKey(String) + */ + @Nullable + protected String getStringOrNull(ResourceBundle bundle, String key) { + if (bundle.containsKey(key)) { + try { + return bundle.getString(key); + } + catch (MissingResourceException ex) { + // Assume key not found for some other reason + // -> do NOT throw the exception to allow for checking parent message source. + } + } + return null; + } + + /** + * Show the configuration of this MessageSource. + */ + @Override + public String toString() { + return getClass().getName() + ": basenames=" + getBasenameSet(); + } + + + /** + * Custom implementation of {@code ResourceBundle.Control}, adding support + * for custom file encodings, deactivating the fallback to the system locale + * and activating ResourceBundle's native cache, if desired. + */ + private class MessageSourceControl extends ResourceBundle.Control { + + @Override + @Nullable + public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload) + throws IllegalAccessException, InstantiationException, IOException { + + // Special handling of default encoding + if (format.equals("java.properties")) { + String bundleName = toBundleName(baseName, locale); + final String resourceName = toResourceName(bundleName, "properties"); + final ClassLoader classLoader = loader; + final boolean reloadFlag = reload; + InputStream inputStream; + try { + inputStream = AccessController.doPrivileged((PrivilegedExceptionAction) () -> { + InputStream is = null; + if (reloadFlag) { + URL url = classLoader.getResource(resourceName); + if (url != null) { + URLConnection connection = url.openConnection(); + if (connection != null) { + connection.setUseCaches(false); + is = connection.getInputStream(); + } + } + } + else { + is = classLoader.getResourceAsStream(resourceName); + } + return is; + }); + } + catch (PrivilegedActionException ex) { + throw (IOException) ex.getException(); + } + if (inputStream != null) { + String encoding = getDefaultEncoding(); + if (encoding != null) { + try (InputStreamReader bundleReader = new InputStreamReader(inputStream, encoding)) { + return loadBundle(bundleReader); + } + } + else { + try (InputStream bundleStream = inputStream) { + return loadBundle(bundleStream); + } + } + } + else { + return null; + } + } + else { + // Delegate handling of "java.class" format to standard Control + return super.newBundle(baseName, locale, format, loader, reload); + } + } + + @Override + @Nullable + public Locale getFallbackLocale(String baseName, Locale locale) { + Locale defaultLocale = getDefaultLocale(); + return (defaultLocale != null && !defaultLocale.equals(locale) ? defaultLocale : null); + } + + @Override + public long getTimeToLive(String baseName, Locale locale) { + long cacheMillis = getCacheMillis(); + return (cacheMillis >= 0 ? cacheMillis : super.getTimeToLive(baseName, locale)); + } + + @Override + public boolean needsReload( + String baseName, Locale locale, String format, ClassLoader loader, ResourceBundle bundle, long loadTime) { + + if (super.needsReload(baseName, locale, format, loader, bundle, loadTime)) { + cachedBundleMessageFormats.remove(bundle); + return true; + } + else { + return false; + } + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/SimpleThreadScope.java b/spring-context/src/main/java/org/springframework/context/support/SimpleThreadScope.java new file mode 100644 index 0000000..a175051 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/SimpleThreadScope.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.config.Scope; +import org.springframework.core.NamedThreadLocal; +import org.springframework.lang.Nullable; + +/** + * A simple thread-backed {@link Scope} implementation. + * + *

    NOTE: This thread scope is not registered by default in common contexts. + * Instead, you need to explicitly assign it to a scope key in your setup, either through + * {@link org.springframework.beans.factory.config.ConfigurableBeanFactory#registerScope} + * or through a {@link org.springframework.beans.factory.config.CustomScopeConfigurer} bean. + * + *

    {@code SimpleThreadScope} does not clean up any objects associated with it. + * It is therefore typically preferable to use a request-bound scope implementation such + * as {@code org.springframework.web.context.request.RequestScope} in web environments, + * implementing the full lifecycle for scoped attributes (including reliable destruction). + * + *

    For an implementation of a thread-based {@code Scope} with support for destruction + * callbacks, refer to + * Spring by Example. + * + *

    Thanks to Eugene Kuleshov for submitting the original prototype for a thread scope! + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @since 3.0 + * @see org.springframework.web.context.request.RequestScope + */ +public class SimpleThreadScope implements Scope { + + private static final Log logger = LogFactory.getLog(SimpleThreadScope.class); + + private final ThreadLocal> threadScope = + new NamedThreadLocal>("SimpleThreadScope") { + @Override + protected Map initialValue() { + return new HashMap<>(); + } + }; + + + @Override + public Object get(String name, ObjectFactory objectFactory) { + Map scope = this.threadScope.get(); + // NOTE: Do NOT modify the following to use Map::computeIfAbsent. For details, + // see https://github.com/spring-projects/spring-framework/issues/25801. + Object scopedObject = scope.get(name); + if (scopedObject == null) { + scopedObject = objectFactory.getObject(); + scope.put(name, scopedObject); + } + return scopedObject; + } + + @Override + @Nullable + public Object remove(String name) { + Map scope = this.threadScope.get(); + return scope.remove(name); + } + + @Override + public void registerDestructionCallback(String name, Runnable callback) { + logger.warn("SimpleThreadScope does not support destruction callbacks. " + + "Consider using RequestScope in a web environment."); + } + + @Override + @Nullable + public Object resolveContextualObject(String key) { + return null; + } + + @Override + public String getConversationId() { + return Thread.currentThread().getName(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/StaticApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/StaticApplicationContext.java new file mode 100644 index 0000000..fc58e3f --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/StaticApplicationContext.java @@ -0,0 +1,148 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.util.Locale; + +import org.springframework.beans.BeansException; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.context.ApplicationContext; +import org.springframework.lang.Nullable; + +/** + * {@link org.springframework.context.ApplicationContext} implementation + * which supports programmatic registration of beans and messages, + * rather than reading bean definitions from external configuration sources. + * Mainly useful for testing. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #registerSingleton + * @see #registerPrototype + * @see #registerBeanDefinition + * @see #refresh + */ +public class StaticApplicationContext extends GenericApplicationContext { + + private final StaticMessageSource staticMessageSource; + + + /** + * Create a new StaticApplicationContext. + * @see #registerSingleton + * @see #registerPrototype + * @see #registerBeanDefinition + * @see #refresh + */ + public StaticApplicationContext() throws BeansException { + this(null); + } + + /** + * Create a new StaticApplicationContext with the given parent. + * @see #registerSingleton + * @see #registerPrototype + * @see #registerBeanDefinition + * @see #refresh + */ + public StaticApplicationContext(@Nullable ApplicationContext parent) throws BeansException { + super(parent); + + // Initialize and register a StaticMessageSource. + this.staticMessageSource = new StaticMessageSource(); + getBeanFactory().registerSingleton(MESSAGE_SOURCE_BEAN_NAME, this.staticMessageSource); + } + + + /** + * Overridden to turn it into a no-op, to be more lenient towards test cases. + */ + @Override + protected void assertBeanFactoryActive() { + } + + /** + * Return the internal StaticMessageSource used by this context. + * Can be used to register messages on it. + * @see #addMessage + */ + public final StaticMessageSource getStaticMessageSource() { + return this.staticMessageSource; + } + + /** + * Register a singleton bean with the underlying bean factory. + *

    For more advanced needs, register with the underlying BeanFactory directly. + * @see #getDefaultListableBeanFactory + */ + public void registerSingleton(String name, Class clazz) throws BeansException { + GenericBeanDefinition bd = new GenericBeanDefinition(); + bd.setBeanClass(clazz); + getDefaultListableBeanFactory().registerBeanDefinition(name, bd); + } + + /** + * Register a singleton bean with the underlying bean factory. + *

    For more advanced needs, register with the underlying BeanFactory directly. + * @see #getDefaultListableBeanFactory + */ + public void registerSingleton(String name, Class clazz, MutablePropertyValues pvs) throws BeansException { + GenericBeanDefinition bd = new GenericBeanDefinition(); + bd.setBeanClass(clazz); + bd.setPropertyValues(pvs); + getDefaultListableBeanFactory().registerBeanDefinition(name, bd); + } + + /** + * Register a prototype bean with the underlying bean factory. + *

    For more advanced needs, register with the underlying BeanFactory directly. + * @see #getDefaultListableBeanFactory + */ + public void registerPrototype(String name, Class clazz) throws BeansException { + GenericBeanDefinition bd = new GenericBeanDefinition(); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bd.setBeanClass(clazz); + getDefaultListableBeanFactory().registerBeanDefinition(name, bd); + } + + /** + * Register a prototype bean with the underlying bean factory. + *

    For more advanced needs, register with the underlying BeanFactory directly. + * @see #getDefaultListableBeanFactory + */ + public void registerPrototype(String name, Class clazz, MutablePropertyValues pvs) throws BeansException { + GenericBeanDefinition bd = new GenericBeanDefinition(); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bd.setBeanClass(clazz); + bd.setPropertyValues(pvs); + getDefaultListableBeanFactory().registerBeanDefinition(name, bd); + } + + /** + * Associate the given message with the given code. + * @param code lookup code + * @param locale the locale message should be found within + * @param defaultMessage message associated with this lookup code + * @see #getStaticMessageSource + */ + public void addMessage(String code, Locale locale, String defaultMessage) { + getStaticMessageSource().addMessage(code, locale, defaultMessage); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/StaticMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/StaticMessageSource.java new file mode 100644 index 0000000..9f8ace3 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/StaticMessageSource.java @@ -0,0 +1,137 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Simple implementation of {@link org.springframework.context.MessageSource} + * which allows messages to be registered programmatically. + * This MessageSource supports basic internationalization. + * + *

    Intended for testing rather than for use in production systems. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +public class StaticMessageSource extends AbstractMessageSource { + + private final Map> messageMap = new HashMap<>(); + + + @Override + @Nullable + protected String resolveCodeWithoutArguments(String code, Locale locale) { + Map localeMap = this.messageMap.get(code); + if (localeMap == null) { + return null; + } + MessageHolder holder = localeMap.get(locale); + if (holder == null) { + return null; + } + return holder.getMessage(); + } + + @Override + @Nullable + protected MessageFormat resolveCode(String code, Locale locale) { + Map localeMap = this.messageMap.get(code); + if (localeMap == null) { + return null; + } + MessageHolder holder = localeMap.get(locale); + if (holder == null) { + return null; + } + return holder.getMessageFormat(); + } + + /** + * Associate the given message with the given code. + * @param code the lookup code + * @param locale the locale that the message should be found within + * @param msg the message associated with this lookup code + */ + public void addMessage(String code, Locale locale, String msg) { + Assert.notNull(code, "Code must not be null"); + Assert.notNull(locale, "Locale must not be null"); + Assert.notNull(msg, "Message must not be null"); + this.messageMap.computeIfAbsent(code, key -> new HashMap<>(4)).put(locale, new MessageHolder(msg, locale)); + if (logger.isDebugEnabled()) { + logger.debug("Added message [" + msg + "] for code [" + code + "] and Locale [" + locale + "]"); + } + } + + /** + * Associate the given message values with the given keys as codes. + * @param messages the messages to register, with messages codes + * as keys and message texts as values + * @param locale the locale that the messages should be found within + */ + public void addMessages(Map messages, Locale locale) { + Assert.notNull(messages, "Messages Map must not be null"); + messages.forEach((code, msg) -> addMessage(code, locale, msg)); + } + + + @Override + public String toString() { + return getClass().getName() + ": " + this.messageMap; + } + + + private class MessageHolder { + + private final String message; + + private final Locale locale; + + @Nullable + private volatile MessageFormat cachedFormat; + + public MessageHolder(String message, Locale locale) { + this.message = message; + this.locale = locale; + } + + public String getMessage() { + return this.message; + } + + public MessageFormat getMessageFormat() { + MessageFormat messageFormat = this.cachedFormat; + if (messageFormat == null) { + messageFormat = createMessageFormat(this.message, this.locale); + this.cachedFormat = messageFormat; + } + return messageFormat; + } + + @Override + public String toString() { + return this.message; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/package-info.java b/spring-context/src/main/java/org/springframework/context/support/package-info.java new file mode 100644 index 0000000..2ec0ef5 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/support/package-info.java @@ -0,0 +1,11 @@ +/** + * Classes supporting the org.springframework.context package, + * such as abstract base classes for ApplicationContext + * implementations and a MessageSource implementation. + */ +@NonNullApi +@NonNullFields +package org.springframework.context.support; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/context/weaving/AspectJWeavingEnabler.java b/spring-context/src/main/java/org/springframework/context/weaving/AspectJWeavingEnabler.java new file mode 100644 index 0000000..db73317 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/weaving/AspectJWeavingEnabler.java @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.weaving; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.security.ProtectionDomain; + +import org.aspectj.weaver.loadtime.ClassPreProcessorAgentAdapter; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.Ordered; +import org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver; +import org.springframework.instrument.classloading.LoadTimeWeaver; +import org.springframework.lang.Nullable; + +/** + * Post-processor that registers AspectJ's + * {@link org.aspectj.weaver.loadtime.ClassPreProcessorAgentAdapter} + * with the Spring application context's default + * {@link org.springframework.instrument.classloading.LoadTimeWeaver}. + * + * @author Juergen Hoeller + * @author Ramnivas Laddad + * @since 2.5 + */ +public class AspectJWeavingEnabler + implements BeanFactoryPostProcessor, BeanClassLoaderAware, LoadTimeWeaverAware, Ordered { + + /** + * The {@code aop.xml} resource location. + */ + public static final String ASPECTJ_AOP_XML_RESOURCE = "META-INF/aop.xml"; + + + @Nullable + private ClassLoader beanClassLoader; + + @Nullable + private LoadTimeWeaver loadTimeWeaver; + + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + @Override + public void setLoadTimeWeaver(LoadTimeWeaver loadTimeWeaver) { + this.loadTimeWeaver = loadTimeWeaver; + } + + @Override + public int getOrder() { + return HIGHEST_PRECEDENCE; + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + enableAspectJWeaving(this.loadTimeWeaver, this.beanClassLoader); + } + + + /** + * Enable AspectJ weaving with the given {@link LoadTimeWeaver}. + * @param weaverToUse the LoadTimeWeaver to apply to (or {@code null} for a default weaver) + * @param beanClassLoader the class loader to create a default weaver for (if necessary) + */ + public static void enableAspectJWeaving( + @Nullable LoadTimeWeaver weaverToUse, @Nullable ClassLoader beanClassLoader) { + + if (weaverToUse == null) { + if (InstrumentationLoadTimeWeaver.isInstrumentationAvailable()) { + weaverToUse = new InstrumentationLoadTimeWeaver(beanClassLoader); + } + else { + throw new IllegalStateException("No LoadTimeWeaver available"); + } + } + weaverToUse.addTransformer( + new AspectJClassBypassingClassFileTransformer(new ClassPreProcessorAgentAdapter())); + } + + + /** + * ClassFileTransformer decorator that suppresses processing of AspectJ + * classes in order to avoid potential LinkageErrors. + * @see org.springframework.context.annotation.LoadTimeWeavingConfiguration + */ + private static class AspectJClassBypassingClassFileTransformer implements ClassFileTransformer { + + private final ClassFileTransformer delegate; + + public AspectJClassBypassingClassFileTransformer(ClassFileTransformer delegate) { + this.delegate = delegate; + } + + @Override + public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, + ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { + + if (className.startsWith("org.aspectj") || className.startsWith("org/aspectj")) { + return classfileBuffer; + } + return this.delegate.transform(loader, className, classBeingRedefined, protectionDomain, classfileBuffer); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/weaving/DefaultContextLoadTimeWeaver.java b/spring-context/src/main/java/org/springframework/context/weaving/DefaultContextLoadTimeWeaver.java new file mode 100644 index 0000000..5820735 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/weaving/DefaultContextLoadTimeWeaver.java @@ -0,0 +1,168 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.weaving; + +import java.lang.instrument.ClassFileTransformer; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.instrument.InstrumentationSavingAgent; +import org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver; +import org.springframework.instrument.classloading.LoadTimeWeaver; +import org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver; +import org.springframework.instrument.classloading.glassfish.GlassFishLoadTimeWeaver; +import org.springframework.instrument.classloading.jboss.JBossLoadTimeWeaver; +import org.springframework.instrument.classloading.tomcat.TomcatLoadTimeWeaver; +import org.springframework.instrument.classloading.weblogic.WebLogicLoadTimeWeaver; +import org.springframework.instrument.classloading.websphere.WebSphereLoadTimeWeaver; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Default {@link LoadTimeWeaver} bean for use in an application context, + * decorating an automatically detected internal {@code LoadTimeWeaver}. + * + *

    Typically registered for the default bean name "{@code loadTimeWeaver}"; + * the most convenient way to achieve this is Spring's + * {@code } XML tag or {@code @EnableLoadTimeWeaving} + * on a {@code @Configuration} class. + * + *

    This class implements a runtime environment check for obtaining the + * appropriate weaver implementation. As of Spring Framework 5.0, it detects + * Oracle WebLogic 10+, GlassFish 4+, Tomcat 8+, WildFly 8+, IBM WebSphere 8.5+, + * {@link InstrumentationSavingAgent Spring's VM agent}, and any {@link ClassLoader} + * supported by Spring's {@link ReflectiveLoadTimeWeaver} (such as Liberty's). + * + * @author Juergen Hoeller + * @author Ramnivas Laddad + * @author Costin Leau + * @since 2.5 + * @see org.springframework.context.ConfigurableApplicationContext#LOAD_TIME_WEAVER_BEAN_NAME + */ +public class DefaultContextLoadTimeWeaver implements LoadTimeWeaver, BeanClassLoaderAware, DisposableBean { + + protected final Log logger = LogFactory.getLog(getClass()); + + @Nullable + private LoadTimeWeaver loadTimeWeaver; + + + public DefaultContextLoadTimeWeaver() { + } + + public DefaultContextLoadTimeWeaver(ClassLoader beanClassLoader) { + setBeanClassLoader(beanClassLoader); + } + + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + LoadTimeWeaver serverSpecificLoadTimeWeaver = createServerSpecificLoadTimeWeaver(classLoader); + if (serverSpecificLoadTimeWeaver != null) { + if (logger.isDebugEnabled()) { + logger.debug("Determined server-specific load-time weaver: " + + serverSpecificLoadTimeWeaver.getClass().getName()); + } + this.loadTimeWeaver = serverSpecificLoadTimeWeaver; + } + else if (InstrumentationLoadTimeWeaver.isInstrumentationAvailable()) { + logger.debug("Found Spring's JVM agent for instrumentation"); + this.loadTimeWeaver = new InstrumentationLoadTimeWeaver(classLoader); + } + else { + try { + this.loadTimeWeaver = new ReflectiveLoadTimeWeaver(classLoader); + if (logger.isDebugEnabled()) { + logger.debug("Using reflective load-time weaver for class loader: " + + this.loadTimeWeaver.getInstrumentableClassLoader().getClass().getName()); + } + } + catch (IllegalStateException ex) { + throw new IllegalStateException(ex.getMessage() + " Specify a custom LoadTimeWeaver or start your " + + "Java virtual machine with Spring's agent: -javaagent:spring-instrument-{version}.jar"); + } + } + } + + /* + * This method never fails, allowing to try other possible ways to use an + * server-agnostic weaver. This non-failure logic is required since + * determining a load-time weaver based on the ClassLoader name alone may + * legitimately fail due to other mismatches. + */ + @Nullable + protected LoadTimeWeaver createServerSpecificLoadTimeWeaver(ClassLoader classLoader) { + String name = classLoader.getClass().getName(); + try { + if (name.startsWith("org.apache.catalina")) { + return new TomcatLoadTimeWeaver(classLoader); + } + else if (name.startsWith("org.glassfish")) { + return new GlassFishLoadTimeWeaver(classLoader); + } + else if (name.startsWith("org.jboss.modules")) { + return new JBossLoadTimeWeaver(classLoader); + } + else if (name.startsWith("com.ibm.ws.classloader")) { + return new WebSphereLoadTimeWeaver(classLoader); + } + else if (name.startsWith("weblogic")) { + return new WebLogicLoadTimeWeaver(classLoader); + } + } + catch (Exception ex) { + if (logger.isInfoEnabled()) { + logger.info("Could not obtain server-specific LoadTimeWeaver: " + ex.getMessage()); + } + } + return null; + } + + @Override + public void destroy() { + if (this.loadTimeWeaver instanceof InstrumentationLoadTimeWeaver) { + if (logger.isDebugEnabled()) { + logger.debug("Removing all registered transformers for class loader: " + + this.loadTimeWeaver.getInstrumentableClassLoader().getClass().getName()); + } + ((InstrumentationLoadTimeWeaver) this.loadTimeWeaver).removeTransformers(); + } + } + + + @Override + public void addTransformer(ClassFileTransformer transformer) { + Assert.state(this.loadTimeWeaver != null, "Not initialized"); + this.loadTimeWeaver.addTransformer(transformer); + } + + @Override + public ClassLoader getInstrumentableClassLoader() { + Assert.state(this.loadTimeWeaver != null, "Not initialized"); + return this.loadTimeWeaver.getInstrumentableClassLoader(); + } + + @Override + public ClassLoader getThrowawayClassLoader() { + Assert.state(this.loadTimeWeaver != null, "Not initialized"); + return this.loadTimeWeaver.getThrowawayClassLoader(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/weaving/LoadTimeWeaverAware.java b/spring-context/src/main/java/org/springframework/context/weaving/LoadTimeWeaverAware.java new file mode 100644 index 0000000..c3d4514 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/weaving/LoadTimeWeaverAware.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.weaving; + +import org.springframework.beans.factory.Aware; +import org.springframework.instrument.classloading.LoadTimeWeaver; + +/** + * Interface to be implemented by any object that wishes to be notified + * of the application context's default {@link LoadTimeWeaver}. + * + * @author Juergen Hoeller + * @author Chris Beams + * @since 2.5 + * @see org.springframework.context.ConfigurableApplicationContext#LOAD_TIME_WEAVER_BEAN_NAME + */ +public interface LoadTimeWeaverAware extends Aware { + + /** + * Set the {@link LoadTimeWeaver} of this object's containing + * {@link org.springframework.context.ApplicationContext ApplicationContext}. + *

    Invoked after the population of normal bean properties but before an + * initialization callback like + * {@link org.springframework.beans.factory.InitializingBean InitializingBean's} + * {@link org.springframework.beans.factory.InitializingBean#afterPropertiesSet() afterPropertiesSet()} + * or a custom init-method. Invoked after + * {@link org.springframework.context.ApplicationContextAware ApplicationContextAware's} + * {@link org.springframework.context.ApplicationContextAware#setApplicationContext setApplicationContext(..)}. + *

    NOTE: This method will only be called if there actually is a + * {@code LoadTimeWeaver} available in the application context. If + * there is none, the method will simply not get invoked, assuming that the + * implementing object is able to activate its weaving dependency accordingly. + * @param loadTimeWeaver the {@code LoadTimeWeaver} instance (never {@code null}) + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet + * @see org.springframework.context.ApplicationContextAware#setApplicationContext + */ + void setLoadTimeWeaver(LoadTimeWeaver loadTimeWeaver); + +} diff --git a/spring-context/src/main/java/org/springframework/context/weaving/LoadTimeWeaverAwareProcessor.java b/spring-context/src/main/java/org/springframework/context/weaving/LoadTimeWeaverAwareProcessor.java new file mode 100644 index 0000000..7094ebd --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/weaving/LoadTimeWeaverAwareProcessor.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.weaving; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.instrument.classloading.LoadTimeWeaver; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * {@link org.springframework.beans.factory.config.BeanPostProcessor} + * implementation that passes the context's default {@link LoadTimeWeaver} + * to beans that implement the {@link LoadTimeWeaverAware} interface. + * + *

    {@link org.springframework.context.ApplicationContext Application contexts} + * will automatically register this with their underlying {@link BeanFactory bean factory}, + * provided that a default {@code LoadTimeWeaver} is actually available. + * + *

    Applications should not use this class directly. + * + * @author Juergen Hoeller + * @since 2.5 + * @see LoadTimeWeaverAware + * @see org.springframework.context.ConfigurableApplicationContext#LOAD_TIME_WEAVER_BEAN_NAME + */ +public class LoadTimeWeaverAwareProcessor implements BeanPostProcessor, BeanFactoryAware { + + @Nullable + private LoadTimeWeaver loadTimeWeaver; + + @Nullable + private BeanFactory beanFactory; + + + /** + * Create a new {@code LoadTimeWeaverAwareProcessor} that will + * auto-retrieve the {@link LoadTimeWeaver} from the containing + * {@link BeanFactory}, expecting a bean named + * {@link ConfigurableApplicationContext#LOAD_TIME_WEAVER_BEAN_NAME "loadTimeWeaver"}. + */ + public LoadTimeWeaverAwareProcessor() { + } + + /** + * Create a new {@code LoadTimeWeaverAwareProcessor} for the given + * {@link LoadTimeWeaver}. + *

    If the given {@code loadTimeWeaver} is {@code null}, then a + * {@code LoadTimeWeaver} will be auto-retrieved from the containing + * {@link BeanFactory}, expecting a bean named + * {@link ConfigurableApplicationContext#LOAD_TIME_WEAVER_BEAN_NAME "loadTimeWeaver"}. + * @param loadTimeWeaver the specific {@code LoadTimeWeaver} that is to be used + */ + public LoadTimeWeaverAwareProcessor(@Nullable LoadTimeWeaver loadTimeWeaver) { + this.loadTimeWeaver = loadTimeWeaver; + } + + /** + * Create a new {@code LoadTimeWeaverAwareProcessor}. + *

    The {@code LoadTimeWeaver} will be auto-retrieved from + * the given {@link BeanFactory}, expecting a bean named + * {@link ConfigurableApplicationContext#LOAD_TIME_WEAVER_BEAN_NAME "loadTimeWeaver"}. + * @param beanFactory the BeanFactory to retrieve the LoadTimeWeaver from + */ + public LoadTimeWeaverAwareProcessor(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof LoadTimeWeaverAware) { + LoadTimeWeaver ltw = this.loadTimeWeaver; + if (ltw == null) { + Assert.state(this.beanFactory != null, + "BeanFactory required if no LoadTimeWeaver explicitly specified"); + ltw = this.beanFactory.getBean( + ConfigurableApplicationContext.LOAD_TIME_WEAVER_BEAN_NAME, LoadTimeWeaver.class); + } + ((LoadTimeWeaverAware) bean).setLoadTimeWeaver(ltw); + } + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String name) { + return bean; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/weaving/package-info.java b/spring-context/src/main/java/org/springframework/context/weaving/package-info.java new file mode 100644 index 0000000..889d99e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/weaving/package-info.java @@ -0,0 +1,10 @@ +/** + * Load-time weaving support for a Spring application context, building on Spring's + * {@link org.springframework.instrument.classloading.LoadTimeWeaver} abstraction. + */ +@NonNullApi +@NonNullFields +package org.springframework.context.weaving; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/ejb/access/AbstractRemoteSlsbInvokerInterceptor.java b/spring-context/src/main/java/org/springframework/ejb/access/AbstractRemoteSlsbInvokerInterceptor.java new file mode 100644 index 0000000..1a294ff --- /dev/null +++ b/spring-context/src/main/java/org/springframework/ejb/access/AbstractRemoteSlsbInvokerInterceptor.java @@ -0,0 +1,213 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ejb.access; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.rmi.RemoteException; + +import javax.ejb.EJBHome; +import javax.ejb.EJBObject; +import javax.naming.NamingException; + +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.lang.Nullable; +import org.springframework.remoting.RemoteConnectFailureException; +import org.springframework.remoting.RemoteLookupFailureException; + +/** + * Base class for interceptors proxying remote Stateless Session Beans. + * Designed for EJB 2.x, but works for EJB 3 Session Beans as well. + * + *

    Such an interceptor must be the last interceptor in the advice chain. + * In this case, there is no target object. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +public abstract class AbstractRemoteSlsbInvokerInterceptor extends AbstractSlsbInvokerInterceptor { + + private boolean refreshHomeOnConnectFailure = false; + + private volatile boolean homeAsComponent; + + + + /** + * Set whether to refresh the EJB home on connect failure. + * Default is "false". + *

    Can be turned on to allow for hot restart of the EJB server. + * If a cached EJB home throws an RMI exception that indicates a + * remote connect failure, a fresh home will be fetched and the + * invocation will be retried. + * @see java.rmi.ConnectException + * @see java.rmi.ConnectIOException + * @see java.rmi.NoSuchObjectException + */ + public void setRefreshHomeOnConnectFailure(boolean refreshHomeOnConnectFailure) { + this.refreshHomeOnConnectFailure = refreshHomeOnConnectFailure; + } + + @Override + protected boolean isHomeRefreshable() { + return this.refreshHomeOnConnectFailure; + } + + + /** + * Check for EJB3-style home object that serves as EJB component directly. + */ + @Override + protected Method getCreateMethod(Object home) throws EjbAccessException { + if (this.homeAsComponent) { + return null; + } + if (!(home instanceof EJBHome)) { + // An EJB3 Session Bean... + this.homeAsComponent = true; + return null; + } + return super.getCreateMethod(home); + } + + + /** + * Fetches an EJB home object and delegates to {@code doInvoke}. + *

    If configured to refresh on connect failure, it will call + * {@link #refreshAndRetry} on corresponding RMI exceptions. + * @see #getHome + * @see #doInvoke + * @see #refreshAndRetry + */ + @Override + @Nullable + public Object invokeInContext(MethodInvocation invocation) throws Throwable { + try { + return doInvoke(invocation); + } + catch (RemoteConnectFailureException ex) { + return handleRemoteConnectFailure(invocation, ex); + } + catch (RemoteException ex) { + if (isConnectFailure(ex)) { + return handleRemoteConnectFailure(invocation, ex); + } + else { + throw ex; + } + } + } + + /** + * Determine whether the given RMI exception indicates a connect failure. + *

    The default implementation delegates to RmiClientInterceptorUtils. + * @param ex the RMI exception to check + * @return whether the exception should be treated as connect failure + * @see org.springframework.remoting.rmi.RmiClientInterceptorUtils#isConnectFailure + */ + @SuppressWarnings("deprecation") + protected boolean isConnectFailure(RemoteException ex) { + return org.springframework.remoting.rmi.RmiClientInterceptorUtils.isConnectFailure(ex); + } + + @Nullable + private Object handleRemoteConnectFailure(MethodInvocation invocation, Exception ex) throws Throwable { + if (this.refreshHomeOnConnectFailure) { + if (logger.isDebugEnabled()) { + logger.debug("Could not connect to remote EJB [" + getJndiName() + "] - retrying", ex); + } + else if (logger.isWarnEnabled()) { + logger.warn("Could not connect to remote EJB [" + getJndiName() + "] - retrying"); + } + return refreshAndRetry(invocation); + } + else { + throw ex; + } + } + + /** + * Refresh the EJB home object and retry the given invocation. + * Called by invoke on connect failure. + * @param invocation the AOP method invocation + * @return the invocation result, if any + * @throws Throwable in case of invocation failure + * @see #invoke + */ + @Nullable + protected Object refreshAndRetry(MethodInvocation invocation) throws Throwable { + try { + refreshHome(); + } + catch (NamingException ex) { + throw new RemoteLookupFailureException("Failed to locate remote EJB [" + getJndiName() + "]", ex); + } + return doInvoke(invocation); + } + + + /** + * Perform the given invocation on the current EJB home. + * Template method to be implemented by subclasses. + * @param invocation the AOP method invocation + * @return the invocation result, if any + * @throws Throwable in case of invocation failure + * @see #getHome + * @see #newSessionBeanInstance + */ + @Nullable + protected abstract Object doInvoke(MethodInvocation invocation) throws Throwable; + + + /** + * Return a new instance of the stateless session bean. + * To be invoked by concrete remote SLSB invoker subclasses. + *

    Can be overridden to change the algorithm. + * @throws NamingException if thrown by JNDI + * @throws InvocationTargetException if thrown by the create method + * @see #create + */ + protected Object newSessionBeanInstance() throws NamingException, InvocationTargetException { + if (logger.isDebugEnabled()) { + logger.debug("Trying to create reference to remote EJB"); + } + Object ejbInstance = create(); + if (logger.isDebugEnabled()) { + logger.debug("Obtained reference to remote EJB: " + ejbInstance); + } + return ejbInstance; + } + + /** + * Remove the given EJB instance. + * To be invoked by concrete remote SLSB invoker subclasses. + * @param ejb the EJB instance to remove + * @see javax.ejb.EJBObject#remove + */ + protected void removeSessionBeanInstance(@Nullable EJBObject ejb) { + if (ejb != null && !this.homeAsComponent) { + try { + ejb.remove(); + } + catch (Throwable ex) { + logger.warn("Could not invoke 'remove' on remote EJB proxy", ex); + } + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/ejb/access/AbstractSlsbInvokerInterceptor.java b/spring-context/src/main/java/org/springframework/ejb/access/AbstractSlsbInvokerInterceptor.java new file mode 100644 index 0000000..0665749 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/ejb/access/AbstractSlsbInvokerInterceptor.java @@ -0,0 +1,238 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ejb.access; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import javax.naming.Context; +import javax.naming.NamingException; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.jndi.JndiObjectLocator; +import org.springframework.lang.Nullable; + +/** + * Base class for AOP interceptors invoking local or remote Stateless Session Beans. + * Designed for EJB 2.x, but works for EJB 3 Session Beans as well. + * + *

    Such an interceptor must be the last interceptor in the advice chain. + * In this case, there is no direct target object: The call is handled in a + * special way, getting executed on an EJB instance retrieved via an EJB home. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +public abstract class AbstractSlsbInvokerInterceptor extends JndiObjectLocator + implements MethodInterceptor { + + private boolean lookupHomeOnStartup = true; + + private boolean cacheHome = true; + + private boolean exposeAccessContext = false; + + /** + * The EJB's home object, potentially cached. + * The type must be Object as it could be either EJBHome or EJBLocalHome. + */ + @Nullable + private Object cachedHome; + + /** + * The no-arg create() method required on EJB homes, potentially cached. + */ + @Nullable + private Method createMethod; + + private final Object homeMonitor = new Object(); + + + /** + * Set whether to look up the EJB home object on startup. + * Default is "true". + *

    Can be turned off to allow for late start of the EJB server. + * In this case, the EJB home object will be fetched on first access. + * @see #setCacheHome + */ + public void setLookupHomeOnStartup(boolean lookupHomeOnStartup) { + this.lookupHomeOnStartup = lookupHomeOnStartup; + } + + /** + * Set whether to cache the EJB home object once it has been located. + * Default is "true". + *

    Can be turned off to allow for hot restart of the EJB server. + * In this case, the EJB home object will be fetched for each invocation. + * @see #setLookupHomeOnStartup + */ + public void setCacheHome(boolean cacheHome) { + this.cacheHome = cacheHome; + } + + /** + * Set whether to expose the JNDI environment context for all access to the target + * EJB, i.e. for all method invocations on the exposed object reference. + *

    Default is "false", i.e. to only expose the JNDI context for object lookup. + * Switch this flag to "true" in order to expose the JNDI environment (including + * the authorization context) for each EJB invocation, as needed by WebLogic + * for EJBs with authorization requirements. + */ + public void setExposeAccessContext(boolean exposeAccessContext) { + this.exposeAccessContext = exposeAccessContext; + } + + + /** + * Fetches EJB home on startup, if necessary. + * @see #setLookupHomeOnStartup + * @see #refreshHome + */ + @Override + public void afterPropertiesSet() throws NamingException { + super.afterPropertiesSet(); + if (this.lookupHomeOnStartup) { + // look up EJB home and create method + refreshHome(); + } + } + + /** + * Refresh the cached home object, if applicable. + * Also caches the create method on the home object. + * @throws NamingException if thrown by the JNDI lookup + * @see #lookup + * @see #getCreateMethod + */ + protected void refreshHome() throws NamingException { + synchronized (this.homeMonitor) { + Object home = lookup(); + if (this.cacheHome) { + this.cachedHome = home; + this.createMethod = getCreateMethod(home); + } + } + } + + /** + * Determine the create method of the given EJB home object. + * @param home the EJB home object + * @return the create method + * @throws EjbAccessException if the method couldn't be retrieved + */ + @Nullable + protected Method getCreateMethod(Object home) throws EjbAccessException { + try { + // Cache the EJB create() method that must be declared on the home interface. + return home.getClass().getMethod("create"); + } + catch (NoSuchMethodException ex) { + throw new EjbAccessException("EJB home [" + home + "] has no no-arg create() method"); + } + } + + /** + * Return the EJB home object to use. Called for each invocation. + *

    Default implementation returns the home created on initialization, + * if any; else, it invokes lookup to get a new proxy for each invocation. + *

    Can be overridden in subclasses, for example to cache a home object + * for a given amount of time before recreating it, or to test the home + * object whether it is still alive. + * @return the EJB home object to use for an invocation + * @throws NamingException if proxy creation failed + * @see #lookup + * @see #getCreateMethod + */ + protected Object getHome() throws NamingException { + if (!this.cacheHome || (this.lookupHomeOnStartup && !isHomeRefreshable())) { + return (this.cachedHome != null ? this.cachedHome : lookup()); + } + else { + synchronized (this.homeMonitor) { + if (this.cachedHome == null) { + this.cachedHome = lookup(); + this.createMethod = getCreateMethod(this.cachedHome); + } + return this.cachedHome; + } + } + } + + /** + * Return whether the cached EJB home object is potentially + * subject to on-demand refreshing. Default is "false". + */ + protected boolean isHomeRefreshable() { + return false; + } + + + /** + * Prepares the thread context if necessary, and delegates to + * {@link #invokeInContext}. + */ + @Override + @Nullable + public Object invoke(MethodInvocation invocation) throws Throwable { + Context ctx = (this.exposeAccessContext ? getJndiTemplate().getContext() : null); + try { + return invokeInContext(invocation); + } + finally { + getJndiTemplate().releaseContext(ctx); + } + } + + /** + * Perform the given invocation on the current EJB home, + * within the thread context being prepared accordingly. + * Template method to be implemented by subclasses. + * @param invocation the AOP method invocation + * @return the invocation result, if any + * @throws Throwable in case of invocation failure + */ + @Nullable + protected abstract Object invokeInContext(MethodInvocation invocation) throws Throwable; + + + /** + * Invokes the {@code create()} method on the cached EJB home object. + * @return a new EJBObject or EJBLocalObject + * @throws NamingException if thrown by JNDI + * @throws InvocationTargetException if thrown by the create method + */ + protected Object create() throws NamingException, InvocationTargetException { + try { + Object home = getHome(); + Method createMethodToUse = this.createMethod; + if (createMethodToUse == null) { + createMethodToUse = getCreateMethod(home); + } + if (createMethodToUse == null) { + return home; + } + // Invoke create() method on EJB home object. + return createMethodToUse.invoke(home, (Object[]) null); + } + catch (IllegalAccessException ex) { + throw new EjbAccessException("Could not access EJB home create() method", ex); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/ejb/access/EjbAccessException.java b/spring-context/src/main/java/org/springframework/ejb/access/EjbAccessException.java new file mode 100644 index 0000000..50f1806 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/ejb/access/EjbAccessException.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ejb.access; + +import org.springframework.core.NestedRuntimeException; + +/** + * Exception that gets thrown when an EJB stub cannot be accessed properly. + * + * @author Juergen Hoeller + * @since 2.0 + */ +@SuppressWarnings("serial") +public class EjbAccessException extends NestedRuntimeException { + + /** + * Constructor for EjbAccessException. + * @param msg the detail message + */ + public EjbAccessException(String msg) { + super(msg); + } + + /** + * Constructor for EjbAccessException. + * @param msg the detail message + * @param cause the root cause + */ + public EjbAccessException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-context/src/main/java/org/springframework/ejb/access/LocalSlsbInvokerInterceptor.java b/spring-context/src/main/java/org/springframework/ejb/access/LocalSlsbInvokerInterceptor.java new file mode 100644 index 0000000..027b7ef --- /dev/null +++ b/spring-context/src/main/java/org/springframework/ejb/access/LocalSlsbInvokerInterceptor.java @@ -0,0 +1,179 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ejb.access; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import javax.ejb.CreateException; +import javax.ejb.EJBLocalHome; +import javax.ejb.EJBLocalObject; +import javax.naming.NamingException; + +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.lang.Nullable; + +/** + * Invoker for a local Stateless Session Bean. + * Designed for EJB 2.x, but works for EJB 3 Session Beans as well. + * + *

    Caches the home object, since a local EJB home can never go stale. + * See {@link org.springframework.jndi.JndiObjectLocator} for info on + * how to specify the JNDI location of the target EJB. + * + *

    In a bean container, this class is normally best used as a singleton. However, + * if that bean container pre-instantiates singletons (as do the XML ApplicationContext + * variants) you may have a problem if the bean container is loaded before the EJB + * container loads the target EJB. That is because by default the JNDI lookup will be + * performed in the init method of this class and cached, but the EJB will not have been + * bound at the target location yet. The best solution is to set the lookupHomeOnStartup + * property to false, in which case the home will be fetched on first access to the EJB. + * (This flag is only true by default for backwards compatibility reasons). + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see AbstractSlsbInvokerInterceptor#setLookupHomeOnStartup + * @see AbstractSlsbInvokerInterceptor#setCacheHome + */ +public class LocalSlsbInvokerInterceptor extends AbstractSlsbInvokerInterceptor { + + private volatile boolean homeAsComponent; + + + /** + * This implementation "creates" a new EJB instance for each invocation. + * Can be overridden for custom invocation strategies. + *

    Alternatively, override {@link #getSessionBeanInstance} and + * {@link #releaseSessionBeanInstance} to change EJB instance creation, + * for example to hold a single shared EJB instance. + */ + @Override + @Nullable + public Object invokeInContext(MethodInvocation invocation) throws Throwable { + Object ejb = null; + try { + ejb = getSessionBeanInstance(); + Method method = invocation.getMethod(); + if (method.getDeclaringClass().isInstance(ejb)) { + // directly implemented + return method.invoke(ejb, invocation.getArguments()); + } + else { + // not directly implemented + Method ejbMethod = ejb.getClass().getMethod(method.getName(), method.getParameterTypes()); + return ejbMethod.invoke(ejb, invocation.getArguments()); + } + } + catch (InvocationTargetException ex) { + Throwable targetEx = ex.getTargetException(); + if (logger.isDebugEnabled()) { + logger.debug("Method of local EJB [" + getJndiName() + "] threw exception", targetEx); + } + if (targetEx instanceof CreateException) { + throw new EjbAccessException("Could not create local EJB [" + getJndiName() + "]", targetEx); + } + else { + throw targetEx; + } + } + catch (NamingException ex) { + throw new EjbAccessException("Failed to locate local EJB [" + getJndiName() + "]", ex); + } + catch (IllegalAccessException ex) { + throw new EjbAccessException("Could not access method [" + invocation.getMethod().getName() + + "] of local EJB [" + getJndiName() + "]", ex); + } + finally { + if (ejb instanceof EJBLocalObject) { + releaseSessionBeanInstance((EJBLocalObject) ejb); + } + } + } + + /** + * Check for EJB3-style home object that serves as EJB component directly. + */ + @Override + protected Method getCreateMethod(Object home) throws EjbAccessException { + if (this.homeAsComponent) { + return null; + } + if (!(home instanceof EJBLocalHome)) { + // An EJB3 Session Bean... + this.homeAsComponent = true; + return null; + } + return super.getCreateMethod(home); + } + + /** + * Return an EJB instance to delegate the call to. + * Default implementation delegates to newSessionBeanInstance. + * @throws NamingException if thrown by JNDI + * @throws InvocationTargetException if thrown by the create method + * @see #newSessionBeanInstance + */ + protected Object getSessionBeanInstance() throws NamingException, InvocationTargetException { + return newSessionBeanInstance(); + } + + /** + * Release the given EJB instance. + * Default implementation delegates to removeSessionBeanInstance. + * @param ejb the EJB instance to release + * @see #removeSessionBeanInstance + */ + protected void releaseSessionBeanInstance(EJBLocalObject ejb) { + removeSessionBeanInstance(ejb); + } + + /** + * Return a new instance of the stateless session bean. + * Can be overridden to change the algorithm. + * @throws NamingException if thrown by JNDI + * @throws InvocationTargetException if thrown by the create method + * @see #create + */ + protected Object newSessionBeanInstance() throws NamingException, InvocationTargetException { + if (logger.isDebugEnabled()) { + logger.debug("Trying to create reference to local EJB"); + } + Object ejbInstance = create(); + if (logger.isDebugEnabled()) { + logger.debug("Obtained reference to local EJB: " + ejbInstance); + } + return ejbInstance; + } + + /** + * Remove the given EJB instance. + * @param ejb the EJB instance to remove + * @see javax.ejb.EJBLocalObject#remove() + */ + protected void removeSessionBeanInstance(@Nullable EJBLocalObject ejb) { + if (ejb != null && !this.homeAsComponent) { + try { + ejb.remove(); + } + catch (Throwable ex) { + logger.warn("Could not invoke 'remove' on local EJB proxy", ex); + } + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/ejb/access/LocalStatelessSessionProxyFactoryBean.java b/spring-context/src/main/java/org/springframework/ejb/access/LocalStatelessSessionProxyFactoryBean.java new file mode 100644 index 0000000..1b780af --- /dev/null +++ b/spring-context/src/main/java/org/springframework/ejb/access/LocalStatelessSessionProxyFactoryBean.java @@ -0,0 +1,116 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ejb.access; + +import javax.naming.NamingException; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * Convenient {@link FactoryBean} for local Stateless Session Bean (SLSB) proxies. + * Designed for EJB 2.x, but works for EJB 3 Session Beans as well. + * + *

    See {@link org.springframework.jndi.JndiObjectLocator} for info on + * how to specify the JNDI location of the target EJB. + * + *

    If you want control over interceptor chaining, use an AOP ProxyFactoryBean + * with LocalSlsbInvokerInterceptor rather than rely on this class. + * + *

    In a bean container, this class is normally best used as a singleton. However, + * if that bean container pre-instantiates singletons (as do the XML ApplicationContext + * variants) you may have a problem if the bean container is loaded before the EJB + * container loads the target EJB. That is because by default the JNDI lookup will be + * performed in the init method of this class and cached, but the EJB will not have been + * bound at the target location yet. The best solution is to set the "lookupHomeOnStartup" + * property to "false", in which case the home will be fetched on first access to the EJB. + * (This flag is only true by default for backwards compatibility reasons). + * + * @author Rod Johnson + * @author Colin Sampaleanu + * @since 09.05.2003 + * @see AbstractSlsbInvokerInterceptor#setLookupHomeOnStartup + * @see AbstractSlsbInvokerInterceptor#setCacheHome + */ +public class LocalStatelessSessionProxyFactoryBean extends LocalSlsbInvokerInterceptor + implements FactoryBean, BeanClassLoaderAware { + + /** The business interface of the EJB we're proxying. */ + @Nullable + private Class businessInterface; + + @Nullable + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + /** EJBLocalObject. */ + @Nullable + private Object proxy; + + + /** + * Set the business interface of the EJB we're proxying. + * This will normally be a super-interface of the EJB local component interface. + * Using a business methods interface is a best practice when implementing EJBs. + * @param businessInterface set the business interface of the EJB + */ + public void setBusinessInterface(@Nullable Class businessInterface) { + this.businessInterface = businessInterface; + } + + /** + * Return the business interface of the EJB we're proxying. + */ + @Nullable + public Class getBusinessInterface() { + return this.businessInterface; + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + @Override + public void afterPropertiesSet() throws NamingException { + super.afterPropertiesSet(); + if (this.businessInterface == null) { + throw new IllegalArgumentException("businessInterface is required"); + } + this.proxy = new ProxyFactory(this.businessInterface, this).getProxy(this.beanClassLoader); + } + + + @Override + @Nullable + public Object getObject() { + return this.proxy; + } + + @Override + public Class getObjectType() { + return this.businessInterface; + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/spring-context/src/main/java/org/springframework/ejb/access/SimpleRemoteSlsbInvokerInterceptor.java b/spring-context/src/main/java/org/springframework/ejb/access/SimpleRemoteSlsbInvokerInterceptor.java new file mode 100644 index 0000000..292ddb6 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/ejb/access/SimpleRemoteSlsbInvokerInterceptor.java @@ -0,0 +1,188 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ejb.access; + +import java.lang.reflect.InvocationTargetException; +import java.rmi.RemoteException; + +import javax.ejb.CreateException; +import javax.ejb.EJBObject; +import javax.naming.NamingException; + +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.lang.Nullable; +import org.springframework.remoting.RemoteLookupFailureException; + +/** + * Basic invoker for a remote Stateless Session Bean. + * Designed for EJB 2.x, but works for EJB 3 Session Beans as well. + * + *

    "Creates" a new EJB instance for each invocation, or caches the session + * bean instance for all invocations (see {@link #setCacheSessionBean}). + * See {@link org.springframework.jndi.JndiObjectLocator} for info on + * how to specify the JNDI location of the target EJB. + * + *

    In a bean container, this class is normally best used as a singleton. However, + * if that bean container pre-instantiates singletons (as do the XML ApplicationContext + * variants) you may have a problem if the bean container is loaded before the EJB + * container loads the target EJB. That is because by default the JNDI lookup will be + * performed in the init method of this class and cached, but the EJB will not have been + * bound at the target location yet. The best solution is to set the "lookupHomeOnStartup" + * property to "false", in which case the home will be fetched on first access to the EJB. + * (This flag is only true by default for backwards compatibility reasons). + * + *

    This invoker is typically used with an RMI business interface, which serves + * as super-interface of the EJB component interface. Alternatively, this invoker + * can also proxy a remote SLSB with a matching non-RMI business interface, i.e. an + * interface that mirrors the EJB business methods but does not declare RemoteExceptions. + * In the latter case, RemoteExceptions thrown by the EJB stub will automatically get + * converted to Spring's unchecked RemoteAccessException. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 09.05.2003 + * @see org.springframework.remoting.RemoteAccessException + * @see AbstractSlsbInvokerInterceptor#setLookupHomeOnStartup + * @see AbstractSlsbInvokerInterceptor#setCacheHome + * @see AbstractRemoteSlsbInvokerInterceptor#setRefreshHomeOnConnectFailure + */ +public class SimpleRemoteSlsbInvokerInterceptor extends AbstractRemoteSlsbInvokerInterceptor + implements DisposableBean { + + private boolean cacheSessionBean = false; + + @Nullable + private Object beanInstance; + + private final Object beanInstanceMonitor = new Object(); + + + /** + * Set whether to cache the actual session bean object. + *

    Off by default for standard EJB compliance. Turn this flag + * on to optimize session bean access for servers that are + * known to allow for caching the actual session bean object. + * @see #setCacheHome + */ + public void setCacheSessionBean(boolean cacheSessionBean) { + this.cacheSessionBean = cacheSessionBean; + } + + + /** + * This implementation "creates" a new EJB instance for each invocation. + * Can be overridden for custom invocation strategies. + *

    Alternatively, override {@link #getSessionBeanInstance} and + * {@link #releaseSessionBeanInstance} to change EJB instance creation, + * for example to hold a single shared EJB component instance. + */ + @Override + @Nullable + @SuppressWarnings("deprecation") + protected Object doInvoke(MethodInvocation invocation) throws Throwable { + Object ejb = null; + try { + ejb = getSessionBeanInstance(); + return org.springframework.remoting.rmi.RmiClientInterceptorUtils.invokeRemoteMethod(invocation, ejb); + } + catch (NamingException ex) { + throw new RemoteLookupFailureException("Failed to locate remote EJB [" + getJndiName() + "]", ex); + } + catch (InvocationTargetException ex) { + Throwable targetEx = ex.getTargetException(); + if (targetEx instanceof RemoteException) { + RemoteException rex = (RemoteException) targetEx; + throw org.springframework.remoting.rmi.RmiClientInterceptorUtils.convertRmiAccessException( + invocation.getMethod(), rex, isConnectFailure(rex), getJndiName()); + } + else if (targetEx instanceof CreateException) { + throw org.springframework.remoting.rmi.RmiClientInterceptorUtils.convertRmiAccessException( + invocation.getMethod(), targetEx, "Could not create remote EJB [" + getJndiName() + "]"); + } + throw targetEx; + } + finally { + if (ejb instanceof EJBObject) { + releaseSessionBeanInstance((EJBObject) ejb); + } + } + } + + /** + * Return an EJB component instance to delegate the call to. + *

    The default implementation delegates to {@link #newSessionBeanInstance}. + * @return the EJB component instance + * @throws NamingException if thrown by JNDI + * @throws InvocationTargetException if thrown by the create method + * @see #newSessionBeanInstance + */ + protected Object getSessionBeanInstance() throws NamingException, InvocationTargetException { + if (this.cacheSessionBean) { + synchronized (this.beanInstanceMonitor) { + if (this.beanInstance == null) { + this.beanInstance = newSessionBeanInstance(); + } + return this.beanInstance; + } + } + else { + return newSessionBeanInstance(); + } + } + + /** + * Release the given EJB instance. + *

    The default implementation delegates to {@link #removeSessionBeanInstance}. + * @param ejb the EJB component instance to release + * @see #removeSessionBeanInstance + */ + protected void releaseSessionBeanInstance(EJBObject ejb) { + if (!this.cacheSessionBean) { + removeSessionBeanInstance(ejb); + } + } + + /** + * Reset the cached session bean instance, if necessary. + */ + @Override + protected void refreshHome() throws NamingException { + super.refreshHome(); + if (this.cacheSessionBean) { + synchronized (this.beanInstanceMonitor) { + this.beanInstance = null; + } + } + } + + /** + * Remove the cached session bean instance, if necessary. + */ + @Override + public void destroy() { + if (this.cacheSessionBean) { + synchronized (this.beanInstanceMonitor) { + if (this.beanInstance instanceof EJBObject) { + removeSessionBeanInstance((EJBObject) this.beanInstance); + } + } + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/ejb/access/SimpleRemoteStatelessSessionProxyFactoryBean.java b/spring-context/src/main/java/org/springframework/ejb/access/SimpleRemoteStatelessSessionProxyFactoryBean.java new file mode 100644 index 0000000..2cf93ad --- /dev/null +++ b/spring-context/src/main/java/org/springframework/ejb/access/SimpleRemoteStatelessSessionProxyFactoryBean.java @@ -0,0 +1,130 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ejb.access; + +import javax.naming.NamingException; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * Convenient {@link FactoryBean} for remote SLSB proxies. + * Designed for EJB 2.x, but works for EJB 3 Session Beans as well. + * + *

    See {@link org.springframework.jndi.JndiObjectLocator} for info on + * how to specify the JNDI location of the target EJB. + * + *

    If you want control over interceptor chaining, use an AOP ProxyFactoryBean + * with SimpleRemoteSlsbInvokerInterceptor rather than rely on this class. + * + *

    In a bean container, this class is normally best used as a singleton. However, + * if that bean container pre-instantiates singletons (as do the XML ApplicationContext + * variants) you may have a problem if the bean container is loaded before the EJB + * container loads the target EJB. That is because by default the JNDI lookup will be + * performed in the init method of this class and cached, but the EJB will not have been + * bound at the target location yet. The best solution is to set the lookupHomeOnStartup + * property to false, in which case the home will be fetched on first access to the EJB. + * (This flag is only true by default for backwards compatibility reasons). + * + *

    This proxy factory is typically used with an RMI business interface, which serves + * as super-interface of the EJB component interface. Alternatively, this factory + * can also proxy a remote SLSB with a matching non-RMI business interface, i.e. an + * interface that mirrors the EJB business methods but does not declare RemoteExceptions. + * In the latter case, RemoteExceptions thrown by the EJB stub will automatically get + * converted to Spring's unchecked RemoteAccessException. + * + * @author Rod Johnson + * @author Colin Sampaleanu + * @author Juergen Hoeller + * @since 09.05.2003 + * @see org.springframework.remoting.RemoteAccessException + * @see AbstractSlsbInvokerInterceptor#setLookupHomeOnStartup + * @see AbstractSlsbInvokerInterceptor#setCacheHome + * @see AbstractRemoteSlsbInvokerInterceptor#setRefreshHomeOnConnectFailure + */ +public class SimpleRemoteStatelessSessionProxyFactoryBean extends SimpleRemoteSlsbInvokerInterceptor + implements FactoryBean, BeanClassLoaderAware { + + /** The business interface of the EJB we're proxying. */ + @Nullable + private Class businessInterface; + + @Nullable + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + /** EJBObject. */ + @Nullable + private Object proxy; + + + /** + * Set the business interface of the EJB we're proxying. + * This will normally be a super-interface of the EJB remote component interface. + * Using a business methods interface is a best practice when implementing EJBs. + *

    You can also specify a matching non-RMI business interface, i.e. an interface + * that mirrors the EJB business methods but does not declare RemoteExceptions. + * In this case, RemoteExceptions thrown by the EJB stub will automatically get + * converted to Spring's generic RemoteAccessException. + * @param businessInterface the business interface of the EJB + */ + public void setBusinessInterface(@Nullable Class businessInterface) { + this.businessInterface = businessInterface; + } + + /** + * Return the business interface of the EJB we're proxying. + */ + @Nullable + public Class getBusinessInterface() { + return this.businessInterface; + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + @Override + public void afterPropertiesSet() throws NamingException { + super.afterPropertiesSet(); + if (this.businessInterface == null) { + throw new IllegalArgumentException("businessInterface is required"); + } + this.proxy = new ProxyFactory(this.businessInterface, this).getProxy(this.beanClassLoader); + } + + + @Override + @Nullable + public Object getObject() { + return this.proxy; + } + + @Override + public Class getObjectType() { + return this.businessInterface; + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/spring-context/src/main/java/org/springframework/ejb/access/package-info.java b/spring-context/src/main/java/org/springframework/ejb/access/package-info.java new file mode 100644 index 0000000..54011fa --- /dev/null +++ b/spring-context/src/main/java/org/springframework/ejb/access/package-info.java @@ -0,0 +1,27 @@ +/** + * This package contains classes that allow easy access to EJBs. + * The basis are AOP interceptors run before and after the EJB invocation. + * In particular, the classes in this package allow transparent access + * to stateless session beans (SLSBs) with local interfaces, avoiding + * the need for application code using them to use EJB-specific APIs + * and JNDI lookups, and work with business interfaces that could be + * implemented without using EJB. This provides a valuable decoupling + * of client (such as web components) and business objects (which may + * or may not be EJBs). This gives us the choice of introducing EJB + * into an application (or removing EJB from an application) without + * affecting code using business objects. + * + *

    The motivation for the classes in this package is discussed in Chapter 11 of + * Expert One-On-One J2EE Design and Development + * by Rod Johnson (Wrox, 2002). + * + *

    However, the implementation and naming of classes in this package has changed. + * It now uses FactoryBeans and AOP, rather than the custom bean definitions described in + * Expert One-on-One J2EE. + */ +@NonNullApi +@NonNullFields +package org.springframework.ejb.access; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/ejb/config/AbstractJndiLocatingBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/ejb/config/AbstractJndiLocatingBeanDefinitionParser.java new file mode 100644 index 0000000..34c4ac5 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/ejb/config/AbstractJndiLocatingBeanDefinitionParser.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ejb.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.AbstractSimpleBeanDefinitionParser; +import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; + +import static org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.DEFAULT_VALUE; +import static org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.LAZY_INIT_ATTRIBUTE; +import static org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.TRUE_VALUE; + +/** + * Abstract base class for BeanDefinitionParsers which build + * JNDI-locating beans, supporting an optional "jndiEnvironment" + * bean property, populated from an "environment" XML sub-element. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Oliver Gierke + * @since 2.0 + */ +abstract class AbstractJndiLocatingBeanDefinitionParser extends AbstractSimpleBeanDefinitionParser { + + public static final String ENVIRONMENT = "environment"; + + public static final String ENVIRONMENT_REF = "environment-ref"; + + public static final String JNDI_ENVIRONMENT = "jndiEnvironment"; + + + @Override + protected boolean isEligibleAttribute(String attributeName) { + return (super.isEligibleAttribute(attributeName) && + !ENVIRONMENT_REF.equals(attributeName) && + !LAZY_INIT_ATTRIBUTE.equals(attributeName)); + } + + @Override + protected void postProcess(BeanDefinitionBuilder definitionBuilder, Element element) { + Object envValue = DomUtils.getChildElementValueByTagName(element, ENVIRONMENT); + if (envValue != null) { + // Specific environment settings defined, overriding any shared properties. + definitionBuilder.addPropertyValue(JNDI_ENVIRONMENT, envValue); + } + else { + // Check whether there is a reference to shared environment properties... + String envRef = element.getAttribute(ENVIRONMENT_REF); + if (StringUtils.hasLength(envRef)) { + definitionBuilder.addPropertyValue(JNDI_ENVIRONMENT, new RuntimeBeanReference(envRef)); + } + } + + String lazyInit = element.getAttribute(LAZY_INIT_ATTRIBUTE); + if (StringUtils.hasText(lazyInit) && !DEFAULT_VALUE.equals(lazyInit)) { + definitionBuilder.setLazyInit(TRUE_VALUE.equals(lazyInit)); + } + } +} diff --git a/spring-context/src/main/java/org/springframework/ejb/config/JeeNamespaceHandler.java b/spring-context/src/main/java/org/springframework/ejb/config/JeeNamespaceHandler.java new file mode 100644 index 0000000..bc8671e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/ejb/config/JeeNamespaceHandler.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ejb.config; + +import org.springframework.beans.factory.xml.NamespaceHandlerSupport; + +/** + * {@link org.springframework.beans.factory.xml.NamespaceHandler} + * for the '{@code jee}' namespace. + * + * @author Rob Harrop + * @since 2.0 + */ +public class JeeNamespaceHandler extends NamespaceHandlerSupport { + + @Override + public void init() { + registerBeanDefinitionParser("jndi-lookup", new JndiLookupBeanDefinitionParser()); + registerBeanDefinitionParser("local-slsb", new LocalStatelessSessionBeanDefinitionParser()); + registerBeanDefinitionParser("remote-slsb", new RemoteStatelessSessionBeanDefinitionParser()); + } + +} diff --git a/spring-context/src/main/java/org/springframework/ejb/config/JndiLookupBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/ejb/config/JndiLookupBeanDefinitionParser.java new file mode 100644 index 0000000..6026046 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/ejb/config/JndiLookupBeanDefinitionParser.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ejb.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.jndi.JndiObjectFactoryBean; +import org.springframework.util.StringUtils; + +/** + * Simple {@link org.springframework.beans.factory.xml.BeanDefinitionParser} implementation that + * translates {@code jndi-lookup} tag into {@link JndiObjectFactoryBean} definitions. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + * @see JndiObjectFactoryBean + */ +class JndiLookupBeanDefinitionParser extends AbstractJndiLocatingBeanDefinitionParser { + + public static final String DEFAULT_VALUE = "default-value"; + + public static final String DEFAULT_REF = "default-ref"; + + public static final String DEFAULT_OBJECT = "defaultObject"; + + + @Override + protected Class getBeanClass(Element element) { + return JndiObjectFactoryBean.class; + } + + @Override + protected boolean isEligibleAttribute(String attributeName) { + return (super.isEligibleAttribute(attributeName) && + !DEFAULT_VALUE.equals(attributeName) && !DEFAULT_REF.equals(attributeName)); + } + + @Override + protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + super.doParse(element, parserContext, builder); + + String defaultValue = element.getAttribute(DEFAULT_VALUE); + String defaultRef = element.getAttribute(DEFAULT_REF); + if (StringUtils.hasLength(defaultValue)) { + if (StringUtils.hasLength(defaultRef)) { + parserContext.getReaderContext().error(" element is only allowed to contain either " + + "'default-value' attribute OR 'default-ref' attribute, not both", element); + } + builder.addPropertyValue(DEFAULT_OBJECT, defaultValue); + } + else if (StringUtils.hasLength(defaultRef)) { + builder.addPropertyValue(DEFAULT_OBJECT, new RuntimeBeanReference(defaultRef)); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/ejb/config/LocalStatelessSessionBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/ejb/config/LocalStatelessSessionBeanDefinitionParser.java new file mode 100644 index 0000000..5027c4a --- /dev/null +++ b/spring-context/src/main/java/org/springframework/ejb/config/LocalStatelessSessionBeanDefinitionParser.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ejb.config; + +import org.w3c.dom.Element; + +import org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean; + +/** + * {@link org.springframework.beans.factory.xml.BeanDefinitionParser} + * implementation for parsing '{@code local-slsb}' tags and + * creating {@link LocalStatelessSessionProxyFactoryBean} definitions. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +class LocalStatelessSessionBeanDefinitionParser extends AbstractJndiLocatingBeanDefinitionParser { + + @Override + protected String getBeanClassName(Element element) { + return "org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean"; + } + +} diff --git a/spring-context/src/main/java/org/springframework/ejb/config/RemoteStatelessSessionBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/ejb/config/RemoteStatelessSessionBeanDefinitionParser.java new file mode 100644 index 0000000..15883bd --- /dev/null +++ b/spring-context/src/main/java/org/springframework/ejb/config/RemoteStatelessSessionBeanDefinitionParser.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ejb.config; + +import org.w3c.dom.Element; + +import org.springframework.ejb.access.SimpleRemoteStatelessSessionProxyFactoryBean; + +/** + * {@link org.springframework.beans.factory.xml.BeanDefinitionParser} + * implementation for parsing '{@code remote-slsb}' tags and + * creating {@link SimpleRemoteStatelessSessionProxyFactoryBean} definitions. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +class RemoteStatelessSessionBeanDefinitionParser extends AbstractJndiLocatingBeanDefinitionParser { + + @Override + protected String getBeanClassName(Element element) { + return "org.springframework.ejb.access.SimpleRemoteStatelessSessionProxyFactoryBean"; + } + +} diff --git a/spring-context/src/main/java/org/springframework/ejb/config/package-info.java b/spring-context/src/main/java/org/springframework/ejb/config/package-info.java new file mode 100644 index 0000000..a5442d9 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/ejb/config/package-info.java @@ -0,0 +1,10 @@ +/** + * Support package for EJB/Java EE-related configuration, + * with XML schema being the primary configuration format. + */ +@NonNullApi +@NonNullFields +package org.springframework.ejb.config; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/format/AnnotationFormatterFactory.java b/spring-context/src/main/java/org/springframework/format/AnnotationFormatterFactory.java new file mode 100644 index 0000000..b9a4b0b --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/AnnotationFormatterFactory.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format; + +import java.lang.annotation.Annotation; +import java.util.Set; + +/** + * A factory that creates formatters to format values of fields annotated with a particular + * {@link Annotation}. + * + *

    For example, a {@code DateTimeFormatAnnotationFormatterFactory} might create a formatter + * that formats {@code Date} values set on fields annotated with {@code @DateTimeFormat}. + * + * @author Keith Donald + * @since 3.0 + * @param the annotation type that should trigger formatting + */ +public interface AnnotationFormatterFactory { + + /** + * The types of fields that may be annotated with the <A> annotation. + */ + Set> getFieldTypes(); + + /** + * Get the Printer to print the value of a field of {@code fieldType} annotated with + * {@code annotation}. + *

    If the type T the printer accepts is not assignable to {@code fieldType}, a + * coercion from {@code fieldType} to T will be attempted before the Printer is invoked. + * @param annotation the annotation instance + * @param fieldType the type of field that was annotated + * @return the printer + */ + Printer getPrinter(A annotation, Class fieldType); + + /** + * Get the Parser to parse a submitted value for a field of {@code fieldType} + * annotated with {@code annotation}. + *

    If the object the parser returns is not assignable to {@code fieldType}, + * a coercion to {@code fieldType} will be attempted before the field is set. + * @param annotation the annotation instance + * @param fieldType the type of field that was annotated + * @return the parser + */ + Parser getParser(A annotation, Class fieldType); + +} diff --git a/spring-context/src/main/java/org/springframework/format/Formatter.java b/spring-context/src/main/java/org/springframework/format/Formatter.java new file mode 100644 index 0000000..12e5cf4 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/Formatter.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format; + +/** + * Formats objects of type T. + * A Formatter is both a Printer and a Parser for an object type. + * + * @author Keith Donald + * @since 3.0 + * @param the type of object this Formatter formats + */ +public interface Formatter extends Printer, Parser { + +} diff --git a/spring-context/src/main/java/org/springframework/format/FormatterRegistrar.java b/spring-context/src/main/java/org/springframework/format/FormatterRegistrar.java new file mode 100644 index 0000000..84c1fba --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/FormatterRegistrar.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format; + +import org.springframework.core.convert.converter.Converter; + +/** + * Registers {@link Converter Converters} and {@link Formatter Formatters} with + * a FormattingConversionService through the {@link FormatterRegistry} SPI. + * + * @author Keith Donald + * @since 3.1 + */ +public interface FormatterRegistrar { + + /** + * Register Formatters and Converters with a FormattingConversionService + * through a FormatterRegistry SPI. + * @param registry the FormatterRegistry instance to use. + */ + void registerFormatters(FormatterRegistry registry); + +} diff --git a/spring-context/src/main/java/org/springframework/format/FormatterRegistry.java b/spring-context/src/main/java/org/springframework/format/FormatterRegistry.java new file mode 100644 index 0000000..94591b9 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/FormatterRegistry.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format; + +import java.lang.annotation.Annotation; + +import org.springframework.core.convert.converter.ConverterRegistry; + +/** + * A registry of field formatting logic. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + */ +public interface FormatterRegistry extends ConverterRegistry { + + /** + * Adds a Printer to print fields of a specific type. + * The field type is implied by the parameterized Printer instance. + * @param printer the printer to add + * @since 5.2 + * @see #addFormatter(Formatter) + */ + void addPrinter(Printer printer); + + /** + * Adds a Parser to parse fields of a specific type. + * The field type is implied by the parameterized Parser instance. + * @param parser the parser to add + * @since 5.2 + * @see #addFormatter(Formatter) + */ + void addParser(Parser parser); + + /** + * Adds a Formatter to format fields of a specific type. + * The field type is implied by the parameterized Formatter instance. + * @param formatter the formatter to add + * @since 3.1 + * @see #addFormatterForFieldType(Class, Formatter) + */ + void addFormatter(Formatter formatter); + + /** + * Adds a Formatter to format fields of the given type. + *

    On print, if the Formatter's type T is declared and {@code fieldType} is not assignable to T, + * a coercion to T will be attempted before delegating to {@code formatter} to print a field value. + * On parse, if the parsed object returned by {@code formatter} is not assignable to the runtime field type, + * a coercion to the field type will be attempted before returning the parsed field value. + * @param fieldType the field type to format + * @param formatter the formatter to add + */ + void addFormatterForFieldType(Class fieldType, Formatter formatter); + + /** + * Adds a Printer/Parser pair to format fields of a specific type. + * The formatter will delegate to the specified {@code printer} for printing + * and the specified {@code parser} for parsing. + *

    On print, if the Printer's type T is declared and {@code fieldType} is not assignable to T, + * a coercion to T will be attempted before delegating to {@code printer} to print a field value. + * On parse, if the object returned by the Parser is not assignable to the runtime field type, + * a coercion to the field type will be attempted before returning the parsed field value. + * @param fieldType the field type to format + * @param printer the printing part of the formatter + * @param parser the parsing part of the formatter + */ + void addFormatterForFieldType(Class fieldType, Printer printer, Parser parser); + + /** + * Adds a Formatter to format fields annotated with a specific format annotation. + * @param annotationFormatterFactory the annotation formatter factory to add + */ + void addFormatterForFieldAnnotation(AnnotationFormatterFactory annotationFormatterFactory); + +} diff --git a/spring-context/src/main/java/org/springframework/format/Parser.java b/spring-context/src/main/java/org/springframework/format/Parser.java new file mode 100644 index 0000000..83a7194 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/Parser.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format; + +import java.text.ParseException; +import java.util.Locale; + +/** + * Parses text strings to produce instances of T. + * + * @author Keith Donald + * @since 3.0 + * @param the type of object this Parser produces + */ +@FunctionalInterface +public interface Parser { + + /** + * Parse a text String to produce a T. + * @param text the text string + * @param locale the current user locale + * @return an instance of T + * @throws ParseException when a parse exception occurs in a java.text parsing library + * @throws IllegalArgumentException when a parse exception occurs + */ + T parse(String text, Locale locale) throws ParseException; + +} diff --git a/spring-context/src/main/java/org/springframework/format/Printer.java b/spring-context/src/main/java/org/springframework/format/Printer.java new file mode 100644 index 0000000..054b8c3 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/Printer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format; + +import java.util.Locale; + +/** + * Prints objects of type T for display. + * + * @author Keith Donald + * @since 3.0 + * @param the type of object this Printer prints + */ +@FunctionalInterface +public interface Printer { + + /** + * Print the object of type T for display. + * @param object the instance to print + * @param locale the current user locale + * @return the printed text string + */ + String print(T object, Locale locale); + +} diff --git a/spring-context/src/main/java/org/springframework/format/annotation/DateTimeFormat.java b/spring-context/src/main/java/org/springframework/format/annotation/DateTimeFormat.java new file mode 100644 index 0000000..488e78d --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/annotation/DateTimeFormat.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Declares that a field or method parameter should be formatted as a date or time. + * + *

    Supports formatting by style pattern, ISO date time pattern, or custom format pattern string. + * Can be applied to {@code java.util.Date}, {@code java.util.Calendar}, {@code Long} (for + * millisecond timestamps) as well as JSR-310 java.time and Joda-Time value types. + * + *

    For style-based formatting, set the {@link #style} attribute to be the style pattern code. + * The first character of the code is the date style, and the second character is the time style. + * Specify a character of 'S' for short style, 'M' for medium, 'L' for long, and 'F' for full. + * A date or time may be omitted by specifying the style character '-'. + * + *

    For ISO-based formatting, set the {@link #iso} attribute to be the desired {@link ISO} format, + * such as {@link ISO#DATE}. For custom formatting, set the {@link #pattern} attribute to be the + * DateTime pattern, such as {@code yyyy/MM/dd hh:mm:ss a}. + * + *

    Each attribute is mutually exclusive, so only set one attribute per annotation instance + * (the one most convenient one for your formatting needs). + * When the pattern attribute is specified, it takes precedence over both the style and ISO attribute. + * When the {@link #iso} attribute is specified, it takes precedence over the style attribute. + * When no annotation attributes are specified, the default format applied is style-based + * with a style code of 'SS' (short date, short time). + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + * @see java.time.format.DateTimeFormatter + * @see org.joda.time.format.DateTimeFormat + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) +public @interface DateTimeFormat { + + /** + * The style pattern to use to format the field. + *

    Defaults to 'SS' for short date time. Set this attribute when you wish to format + * your field in accordance with a common style other than the default style. + */ + String style() default "SS"; + + /** + * The ISO pattern to use to format the field. + *

    The possible ISO patterns are defined in the {@link ISO} enum. + *

    Defaults to {@link ISO#NONE}, indicating this attribute should be ignored. + * Set this attribute when you wish to format your field in accordance with an ISO format. + */ + ISO iso() default ISO.NONE; + + /** + * The custom pattern to use to format the field. + *

    Defaults to empty String, indicating no custom pattern String has been specified. + * Set this attribute when you wish to format your field in accordance with a custom + * date time pattern not represented by a style or ISO format. + *

    Note: This pattern follows the original {@link java.text.SimpleDateFormat} style, + * as also supported by Joda-Time, with strict parsing semantics towards overflows + * (e.g. rejecting a Feb 29 value for a non-leap-year). As a consequence, 'yy' + * characters indicate a year in the traditional style, not a "year-of-era" as in the + * {@link java.time.format.DateTimeFormatter} specification (i.e. 'yy' turns into 'uu' + * when going through that {@code DateTimeFormatter} with strict resolution mode). + */ + String pattern() default ""; + + + /** + * Common ISO date time format patterns. + */ + enum ISO { + + /** + * The most common ISO Date Format {@code yyyy-MM-dd}, + * e.g. "2000-10-31". + */ + DATE, + + /** + * The most common ISO Time Format {@code HH:mm:ss.SSSXXX}, + * e.g. "01:30:00.000-05:00". + */ + TIME, + + /** + * The most common ISO DateTime Format {@code yyyy-MM-dd'T'HH:mm:ss.SSSXXX}, + * e.g. "2000-10-31T01:30:00.000-05:00". + */ + DATE_TIME, + + /** + * Indicates that no ISO-based format pattern should be applied. + */ + NONE + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/annotation/NumberFormat.java b/spring-context/src/main/java/org/springframework/format/annotation/NumberFormat.java new file mode 100644 index 0000000..c51f70b --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/annotation/NumberFormat.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Declares that a field or method parameter should be formatted as a number. + * + *

    Supports formatting by style or custom pattern string. Can be applied + * to any JDK {@code Number} type such as {@code Double} and {@code Long}. + * + *

    For style-based formatting, set the {@link #style} attribute to be the + * desired {@link Style}. For custom formatting, set the {@link #pattern} + * attribute to be the number pattern, such as {@code #, ###.##}. + * + *

    Each attribute is mutually exclusive, so only set one attribute per + * annotation instance (the one most convenient one for your formatting needs). + * When the {@link #pattern} attribute is specified, it takes precedence over + * the {@link #style} attribute. When no annotation attributes are specified, + * the default format applied is style-based for either number of currency, + * depending on the annotated field or method parameter type. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + * @see java.text.NumberFormat + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) +public @interface NumberFormat { + + /** + * The style pattern to use to format the field. + *

    Defaults to {@link Style#DEFAULT} for general-purpose number formatting + * for most annotated types, except for money types which default to currency + * formatting. Set this attribute when you wish to format your field in + * accordance with a common style other than the default style. + */ + Style style() default Style.DEFAULT; + + /** + * The custom pattern to use to format the field. + *

    Defaults to empty String, indicating no custom pattern String has been specified. + * Set this attribute when you wish to format your field in accordance with a + * custom number pattern not represented by a style. + */ + String pattern() default ""; + + + /** + * Common number format styles. + */ + enum Style { + + /** + * The default format for the annotated type: typically 'number' but possibly + * 'currency' for a money type (e.g. {@code javax.money.MonetaryAmount)}. + * @since 4.2 + */ + DEFAULT, + + /** + * The general-purpose number format for the current locale. + */ + NUMBER, + + /** + * The percent format for the current locale. + */ + PERCENT, + + /** + * The currency format for the current locale. + */ + CURRENCY + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/annotation/package-info.java b/spring-context/src/main/java/org/springframework/format/annotation/package-info.java new file mode 100644 index 0000000..4ba09ec --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/annotation/package-info.java @@ -0,0 +1,9 @@ +/** + * Annotations for declaratively configuring field formatting rules. + */ +@NonNullApi +@NonNullFields +package org.springframework.format.annotation; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java new file mode 100644 index 0000000..bef0684 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java @@ -0,0 +1,219 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.Date; +import java.util.EnumMap; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; + +import org.springframework.format.Formatter; +import org.springframework.format.annotation.DateTimeFormat.ISO; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * A formatter for {@link java.util.Date} types. + * Allows the configuration of an explicit date pattern and locale. + * + * @author Keith Donald + * @author Juergen Hoeller + * @author Phillip Webb + * @since 3.0 + * @see SimpleDateFormat + */ +public class DateFormatter implements Formatter { + + private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); + + private static final Map ISO_PATTERNS; + + static { + Map formats = new EnumMap<>(ISO.class); + formats.put(ISO.DATE, "yyyy-MM-dd"); + formats.put(ISO.TIME, "HH:mm:ss.SSSXXX"); + formats.put(ISO.DATE_TIME, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); + ISO_PATTERNS = Collections.unmodifiableMap(formats); + } + + + @Nullable + private String pattern; + + private int style = DateFormat.DEFAULT; + + @Nullable + private String stylePattern; + + @Nullable + private ISO iso; + + @Nullable + private TimeZone timeZone; + + private boolean lenient = false; + + + /** + * Create a new default DateFormatter. + */ + public DateFormatter() { + } + + /** + * Create a new DateFormatter for the given date pattern. + */ + public DateFormatter(String pattern) { + this.pattern = pattern; + } + + + /** + * Set the pattern to use to format date values. + *

    If not specified, DateFormat's default style will be used. + */ + public void setPattern(String pattern) { + this.pattern = pattern; + } + + /** + * Set the ISO format used for this date. + * @param iso the {@link ISO} format + * @since 3.2 + */ + public void setIso(ISO iso) { + this.iso = iso; + } + + /** + * Set the style to use to format date values. + *

    If not specified, DateFormat's default style will be used. + * @see DateFormat#DEFAULT + * @see DateFormat#SHORT + * @see DateFormat#MEDIUM + * @see DateFormat#LONG + * @see DateFormat#FULL + */ + public void setStyle(int style) { + this.style = style; + } + + /** + * Set the two character to use to format date values. The first character used for + * the date style, the second is for the time style. Supported characters are + *

    + * This method mimics the styles supported by Joda-Time. + * @param stylePattern two characters from the set {"S", "M", "L", "F", "-"} + * @since 3.2 + */ + public void setStylePattern(String stylePattern) { + this.stylePattern = stylePattern; + } + + /** + * Set the TimeZone to normalize the date values into, if any. + */ + public void setTimeZone(TimeZone timeZone) { + this.timeZone = timeZone; + } + + /** + * Specify whether or not parsing is to be lenient. Default is false. + *

    With lenient parsing, the parser may allow inputs that do not precisely match the format. + * With strict parsing, inputs must match the format exactly. + */ + public void setLenient(boolean lenient) { + this.lenient = lenient; + } + + + @Override + public String print(Date date, Locale locale) { + return getDateFormat(locale).format(date); + } + + @Override + public Date parse(String text, Locale locale) throws ParseException { + return getDateFormat(locale).parse(text); + } + + + protected DateFormat getDateFormat(Locale locale) { + DateFormat dateFormat = createDateFormat(locale); + if (this.timeZone != null) { + dateFormat.setTimeZone(this.timeZone); + } + dateFormat.setLenient(this.lenient); + return dateFormat; + } + + private DateFormat createDateFormat(Locale locale) { + if (StringUtils.hasLength(this.pattern)) { + return new SimpleDateFormat(this.pattern, locale); + } + if (this.iso != null && this.iso != ISO.NONE) { + String pattern = ISO_PATTERNS.get(this.iso); + if (pattern == null) { + throw new IllegalStateException("Unsupported ISO format " + this.iso); + } + SimpleDateFormat format = new SimpleDateFormat(pattern); + format.setTimeZone(UTC); + return format; + } + if (StringUtils.hasLength(this.stylePattern)) { + int dateStyle = getStylePatternForChar(0); + int timeStyle = getStylePatternForChar(1); + if (dateStyle != -1 && timeStyle != -1) { + return DateFormat.getDateTimeInstance(dateStyle, timeStyle, locale); + } + if (dateStyle != -1) { + return DateFormat.getDateInstance(dateStyle, locale); + } + if (timeStyle != -1) { + return DateFormat.getTimeInstance(timeStyle, locale); + } + throw new IllegalStateException("Unsupported style pattern '" + this.stylePattern + "'"); + + } + return DateFormat.getDateInstance(this.style, locale); + } + + private int getStylePatternForChar(int index) { + if (this.stylePattern != null && this.stylePattern.length() > index) { + switch (this.stylePattern.charAt(index)) { + case 'S': return DateFormat.SHORT; + case 'M': return DateFormat.MEDIUM; + case 'L': return DateFormat.LONG; + case 'F': return DateFormat.FULL; + case '-': return -1; + } + } + throw new IllegalStateException("Unsupported style pattern '" + this.stylePattern + "'"); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/DateFormatterRegistrar.java b/spring-context/src/main/java/org/springframework/format/datetime/DateFormatterRegistrar.java new file mode 100644 index 0000000..3e7a01b --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/DateFormatterRegistrar.java @@ -0,0 +1,144 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime; + +import java.util.Calendar; +import java.util.Date; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterRegistry; +import org.springframework.format.FormatterRegistrar; +import org.springframework.format.FormatterRegistry; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Configures basic date formatting for use with Spring, primarily for + * {@link org.springframework.format.annotation.DateTimeFormat} declarations. + * Applies to fields of type {@link Date}, {@link Calendar} and {@code long}. + * + *

    Designed for direct instantiation but also exposes the static + * {@link #addDateConverters(ConverterRegistry)} utility method for + * ad-hoc use against any {@code ConverterRegistry} instance. + * + * @author Phillip Webb + * @since 3.2 + * @see org.springframework.format.datetime.standard.DateTimeFormatterRegistrar + * @see org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar + * @see FormatterRegistrar#registerFormatters + */ +public class DateFormatterRegistrar implements FormatterRegistrar { + + @Nullable + private DateFormatter dateFormatter; + + + /** + * Set a global date formatter to register. + *

    If not specified, no general formatter for non-annotated + * {@link Date} and {@link Calendar} fields will be registered. + */ + public void setFormatter(DateFormatter dateFormatter) { + Assert.notNull(dateFormatter, "DateFormatter must not be null"); + this.dateFormatter = dateFormatter; + } + + + @Override + public void registerFormatters(FormatterRegistry registry) { + addDateConverters(registry); + // In order to retain back compatibility we only register Date/Calendar + // types when a user defined formatter is specified (see SPR-10105) + if (this.dateFormatter != null) { + registry.addFormatter(this.dateFormatter); + registry.addFormatterForFieldType(Calendar.class, this.dateFormatter); + } + registry.addFormatterForFieldAnnotation(new DateTimeFormatAnnotationFormatterFactory()); + } + + /** + * Add date converters to the specified registry. + * @param converterRegistry the registry of converters to add to + */ + public static void addDateConverters(ConverterRegistry converterRegistry) { + converterRegistry.addConverter(new DateToLongConverter()); + converterRegistry.addConverter(new DateToCalendarConverter()); + converterRegistry.addConverter(new CalendarToDateConverter()); + converterRegistry.addConverter(new CalendarToLongConverter()); + converterRegistry.addConverter(new LongToDateConverter()); + converterRegistry.addConverter(new LongToCalendarConverter()); + } + + + private static class DateToLongConverter implements Converter { + + @Override + public Long convert(Date source) { + return source.getTime(); + } + } + + + private static class DateToCalendarConverter implements Converter { + + @Override + public Calendar convert(Date source) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(source); + return calendar; + } + } + + + private static class CalendarToDateConverter implements Converter { + + @Override + public Date convert(Calendar source) { + return source.getTime(); + } + } + + + private static class CalendarToLongConverter implements Converter { + + @Override + public Long convert(Calendar source) { + return source.getTimeInMillis(); + } + } + + + private static class LongToDateConverter implements Converter { + + @Override + public Date convert(Long source) { + return new Date(source); + } + } + + + private static class LongToCalendarConverter implements Converter { + + @Override + public Calendar convert(Long source) { + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(source); + return calendar; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/DateTimeFormatAnnotationFormatterFactory.java b/spring-context/src/main/java/org/springframework/format/datetime/DateTimeFormatAnnotationFormatterFactory.java new file mode 100644 index 0000000..7b31fd6 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/DateTimeFormatAnnotationFormatterFactory.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime; + +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.context.support.EmbeddedValueResolutionSupport; +import org.springframework.format.AnnotationFormatterFactory; +import org.springframework.format.Formatter; +import org.springframework.format.Parser; +import org.springframework.format.Printer; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.util.StringUtils; + +/** + * Formats fields annotated with the {@link DateTimeFormat} annotation using a {@link DateFormatter}. + * + * @author Phillip Webb + * @since 3.2 + * @see org.springframework.format.datetime.joda.JodaDateTimeFormatAnnotationFormatterFactory + */ +public class DateTimeFormatAnnotationFormatterFactory extends EmbeddedValueResolutionSupport + implements AnnotationFormatterFactory { + + private static final Set> FIELD_TYPES; + + static { + Set> fieldTypes = new HashSet<>(4); + fieldTypes.add(Date.class); + fieldTypes.add(Calendar.class); + fieldTypes.add(Long.class); + FIELD_TYPES = Collections.unmodifiableSet(fieldTypes); + } + + + @Override + public Set> getFieldTypes() { + return FIELD_TYPES; + } + + @Override + public Printer getPrinter(DateTimeFormat annotation, Class fieldType) { + return getFormatter(annotation, fieldType); + } + + @Override + public Parser getParser(DateTimeFormat annotation, Class fieldType) { + return getFormatter(annotation, fieldType); + } + + protected Formatter getFormatter(DateTimeFormat annotation, Class fieldType) { + DateFormatter formatter = new DateFormatter(); + String style = resolveEmbeddedValue(annotation.style()); + if (StringUtils.hasLength(style)) { + formatter.setStylePattern(style); + } + formatter.setIso(annotation.iso()); + String pattern = resolveEmbeddedValue(annotation.pattern()); + if (StringUtils.hasLength(pattern)) { + formatter.setPattern(pattern); + } + return formatter; + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/joda/DateTimeFormatterFactory.java b/spring-context/src/main/java/org/springframework/format/datetime/joda/DateTimeFormatterFactory.java new file mode 100644 index 0000000..e276fd2 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/joda/DateTimeFormatterFactory.java @@ -0,0 +1,169 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.joda; + +import java.util.TimeZone; + +import org.joda.time.DateTimeZone; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.ISODateTimeFormat; + +import org.springframework.format.annotation.DateTimeFormat.ISO; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Factory that creates a Joda-Time {@link DateTimeFormatter}. + * + *

    Formatters will be created using the defined {@link #setPattern pattern}, + * {@link #setIso ISO}, and {@link #setStyle style} methods (considered in that order). + * + * @author Phillip Webb + * @author Sam Brannen + * @since 3.2 + * @see #createDateTimeFormatter() + * @see #createDateTimeFormatter(DateTimeFormatter) + * @see #setPattern + * @see #setStyle + * @see #setIso + * @see DateTimeFormatterFactoryBean + * @deprecated as of 5.3, in favor of standard JSR-310 support + */ +@Deprecated +public class DateTimeFormatterFactory { + + @Nullable + private String pattern; + + @Nullable + private ISO iso; + + @Nullable + private String style; + + @Nullable + private TimeZone timeZone; + + + /** + * Create a new {@code DateTimeFormatterFactory} instance. + */ + public DateTimeFormatterFactory() { + } + + /** + * Create a new {@code DateTimeFormatterFactory} instance. + * @param pattern the pattern to use to format date values + */ + public DateTimeFormatterFactory(String pattern) { + this.pattern = pattern; + } + + + /** + * Set the pattern to use to format date values. + * @param pattern the format pattern + */ + public void setPattern(String pattern) { + this.pattern = pattern; + } + + /** + * Set the ISO format used to format date values. + * @param iso the ISO format + */ + public void setIso(ISO iso) { + this.iso = iso; + } + + /** + * Set the two characters to use to format date values, in Joda-Time style. + *

    The first character is used for the date style; the second is for + * the time style. Supported characters are: + *

      + *
    • 'S' = Small
    • + *
    • 'M' = Medium
    • + *
    • 'L' = Long
    • + *
    • 'F' = Full
    • + *
    • '-' = Omitted
    • + *
    + * @param style two characters from the set {"S", "M", "L", "F", "-"} + */ + public void setStyle(String style) { + this.style = style; + } + + /** + * Set the {@code TimeZone} to normalize the date values into, if any. + * @param timeZone the time zone + */ + public void setTimeZone(TimeZone timeZone) { + this.timeZone = timeZone; + } + + + /** + * Create a new {@code DateTimeFormatter} using this factory. + *

    If no specific pattern or style has been defined, + * {@link DateTimeFormat#mediumDateTime() medium date time format} will be used. + * @return a new date time formatter + * @see #createDateTimeFormatter(DateTimeFormatter) + */ + public DateTimeFormatter createDateTimeFormatter() { + return createDateTimeFormatter(DateTimeFormat.mediumDateTime()); + } + + /** + * Create a new {@code DateTimeFormatter} using this factory. + *

    If no specific pattern or style has been defined, + * the supplied {@code fallbackFormatter} will be used. + * @param fallbackFormatter the fall-back formatter to use + * when no specific factory properties have been set + * @return a new date time formatter + */ + public DateTimeFormatter createDateTimeFormatter(DateTimeFormatter fallbackFormatter) { + DateTimeFormatter dateTimeFormatter = null; + if (StringUtils.hasLength(this.pattern)) { + dateTimeFormatter = DateTimeFormat.forPattern(this.pattern); + } + else if (this.iso != null && this.iso != ISO.NONE) { + switch (this.iso) { + case DATE: + dateTimeFormatter = ISODateTimeFormat.date(); + break; + case TIME: + dateTimeFormatter = ISODateTimeFormat.time(); + break; + case DATE_TIME: + dateTimeFormatter = ISODateTimeFormat.dateTime(); + break; + default: + throw new IllegalStateException("Unsupported ISO format: " + this.iso); + } + } + else if (StringUtils.hasLength(this.style)) { + dateTimeFormatter = DateTimeFormat.forStyle(this.style); + } + + if (dateTimeFormatter != null && this.timeZone != null) { + dateTimeFormatter = dateTimeFormatter.withZone(DateTimeZone.forTimeZone(this.timeZone)); + } + return (dateTimeFormatter != null ? dateTimeFormatter : fallbackFormatter); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/joda/DateTimeFormatterFactoryBean.java b/spring-context/src/main/java/org/springframework/format/datetime/joda/DateTimeFormatterFactoryBean.java new file mode 100644 index 0000000..d7a0aa3 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/joda/DateTimeFormatterFactoryBean.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.joda; + +import org.joda.time.format.DateTimeFormatter; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.lang.Nullable; + +/** + * {@link FactoryBean} that creates a Joda-Time {@link DateTimeFormatter}. + * See the {@link DateTimeFormatterFactory base class} for configuration details. + * + * @author Phillip Webb + * @author Sam Brannen + * @since 3.2 + * @see #setPattern + * @see #setIso + * @see #setStyle + * @see DateTimeFormatterFactory + * @deprecated as of 5.3, in favor of standard JSR-310 support + */ +@Deprecated +public class DateTimeFormatterFactoryBean extends DateTimeFormatterFactory + implements FactoryBean, InitializingBean { + + @Nullable + private DateTimeFormatter dateTimeFormatter; + + + @Override + public void afterPropertiesSet() { + this.dateTimeFormatter = createDateTimeFormatter(); + } + + @Override + @Nullable + public DateTimeFormatter getObject() { + return this.dateTimeFormatter; + } + + @Override + public Class getObjectType() { + return DateTimeFormatter.class; + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/joda/DateTimeParser.java b/spring-context/src/main/java/org/springframework/format/datetime/joda/DateTimeParser.java new file mode 100644 index 0000000..99cc1e5 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/joda/DateTimeParser.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.joda; + +import java.text.ParseException; +import java.util.Locale; + +import org.joda.time.DateTime; +import org.joda.time.format.DateTimeFormatter; + +import org.springframework.format.Parser; + +/** + * Parses Joda {@link DateTime} instances using a {@link DateTimeFormatter}. + * + * @author Keith Donald + * @since 3.0 + * @deprecated as of 5.3, in favor of standard JSR-310 support + */ +@Deprecated +public final class DateTimeParser implements Parser { + + private final DateTimeFormatter formatter; + + + /** + * Create a new DateTimeParser. + * @param formatter the Joda DateTimeFormatter instance + */ + public DateTimeParser(DateTimeFormatter formatter) { + this.formatter = formatter; + } + + + @Override + public DateTime parse(String text, Locale locale) throws ParseException { + return JodaTimeContextHolder.getFormatter(this.formatter, locale).parseDateTime(text); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/joda/DurationFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/joda/DurationFormatter.java new file mode 100644 index 0000000..fe30ac4 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/joda/DurationFormatter.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.joda; + +import java.text.ParseException; +import java.util.Locale; + +import org.joda.time.Duration; + +import org.springframework.format.Formatter; + +/** + * {@link Formatter} implementation for a Joda-Time {@link Duration}, + * following Joda-Time's parsing rules for a Duration. + * + * @author Juergen Hoeller + * @since 4.2.4 + * @see Duration#parse + */ +class DurationFormatter implements Formatter { + + @Override + public Duration parse(String text, Locale locale) throws ParseException { + return Duration.parse(text); + } + + @Override + public String print(Duration object, Locale locale) { + return object.toString(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/joda/JodaDateTimeFormatAnnotationFormatterFactory.java b/spring-context/src/main/java/org/springframework/format/datetime/joda/JodaDateTimeFormatAnnotationFormatterFactory.java new file mode 100644 index 0000000..3d5d541 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/joda/JodaDateTimeFormatAnnotationFormatterFactory.java @@ -0,0 +1,133 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.joda; + +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; + +import org.joda.time.LocalDate; +import org.joda.time.LocalDateTime; +import org.joda.time.LocalTime; +import org.joda.time.ReadableInstant; +import org.joda.time.ReadablePartial; +import org.joda.time.format.DateTimeFormatter; + +import org.springframework.context.support.EmbeddedValueResolutionSupport; +import org.springframework.format.AnnotationFormatterFactory; +import org.springframework.format.Parser; +import org.springframework.format.Printer; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.util.StringUtils; + +/** + * Formats fields annotated with the {@link DateTimeFormat} annotation using Joda-Time. + * + *

    NOTE: Spring's Joda-Time support requires Joda-Time 2.x, as of Spring 4.0. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + * @see DateTimeFormat + * @deprecated as of 5.3, in favor of standard JSR-310 support + */ +@Deprecated +public class JodaDateTimeFormatAnnotationFormatterFactory extends EmbeddedValueResolutionSupport + implements AnnotationFormatterFactory { + + private static final Set> FIELD_TYPES; + + static { + // Create the set of field types that may be annotated with @DateTimeFormat. + // Note: the 3 ReadablePartial concrete types are registered explicitly since + // addFormatterForFieldType rules exist for each of these types + // (if we did not do this, the default byType rules for LocalDate, LocalTime, + // and LocalDateTime would take precedence over the annotation rule, which + // is not what we want) + Set> fieldTypes = new HashSet<>(8); + fieldTypes.add(ReadableInstant.class); + fieldTypes.add(LocalDate.class); + fieldTypes.add(LocalTime.class); + fieldTypes.add(LocalDateTime.class); + fieldTypes.add(Date.class); + fieldTypes.add(Calendar.class); + fieldTypes.add(Long.class); + FIELD_TYPES = Collections.unmodifiableSet(fieldTypes); + } + + + @Override + public final Set> getFieldTypes() { + return FIELD_TYPES; + } + + @Override + public Printer getPrinter(DateTimeFormat annotation, Class fieldType) { + DateTimeFormatter formatter = getFormatter(annotation, fieldType); + if (ReadablePartial.class.isAssignableFrom(fieldType)) { + return new ReadablePartialPrinter(formatter); + } + else if (ReadableInstant.class.isAssignableFrom(fieldType) || Calendar.class.isAssignableFrom(fieldType)) { + // assumes Calendar->ReadableInstant converter is registered + return new ReadableInstantPrinter(formatter); + } + else { + // assumes Date->Long converter is registered + return new MillisecondInstantPrinter(formatter); + } + } + + @Override + public Parser getParser(DateTimeFormat annotation, Class fieldType) { + if (LocalDate.class == fieldType) { + return new LocalDateParser(getFormatter(annotation, fieldType)); + } + else if (LocalTime.class == fieldType) { + return new LocalTimeParser(getFormatter(annotation, fieldType)); + } + else if (LocalDateTime.class == fieldType) { + return new LocalDateTimeParser(getFormatter(annotation, fieldType)); + } + else { + return new DateTimeParser(getFormatter(annotation, fieldType)); + } + } + + /** + * Factory method used to create a {@link DateTimeFormatter}. + * @param annotation the format annotation for the field + * @param fieldType the type of field + * @return a {@link DateTimeFormatter} instance + * @since 3.2 + */ + protected DateTimeFormatter getFormatter(DateTimeFormat annotation, Class fieldType) { + DateTimeFormatterFactory factory = new DateTimeFormatterFactory(); + String style = resolveEmbeddedValue(annotation.style()); + if (StringUtils.hasLength(style)) { + factory.setStyle(style); + } + factory.setIso(annotation.iso()); + String pattern = resolveEmbeddedValue(annotation.pattern()); + if (StringUtils.hasLength(pattern)) { + factory.setPattern(pattern); + } + return factory.createDateTimeFormatter(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/joda/JodaTimeContext.java b/spring-context/src/main/java/org/springframework/format/datetime/joda/JodaTimeContext.java new file mode 100644 index 0000000..c544497 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/joda/JodaTimeContext.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.joda; + +import java.util.TimeZone; + +import org.joda.time.Chronology; +import org.joda.time.DateTimeZone; +import org.joda.time.format.DateTimeFormatter; + +import org.springframework.context.i18n.LocaleContext; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.context.i18n.TimeZoneAwareLocaleContext; +import org.springframework.lang.Nullable; + +/** + * A context that holds user-specific Joda-Time settings such as the user's + * Chronology (calendar system) and time zone. + * + *

    A {@code null} property value indicate the user has not specified a setting. + * + * @author Keith Donald + * @since 3.0 + * @see JodaTimeContextHolder + * @deprecated as of 5.3, in favor of standard JSR-310 support + */ +@Deprecated +public class JodaTimeContext { + + @Nullable + private Chronology chronology; + + @Nullable + private DateTimeZone timeZone; + + + /** + * Set the user's chronology (calendar system). + */ + public void setChronology(@Nullable Chronology chronology) { + this.chronology = chronology; + } + + /** + * Return the user's chronology (calendar system), if any. + */ + @Nullable + public Chronology getChronology() { + return this.chronology; + } + + /** + * Set the user's time zone. + *

    Alternatively, set a {@link TimeZoneAwareLocaleContext} on + * {@link LocaleContextHolder}. This context class will fall back to + * checking the locale context if no setting has been provided here. + * @see org.springframework.context.i18n.LocaleContextHolder#getTimeZone() + * @see org.springframework.context.i18n.LocaleContextHolder#setLocaleContext + */ + public void setTimeZone(@Nullable DateTimeZone timeZone) { + this.timeZone = timeZone; + } + + /** + * Return the user's time zone, if any. + */ + @Nullable + public DateTimeZone getTimeZone() { + return this.timeZone; + } + + + /** + * Get the DateTimeFormatter with the this context's settings + * applied to the base {@code formatter}. + * @param formatter the base formatter that establishes default + * formatting rules, generally context-independent + * @return the contextual DateTimeFormatter + */ + public DateTimeFormatter getFormatter(DateTimeFormatter formatter) { + if (this.chronology != null) { + formatter = formatter.withChronology(this.chronology); + } + if (this.timeZone != null) { + formatter = formatter.withZone(this.timeZone); + } + else { + LocaleContext localeContext = LocaleContextHolder.getLocaleContext(); + if (localeContext instanceof TimeZoneAwareLocaleContext) { + TimeZone timeZone = ((TimeZoneAwareLocaleContext) localeContext).getTimeZone(); + if (timeZone != null) { + formatter = formatter.withZone(DateTimeZone.forTimeZone(timeZone)); + } + } + } + return formatter; + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/joda/JodaTimeContextHolder.java b/spring-context/src/main/java/org/springframework/format/datetime/joda/JodaTimeContextHolder.java new file mode 100644 index 0000000..44cd2e9 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/joda/JodaTimeContextHolder.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.joda; + +import java.util.Locale; + +import org.joda.time.format.DateTimeFormatter; + +import org.springframework.core.NamedThreadLocal; +import org.springframework.lang.Nullable; + +/** + * A holder for a thread-local {@link JodaTimeContext} + * with user-specific Joda-Time settings. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + * @see org.springframework.context.i18n.LocaleContextHolder + * @deprecated as of 5.3, in favor of standard JSR-310 support + */ +@Deprecated +public final class JodaTimeContextHolder { + + private static final ThreadLocal jodaTimeContextHolder = + new NamedThreadLocal<>("JodaTimeContext"); + + + private JodaTimeContextHolder() { + } + + + /** + * Reset the JodaTimeContext for the current thread. + */ + public static void resetJodaTimeContext() { + jodaTimeContextHolder.remove(); + } + + /** + * Associate the given JodaTimeContext with the current thread. + * @param jodaTimeContext the current JodaTimeContext, + * or {@code null} to reset the thread-bound context + */ + public static void setJodaTimeContext(@Nullable JodaTimeContext jodaTimeContext) { + if (jodaTimeContext == null) { + resetJodaTimeContext(); + } + else { + jodaTimeContextHolder.set(jodaTimeContext); + } + } + + /** + * Return the JodaTimeContext associated with the current thread, if any. + * @return the current JodaTimeContext, or {@code null} if none + */ + @Nullable + public static JodaTimeContext getJodaTimeContext() { + return jodaTimeContextHolder.get(); + } + + + /** + * Obtain a DateTimeFormatter with user-specific settings applied to the given base Formatter. + * @param formatter the base formatter that establishes default formatting rules + * (generally user independent) + * @param locale the current user locale (may be {@code null} if not known) + * @return the user-specific DateTimeFormatter + */ + public static DateTimeFormatter getFormatter(DateTimeFormatter formatter, @Nullable Locale locale) { + DateTimeFormatter formatterToUse = (locale != null ? formatter.withLocale(locale) : formatter); + JodaTimeContext context = getJodaTimeContext(); + return (context != null ? context.getFormatter(formatterToUse) : formatterToUse); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/joda/JodaTimeConverters.java b/spring-context/src/main/java/org/springframework/format/datetime/joda/JodaTimeConverters.java new file mode 100644 index 0000000..84ba3d0 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/joda/JodaTimeConverters.java @@ -0,0 +1,219 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.joda; + +import java.util.Calendar; +import java.util.Date; + +import org.joda.time.DateTime; +import org.joda.time.Instant; +import org.joda.time.LocalDate; +import org.joda.time.LocalDateTime; +import org.joda.time.LocalTime; +import org.joda.time.MutableDateTime; +import org.joda.time.ReadableInstant; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterRegistry; +import org.springframework.format.datetime.DateFormatterRegistrar; + +/** + * Installs lower-level type converters required to integrate + * Joda-Time support into Spring's field formatting system. + * + *

    Note: {@link JodaTimeFormatterRegistrar} installs these converters + * and relies on several of them for its formatters. Some additional + * converters are just being registered for custom conversion scenarios. + * + * @author Keith Donald + * @author Phillip Webb + * @author Juergen Hoeller + * @since 3.0 + */ +final class JodaTimeConverters { + + private JodaTimeConverters() { + } + + + /** + * Install the converters into the converter registry. + * @param registry the converter registry + */ + @SuppressWarnings("deprecation") + public static void registerConverters(ConverterRegistry registry) { + DateFormatterRegistrar.addDateConverters(registry); + + registry.addConverter(new DateTimeToLocalDateConverter()); + registry.addConverter(new DateTimeToLocalTimeConverter()); + registry.addConverter(new DateTimeToLocalDateTimeConverter()); + registry.addConverter(new DateTimeToDateMidnightConverter()); + registry.addConverter(new DateTimeToMutableDateTimeConverter()); + registry.addConverter(new DateTimeToInstantConverter()); + registry.addConverter(new DateTimeToDateConverter()); + registry.addConverter(new DateTimeToCalendarConverter()); + registry.addConverter(new DateTimeToLongConverter()); + registry.addConverter(new DateToReadableInstantConverter()); + registry.addConverter(new CalendarToReadableInstantConverter()); + registry.addConverter(new LongToReadableInstantConverter()); + registry.addConverter(new LocalDateTimeToLocalDateConverter()); + registry.addConverter(new LocalDateTimeToLocalTimeConverter()); + } + + + private static class DateTimeToLocalDateConverter implements Converter { + + @Override + public LocalDate convert(DateTime source) { + return source.toLocalDate(); + } + } + + + private static class DateTimeToLocalTimeConverter implements Converter { + + @Override + public LocalTime convert(DateTime source) { + return source.toLocalTime(); + } + } + + + private static class DateTimeToLocalDateTimeConverter implements Converter { + + @Override + public LocalDateTime convert(DateTime source) { + return source.toLocalDateTime(); + } + } + + + @Deprecated + private static class DateTimeToDateMidnightConverter implements Converter { + + @Override + public org.joda.time.DateMidnight convert(DateTime source) { + return source.toDateMidnight(); + } + } + + + private static class DateTimeToMutableDateTimeConverter implements Converter { + + @Override + public MutableDateTime convert(DateTime source) { + return source.toMutableDateTime(); + } + } + + + private static class DateTimeToInstantConverter implements Converter { + + @Override + public Instant convert(DateTime source) { + return source.toInstant(); + } + } + + + private static class DateTimeToDateConverter implements Converter { + + @Override + public Date convert(DateTime source) { + return source.toDate(); + } + } + + + private static class DateTimeToCalendarConverter implements Converter { + + @Override + public Calendar convert(DateTime source) { + return source.toGregorianCalendar(); + } + } + + + private static class DateTimeToLongConverter implements Converter { + + @Override + public Long convert(DateTime source) { + return source.getMillis(); + } + } + + + /** + * Used when printing a {@code java.util.Date} field with a ReadableInstantPrinter. + * @see MillisecondInstantPrinter + * @see JodaDateTimeFormatAnnotationFormatterFactory + */ + private static class DateToReadableInstantConverter implements Converter { + + @Override + public ReadableInstant convert(Date source) { + return new DateTime(source); + } + } + + + /** + * Used when printing a {@code java.util.Calendar} field with a ReadableInstantPrinter. + * @see MillisecondInstantPrinter + * @see JodaDateTimeFormatAnnotationFormatterFactory + */ + private static class CalendarToReadableInstantConverter implements Converter { + + @Override + public ReadableInstant convert(Calendar source) { + return new DateTime(source); + } + } + + + /** + * Used when printing a Long field with a ReadableInstantPrinter. + * @see MillisecondInstantPrinter + * @see JodaDateTimeFormatAnnotationFormatterFactory + */ + private static class LongToReadableInstantConverter implements Converter { + + @Override + public ReadableInstant convert(Long source) { + return new DateTime(source.longValue()); + } + } + + + private static class LocalDateTimeToLocalDateConverter implements Converter { + + @Override + public LocalDate convert(LocalDateTime source) { + return source.toLocalDate(); + } + } + + + private static class LocalDateTimeToLocalTimeConverter implements Converter { + + @Override + public LocalTime convert(LocalDateTime source) { + return source.toLocalTime(); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/joda/JodaTimeFormatterRegistrar.java b/spring-context/src/main/java/org/springframework/format/datetime/joda/JodaTimeFormatterRegistrar.java new file mode 100644 index 0000000..19835bb --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/joda/JodaTimeFormatterRegistrar.java @@ -0,0 +1,236 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.joda; + +import java.util.Calendar; +import java.util.Date; +import java.util.EnumMap; +import java.util.Map; + +import org.joda.time.DateTime; +import org.joda.time.Duration; +import org.joda.time.LocalDate; +import org.joda.time.LocalDateTime; +import org.joda.time.LocalTime; +import org.joda.time.MonthDay; +import org.joda.time.Period; +import org.joda.time.ReadableInstant; +import org.joda.time.YearMonth; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; + +import org.springframework.format.FormatterRegistrar; +import org.springframework.format.FormatterRegistry; +import org.springframework.format.Parser; +import org.springframework.format.Printer; +import org.springframework.format.annotation.DateTimeFormat.ISO; + +/** + * Configures Joda-Time's formatting system for use with Spring. + * + *

    NOTE: Spring's Joda-Time support requires Joda-Time 2.x, as of Spring 4.0. + * + * @author Keith Donald + * @author Juergen Hoeller + * @author Phillip Webb + * @since 3.1 + * @see #setDateStyle + * @see #setTimeStyle + * @see #setDateTimeStyle + * @see #setUseIsoFormat + * @see FormatterRegistrar#registerFormatters + * @see org.springframework.format.datetime.DateFormatterRegistrar + * @see DateTimeFormatterFactoryBean + * @deprecated as of 5.3, in favor of standard JSR-310 support + */ +@Deprecated +public class JodaTimeFormatterRegistrar implements FormatterRegistrar { + + private enum Type {DATE, TIME, DATE_TIME} + + + /** + * User defined formatters. + */ + private final Map formatters = new EnumMap<>(Type.class); + + /** + * Factories used when specific formatters have not been specified. + */ + private final Map factories; + + + public JodaTimeFormatterRegistrar() { + this.factories = new EnumMap<>(Type.class); + for (Type type : Type.values()) { + this.factories.put(type, new DateTimeFormatterFactory()); + } + } + + + /** + * Set whether standard ISO formatting should be applied to all date/time types. + * Default is "false" (no). + *

    If set to "true", the "dateStyle", "timeStyle" and "dateTimeStyle" + * properties are effectively ignored. + */ + public void setUseIsoFormat(boolean useIsoFormat) { + this.factories.get(Type.DATE).setIso(useIsoFormat ? ISO.DATE : ISO.NONE); + this.factories.get(Type.TIME).setIso(useIsoFormat ? ISO.TIME : ISO.NONE); + this.factories.get(Type.DATE_TIME).setIso(useIsoFormat ? ISO.DATE_TIME : ISO.NONE); + } + + /** + * Set the default format style of Joda {@link LocalDate} objects. + * Default is {@link DateTimeFormat#shortDate()}. + */ + public void setDateStyle(String dateStyle) { + this.factories.get(Type.DATE).setStyle(dateStyle + "-"); + } + + /** + * Set the default format style of Joda {@link LocalTime} objects. + * Default is {@link DateTimeFormat#shortTime()}. + */ + public void setTimeStyle(String timeStyle) { + this.factories.get(Type.TIME).setStyle("-" + timeStyle); + } + + /** + * Set the default format style of Joda {@link LocalDateTime} and {@link DateTime} objects, + * as well as JDK {@link Date} and {@link Calendar} objects. + * Default is {@link DateTimeFormat#shortDateTime()}. + */ + public void setDateTimeStyle(String dateTimeStyle) { + this.factories.get(Type.DATE_TIME).setStyle(dateTimeStyle); + } + + /** + * Set the formatter that will be used for objects representing date values. + *

    This formatter will be used for the {@link LocalDate} type. When specified + * the {@link #setDateStyle(String) dateStyle} and + * {@link #setUseIsoFormat(boolean) useIsoFormat} properties will be ignored. + * @param formatter the formatter to use + * @since 3.2 + * @see #setTimeFormatter + * @see #setDateTimeFormatter + */ + public void setDateFormatter(DateTimeFormatter formatter) { + this.formatters.put(Type.DATE, formatter); + } + + /** + * Set the formatter that will be used for objects representing time values. + *

    This formatter will be used for the {@link LocalTime} type. When specified + * the {@link #setTimeStyle(String) timeStyle} and + * {@link #setUseIsoFormat(boolean) useIsoFormat} properties will be ignored. + * @param formatter the formatter to use + * @since 3.2 + * @see #setDateFormatter + * @see #setDateTimeFormatter + */ + public void setTimeFormatter(DateTimeFormatter formatter) { + this.formatters.put(Type.TIME, formatter); + } + + /** + * Set the formatter that will be used for objects representing date and time values. + *

    This formatter will be used for {@link LocalDateTime}, {@link ReadableInstant}, + * {@link Date} and {@link Calendar} types. When specified + * the {@link #setDateTimeStyle(String) dateTimeStyle} and + * {@link #setUseIsoFormat(boolean) useIsoFormat} properties will be ignored. + * @param formatter the formatter to use + * @since 3.2 + * @see #setDateFormatter + * @see #setTimeFormatter + */ + public void setDateTimeFormatter(DateTimeFormatter formatter) { + this.formatters.put(Type.DATE_TIME, formatter); + } + + + @Override + public void registerFormatters(FormatterRegistry registry) { + JodaTimeConverters.registerConverters(registry); + + DateTimeFormatter dateFormatter = getFormatter(Type.DATE); + DateTimeFormatter timeFormatter = getFormatter(Type.TIME); + DateTimeFormatter dateTimeFormatter = getFormatter(Type.DATE_TIME); + + addFormatterForFields(registry, + new ReadablePartialPrinter(dateFormatter), + new LocalDateParser(dateFormatter), + LocalDate.class); + + addFormatterForFields(registry, + new ReadablePartialPrinter(timeFormatter), + new LocalTimeParser(timeFormatter), + LocalTime.class); + + addFormatterForFields(registry, + new ReadablePartialPrinter(dateTimeFormatter), + new LocalDateTimeParser(dateTimeFormatter), + LocalDateTime.class); + + addFormatterForFields(registry, + new ReadableInstantPrinter(dateTimeFormatter), + new DateTimeParser(dateTimeFormatter), + ReadableInstant.class); + + // In order to retain backwards compatibility we only register Date/Calendar + // types when a user defined formatter is specified (see SPR-10105) + if (this.formatters.containsKey(Type.DATE_TIME)) { + addFormatterForFields(registry, + new ReadableInstantPrinter(dateTimeFormatter), + new DateTimeParser(dateTimeFormatter), + Date.class, Calendar.class); + } + + registry.addFormatterForFieldType(Period.class, new PeriodFormatter()); + registry.addFormatterForFieldType(Duration.class, new DurationFormatter()); + registry.addFormatterForFieldType(YearMonth.class, new YearMonthFormatter()); + registry.addFormatterForFieldType(MonthDay.class, new MonthDayFormatter()); + + registry.addFormatterForFieldAnnotation(new JodaDateTimeFormatAnnotationFormatterFactory()); + } + + private DateTimeFormatter getFormatter(Type type) { + DateTimeFormatter formatter = this.formatters.get(type); + if (formatter != null) { + return formatter; + } + DateTimeFormatter fallbackFormatter = getFallbackFormatter(type); + return this.factories.get(type).createDateTimeFormatter(fallbackFormatter); + } + + private DateTimeFormatter getFallbackFormatter(Type type) { + switch (type) { + case DATE: return DateTimeFormat.shortDate(); + case TIME: return DateTimeFormat.shortTime(); + default: return DateTimeFormat.shortDateTime(); + } + } + + private void addFormatterForFields(FormatterRegistry registry, Printer printer, + Parser parser, Class... fieldTypes) { + + for (Class fieldType : fieldTypes) { + registry.addFormatterForFieldType(fieldType, printer, parser); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/joda/LocalDateParser.java b/spring-context/src/main/java/org/springframework/format/datetime/joda/LocalDateParser.java new file mode 100644 index 0000000..189f6ea --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/joda/LocalDateParser.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.joda; + +import java.text.ParseException; +import java.util.Locale; + +import org.joda.time.LocalDate; +import org.joda.time.format.DateTimeFormatter; + +import org.springframework.format.Parser; + +/** + * Parses Joda {@link org.joda.time.LocalDate} instances using a + * {@link org.joda.time.format.DateTimeFormatter}. + * + * @author Juergen Hoeller + * @since 4.0 + * @deprecated as of 5.3, in favor of standard JSR-310 support + */ +@Deprecated +public final class LocalDateParser implements Parser { + + private final DateTimeFormatter formatter; + + + /** + * Create a new DateTimeParser. + * @param formatter the Joda DateTimeFormatter instance + */ + public LocalDateParser(DateTimeFormatter formatter) { + this.formatter = formatter; + } + + + @Override + public LocalDate parse(String text, Locale locale) throws ParseException { + return JodaTimeContextHolder.getFormatter(this.formatter, locale).parseLocalDate(text); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/joda/LocalDateTimeParser.java b/spring-context/src/main/java/org/springframework/format/datetime/joda/LocalDateTimeParser.java new file mode 100644 index 0000000..9a2d6ec --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/joda/LocalDateTimeParser.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.joda; + +import java.text.ParseException; +import java.util.Locale; + +import org.joda.time.LocalDateTime; +import org.joda.time.format.DateTimeFormatter; + +import org.springframework.format.Parser; + +/** + * Parses Joda {@link org.joda.time.LocalDateTime} instances using a + * {@link org.joda.time.format.DateTimeFormatter}. + * + * @author Juergen Hoeller + * @since 4.0 + * @deprecated as of 5.3, in favor of standard JSR-310 support + */ +@Deprecated +public final class LocalDateTimeParser implements Parser { + + private final DateTimeFormatter formatter; + + + /** + * Create a new DateTimeParser. + * @param formatter the Joda DateTimeFormatter instance + */ + public LocalDateTimeParser(DateTimeFormatter formatter) { + this.formatter = formatter; + } + + + @Override + public LocalDateTime parse(String text, Locale locale) throws ParseException { + return JodaTimeContextHolder.getFormatter(this.formatter, locale).parseLocalDateTime(text); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/joda/LocalTimeParser.java b/spring-context/src/main/java/org/springframework/format/datetime/joda/LocalTimeParser.java new file mode 100644 index 0000000..0d68e82 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/joda/LocalTimeParser.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.joda; + +import java.text.ParseException; +import java.util.Locale; + +import org.joda.time.LocalTime; +import org.joda.time.format.DateTimeFormatter; + +import org.springframework.format.Parser; + +/** + * Parses Joda {@link org.joda.time.LocalTime} instances using a + * {@link org.joda.time.format.DateTimeFormatter}. + * + * @author Juergen Hoeller + * @since 4.0 + * @deprecated as of 5.3, in favor of standard JSR-310 support + */ +@Deprecated +public final class LocalTimeParser implements Parser { + + private final DateTimeFormatter formatter; + + + /** + * Create a new DateTimeParser. + * @param formatter the Joda DateTimeFormatter instance + */ + public LocalTimeParser(DateTimeFormatter formatter) { + this.formatter = formatter; + } + + + @Override + public LocalTime parse(String text, Locale locale) throws ParseException { + return JodaTimeContextHolder.getFormatter(this.formatter, locale).parseLocalTime(text); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/joda/MillisecondInstantPrinter.java b/spring-context/src/main/java/org/springframework/format/datetime/joda/MillisecondInstantPrinter.java new file mode 100644 index 0000000..75cd6ba --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/joda/MillisecondInstantPrinter.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.joda; + +import java.util.Locale; + +import org.joda.time.format.DateTimeFormatter; + +import org.springframework.format.Printer; + +/** + * Prints Long instances using a Joda {@link DateTimeFormatter}. + * + * @author Keith Donald + * @since 3.0 + * @deprecated as of 5.3, in favor of standard JSR-310 support + */ +@Deprecated +public final class MillisecondInstantPrinter implements Printer { + + private final DateTimeFormatter formatter; + + + /** + * Create a new ReadableInstantPrinter. + * @param formatter the Joda DateTimeFormatter instance + */ + public MillisecondInstantPrinter(DateTimeFormatter formatter) { + this.formatter = formatter; + } + + + @Override + public String print(Long instant, Locale locale) { + return JodaTimeContextHolder.getFormatter(this.formatter, locale).print(instant); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/joda/MonthDayFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/joda/MonthDayFormatter.java new file mode 100644 index 0000000..1cb12ce --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/joda/MonthDayFormatter.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.joda; + +import java.text.ParseException; +import java.util.Locale; + +import org.joda.time.MonthDay; + +import org.springframework.format.Formatter; + +/** + * {@link Formatter} implementation for a Joda-Time {@link MonthDay}, + * following Joda-Time's parsing rules for a MonthDay. + * + * @author Juergen Hoeller + * @since 4.2.4 + * @see MonthDay#parse + */ +class MonthDayFormatter implements Formatter { + + @Override + public MonthDay parse(String text, Locale locale) throws ParseException { + return MonthDay.parse(text); + } + + @Override + public String print(MonthDay object, Locale locale) { + return object.toString(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/joda/PeriodFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/joda/PeriodFormatter.java new file mode 100644 index 0000000..28133f7 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/joda/PeriodFormatter.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.joda; + +import java.text.ParseException; +import java.util.Locale; + +import org.joda.time.Period; + +import org.springframework.format.Formatter; + +/** + * {@link Formatter} implementation for a Joda-Time {@link Period}, + * following Joda-Time's parsing rules for a Period. + * + * @author Juergen Hoeller + * @since 4.2.4 + * @see Period#parse + */ +class PeriodFormatter implements Formatter { + + @Override + public Period parse(String text, Locale locale) throws ParseException { + return Period.parse(text); + } + + @Override + public String print(Period object, Locale locale) { + return object.toString(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/joda/ReadableInstantPrinter.java b/spring-context/src/main/java/org/springframework/format/datetime/joda/ReadableInstantPrinter.java new file mode 100644 index 0000000..995075b --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/joda/ReadableInstantPrinter.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.joda; + +import java.util.Locale; + +import org.joda.time.ReadableInstant; +import org.joda.time.format.DateTimeFormatter; + +import org.springframework.format.Printer; + +/** + * Prints Joda-Time {@link ReadableInstant} instances using a {@link DateTimeFormatter}. + * + * @author Keith Donald + * @since 3.0 + * @deprecated as of 5.3, in favor of standard JSR-310 support + */ +@Deprecated +public final class ReadableInstantPrinter implements Printer { + + private final DateTimeFormatter formatter; + + + /** + * Create a new ReadableInstantPrinter. + * @param formatter the Joda DateTimeFormatter instance + */ + public ReadableInstantPrinter(DateTimeFormatter formatter) { + this.formatter = formatter; + } + + + @Override + public String print(ReadableInstant instant, Locale locale) { + return JodaTimeContextHolder.getFormatter(this.formatter, locale).print(instant); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/joda/ReadablePartialPrinter.java b/spring-context/src/main/java/org/springframework/format/datetime/joda/ReadablePartialPrinter.java new file mode 100644 index 0000000..85d4e3d --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/joda/ReadablePartialPrinter.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.joda; + +import java.util.Locale; + +import org.joda.time.ReadablePartial; +import org.joda.time.format.DateTimeFormatter; + +import org.springframework.format.Printer; + +/** + * Prints Joda-Time {@link ReadablePartial} instances using a {@link DateTimeFormatter}. + * + * @author Keith Donald + * @since 3.0 + * @deprecated as of 5.3, in favor of standard JSR-310 support + */ +@Deprecated +public final class ReadablePartialPrinter implements Printer { + + private final DateTimeFormatter formatter; + + + /** + * Create a new ReadableInstantPrinter. + * @param formatter the Joda DateTimeFormatter instance + */ + public ReadablePartialPrinter(DateTimeFormatter formatter) { + this.formatter = formatter; + } + + + @Override + public String print(ReadablePartial partial, Locale locale) { + return JodaTimeContextHolder.getFormatter(this.formatter, locale).print(partial); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/joda/YearMonthFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/joda/YearMonthFormatter.java new file mode 100644 index 0000000..1ac44b4 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/joda/YearMonthFormatter.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.joda; + +import java.text.ParseException; +import java.util.Locale; + +import org.joda.time.YearMonth; + +import org.springframework.format.Formatter; + +/** + * {@link Formatter} implementation for a Joda-Time {@link YearMonth}, + * following Joda-Time's parsing rules for a YearMonth. + * + * @author Juergen Hoeller + * @since 4.2.4 + * @see YearMonth#parse + */ +class YearMonthFormatter implements Formatter { + + @Override + public YearMonth parse(String text, Locale locale) throws ParseException { + return YearMonth.parse(text); + } + + @Override + public String print(YearMonth object, Locale locale) { + return object.toString(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/joda/package-info.java b/spring-context/src/main/java/org/springframework/format/datetime/joda/package-info.java new file mode 100644 index 0000000..a8a190b --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/joda/package-info.java @@ -0,0 +1,9 @@ +/** + * Integration with Joda-Time for formatting Joda date and time types as well as standard JDK Date types. + */ +@NonNullApi +@NonNullFields +package org.springframework.format.datetime.joda; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/format/datetime/package-info.java b/spring-context/src/main/java/org/springframework/format/datetime/package-info.java new file mode 100644 index 0000000..b6f4686 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/package-info.java @@ -0,0 +1,9 @@ +/** + * Formatters for {@code java.util.Date} properties. + */ +@NonNullApi +@NonNullFields +package org.springframework.format.datetime; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeContext.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeContext.java new file mode 100644 index 0000000..1e79ec6 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeContext.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.standard; + +import java.time.ZoneId; +import java.time.chrono.Chronology; +import java.time.format.DateTimeFormatter; +import java.util.TimeZone; + +import org.springframework.context.i18n.LocaleContext; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.context.i18n.TimeZoneAwareLocaleContext; +import org.springframework.lang.Nullable; + +/** + * A context that holds user-specific java.time (JSR-310) settings + * such as the user's Chronology (calendar system) and time zone. + * A {@code null} property value indicate the user has not specified a setting. + * + * @author Juergen Hoeller + * @since 4.0 + * @see DateTimeContextHolder + */ +public class DateTimeContext { + + @Nullable + private Chronology chronology; + + @Nullable + private ZoneId timeZone; + + + /** + * Set the user's chronology (calendar system). + */ + public void setChronology(@Nullable Chronology chronology) { + this.chronology = chronology; + } + + /** + * Return the user's chronology (calendar system), if any. + */ + @Nullable + public Chronology getChronology() { + return this.chronology; + } + + /** + * Set the user's time zone. + *

    Alternatively, set a {@link TimeZoneAwareLocaleContext} on + * {@link LocaleContextHolder}. This context class will fall back to + * checking the locale context if no setting has been provided here. + * @see org.springframework.context.i18n.LocaleContextHolder#getTimeZone() + * @see org.springframework.context.i18n.LocaleContextHolder#setLocaleContext + */ + public void setTimeZone(@Nullable ZoneId timeZone) { + this.timeZone = timeZone; + } + + /** + * Return the user's time zone, if any. + */ + @Nullable + public ZoneId getTimeZone() { + return this.timeZone; + } + + + /** + * Get the DateTimeFormatter with the this context's settings + * applied to the base {@code formatter}. + * @param formatter the base formatter that establishes default + * formatting rules, generally context-independent + * @return the contextual DateTimeFormatter + */ + public DateTimeFormatter getFormatter(DateTimeFormatter formatter) { + if (this.chronology != null) { + formatter = formatter.withChronology(this.chronology); + } + if (this.timeZone != null) { + formatter = formatter.withZone(this.timeZone); + } + else { + LocaleContext localeContext = LocaleContextHolder.getLocaleContext(); + if (localeContext instanceof TimeZoneAwareLocaleContext) { + TimeZone timeZone = ((TimeZoneAwareLocaleContext) localeContext).getTimeZone(); + if (timeZone != null) { + formatter = formatter.withZone(timeZone.toZoneId()); + } + } + } + return formatter; + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeContextHolder.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeContextHolder.java new file mode 100644 index 0000000..8877419 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeContextHolder.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.standard; + +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +import org.springframework.core.NamedThreadLocal; +import org.springframework.lang.Nullable; + +/** + * A holder for a thread-local user {@link DateTimeContext}. + * + * @author Juergen Hoeller + * @since 4.0 + * @see org.springframework.context.i18n.LocaleContextHolder + */ +public final class DateTimeContextHolder { + + private static final ThreadLocal dateTimeContextHolder = + new NamedThreadLocal<>("DateTimeContext"); + + + private DateTimeContextHolder() { + } + + + /** + * Reset the DateTimeContext for the current thread. + */ + public static void resetDateTimeContext() { + dateTimeContextHolder.remove(); + } + + /** + * Associate the given DateTimeContext with the current thread. + * @param dateTimeContext the current DateTimeContext, + * or {@code null} to reset the thread-bound context + */ + public static void setDateTimeContext(@Nullable DateTimeContext dateTimeContext) { + if (dateTimeContext == null) { + resetDateTimeContext(); + } + else { + dateTimeContextHolder.set(dateTimeContext); + } + } + + /** + * Return the DateTimeContext associated with the current thread, if any. + * @return the current DateTimeContext, or {@code null} if none + */ + @Nullable + public static DateTimeContext getDateTimeContext() { + return dateTimeContextHolder.get(); + } + + + /** + * Obtain a DateTimeFormatter with user-specific settings applied to the given base Formatter. + * @param formatter the base formatter that establishes default formatting rules + * (generally user independent) + * @param locale the current user locale (may be {@code null} if not known) + * @return the user-specific DateTimeFormatter + */ + public static DateTimeFormatter getFormatter(DateTimeFormatter formatter, @Nullable Locale locale) { + DateTimeFormatter formatterToUse = (locale != null ? formatter.withLocale(locale) : formatter); + DateTimeContext context = getDateTimeContext(); + return (context != null ? context.getFormatter(formatterToUse) : formatterToUse); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeConverters.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeConverters.java new file mode 100644 index 0000000..0295c4a --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeConverters.java @@ -0,0 +1,268 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.standard; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; +import java.util.Calendar; +import java.util.GregorianCalendar; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterRegistry; +import org.springframework.format.datetime.DateFormatterRegistrar; + +/** + * Installs lower-level type converters required to integrate + * JSR-310 support into Spring's field formatting system. + * + *

    Note: {@link DateTimeFormatterRegistrar} installs these converters but + * does not rely on them for its formatters. They are just being registered + * for custom conversion scenarios between different JSR-310 value types + * and also between {@link java.util.Calendar} and JSR-310 value types. + * + * @author Juergen Hoeller + * @since 4.0.1 + */ +final class DateTimeConverters { + + private DateTimeConverters() { + } + + + /** + * Install the converters into the converter registry. + * @param registry the converter registry + */ + public static void registerConverters(ConverterRegistry registry) { + DateFormatterRegistrar.addDateConverters(registry); + + registry.addConverter(new LocalDateTimeToLocalDateConverter()); + registry.addConverter(new LocalDateTimeToLocalTimeConverter()); + registry.addConverter(new ZonedDateTimeToLocalDateConverter()); + registry.addConverter(new ZonedDateTimeToLocalTimeConverter()); + registry.addConverter(new ZonedDateTimeToLocalDateTimeConverter()); + registry.addConverter(new ZonedDateTimeToOffsetDateTimeConverter()); + registry.addConverter(new ZonedDateTimeToInstantConverter()); + registry.addConverter(new OffsetDateTimeToLocalDateConverter()); + registry.addConverter(new OffsetDateTimeToLocalTimeConverter()); + registry.addConverter(new OffsetDateTimeToLocalDateTimeConverter()); + registry.addConverter(new OffsetDateTimeToZonedDateTimeConverter()); + registry.addConverter(new OffsetDateTimeToInstantConverter()); + registry.addConverter(new CalendarToZonedDateTimeConverter()); + registry.addConverter(new CalendarToOffsetDateTimeConverter()); + registry.addConverter(new CalendarToLocalDateConverter()); + registry.addConverter(new CalendarToLocalTimeConverter()); + registry.addConverter(new CalendarToLocalDateTimeConverter()); + registry.addConverter(new CalendarToInstantConverter()); + registry.addConverter(new LongToInstantConverter()); + registry.addConverter(new InstantToLongConverter()); + } + + private static ZonedDateTime calendarToZonedDateTime(Calendar source) { + if (source instanceof GregorianCalendar) { + return ((GregorianCalendar) source).toZonedDateTime(); + } + else { + return ZonedDateTime.ofInstant(Instant.ofEpochMilli(source.getTimeInMillis()), + source.getTimeZone().toZoneId()); + } + } + + + private static class LocalDateTimeToLocalDateConverter implements Converter { + + @Override + public LocalDate convert(LocalDateTime source) { + return source.toLocalDate(); + } + } + + + private static class LocalDateTimeToLocalTimeConverter implements Converter { + + @Override + public LocalTime convert(LocalDateTime source) { + return source.toLocalTime(); + } + } + + + private static class ZonedDateTimeToLocalDateConverter implements Converter { + + @Override + public LocalDate convert(ZonedDateTime source) { + return source.toLocalDate(); + } + } + + + private static class ZonedDateTimeToLocalTimeConverter implements Converter { + + @Override + public LocalTime convert(ZonedDateTime source) { + return source.toLocalTime(); + } + } + + + private static class ZonedDateTimeToLocalDateTimeConverter implements Converter { + + @Override + public LocalDateTime convert(ZonedDateTime source) { + return source.toLocalDateTime(); + } + } + + private static class ZonedDateTimeToOffsetDateTimeConverter implements Converter { + + @Override + public OffsetDateTime convert(ZonedDateTime source) { + return source.toOffsetDateTime(); + } + } + + + private static class ZonedDateTimeToInstantConverter implements Converter { + + @Override + public Instant convert(ZonedDateTime source) { + return source.toInstant(); + } + } + + + private static class OffsetDateTimeToLocalDateConverter implements Converter { + + @Override + public LocalDate convert(OffsetDateTime source) { + return source.toLocalDate(); + } + } + + + private static class OffsetDateTimeToLocalTimeConverter implements Converter { + + @Override + public LocalTime convert(OffsetDateTime source) { + return source.toLocalTime(); + } + } + + + private static class OffsetDateTimeToLocalDateTimeConverter implements Converter { + + @Override + public LocalDateTime convert(OffsetDateTime source) { + return source.toLocalDateTime(); + } + } + + + private static class OffsetDateTimeToZonedDateTimeConverter implements Converter { + + @Override + public ZonedDateTime convert(OffsetDateTime source) { + return source.toZonedDateTime(); + } + } + + + private static class OffsetDateTimeToInstantConverter implements Converter { + + @Override + public Instant convert(OffsetDateTime source) { + return source.toInstant(); + } + } + + + private static class CalendarToZonedDateTimeConverter implements Converter { + + @Override + public ZonedDateTime convert(Calendar source) { + return calendarToZonedDateTime(source); + } + } + + + private static class CalendarToOffsetDateTimeConverter implements Converter { + + @Override + public OffsetDateTime convert(Calendar source) { + return calendarToZonedDateTime(source).toOffsetDateTime(); + } + } + + + private static class CalendarToLocalDateConverter implements Converter { + + @Override + public LocalDate convert(Calendar source) { + return calendarToZonedDateTime(source).toLocalDate(); + } + } + + + private static class CalendarToLocalTimeConverter implements Converter { + + @Override + public LocalTime convert(Calendar source) { + return calendarToZonedDateTime(source).toLocalTime(); + } + } + + + private static class CalendarToLocalDateTimeConverter implements Converter { + + @Override + public LocalDateTime convert(Calendar source) { + return calendarToZonedDateTime(source).toLocalDateTime(); + } + } + + + private static class CalendarToInstantConverter implements Converter { + + @Override + public Instant convert(Calendar source) { + return calendarToZonedDateTime(source).toInstant(); + } + } + + + private static class LongToInstantConverter implements Converter { + + @Override + public Instant convert(Long source) { + return Instant.ofEpochMilli(source); + } + } + + + private static class InstantToLongConverter implements Converter { + + @Override + public Long convert(Instant source) { + return source.toEpochMilli(); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterFactory.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterFactory.java new file mode 100644 index 0000000..3d56f20 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterFactory.java @@ -0,0 +1,220 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.standard; + +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.time.format.ResolverStyle; +import java.util.TimeZone; + +import org.springframework.format.annotation.DateTimeFormat.ISO; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Factory that creates a JSR-310 {@link java.time.format.DateTimeFormatter}. + * + *

    Formatters will be created using the defined {@link #setPattern pattern}, + * {@link #setIso ISO}, and xxxStyle methods (considered in that order). + * + * @author Juergen Hoeller + * @author Phillip Webb + * @since 4.0 + * @see #createDateTimeFormatter() + * @see #createDateTimeFormatter(DateTimeFormatter) + * @see #setPattern + * @see #setIso + * @see #setDateStyle + * @see #setTimeStyle + * @see #setDateTimeStyle + * @see DateTimeFormatterFactoryBean + */ +public class DateTimeFormatterFactory { + + @Nullable + private String pattern; + + @Nullable + private ISO iso; + + @Nullable + private FormatStyle dateStyle; + + @Nullable + private FormatStyle timeStyle; + + @Nullable + private TimeZone timeZone; + + + /** + * Create a new {@code DateTimeFormatterFactory} instance. + */ + public DateTimeFormatterFactory() { + } + + /** + * Create a new {@code DateTimeFormatterFactory} instance. + * @param pattern the pattern to use to format date values + */ + public DateTimeFormatterFactory(String pattern) { + this.pattern = pattern; + } + + + /** + * Set the pattern to use to format date values. + * @param pattern the format pattern + */ + public void setPattern(String pattern) { + this.pattern = pattern; + } + + /** + * Set the ISO format used to format date values. + * @param iso the ISO format + */ + public void setIso(ISO iso) { + this.iso = iso; + } + + /** + * Set the style to use for date types. + */ + public void setDateStyle(FormatStyle dateStyle) { + this.dateStyle = dateStyle; + } + + /** + * Set the style to use for time types. + */ + public void setTimeStyle(FormatStyle timeStyle) { + this.timeStyle = timeStyle; + } + + /** + * Set the style to use for date and time types. + */ + public void setDateTimeStyle(FormatStyle dateTimeStyle) { + this.dateStyle = dateTimeStyle; + this.timeStyle = dateTimeStyle; + } + + /** + * Set the two characters to use to format date values, in Joda-Time style. + *

    The first character is used for the date style; the second is for + * the time style. Supported characters are: + *

      + *
    • 'S' = Small
    • + *
    • 'M' = Medium
    • + *
    • 'L' = Long
    • + *
    • 'F' = Full
    • + *
    • '-' = Omitted
    • + *
    + *

    This method mimics the styles supported by Joda-Time. Note that + * JSR-310 natively favors {@link java.time.format.FormatStyle} as used for + * {@link #setDateStyle}, {@link #setTimeStyle} and {@link #setDateTimeStyle}. + * @param style two characters from the set {"S", "M", "L", "F", "-"} + */ + public void setStylePattern(String style) { + Assert.isTrue(style.length() == 2, "Style pattern must consist of two characters"); + this.dateStyle = convertStyleCharacter(style.charAt(0)); + this.timeStyle = convertStyleCharacter(style.charAt(1)); + } + + @Nullable + private FormatStyle convertStyleCharacter(char c) { + switch (c) { + case 'S': return FormatStyle.SHORT; + case 'M': return FormatStyle.MEDIUM; + case 'L': return FormatStyle.LONG; + case 'F': return FormatStyle.FULL; + case '-': return null; + default: throw new IllegalArgumentException("Invalid style character '" + c + "'"); + } + } + + /** + * Set the {@code TimeZone} to normalize the date values into, if any. + * @param timeZone the time zone + */ + public void setTimeZone(TimeZone timeZone) { + this.timeZone = timeZone; + } + + + /** + * Create a new {@code DateTimeFormatter} using this factory. + *

    If no specific pattern or style has been defined, + * {@link FormatStyle#MEDIUM medium date time format} will be used. + * @return a new date time formatter + * @see #createDateTimeFormatter(DateTimeFormatter) + */ + public DateTimeFormatter createDateTimeFormatter() { + return createDateTimeFormatter(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)); + } + + /** + * Create a new {@code DateTimeFormatter} using this factory. + *

    If no specific pattern or style has been defined, + * the supplied {@code fallbackFormatter} will be used. + * @param fallbackFormatter the fall-back formatter to use + * when no specific factory properties have been set + * @return a new date time formatter + */ + public DateTimeFormatter createDateTimeFormatter(DateTimeFormatter fallbackFormatter) { + DateTimeFormatter dateTimeFormatter = null; + if (StringUtils.hasLength(this.pattern)) { + // Using strict parsing to align with Joda-Time and standard DateFormat behavior: + // otherwise, an overflow like e.g. Feb 29 for a non-leap-year wouldn't get rejected. + // However, with strict parsing, a year digit needs to be specified as 'u'... + String patternToUse = StringUtils.replace(this.pattern, "yy", "uu"); + dateTimeFormatter = DateTimeFormatter.ofPattern(patternToUse).withResolverStyle(ResolverStyle.STRICT); + } + else if (this.iso != null && this.iso != ISO.NONE) { + switch (this.iso) { + case DATE: + dateTimeFormatter = DateTimeFormatter.ISO_DATE; + break; + case TIME: + dateTimeFormatter = DateTimeFormatter.ISO_TIME; + break; + case DATE_TIME: + dateTimeFormatter = DateTimeFormatter.ISO_DATE_TIME; + break; + default: + throw new IllegalStateException("Unsupported ISO format: " + this.iso); + } + } + else if (this.dateStyle != null && this.timeStyle != null) { + dateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(this.dateStyle, this.timeStyle); + } + else if (this.dateStyle != null) { + dateTimeFormatter = DateTimeFormatter.ofLocalizedDate(this.dateStyle); + } + else if (this.timeStyle != null) { + dateTimeFormatter = DateTimeFormatter.ofLocalizedTime(this.timeStyle); + } + + if (dateTimeFormatter != null && this.timeZone != null) { + dateTimeFormatter = dateTimeFormatter.withZone(this.timeZone.toZoneId()); + } + return (dateTimeFormatter != null ? dateTimeFormatter : fallbackFormatter); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryBean.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryBean.java new file mode 100644 index 0000000..1a48b8c --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryBean.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.standard; + +import java.time.format.DateTimeFormatter; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.lang.Nullable; + +/** + * {@link FactoryBean} that creates a JSR-310 {@link java.time.format.DateTimeFormatter}. + * See the {@link DateTimeFormatterFactory base class} for configuration details. + * + * @author Juergen Hoeller + * @since 4.0 + * @see #setPattern + * @see #setIso + * @see #setDateStyle + * @see #setTimeStyle + * @see DateTimeFormatterFactory + */ +public class DateTimeFormatterFactoryBean extends DateTimeFormatterFactory + implements FactoryBean, InitializingBean { + + @Nullable + private DateTimeFormatter dateTimeFormatter; + + + @Override + public void afterPropertiesSet() { + this.dateTimeFormatter = createDateTimeFormatter(); + } + + @Override + @Nullable + public DateTimeFormatter getObject() { + return this.dateTimeFormatter; + } + + @Override + public Class getObjectType() { + return DateTimeFormatter.class; + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterRegistrar.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterRegistrar.java new file mode 100644 index 0000000..96b4a79 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterRegistrar.java @@ -0,0 +1,220 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.standard; + +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Month; +import java.time.MonthDay; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.Period; +import java.time.Year; +import java.time.YearMonth; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.EnumMap; +import java.util.Map; + +import org.springframework.format.FormatterRegistrar; +import org.springframework.format.FormatterRegistry; +import org.springframework.format.annotation.DateTimeFormat.ISO; + +/** + * Configures the JSR-310 java.time formatting system for use with Spring. + * + * @author Juergen Hoeller + * @author Phillip Webb + * @since 4.0 + * @see #setDateStyle + * @see #setTimeStyle + * @see #setDateTimeStyle + * @see #setUseIsoFormat + * @see org.springframework.format.FormatterRegistrar#registerFormatters + * @see org.springframework.format.datetime.DateFormatterRegistrar + * @see org.springframework.format.datetime.joda.DateTimeFormatterFactoryBean + */ +public class DateTimeFormatterRegistrar implements FormatterRegistrar { + + private enum Type {DATE, TIME, DATE_TIME} + + + /** + * User-defined formatters. + */ + private final Map formatters = new EnumMap<>(Type.class); + + /** + * Factories used when specific formatters have not been specified. + */ + private final Map factories = new EnumMap<>(Type.class); + + + public DateTimeFormatterRegistrar() { + for (Type type : Type.values()) { + this.factories.put(type, new DateTimeFormatterFactory()); + } + } + + + /** + * Set whether standard ISO formatting should be applied to all date/time types. + * Default is "false" (no). + *

    If set to "true", the "dateStyle", "timeStyle" and "dateTimeStyle" + * properties are effectively ignored. + */ + public void setUseIsoFormat(boolean useIsoFormat) { + this.factories.get(Type.DATE).setIso(useIsoFormat ? ISO.DATE : ISO.NONE); + this.factories.get(Type.TIME).setIso(useIsoFormat ? ISO.TIME : ISO.NONE); + this.factories.get(Type.DATE_TIME).setIso(useIsoFormat ? ISO.DATE_TIME : ISO.NONE); + } + + /** + * Set the default format style of {@link java.time.LocalDate} objects. + * Default is {@link java.time.format.FormatStyle#SHORT}. + */ + public void setDateStyle(FormatStyle dateStyle) { + this.factories.get(Type.DATE).setDateStyle(dateStyle); + } + + /** + * Set the default format style of {@link java.time.LocalTime} objects. + * Default is {@link java.time.format.FormatStyle#SHORT}. + */ + public void setTimeStyle(FormatStyle timeStyle) { + this.factories.get(Type.TIME).setTimeStyle(timeStyle); + } + + /** + * Set the default format style of {@link java.time.LocalDateTime} objects. + * Default is {@link java.time.format.FormatStyle#SHORT}. + */ + public void setDateTimeStyle(FormatStyle dateTimeStyle) { + this.factories.get(Type.DATE_TIME).setDateTimeStyle(dateTimeStyle); + } + + /** + * Set the formatter that will be used for objects representing date values. + *

    This formatter will be used for the {@link LocalDate} type. + * When specified, the {@link #setDateStyle dateStyle} and + * {@link #setUseIsoFormat useIsoFormat} properties will be ignored. + * @param formatter the formatter to use + * @see #setTimeFormatter + * @see #setDateTimeFormatter + */ + public void setDateFormatter(DateTimeFormatter formatter) { + this.formatters.put(Type.DATE, formatter); + } + + /** + * Set the formatter that will be used for objects representing time values. + *

    This formatter will be used for the {@link LocalTime} and {@link OffsetTime} + * types. When specified, the {@link #setTimeStyle timeStyle} and + * {@link #setUseIsoFormat useIsoFormat} properties will be ignored. + * @param formatter the formatter to use + * @see #setDateFormatter + * @see #setDateTimeFormatter + */ + public void setTimeFormatter(DateTimeFormatter formatter) { + this.formatters.put(Type.TIME, formatter); + } + + /** + * Set the formatter that will be used for objects representing date and time values. + *

    This formatter will be used for {@link LocalDateTime}, {@link ZonedDateTime} + * and {@link OffsetDateTime} types. When specified, the + * {@link #setDateTimeStyle dateTimeStyle} and + * {@link #setUseIsoFormat useIsoFormat} properties will be ignored. + * @param formatter the formatter to use + * @see #setDateFormatter + * @see #setTimeFormatter + */ + public void setDateTimeFormatter(DateTimeFormatter formatter) { + this.formatters.put(Type.DATE_TIME, formatter); + } + + + @Override + public void registerFormatters(FormatterRegistry registry) { + DateTimeConverters.registerConverters(registry); + + DateTimeFormatter df = getFormatter(Type.DATE); + DateTimeFormatter tf = getFormatter(Type.TIME); + DateTimeFormatter dtf = getFormatter(Type.DATE_TIME); + + // Efficient ISO_LOCAL_* variants for printing since they are twice as fast... + + registry.addFormatterForFieldType(LocalDate.class, + new TemporalAccessorPrinter( + df == DateTimeFormatter.ISO_DATE ? DateTimeFormatter.ISO_LOCAL_DATE : df), + new TemporalAccessorParser(LocalDate.class, df)); + + registry.addFormatterForFieldType(LocalTime.class, + new TemporalAccessorPrinter( + tf == DateTimeFormatter.ISO_TIME ? DateTimeFormatter.ISO_LOCAL_TIME : tf), + new TemporalAccessorParser(LocalTime.class, tf)); + + registry.addFormatterForFieldType(LocalDateTime.class, + new TemporalAccessorPrinter( + dtf == DateTimeFormatter.ISO_DATE_TIME ? DateTimeFormatter.ISO_LOCAL_DATE_TIME : dtf), + new TemporalAccessorParser(LocalDateTime.class, dtf)); + + registry.addFormatterForFieldType(ZonedDateTime.class, + new TemporalAccessorPrinter(dtf), + new TemporalAccessorParser(ZonedDateTime.class, dtf)); + + registry.addFormatterForFieldType(OffsetDateTime.class, + new TemporalAccessorPrinter(dtf), + new TemporalAccessorParser(OffsetDateTime.class, dtf)); + + registry.addFormatterForFieldType(OffsetTime.class, + new TemporalAccessorPrinter(tf), + new TemporalAccessorParser(OffsetTime.class, tf)); + + registry.addFormatterForFieldType(Instant.class, new InstantFormatter()); + registry.addFormatterForFieldType(Period.class, new PeriodFormatter()); + registry.addFormatterForFieldType(Duration.class, new DurationFormatter()); + registry.addFormatterForFieldType(Year.class, new YearFormatter()); + registry.addFormatterForFieldType(Month.class, new MonthFormatter()); + registry.addFormatterForFieldType(YearMonth.class, new YearMonthFormatter()); + registry.addFormatterForFieldType(MonthDay.class, new MonthDayFormatter()); + + registry.addFormatterForFieldAnnotation(new Jsr310DateTimeFormatAnnotationFormatterFactory()); + } + + private DateTimeFormatter getFormatter(Type type) { + DateTimeFormatter formatter = this.formatters.get(type); + if (formatter != null) { + return formatter; + } + DateTimeFormatter fallbackFormatter = getFallbackFormatter(type); + return this.factories.get(type).createDateTimeFormatter(fallbackFormatter); + } + + private DateTimeFormatter getFallbackFormatter(Type type) { + switch (type) { + case DATE: return DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT); + case TIME: return DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT); + default: return DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/DurationFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/DurationFormatter.java new file mode 100644 index 0000000..b36169b --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/DurationFormatter.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.standard; + +import java.text.ParseException; +import java.time.Duration; +import java.util.Locale; + +import org.springframework.format.Formatter; + +/** + * {@link Formatter} implementation for a JSR-310 {@link Duration}, + * following JSR-310's parsing rules for a Duration. + * + * @author Juergen Hoeller + * @since 4.2.4 + * @see Duration#parse + */ +class DurationFormatter implements Formatter { + + @Override + public Duration parse(String text, Locale locale) throws ParseException { + return Duration.parse(text); + } + + @Override + public String print(Duration object, Locale locale) { + return object.toString(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java new file mode 100644 index 0000000..456c0ad --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.standard; + +import java.text.ParseException; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +import org.springframework.format.Formatter; + +/** + * {@link Formatter} implementation for a JSR-310 {@link java.time.Instant}, + * following JSR-310's parsing rules for an Instant (that is, not using a + * configurable {@link java.time.format.DateTimeFormatter}): accepting the + * default {@code ISO_INSTANT} format as well as {@code RFC_1123_DATE_TIME} + * (which is commonly used for HTTP date header values), as of Spring 4.3. + * + * @author Juergen Hoeller + * @author Andrei Nevedomskii + * @since 4.0 + * @see java.time.Instant#parse + * @see java.time.format.DateTimeFormatter#ISO_INSTANT + * @see java.time.format.DateTimeFormatter#RFC_1123_DATE_TIME + */ +public class InstantFormatter implements Formatter { + + @Override + public Instant parse(String text, Locale locale) throws ParseException { + if (text.length() > 0 && Character.isAlphabetic(text.charAt(0))) { + // assuming RFC-1123 value a la "Tue, 3 Jun 2008 11:05:30 GMT" + return Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(text)); + } + else { + // assuming UTC instant a la "2007-12-03T10:15:30.00Z" + return Instant.parse(text); + } + } + + @Override + public String print(Instant object, Locale locale) { + return object.toString(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/Jsr310DateTimeFormatAnnotationFormatterFactory.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/Jsr310DateTimeFormatAnnotationFormatterFactory.java new file mode 100644 index 0000000..cd4d3a9 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/Jsr310DateTimeFormatAnnotationFormatterFactory.java @@ -0,0 +1,124 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.standard; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.context.support.EmbeddedValueResolutionSupport; +import org.springframework.format.AnnotationFormatterFactory; +import org.springframework.format.Parser; +import org.springframework.format.Printer; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.util.StringUtils; + +/** + * Formats fields annotated with the {@link DateTimeFormat} annotation using the + * JSR-310 java.time package in JDK 8. + * + * @author Juergen Hoeller + * @since 4.0 + * @see org.springframework.format.annotation.DateTimeFormat + */ +public class Jsr310DateTimeFormatAnnotationFormatterFactory extends EmbeddedValueResolutionSupport + implements AnnotationFormatterFactory { + + private static final Set> FIELD_TYPES; + + static { + // Create the set of field types that may be annotated with @DateTimeFormat. + Set> fieldTypes = new HashSet<>(8); + fieldTypes.add(LocalDate.class); + fieldTypes.add(LocalTime.class); + fieldTypes.add(LocalDateTime.class); + fieldTypes.add(ZonedDateTime.class); + fieldTypes.add(OffsetDateTime.class); + fieldTypes.add(OffsetTime.class); + FIELD_TYPES = Collections.unmodifiableSet(fieldTypes); + } + + + @Override + public final Set> getFieldTypes() { + return FIELD_TYPES; + } + + @Override + public Printer getPrinter(DateTimeFormat annotation, Class fieldType) { + DateTimeFormatter formatter = getFormatter(annotation, fieldType); + + // Efficient ISO_LOCAL_* variants for printing since they are twice as fast... + if (formatter == DateTimeFormatter.ISO_DATE) { + if (isLocal(fieldType)) { + formatter = DateTimeFormatter.ISO_LOCAL_DATE; + } + } + else if (formatter == DateTimeFormatter.ISO_TIME) { + if (isLocal(fieldType)) { + formatter = DateTimeFormatter.ISO_LOCAL_TIME; + } + } + else if (formatter == DateTimeFormatter.ISO_DATE_TIME) { + if (isLocal(fieldType)) { + formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME; + } + } + + return new TemporalAccessorPrinter(formatter); + } + + @Override + @SuppressWarnings("unchecked") + public Parser getParser(DateTimeFormat annotation, Class fieldType) { + DateTimeFormatter formatter = getFormatter(annotation, fieldType); + return new TemporalAccessorParser((Class) fieldType, formatter); + } + + /** + * Factory method used to create a {@link DateTimeFormatter}. + * @param annotation the format annotation for the field + * @param fieldType the declared type of the field + * @return a {@link DateTimeFormatter} instance + */ + protected DateTimeFormatter getFormatter(DateTimeFormat annotation, Class fieldType) { + DateTimeFormatterFactory factory = new DateTimeFormatterFactory(); + String style = resolveEmbeddedValue(annotation.style()); + if (StringUtils.hasLength(style)) { + factory.setStylePattern(style); + } + factory.setIso(annotation.iso()); + String pattern = resolveEmbeddedValue(annotation.pattern()); + if (StringUtils.hasLength(pattern)) { + factory.setPattern(pattern); + } + return factory.createDateTimeFormatter(); + } + + private boolean isLocal(Class fieldType) { + return fieldType.getSimpleName().startsWith("Local"); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/MonthDayFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/MonthDayFormatter.java new file mode 100644 index 0000000..58cb4e1 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/MonthDayFormatter.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.standard; + +import java.text.ParseException; +import java.time.MonthDay; +import java.util.Locale; + +import org.springframework.format.Formatter; + +/** + * {@link Formatter} implementation for a JSR-310 {@link MonthDay}, + * following JSR-310's parsing rules for a MonthDay. + * + * @author Juergen Hoeller + * @since 4.2.4 + * @see MonthDay#parse + */ +class MonthDayFormatter implements Formatter { + + @Override + public MonthDay parse(String text, Locale locale) throws ParseException { + return MonthDay.parse(text); + } + + @Override + public String print(MonthDay object, Locale locale) { + return object.toString(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/MonthFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/MonthFormatter.java new file mode 100644 index 0000000..abae18d --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/MonthFormatter.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.standard; + +import java.text.ParseException; +import java.time.Month; +import java.util.Locale; + +import org.springframework.format.Formatter; + +/** + * {@link Formatter} implementation for a JSR-310 {@link Month}, + * resolving a given String against the Month enum values (ignoring case). + * + * @author Juergen Hoeller + * @since 5.0.4 + * @see Month#valueOf + */ +class MonthFormatter implements Formatter { + + @Override + public Month parse(String text, Locale locale) throws ParseException { + return Month.valueOf(text.toUpperCase()); + } + + @Override + public String print(Month object, Locale locale) { + return object.toString(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/PeriodFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/PeriodFormatter.java new file mode 100644 index 0000000..844a233 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/PeriodFormatter.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.standard; + +import java.text.ParseException; +import java.time.Period; +import java.util.Locale; + +import org.springframework.format.Formatter; + +/** + * {@link Formatter} implementation for a JSR-310 {@link Period}, + * following JSR-310's parsing rules for a Period. + * + * @author Juergen Hoeller + * @since 4.2.4 + * @see Period#parse + */ +class PeriodFormatter implements Formatter { + + @Override + public Period parse(String text, Locale locale) throws ParseException { + return Period.parse(text); + } + + @Override + public String print(Period object, Locale locale) { + return object.toString(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/TemporalAccessorParser.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/TemporalAccessorParser.java new file mode 100644 index 0000000..4e135ee --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/TemporalAccessorParser.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.standard; + +import java.text.ParseException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.util.Locale; + +import org.springframework.format.Parser; + +/** + * {@link Parser} implementation for a JSR-310 {@link java.time.temporal.TemporalAccessor}, + * using a {@link java.time.format.DateTimeFormatter}) (the contextual one, if available). + * + * @author Juergen Hoeller + * @since 4.0 + * @see DateTimeContextHolder#getFormatter + * @see java.time.LocalDate#parse(CharSequence, java.time.format.DateTimeFormatter) + * @see java.time.LocalTime#parse(CharSequence, java.time.format.DateTimeFormatter) + * @see java.time.LocalDateTime#parse(CharSequence, java.time.format.DateTimeFormatter) + * @see java.time.ZonedDateTime#parse(CharSequence, java.time.format.DateTimeFormatter) + * @see java.time.OffsetDateTime#parse(CharSequence, java.time.format.DateTimeFormatter) + * @see java.time.OffsetTime#parse(CharSequence, java.time.format.DateTimeFormatter) + */ +public final class TemporalAccessorParser implements Parser { + + private final Class temporalAccessorType; + + private final DateTimeFormatter formatter; + + + /** + * Create a new TemporalAccessorParser for the given TemporalAccessor type. + * @param temporalAccessorType the specific TemporalAccessor class + * (LocalDate, LocalTime, LocalDateTime, ZonedDateTime, OffsetDateTime, OffsetTime) + * @param formatter the base DateTimeFormatter instance + */ + public TemporalAccessorParser(Class temporalAccessorType, DateTimeFormatter formatter) { + this.temporalAccessorType = temporalAccessorType; + this.formatter = formatter; + } + + + @Override + public TemporalAccessor parse(String text, Locale locale) throws ParseException { + DateTimeFormatter formatterToUse = DateTimeContextHolder.getFormatter(this.formatter, locale); + if (LocalDate.class == this.temporalAccessorType) { + return LocalDate.parse(text, formatterToUse); + } + else if (LocalTime.class == this.temporalAccessorType) { + return LocalTime.parse(text, formatterToUse); + } + else if (LocalDateTime.class == this.temporalAccessorType) { + return LocalDateTime.parse(text, formatterToUse); + } + else if (ZonedDateTime.class == this.temporalAccessorType) { + return ZonedDateTime.parse(text, formatterToUse); + } + else if (OffsetDateTime.class == this.temporalAccessorType) { + return OffsetDateTime.parse(text, formatterToUse); + } + else if (OffsetTime.class == this.temporalAccessorType) { + return OffsetTime.parse(text, formatterToUse); + } + else { + throw new IllegalStateException("Unsupported TemporalAccessor type: " + this.temporalAccessorType); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/TemporalAccessorPrinter.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/TemporalAccessorPrinter.java new file mode 100644 index 0000000..75c49d0 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/TemporalAccessorPrinter.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.standard; + +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.util.Locale; + +import org.springframework.format.Printer; + +/** + * {@link Printer} implementation for a JSR-310 {@link java.time.temporal.TemporalAccessor}, + * using a {@link java.time.format.DateTimeFormatter}) (the contextual one, if available). + * + * @author Juergen Hoeller + * @since 4.0 + * @see DateTimeContextHolder#getFormatter + * @see java.time.format.DateTimeFormatter#format(java.time.temporal.TemporalAccessor) + */ +public final class TemporalAccessorPrinter implements Printer { + + private final DateTimeFormatter formatter; + + + /** + * Create a new TemporalAccessorPrinter. + * @param formatter the base DateTimeFormatter instance + */ + public TemporalAccessorPrinter(DateTimeFormatter formatter) { + this.formatter = formatter; + } + + + @Override + public String print(TemporalAccessor partial, Locale locale) { + return DateTimeContextHolder.getFormatter(this.formatter, locale).format(partial); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/YearFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/YearFormatter.java new file mode 100644 index 0000000..d913cfa --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/YearFormatter.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.standard; + +import java.text.ParseException; +import java.time.Year; +import java.util.Locale; + +import org.springframework.format.Formatter; + +/** + * {@link Formatter} implementation for a JSR-310 {@link Year}, + * following JSR-310's parsing rules for a Year. + * + * @author Juergen Hoeller + * @since 5.0.4 + * @see Year#parse + */ +class YearFormatter implements Formatter { + + @Override + public Year parse(String text, Locale locale) throws ParseException { + return Year.parse(text); + } + + @Override + public String print(Year object, Locale locale) { + return object.toString(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/YearMonthFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/YearMonthFormatter.java new file mode 100644 index 0000000..8550b0f --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/YearMonthFormatter.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.standard; + +import java.text.ParseException; +import java.time.YearMonth; +import java.util.Locale; + +import org.springframework.format.Formatter; + +/** + * {@link Formatter} implementation for a JSR-310 {@link YearMonth}, + * following JSR-310's parsing rules for a YearMonth. + * + * @author Juergen Hoeller + * @since 4.2.4 + * @see YearMonth#parse + */ +class YearMonthFormatter implements Formatter { + + @Override + public YearMonth parse(String text, Locale locale) throws ParseException { + return YearMonth.parse(text); + } + + @Override + public String print(YearMonth object, Locale locale) { + return object.toString(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/package-info.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/package-info.java new file mode 100644 index 0000000..fd73fe6 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/package-info.java @@ -0,0 +1,9 @@ +/** + * Integration with the JSR-310 java.time package in JDK 8. + */ +@NonNullApi +@NonNullFields +package org.springframework.format.datetime.standard; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/format/number/AbstractNumberFormatter.java b/spring-context/src/main/java/org/springframework/format/number/AbstractNumberFormatter.java new file mode 100644 index 0000000..09b6097 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/number/AbstractNumberFormatter.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.number; + +import java.text.NumberFormat; +import java.text.ParseException; +import java.text.ParsePosition; +import java.util.Locale; + +import org.springframework.format.Formatter; + +/** + * Abstract formatter for Numbers, + * providing a {@link #getNumberFormat(java.util.Locale)} template method. + * + * @author Juergen Hoeller + * @author Keith Donald + * @since 3.0 + */ +public abstract class AbstractNumberFormatter implements Formatter { + + private boolean lenient = false; + + + /** + * Specify whether or not parsing is to be lenient. Default is false. + *

    With lenient parsing, the parser may allow inputs that do not precisely match the format. + * With strict parsing, inputs must match the format exactly. + */ + public void setLenient(boolean lenient) { + this.lenient = lenient; + } + + + @Override + public String print(Number number, Locale locale) { + return getNumberFormat(locale).format(number); + } + + @Override + public Number parse(String text, Locale locale) throws ParseException { + NumberFormat format = getNumberFormat(locale); + ParsePosition position = new ParsePosition(0); + Number number = format.parse(text, position); + if (position.getErrorIndex() != -1) { + throw new ParseException(text, position.getIndex()); + } + if (!this.lenient) { + if (text.length() != position.getIndex()) { + // indicates a part of the string that was not parsed + throw new ParseException(text, position.getIndex()); + } + } + return number; + } + + /** + * Obtain a concrete NumberFormat for the specified locale. + * @param locale the current locale + * @return the NumberFormat instance (never {@code null}) + */ + protected abstract NumberFormat getNumberFormat(Locale locale); + +} diff --git a/spring-context/src/main/java/org/springframework/format/number/CurrencyStyleFormatter.java b/spring-context/src/main/java/org/springframework/format/number/CurrencyStyleFormatter.java new file mode 100644 index 0000000..6c030d0 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/number/CurrencyStyleFormatter.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.number; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.text.ParseException; +import java.util.Currency; +import java.util.Locale; + +import org.springframework.lang.Nullable; + +/** + * A BigDecimal formatter for number values in currency style. + * + *

    Delegates to {@link java.text.NumberFormat#getCurrencyInstance(Locale)}. + * Configures BigDecimal parsing so there is no loss of precision. + * Can apply a specified {@link java.math.RoundingMode} to parsed values. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 4.2 + * @see #setLenient + * @see #setRoundingMode + */ +public class CurrencyStyleFormatter extends AbstractNumberFormatter { + + private int fractionDigits = 2; + + @Nullable + private RoundingMode roundingMode; + + @Nullable + private Currency currency; + + @Nullable + private String pattern; + + + /** + * Specify the desired number of fraction digits. + * Default is 2. + */ + public void setFractionDigits(int fractionDigits) { + this.fractionDigits = fractionDigits; + } + + /** + * Specify the rounding mode to use for decimal parsing. + * Default is {@link java.math.RoundingMode#UNNECESSARY}. + */ + public void setRoundingMode(RoundingMode roundingMode) { + this.roundingMode = roundingMode; + } + + /** + * Specify the currency, if known. + */ + public void setCurrency(Currency currency) { + this.currency = currency; + } + + /** + * Specify the pattern to use to format number values. + * If not specified, the default DecimalFormat pattern is used. + * @see java.text.DecimalFormat#applyPattern(String) + */ + public void setPattern(String pattern) { + this.pattern = pattern; + } + + + @Override + public BigDecimal parse(String text, Locale locale) throws ParseException { + BigDecimal decimal = (BigDecimal) super.parse(text, locale); + if (this.roundingMode != null) { + decimal = decimal.setScale(this.fractionDigits, this.roundingMode); + } + else { + decimal = decimal.setScale(this.fractionDigits); + } + return decimal; + } + + @Override + protected NumberFormat getNumberFormat(Locale locale) { + DecimalFormat format = (DecimalFormat) NumberFormat.getCurrencyInstance(locale); + format.setParseBigDecimal(true); + format.setMaximumFractionDigits(this.fractionDigits); + format.setMinimumFractionDigits(this.fractionDigits); + if (this.roundingMode != null) { + format.setRoundingMode(this.roundingMode); + } + if (this.currency != null) { + format.setCurrency(this.currency); + } + if (this.pattern != null) { + format.applyPattern(this.pattern); + } + return format; + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/number/NumberFormatAnnotationFormatterFactory.java b/spring-context/src/main/java/org/springframework/format/number/NumberFormatAnnotationFormatterFactory.java new file mode 100644 index 0000000..85c3b57 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/number/NumberFormatAnnotationFormatterFactory.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.number; + +import java.util.Set; + +import org.springframework.context.support.EmbeddedValueResolutionSupport; +import org.springframework.format.AnnotationFormatterFactory; +import org.springframework.format.Formatter; +import org.springframework.format.Parser; +import org.springframework.format.Printer; +import org.springframework.format.annotation.NumberFormat; +import org.springframework.format.annotation.NumberFormat.Style; +import org.springframework.util.NumberUtils; +import org.springframework.util.StringUtils; + +/** + * Formats fields annotated with the {@link NumberFormat} annotation. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + * @see NumberFormat + */ +public class NumberFormatAnnotationFormatterFactory extends EmbeddedValueResolutionSupport + implements AnnotationFormatterFactory { + + @Override + public Set> getFieldTypes() { + return NumberUtils.STANDARD_NUMBER_TYPES; + } + + @Override + public Printer getPrinter(NumberFormat annotation, Class fieldType) { + return configureFormatterFrom(annotation); + } + + @Override + public Parser getParser(NumberFormat annotation, Class fieldType) { + return configureFormatterFrom(annotation); + } + + + private Formatter configureFormatterFrom(NumberFormat annotation) { + String pattern = resolveEmbeddedValue(annotation.pattern()); + if (StringUtils.hasLength(pattern)) { + return new NumberStyleFormatter(pattern); + } + else { + Style style = annotation.style(); + if (style == Style.CURRENCY) { + return new CurrencyStyleFormatter(); + } + else if (style == Style.PERCENT) { + return new PercentStyleFormatter(); + } + else { + return new NumberStyleFormatter(); + } + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/number/NumberStyleFormatter.java b/spring-context/src/main/java/org/springframework/format/number/NumberStyleFormatter.java new file mode 100644 index 0000000..2ebbe36 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/number/NumberStyleFormatter.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.number; + +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.Locale; + +import org.springframework.lang.Nullable; + +/** + * A general-purpose number formatter using NumberFormat's number style. + * + *

    Delegates to {@link java.text.NumberFormat#getInstance(Locale)}. + * Configures BigDecimal parsing so there is no loss in precision. + * Allows configuration over the decimal number pattern. + * The {@link #parse(String, Locale)} routine always returns a BigDecimal. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 4.2 + * @see #setPattern + * @see #setLenient + */ +public class NumberStyleFormatter extends AbstractNumberFormatter { + + @Nullable + private String pattern; + + + /** + * Create a new NumberStyleFormatter without a pattern. + */ + public NumberStyleFormatter() { + } + + /** + * Create a new NumberStyleFormatter with the specified pattern. + * @param pattern the format pattern + * @see #setPattern + */ + public NumberStyleFormatter(String pattern) { + this.pattern = pattern; + } + + + /** + * Specify the pattern to use to format number values. + * If not specified, the default DecimalFormat pattern is used. + * @see java.text.DecimalFormat#applyPattern(String) + */ + public void setPattern(String pattern) { + this.pattern = pattern; + } + + + @Override + public NumberFormat getNumberFormat(Locale locale) { + NumberFormat format = NumberFormat.getInstance(locale); + if (!(format instanceof DecimalFormat)) { + if (this.pattern != null) { + throw new IllegalStateException("Cannot support pattern for non-DecimalFormat: " + format); + } + return format; + } + DecimalFormat decimalFormat = (DecimalFormat) format; + decimalFormat.setParseBigDecimal(true); + if (this.pattern != null) { + decimalFormat.applyPattern(this.pattern); + } + return decimalFormat; + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/number/PercentStyleFormatter.java b/spring-context/src/main/java/org/springframework/format/number/PercentStyleFormatter.java new file mode 100644 index 0000000..0d8fe67 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/number/PercentStyleFormatter.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.number; + +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.Locale; + +/** + * A formatter for number values in percent style. + * + *

    Delegates to {@link java.text.NumberFormat#getPercentInstance(Locale)}. + * Configures BigDecimal parsing so there is no loss in precision. + * The {@link #parse(String, Locale)} routine always returns a BigDecimal. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 4.2 + * @see #setLenient + */ +public class PercentStyleFormatter extends AbstractNumberFormatter { + + @Override + protected NumberFormat getNumberFormat(Locale locale) { + NumberFormat format = NumberFormat.getPercentInstance(locale); + if (format instanceof DecimalFormat) { + ((DecimalFormat) format).setParseBigDecimal(true); + } + return format; + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/number/money/CurrencyUnitFormatter.java b/spring-context/src/main/java/org/springframework/format/number/money/CurrencyUnitFormatter.java new file mode 100644 index 0000000..3a8f10c --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/number/money/CurrencyUnitFormatter.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.number.money; + +import java.util.Locale; + +import javax.money.CurrencyUnit; +import javax.money.Monetary; + +import org.springframework.format.Formatter; + +/** + * Formatter for JSR-354 {@link javax.money.CurrencyUnit} values, + * from and to currency code Strings. + * + * @author Juergen Hoeller + * @since 4.2 + */ +public class CurrencyUnitFormatter implements Formatter { + + @Override + public String print(CurrencyUnit object, Locale locale) { + return object.getCurrencyCode(); + } + + @Override + public CurrencyUnit parse(String text, Locale locale) { + return Monetary.getCurrency(text); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/number/money/Jsr354NumberFormatAnnotationFormatterFactory.java b/spring-context/src/main/java/org/springframework/format/number/money/Jsr354NumberFormatAnnotationFormatterFactory.java new file mode 100644 index 0000000..1f7c7a2 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/number/money/Jsr354NumberFormatAnnotationFormatterFactory.java @@ -0,0 +1,164 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.number.money; + +import java.text.ParseException; +import java.util.Collections; +import java.util.Currency; +import java.util.Locale; +import java.util.Set; + +import javax.money.CurrencyUnit; +import javax.money.Monetary; +import javax.money.MonetaryAmount; + +import org.springframework.context.support.EmbeddedValueResolutionSupport; +import org.springframework.format.AnnotationFormatterFactory; +import org.springframework.format.Formatter; +import org.springframework.format.Parser; +import org.springframework.format.Printer; +import org.springframework.format.annotation.NumberFormat; +import org.springframework.format.annotation.NumberFormat.Style; +import org.springframework.format.number.CurrencyStyleFormatter; +import org.springframework.format.number.NumberStyleFormatter; +import org.springframework.format.number.PercentStyleFormatter; +import org.springframework.util.StringUtils; + +/** + * Formats {@link javax.money.MonetaryAmount} fields annotated + * with Spring's common {@link NumberFormat} annotation. + * + * @author Juergen Hoeller + * @since 4.2 + * @see NumberFormat + */ +public class Jsr354NumberFormatAnnotationFormatterFactory extends EmbeddedValueResolutionSupport + implements AnnotationFormatterFactory { + + private static final String CURRENCY_CODE_PATTERN = "\u00A4\u00A4"; + + + @Override + public Set> getFieldTypes() { + return Collections.singleton(MonetaryAmount.class); + } + + @Override + public Printer getPrinter(NumberFormat annotation, Class fieldType) { + return configureFormatterFrom(annotation); + } + + @Override + public Parser getParser(NumberFormat annotation, Class fieldType) { + return configureFormatterFrom(annotation); + } + + + private Formatter configureFormatterFrom(NumberFormat annotation) { + String pattern = resolveEmbeddedValue(annotation.pattern()); + if (StringUtils.hasLength(pattern)) { + return new PatternDecoratingFormatter(pattern); + } + else { + Style style = annotation.style(); + if (style == Style.NUMBER) { + return new NumberDecoratingFormatter(new NumberStyleFormatter()); + } + else if (style == Style.PERCENT) { + return new NumberDecoratingFormatter(new PercentStyleFormatter()); + } + else { + return new NumberDecoratingFormatter(new CurrencyStyleFormatter()); + } + } + } + + + private static class NumberDecoratingFormatter implements Formatter { + + private final Formatter numberFormatter; + + public NumberDecoratingFormatter(Formatter numberFormatter) { + this.numberFormatter = numberFormatter; + } + + @Override + public String print(MonetaryAmount object, Locale locale) { + return this.numberFormatter.print(object.getNumber(), locale); + } + + @Override + public MonetaryAmount parse(String text, Locale locale) throws ParseException { + CurrencyUnit currencyUnit = Monetary.getCurrency(locale); + Number numberValue = this.numberFormatter.parse(text, locale); + return Monetary.getDefaultAmountFactory().setNumber(numberValue).setCurrency(currencyUnit).create(); + } + } + + + private static class PatternDecoratingFormatter implements Formatter { + + private final String pattern; + + public PatternDecoratingFormatter(String pattern) { + this.pattern = pattern; + } + + @Override + public String print(MonetaryAmount object, Locale locale) { + CurrencyStyleFormatter formatter = new CurrencyStyleFormatter(); + formatter.setCurrency(Currency.getInstance(object.getCurrency().getCurrencyCode())); + formatter.setPattern(this.pattern); + return formatter.print(object.getNumber(), locale); + } + + @Override + public MonetaryAmount parse(String text, Locale locale) throws ParseException { + CurrencyStyleFormatter formatter = new CurrencyStyleFormatter(); + Currency currency = determineCurrency(text, locale); + CurrencyUnit currencyUnit = Monetary.getCurrency(currency.getCurrencyCode()); + formatter.setCurrency(currency); + formatter.setPattern(this.pattern); + Number numberValue = formatter.parse(text, locale); + return Monetary.getDefaultAmountFactory().setNumber(numberValue).setCurrency(currencyUnit).create(); + } + + private Currency determineCurrency(String text, Locale locale) { + try { + if (text.length() < 3) { + // Could not possibly contain a currency code -> + // try with locale and likely let it fail on parse. + return Currency.getInstance(locale); + } + else if (this.pattern.startsWith(CURRENCY_CODE_PATTERN)) { + return Currency.getInstance(text.substring(0, 3)); + } + else if (this.pattern.endsWith(CURRENCY_CODE_PATTERN)) { + return Currency.getInstance(text.substring(text.length() - 3)); + } + else { + // A pattern without a currency code... + return Currency.getInstance(locale); + } + } + catch (IllegalArgumentException ex) { + throw new IllegalArgumentException("Cannot determine currency for number value [" + text + "]", ex); + } + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/number/money/MonetaryAmountFormatter.java b/spring-context/src/main/java/org/springframework/format/number/money/MonetaryAmountFormatter.java new file mode 100644 index 0000000..1127b92 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/number/money/MonetaryAmountFormatter.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.number.money; + +import java.util.Locale; + +import javax.money.MonetaryAmount; +import javax.money.format.MonetaryAmountFormat; +import javax.money.format.MonetaryFormats; + +import org.springframework.format.Formatter; +import org.springframework.lang.Nullable; + +/** + * Formatter for JSR-354 {@link javax.money.MonetaryAmount} values, + * delegating to {@link javax.money.format.MonetaryAmountFormat#format} + * and {@link javax.money.format.MonetaryAmountFormat#parse}. + * + * @author Juergen Hoeller + * @since 4.2 + * @see #getMonetaryAmountFormat + */ +public class MonetaryAmountFormatter implements Formatter { + + @Nullable + private String formatName; + + + /** + * Create a locale-driven MonetaryAmountFormatter. + */ + public MonetaryAmountFormatter() { + } + + /** + * Create a new MonetaryAmountFormatter for the given format name. + * @param formatName the format name, to be resolved by the JSR-354 + * provider at runtime + */ + public MonetaryAmountFormatter(String formatName) { + this.formatName = formatName; + } + + + /** + * Specify the format name, to be resolved by the JSR-354 provider + * at runtime. + *

    Default is none, obtaining a {@link MonetaryAmountFormat} + * based on the current locale. + */ + public void setFormatName(String formatName) { + this.formatName = formatName; + } + + + @Override + public String print(MonetaryAmount object, Locale locale) { + return getMonetaryAmountFormat(locale).format(object); + } + + @Override + public MonetaryAmount parse(String text, Locale locale) { + return getMonetaryAmountFormat(locale).parse(text); + } + + + /** + * Obtain a MonetaryAmountFormat for the given locale. + *

    The default implementation simply calls + * {@link javax.money.format.MonetaryFormats#getAmountFormat} + * with either the configured format name or the given locale. + * @param locale the current locale + * @return the MonetaryAmountFormat (never {@code null}) + * @see #setFormatName + */ + protected MonetaryAmountFormat getMonetaryAmountFormat(Locale locale) { + if (this.formatName != null) { + return MonetaryFormats.getAmountFormat(this.formatName); + } + else { + return MonetaryFormats.getAmountFormat(locale); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/number/money/package-info.java b/spring-context/src/main/java/org/springframework/format/number/money/package-info.java new file mode 100644 index 0000000..91fbcd0 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/number/money/package-info.java @@ -0,0 +1,9 @@ +/** + * Integration with the JSR-354 javax.money package. + */ +@NonNullApi +@NonNullFields +package org.springframework.format.number.money; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/format/number/package-info.java b/spring-context/src/main/java/org/springframework/format/number/package-info.java new file mode 100644 index 0000000..7fffd8a --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/number/package-info.java @@ -0,0 +1,9 @@ +/** + * Formatters for {@code java.lang.Number} properties. + */ +@NonNullApi +@NonNullFields +package org.springframework.format.number; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/format/package-info.java b/spring-context/src/main/java/org/springframework/format/package-info.java new file mode 100644 index 0000000..727b1ad --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/package-info.java @@ -0,0 +1,9 @@ +/** + * An API for defining Formatters to format field model values for display in a UI. + */ +@NonNullApi +@NonNullFields +package org.springframework.format; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java b/spring-context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java new file mode 100644 index 0000000..805a982 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.support; + +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.format.FormatterRegistry; +import org.springframework.format.datetime.DateFormatterRegistrar; +import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar; +import org.springframework.format.number.NumberFormatAnnotationFormatterFactory; +import org.springframework.format.number.money.CurrencyUnitFormatter; +import org.springframework.format.number.money.Jsr354NumberFormatAnnotationFormatterFactory; +import org.springframework.format.number.money.MonetaryAmountFormatter; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringValueResolver; + +/** + * A specialization of {@link FormattingConversionService} configured by default with + * converters and formatters appropriate for most applications. + * + *

    Designed for direct instantiation but also exposes the static {@link #addDefaultFormatters} + * utility method for ad hoc use against any {@code FormatterRegistry} instance, just + * as {@code DefaultConversionService} exposes its own + * {@link DefaultConversionService#addDefaultConverters addDefaultConverters} method. + * + *

    Automatically registers formatters for JSR-354 Money & Currency, JSR-310 Date-Time + * and/or Joda-Time 2.x, depending on the presence of the corresponding API on the classpath. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + */ +public class DefaultFormattingConversionService extends FormattingConversionService { + + private static final boolean jsr354Present; + + private static final boolean jodaTimePresent; + + static { + ClassLoader classLoader = DefaultFormattingConversionService.class.getClassLoader(); + jsr354Present = ClassUtils.isPresent("javax.money.MonetaryAmount", classLoader); + jodaTimePresent = ClassUtils.isPresent("org.joda.time.YearMonth", classLoader); + } + + /** + * Create a new {@code DefaultFormattingConversionService} with the set of + * {@linkplain DefaultConversionService#addDefaultConverters default converters} and + * {@linkplain #addDefaultFormatters default formatters}. + */ + public DefaultFormattingConversionService() { + this(null, true); + } + + /** + * Create a new {@code DefaultFormattingConversionService} with the set of + * {@linkplain DefaultConversionService#addDefaultConverters default converters} and, + * based on the value of {@code registerDefaultFormatters}, the set of + * {@linkplain #addDefaultFormatters default formatters}. + * @param registerDefaultFormatters whether to register default formatters + */ + public DefaultFormattingConversionService(boolean registerDefaultFormatters) { + this(null, registerDefaultFormatters); + } + + /** + * Create a new {@code DefaultFormattingConversionService} with the set of + * {@linkplain DefaultConversionService#addDefaultConverters default converters} and, + * based on the value of {@code registerDefaultFormatters}, the set of + * {@linkplain #addDefaultFormatters default formatters}. + * @param embeddedValueResolver delegated to {@link #setEmbeddedValueResolver(StringValueResolver)} + * prior to calling {@link #addDefaultFormatters}. + * @param registerDefaultFormatters whether to register default formatters + */ + public DefaultFormattingConversionService( + @Nullable StringValueResolver embeddedValueResolver, boolean registerDefaultFormatters) { + + if (embeddedValueResolver != null) { + setEmbeddedValueResolver(embeddedValueResolver); + } + DefaultConversionService.addDefaultConverters(this); + if (registerDefaultFormatters) { + addDefaultFormatters(this); + } + } + + + /** + * Add formatters appropriate for most environments: including number formatters, + * JSR-354 Money & Currency formatters, JSR-310 Date-Time and/or Joda-Time formatters, + * depending on the presence of the corresponding API on the classpath. + * @param formatterRegistry the service to register default formatters with + */ + @SuppressWarnings("deprecation") + public static void addDefaultFormatters(FormatterRegistry formatterRegistry) { + // Default handling of number values + formatterRegistry.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory()); + + // Default handling of monetary values + if (jsr354Present) { + formatterRegistry.addFormatter(new CurrencyUnitFormatter()); + formatterRegistry.addFormatter(new MonetaryAmountFormatter()); + formatterRegistry.addFormatterForFieldAnnotation(new Jsr354NumberFormatAnnotationFormatterFactory()); + } + + // Default handling of date-time values + + // just handling JSR-310 specific date and time types + new DateTimeFormatterRegistrar().registerFormatters(formatterRegistry); + + if (jodaTimePresent) { + // handles Joda-specific types as well as Date, Calendar, Long + new org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar().registerFormatters(formatterRegistry); + } + else { + // regular DateFormat-based Date, Calendar, Long converters + new DateFormatterRegistrar().registerFormatters(formatterRegistry); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/support/FormatterPropertyEditorAdapter.java b/spring-context/src/main/java/org/springframework/format/support/FormatterPropertyEditorAdapter.java new file mode 100644 index 0000000..48a85a2 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/support/FormatterPropertyEditorAdapter.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.support; + +import java.beans.PropertyEditor; +import java.beans.PropertyEditorSupport; + +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.format.Formatter; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Adapter that bridges between {@link Formatter} and {@link PropertyEditor}. + * + * @author Juergen Hoeller + * @since 4.2 + */ +public class FormatterPropertyEditorAdapter extends PropertyEditorSupport { + + private final Formatter formatter; + + + /** + * Create a new {@code FormatterPropertyEditorAdapter} for the given {@link Formatter}. + * @param formatter the {@link Formatter} to wrap + */ + @SuppressWarnings("unchecked") + public FormatterPropertyEditorAdapter(Formatter formatter) { + Assert.notNull(formatter, "Formatter must not be null"); + this.formatter = (Formatter) formatter; + } + + + /** + * Determine the {@link Formatter}-declared field type. + * @return the field type declared in the wrapped {@link Formatter} implementation + * (never {@code null}) + * @throws IllegalArgumentException if the {@link Formatter}-declared field type + * cannot be inferred + */ + public Class getFieldType() { + return FormattingConversionService.getFieldType(this.formatter); + } + + + @Override + public void setAsText(String text) throws IllegalArgumentException { + if (StringUtils.hasText(text)) { + try { + setValue(this.formatter.parse(text, LocaleContextHolder.getLocale())); + } + catch (IllegalArgumentException ex) { + throw ex; + } + catch (Throwable ex) { + throw new IllegalArgumentException("Parse attempt failed for value [" + text + "]", ex); + } + } + else { + setValue(null); + } + } + + @Override + public String getAsText() { + Object value = getValue(); + return (value != null ? this.formatter.print(value, LocaleContextHolder.getLocale()) : ""); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/support/FormattingConversionService.java b/spring-context/src/main/java/org/springframework/format/support/FormattingConversionService.java new file mode 100644 index 0000000..661c198 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/support/FormattingConversionService.java @@ -0,0 +1,386 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.support; + +import java.lang.annotation.Annotation; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.context.EmbeddedValueResolverAware; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.core.DecoratingProxy; +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.core.convert.converter.GenericConverter; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.format.AnnotationFormatterFactory; +import org.springframework.format.Formatter; +import org.springframework.format.FormatterRegistry; +import org.springframework.format.Parser; +import org.springframework.format.Printer; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; +import org.springframework.util.StringValueResolver; + +/** + * A {@link org.springframework.core.convert.ConversionService} implementation + * designed to be configured as a {@link FormatterRegistry}. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + */ +public class FormattingConversionService extends GenericConversionService + implements FormatterRegistry, EmbeddedValueResolverAware { + + @Nullable + private StringValueResolver embeddedValueResolver; + + private final Map cachedPrinters = new ConcurrentHashMap<>(64); + + private final Map cachedParsers = new ConcurrentHashMap<>(64); + + + @Override + public void setEmbeddedValueResolver(StringValueResolver resolver) { + this.embeddedValueResolver = resolver; + } + + + @Override + public void addPrinter(Printer printer) { + Class fieldType = getFieldType(printer, Printer.class); + addConverter(new PrinterConverter(fieldType, printer, this)); + } + + @Override + public void addParser(Parser parser) { + Class fieldType = getFieldType(parser, Parser.class); + addConverter(new ParserConverter(fieldType, parser, this)); + } + + @Override + public void addFormatter(Formatter formatter) { + addFormatterForFieldType(getFieldType(formatter), formatter); + } + + @Override + public void addFormatterForFieldType(Class fieldType, Formatter formatter) { + addConverter(new PrinterConverter(fieldType, formatter, this)); + addConverter(new ParserConverter(fieldType, formatter, this)); + } + + @Override + public void addFormatterForFieldType(Class fieldType, Printer printer, Parser parser) { + addConverter(new PrinterConverter(fieldType, printer, this)); + addConverter(new ParserConverter(fieldType, parser, this)); + } + + @Override + public void addFormatterForFieldAnnotation(AnnotationFormatterFactory annotationFormatterFactory) { + Class annotationType = getAnnotationType(annotationFormatterFactory); + if (this.embeddedValueResolver != null && annotationFormatterFactory instanceof EmbeddedValueResolverAware) { + ((EmbeddedValueResolverAware) annotationFormatterFactory).setEmbeddedValueResolver(this.embeddedValueResolver); + } + Set> fieldTypes = annotationFormatterFactory.getFieldTypes(); + for (Class fieldType : fieldTypes) { + addConverter(new AnnotationPrinterConverter(annotationType, annotationFormatterFactory, fieldType)); + addConverter(new AnnotationParserConverter(annotationType, annotationFormatterFactory, fieldType)); + } + } + + + static Class getFieldType(Formatter formatter) { + return getFieldType(formatter, Formatter.class); + } + + private static Class getFieldType(T instance, Class genericInterface) { + Class fieldType = GenericTypeResolver.resolveTypeArgument(instance.getClass(), genericInterface); + if (fieldType == null && instance instanceof DecoratingProxy) { + fieldType = GenericTypeResolver.resolveTypeArgument( + ((DecoratingProxy) instance).getDecoratedClass(), genericInterface); + } + Assert.notNull(fieldType, () -> "Unable to extract the parameterized field type from " + + ClassUtils.getShortName(genericInterface) + " [" + instance.getClass().getName() + + "]; does the class parameterize the generic type?"); + return fieldType; + } + + @SuppressWarnings("unchecked") + static Class getAnnotationType(AnnotationFormatterFactory factory) { + Class annotationType = (Class) + GenericTypeResolver.resolveTypeArgument(factory.getClass(), AnnotationFormatterFactory.class); + if (annotationType == null) { + throw new IllegalArgumentException("Unable to extract parameterized Annotation type argument from " + + "AnnotationFormatterFactory [" + factory.getClass().getName() + + "]; does the factory parameterize the generic type?"); + } + return annotationType; + } + + + private static class PrinterConverter implements GenericConverter { + + private final Class fieldType; + + private final TypeDescriptor printerObjectType; + + @SuppressWarnings("rawtypes") + private final Printer printer; + + private final ConversionService conversionService; + + public PrinterConverter(Class fieldType, Printer printer, ConversionService conversionService) { + this.fieldType = fieldType; + this.printerObjectType = TypeDescriptor.valueOf(resolvePrinterObjectType(printer)); + this.printer = printer; + this.conversionService = conversionService; + } + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(this.fieldType, String.class)); + } + + @Override + @SuppressWarnings("unchecked") + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (!sourceType.isAssignableTo(this.printerObjectType)) { + source = this.conversionService.convert(source, sourceType, this.printerObjectType); + } + if (source == null) { + return ""; + } + return this.printer.print(source, LocaleContextHolder.getLocale()); + } + + @Nullable + private Class resolvePrinterObjectType(Printer printer) { + return GenericTypeResolver.resolveTypeArgument(printer.getClass(), Printer.class); + } + + @Override + public String toString() { + return (this.fieldType.getName() + " -> " + String.class.getName() + " : " + this.printer); + } + } + + + private static class ParserConverter implements GenericConverter { + + private final Class fieldType; + + private final Parser parser; + + private final ConversionService conversionService; + + public ParserConverter(Class fieldType, Parser parser, ConversionService conversionService) { + this.fieldType = fieldType; + this.parser = parser; + this.conversionService = conversionService; + } + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(String.class, this.fieldType)); + } + + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + String text = (String) source; + if (!StringUtils.hasText(text)) { + return null; + } + Object result; + try { + result = this.parser.parse(text, LocaleContextHolder.getLocale()); + } + catch (IllegalArgumentException ex) { + throw ex; + } + catch (Throwable ex) { + throw new IllegalArgumentException("Parse attempt failed for value [" + text + "]", ex); + } + TypeDescriptor resultType = TypeDescriptor.valueOf(result.getClass()); + if (!resultType.isAssignableTo(targetType)) { + result = this.conversionService.convert(result, resultType, targetType); + } + return result; + } + + @Override + public String toString() { + return (String.class.getName() + " -> " + this.fieldType.getName() + ": " + this.parser); + } + } + + + private class AnnotationPrinterConverter implements ConditionalGenericConverter { + + private final Class annotationType; + + @SuppressWarnings("rawtypes") + private final AnnotationFormatterFactory annotationFormatterFactory; + + private final Class fieldType; + + public AnnotationPrinterConverter(Class annotationType, + AnnotationFormatterFactory annotationFormatterFactory, Class fieldType) { + + this.annotationType = annotationType; + this.annotationFormatterFactory = annotationFormatterFactory; + this.fieldType = fieldType; + } + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(this.fieldType, String.class)); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return sourceType.hasAnnotation(this.annotationType); + } + + @Override + @SuppressWarnings("unchecked") + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + Annotation ann = sourceType.getAnnotation(this.annotationType); + if (ann == null) { + throw new IllegalStateException( + "Expected [" + this.annotationType.getName() + "] to be present on " + sourceType); + } + AnnotationConverterKey converterKey = new AnnotationConverterKey(ann, sourceType.getObjectType()); + GenericConverter converter = cachedPrinters.get(converterKey); + if (converter == null) { + Printer printer = this.annotationFormatterFactory.getPrinter( + converterKey.getAnnotation(), converterKey.getFieldType()); + converter = new PrinterConverter(this.fieldType, printer, FormattingConversionService.this); + cachedPrinters.put(converterKey, converter); + } + return converter.convert(source, sourceType, targetType); + } + + @Override + public String toString() { + return ("@" + this.annotationType.getName() + " " + this.fieldType.getName() + " -> " + + String.class.getName() + ": " + this.annotationFormatterFactory); + } + } + + + private class AnnotationParserConverter implements ConditionalGenericConverter { + + private final Class annotationType; + + @SuppressWarnings("rawtypes") + private final AnnotationFormatterFactory annotationFormatterFactory; + + private final Class fieldType; + + public AnnotationParserConverter(Class annotationType, + AnnotationFormatterFactory annotationFormatterFactory, Class fieldType) { + + this.annotationType = annotationType; + this.annotationFormatterFactory = annotationFormatterFactory; + this.fieldType = fieldType; + } + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(String.class, this.fieldType)); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return targetType.hasAnnotation(this.annotationType); + } + + @Override + @SuppressWarnings("unchecked") + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + Annotation ann = targetType.getAnnotation(this.annotationType); + if (ann == null) { + throw new IllegalStateException( + "Expected [" + this.annotationType.getName() + "] to be present on " + targetType); + } + AnnotationConverterKey converterKey = new AnnotationConverterKey(ann, targetType.getObjectType()); + GenericConverter converter = cachedParsers.get(converterKey); + if (converter == null) { + Parser parser = this.annotationFormatterFactory.getParser( + converterKey.getAnnotation(), converterKey.getFieldType()); + converter = new ParserConverter(this.fieldType, parser, FormattingConversionService.this); + cachedParsers.put(converterKey, converter); + } + return converter.convert(source, sourceType, targetType); + } + + @Override + public String toString() { + return (String.class.getName() + " -> @" + this.annotationType.getName() + " " + + this.fieldType.getName() + ": " + this.annotationFormatterFactory); + } + } + + + private static class AnnotationConverterKey { + + private final Annotation annotation; + + private final Class fieldType; + + public AnnotationConverterKey(Annotation annotation, Class fieldType) { + this.annotation = annotation; + this.fieldType = fieldType; + } + + public Annotation getAnnotation() { + return this.annotation; + } + + public Class getFieldType() { + return this.fieldType; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof AnnotationConverterKey)) { + return false; + } + AnnotationConverterKey otherKey = (AnnotationConverterKey) other; + return (this.fieldType == otherKey.fieldType && this.annotation.equals(otherKey.annotation)); + } + + @Override + public int hashCode() { + return (this.fieldType.hashCode() * 29 + this.annotation.hashCode()); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/support/FormattingConversionServiceFactoryBean.java b/spring-context/src/main/java/org/springframework/format/support/FormattingConversionServiceFactoryBean.java new file mode 100644 index 0000000..5644f80 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/support/FormattingConversionServiceFactoryBean.java @@ -0,0 +1,185 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.support; + +import java.util.Set; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.EmbeddedValueResolverAware; +import org.springframework.core.convert.support.ConversionServiceFactory; +import org.springframework.format.AnnotationFormatterFactory; +import org.springframework.format.Formatter; +import org.springframework.format.FormatterRegistrar; +import org.springframework.format.FormatterRegistry; +import org.springframework.format.Parser; +import org.springframework.format.Printer; +import org.springframework.lang.Nullable; +import org.springframework.util.StringValueResolver; + +/** + * A factory providing convenient access to a {@code FormattingConversionService} + * configured with converters and formatters for common types such as numbers and + * datetimes. + * + *

    Additional converters and formatters can be registered declaratively through + * {@link #setConverters(Set)} and {@link #setFormatters(Set)}. Another option + * is to register converters and formatters in code by implementing the + * {@link FormatterRegistrar} interface. You can then configure provide the set + * of registrars to use through {@link #setFormatterRegistrars(Set)}. + * + *

    A good example for registering converters and formatters in code is + * {@code JodaTimeFormatterRegistrar}, which registers a number of + * date-related formatters and converters. For a more detailed list of cases + * see {@link #setFormatterRegistrars(Set)} + * + *

    Like all {@code FactoryBean} implementations, this class is suitable for + * use when configuring a Spring application context using Spring {@code } + * XML. When configuring the container with + * {@link org.springframework.context.annotation.Configuration @Configuration} + * classes, simply instantiate, configure and return the appropriate + * {@code FormattingConversionService} object from a + * {@link org.springframework.context.annotation.Bean @Bean} method. + * + * @author Keith Donald + * @author Juergen Hoeller + * @author Rossen Stoyanchev + * @author Chris Beams + * @since 3.0 + */ +public class FormattingConversionServiceFactoryBean + implements FactoryBean, EmbeddedValueResolverAware, InitializingBean { + + @Nullable + private Set converters; + + @Nullable + private Set formatters; + + @Nullable + private Set formatterRegistrars; + + private boolean registerDefaultFormatters = true; + + @Nullable + private StringValueResolver embeddedValueResolver; + + @Nullable + private FormattingConversionService conversionService; + + + /** + * Configure the set of custom converter objects that should be added. + * @param converters instances of any of the following: + * {@link org.springframework.core.convert.converter.Converter}, + * {@link org.springframework.core.convert.converter.ConverterFactory}, + * {@link org.springframework.core.convert.converter.GenericConverter} + */ + public void setConverters(Set converters) { + this.converters = converters; + } + + /** + * Configure the set of custom formatter objects that should be added. + * @param formatters instances of {@link Formatter} or {@link AnnotationFormatterFactory} + */ + public void setFormatters(Set formatters) { + this.formatters = formatters; + } + + /** + *

    Configure the set of FormatterRegistrars to invoke to register + * Converters and Formatters in addition to those added declaratively + * via {@link #setConverters(Set)} and {@link #setFormatters(Set)}. + *

    FormatterRegistrars are useful when registering multiple related + * converters and formatters for a formatting category, such as Date + * formatting. All types related needed to support the formatting + * category can be registered from one place. + *

    FormatterRegistrars can also be used to register Formatters + * indexed under a specific field type different from its own <T>, + * or when registering a Formatter from a Printer/Parser pair. + * @see FormatterRegistry#addFormatterForFieldType(Class, Formatter) + * @see FormatterRegistry#addFormatterForFieldType(Class, Printer, Parser) + */ + public void setFormatterRegistrars(Set formatterRegistrars) { + this.formatterRegistrars = formatterRegistrars; + } + + /** + * Indicate whether default formatters should be registered or not. + *

    By default, built-in formatters are registered. This flag can be used + * to turn that off and rely on explicitly registered formatters only. + * @see #setFormatters(Set) + * @see #setFormatterRegistrars(Set) + */ + public void setRegisterDefaultFormatters(boolean registerDefaultFormatters) { + this.registerDefaultFormatters = registerDefaultFormatters; + } + + @Override + public void setEmbeddedValueResolver(StringValueResolver embeddedValueResolver) { + this.embeddedValueResolver = embeddedValueResolver; + } + + + @Override + public void afterPropertiesSet() { + this.conversionService = new DefaultFormattingConversionService(this.embeddedValueResolver, this.registerDefaultFormatters); + ConversionServiceFactory.registerConverters(this.converters, this.conversionService); + registerFormatters(this.conversionService); + } + + private void registerFormatters(FormattingConversionService conversionService) { + if (this.formatters != null) { + for (Object formatter : this.formatters) { + if (formatter instanceof Formatter) { + conversionService.addFormatter((Formatter) formatter); + } + else if (formatter instanceof AnnotationFormatterFactory) { + conversionService.addFormatterForFieldAnnotation((AnnotationFormatterFactory) formatter); + } + else { + throw new IllegalArgumentException( + "Custom formatters must be implementations of Formatter or AnnotationFormatterFactory"); + } + } + } + if (this.formatterRegistrars != null) { + for (FormatterRegistrar registrar : this.formatterRegistrars) { + registrar.registerFormatters(conversionService); + } + } + } + + + @Override + @Nullable + public FormattingConversionService getObject() { + return this.conversionService; + } + + @Override + public Class getObjectType() { + return FormattingConversionService.class; + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/support/package-info.java b/spring-context/src/main/java/org/springframework/format/support/package-info.java new file mode 100644 index 0000000..27db8d5 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/support/package-info.java @@ -0,0 +1,10 @@ +/** + * Support classes for the formatting package, + * providing common implementations as well as adapters. + */ +@NonNullApi +@NonNullFields +package org.springframework.format.support; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/InstrumentationLoadTimeWeaver.java b/spring-context/src/main/java/org/springframework/instrument/classloading/InstrumentationLoadTimeWeaver.java new file mode 100644 index 0000000..47bedbc --- /dev/null +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/InstrumentationLoadTimeWeaver.java @@ -0,0 +1,202 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.instrument.classloading; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.lang.instrument.Instrumentation; +import java.security.ProtectionDomain; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.instrument.InstrumentationSavingAgent; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * {@link LoadTimeWeaver} relying on VM {@link Instrumentation}. + * + *

    Start the JVM specifying the Java agent to be used — for example, as + * follows where spring-instrument-{version}.jar is a JAR file + * containing the {@link InstrumentationSavingAgent} class shipped with Spring + * and where {version} is the release version of the Spring + * Framework (e.g., {@code 5.1.5.RELEASE}). + * + *

    -javaagent:path/to/spring-instrument-{version}.jar + * + *

    In Eclipse, for example, add something similar to the following to the + * JVM arguments for the Eclipse "Run configuration": + * + *

    -javaagent:${project_loc}/lib/spring-instrument-{version}.jar + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + * @see InstrumentationSavingAgent + */ +public class InstrumentationLoadTimeWeaver implements LoadTimeWeaver { + + private static final boolean AGENT_CLASS_PRESENT = ClassUtils.isPresent( + "org.springframework.instrument.InstrumentationSavingAgent", + InstrumentationLoadTimeWeaver.class.getClassLoader()); + + + @Nullable + private final ClassLoader classLoader; + + @Nullable + private final Instrumentation instrumentation; + + private final List transformers = new ArrayList<>(4); + + + /** + * Create a new InstrumentationLoadTimeWeaver for the default ClassLoader. + */ + public InstrumentationLoadTimeWeaver() { + this(ClassUtils.getDefaultClassLoader()); + } + + /** + * Create a new InstrumentationLoadTimeWeaver for the given ClassLoader. + * @param classLoader the ClassLoader that registered transformers are supposed to apply to + */ + public InstrumentationLoadTimeWeaver(@Nullable ClassLoader classLoader) { + this.classLoader = classLoader; + this.instrumentation = getInstrumentation(); + } + + + @Override + public void addTransformer(ClassFileTransformer transformer) { + Assert.notNull(transformer, "Transformer must not be null"); + FilteringClassFileTransformer actualTransformer = + new FilteringClassFileTransformer(transformer, this.classLoader); + synchronized (this.transformers) { + Assert.state(this.instrumentation != null, + "Must start with Java agent to use InstrumentationLoadTimeWeaver. See Spring documentation."); + this.instrumentation.addTransformer(actualTransformer); + this.transformers.add(actualTransformer); + } + } + + /** + * We have the ability to weave the current class loader when starting the + * JVM in this way, so the instrumentable class loader will always be the + * current loader. + */ + @Override + public ClassLoader getInstrumentableClassLoader() { + Assert.state(this.classLoader != null, "No ClassLoader available"); + return this.classLoader; + } + + /** + * This implementation always returns a {@link SimpleThrowawayClassLoader}. + */ + @Override + public ClassLoader getThrowawayClassLoader() { + return new SimpleThrowawayClassLoader(getInstrumentableClassLoader()); + } + + /** + * Remove all registered transformers, in inverse order of registration. + */ + public void removeTransformers() { + synchronized (this.transformers) { + if (this.instrumentation != null && !this.transformers.isEmpty()) { + for (int i = this.transformers.size() - 1; i >= 0; i--) { + this.instrumentation.removeTransformer(this.transformers.get(i)); + } + this.transformers.clear(); + } + } + } + + + /** + * Check whether an Instrumentation instance is available for the current VM. + * @see #getInstrumentation() + */ + public static boolean isInstrumentationAvailable() { + return (getInstrumentation() != null); + } + + /** + * Obtain the Instrumentation instance for the current VM, if available. + * @return the Instrumentation instance, or {@code null} if none found + * @see #isInstrumentationAvailable() + */ + @Nullable + private static Instrumentation getInstrumentation() { + if (AGENT_CLASS_PRESENT) { + return InstrumentationAccessor.getInstrumentation(); + } + else { + return null; + } + } + + + /** + * Inner class to avoid InstrumentationSavingAgent dependency. + */ + private static class InstrumentationAccessor { + + public static Instrumentation getInstrumentation() { + return InstrumentationSavingAgent.getInstrumentation(); + } + } + + + /** + * Decorator that only applies the given target transformer to a specific ClassLoader. + */ + private static class FilteringClassFileTransformer implements ClassFileTransformer { + + private final ClassFileTransformer targetTransformer; + + @Nullable + private final ClassLoader targetClassLoader; + + public FilteringClassFileTransformer( + ClassFileTransformer targetTransformer, @Nullable ClassLoader targetClassLoader) { + + this.targetTransformer = targetTransformer; + this.targetClassLoader = targetClassLoader; + } + + @Override + @Nullable + public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, + ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { + + if (this.targetClassLoader != loader) { + return null; + } + return this.targetTransformer.transform( + loader, className, classBeingRedefined, protectionDomain, classfileBuffer); + } + + @Override + public String toString() { + return "FilteringClassFileTransformer for: " + this.targetTransformer.toString(); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/LoadTimeWeaver.java b/spring-context/src/main/java/org/springframework/instrument/classloading/LoadTimeWeaver.java new file mode 100644 index 0000000..782a4a3 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/LoadTimeWeaver.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.instrument.classloading; + +import java.lang.instrument.ClassFileTransformer; + +/** + * Defines the contract for adding one or more + * {@link ClassFileTransformer ClassFileTransformers} to a {@link ClassLoader}. + * + *

    Implementations may operate on the current context {@code ClassLoader} + * or expose their own instrumentable {@code ClassLoader}. + * + * @author Rod Johnson + * @author Costin Leau + * @since 2.0 + * @see java.lang.instrument.ClassFileTransformer + */ +public interface LoadTimeWeaver { + + /** + * Add a {@code ClassFileTransformer} to be applied by this + * {@code LoadTimeWeaver}. + * @param transformer the {@code ClassFileTransformer} to add + */ + void addTransformer(ClassFileTransformer transformer); + + /** + * Return a {@code ClassLoader} that supports instrumentation + * through AspectJ-style load-time weaving based on user-defined + * {@link ClassFileTransformer ClassFileTransformers}. + *

    May be the current {@code ClassLoader}, or a {@code ClassLoader} + * created by this {@link LoadTimeWeaver} instance. + * @return the {@code ClassLoader} which will expose + * instrumented classes according to the registered transformers + */ + ClassLoader getInstrumentableClassLoader(); + + /** + * Return a throwaway {@code ClassLoader}, enabling classes to be + * loaded and inspected without affecting the parent {@code ClassLoader}. + *

    Should not return the same instance of the {@link ClassLoader} + * returned from an invocation of {@link #getInstrumentableClassLoader()}. + * @return a temporary throwaway {@code ClassLoader}; should return + * a new instance for each call, with no existing state + */ + ClassLoader getThrowawayClassLoader(); + +} diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/ReflectiveLoadTimeWeaver.java b/spring-context/src/main/java/org/springframework/instrument/classloading/ReflectiveLoadTimeWeaver.java new file mode 100644 index 0000000..6830b89 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/ReflectiveLoadTimeWeaver.java @@ -0,0 +1,148 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.instrument.classloading; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.reflect.Method; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.DecoratingClassLoader; +import org.springframework.core.OverridingClassLoader; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * {@link LoadTimeWeaver} which uses reflection to delegate to an underlying ClassLoader + * with well-known transformation hooks. The underlying ClassLoader is expected to + * support the following weaving methods (as defined in the {@link LoadTimeWeaver} + * interface): + *

      + *
    • {@code public void addTransformer(java.lang.instrument.ClassFileTransformer)}: + * for registering the given ClassFileTransformer on this ClassLoader + *
    • {@code public ClassLoader getThrowawayClassLoader()}: + * for obtaining a throwaway class loader for this ClassLoader (optional; + * ReflectiveLoadTimeWeaver will fall back to a SimpleThrowawayClassLoader if + * that method isn't available) + *
    + * + *

    Please note that the above methods must reside in a class that is + * publicly accessible, although the class itself does not have to be visible + * to the application's class loader. + * + *

    The reflective nature of this LoadTimeWeaver is particularly useful when the + * underlying ClassLoader implementation is loaded in a different class loader itself + * (such as the application server's class loader which is not visible to the + * web application). There is no direct API dependency between this LoadTimeWeaver + * adapter and the underlying ClassLoader, just a 'loose' method contract. + * + *

    This is the LoadTimeWeaver to use e.g. with the Resin application server + * version 3.1+. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 2.0 + * @see #addTransformer(java.lang.instrument.ClassFileTransformer) + * @see #getThrowawayClassLoader() + * @see SimpleThrowawayClassLoader + */ +public class ReflectiveLoadTimeWeaver implements LoadTimeWeaver { + + private static final String ADD_TRANSFORMER_METHOD_NAME = "addTransformer"; + + private static final String GET_THROWAWAY_CLASS_LOADER_METHOD_NAME = "getThrowawayClassLoader"; + + private static final Log logger = LogFactory.getLog(ReflectiveLoadTimeWeaver.class); + + + private final ClassLoader classLoader; + + private final Method addTransformerMethod; + + @Nullable + private final Method getThrowawayClassLoaderMethod; + + + /** + * Create a new ReflectiveLoadTimeWeaver for the current context class + * loader, which needs to support the required weaving methods. + */ + public ReflectiveLoadTimeWeaver() { + this(ClassUtils.getDefaultClassLoader()); + } + + /** + * Create a new SimpleLoadTimeWeaver for the given class loader. + * @param classLoader the {@code ClassLoader} to delegate to for + * weaving (must support the required weaving methods). + * @throws IllegalStateException if the supplied {@code ClassLoader} + * does not support the required weaving methods + */ + public ReflectiveLoadTimeWeaver(@Nullable ClassLoader classLoader) { + Assert.notNull(classLoader, "ClassLoader must not be null"); + this.classLoader = classLoader; + + Method addTransformerMethod = ClassUtils.getMethodIfAvailable( + this.classLoader.getClass(), ADD_TRANSFORMER_METHOD_NAME, ClassFileTransformer.class); + if (addTransformerMethod == null) { + throw new IllegalStateException( + "ClassLoader [" + classLoader.getClass().getName() + "] does NOT provide an " + + "'addTransformer(ClassFileTransformer)' method."); + } + this.addTransformerMethod = addTransformerMethod; + + Method getThrowawayClassLoaderMethod = ClassUtils.getMethodIfAvailable( + this.classLoader.getClass(), GET_THROWAWAY_CLASS_LOADER_METHOD_NAME); + // getThrowawayClassLoader method is optional + if (getThrowawayClassLoaderMethod == null) { + if (logger.isDebugEnabled()) { + logger.debug("The ClassLoader [" + classLoader.getClass().getName() + "] does NOT provide a " + + "'getThrowawayClassLoader()' method; SimpleThrowawayClassLoader will be used instead."); + } + } + this.getThrowawayClassLoaderMethod = getThrowawayClassLoaderMethod; + } + + + @Override + public void addTransformer(ClassFileTransformer transformer) { + Assert.notNull(transformer, "Transformer must not be null"); + ReflectionUtils.invokeMethod(this.addTransformerMethod, this.classLoader, transformer); + } + + @Override + public ClassLoader getInstrumentableClassLoader() { + return this.classLoader; + } + + @Override + public ClassLoader getThrowawayClassLoader() { + if (this.getThrowawayClassLoaderMethod != null) { + ClassLoader target = (ClassLoader) + ReflectionUtils.invokeMethod(this.getThrowawayClassLoaderMethod, this.classLoader); + return (target instanceof DecoratingClassLoader ? target : + new OverridingClassLoader(this.classLoader, target)); + } + else { + return new SimpleThrowawayClassLoader(this.classLoader); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/ResourceOverridingShadowingClassLoader.java b/spring-context/src/main/java/org/springframework/instrument/classloading/ResourceOverridingShadowingClassLoader.java new file mode 100644 index 0000000..4e68ab9 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/ResourceOverridingShadowingClassLoader.java @@ -0,0 +1,131 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.instrument.classloading; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Subclass of ShadowingClassLoader that overrides attempts to + * locate certain files. + * + * @author Rod Johnson + * @author Adrian Colyer + * @since 2.0 + */ +public class ResourceOverridingShadowingClassLoader extends ShadowingClassLoader { + + private static final Enumeration EMPTY_URL_ENUMERATION = new Enumeration() { + @Override + public boolean hasMoreElements() { + return false; + } + @Override + public URL nextElement() { + throw new UnsupportedOperationException("Should not be called. I am empty."); + } + }; + + + /** + * Key is asked for value: value is actual value. + */ + private Map overrides = new HashMap<>(); + + + /** + * Create a new ResourceOverridingShadowingClassLoader, + * decorating the given ClassLoader. + * @param enclosingClassLoader the ClassLoader to decorate + */ + public ResourceOverridingShadowingClassLoader(ClassLoader enclosingClassLoader) { + super(enclosingClassLoader); + } + + + /** + * Return the resource (if any) at the new path + * on an attempt to locate a resource at the old path. + * @param oldPath the path requested + * @param newPath the actual path to be looked up + */ + public void override(String oldPath, String newPath) { + this.overrides.put(oldPath, newPath); + } + + /** + * Ensure that a resource with the given path is not found. + * @param oldPath the path of the resource to hide even if + * it exists in the parent ClassLoader + */ + public void suppress(String oldPath) { + this.overrides.put(oldPath, null); + } + + /** + * Copy all overrides from the given ClassLoader. + * @param other the other ClassLoader to copy from + */ + public void copyOverrides(ResourceOverridingShadowingClassLoader other) { + Assert.notNull(other, "Other ClassLoader must not be null"); + this.overrides.putAll(other.overrides); + } + + + @Override + public URL getResource(String requestedPath) { + if (this.overrides.containsKey(requestedPath)) { + String overriddenPath = this.overrides.get(requestedPath); + return (overriddenPath != null ? super.getResource(overriddenPath) : null); + } + else { + return super.getResource(requestedPath); + } + } + + @Override + @Nullable + public InputStream getResourceAsStream(String requestedPath) { + if (this.overrides.containsKey(requestedPath)) { + String overriddenPath = this.overrides.get(requestedPath); + return (overriddenPath != null ? super.getResourceAsStream(overriddenPath) : null); + } + else { + return super.getResourceAsStream(requestedPath); + } + } + + @Override + public Enumeration getResources(String requestedPath) throws IOException { + if (this.overrides.containsKey(requestedPath)) { + String overriddenLocation = this.overrides.get(requestedPath); + return (overriddenLocation != null ? + super.getResources(overriddenLocation) : EMPTY_URL_ENUMERATION); + } + else { + return super.getResources(requestedPath); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/ShadowingClassLoader.java b/spring-context/src/main/java/org/springframework/instrument/classloading/ShadowingClassLoader.java new file mode 100644 index 0000000..0512d51 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/ShadowingClassLoader.java @@ -0,0 +1,204 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.instrument.classloading; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.core.DecoratingClassLoader; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StringUtils; + +/** + * ClassLoader decorator that shadows an enclosing ClassLoader, + * applying registered transformers to all affected classes. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Costin Leau + * @since 2.0 + * @see #addTransformer + * @see org.springframework.core.OverridingClassLoader + */ +public class ShadowingClassLoader extends DecoratingClassLoader { + + /** Packages that are excluded by default. */ + public static final String[] DEFAULT_EXCLUDED_PACKAGES = + new String[] {"java.", "javax.", "jdk.", "sun.", "oracle.", "com.sun.", "com.ibm.", "COM.ibm.", + "org.w3c.", "org.xml.", "org.dom4j.", "org.eclipse", "org.aspectj.", "net.sf.cglib", + "org.springframework.cglib", "org.apache.xerces.", "org.apache.commons.logging."}; + + + private final ClassLoader enclosingClassLoader; + + private final List classFileTransformers = new ArrayList<>(1); + + private final Map> classCache = new HashMap<>(); + + + /** + * Create a new ShadowingClassLoader, decorating the given ClassLoader, + * applying {@link #DEFAULT_EXCLUDED_PACKAGES}. + * @param enclosingClassLoader the ClassLoader to decorate + * @see #ShadowingClassLoader(ClassLoader, boolean) + */ + public ShadowingClassLoader(ClassLoader enclosingClassLoader) { + this(enclosingClassLoader, true); + } + + /** + * Create a new ShadowingClassLoader, decorating the given ClassLoader. + * @param enclosingClassLoader the ClassLoader to decorate + * @param defaultExcludes whether to apply {@link #DEFAULT_EXCLUDED_PACKAGES} + * @since 4.3.8 + */ + public ShadowingClassLoader(ClassLoader enclosingClassLoader, boolean defaultExcludes) { + Assert.notNull(enclosingClassLoader, "Enclosing ClassLoader must not be null"); + this.enclosingClassLoader = enclosingClassLoader; + if (defaultExcludes) { + for (String excludedPackage : DEFAULT_EXCLUDED_PACKAGES) { + excludePackage(excludedPackage); + } + } + } + + + /** + * Add the given ClassFileTransformer to the list of transformers that this + * ClassLoader will apply. + * @param transformer the ClassFileTransformer + */ + public void addTransformer(ClassFileTransformer transformer) { + Assert.notNull(transformer, "Transformer must not be null"); + this.classFileTransformers.add(transformer); + } + + /** + * Copy all ClassFileTransformers from the given ClassLoader to the list of + * transformers that this ClassLoader will apply. + * @param other the ClassLoader to copy from + */ + public void copyTransformers(ShadowingClassLoader other) { + Assert.notNull(other, "Other ClassLoader must not be null"); + this.classFileTransformers.addAll(other.classFileTransformers); + } + + + @Override + public Class loadClass(String name) throws ClassNotFoundException { + if (shouldShadow(name)) { + Class cls = this.classCache.get(name); + if (cls != null) { + return cls; + } + return doLoadClass(name); + } + else { + return this.enclosingClassLoader.loadClass(name); + } + } + + /** + * Determine whether the given class should be excluded from shadowing. + * @param className the name of the class + * @return whether the specified class should be shadowed + */ + private boolean shouldShadow(String className) { + return (!className.equals(getClass().getName()) && !className.endsWith("ShadowingClassLoader") && + isEligibleForShadowing(className)); + } + + /** + * Determine whether the specified class is eligible for shadowing + * by this class loader. + * @param className the class name to check + * @return whether the specified class is eligible + * @see #isExcluded + */ + protected boolean isEligibleForShadowing(String className) { + return !isExcluded(className); + } + + + private Class doLoadClass(String name) throws ClassNotFoundException { + String internalName = StringUtils.replace(name, ".", "/") + ".class"; + InputStream is = this.enclosingClassLoader.getResourceAsStream(internalName); + if (is == null) { + throw new ClassNotFoundException(name); + } + try { + byte[] bytes = FileCopyUtils.copyToByteArray(is); + bytes = applyTransformers(name, bytes); + Class cls = defineClass(name, bytes, 0, bytes.length); + // Additional check for defining the package, if not defined yet. + if (cls.getPackage() == null) { + int packageSeparator = name.lastIndexOf('.'); + if (packageSeparator != -1) { + String packageName = name.substring(0, packageSeparator); + definePackage(packageName, null, null, null, null, null, null, null); + } + } + this.classCache.put(name, cls); + return cls; + } + catch (IOException ex) { + throw new ClassNotFoundException("Cannot load resource for class [" + name + "]", ex); + } + } + + private byte[] applyTransformers(String name, byte[] bytes) { + String internalName = StringUtils.replace(name, ".", "/"); + try { + for (ClassFileTransformer transformer : this.classFileTransformers) { + byte[] transformed = transformer.transform(this, internalName, null, null, bytes); + bytes = (transformed != null ? transformed : bytes); + } + return bytes; + } + catch (IllegalClassFormatException ex) { + throw new IllegalStateException(ex); + } + } + + + @Override + public URL getResource(String name) { + return this.enclosingClassLoader.getResource(name); + } + + @Override + @Nullable + public InputStream getResourceAsStream(String name) { + return this.enclosingClassLoader.getResourceAsStream(name); + } + + @Override + public Enumeration getResources(String name) throws IOException { + return this.enclosingClassLoader.getResources(name); + } + +} diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/SimpleInstrumentableClassLoader.java b/spring-context/src/main/java/org/springframework/instrument/classloading/SimpleInstrumentableClassLoader.java new file mode 100644 index 0000000..b61cceb --- /dev/null +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/SimpleInstrumentableClassLoader.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.instrument.classloading; + +import java.lang.instrument.ClassFileTransformer; + +import org.springframework.core.OverridingClassLoader; +import org.springframework.lang.Nullable; + +/** + * Simplistic implementation of an instrumentable {@code ClassLoader}. + * + *

    Usable in tests and standalone environments. + * + * @author Rod Johnson + * @author Costin Leau + * @since 2.0 + */ +public class SimpleInstrumentableClassLoader extends OverridingClassLoader { + + static { + ClassLoader.registerAsParallelCapable(); + } + + + private final WeavingTransformer weavingTransformer; + + + /** + * Create a new SimpleInstrumentableClassLoader for the given ClassLoader. + * @param parent the ClassLoader to build an instrumentable ClassLoader for + */ + public SimpleInstrumentableClassLoader(@Nullable ClassLoader parent) { + super(parent); + this.weavingTransformer = new WeavingTransformer(parent); + } + + + /** + * Add a {@link ClassFileTransformer} to be applied by this ClassLoader. + * @param transformer the {@link ClassFileTransformer} to register + */ + public void addTransformer(ClassFileTransformer transformer) { + this.weavingTransformer.addTransformer(transformer); + } + + + @Override + protected byte[] transformIfNecessary(String name, byte[] bytes) { + return this.weavingTransformer.transformIfNecessary(name, bytes); + } + +} diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/SimpleLoadTimeWeaver.java b/spring-context/src/main/java/org/springframework/instrument/classloading/SimpleLoadTimeWeaver.java new file mode 100644 index 0000000..385b311 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/SimpleLoadTimeWeaver.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.instrument.classloading; + +import java.lang.instrument.ClassFileTransformer; + +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * {@code LoadTimeWeaver} that builds and exposes a + * {@link SimpleInstrumentableClassLoader}. + * + *

    Mainly intended for testing environments, where it is sufficient to + * perform all class transformation on a newly created + * {@code ClassLoader} instance. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + * @see #getInstrumentableClassLoader() + * @see SimpleInstrumentableClassLoader + * @see ReflectiveLoadTimeWeaver + */ +public class SimpleLoadTimeWeaver implements LoadTimeWeaver { + + private final SimpleInstrumentableClassLoader classLoader; + + + /** + * Create a new {@code SimpleLoadTimeWeaver} for the current context + * {@code ClassLoader}. + * @see SimpleInstrumentableClassLoader + */ + public SimpleLoadTimeWeaver() { + this.classLoader = new SimpleInstrumentableClassLoader(ClassUtils.getDefaultClassLoader()); + } + + /** + * Create a new {@code SimpleLoadTimeWeaver} for the given + * {@code ClassLoader}. + * @param classLoader the {@code ClassLoader} to build a simple + * instrumentable {@code ClassLoader} on top of + */ + public SimpleLoadTimeWeaver(SimpleInstrumentableClassLoader classLoader) { + Assert.notNull(classLoader, "ClassLoader must not be null"); + this.classLoader = classLoader; + } + + + @Override + public void addTransformer(ClassFileTransformer transformer) { + this.classLoader.addTransformer(transformer); + } + + @Override + public ClassLoader getInstrumentableClassLoader() { + return this.classLoader; + } + + /** + * This implementation builds a {@link SimpleThrowawayClassLoader}. + */ + @Override + public ClassLoader getThrowawayClassLoader() { + return new SimpleThrowawayClassLoader(getInstrumentableClassLoader()); + } + +} diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/SimpleThrowawayClassLoader.java b/spring-context/src/main/java/org/springframework/instrument/classloading/SimpleThrowawayClassLoader.java new file mode 100644 index 0000000..a4329ae --- /dev/null +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/SimpleThrowawayClassLoader.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.instrument.classloading; + +import org.springframework.core.OverridingClassLoader; +import org.springframework.lang.Nullable; + +/** + * ClassLoader that can be used to load classes without bringing them + * into the parent loader. Intended to support JPA "temp class loader" + * requirement, but not JPA-specific. + * + * @author Rod Johnson + * @since 2.0 + */ +public class SimpleThrowawayClassLoader extends OverridingClassLoader { + + static { + ClassLoader.registerAsParallelCapable(); + } + + + /** + * Create a new SimpleThrowawayClassLoader for the given ClassLoader. + * @param parent the ClassLoader to build a throwaway ClassLoader for + */ + public SimpleThrowawayClassLoader(@Nullable ClassLoader parent) { + super(parent); + } + +} diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/WeavingTransformer.java b/spring-context/src/main/java/org/springframework/instrument/classloading/WeavingTransformer.java new file mode 100644 index 0000000..03db47b --- /dev/null +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/WeavingTransformer.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.instrument.classloading; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.security.ProtectionDomain; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * ClassFileTransformer-based weaver, allowing for a list of transformers to be + * applied on a class byte array. Normally used inside class loaders. + * + *

    Note: This class is deliberately implemented for minimal external dependencies, + * since it is included in weaver jars (to be deployed into application servers). + * + * @author Rod Johnson + * @author Costin Leau + * @author Juergen Hoeller + * @since 2.0 + */ +public class WeavingTransformer { + + @Nullable + private final ClassLoader classLoader; + + private final List transformers = new ArrayList<>(); + + + /** + * Create a new WeavingTransformer for the given class loader. + * @param classLoader the ClassLoader to build a transformer for + */ + public WeavingTransformer(@Nullable ClassLoader classLoader) { + this.classLoader = classLoader; + } + + + /** + * Add a class file transformer to be applied by this weaver. + * @param transformer the class file transformer to register + */ + public void addTransformer(ClassFileTransformer transformer) { + Assert.notNull(transformer, "Transformer must not be null"); + this.transformers.add(transformer); + } + + + /** + * Apply transformation on a given class byte definition. + * The method will always return a non-null byte array (if no transformation has taken place + * the array content will be identical to the original one). + * @param className the full qualified name of the class in dot format (i.e. some.package.SomeClass) + * @param bytes class byte definition + * @return (possibly transformed) class byte definition + */ + public byte[] transformIfNecessary(String className, byte[] bytes) { + String internalName = StringUtils.replace(className, ".", "/"); + return transformIfNecessary(className, internalName, bytes, null); + } + + /** + * Apply transformation on a given class byte definition. + * The method will always return a non-null byte array (if no transformation has taken place + * the array content will be identical to the original one). + * @param className the full qualified name of the class in dot format (i.e. some.package.SomeClass) + * @param internalName class name internal name in / format (i.e. some/package/SomeClass) + * @param bytes class byte definition + * @param pd protection domain to be used (can be null) + * @return (possibly transformed) class byte definition + */ + public byte[] transformIfNecessary(String className, String internalName, byte[] bytes, @Nullable ProtectionDomain pd) { + byte[] result = bytes; + for (ClassFileTransformer cft : this.transformers) { + try { + byte[] transformed = cft.transform(this.classLoader, internalName, null, pd, result); + if (transformed != null) { + result = transformed; + } + } + catch (IllegalClassFormatException ex) { + throw new IllegalStateException("Class file transformation failed", ex); + } + } + return result; + } + +} diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/glassfish/GlassFishLoadTimeWeaver.java b/spring-context/src/main/java/org/springframework/instrument/classloading/glassfish/GlassFishLoadTimeWeaver.java new file mode 100644 index 0000000..f2b45c6 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/glassfish/GlassFishLoadTimeWeaver.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.instrument.classloading.glassfish; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import org.springframework.core.OverridingClassLoader; +import org.springframework.instrument.classloading.LoadTimeWeaver; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * {@link LoadTimeWeaver} implementation for GlassFish's + * {@code org.glassfish.api.deployment.InstrumentableClassLoader InstrumentableClassLoader}. + * + *

    As of Spring Framework 5.0, this weaver supports GlassFish 4+. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 2.0.1 + */ +public class GlassFishLoadTimeWeaver implements LoadTimeWeaver { + + private static final String INSTRUMENTABLE_LOADER_CLASS_NAME = + "org.glassfish.api.deployment.InstrumentableClassLoader"; + + + private final ClassLoader classLoader; + + private final Method addTransformerMethod; + + private final Method copyMethod; + + + /** + * Create a new instance of the {@link GlassFishLoadTimeWeaver} class using + * the default {@link ClassLoader class loader}. + * @see org.springframework.util.ClassUtils#getDefaultClassLoader() + */ + public GlassFishLoadTimeWeaver() { + this(ClassUtils.getDefaultClassLoader()); + } + + /** + * Create a new instance of the {@link GlassFishLoadTimeWeaver} class using + * the supplied {@link ClassLoader}. + * @param classLoader the {@code ClassLoader} to delegate to for weaving + */ + public GlassFishLoadTimeWeaver(@Nullable ClassLoader classLoader) { + Assert.notNull(classLoader, "ClassLoader must not be null"); + + Class instrumentableLoaderClass; + try { + instrumentableLoaderClass = classLoader.loadClass(INSTRUMENTABLE_LOADER_CLASS_NAME); + this.addTransformerMethod = instrumentableLoaderClass.getMethod("addTransformer", ClassFileTransformer.class); + this.copyMethod = instrumentableLoaderClass.getMethod("copy"); + } + catch (Throwable ex) { + throw new IllegalStateException( + "Could not initialize GlassFishLoadTimeWeaver because GlassFish API classes are not available", ex); + } + + ClassLoader clazzLoader = null; + // Detect transformation-aware ClassLoader by traversing the hierarchy + // (as in GlassFish, Spring can be loaded by the WebappClassLoader). + for (ClassLoader cl = classLoader; cl != null && clazzLoader == null; cl = cl.getParent()) { + if (instrumentableLoaderClass.isInstance(cl)) { + clazzLoader = cl; + } + } + + if (clazzLoader == null) { + throw new IllegalArgumentException(classLoader + " and its parents are not suitable ClassLoaders: A [" + + instrumentableLoaderClass.getName() + "] implementation is required."); + } + + this.classLoader = clazzLoader; + } + + + @Override + public void addTransformer(ClassFileTransformer transformer) { + try { + this.addTransformerMethod.invoke(this.classLoader, transformer); + } + catch (InvocationTargetException ex) { + throw new IllegalStateException("GlassFish addTransformer method threw exception", ex.getCause()); + } + catch (Throwable ex) { + throw new IllegalStateException("Could not invoke GlassFish addTransformer method", ex); + } + } + + @Override + public ClassLoader getInstrumentableClassLoader() { + return this.classLoader; + } + + @Override + public ClassLoader getThrowawayClassLoader() { + try { + return new OverridingClassLoader(this.classLoader, (ClassLoader) this.copyMethod.invoke(this.classLoader)); + } + catch (InvocationTargetException ex) { + throw new IllegalStateException("GlassFish copy method threw exception", ex.getCause()); + } + catch (Throwable ex) { + throw new IllegalStateException("Could not invoke GlassFish copy method", ex); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/glassfish/package-info.java b/spring-context/src/main/java/org/springframework/instrument/classloading/glassfish/package-info.java new file mode 100644 index 0000000..7ab813f --- /dev/null +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/glassfish/package-info.java @@ -0,0 +1,9 @@ +/** + * Support for class instrumentation on GlassFish. + */ +@NonNullApi +@NonNullFields +package org.springframework.instrument.classloading.glassfish; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/jboss/JBossLoadTimeWeaver.java b/spring-context/src/main/java/org/springframework/instrument/classloading/jboss/JBossLoadTimeWeaver.java new file mode 100644 index 0000000..cef5e33 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/jboss/JBossLoadTimeWeaver.java @@ -0,0 +1,137 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.instrument.classloading.jboss; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import org.springframework.instrument.classloading.LoadTimeWeaver; +import org.springframework.instrument.classloading.SimpleThrowawayClassLoader; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * {@link LoadTimeWeaver} implementation for JBoss's instrumentable ClassLoader. + * Thanks to Ales Justin and Marius Bogoevici for the initial prototype. + * + *

    As of Spring Framework 5.0, this weaver supports WildFly 8+. + * As of Spring Framework 5.1.5, it also supports WildFly 13+. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 3.0 + */ +public class JBossLoadTimeWeaver implements LoadTimeWeaver { + + private static final String DELEGATING_TRANSFORMER_CLASS_NAME = + "org.jboss.as.server.deployment.module.DelegatingClassFileTransformer"; + + private static final String WRAPPER_TRANSFORMER_CLASS_NAME = + "org.jboss.modules.JLIClassTransformer"; + + + private final ClassLoader classLoader; + + private final Object delegatingTransformer; + + private final Method addTransformer; + + + /** + * Create a new instance of the {@link JBossLoadTimeWeaver} class using + * the default {@link ClassLoader class loader}. + * @see org.springframework.util.ClassUtils#getDefaultClassLoader() + */ + public JBossLoadTimeWeaver() { + this(ClassUtils.getDefaultClassLoader()); + } + + /** + * Create a new instance of the {@link JBossLoadTimeWeaver} class using + * the supplied {@link ClassLoader}. + * @param classLoader the {@code ClassLoader} to delegate to for weaving + */ + public JBossLoadTimeWeaver(@Nullable ClassLoader classLoader) { + Assert.notNull(classLoader, "ClassLoader must not be null"); + this.classLoader = classLoader; + + try { + Field transformer = ReflectionUtils.findField(classLoader.getClass(), "transformer"); + if (transformer == null) { + throw new IllegalArgumentException("Could not find 'transformer' field on JBoss ClassLoader: " + + classLoader.getClass().getName()); + } + transformer.setAccessible(true); + + Object suggestedTransformer = transformer.get(classLoader); + if (suggestedTransformer.getClass().getName().equals(WRAPPER_TRANSFORMER_CLASS_NAME)) { + Field wrappedTransformer = ReflectionUtils.findField(suggestedTransformer.getClass(), "transformer"); + if (wrappedTransformer == null) { + throw new IllegalArgumentException( + "Could not find 'transformer' field on JBoss JLIClassTransformer: " + + suggestedTransformer.getClass().getName()); + } + wrappedTransformer.setAccessible(true); + suggestedTransformer = wrappedTransformer.get(suggestedTransformer); + } + if (!suggestedTransformer.getClass().getName().equals(DELEGATING_TRANSFORMER_CLASS_NAME)) { + throw new IllegalStateException( + "Transformer not of the expected type DelegatingClassFileTransformer: " + + suggestedTransformer.getClass().getName()); + } + this.delegatingTransformer = suggestedTransformer; + + Method addTransformer = ReflectionUtils.findMethod(this.delegatingTransformer.getClass(), + "addTransformer", ClassFileTransformer.class); + if (addTransformer == null) { + throw new IllegalArgumentException( + "Could not find 'addTransformer' method on JBoss DelegatingClassFileTransformer: " + + this.delegatingTransformer.getClass().getName()); + } + addTransformer.setAccessible(true); + this.addTransformer = addTransformer; + } + catch (Throwable ex) { + throw new IllegalStateException("Could not initialize JBoss LoadTimeWeaver", ex); + } + } + + + @Override + public void addTransformer(ClassFileTransformer transformer) { + try { + this.addTransformer.invoke(this.delegatingTransformer, transformer); + } + catch (Throwable ex) { + throw new IllegalStateException("Could not add transformer on JBoss ClassLoader: " + this.classLoader, ex); + } + } + + @Override + public ClassLoader getInstrumentableClassLoader() { + return this.classLoader; + } + + @Override + public ClassLoader getThrowawayClassLoader() { + return new SimpleThrowawayClassLoader(getInstrumentableClassLoader()); + } + +} diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/jboss/package-info.java b/spring-context/src/main/java/org/springframework/instrument/classloading/jboss/package-info.java new file mode 100644 index 0000000..7456195 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/jboss/package-info.java @@ -0,0 +1,9 @@ +/** + * Support for class instrumentation on JBoss AS 6 and 7. + */ +@NonNullApi +@NonNullFields +package org.springframework.instrument.classloading.jboss; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/package-info.java b/spring-context/src/main/java/org/springframework/instrument/classloading/package-info.java new file mode 100644 index 0000000..82ed42c --- /dev/null +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/package-info.java @@ -0,0 +1,10 @@ +/** + * Support package for load time weaving based on class loaders, + * as required by JPA providers (but not JPA-specific). + */ +@NonNullApi +@NonNullFields +package org.springframework.instrument.classloading; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/tomcat/TomcatLoadTimeWeaver.java b/spring-context/src/main/java/org/springframework/instrument/classloading/tomcat/TomcatLoadTimeWeaver.java new file mode 100644 index 0000000..a222034 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/tomcat/TomcatLoadTimeWeaver.java @@ -0,0 +1,128 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.instrument.classloading.tomcat; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import org.springframework.core.OverridingClassLoader; +import org.springframework.instrument.classloading.LoadTimeWeaver; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * {@link org.springframework.instrument.classloading.LoadTimeWeaver} implementation + * for Tomcat's new {@code org.apache.tomcat.InstrumentableClassLoader}. + * Also capable of handling Spring's TomcatInstrumentableClassLoader when encountered. + * + * @author Juergen Hoeller + * @since 4.0 + */ +public class TomcatLoadTimeWeaver implements LoadTimeWeaver { + + private static final String INSTRUMENTABLE_LOADER_CLASS_NAME = "org.apache.tomcat.InstrumentableClassLoader"; + + + private final ClassLoader classLoader; + + private final Method addTransformerMethod; + + private final Method copyMethod; + + + /** + * Create a new instance of the {@link TomcatLoadTimeWeaver} class using + * the default {@link ClassLoader class loader}. + * @see org.springframework.util.ClassUtils#getDefaultClassLoader() + */ + public TomcatLoadTimeWeaver() { + this(ClassUtils.getDefaultClassLoader()); + } + + /** + * Create a new instance of the {@link TomcatLoadTimeWeaver} class using + * the supplied {@link ClassLoader}. + * @param classLoader the {@code ClassLoader} to delegate to for weaving + */ + public TomcatLoadTimeWeaver(@Nullable ClassLoader classLoader) { + Assert.notNull(classLoader, "ClassLoader must not be null"); + this.classLoader = classLoader; + + Class instrumentableLoaderClass; + try { + instrumentableLoaderClass = classLoader.loadClass(INSTRUMENTABLE_LOADER_CLASS_NAME); + if (!instrumentableLoaderClass.isInstance(classLoader)) { + // Could still be a custom variant of a convention-compatible ClassLoader + instrumentableLoaderClass = classLoader.getClass(); + } + } + catch (ClassNotFoundException ex) { + // We're on an earlier version of Tomcat, probably with Spring's TomcatInstrumentableClassLoader + instrumentableLoaderClass = classLoader.getClass(); + } + + try { + this.addTransformerMethod = instrumentableLoaderClass.getMethod("addTransformer", ClassFileTransformer.class); + // Check for Tomcat's new copyWithoutTransformers on InstrumentableClassLoader first + Method copyMethod = ClassUtils.getMethodIfAvailable(instrumentableLoaderClass, "copyWithoutTransformers"); + if (copyMethod == null) { + // Fallback: expecting TomcatInstrumentableClassLoader's getThrowawayClassLoader + copyMethod = instrumentableLoaderClass.getMethod("getThrowawayClassLoader"); + } + this.copyMethod = copyMethod; + } + catch (Throwable ex) { + throw new IllegalStateException( + "Could not initialize TomcatLoadTimeWeaver because Tomcat API classes are not available", ex); + } + } + + + @Override + public void addTransformer(ClassFileTransformer transformer) { + try { + this.addTransformerMethod.invoke(this.classLoader, transformer); + } + catch (InvocationTargetException ex) { + throw new IllegalStateException("Tomcat addTransformer method threw exception", ex.getCause()); + } + catch (Throwable ex) { + throw new IllegalStateException("Could not invoke Tomcat addTransformer method", ex); + } + } + + @Override + public ClassLoader getInstrumentableClassLoader() { + return this.classLoader; + } + + @Override + public ClassLoader getThrowawayClassLoader() { + try { + return new OverridingClassLoader(this.classLoader, (ClassLoader) this.copyMethod.invoke(this.classLoader)); + } + catch (InvocationTargetException ex) { + throw new IllegalStateException("Tomcat copy method threw exception", ex.getCause()); + } + catch (Throwable ex) { + throw new IllegalStateException("Could not invoke Tomcat copy method", ex); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/tomcat/package-info.java b/spring-context/src/main/java/org/springframework/instrument/classloading/tomcat/package-info.java new file mode 100644 index 0000000..c1ac847 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/tomcat/package-info.java @@ -0,0 +1,9 @@ +/** + * Support for class instrumentation on Tomcat. + */ +@NonNullApi +@NonNullFields +package org.springframework.instrument.classloading.tomcat; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/weblogic/WebLogicClassLoaderAdapter.java b/spring-context/src/main/java/org/springframework/instrument/classloading/weblogic/WebLogicClassLoaderAdapter.java new file mode 100644 index 0000000..1e4e0cd --- /dev/null +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/weblogic/WebLogicClassLoaderAdapter.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.instrument.classloading.weblogic; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import org.springframework.util.Assert; + +/** + * Reflective wrapper around a WebLogic 10 class loader. Used to + * encapsulate the classloader-specific methods (discovered and + * called through reflection) from the load-time weaver. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 2.5 + */ +class WebLogicClassLoaderAdapter { + + private static final String GENERIC_CLASS_LOADER_NAME = "weblogic.utils.classloaders.GenericClassLoader"; + + private static final String CLASS_PRE_PROCESSOR_NAME = "weblogic.utils.classloaders.ClassPreProcessor"; + + + private final ClassLoader classLoader; + + private final Class wlPreProcessorClass; + + private final Method addPreProcessorMethod; + + private final Method getClassFinderMethod; + + private final Method getParentMethod; + + private final Constructor wlGenericClassLoaderConstructor; + + + public WebLogicClassLoaderAdapter(ClassLoader classLoader) { + Class wlGenericClassLoaderClass; + try { + wlGenericClassLoaderClass = classLoader.loadClass(GENERIC_CLASS_LOADER_NAME); + this.wlPreProcessorClass = classLoader.loadClass(CLASS_PRE_PROCESSOR_NAME); + this.addPreProcessorMethod = classLoader.getClass().getMethod( + "addInstanceClassPreProcessor", this.wlPreProcessorClass); + this.getClassFinderMethod = classLoader.getClass().getMethod("getClassFinder"); + this.getParentMethod = classLoader.getClass().getMethod("getParent"); + this.wlGenericClassLoaderConstructor = wlGenericClassLoaderClass.getConstructor( + this.getClassFinderMethod.getReturnType(), ClassLoader.class); + } + catch (Throwable ex) { + throw new IllegalStateException( + "Could not initialize WebLogic LoadTimeWeaver because WebLogic 10 API classes are not available", ex); + } + + if (!wlGenericClassLoaderClass.isInstance(classLoader)) { + throw new IllegalArgumentException( + "ClassLoader must be an instance of [" + wlGenericClassLoaderClass.getName() + "]: " + classLoader); + } + this.classLoader = classLoader; + } + + + public void addTransformer(ClassFileTransformer transformer) { + Assert.notNull(transformer, "ClassFileTransformer must not be null"); + try { + InvocationHandler adapter = new WebLogicClassPreProcessorAdapter(transformer, this.classLoader); + Object adapterInstance = Proxy.newProxyInstance(this.wlPreProcessorClass.getClassLoader(), + new Class[] {this.wlPreProcessorClass}, adapter); + this.addPreProcessorMethod.invoke(this.classLoader, adapterInstance); + } + catch (InvocationTargetException ex) { + throw new IllegalStateException("WebLogic addInstanceClassPreProcessor method threw exception", ex.getCause()); + } + catch (Throwable ex) { + throw new IllegalStateException("Could not invoke WebLogic addInstanceClassPreProcessor method", ex); + } + } + + public ClassLoader getClassLoader() { + return this.classLoader; + } + + public ClassLoader getThrowawayClassLoader() { + try { + Object classFinder = this.getClassFinderMethod.invoke(this.classLoader); + Object parent = this.getParentMethod.invoke(this.classLoader); + // arguments for 'clone'-like method + return (ClassLoader) this.wlGenericClassLoaderConstructor.newInstance(classFinder, parent); + } + catch (InvocationTargetException ex) { + throw new IllegalStateException("WebLogic GenericClassLoader constructor failed", ex.getCause()); + } + catch (Throwable ex) { + throw new IllegalStateException("Could not construct WebLogic GenericClassLoader", ex); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/weblogic/WebLogicClassPreProcessorAdapter.java b/spring-context/src/main/java/org/springframework/instrument/classloading/weblogic/WebLogicClassPreProcessorAdapter.java new file mode 100644 index 0000000..36c8834 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/weblogic/WebLogicClassPreProcessorAdapter.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.instrument.classloading.weblogic; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.util.Hashtable; + +import org.springframework.lang.Nullable; + +/** + * Adapter that implements WebLogic ClassPreProcessor interface, delegating to a + * standard JDK {@link ClassFileTransformer} underneath. + * + *

    To avoid compile time checks again the vendor API, a dynamic proxy is + * being used. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 2.5 + */ +class WebLogicClassPreProcessorAdapter implements InvocationHandler { + + private final ClassFileTransformer transformer; + + private final ClassLoader loader; + + + /** + * Construct a new {@link WebLogicClassPreProcessorAdapter}. + */ + public WebLogicClassPreProcessorAdapter(ClassFileTransformer transformer, ClassLoader loader) { + this.transformer = transformer; + this.loader = loader; + } + + + @Override + @Nullable + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + String name = method.getName(); + if ("equals".equals(name)) { + return (proxy == args[0]); + } + else if ("hashCode".equals(name)) { + return hashCode(); + } + else if ("toString".equals(name)) { + return toString(); + } + else if ("initialize".equals(name)) { + initialize((Hashtable) args[0]); + return null; + } + else if ("preProcess".equals(name)) { + return preProcess((String) args[0], (byte[]) args[1]); + } + else { + throw new IllegalArgumentException("Unknown method: " + method); + } + } + + public void initialize(Hashtable params) { + } + + public byte[] preProcess(String className, byte[] classBytes) { + try { + byte[] result = this.transformer.transform(this.loader, className, null, null, classBytes); + return (result != null ? result : classBytes); + } + catch (IllegalClassFormatException ex) { + throw new IllegalStateException("Cannot transform due to illegal class format", ex); + } + } + + @Override + public String toString() { + return getClass().getName() + " for transformer: " + this.transformer; + } + +} diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/weblogic/WebLogicLoadTimeWeaver.java b/spring-context/src/main/java/org/springframework/instrument/classloading/weblogic/WebLogicLoadTimeWeaver.java new file mode 100644 index 0000000..9ca2b22 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/weblogic/WebLogicLoadTimeWeaver.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.instrument.classloading.weblogic; + +import java.lang.instrument.ClassFileTransformer; + +import org.springframework.core.OverridingClassLoader; +import org.springframework.instrument.classloading.LoadTimeWeaver; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * {@link LoadTimeWeaver} implementation for WebLogic's instrumentable + * ClassLoader. + * + *

    NOTE: Requires BEA WebLogic version 10 or higher. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 2.5 + */ +public class WebLogicLoadTimeWeaver implements LoadTimeWeaver { + + private final WebLogicClassLoaderAdapter classLoader; + + + /** + * Creates a new instance of the {@link WebLogicLoadTimeWeaver} class using + * the default {@link ClassLoader class loader}. + * @see org.springframework.util.ClassUtils#getDefaultClassLoader() + */ + public WebLogicLoadTimeWeaver() { + this(ClassUtils.getDefaultClassLoader()); + } + + /** + * Creates a new instance of the {@link WebLogicLoadTimeWeaver} class using + * the supplied {@link ClassLoader}. + * @param classLoader the {@code ClassLoader} to delegate to for weaving + */ + public WebLogicLoadTimeWeaver(@Nullable ClassLoader classLoader) { + Assert.notNull(classLoader, "ClassLoader must not be null"); + this.classLoader = new WebLogicClassLoaderAdapter(classLoader); + } + + + @Override + public void addTransformer(ClassFileTransformer transformer) { + this.classLoader.addTransformer(transformer); + } + + @Override + public ClassLoader getInstrumentableClassLoader() { + return this.classLoader.getClassLoader(); + } + + @Override + public ClassLoader getThrowawayClassLoader() { + return new OverridingClassLoader(this.classLoader.getClassLoader(), + this.classLoader.getThrowawayClassLoader()); + } + +} diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/weblogic/package-info.java b/spring-context/src/main/java/org/springframework/instrument/classloading/weblogic/package-info.java new file mode 100644 index 0000000..9335b69 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/weblogic/package-info.java @@ -0,0 +1,9 @@ +/** + * Support for class instrumentation on BEA WebLogic 10+. + */ +@NonNullApi +@NonNullFields +package org.springframework.instrument.classloading.weblogic; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/websphere/WebSphereClassLoaderAdapter.java b/spring-context/src/main/java/org/springframework/instrument/classloading/websphere/WebSphereClassLoaderAdapter.java new file mode 100644 index 0000000..bb68f07 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/websphere/WebSphereClassLoaderAdapter.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.instrument.classloading.websphere; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.List; + +import org.springframework.util.Assert; + +/** + * Reflective wrapper around a WebSphere 7+ class loader. Used to + * encapsulate the classloader-specific methods (discovered and + * called through reflection) from the load-time weaver. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 3.1 + */ +class WebSphereClassLoaderAdapter { + + private static final String COMPOUND_CLASS_LOADER_NAME = "com.ibm.ws.classloader.CompoundClassLoader"; + + private static final String CLASS_PRE_PROCESSOR_NAME = "com.ibm.websphere.classloader.ClassLoaderInstancePreDefinePlugin"; + + private static final String PLUGINS_FIELD = "preDefinePlugins"; + + + private ClassLoader classLoader; + + private Class wsPreProcessorClass; + + private Method addPreDefinePlugin; + + private Constructor cloneConstructor; + + private Field transformerList; + + + public WebSphereClassLoaderAdapter(ClassLoader classLoader) { + Class wsCompoundClassLoaderClass; + try { + wsCompoundClassLoaderClass = classLoader.loadClass(COMPOUND_CLASS_LOADER_NAME); + this.cloneConstructor = classLoader.getClass().getDeclaredConstructor(wsCompoundClassLoaderClass); + this.cloneConstructor.setAccessible(true); + + this.wsPreProcessorClass = classLoader.loadClass(CLASS_PRE_PROCESSOR_NAME); + this.addPreDefinePlugin = classLoader.getClass().getMethod("addPreDefinePlugin", this.wsPreProcessorClass); + this.transformerList = wsCompoundClassLoaderClass.getDeclaredField(PLUGINS_FIELD); + this.transformerList.setAccessible(true); + } + catch (Throwable ex) { + throw new IllegalStateException( + "Could not initialize WebSphere LoadTimeWeaver because WebSphere API classes are not available", ex); + } + + if (!wsCompoundClassLoaderClass.isInstance(classLoader)) { + throw new IllegalArgumentException( + "ClassLoader must be an instance of [" + COMPOUND_CLASS_LOADER_NAME + "]: " + classLoader); + } + this.classLoader = classLoader; + } + + + public ClassLoader getClassLoader() { + return this.classLoader; + } + + public void addTransformer(ClassFileTransformer transformer) { + Assert.notNull(transformer, "ClassFileTransformer must not be null"); + try { + InvocationHandler adapter = new WebSphereClassPreDefinePlugin(transformer); + Object adapterInstance = Proxy.newProxyInstance(this.wsPreProcessorClass.getClassLoader(), + new Class[] {this.wsPreProcessorClass}, adapter); + this.addPreDefinePlugin.invoke(this.classLoader, adapterInstance); + } + catch (InvocationTargetException ex) { + throw new IllegalStateException("WebSphere addPreDefinePlugin method threw exception", ex.getCause()); + } + catch (Throwable ex) { + throw new IllegalStateException("Could not invoke WebSphere addPreDefinePlugin method", ex); + } + } + + public ClassLoader getThrowawayClassLoader() { + try { + ClassLoader loader = this.cloneConstructor.newInstance(getClassLoader()); + // Clear out the transformers (copied as well) + List list = (List) this.transformerList.get(loader); + list.clear(); + return loader; + } + catch (InvocationTargetException ex) { + throw new IllegalStateException("WebSphere CompoundClassLoader constructor failed", ex.getCause()); + } + catch (Throwable ex) { + throw new IllegalStateException("Could not construct WebSphere CompoundClassLoader", ex); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/websphere/WebSphereClassPreDefinePlugin.java b/spring-context/src/main/java/org/springframework/instrument/classloading/websphere/WebSphereClassPreDefinePlugin.java new file mode 100644 index 0000000..c2d96a6 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/websphere/WebSphereClassPreDefinePlugin.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.instrument.classloading.websphere; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.security.CodeSource; + +import org.springframework.util.FileCopyUtils; + +/** + * Adapter that implements WebSphere 7.0 ClassPreProcessPlugin interface, + * delegating to a standard JDK {@link ClassFileTransformer} underneath. + * + *

    To avoid compile time checks again the vendor API, a dynamic proxy is + * being used. + * + * @author Costin Leau + * @since 3.1 + */ +class WebSphereClassPreDefinePlugin implements InvocationHandler { + + private final ClassFileTransformer transformer; + + + /** + * Create a new {@link WebSphereClassPreDefinePlugin}. + * @param transformer the {@link ClassFileTransformer} to be adapted + * (must not be {@code null}) + */ + public WebSphereClassPreDefinePlugin(ClassFileTransformer transformer) { + this.transformer = transformer; + ClassLoader classLoader = transformer.getClass().getClassLoader(); + + // First force the full class loading of the weaver by invoking transformation on a dummy class + try { + String dummyClass = Dummy.class.getName().replace('.', '/'); + byte[] bytes = FileCopyUtils.copyToByteArray(classLoader.getResourceAsStream(dummyClass + ".class")); + transformer.transform(classLoader, dummyClass, null, null, bytes); + } + catch (Throwable ex) { + throw new IllegalArgumentException("Cannot load transformer", ex); + } + } + + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + switch (method.getName()) { + case "equals": + return (proxy == args[0]); + case "hashCode": + return hashCode(); + case "toString": + return toString(); + case "transformClass": + return transform((String) args[0], (byte[]) args[1], (CodeSource) args[2], (ClassLoader) args[3]); + default: + throw new IllegalArgumentException("Unknown method: " + method); + } + } + + protected byte[] transform(String className, byte[] classfileBuffer, CodeSource codeSource, ClassLoader classLoader) + throws Exception { + + // NB: WebSphere passes className as "." without class while the transformer expects a VM "/" format + byte[] result = this.transformer.transform(classLoader, className.replace('.', '/'), null, null, classfileBuffer); + return (result != null ? result : classfileBuffer); + } + + @Override + public String toString() { + return getClass().getName() + " for transformer: " + this.transformer; + } + + + private static class Dummy { + } + +} diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/websphere/WebSphereLoadTimeWeaver.java b/spring-context/src/main/java/org/springframework/instrument/classloading/websphere/WebSphereLoadTimeWeaver.java new file mode 100644 index 0000000..0160aef --- /dev/null +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/websphere/WebSphereLoadTimeWeaver.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.instrument.classloading.websphere; + +import java.lang.instrument.ClassFileTransformer; + +import org.springframework.core.OverridingClassLoader; +import org.springframework.instrument.classloading.LoadTimeWeaver; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * {@link LoadTimeWeaver} implementation for WebSphere's instrumentable ClassLoader. + * Compatible with WebSphere 7 as well as 8 and 9. + * + * @author Costin Leau + * @since 3.1 + */ +public class WebSphereLoadTimeWeaver implements LoadTimeWeaver { + + private final WebSphereClassLoaderAdapter classLoader; + + + /** + * Create a new instance of the {@link WebSphereLoadTimeWeaver} class using + * the default {@link ClassLoader class loader}. + * @see org.springframework.util.ClassUtils#getDefaultClassLoader() + */ + public WebSphereLoadTimeWeaver() { + this(ClassUtils.getDefaultClassLoader()); + } + + /** + * Create a new instance of the {@link WebSphereLoadTimeWeaver} class using + * the supplied {@link ClassLoader}. + * @param classLoader the {@code ClassLoader} to delegate to for weaving + */ + public WebSphereLoadTimeWeaver(@Nullable ClassLoader classLoader) { + Assert.notNull(classLoader, "ClassLoader must not be null"); + this.classLoader = new WebSphereClassLoaderAdapter(classLoader); + } + + + @Override + public void addTransformer(ClassFileTransformer transformer) { + this.classLoader.addTransformer(transformer); + } + + @Override + public ClassLoader getInstrumentableClassLoader() { + return this.classLoader.getClassLoader(); + } + + @Override + public ClassLoader getThrowawayClassLoader() { + return new OverridingClassLoader(this.classLoader.getClassLoader(), + this.classLoader.getThrowawayClassLoader()); + } + +} diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/websphere/package-info.java b/spring-context/src/main/java/org/springframework/instrument/classloading/websphere/package-info.java new file mode 100644 index 0000000..7bf0ff5 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/websphere/package-info.java @@ -0,0 +1,9 @@ +/** + * Support for class instrumentation on IBM WebSphere Application Server 7+. + */ +@NonNullApi +@NonNullFields +package org.springframework.instrument.classloading.websphere; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/jmx/JmxException.java b/spring-context/src/main/java/org/springframework/jmx/JmxException.java new file mode 100644 index 0000000..f30bc13 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/JmxException.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx; + +import org.springframework.core.NestedRuntimeException; + +/** + * General base exception to be thrown on JMX errors. + * Unchecked since JMX failures are usually fatal. + * + * @author Juergen Hoeller + * @since 2.0 + */ +@SuppressWarnings("serial") +public class JmxException extends NestedRuntimeException { + + /** + * Constructor for JmxException. + * @param msg the detail message + */ + public JmxException(String msg) { + super(msg); + } + + /** + * Constructor for JmxException. + * @param msg the detail message + * @param cause the root cause (usually a raw JMX API exception) + */ + public JmxException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/MBeanServerNotFoundException.java b/spring-context/src/main/java/org/springframework/jmx/MBeanServerNotFoundException.java new file mode 100644 index 0000000..cc932ee --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/MBeanServerNotFoundException.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx; + +/** + * Exception thrown when we cannot locate an instance of an {@code MBeanServer}, + * or when more than one instance is found. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see org.springframework.jmx.support.JmxUtils#locateMBeanServer + */ +@SuppressWarnings("serial") +public class MBeanServerNotFoundException extends JmxException { + + /** + * Create a new {@code MBeanServerNotFoundException} with the + * supplied error message. + * @param msg the error message + */ + public MBeanServerNotFoundException(String msg) { + super(msg); + } + + /** + * Create a new {@code MBeanServerNotFoundException} with the + * specified error message and root cause. + * @param msg the error message + * @param cause the root cause + */ + public MBeanServerNotFoundException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/access/ConnectorDelegate.java b/spring-context/src/main/java/org/springframework/jmx/access/ConnectorDelegate.java new file mode 100644 index 0000000..a6e8a89 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/access/ConnectorDelegate.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.access; + +import java.io.IOException; +import java.util.Map; + +import javax.management.MBeanServerConnection; +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXServiceURL; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.jmx.MBeanServerNotFoundException; +import org.springframework.jmx.support.JmxUtils; +import org.springframework.lang.Nullable; + +/** + * Internal helper class for managing a JMX connector. + * + * @author Juergen Hoeller + * @since 2.5.2 + */ +class ConnectorDelegate { + + private static final Log logger = LogFactory.getLog(ConnectorDelegate.class); + + @Nullable + private JMXConnector connector; + + + /** + * Connects to the remote {@code MBeanServer} using the configured {@code JMXServiceURL}: + * to the specified JMX service, or to a local MBeanServer if no service URL specified. + * @param serviceUrl the JMX service URL to connect to (may be {@code null}) + * @param environment the JMX environment for the connector (may be {@code null}) + * @param agentId the local JMX MBeanServer's agent id (may be {@code null}) + */ + public MBeanServerConnection connect(@Nullable JMXServiceURL serviceUrl, @Nullable Map environment, @Nullable String agentId) + throws MBeanServerNotFoundException { + + if (serviceUrl != null) { + if (logger.isDebugEnabled()) { + logger.debug("Connecting to remote MBeanServer at URL [" + serviceUrl + "]"); + } + try { + this.connector = JMXConnectorFactory.connect(serviceUrl, environment); + return this.connector.getMBeanServerConnection(); + } + catch (IOException ex) { + throw new MBeanServerNotFoundException("Could not connect to remote MBeanServer [" + serviceUrl + "]", ex); + } + } + else { + logger.debug("Attempting to locate local MBeanServer"); + return JmxUtils.locateMBeanServer(agentId); + } + } + + /** + * Closes any {@code JMXConnector} that may be managed by this interceptor. + */ + public void close() { + if (this.connector != null) { + try { + this.connector.close(); + } + catch (IOException ex) { + logger.debug("Could not close JMX connector", ex); + } + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/access/InvalidInvocationException.java b/spring-context/src/main/java/org/springframework/jmx/access/InvalidInvocationException.java new file mode 100644 index 0000000..ea16a2f --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/access/InvalidInvocationException.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.access; + +import javax.management.JMRuntimeException; + +/** + * Thrown when trying to invoke an operation on a proxy that is not exposed + * by the proxied MBean resource's management interface. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see MBeanClientInterceptor + */ +@SuppressWarnings("serial") +public class InvalidInvocationException extends JMRuntimeException { + + /** + * Create a new {@code InvalidInvocationException} with the supplied + * error message. + * @param msg the detail message + */ + public InvalidInvocationException(String msg) { + super(msg); + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/access/InvocationFailureException.java b/spring-context/src/main/java/org/springframework/jmx/access/InvocationFailureException.java new file mode 100644 index 0000000..b93e229 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/access/InvocationFailureException.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.access; + +import org.springframework.jmx.JmxException; + +/** + * Thrown when an invocation on an MBean resource failed with an exception (either + * a reflection exception or an exception thrown by the target method itself). + * + * @author Juergen Hoeller + * @since 1.2 + * @see MBeanClientInterceptor + */ +@SuppressWarnings("serial") +public class InvocationFailureException extends JmxException { + + /** + * Create a new {@code InvocationFailureException} with the supplied + * error message. + * @param msg the detail message + */ + public InvocationFailureException(String msg) { + super(msg); + } + + /** + * Create a new {@code InvocationFailureException} with the + * specified error message and root cause. + * @param msg the detail message + * @param cause the root cause + */ + public InvocationFailureException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/access/MBeanClientInterceptor.java b/spring-context/src/main/java/org/springframework/jmx/access/MBeanClientInterceptor.java new file mode 100644 index 0000000..816459b --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/access/MBeanClientInterceptor.java @@ -0,0 +1,702 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.access; + +import java.beans.PropertyDescriptor; +import java.io.IOException; +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.management.Attribute; +import javax.management.InstanceNotFoundException; +import javax.management.IntrospectionException; +import javax.management.JMException; +import javax.management.JMX; +import javax.management.MBeanAttributeInfo; +import javax.management.MBeanException; +import javax.management.MBeanInfo; +import javax.management.MBeanOperationInfo; +import javax.management.MBeanServerConnection; +import javax.management.MBeanServerInvocationHandler; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; +import javax.management.OperationsException; +import javax.management.ReflectionException; +import javax.management.RuntimeErrorException; +import javax.management.RuntimeMBeanException; +import javax.management.RuntimeOperationsException; +import javax.management.openmbean.CompositeData; +import javax.management.openmbean.TabularData; +import javax.management.remote.JMXServiceURL; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.CollectionFactory; +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.jmx.support.JmxUtils; +import org.springframework.jmx.support.ObjectNameManager; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * {@link org.aopalliance.intercept.MethodInterceptor} that routes calls to an + * MBean running on the supplied {@code MBeanServerConnection}. + * Works for both local and remote {@code MBeanServerConnection}s. + * + *

    By default, the {@code MBeanClientInterceptor} will connect to the + * {@code MBeanServer} and cache MBean metadata at startup. This can + * be undesirable when running against a remote {@code MBeanServer} + * that may not be running when the application starts. Through setting the + * {@link #setConnectOnStartup(boolean) connectOnStartup} property to "false", + * you can defer this process until the first invocation against the proxy. + * + *

    This functionality is usually used through {@link MBeanProxyFactoryBean}. + * See the javadoc of that class for more information. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see MBeanProxyFactoryBean + * @see #setConnectOnStartup + */ +public class MBeanClientInterceptor + implements MethodInterceptor, BeanClassLoaderAware, InitializingBean, DisposableBean { + + /** Logger available to subclasses. */ + protected final Log logger = LogFactory.getLog(getClass()); + + @Nullable + private MBeanServerConnection server; + + @Nullable + private JMXServiceURL serviceUrl; + + @Nullable + private Map environment; + + @Nullable + private String agentId; + + private boolean connectOnStartup = true; + + private boolean refreshOnConnectFailure = false; + + @Nullable + private ObjectName objectName; + + private boolean useStrictCasing = true; + + @Nullable + private Class managementInterface; + + @Nullable + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + private final ConnectorDelegate connector = new ConnectorDelegate(); + + @Nullable + private MBeanServerConnection serverToUse; + + @Nullable + private MBeanServerInvocationHandler invocationHandler; + + private Map allowedAttributes = Collections.emptyMap(); + + private Map allowedOperations = Collections.emptyMap(); + + private final Map signatureCache = new HashMap<>(); + + private final Object preparationMonitor = new Object(); + + + /** + * Set the {@code MBeanServerConnection} used to connect to the + * MBean which all invocations are routed to. + */ + public void setServer(MBeanServerConnection server) { + this.server = server; + } + + /** + * Set the service URL of the remote {@code MBeanServer}. + */ + public void setServiceUrl(String url) throws MalformedURLException { + this.serviceUrl = new JMXServiceURL(url); + } + + /** + * Specify the environment for the JMX connector. + * @see javax.management.remote.JMXConnectorFactory#connect(javax.management.remote.JMXServiceURL, java.util.Map) + */ + public void setEnvironment(@Nullable Map environment) { + this.environment = environment; + } + + /** + * Allow Map access to the environment to be set for the connector, + * with the option to add or override specific entries. + *

    Useful for specifying entries directly, for example via + * "environment[myKey]". This is particularly useful for + * adding or overriding entries in child bean definitions. + */ + @Nullable + public Map getEnvironment() { + return this.environment; + } + + /** + * Set the agent id of the {@code MBeanServer} to locate. + *

    Default is none. If specified, this will result in an + * attempt being made to locate the attendant MBeanServer, unless + * the {@link #setServiceUrl "serviceUrl"} property has been set. + * @see javax.management.MBeanServerFactory#findMBeanServer(String) + *

    Specifying the empty String indicates the platform MBeanServer. + */ + public void setAgentId(String agentId) { + this.agentId = agentId; + } + + /** + * Set whether or not the proxy should connect to the {@code MBeanServer} + * at creation time ("true") or the first time it is invoked ("false"). + * Default is "true". + */ + public void setConnectOnStartup(boolean connectOnStartup) { + this.connectOnStartup = connectOnStartup; + } + + /** + * Set whether to refresh the MBeanServer connection on connect failure. + * Default is "false". + *

    Can be turned on to allow for hot restart of the JMX server, + * automatically reconnecting and retrying in case of an IOException. + */ + public void setRefreshOnConnectFailure(boolean refreshOnConnectFailure) { + this.refreshOnConnectFailure = refreshOnConnectFailure; + } + + /** + * Set the {@code ObjectName} of the MBean which calls are routed to, + * as {@code ObjectName} instance or as {@code String}. + */ + public void setObjectName(Object objectName) throws MalformedObjectNameException { + this.objectName = ObjectNameManager.getInstance(objectName); + } + + /** + * Set whether to use strict casing for attributes. Enabled by default. + *

    When using strict casing, a JavaBean property with a getter such as + * {@code getFoo()} translates to an attribute called {@code Foo}. + * With strict casing disabled, {@code getFoo()} would translate to just + * {@code foo}. + */ + public void setUseStrictCasing(boolean useStrictCasing) { + this.useStrictCasing = useStrictCasing; + } + + /** + * Set the management interface of the target MBean, exposing bean property + * setters and getters for MBean attributes and conventional Java methods + * for MBean operations. + */ + public void setManagementInterface(@Nullable Class managementInterface) { + this.managementInterface = managementInterface; + } + + /** + * Return the management interface of the target MBean, + * or {@code null} if none specified. + */ + @Nullable + protected final Class getManagementInterface() { + return this.managementInterface; + } + + @Override + public void setBeanClassLoader(ClassLoader beanClassLoader) { + this.beanClassLoader = beanClassLoader; + } + + + /** + * Prepares the {@code MBeanServerConnection} if the "connectOnStartup" + * is turned on (which it is by default). + */ + @Override + public void afterPropertiesSet() { + if (this.server != null && this.refreshOnConnectFailure) { + throw new IllegalArgumentException("'refreshOnConnectFailure' does not work when setting " + + "a 'server' reference. Prefer 'serviceUrl' etc instead."); + } + if (this.connectOnStartup) { + prepare(); + } + } + + /** + * Ensures that an {@code MBeanServerConnection} is configured and attempts + * to detect a local connection if one is not supplied. + */ + public void prepare() { + synchronized (this.preparationMonitor) { + if (this.server != null) { + this.serverToUse = this.server; + } + else { + this.serverToUse = null; + this.serverToUse = this.connector.connect(this.serviceUrl, this.environment, this.agentId); + } + this.invocationHandler = null; + if (this.useStrictCasing) { + Assert.state(this.objectName != null, "No ObjectName set"); + // Use the JDK's own MBeanServerInvocationHandler, in particular for native MXBean support. + this.invocationHandler = new MBeanServerInvocationHandler(this.serverToUse, this.objectName, + (this.managementInterface != null && JMX.isMXBeanInterface(this.managementInterface))); + } + else { + // Non-strict casing can only be achieved through custom invocation handling. + // Only partial MXBean support available! + retrieveMBeanInfo(this.serverToUse); + } + } + } + /** + * Loads the management interface info for the configured MBean into the caches. + * This information is used by the proxy when determining whether an invocation matches + * a valid operation or attribute on the management interface of the managed resource. + */ + private void retrieveMBeanInfo(MBeanServerConnection server) throws MBeanInfoRetrievalException { + try { + MBeanInfo info = server.getMBeanInfo(this.objectName); + + MBeanAttributeInfo[] attributeInfo = info.getAttributes(); + this.allowedAttributes = CollectionUtils.newHashMap(attributeInfo.length); + for (MBeanAttributeInfo infoEle : attributeInfo) { + this.allowedAttributes.put(infoEle.getName(), infoEle); + } + + MBeanOperationInfo[] operationInfo = info.getOperations(); + this.allowedOperations = CollectionUtils.newHashMap(operationInfo.length); + for (MBeanOperationInfo infoEle : operationInfo) { + Class[] paramTypes = JmxUtils.parameterInfoToTypes(infoEle.getSignature(), this.beanClassLoader); + this.allowedOperations.put(new MethodCacheKey(infoEle.getName(), paramTypes), infoEle); + } + } + catch (ClassNotFoundException ex) { + throw new MBeanInfoRetrievalException("Unable to locate class specified in method signature", ex); + } + catch (IntrospectionException ex) { + throw new MBeanInfoRetrievalException("Unable to obtain MBean info for bean [" + this.objectName + "]", ex); + } + catch (InstanceNotFoundException ex) { + // if we are this far this shouldn't happen, but... + throw new MBeanInfoRetrievalException("Unable to obtain MBean info for bean [" + this.objectName + + "]: it is likely that this bean was unregistered during the proxy creation process", + ex); + } + catch (ReflectionException ex) { + throw new MBeanInfoRetrievalException("Unable to read MBean info for bean [ " + this.objectName + "]", ex); + } + catch (IOException ex) { + throw new MBeanInfoRetrievalException("An IOException occurred when communicating with the " + + "MBeanServer. It is likely that you are communicating with a remote MBeanServer. " + + "Check the inner exception for exact details.", ex); + } + } + + /** + * Return whether this client interceptor has already been prepared, + * i.e. has already looked up the server and cached all metadata. + */ + protected boolean isPrepared() { + synchronized (this.preparationMonitor) { + return (this.serverToUse != null); + } + } + + + /** + * Route the invocation to the configured managed resource.. + * @param invocation the {@code MethodInvocation} to re-route + * @return the value returned as a result of the re-routed invocation + * @throws Throwable an invocation error propagated to the user + * @see #doInvoke + * @see #handleConnectFailure + */ + @Override + @Nullable + public Object invoke(MethodInvocation invocation) throws Throwable { + // Lazily connect to MBeanServer if necessary. + synchronized (this.preparationMonitor) { + if (!isPrepared()) { + prepare(); + } + } + try { + return doInvoke(invocation); + } + catch (MBeanConnectFailureException | IOException ex) { + return handleConnectFailure(invocation, ex); + } + } + + /** + * Refresh the connection and retry the MBean invocation if possible. + *

    If not configured to refresh on connect failure, this method + * simply rethrows the original exception. + * @param invocation the invocation that failed + * @param ex the exception raised on remote invocation + * @return the result value of the new invocation, if succeeded + * @throws Throwable an exception raised by the new invocation, + * if it failed as well + * @see #setRefreshOnConnectFailure + * @see #doInvoke + */ + @Nullable + protected Object handleConnectFailure(MethodInvocation invocation, Exception ex) throws Throwable { + if (this.refreshOnConnectFailure) { + String msg = "Could not connect to JMX server - retrying"; + if (logger.isDebugEnabled()) { + logger.warn(msg, ex); + } + else if (logger.isWarnEnabled()) { + logger.warn(msg); + } + prepare(); + return doInvoke(invocation); + } + else { + throw ex; + } + } + + /** + * Route the invocation to the configured managed resource. Correctly routes JavaBean property + * access to {@code MBeanServerConnection.get/setAttribute} and method invocation to + * {@code MBeanServerConnection.invoke}. + * @param invocation the {@code MethodInvocation} to re-route + * @return the value returned as a result of the re-routed invocation + * @throws Throwable an invocation error propagated to the user + */ + @Nullable + protected Object doInvoke(MethodInvocation invocation) throws Throwable { + Method method = invocation.getMethod(); + try { + Object result; + if (this.invocationHandler != null) { + result = this.invocationHandler.invoke(invocation.getThis(), method, invocation.getArguments()); + } + else { + PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method); + if (pd != null) { + result = invokeAttribute(pd, invocation); + } + else { + result = invokeOperation(method, invocation.getArguments()); + } + } + return convertResultValueIfNecessary(result, new MethodParameter(method, -1)); + } + catch (MBeanException ex) { + throw ex.getTargetException(); + } + catch (RuntimeMBeanException ex) { + throw ex.getTargetException(); + } + catch (RuntimeErrorException ex) { + throw ex.getTargetError(); + } + catch (RuntimeOperationsException ex) { + // This one is only thrown by the JMX 1.2 RI, not by the JDK 1.5 JMX code. + RuntimeException rex = ex.getTargetException(); + if (rex instanceof RuntimeMBeanException) { + throw ((RuntimeMBeanException) rex).getTargetException(); + } + else if (rex instanceof RuntimeErrorException) { + throw ((RuntimeErrorException) rex).getTargetError(); + } + else { + throw rex; + } + } + catch (OperationsException ex) { + if (ReflectionUtils.declaresException(method, ex.getClass())) { + throw ex; + } + else { + throw new InvalidInvocationException(ex.getMessage()); + } + } + catch (JMException ex) { + if (ReflectionUtils.declaresException(method, ex.getClass())) { + throw ex; + } + else { + throw new InvocationFailureException("JMX access failed", ex); + } + } + catch (IOException ex) { + if (ReflectionUtils.declaresException(method, ex.getClass())) { + throw ex; + } + else { + throw new MBeanConnectFailureException("I/O failure during JMX access", ex); + } + } + } + + @Nullable + private Object invokeAttribute(PropertyDescriptor pd, MethodInvocation invocation) + throws JMException, IOException { + + Assert.state(this.serverToUse != null, "No MBeanServerConnection available"); + + String attributeName = JmxUtils.getAttributeName(pd, this.useStrictCasing); + MBeanAttributeInfo inf = this.allowedAttributes.get(attributeName); + // If no attribute is returned, we know that it is not defined in the + // management interface. + if (inf == null) { + throw new InvalidInvocationException( + "Attribute '" + pd.getName() + "' is not exposed on the management interface"); + } + + if (invocation.getMethod().equals(pd.getReadMethod())) { + if (inf.isReadable()) { + return this.serverToUse.getAttribute(this.objectName, attributeName); + } + else { + throw new InvalidInvocationException("Attribute '" + attributeName + "' is not readable"); + } + } + else if (invocation.getMethod().equals(pd.getWriteMethod())) { + if (inf.isWritable()) { + this.serverToUse.setAttribute(this.objectName, new Attribute(attributeName, invocation.getArguments()[0])); + return null; + } + else { + throw new InvalidInvocationException("Attribute '" + attributeName + "' is not writable"); + } + } + else { + throw new IllegalStateException( + "Method [" + invocation.getMethod() + "] is neither a bean property getter nor a setter"); + } + } + + /** + * Routes a method invocation (not a property get/set) to the corresponding + * operation on the managed resource. + * @param method the method corresponding to operation on the managed resource. + * @param args the invocation arguments + * @return the value returned by the method invocation. + */ + private Object invokeOperation(Method method, Object[] args) throws JMException, IOException { + Assert.state(this.serverToUse != null, "No MBeanServerConnection available"); + + MethodCacheKey key = new MethodCacheKey(method.getName(), method.getParameterTypes()); + MBeanOperationInfo info = this.allowedOperations.get(key); + if (info == null) { + throw new InvalidInvocationException("Operation '" + method.getName() + + "' is not exposed on the management interface"); + } + + String[] signature; + synchronized (this.signatureCache) { + signature = this.signatureCache.get(method); + if (signature == null) { + signature = JmxUtils.getMethodSignature(method); + this.signatureCache.put(method, signature); + } + } + + return this.serverToUse.invoke(this.objectName, method.getName(), args, signature); + } + + /** + * Convert the given result object (from attribute access or operation invocation) + * to the specified target class for returning from the proxy method. + * @param result the result object as returned by the {@code MBeanServer} + * @param parameter the method parameter of the proxy method that's been invoked + * @return the converted result object, or the passed-in object if no conversion + * is necessary + */ + @Nullable + protected Object convertResultValueIfNecessary(@Nullable Object result, MethodParameter parameter) { + Class targetClass = parameter.getParameterType(); + try { + if (result == null) { + return null; + } + if (ClassUtils.isAssignableValue(targetClass, result)) { + return result; + } + if (result instanceof CompositeData) { + Method fromMethod = targetClass.getMethod("from", CompositeData.class); + return ReflectionUtils.invokeMethod(fromMethod, null, result); + } + else if (result instanceof CompositeData[]) { + CompositeData[] array = (CompositeData[]) result; + if (targetClass.isArray()) { + return convertDataArrayToTargetArray(array, targetClass); + } + else if (Collection.class.isAssignableFrom(targetClass)) { + Class elementType = + ResolvableType.forMethodParameter(parameter).asCollection().resolveGeneric(); + if (elementType != null) { + return convertDataArrayToTargetCollection(array, targetClass, elementType); + } + } + } + else if (result instanceof TabularData) { + Method fromMethod = targetClass.getMethod("from", TabularData.class); + return ReflectionUtils.invokeMethod(fromMethod, null, result); + } + else if (result instanceof TabularData[]) { + TabularData[] array = (TabularData[]) result; + if (targetClass.isArray()) { + return convertDataArrayToTargetArray(array, targetClass); + } + else if (Collection.class.isAssignableFrom(targetClass)) { + Class elementType = + ResolvableType.forMethodParameter(parameter).asCollection().resolveGeneric(); + if (elementType != null) { + return convertDataArrayToTargetCollection(array, targetClass, elementType); + } + } + } + throw new InvocationFailureException( + "Incompatible result value [" + result + "] for target type [" + targetClass.getName() + "]"); + } + catch (NoSuchMethodException ex) { + throw new InvocationFailureException( + "Could not obtain 'from(CompositeData)' / 'from(TabularData)' method on target type [" + + targetClass.getName() + "] for conversion of MXBean data structure [" + result + "]"); + } + } + + private Object convertDataArrayToTargetArray(Object[] array, Class targetClass) throws NoSuchMethodException { + Class targetType = targetClass.getComponentType(); + Method fromMethod = targetType.getMethod("from", array.getClass().getComponentType()); + Object resultArray = Array.newInstance(targetType, array.length); + for (int i = 0; i < array.length; i++) { + Array.set(resultArray, i, ReflectionUtils.invokeMethod(fromMethod, null, array[i])); + } + return resultArray; + } + + private Collection convertDataArrayToTargetCollection(Object[] array, Class collectionType, Class elementType) + throws NoSuchMethodException { + + Method fromMethod = elementType.getMethod("from", array.getClass().getComponentType()); + Collection resultColl = CollectionFactory.createCollection(collectionType, Array.getLength(array)); + for (int i = 0; i < array.length; i++) { + resultColl.add(ReflectionUtils.invokeMethod(fromMethod, null, array[i])); + } + return resultColl; + } + + + @Override + public void destroy() { + this.connector.close(); + } + + + /** + * Simple wrapper class around a method name and its signature. + * Used as the key when caching methods. + */ + private static final class MethodCacheKey implements Comparable { + + private final String name; + + private final Class[] parameterTypes; + + /** + * Create a new instance of {@code MethodCacheKey} with the supplied + * method name and parameter list. + * @param name the name of the method + * @param parameterTypes the arguments in the method signature + */ + public MethodCacheKey(String name, @Nullable Class[] parameterTypes) { + this.name = name; + this.parameterTypes = (parameterTypes != null ? parameterTypes : new Class[0]); + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof MethodCacheKey)) { + return false; + } + MethodCacheKey otherKey = (MethodCacheKey) other; + return (this.name.equals(otherKey.name) && Arrays.equals(this.parameterTypes, otherKey.parameterTypes)); + } + + @Override + public int hashCode() { + return this.name.hashCode(); + } + + @Override + public String toString() { + return this.name + "(" + StringUtils.arrayToCommaDelimitedString(this.parameterTypes) + ")"; + } + + @Override + public int compareTo(MethodCacheKey other) { + int result = this.name.compareTo(other.name); + if (result != 0) { + return result; + } + if (this.parameterTypes.length < other.parameterTypes.length) { + return -1; + } + if (this.parameterTypes.length > other.parameterTypes.length) { + return 1; + } + for (int i = 0; i < this.parameterTypes.length; i++) { + result = this.parameterTypes[i].getName().compareTo(other.parameterTypes[i].getName()); + if (result != 0) { + return result; + } + } + return 0; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/access/MBeanConnectFailureException.java b/spring-context/src/main/java/org/springframework/jmx/access/MBeanConnectFailureException.java new file mode 100644 index 0000000..1d6b328 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/access/MBeanConnectFailureException.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.access; + +import org.springframework.jmx.JmxException; + +/** + * Thrown when an invocation failed because of an I/O problem on the + * MBeanServerConnection. + * + * @author Juergen Hoeller + * @since 2.5.6 + * @see MBeanClientInterceptor + */ +@SuppressWarnings("serial") +public class MBeanConnectFailureException extends JmxException { + + /** + * Create a new {@code MBeanConnectFailureException} + * with the specified error message and root cause. + * @param msg the detail message + * @param cause the root cause + */ + public MBeanConnectFailureException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/access/MBeanInfoRetrievalException.java b/spring-context/src/main/java/org/springframework/jmx/access/MBeanInfoRetrievalException.java new file mode 100644 index 0000000..93faf74 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/access/MBeanInfoRetrievalException.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.access; + +import org.springframework.jmx.JmxException; + +/** + * Thrown if an exception is encountered when trying to retrieve + * MBean metadata. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see MBeanClientInterceptor + * @see MBeanProxyFactoryBean + */ +@SuppressWarnings("serial") +public class MBeanInfoRetrievalException extends JmxException { + + /** + * Create a new {@code MBeanInfoRetrievalException} with the + * specified error message. + * @param msg the detail message + */ + public MBeanInfoRetrievalException(String msg) { + super(msg); + } + + /** + * Create a new {@code MBeanInfoRetrievalException} with the + * specified error message and root cause. + * @param msg the detail message + * @param cause the root cause + */ + public MBeanInfoRetrievalException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/access/MBeanProxyFactoryBean.java b/spring-context/src/main/java/org/springframework/jmx/access/MBeanProxyFactoryBean.java new file mode 100644 index 0000000..f55de8e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/access/MBeanProxyFactoryBean.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.access; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jmx.MBeanServerNotFoundException; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * Creates a proxy to a managed resource running either locally or remotely. + * The "proxyInterface" property defines the interface that the generated + * proxy is supposed to implement. This interface should define methods and + * properties that correspond to operations and attributes in the management + * interface of the resource you wish to proxy. + * + *

    There is no need for the managed resource to implement the proxy interface, + * although you may find it convenient to do. It is not required that every + * operation and attribute in the management interface is matched by a + * corresponding property or method in the proxy interface. + * + *

    Attempting to invoke or access any method or property on the proxy + * interface that does not correspond to the management interface will lead + * to an {@code InvalidInvocationException}. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see MBeanClientInterceptor + * @see InvalidInvocationException + */ +public class MBeanProxyFactoryBean extends MBeanClientInterceptor + implements FactoryBean, BeanClassLoaderAware, InitializingBean { + + @Nullable + private Class proxyInterface; + + @Nullable + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + @Nullable + private Object mbeanProxy; + + + /** + * Set the interface that the generated proxy will implement. + *

    This will usually be a management interface that matches the target MBean, + * exposing bean property setters and getters for MBean attributes and + * conventional Java methods for MBean operations. + * @see #setObjectName + */ + public void setProxyInterface(Class proxyInterface) { + this.proxyInterface = proxyInterface; + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + /** + * Checks that the {@code proxyInterface} has been specified and then + * generates the proxy for the target MBean. + */ + @Override + public void afterPropertiesSet() throws MBeanServerNotFoundException, MBeanInfoRetrievalException { + super.afterPropertiesSet(); + + if (this.proxyInterface == null) { + this.proxyInterface = getManagementInterface(); + if (this.proxyInterface == null) { + throw new IllegalArgumentException("Property 'proxyInterface' or 'managementInterface' is required"); + } + } + else { + if (getManagementInterface() == null) { + setManagementInterface(this.proxyInterface); + } + } + this.mbeanProxy = new ProxyFactory(this.proxyInterface, this).getProxy(this.beanClassLoader); + } + + + @Override + @Nullable + public Object getObject() { + return this.mbeanProxy; + } + + @Override + public Class getObjectType() { + return this.proxyInterface; + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/access/NotificationListenerRegistrar.java b/spring-context/src/main/java/org/springframework/jmx/access/NotificationListenerRegistrar.java new file mode 100644 index 0000000..3a369bb --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/access/NotificationListenerRegistrar.java @@ -0,0 +1,188 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.access; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.Arrays; +import java.util.Map; + +import javax.management.MBeanServerConnection; +import javax.management.ObjectName; +import javax.management.remote.JMXServiceURL; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jmx.JmxException; +import org.springframework.jmx.MBeanServerNotFoundException; +import org.springframework.jmx.support.NotificationListenerHolder; +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; + +/** + * Registrar object that associates a specific {@link javax.management.NotificationListener} + * with one or more MBeans in an {@link javax.management.MBeanServer} + * (typically via a {@link javax.management.MBeanServerConnection}). + * + * @author Juergen Hoeller + * @since 2.5.2 + * @see #setServer + * @see #setMappedObjectNames + * @see #setNotificationListener + */ +public class NotificationListenerRegistrar extends NotificationListenerHolder + implements InitializingBean, DisposableBean { + + /** Logger available to subclasses. */ + protected final Log logger = LogFactory.getLog(getClass()); + + private final ConnectorDelegate connector = new ConnectorDelegate(); + + @Nullable + private MBeanServerConnection server; + + @Nullable + private JMXServiceURL serviceUrl; + + @Nullable + private Map environment; + + @Nullable + private String agentId; + + @Nullable + private ObjectName[] actualObjectNames; + + + /** + * Set the {@code MBeanServerConnection} used to connect to the + * MBean which all invocations are routed to. + */ + public void setServer(MBeanServerConnection server) { + this.server = server; + } + + /** + * Specify the environment for the JMX connector. + * @see javax.management.remote.JMXConnectorFactory#connect(javax.management.remote.JMXServiceURL, java.util.Map) + */ + public void setEnvironment(@Nullable Map environment) { + this.environment = environment; + } + + /** + * Allow Map access to the environment to be set for the connector, + * with the option to add or override specific entries. + *

    Useful for specifying entries directly, for example via + * "environment[myKey]". This is particularly useful for + * adding or overriding entries in child bean definitions. + */ + @Nullable + public Map getEnvironment() { + return this.environment; + } + + /** + * Set the service URL of the remote {@code MBeanServer}. + */ + public void setServiceUrl(String url) throws MalformedURLException { + this.serviceUrl = new JMXServiceURL(url); + } + + /** + * Set the agent id of the {@code MBeanServer} to locate. + *

    Default is none. If specified, this will result in an + * attempt being made to locate the attendant MBeanServer, unless + * the {@link #setServiceUrl "serviceUrl"} property has been set. + * @see javax.management.MBeanServerFactory#findMBeanServer(String) + *

    Specifying the empty String indicates the platform MBeanServer. + */ + public void setAgentId(String agentId) { + this.agentId = agentId; + } + + + @Override + public void afterPropertiesSet() { + if (getNotificationListener() == null) { + throw new IllegalArgumentException("Property 'notificationListener' is required"); + } + if (CollectionUtils.isEmpty(this.mappedObjectNames)) { + throw new IllegalArgumentException("Property 'mappedObjectName' is required"); + } + prepare(); + } + + /** + * Registers the specified {@code NotificationListener}. + *

    Ensures that an {@code MBeanServerConnection} is configured and attempts + * to detect a local connection if one is not supplied. + */ + public void prepare() { + if (this.server == null) { + this.server = this.connector.connect(this.serviceUrl, this.environment, this.agentId); + } + try { + this.actualObjectNames = getResolvedObjectNames(); + if (this.actualObjectNames != null) { + if (logger.isDebugEnabled()) { + logger.debug("Registering NotificationListener for MBeans " + Arrays.asList(this.actualObjectNames)); + } + for (ObjectName actualObjectName : this.actualObjectNames) { + this.server.addNotificationListener( + actualObjectName, getNotificationListener(), getNotificationFilter(), getHandback()); + } + } + } + catch (IOException ex) { + throw new MBeanServerNotFoundException( + "Could not connect to remote MBeanServer at URL [" + this.serviceUrl + "]", ex); + } + catch (Exception ex) { + throw new JmxException("Unable to register NotificationListener", ex); + } + } + + /** + * Unregisters the specified {@code NotificationListener}. + */ + @Override + public void destroy() { + try { + if (this.server != null && this.actualObjectNames != null) { + for (ObjectName actualObjectName : this.actualObjectNames) { + try { + this.server.removeNotificationListener( + actualObjectName, getNotificationListener(), getNotificationFilter(), getHandback()); + } + catch (Exception ex) { + if (logger.isDebugEnabled()) { + logger.debug("Unable to unregister NotificationListener", ex); + } + } + } + } + } + finally { + this.connector.close(); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/access/package-info.java b/spring-context/src/main/java/org/springframework/jmx/access/package-info.java new file mode 100644 index 0000000..adac687 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/access/package-info.java @@ -0,0 +1,9 @@ +/** + * Provides support for accessing remote MBean resources. + */ +@NonNullApi +@NonNullFields +package org.springframework.jmx.access; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/jmx/export/MBeanExportException.java b/spring-context/src/main/java/org/springframework/jmx/export/MBeanExportException.java new file mode 100644 index 0000000..5c4caa2 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/MBeanExportException.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export; + +import org.springframework.jmx.JmxException; + +/** + * Exception thrown in case of failure when exporting an MBean. + * + * @author Rob Harrop + * @since 2.0 + * @see MBeanExportOperations + */ +@SuppressWarnings("serial") +public class MBeanExportException extends JmxException { + + /** + * Create a new {@code MBeanExportException} with the + * specified error message. + * @param msg the detail message + */ + public MBeanExportException(String msg) { + super(msg); + } + + /** + * Create a new {@code MBeanExportException} with the + * specified error message and root cause. + * @param msg the detail message + * @param cause the root cause + */ + public MBeanExportException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/MBeanExportOperations.java b/spring-context/src/main/java/org/springframework/jmx/export/MBeanExportOperations.java new file mode 100644 index 0000000..7f04921 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/MBeanExportOperations.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export; + +import javax.management.ObjectName; + +/** + * Interface that defines the set of MBean export operations that are intended to be + * accessed by application developers during application runtime. + * + *

    This interface should be used to export application resources to JMX using Spring's + * management interface generation capabilities and, optionally, it's {@link ObjectName} + * generation capabilities. + * + * @author Rob Harrop + * @since 2.0 + * @see MBeanExporter + */ +public interface MBeanExportOperations { + + /** + * Register the supplied resource with JMX. If the resource is not a valid MBean already, + * Spring will generate a management interface for it. The exact interface generated will + * depend on the implementation and its configuration. This call also generates an + * {@link ObjectName} for the managed resource and returns this to the caller. + * @param managedResource the resource to expose via JMX + * @return the {@link ObjectName} under which the resource was exposed + * @throws MBeanExportException if Spring is unable to generate an {@link ObjectName} + * or register the MBean + */ + ObjectName registerManagedResource(Object managedResource) throws MBeanExportException; + + /** + * Register the supplied resource with JMX. If the resource is not a valid MBean already, + * Spring will generate a management interface for it. The exact interface generated will + * depend on the implementation and its configuration. + * @param managedResource the resource to expose via JMX + * @param objectName the {@link ObjectName} under which to expose the resource + * @throws MBeanExportException if Spring is unable to register the MBean + */ + void registerManagedResource(Object managedResource, ObjectName objectName) throws MBeanExportException; + + /** + * Remove the specified MBean from the underlying MBeanServer registry. + * @param objectName the {@link ObjectName} of the resource to remove + */ + void unregisterManagedResource(ObjectName objectName); + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.java b/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.java new file mode 100644 index 0000000..98090fc --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporter.java @@ -0,0 +1,1120 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.management.DynamicMBean; +import javax.management.JMException; +import javax.management.MBeanException; +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.NotCompliantMBeanException; +import javax.management.NotificationListener; +import javax.management.ObjectName; +import javax.management.StandardMBean; +import javax.management.modelmbean.ModelMBean; +import javax.management.modelmbean.ModelMBeanInfo; +import javax.management.modelmbean.RequiredModelMBean; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.scope.ScopedProxyUtils; +import org.springframework.aop.support.AopUtils; +import org.springframework.aop.target.LazyInitTargetSource; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.CannotLoadBeanClassException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.Constants; +import org.springframework.jmx.export.assembler.AutodetectCapableMBeanInfoAssembler; +import org.springframework.jmx.export.assembler.MBeanInfoAssembler; +import org.springframework.jmx.export.assembler.SimpleReflectiveMBeanInfoAssembler; +import org.springframework.jmx.export.naming.KeyNamingStrategy; +import org.springframework.jmx.export.naming.ObjectNamingStrategy; +import org.springframework.jmx.export.naming.SelfNaming; +import org.springframework.jmx.export.notification.ModelMBeanNotificationPublisher; +import org.springframework.jmx.export.notification.NotificationPublisherAware; +import org.springframework.jmx.support.JmxUtils; +import org.springframework.jmx.support.MBeanRegistrationSupport; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; + +/** + * JMX exporter that allows for exposing any Spring-managed bean to a + * JMX {@link javax.management.MBeanServer}, without the need to define any + * JMX-specific information in the bean classes. + * + *

    If a bean implements one of the JMX management interfaces, MBeanExporter can + * simply register the MBean with the server through its autodetection process. + * + *

    If a bean does not implement one of the JMX management interfaces, MBeanExporter + * will create the management information using the supplied {@link MBeanInfoAssembler}. + * + *

    A list of {@link MBeanExporterListener MBeanExporterListeners} can be registered + * via the {@link #setListeners(MBeanExporterListener[]) listeners} property, allowing + * application code to be notified of MBean registration and unregistration events. + * + *

    This exporter is compatible with MBeans as well as MXBeans. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Rick Evans + * @author Mark Fisher + * @author Stephane Nicoll + * @since 1.2 + * @see #setBeans + * @see #setAutodetect + * @see #setAssembler + * @see #setListeners + * @see org.springframework.jmx.export.assembler.MBeanInfoAssembler + * @see MBeanExporterListener + */ +public class MBeanExporter extends MBeanRegistrationSupport implements MBeanExportOperations, + BeanClassLoaderAware, BeanFactoryAware, InitializingBean, SmartInitializingSingleton, DisposableBean { + + /** + * Autodetection mode indicating that no autodetection should be used. + */ + public static final int AUTODETECT_NONE = 0; + + /** + * Autodetection mode indicating that only valid MBeans should be autodetected. + */ + public static final int AUTODETECT_MBEAN = 1; + + /** + * Autodetection mode indicating that only the {@link MBeanInfoAssembler} should be able + * to autodetect beans. + */ + public static final int AUTODETECT_ASSEMBLER = 2; + + /** + * Autodetection mode indicating that all autodetection mechanisms should be used. + */ + public static final int AUTODETECT_ALL = AUTODETECT_MBEAN | AUTODETECT_ASSEMBLER; + + + /** + * Wildcard used to map a {@link javax.management.NotificationListener} + * to all MBeans registered by the {@code MBeanExporter}. + */ + private static final String WILDCARD = "*"; + + /** Constant for the JMX {@code mr_type} "ObjectReference". */ + private static final String MR_TYPE_OBJECT_REFERENCE = "ObjectReference"; + + /** Prefix for the autodetect constants defined in this class. */ + private static final String CONSTANT_PREFIX_AUTODETECT = "AUTODETECT_"; + + + /** Constants instance for this class. */ + private static final Constants constants = new Constants(MBeanExporter.class); + + /** The beans to be exposed as JMX managed resources, with JMX names as keys. */ + @Nullable + private Map beans; + + /** The autodetect mode to use for this MBeanExporter. */ + @Nullable + private Integer autodetectMode; + + /** Whether to eagerly initialize candidate beans when autodetecting MBeans. */ + private boolean allowEagerInit = false; + + /** Stores the MBeanInfoAssembler to use for this exporter. */ + private MBeanInfoAssembler assembler = new SimpleReflectiveMBeanInfoAssembler(); + + /** The strategy to use for creating ObjectNames for an object. */ + private ObjectNamingStrategy namingStrategy = new KeyNamingStrategy(); + + /** Indicates whether Spring should modify generated ObjectNames. */ + private boolean ensureUniqueRuntimeObjectNames = true; + + /** Indicates whether Spring should expose the managed resource ClassLoader in the MBean. */ + private boolean exposeManagedResourceClassLoader = true; + + /** A set of bean names that should be excluded from autodetection. */ + private Set excludedBeans = new HashSet<>(); + + /** The MBeanExporterListeners registered with this exporter. */ + @Nullable + private MBeanExporterListener[] listeners; + + /** The NotificationListeners to register for the MBeans registered by this exporter. */ + @Nullable + private NotificationListenerBean[] notificationListeners; + + /** Map of actually registered NotificationListeners. */ + private final Map registeredNotificationListeners = new LinkedHashMap<>(); + + /** Stores the ClassLoader to use for generating lazy-init proxies. */ + @Nullable + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + /** Stores the BeanFactory for use in autodetection process. */ + @Nullable + private ListableBeanFactory beanFactory; + + + /** + * Supply a {@code Map} of beans to be registered with the JMX + * {@code MBeanServer}. + *

    The String keys are the basis for the creation of JMX object names. + * By default, a JMX {@code ObjectName} will be created straight + * from the given key. This can be customized through specifying a + * custom {@code NamingStrategy}. + *

    Both bean instances and bean names are allowed as values. + * Bean instances are typically linked in through bean references. + * Bean names will be resolved as beans in the current factory, respecting + * lazy-init markers (that is, not triggering initialization of such beans). + * @param beans a Map with JMX names as keys and bean instances or bean names + * as values + * @see #setNamingStrategy + * @see org.springframework.jmx.export.naming.KeyNamingStrategy + * @see javax.management.ObjectName#ObjectName(String) + */ + public void setBeans(Map beans) { + this.beans = beans; + } + + /** + * Set whether to autodetect MBeans in the bean factory that this exporter + * runs in. Will also ask an {@code AutodetectCapableMBeanInfoAssembler} + * if available. + *

    This feature is turned off by default. Explicitly specify + * {@code true} here to enable autodetection. + * @see #setAssembler + * @see AutodetectCapableMBeanInfoAssembler + * @see #isMBean + */ + public void setAutodetect(boolean autodetect) { + this.autodetectMode = (autodetect ? AUTODETECT_ALL : AUTODETECT_NONE); + } + + /** + * Set the autodetection mode to use. + * @throws IllegalArgumentException if the supplied value is not + * one of the {@code AUTODETECT_} constants + * @see #setAutodetectModeName(String) + * @see #AUTODETECT_ALL + * @see #AUTODETECT_ASSEMBLER + * @see #AUTODETECT_MBEAN + * @see #AUTODETECT_NONE + */ + public void setAutodetectMode(int autodetectMode) { + if (!constants.getValues(CONSTANT_PREFIX_AUTODETECT).contains(autodetectMode)) { + throw new IllegalArgumentException("Only values of autodetect constants allowed"); + } + this.autodetectMode = autodetectMode; + } + + /** + * Set the autodetection mode to use by name. + * @throws IllegalArgumentException if the supplied value is not resolvable + * to one of the {@code AUTODETECT_} constants or is {@code null} + * @see #setAutodetectMode(int) + * @see #AUTODETECT_ALL + * @see #AUTODETECT_ASSEMBLER + * @see #AUTODETECT_MBEAN + * @see #AUTODETECT_NONE + */ + public void setAutodetectModeName(String constantName) { + if (!constantName.startsWith(CONSTANT_PREFIX_AUTODETECT)) { + throw new IllegalArgumentException("Only autodetect constants allowed"); + } + this.autodetectMode = (Integer) constants.asNumber(constantName); + } + + /** + * Specify whether to allow eager initialization of candidate beans + * when autodetecting MBeans in the Spring application context. + *

    Default is "false", respecting lazy-init flags on bean definitions. + * Switch this to "true" in order to search lazy-init beans as well, + * including FactoryBean-produced objects that haven't been initialized yet. + */ + public void setAllowEagerInit(boolean allowEagerInit) { + this.allowEagerInit = allowEagerInit; + } + + /** + * Set the implementation of the {@code MBeanInfoAssembler} interface to use + * for this exporter. Default is a {@code SimpleReflectiveMBeanInfoAssembler}. + *

    The passed-in assembler can optionally implement the + * {@code AutodetectCapableMBeanInfoAssembler} interface, which enables it + * to participate in the exporter's MBean autodetection process. + * @see org.springframework.jmx.export.assembler.SimpleReflectiveMBeanInfoAssembler + * @see org.springframework.jmx.export.assembler.AutodetectCapableMBeanInfoAssembler + * @see org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler + * @see #setAutodetect + */ + public void setAssembler(MBeanInfoAssembler assembler) { + this.assembler = assembler; + } + + /** + * Set the implementation of the {@code ObjectNamingStrategy} interface + * to use for this exporter. Default is a {@code KeyNamingStrategy}. + * @see org.springframework.jmx.export.naming.KeyNamingStrategy + * @see org.springframework.jmx.export.naming.MetadataNamingStrategy + */ + public void setNamingStrategy(ObjectNamingStrategy namingStrategy) { + this.namingStrategy = namingStrategy; + } + + /** + * Indicates whether Spring should ensure that {@link ObjectName ObjectNames} + * generated by the configured {@link ObjectNamingStrategy} for + * runtime-registered MBeans ({@link #registerManagedResource}) should get + * modified: to ensure uniqueness for every instance of a managed {@code Class}. + *

    The default value is {@code true}. + * @see #registerManagedResource + * @see JmxUtils#appendIdentityToObjectName(javax.management.ObjectName, Object) + */ + public void setEnsureUniqueRuntimeObjectNames(boolean ensureUniqueRuntimeObjectNames) { + this.ensureUniqueRuntimeObjectNames = ensureUniqueRuntimeObjectNames; + } + + /** + * Indicates whether or not the managed resource should be exposed on the + * {@link Thread#getContextClassLoader() thread context ClassLoader} before + * allowing any invocations on the MBean to occur. + *

    The default value is {@code true}, exposing a {@link SpringModelMBean} + * which performs thread context ClassLoader management. Switch this flag off to + * expose a standard JMX {@link javax.management.modelmbean.RequiredModelMBean}. + */ + public void setExposeManagedResourceClassLoader(boolean exposeManagedResourceClassLoader) { + this.exposeManagedResourceClassLoader = exposeManagedResourceClassLoader; + } + + /** + * Set the list of names for beans that should be excluded from autodetection. + */ + public void setExcludedBeans(String... excludedBeans) { + this.excludedBeans.clear(); + Collections.addAll(this.excludedBeans, excludedBeans); + } + + /** + * Add the name of bean that should be excluded from autodetection. + */ + public void addExcludedBean(String excludedBean) { + Assert.notNull(excludedBean, "ExcludedBean must not be null"); + this.excludedBeans.add(excludedBean); + } + + /** + * Set the {@code MBeanExporterListener}s that should be notified + * of MBean registration and unregistration events. + * @see MBeanExporterListener + */ + public void setListeners(MBeanExporterListener... listeners) { + this.listeners = listeners; + } + + /** + * Set the {@link NotificationListenerBean NotificationListenerBeans} + * containing the + * {@link javax.management.NotificationListener NotificationListeners} + * that will be registered with the {@link MBeanServer}. + * @see #setNotificationListenerMappings(java.util.Map) + * @see NotificationListenerBean + */ + public void setNotificationListeners(NotificationListenerBean... notificationListeners) { + this.notificationListeners = notificationListeners; + } + + /** + * Set the {@link NotificationListener NotificationListeners} to register + * with the {@link javax.management.MBeanServer}. + *

    The key of each entry in the {@code Map} is a {@link String} + * representation of the {@link javax.management.ObjectName} or the bean + * name of the MBean the listener should be registered for. Specifying an + * asterisk ({@code *}) for a key will cause the listener to be + * associated with all MBeans registered by this class at startup time. + *

    The value of each entry is the + * {@link javax.management.NotificationListener} to register. For more + * advanced options such as registering + * {@link javax.management.NotificationFilter NotificationFilters} and + * handback objects see {@link #setNotificationListeners(NotificationListenerBean[])}. + */ + public void setNotificationListenerMappings(Map listeners) { + Assert.notNull(listeners, "'listeners' must not be null"); + List notificationListeners = + new ArrayList<>(listeners.size()); + + listeners.forEach((key, listener) -> { + // Get the listener from the map value. + NotificationListenerBean bean = new NotificationListenerBean(listener); + // Get the ObjectName from the map key. + if (key != null && !WILDCARD.equals(key)) { + // This listener is mapped to a specific ObjectName. + bean.setMappedObjectName(key); + } + notificationListeners.add(bean); + }); + + this.notificationListeners = notificationListeners.toArray(new NotificationListenerBean[0]); + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + /** + * This callback is only required for resolution of bean names in the + * {@link #setBeans(java.util.Map) "beans"} {@link Map} and for + * autodetection of MBeans (in the latter case, a + * {@code ListableBeanFactory} is required). + * @see #setBeans + * @see #setAutodetect + */ + @Override + public void setBeanFactory(BeanFactory beanFactory) { + if (beanFactory instanceof ListableBeanFactory) { + this.beanFactory = (ListableBeanFactory) beanFactory; + } + else { + logger.debug("MBeanExporter not running in a ListableBeanFactory: autodetection of MBeans not available."); + } + } + + + //--------------------------------------------------------------------- + // Lifecycle in bean factory: automatically register/unregister beans + //--------------------------------------------------------------------- + + @Override + public void afterPropertiesSet() { + // If no server was provided then try to find one. This is useful in an environment + // where there is already an MBeanServer loaded. + if (this.server == null) { + this.server = JmxUtils.locateMBeanServer(); + } + } + + /** + * Kick off bean registration automatically after the regular singleton instantiation phase. + * @see #registerBeans() + */ + @Override + public void afterSingletonsInstantiated() { + try { + logger.debug("Registering beans for JMX exposure on startup"); + registerBeans(); + registerNotificationListeners(); + } + catch (RuntimeException ex) { + // Unregister beans already registered by this exporter. + unregisterNotificationListeners(); + unregisterBeans(); + throw ex; + } + } + + /** + * Unregisters all beans that this exported has exposed via JMX + * when the enclosing {@code ApplicationContext} is destroyed. + */ + @Override + public void destroy() { + logger.debug("Unregistering JMX-exposed beans on shutdown"); + unregisterNotificationListeners(); + unregisterBeans(); + } + + + //--------------------------------------------------------------------- + // Implementation of MBeanExportOperations interface + //--------------------------------------------------------------------- + + @Override + public ObjectName registerManagedResource(Object managedResource) throws MBeanExportException { + Assert.notNull(managedResource, "Managed resource must not be null"); + ObjectName objectName; + try { + objectName = getObjectName(managedResource, null); + if (this.ensureUniqueRuntimeObjectNames) { + objectName = JmxUtils.appendIdentityToObjectName(objectName, managedResource); + } + } + catch (Throwable ex) { + throw new MBeanExportException("Unable to generate ObjectName for MBean [" + managedResource + "]", ex); + } + registerManagedResource(managedResource, objectName); + return objectName; + } + + @Override + public void registerManagedResource(Object managedResource, ObjectName objectName) throws MBeanExportException { + Assert.notNull(managedResource, "Managed resource must not be null"); + Assert.notNull(objectName, "ObjectName must not be null"); + try { + if (isMBean(managedResource.getClass())) { + doRegister(managedResource, objectName); + } + else { + ModelMBean mbean = createAndConfigureMBean(managedResource, managedResource.getClass().getName()); + doRegister(mbean, objectName); + injectNotificationPublisherIfNecessary(managedResource, mbean, objectName); + } + } + catch (JMException ex) { + throw new UnableToRegisterMBeanException( + "Unable to register MBean [" + managedResource + "] with object name [" + objectName + "]", ex); + } + } + + @Override + public void unregisterManagedResource(ObjectName objectName) { + Assert.notNull(objectName, "ObjectName must not be null"); + doUnregister(objectName); + } + + + //--------------------------------------------------------------------- + // Exporter implementation + //--------------------------------------------------------------------- + + /** + * Register the defined beans with the {@link MBeanServer}. + *

    Each bean is exposed to the {@code MBeanServer} via a + * {@code ModelMBean}. The actual implementation of the + * {@code ModelMBean} interface used depends on the implementation of + * the {@code ModelMBeanProvider} interface that is configured. By + * default the {@code RequiredModelMBean} class that is supplied with + * all JMX implementations is used. + *

    The management interface produced for each bean is dependent on the + * {@code MBeanInfoAssembler} implementation being used. The + * {@code ObjectName} given to each bean is dependent on the + * implementation of the {@code ObjectNamingStrategy} interface being used. + */ + protected void registerBeans() { + // The beans property may be null, for example if we are relying solely on autodetection. + if (this.beans == null) { + this.beans = new HashMap<>(); + // Use AUTODETECT_ALL as default in no beans specified explicitly. + if (this.autodetectMode == null) { + this.autodetectMode = AUTODETECT_ALL; + } + } + + // Perform autodetection, if desired. + int mode = (this.autodetectMode != null ? this.autodetectMode : AUTODETECT_NONE); + if (mode != AUTODETECT_NONE) { + if (this.beanFactory == null) { + throw new MBeanExportException("Cannot autodetect MBeans if not running in a BeanFactory"); + } + if (mode == AUTODETECT_MBEAN || mode == AUTODETECT_ALL) { + // Autodetect any beans that are already MBeans. + logger.debug("Autodetecting user-defined JMX MBeans"); + autodetect(this.beans, (beanClass, beanName) -> isMBean(beanClass)); + } + // Allow the assembler a chance to vote for bean inclusion. + if ((mode == AUTODETECT_ASSEMBLER || mode == AUTODETECT_ALL) && + this.assembler instanceof AutodetectCapableMBeanInfoAssembler) { + autodetect(this.beans, ((AutodetectCapableMBeanInfoAssembler) this.assembler)::includeBean); + } + } + + if (!this.beans.isEmpty()) { + this.beans.forEach((beanName, instance) -> registerBeanNameOrInstance(instance, beanName)); + } + } + + /** + * Return whether the specified bean definition should be considered as lazy-init. + * @param beanFactory the bean factory that is supposed to contain the bean definition + * @param beanName the name of the bean to check + * @see org.springframework.beans.factory.config.ConfigurableListableBeanFactory#getBeanDefinition + * @see org.springframework.beans.factory.config.BeanDefinition#isLazyInit + */ + protected boolean isBeanDefinitionLazyInit(ListableBeanFactory beanFactory, String beanName) { + return (beanFactory instanceof ConfigurableListableBeanFactory && beanFactory.containsBeanDefinition(beanName) && + ((ConfigurableListableBeanFactory) beanFactory).getBeanDefinition(beanName).isLazyInit()); + } + + /** + * Register an individual bean with the {@link #setServer MBeanServer}. + *

    This method is responsible for deciding how a bean + * should be exposed to the {@code MBeanServer}. Specifically, if the + * supplied {@code mapValue} is the name of a bean that is configured + * for lazy initialization, then a proxy to the resource is registered with + * the {@code MBeanServer} so that the lazy load behavior is + * honored. If the bean is already an MBean then it will be registered + * directly with the {@code MBeanServer} without any intervention. For + * all other beans or bean names, the resource itself is registered with + * the {@code MBeanServer} directly. + * @param mapValue the value configured for this bean in the beans map; + * may be either the {@code String} name of a bean, or the bean itself + * @param beanKey the key associated with this bean in the beans map + * @return the {@code ObjectName} under which the resource was registered + * @throws MBeanExportException if the export failed + * @see #setBeans + * @see #registerBeanInstance + * @see #registerLazyInit + */ + protected ObjectName registerBeanNameOrInstance(Object mapValue, String beanKey) throws MBeanExportException { + try { + if (mapValue instanceof String) { + // Bean name pointing to a potentially lazy-init bean in the factory. + if (this.beanFactory == null) { + throw new MBeanExportException("Cannot resolve bean names if not running in a BeanFactory"); + } + String beanName = (String) mapValue; + if (isBeanDefinitionLazyInit(this.beanFactory, beanName)) { + ObjectName objectName = registerLazyInit(beanName, beanKey); + replaceNotificationListenerBeanNameKeysIfNecessary(beanName, objectName); + return objectName; + } + else { + Object bean = this.beanFactory.getBean(beanName); + ObjectName objectName = registerBeanInstance(bean, beanKey); + replaceNotificationListenerBeanNameKeysIfNecessary(beanName, objectName); + return objectName; + } + } + else { + // Plain bean instance -> register it directly. + if (this.beanFactory != null) { + Map beansOfSameType = + this.beanFactory.getBeansOfType(mapValue.getClass(), false, this.allowEagerInit); + for (Map.Entry entry : beansOfSameType.entrySet()) { + if (entry.getValue() == mapValue) { + String beanName = entry.getKey(); + ObjectName objectName = registerBeanInstance(mapValue, beanKey); + replaceNotificationListenerBeanNameKeysIfNecessary(beanName, objectName); + return objectName; + } + } + } + return registerBeanInstance(mapValue, beanKey); + } + } + catch (Throwable ex) { + throw new UnableToRegisterMBeanException( + "Unable to register MBean [" + mapValue + "] with key '" + beanKey + "'", ex); + } + } + + /** + * Replace any bean names used as keys in the {@code NotificationListener} + * mappings with their corresponding {@code ObjectName} values. + * @param beanName the name of the bean to be registered + * @param objectName the {@code ObjectName} under which the bean will be registered + * with the {@code MBeanServer} + */ + private void replaceNotificationListenerBeanNameKeysIfNecessary(String beanName, ObjectName objectName) { + if (this.notificationListeners != null) { + for (NotificationListenerBean notificationListener : this.notificationListeners) { + notificationListener.replaceObjectName(beanName, objectName); + } + } + } + + /** + * Registers an existing MBean or an MBean adapter for a plain bean + * with the {@code MBeanServer}. + * @param bean the bean to register, either an MBean or a plain bean + * @param beanKey the key associated with this bean in the beans map + * @return the {@code ObjectName} under which the bean was registered + * with the {@code MBeanServer} + */ + private ObjectName registerBeanInstance(Object bean, String beanKey) throws JMException { + ObjectName objectName = getObjectName(bean, beanKey); + Object mbeanToExpose = null; + if (isMBean(bean.getClass())) { + mbeanToExpose = bean; + } + else { + DynamicMBean adaptedBean = adaptMBeanIfPossible(bean); + if (adaptedBean != null) { + mbeanToExpose = adaptedBean; + } + } + + if (mbeanToExpose != null) { + if (logger.isDebugEnabled()) { + logger.debug("Located MBean '" + beanKey + "': registering with JMX server as MBean [" + + objectName + "]"); + } + doRegister(mbeanToExpose, objectName); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Located managed bean '" + beanKey + "': registering with JMX server as MBean [" + + objectName + "]"); + } + ModelMBean mbean = createAndConfigureMBean(bean, beanKey); + doRegister(mbean, objectName); + injectNotificationPublisherIfNecessary(bean, mbean, objectName); + } + + return objectName; + } + + /** + * Register beans that are configured for lazy initialization with the + * {@code MBeanServer} indirectly through a proxy. + * @param beanName the name of the bean in the {@code BeanFactory} + * @param beanKey the key associated with this bean in the beans map + * @return the {@code ObjectName} under which the bean was registered + * with the {@code MBeanServer} + */ + private ObjectName registerLazyInit(String beanName, String beanKey) throws JMException { + Assert.state(this.beanFactory != null, "No BeanFactory set"); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.setProxyTargetClass(true); + proxyFactory.setFrozen(true); + + if (isMBean(this.beanFactory.getType(beanName))) { + // A straight MBean... Let's create a simple lazy-init CGLIB proxy for it. + LazyInitTargetSource targetSource = new LazyInitTargetSource(); + targetSource.setTargetBeanName(beanName); + targetSource.setBeanFactory(this.beanFactory); + proxyFactory.setTargetSource(targetSource); + + Object proxy = proxyFactory.getProxy(this.beanClassLoader); + ObjectName objectName = getObjectName(proxy, beanKey); + if (logger.isDebugEnabled()) { + logger.debug("Located MBean '" + beanKey + "': registering with JMX server as lazy-init MBean [" + + objectName + "]"); + } + doRegister(proxy, objectName); + return objectName; + } + + else { + // A simple bean... Let's create a lazy-init ModelMBean proxy with notification support. + NotificationPublisherAwareLazyTargetSource targetSource = new NotificationPublisherAwareLazyTargetSource(); + targetSource.setTargetBeanName(beanName); + targetSource.setBeanFactory(this.beanFactory); + proxyFactory.setTargetSource(targetSource); + + Object proxy = proxyFactory.getProxy(this.beanClassLoader); + ObjectName objectName = getObjectName(proxy, beanKey); + if (logger.isDebugEnabled()) { + logger.debug("Located simple bean '" + beanKey + "': registering with JMX server as lazy-init MBean [" + + objectName + "]"); + } + ModelMBean mbean = createAndConfigureMBean(proxy, beanKey); + targetSource.setModelMBean(mbean); + targetSource.setObjectName(objectName); + doRegister(mbean, objectName); + return objectName; + } + } + + /** + * Retrieve the {@code ObjectName} for a bean. + *

    If the bean implements the {@code SelfNaming} interface, then the + * {@code ObjectName} will be retrieved using {@code SelfNaming.getObjectName()}. + * Otherwise, the configured {@code ObjectNamingStrategy} is used. + * @param bean the name of the bean in the {@code BeanFactory} + * @param beanKey the key associated with the bean in the beans map + * @return the {@code ObjectName} for the supplied bean + * @throws javax.management.MalformedObjectNameException + * if the retrieved {@code ObjectName} is malformed + */ + protected ObjectName getObjectName(Object bean, @Nullable String beanKey) throws MalformedObjectNameException { + if (bean instanceof SelfNaming) { + return ((SelfNaming) bean).getObjectName(); + } + else { + return this.namingStrategy.getObjectName(bean, beanKey); + } + } + + /** + * Determine whether the given bean class qualifies as an MBean as-is. + *

    The default implementation delegates to {@link JmxUtils#isMBean}, + * which checks for {@link javax.management.DynamicMBean} classes as well + * as classes with corresponding "*MBean" interface (Standard MBeans) + * or corresponding "*MXBean" interface (Java 6 MXBeans). + * @param beanClass the bean class to analyze + * @return whether the class qualifies as an MBean + * @see org.springframework.jmx.support.JmxUtils#isMBean(Class) + */ + protected boolean isMBean(@Nullable Class beanClass) { + return JmxUtils.isMBean(beanClass); + } + + /** + * Build an adapted MBean for the given bean instance, if possible. + *

    The default implementation builds a JMX 1.2 StandardMBean + * for the target's MBean/MXBean interface in case of an AOP proxy, + * delegating the interface's management operations to the proxy. + * @param bean the original bean instance + * @return the adapted MBean, or {@code null} if not possible + */ + @SuppressWarnings("unchecked") + @Nullable + protected DynamicMBean adaptMBeanIfPossible(Object bean) throws JMException { + Class targetClass = AopUtils.getTargetClass(bean); + if (targetClass != bean.getClass()) { + Class ifc = JmxUtils.getMXBeanInterface(targetClass); + if (ifc != null) { + if (!ifc.isInstance(bean)) { + throw new NotCompliantMBeanException("Managed bean [" + bean + + "] has a target class with an MXBean interface but does not expose it in the proxy"); + } + return new StandardMBean(bean, ((Class) ifc), true); + } + else { + ifc = JmxUtils.getMBeanInterface(targetClass); + if (ifc != null) { + if (!ifc.isInstance(bean)) { + throw new NotCompliantMBeanException("Managed bean [" + bean + + "] has a target class with an MBean interface but does not expose it in the proxy"); + } + return new StandardMBean(bean, ((Class) ifc)); + } + } + } + return null; + } + + /** + * Creates an MBean that is configured with the appropriate management + * interface for the supplied managed resource. + * @param managedResource the resource that is to be exported as an MBean + * @param beanKey the key associated with the managed bean + * @see #createModelMBean() + * @see #getMBeanInfo(Object, String) + */ + protected ModelMBean createAndConfigureMBean(Object managedResource, String beanKey) + throws MBeanExportException { + try { + ModelMBean mbean = createModelMBean(); + mbean.setModelMBeanInfo(getMBeanInfo(managedResource, beanKey)); + mbean.setManagedResource(managedResource, MR_TYPE_OBJECT_REFERENCE); + return mbean; + } + catch (Throwable ex) { + throw new MBeanExportException("Could not create ModelMBean for managed resource [" + + managedResource + "] with key '" + beanKey + "'", ex); + } + } + + /** + * Create an instance of a class that implements {@code ModelMBean}. + *

    This method is called to obtain a {@code ModelMBean} instance to + * use when registering a bean. This method is called once per bean during the + * registration phase and must return a new instance of {@code ModelMBean} + * @return a new instance of a class that implements {@code ModelMBean} + * @throws javax.management.MBeanException if creation of the ModelMBean failed + */ + protected ModelMBean createModelMBean() throws MBeanException { + return (this.exposeManagedResourceClassLoader ? new SpringModelMBean() : new RequiredModelMBean()); + } + + /** + * Gets the {@code ModelMBeanInfo} for the bean with the supplied key + * and of the supplied type. + */ + private ModelMBeanInfo getMBeanInfo(Object managedBean, String beanKey) throws JMException { + ModelMBeanInfo info = this.assembler.getMBeanInfo(managedBean, beanKey); + if (logger.isInfoEnabled() && ObjectUtils.isEmpty(info.getAttributes()) && + ObjectUtils.isEmpty(info.getOperations())) { + logger.info("Bean with key '" + beanKey + + "' has been registered as an MBean but has no exposed attributes or operations"); + } + return info; + } + + + //--------------------------------------------------------------------- + // Autodetection process + //--------------------------------------------------------------------- + + /** + * Performs the actual autodetection process, delegating to an + * {@code AutodetectCallback} instance to vote on the inclusion of a + * given bean. + * @param callback the {@code AutodetectCallback} to use when deciding + * whether to include a bean or not + */ + private void autodetect(Map beans, AutodetectCallback callback) { + Assert.state(this.beanFactory != null, "No BeanFactory set"); + Set beanNames = new LinkedHashSet<>(this.beanFactory.getBeanDefinitionCount()); + Collections.addAll(beanNames, this.beanFactory.getBeanDefinitionNames()); + if (this.beanFactory instanceof ConfigurableBeanFactory) { + Collections.addAll(beanNames, ((ConfigurableBeanFactory) this.beanFactory).getSingletonNames()); + } + + for (String beanName : beanNames) { + if (!isExcluded(beanName) && !isBeanDefinitionAbstract(this.beanFactory, beanName)) { + try { + Class beanClass = this.beanFactory.getType(beanName); + if (beanClass != null && callback.include(beanClass, beanName)) { + boolean lazyInit = isBeanDefinitionLazyInit(this.beanFactory, beanName); + Object beanInstance = null; + if (!lazyInit) { + beanInstance = this.beanFactory.getBean(beanName); + if (!beanClass.isInstance(beanInstance)) { + continue; + } + } + if (!ScopedProxyUtils.isScopedTarget(beanName) && !beans.containsValue(beanName) && + (beanInstance == null || + !CollectionUtils.containsInstance(beans.values(), beanInstance))) { + // Not already registered for JMX exposure. + beans.put(beanName, (beanInstance != null ? beanInstance : beanName)); + if (logger.isDebugEnabled()) { + logger.debug("Bean with name '" + beanName + "' has been autodetected for JMX exposure"); + } + } + else { + if (logger.isTraceEnabled()) { + logger.trace("Bean with name '" + beanName + "' is already registered for JMX exposure"); + } + } + } + } + catch (CannotLoadBeanClassException ex) { + if (this.allowEagerInit) { + throw ex; + } + // otherwise ignore beans where the class is not resolvable + } + } + } + } + + /** + * Indicates whether or not a particular bean name is present in the excluded beans list. + */ + private boolean isExcluded(String beanName) { + return (this.excludedBeans.contains(beanName) || + (beanName.startsWith(BeanFactory.FACTORY_BEAN_PREFIX) && + this.excludedBeans.contains(beanName.substring(BeanFactory.FACTORY_BEAN_PREFIX.length())))); + } + + /** + * Return whether the specified bean definition should be considered as abstract. + */ + private boolean isBeanDefinitionAbstract(ListableBeanFactory beanFactory, String beanName) { + return (beanFactory instanceof ConfigurableListableBeanFactory && beanFactory.containsBeanDefinition(beanName) && + ((ConfigurableListableBeanFactory) beanFactory).getBeanDefinition(beanName).isAbstract()); + } + + + //--------------------------------------------------------------------- + // Notification and listener management + //--------------------------------------------------------------------- + + /** + * If the supplied managed resource implements the {@link NotificationPublisherAware} an instance of + * {@link org.springframework.jmx.export.notification.NotificationPublisher} is injected. + */ + private void injectNotificationPublisherIfNecessary( + Object managedResource, @Nullable ModelMBean modelMBean, @Nullable ObjectName objectName) { + + if (managedResource instanceof NotificationPublisherAware && modelMBean != null && objectName != null) { + ((NotificationPublisherAware) managedResource).setNotificationPublisher( + new ModelMBeanNotificationPublisher(modelMBean, objectName, managedResource)); + } + } + + /** + * Register the configured {@link NotificationListener NotificationListeners} + * with the {@link MBeanServer}. + */ + private void registerNotificationListeners() throws MBeanExportException { + if (this.notificationListeners != null) { + Assert.state(this.server != null, "No MBeanServer available"); + for (NotificationListenerBean bean : this.notificationListeners) { + try { + ObjectName[] mappedObjectNames = bean.getResolvedObjectNames(); + if (mappedObjectNames == null) { + // Mapped to all MBeans registered by the MBeanExporter. + mappedObjectNames = getRegisteredObjectNames(); + } + if (this.registeredNotificationListeners.put(bean, mappedObjectNames) == null) { + for (ObjectName mappedObjectName : mappedObjectNames) { + this.server.addNotificationListener(mappedObjectName, bean.getNotificationListener(), + bean.getNotificationFilter(), bean.getHandback()); + } + } + } + catch (Throwable ex) { + throw new MBeanExportException("Unable to register NotificationListener", ex); + } + } + } + } + + /** + * Unregister the configured {@link NotificationListener NotificationListeners} + * from the {@link MBeanServer}. + */ + private void unregisterNotificationListeners() { + if (this.server != null) { + this.registeredNotificationListeners.forEach((bean, mappedObjectNames) -> { + for (ObjectName mappedObjectName : mappedObjectNames) { + try { + this.server.removeNotificationListener(mappedObjectName, bean.getNotificationListener(), + bean.getNotificationFilter(), bean.getHandback()); + } + catch (Throwable ex) { + if (logger.isDebugEnabled()) { + logger.debug("Unable to unregister NotificationListener", ex); + } + } + } + }); + } + this.registeredNotificationListeners.clear(); + } + + /** + * Called when an MBean is registered. Notifies all registered + * {@link MBeanExporterListener MBeanExporterListeners} of the registration event. + *

    Please note that if an {@link MBeanExporterListener} throws a (runtime) + * exception when notified, this will essentially interrupt the notification process + * and any remaining listeners that have yet to be notified will not (obviously) + * receive the {@link MBeanExporterListener#mbeanRegistered(javax.management.ObjectName)} + * callback. + * @param objectName the {@code ObjectName} of the registered MBean + */ + @Override + protected void onRegister(ObjectName objectName) { + notifyListenersOfRegistration(objectName); + } + + /** + * Called when an MBean is unregistered. Notifies all registered + * {@link MBeanExporterListener MBeanExporterListeners} of the unregistration event. + *

    Please note that if an {@link MBeanExporterListener} throws a (runtime) + * exception when notified, this will essentially interrupt the notification process + * and any remaining listeners that have yet to be notified will not (obviously) + * receive the {@link MBeanExporterListener#mbeanUnregistered(javax.management.ObjectName)} + * callback. + * @param objectName the {@code ObjectName} of the unregistered MBean + */ + @Override + protected void onUnregister(ObjectName objectName) { + notifyListenersOfUnregistration(objectName); + } + + + /** + * Notifies all registered {@link MBeanExporterListener MBeanExporterListeners} of the + * registration of the MBean identified by the supplied {@link ObjectName}. + */ + private void notifyListenersOfRegistration(ObjectName objectName) { + if (this.listeners != null) { + for (MBeanExporterListener listener : this.listeners) { + listener.mbeanRegistered(objectName); + } + } + } + + /** + * Notifies all registered {@link MBeanExporterListener MBeanExporterListeners} of the + * unregistration of the MBean identified by the supplied {@link ObjectName}. + */ + private void notifyListenersOfUnregistration(ObjectName objectName) { + if (this.listeners != null) { + for (MBeanExporterListener listener : this.listeners) { + listener.mbeanUnregistered(objectName); + } + } + } + + + //--------------------------------------------------------------------- + // Inner classes for internal use + //--------------------------------------------------------------------- + + /** + * Internal callback interface for the autodetection process. + */ + @FunctionalInterface + private interface AutodetectCallback { + + /** + * Called during the autodetection process to decide whether + * or not a bean should be included. + * @param beanClass the class of the bean + * @param beanName the name of the bean + */ + boolean include(Class beanClass, String beanName); + } + + + /** + * Extension of {@link LazyInitTargetSource} that will inject a + * {@link org.springframework.jmx.export.notification.NotificationPublisher} + * into the lazy resource as it is created if required. + */ + @SuppressWarnings("serial") + private class NotificationPublisherAwareLazyTargetSource extends LazyInitTargetSource { + + @Nullable + private ModelMBean modelMBean; + + @Nullable + private ObjectName objectName; + + public void setModelMBean(ModelMBean modelMBean) { + this.modelMBean = modelMBean; + } + + public void setObjectName(ObjectName objectName) { + this.objectName = objectName; + } + + @Override + @Nullable + public Object getTarget() { + try { + return super.getTarget(); + } + catch (RuntimeException ex) { + if (logger.isInfoEnabled()) { + logger.info("Failed to retrieve target for JMX-exposed bean [" + this.objectName + "]: " + ex); + } + throw ex; + } + } + + @Override + protected void postProcessTargetObject(Object targetObject) { + injectNotificationPublisherIfNecessary(targetObject, this.modelMBean, this.objectName); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporterListener.java b/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporterListener.java new file mode 100644 index 0000000..c76990c --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/MBeanExporterListener.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export; + +import javax.management.ObjectName; + +/** + * A listener that allows application code to be notified when an MBean is + * registered and unregistered via an {@link MBeanExporter}. + * + * @author Rob Harrop + * @since 1.2.2 + * @see org.springframework.jmx.export.MBeanExporter#setListeners + */ +public interface MBeanExporterListener { + + /** + * Called by {@link MBeanExporter} after an MBean has been successfully + * registered with an {@link javax.management.MBeanServer}. + * @param objectName the {@code ObjectName} of the registered MBean + */ + void mbeanRegistered(ObjectName objectName); + + /** + * Called by {@link MBeanExporter} after an MBean has been successfully + * unregistered from an {@link javax.management.MBeanServer}. + * @param objectName the {@code ObjectName} of the unregistered MBean + */ + void mbeanUnregistered(ObjectName objectName); + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/NotificationListenerBean.java b/spring-context/src/main/java/org/springframework/jmx/export/NotificationListenerBean.java new file mode 100644 index 0000000..a4936a6 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/NotificationListenerBean.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export; + +import javax.management.NotificationListener; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jmx.support.NotificationListenerHolder; +import org.springframework.util.Assert; + +/** + * Helper class that aggregates a {@link javax.management.NotificationListener}, + * a {@link javax.management.NotificationFilter}, and an arbitrary handback object. + * + *

    Also provides support for associating the encapsulated + * {@link javax.management.NotificationListener} with any number of + * MBeans from which it wishes to receive + * {@link javax.management.Notification Notifications} via the + * {@link #setMappedObjectNames mappedObjectNames} property. + * + *

    Note: This class supports Spring bean names as + * {@link #setMappedObjectNames "mappedObjectNames"} as well, as alternative + * to specifying JMX object names. Note that only beans exported by the + * same {@link MBeanExporter} are supported for such bean names. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + * @see MBeanExporter#setNotificationListeners + */ +public class NotificationListenerBean extends NotificationListenerHolder implements InitializingBean { + + /** + * Create a new instance of the {@link NotificationListenerBean} class. + */ + public NotificationListenerBean() { + } + + /** + * Create a new instance of the {@link NotificationListenerBean} class. + * @param notificationListener the encapsulated listener + */ + public NotificationListenerBean(NotificationListener notificationListener) { + Assert.notNull(notificationListener, "NotificationListener must not be null"); + setNotificationListener(notificationListener); + } + + + @Override + public void afterPropertiesSet() { + if (getNotificationListener() == null) { + throw new IllegalArgumentException("Property 'notificationListener' is required"); + } + } + + void replaceObjectName(Object originalName, Object newName) { + if (this.mappedObjectNames != null && this.mappedObjectNames.contains(originalName)) { + this.mappedObjectNames.remove(originalName); + this.mappedObjectNames.add(newName); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/SpringModelMBean.java b/spring-context/src/main/java/org/springframework/jmx/export/SpringModelMBean.java new file mode 100644 index 0000000..e0e8c02 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/SpringModelMBean.java @@ -0,0 +1,169 @@ +/* + * Copyright 2002-2007 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export; + +import javax.management.Attribute; +import javax.management.AttributeList; +import javax.management.AttributeNotFoundException; +import javax.management.InstanceNotFoundException; +import javax.management.InvalidAttributeValueException; +import javax.management.MBeanException; +import javax.management.ReflectionException; +import javax.management.RuntimeOperationsException; +import javax.management.modelmbean.InvalidTargetObjectTypeException; +import javax.management.modelmbean.ModelMBeanInfo; +import javax.management.modelmbean.RequiredModelMBean; + +/** + * Extension of the {@link RequiredModelMBean} class that ensures the + * {@link Thread#getContextClassLoader() thread context ClassLoader} is switched + * for the managed resource's {@link ClassLoader} before any invocations occur. + * + * @author Rob Harrop + * @since 2.0 + * @see RequiredModelMBean + */ +public class SpringModelMBean extends RequiredModelMBean { + + /** + * Stores the {@link ClassLoader} to use for invocations. Defaults + * to the current thread {@link ClassLoader}. + */ + private ClassLoader managedResourceClassLoader = Thread.currentThread().getContextClassLoader(); + + + /** + * Construct a new SpringModelMBean instance with an empty {@link ModelMBeanInfo}. + * @see javax.management.modelmbean.RequiredModelMBean#RequiredModelMBean() + */ + public SpringModelMBean() throws MBeanException, RuntimeOperationsException { + super(); + } + + /** + * Construct a new SpringModelMBean instance with the given {@link ModelMBeanInfo}. + * @see javax.management.modelmbean.RequiredModelMBean#RequiredModelMBean(ModelMBeanInfo) + */ + public SpringModelMBean(ModelMBeanInfo mbi) throws MBeanException, RuntimeOperationsException { + super(mbi); + } + + + /** + * Sets managed resource to expose and stores its {@link ClassLoader}. + */ + @Override + public void setManagedResource(Object managedResource, String managedResourceType) + throws MBeanException, InstanceNotFoundException, InvalidTargetObjectTypeException { + + this.managedResourceClassLoader = managedResource.getClass().getClassLoader(); + super.setManagedResource(managedResource, managedResourceType); + } + + + /** + * Switches the {@link Thread#getContextClassLoader() context ClassLoader} for the + * managed resources {@link ClassLoader} before allowing the invocation to occur. + * @see javax.management.modelmbean.ModelMBean#invoke + */ + @Override + public Object invoke(String opName, Object[] opArgs, String[] sig) + throws MBeanException, ReflectionException { + + ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(this.managedResourceClassLoader); + return super.invoke(opName, opArgs, sig); + } + finally { + Thread.currentThread().setContextClassLoader(currentClassLoader); + } + } + + /** + * Switches the {@link Thread#getContextClassLoader() context ClassLoader} for the + * managed resources {@link ClassLoader} before allowing the invocation to occur. + * @see javax.management.modelmbean.ModelMBean#getAttribute + */ + @Override + public Object getAttribute(String attrName) + throws AttributeNotFoundException, MBeanException, ReflectionException { + + ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(this.managedResourceClassLoader); + return super.getAttribute(attrName); + } + finally { + Thread.currentThread().setContextClassLoader(currentClassLoader); + } + } + + /** + * Switches the {@link Thread#getContextClassLoader() context ClassLoader} for the + * managed resources {@link ClassLoader} before allowing the invocation to occur. + * @see javax.management.modelmbean.ModelMBean#getAttributes + */ + @Override + public AttributeList getAttributes(String[] attrNames) { + ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(this.managedResourceClassLoader); + return super.getAttributes(attrNames); + } + finally { + Thread.currentThread().setContextClassLoader(currentClassLoader); + } + } + + /** + * Switches the {@link Thread#getContextClassLoader() context ClassLoader} for the + * managed resources {@link ClassLoader} before allowing the invocation to occur. + * @see javax.management.modelmbean.ModelMBean#setAttribute + */ + @Override + public void setAttribute(Attribute attribute) + throws AttributeNotFoundException, InvalidAttributeValueException, MBeanException, ReflectionException { + + ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(this.managedResourceClassLoader); + super.setAttribute(attribute); + } + finally { + Thread.currentThread().setContextClassLoader(currentClassLoader); + } + } + + /** + * Switches the {@link Thread#getContextClassLoader() context ClassLoader} for the + * managed resources {@link ClassLoader} before allowing the invocation to occur. + * @see javax.management.modelmbean.ModelMBean#setAttributes + */ + @Override + public AttributeList setAttributes(AttributeList attributes) { + ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(this.managedResourceClassLoader); + return super.setAttributes(attributes); + } + finally { + Thread.currentThread().setContextClassLoader(currentClassLoader); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/UnableToRegisterMBeanException.java b/spring-context/src/main/java/org/springframework/jmx/export/UnableToRegisterMBeanException.java new file mode 100644 index 0000000..117ec24 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/UnableToRegisterMBeanException.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export; + +/** + * Exception thrown when we are unable to register an MBean, + * for example because of a naming conflict. + * + * @author Rob Harrop + * @since 2.0 + */ +@SuppressWarnings("serial") +public class UnableToRegisterMBeanException extends MBeanExportException { + + /** + * Create a new {@code UnableToRegisterMBeanException} with the + * specified error message. + * @param msg the detail message + */ + public UnableToRegisterMBeanException(String msg) { + super(msg); + } + + /** + * Create a new {@code UnableToRegisterMBeanException} with the + * specified error message and root cause. + * @param msg the detail message + * @param cause the root caus + */ + public UnableToRegisterMBeanException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/annotation/AnnotationJmxAttributeSource.java b/spring-context/src/main/java/org/springframework/jmx/export/annotation/AnnotationJmxAttributeSource.java new file mode 100644 index 0000000..429767c --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/annotation/AnnotationJmxAttributeSource.java @@ -0,0 +1,200 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.PropertyAccessorFactory; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.EmbeddedValueResolver; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotationPredicates; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; +import org.springframework.core.annotation.RepeatableContainers; +import org.springframework.jmx.export.metadata.InvalidMetadataException; +import org.springframework.jmx.export.metadata.JmxAttributeSource; +import org.springframework.lang.Nullable; +import org.springframework.util.StringValueResolver; + +/** + * Implementation of the {@code JmxAttributeSource} interface that + * reads annotations and exposes the corresponding attributes. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Jennifer Hickey + * @author Stephane Nicoll + * @since 1.2 + * @see ManagedResource + * @see ManagedAttribute + * @see ManagedOperation + */ +public class AnnotationJmxAttributeSource implements JmxAttributeSource, BeanFactoryAware { + + @Nullable + private StringValueResolver embeddedValueResolver; + + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + if (beanFactory instanceof ConfigurableBeanFactory) { + this.embeddedValueResolver = new EmbeddedValueResolver((ConfigurableBeanFactory) beanFactory); + } + } + + + @Override + @Nullable + public org.springframework.jmx.export.metadata.ManagedResource getManagedResource(Class beanClass) throws InvalidMetadataException { + MergedAnnotation ann = MergedAnnotations.from(beanClass, SearchStrategy.TYPE_HIERARCHY) + .get(ManagedResource.class).withNonMergedAttributes(); + if (!ann.isPresent()) { + return null; + } + Class declaringClass = (Class) ann.getSource(); + Class target = (declaringClass != null && !declaringClass.isInterface() ? declaringClass : beanClass); + if (!Modifier.isPublic(target.getModifiers())) { + throw new InvalidMetadataException("@ManagedResource class '" + target.getName() + "' must be public"); + } + + org.springframework.jmx.export.metadata.ManagedResource bean = new org.springframework.jmx.export.metadata.ManagedResource(); + Map map = ann.asMap(); + List list = new ArrayList<>(map.size()); + map.forEach((attrName, attrValue) -> { + if (!"value".equals(attrName)) { + Object value = attrValue; + if (this.embeddedValueResolver != null && value instanceof String) { + value = this.embeddedValueResolver.resolveStringValue((String) value); + } + list.add(new PropertyValue(attrName, value)); + } + }); + PropertyAccessorFactory.forBeanPropertyAccess(bean).setPropertyValues(new MutablePropertyValues(list)); + return bean; + } + + @Override + @Nullable + public org.springframework.jmx.export.metadata.ManagedAttribute getManagedAttribute(Method method) throws InvalidMetadataException { + MergedAnnotation ann = MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY) + .get(ManagedAttribute.class).withNonMergedAttributes(); + if (!ann.isPresent()) { + return null; + } + + org.springframework.jmx.export.metadata.ManagedAttribute bean = new org.springframework.jmx.export.metadata.ManagedAttribute(); + Map map = ann.asMap(); + MutablePropertyValues pvs = new MutablePropertyValues(map); + pvs.removePropertyValue("defaultValue"); + PropertyAccessorFactory.forBeanPropertyAccess(bean).setPropertyValues(pvs); + String defaultValue = (String) map.get("defaultValue"); + if (defaultValue.length() > 0) { + bean.setDefaultValue(defaultValue); + } + return bean; + } + + @Override + @Nullable + public org.springframework.jmx.export.metadata.ManagedMetric getManagedMetric(Method method) throws InvalidMetadataException { + MergedAnnotation ann = MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY) + .get(ManagedMetric.class).withNonMergedAttributes(); + + return copyPropertiesToBean(ann, org.springframework.jmx.export.metadata.ManagedMetric.class); + } + + @Override + @Nullable + public org.springframework.jmx.export.metadata.ManagedOperation getManagedOperation(Method method) throws InvalidMetadataException { + MergedAnnotation ann = MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY) + .get(ManagedOperation.class).withNonMergedAttributes(); + + return copyPropertiesToBean(ann, org.springframework.jmx.export.metadata.ManagedOperation.class); + } + + @Override + public org.springframework.jmx.export.metadata.ManagedOperationParameter[] getManagedOperationParameters(Method method) + throws InvalidMetadataException { + + List> anns = getRepeatableAnnotations( + method, ManagedOperationParameter.class, ManagedOperationParameters.class); + + return copyPropertiesToBeanArray(anns, org.springframework.jmx.export.metadata.ManagedOperationParameter.class); + } + + @Override + public org.springframework.jmx.export.metadata.ManagedNotification[] getManagedNotifications(Class clazz) + throws InvalidMetadataException { + + List> anns = getRepeatableAnnotations( + clazz, ManagedNotification.class, ManagedNotifications.class); + + return copyPropertiesToBeanArray(anns, org.springframework.jmx.export.metadata.ManagedNotification.class); + } + + + private static List> getRepeatableAnnotations( + AnnotatedElement annotatedElement, Class annotationType, + Class containerAnnotationType) { + + return MergedAnnotations.from(annotatedElement, SearchStrategy.TYPE_HIERARCHY, + RepeatableContainers.of(annotationType, containerAnnotationType)) + .stream(annotationType) + .filter(MergedAnnotationPredicates.firstRunOf(MergedAnnotation::getAggregateIndex)) + .map(MergedAnnotation::withNonMergedAttributes) + .collect(Collectors.toList()); + } + + @SuppressWarnings("unchecked") + private static T[] copyPropertiesToBeanArray( + List> anns, Class beanClass) { + + T[] beans = (T[]) Array.newInstance(beanClass, anns.size()); + int i = 0; + for (MergedAnnotation ann : anns) { + beans[i++] = copyPropertiesToBean(ann, beanClass); + } + return beans; + } + + @Nullable + private static T copyPropertiesToBean(MergedAnnotation ann, Class beanClass) { + if (!ann.isPresent()) { + return null; + } + T bean = BeanUtils.instantiateClass(beanClass); + BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(bean); + bw.setPropertyValues(new MutablePropertyValues(ann.asMap())); + return bean; + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/annotation/AnnotationMBeanExporter.java b/spring-context/src/main/java/org/springframework/jmx/export/annotation/AnnotationMBeanExporter.java new file mode 100644 index 0000000..7ef7418 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/annotation/AnnotationMBeanExporter.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.annotation; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.jmx.export.MBeanExporter; +import org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler; +import org.springframework.jmx.export.naming.MetadataNamingStrategy; + +/** + * Convenient subclass of Spring's standard {@link MBeanExporter}, + * activating Java 5 annotation usage for JMX exposure of Spring beans: + * {@link ManagedResource}, {@link ManagedAttribute}, {@link ManagedOperation}, etc. + * + *

    Sets a {@link MetadataNamingStrategy} and a {@link MetadataMBeanInfoAssembler} + * with an {@link AnnotationJmxAttributeSource}, and activates the + * {@link #AUTODETECT_ALL} mode by default. + * + * @author Juergen Hoeller + * @since 2.5 + */ +public class AnnotationMBeanExporter extends MBeanExporter { + + private final AnnotationJmxAttributeSource annotationSource = + new AnnotationJmxAttributeSource(); + + private final MetadataNamingStrategy metadataNamingStrategy = + new MetadataNamingStrategy(this.annotationSource); + + private final MetadataMBeanInfoAssembler metadataAssembler = + new MetadataMBeanInfoAssembler(this.annotationSource); + + + public AnnotationMBeanExporter() { + setNamingStrategy(this.metadataNamingStrategy); + setAssembler(this.metadataAssembler); + setAutodetectMode(AUTODETECT_ALL); + } + + + /** + * Specify the default domain to be used for generating ObjectNames + * when no source-level metadata has been specified. + *

    The default is to use the domain specified in the bean name + * (if the bean name follows the JMX ObjectName syntax); else, + * the package name of the managed bean class. + * @see MetadataNamingStrategy#setDefaultDomain + */ + public void setDefaultDomain(String defaultDomain) { + this.metadataNamingStrategy.setDefaultDomain(defaultDomain); + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + super.setBeanFactory(beanFactory); + this.annotationSource.setBeanFactory(beanFactory); + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedAttribute.java b/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedAttribute.java new file mode 100644 index 0000000..1d233ca --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedAttribute.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Method-level annotation that indicates to expose a given bean property as a + * JMX attribute, corresponding to the + * {@link org.springframework.jmx.export.metadata.ManagedAttribute}. + * + *

    Only valid when used on a JavaBean getter or setter. + * + * @author Rob Harrop + * @since 1.2 + * @see org.springframework.jmx.export.metadata.ManagedAttribute + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ManagedAttribute { + + /** + * Set the default value for the attribute in a {@link javax.management.Descriptor}. + */ + String defaultValue() default ""; + + /** + * Set the description for the attribute in a {@link javax.management.Descriptor}. + */ + String description() default ""; + + /** + * Set the currency time limit field in a {@link javax.management.Descriptor}. + */ + int currencyTimeLimit() default -1; + + /** + * Set the persistPolicy field in a {@link javax.management.Descriptor}. + */ + String persistPolicy() default ""; + + /** + * Set the persistPeriod field in a {@link javax.management.Descriptor}. + */ + int persistPeriod() default -1; + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedMetric.java b/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedMetric.java new file mode 100644 index 0000000..2912aa7 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedMetric.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.jmx.support.MetricType; + +/** + * Method-level annotation that indicates to expose a given bean property as a + * JMX attribute, with added descriptor properties to indicate that it is a metric. + * Only valid when used on a JavaBean getter. + * + * @author Jennifer Hickey + * @since 3.0 + * @see org.springframework.jmx.export.metadata.ManagedMetric + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ManagedMetric { + + String category() default ""; + + int currencyTimeLimit() default -1; + + String description() default ""; + + String displayName() default ""; + + MetricType metricType() default MetricType.GAUGE; + + int persistPeriod() default -1; + + String persistPolicy() default ""; + + String unit() default ""; + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedNotification.java b/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedNotification.java new file mode 100644 index 0000000..1a4f933 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedNotification.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Type-level annotation that indicates a JMX notification emitted by a bean. + * + *

    As of Spring Framework 4.2.4, this annotation is declared as repeatable. + * + * @author Rob Harrop + * @since 2.0 + * @see org.springframework.jmx.export.metadata.ManagedNotification + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +@Repeatable(ManagedNotifications.class) +public @interface ManagedNotification { + + String name(); + + String description() default ""; + + String[] notificationTypes(); + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedNotifications.java b/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedNotifications.java new file mode 100644 index 0000000..3ba09dd --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedNotifications.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Type-level annotation that indicates JMX notifications emitted by a bean, + * containing multiple {@link ManagedNotification ManagedNotifications}. + * + * @author Rob Harrop + * @since 2.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface ManagedNotifications { + + ManagedNotification[] value(); + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedOperation.java b/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedOperation.java new file mode 100644 index 0000000..536f01d --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedOperation.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Method-level annotation that indicates to expose a given method as a + * JMX operation, corresponding to the {@code ManagedOperation} attribute. + * Only valid when used on a method that is not a JavaBean getter or setter. + * + * @author Rob Harrop + * @since 1.2 + * @see org.springframework.jmx.export.metadata.ManagedOperation + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ManagedOperation { + + String description() default ""; + + int currencyTimeLimit() default -1; + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedOperationParameter.java b/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedOperationParameter.java new file mode 100644 index 0000000..1b98436 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedOperationParameter.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Method-level annotation used to provide metadata about operation parameters, + * corresponding to a {@code ManagedOperationParameter} attribute. + * Used as part of a {@link ManagedOperationParameters} annotation. + * + *

    As of Spring Framework 4.2.4, this annotation is declared as repeatable. + * + * @author Rob Harrop + * @since 1.2 + * @see ManagedOperationParameters#value + * @see org.springframework.jmx.export.metadata.ManagedOperationParameter + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Repeatable(ManagedOperationParameters.class) +public @interface ManagedOperationParameter { + + String name(); + + String description(); + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedOperationParameters.java b/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedOperationParameters.java new file mode 100644 index 0000000..ab9b6b0 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedOperationParameters.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Method-level annotation used to provide metadata about operation parameters, + * corresponding to an array of {@code ManagedOperationParameter} attributes. + * + * @author Rob Harrop + * @since 1.2 + * @see org.springframework.jmx.export.metadata.ManagedOperationParameter + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ManagedOperationParameters { + + ManagedOperationParameter[] value() default {}; + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedResource.java b/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedResource.java new file mode 100644 index 0000000..6535f44 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/annotation/ManagedResource.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * Class-level annotation that indicates to register instances of a class + * with a JMX server, corresponding to the {@code ManagedResource} attribute. + * + *

    Note: This annotation is marked as inherited, allowing for generic + * management-aware base classes. In such a scenario, it is recommended to + * not specify an object name value since this would lead to naming + * collisions in case of multiple subclasses getting registered. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Sam Brannen + * @since 1.2 + * @see org.springframework.jmx.export.metadata.ManagedResource + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface ManagedResource { + + /** + * Alias for the {@link #objectName} attribute, for simple default usage. + */ + @AliasFor("objectName") + String value() default ""; + + @AliasFor("value") + String objectName() default ""; + + String description() default ""; + + int currencyTimeLimit() default -1; + + boolean log() default false; + + String logFile() default ""; + + String persistPolicy() default ""; + + int persistPeriod() default -1; + + String persistName() default ""; + + String persistLocation() default ""; + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/annotation/package-info.java b/spring-context/src/main/java/org/springframework/jmx/export/annotation/package-info.java new file mode 100644 index 0000000..b413ea5 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/annotation/package-info.java @@ -0,0 +1,11 @@ +/** + * Java 5 annotations for MBean exposure. + * Hooked into Spring's JMX export infrastructure + * via a special JmxAttributeSource implementation. + */ +@NonNullApi +@NonNullFields +package org.springframework.jmx.export.annotation; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/jmx/export/assembler/AbstractConfigurableMBeanInfoAssembler.java b/spring-context/src/main/java/org/springframework/jmx/export/assembler/AbstractConfigurableMBeanInfoAssembler.java new file mode 100644 index 0000000..4ebcbc4 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/assembler/AbstractConfigurableMBeanInfoAssembler.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.assembler; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.management.modelmbean.ModelMBeanNotificationInfo; + +import org.springframework.jmx.export.metadata.JmxMetadataUtils; +import org.springframework.jmx.export.metadata.ManagedNotification; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Base class for MBeanInfoAssemblers that support configurable + * JMX notification behavior. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +public abstract class AbstractConfigurableMBeanInfoAssembler extends AbstractReflectiveMBeanInfoAssembler { + + @Nullable + private ModelMBeanNotificationInfo[] notificationInfos; + + private final Map notificationInfoMappings = new HashMap<>(); + + + public void setNotificationInfos(ManagedNotification[] notificationInfos) { + ModelMBeanNotificationInfo[] infos = new ModelMBeanNotificationInfo[notificationInfos.length]; + for (int i = 0; i < notificationInfos.length; i++) { + ManagedNotification notificationInfo = notificationInfos[i]; + infos[i] = JmxMetadataUtils.convertToModelMBeanNotificationInfo(notificationInfo); + } + this.notificationInfos = infos; + } + + public void setNotificationInfoMappings(Map notificationInfoMappings) { + notificationInfoMappings.forEach((beanKey, result) -> + this.notificationInfoMappings.put(beanKey, extractNotificationMetadata(result))); + } + + + @Override + protected ModelMBeanNotificationInfo[] getNotificationInfo(Object managedBean, String beanKey) { + ModelMBeanNotificationInfo[] result = null; + if (StringUtils.hasText(beanKey)) { + result = this.notificationInfoMappings.get(beanKey); + } + if (result == null) { + result = this.notificationInfos; + } + return (result != null ? result : new ModelMBeanNotificationInfo[0]); + } + + private ModelMBeanNotificationInfo[] extractNotificationMetadata(Object mapValue) { + if (mapValue instanceof ManagedNotification) { + ManagedNotification mn = (ManagedNotification) mapValue; + return new ModelMBeanNotificationInfo[] {JmxMetadataUtils.convertToModelMBeanNotificationInfo(mn)}; + } + else if (mapValue instanceof Collection) { + Collection col = (Collection) mapValue; + List result = new ArrayList<>(); + for (Object colValue : col) { + if (!(colValue instanceof ManagedNotification)) { + throw new IllegalArgumentException( + "Property 'notificationInfoMappings' only accepts ManagedNotifications for Map values"); + } + ManagedNotification mn = (ManagedNotification) colValue; + result.add(JmxMetadataUtils.convertToModelMBeanNotificationInfo(mn)); + } + return result.toArray(new ModelMBeanNotificationInfo[0]); + } + else { + throw new IllegalArgumentException( + "Property 'notificationInfoMappings' only accepts ManagedNotifications for Map values"); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/assembler/AbstractMBeanInfoAssembler.java b/spring-context/src/main/java/org/springframework/jmx/export/assembler/AbstractMBeanInfoAssembler.java new file mode 100644 index 0000000..d302d10 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/assembler/AbstractMBeanInfoAssembler.java @@ -0,0 +1,226 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.assembler; + +import javax.management.Descriptor; +import javax.management.JMException; +import javax.management.modelmbean.ModelMBeanAttributeInfo; +import javax.management.modelmbean.ModelMBeanConstructorInfo; +import javax.management.modelmbean.ModelMBeanInfo; +import javax.management.modelmbean.ModelMBeanInfoSupport; +import javax.management.modelmbean.ModelMBeanNotificationInfo; +import javax.management.modelmbean.ModelMBeanOperationInfo; + +import org.springframework.aop.support.AopUtils; +import org.springframework.jmx.support.JmxUtils; + +/** + * Abstract implementation of the {@code MBeanInfoAssembler} interface + * that encapsulates the creation of a {@code ModelMBeanInfo} instance + * but delegates the creation of metadata to subclasses. + * + *

    This class offers two flavors of Class extraction from a managed bean + * instance: {@link #getTargetClass}, extracting the target class behind + * any kind of AOP proxy, and {@link #getClassToExpose}, returning the + * class or interface that will be searched for annotations and exposed + * to the JMX runtime. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + */ +public abstract class AbstractMBeanInfoAssembler implements MBeanInfoAssembler { + + /** + * Create an instance of the {@code ModelMBeanInfoSupport} class supplied with all + * JMX implementations and populates the metadata through calls to the subclass. + * @param managedBean the bean that will be exposed (might be an AOP proxy) + * @param beanKey the key associated with the managed bean + * @return the populated ModelMBeanInfo instance + * @throws JMException in case of errors + * @see #getDescription(Object, String) + * @see #getAttributeInfo(Object, String) + * @see #getConstructorInfo(Object, String) + * @see #getOperationInfo(Object, String) + * @see #getNotificationInfo(Object, String) + * @see #populateMBeanDescriptor(javax.management.Descriptor, Object, String) + */ + @Override + public ModelMBeanInfo getMBeanInfo(Object managedBean, String beanKey) throws JMException { + checkManagedBean(managedBean); + ModelMBeanInfo info = new ModelMBeanInfoSupport( + getClassName(managedBean, beanKey), getDescription(managedBean, beanKey), + getAttributeInfo(managedBean, beanKey), getConstructorInfo(managedBean, beanKey), + getOperationInfo(managedBean, beanKey), getNotificationInfo(managedBean, beanKey)); + Descriptor desc = info.getMBeanDescriptor(); + populateMBeanDescriptor(desc, managedBean, beanKey); + info.setMBeanDescriptor(desc); + return info; + } + + /** + * Check the given bean instance, throwing an IllegalArgumentException + * if it is not eligible for exposure with this assembler. + *

    Default implementation is empty, accepting every bean instance. + * @param managedBean the bean that will be exposed (might be an AOP proxy) + * @throws IllegalArgumentException the bean is not valid for exposure + */ + protected void checkManagedBean(Object managedBean) throws IllegalArgumentException { + } + + /** + * Return the actual bean class of the given bean instance. + * This is the class exposed to description-style JMX properties. + *

    Default implementation returns the target class for an AOP proxy, + * and the plain bean class else. + * @param managedBean the bean instance (might be an AOP proxy) + * @return the bean class to expose + * @see org.springframework.aop.support.AopUtils#getTargetClass(Object) + */ + protected Class getTargetClass(Object managedBean) { + return AopUtils.getTargetClass(managedBean); + } + + /** + * Return the class or interface to expose for the given bean. + * This is the class that will be searched for attributes and operations + * (for example, checked for annotations). + * @param managedBean the bean instance (might be an AOP proxy) + * @return the bean class to expose + * @see JmxUtils#getClassToExpose(Object) + */ + protected Class getClassToExpose(Object managedBean) { + return JmxUtils.getClassToExpose(managedBean); + } + + /** + * Return the class or interface to expose for the given bean class. + * This is the class that will be searched for attributes and operations + * @param beanClass the bean class (might be an AOP proxy class) + * @return the bean class to expose + * @see JmxUtils#getClassToExpose(Class) + */ + protected Class getClassToExpose(Class beanClass) { + return JmxUtils.getClassToExpose(beanClass); + } + + /** + * Get the class name of the MBean resource. + *

    Default implementation returns a simple description for the MBean + * based on the class name. + * @param managedBean the bean instance (might be an AOP proxy) + * @param beanKey the key associated with the MBean in the beans map + * of the {@code MBeanExporter} + * @return the MBean description + * @throws JMException in case of errors + */ + protected String getClassName(Object managedBean, String beanKey) throws JMException { + return getTargetClass(managedBean).getName(); + } + + /** + * Get the description of the MBean resource. + *

    Default implementation returns a simple description for the MBean + * based on the class name. + * @param managedBean the bean instance (might be an AOP proxy) + * @param beanKey the key associated with the MBean in the beans map + * of the {@code MBeanExporter} + * @throws JMException in case of errors + */ + protected String getDescription(Object managedBean, String beanKey) throws JMException { + String targetClassName = getTargetClass(managedBean).getName(); + if (AopUtils.isAopProxy(managedBean)) { + return "Proxy for " + targetClassName; + } + return targetClassName; + } + + /** + * Called after the {@code ModelMBeanInfo} instance has been constructed but + * before it is passed to the {@code MBeanExporter}. + *

    Subclasses can implement this method to add additional descriptors to the + * MBean metadata. Default implementation is empty. + * @param descriptor the {@code Descriptor} for the MBean resource. + * @param managedBean the bean instance (might be an AOP proxy) + * @param beanKey the key associated with the MBean in the beans map + * of the {@code MBeanExporter} + * @throws JMException in case of errors + */ + protected void populateMBeanDescriptor(Descriptor descriptor, Object managedBean, String beanKey) + throws JMException { + } + + /** + * Get the constructor metadata for the MBean resource. Subclasses should implement + * this method to return the appropriate metadata for all constructors that should + * be exposed in the management interface for the managed resource. + *

    Default implementation returns an empty array of {@code ModelMBeanConstructorInfo}. + * @param managedBean the bean instance (might be an AOP proxy) + * @param beanKey the key associated with the MBean in the beans map + * of the {@code MBeanExporter} + * @return the constructor metadata + * @throws JMException in case of errors + */ + protected ModelMBeanConstructorInfo[] getConstructorInfo(Object managedBean, String beanKey) + throws JMException { + return new ModelMBeanConstructorInfo[0]; + } + + /** + * Get the notification metadata for the MBean resource. Subclasses should implement + * this method to return the appropriate metadata for all notifications that should + * be exposed in the management interface for the managed resource. + *

    Default implementation returns an empty array of {@code ModelMBeanNotificationInfo}. + * @param managedBean the bean instance (might be an AOP proxy) + * @param beanKey the key associated with the MBean in the beans map + * of the {@code MBeanExporter} + * @return the notification metadata + * @throws JMException in case of errors + */ + protected ModelMBeanNotificationInfo[] getNotificationInfo(Object managedBean, String beanKey) + throws JMException { + return new ModelMBeanNotificationInfo[0]; + } + + + /** + * Get the attribute metadata for the MBean resource. Subclasses should implement + * this method to return the appropriate metadata for all the attributes that should + * be exposed in the management interface for the managed resource. + * @param managedBean the bean instance (might be an AOP proxy) + * @param beanKey the key associated with the MBean in the beans map + * of the {@code MBeanExporter} + * @return the attribute metadata + * @throws JMException in case of errors + */ + protected abstract ModelMBeanAttributeInfo[] getAttributeInfo(Object managedBean, String beanKey) + throws JMException; + + /** + * Get the operation metadata for the MBean resource. Subclasses should implement + * this method to return the appropriate metadata for all operations that should + * be exposed in the management interface for the managed resource. + * @param managedBean the bean instance (might be an AOP proxy) + * @param beanKey the key associated with the MBean in the beans map + * of the {@code MBeanExporter} + * @return the operation metadata + * @throws JMException in case of errors + */ + protected abstract ModelMBeanOperationInfo[] getOperationInfo(Object managedBean, String beanKey) + throws JMException; + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/assembler/AbstractReflectiveMBeanInfoAssembler.java b/spring-context/src/main/java/org/springframework/jmx/export/assembler/AbstractReflectiveMBeanInfoAssembler.java new file mode 100644 index 0000000..b3ec662 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/assembler/AbstractReflectiveMBeanInfoAssembler.java @@ -0,0 +1,617 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.assembler; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import javax.management.Descriptor; +import javax.management.JMException; +import javax.management.MBeanOperationInfo; +import javax.management.MBeanParameterInfo; +import javax.management.modelmbean.ModelMBeanAttributeInfo; +import javax.management.modelmbean.ModelMBeanOperationInfo; + +import org.springframework.aop.framework.AopProxyUtils; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.jmx.support.JmxUtils; +import org.springframework.lang.Nullable; + +/** + * Builds on the {@link AbstractMBeanInfoAssembler} superclass to + * add a basic algorithm for building metadata based on the + * reflective metadata of the MBean class. + * + *

    The logic for creating MBean metadata from the reflective metadata + * is contained in this class, but this class makes no decisions as to + * which methods and properties are to be exposed. Instead it gives + * subclasses a chance to 'vote' on each property or method through + * the {@code includeXXX} methods. + * + *

    Subclasses are also given the opportunity to populate attribute + * and operation metadata with additional descriptors once the metadata + * is assembled through the {@code populateXXXDescriptor} methods. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author David Boden + * @since 1.2 + * @see #includeOperation + * @see #includeReadAttribute + * @see #includeWriteAttribute + * @see #populateAttributeDescriptor + * @see #populateOperationDescriptor + */ +public abstract class AbstractReflectiveMBeanInfoAssembler extends AbstractMBeanInfoAssembler { + + /** + * Identifies a getter method in a JMX {@link Descriptor}. + */ + protected static final String FIELD_GET_METHOD = "getMethod"; + + /** + * Identifies a setter method in a JMX {@link Descriptor}. + */ + protected static final String FIELD_SET_METHOD = "setMethod"; + + /** + * Constant identifier for the role field in a JMX {@link Descriptor}. + */ + protected static final String FIELD_ROLE = "role"; + + /** + * Constant identifier for the getter role field value in a JMX {@link Descriptor}. + */ + protected static final String ROLE_GETTER = "getter"; + + /** + * Constant identifier for the setter role field value in a JMX {@link Descriptor}. + */ + protected static final String ROLE_SETTER = "setter"; + + /** + * Identifies an operation (method) in a JMX {@link Descriptor}. + */ + protected static final String ROLE_OPERATION = "operation"; + + /** + * Constant identifier for the visibility field in a JMX {@link Descriptor}. + */ + protected static final String FIELD_VISIBILITY = "visibility"; + + /** + * Lowest visibility, used for operations that correspond to + * accessors or mutators for attributes. + * @see #FIELD_VISIBILITY + */ + protected static final int ATTRIBUTE_OPERATION_VISIBILITY = 4; + + /** + * Constant identifier for the class field in a JMX {@link Descriptor}. + */ + protected static final String FIELD_CLASS = "class"; + /** + * Constant identifier for the log field in a JMX {@link Descriptor}. + */ + protected static final String FIELD_LOG = "log"; + + /** + * Constant identifier for the logfile field in a JMX {@link Descriptor}. + */ + protected static final String FIELD_LOG_FILE = "logFile"; + + /** + * Constant identifier for the currency time limit field in a JMX {@link Descriptor}. + */ + protected static final String FIELD_CURRENCY_TIME_LIMIT = "currencyTimeLimit"; + + /** + * Constant identifier for the default field in a JMX {@link Descriptor}. + */ + protected static final String FIELD_DEFAULT = "default"; + + /** + * Constant identifier for the persistPolicy field in a JMX {@link Descriptor}. + */ + protected static final String FIELD_PERSIST_POLICY = "persistPolicy"; + + /** + * Constant identifier for the persistPeriod field in a JMX {@link Descriptor}. + */ + protected static final String FIELD_PERSIST_PERIOD = "persistPeriod"; + + /** + * Constant identifier for the persistLocation field in a JMX {@link Descriptor}. + */ + protected static final String FIELD_PERSIST_LOCATION = "persistLocation"; + + /** + * Constant identifier for the persistName field in a JMX {@link Descriptor}. + */ + protected static final String FIELD_PERSIST_NAME = "persistName"; + + /** + * Constant identifier for the displayName field in a JMX {@link Descriptor}. + */ + protected static final String FIELD_DISPLAY_NAME = "displayName"; + + /** + * Constant identifier for the units field in a JMX {@link Descriptor}. + */ + protected static final String FIELD_UNITS = "units"; + + /** + * Constant identifier for the metricType field in a JMX {@link Descriptor}. + */ + protected static final String FIELD_METRIC_TYPE = "metricType"; + + /** + * Constant identifier for the custom metricCategory field in a JMX {@link Descriptor}. + */ + protected static final String FIELD_METRIC_CATEGORY = "metricCategory"; + + + /** + * Default value for the JMX field "currencyTimeLimit". + */ + @Nullable + private Integer defaultCurrencyTimeLimit; + + /** + * Indicates whether or not strict casing is being used for attributes. + */ + private boolean useStrictCasing = true; + + private boolean exposeClassDescriptor = false; + + @Nullable + private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + + + /** + * Set the default for the JMX field "currencyTimeLimit". + * The default will usually indicate to never cache attribute values. + *

    Default is none, not explicitly setting that field, as recommended by the + * JMX 1.2 specification. This should result in "never cache" behavior, always + * reading attribute values freshly (which corresponds to a "currencyTimeLimit" + * of {@code -1} in JMX 1.2). + *

    However, some JMX implementations (that do not follow the JMX 1.2 spec + * in that respect) might require an explicit value to be set here to get + * "never cache" behavior: for example, JBoss 3.2.x. + *

    Note that the "currencyTimeLimit" value can also be specified on a + * managed attribute or operation. The default value will apply if not + * overridden with a "currencyTimeLimit" value {@code >= 0} there: + * a metadata "currencyTimeLimit" value of {@code -1} indicates + * to use the default; a value of {@code 0} indicates to "always cache" + * and will be translated to {@code Integer.MAX_VALUE}; a positive + * value indicates the number of cache seconds. + * @see org.springframework.jmx.export.metadata.AbstractJmxAttribute#setCurrencyTimeLimit + * @see #applyCurrencyTimeLimit(javax.management.Descriptor, int) + */ + public void setDefaultCurrencyTimeLimit(@Nullable Integer defaultCurrencyTimeLimit) { + this.defaultCurrencyTimeLimit = defaultCurrencyTimeLimit; + } + + /** + * Return default value for the JMX field "currencyTimeLimit", if any. + */ + @Nullable + protected Integer getDefaultCurrencyTimeLimit() { + return this.defaultCurrencyTimeLimit; + } + + /** + * Set whether to use strict casing for attributes. Enabled by default. + *

    When using strict casing, a JavaBean property with a getter such as + * {@code getFoo()} translates to an attribute called {@code Foo}. + * With strict casing disabled, {@code getFoo()} would translate to just + * {@code foo}. + */ + public void setUseStrictCasing(boolean useStrictCasing) { + this.useStrictCasing = useStrictCasing; + } + + /** + * Return whether strict casing for attributes is enabled. + */ + protected boolean isUseStrictCasing() { + return this.useStrictCasing; + } + + /** + * Set whether to expose the JMX descriptor field "class" for managed operations. + * Default is "false", letting the JMX implementation determine the actual class + * through reflection. + *

    Set this property to {@code true} for JMX implementations that + * require the "class" field to be specified, for example WebLogic's. + * In that case, Spring will expose the target class name there, in case of + * a plain bean instance or a CGLIB proxy. When encountering a JDK dynamic + * proxy, the first interface implemented by the proxy will be specified. + *

    WARNING: Review your proxy definitions when exposing a JDK dynamic + * proxy through JMX, in particular with this property turned to {@code true}: + * the specified interface list should start with your management interface in + * this case, with all other interfaces following. In general, consider exposing + * your target bean directly or a CGLIB proxy for it instead. + * @see #getClassForDescriptor(Object) + */ + public void setExposeClassDescriptor(boolean exposeClassDescriptor) { + this.exposeClassDescriptor = exposeClassDescriptor; + } + + /** + * Return whether to expose the JMX descriptor field "class" for managed operations. + */ + protected boolean isExposeClassDescriptor() { + return this.exposeClassDescriptor; + } + + /** + * Set the ParameterNameDiscoverer to use for resolving method parameter + * names if needed (e.g. for parameter names of MBean operation methods). + *

    Default is a {@link DefaultParameterNameDiscoverer}. + */ + public void setParameterNameDiscoverer(@Nullable ParameterNameDiscoverer parameterNameDiscoverer) { + this.parameterNameDiscoverer = parameterNameDiscoverer; + } + + /** + * Return the ParameterNameDiscoverer to use for resolving method parameter + * names if needed (may be {@code null} in order to skip parameter detection). + */ + @Nullable + protected ParameterNameDiscoverer getParameterNameDiscoverer() { + return this.parameterNameDiscoverer; + } + + + /** + * Iterate through all properties on the MBean class and gives subclasses + * the chance to vote on the inclusion of both the accessor and mutator. + * If a particular accessor or mutator is voted for inclusion, the appropriate + * metadata is assembled and passed to the subclass for descriptor population. + * @param managedBean the bean instance (might be an AOP proxy) + * @param beanKey the key associated with the MBean in the beans map + * of the {@code MBeanExporter} + * @return the attribute metadata + * @throws JMException in case of errors + * @see #populateAttributeDescriptor + */ + @Override + protected ModelMBeanAttributeInfo[] getAttributeInfo(Object managedBean, String beanKey) throws JMException { + PropertyDescriptor[] props = BeanUtils.getPropertyDescriptors(getClassToExpose(managedBean)); + List infos = new ArrayList<>(); + + for (PropertyDescriptor prop : props) { + Method getter = prop.getReadMethod(); + if (getter != null && getter.getDeclaringClass() == Object.class) { + continue; + } + if (getter != null && !includeReadAttribute(getter, beanKey)) { + getter = null; + } + + Method setter = prop.getWriteMethod(); + if (setter != null && !includeWriteAttribute(setter, beanKey)) { + setter = null; + } + + if (getter != null || setter != null) { + // If both getter and setter are null, then this does not need exposing. + String attrName = JmxUtils.getAttributeName(prop, isUseStrictCasing()); + String description = getAttributeDescription(prop, beanKey); + ModelMBeanAttributeInfo info = new ModelMBeanAttributeInfo(attrName, description, getter, setter); + + Descriptor desc = info.getDescriptor(); + if (getter != null) { + desc.setField(FIELD_GET_METHOD, getter.getName()); + } + if (setter != null) { + desc.setField(FIELD_SET_METHOD, setter.getName()); + } + + populateAttributeDescriptor(desc, getter, setter, beanKey); + info.setDescriptor(desc); + infos.add(info); + } + } + + return infos.toArray(new ModelMBeanAttributeInfo[0]); + } + + /** + * Iterate through all methods on the MBean class and gives subclasses the chance + * to vote on their inclusion. If a particular method corresponds to the accessor + * or mutator of an attribute that is included in the management interface, then + * the corresponding operation is exposed with the "role" descriptor + * field set to the appropriate value. + * @param managedBean the bean instance (might be an AOP proxy) + * @param beanKey the key associated with the MBean in the beans map + * of the {@code MBeanExporter} + * @return the operation metadata + * @see #populateOperationDescriptor + */ + @Override + protected ModelMBeanOperationInfo[] getOperationInfo(Object managedBean, String beanKey) { + Method[] methods = getClassToExpose(managedBean).getMethods(); + List infos = new ArrayList<>(); + + for (Method method : methods) { + if (method.isSynthetic()) { + continue; + } + if (Object.class == method.getDeclaringClass()) { + continue; + } + + ModelMBeanOperationInfo info = null; + PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method); + if (pd != null && ((method.equals(pd.getReadMethod()) && includeReadAttribute(method, beanKey)) || + (method.equals(pd.getWriteMethod()) && includeWriteAttribute(method, beanKey)))) { + // Attributes need to have their methods exposed as + // operations to the JMX server as well. + info = createModelMBeanOperationInfo(method, pd.getName(), beanKey); + Descriptor desc = info.getDescriptor(); + if (method.equals(pd.getReadMethod())) { + desc.setField(FIELD_ROLE, ROLE_GETTER); + } + else { + desc.setField(FIELD_ROLE, ROLE_SETTER); + } + desc.setField(FIELD_VISIBILITY, ATTRIBUTE_OPERATION_VISIBILITY); + if (isExposeClassDescriptor()) { + desc.setField(FIELD_CLASS, getClassForDescriptor(managedBean).getName()); + } + info.setDescriptor(desc); + } + + // allow getters and setters to be marked as operations directly + if (info == null && includeOperation(method, beanKey)) { + info = createModelMBeanOperationInfo(method, method.getName(), beanKey); + Descriptor desc = info.getDescriptor(); + desc.setField(FIELD_ROLE, ROLE_OPERATION); + if (isExposeClassDescriptor()) { + desc.setField(FIELD_CLASS, getClassForDescriptor(managedBean).getName()); + } + populateOperationDescriptor(desc, method, beanKey); + info.setDescriptor(desc); + } + + if (info != null) { + infos.add(info); + } + } + + return infos.toArray(new ModelMBeanOperationInfo[0]); + } + + /** + * Creates an instance of {@code ModelMBeanOperationInfo} for the + * given method. Populates the parameter info for the operation. + * @param method the {@code Method} to create a {@code ModelMBeanOperationInfo} for + * @param name the logical name for the operation (method name or property name); + * not used by the default implementation but possibly by subclasses + * @param beanKey the key associated with the MBean in the beans map + * of the {@code MBeanExporter} + * @return the {@code ModelMBeanOperationInfo} + */ + protected ModelMBeanOperationInfo createModelMBeanOperationInfo(Method method, String name, String beanKey) { + MBeanParameterInfo[] params = getOperationParameters(method, beanKey); + if (params.length == 0) { + return new ModelMBeanOperationInfo(getOperationDescription(method, beanKey), method); + } + else { + return new ModelMBeanOperationInfo(method.getName(), + getOperationDescription(method, beanKey), + getOperationParameters(method, beanKey), + method.getReturnType().getName(), + MBeanOperationInfo.UNKNOWN); + } + } + + /** + * Return the class to be used for the JMX descriptor field "class". + * Only applied when the "exposeClassDescriptor" property is "true". + *

    The default implementation returns the first implemented interface + * for a JDK proxy, and the target class else. + * @param managedBean the bean instance (might be an AOP proxy) + * @return the class to expose in the descriptor field "class" + * @see #setExposeClassDescriptor + * @see #getClassToExpose(Class) + * @see org.springframework.aop.framework.AopProxyUtils#proxiedUserInterfaces(Object) + */ + protected Class getClassForDescriptor(Object managedBean) { + if (AopUtils.isJdkDynamicProxy(managedBean)) { + return AopProxyUtils.proxiedUserInterfaces(managedBean)[0]; + } + return getClassToExpose(managedBean); + } + + + /** + * Allows subclasses to vote on the inclusion of a particular attribute accessor. + * @param method the accessor {@code Method} + * @param beanKey the key associated with the MBean in the beans map + * of the {@code MBeanExporter} + * @return {@code true} if the accessor should be included in the management interface, + * otherwise {@code false} + */ + protected abstract boolean includeReadAttribute(Method method, String beanKey); + + /** + * Allows subclasses to vote on the inclusion of a particular attribute mutator. + * @param method the mutator {@code Method}. + * @param beanKey the key associated with the MBean in the beans map + * of the {@code MBeanExporter} + * @return {@code true} if the mutator should be included in the management interface, + * otherwise {@code false} + */ + protected abstract boolean includeWriteAttribute(Method method, String beanKey); + + /** + * Allows subclasses to vote on the inclusion of a particular operation. + * @param method the operation method + * @param beanKey the key associated with the MBean in the beans map + * of the {@code MBeanExporter} + * @return whether the operation should be included in the management interface + */ + protected abstract boolean includeOperation(Method method, String beanKey); + + /** + * Get the description for a particular attribute. + *

    The default implementation returns a description for the operation + * that is the name of corresponding {@code Method}. + * @param propertyDescriptor the PropertyDescriptor for the attribute + * @param beanKey the key associated with the MBean in the beans map + * of the {@code MBeanExporter} + * @return the description for the attribute + */ + protected String getAttributeDescription(PropertyDescriptor propertyDescriptor, String beanKey) { + return propertyDescriptor.getDisplayName(); + } + + /** + * Get the description for a particular operation. + *

    The default implementation returns a description for the operation + * that is the name of corresponding {@code Method}. + * @param method the operation method + * @param beanKey the key associated with the MBean in the beans map + * of the {@code MBeanExporter} + * @return the description for the operation + */ + protected String getOperationDescription(Method method, String beanKey) { + return method.getName(); + } + + /** + * Create parameter info for the given method. + *

    The default implementation returns an empty array of {@code MBeanParameterInfo}. + * @param method the {@code Method} to get the parameter information for + * @param beanKey the key associated with the MBean in the beans map + * of the {@code MBeanExporter} + * @return the {@code MBeanParameterInfo} array + */ + protected MBeanParameterInfo[] getOperationParameters(Method method, String beanKey) { + ParameterNameDiscoverer paramNameDiscoverer = getParameterNameDiscoverer(); + String[] paramNames = (paramNameDiscoverer != null ? paramNameDiscoverer.getParameterNames(method) : null); + if (paramNames == null) { + return new MBeanParameterInfo[0]; + } + + MBeanParameterInfo[] info = new MBeanParameterInfo[paramNames.length]; + Class[] typeParameters = method.getParameterTypes(); + for (int i = 0; i < info.length; i++) { + info[i] = new MBeanParameterInfo(paramNames[i], typeParameters[i].getName(), paramNames[i]); + } + + return info; + } + + /** + * Allows subclasses to add extra fields to the {@code Descriptor} for an MBean. + *

    The default implementation sets the {@code currencyTimeLimit} field to + * the specified "defaultCurrencyTimeLimit", if any (by default none). + * @param descriptor the {@code Descriptor} for the MBean resource. + * @param managedBean the bean instance (might be an AOP proxy) + * @param beanKey the key associated with the MBean in the beans map + * of the {@code MBeanExporter} + * @see #setDefaultCurrencyTimeLimit(Integer) + * @see #applyDefaultCurrencyTimeLimit(javax.management.Descriptor) + */ + @Override + protected void populateMBeanDescriptor(Descriptor descriptor, Object managedBean, String beanKey) { + applyDefaultCurrencyTimeLimit(descriptor); + } + + /** + * Allows subclasses to add extra fields to the {@code Descriptor} for a + * particular attribute. + *

    The default implementation sets the {@code currencyTimeLimit} field to + * the specified "defaultCurrencyTimeLimit", if any (by default none). + * @param desc the attribute descriptor + * @param getter the accessor method for the attribute + * @param setter the mutator method for the attribute + * @param beanKey the key associated with the MBean in the beans map + * of the {@code MBeanExporter} + * @see #setDefaultCurrencyTimeLimit(Integer) + * @see #applyDefaultCurrencyTimeLimit(javax.management.Descriptor) + */ + protected void populateAttributeDescriptor( + Descriptor desc, @Nullable Method getter, @Nullable Method setter, String beanKey) { + + applyDefaultCurrencyTimeLimit(desc); + } + + /** + * Allows subclasses to add extra fields to the {@code Descriptor} for a + * particular operation. + *

    The default implementation sets the {@code currencyTimeLimit} field to + * the specified "defaultCurrencyTimeLimit", if any (by default none). + * @param desc the operation descriptor + * @param method the method corresponding to the operation + * @param beanKey the key associated with the MBean in the beans map + * of the {@code MBeanExporter} + * @see #setDefaultCurrencyTimeLimit(Integer) + * @see #applyDefaultCurrencyTimeLimit(javax.management.Descriptor) + */ + protected void populateOperationDescriptor(Descriptor desc, Method method, String beanKey) { + applyDefaultCurrencyTimeLimit(desc); + } + + /** + * Set the {@code currencyTimeLimit} field to the specified + * "defaultCurrencyTimeLimit", if any (by default none). + * @param desc the JMX attribute or operation descriptor + * @see #setDefaultCurrencyTimeLimit(Integer) + */ + protected final void applyDefaultCurrencyTimeLimit(Descriptor desc) { + if (getDefaultCurrencyTimeLimit() != null) { + desc.setField(FIELD_CURRENCY_TIME_LIMIT, getDefaultCurrencyTimeLimit().toString()); + } + } + + /** + * Apply the given JMX "currencyTimeLimit" value to the given descriptor. + *

    The default implementation sets a value {@code >0} as-is (as number of cache seconds), + * turns a value of {@code 0} into {@code Integer.MAX_VALUE} ("always cache") + * and sets the "defaultCurrencyTimeLimit" (if any, indicating "never cache") in case of + * a value {@code <0}. This follows the recommendation in the JMX 1.2 specification. + * @param desc the JMX attribute or operation descriptor + * @param currencyTimeLimit the "currencyTimeLimit" value to apply + * @see #setDefaultCurrencyTimeLimit(Integer) + * @see #applyDefaultCurrencyTimeLimit(javax.management.Descriptor) + */ + protected void applyCurrencyTimeLimit(Descriptor desc, int currencyTimeLimit) { + if (currencyTimeLimit > 0) { + // number of cache seconds + desc.setField(FIELD_CURRENCY_TIME_LIMIT, Integer.toString(currencyTimeLimit)); + } + else if (currencyTimeLimit == 0) { + // "always cache" + desc.setField(FIELD_CURRENCY_TIME_LIMIT, Integer.toString(Integer.MAX_VALUE)); + } + else { + // "never cache" + applyDefaultCurrencyTimeLimit(desc); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/assembler/AutodetectCapableMBeanInfoAssembler.java b/spring-context/src/main/java/org/springframework/jmx/export/assembler/AutodetectCapableMBeanInfoAssembler.java new file mode 100644 index 0000000..a033c01 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/assembler/AutodetectCapableMBeanInfoAssembler.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.assembler; + +/** + * Extends the {@code MBeanInfoAssembler} to add autodetection logic. + * Implementations of this interface are given the opportunity by the + * {@code MBeanExporter} to include additional beans in the registration process. + * + *

    The exact mechanism for deciding which beans to include is left to + * implementing classes. + * + * @author Rob Harrop + * @since 1.2 + * @see org.springframework.jmx.export.MBeanExporter + */ +public interface AutodetectCapableMBeanInfoAssembler extends MBeanInfoAssembler { + + /** + * Indicate whether a particular bean should be included in the registration + * process, if it is not specified in the {@code beans} map of the + * {@code MBeanExporter}. + * @param beanClass the class of the bean (might be a proxy class) + * @param beanName the name of the bean in the bean factory + */ + boolean includeBean(Class beanClass, String beanName); + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssembler.java b/spring-context/src/main/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssembler.java new file mode 100644 index 0000000..4be4b5e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssembler.java @@ -0,0 +1,244 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.assembler; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.Map; +import java.util.Properties; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +/** + * Subclass of {@code AbstractReflectiveMBeanInfoAssembler} that allows for + * the management interface of a bean to be defined using arbitrary interfaces. + * Any methods or properties that are defined in those interfaces are exposed + * as MBean operations and attributes. + * + *

    By default, this class votes on the inclusion of each operation or attribute + * based on the interfaces implemented by the bean class. However, you can supply an + * array of interfaces via the {@code managedInterfaces} property that will be + * used instead. If you have multiple beans and you wish each bean to use a different + * set of interfaces, then you can map bean keys (that is the name used to pass the + * bean to the {@code MBeanExporter}) to a list of interface names using the + * {@code interfaceMappings} property. + * + *

    If you specify values for both {@code interfaceMappings} and + * {@code managedInterfaces}, Spring will attempt to find interfaces in the + * mappings first. If no interfaces for the bean are found, it will use the + * interfaces defined by {@code managedInterfaces}. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see #setManagedInterfaces + * @see #setInterfaceMappings + * @see MethodNameBasedMBeanInfoAssembler + * @see SimpleReflectiveMBeanInfoAssembler + * @see org.springframework.jmx.export.MBeanExporter + */ +public class InterfaceBasedMBeanInfoAssembler extends AbstractConfigurableMBeanInfoAssembler + implements BeanClassLoaderAware, InitializingBean { + + @Nullable + private Class[] managedInterfaces; + + /** Mappings of bean keys to an array of classes. */ + @Nullable + private Properties interfaceMappings; + + @Nullable + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + /** Mappings of bean keys to an array of classes. */ + @Nullable + private Map[]> resolvedInterfaceMappings; + + + /** + * Set the array of interfaces to use for creating the management info. + * These interfaces will be used for a bean if no entry corresponding to + * that bean is found in the {@code interfaceMappings} property. + * @param managedInterfaces an array of classes indicating the interfaces to use. + * Each entry MUST be an interface. + * @see #setInterfaceMappings + */ + public void setManagedInterfaces(@Nullable Class... managedInterfaces) { + if (managedInterfaces != null) { + for (Class ifc : managedInterfaces) { + if (!ifc.isInterface()) { + throw new IllegalArgumentException( + "Management interface [" + ifc.getName() + "] is not an interface"); + } + } + } + this.managedInterfaces = managedInterfaces; + } + + /** + * Set the mappings of bean keys to a comma-separated list of interface names. + *

    The property key should match the bean key and the property value should match + * the list of interface names. When searching for interfaces for a bean, Spring + * will check these mappings first. + * @param mappings the mappings of bean keys to interface names + */ + public void setInterfaceMappings(@Nullable Properties mappings) { + this.interfaceMappings = mappings; + } + + @Override + public void setBeanClassLoader(@Nullable ClassLoader beanClassLoader) { + this.beanClassLoader = beanClassLoader; + } + + + @Override + public void afterPropertiesSet() { + if (this.interfaceMappings != null) { + this.resolvedInterfaceMappings = resolveInterfaceMappings(this.interfaceMappings); + } + } + + /** + * Resolve the given interface mappings, turning class names into Class objects. + * @param mappings the specified interface mappings + * @return the resolved interface mappings (with Class objects as values) + */ + private Map[]> resolveInterfaceMappings(Properties mappings) { + Map[]> resolvedMappings = CollectionUtils.newHashMap(mappings.size()); + for (Enumeration en = mappings.propertyNames(); en.hasMoreElements();) { + String beanKey = (String) en.nextElement(); + String[] classNames = StringUtils.commaDelimitedListToStringArray(mappings.getProperty(beanKey)); + Class[] classes = resolveClassNames(classNames, beanKey); + resolvedMappings.put(beanKey, classes); + } + return resolvedMappings; + } + + /** + * Resolve the given class names into Class objects. + * @param classNames the class names to resolve + * @param beanKey the bean key that the class names are associated with + * @return the resolved Class + */ + private Class[] resolveClassNames(String[] classNames, String beanKey) { + Class[] classes = new Class[classNames.length]; + for (int x = 0; x < classes.length; x++) { + Class cls = ClassUtils.resolveClassName(classNames[x].trim(), this.beanClassLoader); + if (!cls.isInterface()) { + throw new IllegalArgumentException( + "Class [" + classNames[x] + "] mapped to bean key [" + beanKey + "] is no interface"); + } + classes[x] = cls; + } + return classes; + } + + + /** + * Check to see if the {@code Method} is declared in + * one of the configured interfaces and that it is public. + * @param method the accessor {@code Method}. + * @param beanKey the key associated with the MBean in the + * {@code beans} {@code Map}. + * @return {@code true} if the {@code Method} is declared in one of the + * configured interfaces, otherwise {@code false}. + */ + @Override + protected boolean includeReadAttribute(Method method, String beanKey) { + return isPublicInInterface(method, beanKey); + } + + /** + * Check to see if the {@code Method} is declared in + * one of the configured interfaces and that it is public. + * @param method the mutator {@code Method}. + * @param beanKey the key associated with the MBean in the + * {@code beans} {@code Map}. + * @return {@code true} if the {@code Method} is declared in one of the + * configured interfaces, otherwise {@code false}. + */ + @Override + protected boolean includeWriteAttribute(Method method, String beanKey) { + return isPublicInInterface(method, beanKey); + } + + /** + * Check to see if the {@code Method} is declared in + * one of the configured interfaces and that it is public. + * @param method the operation {@code Method}. + * @param beanKey the key associated with the MBean in the + * {@code beans} {@code Map}. + * @return {@code true} if the {@code Method} is declared in one of the + * configured interfaces, otherwise {@code false}. + */ + @Override + protected boolean includeOperation(Method method, String beanKey) { + return isPublicInInterface(method, beanKey); + } + + /** + * Check to see if the {@code Method} is both public and declared in + * one of the configured interfaces. + * @param method the {@code Method} to check. + * @param beanKey the key associated with the MBean in the beans map + * @return {@code true} if the {@code Method} is declared in one of the + * configured interfaces and is public, otherwise {@code false}. + */ + private boolean isPublicInInterface(Method method, String beanKey) { + return Modifier.isPublic(method.getModifiers()) && isDeclaredInInterface(method, beanKey); + } + + /** + * Checks to see if the given method is declared in a managed + * interface for the given bean. + */ + private boolean isDeclaredInInterface(Method method, String beanKey) { + Class[] ifaces = null; + + if (this.resolvedInterfaceMappings != null) { + ifaces = this.resolvedInterfaceMappings.get(beanKey); + } + + if (ifaces == null) { + ifaces = this.managedInterfaces; + if (ifaces == null) { + ifaces = ClassUtils.getAllInterfacesForClass(method.getDeclaringClass()); + } + } + + for (Class ifc : ifaces) { + for (Method ifcMethod : ifc.getMethods()) { + if (ifcMethod.getName().equals(method.getName()) && + ifcMethod.getParameterCount() == method.getParameterCount() && + Arrays.equals(ifcMethod.getParameterTypes(), method.getParameterTypes())) { + return true; + } + } + } + + return false; + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/assembler/MBeanInfoAssembler.java b/spring-context/src/main/java/org/springframework/jmx/export/assembler/MBeanInfoAssembler.java new file mode 100644 index 0000000..e639f3d --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/assembler/MBeanInfoAssembler.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.assembler; + +import javax.management.JMException; +import javax.management.modelmbean.ModelMBeanInfo; + +/** + * Interface to be implemented by all classes that can + * create management interface metadata for a managed resource. + * + *

    Used by the {@code MBeanExporter} to generate the management + * interface for any bean that is not an MBean. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see org.springframework.jmx.export.MBeanExporter + */ +public interface MBeanInfoAssembler { + + /** + * Create the ModelMBeanInfo for the given managed resource. + * @param managedBean the bean that will be exposed (might be an AOP proxy) + * @param beanKey the key associated with the managed bean + * @return the ModelMBeanInfo metadata object + * @throws JMException in case of errors + */ + ModelMBeanInfo getMBeanInfo(Object managedBean, String beanKey) throws JMException; + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/assembler/MetadataMBeanInfoAssembler.java b/spring-context/src/main/java/org/springframework/jmx/export/assembler/MetadataMBeanInfoAssembler.java new file mode 100644 index 0000000..8b9492e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/assembler/MetadataMBeanInfoAssembler.java @@ -0,0 +1,451 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.assembler; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; + +import javax.management.Descriptor; +import javax.management.MBeanParameterInfo; +import javax.management.modelmbean.ModelMBeanNotificationInfo; + +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jmx.export.metadata.InvalidMetadataException; +import org.springframework.jmx.export.metadata.JmxAttributeSource; +import org.springframework.jmx.export.metadata.JmxMetadataUtils; +import org.springframework.jmx.export.metadata.ManagedAttribute; +import org.springframework.jmx.export.metadata.ManagedMetric; +import org.springframework.jmx.export.metadata.ManagedNotification; +import org.springframework.jmx.export.metadata.ManagedOperation; +import org.springframework.jmx.export.metadata.ManagedOperationParameter; +import org.springframework.jmx.export.metadata.ManagedResource; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Implementation of the {@link MBeanInfoAssembler} interface that reads + * the management interface information from source level metadata. + * + *

    Uses the {@link JmxAttributeSource} strategy interface, so that + * metadata can be read using any supported implementation. Out of the box, + * Spring provides an implementation based on annotations: + * {@code AnnotationJmxAttributeSource}. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Jennifer Hickey + * @since 1.2 + * @see #setAttributeSource + * @see org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource + */ +public class MetadataMBeanInfoAssembler extends AbstractReflectiveMBeanInfoAssembler + implements AutodetectCapableMBeanInfoAssembler, InitializingBean { + + @Nullable + private JmxAttributeSource attributeSource; + + + /** + * Create a new {@code MetadataMBeanInfoAssembler} which needs to be + * configured through the {@link #setAttributeSource} method. + */ + public MetadataMBeanInfoAssembler() { + } + + /** + * Create a new {@code MetadataMBeanInfoAssembler} for the given + * {@code JmxAttributeSource}. + * @param attributeSource the JmxAttributeSource to use + */ + public MetadataMBeanInfoAssembler(JmxAttributeSource attributeSource) { + Assert.notNull(attributeSource, "JmxAttributeSource must not be null"); + this.attributeSource = attributeSource; + } + + + /** + * Set the {@code JmxAttributeSource} implementation to use for + * reading the metadata from the bean class. + * @see org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource + */ + public void setAttributeSource(JmxAttributeSource attributeSource) { + Assert.notNull(attributeSource, "JmxAttributeSource must not be null"); + this.attributeSource = attributeSource; + } + + @Override + public void afterPropertiesSet() { + if (this.attributeSource == null) { + throw new IllegalArgumentException("Property 'attributeSource' is required"); + } + } + + private JmxAttributeSource obtainAttributeSource() { + Assert.state(this.attributeSource != null, "No JmxAttributeSource set"); + return this.attributeSource; + } + + + /** + * Throws an IllegalArgumentException if it encounters a JDK dynamic proxy. + * Metadata can only be read from target classes and CGLIB proxies! + */ + @Override + protected void checkManagedBean(Object managedBean) throws IllegalArgumentException { + if (AopUtils.isJdkDynamicProxy(managedBean)) { + throw new IllegalArgumentException( + "MetadataMBeanInfoAssembler does not support JDK dynamic proxies - " + + "export the target beans directly or use CGLIB proxies instead"); + } + } + + /** + * Used for autodetection of beans. Checks to see if the bean's class has a + * {@code ManagedResource} attribute. If so it will add it list of included beans. + * @param beanClass the class of the bean + * @param beanName the name of the bean in the bean factory + */ + @Override + public boolean includeBean(Class beanClass, String beanName) { + return (obtainAttributeSource().getManagedResource(getClassToExpose(beanClass)) != null); + } + + /** + * Vote on the inclusion of an attribute accessor. + * @param method the accessor method + * @param beanKey the key associated with the MBean in the beans map + * @return whether the method has the appropriate metadata + */ + @Override + protected boolean includeReadAttribute(Method method, String beanKey) { + return hasManagedAttribute(method) || hasManagedMetric(method); + } + + /** + * Votes on the inclusion of an attribute mutator. + * @param method the mutator method + * @param beanKey the key associated with the MBean in the beans map + * @return whether the method has the appropriate metadata + */ + @Override + protected boolean includeWriteAttribute(Method method, String beanKey) { + return hasManagedAttribute(method); + } + + /** + * Votes on the inclusion of an operation. + * @param method the operation method + * @param beanKey the key associated with the MBean in the beans map + * @return whether the method has the appropriate metadata + */ + @Override + protected boolean includeOperation(Method method, String beanKey) { + PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method); + return (pd != null && hasManagedAttribute(method)) || hasManagedOperation(method); + } + + /** + * Checks to see if the given Method has the {@code ManagedAttribute} attribute. + */ + private boolean hasManagedAttribute(Method method) { + return (obtainAttributeSource().getManagedAttribute(method) != null); + } + + /** + * Checks to see if the given Method has the {@code ManagedMetric} attribute. + */ + private boolean hasManagedMetric(Method method) { + return (obtainAttributeSource().getManagedMetric(method) != null); + } + + /** + * Checks to see if the given Method has the {@code ManagedOperation} attribute. + * @param method the method to check + */ + private boolean hasManagedOperation(Method method) { + return (obtainAttributeSource().getManagedOperation(method) != null); + } + + + /** + * Reads managed resource description from the source level metadata. + * Returns an empty {@code String} if no description can be found. + */ + @Override + protected String getDescription(Object managedBean, String beanKey) { + ManagedResource mr = obtainAttributeSource().getManagedResource(getClassToExpose(managedBean)); + return (mr != null ? mr.getDescription() : ""); + } + + /** + * Creates a description for the attribute corresponding to this property + * descriptor. Attempts to create the description using metadata from either + * the getter or setter attributes, otherwise uses the property name. + */ + @Override + protected String getAttributeDescription(PropertyDescriptor propertyDescriptor, String beanKey) { + Method readMethod = propertyDescriptor.getReadMethod(); + Method writeMethod = propertyDescriptor.getWriteMethod(); + + ManagedAttribute getter = + (readMethod != null ? obtainAttributeSource().getManagedAttribute(readMethod) : null); + ManagedAttribute setter = + (writeMethod != null ? obtainAttributeSource().getManagedAttribute(writeMethod) : null); + + if (getter != null && StringUtils.hasText(getter.getDescription())) { + return getter.getDescription(); + } + else if (setter != null && StringUtils.hasText(setter.getDescription())) { + return setter.getDescription(); + } + + ManagedMetric metric = (readMethod != null ? obtainAttributeSource().getManagedMetric(readMethod) : null); + if (metric != null && StringUtils.hasText(metric.getDescription())) { + return metric.getDescription(); + } + + return propertyDescriptor.getDisplayName(); + } + + /** + * Retrieves the description for the supplied {@code Method} from the + * metadata. Uses the method name is no description is present in the metadata. + */ + @Override + protected String getOperationDescription(Method method, String beanKey) { + PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method); + if (pd != null) { + ManagedAttribute ma = obtainAttributeSource().getManagedAttribute(method); + if (ma != null && StringUtils.hasText(ma.getDescription())) { + return ma.getDescription(); + } + ManagedMetric metric = obtainAttributeSource().getManagedMetric(method); + if (metric != null && StringUtils.hasText(metric.getDescription())) { + return metric.getDescription(); + } + return method.getName(); + } + else { + ManagedOperation mo = obtainAttributeSource().getManagedOperation(method); + if (mo != null && StringUtils.hasText(mo.getDescription())) { + return mo.getDescription(); + } + return method.getName(); + } + } + + /** + * Reads {@code MBeanParameterInfo} from the {@code ManagedOperationParameter} + * attributes attached to a method. Returns an empty array of {@code MBeanParameterInfo} + * if no attributes are found. + */ + @Override + protected MBeanParameterInfo[] getOperationParameters(Method method, String beanKey) { + ManagedOperationParameter[] params = obtainAttributeSource().getManagedOperationParameters(method); + if (ObjectUtils.isEmpty(params)) { + return super.getOperationParameters(method, beanKey); + } + + MBeanParameterInfo[] parameterInfo = new MBeanParameterInfo[params.length]; + Class[] methodParameters = method.getParameterTypes(); + for (int i = 0; i < params.length; i++) { + ManagedOperationParameter param = params[i]; + parameterInfo[i] = + new MBeanParameterInfo(param.getName(), methodParameters[i].getName(), param.getDescription()); + } + return parameterInfo; + } + + /** + * Reads the {@link ManagedNotification} metadata from the {@code Class} of the managed resource + * and generates and returns the corresponding {@link ModelMBeanNotificationInfo} metadata. + */ + @Override + protected ModelMBeanNotificationInfo[] getNotificationInfo(Object managedBean, String beanKey) { + ManagedNotification[] notificationAttributes = + obtainAttributeSource().getManagedNotifications(getClassToExpose(managedBean)); + ModelMBeanNotificationInfo[] notificationInfos = + new ModelMBeanNotificationInfo[notificationAttributes.length]; + + for (int i = 0; i < notificationAttributes.length; i++) { + ManagedNotification attribute = notificationAttributes[i]; + notificationInfos[i] = JmxMetadataUtils.convertToModelMBeanNotificationInfo(attribute); + } + + return notificationInfos; + } + + /** + * Adds descriptor fields from the {@code ManagedResource} attribute + * to the MBean descriptor. Specifically, adds the {@code currencyTimeLimit}, + * {@code persistPolicy}, {@code persistPeriod}, {@code persistLocation} + * and {@code persistName} descriptor fields if they are present in the metadata. + */ + @Override + protected void populateMBeanDescriptor(Descriptor desc, Object managedBean, String beanKey) { + ManagedResource mr = obtainAttributeSource().getManagedResource(getClassToExpose(managedBean)); + if (mr == null) { + throw new InvalidMetadataException( + "No ManagedResource attribute found for class: " + getClassToExpose(managedBean)); + } + + applyCurrencyTimeLimit(desc, mr.getCurrencyTimeLimit()); + + if (mr.isLog()) { + desc.setField(FIELD_LOG, "true"); + } + if (StringUtils.hasLength(mr.getLogFile())) { + desc.setField(FIELD_LOG_FILE, mr.getLogFile()); + } + + if (StringUtils.hasLength(mr.getPersistPolicy())) { + desc.setField(FIELD_PERSIST_POLICY, mr.getPersistPolicy()); + } + if (mr.getPersistPeriod() >= 0) { + desc.setField(FIELD_PERSIST_PERIOD, Integer.toString(mr.getPersistPeriod())); + } + if (StringUtils.hasLength(mr.getPersistName())) { + desc.setField(FIELD_PERSIST_NAME, mr.getPersistName()); + } + if (StringUtils.hasLength(mr.getPersistLocation())) { + desc.setField(FIELD_PERSIST_LOCATION, mr.getPersistLocation()); + } + } + + /** + * Adds descriptor fields from the {@code ManagedAttribute} attribute or the {@code ManagedMetric} attribute + * to the attribute descriptor. + */ + @Override + protected void populateAttributeDescriptor( + Descriptor desc, @Nullable Method getter, @Nullable Method setter, String beanKey) { + + if (getter != null) { + ManagedMetric metric = obtainAttributeSource().getManagedMetric(getter); + if (metric != null) { + populateMetricDescriptor(desc, metric); + return; + } + } + + ManagedAttribute gma = (getter != null ? obtainAttributeSource().getManagedAttribute(getter) : null); + ManagedAttribute sma = (setter != null ? obtainAttributeSource().getManagedAttribute(setter) : null); + populateAttributeDescriptor(desc, + (gma != null ? gma : ManagedAttribute.EMPTY), + (sma != null ? sma : ManagedAttribute.EMPTY)); + } + + private void populateAttributeDescriptor(Descriptor desc, ManagedAttribute gma, ManagedAttribute sma) { + applyCurrencyTimeLimit(desc, resolveIntDescriptor(gma.getCurrencyTimeLimit(), sma.getCurrencyTimeLimit())); + + Object defaultValue = resolveObjectDescriptor(gma.getDefaultValue(), sma.getDefaultValue()); + desc.setField(FIELD_DEFAULT, defaultValue); + + String persistPolicy = resolveStringDescriptor(gma.getPersistPolicy(), sma.getPersistPolicy()); + if (StringUtils.hasLength(persistPolicy)) { + desc.setField(FIELD_PERSIST_POLICY, persistPolicy); + } + int persistPeriod = resolveIntDescriptor(gma.getPersistPeriod(), sma.getPersistPeriod()); + if (persistPeriod >= 0) { + desc.setField(FIELD_PERSIST_PERIOD, Integer.toString(persistPeriod)); + } + } + + private void populateMetricDescriptor(Descriptor desc, ManagedMetric metric) { + applyCurrencyTimeLimit(desc, metric.getCurrencyTimeLimit()); + + if (StringUtils.hasLength(metric.getPersistPolicy())) { + desc.setField(FIELD_PERSIST_POLICY, metric.getPersistPolicy()); + } + if (metric.getPersistPeriod() >= 0) { + desc.setField(FIELD_PERSIST_PERIOD, Integer.toString(metric.getPersistPeriod())); + } + + if (StringUtils.hasLength(metric.getDisplayName())) { + desc.setField(FIELD_DISPLAY_NAME, metric.getDisplayName()); + } + + if (StringUtils.hasLength(metric.getUnit())) { + desc.setField(FIELD_UNITS, metric.getUnit()); + } + + if (StringUtils.hasLength(metric.getCategory())) { + desc.setField(FIELD_METRIC_CATEGORY, metric.getCategory()); + } + + desc.setField(FIELD_METRIC_TYPE, metric.getMetricType().toString()); + } + + /** + * Adds descriptor fields from the {@code ManagedAttribute} attribute + * to the attribute descriptor. Specifically, adds the {@code currencyTimeLimit} + * descriptor field if it is present in the metadata. + */ + @Override + protected void populateOperationDescriptor(Descriptor desc, Method method, String beanKey) { + ManagedOperation mo = obtainAttributeSource().getManagedOperation(method); + if (mo != null) { + applyCurrencyTimeLimit(desc, mo.getCurrencyTimeLimit()); + } + } + + /** + * Determines which of two {@code int} values should be used as the value + * for an attribute descriptor. In general, only the getter or the setter will + * be have a non-negative value so we use that value. In the event that both values + * are non-negative, we use the greater of the two. This method can be used to + * resolve any {@code int} valued descriptor where there are two possible values. + * @param getter the int value associated with the getter for this attribute + * @param setter the int associated with the setter for this attribute + */ + private int resolveIntDescriptor(int getter, int setter) { + return (getter >= setter ? getter : setter); + } + + /** + * Locates the value of a descriptor based on values attached + * to both the getter and setter methods. If both have values + * supplied then the value attached to the getter is preferred. + * @param getter the Object value associated with the get method + * @param setter the Object value associated with the set method + * @return the appropriate Object to use as the value for the descriptor + */ + @Nullable + private Object resolveObjectDescriptor(@Nullable Object getter, @Nullable Object setter) { + return (getter != null ? getter : setter); + } + + /** + * Locates the value of a descriptor based on values attached + * to both the getter and setter methods. If both have values + * supplied then the value attached to the getter is preferred. + * The supplied default value is used to check to see if the value + * associated with the getter has changed from the default. + * @param getter the String value associated with the get method + * @param setter the String value associated with the set method + * @return the appropriate String to use as the value for the descriptor + */ + @Nullable + private String resolveStringDescriptor(@Nullable String getter, @Nullable String setter) { + return (StringUtils.hasLength(getter) ? getter : setter); + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssembler.java b/spring-context/src/main/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssembler.java new file mode 100644 index 0000000..1edd14d --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssembler.java @@ -0,0 +1,130 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.assembler; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * {@code AbstractReflectiveMBeanInfoAssembler} subclass that allows + * method names to be explicitly excluded as MBean operations and attributes. + * + *

    Any method not explicitly excluded from the management interface will be exposed to + * JMX. JavaBean getters and setters will automatically be exposed as JMX attributes. + * + *

    You can supply an array of method names via the {@code ignoredMethods} + * property. If you have multiple beans and you wish each bean to use a different + * set of method names, then you can map bean keys (that is the name used to pass + * the bean to the {@code MBeanExporter}) to a list of method names using the + * {@code ignoredMethodMappings} property. + * + *

    If you specify values for both {@code ignoredMethodMappings} and + * {@code ignoredMethods}, Spring will attempt to find method names in the + * mappings first. If no method names for the bean are found, it will use the + * method names defined by {@code ignoredMethods}. + * + * @author Rob Harrop + * @author Seth Ladd + * @since 1.2.5 + * @see #setIgnoredMethods + * @see #setIgnoredMethodMappings + * @see InterfaceBasedMBeanInfoAssembler + * @see SimpleReflectiveMBeanInfoAssembler + * @see MethodNameBasedMBeanInfoAssembler + * @see org.springframework.jmx.export.MBeanExporter + */ +public class MethodExclusionMBeanInfoAssembler extends AbstractConfigurableMBeanInfoAssembler { + + @Nullable + private Set ignoredMethods; + + @Nullable + private Map> ignoredMethodMappings; + + + /** + * Set the array of method names to be ignored when creating the management info. + *

    These method names will be used for a bean if no entry corresponding to + * that bean is found in the {@code ignoredMethodsMappings} property. + * @see #setIgnoredMethodMappings(java.util.Properties) + */ + public void setIgnoredMethods(String... ignoredMethodNames) { + this.ignoredMethods = new HashSet<>(Arrays.asList(ignoredMethodNames)); + } + + /** + * Set the mappings of bean keys to a comma-separated list of method names. + *

    These method names are ignored when creating the management interface. + *

    The property key must match the bean key and the property value must match + * the list of method names. When searching for method names to ignore for a bean, + * Spring will check these mappings first. + */ + public void setIgnoredMethodMappings(Properties mappings) { + this.ignoredMethodMappings = new HashMap<>(); + for (Enumeration en = mappings.keys(); en.hasMoreElements();) { + String beanKey = (String) en.nextElement(); + String[] methodNames = StringUtils.commaDelimitedListToStringArray(mappings.getProperty(beanKey)); + this.ignoredMethodMappings.put(beanKey, new HashSet<>(Arrays.asList(methodNames))); + } + } + + + @Override + protected boolean includeReadAttribute(Method method, String beanKey) { + return isNotIgnored(method, beanKey); + } + + @Override + protected boolean includeWriteAttribute(Method method, String beanKey) { + return isNotIgnored(method, beanKey); + } + + @Override + protected boolean includeOperation(Method method, String beanKey) { + return isNotIgnored(method, beanKey); + } + + /** + * Determine whether the given method is supposed to be included, + * that is, not configured as to be ignored. + * @param method the operation method + * @param beanKey the key associated with the MBean in the beans map + * of the {@code MBeanExporter} + */ + protected boolean isNotIgnored(Method method, String beanKey) { + if (this.ignoredMethodMappings != null) { + Set methodNames = this.ignoredMethodMappings.get(beanKey); + if (methodNames != null) { + return !methodNames.contains(method.getName()); + } + } + if (this.ignoredMethods != null) { + return !this.ignoredMethods.contains(method.getName()); + } + return true; + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/assembler/MethodNameBasedMBeanInfoAssembler.java b/spring-context/src/main/java/org/springframework/jmx/export/assembler/MethodNameBasedMBeanInfoAssembler.java new file mode 100644 index 0000000..0dfe17e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/assembler/MethodNameBasedMBeanInfoAssembler.java @@ -0,0 +1,124 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.assembler; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Subclass of {@code AbstractReflectiveMBeanInfoAssembler} that allows + * to specify method names to be exposed as MBean operations and attributes. + * JavaBean getters and setters will automatically be exposed as JMX attributes. + * + *

    You can supply an array of method names via the {@code managedMethods} + * property. If you have multiple beans and you wish each bean to use a different + * set of method names, then you can map bean keys (that is the name used to pass + * the bean to the {@code MBeanExporter}) to a list of method names using the + * {@code methodMappings} property. + * + *

    If you specify values for both {@code methodMappings} and + * {@code managedMethods}, Spring will attempt to find method names in the + * mappings first. If no method names for the bean are found, it will use the + * method names defined by {@code managedMethods}. + * + * @author Juergen Hoeller + * @since 1.2 + * @see #setManagedMethods + * @see #setMethodMappings + * @see InterfaceBasedMBeanInfoAssembler + * @see SimpleReflectiveMBeanInfoAssembler + * @see MethodExclusionMBeanInfoAssembler + * @see org.springframework.jmx.export.MBeanExporter + */ +public class MethodNameBasedMBeanInfoAssembler extends AbstractConfigurableMBeanInfoAssembler { + + /** + * Stores the set of method names to use for creating the management interface. + */ + @Nullable + private Set managedMethods; + + /** + * Stores the mappings of bean keys to an array of method names. + */ + @Nullable + private Map> methodMappings; + + + /** + * Set the array of method names to use for creating the management info. + * These method names will be used for a bean if no entry corresponding to + * that bean is found in the {@code methodMappings} property. + * @param methodNames an array of method names indicating the methods to use + * @see #setMethodMappings + */ + public void setManagedMethods(String... methodNames) { + this.managedMethods = new HashSet<>(Arrays.asList(methodNames)); + } + + /** + * Set the mappings of bean keys to a comma-separated list of method names. + * The property key should match the bean key and the property value should match + * the list of method names. When searching for method names for a bean, Spring + * will check these mappings first. + * @param mappings the mappings of bean keys to method names + */ + public void setMethodMappings(Properties mappings) { + this.methodMappings = new HashMap<>(); + for (Enumeration en = mappings.keys(); en.hasMoreElements();) { + String beanKey = (String) en.nextElement(); + String[] methodNames = StringUtils.commaDelimitedListToStringArray(mappings.getProperty(beanKey)); + this.methodMappings.put(beanKey, new HashSet<>(Arrays.asList(methodNames))); + } + } + + + @Override + protected boolean includeReadAttribute(Method method, String beanKey) { + return isMatch(method, beanKey); + } + + @Override + protected boolean includeWriteAttribute(Method method, String beanKey) { + return isMatch(method, beanKey); + } + + @Override + protected boolean includeOperation(Method method, String beanKey) { + return isMatch(method, beanKey); + } + + protected boolean isMatch(Method method, String beanKey) { + if (this.methodMappings != null) { + Set methodNames = this.methodMappings.get(beanKey); + if (methodNames != null) { + return methodNames.contains(method.getName()); + } + } + return (this.managedMethods != null && this.managedMethods.contains(method.getName())); + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/assembler/SimpleReflectiveMBeanInfoAssembler.java b/spring-context/src/main/java/org/springframework/jmx/export/assembler/SimpleReflectiveMBeanInfoAssembler.java new file mode 100644 index 0000000..a9e1185 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/assembler/SimpleReflectiveMBeanInfoAssembler.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.assembler; + +import java.lang.reflect.Method; + +/** + * Simple subclass of {@code AbstractReflectiveMBeanInfoAssembler} + * that always votes yes for method and property inclusion, effectively exposing + * all public methods and properties as operations and attributes. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + */ +public class SimpleReflectiveMBeanInfoAssembler extends AbstractConfigurableMBeanInfoAssembler { + + /** + * Always returns {@code true}. + */ + @Override + protected boolean includeReadAttribute(Method method, String beanKey) { + return true; + } + + /** + * Always returns {@code true}. + */ + @Override + protected boolean includeWriteAttribute(Method method, String beanKey) { + return true; + } + + /** + * Always returns {@code true}. + */ + @Override + protected boolean includeOperation(Method method, String beanKey) { + return true; + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/assembler/package-info.java b/spring-context/src/main/java/org/springframework/jmx/export/assembler/package-info.java new file mode 100644 index 0000000..cb59979 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/assembler/package-info.java @@ -0,0 +1,10 @@ +/** + * Provides a strategy for MBeanInfo assembly. Used by MBeanExporter to + * determine the attributes and operations to expose for Spring-managed beans. + */ +@NonNullApi +@NonNullFields +package org.springframework.jmx.export.assembler; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/jmx/export/metadata/AbstractJmxAttribute.java b/spring-context/src/main/java/org/springframework/jmx/export/metadata/AbstractJmxAttribute.java new file mode 100644 index 0000000..03f8742 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/metadata/AbstractJmxAttribute.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.metadata; + +/** + * Base class for all JMX metadata classes. + * + * @author Rob Harrop + * @since 1.2 + */ +public abstract class AbstractJmxAttribute { + + private String description = ""; + + private int currencyTimeLimit = -1; + + + /** + * Set a description for this attribute. + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Return a description for this attribute. + */ + public String getDescription() { + return this.description; + } + + /** + * Set a currency time limit for this attribute. + */ + public void setCurrencyTimeLimit(int currencyTimeLimit) { + this.currencyTimeLimit = currencyTimeLimit; + } + + /** + * Return a currency time limit for this attribute. + */ + public int getCurrencyTimeLimit() { + return this.currencyTimeLimit; + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/metadata/InvalidMetadataException.java b/spring-context/src/main/java/org/springframework/jmx/export/metadata/InvalidMetadataException.java new file mode 100644 index 0000000..ee2c685 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/metadata/InvalidMetadataException.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.metadata; + +import org.springframework.jmx.JmxException; + +/** + * Thrown by the {@code JmxAttributeSource} when it encounters + * incorrect metadata on a managed resource or one of its methods. + * + * @author Rob Harrop + * @since 1.2 + * @see JmxAttributeSource + * @see org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler + */ +@SuppressWarnings("serial") +public class InvalidMetadataException extends JmxException { + + /** + * Create a new {@code InvalidMetadataException} with the supplied + * error message. + * @param msg the detail message + */ + public InvalidMetadataException(String msg) { + super(msg); + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/metadata/JmxAttributeSource.java b/spring-context/src/main/java/org/springframework/jmx/export/metadata/JmxAttributeSource.java new file mode 100644 index 0000000..adefc28 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/metadata/JmxAttributeSource.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.metadata; + +import java.lang.reflect.Method; + +import org.springframework.lang.Nullable; + +/** + * Interface used by the {@code MetadataMBeanInfoAssembler} to + * read source-level metadata from a managed resource's class. + * + * @author Rob Harrop + * @author Jennifer Hickey + * @since 1.2 + * @see org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler#setAttributeSource + * @see org.springframework.jmx.export.MBeanExporter#setAssembler + */ +public interface JmxAttributeSource { + + /** + * Implementations should return an instance of {@code ManagedResource} + * if the supplied {@code Class} has the appropriate metadata. + * Otherwise should return {@code null}. + * @param clazz the class to read the attribute data from + * @return the attribute, or {@code null} if not found + * @throws InvalidMetadataException in case of invalid attributes + */ + @Nullable + ManagedResource getManagedResource(Class clazz) throws InvalidMetadataException; + + /** + * Implementations should return an instance of {@code ManagedAttribute} + * if the supplied {@code Method} has the corresponding metadata. + * Otherwise should return {@code null}. + * @param method the method to read the attribute data from + * @return the attribute, or {@code null} if not found + * @throws InvalidMetadataException in case of invalid attributes + */ + @Nullable + ManagedAttribute getManagedAttribute(Method method) throws InvalidMetadataException; + + /** + * Implementations should return an instance of {@code ManagedMetric} + * if the supplied {@code Method} has the corresponding metadata. + * Otherwise should return {@code null}. + * @param method the method to read the attribute data from + * @return the metric, or {@code null} if not found + * @throws InvalidMetadataException in case of invalid attributes + */ + @Nullable + ManagedMetric getManagedMetric(Method method) throws InvalidMetadataException; + + /** + * Implementations should return an instance of {@code ManagedOperation} + * if the supplied {@code Method} has the corresponding metadata. + * Otherwise should return {@code null}. + * @param method the method to read the attribute data from + * @return the attribute, or {@code null} if not found + * @throws InvalidMetadataException in case of invalid attributes + */ + @Nullable + ManagedOperation getManagedOperation(Method method) throws InvalidMetadataException; + + /** + * Implementations should return an array of {@code ManagedOperationParameter} + * if the supplied {@code Method} has the corresponding metadata. Otherwise + * should return an empty array if no metadata is found. + * @param method the {@code Method} to read the metadata from + * @return the parameter information. + * @throws InvalidMetadataException in the case of invalid attributes. + */ + ManagedOperationParameter[] getManagedOperationParameters(Method method) throws InvalidMetadataException; + + /** + * Implementations should return an array of {@link ManagedNotification ManagedNotifications} + * if the supplied the {@code Class} has the corresponding metadata. Otherwise + * should return an empty array. + * @param clazz the {@code Class} to read the metadata from + * @return the notification information + * @throws InvalidMetadataException in the case of invalid metadata + */ + ManagedNotification[] getManagedNotifications(Class clazz) throws InvalidMetadataException; + + + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/metadata/JmxMetadataUtils.java b/spring-context/src/main/java/org/springframework/jmx/export/metadata/JmxMetadataUtils.java new file mode 100644 index 0000000..8eb6750 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/metadata/JmxMetadataUtils.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.metadata; + +import javax.management.modelmbean.ModelMBeanNotificationInfo; + +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Utility methods for converting Spring JMX metadata into their plain JMX equivalents. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +public abstract class JmxMetadataUtils { + + /** + * Convert the supplied {@link ManagedNotification} into the corresponding + * {@link javax.management.modelmbean.ModelMBeanNotificationInfo}. + */ + public static ModelMBeanNotificationInfo convertToModelMBeanNotificationInfo(ManagedNotification notificationInfo) { + String[] notifTypes = notificationInfo.getNotificationTypes(); + if (ObjectUtils.isEmpty(notifTypes)) { + throw new IllegalArgumentException("Must specify at least one notification type"); + } + + String name = notificationInfo.getName(); + if (!StringUtils.hasText(name)) { + throw new IllegalArgumentException("Must specify notification name"); + } + + String description = notificationInfo.getDescription(); + return new ModelMBeanNotificationInfo(notifTypes, name, description); + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedAttribute.java b/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedAttribute.java new file mode 100644 index 0000000..d7139b4 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedAttribute.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.metadata; + +import org.springframework.lang.Nullable; + +/** + * Metadata that indicates to expose a given bean property as JMX attribute. + * Only valid when used on a JavaBean getter or setter. + * + * @author Rob Harrop + * @since 1.2 + * @see org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler + * @see org.springframework.jmx.export.MBeanExporter + */ +public class ManagedAttribute extends AbstractJmxAttribute { + + /** + * Empty attributes. + */ + public static final ManagedAttribute EMPTY = new ManagedAttribute(); + + + @Nullable + private Object defaultValue; + + @Nullable + private String persistPolicy; + + private int persistPeriod = -1; + + + /** + * Set the default value of this attribute. + */ + public void setDefaultValue(@Nullable Object defaultValue) { + this.defaultValue = defaultValue; + } + + /** + * Return the default value of this attribute. + */ + @Nullable + public Object getDefaultValue() { + return this.defaultValue; + } + + public void setPersistPolicy(@Nullable String persistPolicy) { + this.persistPolicy = persistPolicy; + } + + @Nullable + public String getPersistPolicy() { + return this.persistPolicy; + } + + public void setPersistPeriod(int persistPeriod) { + this.persistPeriod = persistPeriod; + } + + public int getPersistPeriod() { + return this.persistPeriod; + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedMetric.java b/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedMetric.java new file mode 100644 index 0000000..495fb5c --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedMetric.java @@ -0,0 +1,140 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.metadata; + +import org.springframework.jmx.support.MetricType; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Metadata that indicates to expose a given bean property as a JMX attribute, + * with additional descriptor properties that indicate that the attribute is a + * metric. Only valid when used on a JavaBean getter. + * + * @author Jennifer Hickey + * @since 3.0 + * @see org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler + */ +public class ManagedMetric extends AbstractJmxAttribute { + + @Nullable + private String category; + + @Nullable + private String displayName; + + private MetricType metricType = MetricType.GAUGE; + + private int persistPeriod = -1; + + @Nullable + private String persistPolicy; + + @Nullable + private String unit; + + + /** + * The category of this metric (ex. throughput, performance, utilization). + */ + public void setCategory(@Nullable String category) { + this.category = category; + } + + /** + * The category of this metric (ex. throughput, performance, utilization). + */ + @Nullable + public String getCategory() { + return this.category; + } + + /** + * A display name for this metric. + */ + public void setDisplayName(@Nullable String displayName) { + this.displayName = displayName; + } + + /** + * A display name for this metric. + */ + @Nullable + public String getDisplayName() { + return this.displayName; + } + + /** + * A description of how this metric's values change over time. + */ + public void setMetricType(MetricType metricType) { + Assert.notNull(metricType, "MetricType must not be null"); + this.metricType = metricType; + } + + /** + * A description of how this metric's values change over time. + */ + public MetricType getMetricType() { + return this.metricType; + } + + /** + * The persist period for this metric. + */ + public void setPersistPeriod(int persistPeriod) { + this.persistPeriod = persistPeriod; + } + + /** + * The persist period for this metric. + */ + public int getPersistPeriod() { + return this.persistPeriod; + } + + /** + * The persist policy for this metric. + */ + public void setPersistPolicy(@Nullable String persistPolicy) { + this.persistPolicy = persistPolicy; + } + + /** + * The persist policy for this metric. + */ + @Nullable + public String getPersistPolicy() { + return this.persistPolicy; + } + + /** + * The expected unit of measurement values. + */ + public void setUnit(@Nullable String unit) { + this.unit = unit; + } + + /** + * The expected unit of measurement values. + */ + @Nullable + public String getUnit() { + return this.unit; + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedNotification.java b/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedNotification.java new file mode 100644 index 0000000..3512b26 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedNotification.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.metadata; + +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Metadata that indicates a JMX notification emitted by a bean. + * + * @author Rob Harrop + * @since 2.0 + */ +public class ManagedNotification { + + @Nullable + private String[] notificationTypes; + + @Nullable + private String name; + + @Nullable + private String description; + + + /** + * Set a single notification type, or a list of notification types + * as comma-delimited String. + */ + public void setNotificationType(String notificationType) { + this.notificationTypes = StringUtils.commaDelimitedListToStringArray(notificationType); + } + + /** + * Set a list of notification types. + */ + public void setNotificationTypes(@Nullable String... notificationTypes) { + this.notificationTypes = notificationTypes; + } + + /** + * Return the list of notification types. + */ + @Nullable + public String[] getNotificationTypes() { + return this.notificationTypes; + } + + /** + * Set the name of this notification. + */ + public void setName(@Nullable String name) { + this.name = name; + } + + /** + * Return the name of this notification. + */ + @Nullable + public String getName() { + return this.name; + } + + /** + * Set a description for this notification. + */ + public void setDescription(@Nullable String description) { + this.description = description; + } + + /** + * Return a description for this notification. + */ + @Nullable + public String getDescription() { + return this.description; + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedOperation.java b/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedOperation.java new file mode 100644 index 0000000..736f244 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedOperation.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2005 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.metadata; + +/** + * Metadata that indicates to expose a given method as JMX operation. + * Only valid when used on a method that is not a JavaBean getter or setter. + * + * @author Rob Harrop + * @since 1.2 + * @see org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler + * @see org.springframework.jmx.export.MBeanExporter + */ +public class ManagedOperation extends AbstractJmxAttribute { + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedOperationParameter.java b/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedOperationParameter.java new file mode 100644 index 0000000..2d57f00 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedOperationParameter.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2007 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.metadata; + +/** + * Metadata about JMX operation parameters. + * Used in conjunction with a {@link ManagedOperation} attribute. + * + * @author Rob Harrop + * @since 1.2 + */ +public class ManagedOperationParameter { + + private int index = 0; + + private String name = ""; + + private String description = ""; + + + /** + * Set the index of this parameter in the operation signature. + */ + public void setIndex(int index) { + this.index = index; + } + + /** + * Return the index of this parameter in the operation signature. + */ + public int getIndex() { + return this.index; + } + + /** + * Set the name of this parameter in the operation signature. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Return the name of this parameter in the operation signature. + */ + public String getName() { + return this.name; + } + + /** + * Set a description for this parameter. + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Return a description for this parameter. + */ + public String getDescription() { + return this.description; + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedResource.java b/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedResource.java new file mode 100644 index 0000000..e85bda8 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedResource.java @@ -0,0 +1,121 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.metadata; + +import org.springframework.lang.Nullable; + +/** + * Metadata indicating that instances of an annotated class + * are to be registered with a JMX server. + * Only valid when used on a {@code Class}. + * + * @author Rob Harrop + * @since 1.2 + * @see org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler + * @see org.springframework.jmx.export.naming.MetadataNamingStrategy + * @see org.springframework.jmx.export.MBeanExporter + */ +public class ManagedResource extends AbstractJmxAttribute { + + @Nullable + private String objectName; + + private boolean log = false; + + @Nullable + private String logFile; + + @Nullable + private String persistPolicy; + + private int persistPeriod = -1; + + @Nullable + private String persistName; + + @Nullable + private String persistLocation; + + + /** + * Set the JMX ObjectName of this managed resource. + */ + public void setObjectName(@Nullable String objectName) { + this.objectName = objectName; + } + + /** + * Return the JMX ObjectName of this managed resource. + */ + @Nullable + public String getObjectName() { + return this.objectName; + } + + public void setLog(boolean log) { + this.log = log; + } + + public boolean isLog() { + return this.log; + } + + public void setLogFile(@Nullable String logFile) { + this.logFile = logFile; + } + + @Nullable + public String getLogFile() { + return this.logFile; + } + + public void setPersistPolicy(@Nullable String persistPolicy) { + this.persistPolicy = persistPolicy; + } + + @Nullable + public String getPersistPolicy() { + return this.persistPolicy; + } + + public void setPersistPeriod(int persistPeriod) { + this.persistPeriod = persistPeriod; + } + + public int getPersistPeriod() { + return this.persistPeriod; + } + + public void setPersistName(@Nullable String persistName) { + this.persistName = persistName; + } + + @Nullable + public String getPersistName() { + return this.persistName; + } + + public void setPersistLocation(@Nullable String persistLocation) { + this.persistLocation = persistLocation; + } + + @Nullable + public String getPersistLocation() { + return this.persistLocation; + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/metadata/package-info.java b/spring-context/src/main/java/org/springframework/jmx/export/metadata/package-info.java new file mode 100644 index 0000000..1163b22 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/metadata/package-info.java @@ -0,0 +1,10 @@ +/** + * Provides generic JMX metadata classes and basic support for reading + * JMX metadata in a provider-agnostic manner. + */ +@NonNullApi +@NonNullFields +package org.springframework.jmx.export.metadata; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/jmx/export/naming/IdentityNamingStrategy.java b/spring-context/src/main/java/org/springframework/jmx/export/naming/IdentityNamingStrategy.java new file mode 100644 index 0000000..3c8aafe --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/naming/IdentityNamingStrategy.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.naming; + +import java.util.Hashtable; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.springframework.jmx.support.ObjectNameManager; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; + +/** + * An implementation of the {@code ObjectNamingStrategy} interface that + * creates a name based on the identity of a given instance. + * + *

    The resulting {@code ObjectName} will be in the form + * package:class=class name,hashCode=identity hash (in hex) + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + */ +public class IdentityNamingStrategy implements ObjectNamingStrategy { + + /** + * The type key. + */ + public static final String TYPE_KEY = "type"; + + /** + * The hash code key. + */ + public static final String HASH_CODE_KEY = "hashCode"; + + + /** + * Returns an instance of {@code ObjectName} based on the identity + * of the managed resource. + */ + @Override + public ObjectName getObjectName(Object managedBean, @Nullable String beanKey) throws MalformedObjectNameException { + String domain = ClassUtils.getPackageName(managedBean.getClass()); + Hashtable keys = new Hashtable<>(); + keys.put(TYPE_KEY, ClassUtils.getShortName(managedBean.getClass())); + keys.put(HASH_CODE_KEY, ObjectUtils.getIdentityHexString(managedBean)); + return ObjectNameManager.getInstance(domain, keys); + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/naming/KeyNamingStrategy.java b/spring-context/src/main/java/org/springframework/jmx/export/naming/KeyNamingStrategy.java new file mode 100644 index 0000000..70243a8 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/naming/KeyNamingStrategy.java @@ -0,0 +1,148 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.naming; + +import java.io.IOException; +import java.util.Properties; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.jmx.support.ObjectNameManager; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * {@code ObjectNamingStrategy} implementation that builds + * {@code ObjectName} instances from the key used in the + * "beans" map passed to {@code MBeanExporter}. + * + *

    Can also check object name mappings, given as {@code Properties} + * or as {@code mappingLocations} of properties files. The key used + * to look up is the key used in {@code MBeanExporter}'s "beans" map. + * If no mapping is found for a given key, the key itself is used to + * build an {@code ObjectName}. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see #setMappings + * @see #setMappingLocation + * @see #setMappingLocations + * @see org.springframework.jmx.export.MBeanExporter#setBeans + */ +public class KeyNamingStrategy implements ObjectNamingStrategy, InitializingBean { + + /** + * {@code Log} instance for this class. + */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** + * Stores the mappings of bean key to {@code ObjectName}. + */ + @Nullable + private Properties mappings; + + /** + * Stores the {@code Resource}s containing properties that should be loaded + * into the final merged set of {@code Properties} used for {@code ObjectName} + * resolution. + */ + @Nullable + private Resource[] mappingLocations; + + /** + * Stores the result of merging the {@code mappings} {@code Properties} + * with the properties stored in the resources defined by {@code mappingLocations}. + */ + @Nullable + private Properties mergedMappings; + + + /** + * Set local properties, containing object name mappings, e.g. via + * the "props" tag in XML bean definitions. These can be considered + * defaults, to be overridden by properties loaded from files. + */ + public void setMappings(Properties mappings) { + this.mappings = mappings; + } + + /** + * Set a location of a properties file to be loaded, + * containing object name mappings. + */ + public void setMappingLocation(Resource location) { + this.mappingLocations = new Resource[] {location}; + } + + /** + * Set location of properties files to be loaded, + * containing object name mappings. + */ + public void setMappingLocations(Resource... mappingLocations) { + this.mappingLocations = mappingLocations; + } + + + /** + * Merges the {@code Properties} configured in the {@code mappings} and + * {@code mappingLocations} into the final {@code Properties} instance + * used for {@code ObjectName} resolution. + */ + @Override + public void afterPropertiesSet() throws IOException { + this.mergedMappings = new Properties(); + CollectionUtils.mergePropertiesIntoMap(this.mappings, this.mergedMappings); + + if (this.mappingLocations != null) { + for (Resource location : this.mappingLocations) { + if (logger.isDebugEnabled()) { + logger.debug("Loading JMX object name mappings file from " + location); + } + PropertiesLoaderUtils.fillProperties(this.mergedMappings, location); + } + } + } + + + /** + * Attempts to retrieve the {@code ObjectName} via the given key, trying to + * find a mapped value in the mappings first. + */ + @Override + public ObjectName getObjectName(Object managedBean, @Nullable String beanKey) throws MalformedObjectNameException { + Assert.notNull(beanKey, "KeyNamingStrategy requires bean key"); + String objectName = null; + if (this.mergedMappings != null) { + objectName = this.mergedMappings.getProperty(beanKey); + } + if (objectName == null) { + objectName = beanKey; + } + return ObjectNameManager.getInstance(objectName); + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/naming/MetadataNamingStrategy.java b/spring-context/src/main/java/org/springframework/jmx/export/naming/MetadataNamingStrategy.java new file mode 100644 index 0000000..78c1fe2 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/naming/MetadataNamingStrategy.java @@ -0,0 +1,141 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.naming; + +import java.util.Hashtable; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jmx.export.metadata.JmxAttributeSource; +import org.springframework.jmx.export.metadata.ManagedResource; +import org.springframework.jmx.support.ObjectNameManager; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * An implementation of the {@link ObjectNamingStrategy} interface + * that reads the {@code ObjectName} from the source-level metadata. + * Falls back to the bean key (bean name) if no {@code ObjectName} + * can be found in source-level metadata. + * + *

    Uses the {@link JmxAttributeSource} strategy interface, so that + * metadata can be read using any supported implementation. Out of the box, + * {@link org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource} + * introspects a well-defined set of Java 5 annotations that come with Spring. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see ObjectNamingStrategy + * @see org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource + */ +public class MetadataNamingStrategy implements ObjectNamingStrategy, InitializingBean { + + /** + * The {@code JmxAttributeSource} implementation to use for reading metadata. + */ + @Nullable + private JmxAttributeSource attributeSource; + + @Nullable + private String defaultDomain; + + + /** + * Create a new {@code MetadataNamingStrategy} which needs to be + * configured through the {@link #setAttributeSource} method. + */ + public MetadataNamingStrategy() { + } + + /** + * Create a new {@code MetadataNamingStrategy} for the given + * {@code JmxAttributeSource}. + * @param attributeSource the JmxAttributeSource to use + */ + public MetadataNamingStrategy(JmxAttributeSource attributeSource) { + Assert.notNull(attributeSource, "JmxAttributeSource must not be null"); + this.attributeSource = attributeSource; + } + + + /** + * Set the implementation of the {@code JmxAttributeSource} interface to use + * when reading the source-level metadata. + */ + public void setAttributeSource(JmxAttributeSource attributeSource) { + Assert.notNull(attributeSource, "JmxAttributeSource must not be null"); + this.attributeSource = attributeSource; + } + + /** + * Specify the default domain to be used for generating ObjectNames + * when no source-level metadata has been specified. + *

    The default is to use the domain specified in the bean name + * (if the bean name follows the JMX ObjectName syntax); else, + * the package name of the managed bean class. + */ + public void setDefaultDomain(String defaultDomain) { + this.defaultDomain = defaultDomain; + } + + @Override + public void afterPropertiesSet() { + if (this.attributeSource == null) { + throw new IllegalArgumentException("Property 'attributeSource' is required"); + } + } + + + /** + * Reads the {@code ObjectName} from the source-level metadata associated + * with the managed resource's {@code Class}. + */ + @Override + public ObjectName getObjectName(Object managedBean, @Nullable String beanKey) throws MalformedObjectNameException { + Assert.state(this.attributeSource != null, "No JmxAttributeSource set"); + Class managedClass = AopUtils.getTargetClass(managedBean); + ManagedResource mr = this.attributeSource.getManagedResource(managedClass); + + // Check that an object name has been specified. + if (mr != null && StringUtils.hasText(mr.getObjectName())) { + return ObjectNameManager.getInstance(mr.getObjectName()); + } + else { + Assert.state(beanKey != null, "No ManagedResource attribute and no bean key specified"); + try { + return ObjectNameManager.getInstance(beanKey); + } + catch (MalformedObjectNameException ex) { + String domain = this.defaultDomain; + if (domain == null) { + domain = ClassUtils.getPackageName(managedClass); + } + Hashtable properties = new Hashtable<>(); + properties.put("type", ClassUtils.getShortName(managedClass)); + properties.put("name", beanKey); + return ObjectNameManager.getInstance(domain, properties); + } + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/naming/ObjectNamingStrategy.java b/spring-context/src/main/java/org/springframework/jmx/export/naming/ObjectNamingStrategy.java new file mode 100644 index 0000000..8a75e8d --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/naming/ObjectNamingStrategy.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.naming; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.springframework.lang.Nullable; + +/** + * Strategy interface that encapsulates the creation of {@code ObjectName} instances. + * + *

    Used by the {@code MBeanExporter} to obtain {@code ObjectName}s + * when registering beans. + * + * @author Rob Harrop + * @since 1.2 + * @see org.springframework.jmx.export.MBeanExporter + * @see javax.management.ObjectName + */ +@FunctionalInterface +public interface ObjectNamingStrategy { + + /** + * Obtain an {@code ObjectName} for the supplied bean. + * @param managedBean the bean that will be exposed under the + * returned {@code ObjectName} + * @param beanKey the key associated with this bean in the beans map + * passed to the {@code MBeanExporter} + * @return the {@code ObjectName} instance + * @throws MalformedObjectNameException if the resulting {@code ObjectName} is invalid + */ + ObjectName getObjectName(Object managedBean, @Nullable String beanKey) throws MalformedObjectNameException; + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/naming/SelfNaming.java b/spring-context/src/main/java/org/springframework/jmx/export/naming/SelfNaming.java new file mode 100644 index 0000000..3b00563 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/naming/SelfNaming.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.naming; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +/** + * Interface that allows infrastructure components to provide their own + * {@code ObjectName}s to the {@code MBeanExporter}. + * + *

    Note: This interface is mainly intended for internal usage. + * + * @author Rob Harrop + * @since 1.2.2 + * @see org.springframework.jmx.export.MBeanExporter + */ +public interface SelfNaming { + + /** + * Return the {@code ObjectName} for the implementing object. + * @throws MalformedObjectNameException if thrown by the ObjectName constructor + * @see javax.management.ObjectName#ObjectName(String) + * @see javax.management.ObjectName#getInstance(String) + * @see org.springframework.jmx.support.ObjectNameManager#getInstance(String) + */ + ObjectName getObjectName() throws MalformedObjectNameException; + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/naming/package-info.java b/spring-context/src/main/java/org/springframework/jmx/export/naming/package-info.java new file mode 100644 index 0000000..98056c2 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/naming/package-info.java @@ -0,0 +1,10 @@ +/** + * Provides a strategy for ObjectName creation. Used by MBeanExporter + * to determine the JMX names to use for exported Spring-managed beans. + */ +@NonNullApi +@NonNullFields +package org.springframework.jmx.export.naming; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/jmx/export/notification/ModelMBeanNotificationPublisher.java b/spring-context/src/main/java/org/springframework/jmx/export/notification/ModelMBeanNotificationPublisher.java new file mode 100644 index 0000000..6728204 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/notification/ModelMBeanNotificationPublisher.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.notification; + +import javax.management.AttributeChangeNotification; +import javax.management.MBeanException; +import javax.management.Notification; +import javax.management.ObjectName; +import javax.management.modelmbean.ModelMBean; +import javax.management.modelmbean.ModelMBeanNotificationBroadcaster; + +import org.springframework.util.Assert; + +/** + * {@link NotificationPublisher} implementation that uses the infrastructure + * provided by the {@link ModelMBean} interface to track + * {@link javax.management.NotificationListener javax.management.NotificationListeners} + * and send {@link Notification Notifications} to those listeners. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Rick Evans + * @since 2.0 + * @see javax.management.modelmbean.ModelMBeanNotificationBroadcaster + * @see NotificationPublisherAware + */ +public class ModelMBeanNotificationPublisher implements NotificationPublisher { + + /** + * The {@link ModelMBean} instance wrapping the managed resource into which this + * {@code NotificationPublisher} will be injected. + */ + private final ModelMBeanNotificationBroadcaster modelMBean; + + /** + * The {@link ObjectName} associated with the {@link ModelMBean modelMBean}. + */ + private final ObjectName objectName; + + /** + * The managed resource associated with the {@link ModelMBean modelMBean}. + */ + private final Object managedResource; + + + /** + * Create a new instance of the {@link ModelMBeanNotificationPublisher} class + * that will publish all {@link javax.management.Notification Notifications} + * to the supplied {@link ModelMBean}. + * @param modelMBean the target {@link ModelMBean}; must not be {@code null} + * @param objectName the {@link ObjectName} of the source {@link ModelMBean} + * @param managedResource the managed resource exposed by the supplied {@link ModelMBean} + * @throws IllegalArgumentException if any of the parameters is {@code null} + */ + public ModelMBeanNotificationPublisher( + ModelMBeanNotificationBroadcaster modelMBean, ObjectName objectName, Object managedResource) { + + Assert.notNull(modelMBean, "'modelMBean' must not be null"); + Assert.notNull(objectName, "'objectName' must not be null"); + Assert.notNull(managedResource, "'managedResource' must not be null"); + this.modelMBean = modelMBean; + this.objectName = objectName; + this.managedResource = managedResource; + } + + + /** + * Send the supplied {@link Notification} using the wrapped + * {@link ModelMBean} instance. + * @param notification the {@link Notification} to be sent + * @throws IllegalArgumentException if the supplied {@code notification} is {@code null} + * @throws UnableToSendNotificationException if the supplied {@code notification} could not be sent + */ + @Override + public void sendNotification(Notification notification) { + Assert.notNull(notification, "Notification must not be null"); + replaceNotificationSourceIfNecessary(notification); + try { + if (notification instanceof AttributeChangeNotification) { + this.modelMBean.sendAttributeChangeNotification((AttributeChangeNotification) notification); + } + else { + this.modelMBean.sendNotification(notification); + } + } + catch (MBeanException ex) { + throw new UnableToSendNotificationException("Unable to send notification [" + notification + "]", ex); + } + } + + /** + * Replaces the notification source if necessary to do so. + * From the {@link Notification javadoc}: + * "It is strongly recommended that notification senders use the object name + * rather than a reference to the MBean object as the source." + * @param notification the {@link Notification} whose + * {@link javax.management.Notification#getSource()} might need massaging + */ + private void replaceNotificationSourceIfNecessary(Notification notification) { + if (notification.getSource() == null || notification.getSource().equals(this.managedResource)) { + notification.setSource(this.objectName); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/notification/NotificationPublisher.java b/spring-context/src/main/java/org/springframework/jmx/export/notification/NotificationPublisher.java new file mode 100644 index 0000000..a6cd181 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/notification/NotificationPublisher.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.notification; + +import javax.management.Notification; + +/** + * Simple interface allowing Spring-managed MBeans to publish JMX notifications + * without being aware of how those notifications are being transmitted to the + * {@link javax.management.MBeanServer}. + * + *

    Managed resources can access a {@code NotificationPublisher} by + * implementing the {@link NotificationPublisherAware} interface. After a particular + * managed resource instance is registered with the {@link javax.management.MBeanServer}, + * Spring will inject a {@code NotificationPublisher} instance into it if that + * resource implements the {@link NotificationPublisherAware} interface. + * + *

    Each managed resource instance will have a distinct instance of a + * {@code NotificationPublisher} implementation. This instance will keep + * track of all the {@link javax.management.NotificationListener NotificationListeners} + * registered for a particular mananaged resource. + * + *

    Any existing, user-defined MBeans should use standard JMX APIs for notification + * publication; this interface is intended for use only by Spring-created MBeans. + * + * @author Rob Harrop + * @since 2.0 + * @see NotificationPublisherAware + * @see org.springframework.jmx.export.MBeanExporter + */ +@FunctionalInterface +public interface NotificationPublisher { + + /** + * Send the specified {@link javax.management.Notification} to all registered + * {@link javax.management.NotificationListener NotificationListeners}. + * Managed resources are not responsible for managing the list + * of registered {@link javax.management.NotificationListener NotificationListeners}; + * that is performed automatically. + * @param notification the JMX Notification to send + * @throws UnableToSendNotificationException if sending failed + */ + void sendNotification(Notification notification) throws UnableToSendNotificationException; + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/notification/NotificationPublisherAware.java b/spring-context/src/main/java/org/springframework/jmx/export/notification/NotificationPublisherAware.java new file mode 100644 index 0000000..e8fd618 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/notification/NotificationPublisherAware.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2011 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.notification; + +import org.springframework.beans.factory.Aware; + +/** + * Interface to be implemented by any Spring-managed resource that is to be + * registered with an {@link javax.management.MBeanServer} and wishes to send + * JMX {@link javax.management.Notification javax.management.Notifications}. + * + *

    Provides Spring-created managed resources with a {@link NotificationPublisher} + * as soon as they are registered with the {@link javax.management.MBeanServer}. + * + *

    NOTE: This interface only applies to simple Spring-managed + * beans which happen to get exported through Spring's + * {@link org.springframework.jmx.export.MBeanExporter}. + * It does not apply to any non-exported beans; neither does it apply + * to standard MBeans exported by Spring. For standard JMX MBeans, + * consider implementing the {@link javax.management.modelmbean.ModelMBeanNotificationBroadcaster} + * interface (or implementing a full {@link javax.management.modelmbean.ModelMBean}). + * + * @author Rob Harrop + * @author Chris Beams + * @since 2.0 + * @see NotificationPublisher + */ +public interface NotificationPublisherAware extends Aware { + + /** + * Set the {@link NotificationPublisher} instance for the current managed resource instance. + */ + void setNotificationPublisher(NotificationPublisher notificationPublisher); + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/notification/UnableToSendNotificationException.java b/spring-context/src/main/java/org/springframework/jmx/export/notification/UnableToSendNotificationException.java new file mode 100644 index 0000000..ad37b5f --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/notification/UnableToSendNotificationException.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.notification; + +import org.springframework.jmx.JmxException; + +/** + * Thrown when a JMX {@link javax.management.Notification} is unable to be sent. + * + *

    The root cause of just why a particular notification could not be sent + * will typically be available via the {@link #getCause()} property. + * + * @author Rob Harrop + * @since 2.0 + * @see NotificationPublisher + */ +@SuppressWarnings("serial") +public class UnableToSendNotificationException extends JmxException { + + /** + * Create a new instance of the {@link UnableToSendNotificationException} + * class with the specified error message. + * @param msg the detail message + */ + public UnableToSendNotificationException(String msg) { + super(msg); + } + + /** + * Create a new instance of the {@link UnableToSendNotificationException} + * with the specified error message and root cause. + * @param msg the detail message + * @param cause the root cause + */ + public UnableToSendNotificationException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/export/notification/package-info.java b/spring-context/src/main/java/org/springframework/jmx/export/notification/package-info.java new file mode 100644 index 0000000..10056eb --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/notification/package-info.java @@ -0,0 +1,10 @@ +/** + * Provides supporting infrastructure to allow Spring-created MBeans + * to send JMX notifications. + */ +@NonNullApi +@NonNullFields +package org.springframework.jmx.export.notification; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/jmx/export/package-info.java b/spring-context/src/main/java/org/springframework/jmx/export/package-info.java new file mode 100644 index 0000000..5adaaf6 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/export/package-info.java @@ -0,0 +1,10 @@ +/** + * This package provides declarative creation and registration of + * Spring-managed beans as JMX MBeans. + */ +@NonNullApi +@NonNullFields +package org.springframework.jmx.export; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/jmx/package-info.java b/spring-context/src/main/java/org/springframework/jmx/package-info.java new file mode 100644 index 0000000..65922f4 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/package-info.java @@ -0,0 +1,10 @@ +/** + * This package contains Spring's JMX support, which includes registration of + * Spring-managed beans as JMX MBeans as well as access to remote JMX MBeans. + */ +@NonNullApi +@NonNullFields +package org.springframework.jmx; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/jmx/support/ConnectorServerFactoryBean.java b/spring-context/src/main/java/org/springframework/jmx/support/ConnectorServerFactoryBean.java new file mode 100644 index 0000000..c02f5fb --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/support/ConnectorServerFactoryBean.java @@ -0,0 +1,246 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.support; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import javax.management.JMException; +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; +import javax.management.remote.JMXConnectorServer; +import javax.management.remote.JMXConnectorServerFactory; +import javax.management.remote.JMXServiceURL; +import javax.management.remote.MBeanServerForwarder; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jmx.JmxException; +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; + +/** + * {@link FactoryBean} that creates a JSR-160 {@link JMXConnectorServer}, + * optionally registers it with the {@link MBeanServer}, and then starts it. + * + *

    The {@code JMXConnectorServer} can be started in a separate thread by setting the + * {@code threaded} property to {@code true}. You can configure this thread to be a + * daemon thread by setting the {@code daemon} property to {@code true}. + * + *

    The {@code JMXConnectorServer} is correctly shut down when an instance of this + * class is destroyed on shutdown of the containing {@code ApplicationContext}. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see JMXConnectorServer + * @see MBeanServer + */ +public class ConnectorServerFactoryBean extends MBeanRegistrationSupport + implements FactoryBean, InitializingBean, DisposableBean { + + /** The default service URL. */ + public static final String DEFAULT_SERVICE_URL = "service:jmx:jmxmp://localhost:9875"; + + + private String serviceUrl = DEFAULT_SERVICE_URL; + + private Map environment = new HashMap<>(); + + @Nullable + private MBeanServerForwarder forwarder; + + @Nullable + private ObjectName objectName; + + private boolean threaded = false; + + private boolean daemon = false; + + @Nullable + private JMXConnectorServer connectorServer; + + + /** + * Set the service URL for the {@code JMXConnectorServer}. + */ + public void setServiceUrl(String serviceUrl) { + this.serviceUrl = serviceUrl; + } + + /** + * Set the environment properties used to construct the {@code JMXConnectorServer} + * as {@code java.util.Properties} (String key/value pairs). + */ + public void setEnvironment(@Nullable Properties environment) { + CollectionUtils.mergePropertiesIntoMap(environment, this.environment); + } + + /** + * Set the environment properties used to construct the {@code JMXConnector} + * as a {@code Map} of String keys and arbitrary Object values. + */ + public void setEnvironmentMap(@Nullable Map environment) { + if (environment != null) { + this.environment.putAll(environment); + } + } + + /** + * Set an MBeanServerForwarder to be applied to the {@code JMXConnectorServer}. + */ + public void setForwarder(MBeanServerForwarder forwarder) { + this.forwarder = forwarder; + } + + /** + * Set the {@code ObjectName} used to register the {@code JMXConnectorServer} + * itself with the {@code MBeanServer}, as {@code ObjectName} instance + * or as {@code String}. + * @throws MalformedObjectNameException if the {@code ObjectName} is malformed + */ + public void setObjectName(Object objectName) throws MalformedObjectNameException { + this.objectName = ObjectNameManager.getInstance(objectName); + } + + /** + * Set whether the {@code JMXConnectorServer} should be started in a separate thread. + */ + public void setThreaded(boolean threaded) { + this.threaded = threaded; + } + + /** + * Set whether any threads started for the {@code JMXConnectorServer} should be + * started as daemon threads. + */ + public void setDaemon(boolean daemon) { + this.daemon = daemon; + } + + + /** + * Start the connector server. If the {@code threaded} flag is set to {@code true}, + * the {@code JMXConnectorServer} will be started in a separate thread. + * If the {@code daemon} flag is set to {@code true}, that thread will be + * started as a daemon thread. + * @throws JMException if a problem occurred when registering the connector server + * with the {@code MBeanServer} + * @throws IOException if there is a problem starting the connector server + */ + @Override + public void afterPropertiesSet() throws JMException, IOException { + if (this.server == null) { + this.server = JmxUtils.locateMBeanServer(); + } + + // Create the JMX service URL. + JMXServiceURL url = new JMXServiceURL(this.serviceUrl); + + // Create the connector server now. + this.connectorServer = JMXConnectorServerFactory.newJMXConnectorServer(url, this.environment, this.server); + + // Set the given MBeanServerForwarder, if any. + if (this.forwarder != null) { + this.connectorServer.setMBeanServerForwarder(this.forwarder); + } + + // Do we want to register the connector with the MBean server? + if (this.objectName != null) { + doRegister(this.connectorServer, this.objectName); + } + + try { + if (this.threaded) { + // Start the connector server asynchronously (in a separate thread). + final JMXConnectorServer serverToStart = this.connectorServer; + Thread connectorThread = new Thread() { + @Override + public void run() { + try { + serverToStart.start(); + } + catch (IOException ex) { + throw new JmxException("Could not start JMX connector server after delay", ex); + } + } + }; + + connectorThread.setName("JMX Connector Thread [" + this.serviceUrl + "]"); + connectorThread.setDaemon(this.daemon); + connectorThread.start(); + } + else { + // Start the connector server in the same thread. + this.connectorServer.start(); + } + + if (logger.isInfoEnabled()) { + logger.info("JMX connector server started: " + this.connectorServer); + } + } + + catch (IOException ex) { + // Unregister the connector server if startup failed. + unregisterBeans(); + throw ex; + } + } + + + @Override + @Nullable + public JMXConnectorServer getObject() { + return this.connectorServer; + } + + @Override + public Class getObjectType() { + return (this.connectorServer != null ? this.connectorServer.getClass() : JMXConnectorServer.class); + } + + @Override + public boolean isSingleton() { + return true; + } + + + /** + * Stop the {@code JMXConnectorServer} managed by an instance of this class. + * Automatically called on {@code ApplicationContext} shutdown. + * @throws IOException if there is an error stopping the connector server + */ + @Override + public void destroy() throws IOException { + try { + if (this.connectorServer != null) { + if (logger.isInfoEnabled()) { + logger.info("Stopping JMX connector server: " + this.connectorServer); + } + this.connectorServer.stop(); + } + } + finally { + unregisterBeans(); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/support/JmxUtils.java b/spring-context/src/main/java/org/springframework/jmx/support/JmxUtils.java new file mode 100644 index 0000000..26c91b1 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/support/JmxUtils.java @@ -0,0 +1,312 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.support; + +import java.beans.PropertyDescriptor; +import java.lang.management.ManagementFactory; +import java.lang.reflect.Method; +import java.util.Hashtable; +import java.util.List; + +import javax.management.DynamicMBean; +import javax.management.JMX; +import javax.management.MBeanParameterInfo; +import javax.management.MBeanServer; +import javax.management.MBeanServerFactory; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.jmx.MBeanServerNotFoundException; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Collection of generic utility methods to support Spring JMX. + * Includes a convenient method to locate an MBeanServer. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see #locateMBeanServer + */ +public abstract class JmxUtils { + + /** + * The key used when extending an existing {@link ObjectName} with the + * identity hash code of its corresponding managed resource. + */ + public static final String IDENTITY_OBJECT_NAME_KEY = "identity"; + + /** + * Suffix used to identify an MBean interface. + */ + private static final String MBEAN_SUFFIX = "MBean"; + + + private static final Log logger = LogFactory.getLog(JmxUtils.class); + + + /** + * Attempt to find a locally running {@code MBeanServer}. Fails if no + * {@code MBeanServer} can be found. Logs a warning if more than one + * {@code MBeanServer} found, returning the first one from the list. + * @return the {@code MBeanServer} if found + * @throws MBeanServerNotFoundException if no {@code MBeanServer} could be found + * @see javax.management.MBeanServerFactory#findMBeanServer + */ + public static MBeanServer locateMBeanServer() throws MBeanServerNotFoundException { + return locateMBeanServer(null); + } + + /** + * Attempt to find a locally running {@code MBeanServer}. Fails if no + * {@code MBeanServer} can be found. Logs a warning if more than one + * {@code MBeanServer} found, returning the first one from the list. + * @param agentId the agent identifier of the MBeanServer to retrieve. + * If this parameter is {@code null}, all registered MBeanServers are considered. + * If the empty String is given, the platform MBeanServer will be returned. + * @return the {@code MBeanServer} if found + * @throws MBeanServerNotFoundException if no {@code MBeanServer} could be found + * @see javax.management.MBeanServerFactory#findMBeanServer(String) + */ + public static MBeanServer locateMBeanServer(@Nullable String agentId) throws MBeanServerNotFoundException { + MBeanServer server = null; + + // null means any registered server, but "" specifically means the platform server + if (!"".equals(agentId)) { + List servers = MBeanServerFactory.findMBeanServer(agentId); + if (!CollectionUtils.isEmpty(servers)) { + // Check to see if an MBeanServer is registered. + if (servers.size() > 1 && logger.isInfoEnabled()) { + logger.info("Found more than one MBeanServer instance" + + (agentId != null ? " with agent id [" + agentId + "]" : "") + + ". Returning first from list."); + } + server = servers.get(0); + } + } + + if (server == null && !StringUtils.hasLength(agentId)) { + // Attempt to load the PlatformMBeanServer. + try { + server = ManagementFactory.getPlatformMBeanServer(); + } + catch (SecurityException ex) { + throw new MBeanServerNotFoundException("No specific MBeanServer found, " + + "and not allowed to obtain the Java platform MBeanServer", ex); + } + } + + if (server == null) { + throw new MBeanServerNotFoundException( + "Unable to locate an MBeanServer instance" + + (agentId != null ? " with agent id [" + agentId + "]" : "")); + } + + if (logger.isDebugEnabled()) { + logger.debug("Found MBeanServer: " + server); + } + return server; + } + + /** + * Convert an array of {@code MBeanParameterInfo} into an array of + * {@code Class} instances corresponding to the parameters. + * @param paramInfo the JMX parameter info + * @return the parameter types as classes + * @throws ClassNotFoundException if a parameter type could not be resolved + */ + @Nullable + public static Class[] parameterInfoToTypes(@Nullable MBeanParameterInfo[] paramInfo) + throws ClassNotFoundException { + + return parameterInfoToTypes(paramInfo, ClassUtils.getDefaultClassLoader()); + } + + /** + * Convert an array of {@code MBeanParameterInfo} into an array of + * {@code Class} instances corresponding to the parameters. + * @param paramInfo the JMX parameter info + * @param classLoader the ClassLoader to use for loading parameter types + * @return the parameter types as classes + * @throws ClassNotFoundException if a parameter type could not be resolved + */ + @Nullable + public static Class[] parameterInfoToTypes( + @Nullable MBeanParameterInfo[] paramInfo, @Nullable ClassLoader classLoader) + throws ClassNotFoundException { + + Class[] types = null; + if (paramInfo != null && paramInfo.length > 0) { + types = new Class[paramInfo.length]; + for (int x = 0; x < paramInfo.length; x++) { + types[x] = ClassUtils.forName(paramInfo[x].getType(), classLoader); + } + } + return types; + } + + /** + * Create a {@code String[]} representing the argument signature of a + * method. Each element in the array is the fully qualified class name + * of the corresponding argument in the methods signature. + * @param method the method to build an argument signature for + * @return the signature as array of argument types + */ + public static String[] getMethodSignature(Method method) { + Class[] types = method.getParameterTypes(); + String[] signature = new String[types.length]; + for (int x = 0; x < types.length; x++) { + signature[x] = types[x].getName(); + } + return signature; + } + + /** + * Return the JMX attribute name to use for the given JavaBeans property. + *

    When using strict casing, a JavaBean property with a getter method + * such as {@code getFoo()} translates to an attribute called + * {@code Foo}. With strict casing disabled, {@code getFoo()} + * would translate to just {@code foo}. + * @param property the JavaBeans property descriptor + * @param useStrictCasing whether to use strict casing + * @return the JMX attribute name to use + */ + public static String getAttributeName(PropertyDescriptor property, boolean useStrictCasing) { + if (useStrictCasing) { + return StringUtils.capitalize(property.getName()); + } + else { + return property.getName(); + } + } + + /** + * Append an additional key/value pair to an existing {@link ObjectName} with the key being + * the static value {@code identity} and the value being the identity hash code of the + * managed resource being exposed on the supplied {@link ObjectName}. This can be used to + * provide a unique {@link ObjectName} for each distinct instance of a particular bean or + * class. Useful when generating {@link ObjectName ObjectNames} at runtime for a set of + * managed resources based on the template value supplied by a + * {@link org.springframework.jmx.export.naming.ObjectNamingStrategy}. + * @param objectName the original JMX ObjectName + * @param managedResource the MBean instance + * @return an ObjectName with the MBean identity added + * @throws MalformedObjectNameException in case of an invalid object name specification + * @see org.springframework.util.ObjectUtils#getIdentityHexString(Object) + */ + public static ObjectName appendIdentityToObjectName(ObjectName objectName, Object managedResource) + throws MalformedObjectNameException { + + Hashtable keyProperties = objectName.getKeyPropertyList(); + keyProperties.put(IDENTITY_OBJECT_NAME_KEY, ObjectUtils.getIdentityHexString(managedResource)); + return ObjectNameManager.getInstance(objectName.getDomain(), keyProperties); + } + + /** + * Return the class or interface to expose for the given bean. + * This is the class that will be searched for attributes and operations + * (for example, checked for annotations). + *

    This implementation returns the superclass for a CGLIB proxy and + * the class of the given bean else (for a JDK proxy or a plain bean class). + * @param managedBean the bean instance (might be an AOP proxy) + * @return the bean class to expose + * @see org.springframework.util.ClassUtils#getUserClass(Object) + */ + public static Class getClassToExpose(Object managedBean) { + return ClassUtils.getUserClass(managedBean); + } + + /** + * Return the class or interface to expose for the given bean class. + * This is the class that will be searched for attributes and operations + * (for example, checked for annotations). + *

    This implementation returns the superclass for a CGLIB proxy and + * the class of the given bean else (for a JDK proxy or a plain bean class). + * @param clazz the bean class (might be an AOP proxy class) + * @return the bean class to expose + * @see org.springframework.util.ClassUtils#getUserClass(Class) + */ + public static Class getClassToExpose(Class clazz) { + return ClassUtils.getUserClass(clazz); + } + + /** + * Determine whether the given bean class qualifies as an MBean as-is. + *

    This implementation checks for {@link javax.management.DynamicMBean} + * classes as well as classes with corresponding "*MBean" interface + * (Standard MBeans) or corresponding "*MXBean" interface (Java 6 MXBeans). + * @param clazz the bean class to analyze + * @return whether the class qualifies as an MBean + * @see org.springframework.jmx.export.MBeanExporter#isMBean(Class) + */ + public static boolean isMBean(@Nullable Class clazz) { + return (clazz != null && + (DynamicMBean.class.isAssignableFrom(clazz) || + (getMBeanInterface(clazz) != null || getMXBeanInterface(clazz) != null))); + } + + /** + * Return the Standard MBean interface for the given class, if any + * (that is, an interface whose name matches the class name of the + * given class but with suffix "MBean"). + * @param clazz the class to check + * @return the Standard MBean interface for the given class + */ + @Nullable + public static Class getMBeanInterface(@Nullable Class clazz) { + if (clazz == null || clazz.getSuperclass() == null) { + return null; + } + String mbeanInterfaceName = clazz.getName() + MBEAN_SUFFIX; + Class[] implementedInterfaces = clazz.getInterfaces(); + for (Class iface : implementedInterfaces) { + if (iface.getName().equals(mbeanInterfaceName)) { + return iface; + } + } + return getMBeanInterface(clazz.getSuperclass()); + } + + /** + * Return the Java 6 MXBean interface exists for the given class, if any + * (that is, an interface whose name ends with "MXBean" and/or + * carries an appropriate MXBean annotation). + * @param clazz the class to check + * @return whether there is an MXBean interface for the given class + */ + @Nullable + public static Class getMXBeanInterface(@Nullable Class clazz) { + if (clazz == null || clazz.getSuperclass() == null) { + return null; + } + Class[] implementedInterfaces = clazz.getInterfaces(); + for (Class iface : implementedInterfaces) { + if (JMX.isMXBeanInterface(iface)) { + return iface; + } + } + return getMXBeanInterface(clazz.getSuperclass()); + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/support/MBeanRegistrationSupport.java b/spring-context/src/main/java/org/springframework/jmx/support/MBeanRegistrationSupport.java new file mode 100644 index 0000000..26c696d --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/support/MBeanRegistrationSupport.java @@ -0,0 +1,268 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.support; + +import java.util.LinkedHashSet; +import java.util.Set; + +import javax.management.InstanceAlreadyExistsException; +import javax.management.InstanceNotFoundException; +import javax.management.JMException; +import javax.management.MBeanServer; +import javax.management.ObjectInstance; +import javax.management.ObjectName; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Provides supporting infrastructure for registering MBeans with an + * {@link javax.management.MBeanServer}. The behavior when encountering + * an existing MBean at a given {@link ObjectName} is fully configurable + * allowing for flexible registration settings. + * + *

    All registered MBeans are tracked and can be unregistered by calling + * the #{@link #unregisterBeans()} method. + * + *

    Sub-classes can receive notifications when an MBean is registered or + * unregistered by overriding the {@link #onRegister(ObjectName)} and + * {@link #onUnregister(ObjectName)} methods respectively. + * + *

    By default, the registration process will fail if attempting to + * register an MBean using a {@link javax.management.ObjectName} that is + * already used. + * + *

    By setting the {@link #setRegistrationPolicy(RegistrationPolicy) registrationPolicy} + * property to {@link RegistrationPolicy#IGNORE_EXISTING} the registration process + * will simply ignore existing MBeans leaving them registered. This is useful in settings + * where multiple applications want to share a common MBean in a shared {@link MBeanServer}. + * + *

    Setting {@link #setRegistrationPolicy(RegistrationPolicy) registrationPolicy} property + * to {@link RegistrationPolicy#REPLACE_EXISTING} will cause existing MBeans to be replaced + * during registration if necessary. This is useful in situations where you can't guarantee + * the state of your {@link MBeanServer}. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Phillip Webb + * @since 2.0 + * @see #setServer + * @see #setRegistrationPolicy + * @see org.springframework.jmx.export.MBeanExporter + */ +public class MBeanRegistrationSupport { + + /** + * {@code Log} instance for this class. + */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** + * The {@code MBeanServer} instance being used to register beans. + */ + @Nullable + protected MBeanServer server; + + /** + * The beans that have been registered by this exporter. + */ + private final Set registeredBeans = new LinkedHashSet<>(); + + /** + * The policy used when registering an MBean and finding that it already exists. + * By default an exception is raised. + */ + private RegistrationPolicy registrationPolicy = RegistrationPolicy.FAIL_ON_EXISTING; + + + /** + * Specify the {@code MBeanServer} instance with which all beans should + * be registered. The {@code MBeanExporter} will attempt to locate an + * existing {@code MBeanServer} if none is supplied. + */ + public void setServer(@Nullable MBeanServer server) { + this.server = server; + } + + /** + * Return the {@code MBeanServer} that the beans will be registered with. + */ + @Nullable + public final MBeanServer getServer() { + return this.server; + } + + /** + * The policy to use when attempting to register an MBean + * under an {@link javax.management.ObjectName} that already exists. + * @param registrationPolicy the policy to use + * @since 3.2 + */ + public void setRegistrationPolicy(RegistrationPolicy registrationPolicy) { + Assert.notNull(registrationPolicy, "RegistrationPolicy must not be null"); + this.registrationPolicy = registrationPolicy; + } + + + /** + * Actually register the MBean with the server. The behavior when encountering + * an existing MBean can be configured using {@link #setRegistrationPolicy}. + * @param mbean the MBean instance + * @param objectName the suggested ObjectName for the MBean + * @throws JMException if the registration failed + */ + protected void doRegister(Object mbean, ObjectName objectName) throws JMException { + Assert.state(this.server != null, "No MBeanServer set"); + ObjectName actualObjectName; + + synchronized (this.registeredBeans) { + ObjectInstance registeredBean = null; + try { + registeredBean = this.server.registerMBean(mbean, objectName); + } + catch (InstanceAlreadyExistsException ex) { + if (this.registrationPolicy == RegistrationPolicy.IGNORE_EXISTING) { + if (logger.isDebugEnabled()) { + logger.debug("Ignoring existing MBean at [" + objectName + "]"); + } + } + else if (this.registrationPolicy == RegistrationPolicy.REPLACE_EXISTING) { + try { + if (logger.isDebugEnabled()) { + logger.debug("Replacing existing MBean at [" + objectName + "]"); + } + this.server.unregisterMBean(objectName); + registeredBean = this.server.registerMBean(mbean, objectName); + } + catch (InstanceNotFoundException ex2) { + if (logger.isInfoEnabled()) { + logger.info("Unable to replace existing MBean at [" + objectName + "]", ex2); + } + throw ex; + } + } + else { + throw ex; + } + } + + // Track registration and notify listeners. + actualObjectName = (registeredBean != null ? registeredBean.getObjectName() : null); + if (actualObjectName == null) { + actualObjectName = objectName; + } + this.registeredBeans.add(actualObjectName); + } + + onRegister(actualObjectName, mbean); + } + + /** + * Unregisters all beans that have been registered by an instance of this class. + */ + protected void unregisterBeans() { + Set snapshot; + synchronized (this.registeredBeans) { + snapshot = new LinkedHashSet<>(this.registeredBeans); + } + if (!snapshot.isEmpty()) { + logger.debug("Unregistering JMX-exposed beans"); + for (ObjectName objectName : snapshot) { + doUnregister(objectName); + } + } + } + + /** + * Actually unregister the specified MBean from the server. + * @param objectName the suggested ObjectName for the MBean + */ + protected void doUnregister(ObjectName objectName) { + Assert.state(this.server != null, "No MBeanServer set"); + boolean actuallyUnregistered = false; + + synchronized (this.registeredBeans) { + if (this.registeredBeans.remove(objectName)) { + try { + // MBean might already have been unregistered by an external process + if (this.server.isRegistered(objectName)) { + this.server.unregisterMBean(objectName); + actuallyUnregistered = true; + } + else { + if (logger.isInfoEnabled()) { + logger.info("Could not unregister MBean [" + objectName + "] as said MBean " + + "is not registered (perhaps already unregistered by an external process)"); + } + } + } + catch (JMException ex) { + if (logger.isInfoEnabled()) { + logger.info("Could not unregister MBean [" + objectName + "]", ex); + } + } + } + } + + if (actuallyUnregistered) { + onUnregister(objectName); + } + } + + /** + * Return the {@link ObjectName ObjectNames} of all registered beans. + */ + protected final ObjectName[] getRegisteredObjectNames() { + synchronized (this.registeredBeans) { + return this.registeredBeans.toArray(new ObjectName[0]); + } + } + + + /** + * Called when an MBean is registered under the given {@link ObjectName}. Allows + * subclasses to perform additional processing when an MBean is registered. + *

    The default implementation delegates to {@link #onRegister(ObjectName)}. + * @param objectName the actual {@link ObjectName} that the MBean was registered with + * @param mbean the registered MBean instance + */ + protected void onRegister(ObjectName objectName, Object mbean) { + onRegister(objectName); + } + + /** + * Called when an MBean is registered under the given {@link ObjectName}. Allows + * subclasses to perform additional processing when an MBean is registered. + *

    The default implementation is empty. Can be overridden in subclasses. + * @param objectName the actual {@link ObjectName} that the MBean was registered with + */ + protected void onRegister(ObjectName objectName) { + } + + /** + * Called when an MBean is unregistered under the given {@link ObjectName}. Allows + * subclasses to perform additional processing when an MBean is unregistered. + *

    The default implementation is empty. Can be overridden in subclasses. + * @param objectName the {@link ObjectName} that the MBean was registered with + */ + protected void onUnregister(ObjectName objectName) { + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/support/MBeanServerConnectionFactoryBean.java b/spring-context/src/main/java/org/springframework/jmx/support/MBeanServerConnectionFactoryBean.java new file mode 100644 index 0000000..64258d7 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/support/MBeanServerConnectionFactoryBean.java @@ -0,0 +1,228 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.support; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import javax.management.MBeanServerConnection; +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXServiceURL; + +import org.springframework.aop.TargetSource; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.target.AbstractLazyCreationTargetSource; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; + +/** + * {@link FactoryBean} that creates a JMX 1.2 {@code MBeanServerConnection} + * to a remote {@code MBeanServer} exposed via a {@code JMXServerConnector}. + * Exposes the {@code MBeanServer} for bean references. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see MBeanServerFactoryBean + * @see ConnectorServerFactoryBean + * @see org.springframework.jmx.access.MBeanClientInterceptor#setServer + * @see org.springframework.jmx.access.NotificationListenerRegistrar#setServer + */ +public class MBeanServerConnectionFactoryBean + implements FactoryBean, BeanClassLoaderAware, InitializingBean, DisposableBean { + + @Nullable + private JMXServiceURL serviceUrl; + + private Map environment = new HashMap<>(); + + private boolean connectOnStartup = true; + + @Nullable + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + @Nullable + private JMXConnector connector; + + @Nullable + private MBeanServerConnection connection; + + @Nullable + private JMXConnectorLazyInitTargetSource connectorTargetSource; + + + /** + * Set the service URL of the remote {@code MBeanServer}. + */ + public void setServiceUrl(String url) throws MalformedURLException { + this.serviceUrl = new JMXServiceURL(url); + } + + /** + * Set the environment properties used to construct the {@code JMXConnector} + * as {@code java.util.Properties} (String key/value pairs). + */ + public void setEnvironment(Properties environment) { + CollectionUtils.mergePropertiesIntoMap(environment, this.environment); + } + + /** + * Set the environment properties used to construct the {@code JMXConnector} + * as a {@code Map} of String keys and arbitrary Object values. + */ + public void setEnvironmentMap(@Nullable Map environment) { + if (environment != null) { + this.environment.putAll(environment); + } + } + + /** + * Set whether to connect to the server on startup. + *

    Default is {@code true}. + *

    Can be turned off to allow for late start of the JMX server. + * In this case, the JMX connector will be fetched on first access. + */ + public void setConnectOnStartup(boolean connectOnStartup) { + this.connectOnStartup = connectOnStartup; + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + + /** + * Creates a {@code JMXConnector} for the given settings + * and exposes the associated {@code MBeanServerConnection}. + */ + @Override + public void afterPropertiesSet() throws IOException { + if (this.serviceUrl == null) { + throw new IllegalArgumentException("Property 'serviceUrl' is required"); + } + + if (this.connectOnStartup) { + connect(); + } + else { + createLazyConnection(); + } + } + + /** + * Connects to the remote {@code MBeanServer} using the configured service URL and + * environment properties. + */ + private void connect() throws IOException { + Assert.state(this.serviceUrl != null, "No JMXServiceURL set"); + this.connector = JMXConnectorFactory.connect(this.serviceUrl, this.environment); + this.connection = this.connector.getMBeanServerConnection(); + } + + /** + * Creates lazy proxies for the {@code JMXConnector} and {@code MBeanServerConnection}. + */ + private void createLazyConnection() { + this.connectorTargetSource = new JMXConnectorLazyInitTargetSource(); + TargetSource connectionTargetSource = new MBeanServerConnectionLazyInitTargetSource(); + + this.connector = (JMXConnector) + new ProxyFactory(JMXConnector.class, this.connectorTargetSource).getProxy(this.beanClassLoader); + this.connection = (MBeanServerConnection) + new ProxyFactory(MBeanServerConnection.class, connectionTargetSource).getProxy(this.beanClassLoader); + } + + + @Override + @Nullable + public MBeanServerConnection getObject() { + return this.connection; + } + + @Override + public Class getObjectType() { + return (this.connection != null ? this.connection.getClass() : MBeanServerConnection.class); + } + + @Override + public boolean isSingleton() { + return true; + } + + + /** + * Closes the underlying {@code JMXConnector}. + */ + @Override + public void destroy() throws IOException { + if (this.connector != null && + (this.connectorTargetSource == null || this.connectorTargetSource.isInitialized())) { + this.connector.close(); + } + } + + + /** + * Lazily creates a {@code JMXConnector} using the configured service URL + * and environment properties. + * @see MBeanServerConnectionFactoryBean#setServiceUrl(String) + * @see MBeanServerConnectionFactoryBean#setEnvironment(java.util.Properties) + */ + private class JMXConnectorLazyInitTargetSource extends AbstractLazyCreationTargetSource { + + @Override + protected Object createObject() throws Exception { + Assert.state(serviceUrl != null, "No JMXServiceURL set"); + return JMXConnectorFactory.connect(serviceUrl, environment); + } + + @Override + public Class getTargetClass() { + return JMXConnector.class; + } + } + + + /** + * Lazily creates an {@code MBeanServerConnection}. + */ + private class MBeanServerConnectionLazyInitTargetSource extends AbstractLazyCreationTargetSource { + + @Override + protected Object createObject() throws Exception { + Assert.state(connector != null, "JMXConnector not initialized"); + return connector.getMBeanServerConnection(); + } + + @Override + public Class getTargetClass() { + return MBeanServerConnection.class; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/support/MBeanServerFactoryBean.java b/spring-context/src/main/java/org/springframework/jmx/support/MBeanServerFactoryBean.java new file mode 100644 index 0000000..d47626d --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/support/MBeanServerFactoryBean.java @@ -0,0 +1,216 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.support; + +import javax.management.MBeanServer; +import javax.management.MBeanServerFactory; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jmx.MBeanServerNotFoundException; +import org.springframework.lang.Nullable; + +/** + * {@link FactoryBean} that obtains a {@link javax.management.MBeanServer} reference + * through the standard JMX 1.2 {@link javax.management.MBeanServerFactory} + * API. + * + *

    Exposes the {@code MBeanServer} for bean references. + * + *

    By default, {@code MBeanServerFactoryBean} will always create + * a new {@code MBeanServer} even if one is already running. To have + * the {@code MBeanServerFactoryBean} attempt to locate a running + * {@code MBeanServer} first, set the value of the + * "locateExistingServerIfPossible" property to "true". + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see #setLocateExistingServerIfPossible + * @see #locateMBeanServer + * @see javax.management.MBeanServer + * @see javax.management.MBeanServerFactory#findMBeanServer + * @see javax.management.MBeanServerFactory#createMBeanServer + * @see javax.management.MBeanServerFactory#newMBeanServer + * @see MBeanServerConnectionFactoryBean + * @see ConnectorServerFactoryBean + */ +public class MBeanServerFactoryBean implements FactoryBean, InitializingBean, DisposableBean { + + protected final Log logger = LogFactory.getLog(getClass()); + + private boolean locateExistingServerIfPossible = false; + + @Nullable + private String agentId; + + @Nullable + private String defaultDomain; + + private boolean registerWithFactory = true; + + @Nullable + private MBeanServer server; + + private boolean newlyRegistered = false; + + + /** + * Set whether or not the {@code MBeanServerFactoryBean} should attempt + * to locate a running {@code MBeanServer} before creating one. + *

    Default is {@code false}. + */ + public void setLocateExistingServerIfPossible(boolean locateExistingServerIfPossible) { + this.locateExistingServerIfPossible = locateExistingServerIfPossible; + } + + /** + * Set the agent id of the {@code MBeanServer} to locate. + *

    Default is none. If specified, this will result in an + * automatic attempt being made to locate the attendant MBeanServer, + * and (importantly) if said MBeanServer cannot be located no + * attempt will be made to create a new MBeanServer (and an + * MBeanServerNotFoundException will be thrown at resolution time). + *

    Specifying the empty String indicates the platform MBeanServer. + * @see javax.management.MBeanServerFactory#findMBeanServer(String) + */ + public void setAgentId(String agentId) { + this.agentId = agentId; + } + + /** + * Set the default domain to be used by the {@code MBeanServer}, + * to be passed to {@code MBeanServerFactory.createMBeanServer()} + * or {@code MBeanServerFactory.findMBeanServer()}. + *

    Default is none. + * @see javax.management.MBeanServerFactory#createMBeanServer(String) + * @see javax.management.MBeanServerFactory#findMBeanServer(String) + */ + public void setDefaultDomain(String defaultDomain) { + this.defaultDomain = defaultDomain; + } + + /** + * Set whether to register the {@code MBeanServer} with the + * {@code MBeanServerFactory}, making it available through + * {@code MBeanServerFactory.findMBeanServer()}. + *

    Default is {@code true}. + * @see javax.management.MBeanServerFactory#createMBeanServer + * @see javax.management.MBeanServerFactory#findMBeanServer + */ + public void setRegisterWithFactory(boolean registerWithFactory) { + this.registerWithFactory = registerWithFactory; + } + + + /** + * Creates the {@code MBeanServer} instance. + */ + @Override + public void afterPropertiesSet() throws MBeanServerNotFoundException { + // Try to locate existing MBeanServer, if desired. + if (this.locateExistingServerIfPossible || this.agentId != null) { + try { + this.server = locateMBeanServer(this.agentId); + } + catch (MBeanServerNotFoundException ex) { + // If agentId was specified, we were only supposed to locate that + // specific MBeanServer; so let's bail if we can't find it. + if (this.agentId != null) { + throw ex; + } + logger.debug("No existing MBeanServer found - creating new one"); + } + } + + // Create a new MBeanServer and register it, if desired. + if (this.server == null) { + this.server = createMBeanServer(this.defaultDomain, this.registerWithFactory); + this.newlyRegistered = this.registerWithFactory; + } + } + + /** + * Attempt to locate an existing {@code MBeanServer}. + * Called if {@code locateExistingServerIfPossible} is set to {@code true}. + *

    The default implementation attempts to find an {@code MBeanServer} using + * a standard lookup. Subclasses may override to add additional location logic. + * @param agentId the agent identifier of the MBeanServer to retrieve. + * If this parameter is {@code null}, all registered MBeanServers are + * considered. + * @return the {@code MBeanServer} if found + * @throws org.springframework.jmx.MBeanServerNotFoundException + * if no {@code MBeanServer} could be found + * @see #setLocateExistingServerIfPossible + * @see JmxUtils#locateMBeanServer(String) + * @see javax.management.MBeanServerFactory#findMBeanServer(String) + */ + protected MBeanServer locateMBeanServer(@Nullable String agentId) throws MBeanServerNotFoundException { + return JmxUtils.locateMBeanServer(agentId); + } + + /** + * Create a new {@code MBeanServer} instance and register it with the + * {@code MBeanServerFactory}, if desired. + * @param defaultDomain the default domain, or {@code null} if none + * @param registerWithFactory whether to register the {@code MBeanServer} + * with the {@code MBeanServerFactory} + * @see javax.management.MBeanServerFactory#createMBeanServer + * @see javax.management.MBeanServerFactory#newMBeanServer + */ + protected MBeanServer createMBeanServer(@Nullable String defaultDomain, boolean registerWithFactory) { + if (registerWithFactory) { + return MBeanServerFactory.createMBeanServer(defaultDomain); + } + else { + return MBeanServerFactory.newMBeanServer(defaultDomain); + } + } + + + @Override + @Nullable + public MBeanServer getObject() { + return this.server; + } + + @Override + public Class getObjectType() { + return (this.server != null ? this.server.getClass() : MBeanServer.class); + } + + @Override + public boolean isSingleton() { + return true; + } + + + /** + * Unregisters the {@code MBeanServer} instance, if necessary. + */ + @Override + public void destroy() { + if (this.newlyRegistered) { + MBeanServerFactory.releaseMBeanServer(this.server); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/support/MetricType.java b/spring-context/src/main/java/org/springframework/jmx/support/MetricType.java new file mode 100644 index 0000000..01e3aff --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/support/MetricType.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.support; + +/** + * Represents how the measurement values of a {@code ManagedMetric} will change over time. + * + * @author Jennifer Hickey + * @since 3.0 + */ +public enum MetricType { + + /** + * The measurement values may go up or down over time. + */ + GAUGE, + + /** + * The measurement values will always increase. + */ + COUNTER + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/support/NotificationListenerHolder.java b/spring-context/src/main/java/org/springframework/jmx/support/NotificationListenerHolder.java new file mode 100644 index 0000000..ac573bb --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/support/NotificationListenerHolder.java @@ -0,0 +1,183 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.support; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import javax.management.MalformedObjectNameException; +import javax.management.NotificationFilter; +import javax.management.NotificationListener; +import javax.management.ObjectName; + +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +/** + * Helper class that aggregates a {@link javax.management.NotificationListener}, + * a {@link javax.management.NotificationFilter}, and an arbitrary handback + * object, as well as the names of MBeans from which the listener wishes + * to receive {@link javax.management.Notification Notifications}. + * + * @author Juergen Hoeller + * @since 2.5.2 + * @see org.springframework.jmx.export.NotificationListenerBean + * @see org.springframework.jmx.access.NotificationListenerRegistrar + */ +public class NotificationListenerHolder { + + @Nullable + private NotificationListener notificationListener; + + @Nullable + private NotificationFilter notificationFilter; + + @Nullable + private Object handback; + + @Nullable + protected Set mappedObjectNames; + + + /** + * Set the {@link javax.management.NotificationListener}. + */ + public void setNotificationListener(@Nullable NotificationListener notificationListener) { + this.notificationListener = notificationListener; + } + + /** + * Get the {@link javax.management.NotificationListener}. + */ + @Nullable + public NotificationListener getNotificationListener() { + return this.notificationListener; + } + + /** + * Set the {@link javax.management.NotificationFilter} associated + * with the encapsulated {@link #getNotificationFilter() NotificationFilter}. + *

    May be {@code null}. + */ + public void setNotificationFilter(@Nullable NotificationFilter notificationFilter) { + this.notificationFilter = notificationFilter; + } + + /** + * Return the {@link javax.management.NotificationFilter} associated + * with the encapsulated {@link #getNotificationListener() NotificationListener}. + *

    May be {@code null}. + */ + @Nullable + public NotificationFilter getNotificationFilter() { + return this.notificationFilter; + } + + /** + * Set the (arbitrary) object that will be 'handed back' as-is by an + * {@link javax.management.NotificationBroadcaster} when notifying + * any {@link javax.management.NotificationListener}. + * @param handback the handback object (can be {@code null}) + * @see javax.management.NotificationListener#handleNotification(javax.management.Notification, Object) + */ + public void setHandback(@Nullable Object handback) { + this.handback = handback; + } + + /** + * Return the (arbitrary) object that will be 'handed back' as-is by an + * {@link javax.management.NotificationBroadcaster} when notifying + * any {@link javax.management.NotificationListener}. + * @return the handback object (may be {@code null}) + * @see javax.management.NotificationListener#handleNotification(javax.management.Notification, Object) + */ + @Nullable + public Object getHandback() { + return this.handback; + } + + /** + * Set the {@link javax.management.ObjectName}-style name of the single MBean + * that the encapsulated {@link #getNotificationFilter() NotificationFilter} + * will be registered with to listen for {@link javax.management.Notification Notifications}. + * Can be specified as {@code ObjectName} instance or as {@code String}. + * @see #setMappedObjectNames + */ + public void setMappedObjectName(@Nullable Object mappedObjectName) { + this.mappedObjectNames = (mappedObjectName != null ? + new LinkedHashSet<>(Collections.singleton(mappedObjectName)) : null); + } + + /** + * Set an array of {@link javax.management.ObjectName}-style names of the MBeans + * that the encapsulated {@link #getNotificationFilter() NotificationFilter} + * will be registered with to listen for {@link javax.management.Notification Notifications}. + * Can be specified as {@code ObjectName} instances or as {@code String}s. + * @see #setMappedObjectName + */ + public void setMappedObjectNames(Object... mappedObjectNames) { + this.mappedObjectNames = new LinkedHashSet<>(Arrays.asList(mappedObjectNames)); + } + + /** + * Return the list of {@link javax.management.ObjectName} String representations for + * which the encapsulated {@link #getNotificationFilter() NotificationFilter} will + * be registered as a listener for {@link javax.management.Notification Notifications}. + * @throws MalformedObjectNameException if an {@code ObjectName} is malformed + */ + @Nullable + public ObjectName[] getResolvedObjectNames() throws MalformedObjectNameException { + if (this.mappedObjectNames == null) { + return null; + } + ObjectName[] resolved = new ObjectName[this.mappedObjectNames.size()]; + int i = 0; + for (Object objectName : this.mappedObjectNames) { + resolved[i] = ObjectNameManager.getInstance(objectName); + i++; + } + return resolved; + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof NotificationListenerHolder)) { + return false; + } + NotificationListenerHolder otherNlh = (NotificationListenerHolder) other; + return (ObjectUtils.nullSafeEquals(this.notificationListener, otherNlh.notificationListener) && + ObjectUtils.nullSafeEquals(this.notificationFilter, otherNlh.notificationFilter) && + ObjectUtils.nullSafeEquals(this.handback, otherNlh.handback) && + ObjectUtils.nullSafeEquals(this.mappedObjectNames, otherNlh.mappedObjectNames)); + } + + @Override + public int hashCode() { + int hashCode = ObjectUtils.nullSafeHashCode(this.notificationListener); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.notificationFilter); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.handback); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.mappedObjectNames); + return hashCode; + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/support/ObjectNameManager.java b/spring-context/src/main/java/org/springframework/jmx/support/ObjectNameManager.java new file mode 100644 index 0000000..a3941b1 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/support/ObjectNameManager.java @@ -0,0 +1,103 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.support; + +import java.util.Hashtable; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +/** + * Helper class for the creation of {@link javax.management.ObjectName} instances. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 1.2 + * @see javax.management.ObjectName#getInstance(String) + */ +public final class ObjectNameManager { + + private ObjectNameManager() { + } + + + /** + * Retrieve the {@code ObjectName} instance corresponding to the supplied name. + * @param objectName the {@code ObjectName} in {@code ObjectName} or + * {@code String} format + * @return the {@code ObjectName} instance + * @throws MalformedObjectNameException in case of an invalid object name specification + * @see ObjectName#ObjectName(String) + * @see ObjectName#getInstance(String) + */ + public static ObjectName getInstance(Object objectName) throws MalformedObjectNameException { + if (objectName instanceof ObjectName) { + return (ObjectName) objectName; + } + if (!(objectName instanceof String)) { + throw new MalformedObjectNameException("Invalid ObjectName value type [" + + objectName.getClass().getName() + "]: only ObjectName and String supported."); + } + return getInstance((String) objectName); + } + + /** + * Retrieve the {@code ObjectName} instance corresponding to the supplied name. + * @param objectName the {@code ObjectName} in {@code String} format + * @return the {@code ObjectName} instance + * @throws MalformedObjectNameException in case of an invalid object name specification + * @see ObjectName#ObjectName(String) + * @see ObjectName#getInstance(String) + */ + public static ObjectName getInstance(String objectName) throws MalformedObjectNameException { + return ObjectName.getInstance(objectName); + } + + /** + * Retrieve an {@code ObjectName} instance for the specified domain and a + * single property with the supplied key and value. + * @param domainName the domain name for the {@code ObjectName} + * @param key the key for the single property in the {@code ObjectName} + * @param value the value for the single property in the {@code ObjectName} + * @return the {@code ObjectName} instance + * @throws MalformedObjectNameException in case of an invalid object name specification + * @see ObjectName#ObjectName(String, String, String) + * @see ObjectName#getInstance(String, String, String) + */ + public static ObjectName getInstance(String domainName, String key, String value) + throws MalformedObjectNameException { + + return ObjectName.getInstance(domainName, key, value); + } + + /** + * Retrieve an {@code ObjectName} instance with the specified domain name + * and the supplied key/name properties. + * @param domainName the domain name for the {@code ObjectName} + * @param properties the properties for the {@code ObjectName} + * @return the {@code ObjectName} instance + * @throws MalformedObjectNameException in case of an invalid object name specification + * @see ObjectName#ObjectName(String, java.util.Hashtable) + * @see ObjectName#getInstance(String, java.util.Hashtable) + */ + public static ObjectName getInstance(String domainName, Hashtable properties) + throws MalformedObjectNameException { + + return ObjectName.getInstance(domainName, properties); + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/support/RegistrationPolicy.java b/spring-context/src/main/java/org/springframework/jmx/support/RegistrationPolicy.java new file mode 100644 index 0000000..4c0da82 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/support/RegistrationPolicy.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.support; + +/** + * Indicates registration behavior when attempting to register an MBean that already + * exists. + * + * @author Phillip Webb + * @author Chris Beams + * @since 3.2 + */ +public enum RegistrationPolicy { + + /** + * Registration should fail when attempting to register an MBean under a name that + * already exists. + */ + FAIL_ON_EXISTING, + + /** + * Registration should ignore the affected MBean when attempting to register an MBean + * under a name that already exists. + */ + IGNORE_EXISTING, + + /** + * Registration should replace the affected MBean when attempting to register an MBean + * under a name that already exists. + */ + REPLACE_EXISTING + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/support/WebSphereMBeanServerFactoryBean.java b/spring-context/src/main/java/org/springframework/jmx/support/WebSphereMBeanServerFactoryBean.java new file mode 100644 index 0000000..fc0d4b8 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/support/WebSphereMBeanServerFactoryBean.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.support; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import javax.management.MBeanServer; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jmx.MBeanServerNotFoundException; +import org.springframework.lang.Nullable; + +/** + * {@link FactoryBean} that obtains a WebSphere {@link javax.management.MBeanServer} + * reference through WebSphere's proprietary {@code AdminServiceFactory} API, + * available on WebSphere 5.1 and higher. + * + *

    Exposes the {@code MBeanServer} for bean references. + * + *

    This {@code FactoryBean} is a direct alternative to {@link MBeanServerFactoryBean}, + * which uses standard JMX 1.2 API to access the platform's {@link MBeanServer}. + * + *

    See the javadocs for WebSphere's + * {@code AdminServiceFactory} + * and + * {@code MBeanFactory}. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @since 2.0.3 + * @see javax.management.MBeanServer + * @see MBeanServerFactoryBean + */ +public class WebSphereMBeanServerFactoryBean implements FactoryBean, InitializingBean { + + private static final String ADMIN_SERVICE_FACTORY_CLASS = "com.ibm.websphere.management.AdminServiceFactory"; + + private static final String GET_MBEAN_FACTORY_METHOD = "getMBeanFactory"; + + private static final String GET_MBEAN_SERVER_METHOD = "getMBeanServer"; + + + @Nullable + private MBeanServer mbeanServer; + + + @Override + public void afterPropertiesSet() throws MBeanServerNotFoundException { + try { + /* + * this.mbeanServer = AdminServiceFactory.getMBeanFactory().getMBeanServer(); + */ + Class adminServiceClass = getClass().getClassLoader().loadClass(ADMIN_SERVICE_FACTORY_CLASS); + Method getMBeanFactoryMethod = adminServiceClass.getMethod(GET_MBEAN_FACTORY_METHOD); + Object mbeanFactory = getMBeanFactoryMethod.invoke(null); + Method getMBeanServerMethod = mbeanFactory.getClass().getMethod(GET_MBEAN_SERVER_METHOD); + this.mbeanServer = (MBeanServer) getMBeanServerMethod.invoke(mbeanFactory); + } + catch (ClassNotFoundException ex) { + throw new MBeanServerNotFoundException("Could not find WebSphere's AdminServiceFactory class", ex); + } + catch (InvocationTargetException ex) { + throw new MBeanServerNotFoundException( + "WebSphere's AdminServiceFactory.getMBeanFactory/getMBeanServer method failed", ex.getTargetException()); + } + catch (Exception ex) { + throw new MBeanServerNotFoundException( + "Could not access WebSphere's AdminServiceFactory.getMBeanFactory/getMBeanServer method", ex); + } + } + + + @Override + @Nullable + public MBeanServer getObject() { + return this.mbeanServer; + } + + @Override + public Class getObjectType() { + return (this.mbeanServer != null ? this.mbeanServer.getClass() : MBeanServer.class); + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/spring-context/src/main/java/org/springframework/jmx/support/package-info.java b/spring-context/src/main/java/org/springframework/jmx/support/package-info.java new file mode 100644 index 0000000..d648547 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jmx/support/package-info.java @@ -0,0 +1,10 @@ +/** + * Contains support classes for connecting to local and remote {@code MBeanServer}s + * and for exposing an {@code MBeanServer} to remote clients. + */ +@NonNullApi +@NonNullFields +package org.springframework.jmx.support; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/jndi/JndiAccessor.java b/spring-context/src/main/java/org/springframework/jndi/JndiAccessor.java new file mode 100644 index 0000000..44ed724 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jndi/JndiAccessor.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jndi; + +import java.util.Properties; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.lang.Nullable; + +/** + * Convenient superclass for JNDI accessors, providing "jndiTemplate" + * and "jndiEnvironment" bean properties. + * + * @author Juergen Hoeller + * @since 1.1 + * @see #setJndiTemplate + * @see #setJndiEnvironment + */ +public class JndiAccessor { + + /** + * Logger, available to subclasses. + */ + protected final Log logger = LogFactory.getLog(getClass()); + + private JndiTemplate jndiTemplate = new JndiTemplate(); + + + /** + * Set the JNDI template to use for JNDI lookups. + *

    You can also specify JNDI environment settings via "jndiEnvironment". + * @see #setJndiEnvironment + */ + public void setJndiTemplate(@Nullable JndiTemplate jndiTemplate) { + this.jndiTemplate = (jndiTemplate != null ? jndiTemplate : new JndiTemplate()); + } + + /** + * Return the JNDI template to use for JNDI lookups. + */ + public JndiTemplate getJndiTemplate() { + return this.jndiTemplate; + } + + /** + * Set the JNDI environment to use for JNDI lookups. + *

    Creates a JndiTemplate with the given environment settings. + * @see #setJndiTemplate + */ + public void setJndiEnvironment(@Nullable Properties jndiEnvironment) { + this.jndiTemplate = new JndiTemplate(jndiEnvironment); + } + + /** + * Return the JNDI environment to use for JNDI lookups. + */ + @Nullable + public Properties getJndiEnvironment() { + return this.jndiTemplate.getEnvironment(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/jndi/JndiCallback.java b/spring-context/src/main/java/org/springframework/jndi/JndiCallback.java new file mode 100644 index 0000000..c463cdb --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jndi/JndiCallback.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jndi; + +import javax.naming.Context; +import javax.naming.NamingException; + +import org.springframework.lang.Nullable; + +/** + * Callback interface to be implemented by classes that need to perform an + * operation (such as a lookup) in a JNDI context. This callback approach + * is valuable in simplifying error handling, which is performed by the + * JndiTemplate class. This is a similar to JdbcTemplate's approach. + * + *

    Note that there is hardly any need to implement this callback + * interface, as JndiTemplate provides all usual JNDI operations via + * convenience methods. + * + * @author Rod Johnson + * @param the resulting object type + * @see JndiTemplate + * @see org.springframework.jdbc.core.JdbcTemplate + */ +@FunctionalInterface +public interface JndiCallback { + + /** + * Do something with the given JNDI context. + *

    Implementations don't need to worry about error handling + * or cleanup, as the JndiTemplate class will handle this. + * @param ctx the current JNDI context + * @throws NamingException if thrown by JNDI methods + * @return a result object, or {@code null} + */ + @Nullable + T doInContext(Context ctx) throws NamingException; + +} + diff --git a/spring-context/src/main/java/org/springframework/jndi/JndiLocatorDelegate.java b/spring-context/src/main/java/org/springframework/jndi/JndiLocatorDelegate.java new file mode 100644 index 0000000..7b0c0f2 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jndi/JndiLocatorDelegate.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jndi; + +import javax.naming.InitialContext; +import javax.naming.NamingException; + +import org.springframework.core.SpringProperties; +import org.springframework.lang.Nullable; + +/** + * {@link JndiLocatorSupport} subclass with public lookup methods, + * for convenient use as a delegate. + * + * @author Juergen Hoeller + * @since 3.0.1 + */ +public class JndiLocatorDelegate extends JndiLocatorSupport { + + /** + * System property that instructs Spring to ignore a default JNDI environment, i.e. + * to always return {@code false} from {@link #isDefaultJndiEnvironmentAvailable()}. + *

    The default is "false", allowing for regular default JNDI access e.g. in + * {@link JndiPropertySource}. Switching this flag to {@code true} is an optimization + * for scenarios where nothing is ever to be found for such JNDI fallback searches + * to begin with, avoiding the repeated JNDI lookup overhead. + *

    Note that this flag just affects JNDI fallback searches, not explicitly configured + * JNDI lookups such as for a {@code DataSource} or some other environment resource. + * The flag literally just affects code which attempts JNDI searches based on the + * {@code JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()} check: in particular, + * {@code StandardServletEnvironment} and {@code StandardPortletEnvironment}. + * @since 4.3 + * @see #isDefaultJndiEnvironmentAvailable() + * @see JndiPropertySource + */ + public static final String IGNORE_JNDI_PROPERTY_NAME = "spring.jndi.ignore"; + + + private static final boolean shouldIgnoreDefaultJndiEnvironment = + SpringProperties.getFlag(IGNORE_JNDI_PROPERTY_NAME); + + + @Override + public Object lookup(String jndiName) throws NamingException { + return super.lookup(jndiName); + } + + @Override + public T lookup(String jndiName, @Nullable Class requiredType) throws NamingException { + return super.lookup(jndiName, requiredType); + } + + + /** + * Configure a {@code JndiLocatorDelegate} with its "resourceRef" property set to + * {@code true}, meaning that all names will be prefixed with "java:comp/env/". + * @see #setResourceRef + */ + public static JndiLocatorDelegate createDefaultResourceRefLocator() { + JndiLocatorDelegate jndiLocator = new JndiLocatorDelegate(); + jndiLocator.setResourceRef(true); + return jndiLocator; + } + + /** + * Check whether a default JNDI environment, as in a Java EE environment, + * is available on this JVM. + * @return {@code true} if a default InitialContext can be used, + * {@code false} if not + */ + public static boolean isDefaultJndiEnvironmentAvailable() { + if (shouldIgnoreDefaultJndiEnvironment) { + return false; + } + try { + new InitialContext().getEnvironment(); + return true; + } + catch (Throwable ex) { + return false; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/jndi/JndiLocatorSupport.java b/spring-context/src/main/java/org/springframework/jndi/JndiLocatorSupport.java new file mode 100644 index 0000000..be91015 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jndi/JndiLocatorSupport.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jndi; + +import javax.naming.NamingException; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Convenient superclass for classes that can locate any number of JNDI objects. + * Derives from JndiAccessor to inherit the "jndiTemplate" and "jndiEnvironment" + * bean properties. + * + *

    JNDI names may or may not include the "java:comp/env/" prefix expected + * by Java EE applications when accessing a locally mapped (ENC - Environmental + * Naming Context) resource. If it doesn't, the "java:comp/env/" prefix will + * be prepended if the "resourceRef" property is true (the default is + * false) and no other scheme (e.g. "java:") is given. + * + * @author Juergen Hoeller + * @since 1.1 + * @see #setJndiTemplate + * @see #setJndiEnvironment + * @see #setResourceRef + */ +public abstract class JndiLocatorSupport extends JndiAccessor { + + /** JNDI prefix used in a Java EE container. */ + public static final String CONTAINER_PREFIX = "java:comp/env/"; + + + private boolean resourceRef = false; + + + /** + * Set whether the lookup occurs in a Java EE container, i.e. if the prefix + * "java:comp/env/" needs to be added if the JNDI name doesn't already + * contain it. Default is "false". + *

    Note: Will only get applied if no other scheme (e.g. "java:") is given. + */ + public void setResourceRef(boolean resourceRef) { + this.resourceRef = resourceRef; + } + + /** + * Return whether the lookup occurs in a Java EE container. + */ + public boolean isResourceRef() { + return this.resourceRef; + } + + + /** + * Perform an actual JNDI lookup for the given name via the JndiTemplate. + *

    If the name doesn't begin with "java:comp/env/", this prefix is added + * if "resourceRef" is set to "true". + * @param jndiName the JNDI name to look up + * @return the obtained object + * @throws NamingException if the JNDI lookup failed + * @see #setResourceRef + */ + protected Object lookup(String jndiName) throws NamingException { + return lookup(jndiName, null); + } + + /** + * Perform an actual JNDI lookup for the given name via the JndiTemplate. + *

    If the name doesn't begin with "java:comp/env/", this prefix is added + * if "resourceRef" is set to "true". + * @param jndiName the JNDI name to look up + * @param requiredType the required type of the object + * @return the obtained object + * @throws NamingException if the JNDI lookup failed + * @see #setResourceRef + */ + protected T lookup(String jndiName, @Nullable Class requiredType) throws NamingException { + Assert.notNull(jndiName, "'jndiName' must not be null"); + String convertedName = convertJndiName(jndiName); + T jndiObject; + try { + jndiObject = getJndiTemplate().lookup(convertedName, requiredType); + } + catch (NamingException ex) { + if (!convertedName.equals(jndiName)) { + // Try fallback to originally specified name... + if (logger.isDebugEnabled()) { + logger.debug("Converted JNDI name [" + convertedName + + "] not found - trying original name [" + jndiName + "]. " + ex); + } + jndiObject = getJndiTemplate().lookup(jndiName, requiredType); + } + else { + throw ex; + } + } + if (logger.isDebugEnabled()) { + logger.debug("Located object with JNDI name [" + convertedName + "]"); + } + return jndiObject; + } + + /** + * Convert the given JNDI name into the actual JNDI name to use. + *

    The default implementation applies the "java:comp/env/" prefix if + * "resourceRef" is "true" and no other scheme (e.g. "java:") is given. + * @param jndiName the original JNDI name + * @return the JNDI name to use + * @see #CONTAINER_PREFIX + * @see #setResourceRef + */ + protected String convertJndiName(String jndiName) { + // Prepend container prefix if not already specified and no other scheme given. + if (isResourceRef() && !jndiName.startsWith(CONTAINER_PREFIX) && jndiName.indexOf(':') == -1) { + jndiName = CONTAINER_PREFIX + jndiName; + } + return jndiName; + } + +} diff --git a/spring-context/src/main/java/org/springframework/jndi/JndiLookupFailureException.java b/spring-context/src/main/java/org/springframework/jndi/JndiLookupFailureException.java new file mode 100644 index 0000000..607222e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jndi/JndiLookupFailureException.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jndi; + +import javax.naming.NamingException; + +import org.springframework.core.NestedRuntimeException; + +/** + * RuntimeException to be thrown in case of JNDI lookup failures, + * in particular from code that does not declare JNDI's checked + * {@link javax.naming.NamingException}: for example, from Spring's + * {@link JndiObjectTargetSource}. + * + * @author Juergen Hoeller + * @since 2.0.3 + */ +@SuppressWarnings("serial") +public class JndiLookupFailureException extends NestedRuntimeException { + + /** + * Construct a new JndiLookupFailureException, + * wrapping the given JNDI NamingException. + * @param msg the detail message + * @param cause the NamingException root cause + */ + public JndiLookupFailureException(String msg, NamingException cause) { + super(msg, cause); + } + +} diff --git a/spring-context/src/main/java/org/springframework/jndi/JndiObjectFactoryBean.java b/spring-context/src/main/java/org/springframework/jndi/JndiObjectFactoryBean.java new file mode 100644 index 0000000..9f2d079 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jndi/JndiObjectFactoryBean.java @@ -0,0 +1,387 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jndi; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import javax.naming.Context; +import javax.naming.NamingException; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.SimpleTypeConverter; +import org.springframework.beans.TypeConverter; +import org.springframework.beans.TypeMismatchException; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * {@link org.springframework.beans.factory.FactoryBean} that looks up a + * JNDI object. Exposes the object found in JNDI for bean references, + * e.g. for data access object's "dataSource" property in case of a + * {@link javax.sql.DataSource}. + * + *

    The typical usage will be to register this as singleton factory + * (e.g. for a certain JNDI-bound DataSource) in an application context, + * and give bean references to application services that need it. + * + *

    The default behavior is to look up the JNDI object on startup and cache it. + * This can be customized through the "lookupOnStartup" and "cache" properties, + * using a {@link JndiObjectTargetSource} underneath. Note that you need to specify + * a "proxyInterface" in such a scenario, since the actual JNDI object type is not + * known in advance. + * + *

    Of course, bean classes in a Spring environment may lookup e.g. a DataSource + * from JNDI themselves. This class simply enables central configuration of the + * JNDI name, and easy switching to non-JNDI alternatives. The latter is + * particularly convenient for test setups, reuse in standalone clients, etc. + * + *

    Note that switching to e.g. DriverManagerDataSource is just a matter of + * configuration: Simply replace the definition of this FactoryBean with a + * {@link org.springframework.jdbc.datasource.DriverManagerDataSource} definition! + * + * @author Juergen Hoeller + * @since 22.05.2003 + * @see #setProxyInterface + * @see #setLookupOnStartup + * @see #setCache + * @see JndiObjectTargetSource + */ +public class JndiObjectFactoryBean extends JndiObjectLocator + implements FactoryBean, BeanFactoryAware, BeanClassLoaderAware { + + @Nullable + private Class[] proxyInterfaces; + + private boolean lookupOnStartup = true; + + private boolean cache = true; + + private boolean exposeAccessContext = false; + + @Nullable + private Object defaultObject; + + @Nullable + private ConfigurableBeanFactory beanFactory; + + @Nullable + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + @Nullable + private Object jndiObject; + + + /** + * Specify the proxy interface to use for the JNDI object. + *

    Typically used in conjunction with "lookupOnStartup"=false and/or "cache"=false. + * Needs to be specified because the actual JNDI object type is not known + * in advance in case of a lazy lookup. + * @see #setProxyInterfaces + * @see #setLookupOnStartup + * @see #setCache + */ + public void setProxyInterface(Class proxyInterface) { + this.proxyInterfaces = new Class[] {proxyInterface}; + } + + /** + * Specify multiple proxy interfaces to use for the JNDI object. + *

    Typically used in conjunction with "lookupOnStartup"=false and/or "cache"=false. + * Note that proxy interfaces will be autodetected from a specified "expectedType", + * if necessary. + * @see #setExpectedType + * @see #setLookupOnStartup + * @see #setCache + */ + public void setProxyInterfaces(Class... proxyInterfaces) { + this.proxyInterfaces = proxyInterfaces; + } + + /** + * Set whether to look up the JNDI object on startup. Default is "true". + *

    Can be turned off to allow for late availability of the JNDI object. + * In this case, the JNDI object will be fetched on first access. + *

    For a lazy lookup, a proxy interface needs to be specified. + * @see #setProxyInterface + * @see #setCache + */ + public void setLookupOnStartup(boolean lookupOnStartup) { + this.lookupOnStartup = lookupOnStartup; + } + + /** + * Set whether to cache the JNDI object once it has been located. + * Default is "true". + *

    Can be turned off to allow for hot redeployment of JNDI objects. + * In this case, the JNDI object will be fetched for each invocation. + *

    For hot redeployment, a proxy interface needs to be specified. + * @see #setProxyInterface + * @see #setLookupOnStartup + */ + public void setCache(boolean cache) { + this.cache = cache; + } + + /** + * Set whether to expose the JNDI environment context for all access to the target + * object, i.e. for all method invocations on the exposed object reference. + *

    Default is "false", i.e. to only expose the JNDI context for object lookup. + * Switch this flag to "true" in order to expose the JNDI environment (including + * the authorization context) for each method invocation, as needed by WebLogic + * for JNDI-obtained factories (e.g. JDBC DataSource, JMS ConnectionFactory) + * with authorization requirements. + */ + public void setExposeAccessContext(boolean exposeAccessContext) { + this.exposeAccessContext = exposeAccessContext; + } + + /** + * Specify a default object to fall back to if the JNDI lookup fails. + * Default is none. + *

    This can be an arbitrary bean reference or literal value. + * It is typically used for literal values in scenarios where the JNDI environment + * might define specific config settings but those are not required to be present. + *

    Note: This is only supported for lookup on startup. + * If specified together with {@link #setExpectedType}, the specified value + * needs to be either of that type or convertible to it. + * @see #setLookupOnStartup + * @see ConfigurableBeanFactory#getTypeConverter() + * @see SimpleTypeConverter + */ + public void setDefaultObject(Object defaultObject) { + this.defaultObject = defaultObject; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + if (beanFactory instanceof ConfigurableBeanFactory) { + // Just optional - for getting a specifically configured TypeConverter if needed. + // We'll simply fall back to a SimpleTypeConverter if no specific one available. + this.beanFactory = (ConfigurableBeanFactory) beanFactory; + } + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + + /** + * Look up the JNDI object and store it. + */ + @Override + public void afterPropertiesSet() throws IllegalArgumentException, NamingException { + super.afterPropertiesSet(); + + if (this.proxyInterfaces != null || !this.lookupOnStartup || !this.cache || this.exposeAccessContext) { + // We need to create a proxy for this... + if (this.defaultObject != null) { + throw new IllegalArgumentException( + "'defaultObject' is not supported in combination with 'proxyInterface'"); + } + // We need a proxy and a JndiObjectTargetSource. + this.jndiObject = JndiObjectProxyFactory.createJndiObjectProxy(this); + } + else { + if (this.defaultObject != null && getExpectedType() != null && + !getExpectedType().isInstance(this.defaultObject)) { + TypeConverter converter = (this.beanFactory != null ? + this.beanFactory.getTypeConverter() : new SimpleTypeConverter()); + try { + this.defaultObject = converter.convertIfNecessary(this.defaultObject, getExpectedType()); + } + catch (TypeMismatchException ex) { + throw new IllegalArgumentException("Default object [" + this.defaultObject + "] of type [" + + this.defaultObject.getClass().getName() + "] is not of expected type [" + + getExpectedType().getName() + "] and cannot be converted either", ex); + } + } + // Locate specified JNDI object. + this.jndiObject = lookupWithFallback(); + } + } + + /** + * Lookup variant that returns the specified "defaultObject" + * (if any) in case of lookup failure. + * @return the located object, or the "defaultObject" as fallback + * @throws NamingException in case of lookup failure without fallback + * @see #setDefaultObject + */ + protected Object lookupWithFallback() throws NamingException { + ClassLoader originalClassLoader = ClassUtils.overrideThreadContextClassLoader(this.beanClassLoader); + try { + return lookup(); + } + catch (TypeMismatchNamingException ex) { + // Always let TypeMismatchNamingException through - + // we don't want to fall back to the defaultObject in this case. + throw ex; + } + catch (NamingException ex) { + if (this.defaultObject != null) { + if (logger.isTraceEnabled()) { + logger.trace("JNDI lookup failed - returning specified default object instead", ex); + } + else if (logger.isDebugEnabled()) { + logger.debug("JNDI lookup failed - returning specified default object instead: " + ex); + } + return this.defaultObject; + } + throw ex; + } + finally { + if (originalClassLoader != null) { + Thread.currentThread().setContextClassLoader(originalClassLoader); + } + } + } + + + /** + * Return the singleton JNDI object. + */ + @Override + @Nullable + public Object getObject() { + return this.jndiObject; + } + + @Override + public Class getObjectType() { + if (this.proxyInterfaces != null) { + if (this.proxyInterfaces.length == 1) { + return this.proxyInterfaces[0]; + } + else if (this.proxyInterfaces.length > 1) { + return createCompositeInterface(this.proxyInterfaces); + } + } + if (this.jndiObject != null) { + return this.jndiObject.getClass(); + } + else { + return getExpectedType(); + } + } + + @Override + public boolean isSingleton() { + return true; + } + + + /** + * Create a composite interface Class for the given interfaces, + * implementing the given interfaces in one single Class. + *

    The default implementation builds a JDK proxy class for the + * given interfaces. + * @param interfaces the interfaces to merge + * @return the merged interface as Class + * @see java.lang.reflect.Proxy#getProxyClass + */ + protected Class createCompositeInterface(Class[] interfaces) { + return ClassUtils.createCompositeInterface(interfaces, this.beanClassLoader); + } + + + /** + * Inner class to just introduce an AOP dependency when actually creating a proxy. + */ + private static class JndiObjectProxyFactory { + + private static Object createJndiObjectProxy(JndiObjectFactoryBean jof) throws NamingException { + // Create a JndiObjectTargetSource that mirrors the JndiObjectFactoryBean's configuration. + JndiObjectTargetSource targetSource = new JndiObjectTargetSource(); + targetSource.setJndiTemplate(jof.getJndiTemplate()); + String jndiName = jof.getJndiName(); + Assert.state(jndiName != null, "No JNDI name specified"); + targetSource.setJndiName(jndiName); + targetSource.setExpectedType(jof.getExpectedType()); + targetSource.setResourceRef(jof.isResourceRef()); + targetSource.setLookupOnStartup(jof.lookupOnStartup); + targetSource.setCache(jof.cache); + targetSource.afterPropertiesSet(); + + // Create a proxy with JndiObjectFactoryBean's proxy interface and the JndiObjectTargetSource. + ProxyFactory proxyFactory = new ProxyFactory(); + if (jof.proxyInterfaces != null) { + proxyFactory.setInterfaces(jof.proxyInterfaces); + } + else { + Class targetClass = targetSource.getTargetClass(); + if (targetClass == null) { + throw new IllegalStateException( + "Cannot deactivate 'lookupOnStartup' without specifying a 'proxyInterface' or 'expectedType'"); + } + Class[] ifcs = ClassUtils.getAllInterfacesForClass(targetClass, jof.beanClassLoader); + for (Class ifc : ifcs) { + if (Modifier.isPublic(ifc.getModifiers())) { + proxyFactory.addInterface(ifc); + } + } + } + if (jof.exposeAccessContext) { + proxyFactory.addAdvice(new JndiContextExposingInterceptor(jof.getJndiTemplate())); + } + proxyFactory.setTargetSource(targetSource); + return proxyFactory.getProxy(jof.beanClassLoader); + } + } + + + /** + * Interceptor that exposes the JNDI context for all method invocations, + * according to JndiObjectFactoryBean's "exposeAccessContext" flag. + */ + private static class JndiContextExposingInterceptor implements MethodInterceptor { + + private final JndiTemplate jndiTemplate; + + public JndiContextExposingInterceptor(JndiTemplate jndiTemplate) { + this.jndiTemplate = jndiTemplate; + } + + @Override + @Nullable + public Object invoke(MethodInvocation invocation) throws Throwable { + Context ctx = (isEligible(invocation.getMethod()) ? this.jndiTemplate.getContext() : null); + try { + return invocation.proceed(); + } + finally { + this.jndiTemplate.releaseContext(ctx); + } + } + + protected boolean isEligible(Method method) { + return (Object.class != method.getDeclaringClass()); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/jndi/JndiObjectLocator.java b/spring-context/src/main/java/org/springframework/jndi/JndiObjectLocator.java new file mode 100644 index 0000000..7de88c4 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jndi/JndiObjectLocator.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jndi; + +import javax.naming.NamingException; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Convenient superclass for JNDI-based service locators, + * providing configurable lookup of a specific JNDI resource. + * + *

    Exposes a {@link #setJndiName "jndiName"} property. This may or may not + * include the "java:comp/env/" prefix expected by Java EE applications when + * accessing a locally mapped (Environmental Naming Context) resource. If it + * doesn't, the "java:comp/env/" prefix will be prepended if the "resourceRef" + * property is true (the default is false) and no other scheme + * (e.g. "java:") is given. + * + *

    Subclasses may invoke the {@link #lookup()} method whenever it is appropriate. + * Some classes might do this on initialization, while others might do it + * on demand. The latter strategy is more flexible in that it allows for + * initialization of the locator before the JNDI object is available. + * + * @author Juergen Hoeller + * @since 1.1 + * @see #setJndiName + * @see #setJndiTemplate + * @see #setJndiEnvironment + * @see #setResourceRef + * @see #lookup() + */ +public abstract class JndiObjectLocator extends JndiLocatorSupport implements InitializingBean { + + @Nullable + private String jndiName; + + @Nullable + private Class expectedType; + + + /** + * Specify the JNDI name to look up. If it doesn't begin with "java:comp/env/" + * this prefix is added automatically if "resourceRef" is set to "true". + * @param jndiName the JNDI name to look up + * @see #setResourceRef + */ + public void setJndiName(@Nullable String jndiName) { + this.jndiName = jndiName; + } + + /** + * Return the JNDI name to look up. + */ + @Nullable + public String getJndiName() { + return this.jndiName; + } + + /** + * Specify the type that the located JNDI object is supposed + * to be assignable to, if any. + */ + public void setExpectedType(@Nullable Class expectedType) { + this.expectedType = expectedType; + } + + /** + * Return the type that the located JNDI object is supposed + * to be assignable to, if any. + */ + @Nullable + public Class getExpectedType() { + return this.expectedType; + } + + @Override + public void afterPropertiesSet() throws IllegalArgumentException, NamingException { + if (!StringUtils.hasLength(getJndiName())) { + throw new IllegalArgumentException("Property 'jndiName' is required"); + } + } + + + /** + * Perform the actual JNDI lookup for this locator's target resource. + * @return the located target object + * @throws NamingException if the JNDI lookup failed or if the + * located JNDI object is not assignable to the expected type + * @see #setJndiName + * @see #setExpectedType + * @see #lookup(String, Class) + */ + protected Object lookup() throws NamingException { + String jndiName = getJndiName(); + Assert.state(jndiName != null, "No JNDI name specified"); + return lookup(jndiName, getExpectedType()); + } + +} diff --git a/spring-context/src/main/java/org/springframework/jndi/JndiObjectTargetSource.java b/spring-context/src/main/java/org/springframework/jndi/JndiObjectTargetSource.java new file mode 100644 index 0000000..83da602 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jndi/JndiObjectTargetSource.java @@ -0,0 +1,155 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jndi; + +import javax.naming.NamingException; + +import org.springframework.aop.TargetSource; +import org.springframework.lang.Nullable; + +/** + * AOP {@link org.springframework.aop.TargetSource} that provides + * configurable JNDI lookups for {@code getTarget()} calls. + * + *

    Can be used as alternative to {@link JndiObjectFactoryBean}, to allow for + * relocating a JNDI object lazily or for each operation (see "lookupOnStartup" + * and "cache" properties). This is particularly useful during development, as it + * allows for hot restarting of the JNDI server (for example, a remote JMS server). + * + *

    Example: + * + *

    + * <bean id="queueConnectionFactoryTarget" class="org.springframework.jndi.JndiObjectTargetSource">
    + *   <property name="jndiName" value="JmsQueueConnectionFactory"/>
    + *   <property name="lookupOnStartup" value="false"/>
    + * </bean>
    + *
    + * <bean id="queueConnectionFactory" class="org.springframework.aop.framework.ProxyFactoryBean">
    + *   <property name="proxyInterfaces" value="javax.jms.QueueConnectionFactory"/>
    + *   <property name="targetSource" ref="queueConnectionFactoryTarget"/>
    + * </bean>
    + * + * A {@code createQueueConnection} call on the "queueConnectionFactory" proxy will + * cause a lazy JNDI lookup for "JmsQueueConnectionFactory" and a subsequent delegating + * call to the retrieved QueueConnectionFactory's {@code createQueueConnection}. + * + *

    Alternatively, use a {@link JndiObjectFactoryBean} with a "proxyInterface". + * "lookupOnStartup" and "cache" can then be specified on the JndiObjectFactoryBean, + * creating a JndiObjectTargetSource underneath (instead of defining separate + * ProxyFactoryBean and JndiObjectTargetSource beans). + * + * @author Juergen Hoeller + * @since 1.1 + * @see #setLookupOnStartup + * @see #setCache + * @see org.springframework.aop.framework.ProxyFactoryBean#setTargetSource + * @see JndiObjectFactoryBean#setProxyInterface + */ +public class JndiObjectTargetSource extends JndiObjectLocator implements TargetSource { + + private boolean lookupOnStartup = true; + + private boolean cache = true; + + @Nullable + private Object cachedObject; + + @Nullable + private Class targetClass; + + + /** + * Set whether to look up the JNDI object on startup. Default is "true". + *

    Can be turned off to allow for late availability of the JNDI object. + * In this case, the JNDI object will be fetched on first access. + * @see #setCache + */ + public void setLookupOnStartup(boolean lookupOnStartup) { + this.lookupOnStartup = lookupOnStartup; + } + + /** + * Set whether to cache the JNDI object once it has been located. + * Default is "true". + *

    Can be turned off to allow for hot redeployment of JNDI objects. + * In this case, the JNDI object will be fetched for each invocation. + * @see #setLookupOnStartup + */ + public void setCache(boolean cache) { + this.cache = cache; + } + + @Override + public void afterPropertiesSet() throws NamingException { + super.afterPropertiesSet(); + if (this.lookupOnStartup) { + Object object = lookup(); + if (this.cache) { + this.cachedObject = object; + } + else { + this.targetClass = object.getClass(); + } + } + } + + + @Override + @Nullable + public Class getTargetClass() { + if (this.cachedObject != null) { + return this.cachedObject.getClass(); + } + else if (this.targetClass != null) { + return this.targetClass; + } + else { + return getExpectedType(); + } + } + + @Override + public boolean isStatic() { + return (this.cachedObject != null); + } + + @Override + @Nullable + public Object getTarget() { + try { + if (this.lookupOnStartup || !this.cache) { + return (this.cachedObject != null ? this.cachedObject : lookup()); + } + else { + synchronized (this) { + if (this.cachedObject == null) { + this.cachedObject = lookup(); + } + return this.cachedObject; + } + } + } + catch (NamingException ex) { + throw new JndiLookupFailureException("JndiObjectTargetSource failed to obtain new target object", ex); + } + } + + @Override + public void releaseTarget(Object target) { + } + +} diff --git a/spring-context/src/main/java/org/springframework/jndi/JndiPropertySource.java b/spring-context/src/main/java/org/springframework/jndi/JndiPropertySource.java new file mode 100644 index 0000000..88a4aa4 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jndi/JndiPropertySource.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jndi; + +import javax.naming.NamingException; + +import org.springframework.core.env.PropertySource; +import org.springframework.lang.Nullable; + +/** + * {@link PropertySource} implementation that reads properties from an underlying Spring + * {@link JndiLocatorDelegate}. + * + *

    By default, the underlying {@code JndiLocatorDelegate} will be configured with its + * {@link JndiLocatorDelegate#setResourceRef(boolean) "resourceRef"} property set to + * {@code true}, meaning that names looked up will automatically be prefixed with + * "java:comp/env/" in alignment with published + * JNDI + * naming conventions. To override this setting or to change the prefix, manually + * configure a {@code JndiLocatorDelegate} and provide it to one of the constructors here + * that accepts it. The same applies when providing custom JNDI properties. These should + * be specified using {@link JndiLocatorDelegate#setJndiEnvironment(java.util.Properties)} + * prior to construction of the {@code JndiPropertySource}. + * + *

    Note that {@link org.springframework.web.context.support.StandardServletEnvironment + * StandardServletEnvironment} includes a {@code JndiPropertySource} by default, and any + * customization of the underlying {@link JndiLocatorDelegate} may be performed within an + * {@link org.springframework.context.ApplicationContextInitializer + * ApplicationContextInitializer} or {@link org.springframework.web.WebApplicationInitializer + * WebApplicationInitializer}. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + * @see JndiLocatorDelegate + * @see org.springframework.context.ApplicationContextInitializer + * @see org.springframework.web.WebApplicationInitializer + * @see org.springframework.web.context.support.StandardServletEnvironment + */ +public class JndiPropertySource extends PropertySource { + + /** + * Create a new {@code JndiPropertySource} with the given name + * and a {@link JndiLocatorDelegate} configured to prefix any names with + * "java:comp/env/". + */ + public JndiPropertySource(String name) { + this(name, JndiLocatorDelegate.createDefaultResourceRefLocator()); + } + + /** + * Create a new {@code JndiPropertySource} with the given name and the given + * {@code JndiLocatorDelegate}. + */ + public JndiPropertySource(String name, JndiLocatorDelegate jndiLocator) { + super(name, jndiLocator); + } + + + /** + * This implementation looks up and returns the value associated with the given + * name from the underlying {@link JndiLocatorDelegate}. If a {@link NamingException} + * is thrown during the call to {@link JndiLocatorDelegate#lookup(String)}, returns + * {@code null} and issues a DEBUG-level log statement with the exception message. + */ + @Override + @Nullable + public Object getProperty(String name) { + if (getSource().isResourceRef() && name.indexOf(':') != -1) { + // We're in resource-ref (prefixing with "java:comp/env") mode. Let's not bother + // with property names with a colon it since they're probably just containing a + // default value clause, very unlikely to match including the colon part even in + // a textual property source, and effectively never meant to match that way in + // JNDI where a colon indicates a separator between JNDI scheme and actual name. + return null; + } + + try { + Object value = this.source.lookup(name); + if (logger.isDebugEnabled()) { + logger.debug("JNDI lookup for name [" + name + "] returned: [" + value + "]"); + } + return value; + } + catch (NamingException ex) { + if (logger.isDebugEnabled()) { + logger.debug("JNDI lookup for name [" + name + "] threw NamingException " + + "with message: " + ex.getMessage() + ". Returning null."); + } + return null; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/jndi/JndiTemplate.java b/spring-context/src/main/java/org/springframework/jndi/JndiTemplate.java new file mode 100644 index 0000000..e372ce4 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jndi/JndiTemplate.java @@ -0,0 +1,234 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jndi; + +import java.util.Hashtable; +import java.util.Properties; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NameNotFoundException; +import javax.naming.NamingException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; + +/** + * Helper class that simplifies JNDI operations. It provides methods to lookup and + * bind objects, and allows implementations of the {@link JndiCallback} interface + * to perform any operation they like with a JNDI naming context provided. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see JndiCallback + * @see #execute + */ +public class JndiTemplate { + + protected final Log logger = LogFactory.getLog(getClass()); + + @Nullable + private Properties environment; + + + /** + * Create a new JndiTemplate instance. + */ + public JndiTemplate() { + } + + /** + * Create a new JndiTemplate instance, using the given environment. + */ + public JndiTemplate(@Nullable Properties environment) { + this.environment = environment; + } + + + /** + * Set the environment for the JNDI InitialContext. + */ + public void setEnvironment(@Nullable Properties environment) { + this.environment = environment; + } + + /** + * Return the environment for the JNDI InitialContext, if any. + */ + @Nullable + public Properties getEnvironment() { + return this.environment; + } + + + /** + * Execute the given JNDI context callback implementation. + * @param contextCallback the JndiCallback implementation to use + * @return a result object returned by the callback, or {@code null} + * @throws NamingException thrown by the callback implementation + * @see #createInitialContext + */ + @Nullable + public T execute(JndiCallback contextCallback) throws NamingException { + Context ctx = getContext(); + try { + return contextCallback.doInContext(ctx); + } + finally { + releaseContext(ctx); + } + } + + /** + * Obtain a JNDI context corresponding to this template's configuration. + * Called by {@link #execute}; may also be called directly. + *

    The default implementation delegates to {@link #createInitialContext()}. + * @return the JNDI context (never {@code null}) + * @throws NamingException if context retrieval failed + * @see #releaseContext + */ + public Context getContext() throws NamingException { + return createInitialContext(); + } + + /** + * Release a JNDI context as obtained from {@link #getContext()}. + * @param ctx the JNDI context to release (may be {@code null}) + * @see #getContext + */ + public void releaseContext(@Nullable Context ctx) { + if (ctx != null) { + try { + ctx.close(); + } + catch (NamingException ex) { + logger.debug("Could not close JNDI InitialContext", ex); + } + } + } + + /** + * Create a new JNDI initial context. Invoked by {@link #getContext}. + *

    The default implementation use this template's environment settings. + * Can be subclassed for custom contexts, e.g. for testing. + * @return the initial Context instance + * @throws NamingException in case of initialization errors + */ + protected Context createInitialContext() throws NamingException { + Hashtable icEnv = null; + Properties env = getEnvironment(); + if (env != null) { + icEnv = new Hashtable<>(env.size()); + CollectionUtils.mergePropertiesIntoMap(env, icEnv); + } + return new InitialContext(icEnv); + } + + + /** + * Look up the object with the given name in the current JNDI context. + * @param name the JNDI name of the object + * @return object found (cannot be {@code null}; if a not so well-behaved + * JNDI implementations returns null, a NamingException gets thrown) + * @throws NamingException if there is no object with the given + * name bound to JNDI + */ + public Object lookup(final String name) throws NamingException { + if (logger.isDebugEnabled()) { + logger.debug("Looking up JNDI object with name [" + name + "]"); + } + Object result = execute(ctx -> ctx.lookup(name)); + if (result == null) { + throw new NameNotFoundException( + "JNDI object with [" + name + "] not found: JNDI implementation returned null"); + } + return result; + } + + /** + * Look up the object with the given name in the current JNDI context. + * @param name the JNDI name of the object + * @param requiredType type the JNDI object must match. Can be an interface or + * superclass of the actual class, or {@code null} for any match. For example, + * if the value is {@code Object.class}, this method will succeed whatever + * the class of the returned instance. + * @return object found (cannot be {@code null}; if a not so well-behaved + * JNDI implementations returns null, a NamingException gets thrown) + * @throws NamingException if there is no object with the given + * name bound to JNDI + */ + @SuppressWarnings("unchecked") + public T lookup(String name, @Nullable Class requiredType) throws NamingException { + Object jndiObject = lookup(name); + if (requiredType != null && !requiredType.isInstance(jndiObject)) { + throw new TypeMismatchNamingException(name, requiredType, jndiObject.getClass()); + } + return (T) jndiObject; + } + + /** + * Bind the given object to the current JNDI context, using the given name. + * @param name the JNDI name of the object + * @param object the object to bind + * @throws NamingException thrown by JNDI, mostly name already bound + */ + public void bind(final String name, final Object object) throws NamingException { + if (logger.isDebugEnabled()) { + logger.debug("Binding JNDI object with name [" + name + "]"); + } + execute(ctx -> { + ctx.bind(name, object); + return null; + }); + } + + /** + * Rebind the given object to the current JNDI context, using the given name. + * Overwrites any existing binding. + * @param name the JNDI name of the object + * @param object the object to rebind + * @throws NamingException thrown by JNDI + */ + public void rebind(final String name, final Object object) throws NamingException { + if (logger.isDebugEnabled()) { + logger.debug("Rebinding JNDI object with name [" + name + "]"); + } + execute(ctx -> { + ctx.rebind(name, object); + return null; + }); + } + + /** + * Remove the binding for the given name from the current JNDI context. + * @param name the JNDI name of the object + * @throws NamingException thrown by JNDI, mostly name not found + */ + public void unbind(final String name) throws NamingException { + if (logger.isDebugEnabled()) { + logger.debug("Unbinding JNDI object with name [" + name + "]"); + } + execute(ctx -> { + ctx.unbind(name); + return null; + }); + } + +} diff --git a/spring-context/src/main/java/org/springframework/jndi/JndiTemplateEditor.java b/spring-context/src/main/java/org/springframework/jndi/JndiTemplateEditor.java new file mode 100644 index 0000000..a5931d8 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jndi/JndiTemplateEditor.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jndi; + +import java.beans.PropertyEditorSupport; +import java.util.Properties; + +import org.springframework.beans.propertyeditors.PropertiesEditor; +import org.springframework.lang.Nullable; + +/** + * Properties editor for JndiTemplate objects. Allows properties of type + * JndiTemplate to be populated with a properties-format string. + * + * @author Rod Johnson + * @since 09.05.2003 + */ +public class JndiTemplateEditor extends PropertyEditorSupport { + + private final PropertiesEditor propertiesEditor = new PropertiesEditor(); + + @Override + public void setAsText(@Nullable String text) throws IllegalArgumentException { + if (text == null) { + throw new IllegalArgumentException("JndiTemplate cannot be created from null string"); + } + if (text.isEmpty()) { + // empty environment + setValue(new JndiTemplate()); + } + else { + // we have a non-empty properties string + this.propertiesEditor.setAsText(text); + Properties props = (Properties) this.propertiesEditor.getValue(); + setValue(new JndiTemplate(props)); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/jndi/TypeMismatchNamingException.java b/spring-context/src/main/java/org/springframework/jndi/TypeMismatchNamingException.java new file mode 100644 index 0000000..9b3b52d --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jndi/TypeMismatchNamingException.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jndi; + +import javax.naming.NamingException; + +/** + * Exception thrown if a type mismatch is encountered for an object + * located in a JNDI environment. Thrown by JndiTemplate. + * + * @author Juergen Hoeller + * @since 1.2.8 + * @see JndiTemplate#lookup(String, Class) + */ +@SuppressWarnings("serial") +public class TypeMismatchNamingException extends NamingException { + + private final Class requiredType; + + private final Class actualType; + + + /** + * Construct a new TypeMismatchNamingException, + * building an explanation text from the given arguments. + * @param jndiName the JNDI name + * @param requiredType the required type for the lookup + * @param actualType the actual type that the lookup returned + */ + public TypeMismatchNamingException(String jndiName, Class requiredType, Class actualType) { + super("Object of type [" + actualType + "] available at JNDI location [" + + jndiName + "] is not assignable to [" + requiredType.getName() + "]"); + this.requiredType = requiredType; + this.actualType = actualType; + } + + + /** + * Return the required type for the lookup, if available. + */ + public final Class getRequiredType() { + return this.requiredType; + } + + /** + * Return the actual type that the lookup returned, if available. + */ + public final Class getActualType() { + return this.actualType; + } + +} diff --git a/spring-context/src/main/java/org/springframework/jndi/package-info.java b/spring-context/src/main/java/org/springframework/jndi/package-info.java new file mode 100644 index 0000000..1ef8b64 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jndi/package-info.java @@ -0,0 +1,15 @@ +/** + * The classes in this package make JNDI easier to use, + * facilitating the accessing of configuration stored in JNDI, + * and provide useful superclasses for JNDI access classes. + * + *

    The classes in this package are discussed in Chapter 11 of + * Expert One-On-One J2EE Design and Development + * by Rod Johnson (Wrox, 2002). + */ +@NonNullApi +@NonNullFields +package org.springframework.jndi; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/jndi/support/SimpleJndiBeanFactory.java b/spring-context/src/main/java/org/springframework/jndi/support/SimpleJndiBeanFactory.java new file mode 100644 index 0000000..06bb11d --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jndi/support/SimpleJndiBeanFactory.java @@ -0,0 +1,293 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jndi.support; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.naming.NameNotFoundException; +import javax.naming.NamingException; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanNotOfRequiredTypeException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.core.ResolvableType; +import org.springframework.jndi.JndiLocatorSupport; +import org.springframework.jndi.TypeMismatchNamingException; +import org.springframework.lang.Nullable; + +/** + * Simple JNDI-based implementation of Spring's + * {@link org.springframework.beans.factory.BeanFactory} interface. + * Does not support enumerating bean definitions, hence doesn't implement + * the {@link org.springframework.beans.factory.ListableBeanFactory} interface. + * + *

    This factory resolves given bean names as JNDI names within the + * Java EE application's "java:comp/env/" namespace. It caches the resolved + * types for all obtained objects, and optionally also caches shareable + * objects (if they are explicitly marked as + * {@link #addShareableResource shareable resource}. + * + *

    The main intent of this factory is usage in combination with Spring's + * {@link org.springframework.context.annotation.CommonAnnotationBeanPostProcessor}, + * configured as "resourceFactory" for resolving {@code @Resource} + * annotations as JNDI objects without intermediate bean definitions. + * It may be used for similar lookup scenarios as well, of course, + * in particular if BeanFactory-style type checking is required. + * + * @author Juergen Hoeller + * @since 2.5 + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory + * @see org.springframework.context.annotation.CommonAnnotationBeanPostProcessor + */ +public class SimpleJndiBeanFactory extends JndiLocatorSupport implements BeanFactory { + + /** JNDI names of resources that are known to be shareable, i.e. can be cached */ + private final Set shareableResources = new HashSet<>(); + + /** Cache of shareable singleton objects: bean name to bean instance. */ + private final Map singletonObjects = new HashMap<>(); + + /** Cache of the types of nonshareable resources: bean name to bean type. */ + private final Map> resourceTypes = new HashMap<>(); + + + public SimpleJndiBeanFactory() { + setResourceRef(true); + } + + + /** + * Add the name of a shareable JNDI resource, + * which this factory is allowed to cache once obtained. + * @param shareableResource the JNDI name + * (typically within the "java:comp/env/" namespace) + */ + public void addShareableResource(String shareableResource) { + this.shareableResources.add(shareableResource); + } + + /** + * Set a list of names of shareable JNDI resources, + * which this factory is allowed to cache once obtained. + * @param shareableResources the JNDI names + * (typically within the "java:comp/env/" namespace) + */ + public void setShareableResources(String... shareableResources) { + Collections.addAll(this.shareableResources, shareableResources); + } + + + //--------------------------------------------------------------------- + // Implementation of BeanFactory interface + //--------------------------------------------------------------------- + + + @Override + public Object getBean(String name) throws BeansException { + return getBean(name, Object.class); + } + + @Override + public T getBean(String name, Class requiredType) throws BeansException { + try { + if (isSingleton(name)) { + return doGetSingleton(name, requiredType); + } + else { + return lookup(name, requiredType); + } + } + catch (NameNotFoundException ex) { + throw new NoSuchBeanDefinitionException(name, "not found in JNDI environment"); + } + catch (TypeMismatchNamingException ex) { + throw new BeanNotOfRequiredTypeException(name, ex.getRequiredType(), ex.getActualType()); + } + catch (NamingException ex) { + throw new BeanDefinitionStoreException("JNDI environment", name, "JNDI lookup failed", ex); + } + } + + @Override + public Object getBean(String name, @Nullable Object... args) throws BeansException { + if (args != null) { + throw new UnsupportedOperationException( + "SimpleJndiBeanFactory does not support explicit bean creation arguments"); + } + return getBean(name); + } + + @Override + public T getBean(Class requiredType) throws BeansException { + return getBean(requiredType.getSimpleName(), requiredType); + } + + @Override + public T getBean(Class requiredType, @Nullable Object... args) throws BeansException { + if (args != null) { + throw new UnsupportedOperationException( + "SimpleJndiBeanFactory does not support explicit bean creation arguments"); + } + return getBean(requiredType); + } + + @Override + public ObjectProvider getBeanProvider(Class requiredType) { + return new ObjectProvider() { + @Override + public T getObject() throws BeansException { + return getBean(requiredType); + } + @Override + public T getObject(Object... args) throws BeansException { + return getBean(requiredType, args); + } + @Override + @Nullable + public T getIfAvailable() throws BeansException { + try { + return getBean(requiredType); + } + catch (NoUniqueBeanDefinitionException ex) { + throw ex; + } + catch (NoSuchBeanDefinitionException ex) { + return null; + } + } + @Override + @Nullable + public T getIfUnique() throws BeansException { + try { + return getBean(requiredType); + } + catch (NoSuchBeanDefinitionException ex) { + return null; + } + } + }; + } + + @Override + public ObjectProvider getBeanProvider(ResolvableType requiredType) { + throw new UnsupportedOperationException( + "SimpleJndiBeanFactory does not support resolution by ResolvableType"); + } + + @Override + public boolean containsBean(String name) { + if (this.singletonObjects.containsKey(name) || this.resourceTypes.containsKey(name)) { + return true; + } + try { + doGetType(name); + return true; + } + catch (NamingException ex) { + return false; + } + } + + @Override + public boolean isSingleton(String name) throws NoSuchBeanDefinitionException { + return this.shareableResources.contains(name); + } + + @Override + public boolean isPrototype(String name) throws NoSuchBeanDefinitionException { + return !this.shareableResources.contains(name); + } + + @Override + public boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException { + Class type = getType(name); + return (type != null && typeToMatch.isAssignableFrom(type)); + } + + @Override + public boolean isTypeMatch(String name, @Nullable Class typeToMatch) throws NoSuchBeanDefinitionException { + Class type = getType(name); + return (typeToMatch == null || (type != null && typeToMatch.isAssignableFrom(type))); + } + + @Override + @Nullable + public Class getType(String name) throws NoSuchBeanDefinitionException { + return getType(name, true); + } + + @Override + @Nullable + public Class getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException { + try { + return doGetType(name); + } + catch (NameNotFoundException ex) { + throw new NoSuchBeanDefinitionException(name, "not found in JNDI environment"); + } + catch (NamingException ex) { + return null; + } + } + + @Override + public String[] getAliases(String name) { + return new String[0]; + } + + + @SuppressWarnings("unchecked") + private T doGetSingleton(String name, @Nullable Class requiredType) throws NamingException { + synchronized (this.singletonObjects) { + Object singleton = this.singletonObjects.get(name); + if (singleton != null) { + if (requiredType != null && !requiredType.isInstance(singleton)) { + throw new TypeMismatchNamingException(convertJndiName(name), requiredType, singleton.getClass()); + } + return (T) singleton; + } + T jndiObject = lookup(name, requiredType); + this.singletonObjects.put(name, jndiObject); + return jndiObject; + } + } + + private Class doGetType(String name) throws NamingException { + if (isSingleton(name)) { + return doGetSingleton(name, null).getClass(); + } + else { + synchronized (this.resourceTypes) { + Class type = this.resourceTypes.get(name); + if (type == null) { + type = lookup(name, null).getClass(); + this.resourceTypes.put(name, type); + } + return type; + } + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/jndi/support/package-info.java b/spring-context/src/main/java/org/springframework/jndi/support/package-info.java new file mode 100644 index 0000000..3669b23 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/jndi/support/package-info.java @@ -0,0 +1,10 @@ +/** + * Support classes for JNDI usage, + * including a JNDI-based BeanFactory implementation. + */ +@NonNullApi +@NonNullFields +package org.springframework.jndi.support; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/remoting/RemoteAccessException.java b/spring-context/src/main/java/org/springframework/remoting/RemoteAccessException.java new file mode 100644 index 0000000..0d6cde8 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/RemoteAccessException.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting; + +import org.springframework.core.NestedRuntimeException; + +/** + * Generic remote access exception. A service proxy for any remoting + * protocol should throw this exception or subclasses of it, in order + * to transparently expose a plain Java business interface. + * + *

    When using conforming proxies, switching the actual remoting protocol + * e.g. from Hessian does not affect client code. Clients work with a plain + * natural Java business interface that the service exposes. A client object + * simply receives an implementation for the interface that it needs via a + * bean reference, like it does for a local bean as well. + * + *

    A client may catch RemoteAccessException if it wants to, but as + * remote access errors are typically unrecoverable, it will probably let + * such exceptions propagate to a higher level that handles them generically. + * In this case, the client code doesn't show any signs of being involved in + * remote access, as there aren't any remoting-specific dependencies. + * + *

    Even when switching from a remote service proxy to a local implementation + * of the same interface, this amounts to just a matter of configuration. Obviously, + * the client code should be somewhat aware that it might be working + * against a remote service, for example in terms of repeated method calls that + * cause unnecessary roundtrips etc. However, it doesn't have to be aware whether + * it is actually working against a remote service or a local implementation, + * or with which remoting protocol it is working under the hood. + * + * @author Juergen Hoeller + * @since 14.05.2003 + */ +public class RemoteAccessException extends NestedRuntimeException { + + /** Use serialVersionUID from Spring 1.2 for interoperability. */ + private static final long serialVersionUID = -4906825139312227864L; + + + /** + * Constructor for RemoteAccessException. + * @param msg the detail message + */ + public RemoteAccessException(String msg) { + super(msg); + } + + /** + * Constructor for RemoteAccessException. + * @param msg the detail message + * @param cause the root cause (usually from using an underlying + * remoting API such as RMI) + */ + public RemoteAccessException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/RemoteConnectFailureException.java b/spring-context/src/main/java/org/springframework/remoting/RemoteConnectFailureException.java new file mode 100644 index 0000000..99857c3 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/RemoteConnectFailureException.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting; + +/** + * RemoteAccessException subclass to be thrown when no connection + * could be established with a remote service. + * + * @author Juergen Hoeller + * @since 1.1 + */ +@SuppressWarnings("serial") +public class RemoteConnectFailureException extends RemoteAccessException { + + /** + * Constructor for RemoteConnectFailureException. + * @param msg the detail message + * @param cause the root cause from the remoting API in use + */ + public RemoteConnectFailureException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/RemoteInvocationFailureException.java b/spring-context/src/main/java/org/springframework/remoting/RemoteInvocationFailureException.java new file mode 100644 index 0000000..40e9521 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/RemoteInvocationFailureException.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting; + +/** + * RemoteAccessException subclass to be thrown when the execution + * of the target method failed on the server side, for example + * when a method was not found on the target object. + * + * @author Juergen Hoeller + * @since 2.5 + * @see RemoteProxyFailureException + */ +@SuppressWarnings("serial") +public class RemoteInvocationFailureException extends RemoteAccessException { + + /** + * Constructor for RemoteInvocationFailureException. + * @param msg the detail message + * @param cause the root cause from the remoting API in use + */ + public RemoteInvocationFailureException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/RemoteLookupFailureException.java b/spring-context/src/main/java/org/springframework/remoting/RemoteLookupFailureException.java new file mode 100644 index 0000000..ee0e9fa --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/RemoteLookupFailureException.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting; + +/** + * RemoteAccessException subclass to be thrown in case of a lookup failure, + * typically if the lookup happens on demand for each method invocation. + * + * @author Juergen Hoeller + * @since 1.1 + */ +@SuppressWarnings("serial") +public class RemoteLookupFailureException extends RemoteAccessException { + + /** + * Constructor for RemoteLookupFailureException. + * @param msg the detail message + */ + public RemoteLookupFailureException(String msg) { + super(msg); + } + + /** + * Constructor for RemoteLookupFailureException. + * @param msg message + * @param cause the root cause from the remoting API in use + */ + public RemoteLookupFailureException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/RemoteProxyFailureException.java b/spring-context/src/main/java/org/springframework/remoting/RemoteProxyFailureException.java new file mode 100644 index 0000000..ce36152 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/RemoteProxyFailureException.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting; + +/** + * RemoteAccessException subclass to be thrown in case of a failure + * within the client-side proxy for a remote service, for example + * when a method was not found on the underlying RMI stub. + * + * @author Juergen Hoeller + * @since 1.2.8 + * @see RemoteInvocationFailureException + */ +@SuppressWarnings("serial") +public class RemoteProxyFailureException extends RemoteAccessException { + + /** + * Constructor for RemoteProxyFailureException. + * @param msg the detail message + * @param cause the root cause from the remoting API in use + */ + public RemoteProxyFailureException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/RemoteTimeoutException.java b/spring-context/src/main/java/org/springframework/remoting/RemoteTimeoutException.java new file mode 100644 index 0000000..8dbff58 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/RemoteTimeoutException.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting; + +/** + * RemoteAccessException subclass to be thrown when the execution + * of the target method did not complete before a configurable + * timeout, for example when a reply message was not received. + * @author Stephane Nicoll + * @since 4.2 + */ +@SuppressWarnings("serial") +public class RemoteTimeoutException extends RemoteAccessException { + + /** + * Constructor for RemoteTimeoutException. + * @param msg the detail message + */ + public RemoteTimeoutException(String msg) { + super(msg); + } + + /** + * Constructor for RemoteTimeoutException. + * @param msg the detail message + * @param cause the root cause from the remoting API in use + */ + public RemoteTimeoutException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/spring-context/src/main/java/org/springframework/remoting/package-info.java b/spring-context/src/main/java/org/springframework/remoting/package-info.java new file mode 100644 index 0000000..a4b3e95 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/package-info.java @@ -0,0 +1,10 @@ +/** + * Exception hierarchy for Spring's remoting infrastructure, + * independent of any specific remote method invocation system. + */ +@NonNullApi +@NonNullFields +package org.springframework.remoting; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/remoting/rmi/CodebaseAwareObjectInputStream.java b/spring-context/src/main/java/org/springframework/remoting/rmi/CodebaseAwareObjectInputStream.java new file mode 100644 index 0000000..9c897b8 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/rmi/CodebaseAwareObjectInputStream.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.rmi; + +import java.io.IOException; +import java.io.InputStream; +import java.rmi.server.RMIClassLoader; + +import org.springframework.core.ConfigurableObjectInputStream; +import org.springframework.lang.Nullable; + +/** + * Special ObjectInputStream subclass that falls back to a specified codebase + * to load classes from if not found locally. In contrast to standard RMI + * conventions for dynamic class download, it is the client that determines + * the codebase URL here, rather than the "java.rmi.server.codebase" system + * property on the server. + * + *

    Uses the JDK's RMIClassLoader to load classes from the specified codebase. + * The codebase can consist of multiple URLs, separated by spaces. + * Note that RMIClassLoader requires a SecurityManager to be set, like when + * using dynamic class download with standard RMI! (See the RMI documentation + * for details.) + * + *

    Despite residing in the RMI package, this class is not used for + * RmiClientInterceptor, which uses the standard RMI infrastructure instead + * and thus is only able to rely on RMI's standard dynamic class download via + * "java.rmi.server.codebase". CodebaseAwareObjectInputStream is used by + * HttpInvokerClientInterceptor (see the "codebaseUrl" property there). + * + *

    Thanks to Lionel Mestre for suggesting the option and providing + * a prototype! + * + * @author Juergen Hoeller + * @since 1.1.3 + * @see java.rmi.server.RMIClassLoader + * @see RemoteInvocationSerializingExporter#createObjectInputStream + * @see org.springframework.remoting.httpinvoker.HttpInvokerClientInterceptor#setCodebaseUrl + * @deprecated as of 5.3 (phasing out serialization-based remoting) + */ +@Deprecated +public class CodebaseAwareObjectInputStream extends ConfigurableObjectInputStream { + + private final String codebaseUrl; + + + /** + * Create a new CodebaseAwareObjectInputStream for the given InputStream and codebase. + * @param in the InputStream to read from + * @param codebaseUrl the codebase URL to load classes from if not found locally + * (can consist of multiple URLs, separated by spaces) + * @see java.io.ObjectInputStream#ObjectInputStream(java.io.InputStream) + */ + public CodebaseAwareObjectInputStream(InputStream in, String codebaseUrl) throws IOException { + this(in, null, codebaseUrl); + } + + /** + * Create a new CodebaseAwareObjectInputStream for the given InputStream and codebase. + * @param in the InputStream to read from + * @param classLoader the ClassLoader to use for loading local classes + * (may be {@code null} to indicate RMI's default ClassLoader) + * @param codebaseUrl the codebase URL to load classes from if not found locally + * (can consist of multiple URLs, separated by spaces) + * @see java.io.ObjectInputStream#ObjectInputStream(java.io.InputStream) + */ + public CodebaseAwareObjectInputStream( + InputStream in, @Nullable ClassLoader classLoader, String codebaseUrl) throws IOException { + + super(in, classLoader); + this.codebaseUrl = codebaseUrl; + } + + /** + * Create a new CodebaseAwareObjectInputStream for the given InputStream and codebase. + * @param in the InputStream to read from + * @param classLoader the ClassLoader to use for loading local classes + * (may be {@code null} to indicate RMI's default ClassLoader) + * @param acceptProxyClasses whether to accept deserialization of proxy classes + * (may be deactivated as a security measure) + * @see java.io.ObjectInputStream#ObjectInputStream(java.io.InputStream) + */ + public CodebaseAwareObjectInputStream( + InputStream in, @Nullable ClassLoader classLoader, boolean acceptProxyClasses) throws IOException { + + super(in, classLoader, acceptProxyClasses); + this.codebaseUrl = null; + } + + + @Override + protected Class resolveFallbackIfPossible(String className, ClassNotFoundException ex) + throws IOException, ClassNotFoundException { + + // If codebaseUrl is set, try to load the class with the RMIClassLoader. + // Else, propagate the ClassNotFoundException. + if (this.codebaseUrl == null) { + throw ex; + } + return RMIClassLoader.loadClass(this.codebaseUrl, className); + } + + @Override + protected ClassLoader getFallbackClassLoader() throws IOException { + return RMIClassLoader.getClassLoader(this.codebaseUrl); + } + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/rmi/JndiRmiClientInterceptor.java b/spring-context/src/main/java/org/springframework/remoting/rmi/JndiRmiClientInterceptor.java new file mode 100644 index 0000000..d90dd6a --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/rmi/JndiRmiClientInterceptor.java @@ -0,0 +1,452 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.rmi; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.rmi.RemoteException; + +import javax.naming.Context; +import javax.naming.NamingException; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jndi.JndiObjectLocator; +import org.springframework.lang.Nullable; +import org.springframework.remoting.RemoteConnectFailureException; +import org.springframework.remoting.RemoteInvocationFailureException; +import org.springframework.remoting.RemoteLookupFailureException; +import org.springframework.remoting.support.DefaultRemoteInvocationFactory; +import org.springframework.remoting.support.RemoteInvocation; +import org.springframework.remoting.support.RemoteInvocationFactory; +import org.springframework.util.Assert; + +/** + * {@link org.aopalliance.intercept.MethodInterceptor} for accessing RMI services + * from JNDI. Typically used for RMI-IIOP but can also be used for EJB home objects + * (for example, a Stateful Session Bean home). In contrast to a plain JNDI lookup, + * this accessor also performs narrowing through PortableRemoteObject. + * + *

    With conventional RMI services, this invoker is typically used with the RMI + * service interface. Alternatively, this invoker can also proxy a remote RMI service + * with a matching non-RMI business interface, i.e. an interface that mirrors the RMI + * service methods but does not declare RemoteExceptions. In the latter case, + * RemoteExceptions thrown by the RMI stub will automatically get converted to + * Spring's unchecked RemoteAccessException. + * + *

    The JNDI environment can be specified as "jndiEnvironment" property, + * or be configured in a {@code jndi.properties} file or as system properties. + * For example: + * + *

    <property name="jndiEnvironment">
    + * 	 <props>
    + *		 <prop key="java.naming.factory.initial">com.sun.jndi.cosnaming.CNCtxFactory</prop>
    + *		 <prop key="java.naming.provider.url">iiop://localhost:1050</prop>
    + *	 </props>
    + * </property>
    + * + * @author Juergen Hoeller + * @since 1.1 + * @see #setJndiTemplate + * @see #setJndiEnvironment + * @see #setJndiName + * @see JndiRmiServiceExporter + * @see JndiRmiProxyFactoryBean + * @see org.springframework.remoting.RemoteAccessException + * @see java.rmi.RemoteException + * @see java.rmi.Remote + * @deprecated as of 5.3 (phasing out serialization-based remoting) + */ +@Deprecated +public class JndiRmiClientInterceptor extends JndiObjectLocator implements MethodInterceptor, InitializingBean { + + private Class serviceInterface; + + private RemoteInvocationFactory remoteInvocationFactory = new DefaultRemoteInvocationFactory(); + + private boolean lookupStubOnStartup = true; + + private boolean cacheStub = true; + + private boolean refreshStubOnConnectFailure = false; + + private boolean exposeAccessContext = false; + + private Object cachedStub; + + private final Object stubMonitor = new Object(); + + + /** + * Set the interface of the service to access. + * The interface must be suitable for the particular service and remoting tool. + *

    Typically required to be able to create a suitable service proxy, + * but can also be optional if the lookup returns a typed stub. + */ + public void setServiceInterface(Class serviceInterface) { + Assert.notNull(serviceInterface, "'serviceInterface' must not be null"); + Assert.isTrue(serviceInterface.isInterface(), "'serviceInterface' must be an interface"); + this.serviceInterface = serviceInterface; + } + + /** + * Return the interface of the service to access. + */ + public Class getServiceInterface() { + return this.serviceInterface; + } + + /** + * Set the RemoteInvocationFactory to use for this accessor. + * Default is a {@link DefaultRemoteInvocationFactory}. + *

    A custom invocation factory can add further context information + * to the invocation, for example user credentials. + */ + public void setRemoteInvocationFactory(RemoteInvocationFactory remoteInvocationFactory) { + this.remoteInvocationFactory = remoteInvocationFactory; + } + + /** + * Return the RemoteInvocationFactory used by this accessor. + */ + public RemoteInvocationFactory getRemoteInvocationFactory() { + return this.remoteInvocationFactory; + } + + /** + * Set whether to look up the RMI stub on startup. Default is "true". + *

    Can be turned off to allow for late start of the RMI server. + * In this case, the RMI stub will be fetched on first access. + * @see #setCacheStub + */ + public void setLookupStubOnStartup(boolean lookupStubOnStartup) { + this.lookupStubOnStartup = lookupStubOnStartup; + } + + /** + * Set whether to cache the RMI stub once it has been located. + * Default is "true". + *

    Can be turned off to allow for hot restart of the RMI server. + * In this case, the RMI stub will be fetched for each invocation. + * @see #setLookupStubOnStartup + */ + public void setCacheStub(boolean cacheStub) { + this.cacheStub = cacheStub; + } + + /** + * Set whether to refresh the RMI stub on connect failure. + * Default is "false". + *

    Can be turned on to allow for hot restart of the RMI server. + * If a cached RMI stub throws an RMI exception that indicates a + * remote connect failure, a fresh proxy will be fetched and the + * invocation will be retried. + * @see java.rmi.ConnectException + * @see java.rmi.ConnectIOException + * @see java.rmi.NoSuchObjectException + */ + public void setRefreshStubOnConnectFailure(boolean refreshStubOnConnectFailure) { + this.refreshStubOnConnectFailure = refreshStubOnConnectFailure; + } + + /** + * Set whether to expose the JNDI environment context for all access to the target + * RMI stub, i.e. for all method invocations on the exposed object reference. + *

    Default is "false", i.e. to only expose the JNDI context for object lookup. + * Switch this flag to "true" in order to expose the JNDI environment (including + * the authorization context) for each RMI invocation, as needed by WebLogic + * for RMI stubs with authorization requirements. + */ + public void setExposeAccessContext(boolean exposeAccessContext) { + this.exposeAccessContext = exposeAccessContext; + } + + + @Override + public void afterPropertiesSet() throws NamingException { + super.afterPropertiesSet(); + prepare(); + } + + /** + * Fetches the RMI stub on startup, if necessary. + * @throws RemoteLookupFailureException if RMI stub creation failed + * @see #setLookupStubOnStartup + * @see #lookupStub + */ + public void prepare() throws RemoteLookupFailureException { + // Cache RMI stub on initialization? + if (this.lookupStubOnStartup) { + Object remoteObj = lookupStub(); + if (logger.isDebugEnabled()) { + if (remoteObj instanceof RmiInvocationHandler) { + logger.debug("JNDI RMI object [" + getJndiName() + "] is an RMI invoker"); + } + else if (getServiceInterface() != null) { + boolean isImpl = getServiceInterface().isInstance(remoteObj); + logger.debug("Using service interface [" + getServiceInterface().getName() + + "] for JNDI RMI object [" + getJndiName() + "] - " + + (!isImpl ? "not " : "") + "directly implemented"); + } + } + if (this.cacheStub) { + this.cachedStub = remoteObj; + } + } + } + + /** + * Create the RMI stub, typically by looking it up. + *

    Called on interceptor initialization if "cacheStub" is "true"; + * else called for each invocation by {@link #getStub()}. + *

    The default implementation retrieves the service from the + * JNDI environment. This can be overridden in subclasses. + * @return the RMI stub to store in this interceptor + * @throws RemoteLookupFailureException if RMI stub creation failed + * @see #setCacheStub + * @see #lookup + */ + protected Object lookupStub() throws RemoteLookupFailureException { + try { + return lookup(); + } + catch (NamingException ex) { + throw new RemoteLookupFailureException("JNDI lookup for RMI service [" + getJndiName() + "] failed", ex); + } + } + + /** + * Return the RMI stub to use. Called for each invocation. + *

    The default implementation returns the stub created on initialization, + * if any. Else, it invokes {@link #lookupStub} to get a new stub for + * each invocation. This can be overridden in subclasses, for example in + * order to cache a stub for a given amount of time before recreating it, + * or to test the stub whether it is still alive. + * @return the RMI stub to use for an invocation + * @throws NamingException if stub creation failed + * @throws RemoteLookupFailureException if RMI stub creation failed + */ + protected Object getStub() throws NamingException, RemoteLookupFailureException { + if (!this.cacheStub || (this.lookupStubOnStartup && !this.refreshStubOnConnectFailure)) { + return (this.cachedStub != null ? this.cachedStub : lookupStub()); + } + else { + synchronized (this.stubMonitor) { + if (this.cachedStub == null) { + this.cachedStub = lookupStub(); + } + return this.cachedStub; + } + } + } + + + /** + * Fetches an RMI stub and delegates to {@link #doInvoke}. + * If configured to refresh on connect failure, it will call + * {@link #refreshAndRetry} on corresponding RMI exceptions. + * @see #getStub + * @see #doInvoke + * @see #refreshAndRetry + * @see java.rmi.ConnectException + * @see java.rmi.ConnectIOException + * @see java.rmi.NoSuchObjectException + */ + @Override + @Nullable + public Object invoke(MethodInvocation invocation) throws Throwable { + Object stub; + try { + stub = getStub(); + } + catch (NamingException ex) { + throw new RemoteLookupFailureException("JNDI lookup for RMI service [" + getJndiName() + "] failed", ex); + } + + Context ctx = (this.exposeAccessContext ? getJndiTemplate().getContext() : null); + try { + return doInvoke(invocation, stub); + } + catch (RemoteConnectFailureException ex) { + return handleRemoteConnectFailure(invocation, ex); + } + catch (RemoteException ex) { + if (isConnectFailure(ex)) { + return handleRemoteConnectFailure(invocation, ex); + } + else { + throw ex; + } + } + finally { + getJndiTemplate().releaseContext(ctx); + } + } + + /** + * Determine whether the given RMI exception indicates a connect failure. + *

    The default implementation delegates to + * {@link RmiClientInterceptorUtils#isConnectFailure}. + * @param ex the RMI exception to check + * @return whether the exception should be treated as connect failure + */ + protected boolean isConnectFailure(RemoteException ex) { + return RmiClientInterceptorUtils.isConnectFailure(ex); + } + + /** + * Refresh the stub and retry the remote invocation if necessary. + *

    If not configured to refresh on connect failure, this method + * simply rethrows the original exception. + * @param invocation the invocation that failed + * @param ex the exception raised on remote invocation + * @return the result value of the new invocation, if succeeded + * @throws Throwable an exception raised by the new invocation, if failed too. + */ + private Object handleRemoteConnectFailure(MethodInvocation invocation, Exception ex) throws Throwable { + if (this.refreshStubOnConnectFailure) { + if (logger.isDebugEnabled()) { + logger.debug("Could not connect to RMI service [" + getJndiName() + "] - retrying", ex); + } + else if (logger.isInfoEnabled()) { + logger.info("Could not connect to RMI service [" + getJndiName() + "] - retrying"); + } + return refreshAndRetry(invocation); + } + else { + throw ex; + } + } + + /** + * Refresh the RMI stub and retry the given invocation. + * Called by invoke on connect failure. + * @param invocation the AOP method invocation + * @return the invocation result, if any + * @throws Throwable in case of invocation failure + * @see #invoke + */ + @Nullable + protected Object refreshAndRetry(MethodInvocation invocation) throws Throwable { + Object freshStub; + synchronized (this.stubMonitor) { + this.cachedStub = null; + freshStub = lookupStub(); + if (this.cacheStub) { + this.cachedStub = freshStub; + } + } + return doInvoke(invocation, freshStub); + } + + + /** + * Perform the given invocation on the given RMI stub. + * @param invocation the AOP method invocation + * @param stub the RMI stub to invoke + * @return the invocation result, if any + * @throws Throwable in case of invocation failure + */ + @Nullable + protected Object doInvoke(MethodInvocation invocation, Object stub) throws Throwable { + if (stub instanceof RmiInvocationHandler) { + // RMI invoker + try { + return doInvoke(invocation, (RmiInvocationHandler) stub); + } + catch (RemoteException ex) { + throw convertRmiAccessException(ex, invocation.getMethod()); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + catch (Throwable ex) { + throw new RemoteInvocationFailureException("Invocation of method [" + invocation.getMethod() + + "] failed in RMI service [" + getJndiName() + "]", ex); + } + } + else { + // traditional RMI stub + try { + return RmiClientInterceptorUtils.invokeRemoteMethod(invocation, stub); + } + catch (InvocationTargetException ex) { + Throwable targetEx = ex.getTargetException(); + if (targetEx instanceof RemoteException) { + throw convertRmiAccessException((RemoteException) targetEx, invocation.getMethod()); + } + else { + throw targetEx; + } + } + } + } + + /** + * Apply the given AOP method invocation to the given {@link RmiInvocationHandler}. + *

    The default implementation delegates to {@link #createRemoteInvocation}. + * @param methodInvocation the current AOP method invocation + * @param invocationHandler the RmiInvocationHandler to apply the invocation to + * @return the invocation result + * @throws RemoteException in case of communication errors + * @throws NoSuchMethodException if the method name could not be resolved + * @throws IllegalAccessException if the method could not be accessed + * @throws InvocationTargetException if the method invocation resulted in an exception + * @see org.springframework.remoting.support.RemoteInvocation + */ + protected Object doInvoke(MethodInvocation methodInvocation, RmiInvocationHandler invocationHandler) + throws RemoteException, NoSuchMethodException, IllegalAccessException, InvocationTargetException { + + if (AopUtils.isToStringMethod(methodInvocation.getMethod())) { + return "RMI invoker proxy for service URL [" + getJndiName() + "]"; + } + + return invocationHandler.invoke(createRemoteInvocation(methodInvocation)); + } + + /** + * Create a new RemoteInvocation object for the given AOP method invocation. + *

    The default implementation delegates to the configured + * {@link #setRemoteInvocationFactory RemoteInvocationFactory}. + * This can be overridden in subclasses in order to provide custom RemoteInvocation + * subclasses, containing additional invocation parameters (e.g. user credentials). + *

    Note that it is preferable to build a custom RemoteInvocationFactory + * as a reusable strategy, instead of overriding this method. + * @param methodInvocation the current AOP method invocation + * @return the RemoteInvocation object + * @see RemoteInvocationFactory#createRemoteInvocation + */ + protected RemoteInvocation createRemoteInvocation(MethodInvocation methodInvocation) { + return getRemoteInvocationFactory().createRemoteInvocation(methodInvocation); + } + + /** + * Convert the given RMI RemoteException that happened during remote access + * to Spring's RemoteAccessException if the method signature does not declare + * RemoteException. Else, return the original RemoteException. + * @param method the invoked method + * @param ex the RemoteException that happened + * @return the exception to be thrown to the caller + */ + private Exception convertRmiAccessException(RemoteException ex, Method method) { + return RmiClientInterceptorUtils.convertRmiAccessException(method, ex, isConnectFailure(ex), getJndiName()); + } + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/rmi/JndiRmiProxyFactoryBean.java b/spring-context/src/main/java/org/springframework/remoting/rmi/JndiRmiProxyFactoryBean.java new file mode 100644 index 0000000..a9d33d8 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/rmi/JndiRmiProxyFactoryBean.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.rmi; + +import javax.naming.NamingException; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * {@link FactoryBean} for RMI proxies from JNDI. + * + *

    Typically used for RMI-IIOP (CORBA), but can also be used for EJB home objects + * (for example, a Stateful Session Bean home). In contrast to a plain JNDI lookup, + * this accessor also performs narrowing through {@link javax.rmi.PortableRemoteObject}. + * + *

    With conventional RMI services, this invoker is typically used with the RMI + * service interface. Alternatively, this invoker can also proxy a remote RMI service + * with a matching non-RMI business interface, i.e. an interface that mirrors the RMI + * service methods but does not declare RemoteExceptions. In the latter case, + * RemoteExceptions thrown by the RMI stub will automatically get converted to + * Spring's unchecked RemoteAccessException. + * + *

    The JNDI environment can be specified as "jndiEnvironment" property, + * or be configured in a {@code jndi.properties} file or as system properties. + * For example: + * + *

    <property name="jndiEnvironment">
    + * 	 <props>
    + *		 <prop key="java.naming.factory.initial">com.sun.jndi.cosnaming.CNCtxFactory</prop>
    + *		 <prop key="java.naming.provider.url">iiop://localhost:1050</prop>
    + *	 </props>
    + * </property>
    + * + * @author Juergen Hoeller + * @since 1.1 + * @see #setServiceInterface + * @see #setJndiName + * @see #setJndiTemplate + * @see #setJndiEnvironment + * @see #setJndiName + * @see JndiRmiServiceExporter + * @see org.springframework.remoting.RemoteAccessException + * @see java.rmi.RemoteException + * @see java.rmi.Remote + * @see javax.rmi.PortableRemoteObject#narrow + * @deprecated as of 5.3 (phasing out serialization-based remoting) + */ +@Deprecated +public class JndiRmiProxyFactoryBean extends JndiRmiClientInterceptor + implements FactoryBean, BeanClassLoaderAware { + + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + private Object serviceProxy; + + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + @Override + public void afterPropertiesSet() throws NamingException { + super.afterPropertiesSet(); + Class ifc = getServiceInterface(); + Assert.notNull(ifc, "Property 'serviceInterface' is required"); + this.serviceProxy = new ProxyFactory(ifc, this).getProxy(this.beanClassLoader); + } + + + @Override + public Object getObject() { + return this.serviceProxy; + } + + @Override + public Class getObjectType() { + return getServiceInterface(); + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/rmi/JndiRmiServiceExporter.java b/spring-context/src/main/java/org/springframework/remoting/rmi/JndiRmiServiceExporter.java new file mode 100644 index 0000000..49b2db2 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/rmi/JndiRmiServiceExporter.java @@ -0,0 +1,194 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.rmi; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.rmi.Remote; +import java.rmi.RemoteException; +import java.util.Properties; + +import javax.naming.NamingException; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jndi.JndiTemplate; +import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; + +/** + * Service exporter which binds RMI services to JNDI. + * Typically used for RMI-IIOP (CORBA). + * + *

    Exports services via the {@link javax.rmi.PortableRemoteObject} class. + * You need to run "rmic" with the "-iiop" option to generate corresponding + * stubs and skeletons for each exported service. + * + *

    Also supports exposing any non-RMI service via RMI invokers, to be accessed + * via {@link JndiRmiClientInterceptor} / {@link JndiRmiProxyFactoryBean}'s + * automatic detection of such invokers. + * + *

    With an RMI invoker, RMI communication works on the {@link RmiInvocationHandler} + * level, needing only one stub for any service. Service interfaces do not have to + * extend {@code java.rmi.Remote} or throw {@code java.rmi.RemoteException} + * on all methods, but in and out parameters have to be serializable. + * + *

    The JNDI environment can be specified as "jndiEnvironment" bean property, + * or be configured in a {@code jndi.properties} file or as system properties. + * For example: + * + *

    <property name="jndiEnvironment">
    + * 	 <props>
    + *		 <prop key="java.naming.factory.initial">com.sun.jndi.cosnaming.CNCtxFactory</prop>
    + *		 <prop key="java.naming.provider.url">iiop://localhost:1050</prop>
    + *	 </props>
    + * </property>
    + * + * @author Juergen Hoeller + * @since 1.1 + * @see #setService + * @see #setJndiTemplate + * @see #setJndiEnvironment + * @see #setJndiName + * @see JndiRmiClientInterceptor + * @see JndiRmiProxyFactoryBean + * @see javax.rmi.PortableRemoteObject#exportObject + * @deprecated as of 5.3 (phasing out serialization-based remoting) + */ +@Deprecated +public class JndiRmiServiceExporter extends RmiBasedExporter implements InitializingBean, DisposableBean { + + @Nullable + private static Method exportObject; + + @Nullable + private static Method unexportObject; + + static { + try { + Class portableRemoteObject = + JndiRmiServiceExporter.class.getClassLoader().loadClass("javax.rmi.PortableRemoteObject"); + exportObject = portableRemoteObject.getMethod("exportObject", Remote.class); + unexportObject = portableRemoteObject.getMethod("unexportObject", Remote.class); + } + catch (Throwable ex) { + // java.corba module not available on JDK 9+ + exportObject = null; + unexportObject = null; + } + } + + + private JndiTemplate jndiTemplate = new JndiTemplate(); + + private String jndiName; + + private Remote exportedObject; + + + /** + * Set the JNDI template to use for JNDI lookups. + * You can also specify JNDI environment settings via "jndiEnvironment". + * @see #setJndiEnvironment + */ + public void setJndiTemplate(JndiTemplate jndiTemplate) { + this.jndiTemplate = (jndiTemplate != null ? jndiTemplate : new JndiTemplate()); + } + + /** + * Set the JNDI environment to use for JNDI lookups. + * Creates a JndiTemplate with the given environment settings. + * @see #setJndiTemplate + */ + public void setJndiEnvironment(Properties jndiEnvironment) { + this.jndiTemplate = new JndiTemplate(jndiEnvironment); + } + + /** + * Set the JNDI name of the exported RMI service. + */ + public void setJndiName(String jndiName) { + this.jndiName = jndiName; + } + + + @Override + public void afterPropertiesSet() throws NamingException, RemoteException { + prepare(); + } + + /** + * Initialize this service exporter, binding the specified service to JNDI. + * @throws NamingException if service binding failed + * @throws RemoteException if service export failed + */ + public void prepare() throws NamingException, RemoteException { + if (this.jndiName == null) { + throw new IllegalArgumentException("Property 'jndiName' is required"); + } + + // Initialize and cache exported object. + this.exportedObject = getObjectToExport(); + invokePortableRemoteObject(exportObject); + + rebind(); + } + + /** + * Rebind the specified service to JNDI, for recovering in case + * of the target registry having been restarted. + * @throws NamingException if service binding failed + */ + public void rebind() throws NamingException { + if (logger.isDebugEnabled()) { + logger.debug("Binding RMI service to JNDI location [" + this.jndiName + "]"); + } + this.jndiTemplate.rebind(this.jndiName, this.exportedObject); + } + + /** + * Unbind the RMI service from JNDI on bean factory shutdown. + */ + @Override + public void destroy() throws NamingException, RemoteException { + if (logger.isDebugEnabled()) { + logger.debug("Unbinding RMI service from JNDI location [" + this.jndiName + "]"); + } + this.jndiTemplate.unbind(this.jndiName); + invokePortableRemoteObject(unexportObject); + } + + + private void invokePortableRemoteObject(@Nullable Method method) throws RemoteException { + if (method != null) { + try { + method.invoke(null, this.exportedObject); + } + catch (InvocationTargetException ex) { + Throwable targetEx = ex.getTargetException(); + if (targetEx instanceof RemoteException) { + throw (RemoteException) targetEx; + } + ReflectionUtils.rethrowRuntimeException(targetEx); + } + catch (Throwable ex) { + throw new IllegalStateException("PortableRemoteObject invocation failed", ex); + } + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/rmi/RemoteInvocationSerializingExporter.java b/spring-context/src/main/java/org/springframework/remoting/rmi/RemoteInvocationSerializingExporter.java new file mode 100644 index 0000000..99e81a4 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/rmi/RemoteInvocationSerializingExporter.java @@ -0,0 +1,183 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.rmi; + +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.rmi.RemoteException; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.remoting.support.RemoteInvocation; +import org.springframework.remoting.support.RemoteInvocationBasedExporter; +import org.springframework.remoting.support.RemoteInvocationResult; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Abstract base class for remote service exporters that explicitly deserialize + * {@link org.springframework.remoting.support.RemoteInvocation} objects and serialize + * {@link org.springframework.remoting.support.RemoteInvocationResult} objects, + * for example Spring's HTTP invoker. + * + *

    Provides template methods for {@code ObjectInputStream} and + * {@code ObjectOutputStream} handling. + * + * @author Juergen Hoeller + * @since 2.5.1 + * @see java.io.ObjectInputStream + * @see java.io.ObjectOutputStream + * @see #doReadRemoteInvocation + * @see #doWriteRemoteInvocationResult + * @deprecated as of 5.3 (phasing out serialization-based remoting) + */ +@Deprecated +public abstract class RemoteInvocationSerializingExporter extends RemoteInvocationBasedExporter + implements InitializingBean { + + /** + * Default content type: "application/x-java-serialized-object". + */ + public static final String CONTENT_TYPE_SERIALIZED_OBJECT = "application/x-java-serialized-object"; + + + private String contentType = CONTENT_TYPE_SERIALIZED_OBJECT; + + private boolean acceptProxyClasses = true; + + private Object proxy; + + + /** + * Specify the content type to use for sending remote invocation responses. + *

    Default is "application/x-java-serialized-object". + */ + public void setContentType(String contentType) { + Assert.notNull(contentType, "'contentType' must not be null"); + this.contentType = contentType; + } + + /** + * Return the content type to use for sending remote invocation responses. + */ + public String getContentType() { + return this.contentType; + } + + /** + * Set whether to accept deserialization of proxy classes. + *

    Default is "true". May be deactivated as a security measure. + */ + public void setAcceptProxyClasses(boolean acceptProxyClasses) { + this.acceptProxyClasses = acceptProxyClasses; + } + + /** + * Return whether to accept deserialization of proxy classes. + */ + public boolean isAcceptProxyClasses() { + return this.acceptProxyClasses; + } + + + @Override + public void afterPropertiesSet() { + prepare(); + } + + /** + * Initialize this service exporter. + */ + public void prepare() { + this.proxy = getProxyForService(); + } + + protected final Object getProxy() { + if (this.proxy == null) { + throw new IllegalStateException(ClassUtils.getShortName(getClass()) + " has not been initialized"); + } + return this.proxy; + } + + + /** + * Create an ObjectInputStream for the given InputStream. + *

    The default implementation creates a Spring {@link CodebaseAwareObjectInputStream}. + * @param is the InputStream to read from + * @return the new ObjectInputStream instance to use + * @throws java.io.IOException if creation of the ObjectInputStream failed + */ + protected ObjectInputStream createObjectInputStream(InputStream is) throws IOException { + return new CodebaseAwareObjectInputStream(is, getBeanClassLoader(), isAcceptProxyClasses()); + } + + /** + * Perform the actual reading of an invocation result object from the + * given ObjectInputStream. + *

    The default implementation simply calls + * {@link java.io.ObjectInputStream#readObject()}. + * Can be overridden for deserialization of a custom wrapper object rather + * than the plain invocation, for example an encryption-aware holder. + * @param ois the ObjectInputStream to read from + * @return the RemoteInvocationResult object + * @throws java.io.IOException in case of I/O failure + * @throws ClassNotFoundException if case of a transferred class not + * being found in the local ClassLoader + */ + protected RemoteInvocation doReadRemoteInvocation(ObjectInputStream ois) + throws IOException, ClassNotFoundException { + + Object obj = ois.readObject(); + if (!(obj instanceof RemoteInvocation)) { + throw new RemoteException("Deserialized object needs to be assignable to type [" + + RemoteInvocation.class.getName() + "]: " + ClassUtils.getDescriptiveType(obj)); + } + return (RemoteInvocation) obj; + } + + /** + * Create an ObjectOutputStream for the given OutputStream. + *

    The default implementation creates a plain + * {@link java.io.ObjectOutputStream}. + * @param os the OutputStream to write to + * @return the new ObjectOutputStream instance to use + * @throws java.io.IOException if creation of the ObjectOutputStream failed + */ + protected ObjectOutputStream createObjectOutputStream(OutputStream os) throws IOException { + return new ObjectOutputStream(os); + } + + /** + * Perform the actual writing of the given invocation result object + * to the given ObjectOutputStream. + *

    The default implementation simply calls + * {@link java.io.ObjectOutputStream#writeObject}. + * Can be overridden for serialization of a custom wrapper object rather + * than the plain invocation, for example an encryption-aware holder. + * @param result the RemoteInvocationResult object + * @param oos the ObjectOutputStream to write to + * @throws java.io.IOException if thrown by I/O methods + */ + protected void doWriteRemoteInvocationResult(RemoteInvocationResult result, ObjectOutputStream oos) + throws IOException { + + oos.writeObject(result); + } + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/rmi/RmiBasedExporter.java b/spring-context/src/main/java/org/springframework/remoting/rmi/RmiBasedExporter.java new file mode 100644 index 0000000..e6ae652 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/rmi/RmiBasedExporter.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.rmi; + +import java.lang.reflect.InvocationTargetException; +import java.rmi.Remote; + +import org.springframework.remoting.support.RemoteInvocation; +import org.springframework.remoting.support.RemoteInvocationBasedExporter; + +/** + * Convenient superclass for RMI-based remote exporters. Provides a facility + * to automatically wrap a given plain Java service object with an + * RmiInvocationWrapper, exposing the {@link RmiInvocationHandler} remote interface. + * + *

    Using the RMI invoker mechanism, RMI communication operates at the {@link RmiInvocationHandler} + * level, sharing a common invoker stub for any number of services. Service interfaces are not + * required to extend {@code java.rmi.Remote} or declare {@code java.rmi.RemoteException} + * on all service methods. However, in and out parameters still have to be serializable. + * + * @author Juergen Hoeller + * @since 1.2.5 + * @see RmiServiceExporter + * @see JndiRmiServiceExporter + * @deprecated as of 5.3 (phasing out serialization-based remoting) + */ +@Deprecated +public abstract class RmiBasedExporter extends RemoteInvocationBasedExporter { + + /** + * Determine the object to export: either the service object itself + * or a RmiInvocationWrapper in case of a non-RMI service object. + * @return the RMI object to export + * @see #setService + * @see #setServiceInterface + */ + protected Remote getObjectToExport() { + // determine remote object + if (getService() instanceof Remote && + (getServiceInterface() == null || Remote.class.isAssignableFrom(getServiceInterface()))) { + // conventional RMI service + return (Remote) getService(); + } + else { + // RMI invoker + if (logger.isDebugEnabled()) { + logger.debug("RMI service [" + getService() + "] is an RMI invoker"); + } + return new RmiInvocationWrapper(getProxyForService(), this); + } + } + + /** + * Redefined here to be visible to RmiInvocationWrapper. + * Simply delegates to the corresponding superclass method. + */ + @Override + protected Object invoke(RemoteInvocation invocation, Object targetObject) + throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { + + return super.invoke(invocation, targetObject); + } + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/rmi/RmiClientInterceptor.java b/spring-context/src/main/java/org/springframework/remoting/rmi/RmiClientInterceptor.java new file mode 100644 index 0000000..8d8569a --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/rmi/RmiClientInterceptor.java @@ -0,0 +1,424 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.rmi; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.rmi.Naming; +import java.rmi.NotBoundException; +import java.rmi.Remote; +import java.rmi.RemoteException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; +import java.rmi.server.RMIClientSocketFactory; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.aop.support.AopUtils; +import org.springframework.lang.Nullable; +import org.springframework.remoting.RemoteConnectFailureException; +import org.springframework.remoting.RemoteInvocationFailureException; +import org.springframework.remoting.RemoteLookupFailureException; +import org.springframework.remoting.support.RemoteInvocationBasedAccessor; +import org.springframework.remoting.support.RemoteInvocationUtils; + +/** + * {@link org.aopalliance.intercept.MethodInterceptor} for accessing conventional + * RMI services or RMI invokers. The service URL must be a valid RMI URL + * (e.g. "rmi://localhost:1099/myservice"). + * + *

    RMI invokers work at the RmiInvocationHandler level, needing only one stub for + * any service. Service interfaces do not have to extend {@code java.rmi.Remote} + * or throw {@code java.rmi.RemoteException}. Spring's unchecked + * RemoteAccessException will be thrown on remote invocation failure. + * Of course, in and out parameters have to be serializable. + * + *

    With conventional RMI services, this invoker is typically used with the RMI + * service interface. Alternatively, this invoker can also proxy a remote RMI service + * with a matching non-RMI business interface, i.e. an interface that mirrors the RMI + * service methods but does not declare RemoteExceptions. In the latter case, + * RemoteExceptions thrown by the RMI stub will automatically get converted to + * Spring's unchecked RemoteAccessException. + * + * @author Juergen Hoeller + * @since 29.09.2003 + * @see RmiServiceExporter + * @see RmiProxyFactoryBean + * @see RmiInvocationHandler + * @see org.springframework.remoting.RemoteAccessException + * @see java.rmi.RemoteException + * @see java.rmi.Remote + * @deprecated as of 5.3 (phasing out serialization-based remoting) + */ +@Deprecated +public class RmiClientInterceptor extends RemoteInvocationBasedAccessor + implements MethodInterceptor { + + private boolean lookupStubOnStartup = true; + + private boolean cacheStub = true; + + private boolean refreshStubOnConnectFailure = false; + + private RMIClientSocketFactory registryClientSocketFactory; + + private Remote cachedStub; + + private final Object stubMonitor = new Object(); + + + /** + * Set whether to look up the RMI stub on startup. Default is "true". + *

    Can be turned off to allow for late start of the RMI server. + * In this case, the RMI stub will be fetched on first access. + * @see #setCacheStub + */ + public void setLookupStubOnStartup(boolean lookupStubOnStartup) { + this.lookupStubOnStartup = lookupStubOnStartup; + } + + /** + * Set whether to cache the RMI stub once it has been located. + * Default is "true". + *

    Can be turned off to allow for hot restart of the RMI server. + * In this case, the RMI stub will be fetched for each invocation. + * @see #setLookupStubOnStartup + */ + public void setCacheStub(boolean cacheStub) { + this.cacheStub = cacheStub; + } + + /** + * Set whether to refresh the RMI stub on connect failure. + * Default is "false". + *

    Can be turned on to allow for hot restart of the RMI server. + * If a cached RMI stub throws an RMI exception that indicates a + * remote connect failure, a fresh proxy will be fetched and the + * invocation will be retried. + * @see java.rmi.ConnectException + * @see java.rmi.ConnectIOException + * @see java.rmi.NoSuchObjectException + */ + public void setRefreshStubOnConnectFailure(boolean refreshStubOnConnectFailure) { + this.refreshStubOnConnectFailure = refreshStubOnConnectFailure; + } + + /** + * Set a custom RMI client socket factory to use for accessing the RMI registry. + * @see java.rmi.server.RMIClientSocketFactory + * @see java.rmi.registry.LocateRegistry#getRegistry(String, int, RMIClientSocketFactory) + */ + public void setRegistryClientSocketFactory(RMIClientSocketFactory registryClientSocketFactory) { + this.registryClientSocketFactory = registryClientSocketFactory; + } + + + @Override + public void afterPropertiesSet() { + super.afterPropertiesSet(); + prepare(); + } + + /** + * Fetches RMI stub on startup, if necessary. + * @throws RemoteLookupFailureException if RMI stub creation failed + * @see #setLookupStubOnStartup + * @see #lookupStub + */ + public void prepare() throws RemoteLookupFailureException { + // Cache RMI stub on initialization? + if (this.lookupStubOnStartup) { + Remote remoteObj = lookupStub(); + if (logger.isDebugEnabled()) { + if (remoteObj instanceof RmiInvocationHandler) { + logger.debug("RMI stub [" + getServiceUrl() + "] is an RMI invoker"); + } + else if (getServiceInterface() != null) { + boolean isImpl = getServiceInterface().isInstance(remoteObj); + logger.debug("Using service interface [" + getServiceInterface().getName() + + "] for RMI stub [" + getServiceUrl() + "] - " + + (!isImpl ? "not " : "") + "directly implemented"); + } + } + if (this.cacheStub) { + this.cachedStub = remoteObj; + } + } + } + + /** + * Create the RMI stub, typically by looking it up. + *

    Called on interceptor initialization if "cacheStub" is "true"; + * else called for each invocation by {@link #getStub()}. + *

    The default implementation looks up the service URL via + * {@code java.rmi.Naming}. This can be overridden in subclasses. + * @return the RMI stub to store in this interceptor + * @throws RemoteLookupFailureException if RMI stub creation failed + * @see #setCacheStub + * @see java.rmi.Naming#lookup + */ + protected Remote lookupStub() throws RemoteLookupFailureException { + try { + Remote stub = null; + if (this.registryClientSocketFactory != null) { + // RMIClientSocketFactory specified for registry access. + // Unfortunately, due to RMI API limitations, this means + // that we need to parse the RMI URL ourselves and perform + // straight LocateRegistry.getRegistry/Registry.lookup calls. + URL url = new URL(null, getServiceUrl(), new DummyURLStreamHandler()); + String protocol = url.getProtocol(); + if (protocol != null && !"rmi".equals(protocol)) { + throw new MalformedURLException("Invalid URL scheme '" + protocol + "'"); + } + String host = url.getHost(); + int port = url.getPort(); + String name = url.getPath(); + if (name != null && name.startsWith("/")) { + name = name.substring(1); + } + Registry registry = LocateRegistry.getRegistry(host, port, this.registryClientSocketFactory); + stub = registry.lookup(name); + } + else { + // Can proceed with standard RMI lookup API... + stub = Naming.lookup(getServiceUrl()); + } + if (logger.isDebugEnabled()) { + logger.debug("Located RMI stub with URL [" + getServiceUrl() + "]"); + } + return stub; + } + catch (MalformedURLException ex) { + throw new RemoteLookupFailureException("Service URL [" + getServiceUrl() + "] is invalid", ex); + } + catch (NotBoundException ex) { + throw new RemoteLookupFailureException( + "Could not find RMI service [" + getServiceUrl() + "] in RMI registry", ex); + } + catch (RemoteException ex) { + throw new RemoteLookupFailureException("Lookup of RMI stub failed", ex); + } + } + + /** + * Return the RMI stub to use. Called for each invocation. + *

    The default implementation returns the stub created on initialization, + * if any. Else, it invokes {@link #lookupStub} to get a new stub for + * each invocation. This can be overridden in subclasses, for example in + * order to cache a stub for a given amount of time before recreating it, + * or to test the stub whether it is still alive. + * @return the RMI stub to use for an invocation + * @throws RemoteLookupFailureException if RMI stub creation failed + * @see #lookupStub + */ + protected Remote getStub() throws RemoteLookupFailureException { + if (!this.cacheStub || (this.lookupStubOnStartup && !this.refreshStubOnConnectFailure)) { + return (this.cachedStub != null ? this.cachedStub : lookupStub()); + } + else { + synchronized (this.stubMonitor) { + if (this.cachedStub == null) { + this.cachedStub = lookupStub(); + } + return this.cachedStub; + } + } + } + + + /** + * Fetches an RMI stub and delegates to {@code doInvoke}. + * If configured to refresh on connect failure, it will call + * {@link #refreshAndRetry} on corresponding RMI exceptions. + * @see #getStub + * @see #doInvoke(MethodInvocation, Remote) + * @see #refreshAndRetry + * @see java.rmi.ConnectException + * @see java.rmi.ConnectIOException + * @see java.rmi.NoSuchObjectException + */ + @Override + @Nullable + public Object invoke(MethodInvocation invocation) throws Throwable { + Remote stub = getStub(); + try { + return doInvoke(invocation, stub); + } + catch (RemoteConnectFailureException ex) { + return handleRemoteConnectFailure(invocation, ex); + } + catch (RemoteException ex) { + if (isConnectFailure(ex)) { + return handleRemoteConnectFailure(invocation, ex); + } + else { + throw ex; + } + } + } + + /** + * Determine whether the given RMI exception indicates a connect failure. + *

    The default implementation delegates to + * {@link RmiClientInterceptorUtils#isConnectFailure}. + * @param ex the RMI exception to check + * @return whether the exception should be treated as connect failure + */ + protected boolean isConnectFailure(RemoteException ex) { + return RmiClientInterceptorUtils.isConnectFailure(ex); + } + + /** + * Refresh the stub and retry the remote invocation if necessary. + *

    If not configured to refresh on connect failure, this method + * simply rethrows the original exception. + * @param invocation the invocation that failed + * @param ex the exception raised on remote invocation + * @return the result value of the new invocation, if succeeded + * @throws Throwable an exception raised by the new invocation, + * if it failed as well + * @see #setRefreshStubOnConnectFailure + * @see #doInvoke + */ + @Nullable + private Object handleRemoteConnectFailure(MethodInvocation invocation, Exception ex) throws Throwable { + if (this.refreshStubOnConnectFailure) { + String msg = "Could not connect to RMI service [" + getServiceUrl() + "] - retrying"; + if (logger.isDebugEnabled()) { + logger.warn(msg, ex); + } + else if (logger.isWarnEnabled()) { + logger.warn(msg); + } + return refreshAndRetry(invocation); + } + else { + throw ex; + } + } + + /** + * Refresh the RMI stub and retry the given invocation. + * Called by invoke on connect failure. + * @param invocation the AOP method invocation + * @return the invocation result, if any + * @throws Throwable in case of invocation failure + * @see #invoke + */ + @Nullable + protected Object refreshAndRetry(MethodInvocation invocation) throws Throwable { + Remote freshStub = null; + synchronized (this.stubMonitor) { + this.cachedStub = null; + freshStub = lookupStub(); + if (this.cacheStub) { + this.cachedStub = freshStub; + } + } + return doInvoke(invocation, freshStub); + } + + /** + * Perform the given invocation on the given RMI stub. + * @param invocation the AOP method invocation + * @param stub the RMI stub to invoke + * @return the invocation result, if any + * @throws Throwable in case of invocation failure + */ + @Nullable + protected Object doInvoke(MethodInvocation invocation, Remote stub) throws Throwable { + if (stub instanceof RmiInvocationHandler) { + // RMI invoker + try { + return doInvoke(invocation, (RmiInvocationHandler) stub); + } + catch (RemoteException ex) { + throw RmiClientInterceptorUtils.convertRmiAccessException( + invocation.getMethod(), ex, isConnectFailure(ex), getServiceUrl()); + } + catch (InvocationTargetException ex) { + Throwable exToThrow = ex.getTargetException(); + RemoteInvocationUtils.fillInClientStackTraceIfPossible(exToThrow); + throw exToThrow; + } + catch (Throwable ex) { + throw new RemoteInvocationFailureException("Invocation of method [" + invocation.getMethod() + + "] failed in RMI service [" + getServiceUrl() + "]", ex); + } + } + else { + // traditional RMI stub + try { + return RmiClientInterceptorUtils.invokeRemoteMethod(invocation, stub); + } + catch (InvocationTargetException ex) { + Throwable targetEx = ex.getTargetException(); + if (targetEx instanceof RemoteException) { + RemoteException rex = (RemoteException) targetEx; + throw RmiClientInterceptorUtils.convertRmiAccessException( + invocation.getMethod(), rex, isConnectFailure(rex), getServiceUrl()); + } + else { + throw targetEx; + } + } + } + } + + /** + * Apply the given AOP method invocation to the given {@link RmiInvocationHandler}. + *

    The default implementation delegates to {@link #createRemoteInvocation}. + * @param methodInvocation the current AOP method invocation + * @param invocationHandler the RmiInvocationHandler to apply the invocation to + * @return the invocation result + * @throws RemoteException in case of communication errors + * @throws NoSuchMethodException if the method name could not be resolved + * @throws IllegalAccessException if the method could not be accessed + * @throws InvocationTargetException if the method invocation resulted in an exception + * @see org.springframework.remoting.support.RemoteInvocation + */ + @Nullable + protected Object doInvoke(MethodInvocation methodInvocation, RmiInvocationHandler invocationHandler) + throws RemoteException, NoSuchMethodException, IllegalAccessException, InvocationTargetException { + + if (AopUtils.isToStringMethod(methodInvocation.getMethod())) { + return "RMI invoker proxy for service URL [" + getServiceUrl() + "]"; + } + + return invocationHandler.invoke(createRemoteInvocation(methodInvocation)); + } + + + /** + * Dummy URLStreamHandler that's just specified to suppress the standard + * {@code java.net.URL} URLStreamHandler lookup, to be able to + * use the standard URL class for parsing "rmi:..." URLs. + */ + private static class DummyURLStreamHandler extends URLStreamHandler { + + @Override + protected URLConnection openConnection(URL url) throws IOException { + throw new UnsupportedOperationException(); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/rmi/RmiClientInterceptorUtils.java b/spring-context/src/main/java/org/springframework/remoting/rmi/RmiClientInterceptorUtils.java new file mode 100644 index 0000000..2842e06 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/rmi/RmiClientInterceptorUtils.java @@ -0,0 +1,176 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.rmi; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.SocketException; +import java.rmi.ConnectException; +import java.rmi.ConnectIOException; +import java.rmi.NoSuchObjectException; +import java.rmi.RemoteException; +import java.rmi.StubNotFoundException; +import java.rmi.UnknownHostException; + +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.lang.Nullable; +import org.springframework.remoting.RemoteAccessException; +import org.springframework.remoting.RemoteConnectFailureException; +import org.springframework.remoting.RemoteProxyFailureException; +import org.springframework.util.ReflectionUtils; + +/** + * Factored-out methods for performing invocations within an RMI client. + * Can handle both RMI and non-RMI service interfaces working on an RMI stub. + * + *

    Note: This is an SPI class, not intended to be used by applications. + * + * @author Juergen Hoeller + * @since 1.1 + * @deprecated as of 5.3 (phasing out serialization-based remoting) + */ +@Deprecated +public abstract class RmiClientInterceptorUtils { + + private static final Log logger = LogFactory.getLog(RmiClientInterceptorUtils.class); + + + /** + * Perform a raw method invocation on the given RMI stub, + * letting reflection exceptions through as-is. + * @param invocation the AOP MethodInvocation + * @param stub the RMI stub + * @return the invocation result, if any + * @throws InvocationTargetException if thrown by reflection + */ + @Nullable + public static Object invokeRemoteMethod(MethodInvocation invocation, Object stub) + throws InvocationTargetException { + + Method method = invocation.getMethod(); + try { + if (method.getDeclaringClass().isInstance(stub)) { + // directly implemented + return method.invoke(stub, invocation.getArguments()); + } + else { + // not directly implemented + Method stubMethod = stub.getClass().getMethod(method.getName(), method.getParameterTypes()); + return stubMethod.invoke(stub, invocation.getArguments()); + } + } + catch (InvocationTargetException ex) { + throw ex; + } + catch (NoSuchMethodException ex) { + throw new RemoteProxyFailureException("No matching RMI stub method found for: " + method, ex); + } + catch (Throwable ex) { + throw new RemoteProxyFailureException("Invocation of RMI stub method failed: " + method, ex); + } + } + + /** + * Wrap the given arbitrary exception that happened during remote access + * in either a RemoteException or a Spring RemoteAccessException (if the + * method signature does not support RemoteException). + *

    Only call this for remote access exceptions, not for exceptions + * thrown by the target service itself! + * @param method the invoked method + * @param ex the exception that happened, to be used as cause for the + * RemoteAccessException or RemoteException + * @param message the message for the RemoteAccessException respectively + * RemoteException + * @return the exception to be thrown to the caller + */ + public static Exception convertRmiAccessException(Method method, Throwable ex, String message) { + if (logger.isDebugEnabled()) { + logger.debug(message, ex); + } + if (ReflectionUtils.declaresException(method, RemoteException.class)) { + return new RemoteException(message, ex); + } + else { + return new RemoteAccessException(message, ex); + } + } + + /** + * Convert the given RemoteException that happened during remote access + * to Spring's RemoteAccessException if the method signature does not + * support RemoteException. Else, return the original RemoteException. + * @param method the invoked method + * @param ex the RemoteException that happened + * @param serviceName the name of the service (for debugging purposes) + * @return the exception to be thrown to the caller + */ + public static Exception convertRmiAccessException(Method method, RemoteException ex, String serviceName) { + return convertRmiAccessException(method, ex, isConnectFailure(ex), serviceName); + } + + /** + * Convert the given RemoteException that happened during remote access + * to Spring's RemoteAccessException if the method signature does not + * support RemoteException. Else, return the original RemoteException. + * @param method the invoked method + * @param ex the RemoteException that happened + * @param isConnectFailure whether the given exception should be considered + * a connect failure + * @param serviceName the name of the service (for debugging purposes) + * @return the exception to be thrown to the caller + */ + public static Exception convertRmiAccessException( + Method method, RemoteException ex, boolean isConnectFailure, String serviceName) { + + if (logger.isDebugEnabled()) { + logger.debug("Remote service [" + serviceName + "] threw exception", ex); + } + if (ReflectionUtils.declaresException(method, ex.getClass())) { + return ex; + } + else { + if (isConnectFailure) { + return new RemoteConnectFailureException("Could not connect to remote service [" + serviceName + "]", ex); + } + else { + return new RemoteAccessException("Could not access remote service [" + serviceName + "]", ex); + } + } + } + + /** + * Determine whether the given RMI exception indicates a connect failure. + *

    Treats RMI's ConnectException, ConnectIOException, UnknownHostException, + * NoSuchObjectException and StubNotFoundException as connect failure. + * @param ex the RMI exception to check + * @return whether the exception should be treated as connect failure + * @see java.rmi.ConnectException + * @see java.rmi.ConnectIOException + * @see java.rmi.UnknownHostException + * @see java.rmi.NoSuchObjectException + * @see java.rmi.StubNotFoundException + */ + public static boolean isConnectFailure(RemoteException ex) { + return (ex instanceof ConnectException || ex instanceof ConnectIOException || + ex instanceof UnknownHostException || ex instanceof NoSuchObjectException || + ex instanceof StubNotFoundException || ex.getCause() instanceof SocketException); + } + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/rmi/RmiInvocationHandler.java b/spring-context/src/main/java/org/springframework/remoting/rmi/RmiInvocationHandler.java new file mode 100644 index 0000000..fb26b07 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/rmi/RmiInvocationHandler.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.rmi; + +import java.lang.reflect.InvocationTargetException; +import java.rmi.Remote; +import java.rmi.RemoteException; + +import org.springframework.lang.Nullable; +import org.springframework.remoting.support.RemoteInvocation; + +/** + * Interface for RMI invocation handlers instances on the server, + * wrapping exported services. A client uses a stub implementing + * this interface to access such a service. + * + *

    This is an SPI interface, not to be used directly by applications. + * + * @author Juergen Hoeller + * @since 14.05.2003 + * @deprecated as of 5.3 (phasing out serialization-based remoting) + */ +@Deprecated +public interface RmiInvocationHandler extends Remote { + + /** + * Return the name of the target interface that this invoker operates on. + * @return the name of the target interface, or {@code null} if none + * @throws RemoteException in case of communication errors + * @see RmiServiceExporter#getServiceInterface() + */ + @Nullable + public String getTargetInterfaceName() throws RemoteException; + + /** + * Apply the given invocation to the target object. + *

    Called by + * {@link RmiClientInterceptor#doInvoke(org.aopalliance.intercept.MethodInvocation, RmiInvocationHandler)}. + * @param invocation object that encapsulates invocation parameters + * @return the object returned from the invoked method, if any + * @throws RemoteException in case of communication errors + * @throws NoSuchMethodException if the method name could not be resolved + * @throws IllegalAccessException if the method could not be accessed + * @throws InvocationTargetException if the method invocation resulted in an exception + */ + @Nullable + public Object invoke(RemoteInvocation invocation) + throws RemoteException, NoSuchMethodException, IllegalAccessException, InvocationTargetException; + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/rmi/RmiInvocationWrapper.java b/spring-context/src/main/java/org/springframework/remoting/rmi/RmiInvocationWrapper.java new file mode 100644 index 0000000..1f030b3 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/rmi/RmiInvocationWrapper.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.rmi; + +import java.lang.reflect.InvocationTargetException; +import java.rmi.RemoteException; + +import org.springframework.lang.Nullable; +import org.springframework.remoting.support.RemoteInvocation; +import org.springframework.util.Assert; + +/** + * Server-side implementation of {@link RmiInvocationHandler}. An instance + * of this class exists for each remote object. Automatically created + * by {@link RmiServiceExporter} for non-RMI service implementations. + * + *

    This is an SPI class, not to be used directly by applications. + * + * @author Juergen Hoeller + * @since 14.05.2003 + * @see RmiServiceExporter + */ +@Deprecated +class RmiInvocationWrapper implements RmiInvocationHandler { + + private final Object wrappedObject; + + private final RmiBasedExporter rmiExporter; + + + /** + * Create a new RmiInvocationWrapper for the given object. + * @param wrappedObject the object to wrap with an RmiInvocationHandler + * @param rmiExporter the RMI exporter to handle the actual invocation + */ + public RmiInvocationWrapper(Object wrappedObject, RmiBasedExporter rmiExporter) { + Assert.notNull(wrappedObject, "Object to wrap is required"); + Assert.notNull(rmiExporter, "RMI exporter is required"); + this.wrappedObject = wrappedObject; + this.rmiExporter = rmiExporter; + } + + + /** + * Exposes the exporter's service interface, if any, as target interface. + * @see RmiBasedExporter#getServiceInterface() + */ + @Override + @Nullable + public String getTargetInterfaceName() { + Class ifc = this.rmiExporter.getServiceInterface(); + return (ifc != null ? ifc.getName() : null); + } + + /** + * Delegates the actual invocation handling to the RMI exporter. + * @see RmiBasedExporter#invoke(org.springframework.remoting.support.RemoteInvocation, Object) + */ + @Override + @Nullable + public Object invoke(RemoteInvocation invocation) + throws RemoteException, NoSuchMethodException, IllegalAccessException, InvocationTargetException { + + return this.rmiExporter.invoke(invocation, this.wrappedObject); + } + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/rmi/RmiProxyFactoryBean.java b/spring-context/src/main/java/org/springframework/remoting/rmi/RmiProxyFactoryBean.java new file mode 100644 index 0000000..1426409 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/rmi/RmiProxyFactoryBean.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.rmi; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.util.Assert; + +/** + * {@link FactoryBean} for RMI proxies, supporting both conventional RMI services + * and RMI invokers. Exposes the proxied service for use as a bean reference, + * using the specified service interface. Proxies will throw Spring's unchecked + * RemoteAccessException on remote invocation failure instead of RMI's RemoteException. + * + *

    The service URL must be a valid RMI URL like "rmi://localhost:1099/myservice". + * RMI invokers work at the RmiInvocationHandler level, using the same invoker stub + * for any service. Service interfaces do not have to extend {@code java.rmi.Remote} + * or throw {@code java.rmi.RemoteException}. Of course, in and out parameters + * have to be serializable. + * + *

    With conventional RMI services, this proxy factory is typically used with the + * RMI service interface. Alternatively, this factory can also proxy a remote RMI + * service with a matching non-RMI business interface, i.e. an interface that mirrors + * the RMI service methods but does not declare RemoteExceptions. In the latter case, + * RemoteExceptions thrown by the RMI stub will automatically get converted to + * Spring's unchecked RemoteAccessException. + * + *

    The major advantage of RMI, compared to Hessian, is serialization. + * Effectively, any serializable Java object can be transported without hassle. + * Hessian has its own (de-)serialization mechanisms, but is HTTP-based and thus + * much easier to setup than RMI. Alternatively, consider Spring's HTTP invoker + * to combine Java serialization with HTTP-based transport. + * + * @author Juergen Hoeller + * @since 13.05.2003 + * @see #setServiceInterface + * @see #setServiceUrl + * @see RmiClientInterceptor + * @see RmiServiceExporter + * @see java.rmi.Remote + * @see java.rmi.RemoteException + * @see org.springframework.remoting.RemoteAccessException + * @see org.springframework.remoting.caucho.HessianProxyFactoryBean + * @see org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean + * @deprecated as of 5.3 (phasing out serialization-based remoting) + */ +@Deprecated +public class RmiProxyFactoryBean extends RmiClientInterceptor implements FactoryBean, BeanClassLoaderAware { + + private Object serviceProxy; + + + @Override + public void afterPropertiesSet() { + super.afterPropertiesSet(); + Class ifc = getServiceInterface(); + Assert.notNull(ifc, "Property 'serviceInterface' is required"); + this.serviceProxy = new ProxyFactory(ifc, this).getProxy(getBeanClassLoader()); + } + + + @Override + public Object getObject() { + return this.serviceProxy; + } + + @Override + public Class getObjectType() { + return getServiceInterface(); + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/rmi/RmiRegistryFactoryBean.java b/spring-context/src/main/java/org/springframework/remoting/rmi/RmiRegistryFactoryBean.java new file mode 100644 index 0000000..e0b9cf6 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/rmi/RmiRegistryFactoryBean.java @@ -0,0 +1,317 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.rmi; + +import java.rmi.RemoteException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; +import java.rmi.server.RMIClientSocketFactory; +import java.rmi.server.RMIServerSocketFactory; +import java.rmi.server.UnicastRemoteObject; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.lang.Nullable; + +/** + * {@link FactoryBean} that locates a {@link java.rmi.registry.Registry} and + * exposes it for bean references. Can also create a local RMI registry + * on the fly if none exists already. + * + *

    Can be used to set up and pass around the actual Registry object to + * applications objects that need to work with RMI. One example for such an + * object that needs to work with RMI is Spring's {@link RmiServiceExporter}, + * which either works with a passed-in Registry reference or falls back to + * the registry as specified by its local properties and defaults. + * + *

    Also useful to enforce creation of a local RMI registry at a given port, + * for example for a JMX connector. If used in conjunction with + * {@link org.springframework.jmx.support.ConnectorServerFactoryBean}, + * it is recommended to mark the connector definition (ConnectorServerFactoryBean) + * as "depends-on" the registry definition (RmiRegistryFactoryBean), + * to guarantee starting up the registry first. + * + *

    Note: The implementation of this class mirrors the corresponding logic + * in {@link RmiServiceExporter}, and also offers the same customization hooks. + * RmiServiceExporter implements its own registry lookup as a convenience: + * It is very common to simply rely on the registry defaults. + * + * @author Juergen Hoeller + * @since 1.2.3 + * @see RmiServiceExporter#setRegistry + * @see org.springframework.jmx.support.ConnectorServerFactoryBean + * @see java.rmi.registry.Registry + * @see java.rmi.registry.LocateRegistry + * @deprecated as of 5.3 (phasing out serialization-based remoting) + */ +@Deprecated +public class RmiRegistryFactoryBean implements FactoryBean, InitializingBean, DisposableBean { + + protected final Log logger = LogFactory.getLog(getClass()); + + private String host; + + private int port = Registry.REGISTRY_PORT; + + private RMIClientSocketFactory clientSocketFactory; + + private RMIServerSocketFactory serverSocketFactory; + + private Registry registry; + + private boolean alwaysCreate = false; + + private boolean created = false; + + + /** + * Set the host of the registry for the exported RMI service, + * i.e. {@code rmi://HOST:port/name} + *

    Default is localhost. + */ + public void setHost(String host) { + this.host = host; + } + + /** + * Return the host of the registry for the exported RMI service. + */ + public String getHost() { + return this.host; + } + + /** + * Set the port of the registry for the exported RMI service, + * i.e. {@code rmi://host:PORT/name} + *

    Default is {@code Registry.REGISTRY_PORT} (1099). + */ + public void setPort(int port) { + this.port = port; + } + + /** + * Return the port of the registry for the exported RMI service. + */ + public int getPort() { + return this.port; + } + + /** + * Set a custom RMI client socket factory to use for the RMI registry. + *

    If the given object also implements {@code java.rmi.server.RMIServerSocketFactory}, + * it will automatically be registered as server socket factory too. + * @see #setServerSocketFactory + * @see java.rmi.server.RMIClientSocketFactory + * @see java.rmi.server.RMIServerSocketFactory + * @see java.rmi.registry.LocateRegistry#getRegistry(String, int, java.rmi.server.RMIClientSocketFactory) + */ + public void setClientSocketFactory(RMIClientSocketFactory clientSocketFactory) { + this.clientSocketFactory = clientSocketFactory; + } + + /** + * Set a custom RMI server socket factory to use for the RMI registry. + *

    Only needs to be specified when the client socket factory does not + * implement {@code java.rmi.server.RMIServerSocketFactory} already. + * @see #setClientSocketFactory + * @see java.rmi.server.RMIClientSocketFactory + * @see java.rmi.server.RMIServerSocketFactory + * @see java.rmi.registry.LocateRegistry#createRegistry(int, RMIClientSocketFactory, java.rmi.server.RMIServerSocketFactory) + */ + public void setServerSocketFactory(RMIServerSocketFactory serverSocketFactory) { + this.serverSocketFactory = serverSocketFactory; + } + + /** + * Set whether to always create the registry in-process, + * not attempting to locate an existing registry at the specified port. + *

    Default is "false". Switch this flag to "true" in order to avoid + * the overhead of locating an existing registry when you always + * intend to create a new registry in any case. + */ + public void setAlwaysCreate(boolean alwaysCreate) { + this.alwaysCreate = alwaysCreate; + } + + + @Override + public void afterPropertiesSet() throws Exception { + // Check socket factories for registry. + if (this.clientSocketFactory instanceof RMIServerSocketFactory) { + this.serverSocketFactory = (RMIServerSocketFactory) this.clientSocketFactory; + } + if ((this.clientSocketFactory != null && this.serverSocketFactory == null) || + (this.clientSocketFactory == null && this.serverSocketFactory != null)) { + throw new IllegalArgumentException( + "Both RMIClientSocketFactory and RMIServerSocketFactory or none required"); + } + + // Fetch RMI registry to expose. + this.registry = getRegistry(this.host, this.port, this.clientSocketFactory, this.serverSocketFactory); + } + + + /** + * Locate or create the RMI registry. + * @param registryHost the registry host to use (if this is specified, + * no implicit creation of a RMI registry will happen) + * @param registryPort the registry port to use + * @param clientSocketFactory the RMI client socket factory for the registry (if any) + * @param serverSocketFactory the RMI server socket factory for the registry (if any) + * @return the RMI registry + * @throws java.rmi.RemoteException if the registry couldn't be located or created + */ + protected Registry getRegistry(String registryHost, int registryPort, + @Nullable RMIClientSocketFactory clientSocketFactory, @Nullable RMIServerSocketFactory serverSocketFactory) + throws RemoteException { + + if (registryHost != null) { + // Host explicitly specified: only lookup possible. + if (logger.isDebugEnabled()) { + logger.debug("Looking for RMI registry at port '" + registryPort + "' of host [" + registryHost + "]"); + } + Registry reg = LocateRegistry.getRegistry(registryHost, registryPort, clientSocketFactory); + testRegistry(reg); + return reg; + } + + else { + return getRegistry(registryPort, clientSocketFactory, serverSocketFactory); + } + } + + /** + * Locate or create the RMI registry. + * @param registryPort the registry port to use + * @param clientSocketFactory the RMI client socket factory for the registry (if any) + * @param serverSocketFactory the RMI server socket factory for the registry (if any) + * @return the RMI registry + * @throws RemoteException if the registry couldn't be located or created + */ + protected Registry getRegistry(int registryPort, + @Nullable RMIClientSocketFactory clientSocketFactory, @Nullable RMIServerSocketFactory serverSocketFactory) + throws RemoteException { + + if (clientSocketFactory != null) { + if (this.alwaysCreate) { + logger.debug("Creating new RMI registry"); + this.created = true; + return LocateRegistry.createRegistry(registryPort, clientSocketFactory, serverSocketFactory); + } + if (logger.isDebugEnabled()) { + logger.debug("Looking for RMI registry at port '" + registryPort + "', using custom socket factory"); + } + synchronized (LocateRegistry.class) { + try { + // Retrieve existing registry. + Registry reg = LocateRegistry.getRegistry(null, registryPort, clientSocketFactory); + testRegistry(reg); + return reg; + } + catch (RemoteException ex) { + logger.trace("RMI registry access threw exception", ex); + logger.debug("Could not detect RMI registry - creating new one"); + // Assume no registry found -> create new one. + this.created = true; + return LocateRegistry.createRegistry(registryPort, clientSocketFactory, serverSocketFactory); + } + } + } + + else { + return getRegistry(registryPort); + } + } + + /** + * Locate or create the RMI registry. + * @param registryPort the registry port to use + * @return the RMI registry + * @throws RemoteException if the registry couldn't be located or created + */ + protected Registry getRegistry(int registryPort) throws RemoteException { + if (this.alwaysCreate) { + logger.debug("Creating new RMI registry"); + this.created = true; + return LocateRegistry.createRegistry(registryPort); + } + if (logger.isDebugEnabled()) { + logger.debug("Looking for RMI registry at port '" + registryPort + "'"); + } + synchronized (LocateRegistry.class) { + try { + // Retrieve existing registry. + Registry reg = LocateRegistry.getRegistry(registryPort); + testRegistry(reg); + return reg; + } + catch (RemoteException ex) { + logger.trace("RMI registry access threw exception", ex); + logger.debug("Could not detect RMI registry - creating new one"); + // Assume no registry found -> create new one. + this.created = true; + return LocateRegistry.createRegistry(registryPort); + } + } + } + + /** + * Test the given RMI registry, calling some operation on it to + * check whether it is still active. + *

    Default implementation calls {@code Registry.list()}. + * @param registry the RMI registry to test + * @throws RemoteException if thrown by registry methods + * @see java.rmi.registry.Registry#list() + */ + protected void testRegistry(Registry registry) throws RemoteException { + registry.list(); + } + + + @Override + public Registry getObject() throws Exception { + return this.registry; + } + + @Override + public Class getObjectType() { + return (this.registry != null ? this.registry.getClass() : Registry.class); + } + + @Override + public boolean isSingleton() { + return true; + } + + + /** + * Unexport the RMI registry on bean factory shutdown, + * provided that this bean actually created a registry. + */ + @Override + public void destroy() throws RemoteException { + if (this.created) { + logger.debug("Unexporting RMI registry"); + UnicastRemoteObject.unexportObject(this.registry, true); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/rmi/RmiServiceExporter.java b/spring-context/src/main/java/org/springframework/remoting/rmi/RmiServiceExporter.java new file mode 100644 index 0000000..9ee3a6c --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/rmi/RmiServiceExporter.java @@ -0,0 +1,464 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.rmi; + +import java.rmi.AlreadyBoundException; +import java.rmi.NoSuchObjectException; +import java.rmi.NotBoundException; +import java.rmi.Remote; +import java.rmi.RemoteException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; +import java.rmi.server.RMIClientSocketFactory; +import java.rmi.server.RMIServerSocketFactory; +import java.rmi.server.UnicastRemoteObject; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.lang.Nullable; + +/** + * RMI exporter that exposes the specified service as RMI object with the specified name. + * Such services can be accessed via plain RMI or via {@link RmiProxyFactoryBean}. + * Also supports exposing any non-RMI service via RMI invokers, to be accessed via + * {@link RmiClientInterceptor} / {@link RmiProxyFactoryBean}'s automatic detection + * of such invokers. + * + *

    With an RMI invoker, RMI communication works on the {@link RmiInvocationHandler} + * level, needing only one stub for any service. Service interfaces do not have to + * extend {@code java.rmi.Remote} or throw {@code java.rmi.RemoteException} + * on all methods, but in and out parameters have to be serializable. + * + *

    The major advantage of RMI, compared to Hessian, is serialization. + * Effectively, any serializable Java object can be transported without hassle. + * Hessian has its own (de-)serialization mechanisms, but is HTTP-based and thus + * much easier to setup than RMI. Alternatively, consider Spring's HTTP invoker + * to combine Java serialization with HTTP-based transport. + * + *

    Note: RMI makes a best-effort attempt to obtain the fully qualified host name. + * If one cannot be determined, it will fall back and use the IP address. Depending + * on your network configuration, in some cases it will resolve the IP to the loopback + * address. To ensure that RMI will use the host name bound to the correct network + * interface, you should pass the {@code java.rmi.server.hostname} property to the + * JVM that will export the registry and/or the service using the "-D" JVM argument. + * For example: {@code -Djava.rmi.server.hostname=myserver.com} + * + * @author Juergen Hoeller + * @since 13.05.2003 + * @see RmiClientInterceptor + * @see RmiProxyFactoryBean + * @see java.rmi.Remote + * @see java.rmi.RemoteException + * @see org.springframework.remoting.caucho.HessianServiceExporter + * @see org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter + * @deprecated as of 5.3 (phasing out serialization-based remoting) + */ +@Deprecated +public class RmiServiceExporter extends RmiBasedExporter implements InitializingBean, DisposableBean { + + private String serviceName; + + private int servicePort = 0; // anonymous port + + private RMIClientSocketFactory clientSocketFactory; + + private RMIServerSocketFactory serverSocketFactory; + + private Registry registry; + + private String registryHost; + + private int registryPort = Registry.REGISTRY_PORT; + + private RMIClientSocketFactory registryClientSocketFactory; + + private RMIServerSocketFactory registryServerSocketFactory; + + private boolean alwaysCreateRegistry = false; + + private boolean replaceExistingBinding = true; + + private Remote exportedObject; + + private boolean createdRegistry = false; + + + /** + * Set the name of the exported RMI service, + * i.e. {@code rmi://host:port/NAME} + */ + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + /** + * Set the port that the exported RMI service will use. + *

    Default is 0 (anonymous port). + */ + public void setServicePort(int servicePort) { + this.servicePort = servicePort; + } + + /** + * Set a custom RMI client socket factory to use for exporting the service. + *

    If the given object also implements {@code java.rmi.server.RMIServerSocketFactory}, + * it will automatically be registered as server socket factory too. + * @see #setServerSocketFactory + * @see java.rmi.server.RMIClientSocketFactory + * @see java.rmi.server.RMIServerSocketFactory + * @see UnicastRemoteObject#exportObject(Remote, int, RMIClientSocketFactory, RMIServerSocketFactory) + */ + public void setClientSocketFactory(RMIClientSocketFactory clientSocketFactory) { + this.clientSocketFactory = clientSocketFactory; + } + + /** + * Set a custom RMI server socket factory to use for exporting the service. + *

    Only needs to be specified when the client socket factory does not + * implement {@code java.rmi.server.RMIServerSocketFactory} already. + * @see #setClientSocketFactory + * @see java.rmi.server.RMIClientSocketFactory + * @see java.rmi.server.RMIServerSocketFactory + * @see UnicastRemoteObject#exportObject(Remote, int, RMIClientSocketFactory, RMIServerSocketFactory) + */ + public void setServerSocketFactory(RMIServerSocketFactory serverSocketFactory) { + this.serverSocketFactory = serverSocketFactory; + } + + /** + * Specify the RMI registry to register the exported service with. + * Typically used in combination with RmiRegistryFactoryBean. + *

    Alternatively, you can specify all registry properties locally. + * This exporter will then try to locate the specified registry, + * automatically creating a new local one if appropriate. + *

    Default is a local registry at the default port (1099), + * created on the fly if necessary. + * @see RmiRegistryFactoryBean + * @see #setRegistryHost + * @see #setRegistryPort + * @see #setRegistryClientSocketFactory + * @see #setRegistryServerSocketFactory + */ + public void setRegistry(Registry registry) { + this.registry = registry; + } + + /** + * Set the host of the registry for the exported RMI service, + * i.e. {@code rmi://HOST:port/name} + *

    Default is localhost. + */ + public void setRegistryHost(String registryHost) { + this.registryHost = registryHost; + } + + /** + * Set the port of the registry for the exported RMI service, + * i.e. {@code rmi://host:PORT/name} + *

    Default is {@code Registry.REGISTRY_PORT} (1099). + * @see java.rmi.registry.Registry#REGISTRY_PORT + */ + public void setRegistryPort(int registryPort) { + this.registryPort = registryPort; + } + + /** + * Set a custom RMI client socket factory to use for the RMI registry. + *

    If the given object also implements {@code java.rmi.server.RMIServerSocketFactory}, + * it will automatically be registered as server socket factory too. + * @see #setRegistryServerSocketFactory + * @see java.rmi.server.RMIClientSocketFactory + * @see java.rmi.server.RMIServerSocketFactory + * @see LocateRegistry#getRegistry(String, int, RMIClientSocketFactory) + */ + public void setRegistryClientSocketFactory(RMIClientSocketFactory registryClientSocketFactory) { + this.registryClientSocketFactory = registryClientSocketFactory; + } + + /** + * Set a custom RMI server socket factory to use for the RMI registry. + *

    Only needs to be specified when the client socket factory does not + * implement {@code java.rmi.server.RMIServerSocketFactory} already. + * @see #setRegistryClientSocketFactory + * @see java.rmi.server.RMIClientSocketFactory + * @see java.rmi.server.RMIServerSocketFactory + * @see LocateRegistry#createRegistry(int, RMIClientSocketFactory, RMIServerSocketFactory) + */ + public void setRegistryServerSocketFactory(RMIServerSocketFactory registryServerSocketFactory) { + this.registryServerSocketFactory = registryServerSocketFactory; + } + + /** + * Set whether to always create the registry in-process, + * not attempting to locate an existing registry at the specified port. + *

    Default is "false". Switch this flag to "true" in order to avoid + * the overhead of locating an existing registry when you always + * intend to create a new registry in any case. + */ + public void setAlwaysCreateRegistry(boolean alwaysCreateRegistry) { + this.alwaysCreateRegistry = alwaysCreateRegistry; + } + + /** + * Set whether to replace an existing binding in the RMI registry, + * that is, whether to simply override an existing binding with the + * specified service in case of a naming conflict in the registry. + *

    Default is "true", assuming that an existing binding for this + * exporter's service name is an accidental leftover from a previous + * execution. Switch this to "false" to make the exporter fail in such + * a scenario, indicating that there was already an RMI object bound. + */ + public void setReplaceExistingBinding(boolean replaceExistingBinding) { + this.replaceExistingBinding = replaceExistingBinding; + } + + + @Override + public void afterPropertiesSet() throws RemoteException { + prepare(); + } + + /** + * Initialize this service exporter, registering the service as RMI object. + *

    Creates an RMI registry on the specified port if none exists. + * @throws RemoteException if service registration failed + */ + public void prepare() throws RemoteException { + checkService(); + + if (this.serviceName == null) { + throw new IllegalArgumentException("Property 'serviceName' is required"); + } + + // Check socket factories for exported object. + if (this.clientSocketFactory instanceof RMIServerSocketFactory) { + this.serverSocketFactory = (RMIServerSocketFactory) this.clientSocketFactory; + } + if ((this.clientSocketFactory != null && this.serverSocketFactory == null) || + (this.clientSocketFactory == null && this.serverSocketFactory != null)) { + throw new IllegalArgumentException( + "Both RMIClientSocketFactory and RMIServerSocketFactory or none required"); + } + + // Check socket factories for RMI registry. + if (this.registryClientSocketFactory instanceof RMIServerSocketFactory) { + this.registryServerSocketFactory = (RMIServerSocketFactory) this.registryClientSocketFactory; + } + if (this.registryClientSocketFactory == null && this.registryServerSocketFactory != null) { + throw new IllegalArgumentException( + "RMIServerSocketFactory without RMIClientSocketFactory for registry not supported"); + } + + this.createdRegistry = false; + + // Determine RMI registry to use. + if (this.registry == null) { + this.registry = getRegistry(this.registryHost, this.registryPort, + this.registryClientSocketFactory, this.registryServerSocketFactory); + this.createdRegistry = true; + } + + // Initialize and cache exported object. + this.exportedObject = getObjectToExport(); + + if (logger.isDebugEnabled()) { + logger.debug("Binding service '" + this.serviceName + "' to RMI registry: " + this.registry); + } + + // Export RMI object. + if (this.clientSocketFactory != null) { + UnicastRemoteObject.exportObject( + this.exportedObject, this.servicePort, this.clientSocketFactory, this.serverSocketFactory); + } + else { + UnicastRemoteObject.exportObject(this.exportedObject, this.servicePort); + } + + // Bind RMI object to registry. + try { + if (this.replaceExistingBinding) { + this.registry.rebind(this.serviceName, this.exportedObject); + } + else { + this.registry.bind(this.serviceName, this.exportedObject); + } + } + catch (AlreadyBoundException ex) { + // Already an RMI object bound for the specified service name... + unexportObjectSilently(); + throw new IllegalStateException( + "Already an RMI object bound for name '" + this.serviceName + "': " + ex.toString()); + } + catch (RemoteException ex) { + // Registry binding failed: let's unexport the RMI object as well. + unexportObjectSilently(); + throw ex; + } + } + + + /** + * Locate or create the RMI registry for this exporter. + * @param registryHost the registry host to use (if this is specified, + * no implicit creation of a RMI registry will happen) + * @param registryPort the registry port to use + * @param clientSocketFactory the RMI client socket factory for the registry (if any) + * @param serverSocketFactory the RMI server socket factory for the registry (if any) + * @return the RMI registry + * @throws RemoteException if the registry couldn't be located or created + */ + protected Registry getRegistry(String registryHost, int registryPort, + @Nullable RMIClientSocketFactory clientSocketFactory, @Nullable RMIServerSocketFactory serverSocketFactory) + throws RemoteException { + + if (registryHost != null) { + // Host explicitly specified: only lookup possible. + if (logger.isDebugEnabled()) { + logger.debug("Looking for RMI registry at port '" + registryPort + "' of host [" + registryHost + "]"); + } + Registry reg = LocateRegistry.getRegistry(registryHost, registryPort, clientSocketFactory); + testRegistry(reg); + return reg; + } + + else { + return getRegistry(registryPort, clientSocketFactory, serverSocketFactory); + } + } + + /** + * Locate or create the RMI registry for this exporter. + * @param registryPort the registry port to use + * @param clientSocketFactory the RMI client socket factory for the registry (if any) + * @param serverSocketFactory the RMI server socket factory for the registry (if any) + * @return the RMI registry + * @throws RemoteException if the registry couldn't be located or created + */ + protected Registry getRegistry(int registryPort, + @Nullable RMIClientSocketFactory clientSocketFactory, @Nullable RMIServerSocketFactory serverSocketFactory) + throws RemoteException { + + if (clientSocketFactory != null) { + if (this.alwaysCreateRegistry) { + logger.debug("Creating new RMI registry"); + return LocateRegistry.createRegistry(registryPort, clientSocketFactory, serverSocketFactory); + } + if (logger.isDebugEnabled()) { + logger.debug("Looking for RMI registry at port '" + registryPort + "', using custom socket factory"); + } + synchronized (LocateRegistry.class) { + try { + // Retrieve existing registry. + Registry reg = LocateRegistry.getRegistry(null, registryPort, clientSocketFactory); + testRegistry(reg); + return reg; + } + catch (RemoteException ex) { + logger.trace("RMI registry access threw exception", ex); + logger.debug("Could not detect RMI registry - creating new one"); + // Assume no registry found -> create new one. + return LocateRegistry.createRegistry(registryPort, clientSocketFactory, serverSocketFactory); + } + } + } + + else { + return getRegistry(registryPort); + } + } + + /** + * Locate or create the RMI registry for this exporter. + * @param registryPort the registry port to use + * @return the RMI registry + * @throws RemoteException if the registry couldn't be located or created + */ + protected Registry getRegistry(int registryPort) throws RemoteException { + if (this.alwaysCreateRegistry) { + logger.debug("Creating new RMI registry"); + return LocateRegistry.createRegistry(registryPort); + } + if (logger.isDebugEnabled()) { + logger.debug("Looking for RMI registry at port '" + registryPort + "'"); + } + synchronized (LocateRegistry.class) { + try { + // Retrieve existing registry. + Registry reg = LocateRegistry.getRegistry(registryPort); + testRegistry(reg); + return reg; + } + catch (RemoteException ex) { + logger.trace("RMI registry access threw exception", ex); + logger.debug("Could not detect RMI registry - creating new one"); + // Assume no registry found -> create new one. + return LocateRegistry.createRegistry(registryPort); + } + } + } + + /** + * Test the given RMI registry, calling some operation on it to + * check whether it is still active. + *

    Default implementation calls {@code Registry.list()}. + * @param registry the RMI registry to test + * @throws RemoteException if thrown by registry methods + * @see java.rmi.registry.Registry#list() + */ + protected void testRegistry(Registry registry) throws RemoteException { + registry.list(); + } + + + /** + * Unbind the RMI service from the registry on bean factory shutdown. + */ + @Override + public void destroy() throws RemoteException { + if (logger.isDebugEnabled()) { + logger.debug("Unbinding RMI service '" + this.serviceName + + "' from registry" + (this.createdRegistry ? (" at port '" + this.registryPort + "'") : "")); + } + try { + this.registry.unbind(this.serviceName); + } + catch (NotBoundException ex) { + if (logger.isInfoEnabled()) { + logger.info("RMI service '" + this.serviceName + "' is not bound to registry" + + (this.createdRegistry ? (" at port '" + this.registryPort + "' anymore") : ""), ex); + } + } + finally { + unexportObjectSilently(); + } + } + + /** + * Unexport the registered RMI object, logging any exception that arises. + */ + private void unexportObjectSilently() { + try { + UnicastRemoteObject.unexportObject(this.exportedObject, true); + } + catch (NoSuchObjectException ex) { + if (logger.isInfoEnabled()) { + logger.info("RMI object for service '" + this.serviceName + "' is not exported anymore", ex); + } + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/rmi/package-info.java b/spring-context/src/main/java/org/springframework/remoting/rmi/package-info.java new file mode 100644 index 0000000..8296464 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/rmi/package-info.java @@ -0,0 +1,6 @@ +/** + * Remoting classes for conventional RMI and transparent remoting via + * RMI invokers. Provides a proxy factory for accessing RMI services, + * and an exporter for making beans available to RMI clients. + */ +package org.springframework.remoting.rmi; diff --git a/spring-context/src/main/java/org/springframework/remoting/soap/SoapFaultException.java b/spring-context/src/main/java/org/springframework/remoting/soap/SoapFaultException.java new file mode 100644 index 0000000..175c351 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/soap/SoapFaultException.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.soap; + +import javax.xml.namespace.QName; + +import org.springframework.remoting.RemoteInvocationFailureException; + +/** + * RemoteInvocationFailureException subclass that provides the details + * of a SOAP fault. + * + * @author Juergen Hoeller + * @since 2.5 + * @see javax.xml.rpc.soap.SOAPFaultException + * @see javax.xml.ws.soap.SOAPFaultException + */ +@SuppressWarnings("serial") +public abstract class SoapFaultException extends RemoteInvocationFailureException { + + /** + * Constructor for SoapFaultException. + * @param msg the detail message + * @param cause the root cause from the SOAP API in use + */ + protected SoapFaultException(String msg, Throwable cause) { + super(msg, cause); + } + + + /** + * Return the SOAP fault code. + */ + public abstract String getFaultCode(); + + /** + * Return the SOAP fault code as a {@code QName} object. + */ + public abstract QName getFaultCodeAsQName(); + + /** + * Return the descriptive SOAP fault string. + */ + public abstract String getFaultString(); + + /** + * Return the actor that caused this fault. + */ + public abstract String getFaultActor(); + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/soap/package-info.java b/spring-context/src/main/java/org/springframework/remoting/soap/package-info.java new file mode 100644 index 0000000..e1e1107 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/soap/package-info.java @@ -0,0 +1,4 @@ +/** + * SOAP-specific exceptions and support classes for Spring's remoting subsystem. + */ +package org.springframework.remoting.soap; diff --git a/spring-context/src/main/java/org/springframework/remoting/support/DefaultRemoteInvocationExecutor.java b/spring-context/src/main/java/org/springframework/remoting/support/DefaultRemoteInvocationExecutor.java new file mode 100644 index 0000000..870fe87 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/support/DefaultRemoteInvocationExecutor.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.support; + +import java.lang.reflect.InvocationTargetException; + +import org.springframework.util.Assert; + +/** + * Default implementation of the {@link RemoteInvocationExecutor} interface. + * Simply delegates to {@link RemoteInvocation}'s invoke method. + * + * @author Juergen Hoeller + * @since 1.1 + * @see RemoteInvocation#invoke + */ +public class DefaultRemoteInvocationExecutor implements RemoteInvocationExecutor { + + @Override + public Object invoke(RemoteInvocation invocation, Object targetObject) + throws NoSuchMethodException, IllegalAccessException, InvocationTargetException{ + + Assert.notNull(invocation, "RemoteInvocation must not be null"); + Assert.notNull(targetObject, "Target object must not be null"); + return invocation.invoke(targetObject); + } + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/support/DefaultRemoteInvocationFactory.java b/spring-context/src/main/java/org/springframework/remoting/support/DefaultRemoteInvocationFactory.java new file mode 100644 index 0000000..10f3ab1 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/support/DefaultRemoteInvocationFactory.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.support; + +import org.aopalliance.intercept.MethodInvocation; + +/** + * Default implementation of the {@link RemoteInvocationFactory} interface. + * Simply creates a new standard {@link RemoteInvocation} object. + * + * @author Juergen Hoeller + * @since 1.1 + */ +public class DefaultRemoteInvocationFactory implements RemoteInvocationFactory { + + @Override + public RemoteInvocation createRemoteInvocation(MethodInvocation methodInvocation) { + return new RemoteInvocation(methodInvocation); + } + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/support/RemoteAccessor.java b/spring-context/src/main/java/org/springframework/remoting/support/RemoteAccessor.java new file mode 100644 index 0000000..f26ca9a --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/support/RemoteAccessor.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.support; + +import org.springframework.util.Assert; + +/** + * Abstract base class for classes that access a remote service. + * Provides a "serviceInterface" bean property. + * + *

    Note that the service interface being used will show some signs of + * remotability, like the granularity of method calls that it offers. + * Furthermore, it has to have serializable arguments etc. + * + *

    Accessors are supposed to throw Spring's generic + * {@link org.springframework.remoting.RemoteAccessException} in case + * of remote invocation failure, provided that the service interface + * does not declare {@code java.rmi.RemoteException}. + * + * @author Juergen Hoeller + * @since 13.05.2003 + * @see org.springframework.remoting.RemoteAccessException + * @see java.rmi.RemoteException + */ +public abstract class RemoteAccessor extends RemotingSupport { + + private Class serviceInterface; + + + /** + * Set the interface of the service to access. + * The interface must be suitable for the particular service and remoting strategy. + *

    Typically required to be able to create a suitable service proxy, + * but can also be optional if the lookup returns a typed proxy. + */ + public void setServiceInterface(Class serviceInterface) { + Assert.notNull(serviceInterface, "'serviceInterface' must not be null"); + Assert.isTrue(serviceInterface.isInterface(), "'serviceInterface' must be an interface"); + this.serviceInterface = serviceInterface; + } + + /** + * Return the interface of the service to access. + */ + public Class getServiceInterface() { + return this.serviceInterface; + } + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/support/RemoteExporter.java b/spring-context/src/main/java/org/springframework/remoting/support/RemoteExporter.java new file mode 100644 index 0000000..2c590ff --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/support/RemoteExporter.java @@ -0,0 +1,186 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.support; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.framework.adapter.AdvisorAdapterRegistry; +import org.springframework.aop.framework.adapter.GlobalAdvisorAdapterRegistry; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Abstract base class for classes that export a remote service. + * Provides "service" and "serviceInterface" bean properties. + * + *

    Note that the service interface being used will show some signs of + * remotability, like the granularity of method calls that it offers. + * Furthermore, it has to have serializable arguments etc. + * + * @author Juergen Hoeller + * @since 26.12.2003 + */ +public abstract class RemoteExporter extends RemotingSupport { + + private Object service; + + private Class serviceInterface; + + private Boolean registerTraceInterceptor; + + private Object[] interceptors; + + + /** + * Set the service to export. + * Typically populated via a bean reference. + */ + public void setService(Object service) { + this.service = service; + } + + /** + * Return the service to export. + */ + public Object getService() { + return this.service; + } + + /** + * Set the interface of the service to export. + * The interface must be suitable for the particular service and remoting strategy. + */ + public void setServiceInterface(Class serviceInterface) { + Assert.notNull(serviceInterface, "'serviceInterface' must not be null"); + Assert.isTrue(serviceInterface.isInterface(), "'serviceInterface' must be an interface"); + this.serviceInterface = serviceInterface; + } + + /** + * Return the interface of the service to export. + */ + public Class getServiceInterface() { + return this.serviceInterface; + } + + /** + * Set whether to register a RemoteInvocationTraceInterceptor for exported + * services. Only applied when a subclass uses {@code getProxyForService} + * for creating the proxy to expose. + *

    Default is "true". RemoteInvocationTraceInterceptor's most important value + * is that it logs exception stacktraces on the server, before propagating an + * exception to the client. Note that RemoteInvocationTraceInterceptor will not + * be registered by default if the "interceptors" property has been specified. + * @see #setInterceptors + * @see #getProxyForService + * @see RemoteInvocationTraceInterceptor + */ + public void setRegisterTraceInterceptor(boolean registerTraceInterceptor) { + this.registerTraceInterceptor = registerTraceInterceptor; + } + + /** + * Set additional interceptors (or advisors) to be applied before the + * remote endpoint, e.g. a PerformanceMonitorInterceptor. + *

    You may specify any AOP Alliance MethodInterceptors or other + * Spring AOP Advices, as well as Spring AOP Advisors. + * @see #getProxyForService + * @see org.springframework.aop.interceptor.PerformanceMonitorInterceptor + */ + public void setInterceptors(Object[] interceptors) { + this.interceptors = interceptors; + } + + + /** + * Check whether the service reference has been set. + * @see #setService + */ + protected void checkService() throws IllegalArgumentException { + Assert.notNull(getService(), "Property 'service' is required"); + } + + /** + * Check whether a service reference has been set, + * and whether it matches the specified service. + * @see #setServiceInterface + * @see #setService + */ + protected void checkServiceInterface() throws IllegalArgumentException { + Class serviceInterface = getServiceInterface(); + Assert.notNull(serviceInterface, "Property 'serviceInterface' is required"); + + Object service = getService(); + if (service instanceof String) { + throw new IllegalArgumentException("Service [" + service + "] is a String " + + "rather than an actual service reference: Have you accidentally specified " + + "the service bean name as value instead of as reference?"); + } + if (!serviceInterface.isInstance(service)) { + throw new IllegalArgumentException("Service interface [" + serviceInterface.getName() + + "] needs to be implemented by service [" + service + "] of class [" + + service.getClass().getName() + "]"); + } + } + + /** + * Get a proxy for the given service object, implementing the specified + * service interface. + *

    Used to export a proxy that does not expose any internals but just + * a specific interface intended for remote access. Furthermore, a + * {@link RemoteInvocationTraceInterceptor} will be registered (by default). + * @return the proxy + * @see #setServiceInterface + * @see #setRegisterTraceInterceptor + * @see RemoteInvocationTraceInterceptor + */ + protected Object getProxyForService() { + checkService(); + checkServiceInterface(); + + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.addInterface(getServiceInterface()); + + if (this.registerTraceInterceptor != null ? this.registerTraceInterceptor : this.interceptors == null) { + proxyFactory.addAdvice(new RemoteInvocationTraceInterceptor(getExporterName())); + } + if (this.interceptors != null) { + AdvisorAdapterRegistry adapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); + for (Object interceptor : this.interceptors) { + proxyFactory.addAdvisor(adapterRegistry.wrap(interceptor)); + } + } + + proxyFactory.setTarget(getService()); + proxyFactory.setOpaque(true); + + return proxyFactory.getProxy(getBeanClassLoader()); + } + + /** + * Return a short name for this exporter. + * Used for tracing of remote invocations. + *

    Default is the unqualified class name (without package). + * Can be overridden in subclasses. + * @see #getProxyForService + * @see RemoteInvocationTraceInterceptor + * @see org.springframework.util.ClassUtils#getShortName + */ + protected String getExporterName() { + return ClassUtils.getShortName(getClass()); + } + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/support/RemoteInvocation.java b/spring-context/src/main/java/org/springframework/remoting/support/RemoteInvocation.java new file mode 100644 index 0000000..fec43c6 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/support/RemoteInvocation.java @@ -0,0 +1,225 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.support; + +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * Encapsulates a remote invocation, providing core method invocation properties + * in a serializable fashion. Used for RMI and HTTP-based serialization invokers. + * + *

    This is an SPI class, typically not used directly by applications. + * Can be subclassed for additional invocation parameters. + * + *

    Both {@link RemoteInvocation} and {@link RemoteInvocationResult} are designed + * for use with standard Java serialization as well as JavaBean-style serialization. + * + * @author Juergen Hoeller + * @since 25.02.2004 + * @see RemoteInvocationResult + * @see RemoteInvocationFactory + * @see RemoteInvocationExecutor + * @see org.springframework.remoting.rmi.RmiProxyFactoryBean + * @see org.springframework.remoting.rmi.RmiServiceExporter + * @see org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean + * @see org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter + */ +public class RemoteInvocation implements Serializable { + + /** use serialVersionUID from Spring 1.1 for interoperability. */ + private static final long serialVersionUID = 6876024250231820554L; + + + private String methodName; + + private Class[] parameterTypes; + + private Object[] arguments; + + private Map attributes; + + + /** + * Create a new RemoteInvocation for the given AOP method invocation. + * @param methodInvocation the AOP invocation to convert + */ + public RemoteInvocation(MethodInvocation methodInvocation) { + this.methodName = methodInvocation.getMethod().getName(); + this.parameterTypes = methodInvocation.getMethod().getParameterTypes(); + this.arguments = methodInvocation.getArguments(); + } + + /** + * Create a new RemoteInvocation for the given parameters. + * @param methodName the name of the method to invoke + * @param parameterTypes the parameter types of the method + * @param arguments the arguments for the invocation + */ + public RemoteInvocation(String methodName, Class[] parameterTypes, Object[] arguments) { + this.methodName = methodName; + this.parameterTypes = parameterTypes; + this.arguments = arguments; + } + + /** + * Create a new RemoteInvocation for JavaBean-style deserialization + * (e.g. with Jackson). + */ + public RemoteInvocation() { + } + + + /** + * Set the name of the target method. + *

    This setter is intended for JavaBean-style deserialization. + */ + public void setMethodName(String methodName) { + this.methodName = methodName; + } + + /** + * Return the name of the target method. + */ + public String getMethodName() { + return this.methodName; + } + + /** + * Set the parameter types of the target method. + *

    This setter is intended for JavaBean-style deserialization. + */ + public void setParameterTypes(Class[] parameterTypes) { + this.parameterTypes = parameterTypes; + } + + /** + * Return the parameter types of the target method. + */ + public Class[] getParameterTypes() { + return this.parameterTypes; + } + + /** + * Set the arguments for the target method call. + *

    This setter is intended for JavaBean-style deserialization. + */ + public void setArguments(Object[] arguments) { + this.arguments = arguments; + } + + /** + * Return the arguments for the target method call. + */ + public Object[] getArguments() { + return this.arguments; + } + + + /** + * Add an additional invocation attribute. Useful to add additional + * invocation context without having to subclass RemoteInvocation. + *

    Attribute keys have to be unique, and no overriding of existing + * attributes is allowed. + *

    The implementation avoids to unnecessarily create the attributes + * Map, to minimize serialization size. + * @param key the attribute key + * @param value the attribute value + * @throws IllegalStateException if the key is already bound + */ + public void addAttribute(String key, Serializable value) throws IllegalStateException { + if (this.attributes == null) { + this.attributes = new HashMap<>(); + } + if (this.attributes.containsKey(key)) { + throw new IllegalStateException("There is already an attribute with key '" + key + "' bound"); + } + this.attributes.put(key, value); + } + + /** + * Retrieve the attribute for the given key, if any. + *

    The implementation avoids to unnecessarily create the attributes + * Map, to minimize serialization size. + * @param key the attribute key + * @return the attribute value, or {@code null} if not defined + */ + @Nullable + public Serializable getAttribute(String key) { + if (this.attributes == null) { + return null; + } + return this.attributes.get(key); + } + + /** + * Set the attributes Map. Only here for special purposes: + * Preferably, use {@link #addAttribute} and {@link #getAttribute}. + * @param attributes the attributes Map + * @see #addAttribute + * @see #getAttribute + */ + public void setAttributes(@Nullable Map attributes) { + this.attributes = attributes; + } + + /** + * Return the attributes Map. Mainly here for debugging purposes: + * Preferably, use {@link #addAttribute} and {@link #getAttribute}. + * @return the attributes Map, or {@code null} if none created + * @see #addAttribute + * @see #getAttribute + */ + @Nullable + public Map getAttributes() { + return this.attributes; + } + + + /** + * Perform this invocation on the given target object. + * Typically called when a RemoteInvocation is received on the server. + * @param targetObject the target object to apply the invocation to + * @return the invocation result + * @throws NoSuchMethodException if the method name could not be resolved + * @throws IllegalAccessException if the method could not be accessed + * @throws InvocationTargetException if the method invocation resulted in an exception + * @see java.lang.reflect.Method#invoke + */ + public Object invoke(Object targetObject) + throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { + + Method method = targetObject.getClass().getMethod(this.methodName, this.parameterTypes); + return method.invoke(targetObject, this.arguments); + } + + + @Override + public String toString() { + return "RemoteInvocation: method name '" + this.methodName + "'; parameter types " + + ClassUtils.classNamesToString(this.parameterTypes); + } + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/support/RemoteInvocationBasedAccessor.java b/spring-context/src/main/java/org/springframework/remoting/support/RemoteInvocationBasedAccessor.java new file mode 100644 index 0000000..c7f5815 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/support/RemoteInvocationBasedAccessor.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.support; + +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.lang.Nullable; + +/** + * Abstract base class for remote service accessors that are based + * on serialization of {@link RemoteInvocation} objects. + * + * Provides a "remoteInvocationFactory" property, with a + * {@link DefaultRemoteInvocationFactory} as default strategy. + * + * @author Juergen Hoeller + * @since 1.1 + * @see #setRemoteInvocationFactory + * @see RemoteInvocation + * @see RemoteInvocationFactory + * @see DefaultRemoteInvocationFactory + */ +public abstract class RemoteInvocationBasedAccessor extends UrlBasedRemoteAccessor { + + private RemoteInvocationFactory remoteInvocationFactory = new DefaultRemoteInvocationFactory(); + + + /** + * Set the RemoteInvocationFactory to use for this accessor. + * Default is a {@link DefaultRemoteInvocationFactory}. + *

    A custom invocation factory can add further context information + * to the invocation, for example user credentials. + */ + public void setRemoteInvocationFactory(RemoteInvocationFactory remoteInvocationFactory) { + this.remoteInvocationFactory = + (remoteInvocationFactory != null ? remoteInvocationFactory : new DefaultRemoteInvocationFactory()); + } + + /** + * Return the RemoteInvocationFactory used by this accessor. + */ + public RemoteInvocationFactory getRemoteInvocationFactory() { + return this.remoteInvocationFactory; + } + + /** + * Create a new RemoteInvocation object for the given AOP method invocation. + *

    The default implementation delegates to the configured + * {@link #setRemoteInvocationFactory RemoteInvocationFactory}. + * This can be overridden in subclasses in order to provide custom RemoteInvocation + * subclasses, containing additional invocation parameters (e.g. user credentials). + *

    Note that it is preferable to build a custom RemoteInvocationFactory + * as a reusable strategy, instead of overriding this method. + * @param methodInvocation the current AOP method invocation + * @return the RemoteInvocation object + * @see RemoteInvocationFactory#createRemoteInvocation + */ + protected RemoteInvocation createRemoteInvocation(MethodInvocation methodInvocation) { + return getRemoteInvocationFactory().createRemoteInvocation(methodInvocation); + } + + /** + * Recreate the invocation result contained in the given RemoteInvocationResult object. + *

    The default implementation calls the default {@code recreate()} method. + * This can be overridden in subclass to provide custom recreation, potentially + * processing the returned result object. + * @param result the RemoteInvocationResult to recreate + * @return a return value if the invocation result is a successful return + * @throws Throwable if the invocation result is an exception + * @see RemoteInvocationResult#recreate() + */ + @Nullable + protected Object recreateRemoteInvocationResult(RemoteInvocationResult result) throws Throwable { + return result.recreate(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/support/RemoteInvocationBasedExporter.java b/spring-context/src/main/java/org/springframework/remoting/support/RemoteInvocationBasedExporter.java new file mode 100644 index 0000000..a25aae4 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/support/RemoteInvocationBasedExporter.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.support; + +import java.lang.reflect.InvocationTargetException; + +/** + * Abstract base class for remote service exporters that are based + * on deserialization of {@link RemoteInvocation} objects. + * + *

    Provides a "remoteInvocationExecutor" property, with a + * {@link DefaultRemoteInvocationExecutor} as default strategy. + * + * @author Juergen Hoeller + * @since 1.1 + * @see RemoteInvocationExecutor + * @see DefaultRemoteInvocationExecutor + */ +public abstract class RemoteInvocationBasedExporter extends RemoteExporter { + + private RemoteInvocationExecutor remoteInvocationExecutor = new DefaultRemoteInvocationExecutor(); + + + /** + * Set the RemoteInvocationExecutor to use for this exporter. + * Default is a DefaultRemoteInvocationExecutor. + *

    A custom invocation executor can extract further context information + * from the invocation, for example user credentials. + */ + public void setRemoteInvocationExecutor(RemoteInvocationExecutor remoteInvocationExecutor) { + this.remoteInvocationExecutor = remoteInvocationExecutor; + } + + /** + * Return the RemoteInvocationExecutor used by this exporter. + */ + public RemoteInvocationExecutor getRemoteInvocationExecutor() { + return this.remoteInvocationExecutor; + } + + + /** + * Apply the given remote invocation to the given target object. + * The default implementation delegates to the RemoteInvocationExecutor. + *

    Can be overridden in subclasses for custom invocation behavior, + * possibly for applying additional invocation parameters from a + * custom RemoteInvocation subclass. Note that it is preferable to use + * a custom RemoteInvocationExecutor which is a reusable strategy. + * @param invocation the remote invocation + * @param targetObject the target object to apply the invocation to + * @return the invocation result + * @throws NoSuchMethodException if the method name could not be resolved + * @throws IllegalAccessException if the method could not be accessed + * @throws InvocationTargetException if the method invocation resulted in an exception + * @see RemoteInvocationExecutor#invoke + */ + protected Object invoke(RemoteInvocation invocation, Object targetObject) + throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { + + if (logger.isTraceEnabled()) { + logger.trace("Executing " + invocation); + } + try { + return getRemoteInvocationExecutor().invoke(invocation, targetObject); + } + catch (NoSuchMethodException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Could not find target method for " + invocation, ex); + } + throw ex; + } + catch (IllegalAccessException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Could not access target method for " + invocation, ex); + } + throw ex; + } + catch (InvocationTargetException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Target method failed for " + invocation, ex.getTargetException()); + } + throw ex; + } + } + + /** + * Apply the given remote invocation to the given target object, wrapping + * the invocation result in a serializable RemoteInvocationResult object. + * The default implementation creates a plain RemoteInvocationResult. + *

    Can be overridden in subclasses for custom invocation behavior, + * for example to return additional context information. Note that this + * is not covered by the RemoteInvocationExecutor strategy! + * @param invocation the remote invocation + * @param targetObject the target object to apply the invocation to + * @return the invocation result + * @see #invoke + */ + protected RemoteInvocationResult invokeAndCreateResult(RemoteInvocation invocation, Object targetObject) { + try { + Object value = invoke(invocation, targetObject); + return new RemoteInvocationResult(value); + } + catch (Throwable ex) { + return new RemoteInvocationResult(ex); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/support/RemoteInvocationExecutor.java b/spring-context/src/main/java/org/springframework/remoting/support/RemoteInvocationExecutor.java new file mode 100644 index 0000000..02d456b --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/support/RemoteInvocationExecutor.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2007 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.support; + +import java.lang.reflect.InvocationTargetException; + +/** + * Strategy interface for executing a {@link RemoteInvocation} on a target object. + * + *

    Used by {@link org.springframework.remoting.rmi.RmiServiceExporter} (for RMI invokers) + * and by {@link org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter}. + * + * @author Juergen Hoeller + * @since 1.1 + * @see DefaultRemoteInvocationFactory + * @see org.springframework.remoting.rmi.RmiServiceExporter#setRemoteInvocationExecutor + * @see org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter#setRemoteInvocationExecutor + */ +public interface RemoteInvocationExecutor { + + /** + * Perform this invocation on the given target object. + * Typically called when a RemoteInvocation is received on the server. + * @param invocation the RemoteInvocation + * @param targetObject the target object to apply the invocation to + * @return the invocation result + * @throws NoSuchMethodException if the method name could not be resolved + * @throws IllegalAccessException if the method could not be accessed + * @throws InvocationTargetException if the method invocation resulted in an exception + * @see java.lang.reflect.Method#invoke + */ + Object invoke(RemoteInvocation invocation, Object targetObject) + throws NoSuchMethodException, IllegalAccessException, InvocationTargetException; + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/support/RemoteInvocationFactory.java b/spring-context/src/main/java/org/springframework/remoting/support/RemoteInvocationFactory.java new file mode 100644 index 0000000..5fba21f --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/support/RemoteInvocationFactory.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2007 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.support; + +import org.aopalliance.intercept.MethodInvocation; + +/** + * Strategy interface for creating a {@link RemoteInvocation} from an AOP Alliance + * {@link org.aopalliance.intercept.MethodInvocation}. + * + *

    Used by {@link org.springframework.remoting.rmi.RmiClientInterceptor} (for RMI invokers) + * and by {@link org.springframework.remoting.httpinvoker.HttpInvokerClientInterceptor}. + * + * @author Juergen Hoeller + * @since 1.1 + * @see DefaultRemoteInvocationFactory + * @see org.springframework.remoting.rmi.RmiClientInterceptor#setRemoteInvocationFactory + * @see org.springframework.remoting.httpinvoker.HttpInvokerClientInterceptor#setRemoteInvocationFactory + */ +public interface RemoteInvocationFactory { + + /** + * Create a serializable RemoteInvocation object from the given AOP + * MethodInvocation. + *

    Can be implemented to add custom context information to the + * remote invocation, for example user credentials. + * @param methodInvocation the original AOP MethodInvocation object + * @return the RemoteInvocation object + */ + RemoteInvocation createRemoteInvocation(MethodInvocation methodInvocation); + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/support/RemoteInvocationResult.java b/spring-context/src/main/java/org/springframework/remoting/support/RemoteInvocationResult.java new file mode 100644 index 0000000..76de6b8 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/support/RemoteInvocationResult.java @@ -0,0 +1,164 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.support; + +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; + +import org.springframework.lang.Nullable; + +/** + * Encapsulates a remote invocation result, holding a result value or an exception. + * Used for HTTP-based serialization invokers. + * + *

    This is an SPI class, typically not used directly by applications. + * Can be subclassed for additional invocation parameters. + * + *

    Both {@link RemoteInvocation} and {@link RemoteInvocationResult} are designed + * for use with standard Java serialization as well as JavaBean-style serialization. + * + * @author Juergen Hoeller + * @since 1.1 + * @see RemoteInvocation + */ +public class RemoteInvocationResult implements Serializable { + + /** Use serialVersionUID from Spring 1.1 for interoperability. */ + private static final long serialVersionUID = 2138555143707773549L; + + + @Nullable + private Object value; + + @Nullable + private Throwable exception; + + + /** + * Create a new RemoteInvocationResult for the given result value. + * @param value the result value returned by a successful invocation + * of the target method + */ + public RemoteInvocationResult(@Nullable Object value) { + this.value = value; + } + + /** + * Create a new RemoteInvocationResult for the given exception. + * @param exception the exception thrown by an unsuccessful invocation + * of the target method + */ + public RemoteInvocationResult(@Nullable Throwable exception) { + this.exception = exception; + } + + /** + * Create a new RemoteInvocationResult for JavaBean-style deserialization + * (e.g. with Jackson). + * @see #setValue + * @see #setException + */ + public RemoteInvocationResult() { + } + + + /** + * Set the result value returned by a successful invocation of the + * target method, if any. + *

    This setter is intended for JavaBean-style deserialization. + * Use {@link #RemoteInvocationResult(Object)} otherwise. + * @see #RemoteInvocationResult() + */ + public void setValue(@Nullable Object value) { + this.value = value; + } + + /** + * Return the result value returned by a successful invocation + * of the target method, if any. + * @see #hasException + */ + @Nullable + public Object getValue() { + return this.value; + } + + /** + * Set the exception thrown by an unsuccessful invocation of the + * target method, if any. + *

    This setter is intended for JavaBean-style deserialization. + * Use {@link #RemoteInvocationResult(Throwable)} otherwise. + * @see #RemoteInvocationResult() + */ + public void setException(@Nullable Throwable exception) { + this.exception = exception; + } + + /** + * Return the exception thrown by an unsuccessful invocation + * of the target method, if any. + * @see #hasException + */ + @Nullable + public Throwable getException() { + return this.exception; + } + + /** + * Return whether this invocation result holds an exception. + * If this returns {@code false}, the result value applies + * (even if it is {@code null}). + * @see #getValue + * @see #getException + */ + public boolean hasException() { + return (this.exception != null); + } + + /** + * Return whether this invocation result holds an InvocationTargetException, + * thrown by an invocation of the target method itself. + * @see #hasException() + */ + public boolean hasInvocationTargetException() { + return (this.exception instanceof InvocationTargetException); + } + + + /** + * Recreate the invocation result, either returning the result value + * in case of a successful invocation of the target method, or + * rethrowing the exception thrown by the target method. + * @return the result value, if any + * @throws Throwable the exception, if any + */ + @Nullable + public Object recreate() throws Throwable { + if (this.exception != null) { + Throwable exToThrow = this.exception; + if (this.exception instanceof InvocationTargetException) { + exToThrow = ((InvocationTargetException) this.exception).getTargetException(); + } + RemoteInvocationUtils.fillInClientStackTraceIfPossible(exToThrow); + throw exToThrow; + } + else { + return this.value; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/support/RemoteInvocationTraceInterceptor.java b/spring-context/src/main/java/org/springframework/remoting/support/RemoteInvocationTraceInterceptor.java new file mode 100644 index 0000000..2d5dc8e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/support/RemoteInvocationTraceInterceptor.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.support; + +import java.lang.reflect.Method; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * AOP Alliance MethodInterceptor for tracing remote invocations. + * Automatically applied by RemoteExporter and its subclasses. + * + *

    Logs an incoming remote call as well as the finished processing of a remote call + * at DEBUG level. If the processing of a remote call results in a checked exception, + * the exception will get logged at INFO level; if it results in an unchecked + * exception (or error), the exception will get logged at WARN level. + * + *

    The logging of exceptions is particularly useful to save the stacktrace + * information on the server-side rather than just propagating the exception + * to the client (who might or might not log it properly). + * + * @author Juergen Hoeller + * @since 1.2 + * @see RemoteExporter#setRegisterTraceInterceptor + * @see RemoteExporter#getProxyForService + */ +public class RemoteInvocationTraceInterceptor implements MethodInterceptor { + + protected static final Log logger = LogFactory.getLog(RemoteInvocationTraceInterceptor.class); + + private final String exporterNameClause; + + + /** + * Create a new RemoteInvocationTraceInterceptor. + */ + public RemoteInvocationTraceInterceptor() { + this.exporterNameClause = ""; + } + + /** + * Create a new RemoteInvocationTraceInterceptor. + * @param exporterName the name of the remote exporter + * (to be used as context information in log messages) + */ + public RemoteInvocationTraceInterceptor(String exporterName) { + this.exporterNameClause = exporterName + " "; + } + + + @Override + @Nullable + public Object invoke(MethodInvocation invocation) throws Throwable { + Method method = invocation.getMethod(); + if (logger.isDebugEnabled()) { + logger.debug("Incoming " + this.exporterNameClause + "remote call: " + + ClassUtils.getQualifiedMethodName(method)); + } + try { + Object retVal = invocation.proceed(); + if (logger.isDebugEnabled()) { + logger.debug("Finished processing of " + this.exporterNameClause + "remote call: " + + ClassUtils.getQualifiedMethodName(method)); + } + return retVal; + } + catch (Throwable ex) { + if (ex instanceof RuntimeException || ex instanceof Error) { + if (logger.isWarnEnabled()) { + logger.warn("Processing of " + this.exporterNameClause + "remote call resulted in fatal exception: " + + ClassUtils.getQualifiedMethodName(method), ex); + } + } + else { + if (logger.isInfoEnabled()) { + logger.info("Processing of " + this.exporterNameClause + "remote call resulted in exception: " + + ClassUtils.getQualifiedMethodName(method), ex); + } + } + throw ex; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/support/RemoteInvocationUtils.java b/spring-context/src/main/java/org/springframework/remoting/support/RemoteInvocationUtils.java new file mode 100644 index 0000000..612b73c --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/support/RemoteInvocationUtils.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.support; + +import java.util.HashSet; +import java.util.Set; + +/** + * General utilities for handling remote invocations. + * + *

    Mainly intended for use within the remoting framework. + * + * @author Juergen Hoeller + * @since 2.0 + */ +public abstract class RemoteInvocationUtils { + + /** + * Fill the current client-side stack trace into the given exception. + *

    The given exception is typically thrown on the server and serialized + * as-is, with the client wanting it to contain the client-side portion + * of the stack trace as well. What we can do here is to update the + * {@code StackTraceElement} array with the current client-side stack + * trace, provided that we run on JDK 1.4+. + * @param ex the exception to update + * @see Throwable#getStackTrace() + * @see Throwable#setStackTrace(StackTraceElement[]) + */ + public static void fillInClientStackTraceIfPossible(Throwable ex) { + if (ex != null) { + StackTraceElement[] clientStack = new Throwable().getStackTrace(); + Set visitedExceptions = new HashSet<>(); + Throwable exToUpdate = ex; + while (exToUpdate != null && !visitedExceptions.contains(exToUpdate)) { + StackTraceElement[] serverStack = exToUpdate.getStackTrace(); + StackTraceElement[] combinedStack = new StackTraceElement[serverStack.length + clientStack.length]; + System.arraycopy(serverStack, 0, combinedStack, 0, serverStack.length); + System.arraycopy(clientStack, 0, combinedStack, serverStack.length, clientStack.length); + exToUpdate.setStackTrace(combinedStack); + visitedExceptions.add(exToUpdate); + exToUpdate = exToUpdate.getCause(); + } + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/support/RemotingSupport.java b/spring-context/src/main/java/org/springframework/remoting/support/RemotingSupport.java new file mode 100644 index 0000000..51acfdf --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/support/RemotingSupport.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.support; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * Generic support base class for remote accessor and exporters, + * providing common bean ClassLoader handling. + * + * @author Juergen Hoeller + * @since 2.5.2 + */ +public abstract class RemotingSupport implements BeanClassLoaderAware { + + /** Logger available to subclasses. */ + protected final Log logger = LogFactory.getLog(getClass()); + + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + /** + * Return the ClassLoader that this accessor operates in, + * to be used for deserializing and for generating proxies. + */ + protected ClassLoader getBeanClassLoader() { + return this.beanClassLoader; + } + + + /** + * Override the thread context ClassLoader with the environment's bean ClassLoader + * if necessary, i.e. if the bean ClassLoader is not equivalent to the thread + * context ClassLoader already. + * @return the original thread context ClassLoader, or {@code null} if not overridden + */ + @Nullable + protected ClassLoader overrideThreadContextClassLoader() { + return ClassUtils.overrideThreadContextClassLoader(getBeanClassLoader()); + } + + /** + * Reset the original thread context ClassLoader if necessary. + * @param original the original thread context ClassLoader, + * or {@code null} if not overridden (and hence nothing to reset) + */ + protected void resetThreadContextClassLoader(@Nullable ClassLoader original) { + if (original != null) { + Thread.currentThread().setContextClassLoader(original); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/support/SimpleHttpServerFactoryBean.java b/spring-context/src/main/java/org/springframework/remoting/support/SimpleHttpServerFactoryBean.java new file mode 100644 index 0000000..f72fb9f --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/support/SimpleHttpServerFactoryBean.java @@ -0,0 +1,194 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.support; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; + +import com.sun.net.httpserver.Authenticator; +import com.sun.net.httpserver.Filter; +import com.sun.net.httpserver.HttpContext; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; + +/** + * {@link org.springframework.beans.factory.FactoryBean} that creates a simple + * HTTP server, based on the HTTP server that is included in Sun's JRE 1.6. + * Starts the HTTP server on initialization and stops it on destruction. + * Exposes the resulting {@link com.sun.net.httpserver.HttpServer} object. + * + *

    Allows for registering {@link com.sun.net.httpserver.HttpHandler HttpHandlers} + * for specific {@link #setContexts context paths}. Alternatively, + * register such context-specific handlers programmatically on the + * {@link com.sun.net.httpserver.HttpServer} itself. + * + * @author Juergen Hoeller + * @author Arjen Poutsma + * @since 2.5.1 + * @see #setPort + * @see #setContexts + * @deprecated as of Spring Framework 5.1, in favor of embedded Tomcat/Jetty/Undertow + */ +@Deprecated +@org.springframework.lang.UsesSunHttpServer +public class SimpleHttpServerFactoryBean implements FactoryBean, InitializingBean, DisposableBean { + + protected final Log logger = LogFactory.getLog(getClass()); + + private int port = 8080; + + private String hostname; + + private int backlog = -1; + + private int shutdownDelay = 0; + + private Executor executor; + + private Map contexts; + + private List filters; + + private Authenticator authenticator; + + private HttpServer server; + + + /** + * Specify the HTTP server's port. Default is 8080. + */ + public void setPort(int port) { + this.port = port; + } + + /** + * Specify the HTTP server's hostname to bind to. Default is localhost; + * can be overridden with a specific network address to bind to. + */ + public void setHostname(String hostname) { + this.hostname = hostname; + } + + /** + * Specify the HTTP server's TCP backlog. Default is -1, + * indicating the system's default value. + */ + public void setBacklog(int backlog) { + this.backlog = backlog; + } + + /** + * Specify the number of seconds to wait until HTTP exchanges have + * completed when shutting down the HTTP server. Default is 0. + */ + public void setShutdownDelay(int shutdownDelay) { + this.shutdownDelay = shutdownDelay; + } + + /** + * Set the JDK concurrent executor to use for dispatching incoming requests. + * @see com.sun.net.httpserver.HttpServer#setExecutor + */ + public void setExecutor(Executor executor) { + this.executor = executor; + } + + /** + * Register {@link com.sun.net.httpserver.HttpHandler HttpHandlers} + * for specific context paths. + * @param contexts a Map with context paths as keys and HttpHandler + * objects as values + * @see org.springframework.remoting.httpinvoker.SimpleHttpInvokerServiceExporter + * @see org.springframework.remoting.caucho.SimpleHessianServiceExporter + */ + public void setContexts(Map contexts) { + this.contexts = contexts; + } + + /** + * Register common {@link com.sun.net.httpserver.Filter Filters} to be + * applied to all locally registered {@link #setContexts contexts}. + */ + public void setFilters(List filters) { + this.filters = filters; + } + + /** + * Register a common {@link com.sun.net.httpserver.Authenticator} to be + * applied to all locally registered {@link #setContexts contexts}. + */ + public void setAuthenticator(Authenticator authenticator) { + this.authenticator = authenticator; + } + + + @Override + public void afterPropertiesSet() throws IOException { + InetSocketAddress address = (this.hostname != null ? + new InetSocketAddress(this.hostname, this.port) : new InetSocketAddress(this.port)); + this.server = HttpServer.create(address, this.backlog); + if (this.executor != null) { + this.server.setExecutor(this.executor); + } + if (this.contexts != null) { + this.contexts.forEach((key, context) -> { + HttpContext httpContext = this.server.createContext(key, context); + if (this.filters != null) { + httpContext.getFilters().addAll(this.filters); + } + if (this.authenticator != null) { + httpContext.setAuthenticator(this.authenticator); + } + }); + } + if (logger.isInfoEnabled()) { + logger.info("Starting HttpServer at address " + address); + } + this.server.start(); + } + + @Override + public HttpServer getObject() { + return this.server; + } + + @Override + public Class getObjectType() { + return (this.server != null ? this.server.getClass() : HttpServer.class); + } + + @Override + public boolean isSingleton() { + return true; + } + + @Override + public void destroy() { + logger.info("Stopping HttpServer"); + this.server.stop(this.shutdownDelay); + } + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/support/UrlBasedRemoteAccessor.java b/spring-context/src/main/java/org/springframework/remoting/support/UrlBasedRemoteAccessor.java new file mode 100644 index 0000000..93c65ac --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/support/UrlBasedRemoteAccessor.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.support; + +import org.springframework.beans.factory.InitializingBean; + +/** + * Abstract base class for classes that access remote services via URLs. + * Provides a "serviceUrl" bean property, which is considered as required. + * + * @author Juergen Hoeller + * @since 15.12.2003 + */ +public abstract class UrlBasedRemoteAccessor extends RemoteAccessor implements InitializingBean { + + private String serviceUrl; + + + /** + * Set the URL of this remote accessor's target service. + * The URL must be compatible with the rules of the particular remoting provider. + */ + public void setServiceUrl(String serviceUrl) { + this.serviceUrl = serviceUrl; + } + + /** + * Return the URL of this remote accessor's target service. + */ + public String getServiceUrl() { + return this.serviceUrl; + } + + + @Override + public void afterPropertiesSet() { + if (getServiceUrl() == null) { + throw new IllegalArgumentException("Property 'serviceUrl' is required"); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/remoting/support/package-info.java b/spring-context/src/main/java/org/springframework/remoting/support/package-info.java new file mode 100644 index 0000000..3163061 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/remoting/support/package-info.java @@ -0,0 +1,5 @@ +/** + * Generic support classes for remoting implementations. + * Provides abstract base classes for remote proxy factories. + */ +package org.springframework.remoting.support; diff --git a/spring-context/src/main/java/org/springframework/scheduling/SchedulingAwareRunnable.java b/spring-context/src/main/java/org/springframework/scheduling/SchedulingAwareRunnable.java new file mode 100644 index 0000000..d6de8c1 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/SchedulingAwareRunnable.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling; + +/** + * Extension of the Runnable interface, adding special callbacks + * for long-running operations. + * + *

    This interface closely corresponds to the CommonJ Work interface, + * but is kept separate to avoid a required CommonJ dependency. + * + *

    Scheduling-capable TaskExecutors are encouraged to check a submitted + * Runnable, detecting whether this interface is implemented and reacting + * as appropriately as they are able to. + * + * @author Juergen Hoeller + * @since 2.0 + * @see commonj.work.Work + * @see org.springframework.core.task.TaskExecutor + * @see SchedulingTaskExecutor + * @see org.springframework.scheduling.commonj.WorkManagerTaskExecutor + */ +public interface SchedulingAwareRunnable extends Runnable { + + /** + * Return whether the Runnable's operation is long-lived + * ({@code true}) versus short-lived ({@code false}). + *

    In the former case, the task will not allocate a thread from the thread + * pool (if any) but rather be considered as long-running background thread. + *

    This should be considered a hint. Of course TaskExecutor implementations + * are free to ignore this flag and the SchedulingAwareRunnable interface overall. + */ + boolean isLongLived(); + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/SchedulingException.java b/spring-context/src/main/java/org/springframework/scheduling/SchedulingException.java new file mode 100644 index 0000000..001ca5d --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/SchedulingException.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling; + +import org.springframework.core.NestedRuntimeException; + +/** + * General exception to be thrown on scheduling failures, + * such as the scheduler already having shut down. + * Unchecked since scheduling failures are usually fatal. + * + * @author Juergen Hoeller + * @since 2.0 + */ +@SuppressWarnings("serial") +public class SchedulingException extends NestedRuntimeException { + + /** + * Constructor for SchedulingException. + * @param msg the detail message + */ + public SchedulingException(String msg) { + super(msg); + } + + /** + * Constructor for SchedulingException. + * @param msg the detail message + * @param cause the root cause (usually from using a underlying + * scheduling API such as Quartz) + */ + public SchedulingException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/SchedulingTaskExecutor.java b/spring-context/src/main/java/org/springframework/scheduling/SchedulingTaskExecutor.java new file mode 100644 index 0000000..2f1dd00 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/SchedulingTaskExecutor.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling; + +import org.springframework.core.task.AsyncTaskExecutor; + +/** + * A {@link org.springframework.core.task.TaskExecutor} extension exposing + * scheduling characteristics that are relevant to potential task submitters. + * + *

    Scheduling clients are encouraged to submit + * {@link Runnable Runnables} that match the exposed preferences + * of the {@code TaskExecutor} implementation in use. + * + *

    Note: {@link SchedulingTaskExecutor} implementations are encouraged to also + * implement the {@link org.springframework.core.task.AsyncListenableTaskExecutor} + * interface. This is not required due to the dependency on Spring 4.0's new + * {@link org.springframework.util.concurrent.ListenableFuture} interface, + * which would make it impossible for third-party executor implementations + * to remain compatible with both Spring 4.0 and Spring 3.x. + * + * @author Juergen Hoeller + * @since 2.0 + * @see SchedulingAwareRunnable + * @see org.springframework.core.task.TaskExecutor + * @see org.springframework.scheduling.commonj.WorkManagerTaskExecutor + */ +public interface SchedulingTaskExecutor extends AsyncTaskExecutor { + + /** + * Does this {@code TaskExecutor} prefer short-lived tasks over long-lived tasks? + *

    A {@code SchedulingTaskExecutor} implementation can indicate whether it + * prefers submitted tasks to perform as little work as they can within a single + * task execution. For example, submitted tasks might break a repeated loop into + * individual subtasks which submit a follow-up task afterwards (if feasible). + *

    This should be considered a hint. Of course {@code TaskExecutor} clients + * are free to ignore this flag and hence the {@code SchedulingTaskExecutor} + * interface overall. However, thread pools will usually indicated a preference + * for short-lived tasks, allowing for more fine-grained scheduling. + * @return {@code true} if this executor prefers short-lived tasks (the default), + * {@code false} otherwise (for treatment like a regular {@code TaskExecutor}) + */ + default boolean prefersShortLivedTasks() { + return true; + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/TaskScheduler.java b/spring-context/src/main/java/org/springframework/scheduling/TaskScheduler.java new file mode 100644 index 0000000..326923d --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/TaskScheduler.java @@ -0,0 +1,243 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.Date; +import java.util.concurrent.ScheduledFuture; + +import org.springframework.lang.Nullable; + +/** + * Task scheduler interface that abstracts the scheduling of + * {@link Runnable Runnables} based on different kinds of triggers. + * + *

    This interface is separate from {@link SchedulingTaskExecutor} since it + * usually represents for a different kind of backend, i.e. a thread pool with + * different characteristics and capabilities. Implementations may implement + * both interfaces if they can handle both kinds of execution characteristics. + * + *

    The 'default' implementation is + * {@link org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler}, + * wrapping a native {@link java.util.concurrent.ScheduledExecutorService} + * and adding extended trigger capabilities. + * + *

    This interface is roughly equivalent to a JSR-236 + * {@code ManagedScheduledExecutorService} as supported in Java EE 7 + * environments but aligned with Spring's {@code TaskExecutor} model. + * + * @author Juergen Hoeller + * @since 3.0 + * @see org.springframework.core.task.TaskExecutor + * @see java.util.concurrent.ScheduledExecutorService + * @see org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler + */ +public interface TaskScheduler { + + /** + * Return the clock to use for scheduling purposes. + * @since 5.3 + * @see Clock#systemDefaultZone() + */ + default Clock getClock() { + return Clock.systemDefaultZone(); + } + + /** + * Schedule the given {@link Runnable}, invoking it whenever the trigger + * indicates a next execution time. + *

    Execution will end once the scheduler shuts down or the returned + * {@link ScheduledFuture} gets cancelled. + * @param task the Runnable to execute whenever the trigger fires + * @param trigger an implementation of the {@link Trigger} interface, + * e.g. a {@link org.springframework.scheduling.support.CronTrigger} object + * wrapping a cron expression + * @return a {@link ScheduledFuture} representing pending completion of the task, + * or {@code null} if the given Trigger object never fires (i.e. returns + * {@code null} from {@link Trigger#nextExecutionTime}) + * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted + * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) + * @see org.springframework.scheduling.support.CronTrigger + */ + @Nullable + ScheduledFuture schedule(Runnable task, Trigger trigger); + + /** + * Schedule the given {@link Runnable}, invoking it at the specified execution time. + *

    Execution will end once the scheduler shuts down or the returned + * {@link ScheduledFuture} gets cancelled. + * @param task the Runnable to execute whenever the trigger fires + * @param startTime the desired execution time for the task + * (if this is in the past, the task will be executed immediately, i.e. as soon as possible) + * @return a {@link ScheduledFuture} representing pending completion of the task + * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted + * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) + * @since 5.0 + * @see #schedule(Runnable, Date) + */ + default ScheduledFuture schedule(Runnable task, Instant startTime) { + return schedule(task, Date.from(startTime)); + } + + /** + * Schedule the given {@link Runnable}, invoking it at the specified execution time. + *

    Execution will end once the scheduler shuts down or the returned + * {@link ScheduledFuture} gets cancelled. + * @param task the Runnable to execute whenever the trigger fires + * @param startTime the desired execution time for the task + * (if this is in the past, the task will be executed immediately, i.e. as soon as possible) + * @return a {@link ScheduledFuture} representing pending completion of the task + * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted + * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) + */ + ScheduledFuture schedule(Runnable task, Date startTime); + + /** + * Schedule the given {@link Runnable}, invoking it at the specified execution time + * and subsequently with the given period. + *

    Execution will end once the scheduler shuts down or the returned + * {@link ScheduledFuture} gets cancelled. + * @param task the Runnable to execute whenever the trigger fires + * @param startTime the desired first execution time for the task + * (if this is in the past, the task will be executed immediately, i.e. as soon as possible) + * @param period the interval between successive executions of the task + * @return a {@link ScheduledFuture} representing pending completion of the task + * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted + * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) + * @since 5.0 + * @see #scheduleAtFixedRate(Runnable, Date, long) + */ + default ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period) { + return scheduleAtFixedRate(task, Date.from(startTime), period.toMillis()); + } + + /** + * Schedule the given {@link Runnable}, invoking it at the specified execution time + * and subsequently with the given period. + *

    Execution will end once the scheduler shuts down or the returned + * {@link ScheduledFuture} gets cancelled. + * @param task the Runnable to execute whenever the trigger fires + * @param startTime the desired first execution time for the task + * (if this is in the past, the task will be executed immediately, i.e. as soon as possible) + * @param period the interval between successive executions of the task (in milliseconds) + * @return a {@link ScheduledFuture} representing pending completion of the task + * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted + * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) + */ + ScheduledFuture scheduleAtFixedRate(Runnable task, Date startTime, long period); + + /** + * Schedule the given {@link Runnable}, starting as soon as possible and + * invoking it with the given period. + *

    Execution will end once the scheduler shuts down or the returned + * {@link ScheduledFuture} gets cancelled. + * @param task the Runnable to execute whenever the trigger fires + * @param period the interval between successive executions of the task + * @return a {@link ScheduledFuture} representing pending completion of the task + * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted + * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) + * @since 5.0 + * @see #scheduleAtFixedRate(Runnable, long) + */ + default ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period) { + return scheduleAtFixedRate(task, period.toMillis()); + } + + /** + * Schedule the given {@link Runnable}, starting as soon as possible and + * invoking it with the given period. + *

    Execution will end once the scheduler shuts down or the returned + * {@link ScheduledFuture} gets cancelled. + * @param task the Runnable to execute whenever the trigger fires + * @param period the interval between successive executions of the task (in milliseconds) + * @return a {@link ScheduledFuture} representing pending completion of the task + * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted + * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) + */ + ScheduledFuture scheduleAtFixedRate(Runnable task, long period); + + /** + * Schedule the given {@link Runnable}, invoking it at the specified execution time + * and subsequently with the given delay between the completion of one execution + * and the start of the next. + *

    Execution will end once the scheduler shuts down or the returned + * {@link ScheduledFuture} gets cancelled. + * @param task the Runnable to execute whenever the trigger fires + * @param startTime the desired first execution time for the task + * (if this is in the past, the task will be executed immediately, i.e. as soon as possible) + * @param delay the delay between the completion of one execution and the start of the next + * @return a {@link ScheduledFuture} representing pending completion of the task + * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted + * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) + * @since 5.0 + * @see #scheduleWithFixedDelay(Runnable, Date, long) + */ + default ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay) { + return scheduleWithFixedDelay(task, Date.from(startTime), delay.toMillis()); + } + + /** + * Schedule the given {@link Runnable}, invoking it at the specified execution time + * and subsequently with the given delay between the completion of one execution + * and the start of the next. + *

    Execution will end once the scheduler shuts down or the returned + * {@link ScheduledFuture} gets cancelled. + * @param task the Runnable to execute whenever the trigger fires + * @param startTime the desired first execution time for the task + * (if this is in the past, the task will be executed immediately, i.e. as soon as possible) + * @param delay the delay between the completion of one execution and the start of the next + * (in milliseconds) + * @return a {@link ScheduledFuture} representing pending completion of the task + * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted + * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) + */ + ScheduledFuture scheduleWithFixedDelay(Runnable task, Date startTime, long delay); + + /** + * Schedule the given {@link Runnable}, starting as soon as possible and invoking it with + * the given delay between the completion of one execution and the start of the next. + *

    Execution will end once the scheduler shuts down or the returned + * {@link ScheduledFuture} gets cancelled. + * @param task the Runnable to execute whenever the trigger fires + * @param delay the delay between the completion of one execution and the start of the next + * @return a {@link ScheduledFuture} representing pending completion of the task + * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted + * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) + * @since 5.0 + * @see #scheduleWithFixedDelay(Runnable, long) + */ + default ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay) { + return scheduleWithFixedDelay(task, delay.toMillis()); + } + + /** + * Schedule the given {@link Runnable}, starting as soon as possible and invoking it with + * the given delay between the completion of one execution and the start of the next. + *

    Execution will end once the scheduler shuts down or the returned + * {@link ScheduledFuture} gets cancelled. + * @param task the Runnable to execute whenever the trigger fires + * @param delay the delay between the completion of one execution and the start of the next + * (in milliseconds) + * @return a {@link ScheduledFuture} representing pending completion of the task + * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted + * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) + */ + ScheduledFuture scheduleWithFixedDelay(Runnable task, long delay); + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/Trigger.java b/spring-context/src/main/java/org/springframework/scheduling/Trigger.java new file mode 100644 index 0000000..1653451 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/Trigger.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling; + +import java.util.Date; + +import org.springframework.lang.Nullable; + +/** + * Common interface for trigger objects that determine the next execution time + * of a task that they get associated with. + * + * @author Juergen Hoeller + * @since 3.0 + * @see TaskScheduler#schedule(Runnable, Trigger) + * @see org.springframework.scheduling.support.CronTrigger + */ +public interface Trigger { + + /** + * Determine the next execution time according to the given trigger context. + * @param triggerContext context object encapsulating last execution times + * and last completion time + * @return the next execution time as defined by the trigger, + * or {@code null} if the trigger won't fire anymore + */ + @Nullable + Date nextExecutionTime(TriggerContext triggerContext); + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/TriggerContext.java b/spring-context/src/main/java/org/springframework/scheduling/TriggerContext.java new file mode 100644 index 0000000..f50421e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/TriggerContext.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling; + +import java.time.Clock; +import java.util.Date; + +import org.springframework.lang.Nullable; + +/** + * Context object encapsulating last execution times and last completion time + * of a given task. + * + * @author Juergen Hoeller + * @since 3.0 + */ +public interface TriggerContext { + + /** + * Return the clock to use for trigger calculation. + * @since 5.3 + * @see TaskScheduler#getClock() + * @see Clock#systemDefaultZone() + */ + default Clock getClock() { + return Clock.systemDefaultZone(); + } + + /** + * Return the last scheduled execution time of the task, + * or {@code null} if not scheduled before. + */ + @Nullable + Date lastScheduledExecutionTime(); + + /** + * Return the last actual execution time of the task, + * or {@code null} if not scheduled before. + */ + @Nullable + Date lastActualExecutionTime(); + + /** + * Return the last completion time of the task, + * or {@code null} if not scheduled before. + */ + @Nullable + Date lastCompletionTime(); + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AbstractAsyncConfiguration.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AbstractAsyncConfiguration.java new file mode 100644 index 0000000..60955d5 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AbstractAsyncConfiguration.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.annotation; + +import java.util.Collection; +import java.util.concurrent.Executor; +import java.util.function.Supplier; + +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportAware; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; + +/** + * Abstract base {@code Configuration} class providing common structure for enabling + * Spring's asynchronous method execution capability. + * + * @author Chris Beams + * @author Juergen Hoeller + * @author Stephane Nicoll + * @since 3.1 + * @see EnableAsync + */ +@Configuration(proxyBeanMethods = false) +public abstract class AbstractAsyncConfiguration implements ImportAware { + + @Nullable + protected AnnotationAttributes enableAsync; + + @Nullable + protected Supplier executor; + + @Nullable + protected Supplier exceptionHandler; + + + @Override + public void setImportMetadata(AnnotationMetadata importMetadata) { + this.enableAsync = AnnotationAttributes.fromMap( + importMetadata.getAnnotationAttributes(EnableAsync.class.getName(), false)); + if (this.enableAsync == null) { + throw new IllegalArgumentException( + "@EnableAsync is not present on importing class " + importMetadata.getClassName()); + } + } + + /** + * Collect any {@link AsyncConfigurer} beans through autowiring. + */ + @Autowired(required = false) + void setConfigurers(Collection configurers) { + if (CollectionUtils.isEmpty(configurers)) { + return; + } + if (configurers.size() > 1) { + throw new IllegalStateException("Only one AsyncConfigurer may exist"); + } + AsyncConfigurer configurer = configurers.iterator().next(); + this.executor = configurer::getAsyncExecutor; + this.exceptionHandler = configurer::getAsyncUncaughtExceptionHandler; + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptor.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptor.java new file mode 100644 index 0000000..cd906d3 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptor.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.annotation; + +import java.lang.reflect.Method; +import java.util.concurrent.Executor; + +import org.springframework.aop.interceptor.AsyncExecutionInterceptor; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.lang.Nullable; + +/** + * Specialization of {@link AsyncExecutionInterceptor} that delegates method execution to + * an {@code Executor} based on the {@link Async} annotation. Specifically designed to + * support use of {@link Async#value()} executor qualification mechanism introduced in + * Spring 3.1.2. Supports detecting qualifier metadata via {@code @Async} at the method or + * declaring class level. See {@link #getExecutorQualifier(Method)} for details. + * + * @author Chris Beams + * @author Stephane Nicoll + * @since 3.1.2 + * @see org.springframework.scheduling.annotation.Async + * @see org.springframework.scheduling.annotation.AsyncAnnotationAdvisor + */ +public class AnnotationAsyncExecutionInterceptor extends AsyncExecutionInterceptor { + + /** + * Create a new {@code AnnotationAsyncExecutionInterceptor} with the given executor + * and a simple {@link AsyncUncaughtExceptionHandler}. + * @param defaultExecutor the executor to be used by default if no more specific + * executor has been qualified at the method level using {@link Async#value()}; + * as of 4.2.6, a local executor for this interceptor will be built otherwise + */ + public AnnotationAsyncExecutionInterceptor(@Nullable Executor defaultExecutor) { + super(defaultExecutor); + } + + /** + * Create a new {@code AnnotationAsyncExecutionInterceptor} with the given executor. + * @param defaultExecutor the executor to be used by default if no more specific + * executor has been qualified at the method level using {@link Async#value()}; + * as of 4.2.6, a local executor for this interceptor will be built otherwise + * @param exceptionHandler the {@link AsyncUncaughtExceptionHandler} to use to + * handle exceptions thrown by asynchronous method executions with {@code void} + * return type + */ + public AnnotationAsyncExecutionInterceptor(@Nullable Executor defaultExecutor, AsyncUncaughtExceptionHandler exceptionHandler) { + super(defaultExecutor, exceptionHandler); + } + + + /** + * Return the qualifier or bean name of the executor to be used when executing the + * given method, specified via {@link Async#value} at the method or declaring + * class level. If {@code @Async} is specified at both the method and class level, the + * method's {@code #value} takes precedence (even if empty string, indicating that + * the default executor should be used preferentially). + * @param method the method to inspect for executor qualifier metadata + * @return the qualifier if specified, otherwise empty string indicating that the + * {@linkplain #setExecutor(Executor) default executor} should be used + * @see #determineAsyncExecutor(Method) + */ + @Override + @Nullable + protected String getExecutorQualifier(Method method) { + // Maintainer's note: changes made here should also be made in + // AnnotationAsyncExecutionAspect#getExecutorQualifier + Async async = AnnotatedElementUtils.findMergedAnnotation(method, Async.class); + if (async == null) { + async = AnnotatedElementUtils.findMergedAnnotation(method.getDeclaringClass(), Async.class); + } + return (async != null ? async.value() : null); + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/Async.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/Async.java new file mode 100644 index 0000000..496afa9 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/Async.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation that marks a method as a candidate for asynchronous execution. + * Can also be used at the type level, in which case all of the type's methods are + * considered as asynchronous. Note, however, that {@code @Async} is not supported + * on methods declared within a + * {@link org.springframework.context.annotation.Configuration @Configuration} class. + * + *

    In terms of target method signatures, any parameter types are supported. + * However, the return type is constrained to either {@code void} or + * {@link java.util.concurrent.Future}. In the latter case, you may declare the + * more specific {@link org.springframework.util.concurrent.ListenableFuture} or + * {@link java.util.concurrent.CompletableFuture} types which allow for richer + * interaction with the asynchronous task and for immediate composition with + * further processing steps. + * + *

    A {@code Future} handle returned from the proxy will be an actual asynchronous + * {@code Future} that can be used to track the result of the asynchronous method + * execution. However, since the target method needs to implement the same signature, + * it will have to return a temporary {@code Future} handle that just passes a value + * through: e.g. Spring's {@link AsyncResult}, EJB 3.1's {@link javax.ejb.AsyncResult}, + * or {@link java.util.concurrent.CompletableFuture#completedFuture(Object)}. + * + * @author Juergen Hoeller + * @author Chris Beams + * @since 3.0 + * @see AnnotationAsyncExecutionInterceptor + * @see AsyncAnnotationAdvisor + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Async { + + /** + * A qualifier value for the specified asynchronous operation(s). + *

    May be used to determine the target executor to be used when executing + * the asynchronous operation(s), matching the qualifier value (or the bean + * name) of a specific {@link java.util.concurrent.Executor Executor} or + * {@link org.springframework.core.task.TaskExecutor TaskExecutor} + * bean definition. + *

    When specified on a class-level {@code @Async} annotation, indicates that the + * given executor should be used for all methods within the class. Method-level use + * of {@code Async#value} always overrides any value set at the class level. + * @since 3.1.2 + */ + String value() default ""; + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationAdvisor.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationAdvisor.java new file mode 100644 index 0000000..c5636f2 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationAdvisor.java @@ -0,0 +1,179 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.annotation; + +import java.lang.annotation.Annotation; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.function.Supplier; + +import org.aopalliance.aop.Advice; + +import org.springframework.aop.Pointcut; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.aop.support.AbstractPointcutAdvisor; +import org.springframework.aop.support.ComposablePointcut; +import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.function.SingletonSupplier; + +/** + * Advisor that activates asynchronous method execution through the {@link Async} + * annotation. This annotation can be used at the method and type level in + * implementation classes as well as in service interfaces. + * + *

    This advisor detects the EJB 3.1 {@code javax.ejb.Asynchronous} + * annotation as well, treating it exactly like Spring's own {@code Async}. + * Furthermore, a custom async annotation type may get specified through the + * {@link #setAsyncAnnotationType "asyncAnnotationType"} property. + * + * @author Juergen Hoeller + * @since 3.0 + * @see Async + * @see AnnotationAsyncExecutionInterceptor + */ +@SuppressWarnings("serial") +public class AsyncAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware { + + private Advice advice; + + private Pointcut pointcut; + + + /** + * Create a new {@code AsyncAnnotationAdvisor} for bean-style configuration. + */ + public AsyncAnnotationAdvisor() { + this((Supplier) null, (Supplier) null); + } + + /** + * Create a new {@code AsyncAnnotationAdvisor} for the given task executor. + * @param executor the task executor to use for asynchronous methods + * (can be {@code null} to trigger default executor resolution) + * @param exceptionHandler the {@link AsyncUncaughtExceptionHandler} to use to + * handle unexpected exception thrown by asynchronous method executions + * @see AnnotationAsyncExecutionInterceptor#getDefaultExecutor(BeanFactory) + */ + @SuppressWarnings("unchecked") + public AsyncAnnotationAdvisor( + @Nullable Executor executor, @Nullable AsyncUncaughtExceptionHandler exceptionHandler) { + + this(SingletonSupplier.ofNullable(executor), SingletonSupplier.ofNullable(exceptionHandler)); + } + + /** + * Create a new {@code AsyncAnnotationAdvisor} for the given task executor. + * @param executor the task executor to use for asynchronous methods + * (can be {@code null} to trigger default executor resolution) + * @param exceptionHandler the {@link AsyncUncaughtExceptionHandler} to use to + * handle unexpected exception thrown by asynchronous method executions + * @since 5.1 + * @see AnnotationAsyncExecutionInterceptor#getDefaultExecutor(BeanFactory) + */ + @SuppressWarnings("unchecked") + public AsyncAnnotationAdvisor( + @Nullable Supplier executor, @Nullable Supplier exceptionHandler) { + + Set> asyncAnnotationTypes = new LinkedHashSet<>(2); + asyncAnnotationTypes.add(Async.class); + try { + asyncAnnotationTypes.add((Class) + ClassUtils.forName("javax.ejb.Asynchronous", AsyncAnnotationAdvisor.class.getClassLoader())); + } + catch (ClassNotFoundException ex) { + // If EJB 3.1 API not present, simply ignore. + } + this.advice = buildAdvice(executor, exceptionHandler); + this.pointcut = buildPointcut(asyncAnnotationTypes); + } + + + /** + * Set the 'async' annotation type. + *

    The default async annotation type is the {@link Async} annotation, as well + * as the EJB 3.1 {@code javax.ejb.Asynchronous} annotation (if present). + *

    This setter property exists so that developers can provide their own + * (non-Spring-specific) annotation type to indicate that a method is to + * be executed asynchronously. + * @param asyncAnnotationType the desired annotation type + */ + public void setAsyncAnnotationType(Class asyncAnnotationType) { + Assert.notNull(asyncAnnotationType, "'asyncAnnotationType' must not be null"); + Set> asyncAnnotationTypes = new HashSet<>(); + asyncAnnotationTypes.add(asyncAnnotationType); + this.pointcut = buildPointcut(asyncAnnotationTypes); + } + + /** + * Set the {@code BeanFactory} to be used when looking up executors by qualifier. + */ + @Override + public void setBeanFactory(BeanFactory beanFactory) { + if (this.advice instanceof BeanFactoryAware) { + ((BeanFactoryAware) this.advice).setBeanFactory(beanFactory); + } + } + + + @Override + public Advice getAdvice() { + return this.advice; + } + + @Override + public Pointcut getPointcut() { + return this.pointcut; + } + + + protected Advice buildAdvice( + @Nullable Supplier executor, @Nullable Supplier exceptionHandler) { + + AnnotationAsyncExecutionInterceptor interceptor = new AnnotationAsyncExecutionInterceptor(null); + interceptor.configure(executor, exceptionHandler); + return interceptor; + } + + /** + * Calculate a pointcut for the given async annotation types, if any. + * @param asyncAnnotationTypes the async annotation types to introspect + * @return the applicable Pointcut object, or {@code null} if none + */ + protected Pointcut buildPointcut(Set> asyncAnnotationTypes) { + ComposablePointcut result = null; + for (Class asyncAnnotationType : asyncAnnotationTypes) { + Pointcut cpc = new AnnotationMatchingPointcut(asyncAnnotationType, true); + Pointcut mpc = new AnnotationMatchingPointcut(null, asyncAnnotationType, true); + if (result == null) { + result = new ComposablePointcut(cpc); + } + else { + result.union(cpc); + } + result = result.union(mpc); + } + return (result != null ? result : Pointcut.TRUE); + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessor.java new file mode 100644 index 0000000..1e7d9bb --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessor.java @@ -0,0 +1,157 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.annotation; + +import java.lang.annotation.Annotation; +import java.util.concurrent.Executor; +import java.util.function.Supplier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aop.framework.autoproxy.AbstractBeanFactoryAwareAdvisingPostProcessor; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.core.task.TaskExecutor; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.function.SingletonSupplier; + +/** + * Bean post-processor that automatically applies asynchronous invocation + * behavior to any bean that carries the {@link Async} annotation at class or + * method-level by adding a corresponding {@link AsyncAnnotationAdvisor} to the + * exposed proxy (either an existing AOP proxy or a newly generated proxy that + * implements all of the target's interfaces). + * + *

    The {@link TaskExecutor} responsible for the asynchronous execution may + * be provided as well as the annotation type that indicates a method should be + * invoked asynchronously. If no annotation type is specified, this post- + * processor will detect both Spring's {@link Async @Async} annotation as well + * as the EJB 3.1 {@code javax.ejb.Asynchronous} annotation. + * + *

    For methods having a {@code void} return type, any exception thrown + * during the asynchronous method invocation cannot be accessed by the + * caller. An {@link AsyncUncaughtExceptionHandler} can be specified to handle + * these cases. + * + *

    Note: The underlying async advisor applies before existing advisors by default, + * in order to switch to async execution as early as possible in the invocation chain. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @author Stephane Nicoll + * @since 3.0 + * @see Async + * @see AsyncAnnotationAdvisor + * @see #setBeforeExistingAdvisors + * @see ScheduledAnnotationBeanPostProcessor + */ +@SuppressWarnings("serial") +public class AsyncAnnotationBeanPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor { + + /** + * The default name of the {@link TaskExecutor} bean to pick up: "taskExecutor". + *

    Note that the initial lookup happens by type; this is just the fallback + * in case of multiple executor beans found in the context. + * @since 4.2 + * @see AnnotationAsyncExecutionInterceptor#DEFAULT_TASK_EXECUTOR_BEAN_NAME + */ + public static final String DEFAULT_TASK_EXECUTOR_BEAN_NAME = + AnnotationAsyncExecutionInterceptor.DEFAULT_TASK_EXECUTOR_BEAN_NAME; + + + protected final Log logger = LogFactory.getLog(getClass()); + + @Nullable + private Supplier executor; + + @Nullable + private Supplier exceptionHandler; + + @Nullable + private Class asyncAnnotationType; + + + + public AsyncAnnotationBeanPostProcessor() { + setBeforeExistingAdvisors(true); + } + + + /** + * Configure this post-processor with the given executor and exception handler suppliers, + * applying the corresponding default if a supplier is not resolvable. + * @since 5.1 + */ + public void configure( + @Nullable Supplier executor, @Nullable Supplier exceptionHandler) { + + this.executor = executor; + this.exceptionHandler = exceptionHandler; + } + + /** + * Set the {@link Executor} to use when invoking methods asynchronously. + *

    If not specified, default executor resolution will apply: searching for a + * unique {@link TaskExecutor} bean in the context, or for an {@link Executor} + * bean named "taskExecutor" otherwise. If neither of the two is resolvable, + * a local default executor will be created within the interceptor. + * @see AnnotationAsyncExecutionInterceptor#getDefaultExecutor(BeanFactory) + * @see #DEFAULT_TASK_EXECUTOR_BEAN_NAME + */ + public void setExecutor(Executor executor) { + this.executor = SingletonSupplier.of(executor); + } + + /** + * Set the {@link AsyncUncaughtExceptionHandler} to use to handle uncaught + * exceptions thrown by asynchronous method executions. + * @since 4.1 + */ + public void setExceptionHandler(AsyncUncaughtExceptionHandler exceptionHandler) { + this.exceptionHandler = SingletonSupplier.of(exceptionHandler); + } + + /** + * Set the 'async' annotation type to be detected at either class or method + * level. By default, both the {@link Async} annotation and the EJB 3.1 + * {@code javax.ejb.Asynchronous} annotation will be detected. + *

    This setter property exists so that developers can provide their own + * (non-Spring-specific) annotation type to indicate that a method (or all + * methods of a given class) should be invoked asynchronously. + * @param asyncAnnotationType the desired annotation type + */ + public void setAsyncAnnotationType(Class asyncAnnotationType) { + Assert.notNull(asyncAnnotationType, "'asyncAnnotationType' must not be null"); + this.asyncAnnotationType = asyncAnnotationType; + } + + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + super.setBeanFactory(beanFactory); + + AsyncAnnotationAdvisor advisor = new AsyncAnnotationAdvisor(this.executor, this.exceptionHandler); + if (this.asyncAnnotationType != null) { + advisor.setAsyncAnnotationType(this.asyncAnnotationType); + } + advisor.setBeanFactory(beanFactory); + this.advisor = advisor; + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurationSelector.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurationSelector.java new file mode 100644 index 0000000..76c3380 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurationSelector.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.annotation; + +import org.springframework.context.annotation.AdviceMode; +import org.springframework.context.annotation.AdviceModeImportSelector; +import org.springframework.lang.Nullable; + +/** + * Selects which implementation of {@link AbstractAsyncConfiguration} should + * be used based on the value of {@link EnableAsync#mode} on the importing + * {@code @Configuration} class. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + * @see EnableAsync + * @see ProxyAsyncConfiguration + */ +public class AsyncConfigurationSelector extends AdviceModeImportSelector { + + private static final String ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME = + "org.springframework.scheduling.aspectj.AspectJAsyncConfiguration"; + + + /** + * Returns {@link ProxyAsyncConfiguration} or {@code AspectJAsyncConfiguration} + * for {@code PROXY} and {@code ASPECTJ} values of {@link EnableAsync#mode()}, + * respectively. + */ + @Override + @Nullable + public String[] selectImports(AdviceMode adviceMode) { + switch (adviceMode) { + case PROXY: + return new String[] {ProxyAsyncConfiguration.class.getName()}; + case ASPECTJ: + return new String[] {ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME}; + default: + return null; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurer.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurer.java new file mode 100644 index 0000000..eab9744 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurer.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.annotation; + +import java.util.concurrent.Executor; + +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.lang.Nullable; + +/** + * Interface to be implemented by @{@link org.springframework.context.annotation.Configuration + * Configuration} classes annotated with @{@link EnableAsync} that wish to customize the + * {@link Executor} instance used when processing async method invocations or the + * {@link AsyncUncaughtExceptionHandler} instance used to process exception thrown from + * async method with {@code void} return type. + * + *

    Consider using {@link AsyncConfigurerSupport} providing default implementations for + * both methods if only one element needs to be customized. Furthermore, backward compatibility + * of this interface will be insured in case new customization options are introduced + * in the future. + * + *

    See @{@link EnableAsync} for usage examples. + * + * @author Chris Beams + * @author Stephane Nicoll + * @since 3.1 + * @see AbstractAsyncConfiguration + * @see EnableAsync + * @see AsyncConfigurerSupport + */ +public interface AsyncConfigurer { + + /** + * The {@link Executor} instance to be used when processing async + * method invocations. + */ + @Nullable + default Executor getAsyncExecutor() { + return null; + } + + /** + * The {@link AsyncUncaughtExceptionHandler} instance to be used + * when an exception is thrown during an asynchronous method execution + * with {@code void} return type. + */ + @Nullable + default AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return null; + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurerSupport.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurerSupport.java new file mode 100644 index 0000000..34acb2c --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurerSupport.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.annotation; + +import java.util.concurrent.Executor; + +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.lang.Nullable; + +/** + * A convenience {@link AsyncConfigurer} that implements all methods + * so that the defaults are used. Provides a backward compatible alternative + * of implementing {@link AsyncConfigurer} directly. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public class AsyncConfigurerSupport implements AsyncConfigurer { + + @Override + public Executor getAsyncExecutor() { + return null; + } + + @Override + @Nullable + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return null; + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncResult.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncResult.java new file mode 100644 index 0000000..686845f --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncResult.java @@ -0,0 +1,179 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.annotation; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.springframework.lang.Nullable; +import org.springframework.util.concurrent.FailureCallback; +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.util.concurrent.ListenableFutureCallback; +import org.springframework.util.concurrent.SuccessCallback; + +/** + * A pass-through {@code Future} handle that can be used for method signatures + * which are declared with a {@code Future} return type for asynchronous execution. + * + *

    As of Spring 4.1, this class implements {@link ListenableFuture}, not just + * plain {@link java.util.concurrent.Future}, along with the corresponding support + * in {@code @Async} processing. + * + *

    As of Spring 4.2, this class also supports passing execution exceptions back + * to the caller. + * + * @author Juergen Hoeller + * @author Rossen Stoyanchev + * @since 3.0 + * @param the value type + * @see Async + * @see #forValue(Object) + * @see #forExecutionException(Throwable) + */ +public class AsyncResult implements ListenableFuture { + + @Nullable + private final V value; + + @Nullable + private final Throwable executionException; + + + /** + * Create a new AsyncResult holder. + * @param value the value to pass through + */ + public AsyncResult(@Nullable V value) { + this(value, null); + } + + /** + * Create a new AsyncResult holder. + * @param value the value to pass through + */ + private AsyncResult(@Nullable V value, @Nullable Throwable ex) { + this.value = value; + this.executionException = ex; + } + + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return true; + } + + @Override + @Nullable + public V get() throws ExecutionException { + if (this.executionException != null) { + throw (this.executionException instanceof ExecutionException ? + (ExecutionException) this.executionException : + new ExecutionException(this.executionException)); + } + return this.value; + } + + @Override + @Nullable + public V get(long timeout, TimeUnit unit) throws ExecutionException { + return get(); + } + + @Override + public void addCallback(ListenableFutureCallback callback) { + addCallback(callback, callback); + } + + @Override + public void addCallback(SuccessCallback successCallback, FailureCallback failureCallback) { + try { + if (this.executionException != null) { + failureCallback.onFailure(exposedException(this.executionException)); + } + else { + successCallback.onSuccess(this.value); + } + } + catch (Throwable ex) { + // Ignore + } + } + + @Override + public CompletableFuture completable() { + if (this.executionException != null) { + CompletableFuture completable = new CompletableFuture<>(); + completable.completeExceptionally(exposedException(this.executionException)); + return completable; + } + else { + return CompletableFuture.completedFuture(this.value); + } + } + + + /** + * Create a new async result which exposes the given value from {@link Future#get()}. + * @param value the value to expose + * @since 4.2 + * @see Future#get() + */ + public static ListenableFuture forValue(V value) { + return new AsyncResult<>(value, null); + } + + /** + * Create a new async result which exposes the given exception as an + * {@link ExecutionException} from {@link Future#get()}. + * @param ex the exception to expose (either an pre-built {@link ExecutionException} + * or a cause to be wrapped in an {@link ExecutionException}) + * @since 4.2 + * @see ExecutionException + */ + public static ListenableFuture forExecutionException(Throwable ex) { + return new AsyncResult<>(null, ex); + } + + /** + * Determine the exposed exception: either the cause of a given + * {@link ExecutionException}, or the original exception as-is. + * @param original the original as given to {@link #forExecutionException} + * @return the exposed exception + */ + private static Throwable exposedException(Throwable original) { + if (original instanceof ExecutionException) { + Throwable cause = original.getCause(); + if (cause != null) { + return cause; + } + } + return original; + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/EnableAsync.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/EnableAsync.java new file mode 100644 index 0000000..71f3f03 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/EnableAsync.java @@ -0,0 +1,211 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.AdviceMode; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; + +/** + * Enables Spring's asynchronous method execution capability, similar to functionality + * found in Spring's {@code } XML namespace. + * + *

    To be used together with @{@link Configuration Configuration} classes as follows, + * enabling annotation-driven async processing for an entire Spring application context: + * + *

    + * @Configuration
    + * @EnableAsync
    + * public class AppConfig {
    + *
    + * }
    + * + * {@code MyAsyncBean} is a user-defined type with one or more methods annotated with + * either Spring's {@code @Async} annotation, the EJB 3.1 {@code @javax.ejb.Asynchronous} + * annotation, or any custom annotation specified via the {@link #annotation} attribute. + * The aspect is added transparently for any registered bean, for instance via this + * configuration: + * + *
    + * @Configuration
    + * public class AnotherAppConfig {
    + *
    + *     @Bean
    + *     public MyAsyncBean asyncBean() {
    + *         return new MyAsyncBean();
    + *     }
    + * }
    + * + *

    By default, Spring will be searching for an associated thread pool definition: + * either a unique {@link org.springframework.core.task.TaskExecutor} bean in the context, + * or an {@link java.util.concurrent.Executor} bean named "taskExecutor" otherwise. If + * neither of the two is resolvable, a {@link org.springframework.core.task.SimpleAsyncTaskExecutor} + * will be used to process async method invocations. Besides, annotated methods having a + * {@code void} return type cannot transmit any exception back to the caller. By default, + * such uncaught exceptions are only logged. + * + *

    To customize all this, implement {@link AsyncConfigurer} and provide: + *

      + *
    • your own {@link java.util.concurrent.Executor Executor} through the + * {@link AsyncConfigurer#getAsyncExecutor getAsyncExecutor()} method, and
    • + *
    • your own {@link org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler + * AsyncUncaughtExceptionHandler} through the {@link AsyncConfigurer#getAsyncUncaughtExceptionHandler + * getAsyncUncaughtExceptionHandler()} + * method.
    • + *
    + * + *

    NOTE: {@link AsyncConfigurer} configuration classes get initialized early + * in the application context bootstrap. If you need any dependencies on other beans + * there, make sure to declare them 'lazy' as far as possible in order to let them + * go through other post-processors as well. + * + *

    + * @Configuration
    + * @EnableAsync
    + * public class AppConfig implements AsyncConfigurer {
    + *
    + *     @Override
    + *     public Executor getAsyncExecutor() {
    + *         ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    + *         executor.setCorePoolSize(7);
    + *         executor.setMaxPoolSize(42);
    + *         executor.setQueueCapacity(11);
    + *         executor.setThreadNamePrefix("MyExecutor-");
    + *         executor.initialize();
    + *         return executor;
    + *     }
    + *
    + *     @Override
    + *     public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
    + *         return new MyAsyncUncaughtExceptionHandler();
    + *     }
    + * }
    + * + *

    If only one item needs to be customized, {@code null} can be returned to + * keep the default settings. Consider also extending from {@link AsyncConfigurerSupport} + * when possible. + * + *

    Note: In the above example the {@code ThreadPoolTaskExecutor} is not a fully managed + * Spring bean. Add the {@code @Bean} annotation to the {@code getAsyncExecutor()} method + * if you want a fully managed bean. In such circumstances it is no longer necessary to + * manually call the {@code executor.initialize()} method as this will be invoked + * automatically when the bean is initialized. + * + *

    For reference, the example above can be compared to the following Spring XML + * configuration: + * + *

    + * <beans>
    + *
    + *     <task:annotation-driven executor="myExecutor" exception-handler="exceptionHandler"/>
    + *
    + *     <task:executor id="myExecutor" pool-size="7-42" queue-capacity="11"/>
    + *
    + *     <bean id="asyncBean" class="com.foo.MyAsyncBean"/>
    + *
    + *     <bean id="exceptionHandler" class="com.foo.MyAsyncUncaughtExceptionHandler"/>
    + *
    + * </beans>
    + * 
    + * + * The above XML-based and JavaConfig-based examples are equivalent except for the + * setting of the thread name prefix of the {@code Executor}; this is because + * the {@code } element does not expose such an attribute. This + * demonstrates how the JavaConfig-based approach allows for maximum configurability + * through direct access to actual componentry. + * + *

    The {@link #mode} attribute controls how advice is applied: If the mode is + * {@link AdviceMode#PROXY} (the default), then the other attributes control the behavior + * of the proxying. Please note that proxy mode allows for interception of calls through + * the proxy only; local calls within the same class cannot get intercepted that way. + * + *

    Note that if the {@linkplain #mode} is set to {@link AdviceMode#ASPECTJ}, then the + * value of the {@link #proxyTargetClass} attribute will be ignored. Note also that in + * this case the {@code spring-aspects} module JAR must be present on the classpath, with + * compile-time weaving or load-time weaving applying the aspect to the affected classes. + * There is no proxy involved in such a scenario; local calls will be intercepted as well. + * + * @author Chris Beams + * @author Juergen Hoeller + * @author Stephane Nicoll + * @author Sam Brannen + * @since 3.1 + * @see Async + * @see AsyncConfigurer + * @see AsyncConfigurationSelector + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Import(AsyncConfigurationSelector.class) +public @interface EnableAsync { + + /** + * Indicate the 'async' annotation type to be detected at either class + * or method level. + *

    By default, both Spring's @{@link Async} annotation and the EJB 3.1 + * {@code @javax.ejb.Asynchronous} annotation will be detected. + *

    This attribute exists so that developers can provide their own + * custom annotation type to indicate that a method (or all methods of + * a given class) should be invoked asynchronously. + */ + Class annotation() default Annotation.class; + + /** + * Indicate whether subclass-based (CGLIB) proxies are to be created as opposed + * to standard Java interface-based proxies. + *

    Applicable only if the {@link #mode} is set to {@link AdviceMode#PROXY}. + *

    The default is {@code false}. + *

    Note that setting this attribute to {@code true} will affect all + * Spring-managed beans requiring proxying, not just those marked with {@code @Async}. + * For example, other beans marked with Spring's {@code @Transactional} annotation + * will be upgraded to subclass proxying at the same time. This approach has no + * negative impact in practice unless one is explicitly expecting one type of proxy + * vs. another — for example, in tests. + */ + boolean proxyTargetClass() default false; + + /** + * Indicate how async advice should be applied. + *

    The default is {@link AdviceMode#PROXY}. + * Please note that proxy mode allows for interception of calls through the proxy + * only. Local calls within the same class cannot get intercepted that way; an + * {@link Async} annotation on such a method within a local call will be ignored + * since Spring's interceptor does not even kick in for such a runtime scenario. + * For a more advanced mode of interception, consider switching this to + * {@link AdviceMode#ASPECTJ}. + */ + AdviceMode mode() default AdviceMode.PROXY; + + /** + * Indicate the order in which the {@link AsyncAnnotationBeanPostProcessor} + * should be applied. + *

    The default is {@link Ordered#LOWEST_PRECEDENCE} in order to run + * after all other post-processors, so that it can add an advisor to + * existing proxies rather than double-proxy. + */ + int order() default Ordered.LOWEST_PRECEDENCE; + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/EnableScheduling.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/EnableScheduling.java new file mode 100644 index 0000000..d94b5f5 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/EnableScheduling.java @@ -0,0 +1,211 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.Executor; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.scheduling.Trigger; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; + +/** + * Enables Spring's scheduled task execution capability, similar to + * functionality found in Spring's {@code } XML namespace. To be used + * on @{@link Configuration} classes as follows: + * + *

    + * @Configuration
    + * @EnableScheduling
    + * public class AppConfig {
    + *
    + *     // various @Bean definitions
    + * }
    + * + * This enables detection of @{@link Scheduled} annotations on any Spring-managed + * bean in the container. For example, given a class {@code MyTask} + * + *
    + * package com.myco.tasks;
    + *
    + * public class MyTask {
    + *
    + *     @Scheduled(fixedRate=1000)
    + *     public void work() {
    + *         // task execution logic
    + *     }
    + * }
    + * + * the following configuration would ensure that {@code MyTask.work()} is called + * once every 1000 ms: + * + *
    + * @Configuration
    + * @EnableScheduling
    + * public class AppConfig {
    + *
    + *     @Bean
    + *     public MyTask task() {
    + *         return new MyTask();
    + *     }
    + * }
    + * + * Alternatively, if {@code MyTask} were annotated with {@code @Component}, the + * following configuration would ensure that its {@code @Scheduled} method is + * invoked at the desired interval: + * + *
    + * @Configuration
    + * @EnableScheduling
    + * @ComponentScan(basePackages="com.myco.tasks")
    + * public class AppConfig {
    + * }
    + * + * Methods annotated with {@code @Scheduled} may even be declared directly within + * {@code @Configuration} classes: + * + *
    + * @Configuration
    + * @EnableScheduling
    + * public class AppConfig {
    + *
    + *     @Scheduled(fixedRate=1000)
    + *     public void work() {
    + *         // task execution logic
    + *     }
    + * }
    + * + *

    By default, will be searching for an associated scheduler definition: either + * a unique {@link org.springframework.scheduling.TaskScheduler} bean in the context, + * or a {@code TaskScheduler} bean named "taskScheduler" otherwise; the same lookup + * will also be performed for a {@link java.util.concurrent.ScheduledExecutorService} + * bean. If neither of the two is resolvable, a local single-threaded default + * scheduler will be created and used within the registrar. + * + *

    When more control is desired, a {@code @Configuration} class may implement + * {@link SchedulingConfigurer}. This allows access to the underlying + * {@link ScheduledTaskRegistrar} instance. For example, the following example + * demonstrates how to customize the {@link Executor} used to execute scheduled + * tasks: + * + *

    + * @Configuration
    + * @EnableScheduling
    + * public class AppConfig implements SchedulingConfigurer {
    + *
    + *     @Override
    + *     public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
    + *         taskRegistrar.setScheduler(taskExecutor());
    + *     }
    + *
    + *     @Bean(destroyMethod="shutdown")
    + *     public Executor taskExecutor() {
    + *         return Executors.newScheduledThreadPool(100);
    + *     }
    + * }
    + * + *

    Note in the example above the use of {@code @Bean(destroyMethod="shutdown")}. + * This ensures that the task executor is properly shut down when the Spring + * application context itself is closed. + * + *

    Implementing {@code SchedulingConfigurer} also allows for fine-grained + * control over task registration via the {@code ScheduledTaskRegistrar}. + * For example, the following configures the execution of a particular bean + * method per a custom {@code Trigger} implementation: + * + *

    + * @Configuration
    + * @EnableScheduling
    + * public class AppConfig implements SchedulingConfigurer {
    + *
    + *     @Override
    + *     public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
    + *         taskRegistrar.setScheduler(taskScheduler());
    + *         taskRegistrar.addTriggerTask(
    + *             new Runnable() {
    + *                 public void run() {
    + *                     myTask().work();
    + *                 }
    + *             },
    + *             new CustomTrigger()
    + *         );
    + *     }
    + *
    + *     @Bean(destroyMethod="shutdown")
    + *     public Executor taskScheduler() {
    + *         return Executors.newScheduledThreadPool(42);
    + *     }
    + *
    + *     @Bean
    + *     public MyTask myTask() {
    + *         return new MyTask();
    + *     }
    + * }
    + * + *

    For reference, the example above can be compared to the following Spring XML + * configuration: + * + *

    + * <beans>
    + *
    + *     <task:annotation-driven scheduler="taskScheduler"/>
    + *
    + *     <task:scheduler id="taskScheduler" pool-size="42"/>
    + *
    + *     <task:scheduled-tasks scheduler="taskScheduler">
    + *         <task:scheduled ref="myTask" method="work" fixed-rate="1000"/>
    + *     </task:scheduled-tasks>
    + *
    + *     <bean id="myTask" class="com.foo.MyTask"/>
    + *
    + * </beans>
    + * 
    + * + * The examples are equivalent save that in XML a fixed-rate period is used + * instead of a custom {@code Trigger} implementation; this is because the + * {@code task:} namespace {@code scheduled} cannot easily expose such support. This is + * but one demonstration how the code-based approach allows for maximum configurability + * through direct access to actual componentry.

    + * + * Note: {@code @EnableScheduling} applies to its local application context only, + * allowing for selective scheduling of beans at different levels. Please redeclare + * {@code @EnableScheduling} in each individual context, e.g. the common root web + * application context and any separate {@code DispatcherServlet} application contexts, + * if you need to apply its behavior at multiple levels. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + * @see Scheduled + * @see SchedulingConfiguration + * @see SchedulingConfigurer + * @see ScheduledTaskRegistrar + * @see Trigger + * @see ScheduledAnnotationBeanPostProcessor + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Import(SchedulingConfiguration.class) +@Documented +public @interface EnableScheduling { + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/ProxyAsyncConfiguration.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/ProxyAsyncConfiguration.java new file mode 100644 index 0000000..e3a5b31 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/ProxyAsyncConfiguration.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.annotation; + +import java.lang.annotation.Annotation; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.scheduling.config.TaskManagementConfigUtils; +import org.springframework.util.Assert; + +/** + * {@code @Configuration} class that registers the Spring infrastructure beans necessary + * to enable proxy-based asynchronous method execution. + * + * @author Chris Beams + * @author Stephane Nicoll + * @author Juergen Hoeller + * @since 3.1 + * @see EnableAsync + * @see AsyncConfigurationSelector + */ +@Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +public class ProxyAsyncConfiguration extends AbstractAsyncConfiguration { + + @Bean(name = TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME) + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public AsyncAnnotationBeanPostProcessor asyncAdvisor() { + Assert.notNull(this.enableAsync, "@EnableAsync annotation metadata was not injected"); + AsyncAnnotationBeanPostProcessor bpp = new AsyncAnnotationBeanPostProcessor(); + bpp.configure(this.executor, this.exceptionHandler); + Class customAsyncAnnotation = this.enableAsync.getClass("annotation"); + if (customAsyncAnnotation != AnnotationUtils.getDefaultValue(EnableAsync.class, "annotation")) { + bpp.setAsyncAnnotationType(customAsyncAnnotation); + } + bpp.setProxyTargetClass(this.enableAsync.getBoolean("proxyTargetClass")); + bpp.setOrder(this.enableAsync.getNumber("order")); + return bpp; + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java new file mode 100644 index 0000000..ad8a477 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java @@ -0,0 +1,152 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.scheduling.config.ScheduledTaskRegistrar; + +/** + * Annotation that marks a method to be scheduled. Exactly one of the + * {@link #cron}, {@link #fixedDelay}, or {@link #fixedRate} attributes must be + * specified. + * + *

    The annotated method must expect no arguments. It will typically have + * a {@code void} return type; if not, the returned value will be ignored + * when called through the scheduler. + * + *

    Processing of {@code @Scheduled} annotations is performed by + * registering a {@link ScheduledAnnotationBeanPostProcessor}. This can be + * done manually or, more conveniently, through the {@code } + * element or @{@link EnableScheduling} annotation. + * + *

    This annotation may be used as a meta-annotation to create custom + * composed annotations with attribute overrides. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @author Dave Syer + * @author Chris Beams + * @since 3.0 + * @see EnableScheduling + * @see ScheduledAnnotationBeanPostProcessor + * @see Schedules + */ +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Repeatable(Schedules.class) +public @interface Scheduled { + + /** + * A special cron expression value that indicates a disabled trigger: {@value}. + *

    This is primarily meant for use with ${...} placeholders, + * allowing for external disabling of corresponding scheduled methods. + * @since 5.1 + * @see ScheduledTaskRegistrar#CRON_DISABLED + */ + String CRON_DISABLED = ScheduledTaskRegistrar.CRON_DISABLED; + + + /** + * A cron-like expression, extending the usual UN*X definition to include triggers + * on the second, minute, hour, day of month, month, and day of week. + *

    For example, {@code "0 * * * * MON-FRI"} means once per minute on weekdays + * (at the top of the minute - the 0th second). + *

    The fields read from left to right are interpreted as follows. + *

      + *
    • second
    • + *
    • minute
    • + *
    • hour
    • + *
    • day of month
    • + *
    • month
    • + *
    • day of week
    • + *
    + *

    The special value {@link #CRON_DISABLED "-"} indicates a disabled cron + * trigger, primarily meant for externally specified values resolved by a + * ${...} placeholder. + * @return an expression that can be parsed to a cron schedule + * @see org.springframework.scheduling.support.CronSequenceGenerator + */ + String cron() default ""; + + /** + * A time zone for which the cron expression will be resolved. By default, this + * attribute is the empty String (i.e. the server's local time zone will be used). + * @return a zone id accepted by {@link java.util.TimeZone#getTimeZone(String)}, + * or an empty String to indicate the server's default time zone + * @since 4.0 + * @see org.springframework.scheduling.support.CronTrigger#CronTrigger(String, java.util.TimeZone) + * @see java.util.TimeZone + */ + String zone() default ""; + + /** + * Execute the annotated method with a fixed period in milliseconds between the + * end of the last invocation and the start of the next. + * @return the delay in milliseconds + */ + long fixedDelay() default -1; + + /** + * Execute the annotated method with a fixed period in milliseconds between the + * end of the last invocation and the start of the next. + * @return the delay in milliseconds as a String value, e.g. a placeholder + * or a {@link java.time.Duration#parse java.time.Duration} compliant value + * @since 3.2.2 + */ + String fixedDelayString() default ""; + + /** + * Execute the annotated method with a fixed period in milliseconds between + * invocations. + * @return the period in milliseconds + */ + long fixedRate() default -1; + + /** + * Execute the annotated method with a fixed period in milliseconds between + * invocations. + * @return the period in milliseconds as a String value, e.g. a placeholder + * or a {@link java.time.Duration#parse java.time.Duration} compliant value + * @since 3.2.2 + */ + String fixedRateString() default ""; + + /** + * Number of milliseconds to delay before the first execution of a + * {@link #fixedRate} or {@link #fixedDelay} task. + * @return the initial delay in milliseconds + * @since 3.2 + */ + long initialDelay() default -1; + + /** + * Number of milliseconds to delay before the first execution of a + * {@link #fixedRate} or {@link #fixedDelay} task. + * @return the initial delay in milliseconds as a String value, e.g. a placeholder + * or a {@link java.time.Duration#parse java.time.Duration} compliant value + * @since 3.2.2 + */ + String initialDelayString() default ""; + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java new file mode 100644 index 0000000..0ab8433 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java @@ -0,0 +1,594 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.annotation; + +import java.lang.reflect.Method; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledExecutorService; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aop.framework.AopInfrastructureBean; +import org.springframework.aop.framework.AopProxyUtils; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor; +import org.springframework.beans.factory.config.NamedBeanHolder; +import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationListener; +import org.springframework.context.EmbeddedValueResolverAware; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.core.MethodIntrospector; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.lang.Nullable; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.Trigger; +import org.springframework.scheduling.config.CronTask; +import org.springframework.scheduling.config.FixedDelayTask; +import org.springframework.scheduling.config.FixedRateTask; +import org.springframework.scheduling.config.ScheduledTask; +import org.springframework.scheduling.config.ScheduledTaskHolder; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; +import org.springframework.scheduling.support.CronTrigger; +import org.springframework.scheduling.support.ScheduledMethodRunnable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.util.StringValueResolver; + +/** + * Bean post-processor that registers methods annotated with @{@link Scheduled} + * to be invoked by a {@link org.springframework.scheduling.TaskScheduler} according + * to the "fixedRate", "fixedDelay", or "cron" expression provided via the annotation. + * + *

    This post-processor is automatically registered by Spring's + * {@code } XML element, and also by the + * {@link EnableScheduling @EnableScheduling} annotation. + * + *

    Autodetects any {@link SchedulingConfigurer} instances in the container, + * allowing for customization of the scheduler to be used or for fine-grained + * control over task registration (e.g. registration of {@link Trigger} tasks. + * See the @{@link EnableScheduling} javadocs for complete usage details. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @author Chris Beams + * @author Elizabeth Chatman + * @since 3.0 + * @see Scheduled + * @see EnableScheduling + * @see SchedulingConfigurer + * @see org.springframework.scheduling.TaskScheduler + * @see org.springframework.scheduling.config.ScheduledTaskRegistrar + * @see AsyncAnnotationBeanPostProcessor + */ +public class ScheduledAnnotationBeanPostProcessor + implements ScheduledTaskHolder, MergedBeanDefinitionPostProcessor, DestructionAwareBeanPostProcessor, + Ordered, EmbeddedValueResolverAware, BeanNameAware, BeanFactoryAware, ApplicationContextAware, + SmartInitializingSingleton, ApplicationListener, DisposableBean { + + /** + * The default name of the {@link TaskScheduler} bean to pick up: {@value}. + *

    Note that the initial lookup happens by type; this is just the fallback + * in case of multiple scheduler beans found in the context. + * @since 4.2 + */ + public static final String DEFAULT_TASK_SCHEDULER_BEAN_NAME = "taskScheduler"; + + + protected final Log logger = LogFactory.getLog(getClass()); + + private final ScheduledTaskRegistrar registrar; + + @Nullable + private Object scheduler; + + @Nullable + private StringValueResolver embeddedValueResolver; + + @Nullable + private String beanName; + + @Nullable + private BeanFactory beanFactory; + + @Nullable + private ApplicationContext applicationContext; + + private final Set> nonAnnotatedClasses = Collections.newSetFromMap(new ConcurrentHashMap<>(64)); + + private final Map> scheduledTasks = new IdentityHashMap<>(16); + + + /** + * Create a default {@code ScheduledAnnotationBeanPostProcessor}. + */ + public ScheduledAnnotationBeanPostProcessor() { + this.registrar = new ScheduledTaskRegistrar(); + } + + /** + * Create a {@code ScheduledAnnotationBeanPostProcessor} delegating to the + * specified {@link ScheduledTaskRegistrar}. + * @param registrar the ScheduledTaskRegistrar to register @Scheduled tasks on + * @since 5.1 + */ + public ScheduledAnnotationBeanPostProcessor(ScheduledTaskRegistrar registrar) { + Assert.notNull(registrar, "ScheduledTaskRegistrar is required"); + this.registrar = registrar; + } + + + @Override + public int getOrder() { + return LOWEST_PRECEDENCE; + } + + /** + * Set the {@link org.springframework.scheduling.TaskScheduler} that will invoke + * the scheduled methods, or a {@link java.util.concurrent.ScheduledExecutorService} + * to be wrapped as a TaskScheduler. + *

    If not specified, default scheduler resolution will apply: searching for a + * unique {@link TaskScheduler} bean in the context, or for a {@link TaskScheduler} + * bean named "taskScheduler" otherwise; the same lookup will also be performed for + * a {@link ScheduledExecutorService} bean. If neither of the two is resolvable, + * a local single-threaded default scheduler will be created within the registrar. + * @see #DEFAULT_TASK_SCHEDULER_BEAN_NAME + */ + public void setScheduler(Object scheduler) { + this.scheduler = scheduler; + } + + @Override + public void setEmbeddedValueResolver(StringValueResolver resolver) { + this.embeddedValueResolver = resolver; + } + + @Override + public void setBeanName(String beanName) { + this.beanName = beanName; + } + + /** + * Making a {@link BeanFactory} available is optional; if not set, + * {@link SchedulingConfigurer} beans won't get autodetected and + * a {@link #setScheduler scheduler} has to be explicitly configured. + */ + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + /** + * Setting an {@link ApplicationContext} is optional: If set, registered + * tasks will be activated in the {@link ContextRefreshedEvent} phase; + * if not set, it will happen at {@link #afterSingletonsInstantiated} time. + */ + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + if (this.beanFactory == null) { + this.beanFactory = applicationContext; + } + } + + + @Override + public void afterSingletonsInstantiated() { + // Remove resolved singleton classes from cache + this.nonAnnotatedClasses.clear(); + + if (this.applicationContext == null) { + // Not running in an ApplicationContext -> register tasks early... + finishRegistration(); + } + } + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + if (event.getApplicationContext() == this.applicationContext) { + // Running in an ApplicationContext -> register tasks this late... + // giving other ContextRefreshedEvent listeners a chance to perform + // their work at the same time (e.g. Spring Batch's job registration). + finishRegistration(); + } + } + + private void finishRegistration() { + if (this.scheduler != null) { + this.registrar.setScheduler(this.scheduler); + } + + if (this.beanFactory instanceof ListableBeanFactory) { + Map beans = + ((ListableBeanFactory) this.beanFactory).getBeansOfType(SchedulingConfigurer.class); + List configurers = new ArrayList<>(beans.values()); + AnnotationAwareOrderComparator.sort(configurers); + for (SchedulingConfigurer configurer : configurers) { + configurer.configureTasks(this.registrar); + } + } + + if (this.registrar.hasTasks() && this.registrar.getScheduler() == null) { + Assert.state(this.beanFactory != null, "BeanFactory must be set to find scheduler by type"); + try { + // Search for TaskScheduler bean... + this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, false)); + } + catch (NoUniqueBeanDefinitionException ex) { + if (logger.isTraceEnabled()) { + logger.trace("Could not find unique TaskScheduler bean - attempting to resolve by name: " + + ex.getMessage()); + } + try { + this.registrar.setTaskScheduler(resolveSchedulerBean(this.beanFactory, TaskScheduler.class, true)); + } + catch (NoSuchBeanDefinitionException ex2) { + if (logger.isInfoEnabled()) { + logger.info("More than one TaskScheduler bean exists within the context, and " + + "none is named 'taskScheduler'. Mark one of them as primary or name it 'taskScheduler' " + + "(possibly as an alias); or implement the SchedulingConfigurer interface and call " + + "ScheduledTaskRegistrar#setScheduler explicitly within the configureTasks() callback: " + + ex.getBeanNamesFound()); + } + } + } + catch (NoSuchBeanDefinitionException ex) { + if (logger.isTraceEnabled()) { + logger.trace("Could not find default TaskScheduler bean - attempting to find ScheduledExecutorService: " + + ex.getMessage()); + } + // Search for ScheduledExecutorService bean next... + try { + this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, false)); + } + catch (NoUniqueBeanDefinitionException ex2) { + if (logger.isTraceEnabled()) { + logger.trace("Could not find unique ScheduledExecutorService bean - attempting to resolve by name: " + + ex2.getMessage()); + } + try { + this.registrar.setScheduler(resolveSchedulerBean(this.beanFactory, ScheduledExecutorService.class, true)); + } + catch (NoSuchBeanDefinitionException ex3) { + if (logger.isInfoEnabled()) { + logger.info("More than one ScheduledExecutorService bean exists within the context, and " + + "none is named 'taskScheduler'. Mark one of them as primary or name it 'taskScheduler' " + + "(possibly as an alias); or implement the SchedulingConfigurer interface and call " + + "ScheduledTaskRegistrar#setScheduler explicitly within the configureTasks() callback: " + + ex2.getBeanNamesFound()); + } + } + } + catch (NoSuchBeanDefinitionException ex2) { + if (logger.isTraceEnabled()) { + logger.trace("Could not find default ScheduledExecutorService bean - falling back to default: " + + ex2.getMessage()); + } + // Giving up -> falling back to default scheduler within the registrar... + logger.info("No TaskScheduler/ScheduledExecutorService bean found for scheduled processing"); + } + } + } + + this.registrar.afterPropertiesSet(); + } + + private T resolveSchedulerBean(BeanFactory beanFactory, Class schedulerType, boolean byName) { + if (byName) { + T scheduler = beanFactory.getBean(DEFAULT_TASK_SCHEDULER_BEAN_NAME, schedulerType); + if (this.beanName != null && this.beanFactory instanceof ConfigurableBeanFactory) { + ((ConfigurableBeanFactory) this.beanFactory).registerDependentBean( + DEFAULT_TASK_SCHEDULER_BEAN_NAME, this.beanName); + } + return scheduler; + } + else if (beanFactory instanceof AutowireCapableBeanFactory) { + NamedBeanHolder holder = ((AutowireCapableBeanFactory) beanFactory).resolveNamedBean(schedulerType); + if (this.beanName != null && beanFactory instanceof ConfigurableBeanFactory) { + ((ConfigurableBeanFactory) beanFactory).registerDependentBean(holder.getBeanName(), this.beanName); + } + return holder.getBeanInstance(); + } + else { + return beanFactory.getBean(schedulerType); + } + } + + + @Override + public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class beanType, String beanName) { + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) { + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + if (bean instanceof AopInfrastructureBean || bean instanceof TaskScheduler || + bean instanceof ScheduledExecutorService) { + // Ignore AOP infrastructure such as scoped proxies. + return bean; + } + + Class targetClass = AopProxyUtils.ultimateTargetClass(bean); + if (!this.nonAnnotatedClasses.contains(targetClass) && + AnnotationUtils.isCandidateClass(targetClass, Arrays.asList(Scheduled.class, Schedules.class))) { + Map> annotatedMethods = MethodIntrospector.selectMethods(targetClass, + (MethodIntrospector.MetadataLookup>) method -> { + Set scheduledMethods = AnnotatedElementUtils.getMergedRepeatableAnnotations( + method, Scheduled.class, Schedules.class); + return (!scheduledMethods.isEmpty() ? scheduledMethods : null); + }); + if (annotatedMethods.isEmpty()) { + this.nonAnnotatedClasses.add(targetClass); + if (logger.isTraceEnabled()) { + logger.trace("No @Scheduled annotations found on bean class: " + targetClass); + } + } + else { + // Non-empty set of methods + annotatedMethods.forEach((method, scheduledMethods) -> + scheduledMethods.forEach(scheduled -> processScheduled(scheduled, method, bean))); + if (logger.isTraceEnabled()) { + logger.trace(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName + + "': " + annotatedMethods); + } + } + } + return bean; + } + + /** + * Process the given {@code @Scheduled} method declaration on the given bean. + * @param scheduled the @Scheduled annotation + * @param method the method that the annotation has been declared on + * @param bean the target bean instance + * @see #createRunnable(Object, Method) + */ + protected void processScheduled(Scheduled scheduled, Method method, Object bean) { + try { + Runnable runnable = createRunnable(bean, method); + boolean processedSchedule = false; + String errorMessage = + "Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required"; + + Set tasks = new LinkedHashSet<>(4); + + // Determine initial delay + long initialDelay = scheduled.initialDelay(); + String initialDelayString = scheduled.initialDelayString(); + if (StringUtils.hasText(initialDelayString)) { + Assert.isTrue(initialDelay < 0, "Specify 'initialDelay' or 'initialDelayString', not both"); + if (this.embeddedValueResolver != null) { + initialDelayString = this.embeddedValueResolver.resolveStringValue(initialDelayString); + } + if (StringUtils.hasLength(initialDelayString)) { + try { + initialDelay = parseDelayAsLong(initialDelayString); + } + catch (RuntimeException ex) { + throw new IllegalArgumentException( + "Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into long"); + } + } + } + + // Check cron expression + String cron = scheduled.cron(); + if (StringUtils.hasText(cron)) { + String zone = scheduled.zone(); + if (this.embeddedValueResolver != null) { + cron = this.embeddedValueResolver.resolveStringValue(cron); + zone = this.embeddedValueResolver.resolveStringValue(zone); + } + if (StringUtils.hasLength(cron)) { + Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers"); + processedSchedule = true; + if (!Scheduled.CRON_DISABLED.equals(cron)) { + TimeZone timeZone; + if (StringUtils.hasText(zone)) { + timeZone = StringUtils.parseTimeZoneString(zone); + } + else { + timeZone = TimeZone.getDefault(); + } + tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone)))); + } + } + } + + // At this point we don't need to differentiate between initial delay set or not anymore + if (initialDelay < 0) { + initialDelay = 0; + } + + // Check fixed delay + long fixedDelay = scheduled.fixedDelay(); + if (fixedDelay >= 0) { + Assert.isTrue(!processedSchedule, errorMessage); + processedSchedule = true; + tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay))); + } + String fixedDelayString = scheduled.fixedDelayString(); + if (StringUtils.hasText(fixedDelayString)) { + if (this.embeddedValueResolver != null) { + fixedDelayString = this.embeddedValueResolver.resolveStringValue(fixedDelayString); + } + if (StringUtils.hasLength(fixedDelayString)) { + Assert.isTrue(!processedSchedule, errorMessage); + processedSchedule = true; + try { + fixedDelay = parseDelayAsLong(fixedDelayString); + } + catch (RuntimeException ex) { + throw new IllegalArgumentException( + "Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into long"); + } + tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay))); + } + } + + // Check fixed rate + long fixedRate = scheduled.fixedRate(); + if (fixedRate >= 0) { + Assert.isTrue(!processedSchedule, errorMessage); + processedSchedule = true; + tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay))); + } + String fixedRateString = scheduled.fixedRateString(); + if (StringUtils.hasText(fixedRateString)) { + if (this.embeddedValueResolver != null) { + fixedRateString = this.embeddedValueResolver.resolveStringValue(fixedRateString); + } + if (StringUtils.hasLength(fixedRateString)) { + Assert.isTrue(!processedSchedule, errorMessage); + processedSchedule = true; + try { + fixedRate = parseDelayAsLong(fixedRateString); + } + catch (RuntimeException ex) { + throw new IllegalArgumentException( + "Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into long"); + } + tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay))); + } + } + + // Check whether we had any attribute set + Assert.isTrue(processedSchedule, errorMessage); + + // Finally register the scheduled tasks + synchronized (this.scheduledTasks) { + Set regTasks = this.scheduledTasks.computeIfAbsent(bean, key -> new LinkedHashSet<>(4)); + regTasks.addAll(tasks); + } + } + catch (IllegalArgumentException ex) { + throw new IllegalStateException( + "Encountered invalid @Scheduled method '" + method.getName() + "': " + ex.getMessage()); + } + } + + /** + * Create a {@link Runnable} for the given bean instance, + * calling the specified scheduled method. + *

    The default implementation creates a {@link ScheduledMethodRunnable}. + * @param target the target bean instance + * @param method the scheduled method to call + * @since 5.1 + * @see ScheduledMethodRunnable#ScheduledMethodRunnable(Object, Method) + */ + protected Runnable createRunnable(Object target, Method method) { + Assert.isTrue(method.getParameterCount() == 0, "Only no-arg methods may be annotated with @Scheduled"); + Method invocableMethod = AopUtils.selectInvocableMethod(method, target.getClass()); + return new ScheduledMethodRunnable(target, invocableMethod); + } + + private static long parseDelayAsLong(String value) throws RuntimeException { + if (value.length() > 1 && (isP(value.charAt(0)) || isP(value.charAt(1)))) { + return Duration.parse(value).toMillis(); + } + return Long.parseLong(value); + } + + private static boolean isP(char ch) { + return (ch == 'P' || ch == 'p'); + } + + + /** + * Return all currently scheduled tasks, from {@link Scheduled} methods + * as well as from programmatic {@link SchedulingConfigurer} interaction. + * @since 5.0.2 + */ + @Override + public Set getScheduledTasks() { + Set result = new LinkedHashSet<>(); + synchronized (this.scheduledTasks) { + Collection> allTasks = this.scheduledTasks.values(); + for (Set tasks : allTasks) { + result.addAll(tasks); + } + } + result.addAll(this.registrar.getScheduledTasks()); + return result; + } + + @Override + public void postProcessBeforeDestruction(Object bean, String beanName) { + Set tasks; + synchronized (this.scheduledTasks) { + tasks = this.scheduledTasks.remove(bean); + } + if (tasks != null) { + for (ScheduledTask task : tasks) { + task.cancel(); + } + } + } + + @Override + public boolean requiresDestruction(Object bean) { + synchronized (this.scheduledTasks) { + return this.scheduledTasks.containsKey(bean); + } + } + + @Override + public void destroy() { + synchronized (this.scheduledTasks) { + Collection> allTasks = this.scheduledTasks.values(); + for (Set tasks : allTasks) { + for (ScheduledTask task : tasks) { + task.cancel(); + } + } + this.scheduledTasks.clear(); + } + this.registrar.destroy(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/Schedules.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/Schedules.java new file mode 100644 index 0000000..fa8e970 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/Schedules.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Container annotation that aggregates several {@link Scheduled} annotations. + * + *

    Can be used natively, declaring several nested {@link Scheduled} annotations. + * Can also be used in conjunction with Java 8's support for repeatable annotations, + * where {@link Scheduled} can simply be declared several times on the same method, + * implicitly generating this container annotation. + * + *

    This annotation may be used as a meta-annotation to create custom + * composed annotations. + * + * @author Juergen Hoeller + * @since 4.0 + * @see Scheduled + */ +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Schedules { + + Scheduled[] value(); + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/SchedulingConfiguration.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/SchedulingConfiguration.java new file mode 100644 index 0000000..e02622c --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/SchedulingConfiguration.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.annotation; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.scheduling.config.TaskManagementConfigUtils; + +/** + * {@code @Configuration} class that registers a {@link ScheduledAnnotationBeanPostProcessor} + * bean capable of processing Spring's @{@link Scheduled} annotation. + * + *

    This configuration class is automatically imported when using the + * {@link EnableScheduling @EnableScheduling} annotation. See + * {@code @EnableScheduling}'s javadoc for complete usage details. + * + * @author Chris Beams + * @since 3.1 + * @see EnableScheduling + * @see ScheduledAnnotationBeanPostProcessor + */ +@Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +public class SchedulingConfiguration { + + @Bean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME) + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() { + return new ScheduledAnnotationBeanPostProcessor(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/SchedulingConfigurer.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/SchedulingConfigurer.java new file mode 100644 index 0000000..05283e3 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/SchedulingConfigurer.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.annotation; + +import org.springframework.scheduling.config.ScheduledTaskRegistrar; + +/** + * Optional interface to be implemented by @{@link + * org.springframework.context.annotation.Configuration Configuration} classes annotated + * with @{@link EnableScheduling}. Typically used for setting a specific + * {@link org.springframework.scheduling.TaskScheduler TaskScheduler} bean to be used when + * executing scheduled tasks or for registering scheduled tasks in a programmatic + * fashion as opposed to the declarative approach of using the @{@link Scheduled} + * annotation. For example, this may be necessary when implementing {@link + * org.springframework.scheduling.Trigger Trigger}-based tasks, which are not supported by + * the {@code @Scheduled} annotation. + * + *

    See @{@link EnableScheduling} for detailed usage examples. + * + * @author Chris Beams + * @since 3.1 + * @see EnableScheduling + * @see ScheduledTaskRegistrar + */ +@FunctionalInterface +public interface SchedulingConfigurer { + + /** + * Callback allowing a {@link org.springframework.scheduling.TaskScheduler + * TaskScheduler} and specific {@link org.springframework.scheduling.config.Task Task} + * instances to be registered against the given the {@link ScheduledTaskRegistrar}. + * @param taskRegistrar the registrar to be configured. + */ + void configureTasks(ScheduledTaskRegistrar taskRegistrar); + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/package-info.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/package-info.java new file mode 100644 index 0000000..e6fbc6e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/package-info.java @@ -0,0 +1,9 @@ +/** + * Java 5 annotation for asynchronous method execution. + */ +@NonNullApi +@NonNullFields +package org.springframework.scheduling.annotation; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java new file mode 100644 index 0000000..0b976d3 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java @@ -0,0 +1,251 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.concurrent; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import javax.enterprise.concurrent.ManagedExecutors; +import javax.enterprise.concurrent.ManagedTask; + +import org.springframework.core.task.AsyncListenableTaskExecutor; +import org.springframework.core.task.TaskDecorator; +import org.springframework.core.task.support.TaskExecutorAdapter; +import org.springframework.lang.Nullable; +import org.springframework.scheduling.SchedulingAwareRunnable; +import org.springframework.scheduling.SchedulingTaskExecutor; +import org.springframework.util.ClassUtils; +import org.springframework.util.concurrent.ListenableFuture; + +/** + * Adapter that takes a {@code java.util.concurrent.Executor} and exposes + * a Spring {@link org.springframework.core.task.TaskExecutor} for it. + * Also detects an extended {@code java.util.concurrent.ExecutorService}, adapting + * the {@link org.springframework.core.task.AsyncTaskExecutor} interface accordingly. + * + *

    Autodetects a JSR-236 {@link javax.enterprise.concurrent.ManagedExecutorService} + * in order to expose {@link javax.enterprise.concurrent.ManagedTask} adapters for it, + * exposing a long-running hint based on {@link SchedulingAwareRunnable} and an identity + * name based on the given Runnable/Callable's {@code toString()}. For JSR-236 style + * lookup in a Java EE 7 environment, consider using {@link DefaultManagedTaskExecutor}. + * + *

    Note that there is a pre-built {@link ThreadPoolTaskExecutor} that allows + * for defining a {@link java.util.concurrent.ThreadPoolExecutor} in bean style, + * exposing it as a Spring {@link org.springframework.core.task.TaskExecutor} directly. + * This is a convenient alternative to a raw ThreadPoolExecutor definition with + * a separate definition of the present adapter class. + * + * @author Juergen Hoeller + * @since 2.0 + * @see java.util.concurrent.Executor + * @see java.util.concurrent.ExecutorService + * @see java.util.concurrent.ThreadPoolExecutor + * @see java.util.concurrent.Executors + * @see DefaultManagedTaskExecutor + * @see ThreadPoolTaskExecutor + */ +public class ConcurrentTaskExecutor implements AsyncListenableTaskExecutor, SchedulingTaskExecutor { + + @Nullable + private static Class managedExecutorServiceClass; + + static { + try { + managedExecutorServiceClass = ClassUtils.forName( + "javax.enterprise.concurrent.ManagedExecutorService", + ConcurrentTaskScheduler.class.getClassLoader()); + } + catch (ClassNotFoundException ex) { + // JSR-236 API not available... + managedExecutorServiceClass = null; + } + } + + private Executor concurrentExecutor; + + private TaskExecutorAdapter adaptedExecutor; + + + /** + * Create a new ConcurrentTaskExecutor, using a single thread executor as default. + * @see java.util.concurrent.Executors#newSingleThreadExecutor() + */ + public ConcurrentTaskExecutor() { + this.concurrentExecutor = Executors.newSingleThreadExecutor(); + this.adaptedExecutor = new TaskExecutorAdapter(this.concurrentExecutor); + } + + /** + * Create a new ConcurrentTaskExecutor, using the given {@link java.util.concurrent.Executor}. + *

    Autodetects a JSR-236 {@link javax.enterprise.concurrent.ManagedExecutorService} + * in order to expose {@link javax.enterprise.concurrent.ManagedTask} adapters for it. + * @param executor the {@link java.util.concurrent.Executor} to delegate to + */ + public ConcurrentTaskExecutor(@Nullable Executor executor) { + this.concurrentExecutor = (executor != null ? executor : Executors.newSingleThreadExecutor()); + this.adaptedExecutor = getAdaptedExecutor(this.concurrentExecutor); + } + + + /** + * Specify the {@link java.util.concurrent.Executor} to delegate to. + *

    Autodetects a JSR-236 {@link javax.enterprise.concurrent.ManagedExecutorService} + * in order to expose {@link javax.enterprise.concurrent.ManagedTask} adapters for it. + */ + public final void setConcurrentExecutor(@Nullable Executor executor) { + this.concurrentExecutor = (executor != null ? executor : Executors.newSingleThreadExecutor()); + this.adaptedExecutor = getAdaptedExecutor(this.concurrentExecutor); + } + + /** + * Return the {@link java.util.concurrent.Executor} that this adapter delegates to. + */ + public final Executor getConcurrentExecutor() { + return this.concurrentExecutor; + } + + /** + * Specify a custom {@link TaskDecorator} to be applied to any {@link Runnable} + * about to be executed. + *

    Note that such a decorator is not necessarily being applied to the + * user-supplied {@code Runnable}/{@code Callable} but rather to the actual + * execution callback (which may be a wrapper around the user-supplied task). + *

    The primary use case is to set some execution context around the task's + * invocation, or to provide some monitoring/statistics for task execution. + *

    NOTE: Exception handling in {@code TaskDecorator} implementations + * is limited to plain {@code Runnable} execution via {@code execute} calls. + * In case of {@code #submit} calls, the exposed {@code Runnable} will be a + * {@code FutureTask} which does not propagate any exceptions; you might + * have to cast it and call {@code Future#get} to evaluate exceptions. + * @since 4.3 + */ + public final void setTaskDecorator(TaskDecorator taskDecorator) { + this.adaptedExecutor.setTaskDecorator(taskDecorator); + } + + + @Override + public void execute(Runnable task) { + this.adaptedExecutor.execute(task); + } + + @Override + public void execute(Runnable task, long startTimeout) { + this.adaptedExecutor.execute(task, startTimeout); + } + + @Override + public Future submit(Runnable task) { + return this.adaptedExecutor.submit(task); + } + + @Override + public Future submit(Callable task) { + return this.adaptedExecutor.submit(task); + } + + @Override + public ListenableFuture submitListenable(Runnable task) { + return this.adaptedExecutor.submitListenable(task); + } + + @Override + public ListenableFuture submitListenable(Callable task) { + return this.adaptedExecutor.submitListenable(task); + } + + + private static TaskExecutorAdapter getAdaptedExecutor(Executor concurrentExecutor) { + if (managedExecutorServiceClass != null && managedExecutorServiceClass.isInstance(concurrentExecutor)) { + return new ManagedTaskExecutorAdapter(concurrentExecutor); + } + return new TaskExecutorAdapter(concurrentExecutor); + } + + + /** + * TaskExecutorAdapter subclass that wraps all provided Runnables and Callables + * with a JSR-236 ManagedTask, exposing a long-running hint based on + * {@link SchedulingAwareRunnable} and an identity name based on the task's + * {@code toString()} representation. + */ + private static class ManagedTaskExecutorAdapter extends TaskExecutorAdapter { + + public ManagedTaskExecutorAdapter(Executor concurrentExecutor) { + super(concurrentExecutor); + } + + @Override + public void execute(Runnable task) { + super.execute(ManagedTaskBuilder.buildManagedTask(task, task.toString())); + } + + @Override + public Future submit(Runnable task) { + return super.submit(ManagedTaskBuilder.buildManagedTask(task, task.toString())); + } + + @Override + public Future submit(Callable task) { + return super.submit(ManagedTaskBuilder.buildManagedTask(task, task.toString())); + } + + @Override + public ListenableFuture submitListenable(Runnable task) { + return super.submitListenable(ManagedTaskBuilder.buildManagedTask(task, task.toString())); + } + + @Override + public ListenableFuture submitListenable(Callable task) { + return super.submitListenable(ManagedTaskBuilder.buildManagedTask(task, task.toString())); + } + } + + + /** + * Delegate that wraps a given Runnable/Callable with a JSR-236 ManagedTask, + * exposing a long-running hint based on {@link SchedulingAwareRunnable} + * and a given identity name. + */ + protected static class ManagedTaskBuilder { + + public static Runnable buildManagedTask(Runnable task, String identityName) { + Map properties; + if (task instanceof SchedulingAwareRunnable) { + properties = new HashMap<>(4); + properties.put(ManagedTask.LONGRUNNING_HINT, + Boolean.toString(((SchedulingAwareRunnable) task).isLongLived())); + } + else { + properties = new HashMap<>(2); + } + properties.put(ManagedTask.IDENTITY_NAME, identityName); + return ManagedExecutors.managedTask(task, properties, null); + } + + public static Callable buildManagedTask(Callable task, String identityName) { + Map properties = new HashMap<>(2); + properties.put(ManagedTask.IDENTITY_NAME, identityName); + return ManagedExecutors.managedTask(task, properties, null); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskScheduler.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskScheduler.java new file mode 100644 index 0000000..887c3f5 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskScheduler.java @@ -0,0 +1,294 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.concurrent; + +import java.time.Clock; +import java.util.Date; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import javax.enterprise.concurrent.LastExecution; +import javax.enterprise.concurrent.ManagedScheduledExecutorService; + +import org.springframework.core.task.TaskRejectedException; +import org.springframework.lang.Nullable; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.Trigger; +import org.springframework.scheduling.support.SimpleTriggerContext; +import org.springframework.scheduling.support.TaskUtils; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ErrorHandler; + +/** + * Adapter that takes a {@code java.util.concurrent.ScheduledExecutorService} and + * exposes a Spring {@link org.springframework.scheduling.TaskScheduler} for it. + * Extends {@link ConcurrentTaskExecutor} in order to implement the + * {@link org.springframework.scheduling.SchedulingTaskExecutor} interface as well. + * + *

    Autodetects a JSR-236 {@link javax.enterprise.concurrent.ManagedScheduledExecutorService} + * in order to use it for trigger-based scheduling if possible, instead of Spring's + * local trigger management which ends up delegating to regular delay-based scheduling + * against the {@code java.util.concurrent.ScheduledExecutorService} API. For JSR-236 style + * lookup in a Java EE 7 environment, consider using {@link DefaultManagedTaskScheduler}. + * + *

    Note that there is a pre-built {@link ThreadPoolTaskScheduler} that allows for + * defining a {@link java.util.concurrent.ScheduledThreadPoolExecutor} in bean style, + * exposing it as a Spring {@link org.springframework.scheduling.TaskScheduler} directly. + * This is a convenient alternative to a raw ScheduledThreadPoolExecutor definition with + * a separate definition of the present adapter class. + * + * @author Juergen Hoeller + * @author Mark Fisher + * @since 3.0 + * @see java.util.concurrent.ScheduledExecutorService + * @see java.util.concurrent.ScheduledThreadPoolExecutor + * @see java.util.concurrent.Executors + * @see DefaultManagedTaskScheduler + * @see ThreadPoolTaskScheduler + */ +public class ConcurrentTaskScheduler extends ConcurrentTaskExecutor implements TaskScheduler { + + @Nullable + private static Class managedScheduledExecutorServiceClass; + + static { + try { + managedScheduledExecutorServiceClass = ClassUtils.forName( + "javax.enterprise.concurrent.ManagedScheduledExecutorService", + ConcurrentTaskScheduler.class.getClassLoader()); + } + catch (ClassNotFoundException ex) { + // JSR-236 API not available... + managedScheduledExecutorServiceClass = null; + } + } + + + private ScheduledExecutorService scheduledExecutor; + + private boolean enterpriseConcurrentScheduler = false; + + @Nullable + private ErrorHandler errorHandler; + + private Clock clock = Clock.systemDefaultZone(); + + + /** + * Create a new ConcurrentTaskScheduler, + * using a single thread executor as default. + * @see java.util.concurrent.Executors#newSingleThreadScheduledExecutor() + */ + public ConcurrentTaskScheduler() { + super(); + this.scheduledExecutor = initScheduledExecutor(null); + } + + /** + * Create a new ConcurrentTaskScheduler, using the given + * {@link java.util.concurrent.ScheduledExecutorService} as shared delegate. + *

    Autodetects a JSR-236 {@link javax.enterprise.concurrent.ManagedScheduledExecutorService} + * in order to use it for trigger-based scheduling if possible, + * instead of Spring's local trigger management. + * @param scheduledExecutor the {@link java.util.concurrent.ScheduledExecutorService} + * to delegate to for {@link org.springframework.scheduling.SchedulingTaskExecutor} + * as well as {@link TaskScheduler} invocations + */ + public ConcurrentTaskScheduler(ScheduledExecutorService scheduledExecutor) { + super(scheduledExecutor); + this.scheduledExecutor = initScheduledExecutor(scheduledExecutor); + } + + /** + * Create a new ConcurrentTaskScheduler, using the given {@link java.util.concurrent.Executor} + * and {@link java.util.concurrent.ScheduledExecutorService} as delegates. + *

    Autodetects a JSR-236 {@link javax.enterprise.concurrent.ManagedScheduledExecutorService} + * in order to use it for trigger-based scheduling if possible, + * instead of Spring's local trigger management. + * @param concurrentExecutor the {@link java.util.concurrent.Executor} to delegate to + * for {@link org.springframework.scheduling.SchedulingTaskExecutor} invocations + * @param scheduledExecutor the {@link java.util.concurrent.ScheduledExecutorService} + * to delegate to for {@link TaskScheduler} invocations + */ + public ConcurrentTaskScheduler(Executor concurrentExecutor, ScheduledExecutorService scheduledExecutor) { + super(concurrentExecutor); + this.scheduledExecutor = initScheduledExecutor(scheduledExecutor); + } + + + private ScheduledExecutorService initScheduledExecutor(@Nullable ScheduledExecutorService scheduledExecutor) { + if (scheduledExecutor != null) { + this.scheduledExecutor = scheduledExecutor; + this.enterpriseConcurrentScheduler = (managedScheduledExecutorServiceClass != null && + managedScheduledExecutorServiceClass.isInstance(scheduledExecutor)); + } + else { + this.scheduledExecutor = Executors.newSingleThreadScheduledExecutor(); + this.enterpriseConcurrentScheduler = false; + } + return this.scheduledExecutor; + } + + /** + * Specify the {@link java.util.concurrent.ScheduledExecutorService} to delegate to. + *

    Autodetects a JSR-236 {@link javax.enterprise.concurrent.ManagedScheduledExecutorService} + * in order to use it for trigger-based scheduling if possible, + * instead of Spring's local trigger management. + *

    Note: This will only apply to {@link TaskScheduler} invocations. + * If you want the given executor to apply to + * {@link org.springframework.scheduling.SchedulingTaskExecutor} invocations + * as well, pass the same executor reference to {@link #setConcurrentExecutor}. + * @see #setConcurrentExecutor + */ + public void setScheduledExecutor(@Nullable ScheduledExecutorService scheduledExecutor) { + initScheduledExecutor(scheduledExecutor); + } + + /** + * Provide an {@link ErrorHandler} strategy. + */ + public void setErrorHandler(ErrorHandler errorHandler) { + Assert.notNull(errorHandler, "ErrorHandler must not be null"); + this.errorHandler = errorHandler; + } + + /** + * Set the clock to use for scheduling purposes. + *

    The default clock is the system clock for the default time zone. + * @since 5.3 + * @see Clock#systemDefaultZone() + */ + public void setClock(Clock clock) { + this.clock = clock; + } + + @Override + public Clock getClock() { + return this.clock; + } + + + @Override + @Nullable + public ScheduledFuture schedule(Runnable task, Trigger trigger) { + try { + if (this.enterpriseConcurrentScheduler) { + return new EnterpriseConcurrentTriggerScheduler().schedule(decorateTask(task, true), trigger); + } + else { + ErrorHandler errorHandler = + (this.errorHandler != null ? this.errorHandler : TaskUtils.getDefaultErrorHandler(true)); + return new ReschedulingRunnable(task, trigger, this.clock, this.scheduledExecutor, errorHandler).schedule(); + } + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex); + } + } + + @Override + public ScheduledFuture schedule(Runnable task, Date startTime) { + long initialDelay = startTime.getTime() - this.clock.millis(); + try { + return this.scheduledExecutor.schedule(decorateTask(task, false), initialDelay, TimeUnit.MILLISECONDS); + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex); + } + } + + @Override + public ScheduledFuture scheduleAtFixedRate(Runnable task, Date startTime, long period) { + long initialDelay = startTime.getTime() - this.clock.millis(); + try { + return this.scheduledExecutor.scheduleAtFixedRate(decorateTask(task, true), initialDelay, period, TimeUnit.MILLISECONDS); + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex); + } + } + + @Override + public ScheduledFuture scheduleAtFixedRate(Runnable task, long period) { + try { + return this.scheduledExecutor.scheduleAtFixedRate(decorateTask(task, true), 0, period, TimeUnit.MILLISECONDS); + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex); + } + } + + @Override + public ScheduledFuture scheduleWithFixedDelay(Runnable task, Date startTime, long delay) { + long initialDelay = startTime.getTime() - this.clock.millis(); + try { + return this.scheduledExecutor.scheduleWithFixedDelay(decorateTask(task, true), initialDelay, delay, TimeUnit.MILLISECONDS); + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex); + } + } + + @Override + public ScheduledFuture scheduleWithFixedDelay(Runnable task, long delay) { + try { + return this.scheduledExecutor.scheduleWithFixedDelay(decorateTask(task, true), 0, delay, TimeUnit.MILLISECONDS); + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex); + } + } + + private Runnable decorateTask(Runnable task, boolean isRepeatingTask) { + Runnable result = TaskUtils.decorateTaskWithErrorHandler(task, this.errorHandler, isRepeatingTask); + if (this.enterpriseConcurrentScheduler) { + result = ManagedTaskBuilder.buildManagedTask(result, task.toString()); + } + return result; + } + + + /** + * Delegate that adapts a Spring Trigger to a JSR-236 Trigger. + * Separated into an inner class in order to avoid a hard dependency on the JSR-236 API. + */ + private class EnterpriseConcurrentTriggerScheduler { + + public ScheduledFuture schedule(Runnable task, final Trigger trigger) { + ManagedScheduledExecutorService executor = (ManagedScheduledExecutorService) scheduledExecutor; + return executor.schedule(task, new javax.enterprise.concurrent.Trigger() { + @Override + @Nullable + public Date getNextRunTime(@Nullable LastExecution le, Date taskScheduledTime) { + return (trigger.nextExecutionTime(le != null ? + new SimpleTriggerContext(le.getScheduledStart(), le.getRunStart(), le.getRunEnd()) : + new SimpleTriggerContext())); + } + @Override + public boolean skipRun(LastExecution lastExecution, Date scheduledRunTime) { + return false; + } + }); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/CustomizableThreadFactory.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/CustomizableThreadFactory.java new file mode 100644 index 0000000..8252c00 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/CustomizableThreadFactory.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.concurrent; + +import java.util.concurrent.ThreadFactory; + +import org.springframework.util.CustomizableThreadCreator; + +/** + * Implementation of the {@link java.util.concurrent.ThreadFactory} interface, + * allowing for customizing the created threads (name, priority, etc). + * + *

    See the base class {@link org.springframework.util.CustomizableThreadCreator} + * for details on the available configuration options. + * + * @author Juergen Hoeller + * @since 2.0.3 + * @see #setThreadNamePrefix + * @see #setThreadPriority + */ +@SuppressWarnings("serial") +public class CustomizableThreadFactory extends CustomizableThreadCreator implements ThreadFactory { + + /** + * Create a new CustomizableThreadFactory with default thread name prefix. + */ + public CustomizableThreadFactory() { + super(); + } + + /** + * Create a new CustomizableThreadFactory with the given thread name prefix. + * @param threadNamePrefix the prefix to use for the names of newly created threads + */ + public CustomizableThreadFactory(String threadNamePrefix) { + super(threadNamePrefix); + } + + + @Override + public Thread newThread(Runnable runnable) { + return createThread(runnable); + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/DefaultManagedAwareThreadFactory.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/DefaultManagedAwareThreadFactory.java new file mode 100644 index 0000000..ab18715 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/DefaultManagedAwareThreadFactory.java @@ -0,0 +1,127 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.concurrent; + +import java.util.Properties; +import java.util.concurrent.ThreadFactory; + +import javax.naming.NamingException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jndi.JndiLocatorDelegate; +import org.springframework.jndi.JndiTemplate; +import org.springframework.lang.Nullable; + +/** + * JNDI-based variant of {@link CustomizableThreadFactory}, performing a default lookup + * for JSR-236's "java:comp/DefaultManagedThreadFactory" in a Java EE 7 environment, + * falling back to the local {@link CustomizableThreadFactory} setup if not found. + * + *

    This is a convenient way to use managed threads when running in a Java EE 7 + * environment, simply using regular local threads otherwise - without conditional + * setup (i.e. without profiles). + * + *

    Note: This class is not strictly JSR-236 based; it can work with any regular + * {@link java.util.concurrent.ThreadFactory} that can be found in JNDI. Therefore, + * the default JNDI name "java:comp/DefaultManagedThreadFactory" can be customized + * through the {@link #setJndiName "jndiName"} bean property. + * + * @author Juergen Hoeller + * @since 4.0 + */ +@SuppressWarnings("serial") +public class DefaultManagedAwareThreadFactory extends CustomizableThreadFactory implements InitializingBean { + + protected final Log logger = LogFactory.getLog(getClass()); + + private JndiLocatorDelegate jndiLocator = new JndiLocatorDelegate(); + + @Nullable + private String jndiName = "java:comp/DefaultManagedThreadFactory"; + + @Nullable + private ThreadFactory threadFactory; + + + /** + * Set the JNDI template to use for JNDI lookups. + * @see org.springframework.jndi.JndiAccessor#setJndiTemplate + */ + public void setJndiTemplate(JndiTemplate jndiTemplate) { + this.jndiLocator.setJndiTemplate(jndiTemplate); + } + + /** + * Set the JNDI environment to use for JNDI lookups. + * @see org.springframework.jndi.JndiAccessor#setJndiEnvironment + */ + public void setJndiEnvironment(Properties jndiEnvironment) { + this.jndiLocator.setJndiEnvironment(jndiEnvironment); + } + + /** + * Set whether the lookup occurs in a Java EE container, i.e. if the prefix + * "java:comp/env/" needs to be added if the JNDI name doesn't already + * contain it. PersistenceAnnotationBeanPostProcessor's default is "true". + * @see org.springframework.jndi.JndiLocatorSupport#setResourceRef + */ + public void setResourceRef(boolean resourceRef) { + this.jndiLocator.setResourceRef(resourceRef); + } + + /** + * Specify a JNDI name of the {@link java.util.concurrent.ThreadFactory} to delegate to, + * replacing the default JNDI name "java:comp/DefaultManagedThreadFactory". + *

    This can either be a fully qualified JNDI name, or the JNDI name relative + * to the current environment naming context if "resourceRef" is set to "true". + * @see #setResourceRef + */ + public void setJndiName(String jndiName) { + this.jndiName = jndiName; + } + + @Override + public void afterPropertiesSet() throws NamingException { + if (this.jndiName != null) { + try { + this.threadFactory = this.jndiLocator.lookup(this.jndiName, ThreadFactory.class); + } + catch (NamingException ex) { + if (logger.isTraceEnabled()) { + logger.trace("Failed to retrieve [" + this.jndiName + "] from JNDI", ex); + } + logger.info("Could not find default managed thread factory in JNDI - " + + "proceeding with default local thread factory"); + } + } + } + + + @Override + public Thread newThread(Runnable runnable) { + if (this.threadFactory != null) { + return this.threadFactory.newThread(runnable); + } + else { + return super.newThread(runnable); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/DefaultManagedTaskExecutor.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/DefaultManagedTaskExecutor.java new file mode 100644 index 0000000..62cb3dd --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/DefaultManagedTaskExecutor.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.concurrent; + +import java.util.Properties; +import java.util.concurrent.Executor; + +import javax.naming.NamingException; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jndi.JndiLocatorDelegate; +import org.springframework.jndi.JndiTemplate; +import org.springframework.lang.Nullable; + +/** + * JNDI-based variant of {@link ConcurrentTaskExecutor}, performing a default lookup for + * JSR-236's "java:comp/DefaultManagedExecutorService" in a Java EE 7/8 environment. + * + *

    Note: This class is not strictly JSR-236 based; it can work with any regular + * {@link java.util.concurrent.Executor} that can be found in JNDI. + * The actual adapting to {@link javax.enterprise.concurrent.ManagedExecutorService} + * happens in the base class {@link ConcurrentTaskExecutor} itself. + * + * @author Juergen Hoeller + * @since 4.0 + * @see javax.enterprise.concurrent.ManagedExecutorService + */ +public class DefaultManagedTaskExecutor extends ConcurrentTaskExecutor implements InitializingBean { + + private final JndiLocatorDelegate jndiLocator = new JndiLocatorDelegate(); + + @Nullable + private String jndiName = "java:comp/DefaultManagedExecutorService"; + + + /** + * Set the JNDI template to use for JNDI lookups. + * @see org.springframework.jndi.JndiAccessor#setJndiTemplate + */ + public void setJndiTemplate(JndiTemplate jndiTemplate) { + this.jndiLocator.setJndiTemplate(jndiTemplate); + } + + /** + * Set the JNDI environment to use for JNDI lookups. + * @see org.springframework.jndi.JndiAccessor#setJndiEnvironment + */ + public void setJndiEnvironment(Properties jndiEnvironment) { + this.jndiLocator.setJndiEnvironment(jndiEnvironment); + } + + /** + * Set whether the lookup occurs in a Java EE container, i.e. if the prefix + * "java:comp/env/" needs to be added if the JNDI name doesn't already + * contain it. PersistenceAnnotationBeanPostProcessor's default is "true". + * @see org.springframework.jndi.JndiLocatorSupport#setResourceRef + */ + public void setResourceRef(boolean resourceRef) { + this.jndiLocator.setResourceRef(resourceRef); + } + + /** + * Specify a JNDI name of the {@link java.util.concurrent.Executor} to delegate to, + * replacing the default JNDI name "java:comp/DefaultManagedExecutorService". + *

    This can either be a fully qualified JNDI name, or the JNDI name relative + * to the current environment naming context if "resourceRef" is set to "true". + * @see #setConcurrentExecutor + * @see #setResourceRef + */ + public void setJndiName(String jndiName) { + this.jndiName = jndiName; + } + + @Override + public void afterPropertiesSet() throws NamingException { + if (this.jndiName != null) { + setConcurrentExecutor(this.jndiLocator.lookup(this.jndiName, Executor.class)); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/DefaultManagedTaskScheduler.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/DefaultManagedTaskScheduler.java new file mode 100644 index 0000000..133527e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/DefaultManagedTaskScheduler.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.concurrent; + +import java.util.Properties; +import java.util.concurrent.ScheduledExecutorService; + +import javax.naming.NamingException; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jndi.JndiLocatorDelegate; +import org.springframework.jndi.JndiTemplate; +import org.springframework.lang.Nullable; + +/** + * JNDI-based variant of {@link ConcurrentTaskScheduler}, performing a default lookup for + * JSR-236's "java:comp/DefaultManagedScheduledExecutorService" in a Java EE 7 environment. + * + *

    Note: This class is not strictly JSR-236 based; it can work with any regular + * {@link java.util.concurrent.ScheduledExecutorService} that can be found in JNDI. + * The actual adapting to {@link javax.enterprise.concurrent.ManagedScheduledExecutorService} + * happens in the base class {@link ConcurrentTaskScheduler} itself. + * + * @author Juergen Hoeller + * @since 4.0 + * @see javax.enterprise.concurrent.ManagedScheduledExecutorService + */ +public class DefaultManagedTaskScheduler extends ConcurrentTaskScheduler implements InitializingBean { + + private final JndiLocatorDelegate jndiLocator = new JndiLocatorDelegate(); + + @Nullable + private String jndiName = "java:comp/DefaultManagedScheduledExecutorService"; + + + /** + * Set the JNDI template to use for JNDI lookups. + * @see org.springframework.jndi.JndiAccessor#setJndiTemplate + */ + public void setJndiTemplate(JndiTemplate jndiTemplate) { + this.jndiLocator.setJndiTemplate(jndiTemplate); + } + + /** + * Set the JNDI environment to use for JNDI lookups. + * @see org.springframework.jndi.JndiAccessor#setJndiEnvironment + */ + public void setJndiEnvironment(Properties jndiEnvironment) { + this.jndiLocator.setJndiEnvironment(jndiEnvironment); + } + + /** + * Set whether the lookup occurs in a Java EE container, i.e. if the prefix + * "java:comp/env/" needs to be added if the JNDI name doesn't already + * contain it. PersistenceAnnotationBeanPostProcessor's default is "true". + * @see org.springframework.jndi.JndiLocatorSupport#setResourceRef + */ + public void setResourceRef(boolean resourceRef) { + this.jndiLocator.setResourceRef(resourceRef); + } + + /** + * Specify a JNDI name of the {@link java.util.concurrent.Executor} to delegate to, + * replacing the default JNDI name "java:comp/DefaultManagedScheduledExecutorService". + *

    This can either be a fully qualified JNDI name, or the JNDI name relative + * to the current environment naming context if "resourceRef" is set to "true". + * @see #setConcurrentExecutor + * @see #setResourceRef + */ + public void setJndiName(String jndiName) { + this.jndiName = jndiName; + } + + @Override + public void afterPropertiesSet() throws NamingException { + if (this.jndiName != null) { + ScheduledExecutorService executor = this.jndiLocator.lookup(this.jndiName, ScheduledExecutorService.class); + setConcurrentExecutor(executor); + setScheduledExecutor(executor); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java new file mode 100644 index 0000000..2aaab8c --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java @@ -0,0 +1,271 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.concurrent; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.RunnableFuture; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.lang.Nullable; + +/** + * Base class for setting up a {@link java.util.concurrent.ExecutorService} + * (typically a {@link java.util.concurrent.ThreadPoolExecutor} or + * {@link java.util.concurrent.ScheduledThreadPoolExecutor}). + * Defines common configuration settings and common lifecycle handling. + * + * @author Juergen Hoeller + * @since 3.0 + * @see java.util.concurrent.ExecutorService + * @see java.util.concurrent.Executors + * @see java.util.concurrent.ThreadPoolExecutor + * @see java.util.concurrent.ScheduledThreadPoolExecutor + */ +@SuppressWarnings("serial") +public abstract class ExecutorConfigurationSupport extends CustomizableThreadFactory + implements BeanNameAware, InitializingBean, DisposableBean { + + protected final Log logger = LogFactory.getLog(getClass()); + + private ThreadFactory threadFactory = this; + + private boolean threadNamePrefixSet = false; + + private RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.AbortPolicy(); + + private boolean waitForTasksToCompleteOnShutdown = false; + + private long awaitTerminationMillis = 0; + + @Nullable + private String beanName; + + @Nullable + private ExecutorService executor; + + + /** + * Set the ThreadFactory to use for the ExecutorService's thread pool. + * Default is the underlying ExecutorService's default thread factory. + *

    In a Java EE 7 or other managed environment with JSR-236 support, + * consider specifying a JNDI-located ManagedThreadFactory: by default, + * to be found at "java:comp/DefaultManagedThreadFactory". + * Use the "jee:jndi-lookup" namespace element in XML or the programmatic + * {@link org.springframework.jndi.JndiLocatorDelegate} for convenient lookup. + * Alternatively, consider using Spring's {@link DefaultManagedAwareThreadFactory} + * with its fallback to local threads in case of no managed thread factory found. + * @see java.util.concurrent.Executors#defaultThreadFactory() + * @see javax.enterprise.concurrent.ManagedThreadFactory + * @see DefaultManagedAwareThreadFactory + */ + public void setThreadFactory(@Nullable ThreadFactory threadFactory) { + this.threadFactory = (threadFactory != null ? threadFactory : this); + } + + @Override + public void setThreadNamePrefix(@Nullable String threadNamePrefix) { + super.setThreadNamePrefix(threadNamePrefix); + this.threadNamePrefixSet = true; + } + + /** + * Set the RejectedExecutionHandler to use for the ExecutorService. + * Default is the ExecutorService's default abort policy. + * @see java.util.concurrent.ThreadPoolExecutor.AbortPolicy + */ + public void setRejectedExecutionHandler(@Nullable RejectedExecutionHandler rejectedExecutionHandler) { + this.rejectedExecutionHandler = + (rejectedExecutionHandler != null ? rejectedExecutionHandler : new ThreadPoolExecutor.AbortPolicy()); + } + + /** + * Set whether to wait for scheduled tasks to complete on shutdown, + * not interrupting running tasks and executing all tasks in the queue. + *

    Default is "false", shutting down immediately through interrupting + * ongoing tasks and clearing the queue. Switch this flag to "true" if you + * prefer fully completed tasks at the expense of a longer shutdown phase. + *

    Note that Spring's container shutdown continues while ongoing tasks + * are being completed. If you want this executor to block and wait for the + * termination of tasks before the rest of the container continues to shut + * down - e.g. in order to keep up other resources that your tasks may need -, + * set the {@link #setAwaitTerminationSeconds "awaitTerminationSeconds"} + * property instead of or in addition to this property. + * @see java.util.concurrent.ExecutorService#shutdown() + * @see java.util.concurrent.ExecutorService#shutdownNow() + */ + public void setWaitForTasksToCompleteOnShutdown(boolean waitForJobsToCompleteOnShutdown) { + this.waitForTasksToCompleteOnShutdown = waitForJobsToCompleteOnShutdown; + } + + /** + * Set the maximum number of seconds that this executor is supposed to block + * on shutdown in order to wait for remaining tasks to complete their execution + * before the rest of the container continues to shut down. This is particularly + * useful if your remaining tasks are likely to need access to other resources + * that are also managed by the container. + *

    By default, this executor won't wait for the termination of tasks at all. + * It will either shut down immediately, interrupting ongoing tasks and clearing + * the remaining task queue - or, if the + * {@link #setWaitForTasksToCompleteOnShutdown "waitForTasksToCompleteOnShutdown"} + * flag has been set to {@code true}, it will continue to fully execute all + * ongoing tasks as well as all remaining tasks in the queue, in parallel to + * the rest of the container shutting down. + *

    In either case, if you specify an await-termination period using this property, + * this executor will wait for the given time (max) for the termination of tasks. + * As a rule of thumb, specify a significantly higher timeout here if you set + * "waitForTasksToCompleteOnShutdown" to {@code true} at the same time, + * since all remaining tasks in the queue will still get executed - in contrast + * to the default shutdown behavior where it's just about waiting for currently + * executing tasks that aren't reacting to thread interruption. + * @see #setAwaitTerminationMillis + * @see java.util.concurrent.ExecutorService#shutdown() + * @see java.util.concurrent.ExecutorService#awaitTermination + */ + public void setAwaitTerminationSeconds(int awaitTerminationSeconds) { + this.awaitTerminationMillis = awaitTerminationSeconds * 1000L; + } + + /** + * Variant of {@link #setAwaitTerminationSeconds} with millisecond precision. + * @since 5.2.4 + * @see #setAwaitTerminationSeconds + */ + public void setAwaitTerminationMillis(long awaitTerminationMillis) { + this.awaitTerminationMillis = awaitTerminationMillis; + } + + @Override + public void setBeanName(String name) { + this.beanName = name; + } + + + /** + * Calls {@code initialize()} after the container applied all property values. + * @see #initialize() + */ + @Override + public void afterPropertiesSet() { + initialize(); + } + + /** + * Set up the ExecutorService. + */ + public void initialize() { + if (logger.isInfoEnabled()) { + logger.info("Initializing ExecutorService" + (this.beanName != null ? " '" + this.beanName + "'" : "")); + } + if (!this.threadNamePrefixSet && this.beanName != null) { + setThreadNamePrefix(this.beanName + "-"); + } + this.executor = initializeExecutor(this.threadFactory, this.rejectedExecutionHandler); + } + + /** + * Create the target {@link java.util.concurrent.ExecutorService} instance. + * Called by {@code afterPropertiesSet}. + * @param threadFactory the ThreadFactory to use + * @param rejectedExecutionHandler the RejectedExecutionHandler to use + * @return a new ExecutorService instance + * @see #afterPropertiesSet() + */ + protected abstract ExecutorService initializeExecutor( + ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler); + + + /** + * Calls {@code shutdown} when the BeanFactory destroys + * the task executor instance. + * @see #shutdown() + */ + @Override + public void destroy() { + shutdown(); + } + + /** + * Perform a shutdown on the underlying ExecutorService. + * @see java.util.concurrent.ExecutorService#shutdown() + * @see java.util.concurrent.ExecutorService#shutdownNow() + */ + public void shutdown() { + if (logger.isInfoEnabled()) { + logger.info("Shutting down ExecutorService" + (this.beanName != null ? " '" + this.beanName + "'" : "")); + } + if (this.executor != null) { + if (this.waitForTasksToCompleteOnShutdown) { + this.executor.shutdown(); + } + else { + for (Runnable remainingTask : this.executor.shutdownNow()) { + cancelRemainingTask(remainingTask); + } + } + awaitTerminationIfNecessary(this.executor); + } + } + + /** + * Cancel the given remaining task which never commended execution, + * as returned from {@link ExecutorService#shutdownNow()}. + * @param task the task to cancel (typically a {@link RunnableFuture}) + * @since 5.0.5 + * @see #shutdown() + * @see RunnableFuture#cancel(boolean) + */ + protected void cancelRemainingTask(Runnable task) { + if (task instanceof Future) { + ((Future) task).cancel(true); + } + } + + /** + * Wait for the executor to terminate, according to the value of the + * {@link #setAwaitTerminationSeconds "awaitTerminationSeconds"} property. + */ + private void awaitTerminationIfNecessary(ExecutorService executor) { + if (this.awaitTerminationMillis > 0) { + try { + if (!executor.awaitTermination(this.awaitTerminationMillis, TimeUnit.MILLISECONDS)) { + if (logger.isWarnEnabled()) { + logger.warn("Timed out while waiting for executor" + + (this.beanName != null ? " '" + this.beanName + "'" : "") + " to terminate"); + } + } + } + catch (InterruptedException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Interrupted while waiting for executor" + + (this.beanName != null ? " '" + this.beanName + "'" : "") + " to terminate"); + } + Thread.currentThread().interrupt(); + } + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ForkJoinPoolFactoryBean.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ForkJoinPoolFactoryBean.java new file mode 100644 index 0000000..26b1165 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ForkJoinPoolFactoryBean.java @@ -0,0 +1,165 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.concurrent; + +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.TimeUnit; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.lang.Nullable; + +/** + * A Spring {@link FactoryBean} that builds and exposes a preconfigured {@link ForkJoinPool}. + * + * @author Juergen Hoeller + * @since 3.1 + */ +public class ForkJoinPoolFactoryBean implements FactoryBean, InitializingBean, DisposableBean { + + private boolean commonPool = false; + + private int parallelism = Runtime.getRuntime().availableProcessors(); + + private ForkJoinPool.ForkJoinWorkerThreadFactory threadFactory = ForkJoinPool.defaultForkJoinWorkerThreadFactory; + + @Nullable + private Thread.UncaughtExceptionHandler uncaughtExceptionHandler; + + private boolean asyncMode = false; + + private int awaitTerminationSeconds = 0; + + @Nullable + private ForkJoinPool forkJoinPool; + + + /** + * Set whether to expose JDK 8's 'common' {@link ForkJoinPool}. + *

    Default is "false", creating a local {@link ForkJoinPool} instance based on the + * {@link #setParallelism "parallelism"}, {@link #setThreadFactory "threadFactory"}, + * {@link #setUncaughtExceptionHandler "uncaughtExceptionHandler"} and + * {@link #setAsyncMode "asyncMode"} properties on this FactoryBean. + *

    NOTE: Setting this flag to "true" effectively ignores all other + * properties on this FactoryBean, reusing the shared common JDK {@link ForkJoinPool} + * instead. This is a fine choice on JDK 8 but does remove the application's ability + * to customize ForkJoinPool behavior, in particular the use of custom threads. + * @since 3.2 + * @see java.util.concurrent.ForkJoinPool#commonPool() + */ + public void setCommonPool(boolean commonPool) { + this.commonPool = commonPool; + } + + /** + * Specify the parallelism level. Default is {@link Runtime#availableProcessors()}. + */ + public void setParallelism(int parallelism) { + this.parallelism = parallelism; + } + + /** + * Set the factory for creating new ForkJoinWorkerThreads. + * Default is {@link ForkJoinPool#defaultForkJoinWorkerThreadFactory}. + */ + public void setThreadFactory(ForkJoinPool.ForkJoinWorkerThreadFactory threadFactory) { + this.threadFactory = threadFactory; + } + + /** + * Set the handler for internal worker threads that terminate due to unrecoverable errors + * encountered while executing tasks. Default is none. + */ + public void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler uncaughtExceptionHandler) { + this.uncaughtExceptionHandler = uncaughtExceptionHandler; + } + + /** + * Specify whether to establish a local first-in-first-out scheduling mode for forked tasks + * that are never joined. This mode (asyncMode = {@code true}) may be more appropriate + * than the default locally stack-based mode in applications in which worker threads only + * process event-style asynchronous tasks. Default is {@code false}. + */ + public void setAsyncMode(boolean asyncMode) { + this.asyncMode = asyncMode; + } + + /** + * Set the maximum number of seconds that this ForkJoinPool is supposed to block + * on shutdown in order to wait for remaining tasks to complete their execution + * before the rest of the container continues to shut down. This is particularly + * useful if your remaining tasks are likely to need access to other resources + * that are also managed by the container. + *

    By default, this ForkJoinPool won't wait for the termination of tasks at all. + * It will continue to fully execute all ongoing tasks as well as all remaining + * tasks in the queue, in parallel to the rest of the container shutting down. + * In contrast, if you specify an await-termination period using this property, + * this executor will wait for the given time (max) for the termination of tasks. + *

    Note that this feature works for the {@link #setCommonPool "commonPool"} + * mode as well. The underlying ForkJoinPool won't actually terminate in that + * case but will wait for all tasks to terminate. + * @see java.util.concurrent.ForkJoinPool#shutdown() + * @see java.util.concurrent.ForkJoinPool#awaitTermination + */ + public void setAwaitTerminationSeconds(int awaitTerminationSeconds) { + this.awaitTerminationSeconds = awaitTerminationSeconds; + } + + @Override + public void afterPropertiesSet() { + this.forkJoinPool = (this.commonPool ? ForkJoinPool.commonPool() : + new ForkJoinPool(this.parallelism, this.threadFactory, this.uncaughtExceptionHandler, this.asyncMode)); + } + + + @Override + @Nullable + public ForkJoinPool getObject() { + return this.forkJoinPool; + } + + @Override + public Class getObjectType() { + return ForkJoinPool.class; + } + + @Override + public boolean isSingleton() { + return true; + } + + + @Override + public void destroy() { + if (this.forkJoinPool != null) { + // Ignored for the common pool. + this.forkJoinPool.shutdown(); + + // Wait for all tasks to terminate - works for the common pool as well. + if (this.awaitTerminationSeconds > 0) { + try { + this.forkJoinPool.awaitTermination(this.awaitTerminationSeconds, TimeUnit.SECONDS); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ReschedulingRunnable.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ReschedulingRunnable.java new file mode 100644 index 0000000..ca79c80 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ReschedulingRunnable.java @@ -0,0 +1,164 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.concurrent; + +import java.time.Clock; +import java.util.Date; +import java.util.concurrent.Delayed; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.springframework.lang.Nullable; +import org.springframework.scheduling.Trigger; +import org.springframework.scheduling.support.DelegatingErrorHandlingRunnable; +import org.springframework.scheduling.support.SimpleTriggerContext; +import org.springframework.util.Assert; +import org.springframework.util.ErrorHandler; + +/** + * Internal adapter that reschedules an underlying {@link Runnable} according + * to the next execution time suggested by a given {@link Trigger}. + * + *

    Necessary because a native {@link ScheduledExecutorService} supports + * delay-driven execution only. The flexibility of the {@link Trigger} interface + * will be translated onto a delay for the next execution time (repeatedly). + * + * @author Juergen Hoeller + * @author Mark Fisher + * @since 3.0 + */ +class ReschedulingRunnable extends DelegatingErrorHandlingRunnable implements ScheduledFuture { + + private final Trigger trigger; + + private final SimpleTriggerContext triggerContext; + + private final ScheduledExecutorService executor; + + @Nullable + private ScheduledFuture currentFuture; + + @Nullable + private Date scheduledExecutionTime; + + private final Object triggerContextMonitor = new Object(); + + + public ReschedulingRunnable(Runnable delegate, Trigger trigger, Clock clock, + ScheduledExecutorService executor, ErrorHandler errorHandler) { + + super(delegate, errorHandler); + this.trigger = trigger; + this.triggerContext = new SimpleTriggerContext(clock); + this.executor = executor; + } + + + @Nullable + public ScheduledFuture schedule() { + synchronized (this.triggerContextMonitor) { + this.scheduledExecutionTime = this.trigger.nextExecutionTime(this.triggerContext); + if (this.scheduledExecutionTime == null) { + return null; + } + long initialDelay = this.scheduledExecutionTime.getTime() - this.triggerContext.getClock().millis(); + this.currentFuture = this.executor.schedule(this, initialDelay, TimeUnit.MILLISECONDS); + return this; + } + } + + private ScheduledFuture obtainCurrentFuture() { + Assert.state(this.currentFuture != null, "No scheduled future"); + return this.currentFuture; + } + + @Override + public void run() { + Date actualExecutionTime = new Date(this.triggerContext.getClock().millis()); + super.run(); + Date completionTime = new Date(this.triggerContext.getClock().millis()); + synchronized (this.triggerContextMonitor) { + Assert.state(this.scheduledExecutionTime != null, "No scheduled execution"); + this.triggerContext.update(this.scheduledExecutionTime, actualExecutionTime, completionTime); + if (!obtainCurrentFuture().isCancelled()) { + schedule(); + } + } + } + + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + synchronized (this.triggerContextMonitor) { + return obtainCurrentFuture().cancel(mayInterruptIfRunning); + } + } + + @Override + public boolean isCancelled() { + synchronized (this.triggerContextMonitor) { + return obtainCurrentFuture().isCancelled(); + } + } + + @Override + public boolean isDone() { + synchronized (this.triggerContextMonitor) { + return obtainCurrentFuture().isDone(); + } + } + + @Override + public Object get() throws InterruptedException, ExecutionException { + ScheduledFuture curr; + synchronized (this.triggerContextMonitor) { + curr = obtainCurrentFuture(); + } + return curr.get(); + } + + @Override + public Object get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + ScheduledFuture curr; + synchronized (this.triggerContextMonitor) { + curr = obtainCurrentFuture(); + } + return curr.get(timeout, unit); + } + + @Override + public long getDelay(TimeUnit unit) { + ScheduledFuture curr; + synchronized (this.triggerContextMonitor) { + curr = obtainCurrentFuture(); + } + return curr.getDelay(unit); + } + + @Override + public int compareTo(Delayed other) { + if (this == other) { + return 0; + } + long diff = getDelay(TimeUnit.MILLISECONDS) - other.getDelay(TimeUnit.MILLISECONDS); + return (diff == 0 ? 0 : ((diff < 0)? -1 : 1)); + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ScheduledExecutorFactoryBean.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ScheduledExecutorFactoryBean.java new file mode 100644 index 0000000..66103aa --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ScheduledExecutorFactoryBean.java @@ -0,0 +1,250 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.concurrent; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.lang.Nullable; +import org.springframework.scheduling.support.DelegatingErrorHandlingRunnable; +import org.springframework.scheduling.support.TaskUtils; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * {@link org.springframework.beans.factory.FactoryBean} that sets up + * a {@link java.util.concurrent.ScheduledExecutorService} + * (by default: a {@link java.util.concurrent.ScheduledThreadPoolExecutor}) + * and exposes it for bean references. + * + *

    Allows for registration of {@link ScheduledExecutorTask ScheduledExecutorTasks}, + * automatically starting the {@link ScheduledExecutorService} on initialization and + * cancelling it on destruction of the context. In scenarios that only require static + * registration of tasks at startup, there is no need to access the + * {@link ScheduledExecutorService} instance itself in application code at all; + * {@code ScheduledExecutorFactoryBean} is then just being used for lifecycle integration. + * + *

    For an alternative, you may set up a {@link ScheduledThreadPoolExecutor} instance + * directly using constructor injection, or use a factory method definition that points + * to the {@link java.util.concurrent.Executors} class. + * This is strongly recommended in particular for common {@code @Bean} methods in + * configuration classes, where this {@code FactoryBean} variant would force you to + * return the {@code FactoryBean} type instead of {@code ScheduledExecutorService}. + * + *

    Note that {@link java.util.concurrent.ScheduledExecutorService} + * uses a {@link Runnable} instance that is shared between repeated executions, + * in contrast to Quartz which instantiates a new Job for each execution. + * + *

    WARNING: {@link Runnable Runnables} submitted via a native + * {@link java.util.concurrent.ScheduledExecutorService} are removed from + * the execution schedule once they throw an exception. If you would prefer + * to continue execution after such an exception, switch this FactoryBean's + * {@link #setContinueScheduledExecutionAfterException "continueScheduledExecutionAfterException"} + * property to "true". + * + * @author Juergen Hoeller + * @since 2.0 + * @see #setPoolSize + * @see #setRemoveOnCancelPolicy + * @see #setThreadFactory + * @see ScheduledExecutorTask + * @see java.util.concurrent.ScheduledExecutorService + * @see java.util.concurrent.ScheduledThreadPoolExecutor + */ +@SuppressWarnings("serial") +public class ScheduledExecutorFactoryBean extends ExecutorConfigurationSupport + implements FactoryBean { + + private int poolSize = 1; + + @Nullable + private ScheduledExecutorTask[] scheduledExecutorTasks; + + private boolean removeOnCancelPolicy = false; + + private boolean continueScheduledExecutionAfterException = false; + + private boolean exposeUnconfigurableExecutor = false; + + @Nullable + private ScheduledExecutorService exposedExecutor; + + + /** + * Set the ScheduledExecutorService's pool size. + * Default is 1. + */ + public void setPoolSize(int poolSize) { + Assert.isTrue(poolSize > 0, "'poolSize' must be 1 or higher"); + this.poolSize = poolSize; + } + + /** + * Register a list of ScheduledExecutorTask objects with the ScheduledExecutorService + * that this FactoryBean creates. Depending on each ScheduledExecutorTask's settings, + * it will be registered via one of ScheduledExecutorService's schedule methods. + * @see java.util.concurrent.ScheduledExecutorService#schedule(java.lang.Runnable, long, java.util.concurrent.TimeUnit) + * @see java.util.concurrent.ScheduledExecutorService#scheduleWithFixedDelay(java.lang.Runnable, long, long, java.util.concurrent.TimeUnit) + * @see java.util.concurrent.ScheduledExecutorService#scheduleAtFixedRate(java.lang.Runnable, long, long, java.util.concurrent.TimeUnit) + */ + public void setScheduledExecutorTasks(ScheduledExecutorTask... scheduledExecutorTasks) { + this.scheduledExecutorTasks = scheduledExecutorTasks; + } + + /** + * Set the remove-on-cancel mode on {@link ScheduledThreadPoolExecutor}. + *

    Default is {@code false}. If set to {@code true}, the target executor will be + * switched into remove-on-cancel mode (if possible, with a soft fallback otherwise). + */ + public void setRemoveOnCancelPolicy(boolean removeOnCancelPolicy) { + this.removeOnCancelPolicy = removeOnCancelPolicy; + } + + /** + * Specify whether to continue the execution of a scheduled task + * after it threw an exception. + *

    Default is "false", matching the native behavior of a + * {@link java.util.concurrent.ScheduledExecutorService}. + * Switch this flag to "true" for exception-proof execution of each task, + * continuing scheduled execution as in the case of successful execution. + * @see java.util.concurrent.ScheduledExecutorService#scheduleAtFixedRate + */ + public void setContinueScheduledExecutionAfterException(boolean continueScheduledExecutionAfterException) { + this.continueScheduledExecutionAfterException = continueScheduledExecutionAfterException; + } + + /** + * Specify whether this FactoryBean should expose an unconfigurable + * decorator for the created executor. + *

    Default is "false", exposing the raw executor as bean reference. + * Switch this flag to "true" to strictly prevent clients from + * modifying the executor's configuration. + * @see java.util.concurrent.Executors#unconfigurableScheduledExecutorService + */ + public void setExposeUnconfigurableExecutor(boolean exposeUnconfigurableExecutor) { + this.exposeUnconfigurableExecutor = exposeUnconfigurableExecutor; + } + + + @Override + protected ExecutorService initializeExecutor( + ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) { + + ScheduledExecutorService executor = + createExecutor(this.poolSize, threadFactory, rejectedExecutionHandler); + + if (this.removeOnCancelPolicy) { + if (executor instanceof ScheduledThreadPoolExecutor) { + ((ScheduledThreadPoolExecutor) executor).setRemoveOnCancelPolicy(true); + } + else { + logger.debug("Could not apply remove-on-cancel policy - not a ScheduledThreadPoolExecutor"); + } + } + + // Register specified ScheduledExecutorTasks, if necessary. + if (!ObjectUtils.isEmpty(this.scheduledExecutorTasks)) { + registerTasks(this.scheduledExecutorTasks, executor); + } + + // Wrap executor with an unconfigurable decorator. + this.exposedExecutor = (this.exposeUnconfigurableExecutor ? + Executors.unconfigurableScheduledExecutorService(executor) : executor); + + return executor; + } + + /** + * Create a new {@link ScheduledExecutorService} instance. + *

    The default implementation creates a {@link ScheduledThreadPoolExecutor}. + * Can be overridden in subclasses to provide custom {@link ScheduledExecutorService} instances. + * @param poolSize the specified pool size + * @param threadFactory the ThreadFactory to use + * @param rejectedExecutionHandler the RejectedExecutionHandler to use + * @return a new ScheduledExecutorService instance + * @see #afterPropertiesSet() + * @see java.util.concurrent.ScheduledThreadPoolExecutor + */ + protected ScheduledExecutorService createExecutor( + int poolSize, ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) { + + return new ScheduledThreadPoolExecutor(poolSize, threadFactory, rejectedExecutionHandler); + } + + /** + * Register the specified {@link ScheduledExecutorTask ScheduledExecutorTasks} + * on the given {@link ScheduledExecutorService}. + * @param tasks the specified ScheduledExecutorTasks (never empty) + * @param executor the ScheduledExecutorService to register the tasks on. + */ + protected void registerTasks(ScheduledExecutorTask[] tasks, ScheduledExecutorService executor) { + for (ScheduledExecutorTask task : tasks) { + Runnable runnable = getRunnableToSchedule(task); + if (task.isOneTimeTask()) { + executor.schedule(runnable, task.getDelay(), task.getTimeUnit()); + } + else { + if (task.isFixedRate()) { + executor.scheduleAtFixedRate(runnable, task.getDelay(), task.getPeriod(), task.getTimeUnit()); + } + else { + executor.scheduleWithFixedDelay(runnable, task.getDelay(), task.getPeriod(), task.getTimeUnit()); + } + } + } + } + + /** + * Determine the actual Runnable to schedule for the given task. + *

    Wraps the task's Runnable in a + * {@link org.springframework.scheduling.support.DelegatingErrorHandlingRunnable} + * that will catch and log the Exception. If necessary, it will suppress the + * Exception according to the + * {@link #setContinueScheduledExecutionAfterException "continueScheduledExecutionAfterException"} + * flag. + * @param task the ScheduledExecutorTask to schedule + * @return the actual Runnable to schedule (may be a decorator) + */ + protected Runnable getRunnableToSchedule(ScheduledExecutorTask task) { + return (this.continueScheduledExecutionAfterException ? + new DelegatingErrorHandlingRunnable(task.getRunnable(), TaskUtils.LOG_AND_SUPPRESS_ERROR_HANDLER) : + new DelegatingErrorHandlingRunnable(task.getRunnable(), TaskUtils.LOG_AND_PROPAGATE_ERROR_HANDLER)); + } + + + @Override + @Nullable + public ScheduledExecutorService getObject() { + return this.exposedExecutor; + } + + @Override + public Class getObjectType() { + return (this.exposedExecutor != null ? this.exposedExecutor.getClass() : ScheduledExecutorService.class); + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ScheduledExecutorTask.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ScheduledExecutorTask.java new file mode 100644 index 0000000..7e55e5e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ScheduledExecutorTask.java @@ -0,0 +1,201 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.concurrent; + +import java.util.concurrent.TimeUnit; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * JavaBean that describes a scheduled executor task, consisting of the + * {@link Runnable} and a delay plus period. The period needs to be specified; + * there is no point in a default for it. + * + *

    The {@link java.util.concurrent.ScheduledExecutorService} does not offer + * more sophisticated scheduling options such as cron expressions. + * Consider using {@link ThreadPoolTaskScheduler} for such needs. + * + *

    Note that the {@link java.util.concurrent.ScheduledExecutorService} mechanism + * uses a {@link Runnable} instance that is shared between repeated executions, + * in contrast to Quartz which creates a new Job instance for each execution. + * + * @author Juergen Hoeller + * @since 2.0 + * @see java.util.concurrent.ScheduledExecutorService#scheduleWithFixedDelay(java.lang.Runnable, long, long, java.util.concurrent.TimeUnit) + * @see java.util.concurrent.ScheduledExecutorService#scheduleAtFixedRate(java.lang.Runnable, long, long, java.util.concurrent.TimeUnit) + */ +public class ScheduledExecutorTask { + + @Nullable + private Runnable runnable; + + private long delay = 0; + + private long period = -1; + + private TimeUnit timeUnit = TimeUnit.MILLISECONDS; + + private boolean fixedRate = false; + + + /** + * Create a new ScheduledExecutorTask, + * to be populated via bean properties. + * @see #setDelay + * @see #setPeriod + * @see #setFixedRate + */ + public ScheduledExecutorTask() { + } + + /** + * Create a new ScheduledExecutorTask, with default + * one-time execution without delay. + * @param executorTask the Runnable to schedule + */ + public ScheduledExecutorTask(Runnable executorTask) { + this.runnable = executorTask; + } + + /** + * Create a new ScheduledExecutorTask, with default + * one-time execution with the given delay. + * @param executorTask the Runnable to schedule + * @param delay the delay before starting the task for the first time (ms) + */ + public ScheduledExecutorTask(Runnable executorTask, long delay) { + this.runnable = executorTask; + this.delay = delay; + } + + /** + * Create a new ScheduledExecutorTask. + * @param executorTask the Runnable to schedule + * @param delay the delay before starting the task for the first time (ms) + * @param period the period between repeated task executions (ms) + * @param fixedRate whether to schedule as fixed-rate execution + */ + public ScheduledExecutorTask(Runnable executorTask, long delay, long period, boolean fixedRate) { + this.runnable = executorTask; + this.delay = delay; + this.period = period; + this.fixedRate = fixedRate; + } + + + /** + * Set the Runnable to schedule as executor task. + */ + public void setRunnable(Runnable executorTask) { + this.runnable = executorTask; + } + + /** + * Return the Runnable to schedule as executor task. + */ + public Runnable getRunnable() { + Assert.state(this.runnable != null, "No Runnable set"); + return this.runnable; + } + + /** + * Set the delay before starting the task for the first time, + * in milliseconds. Default is 0, immediately starting the + * task after successful scheduling. + */ + public void setDelay(long delay) { + this.delay = delay; + } + + /** + * Return the delay before starting the job for the first time. + */ + public long getDelay() { + return this.delay; + } + + /** + * Set the period between repeated task executions, in milliseconds. + *

    Default is -1, leading to one-time execution. In case of a positive value, + * the task will be executed repeatedly, with the given interval in-between executions. + *

    Note that the semantics of the period value vary between fixed-rate and + * fixed-delay execution. + *

    Note: A period of 0 (for example as fixed delay) is not supported, + * simply because {@code java.util.concurrent.ScheduledExecutorService} itself + * does not support it. Hence a value of 0 will be treated as one-time execution; + * however, that value should never be specified explicitly in the first place! + * @see #setFixedRate + * @see #isOneTimeTask() + * @see java.util.concurrent.ScheduledExecutorService#scheduleWithFixedDelay(Runnable, long, long, java.util.concurrent.TimeUnit) + */ + public void setPeriod(long period) { + this.period = period; + } + + /** + * Return the period between repeated task executions. + */ + public long getPeriod() { + return this.period; + } + + /** + * Is this task only ever going to execute once? + * @return {@code true} if this task is only ever going to execute once + * @see #getPeriod() + */ + public boolean isOneTimeTask() { + return (this.period <= 0); + } + + /** + * Specify the time unit for the delay and period values. + * Default is milliseconds ({@code TimeUnit.MILLISECONDS}). + * @see java.util.concurrent.TimeUnit#MILLISECONDS + * @see java.util.concurrent.TimeUnit#SECONDS + */ + public void setTimeUnit(@Nullable TimeUnit timeUnit) { + this.timeUnit = (timeUnit != null ? timeUnit : TimeUnit.MILLISECONDS); + } + + /** + * Return the time unit for the delay and period values. + */ + public TimeUnit getTimeUnit() { + return this.timeUnit; + } + + /** + * Set whether to schedule as fixed-rate execution, rather than + * fixed-delay execution. Default is "false", that is, fixed delay. + *

    See ScheduledExecutorService javadoc for details on those execution modes. + * @see java.util.concurrent.ScheduledExecutorService#scheduleWithFixedDelay(java.lang.Runnable, long, long, java.util.concurrent.TimeUnit) + * @see java.util.concurrent.ScheduledExecutorService#scheduleAtFixedRate(java.lang.Runnable, long, long, java.util.concurrent.TimeUnit) + */ + public void setFixedRate(boolean fixedRate) { + this.fixedRate = fixedRate; + } + + /** + * Return whether to schedule as fixed-rate execution. + */ + public boolean isFixedRate() { + return this.fixedRate; + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolExecutorFactoryBean.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolExecutorFactoryBean.java new file mode 100644 index 0000000..e880489 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolExecutorFactoryBean.java @@ -0,0 +1,220 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.concurrent; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.lang.Nullable; + +/** + * JavaBean that allows for configuring a {@link java.util.concurrent.ThreadPoolExecutor} + * in bean style (through its "corePoolSize", "maxPoolSize", "keepAliveSeconds", + * "queueCapacity" properties) and exposing it as a bean reference of its native + * {@link java.util.concurrent.ExecutorService} type. + * + *

    The default configuration is a core pool size of 1, with unlimited max pool size + * and unlimited queue capacity. This is roughly equivalent to + * {@link java.util.concurrent.Executors#newSingleThreadExecutor()}, sharing a single + * thread for all tasks. Setting {@link #setQueueCapacity "queueCapacity"} to 0 mimics + * {@link java.util.concurrent.Executors#newCachedThreadPool()}, with immediate scaling + * of threads in the pool to a potentially very high number. Consider also setting a + * {@link #setMaxPoolSize "maxPoolSize"} at that point, as well as possibly a higher + * {@link #setCorePoolSize "corePoolSize"} (see also the + * {@link #setAllowCoreThreadTimeOut "allowCoreThreadTimeOut"} mode of scaling). + * + *

    For an alternative, you may set up a {@link ThreadPoolExecutor} instance directly + * using constructor injection, or use a factory method definition that points to the + * {@link java.util.concurrent.Executors} class. + * This is strongly recommended in particular for common {@code @Bean} methods in + * configuration classes, where this {@code FactoryBean} variant would force you to + * return the {@code FactoryBean} type instead of the actual {@code Executor} type. + * + *

    If you need a timing-based {@link java.util.concurrent.ScheduledExecutorService} + * instead, consider {@link ScheduledExecutorFactoryBean}. + + * @author Juergen Hoeller + * @since 3.0 + * @see java.util.concurrent.ExecutorService + * @see java.util.concurrent.Executors + * @see java.util.concurrent.ThreadPoolExecutor + */ +@SuppressWarnings("serial") +public class ThreadPoolExecutorFactoryBean extends ExecutorConfigurationSupport + implements FactoryBean, InitializingBean, DisposableBean { + + private int corePoolSize = 1; + + private int maxPoolSize = Integer.MAX_VALUE; + + private int keepAliveSeconds = 60; + + private boolean allowCoreThreadTimeOut = false; + + private int queueCapacity = Integer.MAX_VALUE; + + private boolean exposeUnconfigurableExecutor = false; + + @Nullable + private ExecutorService exposedExecutor; + + + /** + * Set the ThreadPoolExecutor's core pool size. + * Default is 1. + */ + public void setCorePoolSize(int corePoolSize) { + this.corePoolSize = corePoolSize; + } + + /** + * Set the ThreadPoolExecutor's maximum pool size. + * Default is {@code Integer.MAX_VALUE}. + */ + public void setMaxPoolSize(int maxPoolSize) { + this.maxPoolSize = maxPoolSize; + } + + /** + * Set the ThreadPoolExecutor's keep-alive seconds. + * Default is 60. + */ + public void setKeepAliveSeconds(int keepAliveSeconds) { + this.keepAliveSeconds = keepAliveSeconds; + } + + /** + * Specify whether to allow core threads to time out. This enables dynamic + * growing and shrinking even in combination with a non-zero queue (since + * the max pool size will only grow once the queue is full). + *

    Default is "false". + * @see java.util.concurrent.ThreadPoolExecutor#allowCoreThreadTimeOut(boolean) + */ + public void setAllowCoreThreadTimeOut(boolean allowCoreThreadTimeOut) { + this.allowCoreThreadTimeOut = allowCoreThreadTimeOut; + } + + /** + * Set the capacity for the ThreadPoolExecutor's BlockingQueue. + * Default is {@code Integer.MAX_VALUE}. + *

    Any positive value will lead to a LinkedBlockingQueue instance; + * any other value will lead to a SynchronousQueue instance. + * @see java.util.concurrent.LinkedBlockingQueue + * @see java.util.concurrent.SynchronousQueue + */ + public void setQueueCapacity(int queueCapacity) { + this.queueCapacity = queueCapacity; + } + + /** + * Specify whether this FactoryBean should expose an unconfigurable + * decorator for the created executor. + *

    Default is "false", exposing the raw executor as bean reference. + * Switch this flag to "true" to strictly prevent clients from + * modifying the executor's configuration. + * @see java.util.concurrent.Executors#unconfigurableExecutorService + */ + public void setExposeUnconfigurableExecutor(boolean exposeUnconfigurableExecutor) { + this.exposeUnconfigurableExecutor = exposeUnconfigurableExecutor; + } + + + @Override + protected ExecutorService initializeExecutor( + ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) { + + BlockingQueue queue = createQueue(this.queueCapacity); + ThreadPoolExecutor executor = createExecutor(this.corePoolSize, this.maxPoolSize, + this.keepAliveSeconds, queue, threadFactory, rejectedExecutionHandler); + if (this.allowCoreThreadTimeOut) { + executor.allowCoreThreadTimeOut(true); + } + + // Wrap executor with an unconfigurable decorator. + this.exposedExecutor = (this.exposeUnconfigurableExecutor ? + Executors.unconfigurableExecutorService(executor) : executor); + + return executor; + } + + /** + * Create a new instance of {@link ThreadPoolExecutor} or a subclass thereof. + *

    The default implementation creates a standard {@link ThreadPoolExecutor}. + * Can be overridden to provide custom {@link ThreadPoolExecutor} subclasses. + * @param corePoolSize the specified core pool size + * @param maxPoolSize the specified maximum pool size + * @param keepAliveSeconds the specified keep-alive time in seconds + * @param queue the BlockingQueue to use + * @param threadFactory the ThreadFactory to use + * @param rejectedExecutionHandler the RejectedExecutionHandler to use + * @return a new ThreadPoolExecutor instance + * @see #afterPropertiesSet() + */ + protected ThreadPoolExecutor createExecutor( + int corePoolSize, int maxPoolSize, int keepAliveSeconds, BlockingQueue queue, + ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) { + + return new ThreadPoolExecutor(corePoolSize, maxPoolSize, + keepAliveSeconds, TimeUnit.SECONDS, queue, threadFactory, rejectedExecutionHandler); + } + + /** + * Create the BlockingQueue to use for the ThreadPoolExecutor. + *

    A LinkedBlockingQueue instance will be created for a positive + * capacity value; a SynchronousQueue else. + * @param queueCapacity the specified queue capacity + * @return the BlockingQueue instance + * @see java.util.concurrent.LinkedBlockingQueue + * @see java.util.concurrent.SynchronousQueue + */ + protected BlockingQueue createQueue(int queueCapacity) { + if (queueCapacity > 0) { + return new LinkedBlockingQueue<>(queueCapacity); + } + else { + return new SynchronousQueue<>(); + } + } + + + @Override + @Nullable + public ExecutorService getObject() { + return this.exposedExecutor; + } + + @Override + public Class getObjectType() { + return (this.exposedExecutor != null ? this.exposedExecutor.getClass() : ExecutorService.class); + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.java new file mode 100644 index 0000000..f5e9edf --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.java @@ -0,0 +1,391 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.concurrent; + +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import org.springframework.core.task.AsyncListenableTaskExecutor; +import org.springframework.core.task.TaskDecorator; +import org.springframework.core.task.TaskRejectedException; +import org.springframework.lang.Nullable; +import org.springframework.scheduling.SchedulingTaskExecutor; +import org.springframework.util.Assert; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.util.concurrent.ListenableFutureTask; + +/** + * JavaBean that allows for configuring a {@link java.util.concurrent.ThreadPoolExecutor} + * in bean style (through its "corePoolSize", "maxPoolSize", "keepAliveSeconds", "queueCapacity" + * properties) and exposing it as a Spring {@link org.springframework.core.task.TaskExecutor}. + * This class is also well suited for management and monitoring (e.g. through JMX), + * providing several useful attributes: "corePoolSize", "maxPoolSize", "keepAliveSeconds" + * (all supporting updates at runtime); "poolSize", "activeCount" (for introspection only). + * + *

    The default configuration is a core pool size of 1, with unlimited max pool size + * and unlimited queue capacity. This is roughly equivalent to + * {@link java.util.concurrent.Executors#newSingleThreadExecutor()}, sharing a single + * thread for all tasks. Setting {@link #setQueueCapacity "queueCapacity"} to 0 mimics + * {@link java.util.concurrent.Executors#newCachedThreadPool()}, with immediate scaling + * of threads in the pool to a potentially very high number. Consider also setting a + * {@link #setMaxPoolSize "maxPoolSize"} at that point, as well as possibly a higher + * {@link #setCorePoolSize "corePoolSize"} (see also the + * {@link #setAllowCoreThreadTimeOut "allowCoreThreadTimeOut"} mode of scaling). + * + *

    NOTE: This class implements Spring's + * {@link org.springframework.core.task.TaskExecutor} interface as well as the + * {@link java.util.concurrent.Executor} interface, with the former being the primary + * interface, the other just serving as secondary convenience. For this reason, the + * exception handling follows the TaskExecutor contract rather than the Executor contract, + * in particular regarding the {@link org.springframework.core.task.TaskRejectedException}. + * + *

    For an alternative, you may set up a ThreadPoolExecutor instance directly using + * constructor injection, or use a factory method definition that points to the + * {@link java.util.concurrent.Executors} class. To expose such a raw Executor as a + * Spring {@link org.springframework.core.task.TaskExecutor}, simply wrap it with a + * {@link org.springframework.scheduling.concurrent.ConcurrentTaskExecutor} adapter. + * + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.core.task.TaskExecutor + * @see java.util.concurrent.ThreadPoolExecutor + * @see ThreadPoolExecutorFactoryBean + * @see ConcurrentTaskExecutor + */ +@SuppressWarnings("serial") +public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport + implements AsyncListenableTaskExecutor, SchedulingTaskExecutor { + + private final Object poolSizeMonitor = new Object(); + + private int corePoolSize = 1; + + private int maxPoolSize = Integer.MAX_VALUE; + + private int keepAliveSeconds = 60; + + private int queueCapacity = Integer.MAX_VALUE; + + private boolean allowCoreThreadTimeOut = false; + + @Nullable + private TaskDecorator taskDecorator; + + @Nullable + private ThreadPoolExecutor threadPoolExecutor; + + // Runnable decorator to user-level FutureTask, if different + private final Map decoratedTaskMap = + new ConcurrentReferenceHashMap<>(16, ConcurrentReferenceHashMap.ReferenceType.WEAK); + + + /** + * Set the ThreadPoolExecutor's core pool size. + * Default is 1. + *

    This setting can be modified at runtime, for example through JMX. + */ + public void setCorePoolSize(int corePoolSize) { + synchronized (this.poolSizeMonitor) { + this.corePoolSize = corePoolSize; + if (this.threadPoolExecutor != null) { + this.threadPoolExecutor.setCorePoolSize(corePoolSize); + } + } + } + + /** + * Return the ThreadPoolExecutor's core pool size. + */ + public int getCorePoolSize() { + synchronized (this.poolSizeMonitor) { + return this.corePoolSize; + } + } + + /** + * Set the ThreadPoolExecutor's maximum pool size. + * Default is {@code Integer.MAX_VALUE}. + *

    This setting can be modified at runtime, for example through JMX. + */ + public void setMaxPoolSize(int maxPoolSize) { + synchronized (this.poolSizeMonitor) { + this.maxPoolSize = maxPoolSize; + if (this.threadPoolExecutor != null) { + this.threadPoolExecutor.setMaximumPoolSize(maxPoolSize); + } + } + } + + /** + * Return the ThreadPoolExecutor's maximum pool size. + */ + public int getMaxPoolSize() { + synchronized (this.poolSizeMonitor) { + return this.maxPoolSize; + } + } + + /** + * Set the ThreadPoolExecutor's keep-alive seconds. + * Default is 60. + *

    This setting can be modified at runtime, for example through JMX. + */ + public void setKeepAliveSeconds(int keepAliveSeconds) { + synchronized (this.poolSizeMonitor) { + this.keepAliveSeconds = keepAliveSeconds; + if (this.threadPoolExecutor != null) { + this.threadPoolExecutor.setKeepAliveTime(keepAliveSeconds, TimeUnit.SECONDS); + } + } + } + + /** + * Return the ThreadPoolExecutor's keep-alive seconds. + */ + public int getKeepAliveSeconds() { + synchronized (this.poolSizeMonitor) { + return this.keepAliveSeconds; + } + } + + /** + * Set the capacity for the ThreadPoolExecutor's BlockingQueue. + * Default is {@code Integer.MAX_VALUE}. + *

    Any positive value will lead to a LinkedBlockingQueue instance; + * any other value will lead to a SynchronousQueue instance. + * @see java.util.concurrent.LinkedBlockingQueue + * @see java.util.concurrent.SynchronousQueue + */ + public void setQueueCapacity(int queueCapacity) { + this.queueCapacity = queueCapacity; + } + + /** + * Specify whether to allow core threads to time out. This enables dynamic + * growing and shrinking even in combination with a non-zero queue (since + * the max pool size will only grow once the queue is full). + *

    Default is "false". + * @see java.util.concurrent.ThreadPoolExecutor#allowCoreThreadTimeOut(boolean) + */ + public void setAllowCoreThreadTimeOut(boolean allowCoreThreadTimeOut) { + this.allowCoreThreadTimeOut = allowCoreThreadTimeOut; + } + + /** + * Specify a custom {@link TaskDecorator} to be applied to any {@link Runnable} + * about to be executed. + *

    Note that such a decorator is not necessarily being applied to the + * user-supplied {@code Runnable}/{@code Callable} but rather to the actual + * execution callback (which may be a wrapper around the user-supplied task). + *

    The primary use case is to set some execution context around the task's + * invocation, or to provide some monitoring/statistics for task execution. + *

    NOTE: Exception handling in {@code TaskDecorator} implementations + * is limited to plain {@code Runnable} execution via {@code execute} calls. + * In case of {@code #submit} calls, the exposed {@code Runnable} will be a + * {@code FutureTask} which does not propagate any exceptions; you might + * have to cast it and call {@code Future#get} to evaluate exceptions. + * See the {@code ThreadPoolExecutor#afterExecute} javadoc for an example + * of how to access exceptions in such a {@code Future} case. + * @since 4.3 + */ + public void setTaskDecorator(TaskDecorator taskDecorator) { + this.taskDecorator = taskDecorator; + } + + + /** + * Note: This method exposes an {@link ExecutorService} to its base class + * but stores the actual {@link ThreadPoolExecutor} handle internally. + * Do not override this method for replacing the executor, rather just for + * decorating its {@code ExecutorService} handle or storing custom state. + */ + @Override + protected ExecutorService initializeExecutor( + ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) { + + BlockingQueue queue = createQueue(this.queueCapacity); + + ThreadPoolExecutor executor; + if (this.taskDecorator != null) { + executor = new ThreadPoolExecutor( + this.corePoolSize, this.maxPoolSize, this.keepAliveSeconds, TimeUnit.SECONDS, + queue, threadFactory, rejectedExecutionHandler) { + @Override + public void execute(Runnable command) { + Runnable decorated = taskDecorator.decorate(command); + if (decorated != command) { + decoratedTaskMap.put(decorated, command); + } + super.execute(decorated); + } + }; + } + else { + executor = new ThreadPoolExecutor( + this.corePoolSize, this.maxPoolSize, this.keepAliveSeconds, TimeUnit.SECONDS, + queue, threadFactory, rejectedExecutionHandler); + + } + + if (this.allowCoreThreadTimeOut) { + executor.allowCoreThreadTimeOut(true); + } + + this.threadPoolExecutor = executor; + return executor; + } + + /** + * Create the BlockingQueue to use for the ThreadPoolExecutor. + *

    A LinkedBlockingQueue instance will be created for a positive + * capacity value; a SynchronousQueue else. + * @param queueCapacity the specified queue capacity + * @return the BlockingQueue instance + * @see java.util.concurrent.LinkedBlockingQueue + * @see java.util.concurrent.SynchronousQueue + */ + protected BlockingQueue createQueue(int queueCapacity) { + if (queueCapacity > 0) { + return new LinkedBlockingQueue<>(queueCapacity); + } + else { + return new SynchronousQueue<>(); + } + } + + /** + * Return the underlying ThreadPoolExecutor for native access. + * @return the underlying ThreadPoolExecutor (never {@code null}) + * @throws IllegalStateException if the ThreadPoolTaskExecutor hasn't been initialized yet + */ + public ThreadPoolExecutor getThreadPoolExecutor() throws IllegalStateException { + Assert.state(this.threadPoolExecutor != null, "ThreadPoolTaskExecutor not initialized"); + return this.threadPoolExecutor; + } + + /** + * Return the current pool size. + * @see java.util.concurrent.ThreadPoolExecutor#getPoolSize() + */ + public int getPoolSize() { + if (this.threadPoolExecutor == null) { + // Not initialized yet: assume core pool size. + return this.corePoolSize; + } + return this.threadPoolExecutor.getPoolSize(); + } + + /** + * Return the number of currently active threads. + * @see java.util.concurrent.ThreadPoolExecutor#getActiveCount() + */ + public int getActiveCount() { + if (this.threadPoolExecutor == null) { + // Not initialized yet: assume no active threads. + return 0; + } + return this.threadPoolExecutor.getActiveCount(); + } + + + @Override + public void execute(Runnable task) { + Executor executor = getThreadPoolExecutor(); + try { + executor.execute(task); + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + } + } + + @Override + public void execute(Runnable task, long startTimeout) { + execute(task); + } + + @Override + public Future submit(Runnable task) { + ExecutorService executor = getThreadPoolExecutor(); + try { + return executor.submit(task); + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + } + } + + @Override + public Future submit(Callable task) { + ExecutorService executor = getThreadPoolExecutor(); + try { + return executor.submit(task); + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + } + } + + @Override + public ListenableFuture submitListenable(Runnable task) { + ExecutorService executor = getThreadPoolExecutor(); + try { + ListenableFutureTask future = new ListenableFutureTask<>(task, null); + executor.execute(future); + return future; + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + } + } + + @Override + public ListenableFuture submitListenable(Callable task) { + ExecutorService executor = getThreadPoolExecutor(); + try { + ListenableFutureTask future = new ListenableFutureTask<>(task); + executor.execute(future); + return future; + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + } + } + + @Override + protected void cancelRemainingTask(Runnable task) { + super.cancelRemainingTask(task); + // Cancel associated user-level Future handle as well + Object original = this.decoratedTaskMap.get(task); + if (original instanceof Future) { + ((Future) original).cancel(true); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java new file mode 100644 index 0000000..de7bdb2 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java @@ -0,0 +1,426 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.concurrent; + +import java.time.Clock; +import java.util.Date; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +import org.springframework.core.task.AsyncListenableTaskExecutor; +import org.springframework.core.task.TaskRejectedException; +import org.springframework.lang.Nullable; +import org.springframework.scheduling.SchedulingTaskExecutor; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.Trigger; +import org.springframework.scheduling.support.TaskUtils; +import org.springframework.util.Assert; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ErrorHandler; +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.util.concurrent.ListenableFutureTask; + +/** + * Implementation of Spring's {@link TaskScheduler} interface, wrapping + * a native {@link java.util.concurrent.ScheduledThreadPoolExecutor}. + * + * @author Juergen Hoeller + * @author Mark Fisher + * @since 3.0 + * @see #setPoolSize + * @see #setRemoveOnCancelPolicy + * @see #setThreadFactory + * @see #setErrorHandler + */ +@SuppressWarnings("serial") +public class ThreadPoolTaskScheduler extends ExecutorConfigurationSupport + implements AsyncListenableTaskExecutor, SchedulingTaskExecutor, TaskScheduler { + + private volatile int poolSize = 1; + + private volatile boolean removeOnCancelPolicy; + + @Nullable + private volatile ErrorHandler errorHandler; + + private Clock clock = Clock.systemDefaultZone(); + + @Nullable + private ScheduledExecutorService scheduledExecutor; + + // Underlying ScheduledFutureTask to user-level ListenableFuture handle, if any + private final Map> listenableFutureMap = + new ConcurrentReferenceHashMap<>(16, ConcurrentReferenceHashMap.ReferenceType.WEAK); + + + /** + * Set the ScheduledExecutorService's pool size. + * Default is 1. + *

    This setting can be modified at runtime, for example through JMX. + */ + public void setPoolSize(int poolSize) { + Assert.isTrue(poolSize > 0, "'poolSize' must be 1 or higher"); + this.poolSize = poolSize; + if (this.scheduledExecutor instanceof ScheduledThreadPoolExecutor) { + ((ScheduledThreadPoolExecutor) this.scheduledExecutor).setCorePoolSize(poolSize); + } + } + + /** + * Set the remove-on-cancel mode on {@link ScheduledThreadPoolExecutor}. + *

    Default is {@code false}. If set to {@code true}, the target executor will be + * switched into remove-on-cancel mode (if possible, with a soft fallback otherwise). + *

    This setting can be modified at runtime, for example through JMX. + */ + public void setRemoveOnCancelPolicy(boolean removeOnCancelPolicy) { + this.removeOnCancelPolicy = removeOnCancelPolicy; + if (this.scheduledExecutor instanceof ScheduledThreadPoolExecutor) { + ((ScheduledThreadPoolExecutor) this.scheduledExecutor).setRemoveOnCancelPolicy(removeOnCancelPolicy); + } + else if (removeOnCancelPolicy && this.scheduledExecutor != null) { + logger.debug("Could not apply remove-on-cancel policy - not a ScheduledThreadPoolExecutor"); + } + } + + /** + * Set a custom {@link ErrorHandler} strategy. + */ + public void setErrorHandler(ErrorHandler errorHandler) { + this.errorHandler = errorHandler; + } + + /** + * Set the clock to use for scheduling purposes. + *

    The default clock is the system clock for the default time zone. + * @since 5.3 + * @see Clock#systemDefaultZone() + */ + public void setClock(Clock clock) { + this.clock = clock; + } + + @Override + public Clock getClock() { + return this.clock; + } + + + @Override + protected ExecutorService initializeExecutor( + ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) { + + this.scheduledExecutor = createExecutor(this.poolSize, threadFactory, rejectedExecutionHandler); + + if (this.removeOnCancelPolicy) { + if (this.scheduledExecutor instanceof ScheduledThreadPoolExecutor) { + ((ScheduledThreadPoolExecutor) this.scheduledExecutor).setRemoveOnCancelPolicy(true); + } + else { + logger.debug("Could not apply remove-on-cancel policy - not a ScheduledThreadPoolExecutor"); + } + } + + return this.scheduledExecutor; + } + + /** + * Create a new {@link ScheduledExecutorService} instance. + *

    The default implementation creates a {@link ScheduledThreadPoolExecutor}. + * Can be overridden in subclasses to provide custom {@link ScheduledExecutorService} instances. + * @param poolSize the specified pool size + * @param threadFactory the ThreadFactory to use + * @param rejectedExecutionHandler the RejectedExecutionHandler to use + * @return a new ScheduledExecutorService instance + * @see #afterPropertiesSet() + * @see java.util.concurrent.ScheduledThreadPoolExecutor + */ + protected ScheduledExecutorService createExecutor( + int poolSize, ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) { + + return new ScheduledThreadPoolExecutor(poolSize, threadFactory, rejectedExecutionHandler); + } + + /** + * Return the underlying ScheduledExecutorService for native access. + * @return the underlying ScheduledExecutorService (never {@code null}) + * @throws IllegalStateException if the ThreadPoolTaskScheduler hasn't been initialized yet + */ + public ScheduledExecutorService getScheduledExecutor() throws IllegalStateException { + Assert.state(this.scheduledExecutor != null, "ThreadPoolTaskScheduler not initialized"); + return this.scheduledExecutor; + } + + /** + * Return the underlying ScheduledThreadPoolExecutor, if available. + * @return the underlying ScheduledExecutorService (never {@code null}) + * @throws IllegalStateException if the ThreadPoolTaskScheduler hasn't been initialized yet + * or if the underlying ScheduledExecutorService isn't a ScheduledThreadPoolExecutor + * @see #getScheduledExecutor() + */ + public ScheduledThreadPoolExecutor getScheduledThreadPoolExecutor() throws IllegalStateException { + Assert.state(this.scheduledExecutor instanceof ScheduledThreadPoolExecutor, + "No ScheduledThreadPoolExecutor available"); + return (ScheduledThreadPoolExecutor) this.scheduledExecutor; + } + + /** + * Return the current pool size. + *

    Requires an underlying {@link ScheduledThreadPoolExecutor}. + * @see #getScheduledThreadPoolExecutor() + * @see java.util.concurrent.ScheduledThreadPoolExecutor#getPoolSize() + */ + public int getPoolSize() { + if (this.scheduledExecutor == null) { + // Not initialized yet: assume initial pool size. + return this.poolSize; + } + return getScheduledThreadPoolExecutor().getPoolSize(); + } + + /** + * Return the current setting for the remove-on-cancel mode. + *

    Requires an underlying {@link ScheduledThreadPoolExecutor}. + */ + public boolean isRemoveOnCancelPolicy() { + if (this.scheduledExecutor == null) { + // Not initialized yet: return our setting for the time being. + return this.removeOnCancelPolicy; + } + return getScheduledThreadPoolExecutor().getRemoveOnCancelPolicy(); + } + + /** + * Return the number of currently active threads. + *

    Requires an underlying {@link ScheduledThreadPoolExecutor}. + * @see #getScheduledThreadPoolExecutor() + * @see java.util.concurrent.ScheduledThreadPoolExecutor#getActiveCount() + */ + public int getActiveCount() { + if (this.scheduledExecutor == null) { + // Not initialized yet: assume no active threads. + return 0; + } + return getScheduledThreadPoolExecutor().getActiveCount(); + } + + + // SchedulingTaskExecutor implementation + + @Override + public void execute(Runnable task) { + Executor executor = getScheduledExecutor(); + try { + executor.execute(errorHandlingTask(task, false)); + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + } + } + + @Override + public void execute(Runnable task, long startTimeout) { + execute(task); + } + + @Override + public Future submit(Runnable task) { + ExecutorService executor = getScheduledExecutor(); + try { + return executor.submit(errorHandlingTask(task, false)); + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + } + } + + @Override + public Future submit(Callable task) { + ExecutorService executor = getScheduledExecutor(); + try { + Callable taskToUse = task; + ErrorHandler errorHandler = this.errorHandler; + if (errorHandler != null) { + taskToUse = new DelegatingErrorHandlingCallable<>(task, errorHandler); + } + return executor.submit(taskToUse); + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + } + } + + @Override + public ListenableFuture submitListenable(Runnable task) { + ExecutorService executor = getScheduledExecutor(); + try { + ListenableFutureTask listenableFuture = new ListenableFutureTask<>(task, null); + executeAndTrack(executor, listenableFuture); + return listenableFuture; + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + } + } + + @Override + public ListenableFuture submitListenable(Callable task) { + ExecutorService executor = getScheduledExecutor(); + try { + ListenableFutureTask listenableFuture = new ListenableFutureTask<>(task); + executeAndTrack(executor, listenableFuture); + return listenableFuture; + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + } + } + + private void executeAndTrack(ExecutorService executor, ListenableFutureTask listenableFuture) { + Future scheduledFuture = executor.submit(errorHandlingTask(listenableFuture, false)); + this.listenableFutureMap.put(scheduledFuture, listenableFuture); + listenableFuture.addCallback(result -> this.listenableFutureMap.remove(scheduledFuture), + ex -> this.listenableFutureMap.remove(scheduledFuture)); + } + + @Override + protected void cancelRemainingTask(Runnable task) { + super.cancelRemainingTask(task); + // Cancel associated user-level ListenableFuture handle as well + ListenableFuture listenableFuture = this.listenableFutureMap.get(task); + if (listenableFuture != null) { + listenableFuture.cancel(true); + } + } + + + // TaskScheduler implementation + + @Override + @Nullable + public ScheduledFuture schedule(Runnable task, Trigger trigger) { + ScheduledExecutorService executor = getScheduledExecutor(); + try { + ErrorHandler errorHandler = this.errorHandler; + if (errorHandler == null) { + errorHandler = TaskUtils.getDefaultErrorHandler(true); + } + return new ReschedulingRunnable(task, trigger, this.clock, executor, errorHandler).schedule(); + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + } + } + + @Override + public ScheduledFuture schedule(Runnable task, Date startTime) { + ScheduledExecutorService executor = getScheduledExecutor(); + long initialDelay = startTime.getTime() - this.clock.millis(); + try { + return executor.schedule(errorHandlingTask(task, false), initialDelay, TimeUnit.MILLISECONDS); + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + } + } + + @Override + public ScheduledFuture scheduleAtFixedRate(Runnable task, Date startTime, long period) { + ScheduledExecutorService executor = getScheduledExecutor(); + long initialDelay = startTime.getTime() - this.clock.millis(); + try { + return executor.scheduleAtFixedRate(errorHandlingTask(task, true), initialDelay, period, TimeUnit.MILLISECONDS); + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + } + } + + @Override + public ScheduledFuture scheduleAtFixedRate(Runnable task, long period) { + ScheduledExecutorService executor = getScheduledExecutor(); + try { + return executor.scheduleAtFixedRate(errorHandlingTask(task, true), 0, period, TimeUnit.MILLISECONDS); + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + } + } + + @Override + public ScheduledFuture scheduleWithFixedDelay(Runnable task, Date startTime, long delay) { + ScheduledExecutorService executor = getScheduledExecutor(); + long initialDelay = startTime.getTime() - this.clock.millis(); + try { + return executor.scheduleWithFixedDelay(errorHandlingTask(task, true), initialDelay, delay, TimeUnit.MILLISECONDS); + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + } + } + + @Override + public ScheduledFuture scheduleWithFixedDelay(Runnable task, long delay) { + ScheduledExecutorService executor = getScheduledExecutor(); + try { + return executor.scheduleWithFixedDelay(errorHandlingTask(task, true), 0, delay, TimeUnit.MILLISECONDS); + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex); + } + } + + + private Runnable errorHandlingTask(Runnable task, boolean isRepeatingTask) { + return TaskUtils.decorateTaskWithErrorHandler(task, this.errorHandler, isRepeatingTask); + } + + + private static class DelegatingErrorHandlingCallable implements Callable { + + private final Callable delegate; + + private final ErrorHandler errorHandler; + + public DelegatingErrorHandlingCallable(Callable delegate, ErrorHandler errorHandler) { + this.delegate = delegate; + this.errorHandler = errorHandler; + } + + @Override + @Nullable + public V call() throws Exception { + try { + return this.delegate.call(); + } + catch (Throwable ex) { + this.errorHandler.handleError(ex); + return null; + } + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/package-info.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/package-info.java new file mode 100644 index 0000000..a8ea96e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/package-info.java @@ -0,0 +1,13 @@ +/** + * Scheduling convenience classes for the {@code java.util.concurrent} + * and {@code javax.enterprise.concurrent} packages, allowing to set up a + * ThreadPoolExecutor or ScheduledThreadPoolExecutor as a bean in a Spring + * context. Provides support for the native {@code java.util.concurrent} + * interfaces as well as the Spring {@code TaskExecutor} mechanism. + */ +@NonNullApi +@NonNullFields +package org.springframework.scheduling.concurrent; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/AnnotationDrivenBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/scheduling/config/AnnotationDrivenBeanDefinitionParser.java new file mode 100644 index 0000000..8f09ae6 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/config/AnnotationDrivenBeanDefinitionParser.java @@ -0,0 +1,138 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.config; + +import org.w3c.dom.Element; + +import org.springframework.aop.config.AopNamespaceUtils; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.parsing.CompositeComponentDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Parser for the 'annotation-driven' element of the 'task' namespace. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @author Ramnivas Laddad + * @author Chris Beams + * @author Stephane Nicoll + * @since 3.0 + */ +public class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { + + private static final String ASYNC_EXECUTION_ASPECT_CLASS_NAME = + "org.springframework.scheduling.aspectj.AnnotationAsyncExecutionAspect"; + + + @Override + @Nullable + public BeanDefinition parse(Element element, ParserContext parserContext) { + Object source = parserContext.extractSource(element); + + // Register component for the surrounding element. + CompositeComponentDefinition compDefinition = new CompositeComponentDefinition(element.getTagName(), source); + parserContext.pushContainingComponent(compDefinition); + + // Nest the concrete post-processor bean in the surrounding component. + BeanDefinitionRegistry registry = parserContext.getRegistry(); + + String mode = element.getAttribute("mode"); + if ("aspectj".equals(mode)) { + // mode="aspectj" + registerAsyncExecutionAspect(element, parserContext); + } + else { + // mode="proxy" + if (registry.containsBeanDefinition(TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME)) { + parserContext.getReaderContext().error( + "Only one AsyncAnnotationBeanPostProcessor may exist within the context.", source); + } + else { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition( + "org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor"); + builder.getRawBeanDefinition().setSource(source); + String executor = element.getAttribute("executor"); + if (StringUtils.hasText(executor)) { + builder.addPropertyReference("executor", executor); + } + String exceptionHandler = element.getAttribute("exception-handler"); + if (StringUtils.hasText(exceptionHandler)) { + builder.addPropertyReference("exceptionHandler", exceptionHandler); + } + if (Boolean.parseBoolean(element.getAttribute(AopNamespaceUtils.PROXY_TARGET_CLASS_ATTRIBUTE))) { + builder.addPropertyValue("proxyTargetClass", true); + } + registerPostProcessor(parserContext, builder, TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME); + } + } + + if (registry.containsBeanDefinition(TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)) { + parserContext.getReaderContext().error( + "Only one ScheduledAnnotationBeanPostProcessor may exist within the context.", source); + } + else { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition( + "org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor"); + builder.getRawBeanDefinition().setSource(source); + String scheduler = element.getAttribute("scheduler"); + if (StringUtils.hasText(scheduler)) { + builder.addPropertyReference("scheduler", scheduler); + } + registerPostProcessor(parserContext, builder, TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME); + } + + // Finally register the composite component. + parserContext.popAndRegisterContainingComponent(); + + return null; + } + + private void registerAsyncExecutionAspect(Element element, ParserContext parserContext) { + if (!parserContext.getRegistry().containsBeanDefinition(TaskManagementConfigUtils.ASYNC_EXECUTION_ASPECT_BEAN_NAME)) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(ASYNC_EXECUTION_ASPECT_CLASS_NAME); + builder.setFactoryMethod("aspectOf"); + String executor = element.getAttribute("executor"); + if (StringUtils.hasText(executor)) { + builder.addPropertyReference("executor", executor); + } + String exceptionHandler = element.getAttribute("exception-handler"); + if (StringUtils.hasText(exceptionHandler)) { + builder.addPropertyReference("exceptionHandler", exceptionHandler); + } + parserContext.registerBeanComponent(new BeanComponentDefinition(builder.getBeanDefinition(), + TaskManagementConfigUtils.ASYNC_EXECUTION_ASPECT_BEAN_NAME)); + } + } + + private static void registerPostProcessor( + ParserContext parserContext, BeanDefinitionBuilder builder, String beanName) { + + builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + parserContext.getRegistry().registerBeanDefinition(beanName, builder.getBeanDefinition()); + BeanDefinitionHolder holder = new BeanDefinitionHolder(builder.getBeanDefinition(), beanName); + parserContext.registerComponent(new BeanComponentDefinition(holder)); + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/ContextLifecycleScheduledTaskRegistrar.java b/spring-context/src/main/java/org/springframework/scheduling/config/ContextLifecycleScheduledTaskRegistrar.java new file mode 100644 index 0000000..256a998 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/config/ContextLifecycleScheduledTaskRegistrar.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.config; + +import org.springframework.beans.factory.SmartInitializingSingleton; + +/** + * {@link ScheduledTaskRegistrar} subclass which redirects the actual scheduling + * of tasks to the {@link #afterSingletonsInstantiated()} callback (as of 4.1.2). + * + * @author Juergen Hoeller + * @since 3.2.1 + */ +public class ContextLifecycleScheduledTaskRegistrar extends ScheduledTaskRegistrar implements SmartInitializingSingleton { + + @Override + public void afterPropertiesSet() { + // no-op + } + + @Override + public void afterSingletonsInstantiated() { + scheduleTasks(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/CronTask.java b/spring-context/src/main/java/org/springframework/scheduling/config/CronTask.java new file mode 100644 index 0000000..c011222 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/config/CronTask.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.config; + +import org.springframework.scheduling.support.CronTrigger; + +/** + * {@link TriggerTask} implementation defining a {@code Runnable} to be executed according + * to a {@linkplain org.springframework.scheduling.support.CronSequenceGenerator standard + * cron expression}. + * + * @author Chris Beams + * @since 3.2 + * @see org.springframework.scheduling.annotation.Scheduled#cron() + * @see ScheduledTaskRegistrar#addCronTask(CronTask) + */ +public class CronTask extends TriggerTask { + + private final String expression; + + + /** + * Create a new {@code CronTask}. + * @param runnable the underlying task to execute + * @param expression the cron expression defining when the task should be executed + */ + public CronTask(Runnable runnable, String expression) { + this(runnable, new CronTrigger(expression)); + } + + /** + * Create a new {@code CronTask}. + * @param runnable the underlying task to execute + * @param cronTrigger the cron trigger defining when the task should be executed + */ + public CronTask(Runnable runnable, CronTrigger cronTrigger) { + super(runnable, cronTrigger); + this.expression = cronTrigger.getExpression(); + } + + + /** + * Return the cron expression defining when the task should be executed. + */ + public String getExpression() { + return this.expression; + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/ExecutorBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/scheduling/config/ExecutorBeanDefinitionParser.java new file mode 100644 index 0000000..7c53292 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/config/ExecutorBeanDefinitionParser.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.util.StringUtils; + +/** + * Parser for the 'executor' element of the 'task' namespace. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @since 3.0 + */ +public class ExecutorBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { + + @Override + protected String getBeanClassName(Element element) { + return "org.springframework.scheduling.config.TaskExecutorFactoryBean"; + } + + @Override + protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + String keepAliveSeconds = element.getAttribute("keep-alive"); + if (StringUtils.hasText(keepAliveSeconds)) { + builder.addPropertyValue("keepAliveSeconds", keepAliveSeconds); + } + String queueCapacity = element.getAttribute("queue-capacity"); + if (StringUtils.hasText(queueCapacity)) { + builder.addPropertyValue("queueCapacity", queueCapacity); + } + configureRejectionPolicy(element, builder); + String poolSize = element.getAttribute("pool-size"); + if (StringUtils.hasText(poolSize)) { + builder.addPropertyValue("poolSize", poolSize); + } + } + + private void configureRejectionPolicy(Element element, BeanDefinitionBuilder builder) { + String rejectionPolicy = element.getAttribute("rejection-policy"); + if (!StringUtils.hasText(rejectionPolicy)) { + return; + } + String prefix = "java.util.concurrent.ThreadPoolExecutor."; + String policyClassName; + if (rejectionPolicy.equals("ABORT")) { + policyClassName = prefix + "AbortPolicy"; + } + else if (rejectionPolicy.equals("CALLER_RUNS")) { + policyClassName = prefix + "CallerRunsPolicy"; + } + else if (rejectionPolicy.equals("DISCARD")) { + policyClassName = prefix + "DiscardPolicy"; + } + else if (rejectionPolicy.equals("DISCARD_OLDEST")) { + policyClassName = prefix + "DiscardOldestPolicy"; + } + else { + policyClassName = rejectionPolicy; + } + builder.addPropertyValue("rejectedExecutionHandler", new RootBeanDefinition(policyClassName)); + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/FixedDelayTask.java b/spring-context/src/main/java/org/springframework/scheduling/config/FixedDelayTask.java new file mode 100644 index 0000000..2aec5f3 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/config/FixedDelayTask.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.config; + +/** + * Specialization of {@link IntervalTask} for fixed-delay semantics. + * + * @author Juergen Hoeller + * @since 5.0.2 + * @see org.springframework.scheduling.annotation.Scheduled#fixedDelay() + * @see ScheduledTaskRegistrar#addFixedDelayTask(IntervalTask) + */ +public class FixedDelayTask extends IntervalTask { + + /** + * Create a new {@code FixedDelayTask}. + * @param runnable the underlying task to execute + * @param interval how often in milliseconds the task should be executed + * @param initialDelay the initial delay before first execution of the task + */ + public FixedDelayTask(Runnable runnable, long interval, long initialDelay) { + super(runnable, interval, initialDelay); + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/FixedRateTask.java b/spring-context/src/main/java/org/springframework/scheduling/config/FixedRateTask.java new file mode 100644 index 0000000..0b16c57 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/config/FixedRateTask.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.config; + +/** + * Specialization of {@link IntervalTask} for fixed-rate semantics. + * + * @author Juergen Hoeller + * @since 5.0.2 + * @see org.springframework.scheduling.annotation.Scheduled#fixedRate() + * @see ScheduledTaskRegistrar#addFixedRateTask(IntervalTask) + */ +public class FixedRateTask extends IntervalTask { + + /** + * Create a new {@code FixedRateTask}. + * @param runnable the underlying task to execute + * @param interval how often in milliseconds the task should be executed + * @param initialDelay the initial delay before first execution of the task + */ + public FixedRateTask(Runnable runnable, long interval, long initialDelay) { + super(runnable, interval, initialDelay); + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/IntervalTask.java b/spring-context/src/main/java/org/springframework/scheduling/config/IntervalTask.java new file mode 100644 index 0000000..b259c72 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/config/IntervalTask.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.config; + +/** + * {@link Task} implementation defining a {@code Runnable} to be executed at a given + * millisecond interval which may be treated as fixed-rate or fixed-delay depending on + * context. + * + * @author Chris Beams + * @since 3.2 + * @see ScheduledTaskRegistrar#addFixedRateTask(IntervalTask) + * @see ScheduledTaskRegistrar#addFixedDelayTask(IntervalTask) + */ +public class IntervalTask extends Task { + + private final long interval; + + private final long initialDelay; + + + /** + * Create a new {@code IntervalTask}. + * @param runnable the underlying task to execute + * @param interval how often in milliseconds the task should be executed + * @param initialDelay the initial delay before first execution of the task + */ + public IntervalTask(Runnable runnable, long interval, long initialDelay) { + super(runnable); + this.interval = interval; + this.initialDelay = initialDelay; + } + + /** + * Create a new {@code IntervalTask} with no initial delay. + * @param runnable the underlying task to execute + * @param interval how often in milliseconds the task should be executed + */ + public IntervalTask(Runnable runnable, long interval) { + this(runnable, interval, 0); + } + + + /** + * Return how often in milliseconds the task should be executed. + */ + public long getInterval() { + return this.interval; + } + + /** + * Return the initial delay before first execution of the task. + */ + public long getInitialDelay() { + return this.initialDelay; + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTask.java b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTask.java new file mode 100644 index 0000000..3f23657 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTask.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.config; + +import java.util.concurrent.ScheduledFuture; + +import org.springframework.lang.Nullable; + +/** + * A representation of a scheduled task at runtime, + * used as a return value for scheduling methods. + * + * @author Juergen Hoeller + * @since 4.3 + * @see ScheduledTaskRegistrar#scheduleCronTask(CronTask) + * @see ScheduledTaskRegistrar#scheduleFixedRateTask(FixedRateTask) + * @see ScheduledTaskRegistrar#scheduleFixedDelayTask(FixedDelayTask) + */ +public final class ScheduledTask { + + private final Task task; + + @Nullable + volatile ScheduledFuture future; + + + ScheduledTask(Task task) { + this.task = task; + } + + + /** + * Return the underlying task (typically a {@link CronTask}, + * {@link FixedRateTask} or {@link FixedDelayTask}). + * @since 5.0.2 + */ + public Task getTask() { + return this.task; + } + + /** + * Trigger cancellation of this scheduled task. + */ + public void cancel() { + ScheduledFuture future = this.future; + if (future != null) { + future.cancel(true); + } + } + + @Override + public String toString() { + return this.task.toString(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskHolder.java b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskHolder.java new file mode 100644 index 0000000..41aed7b --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskHolder.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.config; + +import java.util.Set; + +/** + * Common interface for exposing locally scheduled tasks. + * + * @author Juergen Hoeller + * @since 5.0.2 + * @see ScheduledTaskRegistrar + * @see org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor + */ +public interface ScheduledTaskHolder { + + /** + * Return an overview of the tasks that have been scheduled by this instance. + */ + Set getScheduledTasks(); + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java new file mode 100644 index 0000000..3b9cc27 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java @@ -0,0 +1,562 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.config; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.lang.Nullable; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.Trigger; +import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler; +import org.springframework.scheduling.support.CronTrigger; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * Helper bean for registering tasks with a {@link TaskScheduler}, typically using cron + * expressions. + * + *

    As of Spring 3.1, {@code ScheduledTaskRegistrar} has a more prominent user-facing + * role when used in conjunction with the {@link + * org.springframework.scheduling.annotation.EnableAsync @EnableAsync} annotation and its + * {@link org.springframework.scheduling.annotation.SchedulingConfigurer + * SchedulingConfigurer} callback interface. + * + * @author Juergen Hoeller + * @author Chris Beams + * @author Tobias Montagna-Hay + * @author Sam Brannen + * @since 3.0 + * @see org.springframework.scheduling.annotation.EnableAsync + * @see org.springframework.scheduling.annotation.SchedulingConfigurer + */ +public class ScheduledTaskRegistrar implements ScheduledTaskHolder, InitializingBean, DisposableBean { + + /** + * A special cron expression value that indicates a disabled trigger: {@value}. + *

    This is primarily meant for use with {@link #addCronTask(Runnable, String)} + * when the value for the supplied {@code expression} is retrieved from an + * external source — for example, from a property in the + * {@link org.springframework.core.env.Environment Environment}. + * @since 5.2 + * @see org.springframework.scheduling.annotation.Scheduled#CRON_DISABLED + */ + public static final String CRON_DISABLED = "-"; + + + @Nullable + private TaskScheduler taskScheduler; + + @Nullable + private ScheduledExecutorService localExecutor; + + @Nullable + private List triggerTasks; + + @Nullable + private List cronTasks; + + @Nullable + private List fixedRateTasks; + + @Nullable + private List fixedDelayTasks; + + private final Map unresolvedTasks = new HashMap<>(16); + + private final Set scheduledTasks = new LinkedHashSet<>(16); + + + /** + * Set the {@link TaskScheduler} to register scheduled tasks with. + */ + public void setTaskScheduler(TaskScheduler taskScheduler) { + Assert.notNull(taskScheduler, "TaskScheduler must not be null"); + this.taskScheduler = taskScheduler; + } + + /** + * Set the {@link TaskScheduler} to register scheduled tasks with, or a + * {@link java.util.concurrent.ScheduledExecutorService} to be wrapped as a + * {@code TaskScheduler}. + */ + public void setScheduler(@Nullable Object scheduler) { + if (scheduler == null) { + this.taskScheduler = null; + } + else if (scheduler instanceof TaskScheduler) { + this.taskScheduler = (TaskScheduler) scheduler; + } + else if (scheduler instanceof ScheduledExecutorService) { + this.taskScheduler = new ConcurrentTaskScheduler(((ScheduledExecutorService) scheduler)); + } + else { + throw new IllegalArgumentException("Unsupported scheduler type: " + scheduler.getClass()); + } + } + + /** + * Return the {@link TaskScheduler} instance for this registrar (may be {@code null}). + */ + @Nullable + public TaskScheduler getScheduler() { + return this.taskScheduler; + } + + + /** + * Specify triggered tasks as a Map of Runnables (the tasks) and Trigger objects + * (typically custom implementations of the {@link Trigger} interface). + */ + public void setTriggerTasks(Map triggerTasks) { + this.triggerTasks = new ArrayList<>(); + triggerTasks.forEach((task, trigger) -> addTriggerTask(new TriggerTask(task, trigger))); + } + + /** + * Specify triggered tasks as a list of {@link TriggerTask} objects. Primarily used + * by {@code } namespace parsing. + * @since 3.2 + * @see ScheduledTasksBeanDefinitionParser + */ + public void setTriggerTasksList(List triggerTasks) { + this.triggerTasks = triggerTasks; + } + + /** + * Get the trigger tasks as an unmodifiable list of {@link TriggerTask} objects. + * @return the list of tasks (never {@code null}) + * @since 4.2 + */ + public List getTriggerTaskList() { + return (this.triggerTasks != null? Collections.unmodifiableList(this.triggerTasks) : + Collections.emptyList()); + } + + /** + * Specify triggered tasks as a Map of Runnables (the tasks) and cron expressions. + * @see CronTrigger + */ + public void setCronTasks(Map cronTasks) { + this.cronTasks = new ArrayList<>(); + cronTasks.forEach(this::addCronTask); + } + + /** + * Specify triggered tasks as a list of {@link CronTask} objects. Primarily used by + * {@code } namespace parsing. + * @since 3.2 + * @see ScheduledTasksBeanDefinitionParser + */ + public void setCronTasksList(List cronTasks) { + this.cronTasks = cronTasks; + } + + /** + * Get the cron tasks as an unmodifiable list of {@link CronTask} objects. + * @return the list of tasks (never {@code null}) + * @since 4.2 + */ + public List getCronTaskList() { + return (this.cronTasks != null ? Collections.unmodifiableList(this.cronTasks) : + Collections.emptyList()); + } + + /** + * Specify triggered tasks as a Map of Runnables (the tasks) and fixed-rate values. + * @see TaskScheduler#scheduleAtFixedRate(Runnable, long) + */ + public void setFixedRateTasks(Map fixedRateTasks) { + this.fixedRateTasks = new ArrayList<>(); + fixedRateTasks.forEach(this::addFixedRateTask); + } + + /** + * Specify fixed-rate tasks as a list of {@link IntervalTask} objects. Primarily used + * by {@code } namespace parsing. + * @since 3.2 + * @see ScheduledTasksBeanDefinitionParser + */ + public void setFixedRateTasksList(List fixedRateTasks) { + this.fixedRateTasks = fixedRateTasks; + } + + /** + * Get the fixed-rate tasks as an unmodifiable list of {@link IntervalTask} objects. + * @return the list of tasks (never {@code null}) + * @since 4.2 + */ + public List getFixedRateTaskList() { + return (this.fixedRateTasks != null ? Collections.unmodifiableList(this.fixedRateTasks) : + Collections.emptyList()); + } + + /** + * Specify triggered tasks as a Map of Runnables (the tasks) and fixed-delay values. + * @see TaskScheduler#scheduleWithFixedDelay(Runnable, long) + */ + public void setFixedDelayTasks(Map fixedDelayTasks) { + this.fixedDelayTasks = new ArrayList<>(); + fixedDelayTasks.forEach(this::addFixedDelayTask); + } + + /** + * Specify fixed-delay tasks as a list of {@link IntervalTask} objects. Primarily used + * by {@code } namespace parsing. + * @since 3.2 + * @see ScheduledTasksBeanDefinitionParser + */ + public void setFixedDelayTasksList(List fixedDelayTasks) { + this.fixedDelayTasks = fixedDelayTasks; + } + + /** + * Get the fixed-delay tasks as an unmodifiable list of {@link IntervalTask} objects. + * @return the list of tasks (never {@code null}) + * @since 4.2 + */ + public List getFixedDelayTaskList() { + return (this.fixedDelayTasks != null ? Collections.unmodifiableList(this.fixedDelayTasks) : + Collections.emptyList()); + } + + + /** + * Add a Runnable task to be triggered per the given {@link Trigger}. + * @see TaskScheduler#scheduleAtFixedRate(Runnable, long) + */ + public void addTriggerTask(Runnable task, Trigger trigger) { + addTriggerTask(new TriggerTask(task, trigger)); + } + + /** + * Add a {@code TriggerTask}. + * @since 3.2 + * @see TaskScheduler#scheduleAtFixedRate(Runnable, long) + */ + public void addTriggerTask(TriggerTask task) { + if (this.triggerTasks == null) { + this.triggerTasks = new ArrayList<>(); + } + this.triggerTasks.add(task); + } + + /** + * Add a {@link Runnable} task to be triggered per the given cron {@code expression}. + *

    As of Spring Framework 5.2, this method will not register the task if the + * {@code expression} is equal to {@link #CRON_DISABLED}. + */ + public void addCronTask(Runnable task, String expression) { + if (!CRON_DISABLED.equals(expression)) { + addCronTask(new CronTask(task, expression)); + } + } + + /** + * Add a {@link CronTask}. + * @since 3.2 + */ + public void addCronTask(CronTask task) { + if (this.cronTasks == null) { + this.cronTasks = new ArrayList<>(); + } + this.cronTasks.add(task); + } + + /** + * Add a {@code Runnable} task to be triggered at the given fixed-rate interval. + * @see TaskScheduler#scheduleAtFixedRate(Runnable, long) + */ + public void addFixedRateTask(Runnable task, long interval) { + addFixedRateTask(new IntervalTask(task, interval, 0)); + } + + /** + * Add a fixed-rate {@link IntervalTask}. + * @since 3.2 + * @see TaskScheduler#scheduleAtFixedRate(Runnable, long) + */ + public void addFixedRateTask(IntervalTask task) { + if (this.fixedRateTasks == null) { + this.fixedRateTasks = new ArrayList<>(); + } + this.fixedRateTasks.add(task); + } + + /** + * Add a Runnable task to be triggered with the given fixed delay. + * @see TaskScheduler#scheduleWithFixedDelay(Runnable, long) + */ + public void addFixedDelayTask(Runnable task, long delay) { + addFixedDelayTask(new IntervalTask(task, delay, 0)); + } + + /** + * Add a fixed-delay {@link IntervalTask}. + * @since 3.2 + * @see TaskScheduler#scheduleWithFixedDelay(Runnable, long) + */ + public void addFixedDelayTask(IntervalTask task) { + if (this.fixedDelayTasks == null) { + this.fixedDelayTasks = new ArrayList<>(); + } + this.fixedDelayTasks.add(task); + } + + + /** + * Return whether this {@code ScheduledTaskRegistrar} has any tasks registered. + * @since 3.2 + */ + public boolean hasTasks() { + return (!CollectionUtils.isEmpty(this.triggerTasks) || + !CollectionUtils.isEmpty(this.cronTasks) || + !CollectionUtils.isEmpty(this.fixedRateTasks) || + !CollectionUtils.isEmpty(this.fixedDelayTasks)); + } + + + /** + * Calls {@link #scheduleTasks()} at bean construction time. + */ + @Override + public void afterPropertiesSet() { + scheduleTasks(); + } + + /** + * Schedule all registered tasks against the underlying + * {@linkplain #setTaskScheduler(TaskScheduler) task scheduler}. + */ + @SuppressWarnings("deprecation") + protected void scheduleTasks() { + if (this.taskScheduler == null) { + this.localExecutor = Executors.newSingleThreadScheduledExecutor(); + this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor); + } + if (this.triggerTasks != null) { + for (TriggerTask task : this.triggerTasks) { + addScheduledTask(scheduleTriggerTask(task)); + } + } + if (this.cronTasks != null) { + for (CronTask task : this.cronTasks) { + addScheduledTask(scheduleCronTask(task)); + } + } + if (this.fixedRateTasks != null) { + for (IntervalTask task : this.fixedRateTasks) { + addScheduledTask(scheduleFixedRateTask(task)); + } + } + if (this.fixedDelayTasks != null) { + for (IntervalTask task : this.fixedDelayTasks) { + addScheduledTask(scheduleFixedDelayTask(task)); + } + } + } + + private void addScheduledTask(@Nullable ScheduledTask task) { + if (task != null) { + this.scheduledTasks.add(task); + } + } + + + /** + * Schedule the specified trigger task, either right away if possible + * or on initialization of the scheduler. + * @return a handle to the scheduled task, allowing to cancel it + * @since 4.3 + */ + @Nullable + public ScheduledTask scheduleTriggerTask(TriggerTask task) { + ScheduledTask scheduledTask = this.unresolvedTasks.remove(task); + boolean newTask = false; + if (scheduledTask == null) { + scheduledTask = new ScheduledTask(task); + newTask = true; + } + if (this.taskScheduler != null) { + scheduledTask.future = this.taskScheduler.schedule(task.getRunnable(), task.getTrigger()); + } + else { + addTriggerTask(task); + this.unresolvedTasks.put(task, scheduledTask); + } + return (newTask ? scheduledTask : null); + } + + /** + * Schedule the specified cron task, either right away if possible + * or on initialization of the scheduler. + * @return a handle to the scheduled task, allowing to cancel it + * (or {@code null} if processing a previously registered task) + * @since 4.3 + */ + @Nullable + public ScheduledTask scheduleCronTask(CronTask task) { + ScheduledTask scheduledTask = this.unresolvedTasks.remove(task); + boolean newTask = false; + if (scheduledTask == null) { + scheduledTask = new ScheduledTask(task); + newTask = true; + } + if (this.taskScheduler != null) { + scheduledTask.future = this.taskScheduler.schedule(task.getRunnable(), task.getTrigger()); + } + else { + addCronTask(task); + this.unresolvedTasks.put(task, scheduledTask); + } + return (newTask ? scheduledTask : null); + } + + /** + * Schedule the specified fixed-rate task, either right away if possible + * or on initialization of the scheduler. + * @return a handle to the scheduled task, allowing to cancel it + * (or {@code null} if processing a previously registered task) + * @since 4.3 + * @deprecated as of 5.0.2, in favor of {@link #scheduleFixedRateTask(FixedRateTask)} + */ + @Deprecated + @Nullable + public ScheduledTask scheduleFixedRateTask(IntervalTask task) { + FixedRateTask taskToUse = (task instanceof FixedRateTask ? (FixedRateTask) task : + new FixedRateTask(task.getRunnable(), task.getInterval(), task.getInitialDelay())); + return scheduleFixedRateTask(taskToUse); + } + + /** + * Schedule the specified fixed-rate task, either right away if possible + * or on initialization of the scheduler. + * @return a handle to the scheduled task, allowing to cancel it + * (or {@code null} if processing a previously registered task) + * @since 5.0.2 + */ + @Nullable + public ScheduledTask scheduleFixedRateTask(FixedRateTask task) { + ScheduledTask scheduledTask = this.unresolvedTasks.remove(task); + boolean newTask = false; + if (scheduledTask == null) { + scheduledTask = new ScheduledTask(task); + newTask = true; + } + if (this.taskScheduler != null) { + if (task.getInitialDelay() > 0) { + Date startTime = new Date(this.taskScheduler.getClock().millis() + task.getInitialDelay()); + scheduledTask.future = + this.taskScheduler.scheduleAtFixedRate(task.getRunnable(), startTime, task.getInterval()); + } + else { + scheduledTask.future = + this.taskScheduler.scheduleAtFixedRate(task.getRunnable(), task.getInterval()); + } + } + else { + addFixedRateTask(task); + this.unresolvedTasks.put(task, scheduledTask); + } + return (newTask ? scheduledTask : null); + } + + /** + * Schedule the specified fixed-delay task, either right away if possible + * or on initialization of the scheduler. + * @return a handle to the scheduled task, allowing to cancel it + * (or {@code null} if processing a previously registered task) + * @since 4.3 + * @deprecated as of 5.0.2, in favor of {@link #scheduleFixedDelayTask(FixedDelayTask)} + */ + @Deprecated + @Nullable + public ScheduledTask scheduleFixedDelayTask(IntervalTask task) { + FixedDelayTask taskToUse = (task instanceof FixedDelayTask ? (FixedDelayTask) task : + new FixedDelayTask(task.getRunnable(), task.getInterval(), task.getInitialDelay())); + return scheduleFixedDelayTask(taskToUse); + } + + /** + * Schedule the specified fixed-delay task, either right away if possible + * or on initialization of the scheduler. + * @return a handle to the scheduled task, allowing to cancel it + * (or {@code null} if processing a previously registered task) + * @since 5.0.2 + */ + @Nullable + public ScheduledTask scheduleFixedDelayTask(FixedDelayTask task) { + ScheduledTask scheduledTask = this.unresolvedTasks.remove(task); + boolean newTask = false; + if (scheduledTask == null) { + scheduledTask = new ScheduledTask(task); + newTask = true; + } + if (this.taskScheduler != null) { + if (task.getInitialDelay() > 0) { + Date startTime = new Date(this.taskScheduler.getClock().millis() + task.getInitialDelay()); + scheduledTask.future = + this.taskScheduler.scheduleWithFixedDelay(task.getRunnable(), startTime, task.getInterval()); + } + else { + scheduledTask.future = + this.taskScheduler.scheduleWithFixedDelay(task.getRunnable(), task.getInterval()); + } + } + else { + addFixedDelayTask(task); + this.unresolvedTasks.put(task, scheduledTask); + } + return (newTask ? scheduledTask : null); + } + + + /** + * Return all locally registered tasks that have been scheduled by this registrar. + * @since 5.0.2 + * @see #addTriggerTask + * @see #addCronTask + * @see #addFixedRateTask + * @see #addFixedDelayTask + */ + @Override + public Set getScheduledTasks() { + return Collections.unmodifiableSet(this.scheduledTasks); + } + + @Override + public void destroy() { + for (ScheduledTask task : this.scheduledTasks) { + task.cancel(); + } + if (this.localExecutor != null) { + this.localExecutor.shutdownNow(); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTasksBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTasksBeanDefinitionParser.java new file mode 100644 index 0000000..6c2bd26 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTasksBeanDefinitionParser.java @@ -0,0 +1,184 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.config; + +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.util.StringUtils; + +/** + * Parser for the 'scheduled-tasks' element of the scheduling namespace. + * + * @author Mark Fisher + * @author Chris Beams + * @since 3.0 + */ +public class ScheduledTasksBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { + + private static final String ELEMENT_SCHEDULED = "scheduled"; + + private static final long ZERO_INITIAL_DELAY = 0; + + + @Override + protected boolean shouldGenerateId() { + return true; + } + + @Override + protected String getBeanClassName(Element element) { + return "org.springframework.scheduling.config.ContextLifecycleScheduledTaskRegistrar"; + } + + @Override + protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + builder.setLazyInit(false); // lazy scheduled tasks are a contradiction in terms -> force to false + ManagedList cronTaskList = new ManagedList<>(); + ManagedList fixedDelayTaskList = new ManagedList<>(); + ManagedList fixedRateTaskList = new ManagedList<>(); + ManagedList triggerTaskList = new ManagedList<>(); + NodeList childNodes = element.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + Node child = childNodes.item(i); + if (!isScheduledElement(child, parserContext)) { + continue; + } + Element taskElement = (Element) child; + String ref = taskElement.getAttribute("ref"); + String method = taskElement.getAttribute("method"); + + // Check that 'ref' and 'method' are specified + if (!StringUtils.hasText(ref) || !StringUtils.hasText(method)) { + parserContext.getReaderContext().error("Both 'ref' and 'method' are required", taskElement); + // Continue with the possible next task element + continue; + } + + String cronAttribute = taskElement.getAttribute("cron"); + String fixedDelayAttribute = taskElement.getAttribute("fixed-delay"); + String fixedRateAttribute = taskElement.getAttribute("fixed-rate"); + String triggerAttribute = taskElement.getAttribute("trigger"); + String initialDelayAttribute = taskElement.getAttribute("initial-delay"); + + boolean hasCronAttribute = StringUtils.hasText(cronAttribute); + boolean hasFixedDelayAttribute = StringUtils.hasText(fixedDelayAttribute); + boolean hasFixedRateAttribute = StringUtils.hasText(fixedRateAttribute); + boolean hasTriggerAttribute = StringUtils.hasText(triggerAttribute); + boolean hasInitialDelayAttribute = StringUtils.hasText(initialDelayAttribute); + + if (!(hasCronAttribute || hasFixedDelayAttribute || hasFixedRateAttribute || hasTriggerAttribute)) { + parserContext.getReaderContext().error( + "one of the 'cron', 'fixed-delay', 'fixed-rate', or 'trigger' attributes is required", taskElement); + continue; // with the possible next task element + } + + if (hasInitialDelayAttribute && (hasCronAttribute || hasTriggerAttribute)) { + parserContext.getReaderContext().error( + "the 'initial-delay' attribute may not be used with cron and trigger tasks", taskElement); + continue; // with the possible next task element + } + + String runnableName = + runnableReference(ref, method, taskElement, parserContext).getBeanName(); + + if (hasFixedDelayAttribute) { + fixedDelayTaskList.add(intervalTaskReference(runnableName, + initialDelayAttribute, fixedDelayAttribute, taskElement, parserContext)); + } + if (hasFixedRateAttribute) { + fixedRateTaskList.add(intervalTaskReference(runnableName, + initialDelayAttribute, fixedRateAttribute, taskElement, parserContext)); + } + if (hasCronAttribute) { + cronTaskList.add(cronTaskReference(runnableName, cronAttribute, + taskElement, parserContext)); + } + if (hasTriggerAttribute) { + String triggerName = new RuntimeBeanReference(triggerAttribute).getBeanName(); + triggerTaskList.add(triggerTaskReference(runnableName, triggerName, + taskElement, parserContext)); + } + } + String schedulerRef = element.getAttribute("scheduler"); + if (StringUtils.hasText(schedulerRef)) { + builder.addPropertyReference("taskScheduler", schedulerRef); + } + builder.addPropertyValue("cronTasksList", cronTaskList); + builder.addPropertyValue("fixedDelayTasksList", fixedDelayTaskList); + builder.addPropertyValue("fixedRateTasksList", fixedRateTaskList); + builder.addPropertyValue("triggerTasksList", triggerTaskList); + } + + private boolean isScheduledElement(Node node, ParserContext parserContext) { + return node.getNodeType() == Node.ELEMENT_NODE && + ELEMENT_SCHEDULED.equals(parserContext.getDelegate().getLocalName(node)); + } + + private RuntimeBeanReference runnableReference(String ref, String method, Element taskElement, ParserContext parserContext) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition( + "org.springframework.scheduling.support.ScheduledMethodRunnable"); + builder.addConstructorArgReference(ref); + builder.addConstructorArgValue(method); + return beanReference(taskElement, parserContext, builder); + } + + private RuntimeBeanReference intervalTaskReference(String runnableBeanName, + String initialDelay, String interval, Element taskElement, ParserContext parserContext) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition( + "org.springframework.scheduling.config.IntervalTask"); + builder.addConstructorArgReference(runnableBeanName); + builder.addConstructorArgValue(interval); + builder.addConstructorArgValue(StringUtils.hasLength(initialDelay) ? initialDelay : ZERO_INITIAL_DELAY); + return beanReference(taskElement, parserContext, builder); + } + + private RuntimeBeanReference cronTaskReference(String runnableBeanName, + String cronExpression, Element taskElement, ParserContext parserContext) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition( + "org.springframework.scheduling.config.CronTask"); + builder.addConstructorArgReference(runnableBeanName); + builder.addConstructorArgValue(cronExpression); + return beanReference(taskElement, parserContext, builder); + } + + private RuntimeBeanReference triggerTaskReference(String runnableBeanName, + String triggerBeanName, Element taskElement, ParserContext parserContext) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition( + "org.springframework.scheduling.config.TriggerTask"); + builder.addConstructorArgReference(runnableBeanName); + builder.addConstructorArgReference(triggerBeanName); + return beanReference(taskElement, parserContext, builder); + } + + private RuntimeBeanReference beanReference(Element taskElement, + ParserContext parserContext, BeanDefinitionBuilder builder) { + // Extract the source of the current task + builder.getRawBeanDefinition().setSource(parserContext.extractSource(taskElement)); + String generatedName = parserContext.getReaderContext().generateBeanName(builder.getRawBeanDefinition()); + parserContext.registerBeanComponent(new BeanComponentDefinition(builder.getBeanDefinition(), generatedName)); + return new RuntimeBeanReference(generatedName); + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/SchedulerBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/scheduling/config/SchedulerBeanDefinitionParser.java new file mode 100644 index 0000000..a9429e4 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/config/SchedulerBeanDefinitionParser.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; +import org.springframework.util.StringUtils; + +/** + * Parser for the 'scheduler' element of the 'task' namespace. + * + * @author Mark Fisher + * @since 3.0 + */ +public class SchedulerBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { + + @Override + protected String getBeanClassName(Element element) { + return "org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler"; + } + + @Override + protected void doParse(Element element, BeanDefinitionBuilder builder) { + String poolSize = element.getAttribute("pool-size"); + if (StringUtils.hasText(poolSize)) { + builder.addPropertyValue("poolSize", poolSize); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/Task.java b/spring-context/src/main/java/org/springframework/scheduling/config/Task.java new file mode 100644 index 0000000..3d9a6e7 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/config/Task.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.config; + +import org.springframework.util.Assert; + +/** + * Holder class defining a {@code Runnable} to be executed as a task, typically at a + * scheduled time or interval. See subclass hierarchy for various scheduling approaches. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.2 + */ +public class Task { + + private final Runnable runnable; + + + /** + * Create a new {@code Task}. + * @param runnable the underlying task to execute + */ + public Task(Runnable runnable) { + Assert.notNull(runnable, "Runnable must not be null"); + this.runnable = runnable; + } + + + /** + * Return the underlying task. + */ + public Runnable getRunnable() { + return this.runnable; + } + + + @Override + public String toString() { + return this.runnable.toString(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/TaskExecutorFactoryBean.java b/spring-context/src/main/java/org/springframework/scheduling/config/TaskExecutorFactoryBean.java new file mode 100644 index 0000000..c49b06b --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/config/TaskExecutorFactoryBean.java @@ -0,0 +1,170 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.config; + +import java.util.concurrent.RejectedExecutionHandler; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.task.TaskExecutor; +import org.springframework.lang.Nullable; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.util.StringUtils; + +/** + * {@link FactoryBean} for creating {@link ThreadPoolTaskExecutor} instances, + * primarily used behind the XML task namespace. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @since 3.0 + */ +public class TaskExecutorFactoryBean implements + FactoryBean, BeanNameAware, InitializingBean, DisposableBean { + + @Nullable + private String poolSize; + + @Nullable + private Integer queueCapacity; + + @Nullable + private RejectedExecutionHandler rejectedExecutionHandler; + + @Nullable + private Integer keepAliveSeconds; + + @Nullable + private String beanName; + + @Nullable + private ThreadPoolTaskExecutor target; + + + public void setPoolSize(String poolSize) { + this.poolSize = poolSize; + } + + public void setQueueCapacity(int queueCapacity) { + this.queueCapacity = queueCapacity; + } + + public void setRejectedExecutionHandler(RejectedExecutionHandler rejectedExecutionHandler) { + this.rejectedExecutionHandler = rejectedExecutionHandler; + } + + public void setKeepAliveSeconds(int keepAliveSeconds) { + this.keepAliveSeconds = keepAliveSeconds; + } + + @Override + public void setBeanName(String beanName) { + this.beanName = beanName; + } + + + @Override + public void afterPropertiesSet() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + determinePoolSizeRange(executor); + if (this.queueCapacity != null) { + executor.setQueueCapacity(this.queueCapacity); + } + if (this.keepAliveSeconds != null) { + executor.setKeepAliveSeconds(this.keepAliveSeconds); + } + if (this.rejectedExecutionHandler != null) { + executor.setRejectedExecutionHandler(this.rejectedExecutionHandler); + } + if (this.beanName != null) { + executor.setThreadNamePrefix(this.beanName + "-"); + } + executor.afterPropertiesSet(); + this.target = executor; + } + + private void determinePoolSizeRange(ThreadPoolTaskExecutor executor) { + if (StringUtils.hasText(this.poolSize)) { + try { + int corePoolSize; + int maxPoolSize; + int separatorIndex = this.poolSize.indexOf('-'); + if (separatorIndex != -1) { + corePoolSize = Integer.parseInt(this.poolSize.substring(0, separatorIndex)); + maxPoolSize = Integer.parseInt(this.poolSize.substring(separatorIndex + 1)); + if (corePoolSize > maxPoolSize) { + throw new IllegalArgumentException( + "Lower bound of pool-size range must not exceed the upper bound"); + } + if (this.queueCapacity == null) { + // No queue-capacity provided, so unbounded + if (corePoolSize == 0) { + // Actually set 'corePoolSize' to the upper bound of the range + // but allow core threads to timeout... + executor.setAllowCoreThreadTimeOut(true); + corePoolSize = maxPoolSize; + } + else { + // Non-zero lower bound implies a core-max size range... + throw new IllegalArgumentException( + "A non-zero lower bound for the size range requires a queue-capacity value"); + } + } + } + else { + int value = Integer.parseInt(this.poolSize); + corePoolSize = value; + maxPoolSize = value; + } + executor.setCorePoolSize(corePoolSize); + executor.setMaxPoolSize(maxPoolSize); + } + catch (NumberFormatException ex) { + throw new IllegalArgumentException("Invalid pool-size value [" + this.poolSize + "]: only single " + + "maximum integer (e.g. \"5\") and minimum-maximum range (e.g. \"3-5\") are supported", ex); + } + } + } + + + @Override + @Nullable + public TaskExecutor getObject() { + return this.target; + } + + @Override + public Class getObjectType() { + return (this.target != null ? this.target.getClass() : ThreadPoolTaskExecutor.class); + } + + @Override + public boolean isSingleton() { + return true; + } + + + @Override + public void destroy() { + if (this.target != null) { + this.target.destroy(); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/TaskManagementConfigUtils.java b/spring-context/src/main/java/org/springframework/scheduling/config/TaskManagementConfigUtils.java new file mode 100644 index 0000000..6d2265b --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/config/TaskManagementConfigUtils.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.config; + +/** + * Configuration constants for internal sharing across subpackages. + * + * @author Juergen Hoeller + * @since 4.1 + */ +public abstract class TaskManagementConfigUtils { + + /** + * The bean name of the internally managed Scheduled annotation processor. + */ + public static final String SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME = + "org.springframework.context.annotation.internalScheduledAnnotationProcessor"; + + /** + * The bean name of the internally managed Async annotation processor. + */ + public static final String ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME = + "org.springframework.context.annotation.internalAsyncAnnotationProcessor"; + + /** + * The bean name of the internally managed AspectJ async execution aspect. + */ + public static final String ASYNC_EXECUTION_ASPECT_BEAN_NAME = + "org.springframework.scheduling.config.internalAsyncExecutionAspect"; + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/TaskNamespaceHandler.java b/spring-context/src/main/java/org/springframework/scheduling/config/TaskNamespaceHandler.java new file mode 100644 index 0000000..3c0fdce --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/config/TaskNamespaceHandler.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.config; + +import org.springframework.beans.factory.xml.NamespaceHandlerSupport; + +/** + * {@code NamespaceHandler} for the 'task' namespace. + * + * @author Mark Fisher + * @since 3.0 + */ +public class TaskNamespaceHandler extends NamespaceHandlerSupport { + + @Override + public void init() { + this.registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenBeanDefinitionParser()); + this.registerBeanDefinitionParser("executor", new ExecutorBeanDefinitionParser()); + this.registerBeanDefinitionParser("scheduled-tasks", new ScheduledTasksBeanDefinitionParser()); + this.registerBeanDefinitionParser("scheduler", new SchedulerBeanDefinitionParser()); + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/TriggerTask.java b/spring-context/src/main/java/org/springframework/scheduling/config/TriggerTask.java new file mode 100644 index 0000000..a35a049 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/config/TriggerTask.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.config; + +import org.springframework.scheduling.Trigger; +import org.springframework.util.Assert; + +/** + * {@link Task} implementation defining a {@code Runnable} to be executed + * according to a given {@link Trigger}. + * + * @author Chris Beams + * @since 3.2 + * @see ScheduledTaskRegistrar#addTriggerTask(TriggerTask) + * @see org.springframework.scheduling.TaskScheduler#schedule(Runnable, Trigger) + */ +public class TriggerTask extends Task { + + private final Trigger trigger; + + + /** + * Create a new {@link TriggerTask}. + * @param runnable the underlying task to execute + * @param trigger specifies when the task should be executed + */ + public TriggerTask(Runnable runnable, Trigger trigger) { + super(runnable); + Assert.notNull(trigger, "Trigger must not be null"); + this.trigger = trigger; + } + + + /** + * Return the associated trigger. + */ + public Trigger getTrigger() { + return this.trigger; + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/package-info.java b/spring-context/src/main/java/org/springframework/scheduling/config/package-info.java new file mode 100644 index 0000000..3ddfc3c --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/config/package-info.java @@ -0,0 +1,10 @@ +/** + * Support package for declarative scheduling configuration, + * with XML schema being the primary configuration format. + */ +@NonNullApi +@NonNullFields +package org.springframework.scheduling.config; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/scheduling/package-info.java b/spring-context/src/main/java/org/springframework/scheduling/package-info.java new file mode 100644 index 0000000..8880b4b --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/package-info.java @@ -0,0 +1,10 @@ +/** + * General exceptions for Spring's scheduling support, + * independent of any specific scheduling system. + */ +@NonNullApi +@NonNullFields +package org.springframework.scheduling; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java b/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java new file mode 100644 index 0000000..f000472 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java @@ -0,0 +1,280 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.support; + +import java.time.DateTimeException; +import java.time.temporal.Temporal; +import java.time.temporal.ValueRange; +import java.util.BitSet; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Efficient {@link BitSet}-based extension of {@link CronField}. + * Created using the {@code parse*} methods. + * + * @author Arjen Poutsma + * @since 5.3 + */ +final class BitsCronField extends CronField { + + private static final long MASK = 0xFFFFFFFFFFFFFFFFL; + + + @Nullable + private static BitsCronField zeroNanos = null; + + + // we store at most 60 bits, for seconds and minutes, so a 64-bit long suffices + private long bits; + + + private BitsCronField(Type type) { + super(type); + } + + /** + * Return a {@code BitsCronField} enabled for 0 nano seconds. + */ + public static BitsCronField zeroNanos() { + if (zeroNanos == null) { + BitsCronField field = new BitsCronField(Type.NANO); + field.setBit(0); + zeroNanos = field; + } + return zeroNanos; + } + + /** + * Parse the given value into a seconds {@code BitsCronField}, the first entry of a cron expression. + */ + public static BitsCronField parseSeconds(String value) { + return parseField(value, Type.SECOND); + } + + /** + * Parse the given value into a minutes {@code BitsCronField}, the second entry of a cron expression. + */ + public static BitsCronField parseMinutes(String value) { + return BitsCronField.parseField(value, Type.MINUTE); + } + + /** + * Parse the given value into a hours {@code BitsCronField}, the third entry of a cron expression. + */ + public static BitsCronField parseHours(String value) { + return BitsCronField.parseField(value, Type.HOUR); + } + + /** + * Parse the given value into a days of months {@code BitsCronField}, the fourth entry of a cron expression. + */ + public static BitsCronField parseDaysOfMonth(String value) { + return parseDate(value, Type.DAY_OF_MONTH); + } + + /** + * Parse the given value into a month {@code BitsCronField}, the fifth entry of a cron expression. + */ + public static BitsCronField parseMonth(String value) { + return BitsCronField.parseField(value, Type.MONTH); + } + + /** + * Parse the given value into a days of week {@code BitsCronField}, the sixth entry of a cron expression. + */ + public static BitsCronField parseDaysOfWeek(String value) { + BitsCronField result = parseDate(value, Type.DAY_OF_WEEK); + if (result.getBit(0)) { + // cron supports 0 for Sunday; we use 7 like java.time + result.setBit(7); + result.clearBit(0); + } + return result; + } + + + private static BitsCronField parseDate(String value, BitsCronField.Type type) { + if (value.equals("?")) { + value = "*"; + } + return BitsCronField.parseField(value, type); + } + + private static BitsCronField parseField(String value, Type type) { + Assert.hasLength(value, "Value must not be empty"); + Assert.notNull(type, "Type must not be null"); + try { + BitsCronField result = new BitsCronField(type); + String[] fields = StringUtils.delimitedListToStringArray(value, ","); + for (String field : fields) { + int slashPos = field.indexOf('/'); + if (slashPos == -1) { + ValueRange range = parseRange(field, type); + result.setBits(range); + } + else { + String rangeStr = value.substring(0, slashPos); + String deltaStr = value.substring(slashPos + 1); + ValueRange range = parseRange(rangeStr, type); + if (rangeStr.indexOf('-') == -1) { + range = ValueRange.of(range.getMinimum(), type.range().getMaximum()); + } + int delta = Integer.parseInt(deltaStr); + if (delta <= 0) { + throw new IllegalArgumentException("Incrementer delta must be 1 or higher"); + } + result.setBits(range, delta); + } + } + return result; + } + catch (DateTimeException | IllegalArgumentException ex) { + String msg = ex.getMessage() + " '" + value + "'"; + throw new IllegalArgumentException(msg, ex); + } + } + + private static ValueRange parseRange(String value, Type type) { + if (value.equals("*")) { + return type.range(); + } + else { + int hyphenPos = value.indexOf('-'); + if (hyphenPos == -1) { + int result = type.checkValidValue(Integer.parseInt(value)); + return ValueRange.of(result, result); + } + else { + int min = Integer.parseInt(value.substring(0, hyphenPos)); + int max = Integer.parseInt(value.substring(hyphenPos + 1)); + min = type.checkValidValue(min); + max = type.checkValidValue(max); + return ValueRange.of(min, max); + } + } + } + + @Nullable + @Override + public > T nextOrSame(T temporal) { + int current = type().get(temporal); + int next = nextSetBit(current); + if (next == -1) { + temporal = type().rollForward(temporal); + next = nextSetBit(0); + } + if (next == current) { + return temporal; + } + else { + int count = 0; + current = type().get(temporal); + while (current != next && count++ < CronExpression.MAX_ATTEMPTS) { + temporal = type().elapseUntil(temporal, next); + current = type().get(temporal); + } + if (count >= CronExpression.MAX_ATTEMPTS) { + return null; + } + return type().reset(temporal); + } + } + + boolean getBit(int index) { + return (this.bits & (1L << index)) != 0; + } + + private int nextSetBit(int fromIndex) { + long result = this.bits & (MASK << fromIndex); + if (result != 0) { + return Long.numberOfTrailingZeros(result); + } + else { + return -1; + } + + } + + private void setBits(ValueRange range) { + if (range.getMinimum() == range.getMaximum()) { + setBit((int) range.getMinimum()); + } + else { + long minMask = MASK << range.getMinimum(); + long maxMask = MASK >>> - (range.getMaximum() + 1); + this.bits |= (minMask & maxMask); + } + } + + private void setBits(ValueRange range, int delta) { + if (delta == 1) { + setBits(range); + } + else { + for (int i = (int) range.getMinimum(); i <= range.getMaximum(); i += delta) { + setBit(i); + } + } + } + + private void setBit(int index) { + this.bits |= (1L << index); + } + + private void clearBit(int index) { + this.bits &= ~(1L << index); + } + + @Override + public int hashCode() { + return Long.hashCode(this.bits); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof BitsCronField)) { + return false; + } + BitsCronField other = (BitsCronField) o; + return type() == other.type() && this.bits == other.bits; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(type().toString()); + builder.append(" {"); + int i = nextSetBit(0); + if (i != -1) { + builder.append(i); + i = nextSetBit(i+1); + while (i != -1) { + builder.append(", "); + builder.append(i); + i = nextSetBit(i+1); + } + } + builder.append('}'); + return builder.toString(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java new file mode 100644 index 0000000..08a6444 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java @@ -0,0 +1,282 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.support; + +import java.time.temporal.ChronoUnit; +import java.time.temporal.Temporal; +import java.util.Arrays; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Representation of a + * crontab expression + * that can calculate the next time it matches. + * + *

    {@code CronExpression} instances are created through + * {@link #parse(String)}; the next match is determined with + * {@link #next(Temporal)}. + * + * @author Arjen Poutsma + * @since 5.3 + * @see CronTrigger + */ +public final class CronExpression { + + static final int MAX_ATTEMPTS = 366; + + private static final String[] MACROS = new String[] { + "@yearly", "0 0 0 1 1 *", + "@annually", "0 0 0 1 1 *", + "@monthly", "0 0 0 1 * *", + "@weekly", "0 0 0 * * 0", + "@daily", "0 0 0 * * *", + "@midnight", "0 0 0 * * *", + "@hourly", "0 0 * * * *" + }; + + + private final CronField[] fields; + + private final String expression; + + + private CronExpression( + CronField seconds, + CronField minutes, + CronField hours, + CronField daysOfMonth, + CronField months, + CronField daysOfWeek, + String expression) { + + // to make sure we end up at 0 nanos, we add an extra field + this.fields = new CronField[]{CronField.zeroNanos(), seconds, minutes, hours, daysOfMonth, months, daysOfWeek}; + this.expression = expression; + } + + + /** + * Parse the given + * crontab expression + * string into a {@code CronExpression}. + * The string has six single space-separated time and date fields: + *

    +	 * ┌───────────── second (0-59)
    +	 * │ ┌───────────── minute (0 - 59)
    +	 * │ │ ┌───────────── hour (0 - 23)
    +	 * │ │ │ ┌───────────── day of the month (1 - 31)
    +	 * │ │ │ │ ┌───────────── month (1 - 12) (or JAN-DEC)
    +	 * │ │ │ │ │ ┌───────────── day of the week (0 - 7)
    +	 * │ │ │ │ │ │          (0 or 7 is Sunday, or MON-SUN)
    +	 * │ │ │ │ │ │
    +	 * * * * * * *
    +	 * 
    + * + *

    The following rules apply: + *

      + *
    • + * A field may be an asterisk ({@code *}), which always stands for + * "first-last". For the "day of the month" or "day of the week" fields, a + * question mark ({@code ?}) may be used instead of an asterisk. + *
    • + *
    • + * Ranges of numbers are expressed by two numbers separated with a hyphen + * ({@code -}). The specified range is inclusive. + *
    • + *
    • Following a range (or {@code *}) with {@code /n} specifies + * the interval of the number's value through the range. + *
    • + *
    • + * English names can also be used for the "month" and "day of week" fields. + * Use the first three letters of the particular day or month (case does not + * matter). + *
    • + *
    • + * The "day of month" and "day of week" fields can contain a + * {@code L}-character, which stands for "last", and has a different meaning + * in each field: + *
        + *
      • + * In the "day of month" field, {@code L} stands for "the last day of the + * month". If followed by an negative offset (i.e. {@code L-n}), it means + * "{@code n}th-to-last day of the month". If followed by {@code W} (i.e. + * {@code LW}), it means "the last weekday of the month". + *
      • + *
      • + * In the "day of week" field, {@code L} stands for "the last day of the + * week". + * If prefixed by a number or three-letter name (i.e. {@code dL} or + * {@code DDDL}), it means "the last day of week {@code d} (or {@code DDD}) + * in the month". + *
      • + *
      + *
    • + *
    • + * The "day of month" field can be {@code nW}, which stands for "the nearest + * weekday to day of the month {@code n}". + * If {@code n} falls on Saturday, this yields the Friday before it. + * If {@code n} falls on Sunday, this yields the Monday after, + * which also happens if {@code n} is {@code 1} and falls on a Saturday + * (i.e. {@code 1W} stands for "the first weekday of the month"). + *
    • + *
    • + * The "day of week" field can be {@code d#n} (or {@code DDD#n}), which + * stands for "the {@code n}-th day of week {@code d} (or {@code DDD}) in + * the month". + *
    • + *
    + * + *

    Example expressions: + *

      + *
    • {@code "0 0 * * * *"} = the top of every hour of every day.
    • + *
    • "*/10 * * * * *" = every ten seconds.
    • + *
    • {@code "0 0 8-10 * * *"} = 8, 9 and 10 o'clock of every day.
    • + *
    • {@code "0 0 6,19 * * *"} = 6:00 AM and 7:00 PM every day.
    • + *
    • {@code "0 0/30 8-10 * * *"} = 8:00, 8:30, 9:00, 9:30, 10:00 and 10:30 every day.
    • + *
    • {@code "0 0 9-17 * * MON-FRI"} = on the hour nine-to-five weekdays
    • + *
    • {@code "0 0 0 25 12 ?"} = every Christmas Day at midnight
    • + *
    • {@code "0 0 0 L * *"} = last day of the month at midnight
    • + *
    • {@code "0 0 0 L-3 * *"} = third-to-last day of the month at midnight
    • + *
    • {@code "0 0 0 1W * *"} = first weekday of the month at midnight
    • + *
    • {@code "0 0 0 LW * *"} = last weekday of the month at midnight
    • + *
    • {@code "0 0 0 * * 5L"} = last Friday of the month at midnight
    • + *
    • {@code "0 0 0 * * THUL"} = last Thursday of the month at midnight
    • + *
    • {@code "0 0 0 ? * 5#2"} = the second Friday in the month at midnight
    • + *
    • {@code "0 0 0 ? * MON#1"} = the first Monday in the month at midnight
    • + *
    + * + *

    The following macros are also supported: + *

      + *
    • {@code "@yearly"} (or {@code "@annually"}) to run un once a year, i.e. {@code "0 0 0 1 1 *"},
    • + *
    • {@code "@monthly"} to run once a month, i.e. {@code "0 0 0 1 * *"},
    • + *
    • {@code "@weekly"} to run once a week, i.e. {@code "0 0 0 * * 0"},
    • + *
    • {@code "@daily"} (or {@code "@midnight"}) to run once a day, i.e. {@code "0 0 0 * * *"},
    • + *
    • {@code "@hourly"} to run once an hour, i.e. {@code "0 0 * * * *"}.
    • + *
    + * + * @param expression the expression string to parse + * @return the parsed {@code CronExpression} object + * @throws IllegalArgumentException in the expression does not conform to + * the cron format + */ + public static CronExpression parse(String expression) { + Assert.hasLength(expression, "Expression string must not be empty"); + + expression = resolveMacros(expression); + + String[] fields = StringUtils.tokenizeToStringArray(expression, " "); + if (fields.length != 6) { + throw new IllegalArgumentException(String.format( + "Cron expression must consist of 6 fields (found %d in \"%s\")", fields.length, expression)); + } + try { + CronField seconds = CronField.parseSeconds(fields[0]); + CronField minutes = CronField.parseMinutes(fields[1]); + CronField hours = CronField.parseHours(fields[2]); + CronField daysOfMonth = CronField.parseDaysOfMonth(fields[3]); + CronField months = CronField.parseMonth(fields[4]); + CronField daysOfWeek = CronField.parseDaysOfWeek(fields[5]); + + return new CronExpression(seconds, minutes, hours, daysOfMonth, months, daysOfWeek, expression); + } + catch (IllegalArgumentException ex) { + String msg = ex.getMessage() + " in cron expression \"" + expression + "\""; + throw new IllegalArgumentException(msg, ex); + } + } + + + private static String resolveMacros(String expression) { + expression = expression.trim(); + for (int i = 0; i < MACROS.length; i = i + 2) { + if (MACROS[i].equalsIgnoreCase(expression)) { + return MACROS[i + 1]; + } + } + return expression; + } + + + /** + * Calculate the next {@link Temporal} that matches this expression. + * @param temporal the seed value + * @param the type of temporal + * @return the next temporal that matches this expression, or {@code null} + * if no such temporal can be found + */ + @Nullable + public > T next(T temporal) { + return nextOrSame(ChronoUnit.NANOS.addTo(temporal, 1)); + } + + + @Nullable + private > T nextOrSame(T temporal) { + for (int i = 0; i < MAX_ATTEMPTS; i++) { + T result = nextOrSameInternal(temporal); + if (result == null || result.equals(temporal)) { + return result; + } + temporal = result; + } + return null; + } + + @Nullable + private > T nextOrSameInternal(T temporal) { + for (CronField field : this.fields) { + temporal = field.nextOrSame(temporal); + if (temporal == null) { + return null; + } + } + return temporal; + } + + + @Override + public int hashCode() { + return Arrays.hashCode(this.fields); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o instanceof CronExpression) { + CronExpression other = (CronExpression) o; + return Arrays.equals(this.fields, other.fields); + } + else { + return false; + } + } + + /** + * Return the expression string used to create this {@code CronExpression}. + * @return the expression string + */ + @Override + public String toString() { + return this.expression; + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java new file mode 100644 index 0000000..ca57bb9 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java @@ -0,0 +1,262 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.support; + +import java.time.DateTimeException; +import java.time.temporal.ChronoField; +import java.time.temporal.Temporal; +import java.time.temporal.ValueRange; + +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Single field in a cron pattern. Created using the {@code parse*} methods, + * main and only entry point is {@link #nextOrSame(Temporal)}. + * + * @author Arjen Poutsma + * @since 5.3 + */ +abstract class CronField { + + private static final String[] MONTHS = new String[]{"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", + "OCT", "NOV", "DEC"}; + + private static final String[] DAYS = new String[]{"MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"}; + + private final Type type; + + + protected CronField(Type type) { + this.type = type; + } + + /** + * Return a {@code CronField} enabled for 0 nano seconds. + */ + public static CronField zeroNanos() { + return BitsCronField.zeroNanos(); + } + + /** + * Parse the given value into a seconds {@code CronField}, the first entry of a cron expression. + */ + public static CronField parseSeconds(String value) { + return BitsCronField.parseSeconds(value); + } + + /** + * Parse the given value into a minutes {@code CronField}, the second entry of a cron expression. + */ + public static CronField parseMinutes(String value) { + return BitsCronField.parseMinutes(value); + } + + /** + * Parse the given value into a hours {@code CronField}, the third entry of a cron expression. + */ + public static CronField parseHours(String value) { + return BitsCronField.parseHours(value); + } + + /** + * Parse the given value into a days of months {@code CronField}, the fourth entry of a cron expression. + */ + public static CronField parseDaysOfMonth(String value) { + if (value.contains("L") || value.contains("W")) { + return QuartzCronField.parseDaysOfMonth(value); + } + else { + return BitsCronField.parseDaysOfMonth(value); + } + } + + /** + * Parse the given value into a month {@code CronField}, the fifth entry of a cron expression. + */ + public static CronField parseMonth(String value) { + value = replaceOrdinals(value, MONTHS); + return BitsCronField.parseMonth(value); + } + + /** + * Parse the given value into a days of week {@code CronField}, the sixth entry of a cron expression. + */ + public static CronField parseDaysOfWeek(String value) { + value = replaceOrdinals(value, DAYS); + if (value.contains("L") || value.contains("#")) { + return QuartzCronField.parseDaysOfWeek(value); + } + else { + return BitsCronField.parseDaysOfWeek(value); + } + } + + + private static String replaceOrdinals(String value, String[] list) { + value = value.toUpperCase(); + for (int i = 0; i < list.length; i++) { + String replacement = Integer.toString(i + 1); + value = StringUtils.replace(value, list[i], replacement); + } + return value; + } + + + /** + * Get the next or same {@link Temporal} in the sequence matching this + * cron field. + * @param temporal the seed value + * @return the next or same temporal matching the pattern + */ + @Nullable + public abstract > T nextOrSame(T temporal); + + + protected Type type() { + return this.type; + } + + + /** + * Represents the type of cron field, i.e. seconds, minutes, hours, + * day-of-month, month, day-of-week. + */ + protected enum Type { + NANO(ChronoField.NANO_OF_SECOND), + SECOND(ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), + MINUTE(ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), + HOUR(ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), + DAY_OF_MONTH(ChronoField.DAY_OF_MONTH, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), + MONTH(ChronoField.MONTH_OF_YEAR, ChronoField.DAY_OF_MONTH, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), + DAY_OF_WEEK(ChronoField.DAY_OF_WEEK, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND); + + + private final ChronoField field; + + private final ChronoField[] lowerOrders; + + + Type(ChronoField field, ChronoField... lowerOrders) { + this.field = field; + this.lowerOrders = lowerOrders; + } + + + /** + * Return the value of this type for the given temporal. + * @return the value of this type + */ + public int get(Temporal date) { + return date.get(this.field); + } + + /** + * Return the general range of this type. For instance, this methods + * will return 0-31 for {@link #MONTH}. + * @return the range of this field + */ + public ValueRange range() { + return this.field.range(); + } + + /** + * Check whether the given value is valid, i.e. whether it falls in + * {@linkplain #range() range}. + * @param value the value to check + * @return the value that was passed in + * @throws IllegalArgumentException if the given value is invalid + */ + public int checkValidValue(int value) { + if (this == DAY_OF_WEEK && value == 0) { + return value; + } + else { + try { + return this.field.checkValidIntValue(value); + } + catch (DateTimeException ex) { + throw new IllegalArgumentException(ex.getMessage(), ex); + } + } + } + + /** + * Elapse the given temporal for the difference between the current + * value of this field and the goal value. Typically, the returned + * temporal will have the given goal as the current value for this type, + * but this is not the case for {@link #DAY_OF_MONTH}. For instance, + * if {@code goal} is 31, and {@code temporal} is April 16th, + * this method returns May 1st, because April 31st does not exist. + * @param temporal the temporal to elapse + * @param goal the goal value + * @param the type of temporal + * @return the elapsed temporal, typically with {@code goal} as value + * for this type. + */ + public > T elapseUntil(T temporal, int goal) { + int current = get(temporal); + if (current < goal) { + return this.field.getBaseUnit().addTo(temporal, goal - current); + } + else { + ValueRange range = temporal.range(this.field); + long amount = goal + range.getMaximum() - current + 1 - range.getMinimum(); + return this.field.getBaseUnit().addTo(temporal, amount); + } + } + + /** + * Roll forward the give temporal until it reaches the next higher + * order field. Calling this method is equivalent to calling + * {@link #elapseUntil(Temporal, int)} with goal set to the + * minimum value of this field's range. + * @param temporal the temporal to roll forward + * @param the type of temporal + * @return the rolled forward temporal + */ + public > T rollForward(T temporal) { + int current = get(temporal); + ValueRange range = temporal.range(this.field); + long amount = range.getMaximum() - current + 1; + return this.field.getBaseUnit().addTo(temporal, amount); + } + + /** + * Reset this and all lower order fields of the given temporal to their + * minimum value. For instance for {@link #MINUTE}, this method + * resets nanos, seconds, and minutes to 0. + * @param temporal the temporal to reset + * @param the type of temporal + * @return the reset temporal + */ + public T reset(T temporal) { + for (ChronoField lowerOrder : this.lowerOrders) { + if (temporal.isSupported(lowerOrder)) { + temporal = lowerOrder.adjustInto(temporal, temporal.range(lowerOrder).getMinimum()); + } + } + return temporal; + } + + @Override + public String toString() { + return this.field.toString(); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronSequenceGenerator.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronSequenceGenerator.java new file mode 100644 index 0000000..1f2b90f --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronSequenceGenerator.java @@ -0,0 +1,461 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.support; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.TimeZone; + +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Date sequence generator for a + * Crontab pattern, + * allowing clients to specify a pattern that the sequence matches. + * + *

    The pattern is a list of six single space-separated fields: representing + * second, minute, hour, day, month, weekday. Month and weekday names can be + * given as the first three letters of the English names. + * + *

    Example patterns: + *

      + *
    • "0 0 * * * *" = the top of every hour of every day.
    • + *
    • "*/10 * * * * *" = every ten seconds.
    • + *
    • "0 0 8-10 * * *" = 8, 9 and 10 o'clock of every day.
    • + *
    • "0 0 6,19 * * *" = 6:00 AM and 7:00 PM every day.
    • + *
    • "0 0/30 8-10 * * *" = 8:00, 8:30, 9:00, 9:30, 10:00 and 10:30 every day.
    • + *
    • "0 0 9-17 * * MON-FRI" = on the hour nine-to-five weekdays
    • + *
    • "0 0 0 25 12 ?" = every Christmas Day at midnight
    • + *
    + * + * @author Dave Syer + * @author Juergen Hoeller + * @author Ruslan Sibgatullin + * @since 3.0 + * @see CronTrigger + * @deprecated as of 5.3, in favor of {@link CronExpression} + */ +@Deprecated +public class CronSequenceGenerator { + + private final String expression; + + @Nullable + private final TimeZone timeZone; + + private final BitSet months = new BitSet(12); + + private final BitSet daysOfMonth = new BitSet(31); + + private final BitSet daysOfWeek = new BitSet(7); + + private final BitSet hours = new BitSet(24); + + private final BitSet minutes = new BitSet(60); + + private final BitSet seconds = new BitSet(60); + + + /** + * Construct a {@code CronSequenceGenerator} from the pattern provided, + * using the default {@link TimeZone}. + * @param expression a space-separated list of time fields + * @throws IllegalArgumentException if the pattern cannot be parsed + * @see java.util.TimeZone#getDefault() + * @deprecated as of 5.3, in favor of {@link CronExpression#parse(String)} + */ + @Deprecated + public CronSequenceGenerator(String expression) { + this(expression, TimeZone.getDefault()); + } + + /** + * Construct a {@code CronSequenceGenerator} from the pattern provided, + * using the specified {@link TimeZone}. + * @param expression a space-separated list of time fields + * @param timeZone the TimeZone to use for generated trigger times + * @throws IllegalArgumentException if the pattern cannot be parsed + * @deprecated as of 5.3, in favor of {@link CronExpression#parse(String)} + */ + @Deprecated + public CronSequenceGenerator(String expression, TimeZone timeZone) { + this.expression = expression; + this.timeZone = timeZone; + parse(expression); + } + + private CronSequenceGenerator(String expression, String[] fields) { + this.expression = expression; + this.timeZone = null; + doParse(fields); + } + + + /** + * Return the cron pattern that this sequence generator has been built for. + */ + String getExpression() { + return this.expression; + } + + + /** + * Get the next {@link Date} in the sequence matching the Cron pattern and + * after the value provided. The return value will have a whole number of + * seconds, and will be after the input value. + * @param date a seed value + * @return the next value matching the pattern + */ + public Date next(Date date) { + /* + The plan: + + 1 Start with whole second (rounding up if necessary) + + 2 If seconds match move on, otherwise find the next match: + 2.1 If next match is in the next minute then roll forwards + + 3 If minute matches move on, otherwise find the next match + 3.1 If next match is in the next hour then roll forwards + 3.2 Reset the seconds and go to 2 + + 4 If hour matches move on, otherwise find the next match + 4.1 If next match is in the next day then roll forwards, + 4.2 Reset the minutes and seconds and go to 2 + */ + + Calendar calendar = new GregorianCalendar(); + calendar.setTimeZone(this.timeZone); + calendar.setTime(date); + + // First, just reset the milliseconds and try to calculate from there... + calendar.set(Calendar.MILLISECOND, 0); + long originalTimestamp = calendar.getTimeInMillis(); + doNext(calendar, calendar.get(Calendar.YEAR)); + + if (calendar.getTimeInMillis() == originalTimestamp) { + // We arrived at the original timestamp - round up to the next whole second and try again... + calendar.add(Calendar.SECOND, 1); + doNext(calendar, calendar.get(Calendar.YEAR)); + } + + return calendar.getTime(); + } + + private void doNext(Calendar calendar, int dot) { + List resets = new ArrayList<>(); + + int second = calendar.get(Calendar.SECOND); + List emptyList = Collections.emptyList(); + int updateSecond = findNext(this.seconds, second, calendar, Calendar.SECOND, Calendar.MINUTE, emptyList); + if (second == updateSecond) { + resets.add(Calendar.SECOND); + } + + int minute = calendar.get(Calendar.MINUTE); + int updateMinute = findNext(this.minutes, minute, calendar, Calendar.MINUTE, Calendar.HOUR_OF_DAY, resets); + if (minute == updateMinute) { + resets.add(Calendar.MINUTE); + } + else { + doNext(calendar, dot); + } + + int hour = calendar.get(Calendar.HOUR_OF_DAY); + int updateHour = findNext(this.hours, hour, calendar, Calendar.HOUR_OF_DAY, Calendar.DAY_OF_WEEK, resets); + if (hour == updateHour) { + resets.add(Calendar.HOUR_OF_DAY); + } + else { + doNext(calendar, dot); + } + + int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK); + int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH); + int updateDayOfMonth = findNextDay(calendar, this.daysOfMonth, dayOfMonth, this.daysOfWeek, dayOfWeek, resets); + if (dayOfMonth == updateDayOfMonth) { + resets.add(Calendar.DAY_OF_MONTH); + } + else { + doNext(calendar, dot); + } + + int month = calendar.get(Calendar.MONTH); + int updateMonth = findNext(this.months, month, calendar, Calendar.MONTH, Calendar.YEAR, resets); + if (month != updateMonth) { + if (calendar.get(Calendar.YEAR) - dot > 4) { + throw new IllegalArgumentException("Invalid cron expression \"" + this.expression + + "\" led to runaway search for next trigger"); + } + doNext(calendar, dot); + } + + } + + private int findNextDay(Calendar calendar, BitSet daysOfMonth, int dayOfMonth, BitSet daysOfWeek, int dayOfWeek, + List resets) { + + int count = 0; + int max = 366; + // the DAY_OF_WEEK values in java.util.Calendar start with 1 (Sunday), + // but in the cron pattern, they start with 0, so we subtract 1 here + while ((!daysOfMonth.get(dayOfMonth) || !daysOfWeek.get(dayOfWeek - 1)) && count++ < max) { + calendar.add(Calendar.DAY_OF_MONTH, 1); + dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH); + dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK); + reset(calendar, resets); + } + if (count >= max) { + throw new IllegalArgumentException("Overflow in day for expression \"" + this.expression + "\""); + } + return dayOfMonth; + } + + /** + * Search the bits provided for the next set bit after the value provided, + * and reset the calendar. + * @param bits a {@link BitSet} representing the allowed values of the field + * @param value the current value of the field + * @param calendar the calendar to increment as we move through the bits + * @param field the field to increment in the calendar (@see + * {@link Calendar} for the static constants defining valid fields) + * @param lowerOrders the Calendar field ids that should be reset (i.e. the + * ones of lower significance than the field of interest) + * @return the value of the calendar field that is next in the sequence + */ + private int findNext(BitSet bits, int value, Calendar calendar, int field, int nextField, List lowerOrders) { + int nextValue = bits.nextSetBit(value); + // roll over if needed + if (nextValue == -1) { + calendar.add(nextField, 1); + reset(calendar, Collections.singletonList(field)); + nextValue = bits.nextSetBit(0); + } + if (nextValue != value) { + calendar.set(field, nextValue); + reset(calendar, lowerOrders); + } + return nextValue; + } + + /** + * Reset the calendar setting all the fields provided to zero. + */ + private void reset(Calendar calendar, List fields) { + for (int field : fields) { + calendar.set(field, field == Calendar.DAY_OF_MONTH ? 1 : 0); + } + } + + + // Parsing logic invoked by the constructor + + /** + * Parse the given pattern expression. + */ + private void parse(String expression) throws IllegalArgumentException { + String[] fields = StringUtils.tokenizeToStringArray(expression, " "); + if (!areValidCronFields(fields)) { + throw new IllegalArgumentException(String.format( + "Cron expression must consist of 6 fields (found %d in \"%s\")", fields.length, expression)); + } + doParse(fields); + } + + private void doParse(String[] fields) { + setNumberHits(this.seconds, fields[0], 0, 60); + setNumberHits(this.minutes, fields[1], 0, 60); + setNumberHits(this.hours, fields[2], 0, 24); + setDaysOfMonth(this.daysOfMonth, fields[3]); + setMonths(this.months, fields[4]); + setDays(this.daysOfWeek, replaceOrdinals(fields[5], "SUN,MON,TUE,WED,THU,FRI,SAT"), 8); + + if (this.daysOfWeek.get(7)) { + // Sunday can be represented as 0 or 7 + this.daysOfWeek.set(0); + this.daysOfWeek.clear(7); + } + } + + /** + * Replace the values in the comma-separated list (case insensitive) + * with their index in the list. + * @return a new String with the values from the list replaced + */ + private String replaceOrdinals(String value, String commaSeparatedList) { + String[] list = StringUtils.commaDelimitedListToStringArray(commaSeparatedList); + for (int i = 0; i < list.length; i++) { + String item = list[i].toUpperCase(); + value = StringUtils.replace(value.toUpperCase(), item, "" + i); + } + return value; + } + + private void setDaysOfMonth(BitSet bits, String field) { + int max = 31; + // Days of month start with 1 (in Cron and Calendar) so add one + setDays(bits, field, max + 1); + // ... and remove it from the front + bits.clear(0); + } + + private void setDays(BitSet bits, String field, int max) { + if (field.contains("?")) { + field = "*"; + } + setNumberHits(bits, field, 0, max); + } + + private void setMonths(BitSet bits, String value) { + int max = 12; + value = replaceOrdinals(value, "FOO,JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC"); + BitSet months = new BitSet(13); + // Months start with 1 in Cron and 0 in Calendar, so push the values first into a longer bit set + setNumberHits(months, value, 1, max + 1); + // ... and then rotate it to the front of the months + for (int i = 1; i <= max; i++) { + if (months.get(i)) { + bits.set(i - 1); + } + } + } + + private void setNumberHits(BitSet bits, String value, int min, int max) { + String[] fields = StringUtils.delimitedListToStringArray(value, ","); + for (String field : fields) { + if (!field.contains("/")) { + // Not an incrementer so it must be a range (possibly empty) + int[] range = getRange(field, min, max); + bits.set(range[0], range[1] + 1); + } + else { + String[] split = StringUtils.delimitedListToStringArray(field, "/"); + if (split.length > 2) { + throw new IllegalArgumentException("Incrementer has more than two fields: '" + + field + "' in expression \"" + this.expression + "\""); + } + int[] range = getRange(split[0], min, max); + if (!split[0].contains("-")) { + range[1] = max - 1; + } + int delta = Integer.parseInt(split[1]); + if (delta <= 0) { + throw new IllegalArgumentException("Incrementer delta must be 1 or higher: '" + + field + "' in expression \"" + this.expression + "\""); + } + for (int i = range[0]; i <= range[1]; i += delta) { + bits.set(i); + } + } + } + } + + private int[] getRange(String field, int min, int max) { + int[] result = new int[2]; + if (field.contains("*")) { + result[0] = min; + result[1] = max - 1; + return result; + } + if (!field.contains("-")) { + result[0] = result[1] = Integer.parseInt(field); + } + else { + String[] split = StringUtils.delimitedListToStringArray(field, "-"); + if (split.length > 2) { + throw new IllegalArgumentException("Range has more than two fields: '" + + field + "' in expression \"" + this.expression + "\""); + } + result[0] = Integer.parseInt(split[0]); + result[1] = Integer.parseInt(split[1]); + } + if (result[0] >= max || result[1] >= max) { + throw new IllegalArgumentException("Range exceeds maximum (" + max + "): '" + + field + "' in expression \"" + this.expression + "\""); + } + if (result[0] < min || result[1] < min) { + throw new IllegalArgumentException("Range less than minimum (" + min + "): '" + + field + "' in expression \"" + this.expression + "\""); + } + if (result[0] > result[1]) { + throw new IllegalArgumentException("Invalid inverted range: '" + field + + "' in expression \"" + this.expression + "\""); + } + return result; + } + + + /** + * Determine whether the specified expression represents a valid cron pattern. + * @param expression the expression to evaluate + * @return {@code true} if the given expression is a valid cron expression + * @since 4.3 + */ + public static boolean isValidExpression(@Nullable String expression) { + if (expression == null) { + return false; + } + String[] fields = StringUtils.tokenizeToStringArray(expression, " "); + if (!areValidCronFields(fields)) { + return false; + } + try { + new CronSequenceGenerator(expression, fields); + return true; + } + catch (IllegalArgumentException ex) { + return false; + } + } + + private static boolean areValidCronFields(@Nullable String[] fields) { + return (fields != null && fields.length == 6); + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof CronSequenceGenerator)) { + return false; + } + CronSequenceGenerator otherCron = (CronSequenceGenerator) other; + return (this.months.equals(otherCron.months) && this.daysOfMonth.equals(otherCron.daysOfMonth) && + this.daysOfWeek.equals(otherCron.daysOfWeek) && this.hours.equals(otherCron.hours) && + this.minutes.equals(otherCron.minutes) && this.seconds.equals(otherCron.seconds)); + } + + @Override + public int hashCode() { + return (17 * this.months.hashCode() + 29 * this.daysOfMonth.hashCode() + 37 * this.daysOfWeek.hashCode() + + 41 * this.hours.hashCode() + 53 * this.minutes.hashCode() + 61 * this.seconds.hashCode()); + } + + @Override + public String toString() { + return getClass().getSimpleName() + ": " + this.expression; + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java new file mode 100644 index 0000000..3af7148 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java @@ -0,0 +1,132 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.support; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Date; +import java.util.TimeZone; + +import org.springframework.lang.Nullable; +import org.springframework.scheduling.Trigger; +import org.springframework.scheduling.TriggerContext; +import org.springframework.util.Assert; + +/** + * {@link Trigger} implementation for cron expressions. + * Wraps a {@link CronExpression}. + * + * @author Juergen Hoeller + * @author Arjen Poutsma + * @since 3.0 + * @see CronExpression + */ +public class CronTrigger implements Trigger { + + private final CronExpression expression; + + private final ZoneId zoneId; + + + /** + * Build a {@code CronTrigger} from the pattern provided in the default time zone. + * @param expression a space-separated list of time fields, following cron + * expression conventions + */ + public CronTrigger(String expression) { + this(expression, ZoneId.systemDefault()); + } + + /** + * Build a {@code CronTrigger} from the pattern provided in the given time zone. + * @param expression a space-separated list of time fields, following cron + * expression conventions + * @param timeZone a time zone in which the trigger times will be generated + */ + public CronTrigger(String expression, TimeZone timeZone) { + this(expression, timeZone.toZoneId()); + } + + /** + * Build a {@code CronTrigger} from the pattern provided in the given time zone. + * @param expression a space-separated list of time fields, following cron + * expression conventions + * @param zoneId a time zone in which the trigger times will be generated + * @since 5.3 + * @see CronExpression#parse(String) + */ + public CronTrigger(String expression, ZoneId zoneId) { + Assert.hasLength(expression, "Expression must not be empty"); + Assert.notNull(zoneId, "ZoneId must not be null"); + + this.expression = CronExpression.parse(expression); + this.zoneId = zoneId; + } + + + /** + * Return the cron pattern that this trigger has been built with. + */ + public String getExpression() { + return this.expression.toString(); + } + + + /** + * Determine the next execution time according to the given trigger context. + *

    Next execution times are calculated based on the + * {@linkplain TriggerContext#lastCompletionTime completion time} of the + * previous execution; therefore, overlapping executions won't occur. + */ + @Override + public Date nextExecutionTime(TriggerContext triggerContext) { + Date date = triggerContext.lastCompletionTime(); + if (date != null) { + Date scheduled = triggerContext.lastScheduledExecutionTime(); + if (scheduled != null && date.before(scheduled)) { + // Previous task apparently executed too early... + // Let's simply use the last calculated execution time then, + // in order to prevent accidental re-fires in the same second. + date = scheduled; + } + } + else { + date = new Date(); + } + ZonedDateTime dateTime = ZonedDateTime.ofInstant(date.toInstant(), this.zoneId); + ZonedDateTime next = this.expression.next(dateTime); + return next != null ? Date.from(next.toInstant()) : null; + } + + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof CronTrigger && + this.expression.equals(((CronTrigger) other).expression))); + } + + @Override + public int hashCode() { + return this.expression.hashCode(); + } + + @Override + public String toString() { + return this.expression.toString(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/DelegatingErrorHandlingRunnable.java b/spring-context/src/main/java/org/springframework/scheduling/support/DelegatingErrorHandlingRunnable.java new file mode 100644 index 0000000..e2296d2 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/support/DelegatingErrorHandlingRunnable.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.support; + +import java.lang.reflect.UndeclaredThrowableException; + +import org.springframework.util.Assert; +import org.springframework.util.ErrorHandler; + +/** + * Runnable wrapper that catches any exception or error thrown from its + * delegate Runnable and allows an {@link ErrorHandler} to handle it. + * + * @author Juergen Hoeller + * @author Mark Fisher + * @since 3.0 + */ +public class DelegatingErrorHandlingRunnable implements Runnable { + + private final Runnable delegate; + + private final ErrorHandler errorHandler; + + + /** + * Create a new DelegatingErrorHandlingRunnable. + * @param delegate the Runnable implementation to delegate to + * @param errorHandler the ErrorHandler for handling any exceptions + */ + public DelegatingErrorHandlingRunnable(Runnable delegate, ErrorHandler errorHandler) { + Assert.notNull(delegate, "Delegate must not be null"); + Assert.notNull(errorHandler, "ErrorHandler must not be null"); + this.delegate = delegate; + this.errorHandler = errorHandler; + } + + @Override + public void run() { + try { + this.delegate.run(); + } + catch (UndeclaredThrowableException ex) { + this.errorHandler.handleError(ex.getUndeclaredThrowable()); + } + catch (Throwable ex) { + this.errorHandler.handleError(ex); + } + } + + @Override + public String toString() { + return "DelegatingErrorHandlingRunnable for " + this.delegate; + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/MethodInvokingRunnable.java b/spring-context/src/main/java/org/springframework/scheduling/support/MethodInvokingRunnable.java new file mode 100644 index 0000000..8b354eb --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/support/MethodInvokingRunnable.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.support; + +import java.lang.reflect.InvocationTargetException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.support.ArgumentConvertingMethodInvoker; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * Adapter that implements the {@link Runnable} interface as a configurable + * method invocation based on Spring's MethodInvoker. + * + *

    Inherits common configuration properties from + * {@link org.springframework.util.MethodInvoker}. + * + * @author Juergen Hoeller + * @since 1.2.4 + * @see java.util.concurrent.Executor#execute(Runnable) + */ +public class MethodInvokingRunnable extends ArgumentConvertingMethodInvoker + implements Runnable, BeanClassLoaderAware, InitializingBean { + + protected final Log logger = LogFactory.getLog(getClass()); + + @Nullable + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + @Override + protected Class resolveClassName(String className) throws ClassNotFoundException { + return ClassUtils.forName(className, this.beanClassLoader); + } + + @Override + public void afterPropertiesSet() throws ClassNotFoundException, NoSuchMethodException { + prepare(); + } + + + @Override + public void run() { + try { + invoke(); + } + catch (InvocationTargetException ex) { + logger.error(getInvocationFailureMessage(), ex.getTargetException()); + // Do not throw exception, else the main loop of the scheduler might stop! + } + catch (Throwable ex) { + logger.error(getInvocationFailureMessage(), ex); + // Do not throw exception, else the main loop of the scheduler might stop! + } + } + + /** + * Build a message for an invocation failure exception. + * @return the error message, including the target method name etc + */ + protected String getInvocationFailureMessage() { + return "Invocation of method '" + getTargetMethod() + + "' on target class [" + getTargetClass() + "] failed"; + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/PeriodicTrigger.java b/spring-context/src/main/java/org/springframework/scheduling/support/PeriodicTrigger.java new file mode 100644 index 0000000..8e19a26 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/support/PeriodicTrigger.java @@ -0,0 +1,164 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.support; + +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import org.springframework.lang.Nullable; +import org.springframework.scheduling.Trigger; +import org.springframework.scheduling.TriggerContext; +import org.springframework.util.Assert; + +/** + * A trigger for periodic task execution. The period may be applied as either + * fixed-rate or fixed-delay, and an initial delay value may also be configured. + * The default initial delay is 0, and the default behavior is fixed-delay + * (i.e. the interval between successive executions is measured from each + * completion time). To measure the interval between the + * scheduled start time of each execution instead, set the + * 'fixedRate' property to {@code true}. + * + *

    Note that the TaskScheduler interface already defines methods for scheduling + * tasks at fixed-rate or with fixed-delay. Both also support an optional value + * for the initial delay. Those methods should be used directly whenever + * possible. The value of this Trigger implementation is that it can be used + * within components that rely on the Trigger abstraction. For example, it may + * be convenient to allow periodic triggers, cron-based triggers, and even + * custom Trigger implementations to be used interchangeably. + * + * @author Mark Fisher + * @since 3.0 + */ +public class PeriodicTrigger implements Trigger { + + private final long period; + + private final TimeUnit timeUnit; + + private volatile long initialDelay; + + private volatile boolean fixedRate; + + + /** + * Create a trigger with the given period in milliseconds. + */ + public PeriodicTrigger(long period) { + this(period, null); + } + + /** + * Create a trigger with the given period and time unit. The time unit will + * apply not only to the period but also to any 'initialDelay' value, if + * configured on this Trigger later via {@link #setInitialDelay(long)}. + */ + public PeriodicTrigger(long period, @Nullable TimeUnit timeUnit) { + Assert.isTrue(period >= 0, "period must not be negative"); + this.timeUnit = (timeUnit != null ? timeUnit : TimeUnit.MILLISECONDS); + this.period = this.timeUnit.toMillis(period); + } + + + /** + * Return this trigger's period. + * @since 5.0.2 + */ + public long getPeriod() { + return this.period; + } + + /** + * Return this trigger's time unit (milliseconds by default). + * @since 5.0.2 + */ + public TimeUnit getTimeUnit() { + return this.timeUnit; + } + + /** + * Specify the delay for the initial execution. It will be evaluated in + * terms of this trigger's {@link TimeUnit}. If no time unit was explicitly + * provided upon instantiation, the default is milliseconds. + */ + public void setInitialDelay(long initialDelay) { + this.initialDelay = this.timeUnit.toMillis(initialDelay); + } + + /** + * Return the initial delay, or 0 if none. + * @since 5.0.2 + */ + public long getInitialDelay() { + return this.initialDelay; + } + + /** + * Specify whether the periodic interval should be measured between the + * scheduled start times rather than between actual completion times. + * The latter, "fixed delay" behavior, is the default. + */ + public void setFixedRate(boolean fixedRate) { + this.fixedRate = fixedRate; + } + + /** + * Return whether this trigger uses fixed rate ({@code true}) or + * fixed delay ({@code false}) behavior. + * @since 5.0.2 + */ + public boolean isFixedRate() { + return this.fixedRate; + } + + + /** + * Returns the time after which a task should run again. + */ + @Override + public Date nextExecutionTime(TriggerContext triggerContext) { + Date lastExecution = triggerContext.lastScheduledExecutionTime(); + Date lastCompletion = triggerContext.lastCompletionTime(); + if (lastExecution == null || lastCompletion == null) { + return new Date(triggerContext.getClock().millis() + this.initialDelay); + } + if (this.fixedRate) { + return new Date(lastExecution.getTime() + this.period); + } + return new Date(lastCompletion.getTime() + this.period); + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof PeriodicTrigger)) { + return false; + } + PeriodicTrigger otherTrigger = (PeriodicTrigger) other; + return (this.fixedRate == otherTrigger.fixedRate && this.initialDelay == otherTrigger.initialDelay && + this.period == otherTrigger.period); + } + + @Override + public int hashCode() { + return (this.fixedRate ? 17 : 29) + (int) (37 * this.period) + (int) (41 * this.initialDelay); + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java b/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java new file mode 100644 index 0000000..8492a59 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java @@ -0,0 +1,294 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.support; + +import java.time.DateTimeException; +import java.time.DayOfWeek; +import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; +import java.time.temporal.Temporal; +import java.time.temporal.TemporalAdjuster; +import java.time.temporal.TemporalAdjusters; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Extension of {@link CronField} for + * = 0) { + throw new IllegalArgumentException("Offset '" + offset + " should be < 0 '" + value + "'"); + } + adjuster = lastDayWithOffset(offset); + } + } + return new QuartzCronField(Type.DAY_OF_MONTH, adjuster, value); + } + idx = value.lastIndexOf('W'); + if (idx != -1) { + if (idx == 0) { + throw new IllegalArgumentException("No day-of-month before 'W' in '" + value + "'"); + } + else if (idx != value.length() - 1) { + throw new IllegalArgumentException("Unrecognized characters after 'W' in '" + value + "'"); + } + else { // "[0-9]+W" + int dayOfMonth = Integer.parseInt(value.substring(0, idx)); + dayOfMonth = Type.DAY_OF_MONTH.checkValidValue(dayOfMonth); + TemporalAdjuster adjuster = weekdayNearestTo(dayOfMonth); + return new QuartzCronField(Type.DAY_OF_MONTH, adjuster, value); + } + } + throw new IllegalArgumentException("No 'L' or 'W' found in '" + value + "'"); + } + + /** + * Parse the given value into a days of week {@code QuartzCronField}, the sixth entry of a cron expression. + * Expects a "L" or "#" in the given value. + */ + public static QuartzCronField parseDaysOfWeek(String value) { + int idx = value.lastIndexOf('L'); + if (idx != -1) { + if (idx != value.length() - 1) { + throw new IllegalArgumentException("Unrecognized characters after 'L' in '" + value + "'"); + } + else { + TemporalAdjuster adjuster; + if (idx == 0) { + throw new IllegalArgumentException("No day-of-week before 'L' in '" + value + "'"); + } + else { // "[0-7]L" + DayOfWeek dayOfWeek = parseDayOfWeek(value.substring(0, idx)); + adjuster = TemporalAdjusters.lastInMonth(dayOfWeek); + } + return new QuartzCronField(Type.DAY_OF_WEEK, Type.DAY_OF_MONTH, adjuster, value); + } + } + idx = value.lastIndexOf('#'); + if (idx != -1) { + if (idx == 0) { + throw new IllegalArgumentException("No day-of-week before '#' in '" + value + "'"); + } + else if (idx == value.length() - 1) { + throw new IllegalArgumentException("No ordinal after '#' in '" + value + "'"); + } + // "[0-7]#[0-9]+" + DayOfWeek dayOfWeek = parseDayOfWeek(value.substring(0, idx)); + int ordinal = Integer.parseInt(value.substring(idx + 1)); + + TemporalAdjuster adjuster = TemporalAdjusters.dayOfWeekInMonth(ordinal, dayOfWeek); + return new QuartzCronField(Type.DAY_OF_WEEK, Type.DAY_OF_MONTH, adjuster, value); + } + throw new IllegalArgumentException("No 'L' or '#' found in '" + value + "'"); + } + + + private static DayOfWeek parseDayOfWeek(String value) { + int dayOfWeek = Integer.parseInt(value); + if (dayOfWeek == 0) { + dayOfWeek = 7; // cron is 0 based; java.time 1 based + } + try { + return DayOfWeek.of(dayOfWeek); + } + catch (DateTimeException ex) { + String msg = ex.getMessage() + " '" + value + "'"; + throw new IllegalArgumentException(msg, ex); + } + } + + /** + * Return a temporal adjuster that finds the nth-to-last day of the month. + * @param offset the negative offset, i.e. -3 means third-to-last + * @return a nth-to-last day-of-month adjuster + */ + private static TemporalAdjuster lastDayWithOffset(int offset) { + Assert.isTrue(offset < 0, "Offset should be < 0"); + return temporal -> { + Temporal lastDayOfMonth = TemporalAdjusters.lastDayOfMonth().adjustInto(temporal); + return lastDayOfMonth.plus(offset, ChronoUnit.DAYS); + }; + } + + /** + * Return a temporal adjuster that finds the weekday nearest to the given + * day-of-month. If {@code dayOfMonth} falls on a Saturday, the date is + * moved back to Friday; if it falls on a Sunday (or if {@code dayOfMonth} + * is 1 and it falls on a Saturday), it is moved forward to Monday. + * @param dayOfMonth the goal day-of-month + * @return the weekday-nearest-to adjuster + */ + private static TemporalAdjuster weekdayNearestTo(int dayOfMonth) { + return temporal -> { + int current = Type.DAY_OF_MONTH.get(temporal); + int dayOfWeek = temporal.get(ChronoField.DAY_OF_WEEK); + + if ((current == dayOfMonth && dayOfWeek < 6) || // dayOfMonth is a weekday + (dayOfWeek == 5 && current == dayOfMonth - 1) || // dayOfMonth is a Saturday, so Friday before + (dayOfWeek == 1 && current == dayOfMonth + 1) || // dayOfMonth is a Sunday, so Monday after + (dayOfWeek == 1 && dayOfMonth == 1 && current == 3)) { // dayOfMonth is the 1st, so Monday 3rd + return temporal; + } + int count = 0; + while (count++ < CronExpression.MAX_ATTEMPTS) { + temporal = Type.DAY_OF_MONTH.elapseUntil(cast(temporal), dayOfMonth); + current = Type.DAY_OF_MONTH.get(temporal); + if (current == dayOfMonth) { + dayOfWeek = temporal.get(ChronoField.DAY_OF_WEEK); + + if (dayOfWeek == 6) { // Saturday + if (dayOfMonth != 1) { + return temporal.minus(1, ChronoUnit.DAYS); + } + else { + // exception for "1W" fields: execute on nearest Monday + return temporal.plus(2, ChronoUnit.DAYS); + } + } + else if (dayOfWeek == 7) { // Sunday + return temporal.plus(1, ChronoUnit.DAYS); + } + else { + return temporal; + } + } + } + return null; + }; + } + + @SuppressWarnings("unchecked") + private static > T cast(Temporal temporal) { + return (T) temporal; + } + + + @Override + public > T nextOrSame(T temporal) { + T result = adjust(temporal); + if (result != null) { + if (result.compareTo(temporal) < 0) { + // We ended up before the start, roll forward and try again + temporal = this.rollForwardType.rollForward(temporal); + result = adjust(temporal); + } + } + return result; + } + + + @Nullable + @SuppressWarnings("unchecked") + private > T adjust(T temporal) { + return (T) this.adjuster.adjustInto(temporal); + } + + + @Override + public int hashCode() { + return this.value.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof QuartzCronField)) { + return false; + } + QuartzCronField other = (QuartzCronField) o; + return type() == other.type() && + this.value.equals(other.value); + } + + @Override + public String toString() { + return type() + " '" + this.value + "'"; + + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/ScheduledMethodRunnable.java b/spring-context/src/main/java/org/springframework/scheduling/support/ScheduledMethodRunnable.java new file mode 100644 index 0000000..4977226 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/support/ScheduledMethodRunnable.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.support; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.UndeclaredThrowableException; + +import org.springframework.util.ReflectionUtils; + +/** + * Variant of {@link MethodInvokingRunnable} meant to be used for processing + * of no-arg scheduled methods. Propagates user exceptions to the caller, + * assuming that an error strategy for Runnables is in place. + * + * @author Juergen Hoeller + * @since 3.0.6 + * @see org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor + */ +public class ScheduledMethodRunnable implements Runnable { + + private final Object target; + + private final Method method; + + + /** + * Create a {@code ScheduledMethodRunnable} for the given target instance, + * calling the specified method. + * @param target the target instance to call the method on + * @param method the target method to call + */ + public ScheduledMethodRunnable(Object target, Method method) { + this.target = target; + this.method = method; + } + + /** + * Create a {@code ScheduledMethodRunnable} for the given target instance, + * calling the specified method by name. + * @param target the target instance to call the method on + * @param methodName the name of the target method + * @throws NoSuchMethodException if the specified method does not exist + */ + public ScheduledMethodRunnable(Object target, String methodName) throws NoSuchMethodException { + this.target = target; + this.method = target.getClass().getMethod(methodName); + } + + + /** + * Return the target instance to call the method on. + */ + public Object getTarget() { + return this.target; + } + + /** + * Return the target method to call. + */ + public Method getMethod() { + return this.method; + } + + + @Override + public void run() { + try { + ReflectionUtils.makeAccessible(this.method); + this.method.invoke(this.target); + } + catch (InvocationTargetException ex) { + ReflectionUtils.rethrowRuntimeException(ex.getTargetException()); + } + catch (IllegalAccessException ex) { + throw new UndeclaredThrowableException(ex); + } + } + + @Override + public String toString() { + return this.method.getDeclaringClass().getName() + "." + this.method.getName(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/SimpleTriggerContext.java b/spring-context/src/main/java/org/springframework/scheduling/support/SimpleTriggerContext.java new file mode 100644 index 0000000..2c647c3 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/support/SimpleTriggerContext.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.support; + +import java.time.Clock; +import java.util.Date; + +import org.springframework.lang.Nullable; +import org.springframework.scheduling.TriggerContext; + +/** + * Simple data holder implementation of the {@link TriggerContext} interface. + * + * @author Juergen Hoeller + * @since 3.0 + */ +public class SimpleTriggerContext implements TriggerContext { + + private final Clock clock; + + @Nullable + private volatile Date lastScheduledExecutionTime; + + @Nullable + private volatile Date lastActualExecutionTime; + + @Nullable + private volatile Date lastCompletionTime; + + + /** + * Create a SimpleTriggerContext with all time values set to {@code null}, + * exposing the system clock for the default time zone. + */ + public SimpleTriggerContext() { + this.clock = Clock.systemDefaultZone(); + } + + /** + * Create a SimpleTriggerContext with the given time values, + * exposing the system clock for the default time zone. + * @param lastScheduledExecutionTime last scheduled execution time + * @param lastActualExecutionTime last actual execution time + * @param lastCompletionTime last completion time + */ + public SimpleTriggerContext(Date lastScheduledExecutionTime, Date lastActualExecutionTime, Date lastCompletionTime) { + this(); + this.lastScheduledExecutionTime = lastScheduledExecutionTime; + this.lastActualExecutionTime = lastActualExecutionTime; + this.lastCompletionTime = lastCompletionTime; + } + + /** + * Create a SimpleTriggerContext with all time values set to {@code null}, + * exposing the given clock. + * @param clock the clock to use for trigger calculation + * @since 5.3 + * @see #update(Date, Date, Date) + */ + public SimpleTriggerContext(Clock clock) { + this.clock = clock; + } + + + /** + * Update this holder's state with the latest time values. + * @param lastScheduledExecutionTime last scheduled execution time + * @param lastActualExecutionTime last actual execution time + * @param lastCompletionTime last completion time + */ + public void update(Date lastScheduledExecutionTime, Date lastActualExecutionTime, Date lastCompletionTime) { + this.lastScheduledExecutionTime = lastScheduledExecutionTime; + this.lastActualExecutionTime = lastActualExecutionTime; + this.lastCompletionTime = lastCompletionTime; + } + + + @Override + public Clock getClock() { + return this.clock; + } + + @Override + @Nullable + public Date lastScheduledExecutionTime() { + return this.lastScheduledExecutionTime; + } + + @Override + @Nullable + public Date lastActualExecutionTime() { + return this.lastActualExecutionTime; + } + + @Override + @Nullable + public Date lastCompletionTime() { + return this.lastCompletionTime; + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/TaskUtils.java b/spring-context/src/main/java/org/springframework/scheduling/support/TaskUtils.java new file mode 100644 index 0000000..f27ab89 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/support/TaskUtils.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.support; + +import java.util.concurrent.Future; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.lang.Nullable; +import org.springframework.util.ErrorHandler; +import org.springframework.util.ReflectionUtils; + +/** + * Utility methods for decorating tasks with error handling. + * + *

    NOTE: This class is intended for internal use by Spring's scheduler + * implementations. It is only public so that it may be accessed from impl classes + * within other packages. It is not intended for general use. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @since 3.0 + */ +public abstract class TaskUtils { + + /** + * An ErrorHandler strategy that will log the Exception but perform + * no further handling. This will suppress the error so that + * subsequent executions of the task will not be prevented. + */ + public static final ErrorHandler LOG_AND_SUPPRESS_ERROR_HANDLER = new LoggingErrorHandler(); + + /** + * An ErrorHandler strategy that will log at error level and then + * re-throw the Exception. Note: this will typically prevent subsequent + * execution of a scheduled task. + */ + public static final ErrorHandler LOG_AND_PROPAGATE_ERROR_HANDLER = new PropagatingErrorHandler(); + + + /** + * Decorate the task for error handling. If the provided {@link ErrorHandler} + * is not {@code null}, it will be used. Otherwise, repeating tasks will have + * errors suppressed by default whereas one-shot tasks will have errors + * propagated by default since those errors may be expected through the + * returned {@link Future}. In both cases, the errors will be logged. + */ + public static DelegatingErrorHandlingRunnable decorateTaskWithErrorHandler( + Runnable task, @Nullable ErrorHandler errorHandler, boolean isRepeatingTask) { + + if (task instanceof DelegatingErrorHandlingRunnable) { + return (DelegatingErrorHandlingRunnable) task; + } + ErrorHandler eh = (errorHandler != null ? errorHandler : getDefaultErrorHandler(isRepeatingTask)); + return new DelegatingErrorHandlingRunnable(task, eh); + } + + /** + * Return the default {@link ErrorHandler} implementation based on the boolean + * value indicating whether the task will be repeating or not. For repeating tasks + * it will suppress errors, but for one-time tasks it will propagate. In both + * cases, the error will be logged. + */ + public static ErrorHandler getDefaultErrorHandler(boolean isRepeatingTask) { + return (isRepeatingTask ? LOG_AND_SUPPRESS_ERROR_HANDLER : LOG_AND_PROPAGATE_ERROR_HANDLER); + } + + + /** + * An {@link ErrorHandler} implementation that logs the Throwable at error + * level. It does not perform any additional error handling. This can be + * useful when suppression of errors is the intended behavior. + */ + private static class LoggingErrorHandler implements ErrorHandler { + + private final Log logger = LogFactory.getLog(LoggingErrorHandler.class); + + @Override + public void handleError(Throwable t) { + logger.error("Unexpected error occurred in scheduled task", t); + } + } + + + /** + * An {@link ErrorHandler} implementation that logs the Throwable at error + * level and then propagates it. + */ + private static class PropagatingErrorHandler extends LoggingErrorHandler { + + @Override + public void handleError(Throwable t) { + super.handleError(t); + ReflectionUtils.rethrowRuntimeException(t); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/package-info.java b/spring-context/src/main/java/org/springframework/scheduling/support/package-info.java new file mode 100644 index 0000000..228c69c --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/support/package-info.java @@ -0,0 +1,10 @@ +/** + * Generic support classes for scheduling. + * Provides a Runnable adapter for Spring's MethodInvoker. + */ +@NonNullApi +@NonNullFields +package org.springframework.scheduling.support; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/scripting/ScriptCompilationException.java b/spring-context/src/main/java/org/springframework/scripting/ScriptCompilationException.java new file mode 100644 index 0000000..25e4b08 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scripting/ScriptCompilationException.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting; + +import org.springframework.core.NestedRuntimeException; +import org.springframework.lang.Nullable; + +/** + * Exception to be thrown on script compilation failure. + * + * @author Juergen Hoeller + * @since 2.0 + */ +@SuppressWarnings("serial") +public class ScriptCompilationException extends NestedRuntimeException { + + @Nullable + private final ScriptSource scriptSource; + + + /** + * Constructor for ScriptCompilationException. + * @param msg the detail message + */ + public ScriptCompilationException(String msg) { + super(msg); + this.scriptSource = null; + } + + /** + * Constructor for ScriptCompilationException. + * @param msg the detail message + * @param cause the root cause (usually from using an underlying script compiler API) + */ + public ScriptCompilationException(String msg, Throwable cause) { + super(msg, cause); + this.scriptSource = null; + } + + /** + * Constructor for ScriptCompilationException. + * @param scriptSource the source for the offending script + * @param msg the detail message + * @since 4.2 + */ + public ScriptCompilationException(ScriptSource scriptSource, String msg) { + super("Could not compile " + scriptSource + ": " + msg); + this.scriptSource = scriptSource; + } + + /** + * Constructor for ScriptCompilationException. + * @param scriptSource the source for the offending script + * @param cause the root cause (usually from using an underlying script compiler API) + */ + public ScriptCompilationException(ScriptSource scriptSource, Throwable cause) { + super("Could not compile " + scriptSource, cause); + this.scriptSource = scriptSource; + } + + /** + * Constructor for ScriptCompilationException. + * @param scriptSource the source for the offending script + * @param msg the detail message + * @param cause the root cause (usually from using an underlying script compiler API) + */ + public ScriptCompilationException(ScriptSource scriptSource, String msg, Throwable cause) { + super("Could not compile " + scriptSource + ": " + msg, cause); + this.scriptSource = scriptSource; + } + + + /** + * Return the source for the offending script. + * @return the source, or {@code null} if not available + */ + @Nullable + public ScriptSource getScriptSource() { + return this.scriptSource; + } + +} diff --git a/spring-context/src/main/java/org/springframework/scripting/ScriptEvaluator.java b/spring-context/src/main/java/org/springframework/scripting/ScriptEvaluator.java new file mode 100644 index 0000000..762a228 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scripting/ScriptEvaluator.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting; + +import java.util.Map; + +import org.springframework.lang.Nullable; + +/** + * Spring's strategy interface for evaluating a script. + * + *

    Aside from language-specific implementations, Spring also ships + * a version based on the standard {@code javax.script} package (JSR-223): + * {@link org.springframework.scripting.support.StandardScriptEvaluator}. + * + * @author Juergen Hoeller + * @author Costin Leau + * @since 4.0 + */ +public interface ScriptEvaluator { + + /** + * Evaluate the given script. + * @param script the ScriptSource for the script to evaluate + * @return the return value of the script, if any + * @throws ScriptCompilationException if the evaluator failed to read, + * compile or evaluate the script + */ + @Nullable + Object evaluate(ScriptSource script) throws ScriptCompilationException; + + /** + * Evaluate the given script with the given arguments. + * @param script the ScriptSource for the script to evaluate + * @param arguments the key-value pairs to expose to the script, + * typically as script variables (may be {@code null} or empty) + * @return the return value of the script, if any + * @throws ScriptCompilationException if the evaluator failed to read, + * compile or evaluate the script + */ + @Nullable + Object evaluate(ScriptSource script, @Nullable Map arguments) throws ScriptCompilationException; + +} diff --git a/spring-context/src/main/java/org/springframework/scripting/ScriptFactory.java b/spring-context/src/main/java/org/springframework/scripting/ScriptFactory.java new file mode 100644 index 0000000..02a76ce --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scripting/ScriptFactory.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting; + +import java.io.IOException; + +import org.springframework.lang.Nullable; + +/** + * Script definition interface, encapsulating the configuration + * of a specific script as well as a factory method for + * creating the actual scripted Java {@code Object}. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @since 2.0 + * @see #getScriptSourceLocator + * @see #getScriptedObject + */ +public interface ScriptFactory { + + /** + * Return a locator that points to the source of the script. + * Interpreted by the post-processor that actually creates the script. + *

    Typical supported locators are Spring resource locations + * (such as "file:C:/myScript.bsh" or "classpath:myPackage/myScript.bsh") + * and inline scripts ("inline:myScriptText..."). + * @return the script source locator + * @see org.springframework.scripting.support.ScriptFactoryPostProcessor#convertToScriptSource + * @see org.springframework.core.io.ResourceLoader + */ + String getScriptSourceLocator(); + + /** + * Return the business interfaces that the script is supposed to implement. + *

    Can return {@code null} if the script itself determines + * its Java interfaces (such as in the case of Groovy). + * @return the interfaces for the script + */ + @Nullable + Class[] getScriptInterfaces(); + + /** + * Return whether the script requires a config interface to be + * generated for it. This is typically the case for scripts that + * do not determine Java signatures themselves, with no appropriate + * config interface specified in {@code getScriptInterfaces()}. + * @return whether the script requires a generated config interface + * @see #getScriptInterfaces() + */ + boolean requiresConfigInterface(); + + /** + * Factory method for creating the scripted Java object. + *

    Implementations are encouraged to cache script metadata such as + * a generated script class. Note that this method may be invoked + * concurrently and must be implemented in a thread-safe fashion. + * @param scriptSource the actual ScriptSource to retrieve + * the script source text from (never {@code null}) + * @param actualInterfaces the actual interfaces to expose, + * including script interfaces as well as a generated config interface + * (if applicable; may be {@code null}) + * @return the scripted Java object + * @throws IOException if script retrieval failed + * @throws ScriptCompilationException if script compilation failed + */ + @Nullable + Object getScriptedObject(ScriptSource scriptSource, @Nullable Class... actualInterfaces) + throws IOException, ScriptCompilationException; + + /** + * Determine the type of the scripted Java object. + *

    Implementations are encouraged to cache script metadata such as + * a generated script class. Note that this method may be invoked + * concurrently and must be implemented in a thread-safe fashion. + * @param scriptSource the actual ScriptSource to retrieve + * the script source text from (never {@code null}) + * @return the type of the scripted Java object, or {@code null} + * if none could be determined + * @throws IOException if script retrieval failed + * @throws ScriptCompilationException if script compilation failed + * @since 2.0.3 + */ + @Nullable + Class getScriptedObjectType(ScriptSource scriptSource) + throws IOException, ScriptCompilationException; + + /** + * Determine whether a refresh is required (e.g. through + * ScriptSource's {@code isModified()} method). + * @param scriptSource the actual ScriptSource to retrieve + * the script source text from (never {@code null}) + * @return whether a fresh {@link #getScriptedObject} call is required + * @since 2.5.2 + * @see ScriptSource#isModified() + */ + boolean requiresScriptedObjectRefresh(ScriptSource scriptSource); + +} diff --git a/spring-context/src/main/java/org/springframework/scripting/ScriptSource.java b/spring-context/src/main/java/org/springframework/scripting/ScriptSource.java new file mode 100644 index 0000000..87e6954 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scripting/ScriptSource.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting; + +import java.io.IOException; + +import org.springframework.lang.Nullable; + +/** + * Interface that defines the source of a script. + * Tracks whether the underlying script has been modified. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +public interface ScriptSource { + + /** + * Retrieve the current script source text as String. + * @return the script text + * @throws IOException if script retrieval failed + */ + String getScriptAsString() throws IOException; + + /** + * Indicate whether the underlying script data has been modified since + * the last time {@link #getScriptAsString()} was called. + * Returns {@code true} if the script has not been read yet. + * @return whether the script data has been modified + */ + boolean isModified(); + + /** + * Determine a class name for the underlying script. + * @return the suggested class name, or {@code null} if none available + */ + @Nullable + String suggestedClassName(); + +} diff --git a/spring-context/src/main/java/org/springframework/scripting/bsh/BshScriptEvaluator.java b/spring-context/src/main/java/org/springframework/scripting/bsh/BshScriptEvaluator.java new file mode 100644 index 0000000..07697f0 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scripting/bsh/BshScriptEvaluator.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.bsh; + +import java.io.IOException; +import java.io.StringReader; +import java.util.Map; + +import bsh.EvalError; +import bsh.Interpreter; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.lang.Nullable; +import org.springframework.scripting.ScriptCompilationException; +import org.springframework.scripting.ScriptEvaluator; +import org.springframework.scripting.ScriptSource; + +/** + * BeanShell-based implementation of Spring's {@link ScriptEvaluator} strategy interface. + * + * @author Juergen Hoeller + * @since 4.0 + * @see Interpreter#eval(String) + */ +public class BshScriptEvaluator implements ScriptEvaluator, BeanClassLoaderAware { + + @Nullable + private ClassLoader classLoader; + + + /** + * Construct a new BshScriptEvaluator. + */ + public BshScriptEvaluator() { + } + + /** + * Construct a new BshScriptEvaluator. + * @param classLoader the ClassLoader to use for the {@link Interpreter} + */ + public BshScriptEvaluator(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + + @Override + @Nullable + public Object evaluate(ScriptSource script) { + return evaluate(script, null); + } + + @Override + @Nullable + public Object evaluate(ScriptSource script, @Nullable Map arguments) { + try { + Interpreter interpreter = new Interpreter(); + interpreter.setClassLoader(this.classLoader); + if (arguments != null) { + for (Map.Entry entry : arguments.entrySet()) { + interpreter.set(entry.getKey(), entry.getValue()); + } + } + return interpreter.eval(new StringReader(script.getScriptAsString())); + } + catch (IOException ex) { + throw new ScriptCompilationException(script, "Cannot access BeanShell script", ex); + } + catch (EvalError ex) { + throw new ScriptCompilationException(script, ex); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/scripting/bsh/BshScriptFactory.java b/spring-context/src/main/java/org/springframework/scripting/bsh/BshScriptFactory.java new file mode 100644 index 0000000..a67f7f6 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scripting/bsh/BshScriptFactory.java @@ -0,0 +1,218 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.bsh; + +import java.io.IOException; + +import bsh.EvalError; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.lang.Nullable; +import org.springframework.scripting.ScriptCompilationException; +import org.springframework.scripting.ScriptFactory; +import org.springframework.scripting.ScriptSource; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * {@link org.springframework.scripting.ScriptFactory} implementation + * for a BeanShell script. + * + *

    Typically used in combination with a + * {@link org.springframework.scripting.support.ScriptFactoryPostProcessor}; + * see the latter's javadoc for a configuration example. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @since 2.0 + * @see BshScriptUtils + * @see org.springframework.scripting.support.ScriptFactoryPostProcessor + */ +public class BshScriptFactory implements ScriptFactory, BeanClassLoaderAware { + + private final String scriptSourceLocator; + + @Nullable + private final Class[] scriptInterfaces; + + @Nullable + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + @Nullable + private Class scriptClass; + + private final Object scriptClassMonitor = new Object(); + + private boolean wasModifiedForTypeCheck = false; + + + /** + * Create a new BshScriptFactory for the given script source. + *

    With this {@code BshScriptFactory} variant, the script needs to + * declare a full class or return an actual instance of the scripted object. + * @param scriptSourceLocator a locator that points to the source of the script. + * Interpreted by the post-processor that actually creates the script. + */ + public BshScriptFactory(String scriptSourceLocator) { + Assert.hasText(scriptSourceLocator, "'scriptSourceLocator' must not be empty"); + this.scriptSourceLocator = scriptSourceLocator; + this.scriptInterfaces = null; + } + + /** + * Create a new BshScriptFactory for the given script source. + *

    The script may either be a simple script that needs a corresponding proxy + * generated (implementing the specified interfaces), or declare a full class + * or return an actual instance of the scripted object (in which case the + * specified interfaces, if any, need to be implemented by that class/instance). + * @param scriptSourceLocator a locator that points to the source of the script. + * Interpreted by the post-processor that actually creates the script. + * @param scriptInterfaces the Java interfaces that the scripted object + * is supposed to implement (may be {@code null}) + */ + public BshScriptFactory(String scriptSourceLocator, @Nullable Class... scriptInterfaces) { + Assert.hasText(scriptSourceLocator, "'scriptSourceLocator' must not be empty"); + this.scriptSourceLocator = scriptSourceLocator; + this.scriptInterfaces = scriptInterfaces; + } + + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + + @Override + public String getScriptSourceLocator() { + return this.scriptSourceLocator; + } + + @Override + @Nullable + public Class[] getScriptInterfaces() { + return this.scriptInterfaces; + } + + /** + * BeanShell scripts do require a config interface. + */ + @Override + public boolean requiresConfigInterface() { + return true; + } + + /** + * Load and parse the BeanShell script via {@link BshScriptUtils}. + * @see BshScriptUtils#createBshObject(String, Class[], ClassLoader) + */ + @Override + @Nullable + public Object getScriptedObject(ScriptSource scriptSource, @Nullable Class... actualInterfaces) + throws IOException, ScriptCompilationException { + + Class clazz; + + try { + synchronized (this.scriptClassMonitor) { + boolean requiresScriptEvaluation = (this.wasModifiedForTypeCheck && this.scriptClass == null); + this.wasModifiedForTypeCheck = false; + + if (scriptSource.isModified() || requiresScriptEvaluation) { + // New script content: Let's check whether it evaluates to a Class. + Object result = BshScriptUtils.evaluateBshScript( + scriptSource.getScriptAsString(), actualInterfaces, this.beanClassLoader); + if (result instanceof Class) { + // A Class: We'll cache the Class here and create an instance + // outside of the synchronized block. + this.scriptClass = (Class) result; + } + else { + // Not a Class: OK, we'll simply create BeanShell objects + // through evaluating the script for every call later on. + // For this first-time check, let's simply return the + // already evaluated object. + return result; + } + } + clazz = this.scriptClass; + } + } + catch (EvalError ex) { + this.scriptClass = null; + throw new ScriptCompilationException(scriptSource, ex); + } + + if (clazz != null) { + // A Class: We need to create an instance for every call. + try { + return ReflectionUtils.accessibleConstructor(clazz).newInstance(); + } + catch (Throwable ex) { + throw new ScriptCompilationException( + scriptSource, "Could not instantiate script class: " + clazz.getName(), ex); + } + } + else { + // Not a Class: We need to evaluate the script for every call. + try { + return BshScriptUtils.createBshObject( + scriptSource.getScriptAsString(), actualInterfaces, this.beanClassLoader); + } + catch (EvalError ex) { + throw new ScriptCompilationException(scriptSource, ex); + } + } + } + + @Override + @Nullable + public Class getScriptedObjectType(ScriptSource scriptSource) + throws IOException, ScriptCompilationException { + + synchronized (this.scriptClassMonitor) { + try { + if (scriptSource.isModified()) { + // New script content: Let's check whether it evaluates to a Class. + this.wasModifiedForTypeCheck = true; + this.scriptClass = BshScriptUtils.determineBshObjectType( + scriptSource.getScriptAsString(), this.beanClassLoader); + } + return this.scriptClass; + } + catch (EvalError ex) { + this.scriptClass = null; + throw new ScriptCompilationException(scriptSource, ex); + } + } + } + + @Override + public boolean requiresScriptedObjectRefresh(ScriptSource scriptSource) { + synchronized (this.scriptClassMonitor) { + return (scriptSource.isModified() || this.wasModifiedForTypeCheck); + } + } + + + @Override + public String toString() { + return "BshScriptFactory: script source locator [" + this.scriptSourceLocator + "]"; + } + +} diff --git a/spring-context/src/main/java/org/springframework/scripting/bsh/BshScriptUtils.java b/spring-context/src/main/java/org/springframework/scripting/bsh/BshScriptUtils.java new file mode 100644 index 0000000..47a368a --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scripting/bsh/BshScriptUtils.java @@ -0,0 +1,235 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.bsh; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import bsh.EvalError; +import bsh.Interpreter; +import bsh.Primitive; +import bsh.XThis; + +import org.springframework.core.NestedRuntimeException; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; + +/** + * Utility methods for handling BeanShell-scripted objects. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +public abstract class BshScriptUtils { + + /** + * Create a new BeanShell-scripted object from the given script source. + *

    With this {@code createBshObject} variant, the script needs to + * declare a full class or return an actual instance of the scripted object. + * @param scriptSource the script source text + * @return the scripted Java object + * @throws EvalError in case of BeanShell parsing failure + */ + public static Object createBshObject(String scriptSource) throws EvalError { + return createBshObject(scriptSource, null, null); + } + + /** + * Create a new BeanShell-scripted object from the given script source, + * using the default ClassLoader. + *

    The script may either be a simple script that needs a corresponding proxy + * generated (implementing the specified interfaces), or declare a full class + * or return an actual instance of the scripted object (in which case the + * specified interfaces, if any, need to be implemented by that class/instance). + * @param scriptSource the script source text + * @param scriptInterfaces the interfaces that the scripted Java object is + * supposed to implement (may be {@code null} or empty if the script itself + * declares a full class or returns an actual instance of the scripted object) + * @return the scripted Java object + * @throws EvalError in case of BeanShell parsing failure + * @see #createBshObject(String, Class[], ClassLoader) + */ + public static Object createBshObject(String scriptSource, @Nullable Class... scriptInterfaces) throws EvalError { + return createBshObject(scriptSource, scriptInterfaces, ClassUtils.getDefaultClassLoader()); + } + + /** + * Create a new BeanShell-scripted object from the given script source. + *

    The script may either be a simple script that needs a corresponding proxy + * generated (implementing the specified interfaces), or declare a full class + * or return an actual instance of the scripted object (in which case the + * specified interfaces, if any, need to be implemented by that class/instance). + * @param scriptSource the script source text + * @param scriptInterfaces the interfaces that the scripted Java object is + * supposed to implement (may be {@code null} or empty if the script itself + * declares a full class or returns an actual instance of the scripted object) + * @param classLoader the ClassLoader to use for evaluating the script + * @return the scripted Java object + * @throws EvalError in case of BeanShell parsing failure + */ + public static Object createBshObject(String scriptSource, @Nullable Class[] scriptInterfaces, @Nullable ClassLoader classLoader) + throws EvalError { + + Object result = evaluateBshScript(scriptSource, scriptInterfaces, classLoader); + if (result instanceof Class) { + Class clazz = (Class) result; + try { + return ReflectionUtils.accessibleConstructor(clazz).newInstance(); + } + catch (Throwable ex) { + throw new IllegalStateException("Could not instantiate script class: " + clazz.getName(), ex); + } + } + else { + return result; + } + } + + /** + * Evaluate the specified BeanShell script based on the given script source, + * returning the Class defined by the script. + *

    The script may either declare a full class or return an actual instance of + * the scripted object (in which case the Class of the object will be returned). + * In any other case, the returned Class will be {@code null}. + * @param scriptSource the script source text + * @param classLoader the ClassLoader to use for evaluating the script + * @return the scripted Java class, or {@code null} if none could be determined + * @throws EvalError in case of BeanShell parsing failure + */ + @Nullable + static Class determineBshObjectType(String scriptSource, @Nullable ClassLoader classLoader) throws EvalError { + Assert.hasText(scriptSource, "Script source must not be empty"); + Interpreter interpreter = new Interpreter(); + if (classLoader != null) { + interpreter.setClassLoader(classLoader); + } + Object result = interpreter.eval(scriptSource); + if (result instanceof Class) { + return (Class) result; + } + else if (result != null) { + return result.getClass(); + } + else { + return null; + } + } + + /** + * Evaluate the specified BeanShell script based on the given script source, + * keeping a returned script Class or script Object as-is. + *

    The script may either be a simple script that needs a corresponding proxy + * generated (implementing the specified interfaces), or declare a full class + * or return an actual instance of the scripted object (in which case the + * specified interfaces, if any, need to be implemented by that class/instance). + * @param scriptSource the script source text + * @param scriptInterfaces the interfaces that the scripted Java object is + * supposed to implement (may be {@code null} or empty if the script itself + * declares a full class or returns an actual instance of the scripted object) + * @param classLoader the ClassLoader to use for evaluating the script + * @return the scripted Java class or Java object + * @throws EvalError in case of BeanShell parsing failure + */ + static Object evaluateBshScript( + String scriptSource, @Nullable Class[] scriptInterfaces, @Nullable ClassLoader classLoader) + throws EvalError { + + Assert.hasText(scriptSource, "Script source must not be empty"); + Interpreter interpreter = new Interpreter(); + interpreter.setClassLoader(classLoader); + Object result = interpreter.eval(scriptSource); + if (result != null) { + return result; + } + else { + // Simple BeanShell script: Let's create a proxy for it, implementing the given interfaces. + if (ObjectUtils.isEmpty(scriptInterfaces)) { + throw new IllegalArgumentException("Given script requires a script proxy: " + + "At least one script interface is required.\nScript: " + scriptSource); + } + XThis xt = (XThis) interpreter.eval("return this"); + return Proxy.newProxyInstance(classLoader, scriptInterfaces, new BshObjectInvocationHandler(xt)); + } + } + + + /** + * InvocationHandler that invokes a BeanShell script method. + */ + private static class BshObjectInvocationHandler implements InvocationHandler { + + private final XThis xt; + + public BshObjectInvocationHandler(XThis xt) { + this.xt = xt; + } + + @Override + @Nullable + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (ReflectionUtils.isEqualsMethod(method)) { + return (isProxyForSameBshObject(args[0])); + } + else if (ReflectionUtils.isHashCodeMethod(method)) { + return this.xt.hashCode(); + } + else if (ReflectionUtils.isToStringMethod(method)) { + return "BeanShell object [" + this.xt + "]"; + } + try { + Object result = this.xt.invokeMethod(method.getName(), args); + if (result == Primitive.NULL || result == Primitive.VOID) { + return null; + } + if (result instanceof Primitive) { + return ((Primitive) result).getValue(); + } + return result; + } + catch (EvalError ex) { + throw new BshExecutionException(ex); + } + } + + private boolean isProxyForSameBshObject(Object other) { + if (!Proxy.isProxyClass(other.getClass())) { + return false; + } + InvocationHandler ih = Proxy.getInvocationHandler(other); + return (ih instanceof BshObjectInvocationHandler && + this.xt.equals(((BshObjectInvocationHandler) ih).xt)); + } + } + + + /** + * Exception to be thrown on script execution failure. + */ + @SuppressWarnings("serial") + public static final class BshExecutionException extends NestedRuntimeException { + + private BshExecutionException(EvalError ex) { + super("BeanShell script execution failed", ex); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/scripting/bsh/package-info.java b/spring-context/src/main/java/org/springframework/scripting/bsh/package-info.java new file mode 100644 index 0000000..18bd5e9 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scripting/bsh/package-info.java @@ -0,0 +1,12 @@ +/** + * Package providing integration of + * BeanShell + * (and BeanShell2) + * into Spring's scripting infrastructure. + */ +@NonNullApi +@NonNullFields +package org.springframework.scripting.bsh; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/scripting/config/LangNamespaceHandler.java b/spring-context/src/main/java/org/springframework/scripting/config/LangNamespaceHandler.java new file mode 100644 index 0000000..1f84b1e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scripting/config/LangNamespaceHandler.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.config; + +import org.springframework.beans.factory.xml.NamespaceHandlerSupport; + +/** + * {@code NamespaceHandler} that supports the wiring of + * objects backed by dynamic languages such as Groovy, JRuby and + * BeanShell. The following is an example (from the reference + * documentation) that details the wiring of a Groovy backed bean: + * + *

    + * <lang:groovy id="messenger"
    + *     refresh-check-delay="5000"
    + *     script-source="classpath:Messenger.groovy">
    + * <lang:property name="message" value="I Can Do The Frug"/>
    + * </lang:groovy>
    + * 
    + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Mark Fisher + * @since 2.0 + */ +public class LangNamespaceHandler extends NamespaceHandlerSupport { + + @Override + public void init() { + registerScriptBeanDefinitionParser("groovy", "org.springframework.scripting.groovy.GroovyScriptFactory"); + registerScriptBeanDefinitionParser("bsh", "org.springframework.scripting.bsh.BshScriptFactory"); + registerScriptBeanDefinitionParser("std", "org.springframework.scripting.support.StandardScriptFactory"); + registerBeanDefinitionParser("defaults", new ScriptingDefaultsParser()); + } + + private void registerScriptBeanDefinitionParser(String key, String scriptFactoryClassName) { + registerBeanDefinitionParser(key, new ScriptBeanDefinitionParser(scriptFactoryClassName)); + } + +} diff --git a/spring-context/src/main/java/org/springframework/scripting/config/LangNamespaceUtils.java b/spring-context/src/main/java/org/springframework/scripting/config/LangNamespaceUtils.java new file mode 100644 index 0000000..eb23a47 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scripting/config/LangNamespaceUtils.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.config; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.scripting.support.ScriptFactoryPostProcessor; + +/** + * Utilities for use with {@link LangNamespaceHandler}. + * + * @author Rob Harrop + * @author Mark Fisher + * @since 2.5 + */ +public abstract class LangNamespaceUtils { + + /** + * The unique name under which the internally managed {@link ScriptFactoryPostProcessor} is + * registered in the {@link BeanDefinitionRegistry}. + */ + private static final String SCRIPT_FACTORY_POST_PROCESSOR_BEAN_NAME = + "org.springframework.scripting.config.scriptFactoryPostProcessor"; + + + /** + * Register a {@link ScriptFactoryPostProcessor} bean definition in the supplied + * {@link BeanDefinitionRegistry} if the {@link ScriptFactoryPostProcessor} hasn't + * already been registered. + * @param registry the {@link BeanDefinitionRegistry} to register the script processor with + * @return the {@link ScriptFactoryPostProcessor} bean definition (new or already registered) + */ + public static BeanDefinition registerScriptFactoryPostProcessorIfNecessary(BeanDefinitionRegistry registry) { + BeanDefinition beanDefinition; + if (registry.containsBeanDefinition(SCRIPT_FACTORY_POST_PROCESSOR_BEAN_NAME)) { + beanDefinition = registry.getBeanDefinition(SCRIPT_FACTORY_POST_PROCESSOR_BEAN_NAME); + } + else { + beanDefinition = new RootBeanDefinition(ScriptFactoryPostProcessor.class); + registry.registerBeanDefinition(SCRIPT_FACTORY_POST_PROCESSOR_BEAN_NAME, beanDefinition); + } + return beanDefinition; + } + +} diff --git a/spring-context/src/main/java/org/springframework/scripting/config/ScriptBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/scripting/config/ScriptBeanDefinitionParser.java new file mode 100644 index 0000000..06d6cd7 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scripting/config/ScriptBeanDefinitionParser.java @@ -0,0 +1,247 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.config; + +import java.util.List; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionDefaults; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser; +import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.beans.factory.xml.XmlReaderContext; +import org.springframework.lang.Nullable; +import org.springframework.scripting.support.ScriptFactoryPostProcessor; +import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; + +/** + * BeanDefinitionParser implementation for the '{@code }', + * '{@code }' and '{@code }' tags. + * Allows for objects written using dynamic languages to be easily exposed with + * the {@link org.springframework.beans.factory.BeanFactory}. + * + *

    The script for each object can be specified either as a reference to the + * resource containing it (using the '{@code script-source}' attribute) or inline + * in the XML configuration itself (using the '{@code inline-script}' attribute. + * + *

    By default, dynamic objects created with these tags are not + * refreshable. To enable refreshing, specify the refresh check delay for each + * object (in milliseconds) using the '{@code refresh-check-delay}' attribute. + * + * @author Rob Harrop + * @author Rod Johnson + * @author Juergen Hoeller + * @author Mark Fisher + * @since 2.0 + */ +class ScriptBeanDefinitionParser extends AbstractBeanDefinitionParser { + + private static final String ENGINE_ATTRIBUTE = "engine"; + + private static final String SCRIPT_SOURCE_ATTRIBUTE = "script-source"; + + private static final String INLINE_SCRIPT_ELEMENT = "inline-script"; + + private static final String SCOPE_ATTRIBUTE = "scope"; + + private static final String AUTOWIRE_ATTRIBUTE = "autowire"; + + private static final String DEPENDS_ON_ATTRIBUTE = "depends-on"; + + private static final String INIT_METHOD_ATTRIBUTE = "init-method"; + + private static final String DESTROY_METHOD_ATTRIBUTE = "destroy-method"; + + private static final String SCRIPT_INTERFACES_ATTRIBUTE = "script-interfaces"; + + private static final String REFRESH_CHECK_DELAY_ATTRIBUTE = "refresh-check-delay"; + + private static final String PROXY_TARGET_CLASS_ATTRIBUTE = "proxy-target-class"; + + private static final String CUSTOMIZER_REF_ATTRIBUTE = "customizer-ref"; + + + /** + * The {@link org.springframework.scripting.ScriptFactory} class that this + * parser instance will create bean definitions for. + */ + private final String scriptFactoryClassName; + + + /** + * Create a new instance of this parser, creating bean definitions for the + * supplied {@link org.springframework.scripting.ScriptFactory} class. + * @param scriptFactoryClassName the ScriptFactory class to operate on + */ + public ScriptBeanDefinitionParser(String scriptFactoryClassName) { + this.scriptFactoryClassName = scriptFactoryClassName; + } + + + /** + * Parses the dynamic object element and returns the resulting bean definition. + * Registers a {@link ScriptFactoryPostProcessor} if needed. + */ + @Override + @SuppressWarnings("deprecation") + @Nullable + protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) { + // Engine attribute only supported for + String engine = element.getAttribute(ENGINE_ATTRIBUTE); + + // Resolve the script source. + String value = resolveScriptSource(element, parserContext.getReaderContext()); + if (value == null) { + return null; + } + + // Set up infrastructure. + LangNamespaceUtils.registerScriptFactoryPostProcessorIfNecessary(parserContext.getRegistry()); + + // Create script factory bean definition. + GenericBeanDefinition bd = new GenericBeanDefinition(); + bd.setBeanClassName(this.scriptFactoryClassName); + bd.setSource(parserContext.extractSource(element)); + bd.setAttribute(ScriptFactoryPostProcessor.LANGUAGE_ATTRIBUTE, element.getLocalName()); + + // Determine bean scope. + String scope = element.getAttribute(SCOPE_ATTRIBUTE); + if (StringUtils.hasLength(scope)) { + bd.setScope(scope); + } + + // Determine autowire mode. + String autowire = element.getAttribute(AUTOWIRE_ATTRIBUTE); + int autowireMode = parserContext.getDelegate().getAutowireMode(autowire); + // Only "byType" and "byName" supported, but maybe other default inherited... + if (autowireMode == AbstractBeanDefinition.AUTOWIRE_AUTODETECT) { + autowireMode = AbstractBeanDefinition.AUTOWIRE_BY_TYPE; + } + else if (autowireMode == AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR) { + autowireMode = AbstractBeanDefinition.AUTOWIRE_NO; + } + bd.setAutowireMode(autowireMode); + + // Parse depends-on list of bean names. + String dependsOn = element.getAttribute(DEPENDS_ON_ATTRIBUTE); + if (StringUtils.hasLength(dependsOn)) { + bd.setDependsOn(StringUtils.tokenizeToStringArray( + dependsOn, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS)); + } + + // Retrieve the defaults for bean definitions within this parser context + BeanDefinitionDefaults beanDefinitionDefaults = parserContext.getDelegate().getBeanDefinitionDefaults(); + + // Determine init method and destroy method. + String initMethod = element.getAttribute(INIT_METHOD_ATTRIBUTE); + if (StringUtils.hasLength(initMethod)) { + bd.setInitMethodName(initMethod); + } + else if (beanDefinitionDefaults.getInitMethodName() != null) { + bd.setInitMethodName(beanDefinitionDefaults.getInitMethodName()); + } + + if (element.hasAttribute(DESTROY_METHOD_ATTRIBUTE)) { + String destroyMethod = element.getAttribute(DESTROY_METHOD_ATTRIBUTE); + bd.setDestroyMethodName(destroyMethod); + } + else if (beanDefinitionDefaults.getDestroyMethodName() != null) { + bd.setDestroyMethodName(beanDefinitionDefaults.getDestroyMethodName()); + } + + // Attach any refresh metadata. + String refreshCheckDelay = element.getAttribute(REFRESH_CHECK_DELAY_ATTRIBUTE); + if (StringUtils.hasText(refreshCheckDelay)) { + bd.setAttribute(ScriptFactoryPostProcessor.REFRESH_CHECK_DELAY_ATTRIBUTE, Long.valueOf(refreshCheckDelay)); + } + + // Attach any proxy target class metadata. + String proxyTargetClass = element.getAttribute(PROXY_TARGET_CLASS_ATTRIBUTE); + if (StringUtils.hasText(proxyTargetClass)) { + bd.setAttribute(ScriptFactoryPostProcessor.PROXY_TARGET_CLASS_ATTRIBUTE, Boolean.valueOf(proxyTargetClass)); + } + + // Add constructor arguments. + ConstructorArgumentValues cav = bd.getConstructorArgumentValues(); + int constructorArgNum = 0; + if (StringUtils.hasLength(engine)) { + cav.addIndexedArgumentValue(constructorArgNum++, engine); + } + cav.addIndexedArgumentValue(constructorArgNum++, value); + if (element.hasAttribute(SCRIPT_INTERFACES_ATTRIBUTE)) { + cav.addIndexedArgumentValue( + constructorArgNum++, element.getAttribute(SCRIPT_INTERFACES_ATTRIBUTE), "java.lang.Class[]"); + } + + // This is used for Groovy. It's a bean reference to a customizer bean. + if (element.hasAttribute(CUSTOMIZER_REF_ATTRIBUTE)) { + String customizerBeanName = element.getAttribute(CUSTOMIZER_REF_ATTRIBUTE); + if (!StringUtils.hasText(customizerBeanName)) { + parserContext.getReaderContext().error("Attribute 'customizer-ref' has empty value", element); + } + else { + cav.addIndexedArgumentValue(constructorArgNum++, new RuntimeBeanReference(customizerBeanName)); + } + } + + // Add any property definitions that need adding. + parserContext.getDelegate().parsePropertyElements(element, bd); + + return bd; + } + + /** + * Resolves the script source from either the '{@code script-source}' attribute or + * the '{@code inline-script}' element. Logs and {@link XmlReaderContext#error} and + * returns {@code null} if neither or both of these values are specified. + */ + @Nullable + private String resolveScriptSource(Element element, XmlReaderContext readerContext) { + boolean hasScriptSource = element.hasAttribute(SCRIPT_SOURCE_ATTRIBUTE); + List elements = DomUtils.getChildElementsByTagName(element, INLINE_SCRIPT_ELEMENT); + if (hasScriptSource && !elements.isEmpty()) { + readerContext.error("Only one of 'script-source' and 'inline-script' should be specified.", element); + return null; + } + else if (hasScriptSource) { + return element.getAttribute(SCRIPT_SOURCE_ATTRIBUTE); + } + else if (!elements.isEmpty()) { + Element inlineElement = elements.get(0); + return "inline:" + DomUtils.getTextValue(inlineElement); + } + else { + readerContext.error("Must specify either 'script-source' or 'inline-script'.", element); + return null; + } + } + + /** + * Scripted beans may be anonymous as well. + */ + @Override + protected boolean shouldGenerateIdAsFallback() { + return true; + } + +} diff --git a/spring-context/src/main/java/org/springframework/scripting/config/ScriptingDefaultsParser.java b/spring-context/src/main/java/org/springframework/scripting/config/ScriptingDefaultsParser.java new file mode 100644 index 0000000..ba2e2c4 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scripting/config/ScriptingDefaultsParser.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.TypedStringValue; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.util.StringUtils; + +/** + * A {@link BeanDefinitionParser} for use when loading scripting XML. + * + * @author Mark Fisher + * @since 2.5 + */ +class ScriptingDefaultsParser implements BeanDefinitionParser { + + private static final String REFRESH_CHECK_DELAY_ATTRIBUTE = "refresh-check-delay"; + + private static final String PROXY_TARGET_CLASS_ATTRIBUTE = "proxy-target-class"; + + + @Override + public BeanDefinition parse(Element element, ParserContext parserContext) { + BeanDefinition bd = + LangNamespaceUtils.registerScriptFactoryPostProcessorIfNecessary(parserContext.getRegistry()); + String refreshCheckDelay = element.getAttribute(REFRESH_CHECK_DELAY_ATTRIBUTE); + if (StringUtils.hasText(refreshCheckDelay)) { + bd.getPropertyValues().add("defaultRefreshCheckDelay", Long.valueOf(refreshCheckDelay)); + } + String proxyTargetClass = element.getAttribute(PROXY_TARGET_CLASS_ATTRIBUTE); + if (StringUtils.hasText(proxyTargetClass)) { + bd.getPropertyValues().add("defaultProxyTargetClass", new TypedStringValue(proxyTargetClass, Boolean.class)); + } + return null; + } + +} diff --git a/spring-context/src/main/java/org/springframework/scripting/config/package-info.java b/spring-context/src/main/java/org/springframework/scripting/config/package-info.java new file mode 100644 index 0000000..0d2c295 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scripting/config/package-info.java @@ -0,0 +1,10 @@ +/** + * Support package for Spring's dynamic language machinery, + * with XML schema being the primary configuration format. + */ +@NonNullApi +@NonNullFields +package org.springframework.scripting.config; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/scripting/groovy/GroovyObjectCustomizer.java b/spring-context/src/main/java/org/springframework/scripting/groovy/GroovyObjectCustomizer.java new file mode 100644 index 0000000..e4c4737 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scripting/groovy/GroovyObjectCustomizer.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.groovy; + +import groovy.lang.GroovyObject; + +/** + * Strategy used by {@link GroovyScriptFactory} to allow the customization of + * a created {@link GroovyObject}. + * + *

    This is useful to allow the authoring of DSLs, the replacement of missing + * methods, and so forth. For example, a custom {@link groovy.lang.MetaClass} + * could be specified. + * + * @author Rod Johnson + * @since 2.0.2 + * @see GroovyScriptFactory + */ +@FunctionalInterface +public interface GroovyObjectCustomizer { + + /** + * Customize the supplied {@link GroovyObject}. + *

    For example, this can be used to set a custom metaclass to + * handle missing methods. + * @param goo the {@code GroovyObject} to customize + */ + void customize(GroovyObject goo); + +} diff --git a/spring-context/src/main/java/org/springframework/scripting/groovy/GroovyScriptEvaluator.java b/spring-context/src/main/java/org/springframework/scripting/groovy/GroovyScriptEvaluator.java new file mode 100644 index 0000000..3b66d3e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scripting/groovy/GroovyScriptEvaluator.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.groovy; + +import java.io.IOException; +import java.util.Map; + +import groovy.lang.Binding; +import groovy.lang.GroovyRuntimeException; +import groovy.lang.GroovyShell; +import org.codehaus.groovy.control.CompilerConfiguration; +import org.codehaus.groovy.control.customizers.CompilationCustomizer; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.lang.Nullable; +import org.springframework.scripting.ScriptCompilationException; +import org.springframework.scripting.ScriptEvaluator; +import org.springframework.scripting.ScriptSource; +import org.springframework.scripting.support.ResourceScriptSource; + +/** + * Groovy-based implementation of Spring's {@link ScriptEvaluator} strategy interface. + * + * @author Juergen Hoeller + * @since 4.0 + * @see GroovyShell#evaluate(String, String) + */ +public class GroovyScriptEvaluator implements ScriptEvaluator, BeanClassLoaderAware { + + @Nullable + private ClassLoader classLoader; + + private CompilerConfiguration compilerConfiguration = new CompilerConfiguration(); + + + /** + * Construct a new GroovyScriptEvaluator. + */ + public GroovyScriptEvaluator() { + } + + /** + * Construct a new GroovyScriptEvaluator. + * @param classLoader the ClassLoader to use as a parent for the {@link GroovyShell} + */ + public GroovyScriptEvaluator(@Nullable ClassLoader classLoader) { + this.classLoader = classLoader; + } + + + /** + * Set a custom compiler configuration for this evaluator. + * @since 4.3.3 + * @see #setCompilationCustomizers + */ + public void setCompilerConfiguration(@Nullable CompilerConfiguration compilerConfiguration) { + this.compilerConfiguration = + (compilerConfiguration != null ? compilerConfiguration : new CompilerConfiguration()); + } + + /** + * Return this evaluator's compiler configuration (never {@code null}). + * @since 4.3.3 + * @see #setCompilerConfiguration + */ + public CompilerConfiguration getCompilerConfiguration() { + return this.compilerConfiguration; + } + + /** + * Set one or more customizers to be applied to this evaluator's compiler configuration. + *

    Note that this modifies the shared compiler configuration held by this evaluator. + * @since 4.3.3 + * @see #setCompilerConfiguration + */ + public void setCompilationCustomizers(CompilationCustomizer... compilationCustomizers) { + this.compilerConfiguration.addCompilationCustomizers(compilationCustomizers); + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + + @Override + @Nullable + public Object evaluate(ScriptSource script) { + return evaluate(script, null); + } + + @Override + @Nullable + public Object evaluate(ScriptSource script, @Nullable Map arguments) { + GroovyShell groovyShell = new GroovyShell( + this.classLoader, new Binding(arguments), this.compilerConfiguration); + try { + String filename = (script instanceof ResourceScriptSource ? + ((ResourceScriptSource) script).getResource().getFilename() : null); + if (filename != null) { + return groovyShell.evaluate(script.getScriptAsString(), filename); + } + else { + return groovyShell.evaluate(script.getScriptAsString()); + } + } + catch (IOException ex) { + throw new ScriptCompilationException(script, "Cannot access Groovy script", ex); + } + catch (GroovyRuntimeException ex) { + throw new ScriptCompilationException(script, ex); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/scripting/groovy/GroovyScriptFactory.java b/spring-context/src/main/java/org/springframework/scripting/groovy/GroovyScriptFactory.java new file mode 100644 index 0000000..90f812e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scripting/groovy/GroovyScriptFactory.java @@ -0,0 +1,375 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.groovy; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; + +import groovy.lang.GroovyClassLoader; +import groovy.lang.GroovyObject; +import groovy.lang.MetaClass; +import groovy.lang.Script; +import org.codehaus.groovy.control.CompilationFailedException; +import org.codehaus.groovy.control.CompilerConfiguration; +import org.codehaus.groovy.control.customizers.CompilationCustomizer; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.lang.Nullable; +import org.springframework.scripting.ScriptCompilationException; +import org.springframework.scripting.ScriptFactory; +import org.springframework.scripting.ScriptSource; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; + +/** + * {@link org.springframework.scripting.ScriptFactory} implementation + * for a Groovy script. + * + *

    Typically used in combination with a + * {@link org.springframework.scripting.support.ScriptFactoryPostProcessor}; + * see the latter's javadoc for a configuration example. + * + *

    Note: Spring 4.0 supports Groovy 1.8 and higher. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @author Rod Johnson + * @since 2.0 + * @see groovy.lang.GroovyClassLoader + * @see org.springframework.scripting.support.ScriptFactoryPostProcessor + */ +public class GroovyScriptFactory implements ScriptFactory, BeanFactoryAware, BeanClassLoaderAware { + + private final String scriptSourceLocator; + + @Nullable + private GroovyObjectCustomizer groovyObjectCustomizer; + + @Nullable + private CompilerConfiguration compilerConfiguration; + + @Nullable + private GroovyClassLoader groovyClassLoader; + + @Nullable + private Class scriptClass; + + @Nullable + private Class scriptResultClass; + + @Nullable + private CachedResultHolder cachedResult; + + private final Object scriptClassMonitor = new Object(); + + private boolean wasModifiedForTypeCheck = false; + + + /** + * Create a new GroovyScriptFactory for the given script source. + *

    We don't need to specify script interfaces here, since + * a Groovy script defines its Java interfaces itself. + * @param scriptSourceLocator a locator that points to the source of the script. + * Interpreted by the post-processor that actually creates the script. + */ + public GroovyScriptFactory(String scriptSourceLocator) { + Assert.hasText(scriptSourceLocator, "'scriptSourceLocator' must not be empty"); + this.scriptSourceLocator = scriptSourceLocator; + } + + /** + * Create a new GroovyScriptFactory for the given script source, + * specifying a strategy interface that can create a custom MetaClass + * to supply missing methods and otherwise change the behavior of the object. + * @param scriptSourceLocator a locator that points to the source of the script. + * Interpreted by the post-processor that actually creates the script. + * @param groovyObjectCustomizer a customizer that can set a custom metaclass + * or make other changes to the GroovyObject created by this factory + * (may be {@code null}) + * @see GroovyObjectCustomizer#customize + */ + public GroovyScriptFactory(String scriptSourceLocator, @Nullable GroovyObjectCustomizer groovyObjectCustomizer) { + this(scriptSourceLocator); + this.groovyObjectCustomizer = groovyObjectCustomizer; + } + + /** + * Create a new GroovyScriptFactory for the given script source, + * specifying a strategy interface that can create a custom MetaClass + * to supply missing methods and otherwise change the behavior of the object. + * @param scriptSourceLocator a locator that points to the source of the script. + * Interpreted by the post-processor that actually creates the script. + * @param compilerConfiguration a custom compiler configuration to be applied + * to the GroovyClassLoader (may be {@code null}) + * @since 4.3.3 + * @see GroovyClassLoader#GroovyClassLoader(ClassLoader, CompilerConfiguration) + */ + public GroovyScriptFactory(String scriptSourceLocator, @Nullable CompilerConfiguration compilerConfiguration) { + this(scriptSourceLocator); + this.compilerConfiguration = compilerConfiguration; + } + + /** + * Create a new GroovyScriptFactory for the given script source, + * specifying a strategy interface that can customize Groovy's compilation + * process within the underlying GroovyClassLoader. + * @param scriptSourceLocator a locator that points to the source of the script. + * Interpreted by the post-processor that actually creates the script. + * @param compilationCustomizers one or more customizers to be applied to the + * GroovyClassLoader compiler configuration + * @since 4.3.3 + * @see CompilerConfiguration#addCompilationCustomizers + * @see org.codehaus.groovy.control.customizers.ImportCustomizer + */ + public GroovyScriptFactory(String scriptSourceLocator, CompilationCustomizer... compilationCustomizers) { + this(scriptSourceLocator); + if (!ObjectUtils.isEmpty(compilationCustomizers)) { + this.compilerConfiguration = new CompilerConfiguration(); + this.compilerConfiguration.addCompilationCustomizers(compilationCustomizers); + } + } + + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + if (beanFactory instanceof ConfigurableListableBeanFactory) { + ((ConfigurableListableBeanFactory) beanFactory).ignoreDependencyType(MetaClass.class); + } + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + if (classLoader instanceof GroovyClassLoader && + (this.compilerConfiguration == null || + ((GroovyClassLoader) classLoader).hasCompatibleConfiguration(this.compilerConfiguration))) { + this.groovyClassLoader = (GroovyClassLoader) classLoader; + } + else { + this.groovyClassLoader = buildGroovyClassLoader(classLoader); + } + } + + /** + * Return the GroovyClassLoader used by this script factory. + */ + public GroovyClassLoader getGroovyClassLoader() { + synchronized (this.scriptClassMonitor) { + if (this.groovyClassLoader == null) { + this.groovyClassLoader = buildGroovyClassLoader(ClassUtils.getDefaultClassLoader()); + } + return this.groovyClassLoader; + } + } + + /** + * Build a {@link GroovyClassLoader} for the given {@code ClassLoader}. + * @param classLoader the ClassLoader to build a GroovyClassLoader for + * @since 4.3.3 + */ + protected GroovyClassLoader buildGroovyClassLoader(@Nullable ClassLoader classLoader) { + return (this.compilerConfiguration != null ? + new GroovyClassLoader(classLoader, this.compilerConfiguration) : new GroovyClassLoader(classLoader)); + } + + + @Override + public String getScriptSourceLocator() { + return this.scriptSourceLocator; + } + + /** + * Groovy scripts determine their interfaces themselves, + * hence we don't need to explicitly expose interfaces here. + * @return {@code null} always + */ + @Override + @Nullable + public Class[] getScriptInterfaces() { + return null; + } + + /** + * Groovy scripts do not need a config interface, + * since they expose their setters as public methods. + */ + @Override + public boolean requiresConfigInterface() { + return false; + } + + + /** + * Loads and parses the Groovy script via the GroovyClassLoader. + * @see groovy.lang.GroovyClassLoader + */ + @Override + @Nullable + public Object getScriptedObject(ScriptSource scriptSource, @Nullable Class... actualInterfaces) + throws IOException, ScriptCompilationException { + + synchronized (this.scriptClassMonitor) { + try { + Class scriptClassToExecute; + this.wasModifiedForTypeCheck = false; + + if (this.cachedResult != null) { + Object result = this.cachedResult.object; + this.cachedResult = null; + return result; + } + + if (this.scriptClass == null || scriptSource.isModified()) { + // New script content... + this.scriptClass = getGroovyClassLoader().parseClass( + scriptSource.getScriptAsString(), scriptSource.suggestedClassName()); + + if (Script.class.isAssignableFrom(this.scriptClass)) { + // A Groovy script, probably creating an instance: let's execute it. + Object result = executeScript(scriptSource, this.scriptClass); + this.scriptResultClass = (result != null ? result.getClass() : null); + return result; + } + else { + this.scriptResultClass = this.scriptClass; + } + } + scriptClassToExecute = this.scriptClass; + + // Process re-execution outside of the synchronized block. + return executeScript(scriptSource, scriptClassToExecute); + } + catch (CompilationFailedException ex) { + this.scriptClass = null; + this.scriptResultClass = null; + throw new ScriptCompilationException(scriptSource, ex); + } + } + } + + @Override + @Nullable + public Class getScriptedObjectType(ScriptSource scriptSource) + throws IOException, ScriptCompilationException { + + synchronized (this.scriptClassMonitor) { + try { + if (this.scriptClass == null || scriptSource.isModified()) { + // New script content... + this.wasModifiedForTypeCheck = true; + this.scriptClass = getGroovyClassLoader().parseClass( + scriptSource.getScriptAsString(), scriptSource.suggestedClassName()); + + if (Script.class.isAssignableFrom(this.scriptClass)) { + // A Groovy script, probably creating an instance: let's execute it. + Object result = executeScript(scriptSource, this.scriptClass); + this.scriptResultClass = (result != null ? result.getClass() : null); + this.cachedResult = new CachedResultHolder(result); + } + else { + this.scriptResultClass = this.scriptClass; + } + } + return this.scriptResultClass; + } + catch (CompilationFailedException ex) { + this.scriptClass = null; + this.scriptResultClass = null; + this.cachedResult = null; + throw new ScriptCompilationException(scriptSource, ex); + } + } + } + + @Override + public boolean requiresScriptedObjectRefresh(ScriptSource scriptSource) { + synchronized (this.scriptClassMonitor) { + return (scriptSource.isModified() || this.wasModifiedForTypeCheck); + } + } + + + /** + * Instantiate the given Groovy script class and run it if necessary. + * @param scriptSource the source for the underlying script + * @param scriptClass the Groovy script class + * @return the result object (either an instance of the script class + * or the result of running the script instance) + * @throws ScriptCompilationException in case of instantiation failure + */ + @Nullable + protected Object executeScript(ScriptSource scriptSource, Class scriptClass) throws ScriptCompilationException { + try { + GroovyObject goo = (GroovyObject) ReflectionUtils.accessibleConstructor(scriptClass).newInstance(); + + if (this.groovyObjectCustomizer != null) { + // Allow metaclass and other customization. + this.groovyObjectCustomizer.customize(goo); + } + + if (goo instanceof Script) { + // A Groovy script, probably creating an instance: let's execute it. + return ((Script) goo).run(); + } + else { + // An instance of the scripted class: let's return it as-is. + return goo; + } + } + catch (NoSuchMethodException ex) { + throw new ScriptCompilationException( + "No default constructor on Groovy script class: " + scriptClass.getName(), ex); + } + catch (InstantiationException ex) { + throw new ScriptCompilationException( + scriptSource, "Unable to instantiate Groovy script class: " + scriptClass.getName(), ex); + } + catch (IllegalAccessException ex) { + throw new ScriptCompilationException( + scriptSource, "Could not access Groovy script constructor: " + scriptClass.getName(), ex); + } + catch (InvocationTargetException ex) { + throw new ScriptCompilationException( + "Failed to invoke Groovy script constructor: " + scriptClass.getName(), ex.getTargetException()); + } + } + + + @Override + public String toString() { + return "GroovyScriptFactory: script source locator [" + this.scriptSourceLocator + "]"; + } + + + /** + * Wrapper that holds a temporarily cached result object. + */ + private static class CachedResultHolder { + + @Nullable + public final Object object; + + public CachedResultHolder(@Nullable Object object) { + this.object = object; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/scripting/groovy/package-info.java b/spring-context/src/main/java/org/springframework/scripting/groovy/package-info.java new file mode 100644 index 0000000..2551109 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scripting/groovy/package-info.java @@ -0,0 +1,11 @@ +/** + * Package providing integration of + * Groovy + * into Spring's scripting infrastructure. + */ +@NonNullApi +@NonNullFields +package org.springframework.scripting.groovy; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/scripting/package-info.java b/spring-context/src/main/java/org/springframework/scripting/package-info.java new file mode 100644 index 0000000..53a043f --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scripting/package-info.java @@ -0,0 +1,9 @@ +/** + * Core interfaces for Spring's scripting support. + */ +@NonNullApi +@NonNullFields +package org.springframework.scripting; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/scripting/support/RefreshableScriptTargetSource.java b/spring-context/src/main/java/org/springframework/scripting/support/RefreshableScriptTargetSource.java new file mode 100644 index 0000000..2e46f30 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scripting/support/RefreshableScriptTargetSource.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.support; + +import org.springframework.aop.target.dynamic.BeanFactoryRefreshableTargetSource; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.scripting.ScriptFactory; +import org.springframework.scripting.ScriptSource; +import org.springframework.util.Assert; + +/** + * Subclass of {@link BeanFactoryRefreshableTargetSource} that determines whether + * a refresh is required through the given {@link ScriptFactory}. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Mark Fisher + * @since 2.0 + */ +public class RefreshableScriptTargetSource extends BeanFactoryRefreshableTargetSource { + + private final ScriptFactory scriptFactory; + + private final ScriptSource scriptSource; + + private final boolean isFactoryBean; + + + /** + * Create a new RefreshableScriptTargetSource. + * @param beanFactory the BeanFactory to fetch the scripted bean from + * @param beanName the name of the target bean + * @param scriptFactory the ScriptFactory to delegate to for determining + * whether a refresh is required + * @param scriptSource the ScriptSource for the script definition + * @param isFactoryBean whether the target script defines a FactoryBean + */ + public RefreshableScriptTargetSource(BeanFactory beanFactory, String beanName, + ScriptFactory scriptFactory, ScriptSource scriptSource, boolean isFactoryBean) { + + super(beanFactory, beanName); + Assert.notNull(scriptFactory, "ScriptFactory must not be null"); + Assert.notNull(scriptSource, "ScriptSource must not be null"); + this.scriptFactory = scriptFactory; + this.scriptSource = scriptSource; + this.isFactoryBean = isFactoryBean; + } + + + /** + * Determine whether a refresh is required through calling + * ScriptFactory's {@code requiresScriptedObjectRefresh} method. + * @see ScriptFactory#requiresScriptedObjectRefresh(ScriptSource) + */ + @Override + protected boolean requiresRefresh() { + return this.scriptFactory.requiresScriptedObjectRefresh(this.scriptSource); + } + + /** + * Obtain a fresh target object, retrieving a FactoryBean if necessary. + */ + @Override + protected Object obtainFreshBean(BeanFactory beanFactory, String beanName) { + return super.obtainFreshBean(beanFactory, + (this.isFactoryBean ? BeanFactory.FACTORY_BEAN_PREFIX + beanName : beanName)); + } + +} diff --git a/spring-context/src/main/java/org/springframework/scripting/support/ResourceScriptSource.java b/spring-context/src/main/java/org/springframework/scripting/support/ResourceScriptSource.java new file mode 100644 index 0000000..8f34508 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scripting/support/ResourceScriptSource.java @@ -0,0 +1,142 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.support; + +import java.io.IOException; +import java.io.Reader; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.lang.Nullable; +import org.springframework.scripting.ScriptSource; +import org.springframework.util.Assert; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StringUtils; + +/** + * {@link org.springframework.scripting.ScriptSource} implementation + * based on Spring's {@link org.springframework.core.io.Resource} + * abstraction. Loads the script text from the underlying Resource's + * {@link org.springframework.core.io.Resource#getFile() File} or + * {@link org.springframework.core.io.Resource#getInputStream() InputStream}, + * and tracks the last-modified timestamp of the file (if possible). + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.core.io.Resource#getInputStream() + * @see org.springframework.core.io.Resource#getFile() + * @see org.springframework.core.io.ResourceLoader + */ +public class ResourceScriptSource implements ScriptSource { + + /** Logger available to subclasses. */ + protected final Log logger = LogFactory.getLog(getClass()); + + private EncodedResource resource; + + private long lastModified = -1; + + private final Object lastModifiedMonitor = new Object(); + + + /** + * Create a new ResourceScriptSource for the given resource. + * @param resource the EncodedResource to load the script from + */ + public ResourceScriptSource(EncodedResource resource) { + Assert.notNull(resource, "Resource must not be null"); + this.resource = resource; + } + + /** + * Create a new ResourceScriptSource for the given resource. + * @param resource the Resource to load the script from (using UTF-8 encoding) + */ + public ResourceScriptSource(Resource resource) { + Assert.notNull(resource, "Resource must not be null"); + this.resource = new EncodedResource(resource, "UTF-8"); + } + + + /** + * Return the {@link org.springframework.core.io.Resource} to load the + * script from. + */ + public final Resource getResource() { + return this.resource.getResource(); + } + + /** + * Set the encoding used for reading the script resource. + *

    The default value for regular Resources is "UTF-8". + * A {@code null} value implies the platform default. + */ + public void setEncoding(@Nullable String encoding) { + this.resource = new EncodedResource(this.resource.getResource(), encoding); + } + + + @Override + public String getScriptAsString() throws IOException { + synchronized (this.lastModifiedMonitor) { + this.lastModified = retrieveLastModifiedTime(); + } + Reader reader = this.resource.getReader(); + return FileCopyUtils.copyToString(reader); + } + + @Override + public boolean isModified() { + synchronized (this.lastModifiedMonitor) { + return (this.lastModified < 0 || retrieveLastModifiedTime() > this.lastModified); + } + } + + /** + * Retrieve the current last-modified timestamp of the underlying resource. + * @return the current timestamp, or 0 if not determinable + */ + protected long retrieveLastModifiedTime() { + try { + return getResource().lastModified(); + } + catch (IOException ex) { + if (logger.isDebugEnabled()) { + logger.debug(getResource() + " could not be resolved in the file system - " + + "current timestamp not available for script modification check", ex); + } + return 0; + } + } + + @Override + @Nullable + public String suggestedClassName() { + String filename = getResource().getFilename(); + return (filename != null ? StringUtils.stripFilenameExtension(filename) : null); + } + + @Override + public String toString() { + return this.resource.toString(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java b/spring-context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java new file mode 100644 index 0000000..3977247 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java @@ -0,0 +1,603 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.support; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aop.TargetSource; +import org.springframework.aop.framework.AopInfrastructureBean; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.support.DelegatingIntroductionInterceptor; +import org.springframework.asm.Type; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.PropertyValues; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanCurrentlyInCreationException; +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor; +import org.springframework.beans.factory.support.BeanDefinitionValidationException; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.cglib.core.Signature; +import org.springframework.cglib.proxy.InterfaceMaker; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.Conventions; +import org.springframework.core.Ordered; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.ResourceLoader; +import org.springframework.lang.Nullable; +import org.springframework.scripting.ScriptFactory; +import org.springframework.scripting.ScriptSource; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * {@link org.springframework.beans.factory.config.BeanPostProcessor} that + * handles {@link org.springframework.scripting.ScriptFactory} definitions, + * replacing each factory with the actual scripted Java object generated by it. + * + *

    This is similar to the + * {@link org.springframework.beans.factory.FactoryBean} mechanism, but is + * specifically tailored for scripts and not built into Spring's core + * container itself but rather implemented as an extension. + * + *

    NOTE: The most important characteristic of this post-processor + * is that constructor arguments are applied to the + * {@link org.springframework.scripting.ScriptFactory} instance + * while bean property values are applied to the generated scripted object. + * Typically, constructor arguments include a script source locator and + * potentially script interfaces, while bean property values include + * references and config values to inject into the scripted object itself. + * + *

    The following {@link ScriptFactoryPostProcessor} will automatically + * be applied to the two + * {@link org.springframework.scripting.ScriptFactory} definitions below. + * At runtime, the actual scripted objects will be exposed for + * "bshMessenger" and "groovyMessenger", rather than the + * {@link org.springframework.scripting.ScriptFactory} instances. Both of + * those are supposed to be castable to the example's {@code Messenger} + * interfaces here. + * + *

    <bean class="org.springframework.scripting.support.ScriptFactoryPostProcessor"/>
    + *
    + * <bean id="bshMessenger" class="org.springframework.scripting.bsh.BshScriptFactory">
    + *   <constructor-arg value="classpath:mypackage/Messenger.bsh"/>
    + *   <constructor-arg value="mypackage.Messenger"/>
    + *   <property name="message" value="Hello World!"/>
    + * </bean>
    + *
    + * <bean id="groovyMessenger" class="org.springframework.scripting.groovy.GroovyScriptFactory">
    + *   <constructor-arg value="classpath:mypackage/Messenger.groovy"/>
    + *   <property name="message" value="Hello World!"/>
    + * </bean>
    + * + *

    NOTE: Please note that the above excerpt from a Spring + * XML bean definition file uses just the <bean/>-style syntax + * (in an effort to illustrate using the {@link ScriptFactoryPostProcessor} itself). + * In reality, you would never create a <bean/> definition for a + * {@link ScriptFactoryPostProcessor} explicitly; rather you would import the + * tags from the {@code 'lang'} namespace and simply create scripted + * beans using the tags in that namespace... as part of doing so, a + * {@link ScriptFactoryPostProcessor} will implicitly be created for you. + * + *

    The Spring reference documentation contains numerous examples of using + * tags in the {@code 'lang'} namespace; by way of an example, find below + * a Groovy-backed bean defined using the {@code 'lang:groovy'} tag. + * + *

    + * <?xml version="1.0" encoding="UTF-8"?>
    + * <beans xmlns="http://www.springframework.org/schema/beans"
    + *     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    + *     xmlns:lang="http://www.springframework.org/schema/lang">
    + *
    + *   <!-- this is the bean definition for the Groovy-backed Messenger implementation -->
    + *   <lang:groovy id="messenger" script-source="classpath:Messenger.groovy">
    + *     <lang:property name="message" value="I Can Do The Frug" />
    + *   </lang:groovy>
    + *
    + *   <!-- an otherwise normal bean that will be injected by the Groovy-backed Messenger -->
    + *   <bean id="bookingService" class="x.y.DefaultBookingService">
    + *     <property name="messenger" ref="messenger" />
    + *   </bean>
    + *
    + * </beans>
    + * + * @author Juergen Hoeller + * @author Rob Harrop + * @author Rick Evans + * @author Mark Fisher + * @author Sam Brannen + * @since 2.0 + */ +public class ScriptFactoryPostProcessor implements SmartInstantiationAwareBeanPostProcessor, + BeanClassLoaderAware, BeanFactoryAware, ResourceLoaderAware, DisposableBean, Ordered { + + /** + * The {@link org.springframework.core.io.Resource}-style prefix that denotes + * an inline script. + *

    An inline script is a script that is defined right there in the (typically XML) + * configuration, as opposed to being defined in an external file. + */ + public static final String INLINE_SCRIPT_PREFIX = "inline:"; + + /** + * The {@code refreshCheckDelay} attribute. + */ + public static final String REFRESH_CHECK_DELAY_ATTRIBUTE = Conventions.getQualifiedAttributeName( + ScriptFactoryPostProcessor.class, "refreshCheckDelay"); + + /** + * The {@code proxyTargetClass} attribute. + */ + public static final String PROXY_TARGET_CLASS_ATTRIBUTE = Conventions.getQualifiedAttributeName( + ScriptFactoryPostProcessor.class, "proxyTargetClass"); + + /** + * The {@code language} attribute. + */ + public static final String LANGUAGE_ATTRIBUTE = Conventions.getQualifiedAttributeName( + ScriptFactoryPostProcessor.class, "language"); + + private static final String SCRIPT_FACTORY_NAME_PREFIX = "scriptFactory."; + + private static final String SCRIPTED_OBJECT_NAME_PREFIX = "scriptedObject."; + + + /** Logger available to subclasses. */ + protected final Log logger = LogFactory.getLog(getClass()); + + private long defaultRefreshCheckDelay = -1; + + private boolean defaultProxyTargetClass = false; + + @Nullable + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + @Nullable + private ConfigurableBeanFactory beanFactory; + + private ResourceLoader resourceLoader = new DefaultResourceLoader(); + + final DefaultListableBeanFactory scriptBeanFactory = new DefaultListableBeanFactory(); + + /** Map from bean name String to ScriptSource object. */ + private final Map scriptSourceCache = new ConcurrentHashMap<>(); + + + /** + * Set the delay between refresh checks, in milliseconds. + * Default is -1, indicating no refresh checks at all. + *

    Note that an actual refresh will only happen when + * the {@link org.springframework.scripting.ScriptSource} indicates + * that it has been modified. + * @see org.springframework.scripting.ScriptSource#isModified() + */ + public void setDefaultRefreshCheckDelay(long defaultRefreshCheckDelay) { + this.defaultRefreshCheckDelay = defaultRefreshCheckDelay; + } + + /** + * Flag to signal that refreshable proxies should be created to proxy the target class not its interfaces. + * @param defaultProxyTargetClass the flag value to set + */ + public void setDefaultProxyTargetClass(boolean defaultProxyTargetClass) { + this.defaultProxyTargetClass = defaultProxyTargetClass; + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + if (!(beanFactory instanceof ConfigurableBeanFactory)) { + throw new IllegalStateException("ScriptFactoryPostProcessor doesn't work with " + + "non-ConfigurableBeanFactory: " + beanFactory.getClass()); + } + this.beanFactory = (ConfigurableBeanFactory) beanFactory; + + // Required so that references (up container hierarchies) are correctly resolved. + this.scriptBeanFactory.setParentBeanFactory(this.beanFactory); + + // Required so that all BeanPostProcessors, Scopes, etc become available. + this.scriptBeanFactory.copyConfigurationFrom(this.beanFactory); + + // Filter out BeanPostProcessors that are part of the AOP infrastructure, + // since those are only meant to apply to beans defined in the original factory. + this.scriptBeanFactory.getBeanPostProcessors().removeIf(beanPostProcessor -> + beanPostProcessor instanceof AopInfrastructureBean); + } + + @Override + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + @Override + public int getOrder() { + return Integer.MIN_VALUE; + } + + + @Override + @Nullable + public Class predictBeanType(Class beanClass, String beanName) { + // We only apply special treatment to ScriptFactory implementations here. + if (!ScriptFactory.class.isAssignableFrom(beanClass)) { + return null; + } + + Assert.state(this.beanFactory != null, "No BeanFactory set"); + BeanDefinition bd = this.beanFactory.getMergedBeanDefinition(beanName); + + try { + String scriptFactoryBeanName = SCRIPT_FACTORY_NAME_PREFIX + beanName; + String scriptedObjectBeanName = SCRIPTED_OBJECT_NAME_PREFIX + beanName; + prepareScriptBeans(bd, scriptFactoryBeanName, scriptedObjectBeanName); + + ScriptFactory scriptFactory = this.scriptBeanFactory.getBean(scriptFactoryBeanName, ScriptFactory.class); + ScriptSource scriptSource = getScriptSource(scriptFactoryBeanName, scriptFactory.getScriptSourceLocator()); + Class[] interfaces = scriptFactory.getScriptInterfaces(); + + Class scriptedType = scriptFactory.getScriptedObjectType(scriptSource); + if (scriptedType != null) { + return scriptedType; + } + else if (!ObjectUtils.isEmpty(interfaces)) { + return (interfaces.length == 1 ? interfaces[0] : createCompositeInterface(interfaces)); + } + else { + if (bd.isSingleton()) { + return this.scriptBeanFactory.getBean(scriptedObjectBeanName).getClass(); + } + } + } + catch (Exception ex) { + if (ex instanceof BeanCreationException && + ((BeanCreationException) ex).getMostSpecificCause() instanceof BeanCurrentlyInCreationException) { + if (logger.isTraceEnabled()) { + logger.trace("Could not determine scripted object type for bean '" + beanName + "': " + + ex.getMessage()); + } + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Could not determine scripted object type for bean '" + beanName + "'", ex); + } + } + } + + return null; + } + + @Override + public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) { + return pvs; + } + + @Override + public Object postProcessBeforeInstantiation(Class beanClass, String beanName) { + // We only apply special treatment to ScriptFactory implementations here. + if (!ScriptFactory.class.isAssignableFrom(beanClass)) { + return null; + } + + Assert.state(this.beanFactory != null, "No BeanFactory set"); + BeanDefinition bd = this.beanFactory.getMergedBeanDefinition(beanName); + String scriptFactoryBeanName = SCRIPT_FACTORY_NAME_PREFIX + beanName; + String scriptedObjectBeanName = SCRIPTED_OBJECT_NAME_PREFIX + beanName; + prepareScriptBeans(bd, scriptFactoryBeanName, scriptedObjectBeanName); + + ScriptFactory scriptFactory = this.scriptBeanFactory.getBean(scriptFactoryBeanName, ScriptFactory.class); + ScriptSource scriptSource = getScriptSource(scriptFactoryBeanName, scriptFactory.getScriptSourceLocator()); + boolean isFactoryBean = false; + try { + Class scriptedObjectType = scriptFactory.getScriptedObjectType(scriptSource); + // Returned type may be null if the factory is unable to determine the type. + if (scriptedObjectType != null) { + isFactoryBean = FactoryBean.class.isAssignableFrom(scriptedObjectType); + } + } + catch (Exception ex) { + throw new BeanCreationException(beanName, + "Could not determine scripted object type for " + scriptFactory, ex); + } + + long refreshCheckDelay = resolveRefreshCheckDelay(bd); + if (refreshCheckDelay >= 0) { + Class[] interfaces = scriptFactory.getScriptInterfaces(); + RefreshableScriptTargetSource ts = new RefreshableScriptTargetSource(this.scriptBeanFactory, + scriptedObjectBeanName, scriptFactory, scriptSource, isFactoryBean); + boolean proxyTargetClass = resolveProxyTargetClass(bd); + String language = (String) bd.getAttribute(LANGUAGE_ATTRIBUTE); + if (proxyTargetClass && (language == null || !language.equals("groovy"))) { + throw new BeanDefinitionValidationException( + "Cannot use proxyTargetClass=true with script beans where language is not 'groovy': '" + + language + "'"); + } + ts.setRefreshCheckDelay(refreshCheckDelay); + return createRefreshableProxy(ts, interfaces, proxyTargetClass); + } + + if (isFactoryBean) { + scriptedObjectBeanName = BeanFactory.FACTORY_BEAN_PREFIX + scriptedObjectBeanName; + } + return this.scriptBeanFactory.getBean(scriptedObjectBeanName); + } + + /** + * Prepare the script beans in the internal BeanFactory that this + * post-processor uses. Each original bean definition will be split + * into a ScriptFactory definition and a scripted object definition. + * @param bd the original bean definition in the main BeanFactory + * @param scriptFactoryBeanName the name of the internal ScriptFactory bean + * @param scriptedObjectBeanName the name of the internal scripted object bean + */ + protected void prepareScriptBeans(BeanDefinition bd, String scriptFactoryBeanName, String scriptedObjectBeanName) { + // Avoid recreation of the script bean definition in case of a prototype. + synchronized (this.scriptBeanFactory) { + if (!this.scriptBeanFactory.containsBeanDefinition(scriptedObjectBeanName)) { + + this.scriptBeanFactory.registerBeanDefinition( + scriptFactoryBeanName, createScriptFactoryBeanDefinition(bd)); + ScriptFactory scriptFactory = + this.scriptBeanFactory.getBean(scriptFactoryBeanName, ScriptFactory.class); + ScriptSource scriptSource = + getScriptSource(scriptFactoryBeanName, scriptFactory.getScriptSourceLocator()); + Class[] interfaces = scriptFactory.getScriptInterfaces(); + + Class[] scriptedInterfaces = interfaces; + if (scriptFactory.requiresConfigInterface() && !bd.getPropertyValues().isEmpty()) { + Class configInterface = createConfigInterface(bd, interfaces); + scriptedInterfaces = ObjectUtils.addObjectToArray(interfaces, configInterface); + } + + BeanDefinition objectBd = createScriptedObjectBeanDefinition( + bd, scriptFactoryBeanName, scriptSource, scriptedInterfaces); + long refreshCheckDelay = resolveRefreshCheckDelay(bd); + if (refreshCheckDelay >= 0) { + objectBd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + } + + this.scriptBeanFactory.registerBeanDefinition(scriptedObjectBeanName, objectBd); + } + } + } + + /** + * Get the refresh check delay for the given {@link ScriptFactory} {@link BeanDefinition}. + * If the {@link BeanDefinition} has a + * {@link org.springframework.core.AttributeAccessor metadata attribute} + * under the key {@link #REFRESH_CHECK_DELAY_ATTRIBUTE} which is a valid {@link Number} + * type, then this value is used. Otherwise, the {@link #defaultRefreshCheckDelay} + * value is used. + * @param beanDefinition the BeanDefinition to check + * @return the refresh check delay + */ + protected long resolveRefreshCheckDelay(BeanDefinition beanDefinition) { + long refreshCheckDelay = this.defaultRefreshCheckDelay; + Object attributeValue = beanDefinition.getAttribute(REFRESH_CHECK_DELAY_ATTRIBUTE); + if (attributeValue instanceof Number) { + refreshCheckDelay = ((Number) attributeValue).longValue(); + } + else if (attributeValue instanceof String) { + refreshCheckDelay = Long.parseLong((String) attributeValue); + } + else if (attributeValue != null) { + throw new BeanDefinitionStoreException("Invalid refresh check delay attribute [" + + REFRESH_CHECK_DELAY_ATTRIBUTE + "] with value '" + attributeValue + + "': needs to be of type Number or String"); + } + return refreshCheckDelay; + } + + protected boolean resolveProxyTargetClass(BeanDefinition beanDefinition) { + boolean proxyTargetClass = this.defaultProxyTargetClass; + Object attributeValue = beanDefinition.getAttribute(PROXY_TARGET_CLASS_ATTRIBUTE); + if (attributeValue instanceof Boolean) { + proxyTargetClass = (Boolean) attributeValue; + } + else if (attributeValue instanceof String) { + proxyTargetClass = Boolean.parseBoolean((String) attributeValue); + } + else if (attributeValue != null) { + throw new BeanDefinitionStoreException("Invalid proxy target class attribute [" + + PROXY_TARGET_CLASS_ATTRIBUTE + "] with value '" + attributeValue + + "': needs to be of type Boolean or String"); + } + return proxyTargetClass; + } + + /** + * Create a ScriptFactory bean definition based on the given script definition, + * extracting only the definition data that is relevant for the ScriptFactory + * (that is, only bean class and constructor arguments). + * @param bd the full script bean definition + * @return the extracted ScriptFactory bean definition + * @see org.springframework.scripting.ScriptFactory + */ + protected BeanDefinition createScriptFactoryBeanDefinition(BeanDefinition bd) { + GenericBeanDefinition scriptBd = new GenericBeanDefinition(); + scriptBd.setBeanClassName(bd.getBeanClassName()); + scriptBd.getConstructorArgumentValues().addArgumentValues(bd.getConstructorArgumentValues()); + return scriptBd; + } + + /** + * Obtain a ScriptSource for the given bean, lazily creating it + * if not cached already. + * @param beanName the name of the scripted bean + * @param scriptSourceLocator the script source locator associated with the bean + * @return the corresponding ScriptSource instance + * @see #convertToScriptSource + */ + protected ScriptSource getScriptSource(String beanName, String scriptSourceLocator) { + return this.scriptSourceCache.computeIfAbsent(beanName, key -> + convertToScriptSource(beanName, scriptSourceLocator, this.resourceLoader)); + } + + /** + * Convert the given script source locator to a ScriptSource instance. + *

    By default, supported locators are Spring resource locations + * (such as "file:C:/myScript.bsh" or "classpath:myPackage/myScript.bsh") + * and inline scripts ("inline:myScriptText..."). + * @param beanName the name of the scripted bean + * @param scriptSourceLocator the script source locator + * @param resourceLoader the ResourceLoader to use (if necessary) + * @return the ScriptSource instance + */ + protected ScriptSource convertToScriptSource(String beanName, String scriptSourceLocator, + ResourceLoader resourceLoader) { + + if (scriptSourceLocator.startsWith(INLINE_SCRIPT_PREFIX)) { + return new StaticScriptSource(scriptSourceLocator.substring(INLINE_SCRIPT_PREFIX.length()), beanName); + } + else { + return new ResourceScriptSource(resourceLoader.getResource(scriptSourceLocator)); + } + } + + /** + * Create a config interface for the given bean definition, defining setter + * methods for the defined property values as well as an init method and + * a destroy method (if defined). + *

    This implementation creates the interface via CGLIB's InterfaceMaker, + * determining the property types from the given interfaces (as far as possible). + * @param bd the bean definition (property values etc) to create a + * config interface for + * @param interfaces the interfaces to check against (might define + * getters corresponding to the setters we're supposed to generate) + * @return the config interface + * @see org.springframework.cglib.proxy.InterfaceMaker + * @see org.springframework.beans.BeanUtils#findPropertyType + */ + protected Class createConfigInterface(BeanDefinition bd, @Nullable Class[] interfaces) { + InterfaceMaker maker = new InterfaceMaker(); + PropertyValue[] pvs = bd.getPropertyValues().getPropertyValues(); + for (PropertyValue pv : pvs) { + String propertyName = pv.getName(); + Class propertyType = BeanUtils.findPropertyType(propertyName, interfaces); + String setterName = "set" + StringUtils.capitalize(propertyName); + Signature signature = new Signature(setterName, Type.VOID_TYPE, new Type[] {Type.getType(propertyType)}); + maker.add(signature, new Type[0]); + } + if (bd.getInitMethodName() != null) { + Signature signature = new Signature(bd.getInitMethodName(), Type.VOID_TYPE, new Type[0]); + maker.add(signature, new Type[0]); + } + if (StringUtils.hasText(bd.getDestroyMethodName())) { + Signature signature = new Signature(bd.getDestroyMethodName(), Type.VOID_TYPE, new Type[0]); + maker.add(signature, new Type[0]); + } + return maker.create(); + } + + /** + * Create a composite interface Class for the given interfaces, + * implementing the given interfaces in one single Class. + *

    The default implementation builds a JDK proxy class + * for the given interfaces. + * @param interfaces the interfaces to merge + * @return the merged interface as Class + * @see java.lang.reflect.Proxy#getProxyClass + */ + protected Class createCompositeInterface(Class[] interfaces) { + return ClassUtils.createCompositeInterface(interfaces, this.beanClassLoader); + } + + /** + * Create a bean definition for the scripted object, based on the given script + * definition, extracting the definition data that is relevant for the scripted + * object (that is, everything but bean class and constructor arguments). + * @param bd the full script bean definition + * @param scriptFactoryBeanName the name of the internal ScriptFactory bean + * @param scriptSource the ScriptSource for the scripted bean + * @param interfaces the interfaces that the scripted bean is supposed to implement + * @return the extracted ScriptFactory bean definition + * @see org.springframework.scripting.ScriptFactory#getScriptedObject + */ + protected BeanDefinition createScriptedObjectBeanDefinition(BeanDefinition bd, String scriptFactoryBeanName, + ScriptSource scriptSource, @Nullable Class[] interfaces) { + + GenericBeanDefinition objectBd = new GenericBeanDefinition(bd); + objectBd.setFactoryBeanName(scriptFactoryBeanName); + objectBd.setFactoryMethodName("getScriptedObject"); + objectBd.getConstructorArgumentValues().clear(); + objectBd.getConstructorArgumentValues().addIndexedArgumentValue(0, scriptSource); + objectBd.getConstructorArgumentValues().addIndexedArgumentValue(1, interfaces); + return objectBd; + } + + /** + * Create a refreshable proxy for the given AOP TargetSource. + * @param ts the refreshable TargetSource + * @param interfaces the proxy interfaces (may be {@code null} to + * indicate proxying of all interfaces implemented by the target class) + * @return the generated proxy + * @see RefreshableScriptTargetSource + */ + protected Object createRefreshableProxy(TargetSource ts, @Nullable Class[] interfaces, boolean proxyTargetClass) { + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.setTargetSource(ts); + ClassLoader classLoader = this.beanClassLoader; + + if (interfaces != null) { + proxyFactory.setInterfaces(interfaces); + } + else { + Class targetClass = ts.getTargetClass(); + if (targetClass != null) { + proxyFactory.setInterfaces(ClassUtils.getAllInterfacesForClass(targetClass, this.beanClassLoader)); + } + } + + if (proxyTargetClass) { + classLoader = null; // force use of Class.getClassLoader() + proxyFactory.setProxyTargetClass(true); + } + + DelegatingIntroductionInterceptor introduction = new DelegatingIntroductionInterceptor(ts); + introduction.suppressInterface(TargetSource.class); + proxyFactory.addAdvice(introduction); + + return proxyFactory.getProxy(classLoader); + } + + /** + * Destroy the inner bean factory (used for scripts) on shutdown. + */ + @Override + public void destroy() { + this.scriptBeanFactory.destroySingletons(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptEvalException.java b/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptEvalException.java new file mode 100644 index 0000000..6545b83 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptEvalException.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.support; + +import javax.script.ScriptException; + +/** + * Exception decorating a {@link javax.script.ScriptException} coming out of + * JSR-223 script evaluation, i.e. a {@link javax.script.ScriptEngine#eval} + * call or {@link javax.script.Invocable#invokeMethod} / + * {@link javax.script.Invocable#invokeFunction} call. + * + *

    This exception does not print the Java stacktrace, since the JSR-223 + * {@link ScriptException} results in a rather convoluted text output. + * From that perspective, this exception is primarily a decorator for a + * {@link ScriptException} root cause passed into an outer exception. + * + * @author Juergen Hoeller + * @author Sebastien Deleuze + * @since 4.2.2 + */ +@SuppressWarnings("serial") +public class StandardScriptEvalException extends RuntimeException { + + private final ScriptException scriptException; + + + /** + * Construct a new script eval exception with the specified original exception. + */ + public StandardScriptEvalException(ScriptException ex) { + super(ex.getMessage()); + this.scriptException = ex; + } + + + public final ScriptException getScriptException() { + return this.scriptException; + } + + @Override + public synchronized Throwable fillInStackTrace() { + return this; + } + +} diff --git a/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptEvaluator.java b/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptEvaluator.java new file mode 100644 index 0000000..9f3f34a --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptEvaluator.java @@ -0,0 +1,195 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.support; + +import java.io.IOException; +import java.util.Map; + +import javax.script.Bindings; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; +import org.springframework.scripting.ScriptCompilationException; +import org.springframework.scripting.ScriptEvaluator; +import org.springframework.scripting.ScriptSource; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +/** + * {@code javax.script} (JSR-223) based implementation of Spring's {@link ScriptEvaluator} + * strategy interface. + * + * @author Juergen Hoeller + * @author Costin Leau + * @since 4.0 + * @see ScriptEngine#eval(String) + */ +public class StandardScriptEvaluator implements ScriptEvaluator, BeanClassLoaderAware { + + @Nullable + private String engineName; + + @Nullable + private volatile Bindings globalBindings; + + @Nullable + private volatile ScriptEngineManager scriptEngineManager; + + + /** + * Construct a new {@code StandardScriptEvaluator}. + */ + public StandardScriptEvaluator() { + } + + /** + * Construct a new {@code StandardScriptEvaluator} for the given class loader. + * @param classLoader the class loader to use for script engine detection + */ + public StandardScriptEvaluator(ClassLoader classLoader) { + this.scriptEngineManager = new ScriptEngineManager(classLoader); + } + + /** + * Construct a new {@code StandardScriptEvaluator} for the given JSR-223 + * {@link ScriptEngineManager} to obtain script engines from. + * @param scriptEngineManager the ScriptEngineManager (or subclass thereof) to use + * @since 4.2.2 + */ + public StandardScriptEvaluator(ScriptEngineManager scriptEngineManager) { + this.scriptEngineManager = scriptEngineManager; + } + + + /** + * Set the name of the language meant for evaluating the scripts (e.g. "Groovy"). + *

    This is effectively an alias for {@link #setEngineName "engineName"}, + * potentially (but not yet) providing common abbreviations for certain languages + * beyond what the JSR-223 script engine factory exposes. + * @see #setEngineName + */ + public void setLanguage(String language) { + this.engineName = language; + } + + /** + * Set the name of the script engine for evaluating the scripts (e.g. "Groovy"), + * as exposed by the JSR-223 script engine factory. + * @since 4.2.2 + * @see #setLanguage + */ + public void setEngineName(String engineName) { + this.engineName = engineName; + } + + /** + * Set the globally scoped bindings on the underlying script engine manager, + * shared by all scripts, as an alternative to script argument bindings. + * @since 4.2.2 + * @see #evaluate(ScriptSource, Map) + * @see javax.script.ScriptEngineManager#setBindings(Bindings) + * @see javax.script.SimpleBindings + */ + public void setGlobalBindings(Map globalBindings) { + Bindings bindings = StandardScriptUtils.getBindings(globalBindings); + this.globalBindings = bindings; + ScriptEngineManager scriptEngineManager = this.scriptEngineManager; + if (scriptEngineManager != null) { + scriptEngineManager.setBindings(bindings); + } + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + ScriptEngineManager scriptEngineManager = this.scriptEngineManager; + if (scriptEngineManager == null) { + scriptEngineManager = new ScriptEngineManager(classLoader); + this.scriptEngineManager = scriptEngineManager; + Bindings bindings = this.globalBindings; + if (bindings != null) { + scriptEngineManager.setBindings(bindings); + } + } + } + + + @Override + @Nullable + public Object evaluate(ScriptSource script) { + return evaluate(script, null); + } + + @Override + @Nullable + public Object evaluate(ScriptSource script, @Nullable Map argumentBindings) { + ScriptEngine engine = getScriptEngine(script); + try { + if (CollectionUtils.isEmpty(argumentBindings)) { + return engine.eval(script.getScriptAsString()); + } + else { + Bindings bindings = StandardScriptUtils.getBindings(argumentBindings); + return engine.eval(script.getScriptAsString(), bindings); + } + } + catch (IOException ex) { + throw new ScriptCompilationException(script, "Cannot access script for ScriptEngine", ex); + } + catch (ScriptException ex) { + throw new ScriptCompilationException(script, new StandardScriptEvalException(ex)); + } + } + + /** + * Obtain the JSR-223 ScriptEngine to use for the given script. + * @param script the script to evaluate + * @return the ScriptEngine (never {@code null}) + */ + protected ScriptEngine getScriptEngine(ScriptSource script) { + ScriptEngineManager scriptEngineManager = this.scriptEngineManager; + if (scriptEngineManager == null) { + scriptEngineManager = new ScriptEngineManager(); + this.scriptEngineManager = scriptEngineManager; + } + + if (StringUtils.hasText(this.engineName)) { + return StandardScriptUtils.retrieveEngineByName(scriptEngineManager, this.engineName); + } + else if (script instanceof ResourceScriptSource) { + Resource resource = ((ResourceScriptSource) script).getResource(); + String extension = StringUtils.getFilenameExtension(resource.getFilename()); + if (extension == null) { + throw new IllegalStateException( + "No script language defined, and no file extension defined for resource: " + resource); + } + ScriptEngine engine = scriptEngineManager.getEngineByExtension(extension); + if (engine == null) { + throw new IllegalStateException("No matching engine found for file extension '" + extension + "'"); + } + return engine; + } + else { + throw new IllegalStateException( + "No script language defined, and no resource associated with script: " + script); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptFactory.java b/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptFactory.java new file mode 100644 index 0000000..80a1a8f --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptFactory.java @@ -0,0 +1,283 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.support; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; + +import javax.script.Invocable; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.lang.Nullable; +import org.springframework.scripting.ScriptCompilationException; +import org.springframework.scripting.ScriptFactory; +import org.springframework.scripting.ScriptSource; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * {@link org.springframework.scripting.ScriptFactory} implementation based + * on the JSR-223 script engine abstraction (as included in Java 6+). + * Supports JavaScript, Groovy, JRuby, and other JSR-223 compliant engines. + * + *

    Typically used in combination with a + * {@link org.springframework.scripting.support.ScriptFactoryPostProcessor}; + * see the latter's javadoc for a configuration example. + * + * @author Juergen Hoeller + * @since 4.2 + * @see ScriptFactoryPostProcessor + */ +public class StandardScriptFactory implements ScriptFactory, BeanClassLoaderAware { + + @Nullable + private final String scriptEngineName; + + private final String scriptSourceLocator; + + @Nullable + private final Class[] scriptInterfaces; + + @Nullable + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + @Nullable + private volatile ScriptEngine scriptEngine; + + + /** + * Create a new StandardScriptFactory for the given script source. + * @param scriptSourceLocator a locator that points to the source of the script. + * Interpreted by the post-processor that actually creates the script. + */ + public StandardScriptFactory(String scriptSourceLocator) { + this(null, scriptSourceLocator, (Class[]) null); + } + + /** + * Create a new StandardScriptFactory for the given script source. + * @param scriptSourceLocator a locator that points to the source of the script. + * Interpreted by the post-processor that actually creates the script. + * @param scriptInterfaces the Java interfaces that the scripted object + * is supposed to implement + */ + public StandardScriptFactory(String scriptSourceLocator, Class... scriptInterfaces) { + this(null, scriptSourceLocator, scriptInterfaces); + } + + /** + * Create a new StandardScriptFactory for the given script source. + * @param scriptEngineName the name of the JSR-223 ScriptEngine to use + * (explicitly given instead of inferred from the script source) + * @param scriptSourceLocator a locator that points to the source of the script. + * Interpreted by the post-processor that actually creates the script. + */ + public StandardScriptFactory(String scriptEngineName, String scriptSourceLocator) { + this(scriptEngineName, scriptSourceLocator, (Class[]) null); + } + + /** + * Create a new StandardScriptFactory for the given script source. + * @param scriptEngineName the name of the JSR-223 ScriptEngine to use + * (explicitly given instead of inferred from the script source) + * @param scriptSourceLocator a locator that points to the source of the script. + * Interpreted by the post-processor that actually creates the script. + * @param scriptInterfaces the Java interfaces that the scripted object + * is supposed to implement + */ + public StandardScriptFactory( + @Nullable String scriptEngineName, String scriptSourceLocator, @Nullable Class... scriptInterfaces) { + + Assert.hasText(scriptSourceLocator, "'scriptSourceLocator' must not be empty"); + this.scriptEngineName = scriptEngineName; + this.scriptSourceLocator = scriptSourceLocator; + this.scriptInterfaces = scriptInterfaces; + } + + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + @Override + public String getScriptSourceLocator() { + return this.scriptSourceLocator; + } + + @Override + @Nullable + public Class[] getScriptInterfaces() { + return this.scriptInterfaces; + } + + @Override + public boolean requiresConfigInterface() { + return false; + } + + + /** + * Load and parse the script via JSR-223's ScriptEngine. + */ + @Override + @Nullable + public Object getScriptedObject(ScriptSource scriptSource, @Nullable Class... actualInterfaces) + throws IOException, ScriptCompilationException { + + Object script = evaluateScript(scriptSource); + + if (!ObjectUtils.isEmpty(actualInterfaces)) { + boolean adaptationRequired = false; + for (Class requestedIfc : actualInterfaces) { + if (script instanceof Class ? !requestedIfc.isAssignableFrom((Class) script) : + !requestedIfc.isInstance(script)) { + adaptationRequired = true; + break; + } + } + if (adaptationRequired) { + script = adaptToInterfaces(script, scriptSource, actualInterfaces); + } + } + + if (script instanceof Class) { + Class scriptClass = (Class) script; + try { + return ReflectionUtils.accessibleConstructor(scriptClass).newInstance(); + } + catch (NoSuchMethodException ex) { + throw new ScriptCompilationException( + "No default constructor on script class: " + scriptClass.getName(), ex); + } + catch (InstantiationException ex) { + throw new ScriptCompilationException( + scriptSource, "Unable to instantiate script class: " + scriptClass.getName(), ex); + } + catch (IllegalAccessException ex) { + throw new ScriptCompilationException( + scriptSource, "Could not access script constructor: " + scriptClass.getName(), ex); + } + catch (InvocationTargetException ex) { + throw new ScriptCompilationException( + "Failed to invoke script constructor: " + scriptClass.getName(), ex.getTargetException()); + } + } + + return script; + } + + protected Object evaluateScript(ScriptSource scriptSource) { + try { + ScriptEngine scriptEngine = this.scriptEngine; + if (scriptEngine == null) { + scriptEngine = retrieveScriptEngine(scriptSource); + if (scriptEngine == null) { + throw new IllegalStateException("Could not determine script engine for " + scriptSource); + } + this.scriptEngine = scriptEngine; + } + return scriptEngine.eval(scriptSource.getScriptAsString()); + } + catch (Exception ex) { + throw new ScriptCompilationException(scriptSource, ex); + } + } + + @Nullable + protected ScriptEngine retrieveScriptEngine(ScriptSource scriptSource) { + ScriptEngineManager scriptEngineManager = new ScriptEngineManager(this.beanClassLoader); + + if (this.scriptEngineName != null) { + return StandardScriptUtils.retrieveEngineByName(scriptEngineManager, this.scriptEngineName); + } + + if (scriptSource instanceof ResourceScriptSource) { + String filename = ((ResourceScriptSource) scriptSource).getResource().getFilename(); + if (filename != null) { + String extension = StringUtils.getFilenameExtension(filename); + if (extension != null) { + ScriptEngine engine = scriptEngineManager.getEngineByExtension(extension); + if (engine != null) { + return engine; + } + } + } + } + + return null; + } + + @Nullable + protected Object adaptToInterfaces( + @Nullable Object script, ScriptSource scriptSource, Class... actualInterfaces) { + + Class adaptedIfc; + if (actualInterfaces.length == 1) { + adaptedIfc = actualInterfaces[0]; + } + else { + adaptedIfc = ClassUtils.createCompositeInterface(actualInterfaces, this.beanClassLoader); + } + + if (adaptedIfc != null) { + ScriptEngine scriptEngine = this.scriptEngine; + if (!(scriptEngine instanceof Invocable)) { + throw new ScriptCompilationException(scriptSource, + "ScriptEngine must implement Invocable in order to adapt it to an interface: " + scriptEngine); + } + Invocable invocable = (Invocable) scriptEngine; + if (script != null) { + script = invocable.getInterface(script, adaptedIfc); + } + if (script == null) { + script = invocable.getInterface(adaptedIfc); + if (script == null) { + throw new ScriptCompilationException(scriptSource, + "Could not adapt script to interface [" + adaptedIfc.getName() + "]"); + } + } + } + + return script; + } + + @Override + @Nullable + public Class getScriptedObjectType(ScriptSource scriptSource) + throws IOException, ScriptCompilationException { + + return null; + } + + @Override + public boolean requiresScriptedObjectRefresh(ScriptSource scriptSource) { + return scriptSource.isModified(); + } + + + @Override + public String toString() { + return "StandardScriptFactory: script source locator [" + this.scriptSourceLocator + "]"; + } + +} diff --git a/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptUtils.java b/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptUtils.java new file mode 100644 index 0000000..50bec8d --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptUtils.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.support; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.script.Bindings; +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineFactory; +import javax.script.ScriptEngineManager; +import javax.script.SimpleBindings; + +/** + * Common operations for dealing with a JSR-223 {@link ScriptEngine}. + * + * @author Juergen Hoeller + * @since 4.2.2 + */ +public abstract class StandardScriptUtils { + + /** + * Retrieve a {@link ScriptEngine} from the given {@link ScriptEngineManager} + * by name, delegating to {@link ScriptEngineManager#getEngineByName} but + * throwing a descriptive exception if not found or if initialization failed. + * @param scriptEngineManager the ScriptEngineManager to use + * @param engineName the name of the engine + * @return a corresponding ScriptEngine (never {@code null}) + * @throws IllegalArgumentException if no matching engine has been found + * @throws IllegalStateException if the desired engine failed to initialize + */ + public static ScriptEngine retrieveEngineByName(ScriptEngineManager scriptEngineManager, String engineName) { + ScriptEngine engine = scriptEngineManager.getEngineByName(engineName); + if (engine == null) { + Set engineNames = new LinkedHashSet<>(); + for (ScriptEngineFactory engineFactory : scriptEngineManager.getEngineFactories()) { + List factoryNames = engineFactory.getNames(); + if (factoryNames.contains(engineName)) { + // Special case: getEngineByName returned null but engine is present... + // Let's assume it failed to initialize (which ScriptEngineManager silently swallows). + // If it happens to initialize fine now, alright, but we really expect an exception. + try { + engine = engineFactory.getScriptEngine(); + engine.setBindings(scriptEngineManager.getBindings(), ScriptContext.GLOBAL_SCOPE); + } + catch (Throwable ex) { + throw new IllegalStateException("Script engine with name '" + engineName + + "' failed to initialize", ex); + } + } + engineNames.addAll(factoryNames); + } + throw new IllegalArgumentException("Script engine with name '" + engineName + + "' not found; registered engine names: " + engineNames); + } + return engine; + } + + static Bindings getBindings(Map bindings) { + return (bindings instanceof Bindings ? (Bindings) bindings : new SimpleBindings(bindings)); + } + +} diff --git a/spring-context/src/main/java/org/springframework/scripting/support/StaticScriptSource.java b/spring-context/src/main/java/org/springframework/scripting/support/StaticScriptSource.java new file mode 100644 index 0000000..c1163e9 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scripting/support/StaticScriptSource.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.support; + +import org.springframework.lang.Nullable; +import org.springframework.scripting.ScriptSource; +import org.springframework.util.Assert; + +/** + * Static implementation of the + * {@link org.springframework.scripting.ScriptSource} interface, + * encapsulating a given String that contains the script source text. + * Supports programmatic updates of the script String. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +public class StaticScriptSource implements ScriptSource { + + private String script = ""; + + private boolean modified; + + @Nullable + private String className; + + + /** + * Create a new StaticScriptSource for the given script. + * @param script the script String + */ + public StaticScriptSource(String script) { + setScript(script); + } + + /** + * Create a new StaticScriptSource for the given script. + * @param script the script String + * @param className the suggested class name for the script + * (may be {@code null}) + */ + public StaticScriptSource(String script, @Nullable String className) { + setScript(script); + this.className = className; + } + + /** + * Set a fresh script String, overriding the previous script. + * @param script the script String + */ + public synchronized void setScript(String script) { + Assert.hasText(script, "Script must not be empty"); + this.modified = !script.equals(this.script); + this.script = script; + } + + + @Override + public synchronized String getScriptAsString() { + this.modified = false; + return this.script; + } + + @Override + public synchronized boolean isModified() { + return this.modified; + } + + @Override + @Nullable + public String suggestedClassName() { + return this.className; + } + + + @Override + public String toString() { + return "static script" + (this.className != null ? " [" + this.className + "]" : ""); + } + +} diff --git a/spring-context/src/main/java/org/springframework/scripting/support/package-info.java b/spring-context/src/main/java/org/springframework/scripting/support/package-info.java new file mode 100644 index 0000000..dc8b765 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scripting/support/package-info.java @@ -0,0 +1,11 @@ +/** + * Support classes for Spring's scripting package. + * Provides a ScriptFactoryPostProcessor for turning ScriptFactory + * definitions into scripted objects. + */ +@NonNullApi +@NonNullFields +package org.springframework.scripting.support; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/stereotype/Component.java b/spring-context/src/main/java/org/springframework/stereotype/Component.java new file mode 100644 index 0000000..a6e09ed --- /dev/null +++ b/spring-context/src/main/java/org/springframework/stereotype/Component.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.stereotype; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that an annotated class is a "component". + * Such classes are considered as candidates for auto-detection + * when using annotation-based configuration and classpath scanning. + * + *

    Other class-level annotations may be considered as identifying + * a component as well, typically a special kind of component: + * e.g. the {@link Repository @Repository} annotation or AspectJ's + * {@link org.aspectj.lang.annotation.Aspect @Aspect} annotation. + * + * @author Mark Fisher + * @since 2.5 + * @see Repository + * @see Service + * @see Controller + * @see org.springframework.context.annotation.ClassPathBeanDefinitionScanner + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Indexed +public @interface Component { + + /** + * The value may indicate a suggestion for a logical component name, + * to be turned into a Spring bean in case of an autodetected component. + * @return the suggested component name, if any (or empty String otherwise) + */ + String value() default ""; + +} diff --git a/spring-context/src/main/java/org/springframework/stereotype/Controller.java b/spring-context/src/main/java/org/springframework/stereotype/Controller.java new file mode 100644 index 0000000..ef4167c --- /dev/null +++ b/spring-context/src/main/java/org/springframework/stereotype/Controller.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.stereotype; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * Indicates that an annotated class is a "Controller" (e.g. a web controller). + * + *

    This annotation serves as a specialization of {@link Component @Component}, + * allowing for implementation classes to be autodetected through classpath scanning. + * It is typically used in combination with annotated handler methods based on the + * {@link org.springframework.web.bind.annotation.RequestMapping} annotation. + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @since 2.5 + * @see Component + * @see org.springframework.web.bind.annotation.RequestMapping + * @see org.springframework.context.annotation.ClassPathBeanDefinitionScanner + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface Controller { + + /** + * The value may indicate a suggestion for a logical component name, + * to be turned into a Spring bean in case of an autodetected component. + * @return the suggested component name, if any (or empty String otherwise) + */ + @AliasFor(annotation = Component.class) + String value() default ""; + +} diff --git a/spring-context/src/main/java/org/springframework/stereotype/Indexed.java b/spring-context/src/main/java/org/springframework/stereotype/Indexed.java new file mode 100644 index 0000000..278f063 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/stereotype/Indexed.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.stereotype; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicate that the annotated element represents a stereotype for the index. + * + *

    The {@code CandidateComponentsIndex} is an alternative to classpath + * scanning that uses a metadata file generated at compilation time. The + * index allows retrieving the candidate components (i.e. fully qualified + * name) based on a stereotype. This annotation instructs the generator to + * index the element on which the annotated element is present or if it + * implements or extends from the annotated element. The stereotype is the + * fully qualified name of the annotated element. + * + *

    Consider the default {@link Component} annotation that is meta-annotated + * with this annotation. If a component is annotated with {@link Component}, + * an entry for that component will be added to the index using the + * {@code org.springframework.stereotype.Component} stereotype. + * + *

    This annotation is also honored on meta-annotations. Consider this + * custom annotation: + *

    + * package com.example;
    + *
    + * @Target(ElementType.TYPE)
    + * @Retention(RetentionPolicy.RUNTIME)
    + * @Documented
    + * @Indexed
    + * @Service
    + * public @interface PrivilegedService { ... }
    + * 
    + * + * If the above annotation is present on a type, it will be indexed with two + * stereotypes: {@code org.springframework.stereotype.Component} and + * {@code com.example.PrivilegedService}. While {@link Service} isn't directly + * annotated with {@code Indexed}, it is meta-annotated with {@link Component}. + * + *

    It is also possible to index all implementations of a certain interface or + * all the subclasses of a given class by adding {@code @Indexed} on it. + * + * Consider this base interface: + *

    + * package com.example;
    + *
    + * @Indexed
    + * public interface AdminService { ... }
    + * 
    + * + * Now, consider an implementation of this {@code AdminService} somewhere: + *
    + * package com.example.foo;
    + *
    + * import com.example.AdminService;
    + *
    + * public class ConfigurationAdminService implements AdminService { ... }
    + * 
    + * + * Because this class implements an interface that is indexed, it will be + * automatically included with the {@code com.example.AdminService} stereotype. + * If there are more {@code @Indexed} interfaces and/or superclasses in the + * hierarchy, the class will map to all their stereotypes. + * + * @author Stephane Nicoll + * @since 5.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Indexed { +} diff --git a/spring-context/src/main/java/org/springframework/stereotype/Repository.java b/spring-context/src/main/java/org/springframework/stereotype/Repository.java new file mode 100644 index 0000000..97cb135 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/stereotype/Repository.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.stereotype; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * Indicates that an annotated class is a "Repository", originally defined by + * Domain-Driven Design (Evans, 2003) as "a mechanism for encapsulating storage, + * retrieval, and search behavior which emulates a collection of objects". + * + *

    Teams implementing traditional Java EE patterns such as "Data Access Object" + * may also apply this stereotype to DAO classes, though care should be taken to + * understand the distinction between Data Access Object and DDD-style repositories + * before doing so. This annotation is a general-purpose stereotype and individual teams + * may narrow their semantics and use as appropriate. + * + *

    A class thus annotated is eligible for Spring + * {@link org.springframework.dao.DataAccessException DataAccessException} translation + * when used in conjunction with a {@link + * org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor + * PersistenceExceptionTranslationPostProcessor}. The annotated class is also clarified as + * to its role in the overall application architecture for the purpose of tooling, + * aspects, etc. + * + *

    As of Spring 2.5, this annotation also serves as a specialization of + * {@link Component @Component}, allowing for implementation classes to be autodetected + * through classpath scanning. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + * @see Component + * @see Service + * @see org.springframework.dao.DataAccessException + * @see org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface Repository { + + /** + * The value may indicate a suggestion for a logical component name, + * to be turned into a Spring bean in case of an autodetected component. + * @return the suggested component name, if any (or empty String otherwise) + */ + @AliasFor(annotation = Component.class) + String value() default ""; + +} diff --git a/spring-context/src/main/java/org/springframework/stereotype/Service.java b/spring-context/src/main/java/org/springframework/stereotype/Service.java new file mode 100644 index 0000000..18e61c5 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/stereotype/Service.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.stereotype; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * Indicates that an annotated class is a "Service", originally defined by Domain-Driven + * Design (Evans, 2003) as "an operation offered as an interface that stands alone in the + * model, with no encapsulated state." + * + *

    May also indicate that a class is a "Business Service Facade" (in the Core J2EE + * patterns sense), or something similar. This annotation is a general-purpose stereotype + * and individual teams may narrow their semantics and use as appropriate. + * + *

    This annotation serves as a specialization of {@link Component @Component}, + * allowing for implementation classes to be autodetected through classpath scanning. + * + * @author Juergen Hoeller + * @since 2.5 + * @see Component + * @see Repository + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface Service { + + /** + * The value may indicate a suggestion for a logical component name, + * to be turned into a Spring bean in case of an autodetected component. + * @return the suggested component name, if any (or empty String otherwise) + */ + @AliasFor(annotation = Component.class) + String value() default ""; + +} diff --git a/spring-context/src/main/java/org/springframework/stereotype/package-info.java b/spring-context/src/main/java/org/springframework/stereotype/package-info.java new file mode 100644 index 0000000..829c45e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/stereotype/package-info.java @@ -0,0 +1,12 @@ +/** + * Annotations denoting the roles of types or methods in the overall architecture + * (at a conceptual, rather than implementation, level). + * + *

    Intended for use by tools and aspects (making an ideal target for pointcuts). + */ +@NonNullApi +@NonNullFields +package org.springframework.stereotype; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/ui/ConcurrentModel.java b/spring-context/src/main/java/org/springframework/ui/ConcurrentModel.java new file mode 100644 index 0000000..d5c2fa4 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/ui/ConcurrentModel.java @@ -0,0 +1,181 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ui; + +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.core.Conventions; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Implementation of the {@link Model} interface based on a {@link ConcurrentHashMap} + * for use in concurrent scenarios. + * + *

    Exposed to handler methods by Spring WebFlux, typically via a declaration of the + * {@link Model} interface. There is typically no need to create it within user code. + * If necessary a handler method can return a regular {@code java.util.Map}, + * likely a {@code java.util.ConcurrentMap}, for a pre-determined model. + * + * @author Rossen Stoyanchev + * @since 5.0 + */ +@SuppressWarnings("serial") +public class ConcurrentModel extends ConcurrentHashMap implements Model { + + /** + * Construct a new, empty {@code ConcurrentModel}. + */ + public ConcurrentModel() { + } + + /** + * Construct a new {@code ModelMap} containing the supplied attribute + * under the supplied name. + * @see #addAttribute(String, Object) + */ + public ConcurrentModel(String attributeName, Object attributeValue) { + addAttribute(attributeName, attributeValue); + } + + /** + * Construct a new {@code ModelMap} containing the supplied attribute. + * Uses attribute name generation to generate the key for the supplied model + * object. + * @see #addAttribute(Object) + */ + public ConcurrentModel(Object attributeValue) { + addAttribute(attributeValue); + } + + + @Override + public Object put(String key, Object value) { + if (value != null) { + return super.put(key, value); + } + else { + return remove(key); + } + } + + @Override + public void putAll(Map map) { + for (Map.Entry entry : map.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + /** + * Add the supplied attribute under the supplied name. + * @param attributeName the name of the model attribute (never {@code null}) + * @param attributeValue the model attribute value (ignored if {@code null}, + * just removing an existing entry if any) + */ + @Override + public ConcurrentModel addAttribute(String attributeName, @Nullable Object attributeValue) { + Assert.notNull(attributeName, "Model attribute name must not be null"); + put(attributeName, attributeValue); + return this; + } + + /** + * Add the supplied attribute to this {@code Map} using a + * {@link org.springframework.core.Conventions#getVariableName generated name}. + *

    Note: Empty {@link Collection Collections} are not added to + * the model when using this method because we cannot correctly determine + * the true convention name. View code should check for {@code null} rather + * than for empty collections as is already done by JSTL tags. + * @param attributeValue the model attribute value (never {@code null}) + */ + @Override + public ConcurrentModel addAttribute(Object attributeValue) { + Assert.notNull(attributeValue, "Model attribute value must not be null"); + if (attributeValue instanceof Collection && ((Collection) attributeValue).isEmpty()) { + return this; + } + return addAttribute(Conventions.getVariableName(attributeValue), attributeValue); + } + + /** + * Copy all attributes in the supplied {@code Collection} into this + * {@code Map}, using attribute name generation for each element. + * @see #addAttribute(Object) + */ + @Override + public ConcurrentModel addAllAttributes(@Nullable Collection attributeValues) { + if (attributeValues != null) { + for (Object attributeValue : attributeValues) { + addAttribute(attributeValue); + } + } + return this; + } + + /** + * Copy all attributes in the supplied {@code Map} into this {@code Map}. + * @see #addAttribute(String, Object) + */ + @Override + public ConcurrentModel addAllAttributes(@Nullable Map attributes) { + if (attributes != null) { + putAll(attributes); + } + return this; + } + + /** + * Copy all attributes in the supplied {@code Map} into this {@code Map}, + * with existing objects of the same name taking precedence (i.e. not getting + * replaced). + */ + @Override + public ConcurrentModel mergeAttributes(@Nullable Map attributes) { + if (attributes != null) { + attributes.forEach((key, value) -> { + if (!containsKey(key)) { + put(key, value); + } + }); + } + return this; + } + + /** + * Does this model contain an attribute of the given name? + * @param attributeName the name of the model attribute (never {@code null}) + * @return whether this model contains a corresponding attribute + */ + @Override + public boolean containsAttribute(String attributeName) { + return containsKey(attributeName); + } + + @Override + @Nullable + public Object getAttribute(String attributeName) { + return get(attributeName); + } + + @Override + public Map asMap() { + return this; + } + +} diff --git a/spring-context/src/main/java/org/springframework/ui/ExtendedModelMap.java b/spring-context/src/main/java/org/springframework/ui/ExtendedModelMap.java new file mode 100644 index 0000000..b37f390 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/ui/ExtendedModelMap.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ui; + +import java.util.Collection; +import java.util.Map; + +import org.springframework.lang.Nullable; + +/** + * Subclass of {@link ModelMap} that implements the {@link Model} interface. + * Java 5 specific like the {@code Model} interface itself. + * + *

    This is an implementation class exposed to handler methods by Spring MVC, typically via + * a declaration of the {@link org.springframework.ui.Model} interface. There is no need to + * build it within user code; a plain {@link org.springframework.ui.ModelMap} or even a just + * a regular {@link Map} with String keys will be good enough to return a user model. + * + * @author Juergen Hoeller + * @since 2.5.1 + */ +@SuppressWarnings("serial") +public class ExtendedModelMap extends ModelMap implements Model { + + @Override + public ExtendedModelMap addAttribute(String attributeName, @Nullable Object attributeValue) { + super.addAttribute(attributeName, attributeValue); + return this; + } + + @Override + public ExtendedModelMap addAttribute(Object attributeValue) { + super.addAttribute(attributeValue); + return this; + } + + @Override + public ExtendedModelMap addAllAttributes(@Nullable Collection attributeValues) { + super.addAllAttributes(attributeValues); + return this; + } + + @Override + public ExtendedModelMap addAllAttributes(@Nullable Map attributes) { + super.addAllAttributes(attributes); + return this; + } + + @Override + public ExtendedModelMap mergeAttributes(@Nullable Map attributes) { + super.mergeAttributes(attributes); + return this; + } + + @Override + public Map asMap() { + return this; + } + +} diff --git a/spring-context/src/main/java/org/springframework/ui/Model.java b/spring-context/src/main/java/org/springframework/ui/Model.java new file mode 100644 index 0000000..8e586cc --- /dev/null +++ b/spring-context/src/main/java/org/springframework/ui/Model.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ui; + +import java.util.Collection; +import java.util.Map; + +import org.springframework.lang.Nullable; + +/** + * Java-5-specific interface that defines a holder for model attributes. + * Primarily designed for adding attributes to the model. + * Allows for accessing the overall model as a {@code java.util.Map}. + * + * @author Juergen Hoeller + * @since 2.5.1 + */ +public interface Model { + + /** + * Add the supplied attribute under the supplied name. + * @param attributeName the name of the model attribute (never {@code null}) + * @param attributeValue the model attribute value (can be {@code null}) + */ + Model addAttribute(String attributeName, @Nullable Object attributeValue); + + /** + * Add the supplied attribute to this {@code Map} using a + * {@link org.springframework.core.Conventions#getVariableName generated name}. + *

    Note: Empty {@link java.util.Collection Collections} are not added to + * the model when using this method because we cannot correctly determine + * the true convention name. View code should check for {@code null} rather + * than for empty collections as is already done by JSTL tags. + * @param attributeValue the model attribute value (never {@code null}) + */ + Model addAttribute(Object attributeValue); + + /** + * Copy all attributes in the supplied {@code Collection} into this + * {@code Map}, using attribute name generation for each element. + * @see #addAttribute(Object) + */ + Model addAllAttributes(Collection attributeValues); + + /** + * Copy all attributes in the supplied {@code Map} into this {@code Map}. + * @see #addAttribute(String, Object) + */ + Model addAllAttributes(Map attributes); + + /** + * Copy all attributes in the supplied {@code Map} into this {@code Map}, + * with existing objects of the same name taking precedence (i.e. not getting + * replaced). + */ + Model mergeAttributes(Map attributes); + + /** + * Does this model contain an attribute of the given name? + * @param attributeName the name of the model attribute (never {@code null}) + * @return whether this model contains a corresponding attribute + */ + boolean containsAttribute(String attributeName); + + /** + * Return the attribute value for the given name, if any. + * @param attributeName the name of the model attribute (never {@code null}) + * @return the corresponding attribute value, or {@code null} if none + * @since 5.2 + */ + @Nullable + Object getAttribute(String attributeName); + + /** + * Return the current set of model attributes as a Map. + */ + Map asMap(); + +} diff --git a/spring-context/src/main/java/org/springframework/ui/ModelMap.java b/spring-context/src/main/java/org/springframework/ui/ModelMap.java new file mode 100644 index 0000000..6a1e438 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/ui/ModelMap.java @@ -0,0 +1,158 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ui; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.core.Conventions; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Implementation of {@link java.util.Map} for use when building model data for use + * with UI tools. Supports chained calls and generation of model attribute names. + * + *

    This class serves as generic model holder for Servlet MVC but is not tied to it. + * Check out the {@link Model} interface for an interface variant. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + * @see Conventions#getVariableName + * @see org.springframework.web.servlet.ModelAndView + */ +@SuppressWarnings("serial") +public class ModelMap extends LinkedHashMap { + + /** + * Construct a new, empty {@code ModelMap}. + */ + public ModelMap() { + } + + /** + * Construct a new {@code ModelMap} containing the supplied attribute + * under the supplied name. + * @see #addAttribute(String, Object) + */ + public ModelMap(String attributeName, @Nullable Object attributeValue) { + addAttribute(attributeName, attributeValue); + } + + /** + * Construct a new {@code ModelMap} containing the supplied attribute. + * Uses attribute name generation to generate the key for the supplied model + * object. + * @see #addAttribute(Object) + */ + public ModelMap(Object attributeValue) { + addAttribute(attributeValue); + } + + + /** + * Add the supplied attribute under the supplied name. + * @param attributeName the name of the model attribute (never {@code null}) + * @param attributeValue the model attribute value (can be {@code null}) + */ + public ModelMap addAttribute(String attributeName, @Nullable Object attributeValue) { + Assert.notNull(attributeName, "Model attribute name must not be null"); + put(attributeName, attributeValue); + return this; + } + + /** + * Add the supplied attribute to this {@code Map} using a + * {@link org.springframework.core.Conventions#getVariableName generated name}. + *

    Note: Empty {@link Collection Collections} are not added to + * the model when using this method because we cannot correctly determine + * the true convention name. View code should check for {@code null} rather + * than for empty collections as is already done by JSTL tags. + * @param attributeValue the model attribute value (never {@code null}) + */ + public ModelMap addAttribute(Object attributeValue) { + Assert.notNull(attributeValue, "Model object must not be null"); + if (attributeValue instanceof Collection && ((Collection) attributeValue).isEmpty()) { + return this; + } + return addAttribute(Conventions.getVariableName(attributeValue), attributeValue); + } + + /** + * Copy all attributes in the supplied {@code Collection} into this + * {@code Map}, using attribute name generation for each element. + * @see #addAttribute(Object) + */ + public ModelMap addAllAttributes(@Nullable Collection attributeValues) { + if (attributeValues != null) { + for (Object attributeValue : attributeValues) { + addAttribute(attributeValue); + } + } + return this; + } + + /** + * Copy all attributes in the supplied {@code Map} into this {@code Map}. + * @see #addAttribute(String, Object) + */ + public ModelMap addAllAttributes(@Nullable Map attributes) { + if (attributes != null) { + putAll(attributes); + } + return this; + } + + /** + * Copy all attributes in the supplied {@code Map} into this {@code Map}, + * with existing objects of the same name taking precedence (i.e. not getting + * replaced). + */ + public ModelMap mergeAttributes(@Nullable Map attributes) { + if (attributes != null) { + attributes.forEach((key, value) -> { + if (!containsKey(key)) { + put(key, value); + } + }); + } + return this; + } + + /** + * Does this model contain an attribute of the given name? + * @param attributeName the name of the model attribute (never {@code null}) + * @return whether this model contains a corresponding attribute + */ + public boolean containsAttribute(String attributeName) { + return containsKey(attributeName); + } + + /** + * Return the attribute value for the given name, if any. + * @param attributeName the name of the model attribute (never {@code null}) + * @return the corresponding attribute value, or {@code null} if none + * @since 5.2 + */ + @Nullable + public Object getAttribute(String attributeName) { + return get(attributeName); + } + +} diff --git a/spring-context/src/main/java/org/springframework/ui/context/HierarchicalThemeSource.java b/spring-context/src/main/java/org/springframework/ui/context/HierarchicalThemeSource.java new file mode 100644 index 0000000..8c9c295 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/ui/context/HierarchicalThemeSource.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ui.context; + +import org.springframework.lang.Nullable; + +/** + * Sub-interface of ThemeSource to be implemented by objects that + * can resolve theme messages hierarchically. + * + * @author Jean-Pierre Pawlak + * @author Juergen Hoeller + */ +public interface HierarchicalThemeSource extends ThemeSource { + + /** + * Set the parent that will be used to try to resolve theme messages + * that this object can't resolve. + * @param parent the parent ThemeSource that will be used to + * resolve messages that this object can't resolve. + * May be {@code null}, in which case no further resolution is possible. + */ + void setParentThemeSource(@Nullable ThemeSource parent); + + /** + * Return the parent of this ThemeSource, or {@code null} if none. + */ + @Nullable + ThemeSource getParentThemeSource(); + +} diff --git a/spring-context/src/main/java/org/springframework/ui/context/Theme.java b/spring-context/src/main/java/org/springframework/ui/context/Theme.java new file mode 100644 index 0000000..b2b5e4f --- /dev/null +++ b/spring-context/src/main/java/org/springframework/ui/context/Theme.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ui.context; + +import org.springframework.context.MessageSource; + +/** + * A Theme can resolve theme-specific messages, codes, file paths, etcetera + * (e.g. CSS and image files in a web environment). + * The exposed {@link org.springframework.context.MessageSource} supports + * theme-specific parameterization and internationalization. + * + * @author Juergen Hoeller + * @since 17.06.2003 + * @see ThemeSource + * @see org.springframework.web.servlet.ThemeResolver + */ +public interface Theme { + + /** + * Return the name of the theme. + * @return the name of the theme (never {@code null}) + */ + String getName(); + + /** + * Return the specific MessageSource that resolves messages + * with respect to this theme. + * @return the theme-specific MessageSource (never {@code null}) + */ + MessageSource getMessageSource(); + +} diff --git a/spring-context/src/main/java/org/springframework/ui/context/ThemeSource.java b/spring-context/src/main/java/org/springframework/ui/context/ThemeSource.java new file mode 100644 index 0000000..9e72ad7 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/ui/context/ThemeSource.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ui.context; + +import org.springframework.lang.Nullable; + +/** + * Interface to be implemented by objects that can resolve {@link Theme Themes}. + * This enables parameterization and internationalization of messages + * for a given 'theme'. + * + * @author Jean-Pierre Pawlak + * @author Juergen Hoeller + * @see Theme + */ +public interface ThemeSource { + + /** + * Return the Theme instance for the given theme name. + *

    The returned Theme will resolve theme-specific messages, codes, + * file paths, etc (e.g. CSS and image files in a web environment). + * @param themeName the name of the theme + * @return the corresponding Theme, or {@code null} if none defined. + * Note that, by convention, a ThemeSource should at least be able to + * return a default Theme for the default theme name "theme" but may also + * return default Themes for other theme names. + * @see org.springframework.web.servlet.theme.AbstractThemeResolver#ORIGINAL_DEFAULT_THEME_NAME + */ + @Nullable + Theme getTheme(String themeName); + +} diff --git a/spring-context/src/main/java/org/springframework/ui/context/package-info.java b/spring-context/src/main/java/org/springframework/ui/context/package-info.java new file mode 100644 index 0000000..cedb3f0 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/ui/context/package-info.java @@ -0,0 +1,10 @@ +/** + * Contains classes defining the application context subinterface + * for UI applications. The theme feature is added here. + */ +@NonNullApi +@NonNullFields +package org.springframework.ui.context; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/ui/context/support/DelegatingThemeSource.java b/spring-context/src/main/java/org/springframework/ui/context/support/DelegatingThemeSource.java new file mode 100644 index 0000000..3f2a5f3 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/ui/context/support/DelegatingThemeSource.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ui.context.support; + +import org.springframework.lang.Nullable; +import org.springframework.ui.context.HierarchicalThemeSource; +import org.springframework.ui.context.Theme; +import org.springframework.ui.context.ThemeSource; + +/** + * Empty ThemeSource that delegates all calls to the parent ThemeSource. + * If no parent is available, it simply won't resolve any theme. + * + *

    Used as placeholder by UiApplicationContextUtils, if a context doesn't + * define its own ThemeSource. Not intended for direct use in applications. + * + * @author Juergen Hoeller + * @since 1.2.4 + * @see UiApplicationContextUtils + */ +public class DelegatingThemeSource implements HierarchicalThemeSource { + + @Nullable + private ThemeSource parentThemeSource; + + + @Override + public void setParentThemeSource(@Nullable ThemeSource parentThemeSource) { + this.parentThemeSource = parentThemeSource; + } + + @Override + @Nullable + public ThemeSource getParentThemeSource() { + return this.parentThemeSource; + } + + + @Override + @Nullable + public Theme getTheme(String themeName) { + if (this.parentThemeSource != null) { + return this.parentThemeSource.getTheme(themeName); + } + else { + return null; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/ui/context/support/ResourceBundleThemeSource.java b/spring-context/src/main/java/org/springframework/ui/context/support/ResourceBundleThemeSource.java new file mode 100644 index 0000000..0e5ba7e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/ui/context/support/ResourceBundleThemeSource.java @@ -0,0 +1,203 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ui.context.support; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.context.HierarchicalMessageSource; +import org.springframework.context.MessageSource; +import org.springframework.context.support.ResourceBundleMessageSource; +import org.springframework.lang.Nullable; +import org.springframework.ui.context.HierarchicalThemeSource; +import org.springframework.ui.context.Theme; +import org.springframework.ui.context.ThemeSource; + +/** + * {@link ThemeSource} implementation that looks up an individual + * {@link java.util.ResourceBundle} per theme. The theme name gets + * interpreted as ResourceBundle basename, supporting a common + * basename prefix for all themes. + * + * @author Jean-Pierre Pawlak + * @author Juergen Hoeller + * @see #setBasenamePrefix + * @see java.util.ResourceBundle + * @see org.springframework.context.support.ResourceBundleMessageSource + */ +public class ResourceBundleThemeSource implements HierarchicalThemeSource, BeanClassLoaderAware { + + protected final Log logger = LogFactory.getLog(getClass()); + + @Nullable + private ThemeSource parentThemeSource; + + private String basenamePrefix = ""; + + @Nullable + private String defaultEncoding; + + @Nullable + private Boolean fallbackToSystemLocale; + + @Nullable + private ClassLoader beanClassLoader; + + /** Map from theme name to Theme instance. */ + private final Map themeCache = new ConcurrentHashMap<>(); + + + @Override + public void setParentThemeSource(@Nullable ThemeSource parent) { + this.parentThemeSource = parent; + + // Update existing Theme objects. + // Usually there shouldn't be any at the time of this call. + synchronized (this.themeCache) { + for (Theme theme : this.themeCache.values()) { + initParent(theme); + } + } + } + + @Override + @Nullable + public ThemeSource getParentThemeSource() { + return this.parentThemeSource; + } + + /** + * Set the prefix that gets applied to the ResourceBundle basenames, + * i.e. the theme names. + * E.g.: basenamePrefix="test.", themeName="theme" -> basename="test.theme". + *

    Note that ResourceBundle names are effectively classpath locations: As a + * consequence, the JDK's standard ResourceBundle treats dots as package separators. + * This means that "test.theme" is effectively equivalent to "test/theme", + * just like it is for programmatic {@code java.util.ResourceBundle} usage. + * @see java.util.ResourceBundle#getBundle(String) + */ + public void setBasenamePrefix(@Nullable String basenamePrefix) { + this.basenamePrefix = (basenamePrefix != null ? basenamePrefix : ""); + } + + /** + * Set the default charset to use for parsing resource bundle files. + *

    {@link ResourceBundleMessageSource}'s default is the + * {@code java.util.ResourceBundle} default encoding: ISO-8859-1. + * @since 4.2 + * @see ResourceBundleMessageSource#setDefaultEncoding + */ + public void setDefaultEncoding(@Nullable String defaultEncoding) { + this.defaultEncoding = defaultEncoding; + } + + /** + * Set whether to fall back to the system Locale if no files for a + * specific Locale have been found. + *

    {@link ResourceBundleMessageSource}'s default is "true". + * @since 4.2 + * @see ResourceBundleMessageSource#setFallbackToSystemLocale + */ + public void setFallbackToSystemLocale(boolean fallbackToSystemLocale) { + this.fallbackToSystemLocale = fallbackToSystemLocale; + } + + @Override + public void setBeanClassLoader(@Nullable ClassLoader beanClassLoader) { + this.beanClassLoader = beanClassLoader; + } + + + /** + * This implementation returns a SimpleTheme instance, holding a + * ResourceBundle-based MessageSource whose basename corresponds to + * the given theme name (prefixed by the configured "basenamePrefix"). + *

    SimpleTheme instances are cached per theme name. Use a reloadable + * MessageSource if themes should reflect changes to the underlying files. + * @see #setBasenamePrefix + * @see #createMessageSource + */ + @Override + @Nullable + public Theme getTheme(String themeName) { + Theme theme = this.themeCache.get(themeName); + if (theme == null) { + synchronized (this.themeCache) { + theme = this.themeCache.get(themeName); + if (theme == null) { + String basename = this.basenamePrefix + themeName; + MessageSource messageSource = createMessageSource(basename); + theme = new SimpleTheme(themeName, messageSource); + initParent(theme); + this.themeCache.put(themeName, theme); + if (logger.isDebugEnabled()) { + logger.debug("Theme created: name '" + themeName + "', basename [" + basename + "]"); + } + } + } + } + return theme; + } + + /** + * Create a MessageSource for the given basename, + * to be used as MessageSource for the corresponding theme. + *

    Default implementation creates a ResourceBundleMessageSource. + * for the given basename. A subclass could create a specifically + * configured ReloadableResourceBundleMessageSource, for example. + * @param basename the basename to create a MessageSource for + * @return the MessageSource + * @see org.springframework.context.support.ResourceBundleMessageSource + * @see org.springframework.context.support.ReloadableResourceBundleMessageSource + */ + protected MessageSource createMessageSource(String basename) { + ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); + messageSource.setBasename(basename); + if (this.defaultEncoding != null) { + messageSource.setDefaultEncoding(this.defaultEncoding); + } + if (this.fallbackToSystemLocale != null) { + messageSource.setFallbackToSystemLocale(this.fallbackToSystemLocale); + } + if (this.beanClassLoader != null) { + messageSource.setBeanClassLoader(this.beanClassLoader); + } + return messageSource; + } + + /** + * Initialize the MessageSource of the given theme with the + * one from the corresponding parent of this ThemeSource. + * @param theme the Theme to (re-)initialize + */ + protected void initParent(Theme theme) { + if (theme.getMessageSource() instanceof HierarchicalMessageSource) { + HierarchicalMessageSource messageSource = (HierarchicalMessageSource) theme.getMessageSource(); + if (getParentThemeSource() != null && messageSource.getParentMessageSource() == null) { + Theme parentTheme = getParentThemeSource().getTheme(theme.getName()); + if (parentTheme != null) { + messageSource.setParentMessageSource(parentTheme.getMessageSource()); + } + } + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/ui/context/support/SimpleTheme.java b/spring-context/src/main/java/org/springframework/ui/context/support/SimpleTheme.java new file mode 100644 index 0000000..35a75a6 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/ui/context/support/SimpleTheme.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ui.context.support; + +import org.springframework.context.MessageSource; +import org.springframework.ui.context.Theme; +import org.springframework.util.Assert; + +/** + * Default {@link Theme} implementation, wrapping a name and an + * underlying {@link org.springframework.context.MessageSource}. + * + * @author Juergen Hoeller + * @since 17.06.2003 + */ +public class SimpleTheme implements Theme { + + private final String name; + + private final MessageSource messageSource; + + + /** + * Create a SimpleTheme. + * @param name the name of the theme + * @param messageSource the MessageSource that resolves theme messages + */ + public SimpleTheme(String name, MessageSource messageSource) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(messageSource, "MessageSource must not be null"); + this.name = name; + this.messageSource = messageSource; + } + + + @Override + public final String getName() { + return this.name; + } + + @Override + public final MessageSource getMessageSource() { + return this.messageSource; + } + +} diff --git a/spring-context/src/main/java/org/springframework/ui/context/support/UiApplicationContextUtils.java b/spring-context/src/main/java/org/springframework/ui/context/support/UiApplicationContextUtils.java new file mode 100644 index 0000000..bd842c4 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/ui/context/support/UiApplicationContextUtils.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ui.context.support; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.ApplicationContext; +import org.springframework.ui.context.HierarchicalThemeSource; +import org.springframework.ui.context.ThemeSource; + +/** + * Utility class for UI application context implementations. + * Provides support for a special bean named "themeSource", + * of type {@link org.springframework.ui.context.ThemeSource}. + * + * @author Jean-Pierre Pawlak + * @author Juergen Hoeller + * @since 17.06.2003 + */ +public abstract class UiApplicationContextUtils { + + /** + * Name of the ThemeSource bean in the factory. + * If none is supplied, theme resolution is delegated to the parent. + * @see org.springframework.ui.context.ThemeSource + */ + public static final String THEME_SOURCE_BEAN_NAME = "themeSource"; + + + private static final Log logger = LogFactory.getLog(UiApplicationContextUtils.class); + + + /** + * Initialize the ThemeSource for the given application context, + * autodetecting a bean with the name "themeSource". If no such + * bean is found, a default (empty) ThemeSource will be used. + * @param context current application context + * @return the initialized theme source (will never be {@code null}) + * @see #THEME_SOURCE_BEAN_NAME + */ + public static ThemeSource initThemeSource(ApplicationContext context) { + if (context.containsLocalBean(THEME_SOURCE_BEAN_NAME)) { + ThemeSource themeSource = context.getBean(THEME_SOURCE_BEAN_NAME, ThemeSource.class); + // Make ThemeSource aware of parent ThemeSource. + if (context.getParent() instanceof ThemeSource && themeSource instanceof HierarchicalThemeSource) { + HierarchicalThemeSource hts = (HierarchicalThemeSource) themeSource; + if (hts.getParentThemeSource() == null) { + // Only set parent context as parent ThemeSource if no parent ThemeSource + // registered already. + hts.setParentThemeSource((ThemeSource) context.getParent()); + } + } + if (logger.isDebugEnabled()) { + logger.debug("Using ThemeSource [" + themeSource + "]"); + } + return themeSource; + } + else { + // Use default ThemeSource to be able to accept getTheme calls, either + // delegating to parent context's default or to local ResourceBundleThemeSource. + HierarchicalThemeSource themeSource = null; + if (context.getParent() instanceof ThemeSource) { + themeSource = new DelegatingThemeSource(); + themeSource.setParentThemeSource((ThemeSource) context.getParent()); + } + else { + themeSource = new ResourceBundleThemeSource(); + } + if (logger.isDebugEnabled()) { + logger.debug("Unable to locate ThemeSource with name '" + THEME_SOURCE_BEAN_NAME + + "': using default [" + themeSource + "]"); + } + return themeSource; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/ui/context/support/package-info.java b/spring-context/src/main/java/org/springframework/ui/context/support/package-info.java new file mode 100644 index 0000000..da2a5b2 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/ui/context/support/package-info.java @@ -0,0 +1,10 @@ +/** + * Classes supporting the org.springframework.ui.context package. + * Provides support classes for specialized UI contexts, e.g. for web UIs. + */ +@NonNullApi +@NonNullFields +package org.springframework.ui.context.support; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/ui/package-info.java b/spring-context/src/main/java/org/springframework/ui/package-info.java new file mode 100644 index 0000000..988880e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/ui/package-info.java @@ -0,0 +1,10 @@ +/** + * Generic support for UI layer concepts. + * Provides a generic ModelMap for model holding. + */ +@NonNullApi +@NonNullFields +package org.springframework.ui; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/validation/AbstractBindingResult.java b/spring-context/src/main/java/org/springframework/validation/AbstractBindingResult.java new file mode 100644 index 0000000..b8857b1 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/AbstractBindingResult.java @@ -0,0 +1,414 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation; + +import java.beans.PropertyEditor; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.beans.PropertyEditorRegistry; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Abstract implementation of the {@link BindingResult} interface and + * its super-interface {@link Errors}. Encapsulates common management of + * {@link ObjectError ObjectErrors} and {@link FieldError FieldErrors}. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @since 2.0 + * @see Errors + */ +@SuppressWarnings("serial") +public abstract class AbstractBindingResult extends AbstractErrors implements BindingResult, Serializable { + + private final String objectName; + + private MessageCodesResolver messageCodesResolver = new DefaultMessageCodesResolver(); + + private final List errors = new ArrayList<>(); + + private final Map> fieldTypes = new HashMap<>(); + + private final Map fieldValues = new HashMap<>(); + + private final Set suppressedFields = new HashSet<>(); + + + /** + * Create a new AbstractBindingResult instance. + * @param objectName the name of the target object + * @see DefaultMessageCodesResolver + */ + protected AbstractBindingResult(String objectName) { + this.objectName = objectName; + } + + + /** + * Set the strategy to use for resolving errors into message codes. + * Default is DefaultMessageCodesResolver. + * @see DefaultMessageCodesResolver + */ + public void setMessageCodesResolver(MessageCodesResolver messageCodesResolver) { + Assert.notNull(messageCodesResolver, "MessageCodesResolver must not be null"); + this.messageCodesResolver = messageCodesResolver; + } + + /** + * Return the strategy to use for resolving errors into message codes. + */ + public MessageCodesResolver getMessageCodesResolver() { + return this.messageCodesResolver; + } + + + //--------------------------------------------------------------------- + // Implementation of the Errors interface + //--------------------------------------------------------------------- + + @Override + public String getObjectName() { + return this.objectName; + } + + @Override + public void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage) { + addError(new ObjectError(getObjectName(), resolveMessageCodes(errorCode), errorArgs, defaultMessage)); + } + + @Override + public void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, + @Nullable String defaultMessage) { + + if (!StringUtils.hasLength(getNestedPath()) && !StringUtils.hasLength(field)) { + // We're at the top of the nested object hierarchy, + // so the present level is not a field but rather the top object. + // The best we can do is register a global error here... + reject(errorCode, errorArgs, defaultMessage); + return; + } + + String fixedField = fixedField(field); + Object newVal = getActualFieldValue(fixedField); + FieldError fe = new FieldError(getObjectName(), fixedField, newVal, false, + resolveMessageCodes(errorCode, field), errorArgs, defaultMessage); + addError(fe); + } + + @Override + public void addAllErrors(Errors errors) { + if (!errors.getObjectName().equals(getObjectName())) { + throw new IllegalArgumentException("Errors object needs to have same object name"); + } + this.errors.addAll(errors.getAllErrors()); + } + + @Override + public boolean hasErrors() { + return !this.errors.isEmpty(); + } + + @Override + public int getErrorCount() { + return this.errors.size(); + } + + @Override + public List getAllErrors() { + return Collections.unmodifiableList(this.errors); + } + + @Override + public List getGlobalErrors() { + List result = new ArrayList<>(); + for (ObjectError objectError : this.errors) { + if (!(objectError instanceof FieldError)) { + result.add(objectError); + } + } + return Collections.unmodifiableList(result); + } + + @Override + @Nullable + public ObjectError getGlobalError() { + for (ObjectError objectError : this.errors) { + if (!(objectError instanceof FieldError)) { + return objectError; + } + } + return null; + } + + @Override + public List getFieldErrors() { + List result = new ArrayList<>(); + for (ObjectError objectError : this.errors) { + if (objectError instanceof FieldError) { + result.add((FieldError) objectError); + } + } + return Collections.unmodifiableList(result); + } + + @Override + @Nullable + public FieldError getFieldError() { + for (ObjectError objectError : this.errors) { + if (objectError instanceof FieldError) { + return (FieldError) objectError; + } + } + return null; + } + + @Override + public List getFieldErrors(String field) { + List result = new ArrayList<>(); + String fixedField = fixedField(field); + for (ObjectError objectError : this.errors) { + if (objectError instanceof FieldError && isMatchingFieldError(fixedField, (FieldError) objectError)) { + result.add((FieldError) objectError); + } + } + return Collections.unmodifiableList(result); + } + + @Override + @Nullable + public FieldError getFieldError(String field) { + String fixedField = fixedField(field); + for (ObjectError objectError : this.errors) { + if (objectError instanceof FieldError) { + FieldError fieldError = (FieldError) objectError; + if (isMatchingFieldError(fixedField, fieldError)) { + return fieldError; + } + } + } + return null; + } + + @Override + @Nullable + public Object getFieldValue(String field) { + FieldError fieldError = getFieldError(field); + // Use rejected value in case of error, current field value otherwise. + if (fieldError != null) { + Object value = fieldError.getRejectedValue(); + // Do not apply formatting on binding failures like type mismatches. + return (fieldError.isBindingFailure() || getTarget() == null ? value : formatFieldValue(field, value)); + } + else if (getTarget() != null) { + Object value = getActualFieldValue(fixedField(field)); + return formatFieldValue(field, value); + } + else { + return this.fieldValues.get(field); + } + } + + /** + * This default implementation determines the type based on the actual + * field value, if any. Subclasses should override this to determine + * the type from a descriptor, even for {@code null} values. + * @see #getActualFieldValue + */ + @Override + @Nullable + public Class getFieldType(@Nullable String field) { + if (getTarget() != null) { + Object value = getActualFieldValue(fixedField(field)); + if (value != null) { + return value.getClass(); + } + } + return this.fieldTypes.get(field); + } + + + //--------------------------------------------------------------------- + // Implementation of BindingResult interface + //--------------------------------------------------------------------- + + /** + * Return a model Map for the obtained state, exposing an Errors + * instance as '{@link #MODEL_KEY_PREFIX MODEL_KEY_PREFIX} + objectName' + * and the object itself. + *

    Note that the Map is constructed every time you're calling this method. + * Adding things to the map and then re-calling this method will not work. + *

    The attributes in the model Map returned by this method are usually + * included in the ModelAndView for a form view that uses Spring's bind tag, + * which needs access to the Errors instance. + * @see #getObjectName + * @see #MODEL_KEY_PREFIX + */ + @Override + public Map getModel() { + Map model = new LinkedHashMap<>(2); + // Mapping from name to target object. + model.put(getObjectName(), getTarget()); + // Errors instance, even if no errors. + model.put(MODEL_KEY_PREFIX + getObjectName(), this); + return model; + } + + @Override + @Nullable + public Object getRawFieldValue(String field) { + return (getTarget() != null ? getActualFieldValue(fixedField(field)) : null); + } + + /** + * This implementation delegates to the + * {@link #getPropertyEditorRegistry() PropertyEditorRegistry}'s + * editor lookup facility, if available. + */ + @Override + @Nullable + public PropertyEditor findEditor(@Nullable String field, @Nullable Class valueType) { + PropertyEditorRegistry editorRegistry = getPropertyEditorRegistry(); + if (editorRegistry != null) { + Class valueTypeToUse = valueType; + if (valueTypeToUse == null) { + valueTypeToUse = getFieldType(field); + } + return editorRegistry.findCustomEditor(valueTypeToUse, fixedField(field)); + } + else { + return null; + } + } + + /** + * This implementation returns {@code null}. + */ + @Override + @Nullable + public PropertyEditorRegistry getPropertyEditorRegistry() { + return null; + } + + @Override + public String[] resolveMessageCodes(String errorCode) { + return getMessageCodesResolver().resolveMessageCodes(errorCode, getObjectName()); + } + + @Override + public String[] resolveMessageCodes(String errorCode, @Nullable String field) { + return getMessageCodesResolver().resolveMessageCodes( + errorCode, getObjectName(), fixedField(field), getFieldType(field)); + } + + @Override + public void addError(ObjectError error) { + this.errors.add(error); + } + + @Override + public void recordFieldValue(String field, Class type, @Nullable Object value) { + this.fieldTypes.put(field, type); + this.fieldValues.put(field, value); + } + + /** + * Mark the specified disallowed field as suppressed. + *

    The data binder invokes this for each field value that was + * detected to target a disallowed field. + * @see DataBinder#setAllowedFields + */ + @Override + public void recordSuppressedField(String field) { + this.suppressedFields.add(field); + } + + /** + * Return the list of fields that were suppressed during the bind process. + *

    Can be used to determine whether any field values were targeting + * disallowed fields. + * @see DataBinder#setAllowedFields + */ + @Override + public String[] getSuppressedFields() { + return StringUtils.toStringArray(this.suppressedFields); + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof BindingResult)) { + return false; + } + BindingResult otherResult = (BindingResult) other; + return (getObjectName().equals(otherResult.getObjectName()) && + ObjectUtils.nullSafeEquals(getTarget(), otherResult.getTarget()) && + getAllErrors().equals(otherResult.getAllErrors())); + } + + @Override + public int hashCode() { + return getObjectName().hashCode(); + } + + + //--------------------------------------------------------------------- + // Template methods to be implemented/overridden by subclasses + //--------------------------------------------------------------------- + + /** + * Return the wrapped target object. + */ + @Override + @Nullable + public abstract Object getTarget(); + + /** + * Extract the actual field value for the given field. + * @param field the field to check + * @return the current value of the field + */ + @Nullable + protected abstract Object getActualFieldValue(String field); + + /** + * Format the given value for the specified field. + *

    The default implementation simply returns the field value as-is. + * @param field the field to check + * @param value the value of the field (either a rejected value + * other than from a binding error, or an actual field value) + * @return the formatted value + */ + @Nullable + protected Object formatFieldValue(String field, @Nullable Object value) { + return value; + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/AbstractErrors.java b/spring-context/src/main/java/org/springframework/validation/AbstractErrors.java new file mode 100644 index 0000000..9556dc3 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/AbstractErrors.java @@ -0,0 +1,253 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation; + +import java.io.Serializable; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.List; +import java.util.NoSuchElementException; + +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Abstract implementation of the {@link Errors} interface. Provides common + * access to evaluated errors; however, does not define concrete management + * of {@link ObjectError ObjectErrors} and {@link FieldError FieldErrors}. + * + * @author Juergen Hoeller + * @author Rossen Stoyanchev + * @since 2.5.3 + */ +@SuppressWarnings("serial") +public abstract class AbstractErrors implements Errors, Serializable { + + private String nestedPath = ""; + + private final Deque nestedPathStack = new ArrayDeque<>(); + + + @Override + public void setNestedPath(@Nullable String nestedPath) { + doSetNestedPath(nestedPath); + this.nestedPathStack.clear(); + } + + @Override + public String getNestedPath() { + return this.nestedPath; + } + + @Override + public void pushNestedPath(String subPath) { + this.nestedPathStack.push(getNestedPath()); + doSetNestedPath(getNestedPath() + subPath); + } + + @Override + public void popNestedPath() throws IllegalStateException { + try { + String formerNestedPath = this.nestedPathStack.pop(); + doSetNestedPath(formerNestedPath); + } + catch (NoSuchElementException ex) { + throw new IllegalStateException("Cannot pop nested path: no nested path on stack"); + } + } + + /** + * Actually set the nested path. + * Delegated to by setNestedPath and pushNestedPath. + */ + protected void doSetNestedPath(@Nullable String nestedPath) { + if (nestedPath == null) { + nestedPath = ""; + } + nestedPath = canonicalFieldName(nestedPath); + if (nestedPath.length() > 0 && !nestedPath.endsWith(Errors.NESTED_PATH_SEPARATOR)) { + nestedPath += Errors.NESTED_PATH_SEPARATOR; + } + this.nestedPath = nestedPath; + } + + /** + * Transform the given field into its full path, + * regarding the nested path of this instance. + */ + protected String fixedField(@Nullable String field) { + if (StringUtils.hasLength(field)) { + return getNestedPath() + canonicalFieldName(field); + } + else { + String path = getNestedPath(); + return (path.endsWith(Errors.NESTED_PATH_SEPARATOR) ? + path.substring(0, path.length() - NESTED_PATH_SEPARATOR.length()) : path); + } + } + + /** + * Determine the canonical field name for the given field. + *

    The default implementation simply returns the field name as-is. + * @param field the original field name + * @return the canonical field name + */ + protected String canonicalFieldName(String field) { + return field; + } + + + @Override + public void reject(String errorCode) { + reject(errorCode, null, null); + } + + @Override + public void reject(String errorCode, String defaultMessage) { + reject(errorCode, null, defaultMessage); + } + + @Override + public void rejectValue(@Nullable String field, String errorCode) { + rejectValue(field, errorCode, null, null); + } + + @Override + public void rejectValue(@Nullable String field, String errorCode, String defaultMessage) { + rejectValue(field, errorCode, null, defaultMessage); + } + + + @Override + public boolean hasErrors() { + return !getAllErrors().isEmpty(); + } + + @Override + public int getErrorCount() { + return getAllErrors().size(); + } + + @Override + public List getAllErrors() { + List result = new ArrayList<>(); + result.addAll(getGlobalErrors()); + result.addAll(getFieldErrors()); + return Collections.unmodifiableList(result); + } + + @Override + public boolean hasGlobalErrors() { + return (getGlobalErrorCount() > 0); + } + + @Override + public int getGlobalErrorCount() { + return getGlobalErrors().size(); + } + + @Override + @Nullable + public ObjectError getGlobalError() { + List globalErrors = getGlobalErrors(); + return (!globalErrors.isEmpty() ? globalErrors.get(0) : null); + } + + @Override + public boolean hasFieldErrors() { + return (getFieldErrorCount() > 0); + } + + @Override + public int getFieldErrorCount() { + return getFieldErrors().size(); + } + + @Override + @Nullable + public FieldError getFieldError() { + List fieldErrors = getFieldErrors(); + return (!fieldErrors.isEmpty() ? fieldErrors.get(0) : null); + } + + @Override + public boolean hasFieldErrors(String field) { + return (getFieldErrorCount(field) > 0); + } + + @Override + public int getFieldErrorCount(String field) { + return getFieldErrors(field).size(); + } + + @Override + public List getFieldErrors(String field) { + List fieldErrors = getFieldErrors(); + List result = new ArrayList<>(); + String fixedField = fixedField(field); + for (FieldError error : fieldErrors) { + if (isMatchingFieldError(fixedField, error)) { + result.add(error); + } + } + return Collections.unmodifiableList(result); + } + + @Override + @Nullable + public FieldError getFieldError(String field) { + List fieldErrors = getFieldErrors(field); + return (!fieldErrors.isEmpty() ? fieldErrors.get(0) : null); + } + + @Override + @Nullable + public Class getFieldType(String field) { + Object value = getFieldValue(field); + return (value != null ? value.getClass() : null); + } + + /** + * Check whether the given FieldError matches the given field. + * @param field the field that we are looking up FieldErrors for + * @param fieldError the candidate FieldError + * @return whether the FieldError matches the given field + */ + protected boolean isMatchingFieldError(String field, FieldError fieldError) { + if (field.equals(fieldError.getField())) { + return true; + } + // Optimization: use charAt and regionMatches instead of endsWith and startsWith (SPR-11304) + int endIndex = field.length() - 1; + return (endIndex >= 0 && field.charAt(endIndex) == '*' && + (endIndex == 0 || field.regionMatches(0, fieldError.getField(), 0, endIndex))); + } + + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(getClass().getName()); + sb.append(": ").append(getErrorCount()).append(" errors"); + for (ObjectError error : getAllErrors()) { + sb.append('\n').append(error); + } + return sb.toString(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/AbstractPropertyBindingResult.java b/spring-context/src/main/java/org/springframework/validation/AbstractPropertyBindingResult.java new file mode 100644 index 0000000..15ef3ae --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/AbstractPropertyBindingResult.java @@ -0,0 +1,193 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation; + +import java.beans.PropertyEditor; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.ConfigurablePropertyAccessor; +import org.springframework.beans.PropertyAccessorUtils; +import org.springframework.beans.PropertyEditorRegistry; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.support.ConvertingPropertyEditorAdapter; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Abstract base class for {@link BindingResult} implementations that work with + * Spring's {@link org.springframework.beans.PropertyAccessor} mechanism. + * Pre-implements field access through delegation to the corresponding + * PropertyAccessor methods. + * + * @author Juergen Hoeller + * @since 2.0 + * @see #getPropertyAccessor() + * @see org.springframework.beans.PropertyAccessor + * @see org.springframework.beans.ConfigurablePropertyAccessor + */ +@SuppressWarnings("serial") +public abstract class AbstractPropertyBindingResult extends AbstractBindingResult { + + @Nullable + private transient ConversionService conversionService; + + + /** + * Create a new AbstractPropertyBindingResult instance. + * @param objectName the name of the target object + * @see DefaultMessageCodesResolver + */ + protected AbstractPropertyBindingResult(String objectName) { + super(objectName); + } + + + public void initConversion(ConversionService conversionService) { + Assert.notNull(conversionService, "ConversionService must not be null"); + this.conversionService = conversionService; + if (getTarget() != null) { + getPropertyAccessor().setConversionService(conversionService); + } + } + + /** + * Returns the underlying PropertyAccessor. + * @see #getPropertyAccessor() + */ + @Override + public PropertyEditorRegistry getPropertyEditorRegistry() { + return (getTarget() != null ? getPropertyAccessor() : null); + } + + /** + * Returns the canonical property name. + * @see org.springframework.beans.PropertyAccessorUtils#canonicalPropertyName + */ + @Override + protected String canonicalFieldName(String field) { + return PropertyAccessorUtils.canonicalPropertyName(field); + } + + /** + * Determines the field type from the property type. + * @see #getPropertyAccessor() + */ + @Override + @Nullable + public Class getFieldType(@Nullable String field) { + return (getTarget() != null ? getPropertyAccessor().getPropertyType(fixedField(field)) : + super.getFieldType(field)); + } + + /** + * Fetches the field value from the PropertyAccessor. + * @see #getPropertyAccessor() + */ + @Override + @Nullable + protected Object getActualFieldValue(String field) { + return getPropertyAccessor().getPropertyValue(field); + } + + /** + * Formats the field value based on registered PropertyEditors. + * @see #getCustomEditor + */ + @Override + protected Object formatFieldValue(String field, @Nullable Object value) { + String fixedField = fixedField(field); + // Try custom editor... + PropertyEditor customEditor = getCustomEditor(fixedField); + if (customEditor != null) { + customEditor.setValue(value); + String textValue = customEditor.getAsText(); + // If the PropertyEditor returned null, there is no appropriate + // text representation for this value: only use it if non-null. + if (textValue != null) { + return textValue; + } + } + if (this.conversionService != null) { + // Try custom converter... + TypeDescriptor fieldDesc = getPropertyAccessor().getPropertyTypeDescriptor(fixedField); + TypeDescriptor strDesc = TypeDescriptor.valueOf(String.class); + if (fieldDesc != null && this.conversionService.canConvert(fieldDesc, strDesc)) { + return this.conversionService.convert(value, fieldDesc, strDesc); + } + } + return value; + } + + /** + * Retrieve the custom PropertyEditor for the given field, if any. + * @param fixedField the fully qualified field name + * @return the custom PropertyEditor, or {@code null} + */ + @Nullable + protected PropertyEditor getCustomEditor(String fixedField) { + Class targetType = getPropertyAccessor().getPropertyType(fixedField); + PropertyEditor editor = getPropertyAccessor().findCustomEditor(targetType, fixedField); + if (editor == null) { + editor = BeanUtils.findEditorByConvention(targetType); + } + return editor; + } + + /** + * This implementation exposes a PropertyEditor adapter for a Formatter, + * if applicable. + */ + @Override + @Nullable + public PropertyEditor findEditor(@Nullable String field, @Nullable Class valueType) { + Class valueTypeForLookup = valueType; + if (valueTypeForLookup == null) { + valueTypeForLookup = getFieldType(field); + } + PropertyEditor editor = super.findEditor(field, valueTypeForLookup); + if (editor == null && this.conversionService != null) { + TypeDescriptor td = null; + if (field != null && getTarget() != null) { + TypeDescriptor ptd = getPropertyAccessor().getPropertyTypeDescriptor(fixedField(field)); + if (ptd != null && (valueType == null || valueType.isAssignableFrom(ptd.getType()))) { + td = ptd; + } + } + if (td == null) { + td = TypeDescriptor.valueOf(valueTypeForLookup); + } + if (this.conversionService.canConvert(TypeDescriptor.valueOf(String.class), td)) { + editor = new ConvertingPropertyEditorAdapter(this.conversionService, td); + } + } + return editor; + } + + + /** + * Provide the PropertyAccessor to work with, according to the + * concrete strategy of access. + *

    Note that a PropertyAccessor used by a BindingResult should + * always have its "extractOldValueForEditor" flag set to "true" + * by default, since this is typically possible without side effects + * for model objects that serve as data binding target. + * @see ConfigurablePropertyAccessor#setExtractOldValueForEditor + */ + public abstract ConfigurablePropertyAccessor getPropertyAccessor(); + +} diff --git a/spring-context/src/main/java/org/springframework/validation/BeanPropertyBindingResult.java b/spring-context/src/main/java/org/springframework/validation/BeanPropertyBindingResult.java new file mode 100644 index 0000000..8558a26 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/BeanPropertyBindingResult.java @@ -0,0 +1,116 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation; + +import java.io.Serializable; + +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.ConfigurablePropertyAccessor; +import org.springframework.beans.PropertyAccessorFactory; +import org.springframework.lang.Nullable; + +/** + * Default implementation of the {@link Errors} and {@link BindingResult} + * interfaces, for the registration and evaluation of binding errors on + * JavaBean objects. + * + *

    Performs standard JavaBean property access, also supporting nested + * properties. Normally, application code will work with the + * {@code Errors} interface or the {@code BindingResult} interface. + * A {@link DataBinder} returns its {@code BindingResult} via + * {@link DataBinder#getBindingResult()}. + * + * @author Juergen Hoeller + * @since 2.0 + * @see DataBinder#getBindingResult() + * @see DataBinder#initBeanPropertyAccess() + * @see DirectFieldBindingResult + */ +@SuppressWarnings("serial") +public class BeanPropertyBindingResult extends AbstractPropertyBindingResult implements Serializable { + + @Nullable + private final Object target; + + private final boolean autoGrowNestedPaths; + + private final int autoGrowCollectionLimit; + + @Nullable + private transient BeanWrapper beanWrapper; + + + /** + * Creates a new instance of the {@link BeanPropertyBindingResult} class. + * @param target the target bean to bind onto + * @param objectName the name of the target object + */ + public BeanPropertyBindingResult(@Nullable Object target, String objectName) { + this(target, objectName, true, Integer.MAX_VALUE); + } + + /** + * Creates a new instance of the {@link BeanPropertyBindingResult} class. + * @param target the target bean to bind onto + * @param objectName the name of the target object + * @param autoGrowNestedPaths whether to "auto-grow" a nested path that contains a null value + * @param autoGrowCollectionLimit the limit for array and collection auto-growing + */ + public BeanPropertyBindingResult(@Nullable Object target, String objectName, + boolean autoGrowNestedPaths, int autoGrowCollectionLimit) { + + super(objectName); + this.target = target; + this.autoGrowNestedPaths = autoGrowNestedPaths; + this.autoGrowCollectionLimit = autoGrowCollectionLimit; + } + + + @Override + @Nullable + public final Object getTarget() { + return this.target; + } + + /** + * Returns the {@link BeanWrapper} that this instance uses. + * Creates a new one if none existed before. + * @see #createBeanWrapper() + */ + @Override + public final ConfigurablePropertyAccessor getPropertyAccessor() { + if (this.beanWrapper == null) { + this.beanWrapper = createBeanWrapper(); + this.beanWrapper.setExtractOldValueForEditor(true); + this.beanWrapper.setAutoGrowNestedPaths(this.autoGrowNestedPaths); + this.beanWrapper.setAutoGrowCollectionLimit(this.autoGrowCollectionLimit); + } + return this.beanWrapper; + } + + /** + * Create a new {@link BeanWrapper} for the underlying target object. + * @see #getTarget() + */ + protected BeanWrapper createBeanWrapper() { + if (this.target == null) { + throw new IllegalStateException("Cannot access properties on null bean instance '" + getObjectName() + "'"); + } + return PropertyAccessorFactory.forBeanPropertyAccess(this.target); + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/BindException.java b/spring-context/src/main/java/org/springframework/validation/BindException.java new file mode 100644 index 0000000..afc0c5c --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/BindException.java @@ -0,0 +1,310 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation; + +import java.beans.PropertyEditor; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.PropertyEditorRegistry; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Thrown when binding errors are considered fatal. Implements the + * {@link BindingResult} interface (and its super-interface {@link Errors}) + * to allow for the direct analysis of binding errors. + * + *

    As of Spring 2.0, this is a special-purpose class. Normally, + * application code will work with the {@link BindingResult} interface, + * or with a {@link DataBinder} that in turn exposes a BindingResult via + * {@link org.springframework.validation.DataBinder#getBindingResult()}. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Rob Harrop + * @see BindingResult + * @see DataBinder#getBindingResult() + * @see DataBinder#close() + */ +@SuppressWarnings("serial") +public class BindException extends Exception implements BindingResult { + + private final BindingResult bindingResult; + + + /** + * Create a new BindException instance for a BindingResult. + * @param bindingResult the BindingResult instance to wrap + */ + public BindException(BindingResult bindingResult) { + Assert.notNull(bindingResult, "BindingResult must not be null"); + this.bindingResult = bindingResult; + } + + /** + * Create a new BindException instance for a target bean. + * @param target the target bean to bind onto + * @param objectName the name of the target object + * @see BeanPropertyBindingResult + */ + public BindException(Object target, String objectName) { + Assert.notNull(target, "Target object must not be null"); + this.bindingResult = new BeanPropertyBindingResult(target, objectName); + } + + + /** + * Return the BindingResult that this BindException wraps. + */ + public final BindingResult getBindingResult() { + return this.bindingResult; + } + + + @Override + public String getObjectName() { + return this.bindingResult.getObjectName(); + } + + @Override + public void setNestedPath(String nestedPath) { + this.bindingResult.setNestedPath(nestedPath); + } + + @Override + public String getNestedPath() { + return this.bindingResult.getNestedPath(); + } + + @Override + public void pushNestedPath(String subPath) { + this.bindingResult.pushNestedPath(subPath); + } + + @Override + public void popNestedPath() throws IllegalStateException { + this.bindingResult.popNestedPath(); + } + + + @Override + public void reject(String errorCode) { + this.bindingResult.reject(errorCode); + } + + @Override + public void reject(String errorCode, String defaultMessage) { + this.bindingResult.reject(errorCode, defaultMessage); + } + + @Override + public void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage) { + this.bindingResult.reject(errorCode, errorArgs, defaultMessage); + } + + @Override + public void rejectValue(@Nullable String field, String errorCode) { + this.bindingResult.rejectValue(field, errorCode); + } + + @Override + public void rejectValue(@Nullable String field, String errorCode, String defaultMessage) { + this.bindingResult.rejectValue(field, errorCode, defaultMessage); + } + + @Override + public void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage) { + this.bindingResult.rejectValue(field, errorCode, errorArgs, defaultMessage); + } + + @Override + public void addAllErrors(Errors errors) { + this.bindingResult.addAllErrors(errors); + } + + + @Override + public boolean hasErrors() { + return this.bindingResult.hasErrors(); + } + + @Override + public int getErrorCount() { + return this.bindingResult.getErrorCount(); + } + + @Override + public List getAllErrors() { + return this.bindingResult.getAllErrors(); + } + + @Override + public boolean hasGlobalErrors() { + return this.bindingResult.hasGlobalErrors(); + } + + @Override + public int getGlobalErrorCount() { + return this.bindingResult.getGlobalErrorCount(); + } + + @Override + public List getGlobalErrors() { + return this.bindingResult.getGlobalErrors(); + } + + @Override + @Nullable + public ObjectError getGlobalError() { + return this.bindingResult.getGlobalError(); + } + + @Override + public boolean hasFieldErrors() { + return this.bindingResult.hasFieldErrors(); + } + + @Override + public int getFieldErrorCount() { + return this.bindingResult.getFieldErrorCount(); + } + + @Override + public List getFieldErrors() { + return this.bindingResult.getFieldErrors(); + } + + @Override + @Nullable + public FieldError getFieldError() { + return this.bindingResult.getFieldError(); + } + + @Override + public boolean hasFieldErrors(String field) { + return this.bindingResult.hasFieldErrors(field); + } + + @Override + public int getFieldErrorCount(String field) { + return this.bindingResult.getFieldErrorCount(field); + } + + @Override + public List getFieldErrors(String field) { + return this.bindingResult.getFieldErrors(field); + } + + @Override + @Nullable + public FieldError getFieldError(String field) { + return this.bindingResult.getFieldError(field); + } + + @Override + @Nullable + public Object getFieldValue(String field) { + return this.bindingResult.getFieldValue(field); + } + + @Override + @Nullable + public Class getFieldType(String field) { + return this.bindingResult.getFieldType(field); + } + + @Override + @Nullable + public Object getTarget() { + return this.bindingResult.getTarget(); + } + + @Override + public Map getModel() { + return this.bindingResult.getModel(); + } + + @Override + @Nullable + public Object getRawFieldValue(String field) { + return this.bindingResult.getRawFieldValue(field); + } + + @Override + @SuppressWarnings("rawtypes") + @Nullable + public PropertyEditor findEditor(@Nullable String field, @Nullable Class valueType) { + return this.bindingResult.findEditor(field, valueType); + } + + @Override + @Nullable + public PropertyEditorRegistry getPropertyEditorRegistry() { + return this.bindingResult.getPropertyEditorRegistry(); + } + + @Override + public String[] resolveMessageCodes(String errorCode) { + return this.bindingResult.resolveMessageCodes(errorCode); + } + + @Override + public String[] resolveMessageCodes(String errorCode, String field) { + return this.bindingResult.resolveMessageCodes(errorCode, field); + } + + @Override + public void addError(ObjectError error) { + this.bindingResult.addError(error); + } + + @Override + public void recordFieldValue(String field, Class type, @Nullable Object value) { + this.bindingResult.recordFieldValue(field, type, value); + } + + @Override + public void recordSuppressedField(String field) { + this.bindingResult.recordSuppressedField(field); + } + + @Override + public String[] getSuppressedFields() { + return this.bindingResult.getSuppressedFields(); + } + + + /** + * Returns diagnostic information about the errors held in this object. + */ + @Override + public String getMessage() { + return this.bindingResult.toString(); + } + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || this.bindingResult.equals(other)); + } + + @Override + public int hashCode() { + return this.bindingResult.hashCode(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/BindingErrorProcessor.java b/spring-context/src/main/java/org/springframework/validation/BindingErrorProcessor.java new file mode 100644 index 0000000..ac58608 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/BindingErrorProcessor.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation; + +import org.springframework.beans.PropertyAccessException; + +/** + * Strategy for processing {@code DataBinder}'s missing field errors, + * and for translating a {@code PropertyAccessException} to a + * {@code FieldError}. + * + *

    The error processor is pluggable so you can treat errors differently + * if you want to. A default implementation is provided for typical needs. + * + *

    Note: As of Spring 2.0, this interface operates on a given BindingResult, + * to be compatible with any binding strategy (bean property, direct field access, etc). + * It can still receive a BindException as argument (since a BindException implements + * the BindingResult interface as well) but no longer operates on it directly. + * + * @author Alef Arendsen + * @author Juergen Hoeller + * @since 1.2 + * @see DataBinder#setBindingErrorProcessor + * @see DefaultBindingErrorProcessor + * @see BindingResult + * @see BindException + */ +public interface BindingErrorProcessor { + + /** + * Apply the missing field error to the given BindException. + *

    Usually, a field error is created for a missing required field. + * @param missingField the field that was missing during binding + * @param bindingResult the errors object to add the error(s) to. + * You can add more than just one error or maybe even ignore it. + * The {@code BindingResult} object features convenience utils such as + * a {@code resolveMessageCodes} method to resolve an error code. + * @see BeanPropertyBindingResult#addError + * @see BeanPropertyBindingResult#resolveMessageCodes + */ + void processMissingFieldError(String missingField, BindingResult bindingResult); + + /** + * Translate the given {@code PropertyAccessException} to an appropriate + * error registered on the given {@code Errors} instance. + *

    Note that two error types are available: {@code FieldError} and + * {@code ObjectError}. Usually, field errors are created, but in certain + * situations one might want to create a global {@code ObjectError} instead. + * @param ex the {@code PropertyAccessException} to translate + * @param bindingResult the errors object to add the error(s) to. + * You can add more than just one error or maybe even ignore it. + * The {@code BindingResult} object features convenience utils such as + * a {@code resolveMessageCodes} method to resolve an error code. + * @see Errors + * @see FieldError + * @see ObjectError + * @see MessageCodesResolver + * @see BeanPropertyBindingResult#addError + * @see BeanPropertyBindingResult#resolveMessageCodes + */ + void processPropertyAccessException(PropertyAccessException ex, BindingResult bindingResult); + +} diff --git a/spring-context/src/main/java/org/springframework/validation/BindingResult.java b/spring-context/src/main/java/org/springframework/validation/BindingResult.java new file mode 100644 index 0000000..9ccbc37 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/BindingResult.java @@ -0,0 +1,168 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation; + +import java.beans.PropertyEditor; +import java.util.Map; + +import org.springframework.beans.PropertyEditorRegistry; +import org.springframework.lang.Nullable; + +/** + * General interface that represents binding results. Extends the + * {@link Errors interface} for error registration capabilities, + * allowing for a {@link Validator} to be applied, and adds + * binding-specific analysis and model building. + * + *

    Serves as result holder for a {@link DataBinder}, obtained via + * the {@link DataBinder#getBindingResult()} method. BindingResult + * implementations can also be used directly, for example to invoke + * a {@link Validator} on it (e.g. as part of a unit test). + * + * @author Juergen Hoeller + * @since 2.0 + * @see DataBinder + * @see Errors + * @see Validator + * @see BeanPropertyBindingResult + * @see DirectFieldBindingResult + * @see MapBindingResult + */ +public interface BindingResult extends Errors { + + /** + * Prefix for the name of the BindingResult instance in a model, + * followed by the object name. + */ + String MODEL_KEY_PREFIX = BindingResult.class.getName() + "."; + + + /** + * Return the wrapped target object, which may be a bean, an object with + * public fields, a Map - depending on the concrete binding strategy. + */ + @Nullable + Object getTarget(); + + /** + * Return a model Map for the obtained state, exposing a BindingResult + * instance as '{@link #MODEL_KEY_PREFIX MODEL_KEY_PREFIX} + objectName' + * and the object itself as 'objectName'. + *

    Note that the Map is constructed every time you're calling this method. + * Adding things to the map and then re-calling this method will not work. + *

    The attributes in the model Map returned by this method are usually + * included in the {@link org.springframework.web.servlet.ModelAndView} + * for a form view that uses Spring's {@code bind} tag in a JSP, + * which needs access to the BindingResult instance. Spring's pre-built + * form controllers will do this for you when rendering a form view. + * When building the ModelAndView instance yourself, you need to include + * the attributes from the model Map returned by this method. + * @see #getObjectName() + * @see #MODEL_KEY_PREFIX + * @see org.springframework.web.servlet.ModelAndView + * @see org.springframework.web.servlet.tags.BindTag + */ + Map getModel(); + + /** + * Extract the raw field value for the given field. + * Typically used for comparison purposes. + * @param field the field to check + * @return the current value of the field in its raw form, or {@code null} if not known + */ + @Nullable + Object getRawFieldValue(String field); + + /** + * Find a custom property editor for the given type and property. + * @param field the path of the property (name or nested path), or + * {@code null} if looking for an editor for all properties of the given type + * @param valueType the type of the property (can be {@code null} if a property + * is given but should be specified in any case for consistency checking) + * @return the registered editor, or {@code null} if none + */ + @Nullable + PropertyEditor findEditor(@Nullable String field, @Nullable Class valueType); + + /** + * Return the underlying PropertyEditorRegistry. + * @return the PropertyEditorRegistry, or {@code null} if none + * available for this BindingResult + */ + @Nullable + PropertyEditorRegistry getPropertyEditorRegistry(); + + /** + * Resolve the given error code into message codes. + *

    Calls the configured {@link MessageCodesResolver} with appropriate parameters. + * @param errorCode the error code to resolve into message codes + * @return the resolved message codes + */ + String[] resolveMessageCodes(String errorCode); + + /** + * Resolve the given error code into message codes for the given field. + *

    Calls the configured {@link MessageCodesResolver} with appropriate parameters. + * @param errorCode the error code to resolve into message codes + * @param field the field to resolve message codes for + * @return the resolved message codes + */ + String[] resolveMessageCodes(String errorCode, String field); + + /** + * Add a custom {@link ObjectError} or {@link FieldError} to the errors list. + *

    Intended to be used by cooperating strategies such as {@link BindingErrorProcessor}. + * @see ObjectError + * @see FieldError + * @see BindingErrorProcessor + */ + void addError(ObjectError error); + + /** + * Record the given value for the specified field. + *

    To be used when a target object cannot be constructed, making + * the original field values available through {@link #getFieldValue}. + * In case of a registered error, the rejected value will be exposed + * for each affected field. + * @param field the field to record the value for + * @param type the type of the field + * @param value the original value + * @since 5.0.4 + */ + default void recordFieldValue(String field, Class type, @Nullable Object value) { + } + + /** + * Mark the specified disallowed field as suppressed. + *

    The data binder invokes this for each field value that was + * detected to target a disallowed field. + * @see DataBinder#setAllowedFields + */ + default void recordSuppressedField(String field) { + } + + /** + * Return the list of fields that were suppressed during the bind process. + *

    Can be used to determine whether any field values were targeting + * disallowed fields. + * @see DataBinder#setAllowedFields + */ + default String[] getSuppressedFields() { + return new String[0]; + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/BindingResultUtils.java b/spring-context/src/main/java/org/springframework/validation/BindingResultUtils.java new file mode 100644 index 0000000..deef4e8 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/BindingResultUtils.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation; + +import java.util.Map; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Convenience methods for looking up BindingResults in a model Map. + * + * @author Juergen Hoeller + * @since 2.0 + * @see BindingResult#MODEL_KEY_PREFIX + */ +public abstract class BindingResultUtils { + + /** + * Find the BindingResult for the given name in the given model. + * @param model the model to search + * @param name the name of the target object to find a BindingResult for + * @return the BindingResult, or {@code null} if none found + * @throws IllegalStateException if the attribute found is not of type BindingResult + */ + @Nullable + public static BindingResult getBindingResult(Map model, String name) { + Assert.notNull(model, "Model map must not be null"); + Assert.notNull(name, "Name must not be null"); + Object attr = model.get(BindingResult.MODEL_KEY_PREFIX + name); + if (attr != null && !(attr instanceof BindingResult)) { + throw new IllegalStateException("BindingResult attribute is not of type BindingResult: " + attr); + } + return (BindingResult) attr; + } + + /** + * Find a required BindingResult for the given name in the given model. + * @param model the model to search + * @param name the name of the target object to find a BindingResult for + * @return the BindingResult (never {@code null}) + * @throws IllegalStateException if no BindingResult found + */ + public static BindingResult getRequiredBindingResult(Map model, String name) { + BindingResult bindingResult = getBindingResult(model, name); + if (bindingResult == null) { + throw new IllegalStateException("No BindingResult attribute found for name '" + name + + "'- have you exposed the correct model?"); + } + return bindingResult; + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/DataBinder.java b/spring-context/src/main/java/org/springframework/validation/DataBinder.java new file mode 100644 index 0000000..1818b94 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/DataBinder.java @@ -0,0 +1,914 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation; + +import java.beans.PropertyEditor; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.ConfigurablePropertyAccessor; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.PropertyAccessException; +import org.springframework.beans.PropertyAccessorUtils; +import org.springframework.beans.PropertyBatchUpdateException; +import org.springframework.beans.PropertyEditorRegistry; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.PropertyValues; +import org.springframework.beans.SimpleTypeConverter; +import org.springframework.beans.TypeConverter; +import org.springframework.beans.TypeMismatchException; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.format.Formatter; +import org.springframework.format.support.FormatterPropertyEditorAdapter; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.PatternMatchUtils; +import org.springframework.util.StringUtils; + +/** + * Binder that allows for setting property values onto a target object, + * including support for validation and binding result analysis. + * The binding process can be customized through specifying allowed fields, + * required fields, custom editors, etc. + * + *

    Note that there are potential security implications in failing to set an array + * of allowed fields. In the case of HTTP form POST data for example, malicious clients + * can attempt to subvert an application by supplying values for fields or properties + * that do not exist on the form. In some cases this could lead to illegal data being + * set on command objects or their nested objects. For this reason, it is + * highly recommended to specify the {@link #setAllowedFields allowedFields} property + * on the DataBinder. + * + *

    The binding results can be examined via the {@link BindingResult} interface, + * extending the {@link Errors} interface: see the {@link #getBindingResult()} method. + * Missing fields and property access exceptions will be converted to {@link FieldError FieldErrors}, + * collected in the Errors instance, using the following error codes: + * + *

      + *
    • Missing field error: "required" + *
    • Type mismatch error: "typeMismatch" + *
    • Method invocation error: "methodInvocation" + *
    + * + *

    By default, binding errors get resolved through the {@link BindingErrorProcessor} + * strategy, processing for missing fields and property access exceptions: see the + * {@link #setBindingErrorProcessor} method. You can override the default strategy + * if needed, for example to generate different error codes. + * + *

    Custom validation errors can be added afterwards. You will typically want to resolve + * such error codes into proper user-visible error messages; this can be achieved through + * resolving each error via a {@link org.springframework.context.MessageSource}, which is + * able to resolve an {@link ObjectError}/{@link FieldError} through its + * {@link org.springframework.context.MessageSource#getMessage(org.springframework.context.MessageSourceResolvable, java.util.Locale)} + * method. The list of message codes can be customized through the {@link MessageCodesResolver} + * strategy: see the {@link #setMessageCodesResolver} method. {@link DefaultMessageCodesResolver}'s + * javadoc states details on the default resolution rules. + * + *

    This generic data binder can be used in any kind of environment. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Rob Harrop + * @author Stephane Nicoll + * @author Kazuki Shimizu + * @see #setAllowedFields + * @see #setRequiredFields + * @see #registerCustomEditor + * @see #setMessageCodesResolver + * @see #setBindingErrorProcessor + * @see #bind + * @see #getBindingResult + * @see DefaultMessageCodesResolver + * @see DefaultBindingErrorProcessor + * @see org.springframework.context.MessageSource + */ +public class DataBinder implements PropertyEditorRegistry, TypeConverter { + + /** Default object name used for binding: "target". */ + public static final String DEFAULT_OBJECT_NAME = "target"; + + /** Default limit for array and collection growing: 256. */ + public static final int DEFAULT_AUTO_GROW_COLLECTION_LIMIT = 256; + + + /** + * We'll create a lot of DataBinder instances: Let's use a static logger. + */ + protected static final Log logger = LogFactory.getLog(DataBinder.class); + + @Nullable + private final Object target; + + private final String objectName; + + @Nullable + private AbstractPropertyBindingResult bindingResult; + + private boolean directFieldAccess = false; + + @Nullable + private SimpleTypeConverter typeConverter; + + private boolean ignoreUnknownFields = true; + + private boolean ignoreInvalidFields = false; + + private boolean autoGrowNestedPaths = true; + + private int autoGrowCollectionLimit = DEFAULT_AUTO_GROW_COLLECTION_LIMIT; + + @Nullable + private String[] allowedFields; + + @Nullable + private String[] disallowedFields; + + @Nullable + private String[] requiredFields; + + @Nullable + private ConversionService conversionService; + + @Nullable + private MessageCodesResolver messageCodesResolver; + + private BindingErrorProcessor bindingErrorProcessor = new DefaultBindingErrorProcessor(); + + private final List validators = new ArrayList<>(); + + + /** + * Create a new DataBinder instance, with default object name. + * @param target the target object to bind onto (or {@code null} + * if the binder is just used to convert a plain parameter value) + * @see #DEFAULT_OBJECT_NAME + */ + public DataBinder(@Nullable Object target) { + this(target, DEFAULT_OBJECT_NAME); + } + + /** + * Create a new DataBinder instance. + * @param target the target object to bind onto (or {@code null} + * if the binder is just used to convert a plain parameter value) + * @param objectName the name of the target object + */ + public DataBinder(@Nullable Object target, String objectName) { + this.target = ObjectUtils.unwrapOptional(target); + this.objectName = objectName; + } + + + /** + * Return the wrapped target object. + */ + @Nullable + public Object getTarget() { + return this.target; + } + + /** + * Return the name of the bound object. + */ + public String getObjectName() { + return this.objectName; + } + + /** + * Set whether this binder should attempt to "auto-grow" a nested path that contains a null value. + *

    If "true", a null path location will be populated with a default object value and traversed + * instead of resulting in an exception. This flag also enables auto-growth of collection elements + * when accessing an out-of-bounds index. + *

    Default is "true" on a standard DataBinder. Note that since Spring 4.1 this feature is supported + * for bean property access (DataBinder's default mode) and field access. + * @see #initBeanPropertyAccess() + * @see org.springframework.beans.BeanWrapper#setAutoGrowNestedPaths + */ + public void setAutoGrowNestedPaths(boolean autoGrowNestedPaths) { + Assert.state(this.bindingResult == null, + "DataBinder is already initialized - call setAutoGrowNestedPaths before other configuration methods"); + this.autoGrowNestedPaths = autoGrowNestedPaths; + } + + /** + * Return whether "auto-growing" of nested paths has been activated. + */ + public boolean isAutoGrowNestedPaths() { + return this.autoGrowNestedPaths; + } + + /** + * Specify the limit for array and collection auto-growing. + *

    Default is 256, preventing OutOfMemoryErrors in case of large indexes. + * Raise this limit if your auto-growing needs are unusually high. + * @see #initBeanPropertyAccess() + * @see org.springframework.beans.BeanWrapper#setAutoGrowCollectionLimit + */ + public void setAutoGrowCollectionLimit(int autoGrowCollectionLimit) { + Assert.state(this.bindingResult == null, + "DataBinder is already initialized - call setAutoGrowCollectionLimit before other configuration methods"); + this.autoGrowCollectionLimit = autoGrowCollectionLimit; + } + + /** + * Return the current limit for array and collection auto-growing. + */ + public int getAutoGrowCollectionLimit() { + return this.autoGrowCollectionLimit; + } + + /** + * Initialize standard JavaBean property access for this DataBinder. + *

    This is the default; an explicit call just leads to eager initialization. + * @see #initDirectFieldAccess() + * @see #createBeanPropertyBindingResult() + */ + public void initBeanPropertyAccess() { + Assert.state(this.bindingResult == null, + "DataBinder is already initialized - call initBeanPropertyAccess before other configuration methods"); + this.directFieldAccess = false; + } + + /** + * Create the {@link AbstractPropertyBindingResult} instance using standard + * JavaBean property access. + * @since 4.2.1 + */ + protected AbstractPropertyBindingResult createBeanPropertyBindingResult() { + BeanPropertyBindingResult result = new BeanPropertyBindingResult(getTarget(), + getObjectName(), isAutoGrowNestedPaths(), getAutoGrowCollectionLimit()); + + if (this.conversionService != null) { + result.initConversion(this.conversionService); + } + if (this.messageCodesResolver != null) { + result.setMessageCodesResolver(this.messageCodesResolver); + } + + return result; + } + + /** + * Initialize direct field access for this DataBinder, + * as alternative to the default bean property access. + * @see #initBeanPropertyAccess() + * @see #createDirectFieldBindingResult() + */ + public void initDirectFieldAccess() { + Assert.state(this.bindingResult == null, + "DataBinder is already initialized - call initDirectFieldAccess before other configuration methods"); + this.directFieldAccess = true; + } + + /** + * Create the {@link AbstractPropertyBindingResult} instance using direct + * field access. + * @since 4.2.1 + */ + protected AbstractPropertyBindingResult createDirectFieldBindingResult() { + DirectFieldBindingResult result = new DirectFieldBindingResult(getTarget(), + getObjectName(), isAutoGrowNestedPaths()); + + if (this.conversionService != null) { + result.initConversion(this.conversionService); + } + if (this.messageCodesResolver != null) { + result.setMessageCodesResolver(this.messageCodesResolver); + } + + return result; + } + + /** + * Return the internal BindingResult held by this DataBinder, + * as an AbstractPropertyBindingResult. + */ + protected AbstractPropertyBindingResult getInternalBindingResult() { + if (this.bindingResult == null) { + this.bindingResult = (this.directFieldAccess ? + createDirectFieldBindingResult(): createBeanPropertyBindingResult()); + } + return this.bindingResult; + } + + /** + * Return the underlying PropertyAccessor of this binder's BindingResult. + */ + protected ConfigurablePropertyAccessor getPropertyAccessor() { + return getInternalBindingResult().getPropertyAccessor(); + } + + /** + * Return this binder's underlying SimpleTypeConverter. + */ + protected SimpleTypeConverter getSimpleTypeConverter() { + if (this.typeConverter == null) { + this.typeConverter = new SimpleTypeConverter(); + if (this.conversionService != null) { + this.typeConverter.setConversionService(this.conversionService); + } + } + return this.typeConverter; + } + + /** + * Return the underlying TypeConverter of this binder's BindingResult. + */ + protected PropertyEditorRegistry getPropertyEditorRegistry() { + if (getTarget() != null) { + return getInternalBindingResult().getPropertyAccessor(); + } + else { + return getSimpleTypeConverter(); + } + } + + /** + * Return the underlying TypeConverter of this binder's BindingResult. + */ + protected TypeConverter getTypeConverter() { + if (getTarget() != null) { + return getInternalBindingResult().getPropertyAccessor(); + } + else { + return getSimpleTypeConverter(); + } + } + + /** + * Return the BindingResult instance created by this DataBinder. + * This allows for convenient access to the binding results after + * a bind operation. + * @return the BindingResult instance, to be treated as BindingResult + * or as Errors instance (Errors is a super-interface of BindingResult) + * @see Errors + * @see #bind + */ + public BindingResult getBindingResult() { + return getInternalBindingResult(); + } + + + /** + * Set whether to ignore unknown fields, that is, whether to ignore bind + * parameters that do not have corresponding fields in the target object. + *

    Default is "true". Turn this off to enforce that all bind parameters + * must have a matching field in the target object. + *

    Note that this setting only applies to binding operations + * on this DataBinder, not to retrieving values via its + * {@link #getBindingResult() BindingResult}. + * @see #bind + */ + public void setIgnoreUnknownFields(boolean ignoreUnknownFields) { + this.ignoreUnknownFields = ignoreUnknownFields; + } + + /** + * Return whether to ignore unknown fields when binding. + */ + public boolean isIgnoreUnknownFields() { + return this.ignoreUnknownFields; + } + + /** + * Set whether to ignore invalid fields, that is, whether to ignore bind + * parameters that have corresponding fields in the target object which are + * not accessible (for example because of null values in the nested path). + *

    Default is "false". Turn this on to ignore bind parameters for + * nested objects in non-existing parts of the target object graph. + *

    Note that this setting only applies to binding operations + * on this DataBinder, not to retrieving values via its + * {@link #getBindingResult() BindingResult}. + * @see #bind + */ + public void setIgnoreInvalidFields(boolean ignoreInvalidFields) { + this.ignoreInvalidFields = ignoreInvalidFields; + } + + /** + * Return whether to ignore invalid fields when binding. + */ + public boolean isIgnoreInvalidFields() { + return this.ignoreInvalidFields; + } + + /** + * Register fields that should be allowed for binding. Default is all + * fields. Restrict this for example to avoid unwanted modifications + * by malicious users when binding HTTP request parameters. + *

    Supports "xxx*", "*xxx" and "*xxx*" patterns. More sophisticated matching + * can be implemented by overriding the {@code isAllowed} method. + *

    Alternatively, specify a list of disallowed fields. + * @param allowedFields array of field names + * @see #setDisallowedFields + * @see #isAllowed(String) + */ + public void setAllowedFields(@Nullable String... allowedFields) { + this.allowedFields = PropertyAccessorUtils.canonicalPropertyNames(allowedFields); + } + + /** + * Return the fields that should be allowed for binding. + * @return array of field names + */ + @Nullable + public String[] getAllowedFields() { + return this.allowedFields; + } + + /** + * Register fields that should not be allowed for binding. Default is none. + * Mark fields as disallowed for example to avoid unwanted modifications + * by malicious users when binding HTTP request parameters. + *

    Supports "xxx*", "*xxx" and "*xxx*" patterns. More sophisticated matching + * can be implemented by overriding the {@code isAllowed} method. + *

    Alternatively, specify a list of allowed fields. + * @param disallowedFields array of field names + * @see #setAllowedFields + * @see #isAllowed(String) + */ + public void setDisallowedFields(@Nullable String... disallowedFields) { + this.disallowedFields = PropertyAccessorUtils.canonicalPropertyNames(disallowedFields); + } + + /** + * Return the fields that should not be allowed for binding. + * @return array of field names + */ + @Nullable + public String[] getDisallowedFields() { + return this.disallowedFields; + } + + /** + * Register fields that are required for each binding process. + *

    If one of the specified fields is not contained in the list of + * incoming property values, a corresponding "missing field" error + * will be created, with error code "required" (by the default + * binding error processor). + * @param requiredFields array of field names + * @see #setBindingErrorProcessor + * @see DefaultBindingErrorProcessor#MISSING_FIELD_ERROR_CODE + */ + public void setRequiredFields(@Nullable String... requiredFields) { + this.requiredFields = PropertyAccessorUtils.canonicalPropertyNames(requiredFields); + if (logger.isDebugEnabled()) { + logger.debug("DataBinder requires binding of required fields [" + + StringUtils.arrayToCommaDelimitedString(requiredFields) + "]"); + } + } + + /** + * Return the fields that are required for each binding process. + * @return array of field names + */ + @Nullable + public String[] getRequiredFields() { + return this.requiredFields; + } + + /** + * Set the strategy to use for resolving errors into message codes. + * Applies the given strategy to the underlying errors holder. + *

    Default is a DefaultMessageCodesResolver. + * @see BeanPropertyBindingResult#setMessageCodesResolver + * @see DefaultMessageCodesResolver + */ + public void setMessageCodesResolver(@Nullable MessageCodesResolver messageCodesResolver) { + Assert.state(this.messageCodesResolver == null, "DataBinder is already initialized with MessageCodesResolver"); + this.messageCodesResolver = messageCodesResolver; + if (this.bindingResult != null && messageCodesResolver != null) { + this.bindingResult.setMessageCodesResolver(messageCodesResolver); + } + } + + /** + * Set the strategy to use for processing binding errors, that is, + * required field errors and {@code PropertyAccessException}s. + *

    Default is a DefaultBindingErrorProcessor. + * @see DefaultBindingErrorProcessor + */ + public void setBindingErrorProcessor(BindingErrorProcessor bindingErrorProcessor) { + Assert.notNull(bindingErrorProcessor, "BindingErrorProcessor must not be null"); + this.bindingErrorProcessor = bindingErrorProcessor; + } + + /** + * Return the strategy for processing binding errors. + */ + public BindingErrorProcessor getBindingErrorProcessor() { + return this.bindingErrorProcessor; + } + + /** + * Set the Validator to apply after each binding step. + * @see #addValidators(Validator...) + * @see #replaceValidators(Validator...) + */ + public void setValidator(@Nullable Validator validator) { + assertValidators(validator); + this.validators.clear(); + if (validator != null) { + this.validators.add(validator); + } + } + + private void assertValidators(Validator... validators) { + Object target = getTarget(); + for (Validator validator : validators) { + if (validator != null && (target != null && !validator.supports(target.getClass()))) { + throw new IllegalStateException("Invalid target for Validator [" + validator + "]: " + target); + } + } + } + + /** + * Add Validators to apply after each binding step. + * @see #setValidator(Validator) + * @see #replaceValidators(Validator...) + */ + public void addValidators(Validator... validators) { + assertValidators(validators); + this.validators.addAll(Arrays.asList(validators)); + } + + /** + * Replace the Validators to apply after each binding step. + * @see #setValidator(Validator) + * @see #addValidators(Validator...) + */ + public void replaceValidators(Validator... validators) { + assertValidators(validators); + this.validators.clear(); + this.validators.addAll(Arrays.asList(validators)); + } + + /** + * Return the primary Validator to apply after each binding step, if any. + */ + @Nullable + public Validator getValidator() { + return (!this.validators.isEmpty() ? this.validators.get(0) : null); + } + + /** + * Return the Validators to apply after data binding. + */ + public List getValidators() { + return Collections.unmodifiableList(this.validators); + } + + + //--------------------------------------------------------------------- + // Implementation of PropertyEditorRegistry/TypeConverter interface + //--------------------------------------------------------------------- + + /** + * Specify a Spring 3.0 ConversionService to use for converting + * property values, as an alternative to JavaBeans PropertyEditors. + */ + public void setConversionService(@Nullable ConversionService conversionService) { + Assert.state(this.conversionService == null, "DataBinder is already initialized with ConversionService"); + this.conversionService = conversionService; + if (this.bindingResult != null && conversionService != null) { + this.bindingResult.initConversion(conversionService); + } + } + + /** + * Return the associated ConversionService, if any. + */ + @Nullable + public ConversionService getConversionService() { + return this.conversionService; + } + + /** + * Add a custom formatter, applying it to all fields matching the + * {@link Formatter}-declared type. + *

    Registers a corresponding {@link PropertyEditor} adapter underneath the covers. + * @param formatter the formatter to add, generically declared for a specific type + * @since 4.2 + * @see #registerCustomEditor(Class, PropertyEditor) + */ + public void addCustomFormatter(Formatter formatter) { + FormatterPropertyEditorAdapter adapter = new FormatterPropertyEditorAdapter(formatter); + getPropertyEditorRegistry().registerCustomEditor(adapter.getFieldType(), adapter); + } + + /** + * Add a custom formatter for the field type specified in {@link Formatter} class, + * applying it to the specified fields only, if any, or otherwise to all fields. + *

    Registers a corresponding {@link PropertyEditor} adapter underneath the covers. + * @param formatter the formatter to add, generically declared for a specific type + * @param fields the fields to apply the formatter to, or none if to be applied to all + * @since 4.2 + * @see #registerCustomEditor(Class, String, PropertyEditor) + */ + public void addCustomFormatter(Formatter formatter, String... fields) { + FormatterPropertyEditorAdapter adapter = new FormatterPropertyEditorAdapter(formatter); + Class fieldType = adapter.getFieldType(); + if (ObjectUtils.isEmpty(fields)) { + getPropertyEditorRegistry().registerCustomEditor(fieldType, adapter); + } + else { + for (String field : fields) { + getPropertyEditorRegistry().registerCustomEditor(fieldType, field, adapter); + } + } + } + + /** + * Add a custom formatter, applying it to the specified field types only, if any, + * or otherwise to all fields matching the {@link Formatter}-declared type. + *

    Registers a corresponding {@link PropertyEditor} adapter underneath the covers. + * @param formatter the formatter to add (does not need to generically declare a + * field type if field types are explicitly specified as parameters) + * @param fieldTypes the field types to apply the formatter to, or none if to be + * derived from the given {@link Formatter} implementation class + * @since 4.2 + * @see #registerCustomEditor(Class, PropertyEditor) + */ + public void addCustomFormatter(Formatter formatter, Class... fieldTypes) { + FormatterPropertyEditorAdapter adapter = new FormatterPropertyEditorAdapter(formatter); + if (ObjectUtils.isEmpty(fieldTypes)) { + getPropertyEditorRegistry().registerCustomEditor(adapter.getFieldType(), adapter); + } + else { + for (Class fieldType : fieldTypes) { + getPropertyEditorRegistry().registerCustomEditor(fieldType, adapter); + } + } + } + + @Override + public void registerCustomEditor(Class requiredType, PropertyEditor propertyEditor) { + getPropertyEditorRegistry().registerCustomEditor(requiredType, propertyEditor); + } + + @Override + public void registerCustomEditor(@Nullable Class requiredType, @Nullable String field, PropertyEditor propertyEditor) { + getPropertyEditorRegistry().registerCustomEditor(requiredType, field, propertyEditor); + } + + @Override + @Nullable + public PropertyEditor findCustomEditor(@Nullable Class requiredType, @Nullable String propertyPath) { + return getPropertyEditorRegistry().findCustomEditor(requiredType, propertyPath); + } + + @Override + @Nullable + public T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType) throws TypeMismatchException { + return getTypeConverter().convertIfNecessary(value, requiredType); + } + + @Override + @Nullable + public T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, + @Nullable MethodParameter methodParam) throws TypeMismatchException { + + return getTypeConverter().convertIfNecessary(value, requiredType, methodParam); + } + + @Override + @Nullable + public T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, @Nullable Field field) + throws TypeMismatchException { + + return getTypeConverter().convertIfNecessary(value, requiredType, field); + } + + @Nullable + @Override + public T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, + @Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException { + + return getTypeConverter().convertIfNecessary(value, requiredType, typeDescriptor); + } + + + /** + * Bind the given property values to this binder's target. + *

    This call can create field errors, representing basic binding + * errors like a required field (code "required"), or type mismatch + * between value and bean property (code "typeMismatch"). + *

    Note that the given PropertyValues should be a throwaway instance: + * For efficiency, it will be modified to just contain allowed fields if it + * implements the MutablePropertyValues interface; else, an internal mutable + * copy will be created for this purpose. Pass in a copy of the PropertyValues + * if you want your original instance to stay unmodified in any case. + * @param pvs property values to bind + * @see #doBind(org.springframework.beans.MutablePropertyValues) + */ + public void bind(PropertyValues pvs) { + MutablePropertyValues mpvs = (pvs instanceof MutablePropertyValues ? + (MutablePropertyValues) pvs : new MutablePropertyValues(pvs)); + doBind(mpvs); + } + + /** + * Actual implementation of the binding process, working with the + * passed-in MutablePropertyValues instance. + * @param mpvs the property values to bind, + * as MutablePropertyValues instance + * @see #checkAllowedFields + * @see #checkRequiredFields + * @see #applyPropertyValues + */ + protected void doBind(MutablePropertyValues mpvs) { + checkAllowedFields(mpvs); + checkRequiredFields(mpvs); + applyPropertyValues(mpvs); + } + + /** + * Check the given property values against the allowed fields, + * removing values for fields that are not allowed. + * @param mpvs the property values to be bound (can be modified) + * @see #getAllowedFields + * @see #isAllowed(String) + */ + protected void checkAllowedFields(MutablePropertyValues mpvs) { + PropertyValue[] pvs = mpvs.getPropertyValues(); + for (PropertyValue pv : pvs) { + String field = PropertyAccessorUtils.canonicalPropertyName(pv.getName()); + if (!isAllowed(field)) { + mpvs.removePropertyValue(pv); + getBindingResult().recordSuppressedField(field); + if (logger.isDebugEnabled()) { + logger.debug("Field [" + field + "] has been removed from PropertyValues " + + "and will not be bound, because it has not been found in the list of allowed fields"); + } + } + } + } + + /** + * Return if the given field is allowed for binding. + * Invoked for each passed-in property value. + *

    The default implementation checks for "xxx*", "*xxx" and "*xxx*" matches, + * as well as direct equality, in the specified lists of allowed fields and + * disallowed fields. A field matching a disallowed pattern will not be accepted + * even if it also happens to match a pattern in the allowed list. + *

    Can be overridden in subclasses. + * @param field the field to check + * @return if the field is allowed + * @see #setAllowedFields + * @see #setDisallowedFields + * @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String) + */ + protected boolean isAllowed(String field) { + String[] allowed = getAllowedFields(); + String[] disallowed = getDisallowedFields(); + return ((ObjectUtils.isEmpty(allowed) || PatternMatchUtils.simpleMatch(allowed, field)) && + (ObjectUtils.isEmpty(disallowed) || !PatternMatchUtils.simpleMatch(disallowed, field))); + } + + /** + * Check the given property values against the required fields, + * generating missing field errors where appropriate. + * @param mpvs the property values to be bound (can be modified) + * @see #getRequiredFields + * @see #getBindingErrorProcessor + * @see BindingErrorProcessor#processMissingFieldError + */ + protected void checkRequiredFields(MutablePropertyValues mpvs) { + String[] requiredFields = getRequiredFields(); + if (!ObjectUtils.isEmpty(requiredFields)) { + Map propertyValues = new HashMap<>(); + PropertyValue[] pvs = mpvs.getPropertyValues(); + for (PropertyValue pv : pvs) { + String canonicalName = PropertyAccessorUtils.canonicalPropertyName(pv.getName()); + propertyValues.put(canonicalName, pv); + } + for (String field : requiredFields) { + PropertyValue pv = propertyValues.get(field); + boolean empty = (pv == null || pv.getValue() == null); + if (!empty) { + if (pv.getValue() instanceof String) { + empty = !StringUtils.hasText((String) pv.getValue()); + } + else if (pv.getValue() instanceof String[]) { + String[] values = (String[]) pv.getValue(); + empty = (values.length == 0 || !StringUtils.hasText(values[0])); + } + } + if (empty) { + // Use bind error processor to create FieldError. + getBindingErrorProcessor().processMissingFieldError(field, getInternalBindingResult()); + // Remove property from property values to bind: + // It has already caused a field error with a rejected value. + if (pv != null) { + mpvs.removePropertyValue(pv); + propertyValues.remove(field); + } + } + } + } + } + + /** + * Apply given property values to the target object. + *

    Default implementation applies all of the supplied property + * values as bean property values. By default, unknown fields will + * be ignored. + * @param mpvs the property values to be bound (can be modified) + * @see #getTarget + * @see #getPropertyAccessor + * @see #isIgnoreUnknownFields + * @see #getBindingErrorProcessor + * @see BindingErrorProcessor#processPropertyAccessException + */ + protected void applyPropertyValues(MutablePropertyValues mpvs) { + try { + // Bind request parameters onto target object. + getPropertyAccessor().setPropertyValues(mpvs, isIgnoreUnknownFields(), isIgnoreInvalidFields()); + } + catch (PropertyBatchUpdateException ex) { + // Use bind error processor to create FieldErrors. + for (PropertyAccessException pae : ex.getPropertyAccessExceptions()) { + getBindingErrorProcessor().processPropertyAccessException(pae, getInternalBindingResult()); + } + } + } + + + /** + * Invoke the specified Validators, if any. + * @see #setValidator(Validator) + * @see #getBindingResult() + */ + public void validate() { + Object target = getTarget(); + Assert.state(target != null, "No target to validate"); + BindingResult bindingResult = getBindingResult(); + // Call each validator with the same binding result + for (Validator validator : getValidators()) { + validator.validate(target, bindingResult); + } + } + + /** + * Invoke the specified Validators, if any, with the given validation hints. + *

    Note: Validation hints may get ignored by the actual target Validator. + * @param validationHints one or more hint objects to be passed to a {@link SmartValidator} + * @since 3.1 + * @see #setValidator(Validator) + * @see SmartValidator#validate(Object, Errors, Object...) + */ + public void validate(Object... validationHints) { + Object target = getTarget(); + Assert.state(target != null, "No target to validate"); + BindingResult bindingResult = getBindingResult(); + // Call each validator with the same binding result + for (Validator validator : getValidators()) { + if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) { + ((SmartValidator) validator).validate(target, bindingResult, validationHints); + } + else if (validator != null) { + validator.validate(target, bindingResult); + } + } + } + + /** + * Close this DataBinder, which may result in throwing + * a BindException if it encountered any errors. + * @return the model Map, containing target object and Errors instance + * @throws BindException if there were any errors in the bind operation + * @see BindingResult#getModel() + */ + public Map close() throws BindException { + if (getBindingResult().hasErrors()) { + throw new BindException(getBindingResult()); + } + return getBindingResult().getModel(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/DefaultBindingErrorProcessor.java b/spring-context/src/main/java/org/springframework/validation/DefaultBindingErrorProcessor.java new file mode 100644 index 0000000..0b69e3f --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/DefaultBindingErrorProcessor.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation; + +import org.springframework.beans.PropertyAccessException; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Default {@link BindingErrorProcessor} implementation. + * + *

    Uses the "required" error code and the field name to resolve message codes + * for a missing field error. + * + *

    Creates a {@code FieldError} for each {@code PropertyAccessException} + * given, using the {@code PropertyAccessException}'s error code ("typeMismatch", + * "methodInvocation") for resolving message codes. + * + * @author Alef Arendsen + * @author Juergen Hoeller + * @since 1.2 + * @see #MISSING_FIELD_ERROR_CODE + * @see DataBinder#setBindingErrorProcessor + * @see BeanPropertyBindingResult#addError + * @see BeanPropertyBindingResult#resolveMessageCodes + * @see org.springframework.beans.PropertyAccessException#getErrorCode + * @see org.springframework.beans.TypeMismatchException#ERROR_CODE + * @see org.springframework.beans.MethodInvocationException#ERROR_CODE + */ +public class DefaultBindingErrorProcessor implements BindingErrorProcessor { + + /** + * Error code that a missing field error (i.e. a required field not + * found in the list of property values) will be registered with: + * "required". + */ + public static final String MISSING_FIELD_ERROR_CODE = "required"; + + + @Override + public void processMissingFieldError(String missingField, BindingResult bindingResult) { + // Create field error with code "required". + String fixedField = bindingResult.getNestedPath() + missingField; + String[] codes = bindingResult.resolveMessageCodes(MISSING_FIELD_ERROR_CODE, missingField); + Object[] arguments = getArgumentsForBindError(bindingResult.getObjectName(), fixedField); + FieldError error = new FieldError(bindingResult.getObjectName(), fixedField, "", true, + codes, arguments, "Field '" + fixedField + "' is required"); + bindingResult.addError(error); + } + + @Override + public void processPropertyAccessException(PropertyAccessException ex, BindingResult bindingResult) { + // Create field error with the exceptions's code, e.g. "typeMismatch". + String field = ex.getPropertyName(); + Assert.state(field != null, "No field in exception"); + String[] codes = bindingResult.resolveMessageCodes(ex.getErrorCode(), field); + Object[] arguments = getArgumentsForBindError(bindingResult.getObjectName(), field); + Object rejectedValue = ex.getValue(); + if (ObjectUtils.isArray(rejectedValue)) { + rejectedValue = StringUtils.arrayToCommaDelimitedString(ObjectUtils.toObjectArray(rejectedValue)); + } + FieldError error = new FieldError(bindingResult.getObjectName(), field, rejectedValue, true, + codes, arguments, ex.getLocalizedMessage()); + error.wrap(ex); + bindingResult.addError(error); + } + + /** + * Return FieldError arguments for a binding error on the given field. + * Invoked for each missing required field and each type mismatch. + *

    The default implementation returns a single argument indicating the field name + * (of type DefaultMessageSourceResolvable, with "objectName.field" and "field" as codes). + * @param objectName the name of the target object + * @param field the field that caused the binding error + * @return the Object array that represents the FieldError arguments + * @see org.springframework.validation.FieldError#getArguments + * @see org.springframework.context.support.DefaultMessageSourceResolvable + */ + protected Object[] getArgumentsForBindError(String objectName, String field) { + String[] codes = new String[] {objectName + Errors.NESTED_PATH_SEPARATOR + field, field}; + return new Object[] {new DefaultMessageSourceResolvable(codes, field)}; + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/DefaultMessageCodesResolver.java b/spring-context/src/main/java/org/springframework/validation/DefaultMessageCodesResolver.java new file mode 100644 index 0000000..15655c1 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/DefaultMessageCodesResolver.java @@ -0,0 +1,256 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.StringJoiner; + +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Default implementation of the {@link MessageCodesResolver} interface. + * + *

    Will create two message codes for an object error, in the following order (when + * using the {@link Format#PREFIX_ERROR_CODE prefixed} + * {@link #setMessageCodeFormatter(MessageCodeFormatter) formatter}): + *

      + *
    • 1.: code + "." + object name + *
    • 2.: code + *
    + * + *

    Will create four message codes for a field specification, in the following order: + *

      + *
    • 1.: code + "." + object name + "." + field + *
    • 2.: code + "." + field + *
    • 3.: code + "." + field type + *
    • 4.: code + *
    + * + *

    For example, in case of code "typeMismatch", object name "user", field "age": + *

      + *
    • 1. try "typeMismatch.user.age" + *
    • 2. try "typeMismatch.age" + *
    • 3. try "typeMismatch.int" + *
    • 4. try "typeMismatch" + *
    + * + *

    This resolution algorithm thus can be leveraged for example to show + * specific messages for binding errors like "required" and "typeMismatch": + *

      + *
    • at the object + field level ("age" field, but only on "user"); + *
    • at the field level (all "age" fields, no matter which object name); + *
    • or at the general level (all fields, on any object). + *
    + * + *

    In case of array, {@link List} or {@link java.util.Map} properties, + * both codes for specific elements and for the whole collection are + * generated. Assuming a field "name" of an array "groups" in object "user": + *

      + *
    • 1. try "typeMismatch.user.groups[0].name" + *
    • 2. try "typeMismatch.user.groups.name" + *
    • 3. try "typeMismatch.groups[0].name" + *
    • 4. try "typeMismatch.groups.name" + *
    • 5. try "typeMismatch.name" + *
    • 6. try "typeMismatch.java.lang.String" + *
    • 7. try "typeMismatch" + *
    + * + *

    By default the {@code errorCode}s will be placed at the beginning of constructed + * message strings. The {@link #setMessageCodeFormatter(MessageCodeFormatter) + * messageCodeFormatter} property can be used to specify an alternative concatenation + * {@link MessageCodeFormatter format}. + * + *

    In order to group all codes into a specific category within your resource bundles, + * e.g. "validation.typeMismatch.name" instead of the default "typeMismatch.name", + * consider specifying a {@link #setPrefix prefix} to be applied. + * + * @author Juergen Hoeller + * @author Phillip Webb + * @author Chris Beams + * @since 1.0.1 + */ +@SuppressWarnings("serial") +public class DefaultMessageCodesResolver implements MessageCodesResolver, Serializable { + + /** + * The separator that this implementation uses when resolving message codes. + */ + public static final String CODE_SEPARATOR = "."; + + private static final MessageCodeFormatter DEFAULT_FORMATTER = Format.PREFIX_ERROR_CODE; + + + private String prefix = ""; + + private MessageCodeFormatter formatter = DEFAULT_FORMATTER; + + + /** + * Specify a prefix to be applied to any code built by this resolver. + *

    Default is none. Specify, for example, "validation." to get + * error codes like "validation.typeMismatch.name". + */ + public void setPrefix(@Nullable String prefix) { + this.prefix = (prefix != null ? prefix : ""); + } + + /** + * Return the prefix to be applied to any code built by this resolver. + *

    Returns an empty String in case of no prefix. + */ + protected String getPrefix() { + return this.prefix; + } + + /** + * Specify the format for message codes built by this resolver. + *

    The default is {@link Format#PREFIX_ERROR_CODE}. + * @since 3.2 + * @see Format + */ + public void setMessageCodeFormatter(@Nullable MessageCodeFormatter formatter) { + this.formatter = (formatter != null ? formatter : DEFAULT_FORMATTER); + } + + + @Override + public String[] resolveMessageCodes(String errorCode, String objectName) { + return resolveMessageCodes(errorCode, objectName, "", null); + } + + /** + * Build the code list for the given code and field: an + * object/field-specific code, a field-specific code, a plain error code. + *

    Arrays, Lists and Maps are resolved both for specific elements and + * the whole collection. + *

    See the {@link DefaultMessageCodesResolver class level javadoc} for + * details on the generated codes. + * @return the list of codes + */ + @Override + public String[] resolveMessageCodes(String errorCode, String objectName, String field, @Nullable Class fieldType) { + Set codeList = new LinkedHashSet<>(); + List fieldList = new ArrayList<>(); + buildFieldList(field, fieldList); + addCodes(codeList, errorCode, objectName, fieldList); + int dotIndex = field.lastIndexOf('.'); + if (dotIndex != -1) { + buildFieldList(field.substring(dotIndex + 1), fieldList); + } + addCodes(codeList, errorCode, null, fieldList); + if (fieldType != null) { + addCode(codeList, errorCode, null, fieldType.getName()); + } + addCode(codeList, errorCode, null, null); + return StringUtils.toStringArray(codeList); + } + + private void addCodes(Collection codeList, String errorCode, @Nullable String objectName, Iterable fields) { + for (String field : fields) { + addCode(codeList, errorCode, objectName, field); + } + } + + private void addCode(Collection codeList, String errorCode, @Nullable String objectName, @Nullable String field) { + codeList.add(postProcessMessageCode(this.formatter.format(errorCode, objectName, field))); + } + + /** + * Add both keyed and non-keyed entries for the supplied {@code field} + * to the supplied field list. + */ + protected void buildFieldList(String field, List fieldList) { + fieldList.add(field); + String plainField = field; + int keyIndex = plainField.lastIndexOf('['); + while (keyIndex != -1) { + int endKeyIndex = plainField.indexOf(']', keyIndex); + if (endKeyIndex != -1) { + plainField = plainField.substring(0, keyIndex) + plainField.substring(endKeyIndex + 1); + fieldList.add(plainField); + keyIndex = plainField.lastIndexOf('['); + } + else { + keyIndex = -1; + } + } + } + + /** + * Post-process the given message code, built by this resolver. + *

    The default implementation applies the specified prefix, if any. + * @param code the message code as built by this resolver + * @return the final message code to be returned + * @see #setPrefix + */ + protected String postProcessMessageCode(String code) { + return getPrefix() + code; + } + + + /** + * Common message code formats. + * @see MessageCodeFormatter + * @see DefaultMessageCodesResolver#setMessageCodeFormatter(MessageCodeFormatter) + */ + public enum Format implements MessageCodeFormatter { + + /** + * Prefix the error code at the beginning of the generated message code. e.g.: + * {@code errorCode + "." + object name + "." + field} + */ + PREFIX_ERROR_CODE { + @Override + public String format(String errorCode, @Nullable String objectName, @Nullable String field) { + return toDelimitedString(errorCode, objectName, field); + } + }, + + /** + * Postfix the error code at the end of the generated message code. e.g.: + * {@code object name + "." + field + "." + errorCode} + */ + POSTFIX_ERROR_CODE { + @Override + public String format(String errorCode, @Nullable String objectName, @Nullable String field) { + return toDelimitedString(objectName, field, errorCode); + } + }; + + /** + * Concatenate the given elements, delimiting each with + * {@link DefaultMessageCodesResolver#CODE_SEPARATOR}, skipping zero-length or + * null elements altogether. + */ + public static String toDelimitedString(String... elements) { + StringJoiner rtn = new StringJoiner(CODE_SEPARATOR); + for (String element : elements) { + if (StringUtils.hasLength(element)) { + rtn.add(element); + } + } + return rtn.toString(); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/DirectFieldBindingResult.java b/spring-context/src/main/java/org/springframework/validation/DirectFieldBindingResult.java new file mode 100644 index 0000000..5ad401d --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/DirectFieldBindingResult.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation; + +import org.springframework.beans.ConfigurablePropertyAccessor; +import org.springframework.beans.PropertyAccessorFactory; +import org.springframework.lang.Nullable; + +/** + * Special implementation of the Errors and BindingResult interfaces, + * supporting registration and evaluation of binding errors on value objects. + * Performs direct field access instead of going through JavaBean getters. + * + *

    Since Spring 4.1 this implementation is able to traverse nested fields. + * + * @author Juergen Hoeller + * @since 2.0 + * @see DataBinder#getBindingResult() + * @see DataBinder#initDirectFieldAccess() + * @see BeanPropertyBindingResult + */ +@SuppressWarnings("serial") +public class DirectFieldBindingResult extends AbstractPropertyBindingResult { + + @Nullable + private final Object target; + + private final boolean autoGrowNestedPaths; + + @Nullable + private transient ConfigurablePropertyAccessor directFieldAccessor; + + + /** + * Create a new DirectFieldBindingResult instance. + * @param target the target object to bind onto + * @param objectName the name of the target object + */ + public DirectFieldBindingResult(@Nullable Object target, String objectName) { + this(target, objectName, true); + } + + /** + * Create a new DirectFieldBindingResult instance. + * @param target the target object to bind onto + * @param objectName the name of the target object + * @param autoGrowNestedPaths whether to "auto-grow" a nested path that contains a null value + */ + public DirectFieldBindingResult(@Nullable Object target, String objectName, boolean autoGrowNestedPaths) { + super(objectName); + this.target = target; + this.autoGrowNestedPaths = autoGrowNestedPaths; + } + + + @Override + @Nullable + public final Object getTarget() { + return this.target; + } + + /** + * Returns the DirectFieldAccessor that this instance uses. + * Creates a new one if none existed before. + * @see #createDirectFieldAccessor() + */ + @Override + public final ConfigurablePropertyAccessor getPropertyAccessor() { + if (this.directFieldAccessor == null) { + this.directFieldAccessor = createDirectFieldAccessor(); + this.directFieldAccessor.setExtractOldValueForEditor(true); + this.directFieldAccessor.setAutoGrowNestedPaths(this.autoGrowNestedPaths); + } + return this.directFieldAccessor; + } + + /** + * Create a new DirectFieldAccessor for the underlying target object. + * @see #getTarget() + */ + protected ConfigurablePropertyAccessor createDirectFieldAccessor() { + if (this.target == null) { + throw new IllegalStateException("Cannot access fields on null target instance '" + getObjectName() + "'"); + } + return PropertyAccessorFactory.forDirectFieldAccess(this.target); + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/Errors.java b/spring-context/src/main/java/org/springframework/validation/Errors.java new file mode 100644 index 0000000..ebaa946 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/Errors.java @@ -0,0 +1,308 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation; + +import java.util.List; + +import org.springframework.beans.PropertyAccessor; +import org.springframework.lang.Nullable; + +/** + * Stores and exposes information about data-binding and validation + * errors for a specific object. + * + *

    Field names can be properties of the target object (e.g. "name" + * when binding to a customer object), or nested fields in case of + * subobjects (e.g. "address.street"). Supports subtree navigation + * via {@link #setNestedPath(String)}: for example, an + * {@code AddressValidator} validates "address", not being aware + * that this is a subobject of customer. + * + *

    Note: {@code Errors} objects are single-threaded. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #setNestedPath + * @see BindException + * @see DataBinder + * @see ValidationUtils + */ +public interface Errors { + + /** + * The separator between path elements in a nested path, + * for example in "customer.name" or "customer.address.street". + *

    "." = same as the + * {@link org.springframework.beans.PropertyAccessor#NESTED_PROPERTY_SEPARATOR nested property separator} + * in the beans package. + */ + String NESTED_PATH_SEPARATOR = PropertyAccessor.NESTED_PROPERTY_SEPARATOR; + + + /** + * Return the name of the bound root object. + */ + String getObjectName(); + + /** + * Allow context to be changed so that standard validators can validate + * subtrees. Reject calls prepend the given path to the field names. + *

    For example, an address validator could validate the subobject + * "address" of a customer object. + * @param nestedPath nested path within this object, + * e.g. "address" (defaults to "", {@code null} is also acceptable). + * Can end with a dot: both "address" and "address." are valid. + */ + void setNestedPath(String nestedPath); + + /** + * Return the current nested path of this {@link Errors} object. + *

    Returns a nested path with a dot, i.e. "address.", for easy + * building of concatenated paths. Default is an empty String. + */ + String getNestedPath(); + + /** + * Push the given sub path onto the nested path stack. + *

    A {@link #popNestedPath()} call will reset the original + * nested path before the corresponding + * {@code pushNestedPath(String)} call. + *

    Using the nested path stack allows to set temporary nested paths + * for subobjects without having to worry about a temporary path holder. + *

    For example: current path "spouse.", pushNestedPath("child") -> + * result path "spouse.child."; popNestedPath() -> "spouse." again. + * @param subPath the sub path to push onto the nested path stack + * @see #popNestedPath + */ + void pushNestedPath(String subPath); + + /** + * Pop the former nested path from the nested path stack. + * @throws IllegalStateException if there is no former nested path on the stack + * @see #pushNestedPath + */ + void popNestedPath() throws IllegalStateException; + + /** + * Register a global error for the entire target object, + * using the given error description. + * @param errorCode error code, interpretable as a message key + */ + void reject(String errorCode); + + /** + * Register a global error for the entire target object, + * using the given error description. + * @param errorCode error code, interpretable as a message key + * @param defaultMessage fallback default message + */ + void reject(String errorCode, String defaultMessage); + + /** + * Register a global error for the entire target object, + * using the given error description. + * @param errorCode error code, interpretable as a message key + * @param errorArgs error arguments, for argument binding via MessageFormat + * (can be {@code null}) + * @param defaultMessage fallback default message + */ + void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage); + + /** + * Register a field error for the specified field of the current object + * (respecting the current nested path, if any), using the given error + * description. + *

    The field name may be {@code null} or empty String to indicate + * the current object itself rather than a field of it. This may result + * in a corresponding field error within the nested object graph or a + * global error if the current object is the top object. + * @param field the field name (may be {@code null} or empty String) + * @param errorCode error code, interpretable as a message key + * @see #getNestedPath() + */ + void rejectValue(@Nullable String field, String errorCode); + + /** + * Register a field error for the specified field of the current object + * (respecting the current nested path, if any), using the given error + * description. + *

    The field name may be {@code null} or empty String to indicate + * the current object itself rather than a field of it. This may result + * in a corresponding field error within the nested object graph or a + * global error if the current object is the top object. + * @param field the field name (may be {@code null} or empty String) + * @param errorCode error code, interpretable as a message key + * @param defaultMessage fallback default message + * @see #getNestedPath() + */ + void rejectValue(@Nullable String field, String errorCode, String defaultMessage); + + /** + * Register a field error for the specified field of the current object + * (respecting the current nested path, if any), using the given error + * description. + *

    The field name may be {@code null} or empty String to indicate + * the current object itself rather than a field of it. This may result + * in a corresponding field error within the nested object graph or a + * global error if the current object is the top object. + * @param field the field name (may be {@code null} or empty String) + * @param errorCode error code, interpretable as a message key + * @param errorArgs error arguments, for argument binding via MessageFormat + * (can be {@code null}) + * @param defaultMessage fallback default message + * @see #getNestedPath() + */ + void rejectValue(@Nullable String field, String errorCode, + @Nullable Object[] errorArgs, @Nullable String defaultMessage); + + /** + * Add all errors from the given {@code Errors} instance to this + * {@code Errors} instance. + *

    This is a convenience method to avoid repeated {@code reject(..)} + * calls for merging an {@code Errors} instance into another + * {@code Errors} instance. + *

    Note that the passed-in {@code Errors} instance is supposed + * to refer to the same target object, or at least contain compatible errors + * that apply to the target object of this {@code Errors} instance. + * @param errors the {@code Errors} instance to merge in + */ + void addAllErrors(Errors errors); + + /** + * Return if there were any errors. + */ + boolean hasErrors(); + + /** + * Return the total number of errors. + */ + int getErrorCount(); + + /** + * Get all errors, both global and field ones. + * @return a list of {@link ObjectError} instances + */ + List getAllErrors(); + + /** + * Are there any global errors? + * @return {@code true} if there are any global errors + * @see #hasFieldErrors() + */ + boolean hasGlobalErrors(); + + /** + * Return the number of global errors. + * @return the number of global errors + * @see #getFieldErrorCount() + */ + int getGlobalErrorCount(); + + /** + * Get all global errors. + * @return a list of {@link ObjectError} instances + */ + List getGlobalErrors(); + + /** + * Get the first global error, if any. + * @return the global error, or {@code null} + */ + @Nullable + ObjectError getGlobalError(); + + /** + * Are there any field errors? + * @return {@code true} if there are any errors associated with a field + * @see #hasGlobalErrors() + */ + boolean hasFieldErrors(); + + /** + * Return the number of errors associated with a field. + * @return the number of errors associated with a field + * @see #getGlobalErrorCount() + */ + int getFieldErrorCount(); + + /** + * Get all errors associated with a field. + * @return a List of {@link FieldError} instances + */ + List getFieldErrors(); + + /** + * Get the first error associated with a field, if any. + * @return the field-specific error, or {@code null} + */ + @Nullable + FieldError getFieldError(); + + /** + * Are there any errors associated with the given field? + * @param field the field name + * @return {@code true} if there were any errors associated with the given field + */ + boolean hasFieldErrors(String field); + + /** + * Return the number of errors associated with the given field. + * @param field the field name + * @return the number of errors associated with the given field + */ + int getFieldErrorCount(String field); + + /** + * Get all errors associated with the given field. + *

    Implementations should support not only full field names like + * "name" but also pattern matches like "na*" or "address.*". + * @param field the field name + * @return a List of {@link FieldError} instances + */ + List getFieldErrors(String field); + + /** + * Get the first error associated with the given field, if any. + * @param field the field name + * @return the field-specific error, or {@code null} + */ + @Nullable + FieldError getFieldError(String field); + + /** + * Return the current value of the given field, either the current + * bean property value or a rejected update from the last binding. + *

    Allows for convenient access to user-specified field values, + * even if there were type mismatches. + * @param field the field name + * @return the current value of the given field + */ + @Nullable + Object getFieldValue(String field); + + /** + * Return the type of a given field. + *

    Implementations should be able to determine the type even + * when the field value is {@code null}, for example from some + * associated descriptor. + * @param field the field name + * @return the type of the field, or {@code null} if not determinable + */ + @Nullable + Class getFieldType(String field); + +} diff --git a/spring-context/src/main/java/org/springframework/validation/FieldError.java b/spring-context/src/main/java/org/springframework/validation/FieldError.java new file mode 100644 index 0000000..a6cd51a --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/FieldError.java @@ -0,0 +1,132 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Encapsulates a field error, that is, a reason for rejecting a specific + * field value. + * + *

    See the {@link DefaultMessageCodesResolver} javadoc for details on + * how a message code list is built for a {@code FieldError}. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 10.03.2003 + * @see DefaultMessageCodesResolver + */ +@SuppressWarnings("serial") +public class FieldError extends ObjectError { + + private final String field; + + @Nullable + private final Object rejectedValue; + + private final boolean bindingFailure; + + + /** + * Create a new FieldError instance. + * @param objectName the name of the affected object + * @param field the affected field of the object + * @param defaultMessage the default message to be used to resolve this message + */ + public FieldError(String objectName, String field, String defaultMessage) { + this(objectName, field, null, false, null, null, defaultMessage); + } + + /** + * Create a new FieldError instance. + * @param objectName the name of the affected object + * @param field the affected field of the object + * @param rejectedValue the rejected field value + * @param bindingFailure whether this error represents a binding failure + * (like a type mismatch); else, it is a validation failure + * @param codes the codes to be used to resolve this message + * @param arguments the array of arguments to be used to resolve this message + * @param defaultMessage the default message to be used to resolve this message + */ + public FieldError(String objectName, String field, @Nullable Object rejectedValue, boolean bindingFailure, + @Nullable String[] codes, @Nullable Object[] arguments, @Nullable String defaultMessage) { + + super(objectName, codes, arguments, defaultMessage); + Assert.notNull(field, "Field must not be null"); + this.field = field; + this.rejectedValue = rejectedValue; + this.bindingFailure = bindingFailure; + } + + + /** + * Return the affected field of the object. + */ + public String getField() { + return this.field; + } + + /** + * Return the rejected field value. + */ + @Nullable + public Object getRejectedValue() { + return this.rejectedValue; + } + + /** + * Return whether this error represents a binding failure + * (like a type mismatch); otherwise it is a validation failure. + */ + public boolean isBindingFailure() { + return this.bindingFailure; + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!super.equals(other)) { + return false; + } + FieldError otherError = (FieldError) other; + return (getField().equals(otherError.getField()) && + ObjectUtils.nullSafeEquals(getRejectedValue(), otherError.getRejectedValue()) && + isBindingFailure() == otherError.isBindingFailure()); + } + + @Override + public int hashCode() { + int hashCode = super.hashCode(); + hashCode = 29 * hashCode + getField().hashCode(); + hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getRejectedValue()); + hashCode = 29 * hashCode + (isBindingFailure() ? 1 : 0); + return hashCode; + } + + @Override + public String toString() { + return "Field error in object '" + getObjectName() + "' on field '" + this.field + + "': rejected value [" + ObjectUtils.nullSafeToString(this.rejectedValue) + "]; " + + resolvableToString(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/MapBindingResult.java b/spring-context/src/main/java/org/springframework/validation/MapBindingResult.java new file mode 100644 index 0000000..41860a4 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/MapBindingResult.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation; + +import java.io.Serializable; +import java.util.Map; + +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Map-based implementation of the BindingResult interface, + * supporting registration and evaluation of binding errors on + * Map attributes. + * + *

    Can be used as errors holder for custom binding onto a + * Map, for example when invoking a Validator for a Map object. + * + * @author Juergen Hoeller + * @since 2.0 + * @see java.util.Map + */ +@SuppressWarnings("serial") +public class MapBindingResult extends AbstractBindingResult implements Serializable { + + private final Map target; + + + /** + * Create a new MapBindingResult instance. + * @param target the target Map to bind onto + * @param objectName the name of the target object + */ + public MapBindingResult(Map target, String objectName) { + super(objectName); + Assert.notNull(target, "Target Map must not be null"); + this.target = target; + } + + + /** + * Return the target Map to bind onto. + */ + public final Map getTargetMap() { + return this.target; + } + + @Override + @NonNull + public final Object getTarget() { + return this.target; + } + + @Override + @Nullable + protected Object getActualFieldValue(String field) { + return this.target.get(field); + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/MessageCodeFormatter.java b/spring-context/src/main/java/org/springframework/validation/MessageCodeFormatter.java new file mode 100644 index 0000000..e8db792 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/MessageCodeFormatter.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation; + +import org.springframework.lang.Nullable; + +/** + * A strategy interface for formatting message codes. + * + * @author Chris Beams + * @since 3.2 + * @see DefaultMessageCodesResolver + * @see DefaultMessageCodesResolver.Format + */ +@FunctionalInterface +public interface MessageCodeFormatter { + + /** + * Build and return a message code consisting of the given fields, + * usually delimited by {@link DefaultMessageCodesResolver#CODE_SEPARATOR}. + * @param errorCode e.g.: "typeMismatch" + * @param objectName e.g.: "user" + * @param field e.g. "age" + * @return concatenated message code, e.g.: "typeMismatch.user.age" + * @see DefaultMessageCodesResolver.Format + */ + String format(String errorCode, @Nullable String objectName, @Nullable String field); + +} diff --git a/spring-context/src/main/java/org/springframework/validation/MessageCodesResolver.java b/spring-context/src/main/java/org/springframework/validation/MessageCodesResolver.java new file mode 100644 index 0000000..2a985e6 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/MessageCodesResolver.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation; + +import org.springframework.lang.Nullable; + +/** + * Strategy interface for building message codes from validation error codes. + * Used by DataBinder to build the codes list for ObjectErrors and FieldErrors. + * + *

    The resulting message codes correspond to the codes of a + * MessageSourceResolvable (as implemented by ObjectError and FieldError). + * + * @author Juergen Hoeller + * @since 1.0.1 + * @see DataBinder#setMessageCodesResolver + * @see ObjectError + * @see FieldError + * @see org.springframework.context.MessageSourceResolvable#getCodes() + */ +public interface MessageCodesResolver { + + /** + * Build message codes for the given error code and object name. + * Used for building the codes list of an ObjectError. + * @param errorCode the error code used for rejecting the object + * @param objectName the name of the object + * @return the message codes to use + */ + String[] resolveMessageCodes(String errorCode, String objectName); + + /** + * Build message codes for the given error code and field specification. + * Used for building the codes list of an FieldError. + * @param errorCode the error code used for rejecting the value + * @param objectName the name of the object + * @param field the field name + * @param fieldType the field type (may be {@code null} if not determinable) + * @return the message codes to use + */ + String[] resolveMessageCodes(String errorCode, String objectName, String field, @Nullable Class fieldType); + +} diff --git a/spring-context/src/main/java/org/springframework/validation/ObjectError.java b/spring-context/src/main/java/org/springframework/validation/ObjectError.java new file mode 100644 index 0000000..227cd08 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/ObjectError.java @@ -0,0 +1,155 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation; + +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Encapsulates an object error, that is, a global reason for rejecting + * an object. + * + *

    See the {@link DefaultMessageCodesResolver} javadoc for details on + * how a message code list is built for an {@code ObjectError}. + * + * @author Juergen Hoeller + * @since 10.03.2003 + * @see FieldError + * @see DefaultMessageCodesResolver + */ +@SuppressWarnings("serial") +public class ObjectError extends DefaultMessageSourceResolvable { + + private final String objectName; + + @Nullable + private transient Object source; + + + /** + * Create a new instance of the ObjectError class. + * @param objectName the name of the affected object + * @param defaultMessage the default message to be used to resolve this message + */ + public ObjectError(String objectName, String defaultMessage) { + this(objectName, null, null, defaultMessage); + } + + /** + * Create a new instance of the ObjectError class. + * @param objectName the name of the affected object + * @param codes the codes to be used to resolve this message + * @param arguments the array of arguments to be used to resolve this message + * @param defaultMessage the default message to be used to resolve this message + */ + public ObjectError( + String objectName, @Nullable String[] codes, @Nullable Object[] arguments, @Nullable String defaultMessage) { + + super(codes, arguments, defaultMessage); + Assert.notNull(objectName, "Object name must not be null"); + this.objectName = objectName; + } + + + /** + * Return the name of the affected object. + */ + public String getObjectName() { + return this.objectName; + } + + /** + * Preserve the source behind this error: possibly an {@link Exception} + * (typically {@link org.springframework.beans.PropertyAccessException}) + * or a Bean Validation {@link javax.validation.ConstraintViolation}. + *

    Note that any such source object is being stored as transient: + * that is, it won't be part of a serialized error representation. + * @param source the source object + * @since 5.0.4 + */ + public void wrap(Object source) { + if (this.source != null) { + throw new IllegalStateException("Already wrapping " + this.source); + } + this.source = source; + } + + /** + * Unwrap the source behind this error: possibly an {@link Exception} + * (typically {@link org.springframework.beans.PropertyAccessException}) + * or a Bean Validation {@link javax.validation.ConstraintViolation}. + *

    The cause of the outermost exception will be introspected as well, + * e.g. the underlying conversion exception or exception thrown from a setter + * (instead of having to unwrap the {@code PropertyAccessException} in turn). + * @return the source object of the given type + * @throws IllegalArgumentException if no such source object is available + * (i.e. none specified or not available anymore after deserialization) + * @since 5.0.4 + */ + public T unwrap(Class sourceType) { + if (sourceType.isInstance(this.source)) { + return sourceType.cast(this.source); + } + else if (this.source instanceof Throwable) { + Throwable cause = ((Throwable) this.source).getCause(); + if (sourceType.isInstance(cause)) { + return sourceType.cast(cause); + } + } + throw new IllegalArgumentException("No source object of the given type available: " + sourceType); + } + + /** + * Check the source behind this error: possibly an {@link Exception} + * (typically {@link org.springframework.beans.PropertyAccessException}) + * or a Bean Validation {@link javax.validation.ConstraintViolation}. + *

    The cause of the outermost exception will be introspected as well, + * e.g. the underlying conversion exception or exception thrown from a setter + * (instead of having to unwrap the {@code PropertyAccessException} in turn). + * @return whether this error has been caused by a source object of the given type + * @since 5.0.4 + */ + public boolean contains(Class sourceType) { + return (sourceType.isInstance(this.source) || + (this.source instanceof Throwable && sourceType.isInstance(((Throwable) this.source).getCause()))); + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null || other.getClass() != getClass() || !super.equals(other)) { + return false; + } + ObjectError otherError = (ObjectError) other; + return getObjectName().equals(otherError.getObjectName()); + } + + @Override + public int hashCode() { + return (29 * super.hashCode() + getObjectName().hashCode()); + } + + @Override + public String toString() { + return "Error in object '" + this.objectName + "': " + resolvableToString(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/SmartValidator.java b/spring-context/src/main/java/org/springframework/validation/SmartValidator.java new file mode 100644 index 0000000..4cc1700 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/SmartValidator.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation; + +import org.springframework.lang.Nullable; + +/** + * Extended variant of the {@link Validator} interface, adding support for + * validation 'hints'. + * + * @author Juergen Hoeller + * @author Sam Brannen + * @since 3.1 + */ +public interface SmartValidator extends Validator { + + /** + * Validate the supplied {@code target} object, which must be of a type of {@link Class} + * for which the {@link #supports(Class)} method typically returns {@code true}. + *

    The supplied {@link Errors errors} instance can be used to report any + * resulting validation errors. + *

    This variant of {@code validate()} supports validation hints, such as + * validation groups against a JSR-303 provider (in which case, the provided hint + * objects need to be annotation arguments of type {@code Class}). + *

    Note: Validation hints may get ignored by the actual target {@code Validator}, + * in which case this method should behave just like its regular + * {@link #validate(Object, Errors)} sibling. + * @param target the object that is to be validated + * @param errors contextual state about the validation process + * @param validationHints one or more hint objects to be passed to the validation engine + * @see javax.validation.Validator#validate(Object, Class[]) + */ + void validate(Object target, Errors errors, Object... validationHints); + + /** + * Validate the supplied value for the specified field on the target type, + * reporting the same validation errors as if the value would be bound to + * the field on an instance of the target class. + * @param targetType the target type + * @param fieldName the name of the field + * @param value the candidate value + * @param errors contextual state about the validation process + * @param validationHints one or more hint objects to be passed to the validation engine + * @since 5.1 + * @see javax.validation.Validator#validateValue(Class, String, Object, Class[]) + */ + default void validateValue( + Class targetType, String fieldName, @Nullable Object value, Errors errors, Object... validationHints) { + + throw new IllegalArgumentException("Cannot validate individual value for " + targetType); + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/ValidationUtils.java b/spring-context/src/main/java/org/springframework/validation/ValidationUtils.java new file mode 100644 index 0000000..8ed9b56 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/ValidationUtils.java @@ -0,0 +1,258 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Utility class offering convenient methods for invoking a {@link Validator} + * and for rejecting empty fields. + * + *

    Checks for an empty field in {@code Validator} implementations can become + * one-liners when using {@link #rejectIfEmpty} or {@link #rejectIfEmptyOrWhitespace}. + * + * @author Juergen Hoeller + * @author Dmitriy Kopylenko + * @since 06.05.2003 + * @see Validator + * @see Errors + */ +public abstract class ValidationUtils { + + private static final Log logger = LogFactory.getLog(ValidationUtils.class); + + + /** + * Invoke the given {@link Validator} for the supplied object and + * {@link Errors} instance. + * @param validator the {@code Validator} to be invoked + * @param target the object to bind the parameters to + * @param errors the {@link Errors} instance that should store the errors + * @throws IllegalArgumentException if either of the {@code Validator} or {@code Errors} + * arguments is {@code null}, or if the supplied {@code Validator} does not + * {@link Validator#supports(Class) support} the validation of the supplied object's type + */ + public static void invokeValidator(Validator validator, Object target, Errors errors) { + invokeValidator(validator, target, errors, (Object[]) null); + } + + /** + * Invoke the given {@link Validator}/{@link SmartValidator} for the supplied object and + * {@link Errors} instance. + * @param validator the {@code Validator} to be invoked + * @param target the object to bind the parameters to + * @param errors the {@link Errors} instance that should store the errors + * @param validationHints one or more hint objects to be passed to the validation engine + * @throws IllegalArgumentException if either of the {@code Validator} or {@code Errors} + * arguments is {@code null}, or if the supplied {@code Validator} does not + * {@link Validator#supports(Class) support} the validation of the supplied object's type + */ + public static void invokeValidator( + Validator validator, Object target, Errors errors, @Nullable Object... validationHints) { + + Assert.notNull(validator, "Validator must not be null"); + Assert.notNull(target, "Target object must not be null"); + Assert.notNull(errors, "Errors object must not be null"); + + if (logger.isDebugEnabled()) { + logger.debug("Invoking validator [" + validator + "]"); + } + if (!validator.supports(target.getClass())) { + throw new IllegalArgumentException( + "Validator [" + validator.getClass() + "] does not support [" + target.getClass() + "]"); + } + + if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) { + ((SmartValidator) validator).validate(target, errors, validationHints); + } + else { + validator.validate(target, errors); + } + + if (logger.isDebugEnabled()) { + if (errors.hasErrors()) { + logger.debug("Validator found " + errors.getErrorCount() + " errors"); + } + else { + logger.debug("Validator found no errors"); + } + } + } + + + /** + * Reject the given field with the given error code if the value is empty. + *

    An 'empty' value in this context means either {@code null} or + * the empty string "". + *

    The object whose field is being validated does not need to be passed + * in because the {@link Errors} instance can resolve field values by itself + * (it will usually hold an internal reference to the target object). + * @param errors the {@code Errors} instance to register errors on + * @param field the field name to check + * @param errorCode the error code, interpretable as message key + */ + public static void rejectIfEmpty(Errors errors, String field, String errorCode) { + rejectIfEmpty(errors, field, errorCode, null, null); + } + + /** + * Reject the given field with the given error code and default message + * if the value is empty. + *

    An 'empty' value in this context means either {@code null} or + * the empty string "". + *

    The object whose field is being validated does not need to be passed + * in because the {@link Errors} instance can resolve field values by itself + * (it will usually hold an internal reference to the target object). + * @param errors the {@code Errors} instance to register errors on + * @param field the field name to check + * @param errorCode error code, interpretable as message key + * @param defaultMessage fallback default message + */ + public static void rejectIfEmpty(Errors errors, String field, String errorCode, String defaultMessage) { + rejectIfEmpty(errors, field, errorCode, null, defaultMessage); + } + + /** + * Reject the given field with the given error code and error arguments + * if the value is empty. + *

    An 'empty' value in this context means either {@code null} or + * the empty string "". + *

    The object whose field is being validated does not need to be passed + * in because the {@link Errors} instance can resolve field values by itself + * (it will usually hold an internal reference to the target object). + * @param errors the {@code Errors} instance to register errors on + * @param field the field name to check + * @param errorCode the error code, interpretable as message key + * @param errorArgs the error arguments, for argument binding via MessageFormat + * (can be {@code null}) + */ + public static void rejectIfEmpty(Errors errors, String field, String errorCode, Object[] errorArgs) { + rejectIfEmpty(errors, field, errorCode, errorArgs, null); + } + + /** + * Reject the given field with the given error code, error arguments + * and default message if the value is empty. + *

    An 'empty' value in this context means either {@code null} or + * the empty string "". + *

    The object whose field is being validated does not need to be passed + * in because the {@link Errors} instance can resolve field values by itself + * (it will usually hold an internal reference to the target object). + * @param errors the {@code Errors} instance to register errors on + * @param field the field name to check + * @param errorCode the error code, interpretable as message key + * @param errorArgs the error arguments, for argument binding via MessageFormat + * (can be {@code null}) + * @param defaultMessage fallback default message + */ + public static void rejectIfEmpty(Errors errors, String field, String errorCode, + @Nullable Object[] errorArgs, @Nullable String defaultMessage) { + + Assert.notNull(errors, "Errors object must not be null"); + Object value = errors.getFieldValue(field); + if (value == null || !StringUtils.hasLength(value.toString())) { + errors.rejectValue(field, errorCode, errorArgs, defaultMessage); + } + } + + /** + * Reject the given field with the given error code if the value is empty + * or just contains whitespace. + *

    An 'empty' value in this context means either {@code null}, + * the empty string "", or consisting wholly of whitespace. + *

    The object whose field is being validated does not need to be passed + * in because the {@link Errors} instance can resolve field values by itself + * (it will usually hold an internal reference to the target object). + * @param errors the {@code Errors} instance to register errors on + * @param field the field name to check + * @param errorCode the error code, interpretable as message key + */ + public static void rejectIfEmptyOrWhitespace(Errors errors, String field, String errorCode) { + rejectIfEmptyOrWhitespace(errors, field, errorCode, null, null); + } + + /** + * Reject the given field with the given error code and default message + * if the value is empty or just contains whitespace. + *

    An 'empty' value in this context means either {@code null}, + * the empty string "", or consisting wholly of whitespace. + *

    The object whose field is being validated does not need to be passed + * in because the {@link Errors} instance can resolve field values by itself + * (it will usually hold an internal reference to the target object). + * @param errors the {@code Errors} instance to register errors on + * @param field the field name to check + * @param errorCode the error code, interpretable as message key + * @param defaultMessage fallback default message + */ + public static void rejectIfEmptyOrWhitespace( + Errors errors, String field, String errorCode, String defaultMessage) { + + rejectIfEmptyOrWhitespace(errors, field, errorCode, null, defaultMessage); + } + + /** + * Reject the given field with the given error code and error arguments + * if the value is empty or just contains whitespace. + *

    An 'empty' value in this context means either {@code null}, + * the empty string "", or consisting wholly of whitespace. + *

    The object whose field is being validated does not need to be passed + * in because the {@link Errors} instance can resolve field values by itself + * (it will usually hold an internal reference to the target object). + * @param errors the {@code Errors} instance to register errors on + * @param field the field name to check + * @param errorCode the error code, interpretable as message key + * @param errorArgs the error arguments, for argument binding via MessageFormat + * (can be {@code null}) + */ + public static void rejectIfEmptyOrWhitespace( + Errors errors, String field, String errorCode, @Nullable Object[] errorArgs) { + + rejectIfEmptyOrWhitespace(errors, field, errorCode, errorArgs, null); + } + + /** + * Reject the given field with the given error code, error arguments + * and default message if the value is empty or just contains whitespace. + *

    An 'empty' value in this context means either {@code null}, + * the empty string "", or consisting wholly of whitespace. + *

    The object whose field is being validated does not need to be passed + * in because the {@link Errors} instance can resolve field values by itself + * (it will usually hold an internal reference to the target object). + * @param errors the {@code Errors} instance to register errors on + * @param field the field name to check + * @param errorCode the error code, interpretable as message key + * @param errorArgs the error arguments, for argument binding via MessageFormat + * (can be {@code null}) + * @param defaultMessage fallback default message + */ + public static void rejectIfEmptyOrWhitespace( + Errors errors, String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage) { + + Assert.notNull(errors, "Errors object must not be null"); + Object value = errors.getFieldValue(field); + if (value == null ||!StringUtils.hasText(value.toString())) { + errors.rejectValue(field, errorCode, errorArgs, defaultMessage); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/Validator.java b/spring-context/src/main/java/org/springframework/validation/Validator.java new file mode 100644 index 0000000..d95b054 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/Validator.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation; + +/** + * A validator for application-specific objects. + * + *

    This interface is totally divorced from any infrastructure + * or context; that is to say it is not coupled to validating + * only objects in the web tier, the data-access tier, or the + * whatever-tier. As such it is amenable to being used in any layer + * of an application, and supports the encapsulation of validation + * logic as a first-class citizen in its own right. + * + *

    Find below a simple but complete {@code Validator} + * implementation, which validates that the various {@link String} + * properties of a {@code UserLogin} instance are not empty + * (that is they are not {@code null} and do not consist + * wholly of whitespace), and that any password that is present is + * at least {@code 'MINIMUM_PASSWORD_LENGTH'} characters in length. + * + *

     public class UserLoginValidator implements Validator {
    + *
    + *    private static final int MINIMUM_PASSWORD_LENGTH = 6;
    + *
    + *    public boolean supports(Class clazz) {
    + *       return UserLogin.class.isAssignableFrom(clazz);
    + *    }
    + *
    + *    public void validate(Object target, Errors errors) {
    + *       ValidationUtils.rejectIfEmptyOrWhitespace(errors, "userName", "field.required");
    + *       ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "field.required");
    + *       UserLogin login = (UserLogin) target;
    + *       if (login.getPassword() != null
    + *             && login.getPassword().trim().length() < MINIMUM_PASSWORD_LENGTH) {
    + *          errors.rejectValue("password", "field.min.length",
    + *                new Object[]{Integer.valueOf(MINIMUM_PASSWORD_LENGTH)},
    + *                "The password must be at least [" + MINIMUM_PASSWORD_LENGTH + "] characters in length.");
    + *       }
    + *    }
    + * }
    + * + *

    See also the Spring reference manual for a fuller discussion of + * the {@code Validator} interface and its role in an enterprise + * application. + * + * @author Rod Johnson + * @see SmartValidator + * @see Errors + * @see ValidationUtils + */ +public interface Validator { + + /** + * Can this {@link Validator} {@link #validate(Object, Errors) validate} + * instances of the supplied {@code clazz}? + *

    This method is typically implemented like so: + *

    return Foo.class.isAssignableFrom(clazz);
    + * (Where {@code Foo} is the class (or superclass) of the actual + * object instance that is to be {@link #validate(Object, Errors) validated}.) + * @param clazz the {@link Class} that this {@link Validator} is + * being asked if it can {@link #validate(Object, Errors) validate} + * @return {@code true} if this {@link Validator} can indeed + * {@link #validate(Object, Errors) validate} instances of the + * supplied {@code clazz} + */ + boolean supports(Class clazz); + + /** + * Validate the supplied {@code target} object, which must be + * of a {@link Class} for which the {@link #supports(Class)} method + * typically has (or would) return {@code true}. + *

    The supplied {@link Errors errors} instance can be used to report + * any resulting validation errors. + * @param target the object that is to be validated + * @param errors contextual state about the validation process + * @see ValidationUtils + */ + void validate(Object target, Errors errors); + +} diff --git a/spring-context/src/main/java/org/springframework/validation/annotation/Validated.java b/spring-context/src/main/java/org/springframework/validation/annotation/Validated.java new file mode 100644 index 0000000..a5939a3 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/annotation/Validated.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Variant of JSR-303's {@link javax.validation.Valid}, supporting the + * specification of validation groups. Designed for convenient use with + * Spring's JSR-303 support but not JSR-303 specific. + * + *

    Can be used e.g. with Spring MVC handler methods arguments. + * Supported through {@link org.springframework.validation.SmartValidator}'s + * validation hint concept, with validation group classes acting as hint objects. + * + *

    Can also be used with method level validation, indicating that a specific + * class is supposed to be validated at the method level (acting as a pointcut + * for the corresponding validation interceptor), but also optionally specifying + * the validation groups for method-level validation in the annotated class. + * Applying this annotation at the method level allows for overriding the + * validation groups for a specific method but does not serve as a pointcut; + * a class-level annotation is nevertheless necessary to trigger method validation + * for a specific bean to begin with. Can also be used as a meta-annotation on a + * custom stereotype annotation or a custom group-specific validated annotation. + * + * @author Juergen Hoeller + * @since 3.1 + * @see javax.validation.Validator#validate(Object, Class[]) + * @see org.springframework.validation.SmartValidator#validate(Object, org.springframework.validation.Errors, Object...) + * @see org.springframework.validation.beanvalidation.SpringValidatorAdapter + * @see org.springframework.validation.beanvalidation.MethodValidationPostProcessor + */ +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Validated { + + /** + * Specify one or more validation groups to apply to the validation step + * kicked off by this annotation. + *

    JSR-303 defines validation groups as custom annotations which an application declares + * for the sole purpose of using them as type-safe group arguments, as implemented in + * {@link org.springframework.validation.beanvalidation.SpringValidatorAdapter}. + *

    Other {@link org.springframework.validation.SmartValidator} implementations may + * support class arguments in other ways as well. + */ + Class[] value() default {}; + +} diff --git a/spring-context/src/main/java/org/springframework/validation/annotation/package-info.java b/spring-context/src/main/java/org/springframework/validation/annotation/package-info.java new file mode 100644 index 0000000..0f55e84 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/annotation/package-info.java @@ -0,0 +1,13 @@ +/** + * Support classes for annotation-based constraint evaluation, + * e.g. using a JSR-303 Bean Validation provider. + * + *

    Provides an extended variant of JSR-303's {@code @Valid}, + * supporting the specification of validation groups. + */ +@NonNullApi +@NonNullFields +package org.springframework.validation.annotation; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/BeanValidationPostProcessor.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/BeanValidationPostProcessor.java new file mode 100644 index 0000000..11dccd3 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/BeanValidationPostProcessor.java @@ -0,0 +1,131 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation.beanvalidation; + +import java.util.Iterator; +import java.util.Set; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; + +import org.springframework.aop.framework.AopProxyUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Simple {@link BeanPostProcessor} that checks JSR-303 constraint annotations + * in Spring-managed beans, throwing an initialization exception in case of + * constraint violations right before calling the bean's init method (if any). + * + * @author Juergen Hoeller + * @since 3.0 + */ +public class BeanValidationPostProcessor implements BeanPostProcessor, InitializingBean { + + @Nullable + private Validator validator; + + private boolean afterInitialization = false; + + + /** + * Set the JSR-303 Validator to delegate to for validating beans. + *

    Default is the default ValidatorFactory's default Validator. + */ + public void setValidator(Validator validator) { + this.validator = validator; + } + + /** + * Set the JSR-303 ValidatorFactory to delegate to for validating beans, + * using its default Validator. + *

    Default is the default ValidatorFactory's default Validator. + * @see javax.validation.ValidatorFactory#getValidator() + */ + public void setValidatorFactory(ValidatorFactory validatorFactory) { + this.validator = validatorFactory.getValidator(); + } + + /** + * Choose whether to perform validation after bean initialization + * (i.e. after init methods) instead of before (which is the default). + *

    Default is "false" (before initialization). Switch this to "true" + * (after initialization) if you would like to give init methods a chance + * to populate constrained fields before they get validated. + */ + public void setAfterInitialization(boolean afterInitialization) { + this.afterInitialization = afterInitialization; + } + + @Override + public void afterPropertiesSet() { + if (this.validator == null) { + this.validator = Validation.buildDefaultValidatorFactory().getValidator(); + } + } + + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (!this.afterInitialization) { + doValidate(bean); + } + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (this.afterInitialization) { + doValidate(bean); + } + return bean; + } + + + /** + * Perform validation of the given bean. + * @param bean the bean instance to validate + * @see javax.validation.Validator#validate + */ + protected void doValidate(Object bean) { + Assert.state(this.validator != null, "No Validator set"); + Object objectToValidate = AopProxyUtils.getSingletonTarget(bean); + if (objectToValidate == null) { + objectToValidate = bean; + } + Set> result = this.validator.validate(objectToValidate); + + if (!result.isEmpty()) { + StringBuilder sb = new StringBuilder("Bean state is invalid: "); + for (Iterator> it = result.iterator(); it.hasNext();) { + ConstraintViolation violation = it.next(); + sb.append(violation.getPropertyPath()).append(" - ").append(violation.getMessage()); + if (it.hasNext()) { + sb.append("; "); + } + } + throw new BeanInitializationException(sb.toString()); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/CustomValidatorBean.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/CustomValidatorBean.java new file mode 100644 index 0000000..b9c074f --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/CustomValidatorBean.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation.beanvalidation; + +import javax.validation.MessageInterpolator; +import javax.validation.TraversableResolver; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorContext; +import javax.validation.ValidatorFactory; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.lang.Nullable; + +/** + * Configurable bean class that exposes a specific JSR-303 Validator + * through its original interface as well as through the Spring + * {@link org.springframework.validation.Validator} interface. + * + * @author Juergen Hoeller + * @since 3.0 + */ +public class CustomValidatorBean extends SpringValidatorAdapter implements Validator, InitializingBean { + + @Nullable + private ValidatorFactory validatorFactory; + + @Nullable + private MessageInterpolator messageInterpolator; + + @Nullable + private TraversableResolver traversableResolver; + + + /** + * Set the ValidatorFactory to obtain the target Validator from. + *

    Default is {@link javax.validation.Validation#buildDefaultValidatorFactory()}. + */ + public void setValidatorFactory(ValidatorFactory validatorFactory) { + this.validatorFactory = validatorFactory; + } + + /** + * Specify a custom MessageInterpolator to use for this Validator. + */ + public void setMessageInterpolator(MessageInterpolator messageInterpolator) { + this.messageInterpolator = messageInterpolator; + } + + /** + * Specify a custom TraversableResolver to use for this Validator. + */ + public void setTraversableResolver(TraversableResolver traversableResolver) { + this.traversableResolver = traversableResolver; + } + + + @Override + public void afterPropertiesSet() { + if (this.validatorFactory == null) { + this.validatorFactory = Validation.buildDefaultValidatorFactory(); + } + + ValidatorContext validatorContext = this.validatorFactory.usingContext(); + MessageInterpolator targetInterpolator = this.messageInterpolator; + if (targetInterpolator == null) { + targetInterpolator = this.validatorFactory.getMessageInterpolator(); + } + validatorContext.messageInterpolator(new LocaleContextMessageInterpolator(targetInterpolator)); + if (this.traversableResolver != null) { + validatorContext.traversableResolver(this.traversableResolver); + } + + setTargetValidator(validatorContext.getValidator()); + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/LocalValidatorFactoryBean.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/LocalValidatorFactoryBean.java new file mode 100644 index 0000000..da0ef73 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/LocalValidatorFactoryBean.java @@ -0,0 +1,441 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation.beanvalidation; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import javax.validation.Configuration; +import javax.validation.ConstraintValidatorFactory; +import javax.validation.MessageInterpolator; +import javax.validation.ParameterNameProvider; +import javax.validation.TraversableResolver; +import javax.validation.Validation; +import javax.validation.ValidationException; +import javax.validation.ValidationProviderResolver; +import javax.validation.Validator; +import javax.validation.ValidatorContext; +import javax.validation.ValidatorFactory; +import javax.validation.bootstrap.GenericBootstrap; +import javax.validation.bootstrap.ProviderSpecificBootstrap; + +import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.MessageSource; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ReflectionUtils; + +/** + * This is the central class for {@code javax.validation} (JSR-303) setup in a Spring + * application context: It bootstraps a {@code javax.validation.ValidationFactory} and + * exposes it through the Spring {@link org.springframework.validation.Validator} interface + * as well as through the JSR-303 {@link javax.validation.Validator} interface and the + * {@link javax.validation.ValidatorFactory} interface itself. + * + *

    When talking to an instance of this bean through the Spring or JSR-303 Validator interfaces, + * you'll be talking to the default Validator of the underlying ValidatorFactory. This is very + * convenient in that you don't have to perform yet another call on the factory, assuming that + * you will almost always use the default Validator anyway. This can also be injected directly + * into any target dependency of type {@link org.springframework.validation.Validator}! + * + *

    As of Spring 5.0, this class requires Bean Validation 1.1+, with special support + * for Hibernate Validator 5.x (see {@link #setValidationMessageSource}). + * This class is also runtime-compatible with Bean Validation 2.0 and Hibernate Validator 6.0, + * with one special note: If you'd like to call BV 2.0's {@code getClockProvider()} method, + * obtain the native {@code ValidatorFactory} through {@code #unwrap(ValidatorFactory.class)} + * and call the {@code getClockProvider()} method on the returned native reference there. + * + *

    This class is also being used by Spring's MVC configuration namespace, in case of the + * {@code javax.validation} API being present but no explicit Validator having been configured. + * + * @author Juergen Hoeller + * @since 3.0 + * @see javax.validation.ValidatorFactory + * @see javax.validation.Validator + * @see javax.validation.Validation#buildDefaultValidatorFactory() + * @see javax.validation.ValidatorFactory#getValidator() + */ +public class LocalValidatorFactoryBean extends SpringValidatorAdapter + implements ValidatorFactory, ApplicationContextAware, InitializingBean, DisposableBean { + + @SuppressWarnings("rawtypes") + @Nullable + private Class providerClass; + + @Nullable + private ValidationProviderResolver validationProviderResolver; + + @Nullable + private MessageInterpolator messageInterpolator; + + @Nullable + private TraversableResolver traversableResolver; + + @Nullable + private ConstraintValidatorFactory constraintValidatorFactory; + + @Nullable + private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + + @Nullable + private Resource[] mappingLocations; + + private final Map validationPropertyMap = new HashMap<>(); + + @Nullable + private ApplicationContext applicationContext; + + @Nullable + private ValidatorFactory validatorFactory; + + + /** + * Specify the desired provider class, if any. + *

    If not specified, JSR-303's default search mechanism will be used. + * @see javax.validation.Validation#byProvider(Class) + * @see javax.validation.Validation#byDefaultProvider() + */ + @SuppressWarnings("rawtypes") + public void setProviderClass(Class providerClass) { + this.providerClass = providerClass; + } + + /** + * Specify a JSR-303 {@link ValidationProviderResolver} for bootstrapping the + * provider of choice, as an alternative to {@code META-INF} driven resolution. + * @since 4.3 + */ + public void setValidationProviderResolver(ValidationProviderResolver validationProviderResolver) { + this.validationProviderResolver = validationProviderResolver; + } + + /** + * Specify a custom MessageInterpolator to use for this ValidatorFactory + * and its exposed default Validator. + */ + public void setMessageInterpolator(MessageInterpolator messageInterpolator) { + this.messageInterpolator = messageInterpolator; + } + + /** + * Specify a custom Spring MessageSource for resolving validation messages, + * instead of relying on JSR-303's default "ValidationMessages.properties" bundle + * in the classpath. This may refer to a Spring context's shared "messageSource" bean, + * or to some special MessageSource setup for validation purposes only. + *

    NOTE: This feature requires Hibernate Validator 4.3 or higher on the classpath. + * You may nevertheless use a different validation provider but Hibernate Validator's + * {@link ResourceBundleMessageInterpolator} class must be accessible during configuration. + *

    Specify either this property or {@link #setMessageInterpolator "messageInterpolator"}, + * not both. If you would like to build a custom MessageInterpolator, consider deriving from + * Hibernate Validator's {@link ResourceBundleMessageInterpolator} and passing in a + * Spring-based {@code ResourceBundleLocator} when constructing your interpolator. + *

    In order for Hibernate's default validation messages to be resolved still, your + * {@link MessageSource} must be configured for optional resolution (usually the default). + * In particular, the {@code MessageSource} instance specified here should not apply + * {@link org.springframework.context.support.AbstractMessageSource#setUseCodeAsDefaultMessage + * "useCodeAsDefaultMessage"} behavior. Please double-check your setup accordingly. + * @see ResourceBundleMessageInterpolator + */ + public void setValidationMessageSource(MessageSource messageSource) { + this.messageInterpolator = HibernateValidatorDelegate.buildMessageInterpolator(messageSource); + } + + /** + * Specify a custom TraversableResolver to use for this ValidatorFactory + * and its exposed default Validator. + */ + public void setTraversableResolver(TraversableResolver traversableResolver) { + this.traversableResolver = traversableResolver; + } + + /** + * Specify a custom ConstraintValidatorFactory to use for this ValidatorFactory. + *

    Default is a {@link SpringConstraintValidatorFactory}, delegating to the + * containing ApplicationContext for creating autowired ConstraintValidator instances. + */ + public void setConstraintValidatorFactory(ConstraintValidatorFactory constraintValidatorFactory) { + this.constraintValidatorFactory = constraintValidatorFactory; + } + + /** + * Set the ParameterNameDiscoverer to use for resolving method and constructor + * parameter names if needed for message interpolation. + *

    Default is a {@link org.springframework.core.DefaultParameterNameDiscoverer}. + */ + public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) { + this.parameterNameDiscoverer = parameterNameDiscoverer; + } + + /** + * Specify resource locations to load XML constraint mapping files from, if any. + */ + public void setMappingLocations(Resource... mappingLocations) { + this.mappingLocations = mappingLocations; + } + + /** + * Specify bean validation properties to be passed to the validation provider. + *

    Can be populated with a String "value" (parsed via PropertiesEditor) + * or a "props" element in XML bean definitions. + * @see javax.validation.Configuration#addProperty(String, String) + */ + public void setValidationProperties(Properties jpaProperties) { + CollectionUtils.mergePropertiesIntoMap(jpaProperties, this.validationPropertyMap); + } + + /** + * Specify bean validation properties to be passed to the validation provider as a Map. + *

    Can be populated with a "map" or "props" element in XML bean definitions. + * @see javax.validation.Configuration#addProperty(String, String) + */ + public void setValidationPropertyMap(@Nullable Map validationProperties) { + if (validationProperties != null) { + this.validationPropertyMap.putAll(validationProperties); + } + } + + /** + * Allow Map access to the bean validation properties to be passed to the validation provider, + * with the option to add or override specific entries. + *

    Useful for specifying entries directly, for example via "validationPropertyMap[myKey]". + */ + public Map getValidationPropertyMap() { + return this.validationPropertyMap; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + + @Override + @SuppressWarnings({"rawtypes", "unchecked"}) + public void afterPropertiesSet() { + Configuration configuration; + if (this.providerClass != null) { + ProviderSpecificBootstrap bootstrap = Validation.byProvider(this.providerClass); + if (this.validationProviderResolver != null) { + bootstrap = bootstrap.providerResolver(this.validationProviderResolver); + } + configuration = bootstrap.configure(); + } + else { + GenericBootstrap bootstrap = Validation.byDefaultProvider(); + if (this.validationProviderResolver != null) { + bootstrap = bootstrap.providerResolver(this.validationProviderResolver); + } + configuration = bootstrap.configure(); + } + + // Try Hibernate Validator 5.2's externalClassLoader(ClassLoader) method + if (this.applicationContext != null) { + try { + Method eclMethod = configuration.getClass().getMethod("externalClassLoader", ClassLoader.class); + ReflectionUtils.invokeMethod(eclMethod, configuration, this.applicationContext.getClassLoader()); + } + catch (NoSuchMethodException ex) { + // Ignore - no Hibernate Validator 5.2+ or similar provider + } + } + + MessageInterpolator targetInterpolator = this.messageInterpolator; + if (targetInterpolator == null) { + targetInterpolator = configuration.getDefaultMessageInterpolator(); + } + configuration.messageInterpolator(new LocaleContextMessageInterpolator(targetInterpolator)); + + if (this.traversableResolver != null) { + configuration.traversableResolver(this.traversableResolver); + } + + ConstraintValidatorFactory targetConstraintValidatorFactory = this.constraintValidatorFactory; + if (targetConstraintValidatorFactory == null && this.applicationContext != null) { + targetConstraintValidatorFactory = + new SpringConstraintValidatorFactory(this.applicationContext.getAutowireCapableBeanFactory()); + } + if (targetConstraintValidatorFactory != null) { + configuration.constraintValidatorFactory(targetConstraintValidatorFactory); + } + + if (this.parameterNameDiscoverer != null) { + configureParameterNameProvider(this.parameterNameDiscoverer, configuration); + } + + if (this.mappingLocations != null) { + for (Resource location : this.mappingLocations) { + try { + configuration.addMapping(location.getInputStream()); + } + catch (IOException ex) { + throw new IllegalStateException("Cannot read mapping resource: " + location); + } + } + } + + this.validationPropertyMap.forEach(configuration::addProperty); + + // Allow for custom post-processing before we actually build the ValidatorFactory. + postProcessConfiguration(configuration); + + this.validatorFactory = configuration.buildValidatorFactory(); + setTargetValidator(this.validatorFactory.getValidator()); + } + + private void configureParameterNameProvider(ParameterNameDiscoverer discoverer, Configuration configuration) { + final ParameterNameProvider defaultProvider = configuration.getDefaultParameterNameProvider(); + configuration.parameterNameProvider(new ParameterNameProvider() { + @Override + public List getParameterNames(Constructor constructor) { + String[] paramNames = discoverer.getParameterNames(constructor); + return (paramNames != null ? Arrays.asList(paramNames) : + defaultProvider.getParameterNames(constructor)); + } + @Override + public List getParameterNames(Method method) { + String[] paramNames = discoverer.getParameterNames(method); + return (paramNames != null ? Arrays.asList(paramNames) : + defaultProvider.getParameterNames(method)); + } + }); + } + + /** + * Post-process the given Bean Validation configuration, + * adding to or overriding any of its settings. + *

    Invoked right before building the {@link ValidatorFactory}. + * @param configuration the Configuration object, pre-populated with + * settings driven by LocalValidatorFactoryBean's properties + */ + protected void postProcessConfiguration(Configuration configuration) { + } + + + @Override + public Validator getValidator() { + Assert.notNull(this.validatorFactory, "No target ValidatorFactory set"); + return this.validatorFactory.getValidator(); + } + + @Override + public ValidatorContext usingContext() { + Assert.notNull(this.validatorFactory, "No target ValidatorFactory set"); + return this.validatorFactory.usingContext(); + } + + @Override + public MessageInterpolator getMessageInterpolator() { + Assert.notNull(this.validatorFactory, "No target ValidatorFactory set"); + return this.validatorFactory.getMessageInterpolator(); + } + + @Override + public TraversableResolver getTraversableResolver() { + Assert.notNull(this.validatorFactory, "No target ValidatorFactory set"); + return this.validatorFactory.getTraversableResolver(); + } + + @Override + public ConstraintValidatorFactory getConstraintValidatorFactory() { + Assert.notNull(this.validatorFactory, "No target ValidatorFactory set"); + return this.validatorFactory.getConstraintValidatorFactory(); + } + + @Override + public ParameterNameProvider getParameterNameProvider() { + Assert.notNull(this.validatorFactory, "No target ValidatorFactory set"); + return this.validatorFactory.getParameterNameProvider(); + } + + // Bean Validation 2.0: currently not implemented here since it would imply + // a hard dependency on the new javax.validation.ClockProvider interface. + // To be resolved once Spring Framework requires Bean Validation 2.0+. + // Obtain the native ValidatorFactory through unwrap(ValidatorFactory.class) + // instead which will fully support a getClockProvider() call as well. + /* + @Override + public javax.validation.ClockProvider getClockProvider() { + Assert.notNull(this.validatorFactory, "No target ValidatorFactory set"); + return this.validatorFactory.getClockProvider(); + } + */ + + @Override + @SuppressWarnings("unchecked") + public T unwrap(@Nullable Class type) { + if (type == null || !ValidatorFactory.class.isAssignableFrom(type)) { + try { + return super.unwrap(type); + } + catch (ValidationException ex) { + // ignore - we'll try ValidatorFactory unwrapping next + } + } + if (this.validatorFactory != null) { + try { + return this.validatorFactory.unwrap(type); + } + catch (ValidationException ex) { + // ignore if just being asked for ValidatorFactory + if (ValidatorFactory.class == type) { + return (T) this.validatorFactory; + } + throw ex; + } + } + throw new ValidationException("Cannot unwrap to " + type); + } + + @Override + public void close() { + if (this.validatorFactory != null) { + this.validatorFactory.close(); + } + } + + @Override + public void destroy() { + close(); + } + + + /** + * Inner class to avoid a hard-coded Hibernate Validator dependency. + */ + private static class HibernateValidatorDelegate { + + public static MessageInterpolator buildMessageInterpolator(MessageSource messageSource) { + return new ResourceBundleMessageInterpolator(new MessageSourceResourceBundleLocator(messageSource)); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/LocaleContextMessageInterpolator.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/LocaleContextMessageInterpolator.java new file mode 100644 index 0000000..40854bc --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/LocaleContextMessageInterpolator.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation.beanvalidation; + +import java.util.Locale; + +import javax.validation.MessageInterpolator; + +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.util.Assert; + +/** + * Delegates to a target {@link MessageInterpolator} implementation but enforces Spring's + * managed Locale. Typically used to wrap the validation provider's default interpolator. + * + * @author Juergen Hoeller + * @since 3.0 + * @see org.springframework.context.i18n.LocaleContextHolder#getLocale() + */ +public class LocaleContextMessageInterpolator implements MessageInterpolator { + + private final MessageInterpolator targetInterpolator; + + + /** + * Create a new LocaleContextMessageInterpolator, wrapping the given target interpolator. + * @param targetInterpolator the target MessageInterpolator to wrap + */ + public LocaleContextMessageInterpolator(MessageInterpolator targetInterpolator) { + Assert.notNull(targetInterpolator, "Target MessageInterpolator must not be null"); + this.targetInterpolator = targetInterpolator; + } + + + @Override + public String interpolate(String message, Context context) { + return this.targetInterpolator.interpolate(message, context, LocaleContextHolder.getLocale()); + } + + @Override + public String interpolate(String message, Context context, Locale locale) { + return this.targetInterpolator.interpolate(message, context, locale); + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MessageSourceResourceBundleLocator.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MessageSourceResourceBundleLocator.java new file mode 100644 index 0000000..0a80b28 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MessageSourceResourceBundleLocator.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation.beanvalidation; + +import java.util.Locale; +import java.util.ResourceBundle; + +import org.hibernate.validator.spi.resourceloading.ResourceBundleLocator; + +import org.springframework.context.MessageSource; +import org.springframework.context.support.MessageSourceResourceBundle; +import org.springframework.util.Assert; + +/** + * Implementation of Hibernate Validator 4.3/5.x's {@link ResourceBundleLocator} interface, + * exposing a Spring {@link MessageSource} as localized {@link MessageSourceResourceBundle}. + * + * @author Juergen Hoeller + * @since 3.0.4 + * @see ResourceBundleLocator + * @see MessageSource + * @see MessageSourceResourceBundle + */ +public class MessageSourceResourceBundleLocator implements ResourceBundleLocator { + + private final MessageSource messageSource; + + /** + * Build a MessageSourceResourceBundleLocator for the given MessageSource. + * @param messageSource the Spring MessageSource to wrap + */ + public MessageSourceResourceBundleLocator(MessageSource messageSource) { + Assert.notNull(messageSource, "MessageSource must not be null"); + this.messageSource = messageSource; + } + + @Override + public ResourceBundle getResourceBundle(Locale locale) { + return new MessageSourceResourceBundle(this.messageSource, locale); + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationInterceptor.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationInterceptor.java new file mode 100644 index 0000000..8baf92a --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationInterceptor.java @@ -0,0 +1,171 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation.beanvalidation; + +import java.lang.reflect.Method; +import java.util.Set; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; +import javax.validation.executable.ExecutableValidator; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.SmartFactoryBean; +import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.validation.annotation.Validated; + +/** + * An AOP Alliance {@link MethodInterceptor} implementation that delegates to a + * JSR-303 provider for performing method-level validation on annotated methods. + * + *

    Applicable methods have JSR-303 constraint annotations on their parameters + * and/or on their return value (in the latter case specified at the method level, + * typically as inline annotation). + * + *

    E.g.: {@code public @NotNull Object myValidMethod(@NotNull String arg1, @Max(10) int arg2)} + * + *

    Validation groups can be specified through Spring's {@link Validated} annotation + * at the type level of the containing target class, applying to all public service methods + * of that class. By default, JSR-303 will validate against its default group only. + * + *

    As of Spring 5.0, this functionality requires a Bean Validation 1.1+ provider. + * + * @author Juergen Hoeller + * @since 3.1 + * @see MethodValidationPostProcessor + * @see javax.validation.executable.ExecutableValidator + */ +public class MethodValidationInterceptor implements MethodInterceptor { + + private final Validator validator; + + + /** + * Create a new MethodValidationInterceptor using a default JSR-303 validator underneath. + */ + public MethodValidationInterceptor() { + this(Validation.buildDefaultValidatorFactory()); + } + + /** + * Create a new MethodValidationInterceptor using the given JSR-303 ValidatorFactory. + * @param validatorFactory the JSR-303 ValidatorFactory to use + */ + public MethodValidationInterceptor(ValidatorFactory validatorFactory) { + this(validatorFactory.getValidator()); + } + + /** + * Create a new MethodValidationInterceptor using the given JSR-303 Validator. + * @param validator the JSR-303 Validator to use + */ + public MethodValidationInterceptor(Validator validator) { + this.validator = validator; + } + + + @Override + @Nullable + public Object invoke(MethodInvocation invocation) throws Throwable { + // Avoid Validator invocation on FactoryBean.getObjectType/isSingleton + if (isFactoryBeanMetadataMethod(invocation.getMethod())) { + return invocation.proceed(); + } + + Class[] groups = determineValidationGroups(invocation); + + // Standard Bean Validation 1.1 API + ExecutableValidator execVal = this.validator.forExecutables(); + Method methodToValidate = invocation.getMethod(); + Set> result; + + Object target = invocation.getThis(); + Assert.state(target != null, "Target must not be null"); + + try { + result = execVal.validateParameters(target, methodToValidate, invocation.getArguments(), groups); + } + catch (IllegalArgumentException ex) { + // Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011 + // Let's try to find the bridged method on the implementation class... + methodToValidate = BridgeMethodResolver.findBridgedMethod( + ClassUtils.getMostSpecificMethod(invocation.getMethod(), target.getClass())); + result = execVal.validateParameters(target, methodToValidate, invocation.getArguments(), groups); + } + if (!result.isEmpty()) { + throw new ConstraintViolationException(result); + } + + Object returnValue = invocation.proceed(); + + result = execVal.validateReturnValue(target, methodToValidate, returnValue, groups); + if (!result.isEmpty()) { + throw new ConstraintViolationException(result); + } + + return returnValue; + } + + private boolean isFactoryBeanMetadataMethod(Method method) { + Class clazz = method.getDeclaringClass(); + + // Call from interface-based proxy handle, allowing for an efficient check? + if (clazz.isInterface()) { + return ((clazz == FactoryBean.class || clazz == SmartFactoryBean.class) && + !method.getName().equals("getObject")); + } + + // Call from CGLIB proxy handle, potentially implementing a FactoryBean method? + Class factoryBeanType = null; + if (SmartFactoryBean.class.isAssignableFrom(clazz)) { + factoryBeanType = SmartFactoryBean.class; + } + else if (FactoryBean.class.isAssignableFrom(clazz)) { + factoryBeanType = FactoryBean.class; + } + return (factoryBeanType != null && !method.getName().equals("getObject") && + ClassUtils.hasMethod(factoryBeanType, method)); + } + + /** + * Determine the validation groups to validate against for the given method invocation. + *

    Default are the validation groups as specified in the {@link Validated} annotation + * on the containing target class of the method. + * @param invocation the current MethodInvocation + * @return the applicable validation groups as a Class array + */ + protected Class[] determineValidationGroups(MethodInvocation invocation) { + Validated validatedAnn = AnnotationUtils.findAnnotation(invocation.getMethod(), Validated.class); + if (validatedAnn == null) { + Object target = invocation.getThis(); + Assert.state(target != null, "Target must not be null"); + validatedAnn = AnnotationUtils.findAnnotation(target.getClass(), Validated.class); + } + return (validatedAnn != null ? validatedAnn.value() : new Class[0]); + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationPostProcessor.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationPostProcessor.java new file mode 100644 index 0000000..45e5d13 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationPostProcessor.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation.beanvalidation; + +import java.lang.annotation.Annotation; + +import javax.validation.Validator; +import javax.validation.ValidatorFactory; + +import org.aopalliance.aop.Advice; + +import org.springframework.aop.Pointcut; +import org.springframework.aop.framework.autoproxy.AbstractBeanFactoryAwareAdvisingPostProcessor; +import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.validation.annotation.Validated; + +/** + * A convenient {@link BeanPostProcessor} implementation that delegates to a + * JSR-303 provider for performing method-level validation on annotated methods. + * + *

    Applicable methods have JSR-303 constraint annotations on their parameters + * and/or on their return value (in the latter case specified at the method level, + * typically as inline annotation), e.g.: + * + *

    + * public @NotNull Object myValidMethod(@NotNull String arg1, @Max(10) int arg2)
    + * 
    + * + *

    Target classes with such annotated methods need to be annotated with Spring's + * {@link Validated} annotation at the type level, for their methods to be searched for + * inline constraint annotations. Validation groups can be specified through {@code @Validated} + * as well. By default, JSR-303 will validate against its default group only. + * + *

    As of Spring 5.0, this functionality requires a Bean Validation 1.1+ provider. + * + * @author Juergen Hoeller + * @since 3.1 + * @see MethodValidationInterceptor + * @see javax.validation.executable.ExecutableValidator + */ +@SuppressWarnings("serial") +public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor + implements InitializingBean { + + private Class validatedAnnotationType = Validated.class; + + @Nullable + private Validator validator; + + + /** + * Set the 'validated' annotation type. + * The default validated annotation type is the {@link Validated} annotation. + *

    This setter property exists so that developers can provide their own + * (non-Spring-specific) annotation type to indicate that a class is supposed + * to be validated in the sense of applying method validation. + * @param validatedAnnotationType the desired annotation type + */ + public void setValidatedAnnotationType(Class validatedAnnotationType) { + Assert.notNull(validatedAnnotationType, "'validatedAnnotationType' must not be null"); + this.validatedAnnotationType = validatedAnnotationType; + } + + /** + * Set the JSR-303 Validator to delegate to for validating methods. + *

    Default is the default ValidatorFactory's default Validator. + */ + public void setValidator(Validator validator) { + // Unwrap to the native Validator with forExecutables support + if (validator instanceof LocalValidatorFactoryBean) { + this.validator = ((LocalValidatorFactoryBean) validator).getValidator(); + } + else if (validator instanceof SpringValidatorAdapter) { + this.validator = validator.unwrap(Validator.class); + } + else { + this.validator = validator; + } + } + + /** + * Set the JSR-303 ValidatorFactory to delegate to for validating methods, + * using its default Validator. + *

    Default is the default ValidatorFactory's default Validator. + * @see javax.validation.ValidatorFactory#getValidator() + */ + public void setValidatorFactory(ValidatorFactory validatorFactory) { + this.validator = validatorFactory.getValidator(); + } + + + @Override + public void afterPropertiesSet() { + Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true); + this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator)); + } + + /** + * Create AOP advice for method validation purposes, to be applied + * with a pointcut for the specified 'validated' annotation. + * @param validator the JSR-303 Validator to delegate to + * @return the interceptor to use (typically, but not necessarily, + * a {@link MethodValidationInterceptor} or subclass thereof) + * @since 4.2 + */ + protected Advice createMethodValidationAdvice(@Nullable Validator validator) { + return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor()); + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/OptionalValidatorFactoryBean.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/OptionalValidatorFactoryBean.java new file mode 100644 index 0000000..6d30694 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/OptionalValidatorFactoryBean.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation.beanvalidation; + +import javax.validation.ValidationException; + +import org.apache.commons.logging.LogFactory; + +/** + * {@link LocalValidatorFactoryBean} subclass that simply turns + * {@link org.springframework.validation.Validator} calls into no-ops + * in case of no Bean Validation provider being available. + * + *

    This is the actual class used by Spring's MVC configuration namespace, + * in case of the {@code javax.validation} API being present but no explicit + * Validator having been configured. + * + * @author Juergen Hoeller + * @since 4.0.1 + */ +public class OptionalValidatorFactoryBean extends LocalValidatorFactoryBean { + + @Override + public void afterPropertiesSet() { + try { + super.afterPropertiesSet(); + } + catch (ValidationException ex) { + LogFactory.getLog(getClass()).debug("Failed to set up a Bean Validation provider", ex); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringConstraintValidatorFactory.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringConstraintValidatorFactory.java new file mode 100644 index 0000000..5b68f07 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringConstraintValidatorFactory.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation.beanvalidation; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorFactory; + +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.util.Assert; + +/** + * JSR-303 {@link ConstraintValidatorFactory} implementation that delegates to a + * Spring BeanFactory for creating autowired {@link ConstraintValidator} instances. + * + *

    Note that this class is meant for programmatic use, not for declarative use + * in a standard {@code validation.xml} file. Consider + * {@link org.springframework.web.bind.support.SpringWebConstraintValidatorFactory} + * for declarative use in a web application, e.g. with JAX-RS or JAX-WS. + * + * @author Juergen Hoeller + * @since 3.0 + * @see org.springframework.beans.factory.config.AutowireCapableBeanFactory#createBean(Class) + * @see org.springframework.context.ApplicationContext#getAutowireCapableBeanFactory() + */ +public class SpringConstraintValidatorFactory implements ConstraintValidatorFactory { + + private final AutowireCapableBeanFactory beanFactory; + + + /** + * Create a new SpringConstraintValidatorFactory for the given BeanFactory. + * @param beanFactory the target BeanFactory + */ + public SpringConstraintValidatorFactory(AutowireCapableBeanFactory beanFactory) { + Assert.notNull(beanFactory, "BeanFactory must not be null"); + this.beanFactory = beanFactory; + } + + + @Override + public > T getInstance(Class key) { + return this.beanFactory.createBean(key); + } + + // Bean Validation 1.1 releaseInstance method + @Override + public void releaseInstance(ConstraintValidator instance) { + this.beanFactory.destroyBean(instance); + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java new file mode 100644 index 0000000..5439bfa --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java @@ -0,0 +1,499 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation.beanvalidation; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import javax.validation.ConstraintViolation; +import javax.validation.ElementKind; +import javax.validation.Path; +import javax.validation.ValidationException; +import javax.validation.executable.ExecutableValidator; +import javax.validation.metadata.BeanDescriptor; +import javax.validation.metadata.ConstraintDescriptor; + +import org.springframework.beans.NotReadablePropertyException; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.validation.BindingResult; +import org.springframework.validation.Errors; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; +import org.springframework.validation.SmartValidator; + +/** + * Adapter that takes a JSR-303 {@code javax.validator.Validator} and + * exposes it as a Spring {@link org.springframework.validation.Validator} + * while also exposing the original JSR-303 Validator interface itself. + * + *

    Can be used as a programmatic wrapper. Also serves as base class for + * {@link CustomValidatorBean} and {@link LocalValidatorFactoryBean}, + * and as the primary implementation of the {@link SmartValidator} interface. + * + *

    As of Spring Framework 5.0, this adapter is fully compatible with + * Bean Validation 1.1 as well as 2.0. + * + * @author Juergen Hoeller + * @since 3.0 + * @see SmartValidator + * @see CustomValidatorBean + * @see LocalValidatorFactoryBean + */ +public class SpringValidatorAdapter implements SmartValidator, javax.validation.Validator { + + private static final Set internalAnnotationAttributes = new HashSet<>(4); + + static { + internalAnnotationAttributes.add("message"); + internalAnnotationAttributes.add("groups"); + internalAnnotationAttributes.add("payload"); + } + + @Nullable + private javax.validation.Validator targetValidator; + + + /** + * Create a new SpringValidatorAdapter for the given JSR-303 Validator. + * @param targetValidator the JSR-303 Validator to wrap + */ + public SpringValidatorAdapter(javax.validation.Validator targetValidator) { + Assert.notNull(targetValidator, "Target Validator must not be null"); + this.targetValidator = targetValidator; + } + + SpringValidatorAdapter() { + } + + void setTargetValidator(javax.validation.Validator targetValidator) { + this.targetValidator = targetValidator; + } + + + //--------------------------------------------------------------------- + // Implementation of Spring Validator interface + //--------------------------------------------------------------------- + + @Override + public boolean supports(Class clazz) { + return (this.targetValidator != null); + } + + @Override + public void validate(Object target, Errors errors) { + if (this.targetValidator != null) { + processConstraintViolations(this.targetValidator.validate(target), errors); + } + } + + @Override + public void validate(Object target, Errors errors, Object... validationHints) { + if (this.targetValidator != null) { + processConstraintViolations( + this.targetValidator.validate(target, asValidationGroups(validationHints)), errors); + } + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public void validateValue( + Class targetType, String fieldName, @Nullable Object value, Errors errors, Object... validationHints) { + + if (this.targetValidator != null) { + processConstraintViolations(this.targetValidator.validateValue( + (Class) targetType, fieldName, value, asValidationGroups(validationHints)), errors); + } + } + + /** + * Turn the specified validation hints into JSR-303 validation groups. + * @since 5.1 + */ + private Class[] asValidationGroups(Object... validationHints) { + Set> groups = new LinkedHashSet<>(4); + for (Object hint : validationHints) { + if (hint instanceof Class) { + groups.add((Class) hint); + } + } + return ClassUtils.toClassArray(groups); + } + + /** + * Process the given JSR-303 ConstraintViolations, adding corresponding errors to + * the provided Spring {@link Errors} object. + * @param violations the JSR-303 ConstraintViolation results + * @param errors the Spring errors object to register to + */ + @SuppressWarnings("serial") + protected void processConstraintViolations(Set> violations, Errors errors) { + for (ConstraintViolation violation : violations) { + String field = determineField(violation); + FieldError fieldError = errors.getFieldError(field); + if (fieldError == null || !fieldError.isBindingFailure()) { + try { + ConstraintDescriptor cd = violation.getConstraintDescriptor(); + String errorCode = determineErrorCode(cd); + Object[] errorArgs = getArgumentsForConstraint(errors.getObjectName(), field, cd); + if (errors instanceof BindingResult) { + // Can do custom FieldError registration with invalid value from ConstraintViolation, + // as necessary for Hibernate Validator compatibility (non-indexed set path in field) + BindingResult bindingResult = (BindingResult) errors; + String nestedField = bindingResult.getNestedPath() + field; + if (nestedField.isEmpty()) { + String[] errorCodes = bindingResult.resolveMessageCodes(errorCode); + ObjectError error = new ViolationObjectError( + errors.getObjectName(), errorCodes, errorArgs, violation, this); + bindingResult.addError(error); + } + else { + Object rejectedValue = getRejectedValue(field, violation, bindingResult); + String[] errorCodes = bindingResult.resolveMessageCodes(errorCode, field); + FieldError error = new ViolationFieldError(errors.getObjectName(), nestedField, + rejectedValue, errorCodes, errorArgs, violation, this); + bindingResult.addError(error); + } + } + else { + // got no BindingResult - can only do standard rejectValue call + // with automatic extraction of the current field value + errors.rejectValue(field, errorCode, errorArgs, violation.getMessage()); + } + } + catch (NotReadablePropertyException ex) { + throw new IllegalStateException("JSR-303 validated property '" + field + + "' does not have a corresponding accessor for Spring data binding - " + + "check your DataBinder's configuration (bean property versus direct field access)", ex); + } + } + } + } + + /** + * Determine a field for the given constraint violation. + *

    The default implementation returns the stringified property path. + * @param violation the current JSR-303 ConstraintViolation + * @return the Spring-reported field (for use with {@link Errors}) + * @since 4.2 + * @see javax.validation.ConstraintViolation#getPropertyPath() + * @see org.springframework.validation.FieldError#getField() + */ + protected String determineField(ConstraintViolation violation) { + Path path = violation.getPropertyPath(); + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (Path.Node node : path) { + if (node.isInIterable()) { + sb.append('['); + Object index = node.getIndex(); + if (index == null) { + index = node.getKey(); + } + if (index != null) { + sb.append(index); + } + sb.append(']'); + } + String name = node.getName(); + if (name != null && node.getKind() == ElementKind.PROPERTY && !name.startsWith("<")) { + if (!first) { + sb.append('.'); + } + first = false; + sb.append(name); + } + } + return sb.toString(); + } + + /** + * Determine a Spring-reported error code for the given constraint descriptor. + *

    The default implementation returns the simple class name of the descriptor's + * annotation type. Note that the configured + * {@link org.springframework.validation.MessageCodesResolver} will automatically + * generate error code variations which include the object name and the field name. + * @param descriptor the JSR-303 ConstraintDescriptor for the current violation + * @return a corresponding error code (for use with {@link Errors}) + * @since 4.2 + * @see javax.validation.metadata.ConstraintDescriptor#getAnnotation() + * @see org.springframework.validation.MessageCodesResolver + */ + protected String determineErrorCode(ConstraintDescriptor descriptor) { + return descriptor.getAnnotation().annotationType().getSimpleName(); + } + + /** + * Return FieldError arguments for a validation error on the given field. + * Invoked for each violated constraint. + *

    The default implementation returns a first argument indicating the field name + * (see {@link #getResolvableField}). Afterwards, it adds all actual constraint + * annotation attributes (i.e. excluding "message", "groups" and "payload") in + * alphabetical order of their attribute names. + *

    Can be overridden to e.g. add further attributes from the constraint descriptor. + * @param objectName the name of the target object + * @param field the field that caused the binding error + * @param descriptor the JSR-303 constraint descriptor + * @return the Object array that represents the FieldError arguments + * @see org.springframework.validation.FieldError#getArguments + * @see org.springframework.context.support.DefaultMessageSourceResolvable + * @see org.springframework.validation.DefaultBindingErrorProcessor#getArgumentsForBindError + */ + protected Object[] getArgumentsForConstraint(String objectName, String field, ConstraintDescriptor descriptor) { + List arguments = new ArrayList<>(); + arguments.add(getResolvableField(objectName, field)); + // Using a TreeMap for alphabetical ordering of attribute names + Map attributesToExpose = new TreeMap<>(); + descriptor.getAttributes().forEach((attributeName, attributeValue) -> { + if (!internalAnnotationAttributes.contains(attributeName)) { + if (attributeValue instanceof String) { + attributeValue = new ResolvableAttribute(attributeValue.toString()); + } + attributesToExpose.put(attributeName, attributeValue); + } + }); + arguments.addAll(attributesToExpose.values()); + return arguments.toArray(); + } + + /** + * Build a resolvable wrapper for the specified field, allowing to resolve the field's + * name in a {@code MessageSource}. + *

    The default implementation returns a first argument indicating the field: + * of type {@code DefaultMessageSourceResolvable}, with "objectName.field" and "field" + * as codes, and with the plain field name as default message. + * @param objectName the name of the target object + * @param field the field that caused the binding error + * @return a corresponding {@code MessageSourceResolvable} for the specified field + * @since 4.3 + * @see #getArgumentsForConstraint + */ + protected MessageSourceResolvable getResolvableField(String objectName, String field) { + String[] codes = new String[] {objectName + Errors.NESTED_PATH_SEPARATOR + field, field}; + return new DefaultMessageSourceResolvable(codes, field); + } + + /** + * Extract the rejected value behind the given constraint violation, + * for exposure through the Spring errors representation. + * @param field the field that caused the binding error + * @param violation the corresponding JSR-303 ConstraintViolation + * @param bindingResult a Spring BindingResult for the backing object + * which contains the current field's value + * @return the invalid value to expose as part of the field error + * @since 4.2 + * @see javax.validation.ConstraintViolation#getInvalidValue() + * @see org.springframework.validation.FieldError#getRejectedValue() + */ + @Nullable + protected Object getRejectedValue(String field, ConstraintViolation violation, BindingResult bindingResult) { + Object invalidValue = violation.getInvalidValue(); + if (!field.isEmpty() && !field.contains("[]") && + (invalidValue == violation.getLeafBean() || field.contains("[") || field.contains("."))) { + // Possibly a bean constraint with property path: retrieve the actual property value. + // However, explicitly avoid this for "address[]" style paths that we can't handle. + invalidValue = bindingResult.getRawFieldValue(field); + } + return invalidValue; + } + + /** + * Indicate whether this violation's interpolated message has remaining + * placeholders and therefore requires {@link java.text.MessageFormat} + * to be applied to it. Called for a Bean Validation defined message + * (coming out {@code ValidationMessages.properties}) when rendered + * as the default message in Spring's MessageSource. + *

    The default implementation considers a Spring-style "{0}" placeholder + * for the field name as an indication for {@link java.text.MessageFormat}. + * Any other placeholder or escape syntax occurrences are typically a + * mismatch, coming out of regex pattern values or the like. Note that + * standard Bean Validation does not support "{0}" style placeholders at all; + * this is a feature typically used in Spring MessageSource resource bundles. + * @param violation the Bean Validation constraint violation, including + * BV-defined interpolation of named attribute references in its message + * @return {@code true} if {@code java.text.MessageFormat} is to be applied, + * or {@code false} if the violation's message should be used as-is + * @since 5.1.8 + * @see #getArgumentsForConstraint + */ + protected boolean requiresMessageFormat(ConstraintViolation violation) { + return containsSpringStylePlaceholder(violation.getMessage()); + } + + private static boolean containsSpringStylePlaceholder(@Nullable String message) { + return (message != null && message.contains("{0}")); + } + + + //--------------------------------------------------------------------- + // Implementation of JSR-303 Validator interface + //--------------------------------------------------------------------- + + @Override + public Set> validate(T object, Class... groups) { + Assert.state(this.targetValidator != null, "No target Validator set"); + return this.targetValidator.validate(object, groups); + } + + @Override + public Set> validateProperty(T object, String propertyName, Class... groups) { + Assert.state(this.targetValidator != null, "No target Validator set"); + return this.targetValidator.validateProperty(object, propertyName, groups); + } + + @Override + public Set> validateValue( + Class beanType, String propertyName, Object value, Class... groups) { + + Assert.state(this.targetValidator != null, "No target Validator set"); + return this.targetValidator.validateValue(beanType, propertyName, value, groups); + } + + @Override + public BeanDescriptor getConstraintsForClass(Class clazz) { + Assert.state(this.targetValidator != null, "No target Validator set"); + return this.targetValidator.getConstraintsForClass(clazz); + } + + @Override + @SuppressWarnings("unchecked") + public T unwrap(@Nullable Class type) { + Assert.state(this.targetValidator != null, "No target Validator set"); + try { + return (type != null ? this.targetValidator.unwrap(type) : (T) this.targetValidator); + } + catch (ValidationException ex) { + // ignore if just being asked for plain Validator + if (javax.validation.Validator.class == type) { + return (T) this.targetValidator; + } + throw ex; + } + } + + @Override + public ExecutableValidator forExecutables() { + Assert.state(this.targetValidator != null, "No target Validator set"); + return this.targetValidator.forExecutables(); + } + + + /** + * Wrapper for a String attribute which can be resolved via a {@code MessageSource}, + * falling back to the original attribute as a default value otherwise. + */ + @SuppressWarnings("serial") + private static class ResolvableAttribute implements MessageSourceResolvable, Serializable { + + private final String resolvableString; + + public ResolvableAttribute(String resolvableString) { + this.resolvableString = resolvableString; + } + + @Override + public String[] getCodes() { + return new String[] {this.resolvableString}; + } + + @Override + @Nullable + public Object[] getArguments() { + return null; + } + + @Override + public String getDefaultMessage() { + return this.resolvableString; + } + + @Override + public String toString() { + return this.resolvableString; + } + } + + + /** + * Subclass of {@code ObjectError} with Spring-style default message rendering. + */ + @SuppressWarnings("serial") + private static class ViolationObjectError extends ObjectError implements Serializable { + + @Nullable + private transient SpringValidatorAdapter adapter; + + @Nullable + private transient ConstraintViolation violation; + + public ViolationObjectError(String objectName, String[] codes, Object[] arguments, + ConstraintViolation violation, SpringValidatorAdapter adapter) { + + super(objectName, codes, arguments, violation.getMessage()); + this.adapter = adapter; + this.violation = violation; + wrap(violation); + } + + @Override + public boolean shouldRenderDefaultMessage() { + return (this.adapter != null && this.violation != null ? + this.adapter.requiresMessageFormat(this.violation) : + containsSpringStylePlaceholder(getDefaultMessage())); + } + } + + + /** + * Subclass of {@code FieldError} with Spring-style default message rendering. + */ + @SuppressWarnings("serial") + private static class ViolationFieldError extends FieldError implements Serializable { + + @Nullable + private transient SpringValidatorAdapter adapter; + + @Nullable + private transient ConstraintViolation violation; + + public ViolationFieldError(String objectName, String field, @Nullable Object rejectedValue, String[] codes, + Object[] arguments, ConstraintViolation violation, SpringValidatorAdapter adapter) { + + super(objectName, field, rejectedValue, false, codes, arguments, violation.getMessage()); + this.adapter = adapter; + this.violation = violation; + wrap(violation); + } + + @Override + public boolean shouldRenderDefaultMessage() { + return (this.adapter != null && this.violation != null ? + this.adapter.requiresMessageFormat(this.violation) : + containsSpringStylePlaceholder(getDefaultMessage())); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/package-info.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/package-info.java new file mode 100644 index 0000000..c367d4b --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/package-info.java @@ -0,0 +1,16 @@ +/** + * Support classes for integrating a JSR-303 Bean Validation provider + * (such as Hibernate Validator) into a Spring ApplicationContext + * and in particular with Spring's data binding and validation APIs. + * + *

    The central class is {@link + * org.springframework.validation.beanvalidation.LocalValidatorFactoryBean} + * which defines a shared ValidatorFactory/Validator setup for availability + * to other Spring components. + */ +@NonNullApi +@NonNullFields +package org.springframework.validation.beanvalidation; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/validation/package-info.java b/spring-context/src/main/java/org/springframework/validation/package-info.java new file mode 100644 index 0000000..cc26d2b --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/package-info.java @@ -0,0 +1,10 @@ +/** + * Provides data binding and validation functionality, + * for usage in business and/or UI layers. + */ +@NonNullApi +@NonNullFields +package org.springframework.validation; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/validation/support/BindingAwareConcurrentModel.java b/spring-context/src/main/java/org/springframework/validation/support/BindingAwareConcurrentModel.java new file mode 100644 index 0000000..61014d0 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/support/BindingAwareConcurrentModel.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation.support; + +import java.util.Map; + +import org.springframework.ui.ConcurrentModel; +import org.springframework.validation.BindingResult; + +/** + * Subclass of {@link ConcurrentModel} that automatically removes + * the {@link BindingResult} object when its corresponding + * target attribute is replaced through regular {@link Map} operations. + * + *

    This is the class exposed to handler methods by Spring WebFlux, + * typically consumed through a declaration of the + * {@link org.springframework.ui.Model} interface as a parameter type. + * There is typically no need to create it within user code. + * If necessary a handler method can return a regular {@code java.util.Map}, + * likely a {@code java.util.ConcurrentMap}, for a pre-determined model. + * + * @author Rossen Stoyanchev + * @since 5.0 + * @see BindingResult + */ +@SuppressWarnings("serial") +public class BindingAwareConcurrentModel extends ConcurrentModel { + + @Override + public Object put(String key, Object value) { + removeBindingResultIfNecessary(key, value); + return super.put(key, value); + } + + private void removeBindingResultIfNecessary(String key, Object value) { + if (!key.startsWith(BindingResult.MODEL_KEY_PREFIX)) { + String resultKey = BindingResult.MODEL_KEY_PREFIX + key; + BindingResult result = (BindingResult) get(resultKey); + if (result != null && result.getTarget() != value) { + remove(resultKey); + } + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/support/BindingAwareModelMap.java b/spring-context/src/main/java/org/springframework/validation/support/BindingAwareModelMap.java new file mode 100644 index 0000000..cfde93a --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/support/BindingAwareModelMap.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation.support; + +import java.util.Map; + +import org.springframework.ui.ExtendedModelMap; +import org.springframework.validation.BindingResult; + +/** + * Subclass of {@link org.springframework.ui.ExtendedModelMap} that automatically removes + * a {@link org.springframework.validation.BindingResult} object if the corresponding + * target attribute gets replaced through regular {@link Map} operations. + * + *

    This is the class exposed to handler methods by Spring MVC, typically consumed through + * a declaration of the {@link org.springframework.ui.Model} interface. There is no need to + * build it within user code; a plain {@link org.springframework.ui.ModelMap} or even a just + * a regular {@link Map} with String keys will be good enough to return a user model. + * + * @author Juergen Hoeller + * @since 2.5.6 + * @see org.springframework.validation.BindingResult + */ +@SuppressWarnings("serial") +public class BindingAwareModelMap extends ExtendedModelMap { + + @Override + public Object put(String key, Object value) { + removeBindingResultIfNecessary(key, value); + return super.put(key, value); + } + + @Override + public void putAll(Map map) { + map.forEach(this::removeBindingResultIfNecessary); + super.putAll(map); + } + + private void removeBindingResultIfNecessary(Object key, Object value) { + if (key instanceof String) { + String attributeName = (String) key; + if (!attributeName.startsWith(BindingResult.MODEL_KEY_PREFIX)) { + String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + attributeName; + BindingResult bindingResult = (BindingResult) get(bindingResultKey); + if (bindingResult != null && bindingResult.getTarget() != value) { + remove(bindingResultKey); + } + } + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/support/package-info.java b/spring-context/src/main/java/org/springframework/validation/support/package-info.java new file mode 100644 index 0000000..355c476 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/support/package-info.java @@ -0,0 +1,9 @@ +/** + * Support classes for handling validation results. + */ +@NonNullApi +@NonNullFields +package org.springframework.validation.support; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/kotlin/org/springframework/context/annotation/AnnotationConfigApplicationContextExtensions.kt b/spring-context/src/main/kotlin/org/springframework/context/annotation/AnnotationConfigApplicationContextExtensions.kt new file mode 100644 index 0000000..ee0e9ea --- /dev/null +++ b/spring-context/src/main/kotlin/org/springframework/context/annotation/AnnotationConfigApplicationContextExtensions.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation + +/** + * Extension for [AnnotationConfigApplicationContext] allowing + * `AnnotationConfigApplicationContext { ... }` style initialization. + * + * @author Sebastien Deleuze + * @since 5.0 + */ +@Deprecated("Use regular apply method instead.", replaceWith = ReplaceWith("AnnotationConfigApplicationContext().apply(configure)")) +fun AnnotationConfigApplicationContext(configure: AnnotationConfigApplicationContext.() -> Unit) = + AnnotationConfigApplicationContext().apply(configure) diff --git a/spring-context/src/main/kotlin/org/springframework/context/support/BeanDefinitionDsl.kt b/spring-context/src/main/kotlin/org/springframework/context/support/BeanDefinitionDsl.kt new file mode 100644 index 0000000..a2c9773 --- /dev/null +++ b/spring-context/src/main/kotlin/org/springframework/context/support/BeanDefinitionDsl.kt @@ -0,0 +1,1156 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support + +import org.springframework.beans.factory.ObjectProvider +import org.springframework.beans.factory.config.BeanDefinition +import org.springframework.beans.factory.config.BeanDefinitionCustomizer +import org.springframework.beans.factory.getBeanProvider +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils +import org.springframework.context.ApplicationContextInitializer +import org.springframework.core.env.ConfigurableEnvironment +import org.springframework.core.env.Profiles +import java.util.function.Supplier + +/** + * Functional bean definition Kotlin DSL. + * + * Example: + * + * ``` + * beans { + * bean() + * bean() + * bean("webHandler") { + * RouterFunctions.toWebHandler( + * ref().router(), + * HandlerStrategies.builder().viewResolver(ref()).build()) + * } + * bean("messageSource") { + * ReloadableResourceBundleMessageSource().apply { + * setBasename("messages") + * setDefaultEncoding("UTF-8") + * } + * } + * bean { + * val prefix = "classpath:/templates/" + * val suffix = ".mustache" + * val loader = MustacheResourceTemplateLoader(prefix, suffix) + * MustacheViewResolver(Mustache.compiler().withLoader(loader)).apply { + * setPrefix(prefix) + * setSuffix(suffix) + * } + * } + * profile("foo") { + * bean() + * } + * } + * ``` + * + * @author Sebastien Deleuze + * @see BeanDefinitionDsl + * @since 5.0 + */ +fun beans(init: BeanDefinitionDsl.() -> Unit) = BeanDefinitionDsl(init) + +/** + * Class implementing functional bean definition Kotlin DSL. + * + * @constructor Create a new bean definition DSL. + * @param condition the predicate to fulfill in order to take in account the inner + * bean definition block + * @author Sebastien Deleuze + * @since 5.0 + */ +open class BeanDefinitionDsl internal constructor (private val init: BeanDefinitionDsl.() -> Unit, + private val condition: (ConfigurableEnvironment) -> Boolean = { true }) + : ApplicationContextInitializer { + + @PublishedApi + internal val children = arrayListOf() + + /** + * @see BeanSupplierContext + */ + @PublishedApi + internal lateinit var context: GenericApplicationContext + + /** + * Shortcut for `context.environment` + * @since 5.1 + */ + val env : ConfigurableEnvironment + get() = context.environment + + /** + * Scope enum constants. + */ + enum class Scope { + + /** + * Scope constant for the standard singleton scope + * @see org.springframework.beans.factory.config.BeanDefinition.SCOPE_SINGLETON + */ + SINGLETON, + + /** + * Scope constant for the standard singleton scope + * @see org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE + */ + PROTOTYPE + } + + /** + * Role enum constants. + */ + enum class Role { + + /** + * Role hint indicating that a [BeanDefinition] is a major part + * of the application. Typically corresponds to a user-defined bean. + * @see org.springframework.beans.factory.config.BeanDefinition.ROLE_APPLICATION + */ + APPLICATION, + + /** + * Role hint indicating that a [BeanDefinition] is a supporting + * part of some larger configuration, typically an outer + * [org.springframework.beans.factory.parsing.ComponentDefinition]. + * [SUPPORT] beans are considered important enough to be aware of + * when looking more closely at a particular + * [org.springframework.beans.factory.parsing.ComponentDefinition], + * but not when looking at the overall configuration of an application. + * @see org.springframework.beans.factory.config.BeanDefinition.ROLE_SUPPORT + */ + SUPPORT, + + /** + * Role hint indicating that a [BeanDefinition] is providing an + * entirely background role and has no relevance to the end-user. This hint is + * used when registering beans that are completely part of the internal workings + * of a [org.springframework.beans.factory.parsing.ComponentDefinition]. + * @see org.springframework.beans.factory.config.BeanDefinition.ROLE_INFRASTRUCTURE + */ + INFRASTRUCTURE + } + + /** + * Declare a bean definition from the given bean class which can be inferred when possible. + * + *

    The preferred constructor (Kotlin primary constructor and standard public constructors) + * are evaluated for autowiring before falling back to default instantiation. + * + * @param name the name of the bean + * @param scope Override the target scope of this bean, specifying a new scope name. + * @param isLazyInit Set whether this bean should be lazily initialized. + * @param isPrimary Set whether this bean is a primary autowire candidate. + * @param isAutowireCandidate Set whether this bean is a candidate for getting + * autowired into some other bean. + * @param initMethodName Set the name of the initializer method + * @param destroyMethodName Set the name of the destroy method + * @param description Set a human-readable description of this bean definition + * @param role Set the role hint for this bean definition + * @see GenericApplicationContext.registerBean + * @see org.springframework.beans.factory.config.BeanDefinition + */ + inline fun bean(name: String? = null, + scope: Scope? = null, + isLazyInit: Boolean? = null, + isPrimary: Boolean? = null, + isAutowireCandidate: Boolean? = null, + initMethodName: String? = null, + destroyMethodName: String? = null, + description: String? = null, + role: Role? = null) { + + val customizer = BeanDefinitionCustomizer { bd -> + scope?.let { bd.scope = scope.name.toLowerCase() } + isLazyInit?.let { bd.isLazyInit = isLazyInit } + isPrimary?.let { bd.isPrimary = isPrimary } + isAutowireCandidate?.let { bd.isAutowireCandidate = isAutowireCandidate } + initMethodName?.let { bd.initMethodName = initMethodName } + destroyMethodName?.let { bd.destroyMethodName = destroyMethodName } + description?.let { bd.description = description } + role?. let { bd.role = role.ordinal } + } + + val beanName = name ?: BeanDefinitionReaderUtils.uniqueBeanName(T::class.java.name, context); + context.registerBean(beanName, T::class.java, customizer) + } + + /** + * Declare a bean definition using the given supplier for obtaining a new instance. + * + * @param name the name of the bean + * @param scope Override the target scope of this bean, specifying a new scope name. + * @param isLazyInit Set whether this bean should be lazily initialized. + * @param isPrimary Set whether this bean is a primary autowire candidate. + * @param isAutowireCandidate Set whether this bean is a candidate for getting + * autowired into some other bean. + * @param initMethodName Set the name of the initializer method + * @param destroyMethodName Set the name of the destroy method + * @param description Set a human-readable description of this bean definition + * @param role Set the role hint for this bean definition + * @param function the bean supplier function + * @see GenericApplicationContext.registerBean + * @see org.springframework.beans.factory.config.BeanDefinition + */ + inline fun bean(name: String? = null, + scope: Scope? = null, + isLazyInit: Boolean? = null, + isPrimary: Boolean? = null, + isAutowireCandidate: Boolean? = null, + initMethodName: String? = null, + destroyMethodName: String? = null, + description: String? = null, + role: Role? = null, + crossinline function: BeanSupplierContext.() -> T) { + + val customizer = BeanDefinitionCustomizer { bd -> + scope?.let { bd.scope = scope.name.toLowerCase() } + isLazyInit?.let { bd.isLazyInit = isLazyInit } + isPrimary?.let { bd.isPrimary = isPrimary } + isAutowireCandidate?.let { bd.isAutowireCandidate = isAutowireCandidate } + initMethodName?.let { bd.initMethodName = initMethodName } + destroyMethodName?.let { bd.destroyMethodName = destroyMethodName } + description?.let { bd.description = description } + role?. let { bd.role = role.ordinal } + } + + + val beanName = name ?: BeanDefinitionReaderUtils.uniqueBeanName(T::class.java.name, context); + context.registerBean(beanName, T::class.java, Supplier { function.invoke(BeanSupplierContext(context)) }, customizer) + } + + /** + * Declare a bean definition using the given callable reference with no parameter + * for obtaining a new instance. + * + * @param f the callable reference + * @param name the name of the bean + * @param scope Override the target scope of this bean, specifying a new scope name. + * @param isLazyInit Set whether this bean should be lazily initialized. + * @param isPrimary Set whether this bean is a primary autowire candidate. + * @param isAutowireCandidate Set whether this bean is a candidate for getting + * autowired into some other bean. + * @param initMethodName Set the name of the initializer method + * @param destroyMethodName Set the name of the destroy method + * @param description Set a human-readable description of this bean definition + * @param role Set the role hint for this bean definition + * @see GenericApplicationContext.registerBean + * @see org.springframework.beans.factory.config.BeanDefinition + * @since 5.2.3 + */ + inline fun + bean(crossinline f: () -> T, + name: String? = null, + scope: BeanDefinitionDsl.Scope? = null, + isLazyInit: Boolean? = null, + isPrimary: Boolean? = null, + isAutowireCandidate: Boolean? = null, + initMethodName: String? = null, + destroyMethodName: String? = null, + description: String? = null, + role: BeanDefinitionDsl.Role? = null) { + + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + f.invoke() + } + } + + /** + * Declare a bean definition using the given callable reference with 1 parameter + * autowired by type for obtaining a new instance. + * + * @param f the callable reference + * @param name the name of the bean + * @param scope Override the target scope of this bean, specifying a new scope name. + * @param isLazyInit Set whether this bean should be lazily initialized. + * @param isPrimary Set whether this bean is a primary autowire candidate. + * @param isAutowireCandidate Set whether this bean is a candidate for getting + * autowired into some other bean. + * @param initMethodName Set the name of the initializer method + * @param destroyMethodName Set the name of the destroy method + * @param description Set a human-readable description of this bean definition + * @param role Set the role hint for this bean definition + * @see GenericApplicationContext.registerBean + * @see org.springframework.beans.factory.config.BeanDefinition + * @since 5.2 + */ + inline fun + bean(crossinline f: (A) -> T, + name: String? = null, + scope: BeanDefinitionDsl.Scope? = null, + isLazyInit: Boolean? = null, + isPrimary: Boolean? = null, + isAutowireCandidate: Boolean? = null, + initMethodName: String? = null, + destroyMethodName: String? = null, + description: String? = null, + role: BeanDefinitionDsl.Role? = null) { + + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + f.invoke(ref()) + } + } + + /** + * Declare a bean definition using the given callable reference with 2 parameters + * autowired by type for obtaining a new instance. + * + * @param f the callable reference + * @param name the name of the bean + * @param scope Override the target scope of this bean, specifying a new scope name. + * @param isLazyInit Set whether this bean should be lazily initialized. + * @param isPrimary Set whether this bean is a primary autowire candidate. + * @param isAutowireCandidate Set whether this bean is a candidate for getting + * autowired into some other bean. + * @param initMethodName Set the name of the initializer method + * @param destroyMethodName Set the name of the destroy method + * @param description Set a human-readable description of this bean definition + * @param role Set the role hint for this bean definition + * @see GenericApplicationContext.registerBean + * @see org.springframework.beans.factory.config.BeanDefinition + * @since 5.2 + */ + inline fun + bean(crossinline f: (A, B) -> T, + name: String? = null, + scope: BeanDefinitionDsl.Scope? = null, + isLazyInit: Boolean? = null, + isPrimary: Boolean? = null, + isAutowireCandidate: Boolean? = null, + initMethodName: String? = null, + destroyMethodName: String? = null, + description: String? = null, + role: BeanDefinitionDsl.Role? = null) { + + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + f.invoke(ref(), ref()) + } + } + + /** + * Declare a bean definition using the given callable reference with 3 parameters + * autowired by type for obtaining a new instance. + * + * @param f the callable reference + * @param name the name of the bean + * @param scope Override the target scope of this bean, specifying a new scope name. + * @param isLazyInit Set whether this bean should be lazily initialized. + * @param isPrimary Set whether this bean is a primary autowire candidate. + * @param isAutowireCandidate Set whether this bean is a candidate for getting + * autowired into some other bean. + * @param initMethodName Set the name of the initializer method + * @param destroyMethodName Set the name of the destroy method + * @param description Set a human-readable description of this bean definition + * @param role Set the role hint for this bean definition + * @see GenericApplicationContext.registerBean + * @see org.springframework.beans.factory.config.BeanDefinition + * @since 5.2 + */ + inline fun + bean(crossinline f: (A, B, C) -> T, + name: String? = null, + scope: BeanDefinitionDsl.Scope? = null, + isLazyInit: Boolean? = null, + isPrimary: Boolean? = null, + isAutowireCandidate: Boolean? = null, + initMethodName: String? = null, + destroyMethodName: String? = null, + description: String? = null, + role: BeanDefinitionDsl.Role? = null) { + + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + f.invoke(ref(), ref(), ref()) + } + } + + /** + * Declare a bean definition using the given callable reference with 4 parameters + * autowired by type for obtaining a new instance. + * + * @param f the callable reference + * @param name the name of the bean + * @param scope Override the target scope of this bean, specifying a new scope name. + * @param isLazyInit Set whether this bean should be lazily initialized. + * @param isPrimary Set whether this bean is a primary autowire candidate. + * @param isAutowireCandidate Set whether this bean is a candidate for getting + * autowired into some other bean. + * @param initMethodName Set the name of the initializer method + * @param destroyMethodName Set the name of the destroy method + * @param description Set a human-readable description of this bean definition + * @param role Set the role hint for this bean definition + * @see GenericApplicationContext.registerBean + * @see org.springframework.beans.factory.config.BeanDefinition + * @since 5.2 + */ + inline fun + bean(crossinline f: (A, B, C, D) -> T, + name: String? = null, + scope: BeanDefinitionDsl.Scope? = null, + isLazyInit: Boolean? = null, + isPrimary: Boolean? = null, + isAutowireCandidate: Boolean? = null, + initMethodName: String? = null, + destroyMethodName: String? = null, + description: String? = null, + role: BeanDefinitionDsl.Role? = null) { + + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + f.invoke(ref(), ref(), ref(), ref()) + } + } + + /** + * Declare a bean definition using the given callable reference with 5 parameters + * autowired by type for obtaining a new instance. + * + * @param f the callable reference + * @param name the name of the bean + * @param scope Override the target scope of this bean, specifying a new scope name. + * @param isLazyInit Set whether this bean should be lazily initialized. + * @param isPrimary Set whether this bean is a primary autowire candidate. + * @param isAutowireCandidate Set whether this bean is a candidate for getting + * autowired into some other bean. + * @param initMethodName Set the name of the initializer method + * @param destroyMethodName Set the name of the destroy method + * @param description Set a human-readable description of this bean definition + * @param role Set the role hint for this bean definition + * @see GenericApplicationContext.registerBean + * @see org.springframework.beans.factory.config.BeanDefinition + * @since 5.2 + */ + inline fun + bean(crossinline f: (A, B, C, D, E) -> T, + name: String? = null, + scope: BeanDefinitionDsl.Scope? = null, + isLazyInit: Boolean? = null, + isPrimary: Boolean? = null, + isAutowireCandidate: Boolean? = null, + initMethodName: String? = null, + destroyMethodName: String? = null, + description: String? = null, + role: BeanDefinitionDsl.Role? = null) { + + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + f.invoke(ref(), ref(), ref(), ref(), ref()) + } + } + + /** + * Declare a bean definition using the given callable reference with 6 parameters + * autowired by type for obtaining a new instance. + * + * @param f the callable reference + * @param name the name of the bean + * @param scope Override the target scope of this bean, specifying a new scope name. + * @param isLazyInit Set whether this bean should be lazily initialized. + * @param isPrimary Set whether this bean is a primary autowire candidate. + * @param isAutowireCandidate Set whether this bean is a candidate for getting + * autowired into some other bean. + * @param initMethodName Set the name of the initializer method + * @param destroyMethodName Set the name of the destroy method + * @param description Set a human-readable description of this bean definition + * @param role Set the role hint for this bean definition + * @see GenericApplicationContext.registerBean + * @see org.springframework.beans.factory.config.BeanDefinition + * @since 5.2 + */ + inline fun + bean(crossinline f: (A, B, C, D, E, F) -> T, + name: String? = null, + scope: BeanDefinitionDsl.Scope? = null, + isLazyInit: Boolean? = null, + isPrimary: Boolean? = null, + isAutowireCandidate: Boolean? = null, + initMethodName: String? = null, + destroyMethodName: String? = null, + description: String? = null, + role: BeanDefinitionDsl.Role? = null) { + + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + f.invoke(ref(), ref(), ref(), ref(), ref(), ref()) + } + } + + /** + * Declare a bean definition using the given callable reference with 7 parameters + * autowired by type for obtaining a new instance. + * + * @param f the callable reference + * @param name the name of the bean + * @param scope Override the target scope of this bean, specifying a new scope name. + * @param isLazyInit Set whether this bean should be lazily initialized. + * @param isPrimary Set whether this bean is a primary autowire candidate. + * @param isAutowireCandidate Set whether this bean is a candidate for getting + * autowired into some other bean. + * @param initMethodName Set the name of the initializer method + * @param destroyMethodName Set the name of the destroy method + * @param description Set a human-readable description of this bean definition + * @param role Set the role hint for this bean definition + * @see GenericApplicationContext.registerBean + * @see org.springframework.beans.factory.config.BeanDefinition + * @since 5.2 + */ + inline fun + bean(crossinline f: (A, B, C, D, E, F, G) -> T, + name: String? = null, + scope: BeanDefinitionDsl.Scope? = null, + isLazyInit: Boolean? = null, + isPrimary: Boolean? = null, + isAutowireCandidate: Boolean? = null, + initMethodName: String? = null, + destroyMethodName: String? = null, + description: String? = null, + role: BeanDefinitionDsl.Role? = null) { + + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref()) + } + } + + /** + * Declare a bean definition using the given callable reference with 8 parameters + * autowired by type for obtaining a new instance. + * + * @param f the callable reference + * @param name the name of the bean + * @param scope Override the target scope of this bean, specifying a new scope name. + * @param isLazyInit Set whether this bean should be lazily initialized. + * @param isPrimary Set whether this bean is a primary autowire candidate. + * @param isAutowireCandidate Set whether this bean is a candidate for getting + * autowired into some other bean. + * @param initMethodName Set the name of the initializer method + * @param destroyMethodName Set the name of the destroy method + * @param description Set a human-readable description of this bean definition + * @param role Set the role hint for this bean definition + * @see GenericApplicationContext.registerBean + * @see org.springframework.beans.factory.config.BeanDefinition + * @since 5.2 + */ + inline fun + bean(crossinline f: (A, B, C, D, E, F, G, H) -> T, + name: String? = null, + scope: BeanDefinitionDsl.Scope? = null, + isLazyInit: Boolean? = null, + isPrimary: Boolean? = null, + isAutowireCandidate: Boolean? = null, + initMethodName: String? = null, + destroyMethodName: String? = null, + description: String? = null, + role: BeanDefinitionDsl.Role? = null) { + + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref()) + } + } + + /** + * Declare a bean definition using the given callable reference with 9 parameters + * autowired by type for obtaining a new instance. + * + * @param f the callable reference + * @param name the name of the bean + * @param scope Override the target scope of this bean, specifying a new scope name. + * @param isLazyInit Set whether this bean should be lazily initialized. + * @param isPrimary Set whether this bean is a primary autowire candidate. + * @param isAutowireCandidate Set whether this bean is a candidate for getting + * autowired into some other bean. + * @param initMethodName Set the name of the initializer method + * @param destroyMethodName Set the name of the destroy method + * @param description Set a human-readable description of this bean definition + * @param role Set the role hint for this bean definition + * @see GenericApplicationContext.registerBean + * @see org.springframework.beans.factory.config.BeanDefinition + * @since 5.2 + */ + inline fun + bean(crossinline f: (A, B, C, D, E, F, G, H, I) -> T, + name: String? = null, + scope: BeanDefinitionDsl.Scope? = null, + isLazyInit: Boolean? = null, + isPrimary: Boolean? = null, + isAutowireCandidate: Boolean? = null, + initMethodName: String? = null, + destroyMethodName: String? = null, + description: String? = null, + role: BeanDefinitionDsl.Role? = null) { + + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref()) + } + } + + /** + * Declare a bean definition using the given callable reference with 10 parameters + * autowired by type for obtaining a new instance. + * + * @param f the callable reference + * @param name the name of the bean + * @param scope Override the target scope of this bean, specifying a new scope name. + * @param isLazyInit Set whether this bean should be lazily initialized. + * @param isPrimary Set whether this bean is a primary autowire candidate. + * @param isAutowireCandidate Set whether this bean is a candidate for getting + * autowired into some other bean. + * @param initMethodName Set the name of the initializer method + * @param destroyMethodName Set the name of the destroy method + * @param description Set a human-readable description of this bean definition + * @param role Set the role hint for this bean definition + * @see GenericApplicationContext.registerBean + * @see org.springframework.beans.factory.config.BeanDefinition + * @since 5.2 + */ + inline fun + bean(crossinline f: (A, B, C, D, E, F, G, H, I, J) -> T, + name: String? = null, + scope: BeanDefinitionDsl.Scope? = null, + isLazyInit: Boolean? = null, + isPrimary: Boolean? = null, + isAutowireCandidate: Boolean? = null, + initMethodName: String? = null, + destroyMethodName: String? = null, + description: String? = null, + role: BeanDefinitionDsl.Role? = null) { + + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref()) + } + } + + /** + * Declare a bean definition using the given callable reference with 11 parameters + * autowired by type for obtaining a new instance. + * + * @param f the callable reference + * @param name the name of the bean + * @param scope Override the target scope of this bean, specifying a new scope name. + * @param isLazyInit Set whether this bean should be lazily initialized. + * @param isPrimary Set whether this bean is a primary autowire candidate. + * @param isAutowireCandidate Set whether this bean is a candidate for getting + * autowired into some other bean. + * @param initMethodName Set the name of the initializer method + * @param destroyMethodName Set the name of the destroy method + * @param description Set a human-readable description of this bean definition + * @param role Set the role hint for this bean definition + * @see GenericApplicationContext.registerBean + * @see org.springframework.beans.factory.config.BeanDefinition + * @since 5.2 + */ + inline fun + bean(crossinline f: (A, B, C, D, E, F, G, H, I, J, K) -> T, + name: String? = null, + scope: BeanDefinitionDsl.Scope? = null, + isLazyInit: Boolean? = null, + isPrimary: Boolean? = null, + isAutowireCandidate: Boolean? = null, + initMethodName: String? = null, + destroyMethodName: String? = null, + description: String? = null, + role: BeanDefinitionDsl.Role? = null) { + + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref()) + } + } + + /** + * Declare a bean definition using the given callable reference with 12 parameters + * autowired by type for obtaining a new instance. + * + * @param f the callable reference + * @param name the name of the bean + * @param scope Override the target scope of this bean, specifying a new scope name. + * @param isLazyInit Set whether this bean should be lazily initialized. + * @param isPrimary Set whether this bean is a primary autowire candidate. + * @param isAutowireCandidate Set whether this bean is a candidate for getting + * autowired into some other bean. + * @param initMethodName Set the name of the initializer method + * @param destroyMethodName Set the name of the destroy method + * @param description Set a human-readable description of this bean definition + * @param role Set the role hint for this bean definition + * @see GenericApplicationContext.registerBean + * @see org.springframework.beans.factory.config.BeanDefinition + * @since 5.2 + */ + inline fun + bean(crossinline f: (A, B, C, D, E, F, G, H, I, J, K, L) -> T, + name: String? = null, + scope: BeanDefinitionDsl.Scope? = null, + isLazyInit: Boolean? = null, + isPrimary: Boolean? = null, + isAutowireCandidate: Boolean? = null, + initMethodName: String? = null, + destroyMethodName: String? = null, + description: String? = null, + role: BeanDefinitionDsl.Role? = null) { + + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref()) + } + } + + /** + * Declare a bean definition using the given callable reference with 13 parameters + * autowired by type for obtaining a new instance. + * + * @param f the callable reference + * @param name the name of the bean + * @param scope Override the target scope of this bean, specifying a new scope name. + * @param isLazyInit Set whether this bean should be lazily initialized. + * @param isPrimary Set whether this bean is a primary autowire candidate. + * @param isAutowireCandidate Set whether this bean is a candidate for getting + * autowired into some other bean. + * @param initMethodName Set the name of the initializer method + * @param destroyMethodName Set the name of the destroy method + * @param description Set a human-readable description of this bean definition + * @param role Set the role hint for this bean definition + * @see GenericApplicationContext.registerBean + * @see org.springframework.beans.factory.config.BeanDefinition + * @since 5.2 + */ + inline fun + bean(crossinline f: (A, B, C, D, E, F, G, H, I, J, K, L, M) -> T, + name: String? = null, + scope: BeanDefinitionDsl.Scope? = null, + isLazyInit: Boolean? = null, + isPrimary: Boolean? = null, + isAutowireCandidate: Boolean? = null, + initMethodName: String? = null, + destroyMethodName: String? = null, + description: String? = null, + role: BeanDefinitionDsl.Role? = null) { + + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref()) + } + } + + /** + * Declare a bean definition using the given callable reference with 14 parameters + * autowired by type for obtaining a new instance. + * + * @param f the callable reference + * @param name the name of the bean + * @param scope Override the target scope of this bean, specifying a new scope name. + * @param isLazyInit Set whether this bean should be lazily initialized. + * @param isPrimary Set whether this bean is a primary autowire candidate. + * @param isAutowireCandidate Set whether this bean is a candidate for getting + * autowired into some other bean. + * @param initMethodName Set the name of the initializer method + * @param destroyMethodName Set the name of the destroy method + * @param description Set a human-readable description of this bean definition + * @param role Set the role hint for this bean definition + * @see GenericApplicationContext.registerBean + * @see org.springframework.beans.factory.config.BeanDefinition + * @since 5.2 + */ + inline fun + bean(crossinline f: (A, B, C, D, E, F, G, H, I, J, K, L, M, N) -> T, + name: String? = null, + scope: BeanDefinitionDsl.Scope? = null, + isLazyInit: Boolean? = null, + isPrimary: Boolean? = null, + isAutowireCandidate: Boolean? = null, + initMethodName: String? = null, + destroyMethodName: String? = null, + description: String? = null, + role: BeanDefinitionDsl.Role? = null) { + + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref()) + } + } + + /** + * Declare a bean definition using the given callable reference with 15 parameters + * autowired by type for obtaining a new instance. + * + * @param f the callable reference + * @param name the name of the bean + * @param scope Override the target scope of this bean, specifying a new scope name. + * @param isLazyInit Set whether this bean should be lazily initialized. + * @param isPrimary Set whether this bean is a primary autowire candidate. + * @param isAutowireCandidate Set whether this bean is a candidate for getting + * autowired into some other bean. + * @param initMethodName Set the name of the initializer method + * @param destroyMethodName Set the name of the destroy method + * @param description Set a human-readable description of this bean definition + * @param role Set the role hint for this bean definition + * @see GenericApplicationContext.registerBean + * @see org.springframework.beans.factory.config.BeanDefinition + * @since 5.2 + */ + inline fun + bean(crossinline f: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O) -> T, + name: String? = null, + scope: BeanDefinitionDsl.Scope? = null, + isLazyInit: Boolean? = null, + isPrimary: Boolean? = null, + isAutowireCandidate: Boolean? = null, + initMethodName: String? = null, + destroyMethodName: String? = null, + description: String? = null, + role: BeanDefinitionDsl.Role? = null) { + + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref()) + } + } + + /** + * Declare a bean definition using the given callable reference with 16 parameters + * autowired by type for obtaining a new instance. + * + * @param f the callable reference + * @param name the name of the bean + * @param scope Override the target scope of this bean, specifying a new scope name. + * @param isLazyInit Set whether this bean should be lazily initialized. + * @param isPrimary Set whether this bean is a primary autowire candidate. + * @param isAutowireCandidate Set whether this bean is a candidate for getting + * autowired into some other bean. + * @param initMethodName Set the name of the initializer method + * @param destroyMethodName Set the name of the destroy method + * @param description Set a human-readable description of this bean definition + * @param role Set the role hint for this bean definition + * @see GenericApplicationContext.registerBean + * @see org.springframework.beans.factory.config.BeanDefinition + * @since 5.2 + */ + inline fun + bean(crossinline f: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P) -> T, + name: String? = null, + scope: BeanDefinitionDsl.Scope? = null, + isLazyInit: Boolean? = null, + isPrimary: Boolean? = null, + isAutowireCandidate: Boolean? = null, + initMethodName: String? = null, + destroyMethodName: String? = null, + description: String? = null, + role: BeanDefinitionDsl.Role? = null) { + + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref()) + } + } + + /** + * Declare a bean definition using the given callable reference with 17 parameters + * autowired by type for obtaining a new instance. + * + * @param f the callable reference + * @param name the name of the bean + * @param scope Override the target scope of this bean, specifying a new scope name. + * @param isLazyInit Set whether this bean should be lazily initialized. + * @param isPrimary Set whether this bean is a primary autowire candidate. + * @param isAutowireCandidate Set whether this bean is a candidate for getting + * autowired into some other bean. + * @param initMethodName Set the name of the initializer method + * @param destroyMethodName Set the name of the destroy method + * @param description Set a human-readable description of this bean definition + * @param role Set the role hint for this bean definition + * @see GenericApplicationContext.registerBean + * @see org.springframework.beans.factory.config.BeanDefinition + * @since 5.2 + */ + inline fun + bean(crossinline f: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q) -> T, + name: String? = null, + scope: BeanDefinitionDsl.Scope? = null, + isLazyInit: Boolean? = null, + isPrimary: Boolean? = null, + isAutowireCandidate: Boolean? = null, + initMethodName: String? = null, + destroyMethodName: String? = null, + description: String? = null, + role: BeanDefinitionDsl.Role? = null) { + + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref()) + } + } + + /** + * Declare a bean definition using the given callable reference with 18 parameters + * autowired by type for obtaining a new instance. + * + * @param f the callable reference + * @param name the name of the bean + * @param scope Override the target scope of this bean, specifying a new scope name. + * @param isLazyInit Set whether this bean should be lazily initialized. + * @param isPrimary Set whether this bean is a primary autowire candidate. + * @param isAutowireCandidate Set whether this bean is a candidate for getting + * autowired into some other bean. + * @param initMethodName Set the name of the initializer method + * @param destroyMethodName Set the name of the destroy method + * @param description Set a human-readable description of this bean definition + * @param role Set the role hint for this bean definition + * @see GenericApplicationContext.registerBean + * @see org.springframework.beans.factory.config.BeanDefinition + * @since 5.2 + */ + inline fun + bean(crossinline f: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R) -> T, + name: String? = null, + scope: BeanDefinitionDsl.Scope? = null, + isLazyInit: Boolean? = null, + isPrimary: Boolean? = null, + isAutowireCandidate: Boolean? = null, + initMethodName: String? = null, + destroyMethodName: String? = null, + description: String? = null, + role: BeanDefinitionDsl.Role? = null) { + + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref()) + } + } + + /** + * Declare a bean definition using the given callable reference with 19 parameters + * autowired by type for obtaining a new instance. + * + * @param f the callable reference + * @param name the name of the bean + * @param scope Override the target scope of this bean, specifying a new scope name. + * @param isLazyInit Set whether this bean should be lazily initialized. + * @param isPrimary Set whether this bean is a primary autowire candidate. + * @param isAutowireCandidate Set whether this bean is a candidate for getting + * autowired into some other bean. + * @param initMethodName Set the name of the initializer method + * @param destroyMethodName Set the name of the destroy method + * @param description Set a human-readable description of this bean definition + * @param role Set the role hint for this bean definition + * @see GenericApplicationContext.registerBean + * @see org.springframework.beans.factory.config.BeanDefinition + * @since 5.2 + */ + inline fun + bean(crossinline f: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S) -> T, + name: String? = null, + scope: BeanDefinitionDsl.Scope? = null, + isLazyInit: Boolean? = null, + isPrimary: Boolean? = null, + isAutowireCandidate: Boolean? = null, + initMethodName: String? = null, + destroyMethodName: String? = null, + description: String? = null, + role: BeanDefinitionDsl.Role? = null) { + + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref()) + } + } + + /** + * Declare a bean definition using the given callable reference with 20 parameters + * autowired by type for obtaining a new instance. + * + * @param f the callable reference + * @param name the name of the bean + * @param scope Override the target scope of this bean, specifying a new scope name. + * @param isLazyInit Set whether this bean should be lazily initialized. + * @param isPrimary Set whether this bean is a primary autowire candidate. + * @param isAutowireCandidate Set whether this bean is a candidate for getting + * autowired into some other bean. + * @param initMethodName Set the name of the initializer method + * @param destroyMethodName Set the name of the destroy method + * @param description Set a human-readable description of this bean definition + * @param role Set the role hint for this bean definition + * @see GenericApplicationContext.registerBean + * @see org.springframework.beans.factory.config.BeanDefinition + * @since 5.2 + */ + inline fun + bean(crossinline f: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, U) -> T, + name: String? = null, + scope: BeanDefinitionDsl.Scope? = null, + isLazyInit: Boolean? = null, + isPrimary: Boolean? = null, + isAutowireCandidate: Boolean? = null, + initMethodName: String? = null, + destroyMethodName: String? = null, + description: String? = null, + role: BeanDefinitionDsl.Role? = null) { + + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref()) + } + } + + /** + * Declare a bean definition using the given callable reference with 21 parameters + * autowired by type for obtaining a new instance. + * + * @param f the callable reference + * @param name the name of the bean + * @param scope Override the target scope of this bean, specifying a new scope name. + * @param isLazyInit Set whether this bean should be lazily initialized. + * @param isPrimary Set whether this bean is a primary autowire candidate. + * @param isAutowireCandidate Set whether this bean is a candidate for getting + * autowired into some other bean. + * @param initMethodName Set the name of the initializer method + * @param destroyMethodName Set the name of the destroy method + * @param description Set a human-readable description of this bean definition + * @param role Set the role hint for this bean definition + * @see GenericApplicationContext.registerBean + * @see org.springframework.beans.factory.config.BeanDefinition + * @since 5.2 + */ + inline fun + bean(crossinline f: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, U, V) -> T, + name: String? = null, + scope: BeanDefinitionDsl.Scope? = null, + isLazyInit: Boolean? = null, + isPrimary: Boolean? = null, + isAutowireCandidate: Boolean? = null, + initMethodName: String? = null, + destroyMethodName: String? = null, + description: String? = null, + role: BeanDefinitionDsl.Role? = null) { + + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref()) + } + } + + /** + * Declare a bean definition using the given callable reference with 22 parameters + * autowired by type for obtaining a new instance. + * + * @param f the callable reference + * @param name the name of the bean + * @param scope Override the target scope of this bean, specifying a new scope name. + * @param isLazyInit Set whether this bean should be lazily initialized. + * @param isPrimary Set whether this bean is a primary autowire candidate. + * @param isAutowireCandidate Set whether this bean is a candidate for getting + * autowired into some other bean. + * @param initMethodName Set the name of the initializer method + * @param destroyMethodName Set the name of the destroy method + * @param description Set a human-readable description of this bean definition + * @param role Set the role hint for this bean definition + * @see GenericApplicationContext.registerBean + * @see org.springframework.beans.factory.config.BeanDefinition + * @since 5.2 + */ + inline fun + bean(crossinline f: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, U, V, W) -> T, + name: String? = null, + scope: BeanDefinitionDsl.Scope? = null, + isLazyInit: Boolean? = null, + isPrimary: Boolean? = null, + isAutowireCandidate: Boolean? = null, + initMethodName: String? = null, + destroyMethodName: String? = null, + description: String? = null, + role: BeanDefinitionDsl.Role? = null) { + + bean(name, scope, isLazyInit, isPrimary, isAutowireCandidate, initMethodName, destroyMethodName, description, role) { + f.invoke(ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref(), ref()) + } + } + + /** + * Limit access to `ref()` and `provider()` to bean supplier lambdas. + * @since 5.2 + */ + open class BeanSupplierContext(@PublishedApi internal val context: GenericApplicationContext) { + + /** + * Get a reference to the bean by type or type + name with the syntax + * `ref()` or `ref("foo")`. When leveraging Kotlin type inference + * it could be as short as `ref()` or `ref("foo")`. + * @param name the name of the bean to retrieve + * @param T type the bean must match, can be an interface or superclass + */ + inline fun ref(name: String? = null) : T = when (name) { + null -> context.getBean(T::class.java) + else -> context.getBean(name, T::class.java) + } + + /** + * Return an provider for the specified bean, allowing for lazy on-demand retrieval + * of instances, including availability and uniqueness options. + * @see org.springframework.beans.factory.BeanFactory.getBeanProvider + */ + inline fun provider() : ObjectProvider = context.getBeanProvider() + + } + + /** + * Take in account bean definitions enclosed in the provided lambda when the + * profile is accepted. + * @see org.springframework.core.env.Profiles.of + */ + fun profile(profile: String, init: BeanDefinitionDsl.() -> Unit) { + val beans = BeanDefinitionDsl(init, { it.acceptsProfiles(Profiles.of(profile)) }) + children.add(beans) + } + + /** + * Take in account bean definitions enclosed in the provided lambda only when the + * specified environment-based predicate is true. + * @param condition the predicate to fulfill in order to take in account the inner + * bean definition block + */ + fun environment(condition: ConfigurableEnvironment.() -> Boolean, + init: BeanDefinitionDsl.() -> Unit) { + val beans = BeanDefinitionDsl(init, condition::invoke) + children.add(beans) + } + + /** + * Register the bean defined via the DSL on the provided application context. + * @param context The `ApplicationContext` to use for registering the beans + */ + override fun initialize(context: GenericApplicationContext) { + this.context = context + init() + for (child in children) { + if (child.condition.invoke(context.environment)) { + child.initialize(context) + } + } + } +} diff --git a/spring-context/src/main/kotlin/org/springframework/context/support/GenericApplicationContextExtensions.kt b/spring-context/src/main/kotlin/org/springframework/context/support/GenericApplicationContextExtensions.kt new file mode 100644 index 0000000..27076b2 --- /dev/null +++ b/spring-context/src/main/kotlin/org/springframework/context/support/GenericApplicationContextExtensions.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support + +import org.springframework.beans.factory.config.BeanDefinitionCustomizer +import org.springframework.context.ApplicationContext +import java.util.function.Supplier + +/** + * Extension for [GenericApplicationContext.registerBean] providing a + * `registerBean()` variant. + * + * @author Sebastien Deleuze + * @since 5.0 + */ +inline fun GenericApplicationContext.registerBean(vararg customizers: BeanDefinitionCustomizer) { + registerBean(T::class.java, *customizers) +} + +/** + * Extension for [GenericApplicationContext.registerBean] providing a + * `registerBean("foo")` variant. + * + * @author Sebastien Deleuze + * @since 5.0 + */ +inline fun GenericApplicationContext.registerBean(beanName: String, + vararg customizers: BeanDefinitionCustomizer) { + registerBean(beanName, T::class.java, *customizers) +} + +/** + * Extension for [GenericApplicationContext.registerBean] providing a `registerBean { Foo() }` variant. + * + * @author Sebastien Deleuze + * @since 5.0 + */ +inline fun GenericApplicationContext.registerBean( + vararg customizers: BeanDefinitionCustomizer, crossinline function: (ApplicationContext) -> T) { + registerBean(T::class.java, Supplier { function.invoke(this) }, *customizers) +} + +/** + * Extension for [GenericApplicationContext.registerBean] providing a + * `registerBean("foo") { Foo() }` variant. + * + * @author Sebastien Deleuze + * @since 5.0 + */ +inline fun GenericApplicationContext.registerBean(name: String, + vararg customizers: BeanDefinitionCustomizer, crossinline function: (ApplicationContext) -> T) { + registerBean(name, T::class.java, Supplier { function.invoke(this) }, *customizers) +} + +/** + * Extension for [GenericApplicationContext] allowing `GenericApplicationContext { ... }` + * style initialization. + * + * @author Sebastien Deleuze + * @since 5.0 + */ +@Deprecated("Use regular apply method instead.", replaceWith = ReplaceWith("GenericApplicationContext().apply(configure)")) +fun GenericApplicationContext(configure: GenericApplicationContext.() -> Unit) = + GenericApplicationContext().apply(configure) + diff --git a/spring-context/src/main/kotlin/org/springframework/ui/ModelExtensions.kt b/spring-context/src/main/kotlin/org/springframework/ui/ModelExtensions.kt new file mode 100644 index 0000000..22ee0c6 --- /dev/null +++ b/spring-context/src/main/kotlin/org/springframework/ui/ModelExtensions.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ui + +/** + * Extension for [Model.addAttribute] providing Array like setter. + * + * ```kotlin + * model["firstName"] = "Mario" + * ``` + * + * @author Mario Arias + * @since 5.0 + */ +operator fun Model.set(attributeName: String, attributeValue: Any) { + this.addAttribute(attributeName, attributeValue) +} diff --git a/spring-context/src/main/kotlin/org/springframework/ui/ModelMapExtensions.kt b/spring-context/src/main/kotlin/org/springframework/ui/ModelMapExtensions.kt new file mode 100644 index 0000000..7b95ac8 --- /dev/null +++ b/spring-context/src/main/kotlin/org/springframework/ui/ModelMapExtensions.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ui + +/** + * + * Extension for [ModelMap] providing Array like setter. + * + * ```kotlin + * model["firstName"] = "Mario" + * ``` + * + * @author Mario Arias + * @since 5.0 + */ +operator fun ModelMap.set(attributeName: String, attributeValue: Any) { + this.addAttribute(attributeName, attributeValue) +} diff --git a/spring-context/src/main/resources/org/springframework/cache/config/spring-cache.gif b/spring-context/src/main/resources/org/springframework/cache/config/spring-cache.gif new file mode 100644 index 0000000..d992999 Binary files /dev/null and b/spring-context/src/main/resources/org/springframework/cache/config/spring-cache.gif differ diff --git a/spring-context/src/main/resources/org/springframework/cache/config/spring-cache.xsd b/spring-context/src/main/resources/org/springframework/cache/config/spring-cache.xsd new file mode 100644 index 0000000..e72012c --- /dev/null +++ b/spring-context/src/main/resources/org/springframework/cache/config/spring-cache.xsd @@ -0,0 +1,313 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/main/resources/org/springframework/context/config/spring-context.gif b/spring-context/src/main/resources/org/springframework/context/config/spring-context.gif new file mode 100644 index 0000000..371c0cd Binary files /dev/null and b/spring-context/src/main/resources/org/springframework/context/config/spring-context.gif differ diff --git a/spring-context/src/main/resources/org/springframework/context/config/spring-context.xsd b/spring-context/src/main/resources/org/springframework/context/config/spring-context.xsd new file mode 100644 index 0000000..86f0ad8 --- /dev/null +++ b/spring-context/src/main/resources/org/springframework/context/config/spring-context.xsd @@ -0,0 +1,547 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + tag for that purpose. + + See javadoc for org.springframework.context.annotation.AnnotationConfigApplicationContext + for information on code-based alternatives to bootstrapping annotation-driven support. + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/main/resources/org/springframework/ejb/config/spring-jee.gif b/spring-context/src/main/resources/org/springframework/ejb/config/spring-jee.gif new file mode 100644 index 0000000..fbd89e2 Binary files /dev/null and b/spring-context/src/main/resources/org/springframework/ejb/config/spring-jee.gif differ diff --git a/spring-context/src/main/resources/org/springframework/ejb/config/spring-jee.xsd b/spring-context/src/main/resources/org/springframework/ejb/config/spring-jee.xsd new file mode 100644 index 0000000..ad681b1 --- /dev/null +++ b/spring-context/src/main/resources/org/springframework/ejb/config/spring-jee.xsd @@ -0,0 +1,267 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/main/resources/org/springframework/remoting/rmi/RmiInvocationWrapperRTD.xml b/spring-context/src/main/resources/org/springframework/remoting/rmi/RmiInvocationWrapperRTD.xml new file mode 100644 index 0000000..3ea5d62 --- /dev/null +++ b/spring-context/src/main/resources/org/springframework/remoting/rmi/RmiInvocationWrapperRTD.xml @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/spring-context/src/main/resources/org/springframework/scheduling/config/spring-task.gif b/spring-context/src/main/resources/org/springframework/scheduling/config/spring-task.gif new file mode 100644 index 0000000..da8b8f1 Binary files /dev/null and b/spring-context/src/main/resources/org/springframework/scheduling/config/spring-task.gif differ diff --git a/spring-context/src/main/resources/org/springframework/scheduling/config/spring-task.xsd b/spring-context/src/main/resources/org/springframework/scheduling/config/spring-task.xsd new file mode 100644 index 0000000..414a2a3 --- /dev/null +++ b/spring-context/src/main/resources/org/springframework/scheduling/config/spring-task.xsd @@ -0,0 +1,309 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/main/resources/org/springframework/scripting/config/spring-lang.gif b/spring-context/src/main/resources/org/springframework/scripting/config/spring-lang.gif new file mode 100644 index 0000000..d992999 Binary files /dev/null and b/spring-context/src/main/resources/org/springframework/scripting/config/spring-lang.gif differ diff --git a/spring-context/src/main/resources/org/springframework/scripting/config/spring-lang.xsd b/spring-context/src/main/resources/org/springframework/scripting/config/spring-lang.xsd new file mode 100644 index 0000000..dbb83dd --- /dev/null +++ b/spring-context/src/main/resources/org/springframework/scripting/config/spring-lang.xsd @@ -0,0 +1,240 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/groovy/org/springframework/context/groovy/GroovyApplicationContextDynamicBeanPropertyTests.groovy b/spring-context/src/test/groovy/org/springframework/context/groovy/GroovyApplicationContextDynamicBeanPropertyTests.groovy new file mode 100644 index 0000000..948fef8 --- /dev/null +++ b/spring-context/src/test/groovy/org/springframework/context/groovy/GroovyApplicationContextDynamicBeanPropertyTests.groovy @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.groovy + +import org.junit.jupiter.api.Test + +import org.springframework.beans.factory.NoSuchBeanDefinitionException +import org.springframework.context.support.GenericGroovyApplicationContext + +import static groovy.test.GroovyAssert.* + +/** + * @author Jeff Brown + * @author Sam Brannen + */ +class GroovyApplicationContextDynamicBeanPropertyTests { + + @Test + void testAccessDynamicBeanProperties() { + def ctx = new GenericGroovyApplicationContext(); + ctx.reader.loadBeanDefinitions("org/springframework/context/groovy/applicationContext.groovy"); + ctx.refresh() + + def framework = ctx.framework + assertNotNull 'could not find framework bean', framework + assertEquals 'Grails', framework + } + + @Test + void testAccessingNonExistentBeanViaDynamicProperty() { + def ctx = new GenericGroovyApplicationContext(); + ctx.reader.loadBeanDefinitions("org/springframework/context/groovy/applicationContext.groovy"); + ctx.refresh() + + def err = shouldFail NoSuchBeanDefinitionException, { ctx.someNonExistentBean } + + assertEquals "No bean named 'someNonExistentBean' available", err.message + } + +} diff --git a/spring-context/src/test/groovy/org/springframework/context/groovy/GroovyBeanDefinitionReaderTests.groovy b/spring-context/src/test/groovy/org/springframework/context/groovy/GroovyBeanDefinitionReaderTests.groovy new file mode 100644 index 0000000..eac5493 --- /dev/null +++ b/spring-context/src/test/groovy/org/springframework/context/groovy/GroovyBeanDefinitionReaderTests.groovy @@ -0,0 +1,1063 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.groovy + +import org.junit.jupiter.api.Test + +import org.springframework.aop.SpringProxy +import org.springframework.beans.factory.ObjectFactory +import org.springframework.beans.factory.config.Scope +import org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader +import org.springframework.context.support.GenericApplicationContext +import org.springframework.context.support.GenericGroovyApplicationContext +import org.springframework.stereotype.Component + +import static groovy.test.GroovyAssert.* + +/** + * @author Jeff Brown + * @author Sam Brannen + */ +class GroovyBeanDefinitionReaderTests { + + @Test + void importSpringXml() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + + reader.beans { + importBeans "classpath:org/springframework/context/groovy/test.xml" + } + + appCtx.refresh() + + def foo = appCtx.getBean("foo") + assertEquals "hello", foo + } + + @Test + void importBeansFromGroovy() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + + reader.beans { + importBeans "classpath:org/springframework/context/groovy/applicationContext.groovy" + } + + appCtx.refresh() + + def foo = appCtx.getBean("foo") + assertEquals "hello", foo + } + + @Test + void singletonPropertyOnBeanDefinition() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + singletonBean(Bean1) { bean -> + bean.singleton = true + } + nonSingletonBean(Bean1) { bean -> + bean.singleton = false + } + unSpecifiedScopeBean(Bean1) + } + appCtx.refresh() + + assertTrue 'singletonBean should have been a singleton', appCtx.isSingleton('singletonBean') + assertFalse 'nonSingletonBean should not have been a singleton', appCtx.isSingleton('nonSingletonBean') + assertTrue 'unSpecifiedScopeBean should not have been a singleton', appCtx.isSingleton('unSpecifiedScopeBean') + } + + @Test + void inheritPropertiesFromAbstractBean() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + + reader.beans { + myB(Bean1){ + person = "wombat" + } + + myAbstractA(Bean2){ bean -> + bean.'abstract' = true + age = 10 + bean1 = myB + } + myConcreteB { + it.parent = myAbstractA + } + } + + appCtx.refresh() + + def bean = appCtx.getBean("myConcreteB") + assertEquals 10, bean.age + assertNotNull bean.bean1 + } + + @Test + void contextComponentScanSpringTag() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + + reader.beans { + xmlns context:"http://www.springframework.org/schema/context" + + context.'component-scan'( 'base-package' :" org.springframework.context.groovy" ) + } + + appCtx.refresh() + + def p = appCtx.getBean("person") + assertTrue(p instanceof AdvisedPerson) + } + + @Test + void useSpringNamespaceAsMethod() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + + reader.beans { + xmlns aop:"http://www.springframework.org/schema/aop" + + fred(AdvisedPerson) { + name = "Fred" + age = 45 + } + birthdayCardSenderAspect(BirthdayCardSender) + + aop { + config("proxy-target-class":true) { + aspect( id:"sendBirthdayCard",ref:"birthdayCardSenderAspect" ) { + after method:"onBirthday", pointcut: "execution(void org.springframework.context.groovy.AdvisedPerson.birthday()) and this(person)" + } + } + } + } + + appCtx.refresh() + + def fred = appCtx.getBean("fred") + assertTrue (fred instanceof SpringProxy) + fred.birthday() + + BirthdayCardSender birthDaySender = appCtx.getBean("birthdayCardSenderAspect") + + assertEquals 1, birthDaySender.peopleSentCards.size() + assertEquals "Fred", birthDaySender.peopleSentCards[0].name + } + + @Test + void useTwoSpringNamespaces() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + TestScope scope = new TestScope() + appCtx.getBeanFactory().registerScope("test", scope) + + reader.beans { + xmlns aop:"http://www.springframework.org/schema/aop" + xmlns util:"http://www.springframework.org/schema/util" + scopedList(ArrayList) { bean -> + bean.scope = "test" + aop.'scoped-proxy'() + } + util.list(id: 'foo') { + value 'one' + value 'two' + } + } + appCtx.refresh() + + assert ['one', 'two'] == appCtx.getBean("foo") + + assertNotNull appCtx.getBean("scopedList") + assertNotNull appCtx.getBean("scopedList").size() + assertNotNull appCtx.getBean("scopedList").size() + + // should only be true because bean not initialized until proxy called + assertEquals 2, scope.instanceCount + + appCtx = new GenericApplicationContext() + reader = new GroovyBeanDefinitionReader(appCtx) + appCtx.getBeanFactory().registerScope("test", scope) + + reader.beans { + xmlns aop:"http://www.springframework.org/schema/aop", + util:"http://www.springframework.org/schema/util" + scopedList(ArrayList) { bean -> + bean.scope = "test" + aop.'scoped-proxy'() + } + util.list(id: 'foo') { + value 'one' + value 'two' + } + } + appCtx.refresh() + + assert ['one', 'two'] == appCtx.getBean("foo") + + assertNotNull appCtx.getBean("scopedList") + assertNotNull appCtx.getBean("scopedList").size() + assertNotNull appCtx.getBean("scopedList").size() + + // should only be true because bean not initialized until proxy called + assertEquals 4, scope.instanceCount + } + + @Test + void springAopSupport() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + + reader.beans { + xmlns aop:"http://www.springframework.org/schema/aop" + + fred(AdvisedPerson) { + name = "Fred" + age = 45 + } + birthdayCardSenderAspect(BirthdayCardSender) + + aop.config("proxy-target-class":true) { + aspect( id:"sendBirthdayCard",ref:"birthdayCardSenderAspect" ) { + after method:"onBirthday", pointcut: "execution(void org.springframework.context.groovy.AdvisedPerson.birthday()) and this(person)" + } + } + } + + appCtx.refresh() + + def fred = appCtx.getBean("fred") + assertTrue (fred instanceof SpringProxy) + fred.birthday() + + BirthdayCardSender birthDaySender = appCtx.getBean("birthdayCardSenderAspect") + + assertEquals 1, birthDaySender.peopleSentCards.size() + assertEquals "Fred", birthDaySender.peopleSentCards[0].name + } + + @Test + void springScopedProxyBean() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + + TestScope scope = new TestScope() + appCtx.getBeanFactory().registerScope("test", scope) + reader.beans { + xmlns aop:"http://www.springframework.org/schema/aop" + scopedList(ArrayList) { bean -> + bean.scope = "test" + aop.'scoped-proxy'() + } + } + appCtx.refresh() + + assertNotNull appCtx.getBean("scopedList") + assertNotNull appCtx.getBean("scopedList").size() + assertNotNull appCtx.getBean("scopedList").size() + + // should only be true because bean not initialized until proxy called + assertEquals 2, scope.instanceCount + } + + @Test + void springNamespaceBean() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + xmlns util: 'http://www.springframework.org/schema/util' + util.list(id: 'foo') { + value 'one' + value 'two' + } + } + appCtx.refresh() + + assert ['one', 'two'] == appCtx.getBean('foo') + } + + @Test + void namedArgumentConstructor() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + holyGrail(HolyGrailQuest) + knights(KnightOfTheRoundTable, "Camelot", leader:"lancelot", quest: holyGrail) + } + appCtx.refresh() + + KnightOfTheRoundTable knights = appCtx.getBean("knights") + HolyGrailQuest quest = appCtx.getBean("holyGrail") + + assertEquals "Camelot", knights.name + assertEquals "lancelot", knights.leader + assertEquals quest, knights.quest + } + + @Test + void abstractBeanDefinition() { + def appCtx = new GenericGroovyApplicationContext() + appCtx.reader.beans { + abstractBean { + leader = "Lancelot" + } + quest(HolyGrailQuest) + knights(KnightOfTheRoundTable, "Camelot") { bean -> + bean.parent = abstractBean + quest = quest + } + } + appCtx.refresh() + + def knights = appCtx.knights + assert knights + shouldFail(org.springframework.beans.factory.BeanIsAbstractException) { + appCtx.abstractBean + } + assertEquals "Lancelot", knights.leader + } + + @Test + void abstractBeanDefinitionWithClass() { + def appCtx = new GenericGroovyApplicationContext() + appCtx.reader.beans { + abstractBean(KnightOfTheRoundTable) { bean -> + bean.'abstract' = true + leader = "Lancelot" + } + quest(HolyGrailQuest) + knights("Camelot") { bean -> + bean.parent = abstractBean + quest = quest + } + } + appCtx.refresh() + + shouldFail(org.springframework.beans.factory.BeanIsAbstractException) { + appCtx.abstractBean + } + def knights = appCtx.knights + assert knights + assertEquals "Lancelot", knights.leader + } + + @Test + void scopes() { + def appCtx = new GenericGroovyApplicationContext() + appCtx.reader.beans { + myBean(ScopeTestBean) { bean -> + bean.scope = "prototype" + } + myBean2(ScopeTestBean) + } + appCtx.refresh() + + def b1 = appCtx.myBean + def b2 = appCtx.myBean + + assert b1 != b2 + + b1 = appCtx.myBean2 + b2 = appCtx.myBean2 + + assertEquals b1, b2 + } + + @Test + void simpleBean() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + bean1(Bean1) { + person = "homer" + age = 45 + props = [overweight:true, height:"1.8m"] + children = ["bart", "lisa"] + } + } + appCtx.refresh() + + assert appCtx.containsBean("bean1") + def bean1 = appCtx.getBean("bean1") + + assertEquals "homer", bean1.person + assertEquals 45, bean1.age + assertEquals true, bean1.props?.overweight + assertEquals "1.8m", bean1.props?.height + assertEquals(["bart", "lisa"], bean1.children) + + } + + @Test + void beanWithParentRef() { + def parentAppCtx = new GenericApplicationContext() + def parentBeanReader = new GroovyBeanDefinitionReader(parentAppCtx) + parentBeanReader.beans { + homer(Bean1) { + person = "homer" + age = 45 + props = [overweight:true, height:"1.8m"] + children = ["bart", "lisa"] + } + } + parentAppCtx.refresh() + + def appCtx = new GenericApplicationContext(parentAppCtx) + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + bart(Bean2) { + person = "bart" + parent = ref("homer", true) + } + } + appCtx.refresh() + + assert appCtx.containsBean("bart") + def bart = appCtx.getBean("bart") + assertEquals "homer",bart.parent?.person + } + + @Test + void withAnonymousInnerBean() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + bart(Bean1) { + person = "bart" + age = 11 + } + lisa(Bean1) { + person = "lisa" + age = 9 + } + marge(Bean2) { + person = "marge" + bean1 = { Bean1 b -> + person = "homer" + age = 45 + props = [overweight:true, height:"1.8m"] + children = ["bart", "lisa"] } + children = [bart, lisa] + } + } + appCtx.refresh() + + def marge = appCtx.getBean("marge") + assertEquals "homer", marge.bean1.person + } + + @Test + void withUntypedAnonymousInnerBean() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + homer(Bean1Factory) + bart(Bean1) { + person = "bart" + age = 11 + } + lisa(Bean1) { + person = "lisa" + age = 9 + } + marge(Bean2) { + person = "marge" + bean1 = { bean -> + bean.factoryBean = "homer" + bean.factoryMethod = "newInstance" + person = "homer" } + children = [bart, lisa] + } + } + appCtx.refresh() + + def marge = appCtx.getBean("marge") + assertEquals "homer", marge.bean1.person + } + + @Test + void beanReferences() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + homer(Bean1) { + person = "homer" + age = 45 + props = [overweight:true, height:"1.8m"] + children = ["bart", "lisa"] + } + bart(Bean1) { + person = "bart" + age = 11 + } + lisa(Bean1) { + person = "lisa" + age = 9 + } + marge(Bean2) { + person = "marge" + bean1 = homer + children = [bart, lisa] + } + } + appCtx.refresh() + + def homer = appCtx.getBean("homer") + def marge = appCtx.getBean("marge") + def bart = appCtx.getBean("bart") + def lisa = appCtx.getBean("lisa") + + assertEquals homer, marge.bean1 + assertEquals 2, marge.children.size() + + assertTrue marge.children.contains(bart) + assertTrue marge.children.contains(lisa) + } + + @Test + void beanWithConstructor() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + homer(Bean1) { + person = "homer" + age = 45 + } + marge(Bean3, "marge", homer) { + age = 40 + } + } + appCtx.refresh() + + def marge = appCtx.getBean("marge") + assertEquals "marge", marge.person + assertEquals "homer", marge.bean1.person + assertEquals 40, marge.age + } + + @Test + void beanWithFactoryMethod() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + homer(Bean1) { + person = "homer" + age = 45 + } + def marge = marge(Bean4) { + person = "marge" + } + marge.factoryMethod = "getInstance" + } + appCtx.refresh() + + def marge = appCtx.getBean("marge") + + assert "marge", marge.person + } + + @Test + void beanWithFactoryMethodUsingClosureArgs() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + homer(Bean1) { + person = "homer" + age = 45 + } + marge(Bean4) { bean -> + bean.factoryMethod = "getInstance" + person = "marge" + } + } + appCtx.refresh() + + def marge = appCtx.getBean("marge") + assert "marge", marge.person + } + + @Test + void beanWithFactoryMethodWithConstructorArgs() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + beanFactory(Bean1FactoryWithArgs){} + + homer(beanFactory:"newInstance", "homer") { + age = 45 + } + //Test with no closure body + marge(beanFactory:"newInstance", "marge") + + //Test more verbose method + mcBain("mcBain"){ + bean -> + bean.factoryBean="beanFactory" + bean.factoryMethod="newInstance" + + } + } + appCtx.refresh() + + def homer = appCtx.getBean("homer") + + assert "homer", homer.person + assert 45, homer.age + + assert "marge", appCtx.getBean("marge").person + + assert "mcBain", appCtx.getBean("mcBain").person + } + + @Test + void getBeanDefinitions() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + jeff(Bean1) { + person = 'jeff' + } + graeme(Bean1) { + person = 'graeme' + } + guillaume(Bean1) { + person = 'guillaume' + } + } + + assertEquals 'beanDefinitions was the wrong size', 3, reader.registry.beanDefinitionCount + assertNotNull 'beanDefinitions did not contain jeff', reader.registry.getBeanDefinition('jeff') + assertNotNull 'beanDefinitions did not contain guillaume', reader.registry.getBeanDefinition('guillaume') + assertNotNull 'beanDefinitions did not contain graeme', reader.registry.getBeanDefinition('graeme') + } + + @Test + void beanWithFactoryBean() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + myFactory(Bean1Factory) + + homer(myFactory) { bean -> + bean.factoryMethod = "newInstance" + person = "homer" + age = 45 + } + } + appCtx.refresh() + + def homer = appCtx.getBean("homer") + + assertEquals "homer", homer.person + } + + @Test + void beanWithFactoryBeanAndMethod() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + myFactory(Bean1Factory) + + homer(myFactory:"newInstance") { bean -> + person = "homer" + age = 45 + } + } + + appCtx.refresh() + + def homer = appCtx.getBean("homer") + assertEquals "homer", homer.person + } + + @Test + void loadExternalBeans() { + def appCtx = new GenericGroovyApplicationContext("org/springframework/context/groovy/applicationContext.groovy") + + assert appCtx.containsBean("foo") + def foo = appCtx.getBean("foo") + } + + @Test + void loadExternalBeansWithExplicitRefresh() { + def appCtx = new GenericGroovyApplicationContext() + appCtx.load("org/springframework/context/groovy/applicationContext.groovy") + appCtx.refresh() + + assert appCtx.containsBean("foo") + def foo = appCtx.getBean("foo") + } + + @Test + void holyGrailWiring() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + + reader.beans { + quest(HolyGrailQuest) + + knight(KnightOfTheRoundTable, "Bedivere") { + quest = ref("quest") + } + } + + appCtx.refresh() + + def knight = appCtx.getBean("knight") + knight.embarkOnQuest() + } + + @Test + void abstractBeanSpecifyingClass() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + + reader.beans { + abstractKnight(KnightOfTheRoundTable) { bean -> + bean.'abstract' = true + leader = "King Arthur" + } + + lancelot("lancelot") { bean -> + bean.parent = ref("abstractKnight") + } + + abstractPerson(Bean1) { bean -> + bean.'abstract'=true + age = 45 + } + homerBean { bean -> + bean.parent = ref("abstractPerson") + person = "homer" + } + } + appCtx.refresh() + + def lancelot = appCtx.getBean("lancelot") + assertEquals "King Arthur", lancelot.leader + assertEquals "lancelot", lancelot.name + + def homerBean = appCtx.getBean("homerBean") + + assertEquals 45, homerBean.age + assertEquals "homer", homerBean.person + } + + @Test + void groovyBeanDefinitionReaderWithScript() { + def script = ''' +def appCtx = new org.springframework.context.support.GenericGroovyApplicationContext() +appCtx.reader.beans { +quest(org.springframework.context.groovy.HolyGrailQuest) {} + +knight(org.springframework.context.groovy.KnightOfTheRoundTable, "Bedivere") { quest = quest } +} +appCtx.refresh() +return appCtx +''' + def appCtx = new GroovyShell().evaluate(script) + + def knight = appCtx.getBean('knight') + knight.embarkOnQuest() + } + + // test for GRAILS-5057 + @Test + void registerBeans() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + + reader.beans { + personA(AdvisedPerson) { + name = "Bob" + } + } + + appCtx.refresh() + assertEquals "Bob", appCtx.getBean("personA").name + + appCtx = new GenericApplicationContext() + reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + personA(AdvisedPerson) { + name = "Fred" + } + } + + appCtx.refresh() + assertEquals "Fred", appCtx.getBean("personA").name + } + + @Test + void listOfBeansAsConstructorArg() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + + reader.beans { + someotherbean(SomeOtherClass, new File('somefile.txt')) + someotherbean2(SomeOtherClass, new File('somefile.txt')) + + somebean(SomeClass, [someotherbean, someotherbean2]) + } + + assert appCtx.containsBean('someotherbean') + assert appCtx.containsBean('someotherbean2') + assert appCtx.containsBean('somebean') + } + + @Test + void beanWithListAndMapConstructor() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + bart(Bean1) { + person = "bart" + age = 11 + } + lisa(Bean1) { + person = "lisa" + age = 9 + } + + beanWithList(Bean5, [bart, lisa]) + + // test runtime references both as ref() and as plain name + beanWithMap(Bean6, [bart:bart, lisa:ref('lisa')]) + } + appCtx.refresh() + + def beanWithList = appCtx.getBean("beanWithList") + assertEquals 2, beanWithList.people.size() + assertEquals "bart", beanWithList.people[0].person + + def beanWithMap = appCtx.getBean("beanWithMap") + assertEquals 9, beanWithMap.peopleByName.lisa.age + assertEquals "bart", beanWithMap.peopleByName.bart.person + } + + @Test + void anonymousInnerBeanViaBeanMethod() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + bart(Bean1) { + person = "bart" + age = 11 + } + lisa(Bean1) { + person = "lisa" + age = 9 + } + marge(Bean2) { + person = "marge" + bean1 = bean(Bean1) { + person = "homer" + age = 45 + props = [overweight:true, height:"1.8m"] + children = ["bart", "lisa"] + } + children = [bart, lisa] + } + } + appCtx.refresh() + + def marge = appCtx.getBean("marge") + assertEquals "homer", marge.bean1.person + } + + @Test + void anonymousInnerBeanViaBeanMethodWithConstructorArgs() { + def appCtx = new GenericApplicationContext() + def reader = new GroovyBeanDefinitionReader(appCtx) + reader.beans { + bart(Bean1) { + person = "bart" + age = 11 + } + lisa(Bean1) { + person = "lisa" + age = 9 + } + marge(Bean2) { + person = "marge" + bean3 = bean(Bean3, "homer", lisa) { + person = "homer" + age = 45 + } + children = [bart, lisa] + } + } + appCtx.refresh() + + def marge = appCtx.getBean("marge") + + assertEquals "homer", marge.bean3.person + assertEquals "lisa", marge.bean3.bean1.person + } +} + + +class HolyGrailQuest { + void start() { println "lets begin" } +} + +class KnightOfTheRoundTable { + String name + String leader + + KnightOfTheRoundTable(String n) { + this.name = n + } + + HolyGrailQuest quest + + void embarkOnQuest() { + quest.start() + } +} + +// simple bean +class Bean1 { + String person + int age + Properties props + List children +} + +// bean referencing other bean +class Bean2 { + int age + String person + Bean1 bean1 + Bean3 bean3 + Properties props + List children + Bean1 parent +} + +// bean with constructor args +class Bean3 { + Bean3(String person, Bean1 bean1) { + this.person = person + this.bean1 = bean1 + } + String person + Bean1 bean1 + int age +} + +// bean with factory method +class Bean4 { + private Bean4() {} + static Bean4 getInstance() { + return new Bean4() + } + String person +} + +// bean with List-valued constructor arg +class Bean5 { + Bean5(List people) { + this.people = people + } + List people +} + +// bean with Map-valued constructor arg +class Bean6 { + Bean6(Map peopleByName) { + this.peopleByName = peopleByName + } + Map peopleByName +} + +// a factory bean +class Bean1Factory { + Bean1 newInstance() { + return new Bean1() + } +} + +class ScopeTestBean { +} + +class TestScope implements Scope { + + int instanceCount + + @Override + public Object remove(String name) { + // do nothing + } + + @Override + public void registerDestructionCallback(String name, Runnable callback) { + } + + @Override + public String getConversationId() { + return "mock" + } + + @Override + public Object get(String name, ObjectFactory objectFactory) { + instanceCount++ + objectFactory.getObject() + } + + @Override + public Object resolveContextualObject(String s) { + return null; // noop + } +} + +class BirthdayCardSender { + List peopleSentCards = [] + + public void onBirthday(AdvisedPerson person) { + peopleSentCards << person + } +} + +@Component(value = "person") +public class AdvisedPerson { + int age; + String name; + + public void birthday() { + ++age; + } +} + +class SomeClass { + public SomeClass(List soc) {} +} + +class SomeOtherClass { + public SomeOtherClass(File f) {} +} + +// a factory bean that takes arguments +class Bean1FactoryWithArgs { + Bean1 newInstance(String name) { + new Bean1(person:name) + } +} diff --git a/spring-context/src/test/java/example/gh24375/AnnotatedComponent.java b/spring-context/src/test/java/example/gh24375/AnnotatedComponent.java new file mode 100644 index 0000000..4eb7fde --- /dev/null +++ b/spring-context/src/test/java/example/gh24375/AnnotatedComponent.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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 example.gh24375; + +import org.springframework.stereotype.Component; + +@Component +@EnclosingAnnotation(nested2 = @NestedAnnotation) +public class AnnotatedComponent { +} diff --git a/spring-context/src/test/java/example/gh24375/EnclosingAnnotation.java b/spring-context/src/test/java/example/gh24375/EnclosingAnnotation.java new file mode 100644 index 0000000..1a925de --- /dev/null +++ b/spring-context/src/test/java/example/gh24375/EnclosingAnnotation.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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 example.gh24375; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface EnclosingAnnotation { + + @AliasFor("nested2") + NestedAnnotation nested1() default @NestedAnnotation; + + @AliasFor("nested1") + NestedAnnotation nested2() default @NestedAnnotation; + +} diff --git a/spring-context/src/test/java/example/gh24375/NestedAnnotation.java b/spring-context/src/test/java/example/gh24375/NestedAnnotation.java new file mode 100644 index 0000000..531de72 --- /dev/null +++ b/spring-context/src/test/java/example/gh24375/NestedAnnotation.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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 example.gh24375; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.ANNOTATION_TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface NestedAnnotation { + + String name() default ""; + +} diff --git a/spring-context/src/test/java/example/profilescan/DevComponent.java b/spring-context/src/test/java/example/profilescan/DevComponent.java new file mode 100644 index 0000000..ec52b77 --- /dev/null +++ b/spring-context/src/test/java/example/profilescan/DevComponent.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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 example.profilescan; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Profile(DevComponent.PROFILE_NAME) +@Component +public @interface DevComponent { + + String PROFILE_NAME = "dev"; + + String value() default ""; + +} diff --git a/spring-context/src/test/java/example/profilescan/ProfileAnnotatedComponent.java b/spring-context/src/test/java/example/profilescan/ProfileAnnotatedComponent.java new file mode 100644 index 0000000..0751da1 --- /dev/null +++ b/spring-context/src/test/java/example/profilescan/ProfileAnnotatedComponent.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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 example.profilescan; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Profile(ProfileAnnotatedComponent.PROFILE_NAME) +@Component(ProfileAnnotatedComponent.BEAN_NAME) +public class ProfileAnnotatedComponent { + + public static final String BEAN_NAME = "profileAnnotatedComponent"; + + public static final String PROFILE_NAME = "test"; + +} diff --git a/spring-context/src/test/java/example/profilescan/ProfileMetaAnnotatedComponent.java b/spring-context/src/test/java/example/profilescan/ProfileMetaAnnotatedComponent.java new file mode 100644 index 0000000..cabbc1a --- /dev/null +++ b/spring-context/src/test/java/example/profilescan/ProfileMetaAnnotatedComponent.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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 example.profilescan; + +@DevComponent(ProfileMetaAnnotatedComponent.BEAN_NAME) +public class ProfileMetaAnnotatedComponent { + + public static final String BEAN_NAME = "profileMetaAnnotatedComponent"; + +} diff --git a/spring-context/src/test/java/example/profilescan/SomeAbstractClass.java b/spring-context/src/test/java/example/profilescan/SomeAbstractClass.java new file mode 100644 index 0000000..f15ca68 --- /dev/null +++ b/spring-context/src/test/java/example/profilescan/SomeAbstractClass.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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 example.profilescan; + +import org.springframework.stereotype.Component; + +@Component +public abstract class SomeAbstractClass { + +} diff --git a/spring-context/src/test/java/example/scannable/AutowiredQualifierFooService.java b/spring-context/src/test/java/example/scannable/AutowiredQualifierFooService.java new file mode 100644 index 0000000..fc59dcf --- /dev/null +++ b/spring-context/src/test/java/example/scannable/AutowiredQualifierFooService.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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 example.scannable; + +import java.util.concurrent.Future; + +import javax.annotation.PostConstruct; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.annotation.AsyncResult; + +/** + * @author Mark Fisher + * @author Juergen Hoeller + */ +@Lazy +public class AutowiredQualifierFooService implements FooService { + + @Autowired + @Qualifier("testing") + private FooDao fooDao; + + private boolean initCalled = false; + + @PostConstruct + private void init() { + if (this.initCalled) { + throw new IllegalStateException("Init already called"); + } + this.initCalled = true; + } + + @Override + public String foo(int id) { + return this.fooDao.findFoo(id); + } + + @Override + public Future asyncFoo(int id) { + return new AsyncResult<>(this.fooDao.findFoo(id)); + } + + @Override + public boolean isInitCalled() { + return this.initCalled; + } + +} diff --git a/spring-context/src/test/java/example/scannable/CustomAnnotations.java b/spring-context/src/test/java/example/scannable/CustomAnnotations.java new file mode 100644 index 0000000..c815599 --- /dev/null +++ b/spring-context/src/test/java/example/scannable/CustomAnnotations.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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 example.scannable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +public class CustomAnnotations { + + @Retention(RetentionPolicy.RUNTIME) + private @interface PrivateAnnotation { + String value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @PrivateAnnotation("special") + public @interface SpecialAnnotation { + } + +} diff --git a/spring-context/src/test/java/example/scannable/CustomAspectStereotype.java b/spring-context/src/test/java/example/scannable/CustomAspectStereotype.java new file mode 100644 index 0000000..9c95e2b --- /dev/null +++ b/spring-context/src/test/java/example/scannable/CustomAspectStereotype.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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 example.scannable; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.stereotype.Component; + +/** + * @author Juergen Hoeller + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Component +public @interface CustomAspectStereotype { + + /** + * Not a plain String value - needs to be ignored during name detection. + */ + String[] value() default {}; + +} diff --git a/spring-context/src/test/java/example/scannable/CustomComponent.java b/spring-context/src/test/java/example/scannable/CustomComponent.java new file mode 100644 index 0000000..dc1e916 --- /dev/null +++ b/spring-context/src/test/java/example/scannable/CustomComponent.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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 example.scannable; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Mark Fisher + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@CustomAnnotations.SpecialAnnotation +public @interface CustomComponent { + +} diff --git a/spring-context/src/test/java/example/scannable/CustomStereotype.java b/spring-context/src/test/java/example/scannable/CustomStereotype.java new file mode 100644 index 0000000..958f7f9 --- /dev/null +++ b/spring-context/src/test/java/example/scannable/CustomStereotype.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2009 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. + * You may obtain a copy of the License at + * + * https://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 example.scannable; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Service; + +/** + * @author Juergen Hoeller + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Service +@Scope("prototype") +public @interface CustomStereotype { + + String value() default "thoreau"; + +} diff --git a/spring-context/src/test/java/example/scannable/DefaultNamedComponent.java b/spring-context/src/test/java/example/scannable/DefaultNamedComponent.java new file mode 100644 index 0000000..1f9f65f --- /dev/null +++ b/spring-context/src/test/java/example/scannable/DefaultNamedComponent.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2008 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. + * You may obtain a copy of the License at + * + * https://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 example.scannable; + + +/** + * @author Juergen Hoeller + */ +@CustomStereotype +public class DefaultNamedComponent { + +} diff --git a/spring-context/src/test/java/example/scannable/FooDao.java b/spring-context/src/test/java/example/scannable/FooDao.java new file mode 100644 index 0000000..76bf94b --- /dev/null +++ b/spring-context/src/test/java/example/scannable/FooDao.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2007 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. + * You may obtain a copy of the License at + * + * https://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 example.scannable; + +/** + * @author Mark Fisher + */ +public interface FooDao { + + String findFoo(int id); + +} diff --git a/spring-context/src/test/java/example/scannable/FooService.java b/spring-context/src/test/java/example/scannable/FooService.java new file mode 100644 index 0000000..90cd3a4 --- /dev/null +++ b/spring-context/src/test/java/example/scannable/FooService.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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 example.scannable; + +import java.util.concurrent.Future; + +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Indexed; + +/** + * @author Mark Fisher + * @author Juergen Hoeller + */ +@Indexed +public interface FooService { + + String foo(int id); + + @Async + Future asyncFoo(int id); + + boolean isInitCalled(); + +} diff --git a/spring-context/src/test/java/example/scannable/FooServiceImpl.java b/spring-context/src/test/java/example/scannable/FooServiceImpl.java new file mode 100644 index 0000000..de06276 --- /dev/null +++ b/spring-context/src/test/java/example/scannable/FooServiceImpl.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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 example.scannable; + +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.Future; + +import javax.annotation.PostConstruct; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Lookup; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.MessageSource; +import org.springframework.context.annotation.DependsOn; +import org.springframework.context.annotation.Lazy; +import org.springframework.context.support.AbstractApplicationContext; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.scheduling.annotation.AsyncResult; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +/** + * @author Mark Fisher + * @author Juergen Hoeller + */ +@Service @Lazy @DependsOn("myNamedComponent") +public abstract class FooServiceImpl implements FooService { + + // Just to test ASM5's bytecode parsing of INVOKESPECIAL/STATIC on interfaces + private static final Comparator COMPARATOR_BY_MESSAGE = Comparator.comparing(MessageBean::getMessage); + + + @Autowired private FooDao fooDao; + + @Autowired public BeanFactory beanFactory; + + @Autowired public List listableBeanFactory; + + @Autowired public ResourceLoader resourceLoader; + + @Autowired public ResourcePatternResolver resourcePatternResolver; + + @Autowired public ApplicationEventPublisher eventPublisher; + + @Autowired public MessageSource messageSource; + + @Autowired public ApplicationContext context; + + @Autowired public ConfigurableApplicationContext[] configurableContext; + + @Autowired public AbstractApplicationContext genericContext; + + private boolean initCalled = false; + + + @PostConstruct + private void init() { + if (this.initCalled) { + throw new IllegalStateException("Init already called"); + } + this.initCalled = true; + } + + @Override + public String foo(int id) { + return this.fooDao.findFoo(id); + } + + public String lookupFoo(int id) { + return fooDao().findFoo(id); + } + + @Override + public Future asyncFoo(int id) { + System.out.println(Thread.currentThread().getName()); + Assert.state(ServiceInvocationCounter.getThreadLocalCount() != null, "Thread-local counter not exposed"); + return new AsyncResult<>(fooDao().findFoo(id)); + } + + @Override + public boolean isInitCalled() { + return this.initCalled; + } + + + @Lookup + protected abstract FooDao fooDao(); + +} diff --git a/spring-context/src/test/java/example/scannable/MessageBean.java b/spring-context/src/test/java/example/scannable/MessageBean.java new file mode 100644 index 0000000..ed6c0ed --- /dev/null +++ b/spring-context/src/test/java/example/scannable/MessageBean.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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 example.scannable; + +/** + * @author Mark Fisher + */ +@CustomComponent +@CustomAnnotations.SpecialAnnotation +public class MessageBean { + + private String message; + + public MessageBean() { + this.message = "DEFAULT MESSAGE"; + } + + public MessageBean(String message) { + this.message = message; + } + + @CustomAnnotations.SpecialAnnotation + public String getMessage() { + return this.message; + } + +} diff --git a/spring-context/src/test/java/example/scannable/NamedComponent.java b/spring-context/src/test/java/example/scannable/NamedComponent.java new file mode 100644 index 0000000..3dede45 --- /dev/null +++ b/spring-context/src/test/java/example/scannable/NamedComponent.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2009 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. + * You may obtain a copy of the License at + * + * https://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 example.scannable; + +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +/** + * @author Mark Fisher + */ +@Component("myNamedComponent") @Lazy +public class NamedComponent { + +} diff --git a/spring-context/src/test/java/example/scannable/NamedStubDao.java b/spring-context/src/test/java/example/scannable/NamedStubDao.java new file mode 100644 index 0000000..bb269ff --- /dev/null +++ b/spring-context/src/test/java/example/scannable/NamedStubDao.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2007 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. + * You may obtain a copy of the License at + * + * https://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 example.scannable; + +import org.springframework.stereotype.Repository; + +/** + * @author Juergen Hoeller + */ +@Repository("myNamedDao") +public class NamedStubDao { + + public String find(int id) { + return "bar"; + } + +} diff --git a/spring-context/src/test/java/example/scannable/PackageMarker.java b/spring-context/src/test/java/example/scannable/PackageMarker.java new file mode 100644 index 0000000..5ee7948 --- /dev/null +++ b/spring-context/src/test/java/example/scannable/PackageMarker.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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 example.scannable; + +/** + * Marker class for {@code example.scannable} package. + * + * @see org.springframework.context.annotation.ComponentScan#basePackageClasses() + */ +public class PackageMarker { +} diff --git a/spring-context/src/test/java/example/scannable/ScopedProxyTestBean.java b/spring-context/src/test/java/example/scannable/ScopedProxyTestBean.java new file mode 100644 index 0000000..90ec394 --- /dev/null +++ b/spring-context/src/test/java/example/scannable/ScopedProxyTestBean.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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 example.scannable; + +import java.util.concurrent.Future; + +import org.springframework.context.annotation.Scope; +import org.springframework.scheduling.annotation.AsyncResult; + +/** + * @author Mark Fisher + * @author Juergen Hoeller + */ +@Scope("myScope") +public class ScopedProxyTestBean implements FooService { + + @Override + public String foo(int id) { + return "bar"; + } + + @Override + public Future asyncFoo(int id) { + return new AsyncResult<>("bar"); + } + + @Override + public boolean isInitCalled() { + return false; + } + +} diff --git a/spring-context/src/test/java/example/scannable/ServiceInvocationCounter.java b/spring-context/src/test/java/example/scannable/ServiceInvocationCounter.java new file mode 100644 index 0000000..17d17d6 --- /dev/null +++ b/spring-context/src/test/java/example/scannable/ServiceInvocationCounter.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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 example.scannable; + +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.Pointcut; + +/** + * @author Mark Fisher + */ +@CustomAspectStereotype({"myPointcutInfo", "otherPointcutInfo"}) +@Aspect +public class ServiceInvocationCounter { + + private int useCount; + + private static final ThreadLocal threadLocalCount = new ThreadLocal<>(); + + + @Pointcut("execution(* example.scannable.FooService+.*(..))") + public void serviceExecution() {} + + @Before("serviceExecution()") + public void countUse() { + this.useCount++; + threadLocalCount.set(this.useCount); + } + + public int getCount() { + return this.useCount; + } + + public static Integer getThreadLocalCount() { + return threadLocalCount.get(); + } + +} diff --git a/spring-context/src/test/java/example/scannable/StubFooDao.java b/spring-context/src/test/java/example/scannable/StubFooDao.java new file mode 100644 index 0000000..3a4bce6 --- /dev/null +++ b/spring-context/src/test/java/example/scannable/StubFooDao.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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 example.scannable; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Repository; + +/** + * @author Mark Fisher + */ +@Repository +@Qualifier("testing") +public class StubFooDao implements FooDao { + + @Override + public String findFoo(int id) { + return "bar"; + } + +} diff --git a/spring-context/src/test/java/example/scannable/sub/BarComponent.java b/spring-context/src/test/java/example/scannable/sub/BarComponent.java new file mode 100644 index 0000000..fbc2ef8 --- /dev/null +++ b/spring-context/src/test/java/example/scannable/sub/BarComponent.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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 example.scannable.sub; + +import org.springframework.stereotype.Component; + +/** + * @author Stephane Nicoll + */ +@Component +public class BarComponent { +} diff --git a/spring-context/src/test/java/example/scannable_implicitbasepackage/ComponentScanAnnotatedConfigWithImplicitBasePackage.java b/spring-context/src/test/java/example/scannable_implicitbasepackage/ComponentScanAnnotatedConfigWithImplicitBasePackage.java new file mode 100644 index 0000000..c4b8cd3 --- /dev/null +++ b/spring-context/src/test/java/example/scannable_implicitbasepackage/ComponentScanAnnotatedConfigWithImplicitBasePackage.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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 example.scannable_implicitbasepackage; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +/** + * @author Phillip Webb + */ +@Configuration +@ComponentScan +public class ComponentScanAnnotatedConfigWithImplicitBasePackage { + + @Bean // override of scanned class + public ConfigurableComponent configurableComponent() { + return new ConfigurableComponent(true); + } + +} diff --git a/spring-context/src/test/java/example/scannable_implicitbasepackage/ConfigurableComponent.java b/spring-context/src/test/java/example/scannable_implicitbasepackage/ConfigurableComponent.java new file mode 100644 index 0000000..3ca2181 --- /dev/null +++ b/spring-context/src/test/java/example/scannable_implicitbasepackage/ConfigurableComponent.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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 example.scannable_implicitbasepackage; + +import org.springframework.stereotype.Component; + +/** + * @author Juergen Hoeller + */ +@Component +public class ConfigurableComponent { + + private final boolean flag; + + public ConfigurableComponent() { + this(false); + } + + public ConfigurableComponent(boolean flag) { + this.flag = flag; + } + + public boolean isFlag() { + return this.flag; + } + +} diff --git a/spring-context/src/test/java/example/scannable_implicitbasepackage/ScannedComponent.java b/spring-context/src/test/java/example/scannable_implicitbasepackage/ScannedComponent.java new file mode 100644 index 0000000..92fe691 --- /dev/null +++ b/spring-context/src/test/java/example/scannable_implicitbasepackage/ScannedComponent.java @@ -0,0 +1,27 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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 example.scannable_implicitbasepackage; + +import org.springframework.stereotype.Component; + +/** + * @author Phillip Webb + */ +@Component +public class ScannedComponent { + +} diff --git a/spring-context/src/test/java/example/scannable_scoped/CustomScopeAnnotationBean.java b/spring-context/src/test/java/example/scannable_scoped/CustomScopeAnnotationBean.java new file mode 100644 index 0000000..beb5531 --- /dev/null +++ b/spring-context/src/test/java/example/scannable_scoped/CustomScopeAnnotationBean.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2011 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. + * You may obtain a copy of the License at + * + * https://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 example.scannable_scoped; + +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.stereotype.Component; + +@Component +@MyScope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) +public class CustomScopeAnnotationBean { +} diff --git a/spring-context/src/test/java/example/scannable_scoped/MyScope.java b/spring-context/src/test/java/example/scannable_scoped/MyScope.java new file mode 100644 index 0000000..8f58d6c --- /dev/null +++ b/spring-context/src/test/java/example/scannable_scoped/MyScope.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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 example.scannable_scoped; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.ScopedProxyMode; + +@Retention(RetentionPolicy.RUNTIME) +public @interface MyScope { + String value() default ConfigurableBeanFactory.SCOPE_SINGLETON; + ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT; +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/AdviceBindingTestAspect.java b/spring-context/src/test/java/org/springframework/aop/aspectj/AdviceBindingTestAspect.java new file mode 100644 index 0000000..c6afc87 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/AdviceBindingTestAspect.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.aspectj.lang.JoinPoint; + +/** + * Aspect used as part of before advice binding tests and + * serves as base class for a number of more specialized test aspects. + * + * @author Adrian Colyer + * @author Chris Beams + */ +class AdviceBindingTestAspect { + + protected AdviceBindingCollaborator collaborator; + + + public void setCollaborator(AdviceBindingCollaborator aCollaborator) { + this.collaborator = aCollaborator; + } + + + // "advice" methods + + public void oneIntArg(int age) { + this.collaborator.oneIntArg(age); + } + + public void oneObjectArg(Object bean) { + this.collaborator.oneObjectArg(bean); + } + + public void oneIntAndOneObject(int x, Object o) { + this.collaborator.oneIntAndOneObject(x,o); + } + + public void needsJoinPoint(JoinPoint tjp) { + this.collaborator.needsJoinPoint(tjp.getSignature().getName()); + } + + public void needsJoinPointStaticPart(JoinPoint.StaticPart tjpsp) { + this.collaborator.needsJoinPointStaticPart(tjpsp.getSignature().getName()); + } + + + /** + * Collaborator interface that makes it easy to test this aspect is + * working as expected through mocking. + */ + public interface AdviceBindingCollaborator { + + void oneIntArg(int x); + + void oneObjectArg(Object o); + + void oneIntAndOneObject(int x, Object o); + + void needsJoinPoint(String s); + + void needsJoinPointStaticPart(String s); + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/AfterAdviceBindingTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/AfterAdviceBindingTests.java new file mode 100644 index 0000000..036a78c --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/AfterAdviceBindingTests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.aspectj.AdviceBindingTestAspect.AdviceBindingCollaborator; +import org.springframework.aop.framework.Advised; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for various parameter binding scenarios with before advice. + * + * @author Adrian Colyer + * @author Rod Johnson + * @author Chris Beams + */ +public class AfterAdviceBindingTests { + + private AdviceBindingCollaborator mockCollaborator; + + private ITestBean testBeanProxy; + + private TestBean testBeanTarget; + + + @BeforeEach + public void setup() throws Exception { + ClassPathXmlApplicationContext ctx = + new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); + AdviceBindingTestAspect afterAdviceAspect = (AdviceBindingTestAspect) ctx.getBean("testAspect"); + + testBeanProxy = (ITestBean) ctx.getBean("testBean"); + assertThat(AopUtils.isAopProxy(testBeanProxy)).isTrue(); + + // we need the real target too, not just the proxy... + testBeanTarget = (TestBean) ((Advised) testBeanProxy).getTargetSource().getTarget(); + + mockCollaborator = mock(AdviceBindingCollaborator.class); + afterAdviceAspect.setCollaborator(mockCollaborator); + } + + + @Test + public void testOneIntArg() { + testBeanProxy.setAge(5); + verify(mockCollaborator).oneIntArg(5); + } + + @Test + public void testOneObjectArgBindingProxyWithThis() { + testBeanProxy.getAge(); + verify(mockCollaborator).oneObjectArg(this.testBeanProxy); + } + + @Test + public void testOneObjectArgBindingTarget() { + testBeanProxy.getDoctor(); + verify(mockCollaborator).oneObjectArg(this.testBeanTarget); + } + + @Test + public void testOneIntAndOneObjectArgs() { + testBeanProxy.setAge(5); + verify(mockCollaborator).oneIntAndOneObject(5,this.testBeanProxy); + } + + @Test + public void testNeedsJoinPoint() { + testBeanProxy.getAge(); + verify(mockCollaborator).needsJoinPoint("getAge"); + } + + @Test + public void testNeedsJoinPointStaticPart() { + testBeanProxy.getAge(); + verify(mockCollaborator).needsJoinPointStaticPart("getAge"); + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/AfterReturningAdviceBindingTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/AfterReturningAdviceBindingTests.java new file mode 100644 index 0000000..1ea6cec --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/AfterReturningAdviceBindingTests.java @@ -0,0 +1,191 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.aspectj.AfterReturningAdviceBindingTestAspect.AfterReturningAdviceBindingCollaborator; +import org.springframework.aop.framework.Advised; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +/** + * Tests for various parameter binding scenarios with before advice. + * + * @author Adrian Colyer + * @author Rod Johnson + * @author Juergen Hoeller + * @author Chris Beams + */ +public class AfterReturningAdviceBindingTests { + + private AfterReturningAdviceBindingTestAspect afterAdviceAspect; + + private ITestBean testBeanProxy; + + private TestBean testBeanTarget; + + private AfterReturningAdviceBindingCollaborator mockCollaborator; + + + @BeforeEach + public void setup() throws Exception { + ClassPathXmlApplicationContext ctx = + new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); + + afterAdviceAspect = (AfterReturningAdviceBindingTestAspect) ctx.getBean("testAspect"); + + mockCollaborator = mock(AfterReturningAdviceBindingCollaborator.class); + afterAdviceAspect.setCollaborator(mockCollaborator); + + testBeanProxy = (ITestBean) ctx.getBean("testBean"); + assertThat(AopUtils.isAopProxy(testBeanProxy)).isTrue(); + + // we need the real target too, not just the proxy... + this.testBeanTarget = (TestBean) ((Advised)testBeanProxy).getTargetSource().getTarget(); + } + + + @Test + public void testOneIntArg() { + testBeanProxy.setAge(5); + verify(mockCollaborator).oneIntArg(5); + } + + @Test + public void testOneObjectArg() { + testBeanProxy.getAge(); + verify(mockCollaborator).oneObjectArg(this.testBeanProxy); + } + + @Test + public void testOneIntAndOneObjectArgs() { + testBeanProxy.setAge(5); + verify(mockCollaborator).oneIntAndOneObject(5,this.testBeanProxy); + } + + @Test + public void testNeedsJoinPoint() { + testBeanProxy.getAge(); + verify(mockCollaborator).needsJoinPoint("getAge"); + } + + @Test + public void testNeedsJoinPointStaticPart() { + testBeanProxy.getAge(); + verify(mockCollaborator).needsJoinPointStaticPart("getAge"); + } + + @Test + public void testReturningString() { + testBeanProxy.setName("adrian"); + testBeanProxy.getName(); + verify(mockCollaborator).oneString("adrian"); + } + + @Test + public void testReturningObject() { + testBeanProxy.returnsThis(); + verify(mockCollaborator).oneObjectArg(this.testBeanTarget); + } + + @Test + public void testReturningBean() { + testBeanProxy.returnsThis(); + verify(mockCollaborator).oneTestBeanArg(this.testBeanTarget); + } + + @Test + public void testReturningBeanArray() { + this.testBeanTarget.setSpouse(new TestBean()); + ITestBean[] spouses = this.testBeanTarget.getSpouses(); + testBeanProxy.getSpouses(); + verify(mockCollaborator).testBeanArrayArg(spouses); + } + + @Test + public void testNoInvokeWhenReturningParameterTypeDoesNotMatch() { + testBeanProxy.setSpouse(this.testBeanProxy); + testBeanProxy.getSpouse(); + verifyNoInteractions(mockCollaborator); + } + + @Test + public void testReturningByType() { + testBeanProxy.returnsThis(); + verify(mockCollaborator).objectMatchNoArgs(); + } + + @Test + public void testReturningPrimitive() { + testBeanProxy.setAge(20); + testBeanProxy.haveBirthday(); + verify(mockCollaborator).oneInt(20); + } + +} + + +final class AfterReturningAdviceBindingTestAspect extends AdviceBindingTestAspect { + + private AfterReturningAdviceBindingCollaborator getCollaborator() { + return (AfterReturningAdviceBindingCollaborator) this.collaborator; + } + + public void oneString(String name) { + getCollaborator().oneString(name); + } + + public void oneTestBeanArg(TestBean bean) { + getCollaborator().oneTestBeanArg(bean); + } + + public void testBeanArrayArg(ITestBean[] beans) { + getCollaborator().testBeanArrayArg(beans); + } + + public void objectMatchNoArgs() { + getCollaborator().objectMatchNoArgs(); + } + + public void stringMatchNoArgs() { + getCollaborator().stringMatchNoArgs(); + } + + public void oneInt(int result) { + getCollaborator().oneInt(result); + } + + interface AfterReturningAdviceBindingCollaborator extends AdviceBindingCollaborator { + + void oneString(String s); + void oneTestBeanArg(TestBean b); + void testBeanArrayArg(ITestBean[] b); + void objectMatchNoArgs(); + void stringMatchNoArgs(); + void oneInt(int result); + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/AfterThrowingAdviceBindingTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/AfterThrowingAdviceBindingTests.java new file mode 100644 index 0000000..000a9f3 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/AfterThrowingAdviceBindingTests.java @@ -0,0 +1,145 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.aspectj.AfterThrowingAdviceBindingTestAspect.AfterThrowingAdviceBindingCollaborator; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for various parameter binding scenarios with before advice. + * + * @author Adrian Colyer + * @author Chris Beams + */ +public class AfterThrowingAdviceBindingTests { + + private ITestBean testBean; + + private AfterThrowingAdviceBindingTestAspect afterThrowingAdviceAspect; + + private AfterThrowingAdviceBindingCollaborator mockCollaborator; + + + @BeforeEach + public void setup() { + ClassPathXmlApplicationContext ctx = + new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); + + testBean = (ITestBean) ctx.getBean("testBean"); + afterThrowingAdviceAspect = (AfterThrowingAdviceBindingTestAspect) ctx.getBean("testAspect"); + + mockCollaborator = mock(AfterThrowingAdviceBindingCollaborator.class); + afterThrowingAdviceAspect.setCollaborator(mockCollaborator); + } + + + @Test + public void testSimpleAfterThrowing() throws Throwable { + assertThatExceptionOfType(Throwable.class).isThrownBy(() -> + this.testBean.exceptional(new Throwable())); + verify(mockCollaborator).noArgs(); + } + + @Test + public void testAfterThrowingWithBinding() throws Throwable { + Throwable t = new Throwable(); + assertThatExceptionOfType(Throwable.class).isThrownBy(() -> + this.testBean.exceptional(t)); + verify(mockCollaborator).oneThrowable(t); + } + + @Test + public void testAfterThrowingWithNamedTypeRestriction() throws Throwable { + Throwable t = new Throwable(); + assertThatExceptionOfType(Throwable.class).isThrownBy(() -> + this.testBean.exceptional(t)); + verify(mockCollaborator).noArgs(); + verify(mockCollaborator).oneThrowable(t); + verify(mockCollaborator).noArgsOnThrowableMatch(); + } + + @Test + public void testAfterThrowingWithRuntimeExceptionBinding() throws Throwable { + RuntimeException ex = new RuntimeException(); + assertThatExceptionOfType(Throwable.class).isThrownBy(() -> + this.testBean.exceptional(ex)); + verify(mockCollaborator).oneRuntimeException(ex); + } + + @Test + public void testAfterThrowingWithTypeSpecified() throws Throwable { + assertThatExceptionOfType(Throwable.class).isThrownBy(() -> + this.testBean.exceptional(new Throwable())); + verify(mockCollaborator).noArgsOnThrowableMatch(); + } + + @Test + public void testAfterThrowingWithRuntimeTypeSpecified() throws Throwable { + assertThatExceptionOfType(Throwable.class).isThrownBy(() -> + this.testBean.exceptional(new RuntimeException())); + verify(mockCollaborator).noArgsOnRuntimeExceptionMatch(); + } + +} + + +final class AfterThrowingAdviceBindingTestAspect { + + // collaborator interface that makes it easy to test this aspect is + // working as expected through mocking. + public interface AfterThrowingAdviceBindingCollaborator { + void noArgs(); + void oneThrowable(Throwable t); + void oneRuntimeException(RuntimeException re); + void noArgsOnThrowableMatch(); + void noArgsOnRuntimeExceptionMatch(); + } + + protected AfterThrowingAdviceBindingCollaborator collaborator = null; + + public void setCollaborator(AfterThrowingAdviceBindingCollaborator aCollaborator) { + this.collaborator = aCollaborator; + } + + public void noArgs() { + this.collaborator.noArgs(); + } + + public void oneThrowable(Throwable t) { + this.collaborator.oneThrowable(t); + } + + public void oneRuntimeException(RuntimeException ex) { + this.collaborator.oneRuntimeException(ex); + } + + public void noArgsOnThrowableMatch() { + this.collaborator.noArgsOnThrowableMatch(); + } + + public void noArgsOnRuntimeExceptionMatch() { + this.collaborator.noArgsOnRuntimeExceptionMatch(); + } +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/AroundAdviceBindingTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/AroundAdviceBindingTests.java new file mode 100644 index 0000000..c3404c7 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/AroundAdviceBindingTests.java @@ -0,0 +1,140 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.aspectj.AroundAdviceBindingTestAspect.AroundAdviceBindingCollaborator; +import org.springframework.aop.framework.Advised; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for various parameter binding scenarios with before advice. + * + * @author Adrian Colyer + * @author Chris Beams + */ +public class AroundAdviceBindingTests { + + private AroundAdviceBindingCollaborator mockCollaborator; + + private ITestBean testBeanProxy; + + private TestBean testBeanTarget; + + protected ApplicationContext ctx; + + @BeforeEach + public void onSetUp() throws Exception { + ctx = new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); + + AroundAdviceBindingTestAspect aroundAdviceAspect = ((AroundAdviceBindingTestAspect) ctx.getBean("testAspect")); + + ITestBean injectedTestBean = (ITestBean) ctx.getBean("testBean"); + assertThat(AopUtils.isAopProxy(injectedTestBean)).isTrue(); + + this.testBeanProxy = injectedTestBean; + // we need the real target too, not just the proxy... + + this.testBeanTarget = (TestBean) ((Advised) testBeanProxy).getTargetSource().getTarget(); + + mockCollaborator = mock(AroundAdviceBindingCollaborator.class); + aroundAdviceAspect.setCollaborator(mockCollaborator); + } + + @Test + public void testOneIntArg() { + testBeanProxy.setAge(5); + verify(mockCollaborator).oneIntArg(5); + } + + @Test + public void testOneObjectArgBoundToTarget() { + testBeanProxy.getAge(); + verify(mockCollaborator).oneObjectArg(this.testBeanTarget); + } + + @Test + public void testOneIntAndOneObjectArgs() { + testBeanProxy.setAge(5); + verify(mockCollaborator).oneIntAndOneObject(5, this.testBeanProxy); + } + + @Test + public void testJustJoinPoint() { + testBeanProxy.getAge(); + verify(mockCollaborator).justJoinPoint("getAge"); + } + +} + + +class AroundAdviceBindingTestAspect { + + private AroundAdviceBindingCollaborator collaborator = null; + + public void setCollaborator(AroundAdviceBindingCollaborator aCollaborator) { + this.collaborator = aCollaborator; + } + + // "advice" methods + public void oneIntArg(ProceedingJoinPoint pjp, int age) throws Throwable { + this.collaborator.oneIntArg(age); + pjp.proceed(); + } + + public int oneObjectArg(ProceedingJoinPoint pjp, Object bean) throws Throwable { + this.collaborator.oneObjectArg(bean); + return ((Integer) pjp.proceed()).intValue(); + } + + public void oneIntAndOneObject(ProceedingJoinPoint pjp, int x , Object o) throws Throwable { + this.collaborator.oneIntAndOneObject(x,o); + pjp.proceed(); + } + + public int justJoinPoint(ProceedingJoinPoint pjp) throws Throwable { + this.collaborator.justJoinPoint(pjp.getSignature().getName()); + return ((Integer) pjp.proceed()).intValue(); + } + + /** + * Collaborator interface that makes it easy to test this aspect + * is working as expected through mocking. + */ + public interface AroundAdviceBindingCollaborator { + + void oneIntArg(int x); + + void oneObjectArg(Object o); + + void oneIntAndOneObject(int x, Object o); + + void justJoinPoint(String s); + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/AroundAdviceCircularTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/AroundAdviceCircularTests.java new file mode 100644 index 0000000..dab66cc --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/AroundAdviceCircularTests.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.support.AopUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @author Chris Beams + */ +public class AroundAdviceCircularTests extends AroundAdviceBindingTests { + + @Test + public void testBothBeansAreProxies() { + Object tb = ctx.getBean("testBean"); + assertThat(AopUtils.isAopProxy(tb)).isTrue(); + Object tb2 = ctx.getBean("testBean2"); + assertThat(AopUtils.isAopProxy(tb2)).isTrue(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/AspectAndAdvicePrecedenceTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/AspectAndAdvicePrecedenceTests.java new file mode 100644 index 0000000..e813f37 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/AspectAndAdvicePrecedenceTests.java @@ -0,0 +1,255 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import java.lang.reflect.Method; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.MethodBeforeAdvice; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.core.Ordered; +import org.springframework.lang.Nullable; + +/** + * @author Adrian Colyer + * @author Chris Beams + */ +public class AspectAndAdvicePrecedenceTests { + + private PrecedenceTestAspect highPrecedenceAspect; + + private PrecedenceTestAspect lowPrecedenceAspect; + + private SimpleSpringBeforeAdvice highPrecedenceSpringAdvice; + + private SimpleSpringBeforeAdvice lowPrecedenceSpringAdvice; + + private ITestBean testBean; + + + @BeforeEach + public void setup() { + ClassPathXmlApplicationContext ctx = + new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); + highPrecedenceAspect = (PrecedenceTestAspect) ctx.getBean("highPrecedenceAspect"); + lowPrecedenceAspect = (PrecedenceTestAspect) ctx.getBean("lowPrecedenceAspect"); + highPrecedenceSpringAdvice = (SimpleSpringBeforeAdvice) ctx.getBean("highPrecedenceSpringAdvice"); + lowPrecedenceSpringAdvice = (SimpleSpringBeforeAdvice) ctx.getBean("lowPrecedenceSpringAdvice"); + testBean = (ITestBean) ctx.getBean("testBean"); + } + + + @Test + public void testAdviceOrder() { + PrecedenceTestAspect.Collaborator collaborator = new PrecedenceVerifyingCollaborator(); + this.highPrecedenceAspect.setCollaborator(collaborator); + this.lowPrecedenceAspect.setCollaborator(collaborator); + this.highPrecedenceSpringAdvice.setCollaborator(collaborator); + this.lowPrecedenceSpringAdvice.setCollaborator(collaborator); + this.testBean.getAge(); + } + + + private static class PrecedenceVerifyingCollaborator implements PrecedenceTestAspect.Collaborator { + + private static final String[] EXPECTED = { + // this order confirmed by running the same aspects (minus the Spring AOP advisors) + // through AspectJ... + "beforeAdviceOne(highPrecedenceAspect)", // 1 + "beforeAdviceTwo(highPrecedenceAspect)", // 2 + "aroundAdviceOne(highPrecedenceAspect)", // 3, before proceed + "aroundAdviceTwo(highPrecedenceAspect)", // 4, before proceed + "beforeAdviceOne(highPrecedenceSpringAdvice)", // 5 + "beforeAdviceOne(lowPrecedenceSpringAdvice)", // 6 + "beforeAdviceOne(lowPrecedenceAspect)", // 7 + "beforeAdviceTwo(lowPrecedenceAspect)", // 8 + "aroundAdviceOne(lowPrecedenceAspect)", // 9, before proceed + "aroundAdviceTwo(lowPrecedenceAspect)", // 10, before proceed + "aroundAdviceTwo(lowPrecedenceAspect)", // 11, after proceed + "aroundAdviceOne(lowPrecedenceAspect)", // 12, after proceed + "afterAdviceOne(lowPrecedenceAspect)", // 13 + "afterAdviceTwo(lowPrecedenceAspect)", // 14 + "aroundAdviceTwo(highPrecedenceAspect)", // 15, after proceed + "aroundAdviceOne(highPrecedenceAspect)", // 16, after proceed + "afterAdviceOne(highPrecedenceAspect)", // 17 + "afterAdviceTwo(highPrecedenceAspect)" // 18 + }; + + private int adviceInvocationNumber = 0; + + private void checkAdvice(String whatJustHappened) { + //System.out.println("[" + adviceInvocationNumber + "] " + whatJustHappened + " ==> " + EXPECTED[adviceInvocationNumber]); + if (adviceInvocationNumber > (EXPECTED.length - 1)) { + throw new AssertionError("Too many advice invocations, expecting " + EXPECTED.length + + " but had " + adviceInvocationNumber); + } + String expecting = EXPECTED[adviceInvocationNumber++]; + if (!whatJustHappened.equals(expecting)) { + throw new AssertionError("Expecting '" + expecting + "' on advice invocation " + adviceInvocationNumber + + " but got '" + whatJustHappened + "'"); + } + } + + @Override + public void beforeAdviceOne(String beanName) { + checkAdvice("beforeAdviceOne(" + beanName + ")"); + } + + @Override + public void beforeAdviceTwo(String beanName) { + checkAdvice("beforeAdviceTwo(" + beanName + ")"); + } + + @Override + public void aroundAdviceOne(String beanName) { + checkAdvice("aroundAdviceOne(" + beanName + ")"); + } + + @Override + public void aroundAdviceTwo(String beanName) { + checkAdvice("aroundAdviceTwo(" + beanName + ")"); + } + + @Override + public void afterAdviceOne(String beanName) { + checkAdvice("afterAdviceOne(" + beanName + ")"); + } + + @Override + public void afterAdviceTwo(String beanName) { + checkAdvice("afterAdviceTwo(" + beanName + ")"); + } + } + +} + + +class PrecedenceTestAspect implements BeanNameAware, Ordered { + + private String name; + + private int order = Ordered.LOWEST_PRECEDENCE; + + private Collaborator collaborator; + + + @Override + public void setBeanName(String name) { + this.name = name; + } + + public void setOrder(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return order; + } + + public void setCollaborator(Collaborator collaborator) { + this.collaborator = collaborator; + } + + public void beforeAdviceOne() { + this.collaborator.beforeAdviceOne(this.name); + } + + public void beforeAdviceTwo() { + this.collaborator.beforeAdviceTwo(this.name); + } + + public int aroundAdviceOne(ProceedingJoinPoint pjp) { + int ret = -1; + this.collaborator.aroundAdviceOne(this.name); + try { + ret = ((Integer)pjp.proceed()).intValue(); + } + catch (Throwable t) { + throw new RuntimeException(t); + } + this.collaborator.aroundAdviceOne(this.name); + return ret; + } + + public int aroundAdviceTwo(ProceedingJoinPoint pjp) { + int ret = -1; + this.collaborator.aroundAdviceTwo(this.name); + try { + ret = ((Integer)pjp.proceed()).intValue(); + } + catch (Throwable t) { + throw new RuntimeException(t); + } + this.collaborator.aroundAdviceTwo(this.name); + return ret; + } + + public void afterAdviceOne() { + this.collaborator.afterAdviceOne(this.name); + } + + public void afterAdviceTwo() { + this.collaborator.afterAdviceTwo(this.name); + } + + + public interface Collaborator { + + void beforeAdviceOne(String beanName); + void beforeAdviceTwo(String beanName); + void aroundAdviceOne(String beanName); + void aroundAdviceTwo(String beanName); + void afterAdviceOne(String beanName); + void afterAdviceTwo(String beanName); + } + +} + + +class SimpleSpringBeforeAdvice implements MethodBeforeAdvice, BeanNameAware { + + private PrecedenceTestAspect.Collaborator collaborator; + private String name; + + /* (non-Javadoc) + * @see org.springframework.aop.MethodBeforeAdvice#before(java.lang.reflect.Method, java.lang.Object[], java.lang.Object) + */ + @Override + public void before(Method method, Object[] args, @Nullable Object target) + throws Throwable { + this.collaborator.beforeAdviceOne(this.name); + } + + public void setCollaborator(PrecedenceTestAspect.Collaborator collaborator) { + this.collaborator = collaborator; + } + + /* (non-Javadoc) + * @see org.springframework.beans.factory.BeanNameAware#setBeanName(java.lang.String) + */ + @Override + public void setBeanName(String name) { + this.name = name; + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutAdvisorTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutAdvisorTests.java new file mode 100644 index 0000000..cb59c1a --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutAdvisorTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + * @author Juergen Hoeller + * @author Chris Beams + */ +public class AspectJExpressionPointcutAdvisorTests { + + private ITestBean testBean; + + private CallCountingInterceptor interceptor; + + + @BeforeEach + public void setup() { + ClassPathXmlApplicationContext ctx = + new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); + testBean = (ITestBean) ctx.getBean("testBean"); + interceptor = (CallCountingInterceptor) ctx.getBean("interceptor"); + } + + + @Test + public void testPointcutting() { + assertThat(interceptor.getCount()).as("Count should be 0").isEqualTo(0); + testBean.getSpouses(); + assertThat(interceptor.getCount()).as("Count should be 1").isEqualTo(1); + testBean.getSpouse(); + assertThat(interceptor.getCount()).as("Count should be 1").isEqualTo(1); + } + +} + + +class CallCountingInterceptor implements MethodInterceptor { + + private int count; + + @Override + public Object invoke(MethodInvocation methodInvocation) throws Throwable { + count++; + return methodInvocation.proceed(); + } + + public int getCount() { + return count; + } + + public void reset() { + this.count = 0; + } +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/BeanNamePointcutAtAspectTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/BeanNamePointcutAtAspectTests.java new file mode 100644 index 0000000..3329cec --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/BeanNamePointcutAtAspectTests.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.aspectj.annotation.AspectJProxyFactory; +import org.springframework.aop.framework.Advised; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for correct application of the bean() PCD for @AspectJ-based aspects. + * + * @author Ramnivas Laddad + * @author Juergen Hoeller + * @author Chris Beams + */ +public class BeanNamePointcutAtAspectTests { + + private ITestBean testBean1; + + private ITestBean testBean3; + + private CounterAspect counterAspect; + + + @org.junit.jupiter.api.BeforeEach + public void setup() { + ClassPathXmlApplicationContext ctx = + new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); + + counterAspect = (CounterAspect) ctx.getBean("counterAspect"); + testBean1 = (ITestBean) ctx.getBean("testBean1"); + testBean3 = (ITestBean) ctx.getBean("testBean3"); + } + + + @Test + public void testMatchingBeanName() { + boolean condition = testBean1 instanceof Advised; + assertThat(condition).as("Expected a proxy").isTrue(); + + // Call two methods to test for SPR-3953-like condition + testBean1.setAge(20); + testBean1.setName(""); + assertThat(counterAspect.count).isEqualTo(2); + } + + @Test + public void testNonMatchingBeanName() { + boolean condition = testBean3 instanceof Advised; + assertThat(condition).as("Didn't expect a proxy").isFalse(); + + testBean3.setAge(20); + assertThat(counterAspect.count).isEqualTo(0); + } + + @Test + public void testProgrammaticProxyCreation() { + ITestBean testBean = new TestBean(); + + AspectJProxyFactory factory = new AspectJProxyFactory(); + factory.setTarget(testBean); + + CounterAspect myCounterAspect = new CounterAspect(); + factory.addAspect(myCounterAspect); + + ITestBean proxyTestBean = factory.getProxy(); + + boolean condition = proxyTestBean instanceof Advised; + assertThat(condition).as("Expected a proxy").isTrue(); + proxyTestBean.setAge(20); + assertThat(myCounterAspect.count).as("Programmatically created proxy shouldn't match bean()").isEqualTo(0); + } + +} + + +@Aspect +class CounterAspect { + + int count; + + @Before("execution(* set*(..)) && bean(testBean1)") + public void increment1ForAnonymousPointcut() { + count++; + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/BeanNamePointcutTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/BeanNamePointcutTests.java new file mode 100644 index 0000000..a560143 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/BeanNamePointcutTests.java @@ -0,0 +1,147 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import java.lang.reflect.Method; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.MethodBeforeAdvice; +import org.springframework.aop.framework.Advised; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for correct application of the bean() PCD for XML-based AspectJ aspects. + * + * @author Ramnivas Laddad + * @author Juergen Hoeller + * @author Chris Beams + */ +public class BeanNamePointcutTests { + + private ITestBean testBean1; + private ITestBean testBean2; + private ITestBean testBeanContainingNestedBean; + private Map testFactoryBean1; + private Map testFactoryBean2; + private Counter counterAspect; + + private ITestBean interceptThis; + private ITestBean dontInterceptThis; + private TestInterceptor testInterceptor; + + private ClassPathXmlApplicationContext ctx; + + + @BeforeEach + public void setup() { + ctx = new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); + testBean1 = (ITestBean) ctx.getBean("testBean1"); + testBean2 = (ITestBean) ctx.getBean("testBean2"); + testBeanContainingNestedBean = (ITestBean) ctx.getBean("testBeanContainingNestedBean"); + testFactoryBean1 = (Map) ctx.getBean("testFactoryBean1"); + testFactoryBean2 = (Map) ctx.getBean("testFactoryBean2"); + counterAspect = (Counter) ctx.getBean("counterAspect"); + interceptThis = (ITestBean) ctx.getBean("interceptThis"); + dontInterceptThis = (ITestBean) ctx.getBean("dontInterceptThis"); + testInterceptor = (TestInterceptor) ctx.getBean("testInterceptor"); + + counterAspect.reset(); + } + + + // We don't need to test all combination of pointcuts due to BeanNamePointcutMatchingTests + + @Test + public void testMatchingBeanName() { + boolean condition = this.testBean1 instanceof Advised; + assertThat(condition).as("Matching bean must be advised (proxied)").isTrue(); + // Call two methods to test for SPR-3953-like condition + this.testBean1.setAge(20); + this.testBean1.setName(""); + assertThat(this.counterAspect.getCount()).as("Advice not executed: must have been").isEqualTo(2); + } + + @Test + public void testNonMatchingBeanName() { + boolean condition = this.testBean2 instanceof Advised; + assertThat(condition).as("Non-matching bean must *not* be advised (proxied)").isFalse(); + this.testBean2.setAge(20); + assertThat(this.counterAspect.getCount()).as("Advice must *not* have been executed").isEqualTo(0); + } + + @Test + public void testNonMatchingNestedBeanName() { + boolean condition = this.testBeanContainingNestedBean.getDoctor() instanceof Advised; + assertThat(condition).as("Non-matching bean must *not* be advised (proxied)").isFalse(); + } + + @Test + public void testMatchingFactoryBeanObject() { + boolean condition1 = this.testFactoryBean1 instanceof Advised; + assertThat(condition1).as("Matching bean must be advised (proxied)").isTrue(); + assertThat(this.testFactoryBean1.get("myKey")).isEqualTo("myValue"); + assertThat(this.testFactoryBean1.get("myKey")).isEqualTo("myValue"); + assertThat(this.counterAspect.getCount()).as("Advice not executed: must have been").isEqualTo(2); + FactoryBean fb = (FactoryBean) ctx.getBean("&testFactoryBean1"); + boolean condition = !(fb instanceof Advised); + assertThat(condition).as("FactoryBean itself must *not* be advised").isTrue(); + } + + @Test + public void testMatchingFactoryBeanItself() { + boolean condition1 = !(this.testFactoryBean2 instanceof Advised); + assertThat(condition1).as("Matching bean must *not* be advised (proxied)").isTrue(); + FactoryBean fb = (FactoryBean) ctx.getBean("&testFactoryBean2"); + boolean condition = fb instanceof Advised; + assertThat(condition).as("FactoryBean itself must be advised").isTrue(); + assertThat(Map.class.isAssignableFrom(fb.getObjectType())).isTrue(); + assertThat(Map.class.isAssignableFrom(fb.getObjectType())).isTrue(); + assertThat(this.counterAspect.getCount()).as("Advice not executed: must have been").isEqualTo(2); + } + + @Test + public void testPointcutAdvisorCombination() { + boolean condition = this.interceptThis instanceof Advised; + assertThat(condition).as("Matching bean must be advised (proxied)").isTrue(); + boolean condition1 = this.dontInterceptThis instanceof Advised; + assertThat(condition1).as("Non-matching bean must *not* be advised (proxied)").isFalse(); + interceptThis.setAge(20); + assertThat(testInterceptor.interceptionCount).isEqualTo(1); + dontInterceptThis.setAge(20); + assertThat(testInterceptor.interceptionCount).isEqualTo(1); + } + + + public static class TestInterceptor implements MethodBeforeAdvice { + + private int interceptionCount; + + @Override + public void before(Method method, Object[] args, @Nullable Object target) throws Throwable { + interceptionCount++; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/BeforeAdviceBindingTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/BeforeAdviceBindingTests.java new file mode 100644 index 0000000..90c5879 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/BeforeAdviceBindingTests.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.aspectj.AdviceBindingTestAspect.AdviceBindingCollaborator; +import org.springframework.aop.framework.Advised; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for various parameter binding scenarios with before advice. + * + * @author Adrian Colyer + * @author Rod Johnson + * @author Chris Beams + */ +public class BeforeAdviceBindingTests { + + private AdviceBindingCollaborator mockCollaborator; + + private ITestBean testBeanProxy; + + private TestBean testBeanTarget; + + + @BeforeEach + public void setup() throws Exception { + ClassPathXmlApplicationContext ctx = + new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); + + testBeanProxy = (ITestBean) ctx.getBean("testBean"); + assertThat(AopUtils.isAopProxy(testBeanProxy)).isTrue(); + + // we need the real target too, not just the proxy... + testBeanTarget = (TestBean) ((Advised) testBeanProxy).getTargetSource().getTarget(); + + AdviceBindingTestAspect beforeAdviceAspect = (AdviceBindingTestAspect) ctx.getBean("testAspect"); + + mockCollaborator = mock(AdviceBindingCollaborator.class); + beforeAdviceAspect.setCollaborator(mockCollaborator); + } + + + @Test + public void testOneIntArg() { + testBeanProxy.setAge(5); + verify(mockCollaborator).oneIntArg(5); + } + + @Test + public void testOneObjectArgBoundToProxyUsingThis() { + testBeanProxy.getAge(); + verify(mockCollaborator).oneObjectArg(this.testBeanProxy); + } + + @Test + public void testOneIntAndOneObjectArgs() { + testBeanProxy.setAge(5); + verify(mockCollaborator).oneIntAndOneObject(5,this.testBeanTarget); + } + + @Test + public void testNeedsJoinPoint() { + testBeanProxy.getAge(); + verify(mockCollaborator).needsJoinPoint("getAge"); + } + + @Test + public void testNeedsJoinPointStaticPart() { + testBeanProxy.getAge(); + verify(mockCollaborator).needsJoinPointStaticPart("getAge"); + } + + +} + + +class AuthenticationLogger { + + public void logAuthenticationAttempt(String username) { + System.out.println("User [" + username + "] attempting to authenticate"); + } + +} + +class SecurityManager { + public boolean authenticate(String username, String password) { + return false; + } +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/Counter.java b/spring-context/src/test/java/org/springframework/aop/aspectj/Counter.java new file mode 100644 index 0000000..a24a6d6 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/Counter.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +/** + * A simple counter for use in simple tests (for example, how many times an advice was executed) + * + * @author Ramnivas Laddad + */ +final class Counter implements ICounter { + + private int count; + + public Counter() { + } + + @Override + public void increment() { + count++; + } + + @Override + public void decrement() { + count--; + } + + @Override + public int getCount() { + return count; + } + + @Override + public void setCount(int counter) { + this.count = counter; + } + + @Override + public void reset() { + this.count = 0; + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/DeclarationOrderIndependenceTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/DeclarationOrderIndependenceTests.java new file mode 100644 index 0000000..ccfbbc7 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/DeclarationOrderIndependenceTests.java @@ -0,0 +1,182 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import java.io.Serializable; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Adrian Colyer + * @author Chris Beams + */ +public class DeclarationOrderIndependenceTests { + + private TopsyTurvyAspect aspect; + + private TopsyTurvyTarget target; + + + @BeforeEach + public void setup() { + ClassPathXmlApplicationContext ctx = + new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); + aspect = (TopsyTurvyAspect) ctx.getBean("topsyTurvyAspect"); + target = (TopsyTurvyTarget) ctx.getBean("topsyTurvyTarget"); + } + + + @Test + public void testTargetIsSerializable() { + boolean condition = this.target instanceof Serializable; + assertThat(condition).as("target bean is serializable").isTrue(); + } + + @Test + public void testTargetIsBeanNameAware() { + boolean condition = this.target instanceof BeanNameAware; + assertThat(condition).as("target bean is bean name aware").isTrue(); + } + + @Test + public void testBeforeAdviceFiringOk() { + AspectCollaborator collab = new AspectCollaborator(); + this.aspect.setCollaborator(collab); + this.target.doSomething(); + assertThat(collab.beforeFired).as("before advice fired").isTrue(); + } + + @Test + public void testAroundAdviceFiringOk() { + AspectCollaborator collab = new AspectCollaborator(); + this.aspect.setCollaborator(collab); + this.target.getX(); + assertThat(collab.aroundFired).as("around advice fired").isTrue(); + } + + @Test + public void testAfterReturningFiringOk() { + AspectCollaborator collab = new AspectCollaborator(); + this.aspect.setCollaborator(collab); + this.target.getX(); + assertThat(collab.afterReturningFired).as("after returning advice fired").isTrue(); + } + + + /** public visibility is required */ + public static class BeanNameAwareMixin implements BeanNameAware { + + @SuppressWarnings("unused") + private String beanName; + + @Override + public void setBeanName(String name) { + this.beanName = name; + } + + } + + /** public visibility is required */ + @SuppressWarnings("serial") + public static class SerializableMixin implements Serializable { + } + +} + + +class TopsyTurvyAspect { + + interface Collaborator { + void beforeAdviceFired(); + void afterReturningAdviceFired(); + void aroundAdviceFired(); + } + + private Collaborator collaborator; + + public void setCollaborator(Collaborator collaborator) { + this.collaborator = collaborator; + } + + public void before() { + this.collaborator.beforeAdviceFired(); + } + + public void afterReturning() { + this.collaborator.afterReturningAdviceFired(); + } + + public Object around(ProceedingJoinPoint pjp) throws Throwable { + Object ret = pjp.proceed(); + this.collaborator.aroundAdviceFired(); + return ret; + } +} + + +interface TopsyTurvyTarget { + + void doSomething(); + + int getX(); +} + + +class TopsyTurvyTargetImpl implements TopsyTurvyTarget { + + private int x = 5; + + @Override + public void doSomething() { + this.x = 10; + } + + @Override + public int getX() { + return x; + } +} + + +class AspectCollaborator implements TopsyTurvyAspect.Collaborator { + + public boolean afterReturningFired = false; + public boolean aroundFired = false; + public boolean beforeFired = false; + + @Override + public void afterReturningAdviceFired() { + this.afterReturningFired = true; + } + + @Override + public void aroundAdviceFired() { + this.aroundFired = true; + } + + @Override + public void beforeAdviceFired() { + this.beforeFired = true; + } +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/DeclareParentsDelegateRefTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/DeclareParentsDelegateRefTests.java new file mode 100644 index 0000000..f40f274 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/DeclareParentsDelegateRefTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Ramnivas Laddad + * @author Chris Beams + */ +public class DeclareParentsDelegateRefTests { + + protected NoMethodsBean noMethodsBean; + + protected Counter counter; + + + @BeforeEach + public void setup() { + ClassPathXmlApplicationContext ctx = + new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); + noMethodsBean = (NoMethodsBean) ctx.getBean("noMethodsBean"); + counter = (Counter) ctx.getBean("counter"); + counter.reset(); + } + + + @Test + public void testIntroductionWasMade() { + boolean condition = noMethodsBean instanceof ICounter; + assertThat(condition).as("Introduction must have been made").isTrue(); + } + + @Test + public void testIntroductionDelegation() { + ((ICounter)noMethodsBean).increment(); + assertThat(counter.getCount()).as("Delegate's counter should be updated").isEqualTo(1); + } + +} + + +interface NoMethodsBean { +} + + +class NoMethodsBeanImpl implements NoMethodsBean { +} + diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/DeclareParentsTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/DeclareParentsTests.java new file mode 100644 index 0000000..c1bf87d --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/DeclareParentsTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import test.mixin.Lockable; + +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * @author Rod Johnson + * @author Chris Beams + */ +public class DeclareParentsTests { + + private ITestBean testBeanProxy; + + private Object introductionObject; + + + @BeforeEach + public void setup() { + ClassPathXmlApplicationContext ctx = + new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); + testBeanProxy = (ITestBean) ctx.getBean("testBean"); + introductionObject = ctx.getBean("introduction"); + } + + + @Test + public void testIntroductionWasMade() { + assertThat(AopUtils.isAopProxy(testBeanProxy)).isTrue(); + assertThat(AopUtils.isAopProxy(introductionObject)).as("Introduction should not be proxied").isFalse(); + boolean condition = testBeanProxy instanceof Lockable; + assertThat(condition).as("Introduction must have been made").isTrue(); + } + + // TODO if you change type pattern from org.springframework.beans..* + // to org.springframework..* it also matches introduction. + // Perhaps generated advisor bean definition could be made to depend + // on the introduction, in which case this would not be a problem. + @Test + public void testLockingWorks() { + Lockable lockable = (Lockable) testBeanProxy; + assertThat(lockable.locked()).isFalse(); + + // Invoke a non-advised method + testBeanProxy.getAge(); + + testBeanProxy.setName(""); + lockable.lock(); + assertThatIllegalStateException().as("should be locked").isThrownBy(() -> + testBeanProxy.setName(" ")); + } + +} + + +class NonAnnotatedMakeLockable { + + public void checkNotLocked(Lockable mixin) { + if (mixin.locked()) { + throw new IllegalStateException("locked"); + } + } +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/ICounter.java b/spring-context/src/test/java/org/springframework/aop/aspectj/ICounter.java new file mode 100644 index 0000000..5c2e5d1 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/ICounter.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +/** + * @author Ramnivas Laddad + */ +interface ICounter { + + void increment(); + + void decrement(); + + int getCount(); + + void setCount(int counter); + + void reset(); + +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/ImplicitJPArgumentMatchingAtAspectJTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/ImplicitJPArgumentMatchingAtAspectJTests.java new file mode 100644 index 0000000..fc9970b --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/ImplicitJPArgumentMatchingAtAspectJTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +/** + * Tests to check if the first implicit join point argument is correctly processed. + * See SPR-3723 for more details. + * + * @author Ramnivas Laddad + * @author Chris Beams + */ +public class ImplicitJPArgumentMatchingAtAspectJTests { + + @Test + @SuppressWarnings("resource") + public void testAspect() { + // nothing to really test; it is enough if we don't get error while creating the app context + new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); + } + + @Aspect + static class CounterAtAspectJAspect { + @Around(value="execution(* org.springframework.beans.testfixture.beans.TestBean.*(..)) and this(bean) and args(argument)", + argNames="bean,argument") + public void increment(ProceedingJoinPoint pjp, TestBean bean, Object argument) throws Throwable { + pjp.proceed(); + } + } +} + diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/ImplicitJPArgumentMatchingTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/ImplicitJPArgumentMatchingTests.java new file mode 100644 index 0000000..7e3b0e7 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/ImplicitJPArgumentMatchingTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.junit.jupiter.api.Test; + +import org.springframework.context.support.ClassPathXmlApplicationContext; + +/** + * Tests to check if the first implicit join point argument is correctly processed. + * See SPR-3723 for more details. + * + * @author Ramnivas Laddad + * @author Chris Beams + */ +public class ImplicitJPArgumentMatchingTests { + + @Test + public void testAspect() { + // nothing to really test; it is enough if we don't get error while creating app context + new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); + } + + static class CounterAspect { + public void increment(ProceedingJoinPoint pjp, Object bean, Object argument) throws Throwable { + pjp.proceed(); + } + } +} + diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/OverloadedAdviceTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/OverloadedAdviceTests.java new file mode 100644 index 0000000..fd8b916 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/OverloadedAdviceTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for overloaded advice. + * + * @author Adrian Colyer + * @author Chris Beams + */ +public class OverloadedAdviceTests { + + @Test + public void testExceptionOnConfigParsingWithMismatchedAdviceMethod() { + try { + new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); + } + catch (BeanCreationException ex) { + Throwable cause = ex.getRootCause(); + boolean condition = cause instanceof IllegalArgumentException; + assertThat(condition).as("Should be IllegalArgumentException").isTrue(); + assertThat(cause.getMessage().contains("invalidAbsoluteTypeName")).as("invalidAbsoluteTypeName should be detected by AJ").isTrue(); + } + } + + @Test + public void testExceptionOnConfigParsingWithAmbiguousAdviceMethod() { + try { + new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-ambiguous.xml", getClass()); + } + catch (BeanCreationException ex) { + Throwable cause = ex.getRootCause(); + boolean condition = cause instanceof IllegalArgumentException; + assertThat(condition).as("Should be IllegalArgumentException").isTrue(); + assertThat(cause.getMessage().contains("Cannot resolve method 'myBeforeAdvice' to a unique method")).as("Cannot resolve method 'myBeforeAdvice' to a unique method").isTrue(); + } + } + +} + + +class OverloadedAdviceTestAspect { + + public void myBeforeAdvice(String name) { + // no-op + } + + public void myBeforeAdvice(int age) { + // no-op + } +} + diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/ProceedTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/ProceedTests.java new file mode 100644 index 0000000..895e40e --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/ProceedTests.java @@ -0,0 +1,217 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.core.Ordered; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for SPR-3522. Arguments changed on a call to proceed should be + * visible to advice further down the invocation chain. + * + * @author Adrian Colyer + * @author Chris Beams + */ +public class ProceedTests { + + private SimpleBean testBean; + + private ProceedTestingAspect firstTestAspect; + + private ProceedTestingAspect secondTestAspect; + + + @BeforeEach + public void setup() { + ClassPathXmlApplicationContext ctx = + new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); + testBean = (SimpleBean) ctx.getBean("testBean"); + firstTestAspect = (ProceedTestingAspect) ctx.getBean("firstTestAspect"); + secondTestAspect = (ProceedTestingAspect) ctx.getBean("secondTestAspect"); + } + + + @Test + public void testSimpleProceedWithChangedArgs() { + this.testBean.setName("abc"); + assertThat(this.testBean.getName()).as("Name changed in around advice").isEqualTo("ABC"); + } + + @Test + public void testGetArgsIsDefensive() { + this.testBean.setAge(5); + assertThat(this.testBean.getAge()).as("getArgs is defensive").isEqualTo(5); + } + + @Test + public void testProceedWithArgsInSameAspect() { + this.testBean.setMyFloat(1.0F); + assertThat(this.testBean.getMyFloat() > 1.9F).as("value changed in around advice").isTrue(); + assertThat(this.firstTestAspect.getLastBeforeFloatValue() > 1.9F).as("changed value visible to next advice in chain").isTrue(); + } + + @Test + public void testProceedWithArgsAcrossAspects() { + this.testBean.setSex("male"); + assertThat(this.testBean.getSex()).as("value changed in around advice").isEqualTo("MALE"); + assertThat(this.secondTestAspect.getLastBeforeStringValue()).as("changed value visible to next before advice in chain").isEqualTo("MALE"); + assertThat(this.secondTestAspect.getLastAroundStringValue()).as("changed value visible to next around advice in chain").isEqualTo("MALE"); + } + + +} + + +interface SimpleBean { + + void setName(String name); + String getName(); + void setAge(int age); + int getAge(); + void setMyFloat(float f); + float getMyFloat(); + void setSex(String sex); + String getSex(); +} + + +class SimpleBeanImpl implements SimpleBean { + + private int age; + private float aFloat; + private String name; + private String sex; + + @Override + public int getAge() { + return age; + } + + @Override + public float getMyFloat() { + return aFloat; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getSex() { + return sex; + } + + @Override + public void setAge(int age) { + this.age = age; + } + + @Override + public void setMyFloat(float f) { + this.aFloat = f; + } + + @Override + public void setName(String name) { + this.name = name; + } + + @Override + public void setSex(String sex) { + this.sex = sex; + } +} + + +class ProceedTestingAspect implements Ordered { + + private String lastBeforeStringValue; + private String lastAroundStringValue; + private float lastBeforeFloatValue; + private int order; + + public void setOrder(int order) { this.order = order; } + @Override + public int getOrder() { return this.order; } + + public Object capitalize(ProceedingJoinPoint pjp, String value) throws Throwable { + return pjp.proceed(new Object[] {value.toUpperCase()}); + } + + public Object doubleOrQuits(ProceedingJoinPoint pjp) throws Throwable { + int value = ((Integer) pjp.getArgs()[0]).intValue(); + pjp.getArgs()[0] = new Integer(value * 2); + return pjp.proceed(); + } + + public Object addOne(ProceedingJoinPoint pjp, Float value) throws Throwable { + float fv = value.floatValue(); + return pjp.proceed(new Object[] {new Float(fv + 1.0F)}); + } + + public void captureStringArgument(JoinPoint tjp, String arg) { + if (!tjp.getArgs()[0].equals(arg)) { + throw new IllegalStateException( + "argument is '" + arg + "', " + + "but args array has '" + tjp.getArgs()[0] + "'" + ); + } + this.lastBeforeStringValue = arg; + } + + public Object captureStringArgumentInAround(ProceedingJoinPoint pjp, String arg) throws Throwable { + if (!pjp.getArgs()[0].equals(arg)) { + throw new IllegalStateException( + "argument is '" + arg + "', " + + "but args array has '" + pjp.getArgs()[0] + "'"); + } + this.lastAroundStringValue = arg; + return pjp.proceed(); + } + + public void captureFloatArgument(JoinPoint tjp, float arg) { + float tjpArg = ((Float) tjp.getArgs()[0]).floatValue(); + if (Math.abs(tjpArg - arg) > 0.000001) { + throw new IllegalStateException( + "argument is '" + arg + "', " + + "but args array has '" + tjpArg + "'" + ); + } + this.lastBeforeFloatValue = arg; + } + + public String getLastBeforeStringValue() { + return this.lastBeforeStringValue; + } + + public String getLastAroundStringValue() { + return this.lastAroundStringValue; + } + + public float getLastBeforeFloatValue() { + return this.lastBeforeFloatValue; + } +} + diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/PropertyDependentAspectTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/PropertyDependentAspectTests.java new file mode 100644 index 0000000..3293f75 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/PropertyDependentAspectTests.java @@ -0,0 +1,154 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.Advised; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Check that an aspect that depends on another bean, where the referenced bean + * itself is advised by the same aspect, works correctly. + * + * @author Ramnivas Laddad + * @author Juergen Hoeller + * @author Chris Beams + */ +@SuppressWarnings("resource") +public class PropertyDependentAspectTests { + + @Test + public void propertyDependentAspectWithPropertyDeclaredBeforeAdvice() + throws Exception { + checkXmlAspect(getClass().getSimpleName() + "-before.xml"); + } + + @Test + public void propertyDependentAspectWithPropertyDeclaredAfterAdvice() throws Exception { + checkXmlAspect(getClass().getSimpleName() + "-after.xml"); + } + + @Test + public void propertyDependentAtAspectJAspectWithPropertyDeclaredBeforeAdvice() + throws Exception { + checkAtAspectJAspect(getClass().getSimpleName() + "-atAspectJ-before.xml"); + } + + @Test + public void propertyDependentAtAspectJAspectWithPropertyDeclaredAfterAdvice() + throws Exception { + checkAtAspectJAspect(getClass().getSimpleName() + "-atAspectJ-after.xml"); + } + + private void checkXmlAspect(String appContextFile) { + ApplicationContext context = new ClassPathXmlApplicationContext(appContextFile, getClass()); + ICounter counter = (ICounter) context.getBean("counter"); + boolean condition = counter instanceof Advised; + assertThat(condition).as("Proxy didn't get created").isTrue(); + + counter.increment(); + JoinPointMonitorAspect callCountingAspect = (JoinPointMonitorAspect)context.getBean("monitoringAspect"); + assertThat(callCountingAspect.beforeExecutions).as("Advise didn't get executed").isEqualTo(1); + assertThat(callCountingAspect.aroundExecutions).as("Advise didn't get executed").isEqualTo(1); + } + + private void checkAtAspectJAspect(String appContextFile) { + ApplicationContext context = new ClassPathXmlApplicationContext(appContextFile, getClass()); + ICounter counter = (ICounter) context.getBean("counter"); + boolean condition = counter instanceof Advised; + assertThat(condition).as("Proxy didn't get created").isTrue(); + + counter.increment(); + JoinPointMonitorAtAspectJAspect callCountingAspect = (JoinPointMonitorAtAspectJAspect)context.getBean("monitoringAspect"); + assertThat(callCountingAspect.beforeExecutions).as("Advise didn't get executed").isEqualTo(1); + assertThat(callCountingAspect.aroundExecutions).as("Advise didn't get executed").isEqualTo(1); + } + +} + + +class JoinPointMonitorAspect { + + /** + * The counter property is purposefully not used in the aspect to avoid distraction + * from the main bug -- merely needing a dependency on an advised bean + * is sufficient to reproduce the bug. + */ + private ICounter counter; + + int beforeExecutions; + int aroundExecutions; + + public void before() { + beforeExecutions++; + } + + public Object around(ProceedingJoinPoint pjp) throws Throwable { + aroundExecutions++; + return pjp.proceed(); + } + + public ICounter getCounter() { + return counter; + } + + public void setCounter(ICounter counter) { + this.counter = counter; + } + +} + + +@Aspect +class JoinPointMonitorAtAspectJAspect { + /* The counter property is purposefully not used in the aspect to avoid distraction + * from the main bug -- merely needing a dependency on an advised bean + * is sufficient to reproduce the bug. + */ + private ICounter counter; + + int beforeExecutions; + int aroundExecutions; + + @Before("execution(* increment*())") + public void before() { + beforeExecutions++; + } + + @Around("execution(* increment*())") + public Object around(ProceedingJoinPoint pjp) throws Throwable { + aroundExecutions++; + return pjp.proceed(); + } + + public ICounter getCounter() { + return counter; + } + + public void setCounter(ICounter counter) { + this.counter = counter; + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/SharedPointcutWithArgsMismatchTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/SharedPointcutWithArgsMismatchTests.java new file mode 100644 index 0000000..9cb4aa0 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/SharedPointcutWithArgsMismatchTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.context.support.ClassPathXmlApplicationContext; + +/** + * See SPR-1682. + * + * @author Adrian Colyer + * @author Chris Beams + */ +public class SharedPointcutWithArgsMismatchTests { + + private ToBeAdvised toBeAdvised; + + + @BeforeEach + public void setup() { + ClassPathXmlApplicationContext ctx = + new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); + toBeAdvised = (ToBeAdvised) ctx.getBean("toBeAdvised"); + } + + + @Test + public void testMismatchedArgBinding() { + this.toBeAdvised.foo("Hello"); + } + +} + + +class ToBeAdvised { + + public void foo(String s) { + System.out.println(s); + } +} + + +class MyAspect { + + public void doBefore(int x) { + System.out.println(x); + } + + public void doBefore(String x) { + System.out.println(x); + } +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/SubtypeSensitiveMatchingTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/SubtypeSensitiveMatchingTests.java new file mode 100644 index 0000000..e129cef --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/SubtypeSensitiveMatchingTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import java.io.Serializable; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.Advised; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Adrian Colyer + * @author Chris Beams + */ +public class SubtypeSensitiveMatchingTests { + + private NonSerializableFoo nonSerializableBean; + + private SerializableFoo serializableBean; + + private Bar bar; + + + @BeforeEach + public void setup() { + ClassPathXmlApplicationContext ctx = + new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); + nonSerializableBean = (NonSerializableFoo) ctx.getBean("testClassA"); + serializableBean = (SerializableFoo) ctx.getBean("testClassB"); + bar = (Bar) ctx.getBean("testClassC"); + } + + + @Test + public void testBeansAreProxiedOnStaticMatch() { + boolean condition = this.serializableBean instanceof Advised; + assertThat(condition).as("bean with serializable type should be proxied").isTrue(); + } + + @Test + public void testBeansThatDoNotMatchBasedSolelyOnRuntimeTypeAreNotProxied() { + boolean condition = this.nonSerializableBean instanceof Advised; + assertThat(condition).as("bean with non-serializable type should not be proxied").isFalse(); + } + + @Test + public void testBeansThatDoNotMatchBasedOnOtherTestAreProxied() { + boolean condition = this.bar instanceof Advised; + assertThat(condition).as("bean with args check should be proxied").isTrue(); + } + +} + + +//strange looking interfaces are just to set up certain test conditions... + +interface NonSerializableFoo { void foo(); } + + +interface SerializableFoo extends Serializable { void foo(); } + + +class SubtypeMatchingTestClassA implements NonSerializableFoo { + + @Override + public void foo() {} + +} + + +@SuppressWarnings("serial") +class SubtypeMatchingTestClassB implements SerializableFoo { + + @Override + public void foo() {} + +} + + +interface Bar { void bar(Object o); } + + +class SubtypeMatchingTestClassC implements Bar { + + @Override + public void bar(Object o) {} + +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/TargetPointcutSelectionTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/TargetPointcutSelectionTests.java new file mode 100644 index 0000000..27bd955 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/TargetPointcutSelectionTests.java @@ -0,0 +1,124 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for target selection matching (see SPR-3783). + *

    Thanks to Tomasz Blachowicz for the bug report! + * + * @author Ramnivas Laddad + * @author Chris Beams + */ +public class TargetPointcutSelectionTests { + + public TestInterface testImpl1; + + public TestInterface testImpl2; + + public TestAspect testAspectForTestImpl1; + + public TestAspect testAspectForAbstractTestImpl; + + public TestInterceptor testInterceptor; + + + @BeforeEach + public void setup() { + ClassPathXmlApplicationContext ctx = + new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); + testImpl1 = (TestInterface) ctx.getBean("testImpl1"); + testImpl2 = (TestInterface) ctx.getBean("testImpl2"); + testAspectForTestImpl1 = (TestAspect) ctx.getBean("testAspectForTestImpl1"); + testAspectForAbstractTestImpl = (TestAspect) ctx.getBean("testAspectForAbstractTestImpl"); + testInterceptor = (TestInterceptor) ctx.getBean("testInterceptor"); + + testAspectForTestImpl1.count = 0; + testAspectForAbstractTestImpl.count = 0; + testInterceptor.count = 0; + } + + + @Test + public void targetSelectionForMatchedType() { + testImpl1.interfaceMethod(); + assertThat(testAspectForTestImpl1.count).as("Should have been advised by POJO advice for impl").isEqualTo(1); + assertThat(testAspectForAbstractTestImpl.count).as("Should have been advised by POJO advice for base type").isEqualTo(1); + assertThat(testInterceptor.count).as("Should have been advised by advisor").isEqualTo(1); + } + + @Test + public void targetNonSelectionForMismatchedType() { + testImpl2.interfaceMethod(); + assertThat(testAspectForTestImpl1.count).as("Shouldn't have been advised by POJO advice for impl").isEqualTo(0); + assertThat(testAspectForAbstractTestImpl.count).as("Should have been advised by POJO advice for base type").isEqualTo(1); + assertThat(testInterceptor.count).as("Shouldn't have been advised by advisor").isEqualTo(0); + } + + + public static interface TestInterface { + + public void interfaceMethod(); + } + + + // Reproducing bug requires that the class specified in target() pointcut doesn't + // include the advised method's implementation (instead a base class should include it) + public static abstract class AbstractTestImpl implements TestInterface { + + @Override + public void interfaceMethod() { + } + } + + + public static class TestImpl1 extends AbstractTestImpl { + } + + + public static class TestImpl2 extends AbstractTestImpl { + } + + + public static class TestAspect { + + public int count; + + public void increment() { + count++; + } + } + + + public static class TestInterceptor extends TestAspect implements MethodInterceptor { + + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + increment(); + return mi.proceed(); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/ThisAndTargetSelectionOnlyPointcutsAtAspectJTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/ThisAndTargetSelectionOnlyPointcutsAtAspectJTests.java new file mode 100644 index 0000000..5e4be4c --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/ThisAndTargetSelectionOnlyPointcutsAtAspectJTests.java @@ -0,0 +1,214 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.junit.jupiter.api.Test; + +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Ramnivas Laddad + * @author Chris Beams + */ +public class ThisAndTargetSelectionOnlyPointcutsAtAspectJTests { + + private TestInterface testBean; + + private TestInterface testAnnotatedClassBean; + + private TestInterface testAnnotatedMethodBean; + + private Counter counter; + + + @org.junit.jupiter.api.BeforeEach + public void setup() { + ClassPathXmlApplicationContext ctx = + new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); + testBean = (TestInterface) ctx.getBean("testBean"); + testAnnotatedClassBean = (TestInterface) ctx.getBean("testAnnotatedClassBean"); + testAnnotatedMethodBean = (TestInterface) ctx.getBean("testAnnotatedMethodBean"); + counter = (Counter) ctx.getBean("counter"); + counter.reset(); + } + + + @Test + public void thisAsClassDoesNotMatch() { + testBean.doIt(); + assertThat(counter.thisAsClassCounter).isEqualTo(0); + } + + @Test + public void thisAsInterfaceMatch() { + testBean.doIt(); + assertThat(counter.thisAsInterfaceCounter).isEqualTo(1); + } + + @Test + public void targetAsClassDoesMatch() { + testBean.doIt(); + assertThat(counter.targetAsClassCounter).isEqualTo(1); + } + + @Test + public void targetAsInterfaceMatch() { + testBean.doIt(); + assertThat(counter.targetAsInterfaceCounter).isEqualTo(1); + } + + @Test + public void thisAsClassAndTargetAsClassCounterNotMatch() { + testBean.doIt(); + assertThat(counter.thisAsClassAndTargetAsClassCounter).isEqualTo(0); + } + + @Test + public void thisAsInterfaceAndTargetAsInterfaceCounterMatch() { + testBean.doIt(); + assertThat(counter.thisAsInterfaceAndTargetAsInterfaceCounter).isEqualTo(1); + } + + @Test + public void thisAsInterfaceAndTargetAsClassCounterMatch() { + testBean.doIt(); + assertThat(counter.thisAsInterfaceAndTargetAsInterfaceCounter).isEqualTo(1); + } + + + @Test + public void atTargetClassAnnotationMatch() { + testAnnotatedClassBean.doIt(); + assertThat(counter.atTargetClassAnnotationCounter).isEqualTo(1); + } + + @Test + public void atAnnotationMethodAnnotationMatch() { + testAnnotatedMethodBean.doIt(); + assertThat(counter.atAnnotationMethodAnnotationCounter).isEqualTo(1); + } + + public static interface TestInterface { + public void doIt(); + } + + public static class TestImpl implements TestInterface { + @Override + public void doIt() { + } + } + + @Retention(RetentionPolicy.RUNTIME) + public static @interface TestAnnotation { + + } + + @TestAnnotation + public static class AnnotatedClassTestImpl implements TestInterface { + @Override + public void doIt() { + } + } + + public static class AnnotatedMethodTestImpl implements TestInterface { + @Override + @TestAnnotation + public void doIt() { + } + } + + @Aspect + public static class Counter { + int thisAsClassCounter; + int thisAsInterfaceCounter; + int targetAsClassCounter; + int targetAsInterfaceCounter; + int thisAsClassAndTargetAsClassCounter; + int thisAsInterfaceAndTargetAsInterfaceCounter; + int thisAsInterfaceAndTargetAsClassCounter; + int atTargetClassAnnotationCounter; + int atAnnotationMethodAnnotationCounter; + + public void reset() { + thisAsClassCounter = 0; + thisAsInterfaceCounter = 0; + targetAsClassCounter = 0; + targetAsInterfaceCounter = 0; + thisAsClassAndTargetAsClassCounter = 0; + thisAsInterfaceAndTargetAsInterfaceCounter = 0; + thisAsInterfaceAndTargetAsClassCounter = 0; + atTargetClassAnnotationCounter = 0; + atAnnotationMethodAnnotationCounter = 0; + } + + @Before("this(org.springframework.aop.aspectj.ThisAndTargetSelectionOnlyPointcutsAtAspectJTests.TestImpl)") + public void incrementThisAsClassCounter() { + thisAsClassCounter++; + } + + @Before("this(org.springframework.aop.aspectj.ThisAndTargetSelectionOnlyPointcutsAtAspectJTests.TestInterface)") + public void incrementThisAsInterfaceCounter() { + thisAsInterfaceCounter++; + } + + @Before("target(org.springframework.aop.aspectj.ThisAndTargetSelectionOnlyPointcutsAtAspectJTests.TestImpl)") + public void incrementTargetAsClassCounter() { + targetAsClassCounter++; + } + + @Before("target(org.springframework.aop.aspectj.ThisAndTargetSelectionOnlyPointcutsAtAspectJTests.TestInterface)") + public void incrementTargetAsInterfaceCounter() { + targetAsInterfaceCounter++; + } + + @Before("this(org.springframework.aop.aspectj.ThisAndTargetSelectionOnlyPointcutsAtAspectJTests.TestImpl) " + + "&& target(org.springframework.aop.aspectj.ThisAndTargetSelectionOnlyPointcutsAtAspectJTests.TestImpl)") + public void incrementThisAsClassAndTargetAsClassCounter() { + thisAsClassAndTargetAsClassCounter++; + } + + @Before("this(org.springframework.aop.aspectj.ThisAndTargetSelectionOnlyPointcutsAtAspectJTests.TestInterface) " + + "&& target(org.springframework.aop.aspectj.ThisAndTargetSelectionOnlyPointcutsAtAspectJTests.TestInterface)") + public void incrementThisAsInterfaceAndTargetAsInterfaceCounter() { + thisAsInterfaceAndTargetAsInterfaceCounter++; + } + + @Before("this(org.springframework.aop.aspectj.ThisAndTargetSelectionOnlyPointcutsAtAspectJTests.TestInterface) " + + "&& target(org.springframework.aop.aspectj.ThisAndTargetSelectionOnlyPointcutsAtAspectJTests.TestImpl)") + public void incrementThisAsInterfaceAndTargetAsClassCounter() { + thisAsInterfaceAndTargetAsClassCounter++; + } + + @Before("@target(org.springframework.aop.aspectj.ThisAndTargetSelectionOnlyPointcutsAtAspectJTests.TestAnnotation)") + public void incrementAtTargetClassAnnotationCounter() { + atTargetClassAnnotationCounter++; + } + + @Before("@annotation(org.springframework.aop.aspectj.ThisAndTargetSelectionOnlyPointcutsAtAspectJTests.TestAnnotation)") + public void incrementAtAnnotationMethodAnnotationCounter() { + atAnnotationMethodAnnotationCounter++; + } + + } +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/ThisAndTargetSelectionOnlyPointcutsTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/ThisAndTargetSelectionOnlyPointcutsTests.java new file mode 100644 index 0000000..410718a --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/ThisAndTargetSelectionOnlyPointcutsTests.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Ramnivas Laddad + * @author Chris Beams + */ +public class ThisAndTargetSelectionOnlyPointcutsTests { + + private TestInterface testBean; + + private Counter thisAsClassCounter; + private Counter thisAsInterfaceCounter; + private Counter targetAsClassCounter; + private Counter targetAsInterfaceCounter; + private Counter thisAsClassAndTargetAsClassCounter; + private Counter thisAsInterfaceAndTargetAsInterfaceCounter; + private Counter thisAsInterfaceAndTargetAsClassCounter; + + + @BeforeEach + public void setup() { + ClassPathXmlApplicationContext ctx = + new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); + testBean = (TestInterface) ctx.getBean("testBean"); + thisAsClassCounter = (Counter) ctx.getBean("thisAsClassCounter"); + thisAsInterfaceCounter = (Counter) ctx.getBean("thisAsInterfaceCounter"); + targetAsClassCounter = (Counter) ctx.getBean("targetAsClassCounter"); + targetAsInterfaceCounter = (Counter) ctx.getBean("targetAsInterfaceCounter"); + thisAsClassAndTargetAsClassCounter = (Counter) ctx.getBean("thisAsClassAndTargetAsClassCounter"); + thisAsInterfaceAndTargetAsInterfaceCounter = (Counter) ctx.getBean("thisAsInterfaceAndTargetAsInterfaceCounter"); + thisAsInterfaceAndTargetAsClassCounter = (Counter) ctx.getBean("thisAsInterfaceAndTargetAsClassCounter"); + + thisAsClassCounter.reset(); + thisAsInterfaceCounter.reset(); + targetAsClassCounter.reset(); + targetAsInterfaceCounter.reset(); + thisAsClassAndTargetAsClassCounter.reset(); + thisAsInterfaceAndTargetAsInterfaceCounter.reset(); + thisAsInterfaceAndTargetAsClassCounter.reset(); + } + + + @Test + public void testThisAsClassDoesNotMatch() { + testBean.doIt(); + assertThat(thisAsClassCounter.getCount()).isEqualTo(0); + } + + @Test + public void testThisAsInterfaceMatch() { + testBean.doIt(); + assertThat(thisAsInterfaceCounter.getCount()).isEqualTo(1); + } + + @Test + public void testTargetAsClassDoesMatch() { + testBean.doIt(); + assertThat(targetAsClassCounter.getCount()).isEqualTo(1); + } + + @Test + public void testTargetAsInterfaceMatch() { + testBean.doIt(); + assertThat(targetAsInterfaceCounter.getCount()).isEqualTo(1); + } + + @Test + public void testThisAsClassAndTargetAsClassCounterNotMatch() { + testBean.doIt(); + assertThat(thisAsClassAndTargetAsClassCounter.getCount()).isEqualTo(0); + } + + @Test + public void testThisAsInterfaceAndTargetAsInterfaceCounterMatch() { + testBean.doIt(); + assertThat(thisAsInterfaceAndTargetAsInterfaceCounter.getCount()).isEqualTo(1); + } + + @Test + public void testThisAsInterfaceAndTargetAsClassCounterMatch() { + testBean.doIt(); + assertThat(thisAsInterfaceAndTargetAsInterfaceCounter.getCount()).isEqualTo(1); + } + +} + + +interface TestInterface { + public void doIt(); +} + + +class TestImpl implements TestInterface { + @Override + public void doIt() { + } +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotatedTestBean.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotatedTestBean.java new file mode 100644 index 0000000..63e0636 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotatedTestBean.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.autoproxy; + +/** + * @author Adrian Colyer + * @since 2.0 + */ +interface AnnotatedTestBean { + + String doThis(); + + String doThat(); + + String doTheOther(); + + String[] doArray(); + +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotatedTestBeanImpl.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotatedTestBeanImpl.java new file mode 100644 index 0000000..a05c915 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotatedTestBeanImpl.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.autoproxy; + +/** + * @author Adrian Colyer + * @since 2.0 + */ +class AnnotatedTestBeanImpl implements AnnotatedTestBean { + + @Override + @TestAnnotation("this value") + public String doThis() { + return "doThis"; + } + + @Override + @TestAnnotation("that value") + public String doThat() { + return "doThat"; + } + + @Override + @TestAnnotation("array value") + public String[] doArray() { + return new String[] {"doThis", "doThat"}; + } + + // not annotated + @Override + public String doTheOther() { + return "doTheOther"; + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotationBindingTestAspect.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotationBindingTestAspect.java new file mode 100644 index 0000000..6fd601d --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotationBindingTestAspect.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.autoproxy; + +import org.aspectj.lang.ProceedingJoinPoint; + +/** + * @author Adrian Colyer + */ +class AnnotationBindingTestAspect { + + public String doWithAnnotation(ProceedingJoinPoint pjp, TestAnnotation testAnnotation) throws Throwable { + return testAnnotation.value(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotationBindingTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotationBindingTests.java new file mode 100644 index 0000000..a10918b --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotationBindingTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.autoproxy; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Adrian Colyer + * @author Chris Beams + */ +public class AnnotationBindingTests { + + private AnnotatedTestBean testBean; + + + @BeforeEach + public void setup() { + ClassPathXmlApplicationContext ctx = + new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-context.xml", getClass()); + testBean = (AnnotatedTestBean) ctx.getBean("testBean"); + } + + + @Test + public void testAnnotationBindingInAroundAdvice() { + assertThat(testBean.doThis()).isEqualTo("this value"); + assertThat(testBean.doThat()).isEqualTo("that value"); + } + + @Test + public void testNoMatchingWithoutAnnotationPresent() { + assertThat(testBean.doTheOther()).isEqualTo("doTheOther"); + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotationPointcutTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotationPointcutTests.java new file mode 100644 index 0000000..10da308 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AnnotationPointcutTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.autoproxy; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @author Chris Beams + */ +public class AnnotationPointcutTests { + + private AnnotatedTestBean testBean; + + + @BeforeEach + public void setup() { + ClassPathXmlApplicationContext ctx = + new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-context.xml", getClass()); + + testBean = (AnnotatedTestBean) ctx.getBean("testBean"); + } + + + @Test + public void testAnnotationBindingInAroundAdvice() { + assertThat(testBean.doThis()).isEqualTo("this value"); + } + + @Test + public void testNoMatchingWithoutAnnotationPresent() { + assertThat(testBean.doTheOther()).isEqualTo("doTheOther"); + } + +} + + +class TestMethodInterceptor implements MethodInterceptor { + + @Override + public Object invoke(MethodInvocation methodInvocation) throws Throwable { + return "this value"; + } +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectImplementingInterfaceTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectImplementingInterfaceTests.java new file mode 100644 index 0000000..dc67e2e --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectImplementingInterfaceTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.autoproxy; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.Advised; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for ensuring the aspects aren't advised. See SPR-3893 for more details. + * + * @author Ramnivas Laddad + * @author Chris Beams + */ +public class AspectImplementingInterfaceTests { + + @Test + public void testProxyCreation() { + ClassPathXmlApplicationContext ctx = + new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-context.xml", getClass()); + + ITestBean testBean = (ITestBean) ctx.getBean("testBean"); + AnInterface interfaceExtendingAspect = (AnInterface) ctx.getBean("interfaceExtendingAspect"); + + boolean condition = testBean instanceof Advised; + assertThat(condition).isTrue(); + boolean condition1 = interfaceExtendingAspect instanceof Advised; + assertThat(condition1).isFalse(); + } + +} + + +interface AnInterface { + public void interfaceMethod(); +} + + +class InterfaceExtendingAspect implements AnInterface { + public void increment(ProceedingJoinPoint pjp) throws Throwable { + pjp.proceed(); + } + + @Override + public void interfaceMethod() { + } +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorAndLazyInitTargetSourceTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorAndLazyInitTargetSourceTests.java new file mode 100644 index 0000000..82b6fbb --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorAndLazyInitTargetSourceTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.autoproxy; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rod Johnson + * @author Rob Harrop + * @author Chris Beams + */ +public class AspectJAutoProxyCreatorAndLazyInitTargetSourceTests { + + @Test + public void testAdrian() { + ClassPathXmlApplicationContext ctx = + new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-context.xml", getClass()); + + ITestBean adrian = (ITestBean) ctx.getBean("adrian"); + assertThat(LazyTestBean.instantiations).isEqualTo(0); + assertThat(adrian).isNotNull(); + adrian.getAge(); + assertThat(adrian.getAge()).isEqualTo(68); + assertThat(LazyTestBean.instantiations).isEqualTo(1); + } + +} + + +class LazyTestBean extends TestBean { + + public static int instantiations; + + public LazyTestBean() { + ++instantiations; + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests.java new file mode 100644 index 0000000..0b6d339 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests.java @@ -0,0 +1,586 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.autoproxy; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Method; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.Pointcut; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.MethodBeforeAdvice; +import org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator; +import org.springframework.aop.aspectj.annotation.AspectMetadata; +import org.springframework.aop.config.AopConfigUtils; +import org.springframework.aop.framework.ProxyConfig; +import org.springframework.aop.support.AopUtils; +import org.springframework.aop.support.StaticMethodMatcherPointcutAdvisor; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.MethodInvokingFactoryBean; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.NestedRuntimeException; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.lang.Nullable; +import org.springframework.util.StopWatch; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for AspectJ auto-proxying. Includes mixing with Spring AOP Advisors + * to demonstrate that existing autoproxying contract is honoured. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Chris Beams + * @author Sam Brannen + */ +public class AspectJAutoProxyCreatorTests { + + private static final Log factoryLog = LogFactory.getLog(DefaultListableBeanFactory.class); + + + @Test + public void testAspectsAreApplied() { + ClassPathXmlApplicationContext bf = newContext("aspects.xml"); + + ITestBean tb = (ITestBean) bf.getBean("adrian"); + assertThat(tb.getAge()).isEqualTo(68); + MethodInvokingFactoryBean factoryBean = (MethodInvokingFactoryBean) bf.getBean("&factoryBean"); + assertThat(AopUtils.isAopProxy(factoryBean.getTargetObject())).isTrue(); + assertThat(((ITestBean) factoryBean.getTargetObject()).getAge()).isEqualTo(68); + } + + @Test + public void testMultipleAspectsWithParameterApplied() { + ClassPathXmlApplicationContext bf = newContext("aspects.xml"); + + ITestBean tb = (ITestBean) bf.getBean("adrian"); + tb.setAge(10); + assertThat(tb.getAge()).isEqualTo(20); + } + + @Test + public void testAspectsAreAppliedInDefinedOrder() { + ClassPathXmlApplicationContext bf = newContext("aspectsWithOrdering.xml"); + + ITestBean tb = (ITestBean) bf.getBean("adrian"); + assertThat(tb.getAge()).isEqualTo(71); + } + + @Test + public void testAspectsAndAdvisorAreApplied() { + ClassPathXmlApplicationContext ac = newContext("aspectsPlusAdvisor.xml"); + + ITestBean shouldBeWeaved = (ITestBean) ac.getBean("adrian"); + doTestAspectsAndAdvisorAreApplied(ac, shouldBeWeaved); + } + + @Test + public void testAspectsAndAdvisorAreAppliedEvenIfComingFromParentFactory() { + ClassPathXmlApplicationContext ac = newContext("aspectsPlusAdvisor.xml"); + + GenericApplicationContext childAc = new GenericApplicationContext(ac); + // Create a child factory with a bean that should be woven + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.getPropertyValues().addPropertyValue(new PropertyValue("name", "Adrian")) + .addPropertyValue(new PropertyValue("age", 34)); + childAc.registerBeanDefinition("adrian2", bd); + // Register the advisor auto proxy creator with subclass + childAc.registerBeanDefinition(AnnotationAwareAspectJAutoProxyCreator.class.getName(), new RootBeanDefinition( + AnnotationAwareAspectJAutoProxyCreator.class)); + childAc.refresh(); + + ITestBean beanFromChildContextThatShouldBeWeaved = (ITestBean) childAc.getBean("adrian2"); + //testAspectsAndAdvisorAreApplied(childAc, (ITestBean) ac.getBean("adrian")); + doTestAspectsAndAdvisorAreApplied(childAc, beanFromChildContextThatShouldBeWeaved); + } + + protected void doTestAspectsAndAdvisorAreApplied(ApplicationContext ac, ITestBean shouldBeWeaved) { + TestBeanAdvisor tba = (TestBeanAdvisor) ac.getBean("advisor"); + + MultiplyReturnValue mrv = (MultiplyReturnValue) ac.getBean("aspect"); + assertThat(mrv.getMultiple()).isEqualTo(3); + + tba.count = 0; + mrv.invocations = 0; + + assertThat(AopUtils.isAopProxy(shouldBeWeaved)).as("Autoproxying must apply from @AspectJ aspect").isTrue(); + assertThat(shouldBeWeaved.getName()).isEqualTo("Adrian"); + assertThat(mrv.invocations).isEqualTo(0); + assertThat(shouldBeWeaved.getAge()).isEqualTo((34 * mrv.getMultiple())); + assertThat(tba.count).as("Spring advisor must be invoked").isEqualTo(2); + assertThat(mrv.invocations).as("Must be able to hold state in aspect").isEqualTo(1); + } + + @Test + public void testPerThisAspect() { + ClassPathXmlApplicationContext bf = newContext("perthis.xml"); + + ITestBean adrian1 = (ITestBean) bf.getBean("adrian"); + assertThat(AopUtils.isAopProxy(adrian1)).isTrue(); + + assertThat(adrian1.getAge()).isEqualTo(0); + assertThat(adrian1.getAge()).isEqualTo(1); + + ITestBean adrian2 = (ITestBean) bf.getBean("adrian"); + assertThat(adrian2).isNotSameAs(adrian1); + assertThat(AopUtils.isAopProxy(adrian1)).isTrue(); + assertThat(adrian2.getAge()).isEqualTo(0); + assertThat(adrian2.getAge()).isEqualTo(1); + assertThat(adrian2.getAge()).isEqualTo(2); + assertThat(adrian2.getAge()).isEqualTo(3); + assertThat(adrian1.getAge()).isEqualTo(2); + } + + @Test + public void testPerTargetAspect() throws SecurityException, NoSuchMethodException { + ClassPathXmlApplicationContext bf = newContext("pertarget.xml"); + + ITestBean adrian1 = (ITestBean) bf.getBean("adrian"); + assertThat(AopUtils.isAopProxy(adrian1)).isTrue(); + + // Does not trigger advice or count + int explicitlySetAge = 25; + adrian1.setAge(explicitlySetAge); + + assertThat(adrian1.getAge()).as("Setter does not initiate advice").isEqualTo(explicitlySetAge); + // Fire aspect + + AspectMetadata am = new AspectMetadata(PerTargetAspect.class, "someBean"); + assertThat(am.getPerClausePointcut().getMethodMatcher().matches(TestBean.class.getMethod("getSpouse"), null)).isTrue(); + + adrian1.getSpouse(); + + assertThat(adrian1.getAge()).as("Advice has now been instantiated").isEqualTo(0); + adrian1.setAge(11); + assertThat(adrian1.getAge()).as("Any int setter increments").isEqualTo(2); + adrian1.setName("Adrian"); + //assertEquals("Any other setter does not increment", 2, adrian1.getAge()); + + ITestBean adrian2 = (ITestBean) bf.getBean("adrian"); + assertThat(adrian2).isNotSameAs(adrian1); + assertThat(AopUtils.isAopProxy(adrian1)).isTrue(); + assertThat(adrian2.getAge()).isEqualTo(34); + adrian2.getSpouse(); + assertThat(adrian2.getAge()).as("Aspect now fired").isEqualTo(0); + assertThat(adrian2.getAge()).isEqualTo(1); + assertThat(adrian2.getAge()).isEqualTo(2); + assertThat(adrian1.getAge()).isEqualTo(3); + } + + @Test + public void testTwoAdviceAspect() { + ClassPathXmlApplicationContext bf = newContext("twoAdviceAspect.xml"); + + ITestBean adrian1 = (ITestBean) bf.getBean("adrian"); + testAgeAspect(adrian1, 0, 2); + } + + @Test + public void testTwoAdviceAspectSingleton() { + ClassPathXmlApplicationContext bf = newContext("twoAdviceAspectSingleton.xml"); + + ITestBean adrian1 = (ITestBean) bf.getBean("adrian"); + testAgeAspect(adrian1, 0, 1); + ITestBean adrian2 = (ITestBean) bf.getBean("adrian"); + assertThat(adrian2).isNotSameAs(adrian1); + testAgeAspect(adrian2, 2, 1); + } + + @Test + public void testTwoAdviceAspectPrototype() { + ClassPathXmlApplicationContext bf = newContext("twoAdviceAspectPrototype.xml"); + + ITestBean adrian1 = (ITestBean) bf.getBean("adrian"); + testAgeAspect(adrian1, 0, 1); + ITestBean adrian2 = (ITestBean) bf.getBean("adrian"); + assertThat(adrian2).isNotSameAs(adrian1); + testAgeAspect(adrian2, 0, 1); + } + + private void testAgeAspect(ITestBean adrian, int start, int increment) { + assertThat(AopUtils.isAopProxy(adrian)).isTrue(); + adrian.setName(""); + assertThat(adrian.age()).isEqualTo(start); + int newAge = 32; + adrian.setAge(newAge); + assertThat(adrian.age()).isEqualTo((start + increment)); + adrian.setAge(0); + assertThat(adrian.age()).isEqualTo((start + increment * 2)); + } + + @Test + public void testAdviceUsingJoinPoint() { + ClassPathXmlApplicationContext bf = newContext("usesJoinPointAspect.xml"); + + ITestBean adrian1 = (ITestBean) bf.getBean("adrian"); + adrian1.getAge(); + AdviceUsingThisJoinPoint aspectInstance = (AdviceUsingThisJoinPoint) bf.getBean("aspect"); + //(AdviceUsingThisJoinPoint) Aspects.aspectOf(AdviceUsingThisJoinPoint.class); + //assertEquals("method-execution(int TestBean.getAge())",aspectInstance.getLastMethodEntered()); + assertThat(aspectInstance.getLastMethodEntered().indexOf("TestBean.getAge())") != 0).isTrue(); + } + + @Test + public void testIncludeMechanism() { + ClassPathXmlApplicationContext bf = newContext("usesInclude.xml"); + + ITestBean adrian = (ITestBean) bf.getBean("adrian"); + assertThat(AopUtils.isAopProxy(adrian)).isTrue(); + assertThat(adrian.getAge()).isEqualTo(68); + } + + @Test + public void testForceProxyTargetClass() { + ClassPathXmlApplicationContext bf = newContext("aspectsWithCGLIB.xml"); + + ProxyConfig pc = (ProxyConfig) bf.getBean(AopConfigUtils.AUTO_PROXY_CREATOR_BEAN_NAME); + assertThat(pc.isProxyTargetClass()).as("should be proxying classes").isTrue(); + assertThat(pc.isExposeProxy()).as("should expose proxy").isTrue(); + } + + @Test + public void testWithAbstractFactoryBeanAreApplied() { + ClassPathXmlApplicationContext bf = newContext("aspectsWithAbstractBean.xml"); + + ITestBean adrian = (ITestBean) bf.getBean("adrian"); + assertThat(AopUtils.isAopProxy(adrian)).isTrue(); + assertThat(adrian.getAge()).isEqualTo(68); + } + + @Test + public void testRetryAspect() { + ClassPathXmlApplicationContext bf = newContext("retryAspect.xml"); + + UnreliableBean bean = (UnreliableBean) bf.getBean("unreliableBean"); + RetryAspect aspect = (RetryAspect) bf.getBean("retryAspect"); + int attempts = bean.unreliable(); + assertThat(attempts).isEqualTo(2); + assertThat(aspect.getBeginCalls()).isEqualTo(2); + assertThat(aspect.getRollbackCalls()).isEqualTo(1); + assertThat(aspect.getCommitCalls()).isEqualTo(1); + } + + @Test + public void testWithBeanNameAutoProxyCreator() { + ClassPathXmlApplicationContext bf = newContext("withBeanNameAutoProxyCreator.xml"); + + ITestBean tb = (ITestBean) bf.getBean("adrian"); + assertThat(tb.getAge()).isEqualTo(68); + } + + + /** + * Returns a new {@link ClassPathXmlApplicationContext} for the file ending in fileSuffix. + */ + private ClassPathXmlApplicationContext newContext(String fileSuffix) { + return new ClassPathXmlApplicationContext(qName(fileSuffix), getClass()); + } + + /** + * Returns the relatively qualified name for fileSuffix. + * e.g. for a fileSuffix='foo.xml', this method will return + * 'AspectJAutoProxyCreatorTests-foo.xml' + */ + private String qName(String fileSuffix) { + return String.format("%s-%s", getClass().getSimpleName(), fileSuffix); + } + + private void assertStopWatchTimeLimit(final StopWatch sw, final long maxTimeMillis) { + long totalTimeMillis = sw.getTotalTimeMillis(); + assertThat(totalTimeMillis < maxTimeMillis).as("'" + sw.getLastTaskName() + "' took too long: expected less than<" + maxTimeMillis + + "> ms, actual<" + totalTimeMillis + "> ms.").isTrue(); + } + +} + +@Aspect("pertarget(execution(* *.getSpouse()))") +class PerTargetAspect implements Ordered { + + public int count; + + private int order = Ordered.LOWEST_PRECEDENCE; + + @Around("execution(int *.getAge())") + public int returnCountAsAge() { + return count++; + } + + @Before("execution(void *.set*(int))") + public void countSetter() { + ++count; + } + + @Override + public int getOrder() { + return this.order; + } + + public void setOrder(int order) { + this.order = order; + } +} + +@Aspect +class AdviceUsingThisJoinPoint { + + private String lastEntry = ""; + + public String getLastMethodEntered() { + return this.lastEntry; + } + + @Pointcut("execution(* *(..))") + public void methodExecution() { + } + + @Before("methodExecution()") + public void entryTrace(JoinPoint jp) { + this.lastEntry = jp.toString(); + } +} + +@Aspect +class DummyAspect { + + @Around("execution(* setAge(int))") + public Object test(ProceedingJoinPoint pjp) throws Throwable { + return pjp.proceed(); + } +} + +@Aspect +class DummyAspectWithParameter { + + @Around("execution(* setAge(int)) && args(age)") + public Object test(ProceedingJoinPoint pjp, int age) throws Throwable { + return pjp.proceed(); + } + +} + +class DummyFactoryBean implements FactoryBean { + + @Override + public Object getObject() { + throw new UnsupportedOperationException(); + } + + @Override + public Class getObjectType() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isSingleton() { + throw new UnsupportedOperationException(); + } + +} + +@Aspect +@Order(10) +class IncreaseReturnValue { + + @Around("execution(int *.getAge())") + public Object doubleReturnValue(ProceedingJoinPoint pjp) throws Throwable { + int result = (Integer) pjp.proceed(); + return result + 3; + } +} + +@Aspect +class MultiplyReturnValue { + + private int multiple = 2; + + public int invocations; + + public void setMultiple(int multiple) { + this.multiple = multiple; + } + + public int getMultiple() { + return this.multiple; + } + + @Around("execution(int *.getAge())") + public Object doubleReturnValue(ProceedingJoinPoint pjp) throws Throwable { + ++this.invocations; + int result = (Integer) pjp.proceed(); + return result * this.multiple; + } +} + +@Retention(RetentionPolicy.RUNTIME) +@interface Marker { +} + +@Aspect +class MultiplyReturnValueForMarker { + + private int multiple = 2; + + public int invocations; + + public void setMultiple(int multiple) { + this.multiple = multiple; + } + + public int getMultiple() { + return this.multiple; + } + + @Around("@annotation(org.springframework.aop.aspectj.autoproxy.Marker)") + public Object doubleReturnValue(ProceedingJoinPoint pjp) throws Throwable { + ++this.invocations; + int result = (Integer) pjp.proceed(); + return result * this.multiple; + } +} + +interface IMarkerTestBean extends ITestBean { + + @Marker + @Override + int getAge(); +} + +class MarkerTestBean extends TestBean implements IMarkerTestBean { + + @Marker + @Override + public int getAge() { + return super.getAge(); + } +} + +@Aspect +class RetryAspect { + + private int beginCalls; + + private int commitCalls; + + private int rollbackCalls; + + @Pointcut("execution(public * UnreliableBean.*(..))") + public void execOfPublicMethod() { + } + + /** + * Retry Advice + */ + @Around("execOfPublicMethod()") + public Object retry(ProceedingJoinPoint jp) throws Throwable { + boolean retry = true; + Object o = null; + while (retry) { + try { + retry = false; + this.beginCalls++; + try { + o = jp.proceed(); + this.commitCalls++; + } + catch (RetryableException re) { + this.rollbackCalls++; + throw re; + } + } + catch (RetryableException re) { + retry = true; + } + } + return o; + } + + public int getBeginCalls() { + return this.beginCalls; + } + + public int getCommitCalls() { + return this.commitCalls; + } + + public int getRollbackCalls() { + return this.rollbackCalls; + } +} + +@SuppressWarnings("serial") +class RetryableException extends NestedRuntimeException { + + public RetryableException(String msg) { + super(msg); + } + + public RetryableException(String msg, Throwable cause) { + super(msg, cause); + } +} + +class UnreliableBean { + + private int calls; + + public int unreliable() { + this.calls++; + if (this.calls % 2 != 0) { + throw new RetryableException("foo"); + } + return this.calls; + } + +} + +@SuppressWarnings("serial") +class TestBeanAdvisor extends StaticMethodMatcherPointcutAdvisor { + + public int count; + + public TestBeanAdvisor() { + setAdvice(new MethodBeforeAdvice() { + @Override + public void before(Method method, Object[] args, @Nullable Object target) throws Throwable { + ++count; + } + }); + } + + @Override + public boolean matches(Method method, @Nullable Class targetClass) { + return ITestBean.class.isAssignableFrom(targetClass); + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AtAspectJAfterThrowingTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AtAspectJAfterThrowingTests.java new file mode 100644 index 0000000..1c6dd13 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AtAspectJAfterThrowingTests.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.autoproxy; + +import java.io.IOException; + +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + * @author Chris Beams + * @since 2.0 + */ +public class AtAspectJAfterThrowingTests { + + @Test + public void testAccessThrowable() { + ClassPathXmlApplicationContext ctx = + new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-context.xml", getClass()); + + ITestBean bean = (ITestBean) ctx.getBean("testBean"); + ExceptionHandlingAspect aspect = (ExceptionHandlingAspect) ctx.getBean("aspect"); + + assertThat(AopUtils.isAopProxy(bean)).isTrue(); + IOException exceptionThrown = null; + try { + bean.unreliableFileOperation(); + } + catch (IOException ex) { + exceptionThrown = ex; + } + + assertThat(aspect.handled).isEqualTo(1); + assertThat(aspect.lastException).isSameAs(exceptionThrown); + } + +} + + +@Aspect +class ExceptionHandlingAspect { + + public int handled; + + public IOException lastException; + + @AfterThrowing(pointcut = "within(org.springframework.beans.testfixture.beans.ITestBean+)", throwing = "ex") + public void handleIOException(IOException ex) { + handled++; + lastException = ex; + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AtAspectJAnnotationBindingTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AtAspectJAnnotationBindingTests.java new file mode 100644 index 0000000..679a4ce --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AtAspectJAnnotationBindingTests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.autoproxy; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.core.io.Resource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Adrian Colyer + * @author Juergen Hoeller + * @author Chris Beams + */ +public class AtAspectJAnnotationBindingTests { + + private AnnotatedTestBean testBean; + + private ClassPathXmlApplicationContext ctx; + + + @BeforeEach + public void setup() { + ctx = new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-context.xml", getClass()); + testBean = (AnnotatedTestBean) ctx.getBean("testBean"); + } + + + @Test + public void testAnnotationBindingInAroundAdvice() { + assertThat(testBean.doThis()).isEqualTo("this value doThis"); + assertThat(testBean.doThat()).isEqualTo("that value doThat"); + assertThat(testBean.doArray().length).isEqualTo(2); + } + + @Test + public void testNoMatchingWithoutAnnotationPresent() { + assertThat(testBean.doTheOther()).isEqualTo("doTheOther"); + } + + @Test + public void testPointcutEvaluatedAgainstArray() { + ctx.getBean("arrayFactoryBean"); + } + +} + + +@Aspect +class AtAspectJAnnotationBindingTestAspect { + + @Around("execution(* *(..)) && @annotation(testAnn)") + public Object doWithAnnotation(ProceedingJoinPoint pjp, TestAnnotation testAnn) throws Throwable { + String annValue = testAnn.value(); + Object result = pjp.proceed(); + return (result instanceof String ? annValue + " " + result : result); + } + +} + + +class ResourceArrayFactoryBean implements FactoryBean { + + @Override + @TestAnnotation("some value") + public Object getObject() { + return new Resource[0]; + } + + @Override + @TestAnnotation("some value") + public Class getObjectType() { + return Resource[].class; + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/TestAnnotation.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/TestAnnotation.java new file mode 100644 index 0000000..6b655bc --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/TestAnnotation.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.autoproxy; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * @author Adrian Colyer + * @since 2.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@interface TestAnnotation { + String value() ; +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/benchmark/BenchmarkTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/benchmark/BenchmarkTests.java new file mode 100644 index 0000000..c26ed5e --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/benchmark/BenchmarkTests.java @@ -0,0 +1,284 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.autoproxy.benchmark; + +import java.lang.reflect.Method; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.Advisor; +import org.springframework.aop.AfterReturningAdvice; +import org.springframework.aop.MethodBeforeAdvice; +import org.springframework.aop.framework.Advised; +import org.springframework.aop.support.AopUtils; +import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.aop.support.StaticMethodMatcherPointcut; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.util.StopWatch; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for AspectJ auto proxying. Includes mixing with Spring AOP + * Advisors to demonstrate that existing autoproxying contract is honoured. + * + * @author Rod Johnson + * @author Chris Beams + */ +public class BenchmarkTests { + + private static final Class CLASS = BenchmarkTests.class; + + private static final String ASPECTJ_CONTEXT = CLASS.getSimpleName() + "-aspectj.xml"; + + private static final String SPRING_AOP_CONTEXT = CLASS.getSimpleName() + "-springAop.xml"; + + @Test + public void testRepeatedAroundAdviceInvocationsWithAspectJ() { + testRepeatedAroundAdviceInvocations(ASPECTJ_CONTEXT, getCount(), "AspectJ"); + } + + @Test + public void testRepeatedAroundAdviceInvocationsWithSpringAop() { + testRepeatedAroundAdviceInvocations(SPRING_AOP_CONTEXT, getCount(), "Spring AOP"); + } + + @Test + public void testRepeatedBeforeAdviceInvocationsWithAspectJ() { + testBeforeAdviceWithoutJoinPoint(ASPECTJ_CONTEXT, getCount(), "AspectJ"); + } + + @Test + public void testRepeatedBeforeAdviceInvocationsWithSpringAop() { + testBeforeAdviceWithoutJoinPoint(SPRING_AOP_CONTEXT, getCount(), "Spring AOP"); + } + + @Test + public void testRepeatedAfterReturningAdviceInvocationsWithAspectJ() { + testAfterReturningAdviceWithoutJoinPoint(ASPECTJ_CONTEXT, getCount(), "AspectJ"); + } + + @Test + public void testRepeatedAfterReturningAdviceInvocationsWithSpringAop() { + testAfterReturningAdviceWithoutJoinPoint(SPRING_AOP_CONTEXT, getCount(), "Spring AOP"); + } + + @Test + public void testRepeatedMixWithAspectJ() { + testMix(ASPECTJ_CONTEXT, getCount(), "AspectJ"); + } + + @Test + public void testRepeatedMixWithSpringAop() { + testMix(SPRING_AOP_CONTEXT, getCount(), "Spring AOP"); + } + + /** + * Change the return number to a higher number to make this test useful. + */ + protected int getCount() { + return 10; + } + + private long testRepeatedAroundAdviceInvocations(String file, int howmany, String technology) { + ClassPathXmlApplicationContext bf = new ClassPathXmlApplicationContext(file, CLASS); + + StopWatch sw = new StopWatch(); + sw.start(howmany + " repeated around advice invocations with " + technology); + ITestBean adrian = (ITestBean) bf.getBean("adrian"); + + assertThat(AopUtils.isAopProxy(adrian)).isTrue(); + assertThat(adrian.getAge()).isEqualTo(68); + + for (int i = 0; i < howmany; i++) { + adrian.getAge(); + } + + sw.stop(); + System.out.println(sw.prettyPrint()); + return sw.getLastTaskTimeMillis(); + } + + private long testBeforeAdviceWithoutJoinPoint(String file, int howmany, String technology) { + ClassPathXmlApplicationContext bf = new ClassPathXmlApplicationContext(file, CLASS); + + StopWatch sw = new StopWatch(); + sw.start(howmany + " repeated before advice invocations with " + technology); + ITestBean adrian = (ITestBean) bf.getBean("adrian"); + + assertThat(AopUtils.isAopProxy(adrian)).isTrue(); + Advised a = (Advised) adrian; + assertThat(a.getAdvisors().length >= 3).isTrue(); + assertThat(adrian.getName()).isEqualTo("adrian"); + + for (int i = 0; i < howmany; i++) { + adrian.getName(); + } + + sw.stop(); + System.out.println(sw.prettyPrint()); + return sw.getLastTaskTimeMillis(); + } + + private long testAfterReturningAdviceWithoutJoinPoint(String file, int howmany, String technology) { + ClassPathXmlApplicationContext bf = new ClassPathXmlApplicationContext(file, CLASS); + + StopWatch sw = new StopWatch(); + sw.start(howmany + " repeated after returning advice invocations with " + technology); + ITestBean adrian = (ITestBean) bf.getBean("adrian"); + + assertThat(AopUtils.isAopProxy(adrian)).isTrue(); + Advised a = (Advised) adrian; + assertThat(a.getAdvisors().length >= 3).isTrue(); + // Hits joinpoint + adrian.setAge(25); + + for (int i = 0; i < howmany; i++) { + adrian.setAge(i); + } + + sw.stop(); + System.out.println(sw.prettyPrint()); + return sw.getLastTaskTimeMillis(); + } + + private long testMix(String file, int howmany, String technology) { + ClassPathXmlApplicationContext bf = new ClassPathXmlApplicationContext(file, CLASS); + + StopWatch sw = new StopWatch(); + sw.start(howmany + " repeated mixed invocations with " + technology); + ITestBean adrian = (ITestBean) bf.getBean("adrian"); + + assertThat(AopUtils.isAopProxy(adrian)).isTrue(); + Advised a = (Advised) adrian; + assertThat(a.getAdvisors().length >= 3).isTrue(); + + for (int i = 0; i < howmany; i++) { + // Hit all 3 joinpoints + adrian.getAge(); + adrian.getName(); + adrian.setAge(i); + + // Invoke three non-advised methods + adrian.getDoctor(); + adrian.getLawyer(); + adrian.getSpouse(); + } + + sw.stop(); + System.out.println(sw.prettyPrint()); + return sw.getLastTaskTimeMillis(); + } + +} + + +class MultiplyReturnValueInterceptor implements MethodInterceptor { + + private int multiple = 2; + + public int invocations; + + public void setMultiple(int multiple) { + this.multiple = multiple; + } + + public int getMultiple() { + return this.multiple; + } + + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + ++invocations; + int result = (Integer) mi.proceed(); + return result * this.multiple; + } + +} + + +class TraceAfterReturningAdvice implements AfterReturningAdvice { + + public int afterTakesInt; + + @Override + public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable { + ++afterTakesInt; + } + + public static Advisor advisor() { + return new DefaultPointcutAdvisor( + new StaticMethodMatcherPointcut() { + @Override + public boolean matches(Method method, Class targetClass) { + return method.getParameterCount() == 1 && + method.getParameterTypes()[0].equals(Integer.class); + } + }, + new TraceAfterReturningAdvice()); + } + +} + + +@Aspect +class TraceAspect { + + public int beforeStringReturn; + + public int afterTakesInt; + + @Before("execution(String *.*(..))") + public void traceWithoutJoinPoint() { + ++beforeStringReturn; + } + + @AfterReturning("execution(void *.*(int))") + public void traceWithoutJoinPoint2() { + ++afterTakesInt; + } + +} + + +class TraceBeforeAdvice implements MethodBeforeAdvice { + + public int beforeStringReturn; + + @Override + public void before(Method method, Object[] args, Object target) throws Throwable { + ++beforeStringReturn; + } + + public static Advisor advisor() { + return new DefaultPointcutAdvisor( + new StaticMethodMatcherPointcut() { + @Override + public boolean matches(Method method, Class targetClass) { + return method.getReturnType().equals(String.class); + } + }, + new TraceBeforeAdvice()); + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/spr3064/SPR3064Tests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/spr3064/SPR3064Tests.java new file mode 100644 index 0000000..48a43c1 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/spr3064/SPR3064Tests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.autoproxy.spr3064; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.junit.jupiter.api.Test; + +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Adrian Colyer + * @author Chris Beams + */ +public class SPR3064Tests { + + private Service service; + + + @Test + public void testServiceIsAdvised() { + ClassPathXmlApplicationContext ctx = + new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); + + service = (Service) ctx.getBean("service"); + assertThatExceptionOfType(RuntimeException.class).isThrownBy( + this.service::serveMe) + .withMessageContaining("advice invoked"); + } + +} + + +@Retention(RetentionPolicy.RUNTIME) +@interface Transaction { +} + + +@Aspect +class TransactionInterceptor { + + @Around(value="execution(* *..Service.*(..)) && @annotation(transaction)") + public Object around(ProceedingJoinPoint pjp, Transaction transaction) throws Throwable { + throw new RuntimeException("advice invoked"); + //return pjp.proceed(); + } +} + + +interface Service { + + void serveMe(); +} + + +class ServiceImpl implements Service { + + @Override + @Transaction + public void serveMe() { + } +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/generic/AfterReturningGenericTypeMatchingTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/generic/AfterReturningGenericTypeMatchingTests.java new file mode 100644 index 0000000..55507fe --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/generic/AfterReturningGenericTypeMatchingTests.java @@ -0,0 +1,176 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.generic; + +import java.util.ArrayList; +import java.util.Collection; + +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.testfixture.beans.Employee; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests ensuring that after-returning advice for generic parameters bound to + * the advice and the return type follow AspectJ semantics. + * + *

    See SPR-3628 for more details. + * + * @author Ramnivas Laddad + * @author Chris Beams + */ +public class AfterReturningGenericTypeMatchingTests { + + private GenericReturnTypeVariationClass testBean; + + private CounterAspect counterAspect; + + + @BeforeEach + public void setup() { + ClassPathXmlApplicationContext ctx = + new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-context.xml", getClass()); + + counterAspect = (CounterAspect) ctx.getBean("counterAspect"); + counterAspect.reset(); + + testBean = (GenericReturnTypeVariationClass) ctx.getBean("testBean"); + } + + + @Test + public void testReturnTypeExactMatching() { + testBean.getStrings(); + assertThat(counterAspect.getStringsInvocationsCount).isEqualTo(1); + assertThat(counterAspect.getIntegersInvocationsCount).isEqualTo(0); + + counterAspect.reset(); + + testBean.getIntegers(); + assertThat(counterAspect.getStringsInvocationsCount).isEqualTo(0); + assertThat(counterAspect.getIntegersInvocationsCount).isEqualTo(1); + } + + @Test + public void testReturnTypeRawMatching() { + testBean.getStrings(); + assertThat(counterAspect.getRawsInvocationsCount).isEqualTo(1); + + counterAspect.reset(); + + testBean.getIntegers(); + assertThat(counterAspect.getRawsInvocationsCount).isEqualTo(1); + } + + @Test + public void testReturnTypeUpperBoundMatching() { + testBean.getIntegers(); + assertThat(counterAspect.getNumbersInvocationsCount).isEqualTo(1); + } + + @Test + public void testReturnTypeLowerBoundMatching() { + testBean.getTestBeans(); + assertThat(counterAspect.getTestBeanInvocationsCount).isEqualTo(1); + + counterAspect.reset(); + + testBean.getEmployees(); + assertThat(counterAspect.getTestBeanInvocationsCount).isEqualTo(0); + } + +} + + +class GenericReturnTypeVariationClass { + + public Collection getStrings() { + return new ArrayList<>(); + } + + public Collection getIntegers() { + return new ArrayList<>(); + } + + public Collection getTestBeans() { + return new ArrayList<>(); + } + + public Collection getEmployees() { + return new ArrayList<>(); + } +} + + +@Aspect +class CounterAspect { + + int getRawsInvocationsCount; + + int getStringsInvocationsCount; + + int getIntegersInvocationsCount; + + int getNumbersInvocationsCount; + + int getTestBeanInvocationsCount; + + @Pointcut("execution(* org.springframework.aop.aspectj.generic.GenericReturnTypeVariationClass.*(..))") + public void anyTestMethod() { + } + + @AfterReturning(pointcut = "anyTestMethod()", returning = "ret") + public void incrementGetRawsInvocationsCount(Collection ret) { + getRawsInvocationsCount++; + } + + @AfterReturning(pointcut = "anyTestMethod()", returning = "ret") + public void incrementGetStringsInvocationsCount(Collection ret) { + getStringsInvocationsCount++; + } + + @AfterReturning(pointcut = "anyTestMethod()", returning = "ret") + public void incrementGetIntegersInvocationsCount(Collection ret) { + getIntegersInvocationsCount++; + } + + @AfterReturning(pointcut = "anyTestMethod()", returning = "ret") + public void incrementGetNumbersInvocationsCount(Collection ret) { + getNumbersInvocationsCount++; + } + + @AfterReturning(pointcut = "anyTestMethod()", returning = "ret") + public void incrementTestBeanInvocationsCount(Collection ret) { + getTestBeanInvocationsCount++; + } + + public void reset() { + getRawsInvocationsCount = 0; + getStringsInvocationsCount = 0; + getIntegersInvocationsCount = 0; + getNumbersInvocationsCount = 0; + getTestBeanInvocationsCount = 0; + } +} + diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/generic/GenericBridgeMethodMatchingClassProxyTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/generic/GenericBridgeMethodMatchingClassProxyTests.java new file mode 100644 index 0000000..6fa0be4 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/generic/GenericBridgeMethodMatchingClassProxyTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.generic; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for AspectJ pointcut expression matching when working with bridge methods. + * + *

    This class focuses on class proxying. + * + *

    See GenericBridgeMethodMatchingTests for more details. + * + * @author Ramnivas Laddad + * @author Chris Beams + */ +public class GenericBridgeMethodMatchingClassProxyTests extends GenericBridgeMethodMatchingTests { + + @Test + public void testGenericDerivedInterfaceMethodThroughClass() { + ((DerivedStringParameterizedClass) testBean).genericDerivedInterfaceMethod(""); + assertThat(counterAspect.count).isEqualTo(1); + } + + @Test + public void testGenericBaseInterfaceMethodThroughClass() { + ((DerivedStringParameterizedClass) testBean).genericBaseInterfaceMethod(""); + assertThat(counterAspect.count).isEqualTo(1); + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/generic/GenericBridgeMethodMatchingTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/generic/GenericBridgeMethodMatchingTests.java new file mode 100644 index 0000000..9e92912 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/generic/GenericBridgeMethodMatchingTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.generic; + +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.junit.jupiter.api.Test; + +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for AspectJ pointcut expression matching when working with bridge methods. + * + *

    Depending on the caller's static type either the bridge method or the user-implemented method + * gets called as the way into the proxy. Therefore, we need tests for calling a bean with + * static type set to type with generic method and to type with specific non-generic implementation. + * + *

    This class focuses on JDK proxy, while a subclass, GenericBridgeMethodMatchingClassProxyTests, + * focuses on class proxying. + * + * See SPR-3556 for more details. + * + * @author Ramnivas Laddad + * @author Chris Beams + */ +public class GenericBridgeMethodMatchingTests { + + protected DerivedInterface testBean; + + protected GenericCounterAspect counterAspect; + + + @SuppressWarnings("unchecked") + @org.junit.jupiter.api.BeforeEach + public void setup() { + ClassPathXmlApplicationContext ctx = + new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-context.xml", getClass()); + + counterAspect = (GenericCounterAspect) ctx.getBean("counterAspect"); + counterAspect.count = 0; + + testBean = (DerivedInterface) ctx.getBean("testBean"); + } + + + @Test + public void testGenericDerivedInterfaceMethodThroughInterface() { + testBean.genericDerivedInterfaceMethod(""); + assertThat(counterAspect.count).isEqualTo(1); + } + + @Test + public void testGenericBaseInterfaceMethodThroughInterface() { + testBean.genericBaseInterfaceMethod(""); + assertThat(counterAspect.count).isEqualTo(1); + } + +} + + +interface BaseInterface { + + void genericBaseInterfaceMethod(T t); +} + + +interface DerivedInterface extends BaseInterface { + + public void genericDerivedInterfaceMethod(T t); +} + + +class DerivedStringParameterizedClass implements DerivedInterface { + + @Override + public void genericDerivedInterfaceMethod(String t) { + } + + @Override + public void genericBaseInterfaceMethod(String t) { + } +} + +@Aspect +class GenericCounterAspect { + + public int count; + + @Before("execution(* *..BaseInterface+.*(..))") + public void increment() { + count++; + } + +} + diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/generic/GenericParameterMatchingTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/generic/GenericParameterMatchingTests.java new file mode 100644 index 0000000..5849a53 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/generic/GenericParameterMatchingTests.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.aspectj.generic; + +import java.util.Collection; + +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.Pointcut; +import org.junit.jupiter.api.Test; + +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests that pointcut matching is correct with generic method parameter. + * See SPR-3904 for more details. + * + * @author Ramnivas Laddad + * @author Chris Beams + */ +public class GenericParameterMatchingTests { + + private CounterAspect counterAspect; + + private GenericInterface testBean; + + + @SuppressWarnings("unchecked") + @org.junit.jupiter.api.BeforeEach + public void setup() { + ClassPathXmlApplicationContext ctx = + new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-context.xml", getClass()); + + counterAspect = (CounterAspect) ctx.getBean("counterAspect"); + counterAspect.reset(); + + testBean = (GenericInterface) ctx.getBean("testBean"); + } + + + @Test + public void testGenericInterfaceGenericArgExecution() { + testBean.save(""); + assertThat(counterAspect.genericInterfaceGenericArgExecutionCount).isEqualTo(1); + } + + @Test + public void testGenericInterfaceGenericCollectionArgExecution() { + testBean.saveAll(null); + assertThat(counterAspect.genericInterfaceGenericCollectionArgExecutionCount).isEqualTo(1); + } + + @Test + public void testGenericInterfaceSubtypeGenericCollectionArgExecution() { + testBean.saveAll(null); + assertThat(counterAspect.genericInterfaceSubtypeGenericCollectionArgExecutionCount).isEqualTo(1); + } + + + static interface GenericInterface { + + public void save(T bean); + + public void saveAll(Collection beans); + } + + + static class GenericImpl implements GenericInterface { + + @Override + public void save(T bean) { + } + + @Override + public void saveAll(Collection beans) { + } + } + + + @Aspect + static class CounterAspect { + + int genericInterfaceGenericArgExecutionCount; + int genericInterfaceGenericCollectionArgExecutionCount; + int genericInterfaceSubtypeGenericCollectionArgExecutionCount; + + public void reset() { + genericInterfaceGenericArgExecutionCount = 0; + genericInterfaceGenericCollectionArgExecutionCount = 0; + genericInterfaceSubtypeGenericCollectionArgExecutionCount = 0; + } + + @Pointcut("execution(* org.springframework.aop.aspectj.generic.GenericParameterMatchingTests.GenericInterface.save(..))") + public void genericInterfaceGenericArgExecution() {} + + @Pointcut("execution(* org.springframework.aop.aspectj.generic.GenericParameterMatchingTests.GenericInterface.saveAll(..))") + public void GenericInterfaceGenericCollectionArgExecution() {} + + @Pointcut("execution(* org.springframework.aop.aspectj.generic.GenericParameterMatchingTests.GenericInterface+.saveAll(..))") + public void genericInterfaceSubtypeGenericCollectionArgExecution() {} + + @Before("genericInterfaceGenericArgExecution()") + public void incrementGenericInterfaceGenericArgExecution() { + genericInterfaceGenericArgExecutionCount++; + } + + @Before("GenericInterfaceGenericCollectionArgExecution()") + public void incrementGenericInterfaceGenericCollectionArgExecution() { + genericInterfaceGenericCollectionArgExecutionCount++; + } + + @Before("genericInterfaceSubtypeGenericCollectionArgExecution()") + public void incrementGenericInterfaceSubtypeGenericCollectionArgExecution() { + genericInterfaceSubtypeGenericCollectionArgExecutionCount++; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerAdviceTypeTests.java b/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerAdviceTypeTests.java new file mode 100644 index 0000000..8e94e90 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerAdviceTypeTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.config; + +import org.junit.jupiter.api.Test; +import org.xml.sax.SAXParseException; + +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Adrian Colyer + * @author Chris Beams + */ +public class AopNamespaceHandlerAdviceTypeTests { + + @Test + public void testParsingOfAdviceTypes() { + new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-ok.xml", getClass()); + } + + @Test + public void testParsingOfAdviceTypesWithError() { + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> + new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-error.xml", getClass())) + .matches(ex -> ex.contains(SAXParseException.class)); + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerArgNamesTests.java b/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerArgNamesTests.java new file mode 100644 index 0000000..5457d96 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerArgNamesTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.config; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Adrian Colyer + * @author Chris Beams + */ +public class AopNamespaceHandlerArgNamesTests { + + @Test + public void testArgNamesOK() { + new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-ok.xml", getClass()); + } + + @Test + public void testArgNamesError() { + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-error.xml", getClass())) + .matches(ex -> ex.contains(IllegalArgumentException.class)); + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerProxyTargetClassTests.java b/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerProxyTargetClassTests.java new file mode 100644 index 0000000..d295b1d --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerProxyTargetClassTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.config; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.Advised; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.testfixture.beans.ITestBean; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + * @author Chris Beams + */ +public class AopNamespaceHandlerProxyTargetClassTests extends AopNamespaceHandlerTests { + + @Test + public void testIsClassProxy() { + ITestBean bean = getTestBean(); + assertThat(AopUtils.isCglibProxy(bean)).as("Should be a CGLIB proxy").isTrue(); + assertThat(((Advised) bean).isExposeProxy()).as("Should expose proxy").isTrue(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerReturningTests.java b/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerReturningTests.java new file mode 100644 index 0000000..f20a929 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerReturningTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.config; + +import org.junit.jupiter.api.Test; +import org.xml.sax.SAXParseException; + +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Adrian Colyer + * @author Chris Beams + */ +public class AopNamespaceHandlerReturningTests { + + @Test + public void testReturningOnReturningAdvice() { + new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-ok.xml", getClass()); + } + + @Test + public void testParseReturningOnOtherAdviceType() { + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> + new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-error.xml", getClass())) + .matches(ex -> ex.contains(SAXParseException.class)); + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerTests.java b/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerTests.java new file mode 100644 index 0000000..b3bf1ed --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerTests.java @@ -0,0 +1,197 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.config; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.Advisor; +import org.springframework.aop.framework.Advised; +import org.springframework.aop.support.AopUtils; +import org.springframework.aop.testfixture.advice.CountingBeforeAdvice; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for aop namespace. + * + * @author Rob Harrop + * @author Chris Beams + */ +public class AopNamespaceHandlerTests { + + private ApplicationContext context; + + + @BeforeEach + public void setup() { + this.context = new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-context.xml", getClass()); + } + + protected ITestBean getTestBean() { + return (ITestBean) this.context.getBean("testBean"); + } + + + @Test + public void testIsProxy() throws Exception { + ITestBean bean = getTestBean(); + + assertThat(AopUtils.isAopProxy(bean)).as("Bean is not a proxy").isTrue(); + + // check the advice details + Advised advised = (Advised) bean; + Advisor[] advisors = advised.getAdvisors(); + + assertThat(advisors.length > 0).as("Advisors should not be empty").isTrue(); + } + + @Test + public void testAdviceInvokedCorrectly() throws Exception { + CountingBeforeAdvice getAgeCounter = (CountingBeforeAdvice) this.context.getBean("getAgeCounter"); + CountingBeforeAdvice getNameCounter = (CountingBeforeAdvice) this.context.getBean("getNameCounter"); + + ITestBean bean = getTestBean(); + + assertThat(getAgeCounter.getCalls("getAge")).as("Incorrect initial getAge count").isEqualTo(0); + assertThat(getNameCounter.getCalls("getName")).as("Incorrect initial getName count").isEqualTo(0); + + bean.getAge(); + + assertThat(getAgeCounter.getCalls("getAge")).as("Incorrect getAge count on getAge counter").isEqualTo(1); + assertThat(getNameCounter.getCalls("getAge")).as("Incorrect getAge count on getName counter").isEqualTo(0); + + bean.getName(); + + assertThat(getNameCounter.getCalls("getName")).as("Incorrect getName count on getName counter").isEqualTo(1); + assertThat(getAgeCounter.getCalls("getName")).as("Incorrect getName count on getAge counter").isEqualTo(0); + } + + @Test + public void testAspectApplied() throws Exception { + ITestBean bean = getTestBean(); + + CountingAspectJAdvice advice = (CountingAspectJAdvice) this.context.getBean("countingAdvice"); + + assertThat(advice.getBeforeCount()).as("Incorrect before count").isEqualTo(0); + assertThat(advice.getAfterCount()).as("Incorrect after count").isEqualTo(0); + + bean.setName("Sally"); + + assertThat(advice.getBeforeCount()).as("Incorrect before count").isEqualTo(1); + assertThat(advice.getAfterCount()).as("Incorrect after count").isEqualTo(1); + + bean.getName(); + + assertThat(advice.getBeforeCount()).as("Incorrect before count").isEqualTo(1); + assertThat(advice.getAfterCount()).as("Incorrect after count").isEqualTo(1); + } + + @Test + public void testAspectAppliedForInitializeBeanWithEmptyName() { + ITestBean bean = (ITestBean) this.context.getAutowireCapableBeanFactory().initializeBean(new TestBean(), ""); + + CountingAspectJAdvice advice = (CountingAspectJAdvice) this.context.getBean("countingAdvice"); + + assertThat(advice.getBeforeCount()).as("Incorrect before count").isEqualTo(0); + assertThat(advice.getAfterCount()).as("Incorrect after count").isEqualTo(0); + + bean.setName("Sally"); + + assertThat(advice.getBeforeCount()).as("Incorrect before count").isEqualTo(1); + assertThat(advice.getAfterCount()).as("Incorrect after count").isEqualTo(1); + + bean.getName(); + + assertThat(advice.getBeforeCount()).as("Incorrect before count").isEqualTo(1); + assertThat(advice.getAfterCount()).as("Incorrect after count").isEqualTo(1); + } + + @Test + public void testAspectAppliedForInitializeBeanWithNullName() { + ITestBean bean = (ITestBean) this.context.getAutowireCapableBeanFactory().initializeBean(new TestBean(), null); + + CountingAspectJAdvice advice = (CountingAspectJAdvice) this.context.getBean("countingAdvice"); + + assertThat(advice.getBeforeCount()).as("Incorrect before count").isEqualTo(0); + assertThat(advice.getAfterCount()).as("Incorrect after count").isEqualTo(0); + + bean.setName("Sally"); + + assertThat(advice.getBeforeCount()).as("Incorrect before count").isEqualTo(1); + assertThat(advice.getAfterCount()).as("Incorrect after count").isEqualTo(1); + + bean.getName(); + + assertThat(advice.getBeforeCount()).as("Incorrect before count").isEqualTo(1); + assertThat(advice.getAfterCount()).as("Incorrect after count").isEqualTo(1); + } + +} + + +class CountingAspectJAdvice { + + private int beforeCount; + + private int afterCount; + + private int aroundCount; + + public void myBeforeAdvice() throws Throwable { + this.beforeCount++; + } + + public void myAfterAdvice() throws Throwable { + this.afterCount++; + } + + public void myAroundAdvice(ProceedingJoinPoint pjp) throws Throwable { + this.aroundCount++; + pjp.proceed(); + } + + public void myAfterReturningAdvice(int age) { + this.afterCount++; + } + + public void myAfterThrowingAdvice(RuntimeException ex) { + this.afterCount++; + } + + public void mySetAgeAdvice(int newAge, ITestBean bean) { + // no-op + } + + public int getBeforeCount() { + return this.beforeCount; + } + + public int getAfterCount() { + return this.afterCount; + } + + public int getAroundCount() { + return this.aroundCount; + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerThrowingTests.java b/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerThrowingTests.java new file mode 100644 index 0000000..3597925 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/config/AopNamespaceHandlerThrowingTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.config; + +import org.junit.jupiter.api.Test; +import org.xml.sax.SAXParseException; + +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Adrian Colyer + * @author Chris Beams + */ +public class AopNamespaceHandlerThrowingTests { + + @Test + public void testThrowingOnThrowingAdvice() { + new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-ok.xml", getClass()); + } + + @Test + public void testParseThrowingOnOtherAdviceType() { + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> + new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-error.xml", getClass())) + .matches(ex -> ex.contains(SAXParseException.class)); + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/config/MethodLocatingFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/aop/config/MethodLocatingFactoryBeanTests.java new file mode 100644 index 0000000..473855b --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/config/MethodLocatingFactoryBeanTests.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.config; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author Rick Evans + * @author Chris Beams + */ +public class MethodLocatingFactoryBeanTests { + + private static final String BEAN_NAME = "string"; + private MethodLocatingFactoryBean factory; + private BeanFactory beanFactory; + + @BeforeEach + public void setUp() { + factory = new MethodLocatingFactoryBean(); + beanFactory = mock(BeanFactory.class); + } + + @Test + public void testIsSingleton() { + assertThat(factory.isSingleton()).isTrue(); + } + + @Test + public void testGetObjectType() { + assertThat(factory.getObjectType()).isEqualTo(Method.class); + } + + @Test + public void testWithNullTargetBeanName() { + factory.setMethodName("toString()"); + assertThatIllegalArgumentException().isThrownBy(() -> + factory.setBeanFactory(beanFactory)); + } + + @Test + public void testWithEmptyTargetBeanName() { + factory.setTargetBeanName(""); + factory.setMethodName("toString()"); + assertThatIllegalArgumentException().isThrownBy(() -> + factory.setBeanFactory(beanFactory)); + } + + @Test + public void testWithNullTargetMethodName() { + factory.setTargetBeanName(BEAN_NAME); + assertThatIllegalArgumentException().isThrownBy(() -> + factory.setBeanFactory(beanFactory)); + } + + @Test + public void testWithEmptyTargetMethodName() { + factory.setTargetBeanName(BEAN_NAME); + factory.setMethodName(""); + assertThatIllegalArgumentException().isThrownBy(() -> + factory.setBeanFactory(beanFactory)); + } + + @Test + public void testWhenTargetBeanClassCannotBeResolved() { + factory.setTargetBeanName(BEAN_NAME); + factory.setMethodName("toString()"); + assertThatIllegalArgumentException().isThrownBy(() -> + factory.setBeanFactory(beanFactory)); + verify(beanFactory).getType(BEAN_NAME); + } + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void testSunnyDayPath() throws Exception { + given(beanFactory.getType(BEAN_NAME)).willReturn((Class)String.class); + factory.setTargetBeanName(BEAN_NAME); + factory.setMethodName("toString()"); + factory.setBeanFactory(beanFactory); + Object result = factory.getObject(); + assertThat(result).isNotNull(); + boolean condition = result instanceof Method; + assertThat(condition).isTrue(); + Method method = (Method) result; + assertThat(method.invoke("Bingo")).isEqualTo("Bingo"); + } + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void testWhereMethodCannotBeResolved() { + given(beanFactory.getType(BEAN_NAME)).willReturn((Class)String.class); + factory.setTargetBeanName(BEAN_NAME); + factory.setMethodName("loadOfOld()"); + assertThatIllegalArgumentException().isThrownBy(() -> + factory.setBeanFactory(beanFactory)); + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/config/PrototypeProxyTests.java b/spring-context/src/test/java/org/springframework/aop/config/PrototypeProxyTests.java new file mode 100644 index 0000000..9093cb6 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/config/PrototypeProxyTests.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.config; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.support.ClassPathXmlApplicationContext; + +/** + * @author Juergen Hoeller + * @author Chris Beams + */ +class PrototypeProxyTests { + + @Test + @SuppressWarnings("resource") + void injectionBeforeWrappingCheckDoesNotKickInForPrototypeProxy() { + new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-context.xml", getClass()); + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/framework/AbstractAopProxyTests.java b/spring-context/src/test/java/org/springframework/aop/framework/AbstractAopProxyTests.java new file mode 100644 index 0000000..db27a57 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/framework/AbstractAopProxyTests.java @@ -0,0 +1,1980 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.lang.reflect.Method; +import java.lang.reflect.UndeclaredThrowableException; +import java.rmi.MarshalException; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.aopalliance.aop.Advice; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import test.mixin.LockMixin; +import test.mixin.LockMixinAdvisor; +import test.mixin.Lockable; +import test.mixin.LockedException; + +import org.springframework.aop.Advisor; +import org.springframework.aop.AfterReturningAdvice; +import org.springframework.aop.DynamicIntroductionAdvice; +import org.springframework.aop.MethodBeforeAdvice; +import org.springframework.aop.TargetSource; +import org.springframework.aop.ThrowsAdvice; +import org.springframework.aop.interceptor.DebugInterceptor; +import org.springframework.aop.interceptor.ExposeInvocationInterceptor; +import org.springframework.aop.support.AopUtils; +import org.springframework.aop.support.DefaultIntroductionAdvisor; +import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.aop.support.DelegatingIntroductionInterceptor; +import org.springframework.aop.support.DynamicMethodMatcherPointcut; +import org.springframework.aop.support.NameMatchMethodPointcut; +import org.springframework.aop.support.Pointcuts; +import org.springframework.aop.support.StaticMethodMatcherPointcutAdvisor; +import org.springframework.aop.target.HotSwappableTargetSource; +import org.springframework.aop.target.SingletonTargetSource; +import org.springframework.aop.testfixture.advice.CountingAfterReturningAdvice; +import org.springframework.aop.testfixture.advice.CountingBeforeAdvice; +import org.springframework.aop.testfixture.advice.MethodCounter; +import org.springframework.aop.testfixture.advice.MyThrowsHandler; +import org.springframework.aop.testfixture.interceptor.NopInterceptor; +import org.springframework.aop.testfixture.interceptor.SerializableNopInterceptor; +import org.springframework.aop.testfixture.interceptor.TimestampIntroductionInterceptor; +import org.springframework.beans.testfixture.beans.IOther; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.Person; +import org.springframework.beans.testfixture.beans.SerializablePerson; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.testfixture.TimeStamped; +import org.springframework.core.testfixture.io.SerializationTestUtils; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * @author Rod Johnson + * @author Juergen Hoeller + * @author Chris Beams + * @since 13.03.2003 + */ +public abstract class AbstractAopProxyTests { + + protected final MockTargetSource mockTargetSource = new MockTargetSource(); + + + /** + * Make a clean target source available if code wants to use it. + * The target must be set. Verification will be automatic in tearDown + * to ensure that it was used appropriately by code. + */ + @BeforeEach + public void setUp() { + mockTargetSource.reset(); + } + + @AfterEach + public void tearDown() { + mockTargetSource.verify(); + } + + + /** + * Set in CGLIB or JDK mode. + */ + protected abstract Object createProxy(ProxyCreatorSupport as); + + protected abstract AopProxy createAopProxy(AdvisedSupport as); + + /** + * Is a target always required? + */ + protected boolean requiresTarget() { + return false; + } + + + @Test + public void testNoInterceptorsAndNoTarget() { + assertThatExceptionOfType(AopConfigException.class).isThrownBy(() -> { + AdvisedSupport pc = new AdvisedSupport(ITestBean.class); + //Add no interceptors + AopProxy aop = createAopProxy(pc); + aop.getProxy(); + }); + } + + /** + * Simple test that if we set values we can get them out again. + */ + @Test + public void testValuesStick() { + int age1 = 33; + int age2 = 37; + String name = "tony"; + + TestBean target1 = new TestBean(); + target1.setAge(age1); + ProxyFactory pf1 = new ProxyFactory(target1); + pf1.addAdvisor(new DefaultPointcutAdvisor(new NopInterceptor())); + pf1.addAdvisor(new DefaultPointcutAdvisor(new TimestampIntroductionInterceptor())); + ITestBean tb = (ITestBean) pf1.getProxy(); + + assertThat(tb.getAge()).isEqualTo(age1); + tb.setAge(age2); + assertThat(tb.getAge()).isEqualTo(age2); + assertThat(tb.getName()).isNull(); + tb.setName(name); + assertThat(tb.getName()).isEqualTo(name); + } + + @Test + public void testSerializationAdviceAndTargetNotSerializable() throws Exception { + TestBean tb = new TestBean(); + assertThat(SerializationTestUtils.isSerializable(tb)).isFalse(); + + ProxyFactory pf = new ProxyFactory(tb); + + pf.addAdvice(new NopInterceptor()); + ITestBean proxy = (ITestBean) createAopProxy(pf).getProxy(); + + assertThat(SerializationTestUtils.isSerializable(proxy)).isFalse(); + } + + @Test + public void testSerializationAdviceNotSerializable() throws Exception { + SerializablePerson sp = new SerializablePerson(); + assertThat(SerializationTestUtils.isSerializable(sp)).isTrue(); + + ProxyFactory pf = new ProxyFactory(sp); + + // This isn't serializable + Advice i = new NopInterceptor(); + pf.addAdvice(i); + assertThat(SerializationTestUtils.isSerializable(i)).isFalse(); + Object proxy = createAopProxy(pf).getProxy(); + + assertThat(SerializationTestUtils.isSerializable(proxy)).isFalse(); + } + + @Test + public void testSerializableTargetAndAdvice() throws Throwable { + SerializablePerson personTarget = new SerializablePerson(); + personTarget.setName("jim"); + personTarget.setAge(26); + + assertThat(SerializationTestUtils.isSerializable(personTarget)).isTrue(); + + ProxyFactory pf = new ProxyFactory(personTarget); + + CountingThrowsAdvice cta = new CountingThrowsAdvice(); + + pf.addAdvice(new SerializableNopInterceptor()); + // Try various advice types + pf.addAdvice(new CountingBeforeAdvice()); + pf.addAdvice(new CountingAfterReturningAdvice()); + pf.addAdvice(cta); + Person p = (Person) createAopProxy(pf).getProxy(); + + p.echo(null); + assertThat(cta.getCalls()).isEqualTo(0); + try { + p.echo(new IOException()); + } + catch (IOException ex) { + /* expected */ + } + assertThat(cta.getCalls()).isEqualTo(1); + + // Will throw exception if it fails + Person p2 = SerializationTestUtils.serializeAndDeserialize(p); + assertThat(p2).isNotSameAs(p); + assertThat(p2.getName()).isEqualTo(p.getName()); + assertThat(p2.getAge()).isEqualTo(p.getAge()); + assertThat(AopUtils.isAopProxy(p2)).as("Deserialized object is an AOP proxy").isTrue(); + + Advised a1 = (Advised) p; + Advised a2 = (Advised) p2; + // Check we can manipulate state of p2 + assertThat(a2.getAdvisors().length).isEqualTo(a1.getAdvisors().length); + + // This should work as SerializablePerson is equal + assertThat(p2).as("Proxies should be equal, even after one was serialized").isEqualTo(p); + assertThat(p).as("Proxies should be equal, even after one was serialized").isEqualTo(p2); + + // Check we can add a new advisor to the target + NopInterceptor ni = new NopInterceptor(); + p2.getAge(); + assertThat(ni.getCount()).isEqualTo(0); + a2.addAdvice(ni); + p2.getAge(); + assertThat(ni.getCount()).isEqualTo(1); + + cta = (CountingThrowsAdvice) a2.getAdvisors()[3].getAdvice(); + p2.echo(null); + assertThat(cta.getCalls()).isEqualTo(1); + try { + p2.echo(new IOException()); + } + catch (IOException ex) { + + } + assertThat(cta.getCalls()).isEqualTo(2); + } + + /** + * Check that the two MethodInvocations necessary are independent and + * don't conflict. + * Check also proxy exposure. + */ + @Test + public void testOneAdvisedObjectCallsAnother() { + int age1 = 33; + int age2 = 37; + + TestBean target1 = new TestBean(); + ProxyFactory pf1 = new ProxyFactory(target1); + // Permit proxy and invocation checkers to get context from AopContext + pf1.setExposeProxy(true); + NopInterceptor di1 = new NopInterceptor(); + pf1.addAdvice(0, di1); + pf1.addAdvice(1, new ProxyMatcherInterceptor()); + pf1.addAdvice(2, new CheckMethodInvocationIsSameInAndOutInterceptor()); + pf1.addAdvice(1, new CheckMethodInvocationViaThreadLocalIsSameInAndOutInterceptor()); + // Must be first + pf1.addAdvice(0, ExposeInvocationInterceptor.INSTANCE); + ITestBean advised1 = (ITestBean) pf1.getProxy(); + advised1.setAge(age1); // = 1 invocation + + TestBean target2 = new TestBean(); + ProxyFactory pf2 = new ProxyFactory(target2); + pf2.setExposeProxy(true); + NopInterceptor di2 = new NopInterceptor(); + pf2.addAdvice(0, di2); + pf2.addAdvice(1, new ProxyMatcherInterceptor()); + pf2.addAdvice(2, new CheckMethodInvocationIsSameInAndOutInterceptor()); + pf2.addAdvice(1, new CheckMethodInvocationViaThreadLocalIsSameInAndOutInterceptor()); + pf2.addAdvice(0, ExposeInvocationInterceptor.INSTANCE); + ITestBean advised2 = (ITestBean) createProxy(pf2); + advised2.setAge(age2); + advised1.setSpouse(advised2); // = 2 invocations + + // = 3 invocations + assertThat(advised1.getAge()).as("Advised one has correct age").isEqualTo(age1); + assertThat(advised2.getAge()).as("Advised two has correct age").isEqualTo(age2); + // Means extra call on advised 2 + // = 4 invocations on 1 and another one on 2 + assertThat(advised1.getSpouse().getAge()).as("Advised one spouse has correct age").isEqualTo(age2); + + assertThat(di1.getCount()).as("one was invoked correct number of times").isEqualTo(4); + // Got hit by call to advised1.getSpouse().getAge() + assertThat(di2.getCount()).as("one was invoked correct number of times").isEqualTo(3); + } + + + @Test + public void testReentrance() { + int age1 = 33; + + TestBean target1 = new TestBean(); + ProxyFactory pf1 = new ProxyFactory(target1); + NopInterceptor di1 = new NopInterceptor(); + pf1.addAdvice(0, di1); + ITestBean advised1 = (ITestBean) createProxy(pf1); + advised1.setAge(age1); // = 1 invocation + advised1.setSpouse(advised1); // = 2 invocations + + assertThat(di1.getCount()).as("one was invoked correct number of times").isEqualTo(2); + + // = 3 invocations + assertThat(advised1.getAge()).as("Advised one has correct age").isEqualTo(age1); + assertThat(di1.getCount()).as("one was invoked correct number of times").isEqualTo(3); + + // = 5 invocations, as reentrant call to spouse is advised also + assertThat(advised1.getSpouse().getAge()).as("Advised spouse has correct age").isEqualTo(age1); + + assertThat(di1.getCount()).as("one was invoked correct number of times").isEqualTo(5); + } + + @Test + public void testTargetCanGetProxy() { + NopInterceptor di = new NopInterceptor(); + INeedsToSeeProxy target = new TargetChecker(); + ProxyFactory proxyFactory = new ProxyFactory(target); + proxyFactory.setExposeProxy(true); + assertThat(proxyFactory.isExposeProxy()).isTrue(); + + proxyFactory.addAdvice(0, di); + INeedsToSeeProxy proxied = (INeedsToSeeProxy) createProxy(proxyFactory); + assertThat(di.getCount()).isEqualTo(0); + assertThat(target.getCount()).isEqualTo(0); + proxied.incrementViaThis(); + assertThat(target.getCount()).as("Increment happened").isEqualTo(1); + + assertThat(di.getCount()).as("Only one invocation via AOP as use of this wasn't proxied").isEqualTo(1); + // 1 invocation + assertThat(proxied.getCount()).as("Increment happened").isEqualTo(1); + proxied.incrementViaProxy(); // 2 invocations + assertThat(target.getCount()).as("Increment happened").isEqualTo(2); + assertThat(di.getCount()).as("3 more invocations via AOP as the first call was reentrant through the proxy").isEqualTo(4); + } + + @Test + // Should fail to get proxy as exposeProxy wasn't set to true + public void testTargetCantGetProxyByDefault() { + NeedsToSeeProxy et = new NeedsToSeeProxy(); + ProxyFactory pf1 = new ProxyFactory(et); + assertThat(pf1.isExposeProxy()).isFalse(); + INeedsToSeeProxy proxied = (INeedsToSeeProxy) createProxy(pf1); + assertThatIllegalStateException().isThrownBy(() -> + proxied.incrementViaProxy()); + } + + @Test + public void testContext() throws Throwable { + testContext(true); + } + + @Test + public void testNoContext() throws Throwable { + testContext(false); + } + + /** + * @param context if true, want context + */ + private void testContext(final boolean context) throws Throwable { + final String s = "foo"; + // Test return value + MethodInterceptor mi = new MethodInterceptor() { + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + if (!context) { + assertNoInvocationContext(); + } + else { + assertThat(ExposeInvocationInterceptor.currentInvocation()).as("have context").isNotNull(); + } + return s; + } + }; + AdvisedSupport pc = new AdvisedSupport(ITestBean.class); + if (context) { + pc.addAdvice(ExposeInvocationInterceptor.INSTANCE); + } + pc.addAdvice(mi); + // Keep CGLIB happy + if (requiresTarget()) { + pc.setTarget(new TestBean()); + } + AopProxy aop = createAopProxy(pc); + + assertNoInvocationContext(); + ITestBean tb = (ITestBean) aop.getProxy(); + assertNoInvocationContext(); + assertThat(tb.getName()).as("correct return value").isSameAs(s); + } + + /** + * Test that the proxy returns itself when the + * target returns {@code this} + */ + @Test + public void testTargetReturnsThis() throws Throwable { + // Test return value + TestBean raw = new OwnSpouse(); + + ProxyCreatorSupport pc = new ProxyCreatorSupport(); + pc.setInterfaces(ITestBean.class); + pc.setTarget(raw); + + ITestBean tb = (ITestBean) createProxy(pc); + assertThat(tb.getSpouse()).as("this return is wrapped in proxy").isSameAs(tb); + } + + @Test + public void testDeclaredException() throws Throwable { + final Exception expectedException = new Exception(); + // Test return value + MethodInterceptor mi = new MethodInterceptor() { + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + throw expectedException; + } + }; + AdvisedSupport pc = new AdvisedSupport(ITestBean.class); + pc.addAdvice(ExposeInvocationInterceptor.INSTANCE); + pc.addAdvice(mi); + + // We don't care about the object + mockTargetSource.setTarget(new TestBean()); + pc.setTargetSource(mockTargetSource); + AopProxy aop = createAopProxy(pc); + + assertThatExceptionOfType(Exception.class).isThrownBy(() -> { + ITestBean tb = (ITestBean) aop.getProxy(); + // Note: exception param below isn't used + tb.exceptional(expectedException); + }).matches(expectedException::equals); + } + + /** + * An interceptor throws a checked exception not on the method signature. + * For efficiency, we don't bother unifying java.lang.reflect and + * org.springframework.cglib UndeclaredThrowableException + */ + @Test + public void testUndeclaredCheckedException() throws Throwable { + final Exception unexpectedException = new Exception(); + // Test return value + MethodInterceptor mi = new MethodInterceptor() { + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + throw unexpectedException; + } + }; + AdvisedSupport pc = new AdvisedSupport(ITestBean.class); + pc.addAdvice(ExposeInvocationInterceptor.INSTANCE); + pc.addAdvice(mi); + + // We don't care about the object + pc.setTarget(new TestBean()); + AopProxy aop = createAopProxy(pc); + ITestBean tb = (ITestBean) aop.getProxy(); + + assertThatExceptionOfType(UndeclaredThrowableException.class).isThrownBy( + tb::getAge) + .satisfies(ex -> assertThat(ex.getUndeclaredThrowable()).isEqualTo(unexpectedException)); + } + + @Test + public void testUndeclaredUncheckedException() throws Throwable { + final RuntimeException unexpectedException = new RuntimeException(); + // Test return value + MethodInterceptor mi = new MethodInterceptor() { + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + throw unexpectedException; + } + }; + AdvisedSupport pc = new AdvisedSupport(ITestBean.class); + pc.addAdvice(ExposeInvocationInterceptor.INSTANCE); + pc.addAdvice(mi); + + // We don't care about the object + pc.setTarget(new TestBean()); + AopProxy aop = createAopProxy(pc); + ITestBean tb = (ITestBean) aop.getProxy(); + + assertThatExceptionOfType(RuntimeException.class).isThrownBy( + tb::getAge) + .matches(unexpectedException::equals); + } + + /** + * Check that although a method is eligible for advice chain optimization and + * direct reflective invocation, it doesn't happen if we've asked to see the proxy, + * so as to guarantee a consistent programming model. + */ + @Test + public void testTargetCanGetInvocationEvenIfNoAdviceChain() throws Throwable { + NeedsToSeeProxy target = new NeedsToSeeProxy(); + AdvisedSupport pc = new AdvisedSupport(INeedsToSeeProxy.class); + pc.setTarget(target); + pc.setExposeProxy(true); + + // Now let's try it with the special target + AopProxy aop = createAopProxy(pc); + INeedsToSeeProxy proxied = (INeedsToSeeProxy) aop.getProxy(); + // It will complain if it can't get the proxy + proxied.incrementViaProxy(); + } + + @Test + public void testTargetCanGetInvocation() throws Throwable { + final InvocationCheckExposedInvocationTestBean expectedTarget = new InvocationCheckExposedInvocationTestBean(); + + AdvisedSupport pc = new AdvisedSupport(ITestBean.class, IOther.class); + pc.addAdvice(ExposeInvocationInterceptor.INSTANCE); + TrapTargetInterceptor tii = new TrapTargetInterceptor() { + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + // Assert that target matches BEFORE invocation returns + assertThat(invocation.getThis()).as("Target is correct").isEqualTo(expectedTarget); + return super.invoke(invocation); + } + }; + pc.addAdvice(tii); + pc.setTarget(expectedTarget); + AopProxy aop = createAopProxy(pc); + + ITestBean tb = (ITestBean) aop.getProxy(); + tb.getName(); + } + + /** + * Throw an exception if there is an Invocation. + */ + private void assertNoInvocationContext() { + assertThatIllegalStateException().isThrownBy( + ExposeInvocationInterceptor::currentInvocation); + } + + /** + * Test stateful interceptor + */ + @Test + public void testMixinWithIntroductionAdvisor() throws Throwable { + TestBean tb = new TestBean(); + ProxyFactory pc = new ProxyFactory(); + pc.addInterface(ITestBean.class); + pc.addAdvisor(new LockMixinAdvisor()); + pc.setTarget(tb); + + testTestBeanIntroduction(pc); + } + + @Test + public void testMixinWithIntroductionInfo() throws Throwable { + TestBean tb = new TestBean(); + ProxyFactory pc = new ProxyFactory(); + pc.addInterface(ITestBean.class); + // We don't use an IntroductionAdvisor, we can just add an advice that implements IntroductionInfo + pc.addAdvice(new LockMixin()); + pc.setTarget(tb); + + testTestBeanIntroduction(pc); + } + + private void testTestBeanIntroduction(ProxyFactory pc) { + int newAge = 65; + ITestBean itb = (ITestBean) createProxy(pc); + itb.setAge(newAge); + assertThat(itb.getAge()).isEqualTo(newAge); + + Lockable lockable = (Lockable) itb; + assertThat(lockable.locked()).isFalse(); + lockable.lock(); + + assertThat(itb.getAge()).isEqualTo(newAge); + assertThatExceptionOfType(LockedException.class).isThrownBy(() -> + itb.setAge(1)); + assertThat(itb.getAge()).isEqualTo(newAge); + + // Unlock + assertThat(lockable.locked()).isTrue(); + lockable.unlock(); + itb.setAge(1); + assertThat(itb.getAge()).isEqualTo(1); + } + + @Test + public void testReplaceArgument() throws Throwable { + TestBean tb = new TestBean(); + ProxyFactory pc = new ProxyFactory(); + pc.addInterface(ITestBean.class); + pc.setTarget(tb); + pc.addAdvisor(new StringSetterNullReplacementAdvice()); + + ITestBean t = (ITestBean) pc.getProxy(); + int newAge = 5; + t.setAge(newAge); + assertThat(t.getAge()).isEqualTo(newAge); + String newName = "greg"; + t.setName(newName); + assertThat(t.getName()).isEqualTo(newName); + + t.setName(null); + // Null replacement magic should work + assertThat(t.getName()).isEqualTo(""); + } + + @Test + public void testCanCastProxyToProxyConfig() throws Throwable { + TestBean tb = new TestBean(); + ProxyFactory pc = new ProxyFactory(tb); + NopInterceptor di = new NopInterceptor(); + pc.addAdvice(0, di); + + ITestBean t = (ITestBean) createProxy(pc); + assertThat(di.getCount()).isEqualTo(0); + t.setAge(23); + assertThat(t.getAge()).isEqualTo(23); + assertThat(di.getCount()).isEqualTo(2); + + Advised advised = (Advised) t; + assertThat(advised.getAdvisors().length).as("Have 1 advisor").isEqualTo(1); + assertThat(advised.getAdvisors()[0].getAdvice()).isEqualTo(di); + NopInterceptor di2 = new NopInterceptor(); + advised.addAdvice(1, di2); + t.getName(); + assertThat(di.getCount()).isEqualTo(3); + assertThat(di2.getCount()).isEqualTo(1); + // will remove di + advised.removeAdvisor(0); + t.getAge(); + // Unchanged + assertThat(di.getCount()).isEqualTo(3); + assertThat(di2.getCount()).isEqualTo(2); + + CountingBeforeAdvice cba = new CountingBeforeAdvice(); + assertThat(cba.getCalls()).isEqualTo(0); + advised.addAdvice(cba); + t.setAge(16); + assertThat(t.getAge()).isEqualTo(16); + assertThat(cba.getCalls()).isEqualTo(2); + } + + @Test + public void testAdviceImplementsIntroductionInfo() throws Throwable { + TestBean tb = new TestBean(); + String name = "tony"; + tb.setName(name); + ProxyFactory pc = new ProxyFactory(tb); + NopInterceptor di = new NopInterceptor(); + pc.addAdvice(di); + final long ts = 37; + pc.addAdvice(new DelegatingIntroductionInterceptor(new TimeStamped() { + @Override + public long getTimeStamp() { + return ts; + } + })); + + ITestBean proxied = (ITestBean) createProxy(pc); + assertThat(proxied.getName()).isEqualTo(name); + TimeStamped intro = (TimeStamped) proxied; + assertThat(intro.getTimeStamp()).isEqualTo(ts); + } + + @Test + public void testCannotAddDynamicIntroductionAdviceExceptInIntroductionAdvice() throws Throwable { + TestBean target = new TestBean(); + target.setAge(21); + ProxyFactory pc = new ProxyFactory(target); + assertThatExceptionOfType(AopConfigException.class).isThrownBy(() -> + pc.addAdvice(new DummyIntroductionAdviceImpl())) + .withMessageContaining("ntroduction"); + // Check it still works: proxy factory state shouldn't have been corrupted + ITestBean proxied = (ITestBean) createProxy(pc); + assertThat(proxied.getAge()).isEqualTo(target.getAge()); + } + + @Test + public void testRejectsBogusDynamicIntroductionAdviceWithNoAdapter() throws Throwable { + TestBean target = new TestBean(); + target.setAge(21); + ProxyFactory pc = new ProxyFactory(target); + pc.addAdvisor(new DefaultIntroductionAdvisor(new DummyIntroductionAdviceImpl(), Comparable.class)); + assertThatExceptionOfType(Exception.class).isThrownBy(() -> { + // TODO May fail on either call: may want to tighten up definition + ITestBean proxied = (ITestBean) createProxy(pc); + proxied.getName(); + }); + // TODO used to catch UnknownAdviceTypeException, but + // with CGLIB some errors are in proxy creation and are wrapped + // in aspect exception. Error message is still fine. + //assertTrue(ex.getMessage().indexOf("ntroduction") > -1); + } + + /** + * Check that the introduction advice isn't allowed to introduce interfaces + * that are unsupported by the IntroductionInterceptor. + */ + @Test + public void testCannotAddIntroductionAdviceWithUnimplementedInterface() throws Throwable { + TestBean target = new TestBean(); + target.setAge(21); + ProxyFactory pc = new ProxyFactory(target); + assertThatIllegalArgumentException().isThrownBy(() -> + pc.addAdvisor(0, new DefaultIntroductionAdvisor(new TimestampIntroductionInterceptor(), ITestBean.class))); + // Check it still works: proxy factory state shouldn't have been corrupted + ITestBean proxied = (ITestBean) createProxy(pc); + assertThat(proxied.getAge()).isEqualTo(target.getAge()); + } + + /** + * Note that an introduction can't throw an unexpected checked exception, + * as it's constrained by the interface. + */ + @Test + public void testIntroductionThrowsUncheckedException() throws Throwable { + TestBean target = new TestBean(); + target.setAge(21); + ProxyFactory pc = new ProxyFactory(target); + + @SuppressWarnings("serial") + class MyDi extends DelegatingIntroductionInterceptor implements TimeStamped { + /** + * @see org.springframework.core.testfixture.TimeStamped#getTimeStamp() + */ + @Override + public long getTimeStamp() { + throw new UnsupportedOperationException(); + } + } + pc.addAdvisor(new DefaultIntroductionAdvisor(new MyDi())); + + TimeStamped ts = (TimeStamped) createProxy(pc); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy( + ts::getTimeStamp); + } + + /** + * Should only be able to introduce interfaces, not classes. + */ + @Test + public void testCannotAddIntroductionAdviceToIntroduceClass() throws Throwable { + TestBean target = new TestBean(); + target.setAge(21); + ProxyFactory pc = new ProxyFactory(target); + assertThatIllegalArgumentException().as("Shouldn't be able to add introduction advice that introduces a class, rather than an interface").isThrownBy(() -> + pc.addAdvisor(0, new DefaultIntroductionAdvisor(new TimestampIntroductionInterceptor(), TestBean.class))) + .withMessageContaining("interface"); + // Check it still works: proxy factory state shouldn't have been corrupted + ITestBean proxied = (ITestBean) createProxy(pc); + assertThat(proxied.getAge()).isEqualTo(target.getAge()); + } + + @Test + public void testCannotAddInterceptorWhenFrozen() throws Throwable { + TestBean target = new TestBean(); + target.setAge(21); + ProxyFactory pc = new ProxyFactory(target); + assertThat(pc.isFrozen()).isFalse(); + pc.addAdvice(new NopInterceptor()); + ITestBean proxied = (ITestBean) createProxy(pc); + pc.setFrozen(true); + assertThatExceptionOfType(AopConfigException.class).as("Shouldn't be able to add interceptor when frozen").isThrownBy(() -> + pc.addAdvice(0, new NopInterceptor())) + .withMessageContaining("frozen"); + // Check it still works: proxy factory state shouldn't have been corrupted + assertThat(proxied.getAge()).isEqualTo(target.getAge()); + assertThat(((Advised) proxied).getAdvisors().length).isEqualTo(1); + } + + /** + * Check that casting to Advised can't get around advice freeze. + */ + @Test + public void testCannotAddAdvisorWhenFrozenUsingCast() throws Throwable { + TestBean target = new TestBean(); + target.setAge(21); + ProxyFactory pc = new ProxyFactory(target); + assertThat(pc.isFrozen()).isFalse(); + pc.addAdvice(new NopInterceptor()); + ITestBean proxied = (ITestBean) createProxy(pc); + pc.setFrozen(true); + Advised advised = (Advised) proxied; + + assertThat(pc.isFrozen()).isTrue(); + assertThatExceptionOfType(AopConfigException.class).as("Shouldn't be able to add Advisor when frozen").isThrownBy(() -> + advised.addAdvisor(new DefaultPointcutAdvisor(new NopInterceptor()))) + .withMessageContaining("frozen"); + // Check it still works: proxy factory state shouldn't have been corrupted + assertThat(proxied.getAge()).isEqualTo(target.getAge()); + assertThat(advised.getAdvisors().length).isEqualTo(1); + } + + @Test + public void testCannotRemoveAdvisorWhenFrozen() throws Throwable { + TestBean target = new TestBean(); + target.setAge(21); + ProxyFactory pc = new ProxyFactory(target); + assertThat(pc.isFrozen()).isFalse(); + pc.addAdvice(new NopInterceptor()); + ITestBean proxied = (ITestBean) createProxy(pc); + pc.setFrozen(true); + Advised advised = (Advised) proxied; + + assertThat(pc.isFrozen()).isTrue(); + assertThatExceptionOfType(AopConfigException.class).as("Shouldn't be able to remove Advisor when frozen").isThrownBy(() -> + advised.removeAdvisor(0)) + .withMessageContaining("frozen"); + // Didn't get removed + assertThat(advised.getAdvisors().length).isEqualTo(1); + pc.setFrozen(false); + // Can now remove it + advised.removeAdvisor(0); + // Check it still works: proxy factory state shouldn't have been corrupted + assertThat(proxied.getAge()).isEqualTo(target.getAge()); + assertThat(advised.getAdvisors().length).isEqualTo(0); + } + + @Test + public void testUseAsHashKey() { + TestBean target1 = new TestBean(); + ProxyFactory pf1 = new ProxyFactory(target1); + pf1.addAdvice(new NopInterceptor()); + ITestBean proxy1 = (ITestBean) createProxy(pf1); + + TestBean target2 = new TestBean(); + ProxyFactory pf2 = new ProxyFactory(target2); + pf2.addAdvisor(new DefaultIntroductionAdvisor(new TimestampIntroductionInterceptor())); + ITestBean proxy2 = (ITestBean) createProxy(pf2); + + HashMap h = new HashMap<>(); + Object value1 = "foo"; + Object value2 = "bar"; + assertThat(h.get(proxy1)).isNull(); + h.put(proxy1, value1); + h.put(proxy2, value2); + assertThat(value1).isEqualTo(h.get(proxy1)); + assertThat(value2).isEqualTo(h.get(proxy2)); + } + + /** + * Check that the string is informative. + */ + @Test + public void testProxyConfigString() { + TestBean target = new TestBean(); + ProxyFactory pc = new ProxyFactory(target); + pc.setInterfaces(ITestBean.class); + pc.addAdvice(new NopInterceptor()); + MethodBeforeAdvice mba = new CountingBeforeAdvice(); + Advisor advisor = new DefaultPointcutAdvisor(new NameMatchMethodPointcut(), mba); + pc.addAdvisor(advisor); + ITestBean proxied = (ITestBean) createProxy(pc); + + String proxyConfigString = ((Advised) proxied).toProxyConfigString(); + assertThat(proxyConfigString.contains(advisor.toString())).isTrue(); + assertThat(proxyConfigString.contains("1 interface")).isTrue(); + } + + @Test + public void testCanPreventCastToAdvisedUsingOpaque() { + TestBean target = new TestBean(); + ProxyFactory pc = new ProxyFactory(target); + pc.setInterfaces(ITestBean.class); + pc.addAdvice(new NopInterceptor()); + CountingBeforeAdvice mba = new CountingBeforeAdvice(); + Advisor advisor = new DefaultPointcutAdvisor(new NameMatchMethodPointcut().addMethodName("setAge"), mba); + pc.addAdvisor(advisor); + assertThat(pc.isOpaque()).as("Opaque defaults to false").isFalse(); + pc.setOpaque(true); + assertThat(pc.isOpaque()).as("Opaque now true for this config").isTrue(); + ITestBean proxied = (ITestBean) createProxy(pc); + proxied.setAge(10); + assertThat(proxied.getAge()).isEqualTo(10); + assertThat(mba.getCalls()).isEqualTo(1); + + boolean condition = proxied instanceof Advised; + assertThat(condition).as("Cannot be cast to Advised").isFalse(); + } + + @Test + public void testAdviceSupportListeners() throws Throwable { + TestBean target = new TestBean(); + target.setAge(21); + + ProxyFactory pc = new ProxyFactory(target); + CountingAdvisorListener l = new CountingAdvisorListener(pc); + pc.addListener(l); + RefreshCountingAdvisorChainFactory acf = new RefreshCountingAdvisorChainFactory(); + // Should be automatically added as a listener + pc.addListener(acf); + assertThat(pc.isActive()).isFalse(); + assertThat(l.activates).isEqualTo(0); + assertThat(acf.refreshes).isEqualTo(0); + ITestBean proxied = (ITestBean) createProxy(pc); + assertThat(acf.refreshes).isEqualTo(1); + assertThat(l.activates).isEqualTo(1); + assertThat(pc.isActive()).isTrue(); + assertThat(proxied.getAge()).isEqualTo(target.getAge()); + assertThat(l.adviceChanges).isEqualTo(0); + NopInterceptor di = new NopInterceptor(); + pc.addAdvice(0, di); + assertThat(l.adviceChanges).isEqualTo(1); + assertThat(acf.refreshes).isEqualTo(2); + assertThat(proxied.getAge()).isEqualTo(target.getAge()); + pc.removeAdvice(di); + assertThat(l.adviceChanges).isEqualTo(2); + assertThat(acf.refreshes).isEqualTo(3); + assertThat(proxied.getAge()).isEqualTo(target.getAge()); + pc.getProxy(); + assertThat(l.activates).isEqualTo(1); + + pc.removeListener(l); + assertThat(l.adviceChanges).isEqualTo(2); + pc.addAdvisor(new DefaultPointcutAdvisor(new NopInterceptor())); + // No longer counting + assertThat(l.adviceChanges).isEqualTo(2); + } + + @Test + public void testExistingProxyChangesTarget() throws Throwable { + TestBean tb1 = new TestBean(); + tb1.setAge(33); + + TestBean tb2 = new TestBean(); + tb2.setAge(26); + tb2.setName("Juergen"); + TestBean tb3 = new TestBean(); + tb3.setAge(37); + ProxyFactory pc = new ProxyFactory(tb1); + NopInterceptor nop = new NopInterceptor(); + pc.addAdvice(nop); + ITestBean proxy = (ITestBean) createProxy(pc); + assertThat(0).isEqualTo(nop.getCount()); + assertThat(proxy.getAge()).isEqualTo(tb1.getAge()); + assertThat(1).isEqualTo(nop.getCount()); + // Change to a new static target + pc.setTarget(tb2); + assertThat(proxy.getAge()).isEqualTo(tb2.getAge()); + assertThat(2).isEqualTo(nop.getCount()); + + // Change to a new dynamic target + HotSwappableTargetSource hts = new HotSwappableTargetSource(tb3); + pc.setTargetSource(hts); + assertThat(proxy.getAge()).isEqualTo(tb3.getAge()); + assertThat(3).isEqualTo(nop.getCount()); + hts.swap(tb1); + assertThat(proxy.getAge()).isEqualTo(tb1.getAge()); + tb1.setName("Colin"); + assertThat(proxy.getName()).isEqualTo(tb1.getName()); + assertThat(5).isEqualTo(nop.getCount()); + + // Change back, relying on casting to Advised + Advised advised = (Advised) proxy; + assertThat(advised.getTargetSource()).isSameAs(hts); + SingletonTargetSource sts = new SingletonTargetSource(tb2); + advised.setTargetSource(sts); + assertThat(proxy.getName()).isEqualTo(tb2.getName()); + assertThat(advised.getTargetSource()).isSameAs(sts); + assertThat(proxy.getAge()).isEqualTo(tb2.getAge()); + } + + @Test + public void testDynamicMethodPointcutThatAlwaysAppliesStatically() throws Throwable { + TestBean tb = new TestBean(); + ProxyFactory pc = new ProxyFactory(); + pc.addInterface(ITestBean.class); + TestDynamicPointcutAdvice dp = new TestDynamicPointcutAdvice(new NopInterceptor(), "getAge"); + pc.addAdvisor(dp); + pc.setTarget(tb); + ITestBean it = (ITestBean) createProxy(pc); + assertThat(dp.count).isEqualTo(0); + it.getAge(); + assertThat(dp.count).isEqualTo(1); + it.setAge(11); + assertThat(it.getAge()).isEqualTo(11); + assertThat(dp.count).isEqualTo(2); + } + + @Test + public void testDynamicMethodPointcutThatAppliesStaticallyOnlyToSetters() throws Throwable { + TestBean tb = new TestBean(); + ProxyFactory pc = new ProxyFactory(); + pc.addInterface(ITestBean.class); + // Could apply dynamically to getAge/setAge but not to getName + TestDynamicPointcutForSettersOnly dp = new TestDynamicPointcutForSettersOnly(new NopInterceptor(), "Age"); + pc.addAdvisor(dp); + this.mockTargetSource.setTarget(tb); + pc.setTargetSource(mockTargetSource); + ITestBean it = (ITestBean) createProxy(pc); + assertThat(dp.count).isEqualTo(0); + it.getAge(); + // Statically vetoed + assertThat(dp.count).isEqualTo(0); + it.setAge(11); + assertThat(it.getAge()).isEqualTo(11); + assertThat(dp.count).isEqualTo(1); + // Applies statically but not dynamically + it.setName("joe"); + assertThat(dp.count).isEqualTo(1); + } + + @Test + public void testStaticMethodPointcut() throws Throwable { + TestBean tb = new TestBean(); + ProxyFactory pc = new ProxyFactory(); + pc.addInterface(ITestBean.class); + NopInterceptor di = new NopInterceptor(); + TestStaticPointcutAdvice sp = new TestStaticPointcutAdvice(di, "getAge"); + pc.addAdvisor(sp); + pc.setTarget(tb); + ITestBean it = (ITestBean) createProxy(pc); + assertThat(0).isEqualTo(di.getCount()); + it.getAge(); + assertThat(1).isEqualTo(di.getCount()); + it.setAge(11); + assertThat(11).isEqualTo(it.getAge()); + assertThat(2).isEqualTo(di.getCount()); + } + + /** + * There are times when we want to call proceed() twice. + * We can do this if we clone the invocation. + */ + @Test + public void testCloneInvocationToProceedThreeTimes() throws Throwable { + TestBean tb = new TestBean(); + ProxyFactory pc = new ProxyFactory(tb); + pc.addInterface(ITestBean.class); + + MethodInterceptor twoBirthdayInterceptor = new MethodInterceptor() { + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + // Clone the invocation to proceed three times + // "The Moor's Last Sigh": this technology can cause premature aging + MethodInvocation clone1 = ((ReflectiveMethodInvocation) mi).invocableClone(); + MethodInvocation clone2 = ((ReflectiveMethodInvocation) mi).invocableClone(); + clone1.proceed(); + clone2.proceed(); + return mi.proceed(); + } + }; + @SuppressWarnings("serial") + StaticMethodMatcherPointcutAdvisor advisor = new StaticMethodMatcherPointcutAdvisor(twoBirthdayInterceptor) { + @Override + public boolean matches(Method m, @Nullable Class targetClass) { + return "haveBirthday".equals(m.getName()); + } + }; + pc.addAdvisor(advisor); + ITestBean it = (ITestBean) createProxy(pc); + + final int age = 20; + it.setAge(age); + assertThat(it.getAge()).isEqualTo(age); + // Should return the age before the third, AOP-induced birthday + assertThat(it.haveBirthday()).isEqualTo((age + 2)); + // Return the final age produced by 3 birthdays + assertThat(it.getAge()).isEqualTo((age + 3)); + } + + /** + * We want to change the arguments on a clone: it shouldn't affect the original. + */ + @Test + public void testCanChangeArgumentsIndependentlyOnClonedInvocation() throws Throwable { + TestBean tb = new TestBean(); + ProxyFactory pc = new ProxyFactory(tb); + pc.addInterface(ITestBean.class); + + /** + * Changes the name, then changes it back. + */ + MethodInterceptor nameReverter = new MethodInterceptor() { + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + MethodInvocation clone = ((ReflectiveMethodInvocation) mi).invocableClone(); + String oldName = ((ITestBean) mi.getThis()).getName(); + clone.getArguments()[0] = oldName; + // Original method invocation should be unaffected by changes to argument list of clone + mi.proceed(); + return clone.proceed(); + } + }; + + class NameSaver implements MethodInterceptor { + private List names = new ArrayList<>(); + + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + names.add(mi.getArguments()[0]); + return mi.proceed(); + } + } + + NameSaver saver = new NameSaver(); + + pc.addAdvisor(new DefaultPointcutAdvisor(Pointcuts.SETTERS, nameReverter)); + pc.addAdvisor(new DefaultPointcutAdvisor(Pointcuts.SETTERS, saver)); + ITestBean it = (ITestBean) createProxy(pc); + + String name1 = "tony"; + String name2 = "gordon"; + + tb.setName(name1); + assertThat(tb.getName()).isEqualTo(name1); + + it.setName(name2); + // NameReverter saved it back + assertThat(it.getName()).isEqualTo(name1); + assertThat(saver.names.size()).isEqualTo(2); + assertThat(saver.names.get(0)).isEqualTo(name2); + assertThat(saver.names.get(1)).isEqualTo(name1); + } + + @SuppressWarnings("serial") + @Test + public void testOverloadedMethodsWithDifferentAdvice() throws Throwable { + Overloads target = new Overloads(); + ProxyFactory pc = new ProxyFactory(target); + + NopInterceptor overLoadVoids = new NopInterceptor(); + pc.addAdvisor(new StaticMethodMatcherPointcutAdvisor(overLoadVoids) { + @Override + public boolean matches(Method m, @Nullable Class targetClass) { + return m.getName().equals("overload") && m.getParameterCount() == 0; + } + }); + + NopInterceptor overLoadInts = new NopInterceptor(); + pc.addAdvisor(new StaticMethodMatcherPointcutAdvisor(overLoadInts) { + @Override + public boolean matches(Method m, @Nullable Class targetClass) { + return m.getName().equals("overload") && m.getParameterCount() == 1 && + m.getParameterTypes()[0].equals(int.class); + } + }); + + IOverloads proxy = (IOverloads) createProxy(pc); + assertThat(overLoadInts.getCount()).isEqualTo(0); + assertThat(overLoadVoids.getCount()).isEqualTo(0); + proxy.overload(); + assertThat(overLoadInts.getCount()).isEqualTo(0); + assertThat(overLoadVoids.getCount()).isEqualTo(1); + assertThat(proxy.overload(25)).isEqualTo(25); + assertThat(overLoadInts.getCount()).isEqualTo(1); + assertThat(overLoadVoids.getCount()).isEqualTo(1); + proxy.noAdvice(); + assertThat(overLoadInts.getCount()).isEqualTo(1); + assertThat(overLoadVoids.getCount()).isEqualTo(1); + } + + @Test + public void testProxyIsBoundBeforeTargetSourceInvoked() { + final TestBean target = new TestBean(); + ProxyFactory pf = new ProxyFactory(target); + pf.addAdvice(new DebugInterceptor()); + pf.setExposeProxy(true); + final ITestBean proxy = (ITestBean) createProxy(pf); + Advised config = (Advised) proxy; + + // This class just checks proxy is bound before getTarget() call + config.setTargetSource(new TargetSource() { + @Override + public Class getTargetClass() { + return TestBean.class; + } + @Override + public boolean isStatic() { + return false; + } + @Override + public Object getTarget() throws Exception { + assertThat(AopContext.currentProxy()).isEqualTo(proxy); + return target; + } + @Override + public void releaseTarget(Object target) throws Exception { + } + }); + + // Just test anything: it will fail if context wasn't found + assertThat(proxy.getAge()).isEqualTo(0); + } + + @Test + public void testEquals() { + IOther a = new AllInstancesAreEqual(); + IOther b = new AllInstancesAreEqual(); + NopInterceptor i1 = new NopInterceptor(); + NopInterceptor i2 = new NopInterceptor(); + ProxyFactory pfa = new ProxyFactory(a); + pfa.addAdvice(i1); + ProxyFactory pfb = new ProxyFactory(b); + pfb.addAdvice(i2); + IOther proxyA = (IOther) createProxy(pfa); + IOther proxyB = (IOther) createProxy(pfb); + + assertThat(pfb.getAdvisors().length).isEqualTo(pfa.getAdvisors().length); + assertThat(b).isEqualTo(a); + assertThat(i2).isEqualTo(i1); + assertThat(proxyB).isEqualTo(proxyA); + assertThat(proxyB.hashCode()).isEqualTo(proxyA.hashCode()); + assertThat(proxyA.equals(a)).isFalse(); + + // Equality checks were handled by the proxy + assertThat(i1.getCount()).isEqualTo(0); + + // When we invoke A, it's NopInterceptor will have count == 1 + // and won't think it's equal to B's NopInterceptor + proxyA.absquatulate(); + assertThat(i1.getCount()).isEqualTo(1); + assertThat(proxyA.equals(proxyB)).isFalse(); + } + + @Test + public void testBeforeAdvisorIsInvoked() { + CountingBeforeAdvice cba = new CountingBeforeAdvice(); + @SuppressWarnings("serial") + Advisor matchesNoArgs = new StaticMethodMatcherPointcutAdvisor(cba) { + @Override + public boolean matches(Method m, @Nullable Class targetClass) { + return m.getParameterCount() == 0; + } + }; + TestBean target = new TestBean(); + target.setAge(80); + ProxyFactory pf = new ProxyFactory(target); + pf.addAdvice(new NopInterceptor()); + pf.addAdvisor(matchesNoArgs); + assertThat(pf.getAdvisors()[1]).as("Advisor was added").isEqualTo(matchesNoArgs); + ITestBean proxied = (ITestBean) createProxy(pf); + assertThat(cba.getCalls()).isEqualTo(0); + assertThat(cba.getCalls("getAge")).isEqualTo(0); + assertThat(proxied.getAge()).isEqualTo(target.getAge()); + assertThat(cba.getCalls()).isEqualTo(1); + assertThat(cba.getCalls("getAge")).isEqualTo(1); + assertThat(cba.getCalls("setAge")).isEqualTo(0); + // Won't be advised + proxied.setAge(26); + assertThat(cba.getCalls()).isEqualTo(1); + assertThat(proxied.getAge()).isEqualTo(26); + } + + @Test + public void testUserAttributes() throws Throwable { + class MapAwareMethodInterceptor implements MethodInterceptor { + private final Map expectedValues; + private final Map valuesToAdd; + public MapAwareMethodInterceptor(Map expectedValues, Map valuesToAdd) { + this.expectedValues = expectedValues; + this.valuesToAdd = valuesToAdd; + } + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + ReflectiveMethodInvocation rmi = (ReflectiveMethodInvocation) invocation; + for (Iterator it = rmi.getUserAttributes().keySet().iterator(); it.hasNext(); ){ + Object key = it.next(); + assertThat(rmi.getUserAttributes().get(key)).isEqualTo(expectedValues.get(key)); + } + rmi.getUserAttributes().putAll(valuesToAdd); + return invocation.proceed(); + } + } + AdvisedSupport pc = new AdvisedSupport(ITestBean.class); + MapAwareMethodInterceptor mami1 = new MapAwareMethodInterceptor(new HashMap<>(), new HashMap()); + Map firstValuesToAdd = new HashMap<>(); + firstValuesToAdd.put("test", ""); + MapAwareMethodInterceptor mami2 = new MapAwareMethodInterceptor(new HashMap<>(), firstValuesToAdd); + MapAwareMethodInterceptor mami3 = new MapAwareMethodInterceptor(firstValuesToAdd, new HashMap<>()); + MapAwareMethodInterceptor mami4 = new MapAwareMethodInterceptor(firstValuesToAdd, new HashMap<>()); + Map secondValuesToAdd = new HashMap<>(); + secondValuesToAdd.put("foo", "bar"); + secondValuesToAdd.put("cat", "dog"); + MapAwareMethodInterceptor mami5 = new MapAwareMethodInterceptor(firstValuesToAdd, secondValuesToAdd); + Map finalExpected = new HashMap<>(firstValuesToAdd); + finalExpected.putAll(secondValuesToAdd); + MapAwareMethodInterceptor mami6 = new MapAwareMethodInterceptor(finalExpected, secondValuesToAdd); + + pc.addAdvice(mami1); + pc.addAdvice(mami2); + pc.addAdvice(mami3); + pc.addAdvice(mami4); + pc.addAdvice(mami5); + pc.addAdvice(mami6); + + // We don't care about the object + pc.setTarget(new TestBean()); + AopProxy aop = createAopProxy(pc); + ITestBean tb = (ITestBean) aop.getProxy(); + + String newName = "foo"; + tb.setName(newName); + assertThat(tb.getName()).isEqualTo(newName); + } + + @Test + public void testMultiAdvice() throws Throwable { + CountingMultiAdvice cca = new CountingMultiAdvice(); + @SuppressWarnings("serial") + Advisor matchesNoArgs = new StaticMethodMatcherPointcutAdvisor(cca) { + @Override + public boolean matches(Method m, @Nullable Class targetClass) { + return m.getParameterCount() == 0 || "exceptional".equals(m.getName()); + } + }; + TestBean target = new TestBean(); + target.setAge(80); + ProxyFactory pf = new ProxyFactory(target); + pf.addAdvice(new NopInterceptor()); + pf.addAdvisor(matchesNoArgs); + assertThat(pf.getAdvisors()[1]).as("Advisor was added").isEqualTo(matchesNoArgs); + ITestBean proxied = (ITestBean) createProxy(pf); + + assertThat(cca.getCalls()).isEqualTo(0); + assertThat(cca.getCalls("getAge")).isEqualTo(0); + assertThat(proxied.getAge()).isEqualTo(target.getAge()); + assertThat(cca.getCalls()).isEqualTo(2); + assertThat(cca.getCalls("getAge")).isEqualTo(2); + assertThat(cca.getCalls("setAge")).isEqualTo(0); + // Won't be advised + proxied.setAge(26); + assertThat(cca.getCalls()).isEqualTo(2); + assertThat(proxied.getAge()).isEqualTo(26); + assertThat(cca.getCalls()).isEqualTo(4); + assertThatExceptionOfType(SpecializedUncheckedException.class).as("Should have thrown CannotGetJdbcConnectionException").isThrownBy(() -> + proxied.exceptional(new SpecializedUncheckedException("foo", (SQLException)null))); + assertThat(cca.getCalls()).isEqualTo(6); + } + + @Test + public void testBeforeAdviceThrowsException() { + final RuntimeException rex = new RuntimeException(); + @SuppressWarnings("serial") + CountingBeforeAdvice ba = new CountingBeforeAdvice() { + @Override + public void before(Method m, Object[] args, Object target) throws Throwable { + super.before(m, args, target); + if (m.getName().startsWith("set")) + throw rex; + } + }; + + TestBean target = new TestBean(); + target.setAge(80); + NopInterceptor nop1 = new NopInterceptor(); + NopInterceptor nop2 = new NopInterceptor(); + ProxyFactory pf = new ProxyFactory(target); + pf.addAdvice(nop1); + pf.addAdvice(ba); + pf.addAdvice(nop2); + ITestBean proxied = (ITestBean) createProxy(pf); + // Won't throw an exception + assertThat(proxied.getAge()).isEqualTo(target.getAge()); + assertThat(ba.getCalls()).isEqualTo(1); + assertThat(ba.getCalls("getAge")).isEqualTo(1); + assertThat(nop1.getCount()).isEqualTo(1); + assertThat(nop2.getCount()).isEqualTo(1); + // Will fail, after invoking Nop1 + assertThatExceptionOfType(RuntimeException.class).as("before advice should have ended chain").isThrownBy(() -> + proxied.setAge(26)) + .matches(rex::equals); + assertThat(ba.getCalls()).isEqualTo(2); + assertThat(nop1.getCount()).isEqualTo(2); + // Nop2 didn't get invoked when the exception was thrown + assertThat(nop2.getCount()).isEqualTo(1); + // Shouldn't have changed value in joinpoint + assertThat(proxied.getAge()).isEqualTo(target.getAge()); + } + + + @Test + public void testAfterReturningAdvisorIsInvoked() { + class SummingAfterAdvice implements AfterReturningAdvice { + public int sum; + @Override + public void afterReturning(@Nullable Object returnValue, Method m, Object[] args, @Nullable Object target) throws Throwable { + sum += ((Integer) returnValue).intValue(); + } + } + SummingAfterAdvice aa = new SummingAfterAdvice(); + @SuppressWarnings("serial") + Advisor matchesInt = new StaticMethodMatcherPointcutAdvisor(aa) { + @Override + public boolean matches(Method m, @Nullable Class targetClass) { + return m.getReturnType() == int.class; + } + }; + TestBean target = new TestBean(); + ProxyFactory pf = new ProxyFactory(target); + pf.addAdvice(new NopInterceptor()); + pf.addAdvisor(matchesInt); + assertThat(pf.getAdvisors()[1]).as("Advisor was added").isEqualTo(matchesInt); + ITestBean proxied = (ITestBean) createProxy(pf); + assertThat(aa.sum).isEqualTo(0); + int i1 = 12; + int i2 = 13; + + // Won't be advised + proxied.setAge(i1); + assertThat(proxied.getAge()).isEqualTo(i1); + assertThat(aa.sum).isEqualTo(i1); + proxied.setAge(i2); + assertThat(proxied.getAge()).isEqualTo(i2); + assertThat(aa.sum).isEqualTo((i1 + i2)); + assertThat(proxied.getAge()).isEqualTo(i2); + } + + @Test + public void testAfterReturningAdvisorIsNotInvokedOnException() { + CountingAfterReturningAdvice car = new CountingAfterReturningAdvice(); + TestBean target = new TestBean(); + ProxyFactory pf = new ProxyFactory(target); + pf.addAdvice(new NopInterceptor()); + pf.addAdvice(car); + assertThat(pf.getAdvisors()[1].getAdvice()).as("Advice was wrapped in Advisor and added").isEqualTo(car); + ITestBean proxied = (ITestBean) createProxy(pf); + assertThat(car.getCalls()).isEqualTo(0); + int age = 10; + proxied.setAge(age); + assertThat(proxied.getAge()).isEqualTo(age); + assertThat(car.getCalls()).isEqualTo(2); + Exception exc = new Exception(); + // On exception it won't be invoked + assertThatExceptionOfType(Throwable.class).isThrownBy(() -> + proxied.exceptional(exc)) + .satisfies(ex -> assertThat(ex).isSameAs(exc)); + assertThat(car.getCalls()).isEqualTo(2); + } + + + @Test + public void testThrowsAdvisorIsInvoked() throws Throwable { + // Reacts to ServletException and RemoteException + MyThrowsHandler th = new MyThrowsHandler(); + @SuppressWarnings("serial") + Advisor matchesEchoInvocations = new StaticMethodMatcherPointcutAdvisor(th) { + @Override + public boolean matches(Method m, @Nullable Class targetClass) { + return m.getName().startsWith("echo"); + } + }; + + Echo target = new Echo(); + target.setA(16); + ProxyFactory pf = new ProxyFactory(target); + pf.addAdvice(new NopInterceptor()); + pf.addAdvisor(matchesEchoInvocations); + assertThat(pf.getAdvisors()[1]).as("Advisor was added").isEqualTo(matchesEchoInvocations); + IEcho proxied = (IEcho) createProxy(pf); + assertThat(th.getCalls()).isEqualTo(0); + assertThat(proxied.getA()).isEqualTo(target.getA()); + assertThat(th.getCalls()).isEqualTo(0); + Exception ex = new Exception(); + // Will be advised but doesn't match + assertThatExceptionOfType(Exception.class).isThrownBy(() -> + proxied.echoException(1, ex)) + .matches(ex::equals); + FileNotFoundException fex = new FileNotFoundException(); + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(() -> + proxied.echoException(1, fex)) + .matches(fex::equals); + assertThat(th.getCalls("ioException")).isEqualTo(1); + } + + @Test + public void testAddThrowsAdviceWithoutAdvisor() throws Throwable { + // Reacts to ServletException and RemoteException + MyThrowsHandler th = new MyThrowsHandler(); + + Echo target = new Echo(); + target.setA(16); + ProxyFactory pf = new ProxyFactory(target); + pf.addAdvice(new NopInterceptor()); + pf.addAdvice(th); + IEcho proxied = (IEcho) createProxy(pf); + assertThat(th.getCalls()).isEqualTo(0); + assertThat(proxied.getA()).isEqualTo(target.getA()); + assertThat(th.getCalls()).isEqualTo(0); + Exception ex = new Exception(); + // Will be advised but doesn't match + assertThatExceptionOfType(Exception.class).isThrownBy(() -> + proxied.echoException(1, ex)) + .matches(ex::equals); + + // Subclass of RemoteException + MarshalException mex = new MarshalException(""); + assertThatExceptionOfType(MarshalException.class).isThrownBy(() -> + proxied.echoException(1, mex)) + .matches(mex::equals); + + assertThat(th.getCalls("remoteException")).isEqualTo(1); + } + + private static class CheckMethodInvocationIsSameInAndOutInterceptor implements MethodInterceptor { + + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + Method m = mi.getMethod(); + Object retval = mi.proceed(); + assertThat(mi.getMethod()).as("Method invocation has same method on way back").isEqualTo(m); + return retval; + } + } + + + /** + * ExposeInvocation must be set to true. + */ + private static class CheckMethodInvocationViaThreadLocalIsSameInAndOutInterceptor implements MethodInterceptor { + + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + String task = "get invocation on way IN"; + try { + MethodInvocation current = ExposeInvocationInterceptor.currentInvocation(); + assertThat(current.getMethod()).isEqualTo(mi.getMethod()); + Object retval = mi.proceed(); + task = "get invocation on way OUT"; + assertThat(ExposeInvocationInterceptor.currentInvocation()).isEqualTo(current); + return retval; + } + catch (IllegalStateException ex) { + System.err.println(task + " for " + mi.getMethod()); + ex.printStackTrace(); + throw ex; + } + } + } + + + /** + * Same thing for a proxy. + * Only works when exposeProxy is set to true. + * Checks that the proxy is the same on the way in and out. + */ + private static class ProxyMatcherInterceptor implements MethodInterceptor { + + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + Object proxy = AopContext.currentProxy(); + Object ret = mi.proceed(); + assertThat(AopContext.currentProxy()).isEqualTo(proxy); + return ret; + } + } + + + /** + * Fires on setter methods that take a string. Replaces null arg with "". + */ + @SuppressWarnings("serial") + protected static class StringSetterNullReplacementAdvice extends DefaultPointcutAdvisor { + + private static MethodInterceptor cleaner = new MethodInterceptor() { + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + // We know it can only be invoked if there's a single parameter of type string + mi.getArguments()[0] = ""; + return mi.proceed(); + } + }; + + public StringSetterNullReplacementAdvice() { + super(cleaner); + setPointcut(new DynamicMethodMatcherPointcut() { + @Override + public boolean matches(Method m, @Nullable Class targetClass, Object... args) { + return args[0] == null; + } + @Override + public boolean matches(Method m, @Nullable Class targetClass) { + return m.getName().startsWith("set") && + m.getParameterCount() == 1 && + m.getParameterTypes()[0].equals(String.class); + } + }); + } + } + + + @SuppressWarnings("serial") + protected static class TestDynamicPointcutAdvice extends DefaultPointcutAdvisor { + + public int count; + + public TestDynamicPointcutAdvice(MethodInterceptor mi, final String pattern) { + super(mi); + setPointcut(new DynamicMethodMatcherPointcut() { + @Override + public boolean matches(Method m, @Nullable Class targetClass, Object... args) { + boolean run = m.getName().contains(pattern); + if (run) ++count; + return run; + } + }); + } + } + + + @SuppressWarnings("serial") + protected static class TestDynamicPointcutForSettersOnly extends DefaultPointcutAdvisor { + + public int count; + + public TestDynamicPointcutForSettersOnly(MethodInterceptor mi, final String pattern) { + super(mi); + setPointcut(new DynamicMethodMatcherPointcut() { + @Override + public boolean matches(Method m, @Nullable Class targetClass, Object... args) { + boolean run = m.getName().contains(pattern); + if (run) ++count; + return run; + } + @Override + public boolean matches(Method m, @Nullable Class clazz) { + return m.getName().startsWith("set"); + } + }); + } + } + + + @SuppressWarnings("serial") + protected static class TestStaticPointcutAdvice extends StaticMethodMatcherPointcutAdvisor { + + private String pattern; + + public TestStaticPointcutAdvice(MethodInterceptor mi, String pattern) { + super(mi); + this.pattern = pattern; + } + @Override + public boolean matches(Method m, @Nullable Class targetClass) { + return m.getName().contains(pattern); + } + } + + + /** + * Note that trapping the Invocation as in previous version of this test + * isn't safe, as invocations may be reused + * and hence cleared at the end of each invocation. + * So we trap only the target. + */ + protected static class TrapTargetInterceptor implements MethodInterceptor { + + public Object target; + + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + this.target = invocation.getThis(); + return invocation.proceed(); + } + } + + + private static class DummyIntroductionAdviceImpl implements DynamicIntroductionAdvice { + + @Override + public boolean implementsInterface(Class intf) { + return true; + } + } + + + public static class OwnSpouse extends TestBean { + + @Override + public ITestBean getSpouse() { + return this; + } + } + + + public static class AllInstancesAreEqual implements IOther { + + @Override + public boolean equals(Object other) { + return (other instanceof AllInstancesAreEqual); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + @Override + public void absquatulate() { + } + } + + + public interface INeedsToSeeProxy { + + int getCount(); + + void incrementViaThis(); + + void incrementViaProxy(); + + void increment(); + } + + + public static class NeedsToSeeProxy implements INeedsToSeeProxy { + + private int count; + + @Override + public int getCount() { + return count; + } + + @Override + public void incrementViaThis() { + this.increment(); + } + + @Override + public void incrementViaProxy() { + INeedsToSeeProxy thisViaProxy = (INeedsToSeeProxy) AopContext.currentProxy(); + thisViaProxy.increment(); + Advised advised = (Advised) thisViaProxy; + checkAdvised(advised); + } + + protected void checkAdvised(Advised advised) { + } + + @Override + public void increment() { + ++count; + } + } + + + public static class TargetChecker extends NeedsToSeeProxy { + + @Override + protected void checkAdvised(Advised advised) { + // TODO replace this check: no longer possible + //assertEquals(advised.getTarget(), this); + } + } + + + public static class CountingAdvisorListener implements AdvisedSupportListener { + + public int adviceChanges; + public int activates; + private AdvisedSupport expectedSource; + + public CountingAdvisorListener(AdvisedSupport expectedSource) { + this.expectedSource = expectedSource; + } + + @Override + public void activated(AdvisedSupport advised) { + assertThat(advised).isEqualTo(expectedSource); + ++activates; + } + + @Override + public void adviceChanged(AdvisedSupport advised) { + assertThat(advised).isEqualTo(expectedSource); + ++adviceChanges; + } + } + + + public static class RefreshCountingAdvisorChainFactory implements AdvisedSupportListener { + + public int refreshes; + + @Override + public void activated(AdvisedSupport advised) { + ++refreshes; + } + + @Override + public void adviceChanged(AdvisedSupport advised) { + ++refreshes; + } + } + + + public static interface IOverloads { + + void overload(); + + int overload(int i); + + String overload(String foo); + + void noAdvice(); + } + + + public static class Overloads implements IOverloads { + + @Override + public void overload() { + } + + @Override + public int overload(int i) { + return i; + } + + @Override + public String overload(String s) { + return s; + } + + @Override + public void noAdvice() { + } + } + + + @SuppressWarnings("serial") + public static class CountingMultiAdvice extends MethodCounter implements MethodBeforeAdvice, + AfterReturningAdvice, ThrowsAdvice { + + @Override + public void before(Method m, Object[] args, @Nullable Object target) throws Throwable { + count(m); + } + + @Override + public void afterReturning(@Nullable Object o, Method m, Object[] args, @Nullable Object target) + throws Throwable { + count(m); + } + + public void afterThrowing(IOException ex) throws Throwable { + count(IOException.class.getName()); + } + + public void afterThrowing(UncheckedException ex) throws Throwable { + count(UncheckedException.class.getName()); + } + + } + + + @SuppressWarnings("serial") + public static class CountingThrowsAdvice extends MethodCounter implements ThrowsAdvice { + + public void afterThrowing(IOException ex) throws Throwable { + count(IOException.class.getName()); + } + + public void afterThrowing(UncheckedException ex) throws Throwable { + count(UncheckedException.class.getName()); + } + + } + + + @SuppressWarnings("serial") + static class UncheckedException extends RuntimeException { + + } + + + @SuppressWarnings("serial") + static class SpecializedUncheckedException extends UncheckedException { + + public SpecializedUncheckedException(String string, SQLException exception) { + } + + } + + + static class MockTargetSource implements TargetSource { + + private Object target; + + public int gets; + + public int releases; + + public void reset() { + this.target = null; + gets = releases = 0; + } + + public void setTarget(Object target) { + this.target = target; + } + + /** + * @see org.springframework.aop.TargetSource#getTargetClass() + */ + @Override + public Class getTargetClass() { + return target.getClass(); + } + + /** + * @see org.springframework.aop.TargetSource#getTarget() + */ + @Override + public Object getTarget() throws Exception { + ++gets; + return target; + } + + /** + * @see org.springframework.aop.TargetSource#releaseTarget(java.lang.Object) + */ + @Override + public void releaseTarget(Object pTarget) throws Exception { + if (pTarget != this.target) + throw new RuntimeException("Released wrong target"); + ++releases; + } + + /** + * Check that gets and releases match + * + */ + public void verify() { + if (gets != releases) + throw new RuntimeException("Expectation failed: " + gets + " gets and " + releases + " releases"); + } + + /** + * @see org.springframework.aop.TargetSource#isStatic() + */ + @Override + public boolean isStatic() { + return false; + } + + } + + + static abstract class ExposedInvocationTestBean extends TestBean { + + @Override + public String getName() { + MethodInvocation invocation = ExposeInvocationInterceptor.currentInvocation(); + assertions(invocation); + return super.getName(); + } + + @Override + public void absquatulate() { + MethodInvocation invocation = ExposeInvocationInterceptor.currentInvocation(); + assertions(invocation); + super.absquatulate(); + } + + protected abstract void assertions(MethodInvocation invocation); + } + + + static class InvocationCheckExposedInvocationTestBean extends ExposedInvocationTestBean { + @Override + protected void assertions(MethodInvocation invocation) { + assertThat(invocation.getThis()).isSameAs(this); + assertThat(ITestBean.class.isAssignableFrom(invocation.getMethod().getDeclaringClass())).as("Invocation should be on ITestBean: " + invocation.getMethod()).isTrue(); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/framework/CglibProxyTests.java b/spring-context/src/test/java/org/springframework/aop/framework/CglibProxyTests.java new file mode 100644 index 0000000..ba247ed --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/framework/CglibProxyTests.java @@ -0,0 +1,557 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import java.io.Serializable; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.junit.jupiter.api.Test; +import test.mixin.LockMixinAdvisor; + +import org.springframework.aop.ClassFilter; +import org.springframework.aop.MethodMatcher; +import org.springframework.aop.Pointcut; +import org.springframework.aop.support.AopUtils; +import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.aop.testfixture.advice.CountingBeforeAdvice; +import org.springframework.aop.testfixture.interceptor.NopInterceptor; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextException; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Additional and overridden tests for CGLIB proxies. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Rob Harrop + * @author Ramnivas Laddad + * @author Chris Beams + */ +@SuppressWarnings("serial") +public class CglibProxyTests extends AbstractAopProxyTests implements Serializable { + + private static final String DEPENDENCY_CHECK_CONTEXT = + CglibProxyTests.class.getSimpleName() + "-with-dependency-checking.xml"; + + + @Override + protected Object createProxy(ProxyCreatorSupport as) { + as.setProxyTargetClass(true); + Object proxy = as.createAopProxy().getProxy(); + assertThat(AopUtils.isCglibProxy(proxy)).isTrue(); + return proxy; + } + + @Override + protected AopProxy createAopProxy(AdvisedSupport as) { + as.setProxyTargetClass(true); + return new CglibAopProxy(as); + } + + @Override + protected boolean requiresTarget() { + return true; + } + + + @Test + public void testNullConfig() { + assertThatIllegalArgumentException().isThrownBy(() -> + new CglibAopProxy(null)); + } + + @Test + public void testNoTarget() { + AdvisedSupport pc = new AdvisedSupport(ITestBean.class); + pc.addAdvice(new NopInterceptor()); + AopProxy aop = createAopProxy(pc); + assertThatExceptionOfType(AopConfigException.class).isThrownBy( + aop::getProxy); + } + + @Test + public void testProtectedMethodInvocation() { + ProtectedMethodTestBean bean = new ProtectedMethodTestBean(); + bean.value = "foo"; + mockTargetSource.setTarget(bean); + + AdvisedSupport as = new AdvisedSupport(); + as.setTargetSource(mockTargetSource); + as.addAdvice(new NopInterceptor()); + AopProxy aop = new CglibAopProxy(as); + + ProtectedMethodTestBean proxy = (ProtectedMethodTestBean) aop.getProxy(); + assertThat(AopUtils.isCglibProxy(proxy)).isTrue(); + assertThat(bean.getClass().getClassLoader()).isEqualTo(proxy.getClass().getClassLoader()); + assertThat(proxy.getString()).isEqualTo("foo"); + } + + @Test + public void testPackageMethodInvocation() { + PackageMethodTestBean bean = new PackageMethodTestBean(); + bean.value = "foo"; + mockTargetSource.setTarget(bean); + + AdvisedSupport as = new AdvisedSupport(); + as.setTargetSource(mockTargetSource); + as.addAdvice(new NopInterceptor()); + AopProxy aop = new CglibAopProxy(as); + + PackageMethodTestBean proxy = (PackageMethodTestBean) aop.getProxy(); + assertThat(AopUtils.isCglibProxy(proxy)).isTrue(); + assertThat(bean.getClass().getClassLoader()).isEqualTo(proxy.getClass().getClassLoader()); + assertThat(proxy.getString()).isEqualTo("foo"); + } + + @Test + public void testProxyCanBeClassNotInterface() { + TestBean raw = new TestBean(); + raw.setAge(32); + mockTargetSource.setTarget(raw); + AdvisedSupport pc = new AdvisedSupport(); + pc.setTargetSource(mockTargetSource); + AopProxy aop = new CglibAopProxy(pc); + + Object proxy = aop.getProxy(); + assertThat(AopUtils.isCglibProxy(proxy)).isTrue(); + assertThat(proxy instanceof ITestBean).isTrue(); + assertThat(proxy instanceof TestBean).isTrue(); + + TestBean tb = (TestBean) proxy; + assertThat(tb.getAge()).isEqualTo(32); + } + + @Test + public void testMethodInvocationDuringConstructor() { + CglibTestBean bean = new CglibTestBean(); + bean.setName("Rob Harrop"); + + AdvisedSupport as = new AdvisedSupport(); + as.setTarget(bean); + as.addAdvice(new NopInterceptor()); + AopProxy aop = new CglibAopProxy(as); + + CglibTestBean proxy = (CglibTestBean) aop.getProxy(); + assertThat(proxy.getName()).as("The name property has been overwritten by the constructor").isEqualTo("Rob Harrop"); + } + + @Test + public void testToStringInvocation() { + PrivateCglibTestBean bean = new PrivateCglibTestBean(); + bean.setName("Rob Harrop"); + + AdvisedSupport as = new AdvisedSupport(); + as.setTarget(bean); + as.addAdvice(new NopInterceptor()); + AopProxy aop = new CglibAopProxy(as); + + PrivateCglibTestBean proxy = (PrivateCglibTestBean) aop.getProxy(); + assertThat(proxy.toString()).as("The name property has been overwritten by the constructor").isEqualTo("Rob Harrop"); + } + + @Test + public void testUnadvisedProxyCreationWithCallDuringConstructor() { + CglibTestBean target = new CglibTestBean(); + target.setName("Rob Harrop"); + + AdvisedSupport pc = new AdvisedSupport(); + pc.setFrozen(true); + pc.setTarget(target); + + CglibAopProxy aop = new CglibAopProxy(pc); + CglibTestBean proxy = (CglibTestBean) aop.getProxy(); + assertThat(proxy).as("Proxy should not be null").isNotNull(); + assertThat(proxy.getName()).as("Constructor overrode the value of name").isEqualTo("Rob Harrop"); + } + + @Test + public void testMultipleProxies() { + TestBean target = new TestBean(); + target.setAge(20); + TestBean target2 = new TestBean(); + target2.setAge(21); + + ITestBean proxy1 = getAdvisedProxy(target); + ITestBean proxy2 = getAdvisedProxy(target2); + assertThat(proxy2.getClass()).isSameAs(proxy1.getClass()); + assertThat(proxy1.getAge()).isEqualTo(target.getAge()); + assertThat(proxy2.getAge()).isEqualTo(target2.getAge()); + } + + private ITestBean getAdvisedProxy(TestBean target) { + ProxyFactory pf = new ProxyFactory(new Class[]{ITestBean.class}); + pf.setProxyTargetClass(true); + + MethodInterceptor advice = new NopInterceptor(); + Pointcut pointcut = new Pointcut() { + @Override + public ClassFilter getClassFilter() { + return ClassFilter.TRUE; + } + @Override + public MethodMatcher getMethodMatcher() { + return MethodMatcher.TRUE; + } + @Override + public boolean equals(Object obj) { + return true; + } + @Override + public int hashCode() { + return 0; + } + }; + pf.addAdvisor(new DefaultPointcutAdvisor(pointcut, advice)); + + pf.setTarget(target); + pf.setFrozen(true); + pf.setExposeProxy(false); + + return (ITestBean) pf.getProxy(); + } + + @Test + public void testMultipleProxiesForIntroductionAdvisor() { + TestBean target1 = new TestBean(); + target1.setAge(20); + TestBean target2 = new TestBean(); + target2.setAge(21); + + ITestBean proxy1 = getIntroductionAdvisorProxy(target1); + ITestBean proxy2 = getIntroductionAdvisorProxy(target2); + assertThat(proxy2.getClass()).as("Incorrect duplicate creation of proxy classes").isSameAs(proxy1.getClass()); + } + + private ITestBean getIntroductionAdvisorProxy(TestBean target) { + ProxyFactory pf = new ProxyFactory(ITestBean.class); + pf.setProxyTargetClass(true); + + pf.addAdvisor(new LockMixinAdvisor()); + pf.setTarget(target); + pf.setFrozen(true); + pf.setExposeProxy(false); + + return (ITestBean) pf.getProxy(); + } + + @Test + public void testWithNoArgConstructor() { + NoArgCtorTestBean target = new NoArgCtorTestBean("b", 1); + target.reset(); + + mockTargetSource.setTarget(target); + AdvisedSupport pc = new AdvisedSupport(); + pc.setTargetSource(mockTargetSource); + CglibAopProxy aop = new CglibAopProxy(pc); + aop.setConstructorArguments(new Object[] {"Rob Harrop", 22}, new Class[] {String.class, int.class}); + + NoArgCtorTestBean proxy = (NoArgCtorTestBean) aop.getProxy(); + assertThat(proxy).isNotNull(); + } + + @Test + public void testProxyAProxy() { + ITestBean target = new TestBean(); + + mockTargetSource.setTarget(target); + AdvisedSupport as = new AdvisedSupport(); + as.setTargetSource(mockTargetSource); + as.addAdvice(new NopInterceptor()); + CglibAopProxy cglib = new CglibAopProxy(as); + + ITestBean proxy1 = (ITestBean) cglib.getProxy(); + + mockTargetSource.setTarget(proxy1); + as = new AdvisedSupport(new Class[]{}); + as.setTargetSource(mockTargetSource); + as.addAdvice(new NopInterceptor()); + cglib = new CglibAopProxy(as); + + assertThat(cglib.getProxy()).isInstanceOf(ITestBean.class); + } + + @Test + public void testProxyAProxyWithAdditionalInterface() { + ITestBean target = new TestBean(); + mockTargetSource.setTarget(target); + + AdvisedSupport as = new AdvisedSupport(); + as.setTargetSource(mockTargetSource); + as.addAdvice(new NopInterceptor()); + as.addInterface(Serializable.class); + CglibAopProxy cglib = new CglibAopProxy(as); + + ITestBean proxy1 = (ITestBean) cglib.getProxy(); + + mockTargetSource.setTarget(proxy1); + as = new AdvisedSupport(new Class[]{}); + as.setTargetSource(mockTargetSource); + as.addAdvice(new NopInterceptor()); + cglib = new CglibAopProxy(as); + + ITestBean proxy2 = (ITestBean) cglib.getProxy(); + assertThat(proxy2 instanceof Serializable).isTrue(); + } + + @Test + public void testExceptionHandling() { + ExceptionThrower bean = new ExceptionThrower(); + mockTargetSource.setTarget(bean); + + AdvisedSupport as = new AdvisedSupport(); + as.setTargetSource(mockTargetSource); + as.addAdvice(new NopInterceptor()); + AopProxy aop = new CglibAopProxy(as); + + ExceptionThrower proxy = (ExceptionThrower) aop.getProxy(); + + try { + proxy.doTest(); + } + catch (Exception ex) { + assertThat(ex instanceof ApplicationContextException).as("Invalid exception class").isTrue(); + } + + assertThat(proxy.isCatchInvoked()).as("Catch was not invoked").isTrue(); + assertThat(proxy.isFinallyInvoked()).as("Finally was not invoked").isTrue(); + } + + @Test + @SuppressWarnings("resource") + public void testWithDependencyChecking() { + ApplicationContext ctx = new ClassPathXmlApplicationContext(DEPENDENCY_CHECK_CONTEXT, getClass()); + ctx.getBean("testBean"); + } + + @Test + public void testAddAdviceAtRuntime() { + TestBean bean = new TestBean(); + CountingBeforeAdvice cba = new CountingBeforeAdvice(); + + ProxyFactory pf = new ProxyFactory(); + pf.setTarget(bean); + pf.setFrozen(false); + pf.setOpaque(false); + pf.setProxyTargetClass(true); + + TestBean proxy = (TestBean) pf.getProxy(); + assertThat(AopUtils.isCglibProxy(proxy)).isTrue(); + + proxy.getAge(); + assertThat(cba.getCalls()).isEqualTo(0); + + ((Advised) proxy).addAdvice(cba); + proxy.getAge(); + assertThat(cba.getCalls()).isEqualTo(1); + } + + @Test + public void testProxyProtectedMethod() { + CountingBeforeAdvice advice = new CountingBeforeAdvice(); + ProxyFactory proxyFactory = new ProxyFactory(new MyBean()); + proxyFactory.addAdvice(advice); + proxyFactory.setProxyTargetClass(true); + + MyBean proxy = (MyBean) proxyFactory.getProxy(); + assertThat(proxy.add(1, 3)).isEqualTo(4); + assertThat(advice.getCalls("add")).isEqualTo(1); + } + + @Test + public void testProxyTargetClassInCaseOfNoInterfaces() { + ProxyFactory proxyFactory = new ProxyFactory(new MyBean()); + MyBean proxy = (MyBean) proxyFactory.getProxy(); + assertThat(proxy.add(1, 3)).isEqualTo(4); + } + + @Test // SPR-13328 + @SuppressWarnings("unchecked") + public void testVarargsWithEnumArray() { + ProxyFactory proxyFactory = new ProxyFactory(new MyBean()); + MyBean proxy = (MyBean) proxyFactory.getProxy(); + assertThat(proxy.doWithVarargs(MyEnum.A, MyOtherEnum.C)).isTrue(); + } + + + public static class MyBean { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + protected int add(int x, int y) { + return x + y; + } + + @SuppressWarnings("unchecked") + public boolean doWithVarargs(V... args) { + return true; + } + } + + + public interface MyInterface { + } + + + public enum MyEnum implements MyInterface { + + A, B; + } + + + public enum MyOtherEnum implements MyInterface { + + C, D; + } + + + public static class ExceptionThrower { + + private boolean catchInvoked; + + private boolean finallyInvoked; + + public boolean isCatchInvoked() { + return catchInvoked; + } + + public boolean isFinallyInvoked() { + return finallyInvoked; + } + + public void doTest() throws Exception { + try { + throw new ApplicationContextException("foo"); + } + catch (Exception ex) { + catchInvoked = true; + throw ex; + } + finally { + finallyInvoked = true; + } + } + } + + + public static class NoArgCtorTestBean { + + private boolean called = false; + + public NoArgCtorTestBean(String x, int y) { + called = true; + } + + public boolean wasCalled() { + return called; + } + + public void reset() { + called = false; + } + } + + + public static class ProtectedMethodTestBean { + + public String value; + + protected String getString() { + return this.value; + } + } + + + public static class PackageMethodTestBean { + + public String value; + + String getString() { + return this.value; + } + } + + + private static class PrivateCglibTestBean { + + private String name; + + public PrivateCglibTestBean() { + setName("Some Default"); + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + + @Override + public String toString() { + return this.name; + } + } +} + + +class CglibTestBean { + + private String name; + + public CglibTestBean() { + setName("Some Default"); + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + + @Override + public String toString() { + return this.name; + } +} + + +class UnsupportedInterceptor implements MethodInterceptor { + + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + throw new UnsupportedOperationException(mi.getMethod().getName()); + } +} diff --git a/spring-context/src/test/java/org/springframework/aop/framework/ClassWithComplexConstructor.java b/spring-context/src/test/java/org/springframework/aop/framework/ClassWithComplexConstructor.java new file mode 100644 index 0000000..82dd189 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/framework/ClassWithComplexConstructor.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +/** + * @author Oliver Gierke + */ +@Component +public class ClassWithComplexConstructor { + + private final Dependency dependency; + + @Autowired ClassWithComplexConstructor selfReference; + + @Autowired + public ClassWithComplexConstructor(Dependency dependency) { + Assert.notNull(dependency, "No Dependency bean injected"); + this.dependency = dependency; + } + + public Dependency getDependency() { + return this.dependency; + } + + public void method() { + Assert.state(this.selfReference != this && AopUtils.isCglibProxy(this.selfReference), + "Self reference must be a CGLIB proxy"); + this.dependency.method(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/framework/Dependency.java b/spring-context/src/test/java/org/springframework/aop/framework/Dependency.java new file mode 100644 index 0000000..c965f3a --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/framework/Dependency.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import org.springframework.stereotype.Component; + +@Component class Dependency { + + private int value = 0; + + public void method() { + value++; + } + + public int getValue() { + return value; + } +} diff --git a/spring-context/src/test/java/org/springframework/aop/framework/Echo.java b/spring-context/src/test/java/org/springframework/aop/framework/Echo.java new file mode 100644 index 0000000..90c8c32 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/framework/Echo.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +public class Echo implements IEcho { + + private int a; + + @Override + public int echoException(int i, Throwable t) throws Throwable { + if (t != null) { + throw t; + } + return i; + } + + @Override + public void setA(int a) { + this.a = a; + } + + @Override + public int getA() { + return a; + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/framework/IEcho.java b/spring-context/src/test/java/org/springframework/aop/framework/IEcho.java new file mode 100644 index 0000000..cf0bd60 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/framework/IEcho.java @@ -0,0 +1,27 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +public interface IEcho { + + int echoException(int i, Throwable t) throws Throwable; + + int getA(); + + void setA(int a); + +} diff --git a/spring-context/src/test/java/org/springframework/aop/framework/JdkDynamicProxyTests.java b/spring-context/src/test/java/org/springframework/aop/framework/JdkDynamicProxyTests.java new file mode 100644 index 0000000..306c3d4 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/framework/JdkDynamicProxyTests.java @@ -0,0 +1,247 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import java.io.Serializable; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.interceptor.ExposeInvocationInterceptor; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.testfixture.beans.IOther; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Rod Johnson + * @author Juergen Hoeller + * @author Chris Beams + * @since 13.03.2003 + */ +@SuppressWarnings("serial") +public class JdkDynamicProxyTests extends AbstractAopProxyTests implements Serializable { + + @Override + protected Object createProxy(ProxyCreatorSupport as) { + assertThat(as.isProxyTargetClass()).as("Not forcible CGLIB").isFalse(); + Object proxy = as.createAopProxy().getProxy(); + assertThat(AopUtils.isJdkDynamicProxy(proxy)).as("Should be a JDK proxy: " + proxy.getClass()).isTrue(); + return proxy; + } + + @Override + protected AopProxy createAopProxy(AdvisedSupport as) { + return new JdkDynamicAopProxy(as); + } + + + @Test + public void testNullConfig() { + assertThatIllegalArgumentException().isThrownBy(() -> + new JdkDynamicAopProxy(null)); + } + + @Test + public void testProxyIsJustInterface() { + TestBean raw = new TestBean(); + raw.setAge(32); + AdvisedSupport pc = new AdvisedSupport(ITestBean.class); + pc.setTarget(raw); + JdkDynamicAopProxy aop = new JdkDynamicAopProxy(pc); + + Object proxy = aop.getProxy(); + boolean condition = proxy instanceof ITestBean; + assertThat(condition).isTrue(); + boolean condition1 = proxy instanceof TestBean; + assertThat(condition1).isFalse(); + } + + @Test + public void testInterceptorIsInvokedWithNoTarget() { + // Test return value + final int age = 25; + MethodInterceptor mi = (invocation -> age); + + AdvisedSupport pc = new AdvisedSupport(ITestBean.class); + pc.addAdvice(mi); + AopProxy aop = createAopProxy(pc); + + ITestBean tb = (ITestBean) aop.getProxy(); + assertThat(tb.getAge()).as("correct return value").isEqualTo(age); + } + + @Test + public void testTargetCanGetInvocationWithPrivateClass() { + final ExposedInvocationTestBean expectedTarget = new ExposedInvocationTestBean() { + @Override + protected void assertions(MethodInvocation invocation) { + assertThat(invocation.getThis()).isEqualTo(this); + assertThat(invocation.getMethod().getDeclaringClass()).as("Invocation should be on ITestBean: " + invocation.getMethod()).isEqualTo(ITestBean.class); + } + }; + + AdvisedSupport pc = new AdvisedSupport(ITestBean.class, IOther.class); + pc.addAdvice(ExposeInvocationInterceptor.INSTANCE); + TrapTargetInterceptor tii = new TrapTargetInterceptor() { + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + // Assert that target matches BEFORE invocation returns + assertThat(invocation.getThis()).as("Target is correct").isEqualTo(expectedTarget); + return super.invoke(invocation); + } + }; + pc.addAdvice(tii); + pc.setTarget(expectedTarget); + AopProxy aop = createAopProxy(pc); + + ITestBean tb = (ITestBean) aop.getProxy(); + tb.getName(); + } + + @Test + public void testProxyNotWrappedIfIncompatible() { + FooBar bean = new FooBar(); + ProxyCreatorSupport as = new ProxyCreatorSupport(); + as.setInterfaces(Foo.class); + as.setTarget(bean); + + Foo proxy = (Foo) createProxy(as); + assertThat(proxy.getBarThis()).as("Target should be returned when return types are incompatible").isSameAs(bean); + assertThat(proxy.getFooThis()).as("Proxy should be returned when return types are compatible").isSameAs(proxy); + } + + @Test + public void testEqualsAndHashCodeDefined() { + AdvisedSupport as = new AdvisedSupport(Named.class); + as.setTarget(new Person()); + JdkDynamicAopProxy aopProxy = new JdkDynamicAopProxy(as); + Named proxy = (Named) aopProxy.getProxy(); + Named named = new Person(); + assertThat(proxy).isEqualTo(named); + assertThat(named.hashCode()).isEqualTo(proxy.hashCode()); + } + + @Test // SPR-13328 + @SuppressWarnings("unchecked") + public void testVarargsWithEnumArray() { + ProxyFactory proxyFactory = new ProxyFactory(new VarargTestBean()); + VarargTestInterface proxy = (VarargTestInterface) proxyFactory.getProxy(); + assertThat(proxy.doWithVarargs(MyEnum.A, MyOtherEnum.C)).isTrue(); + } + + + public interface Foo { + + Bar getBarThis(); + + Foo getFooThis(); + } + + + public interface Bar { + } + + + public static class FooBar implements Foo, Bar { + + @Override + public Bar getBarThis() { + return this; + } + + @Override + public Foo getFooThis() { + return this; + } + } + + + public interface Named { + + String getName(); + + @Override + boolean equals(Object other); + + @Override + int hashCode(); + } + + + public static class Person implements Named { + + private final String name = "Rob Harrop"; + + @Override + public String getName() { + return this.name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Person person = (Person) o; + if (!name.equals(person.name)) return false; + return true; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + } + + + public interface VarargTestInterface { + + @SuppressWarnings("unchecked") + boolean doWithVarargs(V... args); + } + + + public static class VarargTestBean implements VarargTestInterface { + + @SuppressWarnings("unchecked") + @Override + public boolean doWithVarargs(V... args) { + return true; + } + } + + + public interface MyInterface { + } + + + public enum MyEnum implements MyInterface { + + A, B; + } + + + public enum MyOtherEnum implements MyInterface { + + C, D; + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/framework/ObjenesisProxyTests.java b/spring-context/src/test/java/org/springframework/aop/framework/ObjenesisProxyTests.java new file mode 100644 index 0000000..2a67998 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/framework/ObjenesisProxyTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.interceptor.DebugInterceptor; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Integration test for Objenesis proxy creation. + * + * @author Oliver Gierke + */ +public class ObjenesisProxyTests { + + @Test + public void appliesAspectToClassWithComplexConstructor() { + @SuppressWarnings("resource") + ApplicationContext context = new ClassPathXmlApplicationContext("ObjenesisProxyTests-context.xml", getClass()); + + ClassWithComplexConstructor bean = context.getBean(ClassWithComplexConstructor.class); + bean.method(); + + DebugInterceptor interceptor = context.getBean(DebugInterceptor.class); + assertThat(interceptor.getCount()).isEqualTo(1L); + assertThat(bean.getDependency().getValue()).isEqualTo(1); + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/framework/ProxyFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/aop/framework/ProxyFactoryBeanTests.java new file mode 100644 index 0000000..57df369 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/framework/ProxyFactoryBeanTests.java @@ -0,0 +1,750 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework; + +import java.io.FileNotFoundException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.List; + +import org.aopalliance.aop.Advice; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import test.mixin.Lockable; +import test.mixin.LockedException; + +import org.springframework.aop.ClassFilter; +import org.springframework.aop.IntroductionAdvisor; +import org.springframework.aop.IntroductionInterceptor; +import org.springframework.aop.interceptor.DebugInterceptor; +import org.springframework.aop.support.AopUtils; +import org.springframework.aop.support.DefaultIntroductionAdvisor; +import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.aop.support.DynamicMethodMatcherPointcut; +import org.springframework.aop.testfixture.advice.CountingBeforeAdvice; +import org.springframework.aop.testfixture.advice.MyThrowsHandler; +import org.springframework.aop.testfixture.interceptor.NopInterceptor; +import org.springframework.aop.testfixture.interceptor.TimestampIntroductionInterceptor; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.Person; +import org.springframework.beans.testfixture.beans.SideEffectBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.ApplicationListener; +import org.springframework.context.testfixture.beans.TestApplicationListener; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.testfixture.TimeStamped; +import org.springframework.core.testfixture.io.SerializationTestUtils; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIOException; + +/** + * @since 13.03.2003 + * @author Rod Johnson + * @author Juergen Hoeller + * @author Chris Beams + */ +public class ProxyFactoryBeanTests { + + private static final Class CLASS = ProxyFactoryBeanTests.class; + private static final String CLASSNAME = CLASS.getSimpleName(); + + private static final String CONTEXT = CLASSNAME + "-context.xml"; + private static final String SERIALIZATION_CONTEXT = CLASSNAME + "-serialization.xml"; + private static final String AUTOWIRING_CONTEXT = CLASSNAME + "-autowiring.xml"; + private static final String DBL_TARGETSOURCE_CONTEXT = CLASSNAME + "-double-targetsource.xml"; + private static final String NOTLAST_TARGETSOURCE_CONTEXT = CLASSNAME + "-notlast-targetsource.xml"; + private static final String TARGETSOURCE_CONTEXT = CLASSNAME + "-targetsource.xml"; + private static final String INVALID_CONTEXT = CLASSNAME + "-invalid.xml"; + private static final String FROZEN_CONTEXT = CLASSNAME + "-frozen.xml"; + private static final String PROTOTYPE_CONTEXT = CLASSNAME + "-prototype.xml"; + private static final String THROWS_ADVICE_CONTEXT = CLASSNAME + "-throws-advice.xml"; + private static final String INNER_BEAN_TARGET_CONTEXT = CLASSNAME + "-inner-bean-target.xml"; + + private BeanFactory factory; + + + @BeforeEach + public void setUp() throws Exception { + DefaultListableBeanFactory parent = new DefaultListableBeanFactory(); + parent.registerBeanDefinition("target2", new RootBeanDefinition(TestApplicationListener.class)); + this.factory = new DefaultListableBeanFactory(parent); + new XmlBeanDefinitionReader((BeanDefinitionRegistry) this.factory).loadBeanDefinitions( + new ClassPathResource(CONTEXT, getClass())); + } + + + @Test + public void testIsDynamicProxyWhenInterfaceSpecified() { + ITestBean test1 = (ITestBean) factory.getBean("test1"); + assertThat(Proxy.isProxyClass(test1.getClass())).as("test1 is a dynamic proxy").isTrue(); + } + + @Test + public void testIsDynamicProxyWhenInterfaceSpecifiedForPrototype() { + ITestBean test1 = (ITestBean) factory.getBean("test2"); + assertThat(Proxy.isProxyClass(test1.getClass())).as("test2 is a dynamic proxy").isTrue(); + } + + @Test + public void testIsDynamicProxyWhenAutodetectingInterfaces() { + ITestBean test1 = (ITestBean) factory.getBean("test3"); + assertThat(Proxy.isProxyClass(test1.getClass())).as("test3 is a dynamic proxy").isTrue(); + } + + @Test + public void testIsDynamicProxyWhenAutodetectingInterfacesForPrototype() { + ITestBean test1 = (ITestBean) factory.getBean("test4"); + assertThat(Proxy.isProxyClass(test1.getClass())).as("test4 is a dynamic proxy").isTrue(); + } + + /** + * Test that it's forbidden to specify TargetSource in both + * interceptor chain and targetSource property. + */ + @Test + public void testDoubleTargetSourcesAreRejected() { + testDoubleTargetSourceIsRejected("doubleTarget"); + // Now with conversion from arbitrary bean to a TargetSource + testDoubleTargetSourceIsRejected("arbitraryTarget"); + } + + private void testDoubleTargetSourceIsRejected(String name) { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(DBL_TARGETSOURCE_CONTEXT, CLASS)); + assertThatExceptionOfType(BeanCreationException.class).as("Should not allow TargetSource to be specified in interceptorNames as well as targetSource property") + .isThrownBy(() -> bf.getBean(name)) + .havingCause() + .isInstanceOf(AopConfigException.class) + .withMessageContaining("TargetSource"); + } + + @Test + public void testTargetSourceNotAtEndOfInterceptorNamesIsRejected() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(NOTLAST_TARGETSOURCE_CONTEXT, CLASS)); + assertThatExceptionOfType(BeanCreationException.class).as("TargetSource or non-advised object must be last in interceptorNames") + .isThrownBy(() -> bf.getBean("targetSourceNotLast")) + .havingCause() + .isInstanceOf(AopConfigException.class) + .withMessageContaining("interceptorNames"); + } + + @Test + public void testGetObjectTypeWithDirectTarget() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(TARGETSOURCE_CONTEXT, CLASS)); + + // We have a counting before advice here + CountingBeforeAdvice cba = (CountingBeforeAdvice) bf.getBean("countingBeforeAdvice"); + assertThat(cba.getCalls()).isEqualTo(0); + + ITestBean tb = (ITestBean) bf.getBean("directTarget"); + assertThat(tb.getName().equals("Adam")).isTrue(); + assertThat(cba.getCalls()).isEqualTo(1); + + ProxyFactoryBean pfb = (ProxyFactoryBean) bf.getBean("&directTarget"); + assertThat(TestBean.class.isAssignableFrom(pfb.getObjectType())).as("Has correct object type").isTrue(); + } + + @Test + public void testGetObjectTypeWithTargetViaTargetSource() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(TARGETSOURCE_CONTEXT, CLASS)); + ITestBean tb = (ITestBean) bf.getBean("viaTargetSource"); + assertThat(tb.getName().equals("Adam")).isTrue(); + ProxyFactoryBean pfb = (ProxyFactoryBean) bf.getBean("&viaTargetSource"); + assertThat(TestBean.class.isAssignableFrom(pfb.getObjectType())).as("Has correct object type").isTrue(); + } + + @Test + public void testGetObjectTypeWithNoTargetOrTargetSource() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(TARGETSOURCE_CONTEXT, CLASS)); + + ITestBean tb = (ITestBean) bf.getBean("noTarget"); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + tb.getName()) + .withMessage("getName"); + FactoryBean pfb = (ProxyFactoryBean) bf.getBean("&noTarget"); + assertThat(ITestBean.class.isAssignableFrom(pfb.getObjectType())).as("Has correct object type").isTrue(); + } + + /** + * The instances are equal, but do not have object identity. + * Interceptors and interfaces and the target are the same. + */ + @Test + public void testSingletonInstancesAreEqual() { + ITestBean test1 = (ITestBean) factory.getBean("test1"); + ITestBean test1_1 = (ITestBean) factory.getBean("test1"); + //assertTrue("Singleton instances ==", test1 == test1_1); + assertThat(test1_1).as("Singleton instances ==").isEqualTo(test1); + test1.setAge(25); + assertThat(test1_1.getAge()).isEqualTo(test1.getAge()); + test1.setAge(250); + assertThat(test1_1.getAge()).isEqualTo(test1.getAge()); + Advised pc1 = (Advised) test1; + Advised pc2 = (Advised) test1_1; + assertThat(pc2.getAdvisors()).isEqualTo(pc1.getAdvisors()); + int oldLength = pc1.getAdvisors().length; + NopInterceptor di = new NopInterceptor(); + pc1.addAdvice(1, di); + assertThat(pc2.getAdvisors()).isEqualTo(pc1.getAdvisors()); + assertThat(pc2.getAdvisors().length).as("Now have one more advisor").isEqualTo((oldLength + 1)); + assertThat(0).isEqualTo(di.getCount()); + test1.setAge(5); + assertThat(test1.getAge()).isEqualTo(test1_1.getAge()); + assertThat(3).isEqualTo(di.getCount()); + } + + @Test + public void testPrototypeInstancesAreNotEqual() { + assertThat(ITestBean.class.isAssignableFrom(factory.getType("prototype"))).as("Has correct object type").isTrue(); + ITestBean test2 = (ITestBean) factory.getBean("prototype"); + ITestBean test2_1 = (ITestBean) factory.getBean("prototype"); + assertThat(test2 != test2_1).as("Prototype instances !=").isTrue(); + assertThat(test2.equals(test2_1)).as("Prototype instances equal").isTrue(); + assertThat(ITestBean.class.isAssignableFrom(factory.getType("prototype"))).as("Has correct object type").isTrue(); + } + + /** + * Uses its own bean factory XML for clarity + * @param beanName name of the ProxyFactoryBean definition that should + * be a prototype + */ + private Object testPrototypeInstancesAreIndependent(String beanName) { + // Initial count value set in bean factory XML + int INITIAL_COUNT = 10; + + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(PROTOTYPE_CONTEXT, CLASS)); + + // Check it works without AOP + SideEffectBean raw = (SideEffectBean) bf.getBean("prototypeTarget"); + assertThat(raw.getCount()).isEqualTo(INITIAL_COUNT); + raw.doWork(); + assertThat(raw.getCount()).isEqualTo(INITIAL_COUNT+1); + raw = (SideEffectBean) bf.getBean("prototypeTarget"); + assertThat(raw.getCount()).isEqualTo(INITIAL_COUNT); + + // Now try with advised instances + SideEffectBean prototype2FirstInstance = (SideEffectBean) bf.getBean(beanName); + assertThat(prototype2FirstInstance.getCount()).isEqualTo(INITIAL_COUNT); + prototype2FirstInstance.doWork(); + assertThat(prototype2FirstInstance.getCount()).isEqualTo(INITIAL_COUNT + 1); + + SideEffectBean prototype2SecondInstance = (SideEffectBean) bf.getBean(beanName); + assertThat(prototype2FirstInstance == prototype2SecondInstance).as("Prototypes are not ==").isFalse(); + assertThat(prototype2SecondInstance.getCount()).isEqualTo(INITIAL_COUNT); + assertThat(prototype2FirstInstance.getCount()).isEqualTo(INITIAL_COUNT + 1); + + return prototype2FirstInstance; + } + + @Test + public void testCglibPrototypeInstance() { + Object prototype = testPrototypeInstancesAreIndependent("cglibPrototype"); + assertThat(AopUtils.isCglibProxy(prototype)).as("It's a cglib proxy").isTrue(); + assertThat(AopUtils.isJdkDynamicProxy(prototype)).as("It's not a dynamic proxy").isFalse(); + } + + /** + * Test invoker is automatically added to manipulate target. + */ + @Test + public void testAutoInvoker() { + String name = "Hieronymous"; + TestBean target = (TestBean) factory.getBean("test"); + target.setName(name); + ITestBean autoInvoker = (ITestBean) factory.getBean("autoInvoker"); + assertThat(autoInvoker.getName().equals(name)).isTrue(); + } + + @Test + public void testCanGetFactoryReferenceAndManipulate() { + ProxyFactoryBean config = (ProxyFactoryBean) factory.getBean("&test1"); + assertThat(ITestBean.class.isAssignableFrom(config.getObjectType())).as("Has correct object type").isTrue(); + assertThat(ITestBean.class.isAssignableFrom(factory.getType("test1"))).as("Has correct object type").isTrue(); + // Trigger lazy initialization. + config.getObject(); + assertThat(config.getAdvisors().length).as("Have one advisors").isEqualTo(1); + assertThat(ITestBean.class.isAssignableFrom(config.getObjectType())).as("Has correct object type").isTrue(); + assertThat(ITestBean.class.isAssignableFrom(factory.getType("test1"))).as("Has correct object type").isTrue(); + + ITestBean tb = (ITestBean) factory.getBean("test1"); + // no exception + tb.hashCode(); + + final Exception ex = new UnsupportedOperationException("invoke"); + // Add evil interceptor to head of list + config.addAdvice(0, new MethodInterceptor() { + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + throw ex; + } + }); + assertThat(config.getAdvisors().length).as("Have correct advisor count").isEqualTo(2); + + ITestBean tb1 = (ITestBean) factory.getBean("test1"); + assertThatExceptionOfType(Exception.class) + .isThrownBy(tb1::toString) + .isSameAs(ex); + } + + /** + * Test that inner bean for target means that we can use + * autowire without ambiguity from target and proxy + */ + @Test + public void testTargetAsInnerBean() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(INNER_BEAN_TARGET_CONTEXT, CLASS)); + ITestBean itb = (ITestBean) bf.getBean("testBean"); + assertThat(itb.getName()).isEqualTo("innerBeanTarget"); + assertThat(bf.getBeanDefinitionCount()).as("Only have proxy and interceptor: no target").isEqualTo(3); + DependsOnITestBean doit = (DependsOnITestBean) bf.getBean("autowireCheck"); + assertThat(doit.tb).isSameAs(itb); + } + + /** + * Try adding and removing interfaces and interceptors on prototype. + * Changes will only affect future references obtained from the factory. + * Each instance will be independent. + */ + @Test + public void testCanAddAndRemoveAspectInterfacesOnPrototype() { + assertThat(factory.getBean("test2")).as("Shouldn't implement TimeStamped before manipulation") + .isNotInstanceOf(TimeStamped.class); + + ProxyFactoryBean config = (ProxyFactoryBean) factory.getBean("&test2"); + long time = 666L; + TimestampIntroductionInterceptor ti = new TimestampIntroductionInterceptor(); + ti.setTime(time); + // Add to head of interceptor chain + int oldCount = config.getAdvisors().length; + config.addAdvisor(0, new DefaultIntroductionAdvisor(ti, TimeStamped.class)); + assertThat(config.getAdvisors().length == oldCount + 1).isTrue(); + + TimeStamped ts = (TimeStamped) factory.getBean("test2"); + assertThat(ts.getTimeStamp()).isEqualTo(time); + + // Can remove + config.removeAdvice(ti); + assertThat(config.getAdvisors().length == oldCount).isTrue(); + + // Check no change on existing object reference + assertThat(ts.getTimeStamp() == time).isTrue(); + + assertThat(factory.getBean("test2")).as("Should no longer implement TimeStamped") + .isNotInstanceOf(TimeStamped.class); + + // Now check non-effect of removing interceptor that isn't there + config.removeAdvice(new DebugInterceptor()); + assertThat(config.getAdvisors().length == oldCount).isTrue(); + + ITestBean it = (ITestBean) ts; + DebugInterceptor debugInterceptor = new DebugInterceptor(); + config.addAdvice(0, debugInterceptor); + it.getSpouse(); + // Won't affect existing reference + assertThat(debugInterceptor.getCount() == 0).isTrue(); + it = (ITestBean) factory.getBean("test2"); + it.getSpouse(); + assertThat(debugInterceptor.getCount()).isEqualTo(1); + config.removeAdvice(debugInterceptor); + it.getSpouse(); + + // Still invoked with old reference + assertThat(debugInterceptor.getCount()).isEqualTo(2); + + // not invoked with new object + it = (ITestBean) factory.getBean("test2"); + it.getSpouse(); + assertThat(debugInterceptor.getCount()).isEqualTo(2); + + // Our own timestamped reference should still work + assertThat(ts.getTimeStamp()).isEqualTo(time); + } + + /** + * Note that we can't add or remove interfaces without reconfiguring the + * singleton. + */ + @Test + public void testCanAddAndRemoveAdvicesOnSingleton() { + ITestBean it = (ITestBean) factory.getBean("test1"); + Advised pc = (Advised) it; + it.getAge(); + NopInterceptor di = new NopInterceptor(); + pc.addAdvice(0, di); + assertThat(di.getCount()).isEqualTo(0); + it.setAge(25); + assertThat(it.getAge()).isEqualTo(25); + assertThat(di.getCount()).isEqualTo(2); + } + + @Test + public void testMethodPointcuts() { + ITestBean tb = (ITestBean) factory.getBean("pointcuts"); + PointcutForVoid.reset(); + assertThat(PointcutForVoid.methodNames.isEmpty()).as("No methods intercepted").isTrue(); + tb.getAge(); + assertThat(PointcutForVoid.methodNames.isEmpty()).as("Not void: shouldn't have intercepted").isTrue(); + tb.setAge(1); + tb.getAge(); + tb.setName("Tristan"); + tb.toString(); + assertThat(PointcutForVoid.methodNames.size()).as("Recorded wrong number of invocations").isEqualTo(2); + assertThat(PointcutForVoid.methodNames.get(0).equals("setAge")).isTrue(); + assertThat(PointcutForVoid.methodNames.get(1).equals("setName")).isTrue(); + } + + @Test + public void testCanAddThrowsAdviceWithoutAdvisor() throws Throwable { + DefaultListableBeanFactory f = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(f).loadBeanDefinitions(new ClassPathResource(THROWS_ADVICE_CONTEXT, CLASS)); + MyThrowsHandler th = (MyThrowsHandler) f.getBean("throwsAdvice"); + CountingBeforeAdvice cba = (CountingBeforeAdvice) f.getBean("countingBeforeAdvice"); + assertThat(cba.getCalls()).isEqualTo(0); + assertThat(th.getCalls()).isEqualTo(0); + IEcho echo = (IEcho) f.getBean("throwsAdvised"); + int i = 12; + echo.setA(i); + assertThat(echo.getA()).isEqualTo(i); + assertThat(cba.getCalls()).isEqualTo(2); + assertThat(th.getCalls()).isEqualTo(0); + Exception expected = new Exception(); + assertThatExceptionOfType(Exception.class).isThrownBy(() -> + echo.echoException(1, expected)) + .matches(expected::equals); + // No throws handler method: count should still be 0 + assertThat(th.getCalls()).isEqualTo(0); + + // Handler knows how to handle this exception + FileNotFoundException expectedFileNotFound = new FileNotFoundException(); + assertThatIOException().isThrownBy(() -> + echo.echoException(1, expectedFileNotFound)) + .matches(expectedFileNotFound::equals); + + // One match + assertThat(th.getCalls("ioException")).isEqualTo(1); + } + + // These two fail the whole bean factory + // TODO put in sep file to check quality of error message + /* + @Test + public void testNoInterceptorNamesWithoutTarget() { + assertThatExceptionOfType(AopConfigurationException.class).as("Should require interceptor names").isThrownBy(() -> + ITestBean tb = (ITestBean) factory.getBean("noInterceptorNamesWithoutTarget")); + } + + @Test + public void testNoInterceptorNamesWithTarget() { + ITestBean tb = (ITestBean) factory.getBean("noInterceptorNamesWithoutTarget"); + } + */ + + @Test + public void testEmptyInterceptorNames() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(INVALID_CONTEXT, CLASS)); + assertThatExceptionOfType(BeanCreationException.class).as("Interceptor names cannot be empty").isThrownBy(() -> + bf.getBean("emptyInterceptorNames")); + } + + /** + * Globals must be followed by a target. + */ + @Test + public void testGlobalsWithoutTarget() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(INVALID_CONTEXT, CLASS)); + assertThatExceptionOfType(BeanCreationException.class).as("Should require target name").isThrownBy(() -> + bf.getBean("globalsWithoutTarget")) + .withCauseInstanceOf(AopConfigException.class); + } + + /** + * Checks that globals get invoked, + * and that they can add aspect interfaces unavailable + * to other beans. These interfaces don't need + * to be included in proxiedInterface []. + */ + @Test + public void testGlobalsCanAddAspectInterfaces() { + AddedGlobalInterface agi = (AddedGlobalInterface) factory.getBean("autoInvoker"); + assertThat(agi.globalsAdded() == -1).isTrue(); + + ProxyFactoryBean pfb = (ProxyFactoryBean) factory.getBean("&validGlobals"); + // Trigger lazy initialization. + pfb.getObject(); + // 2 globals + 2 explicit + assertThat(pfb.getAdvisors().length).as("Have 2 globals and 2 explicit advisors").isEqualTo(3); + + ApplicationListener l = (ApplicationListener) factory.getBean("validGlobals"); + agi = (AddedGlobalInterface) l; + assertThat(agi.globalsAdded() == -1).isTrue(); + + assertThat(factory.getBean("test1")).as("Aspect interface should't be implemeneted without globals") + .isNotInstanceOf(AddedGlobalInterface.class); + } + + @Test + public void testSerializableSingletonProxy() throws Exception { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(SERIALIZATION_CONTEXT, CLASS)); + Person p = (Person) bf.getBean("serializableSingleton"); + assertThat(bf.getBean("serializableSingleton")).as("Should be a Singleton").isSameAs(p); + Person p2 = SerializationTestUtils.serializeAndDeserialize(p); + assertThat(p2).isEqualTo(p); + assertThat(p2).isNotSameAs(p); + assertThat(p2.getName()).isEqualTo("serializableSingleton"); + + // Add unserializable advice + Advice nop = new NopInterceptor(); + ((Advised) p).addAdvice(nop); + // Check it still works + assertThat(p2.getName()).isEqualTo(p2.getName()); + assertThat(SerializationTestUtils.isSerializable(p)).as("Not serializable because an interceptor isn't serializable").isFalse(); + + // Remove offending interceptor... + assertThat(((Advised) p).removeAdvice(nop)).isTrue(); + assertThat(SerializationTestUtils.isSerializable(p)).as("Serializable again because offending interceptor was removed").isTrue(); + } + + @Test + public void testSerializablePrototypeProxy() throws Exception { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(SERIALIZATION_CONTEXT, CLASS)); + Person p = (Person) bf.getBean("serializablePrototype"); + assertThat(bf.getBean("serializablePrototype")).as("Should not be a Singleton").isNotSameAs(p); + Person p2 = SerializationTestUtils.serializeAndDeserialize(p); + assertThat(p2).isEqualTo(p); + assertThat(p2).isNotSameAs(p); + assertThat(p2.getName()).isEqualTo("serializablePrototype"); + } + + @Test + public void testSerializableSingletonProxyFactoryBean() throws Exception { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(SERIALIZATION_CONTEXT, CLASS)); + Person p = (Person) bf.getBean("serializableSingleton"); + ProxyFactoryBean pfb = (ProxyFactoryBean) bf.getBean("&serializableSingleton"); + ProxyFactoryBean pfb2 = SerializationTestUtils.serializeAndDeserialize(pfb); + Person p2 = (Person) pfb2.getObject(); + assertThat(p2).isEqualTo(p); + assertThat(p2).isNotSameAs(p); + assertThat(p2.getName()).isEqualTo("serializableSingleton"); + } + + @Test + public void testProxyNotSerializableBecauseOfAdvice() throws Exception { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(SERIALIZATION_CONTEXT, CLASS)); + Person p = (Person) bf.getBean("interceptorNotSerializableSingleton"); + assertThat(SerializationTestUtils.isSerializable(p)).as("Not serializable because an interceptor isn't serializable").isFalse(); + } + + @Test + public void testPrototypeAdvisor() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(CONTEXT, CLASS)); + + ITestBean bean1 = (ITestBean) bf.getBean("prototypeTestBeanProxy"); + ITestBean bean2 = (ITestBean) bf.getBean("prototypeTestBeanProxy"); + + bean1.setAge(3); + bean2.setAge(4); + + assertThat(bean1.getAge()).isEqualTo(3); + assertThat(bean2.getAge()).isEqualTo(4); + + ((Lockable) bean1).lock(); + + assertThatExceptionOfType(LockedException.class).isThrownBy(() -> + bean1.setAge(5)); + + bean2.setAge(6); //do not expect LockedException" + } + + @Test + public void testPrototypeInterceptorSingletonTarget() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(CONTEXT, CLASS)); + + ITestBean bean1 = (ITestBean) bf.getBean("prototypeTestBeanProxySingletonTarget"); + ITestBean bean2 = (ITestBean) bf.getBean("prototypeTestBeanProxySingletonTarget"); + + bean1.setAge(1); + bean2.setAge(2); + + assertThat(bean1.getAge()).isEqualTo(2); + + ((Lockable) bean1).lock(); + + assertThatExceptionOfType(LockedException.class).isThrownBy(() -> + bean1.setAge(5)); + + // do not expect LockedException + bean2.setAge(6); + } + + /** + * Simple test of a ProxyFactoryBean that has an inner bean as target that specifies autowiring. + * Checks for correct use of getType() by bean factory. + */ + @Test + public void testInnerBeanTargetUsingAutowiring() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(AUTOWIRING_CONTEXT, CLASS)); + bf.getBean("testBean"); + } + + @Test + public void testFrozenFactoryBean() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions(new ClassPathResource(FROZEN_CONTEXT, CLASS)); + + Advised advised = (Advised)bf.getBean("frozen"); + assertThat(advised.isFrozen()).as("The proxy should be frozen").isTrue(); + } + + @Test + public void testDetectsInterfaces() throws Exception { + ProxyFactoryBean fb = new ProxyFactoryBean(); + fb.setTarget(new TestBean()); + fb.addAdvice(new DebugInterceptor()); + fb.setBeanFactory(new DefaultListableBeanFactory()); + ITestBean proxy = (ITestBean) fb.getObject(); + assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue(); + } + + + /** + * Fires only on void methods. Saves list of methods intercepted. + */ + @SuppressWarnings("serial") + public static class PointcutForVoid extends DefaultPointcutAdvisor { + + public static List methodNames = new ArrayList<>(); + + public static void reset() { + methodNames.clear(); + } + + public PointcutForVoid() { + setAdvice(new MethodInterceptor() { + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + methodNames.add(invocation.getMethod().getName()); + return invocation.proceed(); + } + }); + setPointcut(new DynamicMethodMatcherPointcut() { + @Override + public boolean matches(Method m, @Nullable Class targetClass, Object... args) { + return m.getReturnType() == Void.TYPE; + } + }); + } + } + + + public static class DependsOnITestBean { + + public final ITestBean tb; + + public DependsOnITestBean(ITestBean tb) { + this.tb = tb; + } + } + + /** + * Aspect interface + */ + public interface AddedGlobalInterface { + + int globalsAdded(); + } + + + /** + * Use as a global interceptor. Checks that + * global interceptors can add aspect interfaces. + * NB: Add only via global interceptors in XML file. + */ + public static class GlobalAspectInterfaceInterceptor implements IntroductionInterceptor { + + @Override + public boolean implementsInterface(Class intf) { + return intf.equals(AddedGlobalInterface.class); + } + + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + if (mi.getMethod().getDeclaringClass().equals(AddedGlobalInterface.class)) { + return -1; + } + return mi.proceed(); + } + } + + + public static class GlobalIntroductionAdvice implements IntroductionAdvisor { + + private IntroductionInterceptor gi = new GlobalAspectInterfaceInterceptor(); + + @Override + public ClassFilter getClassFilter() { + return ClassFilter.TRUE; + } + + @Override + public Advice getAdvice() { + return this.gi; + } + + @Override + public Class[] getInterfaces() { + return new Class[] { AddedGlobalInterface.class }; + } + + @Override + public boolean isPerInstance() { + return false; + } + + @Override + public void validateInterfaces() { + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/framework/adapter/AdvisorAdapterRegistrationTests.java b/spring-context/src/test/java/org/springframework/aop/framework/adapter/AdvisorAdapterRegistrationTests.java new file mode 100644 index 0000000..6fc2259 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/framework/adapter/AdvisorAdapterRegistrationTests.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.adapter; + +import java.io.Serializable; + +import org.aopalliance.aop.Advice; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.Advisor; +import org.springframework.aop.BeforeAdvice; +import org.springframework.aop.framework.Advised; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * TestCase for AdvisorAdapterRegistrationManager mechanism. + * + * @author Dmitriy Kopylenko + * @author Chris Beams + */ +public class AdvisorAdapterRegistrationTests { + + @BeforeEach + @AfterEach + public void resetGlobalAdvisorAdapterRegistry() { + GlobalAdvisorAdapterRegistry.reset(); + } + + @Test + public void testAdvisorAdapterRegistrationManagerNotPresentInContext() { + ClassPathXmlApplicationContext ctx = + new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-without-bpp.xml", getClass()); + ITestBean tb = (ITestBean) ctx.getBean("testBean"); + // just invoke any method to see if advice fired + assertThatExceptionOfType(UnknownAdviceTypeException.class).isThrownBy( + tb::getName); + assertThat(getAdviceImpl(tb).getInvocationCounter()).isZero(); + } + + @Test + public void testAdvisorAdapterRegistrationManagerPresentInContext() { + ClassPathXmlApplicationContext ctx = + new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-with-bpp.xml", getClass()); + ITestBean tb = (ITestBean) ctx.getBean("testBean"); + // just invoke any method to see if advice fired + tb.getName(); + getAdviceImpl(tb).getInvocationCounter(); + } + + private SimpleBeforeAdviceImpl getAdviceImpl(ITestBean tb) { + Advised advised = (Advised) tb; + Advisor advisor = advised.getAdvisors()[0]; + return (SimpleBeforeAdviceImpl) advisor.getAdvice(); + } + +} + + +interface SimpleBeforeAdvice extends BeforeAdvice { + + void before() throws Throwable; + +} + + +@SuppressWarnings("serial") +class SimpleBeforeAdviceAdapter implements AdvisorAdapter, Serializable { + + @Override + public boolean supportsAdvice(Advice advice) { + return (advice instanceof SimpleBeforeAdvice); + } + + @Override + public MethodInterceptor getInterceptor(Advisor advisor) { + SimpleBeforeAdvice advice = (SimpleBeforeAdvice) advisor.getAdvice(); + return new SimpleBeforeAdviceInterceptor(advice) ; + } + +} + + +class SimpleBeforeAdviceImpl implements SimpleBeforeAdvice { + + private int invocationCounter; + + @Override + public void before() throws Throwable { + ++invocationCounter; + } + + public int getInvocationCounter() { + return invocationCounter; + } + +} + + +final class SimpleBeforeAdviceInterceptor implements MethodInterceptor { + + private SimpleBeforeAdvice advice; + + public SimpleBeforeAdviceInterceptor(SimpleBeforeAdvice advice) { + this.advice = advice; + } + + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + advice.before(); + return mi.proceed(); + } +} diff --git a/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorTests.java b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorTests.java new file mode 100644 index 0000000..8a111eb --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorTests.java @@ -0,0 +1,241 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.autoproxy; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import test.mixin.Lockable; + +import org.springframework.aop.Advisor; +import org.springframework.aop.framework.Advised; +import org.springframework.aop.framework.autoproxy.target.AbstractBeanFactoryBasedTargetSourceCreator; +import org.springframework.aop.support.AopUtils; +import org.springframework.aop.target.AbstractBeanFactoryBasedTargetSource; +import org.springframework.aop.target.CommonsPool2TargetSource; +import org.springframework.aop.target.LazyInitTargetSource; +import org.springframework.aop.target.PrototypeTargetSource; +import org.springframework.aop.target.ThreadLocalTargetSource; +import org.springframework.aop.testfixture.advice.CountingBeforeAdvice; +import org.springframework.aop.testfixture.interceptor.NopInterceptor; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.testfixture.beans.CountingTestBean; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for auto proxy creation by advisor recognition. + * + * @see org.springframework.aop.framework.autoproxy.AdvisorAutoProxyCreatorIntegrationTests + * + * @author Rod Johnson + * @author Dave Syer + * @author Chris Beams + */ +@SuppressWarnings("resource") +public class AdvisorAutoProxyCreatorTests { + + private static final Class CLASS = AdvisorAutoProxyCreatorTests.class; + private static final String CLASSNAME = CLASS.getSimpleName(); + + private static final String DEFAULT_CONTEXT = CLASSNAME + "-context.xml"; + private static final String COMMON_INTERCEPTORS_CONTEXT = CLASSNAME + "-common-interceptors.xml"; + private static final String CUSTOM_TARGETSOURCE_CONTEXT = CLASSNAME + "-custom-targetsource.xml"; + private static final String QUICK_TARGETSOURCE_CONTEXT = CLASSNAME + "-quick-targetsource.xml"; + private static final String OPTIMIZED_CONTEXT = CLASSNAME + "-optimized.xml"; + + + /** + * Return a bean factory with attributes and EnterpriseServices configured. + */ + protected BeanFactory getBeanFactory() throws IOException { + return new ClassPathXmlApplicationContext(DEFAULT_CONTEXT, CLASS); + } + + + /** + * Check that we can provide a common interceptor that will + * appear in the chain before "specific" interceptors, + * which are sourced from matching advisors + */ + @Test + public void testCommonInterceptorAndAdvisor() throws Exception { + BeanFactory bf = new ClassPathXmlApplicationContext(COMMON_INTERCEPTORS_CONTEXT, CLASS); + ITestBean test1 = (ITestBean) bf.getBean("test1"); + assertThat(AopUtils.isAopProxy(test1)).isTrue(); + + Lockable lockable1 = (Lockable) test1; + NopInterceptor nop1 = (NopInterceptor) bf.getBean("nopInterceptor"); + NopInterceptor nop2 = (NopInterceptor) bf.getBean("pointcutAdvisor", Advisor.class).getAdvice(); + + ITestBean test2 = (ITestBean) bf.getBean("test2"); + Lockable lockable2 = (Lockable) test2; + + // Locking should be independent; nop is shared + assertThat(lockable1.locked()).isFalse(); + assertThat(lockable2.locked()).isFalse(); + // equals 2 calls on shared nop, because it's first and sees calls + // against the Lockable interface introduced by the specific advisor + assertThat(nop1.getCount()).isEqualTo(2); + assertThat(nop2.getCount()).isEqualTo(0); + lockable1.lock(); + assertThat(lockable1.locked()).isTrue(); + assertThat(lockable2.locked()).isFalse(); + assertThat(nop1.getCount()).isEqualTo(5); + assertThat(nop2.getCount()).isEqualTo(0); + + PackageVisibleMethod packageVisibleMethod = (PackageVisibleMethod) bf.getBean("packageVisibleMethod"); + assertThat(nop1.getCount()).isEqualTo(5); + assertThat(nop2.getCount()).isEqualTo(0); + packageVisibleMethod.doSomething(); + assertThat(nop1.getCount()).isEqualTo(6); + assertThat(nop2.getCount()).isEqualTo(1); + boolean condition = packageVisibleMethod instanceof Lockable; + assertThat(condition).isTrue(); + Lockable lockable3 = (Lockable) packageVisibleMethod; + lockable3.lock(); + assertThat(lockable3.locked()).isTrue(); + lockable3.unlock(); + assertThat(lockable3.locked()).isFalse(); + } + + /** + * We have custom TargetSourceCreators but there's no match, and + * hence no proxying, for this bean + */ + @Test + public void testCustomTargetSourceNoMatch() throws Exception { + BeanFactory bf = new ClassPathXmlApplicationContext(CUSTOM_TARGETSOURCE_CONTEXT, CLASS); + ITestBean test = (ITestBean) bf.getBean("test"); + assertThat(AopUtils.isAopProxy(test)).isFalse(); + assertThat(test.getName()).isEqualTo("Rod"); + assertThat(test.getSpouse().getName()).isEqualTo("Kerry"); + } + + @Test + public void testCustomPrototypeTargetSource() throws Exception { + CountingTestBean.count = 0; + BeanFactory bf = new ClassPathXmlApplicationContext(CUSTOM_TARGETSOURCE_CONTEXT, CLASS); + ITestBean test = (ITestBean) bf.getBean("prototypeTest"); + assertThat(AopUtils.isAopProxy(test)).isTrue(); + Advised advised = (Advised) test; + boolean condition = advised.getTargetSource() instanceof PrototypeTargetSource; + assertThat(condition).isTrue(); + assertThat(test.getName()).isEqualTo("Rod"); + // Check that references survived prototype creation + assertThat(test.getSpouse().getName()).isEqualTo("Kerry"); + assertThat(CountingTestBean.count).as("Only 2 CountingTestBeans instantiated").isEqualTo(2); + CountingTestBean.count = 0; + } + + @Test + public void testLazyInitTargetSource() throws Exception { + CountingTestBean.count = 0; + BeanFactory bf = new ClassPathXmlApplicationContext(CUSTOM_TARGETSOURCE_CONTEXT, CLASS); + ITestBean test = (ITestBean) bf.getBean("lazyInitTest"); + assertThat(AopUtils.isAopProxy(test)).isTrue(); + Advised advised = (Advised) test; + boolean condition = advised.getTargetSource() instanceof LazyInitTargetSource; + assertThat(condition).isTrue(); + assertThat(CountingTestBean.count).as("No CountingTestBean instantiated yet").isEqualTo(0); + assertThat(test.getName()).isEqualTo("Rod"); + assertThat(test.getSpouse().getName()).isEqualTo("Kerry"); + assertThat(CountingTestBean.count).as("Only 1 CountingTestBean instantiated").isEqualTo(1); + CountingTestBean.count = 0; + } + + @Test + public void testQuickTargetSourceCreator() throws Exception { + ClassPathXmlApplicationContext bf = + new ClassPathXmlApplicationContext(QUICK_TARGETSOURCE_CONTEXT, CLASS); + ITestBean test = (ITestBean) bf.getBean("test"); + assertThat(AopUtils.isAopProxy(test)).isFalse(); + assertThat(test.getName()).isEqualTo("Rod"); + // Check that references survived pooling + assertThat(test.getSpouse().getName()).isEqualTo("Kerry"); + + // Now test the pooled one + test = (ITestBean) bf.getBean(":test"); + assertThat(AopUtils.isAopProxy(test)).isTrue(); + Advised advised = (Advised) test; + boolean condition2 = advised.getTargetSource() instanceof CommonsPool2TargetSource; + assertThat(condition2).isTrue(); + assertThat(test.getName()).isEqualTo("Rod"); + // Check that references survived pooling + assertThat(test.getSpouse().getName()).isEqualTo("Kerry"); + + // Now test the ThreadLocal one + test = (ITestBean) bf.getBean("%test"); + assertThat(AopUtils.isAopProxy(test)).isTrue(); + advised = (Advised) test; + boolean condition1 = advised.getTargetSource() instanceof ThreadLocalTargetSource; + assertThat(condition1).isTrue(); + assertThat(test.getName()).isEqualTo("Rod"); + // Check that references survived pooling + assertThat(test.getSpouse().getName()).isEqualTo("Kerry"); + + // Now test the Prototype TargetSource + test = (ITestBean) bf.getBean("!test"); + assertThat(AopUtils.isAopProxy(test)).isTrue(); + advised = (Advised) test; + boolean condition = advised.getTargetSource() instanceof PrototypeTargetSource; + assertThat(condition).isTrue(); + assertThat(test.getName()).isEqualTo("Rod"); + // Check that references survived pooling + assertThat(test.getSpouse().getName()).isEqualTo("Kerry"); + + + ITestBean test2 = (ITestBean) bf.getBean("!test"); + assertThat(test == test2).as("Prototypes cannot be the same object").isFalse(); + assertThat(test2.getName()).isEqualTo("Rod"); + assertThat(test2.getSpouse().getName()).isEqualTo("Kerry"); + bf.close(); + } + + @Test + public void testWithOptimizedProxy() throws Exception { + BeanFactory beanFactory = new ClassPathXmlApplicationContext(OPTIMIZED_CONTEXT, CLASS); + + ITestBean testBean = (ITestBean) beanFactory.getBean("optimizedTestBean"); + assertThat(AopUtils.isAopProxy(testBean)).isTrue(); + + CountingBeforeAdvice beforeAdvice = (CountingBeforeAdvice) beanFactory.getBean("countingAdvice"); + + testBean.setAge(23); + testBean.getAge(); + + assertThat(beforeAdvice.getCalls()).as("Incorrect number of calls to proxy").isEqualTo(2); + } + +} + + +class SelectivePrototypeTargetSourceCreator extends AbstractBeanFactoryBasedTargetSourceCreator { + + @Override + protected AbstractBeanFactoryBasedTargetSource createBeanFactoryBasedTargetSource( + Class beanClass, String beanName) { + if (!beanName.startsWith("prototype")) { + return null; + } + return new PrototypeTargetSource(); + } + +} + diff --git a/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/AutoProxyCreatorTests.java b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/AutoProxyCreatorTests.java new file mode 100644 index 0000000..b0fde5f --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/AutoProxyCreatorTests.java @@ -0,0 +1,518 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.autoproxy; + +import java.io.Serializable; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.TargetSource; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.support.AopUtils; +import org.springframework.aop.target.SingletonTargetSource; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.IndexedTestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.beans.testfixture.beans.factory.DummyFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.MessageSource; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.context.support.StaticMessageSource; +import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @author Chris Beams + * @since 09.12.2003 + */ +@SuppressWarnings("resource") +public class AutoProxyCreatorTests { + + @Test + public void testBeanNameAutoProxyCreator() { + StaticApplicationContext sac = new StaticApplicationContext(); + sac.registerSingleton("testInterceptor", TestInterceptor.class); + + RootBeanDefinition proxyCreator = new RootBeanDefinition(BeanNameAutoProxyCreator.class); + proxyCreator.getPropertyValues().add("interceptorNames", "testInterceptor"); + proxyCreator.getPropertyValues().add("beanNames", "singletonToBeProxied,innerBean,singletonFactoryToBeProxied"); + sac.getDefaultListableBeanFactory().registerBeanDefinition("beanNameAutoProxyCreator", proxyCreator); + + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.setAutowireMode(RootBeanDefinition.AUTOWIRE_BY_TYPE); + RootBeanDefinition innerBean = new RootBeanDefinition(TestBean.class); + bd.getPropertyValues().add("spouse", new BeanDefinitionHolder(innerBean, "innerBean")); + sac.getDefaultListableBeanFactory().registerBeanDefinition("singletonToBeProxied", bd); + + sac.registerSingleton("singletonFactoryToBeProxied", DummyFactory.class); + sac.registerSingleton("autowiredIndexedTestBean", IndexedTestBean.class); + + sac.refresh(); + + MessageSource messageSource = (MessageSource) sac.getBean("messageSource"); + ITestBean singletonToBeProxied = (ITestBean) sac.getBean("singletonToBeProxied"); + assertThat(Proxy.isProxyClass(messageSource.getClass())).isFalse(); + assertThat(Proxy.isProxyClass(singletonToBeProxied.getClass())).isTrue(); + assertThat(Proxy.isProxyClass(singletonToBeProxied.getSpouse().getClass())).isTrue(); + + // test whether autowiring succeeded with auto proxy creation + assertThat(singletonToBeProxied.getNestedIndexedBean()).isEqualTo(sac.getBean("autowiredIndexedTestBean")); + + TestInterceptor ti = (TestInterceptor) sac.getBean("testInterceptor"); + // already 2: getSpouse + getNestedIndexedBean calls above + assertThat(ti.nrOfInvocations).isEqualTo(2); + singletonToBeProxied.getName(); + singletonToBeProxied.getSpouse().getName(); + assertThat(ti.nrOfInvocations).isEqualTo(5); + + ITestBean tb = (ITestBean) sac.getBean("singletonFactoryToBeProxied"); + assertThat(AopUtils.isJdkDynamicProxy(tb)).isTrue(); + assertThat(ti.nrOfInvocations).isEqualTo(5); + tb.getAge(); + assertThat(ti.nrOfInvocations).isEqualTo(6); + + ITestBean tb2 = (ITestBean) sac.getBean("singletonFactoryToBeProxied"); + assertThat(tb2).isSameAs(tb); + assertThat(ti.nrOfInvocations).isEqualTo(6); + tb2.getAge(); + assertThat(ti.nrOfInvocations).isEqualTo(7); + } + + @Test + public void testBeanNameAutoProxyCreatorWithFactoryBeanProxy() { + StaticApplicationContext sac = new StaticApplicationContext(); + sac.registerSingleton("testInterceptor", TestInterceptor.class); + + RootBeanDefinition proxyCreator = new RootBeanDefinition(BeanNameAutoProxyCreator.class); + proxyCreator.getPropertyValues().add("interceptorNames", "testInterceptor"); + proxyCreator.getPropertyValues().add("beanNames", "singletonToBeProxied,&singletonFactoryToBeProxied"); + sac.getDefaultListableBeanFactory().registerBeanDefinition("beanNameAutoProxyCreator", proxyCreator); + + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + sac.getDefaultListableBeanFactory().registerBeanDefinition("singletonToBeProxied", bd); + + sac.registerSingleton("singletonFactoryToBeProxied", DummyFactory.class); + + sac.refresh(); + + ITestBean singletonToBeProxied = (ITestBean) sac.getBean("singletonToBeProxied"); + assertThat(Proxy.isProxyClass(singletonToBeProxied.getClass())).isTrue(); + + TestInterceptor ti = (TestInterceptor) sac.getBean("testInterceptor"); + int initialNr = ti.nrOfInvocations; + singletonToBeProxied.getName(); + assertThat(ti.nrOfInvocations).isEqualTo((initialNr + 1)); + + FactoryBean factory = (FactoryBean) sac.getBean("&singletonFactoryToBeProxied"); + assertThat(Proxy.isProxyClass(factory.getClass())).isTrue(); + TestBean tb = (TestBean) sac.getBean("singletonFactoryToBeProxied"); + assertThat(AopUtils.isAopProxy(tb)).isFalse(); + assertThat(ti.nrOfInvocations).isEqualTo((initialNr + 3)); + tb.getAge(); + assertThat(ti.nrOfInvocations).isEqualTo((initialNr + 3)); + } + + @Test + public void testCustomAutoProxyCreator() { + StaticApplicationContext sac = new StaticApplicationContext(); + sac.registerSingleton("testAutoProxyCreator", TestAutoProxyCreator.class); + sac.registerSingleton("noInterfaces", NoInterfaces.class); + sac.registerSingleton("containerCallbackInterfacesOnly", ContainerCallbackInterfacesOnly.class); + sac.registerSingleton("singletonNoInterceptor", TestBean.class); + sac.registerSingleton("singletonToBeProxied", TestBean.class); + sac.registerPrototype("prototypeToBeProxied", TestBean.class); + sac.refresh(); + + MessageSource messageSource = (MessageSource) sac.getBean("messageSource"); + NoInterfaces noInterfaces = (NoInterfaces) sac.getBean("noInterfaces"); + ContainerCallbackInterfacesOnly containerCallbackInterfacesOnly = + (ContainerCallbackInterfacesOnly) sac.getBean("containerCallbackInterfacesOnly"); + ITestBean singletonNoInterceptor = (ITestBean) sac.getBean("singletonNoInterceptor"); + ITestBean singletonToBeProxied = (ITestBean) sac.getBean("singletonToBeProxied"); + ITestBean prototypeToBeProxied = (ITestBean) sac.getBean("prototypeToBeProxied"); + assertThat(AopUtils.isCglibProxy(messageSource)).isFalse(); + assertThat(AopUtils.isCglibProxy(noInterfaces)).isTrue(); + assertThat(AopUtils.isCglibProxy(containerCallbackInterfacesOnly)).isTrue(); + assertThat(AopUtils.isCglibProxy(singletonNoInterceptor)).isTrue(); + assertThat(AopUtils.isCglibProxy(singletonToBeProxied)).isTrue(); + assertThat(AopUtils.isCglibProxy(prototypeToBeProxied)).isTrue(); + + TestAutoProxyCreator tapc = (TestAutoProxyCreator) sac.getBean("testAutoProxyCreator"); + assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(0); + singletonNoInterceptor.getName(); + assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(0); + singletonToBeProxied.getAge(); + assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(1); + prototypeToBeProxied.getSpouse(); + assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(2); + } + + @Test + public void testAutoProxyCreatorWithFallbackToTargetClass() { + StaticApplicationContext sac = new StaticApplicationContext(); + sac.registerSingleton("testAutoProxyCreator", FallbackTestAutoProxyCreator.class); + sac.registerSingleton("noInterfaces", NoInterfaces.class); + sac.registerSingleton("containerCallbackInterfacesOnly", ContainerCallbackInterfacesOnly.class); + sac.registerSingleton("singletonNoInterceptor", TestBean.class); + sac.registerSingleton("singletonToBeProxied", TestBean.class); + sac.registerPrototype("prototypeToBeProxied", TestBean.class); + sac.refresh(); + + MessageSource messageSource = (MessageSource) sac.getBean("messageSource"); + NoInterfaces noInterfaces = (NoInterfaces) sac.getBean("noInterfaces"); + ContainerCallbackInterfacesOnly containerCallbackInterfacesOnly = + (ContainerCallbackInterfacesOnly) sac.getBean("containerCallbackInterfacesOnly"); + ITestBean singletonNoInterceptor = (ITestBean) sac.getBean("singletonNoInterceptor"); + ITestBean singletonToBeProxied = (ITestBean) sac.getBean("singletonToBeProxied"); + ITestBean prototypeToBeProxied = (ITestBean) sac.getBean("prototypeToBeProxied"); + assertThat(AopUtils.isCglibProxy(messageSource)).isFalse(); + assertThat(AopUtils.isCglibProxy(noInterfaces)).isTrue(); + assertThat(AopUtils.isCglibProxy(containerCallbackInterfacesOnly)).isTrue(); + assertThat(AopUtils.isCglibProxy(singletonNoInterceptor)).isFalse(); + assertThat(AopUtils.isCglibProxy(singletonToBeProxied)).isFalse(); + assertThat(AopUtils.isCglibProxy(prototypeToBeProxied)).isFalse(); + + TestAutoProxyCreator tapc = (TestAutoProxyCreator) sac.getBean("testAutoProxyCreator"); + assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(0); + singletonNoInterceptor.getName(); + assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(0); + singletonToBeProxied.getAge(); + assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(1); + prototypeToBeProxied.getSpouse(); + assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(2); + } + + @Test + public void testAutoProxyCreatorWithFallbackToDynamicProxy() { + StaticApplicationContext sac = new StaticApplicationContext(); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("proxyFactoryBean", "false"); + sac.registerSingleton("testAutoProxyCreator", TestAutoProxyCreator.class, pvs); + + sac.registerSingleton("noInterfaces", NoInterfaces.class); + sac.registerSingleton("containerCallbackInterfacesOnly", ContainerCallbackInterfacesOnly.class); + sac.registerSingleton("singletonNoInterceptor", CustomProxyFactoryBean.class); + sac.registerSingleton("singletonToBeProxied", CustomProxyFactoryBean.class); + sac.registerPrototype("prototypeToBeProxied", SpringProxyFactoryBean.class); + + sac.refresh(); + + MessageSource messageSource = (MessageSource) sac.getBean("messageSource"); + NoInterfaces noInterfaces = (NoInterfaces) sac.getBean("noInterfaces"); + ContainerCallbackInterfacesOnly containerCallbackInterfacesOnly = + (ContainerCallbackInterfacesOnly) sac.getBean("containerCallbackInterfacesOnly"); + ITestBean singletonNoInterceptor = (ITestBean) sac.getBean("singletonNoInterceptor"); + ITestBean singletonToBeProxied = (ITestBean) sac.getBean("singletonToBeProxied"); + ITestBean prototypeToBeProxied = (ITestBean) sac.getBean("prototypeToBeProxied"); + assertThat(AopUtils.isCglibProxy(messageSource)).isFalse(); + assertThat(AopUtils.isCglibProxy(noInterfaces)).isTrue(); + assertThat(AopUtils.isCglibProxy(containerCallbackInterfacesOnly)).isTrue(); + assertThat(AopUtils.isCglibProxy(singletonNoInterceptor)).isFalse(); + assertThat(AopUtils.isCglibProxy(singletonToBeProxied)).isFalse(); + assertThat(AopUtils.isCglibProxy(prototypeToBeProxied)).isFalse(); + + TestAutoProxyCreator tapc = (TestAutoProxyCreator) sac.getBean("testAutoProxyCreator"); + assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(0); + singletonNoInterceptor.getName(); + assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(0); + singletonToBeProxied.getAge(); + assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(1); + prototypeToBeProxied.getSpouse(); + assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(2); + } + + @Test + public void testAutoProxyCreatorWithPackageVisibleMethod() { + StaticApplicationContext sac = new StaticApplicationContext(); + sac.registerSingleton("testAutoProxyCreator", TestAutoProxyCreator.class); + sac.registerSingleton("packageVisibleMethodToBeProxied", PackageVisibleMethod.class); + sac.refresh(); + + TestAutoProxyCreator tapc = (TestAutoProxyCreator) sac.getBean("testAutoProxyCreator"); + tapc.testInterceptor.nrOfInvocations = 0; + + PackageVisibleMethod tb = (PackageVisibleMethod) sac.getBean("packageVisibleMethodToBeProxied"); + assertThat(AopUtils.isCglibProxy(tb)).isTrue(); + assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(0); + tb.doSomething(); + assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(1); + } + + @Test + public void testAutoProxyCreatorWithFactoryBean() { + StaticApplicationContext sac = new StaticApplicationContext(); + sac.registerSingleton("testAutoProxyCreator", TestAutoProxyCreator.class); + sac.registerSingleton("singletonFactoryToBeProxied", DummyFactory.class); + sac.refresh(); + + TestAutoProxyCreator tapc = (TestAutoProxyCreator) sac.getBean("testAutoProxyCreator"); + tapc.testInterceptor.nrOfInvocations = 0; + + FactoryBean factory = (FactoryBean) sac.getBean("&singletonFactoryToBeProxied"); + assertThat(AopUtils.isCglibProxy(factory)).isTrue(); + + TestBean tb = (TestBean) sac.getBean("singletonFactoryToBeProxied"); + assertThat(AopUtils.isCglibProxy(tb)).isTrue(); + assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(2); + tb.getAge(); + assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(3); + } + + @Test + public void testAutoProxyCreatorWithFactoryBeanAndPrototype() { + StaticApplicationContext sac = new StaticApplicationContext(); + sac.registerSingleton("testAutoProxyCreator", TestAutoProxyCreator.class); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("singleton", "false"); + sac.registerSingleton("prototypeFactoryToBeProxied", DummyFactory.class, pvs); + + sac.refresh(); + + TestAutoProxyCreator tapc = (TestAutoProxyCreator) sac.getBean("testAutoProxyCreator"); + tapc.testInterceptor.nrOfInvocations = 0; + + FactoryBean prototypeFactory = (FactoryBean) sac.getBean("&prototypeFactoryToBeProxied"); + assertThat(AopUtils.isCglibProxy(prototypeFactory)).isTrue(); + TestBean tb = (TestBean) sac.getBean("prototypeFactoryToBeProxied"); + assertThat(AopUtils.isCglibProxy(tb)).isTrue(); + + assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(2); + tb.getAge(); + assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(3); + } + + @Test + public void testAutoProxyCreatorWithFactoryBeanAndProxyObjectOnly() { + StaticApplicationContext sac = new StaticApplicationContext(); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("proxyFactoryBean", "false"); + sac.registerSingleton("testAutoProxyCreator", TestAutoProxyCreator.class, pvs); + + sac.registerSingleton("singletonFactoryToBeProxied", DummyFactory.class); + + sac.refresh(); + + TestAutoProxyCreator tapc = (TestAutoProxyCreator) sac.getBean("testAutoProxyCreator"); + tapc.testInterceptor.nrOfInvocations = 0; + + FactoryBean factory = (FactoryBean) sac.getBean("&singletonFactoryToBeProxied"); + assertThat(AopUtils.isAopProxy(factory)).isFalse(); + + TestBean tb = (TestBean) sac.getBean("singletonFactoryToBeProxied"); + assertThat(AopUtils.isCglibProxy(tb)).isTrue(); + assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(0); + tb.getAge(); + assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(1); + + TestBean tb2 = (TestBean) sac.getBean("singletonFactoryToBeProxied"); + assertThat(tb2).isSameAs(tb); + assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(1); + tb2.getAge(); + assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(2); + } + + @Test + public void testAutoProxyCreatorWithFactoryBeanAndProxyFactoryBeanOnly() { + StaticApplicationContext sac = new StaticApplicationContext(); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("proxyObject", "false"); + sac.registerSingleton("testAutoProxyCreator", TestAutoProxyCreator.class, pvs); + + pvs = new MutablePropertyValues(); + pvs.add("singleton", "false"); + sac.registerSingleton("prototypeFactoryToBeProxied", DummyFactory.class, pvs); + + sac.refresh(); + + TestAutoProxyCreator tapc = (TestAutoProxyCreator) sac.getBean("testAutoProxyCreator"); + tapc.testInterceptor.nrOfInvocations = 0; + + FactoryBean prototypeFactory = (FactoryBean) sac.getBean("&prototypeFactoryToBeProxied"); + assertThat(AopUtils.isCglibProxy(prototypeFactory)).isTrue(); + TestBean tb = (TestBean) sac.getBean("prototypeFactoryToBeProxied"); + assertThat(AopUtils.isCglibProxy(tb)).isFalse(); + + assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(2); + tb.getAge(); + assertThat(tapc.testInterceptor.nrOfInvocations).isEqualTo(2); + } + + + @SuppressWarnings("serial") + public static class TestAutoProxyCreator extends AbstractAutoProxyCreator { + + private boolean proxyFactoryBean = true; + + private boolean proxyObject = true; + + public TestInterceptor testInterceptor = new TestInterceptor(); + + public TestAutoProxyCreator() { + setProxyTargetClass(true); + setOrder(0); + } + + public void setProxyFactoryBean(boolean proxyFactoryBean) { + this.proxyFactoryBean = proxyFactoryBean; + } + + public void setProxyObject(boolean proxyObject) { + this.proxyObject = proxyObject; + } + + @Override + @Nullable + protected Object[] getAdvicesAndAdvisorsForBean(Class beanClass, String name, @Nullable TargetSource customTargetSource) { + if (StaticMessageSource.class.equals(beanClass)) { + return DO_NOT_PROXY; + } + else if (name.endsWith("ToBeProxied")) { + boolean isFactoryBean = FactoryBean.class.isAssignableFrom(beanClass); + if ((this.proxyFactoryBean && isFactoryBean) || (this.proxyObject && !isFactoryBean)) { + return new Object[] {this.testInterceptor}; + } + else { + return DO_NOT_PROXY; + } + } + else { + return PROXY_WITHOUT_ADDITIONAL_INTERCEPTORS; + } + } + } + + + @SuppressWarnings("serial") + public static class FallbackTestAutoProxyCreator extends TestAutoProxyCreator { + + public FallbackTestAutoProxyCreator() { + setProxyTargetClass(false); + } + } + + + /** + * Interceptor that counts the number of non-finalize method calls. + */ + public static class TestInterceptor implements MethodInterceptor { + + public int nrOfInvocations = 0; + + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + if (!invocation.getMethod().getName().equals("finalize")) { + this.nrOfInvocations++; + } + return invocation.proceed(); + } + } + + + public static class NoInterfaces { + } + + + @SuppressWarnings("serial") + public static class ContainerCallbackInterfacesOnly // as well as an empty marker interface + implements BeanFactoryAware, ApplicationContextAware, InitializingBean, DisposableBean, Serializable { + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + } + + @Override + public void afterPropertiesSet() { + } + + @Override + public void destroy() { + } + } + + + public static class CustomProxyFactoryBean implements FactoryBean { + + private final TestBean tb = new TestBean(); + + @Override + public ITestBean getObject() { + return (ITestBean) Proxy.newProxyInstance(CustomProxyFactoryBean.class.getClassLoader(), new Class[]{ITestBean.class}, new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + return ReflectionUtils.invokeMethod(method, tb, args); + } + }); + } + + @Override + public Class getObjectType() { + return ITestBean.class; + } + + @Override + public boolean isSingleton() { + return false; + } + } + + + public static class SpringProxyFactoryBean implements FactoryBean { + + private final TestBean tb = new TestBean(); + + @Override + public ITestBean getObject() { + return ProxyFactory.getProxy(ITestBean.class, new SingletonTargetSource(tb)); + } + + @Override + public Class getObjectType() { + return ITestBean.class; + } + + @Override + public boolean isSingleton() { + return false; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorInitTests.java b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorInitTests.java new file mode 100644 index 0000000..71d49ff --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorInitTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.autoproxy; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.MethodBeforeAdvice; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Juergen Hoeller + * @author Dave Syer + * @author Chris Beams + */ +public class BeanNameAutoProxyCreatorInitTests { + + @Test + public void testIgnoreAdvisorThatIsCurrentlyInCreation() { + ClassPathXmlApplicationContext ctx = + new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-context.xml", getClass()); + TestBean bean = (TestBean) ctx.getBean("bean"); + bean.setName("foo"); + assertThat(bean.getName()).isEqualTo("foo"); + assertThatIllegalArgumentException().isThrownBy(() -> + bean.setName(null)); + } + +} + + +class NullChecker implements MethodBeforeAdvice { + + @Override + public void before(Method method, Object[] args, @Nullable Object target) throws Throwable { + check(args); + } + + private void check(Object[] args) { + for (int i = 0; i < args.length; i++) { + if (args[i] == null) { + throw new IllegalArgumentException("Null argument at position " + i); + } + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorTests.java b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorTests.java new file mode 100644 index 0000000..153080c --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorTests.java @@ -0,0 +1,215 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.autoproxy; + +import org.junit.jupiter.api.Test; +import test.mixin.Lockable; +import test.mixin.LockedException; + +import org.springframework.aop.framework.Advised; +import org.springframework.aop.support.AopUtils; +import org.springframework.aop.testfixture.advice.CountingBeforeAdvice; +import org.springframework.aop.testfixture.interceptor.NopInterceptor; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.core.testfixture.TimeStamped; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Rod Johnson + * @author Rob Harrop + * @author Chris Beams + */ +class BeanNameAutoProxyCreatorTests { + + // Note that we need an ApplicationContext, not just a BeanFactory, + // for post-processing and hence auto-proxying to work. + private final BeanFactory beanFactory = new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-context.xml", getClass()); + + + @Test + void noProxy() { + TestBean tb = (TestBean) beanFactory.getBean("noproxy"); + assertThat(AopUtils.isAopProxy(tb)).isFalse(); + assertThat(tb.getName()).isEqualTo("noproxy"); + } + + @Test + void proxyWithExactNameMatch() { + ITestBean tb = (ITestBean) beanFactory.getBean("onlyJdk"); + jdkAssertions(tb, 1); + assertThat(tb.getName()).isEqualTo("onlyJdk"); + } + + @Test + void proxyWithDoubleProxying() { + ITestBean tb = (ITestBean) beanFactory.getBean("doubleJdk"); + jdkAssertions(tb, 2); + assertThat(tb.getName()).isEqualTo("doubleJdk"); + } + + @Test + void jdkIntroduction() { + ITestBean tb = (ITestBean) beanFactory.getBean("introductionUsingJdk"); + NopInterceptor nop = (NopInterceptor) beanFactory.getBean("introductionNopInterceptor"); + assertThat(nop.getCount()).isEqualTo(0); + assertThat(AopUtils.isJdkDynamicProxy(tb)).isTrue(); + int age = 5; + tb.setAge(age); + assertThat(tb.getAge()).isEqualTo(age); + boolean condition = tb instanceof TimeStamped; + assertThat(condition).as("Introduction was made").isTrue(); + assertThat(((TimeStamped) tb).getTimeStamp()).isEqualTo(0); + assertThat(nop.getCount()).isEqualTo(3); + assertThat(tb.getName()).isEqualTo("introductionUsingJdk"); + + ITestBean tb2 = (ITestBean) beanFactory.getBean("second-introductionUsingJdk"); + + // Check two per-instance mixins were distinct + Lockable lockable1 = (Lockable) tb; + Lockable lockable2 = (Lockable) tb2; + assertThat(lockable1.locked()).isFalse(); + assertThat(lockable2.locked()).isFalse(); + tb.setAge(65); + assertThat(tb.getAge()).isEqualTo(65); + lockable1.lock(); + assertThat(lockable1.locked()).isTrue(); + // Shouldn't affect second + assertThat(lockable2.locked()).isFalse(); + // Can still mod second object + tb2.setAge(12); + // But can't mod first + assertThatExceptionOfType(LockedException.class).as("mixin should have locked this object").isThrownBy(() -> + tb.setAge(6)); + } + + @Test + void jdkIntroductionAppliesToCreatedObjectsNotFactoryBean() { + ITestBean tb = (ITestBean) beanFactory.getBean("factory-introductionUsingJdk"); + NopInterceptor nop = (NopInterceptor) beanFactory.getBean("introductionNopInterceptor"); + assertThat(nop.getCount()).as("NOP should not have done any work yet").isEqualTo(0); + assertThat(AopUtils.isJdkDynamicProxy(tb)).isTrue(); + int age = 5; + tb.setAge(age); + assertThat(tb.getAge()).isEqualTo(age); + assertThat(tb).as("Introduction was made").isInstanceOf(TimeStamped.class); + assertThat(((TimeStamped) tb).getTimeStamp()).isEqualTo(0); + assertThat(nop.getCount()).isEqualTo(3); + + ITestBean tb2 = (ITestBean) beanFactory.getBean("second-introductionUsingJdk"); + + // Check two per-instance mixins were distinct + Lockable lockable1 = (Lockable) tb; + Lockable lockable2 = (Lockable) tb2; + assertThat(lockable1.locked()).isFalse(); + assertThat(lockable2.locked()).isFalse(); + tb.setAge(65); + assertThat(tb.getAge()).isEqualTo(65); + lockable1.lock(); + assertThat(lockable1.locked()).isTrue(); + // Shouldn't affect second + assertThat(lockable2.locked()).isFalse(); + // Can still mod second object + tb2.setAge(12); + // But can't mod first + assertThatExceptionOfType(LockedException.class).as("mixin should have locked this object").isThrownBy(() -> + tb.setAge(6)); + } + + @Test + void proxyWithWildcardMatch() { + ITestBean tb = (ITestBean) beanFactory.getBean("jdk1"); + jdkAssertions(tb, 1); + assertThat(tb.getName()).isEqualTo("jdk1"); + } + + @Test + void cglibProxyWithWildcardMatch() { + TestBean tb = (TestBean) beanFactory.getBean("cglib1"); + cglibAssertions(tb); + assertThat(tb.getName()).isEqualTo("cglib1"); + } + + @Test + void withFrozenProxy() { + ITestBean testBean = (ITestBean) beanFactory.getBean("frozenBean"); + assertThat(((Advised)testBean).isFrozen()).isTrue(); + } + + @Test + void customTargetSourceCreatorsApplyOnlyToConfiguredBeanNames() { + ITestBean lazy1 = beanFactory.getBean("lazy1", ITestBean.class); + ITestBean alias1 = beanFactory.getBean("lazy1alias", ITestBean.class); + ITestBean lazy2 = beanFactory.getBean("lazy2", ITestBean.class); + assertThat(AopUtils.isAopProxy(lazy1)).isTrue(); + assertThat(AopUtils.isAopProxy(alias1)).isTrue(); + assertThat(AopUtils.isAopProxy(lazy2)).isFalse(); + } + + + private void jdkAssertions(ITestBean tb, int nopInterceptorCount) { + NopInterceptor nop = (NopInterceptor) beanFactory.getBean("nopInterceptor"); + assertThat(nop.getCount()).isEqualTo(0); + assertThat(AopUtils.isJdkDynamicProxy(tb)).isTrue(); + int age = 5; + tb.setAge(age); + assertThat(tb.getAge()).isEqualTo(age); + assertThat(nop.getCount()).isEqualTo((2 * nopInterceptorCount)); + } + + /** + * Also has counting before advice. + */ + private void cglibAssertions(TestBean tb) { + CountingBeforeAdvice cba = (CountingBeforeAdvice) beanFactory.getBean("countingBeforeAdvice"); + NopInterceptor nop = (NopInterceptor) beanFactory.getBean("nopInterceptor"); + assertThat(cba.getCalls()).isEqualTo(0); + assertThat(nop.getCount()).isEqualTo(0); + assertThat(AopUtils.isCglibProxy(tb)).isTrue(); + int age = 5; + tb.setAge(age); + assertThat(tb.getAge()).isEqualTo(age); + assertThat(nop.getCount()).isEqualTo(2); + assertThat(cba.getCalls()).isEqualTo(2); + } + +} + + +class CreatesTestBean implements FactoryBean { + + @Override + public Object getObject() throws Exception { + return new TestBean(); + } + + @Override + public Class getObjectType() { + return TestBean.class; + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/PackageVisibleMethod.java b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/PackageVisibleMethod.java new file mode 100644 index 0000000..976c3d8 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/PackageVisibleMethod.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.framework.autoproxy; + +public class PackageVisibleMethod { + + void doSomething() { + } + +} diff --git a/spring-context/src/test/java/org/springframework/aop/scope/ScopedProxyTests.java b/spring-context/src/test/java/org/springframework/aop/scope/ScopedProxyTests.java new file mode 100644 index 0000000..1f21584 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/aop/scope/ScopedProxyTests.java @@ -0,0 +1,152 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.aop.scope; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.context.testfixture.SimpleMapScope; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.testfixture.io.SerializationTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + * @author Juergen Hoeller + * @author Chris Beams + */ +public class ScopedProxyTests { + + private static final Class CLASS = ScopedProxyTests.class; + private static final String CLASSNAME = CLASS.getSimpleName(); + + private static final ClassPathResource LIST_CONTEXT = new ClassPathResource(CLASSNAME + "-list.xml", CLASS); + private static final ClassPathResource MAP_CONTEXT = new ClassPathResource(CLASSNAME + "-map.xml", CLASS); + private static final ClassPathResource OVERRIDE_CONTEXT = new ClassPathResource(CLASSNAME + "-override.xml", CLASS); + private static final ClassPathResource TESTBEAN_CONTEXT = new ClassPathResource(CLASSNAME + "-testbean.xml", CLASS); + + + @Test // SPR-2108 + public void testProxyAssignable() throws Exception { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions(MAP_CONTEXT); + Object baseMap = bf.getBean("singletonMap"); + boolean condition = baseMap instanceof Map; + assertThat(condition).isTrue(); + } + + @Test + public void testSimpleProxy() throws Exception { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions(MAP_CONTEXT); + Object simpleMap = bf.getBean("simpleMap"); + boolean condition1 = simpleMap instanceof Map; + assertThat(condition1).isTrue(); + boolean condition = simpleMap instanceof HashMap; + assertThat(condition).isTrue(); + } + + @Test + public void testScopedOverride() throws Exception { + GenericApplicationContext ctx = new GenericApplicationContext(); + new XmlBeanDefinitionReader(ctx).loadBeanDefinitions(OVERRIDE_CONTEXT); + SimpleMapScope scope = new SimpleMapScope(); + ctx.getBeanFactory().registerScope("request", scope); + ctx.refresh(); + + ITestBean bean = (ITestBean) ctx.getBean("testBean"); + assertThat(bean.getName()).isEqualTo("male"); + assertThat(bean.getAge()).isEqualTo(99); + + assertThat(scope.getMap().containsKey("scopedTarget.testBean")).isTrue(); + assertThat(scope.getMap().get("scopedTarget.testBean").getClass()).isEqualTo(TestBean.class); + } + + @Test + public void testJdkScopedProxy() throws Exception { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions(TESTBEAN_CONTEXT); + bf.setSerializationId("X"); + SimpleMapScope scope = new SimpleMapScope(); + bf.registerScope("request", scope); + + ITestBean bean = (ITestBean) bf.getBean("testBean"); + assertThat(bean).isNotNull(); + assertThat(AopUtils.isJdkDynamicProxy(bean)).isTrue(); + boolean condition1 = bean instanceof ScopedObject; + assertThat(condition1).isTrue(); + ScopedObject scoped = (ScopedObject) bean; + assertThat(scoped.getTargetObject().getClass()).isEqualTo(TestBean.class); + bean.setAge(101); + + assertThat(scope.getMap().containsKey("testBeanTarget")).isTrue(); + assertThat(scope.getMap().get("testBeanTarget").getClass()).isEqualTo(TestBean.class); + + ITestBean deserialized = SerializationTestUtils.serializeAndDeserialize(bean); + assertThat(deserialized).isNotNull(); + assertThat(AopUtils.isJdkDynamicProxy(deserialized)).isTrue(); + assertThat(bean.getAge()).isEqualTo(101); + boolean condition = deserialized instanceof ScopedObject; + assertThat(condition).isTrue(); + ScopedObject scopedDeserialized = (ScopedObject) deserialized; + assertThat(scopedDeserialized.getTargetObject().getClass()).isEqualTo(TestBean.class); + + bf.setSerializationId(null); + } + + @Test + public void testCglibScopedProxy() throws Exception { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions(LIST_CONTEXT); + bf.setSerializationId("Y"); + SimpleMapScope scope = new SimpleMapScope(); + bf.registerScope("request", scope); + + TestBean tb = (TestBean) bf.getBean("testBean"); + assertThat(AopUtils.isCglibProxy(tb.getFriends())).isTrue(); + boolean condition1 = tb.getFriends() instanceof ScopedObject; + assertThat(condition1).isTrue(); + ScopedObject scoped = (ScopedObject) tb.getFriends(); + assertThat(scoped.getTargetObject().getClass()).isEqualTo(ArrayList.class); + tb.getFriends().add("myFriend"); + + assertThat(scope.getMap().containsKey("scopedTarget.scopedList")).isTrue(); + assertThat(scope.getMap().get("scopedTarget.scopedList").getClass()).isEqualTo(ArrayList.class); + + ArrayList deserialized = (ArrayList) SerializationTestUtils.serializeAndDeserialize(tb.getFriends()); + assertThat(deserialized).isNotNull(); + assertThat(AopUtils.isCglibProxy(deserialized)).isTrue(); + assertThat(deserialized.contains("myFriend")).isTrue(); + boolean condition = deserialized instanceof ScopedObject; + assertThat(condition).isTrue(); + ScopedObject scopedDeserialized = (ScopedObject) deserialized; + assertThat(scopedDeserialized.getTargetObject().getClass()).isEqualTo(ArrayList.class); + + bf.setSerializationId(null); + } + +} diff --git a/spring-context/src/test/java/org/springframework/beans/factory/annotation/BridgeMethodAutowiringTests.java b/spring-context/src/test/java/org/springframework/beans/factory/annotation/BridgeMethodAutowiringTests.java new file mode 100644 index 0000000..0be289d --- /dev/null +++ b/spring-context/src/test/java/org/springframework/beans/factory/annotation/BridgeMethodAutowiringTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.annotation; + +import javax.inject.Inject; +import javax.inject.Named; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.stereotype.Component; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BridgeMethodAutowiringTests { + + @Test + public void SPR8434() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(UserServiceImpl.class, Foo.class); + ctx.refresh(); + assertThat(ctx.getBean(UserServiceImpl.class).object).isNotNull(); + } + + + static abstract class GenericServiceImpl { + + public abstract void setObject(D object); + } + + + public static class UserServiceImpl extends GenericServiceImpl { + + protected Foo object; + + @Override + @Inject + @Named("userObject") + public void setObject(Foo object) { + if (this.object != null) { + throw new IllegalStateException("Already called"); + } + this.object = object; + } + } + + + @Component("userObject") + public static class Foo { + } + +} diff --git a/spring-context/src/test/java/org/springframework/beans/factory/support/InjectAnnotationAutowireContextTests.java b/spring-context/src/test/java/org/springframework/beans/factory/support/InjectAnnotationAutowireContextTests.java new file mode 100644 index 0000000..b36639a --- /dev/null +++ b/spring-context/src/test/java/org/springframework/beans/factory/support/InjectAnnotationAutowireContextTests.java @@ -0,0 +1,666 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Qualifier; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.scope.ScopedProxyUtils; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.UnsatisfiedDependencyException; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.context.annotation.AnnotationConfigUtils; +import org.springframework.context.support.GenericApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Integration tests for handling JSR-303 {@link javax.inject.Qualifier} annotations. + * + * @author Juergen Hoeller + * @since 3.0 + */ +public class InjectAnnotationAutowireContextTests { + + private static final String JUERGEN = "juergen"; + + private static final String MARK = "mark"; + + + @Test + public void testAutowiredFieldWithSingleNonQualifiedCandidate() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs = new ConstructorArgumentValues(); + cavs.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person = new RootBeanDefinition(Person.class, cavs, null); + context.registerBeanDefinition(JUERGEN, person); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedFieldTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy( + context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); + } + + @Test + public void testAutowiredMethodParameterWithSingleNonQualifiedCandidate() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs = new ConstructorArgumentValues(); + cavs.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person = new RootBeanDefinition(Person.class, cavs, null); + context.registerBeanDefinition(JUERGEN, person); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedMethodParameterTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy( + context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); + } + + @Test + public void testAutowiredConstructorArgumentWithSingleNonQualifiedCandidate() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs = new ConstructorArgumentValues(); + cavs.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person = new RootBeanDefinition(Person.class, cavs, null); + context.registerBeanDefinition(JUERGEN, person); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedConstructorArgumentTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy( + context::refresh) + .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("autowired")); + } + + @Test + public void testAutowiredFieldWithSingleQualifiedCandidate() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs = new ConstructorArgumentValues(); + cavs.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person = new RootBeanDefinition(Person.class, cavs, null); + person.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); + context.registerBeanDefinition(JUERGEN, person); + context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedFieldTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + context.refresh(); + QualifiedFieldTestBean bean = (QualifiedFieldTestBean) context.getBean("autowired"); + assertThat(bean.getPerson().getName()).isEqualTo(JUERGEN); + } + + @Test + public void testAutowiredMethodParameterWithSingleQualifiedCandidate() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs = new ConstructorArgumentValues(); + cavs.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person = new RootBeanDefinition(Person.class, cavs, null); + person.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); + context.registerBeanDefinition(JUERGEN, person); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedMethodParameterTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + context.refresh(); + QualifiedMethodParameterTestBean bean = + (QualifiedMethodParameterTestBean) context.getBean("autowired"); + assertThat(bean.getPerson().getName()).isEqualTo(JUERGEN); + } + + @Test + public void testAutowiredMethodParameterWithStaticallyQualifiedCandidate() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs = new ConstructorArgumentValues(); + cavs.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person = new RootBeanDefinition(QualifiedPerson.class, cavs, null); + context.registerBeanDefinition(JUERGEN, + ScopedProxyUtils.createScopedProxy(new BeanDefinitionHolder(person, JUERGEN), context, true).getBeanDefinition()); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedMethodParameterTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + context.refresh(); + QualifiedMethodParameterTestBean bean = + (QualifiedMethodParameterTestBean) context.getBean("autowired"); + assertThat(bean.getPerson().getName()).isEqualTo(JUERGEN); + } + + @Test + public void testAutowiredMethodParameterWithStaticallyQualifiedCandidateAmongOthers() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs = new ConstructorArgumentValues(); + cavs.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person = new RootBeanDefinition(QualifiedPerson.class, cavs, null); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + context.registerBeanDefinition(JUERGEN, person); + context.registerBeanDefinition(MARK, person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedMethodParameterTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + context.refresh(); + QualifiedMethodParameterTestBean bean = + (QualifiedMethodParameterTestBean) context.getBean("autowired"); + assertThat(bean.getPerson().getName()).isEqualTo(JUERGEN); + } + + @Test + public void testAutowiredConstructorArgumentWithSingleQualifiedCandidate() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs = new ConstructorArgumentValues(); + cavs.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person = new RootBeanDefinition(Person.class, cavs, null); + person.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); + context.registerBeanDefinition(JUERGEN, person); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedConstructorArgumentTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + context.refresh(); + QualifiedConstructorArgumentTestBean bean = + (QualifiedConstructorArgumentTestBean) context.getBean("autowired"); + assertThat(bean.getPerson().getName()).isEqualTo(JUERGEN); + } + + @Test + public void testAutowiredFieldWithMultipleNonQualifiedCandidates() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + context.registerBeanDefinition(JUERGEN, person1); + context.registerBeanDefinition(MARK, person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedFieldTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy( + context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); + } + + @Test + public void testAutowiredMethodParameterWithMultipleNonQualifiedCandidates() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + context.registerBeanDefinition(JUERGEN, person1); + context.registerBeanDefinition(MARK, person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedMethodParameterTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy( + context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); + } + + @Test + public void testAutowiredConstructorArgumentWithMultipleNonQualifiedCandidates() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + context.registerBeanDefinition(JUERGEN, person1); + context.registerBeanDefinition(MARK, person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedConstructorArgumentTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy( + context::refresh) + .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("autowired")); + } + + @Test + public void testAutowiredFieldResolvesQualifiedCandidate() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + person1.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + context.registerBeanDefinition(JUERGEN, person1); + context.registerBeanDefinition(MARK, person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedFieldTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + context.refresh(); + QualifiedFieldTestBean bean = (QualifiedFieldTestBean) context.getBean("autowired"); + assertThat(bean.getPerson().getName()).isEqualTo(JUERGEN); + } + + @Test + public void testAutowiredMethodParameterResolvesQualifiedCandidate() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + person1.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + context.registerBeanDefinition(JUERGEN, person1); + context.registerBeanDefinition(MARK, person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedMethodParameterTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + context.refresh(); + QualifiedMethodParameterTestBean bean = + (QualifiedMethodParameterTestBean) context.getBean("autowired"); + assertThat(bean.getPerson().getName()).isEqualTo(JUERGEN); + } + + @Test + public void testAutowiredConstructorArgumentResolvesQualifiedCandidate() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + person1.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + context.registerBeanDefinition(JUERGEN, person1); + context.registerBeanDefinition(MARK, person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedConstructorArgumentTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + context.refresh(); + QualifiedConstructorArgumentTestBean bean = + (QualifiedConstructorArgumentTestBean) context.getBean("autowired"); + assertThat(bean.getPerson().getName()).isEqualTo(JUERGEN); + } + + @Test + public void testAutowiredFieldResolvesQualifiedCandidateWithDefaultValueAndNoValueOnBeanDefinition() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + // qualifier added, but includes no value + person1.addQualifier(new AutowireCandidateQualifier(TestQualifierWithDefaultValue.class)); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + context.registerBeanDefinition(JUERGEN, person1); + context.registerBeanDefinition(MARK, person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedFieldWithDefaultValueTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + context.refresh(); + QualifiedFieldWithDefaultValueTestBean bean = + (QualifiedFieldWithDefaultValueTestBean) context.getBean("autowired"); + assertThat(bean.getPerson().getName()).isEqualTo(JUERGEN); + } + + @Test + public void testAutowiredFieldDoesNotResolveCandidateWithDefaultValueAndConflictingValueOnBeanDefinition() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + // qualifier added, and non-default value specified + person1.addQualifier(new AutowireCandidateQualifier(TestQualifierWithDefaultValue.class, "not the default")); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + context.registerBeanDefinition(JUERGEN, person1); + context.registerBeanDefinition(MARK, person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedFieldWithDefaultValueTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy( + context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); + } + + @Test + public void testAutowiredFieldResolvesWithDefaultValueAndExplicitDefaultValueOnBeanDefinition() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + // qualifier added, and value matches the default + person1.addQualifier(new AutowireCandidateQualifier(TestQualifierWithDefaultValue.class, "default")); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + context.registerBeanDefinition(JUERGEN, person1); + context.registerBeanDefinition(MARK, person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedFieldWithDefaultValueTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + context.refresh(); + QualifiedFieldWithDefaultValueTestBean bean = + (QualifiedFieldWithDefaultValueTestBean) context.getBean("autowired"); + assertThat(bean.getPerson().getName()).isEqualTo(JUERGEN); + } + + @Test + public void testAutowiredFieldResolvesWithMultipleQualifierValues() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + AutowireCandidateQualifier qualifier = new AutowireCandidateQualifier(TestQualifierWithMultipleAttributes.class); + qualifier.setAttribute("number", 456); + person1.addQualifier(qualifier); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + AutowireCandidateQualifier qualifier2 = new AutowireCandidateQualifier(TestQualifierWithMultipleAttributes.class); + qualifier2.setAttribute("number", 123); + person2.addQualifier(qualifier2); + context.registerBeanDefinition(JUERGEN, person1); + context.registerBeanDefinition(MARK, person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedFieldWithMultipleAttributesTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + context.refresh(); + QualifiedFieldWithMultipleAttributesTestBean bean = + (QualifiedFieldWithMultipleAttributesTestBean) context.getBean("autowired"); + assertThat(bean.getPerson().getName()).isEqualTo(MARK); + } + + @Test + public void testAutowiredFieldDoesNotResolveWithMultipleQualifierValuesAndConflictingDefaultValue() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + AutowireCandidateQualifier qualifier = new AutowireCandidateQualifier(TestQualifierWithMultipleAttributes.class); + qualifier.setAttribute("number", 456); + person1.addQualifier(qualifier); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + AutowireCandidateQualifier qualifier2 = new AutowireCandidateQualifier(TestQualifierWithMultipleAttributes.class); + qualifier2.setAttribute("number", 123); + qualifier2.setAttribute("value", "not the default"); + person2.addQualifier(qualifier2); + context.registerBeanDefinition(JUERGEN, person1); + context.registerBeanDefinition(MARK, person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedFieldWithMultipleAttributesTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy( + context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); + } + + @Test + public void testAutowiredFieldResolvesWithMultipleQualifierValuesAndExplicitDefaultValue() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + AutowireCandidateQualifier qualifier = new AutowireCandidateQualifier(TestQualifierWithMultipleAttributes.class); + qualifier.setAttribute("number", 456); + person1.addQualifier(qualifier); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + AutowireCandidateQualifier qualifier2 = new AutowireCandidateQualifier(TestQualifierWithMultipleAttributes.class); + qualifier2.setAttribute("number", 123); + qualifier2.setAttribute("value", "default"); + person2.addQualifier(qualifier2); + context.registerBeanDefinition(JUERGEN, person1); + context.registerBeanDefinition(MARK, person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedFieldWithMultipleAttributesTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + context.refresh(); + QualifiedFieldWithMultipleAttributesTestBean bean = + (QualifiedFieldWithMultipleAttributesTestBean) context.getBean("autowired"); + assertThat(bean.getPerson().getName()).isEqualTo(MARK); + } + + @Test + public void testAutowiredFieldDoesNotResolveWithMultipleQualifierValuesAndMultipleMatchingCandidates() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + AutowireCandidateQualifier qualifier = new AutowireCandidateQualifier(TestQualifierWithMultipleAttributes.class); + qualifier.setAttribute("number", 123); + person1.addQualifier(qualifier); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + AutowireCandidateQualifier qualifier2 = new AutowireCandidateQualifier(TestQualifierWithMultipleAttributes.class); + qualifier2.setAttribute("number", 123); + qualifier2.setAttribute("value", "default"); + person2.addQualifier(qualifier2); + context.registerBeanDefinition(JUERGEN, person1); + context.registerBeanDefinition(MARK, person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedFieldWithMultipleAttributesTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy( + context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); + } + + @Test + public void testAutowiredFieldDoesNotResolveWithBaseQualifierAndNonDefaultValueAndMultipleMatchingCandidates() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue("the real juergen"); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + person1.addQualifier(new AutowireCandidateQualifier(Qualifier.class, "juergen")); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue("juergen imposter"); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + person2.addQualifier(new AutowireCandidateQualifier(Qualifier.class, "juergen")); + context.registerBeanDefinition("juergen1", person1); + context.registerBeanDefinition("juergen2", person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedConstructorArgumentWithBaseQualifierNonDefaultValueTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy( + context::refresh) + .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("autowired")); + } + + + private static class QualifiedFieldTestBean { + + @Inject + @TestQualifier + private Person person; + + public Person getPerson() { + return this.person; + } + } + + + private static class QualifiedMethodParameterTestBean { + + private Person person; + + @Inject + public void setPerson(@TestQualifier Person person) { + this.person = person; + } + + public Person getPerson() { + return this.person; + } + } + + + private static class QualifiedConstructorArgumentTestBean { + + private Person person; + + @Inject + public QualifiedConstructorArgumentTestBean(@TestQualifier Person person) { + this.person = person; + } + + public Person getPerson() { + return this.person; + } + + } + + + public static class QualifiedFieldWithDefaultValueTestBean { + + @Inject + @TestQualifierWithDefaultValue + private Person person; + + public Person getPerson() { + return this.person; + } + } + + + public static class QualifiedFieldWithMultipleAttributesTestBean { + + @Inject + @TestQualifierWithMultipleAttributes(number=123) + private Person person; + + public Person getPerson() { + return this.person; + } + } + + + @SuppressWarnings("unused") + private static class QualifiedFieldWithBaseQualifierDefaultValueTestBean { + + @Inject + private Person person; + + public Person getPerson() { + return this.person; + } + } + + + public static class QualifiedConstructorArgumentWithBaseQualifierNonDefaultValueTestBean { + + private Person person; + + @Inject + public QualifiedConstructorArgumentWithBaseQualifierNonDefaultValueTestBean( + @Named("juergen") Person person) { + this.person = person; + } + + public Person getPerson() { + return this.person; + } + } + + + private static class Person { + + private String name; + + public Person(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + } + + + @TestQualifier + private static class QualifiedPerson extends Person { + + public QualifiedPerson() { + super(null); + } + + public QualifiedPerson(String name) { + super(name); + } + } + + + @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @Qualifier + public @interface TestQualifier { + } + + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @Qualifier + public @interface TestQualifierWithDefaultValue { + + String value() default "default"; + } + + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @Qualifier + public @interface TestQualifierWithMultipleAttributes { + + String value() default "default"; + + int number(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireContextTests.java b/spring-context/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireContextTests.java new file mode 100644 index 0000000..f126d20 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireContextTests.java @@ -0,0 +1,760 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.support; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.scope.ScopedProxyUtils; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.UnsatisfiedDependencyException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.context.annotation.AnnotationConfigUtils; +import org.springframework.context.support.GenericApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Integration tests for handling {@link Qualifier} annotations. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @author Chris Beams + * @author Sam Brannen + */ +public class QualifierAnnotationAutowireContextTests { + + private static final String JUERGEN = "juergen"; + + private static final String MARK = "mark"; + + + @Test + public void autowiredFieldWithSingleNonQualifiedCandidate() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs = new ConstructorArgumentValues(); + cavs.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person = new RootBeanDefinition(Person.class, cavs, null); + context.registerBeanDefinition(JUERGEN, person); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedFieldTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + + assertThatExceptionOfType(BeanCreationException.class).isThrownBy( + context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); + } + + @Test + public void autowiredMethodParameterWithSingleNonQualifiedCandidate() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs = new ConstructorArgumentValues(); + cavs.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person = new RootBeanDefinition(Person.class, cavs, null); + context.registerBeanDefinition(JUERGEN, person); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedMethodParameterTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy( + context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); + + } + + @Test + public void autowiredConstructorArgumentWithSingleNonQualifiedCandidate() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs = new ConstructorArgumentValues(); + cavs.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person = new RootBeanDefinition(Person.class, cavs, null); + context.registerBeanDefinition(JUERGEN, person); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedConstructorArgumentTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy( + context::refresh) + .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("autowired")); + } + + @Test + public void autowiredFieldWithSingleQualifiedCandidate() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs = new ConstructorArgumentValues(); + cavs.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person = new RootBeanDefinition(Person.class, cavs, null); + person.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); + context.registerBeanDefinition(JUERGEN, person); + context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedFieldTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + context.refresh(); + QualifiedFieldTestBean bean = (QualifiedFieldTestBean) context.getBean("autowired"); + assertThat(bean.getPerson().getName()).isEqualTo(JUERGEN); + } + + @Test + public void autowiredMethodParameterWithSingleQualifiedCandidate() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs = new ConstructorArgumentValues(); + cavs.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person = new RootBeanDefinition(Person.class, cavs, null); + person.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); + context.registerBeanDefinition(JUERGEN, person); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedMethodParameterTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + context.refresh(); + QualifiedMethodParameterTestBean bean = + (QualifiedMethodParameterTestBean) context.getBean("autowired"); + assertThat(bean.getPerson().getName()).isEqualTo(JUERGEN); + } + + @Test + public void autowiredMethodParameterWithStaticallyQualifiedCandidate() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs = new ConstructorArgumentValues(); + cavs.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person = new RootBeanDefinition(QualifiedPerson.class, cavs, null); + context.registerBeanDefinition(JUERGEN, + ScopedProxyUtils.createScopedProxy(new BeanDefinitionHolder(person, JUERGEN), context, true).getBeanDefinition()); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedMethodParameterTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + context.refresh(); + QualifiedMethodParameterTestBean bean = + (QualifiedMethodParameterTestBean) context.getBean("autowired"); + assertThat(bean.getPerson().getName()).isEqualTo(JUERGEN); + } + + @Test + public void autowiredMethodParameterWithStaticallyQualifiedCandidateAmongOthers() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs = new ConstructorArgumentValues(); + cavs.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person = new RootBeanDefinition(QualifiedPerson.class, cavs, null); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(DefaultValueQualifiedPerson.class, cavs2, null); + context.registerBeanDefinition(JUERGEN, person); + context.registerBeanDefinition(MARK, person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedMethodParameterTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + context.refresh(); + QualifiedMethodParameterTestBean bean = + (QualifiedMethodParameterTestBean) context.getBean("autowired"); + assertThat(bean.getPerson().getName()).isEqualTo(JUERGEN); + } + + @Test + public void autowiredConstructorArgumentWithSingleQualifiedCandidate() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs = new ConstructorArgumentValues(); + cavs.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person = new RootBeanDefinition(Person.class, cavs, null); + person.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); + context.registerBeanDefinition(JUERGEN, person); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedConstructorArgumentTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + context.refresh(); + QualifiedConstructorArgumentTestBean bean = + (QualifiedConstructorArgumentTestBean) context.getBean("autowired"); + assertThat(bean.getPerson().getName()).isEqualTo(JUERGEN); + } + + @Test + public void autowiredFieldWithMultipleNonQualifiedCandidates() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + context.registerBeanDefinition(JUERGEN, person1); + context.registerBeanDefinition(MARK, person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedFieldTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy( + context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); + } + + @Test + public void autowiredMethodParameterWithMultipleNonQualifiedCandidates() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + context.registerBeanDefinition(JUERGEN, person1); + context.registerBeanDefinition(MARK, person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedMethodParameterTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy( + context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); + } + + @Test + public void autowiredConstructorArgumentWithMultipleNonQualifiedCandidates() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + context.registerBeanDefinition(JUERGEN, person1); + context.registerBeanDefinition(MARK, person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedConstructorArgumentTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy( + context::refresh) + .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("autowired")); + } + + @Test + public void autowiredFieldResolvesQualifiedCandidate() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + person1.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + context.registerBeanDefinition(JUERGEN, person1); + context.registerBeanDefinition(MARK, person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedFieldTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + context.refresh(); + QualifiedFieldTestBean bean = (QualifiedFieldTestBean) context.getBean("autowired"); + assertThat(bean.getPerson().getName()).isEqualTo(JUERGEN); + } + + @Test + public void autowiredFieldResolvesMetaQualifiedCandidate() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + person1.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + context.registerBeanDefinition(JUERGEN, person1); + context.registerBeanDefinition(MARK, person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(MetaQualifiedFieldTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + context.refresh(); + MetaQualifiedFieldTestBean bean = (MetaQualifiedFieldTestBean) context.getBean("autowired"); + assertThat(bean.getPerson().getName()).isEqualTo(JUERGEN); + } + + @Test + public void autowiredMethodParameterResolvesQualifiedCandidate() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + person1.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + context.registerBeanDefinition(JUERGEN, person1); + context.registerBeanDefinition(MARK, person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedMethodParameterTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + context.refresh(); + QualifiedMethodParameterTestBean bean = + (QualifiedMethodParameterTestBean) context.getBean("autowired"); + assertThat(bean.getPerson().getName()).isEqualTo(JUERGEN); + } + + @Test + public void autowiredConstructorArgumentResolvesQualifiedCandidate() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + person1.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + context.registerBeanDefinition(JUERGEN, person1); + context.registerBeanDefinition(MARK, person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedConstructorArgumentTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + context.refresh(); + QualifiedConstructorArgumentTestBean bean = + (QualifiedConstructorArgumentTestBean) context.getBean("autowired"); + assertThat(bean.getPerson().getName()).isEqualTo(JUERGEN); + } + + @Test + public void autowiredFieldResolvesQualifiedCandidateWithDefaultValueAndNoValueOnBeanDefinition() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + // qualifier added, but includes no value + person1.addQualifier(new AutowireCandidateQualifier(TestQualifierWithDefaultValue.class)); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + context.registerBeanDefinition(JUERGEN, person1); + context.registerBeanDefinition(MARK, person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedFieldWithDefaultValueTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + context.refresh(); + QualifiedFieldWithDefaultValueTestBean bean = + (QualifiedFieldWithDefaultValueTestBean) context.getBean("autowired"); + assertThat(bean.getPerson().getName()).isEqualTo(JUERGEN); + } + + @Test + public void autowiredFieldDoesNotResolveCandidateWithDefaultValueAndConflictingValueOnBeanDefinition() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + // qualifier added, and non-default value specified + person1.addQualifier(new AutowireCandidateQualifier(TestQualifierWithDefaultValue.class, "not the default")); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + context.registerBeanDefinition(JUERGEN, person1); + context.registerBeanDefinition(MARK, person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedFieldWithDefaultValueTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy( + context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); + } + + @Test + public void autowiredFieldResolvesWithDefaultValueAndExplicitDefaultValueOnBeanDefinition() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + // qualifier added, and value matches the default + person1.addQualifier(new AutowireCandidateQualifier(TestQualifierWithDefaultValue.class, "default")); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + context.registerBeanDefinition(JUERGEN, person1); + context.registerBeanDefinition(MARK, person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedFieldWithDefaultValueTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + context.refresh(); + QualifiedFieldWithDefaultValueTestBean bean = + (QualifiedFieldWithDefaultValueTestBean) context.getBean("autowired"); + assertThat(bean.getPerson().getName()).isEqualTo(JUERGEN); + } + + @Test + public void autowiredFieldResolvesWithMultipleQualifierValues() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + AutowireCandidateQualifier qualifier = new AutowireCandidateQualifier(TestQualifierWithMultipleAttributes.class); + qualifier.setAttribute("number", 456); + person1.addQualifier(qualifier); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + AutowireCandidateQualifier qualifier2 = new AutowireCandidateQualifier(TestQualifierWithMultipleAttributes.class); + qualifier2.setAttribute("number", 123); + person2.addQualifier(qualifier2); + context.registerBeanDefinition(JUERGEN, person1); + context.registerBeanDefinition(MARK, person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedFieldWithMultipleAttributesTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + context.refresh(); + QualifiedFieldWithMultipleAttributesTestBean bean = + (QualifiedFieldWithMultipleAttributesTestBean) context.getBean("autowired"); + assertThat(bean.getPerson().getName()).isEqualTo(MARK); + } + + @Test + public void autowiredFieldDoesNotResolveWithMultipleQualifierValuesAndConflictingDefaultValue() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + AutowireCandidateQualifier qualifier = new AutowireCandidateQualifier(TestQualifierWithMultipleAttributes.class); + qualifier.setAttribute("number", 456); + person1.addQualifier(qualifier); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + AutowireCandidateQualifier qualifier2 = new AutowireCandidateQualifier(TestQualifierWithMultipleAttributes.class); + qualifier2.setAttribute("number", 123); + qualifier2.setAttribute("value", "not the default"); + person2.addQualifier(qualifier2); + context.registerBeanDefinition(JUERGEN, person1); + context.registerBeanDefinition(MARK, person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedFieldWithMultipleAttributesTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy( + context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); + } + + @Test + public void autowiredFieldResolvesWithMultipleQualifierValuesAndExplicitDefaultValue() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + AutowireCandidateQualifier qualifier = new AutowireCandidateQualifier(TestQualifierWithMultipleAttributes.class); + qualifier.setAttribute("number", 456); + person1.addQualifier(qualifier); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + AutowireCandidateQualifier qualifier2 = new AutowireCandidateQualifier(TestQualifierWithMultipleAttributes.class); + qualifier2.setAttribute("number", 123); + qualifier2.setAttribute("value", "default"); + person2.addQualifier(qualifier2); + context.registerBeanDefinition(JUERGEN, person1); + context.registerBeanDefinition(MARK, person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedFieldWithMultipleAttributesTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + context.refresh(); + QualifiedFieldWithMultipleAttributesTestBean bean = + (QualifiedFieldWithMultipleAttributesTestBean) context.getBean("autowired"); + assertThat(bean.getPerson().getName()).isEqualTo(MARK); + } + + @Test + public void autowiredFieldDoesNotResolveWithMultipleQualifierValuesAndMultipleMatchingCandidates() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + AutowireCandidateQualifier qualifier = new AutowireCandidateQualifier(TestQualifierWithMultipleAttributes.class); + qualifier.setAttribute("number", 123); + person1.addQualifier(qualifier); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + AutowireCandidateQualifier qualifier2 = new AutowireCandidateQualifier(TestQualifierWithMultipleAttributes.class); + qualifier2.setAttribute("number", 123); + qualifier2.setAttribute("value", "default"); + person2.addQualifier(qualifier2); + context.registerBeanDefinition(JUERGEN, person1); + context.registerBeanDefinition(MARK, person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedFieldWithMultipleAttributesTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy( + context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); + } + + @Test + public void autowiredFieldResolvesWithBaseQualifierAndDefaultValue() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue(JUERGEN); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue(MARK); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + person2.addQualifier(new AutowireCandidateQualifier(Qualifier.class)); + context.registerBeanDefinition(JUERGEN, person1); + context.registerBeanDefinition(MARK, person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedFieldWithBaseQualifierDefaultValueTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + context.refresh(); + QualifiedFieldWithBaseQualifierDefaultValueTestBean bean = + (QualifiedFieldWithBaseQualifierDefaultValueTestBean) context.getBean("autowired"); + assertThat(bean.getPerson().getName()).isEqualTo(MARK); + } + + @Test + public void autowiredFieldResolvesWithBaseQualifierAndNonDefaultValue() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue("the real juergen"); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + person1.addQualifier(new AutowireCandidateQualifier(Qualifier.class, "juergen")); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue("juergen imposter"); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + person2.addQualifier(new AutowireCandidateQualifier(Qualifier.class, "not really juergen")); + context.registerBeanDefinition("juergen1", person1); + context.registerBeanDefinition("juergen2", person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedConstructorArgumentWithBaseQualifierNonDefaultValueTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + context.refresh(); + QualifiedConstructorArgumentWithBaseQualifierNonDefaultValueTestBean bean = + (QualifiedConstructorArgumentWithBaseQualifierNonDefaultValueTestBean) context.getBean("autowired"); + assertThat(bean.getPerson().getName()).isEqualTo("the real juergen"); + } + + @Test + public void autowiredFieldDoesNotResolveWithBaseQualifierAndNonDefaultValueAndMultipleMatchingCandidates() { + GenericApplicationContext context = new GenericApplicationContext(); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); + cavs1.addGenericArgumentValue("the real juergen"); + RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); + person1.addQualifier(new AutowireCandidateQualifier(Qualifier.class, "juergen")); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); + cavs2.addGenericArgumentValue("juergen imposter"); + RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); + person2.addQualifier(new AutowireCandidateQualifier(Qualifier.class, "juergen")); + context.registerBeanDefinition("juergen1", person1); + context.registerBeanDefinition("juergen2", person2); + context.registerBeanDefinition("autowired", + new RootBeanDefinition(QualifiedConstructorArgumentWithBaseQualifierNonDefaultValueTestBean.class)); + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy( + context::refresh) + .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("autowired")); + } + + + private static class QualifiedFieldTestBean { + + @Autowired + @TestQualifier + private Person person; + + public Person getPerson() { + return this.person; + } + } + + + private static class MetaQualifiedFieldTestBean { + + @MyAutowired + private Person person; + + public Person getPerson() { + return this.person; + } + } + + + @Autowired + @TestQualifier + @Retention(RetentionPolicy.RUNTIME) + @interface MyAutowired { + } + + + private static class QualifiedMethodParameterTestBean { + + private Person person; + + @Autowired + public void setPerson(@TestQualifier Person person) { + this.person = person; + } + + public Person getPerson() { + return this.person; + } + } + + + private static class QualifiedConstructorArgumentTestBean { + + private Person person; + + @Autowired + public QualifiedConstructorArgumentTestBean(@TestQualifier Person person) { + this.person = person; + } + + public Person getPerson() { + return this.person; + } + + } + + + private static class QualifiedFieldWithDefaultValueTestBean { + + @Autowired + @TestQualifierWithDefaultValue + private Person person; + + public Person getPerson() { + return this.person; + } + } + + + private static class QualifiedFieldWithMultipleAttributesTestBean { + + @Autowired + @TestQualifierWithMultipleAttributes(number=123) + private Person person; + + public Person getPerson() { + return this.person; + } + } + + + private static class QualifiedFieldWithBaseQualifierDefaultValueTestBean { + + @Autowired + @Qualifier + private Person person; + + public Person getPerson() { + return this.person; + } + } + + + private static class QualifiedConstructorArgumentWithBaseQualifierNonDefaultValueTestBean { + + private Person person; + + @Autowired + public QualifiedConstructorArgumentWithBaseQualifierNonDefaultValueTestBean( + @Qualifier("juergen") Person person) { + this.person = person; + } + + public Person getPerson() { + return this.person; + } + } + + + private static class Person { + + private String name; + + public Person(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + } + + + @TestQualifier + private static class QualifiedPerson extends Person { + + public QualifiedPerson() { + super(null); + } + + public QualifiedPerson(String name) { + super(name); + } + } + + + @TestQualifierWithDefaultValue + private static class DefaultValueQualifiedPerson extends Person { + + public DefaultValueQualifiedPerson() { + super(null); + } + + public DefaultValueQualifiedPerson(String name) { + super(name); + } + } + + + @Retention(RetentionPolicy.RUNTIME) + @Qualifier + @interface TestQualifier { + } + + + @Retention(RetentionPolicy.RUNTIME) + @Qualifier + @interface TestQualifierWithDefaultValue { + + String value() default "default"; + } + + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @Qualifier + @interface TestQualifierWithMultipleAttributes { + + String value() default "default"; + + int number(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/beans/factory/xml/LookupMethodWrappedByCglibProxyTests.java b/spring-context/src/test/java/org/springframework/beans/factory/xml/LookupMethodWrappedByCglibProxyTests.java new file mode 100644 index 0000000..e65b730 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/beans/factory/xml/LookupMethodWrappedByCglibProxyTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.interceptor.DebugInterceptor; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests lookup methods wrapped by a CGLIB proxy (see SPR-391). + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Chris Beams + */ +public class LookupMethodWrappedByCglibProxyTests { + + private static final Class CLASS = LookupMethodWrappedByCglibProxyTests.class; + private static final String CLASSNAME = CLASS.getSimpleName(); + + private static final String CONTEXT = CLASSNAME + "-context.xml"; + + private ApplicationContext applicationContext; + + @BeforeEach + public void setUp() { + this.applicationContext = new ClassPathXmlApplicationContext(CONTEXT, CLASS); + resetInterceptor(); + } + + @Test + public void testAutoProxiedLookup() { + OverloadLookup olup = (OverloadLookup) applicationContext.getBean("autoProxiedOverload"); + ITestBean jenny = olup.newTestBean(); + assertThat(jenny.getName()).isEqualTo("Jenny"); + assertThat(olup.testMethod()).isEqualTo("foo"); + assertInterceptorCount(2); + } + + @Test + public void testRegularlyProxiedLookup() { + OverloadLookup olup = (OverloadLookup) applicationContext.getBean("regularlyProxiedOverload"); + ITestBean jenny = olup.newTestBean(); + assertThat(jenny.getName()).isEqualTo("Jenny"); + assertThat(olup.testMethod()).isEqualTo("foo"); + assertInterceptorCount(2); + } + + private void assertInterceptorCount(int count) { + DebugInterceptor interceptor = getInterceptor(); + assertThat(interceptor.getCount()).as("Interceptor count is incorrect").isEqualTo(count); + } + + private void resetInterceptor() { + DebugInterceptor interceptor = getInterceptor(); + interceptor.resetCount(); + } + + private DebugInterceptor getInterceptor() { + return (DebugInterceptor) applicationContext.getBean("interceptor"); + } + +} + + +abstract class OverloadLookup { + + public abstract ITestBean newTestBean(); + + public String testMethod() { + return "foo"; + } +} + diff --git a/spring-context/src/test/java/org/springframework/beans/factory/xml/QualifierAnnotationTests.java b/spring-context/src/test/java/org/springframework/beans/factory/xml/QualifierAnnotationTests.java new file mode 100644 index 0000000..6003d25 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/beans/factory/xml/QualifierAnnotationTests.java @@ -0,0 +1,443 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Properties; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.QualifierAnnotationAutowireCandidateResolver; +import org.springframework.beans.factory.support.AutowireCandidateQualifier; +import org.springframework.beans.factory.support.BeanDefinitionReader; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.context.support.StaticApplicationContext; + +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.util.ClassUtils.convertClassNameToResourcePath; + +/** + * @author Mark Fisher + * @author Juergen Hoeller + * @author Chris Beams + */ +public class QualifierAnnotationTests { + + private static final String CLASSNAME = QualifierAnnotationTests.class.getName(); + private static final String CONFIG_LOCATION = + format("classpath:%s-context.xml", convertClassNameToResourcePath(CLASSNAME)); + + + @Test + public void testNonQualifiedFieldFails() { + StaticApplicationContext context = new StaticApplicationContext(); + BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); + reader.loadBeanDefinitions(CONFIG_LOCATION); + context.registerSingleton("testBean", NonQualifiedTestBean.class); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy( + context::refresh) + .withMessageContaining("found 6"); + } + + @Test + public void testQualifiedByValue() { + StaticApplicationContext context = new StaticApplicationContext(); + BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); + reader.loadBeanDefinitions(CONFIG_LOCATION); + context.registerSingleton("testBean", QualifiedByValueTestBean.class); + context.refresh(); + QualifiedByValueTestBean testBean = (QualifiedByValueTestBean) context.getBean("testBean"); + Person person = testBean.getLarry(); + assertThat(person.getName()).isEqualTo("Larry"); + } + + @Test + public void testQualifiedByParentValue() { + StaticApplicationContext parent = new StaticApplicationContext(); + GenericBeanDefinition parentLarry = new GenericBeanDefinition(); + parentLarry.setBeanClass(Person.class); + parentLarry.getPropertyValues().add("name", "ParentLarry"); + parentLarry.addQualifier(new AutowireCandidateQualifier(Qualifier.class, "parentLarry")); + parent.registerBeanDefinition("someLarry", parentLarry); + GenericBeanDefinition otherLarry = new GenericBeanDefinition(); + otherLarry.setBeanClass(Person.class); + otherLarry.getPropertyValues().add("name", "OtherLarry"); + otherLarry.addQualifier(new AutowireCandidateQualifier(Qualifier.class, "otherLarry")); + parent.registerBeanDefinition("someOtherLarry", otherLarry); + parent.refresh(); + + StaticApplicationContext context = new StaticApplicationContext(parent); + BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); + reader.loadBeanDefinitions(CONFIG_LOCATION); + context.registerSingleton("testBean", QualifiedByParentValueTestBean.class); + context.refresh(); + QualifiedByParentValueTestBean testBean = (QualifiedByParentValueTestBean) context.getBean("testBean"); + Person person = testBean.getLarry(); + assertThat(person.getName()).isEqualTo("ParentLarry"); + } + + @Test + public void testQualifiedByBeanName() { + StaticApplicationContext context = new StaticApplicationContext(); + BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); + reader.loadBeanDefinitions(CONFIG_LOCATION); + context.registerSingleton("testBean", QualifiedByBeanNameTestBean.class); + context.refresh(); + QualifiedByBeanNameTestBean testBean = (QualifiedByBeanNameTestBean) context.getBean("testBean"); + Person person = testBean.getLarry(); + assertThat(person.getName()).isEqualTo("LarryBean"); + assertThat(testBean.myProps != null && testBean.myProps.isEmpty()).isTrue(); + } + + @Test + public void testQualifiedByFieldName() { + StaticApplicationContext context = new StaticApplicationContext(); + BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); + reader.loadBeanDefinitions(CONFIG_LOCATION); + context.registerSingleton("testBean", QualifiedByFieldNameTestBean.class); + context.refresh(); + QualifiedByFieldNameTestBean testBean = (QualifiedByFieldNameTestBean) context.getBean("testBean"); + Person person = testBean.getLarry(); + assertThat(person.getName()).isEqualTo("LarryBean"); + } + + @Test + public void testQualifiedByParameterName() { + StaticApplicationContext context = new StaticApplicationContext(); + BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); + reader.loadBeanDefinitions(CONFIG_LOCATION); + context.registerSingleton("testBean", QualifiedByParameterNameTestBean.class); + context.refresh(); + QualifiedByParameterNameTestBean testBean = (QualifiedByParameterNameTestBean) context.getBean("testBean"); + Person person = testBean.getLarry(); + assertThat(person.getName()).isEqualTo("LarryBean"); + } + + @Test + public void testQualifiedByAlias() { + StaticApplicationContext context = new StaticApplicationContext(); + BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); + reader.loadBeanDefinitions(CONFIG_LOCATION); + context.registerSingleton("testBean", QualifiedByAliasTestBean.class); + context.refresh(); + QualifiedByAliasTestBean testBean = (QualifiedByAliasTestBean) context.getBean("testBean"); + Person person = testBean.getStooge(); + assertThat(person.getName()).isEqualTo("LarryBean"); + } + + @Test + public void testQualifiedByAnnotation() { + StaticApplicationContext context = new StaticApplicationContext(); + BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); + reader.loadBeanDefinitions(CONFIG_LOCATION); + context.registerSingleton("testBean", QualifiedByAnnotationTestBean.class); + context.refresh(); + QualifiedByAnnotationTestBean testBean = (QualifiedByAnnotationTestBean) context.getBean("testBean"); + Person person = testBean.getLarry(); + assertThat(person.getName()).isEqualTo("LarrySpecial"); + } + + @Test + public void testQualifiedByCustomValue() { + StaticApplicationContext context = new StaticApplicationContext(); + BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); + reader.loadBeanDefinitions(CONFIG_LOCATION); + context.registerSingleton("testBean", QualifiedByCustomValueTestBean.class); + context.refresh(); + QualifiedByCustomValueTestBean testBean = (QualifiedByCustomValueTestBean) context.getBean("testBean"); + Person person = testBean.getCurly(); + assertThat(person.getName()).isEqualTo("Curly"); + } + + @Test + public void testQualifiedByAnnotationValue() { + StaticApplicationContext context = new StaticApplicationContext(); + BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); + reader.loadBeanDefinitions(CONFIG_LOCATION); + context.registerSingleton("testBean", QualifiedByAnnotationValueTestBean.class); + context.refresh(); + QualifiedByAnnotationValueTestBean testBean = (QualifiedByAnnotationValueTestBean) context.getBean("testBean"); + Person person = testBean.getLarry(); + assertThat(person.getName()).isEqualTo("LarrySpecial"); + } + + @Test + public void testQualifiedByAttributesFailsWithoutCustomQualifierRegistered() { + StaticApplicationContext context = new StaticApplicationContext(); + BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); + reader.loadBeanDefinitions(CONFIG_LOCATION); + context.registerSingleton("testBean", QualifiedByAttributesTestBean.class); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy( + context::refresh) + .withMessageContaining("found 6"); + } + + @Test + public void testQualifiedByAttributesWithCustomQualifierRegistered() { + StaticApplicationContext context = new StaticApplicationContext(); + BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); + reader.loadBeanDefinitions(CONFIG_LOCATION); + QualifierAnnotationAutowireCandidateResolver resolver = (QualifierAnnotationAutowireCandidateResolver) + context.getDefaultListableBeanFactory().getAutowireCandidateResolver(); + resolver.addQualifierType(MultipleAttributeQualifier.class); + context.registerSingleton("testBean", MultiQualifierClient.class); + context.refresh(); + + MultiQualifierClient testBean = (MultiQualifierClient) context.getBean("testBean"); + + assertThat(testBean.factoryTheta).isNotNull(); + assertThat(testBean.implTheta).isNotNull(); + } + + @Test + public void testInterfaceWithOneQualifiedFactoryAndOneQualifiedBean() { + StaticApplicationContext context = new StaticApplicationContext(); + BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); + reader.loadBeanDefinitions(CONFIG_LOCATION); + } + + + @SuppressWarnings("unused") + private static class NonQualifiedTestBean { + + @Autowired + private Person anonymous; + + public Person getAnonymous() { + return anonymous; + } + } + + + private static class QualifiedByValueTestBean { + + @Autowired @Qualifier("larry") + private Person larry; + + public Person getLarry() { + return larry; + } + } + + + private static class QualifiedByParentValueTestBean { + + @Autowired @Qualifier("parentLarry") + private Person larry; + + public Person getLarry() { + return larry; + } + } + + + private static class QualifiedByBeanNameTestBean { + + @Autowired @Qualifier("larryBean") + private Person larry; + + @Autowired @Qualifier("testProperties") + public Properties myProps; + + public Person getLarry() { + return larry; + } + } + + + private static class QualifiedByFieldNameTestBean { + + @Autowired + private Person larryBean; + + public Person getLarry() { + return larryBean; + } + } + + + private static class QualifiedByParameterNameTestBean { + + private Person larryBean; + + @Autowired + public void setLarryBean(Person larryBean) { + this.larryBean = larryBean; + } + + public Person getLarry() { + return larryBean; + } + } + + + private static class QualifiedByAliasTestBean { + + @Autowired @Qualifier("stooge") + private Person stooge; + + public Person getStooge() { + return stooge; + } + } + + + private static class QualifiedByAnnotationTestBean { + + @Autowired @Qualifier("special") + private Person larry; + + public Person getLarry() { + return larry; + } + } + + + private static class QualifiedByCustomValueTestBean { + + @Autowired @SimpleValueQualifier("curly") + private Person curly; + + public Person getCurly() { + return curly; + } + } + + + private static class QualifiedByAnnotationValueTestBean { + + @Autowired @SimpleValueQualifier("special") + private Person larry; + + public Person getLarry() { + return larry; + } + } + + + @SuppressWarnings("unused") + private static class QualifiedByAttributesTestBean { + + @Autowired @MultipleAttributeQualifier(name="moe", age=42) + private Person moeSenior; + + @Autowired @MultipleAttributeQualifier(name="moe", age=15) + private Person moeJunior; + + public Person getMoeSenior() { + return moeSenior; + } + + public Person getMoeJunior() { + return moeJunior; + } + } + + + @SuppressWarnings("unused") + private static class Person { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + + @Qualifier("special") + @SimpleValueQualifier("special") + private static class SpecialPerson extends Person { + } + + + @Target({ElementType.FIELD, ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @Qualifier + public @interface SimpleValueQualifier { + + String value() default ""; + } + + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + public @interface MultipleAttributeQualifier { + + String name(); + + int age(); + } + + + private static final String FACTORY_QUALIFIER = "FACTORY"; + + private static final String IMPL_QUALIFIER = "IMPL"; + + + public static class MultiQualifierClient { + + @Autowired @Qualifier(FACTORY_QUALIFIER) + public Theta factoryTheta; + + @Autowired @Qualifier(IMPL_QUALIFIER) + public Theta implTheta; + } + + + public interface Theta { + } + + + @Qualifier(IMPL_QUALIFIER) + public static class ThetaImpl implements Theta { + } + + + @Qualifier(FACTORY_QUALIFIER) + public static class QualifiedFactoryBean implements FactoryBean { + + @Override + public Theta getObject() { + return new Theta() {}; + } + + @Override + public Class getObjectType() { + return Theta.class; + } + + @Override + public boolean isSingleton() { + return true; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/beans/factory/xml/SimplePropertyNamespaceHandlerWithExpressionLanguageTests.java b/spring-context/src/test/java/org/springframework/beans/factory/xml/SimplePropertyNamespaceHandlerWithExpressionLanguageTests.java new file mode 100644 index 0000000..954cf1f --- /dev/null +++ b/spring-context/src/test/java/org/springframework/beans/factory/xml/SimplePropertyNamespaceHandlerWithExpressionLanguageTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for combining the expression language and the p namespace. Due to the required EL dependency, this test is in + * context module rather than the beans module. + * + * @author Arjen Poutsma + */ +public class SimplePropertyNamespaceHandlerWithExpressionLanguageTests { + + @Test + public void combineWithExpressionLanguage() { + ApplicationContext applicationContext = + new ClassPathXmlApplicationContext("simplePropertyNamespaceHandlerWithExpressionLanguageTests.xml", + getClass()); + ITestBean foo = applicationContext.getBean("foo", ITestBean.class); + ITestBean bar = applicationContext.getBean("bar", ITestBean.class); + assertThat(foo.getName()).as("Invalid name").isEqualTo("Baz"); + assertThat(bar.getName()).as("Invalid name").isEqualTo("Baz"); + } + +} diff --git a/spring-context/src/test/java/org/springframework/beans/factory/xml/XmlBeanFactoryTestTypes.java b/spring-context/src/test/java/org/springframework/beans/factory/xml/XmlBeanFactoryTestTypes.java new file mode 100644 index 0000000..01c0f54 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/beans/factory/xml/XmlBeanFactoryTestTypes.java @@ -0,0 +1,639 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import java.beans.ConstructorProperties; +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.support.MethodReplacer; +import org.springframework.beans.testfixture.beans.FactoryMethods; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.IndexedTestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.beans.testfixture.beans.factory.DummyFactory; + +/** + * Types used by {@link XmlBeanFactoryTests} and its attendant XML config files. + * + * @author Chris Beams + */ +final class XmlBeanFactoryTestTypes { +} + + +/** + * Simple bean used to check constructor dependency checking. + * + * @author Juergen Hoeller + * @since 09.11.2003 + */ +@SuppressWarnings("serial") +class ConstructorDependenciesBean implements Serializable { + + private int age; + + private String name; + + private TestBean spouse1; + + private TestBean spouse2; + + private IndexedTestBean other; + + public ConstructorDependenciesBean(int age) { + this.age = age; + } + + public ConstructorDependenciesBean(String name) { + this.name = name; + } + + public ConstructorDependenciesBean(TestBean spouse1) { + this.spouse1 = spouse1; + } + + public ConstructorDependenciesBean(TestBean spouse1, TestBean spouse2) { + this.spouse1 = spouse1; + this.spouse2 = spouse2; + } + + @ConstructorProperties({"spouse", "otherSpouse", "myAge"}) + public ConstructorDependenciesBean(TestBean spouse1, TestBean spouse2, int age) { + this.spouse1 = spouse1; + this.spouse2 = spouse2; + this.age = age; + } + + public ConstructorDependenciesBean(TestBean spouse1, TestBean spouse2, IndexedTestBean other) { + this.spouse1 = spouse1; + this.spouse2 = spouse2; + this.other = other; + } + + public int getAge() { + return age; + } + + public String getName() { + return name; + } + + public TestBean getSpouse1() { + return spouse1; + } + + public TestBean getSpouse2() { + return spouse2; + } + + public IndexedTestBean getOther() { + return other; + } + + public void setAge(int age) { + this.age = age; + } + + public void setName(String name) { + this.name = name; + } +} + + +class SimpleConstructorArgBean { + + private int age; + + private String name; + + public SimpleConstructorArgBean() { + } + + public SimpleConstructorArgBean(int age) { + this.age = age; + } + + public SimpleConstructorArgBean(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public String getName() { + return name; + } +} + + +/** + * Bean testing the ability to use both lookup method overrides + * and constructor injection. + * There is also a property ("setterString") to be set via + * Setter Injection. + * + * @author Rod Johnson + */ +abstract class ConstructorInjectedOverrides { + + private ITestBean tb; + + private String setterString; + + public ConstructorInjectedOverrides(ITestBean tb) { + this.tb = tb; + } + + public ITestBean getTestBean() { + return this.tb; + } + + protected abstract FactoryMethods createFactoryMethods(); + + public String getSetterString() { + return setterString; + } + + public void setSetterString(String setterString) { + this.setterString = setterString; + } +} + + +/** + * Simple bean used to check constructor dependency checking. + * + * @author Juergen Hoeller + * @since 09.11.2003 + */ +@SuppressWarnings({ "serial", "unused" }) +class DerivedConstructorDependenciesBean extends ConstructorDependenciesBean { + + boolean initialized; + boolean destroyed; + + DerivedConstructorDependenciesBean(TestBean spouse1, TestBean spouse2, IndexedTestBean other) { + super(spouse1, spouse2, other); + } + + private DerivedConstructorDependenciesBean(TestBean spouse1, Object spouse2, IndexedTestBean other) { + super(spouse1, null, other); + } + + protected DerivedConstructorDependenciesBean(TestBean spouse1, TestBean spouse2, IndexedTestBean other, int age, int otherAge) { + super(spouse1, spouse2, other); + } + + public DerivedConstructorDependenciesBean(TestBean spouse1, TestBean spouse2, IndexedTestBean other, int age, String name) { + super(spouse1, spouse2, other); + setAge(age); + setName(name); + } + + private void init() { + this.initialized = true; + } + + private void destroy() { + this.destroyed = true; + } +} + + +/** + * @author Rod Johnson + */ +interface DummyBo { + + void something(); +} + + +/** + * @author Rod Johnson + */ +class DummyBoImpl implements DummyBo { + + DummyDao dao; + + public DummyBoImpl(DummyDao dao) { + this.dao = dao; + } + + @Override + public void something() { + } +} + + +/** + * @author Rod Johnson + */ +class DummyDao { +} + + +/** + * @author Juergen Hoeller + * @since 21.07.2003 + */ +class DummyReferencer { + + private TestBean testBean1; + + private TestBean testBean2; + + private DummyFactory dummyFactory; + + public DummyReferencer() { + } + + public DummyReferencer(DummyFactory dummyFactory) { + this.dummyFactory = dummyFactory; + } + + public void setDummyFactory(DummyFactory dummyFactory) { + this.dummyFactory = dummyFactory; + } + + public DummyFactory getDummyFactory() { + return dummyFactory; + } + + public void setTestBean1(TestBean testBean1) { + this.testBean1 = testBean1; + } + + public TestBean getTestBean1() { + return testBean1; + } + + public void setTestBean2(TestBean testBean2) { + this.testBean2 = testBean2; + } + + public TestBean getTestBean2() { + return testBean2; + } +} + + +/** + * Fixed method replacer for String return types + * @author Rod Johnson + */ +class FixedMethodReplacer implements MethodReplacer { + + public static final String VALUE = "fixedMethodReplacer"; + + @Override + public Object reimplement(Object obj, Method method, Object[] args) throws Throwable { + return VALUE; + } +} + + +/** + * @author Chris Beams + */ +class MapAndSet { + + private Object obj; + + public MapAndSet(Map map) { + this.obj = map; + } + + public MapAndSet(Set set) { + this.obj = set; + } + + public Object getObject() { + return obj; + } +} + + +/** + * @author Rod Johnson + */ +class MethodReplaceCandidate { + + public String replaceMe(String echo) { + return echo; + } +} + + +/** + * Bean that exposes a simple property that can be set + * to a mix of references and individual values. + */ +class MixedCollectionBean { + + private Collection jumble; + + public void setJumble(Collection jumble) { + this.jumble = jumble; + } + + public Collection getJumble() { + return jumble; + } +} + + +/** + * @author Juergen Hoeller + */ +interface OverrideInterface { + + TestBean getPrototypeDependency(); + + TestBean getPrototypeDependency(Object someParam); +} + + +/** + * @author Rod Johnson + * @author Juergen Hoeller + */ +abstract class OverrideOneMethod extends MethodReplaceCandidate implements OverrideInterface { + + protected abstract TestBean protectedOverrideSingleton(); + + @Override + public TestBean getPrototypeDependency(Object someParam) { + return new TestBean(); + } + + public TestBean invokesOverriddenMethodOnSelf() { + return getPrototypeDependency(); + } + + public String echo(String echo) { + return echo; + } + + /** + * Overloaded form of replaceMe. + */ + public String replaceMe() { + return "replaceMe"; + } + + /** + * Another overloaded form of replaceMe, not getting replaced. + * Must not cause errors when the other replaceMe methods get replaced. + */ + public String replaceMe(int someParam) { + return "replaceMe:" + someParam; + } + + @Override + public String replaceMe(String someParam) { + return "replaceMe:" + someParam; + } +} + + +/** + * Subclass of OverrideOneMethod, to check that overriding is + * supported for inherited methods. + * + * @author Rod Johnson + */ +abstract class OverrideOneMethodSubclass extends OverrideOneMethod { + + protected void doSomething(String arg) { + // This implementation does nothing! + // It's not overloaded + } +} + + +/** + * Simple test of BeanFactory initialization and lifecycle callbacks. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +class ProtectedLifecycleBean implements BeanNameAware, BeanFactoryAware, InitializingBean, DisposableBean { + + protected boolean initMethodDeclared = false; + + protected String beanName; + + protected BeanFactory owningFactory; + + protected boolean postProcessedBeforeInit; + + protected boolean inited; + + protected boolean initedViaDeclaredInitMethod; + + protected boolean postProcessedAfterInit; + + protected boolean destroyed; + + public void setInitMethodDeclared(boolean initMethodDeclared) { + this.initMethodDeclared = initMethodDeclared; + } + + public boolean isInitMethodDeclared() { + return initMethodDeclared; + } + + @Override + public void setBeanName(String name) { + this.beanName = name; + } + + public String getBeanName() { + return beanName; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.owningFactory = beanFactory; + } + + public void postProcessBeforeInit() { + if (this.inited || this.initedViaDeclaredInitMethod) { + throw new RuntimeException("Factory called postProcessBeforeInit after afterPropertiesSet"); + } + if (this.postProcessedBeforeInit) { + throw new RuntimeException("Factory called postProcessBeforeInit twice"); + } + this.postProcessedBeforeInit = true; + } + + @Override + public void afterPropertiesSet() { + if (this.owningFactory == null) { + throw new RuntimeException("Factory didn't call setBeanFactory before afterPropertiesSet on lifecycle bean"); + } + if (!this.postProcessedBeforeInit) { + throw new RuntimeException("Factory didn't call postProcessBeforeInit before afterPropertiesSet on lifecycle bean"); + } + if (this.initedViaDeclaredInitMethod) { + throw new RuntimeException("Factory initialized via declared init method before initializing via afterPropertiesSet"); + } + if (this.inited) { + throw new RuntimeException("Factory called afterPropertiesSet twice"); + } + this.inited = true; + } + + public void declaredInitMethod() { + if (!this.inited) { + throw new RuntimeException("Factory didn't call afterPropertiesSet before declared init method"); + } + + if (this.initedViaDeclaredInitMethod) { + throw new RuntimeException("Factory called declared init method twice"); + } + this.initedViaDeclaredInitMethod = true; + } + + public void postProcessAfterInit() { + if (!this.inited) { + throw new RuntimeException("Factory called postProcessAfterInit before afterPropertiesSet"); + } + if (this.initMethodDeclared && !this.initedViaDeclaredInitMethod) { + throw new RuntimeException("Factory called postProcessAfterInit before calling declared init method"); + } + if (this.postProcessedAfterInit) { + throw new RuntimeException("Factory called postProcessAfterInit twice"); + } + this.postProcessedAfterInit = true; + } + + /** + * Dummy business method that will fail unless the factory + * managed the bean's lifecycle correctly + */ + public void businessMethod() { + if (!this.inited || (this.initMethodDeclared && !this.initedViaDeclaredInitMethod) || + !this.postProcessedAfterInit) { + throw new RuntimeException("Factory didn't initialize lifecycle object correctly"); + } + } + + @Override + public void destroy() { + if (this.destroyed) { + throw new IllegalStateException("Already destroyed"); + } + this.destroyed = true; + } + + public boolean isDestroyed() { + return destroyed; + } + + + public static class PostProcessor implements BeanPostProcessor { + + @Override + public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException { + if (bean instanceof ProtectedLifecycleBean) { + ((ProtectedLifecycleBean) bean).postProcessBeforeInit(); + } + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String name) throws BeansException { + if (bean instanceof ProtectedLifecycleBean) { + ((ProtectedLifecycleBean) bean).postProcessAfterInit(); + } + return bean; + } + } +} + + +/** + * @author Rod Johnson + */ +@SuppressWarnings("serial") +class ReverseMethodReplacer implements MethodReplacer, Serializable { + + @Override + public Object reimplement(Object obj, Method method, Object[] args) throws Throwable { + String s = (String) args[0]; + return new StringBuilder(s).reverse().toString(); + } +} + + +/** + * @author Rod Johnson + */ +@SuppressWarnings("serial") +abstract class SerializableMethodReplacerCandidate extends MethodReplaceCandidate implements Serializable { + + //public abstract Point getPoint(); +} + + +/** + * @author Juergen Hoeller + * @since 23.10.2004 + */ +class SingleSimpleTypeConstructorBean { + + private boolean singleBoolean; + + private boolean secondBoolean; + + private String testString; + + public SingleSimpleTypeConstructorBean(boolean singleBoolean) { + this.singleBoolean = singleBoolean; + } + + protected SingleSimpleTypeConstructorBean(String testString, boolean secondBoolean) { + this.testString = testString; + this.secondBoolean = secondBoolean; + } + + public boolean isSingleBoolean() { + return singleBoolean; + } + + public boolean isSecondBoolean() { + return secondBoolean; + } + + public String getTestString() { + return testString; + } +} diff --git a/spring-context/src/test/java/org/springframework/beans/factory/xml/XmlBeanFactoryTests.java b/spring-context/src/test/java/org/springframework/beans/factory/xml/XmlBeanFactoryTests.java new file mode 100644 index 0000000..a1e3f81 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/beans/factory/xml/XmlBeanFactoryTests.java @@ -0,0 +1,1897 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringWriter; +import java.lang.reflect.Method; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.commons.logging.LogFactory; +import org.junit.jupiter.api.Test; +import org.xml.sax.InputSource; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.FatalBeanException; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanCurrentlyInCreationException; +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanIsAbstractException; +import org.springframework.beans.factory.CannotLoadBeanClassException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.UnsatisfiedDependencyException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.MethodReplacer; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.DependenciesBean; +import org.springframework.beans.testfixture.beans.DerivedTestBean; +import org.springframework.beans.testfixture.beans.FactoryMethods; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.IndexedTestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.beans.testfixture.beans.factory.DummyFactory; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.UrlResource; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.core.testfixture.io.SerializationTestUtils; +import org.springframework.tests.sample.beans.ResourceTestBean; +import org.springframework.util.ClassUtils; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StopWatch; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Miscellaneous tests for XML bean definitions. + * + * @author Juergen Hoeller + * @author Rod Johnson + * @author Rick Evans + * @author Chris Beams + * @author Sam Brannen + */ +class XmlBeanFactoryTests { + + private static final Class CLASS = XmlBeanFactoryTests.class; + private static final String CLASSNAME = CLASS.getSimpleName(); + + private static final ClassPathResource AUTOWIRE_CONTEXT = classPathResource("-autowire.xml"); + private static final ClassPathResource CHILD_CONTEXT = classPathResource("-child.xml"); + private static final ClassPathResource CLASS_NOT_FOUND_CONTEXT = classPathResource("-classNotFound.xml"); + private static final ClassPathResource COMPLEX_FACTORY_CIRCLE_CONTEXT = classPathResource("-complexFactoryCircle.xml"); + private static final ClassPathResource CONSTRUCTOR_ARG_CONTEXT = classPathResource("-constructorArg.xml"); + private static final ClassPathResource CONSTRUCTOR_OVERRIDES_CONTEXT = classPathResource("-constructorOverrides.xml"); + private static final ClassPathResource DELEGATION_OVERRIDES_CONTEXT = classPathResource("-delegationOverrides.xml"); + private static final ClassPathResource DEP_CARG_AUTOWIRE_CONTEXT = classPathResource("-depCargAutowire.xml"); + private static final ClassPathResource DEP_CARG_INNER_CONTEXT = classPathResource("-depCargInner.xml"); + private static final ClassPathResource DEP_CARG_CONTEXT = classPathResource("-depCarg.xml"); + private static final ClassPathResource DEP_DEPENDSON_INNER_CONTEXT = classPathResource("-depDependsOnInner.xml"); + private static final ClassPathResource DEP_DEPENDSON_CONTEXT = classPathResource("-depDependsOn.xml"); + private static final ClassPathResource DEP_PROP = classPathResource("-depProp.xml"); + private static final ClassPathResource DEP_PROP_ABN_CONTEXT = classPathResource("-depPropAutowireByName.xml"); + private static final ClassPathResource DEP_PROP_ABT_CONTEXT = classPathResource("-depPropAutowireByType.xml"); + private static final ClassPathResource DEP_PROP_MIDDLE_CONTEXT = classPathResource("-depPropInTheMiddle.xml"); + private static final ClassPathResource DEP_PROP_INNER_CONTEXT = classPathResource("-depPropInner.xml"); + private static final ClassPathResource DEP_MATERIALIZE_CONTEXT = classPathResource("-depMaterializeThis.xml"); + private static final ClassPathResource FACTORY_CIRCLE_CONTEXT = classPathResource("-factoryCircle.xml"); + private static final ClassPathResource INITIALIZERS_CONTEXT = classPathResource("-initializers.xml"); + private static final ClassPathResource INVALID_CONTEXT = classPathResource("-invalid.xml"); + private static final ClassPathResource INVALID_NO_SUCH_METHOD_CONTEXT = classPathResource("-invalidOverridesNoSuchMethod.xml"); + private static final ClassPathResource COLLECTIONS_XSD_CONTEXT = classPathResource("-localCollectionsUsingXsd.xml"); + private static final ClassPathResource MISSING_CONTEXT = classPathResource("-missing.xml"); + private static final ClassPathResource OVERRIDES_CONTEXT = classPathResource("-overrides.xml"); + private static final ClassPathResource PARENT_CONTEXT = classPathResource("-parent.xml"); + private static final ClassPathResource NO_SUCH_FACTORY_METHOD_CONTEXT = classPathResource("-noSuchFactoryMethod.xml"); + private static final ClassPathResource RECURSIVE_IMPORT_CONTEXT = classPathResource("-recursiveImport.xml"); + private static final ClassPathResource RESOURCE_CONTEXT = classPathResource("-resource.xml"); + private static final ClassPathResource TEST_WITH_DUP_NAMES_CONTEXT = classPathResource("-testWithDuplicateNames.xml"); + private static final ClassPathResource TEST_WITH_DUP_NAME_IN_ALIAS_CONTEXT = classPathResource("-testWithDuplicateNameInAlias.xml"); + private static final ClassPathResource REFTYPES_CONTEXT = classPathResource("-reftypes.xml"); + private static final ClassPathResource DEFAULT_LAZY_CONTEXT = classPathResource("-defaultLazyInit.xml"); + private static final ClassPathResource DEFAULT_AUTOWIRE_CONTEXT = classPathResource("-defaultAutowire.xml"); + + private static ClassPathResource classPathResource(String suffix) { + return new ClassPathResource(CLASSNAME + suffix, CLASS); + } + + + @Test // SPR-2368 + void collectionsReferredToAsRefLocals() { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(factory).loadBeanDefinitions(COLLECTIONS_XSD_CONTEXT); + factory.preInstantiateSingletons(); + } + + @Test + void refToSeparatePrototypeInstances() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_NONE); + reader.loadBeanDefinitions(REFTYPES_CONTEXT); + + TestBean emma = (TestBean) xbf.getBean("emma"); + TestBean georgia = (TestBean) xbf.getBean("georgia"); + ITestBean emmasJenks = emma.getSpouse(); + ITestBean georgiasJenks = georgia.getSpouse(); + assertThat(emmasJenks != georgiasJenks).as("Emma and georgia think they have a different boyfriend").isTrue(); + assertThat(emmasJenks.getName().equals("Andrew")).as("Emmas jenks has right name").isTrue(); + assertThat(emmasJenks != xbf.getBean("jenks")).as("Emmas doesn't equal new ref").isTrue(); + assertThat(emmasJenks.getName().equals("Andrew")).as("Georgias jenks has right name").isTrue(); + assertThat(emmasJenks.equals(georgiasJenks)).as("They are object equal").isTrue(); + assertThat(emmasJenks.equals(xbf.getBean("jenks"))).as("They object equal direct ref").isTrue(); + } + + @Test + void refToSingleton() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_NONE); + reader.loadBeanDefinitions(new EncodedResource(REFTYPES_CONTEXT, "ISO-8859-1")); + + TestBean jen = (TestBean) xbf.getBean("jenny"); + TestBean dave = (TestBean) xbf.getBean("david"); + TestBean jenks = (TestBean) xbf.getBean("jenks"); + ITestBean davesJen = dave.getSpouse(); + ITestBean jenksJen = jenks.getSpouse(); + assertThat(davesJen == jenksJen).as("1 jen instance").isTrue(); + assertThat(davesJen == jen).as("1 jen instance").isTrue(); + } + + @Test + void innerBeans() throws IOException { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + + reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_NONE); + try (InputStream inputStream = getClass().getResourceAsStream(REFTYPES_CONTEXT.getPath())) { + reader.loadBeanDefinitions(new InputSource(inputStream)); + } + + // Let's create the outer bean named "innerBean", + // to check whether it doesn't create any conflicts + // with the actual inner beans named "innerBean". + xbf.getBean("innerBean"); + + TestBean hasInnerBeans = (TestBean) xbf.getBean("hasInnerBeans"); + assertThat(hasInnerBeans.getAge()).isEqualTo(5); + TestBean inner1 = (TestBean) hasInnerBeans.getSpouse(); + assertThat(inner1).isNotNull(); + assertThat(inner1.getBeanName()).isEqualTo("innerBean#1"); + assertThat(inner1.getName()).isEqualTo("inner1"); + assertThat(inner1.getAge()).isEqualTo(6); + + assertThat(hasInnerBeans.getFriends()).isNotNull(); + Object[] friends = hasInnerBeans.getFriends().toArray(); + assertThat(friends.length).isEqualTo(3); + DerivedTestBean inner2 = (DerivedTestBean) friends[0]; + assertThat(inner2.getName()).isEqualTo("inner2"); + assertThat(inner2.getBeanName().startsWith(DerivedTestBean.class.getName())).isTrue(); + assertThat(xbf.containsBean("innerBean#1")).isFalse(); + assertThat(inner2).isNotNull(); + assertThat(inner2.getAge()).isEqualTo(7); + TestBean innerFactory = (TestBean) friends[1]; + assertThat(innerFactory.getName()).isEqualTo(DummyFactory.SINGLETON_NAME); + TestBean inner5 = (TestBean) friends[2]; + assertThat(inner5.getBeanName()).isEqualTo("innerBean#2"); + + assertThat(hasInnerBeans.getSomeMap()).isNotNull(); + assertThat(hasInnerBeans.getSomeMap().size()).isEqualTo(2); + TestBean inner3 = (TestBean) hasInnerBeans.getSomeMap().get("someKey"); + assertThat(inner3.getName()).isEqualTo("Jenny"); + assertThat(inner3.getAge()).isEqualTo(30); + TestBean inner4 = (TestBean) hasInnerBeans.getSomeMap().get("someOtherKey"); + assertThat(inner4.getName()).isEqualTo("inner4"); + assertThat(inner4.getAge()).isEqualTo(9); + + TestBean hasInnerBeansForConstructor = (TestBean) xbf.getBean("hasInnerBeansForConstructor"); + TestBean innerForConstructor = (TestBean) hasInnerBeansForConstructor.getSpouse(); + assertThat(innerForConstructor).isNotNull(); + assertThat(innerForConstructor.getBeanName()).isEqualTo("innerBean#3"); + assertThat(innerForConstructor.getName()).isEqualTo("inner1"); + assertThat(innerForConstructor.getAge()).isEqualTo(6); + + hasInnerBeansForConstructor = (TestBean) xbf.getBean("hasInnerBeansAsPrototype"); + innerForConstructor = (TestBean) hasInnerBeansForConstructor.getSpouse(); + assertThat(innerForConstructor).isNotNull(); + assertThat(innerForConstructor.getBeanName()).isEqualTo("innerBean"); + assertThat(innerForConstructor.getName()).isEqualTo("inner1"); + assertThat(innerForConstructor.getAge()).isEqualTo(6); + + hasInnerBeansForConstructor = (TestBean) xbf.getBean("hasInnerBeansAsPrototype"); + innerForConstructor = (TestBean) hasInnerBeansForConstructor.getSpouse(); + assertThat(innerForConstructor).isNotNull(); + assertThat(innerForConstructor.getBeanName()).isEqualTo("innerBean"); + assertThat(innerForConstructor.getName()).isEqualTo("inner1"); + assertThat(innerForConstructor.getAge()).isEqualTo(6); + + xbf.destroySingletons(); + assertThat(inner1.wasDestroyed()).isTrue(); + assertThat(inner2.wasDestroyed()).isTrue(); + assertThat(innerFactory.getName() == null).isTrue(); + assertThat(inner5.wasDestroyed()).isTrue(); + } + + @Test + void innerBeansWithoutDestroy() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_NONE); + reader.loadBeanDefinitions(REFTYPES_CONTEXT); + + // Let's create the outer bean named "innerBean", + // to check whether it doesn't create any conflicts + // with the actual inner beans named "innerBean". + xbf.getBean("innerBean"); + + TestBean hasInnerBeans = (TestBean) xbf.getBean("hasInnerBeansWithoutDestroy"); + assertThat(hasInnerBeans.getAge()).isEqualTo(5); + TestBean inner1 = (TestBean) hasInnerBeans.getSpouse(); + assertThat(inner1).isNotNull(); + assertThat(inner1.getBeanName().startsWith("innerBean")).isTrue(); + assertThat(inner1.getName()).isEqualTo("inner1"); + assertThat(inner1.getAge()).isEqualTo(6); + + assertThat(hasInnerBeans.getFriends()).isNotNull(); + Object[] friends = hasInnerBeans.getFriends().toArray(); + assertThat(friends.length).isEqualTo(3); + DerivedTestBean inner2 = (DerivedTestBean) friends[0]; + assertThat(inner2.getName()).isEqualTo("inner2"); + assertThat(inner2.getBeanName().startsWith(DerivedTestBean.class.getName())).isTrue(); + assertThat(inner2).isNotNull(); + assertThat(inner2.getAge()).isEqualTo(7); + TestBean innerFactory = (TestBean) friends[1]; + assertThat(innerFactory.getName()).isEqualTo(DummyFactory.SINGLETON_NAME); + TestBean inner5 = (TestBean) friends[2]; + assertThat(inner5.getBeanName().startsWith("innerBean")).isTrue(); + } + + @Test + void failsOnInnerBean() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_NONE); + reader.loadBeanDefinitions(REFTYPES_CONTEXT); + + try { + xbf.getBean("failsOnInnerBean"); + } + catch (BeanCreationException ex) { + // Check whether message contains outer bean name. + ex.printStackTrace(); + assertThat(ex.getMessage().contains("failsOnInnerBean")).isTrue(); + assertThat(ex.getMessage().contains("someMap")).isTrue(); + } + + try { + xbf.getBean("failsOnInnerBeanForConstructor"); + } + catch (BeanCreationException ex) { + // Check whether message contains outer bean name. + ex.printStackTrace(); + assertThat(ex.getMessage().contains("failsOnInnerBeanForConstructor")).isTrue(); + assertThat(ex.getMessage().contains("constructor argument")).isTrue(); + } + } + + @Test + void inheritanceFromParentFactoryPrototype() { + DefaultListableBeanFactory parent = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(parent).loadBeanDefinitions(PARENT_CONTEXT); + DefaultListableBeanFactory child = new DefaultListableBeanFactory(parent); + new XmlBeanDefinitionReader(child).loadBeanDefinitions(CHILD_CONTEXT); + assertThat(child.getType("inheritsFromParentFactory")).isEqualTo(TestBean.class); + TestBean inherits = (TestBean) child.getBean("inheritsFromParentFactory"); + // Name property value is overridden + assertThat(inherits.getName().equals("override")).isTrue(); + // Age property is inherited from bean in parent factory + assertThat(inherits.getAge() == 1).isTrue(); + TestBean inherits2 = (TestBean) child.getBean("inheritsFromParentFactory"); + assertThat(inherits2 == inherits).isFalse(); + } + + @Test + void inheritanceWithDifferentClass() { + DefaultListableBeanFactory parent = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(parent).loadBeanDefinitions(PARENT_CONTEXT); + DefaultListableBeanFactory child = new DefaultListableBeanFactory(parent); + new XmlBeanDefinitionReader(child).loadBeanDefinitions(CHILD_CONTEXT); + assertThat(child.getType("inheritsWithClass")).isEqualTo(DerivedTestBean.class); + DerivedTestBean inherits = (DerivedTestBean) child.getBean("inheritsWithDifferentClass"); + // Name property value is overridden + assertThat(inherits.getName().equals("override")).isTrue(); + // Age property is inherited from bean in parent factory + assertThat(inherits.getAge() == 1).isTrue(); + assertThat(inherits.wasInitialized()).isTrue(); + } + + @Test + void inheritanceWithClass() { + DefaultListableBeanFactory parent = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(parent).loadBeanDefinitions(PARENT_CONTEXT); + DefaultListableBeanFactory child = new DefaultListableBeanFactory(parent); + new XmlBeanDefinitionReader(child).loadBeanDefinitions(CHILD_CONTEXT); + assertThat(child.getType("inheritsWithClass")).isEqualTo(DerivedTestBean.class); + DerivedTestBean inherits = (DerivedTestBean) child.getBean("inheritsWithClass"); + // Name property value is overridden + assertThat(inherits.getName().equals("override")).isTrue(); + // Age property is inherited from bean in parent factory + assertThat(inherits.getAge() == 1).isTrue(); + assertThat(inherits.wasInitialized()).isTrue(); + } + + @Test + void prototypeInheritanceFromParentFactoryPrototype() { + DefaultListableBeanFactory parent = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(parent).loadBeanDefinitions(PARENT_CONTEXT); + DefaultListableBeanFactory child = new DefaultListableBeanFactory(parent); + new XmlBeanDefinitionReader(child).loadBeanDefinitions(CHILD_CONTEXT); + assertThat(child.getType("prototypeInheritsFromParentFactoryPrototype")).isEqualTo(TestBean.class); + TestBean inherits = (TestBean) child.getBean("prototypeInheritsFromParentFactoryPrototype"); + // Name property value is overridden + assertThat(inherits.getName().equals("prototype-override")).isTrue(); + // Age property is inherited from bean in parent factory + assertThat(inherits.getAge() == 2).isTrue(); + TestBean inherits2 = (TestBean) child.getBean("prototypeInheritsFromParentFactoryPrototype"); + assertThat(inherits2 == inherits).isFalse(); + inherits2.setAge(13); + assertThat(inherits2.getAge() == 13).isTrue(); + // Shouldn't have changed first instance + assertThat(inherits.getAge() == 2).isTrue(); + } + + @Test + void prototypeInheritanceFromParentFactorySingleton() { + DefaultListableBeanFactory parent = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(parent).loadBeanDefinitions(PARENT_CONTEXT); + DefaultListableBeanFactory child = new DefaultListableBeanFactory(parent); + new XmlBeanDefinitionReader(child).loadBeanDefinitions(CHILD_CONTEXT); + TestBean inherits = (TestBean) child.getBean("protoypeInheritsFromParentFactorySingleton"); + // Name property value is overridden + assertThat(inherits.getName().equals("prototypeOverridesInheritedSingleton")).isTrue(); + // Age property is inherited from bean in parent factory + assertThat(inherits.getAge() == 1).isTrue(); + TestBean inherits2 = (TestBean) child.getBean("protoypeInheritsFromParentFactorySingleton"); + assertThat(inherits2 == inherits).isFalse(); + inherits2.setAge(13); + assertThat(inherits2.getAge() == 13).isTrue(); + // Shouldn't have changed first instance + assertThat(inherits.getAge() == 1).isTrue(); + } + + @Test + void autowireModeNotInherited() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.loadBeanDefinitions(OVERRIDES_CONTEXT); + + TestBean david = (TestBean) xbf.getBean("magicDavid"); + // the parent bean is autowiring + assertThat(david.getSpouse()).isNotNull(); + + TestBean derivedDavid = (TestBean) xbf.getBean("magicDavidDerived"); + // this fails while it inherits from the child bean + assertThat(derivedDavid.getSpouse()).as("autowiring not propagated along child relationships").isNull(); + } + + @Test + void abstractParentBeans() { + DefaultListableBeanFactory parent = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(parent).loadBeanDefinitions(PARENT_CONTEXT); + parent.preInstantiateSingletons(); + assertThat(parent.isSingleton("inheritedTestBeanWithoutClass")).isTrue(); + + // abstract beans should not match + Map tbs = parent.getBeansOfType(TestBean.class); + assertThat(tbs.size()).isEqualTo(2); + assertThat(tbs.containsKey("inheritedTestBeanPrototype")).isTrue(); + assertThat(tbs.containsKey("inheritedTestBeanSingleton")).isTrue(); + + // abstract bean should throw exception on creation attempt + assertThatExceptionOfType(BeanIsAbstractException.class).isThrownBy(() -> + parent.getBean("inheritedTestBeanWithoutClass")); + + // non-abstract bean should work, even if it serves as parent + assertThat(parent.getBean("inheritedTestBeanPrototype") instanceof TestBean).isTrue(); + } + + @Test + void dependenciesMaterializeThis() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(DEP_MATERIALIZE_CONTEXT); + + assertThat(xbf.getBeansOfType(DummyBo.class, true, false).size()).isEqualTo(2); + assertThat(xbf.getBeansOfType(DummyBo.class, true, true).size()).isEqualTo(3); + assertThat(xbf.getBeansOfType(DummyBo.class, true, false).size()).isEqualTo(3); + assertThat(xbf.getBeansOfType(DummyBo.class).size()).isEqualTo(3); + assertThat(xbf.getBeansOfType(DummyBoImpl.class, true, true).size()).isEqualTo(2); + assertThat(xbf.getBeansOfType(DummyBoImpl.class, false, true).size()).isEqualTo(1); + assertThat(xbf.getBeansOfType(DummyBoImpl.class).size()).isEqualTo(2); + + DummyBoImpl bos = (DummyBoImpl) xbf.getBean("boSingleton"); + DummyBoImpl bop = (DummyBoImpl) xbf.getBean("boPrototype"); + assertThat(bop).isNotSameAs(bos); + assertThat(bos.dao == bop.dao).isTrue(); + } + + @Test + void childOverridesParentBean() { + DefaultListableBeanFactory parent = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(parent).loadBeanDefinitions(PARENT_CONTEXT); + DefaultListableBeanFactory child = new DefaultListableBeanFactory(parent); + new XmlBeanDefinitionReader(child).loadBeanDefinitions(CHILD_CONTEXT); + TestBean inherits = (TestBean) child.getBean("inheritedTestBean"); + // Name property value is overridden + assertThat(inherits.getName().equals("overrideParentBean")).isTrue(); + // Age property is inherited from bean in parent factory + assertThat(inherits.getAge() == 1).isTrue(); + TestBean inherits2 = (TestBean) child.getBean("inheritedTestBean"); + assertThat(inherits2 != inherits).isTrue(); + } + + /** + * Check that a prototype can't inherit from a bogus parent. + * If a singleton does this the factory will fail to load. + */ + @Test + void bogusParentageFromParentFactory() { + DefaultListableBeanFactory parent = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(parent).loadBeanDefinitions(PARENT_CONTEXT); + DefaultListableBeanFactory child = new DefaultListableBeanFactory(parent); + new XmlBeanDefinitionReader(child).loadBeanDefinitions(CHILD_CONTEXT); + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> + child.getBean("bogusParent", TestBean.class)) + .withMessageContaining("bogusParent") + .withCauseInstanceOf(NoSuchBeanDefinitionException.class); + } + + /** + * Note that prototype/singleton distinction is not inherited. + * It's possible for a subclass singleton not to return independent + * instances even if derived from a prototype + */ + @Test + void singletonInheritsFromParentFactoryPrototype() { + DefaultListableBeanFactory parent = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(parent).loadBeanDefinitions(PARENT_CONTEXT); + DefaultListableBeanFactory child = new DefaultListableBeanFactory(parent); + new XmlBeanDefinitionReader(child).loadBeanDefinitions(CHILD_CONTEXT); + TestBean inherits = (TestBean) child.getBean("singletonInheritsFromParentFactoryPrototype"); + // Name property value is overridden + assertThat(inherits.getName().equals("prototype-override")).isTrue(); + // Age property is inherited from bean in parent factory + assertThat(inherits.getAge() == 2).isTrue(); + TestBean inherits2 = (TestBean) child.getBean("singletonInheritsFromParentFactoryPrototype"); + assertThat(inherits2 == inherits).isTrue(); + } + + @Test + void singletonFromParent() { + DefaultListableBeanFactory parent = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(parent).loadBeanDefinitions(PARENT_CONTEXT); + TestBean beanFromParent = (TestBean) parent.getBean("inheritedTestBeanSingleton"); + DefaultListableBeanFactory child = new DefaultListableBeanFactory(parent); + new XmlBeanDefinitionReader(child).loadBeanDefinitions(CHILD_CONTEXT); + TestBean beanFromChild = (TestBean) child.getBean("inheritedTestBeanSingleton"); + assertThat(beanFromParent == beanFromChild).as("singleton from parent and child is the same").isTrue(); + } + + @Test + void nestedPropertyValue() { + DefaultListableBeanFactory parent = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(parent).loadBeanDefinitions(PARENT_CONTEXT); + DefaultListableBeanFactory child = new DefaultListableBeanFactory(parent); + new XmlBeanDefinitionReader(child).loadBeanDefinitions(CHILD_CONTEXT); + IndexedTestBean bean = (IndexedTestBean) child.getBean("indexedTestBean"); + assertThat(bean.getArray()[0].getName()).as("name applied correctly").isEqualTo("myname"); + } + + @Test + void circularReferences() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_NONE); + reader.loadBeanDefinitions(REFTYPES_CONTEXT); + TestBean jenny = (TestBean) xbf.getBean("jenny"); + TestBean david = (TestBean) xbf.getBean("david"); + TestBean ego = (TestBean) xbf.getBean("ego"); + TestBean complexInnerEgo = (TestBean) xbf.getBean("complexInnerEgo"); + TestBean complexEgo = (TestBean) xbf.getBean("complexEgo"); + assertThat(jenny.getSpouse() == david).as("Correct circular reference").isTrue(); + assertThat(david.getSpouse() == jenny).as("Correct circular reference").isTrue(); + assertThat(ego.getSpouse() == ego).as("Correct circular reference").isTrue(); + assertThat(complexInnerEgo.getSpouse().getSpouse() == complexInnerEgo).as("Correct circular reference").isTrue(); + assertThat(complexEgo.getSpouse().getSpouse() == complexEgo).as("Correct circular reference").isTrue(); + } + + @Test + void circularReferencesWithConstructor() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_NONE); + reader.loadBeanDefinitions(REFTYPES_CONTEXT); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> xbf.getBean("jenny_constructor")) + .matches(ex -> ex.contains(BeanCurrentlyInCreationException.class)); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> xbf.getBean("david_constructor")) + .matches(ex -> ex.contains(BeanCurrentlyInCreationException.class)); + } + + @Test + void circularReferencesWithPrototype() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_NONE); + reader.loadBeanDefinitions(REFTYPES_CONTEXT); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> xbf.getBean("jenny_prototype")) + .matches(ex -> ex.contains(BeanCurrentlyInCreationException.class)); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> xbf.getBean("david_prototype")) + .matches(ex -> ex.contains(BeanCurrentlyInCreationException.class)); + } + + @Test + void circularReferencesWithDependOn() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_NONE); + reader.loadBeanDefinitions(REFTYPES_CONTEXT); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> xbf.getBean("jenny_depends_on")); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> xbf.getBean("david_depends_on")); + } + + @Test + void circularReferenceWithFactoryBeanFirst() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_NONE); + reader.loadBeanDefinitions(REFTYPES_CONTEXT); + xbf.getBean("egoBridge"); + TestBean complexEgo = (TestBean) xbf.getBean("complexEgo"); + assertThat(complexEgo.getSpouse().getSpouse() == complexEgo).as("Correct circular reference").isTrue(); + } + + @Test + void circularReferenceWithTwoFactoryBeans() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_NONE); + reader.loadBeanDefinitions(REFTYPES_CONTEXT); + TestBean ego1 = (TestBean) xbf.getBean("ego1"); + assertThat(ego1.getSpouse().getSpouse() == ego1).as("Correct circular reference").isTrue(); + TestBean ego3 = (TestBean) xbf.getBean("ego3"); + assertThat(ego3.getSpouse().getSpouse() == ego3).as("Correct circular reference").isTrue(); + } + + @Test + void circularReferencesWithNotAllowed() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + xbf.setAllowCircularReferences(false); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_NONE); + reader.loadBeanDefinitions(REFTYPES_CONTEXT); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + xbf.getBean("jenny")) + .matches(ex -> ex.contains(BeanCurrentlyInCreationException.class)); + } + + @Test + void circularReferencesWithWrapping() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_NONE); + reader.loadBeanDefinitions(REFTYPES_CONTEXT); + xbf.addBeanPostProcessor(new WrappingPostProcessor()); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + xbf.getBean("jenny")) + .matches(ex -> ex.contains(BeanCurrentlyInCreationException.class)); + } + + @Test + void circularReferencesWithWrappingAndRawInjectionAllowed() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + xbf.setAllowRawInjectionDespiteWrapping(true); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_NONE); + reader.loadBeanDefinitions(REFTYPES_CONTEXT); + xbf.addBeanPostProcessor(new WrappingPostProcessor()); + + ITestBean jenny = (ITestBean) xbf.getBean("jenny"); + ITestBean david = (ITestBean) xbf.getBean("david"); + assertThat(AopUtils.isAopProxy(jenny)).isTrue(); + assertThat(AopUtils.isAopProxy(david)).isTrue(); + assertThat(jenny.getSpouse()).isSameAs(david); + assertThat(david.getSpouse()).isNotSameAs(jenny); + assertThat(david.getSpouse().getName()).isEqualTo("Jenny"); + assertThat(david.getSpouse().getSpouse()).isSameAs(david); + assertThat(AopUtils.isAopProxy(jenny.getSpouse())).isTrue(); + assertThat(!AopUtils.isAopProxy(david.getSpouse())).isTrue(); + } + + @Test + void factoryReferenceCircle() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(FACTORY_CIRCLE_CONTEXT); + TestBean tb = (TestBean) xbf.getBean("singletonFactory"); + DummyFactory db = (DummyFactory) xbf.getBean("&singletonFactory"); + assertThat(tb == db.getOtherTestBean()).isTrue(); + } + + @Test + void factoryReferenceWithDoublePrefix() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(FACTORY_CIRCLE_CONTEXT); + assertThat(xbf.getBean("&&singletonFactory")).isInstanceOf(DummyFactory.class); + } + + @Test + void complexFactoryReferenceCircle() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(COMPLEX_FACTORY_CIRCLE_CONTEXT); + xbf.getBean("proxy1"); + // check that unused instances from autowiring got removed + assertThat(xbf.getSingletonCount()).isEqualTo(4); + // properly create the remaining two instances + xbf.getBean("proxy2"); + assertThat(xbf.getSingletonCount()).isEqualTo(5); + } + + @Test + void noSuchFactoryBeanMethod() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(NO_SUCH_FACTORY_METHOD_CONTEXT); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + xbf.getBean("defaultTestBean")); + } + + @Test + void initMethodIsInvoked() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(INITIALIZERS_CONTEXT); + DoubleInitializer in = (DoubleInitializer) xbf.getBean("init-method1"); + // Initializer should have doubled value + assertThat(in.getNum()).isEqualTo(14); + } + + /** + * Test that if a custom initializer throws an exception, it's handled correctly + */ + @Test + void initMethodThrowsException() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(INITIALIZERS_CONTEXT); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + xbf.getBean("init-method2")) + .withCauseInstanceOf(IOException.class) + .satisfies(ex -> { + assertThat(ex.getResourceDescription()).contains("initializers.xml"); + assertThat(ex.getBeanName()).isEqualTo("init-method2"); + }); + } + + @Test + void noSuchInitMethod() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(INITIALIZERS_CONTEXT); + assertThatExceptionOfType(FatalBeanException.class).isThrownBy(() -> + xbf.getBean("init-method3")) + .withMessageContaining("initializers.xml") + .withMessageContaining("init-method3") + .withMessageContaining("init"); + } + + /** + * Check that InitializingBean method is called first. + */ + @Test + void initializingBeanAndInitMethod() { + InitAndIB.constructed = false; + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(INITIALIZERS_CONTEXT); + assertThat(InitAndIB.constructed).isFalse(); + xbf.preInstantiateSingletons(); + assertThat(InitAndIB.constructed).isFalse(); + InitAndIB iib = (InitAndIB) xbf.getBean("init-and-ib"); + assertThat(InitAndIB.constructed).isTrue(); + assertThat(iib.afterPropertiesSetInvoked && iib.initMethodInvoked).isTrue(); + assertThat(!iib.destroyed && !iib.customDestroyed).isTrue(); + xbf.destroySingletons(); + assertThat(iib.destroyed && iib.customDestroyed).isTrue(); + xbf.destroySingletons(); + assertThat(iib.destroyed && iib.customDestroyed).isTrue(); + } + + /** + * Check that InitializingBean method is not called twice. + */ + @Test + void initializingBeanAndSameInitMethod() { + InitAndIB.constructed = false; + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(INITIALIZERS_CONTEXT); + assertThat(InitAndIB.constructed).isFalse(); + xbf.preInstantiateSingletons(); + assertThat(InitAndIB.constructed).isFalse(); + InitAndIB iib = (InitAndIB) xbf.getBean("ib-same-init"); + assertThat(InitAndIB.constructed).isTrue(); + assertThat(iib.afterPropertiesSetInvoked && !iib.initMethodInvoked).isTrue(); + assertThat(!iib.destroyed && !iib.customDestroyed).isTrue(); + xbf.destroySingletons(); + assertThat(iib.destroyed && !iib.customDestroyed).isTrue(); + xbf.destroySingletons(); + assertThat(iib.destroyed && !iib.customDestroyed).isTrue(); + } + + @Test + void defaultLazyInit() { + InitAndIB.constructed = false; + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(DEFAULT_LAZY_CONTEXT); + assertThat(InitAndIB.constructed).isFalse(); + xbf.preInstantiateSingletons(); + assertThat(InitAndIB.constructed).isTrue(); + try { + xbf.getBean("lazy-and-bad"); + } + catch (BeanCreationException ex) { + assertThat(ex.getCause() instanceof IOException).isTrue(); + } + } + + @Test + void noSuchXmlFile() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(MISSING_CONTEXT)); + } + + @Test + void invalidXmlFile() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(INVALID_CONTEXT)); + } + + @Test + void autowire() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(AUTOWIRE_CONTEXT); + TestBean spouse = new TestBean("kerry", 0); + xbf.registerSingleton("spouse", spouse); + doTestAutowire(xbf); + } + + @Test + void autowireWithParent() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(AUTOWIRE_CONTEXT); + DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "kerry"); + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + bd.setPropertyValues(pvs); + lbf.registerBeanDefinition("spouse", bd); + xbf.setParentBeanFactory(lbf); + doTestAutowire(xbf); + } + + private void doTestAutowire(DefaultListableBeanFactory xbf) { + DependenciesBean rod1 = (DependenciesBean) xbf.getBean("rod1"); + TestBean kerry = (TestBean) xbf.getBean("spouse"); + // should have been autowired + assertThat(rod1.getSpouse()).isEqualTo(kerry); + + DependenciesBean rod1a = (DependenciesBean) xbf.getBean("rod1a"); + // should have been autowired + assertThat(rod1a.getSpouse()).isEqualTo(kerry); + + DependenciesBean rod2 = (DependenciesBean) xbf.getBean("rod2"); + // should have been autowired + assertThat(rod2.getSpouse()).isEqualTo(kerry); + + DependenciesBean rod2a = (DependenciesBean) xbf.getBean("rod2a"); + // should have been set explicitly + assertThat(rod2a.getSpouse()).isEqualTo(kerry); + + ConstructorDependenciesBean rod3 = (ConstructorDependenciesBean) xbf.getBean("rod3"); + IndexedTestBean other = (IndexedTestBean) xbf.getBean("other"); + // should have been autowired + assertThat(rod3.getSpouse1()).isEqualTo(kerry); + assertThat(rod3.getSpouse2()).isEqualTo(kerry); + assertThat(rod3.getOther()).isEqualTo(other); + + ConstructorDependenciesBean rod3a = (ConstructorDependenciesBean) xbf.getBean("rod3a"); + // should have been autowired + assertThat(rod3a.getSpouse1()).isEqualTo(kerry); + assertThat(rod3a.getSpouse2()).isEqualTo(kerry); + assertThat(rod3a.getOther()).isEqualTo(other); + + assertThatExceptionOfType(FatalBeanException.class).isThrownBy(() -> + xbf.getBean("rod4", ConstructorDependenciesBean.class)); + + DependenciesBean rod5 = (DependenciesBean) xbf.getBean("rod5"); + // Should not have been autowired + assertThat((Object) rod5.getSpouse()).isNull(); + + BeanFactory appCtx = (BeanFactory) xbf.getBean("childAppCtx"); + assertThat(appCtx.containsBean("rod1")).isTrue(); + assertThat(appCtx.containsBean("jenny")).isTrue(); + } + + @Test + void autowireWithDefault() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(DEFAULT_AUTOWIRE_CONTEXT); + + DependenciesBean rod1 = (DependenciesBean) xbf.getBean("rod1"); + // should have been autowired + assertThat(rod1.getSpouse()).isNotNull(); + assertThat(rod1.getSpouse().getName().equals("Kerry")).isTrue(); + + DependenciesBean rod2 = (DependenciesBean) xbf.getBean("rod2"); + // should have been autowired + assertThat(rod2.getSpouse()).isNotNull(); + assertThat(rod2.getSpouse().getName().equals("Kerry")).isTrue(); + } + + @Test + void autowireByConstructor() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONSTRUCTOR_ARG_CONTEXT); + ConstructorDependenciesBean rod1 = (ConstructorDependenciesBean) xbf.getBean("rod1"); + TestBean kerry = (TestBean) xbf.getBean("kerry2"); + // should have been autowired + assertThat(rod1.getSpouse1()).isEqualTo(kerry); + assertThat(rod1.getAge()).isEqualTo(0); + assertThat(rod1.getName()).isEqualTo(null); + + ConstructorDependenciesBean rod2 = (ConstructorDependenciesBean) xbf.getBean("rod2"); + TestBean kerry1 = (TestBean) xbf.getBean("kerry1"); + TestBean kerry2 = (TestBean) xbf.getBean("kerry2"); + // should have been autowired + assertThat(rod2.getSpouse1()).isEqualTo(kerry2); + assertThat(rod2.getSpouse2()).isEqualTo(kerry1); + assertThat(rod2.getAge()).isEqualTo(0); + assertThat(rod2.getName()).isEqualTo(null); + + ConstructorDependenciesBean rod = (ConstructorDependenciesBean) xbf.getBean("rod3"); + IndexedTestBean other = (IndexedTestBean) xbf.getBean("other"); + // should have been autowired + assertThat(rod.getSpouse1()).isEqualTo(kerry); + assertThat(rod.getSpouse2()).isEqualTo(kerry); + assertThat(rod.getOther()).isEqualTo(other); + assertThat(rod.getAge()).isEqualTo(0); + assertThat(rod.getName()).isEqualTo(null); + + xbf.getBean("rod4", ConstructorDependenciesBean.class); + // should have been autowired + assertThat(rod.getSpouse1()).isEqualTo(kerry); + assertThat(rod.getSpouse2()).isEqualTo(kerry); + assertThat(rod.getOther()).isEqualTo(other); + assertThat(rod.getAge()).isEqualTo(0); + assertThat(rod.getName()).isEqualTo(null); + } + + @Test + void autowireByConstructorWithSimpleValues() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONSTRUCTOR_ARG_CONTEXT); + + ConstructorDependenciesBean rod5 = (ConstructorDependenciesBean) xbf.getBean("rod5"); + TestBean kerry1 = (TestBean) xbf.getBean("kerry1"); + TestBean kerry2 = (TestBean) xbf.getBean("kerry2"); + IndexedTestBean other = (IndexedTestBean) xbf.getBean("other"); + // should have been autowired + assertThat(rod5.getSpouse1()).isEqualTo(kerry2); + assertThat(rod5.getSpouse2()).isEqualTo(kerry1); + assertThat(rod5.getOther()).isEqualTo(other); + assertThat(rod5.getAge()).isEqualTo(99); + assertThat(rod5.getName()).isEqualTo("myname"); + + DerivedConstructorDependenciesBean rod6 = (DerivedConstructorDependenciesBean) xbf.getBean("rod6"); + // should have been autowired + assertThat(rod6.initialized).isTrue(); + assertThat(!rod6.destroyed).isTrue(); + assertThat(rod6.getSpouse1()).isEqualTo(kerry2); + assertThat(rod6.getSpouse2()).isEqualTo(kerry1); + assertThat(rod6.getOther()).isEqualTo(other); + assertThat(rod6.getAge()).isEqualTo(0); + assertThat(rod6.getName()).isEqualTo(null); + + xbf.destroySingletons(); + assertThat(rod6.destroyed).isTrue(); + } + + @Test + void relatedCausesFromConstructorResolution() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONSTRUCTOR_ARG_CONTEXT); + + try { + xbf.getBean("rod2Accessor"); + } + catch (BeanCreationException ex) { + assertThat(ex.toString().contains("touchy")).isTrue(); + ex.printStackTrace(); + assertThat((Object) ex.getRelatedCauses()).isNull(); + } + } + + @Test + void constructorArgResolution() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONSTRUCTOR_ARG_CONTEXT); + TestBean kerry1 = (TestBean) xbf.getBean("kerry1"); + TestBean kerry2 = (TestBean) xbf.getBean("kerry2"); + + ConstructorDependenciesBean rod9 = (ConstructorDependenciesBean) xbf.getBean("rod9"); + assertThat(rod9.getAge()).isEqualTo(99); + ConstructorDependenciesBean rod9a = (ConstructorDependenciesBean) xbf.getBean("rod9", 98); + assertThat(rod9a.getAge()).isEqualTo(98); + ConstructorDependenciesBean rod9b = (ConstructorDependenciesBean) xbf.getBean("rod9", "myName"); + assertThat(rod9b.getName()).isEqualTo("myName"); + ConstructorDependenciesBean rod9c = (ConstructorDependenciesBean) xbf.getBean("rod9", 97); + assertThat(rod9c.getAge()).isEqualTo(97); + + ConstructorDependenciesBean rod10 = (ConstructorDependenciesBean) xbf.getBean("rod10"); + assertThat(rod10.getName()).isEqualTo(null); + + ConstructorDependenciesBean rod11 = (ConstructorDependenciesBean) xbf.getBean("rod11"); + assertThat(rod11.getSpouse1()).isEqualTo(kerry2); + + ConstructorDependenciesBean rod12 = (ConstructorDependenciesBean) xbf.getBean("rod12"); + assertThat(rod12.getSpouse1()).isEqualTo(kerry1); + assertThat(rod12.getSpouse2()).isNull(); + + ConstructorDependenciesBean rod13 = (ConstructorDependenciesBean) xbf.getBean("rod13"); + assertThat(rod13.getSpouse1()).isEqualTo(kerry1); + assertThat(rod13.getSpouse2()).isEqualTo(kerry2); + + ConstructorDependenciesBean rod14 = (ConstructorDependenciesBean) xbf.getBean("rod14"); + assertThat(rod14.getSpouse1()).isEqualTo(kerry1); + assertThat(rod14.getSpouse2()).isEqualTo(kerry2); + + ConstructorDependenciesBean rod15 = (ConstructorDependenciesBean) xbf.getBean("rod15"); + assertThat(rod15.getSpouse1()).isEqualTo(kerry2); + assertThat(rod15.getSpouse2()).isEqualTo(kerry1); + + ConstructorDependenciesBean rod16 = (ConstructorDependenciesBean) xbf.getBean("rod16"); + assertThat(rod16.getSpouse1()).isEqualTo(kerry2); + assertThat(rod16.getSpouse2()).isEqualTo(kerry1); + assertThat(rod16.getAge()).isEqualTo(29); + + ConstructorDependenciesBean rod17 = (ConstructorDependenciesBean) xbf.getBean("rod17"); + assertThat(rod17.getSpouse1()).isEqualTo(kerry1); + assertThat(rod17.getSpouse2()).isEqualTo(kerry2); + assertThat(rod17.getAge()).isEqualTo(29); + } + + @Test + void prototypeWithExplicitArguments() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONSTRUCTOR_ARG_CONTEXT); + SimpleConstructorArgBean cd1 = (SimpleConstructorArgBean) xbf.getBean("rod18"); + assertThat(cd1.getAge()).isEqualTo(0); + SimpleConstructorArgBean cd2 = (SimpleConstructorArgBean) xbf.getBean("rod18", 98); + assertThat(cd2.getAge()).isEqualTo(98); + SimpleConstructorArgBean cd3 = (SimpleConstructorArgBean) xbf.getBean("rod18", "myName"); + assertThat(cd3.getName()).isEqualTo("myName"); + SimpleConstructorArgBean cd4 = (SimpleConstructorArgBean) xbf.getBean("rod18"); + assertThat(cd4.getAge()).isEqualTo(0); + SimpleConstructorArgBean cd5 = (SimpleConstructorArgBean) xbf.getBean("rod18", 97); + assertThat(cd5.getAge()).isEqualTo(97); + } + + @Test + void constructorArgWithSingleMatch() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONSTRUCTOR_ARG_CONTEXT); + File file = (File) xbf.getBean("file"); + assertThat(file.getPath()).isEqualTo((File.separator + "test")); + } + + @Test + void throwsExceptionOnTooManyArguments() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONSTRUCTOR_ARG_CONTEXT); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + xbf.getBean("rod7", ConstructorDependenciesBean.class)); + } + + @Test + void throwsExceptionOnAmbiguousResolution() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONSTRUCTOR_ARG_CONTEXT); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> + xbf.getBean("rod8", ConstructorDependenciesBean.class)); + } + + @Test + void dependsOn() { + doTestDependencies(DEP_DEPENDSON_CONTEXT, 1); + } + + @Test + void dependsOnInInnerBean() { + doTestDependencies(DEP_DEPENDSON_INNER_CONTEXT, 4); + } + + @Test + void dependenciesThroughConstructorArguments() { + doTestDependencies(DEP_CARG_CONTEXT, 1); + } + + @Test + void dependenciesThroughConstructorArgumentAutowiring() { + doTestDependencies(DEP_CARG_AUTOWIRE_CONTEXT, 1); + } + + @Test + void dependenciesThroughConstructorArgumentsInInnerBean() { + doTestDependencies(DEP_CARG_INNER_CONTEXT, 1); + } + + @Test + void dependenciesThroughProperties() { + doTestDependencies(DEP_PROP, 1); + } + + @Test + void dependenciesThroughPropertiesWithInTheMiddle() { + doTestDependencies(DEP_PROP_MIDDLE_CONTEXT, 1); + } + + @Test + void dependenciesThroughPropertyAutowiringByName() { + doTestDependencies(DEP_PROP_ABN_CONTEXT, 1); + } + + @Test + void dependenciesThroughPropertyAutowiringByType() { + doTestDependencies(DEP_PROP_ABT_CONTEXT, 1); + } + + @Test + void dependenciesThroughPropertiesInInnerBean() { + doTestDependencies(DEP_PROP_INNER_CONTEXT, 1); + } + + private void doTestDependencies(ClassPathResource resource, int nrOfHoldingBeans) { + PreparingBean1.prepared = false; + PreparingBean1.destroyed = false; + PreparingBean2.prepared = false; + PreparingBean2.destroyed = false; + DependingBean.destroyCount = 0; + HoldingBean.destroyCount = 0; + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(resource); + xbf.preInstantiateSingletons(); + xbf.destroySingletons(); + assertThat(PreparingBean1.prepared).isTrue(); + assertThat(PreparingBean1.destroyed).isTrue(); + assertThat(PreparingBean2.prepared).isTrue(); + assertThat(PreparingBean2.destroyed).isTrue(); + assertThat(DependingBean.destroyCount).isEqualTo(nrOfHoldingBeans); + if (!xbf.getBeansOfType(HoldingBean.class, false, false).isEmpty()) { + assertThat(HoldingBean.destroyCount).isEqualTo(nrOfHoldingBeans); + } + } + + /** + * When using a BeanFactory. singletons are of course not pre-instantiated. + * So rubbish class names in bean defs must now not be 'resolved' when the + * bean def is being parsed, 'cos everything on a bean def is now lazy, but + * must rather only be picked up when the bean is instantiated. + */ + @Test + void classNotFoundWithDefaultBeanClassLoader() { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(factory).loadBeanDefinitions(CLASS_NOT_FOUND_CONTEXT); + // cool, no errors, so the rubbish class name in the bean def was not resolved + // let's resolve the bean definition; must blow up + assertThatExceptionOfType(CannotLoadBeanClassException.class).isThrownBy(() -> + factory.getBean("classNotFound")) + .withCauseInstanceOf(ClassNotFoundException.class) + .satisfies(ex -> assertThat(ex.getResourceDescription()).contains("classNotFound.xml")); + } + + @Test + void classNotFoundWithNoBeanClassLoader() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(bf); + reader.setBeanClassLoader(null); + reader.loadBeanDefinitions(CLASS_NOT_FOUND_CONTEXT); + assertThat(bf.getBeanDefinition("classNotFound").getBeanClassName()).isEqualTo("WhatALotOfRubbish"); + } + + @Test + void resourceAndInputStream() throws IOException { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(RESOURCE_CONTEXT); + // comes from "resourceImport.xml" + ResourceTestBean resource1 = (ResourceTestBean) xbf.getBean("resource1"); + // comes from "resource.xml" + ResourceTestBean resource2 = (ResourceTestBean) xbf.getBean("resource2"); + + assertThat(resource1.getResource() instanceof ClassPathResource).isTrue(); + StringWriter writer = new StringWriter(); + FileCopyUtils.copy(new InputStreamReader(resource1.getResource().getInputStream()), writer); + assertThat(writer.toString()).isEqualTo("test"); + writer = new StringWriter(); + FileCopyUtils.copy(new InputStreamReader(resource1.getInputStream()), writer); + assertThat(writer.toString()).isEqualTo("test"); + writer = new StringWriter(); + FileCopyUtils.copy(new InputStreamReader(resource2.getResource().getInputStream()), writer); + assertThat(writer.toString()).isEqualTo("test"); + writer = new StringWriter(); + FileCopyUtils.copy(new InputStreamReader(resource2.getInputStream()), writer); + assertThat(writer.toString()).isEqualTo("test"); + } + + @Test + void classPathResourceWithImport() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(RESOURCE_CONTEXT); + // comes from "resourceImport.xml" + xbf.getBean("resource1", ResourceTestBean.class); + // comes from "resource.xml" + xbf.getBean("resource2", ResourceTestBean.class); + } + + @Test + void urlResourceWithImport() { + URL url = getClass().getResource(RESOURCE_CONTEXT.getPath()); + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(new UrlResource(url)); + // comes from "resourceImport.xml" + xbf.getBean("resource1", ResourceTestBean.class); + // comes from "resource.xml" + xbf.getBean("resource2", ResourceTestBean.class); + } + + @Test + void fileSystemResourceWithImport() throws URISyntaxException { + String file = getClass().getResource(RESOURCE_CONTEXT.getPath()).toURI().getPath(); + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(new FileSystemResource(file)); + // comes from "resourceImport.xml" + xbf.getBean("resource1", ResourceTestBean.class); + // comes from "resource.xml" + xbf.getBean("resource2", ResourceTestBean.class); + } + + @Test + void recursiveImport() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(RECURSIVE_IMPORT_CONTEXT)); + } + + /** + * See SPR-10785 and SPR-11420 + * @since 3.2.8 and 4.0.2 + */ + @Test + @SuppressWarnings("deprecation") + void methodInjectedBeanMustBeOfSameEnhancedCglibSubclassTypeAcrossBeanFactories() { + Class firstClass = null; + + for (int i = 0; i < 10; i++) { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions(OVERRIDES_CONTEXT); + + final Class currentClass = bf.getBean("overrideOneMethod").getClass(); + assertThat(ClassUtils.isCglibProxyClass(currentClass)).as("Method injected bean class [" + currentClass + "] must be a CGLIB enhanced subclass.").isTrue(); + + if (firstClass == null) { + firstClass = currentClass; + } + else { + assertThat(currentClass).isEqualTo(firstClass); + } + } + } + + @Test + void lookupOverrideMethodsWithSetterInjection() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.loadBeanDefinitions(OVERRIDES_CONTEXT); + + lookupOverrideMethodsWithSetterInjection(xbf, "overrideOneMethod", true); + // Should work identically on subclass definition, in which lookup + // methods are inherited + lookupOverrideMethodsWithSetterInjection(xbf, "overrideInheritedMethod", true); + + // Check cost of repeated construction of beans with method overrides + // Will pick up misuse of CGLIB + int howMany = 100; + StopWatch sw = new StopWatch(); + sw.start("Look up " + howMany + " prototype bean instances with method overrides"); + for (int i = 0; i < howMany; i++) { + lookupOverrideMethodsWithSetterInjection(xbf, "overrideOnPrototype", false); + } + sw.stop(); + // System.out.println(sw); + if (!LogFactory.getLog(DefaultListableBeanFactory.class).isDebugEnabled()) { + assertThat(sw.getTotalTimeMillis() < 2000).isTrue(); + } + + // Now test distinct bean with swapped value in factory, to ensure the two are independent + OverrideOneMethod swappedOom = (OverrideOneMethod) xbf.getBean("overrideOneMethodSwappedReturnValues"); + + TestBean tb = swappedOom.getPrototypeDependency(); + assertThat(tb.getName()).isEqualTo("David"); + tb = swappedOom.protectedOverrideSingleton(); + assertThat(tb.getName()).isEqualTo("Jenny"); + } + + private void lookupOverrideMethodsWithSetterInjection(BeanFactory xbf, + String beanName, boolean singleton) { + OverrideOneMethod oom = (OverrideOneMethod) xbf.getBean(beanName); + + if (singleton) { + assertThat(xbf.getBean(beanName)).isSameAs(oom); + } + else { + assertThat(xbf.getBean(beanName)).isNotSameAs(oom); + } + + TestBean jenny1 = oom.getPrototypeDependency(); + assertThat(jenny1.getName()).isEqualTo("Jenny"); + TestBean jenny2 = oom.getPrototypeDependency(); + assertThat(jenny2.getName()).isEqualTo("Jenny"); + assertThat(jenny2).isNotSameAs(jenny1); + + // Check that the bean can invoke the overridden method on itself + // This differs from Spring's AOP support, which has a distinct notion + // of a "target" object, meaning that the target needs explicit knowledge + // of AOP proxying to invoke an advised method on itself. + TestBean jenny3 = oom.invokesOverriddenMethodOnSelf(); + assertThat(jenny3.getName()).isEqualTo("Jenny"); + assertThat(jenny3).isNotSameAs(jenny1); + + // Now try protected method, and singleton + TestBean dave1 = oom.protectedOverrideSingleton(); + assertThat(dave1.getName()).isEqualTo("David"); + TestBean dave2 = oom.protectedOverrideSingleton(); + assertThat(dave2.getName()).isEqualTo("David"); + assertThat(dave2).isSameAs(dave1); + } + + @Test + void replaceMethodOverrideWithSetterInjection() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.loadBeanDefinitions(DELEGATION_OVERRIDES_CONTEXT); + + OverrideOneMethod oom = (OverrideOneMethod) xbf.getBean("overrideOneMethod"); + + // Same contract as for overrides.xml + TestBean jenny1 = oom.getPrototypeDependency(); + assertThat(jenny1.getName()).isEqualTo("Jenny"); + TestBean jenny2 = oom.getPrototypeDependency(); + assertThat(jenny2.getName()).isEqualTo("Jenny"); + assertThat(jenny2).isNotSameAs(jenny1); + + TestBean notJenny = oom.getPrototypeDependency("someParam"); + assertThat(!"Jenny".equals(notJenny.getName())).isTrue(); + + // Now try protected method, and singleton + TestBean dave1 = oom.protectedOverrideSingleton(); + assertThat(dave1.getName()).isEqualTo("David"); + TestBean dave2 = oom.protectedOverrideSingleton(); + assertThat(dave2.getName()).isEqualTo("David"); + assertThat(dave2).isSameAs(dave1); + + // Check unadvised behaviour + String str = "woierowijeiowiej"; + assertThat(oom.echo(str)).isEqualTo(str); + + // Now test replace + String s = "this is not a palindrome"; + String reverse = new StringBuilder(s).reverse().toString(); + assertThat(oom.replaceMe(s)).as("Should have overridden to reverse, not echo").isEqualTo(reverse); + + assertThat(oom.replaceMe()).as("Should have overridden no-arg overloaded replaceMe method to return fixed value").isEqualTo(FixedMethodReplacer.VALUE); + + OverrideOneMethodSubclass ooms = (OverrideOneMethodSubclass) xbf.getBean("replaceVoidMethod"); + DoSomethingReplacer dos = (DoSomethingReplacer) xbf.getBean("doSomethingReplacer"); + assertThat(dos.lastArg).isEqualTo(null); + String s1 = ""; + String s2 = "foo bar black sheep"; + ooms.doSomething(s1); + assertThat(dos.lastArg).isEqualTo(s1); + ooms.doSomething(s2); + assertThat(dos.lastArg).isEqualTo(s2); + } + + @Test + void lookupOverrideOneMethodWithConstructorInjection() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.loadBeanDefinitions(CONSTRUCTOR_OVERRIDES_CONTEXT); + + ConstructorInjectedOverrides cio = (ConstructorInjectedOverrides) xbf.getBean("constructorOverrides"); + + // Check that the setter was invoked... + // We should be able to combine Constructor and + // Setter Injection + assertThat(cio.getSetterString()).as("Setter string was set").isEqualTo("from property element"); + + // Jenny is a singleton + TestBean jenny = (TestBean) xbf.getBean("jenny"); + assertThat(cio.getTestBean()).isSameAs(jenny); + assertThat(cio.getTestBean()).isSameAs(jenny); + FactoryMethods fm1 = cio.createFactoryMethods(); + FactoryMethods fm2 = cio.createFactoryMethods(); + assertThat(fm2).as("FactoryMethods reference is to a prototype").isNotSameAs(fm1); + assertThat(fm2.getTestBean()).as("The two prototypes hold the same singleton reference").isSameAs(fm1.getTestBean()); + } + + @Test + void rejectsOverrideOfBogusMethodName() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.loadBeanDefinitions(INVALID_NO_SUCH_METHOD_CONTEXT); + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> + xbf.getBean("constructorOverrides")) + .withMessageContaining("bogusMethod"); + } + + @Test + void serializableMethodReplacerAndSuperclass() throws IOException { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.loadBeanDefinitions(DELEGATION_OVERRIDES_CONTEXT); + SerializableMethodReplacerCandidate s = (SerializableMethodReplacerCandidate) xbf.getBean("serializableReplacer"); + String forwards = "this is forwards"; + String backwards = new StringBuilder(forwards).reverse().toString(); + assertThat(s.replaceMe(forwards)).isEqualTo(backwards); + // SPR-356: lookup methods & method replacers are not serializable. + assertThat(SerializationTestUtils.isSerializable(s)).as("Lookup methods and method replacers are not meant to be serializable.").isFalse(); + } + + @Test + void innerBeanInheritsScopeFromConcreteChildDefinition() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.loadBeanDefinitions(OVERRIDES_CONTEXT); + + TestBean jenny1 = (TestBean) xbf.getBean("jennyChild"); + assertThat(jenny1.getFriends().size()).isEqualTo(1); + Object friend1 = jenny1.getFriends().iterator().next(); + assertThat(friend1 instanceof TestBean).isTrue(); + + TestBean jenny2 = (TestBean) xbf.getBean("jennyChild"); + assertThat(jenny2.getFriends().size()).isEqualTo(1); + Object friend2 = jenny2.getFriends().iterator().next(); + assertThat(friend2 instanceof TestBean).isTrue(); + + assertThat(jenny2).isNotSameAs(jenny1); + assertThat(friend2).isNotSameAs(friend1); + } + + @Test + void constructorArgWithSingleSimpleTypeMatch() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONSTRUCTOR_ARG_CONTEXT); + + SingleSimpleTypeConstructorBean bean = (SingleSimpleTypeConstructorBean) xbf.getBean("beanWithBoolean"); + assertThat(bean.isSingleBoolean()).isTrue(); + + SingleSimpleTypeConstructorBean bean2 = (SingleSimpleTypeConstructorBean) xbf.getBean("beanWithBoolean2"); + assertThat(bean2.isSingleBoolean()).isTrue(); + } + + @Test + void constructorArgWithDoubleSimpleTypeMatch() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONSTRUCTOR_ARG_CONTEXT); + + SingleSimpleTypeConstructorBean bean = (SingleSimpleTypeConstructorBean) xbf.getBean("beanWithBooleanAndString"); + assertThat(bean.isSecondBoolean()).isTrue(); + assertThat(bean.getTestString()).isEqualTo("A String"); + + SingleSimpleTypeConstructorBean bean2 = (SingleSimpleTypeConstructorBean) xbf.getBean("beanWithBooleanAndString2"); + assertThat(bean2.isSecondBoolean()).isTrue(); + assertThat(bean2.getTestString()).isEqualTo("A String"); + } + + @Test + void doubleBooleanAutowire() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONSTRUCTOR_ARG_CONTEXT); + DoubleBooleanConstructorBean bean = (DoubleBooleanConstructorBean) xbf.getBean("beanWithDoubleBoolean"); + assertThat(bean.boolean1).isEqualTo(Boolean.TRUE); + assertThat(bean.boolean2).isEqualTo(Boolean.FALSE); + } + + @Test + void doubleBooleanAutowireWithIndex() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONSTRUCTOR_ARG_CONTEXT); + DoubleBooleanConstructorBean bean = (DoubleBooleanConstructorBean) xbf.getBean("beanWithDoubleBooleanAndIndex"); + assertThat(bean.boolean1).isEqualTo(Boolean.FALSE); + assertThat(bean.boolean2).isEqualTo(Boolean.TRUE); + } + + @Test + void lenientDependencyMatching() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONSTRUCTOR_ARG_CONTEXT); + LenientDependencyTestBean bean = (LenientDependencyTestBean) xbf.getBean("lenientDependencyTestBean"); + assertThat(bean.tb instanceof DerivedTestBean).isTrue(); + } + + @Test + void lenientDependencyMatchingFactoryMethod() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONSTRUCTOR_ARG_CONTEXT); + LenientDependencyTestBean bean = (LenientDependencyTestBean) xbf.getBean("lenientDependencyTestBeanFactoryMethod"); + assertThat(bean.tb instanceof DerivedTestBean).isTrue(); + } + + @Test + void nonLenientDependencyMatching() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONSTRUCTOR_ARG_CONTEXT); + AbstractBeanDefinition bd = (AbstractBeanDefinition) xbf.getBeanDefinition("lenientDependencyTestBean"); + bd.setLenientConstructorResolution(false); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + xbf.getBean("lenientDependencyTestBean")) + .satisfies(ex -> assertThat(ex.getMostSpecificCause().getMessage()).contains("Ambiguous")); + } + + @Test + void nonLenientDependencyMatchingFactoryMethod() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONSTRUCTOR_ARG_CONTEXT); + AbstractBeanDefinition bd = (AbstractBeanDefinition) xbf.getBeanDefinition("lenientDependencyTestBeanFactoryMethod"); + bd.setLenientConstructorResolution(false); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + xbf.getBean("lenientDependencyTestBeanFactoryMethod")) + .satisfies(ex -> assertThat(ex.getMostSpecificCause().getMessage()).contains("Ambiguous")); + } + + @Test + void javaLangStringConstructor() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONSTRUCTOR_ARG_CONTEXT); + AbstractBeanDefinition bd = (AbstractBeanDefinition) xbf.getBeanDefinition("string"); + bd.setLenientConstructorResolution(false); + String str = (String) xbf.getBean("string"); + assertThat(str).isEqualTo("test"); + } + + @Test + void customStringConstructor() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONSTRUCTOR_ARG_CONTEXT); + AbstractBeanDefinition bd = (AbstractBeanDefinition) xbf.getBeanDefinition("stringConstructor"); + bd.setLenientConstructorResolution(false); + StringConstructorTestBean tb = (StringConstructorTestBean) xbf.getBean("stringConstructor"); + assertThat(tb.name).isEqualTo("test"); + } + + @Test + void primitiveConstructorArray() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONSTRUCTOR_ARG_CONTEXT); + ConstructorArrayTestBean bean = (ConstructorArrayTestBean) xbf.getBean("constructorArray"); + assertThat(bean.array instanceof int[]).isTrue(); + assertThat(((int[]) bean.array).length).isEqualTo(1); + assertThat(((int[]) bean.array)[0]).isEqualTo(1); + } + + @Test + void indexedPrimitiveConstructorArray() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONSTRUCTOR_ARG_CONTEXT); + ConstructorArrayTestBean bean = (ConstructorArrayTestBean) xbf.getBean("indexedConstructorArray"); + assertThat(bean.array instanceof int[]).isTrue(); + assertThat(((int[]) bean.array).length).isEqualTo(1); + assertThat(((int[]) bean.array)[0]).isEqualTo(1); + } + + @Test + void stringConstructorArrayNoType() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONSTRUCTOR_ARG_CONTEXT); + ConstructorArrayTestBean bean = (ConstructorArrayTestBean) xbf.getBean("constructorArrayNoType"); + assertThat(bean.array instanceof String[]).isTrue(); + assertThat(((String[]) bean.array).length).isEqualTo(0); + } + + @Test + void stringConstructorArrayNoTypeNonLenient() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONSTRUCTOR_ARG_CONTEXT); + AbstractBeanDefinition bd = (AbstractBeanDefinition) xbf.getBeanDefinition("constructorArrayNoType"); + bd.setLenientConstructorResolution(false); + ConstructorArrayTestBean bean = (ConstructorArrayTestBean) xbf.getBean("constructorArrayNoType"); + assertThat(bean.array instanceof String[]).isTrue(); + assertThat(((String[]) bean.array).length).isEqualTo(0); + } + + @Test + void constructorWithUnresolvableParameterName() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(CONSTRUCTOR_ARG_CONTEXT); + AtomicInteger bean = (AtomicInteger) xbf.getBean("constructorUnresolvableName"); + assertThat(bean.get()).isEqualTo(1); + bean = (AtomicInteger) xbf.getBean("constructorUnresolvableNameWithIndex"); + assertThat(bean.get()).isEqualTo(1); + } + + @Test + void withDuplicateName() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + assertThatExceptionOfType(BeansException.class).isThrownBy(() -> + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(TEST_WITH_DUP_NAMES_CONTEXT)) + .withMessageContaining("Bean name 'foo'"); + } + + @Test + void withDuplicateNameInAlias() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + assertThatExceptionOfType(BeansException.class).isThrownBy(() -> + new XmlBeanDefinitionReader(xbf).loadBeanDefinitions(TEST_WITH_DUP_NAME_IN_ALIAS_CONTEXT)) + .withMessageContaining("Bean name 'foo'"); + } + + @Test + void overrideMethodByArgTypeAttribute() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.loadBeanDefinitions(DELEGATION_OVERRIDES_CONTEXT); + OverrideOneMethod oom = (OverrideOneMethod) xbf.getBean("overrideOneMethodByAttribute"); + assertThat(oom.replaceMe(1)).as("should not replace").isEqualTo("replaceMe:1"); + assertThat(oom.replaceMe("abc")).as("should replace").isEqualTo("cba"); + } + + @Test + void overrideMethodByArgTypeElement() { + DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(xbf); + reader.loadBeanDefinitions(DELEGATION_OVERRIDES_CONTEXT); + OverrideOneMethod oom = (OverrideOneMethod) xbf.getBean("overrideOneMethodByElement"); + assertThat(oom.replaceMe(1)).as("should not replace").isEqualTo("replaceMe:1"); + assertThat(oom.replaceMe("abc")).as("should replace").isEqualTo("cba"); + } + + static class DoSomethingReplacer implements MethodReplacer { + + public Object lastArg; + + @Override + public Object reimplement(Object obj, Method method, Object[] args) throws Throwable { + assertThat(args.length).isEqualTo(1); + assertThat(method.getName()).isEqualTo("doSomething"); + lastArg = args[0]; + return null; + } + } + + + static class BadInitializer { + + /** Init method */ + public void init2() throws IOException { + throw new IOException(); + } + } + + + static class DoubleInitializer { + + private int num; + + public int getNum() { + return num; + } + + public void setNum(int i) { + num = i; + } + + /** Init method */ + public void init() { + this.num *= 2; + } + } + + + static class InitAndIB implements InitializingBean, DisposableBean { + + public static boolean constructed; + + public boolean afterPropertiesSetInvoked, initMethodInvoked, destroyed, customDestroyed; + + public InitAndIB() { + constructed = true; + } + + @Override + public void afterPropertiesSet() { + assertThat(this.initMethodInvoked).isFalse(); + if (this.afterPropertiesSetInvoked) { + throw new IllegalStateException("Already initialized"); + } + this.afterPropertiesSetInvoked = true; + } + + /** Init method */ + public void customInit() throws IOException { + assertThat(this.afterPropertiesSetInvoked).isTrue(); + if (this.initMethodInvoked) { + throw new IllegalStateException("Already customInitialized"); + } + this.initMethodInvoked = true; + } + + @Override + public void destroy() { + assertThat(this.customDestroyed).isFalse(); + if (this.destroyed) { + throw new IllegalStateException("Already destroyed"); + } + this.destroyed = true; + } + + public void customDestroy() { + assertThat(this.destroyed).isTrue(); + if (this.customDestroyed) { + throw new IllegalStateException("Already customDestroyed"); + } + this.customDestroyed = true; + } + } + + + static class PreparingBean1 implements DisposableBean { + + public static boolean prepared = false; + + public static boolean destroyed = false; + + public PreparingBean1() { + prepared = true; + } + + @Override + public void destroy() { + destroyed = true; + } + } + + + static class PreparingBean2 implements DisposableBean { + + public static boolean prepared = false; + + public static boolean destroyed = false; + + public PreparingBean2() { + prepared = true; + } + + @Override + public void destroy() { + destroyed = true; + } + } + + + static class DependingBean implements InitializingBean, DisposableBean { + + public static int destroyCount = 0; + + public boolean destroyed = false; + + public DependingBean() { + } + + public DependingBean(PreparingBean1 bean1, PreparingBean2 bean2) { + } + + public void setBean1(PreparingBean1 bean1) { + } + + public void setBean2(PreparingBean2 bean2) { + } + + public void setInTheMiddleBean(InTheMiddleBean bean) { + } + + @Override + public void afterPropertiesSet() { + if (!(PreparingBean1.prepared && PreparingBean2.prepared)) { + throw new IllegalStateException("Need prepared PreparingBeans!"); + } + } + + @Override + public void destroy() { + if (PreparingBean1.destroyed || PreparingBean2.destroyed) { + throw new IllegalStateException("Should not be destroyed after PreparingBeans"); + } + destroyed = true; + destroyCount++; + } + } + + + static class InTheMiddleBean { + + public void setBean1(PreparingBean1 bean1) { + } + + public void setBean2(PreparingBean2 bean2) { + } + } + + + static class HoldingBean implements DisposableBean { + + public static int destroyCount = 0; + + private DependingBean dependingBean; + + public boolean destroyed = false; + + public void setDependingBean(DependingBean dependingBean) { + this.dependingBean = dependingBean; + } + + @Override + public void destroy() { + if (this.dependingBean.destroyed) { + throw new IllegalStateException("Should not be destroyed after DependingBean"); + } + this.destroyed = true; + destroyCount++; + } + } + + + static class DoubleBooleanConstructorBean { + + private Boolean boolean1; + private Boolean boolean2; + + public DoubleBooleanConstructorBean(Boolean b1, Boolean b2) { + this.boolean1 = b1; + this.boolean2 = b2; + } + + public DoubleBooleanConstructorBean(String s1, String s2) { + throw new IllegalStateException("Don't pick this constructor"); + } + + public static DoubleBooleanConstructorBean create(Boolean b1, Boolean b2) { + return new DoubleBooleanConstructorBean(b1, b2); + } + + public static DoubleBooleanConstructorBean create(String s1, String s2) { + return new DoubleBooleanConstructorBean(s1, s2); + } + } + + + static class LenientDependencyTestBean { + + public final ITestBean tb; + + public LenientDependencyTestBean(ITestBean tb) { + this.tb = tb; + } + + public LenientDependencyTestBean(TestBean tb) { + this.tb = tb; + } + + public LenientDependencyTestBean(DerivedTestBean tb) { + this.tb = tb; + } + + @SuppressWarnings("rawtypes") + public LenientDependencyTestBean(Map[] m) { + throw new IllegalStateException("Don't pick this constructor"); + } + + public static LenientDependencyTestBean create(ITestBean tb) { + return new LenientDependencyTestBean(tb); + } + + public static LenientDependencyTestBean create(TestBean tb) { + return new LenientDependencyTestBean(tb); + } + + public static LenientDependencyTestBean create(DerivedTestBean tb) { + return new LenientDependencyTestBean(tb); + } + } + + + static class ConstructorArrayTestBean { + + public final Object array; + + public ConstructorArrayTestBean(int[] array) { + this.array = array; + } + + public ConstructorArrayTestBean(float[] array) { + this.array = array; + } + + public ConstructorArrayTestBean(short[] array) { + this.array = array; + } + + public ConstructorArrayTestBean(String[] array) { + this.array = array; + } + } + + + static class StringConstructorTestBean { + + public final String name; + + public StringConstructorTestBean(String name) { + this.name = name; + } + } + + + static class WrappingPostProcessor implements BeanPostProcessor { + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + ProxyFactory pf = new ProxyFactory(bean); + return pf.getProxy(); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/beans/factory/xml/support/CustomNamespaceHandlerTests.java b/spring-context/src/test/java/org/springframework/beans/factory/xml/support/CustomNamespaceHandlerTests.java new file mode 100644 index 0000000..7b03b37 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/beans/factory/xml/support/CustomNamespaceHandlerTests.java @@ -0,0 +1,301 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.beans.factory.xml.support; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Attr; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.xml.sax.InputSource; + +import org.springframework.aop.Advisor; +import org.springframework.aop.config.AbstractInterceptorDrivenBeanDefinitionDecorator; +import org.springframework.aop.framework.Advised; +import org.springframework.aop.interceptor.DebugInterceptor; +import org.springframework.aop.support.AopUtils; +import org.springframework.aop.testfixture.interceptor.NopInterceptor; +import org.springframework.beans.BeanInstantiationException; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; +import org.springframework.beans.factory.xml.BeanDefinitionDecorator; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.DefaultNamespaceHandlerResolver; +import org.springframework.beans.factory.xml.NamespaceHandlerResolver; +import org.springframework.beans.factory.xml.NamespaceHandlerSupport; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.beans.factory.xml.PluggableSchemaResolver; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.ApplicationListener; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; + +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Unit tests for custom XML namespace handler implementations. + * + * @author Rob Harrop + * @author Rick Evans + * @author Chris Beams + * @author Juergen Hoeller + */ +public class CustomNamespaceHandlerTests { + + private static final Class CLASS = CustomNamespaceHandlerTests.class; + private static final String CLASSNAME = CLASS.getSimpleName(); + private static final String FQ_PATH = "org/springframework/beans/factory/xml/support"; + + private static final String NS_PROPS = format("%s/%s.properties", FQ_PATH, CLASSNAME); + private static final String NS_XML = format("%s/%s-context.xml", FQ_PATH, CLASSNAME); + private static final String TEST_XSD = format("%s/%s.xsd", FQ_PATH, CLASSNAME); + + private GenericApplicationContext beanFactory; + + + @BeforeEach + public void setUp() throws Exception { + NamespaceHandlerResolver resolver = new DefaultNamespaceHandlerResolver(CLASS.getClassLoader(), NS_PROPS); + this.beanFactory = new GenericApplicationContext(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this.beanFactory); + reader.setNamespaceHandlerResolver(resolver); + reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_XSD); + reader.setEntityResolver(new DummySchemaResolver()); + reader.loadBeanDefinitions(getResource()); + this.beanFactory.refresh(); + } + + + @Test + public void testSimpleParser() throws Exception { + TestBean bean = (TestBean) this.beanFactory.getBean("testBean"); + assertTestBean(bean); + } + + @Test + public void testSimpleDecorator() throws Exception { + TestBean bean = (TestBean) this.beanFactory.getBean("customisedTestBean"); + assertTestBean(bean); + } + + @Test + public void testProxyingDecorator() throws Exception { + ITestBean bean = (ITestBean) this.beanFactory.getBean("debuggingTestBean"); + assertTestBean(bean); + assertThat(AopUtils.isAopProxy(bean)).isTrue(); + Advisor[] advisors = ((Advised) bean).getAdvisors(); + assertThat(advisors.length).as("Incorrect number of advisors").isEqualTo(1); + assertThat(advisors[0].getAdvice().getClass()).as("Incorrect advice class").isEqualTo(DebugInterceptor.class); + } + + @Test + public void testProxyingDecoratorNoInstance() throws Exception { + String[] beanNames = this.beanFactory.getBeanNamesForType(ApplicationListener.class); + assertThat(Arrays.asList(beanNames).contains("debuggingTestBeanNoInstance")).isTrue(); + assertThat(this.beanFactory.getType("debuggingTestBeanNoInstance")).isEqualTo(ApplicationListener.class); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + this.beanFactory.getBean("debuggingTestBeanNoInstance")) + .havingRootCause() + .isInstanceOf(BeanInstantiationException.class); + } + + @Test + public void testChainedDecorators() throws Exception { + ITestBean bean = (ITestBean) this.beanFactory.getBean("chainedTestBean"); + assertTestBean(bean); + assertThat(AopUtils.isAopProxy(bean)).isTrue(); + Advisor[] advisors = ((Advised) bean).getAdvisors(); + assertThat(advisors.length).as("Incorrect number of advisors").isEqualTo(2); + assertThat(advisors[0].getAdvice().getClass()).as("Incorrect advice class").isEqualTo(DebugInterceptor.class); + assertThat(advisors[1].getAdvice().getClass()).as("Incorrect advice class").isEqualTo(NopInterceptor.class); + } + + @Test + public void testDecorationViaAttribute() throws Exception { + BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition("decorateWithAttribute"); + assertThat(beanDefinition.getAttribute("objectName")).isEqualTo("foo"); + } + + @Test // SPR-2728 + public void testCustomElementNestedWithinUtilList() throws Exception { + List things = (List) this.beanFactory.getBean("list.of.things"); + assertThat(things).isNotNull(); + assertThat(things.size()).isEqualTo(2); + } + + @Test // SPR-2728 + public void testCustomElementNestedWithinUtilSet() throws Exception { + Set things = (Set) this.beanFactory.getBean("set.of.things"); + assertThat(things).isNotNull(); + assertThat(things.size()).isEqualTo(2); + } + + @Test // SPR-2728 + public void testCustomElementNestedWithinUtilMap() throws Exception { + Map things = (Map) this.beanFactory.getBean("map.of.things"); + assertThat(things).isNotNull(); + assertThat(things.size()).isEqualTo(2); + } + + + private void assertTestBean(ITestBean bean) { + assertThat(bean.getName()).as("Invalid name").isEqualTo("Rob Harrop"); + assertThat(bean.getAge()).as("Invalid age").isEqualTo(23); + } + + private Resource getResource() { + return new ClassPathResource(NS_XML); + } + + + private final class DummySchemaResolver extends PluggableSchemaResolver { + + public DummySchemaResolver() { + super(CLASS.getClassLoader()); + } + + @Override + public InputSource resolveEntity(String publicId, String systemId) throws IOException { + InputSource source = super.resolveEntity(publicId, systemId); + if (source == null) { + Resource resource = new ClassPathResource(TEST_XSD); + source = new InputSource(resource.getInputStream()); + source.setPublicId(publicId); + source.setSystemId(systemId); + } + return source; + } + } + +} + + +/** + * Custom namespace handler implementation. + * + * @author Rob Harrop + */ +final class TestNamespaceHandler extends NamespaceHandlerSupport { + + @Override + public void init() { + registerBeanDefinitionParser("testBean", new TestBeanDefinitionParser()); + registerBeanDefinitionParser("person", new PersonDefinitionParser()); + + registerBeanDefinitionDecorator("set", new PropertyModifyingBeanDefinitionDecorator()); + registerBeanDefinitionDecorator("debug", new DebugBeanDefinitionDecorator()); + registerBeanDefinitionDecorator("nop", new NopInterceptorBeanDefinitionDecorator()); + registerBeanDefinitionDecoratorForAttribute("object-name", new ObjectNameBeanDefinitionDecorator()); + } + + + private static class TestBeanDefinitionParser implements BeanDefinitionParser { + + @Override + public BeanDefinition parse(Element element, ParserContext parserContext) { + RootBeanDefinition definition = new RootBeanDefinition(); + definition.setBeanClass(TestBean.class); + + MutablePropertyValues mpvs = new MutablePropertyValues(); + mpvs.add("name", element.getAttribute("name")); + mpvs.add("age", element.getAttribute("age")); + definition.setPropertyValues(mpvs); + + parserContext.getRegistry().registerBeanDefinition(element.getAttribute("id"), definition); + return null; + } + } + + + private static final class PersonDefinitionParser extends AbstractSingleBeanDefinitionParser { + + @Override + protected Class getBeanClass(Element element) { + return TestBean.class; + } + + @Override + protected void doParse(Element element, BeanDefinitionBuilder builder) { + builder.addPropertyValue("name", element.getAttribute("name")); + builder.addPropertyValue("age", element.getAttribute("age")); + } + } + + + private static class PropertyModifyingBeanDefinitionDecorator implements BeanDefinitionDecorator { + + @Override + public BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder definition, ParserContext parserContext) { + Element element = (Element) node; + BeanDefinition def = definition.getBeanDefinition(); + + MutablePropertyValues mpvs = (def.getPropertyValues() == null) ? new MutablePropertyValues() : def.getPropertyValues(); + mpvs.add("name", element.getAttribute("name")); + mpvs.add("age", element.getAttribute("age")); + + ((AbstractBeanDefinition) def).setPropertyValues(mpvs); + return definition; + } + } + + + private static class DebugBeanDefinitionDecorator extends AbstractInterceptorDrivenBeanDefinitionDecorator { + + @Override + protected BeanDefinition createInterceptorDefinition(Node node) { + return new RootBeanDefinition(DebugInterceptor.class); + } + } + + + private static class NopInterceptorBeanDefinitionDecorator extends AbstractInterceptorDrivenBeanDefinitionDecorator { + + @Override + protected BeanDefinition createInterceptorDefinition(Node node) { + return new RootBeanDefinition(NopInterceptor.class); + } + } + + + private static class ObjectNameBeanDefinitionDecorator implements BeanDefinitionDecorator { + + @Override + public BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder definition, ParserContext parserContext) { + Attr objectNameAttribute = (Attr) node; + definition.getBeanDefinition().setAttribute("objectName", objectNameAttribute.getValue()); + return definition; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java b/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java new file mode 100644 index 0000000..4c81623 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java @@ -0,0 +1,470 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.annotation.Caching; +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.concurrent.ConcurrentMapCache; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.cache.interceptor.AbstractCacheResolver; +import org.springframework.cache.interceptor.CacheOperationInvocationContext; +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.support.SimpleCacheManager; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Tests to reproduce raised caching issues. + * + * @author Phillip Webb + * @author Juergen Hoeller + * @author Stephane Nicoll + */ +public class CacheReproTests { + + @Test + public void spr11124MultipleAnnotations() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Spr11124Config.class); + Spr11124Service bean = context.getBean(Spr11124Service.class); + bean.single(2); + bean.single(2); + bean.multiple(2); + bean.multiple(2); + context.close(); + } + + @Test + public void spr11249PrimitiveVarargs() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Spr11249Config.class); + Spr11249Service bean = context.getBean(Spr11249Service.class); + Object result = bean.doSomething("op", 2, 3); + assertThat(bean.doSomething("op", 2, 3)).isSameAs(result); + context.close(); + } + + @Test + public void spr11592GetSimple() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Spr11592Config.class); + Spr11592Service bean = context.getBean(Spr11592Service.class); + Cache cache = context.getBean("cache", Cache.class); + + String key = "1"; + Object result = bean.getSimple("1"); + verify(cache, times(1)).get(key); // first call: cache miss + + Object cachedResult = bean.getSimple("1"); + assertThat(cachedResult).isSameAs(result); + verify(cache, times(2)).get(key); // second call: cache hit + + context.close(); + } + + @Test + public void spr11592GetNeverCache() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Spr11592Config.class); + Spr11592Service bean = context.getBean(Spr11592Service.class); + Cache cache = context.getBean("cache", Cache.class); + + String key = "1"; + Object result = bean.getNeverCache("1"); + verify(cache, times(0)).get(key); // no cache hit at all, caching disabled + + Object cachedResult = bean.getNeverCache("1"); + assertThat(cachedResult).isNotSameAs(result); + verify(cache, times(0)).get(key); // caching disabled + + context.close(); + } + + @Test + public void spr13081ConfigNoCacheNameIsRequired() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Spr13081Config.class); + MyCacheResolver cacheResolver = context.getBean(MyCacheResolver.class); + Spr13081Service bean = context.getBean(Spr13081Service.class); + + assertThat(cacheResolver.getCache("foo").get("foo")).isNull(); + Object result = bean.getSimple("foo"); // cache name = id + assertThat(cacheResolver.getCache("foo").get("foo").get()).isEqualTo(result); + } + + @Test + public void spr13081ConfigFailIfCacheResolverReturnsNullCacheName() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Spr13081Config.class); + Spr13081Service bean = context.getBean(Spr13081Service.class); + + assertThatIllegalStateException().isThrownBy(() -> + bean.getSimple(null)) + .withMessageContaining(MyCacheResolver.class.getName()); + } + + @Test + public void spr14230AdaptsToOptional() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Spr14230Config.class); + Spr14230Service bean = context.getBean(Spr14230Service.class); + Cache cache = context.getBean(CacheManager.class).getCache("itemCache"); + + TestBean tb = new TestBean("tb1"); + bean.insertItem(tb); + assertThat(bean.findById("tb1").get()).isSameAs(tb); + assertThat(cache.get("tb1").get()).isSameAs(tb); + + cache.clear(); + TestBean tb2 = bean.findById("tb1").get(); + assertThat(tb2).isNotSameAs(tb); + assertThat(cache.get("tb1").get()).isSameAs(tb2); + } + + @Test + public void spr14853AdaptsToOptionalWithSync() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Spr14853Config.class); + Spr14853Service bean = context.getBean(Spr14853Service.class); + Cache cache = context.getBean(CacheManager.class).getCache("itemCache"); + + TestBean tb = new TestBean("tb1"); + bean.insertItem(tb); + assertThat(bean.findById("tb1").get()).isSameAs(tb); + assertThat(cache.get("tb1").get()).isSameAs(tb); + + cache.clear(); + TestBean tb2 = bean.findById("tb1").get(); + assertThat(tb2).isNotSameAs(tb); + assertThat(cache.get("tb1").get()).isSameAs(tb2); + } + + @Test + public void spr15271FindsOnInterfaceWithInterfaceProxy() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Spr15271ConfigA.class); + Spr15271Interface bean = context.getBean(Spr15271Interface.class); + Cache cache = context.getBean(CacheManager.class).getCache("itemCache"); + + TestBean tb = new TestBean("tb1"); + bean.insertItem(tb); + assertThat(bean.findById("tb1").get()).isSameAs(tb); + assertThat(cache.get("tb1").get()).isSameAs(tb); + } + + @Test + public void spr15271FindsOnInterfaceWithCglibProxy() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Spr15271ConfigB.class); + Spr15271Interface bean = context.getBean(Spr15271Interface.class); + Cache cache = context.getBean(CacheManager.class).getCache("itemCache"); + + TestBean tb = new TestBean("tb1"); + bean.insertItem(tb); + assertThat(bean.findById("tb1").get()).isSameAs(tb); + assertThat(cache.get("tb1").get()).isSameAs(tb); + } + + + @Configuration + @EnableCaching + public static class Spr11124Config { + + @Bean + public CacheManager cacheManager() { + return new ConcurrentMapCacheManager(); + } + + @Bean + public Spr11124Service service() { + return new Spr11124ServiceImpl(); + } + } + + + public interface Spr11124Service { + + List single(int id); + + List multiple(int id); + } + + + public static class Spr11124ServiceImpl implements Spr11124Service { + + private int multipleCount = 0; + + @Override + @Cacheable("smallCache") + public List single(int id) { + if (this.multipleCount > 0) { + throw new AssertionError("Called too many times"); + } + this.multipleCount++; + return Collections.emptyList(); + } + + @Override + @Caching(cacheable = { + @Cacheable(cacheNames = "bigCache", unless = "#result.size() < 4"), + @Cacheable(cacheNames = "smallCache", unless = "#result.size() > 3")}) + public List multiple(int id) { + if (this.multipleCount > 0) { + throw new AssertionError("Called too many times"); + } + this.multipleCount++; + return Collections.emptyList(); + } + } + + + @Configuration + @EnableCaching + public static class Spr11249Config { + + @Bean + public CacheManager cacheManager() { + return new ConcurrentMapCacheManager(); + } + + @Bean + public Spr11249Service service() { + return new Spr11249Service(); + } + } + + + public static class Spr11249Service { + + @Cacheable("smallCache") + public Object doSomething(String name, int... values) { + return new Object(); + } + } + + + @Configuration + @EnableCaching + public static class Spr11592Config { + + @Bean + public CacheManager cacheManager() { + SimpleCacheManager cacheManager = new SimpleCacheManager(); + cacheManager.setCaches(Collections.singletonList(cache())); + return cacheManager; + } + + @Bean + public Cache cache() { + Cache cache = new ConcurrentMapCache("cache"); + return Mockito.spy(cache); + } + + @Bean + public Spr11592Service service() { + return new Spr11592Service(); + } + } + + + public static class Spr11592Service { + + @Cacheable("cache") + public Object getSimple(String key) { + return new Object(); + } + + @Cacheable(cacheNames = "cache", condition = "false") + public Object getNeverCache(String key) { + return new Object(); + } + } + + + @Configuration + @EnableCaching + public static class Spr13081Config extends CachingConfigurerSupport { + + @Bean + @Override + public CacheResolver cacheResolver() { + return new MyCacheResolver(); + } + + @Bean + public Spr13081Service service() { + return new Spr13081Service(); + } + } + + + public static class MyCacheResolver extends AbstractCacheResolver { + + public MyCacheResolver() { + super(new ConcurrentMapCacheManager()); + } + + @Override + @Nullable + protected Collection getCacheNames(CacheOperationInvocationContext context) { + String cacheName = (String) context.getArgs()[0]; + if (cacheName != null) { + return Collections.singleton(cacheName); + } + return null; + } + + public Cache getCache(String name) { + return getCacheManager().getCache(name); + } + } + + + public static class Spr13081Service { + + @Cacheable + public Object getSimple(String cacheName) { + return new Object(); + } + } + + + public static class Spr14230Service { + + @Cacheable("itemCache") + public Optional findById(String id) { + return Optional.of(new TestBean(id)); + } + + @CachePut(cacheNames = "itemCache", key = "#item.name") + public TestBean insertItem(TestBean item) { + return item; + } + } + + + @Configuration + @EnableCaching + public static class Spr14230Config { + + @Bean + public CacheManager cacheManager() { + return new ConcurrentMapCacheManager(); + } + + @Bean + public Spr14230Service service() { + return new Spr14230Service(); + } + } + + + public static class Spr14853Service { + + @Cacheable(value = "itemCache", sync = true) + public Optional findById(String id) { + return Optional.of(new TestBean(id)); + } + + @CachePut(cacheNames = "itemCache", key = "#item.name") + public TestBean insertItem(TestBean item) { + return item; + } + } + + + @Configuration + @EnableCaching + public static class Spr14853Config { + + @Bean + public CacheManager cacheManager() { + return new ConcurrentMapCacheManager(); + } + + @Bean + public Spr14853Service service() { + return new Spr14853Service(); + } + } + + + public interface Spr15271Interface { + + @Cacheable(value = "itemCache", sync = true) + Optional findById(String id); + + @CachePut(cacheNames = "itemCache", key = "#item.name") + TestBean insertItem(TestBean item); + } + + + public static class Spr15271Service implements Spr15271Interface { + + @Override + public Optional findById(String id) { + return Optional.of(new TestBean(id)); + } + + @Override + public TestBean insertItem(TestBean item) { + return item; + } + } + + + @Configuration + @EnableCaching + public static class Spr15271ConfigA { + + @Bean + public CacheManager cacheManager() { + return new ConcurrentMapCacheManager(); + } + + @Bean + public Spr15271Interface service() { + return new Spr15271Service(); + } + } + + + @Configuration + @EnableCaching(proxyTargetClass = true) + public static class Spr15271ConfigB { + + @Bean + public CacheManager cacheManager() { + return new ConcurrentMapCacheManager(); + } + + @Bean + public Spr15271Interface service() { + return new Spr15271Service(); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/cache/NoOpCacheManagerTests.java b/spring-context/src/test/java/org/springframework/cache/NoOpCacheManagerTests.java new file mode 100644 index 0000000..01867a4 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/cache/NoOpCacheManagerTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2010-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache; + +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +import org.springframework.cache.support.NoOpCacheManager; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link NoOpCacheManager}. + * + * @author Costin Leau + * @author Stephane Nicoll + */ +public class NoOpCacheManagerTests { + + private final CacheManager manager = new NoOpCacheManager(); + + @Test + public void testGetCache() throws Exception { + Cache cache = this.manager.getCache("bucket"); + assertThat(cache).isNotNull(); + assertThat(this.manager.getCache("bucket")).isSameAs(cache); + } + + @Test + public void testNoOpCache() throws Exception { + String name = createRandomKey(); + Cache cache = this.manager.getCache(name); + assertThat(cache.getName()).isEqualTo(name); + Object key = new Object(); + cache.put(key, new Object()); + assertThat(cache.get(key)).isNull(); + assertThat(cache.get(key, Object.class)).isNull(); + assertThat(cache.getNativeCache()).isSameAs(cache); + } + + @Test + public void testCacheName() throws Exception { + String name = "bucket"; + assertThat(this.manager.getCacheNames().contains(name)).isFalse(); + this.manager.getCache(name); + assertThat(this.manager.getCacheNames().contains(name)).isTrue(); + } + + @Test + public void testCacheCallable() throws Exception { + String name = createRandomKey(); + Cache cache = this.manager.getCache(name); + Object returnValue = new Object(); + Object value = cache.get(new Object(), () -> returnValue); + assertThat(value).isEqualTo(returnValue); + } + + @Test + public void testCacheGetCallableFail() { + Cache cache = this.manager.getCache(createRandomKey()); + String key = createRandomKey(); + try { + cache.get(key, () -> { + throw new UnsupportedOperationException("Expected exception"); + }); + } + catch (Cache.ValueRetrievalException ex) { + assertThat(ex.getCause()).isNotNull(); + assertThat(ex.getCause().getClass()).isEqualTo(UnsupportedOperationException.class); + } + } + + private String createRandomKey() { + return UUID.randomUUID().toString(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/cache/annotation/AnnotationCacheOperationSourceTests.java b/spring-context/src/test/java/org/springframework/cache/annotation/AnnotationCacheOperationSourceTests.java new file mode 100644 index 0000000..43366d5 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/cache/annotation/AnnotationCacheOperationSourceTests.java @@ -0,0 +1,556 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; + +import org.junit.jupiter.api.Test; + +import org.springframework.cache.interceptor.CacheEvictOperation; +import org.springframework.cache.interceptor.CacheOperation; +import org.springframework.cache.interceptor.CacheableOperation; +import org.springframework.core.annotation.AliasFor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * @author Costin Leau + * @author Stephane Nicoll + * @author Sam Brannen + */ +public class AnnotationCacheOperationSourceTests { + + private final AnnotationCacheOperationSource source = new AnnotationCacheOperationSource(); + + + @Test + public void singularAnnotation() { + Collection ops = getOps(AnnotatedClass.class, "singular", 1); + assertThat(ops.iterator().next() instanceof CacheableOperation).isTrue(); + } + + @Test + public void multipleAnnotation() { + Collection ops = getOps(AnnotatedClass.class, "multiple", 2); + Iterator it = ops.iterator(); + assertThat(it.next() instanceof CacheableOperation).isTrue(); + assertThat(it.next() instanceof CacheEvictOperation).isTrue(); + } + + @Test + public void caching() { + Collection ops = getOps(AnnotatedClass.class, "caching", 2); + Iterator it = ops.iterator(); + assertThat(it.next() instanceof CacheableOperation).isTrue(); + assertThat(it.next() instanceof CacheEvictOperation).isTrue(); + } + + @Test + public void emptyCaching() { + getOps(AnnotatedClass.class, "emptyCaching", 0); + } + + @Test + public void singularStereotype() { + Collection ops = getOps(AnnotatedClass.class, "singleStereotype", 1); + assertThat(ops.iterator().next() instanceof CacheEvictOperation).isTrue(); + } + + @Test + public void multipleStereotypes() { + Collection ops = getOps(AnnotatedClass.class, "multipleStereotype", 3); + Iterator it = ops.iterator(); + assertThat(it.next() instanceof CacheableOperation).isTrue(); + CacheOperation next = it.next(); + assertThat(next instanceof CacheEvictOperation).isTrue(); + assertThat(next.getCacheNames().contains("foo")).isTrue(); + next = it.next(); + assertThat(next instanceof CacheEvictOperation).isTrue(); + assertThat(next.getCacheNames().contains("bar")).isTrue(); + } + + @Test + public void singleComposedAnnotation() { + Collection ops = getOps(AnnotatedClass.class, "singleComposed", 2); + Iterator it = ops.iterator(); + + CacheOperation cacheOperation = it.next(); + assertThat(cacheOperation).isInstanceOf(CacheableOperation.class); + assertThat(cacheOperation.getCacheNames()).isEqualTo(Collections.singleton("directly declared")); + assertThat(cacheOperation.getKey()).isEqualTo(""); + + cacheOperation = it.next(); + assertThat(cacheOperation).isInstanceOf(CacheableOperation.class); + assertThat(cacheOperation.getCacheNames()).isEqualTo(Collections.singleton("composedCache")); + assertThat(cacheOperation.getKey()).isEqualTo("composedKey"); + } + + @Test + public void multipleComposedAnnotations() { + Collection ops = getOps(AnnotatedClass.class, "multipleComposed", 4); + Iterator it = ops.iterator(); + + CacheOperation cacheOperation = it.next(); + assertThat(cacheOperation).isInstanceOf(CacheableOperation.class); + assertThat(cacheOperation.getCacheNames()).isEqualTo(Collections.singleton("directly declared")); + assertThat(cacheOperation.getKey()).isEqualTo(""); + + cacheOperation = it.next(); + assertThat(cacheOperation).isInstanceOf(CacheableOperation.class); + assertThat(cacheOperation.getCacheNames()).isEqualTo(Collections.singleton("composedCache")); + assertThat(cacheOperation.getKey()).isEqualTo("composedKey"); + + cacheOperation = it.next(); + assertThat(cacheOperation).isInstanceOf(CacheableOperation.class); + assertThat(cacheOperation.getCacheNames()).isEqualTo(Collections.singleton("foo")); + assertThat(cacheOperation.getKey()).isEqualTo(""); + + cacheOperation = it.next(); + assertThat(cacheOperation).isInstanceOf(CacheEvictOperation.class); + assertThat(cacheOperation.getCacheNames()).isEqualTo(Collections.singleton("composedCacheEvict")); + assertThat(cacheOperation.getKey()).isEqualTo("composedEvictionKey"); + } + + @Test + public void customKeyGenerator() { + Collection ops = getOps(AnnotatedClass.class, "customKeyGenerator", 1); + CacheOperation cacheOperation = ops.iterator().next(); + assertThat(cacheOperation.getKeyGenerator()).as("Custom key generator not set").isEqualTo("custom"); + } + + @Test + public void customKeyGeneratorInherited() { + Collection ops = getOps(AnnotatedClass.class, "customKeyGeneratorInherited", 1); + CacheOperation cacheOperation = ops.iterator().next(); + assertThat(cacheOperation.getKeyGenerator()).as("Custom key generator not set").isEqualTo("custom"); + } + + @Test + public void keyAndKeyGeneratorCannotBeSetTogether() { + assertThatIllegalStateException().isThrownBy(() -> + getOps(AnnotatedClass.class, "invalidKeyAndKeyGeneratorSet")); + } + + @Test + public void customCacheManager() { + Collection ops = getOps(AnnotatedClass.class, "customCacheManager", 1); + CacheOperation cacheOperation = ops.iterator().next(); + assertThat(cacheOperation.getCacheManager()).as("Custom cache manager not set").isEqualTo("custom"); + } + + @Test + public void customCacheManagerInherited() { + Collection ops = getOps(AnnotatedClass.class, "customCacheManagerInherited", 1); + CacheOperation cacheOperation = ops.iterator().next(); + assertThat(cacheOperation.getCacheManager()).as("Custom cache manager not set").isEqualTo("custom"); + } + + @Test + public void customCacheResolver() { + Collection ops = getOps(AnnotatedClass.class, "customCacheResolver", 1); + CacheOperation cacheOperation = ops.iterator().next(); + assertThat(cacheOperation.getCacheResolver()).as("Custom cache resolver not set").isEqualTo("custom"); + } + + @Test + public void customCacheResolverInherited() { + Collection ops = getOps(AnnotatedClass.class, "customCacheResolverInherited", 1); + CacheOperation cacheOperation = ops.iterator().next(); + assertThat(cacheOperation.getCacheResolver()).as("Custom cache resolver not set").isEqualTo("custom"); + } + + @Test + public void cacheResolverAndCacheManagerCannotBeSetTogether() { + assertThatIllegalStateException().isThrownBy(() -> + getOps(AnnotatedClass.class, "invalidCacheResolverAndCacheManagerSet")); + } + + @Test + public void fullClassLevelWithCustomCacheName() { + Collection ops = getOps(AnnotatedClassWithFullDefault.class, "methodLevelCacheName", 1); + CacheOperation cacheOperation = ops.iterator().next(); + assertSharedConfig(cacheOperation, "classKeyGenerator", "", "classCacheResolver", "custom"); + } + + @Test + public void fullClassLevelWithCustomKeyManager() { + Collection ops = getOps(AnnotatedClassWithFullDefault.class, "methodLevelKeyGenerator", 1); + CacheOperation cacheOperation = ops.iterator().next(); + assertSharedConfig(cacheOperation, "custom", "", "classCacheResolver" , "classCacheName"); + } + + @Test + public void fullClassLevelWithCustomCacheManager() { + Collection ops = getOps(AnnotatedClassWithFullDefault.class, "methodLevelCacheManager", 1); + CacheOperation cacheOperation = ops.iterator().next(); + assertSharedConfig(cacheOperation, "classKeyGenerator", "custom", "", "classCacheName"); + } + + @Test + public void fullClassLevelWithCustomCacheResolver() { + Collection ops = getOps(AnnotatedClassWithFullDefault.class, "methodLevelCacheResolver", 1); + CacheOperation cacheOperation = ops.iterator().next(); + assertSharedConfig(cacheOperation, "classKeyGenerator", "", "custom" , "classCacheName"); + } + + @Test + public void validateNoCacheIsValid() { + // Valid as a CacheResolver might return the cache names to use with other info + Collection ops = getOps(AnnotatedClass.class, "noCacheNameSpecified"); + CacheOperation cacheOperation = ops.iterator().next(); + assertThat(cacheOperation.getCacheNames()).as("cache names set must not be null").isNotNull(); + assertThat(cacheOperation.getCacheNames().size()).as("no cache names specified").isEqualTo(0); + } + + @Test + public void customClassLevelWithCustomCacheName() { + Collection ops = getOps(AnnotatedClassWithCustomDefault.class, "methodLevelCacheName", 1); + CacheOperation cacheOperation = ops.iterator().next(); + assertSharedConfig(cacheOperation, "classKeyGenerator", "", "classCacheResolver", "custom"); + } + + @Test + public void severalCacheConfigUseClosest() { + Collection ops = getOps(MultipleCacheConfig.class, "multipleCacheConfig"); + CacheOperation cacheOperation = ops.iterator().next(); + assertSharedConfig(cacheOperation, "", "", "", "myCache"); + } + + @Test + public void cacheConfigFromInterface() { + Collection ops = getOps(InterfaceCacheConfig.class, "interfaceCacheConfig"); + CacheOperation cacheOperation = ops.iterator().next(); + assertSharedConfig(cacheOperation, "", "", "", "myCache"); + } + + @Test + public void cacheAnnotationOverride() { + Collection ops = getOps(InterfaceCacheConfig.class, "interfaceCacheableOverride"); + assertThat(ops.size()).isSameAs(1); + CacheOperation cacheOperation = ops.iterator().next(); + assertThat(cacheOperation instanceof CacheableOperation).isTrue(); + } + + @Test + public void partialClassLevelWithCustomCacheManager() { + Collection ops = getOps(AnnotatedClassWithSomeDefault.class, "methodLevelCacheManager", 1); + CacheOperation cacheOperation = ops.iterator().next(); + assertSharedConfig(cacheOperation, "classKeyGenerator", "custom", "", "classCacheName"); + } + + @Test + public void partialClassLevelWithCustomCacheResolver() { + Collection ops = getOps(AnnotatedClassWithSomeDefault.class, "methodLevelCacheResolver", 1); + CacheOperation cacheOperation = ops.iterator().next(); + assertSharedConfig(cacheOperation, "classKeyGenerator", "", "custom", "classCacheName"); + } + + @Test + public void partialClassLevelWithNoCustomization() { + Collection ops = getOps(AnnotatedClassWithSomeDefault.class, "noCustomization", 1); + CacheOperation cacheOperation = ops.iterator().next(); + assertSharedConfig(cacheOperation, "classKeyGenerator", "classCacheManager", "", "classCacheName"); + } + + + private Collection getOps(Class target, String name, int expectedNumberOfOperations) { + Collection result = getOps(target, name); + assertThat(result.size()).as("Wrong number of operation(s) for '" + name + "'").isEqualTo(expectedNumberOfOperations); + return result; + } + + private Collection getOps(Class target, String name) { + try { + Method method = target.getMethod(name); + return this.source.getCacheOperations(method, target); + } + catch (NoSuchMethodException ex) { + throw new IllegalStateException(ex); + } + } + + private void assertSharedConfig(CacheOperation actual, String keyGenerator, String cacheManager, + String cacheResolver, String... cacheNames) { + + assertThat(actual.getKeyGenerator()).as("Wrong key manager").isEqualTo(keyGenerator); + assertThat(actual.getCacheManager()).as("Wrong cache manager").isEqualTo(cacheManager); + assertThat(actual.getCacheResolver()).as("Wrong cache resolver").isEqualTo(cacheResolver); + assertThat(actual.getCacheNames().size()).as("Wrong number of cache names").isEqualTo(cacheNames.length); + Arrays.stream(cacheNames).forEach(cacheName -> assertThat(actual.getCacheNames().contains(cacheName)).as("Cache '" + cacheName + "' not found in " + actual.getCacheNames()).isTrue()); + } + + + private static class AnnotatedClass { + + @Cacheable("test") + public void singular() { + } + + @CacheEvict("test") + @Cacheable("test") + public void multiple() { + } + + @Caching(cacheable = @Cacheable("test"), evict = @CacheEvict("test")) + public void caching() { + } + + @Caching + public void emptyCaching() { + } + + @Cacheable(cacheNames = "test", keyGenerator = "custom") + public void customKeyGenerator() { + } + + @Cacheable(cacheNames = "test", cacheManager = "custom") + public void customCacheManager() { + } + + @Cacheable(cacheNames = "test", cacheResolver = "custom") + public void customCacheResolver() { + } + + @EvictFoo + public void singleStereotype() { + } + + @EvictFoo + @CacheableFoo + @EvictBar + public void multipleStereotype() { + } + + @Cacheable("directly declared") + @ComposedCacheable(cacheNames = "composedCache", key = "composedKey") + public void singleComposed() { + } + + @Cacheable("directly declared") + @ComposedCacheable(cacheNames = "composedCache", key = "composedKey") + @CacheableFoo + @ComposedCacheEvict(cacheNames = "composedCacheEvict", key = "composedEvictionKey") + public void multipleComposed() { + } + + @Caching(cacheable = { @Cacheable(cacheNames = "test", key = "a"), @Cacheable(cacheNames = "test", key = "b") }) + public void multipleCaching() { + } + + @CacheableFooCustomKeyGenerator + public void customKeyGeneratorInherited() { + } + + @Cacheable(cacheNames = "test", key = "#root.methodName", keyGenerator = "custom") + public void invalidKeyAndKeyGeneratorSet() { + } + + @CacheableFooCustomCacheManager + public void customCacheManagerInherited() { + } + + @CacheableFooCustomCacheResolver + public void customCacheResolverInherited() { + } + + @Cacheable(cacheNames = "test", cacheManager = "custom", cacheResolver = "custom") + public void invalidCacheResolverAndCacheManagerSet() { + } + + @Cacheable // cache name can be inherited from CacheConfig. There's none here + public void noCacheNameSpecified() { + } + } + + + @CacheConfig(cacheNames = "classCacheName", + keyGenerator = "classKeyGenerator", + cacheManager = "classCacheManager", cacheResolver = "classCacheResolver") + private static class AnnotatedClassWithFullDefault { + + @Cacheable("custom") + public void methodLevelCacheName() { + } + + @Cacheable(keyGenerator = "custom") + public void methodLevelKeyGenerator() { + } + + @Cacheable(cacheManager = "custom") + public void methodLevelCacheManager() { + } + + @Cacheable(cacheResolver = "custom") + public void methodLevelCacheResolver() { + } + } + + + @CacheConfigFoo + private static class AnnotatedClassWithCustomDefault { + + @Cacheable("custom") + public void methodLevelCacheName() { + } + } + + + @CacheConfig(cacheNames = "classCacheName", + keyGenerator = "classKeyGenerator", + cacheManager = "classCacheManager") + private static class AnnotatedClassWithSomeDefault { + + @Cacheable(cacheManager = "custom") + public void methodLevelCacheManager() { + } + + @Cacheable(cacheResolver = "custom") + public void methodLevelCacheResolver() { + } + + @Cacheable + public void noCustomization() { + } + } + + + @CacheConfigFoo + @CacheConfig(cacheNames = "myCache") // multiple sources + private static class MultipleCacheConfig { + + @Cacheable + public void multipleCacheConfig() { + } + } + + + @CacheConfig(cacheNames = "myCache") + private interface CacheConfigIfc { + + @Cacheable + void interfaceCacheConfig(); + + @CachePut + void interfaceCacheableOverride(); + } + + + private static class InterfaceCacheConfig implements CacheConfigIfc { + + @Override + public void interfaceCacheConfig() { + } + + @Override + @Cacheable + public void interfaceCacheableOverride() { + } + } + + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + @Cacheable("foo") + public @interface CacheableFoo { + } + + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + @Cacheable(cacheNames = "foo", keyGenerator = "custom") + public @interface CacheableFooCustomKeyGenerator { + } + + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + @Cacheable(cacheNames = "foo", cacheManager = "custom") + public @interface CacheableFooCustomCacheManager { + } + + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + @Cacheable(cacheNames = "foo", cacheResolver = "custom") + public @interface CacheableFooCustomCacheResolver { + } + + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + @CacheEvict("foo") + public @interface EvictFoo { + } + + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + @CacheEvict("bar") + public @interface EvictBar { + } + + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @CacheConfig(keyGenerator = "classKeyGenerator", + cacheManager = "classCacheManager", + cacheResolver = "classCacheResolver") + public @interface CacheConfigFoo { + } + + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.METHOD, ElementType.TYPE}) + @Cacheable(cacheNames = "shadowed cache name", key = "shadowed key") + @interface ComposedCacheable { + + @AliasFor(annotation = Cacheable.class) + String[] value() default {}; + + @AliasFor(annotation = Cacheable.class) + String[] cacheNames() default {}; + + @AliasFor(annotation = Cacheable.class) + String key() default ""; + } + + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.METHOD, ElementType.TYPE}) + @CacheEvict(cacheNames = "shadowed cache name", key = "shadowed key") + @interface ComposedCacheEvict { + + @AliasFor(annotation = CacheEvict.class) + String[] value() default {}; + + @AliasFor(annotation = CacheEvict.class) + String[] cacheNames() default {}; + + @AliasFor(annotation = CacheEvict.class) + String key() default ""; + } + +} diff --git a/spring-context/src/test/java/org/springframework/cache/concurrent/ConcurrentMapCacheManagerTests.java b/spring-context/src/test/java/org/springframework/cache/concurrent/ConcurrentMapCacheManagerTests.java new file mode 100644 index 0000000..811b55b --- /dev/null +++ b/spring-context/src/test/java/org/springframework/cache/concurrent/ConcurrentMapCacheManagerTests.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.concurrent; + +import org.junit.jupiter.api.Test; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @author Stephane Nicoll + */ +public class ConcurrentMapCacheManagerTests { + + @Test + public void testDynamicMode() { + CacheManager cm = new ConcurrentMapCacheManager(); + Cache cache1 = cm.getCache("c1"); + assertThat(cache1 instanceof ConcurrentMapCache).isTrue(); + Cache cache1again = cm.getCache("c1"); + assertThat(cache1).isSameAs(cache1again); + Cache cache2 = cm.getCache("c2"); + assertThat(cache2 instanceof ConcurrentMapCache).isTrue(); + Cache cache2again = cm.getCache("c2"); + assertThat(cache2).isSameAs(cache2again); + Cache cache3 = cm.getCache("c3"); + assertThat(cache3 instanceof ConcurrentMapCache).isTrue(); + Cache cache3again = cm.getCache("c3"); + assertThat(cache3).isSameAs(cache3again); + + cache1.put("key1", "value1"); + assertThat(cache1.get("key1").get()).isEqualTo("value1"); + cache1.put("key2", 2); + assertThat(cache1.get("key2").get()).isEqualTo(2); + cache1.put("key3", null); + assertThat(cache1.get("key3").get()).isNull(); + cache1.put("key3", null); + assertThat(cache1.get("key3").get()).isNull(); + cache1.evict("key3"); + assertThat(cache1.get("key3")).isNull(); + + assertThat(cache1.putIfAbsent("key1", "value1x").get()).isEqualTo("value1"); + assertThat(cache1.get("key1").get()).isEqualTo("value1"); + assertThat(cache1.putIfAbsent("key2", 2.1).get()).isEqualTo(2); + assertThat(cache1.putIfAbsent("key3", null)).isNull(); + assertThat(cache1.get("key3").get()).isNull(); + assertThat(cache1.putIfAbsent("key3", null).get()).isNull(); + assertThat(cache1.get("key3").get()).isNull(); + cache1.evict("key3"); + assertThat(cache1.get("key3")).isNull(); + } + + @Test + public void testStaticMode() { + ConcurrentMapCacheManager cm = new ConcurrentMapCacheManager("c1", "c2"); + Cache cache1 = cm.getCache("c1"); + assertThat(cache1 instanceof ConcurrentMapCache).isTrue(); + Cache cache1again = cm.getCache("c1"); + assertThat(cache1).isSameAs(cache1again); + Cache cache2 = cm.getCache("c2"); + assertThat(cache2 instanceof ConcurrentMapCache).isTrue(); + Cache cache2again = cm.getCache("c2"); + assertThat(cache2).isSameAs(cache2again); + Cache cache3 = cm.getCache("c3"); + assertThat(cache3).isNull(); + + cache1.put("key1", "value1"); + assertThat(cache1.get("key1").get()).isEqualTo("value1"); + cache1.put("key2", 2); + assertThat(cache1.get("key2").get()).isEqualTo(2); + cache1.put("key3", null); + assertThat(cache1.get("key3").get()).isNull(); + cache1.evict("key3"); + assertThat(cache1.get("key3")).isNull(); + + cm.setAllowNullValues(false); + Cache cache1x = cm.getCache("c1"); + assertThat(cache1x instanceof ConcurrentMapCache).isTrue(); + assertThat(cache1x != cache1).isTrue(); + Cache cache2x = cm.getCache("c2"); + assertThat(cache2x instanceof ConcurrentMapCache).isTrue(); + assertThat(cache2x != cache2).isTrue(); + Cache cache3x = cm.getCache("c3"); + assertThat(cache3x).isNull(); + + cache1x.put("key1", "value1"); + assertThat(cache1x.get("key1").get()).isEqualTo("value1"); + cache1x.put("key2", 2); + assertThat(cache1x.get("key2").get()).isEqualTo(2); + + cm.setAllowNullValues(true); + Cache cache1y = cm.getCache("c1"); + + cache1y.put("key3", null); + assertThat(cache1y.get("key3").get()).isNull(); + cache1y.evict("key3"); + assertThat(cache1y.get("key3")).isNull(); + } + + @Test + public void testChangeStoreByValue() { + ConcurrentMapCacheManager cm = new ConcurrentMapCacheManager("c1", "c2"); + assertThat(cm.isStoreByValue()).isFalse(); + Cache cache1 = cm.getCache("c1"); + assertThat(cache1 instanceof ConcurrentMapCache).isTrue(); + assertThat(((ConcurrentMapCache) cache1).isStoreByValue()).isFalse(); + cache1.put("key", "value"); + + cm.setStoreByValue(true); + assertThat(cm.isStoreByValue()).isTrue(); + Cache cache1x = cm.getCache("c1"); + assertThat(cache1x instanceof ConcurrentMapCache).isTrue(); + assertThat(cache1x != cache1).isTrue(); + assertThat(cache1x.get("key")).isNull(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/cache/concurrent/ConcurrentMapCacheTests.java b/spring-context/src/test/java/org/springframework/cache/concurrent/ConcurrentMapCacheTests.java new file mode 100644 index 0000000..4e8a9d3 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/cache/concurrent/ConcurrentMapCacheTests.java @@ -0,0 +1,124 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.concurrent; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.context.testfixture.cache.AbstractValueAdaptingCacheTests; +import org.springframework.core.serializer.support.SerializationDelegate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Costin Leau + * @author Juergen Hoeller + * @author Stephane Nicoll + */ +public class ConcurrentMapCacheTests extends AbstractValueAdaptingCacheTests { + + protected ConcurrentMap nativeCache; + + protected ConcurrentMapCache cache; + + protected ConcurrentMap nativeCacheNoNull; + + protected ConcurrentMapCache cacheNoNull; + + + @BeforeEach + public void setup() { + this.nativeCache = new ConcurrentHashMap<>(); + this.cache = new ConcurrentMapCache(CACHE_NAME, this.nativeCache, true); + this.nativeCacheNoNull = new ConcurrentHashMap<>(); + this.cacheNoNull = new ConcurrentMapCache(CACHE_NAME_NO_NULL, this.nativeCacheNoNull, false); + this.cache.clear(); + } + + @Override + protected ConcurrentMapCache getCache() { + return getCache(true); + } + + @Override + protected ConcurrentMapCache getCache(boolean allowNull) { + return allowNull ? this.cache : this.cacheNoNull; + } + + @Override + protected ConcurrentMap getNativeCache() { + return this.nativeCache; + } + + + @Test + public void testIsStoreByReferenceByDefault() { + assertThat(this.cache.isStoreByValue()).isFalse(); + } + + @SuppressWarnings("unchecked") + @Test + public void testSerializer() { + ConcurrentMapCache serializeCache = createCacheWithStoreByValue(); + assertThat(serializeCache.isStoreByValue()).isTrue(); + + Object key = createRandomKey(); + List content = new ArrayList<>(Arrays.asList("one", "two", "three")); + serializeCache.put(key, content); + content.remove(0); + List entry = (List) serializeCache.get(key).get(); + assertThat(entry.size()).isEqualTo(3); + assertThat(entry.get(0)).isEqualTo("one"); + } + + @Test + public void testNonSerializableContent() { + ConcurrentMapCache serializeCache = createCacheWithStoreByValue(); + + assertThatIllegalArgumentException().isThrownBy(() -> + serializeCache.put(createRandomKey(), this.cache)) + .withMessageContaining("Failed to serialize") + .withMessageContaining(this.cache.getClass().getName()); + + } + + @Test + public void testInvalidSerializedContent() { + ConcurrentMapCache serializeCache = createCacheWithStoreByValue(); + + String key = createRandomKey(); + this.nativeCache.put(key, "Some garbage"); + assertThatIllegalArgumentException().isThrownBy(() -> + serializeCache.get(key)) + .withMessageContaining("Failed to deserialize") + .withMessageContaining("Some garbage"); + } + + + private ConcurrentMapCache createCacheWithStoreByValue() { + return new ConcurrentMapCache(CACHE_NAME, this.nativeCache, true, + new SerializationDelegate(ConcurrentMapCacheTests.class.getClassLoader())); + } + +} diff --git a/spring-context/src/test/java/org/springframework/cache/config/AnnotationDrivenCacheConfigTests.java b/spring-context/src/test/java/org/springframework/cache/config/AnnotationDrivenCacheConfigTests.java new file mode 100644 index 0000000..7940c65 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/cache/config/AnnotationDrivenCacheConfigTests.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.config; + +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.GenericXmlApplicationContext; +import org.springframework.context.testfixture.cache.AbstractCacheAnnotationTests; + +/** + * @author Costin Leau + * @author Chris Beams + */ +public class AnnotationDrivenCacheConfigTests extends AbstractCacheAnnotationTests { + + @Override + protected ConfigurableApplicationContext getApplicationContext() { + return new GenericXmlApplicationContext( + "/org/springframework/cache/config/annotationDrivenCacheConfig.xml"); + } + +} diff --git a/spring-context/src/test/java/org/springframework/cache/config/AnnotationNamespaceDrivenTests.java b/spring-context/src/test/java/org/springframework/cache/config/AnnotationNamespaceDrivenTests.java new file mode 100644 index 0000000..2303379 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/cache/config/AnnotationNamespaceDrivenTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.config; + +import org.junit.jupiter.api.Test; + +import org.springframework.cache.interceptor.CacheErrorHandler; +import org.springframework.cache.interceptor.CacheInterceptor; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.GenericXmlApplicationContext; +import org.springframework.context.testfixture.cache.AbstractCacheAnnotationTests; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Costin Leau + * @author Chris Beams + * @author Stephane Nicoll + */ +public class AnnotationNamespaceDrivenTests extends AbstractCacheAnnotationTests { + + @Override + protected ConfigurableApplicationContext getApplicationContext() { + return new GenericXmlApplicationContext( + "/org/springframework/cache/config/annotationDrivenCacheNamespace.xml"); + } + + @Test + public void testKeyStrategy() { + CacheInterceptor ci = this.ctx.getBean( + "org.springframework.cache.interceptor.CacheInterceptor#0", CacheInterceptor.class); + assertThat(ci.getKeyGenerator()).isSameAs(this.ctx.getBean("keyGenerator")); + } + + @Test + public void cacheResolver() { + ConfigurableApplicationContext context = new GenericXmlApplicationContext( + "/org/springframework/cache/config/annotationDrivenCacheNamespace-resolver.xml"); + + CacheInterceptor ci = context.getBean(CacheInterceptor.class); + assertThat(ci.getCacheResolver()).isSameAs(context.getBean("cacheResolver")); + context.close(); + } + + @Test + public void bothSetOnlyResolverIsUsed() { + ConfigurableApplicationContext context = new GenericXmlApplicationContext( + "/org/springframework/cache/config/annotationDrivenCacheNamespace-manager-resolver.xml"); + + CacheInterceptor ci = context.getBean(CacheInterceptor.class); + assertThat(ci.getCacheResolver()).isSameAs(context.getBean("cacheResolver")); + context.close(); + } + + @Test + public void testCacheErrorHandler() { + CacheInterceptor ci = this.ctx.getBean( + "org.springframework.cache.interceptor.CacheInterceptor#0", CacheInterceptor.class); + assertThat(ci.getErrorHandler()).isSameAs(this.ctx.getBean("errorHandler", CacheErrorHandler.class)); + } + +} diff --git a/spring-context/src/test/java/org/springframework/cache/config/CacheAdviceNamespaceTests.java b/spring-context/src/test/java/org/springframework/cache/config/CacheAdviceNamespaceTests.java new file mode 100644 index 0000000..f15aa69 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/cache/config/CacheAdviceNamespaceTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.config; + +import org.junit.jupiter.api.Test; + +import org.springframework.cache.interceptor.CacheInterceptor; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.GenericXmlApplicationContext; +import org.springframework.context.testfixture.cache.AbstractCacheAnnotationTests; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Costin Leau + * @author Chris Beams + */ +public class CacheAdviceNamespaceTests extends AbstractCacheAnnotationTests { + + @Override + protected ConfigurableApplicationContext getApplicationContext() { + return new GenericXmlApplicationContext( + "/org/springframework/cache/config/cache-advice.xml"); + } + + @Test + public void testKeyStrategy() { + CacheInterceptor bean = this.ctx.getBean("cacheAdviceClass", CacheInterceptor.class); + assertThat(bean.getKeyGenerator()).isSameAs(this.ctx.getBean("keyGenerator")); + } + +} diff --git a/spring-context/src/test/java/org/springframework/cache/config/CacheAdviceParserTests.java b/spring-context/src/test/java/org/springframework/cache/config/CacheAdviceParserTests.java new file mode 100644 index 0000000..66d5b2b --- /dev/null +++ b/spring-context/src/test/java/org/springframework/cache/config/CacheAdviceParserTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.config; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.context.support.GenericXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * AOP advice specific parsing tests. + * + * @author Stephane Nicoll + */ +public class CacheAdviceParserTests { + + @Test + public void keyAndKeyGeneratorCannotBeSetTogether() { + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> + new GenericXmlApplicationContext("/org/springframework/cache/config/cache-advice-invalid.xml")); + // TODO better exception handling + } + +} diff --git a/spring-context/src/test/java/org/springframework/cache/config/CustomInterceptorTests.java b/spring-context/src/test/java/org/springframework/cache/config/CustomInterceptorTests.java new file mode 100644 index 0000000..1d83c6d --- /dev/null +++ b/spring-context/src/test/java/org/springframework/cache/config/CustomInterceptorTests.java @@ -0,0 +1,133 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.config; + +import java.io.IOException; +import java.util.Map; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.interceptor.CacheInterceptor; +import org.springframework.cache.interceptor.CacheOperationInvoker; +import org.springframework.cache.interceptor.CacheOperationSource; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.testfixture.cache.CacheTestUtils; +import org.springframework.context.testfixture.cache.beans.CacheableService; +import org.springframework.context.testfixture.cache.beans.DefaultCacheableService; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Stephane Nicoll + */ +public class CustomInterceptorTests { + + protected ConfigurableApplicationContext ctx; + + protected CacheableService cs; + + @BeforeEach + public void setup() { + this.ctx = new AnnotationConfigApplicationContext(EnableCachingConfig.class); + this.cs = ctx.getBean("service", CacheableService.class); + } + + @AfterEach + public void tearDown() { + this.ctx.close(); + } + + @Test + public void onlyOneInterceptorIsAvailable() { + Map interceptors = this.ctx.getBeansOfType(CacheInterceptor.class); + assertThat(interceptors.size()).as("Only one interceptor should be defined").isEqualTo(1); + CacheInterceptor interceptor = interceptors.values().iterator().next(); + assertThat(interceptor.getClass()).as("Custom interceptor not defined").isEqualTo(TestCacheInterceptor.class); + } + + @Test + public void customInterceptorAppliesWithRuntimeException() { + Object o = this.cs.throwUnchecked(0L); + // See TestCacheInterceptor + assertThat(o).isEqualTo(55L); + } + + @Test + public void customInterceptorAppliesWithCheckedException() { + assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> + this.cs.throwChecked(0L)) + .withCauseExactlyInstanceOf(IOException.class); + } + + + @Configuration + @EnableCaching + static class EnableCachingConfig { + + @Bean + public CacheManager cacheManager() { + return CacheTestUtils.createSimpleCacheManager("testCache", "primary", "secondary"); + } + + @Bean + public CacheableService service() { + return new DefaultCacheableService(); + } + + @Bean + public CacheInterceptor cacheInterceptor(CacheOperationSource cacheOperationSource) { + CacheInterceptor cacheInterceptor = new TestCacheInterceptor(); + cacheInterceptor.setCacheManager(cacheManager()); + cacheInterceptor.setCacheOperationSources(cacheOperationSource); + return cacheInterceptor; + } + } + + /** + * A test {@link CacheInterceptor} that handles special exception + * types. + */ + @SuppressWarnings("serial") + static class TestCacheInterceptor extends CacheInterceptor { + + @Override + protected Object invokeOperation(CacheOperationInvoker invoker) { + try { + return super.invokeOperation(invoker); + } + catch (CacheOperationInvoker.ThrowableWrapper e) { + Throwable original = e.getOriginal(); + if (original.getClass() == UnsupportedOperationException.class) { + return 55L; + } + else { + throw new CacheOperationInvoker.ThrowableWrapper( + new RuntimeException("wrapping original", original)); + } + } + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/cache/config/EnableCachingIntegrationTests.java b/spring-context/src/test/java/org/springframework/cache/config/EnableCachingIntegrationTests.java new file mode 100644 index 0000000..858d9c0 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/cache/config/EnableCachingIntegrationTests.java @@ -0,0 +1,219 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.config; + +import java.util.concurrent.atomic.AtomicLong; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.testfixture.cache.CacheTestUtils; +import org.springframework.core.env.Environment; +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.context.testfixture.cache.CacheTestUtils.assertCacheHit; +import static org.springframework.context.testfixture.cache.CacheTestUtils.assertCacheMiss; + +/** + * Tests that represent real use cases with advanced configuration. + * + * @author Stephane Nicoll + */ +public class EnableCachingIntegrationTests { + + private ConfigurableApplicationContext context; + + + @AfterEach + public void closeContext() { + if (this.context != null) { + this.context.close(); + } + } + + + @Test + public void fooServiceWithInterface() { + this.context = new AnnotationConfigApplicationContext(FooConfig.class); + FooService service = this.context.getBean(FooService.class); + fooGetSimple(service); + } + + @Test + public void fooServiceWithInterfaceCglib() { + this.context = new AnnotationConfigApplicationContext(FooConfigCglib.class); + FooService service = this.context.getBean(FooService.class); + fooGetSimple(service); + } + + private void fooGetSimple(FooService service) { + Cache cache = getCache(); + + Object key = new Object(); + assertCacheMiss(key, cache); + + Object value = service.getSimple(key); + assertCacheHit(key, value, cache); + } + + @Test + public void beanConditionOff() { + this.context = new AnnotationConfigApplicationContext(BeanConditionConfig.class); + FooService service = this.context.getBean(FooService.class); + Cache cache = getCache(); + + Object key = new Object(); + service.getWithCondition(key); + assertCacheMiss(key, cache); + service.getWithCondition(key); + assertCacheMiss(key, cache); + + assertThat(this.context.getBean(BeanConditionConfig.Bar.class).count).isEqualTo(2); + } + + @Test + public void beanConditionOn() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.setEnvironment(new MockEnvironment().withProperty("bar.enabled", "true")); + ctx.register(BeanConditionConfig.class); + ctx.refresh(); + this.context = ctx; + + FooService service = this.context.getBean(FooService.class); + Cache cache = getCache(); + + Object key = new Object(); + Object value = service.getWithCondition(key); + assertCacheHit(key, value, cache); + value = service.getWithCondition(key); + assertCacheHit(key, value, cache); + + assertThat(this.context.getBean(BeanConditionConfig.Bar.class).count).isEqualTo(2); + } + + private Cache getCache() { + return this.context.getBean(CacheManager.class).getCache("testCache"); + } + + + @Configuration + static class SharedConfig extends CachingConfigurerSupport { + + @Override + @Bean + public CacheManager cacheManager() { + return CacheTestUtils.createSimpleCacheManager("testCache"); + } + } + + + @Configuration + @Import(SharedConfig.class) + @EnableCaching + static class FooConfig { + + @Bean + public FooService fooService() { + return new FooServiceImpl(); + } + } + + + @Configuration + @Import(SharedConfig.class) + @EnableCaching(proxyTargetClass = true) + static class FooConfigCglib { + + @Bean + public FooService fooService() { + return new FooServiceImpl(); + } + } + + + interface FooService { + + Object getSimple(Object key); + + Object getWithCondition(Object key); + } + + + @CacheConfig(cacheNames = "testCache") + static class FooServiceImpl implements FooService { + + private final AtomicLong counter = new AtomicLong(); + + @Override + @Cacheable + public Object getSimple(Object key) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(condition = "@bar.enabled") + public Object getWithCondition(Object key) { + return this.counter.getAndIncrement(); + } + } + + + @Configuration + @Import(FooConfig.class) + @EnableCaching + static class BeanConditionConfig { + + @Autowired + Environment env; + + @Bean + public Bar bar() { + return new Bar(Boolean.parseBoolean(env.getProperty("bar.enabled"))); + } + + + static class Bar { + + public int count; + + private final boolean enabled; + + public Bar(boolean enabled) { + this.enabled = enabled; + } + + public boolean isEnabled() { + this.count++; + return this.enabled; + } + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/cache/config/EnableCachingTests.java b/spring-context/src/test/java/org/springframework/cache/config/EnableCachingTests.java new file mode 100644 index 0000000..fae93b5 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/cache/config/EnableCachingTests.java @@ -0,0 +1,284 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.config; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.interceptor.CacheErrorHandler; +import org.springframework.cache.interceptor.CacheInterceptor; +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.cache.interceptor.NamedCacheResolver; +import org.springframework.cache.interceptor.SimpleCacheErrorHandler; +import org.springframework.cache.interceptor.SimpleCacheResolver; +import org.springframework.cache.support.NoOpCacheManager; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.testfixture.cache.AbstractCacheAnnotationTests; +import org.springframework.context.testfixture.cache.CacheTestUtils; +import org.springframework.context.testfixture.cache.SomeCustomKeyGenerator; +import org.springframework.context.testfixture.cache.SomeKeyGenerator; +import org.springframework.context.testfixture.cache.beans.AnnotatedClassCacheableService; +import org.springframework.context.testfixture.cache.beans.CacheableService; +import org.springframework.context.testfixture.cache.beans.DefaultCacheableService; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@code @EnableCaching} and its related + * {@code @Configuration} classes. + * + * @author Chris Beams + * @author Stephane Nicoll + */ +public class EnableCachingTests extends AbstractCacheAnnotationTests { + + /** hook into superclass suite of tests */ + @Override + protected ConfigurableApplicationContext getApplicationContext() { + return new AnnotationConfigApplicationContext(EnableCachingConfig.class); + } + + @Test + public void testKeyStrategy() { + CacheInterceptor ci = this.ctx.getBean(CacheInterceptor.class); + assertThat(ci.getKeyGenerator()).isSameAs(this.ctx.getBean("keyGenerator", KeyGenerator.class)); + } + + @Test + public void testCacheErrorHandler() { + CacheInterceptor ci = this.ctx.getBean(CacheInterceptor.class); + assertThat(ci.getErrorHandler()).isSameAs(this.ctx.getBean("errorHandler", CacheErrorHandler.class)); + } + + @Test + public void singleCacheManagerBean() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(SingleCacheManagerConfig.class); + ctx.refresh(); + } + + @Test + public void multipleCacheManagerBeans() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(MultiCacheManagerConfig.class); + try { + ctx.refresh(); + } + catch (IllegalStateException ex) { + assertThat(ex.getMessage().contains("no unique bean of type CacheManager")).isTrue(); + } + } + + @Test + public void multipleCacheManagerBeans_implementsCachingConfigurer() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(MultiCacheManagerConfigurer.class); + ctx.refresh(); // does not throw an exception + } + + @Test + public void multipleCachingConfigurers() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(MultiCacheManagerConfigurer.class, EnableCachingConfig.class); + try { + ctx.refresh(); + } + catch (BeanCreationException ex) { + Throwable root = ex.getRootCause(); + boolean condition = root instanceof IllegalStateException; + assertThat(condition).isTrue(); + assertThat(root.getMessage().contains("implementations of CachingConfigurer")).isTrue(); + } + } + + @Test + public void noCacheManagerBeans() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(EmptyConfig.class); + try { + ctx.refresh(); + } + catch (IllegalStateException ex) { + assertThat(ex.getMessage().contains("no bean of type CacheManager")).isTrue(); + } + } + + @Test + public void emptyConfigSupport() { + ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(EmptyConfigSupportConfig.class); + CacheInterceptor ci = context.getBean(CacheInterceptor.class); + assertThat(ci.getCacheResolver()).isNotNull(); + assertThat(ci.getCacheResolver().getClass()).isEqualTo(SimpleCacheResolver.class); + assertThat(((SimpleCacheResolver) ci.getCacheResolver()).getCacheManager()).isSameAs(context.getBean(CacheManager.class)); + context.close(); + } + + @Test + public void bothSetOnlyResolverIsUsed() { + ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(FullCachingConfig.class); + CacheInterceptor ci = context.getBean(CacheInterceptor.class); + assertThat(ci.getCacheResolver()).isSameAs(context.getBean("cacheResolver")); + assertThat(ci.getKeyGenerator()).isSameAs(context.getBean("keyGenerator")); + context.close(); + } + + + @Configuration + @EnableCaching + static class EnableCachingConfig extends CachingConfigurerSupport { + + @Override + @Bean + public CacheManager cacheManager() { + return CacheTestUtils.createSimpleCacheManager("testCache", "primary", "secondary"); + } + + @Bean + public CacheableService service() { + return new DefaultCacheableService(); + } + + @Bean + public CacheableService classService() { + return new AnnotatedClassCacheableService(); + } + + @Override + @Bean + public KeyGenerator keyGenerator() { + return new SomeKeyGenerator(); + } + + @Override + @Bean + public CacheErrorHandler errorHandler() { + return new SimpleCacheErrorHandler(); + } + + @Bean + public KeyGenerator customKeyGenerator() { + return new SomeCustomKeyGenerator(); + } + + @Bean + public CacheManager customCacheManager() { + return CacheTestUtils.createSimpleCacheManager("testCache"); + } + } + + + @Configuration + @EnableCaching + static class EmptyConfig { + } + + + @Configuration + @EnableCaching + static class SingleCacheManagerConfig { + + @Bean + public CacheManager cm1() { + return new NoOpCacheManager(); + } + } + + + @Configuration + @EnableCaching + static class MultiCacheManagerConfig { + + @Bean + public CacheManager cm1() { + return new NoOpCacheManager(); + } + + @Bean + public CacheManager cm2() { + return new NoOpCacheManager(); + } + } + + + @Configuration + @EnableCaching + static class MultiCacheManagerConfigurer extends CachingConfigurerSupport { + + @Bean + public CacheManager cm1() { + return new NoOpCacheManager(); + } + + @Bean + public CacheManager cm2() { + return new NoOpCacheManager(); + } + + @Override + public CacheManager cacheManager() { + return cm1(); + } + + @Override + public KeyGenerator keyGenerator() { + return null; + } + } + + + @Configuration + @EnableCaching + static class EmptyConfigSupportConfig extends CachingConfigurerSupport { + + @Bean + public CacheManager cm() { + return new NoOpCacheManager(); + } + } + + + @Configuration + @EnableCaching + static class FullCachingConfig extends CachingConfigurerSupport { + + @Override + @Bean + public CacheManager cacheManager() { + return new NoOpCacheManager(); + } + + @Override + @Bean + public KeyGenerator keyGenerator() { + return new SomeKeyGenerator(); + } + + @Override + @Bean + public CacheResolver cacheResolver() { + return new NamedCacheResolver(cacheManager(), "foo"); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/cache/config/ExpressionCachingIntegrationTests.java b/spring-context/src/test/java/org/springframework/cache/config/ExpressionCachingIntegrationTests.java new file mode 100644 index 0000000..ae109ae --- /dev/null +++ b/spring-context/src/test/java/org/springframework/cache/config/ExpressionCachingIntegrationTests.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.config; + +import org.junit.jupiter.api.Test; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Stephane Nicoll + */ +public class ExpressionCachingIntegrationTests { + + @Test // SPR-11692 + @SuppressWarnings("unchecked") + public void expressionIsCacheBasedOnActualMethod() { + ConfigurableApplicationContext context = + new AnnotationConfigApplicationContext(SharedConfig.class, Spr11692Config.class); + + BaseDao userDao = (BaseDao) context.getBean("userDao"); + BaseDao orderDao = (BaseDao) context.getBean("orderDao"); + + userDao.persist(new User("1")); + orderDao.persist(new Order("2")); + + context.close(); + } + + + @Configuration + static class Spr11692Config { + + @Bean + public BaseDao userDao() { + return new UserDaoImpl(); + } + + @Bean + public BaseDao orderDao() { + return new OrderDaoImpl(); + } + } + + + private interface BaseDao { + + T persist(T t); + } + + + private static class UserDaoImpl implements BaseDao { + + @Override + @CachePut(value = "users", key = "#user.id") + public User persist(User user) { + return user; + } + } + + + private static class OrderDaoImpl implements BaseDao { + + @Override + @CachePut(value = "orders", key = "#order.id") + public Order persist(Order order) { + return order; + } + } + + + private static class User { + + private final String id; + + public User(String id) { + this.id = id; + } + + @SuppressWarnings("unused") + public String getId() { + return this.id; + } + } + + + private static class Order { + + private final String id; + + public Order(String id) { + this.id = id; + } + + @SuppressWarnings("unused") + public String getId() { + return this.id; + } + } + + + @Configuration + @EnableCaching + static class SharedConfig extends CachingConfigurerSupport { + + @Override + @Bean + public CacheManager cacheManager() { + return new ConcurrentMapCacheManager(); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/CacheErrorHandlerTests.java b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheErrorHandlerTests.java new file mode 100644 index 0000000..cfcd9cd --- /dev/null +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheErrorHandlerTests.java @@ -0,0 +1,226 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import java.util.Collections; +import java.util.concurrent.atomic.AtomicLong; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.support.SimpleCacheManager; +import org.springframework.cache.support.SimpleValueWrapper; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author Stephane Nicoll + */ +public class CacheErrorHandlerTests { + + private Cache cache; + + private CacheInterceptor cacheInterceptor; + + private CacheErrorHandler errorHandler; + + private SimpleService simpleService; + + @BeforeEach + public void setup() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class); + this.cache = context.getBean("mockCache", Cache.class); + this.cacheInterceptor = context.getBean(CacheInterceptor.class); + this.errorHandler = context.getBean(CacheErrorHandler.class); + this.simpleService = context.getBean(SimpleService.class); + } + + @Test + public void getFail() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on get"); + willThrow(exception).given(this.cache).get(0L); + + Object result = this.simpleService.get(0L); + verify(this.errorHandler).handleCacheGetError(exception, cache, 0L); + verify(this.cache).get(0L); + verify(this.cache).put(0L, result); // result of the invocation + } + + @Test + public void getAndPutFail() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on get"); + willThrow(exception).given(this.cache).get(0L); + willThrow(exception).given(this.cache).put(0L, 0L); // Update of the cache will fail as well + + Object counter = this.simpleService.get(0L); + + willReturn(new SimpleValueWrapper(2L)).given(this.cache).get(0L); + Object counter2 = this.simpleService.get(0L); + Object counter3 = this.simpleService.get(0L); + assertThat(counter2).isNotSameAs(counter); + assertThat(counter3).isEqualTo(counter2); + } + + @Test + public void getFailProperException() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on get"); + willThrow(exception).given(this.cache).get(0L); + + this.cacheInterceptor.setErrorHandler(new SimpleCacheErrorHandler()); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + this.simpleService.get(0L)) + .withMessage("Test exception on get"); + } + + @Test + public void putFail() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on put"); + willThrow(exception).given(this.cache).put(0L, 0L); + + this.simpleService.put(0L); + verify(this.errorHandler).handleCachePutError(exception, cache, 0L, 0L); + } + + @Test + public void putFailProperException() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on put"); + willThrow(exception).given(this.cache).put(0L, 0L); + + this.cacheInterceptor.setErrorHandler(new SimpleCacheErrorHandler()); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + this.simpleService.put(0L)) + .withMessage("Test exception on put"); + } + + @Test + public void evictFail() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on evict"); + willThrow(exception).given(this.cache).evict(0L); + + this.simpleService.evict(0L); + verify(this.errorHandler).handleCacheEvictError(exception, cache, 0L); + } + + @Test + public void evictFailProperException() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on evict"); + willThrow(exception).given(this.cache).evict(0L); + + this.cacheInterceptor.setErrorHandler(new SimpleCacheErrorHandler()); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + this.simpleService.evict(0L)) + .withMessage("Test exception on evict"); + } + + @Test + public void clearFail() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on evict"); + willThrow(exception).given(this.cache).clear(); + + this.simpleService.clear(); + verify(this.errorHandler).handleCacheClearError(exception, cache); + } + + @Test + public void clearFailProperException() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on clear"); + willThrow(exception).given(this.cache).clear(); + + this.cacheInterceptor.setErrorHandler(new SimpleCacheErrorHandler()); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + this.simpleService.clear()) + .withMessage("Test exception on clear"); + } + + + @Configuration + @EnableCaching + static class Config extends CachingConfigurerSupport { + + @Bean + @Override + public CacheErrorHandler errorHandler() { + return mock(CacheErrorHandler.class); + } + + @Bean + public SimpleService simpleService() { + return new SimpleService(); + } + + @Override + @Bean + public CacheManager cacheManager() { + SimpleCacheManager cacheManager = new SimpleCacheManager(); + cacheManager.setCaches(Collections.singletonList(mockCache())); + return cacheManager; + } + + @Bean + public Cache mockCache() { + Cache cache = mock(Cache.class); + given(cache.getName()).willReturn("test"); + return cache; + } + + } + + @CacheConfig(cacheNames = "test") + public static class SimpleService { + private AtomicLong counter = new AtomicLong(); + + @Cacheable + public Object get(long id) { + return this.counter.getAndIncrement(); + } + + @CachePut + public Object put(long id) { + return this.counter.getAndIncrement(); + } + + @CacheEvict + public void evict(long id) { + } + + @CacheEvict(allEntries = true) + public void clear() { + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/CacheProxyFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheProxyFactoryBeanTests.java new file mode 100644 index 0000000..a76ec69 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheProxyFactoryBeanTests.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.jupiter.api.Test; + +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link CacheProxyFactoryBean}. + * + * @author John Blum + * @author Juergen Hoeller + */ +public class CacheProxyFactoryBeanTests { + + @Test + public void configurationClassWithCacheProxyFactoryBean() { + try (AnnotationConfigApplicationContext applicationContext = + new AnnotationConfigApplicationContext(CacheProxyFactoryBeanConfiguration.class)) { + Greeter greeter = applicationContext.getBean("greeter", Greeter.class); + assertThat(greeter).isNotNull(); + assertThat(greeter.isCacheMiss()).isFalse(); + assertThat(greeter.greet("John")).isEqualTo("Hello John!"); + assertThat(greeter.isCacheMiss()).isTrue(); + assertThat(greeter.greet("Jon")).isEqualTo("Hello Jon!"); + assertThat(greeter.isCacheMiss()).isTrue(); + assertThat(greeter.greet("John")).isEqualTo("Hello John!"); + assertThat(greeter.isCacheMiss()).isFalse(); + assertThat(greeter.greet()).isEqualTo("Hello World!"); + assertThat(greeter.isCacheMiss()).isTrue(); + assertThat(greeter.greet()).isEqualTo("Hello World!"); + assertThat(greeter.isCacheMiss()).isFalse(); + } + } + + + @Configuration + @EnableCaching + static class CacheProxyFactoryBeanConfiguration { + + @Bean + ConcurrentMapCacheManager cacheManager() { + return new ConcurrentMapCacheManager("Greetings"); + } + + @Bean + CacheProxyFactoryBean greeter() { + CacheProxyFactoryBean factoryBean = new CacheProxyFactoryBean(); + factoryBean.setCacheOperationSources(newCacheOperationSource("greet", newCacheOperation("Greetings"))); + factoryBean.setTarget(new SimpleGreeter()); + return factoryBean; + } + + CacheOperationSource newCacheOperationSource(String methodName, CacheOperation... cacheOperations) { + NameMatchCacheOperationSource cacheOperationSource = new NameMatchCacheOperationSource(); + cacheOperationSource.addCacheMethod(methodName, Arrays.asList(cacheOperations)); + return cacheOperationSource; + } + + CacheableOperation newCacheOperation(String cacheName) { + CacheableOperation.Builder builder = new CacheableOperation.Builder(); + builder.setCacheManager("cacheManager"); + builder.setCacheName(cacheName); + return builder.build(); + } + } + + + interface Greeter { + + default boolean isCacheHit() { + return !isCacheMiss(); + } + + boolean isCacheMiss(); + + void setCacheMiss(); + + default String greet() { + return greet("World"); + } + + default String greet(String name) { + setCacheMiss(); + return String.format("Hello %s!", name); + } + } + + + static class SimpleGreeter implements Greeter { + + private final AtomicBoolean cacheMiss = new AtomicBoolean(); + + @Override + public boolean isCacheMiss() { + return this.cacheMiss.getAndSet(false); + } + + @Override + public void setCacheMiss() { + this.cacheMiss.set(true); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/CachePutEvaluationTests.java b/spring-context/src/test/java/org/springframework/cache/interceptor/CachePutEvaluationTests.java new file mode 100644 index 0000000..d17630a --- /dev/null +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/CachePutEvaluationTests.java @@ -0,0 +1,147 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import java.util.concurrent.atomic.AtomicLong; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests corner case of using {@link Cacheable} and {@link CachePut} on the + * same operation. + * + * @author Stephane Nicoll + */ +public class CachePutEvaluationTests { + + private ConfigurableApplicationContext context; + + private Cache cache; + + private SimpleService service; + + @BeforeEach + public void setup() { + this.context = new AnnotationConfigApplicationContext(Config.class); + this.cache = this.context.getBean(CacheManager.class).getCache("test"); + this.service = this.context.getBean(SimpleService.class); + } + + @AfterEach + public void close() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void mutualGetPutExclusion() { + String key = "1"; + + Long first = this.service.getOrPut(key, true); + Long second = this.service.getOrPut(key, true); + assertThat(second).isSameAs(first); + + // This forces the method to be executed again + Long expected = first + 1; + Long third = this.service.getOrPut(key, false); + assertThat(third).isEqualTo(expected); + + Long fourth = this.service.getOrPut(key, true); + assertThat(fourth).isSameAs(third); + } + + @Test + public void getAndPut() { + this.cache.clear(); + + long key = 1; + Long value = this.service.getAndPut(key); + + assertThat(this.cache.get(key).get()).as("Wrong value for @Cacheable key").isEqualTo(value); + // See @CachePut + assertThat(this.cache.get(value + 100).get()).as("Wrong value for @CachePut key").isEqualTo(value); + + // CachePut forced a method call + Long anotherValue = this.service.getAndPut(key); + assertThat(anotherValue).isNotSameAs(value); + // NOTE: while you might expect the main key to have been updated, it hasn't. @Cacheable operations + // are only processed in case of a cache miss. This is why combining @Cacheable with @CachePut + // is a very bad idea. We could refine the condition now that we can figure out if we are going + // to invoke the method anyway but that brings a whole new set of potential regressions. + //assertEquals("Wrong value for @Cacheable key", anotherValue, cache.get(key).get()); + assertThat(this.cache.get(anotherValue + 100).get()).as("Wrong value for @CachePut key").isEqualTo(anotherValue); + } + + @Configuration + @EnableCaching + static class Config extends CachingConfigurerSupport { + + @Bean + @Override + public CacheManager cacheManager() { + return new ConcurrentMapCacheManager(); + } + + @Bean + public SimpleService simpleService() { + return new SimpleService(); + } + + } + + @CacheConfig(cacheNames = "test") + public static class SimpleService { + private AtomicLong counter = new AtomicLong(); + + /** + * Represent a mutual exclusion use case. The boolean flag exclude one of the two operation. + */ + @Cacheable(condition = "#p1", key = "#p0") + @CachePut(condition = "!#p1", key = "#p0") + public Long getOrPut(Object id, boolean flag) { + return this.counter.getAndIncrement(); + } + + /** + * Represent an invalid use case. If the result of the operation is non null, then we put + * the value with a different key. This forces the method to be executed every time. + */ + @Cacheable + @CachePut(key = "#result + 100", condition = "#result != null") + public Long getAndPut(long id) { + return this.counter.getAndIncrement(); + } + } +} diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/CacheResolverCustomizationTests.java b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheResolverCustomizationTests.java new file mode 100644 index 0000000..ca2e76f --- /dev/null +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheResolverCustomizationTests.java @@ -0,0 +1,275 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicLong; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.testfixture.cache.CacheTestUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.springframework.context.testfixture.cache.CacheTestUtils.assertCacheHit; +import static org.springframework.context.testfixture.cache.CacheTestUtils.assertCacheMiss; + +/** + * Provides various {@link CacheResolver} customisations scenario + * + * @author Stephane Nicoll + * @since 4.1 + */ +public class CacheResolverCustomizationTests { + + private CacheManager cacheManager; + + private CacheManager anotherCacheManager; + + private SimpleService simpleService; + + + @BeforeEach + public void setup() { + ApplicationContext context = new AnnotationConfigApplicationContext(Config.class); + this.cacheManager = context.getBean("cacheManager", CacheManager.class); + this.anotherCacheManager = context.getBean("anotherCacheManager", CacheManager.class); + this.simpleService = context.getBean(SimpleService.class); + } + + + @Test + public void noCustomization() { + Cache cache = this.cacheManager.getCache("default"); + + Object key = new Object(); + assertCacheMiss(key, cache); + + Object value = this.simpleService.getSimple(key); + assertCacheHit(key, value, cache); + } + + @Test + public void customCacheResolver() { + Cache cache = this.cacheManager.getCache("primary"); + + Object key = new Object(); + assertCacheMiss(key, cache); + + Object value = this.simpleService.getWithCustomCacheResolver(key); + assertCacheHit(key, value, cache); + } + + @Test + public void customCacheManager() { + Cache cache = this.anotherCacheManager.getCache("default"); + + Object key = new Object(); + assertCacheMiss(key, cache); + + Object value = this.simpleService.getWithCustomCacheManager(key); + assertCacheHit(key, value, cache); + } + + @Test + public void runtimeResolution() { + Cache defaultCache = this.cacheManager.getCache("default"); + Cache primaryCache = this.cacheManager.getCache("primary"); + + Object key = new Object(); + assertCacheMiss(key, defaultCache, primaryCache); + Object value = this.simpleService.getWithRuntimeCacheResolution(key, "default"); + assertCacheHit(key, value, defaultCache); + assertCacheMiss(key, primaryCache); + + Object key2 = new Object(); + assertCacheMiss(key2, defaultCache, primaryCache); + Object value2 = this.simpleService.getWithRuntimeCacheResolution(key2, "primary"); + assertCacheHit(key2, value2, primaryCache); + assertCacheMiss(key2, defaultCache); + } + + @Test + public void namedResolution() { + Cache cache = this.cacheManager.getCache("secondary"); + + Object key = new Object(); + assertCacheMiss(key, cache); + + Object value = this.simpleService.getWithNamedCacheResolution(key); + assertCacheHit(key, value, cache); + } + + @Test + public void noCacheResolved() { + Method method = ReflectionUtils.findMethod(SimpleService.class, "noCacheResolved", Object.class); + assertThatIllegalStateException().isThrownBy(() -> + this.simpleService.noCacheResolved(new Object())) + .withMessageContaining(method.toString()); + } + + @Test + public void unknownCacheResolver() { + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> + this.simpleService.unknownCacheResolver(new Object())) + .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("unknownCacheResolver")); + } + + + @Configuration + @EnableCaching + static class Config extends CachingConfigurerSupport { + + @Override + @Bean + public CacheManager cacheManager() { + return CacheTestUtils.createSimpleCacheManager("default", "primary", "secondary"); + } + + @Bean + public CacheManager anotherCacheManager() { + return CacheTestUtils.createSimpleCacheManager("default", "primary", "secondary"); + } + + @Bean + public CacheResolver primaryCacheResolver() { + return new NamedCacheResolver(cacheManager(), "primary"); + } + + @Bean + public CacheResolver secondaryCacheResolver() { + return new NamedCacheResolver(cacheManager(), "primary"); + } + + @Bean + public CacheResolver runtimeCacheResolver() { + return new RuntimeCacheResolver(cacheManager()); + } + + @Bean + public CacheResolver namedCacheResolver() { + NamedCacheResolver resolver = new NamedCacheResolver(); + resolver.setCacheManager(cacheManager()); + resolver.setCacheNames(Collections.singleton("secondary")); + return resolver; + } + + @Bean + public CacheResolver nullCacheResolver() { + return new NullCacheResolver(cacheManager()); + } + + @Bean + public SimpleService simpleService() { + return new SimpleService(); + } + } + + + @CacheConfig(cacheNames = "default") + static class SimpleService { + + private final AtomicLong counter = new AtomicLong(); + + @Cacheable + public Object getSimple(Object key) { + return this.counter.getAndIncrement(); + } + + @Cacheable(cacheResolver = "primaryCacheResolver") + public Object getWithCustomCacheResolver(Object key) { + return this.counter.getAndIncrement(); + } + + @Cacheable(cacheManager = "anotherCacheManager") + public Object getWithCustomCacheManager(Object key) { + return this.counter.getAndIncrement(); + } + + @Cacheable(cacheResolver = "runtimeCacheResolver", key = "#p0") + public Object getWithRuntimeCacheResolution(Object key, String cacheName) { + return this.counter.getAndIncrement(); + } + + @Cacheable(cacheResolver = "namedCacheResolver") + public Object getWithNamedCacheResolution(Object key) { + return this.counter.getAndIncrement(); + } + + @Cacheable(cacheResolver = "nullCacheResolver") // No cache resolved for the operation + public Object noCacheResolved(Object key) { + return this.counter.getAndIncrement(); + } + + @Cacheable(cacheResolver = "unknownCacheResolver") // No such bean defined + public Object unknownCacheResolver(Object key) { + return this.counter.getAndIncrement(); + } + } + + + /** + * Example of {@link CacheResolver} that resolve the caches at + * runtime (i.e. based on method invocation parameters). + *

    Expects the second argument to hold the name of the cache to use + */ + private static class RuntimeCacheResolver extends AbstractCacheResolver { + + private RuntimeCacheResolver(CacheManager cacheManager) { + super(cacheManager); + } + + @Override + @Nullable + protected Collection getCacheNames(CacheOperationInvocationContext context) { + String cacheName = (String) context.getArgs()[1]; + return Collections.singleton(cacheName); + } + } + + + private static class NullCacheResolver extends AbstractCacheResolver { + + private NullCacheResolver(CacheManager cacheManager) { + super(cacheManager); + } + + @Override + @Nullable + protected Collection getCacheNames(CacheOperationInvocationContext context) { + return null; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/CacheSyncFailureTests.java b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheSyncFailureTests.java new file mode 100644 index 0000000..2a3eccc --- /dev/null +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheSyncFailureTests.java @@ -0,0 +1,155 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import java.util.concurrent.atomic.AtomicLong; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.annotation.Caching; +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.testfixture.cache.CacheTestUtils; + +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Provides various failure scenario linked to the use of {@link Cacheable#sync()}. + * + * @author Stephane Nicoll + * @since 4.3 + */ +public class CacheSyncFailureTests { + + private ConfigurableApplicationContext context; + + private SimpleService simpleService; + + @BeforeEach + public void setUp() { + this.context = new AnnotationConfigApplicationContext(Config.class); + this.simpleService = this.context.getBean(SimpleService.class); + } + + @AfterEach + public void closeContext() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void unlessSync() { + assertThatIllegalStateException().isThrownBy(() -> + this.simpleService.unlessSync("key")) + .withMessageContaining("@Cacheable(sync=true) does not support unless attribute"); + } + + @Test + public void severalCachesSync() { + assertThatIllegalStateException().isThrownBy(() -> + this.simpleService.severalCachesSync("key")) + .withMessageContaining("@Cacheable(sync=true) only allows a single cache"); + } + + @Test + public void severalCachesWithResolvedSync() { + assertThatIllegalStateException().isThrownBy(() -> + this.simpleService.severalCachesWithResolvedSync("key")) + .withMessageContaining("@Cacheable(sync=true) only allows a single cache"); + } + + @Test + public void syncWithAnotherOperation() { + assertThatIllegalStateException().isThrownBy(() -> + this.simpleService.syncWithAnotherOperation("key")) + .withMessageContaining("@Cacheable(sync=true) cannot be combined with other cache operations"); + } + + @Test + public void syncWithTwoGetOperations() { + assertThatIllegalStateException().isThrownBy(() -> + this.simpleService.syncWithTwoGetOperations("key")) + .withMessageContaining("Only one @Cacheable(sync=true) entry is allowed"); + } + + + static class SimpleService { + + private final AtomicLong counter = new AtomicLong(); + + @Cacheable(cacheNames = "testCache", sync = true, unless = "#result > 10") + public Object unlessSync(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Cacheable(cacheNames = {"testCache", "anotherTestCache"}, sync = true) + public Object severalCachesSync(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Cacheable(cacheResolver = "testCacheResolver", sync = true) + public Object severalCachesWithResolvedSync(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Cacheable(cacheNames = "testCache", sync = true) + @CacheEvict(cacheNames = "anotherTestCache", key = "#arg1") + public Object syncWithAnotherOperation(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Caching(cacheable = { + @Cacheable(cacheNames = "testCache", sync = true), + @Cacheable(cacheNames = "anotherTestCache", sync = true) + }) + public Object syncWithTwoGetOperations(Object arg1) { + return this.counter.getAndIncrement(); + } + } + + @Configuration + @EnableCaching + static class Config extends CachingConfigurerSupport { + + @Override + @Bean + public CacheManager cacheManager() { + return CacheTestUtils.createSimpleCacheManager("testCache", "anotherTestCache"); + } + + @Bean + public CacheResolver testCacheResolver() { + return new NamedCacheResolver(cacheManager(), "testCache", "anotherTestCache"); + } + + @Bean + public SimpleService simpleService() { + return new SimpleService(); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/ExpressionEvaluatorTests.java b/spring-context/src/test/java/org/springframework/cache/interceptor/ExpressionEvaluatorTests.java new file mode 100644 index 0000000..495c977 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/ExpressionEvaluatorTests.java @@ -0,0 +1,161 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.cache.annotation.AnnotationCacheOperationSource; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.annotation.Caching; +import org.springframework.cache.concurrent.ConcurrentMapCache; +import org.springframework.context.expression.AnnotatedElementKey; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Costin Leau + * @author Phillip Webb + * @author Sam Brannen + * @author Stephane Nicoll + */ +public class ExpressionEvaluatorTests { + + private final CacheOperationExpressionEvaluator eval = new CacheOperationExpressionEvaluator(); + + private final AnnotationCacheOperationSource source = new AnnotationCacheOperationSource(); + + + private Collection getOps(String name) { + Method method = ReflectionUtils.findMethod(AnnotatedClass.class, name, Object.class, Object.class); + return this.source.getCacheOperations(method, AnnotatedClass.class); + } + + + @Test + public void testMultipleCachingSource() { + Collection ops = getOps("multipleCaching"); + assertThat(ops.size()).isEqualTo(2); + Iterator it = ops.iterator(); + CacheOperation next = it.next(); + assertThat(next instanceof CacheableOperation).isTrue(); + assertThat(next.getCacheNames().contains("test")).isTrue(); + assertThat(next.getKey()).isEqualTo("#a"); + next = it.next(); + assertThat(next instanceof CacheableOperation).isTrue(); + assertThat(next.getCacheNames().contains("test")).isTrue(); + assertThat(next.getKey()).isEqualTo("#b"); + } + + @Test + public void testMultipleCachingEval() { + AnnotatedClass target = new AnnotatedClass(); + Method method = ReflectionUtils.findMethod( + AnnotatedClass.class, "multipleCaching", Object.class, Object.class); + Object[] args = new Object[] {new Object(), new Object()}; + Collection caches = Collections.singleton(new ConcurrentMapCache("test")); + + EvaluationContext evalCtx = this.eval.createEvaluationContext(caches, method, args, + target, target.getClass(), method, CacheOperationExpressionEvaluator.NO_RESULT, null); + Collection ops = getOps("multipleCaching"); + + Iterator it = ops.iterator(); + AnnotatedElementKey key = new AnnotatedElementKey(method, AnnotatedClass.class); + + Object keyA = this.eval.key(it.next().getKey(), key, evalCtx); + Object keyB = this.eval.key(it.next().getKey(), key, evalCtx); + + assertThat(keyA).isEqualTo(args[0]); + assertThat(keyB).isEqualTo(args[1]); + } + + @Test + public void withReturnValue() { + EvaluationContext context = createEvaluationContext("theResult"); + Object value = new SpelExpressionParser().parseExpression("#result").getValue(context); + assertThat(value).isEqualTo("theResult"); + } + + @Test + public void withNullReturn() { + EvaluationContext context = createEvaluationContext(null); + Object value = new SpelExpressionParser().parseExpression("#result").getValue(context); + assertThat(value).isNull(); + } + + @Test + public void withoutReturnValue() { + EvaluationContext context = createEvaluationContext(CacheOperationExpressionEvaluator.NO_RESULT); + Object value = new SpelExpressionParser().parseExpression("#result").getValue(context); + assertThat(value).isNull(); + } + + @Test + public void unavailableReturnValue() { + EvaluationContext context = createEvaluationContext(CacheOperationExpressionEvaluator.RESULT_UNAVAILABLE); + assertThatExceptionOfType(VariableNotAvailableException.class).isThrownBy(() -> + new SpelExpressionParser().parseExpression("#result").getValue(context)) + .satisfies(ex -> assertThat(ex.getName()).isEqualTo("result")); + } + + @Test + public void resolveBeanReference() { + StaticApplicationContext applicationContext = new StaticApplicationContext(); + BeanDefinition beanDefinition = new RootBeanDefinition(String.class); + applicationContext.registerBeanDefinition("myBean", beanDefinition); + applicationContext.refresh(); + + EvaluationContext context = createEvaluationContext(CacheOperationExpressionEvaluator.NO_RESULT, applicationContext); + Object value = new SpelExpressionParser().parseExpression("@myBean.class.getName()").getValue(context); + assertThat(value).isEqualTo(String.class.getName()); + } + + private EvaluationContext createEvaluationContext(Object result) { + return createEvaluationContext(result, null); + } + + private EvaluationContext createEvaluationContext(Object result, BeanFactory beanFactory) { + AnnotatedClass target = new AnnotatedClass(); + Method method = ReflectionUtils.findMethod( + AnnotatedClass.class, "multipleCaching", Object.class, Object.class); + Object[] args = new Object[] {new Object(), new Object()}; + Collection caches = Collections.singleton(new ConcurrentMapCache("test")); + return this.eval.createEvaluationContext( + caches, method, args, target, target.getClass(), method, result, beanFactory); + } + + + private static class AnnotatedClass { + + @Caching(cacheable = { @Cacheable(value = "test", key = "#a"), @Cacheable(value = "test", key = "#b") }) + public void multipleCaching(Object a, Object b) { + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/SimpleKeyGeneratorTests.java b/spring-context/src/test/java/org/springframework/cache/interceptor/SimpleKeyGeneratorTests.java new file mode 100644 index 0000000..2d9398c --- /dev/null +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/SimpleKeyGeneratorTests.java @@ -0,0 +1,132 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cache.interceptor; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.testfixture.io.SerializationTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SimpleKeyGenerator} and {@link SimpleKey}. + * + * @author Phillip Webb + * @author Stephane Nicoll + * @author Juergen Hoeller + */ +public class SimpleKeyGeneratorTests { + + private final SimpleKeyGenerator generator = new SimpleKeyGenerator(); + + + @Test + public void noValues() { + Object k1 = generateKey(new Object[] {}); + Object k2 = generateKey(new Object[] {}); + Object k3 = generateKey(new Object[] { "different" }); + assertThat(k1.hashCode()).isEqualTo(k2.hashCode()); + assertThat(k1.hashCode()).isNotEqualTo(k3.hashCode()); + assertThat(k1).isEqualTo(k2); + assertThat(k1).isNotEqualTo(k3); + } + + @Test + public void singleValue() { + Object k1 = generateKey(new Object[] { "a" }); + Object k2 = generateKey(new Object[] { "a" }); + Object k3 = generateKey(new Object[] { "different" }); + assertThat(k1.hashCode()).isEqualTo(k2.hashCode()); + assertThat(k1.hashCode()).isNotEqualTo(k3.hashCode()); + assertThat(k1).isEqualTo(k2); + assertThat(k1).isNotEqualTo(k3); + assertThat(k1).isEqualTo("a"); + } + + @Test + public void multipleValues() { + Object k1 = generateKey(new Object[] { "a", 1, "b" }); + Object k2 = generateKey(new Object[] { "a", 1, "b" }); + Object k3 = generateKey(new Object[] { "b", 1, "a" }); + assertThat(k1.hashCode()).isEqualTo(k2.hashCode()); + assertThat(k1.hashCode()).isNotEqualTo(k3.hashCode()); + assertThat(k1).isEqualTo(k2); + assertThat(k1).isNotEqualTo(k3); + } + + @Test + public void singleNullValue() { + Object k1 = generateKey(new Object[] { null }); + Object k2 = generateKey(new Object[] { null }); + Object k3 = generateKey(new Object[] { "different" }); + assertThat(k1.hashCode()).isEqualTo(k2.hashCode()); + assertThat(k1.hashCode()).isNotEqualTo(k3.hashCode()); + assertThat(k1).isEqualTo(k2); + assertThat(k1).isNotEqualTo(k3); + assertThat(k1).isInstanceOf(SimpleKey.class); + } + + @Test + public void multipleNullValues() { + Object k1 = generateKey(new Object[] { "a", null, "b", null }); + Object k2 = generateKey(new Object[] { "a", null, "b", null }); + Object k3 = generateKey(new Object[] { "a", null, "b" }); + assertThat(k1.hashCode()).isEqualTo(k2.hashCode()); + assertThat(k1.hashCode()).isNotEqualTo(k3.hashCode()); + assertThat(k1).isEqualTo(k2); + assertThat(k1).isNotEqualTo(k3); + } + + @Test + public void plainArray() { + Object k1 = generateKey(new Object[] { new String[]{"a", "b"} }); + Object k2 = generateKey(new Object[] { new String[]{"a", "b"} }); + Object k3 = generateKey(new Object[] { new String[]{"b", "a"} }); + assertThat(k1.hashCode()).isEqualTo(k2.hashCode()); + assertThat(k1.hashCode()).isNotEqualTo(k3.hashCode()); + assertThat(k1).isEqualTo(k2); + assertThat(k1).isNotEqualTo(k3); + } + + @Test + public void arrayWithExtraParameter() { + Object k1 = generateKey(new Object[] { new String[]{"a", "b"}, "c" }); + Object k2 = generateKey(new Object[] { new String[]{"a", "b"}, "c" }); + Object k3 = generateKey(new Object[] { new String[]{"b", "a"}, "c" }); + assertThat(k1.hashCode()).isEqualTo(k2.hashCode()); + assertThat(k1.hashCode()).isNotEqualTo(k3.hashCode()); + assertThat(k1).isEqualTo(k2); + assertThat(k1).isNotEqualTo(k3); + } + + @Test + public void serializedKeys() throws Exception { + Object k1 = SerializationTestUtils.serializeAndDeserialize(generateKey(new Object[] { "a", 1, "b" })); + Object k2 = SerializationTestUtils.serializeAndDeserialize(generateKey(new Object[] { "a", 1, "b" })); + Object k3 = SerializationTestUtils.serializeAndDeserialize(generateKey(new Object[] { "b", 1, "a" })); + assertThat(k1.hashCode()).isEqualTo(k2.hashCode()); + assertThat(k1.hashCode()).isNotEqualTo(k3.hashCode()); + assertThat(k1).isEqualTo(k2); + assertThat(k1).isNotEqualTo(k3); + } + + + private Object generateKey(Object[] arguments) { + return this.generator.generate(null, null, arguments); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/LifecycleContextBean.java b/spring-context/src/test/java/org/springframework/context/LifecycleContextBean.java new file mode 100644 index 0000000..1f04eb1 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/LifecycleContextBean.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.testfixture.beans.LifecycleBean; + +/** + * Simple bean to test ApplicationContext lifecycle methods for beans + * + * @author Colin Sampaleanu + * @since 03.07.2004 + */ +public class LifecycleContextBean extends LifecycleBean implements ApplicationContextAware { + + protected ApplicationContext owningContext; + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + super.setBeanFactory(beanFactory); + if (this.owningContext != null) + throw new RuntimeException("Factory called setBeanFactory after setApplicationContext"); + } + + @Override + public void afterPropertiesSet() { + super.afterPropertiesSet(); + if (this.owningContext == null) + throw new RuntimeException("Factory didn't call setApplicationContext before afterPropertiesSet on lifecycle bean"); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + if (this.owningFactory == null) + throw new RuntimeException("Factory called setApplicationContext before setBeanFactory"); + + this.owningContext = applicationContext; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/AbstractCircularImportDetectionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/AbstractCircularImportDetectionTests.java new file mode 100644 index 0000000..2880893 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/AbstractCircularImportDetectionTests.java @@ -0,0 +1,144 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * TCK-style unit tests for handling circular use of the {@link Import} annotation. + * Explore the subclass hierarchy for specific concrete implementations. + * + * @author Chris Beams + */ +public abstract class AbstractCircularImportDetectionTests { + + protected abstract ConfigurationClassParser newParser(); + + protected abstract String loadAsConfigurationSource(Class clazz) throws Exception; + + + @Test + public void simpleCircularImportIsDetected() throws Exception { + boolean threw = false; + try { + newParser().parse(loadAsConfigurationSource(A.class), "A"); + } + catch (BeanDefinitionParsingException ex) { + assertThat(ex.getMessage().contains( + "Illegal attempt by @Configuration class 'AbstractCircularImportDetectionTests.B' " + + "to import class 'AbstractCircularImportDetectionTests.A'")).as("Wrong message. Got: " + ex.getMessage()).isTrue(); + threw = true; + } + assertThat(threw).isTrue(); + } + + @Test + public void complexCircularImportIsDetected() throws Exception { + boolean threw = false; + try { + newParser().parse(loadAsConfigurationSource(X.class), "X"); + } + catch (BeanDefinitionParsingException ex) { + assertThat(ex.getMessage().contains( + "Illegal attempt by @Configuration class 'AbstractCircularImportDetectionTests.Z2' " + + "to import class 'AbstractCircularImportDetectionTests.Z'")).as("Wrong message. Got: " + ex.getMessage()).isTrue(); + threw = true; + } + assertThat(threw).isTrue(); + } + + + @Configuration + @Import(B.class) + static class A { + + @Bean + TestBean b1() { + return new TestBean(); + } + } + + + @Configuration + @Import(A.class) + static class B { + + @Bean + TestBean b2() { + return new TestBean(); + } + } + + + @Configuration + @Import({Y.class, Z.class}) + class X { + + @Bean + TestBean x() { + return new TestBean(); + } + } + + + @Configuration + class Y { + + @Bean + TestBean y() { + return new TestBean(); + } + } + + + @Configuration + @Import({Z1.class, Z2.class}) + class Z { + + @Bean + TestBean z() { + return new TestBean(); + } + } + + + @Configuration + class Z1 { + + @Bean + TestBean z1() { + return new TestBean(); + } + } + + + @Configuration + @Import(Z.class) + class Z2 { + + @Bean + TestBean z2() { + return new TestBean(); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/AggressiveFactoryBeanInstantiationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/AggressiveFactoryBeanInstantiationTests.java new file mode 100644 index 0000000..9bec246 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/AggressiveFactoryBeanInstantiationTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.context.ApplicationContext; + +/** + * @author Andy Wilkinson + */ +public class AggressiveFactoryBeanInstantiationTests { + + @Test + public void directlyRegisteredFactoryBean() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + context.register(SimpleFactoryBean.class); + context.addBeanFactoryPostProcessor(factory -> + BeanFactoryUtils.beanNamesForTypeIncludingAncestors(factory, String.class) + ); + context.refresh(); + } + } + + @Test + public void beanMethodFactoryBean() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + context.register(BeanMethodConfiguration.class); + context.addBeanFactoryPostProcessor(factory -> + BeanFactoryUtils.beanNamesForTypeIncludingAncestors(factory, String.class) + ); + context.refresh(); + } + } + + + @Configuration + static class BeanMethodConfiguration { + + @Bean + public SimpleFactoryBean simpleFactoryBean(ApplicationContext applicationContext) { + return new SimpleFactoryBean(applicationContext); + } + } + + + static class SimpleFactoryBean implements FactoryBean { + + public SimpleFactoryBean(ApplicationContext applicationContext) { + } + + @Override + public Object getObject() { + return new Object(); + } + + @Override + public Class getObjectType() { + return Object.class; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/AnnotationBeanNameGeneratorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/AnnotationBeanNameGeneratorTests.java new file mode 100644 index 0000000..4438d2e --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/AnnotationBeanNameGeneratorTests.java @@ -0,0 +1,192 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import example.scannable.DefaultNamedComponent; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.SimpleBeanDefinitionRegistry; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Controller; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link AnnotationBeanNameGenerator}. + * + * @author Rick Evans + * @author Juergen Hoeller + * @author Mark Fisher + * @author Chris Beams + * @author Sam Brannen + */ +public class AnnotationBeanNameGeneratorTests { + + private AnnotationBeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator(); + + + @Test + public void generateBeanNameWithNamedComponent() { + BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition(ComponentWithName.class); + String beanName = this.beanNameGenerator.generateBeanName(bd, registry); + assertThat(beanName).as("The generated beanName must *never* be null.").isNotNull(); + assertThat(StringUtils.hasText(beanName)).as("The generated beanName must *never* be blank.").isTrue(); + assertThat(beanName).isEqualTo("walden"); + } + + @Test + public void generateBeanNameWithDefaultNamedComponent() { + BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition(DefaultNamedComponent.class); + String beanName = this.beanNameGenerator.generateBeanName(bd, registry); + assertThat(beanName).as("The generated beanName must *never* be null.").isNotNull(); + assertThat(StringUtils.hasText(beanName)).as("The generated beanName must *never* be blank.").isTrue(); + assertThat(beanName).isEqualTo("thoreau"); + } + + @Test + public void generateBeanNameWithNamedComponentWhereTheNameIsBlank() { + BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition(ComponentWithBlankName.class); + String beanName = this.beanNameGenerator.generateBeanName(bd, registry); + assertThat(beanName).as("The generated beanName must *never* be null.").isNotNull(); + assertThat(StringUtils.hasText(beanName)).as("The generated beanName must *never* be blank.").isTrue(); + String expectedGeneratedBeanName = this.beanNameGenerator.buildDefaultBeanName(bd); + assertThat(beanName).isEqualTo(expectedGeneratedBeanName); + } + + @Test + public void generateBeanNameWithAnonymousComponentYieldsGeneratedBeanName() { + BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition(AnonymousComponent.class); + String beanName = this.beanNameGenerator.generateBeanName(bd, registry); + assertThat(beanName).as("The generated beanName must *never* be null.").isNotNull(); + assertThat(StringUtils.hasText(beanName)).as("The generated beanName must *never* be blank.").isTrue(); + String expectedGeneratedBeanName = this.beanNameGenerator.buildDefaultBeanName(bd); + assertThat(beanName).isEqualTo(expectedGeneratedBeanName); + } + + @Test + public void generateBeanNameFromMetaComponentWithStringValue() { + BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition(ComponentFromStringMeta.class); + String beanName = this.beanNameGenerator.generateBeanName(bd, registry); + assertThat(beanName).isEqualTo("henry"); + } + + @Test + public void generateBeanNameFromMetaComponentWithNonStringValue() { + BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition(ComponentFromNonStringMeta.class); + String beanName = this.beanNameGenerator.generateBeanName(bd, registry); + assertThat(beanName).isEqualTo("annotationBeanNameGeneratorTests.ComponentFromNonStringMeta"); + } + + @Test + public void generateBeanNameFromComposedControllerAnnotationWithoutName() { + // SPR-11360 + BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition(ComposedControllerAnnotationWithoutName.class); + String beanName = this.beanNameGenerator.generateBeanName(bd, registry); + String expectedGeneratedBeanName = this.beanNameGenerator.buildDefaultBeanName(bd); + assertThat(beanName).isEqualTo(expectedGeneratedBeanName); + } + + @Test + public void generateBeanNameFromComposedControllerAnnotationWithBlankName() { + // SPR-11360 + BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition(ComposedControllerAnnotationWithBlankName.class); + String beanName = this.beanNameGenerator.generateBeanName(bd, registry); + String expectedGeneratedBeanName = this.beanNameGenerator.buildDefaultBeanName(bd); + assertThat(beanName).isEqualTo(expectedGeneratedBeanName); + } + + @Test + public void generateBeanNameFromComposedControllerAnnotationWithStringValue() { + // SPR-11360 + BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition( + ComposedControllerAnnotationWithStringValue.class); + String beanName = this.beanNameGenerator.generateBeanName(bd, registry); + assertThat(beanName).isEqualTo("restController"); + } + + + @Component("walden") + private static class ComponentWithName { + } + + @Component(" ") + private static class ComponentWithBlankName { + } + + @Component + private static class AnonymousComponent { + } + + @Service("henry") + private static class ComponentFromStringMeta { + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @Component + public @interface NonStringMetaComponent { + + long value(); + } + + @NonStringMetaComponent(123) + private static class ComponentFromNonStringMeta { + } + + /** + * @see org.springframework.web.bind.annotation.RestController + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @Controller + public static @interface TestRestController { + + String value() default ""; + } + + @TestRestController + public static class ComposedControllerAnnotationWithoutName { + } + + @TestRestController(" ") + public static class ComposedControllerAnnotationWithBlankName { + } + + @TestRestController("restController") + public static class ComposedControllerAnnotationWithStringValue { + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/AnnotationConfigApplicationContextTests.java b/spring-context/src/test/java/org/springframework/context/annotation/AnnotationConfigApplicationContextTests.java new file mode 100644 index 0000000..f2a7c86 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/AnnotationConfigApplicationContextTests.java @@ -0,0 +1,557 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.util.Map; +import java.util.regex.Pattern; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation6.ComponentForScanning; +import org.springframework.context.annotation6.ConfigForScanning; +import org.springframework.context.annotation6.Jsr330NamedForScanning; +import org.springframework.core.ResolvableType; +import org.springframework.util.ObjectUtils; + +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.util.StringUtils.uncapitalize; + +/** + * @author Chris Beams + * @author Juergen Hoeller + */ +@SuppressWarnings("resource") +class AnnotationConfigApplicationContextTests { + + @Test + void scanAndRefresh() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.scan("org.springframework.context.annotation6"); + context.refresh(); + + context.getBean(uncapitalize(ConfigForScanning.class.getSimpleName())); + context.getBean("testBean"); // contributed by ConfigForScanning + context.getBean(uncapitalize(ComponentForScanning.class.getSimpleName())); + context.getBean(uncapitalize(Jsr330NamedForScanning.class.getSimpleName())); + Map beans = context.getBeansWithAnnotation(Configuration.class); + assertThat(beans).hasSize(1); + } + + @Test + void registerAndRefresh() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(Config.class, NameConfig.class); + context.refresh(); + + context.getBean("testBean"); + context.getBean("name"); + Map beans = context.getBeansWithAnnotation(Configuration.class); + assertThat(beans).hasSize(2); + } + + @Test + void getBeansWithAnnotation() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(Config.class, NameConfig.class, UntypedFactoryBean.class); + context.refresh(); + + context.getBean("testBean"); + context.getBean("name"); + Map beans = context.getBeansWithAnnotation(Configuration.class); + assertThat(beans).hasSize(2); + } + + @Test + void getBeanByType() { + ApplicationContext context = new AnnotationConfigApplicationContext(Config.class); + TestBean testBean = context.getBean(TestBean.class); + assertThat(testBean).isNotNull(); + assertThat(testBean.name).isEqualTo("foo"); + } + + @Test + void getBeanByTypeRaisesNoSuchBeanDefinitionException() { + ApplicationContext context = new AnnotationConfigApplicationContext(Config.class); + + // attempt to retrieve a bean that does not exist + Class targetType = Pattern.class; + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> + context.getBean(targetType)) + .withMessageContaining(format("No qualifying bean of type '%s'", targetType.getName())); + } + + @Test + void getBeanByTypeAmbiguityRaisesException() { + ApplicationContext context = new AnnotationConfigApplicationContext(TwoTestBeanConfig.class); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> + context.getBean(TestBean.class)) + .withMessageContaining("No qualifying bean of type '" + TestBean.class.getName() + "'") + .withMessageContaining("tb1") + .withMessageContaining("tb2"); + } + + /** + * Tests that Configuration classes are registered according to convention + * @see org.springframework.beans.factory.support.DefaultBeanNameGenerator#generateBeanName + */ + @Test + void defaultConfigClassBeanNameIsGeneratedProperly() { + ApplicationContext context = new AnnotationConfigApplicationContext(Config.class); + + // attempt to retrieve the instance by its generated bean name + Config configObject = (Config) context.getBean("annotationConfigApplicationContextTests.Config"); + assertThat(configObject).isNotNull(); + } + + /** + * Tests that specifying @Configuration(value="foo") results in registering + * the configuration class with bean name 'foo'. + */ + @Test + void explicitConfigClassBeanNameIsRespected() { + ApplicationContext context = new AnnotationConfigApplicationContext(ConfigWithCustomName.class); + + // attempt to retrieve the instance by its specified name + ConfigWithCustomName configObject = (ConfigWithCustomName) context.getBean("customConfigBeanName"); + assertThat(configObject).isNotNull(); + } + + @Test + void autowiringIsEnabledByDefault() { + ApplicationContext context = new AnnotationConfigApplicationContext(AutowiredConfig.class); + assertThat(context.getBean(TestBean.class).name).isEqualTo("foo"); + } + + @Test + void nullReturningBeanPostProcessor() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(AutowiredConfig.class); + context.getBeanFactory().addBeanPostProcessor(new BeanPostProcessor() { + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) { + return (bean instanceof TestBean ? null : bean); + } + }); + context.getBeanFactory().addBeanPostProcessor(new BeanPostProcessor() { + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) { + bean.getClass().getName(); + return bean; + } + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + bean.getClass().getName(); + return bean; + } + }); + context.refresh(); + } + + @Test + void individualBeans() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(BeanA.class, BeanB.class, BeanC.class); + context.refresh(); + + assertThat(context.getBean(BeanA.class).b).isSameAs(context.getBean(BeanB.class)); + assertThat(context.getBean(BeanA.class).c).isSameAs(context.getBean(BeanC.class)); + assertThat(context.getBean(BeanB.class).applicationContext).isSameAs(context); + } + + @Test + void individualNamedBeans() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean("a", BeanA.class); + context.registerBean("b", BeanB.class); + context.registerBean("c", BeanC.class); + context.refresh(); + + assertThat(context.getBean("a", BeanA.class).b).isSameAs(context.getBean("b")); + assertThat(context.getBean("a", BeanA.class).c).isSameAs(context.getBean("c")); + assertThat(context.getBean("b", BeanB.class).applicationContext).isSameAs(context); + } + + @Test + void individualBeanWithSupplier() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean(BeanA.class, + () -> new BeanA(context.getBean(BeanB.class), context.getBean(BeanC.class))); + context.registerBean(BeanB.class, BeanB::new); + context.registerBean(BeanC.class, BeanC::new); + context.refresh(); + + assertThat(context.getBeanFactory().containsSingleton("annotationConfigApplicationContextTests.BeanA")).isTrue(); + assertThat(context.getBean(BeanA.class).b).isSameAs(context.getBean(BeanB.class)); + assertThat(context.getBean(BeanA.class).c).isSameAs(context.getBean(BeanC.class)); + assertThat(context.getBean(BeanB.class).applicationContext).isSameAs(context); + + assertThat(context.getDefaultListableBeanFactory().getDependentBeans("annotationConfigApplicationContextTests.BeanB")) + .containsExactly("annotationConfigApplicationContextTests.BeanA"); + assertThat(context.getDefaultListableBeanFactory().getDependentBeans("annotationConfigApplicationContextTests.BeanC")) + .containsExactly("annotationConfigApplicationContextTests.BeanA"); + } + + @Test + void individualBeanWithSupplierAndCustomizer() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean(BeanA.class, + () -> new BeanA(context.getBean(BeanB.class), context.getBean(BeanC.class)), + bd -> bd.setLazyInit(true)); + context.registerBean(BeanB.class, BeanB::new); + context.registerBean(BeanC.class, BeanC::new); + context.refresh(); + + assertThat(context.getBeanFactory().containsSingleton("annotationConfigApplicationContextTests.BeanA")).isFalse(); + assertThat(context.getBean(BeanA.class).b).isSameAs(context.getBean(BeanB.class)); + assertThat(context.getBean(BeanA.class).c).isSameAs(context.getBean(BeanC.class)); + assertThat(context.getBean(BeanB.class).applicationContext).isSameAs(context); + } + + @Test + void individualNamedBeanWithSupplier() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean("a", BeanA.class, + () -> new BeanA(context.getBean(BeanB.class), context.getBean(BeanC.class))); + context.registerBean("b", BeanB.class, BeanB::new); + context.registerBean("c", BeanC.class, BeanC::new); + context.refresh(); + + assertThat(context.getBeanFactory().containsSingleton("a")).isTrue(); + assertThat(context.getBean(BeanA.class).b).isSameAs(context.getBean("b", BeanB.class)); + assertThat(context.getBean("a", BeanA.class).c).isSameAs(context.getBean("c")); + assertThat(context.getBean("b", BeanB.class).applicationContext).isSameAs(context); + } + + @Test + void individualNamedBeanWithSupplierAndCustomizer() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean("a", BeanA.class, + () -> new BeanA(context.getBean(BeanB.class), context.getBean(BeanC.class)), + bd -> bd.setLazyInit(true)); + context.registerBean("b", BeanB.class, BeanB::new); + context.registerBean("c", BeanC.class, BeanC::new); + context.refresh(); + + assertThat(context.getBeanFactory().containsSingleton("a")).isFalse(); + assertThat(context.getBean(BeanA.class).b).isSameAs(context.getBean("b", BeanB.class)); + assertThat(context.getBean("a", BeanA.class).c).isSameAs(context.getBean("c")); + assertThat(context.getBean("b", BeanB.class).applicationContext).isSameAs(context); + } + + @Test + void individualBeanWithNullReturningSupplier() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean("a", BeanA.class, () -> null); + context.registerBean("b", BeanB.class, BeanB::new); + context.registerBean("c", BeanC.class, BeanC::new); + context.refresh(); + + assertThat(ObjectUtils.containsElement(context.getBeanNamesForType(BeanA.class), "a")).isTrue(); + assertThat(ObjectUtils.containsElement(context.getBeanNamesForType(BeanB.class), "b")).isTrue(); + assertThat(ObjectUtils.containsElement(context.getBeanNamesForType(BeanC.class), "c")).isTrue(); + assertThat(context.getBeansOfType(BeanA.class)).isEmpty(); + assertThat(context.getBeansOfType(BeanB.class).values().iterator().next()).isSameAs(context.getBean(BeanB.class)); + assertThat(context.getBeansOfType(BeanC.class).values().iterator().next()).isSameAs(context.getBean(BeanC.class)); + } + + @Test + void individualBeanWithSpecifiedConstructorArguments() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + BeanB b = new BeanB(); + BeanC c = new BeanC(); + context.registerBean(BeanA.class, b, c); + context.refresh(); + + assertThat(context.getBean(BeanA.class).b).isSameAs(b); + assertThat(context.getBean(BeanA.class).c).isSameAs(c); + assertThat(b.applicationContext).isNull(); + } + + @Test + void individualNamedBeanWithSpecifiedConstructorArguments() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + BeanB b = new BeanB(); + BeanC c = new BeanC(); + context.registerBean("a", BeanA.class, b, c); + context.refresh(); + + assertThat(context.getBean("a", BeanA.class).b).isSameAs(b); + assertThat(context.getBean("a", BeanA.class).c).isSameAs(c); + assertThat(b.applicationContext).isNull(); + } + + @Test + void individualBeanWithMixedConstructorArguments() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + BeanC c = new BeanC(); + context.registerBean(BeanA.class, c); + context.registerBean(BeanB.class); + context.refresh(); + + assertThat(context.getBean(BeanA.class).b).isSameAs(context.getBean(BeanB.class)); + assertThat(context.getBean(BeanA.class).c).isSameAs(c); + assertThat(context.getBean(BeanB.class).applicationContext).isSameAs(context); + } + + @Test + void individualNamedBeanWithMixedConstructorArguments() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + BeanC c = new BeanC(); + context.registerBean("a", BeanA.class, c); + context.registerBean("b", BeanB.class); + context.refresh(); + + assertThat(context.getBean("a", BeanA.class).b).isSameAs(context.getBean("b", BeanB.class)); + assertThat(context.getBean("a", BeanA.class).c).isSameAs(c); + assertThat(context.getBean("b", BeanB.class).applicationContext).isSameAs(context); + } + + @Test + void individualBeanWithFactoryBeanSupplier() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean("fb", NonInstantiatedFactoryBean.class, NonInstantiatedFactoryBean::new, bd -> bd.setLazyInit(true)); + context.refresh(); + + assertThat(context.getType("fb")).isEqualTo(String.class); + assertThat(context.getType("&fb")).isEqualTo(NonInstantiatedFactoryBean.class); + assertThat(context.getBeanNamesForType(FactoryBean.class)).hasSize(1); + assertThat(context.getBeanNamesForType(NonInstantiatedFactoryBean.class)).hasSize(1); + } + + @Test + void individualBeanWithFactoryBeanSupplierAndTargetType() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + RootBeanDefinition bd = new RootBeanDefinition(); + bd.setInstanceSupplier(NonInstantiatedFactoryBean::new); + bd.setTargetType(ResolvableType.forClassWithGenerics(FactoryBean.class, String.class)); + bd.setLazyInit(true); + context.registerBeanDefinition("fb", bd); + context.refresh(); + + assertThat(context.getType("fb")).isEqualTo(String.class); + assertThat(context.getType("&fb")).isEqualTo(FactoryBean.class); + assertThat(context.getBeanNamesForType(FactoryBean.class)).hasSize(1); + assertThat(context.getBeanNamesForType(NonInstantiatedFactoryBean.class)).isEmpty(); + } + + @Test + void individualBeanWithFactoryBeanObjectTypeAsTargetType() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + RootBeanDefinition bd = new RootBeanDefinition(); + bd.setBeanClass(TypedFactoryBean.class); + bd.setTargetType(String.class); + context.registerBeanDefinition("fb", bd); + context.refresh(); + + assertThat(context.getType("&fb")).isEqualTo(TypedFactoryBean.class); + assertThat(context.getType("fb")).isEqualTo(String.class); + assertThat(context.getBeanNamesForType(FactoryBean.class)).hasSize(1); + assertThat(context.getBeanNamesForType(TypedFactoryBean.class)).hasSize(1); + } + + @Test + void individualBeanWithFactoryBeanObjectTypeAsTargetTypeAndLazy() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + RootBeanDefinition bd = new RootBeanDefinition(); + bd.setBeanClass(TypedFactoryBean.class); + bd.setTargetType(String.class); + bd.setLazyInit(true); + context.registerBeanDefinition("fb", bd); + context.refresh(); + + assertThat(context.getType("&fb")).isNull(); + assertThat(context.getType("fb")).isEqualTo(String.class); + assertThat(context.getBean("&fb")).isInstanceOf(FactoryBean.class); + assertThat(context.getType("&fb")).isEqualTo(TypedFactoryBean.class); + assertThat(context.getType("fb")).isEqualTo(String.class); + assertThat(context.getBeanNamesForType(FactoryBean.class)).hasSize(1); + assertThat(context.getBeanNamesForType(TypedFactoryBean.class)).hasSize(1); + } + + + @Configuration + static class Config { + + @Bean + TestBean testBean() { + TestBean testBean = new TestBean(); + testBean.name = "foo"; + return testBean; + } + } + + @Configuration("customConfigBeanName") + static class ConfigWithCustomName { + + @Bean + TestBean testBean() { + return new TestBean(); + } + } + + @Configuration + static class TwoTestBeanConfig { + + @Bean TestBean tb1() { + return new TestBean(); + } + + @Bean TestBean tb2() { + return new TestBean(); + } + } + + @Configuration + static class NameConfig { + + @Bean String name() { return "foo"; } + } + + @Configuration + @Import(NameConfig.class) + static class AutowiredConfig { + + @Autowired String autowiredName; + + @Bean TestBean testBean() { + TestBean testBean = new TestBean(); + testBean.name = autowiredName; + return testBean; + } + } + + static class BeanA { + + BeanB b; + BeanC c; + + + @Autowired + BeanA(BeanB b, BeanC c) { + this.b = b; + this.c = c; + } + } + + static class BeanB { + + @Autowired ApplicationContext applicationContext; + + public BeanB() { + } + } + + static class BeanC {} + + static class NonInstantiatedFactoryBean implements FactoryBean { + + NonInstantiatedFactoryBean() { + throw new IllegalStateException(); + } + + @Override + public String getObject() { + return ""; + } + + @Override + public Class getObjectType() { + return String.class; + } + + @Override + public boolean isSingleton() { + return true; + } + } + + static class TypedFactoryBean implements FactoryBean { + + @Override + public String getObject() { + return ""; + } + + @Override + public Class getObjectType() { + return String.class; + } + + @Override + public boolean isSingleton() { + return true; + } + } + + static class UntypedFactoryBean implements FactoryBean { + + @Override + public Object getObject() { + return null; + } + + @Override + public Class getObjectType() { + return null; + } + + @Override + public boolean isSingleton() { + return false; + } + } +} + +class TestBean { + + String name; + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (name == null ? 0 : name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + TestBean other = (TestBean) obj; + if (name == null) { + if (other.name != null) + return false; + } + else if (!name.equals(other.name)) + return false; + return true; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/AnnotationScopeMetadataResolverTests.java b/spring-context/src/test/java/org/springframework/context/annotation/AnnotationScopeMetadataResolverTests.java new file mode 100644 index 0000000..838b9cc --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/AnnotationScopeMetadataResolverTests.java @@ -0,0 +1,165 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.springframework.context.annotation.ScopedProxyMode.INTERFACES; +import static org.springframework.context.annotation.ScopedProxyMode.NO; +import static org.springframework.context.annotation.ScopedProxyMode.TARGET_CLASS; + +/** + * Unit tests for {@link AnnotationScopeMetadataResolver}. + * + * @author Rick Evans + * @author Chris Beams + * @author Juergen Hoeller + * @author Sam Brannen + */ +public class AnnotationScopeMetadataResolverTests { + + private AnnotationScopeMetadataResolver scopeMetadataResolver = new AnnotationScopeMetadataResolver(); + + + @Test + public void resolveScopeMetadataShouldNotApplyScopedProxyModeToSingleton() { + AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition(AnnotatedWithSingletonScope.class); + ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(bd); + assertThat(scopeMetadata).as("resolveScopeMetadata(..) must *never* return null.").isNotNull(); + assertThat(scopeMetadata.getScopeName()).isEqualTo(BeanDefinition.SCOPE_SINGLETON); + assertThat(scopeMetadata.getScopedProxyMode()).isEqualTo(NO); + } + + @Test + public void resolveScopeMetadataShouldApplyScopedProxyModeToPrototype() { + this.scopeMetadataResolver = new AnnotationScopeMetadataResolver(INTERFACES); + AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition(AnnotatedWithPrototypeScope.class); + ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(bd); + assertThat(scopeMetadata).as("resolveScopeMetadata(..) must *never* return null.").isNotNull(); + assertThat(scopeMetadata.getScopeName()).isEqualTo(BeanDefinition.SCOPE_PROTOTYPE); + assertThat(scopeMetadata.getScopedProxyMode()).isEqualTo(INTERFACES); + } + + @Test + public void resolveScopeMetadataShouldReadScopedProxyModeFromAnnotation() { + AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition(AnnotatedWithScopedProxy.class); + ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(bd); + assertThat(scopeMetadata).as("resolveScopeMetadata(..) must *never* return null.").isNotNull(); + assertThat(scopeMetadata.getScopeName()).isEqualTo("request"); + assertThat(scopeMetadata.getScopedProxyMode()).isEqualTo(TARGET_CLASS); + } + + @Test + public void customRequestScope() { + AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition(AnnotatedWithCustomRequestScope.class); + ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(bd); + assertThat(scopeMetadata).as("resolveScopeMetadata(..) must *never* return null.").isNotNull(); + assertThat(scopeMetadata.getScopeName()).isEqualTo("request"); + assertThat(scopeMetadata.getScopedProxyMode()).isEqualTo(NO); + } + + @Test + public void customRequestScopeViaAsm() throws IOException { + MetadataReaderFactory readerFactory = new SimpleMetadataReaderFactory(); + MetadataReader reader = readerFactory.getMetadataReader(AnnotatedWithCustomRequestScope.class.getName()); + AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition(reader.getAnnotationMetadata()); + ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(bd); + assertThat(scopeMetadata).as("resolveScopeMetadata(..) must *never* return null.").isNotNull(); + assertThat(scopeMetadata.getScopeName()).isEqualTo("request"); + assertThat(scopeMetadata.getScopedProxyMode()).isEqualTo(NO); + } + + @Test + public void customRequestScopeWithAttribute() { + AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition( + AnnotatedWithCustomRequestScopeWithAttributeOverride.class); + ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(bd); + assertThat(scopeMetadata).as("resolveScopeMetadata(..) must *never* return null.").isNotNull(); + assertThat(scopeMetadata.getScopeName()).isEqualTo("request"); + assertThat(scopeMetadata.getScopedProxyMode()).isEqualTo(TARGET_CLASS); + } + + @Test + public void customRequestScopeWithAttributeViaAsm() throws IOException { + MetadataReaderFactory readerFactory = new SimpleMetadataReaderFactory(); + MetadataReader reader = readerFactory.getMetadataReader(AnnotatedWithCustomRequestScopeWithAttributeOverride.class.getName()); + AnnotatedBeanDefinition bd = new AnnotatedGenericBeanDefinition(reader.getAnnotationMetadata()); + ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(bd); + assertThat(scopeMetadata).as("resolveScopeMetadata(..) must *never* return null.").isNotNull(); + assertThat(scopeMetadata.getScopeName()).isEqualTo("request"); + assertThat(scopeMetadata.getScopedProxyMode()).isEqualTo(TARGET_CLASS); + } + + @Test + public void ctorWithNullScopedProxyMode() { + assertThatIllegalArgumentException().isThrownBy(() -> + new AnnotationScopeMetadataResolver(null)); + } + + @Test + public void setScopeAnnotationTypeWithNullType() { + assertThatIllegalArgumentException().isThrownBy(() -> + scopeMetadataResolver.setScopeAnnotationType(null)); + } + + + @Retention(RetentionPolicy.RUNTIME) + @Scope("request") + @interface CustomRequestScope { + } + + @Retention(RetentionPolicy.RUNTIME) + @Scope("request") + @interface CustomRequestScopeWithAttributeOverride { + + ScopedProxyMode proxyMode(); + } + + @Scope("singleton") + private static class AnnotatedWithSingletonScope { + } + + @Scope("prototype") + private static class AnnotatedWithPrototypeScope { + } + + @Scope(scopeName = "request", proxyMode = TARGET_CLASS) + private static class AnnotatedWithScopedProxy { + } + + @CustomRequestScope + private static class AnnotatedWithCustomRequestScope { + } + + @CustomRequestScopeWithAttributeOverride(proxyMode = TARGET_CLASS) + private static class AnnotatedWithCustomRequestScopeWithAttributeOverride { + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/AsmCircularImportDetectionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/AsmCircularImportDetectionTests.java new file mode 100644 index 0000000..a6ad85b --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/AsmCircularImportDetectionTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.springframework.beans.factory.parsing.FailFastProblemReporter; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.type.classreading.CachingMetadataReaderFactory; + +/** + * Unit test proving that ASM-based {@link ConfigurationClassParser} correctly detects + * circular use of the {@link Import @Import} annotation. + * + * @author Chris Beams + */ +public class AsmCircularImportDetectionTests extends AbstractCircularImportDetectionTests { + + @Override + protected ConfigurationClassParser newParser() { + return new ConfigurationClassParser( + new CachingMetadataReaderFactory(), + new FailFastProblemReporter(), + new StandardEnvironment(), + new DefaultResourceLoader(), + new AnnotationBeanNameGenerator(), + new DefaultListableBeanFactory()); + } + + @Override + protected String loadAsConfigurationSource(Class clazz) throws Exception { + return clazz.getName(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/AutoProxyLazyInitTests.java b/spring-context/src/test/java/org/springframework/context/annotation/AutoProxyLazyInitTests.java new file mode 100644 index 0000000..c73ddf7 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/AutoProxyLazyInitTests.java @@ -0,0 +1,237 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import javax.annotation.PreDestroy; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator; +import org.springframework.aop.framework.autoproxy.target.LazyInitTargetSourceCreator; +import org.springframework.aop.target.AbstractBeanFactoryBasedTargetSource; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.event.ApplicationContextEvent; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link BeanNameAutoProxyCreator} and + * {@link LazyInitTargetSourceCreator}. + * + * @author Juergen Hoeller + * @author Arrault Fabien + * @author Sam Brannen + */ +class AutoProxyLazyInitTests { + + @BeforeEach + void resetBeans() { + MyBeanImpl.initialized = false; + } + + @Test + void withStaticBeanMethod() { + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithStatic.class); + MyBean bean = ctx.getBean(MyBean.class); + + assertThat(MyBeanImpl.initialized).isFalse(); + bean.doIt(); + assertThat(MyBeanImpl.initialized).isTrue(); + + ctx.close(); + } + + @Test + void withStaticBeanMethodAndInterface() { + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithStaticAndInterface.class); + MyBean bean = ctx.getBean(MyBean.class); + + assertThat(MyBeanImpl.initialized).isFalse(); + bean.doIt(); + assertThat(MyBeanImpl.initialized).isTrue(); + + ctx.close(); + } + + @Test + void withNonStaticBeanMethod() { + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithNonStatic.class); + MyBean bean = ctx.getBean(MyBean.class); + + assertThat(MyBeanImpl.initialized).isFalse(); + bean.doIt(); + assertThat(MyBeanImpl.initialized).isTrue(); + + ctx.close(); + } + + @Test + void withNonStaticBeanMethodAndInterface() { + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithNonStaticAndInterface.class); + MyBean bean = ctx.getBean(MyBean.class); + + assertThat(MyBeanImpl.initialized).isFalse(); + bean.doIt(); + assertThat(MyBeanImpl.initialized).isTrue(); + + ctx.close(); + } + + + interface MyBean { + + String doIt(); + } + + + static class MyBeanImpl implements MyBean { + + static boolean initialized = false; + + MyBeanImpl() { + initialized = true; + } + + @Override + public String doIt() { + return "From implementation"; + } + + @PreDestroy + public void destroy() { + } + } + + + @Configuration + static class ConfigWithStatic { + + @Bean + BeanNameAutoProxyCreator lazyInitAutoProxyCreator() { + BeanNameAutoProxyCreator autoProxyCreator = new BeanNameAutoProxyCreator(); + autoProxyCreator.setBeanNames("*"); + autoProxyCreator.setCustomTargetSourceCreators(lazyInitTargetSourceCreator()); + return autoProxyCreator; + } + + @Bean + LazyInitTargetSourceCreator lazyInitTargetSourceCreator() { + return new StrictLazyInitTargetSourceCreator(); + } + + @Bean + @Lazy + static MyBean myBean() { + return new MyBeanImpl(); + } + } + + + @Configuration + static class ConfigWithStaticAndInterface implements ApplicationListener { + + @Bean + BeanNameAutoProxyCreator lazyInitAutoProxyCreator() { + BeanNameAutoProxyCreator autoProxyCreator = new BeanNameAutoProxyCreator(); + autoProxyCreator.setBeanNames("*"); + autoProxyCreator.setCustomTargetSourceCreators(lazyInitTargetSourceCreator()); + return autoProxyCreator; + } + + @Bean + LazyInitTargetSourceCreator lazyInitTargetSourceCreator() { + return new StrictLazyInitTargetSourceCreator(); + } + + @Bean + @Lazy + static MyBean myBean() { + return new MyBeanImpl(); + } + + @Override + public void onApplicationEvent(ApplicationContextEvent event) { + } + } + + + @Configuration + static class ConfigWithNonStatic { + + @Bean + BeanNameAutoProxyCreator lazyInitAutoProxyCreator() { + BeanNameAutoProxyCreator autoProxyCreator = new BeanNameAutoProxyCreator(); + autoProxyCreator.setBeanNames("*"); + autoProxyCreator.setCustomTargetSourceCreators(lazyInitTargetSourceCreator()); + return autoProxyCreator; + } + + @Bean + LazyInitTargetSourceCreator lazyInitTargetSourceCreator() { + return new StrictLazyInitTargetSourceCreator(); + } + + @Bean + @Lazy + MyBean myBean() { + return new MyBeanImpl(); + } + } + + + @Configuration + static class ConfigWithNonStaticAndInterface implements ApplicationListener { + + @Bean + BeanNameAutoProxyCreator lazyInitAutoProxyCreator() { + BeanNameAutoProxyCreator autoProxyCreator = new BeanNameAutoProxyCreator(); + autoProxyCreator.setBeanNames("*"); + autoProxyCreator.setCustomTargetSourceCreators(lazyInitTargetSourceCreator()); + return autoProxyCreator; + } + + @Bean + LazyInitTargetSourceCreator lazyInitTargetSourceCreator() { + return new StrictLazyInitTargetSourceCreator(); + } + + @Bean + @Lazy + MyBean myBean() { + return new MyBeanImpl(); + } + + @Override + public void onApplicationEvent(ApplicationContextEvent event) { + } + } + + + private static class StrictLazyInitTargetSourceCreator extends LazyInitTargetSourceCreator { + + @Override + protected AbstractBeanFactoryBasedTargetSource createBeanFactoryBasedTargetSource(Class beanClass, String beanName) { + if ("myBean".equals(beanName)) { + assertThat(beanClass).isEqualTo(MyBean.class); + } + return super.createBeanFactoryBasedTargetSource(beanClass, beanName); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/BeanAge.java b/spring-context/src/test/java/org/springframework/context/annotation/BeanAge.java new file mode 100644 index 0000000..f37a917 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/BeanAge.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.beans.factory.annotation.Qualifier; + +@Target({ElementType.METHOD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Qualifier +public @interface BeanAge { + int value(); +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/BeanMethodMetadataTests.java b/spring-context/src/test/java/org/springframework/context/annotation/BeanMethodMetadataTests.java new file mode 100644 index 0000000..b812117 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/BeanMethodMetadataTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; + +import static org.assertj.core.api.Assertions.assertThat; + + + + +/** + * @author Phillip Webb + * @author Juergen Hoeller + */ +public class BeanMethodMetadataTests { + + @Test + public void providesBeanMethodBeanDefinition() throws Exception { + AnnotationConfigApplicationContext context= new AnnotationConfigApplicationContext(Conf.class); + BeanDefinition beanDefinition = context.getBeanDefinition("myBean"); + assertThat(beanDefinition).as("should provide AnnotatedBeanDefinition").isInstanceOf(AnnotatedBeanDefinition.class); + Map annotationAttributes = + ((AnnotatedBeanDefinition) beanDefinition).getFactoryMethodMetadata().getAnnotationAttributes(MyAnnotation.class.getName()); + assertThat(annotationAttributes.get("value")).isEqualTo("test"); + context.close(); + } + + + @Configuration + static class Conf { + + @Bean + @MyAnnotation("test") + public MyBean myBean() { + return new MyBean(); + } + } + + + static class MyBean { + } + + + @Retention(RetentionPolicy.RUNTIME) + public static @interface MyAnnotation { + + String value(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/BeanMethodPolymorphismTests.java b/spring-context/src/test/java/org/springframework/context/annotation/BeanMethodPolymorphismTests.java new file mode 100644 index 0000000..61e5868 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/BeanMethodPolymorphismTests.java @@ -0,0 +1,318 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator; +import org.springframework.aop.interceptor.SimpleTraceInterceptor; +import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.beans.factory.support.RootBeanDefinition; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests regarding overloading and overriding of bean methods. + * Related to SPR-6618. + * + * @author Chris Beams + * @author Phillip Webb + * @author Juergen Hoeller + */ +@SuppressWarnings("resource") +public class BeanMethodPolymorphismTests { + + @Test + public void beanMethodDetectedOnSuperClass() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class); + ctx.getBean("testBean", TestBean.class); + } + + @Test + public void beanMethodOverriding() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(OverridingConfig.class); + ctx.setAllowBeanDefinitionOverriding(false); + ctx.refresh(); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isFalse(); + assertThat(ctx.getBean("testBean", TestBean.class).toString()).isEqualTo("overridden"); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isTrue(); + } + + @Test + public void beanMethodOverridingOnASM() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.registerBeanDefinition("config", new RootBeanDefinition(OverridingConfig.class.getName())); + ctx.setAllowBeanDefinitionOverriding(false); + ctx.refresh(); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isFalse(); + assertThat(ctx.getBean("testBean", TestBean.class).toString()).isEqualTo("overridden"); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isTrue(); + } + + @Test + public void beanMethodOverridingWithNarrowedReturnType() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(NarrowedOverridingConfig.class); + ctx.setAllowBeanDefinitionOverriding(false); + ctx.refresh(); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isFalse(); + assertThat(ctx.getBean("testBean", TestBean.class).toString()).isEqualTo("overridden"); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isTrue(); + } + + @Test + public void beanMethodOverridingWithNarrowedReturnTypeOnASM() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.registerBeanDefinition("config", new RootBeanDefinition(NarrowedOverridingConfig.class.getName())); + ctx.setAllowBeanDefinitionOverriding(false); + ctx.refresh(); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isFalse(); + assertThat(ctx.getBean("testBean", TestBean.class).toString()).isEqualTo("overridden"); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("testBean")).isTrue(); + } + + @Test + public void beanMethodOverloadingWithoutInheritance() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConfigWithOverloading.class); + ctx.setAllowBeanDefinitionOverriding(false); + ctx.refresh(); + assertThat(ctx.getBean(String.class)).isEqualTo("regular"); + } + + @Test + public void beanMethodOverloadingWithoutInheritanceAndExtraDependency() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConfigWithOverloading.class); + ctx.getDefaultListableBeanFactory().registerSingleton("anInt", 5); + ctx.setAllowBeanDefinitionOverriding(false); + ctx.refresh(); + assertThat(ctx.getBean(String.class)).isEqualTo("overloaded5"); + } + + @Test + public void beanMethodOverloadingWithAdditionalMetadata() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConfigWithOverloadingAndAdditionalMetadata.class); + ctx.setAllowBeanDefinitionOverriding(false); + ctx.refresh(); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isFalse(); + assertThat(ctx.getBean(String.class)).isEqualTo("regular"); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isTrue(); + } + + @Test + public void beanMethodOverloadingWithAdditionalMetadataButOtherMethodExecuted() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConfigWithOverloadingAndAdditionalMetadata.class); + ctx.getDefaultListableBeanFactory().registerSingleton("anInt", 5); + ctx.setAllowBeanDefinitionOverriding(false); + ctx.refresh(); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isFalse(); + assertThat(ctx.getBean(String.class)).isEqualTo("overloaded5"); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isTrue(); + } + + @Test + public void beanMethodOverloadingWithInheritance() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(SubConfig.class); + ctx.setAllowBeanDefinitionOverriding(false); + ctx.refresh(); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isFalse(); + assertThat(ctx.getBean(String.class)).isEqualTo("overloaded5"); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isTrue(); + } + + // SPR-11025 + @Test + public void beanMethodOverloadingWithInheritanceAndList() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(SubConfigWithList.class); + ctx.setAllowBeanDefinitionOverriding(false); + ctx.refresh(); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isFalse(); + assertThat(ctx.getBean(String.class)).isEqualTo("overloaded5"); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("aString")).isTrue(); + } + + /** + * When inheritance is not involved, it is still possible to override a bean method from + * the container's point of view. This is not strictly 'overloading' of a method per se, + * so it's referred to here as 'shadowing' to distinguish the difference. + */ + @Test + public void beanMethodShadowing() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ShadowConfig.class); + assertThat(ctx.getBean(String.class)).isEqualTo("shadow"); + } + + @Test + public void beanMethodThroughAopProxy() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(Config.class); + ctx.register(AnnotationAwareAspectJAutoProxyCreator.class); + ctx.register(TestAdvisor.class); + ctx.refresh(); + ctx.getBean("testBean", TestBean.class); + } + + + @Configuration + static class BaseConfig { + + @Bean + public TestBean testBean() { + return new TestBean(); + } + } + + + @Configuration + static class Config extends BaseConfig { + } + + + @Configuration + static class OverridingConfig extends BaseConfig { + + @Bean @Lazy + @Override + public TestBean testBean() { + return new TestBean() { + @Override + public String toString() { + return "overridden"; + } + }; + } + } + + + static class ExtendedTestBean extends TestBean { + } + + + @Configuration + static class NarrowedOverridingConfig extends BaseConfig { + + @Bean @Lazy + @Override + public ExtendedTestBean testBean() { + return new ExtendedTestBean() { + @Override + public String toString() { + return "overridden"; + } + }; + } + } + + + @Configuration + static class ConfigWithOverloading { + + @Bean + String aString() { + return "regular"; + } + + @Bean + String aString(Integer dependency) { + return "overloaded" + dependency; + } + } + + + @Configuration + static class ConfigWithOverloadingAndAdditionalMetadata { + + @Bean @Lazy + String aString() { + return "regular"; + } + + @Bean @Lazy + String aString(Integer dependency) { + return "overloaded" + dependency; + } + } + + + @Configuration + static class SuperConfig { + + @Bean + String aString() { + return "super"; + } + } + + + @Configuration + static class SubConfig extends SuperConfig { + + @Bean + Integer anInt() { + return 5; + } + + @Bean @Lazy + String aString(Integer dependency) { + return "overloaded" + dependency; + } + } + + + @Configuration + static class SubConfigWithList extends SuperConfig { + + @Bean + Integer anInt() { + return 5; + } + + @Bean @Lazy + String aString(List dependency) { + return "overloaded" + dependency.get(0); + } + } + + + @Configuration + @Import(SubConfig.class) + static class ShadowConfig { + + @Bean + String aString() { + return "shadow"; + } + } + + + @SuppressWarnings("serial") + public static class TestAdvisor extends DefaultPointcutAdvisor { + + public TestAdvisor() { + super(new SimpleTraceInterceptor()); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java new file mode 100644 index 0000000..cfb389e --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java @@ -0,0 +1,544 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import example.scannable.CustomComponent; +import example.scannable.FooService; +import example.scannable.FooServiceImpl; +import example.scannable.NamedStubDao; +import example.scannable.StubFooDao; +import org.aspectj.lang.annotation.Aspect; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.BeanInstantiationException; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.support.StaticListableBeanFactory; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.MessageSource; +import org.springframework.context.annotation2.NamedStubDao2; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.context.testfixture.index.CandidateComponentsTestClassLoader; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.core.type.filter.AssignableTypeFilter; +import org.springframework.stereotype.Component; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * @author Mark Fisher + * @author Juergen Hoeller + * @author Chris Beams + */ +public class ClassPathBeanDefinitionScannerTests { + + private static final String BASE_PACKAGE = "example.scannable"; + + + @Test + public void testSimpleScanWithDefaultFiltersAndPostProcessors() { + GenericApplicationContext context = new GenericApplicationContext(); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); + int beanCount = scanner.scan(BASE_PACKAGE); + assertThat(beanCount).isEqualTo(12); + assertThat(context.containsBean("serviceInvocationCounter")).isTrue(); + assertThat(context.containsBean("fooServiceImpl")).isTrue(); + assertThat(context.containsBean("stubFooDao")).isTrue(); + assertThat(context.containsBean("myNamedComponent")).isTrue(); + assertThat(context.containsBean("myNamedDao")).isTrue(); + assertThat(context.containsBean("thoreau")).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_PROCESSOR_BEAN_NAME)).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_FACTORY_BEAN_NAME)).isTrue(); + context.refresh(); + + FooServiceImpl fooService = context.getBean("fooServiceImpl", FooServiceImpl.class); + assertThat(context.getDefaultListableBeanFactory().containsSingleton("myNamedComponent")).isTrue(); + assertThat(fooService.foo(123)).isEqualTo("bar"); + assertThat(fooService.lookupFoo(123)).isEqualTo("bar"); + assertThat(context.isPrototype("thoreau")).isTrue(); + } + + @Test + public void testSimpleScanWithDefaultFiltersAndPrimaryLazyBean() { + GenericApplicationContext context = new GenericApplicationContext(); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); + scanner.scan(BASE_PACKAGE); + scanner.scan("org.springframework.context.annotation5"); + assertThat(context.containsBean("serviceInvocationCounter")).isTrue(); + assertThat(context.containsBean("fooServiceImpl")).isTrue(); + assertThat(context.containsBean("stubFooDao")).isTrue(); + assertThat(context.containsBean("myNamedComponent")).isTrue(); + assertThat(context.containsBean("myNamedDao")).isTrue(); + assertThat(context.containsBean("otherFooDao")).isTrue(); + context.refresh(); + + assertThat(context.getBeanFactory().containsSingleton("otherFooDao")).isFalse(); + assertThat(context.getBeanFactory().containsSingleton("fooServiceImpl")).isFalse(); + FooServiceImpl fooService = context.getBean("fooServiceImpl", FooServiceImpl.class); + assertThat(context.getBeanFactory().containsSingleton("otherFooDao")).isTrue(); + assertThat(fooService.foo(123)).isEqualTo("other"); + assertThat(fooService.lookupFoo(123)).isEqualTo("other"); + } + + @Test + public void testDoubleScan() { + GenericApplicationContext context = new GenericApplicationContext(); + + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); + int beanCount = scanner.scan(BASE_PACKAGE); + assertThat(beanCount).isEqualTo(12); + + ClassPathBeanDefinitionScanner scanner2 = new ClassPathBeanDefinitionScanner(context) { + @Override + protected void postProcessBeanDefinition(AbstractBeanDefinition beanDefinition, String beanName) { + super.postProcessBeanDefinition(beanDefinition, beanName); + beanDefinition.setAttribute("someDifference", "someValue"); + } + }; + scanner2.scan(BASE_PACKAGE); + + assertThat(context.containsBean("serviceInvocationCounter")).isTrue(); + assertThat(context.containsBean("fooServiceImpl")).isTrue(); + assertThat(context.containsBean("stubFooDao")).isTrue(); + assertThat(context.containsBean("myNamedComponent")).isTrue(); + assertThat(context.containsBean("myNamedDao")).isTrue(); + assertThat(context.containsBean("thoreau")).isTrue(); + } + + @Test + public void testWithIndex() { + GenericApplicationContext context = new GenericApplicationContext(); + context.setClassLoader(CandidateComponentsTestClassLoader.index( + ClassPathScanningCandidateComponentProviderTests.class.getClassLoader(), + new ClassPathResource("spring.components", FooServiceImpl.class))); + + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); + int beanCount = scanner.scan(BASE_PACKAGE); + assertThat(beanCount).isEqualTo(12); + + assertThat(context.containsBean("serviceInvocationCounter")).isTrue(); + assertThat(context.containsBean("fooServiceImpl")).isTrue(); + assertThat(context.containsBean("stubFooDao")).isTrue(); + assertThat(context.containsBean("myNamedComponent")).isTrue(); + assertThat(context.containsBean("myNamedDao")).isTrue(); + assertThat(context.containsBean("thoreau")).isTrue(); + } + + @Test + public void testDoubleScanWithIndex() { + GenericApplicationContext context = new GenericApplicationContext(); + context.setClassLoader(CandidateComponentsTestClassLoader.index( + ClassPathScanningCandidateComponentProviderTests.class.getClassLoader(), + new ClassPathResource("spring.components", FooServiceImpl.class))); + + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); + int beanCount = scanner.scan(BASE_PACKAGE); + assertThat(beanCount).isEqualTo(12); + + ClassPathBeanDefinitionScanner scanner2 = new ClassPathBeanDefinitionScanner(context) { + @Override + protected void postProcessBeanDefinition(AbstractBeanDefinition beanDefinition, String beanName) { + super.postProcessBeanDefinition(beanDefinition, beanName); + beanDefinition.setAttribute("someDifference", "someValue"); + } + }; + scanner2.scan(BASE_PACKAGE); + + assertThat(context.containsBean("serviceInvocationCounter")).isTrue(); + assertThat(context.containsBean("fooServiceImpl")).isTrue(); + assertThat(context.containsBean("stubFooDao")).isTrue(); + assertThat(context.containsBean("myNamedComponent")).isTrue(); + assertThat(context.containsBean("myNamedDao")).isTrue(); + assertThat(context.containsBean("thoreau")).isTrue(); + } + + @Test + public void testSimpleScanWithDefaultFiltersAndNoPostProcessors() { + GenericApplicationContext context = new GenericApplicationContext(); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); + scanner.setIncludeAnnotationConfig(false); + int beanCount = scanner.scan(BASE_PACKAGE); + assertThat(beanCount).isEqualTo(7); + + assertThat(context.containsBean("serviceInvocationCounter")).isTrue(); + assertThat(context.containsBean("fooServiceImpl")).isTrue(); + assertThat(context.containsBean("stubFooDao")).isTrue(); + assertThat(context.containsBean("myNamedComponent")).isTrue(); + assertThat(context.containsBean("myNamedDao")).isTrue(); + } + + @Test + public void testSimpleScanWithDefaultFiltersAndOverridingBean() { + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBeanDefinition("stubFooDao", new RootBeanDefinition(TestBean.class)); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); + scanner.setIncludeAnnotationConfig(false); + // should not fail! + scanner.scan(BASE_PACKAGE); + } + + @Test + public void testSimpleScanWithDefaultFiltersAndDefaultBeanNameClash() { + GenericApplicationContext context = new GenericApplicationContext(); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); + scanner.setIncludeAnnotationConfig(false); + scanner.scan("org.springframework.context.annotation3"); + assertThatIllegalStateException().isThrownBy(() -> + scanner.scan(BASE_PACKAGE)) + .withMessageContaining("stubFooDao") + .withMessageContaining(StubFooDao.class.getName()); + } + + @Test + public void testSimpleScanWithDefaultFiltersAndOverriddenEqualNamedBean() { + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBeanDefinition("myNamedDao", new RootBeanDefinition(NamedStubDao.class)); + int initialBeanCount = context.getBeanDefinitionCount(); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); + scanner.setIncludeAnnotationConfig(false); + int scannedBeanCount = scanner.scan(BASE_PACKAGE); + + assertThat(scannedBeanCount).isEqualTo(6); + assertThat(context.getBeanDefinitionCount()).isEqualTo((initialBeanCount + scannedBeanCount)); + assertThat(context.containsBean("serviceInvocationCounter")).isTrue(); + assertThat(context.containsBean("fooServiceImpl")).isTrue(); + assertThat(context.containsBean("stubFooDao")).isTrue(); + assertThat(context.containsBean("myNamedComponent")).isTrue(); + assertThat(context.containsBean("myNamedDao")).isTrue(); + } + + @Test + public void testSimpleScanWithDefaultFiltersAndOverriddenCompatibleNamedBean() { + GenericApplicationContext context = new GenericApplicationContext(); + RootBeanDefinition bd = new RootBeanDefinition(NamedStubDao.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + context.registerBeanDefinition("myNamedDao", bd); + int initialBeanCount = context.getBeanDefinitionCount(); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); + scanner.setIncludeAnnotationConfig(false); + int scannedBeanCount = scanner.scan(BASE_PACKAGE); + + assertThat(scannedBeanCount).isEqualTo(6); + assertThat(context.getBeanDefinitionCount()).isEqualTo((initialBeanCount + scannedBeanCount)); + assertThat(context.containsBean("serviceInvocationCounter")).isTrue(); + assertThat(context.containsBean("fooServiceImpl")).isTrue(); + assertThat(context.containsBean("stubFooDao")).isTrue(); + assertThat(context.containsBean("myNamedComponent")).isTrue(); + assertThat(context.containsBean("myNamedDao")).isTrue(); + } + + @Test + public void testSimpleScanWithDefaultFiltersAndSameBeanTwice() { + GenericApplicationContext context = new GenericApplicationContext(); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); + scanner.setIncludeAnnotationConfig(false); + // should not fail! + scanner.scan(BASE_PACKAGE); + scanner.scan(BASE_PACKAGE); + } + + @Test + public void testSimpleScanWithDefaultFiltersAndSpecifiedBeanNameClash() { + GenericApplicationContext context = new GenericApplicationContext(); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); + scanner.setIncludeAnnotationConfig(false); + scanner.scan("org.springframework.context.annotation2"); + assertThatIllegalStateException().isThrownBy(() -> + scanner.scan(BASE_PACKAGE)) + .withMessageContaining("myNamedDao") + .withMessageContaining(NamedStubDao.class.getName()) + .withMessageContaining(NamedStubDao2.class.getName()); + } + + @Test + public void testCustomIncludeFilterWithoutDefaultsButIncludingPostProcessors() { + GenericApplicationContext context = new GenericApplicationContext(); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, false); + scanner.addIncludeFilter(new AnnotationTypeFilter(CustomComponent.class)); + int beanCount = scanner.scan(BASE_PACKAGE); + + assertThat(beanCount).isEqualTo(6); + assertThat(context.containsBean("messageBean")).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_PROCESSOR_BEAN_NAME)).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_FACTORY_BEAN_NAME)).isTrue(); + } + + @Test + public void testCustomIncludeFilterWithoutDefaultsAndNoPostProcessors() { + GenericApplicationContext context = new GenericApplicationContext(); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, false); + scanner.addIncludeFilter(new AnnotationTypeFilter(CustomComponent.class)); + int beanCount = scanner.scan(BASE_PACKAGE); + + assertThat(beanCount).isEqualTo(6); + assertThat(context.containsBean("messageBean")).isTrue(); + assertThat(context.containsBean("serviceInvocationCounter")).isFalse(); + assertThat(context.containsBean("fooServiceImpl")).isFalse(); + assertThat(context.containsBean("stubFooDao")).isFalse(); + assertThat(context.containsBean("myNamedComponent")).isFalse(); + assertThat(context.containsBean("myNamedDao")).isFalse(); + assertThat(context.containsBean(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_PROCESSOR_BEAN_NAME)).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_FACTORY_BEAN_NAME)).isTrue(); + } + + @Test + public void testCustomIncludeFilterAndDefaults() { + GenericApplicationContext context = new GenericApplicationContext(); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, true); + scanner.addIncludeFilter(new AnnotationTypeFilter(CustomComponent.class)); + int beanCount = scanner.scan(BASE_PACKAGE); + + assertThat(beanCount).isEqualTo(13); + assertThat(context.containsBean("messageBean")).isTrue(); + assertThat(context.containsBean("serviceInvocationCounter")).isTrue(); + assertThat(context.containsBean("fooServiceImpl")).isTrue(); + assertThat(context.containsBean("stubFooDao")).isTrue(); + assertThat(context.containsBean("myNamedComponent")).isTrue(); + assertThat(context.containsBean("myNamedDao")).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_PROCESSOR_BEAN_NAME)).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_FACTORY_BEAN_NAME)).isTrue(); + } + + @Test + public void testCustomAnnotationExcludeFilterAndDefaults() { + GenericApplicationContext context = new GenericApplicationContext(); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, true); + scanner.addExcludeFilter(new AnnotationTypeFilter(Aspect.class)); + int beanCount = scanner.scan(BASE_PACKAGE); + + assertThat(beanCount).isEqualTo(11); + assertThat(context.containsBean("serviceInvocationCounter")).isFalse(); + assertThat(context.containsBean("fooServiceImpl")).isTrue(); + assertThat(context.containsBean("stubFooDao")).isTrue(); + assertThat(context.containsBean("myNamedComponent")).isTrue(); + assertThat(context.containsBean("myNamedDao")).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_PROCESSOR_BEAN_NAME)).isTrue(); + } + + @Test + public void testCustomAssignableTypeExcludeFilterAndDefaults() { + GenericApplicationContext context = new GenericApplicationContext(); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, true); + scanner.addExcludeFilter(new AssignableTypeFilter(FooService.class)); + int beanCount = scanner.scan(BASE_PACKAGE); + + assertThat(beanCount).isEqualTo(11); + assertThat(context.containsBean("fooServiceImpl")).isFalse(); + assertThat(context.containsBean("serviceInvocationCounter")).isTrue(); + assertThat(context.containsBean("stubFooDao")).isTrue(); + assertThat(context.containsBean("myNamedComponent")).isTrue(); + assertThat(context.containsBean("myNamedDao")).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_PROCESSOR_BEAN_NAME)).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_FACTORY_BEAN_NAME)).isTrue(); + } + + @Test + public void testCustomAssignableTypeExcludeFilterAndDefaultsWithoutPostProcessors() { + GenericApplicationContext context = new GenericApplicationContext(); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, true); + scanner.setIncludeAnnotationConfig(false); + scanner.addExcludeFilter(new AssignableTypeFilter(FooService.class)); + int beanCount = scanner.scan(BASE_PACKAGE); + + assertThat(beanCount).isEqualTo(6); + assertThat(context.containsBean("fooServiceImpl")).isFalse(); + assertThat(context.containsBean("serviceInvocationCounter")).isTrue(); + assertThat(context.containsBean("stubFooDao")).isTrue(); + assertThat(context.containsBean("myNamedComponent")).isTrue(); + assertThat(context.containsBean("myNamedDao")).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)).isFalse(); + assertThat(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)).isFalse(); + } + + @Test + public void testMultipleCustomExcludeFiltersAndDefaults() { + GenericApplicationContext context = new GenericApplicationContext(); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, true); + scanner.addExcludeFilter(new AssignableTypeFilter(FooService.class)); + scanner.addExcludeFilter(new AnnotationTypeFilter(Aspect.class)); + int beanCount = scanner.scan(BASE_PACKAGE); + + assertThat(beanCount).isEqualTo(10); + assertThat(context.containsBean("fooServiceImpl")).isFalse(); + assertThat(context.containsBean("serviceInvocationCounter")).isFalse(); + assertThat(context.containsBean("stubFooDao")).isTrue(); + assertThat(context.containsBean("myNamedComponent")).isTrue(); + assertThat(context.containsBean("myNamedDao")).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_PROCESSOR_BEAN_NAME)).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_FACTORY_BEAN_NAME)).isTrue(); + } + + @Test + public void testCustomBeanNameGenerator() { + GenericApplicationContext context = new GenericApplicationContext(); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); + scanner.setBeanNameGenerator(new TestBeanNameGenerator()); + int beanCount = scanner.scan(BASE_PACKAGE); + + assertThat(beanCount).isEqualTo(12); + assertThat(context.containsBean("fooServiceImpl")).isFalse(); + assertThat(context.containsBean("fooService")).isTrue(); + assertThat(context.containsBean("serviceInvocationCounter")).isTrue(); + assertThat(context.containsBean("stubFooDao")).isTrue(); + assertThat(context.containsBean("myNamedComponent")).isTrue(); + assertThat(context.containsBean("myNamedDao")).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_PROCESSOR_BEAN_NAME)).isTrue(); + assertThat(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_FACTORY_BEAN_NAME)).isTrue(); + } + + @Test + public void testMultipleBasePackagesWithDefaultsOnly() { + GenericApplicationContext singlePackageContext = new GenericApplicationContext(); + ClassPathBeanDefinitionScanner singlePackageScanner = new ClassPathBeanDefinitionScanner(singlePackageContext); + GenericApplicationContext multiPackageContext = new GenericApplicationContext(); + ClassPathBeanDefinitionScanner multiPackageScanner = new ClassPathBeanDefinitionScanner(multiPackageContext); + int singlePackageBeanCount = singlePackageScanner.scan(BASE_PACKAGE); + assertThat(singlePackageBeanCount).isEqualTo(12); + multiPackageScanner.scan(BASE_PACKAGE, "org.springframework.dao.annotation"); + // assertTrue(multiPackageBeanCount > singlePackageBeanCount); + } + + @Test + public void testMultipleScanCalls() { + GenericApplicationContext context = new GenericApplicationContext(); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); + int initialBeanCount = context.getBeanDefinitionCount(); + int scannedBeanCount = scanner.scan(BASE_PACKAGE); + assertThat(scannedBeanCount).isEqualTo(12); + assertThat((context.getBeanDefinitionCount() - initialBeanCount)).isEqualTo(scannedBeanCount); + int addedBeanCount = scanner.scan("org.springframework.aop.aspectj.annotation"); + assertThat(context.getBeanDefinitionCount()).isEqualTo((initialBeanCount + scannedBeanCount + addedBeanCount)); + } + + @Test + public void testBeanAutowiredWithAnnotationConfigEnabled() { + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBeanDefinition("myBf", new RootBeanDefinition(StaticListableBeanFactory.class)); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); + scanner.setBeanNameGenerator(new TestBeanNameGenerator()); + int beanCount = scanner.scan(BASE_PACKAGE); + assertThat(beanCount).isEqualTo(12); + context.refresh(); + + FooServiceImpl fooService = context.getBean("fooService", FooServiceImpl.class); + StaticListableBeanFactory myBf = (StaticListableBeanFactory) context.getBean("myBf"); + MessageSource ms = (MessageSource) context.getBean("messageSource"); + assertThat(fooService.isInitCalled()).isTrue(); + assertThat(fooService.foo(123)).isEqualTo("bar"); + assertThat(fooService.lookupFoo(123)).isEqualTo("bar"); + assertThat(fooService.beanFactory).isSameAs(context.getDefaultListableBeanFactory()); + assertThat(fooService.listableBeanFactory.size()).isEqualTo(2); + assertThat(fooService.listableBeanFactory.get(0)).isSameAs(context.getDefaultListableBeanFactory()); + assertThat(fooService.listableBeanFactory.get(1)).isSameAs(myBf); + assertThat(fooService.resourceLoader).isSameAs(context); + assertThat(fooService.resourcePatternResolver).isSameAs(context); + assertThat(fooService.eventPublisher).isSameAs(context); + assertThat(fooService.messageSource).isSameAs(ms); + assertThat(fooService.context).isSameAs(context); + assertThat(fooService.configurableContext.length).isEqualTo(1); + assertThat(fooService.configurableContext[0]).isSameAs(context); + assertThat(fooService.genericContext).isSameAs(context); + } + + @Test + public void testBeanNotAutowiredWithAnnotationConfigDisabled() { + GenericApplicationContext context = new GenericApplicationContext(); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); + scanner.setIncludeAnnotationConfig(false); + scanner.setBeanNameGenerator(new TestBeanNameGenerator()); + int beanCount = scanner.scan(BASE_PACKAGE); + assertThat(beanCount).isEqualTo(7); + context.refresh(); + + try { + context.getBean("fooService"); + } + catch (BeanCreationException expected) { + assertThat(expected.contains(BeanInstantiationException.class)).isTrue(); + // @Lookup method not substituted + } + } + + @Test + public void testAutowireCandidatePatternMatches() { + GenericApplicationContext context = new GenericApplicationContext(); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); + scanner.setIncludeAnnotationConfig(true); + scanner.setBeanNameGenerator(new TestBeanNameGenerator()); + scanner.setAutowireCandidatePatterns("*FooDao"); + scanner.scan(BASE_PACKAGE); + context.refresh(); + + FooServiceImpl fooService = (FooServiceImpl) context.getBean("fooService"); + assertThat(fooService.foo(123)).isEqualTo("bar"); + assertThat(fooService.lookupFoo(123)).isEqualTo("bar"); + } + + @Test + public void testAutowireCandidatePatternDoesNotMatch() { + GenericApplicationContext context = new GenericApplicationContext(); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); + scanner.setIncludeAnnotationConfig(true); + scanner.setBeanNameGenerator(new TestBeanNameGenerator()); + scanner.setAutowireCandidatePatterns("*NoSuchDao"); + scanner.scan(BASE_PACKAGE); + context.refresh(); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + context.getBean("fooService")) + .satisfies(ex -> assertThat(ex.getMostSpecificCause()).isInstanceOf(NoSuchBeanDefinitionException.class)); + } + + + private static class TestBeanNameGenerator extends AnnotationBeanNameGenerator { + + @Override + public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) { + String beanName = super.generateBeanName(definition, registry); + return beanName.replace("Impl", ""); + } + } + + + @Component("toBeIgnored") + public class NonStaticInnerClass { + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathFactoryBeanDefinitionScannerTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathFactoryBeanDefinitionScannerTests.java new file mode 100644 index 0000000..0949837 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathFactoryBeanDefinitionScannerTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.scope.ScopedObject; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.annotation4.DependencyBean; +import org.springframework.context.annotation4.FactoryMethodComponent; +import org.springframework.context.support.AbstractApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.context.testfixture.SimpleMapScope; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Mark Pollack + * @author Juergen Hoeller + */ +public class ClassPathFactoryBeanDefinitionScannerTests { + + private static final String BASE_PACKAGE = FactoryMethodComponent.class.getPackage().getName(); + + + @Test + public void testSingletonScopedFactoryMethod() { + GenericApplicationContext context = new GenericApplicationContext(); + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); + + context.getBeanFactory().registerScope("request", new SimpleMapScope()); + + scanner.scan(BASE_PACKAGE); + context.registerBeanDefinition("clientBean", new RootBeanDefinition(QualifiedClientBean.class)); + context.refresh(); + + FactoryMethodComponent fmc = context.getBean("factoryMethodComponent", FactoryMethodComponent.class); + assertThat(fmc.getClass().getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR)).isFalse(); + + TestBean tb = (TestBean) context.getBean("publicInstance"); //2 + assertThat(tb.getName()).isEqualTo("publicInstance"); + TestBean tb2 = (TestBean) context.getBean("publicInstance"); //2 + assertThat(tb2.getName()).isEqualTo("publicInstance"); + assertThat(tb).isSameAs(tb2); + + tb = (TestBean) context.getBean("protectedInstance"); //3 + assertThat(tb.getName()).isEqualTo("protectedInstance"); + assertThat(context.getBean("protectedInstance")).isSameAs(tb); + assertThat(tb.getCountry()).isEqualTo("0"); + tb2 = context.getBean("protectedInstance", TestBean.class); //3 + assertThat(tb2.getName()).isEqualTo("protectedInstance"); + assertThat(tb).isSameAs(tb2); + + tb = context.getBean("privateInstance", TestBean.class); //4 + assertThat(tb.getName()).isEqualTo("privateInstance"); + assertThat(tb.getAge()).isEqualTo(1); + tb2 = context.getBean("privateInstance", TestBean.class); //4 + assertThat(tb2.getAge()).isEqualTo(2); + assertThat(tb).isNotSameAs(tb2); + + Object bean = context.getBean("requestScopedInstance"); //5 + assertThat(AopUtils.isCglibProxy(bean)).isTrue(); + boolean condition = bean instanceof ScopedObject; + assertThat(condition).isTrue(); + + QualifiedClientBean clientBean = context.getBean("clientBean", QualifiedClientBean.class); + assertThat(clientBean.testBean).isSameAs(context.getBean("publicInstance")); + assertThat(clientBean.dependencyBean).isSameAs(context.getBean("dependencyBean")); + assertThat(clientBean.applicationContext).isSameAs(context); + } + + + public static class QualifiedClientBean { + + @Autowired @Qualifier("public") + public TestBean testBean; + + @Autowired + public DependencyBean dependencyBean; + + @Autowired + AbstractApplicationContext applicationContext; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java new file mode 100644 index 0000000..85eca58 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java @@ -0,0 +1,550 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Set; +import java.util.regex.Pattern; + +import example.gh24375.AnnotatedComponent; +import example.profilescan.DevComponent; +import example.profilescan.ProfileAnnotatedComponent; +import example.profilescan.ProfileMetaAnnotatedComponent; +import example.scannable.AutowiredQualifierFooService; +import example.scannable.CustomStereotype; +import example.scannable.DefaultNamedComponent; +import example.scannable.FooDao; +import example.scannable.FooService; +import example.scannable.FooServiceImpl; +import example.scannable.MessageBean; +import example.scannable.NamedComponent; +import example.scannable.NamedStubDao; +import example.scannable.ScopedProxyTestBean; +import example.scannable.ServiceInvocationCounter; +import example.scannable.StubFooDao; +import example.scannable.sub.BarComponent; +import org.aspectj.lang.annotation.Aspect; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.testfixture.index.CandidateComponentsTestClassLoader; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.core.type.filter.AssignableTypeFilter; +import org.springframework.core.type.filter.RegexPatternTypeFilter; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Controller; +import org.springframework.stereotype.Repository; +import org.springframework.stereotype.Service; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Mark Fisher + * @author Juergen Hoeller + * @author Chris Beams + * @author Stephane Nicoll + */ +public class ClassPathScanningCandidateComponentProviderTests { + + private static final String TEST_BASE_PACKAGE = "example.scannable"; + private static final String TEST_PROFILE_PACKAGE = "example.profilescan"; + private static final String TEST_DEFAULT_PROFILE_NAME = "testDefault"; + + private static final ClassLoader TEST_BASE_CLASSLOADER = CandidateComponentsTestClassLoader.index( + ClassPathScanningCandidateComponentProviderTests.class.getClassLoader(), + new ClassPathResource("spring.components", NamedComponent.class)); + + + @Test + public void defaultsWithScan() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); + provider.setResourceLoader(new DefaultResourceLoader( + CandidateComponentsTestClassLoader.disableIndex(getClass().getClassLoader()))); + testDefault(provider); + } + + @Test + public void defaultsWithIndex() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); + provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); + testDefault(provider); + } + + private void testDefault(ClassPathScanningCandidateComponentProvider provider) { + Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); + assertThat(containsBeanClass(candidates, DefaultNamedComponent.class)).isTrue(); + assertThat(containsBeanClass(candidates, NamedComponent.class)).isTrue(); + assertThat(containsBeanClass(candidates, FooServiceImpl.class)).isTrue(); + assertThat(containsBeanClass(candidates, StubFooDao.class)).isTrue(); + assertThat(containsBeanClass(candidates, NamedStubDao.class)).isTrue(); + assertThat(containsBeanClass(candidates, ServiceInvocationCounter.class)).isTrue(); + assertThat(containsBeanClass(candidates, BarComponent.class)).isTrue(); + assertThat(candidates.size()).isEqualTo(7); + assertBeanDefinitionType(candidates); + } + + @Test + public void antStylePackageWithScan() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); + provider.setResourceLoader(new DefaultResourceLoader( + CandidateComponentsTestClassLoader.disableIndex(getClass().getClassLoader()))); + testAntStyle(provider); + } + + @Test + public void antStylePackageWithIndex() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); + provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); + testAntStyle(provider); + } + + private void testAntStyle(ClassPathScanningCandidateComponentProvider provider) { + Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE + ".**.sub"); + assertThat(containsBeanClass(candidates, BarComponent.class)).isTrue(); + assertThat(candidates.size()).isEqualTo(1); + assertBeanDefinitionType(candidates); + } + + @Test + public void bogusPackageWithScan() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); + provider.setResourceLoader(new DefaultResourceLoader( + CandidateComponentsTestClassLoader.disableIndex(getClass().getClassLoader()))); + Set candidates = provider.findCandidateComponents("bogus"); + assertThat(candidates.size()).isEqualTo(0); + } + + @Test + public void bogusPackageWithIndex() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); + provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); + Set candidates = provider.findCandidateComponents("bogus"); + assertThat(candidates.size()).isEqualTo(0); + } + + @Test + public void customFiltersFollowedByResetUseIndex() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); + provider.addIncludeFilter(new AnnotationTypeFilter(Component.class)); + provider.resetFilters(true); + Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); + assertBeanDefinitionType(candidates); + } + + @Test + public void customAnnotationTypeIncludeFilterWithScan() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.setResourceLoader(new DefaultResourceLoader( + CandidateComponentsTestClassLoader.disableIndex(getClass().getClassLoader()))); + testCustomAnnotationTypeIncludeFilter(provider); + } + + @Test + public void customAnnotationTypeIncludeFilterWithIndex() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); + testCustomAnnotationTypeIncludeFilter(provider); + } + + private void testCustomAnnotationTypeIncludeFilter(ClassPathScanningCandidateComponentProvider provider) { + provider.addIncludeFilter(new AnnotationTypeFilter(Component.class)); + testDefault(provider); + } + + @Test + public void customAssignableTypeIncludeFilterWithScan() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.setResourceLoader(new DefaultResourceLoader( + CandidateComponentsTestClassLoader.disableIndex(getClass().getClassLoader()))); + testCustomAssignableTypeIncludeFilter(provider); + } + + @Test + public void customAssignableTypeIncludeFilterWithIndex() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); + testCustomAssignableTypeIncludeFilter(provider); + } + + private void testCustomAssignableTypeIncludeFilter(ClassPathScanningCandidateComponentProvider provider) { + provider.addIncludeFilter(new AssignableTypeFilter(FooService.class)); + Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); + // Interfaces/Abstract class are filtered out automatically. + assertThat(containsBeanClass(candidates, AutowiredQualifierFooService.class)).isTrue(); + assertThat(containsBeanClass(candidates, FooServiceImpl.class)).isTrue(); + assertThat(containsBeanClass(candidates, ScopedProxyTestBean.class)).isTrue(); + assertThat(candidates.size()).isEqualTo(3); + assertBeanDefinitionType(candidates); + } + + @Test + public void customSupportedIncludeAndExcludedFilterWithScan() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.setResourceLoader(new DefaultResourceLoader( + CandidateComponentsTestClassLoader.disableIndex(getClass().getClassLoader()))); + testCustomSupportedIncludeAndExcludeFilter(provider); + } + + @Test + public void customSupportedIncludeAndExcludeFilterWithIndex() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); + testCustomSupportedIncludeAndExcludeFilter(provider); + } + + private void testCustomSupportedIncludeAndExcludeFilter(ClassPathScanningCandidateComponentProvider provider) { + provider.addIncludeFilter(new AnnotationTypeFilter(Component.class)); + provider.addExcludeFilter(new AnnotationTypeFilter(Service.class)); + provider.addExcludeFilter(new AnnotationTypeFilter(Repository.class)); + Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); + assertThat(containsBeanClass(candidates, NamedComponent.class)).isTrue(); + assertThat(containsBeanClass(candidates, ServiceInvocationCounter.class)).isTrue(); + assertThat(containsBeanClass(candidates, BarComponent.class)).isTrue(); + assertThat(candidates.size()).isEqualTo(3); + assertBeanDefinitionType(candidates); + } + + @Test + public void customSupportIncludeFilterWithNonIndexedTypeUseScan() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); + // This annotation type is not directly annotated with Indexed so we can use + // the index to find candidates + provider.addIncludeFilter(new AnnotationTypeFilter(CustomStereotype.class)); + Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); + assertThat(containsBeanClass(candidates, DefaultNamedComponent.class)).isTrue(); + assertThat(candidates.size()).isEqualTo(1); + assertBeanDefinitionType(candidates); + } + + @Test + public void customNotSupportedIncludeFilterUseScan() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); + provider.addIncludeFilter(new AssignableTypeFilter(FooDao.class)); + Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); + assertThat(containsBeanClass(candidates, StubFooDao.class)).isTrue(); + assertThat(candidates.size()).isEqualTo(1); + assertBeanDefinitionType(candidates); + } + + @Test + public void excludeFilterWithScan() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); + provider.setResourceLoader(new DefaultResourceLoader( + CandidateComponentsTestClassLoader.disableIndex(getClass().getClassLoader()))); + provider.addExcludeFilter(new RegexPatternTypeFilter(Pattern.compile(TEST_BASE_PACKAGE + ".*Named.*"))); + testExclude(provider); + } + + @Test + public void excludeFilterWithIndex() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); + provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); + provider.addExcludeFilter(new RegexPatternTypeFilter(Pattern.compile(TEST_BASE_PACKAGE + ".*Named.*"))); + testExclude(provider); + } + + private void testExclude(ClassPathScanningCandidateComponentProvider provider) { + Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); + assertThat(containsBeanClass(candidates, FooServiceImpl.class)).isTrue(); + assertThat(containsBeanClass(candidates, StubFooDao.class)).isTrue(); + assertThat(containsBeanClass(candidates, ServiceInvocationCounter.class)).isTrue(); + assertThat(containsBeanClass(candidates, BarComponent.class)).isTrue(); + assertThat(candidates.size()).isEqualTo(4); + assertBeanDefinitionType(candidates); + } + + @Test + public void testWithNoFilters() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); + assertThat(candidates.size()).isEqualTo(0); + } + + @Test + public void testWithComponentAnnotationOnly() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.addIncludeFilter(new AnnotationTypeFilter(Component.class)); + provider.addExcludeFilter(new AnnotationTypeFilter(Repository.class)); + provider.addExcludeFilter(new AnnotationTypeFilter(Service.class)); + provider.addExcludeFilter(new AnnotationTypeFilter(Controller.class)); + Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); + assertThat(candidates.size()).isEqualTo(3); + assertThat(containsBeanClass(candidates, NamedComponent.class)).isTrue(); + assertThat(containsBeanClass(candidates, ServiceInvocationCounter.class)).isTrue(); + assertThat(containsBeanClass(candidates, BarComponent.class)).isTrue(); + assertThat(containsBeanClass(candidates, FooServiceImpl.class)).isFalse(); + assertThat(containsBeanClass(candidates, StubFooDao.class)).isFalse(); + assertThat(containsBeanClass(candidates, NamedStubDao.class)).isFalse(); + } + + @Test + public void testWithAspectAnnotationOnly() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.addIncludeFilter(new AnnotationTypeFilter(Aspect.class)); + Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); + assertThat(candidates.size()).isEqualTo(1); + assertThat(containsBeanClass(candidates, ServiceInvocationCounter.class)).isTrue(); + } + + @Test + public void testWithInterfaceType() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.addIncludeFilter(new AssignableTypeFilter(FooDao.class)); + Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); + assertThat(candidates.size()).isEqualTo(1); + assertThat(containsBeanClass(candidates, StubFooDao.class)).isTrue(); + } + + @Test + public void testWithClassType() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.addIncludeFilter(new AssignableTypeFilter(MessageBean.class)); + Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); + assertThat(candidates.size()).isEqualTo(1); + assertThat(containsBeanClass(candidates, MessageBean.class)).isTrue(); + } + + @Test + public void testWithMultipleMatchingFilters() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.addIncludeFilter(new AnnotationTypeFilter(Component.class)); + provider.addIncludeFilter(new AssignableTypeFilter(FooServiceImpl.class)); + Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); + assertThat(candidates.size()).isEqualTo(7); + assertThat(containsBeanClass(candidates, NamedComponent.class)).isTrue(); + assertThat(containsBeanClass(candidates, ServiceInvocationCounter.class)).isTrue(); + assertThat(containsBeanClass(candidates, FooServiceImpl.class)).isTrue(); + assertThat(containsBeanClass(candidates, BarComponent.class)).isTrue(); + } + + @Test + public void testExcludeTakesPrecedence() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.addIncludeFilter(new AnnotationTypeFilter(Component.class)); + provider.addIncludeFilter(new AssignableTypeFilter(FooServiceImpl.class)); + provider.addExcludeFilter(new AssignableTypeFilter(FooService.class)); + Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); + assertThat(candidates.size()).isEqualTo(6); + assertThat(containsBeanClass(candidates, NamedComponent.class)).isTrue(); + assertThat(containsBeanClass(candidates, ServiceInvocationCounter.class)).isTrue(); + assertThat(containsBeanClass(candidates, BarComponent.class)).isTrue(); + assertThat(containsBeanClass(candidates, FooServiceImpl.class)).isFalse(); + } + + @Test + public void testWithNullEnvironment() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); + Set candidates = provider.findCandidateComponents(TEST_PROFILE_PACKAGE); + assertThat(containsBeanClass(candidates, ProfileAnnotatedComponent.class)).isFalse(); + } + + @Test + public void testWithInactiveProfile() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); + ConfigurableEnvironment env = new StandardEnvironment(); + env.setActiveProfiles("other"); + provider.setEnvironment(env); + Set candidates = provider.findCandidateComponents(TEST_PROFILE_PACKAGE); + assertThat(containsBeanClass(candidates, ProfileAnnotatedComponent.class)).isFalse(); + } + + @Test + public void testWithActiveProfile() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); + ConfigurableEnvironment env = new StandardEnvironment(); + env.setActiveProfiles(ProfileAnnotatedComponent.PROFILE_NAME); + provider.setEnvironment(env); + Set candidates = provider.findCandidateComponents(TEST_PROFILE_PACKAGE); + assertThat(containsBeanClass(candidates, ProfileAnnotatedComponent.class)).isTrue(); + } + + @Test + public void testIntegrationWithAnnotationConfigApplicationContext_noProfile() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ProfileAnnotatedComponent.class); + ctx.refresh(); + assertThat(ctx.containsBean(ProfileAnnotatedComponent.BEAN_NAME)).isFalse(); + } + + @Test + public void testIntegrationWithAnnotationConfigApplicationContext_validProfile() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.getEnvironment().setActiveProfiles(ProfileAnnotatedComponent.PROFILE_NAME); + ctx.register(ProfileAnnotatedComponent.class); + ctx.refresh(); + assertThat(ctx.containsBean(ProfileAnnotatedComponent.BEAN_NAME)).isTrue(); + } + + @Test + public void testIntegrationWithAnnotationConfigApplicationContext_validMetaAnnotatedProfile() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.getEnvironment().setActiveProfiles(DevComponent.PROFILE_NAME); + ctx.register(ProfileMetaAnnotatedComponent.class); + ctx.refresh(); + assertThat(ctx.containsBean(ProfileMetaAnnotatedComponent.BEAN_NAME)).isTrue(); + } + + @Test + public void testIntegrationWithAnnotationConfigApplicationContext_invalidProfile() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.getEnvironment().setActiveProfiles("other"); + ctx.register(ProfileAnnotatedComponent.class); + ctx.refresh(); + assertThat(ctx.containsBean(ProfileAnnotatedComponent.BEAN_NAME)).isFalse(); + } + + @Test + public void testIntegrationWithAnnotationConfigApplicationContext_invalidMetaAnnotatedProfile() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.getEnvironment().setActiveProfiles("other"); + ctx.register(ProfileMetaAnnotatedComponent.class); + ctx.refresh(); + assertThat(ctx.containsBean(ProfileMetaAnnotatedComponent.BEAN_NAME)).isFalse(); + } + + @Test + public void testIntegrationWithAnnotationConfigApplicationContext_defaultProfile() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.getEnvironment().setDefaultProfiles(TEST_DEFAULT_PROFILE_NAME); + // no active profiles are set + ctx.register(DefaultProfileAnnotatedComponent.class); + ctx.refresh(); + assertThat(ctx.containsBean(DefaultProfileAnnotatedComponent.BEAN_NAME)).isTrue(); + } + + @Test + public void testIntegrationWithAnnotationConfigApplicationContext_defaultAndDevProfile() { + Class beanClass = DefaultAndDevProfileAnnotatedComponent.class; + String beanName = DefaultAndDevProfileAnnotatedComponent.BEAN_NAME; + { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.getEnvironment().setDefaultProfiles(TEST_DEFAULT_PROFILE_NAME); + // no active profiles are set + ctx.register(beanClass); + ctx.refresh(); + assertThat(ctx.containsBean(beanName)).isTrue(); + } + { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.getEnvironment().setDefaultProfiles(TEST_DEFAULT_PROFILE_NAME); + ctx.getEnvironment().setActiveProfiles("dev"); + ctx.register(beanClass); + ctx.refresh(); + assertThat(ctx.containsBean(beanName)).isTrue(); + } + { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.getEnvironment().setDefaultProfiles(TEST_DEFAULT_PROFILE_NAME); + ctx.getEnvironment().setActiveProfiles("other"); + ctx.register(beanClass); + ctx.refresh(); + assertThat(ctx.containsBean(beanName)).isFalse(); + } + } + + @Test + public void testIntegrationWithAnnotationConfigApplicationContext_metaProfile() { + Class beanClass = MetaProfileAnnotatedComponent.class; + String beanName = MetaProfileAnnotatedComponent.BEAN_NAME; + { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.getEnvironment().setDefaultProfiles(TEST_DEFAULT_PROFILE_NAME); + // no active profiles are set + ctx.register(beanClass); + ctx.refresh(); + assertThat(ctx.containsBean(beanName)).isTrue(); + } + { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.getEnvironment().setDefaultProfiles(TEST_DEFAULT_PROFILE_NAME); + ctx.getEnvironment().setActiveProfiles("dev"); + ctx.register(beanClass); + ctx.refresh(); + assertThat(ctx.containsBean(beanName)).isTrue(); + } + { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.getEnvironment().setDefaultProfiles(TEST_DEFAULT_PROFILE_NAME); + ctx.getEnvironment().setActiveProfiles("other"); + ctx.register(beanClass); + ctx.refresh(); + assertThat(ctx.containsBean(beanName)).isFalse(); + } + } + + @Test + public void componentScanningFindsComponentsAnnotatedWithAnnotationsContainingNestedAnnotations() { + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); + Set components = provider.findCandidateComponents(AnnotatedComponent.class.getPackage().getName()); + assertThat(components).hasSize(1); + assertThat(components.iterator().next().getBeanClassName()).isEqualTo(AnnotatedComponent.class.getName()); + } + + + private boolean containsBeanClass(Set candidates, Class beanClass) { + for (BeanDefinition candidate : candidates) { + if (beanClass.getName().equals(candidate.getBeanClassName())) { + return true; + } + } + return false; + } + + private void assertBeanDefinitionType(Set candidates) { + candidates.forEach(c -> + assertThat(c).isInstanceOf(ScannedGenericBeanDefinition.class) + ); + } + + + @Profile(TEST_DEFAULT_PROFILE_NAME) + @Component(DefaultProfileAnnotatedComponent.BEAN_NAME) + private static class DefaultProfileAnnotatedComponent { + static final String BEAN_NAME = "defaultProfileAnnotatedComponent"; + } + + @Profile({TEST_DEFAULT_PROFILE_NAME, "dev"}) + @Component(DefaultAndDevProfileAnnotatedComponent.BEAN_NAME) + private static class DefaultAndDevProfileAnnotatedComponent { + static final String BEAN_NAME = "defaultAndDevProfileAnnotatedComponent"; + } + + @DefaultProfile @DevProfile + @Component(MetaProfileAnnotatedComponent.BEAN_NAME) + private static class MetaProfileAnnotatedComponent { + static final String BEAN_NAME = "metaProfileAnnotatedComponent"; + } + + @Profile(TEST_DEFAULT_PROFILE_NAME) + @Retention(RetentionPolicy.RUNTIME) + public @interface DefaultProfile { + } + + @Profile("dev") + @Retention(RetentionPolicy.RUNTIME) + public @interface DevProfile { + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessorTests.java new file mode 100644 index 0000000..3d300b7 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessorTests.java @@ -0,0 +1,873 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.util.Properties; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.annotation.Resource; +import javax.ejb.EJB; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.INestedTestBean; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.NestedTestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.context.testfixture.jndi.ExpectedLookupTemplate; +import org.springframework.core.testfixture.io.SerializationTestUtils; +import org.springframework.jndi.support.SimpleJndiBeanFactory; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @author Chris Beams + */ +public class CommonAnnotationBeanPostProcessorTests { + + @Test + public void testPostConstructAndPreDestroy() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.addBeanPostProcessor(new CommonAnnotationBeanPostProcessor()); + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(AnnotatedInitDestroyBean.class)); + + AnnotatedInitDestroyBean bean = (AnnotatedInitDestroyBean) bf.getBean("annotatedBean"); + assertThat(bean.initCalled).isTrue(); + bf.destroySingletons(); + assertThat(bean.destroyCalled).isTrue(); + } + + @Test + public void testPostConstructAndPreDestroyWithPostProcessor() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.addBeanPostProcessor(new InitDestroyBeanPostProcessor()); + bf.addBeanPostProcessor(new CommonAnnotationBeanPostProcessor()); + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(AnnotatedInitDestroyBean.class)); + + AnnotatedInitDestroyBean bean = (AnnotatedInitDestroyBean) bf.getBean("annotatedBean"); + assertThat(bean.initCalled).isTrue(); + bf.destroySingletons(); + assertThat(bean.destroyCalled).isTrue(); + } + + @Test + public void testPostConstructAndPreDestroyWithApplicationContextAndPostProcessor() { + GenericApplicationContext ctx = new GenericApplicationContext(); + ctx.registerBeanDefinition("bpp1", new RootBeanDefinition(InitDestroyBeanPostProcessor.class)); + ctx.registerBeanDefinition("bpp2", new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class)); + ctx.registerBeanDefinition("annotatedBean", new RootBeanDefinition(AnnotatedInitDestroyBean.class)); + ctx.refresh(); + + AnnotatedInitDestroyBean bean = (AnnotatedInitDestroyBean) ctx.getBean("annotatedBean"); + assertThat(bean.initCalled).isTrue(); + ctx.close(); + assertThat(bean.destroyCalled).isTrue(); + } + + @Test + public void testPostConstructAndPreDestroyWithManualConfiguration() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + InitDestroyAnnotationBeanPostProcessor bpp = new InitDestroyAnnotationBeanPostProcessor(); + bpp.setInitAnnotationType(PostConstruct.class); + bpp.setDestroyAnnotationType(PreDestroy.class); + bf.addBeanPostProcessor(bpp); + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(AnnotatedInitDestroyBean.class)); + + AnnotatedInitDestroyBean bean = (AnnotatedInitDestroyBean) bf.getBean("annotatedBean"); + assertThat(bean.initCalled).isTrue(); + bf.destroySingletons(); + assertThat(bean.destroyCalled).isTrue(); + } + + @Test + public void testPostProcessorWithNullBean() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.addBeanPostProcessor(new CommonAnnotationBeanPostProcessor()); + RootBeanDefinition rbd = new RootBeanDefinition(NullFactory.class); + rbd.setFactoryMethodName("create"); + bf.registerBeanDefinition("bean", rbd); + + assertThat(bf.getBean("bean").toString()).isEqualTo("null"); + bf.destroySingletons(); + } + + @Test + public void testSerialization() throws Exception { + CommonAnnotationBeanPostProcessor bpp = new CommonAnnotationBeanPostProcessor(); + CommonAnnotationBeanPostProcessor bpp2 = SerializationTestUtils.serializeAndDeserialize(bpp); + + AnnotatedInitDestroyBean bean = new AnnotatedInitDestroyBean(); + bpp2.postProcessBeforeDestruction(bean, "annotatedBean"); + assertThat(bean.destroyCalled).isTrue(); + } + + @Test + public void testSerializationWithManualConfiguration() throws Exception { + InitDestroyAnnotationBeanPostProcessor bpp = new InitDestroyAnnotationBeanPostProcessor(); + bpp.setInitAnnotationType(PostConstruct.class); + bpp.setDestroyAnnotationType(PreDestroy.class); + InitDestroyAnnotationBeanPostProcessor bpp2 = SerializationTestUtils.serializeAndDeserialize(bpp); + + AnnotatedInitDestroyBean bean = new AnnotatedInitDestroyBean(); + bpp2.postProcessBeforeDestruction(bean, "annotatedBean"); + assertThat(bean.destroyCalled).isTrue(); + } + + @Test + public void testResourceInjection() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + CommonAnnotationBeanPostProcessor bpp = new CommonAnnotationBeanPostProcessor(); + bpp.setResourceFactory(bf); + bf.addBeanPostProcessor(bpp); + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ResourceInjectionBean.class)); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + TestBean tb2 = new TestBean(); + bf.registerSingleton("testBean2", tb2); + + ResourceInjectionBean bean = (ResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.initCalled).isTrue(); + assertThat(bean.init2Called).isTrue(); + assertThat(bean.init3Called).isTrue(); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb2); + bf.destroySingletons(); + assertThat(bean.destroyCalled).isTrue(); + assertThat(bean.destroy2Called).isTrue(); + assertThat(bean.destroy3Called).isTrue(); + } + + @Test + public void testResourceInjectionWithPrototypes() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + CommonAnnotationBeanPostProcessor bpp = new CommonAnnotationBeanPostProcessor(); + bpp.setResourceFactory(bf); + bf.addBeanPostProcessor(bpp); + RootBeanDefinition abd = new RootBeanDefinition(ResourceInjectionBean.class); + abd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", abd); + RootBeanDefinition tbd1 = new RootBeanDefinition(TestBean.class); + tbd1.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("testBean", tbd1); + RootBeanDefinition tbd2 = new RootBeanDefinition(TestBean.class); + tbd2.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("testBean2", tbd2); + + ResourceInjectionBean bean = (ResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.initCalled).isTrue(); + assertThat(bean.init2Called).isTrue(); + assertThat(bean.init3Called).isTrue(); + + TestBean tb = bean.getTestBean(); + TestBean tb2 = bean.getTestBean2(); + assertThat(tb).isNotNull(); + assertThat(tb2).isNotNull(); + + ResourceInjectionBean anotherBean = (ResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean).isNotSameAs(anotherBean); + assertThat(tb).isNotSameAs(anotherBean.getTestBean()); + assertThat(tb2).isNotSameAs(anotherBean.getTestBean2()); + + bf.destroyBean("annotatedBean", bean); + assertThat(bean.destroyCalled).isTrue(); + assertThat(bean.destroy2Called).isTrue(); + assertThat(bean.destroy3Called).isTrue(); + } + + @Test + public void testResourceInjectionWithResolvableDependencyType() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + CommonAnnotationBeanPostProcessor bpp = new CommonAnnotationBeanPostProcessor(); + bpp.setBeanFactory(bf); + bf.addBeanPostProcessor(bpp); + RootBeanDefinition abd = new RootBeanDefinition(ExtendedResourceInjectionBean.class); + abd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", abd); + RootBeanDefinition tbd = new RootBeanDefinition(TestBean.class); + tbd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("testBean4", tbd); + + bf.registerResolvableDependency(BeanFactory.class, bf); + bf.registerResolvableDependency(INestedTestBean.class, new ObjectFactory() { + @Override + public Object getObject() throws BeansException { + return new NestedTestBean(); + } + }); + + @SuppressWarnings("deprecation") + org.springframework.beans.factory.config.PropertyPlaceholderConfigurer ppc = new org.springframework.beans.factory.config.PropertyPlaceholderConfigurer(); + Properties props = new Properties(); + props.setProperty("tb", "testBean4"); + ppc.setProperties(props); + ppc.postProcessBeanFactory(bf); + + ExtendedResourceInjectionBean bean = (ExtendedResourceInjectionBean) bf.getBean("annotatedBean"); + INestedTestBean tb = bean.getTestBean6(); + assertThat(tb).isNotNull(); + + ExtendedResourceInjectionBean anotherBean = (ExtendedResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean).isNotSameAs(anotherBean); + assertThat(tb).isNotSameAs(anotherBean.getTestBean6()); + + String[] depBeans = bf.getDependenciesForBean("annotatedBean"); + assertThat(depBeans.length).isEqualTo(1); + assertThat(depBeans[0]).isEqualTo("testBean4"); + } + + @Test + public void testResourceInjectionWithDefaultMethod() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + CommonAnnotationBeanPostProcessor bpp = new CommonAnnotationBeanPostProcessor(); + bpp.setBeanFactory(bf); + bf.addBeanPostProcessor(bpp); + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(DefaultMethodResourceInjectionBean.class)); + TestBean tb2 = new TestBean(); + bf.registerSingleton("testBean2", tb2); + NestedTestBean tb7 = new NestedTestBean(); + bf.registerSingleton("testBean7", tb7); + + DefaultMethodResourceInjectionBean bean = (DefaultMethodResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean2()).isSameAs(tb2); + assertThat(bean.counter).isSameAs(2); + + bf.destroySingletons(); + assertThat(bean.counter).isSameAs(3); + } + + @Test + public void testResourceInjectionWithTwoProcessors() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + CommonAnnotationBeanPostProcessor bpp = new CommonAnnotationBeanPostProcessor(); + bpp.setResourceFactory(bf); + bf.addBeanPostProcessor(bpp); + CommonAnnotationBeanPostProcessor bpp2 = new CommonAnnotationBeanPostProcessor(); + bpp2.setResourceFactory(bf); + bf.addBeanPostProcessor(bpp2); + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ResourceInjectionBean.class)); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + TestBean tb2 = new TestBean(); + bf.registerSingleton("testBean2", tb2); + + ResourceInjectionBean bean = (ResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.initCalled).isTrue(); + assertThat(bean.init2Called).isTrue(); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb2); + bf.destroySingletons(); + assertThat(bean.destroyCalled).isTrue(); + assertThat(bean.destroy2Called).isTrue(); + } + + @Test + public void testResourceInjectionFromJndi() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + CommonAnnotationBeanPostProcessor bpp = new CommonAnnotationBeanPostProcessor(); + SimpleJndiBeanFactory resourceFactory = new SimpleJndiBeanFactory(); + ExpectedLookupTemplate jndiTemplate = new ExpectedLookupTemplate(); + TestBean tb = new TestBean(); + jndiTemplate.addObject("java:comp/env/testBean", tb); + TestBean tb2 = new TestBean(); + jndiTemplate.addObject("java:comp/env/testBean2", tb2); + resourceFactory.setJndiTemplate(jndiTemplate); + bpp.setResourceFactory(resourceFactory); + bf.addBeanPostProcessor(bpp); + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ResourceInjectionBean.class)); + + ResourceInjectionBean bean = (ResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.initCalled).isTrue(); + assertThat(bean.init2Called).isTrue(); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb2); + bf.destroySingletons(); + assertThat(bean.destroyCalled).isTrue(); + assertThat(bean.destroy2Called).isTrue(); + } + + @Test + public void testExtendedResourceInjection() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + CommonAnnotationBeanPostProcessor bpp = new CommonAnnotationBeanPostProcessor(); + bpp.setBeanFactory(bf); + bf.addBeanPostProcessor(bpp); + bf.registerResolvableDependency(BeanFactory.class, bf); + + @SuppressWarnings("deprecation") + org.springframework.beans.factory.config.PropertyPlaceholderConfigurer ppc = new org.springframework.beans.factory.config.PropertyPlaceholderConfigurer(); + Properties props = new Properties(); + props.setProperty("tb", "testBean3"); + ppc.setProperties(props); + ppc.postProcessBeanFactory(bf); + + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ExtendedResourceInjectionBean.class)); + bf.registerBeanDefinition("annotatedBean2", new RootBeanDefinition(NamedResourceInjectionBean.class)); + bf.registerBeanDefinition("annotatedBean3", new RootBeanDefinition(ConvertedResourceInjectionBean.class)); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + TestBean tb2 = new TestBean(); + bf.registerSingleton("testBean2", tb2); + TestBean tb3 = new TestBean(); + bf.registerSingleton("testBean3", tb3); + TestBean tb4 = new TestBean(); + bf.registerSingleton("testBean4", tb4); + NestedTestBean tb6 = new NestedTestBean(); + bf.registerSingleton("value", "5"); + bf.registerSingleton("xy", tb6); + bf.registerAlias("xy", "testBean9"); + + ExtendedResourceInjectionBean bean = (ExtendedResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.initCalled).isTrue(); + assertThat(bean.init2Called).isTrue(); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb2); + assertThat(bean.getTestBean3()).isSameAs(tb4); + assertThat(bean.getTestBean4()).isSameAs(tb3); + assertThat(bean.testBean5).isSameAs(tb6); + assertThat(bean.testBean6).isSameAs(tb6); + assertThat(bean.beanFactory).isSameAs(bf); + + NamedResourceInjectionBean bean2 = (NamedResourceInjectionBean) bf.getBean("annotatedBean2"); + assertThat(bean2.testBean).isSameAs(tb6); + + ConvertedResourceInjectionBean bean3 = (ConvertedResourceInjectionBean) bf.getBean("annotatedBean3"); + assertThat(bean3.value).isSameAs(5); + + bf.destroySingletons(); + assertThat(bean.destroyCalled).isTrue(); + assertThat(bean.destroy2Called).isTrue(); + } + + @Test + public void testExtendedResourceInjectionWithOverriding() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + CommonAnnotationBeanPostProcessor bpp = new CommonAnnotationBeanPostProcessor(); + bpp.setBeanFactory(bf); + bf.addBeanPostProcessor(bpp); + bf.registerResolvableDependency(BeanFactory.class, bf); + + @SuppressWarnings("deprecation") + org.springframework.beans.factory.config.PropertyPlaceholderConfigurer ppc = new org.springframework.beans.factory.config.PropertyPlaceholderConfigurer(); + Properties props = new Properties(); + props.setProperty("tb", "testBean3"); + ppc.setProperties(props); + ppc.postProcessBeanFactory(bf); + + RootBeanDefinition annotatedBd = new RootBeanDefinition(ExtendedResourceInjectionBean.class); + TestBean tb5 = new TestBean(); + annotatedBd.getPropertyValues().add("testBean2", tb5); + bf.registerBeanDefinition("annotatedBean", annotatedBd); + bf.registerBeanDefinition("annotatedBean2", new RootBeanDefinition(NamedResourceInjectionBean.class)); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + TestBean tb2 = new TestBean(); + bf.registerSingleton("testBean2", tb2); + TestBean tb3 = new TestBean(); + bf.registerSingleton("testBean3", tb3); + TestBean tb4 = new TestBean(); + bf.registerSingleton("testBean4", tb4); + NestedTestBean tb6 = new NestedTestBean(); + bf.registerSingleton("xy", tb6); + + ExtendedResourceInjectionBean bean = (ExtendedResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.initCalled).isTrue(); + assertThat(bean.init2Called).isTrue(); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb5); + assertThat(bean.getTestBean3()).isSameAs(tb4); + assertThat(bean.getTestBean4()).isSameAs(tb3); + assertThat(bean.testBean5).isSameAs(tb6); + assertThat(bean.testBean6).isSameAs(tb6); + assertThat(bean.beanFactory).isSameAs(bf); + + try { + bf.getBean("annotatedBean2"); + } + catch (BeanCreationException ex) { + boolean condition = ex.getRootCause() instanceof NoSuchBeanDefinitionException; + assertThat(condition).isTrue(); + NoSuchBeanDefinitionException innerEx = (NoSuchBeanDefinitionException) ex.getRootCause(); + assertThat(innerEx.getBeanName()).isEqualTo("testBean9"); + } + + bf.destroySingletons(); + assertThat(bean.destroyCalled).isTrue(); + assertThat(bean.destroy2Called).isTrue(); + } + + @Test + public void testExtendedEjbInjection() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + CommonAnnotationBeanPostProcessor bpp = new CommonAnnotationBeanPostProcessor(); + bpp.setBeanFactory(bf); + bf.addBeanPostProcessor(bpp); + bf.registerResolvableDependency(BeanFactory.class, bf); + + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ExtendedEjbInjectionBean.class)); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + TestBean tb2 = new TestBean(); + bf.registerSingleton("testBean2", tb2); + TestBean tb3 = new TestBean(); + bf.registerSingleton("testBean3", tb3); + TestBean tb4 = new TestBean(); + bf.registerSingleton("testBean4", tb4); + NestedTestBean tb6 = new NestedTestBean(); + bf.registerSingleton("xy", tb6); + bf.registerAlias("xy", "testBean9"); + + ExtendedEjbInjectionBean bean = (ExtendedEjbInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.initCalled).isTrue(); + assertThat(bean.init2Called).isTrue(); + assertThat(bean.getTestBean()).isSameAs(tb); + assertThat(bean.getTestBean2()).isSameAs(tb2); + assertThat(bean.getTestBean3()).isSameAs(tb4); + assertThat(bean.getTestBean4()).isSameAs(tb3); + assertThat(bean.testBean5).isSameAs(tb6); + assertThat(bean.testBean6).isSameAs(tb6); + assertThat(bean.beanFactory).isSameAs(bf); + + bf.destroySingletons(); + assertThat(bean.destroyCalled).isTrue(); + assertThat(bean.destroy2Called).isTrue(); + } + + @Test + public void testLazyResolutionWithResourceField() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + CommonAnnotationBeanPostProcessor bpp = new CommonAnnotationBeanPostProcessor(); + bpp.setBeanFactory(bf); + bf.addBeanPostProcessor(bpp); + + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(LazyResourceFieldInjectionBean.class)); + bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); + + LazyResourceFieldInjectionBean bean = (LazyResourceFieldInjectionBean) bf.getBean("annotatedBean"); + assertThat(bf.containsSingleton("testBean")).isFalse(); + bean.testBean.setName("notLazyAnymore"); + assertThat(bf.containsSingleton("testBean")).isTrue(); + TestBean tb = (TestBean) bf.getBean("testBean"); + assertThat(tb.getName()).isEqualTo("notLazyAnymore"); + } + + @Test + public void testLazyResolutionWithResourceMethod() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + CommonAnnotationBeanPostProcessor bpp = new CommonAnnotationBeanPostProcessor(); + bpp.setBeanFactory(bf); + bf.addBeanPostProcessor(bpp); + + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(LazyResourceMethodInjectionBean.class)); + bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); + + LazyResourceMethodInjectionBean bean = (LazyResourceMethodInjectionBean) bf.getBean("annotatedBean"); + assertThat(bf.containsSingleton("testBean")).isFalse(); + bean.testBean.setName("notLazyAnymore"); + assertThat(bf.containsSingleton("testBean")).isTrue(); + TestBean tb = (TestBean) bf.getBean("testBean"); + assertThat(tb.getName()).isEqualTo("notLazyAnymore"); + } + + @Test + public void testLazyResolutionWithCglibProxy() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + CommonAnnotationBeanPostProcessor bpp = new CommonAnnotationBeanPostProcessor(); + bpp.setBeanFactory(bf); + bf.addBeanPostProcessor(bpp); + + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(LazyResourceCglibInjectionBean.class)); + bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class)); + + LazyResourceCglibInjectionBean bean = (LazyResourceCglibInjectionBean) bf.getBean("annotatedBean"); + assertThat(bf.containsSingleton("testBean")).isFalse(); + bean.testBean.setName("notLazyAnymore"); + assertThat(bf.containsSingleton("testBean")).isTrue(); + TestBean tb = (TestBean) bf.getBean("testBean"); + assertThat(tb.getName()).isEqualTo("notLazyAnymore"); + } + + + public static class AnnotatedInitDestroyBean { + + public boolean initCalled = false; + + public boolean destroyCalled = false; + + @PostConstruct + private void init() { + if (this.initCalled) { + throw new IllegalStateException("Already called"); + } + this.initCalled = true; + } + + @PreDestroy + private void destroy() { + if (this.destroyCalled) { + throw new IllegalStateException("Already called"); + } + this.destroyCalled = true; + } + } + + + public static class InitDestroyBeanPostProcessor implements DestructionAwareBeanPostProcessor { + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof AnnotatedInitDestroyBean) { + assertThat(((AnnotatedInitDestroyBean) bean).initCalled).isFalse(); + } + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof AnnotatedInitDestroyBean) { + assertThat(((AnnotatedInitDestroyBean) bean).initCalled).isTrue(); + } + return bean; + } + + @Override + public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException { + if (bean instanceof AnnotatedInitDestroyBean) { + assertThat(((AnnotatedInitDestroyBean) bean).destroyCalled).isFalse(); + } + } + + @Override + public boolean requiresDestruction(Object bean) { + return true; + } + } + + + public static class ResourceInjectionBean extends AnnotatedInitDestroyBean { + + public boolean init2Called = false; + + public boolean init3Called = false; + + public boolean destroy2Called = false; + + public boolean destroy3Called = false; + + @Resource + private TestBean testBean; + + private TestBean testBean2; + + @PostConstruct + protected void init2() { + if (this.testBean == null || this.testBean2 == null) { + throw new IllegalStateException("Resources not injected"); + } + if (!this.initCalled) { + throw new IllegalStateException("Superclass init method not called yet"); + } + if (this.init2Called) { + throw new IllegalStateException("Already called"); + } + this.init2Called = true; + } + + @PostConstruct + private void init() { + if (this.init3Called) { + throw new IllegalStateException("Already called"); + } + this.init3Called = true; + } + + @PreDestroy + protected void destroy2() { + if (this.destroyCalled) { + throw new IllegalStateException("Superclass destroy called too soon"); + } + if (this.destroy2Called) { + throw new IllegalStateException("Already called"); + } + this.destroy2Called = true; + } + + @PreDestroy + private void destroy() { + if (this.destroyCalled) { + throw new IllegalStateException("Superclass destroy called too soon"); + } + if (this.destroy3Called) { + throw new IllegalStateException("Already called"); + } + this.destroy3Called = true; + } + + @Resource + public void setTestBean2(TestBean testBean2) { + if (this.testBean2 != null) { + throw new IllegalStateException("Already called"); + } + this.testBean2 = testBean2; + } + + public TestBean getTestBean() { + return testBean; + } + + public TestBean getTestBean2() { + return testBean2; + } + } + + + static class NonPublicResourceInjectionBean extends ResourceInjectionBean { + + @Resource(name="testBean4", type=TestBean.class) + protected ITestBean testBean3; + + private B testBean4; + + @Resource + INestedTestBean testBean5; + + INestedTestBean testBean6; + + @Resource + BeanFactory beanFactory; + + @Override + @Resource + public void setTestBean2(TestBean testBean2) { + super.setTestBean2(testBean2); + } + + @Resource(name="${tb}", type=ITestBean.class) + private void setTestBean4(B testBean4) { + if (this.testBean4 != null) { + throw new IllegalStateException("Already called"); + } + this.testBean4 = testBean4; + } + + @Resource + public void setTestBean6(INestedTestBean testBean6) { + if (this.testBean6 != null) { + throw new IllegalStateException("Already called"); + } + this.testBean6 = testBean6; + } + + public ITestBean getTestBean3() { + return testBean3; + } + + public B getTestBean4() { + return testBean4; + } + + public INestedTestBean getTestBean5() { + return testBean5; + } + + public INestedTestBean getTestBean6() { + return testBean6; + } + + @Override + @PostConstruct + protected void init2() { + if (this.testBean3 == null || this.testBean4 == null) { + throw new IllegalStateException("Resources not injected"); + } + super.init2(); + } + + @Override + @PreDestroy + protected void destroy2() { + super.destroy2(); + } + } + + + public static class ExtendedResourceInjectionBean extends NonPublicResourceInjectionBean { + } + + + public interface InterfaceWithDefaultMethod { + + @Resource + void setTestBean2(TestBean testBean2); + + @Resource + default void setTestBean7(INestedTestBean testBean7) { + increaseCounter(); + } + + @PostConstruct + default void initDefault() { + increaseCounter(); + } + + @PreDestroy + default void destroyDefault() { + increaseCounter(); + } + + void increaseCounter(); + } + + + public static class DefaultMethodResourceInjectionBean extends ResourceInjectionBean + implements InterfaceWithDefaultMethod { + + public int counter = 0; + + @Override + public void increaseCounter() { + counter++; + } + } + + + public static class ExtendedEjbInjectionBean extends ResourceInjectionBean { + + @EJB(name="testBean4", beanInterface=TestBean.class) + protected ITestBean testBean3; + + private ITestBean testBean4; + + @EJB + private INestedTestBean testBean5; + + private INestedTestBean testBean6; + + @Resource + private BeanFactory beanFactory; + + @Override + @EJB + public void setTestBean2(TestBean testBean2) { + super.setTestBean2(testBean2); + } + + @EJB(beanName="testBean3", beanInterface=ITestBean.class) + private void setTestBean4(ITestBean testBean4) { + if (this.testBean4 != null) { + throw new IllegalStateException("Already called"); + } + this.testBean4 = testBean4; + } + + @EJB + public void setTestBean6(INestedTestBean testBean6) { + if (this.testBean6 != null) { + throw new IllegalStateException("Already called"); + } + this.testBean6 = testBean6; + } + + public ITestBean getTestBean3() { + return testBean3; + } + + public ITestBean getTestBean4() { + return testBean4; + } + + @Override + @PostConstruct + protected void init2() { + if (this.testBean3 == null || this.testBean4 == null) { + throw new IllegalStateException("Resources not injected"); + } + super.init2(); + } + + @Override + @PreDestroy + protected void destroy2() { + super.destroy2(); + } + } + + + private static class NamedResourceInjectionBean { + + @Resource(name="testBean9") + private INestedTestBean testBean; + } + + + private static class ConvertedResourceInjectionBean { + + @Resource(name="value") + private int value; + } + + + private static class LazyResourceFieldInjectionBean { + + @Resource @Lazy + private ITestBean testBean; + } + + + private static class LazyResourceMethodInjectionBean { + + private ITestBean testBean; + + @Resource @Lazy + public void setTestBean(ITestBean testBean) { + this.testBean = testBean; + } + } + + + private static class LazyResourceCglibInjectionBean { + + private TestBean testBean; + + @Resource @Lazy + public void setTestBean(TestBean testBean) { + this.testBean = testBean; + } + } + + + @SuppressWarnings("unused") + private static class NullFactory { + + public static Object create() { + return null; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAndImportAnnotationInteractionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAndImportAnnotationInteractionTests.java new file mode 100644 index 0000000..ae0a9b8 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAndImportAnnotationInteractionTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.annotation.componentscan.importing.ImportingConfig; +import org.springframework.context.annotation.componentscan.simple.SimpleComponent; + +/** + * Tests covering overlapping use of @ComponentScan and @Import annotations. + * + * @author Chris Beams + * @since 3.1 + */ +public class ComponentScanAndImportAnnotationInteractionTests { + + @Test + public void componentScanOverlapsWithImport() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(Config1.class); + ctx.register(Config2.class); + ctx.refresh(); // no conflicts found trying to register SimpleComponent + ctx.getBean(SimpleComponent.class); // succeeds -> there is only one bean of type SimpleComponent + } + + @Test + public void componentScanOverlapsWithImportUsingAsm() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.registerBeanDefinition("config1", new RootBeanDefinition(Config1.class.getName())); + ctx.registerBeanDefinition("config2", new RootBeanDefinition(Config2.class.getName())); + ctx.refresh(); // no conflicts found trying to register SimpleComponent + ctx.getBean(SimpleComponent.class); // succeeds -> there is only one bean of type SimpleComponent + } + + @Test + public void componentScanViaImport() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(Config3.class); + ctx.refresh(); + ctx.getBean(SimpleComponent.class); + } + + @Test + public void componentScanViaImportUsingAsm() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.registerBeanDefinition("config", new RootBeanDefinition(Config3.class.getName())); + ctx.refresh(); + ctx.getBean(SimpleComponent.class); + } + + @Test + public void componentScanViaImportUsingScan() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.scan("org.springframework.context.annotation.componentscan.importing"); + ctx.refresh(); + ctx.getBean(SimpleComponent.class); + } + + @Test + public void circularImportViaComponentScan() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.registerBeanDefinition("config", new RootBeanDefinition(ImportingConfig.class.getName())); + ctx.refresh(); + ctx.getBean(SimpleComponent.class); + } + + + @ComponentScan("org.springframework.context.annotation.componentscan.simple") + static final class Config1 { + } + + + @Import(org.springframework.context.annotation.componentscan.simple.SimpleComponent.class) + static final class Config2 { + } + + + @Import(ImportedConfig.class) + static final class Config3 { + } + + + @ComponentScan("org.springframework.context.annotation.componentscan.simple") + @ComponentScan("org.springframework.context.annotation.componentscan.importing") + public static final class ImportedConfig { + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationIntegrationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationIntegrationTests.java new file mode 100644 index 0000000..17e1382 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationIntegrationTests.java @@ -0,0 +1,462 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.io.IOException; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.HashSet; + +import example.scannable.CustomComponent; +import example.scannable.CustomStereotype; +import example.scannable.DefaultNamedComponent; +import example.scannable.FooService; +import example.scannable.MessageBean; +import example.scannable.ScopedProxyTestBean; +import example.scannable_implicitbasepackage.ComponentScanAnnotatedConfigWithImplicitBasePackage; +import example.scannable_implicitbasepackage.ConfigurableComponent; +import example.scannable_scoped.CustomScopeAnnotationBean; +import example.scannable_scoped.MyScope; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.annotation.CustomAutowireConfigurer; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.ComponentScanParserTests.KustomAnnotationAutowiredBean; +import org.springframework.context.annotation.componentscan.simple.ClassWithNestedComponents; +import org.springframework.context.annotation.componentscan.simple.SimpleComponent; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.context.testfixture.SimpleMapScope; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.env.Profiles; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.testfixture.io.SerializationTestUtils; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.TypeFilter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.beans.factory.support.BeanDefinitionBuilder.genericBeanDefinition; + +/** + * Integration tests for processing ComponentScan-annotated Configuration classes. + * + * @author Chris Beams + * @author Juergen Hoeller + * @author Sam Brannen + * @since 3.1 + */ +@SuppressWarnings("resource") +public class ComponentScanAnnotationIntegrationTests { + + @Test + public void controlScan() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.scan(example.scannable.PackageMarker.class.getPackage().getName()); + ctx.refresh(); + assertThat(ctx.containsBean("fooServiceImpl")).as( + "control scan for example.scannable package failed to register FooServiceImpl bean").isTrue(); + } + + @Test + public void viaContextRegistration() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ComponentScanAnnotatedConfig.class); + ctx.refresh(); + ctx.getBean(ComponentScanAnnotatedConfig.class); + ctx.getBean(TestBean.class); + assertThat(ctx.containsBeanDefinition("componentScanAnnotatedConfig")).as("config class bean not found") + .isTrue(); + assertThat(ctx.containsBean("fooServiceImpl")).as("@ComponentScan annotated @Configuration class registered directly against " + + "AnnotationConfigApplicationContext did not trigger component scanning as expected") + .isTrue(); + } + + @Test + public void viaContextRegistration_WithValueAttribute() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ComponentScanAnnotatedConfig_WithValueAttribute.class); + ctx.refresh(); + ctx.getBean(ComponentScanAnnotatedConfig_WithValueAttribute.class); + ctx.getBean(TestBean.class); + assertThat(ctx.containsBeanDefinition("componentScanAnnotatedConfig_WithValueAttribute")).as("config class bean not found") + .isTrue(); + assertThat(ctx.containsBean("fooServiceImpl")).as("@ComponentScan annotated @Configuration class registered directly against " + + "AnnotationConfigApplicationContext did not trigger component scanning as expected") + .isTrue(); + } + + @Test + public void viaContextRegistration_FromPackageOfConfigClass() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ComponentScanAnnotatedConfigWithImplicitBasePackage.class); + ctx.refresh(); + ctx.getBean(ComponentScanAnnotatedConfigWithImplicitBasePackage.class); + assertThat(ctx.containsBeanDefinition("componentScanAnnotatedConfigWithImplicitBasePackage")).as("config class bean not found") + .isTrue(); + assertThat(ctx.containsBean("scannedComponent")).as("@ComponentScan annotated @Configuration class registered directly against " + + "AnnotationConfigApplicationContext did not trigger component scanning as expected") + .isTrue(); + assertThat(ctx.getBean(ConfigurableComponent.class).isFlag()).as("@Bean method overrides scanned class") + .isTrue(); + } + + @Test + public void viaContextRegistration_WithComposedAnnotation() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ComposedAnnotationConfig.class); + ctx.refresh(); + ctx.getBean(ComposedAnnotationConfig.class); + ctx.getBean(SimpleComponent.class); + ctx.getBean(ClassWithNestedComponents.NestedComponent.class); + ctx.getBean(ClassWithNestedComponents.OtherNestedComponent.class); + assertThat(ctx.containsBeanDefinition("componentScanAnnotationIntegrationTests.ComposedAnnotationConfig")).as("config class bean not found") + .isTrue(); + assertThat(ctx.containsBean("simpleComponent")).as("@ComponentScan annotated @Configuration class registered directly against " + + "AnnotationConfigApplicationContext did not trigger component scanning as expected") + .isTrue(); + } + + @Test + public void viaBeanRegistration() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("componentScanAnnotatedConfig", + genericBeanDefinition(ComponentScanAnnotatedConfig.class).getBeanDefinition()); + bf.registerBeanDefinition("configurationClassPostProcessor", + genericBeanDefinition(ConfigurationClassPostProcessor.class).getBeanDefinition()); + GenericApplicationContext ctx = new GenericApplicationContext(bf); + ctx.refresh(); + ctx.getBean(ComponentScanAnnotatedConfig.class); + ctx.getBean(TestBean.class); + assertThat(ctx.containsBeanDefinition("componentScanAnnotatedConfig")).as("config class bean not found") + .isTrue(); + assertThat(ctx.containsBean("fooServiceImpl")).as("@ComponentScan annotated @Configuration class registered as bean " + + "definition did not trigger component scanning as expected") + .isTrue(); + } + + @Test + public void withCustomBeanNameGenerator() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ComponentScanWithBeanNameGenerator.class); + ctx.refresh(); + assertThat(ctx.containsBean("custom_fooServiceImpl")).isTrue(); + assertThat(ctx.containsBean("fooServiceImpl")).isFalse(); + } + + @Test + public void withScopeResolver() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ComponentScanWithScopeResolver.class); + // custom scope annotation makes the bean prototype scoped. subsequent calls + // to getBean should return distinct instances. + assertThat(ctx.getBean(CustomScopeAnnotationBean.class)).isNotSameAs(ctx.getBean(CustomScopeAnnotationBean.class)); + assertThat(ctx.containsBean("scannedComponent")).isFalse(); + } + + @Test + public void multiComponentScan() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(MultiComponentScan.class); + assertThat(ctx.getBean(CustomScopeAnnotationBean.class)).isNotSameAs(ctx.getBean(CustomScopeAnnotationBean.class)); + assertThat(ctx.containsBean("scannedComponent")).isTrue(); + } + + @Test + public void withCustomTypeFilter() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ComponentScanWithCustomTypeFilter.class); + assertThat(ctx.getDefaultListableBeanFactory().containsSingleton("componentScanParserTests.KustomAnnotationAutowiredBean")).isFalse(); + KustomAnnotationAutowiredBean testBean = ctx.getBean("componentScanParserTests.KustomAnnotationAutowiredBean", KustomAnnotationAutowiredBean.class); + assertThat(testBean.getDependency()).isNotNull(); + } + + @Test + public void withAwareTypeFilter() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ComponentScanWithAwareTypeFilter.class); + assertThat(ctx.getEnvironment().acceptsProfiles(Profiles.of("the-filter-ran"))).isTrue(); + } + + @Test + public void withScopedProxy() throws IOException, ClassNotFoundException { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ComponentScanWithScopedProxy.class); + ctx.getBeanFactory().registerScope("myScope", new SimpleMapScope()); + ctx.refresh(); + // should cast to the interface + FooService bean = (FooService) ctx.getBean("scopedProxyTestBean"); + // should be dynamic proxy + assertThat(AopUtils.isJdkDynamicProxy(bean)).isTrue(); + // test serializability + assertThat(bean.foo(1)).isEqualTo("bar"); + FooService deserialized = SerializationTestUtils.serializeAndDeserialize(bean); + assertThat(deserialized).isNotNull(); + assertThat(deserialized.foo(1)).isEqualTo("bar"); + } + + @Test + public void withScopedProxyThroughRegex() throws IOException, ClassNotFoundException { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ComponentScanWithScopedProxyThroughRegex.class); + ctx.getBeanFactory().registerScope("myScope", new SimpleMapScope()); + ctx.refresh(); + // should cast to the interface + FooService bean = (FooService) ctx.getBean("scopedProxyTestBean"); + // should be dynamic proxy + assertThat(AopUtils.isJdkDynamicProxy(bean)).isTrue(); + } + + @Test + public void withScopedProxyThroughAspectJPattern() throws IOException, ClassNotFoundException { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ComponentScanWithScopedProxyThroughAspectJPattern.class); + ctx.getBeanFactory().registerScope("myScope", new SimpleMapScope()); + ctx.refresh(); + // should cast to the interface + FooService bean = (FooService) ctx.getBean("scopedProxyTestBean"); + // should be dynamic proxy + assertThat(AopUtils.isJdkDynamicProxy(bean)).isTrue(); + } + + @Test + public void withMultipleAnnotationIncludeFilters1() throws IOException, ClassNotFoundException { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ComponentScanWithMultipleAnnotationIncludeFilters1.class); + ctx.refresh(); + ctx.getBean(DefaultNamedComponent.class); // @CustomStereotype-annotated + ctx.getBean(MessageBean.class); // @CustomComponent-annotated + } + + @Test + public void withMultipleAnnotationIncludeFilters2() throws IOException, ClassNotFoundException { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ComponentScanWithMultipleAnnotationIncludeFilters2.class); + ctx.refresh(); + ctx.getBean(DefaultNamedComponent.class); // @CustomStereotype-annotated + ctx.getBean(MessageBean.class); // @CustomComponent-annotated + } + + @Test + public void withBasePackagesAndValueAlias() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ComponentScanWithBasePackagesAndValueAlias.class); + ctx.refresh(); + assertThat(ctx.containsBean("fooServiceImpl")).isTrue(); + } + + + @Configuration + @ComponentScan + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface ComposedConfiguration { + + String[] basePackages() default {}; + } + + @ComposedConfiguration(basePackages = "org.springframework.context.annotation.componentscan.simple") + public static class ComposedAnnotationConfig { + } + + public static class AwareTypeFilter implements TypeFilter, EnvironmentAware, + ResourceLoaderAware, BeanClassLoaderAware, BeanFactoryAware { + + private BeanFactory beanFactory; + private ClassLoader classLoader; + private ResourceLoader resourceLoader; + private Environment environment; + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + @Override + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + @Override + public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) { + ((ConfigurableEnvironment) this.environment).addActiveProfile("the-filter-ran"); + assertThat(this.beanFactory).isNotNull(); + assertThat(this.classLoader).isNotNull(); + assertThat(this.resourceLoader).isNotNull(); + assertThat(this.environment).isNotNull(); + return false; + } + + } + + +} + + +@Configuration +@ComponentScan(basePackageClasses = example.scannable.PackageMarker.class) +class ComponentScanAnnotatedConfig { + + @Bean + public TestBean testBean() { + return new TestBean(); + } +} + +@Configuration +@ComponentScan("example.scannable") +class ComponentScanAnnotatedConfig_WithValueAttribute { + + @Bean + public TestBean testBean() { + return new TestBean(); + } +} + +@Configuration +@ComponentScan +class ComponentScanWithNoPackagesConfig { +} + +@Configuration +@ComponentScan(basePackages = "example.scannable", nameGenerator = MyBeanNameGenerator.class) +class ComponentScanWithBeanNameGenerator { +} + +class MyBeanNameGenerator extends AnnotationBeanNameGenerator { + + @Override + public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) { + return "custom_" + super.generateBeanName(definition, registry); + } +} + +@Configuration +@ComponentScan(basePackages = "example.scannable_scoped", scopeResolver = MyScopeMetadataResolver.class) +class ComponentScanWithScopeResolver { +} + +@Configuration +@ComponentScan(basePackages = "example.scannable_scoped", scopeResolver = MyScopeMetadataResolver.class) +@ComponentScan(basePackages = "example.scannable_implicitbasepackage") +class MultiComponentScan { +} + +class MyScopeMetadataResolver extends AnnotationScopeMetadataResolver { + + MyScopeMetadataResolver() { + this.scopeAnnotationType = MyScope.class; + } +} + +@Configuration +@ComponentScan( + basePackages = "org.springframework.context.annotation", + useDefaultFilters = false, + includeFilters = @Filter(type = FilterType.CUSTOM, classes = ComponentScanParserTests.CustomTypeFilter.class), + // exclude this class from scanning since it's in the scanned package + excludeFilters = @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = ComponentScanWithCustomTypeFilter.class), + lazyInit = true) +class ComponentScanWithCustomTypeFilter { + + @Bean + @SuppressWarnings({ "rawtypes", "serial", "unchecked" }) + public static CustomAutowireConfigurer customAutowireConfigurer() { + CustomAutowireConfigurer cac = new CustomAutowireConfigurer(); + cac.setCustomQualifierTypes(new HashSet() {{ + add(ComponentScanParserTests.CustomAnnotation.class); + }}); + return cac; + } + + public ComponentScanParserTests.KustomAnnotationAutowiredBean testBean() { + return new ComponentScanParserTests.KustomAnnotationAutowiredBean(); + } +} + +@Configuration +@ComponentScan( + basePackages = "org.springframework.context.annotation", + useDefaultFilters = false, + includeFilters = @Filter(type = FilterType.CUSTOM, classes = ComponentScanAnnotationIntegrationTests.AwareTypeFilter.class), + lazyInit = true) +class ComponentScanWithAwareTypeFilter {} + +@Configuration +@ComponentScan(basePackages = "example.scannable", + scopedProxy = ScopedProxyMode.INTERFACES, + useDefaultFilters = false, + includeFilters = @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = ScopedProxyTestBean.class)) +class ComponentScanWithScopedProxy {} + +@Configuration +@ComponentScan(basePackages = "example.scannable", + scopedProxy = ScopedProxyMode.INTERFACES, + useDefaultFilters = false, + includeFilters = @Filter(type=FilterType.REGEX, pattern = "((?:[a-z.]+))ScopedProxyTestBean")) +class ComponentScanWithScopedProxyThroughRegex {} + +@Configuration +@ComponentScan(basePackages = "example.scannable", + scopedProxy = ScopedProxyMode.INTERFACES, + useDefaultFilters = false, + includeFilters = @Filter(type=FilterType.ASPECTJ, pattern = "*..ScopedProxyTestBean")) +class ComponentScanWithScopedProxyThroughAspectJPattern {} + +@Configuration +@ComponentScan(basePackages = "example.scannable", + useDefaultFilters = false, + includeFilters = { + @Filter(CustomStereotype.class), + @Filter(CustomComponent.class) + } + ) +class ComponentScanWithMultipleAnnotationIncludeFilters1 {} + +@Configuration +@ComponentScan(basePackages = "example.scannable", + useDefaultFilters = false, + includeFilters = @Filter({CustomStereotype.class, CustomComponent.class}) + ) +class ComponentScanWithMultipleAnnotationIncludeFilters2 {} + +@Configuration +@ComponentScan( + value = "example.scannable", + basePackages = "example.scannable", + basePackageClasses = example.scannable.PackageMarker.class) +class ComponentScanWithBasePackagesAndValueAlias {} + + diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationRecursionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationRecursionTests.java new file mode 100644 index 0000000..f7fad00 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationRecursionTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.annotation.componentscan.cycle.left.LeftConfig; +import org.springframework.context.annotation.componentscan.level1.Level1Config; +import org.springframework.context.annotation.componentscan.level2.Level2Config; +import org.springframework.context.annotation.componentscan.level3.Level3Component; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Tests ensuring that configuration classes marked with @ComponentScan + * may be processed recursively + * + * @author Chris Beams + * @since 3.1 + */ +public class ComponentScanAnnotationRecursionTests { + + @Test + public void recursion() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(Level1Config.class); + ctx.refresh(); + + // assert that all levels have been detected + ctx.getBean(Level1Config.class); + ctx.getBean(Level2Config.class); + ctx.getBean(Level3Component.class); + + // assert that enhancement is working + assertThat(ctx.getBean("level1Bean")).isSameAs(ctx.getBean("level1Bean")); + assertThat(ctx.getBean("level2Bean")).isSameAs(ctx.getBean("level2Bean")); + } + + public void evenCircularScansAreSupported() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(LeftConfig.class); // left scans right, and right scans left + ctx.refresh(); + ctx.getBean("leftConfig"); // but this is handled gracefully + ctx.getBean("rightConfig"); // and beans from both packages are available + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationTests.java new file mode 100644 index 0000000..3853a6f --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.DefaultBeanNameGenerator; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.core.type.filter.TypeFilter; + +/** + * Unit tests for the @ComponentScan annotation. + * + * @author Chris Beams + * @since 3.1 + * @see ComponentScanAnnotationIntegrationTests + */ +public class ComponentScanAnnotationTests { + + @Test + public void noop() { + // no-op; the @ComponentScan-annotated MyConfig class below simply exercises + // available attributes of the annotation. + } +} + + +@interface MyAnnotation { +} + +@Configuration +@ComponentScan( + basePackageClasses = TestBean.class, + nameGenerator = DefaultBeanNameGenerator.class, + scopedProxy = ScopedProxyMode.NO, + scopeResolver = AnnotationScopeMetadataResolver.class, + resourcePattern = "**/*custom.class", + useDefaultFilters = false, + includeFilters = { + @Filter(type = FilterType.ANNOTATION, value = MyAnnotation.class) + }, + excludeFilters = { + @Filter(type = FilterType.CUSTOM, value = TypeFilter.class) + }, + lazyInit = true +) +class MyConfig { +} + +@ComponentScan(basePackageClasses = example.scannable.NamedComponent.class) +class SimpleConfig { +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserBeanDefinitionDefaultsTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserBeanDefinitionDefaultsTests.java new file mode 100644 index 0000000..cbd1b5c --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserBeanDefinitionDefaultsTests.java @@ -0,0 +1,279 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.UnsatisfiedDependencyException; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.context.support.GenericApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Mark Fisher + * @author Chris Beams + */ +public class ComponentScanParserBeanDefinitionDefaultsTests { + + private static final String TEST_BEAN_NAME = "componentScanParserBeanDefinitionDefaultsTests.DefaultsTestBean"; + + private static final String LOCATION_PREFIX = "org/springframework/context/annotation/"; + + + @BeforeEach + public void setUp() { + DefaultsTestBean.INIT_COUNT = 0; + } + + @Test + public void testDefaultLazyInit() { + GenericApplicationContext context = new GenericApplicationContext(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); + reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultWithNoOverridesTests.xml"); + assertThat(context.getBeanDefinition(TEST_BEAN_NAME).isLazyInit()).as("lazy-init should be false").isFalse(); + assertThat(DefaultsTestBean.INIT_COUNT).as("initCount should be 0").isEqualTo(0); + context.refresh(); + assertThat(DefaultsTestBean.INIT_COUNT).as("bean should have been instantiated").isEqualTo(1); + } + + @Test + public void testLazyInitTrue() { + GenericApplicationContext context = new GenericApplicationContext(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); + reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultLazyInitTrueTests.xml"); + assertThat(context.getBeanDefinition(TEST_BEAN_NAME).isLazyInit()).as("lazy-init should be true").isTrue(); + assertThat(DefaultsTestBean.INIT_COUNT).as("initCount should be 0").isEqualTo(0); + context.refresh(); + assertThat(DefaultsTestBean.INIT_COUNT).as("bean should not have been instantiated yet").isEqualTo(0); + context.getBean(TEST_BEAN_NAME); + assertThat(DefaultsTestBean.INIT_COUNT).as("bean should have been instantiated").isEqualTo(1); + } + + @Test + public void testLazyInitFalse() { + GenericApplicationContext context = new GenericApplicationContext(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); + reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultLazyInitFalseTests.xml"); + assertThat(context.getBeanDefinition(TEST_BEAN_NAME).isLazyInit()).as("lazy-init should be false").isFalse(); + assertThat(DefaultsTestBean.INIT_COUNT).as("initCount should be 0").isEqualTo(0); + context.refresh(); + assertThat(DefaultsTestBean.INIT_COUNT).as("bean should have been instantiated").isEqualTo(1); + } + + @Test + public void testDefaultAutowire() { + GenericApplicationContext context = new GenericApplicationContext(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); + reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultWithNoOverridesTests.xml"); + context.refresh(); + DefaultsTestBean bean = (DefaultsTestBean) context.getBean(TEST_BEAN_NAME); + assertThat(bean.getConstructorDependency()).as("no dependencies should have been autowired").isNull(); + assertThat(bean.getPropertyDependency1()).as("no dependencies should have been autowired").isNull(); + assertThat(bean.getPropertyDependency2()).as("no dependencies should have been autowired").isNull(); + } + + @Test + public void testAutowireNo() { + GenericApplicationContext context = new GenericApplicationContext(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); + reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultAutowireNoTests.xml"); + context.refresh(); + DefaultsTestBean bean = (DefaultsTestBean) context.getBean(TEST_BEAN_NAME); + assertThat(bean.getConstructorDependency()).as("no dependencies should have been autowired").isNull(); + assertThat(bean.getPropertyDependency1()).as("no dependencies should have been autowired").isNull(); + assertThat(bean.getPropertyDependency2()).as("no dependencies should have been autowired").isNull(); + } + + @Test + public void testAutowireConstructor() { + GenericApplicationContext context = new GenericApplicationContext(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); + reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultAutowireConstructorTests.xml"); + context.refresh(); + DefaultsTestBean bean = (DefaultsTestBean) context.getBean(TEST_BEAN_NAME); + assertThat(bean.getConstructorDependency()).as("constructor dependency should have been autowired").isNotNull(); + assertThat(bean.getConstructorDependency().getName()).isEqualTo("cd"); + assertThat(bean.getPropertyDependency1()).as("property dependencies should not have been autowired").isNull(); + assertThat(bean.getPropertyDependency2()).as("property dependencies should not have been autowired").isNull(); + } + + @Test + public void testAutowireByType() { + GenericApplicationContext context = new GenericApplicationContext(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); + reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultAutowireByTypeTests.xml"); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy( + context::refresh); + } + + @Test + public void testAutowireByName() { + GenericApplicationContext context = new GenericApplicationContext(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); + reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultAutowireByNameTests.xml"); + context.refresh(); + DefaultsTestBean bean = (DefaultsTestBean) context.getBean(TEST_BEAN_NAME); + assertThat(bean.getConstructorDependency()).as("constructor dependency should not have been autowired").isNull(); + assertThat(bean.getPropertyDependency1()).as("propertyDependency1 should not have been autowired").isNull(); + assertThat(bean.getPropertyDependency2()).as("propertyDependency2 should have been autowired").isNotNull(); + assertThat(bean.getPropertyDependency2().getName()).isEqualTo("pd2"); + } + + @Test + public void testDefaultDependencyCheck() { + GenericApplicationContext context = new GenericApplicationContext(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); + reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultWithNoOverridesTests.xml"); + context.refresh(); + DefaultsTestBean bean = (DefaultsTestBean) context.getBean(TEST_BEAN_NAME); + assertThat(bean.getConstructorDependency()).as("constructor dependency should not have been autowired").isNull(); + assertThat(bean.getPropertyDependency1()).as("property dependencies should not have been autowired").isNull(); + assertThat(bean.getPropertyDependency2()).as("property dependencies should not have been autowired").isNull(); + } + + @Test + public void testDefaultInitAndDestroyMethodsNotDefined() { + GenericApplicationContext context = new GenericApplicationContext(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); + reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultWithNoOverridesTests.xml"); + context.refresh(); + DefaultsTestBean bean = (DefaultsTestBean) context.getBean(TEST_BEAN_NAME); + assertThat(bean.isInitialized()).as("bean should not have been initialized").isFalse(); + context.close(); + assertThat(bean.isDestroyed()).as("bean should not have been destroyed").isFalse(); + } + + @Test + public void testDefaultInitAndDestroyMethodsDefined() { + GenericApplicationContext context = new GenericApplicationContext(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); + reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultInitAndDestroyMethodsTests.xml"); + context.refresh(); + DefaultsTestBean bean = (DefaultsTestBean) context.getBean(TEST_BEAN_NAME); + assertThat(bean.isInitialized()).as("bean should have been initialized").isTrue(); + context.close(); + assertThat(bean.isDestroyed()).as("bean should have been destroyed").isTrue(); + } + + @Test + public void testDefaultNonExistingInitAndDestroyMethodsDefined() { + GenericApplicationContext context = new GenericApplicationContext(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context); + reader.loadBeanDefinitions(LOCATION_PREFIX + "defaultNonExistingInitAndDestroyMethodsTests.xml"); + context.refresh(); + DefaultsTestBean bean = (DefaultsTestBean) context.getBean(TEST_BEAN_NAME); + assertThat(bean.isInitialized()).as("bean should not have been initialized").isFalse(); + context.close(); + assertThat(bean.isDestroyed()).as("bean should not have been destroyed").isFalse(); + } + + + @SuppressWarnings("unused") + private static class DefaultsTestBean { + + static int INIT_COUNT; + + private ConstructorDependencyTestBean constructorDependency; + + private PropertyDependencyTestBean propertyDependency1; + + private PropertyDependencyTestBean propertyDependency2; + + private boolean initialized; + + private boolean destroyed; + + public DefaultsTestBean() { + INIT_COUNT++; + } + + public DefaultsTestBean(ConstructorDependencyTestBean cdtb) { + this(); + this.constructorDependency = cdtb; + } + + public void init() { + this.initialized = true; + } + + public boolean isInitialized() { + return this.initialized; + } + + public void destroy() { + this.destroyed = true; + } + + public boolean isDestroyed() { + return this.destroyed; + } + + public void setPropertyDependency1(PropertyDependencyTestBean pdtb) { + this.propertyDependency1 = pdtb; + } + + public void setPropertyDependency2(PropertyDependencyTestBean pdtb) { + this.propertyDependency2 = pdtb; + } + + public ConstructorDependencyTestBean getConstructorDependency() { + return this.constructorDependency; + } + + public PropertyDependencyTestBean getPropertyDependency1() { + return this.propertyDependency1; + } + + public PropertyDependencyTestBean getPropertyDependency2() { + return this.propertyDependency2; + } + } + + + @SuppressWarnings("unused") + private static class PropertyDependencyTestBean { + + private String name; + + public PropertyDependencyTestBean(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + } + + + @SuppressWarnings("unused") + private static class ConstructorDependencyTestBean { + + private String name; + + public ConstructorDependencyTestBean(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserScopedProxyTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserScopedProxyTests.java new file mode 100644 index 0000000..e498e94 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserScopedProxyTests.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import example.scannable.FooService; +import example.scannable.ScopedProxyTestBean; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.context.testfixture.SimpleMapScope; +import org.springframework.core.testfixture.io.SerializationTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Mark Fisher + * @author Juergen Hoeller + * @author Sam Brannen + */ +public class ComponentScanParserScopedProxyTests { + + @Test + public void testDefaultScopedProxy() { + ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( + "org/springframework/context/annotation/scopedProxyDefaultTests.xml"); + context.getBeanFactory().registerScope("myScope", new SimpleMapScope()); + + ScopedProxyTestBean bean = (ScopedProxyTestBean) context.getBean("scopedProxyTestBean"); + // should not be a proxy + assertThat(AopUtils.isAopProxy(bean)).isFalse(); + context.close(); + } + + @Test + public void testNoScopedProxy() { + ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( + "org/springframework/context/annotation/scopedProxyNoTests.xml"); + context.getBeanFactory().registerScope("myScope", new SimpleMapScope()); + + ScopedProxyTestBean bean = (ScopedProxyTestBean) context.getBean("scopedProxyTestBean"); + // should not be a proxy + assertThat(AopUtils.isAopProxy(bean)).isFalse(); + context.close(); + } + + @Test + public void testInterfacesScopedProxy() throws Exception { + ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( + "org/springframework/context/annotation/scopedProxyInterfacesTests.xml"); + context.getBeanFactory().registerScope("myScope", new SimpleMapScope()); + + // should cast to the interface + FooService bean = (FooService) context.getBean("scopedProxyTestBean"); + // should be dynamic proxy + assertThat(AopUtils.isJdkDynamicProxy(bean)).isTrue(); + // test serializability + assertThat(bean.foo(1)).isEqualTo("bar"); + FooService deserialized = SerializationTestUtils.serializeAndDeserialize(bean); + assertThat(deserialized).isNotNull(); + assertThat(deserialized.foo(1)).isEqualTo("bar"); + context.close(); + } + + @Test + public void testTargetClassScopedProxy() throws Exception { + ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( + "org/springframework/context/annotation/scopedProxyTargetClassTests.xml"); + context.getBeanFactory().registerScope("myScope", new SimpleMapScope()); + + ScopedProxyTestBean bean = (ScopedProxyTestBean) context.getBean("scopedProxyTestBean"); + // should be a class-based proxy + assertThat(AopUtils.isCglibProxy(bean)).isTrue(); + // test serializability + assertThat(bean.foo(1)).isEqualTo("bar"); + ScopedProxyTestBean deserialized = SerializationTestUtils.serializeAndDeserialize(bean); + assertThat(deserialized).isNotNull(); + assertThat(deserialized.foo(1)).isEqualTo("bar"); + context.close(); + } + + @Test + @SuppressWarnings("resource") + public void testInvalidConfigScopedProxy() throws Exception { + assertThatExceptionOfType(BeanDefinitionParsingException.class).isThrownBy(() -> + new ClassPathXmlApplicationContext("org/springframework/context/annotation/scopedProxyInvalidConfigTests.xml")) + .withMessageContaining("Cannot define both 'scope-resolver' and 'scoped-proxy' on tag") + .withMessageContaining("Offending resource: class path resource [org/springframework/context/annotation/scopedProxyInvalidConfigTests.xml]"); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserTests.java new file mode 100644 index 0000000..9bacf08 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserTests.java @@ -0,0 +1,190 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import example.profilescan.ProfileAnnotatedComponent; +import example.scannable.AutowiredQualifierFooService; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.context.support.GenericXmlApplicationContext; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.TypeFilter; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Mark Fisher + * @author Juergen Hoeller + * @author Chris Beams + * @author Sam Brannen + */ +public class ComponentScanParserTests { + + private ClassPathXmlApplicationContext loadContext(String path) { + return new ClassPathXmlApplicationContext(path, getClass()); + } + + + @Test + public void aspectjTypeFilter() { + ClassPathXmlApplicationContext context = loadContext("aspectjTypeFilterTests.xml"); + assertThat(context.containsBean("fooServiceImpl")).isTrue(); + assertThat(context.containsBean("stubFooDao")).isTrue(); + assertThat(context.containsBean("scopedProxyTestBean")).isFalse(); + context.close(); + } + + @Test + public void aspectjTypeFilterWithPlaceholders() { + System.setProperty("basePackage", "example.scannable, test"); + System.setProperty("scanInclude", "example.scannable.FooService+"); + System.setProperty("scanExclude", "example..Scoped*Test*"); + try { + ClassPathXmlApplicationContext context = loadContext("aspectjTypeFilterTestsWithPlaceholders.xml"); + assertThat(context.containsBean("fooServiceImpl")).isTrue(); + assertThat(context.containsBean("stubFooDao")).isTrue(); + assertThat(context.containsBean("scopedProxyTestBean")).isFalse(); + context.close(); + } + finally { + System.clearProperty("basePackage"); + System.clearProperty("scanInclude"); + System.clearProperty("scanExclude"); + } + } + + @Test + public void nonMatchingResourcePattern() { + ClassPathXmlApplicationContext context = loadContext("nonMatchingResourcePatternTests.xml"); + assertThat(context.containsBean("fooServiceImpl")).isFalse(); + context.close(); + } + + @Test + public void matchingResourcePattern() { + ClassPathXmlApplicationContext context = loadContext("matchingResourcePatternTests.xml"); + assertThat(context.containsBean("fooServiceImpl")).isTrue(); + context.close(); + } + + @Test + public void componentScanWithAutowiredQualifier() { + ClassPathXmlApplicationContext context = loadContext("componentScanWithAutowiredQualifierTests.xml"); + AutowiredQualifierFooService fooService = (AutowiredQualifierFooService) context.getBean("fooService"); + assertThat(fooService.isInitCalled()).isTrue(); + assertThat(fooService.foo(123)).isEqualTo("bar"); + context.close(); + } + + @Test + public void customAnnotationUsedForBothComponentScanAndQualifier() { + ClassPathXmlApplicationContext context = loadContext("customAnnotationUsedForBothComponentScanAndQualifierTests.xml"); + KustomAnnotationAutowiredBean testBean = (KustomAnnotationAutowiredBean) context.getBean("testBean"); + assertThat(testBean.getDependency()).isNotNull(); + context.close(); + } + + @Test + public void customTypeFilter() { + ClassPathXmlApplicationContext context = loadContext("customTypeFilterTests.xml"); + KustomAnnotationAutowiredBean testBean = (KustomAnnotationAutowiredBean) context.getBean("testBean"); + assertThat(testBean.getDependency()).isNotNull(); + context.close(); + } + + @Test + public void componentScanRespectsProfileAnnotation() { + String xmlLocation = "org/springframework/context/annotation/componentScanRespectsProfileAnnotationTests.xml"; + { // should exclude the profile-annotated bean if active profiles remains unset + GenericXmlApplicationContext context = new GenericXmlApplicationContext(); + context.load(xmlLocation); + context.refresh(); + assertThat(context.containsBean(ProfileAnnotatedComponent.BEAN_NAME)).isFalse(); + context.close(); + } + { // should include the profile-annotated bean with active profiles set + GenericXmlApplicationContext context = new GenericXmlApplicationContext(); + context.getEnvironment().setActiveProfiles(ProfileAnnotatedComponent.PROFILE_NAME); + context.load(xmlLocation); + context.refresh(); + assertThat(context.containsBean(ProfileAnnotatedComponent.BEAN_NAME)).isTrue(); + context.close(); + } + { // ensure the same works for AbstractRefreshableApplicationContext impls too + ConfigurableApplicationContext context = new ClassPathXmlApplicationContext(new String[] { xmlLocation }, + false); + context.getEnvironment().setActiveProfiles(ProfileAnnotatedComponent.PROFILE_NAME); + context.refresh(); + assertThat(context.containsBean(ProfileAnnotatedComponent.BEAN_NAME)).isTrue(); + context.close(); + } + } + + + @Target({ElementType.TYPE, ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + public @interface CustomAnnotation { + } + + + /** + * Intentionally spelling "custom" with a "k" since there are numerous + * classes in this package named *Custom*. + */ + public static class KustomAnnotationAutowiredBean { + + @Autowired + @CustomAnnotation + private KustomAnnotationDependencyBean dependency; + + public KustomAnnotationDependencyBean getDependency() { + return this.dependency; + } + } + + + /** + * Intentionally spelling "custom" with a "k" since there are numerous + * classes in this package named *Custom*. + */ + @CustomAnnotation + public static class KustomAnnotationDependencyBean { + } + + + public static class CustomTypeFilter implements TypeFilter { + + /** + * Intentionally spelling "custom" with a "k" since there are numerous + * classes in this package named *Custom*. + */ + @Override + public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) { + return metadataReader.getClassMetadata().getClassName().contains("Kustom"); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserWithUserDefinedStrategiesTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserWithUserDefinedStrategiesTests.java new file mode 100644 index 0000000..ef8bc04 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanParserWithUserDefinedStrategiesTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Mark Fisher + */ +public class ComponentScanParserWithUserDefinedStrategiesTests { + + @Test + public void testCustomBeanNameGenerator() { + ApplicationContext context = new ClassPathXmlApplicationContext( + "org/springframework/context/annotation/customNameGeneratorTests.xml"); + assertThat(context.containsBean("testing.fooServiceImpl")).isTrue(); + } + + @Test + public void testCustomScopeMetadataResolver() { + ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( + "org/springframework/context/annotation/customScopeResolverTests.xml"); + BeanDefinition bd = context.getBeanFactory().getBeanDefinition("fooServiceImpl"); + assertThat(bd.getScope()).isEqualTo("myCustomScope"); + assertThat(bd.isSingleton()).isFalse(); + } + + @Test + public void testInvalidConstructorBeanNameGenerator() { + assertThatExceptionOfType(BeansException.class).isThrownBy(() -> + new ClassPathXmlApplicationContext( + "org/springframework/context/annotation/invalidConstructorNameGeneratorTests.xml")); + } + + @Test + public void testInvalidClassNameScopeMetadataResolver() { + assertThatExceptionOfType(BeansException.class).isThrownBy(() -> + new ClassPathXmlApplicationContext( + "org/springframework/context/annotation/invalidClassNameScopeResolverTests.xml")); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassAndBFPPTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassAndBFPPTests.java new file mode 100644 index 0000000..21324a7 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassAndBFPPTests.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Tests semantics of declaring {@link BeanFactoryPostProcessor}-returning @Bean + * methods, specifically as regards static @Bean methods and the avoidance of + * container lifecycle issues when BFPPs are in the mix. + * + * @author Chris Beams + * @since 3.1 + */ +public class ConfigurationClassAndBFPPTests { + + @Test + public void autowiringFailsWithBFPPAsInstanceMethod() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(TestBeanConfig.class, AutowiredConfigWithBFPPAsInstanceMethod.class); + ctx.refresh(); + // instance method BFPP interferes with lifecycle -> autowiring fails! + // WARN-level logging should have been issued about returning BFPP from non-static @Bean method + assertThat(ctx.getBean(AutowiredConfigWithBFPPAsInstanceMethod.class).autowiredTestBean).isNull(); + } + + @Test + public void autowiringSucceedsWithBFPPAsStaticMethod() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(TestBeanConfig.class, AutowiredConfigWithBFPPAsStaticMethod.class); + ctx.refresh(); + // static method BFPP does not interfere with lifecycle -> autowiring succeeds + assertThat(ctx.getBean(AutowiredConfigWithBFPPAsStaticMethod.class).autowiredTestBean).isNotNull(); + } + + + @Configuration + static class TestBeanConfig { + @Bean + public TestBean testBean() { + return new TestBean(); + } + } + + + @Configuration + static class AutowiredConfigWithBFPPAsInstanceMethod { + @Autowired TestBean autowiredTestBean; + + @Bean + public BeanFactoryPostProcessor bfpp() { + return new BeanFactoryPostProcessor() { + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + // no-op + } + }; + } + } + + + @Configuration + static class AutowiredConfigWithBFPPAsStaticMethod { + @Autowired TestBean autowiredTestBean; + + @Bean + public static final BeanFactoryPostProcessor bfpp() { + return new BeanFactoryPostProcessor() { + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + // no-op + } + }; + } + } + + + @Test + public void staticBeanMethodsDoNotRespectScoping() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConfigWithStaticBeanMethod.class); + ctx.refresh(); + assertThat(ConfigWithStaticBeanMethod.testBean()).isNotSameAs(ConfigWithStaticBeanMethod.testBean()); + } + + + @Configuration + static class ConfigWithStaticBeanMethod { + @Bean + public static TestBean testBean() { + return new TestBean("foo"); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostConstructAndAutowiringTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostConstructAndAutowiringTests.java new file mode 100644 index 0000000..0aaf4c0 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostConstructAndAutowiringTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import javax.annotation.PostConstruct; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Tests cornering the issue reported in SPR-8080. If the product of a @Bean method + * was @Autowired into a configuration class while at the same time the declaring + * configuration class for the @Bean method in question has a @PostConstruct + * (or other initializer) method, the container would become confused about the + * 'currently in creation' status of the autowired bean and result in creating multiple + * instances of the given @Bean, violating container scoping / singleton semantics. + * + * This is resolved through no longer relying on 'currently in creation' status, but + * rather on a thread local that informs the enhanced bean method implementation whether + * the factory is the caller or not. + * + * @author Chris Beams + * @since 3.1 + */ +public class ConfigurationClassPostConstructAndAutowiringTests { + + /** + * Prior to the fix for SPR-8080, this method would succeed due to ordering of + * configuration class registration. + */ + @Test + public void control() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(Config1.class, Config2.class); + ctx.refresh(); + + assertions(ctx); + + Config2 config2 = ctx.getBean(Config2.class); + assertThat(config2.testBean).isEqualTo(ctx.getBean(TestBean.class)); + } + + /** + * Prior to the fix for SPR-8080, this method would fail due to ordering of + * configuration class registration. + */ + @Test + public void originalReproCase() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(Config2.class, Config1.class); + ctx.refresh(); + + assertions(ctx); + } + + private void assertions(AnnotationConfigApplicationContext ctx) { + Config1 config1 = ctx.getBean(Config1.class); + TestBean testBean = ctx.getBean(TestBean.class); + assertThat(config1.beanMethodCallCount).isEqualTo(1); + assertThat(testBean.getAge()).isEqualTo(2); + } + + + @Configuration + static class Config1 { + + int beanMethodCallCount = 0; + + @PostConstruct + public void init() { + beanMethod().setAge(beanMethod().getAge() + 1); // age == 2 + } + + @Bean + public TestBean beanMethod() { + beanMethodCallCount++; + TestBean testBean = new TestBean(); + testBean.setAge(1); + return testBean; + } + } + + + @Configuration + static class Config2 { + + TestBean testBean; + + @Autowired + void setTestBean(TestBean testBean) { + this.testBean = testBean; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java new file mode 100644 index 0000000..3b1f1a7 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java @@ -0,0 +1,2022 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.List; +import java.util.Map; + +import javax.annotation.PostConstruct; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; +import org.springframework.aop.interceptor.SimpleTraceInterceptor; +import org.springframework.aop.scope.ScopedObject; +import org.springframework.aop.scope.ScopedProxyUtils; +import org.springframework.aop.support.AopUtils; +import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor; +import org.springframework.beans.factory.annotation.Lookup; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.QualifierAnnotationAutowireCandidateResolver; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.beans.factory.support.ChildBeanDefinition; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.componentscan.simple.SimpleComponent; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.DescriptiveResource; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.core.task.SyncTaskExecutor; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * @author Chris Beams + * @author Juergen Hoeller + * @author Sam Brannen + */ +class ConfigurationClassPostProcessorTests { + + private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + + @BeforeEach + void setup() { + QualifierAnnotationAutowireCandidateResolver acr = new QualifierAnnotationAutowireCandidateResolver(); + acr.setBeanFactory(this.beanFactory); + this.beanFactory.setAutowireCandidateResolver(acr); + } + + + /** + * Enhanced {@link Configuration} classes are only necessary for respecting + * certain bean semantics, like singleton-scoping, scoped proxies, etc. + *

    Technically, {@link ConfigurationClassPostProcessor} could fail to enhance the + * registered Configuration classes and many use cases would still work. + * Certain cases, however, like inter-bean singleton references would not. + * We test for such a case below, and in doing so prove that enhancement is working. + */ + @Test + void enhancementIsPresentBecauseSingletonSemanticsAreRespected() { + beanFactory.registerBeanDefinition("config", new RootBeanDefinition(SingletonBeanConfig.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).hasBeanClass()).isTrue(); + Foo foo = beanFactory.getBean("foo", Foo.class); + Bar bar = beanFactory.getBean("bar", Bar.class); + assertThat(bar.foo).isSameAs(foo); + assertThat(beanFactory.getDependentBeans("foo")).contains("bar"); + assertThat(beanFactory.getDependentBeans("config")).contains("foo"); + assertThat(beanFactory.getDependentBeans("config")).contains("bar"); + } + + @Test + void enhancementIsPresentBecauseSingletonSemanticsAreRespectedUsingAsm() { + beanFactory.registerBeanDefinition("config", new RootBeanDefinition(SingletonBeanConfig.class.getName())); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).hasBeanClass()).isTrue(); + Foo foo = beanFactory.getBean("foo", Foo.class); + Bar bar = beanFactory.getBean("bar", Bar.class); + assertThat(bar.foo).isSameAs(foo); + assertThat(beanFactory.getDependentBeans("foo")).contains("bar"); + assertThat(beanFactory.getDependentBeans("config")).contains("foo"); + assertThat(beanFactory.getDependentBeans("config")).contains("bar"); + } + + @Test + void enhancementIsNotPresentForProxyBeanMethodsFlagSetToFalse() { + beanFactory.registerBeanDefinition("config", new RootBeanDefinition(NonEnhancedSingletonBeanConfig.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).hasBeanClass()).isTrue(); + Foo foo = beanFactory.getBean("foo", Foo.class); + Bar bar = beanFactory.getBean("bar", Bar.class); + assertThat(bar.foo).isNotSameAs(foo); + } + + @Test + void enhancementIsNotPresentForProxyBeanMethodsFlagSetToFalseUsingAsm() { + beanFactory.registerBeanDefinition("config", new RootBeanDefinition(NonEnhancedSingletonBeanConfig.class.getName())); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).hasBeanClass()).isTrue(); + Foo foo = beanFactory.getBean("foo", Foo.class); + Bar bar = beanFactory.getBean("bar", Bar.class); + assertThat(bar.foo).isNotSameAs(foo); + } + + @Test + void enhancementIsNotPresentForStaticMethods() { + beanFactory.registerBeanDefinition("config", new RootBeanDefinition(StaticSingletonBeanConfig.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).hasBeanClass()).isTrue(); + assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("foo")).hasBeanClass()).isTrue(); + assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("bar")).hasBeanClass()).isTrue(); + Foo foo = beanFactory.getBean("foo", Foo.class); + Bar bar = beanFactory.getBean("bar", Bar.class); + assertThat(bar.foo).isNotSameAs(foo); + } + + @Test + void enhancementIsNotPresentForStaticMethodsUsingAsm() { + beanFactory.registerBeanDefinition("config", new RootBeanDefinition(StaticSingletonBeanConfig.class.getName())); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).hasBeanClass()).isTrue(); + assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("foo")).hasBeanClass()).isTrue(); + assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("bar")).hasBeanClass()).isTrue(); + Foo foo = beanFactory.getBean("foo", Foo.class); + Bar bar = beanFactory.getBean("bar", Bar.class); + assertThat(bar.foo).isNotSameAs(foo); + } + + @Test + void configurationIntrospectionOfInnerClassesWorksWithDotNameSyntax() { + beanFactory.registerBeanDefinition("config", new RootBeanDefinition(getClass().getName() + ".SingletonBeanConfig")); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + Foo foo = beanFactory.getBean("foo", Foo.class); + Bar bar = beanFactory.getBean("bar", Bar.class); + assertThat(bar.foo).isSameAs(foo); + } + + /** + * Tests the fix for SPR-5655, a special workaround that prefers reflection over ASM + * if a bean class is already loaded. + */ + @Test + void alreadyLoadedConfigurationClasses() { + beanFactory.registerBeanDefinition("unloadedConfig", new RootBeanDefinition(UnloadedConfig.class.getName())); + beanFactory.registerBeanDefinition("loadedConfig", new RootBeanDefinition(LoadedConfig.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + beanFactory.getBean("foo"); + beanFactory.getBean("bar"); + } + + /** + * Tests whether a bean definition without a specified bean class is handled correctly. + */ + @Test + void postProcessorIntrospectsInheritedDefinitionsCorrectly() { + beanFactory.registerBeanDefinition("config", new RootBeanDefinition(SingletonBeanConfig.class)); + beanFactory.registerBeanDefinition("parent", new RootBeanDefinition(TestBean.class)); + beanFactory.registerBeanDefinition("child", new ChildBeanDefinition("parent")); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + Foo foo = beanFactory.getBean("foo", Foo.class); + Bar bar = beanFactory.getBean("bar", Bar.class); + assertThat(bar.foo).isSameAs(foo); + } + + @Test + void postProcessorWorksWithComposedConfigurationUsingReflection() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(ComposedConfigurationClass.class); + assertSupportForComposedAnnotation(beanDefinition); + } + + @Test + void postProcessorWorksWithComposedConfigurationUsingAsm() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(ComposedConfigurationClass.class.getName()); + assertSupportForComposedAnnotation(beanDefinition); + } + + @Test + void postProcessorWorksWithComposedConfigurationWithAttributeOverrideForBasePackageUsingReflection() { + RootBeanDefinition beanDefinition = new RootBeanDefinition( + ComposedConfigurationWithAttributeOverrideForBasePackage.class); + assertSupportForComposedAnnotation(beanDefinition); + } + + @Test + void postProcessorWorksWithComposedConfigurationWithAttributeOverrideForBasePackageUsingAsm() { + RootBeanDefinition beanDefinition = new RootBeanDefinition( + ComposedConfigurationWithAttributeOverrideForBasePackage.class.getName()); + assertSupportForComposedAnnotation(beanDefinition); + } + + @Test + void postProcessorWorksWithComposedConfigurationWithAttributeOverrideForExcludeFilterUsingReflection() { + RootBeanDefinition beanDefinition = new RootBeanDefinition( + ComposedConfigurationWithAttributeOverrideForExcludeFilter.class); + assertSupportForComposedAnnotationWithExclude(beanDefinition); + } + + @Test + void postProcessorWorksWithComposedConfigurationWithAttributeOverrideForExcludeFilterUsingAsm() { + RootBeanDefinition beanDefinition = new RootBeanDefinition( + ComposedConfigurationWithAttributeOverrideForExcludeFilter.class.getName()); + assertSupportForComposedAnnotationWithExclude(beanDefinition); + } + + @Test + void postProcessorWorksWithExtendedConfigurationWithAttributeOverrideForExcludesFilterUsingReflection() { + RootBeanDefinition beanDefinition = new RootBeanDefinition( + ExtendedConfigurationWithAttributeOverrideForExcludeFilter.class); + assertSupportForComposedAnnotationWithExclude(beanDefinition); + } + + @Test + void postProcessorWorksWithExtendedConfigurationWithAttributeOverrideForExcludesFilterUsingAsm() { + RootBeanDefinition beanDefinition = new RootBeanDefinition( + ExtendedConfigurationWithAttributeOverrideForExcludeFilter.class.getName()); + assertSupportForComposedAnnotationWithExclude(beanDefinition); + } + + @Test + void postProcessorWorksWithComposedComposedConfigurationWithAttributeOverridesUsingReflection() { + RootBeanDefinition beanDefinition = new RootBeanDefinition( + ComposedComposedConfigurationWithAttributeOverridesClass.class); + assertSupportForComposedAnnotation(beanDefinition); + } + + @Test + void postProcessorWorksWithComposedComposedConfigurationWithAttributeOverridesUsingAsm() { + RootBeanDefinition beanDefinition = new RootBeanDefinition( + ComposedComposedConfigurationWithAttributeOverridesClass.class.getName()); + assertSupportForComposedAnnotation(beanDefinition); + } + + @Test + void postProcessorWorksWithMetaComponentScanConfigurationWithAttributeOverridesUsingReflection() { + RootBeanDefinition beanDefinition = new RootBeanDefinition( + MetaComponentScanConfigurationWithAttributeOverridesClass.class); + assertSupportForComposedAnnotation(beanDefinition); + } + + @Test + void postProcessorWorksWithMetaComponentScanConfigurationWithAttributeOverridesUsingAsm() { + RootBeanDefinition beanDefinition = new RootBeanDefinition( + MetaComponentScanConfigurationWithAttributeOverridesClass.class.getName()); + assertSupportForComposedAnnotation(beanDefinition); + } + + @Test + void postProcessorWorksWithMetaComponentScanConfigurationWithAttributeOverridesSubclassUsingReflection() { + RootBeanDefinition beanDefinition = new RootBeanDefinition( + SubMetaComponentScanConfigurationWithAttributeOverridesClass.class); + assertSupportForComposedAnnotation(beanDefinition); + } + + @Test + void postProcessorWorksWithMetaComponentScanConfigurationWithAttributeOverridesSubclassUsingAsm() { + RootBeanDefinition beanDefinition = new RootBeanDefinition( + SubMetaComponentScanConfigurationWithAttributeOverridesClass.class.getName()); + assertSupportForComposedAnnotation(beanDefinition); + } + + private void assertSupportForComposedAnnotation(RootBeanDefinition beanDefinition) { + beanFactory.registerBeanDefinition("config", beanDefinition); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.setEnvironment(new StandardEnvironment()); + pp.postProcessBeanFactory(beanFactory); + SimpleComponent simpleComponent = beanFactory.getBean(SimpleComponent.class); + assertThat(simpleComponent).isNotNull(); + } + + private void assertSupportForComposedAnnotationWithExclude(RootBeanDefinition beanDefinition) { + beanFactory.registerBeanDefinition("config", beanDefinition); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.setEnvironment(new StandardEnvironment()); + pp.postProcessBeanFactory(beanFactory); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> + beanFactory.getBean(SimpleComponent.class)); + } + + @Test + void postProcessorOverridesNonApplicationBeanDefinitions() { + RootBeanDefinition rbd = new RootBeanDefinition(TestBean.class); + rbd.setRole(RootBeanDefinition.ROLE_SUPPORT); + beanFactory.registerBeanDefinition("bar", rbd); + beanFactory.registerBeanDefinition("config", new RootBeanDefinition(SingletonBeanConfig.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + Foo foo = beanFactory.getBean("foo", Foo.class); + Bar bar = beanFactory.getBean("bar", Bar.class); + assertThat(bar.foo).isSameAs(foo); + } + + @Test + void postProcessorDoesNotOverrideRegularBeanDefinitions() { + RootBeanDefinition rbd = new RootBeanDefinition(TestBean.class); + rbd.setResource(new DescriptiveResource("XML or something")); + beanFactory.registerBeanDefinition("bar", rbd); + beanFactory.registerBeanDefinition("config", new RootBeanDefinition(SingletonBeanConfig.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + beanFactory.getBean("foo", Foo.class); + beanFactory.getBean("bar", TestBean.class); + } + + @Test + void postProcessorDoesNotOverrideRegularBeanDefinitionsEvenWithScopedProxy() { + RootBeanDefinition rbd = new RootBeanDefinition(TestBean.class); + rbd.setResource(new DescriptiveResource("XML or something")); + BeanDefinitionHolder proxied = ScopedProxyUtils.createScopedProxy( + new BeanDefinitionHolder(rbd, "bar"), beanFactory, true); + beanFactory.registerBeanDefinition("bar", proxied.getBeanDefinition()); + beanFactory.registerBeanDefinition("config", new RootBeanDefinition(SingletonBeanConfig.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + beanFactory.getBean("foo", Foo.class); + beanFactory.getBean("bar", TestBean.class); + } + + @Test + void postProcessorFailsOnImplicitOverrideIfOverridingIsNotAllowed() { + RootBeanDefinition rbd = new RootBeanDefinition(TestBean.class); + rbd.setResource(new DescriptiveResource("XML or something")); + beanFactory.registerBeanDefinition("bar", rbd); + beanFactory.registerBeanDefinition("config", new RootBeanDefinition(SingletonBeanConfig.class)); + beanFactory.setAllowBeanDefinitionOverriding(false); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> + pp.postProcessBeanFactory(beanFactory)) + .withMessageContaining("bar") + .withMessageContaining("SingletonBeanConfig") + .withMessageContaining(TestBean.class.getName()); + } + + @Test // gh-25430 + void detectAliasOverride() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory(); + beanFactory.setAllowBeanDefinitionOverriding(false); + context.register(FirstConfiguration.class, SecondConfiguration.class); + assertThatIllegalStateException().isThrownBy(context::refresh) + .withMessageContaining("alias 'taskExecutor'") + .withMessageContaining("name 'applicationTaskExecutor'") + .withMessageContaining("bean definition 'taskExecutor'"); + } + + @Test + void configurationClassesProcessedInCorrectOrder() { + beanFactory.registerBeanDefinition("config1", new RootBeanDefinition(OverridingSingletonBeanConfig.class)); + beanFactory.registerBeanDefinition("config2", new RootBeanDefinition(SingletonBeanConfig.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + + Foo foo = beanFactory.getBean(Foo.class); + boolean condition = foo instanceof ExtendedFoo; + assertThat(condition).isTrue(); + Bar bar = beanFactory.getBean(Bar.class); + assertThat(bar.foo).isSameAs(foo); + } + + @Test + void configurationClassesWithValidOverridingForProgrammaticCall() { + beanFactory.registerBeanDefinition("config1", new RootBeanDefinition(OverridingAgainSingletonBeanConfig.class)); + beanFactory.registerBeanDefinition("config2", new RootBeanDefinition(OverridingSingletonBeanConfig.class)); + beanFactory.registerBeanDefinition("config3", new RootBeanDefinition(SingletonBeanConfig.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + + Foo foo = beanFactory.getBean(Foo.class); + boolean condition = foo instanceof ExtendedAgainFoo; + assertThat(condition).isTrue(); + Bar bar = beanFactory.getBean(Bar.class); + assertThat(bar.foo).isSameAs(foo); + } + + @Test + void configurationClassesWithInvalidOverridingForProgrammaticCall() { + beanFactory.registerBeanDefinition("config1", new RootBeanDefinition(InvalidOverridingSingletonBeanConfig.class)); + beanFactory.registerBeanDefinition("config2", new RootBeanDefinition(OverridingSingletonBeanConfig.class)); + beanFactory.registerBeanDefinition("config3", new RootBeanDefinition(SingletonBeanConfig.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + beanFactory.getBean(Bar.class)) + .withMessageContaining("OverridingSingletonBeanConfig.foo") + .withMessageContaining(ExtendedFoo.class.getName()) + .withMessageContaining(Foo.class.getName()) + .withMessageContaining("InvalidOverridingSingletonBeanConfig"); + } + + @Test // SPR-15384 + void nestedConfigurationClassesProcessedInCorrectOrder() { + beanFactory.registerBeanDefinition("config", new RootBeanDefinition(ConfigWithOrderedNestedClasses.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + + Foo foo = beanFactory.getBean(Foo.class); + boolean condition = foo instanceof ExtendedFoo; + assertThat(condition).isTrue(); + Bar bar = beanFactory.getBean(Bar.class); + assertThat(bar.foo).isSameAs(foo); + } + + @Test // SPR-16734 + void innerConfigurationClassesProcessedInCorrectOrder() { + beanFactory.registerBeanDefinition("config", new RootBeanDefinition(ConfigWithOrderedInnerClasses.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + beanFactory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor()); + + Foo foo = beanFactory.getBean(Foo.class); + boolean condition = foo instanceof ExtendedFoo; + assertThat(condition).isTrue(); + Bar bar = beanFactory.getBean(Bar.class); + assertThat(bar.foo).isSameAs(foo); + } + + @Test + void scopedProxyTargetMarkedAsNonAutowireCandidate() { + AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); + bpp.setBeanFactory(beanFactory); + beanFactory.addBeanPostProcessor(bpp); + beanFactory.registerBeanDefinition("config", new RootBeanDefinition(ScopedProxyConfigurationClass.class)); + beanFactory.registerBeanDefinition("consumer", new RootBeanDefinition(ScopedProxyConsumer.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + + ITestBean injected = beanFactory.getBean("consumer", ScopedProxyConsumer.class).testBean; + boolean condition = injected instanceof ScopedObject; + assertThat(condition).isTrue(); + assertThat(injected).isSameAs(beanFactory.getBean("scopedClass")); + assertThat(injected).isSameAs(beanFactory.getBean(ITestBean.class)); + } + + @Test + void processingAllowedOnlyOncePerProcessorRegistryPair() { + DefaultListableBeanFactory bf1 = new DefaultListableBeanFactory(); + DefaultListableBeanFactory bf2 = new DefaultListableBeanFactory(); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(bf1); // first invocation -- should succeed + assertThatIllegalStateException().isThrownBy(() -> + pp.postProcessBeanFactory(bf1)); // second invocation for bf1 -- should throw + pp.postProcessBeanFactory(bf2); // first invocation for bf2 -- should succeed + assertThatIllegalStateException().isThrownBy(() -> + pp.postProcessBeanFactory(bf2)); // second invocation for bf2 -- should throw + } + + @Test + void genericsBasedInjection() { + AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); + bpp.setBeanFactory(beanFactory); + beanFactory.addBeanPostProcessor(bpp); + RootBeanDefinition bd = new RootBeanDefinition(RepositoryInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + beanFactory.registerBeanDefinition("annotatedBean", bd); + beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RepositoryConfiguration.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + + RepositoryInjectionBean bean = (RepositoryInjectionBean) beanFactory.getBean("annotatedBean"); + assertThat(bean.stringRepository.toString()).isEqualTo("Repository"); + assertThat(bean.integerRepository.toString()).isEqualTo("Repository"); + } + + @Test + void genericsBasedInjectionWithScoped() { + AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); + bpp.setBeanFactory(beanFactory); + beanFactory.addBeanPostProcessor(bpp); + RootBeanDefinition bd = new RootBeanDefinition(RepositoryInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + beanFactory.registerBeanDefinition("annotatedBean", bd); + beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(ScopedRepositoryConfiguration.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + + RepositoryInjectionBean bean = (RepositoryInjectionBean) beanFactory.getBean("annotatedBean"); + assertThat(bean.stringRepository.toString()).isEqualTo("Repository"); + assertThat(bean.integerRepository.toString()).isEqualTo("Repository"); + } + + @Test + void genericsBasedInjectionWithScopedProxy() { + AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); + bpp.setBeanFactory(beanFactory); + beanFactory.addBeanPostProcessor(bpp); + RootBeanDefinition bd = new RootBeanDefinition(RepositoryInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + beanFactory.registerBeanDefinition("annotatedBean", bd); + beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(ScopedProxyRepositoryConfiguration.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + beanFactory.freezeConfiguration(); + + RepositoryInjectionBean bean = (RepositoryInjectionBean) beanFactory.getBean("annotatedBean"); + assertThat(bean.stringRepository.toString()).isEqualTo("Repository"); + assertThat(bean.integerRepository.toString()).isEqualTo("Repository"); + assertThat(AopUtils.isCglibProxy(bean.stringRepository)).isTrue(); + assertThat(AopUtils.isCglibProxy(bean.integerRepository)).isTrue(); + } + + @Test + void genericsBasedInjectionWithScopedProxyUsingAsm() { + AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); + bpp.setBeanFactory(beanFactory); + beanFactory.addBeanPostProcessor(bpp); + RootBeanDefinition bd = new RootBeanDefinition(RepositoryInjectionBean.class.getName()); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + beanFactory.registerBeanDefinition("annotatedBean", bd); + beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(ScopedProxyRepositoryConfiguration.class.getName())); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + beanFactory.freezeConfiguration(); + + RepositoryInjectionBean bean = (RepositoryInjectionBean) beanFactory.getBean("annotatedBean"); + assertThat(bean.stringRepository.toString()).isEqualTo("Repository"); + assertThat(bean.integerRepository.toString()).isEqualTo("Repository"); + assertThat(AopUtils.isCglibProxy(bean.stringRepository)).isTrue(); + assertThat(AopUtils.isCglibProxy(bean.integerRepository)).isTrue(); + } + + @Test + void genericsBasedInjectionWithImplTypeAtInjectionPoint() { + AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); + bpp.setBeanFactory(beanFactory); + beanFactory.addBeanPostProcessor(bpp); + RootBeanDefinition bd = new RootBeanDefinition(SpecificRepositoryInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + beanFactory.registerBeanDefinition("annotatedBean", bd); + beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(SpecificRepositoryConfiguration.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + beanFactory.preInstantiateSingletons(); + + SpecificRepositoryInjectionBean bean = (SpecificRepositoryInjectionBean) beanFactory.getBean("annotatedBean"); + assertThat(bean.genericRepository).isSameAs(beanFactory.getBean("genericRepo")); + } + + @Test + void genericsBasedInjectionWithFactoryBean() { + AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); + bpp.setBeanFactory(beanFactory); + beanFactory.addBeanPostProcessor(bpp); + RootBeanDefinition bd = new RootBeanDefinition(RepositoryFactoryBeanInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + beanFactory.registerBeanDefinition("annotatedBean", bd); + beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RepositoryFactoryBeanConfiguration.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + beanFactory.preInstantiateSingletons(); + + RepositoryFactoryBeanInjectionBean bean = (RepositoryFactoryBeanInjectionBean) beanFactory.getBean("annotatedBean"); + assertThat(bean.repositoryFactoryBean).isSameAs(beanFactory.getBean("&repoFactoryBean")); + assertThat(bean.qualifiedRepositoryFactoryBean).isSameAs(beanFactory.getBean("&repoFactoryBean")); + assertThat(bean.prefixQualifiedRepositoryFactoryBean).isSameAs(beanFactory.getBean("&repoFactoryBean")); + } + + @Test + void genericsBasedInjectionWithRawMatch() { + beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RawMatchingConfiguration.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + + assertThat(beanFactory.getBean("repoConsumer")).isSameAs(beanFactory.getBean("rawRepo")); + } + + @Test + void genericsBasedInjectionWithWildcardMatch() { + beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(WildcardMatchingConfiguration.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + + assertThat(beanFactory.getBean("repoConsumer")).isSameAs(beanFactory.getBean("genericRepo")); + } + + @Test + void genericsBasedInjectionWithWildcardWithExtendsMatch() { + beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(WildcardWithExtendsConfiguration.class)); + new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); + + assertThat(beanFactory.getBean("repoConsumer")).isSameAs(beanFactory.getBean("stringRepo")); + } + + @Test + void genericsBasedInjectionWithWildcardWithGenericExtendsMatch() { + beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(WildcardWithGenericExtendsConfiguration.class)); + new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); + + assertThat(beanFactory.getBean("repoConsumer")).isSameAs(beanFactory.getBean("genericRepo")); + } + + @Test + void genericsBasedInjectionWithEarlyGenericsMatching() { + beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RepositoryConfiguration.class)); + new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); + + String[] beanNames = beanFactory.getBeanNamesForType(Repository.class); + assertThat(beanNames).contains("stringRepo"); + + beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class)); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("stringRepo"); + + beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class)); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("stringRepo"); + } + + @Test + void genericsBasedInjectionWithLateGenericsMatching() { + beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RepositoryConfiguration.class)); + new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); + beanFactory.preInstantiateSingletons(); + + String[] beanNames = beanFactory.getBeanNamesForType(Repository.class); + assertThat(beanNames).contains("stringRepo"); + + beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class)); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("stringRepo"); + + beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class)); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("stringRepo"); + } + + @Test + void genericsBasedInjectionWithEarlyGenericsMatchingAndRawFactoryMethod() { + beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RawFactoryMethodRepositoryConfiguration.class)); + new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); + + String[] beanNames = beanFactory.getBeanNamesForType(Repository.class); + assertThat(beanNames).contains("stringRepo"); + + beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class)); + assertThat(beanNames.length).isEqualTo(0); + + beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class)); + assertThat(beanNames.length).isEqualTo(0); + } + + @Test + void genericsBasedInjectionWithLateGenericsMatchingAndRawFactoryMethod() { + beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RawFactoryMethodRepositoryConfiguration.class)); + new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); + beanFactory.preInstantiateSingletons(); + + String[] beanNames = beanFactory.getBeanNamesForType(Repository.class); + assertThat(beanNames).contains("stringRepo"); + + beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class)); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("stringRepo"); + + beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class)); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("stringRepo"); + } + + @Test + void genericsBasedInjectionWithEarlyGenericsMatchingAndRawInstance() { + beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RawInstanceRepositoryConfiguration.class)); + new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); + + String[] beanNames = beanFactory.getBeanNamesForType(Repository.class); + assertThat(beanNames).contains("stringRepo"); + + beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class)); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("stringRepo"); + + beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class)); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("stringRepo"); + } + + @Test + void genericsBasedInjectionWithLateGenericsMatchingAndRawInstance() { + beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RawInstanceRepositoryConfiguration.class)); + new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); + beanFactory.preInstantiateSingletons(); + + String[] beanNames = beanFactory.getBeanNamesForType(Repository.class); + assertThat(beanNames).contains("stringRepo"); + + beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class)); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("stringRepo"); + + beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class)); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("stringRepo"); + } + + @Test + void genericsBasedInjectionWithEarlyGenericsMatchingOnCglibProxy() { + beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RepositoryConfiguration.class)); + new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); + DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); + autoProxyCreator.setProxyTargetClass(true); + autoProxyCreator.setBeanFactory(beanFactory); + beanFactory.addBeanPostProcessor(autoProxyCreator); + beanFactory.registerSingleton("traceInterceptor", new DefaultPointcutAdvisor(new SimpleTraceInterceptor())); + + String[] beanNames = beanFactory.getBeanNamesForType(Repository.class); + assertThat(beanNames).contains("stringRepo"); + + beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class)); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("stringRepo"); + + beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class)); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("stringRepo"); + + assertThat(AopUtils.isCglibProxy(beanFactory.getBean("stringRepo"))).isTrue(); + } + + @Test + void genericsBasedInjectionWithLateGenericsMatchingOnCglibProxy() { + beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RepositoryConfiguration.class)); + new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); + DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); + autoProxyCreator.setProxyTargetClass(true); + autoProxyCreator.setBeanFactory(beanFactory); + beanFactory.addBeanPostProcessor(autoProxyCreator); + beanFactory.registerSingleton("traceInterceptor", new DefaultPointcutAdvisor(new SimpleTraceInterceptor())); + beanFactory.preInstantiateSingletons(); + + String[] beanNames = beanFactory.getBeanNamesForType(Repository.class); + assertThat(beanNames).contains("stringRepo"); + + beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class)); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("stringRepo"); + + beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class)); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("stringRepo"); + + assertThat(AopUtils.isCglibProxy(beanFactory.getBean("stringRepo"))).isTrue(); + } + + @Test + void genericsBasedInjectionWithLateGenericsMatchingOnCglibProxyAndRawFactoryMethod() { + beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RawFactoryMethodRepositoryConfiguration.class)); + new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); + DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); + autoProxyCreator.setProxyTargetClass(true); + autoProxyCreator.setBeanFactory(beanFactory); + beanFactory.addBeanPostProcessor(autoProxyCreator); + beanFactory.registerSingleton("traceInterceptor", new DefaultPointcutAdvisor(new SimpleTraceInterceptor())); + beanFactory.preInstantiateSingletons(); + + String[] beanNames = beanFactory.getBeanNamesForType(Repository.class); + assertThat(beanNames).contains("stringRepo"); + + beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class)); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("stringRepo"); + + beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class)); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("stringRepo"); + + assertThat(AopUtils.isCglibProxy(beanFactory.getBean("stringRepo"))).isTrue(); + } + + @Test + void genericsBasedInjectionWithLateGenericsMatchingOnCglibProxyAndRawInstance() { + beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RawInstanceRepositoryConfiguration.class)); + new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); + DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); + autoProxyCreator.setProxyTargetClass(true); + autoProxyCreator.setBeanFactory(beanFactory); + beanFactory.addBeanPostProcessor(autoProxyCreator); + beanFactory.registerSingleton("traceInterceptor", new DefaultPointcutAdvisor(new SimpleTraceInterceptor())); + beanFactory.preInstantiateSingletons(); + + String[] beanNames = beanFactory.getBeanNamesForType(Repository.class); + assertThat(beanNames).contains("stringRepo"); + + beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class)); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("stringRepo"); + + beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class)); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("stringRepo"); + + assertThat(AopUtils.isCglibProxy(beanFactory.getBean("stringRepo"))).isTrue(); + } + + @Test + void genericsBasedInjectionWithEarlyGenericsMatchingOnJdkProxy() { + beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RepositoryConfiguration.class)); + new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); + DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); + autoProxyCreator.setBeanFactory(beanFactory); + beanFactory.addBeanPostProcessor(autoProxyCreator); + beanFactory.registerSingleton("traceInterceptor", new DefaultPointcutAdvisor(new SimpleTraceInterceptor())); + + String[] beanNames = beanFactory.getBeanNamesForType(RepositoryInterface.class); + assertThat(beanNames).contains("stringRepo"); + + beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(RepositoryInterface.class, String.class)); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("stringRepo"); + + beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(RepositoryInterface.class, String.class)); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("stringRepo"); + + assertThat(AopUtils.isJdkDynamicProxy(beanFactory.getBean("stringRepo"))).isTrue(); + } + + @Test + void genericsBasedInjectionWithLateGenericsMatchingOnJdkProxy() { + beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RepositoryConfiguration.class)); + new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); + DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); + autoProxyCreator.setBeanFactory(beanFactory); + beanFactory.addBeanPostProcessor(autoProxyCreator); + beanFactory.registerSingleton("traceInterceptor", new DefaultPointcutAdvisor(new SimpleTraceInterceptor())); + beanFactory.preInstantiateSingletons(); + + String[] beanNames = beanFactory.getBeanNamesForType(RepositoryInterface.class); + assertThat(beanNames).contains("stringRepo"); + + beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(RepositoryInterface.class, String.class)); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("stringRepo"); + + beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(RepositoryInterface.class, String.class)); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("stringRepo"); + + assertThat(AopUtils.isJdkDynamicProxy(beanFactory.getBean("stringRepo"))).isTrue(); + } + + @Test + void genericsBasedInjectionWithLateGenericsMatchingOnJdkProxyAndRawFactoryMethod() { + beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RawFactoryMethodRepositoryConfiguration.class)); + new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); + DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); + autoProxyCreator.setBeanFactory(beanFactory); + beanFactory.addBeanPostProcessor(autoProxyCreator); + beanFactory.registerSingleton("traceInterceptor", new DefaultPointcutAdvisor(new SimpleTraceInterceptor())); + beanFactory.preInstantiateSingletons(); + + String[] beanNames = beanFactory.getBeanNamesForType(RepositoryInterface.class); + assertThat(beanNames).contains("stringRepo"); + + beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(RepositoryInterface.class, String.class)); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("stringRepo"); + + beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(RepositoryInterface.class, String.class)); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("stringRepo"); + + assertThat(AopUtils.isJdkDynamicProxy(beanFactory.getBean("stringRepo"))).isTrue(); + } + + @Test + void genericsBasedInjectionWithLateGenericsMatchingOnJdkProxyAndRawInstance() { + beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RawInstanceRepositoryConfiguration.class)); + new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); + DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); + autoProxyCreator.setBeanFactory(beanFactory); + beanFactory.addBeanPostProcessor(autoProxyCreator); + beanFactory.registerSingleton("traceInterceptor", new DefaultPointcutAdvisor(new SimpleTraceInterceptor())); + beanFactory.preInstantiateSingletons(); + + String[] beanNames = beanFactory.getBeanNamesForType(RepositoryInterface.class); + assertThat(beanNames).contains("stringRepo"); + + beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(RepositoryInterface.class, String.class)); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("stringRepo"); + + beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(RepositoryInterface.class, String.class)); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("stringRepo"); + + assertThat(AopUtils.isJdkDynamicProxy(beanFactory.getBean("stringRepo"))).isTrue(); + } + + @Test + void testSelfReferenceExclusionForFactoryMethodOnSameBean() { + AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); + bpp.setBeanFactory(beanFactory); + beanFactory.addBeanPostProcessor(bpp); + beanFactory.addBeanPostProcessor(new CommonAnnotationBeanPostProcessor()); + beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(ConcreteConfig.class)); + beanFactory.registerBeanDefinition("serviceBeanProvider", new RootBeanDefinition(ServiceBeanProvider.class)); + new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); + beanFactory.preInstantiateSingletons(); + + beanFactory.getBean(ServiceBean.class); + } + + @Test + void testConfigWithDefaultMethods() { + AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); + bpp.setBeanFactory(beanFactory); + beanFactory.addBeanPostProcessor(bpp); + beanFactory.addBeanPostProcessor(new CommonAnnotationBeanPostProcessor()); + beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(ConcreteConfigWithDefaultMethods.class)); + beanFactory.registerBeanDefinition("serviceBeanProvider", new RootBeanDefinition(ServiceBeanProvider.class)); + new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); + beanFactory.preInstantiateSingletons(); + + beanFactory.getBean(ServiceBean.class); + } + + @Test + void testConfigWithDefaultMethodsUsingAsm() { + AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); + bpp.setBeanFactory(beanFactory); + beanFactory.addBeanPostProcessor(bpp); + beanFactory.addBeanPostProcessor(new CommonAnnotationBeanPostProcessor()); + beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(ConcreteConfigWithDefaultMethods.class.getName())); + beanFactory.registerBeanDefinition("serviceBeanProvider", new RootBeanDefinition(ServiceBeanProvider.class.getName())); + new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); + beanFactory.preInstantiateSingletons(); + + beanFactory.getBean(ServiceBean.class); + } + + @Test + void testCircularDependency() { + AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); + bpp.setBeanFactory(beanFactory); + beanFactory.addBeanPostProcessor(bpp); + beanFactory.registerBeanDefinition("configClass1", new RootBeanDefinition(A.class)); + beanFactory.registerBeanDefinition("configClass2", new RootBeanDefinition(AStrich.class)); + new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy( + beanFactory::preInstantiateSingletons) + .withMessageContaining("Circular reference"); + } + + @Test + void testCircularDependencyWithApplicationContext() { + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + new AnnotationConfigApplicationContext(A.class, AStrich.class)) + .withMessageContaining("Circular reference"); + } + + @Test + void testPrototypeArgumentThroughBeanMethodCall() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanArgumentConfigWithPrototype.class); + ctx.getBean(FooFactory.class).createFoo(new BarArgument()); + } + + @Test + void testSingletonArgumentThroughBeanMethodCall() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanArgumentConfigWithSingleton.class); + ctx.getBean(FooFactory.class).createFoo(new BarArgument()); + } + + @Test + void testNullArgumentThroughBeanMethodCall() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanArgumentConfigWithNull.class); + ctx.getBean("aFoo"); + } + + @Test + void testInjectionPointMatchForNarrowTargetReturnType() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(FooBarConfiguration.class); + assertThat(ctx.getBean(FooImpl.class).bar).isSameAs(ctx.getBean(BarImpl.class)); + } + + @Test + void testVarargOnBeanMethod() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(VarargConfiguration.class, TestBean.class); + VarargConfiguration bean = ctx.getBean(VarargConfiguration.class); + assertThat(bean.testBeans).isNotNull(); + assertThat(bean.testBeans.length).isEqualTo(1); + assertThat(bean.testBeans[0]).isSameAs(ctx.getBean(TestBean.class)); + } + + @Test + void testEmptyVarargOnBeanMethod() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(VarargConfiguration.class); + VarargConfiguration bean = ctx.getBean(VarargConfiguration.class); + assertThat(bean.testBeans).isNotNull(); + assertThat(bean.testBeans.length).isEqualTo(0); + } + + @Test + void testCollectionArgumentOnBeanMethod() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(CollectionArgumentConfiguration.class, TestBean.class); + CollectionArgumentConfiguration bean = ctx.getBean(CollectionArgumentConfiguration.class); + assertThat(bean.testBeans).isNotNull(); + assertThat(bean.testBeans.size()).isEqualTo(1); + assertThat(bean.testBeans.get(0)).isSameAs(ctx.getBean(TestBean.class)); + } + + @Test + void testEmptyCollectionArgumentOnBeanMethod() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(CollectionArgumentConfiguration.class); + CollectionArgumentConfiguration bean = ctx.getBean(CollectionArgumentConfiguration.class); + assertThat(bean.testBeans).isNotNull(); + assertThat(bean.testBeans.isEmpty()).isTrue(); + } + + @Test + void testMapArgumentOnBeanMethod() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(MapArgumentConfiguration.class, DummyRunnable.class); + MapArgumentConfiguration bean = ctx.getBean(MapArgumentConfiguration.class); + assertThat(bean.testBeans).isNotNull(); + assertThat(bean.testBeans.size()).isEqualTo(1); + assertThat(bean.testBeans.values().iterator().next()).isSameAs(ctx.getBean(Runnable.class)); + } + + @Test + void testEmptyMapArgumentOnBeanMethod() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(MapArgumentConfiguration.class); + MapArgumentConfiguration bean = ctx.getBean(MapArgumentConfiguration.class); + assertThat(bean.testBeans).isNotNull(); + assertThat(bean.testBeans.isEmpty()).isTrue(); + } + + @Test + void testCollectionInjectionFromSameConfigurationClass() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(CollectionInjectionConfiguration.class); + CollectionInjectionConfiguration bean = ctx.getBean(CollectionInjectionConfiguration.class); + assertThat(bean.testBeans).isNotNull(); + assertThat(bean.testBeans.size()).isEqualTo(1); + assertThat(bean.testBeans.get(0)).isSameAs(ctx.getBean(TestBean.class)); + } + + @Test + void testMapInjectionFromSameConfigurationClass() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(MapInjectionConfiguration.class); + MapInjectionConfiguration bean = ctx.getBean(MapInjectionConfiguration.class); + assertThat(bean.testBeans).isNotNull(); + assertThat(bean.testBeans.size()).isEqualTo(1); + assertThat(bean.testBeans.get("testBean")).isSameAs(ctx.getBean(Runnable.class)); + } + + @Test + void testBeanLookupFromSameConfigurationClass() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanLookupConfiguration.class); + BeanLookupConfiguration bean = ctx.getBean(BeanLookupConfiguration.class); + assertThat(bean.getTestBean()).isNotNull(); + assertThat(bean.getTestBean()).isSameAs(ctx.getBean(TestBean.class)); + } + + @Test + void testNameClashBetweenConfigurationClassAndBean() { + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> { + ApplicationContext ctx = new AnnotationConfigApplicationContext(MyTestBean.class); + ctx.getBean("myTestBean", TestBean.class); + }); + } + + @Test + void testBeanDefinitionRegistryPostProcessorConfig() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanDefinitionRegistryPostProcessorConfig.class); + boolean condition = ctx.getBean("myTestBean") instanceof TestBean; + assertThat(condition).isTrue(); + } + + + // ------------------------------------------------------------------------- + + @Configuration + @Order(1) + static class SingletonBeanConfig { + + public @Bean Foo foo() { + return new Foo(); + } + + public @Bean Bar bar() { + return new Bar(foo()); + } + } + + @Configuration(proxyBeanMethods = false) + static class NonEnhancedSingletonBeanConfig { + + public @Bean Foo foo() { + return new Foo(); + } + + public @Bean Bar bar() { + return new Bar(foo()); + } + } + + @Configuration + static class StaticSingletonBeanConfig { + + public static @Bean Foo foo() { + return new Foo(); + } + + public static @Bean Bar bar() { + return new Bar(foo()); + } + } + + @Configuration + @Order(2) + static class OverridingSingletonBeanConfig { + + public @Bean ExtendedFoo foo() { + return new ExtendedFoo(); + } + + public @Bean Bar bar() { + return new Bar(foo()); + } + } + + @Configuration + static class OverridingAgainSingletonBeanConfig { + + public @Bean ExtendedAgainFoo foo() { + return new ExtendedAgainFoo(); + } + } + + @Configuration + static class InvalidOverridingSingletonBeanConfig { + + public @Bean Foo foo() { + return new Foo(); + } + } + + @Configuration + static class ConfigWithOrderedNestedClasses { + + @Configuration + @Order(1) + static class SingletonBeanConfig { + + public @Bean Foo foo() { + return new Foo(); + } + + public @Bean Bar bar() { + return new Bar(foo()); + } + } + + @Configuration + @Order(2) + static class OverridingSingletonBeanConfig { + + public @Bean ExtendedFoo foo() { + return new ExtendedFoo(); + } + + public @Bean Bar bar() { + return new Bar(foo()); + } + } + } + + @Configuration + static class ConfigWithOrderedInnerClasses { + + @Configuration + @Order(1) + class SingletonBeanConfig { + + public SingletonBeanConfig(ConfigWithOrderedInnerClasses other) { + } + + public @Bean Foo foo() { + return new Foo(); + } + + public @Bean Bar bar() { + return new Bar(foo()); + } + } + + @Configuration + @Order(2) + class OverridingSingletonBeanConfig { + + public OverridingSingletonBeanConfig(ObjectProvider other) { + other.getObject(); + } + + public @Bean ExtendedFoo foo() { + return new ExtendedFoo(); + } + + public @Bean Bar bar() { + return new Bar(foo()); + } + } + } + + static class Foo { + } + + static class ExtendedFoo extends Foo { + } + + static class ExtendedAgainFoo extends ExtendedFoo { + } + + static class Bar { + + final Foo foo; + + public Bar(Foo foo) { + this.foo = foo; + } + } + + @Configuration + static class UnloadedConfig { + + public @Bean Foo foo() { + return new Foo(); + } + } + + @Configuration + static class LoadedConfig { + + public @Bean Bar bar() { + return new Bar(new Foo()); + } + } + + @Configuration + static class FirstConfiguration { + + @Bean + SyncTaskExecutor taskExecutor() { + return new SyncTaskExecutor(); + } + } + + @Configuration + static class SecondConfiguration { + + @Bean(name = {"applicationTaskExecutor", "taskExecutor"}) + SimpleAsyncTaskExecutor simpleAsyncTaskExecutor() { + return new SimpleAsyncTaskExecutor(); + } + } + + public static class ScopedProxyConsumer { + + @Autowired + public ITestBean testBean; + } + + @Configuration + public static class ScopedProxyConfigurationClass { + + @Bean @Lazy @Scope(proxyMode = ScopedProxyMode.INTERFACES) + public ITestBean scopedClass() { + return new TestBean(); + } + } + + public interface RepositoryInterface { + + @Override + String toString(); + } + + public static class Repository implements RepositoryInterface { + } + + public static class GenericRepository extends Repository { + } + + public static class RepositoryFactoryBean implements FactoryBean { + + @Override + public T getObject() { + throw new IllegalStateException(); + } + + @Override + public Class getObjectType() { + return Object.class; + } + + @Override + public boolean isSingleton() { + return false; + } + } + + public static class RepositoryInjectionBean { + + @Autowired + public Repository stringRepository; + + @Autowired + public Repository integerRepository; + } + + @Configuration + public static class RepositoryConfiguration { + + @Bean + public Repository stringRepo() { + return new Repository() { + @Override + public String toString() { + return "Repository"; + } + }; + } + + @Bean + public Repository integerRepo() { + return new Repository() { + @Override + public String toString() { + return "Repository"; + } + }; + } + + @Bean + public Repository genericRepo() { + return new Repository() { + @Override + public String toString() { + return "Repository"; + } + }; + } + } + + @Configuration + public static class RawFactoryMethodRepositoryConfiguration { + + @SuppressWarnings("rawtypes") // intentionally a raw type + @Bean + public Repository stringRepo() { + return new Repository() { + @Override + public String toString() { + return "Repository"; + } + }; + } + } + + @Configuration + public static class RawInstanceRepositoryConfiguration { + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Bean + public Repository stringRepo() { + return new Repository() { + @Override + public String toString() { + return "Repository"; + } + }; + } + } + + @Configuration + public static class ScopedRepositoryConfiguration { + + @Bean + @Scope("prototype") + public Repository stringRepo() { + return new Repository() { + @Override + public String toString() { + return "Repository"; + } + }; + } + + @Bean + @Scope("prototype") + public Repository integerRepo() { + return new Repository() { + @Override + public String toString() { + return "Repository"; + } + }; + } + + @Bean + @Scope("prototype") + @SuppressWarnings("rawtypes") + public Repository genericRepo() { + return new Repository() { + @Override + public String toString() { + return "Repository"; + } + }; + } + } + + @Retention(RetentionPolicy.RUNTIME) + @Scope(scopeName = "prototype") + public @interface PrototypeScoped { + + ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS; + } + + @Configuration + public static class ScopedProxyRepositoryConfiguration { + + @Bean + @Scope(scopeName = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS) + public Repository stringRepo() { + return new Repository() { + @Override + public String toString() { + return "Repository"; + } + }; + } + + @Bean + @PrototypeScoped + public Repository integerRepo() { + return new Repository() { + @Override + public String toString() { + return "Repository"; + } + }; + } + } + + public static class SpecificRepositoryInjectionBean { + + @Autowired + public GenericRepository genericRepository; + } + + @Configuration + public static class SpecificRepositoryConfiguration { + + @Bean + public Repository genericRepo() { + return new GenericRepository<>(); + } + } + + public static class RepositoryFactoryBeanInjectionBean { + + @Autowired + public RepositoryFactoryBean repositoryFactoryBean; + + @Autowired + @Qualifier("repoFactoryBean") + public RepositoryFactoryBean qualifiedRepositoryFactoryBean; + + @Autowired + @Qualifier("&repoFactoryBean") + public RepositoryFactoryBean prefixQualifiedRepositoryFactoryBean; + } + + @Configuration + public static class RepositoryFactoryBeanConfiguration { + + @Bean + public RepositoryFactoryBean repoFactoryBean() { + return new RepositoryFactoryBean<>(); + } + + @Bean + public FactoryBean nullFactoryBean() { + return null; + } + } + + @Configuration + public static class RawMatchingConfiguration { + + @Bean + @SuppressWarnings("rawtypes") + public Repository rawRepo() { + return new Repository(); + } + + @Bean + public Object repoConsumer(Repository repo) { + return repo; + } + } + + @Configuration + public static class WildcardMatchingConfiguration { + + @Bean + @SuppressWarnings("rawtypes") + public Repository genericRepo() { + return new Repository(); + } + + @Bean + public Object repoConsumer(Repository repo) { + return repo; + } + } + + @Configuration + public static class WildcardWithExtendsConfiguration { + + @Bean + public Repository stringRepo() { + return new Repository<>(); + } + + @Bean + public Repository numberRepo() { + return new Repository<>(); + } + + @Bean + public Object repoConsumer(Repository repo) { + return repo; + } + } + + @Configuration + public static class WildcardWithGenericExtendsConfiguration { + + @Bean + public Repository genericRepo() { + return new Repository(); + } + + @Bean + public Repository numberRepo() { + return new Repository<>(); + } + + @Bean + public Object repoConsumer(Repository repo) { + return repo; + } + } + + @Configuration + @ComponentScan(basePackages = "org.springframework.context.annotation.componentscan.simple") + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface ComposedConfiguration { + } + + @ComposedConfiguration + public static class ComposedConfigurationClass { + } + + @Configuration + @ComponentScan + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface ComposedConfigurationWithAttributeOverrides { + + String[] basePackages() default {}; + + ComponentScan.Filter[] excludeFilters() default {}; + } + + @ComposedConfigurationWithAttributeOverrides(basePackages = "org.springframework.context.annotation.componentscan.simple") + public static class ComposedConfigurationWithAttributeOverrideForBasePackage { + } + + @ComposedConfigurationWithAttributeOverrides(basePackages = "org.springframework.context.annotation.componentscan.simple", + excludeFilters = @ComponentScan.Filter(Component.class)) + public static class ComposedConfigurationWithAttributeOverrideForExcludeFilter { + } + + @ComponentScan(basePackages = "org.springframework.context.annotation.componentscan.base", excludeFilters = {}) + public static class BaseConfigurationWithEmptyExcludeFilters { + } + + @ComponentScan(basePackages = "org.springframework.context.annotation.componentscan.simple", + excludeFilters = @ComponentScan.Filter(Component.class)) + public static class ExtendedConfigurationWithAttributeOverrideForExcludeFilter extends BaseConfigurationWithEmptyExcludeFilters { + } + + @ComposedConfigurationWithAttributeOverrides + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface ComposedComposedConfigurationWithAttributeOverrides { + + String[] basePackages() default {}; + } + + @ComposedComposedConfigurationWithAttributeOverrides(basePackages = "org.springframework.context.annotation.componentscan.simple") + public static class ComposedComposedConfigurationWithAttributeOverridesClass { + } + + @ComponentScan + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface MetaComponentScan { + } + + @MetaComponentScan + @Configuration + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface MetaComponentScanConfigurationWithAttributeOverrides { + + String[] basePackages() default {}; + } + + @MetaComponentScanConfigurationWithAttributeOverrides(basePackages = "org.springframework.context.annotation.componentscan.simple") + public static class MetaComponentScanConfigurationWithAttributeOverridesClass { + } + + @Configuration + public static class SubMetaComponentScanConfigurationWithAttributeOverridesClass extends + MetaComponentScanConfigurationWithAttributeOverridesClass { + } + + public static class ServiceBean { + + private final String parameter; + + public ServiceBean(String parameter) { + this.parameter = parameter; + } + + public String getParameter() { + return parameter; + } + } + + @Configuration + public static abstract class AbstractConfig { + + @Bean + public ServiceBean serviceBean() { + return provider().getServiceBean(); + } + + @Bean + public ServiceBeanProvider provider() { + return new ServiceBeanProvider(); + } + } + + @Configuration + public static class ConcreteConfig extends AbstractConfig { + + @Autowired + private ServiceBeanProvider provider; + + @Bean + @Override + public ServiceBeanProvider provider() { + return provider; + } + + @PostConstruct + public void validate() { + Assert.notNull(provider, "No ServiceBeanProvider injected"); + } + } + + public interface BaseInterface { + + ServiceBean serviceBean(); + } + + public interface BaseDefaultMethods extends BaseInterface { + + @Bean + default ServiceBeanProvider provider() { + return new ServiceBeanProvider(); + } + + @Bean + @Override + default ServiceBean serviceBean() { + return provider().getServiceBean(); + } + } + + public interface DefaultMethodsConfig extends BaseDefaultMethods { + + } + + @Configuration + public static class ConcreteConfigWithDefaultMethods implements DefaultMethodsConfig { + + @Autowired + private ServiceBeanProvider provider; + + @Bean + @Override + public ServiceBeanProvider provider() { + return provider; + } + + @PostConstruct + public void validate() { + Assert.notNull(provider, "No ServiceBeanProvider injected"); + } + } + + @Primary + public static class ServiceBeanProvider { + + public ServiceBean getServiceBean() { + return new ServiceBean("message"); + } + } + + @Configuration + public static class A { + + @Autowired(required = true) + Z z; + + @Bean + public B b() { + if (z == null) { + throw new NullPointerException("z is null"); + } + return new B(z); + } + } + + @Configuration + public static class AStrich { + + @Autowired + B b; + + @Bean + public Z z() { + return new Z(); + } + } + + public static class B { + + public B(Z z) { + } + } + + public static class Z { + } + + @Configuration + static class BeanArgumentConfigWithPrototype { + + @Bean + @Scope("prototype") + public DependingFoo foo(BarArgument bar) { + return new DependingFoo(bar); + } + + @Bean + public FooFactory fooFactory() { + return new FooFactory() { + @Override + public DependingFoo createFoo(BarArgument bar) { + return foo(bar); + } + }; + } + } + + @Configuration + static class BeanArgumentConfigWithSingleton { + + @Bean @Lazy + public DependingFoo foo(BarArgument bar) { + return new DependingFoo(bar); + } + + @Bean + public FooFactory fooFactory() { + return new FooFactory() { + @Override + public DependingFoo createFoo(BarArgument bar) { + return foo(bar); + } + }; + } + } + + @Configuration + static class BeanArgumentConfigWithNull { + + @Bean + public DependingFoo aFoo() { + return foo(null); + } + + @Bean @Lazy + public DependingFoo foo(BarArgument bar) { + return new DependingFoo(bar); + } + + @Bean + public BarArgument bar() { + return new BarArgument(); + } + } + + static class BarArgument { + } + + static class DependingFoo { + + DependingFoo(BarArgument bar) { + Assert.notNull(bar, "No BarArgument injected"); + } + } + + static abstract class FooFactory { + + abstract DependingFoo createFoo(BarArgument bar); + } + + interface BarInterface { + } + + static class BarImpl implements BarInterface { + } + + static class FooImpl { + + @Autowired + public BarImpl bar; + } + + @Configuration + static class FooBarConfiguration { + + @Bean + public BarInterface bar() { + return new BarImpl(); + } + + @Bean + public FooImpl foo() { + return new FooImpl(); + } + } + + public static class DummyRunnable implements Runnable { + + @Override + public void run() { + /* no-op */ + } + } + + @Configuration + static class VarargConfiguration { + + TestBean[] testBeans; + + @Bean(autowireCandidate = false) + public TestBean thing(TestBean... testBeans) { + this.testBeans = testBeans; + return new TestBean(); + } + } + + @Configuration + static class CollectionArgumentConfiguration { + + List testBeans; + + @Bean(autowireCandidate = false) + public TestBean thing(List testBeans) { + this.testBeans = testBeans; + return new TestBean(); + } + } + + @Configuration + public static class MapArgumentConfiguration { + + @Autowired + ConfigurableEnvironment env; + + Map testBeans; + + @Bean(autowireCandidate = false) + Runnable testBean(Map testBeans, + @Qualifier("systemProperties") Map sysprops, + @Qualifier("systemEnvironment") Map sysenv) { + this.testBeans = testBeans; + assertThat(sysprops).isSameAs(env.getSystemProperties()); + assertThat(sysenv).isSameAs(env.getSystemEnvironment()); + return () -> {}; + } + + // Unrelated, not to be considered as a factory method + private boolean testBean(boolean param) { + return param; + } + } + + @Configuration + static class CollectionInjectionConfiguration { + + @Autowired(required = false) + public List testBeans; + + @Bean + public TestBean thing() { + return new TestBean(); + } + } + + @Configuration + public static class MapInjectionConfiguration { + + @Autowired + private Map testBeans; + + @Bean + Runnable testBean() { + return () -> {}; + } + + // Unrelated, not to be considered as a factory method + private boolean testBean(boolean param) { + return param; + } + } + + @Configuration + static abstract class BeanLookupConfiguration { + + @Bean + public TestBean thing() { + return new TestBean(); + } + + @Lookup + public abstract TestBean getTestBean(); + } + + @Configuration + static class BeanDefinitionRegistryPostProcessorConfig { + + @Bean + public static BeanDefinitionRegistryPostProcessor bdrpp() { + return new BeanDefinitionRegistryPostProcessor() { + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { + registry.registerBeanDefinition("myTestBean", new RootBeanDefinition(TestBean.class)); + } + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { + } + }; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassWithConditionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassWithConditionTests.java new file mode 100644 index 0000000..5a434fa --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassWithConditionTests.java @@ -0,0 +1,378 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.stereotype.Component; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for {@link Conditional} beans. + * + * @author Phillip Webb + * @author Juergen Hoeller + */ +@SuppressWarnings("resource") +public class ConfigurationClassWithConditionTests { + + @Test + public void conditionalOnMissingBeanMatch() throws Exception { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(BeanOneConfiguration.class, BeanTwoConfiguration.class); + ctx.refresh(); + assertThat(ctx.containsBean("bean1")).isTrue(); + assertThat(ctx.containsBean("bean2")).isFalse(); + assertThat(ctx.containsBean("configurationClassWithConditionTests.BeanTwoConfiguration")).isFalse(); + } + + @Test + public void conditionalOnMissingBeanNoMatch() throws Exception { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(BeanTwoConfiguration.class); + ctx.refresh(); + assertThat(ctx.containsBean("bean1")).isFalse(); + assertThat(ctx.containsBean("bean2")).isTrue(); + assertThat(ctx.containsBean("configurationClassWithConditionTests.BeanTwoConfiguration")).isTrue(); + } + + @Test + public void conditionalOnBeanMatch() throws Exception { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(BeanOneConfiguration.class, BeanThreeConfiguration.class); + ctx.refresh(); + assertThat(ctx.containsBean("bean1")).isTrue(); + assertThat(ctx.containsBean("bean3")).isTrue(); + } + + @Test + public void conditionalOnBeanNoMatch() throws Exception { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(BeanThreeConfiguration.class); + ctx.refresh(); + assertThat(ctx.containsBean("bean1")).isFalse(); + assertThat(ctx.containsBean("bean3")).isFalse(); + } + + @Test + public void metaConditional() throws Exception { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConfigurationWithMetaCondition.class); + ctx.refresh(); + assertThat(ctx.containsBean("bean")).isTrue(); + } + + @Test + public void metaConditionalWithAsm() throws Exception { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.registerBeanDefinition("config", new RootBeanDefinition(ConfigurationWithMetaCondition.class.getName())); + ctx.refresh(); + assertThat(ctx.containsBean("bean")).isTrue(); + } + + @Test + public void nonConfigurationClass() throws Exception { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(NonConfigurationClass.class); + ctx.refresh(); + assertThat(ctx.containsBean("bean1")).isFalse(); + } + + @Test + public void nonConfigurationClassWithAsm() throws Exception { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.registerBeanDefinition("config", new RootBeanDefinition(NonConfigurationClass.class.getName())); + ctx.refresh(); + assertThat(ctx.containsBean("bean1")).isFalse(); + } + + @Test + public void methodConditional() throws Exception { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConditionOnMethodConfiguration.class); + ctx.refresh(); + assertThat(ctx.containsBean("bean1")).isFalse(); + } + + @Test + public void methodConditionalWithAsm() throws Exception { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.registerBeanDefinition("config", new RootBeanDefinition(ConditionOnMethodConfiguration.class.getName())); + ctx.refresh(); + assertThat(ctx.containsBean("bean1")).isFalse(); + } + + @Test + public void importsNotCreated() throws Exception { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ImportsNotCreated.class); + ctx.refresh(); + } + + @Test + public void conditionOnOverriddenMethodHonored() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConfigWithBeanSkipped.class); + assertThat(context.getBeansOfType(ExampleBean.class).size()).isEqualTo(0); + } + + @Test + public void noConditionOnOverriddenMethodHonored() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConfigWithBeanReactivated.class); + Map beans = context.getBeansOfType(ExampleBean.class); + assertThat(beans.size()).isEqualTo(1); + assertThat(beans.keySet().iterator().next()).isEqualTo("baz"); + } + + @Test + public void configWithAlternativeBeans() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConfigWithAlternativeBeans.class); + Map beans = context.getBeansOfType(ExampleBean.class); + assertThat(beans.size()).isEqualTo(1); + assertThat(beans.keySet().iterator().next()).isEqualTo("baz"); + } + + + @Configuration + static class BeanOneConfiguration { + + @Bean + public ExampleBean bean1() { + return new ExampleBean(); + } + } + + @Configuration + @Conditional(NoBeanOneCondition.class) + static class BeanTwoConfiguration { + + @Bean + public ExampleBean bean2() { + return new ExampleBean(); + } + } + + @Configuration + @Conditional(HasBeanOneCondition.class) + static class BeanThreeConfiguration { + + @Bean + public ExampleBean bean3() { + return new ExampleBean(); + } + } + + @Configuration + @MetaConditional("test") + static class ConfigurationWithMetaCondition { + + @Bean + public ExampleBean bean() { + return new ExampleBean(); + } + } + + @Conditional(MetaConditionalFilter.class) + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface MetaConditional { + + String value(); + } + + @Conditional(NeverCondition.class) + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.TYPE, ElementType.METHOD}) + public @interface Never { + } + + @Conditional(AlwaysCondition.class) + @Never + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.TYPE, ElementType.METHOD}) + public @interface MetaNever { + } + + static class NoBeanOneCondition implements Condition { + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + return !context.getBeanFactory().containsBeanDefinition("bean1"); + } + } + + static class HasBeanOneCondition implements ConfigurationCondition { + + @Override + public ConfigurationPhase getConfigurationPhase() { + return ConfigurationPhase.REGISTER_BEAN; + } + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + return context.getBeanFactory().containsBeanDefinition("bean1"); + } + } + + static class MetaConditionalFilter implements Condition { + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(MetaConditional.class.getName())); + assertThat(attributes.getString("value")).isEqualTo("test"); + return true; + } + } + + static class NeverCondition implements Condition { + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + return false; + } + } + + static class AlwaysCondition implements Condition { + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + return true; + } + } + + @Component + @MetaNever + static class NonConfigurationClass { + + @Bean + public ExampleBean bean1() { + return new ExampleBean(); + } + } + + @Configuration + static class ConditionOnMethodConfiguration { + + @Bean + @Never + public ExampleBean bean1() { + return new ExampleBean(); + } + } + + @Configuration + @Never + @Import({ConfigurationNotCreated.class, RegistrarNotCreated.class, ImportSelectorNotCreated.class}) + static class ImportsNotCreated { + + static { + if (true) throw new RuntimeException(); + } + } + + @Configuration + static class ConfigurationNotCreated { + + static { + if (true) throw new RuntimeException(); + } + } + + static class RegistrarNotCreated implements ImportBeanDefinitionRegistrar { + + static { + if (true) throw new RuntimeException(); + } + + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, + BeanDefinitionRegistry registry) { + } + } + + static class ImportSelectorNotCreated implements ImportSelector { + + static { + if (true) throw new RuntimeException(); + } + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + return new String[] {}; + } + + } + + static class ExampleBean { + } + + @Configuration + static class ConfigWithBeanActive { + + @Bean + public ExampleBean baz() { + return new ExampleBean(); + } + } + + static class ConfigWithBeanSkipped extends ConfigWithBeanActive { + + @Override + @Bean + @Conditional(NeverCondition.class) + public ExampleBean baz() { + return new ExampleBean(); + } + } + + static class ConfigWithBeanReactivated extends ConfigWithBeanSkipped { + + @Override + @Bean + public ExampleBean baz() { + return new ExampleBean(); + } + } + + @Configuration + static class ConfigWithAlternativeBeans { + + @Bean(name = "baz") + @Conditional(AlwaysCondition.class) + public ExampleBean baz1() { + return new ExampleBean(); + } + + @Bean(name = "baz") + @Conditional(NeverCondition.class) + public ExampleBean baz2() { + return new ExampleBean(); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanAndAutowiringTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanAndAutowiringTests.java new file mode 100644 index 0000000..dc07fdb --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanAndAutowiringTests.java @@ -0,0 +1,268 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests cornering bug SPR-8514. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + */ +public class ConfigurationWithFactoryBeanAndAutowiringTests { + + @Test + public void withConcreteFactoryBeanImplementationAsReturnType() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(AppConfig.class); + ctx.register(ConcreteFactoryBeanImplementationConfig.class); + ctx.refresh(); + } + + @Test + public void withParameterizedFactoryBeanImplementationAsReturnType() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(AppConfig.class); + ctx.register(ParameterizedFactoryBeanImplementationConfig.class); + ctx.refresh(); + } + + @Test + public void withParameterizedFactoryBeanInterfaceAsReturnType() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(AppConfig.class); + ctx.register(ParameterizedFactoryBeanInterfaceConfig.class); + ctx.refresh(); + } + + @Test + public void withNonPublicParameterizedFactoryBeanInterfaceAsReturnType() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(AppConfig.class); + ctx.register(NonPublicParameterizedFactoryBeanInterfaceConfig.class); + ctx.refresh(); + } + + @Test + public void withRawFactoryBeanInterfaceAsReturnType() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(AppConfig.class); + ctx.register(RawFactoryBeanInterfaceConfig.class); + ctx.refresh(); + } + + @Test + public void withWildcardParameterizedFactoryBeanInterfaceAsReturnType() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(AppConfig.class); + ctx.register(WildcardParameterizedFactoryBeanInterfaceConfig.class); + ctx.refresh(); + } + + @Test + public void withFactoryBeanCallingBean() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(AppConfig.class); + ctx.register(FactoryBeanCallingConfig.class); + ctx.refresh(); + assertThat(ctx.getBean("myString")).isEqualTo("true"); + } + + + static class DummyBean { + } + + + static class MyFactoryBean implements FactoryBean, InitializingBean { + + private boolean initialized = false; + + @Override + public void afterPropertiesSet() throws Exception { + this.initialized = true; + } + + @Override + public String getObject() throws Exception { + return "foo"; + } + + @Override + public Class getObjectType() { + return String.class; + } + + @Override + public boolean isSingleton() { + return true; + } + + public String getString() { + return Boolean.toString(this.initialized); + } + } + + + static class MyParameterizedFactoryBean implements FactoryBean { + + private final T obj; + + public MyParameterizedFactoryBean(T obj) { + this.obj = obj; + } + + @Override + public T getObject() throws Exception { + return obj; + } + + @Override + @SuppressWarnings("unchecked") + public Class getObjectType() { + return (Class)obj.getClass(); + } + + @Override + public boolean isSingleton() { + return true; + } + } + + + @Configuration + static class AppConfig { + + @Bean + public DummyBean dummyBean() { + return new DummyBean(); + } + } + + + @Configuration + static class ConcreteFactoryBeanImplementationConfig { + + @Autowired + private DummyBean dummyBean; + + @Bean + public MyFactoryBean factoryBean() { + Assert.notNull(dummyBean, "DummyBean was not injected."); + return new MyFactoryBean(); + } + } + + + @Configuration + static class ParameterizedFactoryBeanImplementationConfig { + + @Autowired + private DummyBean dummyBean; + + @Bean + public MyParameterizedFactoryBean factoryBean() { + Assert.notNull(dummyBean, "DummyBean was not injected."); + return new MyParameterizedFactoryBean<>("whatev"); + } + } + + + @Configuration + static class ParameterizedFactoryBeanInterfaceConfig { + + @Autowired + private DummyBean dummyBean; + + @Bean + public FactoryBean factoryBean() { + Assert.notNull(dummyBean, "DummyBean was not injected."); + return new MyFactoryBean(); + } + } + + + @Configuration + static class NonPublicParameterizedFactoryBeanInterfaceConfig { + + @Autowired + private DummyBean dummyBean; + + @Bean + FactoryBean factoryBean() { + Assert.notNull(dummyBean, "DummyBean was not injected."); + return new MyFactoryBean(); + } + } + + + @Configuration + static class RawFactoryBeanInterfaceConfig { + + @Autowired + private DummyBean dummyBean; + + @Bean + @SuppressWarnings("rawtypes") + public FactoryBean factoryBean() { + Assert.notNull(dummyBean, "DummyBean was not injected."); + return new MyFactoryBean(); + } + } + + + @Configuration + static class WildcardParameterizedFactoryBeanInterfaceConfig { + + @Autowired + private DummyBean dummyBean; + + @Bean + public FactoryBean factoryBean() { + Assert.notNull(dummyBean, "DummyBean was not injected."); + return new MyFactoryBean(); + } + } + + + @Configuration + static class FactoryBeanCallingConfig { + + @Autowired + private DummyBean dummyBean; + + @Bean + public MyFactoryBean factoryBean() { + Assert.notNull(dummyBean, "DummyBean was not injected."); + return new MyFactoryBean(); + } + + @Bean + public String myString() { + return factoryBean().getString(); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanAndParametersTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanAndParametersTests.java new file mode 100644 index 0000000..56fcbc1 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanAndParametersTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test case cornering the bug initially raised with SPR-8762, in which a + * NullPointerException would be raised if a FactoryBean-returning @Bean method also + * accepts parameters + * + * @author Chris Beams + * @since 3.1 + */ +public class ConfigurationWithFactoryBeanAndParametersTests { + + @Test + public void test() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class, Bar.class); + assertThat(ctx.getBean(Bar.class).foo).isNotNull(); + } + + + @Configuration + static class Config { + + @Bean + public FactoryBean fb(@Value("42") String answer) { + return new FooFactoryBean(); + } + } + + + static class Foo { + } + + + static class Bar { + + Foo foo; + + @Autowired + public Bar(Foo foo) { + this.foo = foo; + } + } + + + static class FooFactoryBean implements FactoryBean { + + @Override + public Foo getObject() { + return new Foo(); + } + + @Override + public Class getObjectType() { + return Foo.class; + } + + @Override + public boolean isSingleton() { + return true; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanBeanEarlyDeductionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanBeanEarlyDeductionTests.java new file mode 100644 index 0000000..833e357 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanBeanEarlyDeductionTests.java @@ -0,0 +1,256 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.AbstractBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.type.AnnotationMetadata; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for {@link AbstractBeanFactory} type inference from + * {@link FactoryBean FactoryBeans} defined in the configuration. + * + * @author Phillip Webb + */ +public class ConfigurationWithFactoryBeanBeanEarlyDeductionTests { + + @Test + public void preFreezeDirect() { + assertPreFreeze(DirectConfiguration.class); + } + + @Test + public void postFreezeDirect() { + assertPostFreeze(DirectConfiguration.class); + } + + @Test + public void preFreezeGenericMethod() { + assertPreFreeze(GenericMethodConfiguration.class); + } + + @Test + public void postFreezeGenericMethod() { + assertPostFreeze(GenericMethodConfiguration.class); + } + + @Test + public void preFreezeGenericClass() { + assertPreFreeze(GenericClassConfiguration.class); + } + + @Test + public void postFreezeGenericClass() { + assertPostFreeze(GenericClassConfiguration.class); + } + + @Test + public void preFreezeAttribute() { + assertPreFreeze(AttributeClassConfiguration.class); + } + + @Test + public void postFreezeAttribute() { + assertPostFreeze(AttributeClassConfiguration.class); + } + + @Test + public void preFreezeUnresolvedGenericFactoryBean() { + // Covers the case where a @Configuration is picked up via component scanning + // and its bean definition only has a String bean class. In such cases + // beanDefinition.hasBeanClass() returns false so we need to actually + // call determineTargetType ourselves + GenericBeanDefinition factoryBeanDefinition = new GenericBeanDefinition(); + factoryBeanDefinition.setBeanClassName(GenericClassConfiguration.class.getName()); + GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); + beanDefinition.setBeanClass(FactoryBean.class); + beanDefinition.setFactoryBeanName("factoryBean"); + beanDefinition.setFactoryMethodName("myBean"); + GenericApplicationContext context = new GenericApplicationContext(); + try { + context.registerBeanDefinition("factoryBean", factoryBeanDefinition); + context.registerBeanDefinition("myBean", beanDefinition); + NameCollectingBeanFactoryPostProcessor postProcessor = new NameCollectingBeanFactoryPostProcessor(); + context.addBeanFactoryPostProcessor(postProcessor); + context.refresh(); + assertContainsMyBeanName(postProcessor.getNames()); + } + finally { + context.close(); + } + } + + private void assertPostFreeze(Class configurationClass) { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + configurationClass); + assertContainsMyBeanName(context); + } + + private void assertPreFreeze(Class configurationClass, + BeanFactoryPostProcessor... postProcessors) { + NameCollectingBeanFactoryPostProcessor postProcessor = new NameCollectingBeanFactoryPostProcessor(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + try { + Arrays.stream(postProcessors).forEach(context::addBeanFactoryPostProcessor); + context.addBeanFactoryPostProcessor(postProcessor); + context.register(configurationClass); + context.refresh(); + assertContainsMyBeanName(postProcessor.getNames()); + } + finally { + context.close(); + } + } + + private void assertContainsMyBeanName(AnnotationConfigApplicationContext context) { + assertContainsMyBeanName(context.getBeanNamesForType(MyBean.class, true, false)); + } + + private void assertContainsMyBeanName(String[] names) { + assertThat(names).containsExactly("myBean"); + } + + private static class NameCollectingBeanFactoryPostProcessor + implements BeanFactoryPostProcessor { + + private String[] names; + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) + throws BeansException { + this.names = beanFactory.getBeanNamesForType(MyBean.class, true, false); + } + + public String[] getNames() { + return this.names; + } + + } + + @Configuration + static class DirectConfiguration { + + @Bean + MyBean myBean() { + return new MyBean(); + } + + } + + @Configuration + static class GenericMethodConfiguration { + + @Bean + FactoryBean myBean() { + return new TestFactoryBean<>(new MyBean()); + } + + } + + @Configuration + static class GenericClassConfiguration { + + @Bean + MyFactoryBean myBean() { + return new MyFactoryBean(); + } + + } + + @Configuration + @Import(AttributeClassRegistrar.class) + static class AttributeClassConfiguration { + + } + + static class AttributeClassRegistrar implements ImportBeanDefinitionRegistrar { + + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + BeanDefinition definition = BeanDefinitionBuilder.genericBeanDefinition( + RawWithAbstractObjectTypeFactoryBean.class).getBeanDefinition(); + definition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, MyBean.class); + registry.registerBeanDefinition("myBean", definition); + } + + } + + abstract static class AbstractMyBean { + } + + static class MyBean extends AbstractMyBean { + } + + static class TestFactoryBean implements FactoryBean { + + private final T instance; + + public TestFactoryBean(T instance) { + this.instance = instance; + } + + @Override + public T getObject() throws Exception { + return this.instance; + } + + @Override + public Class getObjectType() { + return this.instance.getClass(); + } + + } + + static class MyFactoryBean extends TestFactoryBean { + + public MyFactoryBean() { + super(new MyBean()); + } + + } + + static class RawWithAbstractObjectTypeFactoryBean implements FactoryBean { + + private final Object object = new MyBean(); + + @Override + public Object getObject() throws Exception { + return object; + } + + @Override + public Class getObjectType() { + return MyBean.class; + } + + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/DeferredImportSelectorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/DeferredImportSelectorTests.java new file mode 100644 index 0000000..374ca4d --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/DeferredImportSelectorTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.annotation.DeferredImportSelector.Group; +import org.springframework.core.type.AnnotationMetadata; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link DeferredImportSelector}. + * + * @author Stephane Nicoll + */ +public class DeferredImportSelectorTests { + + @Test + public void entryEqualsSameInstance() { + AnnotationMetadata metadata = mock(AnnotationMetadata.class); + Group.Entry entry = new Group.Entry(metadata, "com.example.Test"); + assertThat(entry).isEqualTo(entry); + } + + @Test + public void entryEqualsSameMetadataAndClassName() { + AnnotationMetadata metadata = mock(AnnotationMetadata.class); + assertThat(new Group.Entry(metadata, "com.example.Test")).isEqualTo(new Group.Entry(metadata, "com.example.Test")); + } + + @Test + public void entryEqualDifferentMetadataAndSameClassName() { + assertThat(new Group.Entry(mock(AnnotationMetadata.class), "com.example.Test")).isNotEqualTo(new Group.Entry(mock(AnnotationMetadata.class), "com.example.Test")); + } + + @Test + public void entryEqualSameMetadataAnDifferentClassName() { + AnnotationMetadata metadata = mock(AnnotationMetadata.class); + assertThat(new Group.Entry(metadata, "com.example.AnotherTest")).isNotEqualTo(new Group.Entry(metadata, "com.example.Test")); + } +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/DestroyMethodInferenceTests.java b/spring-context/src/test/java/org/springframework/context/annotation/DestroyMethodInferenceTests.java new file mode 100644 index 0000000..7da4675 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/DestroyMethodInferenceTests.java @@ -0,0 +1,218 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.io.Closeable; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.GenericXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * @author Chris Beams + * @author Juergen Hoeller + * @author Stephane Nicoll + */ +public class DestroyMethodInferenceTests { + + @Test + public void beanMethods() { + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class); + WithExplicitDestroyMethod c0 = ctx.getBean(WithExplicitDestroyMethod.class); + WithLocalCloseMethod c1 = ctx.getBean("c1", WithLocalCloseMethod.class); + WithLocalCloseMethod c2 = ctx.getBean("c2", WithLocalCloseMethod.class); + WithInheritedCloseMethod c3 = ctx.getBean("c3", WithInheritedCloseMethod.class); + WithInheritedCloseMethod c4 = ctx.getBean("c4", WithInheritedCloseMethod.class); + WithInheritedCloseMethod c5 = ctx.getBean("c5", WithInheritedCloseMethod.class); + WithNoCloseMethod c6 = ctx.getBean("c6", WithNoCloseMethod.class); + WithLocalShutdownMethod c7 = ctx.getBean("c7", WithLocalShutdownMethod.class); + WithInheritedCloseMethod c8 = ctx.getBean("c8", WithInheritedCloseMethod.class); + WithDisposableBean c9 = ctx.getBean("c9", WithDisposableBean.class); + + assertThat(c0.closed).as("c0").isFalse(); + assertThat(c1.closed).as("c1").isFalse(); + assertThat(c2.closed).as("c2").isFalse(); + assertThat(c3.closed).as("c3").isFalse(); + assertThat(c4.closed).as("c4").isFalse(); + assertThat(c5.closed).as("c5").isFalse(); + assertThat(c6.closed).as("c6").isFalse(); + assertThat(c7.closed).as("c7").isFalse(); + assertThat(c8.closed).as("c8").isFalse(); + assertThat(c9.closed).as("c9").isFalse(); + ctx.close(); + assertThat(c0.closed).as("c0").isTrue(); + assertThat(c1.closed).as("c1").isTrue(); + assertThat(c2.closed).as("c2").isTrue(); + assertThat(c3.closed).as("c3").isTrue(); + assertThat(c4.closed).as("c4").isTrue(); + assertThat(c5.closed).as("c5").isTrue(); + assertThat(c6.closed).as("c6").isFalse(); + assertThat(c7.closed).as("c7").isTrue(); + assertThat(c8.closed).as("c8").isFalse(); + assertThat(c9.closed).as("c9").isTrue(); + } + + @Test + public void xml() { + ConfigurableApplicationContext ctx = new GenericXmlApplicationContext( + getClass(), "DestroyMethodInferenceTests-context.xml"); + WithLocalCloseMethod x1 = ctx.getBean("x1", WithLocalCloseMethod.class); + WithLocalCloseMethod x2 = ctx.getBean("x2", WithLocalCloseMethod.class); + WithLocalCloseMethod x3 = ctx.getBean("x3", WithLocalCloseMethod.class); + WithNoCloseMethod x4 = ctx.getBean("x4", WithNoCloseMethod.class); + WithInheritedCloseMethod x8 = ctx.getBean("x8", WithInheritedCloseMethod.class); + + assertThat(x1.closed).isFalse(); + assertThat(x2.closed).isFalse(); + assertThat(x3.closed).isFalse(); + assertThat(x4.closed).isFalse(); + ctx.close(); + assertThat(x1.closed).isFalse(); + assertThat(x2.closed).isTrue(); + assertThat(x3.closed).isTrue(); + assertThat(x4.closed).isFalse(); + assertThat(x8.closed).isFalse(); + } + + + @Configuration + static class Config { + + @Bean(destroyMethod = "explicitClose") + public WithExplicitDestroyMethod c0() { + return new WithExplicitDestroyMethod(); + } + + @Bean + public WithLocalCloseMethod c1() { + return new WithLocalCloseMethod(); + } + + @Bean + public Object c2() { + return new WithLocalCloseMethod(); + } + + @Bean + public WithInheritedCloseMethod c3() { + return new WithInheritedCloseMethod(); + } + + @Bean + public Closeable c4() { + return new WithInheritedCloseMethod(); + } + + @Bean(destroyMethod = "other") + public WithInheritedCloseMethod c5() { + return new WithInheritedCloseMethod() { + @Override + public void close() { + throw new IllegalStateException("close() should not be called"); + } + @SuppressWarnings("unused") + public void other() { + this.closed = true; + } + }; + } + + @Bean + public WithNoCloseMethod c6() { + return new WithNoCloseMethod(); + } + + @Bean + public WithLocalShutdownMethod c7() { + return new WithLocalShutdownMethod(); + } + + @Bean(destroyMethod = "") + public WithInheritedCloseMethod c8() { + return new WithInheritedCloseMethod(); + } + + @Bean(destroyMethod = "") + public WithDisposableBean c9() { + return new WithDisposableBean(); + } + } + + + static class WithExplicitDestroyMethod { + + boolean closed = false; + + public void explicitClose() { + closed = true; + } + } + + + static class WithLocalCloseMethod { + + boolean closed = false; + + public void close() { + closed = true; + } + } + + + static class WithInheritedCloseMethod implements Closeable { + + boolean closed = false; + + @Override + public void close() { + closed = true; + } + } + + + static class WithDisposableBean implements DisposableBean { + + boolean closed = false; + + @Override + public void destroy() { + closed = true; + } + } + + + static class WithNoCloseMethod { + + boolean closed = false; + } + + + static class WithLocalShutdownMethod { + + boolean closed = false; + + public void shutdown() { + closed = true; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/DoubleScanTests.java b/spring-context/src/test/java/org/springframework/context/annotation/DoubleScanTests.java new file mode 100644 index 0000000..50b51ae --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/DoubleScanTests.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +/** + * @author Juergen Hoeller + * @author Chris Beams + */ +public class DoubleScanTests extends SimpleScanTests { + + @Override + protected String[] getConfigLocations() { + return new String[] {"doubleScanTests.xml"}; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/EnableAspectJAutoProxyTests.java b/spring-context/src/test/java/org/springframework/context/annotation/EnableAspectJAutoProxyTests.java new file mode 100644 index 0000000..7fc4856 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/EnableAspectJAutoProxyTests.java @@ -0,0 +1,176 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import example.scannable.FooDao; +import example.scannable.FooService; +import example.scannable.FooServiceImpl; +import example.scannable.ServiceInvocationCounter; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.AopContext; +import org.springframework.aop.support.AopUtils; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @author Chris Beams + */ +public class EnableAspectJAutoProxyTests { + + @Test + public void withJdkProxy() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithJdkProxy.class); + + aspectIsApplied(ctx); + assertThat(AopUtils.isJdkDynamicProxy(ctx.getBean(FooService.class))).isTrue(); + } + + @Test + public void withCglibProxy() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithCglibProxy.class); + + aspectIsApplied(ctx); + assertThat(AopUtils.isCglibProxy(ctx.getBean(FooService.class))).isTrue(); + } + + @Test + public void withExposedProxy() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithExposedProxy.class); + + aspectIsApplied(ctx); + assertThat(AopUtils.isJdkDynamicProxy(ctx.getBean(FooService.class))).isTrue(); + } + + private void aspectIsApplied(ApplicationContext ctx) { + FooService fooService = ctx.getBean(FooService.class); + ServiceInvocationCounter counter = ctx.getBean(ServiceInvocationCounter.class); + + assertThat(counter.getCount()).isEqualTo(0); + + assertThat(fooService.isInitCalled()).isTrue(); + assertThat(counter.getCount()).isEqualTo(1); + + String value = fooService.foo(1); + assertThat(value).isEqualTo("bar"); + assertThat(counter.getCount()).isEqualTo(2); + + fooService.foo(1); + assertThat(counter.getCount()).isEqualTo(3); + } + + @Test + public void withAnnotationOnArgumentAndJdkProxy() { + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext( + ConfigWithJdkProxy.class, SampleService.class, LoggingAspect.class); + + SampleService sampleService = ctx.getBean(SampleService.class); + sampleService.execute(new SampleDto()); + sampleService.execute(new SampleInputBean()); + sampleService.execute((SampleDto) null); + sampleService.execute((SampleInputBean) null); + } + + @Test + public void withAnnotationOnArgumentAndCglibProxy() { + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext( + ConfigWithCglibProxy.class, SampleService.class, LoggingAspect.class); + + SampleService sampleService = ctx.getBean(SampleService.class); + sampleService.execute(new SampleDto()); + sampleService.execute(new SampleInputBean()); + sampleService.execute((SampleDto) null); + sampleService.execute((SampleInputBean) null); + } + + + @ComponentScan("example.scannable") + @EnableAspectJAutoProxy + static class ConfigWithJdkProxy { + } + + + @ComponentScan("example.scannable") + @EnableAspectJAutoProxy(proxyTargetClass = true) + static class ConfigWithCglibProxy { + } + + + @ComponentScan("example.scannable") + @EnableAspectJAutoProxy(exposeProxy = true) + static class ConfigWithExposedProxy { + + @Bean + public FooService fooServiceImpl(final ApplicationContext context) { + return new FooServiceImpl() { + @Override + public String foo(int id) { + assertThat(AopContext.currentProxy()).isNotNull(); + return super.foo(id); + } + @Override + protected FooDao fooDao() { + return context.getBean(FooDao.class); + } + }; + } + } + + + @Retention(RetentionPolicy.RUNTIME) + public @interface Loggable { + } + + + @Loggable + public static class SampleDto { + } + + + public static class SampleInputBean { + } + + + public static class SampleService { + + // Not matched method on {@link LoggingAspect}. + public void execute(SampleInputBean inputBean) { + } + + // Matched method on {@link LoggingAspect} + public void execute(SampleDto dto) { + } + } + + + @Aspect + public static class LoggingAspect { + + @Before("@args(org.springframework.context.annotation.EnableAspectJAutoProxyTests.Loggable))") + public void loggingBeginByAtArgs() { + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/EnableLoadTimeWeavingTests.java b/spring-context/src/test/java/org/springframework/context/annotation/EnableLoadTimeWeavingTests.java new file mode 100644 index 0000000..6bc70e1 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/EnableLoadTimeWeavingTests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.instrument.ClassFileTransformer; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.annotation.EnableLoadTimeWeaving.AspectJWeaving; +import org.springframework.context.support.GenericXmlApplicationContext; +import org.springframework.instrument.classloading.LoadTimeWeaver; + +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +/** + * Unit tests for @EnableLoadTimeWeaving + * + * @author Chris Beams + * @since 3.1 + */ +public class EnableLoadTimeWeavingTests { + + @Test + public void control() { + GenericXmlApplicationContext ctx = + new GenericXmlApplicationContext(getClass(), "EnableLoadTimeWeavingTests-context.xml"); + ctx.getBean("loadTimeWeaver", LoadTimeWeaver.class); + } + + @Test + public void enableLTW_withAjWeavingDisabled() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(EnableLTWConfig_withAjWeavingDisabled.class); + ctx.refresh(); + LoadTimeWeaver loadTimeWeaver = ctx.getBean("loadTimeWeaver", LoadTimeWeaver.class); + verifyNoInteractions(loadTimeWeaver); + } + + @Test + public void enableLTW_withAjWeavingAutodetect() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(EnableLTWConfig_withAjWeavingAutodetect.class); + ctx.refresh(); + LoadTimeWeaver loadTimeWeaver = ctx.getBean("loadTimeWeaver", LoadTimeWeaver.class); + // no expectations -> a class file transformer should NOT be added + // because no META-INF/aop.xml is present on the classpath + verifyNoInteractions(loadTimeWeaver); + } + + @Test + public void enableLTW_withAjWeavingEnabled() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(EnableLTWConfig_withAjWeavingEnabled.class); + ctx.refresh(); + LoadTimeWeaver loadTimeWeaver = ctx.getBean("loadTimeWeaver", LoadTimeWeaver.class); + verify(loadTimeWeaver).addTransformer(isA(ClassFileTransformer.class)); + } + + @Configuration + @EnableLoadTimeWeaving(aspectjWeaving=AspectJWeaving.DISABLED) + static class EnableLTWConfig_withAjWeavingDisabled implements LoadTimeWeavingConfigurer { + @Override + public LoadTimeWeaver getLoadTimeWeaver() { + return mock(LoadTimeWeaver.class); + } + } + + @Configuration + @EnableLoadTimeWeaving(aspectjWeaving=AspectJWeaving.AUTODETECT) + static class EnableLTWConfig_withAjWeavingAutodetect implements LoadTimeWeavingConfigurer { + @Override + public LoadTimeWeaver getLoadTimeWeaver() { + return mock(LoadTimeWeaver.class); + } + } + + @Configuration + @EnableLoadTimeWeaving(aspectjWeaving=AspectJWeaving.ENABLED) + static class EnableLTWConfig_withAjWeavingEnabled implements LoadTimeWeavingConfigurer { + @Override + public LoadTimeWeaver getLoadTimeWeaver() { + return mock(LoadTimeWeaver.class); + } + } +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/FooServiceDependentConverter.java b/spring-context/src/test/java/org/springframework/context/annotation/FooServiceDependentConverter.java new file mode 100644 index 0000000..34af301 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/FooServiceDependentConverter.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import example.scannable.FooService; + +import org.springframework.core.convert.converter.Converter; + +/** + * @author Juergen Hoeller + */ +public class FooServiceDependentConverter implements Converter { + + @SuppressWarnings("unused") + private FooService fooService; + + public void setFooService(FooService fooService) { + this.fooService = fooService; + } + + @Override + public org.springframework.beans.testfixture.beans.TestBean convert(String source) { + return new org.springframework.beans.testfixture.beans.TestBean(source); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ImportAwareTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ImportAwareTests.java new file mode 100644 index 0000000..05df9c9 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ImportAwareTests.java @@ -0,0 +1,417 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.StandardAnnotationMetadata; +import org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor; +import org.springframework.util.Assert; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests that an ImportAware @Configuration classes gets injected with the + * annotation metadata of the @Configuration class that imported it. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + */ +public class ImportAwareTests { + + @Test + public void directlyAnnotatedWithImport() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ImportingConfig.class); + ctx.refresh(); + assertThat(ctx.getBean("importedConfigBean")).isNotNull(); + + ImportedConfig importAwareConfig = ctx.getBean(ImportedConfig.class); + AnnotationMetadata importMetadata = importAwareConfig.importMetadata; + assertThat(importMetadata).isNotNull(); + assertThat(importMetadata.getClassName()).isEqualTo(ImportingConfig.class.getName()); + AnnotationAttributes importAttribs = AnnotationConfigUtils.attributesFor(importMetadata, Import.class); + Class[] importedClasses = importAttribs.getClassArray("value"); + assertThat(importedClasses[0].getName()).isEqualTo(ImportedConfig.class.getName()); + } + + @Test + public void indirectlyAnnotatedWithImport() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(IndirectlyImportingConfig.class); + ctx.refresh(); + assertThat(ctx.getBean("importedConfigBean")).isNotNull(); + + ImportedConfig importAwareConfig = ctx.getBean(ImportedConfig.class); + AnnotationMetadata importMetadata = importAwareConfig.importMetadata; + assertThat(importMetadata).isNotNull(); + assertThat(importMetadata.getClassName()).isEqualTo(IndirectlyImportingConfig.class.getName()); + AnnotationAttributes enableAttribs = AnnotationConfigUtils.attributesFor(importMetadata, EnableImportedConfig.class); + String foo = enableAttribs.getString("foo"); + assertThat(foo).isEqualTo("xyz"); + } + + @Test + public void directlyAnnotatedWithImportLite() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ImportingConfigLite.class); + ctx.refresh(); + assertThat(ctx.getBean("importedConfigBean")).isNotNull(); + + ImportedConfigLite importAwareConfig = ctx.getBean(ImportedConfigLite.class); + AnnotationMetadata importMetadata = importAwareConfig.importMetadata; + assertThat(importMetadata).isNotNull(); + assertThat(importMetadata.getClassName()).isEqualTo(ImportingConfigLite.class.getName()); + AnnotationAttributes importAttribs = AnnotationConfigUtils.attributesFor(importMetadata, Import.class); + Class[] importedClasses = importAttribs.getClassArray("value"); + assertThat(importedClasses[0].getName()).isEqualTo(ImportedConfigLite.class.getName()); + } + + @Test + public void importRegistrar() { + ImportedRegistrar.called = false; + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ImportingRegistrarConfig.class); + ctx.refresh(); + assertThat(ctx.getBean("registrarImportedBean")).isNotNull(); + assertThat(ctx.getBean("otherImportedConfigBean")).isNotNull(); + } + + @Test + public void importRegistrarWithImport() { + ImportedRegistrar.called = false; + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ImportingRegistrarConfigWithImport.class); + ctx.refresh(); + assertThat(ctx.getBean("registrarImportedBean")).isNotNull(); + assertThat(ctx.getBean("otherImportedConfigBean")).isNotNull(); + assertThat(ctx.getBean("importedConfigBean")).isNotNull(); + assertThat(ctx.getBean(ImportedConfig.class)).isNotNull(); + } + + @Test + public void metadataFromImportsOneThenTwo() { + AnnotationMetadata importMetadata = new AnnotationConfigApplicationContext( + ConfigurationOne.class, ConfigurationTwo.class) + .getBean(MetadataHolder.class).importMetadata; + assertThat(((StandardAnnotationMetadata) importMetadata).getIntrospectedClass()).isEqualTo(ConfigurationOne.class); + } + + @Test + public void metadataFromImportsTwoThenOne() { + AnnotationMetadata importMetadata = new AnnotationConfigApplicationContext( + ConfigurationTwo.class, ConfigurationOne.class) + .getBean(MetadataHolder.class).importMetadata; + assertThat(((StandardAnnotationMetadata) importMetadata).getIntrospectedClass()).isEqualTo(ConfigurationOne.class); + } + + @Test + public void metadataFromImportsOneThenThree() { + AnnotationMetadata importMetadata = new AnnotationConfigApplicationContext( + ConfigurationOne.class, ConfigurationThree.class) + .getBean(MetadataHolder.class).importMetadata; + assertThat(((StandardAnnotationMetadata) importMetadata).getIntrospectedClass()).isEqualTo(ConfigurationOne.class); + } + + @Test + public void importAwareWithAnnotationAttributes() { + new AnnotationConfigApplicationContext(ApplicationConfiguration.class); + } + + + @Configuration + @Import(ImportedConfig.class) + static class ImportingConfig { + } + + + @Configuration + @EnableImportedConfig(foo = "xyz") + static class IndirectlyImportingConfig { + } + + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Import(ImportedConfig.class) + public @interface EnableImportedConfig { + String foo() default ""; + } + + + @Configuration + static class ImportedConfig implements ImportAware { + + AnnotationMetadata importMetadata; + + @Override + public void setImportMetadata(AnnotationMetadata importMetadata) { + this.importMetadata = importMetadata; + } + + @Bean + public BPP importedConfigBean() { + return new BPP(); + } + + @Bean + public AsyncAnnotationBeanPostProcessor asyncBPP() { + return new AsyncAnnotationBeanPostProcessor(); + } + } + + + @Configuration + static class OtherImportedConfig { + + @Bean + public String otherImportedConfigBean() { + return ""; + } + } + + + @Configuration + @Import(ImportedConfigLite.class) + static class ImportingConfigLite { + } + + + @Configuration(proxyBeanMethods = false) + static class ImportedConfigLite implements ImportAware { + + AnnotationMetadata importMetadata; + + @Override + public void setImportMetadata(AnnotationMetadata importMetadata) { + this.importMetadata = importMetadata; + } + + @Bean + public BPP importedConfigBean() { + return new BPP(); + } + + @Bean + public AsyncAnnotationBeanPostProcessor asyncBPP() { + return new AsyncAnnotationBeanPostProcessor(); + } + } + + + static class BPP implements BeanPostProcessor, BeanFactoryAware { + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) { + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + return bean; + } + } + + + @Configuration + @EnableImportRegistrar + static class ImportingRegistrarConfig { + } + + + @Configuration + @EnableImportRegistrar + @Import(ImportedConfig.class) + static class ImportingRegistrarConfigWithImport { + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Import(ImportedRegistrar.class) + public @interface EnableImportRegistrar { + } + + + static class ImportedRegistrar implements ImportBeanDefinitionRegistrar { + + static boolean called; + + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); + beanDefinition.setBeanClassName(String.class.getName()); + registry.registerBeanDefinition("registrarImportedBean", beanDefinition); + GenericBeanDefinition beanDefinition2 = new GenericBeanDefinition(); + beanDefinition2.setBeanClass(OtherImportedConfig.class); + registry.registerBeanDefinition("registrarImportedConfig", beanDefinition2); + Assert.state(!called, "ImportedRegistrar called twice"); + called = true; + } + } + + + @EnableSomeConfiguration("bar") + @Configuration + public static class ConfigurationOne { + } + + + @Conditional(OnMissingBeanCondition.class) + @EnableSomeConfiguration("foo") + @Configuration + public static class ConfigurationTwo { + } + + + @Conditional(OnMissingBeanCondition.class) + @EnableLiteConfiguration("foo") + @Configuration + public static class ConfigurationThree { + } + + + @Import(SomeConfiguration.class) + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + public @interface EnableSomeConfiguration { + + String value() default ""; + } + + + @Configuration + public static class SomeConfiguration implements ImportAware { + + private AnnotationMetadata importMetadata; + + @Override + public void setImportMetadata(AnnotationMetadata importMetadata) { + this.importMetadata = importMetadata; + } + + @Bean + public MetadataHolder holder() { + return new MetadataHolder(this.importMetadata); + } + } + + + @Import(LiteConfiguration.class) + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + public @interface EnableLiteConfiguration { + + String value() default ""; + } + + + @Configuration(proxyBeanMethods = false) + public static class LiteConfiguration implements ImportAware { + + private AnnotationMetadata importMetadata; + + @Override + public void setImportMetadata(AnnotationMetadata importMetadata) { + this.importMetadata = importMetadata; + } + + @Bean + public MetadataHolder holder() { + return new MetadataHolder(this.importMetadata); + } + } + + + public static class MetadataHolder { + + private final AnnotationMetadata importMetadata; + + public MetadataHolder(AnnotationMetadata importMetadata) { + this.importMetadata = importMetadata; + } + } + + + private static final class OnMissingBeanCondition implements ConfigurationCondition { + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + return (context.getBeanFactory().getBeanNamesForType(MetadataHolder.class, true, false).length == 0); + } + + @Override + public ConfigurationPhase getConfigurationPhase() { + return ConfigurationPhase.REGISTER_BEAN; + } + } + + + @Configuration + @EnableFeature(policies = { + @EnableFeature.FeaturePolicy(name = "one"), + @EnableFeature.FeaturePolicy(name = "two") + }) + public static class ApplicationConfiguration { + } + + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Import(FeatureConfiguration.class) + public @interface EnableFeature { + + FeaturePolicy[] policies() default {}; + + @interface FeaturePolicy { + + String name(); + } + } + + + @Configuration + public static class FeatureConfiguration implements ImportAware { + + @Override + public void setImportMetadata(AnnotationMetadata annotationMetadata) { + AnnotationAttributes enableFeatureAttributes = + AnnotationAttributes.fromMap(annotationMetadata.getAnnotationAttributes(EnableFeature.class.getName())); + assertThat(enableFeatureAttributes.annotationType()).isEqualTo(EnableFeature.class); + Arrays.stream(enableFeatureAttributes.getAnnotationArray("policies")).forEach(featurePolicyAttributes -> assertThat(featurePolicyAttributes.annotationType()).isEqualTo(EnableFeature.FeaturePolicy.class)); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ImportBeanDefinitionRegistrarTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ImportBeanDefinitionRegistrarTests.java new file mode 100644 index 0000000..6626e01 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ImportBeanDefinitionRegistrarTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.MessageSource; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.AnnotationMetadata; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Integration tests for {@link ImportBeanDefinitionRegistrar}. + * + * @author Oliver Gierke + * @author Chris Beams + */ +public class ImportBeanDefinitionRegistrarTests { + + @Test + public void shouldInvokeAwareMethodsInImportBeanDefinitionRegistrar() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class); + context.getBean(MessageSource.class); + + assertThat(SampleRegistrar.beanFactory).isEqualTo(context.getBeanFactory()); + assertThat(SampleRegistrar.classLoader).isEqualTo(context.getBeanFactory().getBeanClassLoader()); + assertThat(SampleRegistrar.resourceLoader).isNotNull(); + assertThat(SampleRegistrar.environment).isEqualTo(context.getEnvironment()); + } + + + @Sample + @Configuration + static class Config { + } + + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Import(SampleRegistrar.class) + public @interface Sample { + } + + + private static class SampleRegistrar implements ImportBeanDefinitionRegistrar, + BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware { + + static ClassLoader classLoader; + static ResourceLoader resourceLoader; + static BeanFactory beanFactory; + static Environment environment; + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + SampleRegistrar.classLoader = classLoader; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + SampleRegistrar.beanFactory = beanFactory; + } + + @Override + public void setResourceLoader(ResourceLoader resourceLoader) { + SampleRegistrar.resourceLoader = resourceLoader; + } + + @Override + public void setEnvironment(Environment environment) { + SampleRegistrar.environment = environment; + } + + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, + BeanDefinitionRegistry registry) { + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ImportSelectorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ImportSelectorTests.java new file mode 100644 index 0000000..1b92147 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ImportSelectorTests.java @@ -0,0 +1,583 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.lang.Nullable; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.spy; + +/** + * Tests for {@link ImportSelector} and {@link DeferredImportSelector}. + * + * @author Phillip Webb + * @author Stephane Nicoll + */ +@SuppressWarnings("resource") +public class ImportSelectorTests { + + static Map, String> importFrom = new HashMap<>(); + + + @BeforeEach + public void cleanup() { + ImportSelectorTests.importFrom.clear(); + SampleImportSelector.cleanup(); + TestImportGroup.cleanup(); + } + + + @Test + public void importSelectors() { + DefaultListableBeanFactory beanFactory = spy(new DefaultListableBeanFactory()); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(beanFactory); + context.register(Config.class); + context.refresh(); + context.getBean(Config.class); + InOrder ordered = inOrder(beanFactory); + ordered.verify(beanFactory).registerBeanDefinition(eq("a"), any()); + ordered.verify(beanFactory).registerBeanDefinition(eq("b"), any()); + ordered.verify(beanFactory).registerBeanDefinition(eq("d"), any()); + ordered.verify(beanFactory).registerBeanDefinition(eq("c"), any()); + } + + @Test + public void invokeAwareMethodsInImportSelector() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AwareConfig.class); + assertThat(SampleImportSelector.beanFactory).isEqualTo(context.getBeanFactory()); + assertThat(SampleImportSelector.classLoader).isEqualTo(context.getBeanFactory().getBeanClassLoader()); + assertThat(SampleImportSelector.resourceLoader).isNotNull(); + assertThat(SampleImportSelector.environment).isEqualTo(context.getEnvironment()); + } + + @Test + public void correctMetadataOnIndirectImports() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(IndirectConfig.class); + String indirectImport = IndirectImport.class.getName(); + assertThat(importFrom.get(ImportSelector1.class)).isEqualTo(indirectImport); + assertThat(importFrom.get(ImportSelector2.class)).isEqualTo(indirectImport); + assertThat(importFrom.get(DeferredImportSelector1.class)).isEqualTo(indirectImport); + assertThat(importFrom.get(DeferredImportSelector2.class)).isEqualTo(indirectImport); + assertThat(context.containsBean("a")).isFalse(); // since ImportedSelector1 got filtered + assertThat(context.containsBean("b")).isTrue(); + assertThat(context.containsBean("c")).isTrue(); + assertThat(context.containsBean("d")).isTrue(); + } + + @Test + public void importSelectorsWithGroup() { + DefaultListableBeanFactory beanFactory = spy(new DefaultListableBeanFactory()); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(beanFactory); + context.register(GroupedConfig.class); + context.refresh(); + InOrder ordered = inOrder(beanFactory); + ordered.verify(beanFactory).registerBeanDefinition(eq("a"), any()); + ordered.verify(beanFactory).registerBeanDefinition(eq("b"), any()); + ordered.verify(beanFactory).registerBeanDefinition(eq("c"), any()); + ordered.verify(beanFactory).registerBeanDefinition(eq("d"), any()); + assertThat(TestImportGroup.instancesCount.get()).isEqualTo(1); + assertThat(TestImportGroup.imports.size()).isEqualTo(1); + assertThat(TestImportGroup.imports.values().iterator().next().size()).isEqualTo(2); + } + + @Test + public void importSelectorsSeparateWithGroup() { + DefaultListableBeanFactory beanFactory = spy(new DefaultListableBeanFactory()); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(beanFactory); + context.register(GroupedConfig1.class); + context.register(GroupedConfig2.class); + context.refresh(); + InOrder ordered = inOrder(beanFactory); + ordered.verify(beanFactory).registerBeanDefinition(eq("c"), any()); + ordered.verify(beanFactory).registerBeanDefinition(eq("d"), any()); + assertThat(TestImportGroup.instancesCount.get()).isEqualTo(1); + assertThat(TestImportGroup.imports.size()).isEqualTo(2); + Iterator iterator = TestImportGroup.imports.keySet().iterator(); + assertThat(iterator.next().getClassName()).isEqualTo(GroupedConfig2.class.getName()); + assertThat(iterator.next().getClassName()).isEqualTo(GroupedConfig1.class.getName()); + } + + @Test + public void importSelectorsWithNestedGroup() { + DefaultListableBeanFactory beanFactory = spy(new DefaultListableBeanFactory()); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(beanFactory); + context.register(ParentConfiguration1.class); + context.refresh(); + InOrder ordered = inOrder(beanFactory); + ordered.verify(beanFactory).registerBeanDefinition(eq("a"), any()); + ordered.verify(beanFactory).registerBeanDefinition(eq("e"), any()); + ordered.verify(beanFactory).registerBeanDefinition(eq("c"), any()); + assertThat(TestImportGroup.instancesCount.get()).isEqualTo(2); + assertThat(TestImportGroup.imports.size()).isEqualTo(2); + assertThat(TestImportGroup.allImports()) + .containsOnlyKeys(ParentConfiguration1.class.getName(), ChildConfiguration1.class.getName()); + assertThat(TestImportGroup.allImports().get(ParentConfiguration1.class.getName())) + .containsExactly(DeferredImportSelector1.class.getName(), ChildConfiguration1.class.getName()); + assertThat(TestImportGroup.allImports().get(ChildConfiguration1.class.getName())) + .containsExactly(DeferredImportedSelector3.class.getName()); + } + + @Test + public void importSelectorsWithNestedGroupSameDeferredImport() { + DefaultListableBeanFactory beanFactory = spy(new DefaultListableBeanFactory()); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(beanFactory); + context.register(ParentConfiguration2.class); + context.refresh(); + InOrder ordered = inOrder(beanFactory); + ordered.verify(beanFactory).registerBeanDefinition(eq("b"), any()); + ordered.verify(beanFactory).registerBeanDefinition(eq("d"), any()); + assertThat(TestImportGroup.instancesCount.get()).isEqualTo(2); + assertThat(TestImportGroup.allImports().size()).isEqualTo(2); + assertThat(TestImportGroup.allImports()) + .containsOnlyKeys(ParentConfiguration2.class.getName(), ChildConfiguration2.class.getName()); + assertThat(TestImportGroup.allImports().get(ParentConfiguration2.class.getName())) + .containsExactly(DeferredImportSelector2.class.getName(), ChildConfiguration2.class.getName()); + assertThat(TestImportGroup.allImports().get(ChildConfiguration2.class.getName())) + .containsExactly(DeferredImportSelector2.class.getName()); + } + + @Test + public void invokeAwareMethodsInImportGroup() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(GroupedConfig1.class); + assertThat(TestImportGroup.beanFactory).isEqualTo(context.getBeanFactory()); + assertThat(TestImportGroup.classLoader).isEqualTo(context.getBeanFactory().getBeanClassLoader()); + assertThat(TestImportGroup.resourceLoader).isNotNull(); + assertThat(TestImportGroup.environment).isEqualTo(context.getEnvironment()); + } + + + @Configuration + @Import(SampleImportSelector.class) + static class AwareConfig { + } + + + private static class SampleImportSelector implements ImportSelector, + BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware { + + static ClassLoader classLoader; + static ResourceLoader resourceLoader; + static BeanFactory beanFactory; + static Environment environment; + + static void cleanup() { + SampleImportSelector.classLoader = null; + SampleImportSelector.beanFactory = null; + SampleImportSelector.resourceLoader = null; + SampleImportSelector.environment = null; + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + SampleImportSelector.classLoader = classLoader; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + SampleImportSelector.beanFactory = beanFactory; + } + + @Override + public void setResourceLoader(ResourceLoader resourceLoader) { + SampleImportSelector.resourceLoader = resourceLoader; + } + + @Override + public void setEnvironment(Environment environment) { + SampleImportSelector.environment = environment; + } + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + return new String[] {}; + } + } + + + @Sample + @Configuration + static class Config { + } + + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Import({DeferredImportSelector1.class, DeferredImportSelector2.class, + ImportSelector1.class, ImportSelector2.class}) + public @interface Sample { + } + + + public static class ImportSelector1 implements ImportSelector { + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + ImportSelectorTests.importFrom.put(getClass(), importingClassMetadata.getClassName()); + return new String[] { ImportedSelector1.class.getName() }; + } + } + + + public static class ImportSelector2 implements ImportSelector { + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + ImportSelectorTests.importFrom.put(getClass(), importingClassMetadata.getClassName()); + return new String[] { ImportedSelector2.class.getName() }; + } + } + + + public static class DeferredImportSelector1 implements DeferredImportSelector, Ordered { + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + ImportSelectorTests.importFrom.put(getClass(), importingClassMetadata.getClassName()); + return new String[] { DeferredImportedSelector1.class.getName() }; + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + } + + + @Order(Ordered.HIGHEST_PRECEDENCE) + public static class DeferredImportSelector2 implements DeferredImportSelector { + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + ImportSelectorTests.importFrom.put(getClass(), importingClassMetadata.getClassName()); + return new String[] { DeferredImportedSelector2.class.getName() }; + } + } + + + @Configuration + public static class ImportedSelector1 { + + @Bean + public String a() { + return "a"; + } + } + + + @Configuration + public static class ImportedSelector2 { + + @Bean + public String b() { + return "b"; + } + } + + + @Configuration + public static class DeferredImportedSelector1 { + + @Bean + public String c() { + return "c"; + } + } + + + @Configuration + public static class DeferredImportedSelector2 { + + @Bean + public String d() { + return "d"; + } + } + + @Configuration + public static class DeferredImportedSelector3 { + + @Bean + public String e() { + return "e"; + } + } + + + @Configuration + @Import(IndirectImportSelector.class) + public static class IndirectConfig { + } + + + public static class IndirectImportSelector implements ImportSelector { + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + return new String[] {IndirectImport.class.getName()}; + } + + @Override + @Nullable + public Predicate getExclusionFilter() { + return className -> className.endsWith("ImportedSelector1"); + } + } + + + @Sample + public static class IndirectImport { + } + + + @GroupedSample + @Configuration + static class GroupedConfig { + } + + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Import({GroupedDeferredImportSelector1.class, GroupedDeferredImportSelector2.class, ImportSelector1.class, ImportSelector2.class}) + public @interface GroupedSample { + } + + @Configuration + @Import(GroupedDeferredImportSelector1.class) + static class GroupedConfig1 { + } + + @Configuration + @Import(GroupedDeferredImportSelector2.class) + static class GroupedConfig2 { + } + + + public static class GroupedDeferredImportSelector1 extends DeferredImportSelector1 { + + @Nullable + @Override + public Class getImportGroup() { + return TestImportGroup.class; + } + } + + public static class GroupedDeferredImportSelector2 extends DeferredImportSelector2 { + + @Nullable + @Override + public Class getImportGroup() { + return TestImportGroup.class; + } + } + + + @Configuration + @Import({ImportSelector1.class, ParentDeferredImportSelector1.class}) + public static class ParentConfiguration1 { + } + + + public static class ParentDeferredImportSelector1 implements DeferredImportSelector { + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + ImportSelectorTests.importFrom.put(getClass(), importingClassMetadata.getClassName()); + return new String[] { DeferredImportSelector1.class.getName(), ChildConfiguration1.class.getName() }; + } + + @Nullable + @Override + public Class getImportGroup() { + return TestImportGroup.class; + } + + } + + @Configuration + @Import({ImportSelector2.class, ParentDeferredImportSelector2.class}) + public static class ParentConfiguration2 { + } + + public static class ParentDeferredImportSelector2 implements DeferredImportSelector { + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + ImportSelectorTests.importFrom.put(getClass(), importingClassMetadata.getClassName()); + return new String[] { DeferredImportSelector2.class.getName(), ChildConfiguration2.class.getName() }; + } + + @Nullable + @Override + public Class getImportGroup() { + return TestImportGroup.class; + } + + } + + @Configuration + @Import(ChildDeferredImportSelector1.class) + public static class ChildConfiguration1 { + + } + + + public static class ChildDeferredImportSelector1 implements DeferredImportSelector { + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + ImportSelectorTests.importFrom.put(getClass(), importingClassMetadata.getClassName()); + return new String[] { DeferredImportedSelector3.class.getName() }; + } + + @Nullable + @Override + public Class getImportGroup() { + return TestImportGroup.class; + } + + } + + @Configuration + @Import(ChildDeferredImportSelector2.class) + public static class ChildConfiguration2 { + + } + + public static class ChildDeferredImportSelector2 implements DeferredImportSelector { + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + ImportSelectorTests.importFrom.put(getClass(), importingClassMetadata.getClassName()); + return new String[] { DeferredImportSelector2.class.getName() }; + } + + @Nullable + @Override + public Class getImportGroup() { + return TestImportGroup.class; + } + + } + + + public static class TestImportGroup implements DeferredImportSelector.Group, + BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware { + + static ClassLoader classLoader; + static ResourceLoader resourceLoader; + static BeanFactory beanFactory; + static Environment environment; + + static AtomicInteger instancesCount = new AtomicInteger(); + static MultiValueMap imports = new LinkedMultiValueMap<>(); + + public TestImportGroup() { + TestImportGroup.instancesCount.incrementAndGet(); + } + + static void cleanup() { + TestImportGroup.classLoader = null; + TestImportGroup.beanFactory = null; + TestImportGroup.resourceLoader = null; + TestImportGroup.environment = null; + TestImportGroup.instancesCount = new AtomicInteger(); + TestImportGroup.imports.clear(); + } + + static Map> allImports() { + return TestImportGroup.imports.entrySet() + .stream() + .collect(Collectors.toMap(entry -> entry.getKey().getClassName(), + Map.Entry::getValue)); + } + + private final List instanceImports = new ArrayList<>(); + + @Override + public void process(AnnotationMetadata metadata, DeferredImportSelector selector) { + for (String importClassName : selector.selectImports(metadata)) { + this.instanceImports.add(new Entry(metadata, importClassName)); + } + TestImportGroup.imports.addAll(metadata, + Arrays.asList(selector.selectImports(metadata))); + } + + @Override + public Iterable selectImports() { + ArrayList content = new ArrayList<>(this.instanceImports); + Collections.reverse(content); + return content; + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + TestImportGroup.classLoader = classLoader; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + TestImportGroup.beanFactory = beanFactory; + } + + @Override + public void setResourceLoader(ResourceLoader resourceLoader) { + TestImportGroup.resourceLoader = resourceLoader; + } + + @Override + public void setEnvironment(Environment environment) { + TestImportGroup.environment = environment; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ImportVersusDirectRegistrationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ImportVersusDirectRegistrationTests.java new file mode 100644 index 0000000..db22532 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ImportVersusDirectRegistrationTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.support.RootBeanDefinition; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Andy Wilkinson + */ +public class ImportVersusDirectRegistrationTests { + + @Test + public void thingIsNotAvailableWhenOuterConfigurationIsRegisteredDirectly() { + try (AnnotationConfigApplicationContext directRegistration = new AnnotationConfigApplicationContext()) { + directRegistration.register(AccidentalLiteConfiguration.class); + directRegistration.refresh(); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> + directRegistration.getBean(Thing.class)); + } + } + + @Test + public void thingIsNotAvailableWhenOuterConfigurationIsRegisteredWithClassName() { + try (AnnotationConfigApplicationContext directRegistration = new AnnotationConfigApplicationContext()) { + directRegistration.registerBeanDefinition("config", + new RootBeanDefinition(AccidentalLiteConfiguration.class.getName())); + directRegistration.refresh(); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> + directRegistration.getBean(Thing.class)); + } + } + + @Test + public void thingIsNotAvailableWhenOuterConfigurationIsImported() { + try (AnnotationConfigApplicationContext viaImport = new AnnotationConfigApplicationContext()) { + viaImport.register(Importer.class); + viaImport.refresh(); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> + viaImport.getBean(Thing.class)); + } + } + +} + + +@Import(AccidentalLiteConfiguration.class) +class Importer { +} + + +class AccidentalLiteConfiguration { + + @Configuration + class InnerConfiguration { + + @Bean + public Thing thing() { + return new Thing(); + } + } +} + + +class Thing { +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/InvalidConfigurationClassDefinitionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/InvalidConfigurationClassDefinitionTests.java new file mode 100644 index 0000000..d2114d8 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/InvalidConfigurationClassDefinitionTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.beans.factory.support.BeanDefinitionBuilder.rootBeanDefinition; + +/** + * Unit tests covering cases where a user defines an invalid Configuration + * class, e.g.: forgets to annotate with {@link Configuration} or declares + * a Configuration class as final. + * + * @author Chris Beams + */ +public class InvalidConfigurationClassDefinitionTests { + + @Test + public void configurationClassesMayNotBeFinal() { + @Configuration + final class Config { } + + BeanDefinition configBeanDef = rootBeanDefinition(Config.class).getBeanDefinition(); + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition("config", configBeanDef); + + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + assertThatExceptionOfType(BeanDefinitionParsingException.class).isThrownBy(() -> + pp.postProcessBeanFactory(beanFactory)) + .withMessageContaining("Remove the final modifier"); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/LazyAutowiredAnnotationBeanPostProcessorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/LazyAutowiredAnnotationBeanPostProcessorTests.java new file mode 100644 index 0000000..8bd05b7 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/LazyAutowiredAnnotationBeanPostProcessorTests.java @@ -0,0 +1,329 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.util.ObjectUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Juergen Hoeller + * @since 4.0 + */ +public class LazyAutowiredAnnotationBeanPostProcessorTests { + + private void doTestLazyResourceInjection(Class annotatedBeanClass) { + AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(); + RootBeanDefinition abd = new RootBeanDefinition(annotatedBeanClass); + abd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + ac.registerBeanDefinition("annotatedBean", abd); + RootBeanDefinition tbd = new RootBeanDefinition(TestBean.class); + tbd.setLazyInit(true); + ac.registerBeanDefinition("testBean", tbd); + ac.refresh(); + + ConfigurableListableBeanFactory bf = ac.getBeanFactory(); + TestBeanHolder bean = ac.getBean("annotatedBean", TestBeanHolder.class); + assertThat(bf.containsSingleton("testBean")).isFalse(); + assertThat(bean.getTestBean()).isNotNull(); + assertThat(bean.getTestBean().getName()).isNull(); + assertThat(bf.containsSingleton("testBean")).isTrue(); + TestBean tb = (TestBean) ac.getBean("testBean"); + tb.setName("tb"); + assertThat(bean.getTestBean().getName()).isSameAs("tb"); + + assertThat(ObjectUtils.containsElement(bf.getDependenciesForBean("annotatedBean"), "testBean")).isTrue(); + assertThat(ObjectUtils.containsElement(bf.getDependentBeans("testBean"), "annotatedBean")).isTrue(); + } + + @Test + public void testLazyResourceInjectionWithField() { + doTestLazyResourceInjection(FieldResourceInjectionBean.class); + + AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(); + RootBeanDefinition abd = new RootBeanDefinition(FieldResourceInjectionBean.class); + abd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + ac.registerBeanDefinition("annotatedBean", abd); + RootBeanDefinition tbd = new RootBeanDefinition(TestBean.class); + tbd.setLazyInit(true); + ac.registerBeanDefinition("testBean", tbd); + ac.refresh(); + + FieldResourceInjectionBean bean = ac.getBean("annotatedBean", FieldResourceInjectionBean.class); + assertThat(ac.getBeanFactory().containsSingleton("testBean")).isFalse(); + assertThat(bean.getTestBeans().isEmpty()).isFalse(); + assertThat(bean.getTestBeans().get(0).getName()).isNull(); + assertThat(ac.getBeanFactory().containsSingleton("testBean")).isTrue(); + TestBean tb = (TestBean) ac.getBean("testBean"); + tb.setName("tb"); + assertThat(bean.getTestBean().getName()).isSameAs("tb"); + } + + @Test + public void testLazyResourceInjectionWithFieldAndCustomAnnotation() { + doTestLazyResourceInjection(FieldResourceInjectionBeanWithCompositeAnnotation.class); + } + + @Test + public void testLazyResourceInjectionWithMethod() { + doTestLazyResourceInjection(MethodResourceInjectionBean.class); + } + + @Test + public void testLazyResourceInjectionWithMethodLevelLazy() { + doTestLazyResourceInjection(MethodResourceInjectionBeanWithMethodLevelLazy.class); + } + + @Test + public void testLazyResourceInjectionWithMethodAndCustomAnnotation() { + doTestLazyResourceInjection(MethodResourceInjectionBeanWithCompositeAnnotation.class); + } + + @Test + public void testLazyResourceInjectionWithConstructor() { + doTestLazyResourceInjection(ConstructorResourceInjectionBean.class); + } + + @Test + public void testLazyResourceInjectionWithConstructorLevelLazy() { + doTestLazyResourceInjection(ConstructorResourceInjectionBeanWithConstructorLevelLazy.class); + } + + @Test + public void testLazyResourceInjectionWithConstructorAndCustomAnnotation() { + doTestLazyResourceInjection(ConstructorResourceInjectionBeanWithCompositeAnnotation.class); + } + + @Test + public void testLazyResourceInjectionWithNonExistingTarget() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver()); + AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); + bpp.setBeanFactory(bf); + bf.addBeanPostProcessor(bpp); + RootBeanDefinition bd = new RootBeanDefinition(FieldResourceInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + + FieldResourceInjectionBean bean = (FieldResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isNotNull(); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> + bean.getTestBean().getName()); + } + + @Test + public void testLazyOptionalResourceInjectionWithNonExistingTarget() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver()); + AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); + bpp.setBeanFactory(bf); + bf.addBeanPostProcessor(bpp); + RootBeanDefinition bd = new RootBeanDefinition(OptionalFieldResourceInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + + OptionalFieldResourceInjectionBean bean = (OptionalFieldResourceInjectionBean) bf.getBean("annotatedBean"); + assertThat(bean.getTestBean()).isNotNull(); + assertThat(bean.getTestBeans()).isNotNull(); + assertThat(bean.getTestBeans().isEmpty()).isTrue(); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> + bean.getTestBean().getName()); + } + + + public interface TestBeanHolder { + + TestBean getTestBean(); + } + + + public static class FieldResourceInjectionBean implements TestBeanHolder { + + @Autowired @Lazy + private TestBean testBean; + + @Autowired @Lazy + private List testBeans; + + @Override + public TestBean getTestBean() { + return this.testBean; + } + + public List getTestBeans() { + return testBeans; + } + } + + + public static class OptionalFieldResourceInjectionBean implements TestBeanHolder { + + @Autowired(required = false) @Lazy + private TestBean testBean; + + @Autowired(required = false) @Lazy + private List testBeans; + + @Override + public TestBean getTestBean() { + return this.testBean; + } + + public List getTestBeans() { + return this.testBeans; + } + } + + + public static class FieldResourceInjectionBeanWithCompositeAnnotation implements TestBeanHolder { + + @LazyInject + private TestBean testBean; + + @Override + public TestBean getTestBean() { + return this.testBean; + } + } + + + public static class MethodResourceInjectionBean implements TestBeanHolder { + + private TestBean testBean; + + @Autowired + public void setTestBean(@Lazy TestBean testBean) { + if (this.testBean != null) { + throw new IllegalStateException("Already called"); + } + this.testBean = testBean; + } + + @Override + public TestBean getTestBean() { + return this.testBean; + } + } + + + public static class MethodResourceInjectionBeanWithMethodLevelLazy implements TestBeanHolder { + + private TestBean testBean; + + @Autowired @Lazy + public void setTestBean(TestBean testBean) { + if (this.testBean != null) { + throw new IllegalStateException("Already called"); + } + this.testBean = testBean; + } + + @Override + public TestBean getTestBean() { + return this.testBean; + } + } + + + public static class MethodResourceInjectionBeanWithCompositeAnnotation implements TestBeanHolder { + + private TestBean testBean; + + @LazyInject + public void setTestBean(TestBean testBean) { + if (this.testBean != null) { + throw new IllegalStateException("Already called"); + } + this.testBean = testBean; + } + + @Override + public TestBean getTestBean() { + return this.testBean; + } + } + + + public static class ConstructorResourceInjectionBean implements TestBeanHolder { + + private final TestBean testBean; + + @Autowired + public ConstructorResourceInjectionBean(@Lazy TestBean testBean) { + this.testBean = testBean; + } + + @Override + public TestBean getTestBean() { + return this.testBean; + } + } + + + public static class ConstructorResourceInjectionBeanWithConstructorLevelLazy implements TestBeanHolder { + + private final TestBean testBean; + + @Autowired @Lazy + public ConstructorResourceInjectionBeanWithConstructorLevelLazy(TestBean testBean) { + this.testBean = testBean; + } + + @Override + public TestBean getTestBean() { + return this.testBean; + } + } + + + public static class ConstructorResourceInjectionBeanWithCompositeAnnotation implements TestBeanHolder { + + private final TestBean testBean; + + @LazyInject + public ConstructorResourceInjectionBeanWithCompositeAnnotation(TestBean testBean) { + this.testBean = testBean; + } + + @Override + public TestBean getTestBean() { + return this.testBean; + } + } + + + @Autowired @Lazy + @Retention(RetentionPolicy.RUNTIME) + public @interface LazyInject { + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/MyTestBean.java b/spring-context/src/test/java/org/springframework/context/annotation/MyTestBean.java new file mode 100644 index 0000000..5baa5c6 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/MyTestBean.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +/** + * @author Juergen Hoeller + */ +@Configuration +class MyTestBean { + + @Bean + public org.springframework.beans.testfixture.beans.TestBean myTestBean() { + return new org.springframework.beans.testfixture.beans.TestBean(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/NestedConfigurationClassTests.java b/spring-context/src/test/java/org/springframework/context/annotation/NestedConfigurationClassTests.java new file mode 100644 index 0000000..8d7c1c3 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/NestedConfigurationClassTests.java @@ -0,0 +1,408 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.stereotype.Component; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests ensuring that nested static @Configuration classes are automatically detected + * and registered without the need for explicit registration or @Import. See SPR-8186. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + */ +public class NestedConfigurationClassTests { + + @Test + public void oneLevelDeep() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(L0Config.L1Config.class); + ctx.refresh(); + + assertThat(ctx.containsBean("l0Bean")).isFalse(); + + ctx.getBean(L0Config.L1Config.class); + ctx.getBean("l1Bean"); + + ctx.getBean(L0Config.L1Config.L2Config.class); + ctx.getBean("l2Bean"); + + // ensure that override order is correct + assertThat(ctx.getBean("overrideBean", TestBean.class).getName()).isEqualTo("override-l1"); + } + + @Test + public void twoLevelsDeep() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(L0Config.class); + ctx.refresh(); + + assertThat(ctx.getBeanFactory().containsSingleton("nestedConfigurationClassTests.L0Config")).isFalse(); + ctx.getBean(L0Config.class); + ctx.getBean("l0Bean"); + + assertThat(ctx.getBeanFactory().containsSingleton(L0Config.L1Config.class.getName())).isTrue(); + ctx.getBean(L0Config.L1Config.class); + ctx.getBean("l1Bean"); + + assertThat(ctx.getBeanFactory().containsSingleton(L0Config.L1Config.L2Config.class.getName())).isFalse(); + ctx.getBean(L0Config.L1Config.L2Config.class); + ctx.getBean("l2Bean"); + + // ensure that override order is correct + assertThat(ctx.getBean("overrideBean", TestBean.class).getName()).isEqualTo("override-l0"); + } + + @Test + public void twoLevelsInLiteMode() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(L0ConfigLight.class); + ctx.refresh(); + + assertThat(ctx.getBeanFactory().containsSingleton("nestedConfigurationClassTests.L0ConfigLight")).isFalse(); + ctx.getBean(L0ConfigLight.class); + ctx.getBean("l0Bean"); + + assertThat(ctx.getBeanFactory().containsSingleton(L0ConfigLight.L1ConfigLight.class.getName())).isTrue(); + ctx.getBean(L0ConfigLight.L1ConfigLight.class); + ctx.getBean("l1Bean"); + + assertThat(ctx.getBeanFactory().containsSingleton(L0ConfigLight.L1ConfigLight.L2ConfigLight.class.getName())).isFalse(); + ctx.getBean(L0ConfigLight.L1ConfigLight.L2ConfigLight.class); + ctx.getBean("l2Bean"); + + // ensure that override order is correct + assertThat(ctx.getBean("overrideBean", TestBean.class).getName()).isEqualTo("override-l0"); + } + + @Test + public void twoLevelsDeepWithInheritance() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(S1Config.class); + ctx.refresh(); + + S1Config config = ctx.getBean(S1Config.class); + assertThat(config != ctx.getBean(S1Config.class)).isTrue(); + TestBean tb = ctx.getBean("l0Bean", TestBean.class); + assertThat(tb == ctx.getBean("l0Bean", TestBean.class)).isTrue(); + + ctx.getBean(L0Config.L1Config.class); + ctx.getBean("l1Bean"); + + ctx.getBean(L0Config.L1Config.L2Config.class); + ctx.getBean("l2Bean"); + + // ensure that override order is correct and that it is a singleton + TestBean ob = ctx.getBean("overrideBean", TestBean.class); + assertThat(ob.getName()).isEqualTo("override-s1"); + assertThat(ob == ctx.getBean("overrideBean", TestBean.class)).isTrue(); + + TestBean pb1 = ctx.getBean("prototypeBean", TestBean.class); + TestBean pb2 = ctx.getBean("prototypeBean", TestBean.class); + assertThat(pb1 != pb2).isTrue(); + assertThat(pb1.getFriends().iterator().next() != pb2.getFriends().iterator().next()).isTrue(); + } + + @Test + public void twoLevelsDeepWithInheritanceThroughImport() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(S1Importer.class); + ctx.refresh(); + + S1Config config = ctx.getBean(S1Config.class); + assertThat(config != ctx.getBean(S1Config.class)).isTrue(); + TestBean tb = ctx.getBean("l0Bean", TestBean.class); + assertThat(tb == ctx.getBean("l0Bean", TestBean.class)).isTrue(); + + ctx.getBean(L0Config.L1Config.class); + ctx.getBean("l1Bean"); + + ctx.getBean(L0Config.L1Config.L2Config.class); + ctx.getBean("l2Bean"); + + // ensure that override order is correct and that it is a singleton + TestBean ob = ctx.getBean("overrideBean", TestBean.class); + assertThat(ob.getName()).isEqualTo("override-s1"); + assertThat(ob == ctx.getBean("overrideBean", TestBean.class)).isTrue(); + + TestBean pb1 = ctx.getBean("prototypeBean", TestBean.class); + TestBean pb2 = ctx.getBean("prototypeBean", TestBean.class); + assertThat(pb1 != pb2).isTrue(); + assertThat(pb1.getFriends().iterator().next() != pb2.getFriends().iterator().next()).isTrue(); + } + + @Test + public void twoLevelsDeepWithInheritanceAndScopedProxy() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(S1ImporterWithProxy.class); + ctx.refresh(); + + S1ConfigWithProxy config = ctx.getBean(S1ConfigWithProxy.class); + assertThat(config == ctx.getBean(S1ConfigWithProxy.class)).isTrue(); + TestBean tb = ctx.getBean("l0Bean", TestBean.class); + assertThat(tb == ctx.getBean("l0Bean", TestBean.class)).isTrue(); + + ctx.getBean(L0Config.L1Config.class); + ctx.getBean("l1Bean"); + + ctx.getBean(L0Config.L1Config.L2Config.class); + ctx.getBean("l2Bean"); + + // ensure that override order is correct and that it is a singleton + TestBean ob = ctx.getBean("overrideBean", TestBean.class); + assertThat(ob.getName()).isEqualTo("override-s1"); + assertThat(ob == ctx.getBean("overrideBean", TestBean.class)).isTrue(); + + TestBean pb1 = ctx.getBean("prototypeBean", TestBean.class); + TestBean pb2 = ctx.getBean("prototypeBean", TestBean.class); + assertThat(pb1 != pb2).isTrue(); + assertThat(pb1.getFriends().iterator().next() != pb2.getFriends().iterator().next()).isTrue(); + } + + @Test + public void twoLevelsWithNoBeanMethods() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(L0ConfigEmpty.class); + ctx.refresh(); + + assertThat(ctx.getBeanFactory().containsSingleton("l0ConfigEmpty")).isFalse(); + Object l0i1 = ctx.getBean(L0ConfigEmpty.class); + Object l0i2 = ctx.getBean(L0ConfigEmpty.class); + assertThat(l0i1 == l0i2).isTrue(); + + Object l1i1 = ctx.getBean(L0ConfigEmpty.L1ConfigEmpty.class); + Object l1i2 = ctx.getBean(L0ConfigEmpty.L1ConfigEmpty.class); + assertThat(l1i1 != l1i2).isTrue(); + + Object l2i1 = ctx.getBean(L0ConfigEmpty.L1ConfigEmpty.L2ConfigEmpty.class); + Object l2i2 = ctx.getBean(L0ConfigEmpty.L1ConfigEmpty.L2ConfigEmpty.class); + assertThat(l2i1 == l2i2).isTrue(); + assertThat(l2i2.toString()).isNotEqualTo(l2i1.toString()); + } + + @Test + public void twoLevelsOnNonAnnotatedBaseClass() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(L0ConfigConcrete.class); + ctx.refresh(); + + assertThat(ctx.getBeanFactory().containsSingleton("l0ConfigConcrete")).isFalse(); + Object l0i1 = ctx.getBean(L0ConfigConcrete.class); + Object l0i2 = ctx.getBean(L0ConfigConcrete.class); + assertThat(l0i1 == l0i2).isTrue(); + + Object l1i1 = ctx.getBean(L0ConfigConcrete.L1ConfigEmpty.class); + Object l1i2 = ctx.getBean(L0ConfigConcrete.L1ConfigEmpty.class); + assertThat(l1i1 != l1i2).isTrue(); + + Object l2i1 = ctx.getBean(L0ConfigConcrete.L1ConfigEmpty.L2ConfigEmpty.class); + Object l2i2 = ctx.getBean(L0ConfigConcrete.L1ConfigEmpty.L2ConfigEmpty.class); + assertThat(l2i1 == l2i2).isTrue(); + assertThat(l2i2.toString()).isNotEqualTo(l2i1.toString()); + } + + + @Configuration + @Lazy + static class L0Config { + + @Bean + @Lazy + public TestBean l0Bean() { + return new TestBean("l0"); + } + + @Bean + @Lazy + public TestBean overrideBean() { + return new TestBean("override-l0"); + } + + @Configuration + static class L1Config { + + @Bean + public TestBean l1Bean() { + return new TestBean("l1"); + } + + @Bean + public TestBean overrideBean() { + return new TestBean("override-l1"); + } + + @Configuration + @Lazy + protected static class L2Config { + + @Bean + @Lazy + public TestBean l2Bean() { + return new TestBean("l2"); + } + + @Bean + @Lazy + public TestBean overrideBean() { + return new TestBean("override-l2"); + } + } + } + } + + + @Component + @Lazy + static class L0ConfigLight { + + @Bean + @Lazy + public TestBean l0Bean() { + return new TestBean("l0"); + } + + @Bean + @Lazy + public TestBean overrideBean() { + return new TestBean("override-l0"); + } + + @Component + static class L1ConfigLight { + + @Bean + public TestBean l1Bean() { + return new TestBean("l1"); + } + + @Bean + public TestBean overrideBean() { + return new TestBean("override-l1"); + } + + @Component + @Lazy + protected static class L2ConfigLight { + + @Bean + @Lazy + public TestBean l2Bean() { + return new TestBean("l2"); + } + + @Bean + @Lazy + public TestBean overrideBean() { + return new TestBean("override-l2"); + } + } + } + } + + + @Configuration + @Scope("prototype") + static class S1Config extends L0Config { + + @Override + @Bean + public TestBean overrideBean() { + return new TestBean("override-s1"); + } + + @Bean + @Scope("prototype") + public TestBean prototypeBean() { + TestBean tb = new TestBean("override-s1"); + tb.getFriends().add(this); + return tb; + } + } + + + @Configuration + @Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS) + static class S1ConfigWithProxy extends L0Config { + + @Override + @Bean + public TestBean overrideBean() { + return new TestBean("override-s1"); + } + + @Bean + @Scope("prototype") + public TestBean prototypeBean() { + TestBean tb = new TestBean("override-s1"); + tb.getFriends().add(this); + return tb; + } + } + + + @Import(S1Config.class) + static class S1Importer { + } + + + @Import(S1ConfigWithProxy.class) + static class S1ImporterWithProxy { + } + + + @Component + @Lazy + static class L0ConfigEmpty { + + @Component + @Scope("prototype") + static class L1ConfigEmpty { + + @Component + @Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS) + protected static class L2ConfigEmpty { + } + } + } + + + static class L0ConfigBase { + + @Component + @Scope("prototype") + static class L1ConfigEmpty { + + @Component + @Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS) + protected static class L2ConfigEmpty { + } + } + } + + + @Component + @Lazy + static class L0ConfigConcrete extends L0ConfigBase { + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ParserStrategyUtilsTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ParserStrategyUtilsTests.java new file mode 100644 index 0000000..d5e5fc5 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ParserStrategyUtilsTests.java @@ -0,0 +1,264 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.io.InputStream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import org.springframework.beans.BeanInstantiationException; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ResourceLoader; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.reset; + +/** + * Tests for {@link ParserStrategyUtils}. + * + * @author Phillip Webb + */ +public class ParserStrategyUtilsTests { + + @Mock + private Environment environment; + + @Mock(extraInterfaces = BeanFactory.class) + private BeanDefinitionRegistry registry; + + @Mock + private ClassLoader beanClassLoader; + + @Mock + private ResourceLoader resourceLoader; + + @BeforeEach + void setup() { + MockitoAnnotations.initMocks(this); + given(this.resourceLoader.getClassLoader()).willReturn(this.beanClassLoader); + } + + @Test + public void instantiateClassWhenHasNoArgsConstructorCallsAware() { + NoArgsConstructor instance = instantiateClass(NoArgsConstructor.class); + assertThat(instance.setEnvironment).isSameAs(this.environment); + assertThat(instance.setBeanFactory).isSameAs(this.registry); + assertThat(instance.setBeanClassLoader).isSameAs(this.beanClassLoader); + assertThat(instance.setResourceLoader).isSameAs(this.resourceLoader); + } + + @Test + public void instantiateClassWhenHasSingleContructorInjectsParams() { + ArgsConstructor instance = instantiateClass(ArgsConstructor.class); + assertThat(instance.environment).isSameAs(this.environment); + assertThat(instance.beanFactory).isSameAs(this.registry); + assertThat(instance.beanClassLoader).isSameAs(this.beanClassLoader); + assertThat(instance.resourceLoader).isSameAs(this.resourceLoader); + } + + @Test + public void instantiateClassWhenHasSingleContructorAndAwareInjectsParamsAndCallsAware() { + ArgsConstructorAndAware instance = instantiateClass(ArgsConstructorAndAware.class); + assertThat(instance.environment).isSameAs(this.environment); + assertThat(instance.setEnvironment).isSameAs(this.environment); + assertThat(instance.beanFactory).isSameAs(this.registry); + assertThat(instance.setBeanFactory).isSameAs(this.registry); + assertThat(instance.beanClassLoader).isSameAs(this.beanClassLoader); + assertThat(instance.setBeanClassLoader).isSameAs(this.beanClassLoader); + assertThat(instance.resourceLoader).isSameAs(this.resourceLoader); + assertThat(instance.setResourceLoader).isSameAs(this.resourceLoader); + } + + @Test + public void instantiateClassWhenHasMultipleConstructorsUsesNoArgsConstructor() { + // Remain back-compatible by using the default constructor if there's more then one + MultipleConstructors instance = instantiateClass(MultipleConstructors.class); + assertThat(instance.usedDefaultConstructor).isTrue(); + } + + @Test + public void instantiateClassWhenHasMutlipleConstructorsAndNotDefaultThrowsException() { + assertThatExceptionOfType(BeanInstantiationException.class).isThrownBy(() -> + instantiateClass(MultipleConstructorsWithNoDefault.class)); + } + + @Test + public void instantiateClassWhenHasUnsupportedParameterThrowsException() { + assertThatExceptionOfType(BeanInstantiationException.class).isThrownBy(() -> + instantiateClass(InvalidConstructorParameterType.class)) + .withCauseInstanceOf(IllegalStateException.class) + .withMessageContaining("No suitable constructor found"); + } + + @Test + public void instantiateClassHasSubclassParameterThrowsException() { + // To keep the algorithm simple we don't support subtypes + assertThatExceptionOfType(BeanInstantiationException.class).isThrownBy(() -> + instantiateClass(InvalidConstructorParameterSubType.class)) + .withCauseInstanceOf(IllegalStateException.class) + .withMessageContaining("No suitable constructor found"); + } + + @Test + public void instantiateClassWhenHasNoBeanClassLoaderInjectsNull() { + reset(this.resourceLoader); + ArgsConstructor instance = instantiateClass(ArgsConstructor.class); + assertThat(instance.beanClassLoader).isNull(); + } + + @Test + public void instantiateClassWhenHasNoBeanClassLoaderDoesNotCallAware() { + reset(this.resourceLoader); + NoArgsConstructor instance = instantiateClass(NoArgsConstructor.class); + assertThat(instance.setBeanClassLoader).isNull(); + assertThat(instance.setBeanClassLoaderCalled).isFalse(); + } + + private T instantiateClass(Class clazz) { + return ParserStrategyUtils.instantiateClass(clazz, clazz, this.environment, + this.resourceLoader, this.registry); + } + + static class NoArgsConstructor implements BeanClassLoaderAware, + BeanFactoryAware, EnvironmentAware, ResourceLoaderAware { + + Environment setEnvironment; + + BeanFactory setBeanFactory; + + ClassLoader setBeanClassLoader; + + boolean setBeanClassLoaderCalled; + + ResourceLoader setResourceLoader; + + @Override + public void setEnvironment(Environment environment) { + this.setEnvironment = environment; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.setBeanFactory = beanFactory; + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.setBeanClassLoader = classLoader; + this.setBeanClassLoaderCalled = true; + } + + @Override + public void setResourceLoader(ResourceLoader resourceLoader) { + this.setResourceLoader = resourceLoader; + } + + } + + static class ArgsConstructor { + + final Environment environment; + + final BeanFactory beanFactory; + + final ClassLoader beanClassLoader; + + final ResourceLoader resourceLoader; + + ArgsConstructor(Environment environment, BeanFactory beanFactory, + ClassLoader beanClassLoader, ResourceLoader resourceLoader) { + this.environment = environment; + this.beanFactory = beanFactory; + this.beanClassLoader = beanClassLoader; + this.resourceLoader = resourceLoader; + } + + } + + static class ArgsConstructorAndAware extends NoArgsConstructor { + + final Environment environment; + + final BeanFactory beanFactory; + + final ClassLoader beanClassLoader; + + final ResourceLoader resourceLoader; + + ArgsConstructorAndAware(Environment environment, BeanFactory beanFactory, + ClassLoader beanClassLoader, ResourceLoader resourceLoader) { + this.environment = environment; + this.beanFactory = beanFactory; + this.beanClassLoader = beanClassLoader; + this.resourceLoader = resourceLoader; + } + + } + + static class MultipleConstructors { + + final boolean usedDefaultConstructor; + + MultipleConstructors() { + this.usedDefaultConstructor = true; + } + + MultipleConstructors(Environment environment) { + this.usedDefaultConstructor = false; + } + + } + + static class MultipleConstructorsWithNoDefault { + + MultipleConstructorsWithNoDefault(Environment environment, BeanFactory beanFactory) { + } + + MultipleConstructorsWithNoDefault(Environment environment) { + } + + } + + static class InvalidConstructorParameterType { + + InvalidConstructorParameterType(Environment environment, InputStream inputStream) { + } + + } + + static class InvalidConstructorParameterSubType { + + InvalidConstructorParameterSubType(ConfigurableEnvironment environment) { + } + + } + + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/PrimitiveBeanLookupAndAutowiringTests.java b/spring-context/src/test/java/org/springframework/context/annotation/PrimitiveBeanLookupAndAutowiringTests.java new file mode 100644 index 0000000..31a2d7c --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/PrimitiveBeanLookupAndAutowiringTests.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import javax.annotation.Resource; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Tests changes introduced for SPR-8874, allowing beans of primitive types to be looked + * up via getBean(Class), or to be injected using @Autowired or @Injected or @Resource. + * Prior to these changes, an attempt to lookup or inject a bean of type boolean would + * fail because all spring beans are Objects, regardless of initial type due to the way + * that ObjectFactory works. + * + * Now these attempts to lookup or inject primitive types work, thanks to simple changes + * in AbstractBeanFactory using ClassUtils#isAssignable methods instead of the built-in + * Class#isAssignableFrom. The former takes into account primitives and their object + * wrapper types, whereas the latter does not. + * + * @author Chris Beams + * @since 3.1 + */ +public class PrimitiveBeanLookupAndAutowiringTests { + + @Test + public void primitiveLookupByName() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class); + boolean b = ctx.getBean("b", boolean.class); + assertThat(b).isEqualTo(true); + int i = ctx.getBean("i", int.class); + assertThat(i).isEqualTo(42); + } + + @Test + public void primitiveLookupByType() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class); + boolean b = ctx.getBean(boolean.class); + assertThat(b).isEqualTo(true); + int i = ctx.getBean(int.class); + assertThat(i).isEqualTo(42); + } + + @Test + public void primitiveAutowiredInjection() { + ApplicationContext ctx = + new AnnotationConfigApplicationContext(Config.class, AutowiredComponent.class); + assertThat(ctx.getBean(AutowiredComponent.class).b).isEqualTo(true); + assertThat(ctx.getBean(AutowiredComponent.class).i).isEqualTo(42); + } + + @Test + public void primitiveResourceInjection() { + ApplicationContext ctx = + new AnnotationConfigApplicationContext(Config.class, ResourceComponent.class); + assertThat(ctx.getBean(ResourceComponent.class).b).isEqualTo(true); + assertThat(ctx.getBean(ResourceComponent.class).i).isEqualTo(42); + } + + + @Configuration + static class Config { + @Bean + public boolean b() { + return true; + } + + @Bean + public int i() { + return 42; + } + } + + + static class AutowiredComponent { + @Autowired boolean b; + @Autowired int i; + } + + + static class ResourceComponent { + @Resource boolean b; + @Autowired int i; + } +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java new file mode 100644 index 0000000..b127a37 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java @@ -0,0 +1,510 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Collections; +import java.util.Iterator; +import java.util.Properties; + +import javax.inject.Inject; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.annotation.AliasFor; +import org.springframework.core.env.Environment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.core.io.support.PropertySourceFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests the processing of @PropertySource annotations on @Configuration classes. + * + * @author Chris Beams + * @author Phillip Webb + * @since 3.1 + */ +public class PropertySourceAnnotationTests { + + + @Test + public void withExplicitName() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConfigWithExplicitName.class); + ctx.refresh(); + assertThat(ctx.getEnvironment().getPropertySources().contains("p1")).as("property source p1 was not added").isTrue(); + assertThat(ctx.getBean(TestBean.class).getName()).isEqualTo("p1TestBean"); + + // assert that the property source was added last to the set of sources + String name; + MutablePropertySources sources = ctx.getEnvironment().getPropertySources(); + Iterator> iterator = sources.iterator(); + do { + name = iterator.next().getName(); + } + while (iterator.hasNext()); + + assertThat(name).isEqualTo("p1"); + } + + @Test + public void withImplicitName() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConfigWithImplicitName.class); + ctx.refresh(); + assertThat(ctx.getEnvironment().getPropertySources().contains("class path resource [org/springframework/context/annotation/p1.properties]")).as("property source p1 was not added").isTrue(); + assertThat(ctx.getBean(TestBean.class).getName()).isEqualTo("p1TestBean"); + } + + @Test + public void withTestProfileBeans() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConfigWithTestProfileBeans.class); + ctx.refresh(); + assertThat(ctx.containsBean("testBean")).isTrue(); + assertThat(ctx.containsBean("testProfileBean")).isTrue(); + } + + /** + * Tests the LIFO behavior of @PropertySource annotaitons. + * The last one registered should 'win'. + */ + @Test + public void orderingIsLifo() { + { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConfigWithImplicitName.class, P2Config.class); + ctx.refresh(); + // p2 should 'win' as it was registered last + assertThat(ctx.getBean(TestBean.class).getName()).isEqualTo("p2TestBean"); + } + + { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(P2Config.class, ConfigWithImplicitName.class); + ctx.refresh(); + // p1 should 'win' as it was registered last + assertThat(ctx.getBean(TestBean.class).getName()).isEqualTo("p1TestBean"); + } + } + + @Test + public void withCustomFactory() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConfigWithImplicitName.class, WithCustomFactory.class); + ctx.refresh(); + assertThat(ctx.getBean(TestBean.class).getName()).isEqualTo("P2TESTBEAN"); + } + + @Test + public void withCustomFactoryAsMeta() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConfigWithImplicitName.class, WithCustomFactoryAsMeta.class); + ctx.refresh(); + assertThat(ctx.getBean(TestBean.class).getName()).isEqualTo("P2TESTBEAN"); + } + + @Test + public void withUnresolvablePlaceholder() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConfigWithUnresolvablePlaceholder.class); + try { + ctx.refresh(); + } + catch (BeanDefinitionStoreException ex) { + assertThat(ex.getCause() instanceof IllegalArgumentException).isTrue(); + } + } + + @Test + public void withUnresolvablePlaceholderAndDefault() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConfigWithUnresolvablePlaceholderAndDefault.class); + ctx.refresh(); + assertThat(ctx.getBean(TestBean.class).getName()).isEqualTo("p1TestBean"); + } + + @Test + public void withResolvablePlaceholder() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConfigWithResolvablePlaceholder.class); + System.setProperty("path.to.properties", "org/springframework/context/annotation"); + ctx.refresh(); + assertThat(ctx.getBean(TestBean.class).getName()).isEqualTo("p1TestBean"); + System.clearProperty("path.to.properties"); + } + + @Test + public void withResolvablePlaceholderAndFactoryBean() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConfigWithResolvablePlaceholderAndFactoryBean.class); + System.setProperty("path.to.properties", "org/springframework/context/annotation"); + ctx.refresh(); + assertThat(ctx.getBean(TestBean.class).getName()).isEqualTo("p1TestBean"); + System.clearProperty("path.to.properties"); + } + + @Test + public void withEmptyResourceLocations() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConfigWithEmptyResourceLocations.class); + try { + ctx.refresh(); + } + catch (BeanDefinitionStoreException ex) { + assertThat(ex.getCause() instanceof IllegalArgumentException).isTrue(); + } + } + + @Test + public void withNameAndMultipleResourceLocations() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithNameAndMultipleResourceLocations.class); + assertThat(ctx.getEnvironment().containsProperty("from.p1")).isTrue(); + assertThat(ctx.getEnvironment().containsProperty("from.p2")).isTrue(); + // p2 should 'win' as it was registered last + assertThat(ctx.getEnvironment().getProperty("testbean.name")).isEqualTo("p2TestBean"); + } + + @Test + public void withMultipleResourceLocations() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithMultipleResourceLocations.class); + assertThat(ctx.getEnvironment().containsProperty("from.p1")).isTrue(); + assertThat(ctx.getEnvironment().containsProperty("from.p2")).isTrue(); + // p2 should 'win' as it was registered last + assertThat(ctx.getEnvironment().getProperty("testbean.name")).isEqualTo("p2TestBean"); + } + + @Test + public void withPropertySources() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithPropertySources.class); + assertThat(ctx.getEnvironment().containsProperty("from.p1")).isTrue(); + assertThat(ctx.getEnvironment().containsProperty("from.p2")).isTrue(); + // p2 should 'win' as it was registered last + assertThat(ctx.getEnvironment().getProperty("testbean.name")).isEqualTo("p2TestBean"); + } + + @Test + public void withNamedPropertySources() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithNamedPropertySources.class); + assertThat(ctx.getEnvironment().containsProperty("from.p1")).isTrue(); + assertThat(ctx.getEnvironment().containsProperty("from.p2")).isTrue(); + // p2 should 'win' as it was registered last + assertThat(ctx.getEnvironment().getProperty("testbean.name")).isEqualTo("p2TestBean"); + } + + @Test + public void withMissingPropertySource() { + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> + new AnnotationConfigApplicationContext(ConfigWithMissingPropertySource.class)) + .withCauseInstanceOf(FileNotFoundException.class); + } + + @Test + public void withIgnoredPropertySource() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithIgnoredPropertySource.class); + assertThat(ctx.getEnvironment().containsProperty("from.p1")).isTrue(); + assertThat(ctx.getEnvironment().containsProperty("from.p2")).isTrue(); + } + + @Test + public void withSameSourceImportedInDifferentOrder() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithSameSourceImportedInDifferentOrder.class); + assertThat(ctx.getEnvironment().containsProperty("from.p1")).isTrue(); + assertThat(ctx.getEnvironment().containsProperty("from.p2")).isTrue(); + assertThat(ctx.getEnvironment().getProperty("testbean.name")).isEqualTo("p2TestBean"); + } + + @Test + public void orderingWithAndWithoutNameAndMultipleResourceLocations() { + // SPR-10820: p2 should 'win' as it was registered last + AnnotationConfigApplicationContext ctxWithName = new AnnotationConfigApplicationContext(ConfigWithNameAndMultipleResourceLocations.class); + AnnotationConfigApplicationContext ctxWithoutName = new AnnotationConfigApplicationContext(ConfigWithMultipleResourceLocations.class); + assertThat(ctxWithoutName.getEnvironment().getProperty("testbean.name")).isEqualTo("p2TestBean"); + assertThat(ctxWithName.getEnvironment().getProperty("testbean.name")).isEqualTo("p2TestBean"); + } + + @Test + public void orderingWithAndWithoutNameAndFourResourceLocations() { + // SPR-12198: p4 should 'win' as it was registered last + AnnotationConfigApplicationContext ctxWithoutName = new AnnotationConfigApplicationContext(ConfigWithFourResourceLocations.class); + assertThat(ctxWithoutName.getEnvironment().getProperty("testbean.name")).isEqualTo("p4TestBean"); + } + + @Test + public void orderingDoesntReplaceExisting() throws Exception { + // SPR-12198: mySource should 'win' as it was registered manually + AnnotationConfigApplicationContext ctxWithoutName = new AnnotationConfigApplicationContext(); + MapPropertySource mySource = new MapPropertySource("mine", Collections.singletonMap("testbean.name", "myTestBean")); + ctxWithoutName.getEnvironment().getPropertySources().addLast(mySource); + ctxWithoutName.register(ConfigWithFourResourceLocations.class); + ctxWithoutName.refresh(); + assertThat(ctxWithoutName.getEnvironment().getProperty("testbean.name")).isEqualTo("myTestBean"); + + } + + @Configuration + @PropertySource(value="classpath:${unresolvable}/p1.properties") + static class ConfigWithUnresolvablePlaceholder { + } + + + @Configuration + @PropertySource(value="classpath:${unresolvable:org/springframework/context/annotation}/p1.properties") + static class ConfigWithUnresolvablePlaceholderAndDefault { + + @Inject Environment env; + + @Bean + public TestBean testBean() { + return new TestBean(env.getProperty("testbean.name")); + } + } + + + @Configuration + @PropertySource(value="classpath:${path.to.properties}/p1.properties") + static class ConfigWithResolvablePlaceholder { + + @Inject Environment env; + + @Bean + public TestBean testBean() { + return new TestBean(env.getProperty("testbean.name")); + } + } + + + @Configuration + @PropertySource(value="classpath:${path.to.properties}/p1.properties") + static class ConfigWithResolvablePlaceholderAndFactoryBean { + + @Inject Environment env; + + @SuppressWarnings("rawtypes") + @Bean + public FactoryBean testBean() { + final String name = env.getProperty("testbean.name"); + return new FactoryBean() { + @Override + public Object getObject() { + return new TestBean(name); + } + @Override + public Class getObjectType() { + return TestBean.class; + } + @Override + public boolean isSingleton() { + return false; + } + }; + } + } + + + @Configuration + @PropertySource(name="p1", value="classpath:org/springframework/context/annotation/p1.properties") + static class ConfigWithExplicitName { + + @Inject Environment env; + + @Bean + public TestBean testBean() { + return new TestBean(env.getProperty("testbean.name")); + } + } + + + @Configuration + @PropertySource("classpath:org/springframework/context/annotation/p1.properties") + static class ConfigWithImplicitName { + + @Inject Environment env; + + @Bean + public TestBean testBean() { + return new TestBean(env.getProperty("testbean.name")); + } + } + + + @Configuration + @PropertySource(name="p1", value="classpath:org/springframework/context/annotation/p1.properties") + @ComponentScan("org.springframework.context.annotation.spr12111") + static class ConfigWithTestProfileBeans { + + @Inject Environment env; + + @Bean @Profile("test") + public TestBean testBean() { + return new TestBean(env.getProperty("testbean.name")); + } + } + + + @Configuration + @PropertySource("classpath:org/springframework/context/annotation/p2.properties") + static class P2Config { + } + + + @Configuration + @PropertySource(value = "classpath:org/springframework/context/annotation/p2.properties", factory = MyCustomFactory.class) + static class WithCustomFactory { + } + + + @Configuration + @MyPropertySource(value = "classpath:org/springframework/context/annotation/p2.properties") + static class WithCustomFactoryAsMeta { + } + + + @Retention(RetentionPolicy.RUNTIME) + @PropertySource(value = {}, factory = MyCustomFactory.class) + public @interface MyPropertySource { + + @AliasFor(annotation = PropertySource.class) + String value(); + } + + + public static class MyCustomFactory implements PropertySourceFactory { + + @Override + public org.springframework.core.env.PropertySource createPropertySource(String name, EncodedResource resource) throws IOException { + Properties props = PropertiesLoaderUtils.loadProperties(resource); + return new org.springframework.core.env.PropertySource("my" + name, props) { + @Override + public Object getProperty(String name) { + String value = props.getProperty(name); + return (value != null ? value.toUpperCase() : null); + } + }; + } + } + + + @Configuration + @PropertySource( + name = "psName", + value = { + "classpath:org/springframework/context/annotation/p1.properties", + "classpath:org/springframework/context/annotation/p2.properties" + }) + static class ConfigWithNameAndMultipleResourceLocations { + } + + + @Configuration + @PropertySource( + value = { + "classpath:org/springframework/context/annotation/p1.properties", + "classpath:org/springframework/context/annotation/p2.properties" + }) + static class ConfigWithMultipleResourceLocations { + } + + + @Configuration + @PropertySources({ + @PropertySource("classpath:org/springframework/context/annotation/p1.properties"), + @PropertySource("classpath:${base.package}/p2.properties"), + }) + static class ConfigWithPropertySources { + } + + + @Configuration + @PropertySources({ + @PropertySource(name = "psName", value = "classpath:org/springframework/context/annotation/p1.properties"), + @PropertySource(name = "psName", value = "classpath:org/springframework/context/annotation/p2.properties"), + }) + static class ConfigWithNamedPropertySources { + } + + + @Configuration + @PropertySources({ + @PropertySource(name = "psName", value = "classpath:org/springframework/context/annotation/p1.properties"), + @PropertySource(name = "psName", value = "classpath:org/springframework/context/annotation/missing.properties"), + @PropertySource(name = "psName", value = "classpath:org/springframework/context/annotation/p2.properties") + }) + static class ConfigWithMissingPropertySource { + } + + + @Configuration + @PropertySources({ + @PropertySource(name = "psName", value = "classpath:org/springframework/context/annotation/p1.properties"), + @PropertySource(name = "psName", value = "classpath:org/springframework/context/annotation/missing.properties", ignoreResourceNotFound=true), + @PropertySource(name = "psName", value = "classpath:${myPath}/missing.properties", ignoreResourceNotFound=true), + @PropertySource(name = "psName", value = "classpath:org/springframework/context/annotation/p2.properties") + }) + static class ConfigWithIgnoredPropertySource { + } + + + @Configuration + @PropertySource(value = {}) + static class ConfigWithEmptyResourceLocations { + } + + + @Import(ConfigImportedWithSameSourceImportedInDifferentOrder.class) + @PropertySources({ + @PropertySource("classpath:org/springframework/context/annotation/p1.properties"), + @PropertySource("classpath:org/springframework/context/annotation/p2.properties") + }) + @Configuration + public static class ConfigWithSameSourceImportedInDifferentOrder { + + } + + + @Configuration + @PropertySources({ + @PropertySource("classpath:org/springframework/context/annotation/p2.properties"), + @PropertySource("classpath:org/springframework/context/annotation/p1.properties") + }) + public static class ConfigImportedWithSameSourceImportedInDifferentOrder { + } + + + @Configuration + @PropertySource( + value = { + "classpath:org/springframework/context/annotation/p1.properties", + "classpath:org/springframework/context/annotation/p2.properties", + "classpath:org/springframework/context/annotation/p3.properties", + "classpath:org/springframework/context/annotation/p4.properties" + }) + static class ConfigWithFourResourceLocations { + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ReflectionUtilsIntegrationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ReflectionUtilsIntegrationTests.java new file mode 100644 index 0000000..5199f2b --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ReflectionUtilsIntegrationTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests ReflectionUtils methods as used against CGLIB-generated classes created + * by ConfigurationClassEnhancer. + * + * @author Chris Beams + * @since 3.1 + * @see org.springframework.util.ReflectionUtilsTests + */ +public class ReflectionUtilsIntegrationTests { + + @Test + public void getUniqueDeclaredMethods_withCovariantReturnType_andCglibRewrittenMethodNames() throws Exception { + Class cglibLeaf = new ConfigurationClassEnhancer().enhance(Leaf.class, null); + int m1MethodCount = 0; + Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(cglibLeaf); + for (Method method : methods) { + if (method.getName().equals("m1")) { + m1MethodCount++; + } + } + assertThat(m1MethodCount).isEqualTo(1); + for (Method method : methods) { + if (method.getName().contains("m1")) { + assertThat(Integer.class).isEqualTo(method.getReturnType()); + } + } + } + + + @Configuration + static abstract class Parent { + public abstract Number m1(); + } + + + @Configuration + static class Leaf extends Parent { + @Override + @Bean + public Integer m1() { + return 42; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/RoleAndDescriptionAnnotationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/RoleAndDescriptionAnnotationTests.java new file mode 100644 index 0000000..345b288 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/RoleAndDescriptionAnnotationTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.role.ComponentWithRole; +import org.springframework.context.annotation.role.ComponentWithoutRole; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Tests the use of the @Role and @Description annotation on @Bean methods and @Component classes. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + */ +public class RoleAndDescriptionAnnotationTests { + + @Test + public void onBeanMethod() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(Config.class); + ctx.refresh(); + assertThat(ctx.getBeanDefinition("foo").getRole()).isEqualTo(BeanDefinition.ROLE_APPLICATION); + assertThat(ctx.getBeanDefinition("foo").getDescription()).isNull(); + assertThat(ctx.getBeanDefinition("bar").getRole()).isEqualTo(BeanDefinition.ROLE_INFRASTRUCTURE); + assertThat(ctx.getBeanDefinition("bar").getDescription()).isEqualTo("A Bean method with a role"); + } + + @Test + public void onComponentClass() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ComponentWithoutRole.class, ComponentWithRole.class); + ctx.refresh(); + assertThat(ctx.getBeanDefinition("componentWithoutRole").getRole()).isEqualTo(BeanDefinition.ROLE_APPLICATION); + assertThat(ctx.getBeanDefinition("componentWithoutRole").getDescription()).isNull(); + assertThat(ctx.getBeanDefinition("componentWithRole").getRole()).isEqualTo(BeanDefinition.ROLE_INFRASTRUCTURE); + assertThat(ctx.getBeanDefinition("componentWithRole").getDescription()).isEqualTo("A Component with a role"); + } + + + @Test + public void viaComponentScanning() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.scan("org.springframework.context.annotation.role"); + ctx.refresh(); + assertThat(ctx.getBeanDefinition("componentWithoutRole").getRole()).isEqualTo(BeanDefinition.ROLE_APPLICATION); + assertThat(ctx.getBeanDefinition("componentWithoutRole").getDescription()).isNull(); + assertThat(ctx.getBeanDefinition("componentWithRole").getRole()).isEqualTo(BeanDefinition.ROLE_INFRASTRUCTURE); + assertThat(ctx.getBeanDefinition("componentWithRole").getDescription()).isEqualTo("A Component with a role"); + } + + + @Configuration + static class Config { + @Bean + public String foo() { + return "foo"; + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + @Description("A Bean method with a role") + public String bar() { + return "bar"; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/SimpleConfigTests.java b/spring-context/src/test/java/org/springframework/context/annotation/SimpleConfigTests.java new file mode 100644 index 0000000..9d7be1a --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/SimpleConfigTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; + +import example.scannable.FooService; +import example.scannable.ServiceInvocationCounter; +import org.junit.jupiter.api.Test; + +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Mark Fisher + * @author Juergen Hoeller + */ +public class SimpleConfigTests { + + @Test + public void testFooService() throws Exception { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(getConfigLocations(), getClass()); + + FooService fooService = ctx.getBean("fooServiceImpl", FooService.class); + ServiceInvocationCounter serviceInvocationCounter = ctx.getBean("serviceInvocationCounter", ServiceInvocationCounter.class); + + String value = fooService.foo(1); + assertThat(value).isEqualTo("bar"); + + Future future = fooService.asyncFoo(1); + boolean condition = future instanceof FutureTask; + assertThat(condition).isTrue(); + assertThat(future.get()).isEqualTo("bar"); + + assertThat(serviceInvocationCounter.getCount()).isEqualTo(2); + + fooService.foo(1); + assertThat(serviceInvocationCounter.getCount()).isEqualTo(3); + } + + public String[] getConfigLocations() { + return new String[] {"simpleConfigTests.xml"}; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/SimpleScanTests.java b/spring-context/src/test/java/org/springframework/context/annotation/SimpleScanTests.java new file mode 100644 index 0000000..ee4f474 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/SimpleScanTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import example.scannable.FooService; +import example.scannable.ServiceInvocationCounter; +import org.junit.jupiter.api.Test; + +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Mark Fisher + * @author Juergen Hoeller + * @author Chris Beams + */ +public class SimpleScanTests { + + protected String[] getConfigLocations() { + return new String[] {"simpleScanTests.xml"}; + } + + @Test + public void testFooService() throws Exception { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(getConfigLocations(), getClass()); + + FooService fooService = (FooService) ctx.getBean("fooServiceImpl"); + ServiceInvocationCounter serviceInvocationCounter = (ServiceInvocationCounter) ctx.getBean("serviceInvocationCounter"); + + assertThat(serviceInvocationCounter.getCount()).isEqualTo(0); + + assertThat(fooService.isInitCalled()).isTrue(); + assertThat(serviceInvocationCounter.getCount()).isEqualTo(1); + + String value = fooService.foo(1); + assertThat(value).isEqualTo("bar"); + assertThat(serviceInvocationCounter.getCount()).isEqualTo(2); + + fooService.foo(1); + assertThat(serviceInvocationCounter.getCount()).isEqualTo(3); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Spr11202Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/Spr11202Tests.java new file mode 100644 index 0000000..14fe41d --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/Spr11202Tests.java @@ -0,0 +1,146 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.util.Assert; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Dave Syer + */ +public class Spr11202Tests { + + @Test + public void testWithImporter() { + ApplicationContext context = new AnnotationConfigApplicationContext(Wrapper.class); + assertThat(context.getBean("value")).isEqualTo("foo"); + } + + @Test + public void testWithoutImporter() { + ApplicationContext context = new AnnotationConfigApplicationContext(Config.class); + assertThat(context.getBean("value")).isEqualTo("foo"); + } + + + @Configuration + @Import(Selector.class) + protected static class Wrapper { + } + + + protected static class Selector implements ImportSelector { + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + return new String[] {Config.class.getName()}; + } + } + + + @Configuration + protected static class Config { + + @Bean + public FooFactoryBean foo() { + return new FooFactoryBean(); + } + + @Bean + public String value() throws Exception { + String name = foo().getObject().getName(); + Assert.state(name != null, "Name cannot be null"); + return name; + } + + @Bean + @Conditional(NoBarCondition.class) + public String bar() throws Exception { + return "bar"; + } + } + + + protected static class NoBarCondition implements Condition { + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + if (context.getBeanFactory().getBeanNamesForAnnotation(Bar.class).length > 0) { + return false; + } + return true; + } + } + + + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Target(ElementType.TYPE) + protected @interface Bar { + } + + + protected static class FooFactoryBean implements FactoryBean, InitializingBean { + + private Foo foo = new Foo(); + + @Override + public Foo getObject() throws Exception { + return foo; + } + + @Override + public Class getObjectType() { + return Foo.class; + } + + @Override + public boolean isSingleton() { + return true; + } + + @Override + public void afterPropertiesSet() throws Exception { + this.foo.name = "foo"; + } + } + + + protected static class Foo { + + private String name; + + public String getName() { + return name; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Spr11310Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/Spr11310Tests.java new file mode 100644 index 0000000..551d8ab --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/Spr11310Tests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.core.annotation.Order; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Stephane Nicoll + */ +public class Spr11310Tests { + + @Test + public void orderedList() { + ApplicationContext context = new AnnotationConfigApplicationContext(Config.class); + StringHolder holder = context.getBean(StringHolder.class); + assertThat(holder.itemsList.get(0)).isEqualTo("second"); + assertThat(holder.itemsList.get(1)).isEqualTo("first"); + assertThat(holder.itemsList.get(2)).isEqualTo("unknownOrder"); + } + + @Test + public void orderedArray() { + ApplicationContext context = new AnnotationConfigApplicationContext(Config.class); + StringHolder holder = context.getBean(StringHolder.class); + assertThat(holder.itemsArray[0]).isEqualTo("second"); + assertThat(holder.itemsArray[1]).isEqualTo("first"); + assertThat(holder.itemsArray[2]).isEqualTo("unknownOrder"); + } + + + @Configuration + static class Config { + + @Bean + @Order(50) + public String first() { + return "first"; + } + + @Bean + public String unknownOrder() { + return "unknownOrder"; + } + + @Bean + @Order(5) + public String second() { + return "second"; + } + + @Bean + public StringHolder stringHolder() { + return new StringHolder(); + } + + } + + + private static class StringHolder { + @Autowired + private List itemsList; + + @Autowired + private String[] itemsArray; + + } +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Spr12278Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/Spr12278Tests.java new file mode 100644 index 0000000..afceea8 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/Spr12278Tests.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanCreationException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Stephane Nicoll + */ +public class Spr12278Tests { + + private AnnotationConfigApplicationContext context; + + @AfterEach + public void close() { + if (context != null) { + context.close(); + } + } + + @Test + public void componentSingleConstructor() { + this.context = new AnnotationConfigApplicationContext(BaseConfiguration.class, + SingleConstructorComponent.class); + assertThat(this.context.getBean(SingleConstructorComponent.class).autowiredName).isEqualTo("foo"); + } + + @Test + public void componentTwoConstructorsNoHint() { + this.context = new AnnotationConfigApplicationContext(BaseConfiguration.class, + TwoConstructorsComponent.class); + assertThat(this.context.getBean(TwoConstructorsComponent.class).name).isEqualTo("fallback"); + } + + @Test + public void componentTwoSpecificConstructorsNoHint() { + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + new AnnotationConfigApplicationContext(BaseConfiguration.class, TwoSpecificConstructorsComponent.class)) + .withMessageContaining(NoSuchMethodException.class.getName()); + } + + + @Configuration + static class BaseConfiguration { + + @Bean + public String autowiredName() { + return "foo"; + } + } + + private static class SingleConstructorComponent { + + private final String autowiredName; + + // No @Autowired - implicit wiring + public SingleConstructorComponent(String autowiredName) { + this.autowiredName = autowiredName; + } + + } + + private static class TwoConstructorsComponent { + + private final String name; + + public TwoConstructorsComponent(String name) { + this.name = name; + } + + public TwoConstructorsComponent() { + this("fallback"); + } + } + + private static class TwoSpecificConstructorsComponent { + + private final Integer counter; + + public TwoSpecificConstructorsComponent(Integer counter) { + this.counter = counter; + } + + public TwoSpecificConstructorsComponent(String name) { + this(Integer.valueOf(name)); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Spr12636Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/Spr12636Tests.java new file mode 100644 index 0000000..241319a --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/Spr12636Tests.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.annotation.Order; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.stereotype.Component; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Stephane Nicoll + */ +public class Spr12636Tests { + + private ConfigurableApplicationContext context; + + @AfterEach + public void closeContext() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void orderOnImplementation() { + this.context = new AnnotationConfigApplicationContext( + UserServiceTwo.class, UserServiceOne.class, UserServiceCollector.class); + UserServiceCollector bean = this.context.getBean(UserServiceCollector.class); + assertThat(bean.userServices.get(0)).isSameAs(context.getBean("serviceOne", UserService.class)); + assertThat(bean.userServices.get(1)).isSameAs(context.getBean("serviceTwo", UserService.class)); + + } + + @Test + public void orderOnImplementationWithProxy() { + this.context = new AnnotationConfigApplicationContext( + UserServiceTwo.class, UserServiceOne.class, UserServiceCollector.class, AsyncConfig.class); + + // Validate those beans are indeed wrapped by a proxy + UserService serviceOne = this.context.getBean("serviceOne", UserService.class); + UserService serviceTwo = this.context.getBean("serviceTwo", UserService.class); + assertThat(AopUtils.isAopProxy(serviceOne)).isTrue(); + assertThat(AopUtils.isAopProxy(serviceTwo)).isTrue(); + + UserServiceCollector bean = this.context.getBean(UserServiceCollector.class); + assertThat(bean.userServices.get(0)).isSameAs(serviceOne); + assertThat(bean.userServices.get(1)).isSameAs(serviceTwo); + } + + @Configuration + @EnableAsync + static class AsyncConfig { + } + + + @Component + static class UserServiceCollector { + + public final List userServices; + + @Autowired + UserServiceCollector(List userServices) { + this.userServices = userServices; + } + } + + interface UserService { + + void doIt(); + } + + @Component("serviceOne") + @Order(1) + static class UserServiceOne implements UserService { + + @Async + @Override + public void doIt() { + + } + } + + @Component("serviceTwo") + @Order(2) + static class UserServiceTwo implements UserService { + + @Async + @Override + public void doIt() { + + } + } +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Spr15042Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/Spr15042Tests.java new file mode 100644 index 0000000..2048b92 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/Spr15042Tests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.ProxyFactoryBean; +import org.springframework.aop.target.CommonsPool2TargetSource; + +/** + * @author Juergen Hoeller + */ +public class Spr15042Tests { + + @Test + public void poolingTargetSource() { + new AnnotationConfigApplicationContext(PoolingTargetSourceConfig.class); + } + + + @Configuration + static class PoolingTargetSourceConfig { + + @Bean + @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) + public ProxyFactoryBean myObject() { + ProxyFactoryBean pfb = new ProxyFactoryBean(); + pfb.setTargetSource(poolTargetSource()); + return pfb; + } + + @Bean + public CommonsPool2TargetSource poolTargetSource() { + CommonsPool2TargetSource pool = new CommonsPool2TargetSource(); + pool.setMaxSize(3); + pool.setTargetBeanName("myObjectTarget"); + return pool; + } + + @Bean(name = "myObjectTarget") + @Scope(scopeName = "prototype") + public Object myObjectTarget() { + return new Object(); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Spr15275Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/Spr15275Tests.java new file mode 100644 index 0000000..ac8222f --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/Spr15275Tests.java @@ -0,0 +1,250 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.AbstractFactoryBean; +import org.springframework.context.ApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + */ +public class Spr15275Tests { + + @Test + public void testWithFactoryBean() { + ApplicationContext context = new AnnotationConfigApplicationContext(ConfigWithFactoryBean.class); + assertThat(context.getBean(Bar.class).foo.toString()).isEqualTo("x"); + assertThat(context.getBean(Bar.class).foo).isSameAs(context.getBean(FooInterface.class)); + } + + @Test + public void testWithAbstractFactoryBean() { + ApplicationContext context = new AnnotationConfigApplicationContext(ConfigWithAbstractFactoryBean.class); + assertThat(context.getBean(Bar.class).foo.toString()).isEqualTo("x"); + assertThat(context.getBean(Bar.class).foo).isSameAs(context.getBean(FooInterface.class)); + } + + @Test + public void testWithAbstractFactoryBeanForInterface() { + ApplicationContext context = new AnnotationConfigApplicationContext(ConfigWithAbstractFactoryBeanForInterface.class); + assertThat(context.getBean(Bar.class).foo.toString()).isEqualTo("x"); + assertThat(context.getBean(Bar.class).foo).isSameAs(context.getBean(FooInterface.class)); + } + + @Test + public void testWithAbstractFactoryBeanAsReturnType() { + ApplicationContext context = new AnnotationConfigApplicationContext(ConfigWithAbstractFactoryBeanAsReturnType.class); + assertThat(context.getBean(Bar.class).foo.toString()).isEqualTo("x"); + assertThat(context.getBean(Bar.class).foo).isSameAs(context.getBean(FooInterface.class)); + } + + @Test + public void testWithFinalFactoryBean() { + ApplicationContext context = new AnnotationConfigApplicationContext(ConfigWithFinalFactoryBean.class); + assertThat(context.getBean(Bar.class).foo.toString()).isEqualTo("x"); + assertThat(context.getBean(Bar.class).foo).isSameAs(context.getBean(FooInterface.class)); + } + + @Test + public void testWithFinalFactoryBeanAsReturnType() { + ApplicationContext context = new AnnotationConfigApplicationContext(ConfigWithFinalFactoryBeanAsReturnType.class); + assertThat(context.getBean(Bar.class).foo.toString()).isEqualTo("x"); + // not same due to fallback to raw FinalFactoryBean instance with repeated getObject() invocations + assertThat(context.getBean(Bar.class).foo).isNotSameAs(context.getBean(FooInterface.class)); + } + + + @Configuration + protected static class ConfigWithFactoryBean { + + @Bean + public FactoryBean foo() { + return new FactoryBean() { + @Override + public Foo getObject() { + return new Foo("x"); + } + @Override + public Class getObjectType() { + return Foo.class; + } + }; + } + + @Bean + public Bar bar() throws Exception { + assertThat(foo().isSingleton()).isTrue(); + return new Bar(foo().getObject()); + } + } + + + @Configuration + protected static class ConfigWithAbstractFactoryBean { + + @Bean + public FactoryBean foo() { + return new AbstractFactoryBean() { + @Override + public Foo createInstance() { + return new Foo("x"); + } + @Override + public Class getObjectType() { + return Foo.class; + } + }; + } + + @Bean + public Bar bar() throws Exception { + assertThat(foo().isSingleton()).isTrue(); + return new Bar(foo().getObject()); + } + } + + + @Configuration + protected static class ConfigWithAbstractFactoryBeanForInterface { + + @Bean + public FactoryBean foo() { + return new AbstractFactoryBean() { + @Override + public FooInterface createInstance() { + return new Foo("x"); + } + @Override + public Class getObjectType() { + return FooInterface.class; + } + }; + } + + @Bean + public Bar bar() throws Exception { + assertThat(foo().isSingleton()).isTrue(); + return new Bar(foo().getObject()); + } + } + + + @Configuration + protected static class ConfigWithAbstractFactoryBeanAsReturnType { + + @Bean + public AbstractFactoryBean foo() { + return new AbstractFactoryBean() { + @Override + public FooInterface createInstance() { + return new Foo("x"); + } + @Override + public Class getObjectType() { + return Foo.class; + } + }; + } + + @Bean + public Bar bar() throws Exception { + assertThat(foo().isSingleton()).isTrue(); + return new Bar(foo().getObject()); + } + } + + + @Configuration + protected static class ConfigWithFinalFactoryBean { + + @Bean + public FactoryBean foo() { + return new FinalFactoryBean(); + } + + @Bean + public Bar bar() throws Exception { + assertThat(foo().isSingleton()).isTrue(); + return new Bar(foo().getObject()); + } + } + + + @Configuration + protected static class ConfigWithFinalFactoryBeanAsReturnType { + + @Bean + public FinalFactoryBean foo() { + return new FinalFactoryBean(); + } + + @Bean + public Bar bar() throws Exception { + assertThat(foo().isSingleton()).isTrue(); + return new Bar(foo().getObject()); + } + } + + + private static final class FinalFactoryBean implements FactoryBean { + + @Override + public Foo getObject() { + return new Foo("x"); + } + + @Override + public Class getObjectType() { + return FooInterface.class; + } + } + + + protected interface FooInterface { + } + + + protected static class Foo implements FooInterface { + + private final String value; + + public Foo(String value) { + this.value = value; + } + + @Override + public String toString() { + return this.value; + } + } + + + protected static class Bar { + + public final FooInterface foo; + + public Bar(FooInterface foo) { + this.foo = foo; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Spr16179Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/Spr16179Tests.java new file mode 100644 index 0000000..afeae7e --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/Spr16179Tests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @author Oliver Gierke + */ +public class Spr16179Tests { + + @Test + public void repro() { + AnnotationConfigApplicationContext bf = + new AnnotationConfigApplicationContext(AssemblerConfig.class, AssemblerInjection.class); + + assertThat(bf.getBean(AssemblerInjection.class).assembler0).isSameAs(bf.getBean("someAssembler")); + // assertNull(bf.getBean(AssemblerInjection.class).assembler1); TODO: accidental match + // assertNull(bf.getBean(AssemblerInjection.class).assembler2); + assertThat(bf.getBean(AssemblerInjection.class).assembler3).isSameAs(bf.getBean("pageAssembler")); + assertThat(bf.getBean(AssemblerInjection.class).assembler4).isSameAs(bf.getBean("pageAssembler")); + assertThat(bf.getBean(AssemblerInjection.class).assembler5).isSameAs(bf.getBean("pageAssembler")); + assertThat(bf.getBean(AssemblerInjection.class).assembler6).isSameAs(bf.getBean("pageAssembler")); + } + + + @Configuration + static class AssemblerConfig { + + @Bean + PageAssemblerImpl pageAssembler() { + return new PageAssemblerImpl<>(); + } + + @Bean + Assembler someAssembler() { + return new Assembler() {}; + } + } + + + public static class AssemblerInjection { + + @Autowired(required = false) + Assembler assembler0; + + @Autowired(required = false) + Assembler assembler1; + + @Autowired(required = false) + Assembler> assembler2; + + @Autowired(required = false) + Assembler assembler3; + + @Autowired(required = false) + Assembler> assembler4; + + @Autowired(required = false) + PageAssembler assembler5; + + @Autowired(required = false) + PageAssembler assembler6; + } + + + interface Assembler {} + + interface PageAssembler extends Assembler> {} + + static class PageAssemblerImpl implements PageAssembler {} + + interface Page {} + + interface SomeType {} + + interface SomeOtherType {} + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Spr16217Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/Spr16217Tests.java new file mode 100644 index 0000000..056f8fa --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/Spr16217Tests.java @@ -0,0 +1,131 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.core.type.AnnotatedTypeMetadata; + +/** + * @author Andy Wilkinson + * @author Juergen Hoeller + */ +public class Spr16217Tests { + + @Test + @Disabled("TODO") + public void baseConfigurationIsIncludedWhenFirstSuperclassReferenceIsSkippedInRegisterBeanPhase() { + try (AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(RegisterBeanPhaseImportingConfiguration.class)) { + context.getBean("someBean"); + } + } + + @Test + public void baseConfigurationIsIncludedWhenFirstSuperclassReferenceIsSkippedInParseConfigurationPhase() { + try (AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(ParseConfigurationPhaseImportingConfiguration.class)) { + context.getBean("someBean"); + } + } + + @Test + public void baseConfigurationIsIncludedOnceWhenBothConfigurationClassesAreActive() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.setAllowBeanDefinitionOverriding(false); + context.register(UnconditionalImportingConfiguration.class); + context.refresh(); + try { + context.getBean("someBean"); + } + finally { + context.close(); + } + } + + + public static class RegisterBeanPhaseCondition implements ConfigurationCondition { + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + return false; + } + + @Override + public ConfigurationPhase getConfigurationPhase() { + return ConfigurationPhase.REGISTER_BEAN; + } + } + + + public static class ParseConfigurationPhaseCondition implements ConfigurationCondition { + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + return false; + } + + @Override + public ConfigurationPhase getConfigurationPhase() { + return ConfigurationPhase.PARSE_CONFIGURATION; + } + } + + + @Import({RegisterBeanPhaseConditionConfiguration.class, BarConfiguration.class}) + public static class RegisterBeanPhaseImportingConfiguration { + } + + + @Import({ParseConfigurationPhaseConditionConfiguration.class, BarConfiguration.class}) + public static class ParseConfigurationPhaseImportingConfiguration { + } + + + @Import({UnconditionalConfiguration.class, BarConfiguration.class}) + public static class UnconditionalImportingConfiguration { + } + + + public static class BaseConfiguration { + + @Bean + public String someBean() { + return "foo"; + } + } + + + @Conditional(RegisterBeanPhaseCondition.class) + public static class RegisterBeanPhaseConditionConfiguration extends BaseConfiguration { + } + + + @Conditional(ParseConfigurationPhaseCondition.class) + public static class ParseConfigurationPhaseConditionConfiguration extends BaseConfiguration { + } + + + public static class UnconditionalConfiguration extends BaseConfiguration { + } + + + public static class BarConfiguration extends BaseConfiguration { + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Spr3775InitDestroyLifecycleTests.java b/spring-context/src/test/java/org/springframework/context/annotation/Spr3775InitDestroyLifecycleTests.java new file mode 100644 index 0000000..0fb2495 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/Spr3775InitDestroyLifecycleTests.java @@ -0,0 +1,272 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.util.ObjectUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + *

    + * JUnit-3.8-based unit test which verifies expected init and + * destroy bean lifecycle behavior as requested in SPR-3775. + *

    + *

    + * Specifically, combinations of the following are tested: + *

    + *
      + *
    • {@link InitializingBean} & {@link DisposableBean} interfaces
    • + *
    • Custom {@link RootBeanDefinition#getInitMethodName() init} & + * {@link RootBeanDefinition#getDestroyMethodName() destroy} methods
    • + *
    • JSR 250's {@link javax.annotation.PostConstruct @PostConstruct} & + * {@link javax.annotation.PreDestroy @PreDestroy} annotations
    • + *
    + * + * @author Sam Brannen + * @since 2.5 + */ +public class Spr3775InitDestroyLifecycleTests { + + private static final Log logger = LogFactory.getLog(Spr3775InitDestroyLifecycleTests.class); + + /** LIFECYCLE_TEST_BEAN. */ + private static final String LIFECYCLE_TEST_BEAN = "lifecycleTestBean"; + + + private void debugMethods(Class clazz, String category, List methodNames) { + if (logger.isDebugEnabled()) { + logger.debug(clazz.getSimpleName() + ": " + category + ": " + methodNames); + } + } + + private void assertMethodOrdering(Class clazz, String category, List expectedMethods, + List actualMethods) { + debugMethods(clazz, category, actualMethods); + assertThat(ObjectUtils.nullSafeEquals(expectedMethods, actualMethods)).as("Verifying " + category + ": expected<" + expectedMethods + "> but got<" + actualMethods + ">.").isTrue(); + } + + private DefaultListableBeanFactory createBeanFactoryAndRegisterBean(final Class beanClass, + final String initMethodName, final String destroyMethodName) { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + RootBeanDefinition beanDefinition = new RootBeanDefinition(beanClass); + beanDefinition.setInitMethodName(initMethodName); + beanDefinition.setDestroyMethodName(destroyMethodName); + beanFactory.addBeanPostProcessor(new CommonAnnotationBeanPostProcessor()); + beanFactory.registerBeanDefinition(LIFECYCLE_TEST_BEAN, beanDefinition); + return beanFactory; + } + + @Test + public void testInitDestroyMethods() { + final Class beanClass = InitDestroyBean.class; + final DefaultListableBeanFactory beanFactory = createBeanFactoryAndRegisterBean(beanClass, + "afterPropertiesSet", "destroy"); + final InitDestroyBean bean = (InitDestroyBean) beanFactory.getBean(LIFECYCLE_TEST_BEAN); + assertMethodOrdering(beanClass, "init-methods", Arrays.asList("afterPropertiesSet"), bean.initMethods); + beanFactory.destroySingletons(); + assertMethodOrdering(beanClass, "destroy-methods", Arrays.asList("destroy"), bean.destroyMethods); + } + + @Test + public void testInitializingDisposableInterfaces() { + final Class beanClass = CustomInitializingDisposableBean.class; + final DefaultListableBeanFactory beanFactory = createBeanFactoryAndRegisterBean(beanClass, "customInit", + "customDestroy"); + final CustomInitializingDisposableBean bean = (CustomInitializingDisposableBean) beanFactory.getBean(LIFECYCLE_TEST_BEAN); + assertMethodOrdering(beanClass, "init-methods", Arrays.asList("afterPropertiesSet", "customInit"), + bean.initMethods); + beanFactory.destroySingletons(); + assertMethodOrdering(beanClass, "destroy-methods", Arrays.asList("destroy", "customDestroy"), + bean.destroyMethods); + } + + @Test + public void testInitializingDisposableInterfacesWithShadowedMethods() { + final Class beanClass = InitializingDisposableWithShadowedMethodsBean.class; + final DefaultListableBeanFactory beanFactory = createBeanFactoryAndRegisterBean(beanClass, + "afterPropertiesSet", "destroy"); + final InitializingDisposableWithShadowedMethodsBean bean = (InitializingDisposableWithShadowedMethodsBean) beanFactory.getBean(LIFECYCLE_TEST_BEAN); + assertMethodOrdering(beanClass, "init-methods", Arrays.asList("InitializingBean.afterPropertiesSet"), + bean.initMethods); + beanFactory.destroySingletons(); + assertMethodOrdering(beanClass, "destroy-methods", Arrays.asList("DisposableBean.destroy"), bean.destroyMethods); + } + + @Test + public void testJsr250Annotations() { + final Class beanClass = CustomAnnotatedInitDestroyBean.class; + final DefaultListableBeanFactory beanFactory = createBeanFactoryAndRegisterBean(beanClass, "customInit", + "customDestroy"); + final CustomAnnotatedInitDestroyBean bean = (CustomAnnotatedInitDestroyBean) beanFactory.getBean(LIFECYCLE_TEST_BEAN); + assertMethodOrdering(beanClass, "init-methods", Arrays.asList("postConstruct", "afterPropertiesSet", + "customInit"), bean.initMethods); + beanFactory.destroySingletons(); + assertMethodOrdering(beanClass, "destroy-methods", Arrays.asList("preDestroy", "destroy", "customDestroy"), + bean.destroyMethods); + } + + @Test + public void testJsr250AnnotationsWithShadowedMethods() { + final Class beanClass = CustomAnnotatedInitDestroyWithShadowedMethodsBean.class; + final DefaultListableBeanFactory beanFactory = createBeanFactoryAndRegisterBean(beanClass, "customInit", + "customDestroy"); + final CustomAnnotatedInitDestroyWithShadowedMethodsBean bean = (CustomAnnotatedInitDestroyWithShadowedMethodsBean) beanFactory.getBean(LIFECYCLE_TEST_BEAN); + assertMethodOrdering(beanClass, "init-methods", + Arrays.asList("@PostConstruct.afterPropertiesSet", "customInit"), bean.initMethods); + beanFactory.destroySingletons(); + assertMethodOrdering(beanClass, "destroy-methods", Arrays.asList("@PreDestroy.destroy", "customDestroy"), + bean.destroyMethods); + } + + @Test + public void testAllLifecycleMechanismsAtOnce() { + final Class beanClass = AllInOneBean.class; + final DefaultListableBeanFactory beanFactory = createBeanFactoryAndRegisterBean(beanClass, + "afterPropertiesSet", "destroy"); + final AllInOneBean bean = (AllInOneBean) beanFactory.getBean(LIFECYCLE_TEST_BEAN); + assertMethodOrdering(beanClass, "init-methods", Arrays.asList("afterPropertiesSet"), bean.initMethods); + beanFactory.destroySingletons(); + assertMethodOrdering(beanClass, "destroy-methods", Arrays.asList("destroy"), bean.destroyMethods); + } + + + public static class InitDestroyBean { + + final List initMethods = new ArrayList<>(); + final List destroyMethods = new ArrayList<>(); + + + public void afterPropertiesSet() throws Exception { + this.initMethods.add("afterPropertiesSet"); + } + + public void destroy() throws Exception { + this.destroyMethods.add("destroy"); + } + } + + public static class InitializingDisposableWithShadowedMethodsBean extends InitDestroyBean implements + InitializingBean, DisposableBean { + + @Override + public void afterPropertiesSet() throws Exception { + this.initMethods.add("InitializingBean.afterPropertiesSet"); + } + + @Override + public void destroy() throws Exception { + this.destroyMethods.add("DisposableBean.destroy"); + } + } + + + public static class CustomInitDestroyBean { + + final List initMethods = new ArrayList<>(); + final List destroyMethods = new ArrayList<>(); + + public void customInit() throws Exception { + this.initMethods.add("customInit"); + } + + public void customDestroy() throws Exception { + this.destroyMethods.add("customDestroy"); + } + } + + + public static class CustomInitializingDisposableBean extends CustomInitDestroyBean + implements InitializingBean, DisposableBean { + + @Override + public void afterPropertiesSet() throws Exception { + this.initMethods.add("afterPropertiesSet"); + } + + @Override + public void destroy() throws Exception { + this.destroyMethods.add("destroy"); + } + } + + + public static class CustomAnnotatedInitDestroyBean extends CustomInitializingDisposableBean { + + @PostConstruct + public void postConstruct() throws Exception { + this.initMethods.add("postConstruct"); + } + + @PreDestroy + public void preDestroy() throws Exception { + this.destroyMethods.add("preDestroy"); + } + } + + + public static class CustomAnnotatedInitDestroyWithShadowedMethodsBean extends CustomInitializingDisposableBean { + + @PostConstruct + @Override + public void afterPropertiesSet() throws Exception { + this.initMethods.add("@PostConstruct.afterPropertiesSet"); + } + + @PreDestroy + @Override + public void destroy() throws Exception { + this.destroyMethods.add("@PreDestroy.destroy"); + } + } + + + public static class AllInOneBean implements InitializingBean, DisposableBean { + + final List initMethods = new ArrayList<>(); + final List destroyMethods = new ArrayList<>(); + + @Override + @PostConstruct + public void afterPropertiesSet() throws Exception { + this.initMethods.add("afterPropertiesSet"); + } + + @Override + @PreDestroy + public void destroy() throws Exception { + this.destroyMethods.add("destroy"); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Spr6602Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/Spr6602Tests.java new file mode 100644 index 0000000..faf5579 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/Spr6602Tests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Tests to verify that FactoryBean semantics are the same in Configuration + * classes as in XML. + * + * @author Chris Beams + */ +public class Spr6602Tests { + + @Test + public void testXmlBehavior() throws Exception { + doAssertions(new ClassPathXmlApplicationContext("Spr6602Tests-context.xml", Spr6602Tests.class)); + } + + @Test + public void testConfigurationClassBehavior() throws Exception { + doAssertions(new AnnotationConfigApplicationContext(FooConfig.class)); + } + + private void doAssertions(ApplicationContext ctx) throws Exception { + Foo foo = ctx.getBean(Foo.class); + + Bar bar1 = ctx.getBean(Bar.class); + Bar bar2 = ctx.getBean(Bar.class); + assertThat(bar1).isEqualTo(bar2); + assertThat(bar1).isEqualTo(foo.bar); + + BarFactory barFactory1 = ctx.getBean(BarFactory.class); + BarFactory barFactory2 = ctx.getBean(BarFactory.class); + assertThat(barFactory1).isEqualTo(barFactory2); + + Bar bar3 = barFactory1.getObject(); + Bar bar4 = barFactory1.getObject(); + assertThat(bar3).isNotEqualTo(bar4); + } + + + @Configuration + public static class FooConfig { + + @Bean + public Foo foo() throws Exception { + return new Foo(barFactory().getObject()); + } + + @Bean + public BarFactory barFactory() { + return new BarFactory(); + } + } + + + public static class Foo { + + final Bar bar; + + public Foo(Bar bar) { + this.bar = bar; + } + } + + + public static class Bar { + } + + + public static class BarFactory implements FactoryBean { + + @Override + public Bar getObject() throws Exception { + return new Bar(); + } + + @Override + public Class getObjectType() { + return Bar.class; + } + + @Override + public boolean isSingleton() { + return true; + } + + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/Spr8954Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/Spr8954Tests.java new file mode 100644 index 0000000..9a2cb00 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/Spr8954Tests.java @@ -0,0 +1,140 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.PropertyValues; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor; +import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor; +import org.springframework.beans.factory.support.AbstractBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for SPR-8954, in which a custom {@link InstantiationAwareBeanPostProcessor} + * forces the predicted type of a FactoryBean, effectively preventing retrieval of the + * bean from calls to #getBeansOfType(FactoryBean.class). The implementation of + * {@link AbstractBeanFactory#isFactoryBean(String, RootBeanDefinition)} now ensures + * that not only the predicted bean type is considered, but also the original bean + * definition's beanClass. + * + * @author Chris Beams + * @author Oliver Gierke + */ +@SuppressWarnings("resource") +public class Spr8954Tests { + + @Test + public void repro() { + AnnotationConfigApplicationContext bf = new AnnotationConfigApplicationContext(); + bf.registerBeanDefinition("fooConfig", new RootBeanDefinition(FooConfig.class)); + bf.getBeanFactory().addBeanPostProcessor(new PredictingBPP()); + bf.refresh(); + + assertThat(bf.getBean("foo")).isInstanceOf(Foo.class); + assertThat(bf.getBean("&foo")).isInstanceOf(FooFactoryBean.class); + + assertThat(bf.isTypeMatch("&foo", FactoryBean.class)).isTrue(); + + @SuppressWarnings("rawtypes") + Map fbBeans = bf.getBeansOfType(FactoryBean.class); + assertThat(1).isEqualTo(fbBeans.size()); + assertThat("&foo").isEqualTo(fbBeans.keySet().iterator().next()); + + Map aiBeans = bf.getBeansOfType(AnInterface.class); + assertThat(1).isEqualTo(aiBeans.size()); + assertThat("&foo").isEqualTo(aiBeans.keySet().iterator().next()); + } + + @Test + public void findsBeansByTypeIfNotInstantiated() { + AnnotationConfigApplicationContext bf = new AnnotationConfigApplicationContext(); + bf.registerBeanDefinition("fooConfig", new RootBeanDefinition(FooConfig.class)); + bf.getBeanFactory().addBeanPostProcessor(new PredictingBPP()); + bf.refresh(); + + assertThat(bf.isTypeMatch("&foo", FactoryBean.class)).isTrue(); + + @SuppressWarnings("rawtypes") + Map fbBeans = bf.getBeansOfType(FactoryBean.class); + assertThat(1).isEqualTo(fbBeans.size()); + assertThat("&foo").isEqualTo(fbBeans.keySet().iterator().next()); + + Map aiBeans = bf.getBeansOfType(AnInterface.class); + assertThat(1).isEqualTo(aiBeans.size()); + assertThat("&foo").isEqualTo(aiBeans.keySet().iterator().next()); + } + + + static class FooConfig { + + @Bean FooFactoryBean foo() { + return new FooFactoryBean(); + } + } + + + static class FooFactoryBean implements FactoryBean, AnInterface { + + @Override + public Foo getObject() { + return new Foo(); + } + + @Override + public Class getObjectType() { + return Foo.class; + } + + @Override + public boolean isSingleton() { + return true; + } + } + + + interface AnInterface { + } + + + static class Foo { + } + + + interface PredictedType { + } + + + static class PredictingBPP implements SmartInstantiationAwareBeanPostProcessor { + + @Override + public Class predictBeanType(Class beanClass, String beanName) { + return FactoryBean.class.isAssignableFrom(beanClass) ? PredictedType.class : null; + } + + @Override + public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) { + return pvs; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/TestBeanNameGenerator.java b/spring-context/src/test/java/org/springframework/context/annotation/TestBeanNameGenerator.java new file mode 100644 index 0000000..7a42ce6 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/TestBeanNameGenerator.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; + +/** + * @author Mark Fisher + */ +public class TestBeanNameGenerator extends AnnotationBeanNameGenerator { + + @Override + public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) { + String beanName = super.generateBeanName(definition, registry); + return "testing." + beanName; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/TestScopeMetadataResolver.java b/spring-context/src/test/java/org/springframework/context/annotation/TestScopeMetadataResolver.java new file mode 100644 index 0000000..8a8bec8 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/TestScopeMetadataResolver.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation; + +import org.springframework.beans.factory.config.BeanDefinition; + +/** + * @author Mark Fisher + */ +public class TestScopeMetadataResolver implements ScopeMetadataResolver { + + @Override + public ScopeMetadata resolveScopeMetadata(BeanDefinition beanDefinition) { + ScopeMetadata metadata = new ScopeMetadata(); + metadata.setScopeName("myCustomScope"); + return metadata; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/cycle/left/LeftConfig.java b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/cycle/left/LeftConfig.java new file mode 100644 index 0000000..2ff66f1 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/cycle/left/LeftConfig.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2011 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.componentscan.cycle.left; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ComponentScan("org.springframework.context.annotation.componentscan.cycle.right") +public class LeftConfig { + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/cycle/right/RightConfig.java b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/cycle/right/RightConfig.java new file mode 100644 index 0000000..73d5297 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/cycle/right/RightConfig.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2011 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.componentscan.cycle.right; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ComponentScan("org.springframework.context.annotation.componentscan.cycle.left") +public class RightConfig { + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/importing/ImportingConfig.java b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/importing/ImportingConfig.java new file mode 100644 index 0000000..8177c06 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/importing/ImportingConfig.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.componentscan.importing; + +import org.springframework.context.annotation.ComponentScanAndImportAnnotationInteractionTests; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import(ComponentScanAndImportAnnotationInteractionTests.ImportedConfig.class) +public class ImportingConfig { +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/level1/Level1Config.java b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/level1/Level1Config.java new file mode 100644 index 0000000..bf817af --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/level1/Level1Config.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.componentscan.level1; + +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ComponentScan("org.springframework.context.annotation.componentscan.level2") +public class Level1Config { + @Bean + public TestBean level1Bean() { + return new TestBean("level1Bean"); + } +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/level2/Level2Config.java b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/level2/Level2Config.java new file mode 100644 index 0000000..3a69e5e --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/level2/Level2Config.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.componentscan.level2; + +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ComponentScan("org.springframework.context.annotation.componentscan.level3") +public class Level2Config { + @Bean + public TestBean level2Bean() { + return new TestBean("level2Bean"); + } +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/level3/Level3Component.java b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/level3/Level3Component.java new file mode 100644 index 0000000..4892450 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/level3/Level3Component.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2011 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.componentscan.level3; + +import org.springframework.stereotype.Component; + +@Component +public class Level3Component { + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/simple/ClassWithNestedComponents.java b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/simple/ClassWithNestedComponents.java new file mode 100644 index 0000000..84bb476 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/simple/ClassWithNestedComponents.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.componentscan.simple; + +import org.springframework.stereotype.Component; + +public class ClassWithNestedComponents { + + @Component + public static class NestedComponent extends ClassWithNestedComponents { + } + + @Component + public static class OtherNestedComponent extends ClassWithNestedComponents { + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/componentscan/simple/SimpleComponent.java b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/simple/SimpleComponent.java new file mode 100644 index 0000000..9eb5738 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/componentscan/simple/SimpleComponent.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.componentscan.simple; + +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +@Component +public class SimpleComponent { + + @Bean + public String exampleBean() { + return "example"; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/AutowiredConfigurationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/AutowiredConfigurationTests.java new file mode 100644 index 0000000..fa90520 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/AutowiredConfigurationTests.java @@ -0,0 +1,495 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.configuration; + +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; +import java.util.Optional; + +import javax.inject.Provider; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.beans.testfixture.beans.Colour; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.annotation.AliasFor; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * System tests covering use of {@link Autowired} and {@link Value} within + * {@link Configuration} classes. + * + * @author Chris Beams + * @author Juergen Hoeller + * @author Sam Brannen + */ +public class AutowiredConfigurationTests { + + @Test + public void testAutowiredConfigurationDependencies() { + ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( + AutowiredConfigurationTests.class.getSimpleName() + ".xml", AutowiredConfigurationTests.class); + + assertThat(context.getBean("colour", Colour.class)).isEqualTo(Colour.RED); + assertThat(context.getBean("testBean", TestBean.class).getName()).isEqualTo(Colour.RED.toString()); + } + + @Test + public void testAutowiredConfigurationMethodDependencies() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + AutowiredMethodConfig.class, ColorConfig.class); + + assertThat(context.getBean(Colour.class)).isEqualTo(Colour.RED); + assertThat(context.getBean(TestBean.class).getName()).isEqualTo("RED-RED"); + } + + @Test + public void testAutowiredConfigurationMethodDependenciesWithOptionalAndAvailable() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + OptionalAutowiredMethodConfig.class, ColorConfig.class); + + assertThat(context.getBean(Colour.class)).isEqualTo(Colour.RED); + assertThat(context.getBean(TestBean.class).getName()).isEqualTo("RED-RED"); + } + + @Test + public void testAutowiredConfigurationMethodDependenciesWithOptionalAndNotAvailable() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + OptionalAutowiredMethodConfig.class); + + assertThat(context.getBeansOfType(Colour.class).isEmpty()).isTrue(); + assertThat(context.getBean(TestBean.class).getName()).isEqualTo(""); + } + + @Test + public void testAutowiredSingleConstructorSupported() { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(factory).loadBeanDefinitions( + new ClassPathResource("annotation-config.xml", AutowiredConstructorConfig.class)); + GenericApplicationContext ctx = new GenericApplicationContext(factory); + ctx.registerBeanDefinition("config1", new RootBeanDefinition(AutowiredConstructorConfig.class)); + ctx.registerBeanDefinition("config2", new RootBeanDefinition(ColorConfig.class)); + ctx.refresh(); + assertThat(ctx.getBean(Colour.class)).isSameAs(ctx.getBean(AutowiredConstructorConfig.class).colour); + } + + @Test + public void testObjectFactoryConstructorWithTypeVariable() { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(factory).loadBeanDefinitions( + new ClassPathResource("annotation-config.xml", ObjectFactoryConstructorConfig.class)); + GenericApplicationContext ctx = new GenericApplicationContext(factory); + ctx.registerBeanDefinition("config1", new RootBeanDefinition(ObjectFactoryConstructorConfig.class)); + ctx.registerBeanDefinition("config2", new RootBeanDefinition(ColorConfig.class)); + ctx.refresh(); + assertThat(ctx.getBean(Colour.class)).isSameAs(ctx.getBean(ObjectFactoryConstructorConfig.class).colour); + } + + @Test + public void testAutowiredAnnotatedConstructorSupported() { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(factory).loadBeanDefinitions( + new ClassPathResource("annotation-config.xml", MultipleConstructorConfig.class)); + GenericApplicationContext ctx = new GenericApplicationContext(factory); + ctx.registerBeanDefinition("config1", new RootBeanDefinition(MultipleConstructorConfig.class)); + ctx.registerBeanDefinition("config2", new RootBeanDefinition(ColorConfig.class)); + ctx.refresh(); + assertThat(ctx.getBean(Colour.class)).isSameAs(ctx.getBean(MultipleConstructorConfig.class).colour); + } + + @Test + public void testValueInjection() { + ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( + "ValueInjectionTests.xml", AutowiredConfigurationTests.class); + doTestValueInjection(context); + } + + @Test + public void testValueInjectionWithMetaAnnotation() { + AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(ValueConfigWithMetaAnnotation.class); + doTestValueInjection(context); + } + + @Test + public void testValueInjectionWithAliasedMetaAnnotation() { + AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(ValueConfigWithAliasedMetaAnnotation.class); + doTestValueInjection(context); + } + + @Test + public void testValueInjectionWithProviderFields() { + AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(ValueConfigWithProviderFields.class); + doTestValueInjection(context); + } + + @Test + public void testValueInjectionWithProviderConstructorArguments() { + AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(ValueConfigWithProviderConstructorArguments.class); + doTestValueInjection(context); + } + + @Test + public void testValueInjectionWithProviderMethodArguments() { + AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(ValueConfigWithProviderMethodArguments.class); + doTestValueInjection(context); + } + + private void doTestValueInjection(BeanFactory context) { + System.clearProperty("myProp"); + + TestBean testBean = context.getBean("testBean", TestBean.class); + assertThat((Object) testBean.getName()).isNull(); + + testBean = context.getBean("testBean2", TestBean.class); + assertThat((Object) testBean.getName()).isNull(); + + System.setProperty("myProp", "foo"); + + testBean = context.getBean("testBean", TestBean.class); + assertThat(testBean.getName()).isEqualTo("foo"); + + testBean = context.getBean("testBean2", TestBean.class); + assertThat(testBean.getName()).isEqualTo("foo"); + + System.clearProperty("myProp"); + + testBean = context.getBean("testBean", TestBean.class); + assertThat((Object) testBean.getName()).isNull(); + + testBean = context.getBean("testBean2", TestBean.class); + assertThat((Object) testBean.getName()).isNull(); + } + + @Test + public void testCustomPropertiesWithClassPathContext() throws IOException { + ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( + "AutowiredConfigurationTests-custom.xml", AutowiredConfigurationTests.class); + + TestBean testBean = context.getBean("testBean", TestBean.class); + assertThat(testBean.getName()).isEqualTo("localhost"); + assertThat(testBean.getAge()).isEqualTo(contentLength()); + } + + @Test + public void testCustomPropertiesWithGenericContext() throws IOException { + GenericApplicationContext context = new GenericApplicationContext(); + new XmlBeanDefinitionReader(context).loadBeanDefinitions( + new ClassPathResource("AutowiredConfigurationTests-custom.xml", AutowiredConfigurationTests.class)); + context.refresh(); + + TestBean testBean = context.getBean("testBean", TestBean.class); + assertThat(testBean.getName()).isEqualTo("localhost"); + assertThat(testBean.getAge()).isEqualTo(contentLength()); + } + + private int contentLength() throws IOException { + return (int) new ClassPathResource("do_not_delete_me.txt").contentLength(); + } + + + @Configuration + static class AutowiredConfig { + + @Autowired + private Colour colour; + + @Bean + public TestBean testBean() { + return new TestBean(colour.toString()); + } + } + + + @Configuration + static class AutowiredMethodConfig { + + @Bean + public TestBean testBean(Colour colour, List colours) { + return new TestBean(colour.toString() + "-" + colours.get(0).toString()); + } + } + + + @Configuration + static class OptionalAutowiredMethodConfig { + + @Bean + public TestBean testBean(Optional colour, Optional> colours) { + if (!colour.isPresent() && !colours.isPresent()) { + return new TestBean(""); + } + else { + return new TestBean(colour.get().toString() + "-" + colours.get().get(0).toString()); + } + } + } + + + @Configuration + static class AutowiredConstructorConfig { + + Colour colour; + + // @Autowired + AutowiredConstructorConfig(Colour colour) { + this.colour = colour; + } + } + + + @Configuration + static class ObjectFactoryConstructorConfig { + + Colour colour; + + // @Autowired + ObjectFactoryConstructorConfig(ObjectFactory colourFactory) { + this.colour = colourFactory.getObject(); + } + } + + + @Configuration + static class MultipleConstructorConfig { + + Colour colour; + + @Autowired + MultipleConstructorConfig(Colour colour) { + this.colour = colour; + } + + MultipleConstructorConfig(String test) { + this.colour = new Colour(test); + } + } + + + @Configuration + static class ColorConfig { + + @Bean + public Colour colour() { + return Colour.RED; + } + } + + + @Configuration + static class ValueConfig { + + @Value("#{systemProperties[myProp]}") + private String name; + + private String name2; + + @Value("#{systemProperties[myProp]}") + public void setName2(String name) { + this.name2 = name; + } + + @Bean @Scope("prototype") + public TestBean testBean() { + return new TestBean(name); + } + + @Bean @Scope("prototype") + public TestBean testBean2() { + return new TestBean(name2); + } + } + + + @Value("#{systemProperties[myProp]}") + @Retention(RetentionPolicy.RUNTIME) + public @interface MyProp { + } + + + @Configuration + @Scope("prototype") + static class ValueConfigWithMetaAnnotation { + + @MyProp + private String name; + + private String name2; + + @MyProp + public void setName2(String name) { + this.name2 = name; + } + + @Bean @Scope("prototype") + public TestBean testBean() { + return new TestBean(name); + } + + @Bean @Scope("prototype") + public TestBean testBean2() { + return new TestBean(name2); + } + } + + + @Value("") + @Retention(RetentionPolicy.RUNTIME) + public @interface AliasedProp { + + @AliasFor(annotation = Value.class) + String value(); + } + + + @Configuration + @Scope("prototype") + static class ValueConfigWithAliasedMetaAnnotation { + + @AliasedProp("#{systemProperties[myProp]}") + private String name; + + private String name2; + + @AliasedProp("#{systemProperties[myProp]}") + public void setName2(String name) { + this.name2 = name; + } + + @Bean @Scope("prototype") + public TestBean testBean() { + return new TestBean(name); + } + + @Bean @Scope("prototype") + public TestBean testBean2() { + return new TestBean(name2); + } + } + + + @Configuration + static class ValueConfigWithProviderFields { + + @Value("#{systemProperties[myProp]}") + private Provider name; + + private Provider name2; + + @Value("#{systemProperties[myProp]}") + public void setName2(Provider name) { + this.name2 = name; + } + + @Bean @Scope("prototype") + public TestBean testBean() { + return new TestBean(name.get()); + } + + @Bean @Scope("prototype") + public TestBean testBean2() { + return new TestBean(name2.get()); + } + } + + + static class ValueConfigWithProviderConstructorArguments { + + private final Provider name; + + private final Provider name2; + + @Autowired + public ValueConfigWithProviderConstructorArguments(@Value("#{systemProperties[myProp]}") Provider name, + @Value("#{systemProperties[myProp]}") Provider name2) { + this.name = name; + this.name2 = name2; + } + + @Bean @Scope("prototype") + public TestBean testBean() { + return new TestBean(name.get()); + } + + @Bean @Scope("prototype") + public TestBean testBean2() { + return new TestBean(name2.get()); + } + } + + + @Configuration + static class ValueConfigWithProviderMethodArguments { + + @Bean @Scope("prototype") + public TestBean testBean(@Value("#{systemProperties[myProp]}") Provider name) { + return new TestBean(name.get()); + } + + @Bean @Scope("prototype") + public TestBean testBean2(@Value("#{systemProperties[myProp]}") Provider name2) { + return new TestBean(name2.get()); + } + } + + + @Configuration + static class PropertiesConfig { + + private String hostname; + + private Resource resource; + + @Value("#{myProps.hostname}") + public void setHostname(String hostname) { + this.hostname = hostname; + } + + @Value("do_not_delete_me.txt") + public void setResource(Resource resource) { + this.resource = resource; + } + + @Bean + public TestBean testBean() throws IOException { + return new TestBean(hostname, (int) resource.contentLength()); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanAnnotationAttributePropagationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanAnnotationAttributePropagationTests.java new file mode 100644 index 0000000..0ba5a87 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanAnnotationAttributePropagationTests.java @@ -0,0 +1,167 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.configuration; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowire; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ConfigurationClassPostProcessor; +import org.springframework.context.annotation.DependsOn; +import org.springframework.context.annotation.Lazy; +import org.springframework.context.annotation.Primary; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests proving that the various attributes available via the {@link Bean} + * annotation are correctly reflected in the {@link BeanDefinition} created when + * processing the {@link Configuration} class. + * + *

    Also includes tests proving that using {@link Lazy} and {@link Primary} + * annotations in conjunction with Bean propagate their respective metadata + * correctly into the resulting BeanDefinition + * + * @author Chris Beams + * @author Juergen Hoeller + */ +public class BeanAnnotationAttributePropagationTests { + + @Test + public void autowireMetadataIsPropagated() { + @Configuration class Config { + @Bean(autowire=Autowire.BY_TYPE) Object foo() { return null; } + } + + assertThat(beanDef(Config.class).getAutowireMode()).as("autowire mode was not propagated").isEqualTo(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); + } + + @Test + public void autowireCandidateMetadataIsPropagated() { + @Configuration class Config { + @Bean(autowireCandidate=false) Object foo() { return null; } + } + + assertThat(beanDef(Config.class).isAutowireCandidate()).as("autowire candidate flag was not propagated").isFalse(); + } + + @Test + public void initMethodMetadataIsPropagated() { + @Configuration class Config { + @Bean(initMethod="start") Object foo() { return null; } + } + + assertThat(beanDef(Config.class).getInitMethodName()).as("init method name was not propagated").isEqualTo("start"); + } + + @Test + public void destroyMethodMetadataIsPropagated() { + @Configuration class Config { + @Bean(destroyMethod="destroy") Object foo() { return null; } + } + + assertThat(beanDef(Config.class).getDestroyMethodName()).as("destroy method name was not propagated").isEqualTo("destroy"); + } + + @Test + public void dependsOnMetadataIsPropagated() { + @Configuration class Config { + @Bean() @DependsOn({"bar", "baz"}) Object foo() { return null; } + } + + assertThat(beanDef(Config.class).getDependsOn()).as("dependsOn metadata was not propagated").isEqualTo(new String[] {"bar", "baz"}); + } + + @Test + public void primaryMetadataIsPropagated() { + @Configuration class Config { + @Primary @Bean + Object foo() { return null; } + } + + assertThat(beanDef(Config.class).isPrimary()).as("primary metadata was not propagated").isTrue(); + } + + @Test + public void primaryMetadataIsFalseByDefault() { + @Configuration class Config { + @Bean Object foo() { return null; } + } + + assertThat(beanDef(Config.class).isPrimary()).as("@Bean methods should be non-primary by default").isFalse(); + } + + @Test + public void lazyMetadataIsPropagated() { + @Configuration class Config { + @Lazy @Bean + Object foo() { return null; } + } + + assertThat(beanDef(Config.class).isLazyInit()).as("lazy metadata was not propagated").isTrue(); + } + + @Test + public void lazyMetadataIsFalseByDefault() { + @Configuration class Config { + @Bean Object foo() { return null; } + } + + assertThat(beanDef(Config.class).isLazyInit()).as("@Bean methods should be non-lazy by default").isFalse(); + } + + @Test + public void defaultLazyConfigurationPropagatesToIndividualBeans() { + @Lazy @Configuration class Config { + @Bean Object foo() { return null; } + } + + assertThat(beanDef(Config.class).isLazyInit()).as("@Bean methods declared in a @Lazy @Configuration should be lazily instantiated").isTrue(); + } + + @Test + public void eagerBeanOverridesDefaultLazyConfiguration() { + @Lazy @Configuration class Config { + @Lazy(false) @Bean Object foo() { return null; } + } + + assertThat(beanDef(Config.class).isLazyInit()).as("@Lazy(false) @Bean methods declared in a @Lazy @Configuration should be eagerly instantiated").isFalse(); + } + + @Test + public void eagerConfigurationProducesEagerBeanDefinitions() { + @Lazy(false) @Configuration class Config { // will probably never happen, doesn't make much sense + @Bean Object foo() { return null; } + } + + assertThat(beanDef(Config.class).isLazyInit()).as("@Lazy(false) @Configuration should produce eager bean definitions").isFalse(); + } + + private AbstractBeanDefinition beanDef(Class configClass) { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerBeanDefinition("config", new RootBeanDefinition(configClass)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(factory); + return (AbstractBeanDefinition) factory.getBeanDefinition("foo"); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java new file mode 100644 index 0000000..d38bda7 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java @@ -0,0 +1,271 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.configuration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.NestedTestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.stereotype.Component; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests proving that @Qualifier annotations work when used + * with @Configuration classes on @Bean methods. + * + * @author Chris Beams + * @author Juergen Hoeller + */ +public class BeanMethodQualificationTests { + + @Test + public void testStandard() { + AnnotationConfigApplicationContext ctx = + new AnnotationConfigApplicationContext(StandardConfig.class, StandardPojo.class); + assertThat(ctx.getBeanFactory().containsSingleton("testBean1")).isFalse(); + StandardPojo pojo = ctx.getBean(StandardPojo.class); + assertThat(pojo.testBean.getName()).isEqualTo("interesting"); + assertThat(pojo.testBean2.getName()).isEqualTo("boring"); + } + + @Test + public void testScoped() { + AnnotationConfigApplicationContext ctx = + new AnnotationConfigApplicationContext(ScopedConfig.class, StandardPojo.class); + assertThat(ctx.getBeanFactory().containsSingleton("testBean1")).isFalse(); + StandardPojo pojo = ctx.getBean(StandardPojo.class); + assertThat(pojo.testBean.getName()).isEqualTo("interesting"); + assertThat(pojo.testBean2.getName()).isEqualTo("boring"); + } + + @Test + public void testScopedProxy() { + AnnotationConfigApplicationContext ctx = + new AnnotationConfigApplicationContext(ScopedProxyConfig.class, StandardPojo.class); + assertThat(ctx.getBeanFactory().containsSingleton("testBean1")).isTrue(); // a shared scoped proxy + StandardPojo pojo = ctx.getBean(StandardPojo.class); + assertThat(pojo.testBean.getName()).isEqualTo("interesting"); + assertThat(pojo.testBean2.getName()).isEqualTo("boring"); + } + + @Test + public void testCustomWithLazyResolution() { + AnnotationConfigApplicationContext ctx = + new AnnotationConfigApplicationContext(CustomConfig.class, CustomPojo.class); + assertThat(ctx.getBeanFactory().containsSingleton("testBean1")).isFalse(); + assertThat(ctx.getBeanFactory().containsSingleton("testBean2")).isFalse(); + assertThat(BeanFactoryAnnotationUtils.isQualifierMatch(value -> value.equals("boring"), + "testBean2", ctx.getDefaultListableBeanFactory())).isTrue(); + CustomPojo pojo = ctx.getBean(CustomPojo.class); + assertThat(pojo.testBean.getName()).isEqualTo("interesting"); + TestBean testBean2 = BeanFactoryAnnotationUtils.qualifiedBeanOfType( + ctx.getDefaultListableBeanFactory(), TestBean.class, "boring"); + assertThat(testBean2.getName()).isEqualTo("boring"); + } + + @Test + public void testCustomWithEarlyResolution() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(CustomConfig.class, CustomPojo.class); + ctx.refresh(); + assertThat(ctx.getBeanFactory().containsSingleton("testBean1")).isFalse(); + assertThat(ctx.getBeanFactory().containsSingleton("testBean2")).isFalse(); + ctx.getBean("testBean2"); + assertThat(BeanFactoryAnnotationUtils.isQualifierMatch(value -> value.equals("boring"), + "testBean2", ctx.getDefaultListableBeanFactory())).isTrue(); + CustomPojo pojo = ctx.getBean(CustomPojo.class); + assertThat(pojo.testBean.getName()).isEqualTo("interesting"); + } + + @Test + public void testCustomWithAsm() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.registerBeanDefinition("customConfig", new RootBeanDefinition(CustomConfig.class.getName())); + RootBeanDefinition customPojo = new RootBeanDefinition(CustomPojo.class.getName()); + customPojo.setLazyInit(true); + ctx.registerBeanDefinition("customPojo", customPojo); + ctx.refresh(); + assertThat(ctx.getBeanFactory().containsSingleton("testBean1")).isFalse(); + assertThat(ctx.getBeanFactory().containsSingleton("testBean2")).isFalse(); + CustomPojo pojo = ctx.getBean(CustomPojo.class); + assertThat(pojo.testBean.getName()).isEqualTo("interesting"); + } + + @Test + public void testCustomWithAttributeOverride() { + AnnotationConfigApplicationContext ctx = + new AnnotationConfigApplicationContext(CustomConfigWithAttributeOverride.class, CustomPojo.class); + assertThat(ctx.getBeanFactory().containsSingleton("testBeanX")).isFalse(); + CustomPojo pojo = ctx.getBean(CustomPojo.class); + assertThat(pojo.testBean.getName()).isEqualTo("interesting"); + } + + @Test + public void testBeanNamesForAnnotation() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(StandardConfig.class); + assertThat(ctx.getBeanNamesForAnnotation(Configuration.class)).isEqualTo(new String[] {"beanMethodQualificationTests.StandardConfig"}); + assertThat(ctx.getBeanNamesForAnnotation(Scope.class)).isEqualTo(new String[] {}); + assertThat(ctx.getBeanNamesForAnnotation(Lazy.class)).isEqualTo(new String[] {"testBean1"}); + assertThat(ctx.getBeanNamesForAnnotation(Boring.class)).isEqualTo(new String[] {"testBean2"}); + } + + + @Configuration + static class StandardConfig { + + @Bean @Qualifier("interesting") @Lazy + public TestBean testBean1() { + return new TestBean("interesting"); + } + + @Bean @Boring + public TestBean testBean2(@Lazy TestBean testBean1) { + TestBean tb = new TestBean("boring"); + tb.setSpouse(testBean1); + return tb; + } + } + + @Configuration + static class ScopedConfig { + + @Bean @Qualifier("interesting") @Scope("prototype") + public TestBean testBean1() { + return new TestBean("interesting"); + } + + @Bean @Boring @Scope("prototype") + public TestBean testBean2(TestBean testBean1) { + TestBean tb = new TestBean("boring"); + tb.setSpouse(testBean1); + return tb; + } + } + + @Configuration + static class ScopedProxyConfig { + + @Bean @Qualifier("interesting") @Scope(value="prototype", proxyMode=ScopedProxyMode.TARGET_CLASS) + public TestBean testBean1() { + return new TestBean("interesting"); + } + + @Bean @Boring @Scope(value="prototype", proxyMode=ScopedProxyMode.TARGET_CLASS) + public TestBean testBean2(TestBean testBean1) { + TestBean tb = new TestBean("boring"); + tb.setSpouse(testBean1); + return tb; + } + } + + @Component @Lazy + static class StandardPojo { + + @Autowired @Qualifier("interesting") TestBean testBean; + + @Autowired @Boring TestBean testBean2; + } + + @Qualifier + @Retention(RetentionPolicy.RUNTIME) + public @interface Boring { + } + + @Configuration + static class CustomConfig { + + @InterestingBean + public TestBean testBean1() { + return new TestBean("interesting"); + } + + @Bean @Qualifier("boring") @Lazy + public TestBean testBean2(@Lazy TestBean testBean1) { + TestBean tb = new TestBean("boring"); + tb.setSpouse(testBean1); + return tb; + } + } + + @Configuration + static class CustomConfigWithAttributeOverride { + + @InterestingBeanWithName(name="testBeanX") + public TestBean testBean1() { + return new TestBean("interesting"); + } + + @Bean @Qualifier("boring") + public TestBean testBean2(@Lazy TestBean testBean1) { + TestBean tb = new TestBean("boring"); + tb.setSpouse(testBean1); + return tb; + } + } + + @InterestingPojo + static class CustomPojo { + + @InterestingNeed TestBean testBean; + + @InterestingNeedWithRequiredOverride(required=false) NestedTestBean nestedTestBean; + } + + @Bean @Lazy @Qualifier("interesting") + @Retention(RetentionPolicy.RUNTIME) + public @interface InterestingBean { + } + + @Bean @Lazy @Qualifier("interesting") + @Retention(RetentionPolicy.RUNTIME) + public @interface InterestingBeanWithName { + + String name(); + } + + @Autowired @Qualifier("interesting") + @Retention(RetentionPolicy.RUNTIME) + public @interface InterestingNeed { + } + + @Autowired @Qualifier("interesting") + @Retention(RetentionPolicy.RUNTIME) + public @interface InterestingNeedWithRequiredOverride { + + boolean required(); + } + + @Component @Lazy + @Retention(RetentionPolicy.RUNTIME) + public @interface InterestingPojo { + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationBeanNameTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationBeanNameTests.java new file mode 100644 index 0000000..cbe2d51 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationBeanNameTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.configuration; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.annotation.AnnotationBeanNameGenerator; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.stereotype.Component; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Unit tests ensuring that configuration class bean names as expressed via @Configuration + * or @Component 'value' attributes are indeed respected, and that customization of bean + * naming through a BeanNameGenerator strategy works as expected. + * + * @author Chris Beams + * @since 3.1.1 + */ +public class ConfigurationBeanNameTests { + + @Test + public void registerOuterConfig() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(A.class); + ctx.refresh(); + assertThat(ctx.containsBean("outer")).isTrue(); + assertThat(ctx.containsBean("imported")).isTrue(); + assertThat(ctx.containsBean("nested")).isTrue(); + assertThat(ctx.containsBean("nestedBean")).isTrue(); + } + + @Test + public void registerNestedConfig() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(A.B.class); + ctx.refresh(); + assertThat(ctx.containsBean("outer")).isFalse(); + assertThat(ctx.containsBean("imported")).isFalse(); + assertThat(ctx.containsBean("nested")).isTrue(); + assertThat(ctx.containsBean("nestedBean")).isTrue(); + } + + @Test + public void registerOuterConfig_withBeanNameGenerator() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.setBeanNameGenerator(new AnnotationBeanNameGenerator() { + @Override + public String generateBeanName( + BeanDefinition definition, BeanDefinitionRegistry registry) { + return "custom-" + super.generateBeanName(definition, registry); + } + }); + ctx.register(A.class); + ctx.refresh(); + assertThat(ctx.containsBean("custom-outer")).isTrue(); + assertThat(ctx.containsBean("custom-imported")).isTrue(); + assertThat(ctx.containsBean("custom-nested")).isTrue(); + assertThat(ctx.containsBean("nestedBean")).isTrue(); + } + + @Configuration("outer") + @Import(C.class) + static class A { + @Component("nested") + static class B { + @Bean public String nestedBean() { return ""; } + } + } + + @Configuration("imported") + static class C { + @Bean public String s() { return "s"; } + } +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassAspectIntegrationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassAspectIntegrationTests.java new file mode 100644 index 0000000..d46c3a4 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassAspectIntegrationTests.java @@ -0,0 +1,165 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.configuration; + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.After; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ConfigurationClassPostProcessor; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * System tests covering use of AspectJ {@link Aspect}s in conjunction with {@link Configuration} classes. + * {@link Bean} methods may return aspects, or Configuration classes may themselves be annotated with Aspect. + * In the latter case, advice methods are declared inline within the Configuration class. This makes for a + * particularly convenient syntax requiring no extra artifact for the aspect. + * + *

    Currently it is assumed that the user is bootstrapping Configuration class processing via XML (using + * annotation-config or component-scan), and thus will also use {@code } to enable + * processing of the Aspect annotation. + * + * @author Chris Beams + * @author Juergen Hoeller + */ +public class ConfigurationClassAspectIntegrationTests { + + @Test + public void aspectAnnotatedConfiguration() { + assertAdviceWasApplied(AspectConfig.class); + } + + @Test + public void configurationIncludesAspect() { + assertAdviceWasApplied(ConfigurationWithAspect.class); + } + + private void assertAdviceWasApplied(Class configClass) { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(factory).loadBeanDefinitions( + new ClassPathResource("aspectj-autoproxy-config.xml", ConfigurationClassAspectIntegrationTests.class)); + GenericApplicationContext ctx = new GenericApplicationContext(factory); + ctx.addBeanFactoryPostProcessor(new ConfigurationClassPostProcessor()); + ctx.registerBeanDefinition("config", new RootBeanDefinition(configClass)); + ctx.refresh(); + + TestBean testBean = ctx.getBean("testBean", TestBean.class); + assertThat(testBean.getName()).isEqualTo("name"); + testBean.absquatulate(); + assertThat(testBean.getName()).isEqualTo("advisedName"); + } + + @Test + public void withInnerClassAndLambdaExpression() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(Application.class, CountingAspect.class); + ctx.getBeansOfType(Runnable.class).forEach((k, v) -> v.run()); + + // TODO: returns just 1 as of AspectJ 1.9 beta 3, not detecting the applicable lambda expression anymore + // assertEquals(2, ctx.getBean(CountingAspect.class).count); + } + + + @Aspect + @Configuration + static class AspectConfig { + + @Bean + public TestBean testBean() { + return new TestBean("name"); + } + + @Before("execution(* org.springframework.beans.testfixture.beans.TestBean.absquatulate(..)) && target(testBean)") + public void touchBean(TestBean testBean) { + testBean.setName("advisedName"); + } + } + + + @Configuration + static class ConfigurationWithAspect { + + @Bean + public TestBean testBean() { + return new TestBean("name"); + } + + @Bean + public NameChangingAspect nameChangingAspect() { + return new NameChangingAspect(); + } + } + + + @Aspect + static class NameChangingAspect { + + @Before("execution(* org.springframework.beans.testfixture.beans.TestBean.absquatulate(..)) && target(testBean)") + public void touchBean(TestBean testBean) { + testBean.setName("advisedName"); + } + } + + + + @Configuration + @EnableAspectJAutoProxy + public static class Application { + + @Bean + Runnable fromInnerClass() { + return new Runnable() { + @Override + public void run() { + } + }; + } + + @Bean + Runnable fromLambdaExpression() { + return () -> { + }; + } + } + + + @Aspect + public static class CountingAspect { + + public int count = 0; + + @After("execution(* java.lang.Runnable.*(..))") + public void after(JoinPoint joinPoint) { + count++; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java new file mode 100644 index 0000000..89da3b9 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java @@ -0,0 +1,690 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.configuration; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; + +import javax.annotation.Resource; +import javax.inject.Provider; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InjectionPoint; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.config.ListFactoryBean; +import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.NestedTestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.AnnotationConfigUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ConfigurationClassPostProcessor; +import org.springframework.context.annotation.Scope; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.support.GenericApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Miscellaneous system tests covering {@link Bean} naming, aliases, scoping and + * error handling within {@link Configuration} class definitions. + * + * @author Chris Beams + * @author Juergen Hoeller + * @author Sam Brannen + */ +public class ConfigurationClassProcessingTests { + + @Test + public void customBeanNameIsRespectedWhenConfiguredViaNameAttribute() { + customBeanNameIsRespected(ConfigWithBeanWithCustomName.class, + () -> ConfigWithBeanWithCustomName.testBean, "customName"); + } + + @Test + public void customBeanNameIsRespectedWhenConfiguredViaValueAttribute() { + customBeanNameIsRespected(ConfigWithBeanWithCustomNameConfiguredViaValueAttribute.class, + () -> ConfigWithBeanWithCustomNameConfiguredViaValueAttribute.testBean, "enigma"); + } + + private void customBeanNameIsRespected(Class testClass, Supplier testBeanSupplier, String beanName) { + GenericApplicationContext ac = new GenericApplicationContext(); + AnnotationConfigUtils.registerAnnotationConfigProcessors(ac); + ac.registerBeanDefinition("config", new RootBeanDefinition(testClass)); + ac.refresh(); + + assertThat(ac.getBean(beanName)).isSameAs(testBeanSupplier.get()); + + // method name should not be registered + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> + ac.getBean("methodName")); + } + + @Test + public void aliasesAreRespectedWhenConfiguredViaNameAttribute() { + aliasesAreRespected(ConfigWithBeanWithAliases.class, + () -> ConfigWithBeanWithAliases.testBean, "name1"); + } + + @Test + public void aliasesAreRespectedWhenConfiguredViaValueAttribute() { + aliasesAreRespected(ConfigWithBeanWithAliasesConfiguredViaValueAttribute.class, + () -> ConfigWithBeanWithAliasesConfiguredViaValueAttribute.testBean, "enigma"); + } + + private void aliasesAreRespected(Class testClass, Supplier testBeanSupplier, String beanName) { + TestBean testBean = testBeanSupplier.get(); + BeanFactory factory = initBeanFactory(testClass); + + assertThat(factory.getBean(beanName)).isSameAs(testBean); + Arrays.stream(factory.getAliases(beanName)).map(factory::getBean).forEach(alias -> assertThat(alias).isSameAs(testBean)); + + // method name should not be registered + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> + factory.getBean("methodName")); + } + + @Test // SPR-11830 + public void configWithBeanWithProviderImplementation() { + GenericApplicationContext ac = new GenericApplicationContext(); + AnnotationConfigUtils.registerAnnotationConfigProcessors(ac); + ac.registerBeanDefinition("config", new RootBeanDefinition(ConfigWithBeanWithProviderImplementation.class)); + ac.refresh(); + assertThat(ConfigWithBeanWithProviderImplementation.testBean).isSameAs(ac.getBean("customName")); + } + + @Test // SPR-11830 + public void configWithSetWithProviderImplementation() { + GenericApplicationContext ac = new GenericApplicationContext(); + AnnotationConfigUtils.registerAnnotationConfigProcessors(ac); + ac.registerBeanDefinition("config", new RootBeanDefinition(ConfigWithSetWithProviderImplementation.class)); + ac.refresh(); + assertThat(ConfigWithSetWithProviderImplementation.set).isSameAs(ac.getBean("customName")); + } + + @Test + public void testFinalBeanMethod() { + assertThatExceptionOfType(BeanDefinitionParsingException.class).isThrownBy(() -> + initBeanFactory(ConfigWithFinalBean.class)); + } + + @Test + public void simplestPossibleConfig() { + BeanFactory factory = initBeanFactory(SimplestPossibleConfig.class); + String stringBean = factory.getBean("stringBean", String.class); + assertThat(stringBean).isEqualTo("foo"); + } + + @Test + public void configWithObjectReturnType() { + BeanFactory factory = initBeanFactory(ConfigWithNonSpecificReturnTypes.class); + assertThat(factory.getType("stringBean")).isEqualTo(Object.class); + assertThat(factory.isTypeMatch("stringBean", String.class)).isFalse(); + String stringBean = factory.getBean("stringBean", String.class); + assertThat(stringBean).isEqualTo("foo"); + } + + @Test + public void configWithFactoryBeanReturnType() { + ListableBeanFactory factory = initBeanFactory(ConfigWithNonSpecificReturnTypes.class); + assertThat(factory.getType("factoryBean")).isEqualTo(List.class); + assertThat(factory.isTypeMatch("factoryBean", List.class)).isTrue(); + assertThat(factory.getType("&factoryBean")).isEqualTo(FactoryBean.class); + assertThat(factory.isTypeMatch("&factoryBean", FactoryBean.class)).isTrue(); + assertThat(factory.isTypeMatch("&factoryBean", BeanClassLoaderAware.class)).isFalse(); + assertThat(factory.isTypeMatch("&factoryBean", ListFactoryBean.class)).isFalse(); + boolean condition = factory.getBean("factoryBean") instanceof List; + assertThat(condition).isTrue(); + + String[] beanNames = factory.getBeanNamesForType(FactoryBean.class); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("&factoryBean"); + + beanNames = factory.getBeanNamesForType(BeanClassLoaderAware.class); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("&factoryBean"); + + beanNames = factory.getBeanNamesForType(ListFactoryBean.class); + assertThat(beanNames.length).isEqualTo(1); + assertThat(beanNames[0]).isEqualTo("&factoryBean"); + + beanNames = factory.getBeanNamesForType(List.class); + assertThat(beanNames[0]).isEqualTo("factoryBean"); + } + + @Test + public void configurationWithPrototypeScopedBeans() { + BeanFactory factory = initBeanFactory(ConfigWithPrototypeBean.class); + + TestBean foo = factory.getBean("foo", TestBean.class); + ITestBean bar = factory.getBean("bar", ITestBean.class); + ITestBean baz = factory.getBean("baz", ITestBean.class); + + assertThat(bar).isSameAs(foo.getSpouse()); + assertThat(baz).isNotSameAs(bar.getSpouse()); + } + + @Test + public void configurationWithNullReference() { + BeanFactory factory = initBeanFactory(ConfigWithNullReference.class); + + TestBean foo = factory.getBean("foo", TestBean.class); + assertThat(factory.getBean("bar").equals(null)).isTrue(); + assertThat(foo.getSpouse()).isNull(); + } + + @Test + public void configurationWithAdaptivePrototypes() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConfigWithPrototypeBean.class, AdaptiveInjectionPoints.class); + ctx.refresh(); + + AdaptiveInjectionPoints adaptive = ctx.getBean(AdaptiveInjectionPoints.class); + assertThat(adaptive.adaptiveInjectionPoint1.getName()).isEqualTo("adaptiveInjectionPoint1"); + assertThat(adaptive.adaptiveInjectionPoint2.getName()).isEqualTo("setAdaptiveInjectionPoint2"); + + adaptive = ctx.getBean(AdaptiveInjectionPoints.class); + assertThat(adaptive.adaptiveInjectionPoint1.getName()).isEqualTo("adaptiveInjectionPoint1"); + assertThat(adaptive.adaptiveInjectionPoint2.getName()).isEqualTo("setAdaptiveInjectionPoint2"); + ctx.close(); + } + + @Test + public void configurationWithAdaptiveResourcePrototypes() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConfigWithPrototypeBean.class, AdaptiveResourceInjectionPoints.class); + ctx.refresh(); + + AdaptiveResourceInjectionPoints adaptive = ctx.getBean(AdaptiveResourceInjectionPoints.class); + assertThat(adaptive.adaptiveInjectionPoint1.getName()).isEqualTo("adaptiveInjectionPoint1"); + assertThat(adaptive.adaptiveInjectionPoint2.getName()).isEqualTo("setAdaptiveInjectionPoint2"); + + adaptive = ctx.getBean(AdaptiveResourceInjectionPoints.class); + assertThat(adaptive.adaptiveInjectionPoint1.getName()).isEqualTo("adaptiveInjectionPoint1"); + assertThat(adaptive.adaptiveInjectionPoint2.getName()).isEqualTo("setAdaptiveInjectionPoint2"); + ctx.close(); + } + + @Test + public void configurationWithPostProcessor() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConfigWithPostProcessor.class); + RootBeanDefinition placeholderConfigurer = new RootBeanDefinition(PropertyPlaceholderConfigurer.class); + placeholderConfigurer.getPropertyValues().add("properties", "myProp=myValue"); + ctx.registerBeanDefinition("placeholderConfigurer", placeholderConfigurer); + ctx.refresh(); + + TestBean foo = ctx.getBean("foo", TestBean.class); + ITestBean bar = ctx.getBean("bar", ITestBean.class); + ITestBean baz = ctx.getBean("baz", ITestBean.class); + + assertThat(foo.getName()).isEqualTo("foo-processed-myValue"); + assertThat(bar.getName()).isEqualTo("bar-processed-myValue"); + assertThat(baz.getName()).isEqualTo("baz-processed-myValue"); + + SpousyTestBean listener = ctx.getBean("listenerTestBean", SpousyTestBean.class); + assertThat(listener.refreshed).isTrue(); + ctx.close(); + } + + @Test + public void configurationWithFunctionalRegistration() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConfigWithFunctionalRegistration.class); + ctx.refresh(); + + assertThat(ctx.getBean(TestBean.class).getSpouse()).isSameAs(ctx.getBean("spouse")); + assertThat(ctx.getBean(NestedTestBean.class).getCompany()).isEqualTo("functional"); + } + + @Test + public void configurationWithApplicationListener() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConfigWithApplicationListener.class); + ctx.refresh(); + + ConfigWithApplicationListener config = ctx.getBean(ConfigWithApplicationListener.class); + assertThat(config.closed).isFalse(); + ctx.close(); + assertThat(config.closed).isTrue(); + } + + @Test + public void configurationWithOverloadedBeanMismatch() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.registerBeanDefinition("config", new RootBeanDefinition(OverloadedBeanMismatch.class)); + ctx.refresh(); + + TestBean tb = ctx.getBean(TestBean.class); + assertThat(tb.getLawyer()).isEqualTo(ctx.getBean(NestedTestBean.class)); + } + + @Test + public void configurationWithOverloadedBeanMismatchWithAsm() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.registerBeanDefinition("config", new RootBeanDefinition(OverloadedBeanMismatch.class.getName())); + ctx.refresh(); + + TestBean tb = ctx.getBean(TestBean.class); + assertThat(tb.getLawyer()).isEqualTo(ctx.getBean(NestedTestBean.class)); + } + + @Test // gh-26019 + public void autowiringWithDynamicPrototypeBeanClass() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext( + ConfigWithDynamicPrototype.class, PrototypeDependency.class); + + PrototypeInterface p1 = ctx.getBean(PrototypeInterface.class, 1); + assertThat(p1).isInstanceOf(PrototypeOne.class); + assertThat(((PrototypeOne) p1).prototypeDependency).isNotNull(); + + PrototypeInterface p2 = ctx.getBean(PrototypeInterface.class, 2); + assertThat(p2).isInstanceOf(PrototypeTwo.class); + + PrototypeInterface p3 = ctx.getBean(PrototypeInterface.class, 1); + assertThat(p3).isInstanceOf(PrototypeOne.class); + assertThat(((PrototypeOne) p3).prototypeDependency).isNotNull(); + } + + + /** + * Creates a new {@link BeanFactory}, populates it with a {@link BeanDefinition} + * for each of the given {@link Configuration} {@code configClasses}, and then + * post-processes the factory using JavaConfig's {@link ConfigurationClassPostProcessor}. + * When complete, the factory is ready to service requests for any {@link Bean} methods + * declared by {@code configClasses}. + */ + private DefaultListableBeanFactory initBeanFactory(Class... configClasses) { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + for (Class configClass : configClasses) { + String configBeanName = configClass.getName(); + factory.registerBeanDefinition(configBeanName, new RootBeanDefinition(configClass)); + } + ConfigurationClassPostProcessor ccpp = new ConfigurationClassPostProcessor(); + ccpp.postProcessBeanDefinitionRegistry(factory); + ccpp.postProcessBeanFactory(factory); + factory.freezeConfiguration(); + return factory; + } + + + @Configuration + static class ConfigWithBeanWithCustomName { + + static TestBean testBean = new TestBean(ConfigWithBeanWithCustomName.class.getSimpleName()); + + @Bean(name = "customName") + public TestBean methodName() { + return testBean; + } + } + + + @Configuration + static class ConfigWithBeanWithCustomNameConfiguredViaValueAttribute { + + static TestBean testBean = new TestBean(ConfigWithBeanWithCustomNameConfiguredViaValueAttribute.class.getSimpleName()); + + @Bean("enigma") + public TestBean methodName() { + return testBean; + } + } + + + @Configuration + static class ConfigWithBeanWithAliases { + + static TestBean testBean = new TestBean(ConfigWithBeanWithAliases.class.getSimpleName()); + + @Bean(name = {"name1", "alias1", "alias2", "alias3"}) + public TestBean methodName() { + return testBean; + } + } + + + @Configuration + static class ConfigWithBeanWithAliasesConfiguredViaValueAttribute { + + static TestBean testBean = new TestBean(ConfigWithBeanWithAliasesConfiguredViaValueAttribute.class.getSimpleName()); + + @Bean({"enigma", "alias1", "alias2", "alias3"}) + public TestBean methodName() { + return testBean; + } + } + + + @Configuration + static class ConfigWithBeanWithProviderImplementation implements Provider { + + static TestBean testBean = new TestBean(ConfigWithBeanWithProviderImplementation.class.getSimpleName()); + + @Override + @Bean(name = "customName") + public TestBean get() { + return testBean; + } + } + + + @Configuration + static class ConfigWithSetWithProviderImplementation implements Provider> { + + static Set set = Collections.singleton("value"); + + @Override + @Bean(name = "customName") + public Set get() { + return set; + } + } + + + @Configuration + static class ConfigWithFinalBean { + + public final @Bean TestBean testBean() { + return new TestBean(); + } + } + + + @Configuration + static class SimplestPossibleConfig { + + public @Bean String stringBean() { + return "foo"; + } + } + + + @Configuration + static class ConfigWithNonSpecificReturnTypes { + + public @Bean Object stringBean() { + return "foo"; + } + + public @Bean FactoryBean factoryBean() { + ListFactoryBean fb = new ListFactoryBean(); + fb.setSourceList(Arrays.asList("element1", "element2")); + return fb; + } + } + + + @Configuration + static class ConfigWithPrototypeBean { + + public @Bean TestBean foo() { + TestBean foo = new SpousyTestBean("foo"); + foo.setSpouse(bar()); + return foo; + } + + public @Bean TestBean bar() { + TestBean bar = new SpousyTestBean("bar"); + bar.setSpouse(baz()); + return bar; + } + + @Bean @Scope("prototype") + public TestBean baz() { + return new TestBean("baz"); + } + + @Bean @Scope("prototype") + public TestBean adaptive1(InjectionPoint ip) { + return new TestBean(ip.getMember().getName()); + } + + @Bean @Scope("prototype") + public TestBean adaptive2(DependencyDescriptor dd) { + return new TestBean(dd.getMember().getName()); + } + } + + + @Configuration + static class ConfigWithNullReference extends ConfigWithPrototypeBean { + + @Override + public TestBean bar() { + return null; + } + } + + + @Scope("prototype") + static class AdaptiveInjectionPoints { + + @Autowired @Qualifier("adaptive1") + public TestBean adaptiveInjectionPoint1; + + public TestBean adaptiveInjectionPoint2; + + @Autowired @Qualifier("adaptive2") + public void setAdaptiveInjectionPoint2(TestBean adaptiveInjectionPoint2) { + this.adaptiveInjectionPoint2 = adaptiveInjectionPoint2; + } + } + + + @Scope("prototype") + static class AdaptiveResourceInjectionPoints { + + @Resource(name = "adaptive1") + public TestBean adaptiveInjectionPoint1; + + public TestBean adaptiveInjectionPoint2; + + @Resource(name = "adaptive2") + public void setAdaptiveInjectionPoint2(TestBean adaptiveInjectionPoint2) { + this.adaptiveInjectionPoint2 = adaptiveInjectionPoint2; + } + } + + + static class ConfigWithPostProcessor extends ConfigWithPrototypeBean { + + @Value("${myProp}") + private String myProp; + + @Bean + public POBPP beanPostProcessor() { + return new POBPP() { + + String nameSuffix = "-processed-" + myProp; + + public void setNameSuffix(String nameSuffix) { + this.nameSuffix = nameSuffix; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) { + if (bean instanceof ITestBean) { + ((ITestBean) bean).setName(((ITestBean) bean).getName() + nameSuffix); + } + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + return bean; + } + + public int getOrder() { + return 0; + } + }; + } + + // @Bean + public BeanFactoryPostProcessor beanFactoryPostProcessor() { + return new BeanFactoryPostProcessor() { + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { + BeanDefinition bd = beanFactory.getBeanDefinition("beanPostProcessor"); + bd.getPropertyValues().addPropertyValue("nameSuffix", "-processed-" + myProp); + } + }; + } + + @Bean + public ITestBean listenerTestBean() { + return new SpousyTestBean("listener"); + } + } + + + public interface POBPP extends BeanPostProcessor { + } + + + private static class SpousyTestBean extends TestBean implements ApplicationListener { + + public boolean refreshed = false; + + public SpousyTestBean(String name) { + super(name); + } + + @Override + public void setSpouse(ITestBean spouse) { + super.setSpouse(spouse); + } + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + this.refreshed = true; + } + } + + + @Configuration + static class ConfigWithFunctionalRegistration { + + @Autowired + void register(GenericApplicationContext ctx) { + ctx.registerBean("spouse", TestBean.class, + () -> new TestBean("functional")); + Supplier testBeanSupplier = () -> new TestBean(ctx.getBean("spouse", TestBean.class)); + ctx.registerBean(TestBean.class, + testBeanSupplier, + bd -> bd.setPrimary(true)); + } + + @Bean + public NestedTestBean nestedTestBean(TestBean testBean) { + return new NestedTestBean(testBean.getSpouse().getName()); + } + } + + + @Configuration + static class ConfigWithApplicationListener { + + boolean closed = false; + + @Bean + public ApplicationListener listener() { + return (event -> this.closed = true); + } + } + + + @Configuration + public static class OverloadedBeanMismatch { + + @Bean(name = "other") + public NestedTestBean foo() { + return new NestedTestBean(); + } + + @Bean(name = "foo") + public TestBean foo(@Qualifier("other") NestedTestBean other) { + TestBean tb = new TestBean(); + tb.setLawyer(other); + return tb; + } + } + + + static class PrototypeDependency { + } + + interface PrototypeInterface { + } + + static class PrototypeOne extends AbstractPrototype { + + @Autowired + PrototypeDependency prototypeDependency; + + } + + static class PrototypeTwo extends AbstractPrototype { + + // no autowired dependency here, in contrast to above + } + + static class AbstractPrototype implements PrototypeInterface { + } + + @Configuration + static class ConfigWithDynamicPrototype { + + @Bean + @Scope(value = "prototype") + public PrototypeInterface getDemoBean( int i) { + switch ( i) { + case 1: return new PrototypeOne(); + case 2: + default: + return new PrototypeTwo(); + + } + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassWithPlaceholderConfigurerBeanTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassWithPlaceholderConfigurerBeanTests.java new file mode 100644 index 0000000..0dfcb05 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassWithPlaceholderConfigurerBeanTests.java @@ -0,0 +1,185 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.configuration; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * A configuration class that registers a non-static placeholder configurer {@code @Bean} + * method cannot also have {@code @Value} fields. Logically, the config class must be + * instantiated in order to invoke the placeholder configurer bean method, and it is a + * chicken-and-egg problem to process the {@code @Value} field. + * + *

    Therefore, placeholder configurer bean methods should either be {@code static} or + * put in separate configuration classes as has been done in the tests below. Simply said, + * placeholder configurer {@code @Bean} methods and {@code @Value} fields in the same + * configuration class are mutually exclusive unless the placeholder configurer + * {@code @Bean} method is {@code static}. + * + * @author Chris Beams + * @author Juergen Hoeller + * @author Sam Brannen + */ +public class ConfigurationClassWithPlaceholderConfigurerBeanTests { + + /** + * Test which proves that a non-static property placeholder bean cannot be declared + * in the same configuration class that has a {@code @Value} field in need of + * placeholder replacement. It's an obvious chicken-and-egg issue. + * + *

    One solution is to do as {@link #valueFieldsAreProcessedWhenPlaceholderConfigurerIsSegregated()} + * does and segregate the two bean definitions across configuration classes. + * + *

    Another solution is to simply make the {@code @Bean} method for the property + * placeholder {@code static} as in + * {@link #valueFieldsAreProcessedWhenStaticPlaceholderConfigurerIsIntegrated()}. + */ + @Test + @SuppressWarnings("resource") + public void valueFieldsAreNotProcessedWhenPlaceholderConfigurerIsIntegrated() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConfigWithValueFieldAndPlaceholderConfigurer.class); + System.setProperty("test.name", "foo"); + ctx.refresh(); + System.clearProperty("test.name"); + + TestBean testBean = ctx.getBean(TestBean.class); + // Proof that the @Value field did not get set: + assertThat(testBean.getName()).isNull(); + } + + @Test + @SuppressWarnings("resource") + public void valueFieldsAreProcessedWhenStaticPlaceholderConfigurerIsIntegrated() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConfigWithValueFieldAndStaticPlaceholderConfigurer.class); + System.setProperty("test.name", "foo"); + ctx.refresh(); + System.clearProperty("test.name"); + + TestBean testBean = ctx.getBean(TestBean.class); + assertThat(testBean.getName()).isEqualTo("foo"); + } + + @Test + @SuppressWarnings("resource") + public void valueFieldsAreProcessedWhenPlaceholderConfigurerIsSegregated() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConfigWithValueField.class); + ctx.register(ConfigWithPlaceholderConfigurer.class); + System.setProperty("test.name", "foo"); + ctx.refresh(); + System.clearProperty("test.name"); + + TestBean testBean = ctx.getBean(TestBean.class); + assertThat(testBean.getName()).isEqualTo("foo"); + } + + @Test + @SuppressWarnings("resource") + public void valueFieldsResolveToPlaceholderSpecifiedDefaultValuesWithPlaceholderConfigurer() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConfigWithValueField.class); + ctx.register(ConfigWithPlaceholderConfigurer.class); + ctx.refresh(); + + TestBean testBean = ctx.getBean(TestBean.class); + assertThat(testBean.getName()).isEqualTo("bar"); + } + + @Test + @SuppressWarnings("resource") + public void valueFieldsResolveToPlaceholderSpecifiedDefaultValuesWithoutPlaceholderConfigurer() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ConfigWithValueField.class); + // ctx.register(ConfigWithPlaceholderConfigurer.class); + ctx.refresh(); + + TestBean testBean = ctx.getBean(TestBean.class); + assertThat(testBean.getName()).isEqualTo("bar"); + } + + + @Configuration + static class ConfigWithValueField { + + @Value("${test.name:bar}") + private String name; + + @Bean + public ITestBean testBean() { + return new TestBean(this.name); + } + } + + + @Configuration + static class ConfigWithPlaceholderConfigurer { + + @Bean + public PropertySourcesPlaceholderConfigurer ppc() { + return new PropertySourcesPlaceholderConfigurer(); + } + } + + + @Configuration + static class ConfigWithValueFieldAndPlaceholderConfigurer { + + @Value("${test.name}") + private String name; + + @Bean + public ITestBean testBean() { + return new TestBean(this.name); + } + + @Bean + public PropertySourcesPlaceholderConfigurer ppc() { + return new PropertySourcesPlaceholderConfigurer(); + } + } + + @Configuration + static class ConfigWithValueFieldAndStaticPlaceholderConfigurer { + + @Value("${test.name}") + private String name; + + @Bean + public ITestBean testBean() { + return new TestBean(this.name); + } + + @Bean + public static PropertySourcesPlaceholderConfigurer ppc() { + return new PropertySourcesPlaceholderConfigurer(); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationMetaAnnotationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationMetaAnnotationTests.java new file mode 100644 index 0000000..43aac4f --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationMetaAnnotationTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.configuration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Ensures that @Configuration is supported properly as a meta-annotation. + * + * @author Chris Beams + */ +public class ConfigurationMetaAnnotationTests { + + @Test + public void customConfigurationStereotype() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(Config.class); + ctx.refresh(); + assertThat(ctx.containsBean("customName")).isTrue(); + TestBean a = ctx.getBean("a", TestBean.class); + TestBean b = ctx.getBean("b", TestBean.class); + assertThat(b).isSameAs(a.getSpouse()); + } + + + @TestConfiguration("customName") + static class Config { + @Bean + public TestBean a() { + TestBean a = new TestBean(); + a.setSpouse(b()); + return a; + } + + @Bean + public TestBean b() { + return new TestBean(); + } + } + + + @Configuration + @Retention(RetentionPolicy.RUNTIME) + public @interface TestConfiguration { + String value() default ""; + } +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/DuplicateConfigurationClassPostProcessorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/DuplicateConfigurationClassPostProcessorTests.java new file mode 100644 index 0000000..c3e4345 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/DuplicateConfigurationClassPostProcessorTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2011 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.configuration; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ConfigurationClassPostProcessor; +import org.springframework.context.support.GenericApplicationContext; + +/** + * Corners the bug originally reported by SPR-8824, where the presence of two + * {@link ConfigurationClassPostProcessor} beans in combination with a @Configuration + * class having at least one @Bean method causes a "Singleton 'foo' isn't currently in + * creation" exception. + * + * @author Chris Beams + * @since 3.1 + */ +public class DuplicateConfigurationClassPostProcessorTests { + + @Test + public void repro() { + GenericApplicationContext ctx = new GenericApplicationContext(); + ctx.registerBeanDefinition("a", new RootBeanDefinition(ConfigurationClassPostProcessor.class)); + ctx.registerBeanDefinition("b", new RootBeanDefinition(ConfigurationClassPostProcessor.class)); + ctx.registerBeanDefinition("myConfig", new RootBeanDefinition(Config.class)); + ctx.refresh(); + } + + @Configuration + static class Config { + @Bean + public String string() { + return "bean"; + } + } +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/DuplicatePostProcessingTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/DuplicatePostProcessingTests.java new file mode 100644 index 0000000..3271beb --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/DuplicatePostProcessingTests.java @@ -0,0 +1,137 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.configuration; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; + +/** + * @author Andy Wilkinson + * @author Juergen Hoeller + */ +public class DuplicatePostProcessingTests { + + @Test + public void testWithFactoryBeanAndEventListener() { + new AnnotationConfigApplicationContext(Config.class).getBean(ExampleBean.class); + } + + + + static class Config { + + @Bean + public ExampleFactoryBean exampleFactory() { + return new ExampleFactoryBean(); + } + + @Bean + public static ExampleBeanPostProcessor exampleBeanPostProcessor() { + return new ExampleBeanPostProcessor(); + } + + @Bean + public ExampleApplicationEventListener exampleApplicationEventListener() { + return new ExampleApplicationEventListener(); + } + } + + + static class ExampleFactoryBean implements FactoryBean { + + private final ExampleBean exampleBean = new ExampleBean(); + + @Override + public ExampleBean getObject() { + return this.exampleBean; + } + + @Override + public Class getObjectType() { + return ExampleBean.class; + } + + @Override + public boolean isSingleton() { + return true; + } + } + + + static class ExampleBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware { + + private ApplicationContext applicationContext; + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) { + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + if (bean instanceof ExampleBean) { + this.applicationContext.publishEvent(new ExampleApplicationEvent(this)); + } + return bean; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + } + + + @SuppressWarnings("serial") + static class ExampleApplicationEvent extends ApplicationEvent { + + public ExampleApplicationEvent(Object source) { + super(source); + } + } + + + static class ExampleApplicationEventListener implements ApplicationListener, BeanFactoryAware { + + private BeanFactory beanFactory; + + @Override + public void onApplicationEvent(ExampleApplicationEvent event) { + this.beanFactory.getBean(ExampleBean.class); + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + } + + + static class ExampleBean { + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportAnnotationDetectionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportAnnotationDetectionTests.java new file mode 100644 index 0000000..240640c --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportAnnotationDetectionTests.java @@ -0,0 +1,156 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.configuration; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Tests that @Import may be used both as a locally declared and meta-declared + * annotation, that all declarations are processed, and that any local declaration + * is processed last. + * + * @author Chris Beams + * @since 3.1 + */ +@SuppressWarnings("resource") +public class ImportAnnotationDetectionTests { + + @Test + public void multipleMetaImportsAreProcessed() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(MultiMetaImportConfig.class); + ctx.refresh(); + assertThat(ctx.containsBean("testBean1")).isTrue(); + assertThat(ctx.containsBean("testBean2")).isTrue(); + } + + @Test + public void localAndMetaImportsAreProcessed() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(MultiMetaImportConfigWithLocalImport.class); + ctx.refresh(); + assertThat(ctx.containsBean("testBean1")).isTrue(); + assertThat(ctx.containsBean("testBean2")).isTrue(); + assertThat(ctx.containsBean("testBean3")).isTrue(); + } + + @Test + public void localImportIsProcessedLast() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(MultiMetaImportConfigWithLocalImportWithBeanOverride.class); + ctx.refresh(); + assertThat(ctx.containsBean("testBean1")).isTrue(); + assertThat(ctx.containsBean("testBean2")).isTrue(); + assertThat(ctx.getBean("testBean2", TestBean.class).getName()).isEqualTo("2a"); + } + + @Test + public void importFromBean() throws Exception { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ImportFromBean.class); + ctx.refresh(); + assertThat(ctx.containsBean("importAnnotationDetectionTests.ImportFromBean")).isTrue(); + assertThat(ctx.containsBean("testBean1")).isTrue(); + assertThat(ctx.getBean("testBean1", TestBean.class).getName()).isEqualTo("1"); + } + + @Configuration + @MetaImport1 + @MetaImport2 + static class MultiMetaImportConfig { + } + + @Configuration + @MetaImport1 + @MetaImport2 + @Import(Config3.class) + static class MultiMetaImportConfigWithLocalImport { + } + + @Configuration + @MetaImport1 + @MetaImport2 + @Import(Config2a.class) + static class MultiMetaImportConfigWithLocalImportWithBeanOverride { + } + + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Import(Config1.class) + @interface MetaImport1 { + } + + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Import(Config2.class) + @interface MetaImport2 { + } + + + @Configuration + static class Config1 { + @Bean + TestBean testBean1() { + return new TestBean("1"); + } + } + + @Configuration + static class Config2 { + @Bean + TestBean testBean2() { + return new TestBean("2"); + } + } + + @Configuration + static class Config2a { + @Bean + TestBean testBean2() { + return new TestBean("2a"); + } + } + + @Configuration + static class Config3 { + @Bean + TestBean testBean3() { + return new TestBean("3"); + } + } + + @MetaImport1 + static class ImportFromBean { + + } +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportResourceTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportResourceTests.java new file mode 100644 index 0000000..87dd37d --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportResourceTests.java @@ -0,0 +1,174 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.configuration; + +import java.util.Collections; + +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.support.PropertiesBeanDefinitionReader; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportResource; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.PropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link ImportResource} support. + * + * @author Chris Beams + * @author Juergen Hoeller + * @author Sam Brannen + */ +public class ImportResourceTests { + + @Test + public void importXml() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportXmlConfig.class); + assertThat(ctx.containsBean("javaDeclaredBean")).as("did not contain java-declared bean").isTrue(); + assertThat(ctx.containsBean("xmlDeclaredBean")).as("did not contain xml-declared bean").isTrue(); + TestBean tb = ctx.getBean("javaDeclaredBean", TestBean.class); + assertThat(tb.getName()).isEqualTo("myName"); + ctx.close(); + } + + @Test + public void importXmlIsInheritedFromSuperclassDeclarations() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(FirstLevelSubConfig.class); + assertThat(ctx.containsBean("xmlDeclaredBean")).isTrue(); + ctx.close(); + } + + @Test + public void importXmlIsMergedFromSuperclassDeclarations() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SecondLevelSubConfig.class); + assertThat(ctx.containsBean("secondLevelXmlDeclaredBean")).as("failed to pick up second-level-declared XML bean").isTrue(); + assertThat(ctx.containsBean("xmlDeclaredBean")).as("failed to pick up parent-declared XML bean").isTrue(); + ctx.close(); + } + + @Test + public void importXmlWithNamespaceConfig() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportXmlWithAopNamespaceConfig.class); + Object bean = ctx.getBean("proxiedXmlBean"); + assertThat(AopUtils.isAopProxy(bean)).isTrue(); + ctx.close(); + } + + @Test + public void importXmlWithOtherConfigurationClass() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportXmlWithConfigurationClass.class); + assertThat(ctx.containsBean("javaDeclaredBean")).as("did not contain java-declared bean").isTrue(); + assertThat(ctx.containsBean("xmlDeclaredBean")).as("did not contain xml-declared bean").isTrue(); + TestBean tb = ctx.getBean("javaDeclaredBean", TestBean.class); + assertThat(tb.getName()).isEqualTo("myName"); + ctx.close(); + } + + @Test + public void importWithPlaceholder() throws Exception { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + PropertySource propertySource = new MapPropertySource("test", + Collections. singletonMap("test", "springframework")); + ctx.getEnvironment().getPropertySources().addFirst(propertySource); + ctx.register(ImportXmlConfig.class); + ctx.refresh(); + assertThat(ctx.containsBean("xmlDeclaredBean")).as("did not contain xml-declared bean").isTrue(); + ctx.close(); + } + + @Test + public void importXmlWithAutowiredConfig() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportXmlAutowiredConfig.class); + String name = ctx.getBean("xmlBeanName", String.class); + assertThat(name).isEqualTo("xml.declared"); + ctx.close(); + } + + @Test + public void importNonXmlResource() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportNonXmlResourceConfig.class); + assertThat(ctx.containsBean("propertiesDeclaredBean")).isTrue(); + ctx.close(); + } + + + @Configuration + @ImportResource("classpath:org/springframework/context/annotation/configuration/ImportXmlConfig-context.xml") + static class ImportXmlConfig { + @Value("${name}") + private String name; + public @Bean TestBean javaDeclaredBean() { + return new TestBean(this.name); + } + } + + @Configuration + @ImportResource("classpath:org/springframework/context/annotation/configuration/ImportXmlConfig-context.xml") + static class BaseConfig { + } + + @Configuration + static class FirstLevelSubConfig extends BaseConfig { + } + + @Configuration + @ImportResource("classpath:org/springframework/context/annotation/configuration/SecondLevelSubConfig-context.xml") + static class SecondLevelSubConfig extends BaseConfig { + } + + @Configuration + @ImportResource("classpath:org/springframework/context/annotation/configuration/ImportXmlWithAopNamespace-context.xml") + static class ImportXmlWithAopNamespaceConfig { + } + + @Aspect + static class AnAspect { + @Before("execution(* org.springframework.beans.testfixture.beans.TestBean.*(..))") + public void advice() { } + } + + @Configuration + @ImportResource("classpath:org/springframework/context/annotation/configuration/ImportXmlWithConfigurationClass-context.xml") + static class ImportXmlWithConfigurationClass { + } + + @Configuration + @ImportResource("classpath:org/springframework/context/annotation/configuration/ImportXmlConfig-context.xml") + static class ImportXmlAutowiredConfig { + @Autowired TestBean xmlDeclaredBean; + + public @Bean String xmlBeanName() { + return xmlDeclaredBean.getName(); + } + } + + @Configuration + @ImportResource(locations = "classpath:org/springframework/context/annotation/configuration/ImportNonXmlResourceConfig-context.properties", reader = PropertiesBeanDefinitionReader.class) + static class ImportNonXmlResourceConfig { + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportTests.java new file mode 100644 index 0000000..e82f604 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportTests.java @@ -0,0 +1,359 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.configuration; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ConfigurationClassPostProcessor; +import org.springframework.context.annotation.DependsOn; +import org.springframework.context.annotation.Import; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * System tests for {@link Import} annotation support. + * + * @author Chris Beams + * @author Juergen Hoeller + */ +public class ImportTests { + + private DefaultListableBeanFactory processConfigurationClasses(Class... classes) { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + for (Class clazz : classes) { + beanFactory.registerBeanDefinition(clazz.getSimpleName(), new RootBeanDefinition(clazz)); + } + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + return beanFactory; + } + + private void assertBeanDefinitionCount(int expectedCount, Class... classes) { + DefaultListableBeanFactory beanFactory = processConfigurationClasses(classes); + assertThat(beanFactory.getBeanDefinitionCount()).isEqualTo(expectedCount); + beanFactory.preInstantiateSingletons(); + for (Class clazz : classes) { + beanFactory.getBean(clazz); + } + + } + + @Test + public void testProcessImportsWithAsm() { + int configClasses = 2; + int beansInClasses = 2; + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition("config", new RootBeanDefinition(ConfigurationWithImportAnnotation.class.getName())); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + assertThat(beanFactory.getBeanDefinitionCount()).isEqualTo(configClasses + beansInClasses); + } + + @Test + public void testProcessImportsWithDoubleImports() { + int configClasses = 3; + int beansInClasses = 3; + assertBeanDefinitionCount((configClasses + beansInClasses), ConfigurationWithImportAnnotation.class, OtherConfigurationWithImportAnnotation.class); + } + + @Test + public void testProcessImportsWithExplicitOverridingBefore() { + int configClasses = 2; + int beansInClasses = 2; + assertBeanDefinitionCount((configClasses + beansInClasses), OtherConfiguration.class, ConfigurationWithImportAnnotation.class); + } + + @Test + public void testProcessImportsWithExplicitOverridingAfter() { + int configClasses = 2; + int beansInClasses = 2; + assertBeanDefinitionCount((configClasses + beansInClasses), ConfigurationWithImportAnnotation.class, OtherConfiguration.class); + } + + @Configuration + @Import(OtherConfiguration.class) + static class ConfigurationWithImportAnnotation { + @Bean + public ITestBean one() { + return new TestBean(); + } + } + + @Configuration + @Import(OtherConfiguration.class) + static class OtherConfigurationWithImportAnnotation { + @Bean + public ITestBean two() { + return new TestBean(); + } + } + + @Configuration + static class OtherConfiguration { + @Bean + public ITestBean three() { + return new TestBean(); + } + } + + // ------------------------------------------------------------------------ + + @Test + public void testImportAnnotationWithTwoLevelRecursion() { + int configClasses = 2; + int beansInClasses = 3; + assertBeanDefinitionCount((configClasses + beansInClasses), AppConfig.class); + } + + @Configuration + @Import(DataSourceConfig.class) + static class AppConfig { + + @Bean + public ITestBean transferService() { + return new TestBean(accountRepository()); + } + + @Bean + public ITestBean accountRepository() { + return new TestBean(); + } + } + + @Configuration + static class DataSourceConfig { + @Bean + public ITestBean dataSourceA() { + return new TestBean(); + } + } + + // ------------------------------------------------------------------------ + + @Test + public void testImportAnnotationWithThreeLevelRecursion() { + int configClasses = 4; + int beansInClasses = 5; + assertBeanDefinitionCount(configClasses + beansInClasses, FirstLevel.class); + } + + // ------------------------------------------------------------------------ + + @Test + public void testImportAnnotationWithMultipleArguments() { + int configClasses = 3; + int beansInClasses = 3; + assertBeanDefinitionCount((configClasses + beansInClasses), WithMultipleArgumentsToImportAnnotation.class); + } + + + @Test + public void testImportAnnotationWithMultipleArgumentsResultingInOverriddenBeanDefinition() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition("config", new RootBeanDefinition( + WithMultipleArgumentsThatWillCauseDuplication.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + assertThat(beanFactory.getBeanDefinitionCount()).isEqualTo(4); + assertThat(beanFactory.getBean("foo", ITestBean.class).getName()).isEqualTo("foo2"); + } + + @Configuration + @Import({Foo1.class, Foo2.class}) + static class WithMultipleArgumentsThatWillCauseDuplication { + } + + @Configuration + static class Foo1 { + @Bean + public ITestBean foo() { + return new TestBean("foo1"); + } + } + + @Configuration + static class Foo2 { + @Bean + public ITestBean foo() { + return new TestBean("foo2"); + } + } + + // ------------------------------------------------------------------------ + + @Test + public void testImportAnnotationOnInnerClasses() { + int configClasses = 2; + int beansInClasses = 2; + assertBeanDefinitionCount((configClasses + beansInClasses), OuterConfig.InnerConfig.class); + } + + @Configuration + static class OuterConfig { + @Bean + String whatev() { + return "whatev"; + } + + @Configuration + @Import(ExternalConfig.class) + static class InnerConfig { + @Bean + public ITestBean innerBean() { + return new TestBean(); + } + } + } + + @Configuration + static class ExternalConfig { + @Bean + public ITestBean extBean() { + return new TestBean(); + } + } + + // ------------------------------------------------------------------------ + + @Configuration + @Import(SecondLevel.class) + static class FirstLevel { + @Bean + public TestBean m() { + return new TestBean(); + } + } + + @Configuration + @Import({ThirdLevel.class, InitBean.class}) + static class SecondLevel { + @Bean + public TestBean n() { + return new TestBean(); + } + } + + @Configuration + @DependsOn("org.springframework.context.annotation.configuration.ImportTests$InitBean") + static class ThirdLevel { + public ThirdLevel() { + assertThat(InitBean.initialized).isTrue(); + } + + @Bean + public ITestBean thirdLevelA() { + return new TestBean(); + } + + @Bean + public ITestBean thirdLevelB() { + return new TestBean(); + } + + @Bean + public ITestBean thirdLevelC() { + return new TestBean(); + } + } + + static class InitBean { + public static boolean initialized = false; + + public InitBean() { + initialized = true; + } + } + + @Configuration + @Import({LeftConfig.class, RightConfig.class}) + static class WithMultipleArgumentsToImportAnnotation { + @Bean + public TestBean m() { + return new TestBean(); + } + } + + @Configuration + static class LeftConfig { + @Bean + public ITestBean left() { + return new TestBean(); + } + } + + @Configuration + static class RightConfig { + @Bean + public ITestBean right() { + return new TestBean(); + } + } + + // ------------------------------------------------------------------------ + + @Test + public void testImportNonConfigurationAnnotationClass() { + int configClasses = 2; + int beansInClasses = 0; + assertBeanDefinitionCount((configClasses + beansInClasses), ConfigAnnotated.class); + } + + @Configuration + @Import(NonConfigAnnotated.class) + static class ConfigAnnotated { } + + static class NonConfigAnnotated { } + + // ------------------------------------------------------------------------ + + /** + * Test that values supplied to @Configuration(value="...") are propagated as the + * bean name for the configuration class even in the case of inclusion via @Import + * or in the case of automatic registration via nesting + */ + @Test + public void reproSpr9023() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(B.class); + ctx.refresh(); + System.out.println(ctx.getBeanFactory()); + assertThat(ctx.getBeanNamesForType(B.class)[0]).isEqualTo("config-b"); + assertThat(ctx.getBeanNamesForType(A.class)[0]).isEqualTo("config-a"); + } + + @Configuration("config-a") + static class A { } + + @Configuration("config-b") + @Import(A.class) + static class B { } + + @Test + public void testProcessImports() { + int configClasses = 2; + int beansInClasses = 2; + assertBeanDefinitionCount((configClasses + beansInClasses), ConfigurationWithImportAnnotation.class); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportWithConditionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportWithConditionTests.java new file mode 100644 index 0000000..1a7b5f9 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportWithConditionTests.java @@ -0,0 +1,115 @@ +/* + * Copyright 2012-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.configuration; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ConfigurationCondition; +import org.springframework.context.annotation.Import; +import org.springframework.core.type.AnnotatedTypeMetadata; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Andy Wilkinson + */ +public class ImportWithConditionTests { + + private AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + + @Test + public void conditionalThenUnconditional() throws Exception { + this.context.register(ConditionalThenUnconditional.class); + this.context.refresh(); + assertThat(this.context.containsBean("beanTwo")).isFalse(); + assertThat(this.context.containsBean("beanOne")).isTrue(); + } + + @Test + public void unconditionalThenConditional() throws Exception { + this.context.register(UnconditionalThenConditional.class); + this.context.refresh(); + assertThat(this.context.containsBean("beanTwo")).isFalse(); + assertThat(this.context.containsBean("beanOne")).isTrue(); + } + + + @Configuration + @Import({ConditionalConfiguration.class, UnconditionalConfiguration.class}) + protected static class ConditionalThenUnconditional { + + @Autowired + private BeanOne beanOne; + } + + + @Configuration + @Import({UnconditionalConfiguration.class, ConditionalConfiguration.class}) + protected static class UnconditionalThenConditional { + + @Autowired + private BeanOne beanOne; + } + + + @Configuration + @Import(BeanProvidingConfiguration.class) + protected static class UnconditionalConfiguration { + } + + + @Configuration + @Conditional(NeverMatchingCondition.class) + @Import(BeanProvidingConfiguration.class) + protected static class ConditionalConfiguration { + } + + + @Configuration + protected static class BeanProvidingConfiguration { + + @Bean + BeanOne beanOne() { + return new BeanOne(); + } + } + + + private static final class BeanOne { + } + + + private static final class NeverMatchingCondition implements ConfigurationCondition { + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + return false; + } + + @Override + public ConfigurationPhase getConfigurationPhase() { + return ConfigurationPhase.REGISTER_BEAN; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportedConfigurationClassEnhancementTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportedConfigurationClassEnhancementTests.java new file mode 100644 index 0000000..df640ff --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ImportedConfigurationClassEnhancementTests.java @@ -0,0 +1,116 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.configuration; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests cornering the bug exposed in SPR-6779. + * + * @author Chris Beams + * @author Juergen Hoeller + */ +public class ImportedConfigurationClassEnhancementTests { + + @Test + public void autowiredConfigClassIsEnhancedWhenImported() { + autowiredConfigClassIsEnhanced(ConfigThatDoesImport.class); + } + + @Test + public void autowiredConfigClassIsEnhancedWhenRegisteredViaConstructor() { + autowiredConfigClassIsEnhanced(ConfigThatDoesNotImport.class, ConfigToBeAutowired.class); + } + + private void autowiredConfigClassIsEnhanced(Class... configClasses) { + ApplicationContext ctx = new AnnotationConfigApplicationContext(configClasses); + Config config = ctx.getBean(Config.class); + assertThat(ClassUtils.isCglibProxy(config.autowiredConfig)).as("autowired config class has not been enhanced").isTrue(); + } + + + @Test + public void autowiredConfigClassBeanMethodsRespectScopingWhenImported() { + autowiredConfigClassBeanMethodsRespectScoping(ConfigThatDoesImport.class); + } + + @Test + public void autowiredConfigClassBeanMethodsRespectScopingWhenRegisteredViaConstructor() { + autowiredConfigClassBeanMethodsRespectScoping(ConfigThatDoesNotImport.class, ConfigToBeAutowired.class); + } + + private void autowiredConfigClassBeanMethodsRespectScoping(Class... configClasses) { + ApplicationContext ctx = new AnnotationConfigApplicationContext(configClasses); + Config config = ctx.getBean(Config.class); + TestBean testBean1 = config.autowiredConfig.testBean(); + TestBean testBean2 = config.autowiredConfig.testBean(); + assertThat(testBean1) + .as("got two distinct instances of testBean when singleton scoping was expected") + .isSameAs(testBean2); + } + + + @Test + public void importingNonConfigurationClassCausesBeanDefinitionParsingException() { + ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigThatImportsNonConfigClass.class); + ConfigThatImportsNonConfigClass config = ctx.getBean(ConfigThatImportsNonConfigClass.class); + assertThat(config.testBean).isSameAs(ctx.getBean(TestBean.class)); + } + + + + @Configuration + static class ConfigToBeAutowired { + + public @Bean TestBean testBean() { + return new TestBean(); + } + } + + static class Config { + + @Autowired ConfigToBeAutowired autowiredConfig; + } + + @Import(ConfigToBeAutowired.class) + @Configuration + static class ConfigThatDoesImport extends Config { + } + + @Configuration + static class ConfigThatDoesNotImport extends Config { + } + + @Configuration + @Import(TestBean.class) + static class ConfigThatImportsNonConfigClass { + + @Autowired TestBean testBean; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/PackagePrivateBeanMethodInheritanceTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/PackagePrivateBeanMethodInheritanceTests.java new file mode 100644 index 0000000..39b6995 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/PackagePrivateBeanMethodInheritanceTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.configuration; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Reproduces SPR-8756, which has been marked as "won't fix" for reasons + * described in the issue. Also demonstrates the suggested workaround. + * + * @author Chris Beams + */ +public class PackagePrivateBeanMethodInheritanceTests { + + @Test + public void repro() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ReproConfig.class); + ctx.refresh(); + Foo foo1 = ctx.getBean("foo1", Foo.class); + Foo foo2 = ctx.getBean("foo2", Foo.class); + ctx.getBean("packagePrivateBar", Bar.class); // <-- i.e. @Bean was registered + assertThat(foo1.bar).isNotEqualTo(foo2.bar); // <-- i.e. @Bean *not* enhanced + } + + @Test + public void workaround() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(WorkaroundConfig.class); + ctx.refresh(); + Foo foo1 = ctx.getBean("foo1", Foo.class); + Foo foo2 = ctx.getBean("foo2", Foo.class); + ctx.getBean("protectedBar", Bar.class); // <-- i.e. @Bean was registered + assertThat(foo1.bar).isEqualTo(foo2.bar); // <-- i.e. @Bean *was* enhanced + } + + public static class Foo { + final Bar bar; + public Foo(Bar bar) { + this.bar = bar; + } + } + + public static class Bar { + } + + @Configuration + public static class ReproConfig extends org.springframework.context.annotation.configuration.a.BaseConfig { + @Bean + public Foo foo1() { + return new Foo(reproBar()); + } + + @Bean + public Foo foo2() { + return new Foo(reproBar()); + } + } + + @Configuration + public static class WorkaroundConfig extends org.springframework.context.annotation.configuration.a.BaseConfig { + @Bean + public Foo foo1() { + return new Foo(workaroundBar()); + } + + @Bean + public Foo foo2() { + return new Foo(workaroundBar()); + } + } +} + diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ScopingTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ScopingTests.java new file mode 100644 index 0000000..d407d04 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ScopingTests.java @@ -0,0 +1,377 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.configuration; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.scope.ScopedObject; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.context.support.GenericApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests that scopes are properly supported by using a custom Scope implementations + * and scoped proxy {@link Bean} declarations. + * + * @author Costin Leau + * @author Chris Beams + */ +public class ScopingTests { + + public static String flag = "1"; + + private static final String SCOPE = "my scope"; + + private CustomScope customScope; + + private GenericApplicationContext ctx; + + + @BeforeEach + public void setUp() throws Exception { + customScope = new CustomScope(); + ctx = createContext(ScopedConfigurationClass.class); + } + + @AfterEach + public void tearDown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + private GenericApplicationContext createContext(Class configClass) { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + if (customScope != null) { + beanFactory.registerScope(SCOPE, customScope); + } + beanFactory.registerBeanDefinition("config", new RootBeanDefinition(configClass)); + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(beanFactory); + ctx.refresh(); + return ctx; + } + + + @Test + public void testScopeOnClasses() throws Exception { + genericTestScope("scopedClass"); + } + + @Test + public void testScopeOnInterfaces() throws Exception { + genericTestScope("scopedInterface"); + } + + private void genericTestScope(String beanName) throws Exception { + String message = "scope is ignored"; + Object bean1 = ctx.getBean(beanName); + Object bean2 = ctx.getBean(beanName); + + assertThat(bean2).as(message).isSameAs(bean1); + + Object bean3 = ctx.getBean(beanName); + + assertThat(bean3).as(message).isSameAs(bean1); + + // make the scope create a new object + customScope.createNewScope = true; + + Object newBean1 = ctx.getBean(beanName); + assertThat(newBean1).as(message).isNotSameAs(bean1); + + Object sameBean1 = ctx.getBean(beanName); + + assertThat(sameBean1).as(message).isSameAs(newBean1); + + // make the scope create a new object + customScope.createNewScope = true; + + Object newBean2 = ctx.getBean(beanName); + assertThat(newBean2).as(message).isNotSameAs(newBean1); + + // make the scope create a new object .. again + customScope.createNewScope = true; + + Object newBean3 = ctx.getBean(beanName); + assertThat(newBean3).as(message).isNotSameAs(newBean2); + } + + @Test + public void testSameScopeOnDifferentBeans() throws Exception { + Object beanAInScope = ctx.getBean("scopedClass"); + Object beanBInScope = ctx.getBean("scopedInterface"); + + assertThat(beanBInScope).isNotSameAs(beanAInScope); + + customScope.createNewScope = true; + + Object newBeanAInScope = ctx.getBean("scopedClass"); + Object newBeanBInScope = ctx.getBean("scopedInterface"); + + assertThat(newBeanBInScope).isNotSameAs(newBeanAInScope); + assertThat(beanAInScope).isNotSameAs(newBeanAInScope); + assertThat(beanBInScope).isNotSameAs(newBeanBInScope); + } + + @Test + public void testRawScopes() throws Exception { + String beanName = "scopedProxyInterface"; + + // get hidden bean + Object bean = ctx.getBean("scopedTarget." + beanName); + + boolean condition = bean instanceof ScopedObject; + assertThat(condition).isFalse(); + } + + @Test + public void testScopedProxyConfiguration() throws Exception { + TestBean singleton = (TestBean) ctx.getBean("singletonWithScopedInterfaceDep"); + ITestBean spouse = singleton.getSpouse(); + boolean condition = spouse instanceof ScopedObject; + assertThat(condition).as("scoped bean is not wrapped by the scoped-proxy").isTrue(); + + String beanName = "scopedProxyInterface"; + + String scopedBeanName = "scopedTarget." + beanName; + + // get hidden bean + assertThat(spouse.getName()).isEqualTo(flag); + + ITestBean spouseFromBF = (ITestBean) ctx.getBean(scopedBeanName); + assertThat(spouseFromBF.getName()).isEqualTo(spouse.getName()); + // the scope proxy has kicked in + assertThat(spouseFromBF).isNotSameAs(spouse); + + // create a new bean + customScope.createNewScope = true; + + // get the bean again from the BF + spouseFromBF = (ITestBean) ctx.getBean(scopedBeanName); + // make sure the name has been updated + assertThat(spouseFromBF.getName()).isSameAs(spouse.getName()); + assertThat(spouseFromBF).isNotSameAs(spouse); + + // get the bean again + spouseFromBF = (ITestBean) ctx.getBean(scopedBeanName); + assertThat(spouseFromBF.getName()).isSameAs(spouse.getName()); + } + + @Test + public void testScopedProxyConfigurationWithClasses() throws Exception { + TestBean singleton = (TestBean) ctx.getBean("singletonWithScopedClassDep"); + ITestBean spouse = singleton.getSpouse(); + boolean condition = spouse instanceof ScopedObject; + assertThat(condition).as("scoped bean is not wrapped by the scoped-proxy").isTrue(); + + String beanName = "scopedProxyClass"; + + String scopedBeanName = "scopedTarget." + beanName; + + // get hidden bean + assertThat(spouse.getName()).isEqualTo(flag); + + TestBean spouseFromBF = (TestBean) ctx.getBean(scopedBeanName); + assertThat(spouseFromBF.getName()).isEqualTo(spouse.getName()); + // the scope proxy has kicked in + assertThat(spouseFromBF).isNotSameAs(spouse); + + // create a new bean + customScope.createNewScope = true; + flag = "boo"; + + // get the bean again from the BF + spouseFromBF = (TestBean) ctx.getBean(scopedBeanName); + // make sure the name has been updated + assertThat(spouseFromBF.getName()).isSameAs(spouse.getName()); + assertThat(spouseFromBF).isNotSameAs(spouse); + + // get the bean again + spouseFromBF = (TestBean) ctx.getBean(scopedBeanName); + assertThat(spouseFromBF.getName()).isSameAs(spouse.getName()); + } + + + static class Foo { + + public Foo() { + } + + public void doSomething() { + } + } + + + static class Bar { + + private final Foo foo; + + public Bar(Foo foo) { + this.foo = foo; + } + + public Foo getFoo() { + return foo; + } + } + + + @Configuration + public static class InvalidProxyOnPredefinedScopesConfiguration { + + @Bean @Scope(proxyMode=ScopedProxyMode.INTERFACES) + public Object invalidProxyOnPredefinedScopes() { + return new Object(); + } + } + + + @Configuration + public static class ScopedConfigurationClass { + + @Bean + @MyScope + public TestBean scopedClass() { + TestBean tb = new TestBean(); + tb.setName(flag); + return tb; + } + + @Bean + @MyScope + public ITestBean scopedInterface() { + TestBean tb = new TestBean(); + tb.setName(flag); + return tb; + } + + @Bean + @MyProxiedScope + public ITestBean scopedProxyInterface() { + TestBean tb = new TestBean(); + tb.setName(flag); + return tb; + } + + @MyProxiedScope + public TestBean scopedProxyClass() { + TestBean tb = new TestBean(); + tb.setName(flag); + return tb; + } + + @Bean + public TestBean singletonWithScopedClassDep() { + TestBean singleton = new TestBean(); + singleton.setSpouse(scopedProxyClass()); + return singleton; + } + + @Bean + public TestBean singletonWithScopedInterfaceDep() { + TestBean singleton = new TestBean(); + singleton.setSpouse(scopedProxyInterface()); + return singleton; + } + } + + + @Target({ElementType.METHOD}) + @Retention(RetentionPolicy.RUNTIME) + @Scope(SCOPE) + @interface MyScope { + } + + + @Target({ElementType.METHOD}) + @Retention(RetentionPolicy.RUNTIME) + @Bean + @Scope(value=SCOPE, proxyMode=ScopedProxyMode.TARGET_CLASS) + @interface MyProxiedScope { + } + + + /** + * Simple scope implementation which creates object based on a flag. + * @author Costin Leau + * @author Chris Beams + */ + static class CustomScope implements org.springframework.beans.factory.config.Scope { + + public boolean createNewScope = true; + + private Map beans = new HashMap<>(); + + @Override + public Object get(String name, ObjectFactory objectFactory) { + if (createNewScope) { + beans.clear(); + // reset the flag back + createNewScope = false; + } + + Object bean = beans.get(name); + // if a new object is requested or none exists under the current + // name, create one + if (bean == null) { + beans.put(name, objectFactory.getObject()); + } + + return beans.get(name); + } + + @Override + public String getConversationId() { + return null; + } + + @Override + public void registerDestructionCallback(String name, Runnable callback) { + throw new IllegalStateException("Not supposed to be called"); + } + + @Override + public Object remove(String name) { + return beans.remove(name); + } + + @Override + public Object resolveContextualObject(String key) { + return null; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/Spr10668Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/Spr10668Tests.java new file mode 100644 index 0000000..a8237a1 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/Spr10668Tests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.configuration; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for SPR-10668. + * + * @author Oliver Gierke + * @author Phillip Webb + */ +public class Spr10668Tests { + + @Test + public void testSelfInjectHierarchy() throws Exception { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ChildConfig.class); + assertThat(context.getBean(MyComponent.class)).isNotNull(); + context.close(); + } + + + @Configuration + public static class ParentConfig { + + @Autowired(required = false) + MyComponent component; + } + + + @Configuration + public static class ChildConfig extends ParentConfig { + + @Bean + public MyComponentImpl myComponent() { + return new MyComponentImpl(); + } + } + + + public interface MyComponent {} + + public static class MyComponentImpl implements MyComponent {} + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/Spr10744Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/Spr10744Tests.java new file mode 100644 index 0000000..279d520 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/Spr10744Tests.java @@ -0,0 +1,132 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.configuration; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; + +import static org.assertj.core.api.Assertions.assertThat; + + + + +/** + * @author Phillip Webb + */ +public class Spr10744Tests { + + private static int createCount = 0; + + private static int scopeCount = 0; + + + @Test + public void testSpr10744() throws Exception { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.getBeanFactory().registerScope("myTestScope", new MyTestScope()); + context.register(MyTestConfiguration.class); + context.refresh(); + + Foo bean1 = context.getBean("foo", Foo.class); + Foo bean2 = context.getBean("foo", Foo.class); + assertThat(bean1).isSameAs(bean2); + + // Should not have invoked constructor for the proxy instance + assertThat(createCount).isEqualTo(0); + assertThat(scopeCount).isEqualTo(0); + + // Proxy mode should create new scoped object on each method call + bean1.getMessage(); + assertThat(createCount).isEqualTo(1); + assertThat(scopeCount).isEqualTo(1); + bean1.getMessage(); + assertThat(createCount).isEqualTo(2); + assertThat(scopeCount).isEqualTo(2); + + context.close(); + } + + + private static class MyTestScope implements org.springframework.beans.factory.config.Scope { + + @Override + public Object get(String name, ObjectFactory objectFactory) { + scopeCount++; + return objectFactory.getObject(); + } + + @Override + public Object remove(String name) { + return null; + } + + @Override + public void registerDestructionCallback(String name, Runnable callback) { + } + + @Override + public Object resolveContextualObject(String key) { + return null; + } + + @Override + public String getConversationId() { + return null; + } + } + + + static class Foo { + + public Foo() { + createCount++; + } + + public String getMessage() { + return "Hello"; + } + } + + + @Configuration + static class MyConfiguration { + + @Bean + public Foo foo() { + return new Foo(); + } + } + + + @Configuration + static class MyTestConfiguration extends MyConfiguration { + + @Bean + @Scope(value = "myTestScope", proxyMode = ScopedProxyMode.TARGET_CLASS) + @Override + public Foo foo() { + return new Foo(); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/Spr12526Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/Spr12526Tests.java new file mode 100644 index 0000000..8c069e1 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/Spr12526Tests.java @@ -0,0 +1,156 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.configuration; + +import javax.annotation.Resource; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.beans.factory.config.ConfigurableBeanFactory.SCOPE_PROTOTYPE; +import static org.springframework.beans.factory.config.ConfigurableBeanFactory.SCOPE_SINGLETON; + +/** + * @author Marcin Piela + * @author Juergen Hoeller + */ +class Spr12526Tests { + + @Test + void testInjection() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(TestContext.class); + CustomCondition condition = ctx.getBean(CustomCondition.class); + + condition.setCondition(true); + FirstService firstService = (FirstService) ctx.getBean(Service.class); + assertThat(firstService.getDependency()).as("FirstService.dependency is null").isNotNull(); + + condition.setCondition(false); + SecondService secondService = (SecondService) ctx.getBean(Service.class); + assertThat(secondService.getDependency()).as("SecondService.dependency is null").isNotNull(); + + ctx.close(); + } + + + @Configuration + static class TestContext { + + @Bean + @Scope(SCOPE_SINGLETON) + CustomCondition condition() { + return new CustomCondition(); + } + + @Bean + @Scope(SCOPE_PROTOTYPE) + Service service(CustomCondition condition) { + return (condition.check() ? new FirstService() : new SecondService()); + } + + @Bean + DependencyOne dependencyOne() { + return new DependencyOne(); + } + + @Bean + DependencyTwo dependencyTwo() { + return new DependencyTwo(); + } + } + + + public static class CustomCondition { + + private boolean condition; + + public boolean check() { + return condition; + } + + public void setCondition(boolean value) { + this.condition = value; + } + } + + + public interface Service { + + void doStuff(); + } + + + public static class FirstService implements Service { + + private DependencyOne dependency; + + + @Override + public void doStuff() { + if (dependency == null) { + throw new IllegalStateException("FirstService: dependency is null"); + } + } + + @Resource(name = "dependencyOne") + public void setDependency(DependencyOne dependency) { + this.dependency = dependency; + } + + + public DependencyOne getDependency() { + return dependency; + } + } + + + public static class SecondService implements Service { + + private DependencyTwo dependency; + + @Override + public void doStuff() { + if (dependency == null) { + throw new IllegalStateException("SecondService: dependency is null"); + } + } + + @Resource(name = "dependencyTwo") + public void setDependency(DependencyTwo dependency) { + this.dependency = dependency; + } + + + public DependencyTwo getDependency() { + return dependency; + } + } + + + public static class DependencyOne { + } + + + public static class DependencyTwo { + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/Spr7167Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/Spr7167Tests.java new file mode 100644 index 0000000..ef73432 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/Spr7167Tests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.configuration; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +public class Spr7167Tests { + + @Test + public void test() { + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(MyConfig.class); + + assertThat(ctx.getBeanFactory().getBeanDefinition("someDependency").getDescription()) + .as("someDependency was not post processed") + .isEqualTo("post processed by MyPostProcessor"); + + MyConfig config = ctx.getBean(MyConfig.class); + assertThat(ClassUtils.isCglibProxy(config)).as("Config class was not enhanced").isTrue(); + } + +} + +@Configuration +class MyConfig { + + @Bean + public Dependency someDependency() { + return new Dependency(); + } + + @Bean + public BeanFactoryPostProcessor thePostProcessor() { + return new MyPostProcessor(someDependency()); + } +} + +class Dependency { +} + +class MyPostProcessor implements BeanFactoryPostProcessor { + + public MyPostProcessor(Dependency someDependency) { + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + AbstractBeanDefinition bd = (AbstractBeanDefinition) beanFactory.getBeanDefinition("someDependency"); + bd.setDescription("post processed by MyPostProcessor"); + } +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/a/BaseConfig.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/a/BaseConfig.java new file mode 100644 index 0000000..03fcb83 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/a/BaseConfig.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.configuration.a; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.configuration.PackagePrivateBeanMethodInheritanceTests.Bar; + +public abstract class BaseConfig { + + // ---- reproduce ---- + @Bean + Bar packagePrivateBar() { + return new Bar(); + } + + public Bar reproBar() { + return packagePrivateBar(); + } + + // ---- workaround ---- + @Bean + protected Bar protectedBar() { + return new Bar(); + } + + public Bar workaroundBar() { + return protectedBar(); + } +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr8955/Spr8955Parent.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr8955/Spr8955Parent.java new file mode 100644 index 0000000..bc53b48 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr8955/Spr8955Parent.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.configuration.spr8955; + +import org.springframework.stereotype.Component; + +/** + * @author Chris Beams + * @author Willem Dekker + */ +abstract class Spr8955Parent { + + @Component + static class Spr8955Child extends Spr8955Parent { + + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr8955/Spr8955Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr8955/Spr8955Tests.java new file mode 100644 index 0000000..396dc12 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr8955/Spr8955Tests.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.configuration.spr8955; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +/** + * @author Chris Beams + * @author Willem Dekker + */ +public class Spr8955Tests { + + @Test + public void repro() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.scan("org.springframework.context.annotation.configuration.spr8955"); + ctx.refresh(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr9031/Spr9031Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr9031/Spr9031Tests.java new file mode 100644 index 0000000..74adf34 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr9031/Spr9031Tests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.configuration.spr9031; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.configuration.spr9031.scanpackage.Spr9031Component; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Unit tests cornering bug SPR-9031. + * + * @author Chris Beams + * @since 3.1.1 + */ +public class Spr9031Tests { + + /** + * Use of @Import to register LowLevelConfig results in ASM-based annotation + * processing. + */ + @Test + public void withAsmAnnotationProcessing() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(HighLevelConfig.class); + ctx.refresh(); + assertThat(ctx.getBean(LowLevelConfig.class).scanned).isNotNull(); + } + + /** + * Direct registration of LowLevelConfig results in reflection-based annotation + * processing. + */ + @Test + public void withoutAsmAnnotationProcessing() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(LowLevelConfig.class); + ctx.refresh(); + assertThat(ctx.getBean(LowLevelConfig.class).scanned).isNotNull(); + } + + @Configuration + @Import(LowLevelConfig.class) + static class HighLevelConfig {} + + @Configuration + @ComponentScan( + basePackages = "org.springframework.context.annotation.configuration.spr9031.scanpackage", + includeFilters = { @Filter(MarkerAnnotation.class) }) + static class LowLevelConfig { + // fails to wire when LowLevelConfig is processed with ASM because nested @Filter + // annotation is not parsed + @Autowired Spr9031Component scanned; + } + + @Retention(RetentionPolicy.RUNTIME) + public @interface MarkerAnnotation {} +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr9031/scanpackage/Spr9031Component.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr9031/scanpackage/Spr9031Component.java new file mode 100644 index 0000000..fcf57ef --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/spr9031/scanpackage/Spr9031Component.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.configuration.spr9031.scanpackage; + +import org.springframework.context.annotation.configuration.spr9031.Spr9031Tests.MarkerAnnotation; + +@MarkerAnnotation +public class Spr9031Component { + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/jsr330/SpringAtInjectTckTests.java b/spring-context/src/test/java/org/springframework/context/annotation/jsr330/SpringAtInjectTckTests.java new file mode 100644 index 0000000..4a8ff2b --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/jsr330/SpringAtInjectTckTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.jsr330; + +import junit.framework.Test; +import org.atinject.tck.Tck; +import org.atinject.tck.auto.Car; +import org.atinject.tck.auto.Convertible; +import org.atinject.tck.auto.Drivers; +import org.atinject.tck.auto.DriversSeat; +import org.atinject.tck.auto.FuelTank; +import org.atinject.tck.auto.Seat; +import org.atinject.tck.auto.Tire; +import org.atinject.tck.auto.V8Engine; +import org.atinject.tck.auto.accessories.Cupholder; +import org.atinject.tck.auto.accessories.SpareTire; + +import org.springframework.context.annotation.AnnotatedBeanDefinitionReader; +import org.springframework.context.annotation.Jsr330ScopeMetadataResolver; +import org.springframework.context.annotation.Primary; +import org.springframework.context.support.GenericApplicationContext; + +/** + * @author Juergen Hoeller + * @since 3.0 + */ +public class SpringAtInjectTckTests { + + @SuppressWarnings("unchecked") + public static Test suite() { + GenericApplicationContext ac = new GenericApplicationContext(); + AnnotatedBeanDefinitionReader bdr = new AnnotatedBeanDefinitionReader(ac); + bdr.setScopeMetadataResolver(new Jsr330ScopeMetadataResolver()); + + bdr.registerBean(Convertible.class); + bdr.registerBean(DriversSeat.class, Drivers.class); + bdr.registerBean(Seat.class, Primary.class); + bdr.registerBean(V8Engine.class); + bdr.registerBean(SpareTire.class, "spare"); + bdr.registerBean(Cupholder.class); + bdr.registerBean(Tire.class, Primary.class); + bdr.registerBean(FuelTank.class); + + ac.refresh(); + Car car = ac.getBean(Car.class); + + return Tck.testsFor(car, false, true); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/role/ComponentWithRole.java b/spring-context/src/test/java/org/springframework/context/annotation/role/ComponentWithRole.java new file mode 100644 index 0000000..a4c17ca --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/role/ComponentWithRole.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.role; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Description; +import org.springframework.context.annotation.Role; +import org.springframework.stereotype.Component; + +@Component("componentWithRole") +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +@Description("A Component with a role") +public class ComponentWithRole { +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/role/ComponentWithoutRole.java b/spring-context/src/test/java/org/springframework/context/annotation/role/ComponentWithoutRole.java new file mode 100644 index 0000000..205b6ea --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/role/ComponentWithoutRole.java @@ -0,0 +1,23 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.role; + +import org.springframework.stereotype.Component; + +@Component("componentWithoutRole") +public class ComponentWithoutRole { +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ImportedConfig.java b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ImportedConfig.java new file mode 100644 index 0000000..bdf6cc4 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ImportedConfig.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.spr10546; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * + * @author Rob Winch + */ +@Configuration +public class ImportedConfig { + @Bean + public String myBean() { + return "myBean"; + } +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentConfig.java b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentConfig.java new file mode 100644 index 0000000..84e8ec5 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentConfig.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.spr10546; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * + * @author Rob Winch + */ +@Configuration +public class ParentConfig { + @Bean + public String myBean() { + return "myBean"; + } +} + diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentWithComponentScanConfig.java b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentWithComponentScanConfig.java new file mode 100644 index 0000000..4ce2e03 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentWithComponentScanConfig.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.spr10546; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.spr10546.scanpackage.AEnclosingConfig; + +/** + * + * @author Rob Winch + */ +@Configuration +@ComponentScan(basePackageClasses=AEnclosingConfig.class) +public class ParentWithComponentScanConfig { + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentWithImportConfig.java b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentWithImportConfig.java new file mode 100644 index 0000000..10176d2 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentWithImportConfig.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.spr10546; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * + * @author Rob Winch + */ +@Configuration +@Import(ImportedConfig.class) +public class ParentWithImportConfig { + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentWithImportResourceConfig.java b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentWithImportResourceConfig.java new file mode 100644 index 0000000..80d8baf --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentWithImportResourceConfig.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.spr10546; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportResource; + +/** + * + * @author Rob Winch + */ +@Configuration +@ImportResource("classpath:org/springframework/context/annotation/spr10546/importedResource.xml") +public class ParentWithImportResourceConfig { + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentWithParentConfig.java b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentWithParentConfig.java new file mode 100644 index 0000000..9efbec0 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/ParentWithParentConfig.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.spr10546; + +import org.springframework.context.annotation.Configuration; + +/** + * + * @author Rob Winch + */ +@Configuration +public class ParentWithParentConfig extends ParentConfig { + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr10546/Spr10546Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/Spr10546Tests.java new file mode 100644 index 0000000..c7a32b2 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/Spr10546Tests.java @@ -0,0 +1,149 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.spr10546; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.spr10546.scanpackage.AEnclosingConfig; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * @author Rob Winch + */ +public class Spr10546Tests { + private ConfigurableApplicationContext context; + + @AfterEach + public void closeContext() { + if (context != null) { + context.close(); + } + } + + // These fail prior to fixing SPR-10546 + + @Test + public void enclosingConfigFirstParentDefinesBean() { + assertLoadsMyBean(AEnclosingConfig.class,AEnclosingConfig.ChildConfig.class); + } + + /** + * Prior to fixing SPR-10546 this might have succeeded depending on the ordering the + * classes were picked up. If they are picked up in the same order as + * {@link #enclosingConfigFirstParentDefinesBean()} then it would fail. This test is + * mostly for illustration purposes, but doesn't hurt to continue using it. + * + *

    We purposely use the {@link AEnclosingConfig} to make it alphabetically prior to the + * {@link AEnclosingConfig.ChildConfig} which encourages this to occur with the + * classpath scanning implementation being used by the author of this test. + */ + @Test + public void enclosingConfigFirstParentDefinesBeanWithScanning() { + AnnotationConfigApplicationContext ctx= new AnnotationConfigApplicationContext(); + context = ctx; + ctx.scan(AEnclosingConfig.class.getPackage().getName()); + ctx.refresh(); + assertThat(context.getBean("myBean",String.class)).isEqualTo("myBean"); + } + + @Test + public void enclosingConfigFirstParentDefinesBeanWithImportResource() { + assertLoadsMyBean(AEnclosingWithImportResourceConfig.class,AEnclosingWithImportResourceConfig.ChildConfig.class); + } + + @Configuration + static class AEnclosingWithImportResourceConfig { + @Configuration + public static class ChildConfig extends ParentWithImportResourceConfig {} + } + + @Test + public void enclosingConfigFirstParentDefinesBeanWithComponentScan() { + assertLoadsMyBean(AEnclosingWithComponentScanConfig.class,AEnclosingWithComponentScanConfig.ChildConfig.class); + } + + @Configuration + static class AEnclosingWithComponentScanConfig { + @Configuration + public static class ChildConfig extends ParentWithComponentScanConfig {} + } + + @Test + public void enclosingConfigFirstParentWithParentDefinesBean() { + assertLoadsMyBean(AEnclosingWithGrandparentConfig.class,AEnclosingWithGrandparentConfig.ChildConfig.class); + } + + @Configuration + static class AEnclosingWithGrandparentConfig { + @Configuration + public static class ChildConfig extends ParentWithParentConfig {} + } + + @Test + public void importChildConfigThenChildConfig() { + assertLoadsMyBean(ImportChildConfig.class,ChildConfig.class); + } + + @Configuration + static class ChildConfig extends ParentConfig {} + + @Configuration + @Import(ChildConfig.class) + static class ImportChildConfig {} + + + // These worked prior, but validating they continue to work + + @Test + public void enclosingConfigFirstParentDefinesBeanWithImport() { + assertLoadsMyBean(AEnclosingWithImportConfig.class,AEnclosingWithImportConfig.ChildConfig.class); + } + + @Configuration + static class AEnclosingWithImportConfig { + @Configuration + public static class ChildConfig extends ParentWithImportConfig {} + } + + @Test + public void childConfigFirst() { + assertLoadsMyBean(AEnclosingConfig.ChildConfig.class, AEnclosingConfig.class); + } + + @Test + public void enclosingConfigOnly() { + assertLoadsMyBean(AEnclosingConfig.class); + } + + @Test + public void childConfigOnly() { + assertLoadsMyBean(AEnclosingConfig.ChildConfig.class); + } + + private void assertLoadsMyBean(Class... annotatedClasses) { + context = new AnnotationConfigApplicationContext(annotatedClasses); + assertThat(context.getBean("myBean",String.class)).isEqualTo("myBean"); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr10546/scanpackage/AEnclosingConfig.java b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/scanpackage/AEnclosingConfig.java new file mode 100644 index 0000000..1d7b858 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr10546/scanpackage/AEnclosingConfig.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.spr10546.scanpackage; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.spr10546.ParentConfig; + +/** + * Note the name of {@link AEnclosingConfig} is chosen to help ensure scanning picks up + * the enclosing configuration prior to {@link ChildConfig} to demonstrate this can happen + * with classpath scanning. + * + * @author Rob Winch + */ +@Configuration +public class AEnclosingConfig { + @Configuration + public static class ChildConfig extends ParentConfig {} +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr12111/TestProfileBean.java b/spring-context/src/test/java/org/springframework/context/annotation/spr12111/TestProfileBean.java new file mode 100644 index 0000000..b4fc752 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr12111/TestProfileBean.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.spr12111; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Component +@Profile("test") +public class TestProfileBean { +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr12233/Spr12233Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/spr12233/Spr12233Tests.java new file mode 100644 index 0000000..d9b8044 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr12233/Spr12233Tests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.spr12233; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ConfigurationCondition; +import org.springframework.context.annotation.Import; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.core.type.AnnotatedTypeMetadata; + + +/** + * Tests cornering the regression reported in SPR-12233. + * + * @author Phillip Webb + */ +public class Spr12233Tests { + + @Test + public void spr12233() throws Exception { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(PropertySourcesPlaceholderConfigurer.class); + ctx.register(ImportConfiguration.class); + ctx.refresh(); + ctx.close(); + } + + static class NeverConfigurationCondition implements ConfigurationCondition { + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + return false; + } + + @Override + public ConfigurationPhase getConfigurationPhase() { + return ConfigurationPhase.REGISTER_BEAN; + } + } + + @Import(ComponentScanningConfiguration.class) + static class ImportConfiguration { + + } + + @Configuration + @ComponentScan + static class ComponentScanningConfiguration { + + } + + + @Configuration + @Conditional(NeverConfigurationCondition.class) + static class ConditionWithPropertyValueInjection { + + @Value("${idontexist}") + private String property; + } +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr12334/Spr12334Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/spr12334/Spr12334Tests.java new file mode 100644 index 0000000..079b2a8 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr12334/Spr12334Tests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.spr12334; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.type.AnnotationMetadata; + +/** + * @author Juergen Hoeller + * @author Alex Pogrebnyak + */ +public class Spr12334Tests { + + @Test + public void shouldNotScanTwice() { + TestImport.scanned = false; + + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.scan(TestImport.class.getPackage().getName()); + context.refresh(); + context.getBean(TestConfiguration.class); + } + + + @Import(TestImport.class) + public @interface AnotherImport { + } + + + @Configuration + @AnotherImport + public static class TestConfiguration { + } + + + public static class TestImport implements ImportBeanDefinitionRegistrar { + + private static boolean scanned = false; + + @Override + public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { + if (scanned) { + throw new IllegalStateException("Already scanned"); + } + scanned = true; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr16756/ScannedComponent.java b/spring-context/src/test/java/org/springframework/context/annotation/spr16756/ScannedComponent.java new file mode 100644 index 0000000..5aae3af --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr16756/ScannedComponent.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.spr16756; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.stereotype.Component; + +@Component +public class ScannedComponent { + + @Autowired + private State state; + + public String iDoAnything() { + return state.anyMethod(); + } + + + public interface State { + + String anyMethod(); + } + + + @Component + @Scope(proxyMode = ScopedProxyMode.INTERFACES, value = "prototype") + public static class StateImpl implements State { + + @Override + public String anyMethod() { + return "anyMethod called"; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr16756/ScanningConfiguration.java b/spring-context/src/test/java/org/springframework/context/annotation/spr16756/ScanningConfiguration.java new file mode 100644 index 0000000..229e8eb --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr16756/ScanningConfiguration.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.spr16756; + +import org.springframework.context.annotation.ComponentScan; + +@ComponentScan +public class ScanningConfiguration { + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr16756/Spr16756Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/spr16756/Spr16756Tests.java new file mode 100644 index 0000000..54e3d4e --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr16756/Spr16756Tests.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.spr16756; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +/** + * @author Juergen Hoeller + */ +public class Spr16756Tests { + + @Test + public void shouldNotFailOnNestedScopedComponent() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(ScanningConfiguration.class); + context.refresh(); + context.getBean(ScannedComponent.class); + context.getBean(ScannedComponent.State.class); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr8761/Spr8761Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/spr8761/Spr8761Tests.java new file mode 100644 index 0000000..946b35e --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr8761/Spr8761Tests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.spr8761; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.stereotype.Component; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Tests cornering the regression reported in SPR-8761. + * + * @author Chris Beams + */ +public class Spr8761Tests { + + /** + * Prior to the fix for SPR-8761, this test threw because the nested MyComponent + * annotation was being falsely considered as a 'lite' Configuration class candidate. + */ + @Test + public void repro() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.scan(getClass().getPackage().getName()); + ctx.refresh(); + assertThat(ctx.containsBean("withNestedAnnotation")).isTrue(); + } + +} + +@Component +class WithNestedAnnotation { + + @Retention(RetentionPolicy.RUNTIME) + @Component + public static @interface MyComponent { + } +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/spr8808/Spr8808Tests.java b/spring-context/src/test/java/org/springframework/context/annotation/spr8808/Spr8808Tests.java new file mode 100644 index 0000000..ad71604 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/spr8808/Spr8808Tests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2011 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation.spr8808; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +/** + * Tests cornering the bug in which @Configuration classes that @ComponentScan themselves + * would result in a ConflictingBeanDefinitionException. + * + * @author Chris Beams + * @since 3.1 + */ +public class Spr8808Tests { + + /** + * This test failed with ConflictingBeanDefinitionException prior to fixes for + * SPR-8808. + */ + @Test + public void repro() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(Config.class); + ctx.refresh(); + } + +} + +@Configuration +@ComponentScan(basePackageClasses=Spr8808Tests.class) // scan *this* package +class Config { +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation2/NamedStubDao2.java b/spring-context/src/test/java/org/springframework/context/annotation2/NamedStubDao2.java new file mode 100644 index 0000000..2296003 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation2/NamedStubDao2.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2008 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation2; + +import org.springframework.stereotype.Repository; + +/** + * @author Juergen Hoeller + */ +@Repository("myNamedDao") +public class NamedStubDao2 { + + public String find(int id) { + return "bar"; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation3/StubFooDao.java b/spring-context/src/test/java/org/springframework/context/annotation3/StubFooDao.java new file mode 100644 index 0000000..2aae57c --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation3/StubFooDao.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation3; + +import example.scannable.FooDao; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Repository; + +/** + * @author Mark Fisher + */ +@Repository +@Qualifier("testing") +public class StubFooDao implements FooDao { + + @Override + public String findFoo(int id) { + return "bar"; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation4/DependencyBean.java b/spring-context/src/test/java/org/springframework/context/annotation4/DependencyBean.java new file mode 100644 index 0000000..38abca5 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation4/DependencyBean.java @@ -0,0 +1,27 @@ +/* + * Copyright 2002-2009 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation4; + +import org.springframework.stereotype.Component; + +/** + * @author Juergen Hoeller + */ +@Component +public class DependencyBean { + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation4/FactoryMethodComponent.java b/spring-context/src/test/java/org/springframework/context/annotation4/FactoryMethodComponent.java new file mode 100644 index 0000000..1e751e3 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation4/FactoryMethodComponent.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation4; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.BeanAge; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.stereotype.Component; + +/** + * Class used to test the functionality of factory method bean definitions + * declared inside a Spring component class. + * + * @author Mark Pollack + * @author Juergen Hoeller + */ +@Component +public class FactoryMethodComponent { + + private int i; + + public static TestBean nullInstance() { + return null; + } + + @Bean @Qualifier("public") + public TestBean publicInstance() { + return new TestBean("publicInstance"); + } + + // to be ignored + public TestBean publicInstance(boolean doIt) { + return new TestBean("publicInstance"); + } + + @Bean @BeanAge(1) + protected TestBean protectedInstance(@Qualifier("public") TestBean spouse, @Value("#{privateInstance.age}") String country) { + TestBean tb = new TestBean("protectedInstance", 1); + tb.setSpouse(tb); + tb.setCountry(country); + return tb; + } + + @Bean @Scope("prototype") + private TestBean privateInstance() { + return new TestBean("privateInstance", i++); + } + + @Bean @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) + public TestBean requestScopedInstance() { + return new TestBean("requestScopedInstance", 3); + } + + @Bean + public DependencyBean secondInstance() { + return new DependencyBean(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation4/SimpleBean.java b/spring-context/src/test/java/org/springframework/context/annotation4/SimpleBean.java new file mode 100644 index 0000000..69b5d94 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation4/SimpleBean.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation4; + +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.annotation.Bean; + +/** + * Class to test that @FactoryMethods are detected only when inside a class with an @Component + * class annotation. + * + * @author Mark Pollack + */ +public class SimpleBean { + + // This should *not* recognized as a bean since it does not reside inside an @Component + @Bean + public TestBean getPublicInstance() { + return new TestBean("publicInstance"); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation5/MyRepository.java b/spring-context/src/test/java/org/springframework/context/annotation5/MyRepository.java new file mode 100644 index 0000000..25e8fda --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation5/MyRepository.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2009 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation5; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Lazy; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Repository; + +/** + * @author Juergen Hoeller + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Repository +@Primary +@Lazy +public @interface MyRepository { +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation5/OtherFooDao.java b/spring-context/src/test/java/org/springframework/context/annotation5/OtherFooDao.java new file mode 100644 index 0000000..8ded77f --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation5/OtherFooDao.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation5; + +import example.scannable.FooDao; + +/** + * @author Juergen Hoeller + */ +@MyRepository +public class OtherFooDao implements FooDao { + + @Override + public String findFoo(int id) { + return "other"; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation6/ComponentForScanning.java b/spring-context/src/test/java/org/springframework/context/annotation6/ComponentForScanning.java new file mode 100644 index 0000000..167ec8e --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation6/ComponentForScanning.java @@ -0,0 +1,23 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation6; + +import org.springframework.stereotype.Component; + +@Component +public class ComponentForScanning { +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation6/ConfigForScanning.java b/spring-context/src/test/java/org/springframework/context/annotation6/ConfigForScanning.java new file mode 100644 index 0000000..a95eafc --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation6/ConfigForScanning.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation6; + +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ConfigForScanning { + @Bean + public TestBean testBean() { + return new TestBean(); + } +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation6/Jsr330NamedForScanning.java b/spring-context/src/test/java/org/springframework/context/annotation6/Jsr330NamedForScanning.java new file mode 100644 index 0000000..d474a99 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation6/Jsr330NamedForScanning.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation6; + +import javax.inject.Named; + +@Named +public class Jsr330NamedForScanning { + +} diff --git a/spring-context/src/test/java/org/springframework/context/config/ContextNamespaceHandlerTests.java b/spring-context/src/test/java/org/springframework/context/config/ContextNamespaceHandlerTests.java new file mode 100644 index 0000000..c7e6f17 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/config/ContextNamespaceHandlerTests.java @@ -0,0 +1,155 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.config; + +import java.util.Calendar; +import java.util.Date; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.FatalBeanException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.context.support.GenericXmlApplicationContext; +import org.springframework.core.io.ClassPathResource; +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Arjen Poutsma + * @author Dave Syer + * @author Chris Beams + * @author Juergen Hoeller + * @since 2.5.6 + */ +public class ContextNamespaceHandlerTests { + + @AfterEach + public void tearDown() { + System.getProperties().remove("foo"); + } + + + @Test + public void propertyPlaceholder() { + ApplicationContext applicationContext = new ClassPathXmlApplicationContext( + "contextNamespaceHandlerTests-replace.xml", getClass()); + assertThat(applicationContext.getBean("string")).isEqualTo("bar"); + assertThat(applicationContext.getBean("nullString")).isEqualTo("null"); + } + + @Test + public void propertyPlaceholderSystemProperties() { + String value = System.setProperty("foo", "spam"); + try { + ApplicationContext applicationContext = new ClassPathXmlApplicationContext( + "contextNamespaceHandlerTests-system.xml", getClass()); + assertThat(applicationContext.getBean("string")).isEqualTo("spam"); + assertThat(applicationContext.getBean("fallback")).isEqualTo("none"); + } + finally { + if (value != null) { + System.setProperty("foo", value); + } + } + } + + @Test + public void propertyPlaceholderEnvironmentProperties() { + MockEnvironment env = new MockEnvironment().withProperty("foo", "spam"); + GenericXmlApplicationContext applicationContext = new GenericXmlApplicationContext(); + applicationContext.setEnvironment(env); + applicationContext.load(new ClassPathResource("contextNamespaceHandlerTests-simple.xml", getClass())); + applicationContext.refresh(); + assertThat(applicationContext.getBean("string")).isEqualTo("spam"); + assertThat(applicationContext.getBean("fallback")).isEqualTo("none"); + } + + @Test + public void propertyPlaceholderLocation() { + ApplicationContext applicationContext = new ClassPathXmlApplicationContext( + "contextNamespaceHandlerTests-location.xml", getClass()); + assertThat(applicationContext.getBean("foo")).isEqualTo("bar"); + assertThat(applicationContext.getBean("bar")).isEqualTo("foo"); + assertThat(applicationContext.getBean("spam")).isEqualTo("maps"); + } + + @Test + public void propertyPlaceholderLocationWithSystemPropertyForOneLocation() { + System.setProperty("properties", + "classpath*:/org/springframework/context/config/test-*.properties"); + try { + ApplicationContext applicationContext = new ClassPathXmlApplicationContext( + "contextNamespaceHandlerTests-location-placeholder.xml", getClass()); + assertThat(applicationContext.getBean("foo")).isEqualTo("bar"); + assertThat(applicationContext.getBean("bar")).isEqualTo("foo"); + assertThat(applicationContext.getBean("spam")).isEqualTo("maps"); + } + finally { + System.clearProperty("properties"); + } + } + + @Test + public void propertyPlaceholderLocationWithSystemPropertyForMultipleLocations() { + System.setProperty("properties", + "classpath*:/org/springframework/context/config/test-*.properties," + + "classpath*:/org/springframework/context/config/empty-*.properties," + + "classpath*:/org/springframework/context/config/missing-*.properties"); + try { + ApplicationContext applicationContext = new ClassPathXmlApplicationContext( + "contextNamespaceHandlerTests-location-placeholder.xml", getClass()); + assertThat(applicationContext.getBean("foo")).isEqualTo("bar"); + assertThat(applicationContext.getBean("bar")).isEqualTo("foo"); + assertThat(applicationContext.getBean("spam")).isEqualTo("maps"); + } + finally { + System.clearProperty("properties"); + } + } + + @Test + public void propertyPlaceholderLocationWithSystemPropertyMissing() { + assertThatExceptionOfType(FatalBeanException.class).isThrownBy(() -> + new ClassPathXmlApplicationContext("contextNamespaceHandlerTests-location-placeholder.xml", getClass())) + .havingRootCause() + .isInstanceOf(IllegalArgumentException.class) + .withMessage("Could not resolve placeholder 'foo' in value \"${foo}\""); + } + + @Test + public void propertyPlaceholderIgnored() { + ApplicationContext applicationContext = new ClassPathXmlApplicationContext( + "contextNamespaceHandlerTests-replace-ignore.xml", getClass()); + assertThat(applicationContext.getBean("string")).isEqualTo("${bar}"); + assertThat(applicationContext.getBean("nullString")).isEqualTo("null"); + } + + @Test + public void propertyOverride() { + ApplicationContext applicationContext = new ClassPathXmlApplicationContext( + "contextNamespaceHandlerTests-override.xml", getClass()); + Date date = (Date) applicationContext.getBean("date"); + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + assertThat(calendar.get(Calendar.MINUTE)).isEqualTo(42); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/conversionservice/Bar.java b/spring-context/src/test/java/org/springframework/context/conversionservice/Bar.java new file mode 100644 index 0000000..772dba1 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/conversionservice/Bar.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.conversionservice; + +/** + * @author Keith Donald + */ +public class Bar { + + private String value; + + public Bar(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/conversionservice/ConversionServiceContextConfigTests.java b/spring-context/src/test/java/org/springframework/context/conversionservice/ConversionServiceContextConfigTests.java new file mode 100644 index 0000000..e4e0883 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/conversionservice/ConversionServiceContextConfigTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.conversionservice; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Keith Donald + */ +public class ConversionServiceContextConfigTests { + + @Test + public void testConfigOk() { + ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("org/springframework/context/conversionservice/conversionService.xml"); + TestClient client = context.getBean("testClient", TestClient.class); + assertThat(client.getBars().size()).isEqualTo(2); + assertThat(client.getBars().get(0).getValue()).isEqualTo("value1"); + assertThat(client.getBars().get(1).getValue()).isEqualTo("value2"); + assertThat(client.isBool()).isTrue(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/conversionservice/StringToBarConverter.java b/spring-context/src/test/java/org/springframework/context/conversionservice/StringToBarConverter.java new file mode 100644 index 0000000..747d593 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/conversionservice/StringToBarConverter.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.conversionservice; + +import org.springframework.core.convert.converter.Converter; + +/** + * @author Keith Donald + */ +public class StringToBarConverter implements Converter { + + @Override + public Bar convert(String source) { + return new Bar(source); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/conversionservice/TestClient.java b/spring-context/src/test/java/org/springframework/context/conversionservice/TestClient.java new file mode 100644 index 0000000..01fe855 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/conversionservice/TestClient.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2006 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.conversionservice; + +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.Resource; + +/** + * @author Keith Donald + * @author Juergen Hoeller + */ +public class TestClient { + + private List bars; + + private boolean bool; + + private List stringList; + + private Resource[] resourceArray; + + private List resourceList; + + private Map resourceMap; + + + public List getBars() { + return bars; + } + + @Autowired + public void setBars(List bars) { + this.bars = bars; + } + + public boolean isBool() { + return bool; + } + + public void setBool(boolean bool) { + this.bool = bool; + } + + public List getStringList() { + return stringList; + } + + public void setStringList(List stringList) { + this.stringList = stringList; + } + + public Resource[] getResourceArray() { + return resourceArray; + } + + public void setResourceArray(Resource[] resourceArray) { + this.resourceArray = resourceArray; + } + + public List getResourceList() { + return resourceList; + } + + public void setResourceList(List resourceList) { + this.resourceList = resourceList; + } + + public Map getResourceMap() { + return resourceMap; + } + + public void setResourceMap(Map resourceMap) { + this.resourceMap = resourceMap; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/event/AbstractApplicationEventListenerTests.java b/spring-context/src/test/java/org/springframework/context/event/AbstractApplicationEventListenerTests.java new file mode 100644 index 0000000..bf4360e --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/AbstractApplicationEventListenerTests.java @@ -0,0 +1,130 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event; + +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.core.ResolvableType; +import org.springframework.core.ResolvableTypeProvider; + +/** + * @author Stephane Nicoll + */ +@SuppressWarnings("serial") +public abstract class AbstractApplicationEventListenerTests { + + protected ResolvableType getGenericApplicationEventType(String fieldName) { + try { + return ResolvableType.forField(TestEvents.class.getField(fieldName)); + } + catch (NoSuchFieldException ex) { + throw new IllegalStateException("No such field on Events '" + fieldName + "'"); + } + } + + + protected static class GenericTestEvent extends ApplicationEvent { + + private final T payload; + + public GenericTestEvent(Object source, T payload) { + super(source); + this.payload = payload; + } + + public T getPayload() { + return this.payload; + } + } + + protected static class SmartGenericTestEvent extends GenericTestEvent implements ResolvableTypeProvider { + + private final ResolvableType resolvableType; + + public SmartGenericTestEvent(Object source, T payload) { + super(source, payload); + this.resolvableType = ResolvableType.forClassWithGenerics( + getClass(), payload.getClass()); + } + + @Override + public ResolvableType getResolvableType() { + return this.resolvableType; + } + } + + protected static class StringEvent extends GenericTestEvent { + + public StringEvent(Object source, String payload) { + super(source, payload); + } + } + + protected static class LongEvent extends GenericTestEvent { + + public LongEvent(Object source, Long payload) { + super(source, payload); + } + } + + protected GenericTestEvent createGenericTestEvent(T payload) { + return new GenericTestEvent<>(this, payload); + } + + + static class GenericEventListener implements ApplicationListener> { + @Override + public void onApplicationEvent(GenericTestEvent event) { + } + } + + static class ObjectEventListener implements ApplicationListener> { + @Override + public void onApplicationEvent(GenericTestEvent event) { + } + } + + static class UpperBoundEventListener + implements ApplicationListener> { + + @Override + public void onApplicationEvent(GenericTestEvent event) { + } + } + + static class StringEventListener implements ApplicationListener> { + + @Override + public void onApplicationEvent(GenericTestEvent event) { + } + } + + @SuppressWarnings("rawtypes") + static class RawApplicationListener implements ApplicationListener { + + @Override + public void onApplicationEvent(ApplicationEvent event) { + } + } + + static class TestEvents { + + public GenericTestEvent wildcardEvent; + + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java b/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java new file mode 100644 index 0000000..95ead7c --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java @@ -0,0 +1,1120 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.aop.framework.Advised; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.PayloadApplicationEvent; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.context.event.test.AbstractIdentifiable; +import org.springframework.context.event.test.AnotherTestEvent; +import org.springframework.context.event.test.EventCollector; +import org.springframework.context.event.test.GenericEventPojo; +import org.springframework.context.event.test.Identifiable; +import org.springframework.context.event.test.TestEvent; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.core.annotation.AliasFor; +import org.springframework.core.annotation.Order; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.util.concurrent.SettableListenableFuture; +import org.springframework.validation.annotation.Validated; +import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * @author Stephane Nicoll + * @author Juergen Hoeller + */ +public class AnnotationDrivenEventListenerTests { + + private ConfigurableApplicationContext context; + + private EventCollector eventCollector; + + private CountDownLatch countDownLatch; // 1 call by default + + + @AfterEach + public void closeContext() { + if (this.context != null) { + this.context.close(); + } + } + + + @Test + public void simpleEventJavaConfig() { + load(TestEventListener.class); + TestEvent event = new TestEvent(this, "test"); + TestEventListener listener = this.context.getBean(TestEventListener.class); + + this.eventCollector.assertNoEventReceived(listener); + this.context.publishEvent(event); + this.eventCollector.assertEvent(listener, event); + this.eventCollector.assertTotalEventsCount(1); + + this.eventCollector.clear(); + this.context.publishEvent(event); + this.eventCollector.assertEvent(listener, event); + this.eventCollector.assertTotalEventsCount(1); + } + + @Test + public void simpleEventXmlConfig() { + this.context = new ClassPathXmlApplicationContext( + "org/springframework/context/event/simple-event-configuration.xml"); + + TestEvent event = new TestEvent(this, "test"); + TestEventListener listener = this.context.getBean(TestEventListener.class); + this.eventCollector = getEventCollector(this.context); + + this.eventCollector.assertNoEventReceived(listener); + this.context.publishEvent(event); + this.eventCollector.assertEvent(listener, event); + this.eventCollector.assertTotalEventsCount(1); + } + + @Test + public void metaAnnotationIsDiscovered() { + load(MetaAnnotationListenerTestBean.class); + MetaAnnotationListenerTestBean bean = this.context.getBean(MetaAnnotationListenerTestBean.class); + this.eventCollector.assertNoEventReceived(bean); + + TestEvent event = new TestEvent(); + this.context.publishEvent(event); + this.eventCollector.assertEvent(bean, event); + this.eventCollector.assertTotalEventsCount(1); + } + + @Test + public void contextEventsAreReceived() { + load(ContextEventListener.class); + ContextEventListener listener = this.context.getBean(ContextEventListener.class); + + List events = this.eventCollector.getEvents(listener); + assertThat(events.size()).as("Wrong number of initial context events").isEqualTo(1); + assertThat(events.get(0).getClass()).isEqualTo(ContextRefreshedEvent.class); + + this.context.stop(); + List eventsAfterStop = this.eventCollector.getEvents(listener); + assertThat(eventsAfterStop.size()).as("Wrong number of context events on shutdown").isEqualTo(2); + assertThat(eventsAfterStop.get(1).getClass()).isEqualTo(ContextStoppedEvent.class); + this.eventCollector.assertTotalEventsCount(2); + } + + @Test + public void methodSignatureNoEvent() { + @SuppressWarnings("resource") + AnnotationConfigApplicationContext failingContext = + new AnnotationConfigApplicationContext(); + failingContext.register(BasicConfiguration.class, + InvalidMethodSignatureEventListener.class); + + assertThatExceptionOfType(BeanInitializationException.class).isThrownBy(() -> + failingContext.refresh()) + .withMessageContaining(InvalidMethodSignatureEventListener.class.getName()) + .withMessageContaining("cannotBeCalled"); + } + + @Test + public void simpleReply() { + load(TestEventListener.class, ReplyEventListener.class); + AnotherTestEvent event = new AnotherTestEvent(this, "dummy"); + ReplyEventListener replyEventListener = this.context.getBean(ReplyEventListener.class); + TestEventListener listener = this.context.getBean(TestEventListener.class); + + this.eventCollector.assertNoEventReceived(listener); + this.eventCollector.assertNoEventReceived(replyEventListener); + this.context.publishEvent(event); + this.eventCollector.assertEvent(replyEventListener, event); + this.eventCollector.assertEvent(listener, new TestEvent(replyEventListener, event.getId(), "dummy")); // reply + this.eventCollector.assertTotalEventsCount(2); + } + + @Test + public void nullReplyIgnored() { + load(TestEventListener.class, ReplyEventListener.class); + AnotherTestEvent event = new AnotherTestEvent(this, null); // No response + ReplyEventListener replyEventListener = this.context.getBean(ReplyEventListener.class); + TestEventListener listener = this.context.getBean(TestEventListener.class); + + this.eventCollector.assertNoEventReceived(listener); + this.eventCollector.assertNoEventReceived(replyEventListener); + this.context.publishEvent(event); + this.eventCollector.assertEvent(replyEventListener, event); + this.eventCollector.assertNoEventReceived(listener); + this.eventCollector.assertTotalEventsCount(1); + } + + @Test + public void arrayReply() { + load(TestEventListener.class, ReplyEventListener.class); + AnotherTestEvent event = new AnotherTestEvent(this, new String[]{"first", "second"}); + ReplyEventListener replyEventListener = this.context.getBean(ReplyEventListener.class); + TestEventListener listener = this.context.getBean(TestEventListener.class); + + this.eventCollector.assertNoEventReceived(listener); + this.eventCollector.assertNoEventReceived(replyEventListener); + this.context.publishEvent(event); + this.eventCollector.assertEvent(replyEventListener, event); + this.eventCollector.assertEvent(listener, "first", "second"); // reply + this.eventCollector.assertTotalEventsCount(3); + } + + @Test + public void collectionReply() { + load(TestEventListener.class, ReplyEventListener.class); + Set replies = new LinkedHashSet<>(); + replies.add("first"); + replies.add(4L); + replies.add("third"); + AnotherTestEvent event = new AnotherTestEvent(this, replies); + ReplyEventListener replyEventListener = this.context.getBean(ReplyEventListener.class); + TestEventListener listener = this.context.getBean(TestEventListener.class); + + this.eventCollector.assertNoEventReceived(listener); + this.eventCollector.assertNoEventReceived(replyEventListener); + this.context.publishEvent(event); + this.eventCollector.assertEvent(replyEventListener, event); + this.eventCollector.assertEvent(listener, "first", "third"); // reply (no listener for 4L) + this.eventCollector.assertTotalEventsCount(3); + } + + @Test + public void collectionReplyNullValue() { + load(TestEventListener.class, ReplyEventListener.class); + AnotherTestEvent event = new AnotherTestEvent(this, Arrays.asList(null, "test")); + ReplyEventListener replyEventListener = this.context.getBean(ReplyEventListener.class); + TestEventListener listener = this.context.getBean(TestEventListener.class); + + this.eventCollector.assertNoEventReceived(listener); + this.eventCollector.assertNoEventReceived(replyEventListener); + this.context.publishEvent(event); + this.eventCollector.assertEvent(replyEventListener, event); + this.eventCollector.assertEvent(listener, "test"); + this.eventCollector.assertTotalEventsCount(2); + } + + @Test + public void listenableFutureReply() { + load(TestEventListener.class, ReplyEventListener.class); + SettableListenableFuture future = new SettableListenableFuture<>(); + future.set("dummy"); + AnotherTestEvent event = new AnotherTestEvent(this, future); + ReplyEventListener replyEventListener = this.context.getBean(ReplyEventListener.class); + TestEventListener listener = this.context.getBean(TestEventListener.class); + + this.eventCollector.assertNoEventReceived(listener); + this.eventCollector.assertNoEventReceived(replyEventListener); + this.context.publishEvent(event); + this.eventCollector.assertEvent(replyEventListener, event); + this.eventCollector.assertEvent(listener, "dummy"); // reply + this.eventCollector.assertTotalEventsCount(2); + } + + @Test + public void completableFutureReply() { + load(TestEventListener.class, ReplyEventListener.class); + AnotherTestEvent event = new AnotherTestEvent(this, CompletableFuture.completedFuture("dummy")); + ReplyEventListener replyEventListener = this.context.getBean(ReplyEventListener.class); + TestEventListener listener = this.context.getBean(TestEventListener.class); + + this.eventCollector.assertNoEventReceived(listener); + this.eventCollector.assertNoEventReceived(replyEventListener); + this.context.publishEvent(event); + this.eventCollector.assertEvent(replyEventListener, event); + this.eventCollector.assertEvent(listener, "dummy"); // reply + this.eventCollector.assertTotalEventsCount(2); + } + + @Test + public void monoReply() { + load(TestEventListener.class, ReplyEventListener.class); + AnotherTestEvent event = new AnotherTestEvent(this, Mono.just("dummy")); + ReplyEventListener replyEventListener = this.context.getBean(ReplyEventListener.class); + TestEventListener listener = this.context.getBean(TestEventListener.class); + + this.eventCollector.assertNoEventReceived(listener); + this.eventCollector.assertNoEventReceived(replyEventListener); + this.context.publishEvent(event); + this.eventCollector.assertEvent(replyEventListener, event); + this.eventCollector.assertEvent(listener, "dummy"); // reply + this.eventCollector.assertTotalEventsCount(2); + } + + @Test + public void fluxReply() { + load(TestEventListener.class, ReplyEventListener.class); + AnotherTestEvent event = new AnotherTestEvent(this, Flux.just("dummy1", "dummy2")); + ReplyEventListener replyEventListener = this.context.getBean(ReplyEventListener.class); + TestEventListener listener = this.context.getBean(TestEventListener.class); + + this.eventCollector.assertNoEventReceived(listener); + this.eventCollector.assertNoEventReceived(replyEventListener); + this.context.publishEvent(event); + this.eventCollector.assertEvent(replyEventListener, event); + this.eventCollector.assertEvent(listener, "dummy1", "dummy2"); // reply + this.eventCollector.assertTotalEventsCount(3); + } + + @Test + public void eventListenerWorksWithSimpleInterfaceProxy() { + load(ScopedProxyTestBean.class); + + SimpleService proxy = this.context.getBean(SimpleService.class); + assertThat(proxy instanceof Advised).as("bean should be a proxy").isTrue(); + this.eventCollector.assertNoEventReceived(proxy.getId()); + + this.context.publishEvent(new ContextRefreshedEvent(this.context)); + this.eventCollector.assertNoEventReceived(proxy.getId()); + + TestEvent event = new TestEvent(); + this.context.publishEvent(event); + this.eventCollector.assertEvent(proxy.getId(), event); + this.eventCollector.assertTotalEventsCount(1); + } + + @Test + public void eventListenerWorksWithAnnotatedInterfaceProxy() { + load(AnnotatedProxyTestBean.class); + + AnnotatedSimpleService proxy = this.context.getBean(AnnotatedSimpleService.class); + assertThat(proxy instanceof Advised).as("bean should be a proxy").isTrue(); + this.eventCollector.assertNoEventReceived(proxy.getId()); + + this.context.publishEvent(new ContextRefreshedEvent(this.context)); + this.eventCollector.assertNoEventReceived(proxy.getId()); + + TestEvent event = new TestEvent(); + this.context.publishEvent(event); + this.eventCollector.assertEvent(proxy.getId(), event); + this.eventCollector.assertTotalEventsCount(1); + } + + @Test + public void eventListenerWorksWithCglibProxy() { + load(CglibProxyTestBean.class); + + CglibProxyTestBean proxy = this.context.getBean(CglibProxyTestBean.class); + assertThat(AopUtils.isCglibProxy(proxy)).as("bean should be a cglib proxy").isTrue(); + this.eventCollector.assertNoEventReceived(proxy.getId()); + + this.context.publishEvent(new ContextRefreshedEvent(this.context)); + this.eventCollector.assertNoEventReceived(proxy.getId()); + + TestEvent event = new TestEvent(); + this.context.publishEvent(event); + this.eventCollector.assertEvent(proxy.getId(), event); + this.eventCollector.assertTotalEventsCount(1); + } + + @Test + public void privateMethodOnCglibProxyFails() { + assertThatExceptionOfType(BeanInitializationException.class).isThrownBy(() -> + load(CglibProxyWithPrivateMethod.class)) + .withCauseInstanceOf(IllegalStateException.class); + } + + @Test + public void eventListenerWorksWithCustomScope() { + load(CustomScopeTestBean.class); + CustomScope customScope = new CustomScope(); + this.context.getBeanFactory().registerScope("custom", customScope); + + CustomScopeTestBean proxy = this.context.getBean(CustomScopeTestBean.class); + assertThat(AopUtils.isCglibProxy(proxy)).as("bean should be a cglib proxy").isTrue(); + this.eventCollector.assertNoEventReceived(proxy.getId()); + + this.context.publishEvent(new ContextRefreshedEvent(this.context)); + this.eventCollector.assertNoEventReceived(proxy.getId()); + + customScope.active = false; + this.context.publishEvent(new ContextRefreshedEvent(this.context)); + customScope.active = true; + this.eventCollector.assertNoEventReceived(proxy.getId()); + + TestEvent event = new TestEvent(); + this.context.publishEvent(event); + this.eventCollector.assertEvent(proxy.getId(), event); + this.eventCollector.assertTotalEventsCount(1); + + customScope.active = false; + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + this.context.publishEvent(new TestEvent())) + .withCauseInstanceOf(IllegalStateException.class); + } + + @Test + public void asyncProcessingApplied() throws InterruptedException { + loadAsync(AsyncEventListener.class); + + String threadName = Thread.currentThread().getName(); + AnotherTestEvent event = new AnotherTestEvent(this, threadName); + AsyncEventListener listener = this.context.getBean(AsyncEventListener.class); + this.eventCollector.assertNoEventReceived(listener); + + this.context.publishEvent(event); + this.countDownLatch.await(2, TimeUnit.SECONDS); + this.eventCollector.assertEvent(listener, event); + this.eventCollector.assertTotalEventsCount(1); + } + + @Test + public void asyncProcessingAppliedWithInterfaceProxy() throws InterruptedException { + doLoad(AsyncConfigurationWithInterfaces.class, SimpleProxyTestBean.class); + + String threadName = Thread.currentThread().getName(); + AnotherTestEvent event = new AnotherTestEvent(this, threadName); + SimpleService listener = this.context.getBean(SimpleService.class); + this.eventCollector.assertNoEventReceived(listener); + + this.context.publishEvent(event); + this.countDownLatch.await(2, TimeUnit.SECONDS); + this.eventCollector.assertEvent(listener, event); + this.eventCollector.assertTotalEventsCount(1); + } + + @Test + public void asyncProcessingAppliedWithScopedProxy() throws InterruptedException { + doLoad(AsyncConfigurationWithInterfaces.class, ScopedProxyTestBean.class); + + String threadName = Thread.currentThread().getName(); + AnotherTestEvent event = new AnotherTestEvent(this, threadName); + SimpleService listener = this.context.getBean(SimpleService.class); + this.eventCollector.assertNoEventReceived(listener); + + this.context.publishEvent(event); + this.countDownLatch.await(2, TimeUnit.SECONDS); + this.eventCollector.assertEvent(listener, event); + this.eventCollector.assertTotalEventsCount(1); + } + + @Test + public void exceptionPropagated() { + load(ExceptionEventListener.class); + TestEvent event = new TestEvent(this, "fail"); + ExceptionEventListener listener = this.context.getBean(ExceptionEventListener.class); + this.eventCollector.assertNoEventReceived(listener); + assertThatIllegalStateException().isThrownBy(() -> + this.context.publishEvent(event)) + .withMessage("Test exception"); + this.eventCollector.assertEvent(listener, event); + this.eventCollector.assertTotalEventsCount(1); + } + + @Test + public void exceptionNotPropagatedWithAsync() throws InterruptedException { + loadAsync(ExceptionEventListener.class); + AnotherTestEvent event = new AnotherTestEvent(this, "fail"); + ExceptionEventListener listener = this.context.getBean(ExceptionEventListener.class); + this.eventCollector.assertNoEventReceived(listener); + + this.context.publishEvent(event); + this.countDownLatch.await(2, TimeUnit.SECONDS); + + this.eventCollector.assertEvent(listener, event); + this.eventCollector.assertTotalEventsCount(1); + } + + @Test + public void listenerWithSimplePayload() { + load(TestEventListener.class); + TestEventListener listener = this.context.getBean(TestEventListener.class); + + this.eventCollector.assertNoEventReceived(listener); + this.context.publishEvent("test"); + this.eventCollector.assertEvent(listener, "test"); + this.eventCollector.assertTotalEventsCount(1); + } + + @Test + public void listenerWithNonMatchingPayload() { + load(TestEventListener.class); + TestEventListener listener = this.context.getBean(TestEventListener.class); + + this.eventCollector.assertNoEventReceived(listener); + this.context.publishEvent(123L); + this.eventCollector.assertNoEventReceived(listener); + this.eventCollector.assertTotalEventsCount(0); + } + + @Test + public void replyWithPayload() { + load(TestEventListener.class, ReplyEventListener.class); + AnotherTestEvent event = new AnotherTestEvent(this, "String"); + ReplyEventListener replyEventListener = this.context.getBean(ReplyEventListener.class); + TestEventListener listener = this.context.getBean(TestEventListener.class); + + + this.eventCollector.assertNoEventReceived(listener); + this.eventCollector.assertNoEventReceived(replyEventListener); + this.context.publishEvent(event); + this.eventCollector.assertEvent(replyEventListener, event); + this.eventCollector.assertEvent(listener, "String"); // reply + this.eventCollector.assertTotalEventsCount(2); + } + + @Test + public void listenerWithGenericApplicationEvent() { + load(GenericEventListener.class); + GenericEventListener listener = this.context.getBean(GenericEventListener.class); + + this.eventCollector.assertNoEventReceived(listener); + this.context.publishEvent("TEST"); + this.eventCollector.assertEvent(listener, "TEST"); + this.eventCollector.assertTotalEventsCount(1); + } + + @Test + public void listenerWithResolvableTypeEvent() { + load(ResolvableTypeEventListener.class); + ResolvableTypeEventListener listener = this.context.getBean(ResolvableTypeEventListener.class); + + this.eventCollector.assertNoEventReceived(listener); + GenericEventPojo event = new GenericEventPojo<>("TEST"); + this.context.publishEvent(event); + this.eventCollector.assertEvent(listener, event); + this.eventCollector.assertTotalEventsCount(1); + } + + @Test + public void listenerWithResolvableTypeEventWrongGeneric() { + load(ResolvableTypeEventListener.class); + ResolvableTypeEventListener listener = this.context.getBean(ResolvableTypeEventListener.class); + + this.eventCollector.assertNoEventReceived(listener); + GenericEventPojo event = new GenericEventPojo<>(123L); + this.context.publishEvent(event); + this.eventCollector.assertNoEventReceived(listener); + this.eventCollector.assertTotalEventsCount(0); + } + + @Test + public void conditionMatch() { + validateConditionMatch(ConditionalEventListener.class); + } + + @Test + public void conditionMatchWithProxy() { + validateConditionMatch(ConditionalEventListener.class, MethodValidationPostProcessor.class); + } + + private void validateConditionMatch(Class... classes) { + long timestamp = System.currentTimeMillis(); + load(classes); + TestEvent event = new TestEvent(this, "OK"); + + ConditionalEventInterface listener = this.context.getBean(ConditionalEventInterface.class); + this.eventCollector.assertNoEventReceived(listener); + + this.context.publishEvent(event); + this.eventCollector.assertEvent(listener, event); + this.eventCollector.assertTotalEventsCount(1); + + this.context.publishEvent("OK"); + this.eventCollector.assertEvent(listener, event, "OK"); + this.eventCollector.assertTotalEventsCount(2); + + this.context.publishEvent("NOT OK"); + this.eventCollector.assertTotalEventsCount(2); + + this.context.publishEvent(timestamp); + this.eventCollector.assertEvent(listener, event, "OK", timestamp); + this.eventCollector.assertTotalEventsCount(3); + + this.context.publishEvent(42d); + this.eventCollector.assertEvent(listener, event, "OK", timestamp, 42d); + this.eventCollector.assertTotalEventsCount(4); + } + + @Test + public void conditionDoesNotMatch() { + long maxLong = Long.MAX_VALUE; + load(ConditionalEventListener.class); + TestEvent event = new TestEvent(this, "KO"); + TestEventListener listener = this.context.getBean(ConditionalEventListener.class); + this.eventCollector.assertNoEventReceived(listener); + + this.context.publishEvent(event); + this.eventCollector.assertNoEventReceived(listener); + this.eventCollector.assertTotalEventsCount(0); + + this.context.publishEvent("KO"); + this.eventCollector.assertNoEventReceived(listener); + this.eventCollector.assertTotalEventsCount(0); + + this.context.publishEvent(maxLong); + this.eventCollector.assertNoEventReceived(listener); + this.eventCollector.assertTotalEventsCount(0); + + this.context.publishEvent(24d); + this.eventCollector.assertNoEventReceived(listener); + this.eventCollector.assertTotalEventsCount(0); + } + + @Test + public void orderedListeners() { + load(OrderedTestListener.class); + OrderedTestListener listener = this.context.getBean(OrderedTestListener.class); + + assertThat(listener.order.isEmpty()).isTrue(); + this.context.publishEvent("whatever"); + assertThat(listener.order).contains("first", "second", "third"); + } + + @Test @Disabled // SPR-15122 + public void listenersReceiveEarlyEvents() { + load(EventOnPostConstruct.class, OrderedTestListener.class); + OrderedTestListener listener = this.context.getBean(OrderedTestListener.class); + + assertThat(listener.order).contains("first", "second", "third"); + } + + @Test + public void missingListenerBeanIgnored() { + load(MissingEventListener.class); + context.getBean(UseMissingEventListener.class); + context.getBean(ApplicationEventMulticaster.class).multicastEvent(new TestEvent(this)); + } + + + private void load(Class... classes) { + List> allClasses = new ArrayList<>(); + allClasses.add(BasicConfiguration.class); + allClasses.addAll(Arrays.asList(classes)); + doLoad(allClasses.toArray(new Class[allClasses.size()])); + } + + private void loadAsync(Class... classes) { + List> allClasses = new ArrayList<>(); + allClasses.add(AsyncConfiguration.class); + allClasses.addAll(Arrays.asList(classes)); + doLoad(allClasses.toArray(new Class[allClasses.size()])); + } + + private void doLoad(Class... classes) { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(classes); + this.eventCollector = ctx.getBean(EventCollector.class); + this.countDownLatch = ctx.getBean(CountDownLatch.class); + this.context = ctx; + } + + private EventCollector getEventCollector(ConfigurableApplicationContext context) { + return context.getBean(EventCollector.class); + } + + + @Configuration + static class BasicConfiguration { + + @Bean + public EventCollector eventCollector() { + return new EventCollector(); + } + + @Bean + public CountDownLatch testCountDownLatch() { + return new CountDownLatch(1); + } + + @Bean + public TestConditionEvaluator conditionEvaluator() { + return new TestConditionEvaluator(); + } + + static class TestConditionEvaluator { + + public boolean valid(Double ratio) { + return new Double(42).equals(ratio); + } + } + } + + + static abstract class AbstractTestEventListener extends AbstractIdentifiable { + + @Autowired + private EventCollector eventCollector; + + protected void collectEvent(Object content) { + this.eventCollector.addEvent(this, content); + } + } + + + @Component + static class TestEventListener extends AbstractTestEventListener { + + @EventListener + public void handle(TestEvent event) { + collectEvent(event); + } + + @EventListener + public void handleString(String content) { + collectEvent(content); + } + } + + + @EventListener + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @interface FooListener { + } + + + @Component + static class MetaAnnotationListenerTestBean extends AbstractTestEventListener { + + @FooListener + public void handleIt(TestEvent event) { + collectEvent(event); + } + } + + + @Component + static class ContextEventListener extends AbstractTestEventListener { + + @EventListener + public void handleContextEvent(ApplicationContextEvent event) { + collectEvent(event); + } + + } + + + @Component + static class InvalidMethodSignatureEventListener { + + @EventListener + public void cannotBeCalled(String s, Integer what) { + } + } + + + @Component + static class ReplyEventListener extends AbstractTestEventListener { + + @EventListener + public Object handle(AnotherTestEvent event) { + collectEvent(event); + if (event.content == null) { + return null; + } + else if (event.content instanceof String) { + String s = (String) event.content; + if (s.equals("String")) { + return event.content; + } + else { + return new TestEvent(this, event.getId(), s); + } + } + return event.content; + } + } + + + @Component + static class ExceptionEventListener extends AbstractTestEventListener { + + @Autowired + private CountDownLatch countDownLatch; + + @EventListener + public void handle(TestEvent event) { + collectEvent(event); + if ("fail".equals(event.msg)) { + throw new IllegalStateException("Test exception"); + } + } + + @EventListener + @Async + public void handleAsync(AnotherTestEvent event) { + collectEvent(event); + if ("fail".equals(event.content)) { + this.countDownLatch.countDown(); + throw new IllegalStateException("Test exception"); + } + } + } + + + @Component + static class AsyncEventListener extends AbstractTestEventListener { + + @Autowired + private CountDownLatch countDownLatch; + + @EventListener + @Async + public void handleAsync(AnotherTestEvent event) { + assertThat(Thread.currentThread().getName()).isNotEqualTo(event.content); + collectEvent(event); + this.countDownLatch.countDown(); + } + } + + + @Configuration + @Import(BasicConfiguration.class) + @EnableAsync(proxyTargetClass = true) + static class AsyncConfiguration { + } + + + @Configuration + @Import(BasicConfiguration.class) + @EnableAsync(proxyTargetClass = false) + static class AsyncConfigurationWithInterfaces { + } + + + interface SimpleService extends Identifiable { + + void handleIt(TestEvent event); + + void handleAsync(AnotherTestEvent event); + } + + + @Component + static class SimpleProxyTestBean extends AbstractIdentifiable implements SimpleService { + + @Autowired + private EventCollector eventCollector; + + @Autowired + private CountDownLatch countDownLatch; + + @EventListener + @Override + public void handleIt(TestEvent event) { + this.eventCollector.addEvent(this, event); + } + + @EventListener + @Async + @Override + public void handleAsync(AnotherTestEvent event) { + assertThat(Thread.currentThread().getName()).isNotEqualTo(event.content); + this.eventCollector.addEvent(this, event); + this.countDownLatch.countDown(); + } + } + + + @Component + @Scope(proxyMode = ScopedProxyMode.INTERFACES) + static class ScopedProxyTestBean extends AbstractIdentifiable implements SimpleService { + + @Autowired + private EventCollector eventCollector; + + @Autowired + private CountDownLatch countDownLatch; + + @EventListener + @Override + public void handleIt(TestEvent event) { + this.eventCollector.addEvent(this, event); + } + + @EventListener + @Async + @Override + public void handleAsync(AnotherTestEvent event) { + assertThat(Thread.currentThread().getName()).isNotEqualTo(event.content); + this.eventCollector.addEvent(this, event); + this.countDownLatch.countDown(); + } + } + + + interface AnnotatedSimpleService extends Identifiable { + + @EventListener + void handleIt(TestEvent event); + } + + + @Component + @Scope(proxyMode = ScopedProxyMode.INTERFACES) + static class AnnotatedProxyTestBean extends AbstractIdentifiable implements AnnotatedSimpleService { + + @Autowired + private EventCollector eventCollector; + + @Override + public void handleIt(TestEvent event) { + this.eventCollector.addEvent(this, event); + } + } + + + @Component + @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) + static class CglibProxyTestBean extends AbstractTestEventListener { + + @EventListener + public void handleIt(TestEvent event) { + collectEvent(event); + } + } + + + @Component + @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) + static class CglibProxyWithPrivateMethod extends AbstractTestEventListener { + + @EventListener + private void handleIt(TestEvent event) { + collectEvent(event); + } + } + + + @Component + @Scope(scopeName = "custom", proxyMode = ScopedProxyMode.TARGET_CLASS) + static class CustomScopeTestBean extends AbstractTestEventListener { + + @EventListener + public void handleIt(TestEvent event) { + collectEvent(event); + } + } + + + @Component + static class GenericEventListener extends AbstractTestEventListener { + + @EventListener + public void handleString(PayloadApplicationEvent event) { + collectEvent(event.getPayload()); + } + } + + + @Component + static class ResolvableTypeEventListener extends AbstractTestEventListener { + + @EventListener + public void handleString(GenericEventPojo value) { + collectEvent(value); + } + } + + + + @EventListener + @Retention(RetentionPolicy.RUNTIME) + public @interface ConditionalEvent { + + @AliasFor(annotation = EventListener.class, attribute = "condition") + String value(); + } + + + interface ConditionalEventInterface extends Identifiable { + + void handle(TestEvent event); + + void handleString(String payload); + + void handleTimestamp(Long timestamp); + + void handleRatio(Double ratio); + } + + + @Component + @Validated + static class ConditionalEventListener extends TestEventListener implements ConditionalEventInterface { + + @EventListener(condition = "'OK'.equals(#root.event.msg)") + @Override + public void handle(TestEvent event) { + super.handle(event); + } + + @EventListener(condition = "#payload.startsWith('OK')") + @Override + public void handleString(String payload) { + super.handleString(payload); + } + + @ConditionalEvent("#root.event.timestamp > #p0") + @Override + public void handleTimestamp(Long timestamp) { + collectEvent(timestamp); + } + + @ConditionalEvent("@conditionEvaluator.valid(#p0)") + @Override + public void handleRatio(Double ratio) { + collectEvent(ratio); + } + } + + + @Configuration + static class OrderedTestListener extends TestEventListener { + + public final List order = new ArrayList<>(); + + @EventListener + @Order(50) + public void handleThird(String payload) { + this.order.add("third"); + } + + @EventListener + @Order(-50) + public void handleFirst(String payload) { + this.order.add("first"); + } + + @EventListener + public void handleSecond(String payload) { + this.order.add("second"); + } + } + + + static class EventOnPostConstruct { + + @Autowired + ApplicationEventPublisher publisher; + + @PostConstruct + public void init() { + this.publisher.publishEvent("earlyEvent"); + } + } + + + private static class CustomScope implements org.springframework.beans.factory.config.Scope { + + public boolean active = true; + + private Object instance = null; + + @Override + public Object get(String name, ObjectFactory objectFactory) { + Assert.state(this.active, "Not active"); + if (this.instance == null) { + this.instance = objectFactory.getObject(); + } + return this.instance; + } + + @Override + public Object remove(String name) { + return null; + } + + @Override + public void registerDestructionCallback(String name, Runnable callback) { + } + + @Override + public Object resolveContextualObject(String key) { + return null; + } + + @Override + public String getConversationId() { + return null; + } + } + + + @Configuration + @Import(UseMissingEventListener.class) + public static class MissingEventListener { + + @Bean + public MyEventListener missing() { + return null; + } + } + + + @Component + public static class MyEventListener { + + @EventListener + public void hear(TestEvent e) { + throw new AssertionError(); + } + } + + + public static class UseMissingEventListener { + + @Inject + public UseMissingEventListener(Optional notHere) { + if (notHere.isPresent()) { + throw new AssertionError(); + } + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java b/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java new file mode 100644 index 0000000..331336a --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java @@ -0,0 +1,735 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executor; + +import org.aopalliance.intercept.MethodInvocation; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.context.ApplicationListener; +import org.springframework.context.PayloadApplicationEvent; +import org.springframework.context.support.AbstractApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.context.support.StaticMessageSource; +import org.springframework.context.testfixture.beans.BeanThatBroadcasts; +import org.springframework.context.testfixture.beans.BeanThatListens; +import org.springframework.core.Ordered; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.Order; +import org.springframework.scheduling.support.TaskUtils; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Unit and integration tests for the ApplicationContext event support. + * + * @author Alef Arendsen + * @author Rick Evans + * @author Stephane Nicoll + * @author Juergen Hoeller + */ +public class ApplicationContextEventTests extends AbstractApplicationEventListenerTests { + + @Test + public void multicastSimpleEvent() { + multicastEvent(true, ApplicationListener.class, + new ContextRefreshedEvent(new StaticApplicationContext()), null); + multicastEvent(true, ApplicationListener.class, + new ContextClosedEvent(new StaticApplicationContext()), null); + } + + @Test + public void multicastGenericEvent() { + multicastEvent(true, StringEventListener.class, createGenericTestEvent("test"), + ResolvableType.forClassWithGenerics(GenericTestEvent.class, String.class)); + } + + @Test + public void multicastGenericEventWrongType() { + multicastEvent(false, StringEventListener.class, createGenericTestEvent(123L), + ResolvableType.forClassWithGenerics(GenericTestEvent.class, Long.class)); + } + + @Test + public void multicastGenericEventWildcardSubType() { + multicastEvent(false, StringEventListener.class, createGenericTestEvent("test"), + getGenericApplicationEventType("wildcardEvent")); + } + + @Test + public void multicastConcreteTypeGenericListener() { + multicastEvent(true, StringEventListener.class, new StringEvent(this, "test"), null); + } + + @Test + public void multicastConcreteWrongTypeGenericListener() { + multicastEvent(false, StringEventListener.class, new LongEvent(this, 123L), null); + } + + @Test + public void multicastSmartGenericTypeGenericListener() { + multicastEvent(true, StringEventListener.class, new SmartGenericTestEvent<>(this, "test"), null); + } + + @Test + public void multicastSmartGenericWrongTypeGenericListener() { + multicastEvent(false, StringEventListener.class, new SmartGenericTestEvent<>(this, 123L), null); + } + + private void multicastEvent(boolean match, Class listenerType, ApplicationEvent event, ResolvableType eventType) { + @SuppressWarnings("unchecked") + ApplicationListener listener = + (ApplicationListener) mock(listenerType); + SimpleApplicationEventMulticaster smc = new SimpleApplicationEventMulticaster(); + smc.addApplicationListener(listener); + + if (eventType != null) { + smc.multicastEvent(event, eventType); + } + else { + smc.multicastEvent(event); + } + int invocation = match ? 1 : 0; + verify(listener, times(invocation)).onApplicationEvent(event); + } + + @Test + public void simpleApplicationEventMulticasterWithTaskExecutor() { + @SuppressWarnings("unchecked") + ApplicationListener listener = mock(ApplicationListener.class); + ApplicationEvent evt = new ContextClosedEvent(new StaticApplicationContext()); + + SimpleApplicationEventMulticaster smc = new SimpleApplicationEventMulticaster(); + smc.setTaskExecutor(new Executor() { + @Override + public void execute(Runnable command) { + command.run(); + command.run(); + } + }); + smc.addApplicationListener(listener); + + smc.multicastEvent(evt); + verify(listener, times(2)).onApplicationEvent(evt); + } + + @Test + public void simpleApplicationEventMulticasterWithException() { + @SuppressWarnings("unchecked") + ApplicationListener listener = mock(ApplicationListener.class); + ApplicationEvent evt = new ContextClosedEvent(new StaticApplicationContext()); + + SimpleApplicationEventMulticaster smc = new SimpleApplicationEventMulticaster(); + smc.addApplicationListener(listener); + + RuntimeException thrown = new RuntimeException(); + willThrow(thrown).given(listener).onApplicationEvent(evt); + assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> + smc.multicastEvent(evt)) + .satisfies(ex -> assertThat(ex).isSameAs(thrown)); + } + + @Test + public void simpleApplicationEventMulticasterWithErrorHandler() { + @SuppressWarnings("unchecked") + ApplicationListener listener = mock(ApplicationListener.class); + ApplicationEvent evt = new ContextClosedEvent(new StaticApplicationContext()); + + SimpleApplicationEventMulticaster smc = new SimpleApplicationEventMulticaster(); + smc.setErrorHandler(TaskUtils.LOG_AND_SUPPRESS_ERROR_HANDLER); + smc.addApplicationListener(listener); + + willThrow(new RuntimeException()).given(listener).onApplicationEvent(evt); + smc.multicastEvent(evt); + } + + @Test + public void orderedListeners() { + MyOrderedListener1 listener1 = new MyOrderedListener1(); + MyOrderedListener2 listener2 = new MyOrderedListener2(listener1); + + SimpleApplicationEventMulticaster smc = new SimpleApplicationEventMulticaster(); + smc.addApplicationListener(listener2); + smc.addApplicationListener(listener1); + + smc.multicastEvent(new MyEvent(this)); + smc.multicastEvent(new MyOtherEvent(this)); + assertThat(listener1.seenEvents.size()).isEqualTo(2); + } + + @Test + public void orderedListenersWithAnnotation() { + MyOrderedListener3 listener1 = new MyOrderedListener3(); + MyOrderedListener4 listener2 = new MyOrderedListener4(listener1); + + SimpleApplicationEventMulticaster smc = new SimpleApplicationEventMulticaster(); + smc.addApplicationListener(listener2); + smc.addApplicationListener(listener1); + + smc.multicastEvent(new MyEvent(this)); + smc.multicastEvent(new MyOtherEvent(this)); + assertThat(listener1.seenEvents.size()).isEqualTo(2); + } + + @Test + @SuppressWarnings("unchecked") + public void proxiedListeners() { + MyOrderedListener1 listener1 = new MyOrderedListener1(); + MyOrderedListener2 listener2 = new MyOrderedListener2(listener1); + ApplicationListener proxy1 = (ApplicationListener) new ProxyFactory(listener1).getProxy(); + ApplicationListener proxy2 = (ApplicationListener) new ProxyFactory(listener2).getProxy(); + + SimpleApplicationEventMulticaster smc = new SimpleApplicationEventMulticaster(); + smc.addApplicationListener(proxy1); + smc.addApplicationListener(proxy2); + + smc.multicastEvent(new MyEvent(this)); + smc.multicastEvent(new MyOtherEvent(this)); + assertThat(listener1.seenEvents.size()).isEqualTo(2); + } + + @Test + @SuppressWarnings("unchecked") + public void proxiedListenersMixedWithTargetListeners() { + MyOrderedListener1 listener1 = new MyOrderedListener1(); + MyOrderedListener2 listener2 = new MyOrderedListener2(listener1); + ApplicationListener proxy1 = (ApplicationListener) new ProxyFactory(listener1).getProxy(); + ApplicationListener proxy2 = (ApplicationListener) new ProxyFactory(listener2).getProxy(); + + SimpleApplicationEventMulticaster smc = new SimpleApplicationEventMulticaster(); + smc.addApplicationListener(listener1); + smc.addApplicationListener(listener2); + smc.addApplicationListener(proxy1); + smc.addApplicationListener(proxy2); + + smc.multicastEvent(new MyEvent(this)); + smc.multicastEvent(new MyOtherEvent(this)); + assertThat(listener1.seenEvents.size()).isEqualTo(2); + } + + @Test + public void testEventPublicationInterceptor() throws Throwable { + MethodInvocation invocation = mock(MethodInvocation.class); + ApplicationContext ctx = mock(ApplicationContext.class); + + EventPublicationInterceptor interceptor = new EventPublicationInterceptor(); + interceptor.setApplicationEventClass(MyEvent.class); + interceptor.setApplicationEventPublisher(ctx); + interceptor.afterPropertiesSet(); + + given(invocation.proceed()).willReturn(new Object()); + given(invocation.getThis()).willReturn(new Object()); + interceptor.invoke(invocation); + verify(ctx).publishEvent(isA(MyEvent.class)); + } + + @Test + public void listenersInApplicationContext() { + StaticApplicationContext context = new StaticApplicationContext(); + context.registerBeanDefinition("listener1", new RootBeanDefinition(MyOrderedListener1.class)); + RootBeanDefinition listener2 = new RootBeanDefinition(MyOrderedListener2.class); + listener2.getConstructorArgumentValues().addGenericArgumentValue(new RuntimeBeanReference("listener1")); + listener2.setLazyInit(true); + context.registerBeanDefinition("listener2", listener2); + context.refresh(); + assertThat(context.getDefaultListableBeanFactory().containsSingleton("listener2")).isFalse(); + + MyOrderedListener1 listener1 = context.getBean("listener1", MyOrderedListener1.class); + MyOtherEvent event1 = new MyOtherEvent(context); + context.publishEvent(event1); + assertThat(context.getDefaultListableBeanFactory().containsSingleton("listener2")).isFalse(); + MyEvent event2 = new MyEvent(context); + context.publishEvent(event2); + assertThat(context.getDefaultListableBeanFactory().containsSingleton("listener2")).isTrue(); + MyEvent event3 = new MyEvent(context); + context.publishEvent(event3); + MyOtherEvent event4 = new MyOtherEvent(context); + context.publishEvent(event4); + assertThat(listener1.seenEvents.contains(event1)).isTrue(); + assertThat(listener1.seenEvents.contains(event2)).isTrue(); + assertThat(listener1.seenEvents.contains(event3)).isTrue(); + assertThat(listener1.seenEvents.contains(event4)).isTrue(); + + listener1.seenEvents.clear(); + context.publishEvent(event1); + context.publishEvent(event2); + context.publishEvent(event3); + context.publishEvent(event4); + assertThat(listener1.seenEvents.contains(event1)).isTrue(); + assertThat(listener1.seenEvents.contains(event2)).isTrue(); + assertThat(listener1.seenEvents.contains(event3)).isTrue(); + assertThat(listener1.seenEvents.contains(event4)).isTrue(); + + AbstractApplicationEventMulticaster multicaster = context.getBean(AbstractApplicationEventMulticaster.class); + assertThat(multicaster.retrieverCache.size()).isEqualTo(2); + + context.close(); + } + + @Test + public void listenersInApplicationContextWithPayloadEvents() { + StaticApplicationContext context = new StaticApplicationContext(); + context.registerBeanDefinition("listener", new RootBeanDefinition(MyPayloadListener.class)); + context.refresh(); + + MyPayloadListener listener = context.getBean("listener", MyPayloadListener.class); + context.publishEvent("event1"); + context.publishEvent("event2"); + context.publishEvent("event3"); + context.publishEvent("event4"); + assertThat(listener.seenPayloads.contains("event1")).isTrue(); + assertThat(listener.seenPayloads.contains("event2")).isTrue(); + assertThat(listener.seenPayloads.contains("event3")).isTrue(); + assertThat(listener.seenPayloads.contains("event4")).isTrue(); + + AbstractApplicationEventMulticaster multicaster = context.getBean(AbstractApplicationEventMulticaster.class); + assertThat(multicaster.retrieverCache.size()).isEqualTo(2); + + context.close(); + } + + @Test + public void listenersInApplicationContextWithNestedChild() { + StaticApplicationContext context = new StaticApplicationContext(); + RootBeanDefinition nestedChild = new RootBeanDefinition(StaticApplicationContext.class); + nestedChild.getPropertyValues().add("parent", context); + nestedChild.setInitMethodName("refresh"); + context.registerBeanDefinition("nestedChild", nestedChild); + RootBeanDefinition listener1Def = new RootBeanDefinition(MyOrderedListener1.class); + listener1Def.setDependsOn("nestedChild"); + context.registerBeanDefinition("listener1", listener1Def); + context.refresh(); + + MyOrderedListener1 listener1 = context.getBean("listener1", MyOrderedListener1.class); + MyEvent event1 = new MyEvent(context); + context.publishEvent(event1); + assertThat(listener1.seenEvents.contains(event1)).isTrue(); + + SimpleApplicationEventMulticaster multicaster = context.getBean( + AbstractApplicationContext.APPLICATION_EVENT_MULTICASTER_BEAN_NAME, + SimpleApplicationEventMulticaster.class); + assertThat(multicaster.getApplicationListeners().isEmpty()).isFalse(); + + context.close(); + assertThat(multicaster.getApplicationListeners().isEmpty()).isTrue(); + } + + @Test + public void nonSingletonListenerInApplicationContext() { + StaticApplicationContext context = new StaticApplicationContext(); + RootBeanDefinition listener = new RootBeanDefinition(MyNonSingletonListener.class); + listener.setScope(BeanDefinition.SCOPE_PROTOTYPE); + context.registerBeanDefinition("listener", listener); + context.refresh(); + + MyEvent event1 = new MyEvent(context); + context.publishEvent(event1); + MyOtherEvent event2 = new MyOtherEvent(context); + context.publishEvent(event2); + MyEvent event3 = new MyEvent(context); + context.publishEvent(event3); + MyOtherEvent event4 = new MyOtherEvent(context); + context.publishEvent(event4); + assertThat(MyNonSingletonListener.seenEvents.contains(event1)).isTrue(); + assertThat(MyNonSingletonListener.seenEvents.contains(event2)).isTrue(); + assertThat(MyNonSingletonListener.seenEvents.contains(event3)).isTrue(); + assertThat(MyNonSingletonListener.seenEvents.contains(event4)).isTrue(); + MyNonSingletonListener.seenEvents.clear(); + + context.publishEvent(event1); + context.publishEvent(event2); + context.publishEvent(event3); + context.publishEvent(event4); + assertThat(MyNonSingletonListener.seenEvents.contains(event1)).isTrue(); + assertThat(MyNonSingletonListener.seenEvents.contains(event2)).isTrue(); + assertThat(MyNonSingletonListener.seenEvents.contains(event3)).isTrue(); + assertThat(MyNonSingletonListener.seenEvents.contains(event4)).isTrue(); + MyNonSingletonListener.seenEvents.clear(); + + AbstractApplicationEventMulticaster multicaster = context.getBean(AbstractApplicationEventMulticaster.class); + assertThat(multicaster.retrieverCache.size()).isEqualTo(3); + + context.close(); + } + + @Test + public void listenerAndBroadcasterWithCircularReference() { + StaticApplicationContext context = new StaticApplicationContext(); + context.registerBeanDefinition("broadcaster", new RootBeanDefinition(BeanThatBroadcasts.class)); + RootBeanDefinition listenerDef = new RootBeanDefinition(BeanThatListens.class); + listenerDef.getConstructorArgumentValues().addGenericArgumentValue(new RuntimeBeanReference("broadcaster")); + context.registerBeanDefinition("listener", listenerDef); + context.refresh(); + + BeanThatBroadcasts broadcaster = context.getBean("broadcaster", BeanThatBroadcasts.class); + context.publishEvent(new MyEvent(context)); + assertThat(broadcaster.receivedCount).as("The event was not received by the listener").isEqualTo(2); + + context.close(); + } + + @Test + public void innerBeanAsListener() { + StaticApplicationContext context = new StaticApplicationContext(); + RootBeanDefinition listenerDef = new RootBeanDefinition(TestBean.class); + listenerDef.getPropertyValues().add("friends", new RootBeanDefinition(BeanThatListens.class)); + context.registerBeanDefinition("listener", listenerDef); + context.refresh(); + + context.publishEvent(new MyEvent(this)); + context.publishEvent(new MyEvent(this)); + TestBean listener = context.getBean(TestBean.class); + assertThat(((BeanThatListens) listener.getFriends().iterator().next()).getEventCount()).isEqualTo(3); + + context.close(); + } + + @Test + public void anonymousClassAsListener() { + final Set seenEvents = new HashSet<>(); + StaticApplicationContext context = new StaticApplicationContext(); + context.addApplicationListener(new ApplicationListener() { + @Override + public void onApplicationEvent(MyEvent event) { + seenEvents.add(event); + } + }); + context.refresh(); + + MyEvent event1 = new MyEvent(context); + context.publishEvent(event1); + context.publishEvent(new MyOtherEvent(context)); + MyEvent event2 = new MyEvent(context); + context.publishEvent(event2); + assertThat(seenEvents.size()).isSameAs(2); + assertThat(seenEvents.contains(event1)).isTrue(); + assertThat(seenEvents.contains(event2)).isTrue(); + + context.close(); + } + + @Test + public void lambdaAsListener() { + final Set seenEvents = new HashSet<>(); + StaticApplicationContext context = new StaticApplicationContext(); + ApplicationListener listener = seenEvents::add; + context.addApplicationListener(listener); + context.refresh(); + + MyEvent event1 = new MyEvent(context); + context.publishEvent(event1); + context.publishEvent(new MyOtherEvent(context)); + MyEvent event2 = new MyEvent(context); + context.publishEvent(event2); + assertThat(seenEvents.size()).isSameAs(2); + assertThat(seenEvents.contains(event1)).isTrue(); + assertThat(seenEvents.contains(event2)).isTrue(); + + context.close(); + } + + @Test + public void lambdaAsListenerWithErrorHandler() { + final Set seenEvents = new HashSet<>(); + StaticApplicationContext context = new StaticApplicationContext(); + SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster(); + multicaster.setErrorHandler(ReflectionUtils::rethrowRuntimeException); + context.getBeanFactory().registerSingleton( + StaticApplicationContext.APPLICATION_EVENT_MULTICASTER_BEAN_NAME, multicaster); + ApplicationListener listener = seenEvents::add; + context.addApplicationListener(listener); + context.refresh(); + + MyEvent event1 = new MyEvent(context); + context.publishEvent(event1); + context.publishEvent(new MyOtherEvent(context)); + MyEvent event2 = new MyEvent(context); + context.publishEvent(event2); + assertThat(seenEvents.size()).isSameAs(2); + assertThat(seenEvents.contains(event1)).isTrue(); + assertThat(seenEvents.contains(event2)).isTrue(); + + context.close(); + } + + @Test + public void lambdaAsListenerWithJava8StyleClassCastMessage() { + StaticApplicationContext context = new StaticApplicationContext(); + ApplicationListener listener = + event -> { throw new ClassCastException(event.getClass().getName()); }; + context.addApplicationListener(listener); + context.refresh(); + + context.publishEvent(new MyEvent(context)); + context.close(); + } + + @Test + public void lambdaAsListenerWithJava9StyleClassCastMessage() { + StaticApplicationContext context = new StaticApplicationContext(); + ApplicationListener listener = + event -> { throw new ClassCastException("spring.context/" + event.getClass().getName()); }; + context.addApplicationListener(listener); + context.refresh(); + + context.publishEvent(new MyEvent(context)); + context.close(); + } + + @Test + public void beanPostProcessorPublishesEvents() { + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBeanDefinition("listener", new RootBeanDefinition(BeanThatListens.class)); + context.registerBeanDefinition("messageSource", new RootBeanDefinition(StaticMessageSource.class)); + context.registerBeanDefinition("postProcessor", new RootBeanDefinition(EventPublishingBeanPostProcessor.class)); + context.refresh(); + + context.publishEvent(new MyEvent(this)); + BeanThatListens listener = context.getBean(BeanThatListens.class); + assertThat(listener.getEventCount()).isEqualTo(4); + + context.close(); + } + + @Test + public void initMethodPublishesEvent() { + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBeanDefinition("listener", new RootBeanDefinition(BeanThatListens.class)); + context.registerBeanDefinition("messageSource", new RootBeanDefinition(StaticMessageSource.class)); + context.registerBeanDefinition("initMethod", new RootBeanDefinition(EventPublishingInitMethod.class)); + context.refresh(); + + context.publishEvent(new MyEvent(this)); + BeanThatListens listener = context.getBean(BeanThatListens.class); + assertThat(listener.getEventCount()).isEqualTo(3); + + context.close(); + } + + @Test + public void initMethodPublishesAsyncEvent() { + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBeanDefinition("listener", new RootBeanDefinition(BeanThatListens.class)); + context.registerBeanDefinition("messageSource", new RootBeanDefinition(StaticMessageSource.class)); + context.registerBeanDefinition("initMethod", new RootBeanDefinition(AsyncEventPublishingInitMethod.class)); + context.refresh(); + + context.publishEvent(new MyEvent(this)); + BeanThatListens listener = context.getBean(BeanThatListens.class); + assertThat(listener.getEventCount()).isEqualTo(3); + + context.close(); + } + + + @SuppressWarnings("serial") + public static class MyEvent extends ApplicationEvent { + + public MyEvent(Object source) { + super(source); + } + } + + + @SuppressWarnings("serial") + public static class MyOtherEvent extends ApplicationEvent { + + public MyOtherEvent(Object source) { + super(source); + } + } + + + public static class MyOrderedListener1 implements ApplicationListener, Ordered { + + public final List seenEvents = new ArrayList<>(); + + @Override + public void onApplicationEvent(ApplicationEvent event) { + this.seenEvents.add(event); + } + + @Override + public int getOrder() { + return 0; + } + } + + + public interface MyOrderedListenerIfc extends ApplicationListener, Ordered { + } + + + public static abstract class MyOrderedListenerBase implements MyOrderedListenerIfc { + + @Override + public int getOrder() { + return 1; + } + } + + + public static class MyOrderedListener2 extends MyOrderedListenerBase { + + private final MyOrderedListener1 otherListener; + + public MyOrderedListener2(MyOrderedListener1 otherListener) { + this.otherListener = otherListener; + } + + @Override + public void onApplicationEvent(MyEvent event) { + assertThat(this.otherListener.seenEvents.contains(event)).isTrue(); + } + } + + + @SuppressWarnings("rawtypes") + public static class MyPayloadListener implements ApplicationListener { + + public final Set seenPayloads = new HashSet<>(); + + @Override + public void onApplicationEvent(PayloadApplicationEvent event) { + this.seenPayloads.add(event.getPayload()); + } + } + + + public static class MyNonSingletonListener implements ApplicationListener { + + public static final Set seenEvents = new HashSet<>(); + + @Override + public void onApplicationEvent(ApplicationEvent event) { + seenEvents.add(event); + } + } + + + @Order(5) + public static class MyOrderedListener3 implements ApplicationListener { + + public final Set seenEvents = new HashSet<>(); + + @Override + public void onApplicationEvent(ApplicationEvent event) { + this.seenEvents.add(event); + } + + } + + + @Order(50) + public static class MyOrderedListener4 implements ApplicationListener { + + private final MyOrderedListener3 otherListener; + + public MyOrderedListener4(MyOrderedListener3 otherListener) { + this.otherListener = otherListener; + } + + @Override + public void onApplicationEvent(MyEvent event) { + assertThat(this.otherListener.seenEvents.contains(event)).isTrue(); + } + } + + + public static class EventPublishingBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware { + + private ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + this.applicationContext.publishEvent(new MyEvent(this)); + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + } + + + public static class EventPublishingInitMethod implements ApplicationEventPublisherAware, InitializingBean { + + private ApplicationEventPublisher publisher; + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.publisher = applicationEventPublisher; + } + + @Override + public void afterPropertiesSet() throws Exception { + this.publisher.publishEvent(new MyEvent(this)); + } + } + + + public static class AsyncEventPublishingInitMethod implements ApplicationEventPublisherAware, InitializingBean { + + private ApplicationEventPublisher publisher; + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.publisher = applicationEventPublisher; + } + + @Override + public void afterPropertiesSet() throws Exception { + Thread thread = new Thread(() -> this.publisher.publishEvent(new MyEvent(this))); + thread.start(); + thread.join(); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/event/ApplicationListenerMethodAdapterTests.java b/spring-context/src/test/java/org/springframework/context/event/ApplicationListenerMethodAdapterTests.java new file mode 100644 index 0000000..83fea0f --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/ApplicationListenerMethodAdapterTests.java @@ -0,0 +1,489 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.lang.reflect.UndeclaredThrowableException; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.PayloadApplicationEvent; +import org.springframework.core.Ordered; +import org.springframework.core.ResolvableType; +import org.springframework.core.ResolvableTypeProvider; +import org.springframework.core.annotation.Order; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author Stephane Nicoll + */ +public class ApplicationListenerMethodAdapterTests extends AbstractApplicationEventListenerTests { + + private final SampleEvents sampleEvents = spy(new SampleEvents()); + + private final ApplicationContext context = mock(ApplicationContext.class); + + + @Test + public void rawListener() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleRaw", ApplicationEvent.class); + supportsEventType(true, method, ResolvableType.forClass(ApplicationEvent.class)); + } + + @Test + public void rawListenerWithGenericEvent() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleRaw", ApplicationEvent.class); + supportsEventType(true, method, ResolvableType.forClassWithGenerics(GenericTestEvent.class, String.class)); + } + + @Test + public void genericListener() { + Method method = ReflectionUtils.findMethod( + SampleEvents.class, "handleGenericString", GenericTestEvent.class); + supportsEventType(true, method, ResolvableType.forClassWithGenerics(GenericTestEvent.class, String.class)); + } + + @Test + public void genericListenerWrongParameterizedType() { + Method method = ReflectionUtils.findMethod( + SampleEvents.class, "handleGenericString", GenericTestEvent.class); + supportsEventType(false, method, ResolvableType.forClassWithGenerics(GenericTestEvent.class, Long.class)); + } + + @Test + public void listenerWithPayloadAndGenericInformation() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleString", String.class); + supportsEventType(true, method, createGenericEventType(String.class)); + } + + @Test + public void listenerWithInvalidPayloadAndGenericInformation() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleString", String.class); + supportsEventType(false, method, createGenericEventType(Integer.class)); + } + + @Test + public void listenerWithPayloadTypeErasure() { // Always accept such event when the type is unknown + Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleString", String.class); + supportsEventType(true, method, ResolvableType.forClass(PayloadApplicationEvent.class)); + } + + @Test + public void listenerWithSubTypeSeveralGenerics() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleString", String.class); + supportsEventType(true, method, ResolvableType.forClass(PayloadTestEvent.class)); + } + + @Test + public void listenerWithSubTypeSeveralGenericsResolved() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleString", String.class); + supportsEventType(true, method, ResolvableType.forClass(PayloadStringTestEvent.class)); + } + + @Test + public void listenerWithAnnotationValue() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleStringAnnotationValue"); + supportsEventType(true, method, createGenericEventType(String.class)); + } + + @Test + public void listenerWithAnnotationClasses() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleStringAnnotationClasses"); + supportsEventType(true, method, createGenericEventType(String.class)); + } + + @Test + public void listenerWithAnnotationValueAndParameter() { + Method method = ReflectionUtils.findMethod( + SampleEvents.class, "handleStringAnnotationValueAndParameter", String.class); + supportsEventType(true, method, createGenericEventType(String.class)); + } + + @Test + public void listenerWithSeveralTypes() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleStringOrInteger"); + supportsEventType(true, method, createGenericEventType(String.class)); + supportsEventType(true, method, createGenericEventType(Integer.class)); + supportsEventType(false, method, createGenericEventType(Double.class)); + } + + @Test + public void listenerWithTooManyParameters() { + Method method = ReflectionUtils.findMethod( + SampleEvents.class, "tooManyParameters", String.class, String.class); + assertThatIllegalStateException().isThrownBy(() -> createTestInstance(method)); + } + + @Test + public void listenerWithNoParameter() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, "noParameter"); + assertThatIllegalStateException().isThrownBy(() -> createTestInstance(method)); + } + + @Test + public void listenerWithMoreThanOneParameter() { + Method method = ReflectionUtils.findMethod( + SampleEvents.class, "moreThanOneParameter", String.class, Integer.class); + assertThatIllegalStateException().isThrownBy(() -> createTestInstance(method)); + } + + @Test + public void defaultOrder() { + Method method = ReflectionUtils.findMethod( + SampleEvents.class, "handleGenericString", GenericTestEvent.class); + ApplicationListenerMethodAdapter adapter = createTestInstance(method); + assertThat(adapter.getOrder()).isEqualTo(Ordered.LOWEST_PRECEDENCE); + } + + @Test + public void specifiedOrder() { + Method method = ReflectionUtils.findMethod( + SampleEvents.class, "handleRaw", ApplicationEvent.class); + ApplicationListenerMethodAdapter adapter = createTestInstance(method); + assertThat(adapter.getOrder()).isEqualTo(42); + } + + @Test + public void invokeListener() { + Method method = ReflectionUtils.findMethod( + SampleEvents.class, "handleGenericString", GenericTestEvent.class); + GenericTestEvent event = createGenericTestEvent("test"); + invokeListener(method, event); + verify(this.sampleEvents, times(1)).handleGenericString(event); + } + + @Test + public void invokeListenerWithGenericEvent() { + Method method = ReflectionUtils.findMethod( + SampleEvents.class, "handleGenericString", GenericTestEvent.class); + GenericTestEvent event = new SmartGenericTestEvent<>(this, "test"); + invokeListener(method, event); + verify(this.sampleEvents, times(1)).handleGenericString(event); + } + + @Test + public void invokeListenerWithGenericPayload() { + Method method = ReflectionUtils.findMethod( + SampleEvents.class, "handleGenericStringPayload", EntityWrapper.class); + EntityWrapper payload = new EntityWrapper<>("test"); + invokeListener(method, new PayloadApplicationEvent<>(this, payload)); + verify(this.sampleEvents, times(1)).handleGenericStringPayload(payload); + } + + @Test + public void invokeListenerWithWrongGenericPayload() { + Method method = ReflectionUtils.findMethod + (SampleEvents.class, "handleGenericStringPayload", EntityWrapper.class); + EntityWrapper payload = new EntityWrapper<>(123); + invokeListener(method, new PayloadApplicationEvent<>(this, payload)); + verify(this.sampleEvents, times(0)).handleGenericStringPayload(any()); + } + + @Test + public void invokeListenerWithAnyGenericPayload() { + Method method = ReflectionUtils.findMethod( + SampleEvents.class, "handleGenericAnyPayload", EntityWrapper.class); + EntityWrapper payload = new EntityWrapper<>("test"); + invokeListener(method, new PayloadApplicationEvent<>(this, payload)); + verify(this.sampleEvents, times(1)).handleGenericAnyPayload(payload); + } + + @Test + public void invokeListenerRuntimeException() { + Method method = ReflectionUtils.findMethod( + SampleEvents.class, "generateRuntimeException", GenericTestEvent.class); + GenericTestEvent event = createGenericTestEvent("fail"); + + assertThatIllegalStateException().isThrownBy(() -> + invokeListener(method, event)) + .withMessageContaining("Test exception") + .withNoCause(); + } + + @Test + public void invokeListenerCheckedException() { + Method method = ReflectionUtils.findMethod( + SampleEvents.class, "generateCheckedException", GenericTestEvent.class); + GenericTestEvent event = createGenericTestEvent("fail"); + + assertThatExceptionOfType(UndeclaredThrowableException.class).isThrownBy(() -> + invokeListener(method, event)) + .withCauseInstanceOf(IOException.class); + } + + @Test + public void invokeListenerInvalidProxy() { + Object target = new InvalidProxyTestBean(); + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.setTarget(target); + proxyFactory.addInterface(SimpleService.class); + Object bean = proxyFactory.getProxy(getClass().getClassLoader()); + + Method method = ReflectionUtils.findMethod( + InvalidProxyTestBean.class, "handleIt2", ApplicationEvent.class); + StaticApplicationListenerMethodAdapter listener = + new StaticApplicationListenerMethodAdapter(method, bean); + assertThatIllegalStateException().isThrownBy(() -> + listener.onApplicationEvent(createGenericTestEvent("test"))) + .withMessageContaining("handleIt2"); + } + + @Test + public void invokeListenerWithPayload() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleString", String.class); + PayloadApplicationEvent event = new PayloadApplicationEvent<>(this, "test"); + invokeListener(method, event); + verify(this.sampleEvents, times(1)).handleString("test"); + } + + @Test + public void invokeListenerWithPayloadWrongType() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleString", String.class); + PayloadApplicationEvent event = new PayloadApplicationEvent<>(this, 123L); + invokeListener(method, event); + verify(this.sampleEvents, never()).handleString(anyString()); + } + + @Test + public void invokeListenerWithAnnotationValue() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleStringAnnotationClasses"); + PayloadApplicationEvent event = new PayloadApplicationEvent<>(this, "test"); + invokeListener(method, event); + verify(this.sampleEvents, times(1)).handleStringAnnotationClasses(); + } + + @Test + public void invokeListenerWithAnnotationValueAndParameter() { + Method method = ReflectionUtils.findMethod( + SampleEvents.class, "handleStringAnnotationValueAndParameter", String.class); + PayloadApplicationEvent event = new PayloadApplicationEvent<>(this, "test"); + invokeListener(method, event); + verify(this.sampleEvents, times(1)).handleStringAnnotationValueAndParameter("test"); + } + + @Test + public void invokeListenerWithSeveralTypes() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, "handleStringOrInteger"); + PayloadApplicationEvent event = new PayloadApplicationEvent<>(this, "test"); + invokeListener(method, event); + verify(this.sampleEvents, times(1)).handleStringOrInteger(); + PayloadApplicationEvent event2 = new PayloadApplicationEvent<>(this, 123); + invokeListener(method, event2); + verify(this.sampleEvents, times(2)).handleStringOrInteger(); + PayloadApplicationEvent event3 = new PayloadApplicationEvent<>(this, 23.2); + invokeListener(method, event3); + verify(this.sampleEvents, times(2)).handleStringOrInteger(); + } + + @Test + public void beanInstanceRetrievedAtEveryInvocation() { + Method method = ReflectionUtils.findMethod( + SampleEvents.class, "handleGenericString", GenericTestEvent.class); + given(this.context.getBean("testBean")).willReturn(this.sampleEvents); + ApplicationListenerMethodAdapter listener = new ApplicationListenerMethodAdapter( + "testBean", GenericTestEvent.class, method); + listener.init(this.context, new EventExpressionEvaluator()); + GenericTestEvent event = createGenericTestEvent("test"); + + + listener.onApplicationEvent(event); + verify(this.sampleEvents, times(1)).handleGenericString(event); + verify(this.context, times(1)).getBean("testBean"); + + listener.onApplicationEvent(event); + verify(this.sampleEvents, times(2)).handleGenericString(event); + verify(this.context, times(2)).getBean("testBean"); + } + + + private void supportsEventType(boolean match, Method method, ResolvableType eventType) { + ApplicationListenerMethodAdapter adapter = createTestInstance(method); + assertThat(adapter.supportsEventType(eventType)) + .as("Wrong match for event '" + eventType + "' on " + method).isEqualTo(match); + } + + private void invokeListener(Method method, ApplicationEvent event) { + ApplicationListenerMethodAdapter adapter = createTestInstance(method); + adapter.onApplicationEvent(event); + } + + private ApplicationListenerMethodAdapter createTestInstance(Method method) { + return new StaticApplicationListenerMethodAdapter(method, this.sampleEvents); + } + + private ResolvableType createGenericEventType(Class payloadType) { + return ResolvableType.forClassWithGenerics(PayloadApplicationEvent.class, payloadType); + } + + + private static class StaticApplicationListenerMethodAdapter extends ApplicationListenerMethodAdapter { + + private final Object targetBean; + + public StaticApplicationListenerMethodAdapter(Method method, Object targetBean) { + super("unused", targetBean.getClass(), method); + this.targetBean = targetBean; + } + + @Override + public Object getTargetBean() { + return this.targetBean; + } + } + + + private static class SampleEvents { + + @EventListener + @Order(42) + public void handleRaw(ApplicationEvent event) { + } + + @EventListener + public void handleGenericString(GenericTestEvent event) { + } + + @EventListener + public void handleString(String payload) { + } + + @EventListener(String.class) + public void handleStringAnnotationValue() { + } + + @EventListener(classes = String.class) + public void handleStringAnnotationClasses() { + } + + @EventListener(String.class) + public void handleStringAnnotationValueAndParameter(String payload) { + } + + @EventListener({String.class, Integer.class}) + public void handleStringOrInteger() { + } + + @EventListener({String.class, Integer.class}) + public void handleStringOrIntegerWithParam(String invalid) { + } + + @EventListener + public void handleGenericStringPayload(EntityWrapper event) { + } + + @EventListener + public void handleGenericAnyPayload(EntityWrapper event) { + } + + @EventListener + public void tooManyParameters(String event, String whatIsThis) { + } + + @EventListener + public void noParameter() { + } + + @EventListener + public void moreThanOneParameter(String foo, Integer bar) { + } + + @EventListener + public void generateRuntimeException(GenericTestEvent event) { + if ("fail".equals(event.getPayload())) { + throw new IllegalStateException("Test exception"); + } + } + + @EventListener + public void generateCheckedException(GenericTestEvent event) throws IOException { + if ("fail".equals(event.getPayload())) { + throw new IOException("Test exception"); + } + } + } + + + interface SimpleService { + + void handleIt(ApplicationEvent event); + } + + + private static class EntityWrapper implements ResolvableTypeProvider { + + private final T entity; + + public EntityWrapper(T entity) { + this.entity = entity; + } + + @Override + public ResolvableType getResolvableType() { + return ResolvableType.forClassWithGenerics(getClass(), this.entity.getClass()); + } + } + + + static class InvalidProxyTestBean implements SimpleService { + + @Override + public void handleIt(ApplicationEvent event) { + } + + @EventListener + public void handleIt2(ApplicationEvent event) { + } + } + + + @SuppressWarnings({"unused", "serial"}) + static class PayloadTestEvent extends PayloadApplicationEvent { + + private final V something; + + public PayloadTestEvent(Object source, T payload, V something) { + super(source, payload); + this.something = something; + } + } + + + @SuppressWarnings({ "serial" }) + static class PayloadStringTestEvent extends PayloadTestEvent { + + public PayloadStringTestEvent(Object source, String payload, Long something) { + super(source, payload, something); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/event/EventPublicationInterceptorTests.java b/spring-context/src/test/java/org/springframework/context/event/EventPublicationInterceptorTests.java new file mode 100644 index 0000000..ace9382 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/EventPublicationInterceptorTests.java @@ -0,0 +1,149 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.event.test.TestEvent; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.context.testfixture.beans.TestApplicationListener; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.mock; + +/** + * @author Dmitriy Kopylenko + * @author Juergen Hoeller + * @author Rick Evans + */ +public class EventPublicationInterceptorTests { + + private final ApplicationEventPublisher publisher = mock(ApplicationEventPublisher.class); + + + @Test + public void testWithNoApplicationEventClassSupplied() { + EventPublicationInterceptor interceptor = new EventPublicationInterceptor(); + interceptor.setApplicationEventPublisher(this.publisher); + assertThatIllegalArgumentException().isThrownBy( + interceptor::afterPropertiesSet); + } + + @Test + public void testWithNonApplicationEventClassSupplied() { + EventPublicationInterceptor interceptor = new EventPublicationInterceptor(); + interceptor.setApplicationEventPublisher(this.publisher); + assertThatIllegalArgumentException().isThrownBy(() -> { + interceptor.setApplicationEventClass(getClass()); + interceptor.afterPropertiesSet(); + }); + } + + @Test + public void testWithAbstractStraightApplicationEventClassSupplied() { + EventPublicationInterceptor interceptor = new EventPublicationInterceptor(); + interceptor.setApplicationEventPublisher(this.publisher); + assertThatIllegalArgumentException().isThrownBy(() -> { + interceptor.setApplicationEventClass(ApplicationEvent.class); + interceptor.afterPropertiesSet(); + }); + } + + @Test + public void testWithApplicationEventClassThatDoesntExposeAValidCtor() { + EventPublicationInterceptor interceptor = new EventPublicationInterceptor(); + interceptor.setApplicationEventPublisher(this.publisher); + assertThatIllegalArgumentException().isThrownBy(() -> { + interceptor.setApplicationEventClass(TestEventWithNoValidOneArgObjectCtor.class); + interceptor.afterPropertiesSet(); + }); + } + + @Test + public void testExpectedBehavior() { + TestBean target = new TestBean(); + final TestApplicationListener listener = new TestApplicationListener(); + + class TestContext extends StaticApplicationContext { + @Override + protected void onRefresh() throws BeansException { + addApplicationListener(listener); + } + } + + StaticApplicationContext ctx = new TestContext(); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("applicationEventClass", TestEvent.class.getName()); + // should automatically receive applicationEventPublisher reference + ctx.registerSingleton("publisher", EventPublicationInterceptor.class, pvs); + ctx.registerSingleton("otherListener", FactoryBeanTestListener.class); + ctx.refresh(); + + EventPublicationInterceptor interceptor = + (EventPublicationInterceptor) ctx.getBean("publisher"); + ProxyFactory factory = new ProxyFactory(target); + factory.addAdvice(0, interceptor); + + ITestBean testBean = (ITestBean) factory.getProxy(); + + // invoke any method on the advised proxy to see if the interceptor has been invoked + testBean.getAge(); + + // two events: ContextRefreshedEvent and TestEvent + assertThat(listener.getEventCount() == 2).as("Interceptor must have published 2 events").isTrue(); + TestApplicationListener otherListener = (TestApplicationListener) ctx.getBean("&otherListener"); + assertThat(otherListener.getEventCount() == 2).as("Interceptor must have published 2 events").isTrue(); + } + + + @SuppressWarnings("serial") + public static final class TestEventWithNoValidOneArgObjectCtor extends ApplicationEvent { + + public TestEventWithNoValidOneArgObjectCtor() { + super(""); + } + } + + + public static class FactoryBeanTestListener extends TestApplicationListener implements FactoryBean { + + @Override + public Object getObject() { + return "test"; + } + + @Override + public Class getObjectType() { + return String.class; + } + + @Override + public boolean isSingleton() { + return true; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/event/GenericApplicationListenerAdapterTests.java b/spring-context/src/test/java/org/springframework/context/event/GenericApplicationListenerAdapterTests.java new file mode 100644 index 0000000..4ef4fde --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/GenericApplicationListenerAdapterTests.java @@ -0,0 +1,158 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.core.ResolvableType; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author Stephane Nicoll + */ +public class GenericApplicationListenerAdapterTests extends AbstractApplicationEventListenerTests { + + @Test + public void supportsEventTypeWithSmartApplicationListener() { + SmartApplicationListener smartListener = mock(SmartApplicationListener.class); + GenericApplicationListenerAdapter listener = new GenericApplicationListenerAdapter(smartListener); + ResolvableType type = ResolvableType.forClass(ApplicationEvent.class); + listener.supportsEventType(type); + verify(smartListener, times(1)).supportsEventType(ApplicationEvent.class); + } + + @Test + public void supportsSourceTypeWithSmartApplicationListener() { + SmartApplicationListener smartListener = mock(SmartApplicationListener.class); + GenericApplicationListenerAdapter listener = new GenericApplicationListenerAdapter(smartListener); + listener.supportsSourceType(Object.class); + verify(smartListener, times(1)).supportsSourceType(Object.class); + } + + @Test + public void genericListenerStrictType() { + supportsEventType(true, StringEventListener.class, ResolvableType.forClassWithGenerics(GenericTestEvent.class, String.class)); + } + + @Test // Demonstrates we can't inject that event because the generic type is lost + public void genericListenerStrictTypeTypeErasure() { + GenericTestEvent stringEvent = createGenericTestEvent("test"); + ResolvableType eventType = ResolvableType.forType(stringEvent.getClass()); + supportsEventType(false, StringEventListener.class, eventType); + } + + @Test // But it works if we specify the type properly + public void genericListenerStrictTypeAndResolvableType() { + ResolvableType eventType = ResolvableType + .forClassWithGenerics(GenericTestEvent.class, String.class); + supportsEventType(true, StringEventListener.class, eventType); + } + + @Test // or if the event provides its precise type + public void genericListenerStrictTypeAndResolvableTypeProvider() { + ResolvableType eventType = new SmartGenericTestEvent<>(this, "foo").getResolvableType(); + supportsEventType(true, StringEventListener.class, eventType); + } + + @Test // Demonstrates it works if we actually use the subtype + public void genericListenerStrictTypeEventSubType() { + StringEvent stringEvent = new StringEvent(this, "test"); + ResolvableType eventType = ResolvableType.forType(stringEvent.getClass()); + supportsEventType(true, StringEventListener.class, eventType); + } + + @Test + public void genericListenerStrictTypeNotMatching() { + supportsEventType(false, StringEventListener.class, ResolvableType.forClassWithGenerics(GenericTestEvent.class, Long.class)); + } + + @Test + public void genericListenerStrictTypeEventSubTypeNotMatching() { + LongEvent stringEvent = new LongEvent(this, 123L); + ResolvableType eventType = ResolvableType.forType(stringEvent.getClass()); + supportsEventType(false, StringEventListener.class, eventType); + } + + @Test + public void genericListenerStrictTypeNotMatchTypeErasure() { + GenericTestEvent longEvent = createGenericTestEvent(123L); + ResolvableType eventType = ResolvableType.forType(longEvent.getClass()); + supportsEventType(false, StringEventListener.class, eventType); + } + + @Test + public void genericListenerStrictTypeSubClass() { + supportsEventType(false, ObjectEventListener.class, ResolvableType.forClassWithGenerics(GenericTestEvent.class, Long.class)); + } + + @Test + public void genericListenerUpperBoundType() { + supportsEventType(true, UpperBoundEventListener.class, + ResolvableType.forClassWithGenerics(GenericTestEvent.class, IllegalStateException.class)); + } + + @Test + public void genericListenerUpperBoundTypeNotMatching() { + supportsEventType(false, UpperBoundEventListener.class, + ResolvableType.forClassWithGenerics(GenericTestEvent.class, IOException.class)); + } + + @Test + public void genericListenerWildcardType() { + supportsEventType(true, GenericEventListener.class, + ResolvableType.forClassWithGenerics(GenericTestEvent.class, String.class)); + } + + @Test // Demonstrates we cant inject that event because the listener has a wildcard + public void genericListenerWildcardTypeTypeErasure() { + GenericTestEvent stringEvent = createGenericTestEvent("test"); + ResolvableType eventType = ResolvableType.forType(stringEvent.getClass()); + supportsEventType(true, GenericEventListener.class, eventType); + } + + @Test + public void genericListenerRawType() { + supportsEventType(true, RawApplicationListener.class, + ResolvableType.forClassWithGenerics(GenericTestEvent.class, String.class)); + } + + @Test // Demonstrates we cant inject that event because the listener has a raw type + public void genericListenerRawTypeTypeErasure() { + GenericTestEvent stringEvent = createGenericTestEvent("test"); + ResolvableType eventType = ResolvableType.forType(stringEvent.getClass()); + supportsEventType(true, RawApplicationListener.class, eventType); + } + + + @SuppressWarnings("rawtypes") + private void supportsEventType( + boolean match, Class listenerType, ResolvableType eventType) { + + ApplicationListener listener = mock(listenerType); + GenericApplicationListenerAdapter adapter = new GenericApplicationListenerAdapter(listener); + assertThat(adapter.supportsEventType(eventType)).as("Wrong match for event '" + eventType + "' on " + listenerType.getClass().getName()).isEqualTo(match); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/event/LifecycleEventTests.java b/spring-context/src/test/java/org/springframework/context/event/LifecycleEventTests.java new file mode 100644 index 0000000..5c4917c --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/LifecycleEventTests.java @@ -0,0 +1,124 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.Lifecycle; +import org.springframework.context.support.StaticApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Mark Fisher + * @author Juergen Hoeller + */ +public class LifecycleEventTests { + + @Test + public void contextStartedEvent() { + StaticApplicationContext context = new StaticApplicationContext(); + context.registerSingleton("lifecycle", LifecycleTestBean.class); + context.registerSingleton("listener", LifecycleListener.class); + context.refresh(); + LifecycleTestBean lifecycleBean = (LifecycleTestBean) context.getBean("lifecycle"); + LifecycleListener listener = (LifecycleListener) context.getBean("listener"); + assertThat(lifecycleBean.isRunning()).isFalse(); + assertThat(listener.getStartedCount()).isEqualTo(0); + context.start(); + assertThat(lifecycleBean.isRunning()).isTrue(); + assertThat(listener.getStartedCount()).isEqualTo(1); + assertThat(listener.getApplicationContext()).isSameAs(context); + } + + @Test + public void contextStoppedEvent() { + StaticApplicationContext context = new StaticApplicationContext(); + context.registerSingleton("lifecycle", LifecycleTestBean.class); + context.registerSingleton("listener", LifecycleListener.class); + context.refresh(); + LifecycleTestBean lifecycleBean = (LifecycleTestBean) context.getBean("lifecycle"); + LifecycleListener listener = (LifecycleListener) context.getBean("listener"); + assertThat(lifecycleBean.isRunning()).isFalse(); + context.start(); + assertThat(lifecycleBean.isRunning()).isTrue(); + assertThat(listener.getStoppedCount()).isEqualTo(0); + context.stop(); + assertThat(lifecycleBean.isRunning()).isFalse(); + assertThat(listener.getStoppedCount()).isEqualTo(1); + assertThat(listener.getApplicationContext()).isSameAs(context); + } + + + private static class LifecycleListener implements ApplicationListener { + + private ApplicationContext context; + + private int startedCount; + + private int stoppedCount; + + @Override + public void onApplicationEvent(ApplicationEvent event) { + if (event instanceof ContextStartedEvent) { + this.context = ((ContextStartedEvent) event).getApplicationContext(); + this.startedCount++; + } + else if (event instanceof ContextStoppedEvent) { + this.context = ((ContextStoppedEvent) event).getApplicationContext(); + this.stoppedCount++; + } + } + + public ApplicationContext getApplicationContext() { + return this.context; + } + + public int getStartedCount() { + return this.startedCount; + } + + public int getStoppedCount() { + return this.stoppedCount; + } + } + + + private static class LifecycleTestBean implements Lifecycle { + + private boolean running; + + @Override + public boolean isRunning() { + return this.running; + } + + @Override + public void start() { + this.running = true; + } + + @Override + public void stop() { + this.running = false; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/event/PayloadApplicationEventTests.java b/spring-context/src/test/java/org/springframework/context/event/PayloadApplicationEventTests.java new file mode 100644 index 0000000..39db32f --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/PayloadApplicationEventTests.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.PayloadApplicationEvent; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.stereotype.Component; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + */ +public class PayloadApplicationEventTests { + + @Test + public void testEventClassWithInterface() { + ApplicationContext ac = new AnnotationConfigApplicationContext(AuditableListener.class); + + AuditablePayloadEvent event = new AuditablePayloadEvent<>(this, "xyz"); + ac.publishEvent(event); + assertThat(ac.getBean(AuditableListener.class).events.contains(event)).isTrue(); + } + + @Test + public void testProgrammaticEventListener() { + List events = new ArrayList<>(); + ApplicationListener> listener = events::add; + + ConfigurableApplicationContext ac = new GenericApplicationContext(); + ac.addApplicationListener(listener); + ac.refresh(); + + AuditablePayloadEvent event = new AuditablePayloadEvent<>(this, "xyz"); + ac.publishEvent(event); + assertThat(events.contains(event)).isTrue(); + } + + @Test + public void testProgrammaticPayloadListener() { + List events = new ArrayList<>(); + ApplicationListener> listener = ApplicationListener.forPayload(events::add); + + ConfigurableApplicationContext ac = new GenericApplicationContext(); + ac.addApplicationListener(listener); + ac.refresh(); + + AuditablePayloadEvent event = new AuditablePayloadEvent<>(this, "xyz"); + ac.publishEvent(event); + assertThat(events.contains(event.getPayload())).isTrue(); + } + + + public interface Auditable { + } + + + @SuppressWarnings("serial") + public static class AuditablePayloadEvent extends PayloadApplicationEvent implements Auditable { + + public AuditablePayloadEvent(Object source, T payload) { + super(source, payload); + } + } + + + @Component + public static class AuditableListener { + + public final List events = new ArrayList<>(); + + @EventListener + public void onEvent(Auditable event) { + events.add(event); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/event/test/AbstractIdentifiable.java b/spring-context/src/test/java/org/springframework/context/event/test/AbstractIdentifiable.java new file mode 100644 index 0000000..7dd8f26 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/test/AbstractIdentifiable.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event.test; + +import java.util.UUID; + +/** + * @author Stephane Nicoll + */ +public abstract class AbstractIdentifiable implements Identifiable { + + private final String id; + + public AbstractIdentifiable() { + this.id = UUID.randomUUID().toString(); + } + + @Override + public String getId() { + return this.id; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + AbstractIdentifiable that = (AbstractIdentifiable) o; + + return this.id.equals(that.id); + } + + @Override + public int hashCode() { + return this.id.hashCode(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/event/test/AnotherTestEvent.java b/spring-context/src/test/java/org/springframework/context/event/test/AnotherTestEvent.java new file mode 100644 index 0000000..4e3e48e --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/test/AnotherTestEvent.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event.test; + +/** + * @author Stephane Nicoll + */ +@SuppressWarnings("serial") +public class AnotherTestEvent extends IdentifiableApplicationEvent { + + public final Object content; + + public AnotherTestEvent(Object source, Object content) { + super(source); + this.content = content; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/event/test/EventCollector.java b/spring-context/src/test/java/org/springframework/context/event/test/EventCollector.java new file mode 100644 index 0000000..fdd8fa8 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/test/EventCollector.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event.test; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test utility to collect and assert events. + * + * @author Stephane Nicoll + * @author Juergen Hoeller + */ +@Component +public class EventCollector { + + private final MultiValueMap content = new LinkedMultiValueMap<>(); + + + /** + * Register an event for the specified listener. + */ + public void addEvent(Identifiable listener, Object event) { + this.content.add(listener.getId(), event); + } + + /** + * Return the events that the specified listener has received. The list of events + * is ordered according to their reception order. + */ + public List getEvents(Identifiable listener) { + return this.content.get(listener.getId()); + } + + /** + * Assert that the listener identified by the specified id has not received any event. + */ + public void assertNoEventReceived(String listenerId) { + List events = this.content.getOrDefault(listenerId, Collections.emptyList()); + assertThat(events.size()).as("Expected no events but got " + events).isEqualTo(0); + } + + /** + * Assert that the specified listener has not received any event. + */ + public void assertNoEventReceived(Identifiable listener) { + assertNoEventReceived(listener.getId()); + } + + /** + * Assert that the listener identified by the specified id has received the + * specified events, in that specific order. + */ + public void assertEvent(String listenerId, Object... events) { + List actual = this.content.getOrDefault(listenerId, Collections.emptyList()); + assertThat(actual.size()).as("Wrong number of events").isEqualTo(events.length); + for (int i = 0; i < events.length; i++) { + assertThat(actual.get(i)).as("Wrong event at index " + i).isEqualTo(events[i]); + } + } + + /** + * Assert that the specified listener has received the specified events, in + * that specific order. + */ + public void assertEvent(Identifiable listener, Object... events) { + assertEvent(listener.getId(), events); + } + + /** + * Assert the number of events received by this instance. Checks that + * unexpected events have not been received. If an event is handled by + * several listeners, each instance will be registered. + */ + public void assertTotalEventsCount(int number) { + int actual = 0; + for (Map.Entry> entry : this.content.entrySet()) { + actual += entry.getValue().size(); + } + assertThat(actual).as("Wrong number of total events (" + this.content.size() + + ") registered listener(s)").isEqualTo(number); + } + + /** + * Clear the collected events, allowing for reuse of the collector. + */ + public void clear() { + this.content.clear(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/event/test/GenericEventPojo.java b/spring-context/src/test/java/org/springframework/context/event/test/GenericEventPojo.java new file mode 100644 index 0000000..b2d1bc6 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/test/GenericEventPojo.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event.test; + +import org.springframework.core.ResolvableType; +import org.springframework.core.ResolvableTypeProvider; + +/** + * A simple POJO that implements {@link ResolvableTypeProvider}. + * + * @author Stephane Nicoll + */ +public class GenericEventPojo implements ResolvableTypeProvider { + private final T value; + + public GenericEventPojo(T value) { + this.value = value; + } + + @Override + public ResolvableType getResolvableType() { + return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(this.value)); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + GenericEventPojo that = (GenericEventPojo) o; + + return this.value.equals(that.value); + } + + @Override + public int hashCode() { + return this.value.hashCode(); + } +} diff --git a/spring-context/src/test/java/org/springframework/context/event/test/Identifiable.java b/spring-context/src/test/java/org/springframework/context/event/test/Identifiable.java new file mode 100644 index 0000000..397fece --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/test/Identifiable.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event.test; + +/** + * A simple marker interface used to identify an event or an event listener + * + * @author Stephane Nicoll + */ +public interface Identifiable { + + /** + * Return a unique global id used to identify this instance. + */ + String getId(); + +} diff --git a/spring-context/src/test/java/org/springframework/context/event/test/IdentifiableApplicationEvent.java b/spring-context/src/test/java/org/springframework/context/event/test/IdentifiableApplicationEvent.java new file mode 100644 index 0000000..b225179 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/test/IdentifiableApplicationEvent.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event.test; + +import java.util.UUID; + +import org.springframework.context.ApplicationEvent; + +/** + * A basic test event that can be uniquely identified easily. + * + * @author Stephane Nicoll + */ +@SuppressWarnings("serial") +public abstract class IdentifiableApplicationEvent extends ApplicationEvent implements Identifiable { + + private final String id; + + protected IdentifiableApplicationEvent(Object source, String id) { + super(source); + this.id = id; + } + + protected IdentifiableApplicationEvent(Object source) { + this(source, UUID.randomUUID().toString()); + } + + protected IdentifiableApplicationEvent() { + this(new Object()); + } + + @Override + public String getId() { + return this.id; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + IdentifiableApplicationEvent that = (IdentifiableApplicationEvent) o; + + return this.id.equals(that.id); + + } + + @Override + public int hashCode() { + return this.id.hashCode(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/event/test/TestEvent.java b/spring-context/src/test/java/org/springframework/context/event/test/TestEvent.java new file mode 100644 index 0000000..f2687a6 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/test/TestEvent.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.event.test; + +/** + * @author Stephane Nicoll + */ +@SuppressWarnings("serial") +public class TestEvent extends IdentifiableApplicationEvent { + + public final String msg; + + public TestEvent(Object source, String id, String msg) { + super(source, id); + this.msg = msg; + } + + public TestEvent(Object source, String msg) { + super(source); + this.msg = msg; + } + + public TestEvent(Object source) { + this(source, "test"); + } + + public TestEvent() { + this(new Object()); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/expression/AnnotatedElementKeyTests.java b/spring-context/src/test/java/org/springframework/context/expression/AnnotatedElementKeyTests.java new file mode 100644 index 0000000..6726f65 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/expression/AnnotatedElementKeyTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.expression; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; + +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Stephane Nicoll + * @author Sam Brannen + */ +class AnnotatedElementKeyTests { + + private Method method; + + @BeforeEach + void setUpMethod(TestInfo testInfo) { + this.method = ReflectionUtils.findMethod(getClass(), testInfo.getTestMethod().get().getName()); + } + + @Test + void sameInstanceEquals() { + AnnotatedElementKey instance = new AnnotatedElementKey(this.method, getClass()); + + assertKeyEquals(instance, instance); + } + + @Test + void equals() { + AnnotatedElementKey first = new AnnotatedElementKey(this.method, getClass()); + AnnotatedElementKey second = new AnnotatedElementKey(this.method, getClass()); + + assertKeyEquals(first, second); + } + + @Test + void equalsNoTarget() { + AnnotatedElementKey first = new AnnotatedElementKey(this.method, null); + AnnotatedElementKey second = new AnnotatedElementKey(this.method, null); + + assertKeyEquals(first, second); + } + + @Test + void noTargetClassNotEquals() { + AnnotatedElementKey first = new AnnotatedElementKey(this.method, getClass()); + AnnotatedElementKey second = new AnnotatedElementKey(this.method, null); + + assertThat(first.equals(second)).isFalse(); + } + + private void assertKeyEquals(AnnotatedElementKey first, AnnotatedElementKey second) { + assertThat(second).isEqualTo(first); + assertThat(second.hashCode()).isEqualTo(first.hashCode()); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/expression/ApplicationContextExpressionTests.java b/spring-context/src/test/java/org/springframework/context/expression/ApplicationContextExpressionTests.java new file mode 100644 index 0000000..e3c9921 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/expression/ApplicationContextExpressionTests.java @@ -0,0 +1,485 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.expression; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.Serializable; +import java.net.URI; +import java.net.URL; +import java.security.AccessControlException; +import java.security.Permission; +import java.util.Optional; +import java.util.Properties; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.Scope; +import org.springframework.beans.factory.config.TypedStringValue; +import org.springframework.beans.factory.support.AutowireCandidateQualifier; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.AnnotationConfigUtils; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.core.testfixture.io.SerializationTestUtils; +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @author Sam Brannen + * @since 3.0 + */ +class ApplicationContextExpressionTests { + + private static final Log factoryLog = LogFactory.getLog(DefaultListableBeanFactory.class); + + + @Test + @SuppressWarnings("deprecation") + void genericApplicationContext() throws Exception { + GenericApplicationContext ac = new GenericApplicationContext(); + AnnotationConfigUtils.registerAnnotationConfigProcessors(ac); + + ac.getBeanFactory().registerScope("myScope", new Scope() { + @Override + public Object get(String name, ObjectFactory objectFactory) { + return objectFactory.getObject(); + } + @Override + public Object remove(String name) { + return null; + } + @Override + public void registerDestructionCallback(String name, Runnable callback) { + } + @Override + public Object resolveContextualObject(String key) { + if (key.equals("mySpecialAttr")) { + return "42"; + } + else { + return null; + } + } + @Override + public String getConversationId() { + return null; + } + }); + + ac.getBeanFactory().setConversionService(new DefaultConversionService()); + + org.springframework.beans.factory.config.PropertyPlaceholderConfigurer ppc = + new org.springframework.beans.factory.config.PropertyPlaceholderConfigurer(); + Properties placeholders = new Properties(); + placeholders.setProperty("code", "123"); + ppc.setProperties(placeholders); + ac.addBeanFactoryPostProcessor(ppc); + + GenericBeanDefinition bd0 = new GenericBeanDefinition(); + bd0.setBeanClass(TestBean.class); + bd0.getPropertyValues().add("name", "myName"); + bd0.addQualifier(new AutowireCandidateQualifier(Qualifier.class, "original")); + ac.registerBeanDefinition("tb0", bd0); + + GenericBeanDefinition bd1 = new GenericBeanDefinition(); + bd1.setBeanClassName("#{tb0.class}"); + bd1.setScope("myScope"); + bd1.getConstructorArgumentValues().addGenericArgumentValue("XXX#{tb0.name}YYY#{mySpecialAttr}ZZZ"); + bd1.getConstructorArgumentValues().addGenericArgumentValue("#{mySpecialAttr}"); + ac.registerBeanDefinition("tb1", bd1); + + GenericBeanDefinition bd2 = new GenericBeanDefinition(); + bd2.setBeanClassName("#{tb1.class.name}"); + bd2.setScope("myScope"); + bd2.getPropertyValues().add("name", "{ XXX#{tb0.name}YYY#{mySpecialAttr}ZZZ }"); + bd2.getPropertyValues().add("age", "#{mySpecialAttr}"); + bd2.getPropertyValues().add("country", "${code} #{systemProperties.country}"); + ac.registerBeanDefinition("tb2", bd2); + + GenericBeanDefinition bd3 = new GenericBeanDefinition(); + bd3.setBeanClass(ValueTestBean.class); + bd3.setScope("myScope"); + ac.registerBeanDefinition("tb3", bd3); + + GenericBeanDefinition bd4 = new GenericBeanDefinition(); + bd4.setBeanClass(ConstructorValueTestBean.class); + bd4.setScope("myScope"); + ac.registerBeanDefinition("tb4", bd4); + + GenericBeanDefinition bd5 = new GenericBeanDefinition(); + bd5.setBeanClass(MethodValueTestBean.class); + bd5.setScope("myScope"); + ac.registerBeanDefinition("tb5", bd5); + + GenericBeanDefinition bd6 = new GenericBeanDefinition(); + bd6.setBeanClass(PropertyValueTestBean.class); + bd6.setScope("myScope"); + ac.registerBeanDefinition("tb6", bd6); + + System.getProperties().put("country", "UK"); + try { + ac.refresh(); + + TestBean tb0 = ac.getBean("tb0", TestBean.class); + + TestBean tb1 = ac.getBean("tb1", TestBean.class); + assertThat(tb1.getName()).isEqualTo("XXXmyNameYYY42ZZZ"); + assertThat(tb1.getAge()).isEqualTo(42); + + TestBean tb2 = ac.getBean("tb2", TestBean.class); + assertThat(tb2.getName()).isEqualTo("{ XXXmyNameYYY42ZZZ }"); + assertThat(tb2.getAge()).isEqualTo(42); + assertThat(tb2.getCountry()).isEqualTo("123 UK"); + + ValueTestBean tb3 = ac.getBean("tb3", ValueTestBean.class); + assertThat(tb3.name).isEqualTo("XXXmyNameYYY42ZZZ"); + assertThat(tb3.age).isEqualTo(42); + assertThat(tb3.ageFactory.getObject().intValue()).isEqualTo(42); + assertThat(tb3.country).isEqualTo("123 UK"); + assertThat(tb3.countryFactory.getObject()).isEqualTo("123 UK"); + System.getProperties().put("country", "US"); + assertThat(tb3.country).isEqualTo("123 UK"); + assertThat(tb3.countryFactory.getObject()).isEqualTo("123 US"); + System.getProperties().put("country", "UK"); + assertThat(tb3.country).isEqualTo("123 UK"); + assertThat(tb3.countryFactory.getObject()).isEqualTo("123 UK"); + assertThat(tb3.optionalValue1.get()).isEqualTo("123"); + assertThat(tb3.optionalValue2.get()).isEqualTo("123"); + assertThat(tb3.optionalValue3.isPresent()).isFalse(); + assertThat(tb3.tb).isSameAs(tb0); + + tb3 = SerializationTestUtils.serializeAndDeserialize(tb3); + assertThat(tb3.countryFactory.getObject()).isEqualTo("123 UK"); + + ConstructorValueTestBean tb4 = ac.getBean("tb4", ConstructorValueTestBean.class); + assertThat(tb4.name).isEqualTo("XXXmyNameYYY42ZZZ"); + assertThat(tb4.age).isEqualTo(42); + assertThat(tb4.country).isEqualTo("123 UK"); + assertThat(tb4.tb).isSameAs(tb0); + + MethodValueTestBean tb5 = ac.getBean("tb5", MethodValueTestBean.class); + assertThat(tb5.name).isEqualTo("XXXmyNameYYY42ZZZ"); + assertThat(tb5.age).isEqualTo(42); + assertThat(tb5.country).isEqualTo("123 UK"); + assertThat(tb5.tb).isSameAs(tb0); + + PropertyValueTestBean tb6 = ac.getBean("tb6", PropertyValueTestBean.class); + assertThat(tb6.name).isEqualTo("XXXmyNameYYY42ZZZ"); + assertThat(tb6.age).isEqualTo(42); + assertThat(tb6.country).isEqualTo("123 UK"); + assertThat(tb6.tb).isSameAs(tb0); + } + finally { + System.getProperties().remove("country"); + } + } + + @Test + void prototypeCreationReevaluatesExpressions() { + GenericApplicationContext ac = new GenericApplicationContext(); + AnnotationConfigUtils.registerAnnotationConfigProcessors(ac); + GenericConversionService cs = new GenericConversionService(); + cs.addConverter(String.class, String.class, String::trim); + ac.getBeanFactory().registerSingleton(GenericApplicationContext.CONVERSION_SERVICE_BEAN_NAME, cs); + RootBeanDefinition rbd = new RootBeanDefinition(PrototypeTestBean.class); + rbd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + rbd.getPropertyValues().add("country", "#{systemProperties.country}"); + rbd.getPropertyValues().add("country2", new TypedStringValue("-#{systemProperties.country}-")); + ac.registerBeanDefinition("test", rbd); + ac.refresh(); + + try { + System.getProperties().put("name", "juergen1"); + System.getProperties().put("country", " UK1 "); + PrototypeTestBean tb = (PrototypeTestBean) ac.getBean("test"); + assertThat(tb.getName()).isEqualTo("juergen1"); + assertThat(tb.getCountry()).isEqualTo("UK1"); + assertThat(tb.getCountry2()).isEqualTo("-UK1-"); + + System.getProperties().put("name", "juergen2"); + System.getProperties().put("country", " UK2 "); + tb = (PrototypeTestBean) ac.getBean("test"); + assertThat(tb.getName()).isEqualTo("juergen2"); + assertThat(tb.getCountry()).isEqualTo("UK2"); + assertThat(tb.getCountry2()).isEqualTo("-UK2-"); + } + finally { + System.getProperties().remove("name"); + System.getProperties().remove("country"); + } + } + + @Test + void systemPropertiesSecurityManager() { + AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(); + + GenericBeanDefinition bd = new GenericBeanDefinition(); + bd.setBeanClass(TestBean.class); + bd.getPropertyValues().add("country", "#{systemProperties.country}"); + ac.registerBeanDefinition("tb", bd); + + SecurityManager oldSecurityManager = System.getSecurityManager(); + try { + System.setProperty("country", "NL"); + + SecurityManager securityManager = new SecurityManager() { + @Override + public void checkPropertiesAccess() { + throw new AccessControlException("Not Allowed"); + } + @Override + public void checkPermission(Permission perm) { + // allow everything else + } + }; + System.setSecurityManager(securityManager); + ac.refresh(); + + TestBean tb = ac.getBean("tb", TestBean.class); + assertThat(tb.getCountry()).isEqualTo("NL"); + + } + finally { + System.setSecurityManager(oldSecurityManager); + System.getProperties().remove("country"); + } + ac.close(); + } + + @Test + void stringConcatenationWithDebugLogging() { + AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(); + + GenericBeanDefinition bd = new GenericBeanDefinition(); + bd.setBeanClass(String.class); + bd.getConstructorArgumentValues().addGenericArgumentValue("test-#{ T(java.lang.System).currentTimeMillis() }"); + ac.registerBeanDefinition("str", bd); + ac.refresh(); + + String str = ac.getBean("str", String.class); + assertThat(str.startsWith("test-")).isTrue(); + ac.close(); + } + + @Test + void resourceInjection() throws IOException { + System.setProperty("logfile", "do_not_delete_me.txt"); + try (AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ResourceInjectionBean.class)) { + ResourceInjectionBean resourceInjectionBean = ac.getBean(ResourceInjectionBean.class); + Resource resource = new ClassPathResource("do_not_delete_me.txt"); + assertThat(resourceInjectionBean.resource).isEqualTo(resource); + assertThat(resourceInjectionBean.url).isEqualTo(resource.getURL()); + assertThat(resourceInjectionBean.uri).isEqualTo(resource.getURI()); + assertThat(resourceInjectionBean.file).isEqualTo(resource.getFile()); + assertThat(FileCopyUtils.copyToByteArray(resourceInjectionBean.inputStream)).isEqualTo(FileCopyUtils.copyToByteArray(resource.getInputStream())); + assertThat(FileCopyUtils.copyToString(resourceInjectionBean.reader)).isEqualTo(FileCopyUtils.copyToString(new EncodedResource(resource).getReader())); + } + finally { + System.getProperties().remove("logfile"); + } + } + + + @SuppressWarnings("serial") + public static class ValueTestBean implements Serializable { + + @Autowired @Value("XXX#{tb0.name}YYY#{mySpecialAttr}ZZZ") + public String name; + + @Autowired @Value("#{mySpecialAttr}") + public int age; + + @Value("#{mySpecialAttr}") + public ObjectFactory ageFactory; + + @Value("${code} #{systemProperties.country}") + public String country; + + @Value("${code} #{systemProperties.country}") + public ObjectFactory countryFactory; + + @Value("${code}") + private transient Optional optionalValue1; + + @Value("${code:#{null}}") + private transient Optional optionalValue2; + + @Value("${codeX:#{null}}") + private transient Optional optionalValue3; + + @Autowired @Qualifier("original") + public transient TestBean tb; + } + + + public static class ConstructorValueTestBean { + + public String name; + + public int age; + + public String country; + + public TestBean tb; + + @Autowired + public ConstructorValueTestBean( + @Value("XXX#{tb0.name}YYY#{mySpecialAttr}ZZZ") String name, + @Value("#{mySpecialAttr}") int age, + @Qualifier("original") TestBean tb, + @Value("${code} #{systemProperties.country}") String country) { + this.name = name; + this.age = age; + this.country = country; + this.tb = tb; + } + } + + + public static class MethodValueTestBean { + + public String name; + + public int age; + + public String country; + + public TestBean tb; + + @Autowired + public void configure( + @Qualifier("original") TestBean tb, + @Value("XXX#{tb0.name}YYY#{mySpecialAttr}ZZZ") String name, + @Value("#{mySpecialAttr}") int age, + @Value("${code} #{systemProperties.country}") String country) { + this.name = name; + this.age = age; + this.country = country; + this.tb = tb; + } + } + + + public static class PropertyValueTestBean { + + public String name; + + public int age; + + public String country; + + public TestBean tb; + + @Value("XXX#{tb0.name}YYY#{mySpecialAttr}ZZZ") + public void setName(String name) { + this.name = name; + } + + @Value("#{mySpecialAttr}") + public void setAge(int age) { + this.age = age; + } + + @Value("${code} #{systemProperties.country}") + public void setCountry(String country) { + this.country = country; + } + + @Autowired @Qualifier("original") + public void setTb(TestBean tb) { + this.tb = tb; + } + } + + + public static class PrototypeTestBean { + + public String name; + + public String country; + + public String country2; + + @Value("#{systemProperties.name}") + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setCountry(String country) { + this.country = country; + } + + public String getCountry() { + return country; + } + + public void setCountry2(String country2) { + this.country2 = country2; + } + + public String getCountry2() { + return country2; + } + } + + + public static class ResourceInjectionBean { + + @Value("classpath:#{systemProperties.logfile}") + Resource resource; + + @Value("classpath:#{systemProperties.logfile}") + URL url; + + @Value("classpath:#{systemProperties.logfile}") + URI uri; + + @Value("classpath:#{systemProperties.logfile}") + File file; + + @Value("classpath:#{systemProperties.logfile}") + InputStream inputStream; + + @Value("classpath:#{systemProperties.logfile}") + Reader reader; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/expression/CachedExpressionEvaluatorTests.java b/spring-context/src/test/java/org/springframework/context/expression/CachedExpressionEvaluatorTests.java new file mode 100644 index 0000000..f81a794 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/expression/CachedExpressionEvaluatorTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.expression; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author Stephane Nicoll + */ +public class CachedExpressionEvaluatorTests { + + private final TestExpressionEvaluator expressionEvaluator = new TestExpressionEvaluator(); + + @Test + public void parseNewExpression() { + Method method = ReflectionUtils.findMethod(getClass(), "toString"); + Expression expression = expressionEvaluator.getTestExpression("true", method, getClass()); + hasParsedExpression("true"); + assertThat(expression.getValue()).isEqualTo(true); + assertThat(expressionEvaluator.testCache.size()).as("Expression should be in cache").isEqualTo(1); + } + + @Test + public void cacheExpression() { + Method method = ReflectionUtils.findMethod(getClass(), "toString"); + + expressionEvaluator.getTestExpression("true", method, getClass()); + expressionEvaluator.getTestExpression("true", method, getClass()); + expressionEvaluator.getTestExpression("true", method, getClass()); + hasParsedExpression("true"); + assertThat(expressionEvaluator.testCache.size()).as("Only one expression should be in cache").isEqualTo(1); + } + + @Test + public void cacheExpressionBasedOnConcreteType() { + Method method = ReflectionUtils.findMethod(getClass(), "toString"); + expressionEvaluator.getTestExpression("true", method, getClass()); + expressionEvaluator.getTestExpression("true", method, Object.class); + assertThat(expressionEvaluator.testCache.size()).as("Cached expression should be based on type").isEqualTo(2); + } + + private void hasParsedExpression(String expression) { + verify(expressionEvaluator.getParser(), times(1)).parseExpression(expression); + } + + private static class TestExpressionEvaluator extends CachedExpressionEvaluator { + + private final Map testCache = new ConcurrentHashMap<>(); + + public TestExpressionEvaluator() { + super(mockSpelExpressionParser()); + } + + public Expression getTestExpression(String expression, Method method, Class type) { + return getExpression(this.testCache, new AnnotatedElementKey(method, type), expression); + } + + private static SpelExpressionParser mockSpelExpressionParser() { + SpelExpressionParser parser = new SpelExpressionParser(); + return spy(parser); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/expression/EnvironmentAccessorIntegrationTests.java b/spring-context/src/test/java/org/springframework/context/expression/EnvironmentAccessorIntegrationTests.java new file mode 100644 index 0000000..93bd12a --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/expression/EnvironmentAccessorIntegrationTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.expression; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.support.AbstractApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.testfixture.env.MockPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.beans.factory.support.BeanDefinitionBuilder.genericBeanDefinition; + +/** + * Integration tests for {@link EnvironmentAccessor}, which is registered with + * SpEL for all {@link AbstractApplicationContext} implementations via + * {@link StandardBeanExpressionResolver}. + * + * @author Chris Beams + */ +public class EnvironmentAccessorIntegrationTests { + + @Test + public void braceAccess() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("testBean", + genericBeanDefinition(TestBean.class) + .addPropertyValue("name", "#{environment['my.name']}") + .getBeanDefinition()); + + GenericApplicationContext ctx = new GenericApplicationContext(bf); + ctx.getEnvironment().getPropertySources().addFirst(new MockPropertySource().withProperty("my.name", "myBean")); + ctx.refresh(); + + assertThat(ctx.getBean(TestBean.class).getName()).isEqualTo("myBean"); + ctx.close(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/expression/FactoryBeanAccessTests.java b/spring-context/src/test/java/org/springframework/context/expression/FactoryBeanAccessTests.java new file mode 100644 index 0000000..d184a07 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/expression/FactoryBeanAccessTests.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.expression; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanIsNotAFactoryException; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.expression.FactoryBeanAccessTests.SimpleBeanResolver.CarFactoryBean; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Unit tests for expressions accessing beans and factory beans. + * + * @author Andy Clement + */ +public class FactoryBeanAccessTests { + + @Test + public void factoryBeanAccess() { // SPR9511 + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setBeanResolver(new SimpleBeanResolver()); + Expression expr = new SpelExpressionParser().parseRaw("@car.colour"); + assertThat(expr.getValue(context)).isEqualTo("red"); + expr = new SpelExpressionParser().parseRaw("&car.class.name"); + assertThat(expr.getValue(context)).isEqualTo(CarFactoryBean.class.getName()); + + expr = new SpelExpressionParser().parseRaw("@boat.colour"); + assertThat(expr.getValue(context)).isEqualTo("blue"); + Expression notFactoryExpr = new SpelExpressionParser().parseRaw("&boat.class.name"); + assertThatExceptionOfType(BeanIsNotAFactoryException.class).isThrownBy(() -> + notFactoryExpr.getValue(context)); + + // No such bean + Expression noBeanExpr = new SpelExpressionParser().parseRaw("@truck"); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> + noBeanExpr.getValue(context)); + + // No such factory bean + Expression noFactoryBeanExpr = new SpelExpressionParser().parseRaw("&truck"); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> + noFactoryBeanExpr.getValue(context)); + } + + static class SimpleBeanResolver + implements org.springframework.expression.BeanResolver { + + static class Car { + + public String getColour() { + return "red"; + } + } + + static class CarFactoryBean implements FactoryBean { + + @Override + public Car getObject() { + return new Car(); + } + + @Override + public Class getObjectType() { + return Car.class; + } + + @Override + public boolean isSingleton() { + return false; + } + + } + + static class Boat { + + public String getColour() { + return "blue"; + } + + } + + StaticApplicationContext ac = new StaticApplicationContext(); + + public SimpleBeanResolver() { + ac.registerSingleton("car", CarFactoryBean.class); + ac.registerSingleton("boat", Boat.class); + } + + @Override + public Object resolve(EvaluationContext context, String beanName) + throws AccessException { + return ac.getBean(beanName); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/expression/MapAccessorTests.java b/spring-context/src/test/java/org/springframework/context/expression/MapAccessorTests.java new file mode 100644 index 0000000..df73ada --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/expression/MapAccessorTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.expression; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelCompiler; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for compilation of {@link MapAccessor}. + * + * @author Andy Clement + */ +public class MapAccessorTests { + + @Test + public void mapAccessorCompilable() { + Map testMap = getSimpleTestMap(); + StandardEvaluationContext sec = new StandardEvaluationContext(); + sec.addPropertyAccessor(new MapAccessor()); + SpelExpressionParser sep = new SpelExpressionParser(); + + // basic + Expression ex = sep.parseExpression("foo"); + assertThat(ex.getValue(sec,testMap)).isEqualTo("bar"); + assertThat(SpelCompiler.compile(ex)).isTrue(); + assertThat(ex.getValue(sec,testMap)).isEqualTo("bar"); + + // compound expression + ex = sep.parseExpression("foo.toUpperCase()"); + assertThat(ex.getValue(sec,testMap)).isEqualTo("BAR"); + assertThat(SpelCompiler.compile(ex)).isTrue(); + assertThat(ex.getValue(sec,testMap)).isEqualTo("BAR"); + + // nested map + Map> nestedMap = getNestedTestMap(); + ex = sep.parseExpression("aaa.foo.toUpperCase()"); + assertThat(ex.getValue(sec,nestedMap)).isEqualTo("BAR"); + assertThat(SpelCompiler.compile(ex)).isTrue(); + assertThat(ex.getValue(sec,nestedMap)).isEqualTo("BAR"); + + // avoiding inserting checkcast because first part of expression returns a Map + ex = sep.parseExpression("getMap().foo"); + MapGetter mapGetter = new MapGetter(); + assertThat(ex.getValue(sec,mapGetter)).isEqualTo("bar"); + assertThat(SpelCompiler.compile(ex)).isTrue(); + assertThat(ex.getValue(sec,mapGetter)).isEqualTo("bar"); + } + + public static class MapGetter { + Map map = new HashMap<>(); + + public MapGetter() { + map.put("foo", "bar"); + } + + @SuppressWarnings("rawtypes") + public Map getMap() { + return map; + } + } + + public Map getSimpleTestMap() { + Map map = new HashMap<>(); + map.put("foo","bar"); + return map; + } + + public Map> getNestedTestMap() { + Map map = new HashMap<>(); + map.put("foo","bar"); + Map> map2 = new HashMap<>(); + map2.put("aaa", map); + return map2; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/expression/MethodBasedEvaluationContextTests.java b/spring-context/src/test/java/org/springframework/context/expression/MethodBasedEvaluationContextTests.java new file mode 100644 index 0000000..de40c7d --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/expression/MethodBasedEvaluationContextTests.java @@ -0,0 +1,143 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.expression; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link MethodBasedEvaluationContext}. + * + * @author Stephane Nicoll + * @author Juergen Hoeller + * @author Sergey Podgurskiy + */ +public class MethodBasedEvaluationContextTests { + + private final ParameterNameDiscoverer paramDiscover = new DefaultParameterNameDiscoverer(); + + + @Test + public void simpleArguments() { + Method method = ReflectionUtils.findMethod(SampleMethods.class, "hello", String.class, Boolean.class); + MethodBasedEvaluationContext context = createEvaluationContext(method, "test", true); + + assertThat(context.lookupVariable("a0")).isEqualTo("test"); + assertThat(context.lookupVariable("p0")).isEqualTo("test"); + assertThat(context.lookupVariable("foo")).isEqualTo("test"); + + assertThat(context.lookupVariable("a1")).isEqualTo(true); + assertThat(context.lookupVariable("p1")).isEqualTo(true); + assertThat(context.lookupVariable("flag")).isEqualTo(true); + + assertThat(context.lookupVariable("a2")).isNull(); + assertThat(context.lookupVariable("p2")).isNull(); + } + + @Test + public void nullArgument() { + Method method = ReflectionUtils.findMethod(SampleMethods.class, "hello", String.class, Boolean.class); + MethodBasedEvaluationContext context = createEvaluationContext(method, null, null); + + assertThat(context.lookupVariable("a0")).isNull(); + assertThat(context.lookupVariable("p0")).isNull(); + assertThat(context.lookupVariable("foo")).isNull(); + + assertThat(context.lookupVariable("a1")).isNull(); + assertThat(context.lookupVariable("p1")).isNull(); + assertThat(context.lookupVariable("flag")).isNull(); + } + + @Test + public void varArgEmpty() { + Method method = ReflectionUtils.findMethod(SampleMethods.class, "hello", Boolean.class, String[].class); + MethodBasedEvaluationContext context = createEvaluationContext(method, new Object[] {null}); + + assertThat(context.lookupVariable("a0")).isNull(); + assertThat(context.lookupVariable("p0")).isNull(); + assertThat(context.lookupVariable("flag")).isNull(); + + assertThat(context.lookupVariable("a1")).isNull(); + assertThat(context.lookupVariable("p1")).isNull(); + assertThat(context.lookupVariable("vararg")).isNull(); + } + + @Test + public void varArgNull() { + Method method = ReflectionUtils.findMethod(SampleMethods.class, "hello", Boolean.class, String[].class); + MethodBasedEvaluationContext context = createEvaluationContext(method, null, null); + + assertThat(context.lookupVariable("a0")).isNull(); + assertThat(context.lookupVariable("p0")).isNull(); + assertThat(context.lookupVariable("flag")).isNull(); + + assertThat(context.lookupVariable("a1")).isNull(); + assertThat(context.lookupVariable("p1")).isNull(); + assertThat(context.lookupVariable("vararg")).isNull(); + } + + @Test + public void varArgSingle() { + Method method = ReflectionUtils.findMethod(SampleMethods.class, "hello", Boolean.class, String[].class); + MethodBasedEvaluationContext context = createEvaluationContext(method, null, "hello"); + + assertThat(context.lookupVariable("a0")).isNull(); + assertThat(context.lookupVariable("p0")).isNull(); + assertThat(context.lookupVariable("flag")).isNull(); + + assertThat(context.lookupVariable("a1")).isEqualTo("hello"); + assertThat(context.lookupVariable("p1")).isEqualTo("hello"); + assertThat(context.lookupVariable("vararg")).isEqualTo("hello"); + } + + @Test + public void varArgMultiple() { + Method method = ReflectionUtils.findMethod(SampleMethods.class, "hello", Boolean.class, String[].class); + MethodBasedEvaluationContext context = createEvaluationContext(method, null, "hello", "hi"); + + assertThat(context.lookupVariable("a0")).isNull(); + assertThat(context.lookupVariable("p0")).isNull(); + assertThat(context.lookupVariable("flag")).isNull(); + + assertThat(context.lookupVariable("a1")).isEqualTo(new Object[] {"hello", "hi"}); + assertThat(context.lookupVariable("p1")).isEqualTo(new Object[] {"hello", "hi"}); + assertThat(context.lookupVariable("vararg")).isEqualTo(new Object[] {"hello", "hi"}); + } + + private MethodBasedEvaluationContext createEvaluationContext(Method method, Object... args) { + return new MethodBasedEvaluationContext(this, method, args, this.paramDiscover); + } + + + @SuppressWarnings("unused") + private static class SampleMethods { + + private void hello(String foo, Boolean flag) { + } + + private void hello(Boolean flag, String... vararg){ + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/groovy/GroovyApplicationContextTests.java b/spring-context/src/test/java/org/springframework/context/groovy/GroovyApplicationContextTests.java new file mode 100644 index 0000000..c4caed2 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/groovy/GroovyApplicationContextTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.groovy; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.context.support.GenericGroovyApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Jeff Brown + * @author Juergen Hoeller + */ +public class GroovyApplicationContextTests { + + @Test + public void testLoadingConfigFile() { + GenericGroovyApplicationContext ctx = new GenericGroovyApplicationContext( + "org/springframework/context/groovy/applicationContext.groovy"); + + Object framework = ctx.getBean("framework"); + assertThat(framework).as("could not find framework bean").isNotNull(); + assertThat(framework).isEqualTo("Grails"); + } + + @Test + public void testLoadingMultipleConfigFiles() { + GenericGroovyApplicationContext ctx = new GenericGroovyApplicationContext( + "org/springframework/context/groovy/applicationContext2.groovy", + "org/springframework/context/groovy/applicationContext.groovy"); + + Object framework = ctx.getBean("framework"); + assertThat(framework).as("could not find framework bean").isNotNull(); + assertThat(framework).isEqualTo("Grails"); + + Object company = ctx.getBean("company"); + assertThat(company).as("could not find company bean").isNotNull(); + assertThat(company).isEqualTo("SpringSource"); + } + + @Test + public void testLoadingMultipleConfigFilesWithRelativeClass() { + GenericGroovyApplicationContext ctx = new GenericGroovyApplicationContext(); + ctx.load(GroovyApplicationContextTests.class, "applicationContext2.groovy", "applicationContext.groovy"); + ctx.refresh(); + + Object framework = ctx.getBean("framework"); + assertThat(framework).as("could not find framework bean").isNotNull(); + assertThat(framework).isEqualTo("Grails"); + + Object company = ctx.getBean("company"); + assertThat(company).as("could not find company bean").isNotNull(); + assertThat(company).isEqualTo("SpringSource"); + } + + @Test + public void testConfigFileParsingError() { + assertThatExceptionOfType(BeanDefinitionParsingException.class).isThrownBy(() -> + new GenericGroovyApplicationContext("org/springframework/context/groovy/applicationContext-error.groovy")); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/i18n/LocaleContextHolderTests.java b/spring-context/src/test/java/org/springframework/context/i18n/LocaleContextHolderTests.java new file mode 100644 index 0000000..2355c8c --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/i18n/LocaleContextHolderTests.java @@ -0,0 +1,175 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.i18n; + +import java.util.Locale; +import java.util.TimeZone; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + */ +public class LocaleContextHolderTests { + + @Test + public void testSetLocaleContext() { + LocaleContext lc = new SimpleLocaleContext(Locale.GERMAN); + LocaleContextHolder.setLocaleContext(lc); + assertThat(LocaleContextHolder.getLocaleContext()).isSameAs(lc); + assertThat(LocaleContextHolder.getLocale()).isEqualTo(Locale.GERMAN); + assertThat(LocaleContextHolder.getTimeZone()).isEqualTo(TimeZone.getDefault()); + + lc = new SimpleLocaleContext(Locale.GERMANY); + LocaleContextHolder.setLocaleContext(lc); + assertThat(LocaleContextHolder.getLocaleContext()).isSameAs(lc); + assertThat(LocaleContextHolder.getLocale()).isEqualTo(Locale.GERMANY); + assertThat(LocaleContextHolder.getTimeZone()).isEqualTo(TimeZone.getDefault()); + + LocaleContextHolder.resetLocaleContext(); + assertThat(LocaleContextHolder.getLocaleContext()).isNull(); + assertThat(LocaleContextHolder.getLocale()).isEqualTo(Locale.getDefault()); + assertThat(LocaleContextHolder.getTimeZone()).isEqualTo(TimeZone.getDefault()); + } + + @Test + public void testSetTimeZoneAwareLocaleContext() { + LocaleContext lc = new SimpleTimeZoneAwareLocaleContext(Locale.GERMANY, TimeZone.getTimeZone("GMT+1")); + LocaleContextHolder.setLocaleContext(lc); + assertThat(LocaleContextHolder.getLocaleContext()).isSameAs(lc); + assertThat(LocaleContextHolder.getLocale()).isEqualTo(Locale.GERMANY); + assertThat(LocaleContextHolder.getTimeZone()).isEqualTo(TimeZone.getTimeZone("GMT+1")); + + LocaleContextHolder.resetLocaleContext(); + assertThat(LocaleContextHolder.getLocaleContext()).isNull(); + assertThat(LocaleContextHolder.getLocale()).isEqualTo(Locale.getDefault()); + assertThat(LocaleContextHolder.getTimeZone()).isEqualTo(TimeZone.getDefault()); + } + + @Test + public void testSetLocale() { + LocaleContextHolder.setLocale(Locale.GERMAN); + assertThat(LocaleContextHolder.getLocale()).isEqualTo(Locale.GERMAN); + assertThat(LocaleContextHolder.getTimeZone()).isEqualTo(TimeZone.getDefault()); + boolean condition1 = LocaleContextHolder.getLocaleContext() instanceof TimeZoneAwareLocaleContext; + assertThat(condition1).isFalse(); + assertThat(LocaleContextHolder.getLocaleContext().getLocale()).isEqualTo(Locale.GERMAN); + + LocaleContextHolder.setLocale(Locale.GERMANY); + assertThat(LocaleContextHolder.getLocale()).isEqualTo(Locale.GERMANY); + assertThat(LocaleContextHolder.getTimeZone()).isEqualTo(TimeZone.getDefault()); + boolean condition = LocaleContextHolder.getLocaleContext() instanceof TimeZoneAwareLocaleContext; + assertThat(condition).isFalse(); + assertThat(LocaleContextHolder.getLocaleContext().getLocale()).isEqualTo(Locale.GERMANY); + + LocaleContextHolder.setLocale(null); + assertThat(LocaleContextHolder.getLocaleContext()).isNull(); + assertThat(LocaleContextHolder.getLocale()).isEqualTo(Locale.getDefault()); + assertThat(LocaleContextHolder.getTimeZone()).isEqualTo(TimeZone.getDefault()); + + LocaleContextHolder.setDefaultLocale(Locale.GERMAN); + assertThat(LocaleContextHolder.getLocale()).isEqualTo(Locale.GERMAN); + LocaleContextHolder.setDefaultLocale(null); + assertThat(LocaleContextHolder.getLocale()).isEqualTo(Locale.getDefault()); + } + + @Test + public void testSetTimeZone() { + LocaleContextHolder.setTimeZone(TimeZone.getTimeZone("GMT+1")); + assertThat(LocaleContextHolder.getLocale()).isEqualTo(Locale.getDefault()); + assertThat(LocaleContextHolder.getTimeZone()).isEqualTo(TimeZone.getTimeZone("GMT+1")); + boolean condition1 = LocaleContextHolder.getLocaleContext() instanceof TimeZoneAwareLocaleContext; + assertThat(condition1).isTrue(); + assertThat(LocaleContextHolder.getLocaleContext().getLocale()).isNull(); + assertThat(((TimeZoneAwareLocaleContext) LocaleContextHolder.getLocaleContext()).getTimeZone()).isEqualTo(TimeZone.getTimeZone("GMT+1")); + + LocaleContextHolder.setTimeZone(TimeZone.getTimeZone("GMT+2")); + assertThat(LocaleContextHolder.getLocale()).isEqualTo(Locale.getDefault()); + assertThat(LocaleContextHolder.getTimeZone()).isEqualTo(TimeZone.getTimeZone("GMT+2")); + boolean condition = LocaleContextHolder.getLocaleContext() instanceof TimeZoneAwareLocaleContext; + assertThat(condition).isTrue(); + assertThat(LocaleContextHolder.getLocaleContext().getLocale()).isNull(); + assertThat(((TimeZoneAwareLocaleContext) LocaleContextHolder.getLocaleContext()).getTimeZone()).isEqualTo(TimeZone.getTimeZone("GMT+2")); + + LocaleContextHolder.setTimeZone(null); + assertThat(LocaleContextHolder.getLocaleContext()).isNull(); + assertThat(LocaleContextHolder.getLocale()).isEqualTo(Locale.getDefault()); + assertThat(LocaleContextHolder.getTimeZone()).isEqualTo(TimeZone.getDefault()); + + LocaleContextHolder.setDefaultTimeZone(TimeZone.getTimeZone("GMT+1")); + assertThat(LocaleContextHolder.getTimeZone()).isEqualTo(TimeZone.getTimeZone("GMT+1")); + LocaleContextHolder.setDefaultTimeZone(null); + assertThat(LocaleContextHolder.getTimeZone()).isEqualTo(TimeZone.getDefault()); + } + + @Test + public void testSetLocaleAndSetTimeZoneMixed() { + LocaleContextHolder.setLocale(Locale.GERMANY); + assertThat(LocaleContextHolder.getLocale()).isEqualTo(Locale.GERMANY); + assertThat(LocaleContextHolder.getTimeZone()).isEqualTo(TimeZone.getDefault()); + boolean condition5 = LocaleContextHolder.getLocaleContext() instanceof TimeZoneAwareLocaleContext; + assertThat(condition5).isFalse(); + assertThat(LocaleContextHolder.getLocaleContext().getLocale()).isEqualTo(Locale.GERMANY); + + LocaleContextHolder.setTimeZone(TimeZone.getTimeZone("GMT+1")); + assertThat(LocaleContextHolder.getLocale()).isEqualTo(Locale.GERMANY); + assertThat(LocaleContextHolder.getTimeZone()).isEqualTo(TimeZone.getTimeZone("GMT+1")); + boolean condition3 = LocaleContextHolder.getLocaleContext() instanceof TimeZoneAwareLocaleContext; + assertThat(condition3).isTrue(); + assertThat(LocaleContextHolder.getLocaleContext().getLocale()).isEqualTo(Locale.GERMANY); + assertThat(((TimeZoneAwareLocaleContext) LocaleContextHolder.getLocaleContext()).getTimeZone()).isEqualTo(TimeZone.getTimeZone("GMT+1")); + + LocaleContextHolder.setLocale(Locale.GERMAN); + assertThat(LocaleContextHolder.getLocale()).isEqualTo(Locale.GERMAN); + assertThat(LocaleContextHolder.getTimeZone()).isEqualTo(TimeZone.getTimeZone("GMT+1")); + boolean condition2 = LocaleContextHolder.getLocaleContext() instanceof TimeZoneAwareLocaleContext; + assertThat(condition2).isTrue(); + assertThat(LocaleContextHolder.getLocaleContext().getLocale()).isEqualTo(Locale.GERMAN); + assertThat(((TimeZoneAwareLocaleContext) LocaleContextHolder.getLocaleContext()).getTimeZone()).isEqualTo(TimeZone.getTimeZone("GMT+1")); + + LocaleContextHolder.setTimeZone(null); + assertThat(LocaleContextHolder.getLocale()).isEqualTo(Locale.GERMAN); + assertThat(LocaleContextHolder.getTimeZone()).isEqualTo(TimeZone.getDefault()); + boolean condition4 = LocaleContextHolder.getLocaleContext() instanceof TimeZoneAwareLocaleContext; + assertThat(condition4).isFalse(); + assertThat(LocaleContextHolder.getLocaleContext().getLocale()).isEqualTo(Locale.GERMAN); + + LocaleContextHolder.setTimeZone(TimeZone.getTimeZone("GMT+2")); + assertThat(LocaleContextHolder.getLocale()).isEqualTo(Locale.GERMAN); + assertThat(LocaleContextHolder.getTimeZone()).isEqualTo(TimeZone.getTimeZone("GMT+2")); + boolean condition1 = LocaleContextHolder.getLocaleContext() instanceof TimeZoneAwareLocaleContext; + assertThat(condition1).isTrue(); + assertThat(LocaleContextHolder.getLocaleContext().getLocale()).isEqualTo(Locale.GERMAN); + assertThat(((TimeZoneAwareLocaleContext) LocaleContextHolder.getLocaleContext()).getTimeZone()).isEqualTo(TimeZone.getTimeZone("GMT+2")); + + LocaleContextHolder.setLocale(null); + assertThat(LocaleContextHolder.getLocale()).isEqualTo(Locale.getDefault()); + assertThat(LocaleContextHolder.getTimeZone()).isEqualTo(TimeZone.getTimeZone("GMT+2")); + boolean condition = LocaleContextHolder.getLocaleContext() instanceof TimeZoneAwareLocaleContext; + assertThat(condition).isTrue(); + assertThat(LocaleContextHolder.getLocaleContext().getLocale()).isNull(); + assertThat(((TimeZoneAwareLocaleContext) LocaleContextHolder.getLocaleContext()).getTimeZone()).isEqualTo(TimeZone.getTimeZone("GMT+2")); + + LocaleContextHolder.setTimeZone(null); + assertThat(LocaleContextHolder.getLocale()).isEqualTo(Locale.getDefault()); + assertThat(LocaleContextHolder.getTimeZone()).isEqualTo(TimeZone.getDefault()); + assertThat(LocaleContextHolder.getLocaleContext()).isNull(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsIndexLoaderTests.java b/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsIndexLoaderTests.java new file mode 100644 index 0000000..cba8661 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsIndexLoaderTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index; + +import java.io.IOException; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.testfixture.index.CandidateComponentsTestClassLoader; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link CandidateComponentsIndexLoader}. + * + * @author Stephane Nicoll + */ +public class CandidateComponentsIndexLoaderTests { + + @Test + public void validateIndexIsDisabledByDefault() { + CandidateComponentsIndex index = CandidateComponentsIndexLoader.loadIndex(null); + assertThat(index).as("No spring.components should be available at the default location").isNull(); + } + + @Test + public void loadIndexSeveralMatches() { + CandidateComponentsIndex index = CandidateComponentsIndexLoader.loadIndex( + CandidateComponentsTestClassLoader.index(getClass().getClassLoader(), + new ClassPathResource("spring.components", getClass()))); + Set components = index.getCandidateTypes("org.springframework", "foo"); + assertThat(components).contains( + "org.springframework.context.index.Sample1", + "org.springframework.context.index.Sample2"); + } + + @Test + public void loadIndexSingleMatch() { + CandidateComponentsIndex index = CandidateComponentsIndexLoader.loadIndex( + CandidateComponentsTestClassLoader.index(getClass().getClassLoader(), + new ClassPathResource("spring.components", getClass()))); + Set components = index.getCandidateTypes("org.springframework", "biz"); + assertThat(components).contains( + "org.springframework.context.index.Sample3"); + } + + @Test + public void loadIndexNoMatch() { + CandidateComponentsIndex index = CandidateComponentsIndexLoader.loadIndex( + CandidateComponentsTestClassLoader.index(getClass().getClassLoader(), + new ClassPathResource("spring.components", getClass()))); + Set components = index.getCandidateTypes("org.springframework", "none"); + assertThat(components).isEmpty(); + } + + @Test + public void loadIndexNoPackage() { + CandidateComponentsIndex index = CandidateComponentsIndexLoader.loadIndex( + CandidateComponentsTestClassLoader.index(getClass().getClassLoader(), + new ClassPathResource("spring.components", getClass()))); + Set components = index.getCandidateTypes("com.example", "foo"); + assertThat(components).isEmpty(); + } + + @Test + public void loadIndexNoSpringComponentsResource() { + CandidateComponentsIndex index = CandidateComponentsIndexLoader.loadIndex( + CandidateComponentsTestClassLoader.disableIndex(getClass().getClassLoader())); + assertThat(index).isNull(); + } + + @Test + public void loadIndexNoEntry() { + CandidateComponentsIndex index = CandidateComponentsIndexLoader.loadIndex( + CandidateComponentsTestClassLoader.index(getClass().getClassLoader(), + new ClassPathResource("empty-spring.components", getClass()))); + assertThat(index).isNull(); + } + + @Test + public void loadIndexWithException() { + final IOException cause = new IOException("test exception"); + assertThatIllegalStateException().isThrownBy(() -> { + CandidateComponentsTestClassLoader classLoader = new CandidateComponentsTestClassLoader(getClass().getClassLoader(), cause); + CandidateComponentsIndexLoader.loadIndex(classLoader); + }).withMessageContaining("Unable to load indexes").withCause(cause); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsIndexTests.java b/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsIndexTests.java new file mode 100644 index 0000000..ab94bfd --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/index/CandidateComponentsIndexTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.index; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Properties; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link CandidateComponentsIndex}. + * + * @author Stephane Nicoll + */ +public class CandidateComponentsIndexTests { + + @Test + public void getCandidateTypes() { + CandidateComponentsIndex index = new CandidateComponentsIndex( + Collections.singletonList(createSampleProperties())); + Set actual = index.getCandidateTypes("com.example.service", "service"); + assertThat(actual).contains("com.example.service.One", + "com.example.service.sub.Two", "com.example.service.Three"); + } + + @Test + public void getCandidateTypesSubPackage() { + CandidateComponentsIndex index = new CandidateComponentsIndex( + Collections.singletonList(createSampleProperties())); + Set actual = index.getCandidateTypes("com.example.service.sub", "service"); + assertThat(actual).contains("com.example.service.sub.Two"); + } + + @Test + public void getCandidateTypesSubPackageNoMatch() { + CandidateComponentsIndex index = new CandidateComponentsIndex( + Collections.singletonList(createSampleProperties())); + Set actual = index.getCandidateTypes("com.example.service.none", "service"); + assertThat(actual).isEmpty(); + } + + @Test + public void getCandidateTypesNoMatch() { + CandidateComponentsIndex index = new CandidateComponentsIndex( + Collections.singletonList(createSampleProperties())); + Set actual = index.getCandidateTypes("com.example.service", "entity"); + assertThat(actual).isEmpty(); + } + + @Test + public void mergeCandidateStereotypes() { + CandidateComponentsIndex index = new CandidateComponentsIndex(Arrays.asList( + createProperties("com.example.Foo", "service"), + createProperties("com.example.Foo", "entity"))); + assertThat(index.getCandidateTypes("com.example", "service")) + .contains("com.example.Foo"); + assertThat(index.getCandidateTypes("com.example", "entity")) + .contains("com.example.Foo"); + } + + private static Properties createProperties(String key, String stereotypes) { + Properties properties = new Properties(); + properties.put(key, String.join(",", stereotypes)); + return properties; + } + + private static Properties createSampleProperties() { + Properties properties = new Properties(); + properties.put("com.example.service.One", "service"); + properties.put("com.example.service.sub.Two", "service"); + properties.put("com.example.service.Three", "service"); + properties.put("com.example.domain.Four", "entity"); + return properties; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/support/ApplicationContextLifecycleTests.java b/spring-context/src/test/java/org/springframework/context/support/ApplicationContextLifecycleTests.java new file mode 100644 index 0000000..6208d79 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/support/ApplicationContextLifecycleTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Mark Fisher + * @author Chris Beams + */ +public class ApplicationContextLifecycleTests { + + @Test + public void testBeansStart() { + AbstractApplicationContext context = new ClassPathXmlApplicationContext("lifecycleTests.xml", getClass()); + context.start(); + LifecycleTestBean bean1 = (LifecycleTestBean) context.getBean("bean1"); + LifecycleTestBean bean2 = (LifecycleTestBean) context.getBean("bean2"); + LifecycleTestBean bean3 = (LifecycleTestBean) context.getBean("bean3"); + LifecycleTestBean bean4 = (LifecycleTestBean) context.getBean("bean4"); + String error = "bean was not started"; + assertThat(bean1.isRunning()).as(error).isTrue(); + assertThat(bean2.isRunning()).as(error).isTrue(); + assertThat(bean3.isRunning()).as(error).isTrue(); + assertThat(bean4.isRunning()).as(error).isTrue(); + } + + @Test + public void testBeansStop() { + AbstractApplicationContext context = new ClassPathXmlApplicationContext("lifecycleTests.xml", getClass()); + context.start(); + LifecycleTestBean bean1 = (LifecycleTestBean) context.getBean("bean1"); + LifecycleTestBean bean2 = (LifecycleTestBean) context.getBean("bean2"); + LifecycleTestBean bean3 = (LifecycleTestBean) context.getBean("bean3"); + LifecycleTestBean bean4 = (LifecycleTestBean) context.getBean("bean4"); + String startError = "bean was not started"; + assertThat(bean1.isRunning()).as(startError).isTrue(); + assertThat(bean2.isRunning()).as(startError).isTrue(); + assertThat(bean3.isRunning()).as(startError).isTrue(); + assertThat(bean4.isRunning()).as(startError).isTrue(); + context.stop(); + String stopError = "bean was not stopped"; + assertThat(bean1.isRunning()).as(stopError).isFalse(); + assertThat(bean2.isRunning()).as(stopError).isFalse(); + assertThat(bean3.isRunning()).as(stopError).isFalse(); + assertThat(bean4.isRunning()).as(stopError).isFalse(); + } + + @Test + public void testStartOrder() { + AbstractApplicationContext context = new ClassPathXmlApplicationContext("lifecycleTests.xml", getClass()); + context.start(); + LifecycleTestBean bean1 = (LifecycleTestBean) context.getBean("bean1"); + LifecycleTestBean bean2 = (LifecycleTestBean) context.getBean("bean2"); + LifecycleTestBean bean3 = (LifecycleTestBean) context.getBean("bean3"); + LifecycleTestBean bean4 = (LifecycleTestBean) context.getBean("bean4"); + String notStartedError = "bean was not started"; + assertThat(bean1.getStartOrder() > 0).as(notStartedError).isTrue(); + assertThat(bean2.getStartOrder() > 0).as(notStartedError).isTrue(); + assertThat(bean3.getStartOrder() > 0).as(notStartedError).isTrue(); + assertThat(bean4.getStartOrder() > 0).as(notStartedError).isTrue(); + String orderError = "dependent bean must start after the bean it depends on"; + assertThat(bean2.getStartOrder() > bean1.getStartOrder()).as(orderError).isTrue(); + assertThat(bean3.getStartOrder() > bean2.getStartOrder()).as(orderError).isTrue(); + assertThat(bean4.getStartOrder() > bean2.getStartOrder()).as(orderError).isTrue(); + } + + @Test + public void testStopOrder() { + AbstractApplicationContext context = new ClassPathXmlApplicationContext("lifecycleTests.xml", getClass()); + context.start(); + context.stop(); + LifecycleTestBean bean1 = (LifecycleTestBean) context.getBean("bean1"); + LifecycleTestBean bean2 = (LifecycleTestBean) context.getBean("bean2"); + LifecycleTestBean bean3 = (LifecycleTestBean) context.getBean("bean3"); + LifecycleTestBean bean4 = (LifecycleTestBean) context.getBean("bean4"); + String notStoppedError = "bean was not stopped"; + assertThat(bean1.getStopOrder() > 0).as(notStoppedError).isTrue(); + assertThat(bean2.getStopOrder() > 0).as(notStoppedError).isTrue(); + assertThat(bean3.getStopOrder() > 0).as(notStoppedError).isTrue(); + assertThat(bean4.getStopOrder() > 0).as(notStoppedError).isTrue(); + String orderError = "dependent bean must stop before the bean it depends on"; + assertThat(bean2.getStopOrder() < bean1.getStopOrder()).as(orderError).isTrue(); + assertThat(bean3.getStopOrder() < bean2.getStopOrder()).as(orderError).isTrue(); + assertThat(bean4.getStopOrder() < bean2.getStopOrder()).as(orderError).isTrue(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/support/Assembler.java b/spring-context/src/test/java/org/springframework/context/support/Assembler.java new file mode 100644 index 0000000..53f40a5 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/support/Assembler.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +/** + * @author Alef Arendsen + */ +public class Assembler implements TestIF { + + @SuppressWarnings("unused") + private Service service; + private Logic l; + private String name; + + public void setService(Service service) { + this.service = service; + } + + public void setLogic(Logic l) { + this.l = l; + } + + public void setBeanName(String name) { + this.name = name; + } + + public void test() { + } + + public void output() { + System.out.println("Bean " + name); + l.output(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/support/AutowiredService.java b/spring-context/src/test/java/org/springframework/context/support/AutowiredService.java new file mode 100644 index 0000000..a11208c --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/support/AutowiredService.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import org.springframework.context.MessageSource; + +/** + * @author Juergen Hoeller + */ +public class AutowiredService { + + private MessageSource messageSource; + + public void setMessageSource(MessageSource messageSource) { + if (this.messageSource != null) { + throw new IllegalArgumentException("MessageSource should not be set twice"); + } + this.messageSource = messageSource; + } + + public MessageSource getMessageSource() { + return messageSource; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/support/BeanFactoryPostProcessorTests.java b/spring-context/src/test/java/org/springframework/context/support/BeanFactoryPostProcessorTests.java new file mode 100644 index 0000000..ffbdceb --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/support/BeanFactoryPostProcessorTests.java @@ -0,0 +1,283 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.BeansException; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; +import org.springframework.util.Assert; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests the interaction between {@link ApplicationContext} implementations and + * any registered {@link BeanFactoryPostProcessor} implementations. Specifically + * {@link StaticApplicationContext} is used for the tests, but what's represented + * here is any {@link AbstractApplicationContext} implementation. + * + * @author Colin Sampaleanu + * @author Juergen Hoeller + * @author Chris Beams + * @since 02.10.2003 + */ +public class BeanFactoryPostProcessorTests { + + @Test + public void testRegisteredBeanFactoryPostProcessor() { + StaticApplicationContext ac = new StaticApplicationContext(); + ac.registerSingleton("tb1", TestBean.class); + ac.registerSingleton("tb2", TestBean.class); + TestBeanFactoryPostProcessor bfpp = new TestBeanFactoryPostProcessor(); + ac.addBeanFactoryPostProcessor(bfpp); + assertThat(bfpp.wasCalled).isFalse(); + ac.refresh(); + assertThat(bfpp.wasCalled).isTrue(); + } + + @Test + public void testDefinedBeanFactoryPostProcessor() { + StaticApplicationContext ac = new StaticApplicationContext(); + ac.registerSingleton("tb1", TestBean.class); + ac.registerSingleton("tb2", TestBean.class); + ac.registerSingleton("bfpp", TestBeanFactoryPostProcessor.class); + ac.refresh(); + TestBeanFactoryPostProcessor bfpp = (TestBeanFactoryPostProcessor) ac.getBean("bfpp"); + assertThat(bfpp.wasCalled).isTrue(); + } + + @Test + public void testMultipleDefinedBeanFactoryPostProcessors() { + StaticApplicationContext ac = new StaticApplicationContext(); + ac.registerSingleton("tb1", TestBean.class); + ac.registerSingleton("tb2", TestBean.class); + MutablePropertyValues pvs1 = new MutablePropertyValues(); + pvs1.add("initValue", "${key}"); + ac.registerSingleton("bfpp1", TestBeanFactoryPostProcessor.class, pvs1); + MutablePropertyValues pvs2 = new MutablePropertyValues(); + pvs2.add("properties", "key=value"); + ac.registerSingleton("bfpp2", PropertyPlaceholderConfigurer.class, pvs2); + ac.refresh(); + TestBeanFactoryPostProcessor bfpp = (TestBeanFactoryPostProcessor) ac.getBean("bfpp1"); + assertThat(bfpp.initValue).isEqualTo("value"); + assertThat(bfpp.wasCalled).isTrue(); + } + + @Test + public void testBeanFactoryPostProcessorNotExecutedByBeanFactory() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("tb1", new RootBeanDefinition(TestBean.class)); + bf.registerBeanDefinition("tb2", new RootBeanDefinition(TestBean.class)); + bf.registerBeanDefinition("bfpp", new RootBeanDefinition(TestBeanFactoryPostProcessor.class)); + TestBeanFactoryPostProcessor bfpp = (TestBeanFactoryPostProcessor) bf.getBean("bfpp"); + assertThat(bfpp.wasCalled).isFalse(); + } + + @Test + public void testBeanDefinitionRegistryPostProcessor() { + StaticApplicationContext ac = new StaticApplicationContext(); + ac.registerSingleton("tb1", TestBean.class); + ac.registerSingleton("tb2", TestBean.class); + ac.addBeanFactoryPostProcessor(new PrioritizedBeanDefinitionRegistryPostProcessor()); + TestBeanDefinitionRegistryPostProcessor bdrpp = new TestBeanDefinitionRegistryPostProcessor(); + ac.addBeanFactoryPostProcessor(bdrpp); + assertThat(bdrpp.wasCalled).isFalse(); + ac.refresh(); + assertThat(bdrpp.wasCalled).isTrue(); + assertThat(ac.getBean("bfpp1", TestBeanFactoryPostProcessor.class).wasCalled).isTrue(); + assertThat(ac.getBean("bfpp2", TestBeanFactoryPostProcessor.class).wasCalled).isTrue(); + } + + @Test + public void testBeanDefinitionRegistryPostProcessorRegisteringAnother() { + StaticApplicationContext ac = new StaticApplicationContext(); + ac.registerSingleton("tb1", TestBean.class); + ac.registerSingleton("tb2", TestBean.class); + ac.registerBeanDefinition("bdrpp2", new RootBeanDefinition(OuterBeanDefinitionRegistryPostProcessor.class)); + ac.refresh(); + assertThat(ac.getBean("bfpp1", TestBeanFactoryPostProcessor.class).wasCalled).isTrue(); + assertThat(ac.getBean("bfpp2", TestBeanFactoryPostProcessor.class).wasCalled).isTrue(); + } + + @Test + public void testPrioritizedBeanDefinitionRegistryPostProcessorRegisteringAnother() { + StaticApplicationContext ac = new StaticApplicationContext(); + ac.registerSingleton("tb1", TestBean.class); + ac.registerSingleton("tb2", TestBean.class); + ac.registerBeanDefinition("bdrpp2", new RootBeanDefinition(PrioritizedOuterBeanDefinitionRegistryPostProcessor.class)); + ac.refresh(); + assertThat(ac.getBean("bfpp1", TestBeanFactoryPostProcessor.class).wasCalled).isTrue(); + assertThat(ac.getBean("bfpp2", TestBeanFactoryPostProcessor.class).wasCalled).isTrue(); + } + + @Test + public void testBeanFactoryPostProcessorAsApplicationListener() { + StaticApplicationContext ac = new StaticApplicationContext(); + ac.registerBeanDefinition("bfpp", new RootBeanDefinition(ListeningBeanFactoryPostProcessor.class)); + ac.refresh(); + boolean condition = ac.getBean(ListeningBeanFactoryPostProcessor.class).received instanceof ContextRefreshedEvent; + assertThat(condition).isTrue(); + } + + @Test + public void testBeanFactoryPostProcessorWithInnerBeanAsApplicationListener() { + StaticApplicationContext ac = new StaticApplicationContext(); + RootBeanDefinition rbd = new RootBeanDefinition(NestingBeanFactoryPostProcessor.class); + rbd.getPropertyValues().add("listeningBean", new RootBeanDefinition(ListeningBean.class)); + ac.registerBeanDefinition("bfpp", rbd); + ac.refresh(); + boolean condition = ac.getBean(NestingBeanFactoryPostProcessor.class).getListeningBean().received instanceof ContextRefreshedEvent; + assertThat(condition).isTrue(); + } + + + public static class TestBeanFactoryPostProcessor implements BeanFactoryPostProcessor { + + public String initValue; + + public void setInitValue(String initValue) { + this.initValue = initValue; + } + + public boolean wasCalled = false; + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { + wasCalled = true; + } + } + + + public static class PrioritizedBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor, Ordered { + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE; + } + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + registry.registerBeanDefinition("bfpp1", new RootBeanDefinition(TestBeanFactoryPostProcessor.class)); + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + } + } + + + public static class TestBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor { + + public boolean wasCalled; + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + assertThat(registry.containsBeanDefinition("bfpp1")).isTrue(); + registry.registerBeanDefinition("bfpp2", new RootBeanDefinition(TestBeanFactoryPostProcessor.class)); + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + this.wasCalled = true; + } + } + + + public static class OuterBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor { + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + registry.registerBeanDefinition("anotherpp", new RootBeanDefinition(TestBeanDefinitionRegistryPostProcessor.class)); + registry.registerBeanDefinition("ppp", new RootBeanDefinition(PrioritizedBeanDefinitionRegistryPostProcessor.class)); + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + } + } + + + public static class PrioritizedOuterBeanDefinitionRegistryPostProcessor extends OuterBeanDefinitionRegistryPostProcessor + implements PriorityOrdered { + + @Override + public int getOrder() { + return HIGHEST_PRECEDENCE; + } + } + + + public static class ListeningBeanFactoryPostProcessor implements BeanFactoryPostProcessor, ApplicationListener { + + public ApplicationEvent received; + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + } + + @Override + public void onApplicationEvent(ApplicationEvent event) { + Assert.state(this.received == null, "Just one ContextRefreshedEvent expected"); + this.received = event; + } + } + + + public static class ListeningBean implements ApplicationListener { + + public ApplicationEvent received; + + @Override + public void onApplicationEvent(ApplicationEvent event) { + Assert.state(this.received == null, "Just one ContextRefreshedEvent expected"); + this.received = event; + } + } + + + public static class NestingBeanFactoryPostProcessor implements BeanFactoryPostProcessor { + + private ListeningBean listeningBean; + + public void setListeningBean(ListeningBean listeningBean) { + this.listeningBean = listeningBean; + } + + public ListeningBean getListeningBean() { + return listeningBean; + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/support/ClassPathXmlApplicationContextTests.java b/spring-context/src/test/java/org/springframework/context/support/ClassPathXmlApplicationContextTests.java new file mode 100644 index 0000000..b3c46cd --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/support/ClassPathXmlApplicationContextTests.java @@ -0,0 +1,405 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.TypeMismatchException; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.CannotLoadBeanClassException; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.context.ApplicationListener; +import org.springframework.context.MessageSource; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.tests.sample.beans.ResourceTestBean; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.ObjectUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Juergen Hoeller + * @author Chris Beams + */ +public class ClassPathXmlApplicationContextTests { + + private static final String PATH = "/org/springframework/context/support/"; + private static final String RESOURCE_CONTEXT = PATH + "ClassPathXmlApplicationContextTests-resource.xml"; + private static final String CONTEXT_WILDCARD = PATH + "test/context*.xml"; + private static final String CONTEXT_A = "test/contextA.xml"; + private static final String CONTEXT_B = "test/contextB.xml"; + private static final String CONTEXT_C = "test/contextC.xml"; + private static final String FQ_CONTEXT_A = PATH + CONTEXT_A; + private static final String FQ_CONTEXT_B = PATH + CONTEXT_B; + private static final String FQ_CONTEXT_C = PATH + CONTEXT_C; + private static final String SIMPLE_CONTEXT = "simpleContext.xml"; + private static final String FQ_SIMPLE_CONTEXT = PATH + "simpleContext.xml"; + private static final String FQ_ALIASED_CONTEXT_C = PATH + "test/aliased-contextC.xml"; + private static final String INVALID_VALUE_TYPE_CONTEXT = PATH + "invalidValueType.xml"; + private static final String CHILD_WITH_PROXY_CONTEXT = PATH + "childWithProxy.xml"; + private static final String INVALID_CLASS_CONTEXT = "invalidClass.xml"; + private static final String CLASS_WITH_PLACEHOLDER_CONTEXT = "classWithPlaceholder.xml"; + private static final String ALIAS_THAT_OVERRIDES_PARENT_CONTEXT = PATH + "aliasThatOverridesParent.xml"; + private static final String ALIAS_FOR_PARENT_CONTEXT = PATH + "aliasForParent.xml"; + private static final String TEST_PROPERTIES = "test.properties"; + + + @Test + public void testSingleConfigLocation() { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(FQ_SIMPLE_CONTEXT); + assertThat(ctx.containsBean("someMessageSource")).isTrue(); + ctx.close(); + } + + @Test + public void testMultipleConfigLocations() { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext( + FQ_CONTEXT_B, FQ_CONTEXT_C, FQ_CONTEXT_A); + assertThat(ctx.containsBean("service")).isTrue(); + assertThat(ctx.containsBean("logicOne")).isTrue(); + assertThat(ctx.containsBean("logicTwo")).isTrue(); + + // re-refresh (after construction refresh) + Service service = (Service) ctx.getBean("service"); + ctx.refresh(); + assertThat(service.isProperlyDestroyed()).isTrue(); + + // regular close call + service = (Service) ctx.getBean("service"); + ctx.close(); + assertThat(service.isProperlyDestroyed()).isTrue(); + + // re-activating and re-closing the context (SPR-13425) + ctx.refresh(); + service = (Service) ctx.getBean("service"); + ctx.close(); + assertThat(service.isProperlyDestroyed()).isTrue(); + } + + @Test + public void testConfigLocationPattern() { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(CONTEXT_WILDCARD); + assertThat(ctx.containsBean("service")).isTrue(); + assertThat(ctx.containsBean("logicOne")).isTrue(); + assertThat(ctx.containsBean("logicTwo")).isTrue(); + Service service = (Service) ctx.getBean("service"); + ctx.close(); + assertThat(service.isProperlyDestroyed()).isTrue(); + } + + @Test + public void testSingleConfigLocationWithClass() { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(SIMPLE_CONTEXT, getClass()); + assertThat(ctx.containsBean("someMessageSource")).isTrue(); + ctx.close(); + } + + @Test + public void testAliasWithPlaceholder() { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext( + FQ_CONTEXT_B, FQ_ALIASED_CONTEXT_C, FQ_CONTEXT_A); + assertThat(ctx.containsBean("service")).isTrue(); + assertThat(ctx.containsBean("logicOne")).isTrue(); + assertThat(ctx.containsBean("logicTwo")).isTrue(); + ctx.refresh(); + } + + @Test + public void testContextWithInvalidValueType() throws IOException { + ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( + new String[] {INVALID_VALUE_TYPE_CONTEXT}, false); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy( + context::refresh) + .satisfies(ex -> { + assertThat(ex.contains(TypeMismatchException.class)).isTrue(); + assertThat(ex.toString()).contains("someMessageSource", "useCodeAsDefaultMessage"); + checkExceptionFromInvalidValueType(ex); + checkExceptionFromInvalidValueType(new ExceptionInInitializerError(ex)); + assertThat(context.isActive()).isFalse(); + }); + } + + private void checkExceptionFromInvalidValueType(Throwable ex) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ex.printStackTrace(new PrintStream(baos)); + String dump = FileCopyUtils.copyToString(new InputStreamReader(new ByteArrayInputStream(baos.toByteArray()))); + assertThat(dump.contains("someMessageSource")).isTrue(); + assertThat(dump.contains("useCodeAsDefaultMessage")).isTrue(); + } + catch (IOException ioex) { + throw new IllegalStateException(ioex); + } + } + + @Test + public void testContextWithInvalidLazyClass() { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(INVALID_CLASS_CONTEXT, getClass()); + assertThat(ctx.containsBean("someMessageSource")).isTrue(); + assertThatExceptionOfType(CannotLoadBeanClassException.class).isThrownBy(() -> + ctx.getBean("someMessageSource")) + .satisfies(ex -> assertThat(ex.contains(ClassNotFoundException.class)).isTrue()); + ctx.close(); + } + + @Test + public void testContextWithClassNameThatContainsPlaceholder() { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(CLASS_WITH_PLACEHOLDER_CONTEXT, getClass()); + assertThat(ctx.containsBean("someMessageSource")).isTrue(); + boolean condition = ctx.getBean("someMessageSource") instanceof StaticMessageSource; + assertThat(condition).isTrue(); + ctx.close(); + } + + @Test + public void testMultipleConfigLocationsWithClass() { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext( + new String[] {CONTEXT_B, CONTEXT_C, CONTEXT_A}, getClass()); + assertThat(ctx.containsBean("service")).isTrue(); + assertThat(ctx.containsBean("logicOne")).isTrue(); + assertThat(ctx.containsBean("logicTwo")).isTrue(); + ctx.close(); + } + + @Test + public void testFactoryBeanAndApplicationListener() { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(CONTEXT_WILDCARD); + ctx.getBeanFactory().registerSingleton("manualFBAAL", new FactoryBeanAndApplicationListener()); + assertThat(ctx.getBeansOfType(ApplicationListener.class).size()).isEqualTo(2); + ctx.close(); + } + + @Test + public void testMessageSourceAware() { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(CONTEXT_WILDCARD); + MessageSource messageSource = (MessageSource) ctx.getBean("messageSource"); + Service service1 = (Service) ctx.getBean("service"); + assertThat(service1.getMessageSource()).isEqualTo(ctx); + Service service2 = (Service) ctx.getBean("service2"); + assertThat(service2.getMessageSource()).isEqualTo(ctx); + AutowiredService autowiredService1 = (AutowiredService) ctx.getBean("autowiredService"); + assertThat(autowiredService1.getMessageSource()).isEqualTo(messageSource); + AutowiredService autowiredService2 = (AutowiredService) ctx.getBean("autowiredService2"); + assertThat(autowiredService2.getMessageSource()).isEqualTo(messageSource); + ctx.close(); + } + + @Test + public void testResourceArrayPropertyEditor() throws IOException { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(CONTEXT_WILDCARD); + Service service = (Service) ctx.getBean("service"); + assertThat(service.getResources().length).isEqualTo(3); + List resources = Arrays.asList(service.getResources()); + assertThat(resources.contains(new FileSystemResource(new ClassPathResource(FQ_CONTEXT_A).getFile()))).isTrue(); + assertThat(resources.contains(new FileSystemResource(new ClassPathResource(FQ_CONTEXT_B).getFile()))).isTrue(); + assertThat(resources.contains(new FileSystemResource(new ClassPathResource(FQ_CONTEXT_C).getFile()))).isTrue(); + ctx.close(); + } + + @Test + public void testChildWithProxy() throws Exception { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(CONTEXT_WILDCARD); + ClassPathXmlApplicationContext child = new ClassPathXmlApplicationContext( + new String[] {CHILD_WITH_PROXY_CONTEXT}, ctx); + assertThat(AopUtils.isAopProxy(child.getBean("assemblerOne"))).isTrue(); + assertThat(AopUtils.isAopProxy(child.getBean("assemblerTwo"))).isTrue(); + ctx.close(); + } + + @Test + public void testAliasForParentContext() { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(FQ_SIMPLE_CONTEXT); + assertThat(ctx.containsBean("someMessageSource")).isTrue(); + + ClassPathXmlApplicationContext child = new ClassPathXmlApplicationContext( + new String[] {ALIAS_FOR_PARENT_CONTEXT}, ctx); + assertThat(child.containsBean("someMessageSource")).isTrue(); + assertThat(child.containsBean("yourMessageSource")).isTrue(); + assertThat(child.containsBean("myMessageSource")).isTrue(); + assertThat(child.isSingleton("someMessageSource")).isTrue(); + assertThat(child.isSingleton("yourMessageSource")).isTrue(); + assertThat(child.isSingleton("myMessageSource")).isTrue(); + assertThat(child.getType("someMessageSource")).isEqualTo(StaticMessageSource.class); + assertThat(child.getType("yourMessageSource")).isEqualTo(StaticMessageSource.class); + assertThat(child.getType("myMessageSource")).isEqualTo(StaticMessageSource.class); + + Object someMs = child.getBean("someMessageSource"); + Object yourMs = child.getBean("yourMessageSource"); + Object myMs = child.getBean("myMessageSource"); + assertThat(yourMs).isSameAs(someMs); + assertThat(myMs).isSameAs(someMs); + + String[] aliases = child.getAliases("someMessageSource"); + assertThat(aliases.length).isEqualTo(2); + assertThat(aliases[0]).isEqualTo("myMessageSource"); + assertThat(aliases[1]).isEqualTo("yourMessageSource"); + aliases = child.getAliases("myMessageSource"); + assertThat(aliases.length).isEqualTo(2); + assertThat(aliases[0]).isEqualTo("someMessageSource"); + assertThat(aliases[1]).isEqualTo("yourMessageSource"); + + child.close(); + ctx.close(); + } + + @Test + public void testAliasThatOverridesParent() { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(FQ_SIMPLE_CONTEXT); + Object someMs = ctx.getBean("someMessageSource"); + + ClassPathXmlApplicationContext child = new ClassPathXmlApplicationContext( + new String[] {ALIAS_THAT_OVERRIDES_PARENT_CONTEXT}, ctx); + Object myMs = child.getBean("myMessageSource"); + Object someMs2 = child.getBean("someMessageSource"); + assertThat(someMs2).isSameAs(myMs); + assertThat(someMs2).isNotSameAs(someMs); + assertOneMessageSourceOnly(child, myMs); + } + + @Test + public void testAliasThatOverridesEarlierBean() { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext( + FQ_SIMPLE_CONTEXT, ALIAS_THAT_OVERRIDES_PARENT_CONTEXT); + Object myMs = ctx.getBean("myMessageSource"); + Object someMs2 = ctx.getBean("someMessageSource"); + assertThat(someMs2).isSameAs(myMs); + assertOneMessageSourceOnly(ctx, myMs); + } + + private void assertOneMessageSourceOnly(ClassPathXmlApplicationContext ctx, Object myMessageSource) { + String[] beanNamesForType = ctx.getBeanNamesForType(StaticMessageSource.class); + assertThat(beanNamesForType.length).isEqualTo(1); + assertThat(beanNamesForType[0]).isEqualTo("myMessageSource"); + beanNamesForType = ctx.getBeanNamesForType(StaticMessageSource.class, true, true); + assertThat(beanNamesForType.length).isEqualTo(1); + assertThat(beanNamesForType[0]).isEqualTo("myMessageSource"); + beanNamesForType = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(ctx, StaticMessageSource.class); + assertThat(beanNamesForType.length).isEqualTo(1); + assertThat(beanNamesForType[0]).isEqualTo("myMessageSource"); + beanNamesForType = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(ctx, StaticMessageSource.class, true, true); + assertThat(beanNamesForType.length).isEqualTo(1); + assertThat(beanNamesForType[0]).isEqualTo("myMessageSource"); + + Map beansOfType = ctx.getBeansOfType(StaticMessageSource.class); + assertThat(beansOfType.size()).isEqualTo(1); + assertThat(beansOfType.values().iterator().next()).isSameAs(myMessageSource); + beansOfType = ctx.getBeansOfType(StaticMessageSource.class, true, true); + assertThat(beansOfType.size()).isEqualTo(1); + assertThat(beansOfType.values().iterator().next()).isSameAs(myMessageSource); + beansOfType = BeanFactoryUtils.beansOfTypeIncludingAncestors(ctx, StaticMessageSource.class); + assertThat(beansOfType.size()).isEqualTo(1); + assertThat(beansOfType.values().iterator().next()).isSameAs(myMessageSource); + beansOfType = BeanFactoryUtils.beansOfTypeIncludingAncestors(ctx, StaticMessageSource.class, true, true); + assertThat(beansOfType.size()).isEqualTo(1); + assertThat(beansOfType.values().iterator().next()).isSameAs(myMessageSource); + } + + @Test + public void testResourceAndInputStream() throws IOException { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(RESOURCE_CONTEXT) { + @Override + public Resource getResource(String location) { + if (TEST_PROPERTIES.equals(location)) { + return new ClassPathResource(TEST_PROPERTIES, ClassPathXmlApplicationContextTests.class); + } + return super.getResource(location); + } + }; + ResourceTestBean resource1 = (ResourceTestBean) ctx.getBean("resource1"); + ResourceTestBean resource2 = (ResourceTestBean) ctx.getBean("resource2"); + boolean condition = resource1.getResource() instanceof ClassPathResource; + assertThat(condition).isTrue(); + StringWriter writer = new StringWriter(); + FileCopyUtils.copy(new InputStreamReader(resource1.getResource().getInputStream()), writer); + assertThat(writer.toString()).isEqualTo("contexttest"); + writer = new StringWriter(); + FileCopyUtils.copy(new InputStreamReader(resource1.getInputStream()), writer); + assertThat(writer.toString()).isEqualTo("test"); + writer = new StringWriter(); + FileCopyUtils.copy(new InputStreamReader(resource2.getResource().getInputStream()), writer); + assertThat(writer.toString()).isEqualTo("contexttest"); + writer = new StringWriter(); + FileCopyUtils.copy(new InputStreamReader(resource2.getInputStream()), writer); + assertThat(writer.toString()).isEqualTo("test"); + ctx.close(); + } + + @Test + public void testGenericApplicationContextWithXmlBeanDefinitions() { + GenericApplicationContext ctx = new GenericApplicationContext(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(ctx); + reader.loadBeanDefinitions(new ClassPathResource(CONTEXT_B, getClass())); + reader.loadBeanDefinitions(new ClassPathResource(CONTEXT_C, getClass())); + reader.loadBeanDefinitions(new ClassPathResource(CONTEXT_A, getClass())); + ctx.refresh(); + assertThat(ctx.containsBean("service")).isTrue(); + assertThat(ctx.containsBean("logicOne")).isTrue(); + assertThat(ctx.containsBean("logicTwo")).isTrue(); + ctx.close(); + } + + @Test + public void testGenericApplicationContextWithXmlBeanDefinitionsAndClassLoaderNull() { + GenericApplicationContext ctx = new GenericApplicationContext(); + ctx.setClassLoader(null); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(ctx); + reader.loadBeanDefinitions(new ClassPathResource(CONTEXT_B, getClass())); + reader.loadBeanDefinitions(new ClassPathResource(CONTEXT_C, getClass())); + reader.loadBeanDefinitions(new ClassPathResource(CONTEXT_A, getClass())); + ctx.refresh(); + assertThat(ctx.getId()).isEqualTo(ObjectUtils.identityToString(ctx)); + assertThat(ctx.getDisplayName()).isEqualTo(ObjectUtils.identityToString(ctx)); + assertThat(ctx.containsBean("service")).isTrue(); + assertThat(ctx.containsBean("logicOne")).isTrue(); + assertThat(ctx.containsBean("logicTwo")).isTrue(); + ctx.close(); + } + + @Test + public void testGenericApplicationContextWithXmlBeanDefinitionsAndSpecifiedId() { + GenericApplicationContext ctx = new GenericApplicationContext(); + ctx.setId("testContext"); + ctx.setDisplayName("Test Context"); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(ctx); + reader.loadBeanDefinitions(new ClassPathResource(CONTEXT_B, getClass())); + reader.loadBeanDefinitions(new ClassPathResource(CONTEXT_C, getClass())); + reader.loadBeanDefinitions(new ClassPathResource(CONTEXT_A, getClass())); + ctx.refresh(); + assertThat(ctx.getId()).isEqualTo("testContext"); + assertThat(ctx.getDisplayName()).isEqualTo("Test Context"); + assertThat(ctx.containsBean("service")).isTrue(); + assertThat(ctx.containsBean("logicOne")).isTrue(); + assertThat(ctx.containsBean("logicTwo")).isTrue(); + ctx.close(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/support/ConversionServiceFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/context/support/ConversionServiceFactoryBeanTests.java new file mode 100644 index 0000000..ec96b60 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/support/ConversionServiceFactoryBeanTests.java @@ -0,0 +1,148 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.ApplicationContext; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.core.convert.converter.GenericConverter; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.lang.Nullable; +import org.springframework.tests.sample.beans.ResourceTestBean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Keith Donald + * @author Juergen Hoeller + */ +public class ConversionServiceFactoryBeanTests { + + @Test + public void createDefaultConversionService() { + ConversionServiceFactoryBean factory = new ConversionServiceFactoryBean(); + factory.afterPropertiesSet(); + ConversionService service = factory.getObject(); + assertThat(service.canConvert(String.class, Integer.class)).isTrue(); + } + + @Test + public void createDefaultConversionServiceWithSupplements() { + ConversionServiceFactoryBean factory = new ConversionServiceFactoryBean(); + Set converters = new HashSet<>(); + converters.add(new Converter() { + @Override + public Foo convert(String source) { + return new Foo(); + } + }); + converters.add(new ConverterFactory() { + @Override + public Converter getConverter(Class targetType) { + return new Converter () { + @SuppressWarnings("unchecked") + @Override + public T convert(String source) { + return (T) new Bar(); + } + }; + } + }); + converters.add(new GenericConverter() { + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(String.class, Baz.class)); + } + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + return new Baz(); + } + }); + factory.setConverters(converters); + factory.afterPropertiesSet(); + ConversionService service = factory.getObject(); + assertThat(service.canConvert(String.class, Integer.class)).isTrue(); + assertThat(service.canConvert(String.class, Foo.class)).isTrue(); + assertThat(service.canConvert(String.class, Bar.class)).isTrue(); + assertThat(service.canConvert(String.class, Baz.class)).isTrue(); + } + + @Test + public void createDefaultConversionServiceWithInvalidSupplements() { + ConversionServiceFactoryBean factory = new ConversionServiceFactoryBean(); + Set converters = new HashSet<>(); + converters.add("bogus"); + factory.setConverters(converters); + assertThatIllegalArgumentException().isThrownBy( + factory::afterPropertiesSet); + } + + @Test + public void conversionServiceInApplicationContext() { + doTestConversionServiceInApplicationContext("conversionService.xml", ClassPathResource.class); + } + + @Test + public void conversionServiceInApplicationContextWithResourceOverriding() { + doTestConversionServiceInApplicationContext("conversionServiceWithResourceOverriding.xml", FileSystemResource.class); + } + + private void doTestConversionServiceInApplicationContext(String fileName, Class resourceClass) { + ApplicationContext ctx = new ClassPathXmlApplicationContext(fileName, getClass()); + ResourceTestBean tb = ctx.getBean("resourceTestBean", ResourceTestBean.class); + assertThat(resourceClass.isInstance(tb.getResource())).isTrue(); + assertThat(tb.getResourceArray().length > 0).isTrue(); + assertThat(resourceClass.isInstance(tb.getResourceArray()[0])).isTrue(); + assertThat(tb.getResourceMap().size() == 1).isTrue(); + assertThat(resourceClass.isInstance(tb.getResourceMap().get("key1"))).isTrue(); + assertThat(tb.getResourceArrayMap().size() == 1).isTrue(); + assertThat(tb.getResourceArrayMap().get("key1").length > 0).isTrue(); + assertThat(resourceClass.isInstance(tb.getResourceArrayMap().get("key1")[0])).isTrue(); + } + + + public static class Foo { + } + + public static class Bar { + } + + public static class Baz { + } + + public static class ComplexConstructorArgument { + + public ComplexConstructorArgument(Map> map) { + assertThat(!map.isEmpty()).isTrue(); + assertThat(map.keySet().iterator().next()).isInstanceOf(String.class); + assertThat(map.values().iterator().next()).isInstanceOf(Class.class); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/support/DefaultLifecycleProcessorTests.java b/spring-context/src/test/java/org/springframework/context/support/DefaultLifecycleProcessorTests.java new file mode 100644 index 0000000..338baf5 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/support/DefaultLifecycleProcessorTests.java @@ -0,0 +1,765 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.util.concurrent.CopyOnWriteArrayList; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.Lifecycle; +import org.springframework.context.LifecycleProcessor; +import org.springframework.context.SmartLifecycle; +import org.springframework.core.testfixture.EnabledForTestGroups; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; + +/** + * @author Mark Fisher + * @since 3.0 + */ +public class DefaultLifecycleProcessorTests { + + @Test + public void defaultLifecycleProcessorInstance() { + StaticApplicationContext context = new StaticApplicationContext(); + context.refresh(); + Object lifecycleProcessor = new DirectFieldAccessor(context).getPropertyValue("lifecycleProcessor"); + assertThat(lifecycleProcessor).isNotNull(); + assertThat(lifecycleProcessor.getClass()).isEqualTo(DefaultLifecycleProcessor.class); + } + + @Test + public void customLifecycleProcessorInstance() { + BeanDefinition beanDefinition = new RootBeanDefinition(DefaultLifecycleProcessor.class); + beanDefinition.getPropertyValues().addPropertyValue("timeoutPerShutdownPhase", 1000); + StaticApplicationContext context = new StaticApplicationContext(); + context.registerBeanDefinition("lifecycleProcessor", beanDefinition); + context.refresh(); + LifecycleProcessor bean = context.getBean("lifecycleProcessor", LifecycleProcessor.class); + Object contextLifecycleProcessor = new DirectFieldAccessor(context).getPropertyValue("lifecycleProcessor"); + assertThat(contextLifecycleProcessor).isNotNull(); + assertThat(contextLifecycleProcessor).isSameAs(bean); + assertThat(new DirectFieldAccessor(contextLifecycleProcessor).getPropertyValue( + "timeoutPerShutdownPhase")).isEqualTo(1000L); + } + + @Test + public void singleSmartLifecycleAutoStartup() throws Exception { + CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); + TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans); + bean.setAutoStartup(true); + StaticApplicationContext context = new StaticApplicationContext(); + context.getBeanFactory().registerSingleton("bean", bean); + assertThat(bean.isRunning()).isFalse(); + context.refresh(); + assertThat(bean.isRunning()).isTrue(); + context.stop(); + assertThat(bean.isRunning()).isFalse(); + assertThat(startedBeans.size()).isEqualTo(1); + } + + @Test + public void singleSmartLifecycleAutoStartupWithLazyInit() throws Exception { + StaticApplicationContext context = new StaticApplicationContext(); + RootBeanDefinition bd = new RootBeanDefinition(DummySmartLifecycleBean.class); + bd.setLazyInit(true); + context.registerBeanDefinition("bean", bd); + context.refresh(); + DummySmartLifecycleBean bean = context.getBean("bean", DummySmartLifecycleBean.class); + assertThat(bean.isRunning()).isTrue(); + context.stop(); + assertThat(bean.isRunning()).isFalse(); + } + + @Test + public void singleSmartLifecycleAutoStartupWithLazyInitFactoryBean() throws Exception { + StaticApplicationContext context = new StaticApplicationContext(); + RootBeanDefinition bd = new RootBeanDefinition(DummySmartLifecycleFactoryBean.class); + bd.setLazyInit(true); + context.registerBeanDefinition("bean", bd); + context.refresh(); + DummySmartLifecycleFactoryBean bean = context.getBean("&bean", DummySmartLifecycleFactoryBean.class); + assertThat(bean.isRunning()).isTrue(); + context.stop(); + assertThat(bean.isRunning()).isFalse(); + } + + @Test + public void singleSmartLifecycleWithoutAutoStartup() throws Exception { + CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); + TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans); + bean.setAutoStartup(false); + StaticApplicationContext context = new StaticApplicationContext(); + context.getBeanFactory().registerSingleton("bean", bean); + assertThat(bean.isRunning()).isFalse(); + context.refresh(); + assertThat(bean.isRunning()).isFalse(); + assertThat(startedBeans.size()).isEqualTo(0); + context.start(); + assertThat(bean.isRunning()).isTrue(); + assertThat(startedBeans.size()).isEqualTo(1); + context.stop(); + } + + @Test + public void singleSmartLifecycleAutoStartupWithNonAutoStartupDependency() throws Exception { + CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); + TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans); + bean.setAutoStartup(true); + TestSmartLifecycleBean dependency = TestSmartLifecycleBean.forStartupTests(1, startedBeans); + dependency.setAutoStartup(false); + StaticApplicationContext context = new StaticApplicationContext(); + context.getBeanFactory().registerSingleton("bean", bean); + context.getBeanFactory().registerSingleton("dependency", dependency); + context.getBeanFactory().registerDependentBean("dependency", "bean"); + assertThat(bean.isRunning()).isFalse(); + assertThat(dependency.isRunning()).isFalse(); + context.refresh(); + assertThat(bean.isRunning()).isTrue(); + assertThat(dependency.isRunning()).isFalse(); + context.stop(); + assertThat(bean.isRunning()).isFalse(); + assertThat(dependency.isRunning()).isFalse(); + assertThat(startedBeans.size()).isEqualTo(1); + } + + @Test + public void smartLifecycleGroupStartup() throws Exception { + CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); + TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forStartupTests(Integer.MIN_VALUE, startedBeans); + TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forStartupTests(1, startedBeans); + TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forStartupTests(2, startedBeans); + TestSmartLifecycleBean bean3 = TestSmartLifecycleBean.forStartupTests(3, startedBeans); + TestSmartLifecycleBean beanMax = TestSmartLifecycleBean.forStartupTests(Integer.MAX_VALUE, startedBeans); + StaticApplicationContext context = new StaticApplicationContext(); + context.getBeanFactory().registerSingleton("bean3", bean3); + context.getBeanFactory().registerSingleton("beanMin", beanMin); + context.getBeanFactory().registerSingleton("bean2", bean2); + context.getBeanFactory().registerSingleton("beanMax", beanMax); + context.getBeanFactory().registerSingleton("bean1", bean1); + assertThat(beanMin.isRunning()).isFalse(); + assertThat(bean1.isRunning()).isFalse(); + assertThat(bean2.isRunning()).isFalse(); + assertThat(bean3.isRunning()).isFalse(); + assertThat(beanMax.isRunning()).isFalse(); + context.refresh(); + assertThat(beanMin.isRunning()).isTrue(); + assertThat(bean1.isRunning()).isTrue(); + assertThat(bean2.isRunning()).isTrue(); + assertThat(bean3.isRunning()).isTrue(); + assertThat(beanMax.isRunning()).isTrue(); + context.stop(); + assertThat(startedBeans.size()).isEqualTo(5); + assertThat(getPhase(startedBeans.get(0))).isEqualTo(Integer.MIN_VALUE); + assertThat(getPhase(startedBeans.get(1))).isEqualTo(1); + assertThat(getPhase(startedBeans.get(2))).isEqualTo(2); + assertThat(getPhase(startedBeans.get(3))).isEqualTo(3); + assertThat(getPhase(startedBeans.get(4))).isEqualTo(Integer.MAX_VALUE); + } + + @Test + public void contextRefreshThenStartWithMixedBeans() throws Exception { + CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); + TestLifecycleBean simpleBean1 = TestLifecycleBean.forStartupTests(startedBeans); + TestLifecycleBean simpleBean2 = TestLifecycleBean.forStartupTests(startedBeans); + TestSmartLifecycleBean smartBean1 = TestSmartLifecycleBean.forStartupTests(5, startedBeans); + TestSmartLifecycleBean smartBean2 = TestSmartLifecycleBean.forStartupTests(-3, startedBeans); + StaticApplicationContext context = new StaticApplicationContext(); + context.getBeanFactory().registerSingleton("simpleBean1", simpleBean1); + context.getBeanFactory().registerSingleton("smartBean1", smartBean1); + context.getBeanFactory().registerSingleton("simpleBean2", simpleBean2); + context.getBeanFactory().registerSingleton("smartBean2", smartBean2); + assertThat(simpleBean1.isRunning()).isFalse(); + assertThat(simpleBean2.isRunning()).isFalse(); + assertThat(smartBean1.isRunning()).isFalse(); + assertThat(smartBean2.isRunning()).isFalse(); + context.refresh(); + assertThat(smartBean1.isRunning()).isTrue(); + assertThat(smartBean2.isRunning()).isTrue(); + assertThat(simpleBean1.isRunning()).isFalse(); + assertThat(simpleBean2.isRunning()).isFalse(); + assertThat(startedBeans.size()).isEqualTo(2); + assertThat(getPhase(startedBeans.get(0))).isEqualTo(-3); + assertThat(getPhase(startedBeans.get(1))).isEqualTo(5); + context.start(); + assertThat(smartBean1.isRunning()).isTrue(); + assertThat(smartBean2.isRunning()).isTrue(); + assertThat(simpleBean1.isRunning()).isTrue(); + assertThat(simpleBean2.isRunning()).isTrue(); + assertThat(startedBeans.size()).isEqualTo(4); + assertThat(getPhase(startedBeans.get(2))).isEqualTo(0); + assertThat(getPhase(startedBeans.get(3))).isEqualTo(0); + } + + @Test + public void contextRefreshThenStopAndRestartWithMixedBeans() throws Exception { + CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); + TestLifecycleBean simpleBean1 = TestLifecycleBean.forStartupTests(startedBeans); + TestLifecycleBean simpleBean2 = TestLifecycleBean.forStartupTests(startedBeans); + TestSmartLifecycleBean smartBean1 = TestSmartLifecycleBean.forStartupTests(5, startedBeans); + TestSmartLifecycleBean smartBean2 = TestSmartLifecycleBean.forStartupTests(-3, startedBeans); + StaticApplicationContext context = new StaticApplicationContext(); + context.getBeanFactory().registerSingleton("simpleBean1", simpleBean1); + context.getBeanFactory().registerSingleton("smartBean1", smartBean1); + context.getBeanFactory().registerSingleton("simpleBean2", simpleBean2); + context.getBeanFactory().registerSingleton("smartBean2", smartBean2); + assertThat(simpleBean1.isRunning()).isFalse(); + assertThat(simpleBean2.isRunning()).isFalse(); + assertThat(smartBean1.isRunning()).isFalse(); + assertThat(smartBean2.isRunning()).isFalse(); + context.refresh(); + assertThat(smartBean1.isRunning()).isTrue(); + assertThat(smartBean2.isRunning()).isTrue(); + assertThat(simpleBean1.isRunning()).isFalse(); + assertThat(simpleBean2.isRunning()).isFalse(); + assertThat(startedBeans.size()).isEqualTo(2); + assertThat(getPhase(startedBeans.get(0))).isEqualTo(-3); + assertThat(getPhase(startedBeans.get(1))).isEqualTo(5); + context.stop(); + assertThat(simpleBean1.isRunning()).isFalse(); + assertThat(simpleBean2.isRunning()).isFalse(); + assertThat(smartBean1.isRunning()).isFalse(); + assertThat(smartBean2.isRunning()).isFalse(); + context.start(); + assertThat(smartBean1.isRunning()).isTrue(); + assertThat(smartBean2.isRunning()).isTrue(); + assertThat(simpleBean1.isRunning()).isTrue(); + assertThat(simpleBean2.isRunning()).isTrue(); + assertThat(startedBeans.size()).isEqualTo(6); + assertThat(getPhase(startedBeans.get(2))).isEqualTo(-3); + assertThat(getPhase(startedBeans.get(3))).isEqualTo(0); + assertThat(getPhase(startedBeans.get(4))).isEqualTo(0); + assertThat(getPhase(startedBeans.get(5))).isEqualTo(5); + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + public void smartLifecycleGroupShutdown() throws Exception { + CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); + TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forShutdownTests(1, 300, stoppedBeans); + TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forShutdownTests(3, 100, stoppedBeans); + TestSmartLifecycleBean bean3 = TestSmartLifecycleBean.forShutdownTests(1, 600, stoppedBeans); + TestSmartLifecycleBean bean4 = TestSmartLifecycleBean.forShutdownTests(2, 400, stoppedBeans); + TestSmartLifecycleBean bean5 = TestSmartLifecycleBean.forShutdownTests(2, 700, stoppedBeans); + TestSmartLifecycleBean bean6 = TestSmartLifecycleBean.forShutdownTests(Integer.MAX_VALUE, 200, stoppedBeans); + TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forShutdownTests(3, 200, stoppedBeans); + StaticApplicationContext context = new StaticApplicationContext(); + context.getBeanFactory().registerSingleton("bean1", bean1); + context.getBeanFactory().registerSingleton("bean2", bean2); + context.getBeanFactory().registerSingleton("bean3", bean3); + context.getBeanFactory().registerSingleton("bean4", bean4); + context.getBeanFactory().registerSingleton("bean5", bean5); + context.getBeanFactory().registerSingleton("bean6", bean6); + context.getBeanFactory().registerSingleton("bean7", bean7); + context.refresh(); + context.stop(); + assertThat(getPhase(stoppedBeans.get(0))).isEqualTo(Integer.MAX_VALUE); + assertThat(getPhase(stoppedBeans.get(1))).isEqualTo(3); + assertThat(getPhase(stoppedBeans.get(2))).isEqualTo(3); + assertThat(getPhase(stoppedBeans.get(3))).isEqualTo(2); + assertThat(getPhase(stoppedBeans.get(4))).isEqualTo(2); + assertThat(getPhase(stoppedBeans.get(5))).isEqualTo(1); + assertThat(getPhase(stoppedBeans.get(6))).isEqualTo(1); + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + public void singleSmartLifecycleShutdown() throws Exception { + CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); + TestSmartLifecycleBean bean = TestSmartLifecycleBean.forShutdownTests(99, 300, stoppedBeans); + StaticApplicationContext context = new StaticApplicationContext(); + context.getBeanFactory().registerSingleton("bean", bean); + context.refresh(); + assertThat(bean.isRunning()).isTrue(); + context.stop(); + assertThat(stoppedBeans.size()).isEqualTo(1); + assertThat(bean.isRunning()).isFalse(); + assertThat(stoppedBeans.get(0)).isEqualTo(bean); + } + + @Test + public void singleLifecycleShutdown() throws Exception { + CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); + Lifecycle bean = new TestLifecycleBean(null, stoppedBeans); + StaticApplicationContext context = new StaticApplicationContext(); + context.getBeanFactory().registerSingleton("bean", bean); + context.refresh(); + assertThat(bean.isRunning()).isFalse(); + bean.start(); + assertThat(bean.isRunning()).isTrue(); + context.stop(); + assertThat(stoppedBeans.size()).isEqualTo(1); + assertThat(bean.isRunning()).isFalse(); + assertThat(stoppedBeans.get(0)).isEqualTo(bean); + } + + @Test + public void mixedShutdown() throws Exception { + CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); + Lifecycle bean1 = TestLifecycleBean.forShutdownTests(stoppedBeans); + Lifecycle bean2 = TestSmartLifecycleBean.forShutdownTests(500, 200, stoppedBeans); + Lifecycle bean3 = TestSmartLifecycleBean.forShutdownTests(Integer.MAX_VALUE, 100, stoppedBeans); + Lifecycle bean4 = TestLifecycleBean.forShutdownTests(stoppedBeans); + Lifecycle bean5 = TestSmartLifecycleBean.forShutdownTests(1, 200, stoppedBeans); + Lifecycle bean6 = TestSmartLifecycleBean.forShutdownTests(-1, 100, stoppedBeans); + Lifecycle bean7 = TestSmartLifecycleBean.forShutdownTests(Integer.MIN_VALUE, 300, stoppedBeans); + StaticApplicationContext context = new StaticApplicationContext(); + context.getBeanFactory().registerSingleton("bean1", bean1); + context.getBeanFactory().registerSingleton("bean2", bean2); + context.getBeanFactory().registerSingleton("bean3", bean3); + context.getBeanFactory().registerSingleton("bean4", bean4); + context.getBeanFactory().registerSingleton("bean5", bean5); + context.getBeanFactory().registerSingleton("bean6", bean6); + context.getBeanFactory().registerSingleton("bean7", bean7); + context.refresh(); + assertThat(bean2.isRunning()).isTrue(); + assertThat(bean3.isRunning()).isTrue(); + assertThat(bean5.isRunning()).isTrue(); + assertThat(bean6.isRunning()).isTrue(); + assertThat(bean7.isRunning()).isTrue(); + assertThat(bean1.isRunning()).isFalse(); + assertThat(bean4.isRunning()).isFalse(); + bean1.start(); + bean4.start(); + assertThat(bean1.isRunning()).isTrue(); + assertThat(bean4.isRunning()).isTrue(); + context.stop(); + assertThat(bean1.isRunning()).isFalse(); + assertThat(bean2.isRunning()).isFalse(); + assertThat(bean3.isRunning()).isFalse(); + assertThat(bean4.isRunning()).isFalse(); + assertThat(bean5.isRunning()).isFalse(); + assertThat(bean6.isRunning()).isFalse(); + assertThat(bean7.isRunning()).isFalse(); + assertThat(stoppedBeans.size()).isEqualTo(7); + assertThat(getPhase(stoppedBeans.get(0))).isEqualTo(Integer.MAX_VALUE); + assertThat(getPhase(stoppedBeans.get(1))).isEqualTo(500); + assertThat(getPhase(stoppedBeans.get(2))).isEqualTo(1); + assertThat(getPhase(stoppedBeans.get(3))).isEqualTo(0); + assertThat(getPhase(stoppedBeans.get(4))).isEqualTo(0); + assertThat(getPhase(stoppedBeans.get(5))).isEqualTo(-1); + assertThat(getPhase(stoppedBeans.get(6))).isEqualTo(Integer.MIN_VALUE); + } + + @Test + public void dependencyStartedFirstEvenIfItsPhaseIsHigher() throws Exception { + CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); + TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forStartupTests(Integer.MIN_VALUE, startedBeans); + TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forStartupTests(2, startedBeans); + TestSmartLifecycleBean bean99 = TestSmartLifecycleBean.forStartupTests(99, startedBeans); + TestSmartLifecycleBean beanMax = TestSmartLifecycleBean.forStartupTests(Integer.MAX_VALUE, startedBeans); + StaticApplicationContext context = new StaticApplicationContext(); + context.getBeanFactory().registerSingleton("beanMin", beanMin); + context.getBeanFactory().registerSingleton("bean2", bean2); + context.getBeanFactory().registerSingleton("bean99", bean99); + context.getBeanFactory().registerSingleton("beanMax", beanMax); + context.getBeanFactory().registerDependentBean("bean99", "bean2"); + context.refresh(); + assertThat(beanMin.isRunning()).isTrue(); + assertThat(bean2.isRunning()).isTrue(); + assertThat(bean99.isRunning()).isTrue(); + assertThat(beanMax.isRunning()).isTrue(); + assertThat(startedBeans.size()).isEqualTo(4); + assertThat(getPhase(startedBeans.get(0))).isEqualTo(Integer.MIN_VALUE); + assertThat(getPhase(startedBeans.get(1))).isEqualTo(99); + assertThat(startedBeans.get(1)).isEqualTo(bean99); + assertThat(getPhase(startedBeans.get(2))).isEqualTo(2); + assertThat(startedBeans.get(2)).isEqualTo(bean2); + assertThat(getPhase(startedBeans.get(3))).isEqualTo(Integer.MAX_VALUE); + context.stop(); + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + public void dependentShutdownFirstEvenIfItsPhaseIsLower() throws Exception { + CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); + TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forShutdownTests(Integer.MIN_VALUE, 100, stoppedBeans); + TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forShutdownTests(1, 200, stoppedBeans); + TestSmartLifecycleBean bean99 = TestSmartLifecycleBean.forShutdownTests(99, 100, stoppedBeans); + TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forShutdownTests(2, 300, stoppedBeans); + TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forShutdownTests(7, 400, stoppedBeans); + TestSmartLifecycleBean beanMax = TestSmartLifecycleBean.forShutdownTests(Integer.MAX_VALUE, 400, stoppedBeans); + StaticApplicationContext context = new StaticApplicationContext(); + context.getBeanFactory().registerSingleton("beanMin", beanMin); + context.getBeanFactory().registerSingleton("bean1", bean1); + context.getBeanFactory().registerSingleton("bean2", bean2); + context.getBeanFactory().registerSingleton("bean7", bean7); + context.getBeanFactory().registerSingleton("bean99", bean99); + context.getBeanFactory().registerSingleton("beanMax", beanMax); + context.getBeanFactory().registerDependentBean("bean99", "bean2"); + context.refresh(); + assertThat(beanMin.isRunning()).isTrue(); + assertThat(bean1.isRunning()).isTrue(); + assertThat(bean2.isRunning()).isTrue(); + assertThat(bean7.isRunning()).isTrue(); + assertThat(bean99.isRunning()).isTrue(); + assertThat(beanMax.isRunning()).isTrue(); + context.stop(); + assertThat(beanMin.isRunning()).isFalse(); + assertThat(bean1.isRunning()).isFalse(); + assertThat(bean2.isRunning()).isFalse(); + assertThat(bean7.isRunning()).isFalse(); + assertThat(bean99.isRunning()).isFalse(); + assertThat(beanMax.isRunning()).isFalse(); + assertThat(stoppedBeans.size()).isEqualTo(6); + assertThat(getPhase(stoppedBeans.get(0))).isEqualTo(Integer.MAX_VALUE); + assertThat(getPhase(stoppedBeans.get(1))).isEqualTo(2); + assertThat(stoppedBeans.get(1)).isEqualTo(bean2); + assertThat(getPhase(stoppedBeans.get(2))).isEqualTo(99); + assertThat(stoppedBeans.get(2)).isEqualTo(bean99); + assertThat(getPhase(stoppedBeans.get(3))).isEqualTo(7); + assertThat(getPhase(stoppedBeans.get(4))).isEqualTo(1); + assertThat(getPhase(stoppedBeans.get(5))).isEqualTo(Integer.MIN_VALUE); + } + + @Test + public void dependencyStartedFirstAndIsSmartLifecycle() throws Exception { + CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); + TestSmartLifecycleBean beanNegative = TestSmartLifecycleBean.forStartupTests(-99, startedBeans); + TestSmartLifecycleBean bean99 = TestSmartLifecycleBean.forStartupTests(99, startedBeans); + TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forStartupTests(7, startedBeans); + TestLifecycleBean simpleBean = TestLifecycleBean.forStartupTests(startedBeans); + StaticApplicationContext context = new StaticApplicationContext(); + context.getBeanFactory().registerSingleton("beanNegative", beanNegative); + context.getBeanFactory().registerSingleton("bean7", bean7); + context.getBeanFactory().registerSingleton("bean99", bean99); + context.getBeanFactory().registerSingleton("simpleBean", simpleBean); + context.getBeanFactory().registerDependentBean("bean7", "simpleBean"); + context.refresh(); + context.stop(); + startedBeans.clear(); + // clean start so that simpleBean is included + context.start(); + assertThat(beanNegative.isRunning()).isTrue(); + assertThat(bean99.isRunning()).isTrue(); + assertThat(bean7.isRunning()).isTrue(); + assertThat(simpleBean.isRunning()).isTrue(); + assertThat(startedBeans.size()).isEqualTo(4); + assertThat(getPhase(startedBeans.get(0))).isEqualTo(-99); + assertThat(getPhase(startedBeans.get(1))).isEqualTo(7); + assertThat(getPhase(startedBeans.get(2))).isEqualTo(0); + assertThat(getPhase(startedBeans.get(3))).isEqualTo(99); + context.stop(); + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + public void dependentShutdownFirstAndIsSmartLifecycle() throws Exception { + CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); + TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forShutdownTests(Integer.MIN_VALUE, 400, stoppedBeans); + TestSmartLifecycleBean beanNegative = TestSmartLifecycleBean.forShutdownTests(-99, 100, stoppedBeans); + TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forShutdownTests(1, 200, stoppedBeans); + TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forShutdownTests(2, 300, stoppedBeans); + TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forShutdownTests(7, 400, stoppedBeans); + TestLifecycleBean simpleBean = TestLifecycleBean.forShutdownTests(stoppedBeans); + StaticApplicationContext context = new StaticApplicationContext(); + context.getBeanFactory().registerSingleton("beanMin", beanMin); + context.getBeanFactory().registerSingleton("beanNegative", beanNegative); + context.getBeanFactory().registerSingleton("bean1", bean1); + context.getBeanFactory().registerSingleton("bean2", bean2); + context.getBeanFactory().registerSingleton("bean7", bean7); + context.getBeanFactory().registerSingleton("simpleBean", simpleBean); + context.getBeanFactory().registerDependentBean("simpleBean", "beanNegative"); + context.refresh(); + assertThat(beanMin.isRunning()).isTrue(); + assertThat(beanNegative.isRunning()).isTrue(); + assertThat(bean1.isRunning()).isTrue(); + assertThat(bean2.isRunning()).isTrue(); + assertThat(bean7.isRunning()).isTrue(); + // should start since it's a dependency of an auto-started bean + assertThat(simpleBean.isRunning()).isTrue(); + context.stop(); + assertThat(beanMin.isRunning()).isFalse(); + assertThat(beanNegative.isRunning()).isFalse(); + assertThat(bean1.isRunning()).isFalse(); + assertThat(bean2.isRunning()).isFalse(); + assertThat(bean7.isRunning()).isFalse(); + assertThat(simpleBean.isRunning()).isFalse(); + assertThat(stoppedBeans.size()).isEqualTo(6); + assertThat(getPhase(stoppedBeans.get(0))).isEqualTo(7); + assertThat(getPhase(stoppedBeans.get(1))).isEqualTo(2); + assertThat(getPhase(stoppedBeans.get(2))).isEqualTo(1); + assertThat(getPhase(stoppedBeans.get(3))).isEqualTo(-99); + assertThat(getPhase(stoppedBeans.get(4))).isEqualTo(0); + assertThat(getPhase(stoppedBeans.get(5))).isEqualTo(Integer.MIN_VALUE); + } + + @Test + public void dependencyStartedFirstButNotSmartLifecycle() throws Exception { + CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); + TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forStartupTests(Integer.MIN_VALUE, startedBeans); + TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forStartupTests(7, startedBeans); + TestLifecycleBean simpleBean = TestLifecycleBean.forStartupTests(startedBeans); + StaticApplicationContext context = new StaticApplicationContext(); + context.getBeanFactory().registerSingleton("beanMin", beanMin); + context.getBeanFactory().registerSingleton("bean7", bean7); + context.getBeanFactory().registerSingleton("simpleBean", simpleBean); + context.getBeanFactory().registerDependentBean("simpleBean", "beanMin"); + context.refresh(); + assertThat(beanMin.isRunning()).isTrue(); + assertThat(bean7.isRunning()).isTrue(); + assertThat(simpleBean.isRunning()).isTrue(); + assertThat(startedBeans.size()).isEqualTo(3); + assertThat(getPhase(startedBeans.get(0))).isEqualTo(0); + assertThat(getPhase(startedBeans.get(1))).isEqualTo(Integer.MIN_VALUE); + assertThat(getPhase(startedBeans.get(2))).isEqualTo(7); + context.stop(); + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + public void dependentShutdownFirstButNotSmartLifecycle() throws Exception { + CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); + TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forShutdownTests(1, 200, stoppedBeans); + TestLifecycleBean simpleBean = TestLifecycleBean.forShutdownTests(stoppedBeans); + TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forShutdownTests(2, 300, stoppedBeans); + TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forShutdownTests(7, 400, stoppedBeans); + TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forShutdownTests(Integer.MIN_VALUE, 400, stoppedBeans); + StaticApplicationContext context = new StaticApplicationContext(); + context.getBeanFactory().registerSingleton("beanMin", beanMin); + context.getBeanFactory().registerSingleton("bean1", bean1); + context.getBeanFactory().registerSingleton("bean2", bean2); + context.getBeanFactory().registerSingleton("bean7", bean7); + context.getBeanFactory().registerSingleton("simpleBean", simpleBean); + context.getBeanFactory().registerDependentBean("bean2", "simpleBean"); + context.refresh(); + assertThat(beanMin.isRunning()).isTrue(); + assertThat(bean1.isRunning()).isTrue(); + assertThat(bean2.isRunning()).isTrue(); + assertThat(bean7.isRunning()).isTrue(); + assertThat(simpleBean.isRunning()).isFalse(); + simpleBean.start(); + assertThat(simpleBean.isRunning()).isTrue(); + context.stop(); + assertThat(beanMin.isRunning()).isFalse(); + assertThat(bean1.isRunning()).isFalse(); + assertThat(bean2.isRunning()).isFalse(); + assertThat(bean7.isRunning()).isFalse(); + assertThat(simpleBean.isRunning()).isFalse(); + assertThat(stoppedBeans.size()).isEqualTo(5); + assertThat(getPhase(stoppedBeans.get(0))).isEqualTo(7); + assertThat(getPhase(stoppedBeans.get(1))).isEqualTo(0); + assertThat(getPhase(stoppedBeans.get(2))).isEqualTo(2); + assertThat(getPhase(stoppedBeans.get(3))).isEqualTo(1); + assertThat(getPhase(stoppedBeans.get(4))).isEqualTo(Integer.MIN_VALUE); + } + + + private static int getPhase(Lifecycle lifecycle) { + return (lifecycle instanceof SmartLifecycle) ? + ((SmartLifecycle) lifecycle).getPhase() : 0; + } + + + private static class TestLifecycleBean implements Lifecycle { + + private final CopyOnWriteArrayList startedBeans; + + private final CopyOnWriteArrayList stoppedBeans; + + private volatile boolean running; + + + static TestLifecycleBean forStartupTests(CopyOnWriteArrayList startedBeans) { + return new TestLifecycleBean(startedBeans, null); + } + + static TestLifecycleBean forShutdownTests(CopyOnWriteArrayList stoppedBeans) { + return new TestLifecycleBean(null, stoppedBeans); + } + + private TestLifecycleBean(CopyOnWriteArrayList startedBeans, CopyOnWriteArrayList stoppedBeans) { + this.startedBeans = startedBeans; + this.stoppedBeans = stoppedBeans; + } + + @Override + public boolean isRunning() { + return this.running; + } + + @Override + public void start() { + if (this.startedBeans != null) { + this.startedBeans.add(this); + } + this.running = true; + } + + @Override + public void stop() { + if (this.stoppedBeans != null) { + this.stoppedBeans.add(this); + } + this.running = false; + } + } + + + private static class TestSmartLifecycleBean extends TestLifecycleBean implements SmartLifecycle { + + private final int phase; + + private final int shutdownDelay; + + private volatile boolean autoStartup = true; + + static TestSmartLifecycleBean forStartupTests(int phase, CopyOnWriteArrayList startedBeans) { + return new TestSmartLifecycleBean(phase, 0, startedBeans, null); + } + + static TestSmartLifecycleBean forShutdownTests(int phase, int shutdownDelay, CopyOnWriteArrayList stoppedBeans) { + return new TestSmartLifecycleBean(phase, shutdownDelay, null, stoppedBeans); + } + + private TestSmartLifecycleBean(int phase, int shutdownDelay, CopyOnWriteArrayList startedBeans, CopyOnWriteArrayList stoppedBeans) { + super(startedBeans, stoppedBeans); + this.phase = phase; + this.shutdownDelay = shutdownDelay; + } + + @Override + public int getPhase() { + return this.phase; + } + + @Override + public boolean isAutoStartup() { + return this.autoStartup; + } + + public void setAutoStartup(boolean autoStartup) { + this.autoStartup = autoStartup; + } + + @Override + public void stop(final Runnable callback) { + // calling stop() before the delay to preserve + // invocation order in the 'stoppedBeans' list + stop(); + final int delay = this.shutdownDelay; + new Thread(() -> { + try { + Thread.sleep(delay); + } + catch (InterruptedException e) { + // ignore + } + finally { + callback.run(); + } + }).start(); + } + } + + + public static class DummySmartLifecycleBean implements SmartLifecycle { + + public boolean running = false; + + @Override + public boolean isAutoStartup() { + return true; + } + + @Override + public void stop(Runnable callback) { + this.running = false; + callback.run(); + } + + @Override + public void start() { + this.running = true; + } + + @Override + public void stop() { + this.running = false; + } + + @Override + public boolean isRunning() { + return this.running; + } + + @Override + public int getPhase() { + return 0; + } + } + + + public static class DummySmartLifecycleFactoryBean implements FactoryBean, SmartLifecycle { + + public boolean running = false; + + DummySmartLifecycleBean bean = new DummySmartLifecycleBean(); + + @Override + public Object getObject() throws Exception { + return this.bean; + } + + @Override + public Class getObjectType() { + return DummySmartLifecycleBean.class; + } + + @Override + public boolean isSingleton() { + return true; + } + + @Override + public boolean isAutoStartup() { + return true; + } + + @Override + public void stop(Runnable callback) { + this.running = false; + callback.run(); + } + + @Override + public void start() { + this.running = true; + } + + @Override + public void stop() { + this.running = false; + } + + @Override + public boolean isRunning() { + return this.running; + } + + @Override + public int getPhase() { + return 0; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/support/EnvironmentIntegrationTests.java b/spring-context/src/test/java/org/springframework/context/support/EnvironmentIntegrationTests.java new file mode 100644 index 0000000..b0c344f --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/support/EnvironmentIntegrationTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Tests covering the integration of the {@link Environment} into + * {@link ApplicationContext} hierarchies. + * + * @author Chris Beams + * @see org.springframework.core.env.EnvironmentSystemIntegrationTests + */ +public class EnvironmentIntegrationTests { + + @Test + public void repro() { + ConfigurableApplicationContext parent = new GenericApplicationContext(); + parent.refresh(); + + AnnotationConfigApplicationContext child = new AnnotationConfigApplicationContext(); + child.setParent(parent); + child.refresh(); + + ConfigurableEnvironment env = child.getBean(ConfigurableEnvironment.class); + assertThat(env).isSameAs(child.getEnvironment()); + + child.close(); + parent.close(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/support/EnvironmentSecurityManagerIntegrationTests.java b/spring-context/src/test/java/org/springframework/context/support/EnvironmentSecurityManagerIntegrationTests.java new file mode 100644 index 0000000..968aba7 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/support/EnvironmentSecurityManagerIntegrationTests.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.security.AccessControlException; +import java.security.Permission; +import java.util.Map; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.annotation.AnnotatedBeanDefinitionReader; +import org.springframework.context.annotation.Profile; +import org.springframework.core.env.AbstractEnvironment; +import org.springframework.core.testfixture.env.EnvironmentTestUtils; +import org.springframework.stereotype.Component; + +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests integration between Environment and SecurityManagers. See SPR-9970. + * + * @author Chris Beams + */ +public class EnvironmentSecurityManagerIntegrationTests { + + private SecurityManager originalSecurityManager; + + private Map env; + + + @BeforeEach + public void setUp() { + originalSecurityManager = System.getSecurityManager(); + env = EnvironmentTestUtils.getModifiableSystemEnvironment(); + env.put(AbstractEnvironment.ACTIVE_PROFILES_PROPERTY_NAME, "p1"); + } + + @AfterEach + public void tearDown() { + env.remove(AbstractEnvironment.ACTIVE_PROFILES_PROPERTY_NAME); + System.setSecurityManager(originalSecurityManager); + } + + + @Test + public void securityManagerDisallowsAccessToSystemEnvironmentButAllowsAccessToIndividualKeys() { + SecurityManager securityManager = new SecurityManager() { + @Override + public void checkPermission(Permission perm) { + // Disallowing access to System#getenv means that our + // ReadOnlySystemAttributesMap will come into play. + if ("getenv.*".equals(perm.getName())) { + throw new AccessControlException("Accessing the system environment is disallowed"); + } + } + }; + System.setSecurityManager(securityManager); + + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + AnnotatedBeanDefinitionReader reader = new AnnotatedBeanDefinitionReader(bf); + reader.register(C1.class); + assertThat(bf.containsBean("c1")).isTrue(); + } + + @Test + public void securityManagerDisallowsAccessToSystemEnvironmentAndDisallowsAccessToIndividualKey() { + SecurityManager securityManager = new SecurityManager() { + @Override + public void checkPermission(Permission perm) { + // Disallowing access to System#getenv means that our + // ReadOnlySystemAttributesMap will come into play. + if ("getenv.*".equals(perm.getName())) { + throw new AccessControlException("Accessing the system environment is disallowed"); + } + // Disallowing access to the spring.profiles.active property means that + // the BeanDefinitionReader won't be able to determine which profiles are + // active. We should see an INFO-level message in the console about this + // and as a result, any components marked with a non-default profile will + // be ignored. + if (("getenv." + AbstractEnvironment.ACTIVE_PROFILES_PROPERTY_NAME).equals(perm.getName())) { + throw new AccessControlException( + format("Accessing system environment variable [%s] is disallowed", + AbstractEnvironment.ACTIVE_PROFILES_PROPERTY_NAME)); + } + } + }; + System.setSecurityManager(securityManager); + + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + AnnotatedBeanDefinitionReader reader = new AnnotatedBeanDefinitionReader(bf); + reader.register(C1.class); + assertThat(bf.containsBean("c1")).isFalse(); + } + + + @Component("c1") + @Profile("p1") + static class C1 { + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/support/FactoryBeanAndApplicationListener.java b/spring-context/src/test/java/org/springframework/context/support/FactoryBeanAndApplicationListener.java new file mode 100644 index 0000000..c392ed7 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/support/FactoryBeanAndApplicationListener.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; + +/** + * @author Juergen Hoeller + * @since 06.10.2004 + */ +public class FactoryBeanAndApplicationListener implements FactoryBean, ApplicationListener { + + @Override + public String getObject() throws Exception { + return ""; + } + + @Override + public Class getObjectType() { + return String.class; + } + + @Override + public boolean isSingleton() { + return true; + } + + @Override + public void onApplicationEvent(ApplicationEvent event) { + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java b/spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java new file mode 100644 index 0000000..36604c6 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java @@ -0,0 +1,240 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.metrics.jfr.FlightRecorderApplicationStartup; +import org.springframework.util.ObjectUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * @author Juergen Hoeller + * @author Chris Beams + */ +public class GenericApplicationContextTests { + + @Test + void getBeanForClass() { + GenericApplicationContext ac = new GenericApplicationContext(); + ac.registerBeanDefinition("testBean", new RootBeanDefinition(String.class)); + ac.refresh(); + + assertThat(ac.getBean("testBean")).isEqualTo(""); + assertThat(ac.getBean(String.class)).isSameAs(ac.getBean("testBean")); + assertThat(ac.getBean(CharSequence.class)).isSameAs(ac.getBean("testBean")); + + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(() -> + ac.getBean(Object.class)); + } + + @Test + void withSingletonSupplier() { + GenericApplicationContext ac = new GenericApplicationContext(); + ac.registerBeanDefinition("testBean", new RootBeanDefinition(String.class, ac::toString)); + ac.refresh(); + + assertThat(ac.getBean("testBean")).isSameAs(ac.getBean("testBean")); + assertThat(ac.getBean(String.class)).isSameAs(ac.getBean("testBean")); + assertThat(ac.getBean(CharSequence.class)).isSameAs(ac.getBean("testBean")); + assertThat(ac.getBean("testBean")).isEqualTo(ac.toString()); + } + + @Test + void withScopedSupplier() { + GenericApplicationContext ac = new GenericApplicationContext(); + ac.registerBeanDefinition("testBean", + new RootBeanDefinition(String.class, BeanDefinition.SCOPE_PROTOTYPE, ac::toString)); + ac.refresh(); + + assertThat(ac.getBean("testBean")).isNotSameAs(ac.getBean("testBean")); + assertThat(ac.getBean(String.class)).isEqualTo(ac.getBean("testBean")); + assertThat(ac.getBean(CharSequence.class)).isEqualTo(ac.getBean("testBean")); + assertThat(ac.getBean("testBean")).isEqualTo(ac.toString()); + } + + @Test + void accessAfterClosing() { + GenericApplicationContext ac = new GenericApplicationContext(); + ac.registerBeanDefinition("testBean", new RootBeanDefinition(String.class)); + ac.refresh(); + + assertThat(ac.getBean(String.class)).isSameAs(ac.getBean("testBean")); + assertThat(ac.getAutowireCapableBeanFactory().getBean(String.class)).isSameAs(ac.getAutowireCapableBeanFactory().getBean("testBean")); + + ac.close(); + + assertThatIllegalStateException().isThrownBy(() -> + ac.getBean(String.class)); + + assertThatIllegalStateException().isThrownBy(() -> { + ac.getAutowireCapableBeanFactory().getBean("testBean"); + ac.getAutowireCapableBeanFactory().getBean(String.class); + }); + } + + @Test + void individualBeans() { + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBean(BeanA.class); + context.registerBean(BeanB.class); + context.registerBean(BeanC.class); + context.refresh(); + + assertThat(context.getBean(BeanA.class).b).isSameAs(context.getBean(BeanB.class)); + assertThat(context.getBean(BeanA.class).c).isSameAs(context.getBean(BeanC.class)); + assertThat(context.getBean(BeanB.class).applicationContext).isSameAs(context); + } + + @Test + void individualNamedBeans() { + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBean("a", BeanA.class); + context.registerBean("b", BeanB.class); + context.registerBean("c", BeanC.class); + context.refresh(); + + assertThat(context.getBean("a", BeanA.class).b).isSameAs(context.getBean("b")); + assertThat(context.getBean("a", BeanA.class).c).isSameAs(context.getBean("c")); + assertThat(context.getBean("b", BeanB.class).applicationContext).isSameAs(context); + } + + @Test + void individualBeanWithSupplier() { + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBean(BeanA.class, + () -> new BeanA(context.getBean(BeanB.class), context.getBean(BeanC.class))); + context.registerBean(BeanB.class, BeanB::new); + context.registerBean(BeanC.class, BeanC::new); + context.refresh(); + + assertThat(context.getBeanFactory().containsSingleton(BeanA.class.getName())).isTrue(); + assertThat(context.getBean(BeanA.class).b).isSameAs(context.getBean(BeanB.class)); + assertThat(context.getBean(BeanA.class).c).isSameAs(context.getBean(BeanC.class)); + assertThat(context.getBean(BeanB.class).applicationContext).isSameAs(context); + + assertThat(context.getDefaultListableBeanFactory().getDependentBeans(BeanB.class.getName())).isEqualTo(new String[] {BeanA.class.getName()}); + assertThat(context.getDefaultListableBeanFactory().getDependentBeans(BeanC.class.getName())).isEqualTo(new String[] {BeanA.class.getName()}); + } + + @Test + void individualBeanWithSupplierAndCustomizer() { + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBean(BeanA.class, + () -> new BeanA(context.getBean(BeanB.class), context.getBean(BeanC.class)), + bd -> bd.setLazyInit(true)); + context.registerBean(BeanB.class, BeanB::new); + context.registerBean(BeanC.class, BeanC::new); + context.refresh(); + + assertThat(context.getBeanFactory().containsSingleton(BeanA.class.getName())).isFalse(); + assertThat(context.getBean(BeanA.class).b).isSameAs(context.getBean(BeanB.class)); + assertThat(context.getBean(BeanA.class).c).isSameAs(context.getBean(BeanC.class)); + assertThat(context.getBean(BeanB.class).applicationContext).isSameAs(context); + } + + @Test + void individualNamedBeanWithSupplier() { + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBean("a", BeanA.class, + () -> new BeanA(context.getBean(BeanB.class), context.getBean(BeanC.class))); + context.registerBean("b", BeanB.class, BeanB::new); + context.registerBean("c", BeanC.class, BeanC::new); + context.refresh(); + + assertThat(context.getBeanFactory().containsSingleton("a")).isTrue(); + assertThat(context.getBean(BeanA.class).b).isSameAs(context.getBean("b", BeanB.class)); + assertThat(context.getBean("a", BeanA.class).c).isSameAs(context.getBean("c")); + assertThat(context.getBean("b", BeanB.class).applicationContext).isSameAs(context); + } + + @Test + void individualNamedBeanWithSupplierAndCustomizer() { + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBean("a", BeanA.class, + () -> new BeanA(context.getBean(BeanB.class), context.getBean(BeanC.class)), + bd -> bd.setLazyInit(true)); + context.registerBean("b", BeanB.class, BeanB::new); + context.registerBean("c", BeanC.class, BeanC::new); + context.refresh(); + + assertThat(context.getBeanFactory().containsSingleton("a")).isFalse(); + assertThat(context.getBean(BeanA.class).b).isSameAs(context.getBean("b", BeanB.class)); + assertThat(context.getBean("a", BeanA.class).c).isSameAs(context.getBean("c")); + assertThat(context.getBean("b", BeanB.class).applicationContext).isSameAs(context); + } + + @Test + void individualBeanWithNullReturningSupplier() { + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBean("a", BeanA.class, () -> null); + context.registerBean("b", BeanB.class, BeanB::new); + context.registerBean("c", BeanC.class, BeanC::new); + context.refresh(); + + assertThat(ObjectUtils.containsElement(context.getBeanNamesForType(BeanA.class), "a")).isTrue(); + assertThat(ObjectUtils.containsElement(context.getBeanNamesForType(BeanB.class), "b")).isTrue(); + assertThat(ObjectUtils.containsElement(context.getBeanNamesForType(BeanC.class), "c")).isTrue(); + assertThat(context.getBeansOfType(BeanA.class).isEmpty()).isTrue(); + assertThat(context.getBeansOfType(BeanB.class).values().iterator().next()).isSameAs(context.getBean(BeanB.class)); + assertThat(context.getBeansOfType(BeanC.class).values().iterator().next()).isSameAs(context.getBean(BeanC.class)); + } + + @Test + void configureApplicationStartupOnBeanFactory() { + FlightRecorderApplicationStartup applicationStartup = new FlightRecorderApplicationStartup(); + GenericApplicationContext context = new GenericApplicationContext(); + context.setApplicationStartup(applicationStartup); + assertThat(context.getBeanFactory().getApplicationStartup()).isEqualTo(applicationStartup); + } + + + static class BeanA { + + BeanB b; + BeanC c; + + public BeanA(BeanB b, BeanC c) { + this.b = b; + this.c = c; + } + } + + static class BeanB implements ApplicationContextAware { + + ApplicationContext applicationContext; + + public BeanB() { + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + } + + static class BeanC {} + +} diff --git a/spring-context/src/test/java/org/springframework/context/support/GenericXmlApplicationContextTests.java b/spring-context/src/test/java/org/springframework/context/support/GenericXmlApplicationContextTests.java new file mode 100644 index 0000000..cfb96ac --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/support/GenericXmlApplicationContextTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.ApplicationContext; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Unit tests for {@link GenericXmlApplicationContext}. + * + * See SPR-7530. + * + * @author Chris Beams + */ +public class GenericXmlApplicationContextTests { + + private static final Class RELATIVE_CLASS = GenericXmlApplicationContextTests.class; + private static final String RESOURCE_BASE_PATH = ClassUtils.classPackageAsResourcePath(RELATIVE_CLASS); + private static final String RESOURCE_NAME = GenericXmlApplicationContextTests.class.getSimpleName() + "-context.xml"; + private static final String FQ_RESOURCE_PATH = RESOURCE_BASE_PATH + '/' + RESOURCE_NAME; + private static final String TEST_BEAN_NAME = "testBean"; + + + @Test + public void classRelativeResourceLoading_ctor() { + ApplicationContext ctx = new GenericXmlApplicationContext(RELATIVE_CLASS, RESOURCE_NAME); + assertThat(ctx.containsBean(TEST_BEAN_NAME)).isTrue(); + } + + @Test + public void classRelativeResourceLoading_load() { + GenericXmlApplicationContext ctx = new GenericXmlApplicationContext(); + ctx.load(RELATIVE_CLASS, RESOURCE_NAME); + ctx.refresh(); + assertThat(ctx.containsBean(TEST_BEAN_NAME)).isTrue(); + } + + @Test + public void fullyQualifiedResourceLoading_ctor() { + ApplicationContext ctx = new GenericXmlApplicationContext(FQ_RESOURCE_PATH); + assertThat(ctx.containsBean(TEST_BEAN_NAME)).isTrue(); + } + + @Test + public void fullyQualifiedResourceLoading_load() { + GenericXmlApplicationContext ctx = new GenericXmlApplicationContext(); + ctx.load(FQ_RESOURCE_PATH); + ctx.refresh(); + assertThat(ctx.containsBean(TEST_BEAN_NAME)).isTrue(); + } +} diff --git a/spring-context/src/test/java/org/springframework/context/support/LifecycleTestBean.java b/spring-context/src/test/java/org/springframework/context/support/LifecycleTestBean.java new file mode 100644 index 0000000..2207407 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/support/LifecycleTestBean.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import org.springframework.context.Lifecycle; + +/** + * @author Mark Fisher + */ +public class LifecycleTestBean implements Lifecycle { + + private static int startCounter; + + private static int stopCounter; + + + private int startOrder; + + private int stopOrder; + + private boolean running; + + + public int getStartOrder() { + return startOrder; + } + + public int getStopOrder() { + return stopOrder; + } + + @Override + public boolean isRunning() { + return this.running; + } + + @Override + public void start() { + this.startOrder = ++startCounter; + this.running = true; + } + + @Override + public void stop() { + this.stopOrder = ++stopCounter; + this.running = false; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/support/LiveBeansViewTests.java b/spring-context/src/test/java/org/springframework/context/support/LiveBeansViewTests.java new file mode 100644 index 0000000..56c9ef4 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/support/LiveBeansViewTests.java @@ -0,0 +1,121 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.lang.management.ManagementFactory; +import java.util.Set; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; + +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link LiveBeansView} + * + * @author Stephane Nicoll + * @author Sam Brannen + */ +class LiveBeansViewTests { + + private final MockEnvironment environment = new MockEnvironment(); + + + @Test + void registerIgnoredIfPropertyIsNotSet(TestInfo testInfo) throws MalformedObjectNameException { + ConfigurableApplicationContext context = createApplicationContext("app"); + assertThat(searchLiveBeansViewMeans(testInfo).size()).isEqualTo(0); + LiveBeansView.registerApplicationContext(context); + assertThat(searchLiveBeansViewMeans(testInfo).size()).isEqualTo(0); + LiveBeansView.unregisterApplicationContext(context); + } + + @Test + void registerUnregisterSingleContext(TestInfo testInfo) throws MalformedObjectNameException { + this.environment.setProperty(LiveBeansView.MBEAN_DOMAIN_PROPERTY_NAME, + testInfo.getTestMethod().get().getName()); + ConfigurableApplicationContext context = createApplicationContext("app"); + assertThat(searchLiveBeansViewMeans(testInfo).size()).isEqualTo(0); + LiveBeansView.registerApplicationContext(context); + assertSingleLiveBeansViewMbean(testInfo, "app"); + LiveBeansView.unregisterApplicationContext(context); + assertThat(searchLiveBeansViewMeans(testInfo).size()).isEqualTo(0); + } + + @Test + void registerUnregisterSeveralContexts(TestInfo testInfo) throws MalformedObjectNameException { + this.environment.setProperty(LiveBeansView.MBEAN_DOMAIN_PROPERTY_NAME, + testInfo.getTestMethod().get().getName()); + ConfigurableApplicationContext context = createApplicationContext("app"); + ConfigurableApplicationContext childContext = createApplicationContext("child"); + assertThat(searchLiveBeansViewMeans(testInfo).size()).isEqualTo(0); + LiveBeansView.registerApplicationContext(context); + assertSingleLiveBeansViewMbean(testInfo, "app"); + LiveBeansView.registerApplicationContext(childContext); + // Only one MBean + assertThat(searchLiveBeansViewMeans(testInfo).size()).isEqualTo(1); + LiveBeansView.unregisterApplicationContext(childContext); + assertSingleLiveBeansViewMbean(testInfo, "app"); // Root context removes it + LiveBeansView.unregisterApplicationContext(context); + assertThat(searchLiveBeansViewMeans(testInfo).size()).isEqualTo(0); + } + + @Test + void registerUnregisterSeveralContextsDifferentOrder(TestInfo testInfo) throws MalformedObjectNameException { + this.environment.setProperty(LiveBeansView.MBEAN_DOMAIN_PROPERTY_NAME, + testInfo.getTestMethod().get().getName()); + ConfigurableApplicationContext context = createApplicationContext("app"); + ConfigurableApplicationContext childContext = createApplicationContext("child"); + assertThat(searchLiveBeansViewMeans(testInfo).size()).isEqualTo(0); + LiveBeansView.registerApplicationContext(context); + assertSingleLiveBeansViewMbean(testInfo, "app"); + LiveBeansView.registerApplicationContext(childContext); + assertSingleLiveBeansViewMbean(testInfo, "app"); // Only one MBean + LiveBeansView.unregisterApplicationContext(context); + LiveBeansView.unregisterApplicationContext(childContext); + assertThat(searchLiveBeansViewMeans(testInfo).size()).isEqualTo(0); + } + + private ConfigurableApplicationContext createApplicationContext(String applicationName) { + ConfigurableApplicationContext context = mock(ConfigurableApplicationContext.class); + given(context.getEnvironment()).willReturn(this.environment); + given(context.getApplicationName()).willReturn(applicationName); + return context; + } + + private void assertSingleLiveBeansViewMbean(TestInfo testInfo, String applicationName) throws MalformedObjectNameException { + Set objectNames = searchLiveBeansViewMeans(testInfo); + assertThat(objectNames.size()).isEqualTo(1); + assertThat(objectNames.iterator().next().getCanonicalName()).as("Wrong MBean name").isEqualTo( + String.format("%s:application=%s", testInfo.getTestMethod().get().getName(), applicationName)); + } + + private Set searchLiveBeansViewMeans(TestInfo testInfo) throws MalformedObjectNameException { + String objectName = String.format("%s:*,%s=*", testInfo.getTestMethod().get().getName(), + LiveBeansView.MBEAN_APPLICATION_KEY); + return ManagementFactory.getPlatformMBeanServer().queryNames(new ObjectName(objectName), null); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/support/Logic.java b/spring-context/src/test/java/org/springframework/context/support/Logic.java new file mode 100644 index 0000000..6ac62b3 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/support/Logic.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import org.springframework.beans.factory.BeanNameAware; + +public class Logic implements BeanNameAware { + + private String name; + + @SuppressWarnings("unused") + private Assembler a; + + public void setAssembler(Assembler a) { + this.a = a; + } + + @Override + public void setBeanName(String name) { + this.name = name; + } + + public void output() { + System.out.println("Bean " + name); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/support/NoOpAdvice.java b/spring-context/src/test/java/org/springframework/context/support/NoOpAdvice.java new file mode 100644 index 0000000..0ade47c --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/support/NoOpAdvice.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import org.springframework.aop.ThrowsAdvice; + +/** + * Advice object that implements multiple Advice interfaces. + * + * @author Chris Beams + */ +public class NoOpAdvice implements ThrowsAdvice { + + public void afterThrowing(Exception ex) throws Throwable { + // no-op + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/support/PropertyResourceConfigurerIntegrationTests.java b/spring-context/src/test/java/org/springframework/context/support/PropertyResourceConfigurerIntegrationTests.java new file mode 100644 index 0000000..82a018c --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/support/PropertyResourceConfigurerIntegrationTests.java @@ -0,0 +1,168 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.io.FileNotFoundException; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer; +import org.springframework.beans.factory.config.PropertyResourceConfigurer; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.ApplicationContext; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Integration tests for {@link PropertyResourceConfigurer} implementations requiring + * interaction with an {@link ApplicationContext}. For example, a {@link PropertyPlaceholderConfigurer} + * that contains ${..} tokens in its 'location' property requires being tested through an ApplicationContext + * as opposed to using only a BeanFactory during testing. + * + * @author Chris Beams + * @author Sam Brannen + * @see org.springframework.beans.factory.config.PropertyResourceConfigurerTests + */ +public class PropertyResourceConfigurerIntegrationTests { + + @Test + public void testPropertyPlaceholderConfigurerWithSystemPropertyInLocation() { + StaticApplicationContext ac = new StaticApplicationContext(); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("spouse", new RuntimeBeanReference("${ref}")); + ac.registerSingleton("tb", TestBean.class, pvs); + pvs = new MutablePropertyValues(); + pvs.add("location", "${user.dir}/test"); + ac.registerSingleton("configurer", PropertyPlaceholderConfigurer.class, pvs); + String userDir = getUserDir(); + assertThatExceptionOfType(BeanInitializationException.class) + .isThrownBy(ac::refresh) + .withCauseInstanceOf(FileNotFoundException.class) + .withMessageContaining(userDir); + } + + @Test + public void testPropertyPlaceholderConfigurerWithSystemPropertiesInLocation() { + StaticApplicationContext ac = new StaticApplicationContext(); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("spouse", new RuntimeBeanReference("${ref}")); + ac.registerSingleton("tb", TestBean.class, pvs); + pvs = new MutablePropertyValues(); + pvs.add("location", "${user.dir}/test/${user.dir}"); + ac.registerSingleton("configurer", PropertyPlaceholderConfigurer.class, pvs); + String userDir = getUserDir(); + assertThatExceptionOfType(BeanInitializationException.class) + .isThrownBy(ac::refresh) + .withCauseInstanceOf(FileNotFoundException.class) + .matches(ex -> ex.getMessage().contains(userDir + "/test/" + userDir) || + ex.getMessage().contains(userDir + "/test//" + userDir)); + } + + private String getUserDir() { + // slight hack for Linux/Unix systems + String userDir = StringUtils.cleanPath(System.getProperty("user.dir")); + if (userDir.startsWith("/")) { + userDir = userDir.substring(1); + } + return userDir; + } + + @Test + public void testPropertyPlaceholderConfigurerWithUnresolvableSystemPropertiesInLocation() { + StaticApplicationContext ac = new StaticApplicationContext(); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("spouse", new RuntimeBeanReference("${ref}")); + ac.registerSingleton("tb", TestBean.class, pvs); + pvs = new MutablePropertyValues(); + pvs.add("location", "${myprop}/test/${myprop}"); + ac.registerSingleton("configurer", PropertyPlaceholderConfigurer.class, pvs); + assertThatExceptionOfType(BeanInitializationException.class) + .isThrownBy(ac::refresh) + .withMessageContaining("myprop"); + } + + @Test + public void testPropertyPlaceholderConfigurerWithMultiLevelCircularReference() { + StaticApplicationContext ac = new StaticApplicationContext(); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "name${var}"); + ac.registerSingleton("tb1", TestBean.class, pvs); + pvs = new MutablePropertyValues(); + pvs.add("properties", "var=${m}var\nm=${var2}\nvar2=${var}"); + ac.registerSingleton("configurer1", PropertyPlaceholderConfigurer.class, pvs); + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(ac::refresh); + } + + @Test + public void testPropertyPlaceholderConfigurerWithNestedCircularReference() { + StaticApplicationContext ac = new StaticApplicationContext(); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "name${var}"); + ac.registerSingleton("tb1", TestBean.class, pvs); + pvs = new MutablePropertyValues(); + pvs.add("properties", "var=${m}var\nm=${var2}\nvar2=${m}"); + ac.registerSingleton("configurer1", PropertyPlaceholderConfigurer.class, pvs); + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(ac::refresh); + } + + @Test + public void testPropertyPlaceholderConfigurerWithNestedUnresolvableReference() { + StaticApplicationContext ac = new StaticApplicationContext(); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "name${var}"); + ac.registerSingleton("tb1", TestBean.class, pvs); + pvs = new MutablePropertyValues(); + pvs.add("properties", "var=${m}var\nm=${var2}\nvar2=${m2}"); + ac.registerSingleton("configurer1", PropertyPlaceholderConfigurer.class, pvs); + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(ac::refresh); + } + + @Test + public void testPropertyPlaceholderConfigurerWithValueFromSystemProperty() { + final String propertyName = getClass().getName() + ".test"; + + try { + System.setProperty(propertyName, "mytest"); + + StaticApplicationContext context = new StaticApplicationContext(); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.addPropertyValue("touchy", "${" + propertyName + "}"); + context.registerSingleton("tb", TestBean.class, pvs); + + pvs = new MutablePropertyValues(); + pvs.addPropertyValue("target", new RuntimeBeanReference("tb")); + context.registerSingleton("tbProxy", org.springframework.aop.framework.ProxyFactoryBean.class, pvs); + + context.registerSingleton("configurer", PropertyPlaceholderConfigurer.class); + context.refresh(); + + TestBean testBean = context.getBean("tb", TestBean.class); + assertThat(testBean.getTouchy()).isEqualTo("mytest"); + } + finally { + System.clearProperty(propertyName); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurerTests.java b/spring-context/src/test/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurerTests.java new file mode 100644 index 0000000..03606bc --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurerTests.java @@ -0,0 +1,405 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.util.Optional; +import java.util.Properties; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.testfixture.env.MockPropertySource; +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.springframework.beans.factory.support.BeanDefinitionBuilder.genericBeanDefinition; +import static org.springframework.beans.factory.support.BeanDefinitionBuilder.rootBeanDefinition; + +/** + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + */ +public class PropertySourcesPlaceholderConfigurerTests { + + + @Test + public void replacementFromEnvironmentProperties() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("testBean", + genericBeanDefinition(TestBean.class) + .addPropertyValue("name", "${my.name}") + .getBeanDefinition()); + + MockEnvironment env = new MockEnvironment(); + env.setProperty("my.name", "myValue"); + + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setEnvironment(env); + ppc.postProcessBeanFactory(bf); + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("myValue"); + assertThat(ppc.getAppliedPropertySources()).isNotNull(); + } + + @Test + public void localPropertiesViaResource() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("testBean", + genericBeanDefinition(TestBean.class) + .addPropertyValue("name", "${my.name}") + .getBeanDefinition()); + + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + Resource resource = new ClassPathResource("PropertySourcesPlaceholderConfigurerTests.properties", this.getClass()); + ppc.setLocation(resource); + ppc.postProcessBeanFactory(bf); + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("foo"); + } + + @Test + public void localPropertiesOverrideFalse() { + localPropertiesOverride(false); + } + + @Test + public void localPropertiesOverrideTrue() { + localPropertiesOverride(true); + } + + @Test + public void explicitPropertySources() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("testBean", + genericBeanDefinition(TestBean.class) + .addPropertyValue("name", "${my.name}") + .getBeanDefinition()); + + MutablePropertySources propertySources = new MutablePropertySources(); + propertySources.addLast(new MockPropertySource().withProperty("my.name", "foo")); + + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setPropertySources(propertySources); + ppc.postProcessBeanFactory(bf); + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("foo"); + assertThat(propertySources.iterator().next()).isEqualTo(ppc.getAppliedPropertySources().iterator().next()); + } + + @Test + public void explicitPropertySourcesExcludesEnvironment() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("testBean", + genericBeanDefinition(TestBean.class) + .addPropertyValue("name", "${my.name}") + .getBeanDefinition()); + + MutablePropertySources propertySources = new MutablePropertySources(); + propertySources.addLast(new MockPropertySource()); + + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setPropertySources(propertySources); + ppc.setEnvironment(new MockEnvironment().withProperty("my.name", "env")); + ppc.setIgnoreUnresolvablePlaceholders(true); + ppc.postProcessBeanFactory(bf); + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("${my.name}"); + assertThat(propertySources.iterator().next()).isEqualTo(ppc.getAppliedPropertySources().iterator().next()); + } + + @Test + @SuppressWarnings("serial") + public void explicitPropertySourcesExcludesLocalProperties() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("testBean", + genericBeanDefinition(TestBean.class) + .addPropertyValue("name", "${my.name}") + .getBeanDefinition()); + + MutablePropertySources propertySources = new MutablePropertySources(); + propertySources.addLast(new MockPropertySource()); + + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setPropertySources(propertySources); + ppc.setProperties(new Properties() {{ + put("my.name", "local"); + }}); + ppc.setIgnoreUnresolvablePlaceholders(true); + ppc.postProcessBeanFactory(bf); + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("${my.name}"); + } + + @Test + public void ignoreUnresolvablePlaceholders_falseIsDefault() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("testBean", + genericBeanDefinition(TestBean.class) + .addPropertyValue("name", "${my.name}") + .getBeanDefinition()); + + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + //pc.setIgnoreUnresolvablePlaceholders(false); // the default + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> + ppc.postProcessBeanFactory(bf)); + } + + @Test + public void ignoreUnresolvablePlaceholders_true() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("testBean", + genericBeanDefinition(TestBean.class) + .addPropertyValue("name", "${my.name}") + .getBeanDefinition()); + + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setIgnoreUnresolvablePlaceholders(true); + ppc.postProcessBeanFactory(bf); + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("${my.name}"); + } + + @Test + @SuppressWarnings("serial") + public void nestedUnresolvablePlaceholder() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("testBean", + genericBeanDefinition(TestBean.class) + .addPropertyValue("name", "${my.name}") + .getBeanDefinition()); + + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setProperties(new Properties() {{ + put("my.name", "${bogus}"); + }}); + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> + ppc.postProcessBeanFactory(bf)); + } + + @Test + @SuppressWarnings("serial") + public void ignoredNestedUnresolvablePlaceholder() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("testBean", + genericBeanDefinition(TestBean.class) + .addPropertyValue("name", "${my.name}") + .getBeanDefinition()); + + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setProperties(new Properties() {{ + put("my.name", "${bogus}"); + }}); + ppc.setIgnoreUnresolvablePlaceholders(true); + ppc.postProcessBeanFactory(bf); + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("${bogus}"); + } + + @Test + public void withNonEnumerablePropertySource() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("testBean", + genericBeanDefinition(TestBean.class) + .addPropertyValue("name", "${foo}") + .getBeanDefinition()); + + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + + PropertySource ps = new PropertySource("simplePropertySource", new Object()) { + @Override + public Object getProperty(String key) { + return "bar"; + } + }; + + MockEnvironment env = new MockEnvironment(); + env.getPropertySources().addFirst(ps); + ppc.setEnvironment(env); + + ppc.postProcessBeanFactory(bf); + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("bar"); + } + + @SuppressWarnings("serial") + private void localPropertiesOverride(boolean override) { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("testBean", + genericBeanDefinition(TestBean.class) + .addPropertyValue("name", "${foo}") + .getBeanDefinition()); + + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + + ppc.setLocalOverride(override); + ppc.setProperties(new Properties() {{ + setProperty("foo", "local"); + }}); + ppc.setEnvironment(new MockEnvironment().withProperty("foo", "enclosing")); + ppc.postProcessBeanFactory(bf); + if (override) { + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("local"); + } + else { + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("enclosing"); + } + } + + @Test + public void customPlaceholderPrefixAndSuffix() { + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setPlaceholderPrefix("@<"); + ppc.setPlaceholderSuffix(">"); + + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("testBean", + rootBeanDefinition(TestBean.class) + .addPropertyValue("name", "@") + .addPropertyValue("sex", "${key2}") + .getBeanDefinition()); + + System.setProperty("key1", "systemKey1Value"); + System.setProperty("key2", "systemKey2Value"); + ppc.setEnvironment(new StandardEnvironment()); + ppc.postProcessBeanFactory(bf); + System.clearProperty("key1"); + System.clearProperty("key2"); + + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("systemKey1Value"); + assertThat(bf.getBean(TestBean.class).getSex()).isEqualTo("${key2}"); + } + + @Test + public void nullValueIsPreserved() { + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setNullValue("customNull"); + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("testBean", rootBeanDefinition(TestBean.class) + .addPropertyValue("name", "${my.name}") + .getBeanDefinition()); + ppc.setEnvironment(new MockEnvironment().withProperty("my.name", "customNull")); + ppc.postProcessBeanFactory(bf); + assertThat(bf.getBean(TestBean.class).getName()).isNull(); + } + + @Test + public void trimValuesIsOffByDefault() { + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("testBean", rootBeanDefinition(TestBean.class) + .addPropertyValue("name", "${my.name}") + .getBeanDefinition()); + ppc.setEnvironment(new MockEnvironment().withProperty("my.name", " myValue ")); + ppc.postProcessBeanFactory(bf); + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo(" myValue "); + } + + @Test + public void trimValuesIsApplied() { + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setTrimValues(true); + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("testBean", rootBeanDefinition(TestBean.class) + .addPropertyValue("name", "${my.name}") + .getBeanDefinition()); + ppc.setEnvironment(new MockEnvironment().withProperty("my.name", " myValue ")); + ppc.postProcessBeanFactory(bf); + assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("myValue"); + } + + @Test + public void getAppliedPropertySourcesTooEarly() throws Exception { + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + assertThatIllegalStateException().isThrownBy( + ppc::getAppliedPropertySources); + } + + @Test + public void multipleLocationsWithDefaultResolvedValue() throws Exception { + // SPR-10619 + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ClassPathResource doesNotHave = new ClassPathResource("test.properties", getClass()); + ClassPathResource setToTrue = new ClassPathResource("placeholder.properties", getClass()); + ppc.setLocations(doesNotHave, setToTrue); + ppc.setIgnoreResourceNotFound(true); + ppc.setIgnoreUnresolvablePlaceholders(true); + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.registerBeanDefinition("testBean", + genericBeanDefinition(TestBean.class) + .addPropertyValue("jedi", "${jedi:false}") + .getBeanDefinition()); + ppc.postProcessBeanFactory(bf); + assertThat(bf.getBean(TestBean.class).isJedi()).isEqualTo(true); + } + + @Test + public void optionalPropertyWithValue() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.setConversionService(new DefaultConversionService()); + bf.registerBeanDefinition("testBean", + genericBeanDefinition(OptionalTestBean.class) + .addPropertyValue("name", "${my.name}") + .getBeanDefinition()); + + MockEnvironment env = new MockEnvironment(); + env.setProperty("my.name", "myValue"); + + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setEnvironment(env); + ppc.setIgnoreUnresolvablePlaceholders(true); + ppc.postProcessBeanFactory(bf); + assertThat(bf.getBean(OptionalTestBean.class).getName()).isEqualTo(Optional.of("myValue")); + } + + @Test + public void optionalPropertyWithoutValue() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.setConversionService(new DefaultConversionService()); + bf.registerBeanDefinition("testBean", + genericBeanDefinition(OptionalTestBean.class) + .addPropertyValue("name", "${my.name}") + .getBeanDefinition()); + + MockEnvironment env = new MockEnvironment(); + env.setProperty("my.name", ""); + + PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer(); + ppc.setEnvironment(env); + ppc.setIgnoreUnresolvablePlaceholders(true); + ppc.setNullValue(""); + ppc.postProcessBeanFactory(bf); + assertThat(bf.getBean(OptionalTestBean.class).getName()).isEqualTo(Optional.empty()); + } + + + private static class OptionalTestBean { + + private Optional name; + + public Optional getName() { + return name; + } + + public void setName(Optional name) { + this.name = name; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/support/ResourceBundleMessageSourceTests.java b/spring-context/src/test/java/org/springframework/context/support/ResourceBundleMessageSourceTests.java new file mode 100644 index 0000000..a0c6fc1 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/support/ResourceBundleMessageSourceTests.java @@ -0,0 +1,438 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.util.List; +import java.util.Locale; +import java.util.Properties; +import java.util.ResourceBundle; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.NoSuchMessageException; +import org.springframework.context.i18n.LocaleContextHolder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Juergen Hoeller + * @since 03.02.2004 + */ +class ResourceBundleMessageSourceTests { + + @Test + void messageAccessWithDefaultMessageSource() { + doTestMessageAccess(false, true, false, false, false); + } + + @Test + void messageAccessWithDefaultMessageSourceAndMessageFormat() { + doTestMessageAccess(false, true, false, false, true); + } + + @Test + void messageAccessWithDefaultMessageSourceAndFallbackToGerman() { + doTestMessageAccess(false, true, true, true, false); + } + + @Test + void messageAccessWithDefaultMessageSourceAndFallbackTurnedOff() { + doTestMessageAccess(false, false, false, false, false); + } + + @Test + void messageAccessWithDefaultMessageSourceAndFallbackTurnedOffAndFallbackToGerman() { + doTestMessageAccess(false, false, true, true, false); + } + + @Test + void messageAccessWithReloadableMessageSource() { + doTestMessageAccess(true, true, false, false, false); + } + + @Test + void messageAccessWithReloadableMessageSourceAndMessageFormat() { + doTestMessageAccess(true, true, false, false, true); + } + + @Test + void messageAccessWithReloadableMessageSourceAndFallbackToGerman() { + doTestMessageAccess(true, true, true, true, false); + } + + @Test + void messageAccessWithReloadableMessageSourceAndFallbackTurnedOff() { + doTestMessageAccess(true, false, false, false, false); + } + + @Test + void messageAccessWithReloadableMessageSourceAndFallbackTurnedOffAndFallbackToGerman() { + doTestMessageAccess(true, false, true, true, false); + } + + protected void doTestMessageAccess( + boolean reloadable, boolean fallbackToSystemLocale, + boolean expectGermanFallback, boolean useCodeAsDefaultMessage, boolean alwaysUseMessageFormat) { + + StaticApplicationContext ac = new StaticApplicationContext(); + if (reloadable) { + StaticApplicationContext parent = new StaticApplicationContext(); + parent.refresh(); + ac.setParent(parent); + } + + MutablePropertyValues pvs = new MutablePropertyValues(); + String basepath = "org/springframework/context/support/"; + String[] basenames; + if (reloadable) { + basenames = new String[] { + "classpath:" + basepath + "messages", + "classpath:" + basepath + "more-messages"}; + } + else { + basenames = new String[] { + basepath + "messages", + basepath + "more-messages"}; + } + pvs.add("basenames", basenames); + if (!fallbackToSystemLocale) { + pvs.add("fallbackToSystemLocale", Boolean.FALSE); + } + if (useCodeAsDefaultMessage) { + pvs.add("useCodeAsDefaultMessage", Boolean.TRUE); + } + if (alwaysUseMessageFormat) { + pvs.add("alwaysUseMessageFormat", Boolean.TRUE); + } + Class clazz = reloadable ? + (Class) ReloadableResourceBundleMessageSource.class : ResourceBundleMessageSource.class; + ac.registerSingleton("messageSource", clazz, pvs); + ac.refresh(); + + Locale.setDefault(expectGermanFallback ? Locale.GERMAN : Locale.CANADA); + assertThat(ac.getMessage("code1", null, Locale.ENGLISH)).isEqualTo("message1"); + Object expected = fallbackToSystemLocale && expectGermanFallback ? "nachricht2" : "message2"; + assertThat(ac.getMessage("code2", null, Locale.ENGLISH)).isEqualTo(expected); + + assertThat(ac.getMessage("code2", null, Locale.GERMAN)).isEqualTo("nachricht2"); + assertThat(ac.getMessage("code2", null, new Locale("DE", "at"))).isEqualTo("nochricht2"); + assertThat(ac.getMessage("code2", null, new Locale("DE", "at", "oo"))).isEqualTo("noochricht2"); + + if (reloadable) { + assertThat(ac.getMessage("code2", null, Locale.GERMANY)).isEqualTo("nachricht2xml"); + } + + MessageSourceAccessor accessor = new MessageSourceAccessor(ac); + LocaleContextHolder.setLocale(new Locale("DE", "at")); + try { + assertThat(accessor.getMessage("code2")).isEqualTo("nochricht2"); + } + finally { + LocaleContextHolder.setLocale(null); + } + + assertThat(ac.getMessage("code3", null, Locale.ENGLISH)).isEqualTo("message3"); + MessageSourceResolvable resolvable = new DefaultMessageSourceResolvable("code3"); + assertThat(ac.getMessage(resolvable, Locale.ENGLISH)).isEqualTo("message3"); + resolvable = new DefaultMessageSourceResolvable(new String[] {"code4", "code3"}); + assertThat(ac.getMessage(resolvable, Locale.ENGLISH)).isEqualTo("message3"); + + assertThat(ac.getMessage("code3", null, Locale.ENGLISH)).isEqualTo("message3"); + resolvable = new DefaultMessageSourceResolvable(new String[] {"code4", "code3"}); + assertThat(ac.getMessage(resolvable, Locale.ENGLISH)).isEqualTo("message3"); + + Object[] args = new Object[] {"Hello", new DefaultMessageSourceResolvable(new String[] {"code1"})}; + assertThat(ac.getMessage("hello", args, Locale.ENGLISH)).isEqualTo("Hello, message1"); + + // test default message without and with args + assertThat(ac.getMessage(null, null, null, Locale.ENGLISH)).isNull(); + assertThat(ac.getMessage(null, null, "default", Locale.ENGLISH)).isEqualTo("default"); + assertThat(ac.getMessage(null, args, "default", Locale.ENGLISH)).isEqualTo("default"); + assertThat(ac.getMessage(null, null, "{0}, default", Locale.ENGLISH)).isEqualTo("{0}, default"); + assertThat(ac.getMessage(null, args, "{0}, default", Locale.ENGLISH)).isEqualTo("Hello, default"); + + // test resolvable with default message, without and with args + resolvable = new DefaultMessageSourceResolvable(null, null, "default"); + assertThat(ac.getMessage(resolvable, Locale.ENGLISH)).isEqualTo("default"); + resolvable = new DefaultMessageSourceResolvable(null, args, "default"); + assertThat(ac.getMessage(resolvable, Locale.ENGLISH)).isEqualTo("default"); + resolvable = new DefaultMessageSourceResolvable(null, null, "{0}, default"); + assertThat(ac.getMessage(resolvable, Locale.ENGLISH)).isEqualTo("{0}, default"); + resolvable = new DefaultMessageSourceResolvable(null, args, "{0}, default"); + assertThat(ac.getMessage(resolvable, Locale.ENGLISH)).isEqualTo("Hello, default"); + + // test message args + assertThat(ac.getMessage("hello", new Object[]{"Arg1", "Arg2"}, Locale.ENGLISH)).isEqualTo("Arg1, Arg2"); + assertThat(ac.getMessage("hello", null, Locale.ENGLISH)).isEqualTo("{0}, {1}"); + + if (alwaysUseMessageFormat) { + assertThat(ac.getMessage("escaped", null, Locale.ENGLISH)).isEqualTo("I'm"); + } + else { + assertThat(ac.getMessage("escaped", null, Locale.ENGLISH)).isEqualTo("I''m"); + } + assertThat(ac.getMessage("escaped", new Object[]{"some arg"}, Locale.ENGLISH)).isEqualTo("I'm"); + + if (useCodeAsDefaultMessage) { + assertThat(ac.getMessage("code4", null, Locale.GERMAN)).isEqualTo("code4"); + } + else { + assertThatExceptionOfType(NoSuchMessageException.class).isThrownBy(() -> + ac.getMessage("code4", null, Locale.GERMAN)); + } + } + + @Test + @SuppressWarnings("resource") + void defaultApplicationContextMessageSource() { + GenericApplicationContext ac = new GenericApplicationContext(); + ac.refresh(); + assertThat(ac.getMessage("code1", null, "default", Locale.ENGLISH)).isEqualTo("default"); + assertThat(ac.getMessage("code1", new Object[]{"value"}, "default {0}", Locale.ENGLISH)).isEqualTo("default value"); + } + + @Test + @SuppressWarnings("resource") + void defaultApplicationContextMessageSourceWithParent() { + GenericApplicationContext ac = new GenericApplicationContext(); + GenericApplicationContext parent = new GenericApplicationContext(); + parent.refresh(); + ac.setParent(parent); + ac.refresh(); + assertThat(ac.getMessage("code1", null, "default", Locale.ENGLISH)).isEqualTo("default"); + assertThat(ac.getMessage("code1", new Object[]{"value"}, "default {0}", Locale.ENGLISH)).isEqualTo("default value"); + } + + @Test + @SuppressWarnings("resource") + void staticApplicationContextMessageSourceWithStaticParent() { + StaticApplicationContext ac = new StaticApplicationContext(); + StaticApplicationContext parent = new StaticApplicationContext(); + parent.refresh(); + ac.setParent(parent); + ac.refresh(); + assertThat(ac.getMessage("code1", null, "default", Locale.ENGLISH)).isEqualTo("default"); + assertThat(ac.getMessage("code1", new Object[]{"value"}, "default {0}", Locale.ENGLISH)).isEqualTo("default value"); + } + + @Test + @SuppressWarnings("resource") + void staticApplicationContextMessageSourceWithDefaultParent() { + StaticApplicationContext ac = new StaticApplicationContext(); + GenericApplicationContext parent = new GenericApplicationContext(); + parent.refresh(); + ac.setParent(parent); + ac.refresh(); + assertThat(ac.getMessage("code1", null, "default", Locale.ENGLISH)).isEqualTo("default"); + assertThat(ac.getMessage("code1", new Object[]{"value"}, "default {0}", Locale.ENGLISH)).isEqualTo("default value"); + } + + @Test + void resourceBundleMessageSourceStandalone() { + ResourceBundleMessageSource ms = new ResourceBundleMessageSource(); + ms.setBasename("org/springframework/context/support/messages"); + assertThat(ms.getMessage("code1", null, Locale.ENGLISH)).isEqualTo("message1"); + assertThat(ms.getMessage("code2", null, Locale.GERMAN)).isEqualTo("nachricht2"); + } + + @Test + void resourceBundleMessageSourceWithWhitespaceInBasename() { + ResourceBundleMessageSource ms = new ResourceBundleMessageSource(); + ms.setBasename(" org/springframework/context/support/messages "); + assertThat(ms.getMessage("code1", null, Locale.ENGLISH)).isEqualTo("message1"); + assertThat(ms.getMessage("code2", null, Locale.GERMAN)).isEqualTo("nachricht2"); + } + + @Test + void resourceBundleMessageSourceWithDefaultCharset() { + ResourceBundleMessageSource ms = new ResourceBundleMessageSource(); + ms.setBasename("org/springframework/context/support/messages"); + ms.setDefaultEncoding("ISO-8859-1"); + assertThat(ms.getMessage("code1", null, Locale.ENGLISH)).isEqualTo("message1"); + assertThat(ms.getMessage("code2", null, Locale.GERMAN)).isEqualTo("nachricht2"); + } + + @Test + void resourceBundleMessageSourceWithInappropriateDefaultCharset() { + ResourceBundleMessageSource ms = new ResourceBundleMessageSource(); + ms.setBasename("org/springframework/context/support/messages"); + ms.setDefaultEncoding("argh"); + ms.setFallbackToSystemLocale(false); + assertThatExceptionOfType(NoSuchMessageException.class).isThrownBy(() -> + ms.getMessage("code1", null, Locale.ENGLISH)); + } + + @Test + void reloadableResourceBundleMessageSourceStandalone() { + ReloadableResourceBundleMessageSource ms = new ReloadableResourceBundleMessageSource(); + ms.setBasename("org/springframework/context/support/messages"); + assertThat(ms.getMessage("code1", null, Locale.ENGLISH)).isEqualTo("message1"); + assertThat(ms.getMessage("code2", null, Locale.GERMAN)).isEqualTo("nachricht2"); + } + + @Test + void reloadableResourceBundleMessageSourceWithCacheSeconds() throws InterruptedException { + ReloadableResourceBundleMessageSource ms = new ReloadableResourceBundleMessageSource(); + ms.setBasename("org/springframework/context/support/messages"); + ms.setCacheMillis(100); + // Initial cache attempt + assertThat(ms.getMessage("code1", null, Locale.ENGLISH)).isEqualTo("message1"); + assertThat(ms.getMessage("code2", null, Locale.GERMAN)).isEqualTo("nachricht2"); + Thread.sleep(200); + // Late enough for a re-cache attempt + assertThat(ms.getMessage("code1", null, Locale.ENGLISH)).isEqualTo("message1"); + assertThat(ms.getMessage("code2", null, Locale.GERMAN)).isEqualTo("nachricht2"); + } + + @Test + void reloadableResourceBundleMessageSourceWithNonConcurrentRefresh() throws InterruptedException { + ReloadableResourceBundleMessageSource ms = new ReloadableResourceBundleMessageSource(); + ms.setBasename("org/springframework/context/support/messages"); + ms.setCacheMillis(100); + ms.setConcurrentRefresh(false); + // Initial cache attempt + assertThat(ms.getMessage("code1", null, Locale.ENGLISH)).isEqualTo("message1"); + assertThat(ms.getMessage("code2", null, Locale.GERMAN)).isEqualTo("nachricht2"); + Thread.sleep(200); + // Late enough for a re-cache attempt + assertThat(ms.getMessage("code1", null, Locale.ENGLISH)).isEqualTo("message1"); + assertThat(ms.getMessage("code2", null, Locale.GERMAN)).isEqualTo("nachricht2"); + } + + @Test + void reloadableResourceBundleMessageSourceWithCommonMessages() { + ReloadableResourceBundleMessageSource ms = new ReloadableResourceBundleMessageSource(); + Properties commonMessages = new Properties(); + commonMessages.setProperty("warning", "Do not do {0}"); + ms.setCommonMessages(commonMessages); + ms.setBasename("org/springframework/context/support/messages"); + assertThat(ms.getMessage("code1", null, Locale.ENGLISH)).isEqualTo("message1"); + assertThat(ms.getMessage("code2", null, Locale.GERMAN)).isEqualTo("nachricht2"); + assertThat(ms.getMessage("warning", new Object[]{"this"}, Locale.ENGLISH)).isEqualTo("Do not do this"); + assertThat(ms.getMessage("warning", new Object[]{"that"}, Locale.GERMAN)).isEqualTo("Do not do that"); + } + + @Test + void reloadableResourceBundleMessageSourceWithWhitespaceInBasename() { + ReloadableResourceBundleMessageSource ms = new ReloadableResourceBundleMessageSource(); + ms.setBasename(" org/springframework/context/support/messages "); + assertThat(ms.getMessage("code1", null, Locale.ENGLISH)).isEqualTo("message1"); + assertThat(ms.getMessage("code2", null, Locale.GERMAN)).isEqualTo("nachricht2"); + } + + @Test + void reloadableResourceBundleMessageSourceWithDefaultCharset() { + ReloadableResourceBundleMessageSource ms = new ReloadableResourceBundleMessageSource(); + ms.setBasename("org/springframework/context/support/messages"); + ms.setDefaultEncoding("ISO-8859-1"); + assertThat(ms.getMessage("code1", null, Locale.ENGLISH)).isEqualTo("message1"); + assertThat(ms.getMessage("code2", null, Locale.GERMAN)).isEqualTo("nachricht2"); + } + + @Test + void reloadableResourceBundleMessageSourceWithInappropriateDefaultCharset() { + ReloadableResourceBundleMessageSource ms = new ReloadableResourceBundleMessageSource(); + ms.setBasename("org/springframework/context/support/messages"); + ms.setDefaultEncoding("unicode"); + Properties fileCharsets = new Properties(); + fileCharsets.setProperty("org/springframework/context/support/messages_de", "unicode"); + ms.setFileEncodings(fileCharsets); + ms.setFallbackToSystemLocale(false); + assertThatExceptionOfType(NoSuchMessageException.class).isThrownBy(() -> + ms.getMessage("code1", null, Locale.ENGLISH)); + } + + @Test + void reloadableResourceBundleMessageSourceWithInappropriateEnglishCharset() { + ReloadableResourceBundleMessageSource ms = new ReloadableResourceBundleMessageSource(); + ms.setBasename("org/springframework/context/support/messages"); + ms.setFallbackToSystemLocale(false); + Properties fileCharsets = new Properties(); + fileCharsets.setProperty("org/springframework/context/support/messages", "unicode"); + ms.setFileEncodings(fileCharsets); + assertThatExceptionOfType(NoSuchMessageException.class).isThrownBy(() -> + ms.getMessage("code1", null, Locale.ENGLISH)); + } + + @Test + void reloadableResourceBundleMessageSourceWithInappropriateGermanCharset() { + ReloadableResourceBundleMessageSource ms = new ReloadableResourceBundleMessageSource(); + ms.setBasename("org/springframework/context/support/messages"); + ms.setFallbackToSystemLocale(false); + Properties fileCharsets = new Properties(); + fileCharsets.setProperty("org/springframework/context/support/messages_de", "unicode"); + ms.setFileEncodings(fileCharsets); + assertThat(ms.getMessage("code1", null, Locale.ENGLISH)).isEqualTo("message1"); + assertThat(ms.getMessage("code2", null, Locale.GERMAN)).isEqualTo("message2"); + } + + @Test + void reloadableResourceBundleMessageSourceFileNameCalculation() { + ReloadableResourceBundleMessageSource ms = new ReloadableResourceBundleMessageSource(); + + List filenames = ms.calculateFilenamesForLocale("messages", Locale.ENGLISH); + assertThat(filenames.size()).isEqualTo(1); + assertThat(filenames.get(0)).isEqualTo("messages_en"); + + filenames = ms.calculateFilenamesForLocale("messages", Locale.UK); + assertThat(filenames.size()).isEqualTo(2); + assertThat(filenames.get(1)).isEqualTo("messages_en"); + assertThat(filenames.get(0)).isEqualTo("messages_en_GB"); + + filenames = ms.calculateFilenamesForLocale("messages", new Locale("en", "GB", "POSIX")); + assertThat(filenames.size()).isEqualTo(3); + assertThat(filenames.get(2)).isEqualTo("messages_en"); + assertThat(filenames.get(1)).isEqualTo("messages_en_GB"); + assertThat(filenames.get(0)).isEqualTo("messages_en_GB_POSIX"); + + filenames = ms.calculateFilenamesForLocale("messages", new Locale("en", "", "POSIX")); + assertThat(filenames.size()).isEqualTo(2); + assertThat(filenames.get(1)).isEqualTo("messages_en"); + assertThat(filenames.get(0)).isEqualTo("messages_en__POSIX"); + + filenames = ms.calculateFilenamesForLocale("messages", new Locale("", "UK", "POSIX")); + assertThat(filenames.size()).isEqualTo(2); + assertThat(filenames.get(1)).isEqualTo("messages__UK"); + assertThat(filenames.get(0)).isEqualTo("messages__UK_POSIX"); + + filenames = ms.calculateFilenamesForLocale("messages", new Locale("", "", "POSIX")); + assertThat(filenames.size()).isEqualTo(0); + } + + @Test + void messageSourceResourceBundle() { + ResourceBundleMessageSource ms = new ResourceBundleMessageSource(); + ms.setBasename("org/springframework/context/support/messages"); + MessageSourceResourceBundle rbe = new MessageSourceResourceBundle(ms, Locale.ENGLISH); + assertThat(rbe.getString("code1")).isEqualTo("message1"); + assertThat(rbe.containsKey("code1")).isTrue(); + MessageSourceResourceBundle rbg = new MessageSourceResourceBundle(ms, Locale.GERMAN); + assertThat(rbg.getString("code2")).isEqualTo("nachricht2"); + assertThat(rbg.containsKey("code2")).isTrue(); + } + + + @AfterEach + void tearDown() { + ResourceBundle.clearCache(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/support/ResourceConverter.java b/spring-context/src/test/java/org/springframework/context/support/ResourceConverter.java new file mode 100644 index 0000000..73a5fa6 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/support/ResourceConverter.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; + +/** + * @author Juergen Hoeller + */ +public class ResourceConverter implements Converter { + + @Override + public Resource convert(String source) { + return new FileSystemResource(source + ".xml"); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/support/SerializableBeanFactoryMemoryLeakTests.java b/spring-context/src/test/java/org/springframework/context/support/SerializableBeanFactoryMemoryLeakTests.java new file mode 100644 index 0000000..45aaac5 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/support/SerializableBeanFactoryMemoryLeakTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.lang.reflect.Field; +import java.util.Map; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.ConfigurableApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.beans.factory.support.BeanDefinitionBuilder.rootBeanDefinition; + +/** + * Unit tests cornering SPR-7502. + * + * @author Chris Beams + */ +public class SerializableBeanFactoryMemoryLeakTests { + + /** + * Defensively zero-out static factory count - other tests + * may have misbehaved before us. + */ + @BeforeAll + @AfterAll + public static void zeroOutFactoryCount() throws Exception { + getSerializableFactoryMap().clear(); + } + + @Test + public void genericContext() throws Exception { + assertFactoryCountThroughoutLifecycle(new GenericApplicationContext()); + } + + @Test + public void abstractRefreshableContext() throws Exception { + assertFactoryCountThroughoutLifecycle(new ClassPathXmlApplicationContext()); + } + + @Test + public void genericContextWithMisconfiguredBean() throws Exception { + GenericApplicationContext ctx = new GenericApplicationContext(); + registerMisconfiguredBeanDefinition(ctx); + assertFactoryCountThroughoutLifecycle(ctx); + } + + @Test + public void abstractRefreshableContextWithMisconfiguredBean() throws Exception { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext() { + @Override + protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory) { + super.customizeBeanFactory(beanFactory); + registerMisconfiguredBeanDefinition(beanFactory); + } + }; + assertFactoryCountThroughoutLifecycle(ctx); + } + + private void assertFactoryCountThroughoutLifecycle(ConfigurableApplicationContext ctx) throws Exception { + assertThat(serializableFactoryCount()).isEqualTo(0); + try { + ctx.refresh(); + assertThat(serializableFactoryCount()).isEqualTo(1); + ctx.close(); + } + catch (BeanCreationException ex) { + // ignore - this is expected on refresh() for failure case tests + } + finally { + assertThat(serializableFactoryCount()).isEqualTo(0); + } + } + + private void registerMisconfiguredBeanDefinition(BeanDefinitionRegistry registry) { + registry.registerBeanDefinition("misconfigured", + rootBeanDefinition(Object.class).addPropertyValue("nonexistent", "bogus") + .getBeanDefinition()); + } + + private int serializableFactoryCount() throws Exception { + Map map = getSerializableFactoryMap(); + return map.size(); + } + + private static Map getSerializableFactoryMap() throws Exception { + Field field = DefaultListableBeanFactory.class.getDeclaredField("serializableFactories"); + field.setAccessible(true); + return (Map) field.get(DefaultListableBeanFactory.class); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/support/Service.java b/spring-context/src/test/java/org/springframework/context/support/Service.java new file mode 100644 index 0000000..680e809 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/support/Service.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import org.springframework.beans.factory.BeanCreationNotAllowedException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceAware; +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; + +/** + * @author Alef Arendsen + * @author Juergen Hoeller + */ +public class Service implements ApplicationContextAware, MessageSourceAware, DisposableBean { + + private ApplicationContext applicationContext; + + private MessageSource messageSource; + + private Resource[] resources; + + private boolean properlyDestroyed = false; + + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Override + public void setMessageSource(MessageSource messageSource) { + if (this.messageSource != null) { + throw new IllegalArgumentException("MessageSource should not be set twice"); + } + this.messageSource = messageSource; + } + + public MessageSource getMessageSource() { + return messageSource; + } + + public void setResources(Resource[] resources) { + this.resources = resources; + } + + public Resource[] getResources() { + return resources; + } + + + @Override + public void destroy() { + this.properlyDestroyed = true; + Thread thread = new Thread() { + @Override + public void run() { + Assert.state(applicationContext.getBean("messageSource") instanceof StaticMessageSource, + "Invalid MessageSource bean"); + try { + applicationContext.getBean("service2"); + // Should have thrown BeanCreationNotAllowedException + properlyDestroyed = false; + } + catch (BeanCreationNotAllowedException ex) { + // expected + } + } + }; + thread.start(); + try { + thread.join(); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + + public boolean isProperlyDestroyed() { + return properlyDestroyed; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/support/SimpleThreadScopeTests.java b/spring-context/src/test/java/org/springframework/context/support/SimpleThreadScopeTests.java new file mode 100644 index 0000000..3119d05 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/support/SimpleThreadScopeTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.util.concurrent.TimeUnit; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.ApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + * @author Sam Brannen + */ +class SimpleThreadScopeTests { + + private final ApplicationContext applicationContext = + new ClassPathXmlApplicationContext("simpleThreadScopeTests.xml", getClass()); + + + @Test + void getFromScope() throws Exception { + String name = "removeNodeStatusScreen"; + TestBean bean = this.applicationContext.getBean(name, TestBean.class); + assertThat(bean).isNotNull(); + assertThat(this.applicationContext.getBean(name)).isSameAs(bean); + TestBean bean2 = this.applicationContext.getBean(name, TestBean.class); + assertThat(bean2).isSameAs(bean); + } + + @Test + void getMultipleInstances() throws Exception { + // Arrange + TestBean[] beans = new TestBean[2]; + Thread thread1 = new Thread(() -> beans[0] = applicationContext.getBean("removeNodeStatusScreen", TestBean.class)); + Thread thread2 = new Thread(() -> beans[1] = applicationContext.getBean("removeNodeStatusScreen", TestBean.class)); + // Act + thread1.start(); + thread2.start(); + // Assert + Awaitility.await() + .atMost(500, TimeUnit.MILLISECONDS) + .pollInterval(10, TimeUnit.MILLISECONDS) + .until(() -> (beans[0] != null) && (beans[1] != null)); + assertThat(beans[1]).isNotSameAs(beans[0]); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/support/Spr7283Tests.java b/spring-context/src/test/java/org/springframework/context/support/Spr7283Tests.java new file mode 100644 index 0000000..374613f --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/support/Spr7283Tests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Scott Andrews + * @author Juergen Hoeller + */ +public class Spr7283Tests { + + @Test + public void testListWithInconsistentElementType() { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("spr7283.xml", getClass()); + List list = ctx.getBean("list", List.class); + assertThat(list.size()).isEqualTo(2); + boolean condition1 = list.get(0) instanceof A; + assertThat(condition1).isTrue(); + boolean condition = list.get(1) instanceof B; + assertThat(condition).isTrue(); + } + + + public static class A { + public A() {} + } + + public static class B { + public B() {} + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/support/Spr7816Tests.java b/spring-context/src/test/java/org/springframework/context/support/Spr7816Tests.java new file mode 100644 index 0000000..087d723 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/support/Spr7816Tests.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.ApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Keith Donald + * @author Juergen Hoeller + */ +public class Spr7816Tests { + + @Test + public void spr7816() { + ApplicationContext ctx = new ClassPathXmlApplicationContext("spr7816.xml", getClass()); + FilterAdapter adapter = ctx.getBean(FilterAdapter.class); + assertThat(adapter.getSupportedTypes().get("Building")).isEqualTo(Building.class); + assertThat(adapter.getSupportedTypes().get("Entrance")).isEqualTo(Entrance.class); + assertThat(adapter.getSupportedTypes().get("Dwelling")).isEqualTo(Dwelling.class); + } + + public static class FilterAdapter { + + private String extensionPrefix; + + private Map> supportedTypes; + + public FilterAdapter(final String extensionPrefix, final Map> supportedTypes) { + this.extensionPrefix = extensionPrefix; + this.supportedTypes = supportedTypes; + } + + public String getExtensionPrefix() { + return extensionPrefix; + } + + public Map> getSupportedTypes() { + return supportedTypes; + } + + } + + public static class Building extends DomainEntity { + } + + public static class Entrance extends DomainEntity { + } + + public static class Dwelling extends DomainEntity { + } + + public abstract static class DomainEntity { + + } +} diff --git a/spring-context/src/test/java/org/springframework/context/support/StaticApplicationContextMulticasterTests.java b/spring-context/src/test/java/org/springframework/context/support/StaticApplicationContextMulticasterTests.java new file mode 100644 index 0000000..403bf61 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/support/StaticApplicationContextMulticasterTests.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.support.PropertiesBeanDefinitionReader; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.event.SimpleApplicationEventMulticaster; +import org.springframework.context.testfixture.AbstractApplicationContextTests; +import org.springframework.context.testfixture.beans.ACATester; +import org.springframework.context.testfixture.beans.BeanThatListens; +import org.springframework.core.ResolvableType; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for static application context with custom application event multicaster. + * + * @author Juergen Hoeller + */ +public class StaticApplicationContextMulticasterTests extends AbstractApplicationContextTests { + + protected StaticApplicationContext sac; + + @Override + protected ConfigurableApplicationContext createContext() throws Exception { + StaticApplicationContext parent = new StaticApplicationContext(); + Map m = new HashMap<>(); + m.put("name", "Roderick"); + parent.registerPrototype("rod", TestBean.class, new MutablePropertyValues(m)); + m.put("name", "Albert"); + parent.registerPrototype("father", TestBean.class, new MutablePropertyValues(m)); + parent.registerSingleton(StaticApplicationContext.APPLICATION_EVENT_MULTICASTER_BEAN_NAME, + TestApplicationEventMulticaster.class, null); + parent.refresh(); + parent.addApplicationListener(parentListener) ; + + parent.getStaticMessageSource().addMessage("code1", Locale.getDefault(), "message1"); + + this.sac = new StaticApplicationContext(parent); + sac.registerSingleton("beanThatListens", BeanThatListens.class, new MutablePropertyValues()); + sac.registerSingleton("aca", ACATester.class, new MutablePropertyValues()); + sac.registerPrototype("aca-prototype", ACATester.class, new MutablePropertyValues()); + PropertiesBeanDefinitionReader reader = new PropertiesBeanDefinitionReader(sac.getDefaultListableBeanFactory()); + Resource resource = new ClassPathResource("testBeans.properties", getClass()); + reader.loadBeanDefinitions(new EncodedResource(resource, "ISO-8859-1")); + sac.refresh(); + sac.addApplicationListener(listener); + + sac.getStaticMessageSource().addMessage("code2", Locale.getDefault(), "message2"); + + return sac; + } + + @Test + @Override + public void count() { + assertCount(15); + } + + @Test + @Override + public void events() throws Exception { + TestApplicationEventMulticaster.counter = 0; + super.events(); + assertThat(TestApplicationEventMulticaster.counter).isEqualTo(1); + } + + + public static class TestApplicationEventMulticaster extends SimpleApplicationEventMulticaster { + + private static int counter = 0; + + @Override + public void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType) { + super.multicastEvent(event, eventType); + counter++; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/support/StaticApplicationContextTests.java b/spring-context/src/test/java/org/springframework/context/support/StaticApplicationContextTests.java new file mode 100644 index 0000000..91c06a8 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/support/StaticApplicationContextTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.support.PropertiesBeanDefinitionReader; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.testfixture.AbstractApplicationContextTests; +import org.springframework.context.testfixture.beans.ACATester; +import org.springframework.context.testfixture.beans.BeanThatListens; +import org.springframework.core.io.ClassPathResource; + +/** + * Tests for static application context. + * + * @author Rod Johnson + */ +public class StaticApplicationContextTests extends AbstractApplicationContextTests { + + protected StaticApplicationContext sac; + + @Override + protected ConfigurableApplicationContext createContext() throws Exception { + StaticApplicationContext parent = new StaticApplicationContext(); + Map m = new HashMap<>(); + m.put("name", "Roderick"); + parent.registerPrototype("rod", TestBean.class, new MutablePropertyValues(m)); + m.put("name", "Albert"); + parent.registerPrototype("father", TestBean.class, new MutablePropertyValues(m)); + parent.refresh(); + parent.addApplicationListener(parentListener) ; + + parent.getStaticMessageSource().addMessage("code1", Locale.getDefault(), "message1"); + + this.sac = new StaticApplicationContext(parent); + sac.registerSingleton("beanThatListens", BeanThatListens.class, new MutablePropertyValues()); + sac.registerSingleton("aca", ACATester.class, new MutablePropertyValues()); + sac.registerPrototype("aca-prototype", ACATester.class, new MutablePropertyValues()); + PropertiesBeanDefinitionReader reader = new PropertiesBeanDefinitionReader(sac.getDefaultListableBeanFactory()); + reader.loadBeanDefinitions(new ClassPathResource("testBeans.properties", getClass())); + sac.refresh(); + sac.addApplicationListener(listener); + + sac.getStaticMessageSource().addMessage("code2", Locale.getDefault(), "message2"); + + return sac; + } + + @Test + @Override + public void count() { + assertCount(15); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/support/StaticMessageSourceTests.java b/spring-context/src/test/java/org/springframework/context/support/StaticMessageSourceTests.java new file mode 100644 index 0000000..3b84bd9 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/support/StaticMessageSourceTests.java @@ -0,0 +1,246 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import java.util.Date; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.support.PropertiesBeanDefinitionReader; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.NoSuchMessageException; +import org.springframework.context.testfixture.AbstractApplicationContextTests; +import org.springframework.context.testfixture.beans.ACATester; +import org.springframework.context.testfixture.beans.BeanThatListens; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Rod Johnson + * @author Juergen Hoeller + * @author Sam Brannen + */ +public class StaticMessageSourceTests extends AbstractApplicationContextTests { + + protected static final String MSG_TXT1_US = + "At '{1,time}' on \"{1,date}\", there was \"{2}\" on planet {0,number,integer}."; + protected static final String MSG_TXT1_UK = + "At '{1,time}' on \"{1,date}\", there was \"{2}\" on station number {0,number,integer}."; + protected static final String MSG_TXT2_US = + "This is a test message in the message catalog with no args."; + protected static final String MSG_TXT3_US = + "This is another test message in the message catalog with no args."; + + protected StaticApplicationContext sac; + + + @Test + @Override + public void count() { + assertCount(15); + } + + @Test + @Override + @Disabled("Do nothing here since super is looking for errorCodes we do NOT have in the Context") + public void messageSource() throws NoSuchMessageException { + } + + @Test + public void getMessageWithDefaultPassedInAndFoundInMsgCatalog() { + // Try with Locale.US + assertThat(sac.getMessage("message.format.example2", null, "This is a default msg if not found in MessageSource.", Locale.US) + .equals("This is a test message in the message catalog with no args.")).as("valid msg from staticMsgSource with default msg passed in returned msg from msg catalog for Locale.US").isTrue(); + } + + @Test + public void getMessageWithDefaultPassedInAndNotFoundInMsgCatalog() { + // Try with Locale.US + assertThat(sac.getMessage("bogus.message", null, "This is a default msg if not found in MessageSource.", Locale.US) + .equals("This is a default msg if not found in MessageSource.")).as("bogus msg from staticMsgSource with default msg passed in returned default msg for Locale.US").isTrue(); + } + + /** + * We really are testing the AbstractMessageSource class here. + * The underlying implementation uses a hashMap to cache messageFormats + * once a message has been asked for. This test is an attempt to + * make sure the cache is being used properly. + * @see org.springframework.context.support.AbstractMessageSource for more details. + */ + @Test + public void getMessageWithMessageAlreadyLookedFor() { + Object[] arguments = { + 7, new Date(System.currentTimeMillis()), + "a disturbance in the Force" + }; + + // The first time searching, we don't care about for this test + // Try with Locale.US + sac.getMessage("message.format.example1", arguments, Locale.US); + + // Now msg better be as expected + assertThat(sac.getMessage("message.format.example1", arguments, Locale.US). + contains("there was \"a disturbance in the Force\" on planet 7.")).as("2nd search within MsgFormat cache returned expected message for Locale.US").isTrue(); + + Object[] newArguments = { + 8, new Date(System.currentTimeMillis()), + "a disturbance in the Force" + }; + + // Now msg better be as expected even with different args + assertThat(sac.getMessage("message.format.example1", newArguments, Locale.US). + contains("there was \"a disturbance in the Force\" on planet 8.")).as("2nd search within MsgFormat cache with different args returned expected message for Locale.US").isTrue(); + } + + /** + * Example taken from the javadocs for the java.text.MessageFormat class + */ + @Test + public void getMessageWithNoDefaultPassedInAndFoundInMsgCatalog() { + Object[] arguments = { + 7, new Date(System.currentTimeMillis()), + "a disturbance in the Force" + }; + + /* + Try with Locale.US + Since the msg has a time value in it, we will use String.indexOf(...) + to just look for a substring without the time. This is because it is + possible that by the time we store a time variable in this method + and the time the ResourceBundleMessageSource resolves the msg the + minutes of the time might not be the same. + */ + assertThat(sac.getMessage("message.format.example1", arguments, Locale.US). + contains("there was \"a disturbance in the Force\" on planet 7.")).as("msg from staticMsgSource for Locale.US substituting args for placeholders is as expected").isTrue(); + + // Try with Locale.UK + assertThat(sac.getMessage("message.format.example1", arguments, Locale.UK). + contains("there was \"a disturbance in the Force\" on station number 7.")).as("msg from staticMsgSource for Locale.UK substituting args for placeholders is as expected").isTrue(); + + // Try with Locale.US - Use a different test msg that requires no args + assertThat(sac.getMessage("message.format.example2", null, Locale.US) + .equals("This is a test message in the message catalog with no args.")).as("msg from staticMsgSource for Locale.US that requires no args is as expected").isTrue(); + } + + @Test + public void getMessageWithNoDefaultPassedInAndNotFoundInMsgCatalog() { + // Try with Locale.US + assertThatExceptionOfType(NoSuchMessageException.class).isThrownBy(() -> + sac.getMessage("bogus.message", null, Locale.US)); + } + + @Test + public void messageSourceResolvable() { + // first code valid + String[] codes1 = new String[] {"message.format.example3", "message.format.example2"}; + MessageSourceResolvable resolvable1 = new DefaultMessageSourceResolvable(codes1, null, "default"); + assertThat(MSG_TXT3_US.equals(sac.getMessage(resolvable1, Locale.US))).as("correct message retrieved").isTrue(); + + // only second code valid + String[] codes2 = new String[] {"message.format.example99", "message.format.example2"}; + MessageSourceResolvable resolvable2 = new DefaultMessageSourceResolvable(codes2, null, "default"); + assertThat(MSG_TXT2_US.equals(sac.getMessage(resolvable2, Locale.US))).as("correct message retrieved").isTrue(); + + // no code valid, but default given + String[] codes3 = new String[] {"message.format.example99", "message.format.example98"}; + MessageSourceResolvable resolvable3 = new DefaultMessageSourceResolvable(codes3, null, "default"); + assertThat("default".equals(sac.getMessage(resolvable3, Locale.US))).as("correct message retrieved").isTrue(); + + // no code valid, no default + String[] codes4 = new String[] {"message.format.example99", "message.format.example98"}; + MessageSourceResolvable resolvable4 = new DefaultMessageSourceResolvable(codes4); + + assertThatExceptionOfType(NoSuchMessageException.class).isThrownBy(() -> + sac.getMessage(resolvable4, Locale.US)); + } + + @Override + protected ConfigurableApplicationContext createContext() throws Exception { + StaticApplicationContext parent = new StaticApplicationContext(); + + Map m = new HashMap<>(); + m.put("name", "Roderick"); + parent.registerPrototype("rod", org.springframework.beans.testfixture.beans.TestBean.class, new MutablePropertyValues(m)); + m.put("name", "Albert"); + parent.registerPrototype("father", org.springframework.beans.testfixture.beans.TestBean.class, new MutablePropertyValues(m)); + + parent.refresh(); + parent.addApplicationListener(parentListener); + + this.sac = new StaticApplicationContext(parent); + + sac.registerSingleton("beanThatListens", BeanThatListens.class, new MutablePropertyValues()); + + sac.registerSingleton("aca", ACATester.class, new MutablePropertyValues()); + + sac.registerPrototype("aca-prototype", ACATester.class, new MutablePropertyValues()); + + PropertiesBeanDefinitionReader reader = new PropertiesBeanDefinitionReader(sac.getDefaultListableBeanFactory()); + reader.loadBeanDefinitions(new ClassPathResource("testBeans.properties", getClass())); + sac.refresh(); + sac.addApplicationListener(listener); + + StaticMessageSource messageSource = sac.getStaticMessageSource(); + Map usMessages = new HashMap<>(3); + usMessages.put("message.format.example1", MSG_TXT1_US); + usMessages.put("message.format.example2", MSG_TXT2_US); + usMessages.put("message.format.example3", MSG_TXT3_US); + messageSource.addMessages(usMessages, Locale.US); + messageSource.addMessage("message.format.example1", Locale.UK, MSG_TXT1_UK); + + return sac; + } + + @Test + public void nestedMessageSourceWithParamInChild() { + StaticMessageSource source = new StaticMessageSource(); + StaticMessageSource parent = new StaticMessageSource(); + source.setParentMessageSource(parent); + + source.addMessage("param", Locale.ENGLISH, "value"); + parent.addMessage("with.param", Locale.ENGLISH, "put {0} here"); + + MessageSourceResolvable resolvable = new DefaultMessageSourceResolvable( + new String[] {"with.param"}, new Object[] {new DefaultMessageSourceResolvable("param")}); + + assertThat(source.getMessage(resolvable, Locale.ENGLISH)).isEqualTo("put value here"); + } + + @Test + public void nestedMessageSourceWithParamInParent() { + StaticMessageSource source = new StaticMessageSource(); + StaticMessageSource parent = new StaticMessageSource(); + source.setParentMessageSource(parent); + + parent.addMessage("param", Locale.ENGLISH, "value"); + source.addMessage("with.param", Locale.ENGLISH, "put {0} here"); + + MessageSourceResolvable resolvable = new DefaultMessageSourceResolvable( + new String[] {"with.param"}, new Object[] {new DefaultMessageSourceResolvable("param")}); + + assertThat(source.getMessage(resolvable, Locale.ENGLISH)).isEqualTo("put value here"); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/support/TestIF.java b/spring-context/src/test/java/org/springframework/context/support/TestIF.java new file mode 100644 index 0000000..4da2735 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/support/TestIF.java @@ -0,0 +1,21 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +public interface TestIF { + +} diff --git a/spring-context/src/test/java/org/springframework/context/support/TestProxyFactoryBean.java b/spring-context/src/test/java/org/springframework/context/support/TestProxyFactoryBean.java new file mode 100644 index 0000000..3c032e0 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/support/TestProxyFactoryBean.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support; + +import org.springframework.aop.framework.AbstractSingletonProxyFactoryBean; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; + +@SuppressWarnings("serial") +public class TestProxyFactoryBean extends AbstractSingletonProxyFactoryBean implements BeanFactoryAware { + + @Override + protected Object createMainInterceptor() { + return new NoOpAdvice(); + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + } + +} diff --git a/spring-context/src/test/java/org/springframework/core/task/NoOpRunnable.java b/spring-context/src/test/java/org/springframework/core/task/NoOpRunnable.java new file mode 100644 index 0000000..854c1e1 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/core/task/NoOpRunnable.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.task; + +/** + * A no-op {@link Runnable} implementation. + * + * @author Rick Evans + */ +public class NoOpRunnable implements Runnable { + + @Override + public void run() { + // explicit no-op + } + +} diff --git a/spring-context/src/test/java/org/springframework/ejb/access/LocalSlsbInvokerInterceptorTests.java b/spring-context/src/test/java/org/springframework/ejb/access/LocalSlsbInvokerInterceptorTests.java new file mode 100644 index 0000000..f348977 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/ejb/access/LocalSlsbInvokerInterceptorTests.java @@ -0,0 +1,207 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ejb.access; + +import javax.ejb.CreateException; +import javax.ejb.EJBLocalHome; +import javax.ejb.EJBLocalObject; +import javax.naming.Context; +import javax.naming.NamingException; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.jndi.JndiTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author Rod Johnson + * @author Juergen Hoeller + * @author Chris Beams +*/ +public class LocalSlsbInvokerInterceptorTests { + + /** + * Test that it performs the correct lookup. + */ + @Test + public void testPerformsLookup() throws Exception { + LocalInterfaceWithBusinessMethods ejb = mock(LocalInterfaceWithBusinessMethods.class); + + String jndiName= "foobar"; + Context mockContext = mockContext(jndiName, ejb); + + configuredInterceptor(mockContext, jndiName); + + verify(mockContext).close(); + } + + @Test + public void testLookupFailure() throws Exception { + final NamingException nex = new NamingException(); + final String jndiName= "foobar"; + JndiTemplate jt = new JndiTemplate() { + @Override + public Object lookup(String name) throws NamingException { + assertThat(jndiName.equals(name)).isTrue(); + throw nex; + } + }; + + LocalSlsbInvokerInterceptor si = new LocalSlsbInvokerInterceptor(); + si.setJndiName("foobar"); + // default resourceRef=false should cause this to fail, as java:/comp/env will not + // automatically be added + si.setJndiTemplate(jt); + assertThatExceptionOfType(NamingException.class) + .isThrownBy(si::afterPropertiesSet) + .isSameAs(nex); + } + + @Test + public void testInvokesMethodOnEjbInstance() throws Exception { + Object retVal = new Object(); + LocalInterfaceWithBusinessMethods ejb = mock(LocalInterfaceWithBusinessMethods.class); + given(ejb.targetMethod()).willReturn(retVal); + + String jndiName= "foobar"; + Context mockContext = mockContext(jndiName, ejb); + + LocalSlsbInvokerInterceptor si = configuredInterceptor(mockContext, jndiName); + + ProxyFactory pf = new ProxyFactory(new Class[] { BusinessMethods.class }); + pf.addAdvice(si); + BusinessMethods target = (BusinessMethods) pf.getProxy(); + + assertThat(target.targetMethod() == retVal).isTrue(); + + verify(mockContext).close(); + verify(ejb).remove(); + } + + @Test + public void testInvokesMethodOnEjbInstanceWithSeparateBusinessMethods() throws Exception { + Object retVal = new Object(); + LocalInterface ejb = mock(LocalInterface.class); + given(ejb.targetMethod()).willReturn(retVal); + + String jndiName= "foobar"; + Context mockContext = mockContext(jndiName, ejb); + + LocalSlsbInvokerInterceptor si = configuredInterceptor(mockContext, jndiName); + + ProxyFactory pf = new ProxyFactory(new Class[] { BusinessMethods.class }); + pf.addAdvice(si); + BusinessMethods target = (BusinessMethods) pf.getProxy(); + + assertThat(target.targetMethod() == retVal).isTrue(); + + verify(mockContext).close(); + verify(ejb).remove(); + } + + private void testException(Exception expected) throws Exception { + LocalInterfaceWithBusinessMethods ejb = mock(LocalInterfaceWithBusinessMethods.class); + given(ejb.targetMethod()).willThrow(expected); + + String jndiName= "foobar"; + Context mockContext = mockContext(jndiName, ejb); + + LocalSlsbInvokerInterceptor si = configuredInterceptor(mockContext, jndiName); + + ProxyFactory pf = new ProxyFactory(new Class[] { LocalInterfaceWithBusinessMethods.class }); + pf.addAdvice(si); + LocalInterfaceWithBusinessMethods target = (LocalInterfaceWithBusinessMethods) pf.getProxy(); + + assertThatExceptionOfType(Exception.class) + .isThrownBy(target::targetMethod) + .isSameAs(expected); + + verify(mockContext).close(); + } + + @Test + public void testApplicationException() throws Exception { + testException(new ApplicationException()); + } + + protected Context mockContext(final String jndiName, final Object ejbInstance) + throws Exception { + SlsbHome mockHome = mock(SlsbHome.class); + given(mockHome.create()).willReturn((LocalInterface)ejbInstance); + Context mockCtx = mock(Context.class); + given(mockCtx.lookup("java:comp/env/" + jndiName)).willReturn(mockHome); + return mockCtx; + } + + protected LocalSlsbInvokerInterceptor configuredInterceptor(final Context mockCtx, final String jndiName) + throws Exception { + + LocalSlsbInvokerInterceptor si = new LocalSlsbInvokerInterceptor(); + si.setJndiTemplate(new JndiTemplate() { + @Override + protected Context createInitialContext() throws NamingException { + return mockCtx; + } + }); + si.setJndiName(jndiName); + si.setResourceRef(true); + si.afterPropertiesSet(); + + return si; + } + + + /** + * Needed so that we can mock the create() method. + */ + private interface SlsbHome extends EJBLocalHome { + + LocalInterface create() throws CreateException; + } + + + private interface BusinessMethods { + + Object targetMethod() throws ApplicationException; + } + + + private interface LocalInterface extends EJBLocalObject { + + Object targetMethod() throws ApplicationException; + } + + + private interface LocalInterfaceWithBusinessMethods extends LocalInterface, BusinessMethods { + } + + + @SuppressWarnings("serial") + private class ApplicationException extends Exception { + + public ApplicationException() { + super("appException"); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/ejb/access/LocalStatelessSessionProxyFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/ejb/access/LocalStatelessSessionProxyFactoryBeanTests.java new file mode 100644 index 0000000..545e6c2 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/ejb/access/LocalStatelessSessionProxyFactoryBeanTests.java @@ -0,0 +1,197 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ejb.access; + +import java.lang.reflect.Proxy; + +import javax.ejb.CreateException; +import javax.ejb.EJBLocalHome; +import javax.ejb.EJBLocalObject; +import javax.naming.NamingException; + +import org.junit.jupiter.api.Test; + +import org.springframework.jndi.JndiTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +/** + * @author Rod Johnson + * @author Juergen Hoeller + * @author Chris Beams + * @since 21.05.2003 + */ +public class LocalStatelessSessionProxyFactoryBeanTests { + + @Test + public void testInvokesMethod() throws Exception { + final int value = 11; + final String jndiName = "foo"; + + MyEjb myEjb = mock(MyEjb.class); + given(myEjb.getValue()).willReturn(value); + + final MyHome home = mock(MyHome.class); + given(home.create()).willReturn(myEjb); + + JndiTemplate jt = new JndiTemplate() { + @Override + public Object lookup(String name) throws NamingException { + // parameterize + assertThat(name.equals("java:comp/env/" + jndiName)).isTrue(); + return home; + } + }; + + LocalStatelessSessionProxyFactoryBean fb = new LocalStatelessSessionProxyFactoryBean(); + fb.setJndiName(jndiName); + fb.setResourceRef(true); + fb.setBusinessInterface(MyBusinessMethods.class); + fb.setJndiTemplate(jt); + + // Need lifecycle methods + fb.afterPropertiesSet(); + + MyBusinessMethods mbm = (MyBusinessMethods) fb.getObject(); + assertThat(Proxy.isProxyClass(mbm.getClass())).isTrue(); + assertThat(mbm.getValue() == value).isTrue(); + verify(myEjb).remove(); + } + + @Test + public void testInvokesMethodOnEjb3StyleBean() throws Exception { + final int value = 11; + final String jndiName = "foo"; + + final MyEjb myEjb = mock(MyEjb.class); + given(myEjb.getValue()).willReturn(value); + + JndiTemplate jt = new JndiTemplate() { + @Override + public Object lookup(String name) throws NamingException { + // parameterize + assertThat(name.equals("java:comp/env/" + jndiName)).isTrue(); + return myEjb; + } + }; + + LocalStatelessSessionProxyFactoryBean fb = new LocalStatelessSessionProxyFactoryBean(); + fb.setJndiName(jndiName); + fb.setResourceRef(true); + fb.setBusinessInterface(MyBusinessMethods.class); + fb.setJndiTemplate(jt); + + // Need lifecycle methods + fb.afterPropertiesSet(); + + MyBusinessMethods mbm = (MyBusinessMethods) fb.getObject(); + assertThat(Proxy.isProxyClass(mbm.getClass())).isTrue(); + assertThat(mbm.getValue() == value).isTrue(); + } + + @Test + public void testCreateException() throws Exception { + final String jndiName = "foo"; + + final CreateException cex = new CreateException(); + final MyHome home = mock(MyHome.class); + given(home.create()).willThrow(cex); + + JndiTemplate jt = new JndiTemplate() { + @Override + public Object lookup(String name) throws NamingException { + // parameterize + assertThat(name.equals(jndiName)).isTrue(); + return home; + } + }; + + LocalStatelessSessionProxyFactoryBean fb = new LocalStatelessSessionProxyFactoryBean(); + fb.setJndiName(jndiName); + fb.setResourceRef(false); // no java:comp/env prefix + fb.setBusinessInterface(MyBusinessMethods.class); + assertThat(MyBusinessMethods.class).isEqualTo(fb.getBusinessInterface()); + fb.setJndiTemplate(jt); + + // Need lifecycle methods + fb.afterPropertiesSet(); + + MyBusinessMethods mbm = (MyBusinessMethods) fb.getObject(); + assertThat(Proxy.isProxyClass(mbm.getClass())).isTrue(); + + assertThatExceptionOfType(EjbAccessException.class).isThrownBy( + mbm::getValue) + .withCause(cex); + } + + @Test + public void testNoBusinessInterfaceSpecified() throws Exception { + // Will do JNDI lookup to get home but won't call create + // Could actually try to figure out interface from create? + final String jndiName = "foo"; + + final MyHome home = mock(MyHome.class); + + JndiTemplate jt = new JndiTemplate() { + @Override + public Object lookup(String name) throws NamingException { + // parameterize + assertThat(name.equals("java:comp/env/" + jndiName)).isTrue(); + return home; + } + }; + + LocalStatelessSessionProxyFactoryBean fb = new LocalStatelessSessionProxyFactoryBean(); + fb.setJndiName(jndiName); + fb.setResourceRef(true); + // Don't set business interface + fb.setJndiTemplate(jt); + + // Check it's a singleton + assertThat(fb.isSingleton()).isTrue(); + + assertThatIllegalArgumentException().isThrownBy( + fb::afterPropertiesSet) + .withMessageContaining("businessInterface"); + + // Expect no methods on home + verifyNoInteractions(home); + } + + + public interface MyHome extends EJBLocalHome { + + MyBusinessMethods create() throws CreateException; + } + + + public interface MyBusinessMethods { + + int getValue(); + } + + + public interface MyEjb extends EJBLocalObject, MyBusinessMethods { + } + +} diff --git a/spring-context/src/test/java/org/springframework/ejb/access/SimpleRemoteSlsbInvokerInterceptorTests.java b/spring-context/src/test/java/org/springframework/ejb/access/SimpleRemoteSlsbInvokerInterceptorTests.java new file mode 100644 index 0000000..3397cb0 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/ejb/access/SimpleRemoteSlsbInvokerInterceptorTests.java @@ -0,0 +1,349 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ejb.access; + +import java.rmi.ConnectException; +import java.rmi.RemoteException; + +import javax.ejb.CreateException; +import javax.ejb.EJBHome; +import javax.ejb.EJBObject; +import javax.naming.Context; +import javax.naming.NamingException; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.jndi.JndiTemplate; +import org.springframework.remoting.RemoteAccessException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author Rod Johnson + * @author Juergen Hoeller + * @author Chris Beams + */ +public class SimpleRemoteSlsbInvokerInterceptorTests { + + private Context mockContext( + String jndiName, RemoteInterface ejbInstance) + throws Exception { + SlsbHome mockHome = mock(SlsbHome.class); + given(mockHome.create()).willReturn(ejbInstance); + Context mockCtx = mock(Context.class); + given(mockCtx.lookup("java:comp/env/" + jndiName)).willReturn(mockHome); + return mockCtx; + } + + private SimpleRemoteSlsbInvokerInterceptor configuredInterceptor( + final Context mockCtx, String jndiName) throws Exception { + + SimpleRemoteSlsbInvokerInterceptor si = createInterceptor(); + si.setJndiTemplate(new JndiTemplate() { + @Override + protected Context createInitialContext() { + return mockCtx; + } + }); + si.setResourceRef(true); + si.setJndiName(jndiName); + + return si; + } + + protected SimpleRemoteSlsbInvokerInterceptor createInterceptor() { + return new SimpleRemoteSlsbInvokerInterceptor(); + } + + protected Object configuredProxy(SimpleRemoteSlsbInvokerInterceptor si, Class ifc) throws NamingException { + si.afterPropertiesSet(); + ProxyFactory pf = new ProxyFactory(new Class[] {ifc}); + pf.addAdvice(si); + return pf.getProxy(); + } + + + @Test + public void testPerformsLookup() throws Exception { + RemoteInterface ejb = mock(RemoteInterface.class); + + String jndiName= "foobar"; + Context mockContext = mockContext(jndiName, ejb); + + SimpleRemoteSlsbInvokerInterceptor si = configuredInterceptor(mockContext, jndiName); + configuredProxy(si, RemoteInterface.class); + + verify(mockContext).close(); + } + + @Test + public void testPerformsLookupWithAccessContext() throws Exception { + RemoteInterface ejb = mock(RemoteInterface.class); + + String jndiName= "foobar"; + Context mockContext = mockContext(jndiName, ejb); + + SimpleRemoteSlsbInvokerInterceptor si = configuredInterceptor(mockContext, jndiName); + si.setExposeAccessContext(true); + RemoteInterface target = (RemoteInterface) configuredProxy(si, RemoteInterface.class); + assertThat(target.targetMethod()).isNull(); + + verify(mockContext, times(2)).close(); + verify(ejb).targetMethod(); + + } + + @Test + public void testLookupFailure() throws Exception { + final NamingException nex = new NamingException(); + final String jndiName = "foobar"; + JndiTemplate jt = new JndiTemplate() { + @Override + public Object lookup(String name) throws NamingException { + assertThat(jndiName.equals(name)).isTrue(); + throw nex; + } + }; + + SimpleRemoteSlsbInvokerInterceptor si = new SimpleRemoteSlsbInvokerInterceptor(); + si.setJndiName("foobar"); + // default resourceRef=false should cause this to fail, as java:/comp/env will not + // automatically be added + si.setJndiTemplate(jt); + assertThatExceptionOfType(NamingException.class) + .isThrownBy(si::afterPropertiesSet) + .isSameAs(nex); + } + + @Test + public void testInvokesMethodOnEjbInstance() throws Exception { + doTestInvokesMethodOnEjbInstance(true, true); + } + + @Test + public void testInvokesMethodOnEjbInstanceWithLazyLookup() throws Exception { + doTestInvokesMethodOnEjbInstance(false, true); + } + + @Test + public void testInvokesMethodOnEjbInstanceWithLazyLookupAndNoCache() throws Exception { + doTestInvokesMethodOnEjbInstance(false, false); + } + + @Test + public void testInvokesMethodOnEjbInstanceWithNoCache() throws Exception { + doTestInvokesMethodOnEjbInstance(true, false); + } + + private void doTestInvokesMethodOnEjbInstance(boolean lookupHomeOnStartup, boolean cacheHome) throws Exception { + Object retVal = new Object(); + final RemoteInterface ejb = mock(RemoteInterface.class); + given(ejb.targetMethod()).willReturn(retVal); + + int lookupCount = 1; + if (!cacheHome) { + lookupCount++; + if (lookupHomeOnStartup) { + lookupCount++; + } + } + + final String jndiName= "foobar"; + Context mockContext = mockContext(jndiName, ejb); + + SimpleRemoteSlsbInvokerInterceptor si = configuredInterceptor(mockContext, jndiName); + si.setLookupHomeOnStartup(lookupHomeOnStartup); + si.setCacheHome(cacheHome); + + RemoteInterface target = (RemoteInterface) configuredProxy(si, RemoteInterface.class); + assertThat(target.targetMethod() == retVal).isTrue(); + assertThat(target.targetMethod() == retVal).isTrue(); + + verify(mockContext, times(lookupCount)).close(); + verify(ejb, times(2)).remove(); + } + + @Test + public void testInvokesMethodOnEjbInstanceWithRemoteException() throws Exception { + final RemoteInterface ejb = mock(RemoteInterface.class); + given(ejb.targetMethod()).willThrow(new RemoteException()); + ejb.remove(); + + final String jndiName= "foobar"; + Context mockContext = mockContext(jndiName, ejb); + + SimpleRemoteSlsbInvokerInterceptor si = configuredInterceptor(mockContext, jndiName); + + RemoteInterface target = (RemoteInterface) configuredProxy(si, RemoteInterface.class); + assertThatExceptionOfType(RemoteException.class).isThrownBy( + target::targetMethod); + + verify(mockContext).close(); + verify(ejb, times(2)).remove(); + } + + @Test + public void testInvokesMethodOnEjbInstanceWithConnectExceptionWithRefresh() throws Exception { + doTestInvokesMethodOnEjbInstanceWithConnectExceptionWithRefresh(true, true); + } + + @Test + public void testInvokesMethodOnEjbInstanceWithConnectExceptionWithRefreshAndLazyLookup() throws Exception { + doTestInvokesMethodOnEjbInstanceWithConnectExceptionWithRefresh(false, true); + } + + @Test + public void testInvokesMethodOnEjbInstanceWithConnectExceptionWithRefreshAndLazyLookupAndNoCache() throws Exception { + doTestInvokesMethodOnEjbInstanceWithConnectExceptionWithRefresh(false, false); + } + + @Test + public void testInvokesMethodOnEjbInstanceWithConnectExceptionWithRefreshAndNoCache() throws Exception { + doTestInvokesMethodOnEjbInstanceWithConnectExceptionWithRefresh(true, false); + } + + private void doTestInvokesMethodOnEjbInstanceWithConnectExceptionWithRefresh( + boolean lookupHomeOnStartup, boolean cacheHome) throws Exception { + + final RemoteInterface ejb = mock(RemoteInterface.class); + given(ejb.targetMethod()).willThrow(new ConnectException("")); + + int lookupCount = 2; + if (!cacheHome) { + lookupCount++; + if (lookupHomeOnStartup) { + lookupCount++; + } + } + + final String jndiName= "foobar"; + Context mockContext = mockContext(jndiName, ejb); + + SimpleRemoteSlsbInvokerInterceptor si = configuredInterceptor(mockContext, jndiName); + si.setRefreshHomeOnConnectFailure(true); + si.setLookupHomeOnStartup(lookupHomeOnStartup); + si.setCacheHome(cacheHome); + + RemoteInterface target = (RemoteInterface) configuredProxy(si, RemoteInterface.class); + assertThatExceptionOfType(ConnectException.class).isThrownBy( + target::targetMethod); + + verify(mockContext, times(lookupCount)).close(); + verify(ejb, times(2)).remove(); + } + + @Test + public void testInvokesMethodOnEjbInstanceWithBusinessInterface() throws Exception { + Object retVal = new Object(); + final RemoteInterface ejb = mock(RemoteInterface.class); + given(ejb.targetMethod()).willReturn(retVal); + + final String jndiName= "foobar"; + Context mockContext = mockContext(jndiName, ejb); + + SimpleRemoteSlsbInvokerInterceptor si = configuredInterceptor(mockContext, jndiName); + + BusinessInterface target = (BusinessInterface) configuredProxy(si, BusinessInterface.class); + assertThat(target.targetMethod() == retVal).isTrue(); + + verify(mockContext).close(); + verify(ejb).remove(); + } + + @Test + public void testInvokesMethodOnEjbInstanceWithBusinessInterfaceWithRemoteException() throws Exception { + final RemoteInterface ejb = mock(RemoteInterface.class); + given(ejb.targetMethod()).willThrow(new RemoteException()); + + final String jndiName= "foobar"; + Context mockContext = mockContext(jndiName, ejb); + + SimpleRemoteSlsbInvokerInterceptor si = configuredInterceptor(mockContext, jndiName); + + BusinessInterface target = (BusinessInterface) configuredProxy(si, BusinessInterface.class); + assertThatExceptionOfType(RemoteAccessException.class).isThrownBy( + target::targetMethod); + + verify(mockContext).close(); + verify(ejb).remove(); + } + + @Test + public void testApplicationException() throws Exception { + doTestException(new ApplicationException()); + } + + @Test + public void testRemoteException() throws Exception { + doTestException(new RemoteException()); + } + + private void doTestException(Exception expected) throws Exception { + final RemoteInterface ejb = mock(RemoteInterface.class); + given(ejb.targetMethod()).willThrow(expected); + + final String jndiName= "foobar"; + Context mockContext = mockContext(jndiName, ejb); + + SimpleRemoteSlsbInvokerInterceptor si = configuredInterceptor(mockContext, jndiName); + + RemoteInterface target = (RemoteInterface) configuredProxy(si, RemoteInterface.class); + assertThatExceptionOfType(Exception.class) + .isThrownBy(target::targetMethod) + .isSameAs(expected); + verify(mockContext).close(); + verify(ejb).remove(); + } + + + /** + * Needed so that we can mock create() method. + */ + protected interface SlsbHome extends EJBHome { + + EJBObject create() throws RemoteException, CreateException; + } + + + protected interface RemoteInterface extends EJBObject { + + // Also business exception!? + Object targetMethod() throws RemoteException, ApplicationException; + } + + + protected interface BusinessInterface { + + Object targetMethod() throws ApplicationException; + } + + + @SuppressWarnings("serial") + protected class ApplicationException extends Exception { + + public ApplicationException() { + super("appException"); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/ejb/access/SimpleRemoteStatelessSessionProxyFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/ejb/access/SimpleRemoteStatelessSessionProxyFactoryBeanTests.java new file mode 100644 index 0000000..cc5dd44 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/ejb/access/SimpleRemoteStatelessSessionProxyFactoryBeanTests.java @@ -0,0 +1,288 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ejb.access; + +import java.lang.reflect.Proxy; +import java.rmi.RemoteException; + +import javax.ejb.CreateException; +import javax.ejb.EJBHome; +import javax.ejb.EJBObject; +import javax.naming.NamingException; + +import org.junit.jupiter.api.Test; + +import org.springframework.jndi.JndiTemplate; +import org.springframework.remoting.RemoteAccessException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +/** + * @author Rod Johnson + * @author Juergen Hoeller + * @since 21.05.2003 + */ +public class SimpleRemoteStatelessSessionProxyFactoryBeanTests extends SimpleRemoteSlsbInvokerInterceptorTests { + + @Override + protected SimpleRemoteSlsbInvokerInterceptor createInterceptor() { + return new SimpleRemoteStatelessSessionProxyFactoryBean(); + } + + @Override + protected Object configuredProxy(SimpleRemoteSlsbInvokerInterceptor si, Class ifc) throws NamingException { + SimpleRemoteStatelessSessionProxyFactoryBean fb = (SimpleRemoteStatelessSessionProxyFactoryBean) si; + fb.setBusinessInterface(ifc); + fb.afterPropertiesSet(); + return fb.getObject(); + } + + @Test + public void testInvokesMethod() throws Exception { + final int value = 11; + final String jndiName = "foo"; + + MyEjb myEjb = mock(MyEjb.class); + given(myEjb.getValue()).willReturn(value); + + final MyHome home = mock(MyHome.class); + given(home.create()).willReturn(myEjb); + + JndiTemplate jt = new JndiTemplate() { + @Override + public Object lookup(String name) { + // parameterize + assertThat(name.equals("java:comp/env/" + jndiName)).isTrue(); + return home; + } + }; + + SimpleRemoteStatelessSessionProxyFactoryBean fb = new SimpleRemoteStatelessSessionProxyFactoryBean(); + fb.setJndiName(jndiName); + fb.setResourceRef(true); + fb.setBusinessInterface(MyBusinessMethods.class); + fb.setJndiTemplate(jt); + + // Need lifecycle methods + fb.afterPropertiesSet(); + + MyBusinessMethods mbm = (MyBusinessMethods) fb.getObject(); + assertThat(Proxy.isProxyClass(mbm.getClass())).isTrue(); + assertThat(mbm.getValue()).as("Returns expected value").isEqualTo(value); + verify(myEjb).remove(); + } + + @Test + public void testInvokesMethodOnEjb3StyleBean() throws Exception { + final int value = 11; + final String jndiName = "foo"; + + final MyEjb myEjb = mock(MyEjb.class); + given(myEjb.getValue()).willReturn(value); + + JndiTemplate jt = new JndiTemplate() { + @Override + public Object lookup(String name) { + // parameterize + assertThat(name.equals("java:comp/env/" + jndiName)).isTrue(); + return myEjb; + } + }; + + SimpleRemoteStatelessSessionProxyFactoryBean fb = new SimpleRemoteStatelessSessionProxyFactoryBean(); + fb.setJndiName(jndiName); + fb.setResourceRef(true); + fb.setBusinessInterface(MyBusinessMethods.class); + fb.setJndiTemplate(jt); + + // Need lifecycle methods + fb.afterPropertiesSet(); + + MyBusinessMethods mbm = (MyBusinessMethods) fb.getObject(); + assertThat(Proxy.isProxyClass(mbm.getClass())).isTrue(); + assertThat(mbm.getValue()).as("Returns expected value").isEqualTo(value); + } + + @Override + @Test + public void testRemoteException() throws Exception { + final RemoteException rex = new RemoteException(); + final String jndiName = "foo"; + + MyEjb myEjb = mock(MyEjb.class); + given(myEjb.getValue()).willThrow(rex); + // TODO might want to control this behaviour... + // Do we really want to call remove after a remote exception? + + final MyHome home = mock(MyHome.class); + given(home.create()).willReturn(myEjb); + + JndiTemplate jt = new JndiTemplate() { + @Override + public Object lookup(String name) { + // parameterize + assertThat(name.equals("java:comp/env/" + jndiName)).isTrue(); + return home; + } + }; + + SimpleRemoteStatelessSessionProxyFactoryBean fb = new SimpleRemoteStatelessSessionProxyFactoryBean(); + fb.setJndiName(jndiName); + fb.setResourceRef(true); + fb.setBusinessInterface(MyBusinessMethods.class); + fb.setJndiTemplate(jt); + + // Need lifecycle methods + fb.afterPropertiesSet(); + + MyBusinessMethods mbm = (MyBusinessMethods) fb.getObject(); + assertThat(Proxy.isProxyClass(mbm.getClass())).isTrue(); + assertThatExceptionOfType(RemoteException.class) + .isThrownBy(mbm::getValue) + .isSameAs(rex); + verify(myEjb).remove(); + } + + @Test + public void testCreateException() throws Exception { + final String jndiName = "foo"; + + final CreateException cex = new CreateException(); + final MyHome home = mock(MyHome.class); + given(home.create()).willThrow(cex); + + JndiTemplate jt = new JndiTemplate() { + @Override + public Object lookup(String name) { + // parameterize + assertThat(name.equals(jndiName)).isTrue(); + return home; + } + }; + + SimpleRemoteStatelessSessionProxyFactoryBean fb = new SimpleRemoteStatelessSessionProxyFactoryBean(); + fb.setJndiName(jndiName); + // rely on default setting of resourceRef=false, no auto addition of java:/comp/env prefix + fb.setBusinessInterface(MyBusinessMethods.class); + assertThat(MyBusinessMethods.class).isEqualTo(fb.getBusinessInterface()); + fb.setJndiTemplate(jt); + + // Need lifecycle methods + fb.afterPropertiesSet(); + + MyBusinessMethods mbm = (MyBusinessMethods) fb.getObject(); + assertThat(Proxy.isProxyClass(mbm.getClass())).isTrue(); + assertThatExceptionOfType(RemoteException.class).isThrownBy(mbm::getValue); + } + + @Test + public void testCreateExceptionWithLocalBusinessInterface() throws Exception { + final String jndiName = "foo"; + + final CreateException cex = new CreateException(); + final MyHome home = mock(MyHome.class); + given(home.create()).willThrow(cex); + + JndiTemplate jt = new JndiTemplate() { + @Override + public Object lookup(String name) { + // parameterize + assertThat(name.equals(jndiName)).isTrue(); + return home; + } + }; + + SimpleRemoteStatelessSessionProxyFactoryBean fb = new SimpleRemoteStatelessSessionProxyFactoryBean(); + fb.setJndiName(jndiName); + // rely on default setting of resourceRef=false, no auto addition of java:/comp/env prefix + fb.setBusinessInterface(MyLocalBusinessMethods.class); + assertThat(MyLocalBusinessMethods.class).isEqualTo(fb.getBusinessInterface()); + fb.setJndiTemplate(jt); + + // Need lifecycle methods + fb.afterPropertiesSet(); + + MyLocalBusinessMethods mbm = (MyLocalBusinessMethods) fb.getObject(); + assertThat(Proxy.isProxyClass(mbm.getClass())).isTrue(); + assertThatExceptionOfType(RemoteAccessException.class).isThrownBy( + mbm::getValue) + .withCause(cex); + } + + @Test + public void testNoBusinessInterfaceSpecified() throws Exception { + // Will do JNDI lookup to get home but won't call create + // Could actually try to figure out interface from create? + final String jndiName = "foo"; + + final MyHome home = mock(MyHome.class); + + JndiTemplate jt = new JndiTemplate() { + @Override + public Object lookup(String name) throws NamingException { + // parameterize + assertThat(name.equals(jndiName)).isTrue(); + return home; + } + }; + + SimpleRemoteStatelessSessionProxyFactoryBean fb = new SimpleRemoteStatelessSessionProxyFactoryBean(); + fb.setJndiName(jndiName); + // rely on default setting of resourceRef=false, no auto addition of java:/comp/env prefix + // Don't set business interface + fb.setJndiTemplate(jt); + + // Check it's a singleton + assertThat(fb.isSingleton()).isTrue(); + + assertThatIllegalArgumentException().isThrownBy( + fb::afterPropertiesSet) + .withMessageContaining("businessInterface"); + + // Expect no methods on home + verifyNoInteractions(home); + } + + + protected interface MyHome extends EJBHome { + + MyBusinessMethods create() throws CreateException, RemoteException; + } + + + protected interface MyBusinessMethods { + + int getValue() throws RemoteException; + } + + + protected interface MyLocalBusinessMethods { + + int getValue(); + } + + + protected interface MyEjb extends EJBObject, MyBusinessMethods { + } + +} diff --git a/spring-context/src/test/java/org/springframework/ejb/config/JeeNamespaceHandlerEventTests.java b/spring-context/src/test/java/org/springframework/ejb/config/JeeNamespaceHandlerEventTests.java new file mode 100644 index 0000000..dc0e819 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/ejb/config/JeeNamespaceHandlerEventTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ejb.config; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.parsing.ComponentDefinition; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.beans.testfixture.beans.CollectingReaderEventListener; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Torsten Juergeleit + * @author Juergen Hoeller + * @author Chris Beams + */ +public class JeeNamespaceHandlerEventTests { + + private CollectingReaderEventListener eventListener = new CollectingReaderEventListener(); + + private XmlBeanDefinitionReader reader; + + private DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + + @BeforeEach + public void setUp() throws Exception { + this.reader = new XmlBeanDefinitionReader(this.beanFactory); + this.reader.setEventListener(this.eventListener); + this.reader.loadBeanDefinitions(new ClassPathResource("jeeNamespaceHandlerTests.xml", getClass())); + } + + @Test + public void testJndiLookupComponentEventReceived() { + ComponentDefinition component = this.eventListener.getComponentDefinition("simple"); + boolean condition = component instanceof BeanComponentDefinition; + assertThat(condition).isTrue(); + } + + @Test + public void testLocalSlsbComponentEventReceived() { + ComponentDefinition component = this.eventListener.getComponentDefinition("simpleLocalEjb"); + boolean condition = component instanceof BeanComponentDefinition; + assertThat(condition).isTrue(); + } + + @Test + public void testRemoteSlsbComponentEventReceived() { + ComponentDefinition component = this.eventListener.getComponentDefinition("simpleRemoteEjb"); + boolean condition = component instanceof BeanComponentDefinition; + assertThat(condition).isTrue(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/ejb/config/JeeNamespaceHandlerTests.java b/spring-context/src/test/java/org/springframework/ejb/config/JeeNamespaceHandlerTests.java new file mode 100644 index 0000000..ec96628 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/ejb/config/JeeNamespaceHandlerTests.java @@ -0,0 +1,150 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ejb.config; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.io.ClassPathResource; +import org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean; +import org.springframework.ejb.access.SimpleRemoteStatelessSessionProxyFactoryBean; +import org.springframework.jndi.JndiObjectFactoryBean; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + * @author Juergen Hoeller + * @author Chris Beams + * @author Oliver Gierke + */ +public class JeeNamespaceHandlerTests { + + private ConfigurableListableBeanFactory beanFactory; + + + @BeforeEach + public void setup() { + GenericApplicationContext ctx = new GenericApplicationContext(); + new XmlBeanDefinitionReader(ctx).loadBeanDefinitions( + new ClassPathResource("jeeNamespaceHandlerTests.xml", getClass())); + ctx.refresh(); + this.beanFactory = ctx.getBeanFactory(); + this.beanFactory.getBeanNamesForType(ITestBean.class); + } + + + @Test + public void testSimpleDefinition() { + BeanDefinition beanDefinition = this.beanFactory.getMergedBeanDefinition("simple"); + assertThat(beanDefinition.getBeanClassName()).isEqualTo(JndiObjectFactoryBean.class.getName()); + assertPropertyValue(beanDefinition, "jndiName", "jdbc/MyDataSource"); + assertPropertyValue(beanDefinition, "resourceRef", "true"); + } + + @Test + public void testComplexDefinition() { + BeanDefinition beanDefinition = this.beanFactory.getMergedBeanDefinition("complex"); + assertThat(beanDefinition.getBeanClassName()).isEqualTo(JndiObjectFactoryBean.class.getName()); + assertPropertyValue(beanDefinition, "jndiName", "jdbc/MyDataSource"); + assertPropertyValue(beanDefinition, "resourceRef", "true"); + assertPropertyValue(beanDefinition, "cache", "true"); + assertPropertyValue(beanDefinition, "lookupOnStartup", "true"); + assertPropertyValue(beanDefinition, "exposeAccessContext", "true"); + assertPropertyValue(beanDefinition, "expectedType", "com.myapp.DefaultFoo"); + assertPropertyValue(beanDefinition, "proxyInterface", "com.myapp.Foo"); + assertPropertyValue(beanDefinition, "defaultObject", "myValue"); + } + + @Test + public void testWithEnvironment() { + BeanDefinition beanDefinition = this.beanFactory.getMergedBeanDefinition("withEnvironment"); + assertPropertyValue(beanDefinition, "jndiEnvironment", "foo=bar"); + assertPropertyValue(beanDefinition, "defaultObject", new RuntimeBeanReference("myBean")); + } + + @Test + public void testWithReferencedEnvironment() { + BeanDefinition beanDefinition = this.beanFactory.getMergedBeanDefinition("withReferencedEnvironment"); + assertPropertyValue(beanDefinition, "jndiEnvironment", new RuntimeBeanReference("myEnvironment")); + assertThat(beanDefinition.getPropertyValues().contains("environmentRef")).isFalse(); + } + + @Test + public void testSimpleLocalSlsb() { + BeanDefinition beanDefinition = this.beanFactory.getMergedBeanDefinition("simpleLocalEjb"); + assertThat(beanDefinition.getBeanClassName()).isEqualTo(LocalStatelessSessionProxyFactoryBean.class.getName()); + assertPropertyValue(beanDefinition, "businessInterface", ITestBean.class.getName()); + assertPropertyValue(beanDefinition, "jndiName", "ejb/MyLocalBean"); + } + + @Test + public void testSimpleRemoteSlsb() { + BeanDefinition beanDefinition = this.beanFactory.getMergedBeanDefinition("simpleRemoteEjb"); + assertThat(beanDefinition.getBeanClassName()).isEqualTo(SimpleRemoteStatelessSessionProxyFactoryBean.class.getName()); + assertPropertyValue(beanDefinition, "businessInterface", ITestBean.class.getName()); + assertPropertyValue(beanDefinition, "jndiName", "ejb/MyRemoteBean"); + } + + @Test + public void testComplexLocalSlsb() { + BeanDefinition beanDefinition = this.beanFactory.getMergedBeanDefinition("complexLocalEjb"); + assertThat(beanDefinition.getBeanClassName()).isEqualTo(LocalStatelessSessionProxyFactoryBean.class.getName()); + assertPropertyValue(beanDefinition, "businessInterface", ITestBean.class.getName()); + assertPropertyValue(beanDefinition, "jndiName", "ejb/MyLocalBean"); + assertPropertyValue(beanDefinition, "cacheHome", "true"); + assertPropertyValue(beanDefinition, "lookupHomeOnStartup", "true"); + assertPropertyValue(beanDefinition, "resourceRef", "true"); + assertPropertyValue(beanDefinition, "jndiEnvironment", "foo=bar"); + } + + @Test + public void testComplexRemoteSlsb() { + BeanDefinition beanDefinition = this.beanFactory.getMergedBeanDefinition("complexRemoteEjb"); + assertThat(beanDefinition.getBeanClassName()).isEqualTo(SimpleRemoteStatelessSessionProxyFactoryBean.class.getName()); + assertPropertyValue(beanDefinition, "businessInterface", ITestBean.class.getName()); + assertPropertyValue(beanDefinition, "jndiName", "ejb/MyRemoteBean"); + assertPropertyValue(beanDefinition, "cacheHome", "true"); + assertPropertyValue(beanDefinition, "lookupHomeOnStartup", "true"); + assertPropertyValue(beanDefinition, "resourceRef", "true"); + assertPropertyValue(beanDefinition, "jndiEnvironment", "foo=bar"); + assertPropertyValue(beanDefinition, "homeInterface", "org.springframework.beans.testfixture.beans.ITestBean"); + assertPropertyValue(beanDefinition, "refreshHomeOnConnectFailure", "true"); + assertPropertyValue(beanDefinition, "cacheSessionBean", "true"); + } + + @Test + public void testLazyInitJndiLookup() { + BeanDefinition definition = this.beanFactory.getMergedBeanDefinition("lazyDataSource"); + assertThat(definition.isLazyInit()).isTrue(); + definition = this.beanFactory.getMergedBeanDefinition("lazyLocalBean"); + assertThat(definition.isLazyInit()).isTrue(); + definition = this.beanFactory.getMergedBeanDefinition("lazyRemoteBean"); + assertThat(definition.isLazyInit()).isTrue(); + } + + private void assertPropertyValue(BeanDefinition beanDefinition, String propertyName, Object expectedValue) { + assertThat(beanDefinition.getPropertyValues().getPropertyValue(propertyName).getValue()).as("Property '" + propertyName + "' incorrect").isEqualTo(expectedValue); + } + +} diff --git a/spring-context/src/test/java/org/springframework/format/datetime/DateFormatterTests.java b/spring-context/src/test/java/org/springframework/format/datetime/DateFormatterTests.java new file mode 100644 index 0000000..7737902 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/format/datetime/DateFormatterTests.java @@ -0,0 +1,224 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime; + +import java.text.DateFormat; +import java.text.ParseException; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +import org.joda.time.DateTimeZone; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import org.junit.jupiter.api.Test; + +import org.springframework.format.annotation.DateTimeFormat.ISO; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + + + + +/** + * Tests for {@link DateFormatter}. + * + * @author Keith Donald + * @author Phillip Webb + */ +public class DateFormatterTests { + + private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); + + + @Test + public void shouldPrintAndParseDefault() throws Exception { + DateFormatter formatter = new DateFormatter(); + formatter.setTimeZone(UTC); + Date date = getDate(2009, Calendar.JUNE, 1); + assertThat(formatter.print(date, Locale.US)).isEqualTo("Jun 1, 2009"); + assertThat(formatter.parse("Jun 1, 2009", Locale.US)).isEqualTo(date); + } + + @Test + public void shouldPrintAndParseFromPattern() throws ParseException { + DateFormatter formatter = new DateFormatter("yyyy-MM-dd"); + formatter.setTimeZone(UTC); + Date date = getDate(2009, Calendar.JUNE, 1); + assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01"); + assertThat(formatter.parse("2009-06-01", Locale.US)).isEqualTo(date); + } + + @Test + public void shouldPrintAndParseShort() throws Exception { + DateFormatter formatter = new DateFormatter(); + formatter.setTimeZone(UTC); + formatter.setStyle(DateFormat.SHORT); + Date date = getDate(2009, Calendar.JUNE, 1); + assertThat(formatter.print(date, Locale.US)).isEqualTo("6/1/09"); + assertThat(formatter.parse("6/1/09", Locale.US)).isEqualTo(date); + } + + @Test + public void shouldPrintAndParseMedium() throws Exception { + DateFormatter formatter = new DateFormatter(); + formatter.setTimeZone(UTC); + formatter.setStyle(DateFormat.MEDIUM); + Date date = getDate(2009, Calendar.JUNE, 1); + assertThat(formatter.print(date, Locale.US)).isEqualTo("Jun 1, 2009"); + assertThat(formatter.parse("Jun 1, 2009", Locale.US)).isEqualTo(date); + } + + @Test + public void shouldPrintAndParseLong() throws Exception { + DateFormatter formatter = new DateFormatter(); + formatter.setTimeZone(UTC); + formatter.setStyle(DateFormat.LONG); + Date date = getDate(2009, Calendar.JUNE, 1); + assertThat(formatter.print(date, Locale.US)).isEqualTo("June 1, 2009"); + assertThat(formatter.parse("June 1, 2009", Locale.US)).isEqualTo(date); + } + + @Test + public void shouldPrintAndParseFull() throws Exception { + DateFormatter formatter = new DateFormatter(); + formatter.setTimeZone(UTC); + formatter.setStyle(DateFormat.FULL); + Date date = getDate(2009, Calendar.JUNE, 1); + assertThat(formatter.print(date, Locale.US)).isEqualTo("Monday, June 1, 2009"); + assertThat(formatter.parse("Monday, June 1, 2009", Locale.US)).isEqualTo(date); + } + + @Test + public void shouldPrintAndParseISODate() throws Exception { + DateFormatter formatter = new DateFormatter(); + formatter.setTimeZone(UTC); + formatter.setIso(ISO.DATE); + Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3); + assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01"); + assertThat(formatter.parse("2009-6-01", Locale.US)) + .isEqualTo(getDate(2009, Calendar.JUNE, 1)); + } + + @Test + public void shouldPrintAndParseISOTime() throws Exception { + DateFormatter formatter = new DateFormatter(); + formatter.setTimeZone(UTC); + formatter.setIso(ISO.TIME); + Date date = getDate(2009, Calendar.JANUARY, 1, 14, 23, 5, 3); + assertThat(formatter.print(date, Locale.US)).isEqualTo("14:23:05.003Z"); + assertThat(formatter.parse("14:23:05.003Z", Locale.US)) + .isEqualTo(getDate(1970, Calendar.JANUARY, 1, 14, 23, 5, 3)); + } + + @Test + public void shouldPrintAndParseISODateTime() throws Exception { + DateFormatter formatter = new DateFormatter(); + formatter.setTimeZone(UTC); + formatter.setIso(ISO.DATE_TIME); + Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3); + assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01T14:23:05.003Z"); + assertThat(formatter.parse("2009-06-01T14:23:05.003Z", Locale.US)).isEqualTo(date); + } + + @Test + public void shouldSupportJodaStylePatterns() throws Exception { + String[] chars = { "S", "M", "-" }; + for (String d : chars) { + for (String t : chars) { + String style = d + t; + if (!style.equals("--")) { + Date date = getDate(2009, Calendar.JUNE, 10, 14, 23, 0, 0); + if (t.equals("-")) { + date = getDate(2009, Calendar.JUNE, 10); + } + else if (d.equals("-")) { + date = getDate(1970, Calendar.JANUARY, 1, 14, 23, 0, 0); + } + testJodaStylePatterns(style, Locale.US, date); + } + } + } + } + + private void testJodaStylePatterns(String style, Locale locale, Date date) throws Exception { + DateFormatter formatter = new DateFormatter(); + formatter.setTimeZone(UTC); + formatter.setStylePattern(style); + DateTimeFormatter jodaFormatter = DateTimeFormat.forStyle(style).withLocale(locale).withZone(DateTimeZone.UTC); + String jodaPrinted = jodaFormatter.print(date.getTime()); + assertThat(formatter.print(date, locale)) + .as("Unable to print style pattern " + style) + .isEqualTo(jodaPrinted); + assertThat(formatter.parse(jodaPrinted, locale)) + .as("Unable to parse style pattern " + style) + .isEqualTo(date); + } + + @Test + public void shouldThrowOnUnsupportedStylePattern() throws Exception { + DateFormatter formatter = new DateFormatter(); + formatter.setStylePattern("OO"); + assertThatIllegalStateException().isThrownBy(() -> + formatter.parse("2009", Locale.US)) + .withMessageContaining("Unsupported style pattern 'OO'"); + } + + @Test + public void shouldUseCorrectOrder() throws Exception { + DateFormatter formatter = new DateFormatter(); + formatter.setTimeZone(UTC); + formatter.setStyle(DateFormat.SHORT); + formatter.setStylePattern("L-"); + formatter.setIso(ISO.DATE_TIME); + formatter.setPattern("yyyy"); + Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3); + + assertThat(formatter.print(date, Locale.US)).as("uses pattern").isEqualTo("2009"); + + formatter.setPattern(""); + assertThat(formatter.print(date, Locale.US)).as("uses ISO").isEqualTo("2009-06-01T14:23:05.003Z"); + + formatter.setIso(ISO.NONE); + assertThat(formatter.print(date, Locale.US)).as("uses style pattern").isEqualTo("June 1, 2009"); + + formatter.setStylePattern(""); + assertThat(formatter.print(date, Locale.US)).as("uses style").isEqualTo("6/1/09"); + } + + + private Date getDate(int year, int month, int dayOfMonth) { + return getDate(year, month, dayOfMonth, 0, 0, 0, 0); + } + + private Date getDate(int year, int month, int dayOfMonth, int hour, int minute, int second, int millisecond) { + Calendar cal = Calendar.getInstance(Locale.US); + cal.setTimeZone(UTC); + cal.clear(); + cal.set(Calendar.YEAR, year); + cal.set(Calendar.MONTH, month); + cal.set(Calendar.DAY_OF_MONTH, dayOfMonth); + cal.set(Calendar.HOUR, hour); + cal.set(Calendar.MINUTE, minute); + cal.set(Calendar.SECOND, second); + cal.set(Calendar.MILLISECOND, millisecond); + return cal.getTime(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java b/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java new file mode 100644 index 0000000..30b45b1 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java @@ -0,0 +1,347 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.format.annotation.DateTimeFormat.ISO; +import org.springframework.format.support.FormattingConversionService; +import org.springframework.validation.DataBinder; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Phillip Webb + * @author Keith Donald + * @author Juergen Hoeller + */ +public class DateFormattingTests { + + private FormattingConversionService conversionService; + + private DataBinder binder; + + + @BeforeEach + void setup() { + DateFormatterRegistrar registrar = new DateFormatterRegistrar(); + setup(registrar); + } + + private void setup(DateFormatterRegistrar registrar) { + conversionService = new FormattingConversionService(); + DefaultConversionService.addDefaultConverters(conversionService); + registrar.registerFormatters(conversionService); + + SimpleDateBean bean = new SimpleDateBean(); + bean.getChildren().add(new SimpleDateBean()); + binder = new DataBinder(bean); + binder.setConversionService(conversionService); + + LocaleContextHolder.setLocale(Locale.US); + } + + @AfterEach + void tearDown() { + LocaleContextHolder.setLocale(null); + } + + + @Test + void testBindLong() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("millis", "1256961600"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("millis")).isEqualTo("1256961600"); + } + + @Test + void testBindLongAnnotated() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("millisAnnotated", "10/31/09"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("millisAnnotated")).isEqualTo("10/31/09"); + } + + @Test + void testBindCalendarAnnotated() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("calendarAnnotated", "10/31/09"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("calendarAnnotated")).isEqualTo("10/31/09"); + } + + @Test + void testBindDateAnnotated() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("dateAnnotated", "10/31/09"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("dateAnnotated")).isEqualTo("10/31/09"); + } + + @Test + void testBindDateArray() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("dateAnnotated", new String[]{"10/31/09 12:00 PM"}); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + } + + @Test + void testBindDateAnnotatedWithError() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("dateAnnotated", "Oct X31, 2009"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getFieldErrorCount("dateAnnotated")).isEqualTo(1); + assertThat(binder.getBindingResult().getFieldValue("dateAnnotated")).isEqualTo("Oct X31, 2009"); + } + + @Test + @Disabled + void testBindDateAnnotatedWithFallbackError() { + // TODO This currently passes because of the Date(String) constructor fallback is used + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("dateAnnotated", "Oct 031, 2009"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getFieldErrorCount("dateAnnotated")).isEqualTo(1); + assertThat(binder.getBindingResult().getFieldValue("dateAnnotated")).isEqualTo("Oct 031, 2009"); + } + + @Test + void testBindDateAnnotatedPattern() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("dateAnnotatedPattern", "10/31/09 1:05"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("dateAnnotatedPattern")).isEqualTo("10/31/09 1:05"); + } + + @Test + void testBindDateAnnotatedPatternWithGlobalFormat() { + DateFormatterRegistrar registrar = new DateFormatterRegistrar(); + DateFormatter dateFormatter = new DateFormatter(); + dateFormatter.setIso(ISO.DATE_TIME); + registrar.setFormatter(dateFormatter); + setup(registrar); + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("dateAnnotatedPattern", "10/31/09 1:05"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("dateAnnotatedPattern")).isEqualTo("10/31/09 1:05"); + } + + @Test + void testBindDateTimeOverflow() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("dateAnnotatedPattern", "02/29/09 12:00 PM"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(1); + } + + @Test + void testBindISODate() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("isoDate", "2009-10-31"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("isoDate")).isEqualTo("2009-10-31"); + } + + @Test + void testBindISOTime() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("isoTime", "12:00:00.000-05:00"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("isoTime")).isEqualTo("17:00:00.000Z"); + } + + @Test + void testBindISODateTime() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("isoDateTime", "2009-10-31T12:00:00.000-08:00"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("isoDateTime")).isEqualTo("2009-10-31T20:00:00.000Z"); + } + + @Test + void testBindNestedDateAnnotated() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("children[0].dateAnnotated", "10/31/09"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("children[0].dateAnnotated")).isEqualTo("10/31/09"); + } + + @Test + void dateToStringWithoutGlobalFormat() { + Date date = new Date(); + Object actual = this.conversionService.convert(date, TypeDescriptor.valueOf(Date.class), TypeDescriptor.valueOf(String.class)); + String expected = date.toString(); + assertThat(actual).isEqualTo(expected); + } + + @Test + void dateToStringWithGlobalFormat() { + DateFormatterRegistrar registrar = new DateFormatterRegistrar(); + registrar.setFormatter(new DateFormatter()); + setup(registrar); + Date date = new Date(); + Object actual = this.conversionService.convert(date, TypeDescriptor.valueOf(Date.class), TypeDescriptor.valueOf(String.class)); + String expected = new DateFormatter().print(date, Locale.US); + assertThat(actual).isEqualTo(expected); + } + + @Test // SPR-10105 + @SuppressWarnings("deprecation") + void stringToDateWithoutGlobalFormat() { + String string = "Sat, 12 Aug 1995 13:30:00 GM"; + Date date = this.conversionService.convert(string, Date.class); + assertThat(date).isEqualTo(new Date(string)); + } + + @Test // SPR-10105 + void stringToDateWithGlobalFormat() { + DateFormatterRegistrar registrar = new DateFormatterRegistrar(); + DateFormatter dateFormatter = new DateFormatter(); + dateFormatter.setIso(ISO.DATE_TIME); + registrar.setFormatter(dateFormatter); + setup(registrar); + // This is a format that cannot be parsed by new Date(String) + String string = "2009-06-01T14:23:05.003+00:00"; + Date date = this.conversionService.convert(string, Date.class); + assertThat(date).isNotNull(); + } + + + @SuppressWarnings("unused") + private static class SimpleDateBean { + + private Long millis; + + private Long millisAnnotated; + + @DateTimeFormat(style="S-") + private Calendar calendarAnnotated; + + @DateTimeFormat(style="S-") + private Date dateAnnotated; + + @DateTimeFormat(pattern="M/d/yy h:mm") + private Date dateAnnotatedPattern; + + @DateTimeFormat(iso=ISO.DATE) + private Date isoDate; + + @DateTimeFormat(iso=ISO.TIME) + private Date isoTime; + + @DateTimeFormat(iso=ISO.DATE_TIME) + private Date isoDateTime; + + private final List children = new ArrayList<>(); + + public Long getMillis() { + return millis; + } + + public void setMillis(Long millis) { + this.millis = millis; + } + + @DateTimeFormat(style="S-") + public Long getMillisAnnotated() { + return millisAnnotated; + } + + public void setMillisAnnotated(@DateTimeFormat(style="S-") Long millisAnnotated) { + this.millisAnnotated = millisAnnotated; + } + + public Calendar getCalendarAnnotated() { + return calendarAnnotated; + } + + public void setCalendarAnnotated(Calendar calendarAnnotated) { + this.calendarAnnotated = calendarAnnotated; + } + + public Date getDateAnnotated() { + return dateAnnotated; + } + + public void setDateAnnotated(Date dateAnnotated) { + this.dateAnnotated = dateAnnotated; + } + + public Date getDateAnnotatedPattern() { + return dateAnnotatedPattern; + } + + public void setDateAnnotatedPattern(Date dateAnnotatedPattern) { + this.dateAnnotatedPattern = dateAnnotatedPattern; + } + + public Date getIsoDate() { + return isoDate; + } + + public void setIsoDate(Date isoDate) { + this.isoDate = isoDate; + } + + public Date getIsoTime() { + return isoTime; + } + + public void setIsoTime(Date isoTime) { + this.isoTime = isoTime; + } + + public Date getIsoDateTime() { + return isoDateTime; + } + + public void setIsoDateTime(Date isoDateTime) { + this.isoDateTime = isoDateTime; + } + + public List getChildren() { + return children; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/format/datetime/joda/DateTimeFormatterFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/format/datetime/joda/DateTimeFormatterFactoryBeanTests.java new file mode 100644 index 0000000..4472cb3 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/format/datetime/joda/DateTimeFormatterFactoryBeanTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.joda; + +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + + + + + +/** + * @author Phillip Webb + * @author Sam Brannen + */ +public class DateTimeFormatterFactoryBeanTests { + + private final DateTimeFormatterFactoryBean factory = new DateTimeFormatterFactoryBean(); + + + @Test + public void isSingleton() { + assertThat(factory.isSingleton()).isTrue(); + } + + @Test + public void getObjectType() { + assertThat(factory.getObjectType()).isEqualTo(DateTimeFormatter.class); + } + + @Test + public void getObject() { + factory.afterPropertiesSet(); + assertThat(factory.getObject()).isEqualTo(DateTimeFormat.mediumDateTime()); + } + + @Test + public void getObjectIsAlwaysSingleton() { + factory.afterPropertiesSet(); + DateTimeFormatter formatter = factory.getObject(); + assertThat(formatter).isEqualTo(DateTimeFormat.mediumDateTime()); + factory.setStyle("LL"); + assertThat(factory.getObject()).isSameAs(formatter); + } + +} diff --git a/spring-context/src/test/java/org/springframework/format/datetime/joda/DateTimeFormatterFactoryTests.java b/spring-context/src/test/java/org/springframework/format/datetime/joda/DateTimeFormatterFactoryTests.java new file mode 100644 index 0000000..d76302f --- /dev/null +++ b/spring-context/src/test/java/org/springframework/format/datetime/joda/DateTimeFormatterFactoryTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.joda; + +import java.util.Locale; +import java.util.TimeZone; + +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import org.junit.jupiter.api.Test; + +import org.springframework.format.annotation.DateTimeFormat.ISO; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Phillip Webb + * @author Sam Brannen + */ +public class DateTimeFormatterFactoryTests { + + // Potential test timezone, both have daylight savings on October 21st + private static final TimeZone ZURICH = TimeZone.getTimeZone("Europe/Zurich"); + private static final TimeZone NEW_YORK = TimeZone.getTimeZone("America/New_York"); + + // Ensure that we are testing against a timezone other than the default. + private static final TimeZone TEST_TIMEZONE = ZURICH.equals(TimeZone.getDefault()) ? NEW_YORK : ZURICH; + + + private DateTimeFormatterFactory factory = new DateTimeFormatterFactory(); + + private DateTime dateTime = new DateTime(2009, 10, 21, 12, 10, 00, 00); + + + @Test + public void createDateTimeFormatter() { + assertThat(factory.createDateTimeFormatter()).isEqualTo(DateTimeFormat.mediumDateTime()); + } + + @Test + public void createDateTimeFormatterWithPattern() { + factory = new DateTimeFormatterFactory("yyyyMMddHHmmss"); + DateTimeFormatter formatter = factory.createDateTimeFormatter(); + assertThat(formatter.print(dateTime)).isEqualTo("20091021121000"); + } + + @Test + public void createDateTimeFormatterWithNullFallback() { + DateTimeFormatter formatter = factory.createDateTimeFormatter(null); + assertThat(formatter).isNull(); + } + + @Test + public void createDateTimeFormatterWithFallback() { + DateTimeFormatter fallback = DateTimeFormat.forStyle("LL"); + DateTimeFormatter formatter = factory.createDateTimeFormatter(fallback); + assertThat(formatter).isSameAs(fallback); + } + + @Test + public void createDateTimeFormatterInOrderOfPropertyPriority() { + factory.setStyle("SS"); + String value = applyLocale(factory.createDateTimeFormatter()).print(dateTime); + assertThat(value.startsWith("10/21/09")).isTrue(); + assertThat(value.endsWith("12:10 PM")).isTrue(); + + factory.setIso(ISO.DATE); + assertThat(applyLocale(factory.createDateTimeFormatter()).print(dateTime)).isEqualTo("2009-10-21"); + + factory.setPattern("yyyyMMddHHmmss"); + assertThat(factory.createDateTimeFormatter().print(dateTime)).isEqualTo("20091021121000"); + } + + @Test + public void createDateTimeFormatterWithTimeZone() { + factory.setPattern("yyyyMMddHHmmss Z"); + factory.setTimeZone(TEST_TIMEZONE); + DateTimeZone dateTimeZone = DateTimeZone.forTimeZone(TEST_TIMEZONE); + DateTime dateTime = new DateTime(2009, 10, 21, 12, 10, 00, 00, dateTimeZone); + String offset = (TEST_TIMEZONE.equals(NEW_YORK) ? "-0400" : "+0200"); + assertThat(factory.createDateTimeFormatter().print(dateTime)).isEqualTo("20091021121000 " + offset); + } + + private DateTimeFormatter applyLocale(DateTimeFormatter dateTimeFormatter) { + return dateTimeFormatter.withLocale(Locale.US); + } + +} diff --git a/spring-context/src/test/java/org/springframework/format/datetime/joda/JodaTimeFormattingTests.java b/spring-context/src/test/java/org/springframework/format/datetime/joda/JodaTimeFormattingTests.java new file mode 100644 index 0000000..63a241f --- /dev/null +++ b/spring-context/src/test/java/org/springframework/format/datetime/joda/JodaTimeFormattingTests.java @@ -0,0 +1,770 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.joda; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.Duration; +import org.joda.time.Instant; +import org.joda.time.LocalDate; +import org.joda.time.LocalDateTime; +import org.joda.time.LocalTime; +import org.joda.time.MonthDay; +import org.joda.time.Period; +import org.joda.time.YearMonth; +import org.joda.time.chrono.ISOChronology; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.format.annotation.DateTimeFormat.ISO; +import org.springframework.format.support.FormattingConversionService; +import org.springframework.validation.DataBinder; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Keith Donald + * @author Juergen Hoeller + * @author Phillip Webb + */ +public class JodaTimeFormattingTests { + + private FormattingConversionService conversionService; + + private DataBinder binder; + + + @BeforeEach + public void setup() { + JodaTimeFormatterRegistrar registrar = new JodaTimeFormatterRegistrar(); + setup(registrar); + } + + private void setup(JodaTimeFormatterRegistrar registrar) { + conversionService = new FormattingConversionService(); + DefaultConversionService.addDefaultConverters(conversionService); + registrar.registerFormatters(conversionService); + + JodaTimeBean bean = new JodaTimeBean(); + bean.getChildren().add(new JodaTimeBean()); + binder = new DataBinder(bean); + binder.setConversionService(conversionService); + + LocaleContextHolder.setLocale(Locale.US); + JodaTimeContext context = new JodaTimeContext(); + context.setTimeZone(DateTimeZone.forID("-05:00")); + JodaTimeContextHolder.setJodaTimeContext(context); + } + + @AfterEach + public void cleanup() { + LocaleContextHolder.setLocale(null); + JodaTimeContextHolder.setJodaTimeContext(null); + } + + + @Test + public void testJodaTimePatternsForStyle() { + System.out.println(org.joda.time.format.DateTimeFormat.patternForStyle("SS", LocaleContextHolder.getLocale())); + System.out.println(org.joda.time.format.DateTimeFormat.patternForStyle("MM", LocaleContextHolder.getLocale())); + System.out.println(org.joda.time.format.DateTimeFormat.patternForStyle("LL", LocaleContextHolder.getLocale())); + System.out.println(org.joda.time.format.DateTimeFormat.patternForStyle("FF", LocaleContextHolder.getLocale())); + } + + @Test + public void testBindLocalDate() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDate", "10/31/09"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("localDate")).isEqualTo("10/31/09"); + } + + @Test + public void testBindLocalDateWithSpecificStyle() { + JodaTimeFormatterRegistrar registrar = new JodaTimeFormatterRegistrar(); + registrar.setDateStyle("L"); + setup(registrar); + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDate", "October 31, 2009"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("localDate")).isEqualTo("October 31, 2009"); + } + + @Test + public void testBindLocalDateWithSpecificFormatter() { + JodaTimeFormatterRegistrar registrar = new JodaTimeFormatterRegistrar(); + registrar.setDateFormatter(org.joda.time.format.DateTimeFormat.forPattern("yyyyMMdd")); + setup(registrar); + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDate", "20091031"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("localDate")).isEqualTo("20091031"); + } + + @Test + public void testBindLocalDateArray() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDate", new String[]{"10/31/09"}); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + } + + @Test + public void testBindLocalDateAnnotated() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDateAnnotated", "Oct 31, 2009"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("localDateAnnotated")).isEqualTo("Oct 31, 2009"); + } + + @Test + public void testBindLocalDateAnnotatedWithError() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDateAnnotated", "Oct 031, 2009"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getFieldErrorCount("localDateAnnotated")).isEqualTo(1); + assertThat(binder.getBindingResult().getFieldValue("localDateAnnotated")).isEqualTo("Oct 031, 2009"); + } + + @Test + public void testBindNestedLocalDateAnnotated() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("children[0].localDateAnnotated", "Oct 31, 2009"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("children[0].localDateAnnotated")).isEqualTo("Oct 31, 2009"); + } + + @Test + public void testBindLocalDateAnnotatedWithDirectFieldAccess() { + binder.initDirectFieldAccess(); + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDateAnnotated", "Oct 31, 2009"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("localDateAnnotated")).isEqualTo("Oct 31, 2009"); + } + + @Test + public void testBindLocalDateAnnotatedWithDirectFieldAccessAndError() { + binder.initDirectFieldAccess(); + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDateAnnotated", "Oct 031, 2009"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getFieldErrorCount("localDateAnnotated")).isEqualTo(1); + assertThat(binder.getBindingResult().getFieldValue("localDateAnnotated")).isEqualTo("Oct 031, 2009"); + } + + @Test + public void testBindLocalTime() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localTime", "12:00 PM"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("localTime")).isEqualTo("12:00 PM"); + } + + @Test + public void testBindLocalTimeWithSpecificStyle() { + JodaTimeFormatterRegistrar registrar = new JodaTimeFormatterRegistrar(); + registrar.setTimeStyle("M"); + setup(registrar); + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localTime", "12:00:00 PM"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("localTime")).isEqualTo("12:00:00 PM"); + } + + @Test + public void testBindLocalTimeWithSpecificFormatter() { + JodaTimeFormatterRegistrar registrar = new JodaTimeFormatterRegistrar(); + registrar.setTimeFormatter(org.joda.time.format.DateTimeFormat.forPattern("HHmmss")); + setup(registrar); + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localTime", "130000"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("localTime")).isEqualTo("130000"); + } + + @Test + public void testBindLocalTimeAnnotated() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localTimeAnnotated", "12:00:00 PM"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("localTimeAnnotated")).isEqualTo("12:00:00 PM"); + } + + @Test + public void testBindLocalDateTime() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDateTime", new LocalDateTime(2009, 10, 31, 12, 0)); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + String value = binder.getBindingResult().getFieldValue("localDateTime").toString(); + assertThat(value.startsWith("10/31/09")).isTrue(); + assertThat(value.endsWith("12:00 PM")).isTrue(); + } + + @Test + public void testBindLocalDateTimeAnnotated() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDateTimeAnnotated", new LocalDateTime(2009, 10, 31, 12, 0)); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + String value = binder.getBindingResult().getFieldValue("localDateTimeAnnotated").toString(); + assertThat(value.startsWith("Oct 31, 2009")).isTrue(); + assertThat(value.endsWith("12:00 PM")).isTrue(); + } + + @Test + public void testBindDateTime() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("dateTime", new DateTime(2009, 10, 31, 12, 0, ISOChronology.getInstanceUTC())); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + String value = binder.getBindingResult().getFieldValue("dateTime").toString(); + assertThat(value.startsWith("10/31/09")).isTrue(); + } + + @Test + public void testBindDateTimeWithSpecificStyle() { + JodaTimeFormatterRegistrar registrar = new JodaTimeFormatterRegistrar(); + registrar.setDateTimeStyle("MM"); + setup(registrar); + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDateTime", new LocalDateTime(2009, 10, 31, 12, 0)); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + String value = binder.getBindingResult().getFieldValue("localDateTime").toString(); + assertThat(value.startsWith("Oct 31, 2009")).isTrue(); + assertThat(value.endsWith("12:00:00 PM")).isTrue(); + } + + @Test + public void testBindDateTimeISO() { + JodaTimeFormatterRegistrar registrar = new JodaTimeFormatterRegistrar(); + registrar.setUseIsoFormat(true); + setup(registrar); + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("dateTime", "2009-10-31T12:00:00.000Z"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("dateTime")).isEqualTo("2009-10-31T07:00:00.000-05:00"); + } + + @Test + public void testBindDateTimeWithSpecificFormatter() { + JodaTimeFormatterRegistrar registrar = new JodaTimeFormatterRegistrar(); + registrar.setDateTimeFormatter(org.joda.time.format.DateTimeFormat.forPattern("yyyyMMddHHmmss")); + setup(registrar); + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("dateTime", "20091031130000"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("dateTime")).isEqualTo("20091031130000"); + } + + @Test + public void testBindDateTimeAnnotated() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("dateTimeAnnotated", new DateTime(2009, 10, 31, 12, 0, ISOChronology.getInstanceUTC())); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + String value = binder.getBindingResult().getFieldValue("dateTimeAnnotated").toString(); + assertThat(value.startsWith("Oct 31, 2009")).isTrue(); + } + + @Test + public void testBindDateTimeAnnotatedPattern() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("dateTimeAnnotatedPattern", "10/31/09 12:00 PM"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("dateTimeAnnotatedPattern")).isEqualTo("10/31/09 12:00 PM"); + } + + @Test + public void testBindDateTimeOverflow() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("dateTimeAnnotatedPattern", "02/29/09 12:00 PM"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(1); + } + + @Test + public void testBindDateTimeAnnotatedDefault() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("dateTimeAnnotatedDefault", new DateTime(2009, 10, 31, 12, 0, ISOChronology.getInstanceUTC())); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + String value = binder.getBindingResult().getFieldValue("dateTimeAnnotatedDefault").toString(); + assertThat(value.startsWith("10/31/09")).isTrue(); + } + + @Test + public void testBindDateWithErrorAvoidingDateConstructor() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("date", "Sat, 12 Aug 1995 13:30:00 GMT"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(1); + assertThat(binder.getBindingResult().getFieldValue("date")).isEqualTo("Sat, 12 Aug 1995 13:30:00 GMT"); + } + + @Test + public void testBindDateWithoutErrorFallingBackToDateConstructor() { + DataBinder binder = new DataBinder(new JodaTimeBean()); + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("date", "Sat, 12 Aug 1995 13:30:00 GMT"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + } + + @Test + public void testBindDateAnnotated() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("dateAnnotated", "10/31/09"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("dateAnnotated")).isEqualTo("10/31/09"); + } + + @Test + public void testBindCalendarAnnotated() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("calendarAnnotated", "10/31/09"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("calendarAnnotated")).isEqualTo("10/31/09"); + } + + @Test + public void testBindLong() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("millis", "1256961600"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("millis")).isEqualTo("1256961600"); + } + + @Test + public void testBindLongAnnotated() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("millisAnnotated", "10/31/09"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("millisAnnotated")).isEqualTo("10/31/09"); + } + + @Test + public void testBindISODate() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("isoDate", "2009-10-31"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("isoDate")).isEqualTo("2009-10-31"); + } + + @Test + public void testBindISOTime() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("isoTime", "12:00:00.000-05:00"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("isoTime")).isEqualTo("12:00:00.000"); + } + + @Test + public void testBindISODateTime() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("isoDateTime", "2009-10-31T12:00:00.000Z"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("isoDateTime")).isEqualTo("2009-10-31T07:00:00.000-05:00"); + } + + @Test + public void testBindInstantAnnotated() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("instantAnnotated", "2009-10-31T12:00:00.000Z"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("instantAnnotated")).isEqualTo("2009-10-31T07:00:00.000-05:00"); + } + + @Test + public void testBindMutableDateTimeAnnotated() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("mutableDateTimeAnnotated", "2009-10-31T12:00:00.000Z"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("mutableDateTimeAnnotated")).isEqualTo("2009-10-31T07:00:00.000-05:00"); + } + + @Test + public void dateToStringWithFormat() { + JodaTimeFormatterRegistrar registrar = new JodaTimeFormatterRegistrar(); + registrar.setDateTimeFormatter(org.joda.time.format.DateTimeFormat.shortDateTime()); + setup(registrar); + Date date = new Date(); + Object actual = this.conversionService.convert(date, TypeDescriptor.valueOf(Date.class), TypeDescriptor.valueOf(String.class)); + String expected = JodaTimeContextHolder.getFormatter(org.joda.time.format.DateTimeFormat.shortDateTime(), Locale.US).print(new DateTime(date)); + assertThat(actual).isEqualTo(expected); + } + + @Test // SPR-10105 + @SuppressWarnings("deprecation") + public void stringToDateWithoutGlobalFormat() { + String string = "Sat, 12 Aug 1995 13:30:00 GM"; + Date date = this.conversionService.convert(string, Date.class); + assertThat(date).isEqualTo(new Date(string)); + } + + @Test // SPR-10105 + public void stringToDateWithGlobalFormat() { + JodaTimeFormatterRegistrar registrar = new JodaTimeFormatterRegistrar(); + DateTimeFormatterFactory factory = new DateTimeFormatterFactory(); + factory.setIso(ISO.DATE_TIME); + registrar.setDateTimeFormatter(factory.createDateTimeFormatter()); + setup(registrar); + // This is a format that cannot be parsed by new Date(String) + String string = "2009-10-31T07:00:00.000-05:00"; + Date date = this.conversionService.convert(string, Date.class); + assertThat(date).isNotNull(); + } + + @Test + public void testBindPeriod() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("period", "P6Y3M1D"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("period").toString().equals("P6Y3M1D")).isTrue(); + } + + @Test + public void testBindDuration() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("duration", "PT72.345S"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("duration").toString().equals("PT72.345S")).isTrue(); + } + + @Test + public void testBindYearMonth() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("yearMonth", "2007-12"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("yearMonth").toString().equals("2007-12")).isTrue(); + } + + @Test + public void testBindMonthDay() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("monthDay", "--12-03"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("monthDay").toString().equals("--12-03")).isTrue(); + } + + + @SuppressWarnings("unused") + private static class JodaTimeBean { + + private LocalDate localDate; + + @DateTimeFormat(style="M-") + private LocalDate localDateAnnotated; + + private LocalTime localTime; + + @DateTimeFormat(style="-M") + private LocalTime localTimeAnnotated; + + private LocalDateTime localDateTime; + + @DateTimeFormat(style="MS") + private LocalDateTime localDateTimeAnnotated; + + private DateTime dateTime; + + @DateTimeFormat(style="MS") + private DateTime dateTimeAnnotated; + + @DateTimeFormat + private Date date; + + @DateTimeFormat(style="S-") + private Date dateAnnotated; + + @DateTimeFormat(style="S-") + private Calendar calendarAnnotated; + + private Long millis; + + @DateTimeFormat + private DateTime dateTimeAnnotatedDefault; + + private Long millisAnnotated; + + @DateTimeFormat(pattern="M/d/yy h:mm a") + private DateTime dateTimeAnnotatedPattern; + + @DateTimeFormat(iso=ISO.DATE) + private LocalDate isoDate; + + @DateTimeFormat(iso=ISO.TIME) + private LocalTime isoTime; + + @DateTimeFormat(iso=ISO.DATE_TIME) + private DateTime isoDateTime; + + @DateTimeFormat(iso=ISO.DATE_TIME) + private Instant instantAnnotated; + + @DateTimeFormat(iso=ISO.DATE_TIME) + private Instant mutableDateTimeAnnotated; + + private Period period; + + private Duration duration; + + private YearMonth yearMonth; + + private MonthDay monthDay; + + private final List children = new ArrayList<>(); + + public LocalDate getLocalDate() { + return localDate; + } + + public void setLocalDate(LocalDate localDate) { + this.localDate = localDate; + } + + public LocalDate getLocalDateAnnotated() { + return localDateAnnotated; + } + + public void setLocalDateAnnotated(LocalDate localDateAnnotated) { + this.localDateAnnotated = localDateAnnotated; + } + + public LocalTime getLocalTime() { + return localTime; + } + + public void setLocalTime(LocalTime localTime) { + this.localTime = localTime; + } + + public LocalTime getLocalTimeAnnotated() { + return localTimeAnnotated; + } + + public void setLocalTimeAnnotated(LocalTime localTimeAnnotated) { + this.localTimeAnnotated = localTimeAnnotated; + } + + public LocalDateTime getLocalDateTime() { + return localDateTime; + } + + public void setLocalDateTime(LocalDateTime localDateTime) { + this.localDateTime = localDateTime; + } + + public LocalDateTime getLocalDateTimeAnnotated() { + return localDateTimeAnnotated; + } + + public void setLocalDateTimeAnnotated(LocalDateTime localDateTimeAnnotated) { + this.localDateTimeAnnotated = localDateTimeAnnotated; + } + + public DateTime getDateTime() { + return dateTime; + } + + public void setDateTime(DateTime dateTime) { + this.dateTime = dateTime; + } + + public DateTime getDateTimeAnnotated() { + return dateTimeAnnotated; + } + + public void setDateTimeAnnotated(DateTime dateTimeAnnotated) { + this.dateTimeAnnotated = dateTimeAnnotated; + } + + public DateTime getDateTimeAnnotatedPattern() { + return dateTimeAnnotatedPattern; + } + + public void setDateTimeAnnotatedPattern(DateTime dateTimeAnnotatedPattern) { + this.dateTimeAnnotatedPattern = dateTimeAnnotatedPattern; + } + + public DateTime getDateTimeAnnotatedDefault() { + return dateTimeAnnotatedDefault; + } + + public void setDateTimeAnnotatedDefault(DateTime dateTimeAnnotatedDefault) { + this.dateTimeAnnotatedDefault = dateTimeAnnotatedDefault; + } + + public Date getDate() { + return date; + } + + public void setDate(Date date) { + this.date = date; + } + + public Date getDateAnnotated() { + return dateAnnotated; + } + + public void setDateAnnotated(Date dateAnnotated) { + this.dateAnnotated = dateAnnotated; + } + + public Calendar getCalendarAnnotated() { + return calendarAnnotated; + } + + public void setCalendarAnnotated(Calendar calendarAnnotated) { + this.calendarAnnotated = calendarAnnotated; + } + + public Long getMillis() { + return millis; + } + + public void setMillis(Long millis) { + this.millis = millis; + } + + @DateTimeFormat(style="S-") + public Long getMillisAnnotated() { + return millisAnnotated; + } + + public void setMillisAnnotated(@DateTimeFormat(style="S-") Long millisAnnotated) { + this.millisAnnotated = millisAnnotated; + } + + public LocalDate getIsoDate() { + return isoDate; + } + + public void setIsoDate(LocalDate isoDate) { + this.isoDate = isoDate; + } + + public LocalTime getIsoTime() { + return isoTime; + } + + public void setIsoTime(LocalTime isoTime) { + this.isoTime = isoTime; + } + + public DateTime getIsoDateTime() { + return isoDateTime; + } + + public void setIsoDateTime(DateTime isoDateTime) { + this.isoDateTime = isoDateTime; + } + + public Instant getInstantAnnotated() { + return instantAnnotated; + } + + public void setInstantAnnotated(Instant instantAnnotated) { + this.instantAnnotated = instantAnnotated; + } + + public Instant getMutableDateTimeAnnotated() { + return mutableDateTimeAnnotated; + } + + public void setMutableDateTimeAnnotated(Instant mutableDateTimeAnnotated) { + this.mutableDateTimeAnnotated = mutableDateTimeAnnotated; + } + + public Period getPeriod() { + return period; + } + + public void setPeriod(Period period) { + this.period = period; + } + + public Duration getDuration() { + return duration; + } + + public void setDuration(Duration duration) { + this.duration = duration; + } + + public YearMonth getYearMonth() { + return yearMonth; + } + + public void setYearMonth(YearMonth yearMonth) { + this.yearMonth = yearMonth; + } + + public MonthDay getMonthDay() { + return monthDay; + } + + public void setMonthDay(MonthDay monthDay) { + this.monthDay = monthDay; + } + + public List getChildren() { + return children; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryBeanTests.java new file mode 100644 index 0000000..eb3b3ea --- /dev/null +++ b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryBeanTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.standard; + +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + + + + + +/** + * @author Phillip Webb + * @author Sam Brannen + */ +public class DateTimeFormatterFactoryBeanTests { + + private final DateTimeFormatterFactoryBean factory = new DateTimeFormatterFactoryBean(); + + + @Test + public void isSingleton() { + assertThat(factory.isSingleton()).isTrue(); + } + + @Test + public void getObjectType() { + assertThat(factory.getObjectType()).isEqualTo(DateTimeFormatter.class); + } + + @Test + public void getObject() { + factory.afterPropertiesSet(); + assertThat(factory.getObject().toString()).isEqualTo(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).toString()); + } + + @Test + public void getObjectIsAlwaysSingleton() { + factory.afterPropertiesSet(); + DateTimeFormatter formatter = factory.getObject(); + assertThat(formatter.toString()).isEqualTo(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).toString()); + factory.setStylePattern("LL"); + assertThat(factory.getObject()).isSameAs(formatter); + } + +} diff --git a/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryTests.java b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryTests.java new file mode 100644 index 0000000..3ae8303 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryTests.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.standard; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.Locale; +import java.util.TimeZone; + +import org.junit.jupiter.api.Test; + +import org.springframework.format.annotation.DateTimeFormat.ISO; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Phillip Webb + * @author Sam Brannen + */ +public class DateTimeFormatterFactoryTests { + + // Potential test timezone, both have daylight savings on October 21st + private static final TimeZone ZURICH = TimeZone.getTimeZone("Europe/Zurich"); + private static final TimeZone NEW_YORK = TimeZone.getTimeZone("America/New_York"); + + // Ensure that we are testing against a timezone other than the default. + private static final TimeZone TEST_TIMEZONE = ZURICH.equals(TimeZone.getDefault()) ? NEW_YORK : ZURICH; + + + private DateTimeFormatterFactory factory = new DateTimeFormatterFactory(); + + private LocalDateTime dateTime = LocalDateTime.of(2009, 10, 21, 12, 10, 00, 00); + + + @Test + public void createDateTimeFormatter() { + assertThat(factory.createDateTimeFormatter().toString()).isEqualTo(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).toString()); + } + + @Test + public void createDateTimeFormatterWithPattern() { + factory = new DateTimeFormatterFactory("yyyyMMddHHmmss"); + DateTimeFormatter formatter = factory.createDateTimeFormatter(); + assertThat(formatter.format(dateTime)).isEqualTo("20091021121000"); + } + + @Test + public void createDateTimeFormatterWithNullFallback() { + DateTimeFormatter formatter = factory.createDateTimeFormatter(null); + assertThat(formatter).isNull(); + } + + @Test + public void createDateTimeFormatterWithFallback() { + DateTimeFormatter fallback = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG); + DateTimeFormatter formatter = factory.createDateTimeFormatter(fallback); + assertThat(formatter).isSameAs(fallback); + } + + @Test + public void createDateTimeFormatterInOrderOfPropertyPriority() { + factory.setStylePattern("SS"); + String value = applyLocale(factory.createDateTimeFormatter()).format(dateTime); + assertThat(value.startsWith("10/21/09")).isTrue(); + assertThat(value.endsWith("12:10 PM")).isTrue(); + + factory.setIso(ISO.DATE); + assertThat(applyLocale(factory.createDateTimeFormatter()).format(dateTime)).isEqualTo("2009-10-21"); + + factory.setPattern("yyyyMMddHHmmss"); + assertThat(factory.createDateTimeFormatter().format(dateTime)).isEqualTo("20091021121000"); + } + + @Test + public void createDateTimeFormatterWithTimeZone() { + factory.setPattern("yyyyMMddHHmmss Z"); + factory.setTimeZone(TEST_TIMEZONE); + ZoneId dateTimeZone = TEST_TIMEZONE.toZoneId(); + ZonedDateTime dateTime = ZonedDateTime.of(2009, 10, 21, 12, 10, 00, 00, dateTimeZone); + String offset = (TEST_TIMEZONE.equals(NEW_YORK) ? "-0400" : "+0200"); + assertThat(factory.createDateTimeFormatter().format(dateTime)).isEqualTo("20091021121000 " + offset); + } + + private DateTimeFormatter applyLocale(DateTimeFormatter dateTimeFormatter) { + return dateTimeFormatter.withLocale(Locale.US); + } + +} diff --git a/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java new file mode 100644 index 0000000..23f640c --- /dev/null +++ b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java @@ -0,0 +1,627 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.standard; + +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Month; +import java.time.MonthDay; +import java.time.Period; +import java.time.Year; +import java.time.YearMonth; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.ArrayList; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.format.annotation.DateTimeFormat.ISO; +import org.springframework.format.support.FormattingConversionService; +import org.springframework.validation.DataBinder; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Keith Donald + * @author Juergen Hoeller + * @author Phillip Webb + */ +public class DateTimeFormattingTests { + + private FormattingConversionService conversionService; + + private DataBinder binder; + + + @BeforeEach + public void setup() { + DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); + setup(registrar); + } + + private void setup(DateTimeFormatterRegistrar registrar) { + conversionService = new FormattingConversionService(); + DefaultConversionService.addDefaultConverters(conversionService); + registrar.registerFormatters(conversionService); + + DateTimeBean bean = new DateTimeBean(); + bean.getChildren().add(new DateTimeBean()); + binder = new DataBinder(bean); + binder.setConversionService(conversionService); + + LocaleContextHolder.setLocale(Locale.US); + DateTimeContext context = new DateTimeContext(); + context.setTimeZone(ZoneId.of("-05:00")); + DateTimeContextHolder.setDateTimeContext(context); + } + + @AfterEach + public void cleanup() { + LocaleContextHolder.setLocale(null); + DateTimeContextHolder.setDateTimeContext(null); + } + + + @Test + public void testBindLocalDate() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDate", "10/31/09"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("localDate")).isEqualTo("10/31/09"); + } + + @Test + public void testBindLocalDateWithSpecificStyle() { + DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); + registrar.setDateStyle(FormatStyle.LONG); + setup(registrar); + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDate", "October 31, 2009"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("localDate")).isEqualTo("October 31, 2009"); + } + + @Test + public void testBindLocalDateWithSpecificFormatter() { + DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); + registrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyyMMdd")); + setup(registrar); + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDate", "20091031"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("localDate")).isEqualTo("20091031"); + } + + @Test + public void testBindLocalDateArray() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDate", new String[] {"10/31/09"}); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + } + + @Test + public void testBindLocalDateAnnotated() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDateAnnotated", "Oct 31, 2009"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("localDateAnnotated")).isEqualTo("Oct 31, 2009"); + } + + @Test + public void testBindLocalDateAnnotatedWithError() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDateAnnotated", "Oct -31, 2009"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getFieldErrorCount("localDateAnnotated")).isEqualTo(1); + assertThat(binder.getBindingResult().getFieldValue("localDateAnnotated")).isEqualTo("Oct -31, 2009"); + } + + @Test + public void testBindNestedLocalDateAnnotated() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("children[0].localDateAnnotated", "Oct 31, 2009"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("children[0].localDateAnnotated")).isEqualTo("Oct 31, 2009"); + } + + @Test + public void testBindLocalDateAnnotatedWithDirectFieldAccess() { + binder.initDirectFieldAccess(); + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDateAnnotated", "Oct 31, 2009"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("localDateAnnotated")).isEqualTo("Oct 31, 2009"); + } + + @Test + public void testBindLocalDateAnnotatedWithDirectFieldAccessAndError() { + binder.initDirectFieldAccess(); + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDateAnnotated", "Oct -31, 2009"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getFieldErrorCount("localDateAnnotated")).isEqualTo(1); + assertThat(binder.getBindingResult().getFieldValue("localDateAnnotated")).isEqualTo("Oct -31, 2009"); + } + + @Test + public void testBindLocalDateFromJavaUtilCalendar() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDate", new GregorianCalendar(2009, 9, 31, 0, 0)); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("localDate")).isEqualTo("10/31/09"); + } + + @Test + public void testBindLocalTime() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localTime", "12:00 PM"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("localTime")).isEqualTo("12:00 PM"); + } + + @Test + public void testBindLocalTimeWithSpecificStyle() { + DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); + registrar.setTimeStyle(FormatStyle.MEDIUM); + setup(registrar); + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localTime", "12:00:00 PM"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("localTime")).isEqualTo("12:00:00 PM"); + } + + @Test + public void testBindLocalTimeWithSpecificFormatter() { + DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); + registrar.setTimeFormatter(DateTimeFormatter.ofPattern("HHmmss")); + setup(registrar); + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localTime", "130000"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("localTime")).isEqualTo("130000"); + } + + @Test + public void testBindLocalTimeAnnotated() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localTimeAnnotated", "12:00:00 PM"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("localTimeAnnotated")).isEqualTo("12:00:00 PM"); + } + + @Test + public void testBindLocalTimeFromJavaUtilCalendar() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localTime", new GregorianCalendar(1970, 0, 0, 12, 0)); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("localTime")).isEqualTo("12:00 PM"); + } + + @Test + public void testBindLocalDateTime() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDateTime", LocalDateTime.of(2009, 10, 31, 12, 0)); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + String value = binder.getBindingResult().getFieldValue("localDateTime").toString(); + assertThat(value.startsWith("10/31/09")).isTrue(); + assertThat(value.endsWith("12:00 PM")).isTrue(); + } + + @Test + public void testBindLocalDateTimeAnnotated() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDateTimeAnnotated", LocalDateTime.of(2009, 10, 31, 12, 0)); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + String value = binder.getBindingResult().getFieldValue("localDateTimeAnnotated").toString(); + assertThat(value.startsWith("Oct 31, 2009")).isTrue(); + assertThat(value.endsWith("12:00:00 PM")).isTrue(); + } + + @Test + public void testBindLocalDateTimeFromJavaUtilCalendar() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDateTime", new GregorianCalendar(2009, 9, 31, 12, 0)); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + String value = binder.getBindingResult().getFieldValue("localDateTime").toString(); + assertThat(value.startsWith("10/31/09")).isTrue(); + assertThat(value.endsWith("12:00 PM")).isTrue(); + } + + @Test + public void testBindDateTimeWithSpecificStyle() { + DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); + registrar.setDateTimeStyle(FormatStyle.MEDIUM); + setup(registrar); + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDateTime", LocalDateTime.of(2009, 10, 31, 12, 0)); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + String value = binder.getBindingResult().getFieldValue("localDateTime").toString(); + assertThat(value.startsWith("Oct 31, 2009")).isTrue(); + assertThat(value.endsWith("12:00:00 PM")).isTrue(); + } + + @Test + public void testBindDateTimeAnnotatedPattern() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("dateTimeAnnotatedPattern", "10/31/09 12:00 PM"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("dateTimeAnnotatedPattern")).isEqualTo("10/31/09 12:00 PM"); + } + + @Test + public void testBindDateTimeOverflow() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("dateTimeAnnotatedPattern", "02/29/09 12:00 PM"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(1); + } + + @Test + public void testBindISODate() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("isoDate", "2009-10-31"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("isoDate")).isEqualTo("2009-10-31"); + } + + @Test + public void testBindISOTime() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("isoTime", "12:00:00"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("isoTime")).isEqualTo("12:00:00"); + } + + @Test + public void testBindISOTimeWithZone() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("isoTime", "12:00:00.000-05:00"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("isoTime")).isEqualTo("12:00:00"); + } + + @Test + public void testBindISODateTime() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("isoDateTime", "2009-10-31T12:00:00"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("isoDateTime")).isEqualTo("2009-10-31T12:00:00"); + } + + @Test + public void testBindISODateTimeWithZone() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("isoDateTime", "2009-10-31T12:00:00.000Z"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("isoDateTime")).isEqualTo("2009-10-31T12:00:00"); + } + + @Test + public void testBindInstant() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("instant", "2009-10-31T12:00:00.000Z"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("instant").toString().startsWith("2009-10-31T12:00")).isTrue(); + } + + @Test + @SuppressWarnings("deprecation") + public void testBindInstantFromJavaUtilDate() { + TimeZone defaultZone = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("GMT")); + try { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("instant", new Date(109, 9, 31, 12, 0)); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("instant").toString().startsWith("2009-10-31")).isTrue(); + } + finally { + TimeZone.setDefault(defaultZone); + } + } + + @Test + public void testBindPeriod() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("period", "P6Y3M1D"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("period").toString().equals("P6Y3M1D")).isTrue(); + } + + @Test + public void testBindDuration() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("duration", "PT8H6M12.345S"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("duration").toString().equals("PT8H6M12.345S")).isTrue(); + } + + @Test + public void testBindYear() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("year", "2007"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("year").toString().equals("2007")).isTrue(); + } + + @Test + public void testBindMonth() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("month", "JULY"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("month").toString().equals("JULY")).isTrue(); + } + + @Test + public void testBindMonthInAnyCase() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("month", "July"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("month").toString().equals("JULY")).isTrue(); + } + + @Test + public void testBindYearMonth() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("yearMonth", "2007-12"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("yearMonth").toString().equals("2007-12")).isTrue(); + } + + @Test + public void testBindMonthDay() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("monthDay", "--12-03"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("monthDay").toString().equals("--12-03")).isTrue(); + } + + + public static class DateTimeBean { + + private LocalDate localDate; + + @DateTimeFormat(style = "M-") + private LocalDate localDateAnnotated; + + private LocalTime localTime; + + @DateTimeFormat(style = "-M") + private LocalTime localTimeAnnotated; + + private LocalDateTime localDateTime; + + @DateTimeFormat(style = "MM") + private LocalDateTime localDateTimeAnnotated; + + @DateTimeFormat(pattern = "M/d/yy h:mm a") + private LocalDateTime dateTimeAnnotatedPattern; + + @DateTimeFormat(iso = ISO.DATE) + private LocalDate isoDate; + + @DateTimeFormat(iso = ISO.TIME) + private LocalTime isoTime; + + @DateTimeFormat(iso = ISO.DATE_TIME) + private LocalDateTime isoDateTime; + + private Instant instant; + + private Period period; + + private Duration duration; + + private Year year; + + private Month month; + + private YearMonth yearMonth; + + private MonthDay monthDay; + + private final List children = new ArrayList<>(); + + public LocalDate getLocalDate() { + return localDate; + } + + public void setLocalDate(LocalDate localDate) { + this.localDate = localDate; + } + + public LocalDate getLocalDateAnnotated() { + return localDateAnnotated; + } + + public void setLocalDateAnnotated(LocalDate localDateAnnotated) { + this.localDateAnnotated = localDateAnnotated; + } + + public LocalTime getLocalTime() { + return localTime; + } + + public void setLocalTime(LocalTime localTime) { + this.localTime = localTime; + } + + public LocalTime getLocalTimeAnnotated() { + return localTimeAnnotated; + } + + public void setLocalTimeAnnotated(LocalTime localTimeAnnotated) { + this.localTimeAnnotated = localTimeAnnotated; + } + + public LocalDateTime getLocalDateTime() { + return localDateTime; + } + + public void setLocalDateTime(LocalDateTime localDateTime) { + this.localDateTime = localDateTime; + } + + public LocalDateTime getLocalDateTimeAnnotated() { + return localDateTimeAnnotated; + } + + public void setLocalDateTimeAnnotated(LocalDateTime localDateTimeAnnotated) { + this.localDateTimeAnnotated = localDateTimeAnnotated; + } + + public LocalDateTime getDateTimeAnnotatedPattern() { + return dateTimeAnnotatedPattern; + } + + public void setDateTimeAnnotatedPattern(LocalDateTime dateTimeAnnotatedPattern) { + this.dateTimeAnnotatedPattern = dateTimeAnnotatedPattern; + } + + public LocalDate getIsoDate() { + return isoDate; + } + + public void setIsoDate(LocalDate isoDate) { + this.isoDate = isoDate; + } + + public LocalTime getIsoTime() { + return isoTime; + } + + public void setIsoTime(LocalTime isoTime) { + this.isoTime = isoTime; + } + + public LocalDateTime getIsoDateTime() { + return isoDateTime; + } + + public void setIsoDateTime(LocalDateTime isoDateTime) { + this.isoDateTime = isoDateTime; + } + + public Instant getInstant() { + return instant; + } + + public void setInstant(Instant instant) { + this.instant = instant; + } + + public Period getPeriod() { + return period; + } + + public void setPeriod(Period period) { + this.period = period; + } + + public Duration getDuration() { + return duration; + } + + public void setDuration(Duration duration) { + this.duration = duration; + } + + public Year getYear() { + return year; + } + + public void setYear(Year year) { + this.year = year; + } + + public Month getMonth() { + return month; + } + + public void setMonth(Month month) { + this.month = month; + } + + public YearMonth getYearMonth() { + return yearMonth; + } + + public void setYearMonth(YearMonth yearMonth) { + this.yearMonth = yearMonth; + } + + public MonthDay getMonthDay() { + return monthDay; + } + + public void setMonthDay(MonthDay monthDay) { + this.monthDay = monthDay; + } + + public List getChildren() { + return children; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java b/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java new file mode 100644 index 0000000..16ba2cb --- /dev/null +++ b/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.datetime.standard; + +import java.text.ParseException; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.Random; +import java.util.stream.Stream; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import static java.time.Instant.MAX; +import static java.time.Instant.MIN; +import static java.time.ZoneId.systemDefault; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link InstantFormatter}. + * + * @author Andrei Nevedomskii + * @author Sam Brannen + * @since 5.1.12 + */ +@DisplayName("InstantFormatter unit tests") +@DisplayNameGeneration(ReplaceUnderscores.class) +class InstantFormatterTests { + + private final InstantFormatter instantFormatter = new InstantFormatter(); + + @ParameterizedTest + @ArgumentsSource(ISOSerializedInstantProvider.class) + void should_parse_an_ISO_formatted_string_representation_of_an_Instant(String input) throws ParseException { + Instant expected = DateTimeFormatter.ISO_INSTANT.parse(input, Instant::from); + + Instant actual = instantFormatter.parse(input, null); + + assertThat(actual).isEqualTo(expected); + } + + @ParameterizedTest + @ArgumentsSource(RFC1123SerializedInstantProvider.class) + void should_parse_an_RFC1123_formatted_string_representation_of_an_Instant(String input) throws ParseException { + Instant expected = DateTimeFormatter.RFC_1123_DATE_TIME.parse(input, Instant::from); + + Instant actual = instantFormatter.parse(input, null); + + assertThat(actual).isEqualTo(expected); + } + + @ParameterizedTest + @ArgumentsSource(RandomInstantProvider.class) + void should_serialize_an_Instant_using_ISO_format_and_ignoring_Locale(Instant input) { + String expected = DateTimeFormatter.ISO_INSTANT.format(input); + + String actual = instantFormatter.print(input, null); + + assertThat(actual).isEqualTo(expected); + } + + private static class RandomInstantProvider implements ArgumentsProvider { + + private static final long DATA_SET_SIZE = 10; + + private static final Random random = new Random(); + + @Override + public final Stream provideArguments(ExtensionContext context) { + return provideArguments().map(Arguments::of).limit(DATA_SET_SIZE); + } + + Stream provideArguments() { + return randomInstantStream(MIN, MAX); + } + + Stream randomInstantStream(Instant min, Instant max) { + return Stream.concat(Stream.of(Instant.now()), // make sure that the data set includes current instant + random.longs(min.getEpochSecond(), max.getEpochSecond()).mapToObj(Instant::ofEpochSecond)); + } + } + + private static class ISOSerializedInstantProvider extends RandomInstantProvider { + + @Override + Stream provideArguments() { + return randomInstantStream(MIN, MAX).map(DateTimeFormatter.ISO_INSTANT::format); + } + } + + private static class RFC1123SerializedInstantProvider extends RandomInstantProvider { + + // RFC-1123 supports only 4-digit years + private final Instant min = Instant.parse("0000-01-01T00:00:00.00Z"); + + private final Instant max = Instant.parse("9999-12-31T23:59:59.99Z"); + + @Override + Stream provideArguments() { + return randomInstantStream(min, max) + .map(DateTimeFormatter.RFC_1123_DATE_TIME.withZone(systemDefault())::format); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/format/number/CurrencyStyleFormatterTests.java b/spring-context/src/test/java/org/springframework/format/number/CurrencyStyleFormatterTests.java new file mode 100644 index 0000000..2a8feb8 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/format/number/CurrencyStyleFormatterTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.number; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.ParseException; +import java.util.Locale; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Keith Donald + */ +public class CurrencyStyleFormatterTests { + + private final CurrencyStyleFormatter formatter = new CurrencyStyleFormatter(); + + + @Test + public void formatValue() { + assertThat(formatter.print(new BigDecimal("23"), Locale.US)).isEqualTo("$23.00"); + } + + @Test + public void parseValue() throws ParseException { + assertThat(formatter.parse("$23.56", Locale.US)).isEqualTo(new BigDecimal("23.56")); + } + + @Test + public void parseBogusValue() throws ParseException { + assertThatExceptionOfType(ParseException.class).isThrownBy(() -> + formatter.parse("bogus", Locale.US)); + } + + @Test + public void parseValueDefaultRoundDown() throws ParseException { + this.formatter.setRoundingMode(RoundingMode.DOWN); + assertThat(formatter.parse("$23.567", Locale.US)).isEqualTo(new BigDecimal("23.56")); + } + + @Test + public void parseWholeValue() throws ParseException { + assertThat(formatter.parse("$23", Locale.US)).isEqualTo(new BigDecimal("23.00")); + } + + @Test + public void parseValueNotLenientFailure() throws ParseException { + assertThatExceptionOfType(ParseException.class).isThrownBy(() -> + formatter.parse("$23.56bogus", Locale.US)); + } + +} diff --git a/spring-context/src/test/java/org/springframework/format/number/NumberFormattingTests.java b/spring-context/src/test/java/org/springframework/format/number/NumberFormattingTests.java new file mode 100644 index 0000000..406d654 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/format/number/NumberFormattingTests.java @@ -0,0 +1,270 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.number; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Locale; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.format.annotation.NumberFormat; +import org.springframework.format.annotation.NumberFormat.Style; +import org.springframework.format.support.FormattingConversionService; +import org.springframework.util.StringValueResolver; +import org.springframework.validation.DataBinder; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Keith Donald + * @author Juergen Hoeller + */ +public class NumberFormattingTests { + + private final FormattingConversionService conversionService = new FormattingConversionService(); + + private DataBinder binder; + + + @BeforeEach + public void setUp() { + DefaultConversionService.addDefaultConverters(conversionService); + conversionService.setEmbeddedValueResolver(new StringValueResolver() { + @Override + public String resolveStringValue(String strVal) { + if ("${pattern}".equals(strVal)) { + return "#,##.00"; + } + else { + return strVal; + } + } + }); + conversionService.addFormatterForFieldType(Number.class, new NumberStyleFormatter()); + conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory()); + LocaleContextHolder.setLocale(Locale.US); + binder = new DataBinder(new TestBean()); + binder.setConversionService(conversionService); + } + + @AfterEach + public void tearDown() { + LocaleContextHolder.setLocale(null); + } + + + @Test + public void testDefaultNumberFormatting() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("numberDefault", "3,339.12"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("numberDefault")).isEqualTo("3,339"); + } + + @Test + public void testDefaultNumberFormattingAnnotated() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("numberDefaultAnnotated", "3,339.12"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("numberDefaultAnnotated")).isEqualTo("3,339.12"); + } + + @Test + public void testCurrencyFormatting() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("currency", "$3,339.12"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("currency")).isEqualTo("$3,339.12"); + } + + @Test + public void testPercentFormatting() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("percent", "53%"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("percent")).isEqualTo("53%"); + } + + @Test + public void testPatternFormatting() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("pattern", "1,25.00"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("pattern")).isEqualTo("1,25.00"); + } + + @Test + public void testPatternArrayFormatting() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("patternArray", new String[] { "1,25.00", "2,35.00" }); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("patternArray[0]")).isEqualTo("1,25.00"); + assertThat(binder.getBindingResult().getFieldValue("patternArray[1]")).isEqualTo("2,35.00"); + + propertyValues = new MutablePropertyValues(); + propertyValues.add("patternArray[0]", "1,25.00"); + propertyValues.add("patternArray[1]", "2,35.00"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("patternArray[0]")).isEqualTo("1,25.00"); + assertThat(binder.getBindingResult().getFieldValue("patternArray[1]")).isEqualTo("2,35.00"); + } + + @Test + public void testPatternListFormatting() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("patternList", new String[] { "1,25.00", "2,35.00" }); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("patternList[0]")).isEqualTo("1,25.00"); + assertThat(binder.getBindingResult().getFieldValue("patternList[1]")).isEqualTo("2,35.00"); + + propertyValues = new MutablePropertyValues(); + propertyValues.add("patternList[0]", "1,25.00"); + propertyValues.add("patternList[1]", "2,35.00"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("patternList[0]")).isEqualTo("1,25.00"); + assertThat(binder.getBindingResult().getFieldValue("patternList[1]")).isEqualTo("2,35.00"); + } + + @Test + public void testPatternList2FormattingListElement() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("patternList2[0]", "1,25.00"); + propertyValues.add("patternList2[1]", "2,35.00"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("patternList2[0]")).isEqualTo("1,25.00"); + assertThat(binder.getBindingResult().getFieldValue("patternList2[1]")).isEqualTo("2,35.00"); + } + + @Test + public void testPatternList2FormattingList() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("patternList2[0]", "1,25.00"); + propertyValues.add("patternList2[1]", "2,35.00"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("patternList2")).isEqualTo("1,25.00,2,35.00"); + } + + + @SuppressWarnings("unused") + private static class TestBean { + + private Integer numberDefault; + + @NumberFormat + private Double numberDefaultAnnotated; + + @NumberFormat(style = Style.CURRENCY) + private BigDecimal currency; + + @NumberFormat(style = Style.PERCENT) + private BigDecimal percent; + + @NumberFormat(pattern = "${pattern}") + private BigDecimal pattern; + + @NumberFormat(pattern = "#,##.00") + private BigDecimal[] patternArray; + + @NumberFormat(pattern = "#,##.00") + private List patternList; + + @NumberFormat(pattern = "#,##.00") + private List patternList2; + + public Integer getNumberDefault() { + return numberDefault; + } + + public void setNumberDefault(Integer numberDefault) { + this.numberDefault = numberDefault; + } + + public Double getNumberDefaultAnnotated() { + return numberDefaultAnnotated; + } + + public void setNumberDefaultAnnotated(Double numberDefaultAnnotated) { + this.numberDefaultAnnotated = numberDefaultAnnotated; + } + + public BigDecimal getCurrency() { + return currency; + } + + public void setCurrency(BigDecimal currency) { + this.currency = currency; + } + + public BigDecimal getPercent() { + return percent; + } + + public void setPercent(BigDecimal percent) { + this.percent = percent; + } + + public BigDecimal getPattern() { + return pattern; + } + + public void setPattern(BigDecimal pattern) { + this.pattern = pattern; + } + + public BigDecimal[] getPatternArray() { + return patternArray; + } + + public void setPatternArray(BigDecimal[] patternArray) { + this.patternArray = patternArray; + } + + public List getPatternList() { + return patternList; + } + + public void setPatternList(List patternList) { + this.patternList = patternList; + } + + public List getPatternList2() { + return patternList2; + } + + public void setPatternList2(List patternList2) { + this.patternList2 = patternList2; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/format/number/NumberStyleFormatterTests.java b/spring-context/src/test/java/org/springframework/format/number/NumberStyleFormatterTests.java new file mode 100644 index 0000000..babd8a3 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/format/number/NumberStyleFormatterTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.number; + +import java.math.BigDecimal; +import java.text.ParseException; +import java.util.Locale; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Keith Donald + */ +public class NumberStyleFormatterTests { + + private final NumberStyleFormatter formatter = new NumberStyleFormatter(); + + + @Test + public void formatValue() { + assertThat(formatter.print(new BigDecimal("23.56"), Locale.US)).isEqualTo("23.56"); + } + + @Test + public void parseValue() throws ParseException { + assertThat(formatter.parse("23.56", Locale.US)).isEqualTo(new BigDecimal("23.56")); + } + + @Test + public void parseBogusValue() throws ParseException { + assertThatExceptionOfType(ParseException.class).isThrownBy(() -> + formatter.parse("bogus", Locale.US)); + } + + @Test + public void parsePercentValueNotLenientFailure() throws ParseException { + assertThatExceptionOfType(ParseException.class).isThrownBy(() -> + formatter.parse("23.56bogus", Locale.US)); + } + +} diff --git a/spring-context/src/test/java/org/springframework/format/number/PercentStyleFormatterTests.java b/spring-context/src/test/java/org/springframework/format/number/PercentStyleFormatterTests.java new file mode 100644 index 0000000..de9cbf2 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/format/number/PercentStyleFormatterTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.number; + +import java.math.BigDecimal; +import java.text.ParseException; +import java.util.Locale; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Keith Donald + */ +public class PercentStyleFormatterTests { + + private final PercentStyleFormatter formatter = new PercentStyleFormatter(); + + + @Test + public void formatValue() { + assertThat(formatter.print(new BigDecimal(".23"), Locale.US)).isEqualTo("23%"); + } + + @Test + public void parseValue() throws ParseException { + assertThat(formatter.parse("23.56%", Locale.US)).isEqualTo(new BigDecimal(".2356")); + } + + @Test + public void parseBogusValue() throws ParseException { + assertThatExceptionOfType(ParseException.class).isThrownBy(() -> + formatter.parse("bogus", Locale.US)); + } + + @Test + public void parsePercentValueNotLenientFailure() throws ParseException { + assertThatExceptionOfType(ParseException.class).isThrownBy(() -> + formatter.parse("23.56%bogus", Locale.US)); + } + +} diff --git a/spring-context/src/test/java/org/springframework/format/number/money/MoneyFormattingTests.java b/spring-context/src/test/java/org/springframework/format/number/money/MoneyFormattingTests.java new file mode 100644 index 0000000..40f250f --- /dev/null +++ b/spring-context/src/test/java/org/springframework/format/number/money/MoneyFormattingTests.java @@ -0,0 +1,273 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.number.money; + +import java.util.Locale; + +import javax.money.CurrencyUnit; +import javax.money.MonetaryAmount; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.format.annotation.NumberFormat; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.format.support.FormattingConversionService; +import org.springframework.validation.DataBinder; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @since 4.2 + */ +public class MoneyFormattingTests { + + private final FormattingConversionService conversionService = new DefaultFormattingConversionService(); + + + @BeforeEach + public void setUp() { + LocaleContextHolder.setLocale(Locale.US); + } + + @AfterEach + public void tearDown() { + LocaleContextHolder.setLocale(null); + } + + + @Test + public void testAmountAndUnit() { + MoneyHolder bean = new MoneyHolder(); + DataBinder binder = new DataBinder(bean); + binder.setConversionService(conversionService); + + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("amount", "USD 10.50"); + propertyValues.add("unit", "USD"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("amount")).isEqualTo("USD10.50"); + assertThat(binder.getBindingResult().getFieldValue("unit")).isEqualTo("USD"); + assertThat(bean.getAmount().getNumber().doubleValue() == 10.5d).isTrue(); + assertThat(bean.getAmount().getCurrency().getCurrencyCode()).isEqualTo("USD"); + + LocaleContextHolder.setLocale(Locale.CANADA); + binder.bind(propertyValues); + LocaleContextHolder.setLocale(Locale.US); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("amount")).isEqualTo("USD10.50"); + assertThat(binder.getBindingResult().getFieldValue("unit")).isEqualTo("USD"); + assertThat(bean.getAmount().getNumber().doubleValue() == 10.5d).isTrue(); + assertThat(bean.getAmount().getCurrency().getCurrencyCode()).isEqualTo("USD"); + } + + @Test + public void testAmountWithNumberFormat1() { + FormattedMoneyHolder1 bean = new FormattedMoneyHolder1(); + DataBinder binder = new DataBinder(bean); + binder.setConversionService(conversionService); + + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("amount", "$10.50"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("amount")).isEqualTo("$10.50"); + assertThat(bean.getAmount().getNumber().doubleValue() == 10.5d).isTrue(); + assertThat(bean.getAmount().getCurrency().getCurrencyCode()).isEqualTo("USD"); + + LocaleContextHolder.setLocale(Locale.CANADA); + binder.bind(propertyValues); + LocaleContextHolder.setLocale(Locale.US); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("amount")).isEqualTo("$10.50"); + assertThat(bean.getAmount().getNumber().doubleValue() == 10.5d).isTrue(); + assertThat(bean.getAmount().getCurrency().getCurrencyCode()).isEqualTo("CAD"); + } + + @Test + public void testAmountWithNumberFormat2() { + FormattedMoneyHolder2 bean = new FormattedMoneyHolder2(); + DataBinder binder = new DataBinder(bean); + binder.setConversionService(conversionService); + + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("amount", "10.50"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("amount")).isEqualTo("10.5"); + assertThat(bean.getAmount().getNumber().doubleValue() == 10.5d).isTrue(); + assertThat(bean.getAmount().getCurrency().getCurrencyCode()).isEqualTo("USD"); + } + + @Test + public void testAmountWithNumberFormat3() { + FormattedMoneyHolder3 bean = new FormattedMoneyHolder3(); + DataBinder binder = new DataBinder(bean); + binder.setConversionService(conversionService); + + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("amount", "10%"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("amount")).isEqualTo("10%"); + assertThat(bean.getAmount().getNumber().doubleValue() == 0.1d).isTrue(); + assertThat(bean.getAmount().getCurrency().getCurrencyCode()).isEqualTo("USD"); + } + + @Test + public void testAmountWithNumberFormat4() { + FormattedMoneyHolder4 bean = new FormattedMoneyHolder4(); + DataBinder binder = new DataBinder(bean); + binder.setConversionService(conversionService); + + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("amount", "010.500"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("amount")).isEqualTo("010.500"); + assertThat(bean.getAmount().getNumber().doubleValue() == 10.5d).isTrue(); + assertThat(bean.getAmount().getCurrency().getCurrencyCode()).isEqualTo("USD"); + } + + @Test + public void testAmountWithNumberFormat5() { + FormattedMoneyHolder5 bean = new FormattedMoneyHolder5(); + DataBinder binder = new DataBinder(bean); + binder.setConversionService(conversionService); + + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("amount", "USD 10.50"); + binder.bind(propertyValues); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("amount")).isEqualTo("USD 010.500"); + assertThat(bean.getAmount().getNumber().doubleValue() == 10.5d).isTrue(); + assertThat(bean.getAmount().getCurrency().getCurrencyCode()).isEqualTo("USD"); + + LocaleContextHolder.setLocale(Locale.CANADA); + binder.bind(propertyValues); + LocaleContextHolder.setLocale(Locale.US); + assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); + assertThat(binder.getBindingResult().getFieldValue("amount")).isEqualTo("USD 010.500"); + assertThat(bean.getAmount().getNumber().doubleValue() == 10.5d).isTrue(); + assertThat(bean.getAmount().getCurrency().getCurrencyCode()).isEqualTo("USD"); + } + + + public static class MoneyHolder { + + private MonetaryAmount amount; + + private CurrencyUnit unit; + + public MonetaryAmount getAmount() { + return amount; + } + + public void setAmount(MonetaryAmount amount) { + this.amount = amount; + } + + public CurrencyUnit getUnit() { + return unit; + } + + public void setUnit(CurrencyUnit unit) { + this.unit = unit; + } + } + + + public static class FormattedMoneyHolder1 { + + @NumberFormat + private MonetaryAmount amount; + + public MonetaryAmount getAmount() { + return amount; + } + + public void setAmount(MonetaryAmount amount) { + this.amount = amount; + } + } + + + public static class FormattedMoneyHolder2 { + + @NumberFormat(style = NumberFormat.Style.NUMBER) + private MonetaryAmount amount; + + public MonetaryAmount getAmount() { + return amount; + } + + public void setAmount(MonetaryAmount amount) { + this.amount = amount; + } + } + + + public static class FormattedMoneyHolder3 { + + @NumberFormat(style = NumberFormat.Style.PERCENT) + private MonetaryAmount amount; + + public MonetaryAmount getAmount() { + return amount; + } + + public void setAmount(MonetaryAmount amount) { + this.amount = amount; + } + } + + + public static class FormattedMoneyHolder4 { + + @NumberFormat(pattern = "#000.000#") + private MonetaryAmount amount; + + public MonetaryAmount getAmount() { + return amount; + } + + public void setAmount(MonetaryAmount amount) { + this.amount = amount; + } + } + + + public static class FormattedMoneyHolder5 { + + @NumberFormat(pattern = "\u00A4\u00A4 #000.000#") + private MonetaryAmount amount; + + public MonetaryAmount getAmount() { + return amount; + } + + public void setAmount(MonetaryAmount amount) { + this.amount = amount; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceFactoryBeanTests.java new file mode 100644 index 0000000..8b8aacf --- /dev/null +++ b/spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceFactoryBeanTests.java @@ -0,0 +1,222 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.support; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.text.ParseException; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.core.annotation.AliasFor; +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.format.AnnotationFormatterFactory; +import org.springframework.format.Formatter; +import org.springframework.format.FormatterRegistrar; +import org.springframework.format.FormatterRegistry; +import org.springframework.format.Parser; +import org.springframework.format.Printer; +import org.springframework.format.annotation.NumberFormat; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Rossen Stoyanchev + * @author Juergen Hoeller + */ +public class FormattingConversionServiceFactoryBeanTests { + + @Test + public void testDefaultFormattersOn() throws Exception { + FormattingConversionServiceFactoryBean factory = new FormattingConversionServiceFactoryBean(); + factory.afterPropertiesSet(); + FormattingConversionService fcs = factory.getObject(); + TypeDescriptor descriptor = new TypeDescriptor(TestBean.class.getDeclaredField("pattern")); + + LocaleContextHolder.setLocale(Locale.GERMAN); + try { + Object value = fcs.convert("15,00", TypeDescriptor.valueOf(String.class), descriptor); + assertThat(value).isEqualTo(15.0); + value = fcs.convert(15.0, descriptor, TypeDescriptor.valueOf(String.class)); + assertThat(value).isEqualTo("15"); + } + finally { + LocaleContextHolder.resetLocaleContext(); + } + } + + @Test + public void testDefaultFormattersOff() throws Exception { + FormattingConversionServiceFactoryBean factory = new FormattingConversionServiceFactoryBean(); + factory.setRegisterDefaultFormatters(false); + factory.afterPropertiesSet(); + FormattingConversionService fcs = factory.getObject(); + TypeDescriptor descriptor = new TypeDescriptor(TestBean.class.getDeclaredField("pattern")); + + assertThatExceptionOfType(ConversionFailedException.class).isThrownBy(() -> + fcs.convert("15,00", TypeDescriptor.valueOf(String.class), descriptor)) + .withCauseInstanceOf(NumberFormatException.class); + } + + @Test + public void testCustomFormatter() throws Exception { + FormattingConversionServiceFactoryBean factory = new FormattingConversionServiceFactoryBean(); + Set formatters = new HashSet<>(); + formatters.add(new TestBeanFormatter()); + formatters.add(new SpecialIntAnnotationFormatterFactory()); + factory.setFormatters(formatters); + factory.afterPropertiesSet(); + FormattingConversionService fcs = factory.getObject(); + + TestBean testBean = fcs.convert("5", TestBean.class); + assertThat(testBean.getSpecialInt()).isEqualTo(5); + assertThat(fcs.convert(testBean, String.class)).isEqualTo("5"); + + TypeDescriptor descriptor = new TypeDescriptor(TestBean.class.getDeclaredField("specialInt")); + Object value = fcs.convert(":5", TypeDescriptor.valueOf(String.class), descriptor); + assertThat(value).isEqualTo(5); + value = fcs.convert(5, descriptor, TypeDescriptor.valueOf(String.class)); + assertThat(value).isEqualTo(":5"); + } + + @Test + public void testFormatterRegistrar() throws Exception { + FormattingConversionServiceFactoryBean factory = new FormattingConversionServiceFactoryBean(); + Set registrars = new HashSet<>(); + registrars.add(new TestFormatterRegistrar()); + factory.setFormatterRegistrars(registrars); + factory.afterPropertiesSet(); + FormattingConversionService fcs = factory.getObject(); + + TestBean testBean = fcs.convert("5", TestBean.class); + assertThat(testBean.getSpecialInt()).isEqualTo(5); + assertThat(fcs.convert(testBean, String.class)).isEqualTo("5"); + } + + @Test + public void testInvalidFormatter() throws Exception { + FormattingConversionServiceFactoryBean factory = new FormattingConversionServiceFactoryBean(); + Set formatters = new HashSet<>(); + formatters.add(new Object()); + factory.setFormatters(formatters); + assertThatIllegalArgumentException().isThrownBy(factory::afterPropertiesSet); + } + + + @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) + @Retention(RetentionPolicy.RUNTIME) + private @interface SpecialInt { + + @AliasFor("alias") + String value() default ""; + + @AliasFor("value") + String alias() default ""; + } + + + private static class TestBean { + + @NumberFormat(pattern = "##,00") + private double pattern; + + @SpecialInt("aliased") + private int specialInt; + + public int getSpecialInt() { + return specialInt; + } + + public void setSpecialInt(int field) { + this.specialInt = field; + } + } + + + private static class TestBeanFormatter implements Formatter { + + @Override + public String print(TestBean object, Locale locale) { + return String.valueOf(object.getSpecialInt()); + } + + @Override + public TestBean parse(String text, Locale locale) throws ParseException { + TestBean object = new TestBean(); + object.setSpecialInt(Integer.parseInt(text)); + return object; + } + } + + + private static class SpecialIntAnnotationFormatterFactory implements AnnotationFormatterFactory { + + private final Set> fieldTypes = new HashSet<>(1); + + public SpecialIntAnnotationFormatterFactory() { + fieldTypes.add(Integer.class); + } + + @Override + public Set> getFieldTypes() { + return fieldTypes; + } + + @Override + public Printer getPrinter(SpecialInt annotation, Class fieldType) { + assertThat(annotation.value()).isEqualTo("aliased"); + assertThat(annotation.alias()).isEqualTo("aliased"); + return new Printer() { + @Override + public String print(Integer object, Locale locale) { + return ":" + object.toString(); + } + }; + } + + @Override + public Parser getParser(SpecialInt annotation, Class fieldType) { + assertThat(annotation.value()).isEqualTo("aliased"); + assertThat(annotation.alias()).isEqualTo("aliased"); + return new Parser() { + @Override + public Integer parse(String text, Locale locale) throws ParseException { + return Integer.parseInt(text.substring(1)); + } + }; + } + } + + + private static class TestFormatterRegistrar implements FormatterRegistrar { + + @Override + public void registerFormatters(FormatterRegistry registry) { + registry.addFormatter(new TestBeanFormatter()); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceTests.java b/spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceTests.java new file mode 100644 index 0000000..eeb6895 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/format/support/FormattingConversionServiceTests.java @@ -0,0 +1,526 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.format.support; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Properties; + +import org.joda.time.DateTime; +import org.joda.time.LocalDate; +import org.joda.time.format.DateTimeFormat; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.ConfigurablePropertyAccessor; +import org.springframework.beans.PropertyAccessorFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.format.Formatter; +import org.springframework.format.Printer; +import org.springframework.format.annotation.NumberFormat; +import org.springframework.format.datetime.joda.DateTimeParser; +import org.springframework.format.datetime.joda.JodaDateTimeFormatAnnotationFormatterFactory; +import org.springframework.format.datetime.joda.ReadablePartialPrinter; +import org.springframework.format.number.NumberStyleFormatter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Keith Donald + * @author Juergen Hoeller + * @author Kazuki Shimizu + * @author Sam Brannen + */ +public class FormattingConversionServiceTests { + + private FormattingConversionService formattingService; + + + @BeforeEach + public void setUp() { + formattingService = new FormattingConversionService(); + DefaultConversionService.addDefaultConverters(formattingService); + LocaleContextHolder.setLocale(Locale.US); + } + + @AfterEach + public void tearDown() { + LocaleContextHolder.setLocale(null); + } + + + @Test + public void formatFieldForTypeWithFormatter() { + formattingService.addFormatterForFieldType(Number.class, new NumberStyleFormatter()); + String formatted = formattingService.convert(3, String.class); + assertThat(formatted).isEqualTo("3"); + Integer i = formattingService.convert("3", Integer.class); + assertThat(i).isEqualTo(3); + } + + @Test + public void formatFieldForTypeWithPrinterParserWithCoercion() { + formattingService.addConverter(new Converter() { + @Override + public LocalDate convert(DateTime source) { + return source.toLocalDate(); + } + }); + formattingService.addFormatterForFieldType(LocalDate.class, new ReadablePartialPrinter(DateTimeFormat + .shortDate()), new DateTimeParser(DateTimeFormat.shortDate())); + String formatted = formattingService.convert(new LocalDate(2009, 10, 31), String.class); + assertThat(formatted).isEqualTo("10/31/09"); + LocalDate date = formattingService.convert("10/31/09", LocalDate.class); + assertThat(date).isEqualTo(new LocalDate(2009, 10, 31)); + } + + @Test + @SuppressWarnings("resource") + public void formatFieldForValueInjection() { + AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(); + ac.registerBeanDefinition("valueBean", new RootBeanDefinition(ValueBean.class)); + ac.registerBeanDefinition("conversionService", new RootBeanDefinition(FormattingConversionServiceFactoryBean.class)); + ac.refresh(); + ValueBean valueBean = ac.getBean(ValueBean.class); + assertThat(new LocalDate(valueBean.date)).isEqualTo(new LocalDate(2009, 10, 31)); + } + + @Test + @SuppressWarnings("resource") + public void formatFieldForValueInjectionUsingMetaAnnotations() { + AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(); + RootBeanDefinition bd = new RootBeanDefinition(MetaValueBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + ac.registerBeanDefinition("valueBean", bd); + ac.registerBeanDefinition("conversionService", new RootBeanDefinition(FormattingConversionServiceFactoryBean.class)); + ac.registerBeanDefinition("ppc", new RootBeanDefinition(PropertyPlaceholderConfigurer.class)); + ac.refresh(); + System.setProperty("myDate", "10-31-09"); + System.setProperty("myNumber", "99.99%"); + try { + MetaValueBean valueBean = ac.getBean(MetaValueBean.class); + assertThat(new LocalDate(valueBean.date)).isEqualTo(new LocalDate(2009, 10, 31)); + assertThat(valueBean.number).isEqualTo(Double.valueOf(0.9999)); + } + finally { + System.clearProperty("myDate"); + System.clearProperty("myNumber"); + } + } + + @Test + public void formatFieldForAnnotation() throws Exception { + formattingService.addFormatterForFieldAnnotation(new JodaDateTimeFormatAnnotationFormatterFactory()); + doTestFormatFieldForAnnotation(Model.class, false); + } + + @Test + public void formatFieldForAnnotationWithDirectFieldAccess() throws Exception { + formattingService.addFormatterForFieldAnnotation(new JodaDateTimeFormatAnnotationFormatterFactory()); + doTestFormatFieldForAnnotation(Model.class, true); + } + + @Test + @SuppressWarnings("resource") + public void formatFieldForAnnotationWithPlaceholders() throws Exception { + GenericApplicationContext context = new GenericApplicationContext(); + PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); + Properties props = new Properties(); + props.setProperty("dateStyle", "S-"); + props.setProperty("datePattern", "M-d-yy"); + ppc.setProperties(props); + context.getBeanFactory().registerSingleton("ppc", ppc); + context.refresh(); + context.getBeanFactory().initializeBean(formattingService, "formattingService"); + formattingService.addFormatterForFieldAnnotation(new JodaDateTimeFormatAnnotationFormatterFactory()); + doTestFormatFieldForAnnotation(ModelWithPlaceholders.class, false); + } + + @Test + @SuppressWarnings("resource") + public void formatFieldForAnnotationWithPlaceholdersAndFactoryBean() throws Exception { + GenericApplicationContext context = new GenericApplicationContext(); + PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); + Properties props = new Properties(); + props.setProperty("dateStyle", "S-"); + props.setProperty("datePattern", "M-d-yy"); + ppc.setProperties(props); + context.registerBeanDefinition("formattingService", new RootBeanDefinition(FormattingConversionServiceFactoryBean.class)); + context.getBeanFactory().registerSingleton("ppc", ppc); + context.refresh(); + formattingService = context.getBean("formattingService", FormattingConversionService.class); + doTestFormatFieldForAnnotation(ModelWithPlaceholders.class, false); + } + + @SuppressWarnings("unchecked") + private void doTestFormatFieldForAnnotation(Class modelClass, boolean directFieldAccess) throws Exception { + formattingService.addConverter(new Converter() { + @Override + public Long convert(Date source) { + return source.getTime(); + } + }); + formattingService.addConverter(new Converter() { + @Override + public Date convert(DateTime source) { + return source.toDate(); + } + }); + + String formatted = (String) formattingService.convert(new LocalDate(2009, 10, 31).toDateTimeAtCurrentTime() + .toDate(), new TypeDescriptor(modelClass.getField("date")), TypeDescriptor.valueOf(String.class)); + assertThat(formatted).isEqualTo("10/31/09"); + LocalDate date = new LocalDate(formattingService.convert("10/31/09", TypeDescriptor.valueOf(String.class), + new TypeDescriptor(modelClass.getField("date")))); + assertThat(date).isEqualTo(new LocalDate(2009, 10, 31)); + + List dates = new ArrayList<>(); + dates.add(new LocalDate(2009, 10, 31).toDateTimeAtCurrentTime().toDate()); + dates.add(new LocalDate(2009, 11, 1).toDateTimeAtCurrentTime().toDate()); + dates.add(new LocalDate(2009, 11, 2).toDateTimeAtCurrentTime().toDate()); + formatted = (String) formattingService.convert(dates, + new TypeDescriptor(modelClass.getField("dates")), TypeDescriptor.valueOf(String.class)); + assertThat(formatted).isEqualTo("10-31-09,11-1-09,11-2-09"); + dates = (List) formattingService.convert("10-31-09,11-1-09,11-2-09", + TypeDescriptor.valueOf(String.class), new TypeDescriptor(modelClass.getField("dates"))); + assertThat(new LocalDate(dates.get(0))).isEqualTo(new LocalDate(2009, 10, 31)); + assertThat(new LocalDate(dates.get(1))).isEqualTo(new LocalDate(2009, 11, 1)); + assertThat(new LocalDate(dates.get(2))).isEqualTo(new LocalDate(2009, 11, 2)); + + Object model = modelClass.newInstance(); + ConfigurablePropertyAccessor accessor = directFieldAccess ? PropertyAccessorFactory.forDirectFieldAccess(model) : + PropertyAccessorFactory.forBeanPropertyAccess(model); + accessor.setConversionService(formattingService); + accessor.setPropertyValue("dates", "10-31-09,11-1-09,11-2-09"); + dates = (List) accessor.getPropertyValue("dates"); + assertThat(new LocalDate(dates.get(0))).isEqualTo(new LocalDate(2009, 10, 31)); + assertThat(new LocalDate(dates.get(1))).isEqualTo(new LocalDate(2009, 11, 1)); + assertThat(new LocalDate(dates.get(2))).isEqualTo(new LocalDate(2009, 11, 2)); + if (!directFieldAccess) { + accessor.setPropertyValue("dates[0]", "10-30-09"); + accessor.setPropertyValue("dates[1]", "10-1-09"); + accessor.setPropertyValue("dates[2]", "10-2-09"); + dates = (List) accessor.getPropertyValue("dates"); + assertThat(new LocalDate(dates.get(0))).isEqualTo(new LocalDate(2009, 10, 30)); + assertThat(new LocalDate(dates.get(1))).isEqualTo(new LocalDate(2009, 10, 1)); + assertThat(new LocalDate(dates.get(2))).isEqualTo(new LocalDate(2009, 10, 2)); + } + } + + @Test + public void printNull() { + formattingService.addFormatterForFieldType(Number.class, new NumberStyleFormatter()); + assertThat(formattingService.convert(null, TypeDescriptor.valueOf(Integer.class), TypeDescriptor.valueOf(String.class))).isEqualTo(""); + } + + @Test + public void parseNull() { + formattingService.addFormatterForFieldType(Number.class, new NumberStyleFormatter()); + assertThat(formattingService + .convert(null, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class))).isNull(); + } + + @Test + public void parseEmptyString() { + formattingService.addFormatterForFieldType(Number.class, new NumberStyleFormatter()); + assertThat(formattingService.convert("", TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class))).isNull(); + } + + @Test + public void parseBlankString() { + formattingService.addFormatterForFieldType(Number.class, new NumberStyleFormatter()); + assertThat(formattingService.convert(" ", TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class))).isNull(); + } + + @Test + public void parseParserReturnsNull() { + formattingService.addFormatterForFieldType(Integer.class, new NullReturningFormatter()); + assertThatExceptionOfType(ConversionFailedException.class).isThrownBy(() -> + formattingService.convert("1", TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class))); + } + + @Test + public void parseNullPrimitiveProperty() { + formattingService.addFormatterForFieldType(Integer.class, new NumberStyleFormatter()); + assertThatExceptionOfType(ConversionFailedException.class).isThrownBy(() -> + formattingService.convert(null, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(int.class))); + } + + @Test + public void printNullDefault() { + assertThat(formattingService + .convert(null, TypeDescriptor.valueOf(Integer.class), TypeDescriptor.valueOf(String.class))).isEqualTo(null); + } + + @Test + public void parseNullDefault() { + assertThat(formattingService + .convert(null, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class))).isNull(); + } + + @Test + public void parseEmptyStringDefault() { + assertThat(formattingService.convert("", TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class))).isNull(); + } + + @Test + public void formatFieldForAnnotationWithSubclassAsFieldType() throws Exception { + formattingService.addFormatterForFieldAnnotation(new JodaDateTimeFormatAnnotationFormatterFactory() { + @Override + public Printer getPrinter(org.springframework.format.annotation.DateTimeFormat annotation, Class fieldType) { + assertThat(fieldType).isEqualTo(MyDate.class); + return super.getPrinter(annotation, fieldType); + } + }); + formattingService.addConverter(new Converter() { + @Override + public Long convert(MyDate source) { + return source.getTime(); + } + }); + formattingService.addConverter(new Converter() { + @Override + public Date convert(MyDate source) { + return source; + } + }); + + formattingService.convert(new MyDate(), new TypeDescriptor(ModelWithSubclassField.class.getField("date")), + TypeDescriptor.valueOf(String.class)); + } + + @Test + public void registerDefaultValueViaFormatter() { + registerDefaultValue(Date.class, new Date()); + } + + private void registerDefaultValue(Class clazz, final T defaultValue) { + formattingService.addFormatterForFieldType(clazz, new Formatter() { + @Override + public T parse(String text, Locale locale) { + return defaultValue; + } + @Override + public String print(T t, Locale locale) { + return defaultValue.toString(); + } + @Override + public String toString() { + return defaultValue.toString(); + } + }); + } + + @Test + public void introspectedFormatter() { + formattingService.addFormatter(new NumberStyleFormatter("#,#00.0#")); + assertThat(formattingService.convert(123, String.class)).isEqualTo("123.0"); + assertThat(formattingService.convert("123.0", Integer.class)).isEqualTo(123); + } + + @Test + public void introspectedPrinter() { + formattingService.addPrinter(new NumberStyleFormatter("#,#00.0#")); + assertThat(formattingService.convert(123, String.class)).isEqualTo("123.0"); + assertThatExceptionOfType(ConversionFailedException.class).isThrownBy(() -> + formattingService.convert("123.0", Integer.class)) + .withCauseInstanceOf(NumberFormatException.class); + } + + @Test + public void introspectedParser() { + formattingService.addParser(new NumberStyleFormatter("#,#00.0#")); + assertThat(formattingService.convert("123.0", Integer.class)).isEqualTo(123); + assertThat(formattingService.convert(123, String.class)).isEqualTo("123"); + } + + @Test + public void proxiedFormatter() { + Formatter formatter = new NumberStyleFormatter(); + formattingService.addFormatter((Formatter) new ProxyFactory(formatter).getProxy()); + assertThat(formattingService.convert(null, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class))).isNull(); + } + + @Test + public void introspectedConverter() { + formattingService.addConverter(new IntegerConverter()); + assertThat(formattingService.convert("1", Integer.class)).isEqualTo(Integer.valueOf(1)); + } + + @Test + public void proxiedConverter() { + Converter converter = new IntegerConverter(); + formattingService.addConverter((Converter) new ProxyFactory(converter).getProxy()); + assertThat(formattingService.convert("1", Integer.class)).isEqualTo(Integer.valueOf(1)); + } + + @Test + public void introspectedConverterFactory() { + formattingService.addConverterFactory(new IntegerConverterFactory()); + assertThat(formattingService.convert("1", Integer.class)).isEqualTo(Integer.valueOf(1)); + } + + @Test + public void proxiedConverterFactory() { + ConverterFactory converterFactory = new IntegerConverterFactory(); + formattingService.addConverterFactory((ConverterFactory) new ProxyFactory(converterFactory).getProxy()); + assertThat(formattingService.convert("1", Integer.class)).isEqualTo(Integer.valueOf(1)); + } + + + public static class ValueBean { + + @Value("10-31-09") + @org.springframework.format.annotation.DateTimeFormat(pattern="MM-d-yy") + public Date date; + } + + + public static class MetaValueBean { + + @MyDateAnn + public Date date; + + @MyNumberAnn + public Double number; + } + + + @Value("${myDate}") + @org.springframework.format.annotation.DateTimeFormat(pattern="MM-d-yy") + @Retention(RetentionPolicy.RUNTIME) + public @interface MyDateAnn { + } + + + @Value("${myNumber}") + @NumberFormat(style = NumberFormat.Style.PERCENT) + @Retention(RetentionPolicy.RUNTIME) + public @interface MyNumberAnn { + } + + + public static class Model { + + @org.springframework.format.annotation.DateTimeFormat(style="S-") + public Date date; + + @org.springframework.format.annotation.DateTimeFormat(pattern="M-d-yy") + public List dates; + + public List getDates() { + return dates; + } + + public void setDates(List dates) { + this.dates = dates; + } + } + + + public static class ModelWithPlaceholders { + + @org.springframework.format.annotation.DateTimeFormat(style="${dateStyle}") + public Date date; + + @MyDatePattern + public List dates; + + public List getDates() { + return dates; + } + + public void setDates(List dates) { + this.dates = dates; + } + } + + + @org.springframework.format.annotation.DateTimeFormat(pattern="${datePattern}") + @Retention(RetentionPolicy.RUNTIME) + public @interface MyDatePattern { + } + + + public static class NullReturningFormatter implements Formatter { + + @Override + public String print(Integer object, Locale locale) { + return null; + } + + @Override + public Integer parse(String text, Locale locale) { + return null; + } + } + + + @SuppressWarnings("serial") + public static class MyDate extends Date { + } + + + private static class ModelWithSubclassField { + + @org.springframework.format.annotation.DateTimeFormat(style = "S-") + public MyDate date; + } + + + private static class IntegerConverter implements Converter { + + @Override + public Integer convert(String source) { + return Integer.parseInt(source); + } + } + + + private static class IntegerConverterFactory implements ConverterFactory { + + @Override + @SuppressWarnings("unchecked") + public Converter getConverter(Class targetType) { + if (Integer.class == targetType) { + return (Converter) new IntegerConverter(); + } + else { + throw new IllegalStateException(); + } + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/instrument/classloading/InstrumentableClassLoaderTests.java b/spring-context/src/test/java/org/springframework/instrument/classloading/InstrumentableClassLoaderTests.java new file mode 100644 index 0000000..b70628e --- /dev/null +++ b/spring-context/src/test/java/org/springframework/instrument/classloading/InstrumentableClassLoaderTests.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.instrument.classloading; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Costin Leau + * @author Juergen Hoeller + * @author Chris Beams + */ +public class InstrumentableClassLoaderTests { + + @Test + public void testDefaultLoadTimeWeaver() { + ClassLoader loader = new SimpleInstrumentableClassLoader(ClassUtils.getDefaultClassLoader()); + ReflectiveLoadTimeWeaver handler = new ReflectiveLoadTimeWeaver(loader); + assertThat(handler.getInstrumentableClassLoader()).isSameAs(loader); + } + +} diff --git a/spring-context/src/test/java/org/springframework/instrument/classloading/ReflectiveLoadTimeWeaverTests.java b/spring-context/src/test/java/org/springframework/instrument/classloading/ReflectiveLoadTimeWeaverTests.java new file mode 100644 index 0000000..448f715 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/instrument/classloading/ReflectiveLoadTimeWeaverTests.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.instrument.classloading; + +import java.lang.instrument.ClassFileTransformer; +import java.security.ProtectionDomain; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Unit tests for the {@link ReflectiveLoadTimeWeaver} class. + * + * @author Rick Evans + * @author Chris Beams + */ +public class ReflectiveLoadTimeWeaverTests { + + @Test + public void testCtorWithNullClassLoader() { + assertThatIllegalArgumentException().isThrownBy(() -> + new ReflectiveLoadTimeWeaver(null)); + } + + @Test + public void testCtorWithClassLoaderThatDoesNotExposeAnAddTransformerMethod() { + assertThatIllegalStateException().isThrownBy(() -> + new ReflectiveLoadTimeWeaver(getClass().getClassLoader())); + } + + @Test + public void testCtorWithClassLoaderThatDoesNotExposeAGetThrowawayClassLoaderMethodIsOkay() { + JustAddTransformerClassLoader classLoader = new JustAddTransformerClassLoader(); + ReflectiveLoadTimeWeaver weaver = new ReflectiveLoadTimeWeaver(classLoader); + weaver.addTransformer(new ClassFileTransformer() { + @Override + public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { + return "CAFEDEAD".getBytes(); + } + }); + assertThat(classLoader.getNumTimesGetThrowawayClassLoaderCalled()).isEqualTo(1); + } + + @Test + public void testAddTransformerWithNullTransformer() { + assertThatIllegalArgumentException().isThrownBy(() -> + new ReflectiveLoadTimeWeaver(new JustAddTransformerClassLoader()).addTransformer(null)); + } + + @Test + public void testGetThrowawayClassLoaderWithClassLoaderThatDoesNotExposeAGetThrowawayClassLoaderMethodYieldsFallbackClassLoader() { + ReflectiveLoadTimeWeaver weaver = new ReflectiveLoadTimeWeaver(new JustAddTransformerClassLoader()); + ClassLoader throwawayClassLoader = weaver.getThrowawayClassLoader(); + assertThat(throwawayClassLoader).isNotNull(); + } + + @Test + public void testGetThrowawayClassLoaderWithTotallyCompliantClassLoader() { + TotallyCompliantClassLoader classLoader = new TotallyCompliantClassLoader(); + ReflectiveLoadTimeWeaver weaver = new ReflectiveLoadTimeWeaver(classLoader); + ClassLoader throwawayClassLoader = weaver.getThrowawayClassLoader(); + assertThat(throwawayClassLoader).isNotNull(); + assertThat(classLoader.getNumTimesGetThrowawayClassLoaderCalled()).isEqualTo(1); + } + + + public static class JustAddTransformerClassLoader extends ClassLoader { + + private int numTimesAddTransformerCalled = 0; + + + public int getNumTimesGetThrowawayClassLoaderCalled() { + return this.numTimesAddTransformerCalled; + } + + + public void addTransformer(ClassFileTransformer transformer) { + ++this.numTimesAddTransformerCalled; + } + + } + + + public static final class TotallyCompliantClassLoader extends JustAddTransformerClassLoader { + + private int numTimesGetThrowawayClassLoaderCalled = 0; + + + @Override + public int getNumTimesGetThrowawayClassLoaderCalled() { + return this.numTimesGetThrowawayClassLoaderCalled; + } + + + public ClassLoader getThrowawayClassLoader() { + ++this.numTimesGetThrowawayClassLoaderCalled; + return getClass().getClassLoader(); + } + + } + +} diff --git a/spring-context/src/test/java/org/springframework/instrument/classloading/ResourceOverridingShadowingClassLoaderTests.java b/spring-context/src/test/java/org/springframework/instrument/classloading/ResourceOverridingShadowingClassLoaderTests.java new file mode 100644 index 0000000..86d5a69 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/instrument/classloading/ResourceOverridingShadowingClassLoaderTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.instrument.classloading; + +import java.io.IOException; +import java.util.Enumeration; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rod Johnson + * @author Chris Beams + * @since 2.0 + */ +public class ResourceOverridingShadowingClassLoaderTests { + + private static final String EXISTING_RESOURCE = "org/springframework/instrument/classloading/testResource.xml"; + + private ClassLoader thisClassLoader = getClass().getClassLoader(); + + private ResourceOverridingShadowingClassLoader overridingLoader = new ResourceOverridingShadowingClassLoader(thisClassLoader); + + + @Test + public void testFindsExistingResourceWithGetResourceAndNoOverrides() { + assertThat(thisClassLoader.getResource(EXISTING_RESOURCE)).isNotNull(); + assertThat(overridingLoader.getResource(EXISTING_RESOURCE)).isNotNull(); + } + + @Test + public void testDoesNotFindExistingResourceWithGetResourceAndNullOverride() { + assertThat(thisClassLoader.getResource(EXISTING_RESOURCE)).isNotNull(); + overridingLoader.override(EXISTING_RESOURCE, null); + assertThat(overridingLoader.getResource(EXISTING_RESOURCE)).isNull(); + } + + @Test + public void testFindsExistingResourceWithGetResourceAsStreamAndNoOverrides() { + assertThat(thisClassLoader.getResourceAsStream(EXISTING_RESOURCE)).isNotNull(); + assertThat(overridingLoader.getResourceAsStream(EXISTING_RESOURCE)).isNotNull(); + } + + @Test + public void testDoesNotFindExistingResourceWithGetResourceAsStreamAndNullOverride() { + assertThat(thisClassLoader.getResourceAsStream(EXISTING_RESOURCE)).isNotNull(); + overridingLoader.override(EXISTING_RESOURCE, null); + assertThat(overridingLoader.getResourceAsStream(EXISTING_RESOURCE)).isNull(); + } + + @Test + public void testFindsExistingResourceWithGetResourcesAndNoOverrides() throws IOException { + assertThat(thisClassLoader.getResources(EXISTING_RESOURCE)).isNotNull(); + assertThat(overridingLoader.getResources(EXISTING_RESOURCE)).isNotNull(); + assertThat(countElements(overridingLoader.getResources(EXISTING_RESOURCE))).isEqualTo(1); + } + + @Test + public void testDoesNotFindExistingResourceWithGetResourcesAndNullOverride() throws IOException { + assertThat(thisClassLoader.getResources(EXISTING_RESOURCE)).isNotNull(); + overridingLoader.override(EXISTING_RESOURCE, null); + assertThat(countElements(overridingLoader.getResources(EXISTING_RESOURCE))).isEqualTo(0); + } + + private int countElements(Enumeration e) { + int elts = 0; + while (e.hasMoreElements()) { + e.nextElement(); + ++elts; + } + return elts; + } +} diff --git a/spring-context/src/test/java/org/springframework/jmx/AbstractJmxTests.java b/spring-context/src/test/java/org/springframework/jmx/AbstractJmxTests.java new file mode 100644 index 0000000..7147ccc --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/AbstractJmxTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; + +/** + * Base JMX test class that pre-loads an ApplicationContext from a user-configurable file. Override the + * {@link #getApplicationContextPath()} method to control the configuration file location. + * + * @author Rob Harrop + * @author Juergen Hoeller + */ +public abstract class AbstractJmxTests extends AbstractMBeanServerTests { + + private ConfigurableApplicationContext ctx; + + + @Override + protected final void onSetUp() throws Exception { + ctx = loadContext(getApplicationContextPath()); + } + + @Override + protected final void onTearDown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + protected String getApplicationContextPath() { + return "org/springframework/jmx/applicationContext.xml"; + } + + protected ApplicationContext getContext() { + return this.ctx; + } +} diff --git a/spring-context/src/test/java/org/springframework/jmx/AbstractMBeanServerTests.java b/spring-context/src/test/java/org/springframework/jmx/AbstractMBeanServerTests.java new file mode 100644 index 0000000..99b0415 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/AbstractMBeanServerTests.java @@ -0,0 +1,163 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx; + +import java.net.BindException; + +import javax.management.MBeanServer; +import javax.management.MBeanServerFactory; +import javax.management.ObjectName; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler; +import org.junit.jupiter.api.extension.TestExecutionExceptionHandler; +import org.opentest4j.TestAbortedException; + +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.jmx.AbstractMBeanServerTests.BindExceptionHandler; +import org.springframework.jmx.export.MBeanExporter; +import org.springframework.util.MBeanTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + *

    If you run into the "Unsupported protocol: jmxmp" error, you will need to + * download the JMX + * Remote API 1.0.1_04 Reference Implementation from Oracle and extract + * {@code jmxremote_optional.jar} into your classpath, for example in the {@code lib/ext} + * folder of your JVM. + * + *

    See also: + *

    + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Sam Brannen + * @author Chris Beams + * @author Stephane Nicoll + */ +@ExtendWith(BindExceptionHandler.class) +public abstract class AbstractMBeanServerTests { + + protected MBeanServer server; + + + @BeforeEach + public final void setUp() throws Exception { + this.server = MBeanServerFactory.createMBeanServer(); + try { + onSetUp(); + } + catch (Exception ex) { + releaseServer(); + throw ex; + } + } + + @AfterEach + public void tearDown() throws Exception { + releaseServer(); + onTearDown(); + } + + private void releaseServer() throws Exception { + try { + MBeanServerFactory.releaseMBeanServer(getServer()); + } + catch (IllegalArgumentException ex) { + if (!ex.getMessage().contains("not in list")) { + throw ex; + } + } + MBeanTestUtils.resetMBeanServers(); + } + + protected final ConfigurableApplicationContext loadContext(String configLocation) { + GenericApplicationContext ctx = new GenericApplicationContext(); + new XmlBeanDefinitionReader(ctx).loadBeanDefinitions(configLocation); + ctx.getDefaultListableBeanFactory().registerSingleton("server", getServer()); + ctx.refresh(); + return ctx; + } + + protected void onSetUp() throws Exception { + } + + protected void onTearDown() throws Exception { + } + + protected final MBeanServer getServer() { + return this.server; + } + + /** + * Start the specified {@link MBeanExporter}. + */ + protected void start(MBeanExporter exporter) { + exporter.afterPropertiesSet(); + exporter.afterSingletonsInstantiated(); + } + + protected void assertIsRegistered(String message, ObjectName objectName) { + assertThat(getServer().isRegistered(objectName)).as(message).isTrue(); + } + + protected void assertIsNotRegistered(String message, ObjectName objectName) { + assertThat(getServer().isRegistered(objectName)).as(message).isFalse(); + } + + + static class BindExceptionHandler implements TestExecutionExceptionHandler, LifecycleMethodExecutionExceptionHandler { + + @Override + public void handleTestExecutionException(ExtensionContext context, Throwable throwable) throws Throwable { + handleBindException(throwable); + } + + @Override + public void handleBeforeEachMethodExecutionException(ExtensionContext context, Throwable throwable) + throws Throwable { + handleBindException(throwable); + } + + @Override + public void handleAfterEachMethodExecutionException(ExtensionContext context, Throwable throwable) + throws Throwable { + handleBindException(throwable); + } + + private void handleBindException(Throwable throwable) throws Throwable { + // Abort test? + if (throwable instanceof BindException) { + throw new TestAbortedException("Failed to bind to MBeanServer", throwable); + } + // Else rethrow to conform to the contracts of TestExecutionExceptionHandler and LifecycleMethodExecutionExceptionHandler + throw throwable; + } + + } + +} + diff --git a/spring-context/src/test/java/org/springframework/jmx/IJmxTestBean.java b/spring-context/src/test/java/org/springframework/jmx/IJmxTestBean.java new file mode 100644 index 0000000..26d2491 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/IJmxTestBean.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx; + +/** + * @author Rob Harrop + * @author Juergen Hoeller + */ +public interface IJmxTestBean { + + int add(int x, int y); + + long myOperation(); + + int getAge(); + + void setAge(int age); + + void setName(String name) throws Exception; + + String getName(); + + // used to test invalid methods that exist in the proxy interface + void dontExposeMe(); + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/JmxTestBean.java b/spring-context/src/test/java/org/springframework/jmx/JmxTestBean.java new file mode 100644 index 0000000..ccd3fb0 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/JmxTestBean.java @@ -0,0 +1,152 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx; + +import java.io.IOException; + +/** + * @@org.springframework.jmx.export.metadata.ManagedResource + * (description="My Managed Bean", objectName="spring:bean=test", + * log=true, logFile="build/jmx.log", currencyTimeLimit=15, persistPolicy="OnUpdate", + * persistPeriod=200, persistLocation="./foo", persistName="bar.jmx") + * @@org.springframework.jmx.export.metadata.ManagedNotification + * (name="My Notification", description="A Notification", notificationType="type.foo,type.bar") + * @author Rob Harrop + * @author Juergen Hoeller + */ +public class JmxTestBean implements IJmxTestBean { + + private String name; + + private String nickName; + + private int age; + + private boolean isSuperman; + + + /** + * @@org.springframework.jmx.export.metadata.ManagedAttribute + * (description="The Age Attribute", currencyTimeLimit=15) + */ + @Override + public int getAge() { + return age; + } + + @Override + public void setAge(int age) { + this.age = age; + } + + /** + * @@org.springframework.jmx.export.metadata.ManagedOperation(currencyTimeLimit=30) + */ + @Override + public long myOperation() { + return 1L; + } + + /** + * @@org.springframework.jmx.export.metadata.ManagedAttribute + * (description="The Name Attribute", currencyTimeLimit=20, + * defaultValue="bar", persistPolicy="OnUpdate") + */ + @Override + public void setName(String name) throws Exception { + if ("Juergen".equals(name)) { + throw new IllegalArgumentException("Juergen"); + } + if ("Juergen Class".equals(name)) { + throw new ClassNotFoundException("Juergen"); + } + if ("Juergen IO".equals(name)) { + throw new IOException("Juergen"); + } + this.name = name; + } + + /** + * @@org.springframework.jmx.export.metadata.ManagedAttribute + * (defaultValue="foo", persistPeriod=300) + */ + @Override + public String getName() { + return name; + } + + /** + * @@org.springframework.jmx.export.metadata.ManagedAttribute(description="The Nick + * Name + * Attribute") + */ + public void setNickName(String nickName) { + this.nickName = nickName; + } + + public String getNickName() { + return this.nickName; + } + + public void setSuperman(boolean superman) { + this.isSuperman = superman; + } + + /** + * @@org.springframework.jmx.export.metadata.ManagedAttribute(description="The Is + * Superman + * Attribute") + */ + public boolean isSuperman() { + return isSuperman; + } + + /** + * @@org.springframework.jmx.export.metadata.ManagedOperation(description="Add Two + * Numbers + * Together") + * @@org.springframework.jmx.export.metadata.ManagedOperationParameter(index=0, name="x", description="Left operand") + * @@org.springframework.jmx.export.metadata.ManagedOperationParameter(index=1, name="y", description="Right operand") + */ + @Override + public int add(int x, int y) { + return x + y; + } + + /** + * Test method that is not exposed by the MetadataAssembler. + */ + @Override + public void dontExposeMe() { + throw new RuntimeException(); + } + + protected void someProtectedMethod() { + } + + @SuppressWarnings("unused") + private void somePrivateMethod() { + } + + protected void getSomething() { + } + + @SuppressWarnings("unused") + private void getSomethingElse() { + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/access/MBeanClientInterceptorTests.java b/spring-context/src/test/java/org/springframework/jmx/access/MBeanClientInterceptorTests.java new file mode 100644 index 0000000..f98384c --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/access/MBeanClientInterceptorTests.java @@ -0,0 +1,286 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.access; + +import java.beans.PropertyDescriptor; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.lang.management.ThreadMXBean; +import java.lang.reflect.Method; +import java.net.BindException; +import java.util.HashMap; +import java.util.Map; + +import javax.management.Descriptor; +import javax.management.MBeanServerConnection; +import javax.management.remote.JMXConnectorServer; +import javax.management.remote.JMXConnectorServerFactory; +import javax.management.remote.JMXServiceURL; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.jmx.AbstractMBeanServerTests; +import org.springframework.jmx.IJmxTestBean; +import org.springframework.jmx.JmxTestBean; +import org.springframework.jmx.export.MBeanExporter; +import org.springframework.jmx.export.assembler.AbstractReflectiveMBeanInfoAssembler; +import org.springframework.util.SocketUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIOException; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * @author Rob Harrop + * @author Juergen Hoeller + * @author Sam Brannen + * @author Chris Beams + */ +class MBeanClientInterceptorTests extends AbstractMBeanServerTests { + + protected static final String OBJECT_NAME = "spring:test=proxy"; + + protected JmxTestBean target; + + protected boolean runTests = true; + + @Override + public void onSetUp() throws Exception { + target = new JmxTestBean(); + target.setAge(100); + target.setName("Rob Harrop"); + + MBeanExporter adapter = new MBeanExporter(); + Map beans = new HashMap<>(); + beans.put(OBJECT_NAME, target); + adapter.setServer(getServer()); + adapter.setBeans(beans); + adapter.setAssembler(new ProxyTestAssembler()); + start(adapter); + } + + protected MBeanServerConnection getServerConnection() throws Exception { + return getServer(); + } + + protected IJmxTestBean getProxy() throws Exception { + MBeanProxyFactoryBean factory = new MBeanProxyFactoryBean(); + factory.setServer(getServerConnection()); + factory.setProxyInterface(IJmxTestBean.class); + factory.setObjectName(OBJECT_NAME); + factory.afterPropertiesSet(); + return (IJmxTestBean) factory.getObject(); + } + + @Test + void proxyClassIsDifferent() throws Exception { + assumeTrue(runTests); + IJmxTestBean proxy = getProxy(); + assertThat(proxy.getClass()).as("The proxy class should be different than the base class").isNotSameAs(IJmxTestBean.class); + } + + @Test + void differentProxiesSameClass() throws Exception { + assumeTrue(runTests); + IJmxTestBean proxy1 = getProxy(); + IJmxTestBean proxy2 = getProxy(); + + assertThat(proxy2).as("The proxies should NOT be the same").isNotSameAs(proxy1); + assertThat(proxy2.getClass()).as("The proxy classes should be the same").isSameAs(proxy1.getClass()); + } + + @Test + void getAttributeValue() throws Exception { + assumeTrue(runTests); + IJmxTestBean proxy1 = getProxy(); + int age = proxy1.getAge(); + assertThat(age).as("The age should be 100").isEqualTo(100); + } + + @Test + void setAttributeValue() throws Exception { + assumeTrue(runTests); + IJmxTestBean proxy = getProxy(); + proxy.setName("Rob Harrop"); + assertThat(target.getName()).as("The name of the bean should have been updated").isEqualTo("Rob Harrop"); + } + + @Test + void setAttributeValueWithRuntimeException() throws Exception { + assumeTrue(runTests); + IJmxTestBean proxy = getProxy(); + assertThatIllegalArgumentException().isThrownBy(() -> proxy.setName("Juergen")); + } + + @Test + void setAttributeValueWithCheckedException() throws Exception { + assumeTrue(runTests); + IJmxTestBean proxy = getProxy(); + assertThatExceptionOfType(ClassNotFoundException.class).isThrownBy(() -> proxy.setName("Juergen Class")); + } + + @Test + void setAttributeValueWithIOException() throws Exception { + assumeTrue(runTests); + IJmxTestBean proxy = getProxy(); + assertThatIOException().isThrownBy(() -> proxy.setName("Juergen IO")); + } + + @Test + void setReadOnlyAttribute() throws Exception { + assumeTrue(runTests); + IJmxTestBean proxy = getProxy(); + assertThatExceptionOfType(InvalidInvocationException.class).isThrownBy(() -> proxy.setAge(900)); + } + + @Test + void invokeNoArgs() throws Exception { + assumeTrue(runTests); + IJmxTestBean proxy = getProxy(); + long result = proxy.myOperation(); + assertThat(result).as("The operation should return 1").isEqualTo(1); + } + + @Test + void invokeArgs() throws Exception { + assumeTrue(runTests); + IJmxTestBean proxy = getProxy(); + int result = proxy.add(1, 2); + assertThat(result).as("The operation should return 3").isEqualTo(3); + } + + @Test + void invokeUnexposedMethodWithException() throws Exception { + assumeTrue(runTests); + IJmxTestBean bean = getProxy(); + assertThatExceptionOfType(InvalidInvocationException.class).isThrownBy(() -> bean.dontExposeMe()); + } + + @Test + void lazyConnectionToRemote() throws Exception { + assumeTrue(runTests); + + final int port = SocketUtils.findAvailableTcpPort(); + + JMXServiceURL url = new JMXServiceURL("service:jmx:jmxmp://localhost:" + port); + JMXConnectorServer connector = JMXConnectorServerFactory.newJMXConnectorServer(url, null, getServer()); + + MBeanProxyFactoryBean factory = new MBeanProxyFactoryBean(); + factory.setServiceUrl(url.toString()); + factory.setProxyInterface(IJmxTestBean.class); + factory.setObjectName(OBJECT_NAME); + factory.setConnectOnStartup(false); + factory.setRefreshOnConnectFailure(true); + // should skip connection to the server + factory.afterPropertiesSet(); + IJmxTestBean bean = (IJmxTestBean) factory.getObject(); + + // now start the connector + try { + connector.start(); + } + catch (BindException ex) { + System.out.println("Skipping remainder of JMX LazyConnectionToRemote test because binding to local port [" + + port + "] failed: " + ex.getMessage()); + return; + } + + // should now be able to access data via the lazy proxy + try { + assertThat(bean.getName()).isEqualTo("Rob Harrop"); + assertThat(bean.getAge()).isEqualTo(100); + } + finally { + connector.stop(); + } + } + + @Test + void mxBeanAttributeAccess() throws Exception { + MBeanClientInterceptor interceptor = new MBeanClientInterceptor(); + interceptor.setServer(ManagementFactory.getPlatformMBeanServer()); + interceptor.setObjectName("java.lang:type=Memory"); + interceptor.setManagementInterface(MemoryMXBean.class); + MemoryMXBean proxy = ProxyFactory.getProxy(MemoryMXBean.class, interceptor); + assertThat(proxy.getHeapMemoryUsage().getMax()).isGreaterThan(0); + } + + @Test + void mxBeanOperationAccess() throws Exception { + MBeanClientInterceptor interceptor = new MBeanClientInterceptor(); + interceptor.setServer(ManagementFactory.getPlatformMBeanServer()); + interceptor.setObjectName("java.lang:type=Threading"); + ThreadMXBean proxy = ProxyFactory.getProxy(ThreadMXBean.class, interceptor); + assertThat(proxy.getThreadInfo(Thread.currentThread().getId()).getStackTrace()).isNotNull(); + } + + + private static class ProxyTestAssembler extends AbstractReflectiveMBeanInfoAssembler { + + @Override + protected boolean includeReadAttribute(Method method, String beanKey) { + return true; + } + + @Override + protected boolean includeWriteAttribute(Method method, String beanKey) { + if ("setAge".equals(method.getName())) { + return false; + } + return true; + } + + @Override + protected boolean includeOperation(Method method, String beanKey) { + if ("dontExposeMe".equals(method.getName())) { + return false; + } + return true; + } + + @SuppressWarnings("unused") + protected String getOperationDescription(Method method) { + return method.getName(); + } + + @SuppressWarnings("unused") + protected String getAttributeDescription(PropertyDescriptor propertyDescriptor) { + return propertyDescriptor.getDisplayName(); + } + + @SuppressWarnings("unused") + protected void populateAttributeDescriptor(Descriptor descriptor, Method getter, Method setter) { + } + + @SuppressWarnings("unused") + protected void populateOperationDescriptor(Descriptor descriptor, Method method) { + } + + @SuppressWarnings({ "unused", "rawtypes" }) + protected String getDescription(String beanKey, Class beanClass) { + return ""; + } + + @SuppressWarnings({ "unused", "rawtypes" }) + protected void populateMBeanDescriptor(Descriptor mbeanDescriptor, String beanKey, Class beanClass) { + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/access/RemoteMBeanClientInterceptorTests.java b/spring-context/src/test/java/org/springframework/jmx/access/RemoteMBeanClientInterceptorTests.java new file mode 100644 index 0000000..5c4c4a3 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/access/RemoteMBeanClientInterceptorTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.access; + +import java.net.BindException; +import java.net.MalformedURLException; + +import javax.management.MBeanServerConnection; +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXConnectorServer; +import javax.management.remote.JMXConnectorServerFactory; +import javax.management.remote.JMXServiceURL; + +import org.junit.jupiter.api.AfterEach; + +import org.springframework.util.SocketUtils; + +/** + * @author Rob Harrop + * @author Chris Beams + * @author Sam Brannen + */ +class RemoteMBeanClientInterceptorTests extends MBeanClientInterceptorTests { + + private final int servicePort = SocketUtils.findAvailableTcpPort(); + + private final String serviceUrl = "service:jmx:jmxmp://localhost:" + servicePort; + + + private JMXConnectorServer connectorServer; + + private JMXConnector connector; + + + @Override + public void onSetUp() throws Exception { + super.onSetUp(); + this.connectorServer = JMXConnectorServerFactory.newJMXConnectorServer(getServiceUrl(), null, getServer()); + try { + this.connectorServer.start(); + } + catch (BindException ex) { + System.out.println("Skipping remote JMX tests because binding to local port [" + + this.servicePort + "] failed: " + ex.getMessage()); + runTests = false; + } + } + + private JMXServiceURL getServiceUrl() throws MalformedURLException { + return new JMXServiceURL(this.serviceUrl); + } + + @Override + protected MBeanServerConnection getServerConnection() throws Exception { + this.connector = JMXConnectorFactory.connect(getServiceUrl()); + return this.connector.getMBeanServerConnection(); + } + + @AfterEach + @Override + public void tearDown() throws Exception { + if (this.connector != null) { + this.connector.close(); + } + if (this.connectorServer != null) { + this.connectorServer.stop(); + } + if (runTests) { + super.tearDown(); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/CustomDateEditorRegistrar.java b/spring-context/src/test/java/org/springframework/jmx/export/CustomDateEditorRegistrar.java new file mode 100644 index 0000000..0b991ad --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/CustomDateEditorRegistrar.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export; + +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.springframework.beans.PropertyEditorRegistrar; +import org.springframework.beans.PropertyEditorRegistry; +import org.springframework.beans.propertyeditors.CustomDateEditor; + +/** + * @author Juergen Hoeller + */ +public class CustomDateEditorRegistrar implements PropertyEditorRegistrar { + + @Override + public void registerCustomEditors(PropertyEditorRegistry registry) { + registry.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("yyyy/MM/dd"), true)); + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/CustomEditorConfigurerTests.java b/spring-context/src/test/java/org/springframework/jmx/export/CustomEditorConfigurerTests.java new file mode 100644 index 0000000..6abb6dc --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/CustomEditorConfigurerTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import javax.management.ObjectName; + +import org.junit.jupiter.api.Test; + +import org.springframework.jmx.AbstractJmxTests; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + */ +class CustomEditorConfigurerTests extends AbstractJmxTests { + + private final SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd"); + + @Override + protected String getApplicationContextPath() { + return "org/springframework/jmx/export/customConfigurer.xml"; + } + + @Test + void datesInApplicationContext() throws Exception { + DateRange dr = getContext().getBean("dateRange", DateRange.class); + + assertThat(dr.getStartDate()).as("startDate").isEqualTo(getStartDate()); + assertThat(dr.getEndDate()).as("endDate").isEqualTo(getEndDate()); + } + + @Test + void datesInJmx() throws Exception { + ObjectName oname = new ObjectName("bean:name=dateRange"); + + Date startJmx = (Date) getServer().getAttribute(oname, "StartDate"); + Date endJmx = (Date) getServer().getAttribute(oname, "EndDate"); + + assertThat(startJmx).as("startDate").isEqualTo(getStartDate()); + assertThat(endJmx).as("endDate").isEqualTo(getEndDate()); + } + + private Date getStartDate() throws ParseException { + return df.parse("2004/10/12"); + } + + private Date getEndDate() throws ParseException { + return df.parse("2004/11/13"); + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/DateRange.java b/spring-context/src/test/java/org/springframework/jmx/export/DateRange.java new file mode 100644 index 0000000..3a66069 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/DateRange.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export; + +import java.util.Date; + +/** + * @author Rob Harrop + */ +public class DateRange { + + private Date startDate; + + private Date endDate; + + public Date getStartDate() { + return startDate; + } + + public void setStartDate(Date startDate) { + this.startDate = startDate; + } + + public Date getEndDate() { + return endDate; + } + + public void setEndDate(Date endDate) { + this.endDate = endDate; + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/ExceptionOnInitBean.java b/spring-context/src/test/java/org/springframework/jmx/export/ExceptionOnInitBean.java new file mode 100644 index 0000000..47fe7d7 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/ExceptionOnInitBean.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export; + +/** + * @author Rob Harrop + */ +public class ExceptionOnInitBean { + + private boolean exceptOnInit = false; + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public void setExceptOnInit(boolean exceptOnInit) { + this.exceptOnInit = exceptOnInit; + } + + public ExceptionOnInitBean() { + if (exceptOnInit) { + throw new RuntimeException("I am being init'd!"); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/LazyInitMBeanTests.java b/spring-context/src/test/java/org/springframework/jmx/export/LazyInitMBeanTests.java new file mode 100644 index 0000000..8333578 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/LazyInitMBeanTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export; + +import javax.management.MBeanServer; +import javax.management.ObjectName; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.jmx.support.ObjectNameManager; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + * @author Juergen Hoeller + */ +public class LazyInitMBeanTests { + + @Test + public void invokeOnLazyInitBean() throws Exception { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("org/springframework/jmx/export/lazyInit.xml"); + assertThat(ctx.getBeanFactory().containsSingleton("testBean")).isFalse(); + assertThat(ctx.getBeanFactory().containsSingleton("testBean2")).isFalse(); + try { + MBeanServer server = (MBeanServer) ctx.getBean("server"); + ObjectName oname = ObjectNameManager.getInstance("bean:name=testBean2"); + String name = (String) server.getAttribute(oname, "Name"); + assertThat(name).as("Invalid name returned").isEqualTo("foo"); + } + finally { + ctx.close(); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/MBeanExporterOperationsTests.java b/spring-context/src/test/java/org/springframework/jmx/export/MBeanExporterOperationsTests.java new file mode 100644 index 0000000..b850799 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/MBeanExporterOperationsTests.java @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export; + +import javax.management.InstanceAlreadyExistsException; +import javax.management.MBeanInfo; +import javax.management.ObjectName; +import javax.management.modelmbean.ModelMBeanInfo; +import javax.management.modelmbean.ModelMBeanInfoSupport; +import javax.management.modelmbean.RequiredModelMBean; + +import org.junit.jupiter.api.Test; + +import org.springframework.jmx.AbstractMBeanServerTests; +import org.springframework.jmx.JmxTestBean; +import org.springframework.jmx.export.naming.ObjectNamingStrategy; +import org.springframework.jmx.support.ObjectNameManager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Rob Harrop + * @author Juergen Hoeller + */ +class MBeanExporterOperationsTests extends AbstractMBeanServerTests { + + @Test + void testRegisterManagedResourceWithUserSuppliedObjectName() throws Exception { + ObjectName objectName = ObjectNameManager.getInstance("spring:name=Foo"); + + JmxTestBean bean = new JmxTestBean(); + bean.setName("Rob Harrop"); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(getServer()); + exporter.registerManagedResource(bean, objectName); + + String name = (String) getServer().getAttribute(objectName, "Name"); + assertThat(bean.getName()).as("Incorrect name on MBean").isEqualTo(name); + } + + @Test + void testRegisterExistingMBeanWithUserSuppliedObjectName() throws Exception { + ObjectName objectName = ObjectNameManager.getInstance("spring:name=Foo"); + ModelMBeanInfo info = new ModelMBeanInfoSupport("myClass", "myDescription", null, null, null, null); + RequiredModelMBean bean = new RequiredModelMBean(info); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(getServer()); + exporter.registerManagedResource(bean, objectName); + + MBeanInfo infoFromServer = getServer().getMBeanInfo(objectName); + assertThat(infoFromServer).isEqualTo(info); + } + + @Test + void testRegisterManagedResourceWithGeneratedObjectName() throws Exception { + final ObjectName objectNameTemplate = ObjectNameManager.getInstance("spring:type=Test"); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(getServer()); + exporter.setNamingStrategy(new ObjectNamingStrategy() { + @Override + public ObjectName getObjectName(Object managedBean, String beanKey) { + return objectNameTemplate; + } + }); + + JmxTestBean bean1 = new JmxTestBean(); + JmxTestBean bean2 = new JmxTestBean(); + + ObjectName reg1 = exporter.registerManagedResource(bean1); + ObjectName reg2 = exporter.registerManagedResource(bean2); + + assertIsRegistered("Bean 1 not registered with MBeanServer", reg1); + assertIsRegistered("Bean 2 not registered with MBeanServer", reg2); + + assertObjectNameMatchesTemplate(objectNameTemplate, reg1); + assertObjectNameMatchesTemplate(objectNameTemplate, reg2); + } + + @Test + void testRegisterManagedResourceWithGeneratedObjectNameWithoutUniqueness() throws Exception { + final ObjectName objectNameTemplate = ObjectNameManager.getInstance("spring:type=Test"); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(getServer()); + exporter.setEnsureUniqueRuntimeObjectNames(false); + exporter.setNamingStrategy(new ObjectNamingStrategy() { + @Override + public ObjectName getObjectName(Object managedBean, String beanKey) { + return objectNameTemplate; + } + }); + + JmxTestBean bean1 = new JmxTestBean(); + JmxTestBean bean2 = new JmxTestBean(); + + ObjectName reg1 = exporter.registerManagedResource(bean1); + assertIsRegistered("Bean 1 not registered with MBeanServer", reg1); + + assertThatExceptionOfType(MBeanExportException.class).isThrownBy(()-> + exporter.registerManagedResource(bean2)) + .withCauseExactlyInstanceOf(InstanceAlreadyExistsException.class); + } + + private void assertObjectNameMatchesTemplate(ObjectName objectNameTemplate, ObjectName registeredName) { + assertThat(registeredName.getDomain()).as("Domain is incorrect").isEqualTo(objectNameTemplate.getDomain()); + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/MBeanExporterTests.java b/spring-context/src/test/java/org/springframework/jmx/export/MBeanExporterTests.java new file mode 100644 index 0000000..adff3bb --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/MBeanExporterTests.java @@ -0,0 +1,826 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.management.Attribute; +import javax.management.InstanceNotFoundException; +import javax.management.JMException; +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.Notification; +import javax.management.NotificationListener; +import javax.management.ObjectInstance; +import javax.management.ObjectName; +import javax.management.modelmbean.ModelMBeanInfo; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.testfixture.interceptor.NopInterceptor; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.jmx.AbstractMBeanServerTests; +import org.springframework.jmx.IJmxTestBean; +import org.springframework.jmx.JmxTestBean; +import org.springframework.jmx.export.assembler.AutodetectCapableMBeanInfoAssembler; +import org.springframework.jmx.export.assembler.MBeanInfoAssembler; +import org.springframework.jmx.export.assembler.SimpleReflectiveMBeanInfoAssembler; +import org.springframework.jmx.export.naming.SelfNaming; +import org.springframework.jmx.support.ObjectNameManager; +import org.springframework.jmx.support.RegistrationPolicy; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Integration tests for the {@link MBeanExporter} class. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Rick Evans + * @author Mark Fisher + * @author Chris Beams + * @author Sam Brannen + * @author Stephane Nicoll + */ +public class MBeanExporterTests extends AbstractMBeanServerTests { + + private static final String OBJECT_NAME = "spring:test=jmxMBeanAdaptor"; + + + @Test + void testRegisterNullNotificationListenerType() throws Exception { + Map listeners = new HashMap<>(); + // put null in as a value... + listeners.put("*", null); + MBeanExporter exporter = new MBeanExporter(); + + assertThatIllegalArgumentException().isThrownBy(() -> + exporter.setNotificationListenerMappings(listeners)); + } + + @Test + void testRegisterNotificationListenerForNonExistentMBean() throws Exception { + Map listeners = new HashMap<>(); + NotificationListener dummyListener = new NotificationListener() { + @Override + public void handleNotification(Notification notification, Object handback) { + throw new UnsupportedOperationException(); + } + }; + // the MBean with the supplied object name does not exist... + listeners.put("spring:type=Test", dummyListener); + MBeanExporter exporter = new MBeanExporter(); + exporter.setBeans(getBeanMap()); + exporter.setServer(server); + exporter.setNotificationListenerMappings(listeners); + assertThatExceptionOfType(MBeanExportException.class).as("NotificationListener on a non-existent MBean").isThrownBy(() -> + start(exporter)) + .satisfies(ex -> assertThat(ex.contains(InstanceNotFoundException.class))); + } + + @Test + void testWithSuppliedMBeanServer() throws Exception { + MBeanExporter exporter = new MBeanExporter(); + exporter.setBeans(getBeanMap()); + exporter.setServer(server); + try { + start(exporter); + assertIsRegistered("The bean was not registered with the MBeanServer", + ObjectNameManager.getInstance(OBJECT_NAME)); + } + finally { + exporter.destroy(); + } + } + + @Test + void testUserCreatedMBeanRegWithDynamicMBean() throws Exception { + Map map = new HashMap<>(); + map.put("spring:name=dynBean", new TestDynamicMBean()); + + InvokeDetectAssembler asm = new InvokeDetectAssembler(); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(server); + exporter.setBeans(map); + exporter.setAssembler(asm); + + try { + start(exporter); + Object name = server.getAttribute(ObjectNameManager.getInstance("spring:name=dynBean"), "Name"); + assertThat(name).as("The name attribute is incorrect").isEqualTo("Rob Harrop"); + assertThat(asm.invoked).as("Assembler should not have been invoked").isFalse(); + } + finally { + exporter.destroy(); + } + } + + @Test + void testAutodetectMBeans() throws Exception { + try (ConfigurableApplicationContext ctx = load("autodetectMBeans.xml")) { + ctx.getBean("exporter"); + MBeanServer server = ctx.getBean("server", MBeanServer.class); + ObjectInstance instance = server.getObjectInstance(ObjectNameManager.getInstance("spring:mbean=true")); + assertThat(instance).isNotNull(); + instance = server.getObjectInstance(ObjectNameManager.getInstance("spring:mbean2=true")); + assertThat(instance).isNotNull(); + instance = server.getObjectInstance(ObjectNameManager.getInstance("spring:mbean3=true")); + assertThat(instance).isNotNull(); + } + } + + @Test + void testAutodetectWithExclude() throws Exception { + try (ConfigurableApplicationContext ctx = load("autodetectMBeans.xml")) { + ctx.getBean("exporter"); + MBeanServer server = ctx.getBean("server", MBeanServer.class); + ObjectInstance instance = server.getObjectInstance(ObjectNameManager.getInstance("spring:mbean=true")); + assertThat(instance).isNotNull(); + + assertThatExceptionOfType(InstanceNotFoundException.class).isThrownBy(() -> + server.getObjectInstance(ObjectNameManager.getInstance("spring:mbean=false"))); + } + } + + @Test + void testAutodetectLazyMBeans() throws Exception { + try (ConfigurableApplicationContext ctx = load("autodetectLazyMBeans.xml")) { + ctx.getBean("exporter"); + MBeanServer server = ctx.getBean("server", MBeanServer.class); + + ObjectName oname = ObjectNameManager.getInstance("spring:mbean=true"); + assertThat(server.getObjectInstance(oname)).isNotNull(); + String name = (String) server.getAttribute(oname, "Name"); + assertThat(name).as("Invalid name returned").isEqualTo("Rob Harrop"); + + oname = ObjectNameManager.getInstance("spring:mbean=another"); + assertThat(server.getObjectInstance(oname)).isNotNull(); + name = (String) server.getAttribute(oname, "Name"); + assertThat(name).as("Invalid name returned").isEqualTo("Juergen Hoeller"); + } + } + + @Test + void testAutodetectNoMBeans() throws Exception { + try (ConfigurableApplicationContext ctx = load("autodetectNoMBeans.xml")) { + ctx.getBean("exporter"); + } + } + + @Test + void testWithMBeanExporterListeners() throws Exception { + MockMBeanExporterListener listener1 = new MockMBeanExporterListener(); + MockMBeanExporterListener listener2 = new MockMBeanExporterListener(); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setBeans(getBeanMap()); + exporter.setServer(server); + exporter.setListeners(listener1, listener2); + start(exporter); + exporter.destroy(); + + assertListener(listener1); + assertListener(listener2); + } + + @Test + void testExportJdkProxy() throws Exception { + JmxTestBean bean = new JmxTestBean(); + bean.setName("Rob Harrop"); + + ProxyFactory factory = new ProxyFactory(); + factory.setTarget(bean); + factory.addAdvice(new NopInterceptor()); + factory.setInterfaces(IJmxTestBean.class); + + IJmxTestBean proxy = (IJmxTestBean) factory.getProxy(); + String name = "bean:mmm=whatever"; + + Map beans = new HashMap<>(); + beans.put(name, proxy); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(server); + exporter.setBeans(beans); + exporter.registerBeans(); + + ObjectName oname = ObjectName.getInstance(name); + Object nameValue = server.getAttribute(oname, "Name"); + assertThat(nameValue).isEqualTo("Rob Harrop"); + } + + @Test + void testSelfNaming() throws Exception { + ObjectName objectName = ObjectNameManager.getInstance(OBJECT_NAME); + SelfNamingTestBean testBean = new SelfNamingTestBean(); + testBean.setObjectName(objectName); + + Map beans = new HashMap<>(); + beans.put("foo", testBean); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(server); + exporter.setBeans(beans); + + start(exporter); + + ObjectInstance instance = server.getObjectInstance(objectName); + assertThat(instance).isNotNull(); + } + + @Test + void testRegisterIgnoreExisting() throws Exception { + ObjectName objectName = ObjectNameManager.getInstance(OBJECT_NAME); + + Person preRegistered = new Person(); + preRegistered.setName("Rob Harrop"); + + server.registerMBean(preRegistered, objectName); + + Person springRegistered = new Person(); + springRegistered.setName("Sally Greenwood"); + + String objectName2 = "spring:test=equalBean"; + + Map beans = new HashMap<>(); + beans.put(objectName.toString(), springRegistered); + beans.put(objectName2, springRegistered); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(server); + exporter.setBeans(beans); + exporter.setRegistrationPolicy(RegistrationPolicy.IGNORE_EXISTING); + + start(exporter); + + ObjectInstance instance = server.getObjectInstance(objectName); + assertThat(instance).isNotNull(); + ObjectInstance instance2 = server.getObjectInstance(new ObjectName(objectName2)); + assertThat(instance2).isNotNull(); + + // should still be the first bean with name Rob Harrop + assertThat(server.getAttribute(objectName, "Name")).isEqualTo("Rob Harrop"); + } + + @Test + void testRegisterReplaceExisting() throws Exception { + ObjectName objectName = ObjectNameManager.getInstance(OBJECT_NAME); + + Person preRegistered = new Person(); + preRegistered.setName("Rob Harrop"); + + server.registerMBean(preRegistered, objectName); + + Person springRegistered = new Person(); + springRegistered.setName("Sally Greenwood"); + + Map beans = new HashMap<>(); + beans.put(objectName.toString(), springRegistered); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(server); + exporter.setBeans(beans); + exporter.setRegistrationPolicy(RegistrationPolicy.REPLACE_EXISTING); + + start(exporter); + + ObjectInstance instance = server.getObjectInstance(objectName); + assertThat(instance).isNotNull(); + + // should still be the new bean with name Sally Greenwood + assertThat(server.getAttribute(objectName, "Name")).isEqualTo("Sally Greenwood"); + } + + @Test + void testWithExposeClassLoader() throws Exception { + String name = "Rob Harrop"; + String otherName = "Juergen Hoeller"; + + JmxTestBean bean = new JmxTestBean(); + bean.setName(name); + ObjectName objectName = ObjectNameManager.getInstance("spring:type=Test"); + + Map beans = new HashMap<>(); + beans.put(objectName.toString(), bean); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(getServer()); + exporter.setBeans(beans); + exporter.setExposeManagedResourceClassLoader(true); + start(exporter); + + assertIsRegistered("Bean instance not registered", objectName); + + Object result = server.invoke(objectName, "add", new Object[] {new Integer(2), new Integer(3)}, new String[] { + int.class.getName(), int.class.getName()}); + + assertThat(new Integer(5)).as("Incorrect result return from add").isEqualTo(result); + assertThat(server.getAttribute(objectName, "Name")).as("Incorrect attribute value").isEqualTo(name); + + server.setAttribute(objectName, new Attribute("Name", otherName)); + assertThat(bean.getName()).as("Incorrect updated name.").isEqualTo(otherName); + } + + @Test + void testBonaFideMBeanIsNotExportedWhenAutodetectIsTotallyTurnedOff() throws Exception { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(Person.class); + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerBeanDefinition("^&_invalidObjectName_(*", builder.getBeanDefinition()); + String exportedBeanName = "export.me.please"; + factory.registerSingleton(exportedBeanName, new TestBean()); + + MBeanExporter exporter = new MBeanExporter(); + Map beansToExport = new HashMap<>(); + beansToExport.put(OBJECT_NAME, exportedBeanName); + exporter.setBeans(beansToExport); + exporter.setServer(getServer()); + exporter.setBeanFactory(factory); + exporter.setAutodetectMode(MBeanExporter.AUTODETECT_NONE); + // MBean has a bad ObjectName, so if said MBean is autodetected, an exception will be thrown... + start(exporter); + + } + + @Test + void testOnlyBonaFideMBeanIsExportedWhenAutodetectIsMBeanOnly() throws Exception { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(Person.class); + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerBeanDefinition(OBJECT_NAME, builder.getBeanDefinition()); + String exportedBeanName = "spring:type=TestBean"; + factory.registerSingleton(exportedBeanName, new TestBean()); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(getServer()); + exporter.setAssembler(new NamedBeanAutodetectCapableMBeanInfoAssemblerStub(exportedBeanName)); + exporter.setBeanFactory(factory); + exporter.setAutodetectMode(MBeanExporter.AUTODETECT_MBEAN); + start(exporter); + + assertIsRegistered("Bona fide MBean not autodetected in AUTODETECT_MBEAN mode", + ObjectNameManager.getInstance(OBJECT_NAME)); + assertIsNotRegistered("Bean autodetected and (only) AUTODETECT_MBEAN mode is on", + ObjectNameManager.getInstance(exportedBeanName)); + } + + @Test + void testBonaFideMBeanAndRegularBeanExporterWithAutodetectAll() throws Exception { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(Person.class); + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerBeanDefinition(OBJECT_NAME, builder.getBeanDefinition()); + String exportedBeanName = "spring:type=TestBean"; + factory.registerSingleton(exportedBeanName, new TestBean()); + String notToBeExportedBeanName = "spring:type=NotToBeExported"; + factory.registerSingleton(notToBeExportedBeanName, new TestBean()); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(getServer()); + exporter.setAssembler(new NamedBeanAutodetectCapableMBeanInfoAssemblerStub(exportedBeanName)); + exporter.setBeanFactory(factory); + exporter.setAutodetectMode(MBeanExporter.AUTODETECT_ALL); + start(exporter); + assertIsRegistered("Bona fide MBean not autodetected in (AUTODETECT_ALL) mode", + ObjectNameManager.getInstance(OBJECT_NAME)); + assertIsRegistered("Bean not autodetected in (AUTODETECT_ALL) mode", + ObjectNameManager.getInstance(exportedBeanName)); + assertIsNotRegistered("Bean autodetected and did not satisfy the autodetect info assembler", + ObjectNameManager.getInstance(notToBeExportedBeanName)); + } + + @Test + void testBonaFideMBeanIsNotExportedWithAutodetectAssembler() throws Exception { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(Person.class); + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerBeanDefinition(OBJECT_NAME, builder.getBeanDefinition()); + String exportedBeanName = "spring:type=TestBean"; + factory.registerSingleton(exportedBeanName, new TestBean()); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(getServer()); + exporter.setAssembler(new NamedBeanAutodetectCapableMBeanInfoAssemblerStub(exportedBeanName)); + exporter.setBeanFactory(factory); + exporter.setAutodetectMode(MBeanExporter.AUTODETECT_ASSEMBLER); + start(exporter); + assertIsNotRegistered("Bona fide MBean was autodetected in AUTODETECT_ASSEMBLER mode - must not have been", + ObjectNameManager.getInstance(OBJECT_NAME)); + assertIsRegistered("Bean not autodetected in AUTODETECT_ASSEMBLER mode", + ObjectNameManager.getInstance(exportedBeanName)); + } + + /** + * Want to ensure that said MBean is not exported twice. + */ + @Test + void testBonaFideMBeanExplicitlyExportedAndAutodetectionIsOn() throws Exception { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(Person.class); + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerBeanDefinition(OBJECT_NAME, builder.getBeanDefinition()); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(getServer()); + Map beansToExport = new HashMap<>(); + beansToExport.put(OBJECT_NAME, OBJECT_NAME); + exporter.setBeans(beansToExport); + exporter.setAssembler(new NamedBeanAutodetectCapableMBeanInfoAssemblerStub(OBJECT_NAME)); + exporter.setBeanFactory(factory); + exporter.setAutodetectMode(MBeanExporter.AUTODETECT_ASSEMBLER); + start(exporter); + assertIsRegistered("Explicitly exported bona fide MBean obviously not exported.", + ObjectNameManager.getInstance(OBJECT_NAME)); + } + + @Test + void testSetAutodetectModeToOutOfRangeNegativeValue() { + MBeanExporter exporter = new MBeanExporter(); + assertThatIllegalArgumentException().isThrownBy(() -> + exporter.setAutodetectMode(-1)); + } + + @Test + void testSetAutodetectModeToOutOfRangePositiveValue() { + MBeanExporter exporter = new MBeanExporter(); + assertThatIllegalArgumentException().isThrownBy(() -> + exporter.setAutodetectMode(5)); + } + + @Test + void testSetAutodetectModeNameToAnEmptyString() { + MBeanExporter exporter = new MBeanExporter(); + assertThatIllegalArgumentException().isThrownBy(() -> + exporter.setAutodetectModeName("")); + } + + @Test + void testSetAutodetectModeNameToAWhitespacedString() { + MBeanExporter exporter = new MBeanExporter(); + assertThatIllegalArgumentException().isThrownBy(() -> + exporter.setAutodetectModeName(" \t")); + } + + @Test + void testSetAutodetectModeNameToARubbishValue() { + MBeanExporter exporter = new MBeanExporter(); + assertThatIllegalArgumentException().isThrownBy(() -> + exporter.setAutodetectModeName("That Hansel is... *sssooo* hot right now!")); + } + + @Test + void testNotRunningInBeanFactoryAndPassedBeanNameToExport() throws Exception { + MBeanExporter exporter = new MBeanExporter(); + Map beans = new HashMap<>(); + beans.put(OBJECT_NAME, "beanName"); + exporter.setBeans(beans); + assertThatExceptionOfType(MBeanExportException.class).isThrownBy(() -> + start(exporter)); + } + + @Test + void testNotRunningInBeanFactoryAndAutodetectionIsOn() throws Exception { + MBeanExporter exporter = new MBeanExporter(); + exporter.setAutodetectMode(MBeanExporter.AUTODETECT_ALL); + assertThatExceptionOfType(MBeanExportException.class).isThrownBy(() -> + start(exporter)); + } + + @Test // SPR-2158 + void testMBeanIsNotUnregisteredSpuriouslyIfSomeExternalProcessHasUnregisteredMBean() throws Exception { + MBeanExporter exporter = new MBeanExporter(); + exporter.setBeans(getBeanMap()); + exporter.setServer(this.server); + MockMBeanExporterListener listener = new MockMBeanExporterListener(); + exporter.setListeners(listener); + start(exporter); + assertIsRegistered("The bean was not registered with the MBeanServer", + ObjectNameManager.getInstance(OBJECT_NAME)); + + this.server.unregisterMBean(new ObjectName(OBJECT_NAME)); + exporter.destroy(); + assertThat(listener.getUnregistered().size()).as("Listener should not have been invoked (MBean previously unregistered by external agent)").isEqualTo(0); + } + + @Test // SPR-3302 + void testBeanNameCanBeUsedInNotificationListenersMap() throws Exception { + String beanName = "charlesDexterWard"; + BeanDefinitionBuilder testBean = BeanDefinitionBuilder.rootBeanDefinition(JmxTestBean.class); + + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerBeanDefinition(beanName, testBean.getBeanDefinition()); + factory.preInstantiateSingletons(); + Object testBeanInstance = factory.getBean(beanName); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(getServer()); + Map beansToExport = new HashMap<>(); + beansToExport.put("test:what=ever", testBeanInstance); + exporter.setBeans(beansToExport); + exporter.setBeanFactory(factory); + StubNotificationListener listener = new StubNotificationListener(); + exporter.setNotificationListenerMappings(Collections.singletonMap(beanName, listener)); + + start(exporter); + } + + @Test + void testWildcardCanBeUsedInNotificationListenersMap() throws Exception { + String beanName = "charlesDexterWard"; + BeanDefinitionBuilder testBean = BeanDefinitionBuilder.rootBeanDefinition(JmxTestBean.class); + + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerBeanDefinition(beanName, testBean.getBeanDefinition()); + factory.preInstantiateSingletons(); + Object testBeanInstance = factory.getBean(beanName); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(getServer()); + Map beansToExport = new HashMap<>(); + beansToExport.put("test:what=ever", testBeanInstance); + exporter.setBeans(beansToExport); + exporter.setBeanFactory(factory); + StubNotificationListener listener = new StubNotificationListener(); + exporter.setNotificationListenerMappings(Collections.singletonMap("*", listener)); + + start(exporter); + } + + @Test // SPR-3625 + void testMBeanIsUnregisteredForRuntimeExceptionDuringInitialization() throws Exception { + BeanDefinitionBuilder builder1 = BeanDefinitionBuilder.rootBeanDefinition(Person.class); + BeanDefinitionBuilder builder2 = BeanDefinitionBuilder + .rootBeanDefinition(RuntimeExceptionThrowingConstructorBean.class); + + String objectName1 = "spring:test=bean1"; + String objectName2 = "spring:test=bean2"; + + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerBeanDefinition(objectName1, builder1.getBeanDefinition()); + factory.registerBeanDefinition(objectName2, builder2.getBeanDefinition()); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(getServer()); + Map beansToExport = new HashMap<>(); + beansToExport.put(objectName1, objectName1); + beansToExport.put(objectName2, objectName2); + exporter.setBeans(beansToExport); + exporter.setBeanFactory(factory); + + assertThatExceptionOfType(RuntimeException.class).as("failed during creation of RuntimeExceptionThrowingConstructorBean").isThrownBy(() -> + start(exporter)); + + assertIsNotRegistered("Must have unregistered all previously registered MBeans due to RuntimeException", + ObjectNameManager.getInstance(objectName1)); + assertIsNotRegistered("Must have never registered this MBean due to RuntimeException", + ObjectNameManager.getInstance(objectName2)); + } + + @Test + void testIgnoreBeanName() throws MalformedObjectNameException { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + String firstBeanName = "spring:type=TestBean"; + factory.registerSingleton(firstBeanName, new TestBean("test")); + String secondBeanName = "spring:type=TestBean2"; + factory.registerSingleton(secondBeanName, new TestBean("test2")); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(getServer()); + exporter.setAssembler(new NamedBeanAutodetectCapableMBeanInfoAssemblerStub(firstBeanName, secondBeanName)); + exporter.setBeanFactory(factory); + exporter.setAutodetectMode(MBeanExporter.AUTODETECT_ALL); + exporter.addExcludedBean(secondBeanName); + + start(exporter); + assertIsRegistered("Bean not autodetected in (AUTODETECT_ALL) mode", + ObjectNameManager.getInstance(firstBeanName)); + assertIsNotRegistered("Bean should have been excluded", + ObjectNameManager.getInstance(secondBeanName)); + } + + @Test + void testRegisterFactoryBean() throws MalformedObjectNameException { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerBeanDefinition("spring:type=FactoryBean", new RootBeanDefinition(ProperSomethingFactoryBean.class)); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(getServer()); + exporter.setBeanFactory(factory); + exporter.setAutodetectMode(MBeanExporter.AUTODETECT_ALL); + + start(exporter); + assertIsRegistered("Non-null FactoryBean object registered", + ObjectNameManager.getInstance("spring:type=FactoryBean")); + } + + @Test + void testIgnoreNullObjectFromFactoryBean() throws MalformedObjectNameException { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerBeanDefinition("spring:type=FactoryBean", new RootBeanDefinition(NullSomethingFactoryBean.class)); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(getServer()); + exporter.setBeanFactory(factory); + exporter.setAutodetectMode(MBeanExporter.AUTODETECT_ALL); + + start(exporter); + assertIsNotRegistered("Null FactoryBean object not registered", + ObjectNameManager.getInstance("spring:type=FactoryBean")); + } + + + private ConfigurableApplicationContext load(String context) { + return new ClassPathXmlApplicationContext(context, getClass()); + } + + private Map getBeanMap() { + Map map = new HashMap<>(); + map.put(OBJECT_NAME, new JmxTestBean()); + return map; + } + + private void assertListener(MockMBeanExporterListener listener) throws MalformedObjectNameException { + ObjectName desired = ObjectNameManager.getInstance(OBJECT_NAME); + assertThat(listener.getRegistered().size()).as("Incorrect number of registrations").isEqualTo(1); + assertThat(listener.getUnregistered().size()).as("Incorrect number of unregistrations").isEqualTo(1); + assertThat(listener.getRegistered().get(0)).as("Incorrect ObjectName in register").isEqualTo(desired); + assertThat(listener.getUnregistered().get(0)).as("Incorrect ObjectName in unregister").isEqualTo(desired); + } + + + private static class InvokeDetectAssembler implements MBeanInfoAssembler { + + private boolean invoked = false; + + @Override + public ModelMBeanInfo getMBeanInfo(Object managedResource, String beanKey) throws JMException { + invoked = true; + return null; + } + } + + + private static class MockMBeanExporterListener implements MBeanExporterListener { + + private List registered = new ArrayList<>(); + + private List unregistered = new ArrayList<>(); + + @Override + public void mbeanRegistered(ObjectName objectName) { + registered.add(objectName); + } + + @Override + public void mbeanUnregistered(ObjectName objectName) { + unregistered.add(objectName); + } + + public List getRegistered() { + return registered; + } + + public List getUnregistered() { + return unregistered; + } + } + + + private static class SelfNamingTestBean implements SelfNaming { + + private ObjectName objectName; + + public void setObjectName(ObjectName objectName) { + this.objectName = objectName; + } + + @Override + public ObjectName getObjectName() throws MalformedObjectNameException { + return this.objectName; + } + } + + + public static interface PersonMBean { + + String getName(); + } + + + public static class Person implements PersonMBean { + + private String name; + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + + public static final class StubNotificationListener implements NotificationListener { + + private List notifications = new ArrayList<>(); + + @Override + public void handleNotification(Notification notification, Object handback) { + this.notifications.add(notification); + } + + public List getNotifications() { + return this.notifications; + } + } + + + private static class RuntimeExceptionThrowingConstructorBean { + + @SuppressWarnings("unused") + public RuntimeExceptionThrowingConstructorBean() { + throw new RuntimeException(); + } + } + + + private static final class NamedBeanAutodetectCapableMBeanInfoAssemblerStub extends + SimpleReflectiveMBeanInfoAssembler implements AutodetectCapableMBeanInfoAssembler { + + private Collection namedBeans; + + public NamedBeanAutodetectCapableMBeanInfoAssemblerStub(String... namedBeans) { + this.namedBeans = Arrays.asList(namedBeans); + } + + @Override + public boolean includeBean(Class beanClass, String beanName) { + return this.namedBeans.contains(beanName); + } + } + + + public interface SomethingMBean {} + + public static class Something implements SomethingMBean {} + + + public static class ProperSomethingFactoryBean implements FactoryBean { + + @Override public Something getObject() { + return new Something(); + } + + @Override public Class getObjectType() { + return Something.class; + } + + @Override public boolean isSingleton() { + return true; + } + } + + + public static class NullSomethingFactoryBean implements FactoryBean { + + @Override public Something getObject() { + return null; + } + + @Override public Class getObjectType() { + return Something.class; + } + + @Override public boolean isSingleton() { + return true; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/NotificationListenerTests.java b/spring-context/src/test/java/org/springframework/jmx/export/NotificationListenerTests.java new file mode 100644 index 0000000..d38eb83 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/NotificationListenerTests.java @@ -0,0 +1,510 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export; + +import java.util.HashMap; +import java.util.Map; + +import javax.management.Attribute; +import javax.management.AttributeChangeNotification; +import javax.management.MalformedObjectNameException; +import javax.management.Notification; +import javax.management.NotificationFilter; +import javax.management.NotificationListener; +import javax.management.ObjectName; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.jmx.AbstractMBeanServerTests; +import org.springframework.jmx.JmxTestBean; +import org.springframework.jmx.access.NotificationListenerRegistrar; +import org.springframework.jmx.export.naming.SelfNaming; +import org.springframework.jmx.support.ObjectNameManager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Rob Harrop + * @author Mark Fisher + * @author Sam Brannen + */ +public class NotificationListenerTests extends AbstractMBeanServerTests { + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Test + public void testRegisterNotificationListenerForMBean() throws Exception { + ObjectName objectName = ObjectName.getInstance("spring:name=Test"); + JmxTestBean bean = new JmxTestBean(); + + Map beans = new HashMap<>(); + beans.put(objectName.getCanonicalName(), bean); + + CountingAttributeChangeNotificationListener listener = new CountingAttributeChangeNotificationListener(); + + Map notificationListeners = new HashMap(); + notificationListeners.put(objectName, listener); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(server); + exporter.setBeans(beans); + exporter.setNotificationListenerMappings(notificationListeners); + start(exporter); + + // update the attribute + String attributeName = "Name"; + server.setAttribute(objectName, new Attribute(attributeName, "Rob Harrop")); + assertThat(listener.getCount(attributeName)).as("Listener not notified").isEqualTo(1); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + public void testRegisterNotificationListenerWithWildcard() throws Exception { + ObjectName objectName = ObjectName.getInstance("spring:name=Test"); + JmxTestBean bean = new JmxTestBean(); + + Map beans = new HashMap<>(); + beans.put(objectName.getCanonicalName(), bean); + + CountingAttributeChangeNotificationListener listener = new CountingAttributeChangeNotificationListener(); + + Map notificationListeners = new HashMap(); + notificationListeners.put("*", listener); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(server); + exporter.setBeans(beans); + exporter.setNotificationListenerMappings(notificationListeners); + start(exporter); + + // update the attribute + String attributeName = "Name"; + server.setAttribute(objectName, new Attribute(attributeName, "Rob Harrop")); + assertThat(listener.getCount(attributeName)).as("Listener not notified").isEqualTo(1); + } + + @Test + public void testRegisterNotificationListenerWithHandback() throws Exception { + String objectName = "spring:name=Test"; + JmxTestBean bean = new JmxTestBean(); + + Map beans = new HashMap<>(); + beans.put(objectName, bean); + + CountingAttributeChangeNotificationListener listener = new CountingAttributeChangeNotificationListener(); + Object handback = new Object(); + + NotificationListenerBean listenerBean = new NotificationListenerBean(); + listenerBean.setNotificationListener(listener); + listenerBean.setMappedObjectName("spring:name=Test"); + listenerBean.setHandback(handback); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(server); + exporter.setBeans(beans); + exporter.setNotificationListeners(new NotificationListenerBean[] { listenerBean }); + start(exporter); + + // update the attribute + String attributeName = "Name"; + server.setAttribute(ObjectNameManager.getInstance("spring:name=Test"), new Attribute(attributeName, + "Rob Harrop")); + + assertThat(listener.getCount(attributeName)).as("Listener not notified").isEqualTo(1); + assertThat(listener.getLastHandback(attributeName)).as("Handback object not transmitted correctly").isEqualTo(handback); + } + + @Test + public void testRegisterNotificationListenerForAllMBeans() throws Exception { + ObjectName objectName = ObjectName.getInstance("spring:name=Test"); + JmxTestBean bean = new JmxTestBean(); + + Map beans = new HashMap<>(); + beans.put(objectName.getCanonicalName(), bean); + + CountingAttributeChangeNotificationListener listener = new CountingAttributeChangeNotificationListener(); + + NotificationListenerBean listenerBean = new NotificationListenerBean(); + listenerBean.setNotificationListener(listener); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(server); + exporter.setBeans(beans); + exporter.setNotificationListeners(new NotificationListenerBean[] { listenerBean }); + start(exporter); + + // update the attribute + String attributeName = "Name"; + server.setAttribute(objectName, new Attribute(attributeName, "Rob Harrop")); + + assertThat(listener.getCount(attributeName)).as("Listener not notified").isEqualTo(1); + } + + @SuppressWarnings("serial") + @Test + public void testRegisterNotificationListenerWithFilter() throws Exception { + ObjectName objectName = ObjectName.getInstance("spring:name=Test"); + JmxTestBean bean = new JmxTestBean(); + + Map beans = new HashMap<>(); + beans.put(objectName.getCanonicalName(), bean); + + CountingAttributeChangeNotificationListener listener = new CountingAttributeChangeNotificationListener(); + + NotificationListenerBean listenerBean = new NotificationListenerBean(); + listenerBean.setNotificationListener(listener); + listenerBean.setNotificationFilter(new NotificationFilter() { + @Override + public boolean isNotificationEnabled(Notification notification) { + if (notification instanceof AttributeChangeNotification) { + AttributeChangeNotification changeNotification = (AttributeChangeNotification) notification; + return "Name".equals(changeNotification.getAttributeName()); + } + else { + return false; + } + } + }); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(server); + exporter.setBeans(beans); + exporter.setNotificationListeners(new NotificationListenerBean[] { listenerBean }); + start(exporter); + + // update the attributes + String nameAttribute = "Name"; + String ageAttribute = "Age"; + + server.setAttribute(objectName, new Attribute(nameAttribute, "Rob Harrop")); + server.setAttribute(objectName, new Attribute(ageAttribute, 90)); + + assertThat(listener.getCount(nameAttribute)).as("Listener not notified for Name").isEqualTo(1); + assertThat(listener.getCount(ageAttribute)).as("Listener incorrectly notified for Age").isEqualTo(0); + } + + @Test + public void testCreationWithNoNotificationListenerSet() { + assertThatIllegalArgumentException().as("no NotificationListener supplied").isThrownBy( + new NotificationListenerBean()::afterPropertiesSet); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + public void testRegisterNotificationListenerWithBeanNameAndBeanNameInBeansMap() throws Exception { + String beanName = "testBean"; + ObjectName objectName = ObjectName.getInstance("spring:name=Test"); + + SelfNamingTestBean testBean = new SelfNamingTestBean(); + testBean.setObjectName(objectName); + + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerSingleton(beanName, testBean); + + Map beans = new HashMap<>(); + beans.put(beanName, beanName); + + Map listenerMappings = new HashMap(); + CountingAttributeChangeNotificationListener listener = new CountingAttributeChangeNotificationListener(); + listenerMappings.put(beanName, listener); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(server); + exporter.setBeans(beans); + exporter.setNotificationListenerMappings(listenerMappings); + exporter.setBeanFactory(factory); + start(exporter); + assertIsRegistered("Should have registered MBean", objectName); + + server.setAttribute(objectName, new Attribute("Age", 77)); + assertThat(listener.getCount("Age")).as("Listener not notified").isEqualTo(1); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + public void testRegisterNotificationListenerWithBeanNameAndBeanInstanceInBeansMap() throws Exception { + String beanName = "testBean"; + ObjectName objectName = ObjectName.getInstance("spring:name=Test"); + + SelfNamingTestBean testBean = new SelfNamingTestBean(); + testBean.setObjectName(objectName); + + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerSingleton(beanName, testBean); + + Map beans = new HashMap<>(); + beans.put(beanName, testBean); + + Map listenerMappings = new HashMap(); + CountingAttributeChangeNotificationListener listener = new CountingAttributeChangeNotificationListener(); + listenerMappings.put(beanName, listener); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(server); + exporter.setBeans(beans); + exporter.setNotificationListenerMappings(listenerMappings); + exporter.setBeanFactory(factory); + start(exporter); + assertIsRegistered("Should have registered MBean", objectName); + + server.setAttribute(objectName, new Attribute("Age", 77)); + assertThat(listener.getCount("Age")).as("Listener not notified").isEqualTo(1); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + public void testRegisterNotificationListenerWithBeanNameBeforeObjectNameMappedToSameBeanInstance() throws Exception { + String beanName = "testBean"; + ObjectName objectName = ObjectName.getInstance("spring:name=Test"); + + SelfNamingTestBean testBean = new SelfNamingTestBean(); + testBean.setObjectName(objectName); + + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerSingleton(beanName, testBean); + + Map beans = new HashMap<>(); + beans.put(beanName, testBean); + + Map listenerMappings = new HashMap(); + CountingAttributeChangeNotificationListener listener = new CountingAttributeChangeNotificationListener(); + listenerMappings.put(beanName, listener); + listenerMappings.put(objectName, listener); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(server); + exporter.setBeans(beans); + exporter.setNotificationListenerMappings(listenerMappings); + exporter.setBeanFactory(factory); + start(exporter); + assertIsRegistered("Should have registered MBean", objectName); + + server.setAttribute(objectName, new Attribute("Age", 77)); + assertThat(listener.getCount("Age")).as("Listener should have been notified exactly once").isEqualTo(1); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + public void testRegisterNotificationListenerWithObjectNameBeforeBeanNameMappedToSameBeanInstance() throws Exception { + String beanName = "testBean"; + ObjectName objectName = ObjectName.getInstance("spring:name=Test"); + + SelfNamingTestBean testBean = new SelfNamingTestBean(); + testBean.setObjectName(objectName); + + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerSingleton(beanName, testBean); + + Map beans = new HashMap<>(); + beans.put(beanName, testBean); + + Map listenerMappings = new HashMap(); + CountingAttributeChangeNotificationListener listener = new CountingAttributeChangeNotificationListener(); + listenerMappings.put(objectName, listener); + listenerMappings.put(beanName, listener); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(server); + exporter.setBeans(beans); + exporter.setNotificationListenerMappings(listenerMappings); + exporter.setBeanFactory(factory); + start(exporter); + assertIsRegistered("Should have registered MBean", objectName); + + server.setAttribute(objectName, new Attribute("Age", 77)); + assertThat(listener.getCount("Age")).as("Listener should have been notified exactly once").isEqualTo(1); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + public void testRegisterNotificationListenerWithTwoBeanNamesMappedToDifferentBeanInstances() throws Exception { + String beanName1 = "testBean1"; + String beanName2 = "testBean2"; + + ObjectName objectName1 = ObjectName.getInstance("spring:name=Test1"); + ObjectName objectName2 = ObjectName.getInstance("spring:name=Test2"); + + SelfNamingTestBean testBean1 = new SelfNamingTestBean(); + testBean1.setObjectName(objectName1); + + SelfNamingTestBean testBean2 = new SelfNamingTestBean(); + testBean2.setObjectName(objectName2); + + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerSingleton(beanName1, testBean1); + factory.registerSingleton(beanName2, testBean2); + + Map beans = new HashMap<>(); + beans.put(beanName1, testBean1); + beans.put(beanName2, testBean2); + + Map listenerMappings = new HashMap(); + CountingAttributeChangeNotificationListener listener = new CountingAttributeChangeNotificationListener(); + listenerMappings.put(beanName1, listener); + listenerMappings.put(beanName2, listener); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(server); + exporter.setBeans(beans); + exporter.setNotificationListenerMappings(listenerMappings); + exporter.setBeanFactory(factory); + start(exporter); + assertIsRegistered("Should have registered MBean", objectName1); + assertIsRegistered("Should have registered MBean", objectName2); + + server.setAttribute(ObjectNameManager.getInstance(objectName1), new Attribute("Age", 77)); + assertThat(listener.getCount("Age")).as("Listener not notified for testBean1").isEqualTo(1); + + server.setAttribute(ObjectNameManager.getInstance(objectName2), new Attribute("Age", 33)); + assertThat(listener.getCount("Age")).as("Listener not notified for testBean2").isEqualTo(2); + } + + @Test + public void testNotificationListenerRegistrar() throws Exception { + ObjectName objectName = ObjectName.getInstance("spring:name=Test"); + JmxTestBean bean = new JmxTestBean(); + + Map beans = new HashMap<>(); + beans.put(objectName.getCanonicalName(), bean); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(server); + exporter.setBeans(beans); + start(exporter); + + CountingAttributeChangeNotificationListener listener = new CountingAttributeChangeNotificationListener(); + + NotificationListenerRegistrar registrar = new NotificationListenerRegistrar(); + registrar.setServer(server); + registrar.setNotificationListener(listener); + registrar.setMappedObjectName(objectName); + registrar.afterPropertiesSet(); + + // update the attribute + String attributeName = "Name"; + server.setAttribute(objectName, new Attribute(attributeName, "Rob Harrop")); + assertThat(listener.getCount(attributeName)).as("Listener not notified").isEqualTo(1); + + registrar.destroy(); + + // try to update the attribute again + server.setAttribute(objectName, new Attribute(attributeName, "Rob Harrop")); + assertThat(listener.getCount(attributeName)).as("Listener notified after destruction").isEqualTo(1); + } + + @Test + public void testNotificationListenerRegistrarWithMultipleNames() throws Exception { + ObjectName objectName = ObjectName.getInstance("spring:name=Test"); + ObjectName objectName2 = ObjectName.getInstance("spring:name=Test2"); + JmxTestBean bean = new JmxTestBean(); + JmxTestBean bean2 = new JmxTestBean(); + + Map beans = new HashMap<>(); + beans.put(objectName.getCanonicalName(), bean); + beans.put(objectName2.getCanonicalName(), bean2); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setServer(server); + exporter.setBeans(beans); + start(exporter); + + CountingAttributeChangeNotificationListener listener = new CountingAttributeChangeNotificationListener(); + + NotificationListenerRegistrar registrar = new NotificationListenerRegistrar(); + registrar.setServer(server); + registrar.setNotificationListener(listener); + //registrar.setMappedObjectNames(new Object[] {objectName, objectName2}); + registrar.setMappedObjectNames("spring:name=Test", "spring:name=Test2"); + registrar.afterPropertiesSet(); + + // update the attribute + String attributeName = "Name"; + server.setAttribute(objectName, new Attribute(attributeName, "Rob Harrop")); + assertThat(listener.getCount(attributeName)).as("Listener not notified").isEqualTo(1); + + registrar.destroy(); + + // try to update the attribute again + server.setAttribute(objectName, new Attribute(attributeName, "Rob Harrop")); + assertThat(listener.getCount(attributeName)).as("Listener notified after destruction").isEqualTo(1); + } + + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static class CountingAttributeChangeNotificationListener implements NotificationListener { + + private Map attributeCounts = new HashMap(); + + private Map attributeHandbacks = new HashMap(); + + @Override + public void handleNotification(Notification notification, Object handback) { + if (notification instanceof AttributeChangeNotification) { + AttributeChangeNotification attNotification = (AttributeChangeNotification) notification; + String attributeName = attNotification.getAttributeName(); + + Integer currentCount = (Integer) this.attributeCounts.get(attributeName); + + if (currentCount != null) { + int count = currentCount.intValue() + 1; + this.attributeCounts.put(attributeName, count); + } + else { + this.attributeCounts.put(attributeName, 1); + } + + this.attributeHandbacks.put(attributeName, handback); + } + } + + public int getCount(String attribute) { + Integer count = (Integer) this.attributeCounts.get(attribute); + return (count == null) ? 0 : count.intValue(); + } + + public Object getLastHandback(String attributeName) { + return this.attributeHandbacks.get(attributeName); + } + } + + + public static class SelfNamingTestBean implements SelfNaming { + + private ObjectName objectName; + + private int age; + + public void setObjectName(ObjectName objectName) { + this.objectName = objectName; + } + + @Override + public ObjectName getObjectName() throws MalformedObjectNameException { + return this.objectName; + } + + public void setAge(int age) { + this.age = age; + } + + public int getAge() { + return this.age; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/NotificationPublisherTests.java b/spring-context/src/test/java/org/springframework/jmx/export/NotificationPublisherTests.java new file mode 100644 index 0000000..786c608 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/NotificationPublisherTests.java @@ -0,0 +1,223 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export; + +import javax.management.Attribute; +import javax.management.AttributeList; +import javax.management.AttributeNotFoundException; +import javax.management.DynamicMBean; +import javax.management.InvalidAttributeValueException; +import javax.management.MBeanAttributeInfo; +import javax.management.MBeanConstructorInfo; +import javax.management.MBeanException; +import javax.management.MBeanInfo; +import javax.management.MBeanNotificationInfo; +import javax.management.MBeanOperationInfo; +import javax.management.Notification; +import javax.management.NotificationBroadcasterSupport; +import javax.management.NotificationListener; +import javax.management.ReflectionException; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.jmx.AbstractMBeanServerTests; +import org.springframework.jmx.export.notification.NotificationPublisher; +import org.springframework.jmx.export.notification.NotificationPublisherAware; +import org.springframework.jmx.support.ObjectNameManager; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for the Spring JMX {@link NotificationPublisher} functionality. + * + * @author Rob Harrop + * @author Juergen Hoeller + */ +public class NotificationPublisherTests extends AbstractMBeanServerTests { + + private CountingNotificationListener listener = new CountingNotificationListener(); + + @Test + public void testSimpleBean() throws Exception { + // start the MBeanExporter + ConfigurableApplicationContext ctx = loadContext("org/springframework/jmx/export/notificationPublisherTests.xml"); + this.server.addNotificationListener(ObjectNameManager.getInstance("spring:type=Publisher"), listener, null, + null); + + MyNotificationPublisher publisher = (MyNotificationPublisher) ctx.getBean("publisher"); + assertThat(publisher.getNotificationPublisher()).as("NotificationPublisher should not be null").isNotNull(); + publisher.sendNotification(); + assertThat(listener.count).as("Notification not sent").isEqualTo(1); + } + + @Test + public void testSimpleBeanRegisteredManually() throws Exception { + // start the MBeanExporter + ConfigurableApplicationContext ctx = loadContext("org/springframework/jmx/export/notificationPublisherTests.xml"); + MBeanExporter exporter = (MBeanExporter) ctx.getBean("exporter"); + MyNotificationPublisher publisher = new MyNotificationPublisher(); + exporter.registerManagedResource(publisher, ObjectNameManager.getInstance("spring:type=Publisher2")); + this.server.addNotificationListener(ObjectNameManager.getInstance("spring:type=Publisher2"), listener, null, + null); + + assertThat(publisher.getNotificationPublisher()).as("NotificationPublisher should not be null").isNotNull(); + publisher.sendNotification(); + assertThat(listener.count).as("Notification not sent").isEqualTo(1); + } + + @Test + public void testMBean() throws Exception { + // start the MBeanExporter + ConfigurableApplicationContext ctx = loadContext("org/springframework/jmx/export/notificationPublisherTests.xml"); + this.server.addNotificationListener(ObjectNameManager.getInstance("spring:type=PublisherMBean"), listener, + null, null); + + MyNotificationPublisherMBean publisher = (MyNotificationPublisherMBean) ctx.getBean("publisherMBean"); + publisher.sendNotification(); + assertThat(listener.count).as("Notification not sent").isEqualTo(1); + } + + /* + @Test + public void testStandardMBean() throws Exception { + // start the MBeanExporter + ApplicationContext ctx = new ClassPathXmlApplicationContext("org/springframework/jmx/export/notificationPublisherTests.xml"); + this.server.addNotificationListener(ObjectNameManager.getInstance("spring:type=PublisherStandardMBean"), listener, null, null); + + MyNotificationPublisherStandardMBean publisher = (MyNotificationPublisherStandardMBean) ctx.getBean("publisherStandardMBean"); + publisher.sendNotification(); + assertEquals("Notification not sent", 1, listener.count); + } + */ + + @Test + public void testLazyInit() throws Exception { + // start the MBeanExporter + ConfigurableApplicationContext ctx = loadContext("org/springframework/jmx/export/notificationPublisherLazyTests.xml"); + assertThat(ctx.getBeanFactory().containsSingleton("publisher")).as("Should not have instantiated the bean yet").isFalse(); + + // need to touch the MBean proxy + server.getAttribute(ObjectNameManager.getInstance("spring:type=Publisher"), "Name"); + this.server.addNotificationListener(ObjectNameManager.getInstance("spring:type=Publisher"), listener, null, + null); + + MyNotificationPublisher publisher = (MyNotificationPublisher) ctx.getBean("publisher"); + assertThat(publisher.getNotificationPublisher()).as("NotificationPublisher should not be null").isNotNull(); + publisher.sendNotification(); + assertThat(listener.count).as("Notification not sent").isEqualTo(1); + } + + private static class CountingNotificationListener implements NotificationListener { + + private int count; + + private Notification lastNotification; + + @Override + public void handleNotification(Notification notification, Object handback) { + this.lastNotification = notification; + this.count++; + } + + @SuppressWarnings("unused") + public int getCount() { + return count; + } + + @SuppressWarnings("unused") + public Notification getLastNotification() { + return lastNotification; + } + } + + public static class MyNotificationPublisher implements NotificationPublisherAware { + + private NotificationPublisher notificationPublisher; + + @Override + public void setNotificationPublisher(NotificationPublisher notificationPublisher) { + this.notificationPublisher = notificationPublisher; + } + + public NotificationPublisher getNotificationPublisher() { + return notificationPublisher; + } + + public void sendNotification() { + this.notificationPublisher.sendNotification(new Notification("test", this, 1)); + } + + public String getName() { + return "Rob Harrop"; + } + } + + public static class MyNotificationPublisherMBean extends NotificationBroadcasterSupport implements DynamicMBean { + + @Override + public Object getAttribute(String attribute) throws AttributeNotFoundException, MBeanException, + ReflectionException { + return null; + } + + @Override + public void setAttribute(Attribute attribute) throws AttributeNotFoundException, + InvalidAttributeValueException, MBeanException, ReflectionException { + } + + @Override + public AttributeList getAttributes(String[] attributes) { + return null; + } + + @Override + public AttributeList setAttributes(AttributeList attributes) { + return null; + } + + @Override + public Object invoke(String actionName, Object[] params, String[] signature) throws MBeanException, + ReflectionException { + return null; + } + + @Override + public MBeanInfo getMBeanInfo() { + return new MBeanInfo(MyNotificationPublisherMBean.class.getName(), "", new MBeanAttributeInfo[0], + new MBeanConstructorInfo[0], new MBeanOperationInfo[0], new MBeanNotificationInfo[0]); + } + + public void sendNotification() { + sendNotification(new Notification("test", this, 1)); + } + } + + public static class MyNotificationPublisherStandardMBean extends NotificationBroadcasterSupport implements MyMBean { + + @Override + public void sendNotification() { + sendNotification(new Notification("test", this, 1)); + } + } + + public interface MyMBean { + + void sendNotification(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/PropertyPlaceholderConfigurerTests.java b/spring-context/src/test/java/org/springframework/jmx/export/PropertyPlaceholderConfigurerTests.java new file mode 100644 index 0000000..f6008fe --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/PropertyPlaceholderConfigurerTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export; + +import javax.management.ObjectName; + +import org.junit.jupiter.api.Test; + +import org.springframework.jmx.AbstractJmxTests; +import org.springframework.jmx.IJmxTestBean; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + * @author Chris Beams + */ +class PropertyPlaceholderConfigurerTests extends AbstractJmxTests { + + @Override + protected String getApplicationContextPath() { + return "org/springframework/jmx/export/propertyPlaceholderConfigurer.xml"; + } + + @Test + void propertiesReplacedInApplicationContext() { + IJmxTestBean bean = getContext().getBean("testBean", IJmxTestBean.class); + + assertThat(bean.getName()).as("Name").isEqualTo("Rob Harrop"); + assertThat(bean.getAge()).as("Age").isEqualTo(100); + } + + @Test + void propertiesCorrectInJmx() throws Exception { + ObjectName oname = new ObjectName("bean:name=proxyTestBean1"); + Object name = getServer().getAttribute(oname, "Name"); + Integer age = (Integer) getServer().getAttribute(oname, "Age"); + + assertThat(name).as("Name is incorrect in JMX").isEqualTo("Rob Harrop"); + assertThat(age.intValue()).as("Age is incorrect in JMX").isEqualTo(100); + } + +} + diff --git a/spring-context/src/test/java/org/springframework/jmx/export/TestDynamicMBean.java b/spring-context/src/test/java/org/springframework/jmx/export/TestDynamicMBean.java new file mode 100644 index 0000000..a7a77e9 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/TestDynamicMBean.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export; + +import javax.management.Attribute; +import javax.management.AttributeList; +import javax.management.DynamicMBean; +import javax.management.MBeanAttributeInfo; +import javax.management.MBeanConstructorInfo; +import javax.management.MBeanInfo; +import javax.management.MBeanNotificationInfo; +import javax.management.MBeanOperationInfo; + +/** + * @author Rob Harrop + * @author Juergen Hoeller + */ +public class TestDynamicMBean implements DynamicMBean { + + public void setFailOnInit(boolean failOnInit) { + if (failOnInit) { + throw new IllegalArgumentException("Failing on initialization"); + } + } + + @Override + public Object getAttribute(String attribute) { + if ("Name".equals(attribute)) { + return "Rob Harrop"; + } + return null; + } + + @Override + public void setAttribute(Attribute attribute) { + } + + @Override + public AttributeList getAttributes(String[] attributes) { + return null; + } + + @Override + public AttributeList setAttributes(AttributeList attributes) { + return null; + } + + @Override + public Object invoke(String actionName, Object[] params, String[] signature) { + return null; + } + + @Override + public MBeanInfo getMBeanInfo() { + MBeanAttributeInfo attr = new MBeanAttributeInfo("name", "java.lang.String", "", true, false, false); + return new MBeanInfo( + TestDynamicMBean.class.getName(), "", + new MBeanAttributeInfo[]{attr}, + new MBeanConstructorInfo[0], + new MBeanOperationInfo[0], + new MBeanNotificationInfo[0]); + } + + @Override + public boolean equals(Object obj) { + return (obj instanceof TestDynamicMBean); + } + + @Override + public int hashCode() { + return TestDynamicMBean.class.hashCode(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationLazyInitMBeanTests.java b/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationLazyInitMBeanTests.java new file mode 100644 index 0000000..46b8d5e --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationLazyInitMBeanTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.annotation; + +import javax.management.MBeanServer; +import javax.management.ObjectName; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.jmx.support.ObjectNameManager; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + * @author Juergen Hoeller + */ +class AnnotationLazyInitMBeanTests { + + @Test + void lazyNaming() throws Exception { + try (ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext("org/springframework/jmx/export/annotation/lazyNaming.xml")) { + MBeanServer server = (MBeanServer) ctx.getBean("server"); + ObjectName oname = ObjectNameManager.getInstance("bean:name=testBean4"); + assertThat(server.getObjectInstance(oname)).isNotNull(); + String name = (String) server.getAttribute(oname, "Name"); + assertThat(name).as("Invalid name returned").isEqualTo("TEST"); + } + } + + @Test + void lazyAssembling() throws Exception { + System.setProperty("domain", "bean"); + try (ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext("org/springframework/jmx/export/annotation/lazyAssembling.xml")) { + MBeanServer server = (MBeanServer) ctx.getBean("server"); + + ObjectName oname = ObjectNameManager.getInstance("bean:name=testBean4"); + assertThat(server.getObjectInstance(oname)).isNotNull(); + String name = (String) server.getAttribute(oname, "Name"); + assertThat(name).as("Invalid name returned").isEqualTo("TEST"); + + oname = ObjectNameManager.getInstance("bean:name=testBean5"); + assertThat(server.getObjectInstance(oname)).isNotNull(); + name = (String) server.getAttribute(oname, "Name"); + assertThat(name).as("Invalid name returned").isEqualTo("FACTORY"); + + oname = ObjectNameManager.getInstance("spring:mbean=true"); + assertThat(server.getObjectInstance(oname)).isNotNull(); + name = (String) server.getAttribute(oname, "Name"); + assertThat(name).as("Invalid name returned").isEqualTo("Rob Harrop"); + + oname = ObjectNameManager.getInstance("spring:mbean=another"); + assertThat(server.getObjectInstance(oname)).isNotNull(); + name = (String) server.getAttribute(oname, "Name"); + assertThat(name).as("Invalid name returned").isEqualTo("Juergen Hoeller"); + } + finally { + System.clearProperty("domain"); + } + } + + @Test + void componentScan() throws Exception { + try (ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext("org/springframework/jmx/export/annotation/componentScan.xml")) { + MBeanServer server = (MBeanServer) ctx.getBean("server"); + ObjectName oname = ObjectNameManager.getInstance("bean:name=testBean4"); + assertThat(server.getObjectInstance(oname)).isNotNull(); + String name = (String) server.getAttribute(oname, "Name"); + assertThat(name).isNull(); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationMetadataAssemblerTests.java b/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationMetadataAssemblerTests.java new file mode 100644 index 0000000..e58d826 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationMetadataAssemblerTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.annotation; + +import javax.management.modelmbean.ModelMBeanAttributeInfo; +import javax.management.modelmbean.ModelMBeanInfo; +import javax.management.modelmbean.ModelMBeanOperationInfo; + +import org.junit.jupiter.api.Test; + +import org.springframework.jmx.IJmxTestBean; +import org.springframework.jmx.export.assembler.AbstractMetadataAssemblerTests; +import org.springframework.jmx.export.metadata.JmxAttributeSource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + * @author Chris Beams + */ +public class AnnotationMetadataAssemblerTests extends AbstractMetadataAssemblerTests { + + private static final String OBJECT_NAME = "bean:name=testBean4"; + + + @Test + public void testAttributeFromInterface() throws Exception { + ModelMBeanInfo inf = getMBeanInfoFromAssembler(); + ModelMBeanAttributeInfo attr = inf.getAttribute("Colour"); + assertThat(attr.isWritable()).as("The name attribute should be writable").isTrue(); + assertThat(attr.isReadable()).as("The name attribute should be readable").isTrue(); + } + + @Test + public void testOperationFromInterface() throws Exception { + ModelMBeanInfo inf = getMBeanInfoFromAssembler(); + ModelMBeanOperationInfo op = inf.getOperation("fromInterface"); + assertThat(op).isNotNull(); + } + + @Test + public void testOperationOnGetter() throws Exception { + ModelMBeanInfo inf = getMBeanInfoFromAssembler(); + ModelMBeanOperationInfo op = inf.getOperation("getExpensiveToCalculate"); + assertThat(op).isNotNull(); + } + + @Test + public void testRegistrationOnInterface() throws Exception { + Object bean = getContext().getBean("testInterfaceBean"); + ModelMBeanInfo inf = getAssembler().getMBeanInfo(bean, "bean:name=interfaceTestBean"); + assertThat(inf).isNotNull(); + assertThat(inf.getDescription()).isEqualTo("My Managed Bean"); + + ModelMBeanOperationInfo op = inf.getOperation("foo"); + assertThat(op).as("foo operation not exposed").isNotNull(); + assertThat(op.getDescription()).isEqualTo("invoke foo"); + + assertThat(inf.getOperation("doNotExpose")).as("doNotExpose operation should not be exposed").isNull(); + + ModelMBeanAttributeInfo attr = inf.getAttribute("Bar"); + assertThat(attr).as("bar attribute not exposed").isNotNull(); + assertThat(attr.getDescription()).isEqualTo("Bar description"); + + ModelMBeanAttributeInfo attr2 = inf.getAttribute("CacheEntries"); + assertThat(attr2).as("cacheEntries attribute not exposed").isNotNull(); + assertThat(attr2.getDescriptor().getFieldValue("metricType")).as("Metric Type should be COUNTER").isEqualTo("COUNTER"); + } + + + @Override + protected JmxAttributeSource getAttributeSource() { + return new AnnotationJmxAttributeSource(); + } + + @Override + protected String getObjectName() { + return OBJECT_NAME; + } + + @Override + protected IJmxTestBean createJmxTestBean() { + return new AnnotationTestSubBean(); + } + + @Override + protected String getApplicationContextPath() { + return "org/springframework/jmx/export/annotation/annotations.xml"; + } + + @Override + protected int getExpectedAttributeCount() { + return super.getExpectedAttributeCount() + 1; + } + + @Override + protected int getExpectedOperationCount() { + return super.getExpectedOperationCount() + 4; + } +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationTestBean.java b/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationTestBean.java new file mode 100644 index 0000000..db38feb --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationTestBean.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.annotation; + +import org.springframework.jmx.IJmxTestBean; +import org.springframework.jmx.support.MetricType; +import org.springframework.stereotype.Service; + +/** + * @author Rob Harrop + * @author Juergen Hoeller + */ +@Service("testBean") +@ManagedResource(objectName = "bean:name=testBean4", description = "My Managed Bean", log = true, + logFile = "build/jmx.log", currencyTimeLimit = 15, persistPolicy = "OnUpdate", persistPeriod = 200, + persistLocation = "./foo", persistName = "bar.jmx") +@ManagedNotification(name = "My Notification", notificationTypes = { "type.foo", "type.bar" }) +public class AnnotationTestBean implements IJmxTestBean { + + private String name; + + private String nickName; + + private int age; + + private boolean isSuperman; + + + @Override + @ManagedAttribute(description = "The Age Attribute", currencyTimeLimit = 15) + public int getAge() { + return age; + } + + @Override + public void setAge(int age) { + this.age = age; + } + + @Override + @ManagedOperation(currencyTimeLimit = 30) + public long myOperation() { + return 1L; + } + + @Override + @ManagedAttribute(description = "The Name Attribute", + currencyTimeLimit = 20, + defaultValue = "bar", + persistPolicy = "OnUpdate") + public void setName(String name) { + this.name = name; + } + + @Override + @ManagedAttribute(defaultValue = "foo", persistPeriod = 300) + public String getName() { + return name; + } + + @ManagedAttribute(description = "The Nick Name Attribute") + public void setNickName(String nickName) { + this.nickName = nickName; + } + + public String getNickName() { + return this.nickName; + } + + public void setSuperman(boolean superman) { + this.isSuperman = superman; + } + + @ManagedAttribute(description = "The Is Superman Attribute") + public boolean isSuperman() { + return isSuperman; + } + + @Override + @ManagedOperation(description = "Add Two Numbers Together") + @ManagedOperationParameter(name="x", description="Left operand") + @ManagedOperationParameter(name="y", description="Right operand") + public int add(int x, int y) { + return x + y; + } + + /** + * Test method that is not exposed by the MetadataAssembler. + */ + @Override + public void dontExposeMe() { + throw new RuntimeException(); + } + + @ManagedMetric(description="The QueueSize metric", currencyTimeLimit = 20, persistPolicy="OnUpdate", persistPeriod=300, + category="utilization", metricType = MetricType.COUNTER, displayName="Queue Size", unit="messages") + public long getQueueSize() { + return 100L; + } + + @ManagedMetric + public int getCacheEntries() { + return 3; + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationTestBeanFactory.java b/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationTestBeanFactory.java new file mode 100644 index 0000000..1634b6b --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationTestBeanFactory.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.annotation; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.jmx.IJmxTestBean; + +/** + * @author Juergen Hoeller + */ +public class AnnotationTestBeanFactory implements FactoryBean { + + private final FactoryCreatedAnnotationTestBean instance = new FactoryCreatedAnnotationTestBean(); + + public AnnotationTestBeanFactory() { + this.instance.setName("FACTORY"); + } + + @Override + public FactoryCreatedAnnotationTestBean getObject() throws Exception { + return this.instance; + } + + @Override + public Class getObjectType() { + return FactoryCreatedAnnotationTestBean.class; + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationTestSubBean.java b/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationTestSubBean.java new file mode 100644 index 0000000..46990f9 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationTestSubBean.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.annotation; + +/** + * @author Rob Harrop + */ +public class AnnotationTestSubBean extends AnnotationTestBean implements IAnnotationTestBean { + + private String colour; + + @Override + public long myOperation() { + return 123L; + } + + @Override + public void setAge(int age) { + super.setAge(age); + } + + @Override + public int getAge() { + return super.getAge(); + } + + @Override + public String getColour() { + return this.colour; + } + + @Override + public void setColour(String colour) { + this.colour = colour; + } + + @Override + public void fromInterface() { + } + + @Override + public int getExpensiveToCalculate() { + return Integer.MAX_VALUE; + } +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnotherAnnotationTestBean.java b/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnotherAnnotationTestBean.java new file mode 100644 index 0000000..e7f5623 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnotherAnnotationTestBean.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.annotation; + +import org.springframework.jmx.support.MetricType; + +/** + * @author Stephane Nicoll + */ +@ManagedResource(objectName = "bean:name=interfaceTestBean", description = "My Managed Bean") +public interface AnotherAnnotationTestBean { + + @ManagedOperation(description = "invoke foo") + void foo(); + + @ManagedAttribute(description = "Bar description") + String getBar(); + + void setBar(String bar); + + @ManagedMetric(description = "a metric", metricType = MetricType.COUNTER) + int getCacheEntries(); + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnotherAnnotationTestBeanImpl.java b/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnotherAnnotationTestBeanImpl.java new file mode 100644 index 0000000..a2fd051 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnotherAnnotationTestBeanImpl.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.annotation; + +/** + * @author Stephane Nicoll + */ +public class AnotherAnnotationTestBeanImpl implements AnotherAnnotationTestBean { + + private String bar; + + @Override + public void foo() { + } + + public void doNotExpose() { + + } + + @Override + public String getBar() { + return this.bar; + } + + @Override + public void setBar(String bar) { + this.bar = bar; + } + + @Override + public int getCacheEntries() { + return 42; + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/annotation/EnableMBeanExportConfigurationTests.java b/spring-context/src/test/java/org/springframework/jmx/export/annotation/EnableMBeanExportConfigurationTests.java new file mode 100644 index 0000000..5d7f899 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/annotation/EnableMBeanExportConfigurationTests.java @@ -0,0 +1,362 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.annotation; + +import javax.management.MBeanServer; +import javax.management.ObjectName; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableMBeanExport; +import org.springframework.context.annotation.Lazy; +import org.springframework.context.annotation.MBeanExportConfiguration; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.jmx.export.MBeanExporterTests; +import org.springframework.jmx.export.TestDynamicMBean; +import org.springframework.jmx.export.metadata.InvalidMetadataException; +import org.springframework.jmx.support.MBeanServerFactoryBean; +import org.springframework.jmx.support.ObjectNameManager; +import org.springframework.jmx.support.RegistrationPolicy; +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link EnableMBeanExport} and {@link MBeanExportConfiguration}. + * + * @author Phillip Webb + * @author Stephane Nicoll + * @see AnnotationLazyInitMBeanTests + */ +public class EnableMBeanExportConfigurationTests { + + private AnnotationConfigApplicationContext ctx; + + + @AfterEach + public void closeContext() { + if (this.ctx != null) { + this.ctx.close(); + } + } + + + @Test + public void testLazyNaming() throws Exception { + load(LazyNamingConfiguration.class); + validateAnnotationTestBean(); + } + + private void load(Class... config) { + this.ctx = new AnnotationConfigApplicationContext(config); + } + + @Test + public void testOnlyTargetClassIsExposed() throws Exception { + load(ProxyConfiguration.class); + validateAnnotationTestBean(); + } + + @Test + @SuppressWarnings("resource") + public void testPackagePrivateExtensionCantBeExposed() { + assertThatExceptionOfType(InvalidMetadataException.class).isThrownBy(() -> + new AnnotationConfigApplicationContext(PackagePrivateConfiguration.class)) + .withMessageContaining(PackagePrivateTestBean.class.getName()) + .withMessageContaining("must be public"); + } + + @Test + @SuppressWarnings("resource") + public void testPackagePrivateImplementationCantBeExposed() { + assertThatExceptionOfType(InvalidMetadataException.class).isThrownBy(() -> + new AnnotationConfigApplicationContext(PackagePrivateInterfaceImplementationConfiguration.class)) + .withMessageContaining(PackagePrivateAnnotationTestBean.class.getName()) + .withMessageContaining("must be public"); + } + + @Test + public void testPackagePrivateClassExtensionCanBeExposed() throws Exception { + load(PackagePrivateExtensionConfiguration.class); + validateAnnotationTestBean(); + } + + @Test + public void testPlaceholderBased() throws Exception { + MockEnvironment env = new MockEnvironment(); + env.setProperty("serverName", "server"); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.setEnvironment(env); + context.register(PlaceholderBasedConfiguration.class); + context.refresh(); + this.ctx = context; + validateAnnotationTestBean(); + } + + @Test + public void testLazyAssembling() throws Exception { + System.setProperty("domain", "bean"); + load(LazyAssemblingConfiguration.class); + try { + MBeanServer server = (MBeanServer) this.ctx.getBean("server"); + + validateMBeanAttribute(server, "bean:name=testBean4", "TEST"); + validateMBeanAttribute(server, "bean:name=testBean5", "FACTORY"); + validateMBeanAttribute(server, "spring:mbean=true", "Rob Harrop"); + validateMBeanAttribute(server, "spring:mbean=another", "Juergen Hoeller"); + } + finally { + System.clearProperty("domain"); + } + } + + @Test + public void testComponentScan() throws Exception { + load(ComponentScanConfiguration.class); + MBeanServer server = (MBeanServer) this.ctx.getBean("server"); + validateMBeanAttribute(server, "bean:name=testBean4", null); + } + + private void validateAnnotationTestBean() throws Exception { + MBeanServer server = (MBeanServer) this.ctx.getBean("server"); + validateMBeanAttribute(server,"bean:name=testBean4", "TEST"); + } + + private void validateMBeanAttribute(MBeanServer server, String objectName, String expected) throws Exception { + ObjectName oname = ObjectNameManager.getInstance(objectName); + assertThat(server.getObjectInstance(oname)).isNotNull(); + String name = (String) server.getAttribute(oname, "Name"); + assertThat(name).as("Invalid name returned").isEqualTo(expected); + } + + + @Configuration + @EnableMBeanExport(server = "server") + static class LazyNamingConfiguration { + + @Bean + public MBeanServerFactoryBean server() { + return new MBeanServerFactoryBean(); + } + + @Bean + @Lazy + public AnnotationTestBean testBean() { + AnnotationTestBean bean = new AnnotationTestBean(); + bean.setName("TEST"); + bean.setAge(100); + return bean; + } + } + + @Configuration + @EnableMBeanExport(server = "server") + static class ProxyConfiguration { + + @Bean + public MBeanServerFactoryBean server() { + return new MBeanServerFactoryBean(); + } + + @Bean + @Lazy + @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) + public AnnotationTestBean testBean() { + AnnotationTestBean bean = new AnnotationTestBean(); + bean.setName("TEST"); + bean.setAge(100); + return bean; + } + } + + + @Configuration + @EnableMBeanExport(server = "${serverName}") + static class PlaceholderBasedConfiguration { + + @Bean + public MBeanServerFactoryBean server() { + return new MBeanServerFactoryBean(); + } + + @Bean + @Lazy + public AnnotationTestBean testBean() { + AnnotationTestBean bean = new AnnotationTestBean(); + bean.setName("TEST"); + bean.setAge(100); + return bean; + } + } + + + @Configuration + @EnableMBeanExport(server="server", registration=RegistrationPolicy.REPLACE_EXISTING) + static class LazyAssemblingConfiguration { + + @Bean + public MBeanServerFactoryBean server() { + return new MBeanServerFactoryBean(); + } + + @Bean("bean:name=testBean4") + @Lazy + public AnnotationTestBean testBean4() { + AnnotationTestBean bean = new AnnotationTestBean(); + bean.setName("TEST"); + bean.setAge(100); + return bean; + } + + @Bean("bean:name=testBean5") + public AnnotationTestBeanFactory testBean5() { + return new AnnotationTestBeanFactory(); + } + + @Bean(name="spring:mbean=true") + @Lazy + public TestDynamicMBean dynamic() { + return new TestDynamicMBean(); + } + + @Bean(name="spring:mbean=another") + @Lazy + public MBeanExporterTests.Person person() { + MBeanExporterTests.Person person = new MBeanExporterTests.Person(); + person.setName("Juergen Hoeller"); + return person; + } + + @Bean + @Lazy + public Object notLoadable() throws Exception { + return Class.forName("does.not.exist").newInstance(); + } + } + + + @Configuration + @ComponentScan(excludeFilters = @ComponentScan.Filter(Configuration.class)) + @EnableMBeanExport(server = "server") + static class ComponentScanConfiguration { + + @Bean + public MBeanServerFactoryBean server() { + return new MBeanServerFactoryBean(); + } + } + + @Configuration + @EnableMBeanExport(server = "server") + static class PackagePrivateConfiguration { + + @Bean + public MBeanServerFactoryBean server() { + return new MBeanServerFactoryBean(); + } + + @Bean + public PackagePrivateTestBean testBean() { + return new PackagePrivateTestBean(); + } + } + + @ManagedResource(objectName = "bean:name=packagePrivate") + private static class PackagePrivateTestBean { + + private String name; + + @ManagedAttribute + public String getName() { + return this.name; + } + + @ManagedAttribute + public void setName(String name) { + this.name = name; + } + } + + + @Configuration + @EnableMBeanExport(server = "server") + static class PackagePrivateExtensionConfiguration { + + @Bean + public MBeanServerFactoryBean server() { + return new MBeanServerFactoryBean(); + } + + @Bean + public PackagePrivateTestBeanExtension testBean() { + PackagePrivateTestBeanExtension bean = new PackagePrivateTestBeanExtension(); + bean.setName("TEST"); + return bean; + } + } + + private static class PackagePrivateTestBeanExtension extends AnnotationTestBean { + + } + + @Configuration + @EnableMBeanExport(server = "server") + static class PackagePrivateInterfaceImplementationConfiguration { + + @Bean + public MBeanServerFactoryBean server() { + return new MBeanServerFactoryBean(); + } + + @Bean + public PackagePrivateAnnotationTestBean testBean() { + return new PackagePrivateAnnotationTestBean(); + } + } + + private static class PackagePrivateAnnotationTestBean implements AnotherAnnotationTestBean { + + private String bar; + + @Override + public void foo() { + } + + @Override + public String getBar() { + return this.bar; + } + + @Override + public void setBar(String bar) { + this.bar = bar; + } + + @Override + public int getCacheEntries() { + return 0; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/annotation/FactoryCreatedAnnotationTestBean.java b/spring-context/src/test/java/org/springframework/jmx/export/annotation/FactoryCreatedAnnotationTestBean.java new file mode 100644 index 0000000..6441c83 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/annotation/FactoryCreatedAnnotationTestBean.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.annotation; + +/** + * @author Juergen Hoeller + */ +@ManagedResource("${domain}:name=testBean5") +public class FactoryCreatedAnnotationTestBean extends AnnotationTestBean { + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/annotation/IAnnotationTestBean.java b/spring-context/src/test/java/org/springframework/jmx/export/annotation/IAnnotationTestBean.java new file mode 100644 index 0000000..e19f1ad --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/annotation/IAnnotationTestBean.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2009 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.annotation; + +/** + * @author Rob Harrop + */ +public interface IAnnotationTestBean { + + @ManagedAttribute + String getColour(); + + @ManagedAttribute + void setColour(String colour); + + @ManagedOperation + void fromInterface(); + + @ManagedOperation + int getExpensiveToCalculate(); +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/annotation/JmxUtilsAnnotationTests.java b/spring-context/src/test/java/org/springframework/jmx/export/annotation/JmxUtilsAnnotationTests.java new file mode 100644 index 0000000..b96118a --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/annotation/JmxUtilsAnnotationTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.annotation; + +import javax.management.MXBean; + +import org.junit.jupiter.api.Test; + +import org.springframework.jmx.support.JmxUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + */ +public class JmxUtilsAnnotationTests { + + @Test + public void notMXBean() throws Exception { + assertThat(JmxUtils.isMBean(FooNotX.class)).as("MXBean annotation not detected correctly").isFalse(); + } + + @Test + public void annotatedMXBean() throws Exception { + assertThat(JmxUtils.isMBean(FooX.class)).as("MXBean annotation not detected correctly").isTrue(); + } + + + @MXBean(false) + public interface FooNotMXBean { + String getName(); + } + + public static class FooNotX implements FooNotMXBean { + + @Override + public String getName() { + return "Rob Harrop"; + } + } + + @MXBean(true) + public interface FooIfc { + String getName(); + } + + public static class FooX implements FooIfc { + + @Override + public String getName() { + return "Rob Harrop"; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/assembler/AbstractJmxAssemblerTests.java b/spring-context/src/test/java/org/springframework/jmx/export/assembler/AbstractJmxAssemblerTests.java new file mode 100644 index 0000000..26a8b0d --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/assembler/AbstractJmxAssemblerTests.java @@ -0,0 +1,201 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.assembler; + +import javax.management.Attribute; +import javax.management.Descriptor; +import javax.management.MBeanAttributeInfo; +import javax.management.MBeanInfo; +import javax.management.MBeanNotificationInfo; +import javax.management.MBeanOperationInfo; +import javax.management.ObjectInstance; +import javax.management.ObjectName; +import javax.management.modelmbean.ModelMBeanAttributeInfo; +import javax.management.modelmbean.ModelMBeanInfo; +import javax.management.modelmbean.ModelMBeanOperationInfo; + +import org.junit.jupiter.api.Test; + +import org.springframework.jmx.AbstractJmxTests; +import org.springframework.jmx.IJmxTestBean; +import org.springframework.jmx.support.ObjectNameManager; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + * @author Chris Beams + */ +public abstract class AbstractJmxAssemblerTests extends AbstractJmxTests { + + protected static final String AGE_ATTRIBUTE = "Age"; + + protected static final String NAME_ATTRIBUTE = "Name"; + + protected abstract String getObjectName(); + + @Test + public void testMBeanRegistration() throws Exception { + // beans are registered at this point - just grab them from the server + ObjectInstance instance = getObjectInstance(); + assertThat(instance).as("Bean should not be null").isNotNull(); + } + + @Test + public void testRegisterOperations() throws Exception { + IJmxTestBean bean = getBean(); + assertThat(bean).isNotNull(); + MBeanInfo inf = getMBeanInfo(); + assertThat(inf.getOperations().length).as("Incorrect number of operations registered").isEqualTo(getExpectedOperationCount()); + } + + @Test + public void testRegisterAttributes() throws Exception { + IJmxTestBean bean = getBean(); + assertThat(bean).isNotNull(); + MBeanInfo inf = getMBeanInfo(); + assertThat(inf.getAttributes().length).as("Incorrect number of attributes registered").isEqualTo(getExpectedAttributeCount()); + } + + @Test + public void testGetMBeanInfo() throws Exception { + ModelMBeanInfo info = getMBeanInfoFromAssembler(); + assertThat(info).as("MBeanInfo should not be null").isNotNull(); + } + + @Test + public void testGetMBeanAttributeInfo() throws Exception { + ModelMBeanInfo info = getMBeanInfoFromAssembler(); + MBeanAttributeInfo[] inf = info.getAttributes(); + assertThat(inf.length).as("Invalid number of Attributes returned").isEqualTo(getExpectedAttributeCount()); + + for (int x = 0; x < inf.length; x++) { + assertThat(inf[x]).as("MBeanAttributeInfo should not be null").isNotNull(); + assertThat(inf[x].getDescription()).as("Description for MBeanAttributeInfo should not be null").isNotNull(); + } + } + + @Test + public void testGetMBeanOperationInfo() throws Exception { + ModelMBeanInfo info = getMBeanInfoFromAssembler(); + MBeanOperationInfo[] inf = info.getOperations(); + assertThat(inf.length).as("Invalid number of Operations returned").isEqualTo(getExpectedOperationCount()); + + for (int x = 0; x < inf.length; x++) { + assertThat(inf[x]).as("MBeanOperationInfo should not be null").isNotNull(); + assertThat(inf[x].getDescription()).as("Description for MBeanOperationInfo should not be null").isNotNull(); + } + } + + @Test + public void testDescriptionNotNull() throws Exception { + ModelMBeanInfo info = getMBeanInfoFromAssembler(); + + assertThat(info.getDescription()).as("The MBean description should not be null").isNotNull(); + } + + @Test + public void testSetAttribute() throws Exception { + ObjectName objectName = ObjectNameManager.getInstance(getObjectName()); + getServer().setAttribute(objectName, new Attribute(NAME_ATTRIBUTE, "Rob Harrop")); + IJmxTestBean bean = (IJmxTestBean) getContext().getBean("testBean"); + assertThat(bean.getName()).isEqualTo("Rob Harrop"); + } + + @Test + public void testGetAttribute() throws Exception { + ObjectName objectName = ObjectNameManager.getInstance(getObjectName()); + getBean().setName("John Smith"); + Object val = getServer().getAttribute(objectName, NAME_ATTRIBUTE); + assertThat(val).as("Incorrect result").isEqualTo("John Smith"); + } + + @Test + public void testOperationInvocation() throws Exception{ + ObjectName objectName = ObjectNameManager.getInstance(getObjectName()); + Object result = getServer().invoke(objectName, "add", + new Object[] {new Integer(20), new Integer(30)}, new String[] {"int", "int"}); + assertThat(result).as("Incorrect result").isEqualTo(new Integer(50)); + } + + @Test + public void testAttributeInfoHasDescriptors() throws Exception { + ModelMBeanInfo info = getMBeanInfoFromAssembler(); + + ModelMBeanAttributeInfo attr = info.getAttribute(NAME_ATTRIBUTE); + Descriptor desc = attr.getDescriptor(); + assertThat(desc.getFieldValue("getMethod")).as("getMethod field should not be null").isNotNull(); + assertThat(desc.getFieldValue("setMethod")).as("setMethod field should not be null").isNotNull(); + assertThat(desc.getFieldValue("getMethod")).as("getMethod field has incorrect value").isEqualTo("getName"); + assertThat(desc.getFieldValue("setMethod")).as("setMethod field has incorrect value").isEqualTo("setName"); + } + + @Test + public void testAttributeHasCorrespondingOperations() throws Exception { + ModelMBeanInfo info = getMBeanInfoFromAssembler(); + + ModelMBeanOperationInfo get = info.getOperation("getName"); + assertThat(get).as("get operation should not be null").isNotNull(); + assertThat(new Integer(4)).as("get operation should have visibility of four").isEqualTo(get.getDescriptor().getFieldValue("visibility")); + assertThat(get.getDescriptor().getFieldValue("role")).as("get operation should have role \"getter\"").isEqualTo("getter"); + + ModelMBeanOperationInfo set = info.getOperation("setName"); + assertThat(set).as("set operation should not be null").isNotNull(); + assertThat(new Integer(4)).as("set operation should have visibility of four").isEqualTo(set.getDescriptor().getFieldValue("visibility")); + assertThat(set.getDescriptor().getFieldValue("role")).as("set operation should have role \"setter\"").isEqualTo("setter"); + } + + @Test + public void testNotificationMetadata() throws Exception { + ModelMBeanInfo info = (ModelMBeanInfo) getMBeanInfo(); + MBeanNotificationInfo[] notifications = info.getNotifications(); + assertThat(notifications.length).as("Incorrect number of notifications").isEqualTo(1); + assertThat(notifications[0].getName()).as("Incorrect notification name").isEqualTo("My Notification"); + + String[] notifTypes = notifications[0].getNotifTypes(); + + assertThat(notifTypes.length).as("Incorrect number of notification types").isEqualTo(2); + assertThat(notifTypes[0]).as("Notification type.foo not found").isEqualTo("type.foo"); + assertThat(notifTypes[1]).as("Notification type.bar not found").isEqualTo("type.bar"); + } + + protected ModelMBeanInfo getMBeanInfoFromAssembler() throws Exception { + IJmxTestBean bean = getBean(); + ModelMBeanInfo info = getAssembler().getMBeanInfo(bean, getObjectName()); + return info; + } + + protected IJmxTestBean getBean() { + Object bean = getContext().getBean("testBean"); + return (IJmxTestBean) bean; + } + + protected MBeanInfo getMBeanInfo() throws Exception { + return getServer().getMBeanInfo(ObjectNameManager.getInstance(getObjectName())); + } + + protected ObjectInstance getObjectInstance() throws Exception { + return getServer().getObjectInstance(ObjectNameManager.getInstance(getObjectName())); + } + + protected abstract int getExpectedOperationCount(); + + protected abstract int getExpectedAttributeCount(); + + protected abstract MBeanInfoAssembler getAssembler() throws Exception; + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/assembler/AbstractMetadataAssemblerTests.java b/spring-context/src/test/java/org/springframework/jmx/export/assembler/AbstractMetadataAssemblerTests.java new file mode 100644 index 0000000..f851ae7 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/assembler/AbstractMetadataAssemblerTests.java @@ -0,0 +1,246 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.assembler; + +import java.util.HashMap; +import java.util.Map; + +import javax.management.Descriptor; +import javax.management.MBeanInfo; +import javax.management.MBeanParameterInfo; +import javax.management.modelmbean.ModelMBeanAttributeInfo; +import javax.management.modelmbean.ModelMBeanInfo; +import javax.management.modelmbean.ModelMBeanOperationInfo; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.testfixture.interceptor.NopInterceptor; +import org.springframework.jmx.IJmxTestBean; +import org.springframework.jmx.JmxTestBean; +import org.springframework.jmx.export.MBeanExporter; +import org.springframework.jmx.export.metadata.JmxAttributeSource; +import org.springframework.jmx.support.ObjectNameManager; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + * @author Chris Beams + */ +public abstract class AbstractMetadataAssemblerTests extends AbstractJmxAssemblerTests { + + protected static final String QUEUE_SIZE_METRIC = "QueueSize"; + + protected static final String CACHE_ENTRIES_METRIC = "CacheEntries"; + + @Test + public void testDescription() throws Exception { + ModelMBeanInfo info = getMBeanInfoFromAssembler(); + assertThat(info.getDescription()).as("The descriptions are not the same").isEqualTo("My Managed Bean"); + } + + @Test + public void testAttributeDescriptionOnSetter() throws Exception { + ModelMBeanInfo inf = getMBeanInfoFromAssembler(); + ModelMBeanAttributeInfo attr = inf.getAttribute(AGE_ATTRIBUTE); + assertThat(attr.getDescription()).as("The description for the age attribute is incorrect").isEqualTo("The Age Attribute"); + } + + @Test + public void testAttributeDescriptionOnGetter() throws Exception { + ModelMBeanInfo inf = getMBeanInfoFromAssembler(); + ModelMBeanAttributeInfo attr = inf.getAttribute(NAME_ATTRIBUTE); + assertThat(attr.getDescription()).as("The description for the name attribute is incorrect").isEqualTo("The Name Attribute"); + } + + /** + * Tests the situation where the attribute is only defined on the getter. + */ + @Test + public void testReadOnlyAttribute() throws Exception { + ModelMBeanInfo inf = getMBeanInfoFromAssembler(); + ModelMBeanAttributeInfo attr = inf.getAttribute(AGE_ATTRIBUTE); + assertThat(attr.isWritable()).as("The age attribute should not be writable").isFalse(); + } + + @Test + public void testReadWriteAttribute() throws Exception { + ModelMBeanInfo inf = getMBeanInfoFromAssembler(); + ModelMBeanAttributeInfo attr = inf.getAttribute(NAME_ATTRIBUTE); + assertThat(attr.isWritable()).as("The name attribute should be writable").isTrue(); + assertThat(attr.isReadable()).as("The name attribute should be readable").isTrue(); + } + + /** + * Tests the situation where the property only has a getter. + */ + @Test + public void testWithOnlySetter() throws Exception { + ModelMBeanInfo inf = getMBeanInfoFromAssembler(); + ModelMBeanAttributeInfo attr = inf.getAttribute("NickName"); + assertThat(attr).as("Attribute should not be null").isNotNull(); + } + + /** + * Tests the situation where the property only has a setter. + */ + @Test + public void testWithOnlyGetter() throws Exception { + ModelMBeanInfo info = getMBeanInfoFromAssembler(); + ModelMBeanAttributeInfo attr = info.getAttribute("Superman"); + assertThat(attr).as("Attribute should not be null").isNotNull(); + } + + @Test + public void testManagedResourceDescriptor() throws Exception { + ModelMBeanInfo info = getMBeanInfoFromAssembler(); + Descriptor desc = info.getMBeanDescriptor(); + + assertThat(desc.getFieldValue("log")).as("Logging should be set to true").isEqualTo("true"); + assertThat(desc.getFieldValue("logFile")).as("Log file should be build/jmx.log").isEqualTo("build/jmx.log"); + assertThat(desc.getFieldValue("currencyTimeLimit")).as("Currency Time Limit should be 15").isEqualTo("15"); + assertThat(desc.getFieldValue("persistPolicy")).as("Persist Policy should be OnUpdate").isEqualTo("OnUpdate"); + assertThat(desc.getFieldValue("persistPeriod")).as("Persist Period should be 200").isEqualTo("200"); + assertThat(desc.getFieldValue("persistLocation")).as("Persist Location should be foo").isEqualTo("./foo"); + assertThat(desc.getFieldValue("persistName")).as("Persist Name should be bar").isEqualTo("bar.jmx"); + } + + @Test + public void testAttributeDescriptor() throws Exception { + ModelMBeanInfo info = getMBeanInfoFromAssembler(); + Descriptor desc = info.getAttribute(NAME_ATTRIBUTE).getDescriptor(); + + assertThat(desc.getFieldValue("default")).as("Default value should be foo").isEqualTo("foo"); + assertThat(desc.getFieldValue("currencyTimeLimit")).as("Currency Time Limit should be 20").isEqualTo("20"); + assertThat(desc.getFieldValue("persistPolicy")).as("Persist Policy should be OnUpdate").isEqualTo("OnUpdate"); + assertThat(desc.getFieldValue("persistPeriod")).as("Persist Period should be 300").isEqualTo("300"); + } + + @Test + public void testOperationDescriptor() throws Exception { + ModelMBeanInfo info = getMBeanInfoFromAssembler(); + Descriptor desc = info.getOperation("myOperation").getDescriptor(); + + assertThat(desc.getFieldValue("currencyTimeLimit")).as("Currency Time Limit should be 30").isEqualTo("30"); + assertThat(desc.getFieldValue("role")).as("Role should be \"operation\"").isEqualTo("operation"); + } + + @Test + public void testOperationParameterMetadata() throws Exception { + ModelMBeanInfo info = getMBeanInfoFromAssembler(); + ModelMBeanOperationInfo oper = info.getOperation("add"); + MBeanParameterInfo[] params = oper.getSignature(); + + assertThat(params.length).as("Invalid number of params").isEqualTo(2); + assertThat(params[0].getName()).as("Incorrect name for x param").isEqualTo("x"); + assertThat(params[0].getType()).as("Incorrect type for x param").isEqualTo(int.class.getName()); + + assertThat(params[1].getName()).as("Incorrect name for y param").isEqualTo("y"); + assertThat(params[1].getType()).as("Incorrect type for y param").isEqualTo(int.class.getName()); + } + + @Test + public void testWithCglibProxy() throws Exception { + IJmxTestBean tb = createJmxTestBean(); + ProxyFactory pf = new ProxyFactory(); + pf.setTarget(tb); + pf.addAdvice(new NopInterceptor()); + Object proxy = pf.getProxy(); + + MetadataMBeanInfoAssembler assembler = (MetadataMBeanInfoAssembler) getAssembler(); + + MBeanExporter exporter = new MBeanExporter(); + exporter.setBeanFactory(getContext()); + exporter.setAssembler(assembler); + + String objectName = "spring:bean=test,proxy=true"; + + Map beans = new HashMap<>(); + beans.put(objectName, proxy); + exporter.setBeans(beans); + start(exporter); + + MBeanInfo inf = getServer().getMBeanInfo(ObjectNameManager.getInstance(objectName)); + assertThat(inf.getOperations().length).as("Incorrect number of operations").isEqualTo(getExpectedOperationCount()); + assertThat(inf.getAttributes().length).as("Incorrect number of attributes").isEqualTo(getExpectedAttributeCount()); + + assertThat(assembler.includeBean(proxy.getClass(), "some bean name")).as("Not included in autodetection").isTrue(); + } + + @Test + public void testMetricDescription() throws Exception { + ModelMBeanInfo inf = getMBeanInfoFromAssembler(); + ModelMBeanAttributeInfo metric = inf.getAttribute(QUEUE_SIZE_METRIC); + ModelMBeanOperationInfo operation = inf.getOperation("getQueueSize"); + assertThat(metric.getDescription()).as("The description for the queue size metric is incorrect").isEqualTo("The QueueSize metric"); + assertThat(operation.getDescription()).as("The description for the getter operation of the queue size metric is incorrect").isEqualTo("The QueueSize metric"); + } + + @Test + public void testMetricDescriptor() throws Exception { + ModelMBeanInfo info = getMBeanInfoFromAssembler(); + Descriptor desc = info.getAttribute(QUEUE_SIZE_METRIC).getDescriptor(); + assertThat(desc.getFieldValue("currencyTimeLimit")).as("Currency Time Limit should be 20").isEqualTo("20"); + assertThat(desc.getFieldValue("persistPolicy")).as("Persist Policy should be OnUpdate").isEqualTo("OnUpdate"); + assertThat(desc.getFieldValue("persistPeriod")).as("Persist Period should be 300").isEqualTo("300"); + assertThat(desc.getFieldValue("units")).as("Unit should be messages").isEqualTo("messages"); + assertThat(desc.getFieldValue("displayName")).as("Display Name should be Queue Size").isEqualTo("Queue Size"); + assertThat(desc.getFieldValue("metricType")).as("Metric Type should be COUNTER").isEqualTo("COUNTER"); + assertThat(desc.getFieldValue("metricCategory")).as("Metric Category should be utilization").isEqualTo("utilization"); + } + + @Test + public void testMetricDescriptorDefaults() throws Exception { + ModelMBeanInfo info = getMBeanInfoFromAssembler(); + Descriptor desc = info.getAttribute(CACHE_ENTRIES_METRIC).getDescriptor(); + assertThat(desc.getFieldValue("currencyTimeLimit")).as("Currency Time Limit should not be populated").isNull(); + assertThat(desc.getFieldValue("persistPolicy")).as("Persist Policy should not be populated").isNull(); + assertThat(desc.getFieldValue("persistPeriod")).as("Persist Period should not be populated").isNull(); + assertThat(desc.getFieldValue("units")).as("Unit should not be populated").isNull(); + assertThat(desc.getFieldValue("displayName")).as("Display Name should be populated by default via JMX").isEqualTo(CACHE_ENTRIES_METRIC); + assertThat(desc.getFieldValue("metricType")).as("Metric Type should be GAUGE").isEqualTo("GAUGE"); + assertThat(desc.getFieldValue("metricCategory")).as("Metric Category should not be populated").isNull(); + } + + @Override + protected abstract String getObjectName(); + + @Override + protected int getExpectedAttributeCount() { + return 6; + } + + @Override + protected int getExpectedOperationCount() { + return 9; + } + + protected IJmxTestBean createJmxTestBean() { + return new JmxTestBean(); + } + + @Override + protected MBeanInfoAssembler getAssembler() { + MetadataMBeanInfoAssembler assembler = new MetadataMBeanInfoAssembler(); + assembler.setAttributeSource(getAttributeSource()); + return assembler; + } + + protected abstract JmxAttributeSource getAttributeSource(); + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/assembler/IAdditionalTestMethods.java b/spring-context/src/test/java/org/springframework/jmx/export/assembler/IAdditionalTestMethods.java new file mode 100644 index 0000000..9685421 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/assembler/IAdditionalTestMethods.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.assembler; + +/** + * @author Rob Harrop + */ +public interface IAdditionalTestMethods { + + String getNickName(); + + void setNickName(String nickName); + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/assembler/ICustomBase.java b/spring-context/src/test/java/org/springframework/jmx/export/assembler/ICustomBase.java new file mode 100644 index 0000000..7d5523e --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/assembler/ICustomBase.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.assembler; + +/** + * @author Juergen Hoeller + */ +public interface ICustomBase { + + int add(int x, int y); + + long myOperation(); + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/assembler/ICustomJmxBean.java b/spring-context/src/test/java/org/springframework/jmx/export/assembler/ICustomJmxBean.java new file mode 100644 index 0000000..5484923 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/assembler/ICustomJmxBean.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.assembler; + +/** + * @author Rob Harrop + */ +public interface ICustomJmxBean extends ICustomBase { + + String getName(); + + void setName(String name); + + int getAge(); + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssemblerCustomTests.java b/spring-context/src/test/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssemblerCustomTests.java new file mode 100644 index 0000000..a3a2a80 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssemblerCustomTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.assembler; + +import javax.management.modelmbean.ModelMBeanAttributeInfo; +import javax.management.modelmbean.ModelMBeanInfo; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + * @author Chris Beams + */ +public class InterfaceBasedMBeanInfoAssemblerCustomTests extends AbstractJmxAssemblerTests { + + protected static final String OBJECT_NAME = "bean:name=testBean5"; + + + @Override + protected String getObjectName() { + return OBJECT_NAME; + } + + @Override + protected int getExpectedOperationCount() { + return 5; + } + + @Override + protected int getExpectedAttributeCount() { + return 2; + } + + @Override + protected MBeanInfoAssembler getAssembler() { + InterfaceBasedMBeanInfoAssembler assembler = new InterfaceBasedMBeanInfoAssembler(); + assembler.setManagedInterfaces(new Class[] {ICustomJmxBean.class}); + return assembler; + } + + @Test + public void testGetAgeIsReadOnly() throws Exception { + ModelMBeanInfo info = getMBeanInfoFromAssembler(); + ModelMBeanAttributeInfo attr = info.getAttribute(AGE_ATTRIBUTE); + + assertThat(attr.isReadable()).isTrue(); + assertThat(attr.isWritable()).isFalse(); + } + + @Override + protected String getApplicationContextPath() { + return "org/springframework/jmx/export/assembler/interfaceAssemblerCustom.xml"; + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssemblerMappedTests.java b/spring-context/src/test/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssemblerMappedTests.java new file mode 100644 index 0000000..15cc8a3 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssemblerMappedTests.java @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.assembler; + +import java.util.Properties; + +import javax.management.MBeanAttributeInfo; +import javax.management.modelmbean.ModelMBeanAttributeInfo; +import javax.management.modelmbean.ModelMBeanInfo; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Rob Harrop + * @author Chris Beams + */ +public class InterfaceBasedMBeanInfoAssemblerMappedTests extends AbstractJmxAssemblerTests { + + protected static final String OBJECT_NAME = "bean:name=testBean4"; + + @Test + public void testGetAgeIsReadOnly() throws Exception { + ModelMBeanInfo info = getMBeanInfoFromAssembler(); + ModelMBeanAttributeInfo attr = info.getAttribute(AGE_ATTRIBUTE); + + assertThat(attr.isReadable()).as("Age is not readable").isTrue(); + assertThat(attr.isWritable()).as("Age is not writable").isFalse(); + } + + @Test + public void testWithUnknownClass() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + getWithMapping("com.foo.bar.Unknown")); + } + + @Test + public void testWithNonInterface() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + getWithMapping("JmxTestBean")); + } + + @Test + public void testWithFallThrough() throws Exception { + InterfaceBasedMBeanInfoAssembler assembler = + getWithMapping("foobar", "org.springframework.jmx.export.assembler.ICustomJmxBean"); + assembler.setManagedInterfaces(new Class[] {IAdditionalTestMethods.class}); + + ModelMBeanInfo inf = assembler.getMBeanInfo(getBean(), getObjectName()); + MBeanAttributeInfo attr = inf.getAttribute("NickName"); + + assertNickName(attr); + } + + @Test + public void testNickNameIsExposed() throws Exception { + ModelMBeanInfo inf = (ModelMBeanInfo) getMBeanInfo(); + MBeanAttributeInfo attr = inf.getAttribute("NickName"); + + assertNickName(attr); + } + + @Override + protected String getObjectName() { + return OBJECT_NAME; + } + + @Override + protected int getExpectedOperationCount() { + return 7; + } + + @Override + protected int getExpectedAttributeCount() { + return 3; + } + + @Override + protected MBeanInfoAssembler getAssembler() throws Exception { + return getWithMapping( + "org.springframework.jmx.export.assembler.IAdditionalTestMethods, " + + "org.springframework.jmx.export.assembler.ICustomJmxBean"); + } + + @Override + protected String getApplicationContextPath() { + return "org/springframework/jmx/export/assembler/interfaceAssemblerMapped.xml"; + } + + private InterfaceBasedMBeanInfoAssembler getWithMapping(String mapping) { + return getWithMapping(OBJECT_NAME, mapping); + } + + private InterfaceBasedMBeanInfoAssembler getWithMapping(String name, String mapping) { + InterfaceBasedMBeanInfoAssembler assembler = new InterfaceBasedMBeanInfoAssembler(); + Properties props = new Properties(); + props.setProperty(name, mapping); + assembler.setInterfaceMappings(props); + assembler.afterPropertiesSet(); + return assembler; + } + + private void assertNickName(MBeanAttributeInfo attr) { + assertThat(attr).as("Nick Name should not be null").isNotNull(); + assertThat(attr.isWritable()).as("Nick Name should be writable").isTrue(); + assertThat(attr.isReadable()).as("Nick Name should be readable").isTrue(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssemblerTests.java b/spring-context/src/test/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssemblerTests.java new file mode 100644 index 0000000..699b563 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssemblerTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.assembler; + +/** + * @author Rob Harrop + */ +public class InterfaceBasedMBeanInfoAssemblerTests extends AbstractJmxAssemblerTests { + + @Override + protected String getObjectName() { + return "bean:name=testBean4"; + } + + @Override + protected int getExpectedOperationCount() { + return 7; + } + + @Override + protected int getExpectedAttributeCount() { + return 2; + } + + @Override + protected MBeanInfoAssembler getAssembler() { + return new InterfaceBasedMBeanInfoAssembler(); + } + + @Override + protected String getApplicationContextPath() { + return "org/springframework/jmx/export/assembler/interfaceAssembler.xml"; + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssemblerComboTests.java b/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssemblerComboTests.java new file mode 100644 index 0000000..4221476 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssemblerComboTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.assembler; + +import java.util.Properties; + +import javax.management.MBeanAttributeInfo; +import javax.management.modelmbean.ModelMBeanAttributeInfo; +import javax.management.modelmbean.ModelMBeanInfo; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @author Rob Harrop + * @author Chris Beams + */ +public class MethodExclusionMBeanInfoAssemblerComboTests extends AbstractJmxAssemblerTests { + + protected static final String OBJECT_NAME = "bean:name=testBean4"; + + @Test + public void testGetAgeIsReadOnly() throws Exception { + ModelMBeanInfo info = getMBeanInfoFromAssembler(); + ModelMBeanAttributeInfo attr = info.getAttribute(AGE_ATTRIBUTE); + assertThat(attr.isReadable()).as("Age is not readable").isTrue(); + assertThat(attr.isWritable()).as("Age is not writable").isFalse(); + } + + @Test + public void testNickNameIsExposed() throws Exception { + ModelMBeanInfo inf = (ModelMBeanInfo) getMBeanInfo(); + MBeanAttributeInfo attr = inf.getAttribute("NickName"); + assertThat(attr).as("Nick Name should not be null").isNotNull(); + assertThat(attr.isWritable()).as("Nick Name should be writable").isTrue(); + assertThat(attr.isReadable()).as("Nick Name should be readable").isTrue(); + } + + @Override + protected String getObjectName() { + return OBJECT_NAME; + } + + @Override + protected int getExpectedOperationCount() { + return 7; + } + + @Override + protected int getExpectedAttributeCount() { + return 3; + } + + @Override + protected String getApplicationContextPath() { + return "org/springframework/jmx/export/assembler/methodExclusionAssemblerCombo.xml"; + } + + @Override + protected MBeanInfoAssembler getAssembler() throws Exception { + MethodExclusionMBeanInfoAssembler assembler = new MethodExclusionMBeanInfoAssembler(); + Properties props = new Properties(); + props.setProperty(OBJECT_NAME, "setAge,isSuperman,setSuperman,dontExposeMe"); + assembler.setIgnoredMethodMappings(props); + assembler.setIgnoredMethods(new String[] {"someMethod"}); + return assembler; + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssemblerMappedTests.java b/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssemblerMappedTests.java new file mode 100644 index 0000000..c5a35cc --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssemblerMappedTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.assembler; + +import java.util.Properties; + +import javax.management.MBeanAttributeInfo; +import javax.management.modelmbean.ModelMBeanAttributeInfo; +import javax.management.modelmbean.ModelMBeanInfo; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + * @author Chris Beams + */ +public class MethodExclusionMBeanInfoAssemblerMappedTests extends AbstractJmxAssemblerTests { + + protected static final String OBJECT_NAME = "bean:name=testBean4"; + + @Test + public void testGetAgeIsReadOnly() throws Exception { + ModelMBeanInfo info = getMBeanInfoFromAssembler(); + ModelMBeanAttributeInfo attr = info.getAttribute(AGE_ATTRIBUTE); + assertThat(attr.isReadable()).as("Age is not readable").isTrue(); + assertThat(attr.isWritable()).as("Age is not writable").isFalse(); + } + + @Test + public void testNickNameIsExposed() throws Exception { + ModelMBeanInfo inf = (ModelMBeanInfo) getMBeanInfo(); + MBeanAttributeInfo attr = inf.getAttribute("NickName"); + assertThat(attr).as("Nick Name should not be null").isNotNull(); + assertThat(attr.isWritable()).as("Nick Name should be writable").isTrue(); + assertThat(attr.isReadable()).as("Nick Name should be readable").isTrue(); + } + + @Override + protected String getObjectName() { + return OBJECT_NAME; + } + + @Override + protected int getExpectedOperationCount() { + return 7; + } + + @Override + protected int getExpectedAttributeCount() { + return 3; + } + + @Override + protected String getApplicationContextPath() { + return "org/springframework/jmx/export/assembler/methodExclusionAssemblerMapped.xml"; + } + + @Override + protected MBeanInfoAssembler getAssembler() throws Exception { + MethodExclusionMBeanInfoAssembler assembler = new MethodExclusionMBeanInfoAssembler(); + Properties props = new Properties(); + props.setProperty(OBJECT_NAME, "setAge,isSuperman,setSuperman,dontExposeMe"); + assembler.setIgnoredMethodMappings(props); + return assembler; + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssemblerNotMappedTests.java b/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssemblerNotMappedTests.java new file mode 100644 index 0000000..514a75d --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssemblerNotMappedTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.assembler; + +import java.util.Properties; + +import javax.management.MBeanAttributeInfo; +import javax.management.modelmbean.ModelMBeanAttributeInfo; +import javax.management.modelmbean.ModelMBeanInfo; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @author Rob Harrop + * @author Chris Beams + */ +public class MethodExclusionMBeanInfoAssemblerNotMappedTests extends AbstractJmxAssemblerTests { + + protected static final String OBJECT_NAME = "bean:name=testBean4"; + + @Test + public void testGetAgeIsReadOnly() throws Exception { + ModelMBeanInfo info = getMBeanInfoFromAssembler(); + ModelMBeanAttributeInfo attr = info.getAttribute(AGE_ATTRIBUTE); + assertThat(attr.isReadable()).as("Age is not readable").isTrue(); + assertThat(attr.isWritable()).as("Age is not writable").isTrue(); + } + + @Test + public void testNickNameIsExposed() throws Exception { + ModelMBeanInfo inf = (ModelMBeanInfo) getMBeanInfo(); + MBeanAttributeInfo attr = inf.getAttribute("NickName"); + assertThat(attr).as("Nick Name should not be null").isNotNull(); + assertThat(attr.isWritable()).as("Nick Name should be writable").isTrue(); + assertThat(attr.isReadable()).as("Nick Name should be readable").isTrue(); + } + + @Override + protected String getObjectName() { + return OBJECT_NAME; + } + + @Override + protected int getExpectedOperationCount() { + return 11; + } + + @Override + protected int getExpectedAttributeCount() { + return 4; + } + + @Override + protected String getApplicationContextPath() { + return "org/springframework/jmx/export/assembler/methodExclusionAssemblerNotMapped.xml"; + } + + @Override + protected MBeanInfoAssembler getAssembler() throws Exception { + MethodExclusionMBeanInfoAssembler assembler = new MethodExclusionMBeanInfoAssembler(); + Properties props = new Properties(); + props.setProperty("bean:name=testBean5", "setAge,isSuperman,setSuperman,dontExposeMe"); + assembler.setIgnoredMethodMappings(props); + return assembler; + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssemblerTests.java b/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssemblerTests.java new file mode 100644 index 0000000..ce6fa1b --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssemblerTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.assembler; + +import java.lang.reflect.Method; +import java.util.Properties; + +import javax.management.modelmbean.ModelMBeanAttributeInfo; +import javax.management.modelmbean.ModelMBeanInfo; + +import org.junit.jupiter.api.Test; + +import org.springframework.jmx.JmxTestBean; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + * @author Rick Evans + * @author Chris Beams + */ +public class MethodExclusionMBeanInfoAssemblerTests extends AbstractJmxAssemblerTests { + + private static final String OBJECT_NAME = "bean:name=testBean5"; + + + @Override + protected String getObjectName() { + return OBJECT_NAME; + } + + @Override + protected int getExpectedOperationCount() { + return 9; + } + + @Override + protected int getExpectedAttributeCount() { + return 4; + } + + @Override + protected String getApplicationContextPath() { + return "org/springframework/jmx/export/assembler/methodExclusionAssembler.xml"; + } + + @Override + protected MBeanInfoAssembler getAssembler() { + MethodExclusionMBeanInfoAssembler assembler = new MethodExclusionMBeanInfoAssembler(); + assembler.setIgnoredMethods(new String[] {"dontExposeMe", "setSuperman"}); + return assembler; + } + + @Test + public void testSupermanIsReadOnly() throws Exception { + ModelMBeanInfo info = getMBeanInfoFromAssembler(); + ModelMBeanAttributeInfo attr = info.getAttribute("Superman"); + + assertThat(attr.isReadable()).isTrue(); + assertThat(attr.isWritable()).isFalse(); + } + + /* + * https://opensource.atlassian.com/projects/spring/browse/SPR-2754 + */ + @Test + public void testIsNotIgnoredDoesntIgnoreUnspecifiedBeanMethods() throws Exception { + final String beanKey = "myTestBean"; + MethodExclusionMBeanInfoAssembler assembler = new MethodExclusionMBeanInfoAssembler(); + Properties ignored = new Properties(); + ignored.setProperty(beanKey, "dontExposeMe,setSuperman"); + assembler.setIgnoredMethodMappings(ignored); + Method method = JmxTestBean.class.getMethod("dontExposeMe"); + assertThat(assembler.isNotIgnored(method, beanKey)).isFalse(); + // this bean does not have any ignored methods on it, so must obviously not be ignored... + assertThat(assembler.isNotIgnored(method, "someOtherBeanKey")).isTrue(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodNameBasedMBeanInfoAssemblerMappedTests.java b/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodNameBasedMBeanInfoAssemblerMappedTests.java new file mode 100644 index 0000000..875d932 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodNameBasedMBeanInfoAssemblerMappedTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.assembler; + +import java.util.Properties; + +import javax.management.MBeanAttributeInfo; +import javax.management.modelmbean.ModelMBeanAttributeInfo; +import javax.management.modelmbean.ModelMBeanInfo; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + * @author Chris Beams + */ +public class MethodNameBasedMBeanInfoAssemblerMappedTests extends AbstractJmxAssemblerTests { + + protected static final String OBJECT_NAME = "bean:name=testBean4"; + + + @Test + public void testGetAgeIsReadOnly() throws Exception { + ModelMBeanInfo info = getMBeanInfoFromAssembler(); + ModelMBeanAttributeInfo attr = info.getAttribute(AGE_ATTRIBUTE); + + assertThat(attr.isReadable()).as("Age is not readable").isTrue(); + assertThat(attr.isWritable()).as("Age is not writable").isFalse(); + } + + @Test + public void testWithFallThrough() throws Exception { + MethodNameBasedMBeanInfoAssembler assembler = + getWithMapping("foobar", "add,myOperation,getName,setName,getAge"); + assembler.setManagedMethods("getNickName", "setNickName"); + + ModelMBeanInfo inf = assembler.getMBeanInfo(getBean(), getObjectName()); + MBeanAttributeInfo attr = inf.getAttribute("NickName"); + + assertNickName(attr); + } + + @Test + public void testNickNameIsExposed() throws Exception { + ModelMBeanInfo inf = (ModelMBeanInfo) getMBeanInfo(); + MBeanAttributeInfo attr = inf.getAttribute("NickName"); + + assertNickName(attr); + } + + @Override + protected String getObjectName() { + return OBJECT_NAME; + } + + @Override + protected int getExpectedOperationCount() { + return 7; + } + + @Override + protected int getExpectedAttributeCount() { + return 3; + } + + @Override + protected MBeanInfoAssembler getAssembler() throws Exception { + return getWithMapping("getNickName,setNickName,add,myOperation,getName,setName,getAge"); + } + + @Override + protected String getApplicationContextPath() { + return "org/springframework/jmx/export/assembler/methodNameAssemblerMapped.xml"; + } + + private MethodNameBasedMBeanInfoAssembler getWithMapping(String mapping) { + return getWithMapping(OBJECT_NAME, mapping); + } + + private MethodNameBasedMBeanInfoAssembler getWithMapping(String name, String mapping) { + MethodNameBasedMBeanInfoAssembler assembler = new MethodNameBasedMBeanInfoAssembler(); + Properties props = new Properties(); + props.setProperty(name, mapping); + assembler.setMethodMappings(props); + return assembler; + } + + private void assertNickName(MBeanAttributeInfo attr) { + assertThat(attr).as("Nick Name should not be null").isNotNull(); + assertThat(attr.isWritable()).as("Nick Name should be writable").isTrue(); + assertThat(attr.isReadable()).as("Nick Name should be readable").isTrue(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodNameBasedMBeanInfoAssemblerTests.java b/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodNameBasedMBeanInfoAssemblerTests.java new file mode 100644 index 0000000..f287abb --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/assembler/MethodNameBasedMBeanInfoAssemblerTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.assembler; + +import javax.management.MBeanOperationInfo; +import javax.management.modelmbean.ModelMBeanAttributeInfo; +import javax.management.modelmbean.ModelMBeanInfo; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + * @author David Boden + * @author Chris Beams + */ +public class MethodNameBasedMBeanInfoAssemblerTests extends AbstractJmxAssemblerTests { + + protected static final String OBJECT_NAME = "bean:name=testBean5"; + + + @Override + protected String getObjectName() { + return OBJECT_NAME; + } + + @Override + protected int getExpectedOperationCount() { + return 5; + } + + @Override + protected int getExpectedAttributeCount() { + return 2; + } + + @Override + protected MBeanInfoAssembler getAssembler() { + MethodNameBasedMBeanInfoAssembler assembler = new MethodNameBasedMBeanInfoAssembler(); + assembler.setManagedMethods("add", "myOperation", "getName", "setName", "getAge"); + return assembler; + } + + @Test + public void testGetAgeIsReadOnly() throws Exception { + ModelMBeanInfo info = getMBeanInfoFromAssembler(); + ModelMBeanAttributeInfo attr = info.getAttribute(AGE_ATTRIBUTE); + + assertThat(attr.isReadable()).isTrue(); + assertThat(attr.isWritable()).isFalse(); + } + + @Test + public void testSetNameParameterIsNamed() throws Exception { + ModelMBeanInfo info = getMBeanInfoFromAssembler(); + + MBeanOperationInfo operationSetAge = info.getOperation("setName"); + assertThat(operationSetAge.getSignature()[0].getName()).isEqualTo("name"); + } + + @Override + protected String getApplicationContextPath() { + return "org/springframework/jmx/export/assembler/methodNameAssembler.xml"; + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/assembler/ReflectiveAssemblerTests.java b/spring-context/src/test/java/org/springframework/jmx/export/assembler/ReflectiveAssemblerTests.java new file mode 100644 index 0000000..54b3c54 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/assembler/ReflectiveAssemblerTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.assembler; + +/** + * @author Rob Harrop + */ +public class ReflectiveAssemblerTests extends AbstractJmxAssemblerTests { + + protected static final String OBJECT_NAME = "bean:name=testBean1"; + + + @Override + protected String getObjectName() { + return OBJECT_NAME; + } + + @Override + protected int getExpectedOperationCount() { + return 11; + } + + @Override + protected int getExpectedAttributeCount() { + return 4; + } + + @Override + protected MBeanInfoAssembler getAssembler() { + return new SimpleReflectiveMBeanInfoAssembler(); + } + + @Override + protected String getApplicationContextPath() { + return "org/springframework/jmx/export/assembler/reflectiveAssembler.xml"; + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/naming/AbstractNamingStrategyTests.java b/spring-context/src/test/java/org/springframework/jmx/export/naming/AbstractNamingStrategyTests.java new file mode 100644 index 0000000..404c7df --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/naming/AbstractNamingStrategyTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.naming; + +import javax.management.ObjectName; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + */ +public abstract class AbstractNamingStrategyTests { + + @Test + public void naming() throws Exception { + ObjectNamingStrategy strat = getStrategy(); + ObjectName objectName = strat.getObjectName(getManagedResource(), getKey()); + assertThat(getCorrectObjectName()).isEqualTo(objectName.getCanonicalName()); + } + + protected abstract ObjectNamingStrategy getStrategy() throws Exception; + + protected abstract Object getManagedResource() throws Exception; + + protected abstract String getKey(); + + protected abstract String getCorrectObjectName(); + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/naming/IdentityNamingStrategyTests.java b/spring-context/src/test/java/org/springframework/jmx/export/naming/IdentityNamingStrategyTests.java new file mode 100644 index 0000000..3aa4baa --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/naming/IdentityNamingStrategyTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.naming; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.junit.jupiter.api.Test; + +import org.springframework.jmx.JmxTestBean; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + */ +public class IdentityNamingStrategyTests { + + @Test + public void naming() throws MalformedObjectNameException { + JmxTestBean bean = new JmxTestBean(); + IdentityNamingStrategy strategy = new IdentityNamingStrategy(); + ObjectName objectName = strategy.getObjectName(bean, "null"); + assertThat(objectName.getDomain()).as("Domain is incorrect").isEqualTo(bean.getClass().getPackage().getName()); + assertThat(objectName.getKeyProperty(IdentityNamingStrategy.TYPE_KEY)).as("Type property is incorrect").isEqualTo(ClassUtils.getShortName(bean.getClass())); + assertThat(objectName.getKeyProperty(IdentityNamingStrategy.HASH_CODE_KEY)).as("HashCode property is incorrect").isEqualTo(ObjectUtils.getIdentityHexString(bean)); + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/naming/KeyNamingStrategyTests.java b/spring-context/src/test/java/org/springframework/jmx/export/naming/KeyNamingStrategyTests.java new file mode 100644 index 0000000..53b6436 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/naming/KeyNamingStrategyTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.naming; + +/** + * @author Rob Harrop + */ +public class KeyNamingStrategyTests extends AbstractNamingStrategyTests { + + private static final String OBJECT_NAME = "spring:name=test"; + + + @Override + protected ObjectNamingStrategy getStrategy() throws Exception { + return new KeyNamingStrategy(); + } + + @Override + protected Object getManagedResource() { + return new Object(); + } + + @Override + protected String getKey() { + return OBJECT_NAME; + } + + @Override + protected String getCorrectObjectName() { + return OBJECT_NAME; + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/naming/PropertiesFileNamingStrategyTests.java b/spring-context/src/test/java/org/springframework/jmx/export/naming/PropertiesFileNamingStrategyTests.java new file mode 100644 index 0000000..aaade22 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/naming/PropertiesFileNamingStrategyTests.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.naming; + +import org.springframework.core.io.ClassPathResource; + +/** + * @author Juergen Hoeller + */ +public class PropertiesFileNamingStrategyTests extends PropertiesNamingStrategyTests { + + @Override + protected ObjectNamingStrategy getStrategy() throws Exception { + KeyNamingStrategy strat = new KeyNamingStrategy(); + strat.setMappingLocation(new ClassPathResource("jmx-names.properties", getClass())); + strat.afterPropertiesSet(); + return strat; + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/naming/PropertiesNamingStrategyTests.java b/spring-context/src/test/java/org/springframework/jmx/export/naming/PropertiesNamingStrategyTests.java new file mode 100644 index 0000000..d2a2a41 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/naming/PropertiesNamingStrategyTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.naming; + +import java.util.Properties; + +/** + * @author Rob Harrop + * @author Juergen Hoeller + */ +public class PropertiesNamingStrategyTests extends AbstractNamingStrategyTests { + + private static final String OBJECT_NAME = "bean:name=namingTest"; + + + @Override + protected ObjectNamingStrategy getStrategy() throws Exception { + KeyNamingStrategy strat = new KeyNamingStrategy(); + Properties mappings = new Properties(); + mappings.setProperty("namingTest", "bean:name=namingTest"); + strat.setMappings(mappings); + strat.afterPropertiesSet(); + return strat; + } + + @Override + protected Object getManagedResource() { + return new Object(); + } + + @Override + protected String getKey() { + return "namingTest"; + } + + @Override + protected String getCorrectObjectName() { + return OBJECT_NAME; + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/export/notification/ModelMBeanNotificationPublisherTests.java b/spring-context/src/test/java/org/springframework/jmx/export/notification/ModelMBeanNotificationPublisherTests.java new file mode 100644 index 0000000..b8d28a9 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/notification/ModelMBeanNotificationPublisherTests.java @@ -0,0 +1,135 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.export.notification; + +import javax.management.AttributeChangeNotification; +import javax.management.MBeanException; +import javax.management.MalformedObjectNameException; +import javax.management.Notification; +import javax.management.ObjectName; +import javax.management.RuntimeOperationsException; + +import org.junit.jupiter.api.Test; + +import org.springframework.jmx.export.SpringModelMBean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Rick Evans + * @author Chris Beams + */ +public class ModelMBeanNotificationPublisherTests { + + @Test + public void testCtorWithNullMBean() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new ModelMBeanNotificationPublisher(null, createObjectName(), this)); + } + + @Test + public void testCtorWithNullObjectName() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new ModelMBeanNotificationPublisher(new SpringModelMBean(), null, this)); + } + + @Test + public void testCtorWithNullManagedResource() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new ModelMBeanNotificationPublisher(new SpringModelMBean(), createObjectName(), null)); + } + + @Test + public void testSendNullNotification() throws Exception { + NotificationPublisher publisher + = new ModelMBeanNotificationPublisher(new SpringModelMBean(), createObjectName(), this); + assertThatIllegalArgumentException().isThrownBy(() -> + publisher.sendNotification(null)); + } + + public void testSendVanillaNotification() throws Exception { + StubSpringModelMBean mbean = new StubSpringModelMBean(); + Notification notification = new Notification("network.alarm.router", mbean, 1872); + ObjectName objectName = createObjectName(); + + NotificationPublisher publisher = new ModelMBeanNotificationPublisher(mbean, objectName, mbean); + publisher.sendNotification(notification); + + assertThat(mbean.getActualNotification()).isNotNull(); + assertThat(mbean.getActualNotification()).as("The exact same Notification is not being passed through from the publisher to the mbean.").isSameAs(notification); + assertThat(mbean.getActualNotification().getSource()).as("The 'source' property of the Notification is not being set to the ObjectName of the associated MBean.").isSameAs(objectName); + } + + public void testSendAttributeChangeNotification() throws Exception { + StubSpringModelMBean mbean = new StubSpringModelMBean(); + Notification notification = new AttributeChangeNotification(mbean, 1872, System.currentTimeMillis(), "Shall we break for some tea?", "agree", "java.lang.Boolean", Boolean.FALSE, Boolean.TRUE); + ObjectName objectName = createObjectName(); + + NotificationPublisher publisher = new ModelMBeanNotificationPublisher(mbean, objectName, mbean); + publisher.sendNotification(notification); + + assertThat(mbean.getActualNotification()).isNotNull(); + boolean condition = mbean.getActualNotification() instanceof AttributeChangeNotification; + assertThat(condition).isTrue(); + assertThat(mbean.getActualNotification()).as("The exact same Notification is not being passed through from the publisher to the mbean.").isSameAs(notification); + assertThat(mbean.getActualNotification().getSource()).as("The 'source' property of the Notification is not being set to the ObjectName of the associated MBean.").isSameAs(objectName); + } + + public void testSendAttributeChangeNotificationWhereSourceIsNotTheManagedResource() throws Exception { + StubSpringModelMBean mbean = new StubSpringModelMBean(); + Notification notification = new AttributeChangeNotification(this, 1872, System.currentTimeMillis(), "Shall we break for some tea?", "agree", "java.lang.Boolean", Boolean.FALSE, Boolean.TRUE); + ObjectName objectName = createObjectName(); + + NotificationPublisher publisher = new ModelMBeanNotificationPublisher(mbean, objectName, mbean); + publisher.sendNotification(notification); + + assertThat(mbean.getActualNotification()).isNotNull(); + boolean condition = mbean.getActualNotification() instanceof AttributeChangeNotification; + assertThat(condition).isTrue(); + assertThat(mbean.getActualNotification()).as("The exact same Notification is not being passed through from the publisher to the mbean.").isSameAs(notification); + assertThat(mbean.getActualNotification().getSource()).as("The 'source' property of the Notification is *wrongly* being set to the ObjectName of the associated MBean.").isSameAs(this); + } + + private static ObjectName createObjectName() throws MalformedObjectNameException { + return ObjectName.getInstance("foo:type=bar"); + } + + + private static class StubSpringModelMBean extends SpringModelMBean { + + private Notification actualNotification; + + public StubSpringModelMBean() throws MBeanException, RuntimeOperationsException { + } + + public Notification getActualNotification() { + return this.actualNotification; + } + + @Override + public void sendNotification(Notification notification) throws RuntimeOperationsException { + this.actualNotification = notification; + } + + @Override + public void sendAttributeChangeNotification(AttributeChangeNotification notification) throws RuntimeOperationsException { + this.actualNotification = notification; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/support/ConnectorServerFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/jmx/support/ConnectorServerFactoryBeanTests.java new file mode 100644 index 0000000..111721d --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/support/ConnectorServerFactoryBeanTests.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.support; + +import java.io.IOException; +import java.net.MalformedURLException; + +import javax.management.InstanceNotFoundException; +import javax.management.MBeanServer; +import javax.management.MBeanServerConnection; +import javax.management.ObjectInstance; +import javax.management.ObjectName; +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXServiceURL; + +import org.junit.jupiter.api.Test; + +import org.springframework.jmx.AbstractMBeanServerTests; +import org.springframework.util.SocketUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Integration tests for {@link ConnectorServerFactoryBean}. + * + * @author Rob Harrop + * @author Chris Beams + * @author Sam Brannen + */ +class ConnectorServerFactoryBeanTests extends AbstractMBeanServerTests { + + private static final String OBJECT_NAME = "spring:type=connector,name=test"; + + private final String serviceUrl = "service:jmx:jmxmp://localhost:" + SocketUtils.findAvailableTcpPort(); + + + @Test + void startupWithLocatedServer() throws Exception { + ConnectorServerFactoryBean bean = new ConnectorServerFactoryBean(); + bean.setServiceUrl(this.serviceUrl); + bean.afterPropertiesSet(); + + try { + checkServerConnection(getServer()); + } + finally { + bean.destroy(); + } + } + + @Test + void startupWithSuppliedServer() throws Exception { + ConnectorServerFactoryBean bean = new ConnectorServerFactoryBean(); + bean.setServiceUrl(this.serviceUrl); + bean.setServer(getServer()); + bean.afterPropertiesSet(); + + try { + checkServerConnection(getServer()); + } + finally { + bean.destroy(); + } + } + + @Test + void registerWithMBeanServer() throws Exception { + ConnectorServerFactoryBean bean = new ConnectorServerFactoryBean(); + bean.setServiceUrl(this.serviceUrl); + bean.setObjectName(OBJECT_NAME); + bean.afterPropertiesSet(); + + try { + // Try to get the connector bean. + ObjectInstance instance = getServer().getObjectInstance(ObjectName.getInstance(OBJECT_NAME)); + assertThat(instance).as("ObjectInstance should not be null").isNotNull(); + } + finally { + bean.destroy(); + } + } + + @Test + void noRegisterWithMBeanServer() throws Exception { + ConnectorServerFactoryBean bean = new ConnectorServerFactoryBean(); + bean.setServiceUrl(this.serviceUrl); + bean.afterPropertiesSet(); + try { + // Try to get the connector bean. + assertThatExceptionOfType(InstanceNotFoundException.class).isThrownBy(() -> + getServer().getObjectInstance(ObjectName.getInstance(OBJECT_NAME))); + } + finally { + bean.destroy(); + } + } + + private void checkServerConnection(MBeanServer hostedServer) throws IOException, MalformedURLException { + // Try to connect using client. + JMXServiceURL serviceURL = new JMXServiceURL(this.serviceUrl); + JMXConnector connector = JMXConnectorFactory.connect(serviceURL); + + assertThat(connector).as("Client Connector should not be null").isNotNull(); + + // Get the MBean server connection. + MBeanServerConnection connection = connector.getMBeanServerConnection(); + assertThat(connection).as("MBeanServerConnection should not be null").isNotNull(); + + // Test for MBean server equality. + assertThat(connection.getMBeanCount()).as("Registered MBean count should be the same").isEqualTo(hostedServer.getMBeanCount()); + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/support/JmxUtilsTests.java b/spring-context/src/test/java/org/springframework/jmx/support/JmxUtilsTests.java new file mode 100644 index 0000000..f8ef434 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/support/JmxUtilsTests.java @@ -0,0 +1,254 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.support; + +import java.beans.PropertyDescriptor; + +import javax.management.DynamicMBean; +import javax.management.MBeanServer; +import javax.management.MBeanServerFactory; +import javax.management.MalformedObjectNameException; +import javax.management.NotCompliantMBeanException; +import javax.management.ObjectName; +import javax.management.StandardMBean; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.BeanWrapperImpl; +import org.springframework.jmx.IJmxTestBean; +import org.springframework.jmx.JmxTestBean; +import org.springframework.jmx.export.TestDynamicMBean; +import org.springframework.util.ObjectUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link JmxUtils}. + * + * @author Rob Harrop + * @author Juergen Hoeller + */ +class JmxUtilsTests { + + @Test + void isMBean() { + // Correctly returns true for a class + assertThat(JmxUtils.isMBean(JmxClass.class)).isTrue(); + + // Correctly returns false since JmxUtils won't navigate to the extended interface + assertThat(JmxUtils.isMBean(SpecializedJmxInterface.class)).isFalse(); + + // Incorrectly returns true since it doesn't detect that this is an interface + assertThat(JmxUtils.isMBean(JmxInterface.class)).isFalse(); + } + + @Test + void isMBeanWithDynamicMBean() throws Exception { + DynamicMBean mbean = new TestDynamicMBean(); + assertThat(JmxUtils.isMBean(mbean.getClass())).as("Dynamic MBean not detected correctly").isTrue(); + } + + @Test + void isMBeanWithStandardMBeanWrapper() throws Exception { + StandardMBean mbean = new StandardMBean(new JmxTestBean(), IJmxTestBean.class); + assertThat(JmxUtils.isMBean(mbean.getClass())).as("Standard MBean not detected correctly").isTrue(); + } + + @Test + void isMBeanWithStandardMBeanInherited() throws Exception { + StandardMBean mbean = new StandardMBeanImpl(); + assertThat(JmxUtils.isMBean(mbean.getClass())).as("Standard MBean not detected correctly").isTrue(); + } + + @Test + void notAnMBean() throws Exception { + assertThat(JmxUtils.isMBean(Object.class)).as("Object incorrectly identified as an MBean").isFalse(); + } + + @Test + void simpleMBean() throws Exception { + Foo foo = new Foo(); + assertThat(JmxUtils.isMBean(foo.getClass())).as("Simple MBean not detected correctly").isTrue(); + } + + @Test + void simpleMXBean() throws Exception { + FooX foo = new FooX(); + assertThat(JmxUtils.isMBean(foo.getClass())).as("Simple MXBean not detected correctly").isTrue(); + } + + @Test + void simpleMBeanThroughInheritance() throws Exception { + Bar bar = new Bar(); + Abc abc = new Abc(); + assertThat(JmxUtils.isMBean(bar.getClass())).as("Simple MBean (through inheritance) not detected correctly").isTrue(); + assertThat(JmxUtils.isMBean(abc.getClass())).as("Simple MBean (through 2 levels of inheritance) not detected correctly").isTrue(); + } + + @Test + void getAttributeNameWithStrictCasing() { + PropertyDescriptor pd = new BeanWrapperImpl(AttributeTestBean.class).getPropertyDescriptor("name"); + String attributeName = JmxUtils.getAttributeName(pd, true); + assertThat(attributeName).as("Incorrect casing on attribute name").isEqualTo("Name"); + } + + @Test + void getAttributeNameWithoutStrictCasing() { + PropertyDescriptor pd = new BeanWrapperImpl(AttributeTestBean.class).getPropertyDescriptor("name"); + String attributeName = JmxUtils.getAttributeName(pd, false); + assertThat(attributeName).as("Incorrect casing on attribute name").isEqualTo("name"); + } + + @Test + void appendIdentityToObjectName() throws MalformedObjectNameException { + ObjectName objectName = ObjectNameManager.getInstance("spring:type=Test"); + Object managedResource = new Object(); + ObjectName uniqueName = JmxUtils.appendIdentityToObjectName(objectName, managedResource); + + String typeProperty = "type"; + + assertThat(uniqueName.getDomain()).as("Domain of transformed name is incorrect").isEqualTo(objectName.getDomain()); + assertThat(uniqueName.getKeyProperty("type")).as("Type key is incorrect").isEqualTo(objectName.getKeyProperty(typeProperty)); + assertThat(uniqueName.getKeyProperty(JmxUtils.IDENTITY_OBJECT_NAME_KEY)).as("Identity key is incorrect").isEqualTo(ObjectUtils.getIdentityHexString(managedResource)); + } + + @Test + void locatePlatformMBeanServer() { + MBeanServer server = null; + try { + server = JmxUtils.locateMBeanServer(); + } + finally { + if (server != null) { + MBeanServerFactory.releaseMBeanServer(server); + } + } + } + + + public static class AttributeTestBean { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + + public static class StandardMBeanImpl extends StandardMBean implements IJmxTestBean { + + public StandardMBeanImpl() throws NotCompliantMBeanException { + super(IJmxTestBean.class); + } + + @Override + public int add(int x, int y) { + return 0; + } + + @Override + public long myOperation() { + return 0; + } + + @Override + public int getAge() { + return 0; + } + + @Override + public void setAge(int age) { + } + + @Override + public void setName(String name) { + } + + @Override + public String getName() { + return null; + } + + @Override + public void dontExposeMe() { + } + } + + + public interface FooMBean { + + String getName(); + } + + + public static class Foo implements FooMBean { + + @Override + public String getName() { + return "Rob Harrop"; + } + } + + + public interface FooMXBean { + + String getName(); + } + + + public static class FooX implements FooMXBean { + + @Override + public String getName() { + return "Rob Harrop"; + } + } + + + public static class Bar extends Foo { + } + + + public static class Abc extends Bar { + } + + + private interface JmxInterfaceMBean { + } + + + private interface JmxInterface extends JmxInterfaceMBean { + } + + + private interface SpecializedJmxInterface extends JmxInterface { + } + + + private interface JmxClassMBean { + } + + + private static class JmxClass implements JmxClassMBean { + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/support/MBeanServerConnectionFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/jmx/support/MBeanServerConnectionFactoryBeanTests.java new file mode 100644 index 0000000..faee0ae --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/support/MBeanServerConnectionFactoryBeanTests.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.support; + +import javax.management.MBeanServerConnection; +import javax.management.remote.JMXConnectorServer; +import javax.management.remote.JMXConnectorServerFactory; +import javax.management.remote.JMXServiceURL; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.support.AopUtils; +import org.springframework.jmx.AbstractMBeanServerTests; +import org.springframework.util.SocketUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Integration tests for {@link MBeanServerConnectionFactoryBean}. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Sam Brannen + */ +class MBeanServerConnectionFactoryBeanTests extends AbstractMBeanServerTests { + + private final String serviceUrl = "service:jmx:jmxmp://localhost:" + SocketUtils.findAvailableTcpPort(); + + + @Test + void noServiceUrl() throws Exception { + MBeanServerConnectionFactoryBean bean = new MBeanServerConnectionFactoryBean(); + assertThatIllegalArgumentException() + .isThrownBy(bean::afterPropertiesSet) + .withMessage("Property 'serviceUrl' is required"); + } + + @Test + void validConnection() throws Exception { + JMXConnectorServer connectorServer = startConnectorServer(); + + try { + MBeanServerConnectionFactoryBean bean = new MBeanServerConnectionFactoryBean(); + bean.setServiceUrl(this.serviceUrl); + bean.afterPropertiesSet(); + + try { + MBeanServerConnection connection = bean.getObject(); + assertThat(connection).as("Connection should not be null").isNotNull(); + + // perform simple MBean count test + assertThat(connection.getMBeanCount()).as("MBean count should be the same").isEqualTo(getServer().getMBeanCount()); + } + finally { + bean.destroy(); + } + } + finally { + connectorServer.stop(); + } + } + + @Test + void lazyConnection() throws Exception { + MBeanServerConnectionFactoryBean bean = new MBeanServerConnectionFactoryBean(); + bean.setServiceUrl(this.serviceUrl); + bean.setConnectOnStartup(false); + bean.afterPropertiesSet(); + + MBeanServerConnection connection = bean.getObject(); + assertThat(AopUtils.isAopProxy(connection)).isTrue(); + + JMXConnectorServer connector = null; + try { + connector = startConnectorServer(); + assertThat(connection.getMBeanCount()).as("Incorrect MBean count").isEqualTo(getServer().getMBeanCount()); + } + finally { + bean.destroy(); + if (connector != null) { + connector.stop(); + } + } + } + + @Test + void lazyConnectionAndNoAccess() throws Exception { + MBeanServerConnectionFactoryBean bean = new MBeanServerConnectionFactoryBean(); + bean.setServiceUrl(this.serviceUrl); + bean.setConnectOnStartup(false); + bean.afterPropertiesSet(); + + MBeanServerConnection connection = bean.getObject(); + assertThat(AopUtils.isAopProxy(connection)).isTrue(); + bean.destroy(); + } + + private JMXConnectorServer startConnectorServer() throws Exception { + JMXServiceURL jmxServiceUrl = new JMXServiceURL(this.serviceUrl); + JMXConnectorServer connectorServer = JMXConnectorServerFactory.newJMXConnectorServer(jmxServiceUrl, null, getServer()); + connectorServer.start(); + return connectorServer; + } + +} diff --git a/spring-context/src/test/java/org/springframework/jmx/support/MBeanServerFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/jmx/support/MBeanServerFactoryBeanTests.java new file mode 100644 index 0000000..02a24b4 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/support/MBeanServerFactoryBeanTests.java @@ -0,0 +1,150 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jmx.support; + +import java.lang.management.ManagementFactory; +import java.util.List; + +import javax.management.MBeanServer; +import javax.management.MBeanServerFactory; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.util.MBeanTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link MBeanServerFactoryBean}. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Phillip Webb + * @author Sam Brannen + */ +class MBeanServerFactoryBeanTests { + + @BeforeEach + @AfterEach + void resetMBeanServers() throws Exception { + MBeanTestUtils.resetMBeanServers(); + } + + @Test + void defaultValues() throws Exception { + MBeanServerFactoryBean bean = new MBeanServerFactoryBean(); + bean.afterPropertiesSet(); + try { + MBeanServer server = bean.getObject(); + assertThat(server).as("The MBeanServer should not be null").isNotNull(); + } + finally { + bean.destroy(); + } + } + + @Test + void defaultDomain() throws Exception { + MBeanServerFactoryBean bean = new MBeanServerFactoryBean(); + bean.setDefaultDomain("foo"); + bean.afterPropertiesSet(); + try { + MBeanServer server = bean.getObject(); + assertThat(server.getDefaultDomain()).as("The default domain should be foo").isEqualTo("foo"); + } + finally { + bean.destroy(); + } + } + + @Test + void locateExistingServerIfPossibleWithExistingServer() { + MBeanServer server = MBeanServerFactory.createMBeanServer(); + try { + MBeanServerFactoryBean bean = new MBeanServerFactoryBean(); + bean.setLocateExistingServerIfPossible(true); + bean.afterPropertiesSet(); + try { + MBeanServer otherServer = bean.getObject(); + assertThat(otherServer).as("Existing MBeanServer not located").isSameAs(server); + } + finally { + bean.destroy(); + } + } + finally { + MBeanServerFactory.releaseMBeanServer(server); + } + } + + @Test + void locateExistingServerIfPossibleWithFallbackToPlatformServer() { + MBeanServerFactoryBean bean = new MBeanServerFactoryBean(); + bean.setLocateExistingServerIfPossible(true); + bean.afterPropertiesSet(); + try { + assertThat(bean.getObject()).isSameAs(ManagementFactory.getPlatformMBeanServer()); + } + finally { + bean.destroy(); + } + } + + @Test + void withEmptyAgentIdAndFallbackToPlatformServer() { + MBeanServerFactoryBean bean = new MBeanServerFactoryBean(); + bean.setAgentId(""); + bean.afterPropertiesSet(); + try { + assertThat(bean.getObject()).isSameAs(ManagementFactory.getPlatformMBeanServer()); + } + finally { + bean.destroy(); + } + } + + @Test + void createMBeanServer() throws Exception { + assertCreation(true, "The server should be available in the list"); + } + + @Test + void newMBeanServer() throws Exception { + assertCreation(false, "The server should not be available in the list"); + } + + private void assertCreation(boolean referenceShouldExist, String failMsg) throws Exception { + MBeanServerFactoryBean bean = new MBeanServerFactoryBean(); + bean.setRegisterWithFactory(referenceShouldExist); + bean.afterPropertiesSet(); + try { + MBeanServer server = bean.getObject(); + List servers = MBeanServerFactory.findMBeanServer(null); + assertThat(hasInstance(servers, server)).as(failMsg).isEqualTo(referenceShouldExist); + } + finally { + bean.destroy(); + } + } + + private boolean hasInstance(List servers, MBeanServer server) { + return servers.stream().anyMatch(current -> current == server); + } + +} diff --git a/spring-context/src/test/java/org/springframework/jndi/JndiLocatorDelegateTests.java b/spring-context/src/test/java/org/springframework/jndi/JndiLocatorDelegateTests.java new file mode 100644 index 0000000..c3601d3 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jndi/JndiLocatorDelegateTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jndi; + +import java.lang.reflect.Field; + +import javax.naming.spi.NamingManager; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + + + +/** + * Tests for {@link JndiLocatorDelegate}. + * + * @author Phillip Webb + * @author Juergen Hoeller + */ +public class JndiLocatorDelegateTests { + + @Test + public void isDefaultJndiEnvironmentAvailableFalse() throws Exception { + Field builderField = NamingManager.class.getDeclaredField("initctx_factory_builder"); + builderField.setAccessible(true); + Object oldBuilder = builderField.get(null); + builderField.set(null, null); + + try { + assertThat(JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()).isEqualTo(false); + } + finally { + builderField.set(null, oldBuilder); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/jndi/JndiObjectFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/jndi/JndiObjectFactoryBeanTests.java new file mode 100644 index 0000000..d04a8cf --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jndi/JndiObjectFactoryBeanTests.java @@ -0,0 +1,399 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jndi; + +import javax.naming.Context; +import javax.naming.NamingException; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.testfixture.beans.DerivedTestBean; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.testfixture.jndi.ExpectedLookupTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author Rod Johnson + * @author Juergen Hoeller + * @author Chris Beams + */ +public class JndiObjectFactoryBeanTests { + + @Test + public void testNoJndiName() throws NamingException { + JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); + assertThatIllegalArgumentException().isThrownBy(jof::afterPropertiesSet); + } + + @Test + public void testLookupWithFullNameAndResourceRefTrue() throws Exception { + JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); + Object o = new Object(); + jof.setJndiTemplate(new ExpectedLookupTemplate("java:comp/env/foo", o)); + jof.setJndiName("java:comp/env/foo"); + jof.setResourceRef(true); + jof.afterPropertiesSet(); + assertThat(jof.getObject() == o).isTrue(); + } + + @Test + public void testLookupWithFullNameAndResourceRefFalse() throws Exception { + JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); + Object o = new Object(); + jof.setJndiTemplate(new ExpectedLookupTemplate("java:comp/env/foo", o)); + jof.setJndiName("java:comp/env/foo"); + jof.setResourceRef(false); + jof.afterPropertiesSet(); + assertThat(jof.getObject() == o).isTrue(); + } + + @Test + public void testLookupWithSchemeNameAndResourceRefTrue() throws Exception { + JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); + Object o = new Object(); + jof.setJndiTemplate(new ExpectedLookupTemplate("java:foo", o)); + jof.setJndiName("java:foo"); + jof.setResourceRef(true); + jof.afterPropertiesSet(); + assertThat(jof.getObject() == o).isTrue(); + } + + @Test + public void testLookupWithSchemeNameAndResourceRefFalse() throws Exception { + JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); + Object o = new Object(); + jof.setJndiTemplate(new ExpectedLookupTemplate("java:foo", o)); + jof.setJndiName("java:foo"); + jof.setResourceRef(false); + jof.afterPropertiesSet(); + assertThat(jof.getObject() == o).isTrue(); + } + + @Test + public void testLookupWithShortNameAndResourceRefTrue() throws Exception { + JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); + Object o = new Object(); + jof.setJndiTemplate(new ExpectedLookupTemplate("java:comp/env/foo", o)); + jof.setJndiName("foo"); + jof.setResourceRef(true); + jof.afterPropertiesSet(); + assertThat(jof.getObject() == o).isTrue(); + } + + @Test + public void testLookupWithShortNameAndResourceRefFalse() throws Exception { + JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); + Object o = new Object(); + jof.setJndiTemplate(new ExpectedLookupTemplate("java:comp/env/foo", o)); + jof.setJndiName("foo"); + jof.setResourceRef(false); + assertThatExceptionOfType(NamingException.class).isThrownBy(jof::afterPropertiesSet); + } + + @Test + public void testLookupWithArbitraryNameAndResourceRefFalse() throws Exception { + JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); + Object o = new Object(); + jof.setJndiTemplate(new ExpectedLookupTemplate("foo", o)); + jof.setJndiName("foo"); + jof.setResourceRef(false); + jof.afterPropertiesSet(); + assertThat(jof.getObject() == o).isTrue(); + } + + @Test + public void testLookupWithExpectedTypeAndMatch() throws Exception { + JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); + String s = ""; + jof.setJndiTemplate(new ExpectedLookupTemplate("foo", s)); + jof.setJndiName("foo"); + jof.setExpectedType(String.class); + jof.afterPropertiesSet(); + assertThat(jof.getObject() == s).isTrue(); + } + + @Test + public void testLookupWithExpectedTypeAndNoMatch() throws Exception { + JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); + jof.setJndiTemplate(new ExpectedLookupTemplate("foo", new Object())); + jof.setJndiName("foo"); + jof.setExpectedType(String.class); + assertThatExceptionOfType(NamingException.class).isThrownBy( + jof::afterPropertiesSet) + .withMessageContaining("java.lang.String"); + } + + @Test + public void testLookupWithDefaultObject() throws Exception { + JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); + jof.setJndiTemplate(new ExpectedLookupTemplate("foo", "")); + jof.setJndiName("myFoo"); + jof.setExpectedType(String.class); + jof.setDefaultObject("myString"); + jof.afterPropertiesSet(); + assertThat(jof.getObject()).isEqualTo("myString"); + } + + @Test + public void testLookupWithDefaultObjectAndExpectedType() throws Exception { + JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); + jof.setJndiTemplate(new ExpectedLookupTemplate("foo", "")); + jof.setJndiName("myFoo"); + jof.setExpectedType(String.class); + jof.setDefaultObject("myString"); + jof.afterPropertiesSet(); + assertThat(jof.getObject()).isEqualTo("myString"); + } + + @Test + public void testLookupWithDefaultObjectAndExpectedTypeConversion() throws Exception { + JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); + jof.setJndiTemplate(new ExpectedLookupTemplate("foo", "")); + jof.setJndiName("myFoo"); + jof.setExpectedType(Integer.class); + jof.setDefaultObject("5"); + jof.afterPropertiesSet(); + assertThat(jof.getObject()).isEqualTo(5); + } + + @Test + public void testLookupWithDefaultObjectAndExpectedTypeConversionViaBeanFactory() throws Exception { + JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); + jof.setJndiTemplate(new ExpectedLookupTemplate("foo", "")); + jof.setJndiName("myFoo"); + jof.setExpectedType(Integer.class); + jof.setDefaultObject("5"); + jof.setBeanFactory(new DefaultListableBeanFactory()); + jof.afterPropertiesSet(); + assertThat(jof.getObject()).isEqualTo(5); + } + + @Test + public void testLookupWithDefaultObjectAndExpectedTypeNoMatch() throws Exception { + JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); + jof.setJndiTemplate(new ExpectedLookupTemplate("foo", "")); + jof.setJndiName("myFoo"); + jof.setExpectedType(Boolean.class); + jof.setDefaultObject("5"); + assertThatIllegalArgumentException().isThrownBy(jof::afterPropertiesSet); + } + + @Test + public void testLookupWithProxyInterface() throws Exception { + JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); + TestBean tb = new TestBean(); + jof.setJndiTemplate(new ExpectedLookupTemplate("foo", tb)); + jof.setJndiName("foo"); + jof.setProxyInterface(ITestBean.class); + jof.afterPropertiesSet(); + boolean condition = jof.getObject() instanceof ITestBean; + assertThat(condition).isTrue(); + ITestBean proxy = (ITestBean) jof.getObject(); + assertThat(tb.getAge()).isEqualTo(0); + proxy.setAge(99); + assertThat(tb.getAge()).isEqualTo(99); + } + + @Test + public void testLookupWithProxyInterfaceAndDefaultObject() throws Exception { + JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); + TestBean tb = new TestBean(); + jof.setJndiTemplate(new ExpectedLookupTemplate("foo", tb)); + jof.setJndiName("myFoo"); + jof.setProxyInterface(ITestBean.class); + jof.setDefaultObject(Boolean.TRUE); + assertThatIllegalArgumentException().isThrownBy(jof::afterPropertiesSet); + } + + @Test + public void testLookupWithProxyInterfaceAndLazyLookup() throws Exception { + JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); + final TestBean tb = new TestBean(); + jof.setJndiTemplate(new JndiTemplate() { + @Override + public Object lookup(String name) { + if ("foo".equals(name)) { + tb.setName("tb"); + return tb; + } + return null; + } + }); + jof.setJndiName("foo"); + jof.setProxyInterface(ITestBean.class); + jof.setLookupOnStartup(false); + jof.afterPropertiesSet(); + boolean condition = jof.getObject() instanceof ITestBean; + assertThat(condition).isTrue(); + ITestBean proxy = (ITestBean) jof.getObject(); + assertThat(tb.getName()).isNull(); + assertThat(tb.getAge()).isEqualTo(0); + proxy.setAge(99); + assertThat(tb.getName()).isEqualTo("tb"); + assertThat(tb.getAge()).isEqualTo(99); + } + + @Test + public void testLookupWithProxyInterfaceWithNotCache() throws Exception { + JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); + final TestBean tb = new TestBean(); + jof.setJndiTemplate(new JndiTemplate() { + @Override + public Object lookup(String name) { + if ("foo".equals(name)) { + tb.setName("tb"); + tb.setAge(tb.getAge() + 1); + return tb; + } + return null; + } + }); + jof.setJndiName("foo"); + jof.setProxyInterface(ITestBean.class); + jof.setCache(false); + jof.afterPropertiesSet(); + boolean condition = jof.getObject() instanceof ITestBean; + assertThat(condition).isTrue(); + ITestBean proxy = (ITestBean) jof.getObject(); + assertThat(tb.getName()).isEqualTo("tb"); + assertThat(tb.getAge()).isEqualTo(1); + proxy.returnsThis(); + assertThat(tb.getAge()).isEqualTo(2); + proxy.haveBirthday(); + assertThat(tb.getAge()).isEqualTo(4); + } + + @Test + public void testLookupWithProxyInterfaceWithLazyLookupAndNotCache() throws Exception { + JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); + final TestBean tb = new TestBean(); + jof.setJndiTemplate(new JndiTemplate() { + @Override + public Object lookup(String name) { + if ("foo".equals(name)) { + tb.setName("tb"); + tb.setAge(tb.getAge() + 1); + return tb; + } + return null; + } + }); + jof.setJndiName("foo"); + jof.setProxyInterface(ITestBean.class); + jof.setLookupOnStartup(false); + jof.setCache(false); + jof.afterPropertiesSet(); + boolean condition = jof.getObject() instanceof ITestBean; + assertThat(condition).isTrue(); + ITestBean proxy = (ITestBean) jof.getObject(); + assertThat(tb.getName()).isNull(); + assertThat(tb.getAge()).isEqualTo(0); + proxy.returnsThis(); + assertThat(tb.getName()).isEqualTo("tb"); + assertThat(tb.getAge()).isEqualTo(1); + proxy.returnsThis(); + assertThat(tb.getAge()).isEqualTo(2); + proxy.haveBirthday(); + assertThat(tb.getAge()).isEqualTo(4); + } + + @Test + public void testLazyLookupWithoutProxyInterface() throws NamingException { + JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); + jof.setJndiName("foo"); + jof.setLookupOnStartup(false); + assertThatIllegalStateException().isThrownBy(jof::afterPropertiesSet); + } + + @Test + public void testNotCacheWithoutProxyInterface() throws NamingException { + JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); + jof.setJndiName("foo"); + jof.setCache(false); + jof.setLookupOnStartup(false); + assertThatIllegalStateException().isThrownBy(jof::afterPropertiesSet); + } + + @Test + public void testLookupWithProxyInterfaceAndExpectedTypeAndMatch() throws Exception { + JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); + TestBean tb = new TestBean(); + jof.setJndiTemplate(new ExpectedLookupTemplate("foo", tb)); + jof.setJndiName("foo"); + jof.setExpectedType(TestBean.class); + jof.setProxyInterface(ITestBean.class); + jof.afterPropertiesSet(); + boolean condition = jof.getObject() instanceof ITestBean; + assertThat(condition).isTrue(); + ITestBean proxy = (ITestBean) jof.getObject(); + assertThat(tb.getAge()).isEqualTo(0); + proxy.setAge(99); + assertThat(tb.getAge()).isEqualTo(99); + } + + @Test + public void testLookupWithProxyInterfaceAndExpectedTypeAndNoMatch() { + JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); + TestBean tb = new TestBean(); + jof.setJndiTemplate(new ExpectedLookupTemplate("foo", tb)); + jof.setJndiName("foo"); + jof.setExpectedType(DerivedTestBean.class); + jof.setProxyInterface(ITestBean.class); + assertThatExceptionOfType(NamingException.class).isThrownBy( + jof::afterPropertiesSet) + .withMessageContaining("org.springframework.beans.testfixture.beans.DerivedTestBean"); + } + + @Test + public void testLookupWithExposeAccessContext() throws Exception { + JndiObjectFactoryBean jof = new JndiObjectFactoryBean(); + TestBean tb = new TestBean(); + final Context mockCtx = mock(Context.class); + given(mockCtx.lookup("foo")).willReturn(tb); + jof.setJndiTemplate(new JndiTemplate() { + @Override + protected Context createInitialContext() { + return mockCtx; + } + }); + jof.setJndiName("foo"); + jof.setProxyInterface(ITestBean.class); + jof.setExposeAccessContext(true); + jof.afterPropertiesSet(); + boolean condition = jof.getObject() instanceof ITestBean; + assertThat(condition).isTrue(); + ITestBean proxy = (ITestBean) jof.getObject(); + assertThat(tb.getAge()).isEqualTo(0); + proxy.setAge(99); + assertThat(tb.getAge()).isEqualTo(99); + proxy.equals(proxy); + proxy.hashCode(); + proxy.toString(); + verify(mockCtx, times(2)).close(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/jndi/JndiPropertySourceTests.java b/spring-context/src/test/java/org/springframework/jndi/JndiPropertySourceTests.java new file mode 100644 index 0000000..9ece68c --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jndi/JndiPropertySourceTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jndi; + +import javax.naming.Context; +import javax.naming.NamingException; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.testfixture.jndi.SimpleNamingContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link JndiPropertySource}. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + */ +public class JndiPropertySourceTests { + + @Test + public void nonExistentProperty() { + JndiPropertySource ps = new JndiPropertySource("jndiProperties"); + assertThat(ps.getProperty("bogus")).isNull(); + } + + @Test + public void nameBoundWithoutPrefix() { + final SimpleNamingContext context = new SimpleNamingContext(); + context.bind("p1", "v1"); + + JndiTemplate jndiTemplate = new JndiTemplate() { + @Override + protected Context createInitialContext() throws NamingException { + return context; + } + }; + JndiLocatorDelegate jndiLocator = new JndiLocatorDelegate(); + jndiLocator.setResourceRef(true); + jndiLocator.setJndiTemplate(jndiTemplate); + + JndiPropertySource ps = new JndiPropertySource("jndiProperties", jndiLocator); + assertThat(ps.getProperty("p1")).isEqualTo("v1"); + } + + @Test + public void nameBoundWithPrefix() { + final SimpleNamingContext context = new SimpleNamingContext(); + context.bind("java:comp/env/p1", "v1"); + + JndiTemplate jndiTemplate = new JndiTemplate() { + @Override + protected Context createInitialContext() throws NamingException { + return context; + } + }; + JndiLocatorDelegate jndiLocator = new JndiLocatorDelegate(); + jndiLocator.setResourceRef(true); + jndiLocator.setJndiTemplate(jndiTemplate); + + JndiPropertySource ps = new JndiPropertySource("jndiProperties", jndiLocator); + assertThat(ps.getProperty("p1")).isEqualTo("v1"); + } + + @Test + public void propertyWithDefaultClauseInResourceRefMode() { + JndiLocatorDelegate jndiLocator = new JndiLocatorDelegate() { + @Override + public Object lookup(String jndiName) throws NamingException { + throw new IllegalStateException("Should not get called"); + } + }; + jndiLocator.setResourceRef(true); + + JndiPropertySource ps = new JndiPropertySource("jndiProperties", jndiLocator); + assertThat(ps.getProperty("propertyKey:defaultValue")).isNull(); + } + + @Test + public void propertyWithColonInNonResourceRefMode() { + JndiLocatorDelegate jndiLocator = new JndiLocatorDelegate() { + @Override + public Object lookup(String jndiName) throws NamingException { + assertThat(jndiName).isEqualTo("my:key"); + return "my:value"; + } + }; + jndiLocator.setResourceRef(false); + + JndiPropertySource ps = new JndiPropertySource("jndiProperties", jndiLocator); + assertThat(ps.getProperty("my:key")).isEqualTo("my:value"); + } + +} diff --git a/spring-context/src/test/java/org/springframework/jndi/JndiTemplateEditorTests.java b/spring-context/src/test/java/org/springframework/jndi/JndiTemplateEditorTests.java new file mode 100644 index 0000000..ab69807 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jndi/JndiTemplateEditorTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jndi; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Rod Johnson + * @author Chris Beams + */ +public class JndiTemplateEditorTests { + + @Test + public void testNullIsIllegalArgument() { + assertThatIllegalArgumentException().isThrownBy(() -> + new JndiTemplateEditor().setAsText(null)); + } + + @Test + public void testEmptyStringMeansNullEnvironment() { + JndiTemplateEditor je = new JndiTemplateEditor(); + je.setAsText(""); + JndiTemplate jt = (JndiTemplate) je.getValue(); + assertThat(jt.getEnvironment() == null).isTrue(); + } + + @Test + public void testCustomEnvironment() { + JndiTemplateEditor je = new JndiTemplateEditor(); + // These properties are meaningless for JNDI, but we don't worry about that: + // the underlying JNDI implementation will throw exceptions when the user tries + // to look anything up + je.setAsText("jndiInitialSomethingOrOther=org.springframework.myjndi.CompleteRubbish\nfoo=bar"); + JndiTemplate jt = (JndiTemplate) je.getValue(); + assertThat(jt.getEnvironment().size() == 2).isTrue(); + assertThat(jt.getEnvironment().getProperty("jndiInitialSomethingOrOther").equals("org.springframework.myjndi.CompleteRubbish")).isTrue(); + assertThat(jt.getEnvironment().getProperty("foo").equals("bar")).isTrue(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/jndi/JndiTemplateTests.java b/spring-context/src/test/java/org/springframework/jndi/JndiTemplateTests.java new file mode 100644 index 0000000..5cb9173 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jndi/JndiTemplateTests.java @@ -0,0 +1,166 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jndi; + +import javax.naming.Context; +import javax.naming.NameNotFoundException; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author Rod Johnson + * @author Juergen Hoeller + * @author Chris Beams + * @since 08.07.2003 + */ +public class JndiTemplateTests { + + @Test + public void testLookupSucceeds() throws Exception { + Object o = new Object(); + String name = "foo"; + final Context context = mock(Context.class); + given(context.lookup(name)).willReturn(o); + + JndiTemplate jt = new JndiTemplate() { + @Override + protected Context createInitialContext() { + return context; + } + }; + + Object o2 = jt.lookup(name); + assertThat(o2).isEqualTo(o); + verify(context).close(); + } + + @Test + public void testLookupFails() throws Exception { + NameNotFoundException ne = new NameNotFoundException(); + String name = "foo"; + final Context context = mock(Context.class); + given(context.lookup(name)).willThrow(ne); + + JndiTemplate jt = new JndiTemplate() { + @Override + protected Context createInitialContext() { + return context; + } + }; + + assertThatExceptionOfType(NameNotFoundException.class).isThrownBy(() -> + jt.lookup(name)); + verify(context).close(); + } + + @Test + public void testLookupReturnsNull() throws Exception { + String name = "foo"; + final Context context = mock(Context.class); + given(context.lookup(name)).willReturn(null); + + JndiTemplate jt = new JndiTemplate() { + @Override + protected Context createInitialContext() { + return context; + } + }; + + assertThatExceptionOfType(NameNotFoundException.class).isThrownBy(() -> + jt.lookup(name)); + verify(context).close(); + } + + @Test + public void testLookupFailsWithTypeMismatch() throws Exception { + Object o = new Object(); + String name = "foo"; + final Context context = mock(Context.class); + given(context.lookup(name)).willReturn(o); + + JndiTemplate jt = new JndiTemplate() { + @Override + protected Context createInitialContext() { + return context; + } + }; + + assertThatExceptionOfType(TypeMismatchNamingException.class).isThrownBy(() -> + jt.lookup(name, String.class)); + verify(context).close(); + } + + @Test + public void testBind() throws Exception { + Object o = new Object(); + String name = "foo"; + final Context context = mock(Context.class); + + JndiTemplate jt = new JndiTemplate() { + @Override + protected Context createInitialContext() { + return context; + } + }; + + jt.bind(name, o); + verify(context).bind(name, o); + verify(context).close(); + } + + @Test + public void testRebind() throws Exception { + Object o = new Object(); + String name = "foo"; + final Context context = mock(Context.class); + + JndiTemplate jt = new JndiTemplate() { + @Override + protected Context createInitialContext() { + return context; + } + }; + + jt.rebind(name, o); + verify(context).rebind(name, o); + verify(context).close(); + } + + @Test + public void testUnbind() throws Exception { + String name = "something"; + final Context context = mock(Context.class); + + JndiTemplate jt = new JndiTemplate() { + @Override + protected Context createInitialContext() { + return context; + } + }; + + jt.unbind(name); + verify(context).unbind(name); + verify(context).close(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/mock/env/MockEnvironment.java b/spring-context/src/test/java/org/springframework/mock/env/MockEnvironment.java new file mode 100644 index 0000000..6397865 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/mock/env/MockEnvironment.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.mock.env; + +import org.springframework.core.env.AbstractEnvironment; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.testfixture.env.MockPropertySource; + +/** + * Simple {@link ConfigurableEnvironment} implementation exposing + * {@link #setProperty} and {@link #withProperty} methods for testing purposes. + * + * @author Chris Beams + * @author Sam Brannen + * @since 3.2 + * @see org.springframework.core.testfixture.env.MockPropertySource + */ +public class MockEnvironment extends AbstractEnvironment { + + private final MockPropertySource propertySource = new MockPropertySource(); + + + /** + * Create a new {@code MockEnvironment} with a single {@link MockPropertySource}. + */ + public MockEnvironment() { + getPropertySources().addLast(this.propertySource); + } + + + /** + * Set a property on the underlying {@link MockPropertySource} for this environment. + */ + public void setProperty(String key, String value) { + this.propertySource.setProperty(key, value); + } + + /** + * Convenient synonym for {@link #setProperty} that returns the current instance. + * Useful for method chaining and fluent-style use. + * @return this {@link MockEnvironment} instance + * @see MockPropertySource#withProperty + */ + public MockEnvironment withProperty(String key, String value) { + setProperty(key, value); + return this; + } + +} diff --git a/spring-context/src/test/java/org/springframework/remoting/rmi/RmiSupportTests.java b/spring-context/src/test/java/org/springframework/remoting/rmi/RmiSupportTests.java new file mode 100644 index 0000000..8278025 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/remoting/rmi/RmiSupportTests.java @@ -0,0 +1,445 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.rmi; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.rmi.ConnectException; +import java.rmi.ConnectIOException; +import java.rmi.MarshalException; +import java.rmi.NoSuchObjectException; +import java.rmi.Remote; +import java.rmi.RemoteException; +import java.rmi.StubNotFoundException; +import java.rmi.UnknownHostException; +import java.rmi.UnmarshalException; + +import org.aopalliance.intercept.MethodInvocation; +import org.junit.jupiter.api.Test; + +import org.springframework.remoting.RemoteAccessException; +import org.springframework.remoting.RemoteConnectFailureException; +import org.springframework.remoting.RemoteProxyFailureException; +import org.springframework.remoting.support.RemoteInvocation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Juergen Hoeller + * @since 16.05.2003 + */ +public class RmiSupportTests { + + @Test + public void rmiProxyFactoryBean() throws Exception { + CountingRmiProxyFactoryBean factory = new CountingRmiProxyFactoryBean(); + factory.setServiceInterface(IRemoteBean.class); + factory.setServiceUrl("rmi://localhost:1090/test"); + factory.afterPropertiesSet(); + assertThat(factory.isSingleton()).as("Correct singleton value").isTrue(); + boolean condition = factory.getObject() instanceof IRemoteBean; + assertThat(condition).isTrue(); + IRemoteBean proxy = (IRemoteBean) factory.getObject(); + proxy.setName("myName"); + assertThat(RemoteBean.name).isEqualTo("myName"); + assertThat(factory.counter).isEqualTo(1); + } + + @Test + public void rmiProxyFactoryBeanWithRemoteException() throws Exception { + doTestRmiProxyFactoryBeanWithException(RemoteException.class); + } + + @Test + public void rmiProxyFactoryBeanWithConnectException() throws Exception { + doTestRmiProxyFactoryBeanWithException(ConnectException.class); + } + + @Test + public void rmiProxyFactoryBeanWithConnectIOException() throws Exception { + doTestRmiProxyFactoryBeanWithException(ConnectIOException.class); + } + + @Test + public void rmiProxyFactoryBeanWithUnknownHostException() throws Exception { + doTestRmiProxyFactoryBeanWithException(UnknownHostException.class); + } + + @Test + public void rmiProxyFactoryBeanWithNoSuchObjectException() throws Exception { + doTestRmiProxyFactoryBeanWithException(NoSuchObjectException.class); + } + + @Test + public void rmiProxyFactoryBeanWithStubNotFoundException() throws Exception { + doTestRmiProxyFactoryBeanWithException(StubNotFoundException.class); + } + + @Test + public void rmiProxyFactoryBeanWithMarshalException() throws Exception { + doTestRmiProxyFactoryBeanWithException(MarshalException.class); + } + + @Test + public void rmiProxyFactoryBeanWithUnmarshalException() throws Exception { + doTestRmiProxyFactoryBeanWithException(UnmarshalException.class); + } + + private void doTestRmiProxyFactoryBeanWithException(Class exceptionClass) throws Exception { + CountingRmiProxyFactoryBean factory = new CountingRmiProxyFactoryBean(); + factory.setServiceInterface(IRemoteBean.class); + factory.setServiceUrl("rmi://localhost:1090/test"); + factory.afterPropertiesSet(); + boolean condition = factory.getObject() instanceof IRemoteBean; + assertThat(condition).isTrue(); + IRemoteBean proxy = (IRemoteBean) factory.getObject(); + assertThatExceptionOfType(exceptionClass).isThrownBy(() -> + proxy.setName(exceptionClass.getName())); + assertThat(factory.counter).isEqualTo(1); + } + + @Test + public void rmiProxyFactoryBeanWithConnectExceptionAndRefresh() throws Exception { + doTestRmiProxyFactoryBeanWithExceptionAndRefresh(ConnectException.class); + } + + @Test + public void rmiProxyFactoryBeanWithConnectIOExceptionAndRefresh() throws Exception { + doTestRmiProxyFactoryBeanWithExceptionAndRefresh(ConnectIOException.class); + } + + @Test + public void rmiProxyFactoryBeanWithUnknownHostExceptionAndRefresh() throws Exception { + doTestRmiProxyFactoryBeanWithExceptionAndRefresh(UnknownHostException.class); + } + + @Test + public void rmiProxyFactoryBeanWithNoSuchObjectExceptionAndRefresh() throws Exception { + doTestRmiProxyFactoryBeanWithExceptionAndRefresh(NoSuchObjectException.class); + } + + @Test + public void rmiProxyFactoryBeanWithStubNotFoundExceptionAndRefresh() throws Exception { + doTestRmiProxyFactoryBeanWithExceptionAndRefresh(StubNotFoundException.class); + } + + private void doTestRmiProxyFactoryBeanWithExceptionAndRefresh(Class exceptionClass) throws Exception { + CountingRmiProxyFactoryBean factory = new CountingRmiProxyFactoryBean(); + factory.setServiceInterface(IRemoteBean.class); + factory.setServiceUrl("rmi://localhost:1090/test"); + factory.setRefreshStubOnConnectFailure(true); + factory.afterPropertiesSet(); + boolean condition = factory.getObject() instanceof IRemoteBean; + assertThat(condition).isTrue(); + IRemoteBean proxy = (IRemoteBean) factory.getObject(); + assertThatExceptionOfType(exceptionClass).isThrownBy(() -> + proxy.setName(exceptionClass.getName())); + assertThat(factory.counter).isEqualTo(2); + } + + @Test + public void rmiProxyFactoryBeanWithBusinessInterface() throws Exception { + CountingRmiProxyFactoryBean factory = new CountingRmiProxyFactoryBean(); + factory.setServiceInterface(IBusinessBean.class); + factory.setServiceUrl("rmi://localhost:1090/test"); + factory.afterPropertiesSet(); + boolean condition = factory.getObject() instanceof IBusinessBean; + assertThat(condition).isTrue(); + IBusinessBean proxy = (IBusinessBean) factory.getObject(); + boolean condition1 = proxy instanceof IRemoteBean; + assertThat(condition1).isFalse(); + proxy.setName("myName"); + assertThat(RemoteBean.name).isEqualTo("myName"); + assertThat(factory.counter).isEqualTo(1); + } + + @Test + public void rmiProxyFactoryBeanWithWrongBusinessInterface() throws Exception { + CountingRmiProxyFactoryBean factory = new CountingRmiProxyFactoryBean(); + factory.setServiceInterface(IWrongBusinessBean.class); + factory.setServiceUrl("rmi://localhost:1090/test"); + factory.afterPropertiesSet(); + boolean condition = factory.getObject() instanceof IWrongBusinessBean; + assertThat(condition).isTrue(); + IWrongBusinessBean proxy = (IWrongBusinessBean) factory.getObject(); + boolean condition1 = proxy instanceof IRemoteBean; + assertThat(condition1).isFalse(); + assertThatExceptionOfType(RemoteProxyFailureException.class).isThrownBy(() -> + proxy.setOtherName("name")) + .withCauseInstanceOf(NoSuchMethodException.class) + .withMessageContaining("setOtherName") + .withMessageContaining("IWrongBusinessBean"); + assertThat(factory.counter).isEqualTo(1); + } + + @Test + public void rmiProxyFactoryBeanWithBusinessInterfaceAndRemoteException() throws Exception { + doTestRmiProxyFactoryBeanWithBusinessInterfaceAndException( + RemoteException.class, RemoteAccessException.class); + } + + @Test + public void rmiProxyFactoryBeanWithBusinessInterfaceAndConnectException() throws Exception { + doTestRmiProxyFactoryBeanWithBusinessInterfaceAndException( + ConnectException.class, RemoteConnectFailureException.class); + } + + @Test + public void rmiProxyFactoryBeanWithBusinessInterfaceAndConnectIOException() throws Exception { + doTestRmiProxyFactoryBeanWithBusinessInterfaceAndException( + ConnectIOException.class, RemoteConnectFailureException.class); + } + + @Test + public void rmiProxyFactoryBeanWithBusinessInterfaceAndUnknownHostException() throws Exception { + doTestRmiProxyFactoryBeanWithBusinessInterfaceAndException( + UnknownHostException.class, RemoteConnectFailureException.class); + } + + @Test + public void rmiProxyFactoryBeanWithBusinessInterfaceAndNoSuchObjectExceptionException() throws Exception { + doTestRmiProxyFactoryBeanWithBusinessInterfaceAndException( + NoSuchObjectException.class, RemoteConnectFailureException.class); + } + + @Test + public void rmiProxyFactoryBeanWithBusinessInterfaceAndStubNotFoundException() throws Exception { + doTestRmiProxyFactoryBeanWithBusinessInterfaceAndException( + StubNotFoundException.class, RemoteConnectFailureException.class); + } + + private void doTestRmiProxyFactoryBeanWithBusinessInterfaceAndException( + Class rmiExceptionClass, Class springExceptionClass) throws Exception { + + CountingRmiProxyFactoryBean factory = new CountingRmiProxyFactoryBean(); + factory.setServiceInterface(IBusinessBean.class); + factory.setServiceUrl("rmi://localhost:1090/test"); + factory.afterPropertiesSet(); + boolean condition = factory.getObject() instanceof IBusinessBean; + assertThat(condition).isTrue(); + IBusinessBean proxy = (IBusinessBean) factory.getObject(); + boolean condition1 = proxy instanceof IRemoteBean; + assertThat(condition1).isFalse(); + assertThatExceptionOfType(springExceptionClass).isThrownBy(() -> + proxy.setName(rmiExceptionClass.getName())); + assertThat(factory.counter).isEqualTo(1); + } + + @Test + public void rmiProxyFactoryBeanWithBusinessInterfaceAndRemoteExceptionAndRefresh() throws Exception { + doTestRmiProxyFactoryBeanWithBusinessInterfaceAndExceptionAndRefresh( + RemoteException.class, RemoteAccessException.class); + } + + @Test + public void rmiProxyFactoryBeanWithBusinessInterfaceAndConnectExceptionAndRefresh() throws Exception { + doTestRmiProxyFactoryBeanWithBusinessInterfaceAndExceptionAndRefresh( + ConnectException.class, RemoteConnectFailureException.class); + } + + @Test + public void rmiProxyFactoryBeanWithBusinessInterfaceAndConnectIOExceptionAndRefresh() throws Exception { + doTestRmiProxyFactoryBeanWithBusinessInterfaceAndExceptionAndRefresh( + ConnectIOException.class, RemoteConnectFailureException.class); + } + + @Test + public void rmiProxyFactoryBeanWithBusinessInterfaceAndUnknownHostExceptionAndRefresh() throws Exception { + doTestRmiProxyFactoryBeanWithBusinessInterfaceAndExceptionAndRefresh( + UnknownHostException.class, RemoteConnectFailureException.class); + } + + @Test + public void rmiProxyFactoryBeanWithBusinessInterfaceAndNoSuchObjectExceptionAndRefresh() throws Exception { + doTestRmiProxyFactoryBeanWithBusinessInterfaceAndExceptionAndRefresh( + NoSuchObjectException.class, RemoteConnectFailureException.class); + } + + @Test + public void rmiProxyFactoryBeanWithBusinessInterfaceAndStubNotFoundExceptionAndRefresh() throws Exception { + doTestRmiProxyFactoryBeanWithBusinessInterfaceAndExceptionAndRefresh( + StubNotFoundException.class, RemoteConnectFailureException.class); + } + + private void doTestRmiProxyFactoryBeanWithBusinessInterfaceAndExceptionAndRefresh( + Class rmiExceptionClass, Class springExceptionClass) throws Exception { + + CountingRmiProxyFactoryBean factory = new CountingRmiProxyFactoryBean(); + factory.setServiceInterface(IBusinessBean.class); + factory.setServiceUrl("rmi://localhost:1090/test"); + factory.setRefreshStubOnConnectFailure(true); + factory.afterPropertiesSet(); + boolean condition = factory.getObject() instanceof IBusinessBean; + assertThat(condition).isTrue(); + IBusinessBean proxy = (IBusinessBean) factory.getObject(); + boolean condition1 = proxy instanceof IRemoteBean; + assertThat(condition1).isFalse(); + assertThatExceptionOfType(springExceptionClass).isThrownBy(() -> + proxy.setName(rmiExceptionClass.getName())); + boolean isRemoteConnectFailure = RemoteConnectFailureException.class.isAssignableFrom(springExceptionClass); + assertThat(factory.counter).isEqualTo(isRemoteConnectFailure ? 2 : 1); + } + + @Test + public void rmiClientInterceptorRequiresUrl() throws Exception{ + RmiClientInterceptor client = new RmiClientInterceptor(); + client.setServiceInterface(IRemoteBean.class); + assertThatIllegalArgumentException().isThrownBy(client::afterPropertiesSet); + } + + @Test + public void remoteInvocation() throws NoSuchMethodException { + // let's see if the remote invocation object works + + final RemoteBean rb = new RemoteBean(); + final Method setNameMethod = rb.getClass().getDeclaredMethod("setName", String.class); + + MethodInvocation mi = new MethodInvocation() { + @Override + public Method getMethod() { + return setNameMethod; + } + @Override + public Object[] getArguments() { + return new Object[] {"bla"}; + } + @Override + public Object proceed() throws Throwable { + throw new UnsupportedOperationException(); + } + @Override + public Object getThis() { + return rb; + } + @Override + public AccessibleObject getStaticPart() { + return setNameMethod; + } + }; + + RemoteInvocation inv = new RemoteInvocation(mi); + + assertThat(inv.getMethodName()).isEqualTo("setName"); + assertThat(inv.getArguments()[0]).isEqualTo("bla"); + assertThat(inv.getParameterTypes()[0]).isEqualTo(String.class); + + // this is a bit BS, but we need to test it + inv = new RemoteInvocation(); + inv.setArguments(new Object[] { "bla" }); + assertThat(inv.getArguments()[0]).isEqualTo("bla"); + inv.setMethodName("setName"); + assertThat(inv.getMethodName()).isEqualTo("setName"); + inv.setParameterTypes(new Class[] {String.class}); + assertThat(inv.getParameterTypes()[0]).isEqualTo(String.class); + + inv = new RemoteInvocation("setName", new Class[] {String.class}, new Object[] {"bla"}); + assertThat(inv.getArguments()[0]).isEqualTo("bla"); + assertThat(inv.getMethodName()).isEqualTo("setName"); + assertThat(inv.getParameterTypes()[0]).isEqualTo(String.class); + } + + @Test + public void rmiInvokerWithSpecialLocalMethods() throws Exception { + String serviceUrl = "rmi://localhost:1090/test"; + RmiProxyFactoryBean factory = new RmiProxyFactoryBean() { + @Override + protected Remote lookupStub() { + return new RmiInvocationHandler() { + @Override + public String getTargetInterfaceName() { + return null; + } + @Override + public Object invoke(RemoteInvocation invocation) throws RemoteException { + throw new RemoteException(); + } + }; + } + }; + factory.setServiceInterface(IBusinessBean.class); + factory.setServiceUrl(serviceUrl); + factory.afterPropertiesSet(); + IBusinessBean proxy = (IBusinessBean) factory.getObject(); + + // shouldn't go through to remote service + assertThat(proxy.toString().contains("RMI invoker")).isTrue(); + assertThat(proxy.toString().contains(serviceUrl)).isTrue(); + assertThat(proxy.hashCode()).isEqualTo(proxy.hashCode()); + assertThat(proxy.equals(proxy)).isTrue(); + + // should go through + assertThatExceptionOfType(RemoteAccessException.class).isThrownBy(() -> + proxy.setName("test")); + } + + + private static class CountingRmiProxyFactoryBean extends RmiProxyFactoryBean { + + private int counter = 0; + + @Override + protected Remote lookupStub() { + counter++; + return new RemoteBean(); + } + } + + + public interface IBusinessBean { + + void setName(String name); + } + + + public interface IWrongBusinessBean { + + void setOtherName(String name); + } + + + public interface IRemoteBean extends Remote { + + void setName(String name) throws RemoteException; + } + + + public static class RemoteBean implements IRemoteBean { + + private static String name; + + @Override + public void setName(String nam) throws RemoteException { + if (nam != null && nam.endsWith("Exception")) { + RemoteException rex; + try { + Class exClass = Class.forName(nam); + Constructor ctor = exClass.getConstructor(String.class); + rex = (RemoteException) ctor.newInstance("myMessage"); + } + catch (Exception ex) { + throw new RemoteException("Illegal exception class name: " + nam, ex); + } + throw rex; + } + name = nam; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/remoting/support/RemoteInvocationUtilsTests.java b/spring-context/src/test/java/org/springframework/remoting/support/RemoteInvocationUtilsTests.java new file mode 100644 index 0000000..dbaa7a4 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/remoting/support/RemoteInvocationUtilsTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.remoting.support; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rick Evans + */ +public class RemoteInvocationUtilsTests { + + @Test + public void fillInClientStackTraceIfPossibleSunnyDay() throws Exception { + try { + throw new IllegalStateException("Mmm"); + } + catch (Exception ex) { + int originalStackTraceLngth = ex.getStackTrace().length; + RemoteInvocationUtils.fillInClientStackTraceIfPossible(ex); + assertThat(ex.getStackTrace().length > originalStackTraceLngth).as("Stack trace not being filled in").isTrue(); + } + } + + @Test + public void fillInClientStackTraceIfPossibleWithNullThrowable() throws Exception { + // just want to ensure that it doesn't bomb + RemoteInvocationUtils.fillInClientStackTraceIfPossible(null); + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptorTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptorTests.java new file mode 100644 index 0000000..51566b0 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptorTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Unit tests for {@link AnnotationAsyncExecutionInterceptor}. + * + * @author Chris Beams + * @since 3.1.2 + */ +public class AnnotationAsyncExecutionInterceptorTests { + + @Test + @SuppressWarnings("unused") + public void testGetExecutorQualifier() throws SecurityException, NoSuchMethodException { + AnnotationAsyncExecutionInterceptor i = new AnnotationAsyncExecutionInterceptor(null); + { // method level + class C { @Async("qMethod") void m() { } } + assertThat(i.getExecutorQualifier(C.class.getDeclaredMethod("m"))).isEqualTo("qMethod"); + } + { // class level + @Async("qClass") class C { void m() { } } + assertThat(i.getExecutorQualifier(C.class.getDeclaredMethod("m"))).isEqualTo("qClass"); + } + { // method and class level -> method value overrides + @Async("qClass") class C { @Async("qMethod") void m() { } } + assertThat(i.getExecutorQualifier(C.class.getDeclaredMethod("m"))).isEqualTo("qMethod"); + } + { // method and class level -> method value, even if empty, overrides + @Async("qClass") class C { @Async void m() { } } + assertThat(i.getExecutorQualifier(C.class.getDeclaredMethod("m"))).isEqualTo(""); + } + { // meta annotation with qualifier + @MyAsync class C { void m() { } } + assertThat(i.getExecutorQualifier(C.class.getDeclaredMethod("m"))).isEqualTo("qMeta"); + } + } + + @Async("qMeta") + @Retention(RetentionPolicy.RUNTIME) + @interface MyAsync { } +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessorTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessorTests.java new file mode 100644 index 0000000..477a97d --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessorTests.java @@ -0,0 +1,364 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.annotation; + +import java.lang.reflect.Method; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.aopalliance.intercept.MethodInterceptor; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.GenericXmlApplicationContext; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.core.io.ClassPathResource; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.concurrent.ListenableFuture; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Mark Fisher + * @author Juergen Hoeller + * @author Stephane Nicoll + */ +public class AsyncAnnotationBeanPostProcessorTests { + + @Test + public void proxyCreated() { + ConfigurableApplicationContext context = initContext( + new RootBeanDefinition(AsyncAnnotationBeanPostProcessor.class)); + Object target = context.getBean("target"); + assertThat(AopUtils.isAopProxy(target)).isTrue(); + context.close(); + } + + @Test + public void invokedAsynchronously() { + ConfigurableApplicationContext context = initContext( + new RootBeanDefinition(AsyncAnnotationBeanPostProcessor.class)); + + ITestBean testBean = context.getBean("target", ITestBean.class); + testBean.test(); + Thread mainThread = Thread.currentThread(); + testBean.await(3000); + Thread asyncThread = testBean.getThread(); + assertThat(asyncThread).isNotSameAs(mainThread); + context.close(); + } + + @Test + public void invokedAsynchronouslyOnProxyTarget() { + StaticApplicationContext context = new StaticApplicationContext(); + context.registerBeanDefinition("postProcessor", new RootBeanDefinition(AsyncAnnotationBeanPostProcessor.class)); + TestBean tb = new TestBean(); + ProxyFactory pf = new ProxyFactory(ITestBean.class, + (MethodInterceptor) invocation -> invocation.getMethod().invoke(tb, invocation.getArguments())); + context.registerBean("target", ITestBean.class, () -> (ITestBean) pf.getProxy()); + context.refresh(); + + ITestBean testBean = context.getBean("target", ITestBean.class); + testBean.test(); + Thread mainThread = Thread.currentThread(); + testBean.await(3000); + Thread asyncThread = testBean.getThread(); + assertThat(asyncThread).isNotSameAs(mainThread); + context.close(); + } + + @Test + public void threadNamePrefix() { + BeanDefinition processorDefinition = new RootBeanDefinition(AsyncAnnotationBeanPostProcessor.class); + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setThreadNamePrefix("testExecutor"); + executor.afterPropertiesSet(); + processorDefinition.getPropertyValues().add("executor", executor); + ConfigurableApplicationContext context = initContext(processorDefinition); + + ITestBean testBean = context.getBean("target", ITestBean.class); + testBean.test(); + testBean.await(3000); + Thread asyncThread = testBean.getThread(); + assertThat(asyncThread.getName().startsWith("testExecutor")).isTrue(); + context.close(); + } + + @Test + public void taskExecutorByBeanType() { + StaticApplicationContext context = new StaticApplicationContext(); + + BeanDefinition processorDefinition = new RootBeanDefinition(AsyncAnnotationBeanPostProcessor.class); + context.registerBeanDefinition("postProcessor", processorDefinition); + + BeanDefinition executorDefinition = new RootBeanDefinition(ThreadPoolTaskExecutor.class); + executorDefinition.getPropertyValues().add("threadNamePrefix", "testExecutor"); + context.registerBeanDefinition("myExecutor", executorDefinition); + + BeanDefinition targetDefinition = + new RootBeanDefinition(AsyncAnnotationBeanPostProcessorTests.TestBean.class); + context.registerBeanDefinition("target", targetDefinition); + + context.refresh(); + + ITestBean testBean = context.getBean("target", ITestBean.class); + testBean.test(); + testBean.await(3000); + Thread asyncThread = testBean.getThread(); + assertThat(asyncThread.getName().startsWith("testExecutor")).isTrue(); + context.close(); + } + + @Test + public void taskExecutorByBeanName() { + StaticApplicationContext context = new StaticApplicationContext(); + + BeanDefinition processorDefinition = new RootBeanDefinition(AsyncAnnotationBeanPostProcessor.class); + context.registerBeanDefinition("postProcessor", processorDefinition); + + BeanDefinition executorDefinition = new RootBeanDefinition(ThreadPoolTaskExecutor.class); + executorDefinition.getPropertyValues().add("threadNamePrefix", "testExecutor"); + context.registerBeanDefinition("myExecutor", executorDefinition); + + BeanDefinition executorDefinition2 = new RootBeanDefinition(ThreadPoolTaskExecutor.class); + executorDefinition2.getPropertyValues().add("threadNamePrefix", "testExecutor2"); + context.registerBeanDefinition("taskExecutor", executorDefinition2); + + BeanDefinition targetDefinition = + new RootBeanDefinition(AsyncAnnotationBeanPostProcessorTests.TestBean.class); + context.registerBeanDefinition("target", targetDefinition); + + context.refresh(); + + ITestBean testBean = context.getBean("target", ITestBean.class); + testBean.test(); + testBean.await(3000); + Thread asyncThread = testBean.getThread(); + assertThat(asyncThread.getName().startsWith("testExecutor2")).isTrue(); + context.close(); + } + + @Test + public void configuredThroughNamespace() { + GenericXmlApplicationContext context = new GenericXmlApplicationContext(); + context.load(new ClassPathResource("taskNamespaceTests.xml", getClass())); + context.refresh(); + ITestBean testBean = context.getBean("target", ITestBean.class); + testBean.test(); + testBean.await(3000); + Thread asyncThread = testBean.getThread(); + assertThat(asyncThread.getName().startsWith("testExecutor")).isTrue(); + + TestableAsyncUncaughtExceptionHandler exceptionHandler = + context.getBean("exceptionHandler", TestableAsyncUncaughtExceptionHandler.class); + assertThat(exceptionHandler.isCalled()).as("handler should not have been called yet").isFalse(); + + testBean.failWithVoid(); + exceptionHandler.await(3000); + Method m = ReflectionUtils.findMethod(TestBean.class, "failWithVoid"); + exceptionHandler.assertCalledWith(m, UnsupportedOperationException.class); + context.close(); + } + + @Test + @SuppressWarnings("resource") + public void handleExceptionWithFuture() { + ConfigurableApplicationContext context = + new AnnotationConfigApplicationContext(ConfigWithExceptionHandler.class); + ITestBean testBean = context.getBean("target", ITestBean.class); + + TestableAsyncUncaughtExceptionHandler exceptionHandler = + context.getBean("exceptionHandler", TestableAsyncUncaughtExceptionHandler.class); + assertThat(exceptionHandler.isCalled()).as("handler should not have been called yet").isFalse(); + Future result = testBean.failWithFuture(); + assertFutureWithException(result, exceptionHandler); + } + + @Test + @SuppressWarnings("resource") + public void handleExceptionWithListenableFuture() { + ConfigurableApplicationContext context = + new AnnotationConfigApplicationContext(ConfigWithExceptionHandler.class); + ITestBean testBean = context.getBean("target", ITestBean.class); + + TestableAsyncUncaughtExceptionHandler exceptionHandler = + context.getBean("exceptionHandler", TestableAsyncUncaughtExceptionHandler.class); + assertThat(exceptionHandler.isCalled()).as("handler should not have been called yet").isFalse(); + Future result = testBean.failWithListenableFuture(); + assertFutureWithException(result, exceptionHandler); + } + + private void assertFutureWithException(Future result, + TestableAsyncUncaughtExceptionHandler exceptionHandler) { + assertThatExceptionOfType(ExecutionException.class).isThrownBy( + result::get) + .withCauseExactlyInstanceOf(UnsupportedOperationException.class); + assertThat(exceptionHandler.isCalled()).as("handler should never be called with Future return type").isFalse(); + } + + @Test + public void handleExceptionWithCustomExceptionHandler() { + Method m = ReflectionUtils.findMethod(TestBean.class, "failWithVoid"); + TestableAsyncUncaughtExceptionHandler exceptionHandler = + new TestableAsyncUncaughtExceptionHandler(); + BeanDefinition processorDefinition = new RootBeanDefinition(AsyncAnnotationBeanPostProcessor.class); + processorDefinition.getPropertyValues().add("exceptionHandler", exceptionHandler); + + ConfigurableApplicationContext context = initContext(processorDefinition); + ITestBean testBean = context.getBean("target", ITestBean.class); + + assertThat(exceptionHandler.isCalled()).as("Handler should not have been called").isFalse(); + testBean.failWithVoid(); + exceptionHandler.await(3000); + exceptionHandler.assertCalledWith(m, UnsupportedOperationException.class); + } + + @Test + public void exceptionHandlerThrowsUnexpectedException() { + Method m = ReflectionUtils.findMethod(TestBean.class, "failWithVoid"); + TestableAsyncUncaughtExceptionHandler exceptionHandler = + new TestableAsyncUncaughtExceptionHandler(true); + BeanDefinition processorDefinition = new RootBeanDefinition(AsyncAnnotationBeanPostProcessor.class); + processorDefinition.getPropertyValues().add("exceptionHandler", exceptionHandler); + processorDefinition.getPropertyValues().add("executor", new DirectExecutor()); + + ConfigurableApplicationContext context = initContext(processorDefinition); + ITestBean testBean = context.getBean("target", ITestBean.class); + + assertThat(exceptionHandler.isCalled()).as("Handler should not have been called").isFalse(); + testBean.failWithVoid(); + exceptionHandler.assertCalledWith(m, UnsupportedOperationException.class); + } + + private ConfigurableApplicationContext initContext(BeanDefinition asyncAnnotationBeanPostProcessorDefinition) { + StaticApplicationContext context = new StaticApplicationContext(); + BeanDefinition targetDefinition = new RootBeanDefinition(TestBean.class); + context.registerBeanDefinition("postProcessor", asyncAnnotationBeanPostProcessorDefinition); + context.registerBeanDefinition("target", targetDefinition); + context.refresh(); + return context; + } + + + private interface ITestBean { + + Thread getThread(); + + @Async + void test(); + + Future failWithFuture(); + + ListenableFuture failWithListenableFuture(); + + void failWithVoid(); + + void await(long timeout); + } + + + public static class TestBean implements ITestBean { + + private Thread thread; + + private final CountDownLatch latch = new CountDownLatch(1); + + @Override + public Thread getThread() { + return this.thread; + } + + @Override + @Async + public void test() { + this.thread = Thread.currentThread(); + this.latch.countDown(); + } + + @Async + @Override + public Future failWithFuture() { + throw new UnsupportedOperationException("failWithFuture"); + } + + @Async + @Override + public ListenableFuture failWithListenableFuture() { + throw new UnsupportedOperationException("failWithListenableFuture"); + } + + @Async + @Override + public void failWithVoid() { + throw new UnsupportedOperationException("failWithVoid"); + } + + @Override + public void await(long timeout) { + try { + this.latch.await(timeout, TimeUnit.MILLISECONDS); + } + catch (Exception e) { + Thread.currentThread().interrupt(); + } + } + } + + + private static class DirectExecutor implements Executor { + + @Override + public void execute(Runnable r) { + r.run(); + } + } + + + @Configuration + @EnableAsync + static class ConfigWithExceptionHandler extends AsyncConfigurerSupport { + + @Bean + public ITestBean target() { + return new TestBean(); + } + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return exceptionHandler(); + } + + @Bean + public TestableAsyncUncaughtExceptionHandler exceptionHandler() { + return new TestableAsyncUncaughtExceptionHandler(); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncExecutionTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncExecutionTests.java new file mode 100644 index 0000000..9fb8494 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncExecutionTests.java @@ -0,0 +1,749 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.annotation; + +import java.io.IOException; +import java.io.Serializable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.HashMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; +import org.springframework.aop.support.DefaultIntroductionAdvisor; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.util.concurrent.ListenableFuture; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Juergen Hoeller + * @author Chris Beams + */ +@SuppressWarnings("resource") +public class AsyncExecutionTests { + + private static String originalThreadName; + + private static int listenerCalled = 0; + + private static int listenerConstructed = 0; + + + @Test + public void asyncMethods() throws Exception { + originalThreadName = Thread.currentThread().getName(); + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBeanDefinition("asyncTest", new RootBeanDefinition(AsyncMethodBean.class)); + context.registerBeanDefinition("autoProxyCreator", new RootBeanDefinition(DefaultAdvisorAutoProxyCreator.class)); + context.registerBeanDefinition("asyncAdvisor", new RootBeanDefinition(AsyncAnnotationAdvisor.class)); + context.refresh(); + + AsyncMethodBean asyncTest = context.getBean("asyncTest", AsyncMethodBean.class); + asyncTest.doNothing(5); + asyncTest.doSomething(10); + Future future = asyncTest.returnSomething(20); + assertThat(future.get()).isEqualTo("20"); + ListenableFuture listenableFuture = asyncTest.returnSomethingListenable(20); + assertThat(listenableFuture.get()).isEqualTo("20"); + CompletableFuture completableFuture = asyncTest.returnSomethingCompletable(20); + assertThat(completableFuture.get()).isEqualTo("20"); + + assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> + asyncTest.returnSomething(0).get()) + .withCauseInstanceOf(IllegalArgumentException.class); + + assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> + asyncTest.returnSomething(-1).get()) + .withCauseInstanceOf(IOException.class); + + assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> + asyncTest.returnSomethingListenable(0).get()) + .withCauseInstanceOf(IllegalArgumentException.class); + + assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> + asyncTest.returnSomethingListenable(-1).get()) + .withCauseInstanceOf(IOException.class); + + assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> + asyncTest.returnSomethingCompletable(0).get()) + .withCauseInstanceOf(IllegalArgumentException.class); + } + + @Test + public void asyncMethodsThroughInterface() throws Exception { + originalThreadName = Thread.currentThread().getName(); + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBeanDefinition("asyncTest", new RootBeanDefinition(SimpleAsyncMethodBean.class)); + context.registerBeanDefinition("autoProxyCreator", new RootBeanDefinition(DefaultAdvisorAutoProxyCreator.class)); + context.registerBeanDefinition("asyncAdvisor", new RootBeanDefinition(AsyncAnnotationAdvisor.class)); + context.refresh(); + + SimpleInterface asyncTest = context.getBean("asyncTest", SimpleInterface.class); + asyncTest.doNothing(5); + asyncTest.doSomething(10); + Future future = asyncTest.returnSomething(20); + assertThat(future.get()).isEqualTo("20"); + } + + @Test + public void asyncMethodsWithQualifier() throws Exception { + originalThreadName = Thread.currentThread().getName(); + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBeanDefinition("asyncTest", new RootBeanDefinition(AsyncMethodWithQualifierBean.class)); + context.registerBeanDefinition("autoProxyCreator", new RootBeanDefinition(DefaultAdvisorAutoProxyCreator.class)); + context.registerBeanDefinition("asyncAdvisor", new RootBeanDefinition(AsyncAnnotationAdvisor.class)); + context.registerBeanDefinition("e0", new RootBeanDefinition(ThreadPoolTaskExecutor.class)); + context.registerBeanDefinition("e1", new RootBeanDefinition(ThreadPoolTaskExecutor.class)); + context.registerBeanDefinition("e2", new RootBeanDefinition(ThreadPoolTaskExecutor.class)); + context.refresh(); + + AsyncMethodWithQualifierBean asyncTest = context.getBean("asyncTest", AsyncMethodWithQualifierBean.class); + asyncTest.doNothing(5); + asyncTest.doSomething(10); + Future future = asyncTest.returnSomething(20); + assertThat(future.get()).isEqualTo("20"); + Future future2 = asyncTest.returnSomething2(30); + assertThat(future2.get()).isEqualTo("30"); + } + + @Test + public void asyncMethodsWithQualifierThroughInterface() throws Exception { + originalThreadName = Thread.currentThread().getName(); + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBeanDefinition("asyncTest", new RootBeanDefinition(SimpleAsyncMethodWithQualifierBean.class)); + context.registerBeanDefinition("autoProxyCreator", new RootBeanDefinition(DefaultAdvisorAutoProxyCreator.class)); + context.registerBeanDefinition("asyncAdvisor", new RootBeanDefinition(AsyncAnnotationAdvisor.class)); + context.registerBeanDefinition("e0", new RootBeanDefinition(ThreadPoolTaskExecutor.class)); + context.registerBeanDefinition("e1", new RootBeanDefinition(ThreadPoolTaskExecutor.class)); + context.registerBeanDefinition("e2", new RootBeanDefinition(ThreadPoolTaskExecutor.class)); + context.refresh(); + + SimpleInterface asyncTest = context.getBean("asyncTest", SimpleInterface.class); + asyncTest.doNothing(5); + asyncTest.doSomething(10); + Future future = asyncTest.returnSomething(20); + assertThat(future.get()).isEqualTo("20"); + Future future2 = asyncTest.returnSomething2(30); + assertThat(future2.get()).isEqualTo("30"); + } + + @Test + public void asyncClass() throws Exception { + originalThreadName = Thread.currentThread().getName(); + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBeanDefinition("asyncTest", new RootBeanDefinition(AsyncClassBean.class)); + context.registerBeanDefinition("autoProxyCreator", new RootBeanDefinition(DefaultAdvisorAutoProxyCreator.class)); + context.registerBeanDefinition("asyncAdvisor", new RootBeanDefinition(AsyncAnnotationAdvisor.class)); + context.refresh(); + + AsyncClassBean asyncTest = context.getBean("asyncTest", AsyncClassBean.class); + asyncTest.doSomething(10); + Future future = asyncTest.returnSomething(20); + assertThat(future.get()).isEqualTo("20"); + ListenableFuture listenableFuture = asyncTest.returnSomethingListenable(20); + assertThat(listenableFuture.get()).isEqualTo("20"); + CompletableFuture completableFuture = asyncTest.returnSomethingCompletable(20); + assertThat(completableFuture.get()).isEqualTo("20"); + + assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> + asyncTest.returnSomething(0).get()) + .withCauseInstanceOf(IllegalArgumentException.class); + + assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> + asyncTest.returnSomethingListenable(0).get()) + .withCauseInstanceOf(IllegalArgumentException.class); + + assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> + asyncTest.returnSomethingCompletable(0).get()) + .withCauseInstanceOf(IllegalArgumentException.class); + } + + @Test + public void asyncClassWithPostProcessor() throws Exception { + originalThreadName = Thread.currentThread().getName(); + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBeanDefinition("asyncTest", new RootBeanDefinition(AsyncClassBean.class)); + context.registerBeanDefinition("asyncProcessor", new RootBeanDefinition(AsyncAnnotationBeanPostProcessor.class)); + context.refresh(); + + AsyncClassBean asyncTest = context.getBean("asyncTest", AsyncClassBean.class); + asyncTest.doSomething(10); + Future future = asyncTest.returnSomething(20); + assertThat(future.get()).isEqualTo("20"); + } + + @Test + public void asyncClassWithInterface() throws Exception { + originalThreadName = Thread.currentThread().getName(); + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBeanDefinition("asyncTest", new RootBeanDefinition(AsyncClassBeanWithInterface.class)); + context.registerBeanDefinition("autoProxyCreator", new RootBeanDefinition(DefaultAdvisorAutoProxyCreator.class)); + context.registerBeanDefinition("asyncAdvisor", new RootBeanDefinition(AsyncAnnotationAdvisor.class)); + context.refresh(); + + RegularInterface asyncTest = context.getBean("asyncTest", RegularInterface.class); + asyncTest.doSomething(10); + Future future = asyncTest.returnSomething(20); + assertThat(future.get()).isEqualTo("20"); + } + + @Test + public void asyncClassWithInterfaceAndPostProcessor() throws Exception { + originalThreadName = Thread.currentThread().getName(); + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBeanDefinition("asyncTest", new RootBeanDefinition(AsyncClassBeanWithInterface.class)); + context.registerBeanDefinition("asyncProcessor", new RootBeanDefinition(AsyncAnnotationBeanPostProcessor.class)); + context.refresh(); + + RegularInterface asyncTest = context.getBean("asyncTest", RegularInterface.class); + asyncTest.doSomething(10); + Future future = asyncTest.returnSomething(20); + assertThat(future.get()).isEqualTo("20"); + } + + @Test + public void asyncInterface() throws Exception { + originalThreadName = Thread.currentThread().getName(); + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBeanDefinition("asyncTest", new RootBeanDefinition(AsyncInterfaceBean.class)); + context.registerBeanDefinition("autoProxyCreator", new RootBeanDefinition(DefaultAdvisorAutoProxyCreator.class)); + context.registerBeanDefinition("asyncAdvisor", new RootBeanDefinition(AsyncAnnotationAdvisor.class)); + context.refresh(); + + AsyncInterface asyncTest = context.getBean("asyncTest", AsyncInterface.class); + asyncTest.doSomething(10); + Future future = asyncTest.returnSomething(20); + assertThat(future.get()).isEqualTo("20"); + } + + @Test + public void asyncInterfaceWithPostProcessor() throws Exception { + originalThreadName = Thread.currentThread().getName(); + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBeanDefinition("asyncTest", new RootBeanDefinition(AsyncInterfaceBean.class)); + context.registerBeanDefinition("asyncProcessor", new RootBeanDefinition(AsyncAnnotationBeanPostProcessor.class)); + context.refresh(); + + AsyncInterface asyncTest = context.getBean("asyncTest", AsyncInterface.class); + asyncTest.doSomething(10); + Future future = asyncTest.returnSomething(20); + assertThat(future.get()).isEqualTo("20"); + } + + @Test + public void dynamicAsyncInterface() throws Exception { + originalThreadName = Thread.currentThread().getName(); + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBeanDefinition("asyncTest", new RootBeanDefinition(DynamicAsyncInterfaceBean.class)); + context.registerBeanDefinition("autoProxyCreator", new RootBeanDefinition(DefaultAdvisorAutoProxyCreator.class)); + context.registerBeanDefinition("asyncAdvisor", new RootBeanDefinition(AsyncAnnotationAdvisor.class)); + context.refresh(); + + AsyncInterface asyncTest = context.getBean("asyncTest", AsyncInterface.class); + asyncTest.doSomething(10); + Future future = asyncTest.returnSomething(20); + assertThat(future.get()).isEqualTo("20"); + } + + @Test + public void dynamicAsyncInterfaceWithPostProcessor() throws Exception { + originalThreadName = Thread.currentThread().getName(); + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBeanDefinition("asyncTest", new RootBeanDefinition(DynamicAsyncInterfaceBean.class)); + context.registerBeanDefinition("asyncProcessor", new RootBeanDefinition(AsyncAnnotationBeanPostProcessor.class)); + context.refresh(); + + AsyncInterface asyncTest = context.getBean("asyncTest", AsyncInterface.class); + asyncTest.doSomething(10); + Future future = asyncTest.returnSomething(20); + assertThat(future.get()).isEqualTo("20"); + } + + @Test + public void asyncMethodsInInterface() throws Exception { + originalThreadName = Thread.currentThread().getName(); + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBeanDefinition("asyncTest", new RootBeanDefinition(AsyncMethodsInterfaceBean.class)); + context.registerBeanDefinition("autoProxyCreator", new RootBeanDefinition(DefaultAdvisorAutoProxyCreator.class)); + context.registerBeanDefinition("asyncAdvisor", new RootBeanDefinition(AsyncAnnotationAdvisor.class)); + context.refresh(); + + AsyncMethodsInterface asyncTest = context.getBean("asyncTest", AsyncMethodsInterface.class); + asyncTest.doNothing(5); + asyncTest.doSomething(10); + Future future = asyncTest.returnSomething(20); + assertThat(future.get()).isEqualTo("20"); + } + + @Test + public void asyncMethodsInInterfaceWithPostProcessor() throws Exception { + originalThreadName = Thread.currentThread().getName(); + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBeanDefinition("asyncTest", new RootBeanDefinition(AsyncMethodsInterfaceBean.class)); + context.registerBeanDefinition("asyncProcessor", new RootBeanDefinition(AsyncAnnotationBeanPostProcessor.class)); + context.refresh(); + + AsyncMethodsInterface asyncTest = context.getBean("asyncTest", AsyncMethodsInterface.class); + asyncTest.doNothing(5); + asyncTest.doSomething(10); + Future future = asyncTest.returnSomething(20); + assertThat(future.get()).isEqualTo("20"); + } + + @Test + public void dynamicAsyncMethodsInInterfaceWithPostProcessor() throws Exception { + originalThreadName = Thread.currentThread().getName(); + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBeanDefinition("asyncTest", new RootBeanDefinition(DynamicAsyncMethodsInterfaceBean.class)); + context.registerBeanDefinition("asyncProcessor", new RootBeanDefinition(AsyncAnnotationBeanPostProcessor.class)); + context.refresh(); + + AsyncMethodsInterface asyncTest = context.getBean("asyncTest", AsyncMethodsInterface.class); + asyncTest.doSomething(10); + Future future = asyncTest.returnSomething(20); + assertThat(future.get()).isEqualTo("20"); + } + + @Test + public void asyncMethodListener() throws Exception { + // Arrange + originalThreadName = Thread.currentThread().getName(); + listenerCalled = 0; + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBeanDefinition("asyncTest", new RootBeanDefinition(AsyncMethodListener.class)); + context.registerBeanDefinition("autoProxyCreator", new RootBeanDefinition(DefaultAdvisorAutoProxyCreator.class)); + context.registerBeanDefinition("asyncAdvisor", new RootBeanDefinition(AsyncAnnotationAdvisor.class)); + // Act + context.refresh(); + // Assert + Awaitility.await() + .atMost(1, TimeUnit.SECONDS) + .pollInterval(10, TimeUnit.MILLISECONDS) + .until(() -> listenerCalled == 1); + context.close(); + } + + @Test + public void asyncClassListener() throws Exception { + // Arrange + originalThreadName = Thread.currentThread().getName(); + listenerCalled = 0; + listenerConstructed = 0; + GenericApplicationContext context = new GenericApplicationContext(); + context.registerBeanDefinition("asyncTest", new RootBeanDefinition(AsyncClassListener.class)); + context.registerBeanDefinition("autoProxyCreator", new RootBeanDefinition(DefaultAdvisorAutoProxyCreator.class)); + context.registerBeanDefinition("asyncAdvisor", new RootBeanDefinition(AsyncAnnotationAdvisor.class)); + // Act + context.refresh(); + context.close(); + // Assert + Awaitility.await() + .atMost(1, TimeUnit.SECONDS) + .pollInterval(10, TimeUnit.MILLISECONDS) + .until(() -> listenerCalled == 2); + assertThat(listenerConstructed).isEqualTo(1); + } + + @Test + public void asyncPrototypeClassListener() throws Exception { + // Arrange + originalThreadName = Thread.currentThread().getName(); + listenerCalled = 0; + listenerConstructed = 0; + GenericApplicationContext context = new GenericApplicationContext(); + RootBeanDefinition listenerDef = new RootBeanDefinition(AsyncClassListener.class); + listenerDef.setScope(BeanDefinition.SCOPE_PROTOTYPE); + context.registerBeanDefinition("asyncTest", listenerDef); + context.registerBeanDefinition("autoProxyCreator", new RootBeanDefinition(DefaultAdvisorAutoProxyCreator.class)); + context.registerBeanDefinition("asyncAdvisor", new RootBeanDefinition(AsyncAnnotationAdvisor.class)); + // Act + context.refresh(); + context.close(); + // Assert + Awaitility.await() + .atMost(1, TimeUnit.SECONDS) + .pollInterval(10, TimeUnit.MILLISECONDS) + .until(() -> listenerCalled == 2); + assertThat(listenerConstructed).isEqualTo(2); + } + + + public interface SimpleInterface { + + void doNothing(int i); + + void doSomething(int i); + + Future returnSomething(int i); + + Future returnSomething2(int i); + } + + + public static class AsyncMethodBean { + + public void doNothing(int i) { + assertThat(Thread.currentThread().getName().equals(originalThreadName)).isTrue(); + } + + @Async + public void doSomething(int i) { + boolean condition = !Thread.currentThread().getName().equals(originalThreadName); + assertThat(condition).isTrue(); + } + + @Async + public Future returnSomething(int i) { + boolean condition = !Thread.currentThread().getName().equals(originalThreadName); + assertThat(condition).isTrue(); + if (i == 0) { + throw new IllegalArgumentException(); + } + else if (i < 0) { + return AsyncResult.forExecutionException(new IOException()); + } + return AsyncResult.forValue(Integer.toString(i)); + } + + @Async + public ListenableFuture returnSomethingListenable(int i) { + boolean condition = !Thread.currentThread().getName().equals(originalThreadName); + assertThat(condition).isTrue(); + if (i == 0) { + throw new IllegalArgumentException(); + } + else if (i < 0) { + return AsyncResult.forExecutionException(new IOException()); + } + return new AsyncResult<>(Integer.toString(i)); + } + + @Async + public CompletableFuture returnSomethingCompletable(int i) { + boolean condition = !Thread.currentThread().getName().equals(originalThreadName); + assertThat(condition).isTrue(); + if (i == 0) { + throw new IllegalArgumentException(); + } + return CompletableFuture.completedFuture(Integer.toString(i)); + } + } + + + public static class SimpleAsyncMethodBean extends AsyncMethodBean implements SimpleInterface { + + @Override + public Future returnSomething2(int i) { + throw new UnsupportedOperationException(); + } + } + + + @Async("e0") + public static class AsyncMethodWithQualifierBean { + + public void doNothing(int i) { + assertThat(Thread.currentThread().getName().equals(originalThreadName)).isTrue(); + } + + @Async("e1") + public void doSomething(int i) { + boolean condition = !Thread.currentThread().getName().equals(originalThreadName); + assertThat(condition).isTrue(); + assertThat(Thread.currentThread().getName().startsWith("e1-")).isTrue(); + } + + @MyAsync + public Future returnSomething(int i) { + boolean condition = !Thread.currentThread().getName().equals(originalThreadName); + assertThat(condition).isTrue(); + assertThat(Thread.currentThread().getName().startsWith("e2-")).isTrue(); + return new AsyncResult<>(Integer.toString(i)); + } + + public Future returnSomething2(int i) { + boolean condition = !Thread.currentThread().getName().equals(originalThreadName); + assertThat(condition).isTrue(); + assertThat(Thread.currentThread().getName().startsWith("e0-")).isTrue(); + return new AsyncResult<>(Integer.toString(i)); + } + } + + + public static class SimpleAsyncMethodWithQualifierBean extends AsyncMethodWithQualifierBean implements SimpleInterface { + } + + + @Async("e2") + @Retention(RetentionPolicy.RUNTIME) + public @interface MyAsync { + } + + + @Async + @SuppressWarnings("serial") + public static class AsyncClassBean implements Serializable, DisposableBean { + + public void doSomething(int i) { + boolean condition = !Thread.currentThread().getName().equals(originalThreadName); + assertThat(condition).isTrue(); + } + + public Future returnSomething(int i) { + boolean condition = !Thread.currentThread().getName().equals(originalThreadName); + assertThat(condition).isTrue(); + if (i == 0) { + throw new IllegalArgumentException(); + } + return new AsyncResult<>(Integer.toString(i)); + } + + public ListenableFuture returnSomethingListenable(int i) { + boolean condition = !Thread.currentThread().getName().equals(originalThreadName); + assertThat(condition).isTrue(); + if (i == 0) { + throw new IllegalArgumentException(); + } + return new AsyncResult<>(Integer.toString(i)); + } + + @Async + public CompletableFuture returnSomethingCompletable(int i) { + boolean condition = !Thread.currentThread().getName().equals(originalThreadName); + assertThat(condition).isTrue(); + if (i == 0) { + throw new IllegalArgumentException(); + } + return CompletableFuture.completedFuture(Integer.toString(i)); + } + + @Override + public void destroy() { + } + } + + + public interface RegularInterface { + + void doSomething(int i); + + Future returnSomething(int i); + } + + + @Async + public static class AsyncClassBeanWithInterface implements RegularInterface { + + @Override + public void doSomething(int i) { + boolean condition = !Thread.currentThread().getName().equals(originalThreadName); + assertThat(condition).isTrue(); + } + + @Override + public Future returnSomething(int i) { + boolean condition = !Thread.currentThread().getName().equals(originalThreadName); + assertThat(condition).isTrue(); + return new AsyncResult<>(Integer.toString(i)); + } + } + + + @Async + public interface AsyncInterface { + + void doSomething(int i); + + Future returnSomething(int i); + } + + + public static class AsyncInterfaceBean implements AsyncInterface { + + @Override + public void doSomething(int i) { + boolean condition = !Thread.currentThread().getName().equals(originalThreadName); + assertThat(condition).isTrue(); + } + + @Override + public Future returnSomething(int i) { + boolean condition = !Thread.currentThread().getName().equals(originalThreadName); + assertThat(condition).isTrue(); + return new AsyncResult<>(Integer.toString(i)); + } + } + + + public static class DynamicAsyncInterfaceBean implements FactoryBean { + + private final AsyncInterface proxy; + + public DynamicAsyncInterfaceBean() { + ProxyFactory pf = new ProxyFactory(new HashMap<>()); + DefaultIntroductionAdvisor advisor = new DefaultIntroductionAdvisor(new MethodInterceptor() { + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + boolean condition = !Thread.currentThread().getName().equals(originalThreadName); + assertThat(condition).isTrue(); + if (Future.class.equals(invocation.getMethod().getReturnType())) { + return new AsyncResult<>(invocation.getArguments()[0].toString()); + } + return null; + } + }); + advisor.addInterface(AsyncInterface.class); + pf.addAdvisor(advisor); + this.proxy = (AsyncInterface) pf.getProxy(); + } + + @Override + public AsyncInterface getObject() { + return this.proxy; + } + + @Override + public Class getObjectType() { + return this.proxy.getClass(); + } + + @Override + public boolean isSingleton() { + return true; + } + } + + + public interface AsyncMethodsInterface { + + void doNothing(int i); + + @Async + void doSomething(int i); + + @Async + Future returnSomething(int i); + } + + + public static class AsyncMethodsInterfaceBean implements AsyncMethodsInterface { + + @Override + public void doNothing(int i) { + assertThat(Thread.currentThread().getName().equals(originalThreadName)).isTrue(); + } + + @Override + public void doSomething(int i) { + boolean condition = !Thread.currentThread().getName().equals(originalThreadName); + assertThat(condition).isTrue(); + } + + @Override + public Future returnSomething(int i) { + boolean condition = !Thread.currentThread().getName().equals(originalThreadName); + assertThat(condition).isTrue(); + return new AsyncResult<>(Integer.toString(i)); + } + } + + + public static class DynamicAsyncMethodsInterfaceBean implements FactoryBean { + + private final AsyncMethodsInterface proxy; + + public DynamicAsyncMethodsInterfaceBean() { + ProxyFactory pf = new ProxyFactory(new HashMap<>()); + DefaultIntroductionAdvisor advisor = new DefaultIntroductionAdvisor(new MethodInterceptor() { + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + boolean condition = !Thread.currentThread().getName().equals(originalThreadName); + assertThat(condition).isTrue(); + if (Future.class.equals(invocation.getMethod().getReturnType())) { + return new AsyncResult<>(invocation.getArguments()[0].toString()); + } + return null; + } + }); + advisor.addInterface(AsyncMethodsInterface.class); + pf.addAdvisor(advisor); + this.proxy = (AsyncMethodsInterface) pf.getProxy(); + } + + @Override + public AsyncMethodsInterface getObject() { + return this.proxy; + } + + @Override + public Class getObjectType() { + return this.proxy.getClass(); + } + + @Override + public boolean isSingleton() { + return true; + } + } + + + public static class AsyncMethodListener implements ApplicationListener { + + @Override + @Async + public void onApplicationEvent(ApplicationEvent event) { + listenerCalled++; + boolean condition = !Thread.currentThread().getName().equals(originalThreadName); + assertThat(condition).isTrue(); + } + } + + + @Async + public static class AsyncClassListener implements ApplicationListener { + + public AsyncClassListener() { + listenerConstructed++; + } + + @Override + public void onApplicationEvent(ApplicationEvent event) { + listenerCalled++; + boolean condition = !Thread.currentThread().getName().equals(originalThreadName); + assertThat(condition).isTrue(); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncResultTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncResultTests.java new file mode 100644 index 0000000..6b4f67a --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncResultTests.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.annotation; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ExecutionException; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.util.concurrent.ListenableFutureCallback; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Juergen Hoeller + */ +public class AsyncResultTests { + + @Test + public void asyncResultWithCallbackAndValue() throws Exception { + String value = "val"; + final Set values = new HashSet<>(1); + ListenableFuture future = AsyncResult.forValue(value); + future.addCallback(new ListenableFutureCallback() { + @Override + public void onSuccess(String result) { + values.add(result); + } + @Override + public void onFailure(Throwable ex) { + throw new AssertionError("Failure callback not expected: " + ex, ex); + } + }); + assertThat(values.iterator().next()).isSameAs(value); + assertThat(future.get()).isSameAs(value); + assertThat(future.completable().get()).isSameAs(value); + future.completable().thenAccept(v -> assertThat(v).isSameAs(value)); + } + + @Test + public void asyncResultWithCallbackAndException() throws Exception { + IOException ex = new IOException(); + final Set values = new HashSet<>(1); + ListenableFuture future = AsyncResult.forExecutionException(ex); + future.addCallback(new ListenableFutureCallback() { + @Override + public void onSuccess(String result) { + throw new AssertionError("Success callback not expected: " + result); + } + @Override + public void onFailure(Throwable ex) { + values.add(ex); + } + }); + assertThat(values.iterator().next()).isSameAs(ex); + assertThatExceptionOfType(ExecutionException.class).isThrownBy( + future::get) + .withCause(ex); + assertThatExceptionOfType(ExecutionException.class).isThrownBy( + future.completable()::get) + .withCause(ex); + } + + @Test + public void asyncResultWithSeparateCallbacksAndValue() throws Exception { + String value = "val"; + final Set values = new HashSet<>(1); + ListenableFuture future = AsyncResult.forValue(value); + future.addCallback(values::add, ex -> new AssertionError("Failure callback not expected: " + ex)); + assertThat(values.iterator().next()).isSameAs(value); + assertThat(future.get()).isSameAs(value); + assertThat(future.completable().get()).isSameAs(value); + future.completable().thenAccept(v -> assertThat(v).isSameAs(value)); + } + + @Test + public void asyncResultWithSeparateCallbacksAndException() throws Exception { + IOException ex = new IOException(); + final Set values = new HashSet<>(1); + ListenableFuture future = AsyncResult.forExecutionException(ex); + future.addCallback(result -> new AssertionError("Success callback not expected: " + result), values::add); + assertThat(values.iterator().next()).isSameAs(ex); + assertThatExceptionOfType(ExecutionException.class).isThrownBy( + future::get) + .withCause(ex); + assertThatExceptionOfType(ExecutionException.class).isThrownBy( + future.completable()::get) + .withCause(ex); + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableAsyncTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableAsyncTests.java new file mode 100644 index 0000000..e603339 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableAsyncTests.java @@ -0,0 +1,665 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Method; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.aop.Advisor; +import org.springframework.aop.framework.Advised; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.BeanNotOfRequiredTypeException; +import org.springframework.beans.factory.UnsatisfiedDependencyException; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.annotation.AdviceMode; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Lazy; +import org.springframework.core.Ordered; +import org.springframework.lang.Nullable; +import org.springframework.scheduling.concurrent.CustomizableThreadFactory; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Component; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests use of @EnableAsync on @Configuration classes. + * + * @author Chris Beams + * @author Stephane Nicoll + * @since 3.1 + */ +public class EnableAsyncTests { + + @Test + public void proxyingOccurs() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(AsyncConfig.class); + ctx.refresh(); + + AsyncBean asyncBean = ctx.getBean(AsyncBean.class); + assertThat(AopUtils.isAopProxy(asyncBean)).isTrue(); + asyncBean.work(); + ctx.close(); + } + + @Test + public void proxyingOccursWithMockitoStub() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(AsyncConfigWithMockito.class, AsyncBeanUser.class); + ctx.refresh(); + + AsyncBeanUser asyncBeanUser = ctx.getBean(AsyncBeanUser.class); + AsyncBean asyncBean = asyncBeanUser.getAsyncBean(); + assertThat(AopUtils.isAopProxy(asyncBean)).isTrue(); + asyncBean.work(); + ctx.close(); + } + + @Test + public void properExceptionForExistingProxyDependencyMismatch() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(AsyncConfig.class, AsyncBeanWithInterface.class, AsyncBeanUser.class); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy( + ctx::refresh) + .withCauseInstanceOf(BeanNotOfRequiredTypeException.class); + ctx.close(); + } + + @Test + public void properExceptionForResolvedProxyDependencyMismatch() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(AsyncConfig.class, AsyncBeanUser.class, AsyncBeanWithInterface.class); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy( + ctx::refresh) + .withCauseInstanceOf(BeanNotOfRequiredTypeException.class); + ctx.close(); + } + + @Test + public void withAsyncBeanWithExecutorQualifiedByName() throws ExecutionException, InterruptedException { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(AsyncWithExecutorQualifiedByNameConfig.class); + ctx.refresh(); + + AsyncBeanWithExecutorQualifiedByName asyncBean = ctx.getBean(AsyncBeanWithExecutorQualifiedByName.class); + Future workerThread0 = asyncBean.work0(); + assertThat(workerThread0.get().getName()).doesNotStartWith("e1-").doesNotStartWith("otherExecutor-"); + Future workerThread = asyncBean.work(); + assertThat(workerThread.get().getName()).startsWith("e1-"); + Future workerThread2 = asyncBean.work2(); + assertThat(workerThread2.get().getName()).startsWith("otherExecutor-"); + Future workerThread3 = asyncBean.work3(); + assertThat(workerThread3.get().getName()).startsWith("otherExecutor-"); + + ctx.close(); + } + + @Test + public void asyncProcessorIsOrderedLowestPrecedenceByDefault() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(AsyncConfig.class); + ctx.refresh(); + + AsyncAnnotationBeanPostProcessor bpp = ctx.getBean(AsyncAnnotationBeanPostProcessor.class); + assertThat(bpp.getOrder()).isEqualTo(Ordered.LOWEST_PRECEDENCE); + + ctx.close(); + } + + @Test + public void orderAttributeIsPropagated() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(OrderedAsyncConfig.class); + ctx.refresh(); + + AsyncAnnotationBeanPostProcessor bpp = ctx.getBean(AsyncAnnotationBeanPostProcessor.class); + assertThat(bpp.getOrder()).isEqualTo(Ordered.HIGHEST_PRECEDENCE); + + ctx.close(); + } + + @Test + public void customAsyncAnnotationIsPropagated() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(CustomAsyncAnnotationConfig.class, CustomAsyncBean.class); + ctx.refresh(); + + Object bean = ctx.getBean(CustomAsyncBean.class); + assertThat(AopUtils.isAopProxy(bean)).isTrue(); + boolean isAsyncAdvised = false; + for (Advisor advisor : ((Advised) bean).getAdvisors()) { + if (advisor instanceof AsyncAnnotationAdvisor) { + isAsyncAdvised = true; + break; + } + } + assertThat(isAsyncAdvised).as("bean was not async advised as expected").isTrue(); + + ctx.close(); + } + + /** + * Fails with classpath errors on trying to classload AnnotationAsyncExecutionAspect. + */ + @Test + public void aspectModeAspectJAttemptsToRegisterAsyncAspect() { + @SuppressWarnings("resource") + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(AspectJAsyncAnnotationConfig.class); + assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy( + ctx::refresh); + } + + @Test + public void customExecutorBean() { + // Arrange + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(CustomExecutorBean.class); + ctx.refresh(); + AsyncBean asyncBean = ctx.getBean(AsyncBean.class); + // Act + asyncBean.work(); + // Assert + Awaitility.await() + .atMost(500, TimeUnit.MILLISECONDS) + .pollInterval(10, TimeUnit.MILLISECONDS) + .until(() -> asyncBean.getThreadOfExecution() != null); + assertThat(asyncBean.getThreadOfExecution().getName()).startsWith("Custom-"); + ctx.close(); + } + + @Test + public void customExecutorConfig() { + // Arrange + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(CustomExecutorConfig.class); + ctx.refresh(); + AsyncBean asyncBean = ctx.getBean(AsyncBean.class); + // Act + asyncBean.work(); + // Assert + Awaitility.await() + .atMost(500, TimeUnit.MILLISECONDS) + .pollInterval(10, TimeUnit.MILLISECONDS) + .until(() -> asyncBean.getThreadOfExecution() != null); + assertThat(asyncBean.getThreadOfExecution().getName()).startsWith("Custom-"); + ctx.close(); + } + + @Test + public void customExecutorConfigWithThrowsException() { + // Arrange + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(CustomExecutorConfig.class); + ctx.refresh(); + AsyncBean asyncBean = ctx.getBean(AsyncBean.class); + Method method = ReflectionUtils.findMethod(AsyncBean.class, "fail"); + TestableAsyncUncaughtExceptionHandler exceptionHandler = + (TestableAsyncUncaughtExceptionHandler) ctx.getBean("exceptionHandler"); + assertThat(exceptionHandler.isCalled()).as("handler should not have been called yet").isFalse(); + // Act + asyncBean.fail(); + // Assert + Awaitility.await() + .atMost(500, TimeUnit.MILLISECONDS) + .pollInterval(10, TimeUnit.MILLISECONDS) + .untilAsserted(() -> exceptionHandler.assertCalledWith(method, UnsupportedOperationException.class)); + ctx.close(); + } + + @Test + public void customExecutorBeanConfig() { + // Arrange + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(CustomExecutorBeanConfig.class, ExecutorPostProcessor.class); + ctx.refresh(); + AsyncBean asyncBean = ctx.getBean(AsyncBean.class); + // Act + asyncBean.work(); + // Assert + Awaitility.await() + .atMost(500, TimeUnit.MILLISECONDS) + .pollInterval(10, TimeUnit.MILLISECONDS) + .until(() -> asyncBean.getThreadOfExecution() != null); + assertThat(asyncBean.getThreadOfExecution().getName()).startsWith("Post-"); + ctx.close(); + } + + @Test + public void customExecutorBeanConfigWithThrowsException() { + // Arrange + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(CustomExecutorBeanConfig.class, ExecutorPostProcessor.class); + ctx.refresh(); + AsyncBean asyncBean = ctx.getBean(AsyncBean.class); + TestableAsyncUncaughtExceptionHandler exceptionHandler = + (TestableAsyncUncaughtExceptionHandler) ctx.getBean("exceptionHandler"); + assertThat(exceptionHandler.isCalled()).as("handler should not have been called yet").isFalse(); + Method method = ReflectionUtils.findMethod(AsyncBean.class, "fail"); + // Act + asyncBean.fail(); + // Assert + Awaitility.await() + .atMost(500, TimeUnit.MILLISECONDS) + .pollInterval(10, TimeUnit.MILLISECONDS) + .untilAsserted(() -> exceptionHandler.assertCalledWith(method, UnsupportedOperationException.class)); + ctx.close(); + } + + @Test // SPR-14949 + public void findOnInterfaceWithInterfaceProxy() { + // Arrange + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Spr14949ConfigA.class); + AsyncInterface asyncBean = ctx.getBean(AsyncInterface.class); + // Act + asyncBean.work(); + // Assert + Awaitility.await() + .atMost(500, TimeUnit.MILLISECONDS) + .pollInterval(10, TimeUnit.MILLISECONDS) + .until(() -> asyncBean.getThreadOfExecution() != null); + assertThat(asyncBean.getThreadOfExecution().getName()).startsWith("Custom-"); + ctx.close(); + } + + @Test // SPR-14949 + public void findOnInterfaceWithCglibProxy() { + // Arrange + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Spr14949ConfigB.class); + AsyncInterface asyncBean = ctx.getBean(AsyncInterface.class); + // Act + asyncBean.work(); + // Assert + Awaitility.await() + .atMost(500, TimeUnit.MILLISECONDS) + .pollInterval(10, TimeUnit.MILLISECONDS) + .until(() -> asyncBean.getThreadOfExecution() != null); + assertThat(asyncBean.getThreadOfExecution().getName()).startsWith("Custom-"); + ctx.close(); + } + + @Test + @SuppressWarnings("resource") + public void exceptionThrownWithBeanNotOfRequiredTypeRootCause() { + assertThatExceptionOfType(Throwable.class).isThrownBy(() -> + new AnnotationConfigApplicationContext(JdkProxyConfiguration.class)) + .withCauseInstanceOf(BeanNotOfRequiredTypeException.class); + } + + + static class AsyncBeanWithExecutorQualifiedByName { + + @Async + public Future work0() { + return new AsyncResult<>(Thread.currentThread()); + } + + @Async("e1") + public Future work() { + return new AsyncResult<>(Thread.currentThread()); + } + + @Async("otherExecutor") + public Future work2() { + return new AsyncResult<>(Thread.currentThread()); + } + + @Async("e2") + public Future work3() { + return new AsyncResult<>(Thread.currentThread()); + } + } + + + static class AsyncBean { + + private Thread threadOfExecution; + + @Async + public void work() { + this.threadOfExecution = Thread.currentThread(); + } + + @Async + public void fail() { + throw new UnsupportedOperationException(); + } + + public Thread getThreadOfExecution() { + return threadOfExecution; + } + } + + + @Component("asyncBean") + static class AsyncBeanWithInterface extends AsyncBean implements Runnable { + + @Override + public void run() { + } + } + + + static class AsyncBeanUser { + + private final AsyncBean asyncBean; + + public AsyncBeanUser(AsyncBean asyncBean) { + this.asyncBean = asyncBean; + } + + public AsyncBean getAsyncBean() { + return asyncBean; + } + } + + + @EnableAsync(annotation = CustomAsync.class) + static class CustomAsyncAnnotationConfig { + } + + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @interface CustomAsync { + } + + + static class CustomAsyncBean { + + @CustomAsync + public void work() { + } + } + + + @Configuration + @EnableAsync(order = Ordered.HIGHEST_PRECEDENCE) + static class OrderedAsyncConfig { + + @Bean + public AsyncBean asyncBean() { + return new AsyncBean(); + } + } + + + @Configuration + @EnableAsync(mode = AdviceMode.ASPECTJ) + static class AspectJAsyncAnnotationConfig { + + @Bean + public AsyncBean asyncBean() { + return new AsyncBean(); + } + } + + + @Configuration + @EnableAsync + static class AsyncConfig { + + @Bean + public AsyncBean asyncBean() { + return new AsyncBean(); + } + } + + + @Configuration + @EnableAsync + static class AsyncConfigWithMockito { + + @Bean + @Lazy + public AsyncBean asyncBean() { + return Mockito.mock(AsyncBean.class); + } + } + + + @Configuration + @EnableAsync + static class AsyncWithExecutorQualifiedByNameConfig { + + @Bean + public AsyncBeanWithExecutorQualifiedByName asyncBean() { + return new AsyncBeanWithExecutorQualifiedByName(); + } + + @Bean + public Executor e1() { + return new ThreadPoolTaskExecutor(); + } + + @Bean + @Qualifier("e2") + public Executor otherExecutor() { + return new ThreadPoolTaskExecutor(); + } + } + + + @Configuration + @EnableAsync + static class CustomExecutorBean { + + @Bean + public AsyncBean asyncBean() { + return new AsyncBean(); + } + + @Bean + public Executor taskExecutor() { + return Executors.newSingleThreadExecutor(new CustomizableThreadFactory("Custom-")); + } + } + + + @Configuration + @EnableAsync + static class CustomExecutorConfig implements AsyncConfigurer { + + @Bean + public AsyncBean asyncBean() { + return new AsyncBean(); + } + + @Override + public Executor getAsyncExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setThreadNamePrefix("Custom-"); + executor.initialize(); + return executor; + } + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return exceptionHandler(); + } + + @Bean + public AsyncUncaughtExceptionHandler exceptionHandler() { + return new TestableAsyncUncaughtExceptionHandler(); + } + } + + + @Configuration + @EnableAsync + static class CustomExecutorBeanConfig implements AsyncConfigurer { + + @Bean + public AsyncBean asyncBean() { + return new AsyncBean(); + } + + @Override + public Executor getAsyncExecutor() { + return executor(); + } + + @Bean + public ThreadPoolTaskExecutor executor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setThreadNamePrefix("Custom-"); + executor.initialize(); + return executor; + } + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return exceptionHandler(); + } + + @Bean + public AsyncUncaughtExceptionHandler exceptionHandler() { + return new TestableAsyncUncaughtExceptionHandler(); + } + } + + + public static class ExecutorPostProcessor implements BeanPostProcessor { + + @Nullable + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof ThreadPoolTaskExecutor) { + ((ThreadPoolTaskExecutor) bean).setThreadNamePrefix("Post-"); + } + return bean; + } + } + + + public interface AsyncInterface { + + @Async + void work(); + + Thread getThreadOfExecution(); + } + + + public static class AsyncService implements AsyncInterface { + + private Thread threadOfExecution; + + @Override + public void work() { + this.threadOfExecution = Thread.currentThread(); + } + + @Override + public Thread getThreadOfExecution() { + return threadOfExecution; + } + } + + + @Configuration + @EnableAsync + static class Spr14949ConfigA implements AsyncConfigurer { + + @Bean + public AsyncInterface asyncBean() { + return new AsyncService(); + } + + @Override + public Executor getAsyncExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setThreadNamePrefix("Custom-"); + executor.initialize(); + return executor; + } + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return null; + } + } + + + @Configuration + @EnableAsync(proxyTargetClass = true) + static class Spr14949ConfigB implements AsyncConfigurer { + + @Bean + public AsyncInterface asyncBean() { + return new AsyncService(); + } + + @Override + public Executor getAsyncExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setThreadNamePrefix("Custom-"); + executor.initialize(); + return executor; + } + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return null; + } + } + + + @Configuration + @EnableAsync + @Import(UserConfiguration.class) + static class JdkProxyConfiguration { + + @Bean + public AsyncBeanWithInterface asyncBean() { + return new AsyncBeanWithInterface(); + } + } + + + @Configuration + static class UserConfiguration { + + @Bean + public AsyncBeanUser user(AsyncBeanWithInterface bean) { + return new AsyncBeanUser(bean); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableSchedulingTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableSchedulingTests.java new file mode 100644 index 0000000..28bdafd --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableSchedulingTests.java @@ -0,0 +1,465 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.annotation; + +import java.util.Arrays; +import java.util.Date; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.testfixture.EnabledForTestGroups; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.scheduling.config.IntervalTask; +import org.springframework.scheduling.config.ScheduledTaskHolder; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; +import org.springframework.scheduling.config.TaskManagementConfigUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; + +/** + * Tests use of @EnableScheduling on @Configuration classes. + * + * @author Chris Beams + * @author Sam Brannen + * @since 3.1 + */ +public class EnableSchedulingTests { + + private AnnotationConfigApplicationContext ctx; + + + @AfterEach + public void tearDown() { + if (ctx != null) { + ctx.close(); + } + } + + + @Test + @EnabledForTestGroups(LONG_RUNNING) + public void withFixedRateTask() throws InterruptedException { + ctx = new AnnotationConfigApplicationContext(FixedRateTaskConfig.class); + assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks().size()).isEqualTo(2); + + Thread.sleep(100); + assertThat(ctx.getBean(AtomicInteger.class).get()).isGreaterThanOrEqualTo(10); + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + public void withSubclass() throws InterruptedException { + ctx = new AnnotationConfigApplicationContext(FixedRateTaskConfigSubclass.class); + assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks().size()).isEqualTo(2); + + Thread.sleep(100); + assertThat(ctx.getBean(AtomicInteger.class).get()).isGreaterThanOrEqualTo(10); + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + public void withExplicitScheduler() throws InterruptedException { + ctx = new AnnotationConfigApplicationContext(ExplicitSchedulerConfig.class); + assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks().size()).isEqualTo(1); + + Thread.sleep(100); + assertThat(ctx.getBean(AtomicInteger.class).get()).isGreaterThanOrEqualTo(10); + assertThat(ctx.getBean(ExplicitSchedulerConfig.class).threadName).startsWith("explicitScheduler-"); + assertThat(Arrays.asList(ctx.getDefaultListableBeanFactory().getDependentBeans("myTaskScheduler")).contains( + TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue(); + } + + @Test + public void withExplicitSchedulerAmbiguity_andSchedulingEnabled() { + // No exception raised as of 4.3, aligned with the behavior for @Async methods (SPR-14030) + ctx = new AnnotationConfigApplicationContext(AmbiguousExplicitSchedulerConfig.class); + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + public void withExplicitScheduledTaskRegistrar() throws InterruptedException { + ctx = new AnnotationConfigApplicationContext(ExplicitScheduledTaskRegistrarConfig.class); + assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks().size()).isEqualTo(1); + + Thread.sleep(100); + assertThat(ctx.getBean(AtomicInteger.class).get()).isGreaterThanOrEqualTo(10); + assertThat(ctx.getBean(ExplicitScheduledTaskRegistrarConfig.class).threadName).startsWith("explicitScheduler1"); + } + + @Test + public void withAmbiguousTaskSchedulers_butNoActualTasks() { + ctx = new AnnotationConfigApplicationContext(SchedulingEnabled_withAmbiguousTaskSchedulers_butNoActualTasks.class); + } + + @Test + public void withAmbiguousTaskSchedulers_andSingleTask() { + // No exception raised as of 4.3, aligned with the behavior for @Async methods (SPR-14030) + ctx = new AnnotationConfigApplicationContext(SchedulingEnabled_withAmbiguousTaskSchedulers_andSingleTask.class); + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + public void withAmbiguousTaskSchedulers_andSingleTask_disambiguatedByScheduledTaskRegistrarBean() throws InterruptedException { + ctx = new AnnotationConfigApplicationContext( + SchedulingEnabled_withAmbiguousTaskSchedulers_andSingleTask_disambiguatedByScheduledTaskRegistrar.class); + + Thread.sleep(100); + assertThat(ctx.getBean(ThreadAwareWorker.class).executedByThread).startsWith("explicitScheduler2-"); + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + public void withAmbiguousTaskSchedulers_andSingleTask_disambiguatedBySchedulerNameAttribute() throws InterruptedException { + ctx = new AnnotationConfigApplicationContext( + SchedulingEnabled_withAmbiguousTaskSchedulers_andSingleTask_disambiguatedBySchedulerNameAttribute.class); + + Thread.sleep(100); + assertThat(ctx.getBean(ThreadAwareWorker.class).executedByThread).startsWith("explicitScheduler2-"); + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + public void withTaskAddedVia_configureTasks() throws InterruptedException { + ctx = new AnnotationConfigApplicationContext(SchedulingEnabled_withTaskAddedVia_configureTasks.class); + + Thread.sleep(100); + assertThat(ctx.getBean(ThreadAwareWorker.class).executedByThread).startsWith("taskScheduler-"); + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + public void withTriggerTask() throws InterruptedException { + ctx = new AnnotationConfigApplicationContext(TriggerTaskConfig.class); + + Thread.sleep(100); + assertThat(ctx.getBean(AtomicInteger.class).get()).isGreaterThan(1); + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + public void withInitiallyDelayedFixedRateTask() throws InterruptedException { + ctx = new AnnotationConfigApplicationContext(FixedRateTaskConfig_withInitialDelay.class); + + Thread.sleep(1950); + AtomicInteger counter = ctx.getBean(AtomicInteger.class); + + // The @Scheduled method should have been called at least once but + // not more times than the delay allows. + assertThat(counter.get()).isBetween(1, 10); + } + + + @Configuration + @EnableScheduling + static class FixedRateTaskConfig implements SchedulingConfigurer { + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + taskRegistrar.addFixedRateTask(() -> {}, 100); + } + + @Bean + public AtomicInteger counter() { + return new AtomicInteger(); + } + + @Scheduled(fixedRate = 10) + public void task() { + counter().incrementAndGet(); + } + } + + + @Configuration + static class FixedRateTaskConfigSubclass extends FixedRateTaskConfig { + } + + + @Configuration + @EnableScheduling + static class ExplicitSchedulerConfig { + + String threadName; + + @Bean + public TaskScheduler myTaskScheduler() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setThreadNamePrefix("explicitScheduler-"); + return scheduler; + } + + @Bean + public AtomicInteger counter() { + return new AtomicInteger(); + } + + @Scheduled(fixedRate = 10) + public void task() { + threadName = Thread.currentThread().getName(); + counter().incrementAndGet(); + } + } + + + @Configuration + @EnableScheduling + static class AmbiguousExplicitSchedulerConfig { + + @Bean + public TaskScheduler taskScheduler1() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setThreadNamePrefix("explicitScheduler1"); + return scheduler; + } + + @Bean + public TaskScheduler taskScheduler2() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setThreadNamePrefix("explicitScheduler2"); + return scheduler; + } + + @Scheduled(fixedRate = 10) + public void task() { + } + } + + + @Configuration + @EnableScheduling + static class ExplicitScheduledTaskRegistrarConfig implements SchedulingConfigurer { + + String threadName; + + @Bean + public TaskScheduler taskScheduler1() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setThreadNamePrefix("explicitScheduler1"); + return scheduler; + } + + @Bean + public TaskScheduler taskScheduler2() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setThreadNamePrefix("explicitScheduler2"); + return scheduler; + } + + @Bean + public AtomicInteger counter() { + return new AtomicInteger(); + } + + @Scheduled(fixedRate = 10) + public void task() { + threadName = Thread.currentThread().getName(); + counter().incrementAndGet(); + } + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + taskRegistrar.setScheduler(taskScheduler1()); + } + } + + + @Configuration + @EnableScheduling + static class SchedulingEnabled_withAmbiguousTaskSchedulers_butNoActualTasks { + + @Bean + public TaskScheduler taskScheduler1() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setThreadNamePrefix("explicitScheduler1"); + return scheduler; + } + + @Bean + public TaskScheduler taskScheduler2() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setThreadNamePrefix("explicitScheduler2"); + return scheduler; + } + } + + + @Configuration + @EnableScheduling + static class SchedulingEnabled_withAmbiguousTaskSchedulers_andSingleTask { + + @Scheduled(fixedRate = 10L) + public void task() { + } + + @Bean + public TaskScheduler taskScheduler1() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setThreadNamePrefix("explicitScheduler1"); + return scheduler; + } + + @Bean + public TaskScheduler taskScheduler2() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setThreadNamePrefix("explicitScheduler2"); + return scheduler; + } + } + + + static class ThreadAwareWorker { + + String executedByThread; + } + + + @Configuration + @EnableScheduling + static class SchedulingEnabled_withAmbiguousTaskSchedulers_andSingleTask_disambiguatedByScheduledTaskRegistrar implements SchedulingConfigurer { + + @Scheduled(fixedRate = 10) + public void task() { + worker().executedByThread = Thread.currentThread().getName(); + } + + @Bean + public ThreadAwareWorker worker() { + return new ThreadAwareWorker(); + } + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + taskRegistrar.setScheduler(taskScheduler2()); + } + + @Bean + public TaskScheduler taskScheduler1() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setThreadNamePrefix("explicitScheduler1-"); + return scheduler; + } + + @Bean + public TaskScheduler taskScheduler2() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setThreadNamePrefix("explicitScheduler2-"); + return scheduler; + } + } + + + @Configuration + @EnableScheduling + static class SchedulingEnabled_withAmbiguousTaskSchedulers_andSingleTask_disambiguatedBySchedulerNameAttribute implements SchedulingConfigurer { + + @Scheduled(fixedRate = 10) + public void task() { + worker().executedByThread = Thread.currentThread().getName(); + } + + @Bean + public ThreadAwareWorker worker() { + return new ThreadAwareWorker(); + } + + @Bean + public TaskScheduler taskScheduler1() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setThreadNamePrefix("explicitScheduler1-"); + return scheduler; + } + + @Bean + public TaskScheduler taskScheduler2() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setThreadNamePrefix("explicitScheduler2-"); + return scheduler; + } + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + taskRegistrar.setScheduler(taskScheduler2()); + } + } + + + @Configuration + @EnableScheduling + static class SchedulingEnabled_withTaskAddedVia_configureTasks implements SchedulingConfigurer { + + @Bean + public ThreadAwareWorker worker() { + return new ThreadAwareWorker(); + } + + @Bean + public TaskScheduler taskScheduler() { + return new ThreadPoolTaskScheduler(); + } + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + taskRegistrar.setScheduler(taskScheduler()); + taskRegistrar.addFixedRateTask(new IntervalTask( + () -> worker().executedByThread = Thread.currentThread().getName(), + 10, 0)); + } + } + + + @Configuration + static class TriggerTaskConfig { + + @Bean + public AtomicInteger counter() { + return new AtomicInteger(); + } + + @Bean + public TaskScheduler scheduler() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.initialize(); + scheduler.schedule(() -> counter().incrementAndGet(), + triggerContext -> new Date(new Date().getTime()+10)); + return scheduler; + } + } + + + @Configuration + @EnableScheduling + static class FixedRateTaskConfig_withInitialDelay { + + @Bean + public AtomicInteger counter() { + return new AtomicInteger(); + } + + @Scheduled(initialDelay = 1000, fixedRate = 100) + public void task() { + counter().incrementAndGet(); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorTests.java new file mode 100644 index 0000000..8429100 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorTests.java @@ -0,0 +1,946 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.annotation; + +import java.io.IOException; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Method; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.TimeZone; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.scope.ScopedProxyUtils; +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.annotation.AnnotatedBeanDefinitionReader; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.core.annotation.AliasFor; +import org.springframework.scheduling.Trigger; +import org.springframework.scheduling.TriggerContext; +import org.springframework.scheduling.config.CronTask; +import org.springframework.scheduling.config.IntervalTask; +import org.springframework.scheduling.config.ScheduledTaskHolder; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; +import org.springframework.scheduling.support.CronTrigger; +import org.springframework.scheduling.support.ScheduledMethodRunnable; +import org.springframework.scheduling.support.SimpleTriggerContext; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; +import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Mark Fisher + * @author Juergen Hoeller + * @author Chris Beams + * @author Sam Brannen + * @author Stevo Slavić + */ +public class ScheduledAnnotationBeanPostProcessorTests { + + private final StaticApplicationContext context = new StaticApplicationContext(); + + + @AfterEach + public void closeContextAfterTest() { + context.close(); + } + + + @Test + public void fixedDelayTask() { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(FixedDelayTestBean.class); + context.registerBeanDefinition("postProcessor", processorDefinition); + context.registerBeanDefinition("target", targetDefinition); + context.refresh(); + + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertThat(postProcessor.getScheduledTasks().size()).isEqualTo(1); + + Object target = context.getBean("target"); + ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) + new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); + @SuppressWarnings("unchecked") + List fixedDelayTasks = (List) + new DirectFieldAccessor(registrar).getPropertyValue("fixedDelayTasks"); + assertThat(fixedDelayTasks.size()).isEqualTo(1); + IntervalTask task = fixedDelayTasks.get(0); + ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); + Object targetObject = runnable.getTarget(); + Method targetMethod = runnable.getMethod(); + assertThat(targetObject).isEqualTo(target); + assertThat(targetMethod.getName()).isEqualTo("fixedDelay"); + assertThat(task.getInitialDelay()).isEqualTo(0L); + assertThat(task.getInterval()).isEqualTo(5000L); + } + + @Test + public void fixedRateTask() { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(FixedRateTestBean.class); + context.registerBeanDefinition("postProcessor", processorDefinition); + context.registerBeanDefinition("target", targetDefinition); + context.refresh(); + + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertThat(postProcessor.getScheduledTasks().size()).isEqualTo(1); + + Object target = context.getBean("target"); + ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) + new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); + @SuppressWarnings("unchecked") + List fixedRateTasks = (List) + new DirectFieldAccessor(registrar).getPropertyValue("fixedRateTasks"); + assertThat(fixedRateTasks.size()).isEqualTo(1); + IntervalTask task = fixedRateTasks.get(0); + ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); + Object targetObject = runnable.getTarget(); + Method targetMethod = runnable.getMethod(); + assertThat(targetObject).isEqualTo(target); + assertThat(targetMethod.getName()).isEqualTo("fixedRate"); + assertThat(task.getInitialDelay()).isEqualTo(0L); + assertThat(task.getInterval()).isEqualTo(3000L); + } + + @Test + public void fixedRateTaskWithInitialDelay() { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(FixedRateWithInitialDelayTestBean.class); + context.registerBeanDefinition("postProcessor", processorDefinition); + context.registerBeanDefinition("target", targetDefinition); + context.refresh(); + + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertThat(postProcessor.getScheduledTasks().size()).isEqualTo(1); + + Object target = context.getBean("target"); + ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) + new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); + @SuppressWarnings("unchecked") + List fixedRateTasks = (List) + new DirectFieldAccessor(registrar).getPropertyValue("fixedRateTasks"); + assertThat(fixedRateTasks.size()).isEqualTo(1); + IntervalTask task = fixedRateTasks.get(0); + ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); + Object targetObject = runnable.getTarget(); + Method targetMethod = runnable.getMethod(); + assertThat(targetObject).isEqualTo(target); + assertThat(targetMethod.getName()).isEqualTo("fixedRate"); + assertThat(task.getInitialDelay()).isEqualTo(1000L); + assertThat(task.getInterval()).isEqualTo(3000L); + } + + @Test + public void severalFixedRatesWithRepeatedScheduledAnnotation() { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(SeveralFixedRatesWithRepeatedScheduledAnnotationTestBean.class); + severalFixedRates(context, processorDefinition, targetDefinition); + } + + @Test + public void severalFixedRatesWithSchedulesContainerAnnotation() { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(SeveralFixedRatesWithSchedulesContainerAnnotationTestBean.class); + severalFixedRates(context, processorDefinition, targetDefinition); + } + + @Test + public void severalFixedRatesOnBaseClass() { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(FixedRatesSubBean.class); + severalFixedRates(context, processorDefinition, targetDefinition); + } + + @Test + public void severalFixedRatesOnDefaultMethod() { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(FixedRatesDefaultBean.class); + severalFixedRates(context, processorDefinition, targetDefinition); + } + + @Test + public void severalFixedRatesAgainstNestedCglibProxy() { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(SeveralFixedRatesWithRepeatedScheduledAnnotationTestBean.class); + targetDefinition.setFactoryMethodName("nestedProxy"); + severalFixedRates(context, processorDefinition, targetDefinition); + } + + private void severalFixedRates(StaticApplicationContext context, + BeanDefinition processorDefinition, BeanDefinition targetDefinition) { + + context.registerBeanDefinition("postProcessor", processorDefinition); + context.registerBeanDefinition("target", targetDefinition); + context.refresh(); + + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertThat(postProcessor.getScheduledTasks().size()).isEqualTo(2); + + Object target = context.getBean("target"); + ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) + new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); + @SuppressWarnings("unchecked") + List fixedRateTasks = (List) + new DirectFieldAccessor(registrar).getPropertyValue("fixedRateTasks"); + assertThat(fixedRateTasks.size()).isEqualTo(2); + IntervalTask task1 = fixedRateTasks.get(0); + ScheduledMethodRunnable runnable1 = (ScheduledMethodRunnable) task1.getRunnable(); + Object targetObject = runnable1.getTarget(); + Method targetMethod = runnable1.getMethod(); + assertThat(targetObject).isEqualTo(target); + assertThat(targetMethod.getName()).isEqualTo("fixedRate"); + assertThat(task1.getInitialDelay()).isEqualTo(0); + assertThat(task1.getInterval()).isEqualTo(4000L); + IntervalTask task2 = fixedRateTasks.get(1); + ScheduledMethodRunnable runnable2 = (ScheduledMethodRunnable) task2.getRunnable(); + targetObject = runnable2.getTarget(); + targetMethod = runnable2.getMethod(); + assertThat(targetObject).isEqualTo(target); + assertThat(targetMethod.getName()).isEqualTo("fixedRate"); + assertThat(task2.getInitialDelay()).isEqualTo(2000L); + assertThat(task2.getInterval()).isEqualTo(4000L); + } + + @Test + public void cronTask() { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(CronTestBean.class); + context.registerBeanDefinition("postProcessor", processorDefinition); + context.registerBeanDefinition("target", targetDefinition); + context.refresh(); + + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertThat(postProcessor.getScheduledTasks().size()).isEqualTo(1); + + Object target = context.getBean("target"); + ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) + new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); + @SuppressWarnings("unchecked") + List cronTasks = (List) + new DirectFieldAccessor(registrar).getPropertyValue("cronTasks"); + assertThat(cronTasks.size()).isEqualTo(1); + CronTask task = cronTasks.get(0); + ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); + Object targetObject = runnable.getTarget(); + Method targetMethod = runnable.getMethod(); + assertThat(targetObject).isEqualTo(target); + assertThat(targetMethod.getName()).isEqualTo("cron"); + assertThat(task.getExpression()).isEqualTo("*/7 * * * * ?"); + } + + @Test + public void cronTaskWithZone() { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(CronWithTimezoneTestBean.class); + context.registerBeanDefinition("postProcessor", processorDefinition); + context.registerBeanDefinition("target", targetDefinition); + context.refresh(); + + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertThat(postProcessor.getScheduledTasks().size()).isEqualTo(1); + + Object target = context.getBean("target"); + ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) + new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); + @SuppressWarnings("unchecked") + List cronTasks = (List) + new DirectFieldAccessor(registrar).getPropertyValue("cronTasks"); + assertThat(cronTasks.size()).isEqualTo(1); + CronTask task = cronTasks.get(0); + ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); + Object targetObject = runnable.getTarget(); + Method targetMethod = runnable.getMethod(); + assertThat(targetObject).isEqualTo(target); + assertThat(targetMethod.getName()).isEqualTo("cron"); + assertThat(task.getExpression()).isEqualTo("0 0 0-4,6-23 * * ?"); + Trigger trigger = task.getTrigger(); + assertThat(trigger).isNotNull(); + boolean condition = trigger instanceof CronTrigger; + assertThat(condition).isTrue(); + CronTrigger cronTrigger = (CronTrigger) trigger; + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT+10")); + cal.clear(); + cal.set(2013, 3, 15, 4, 0); // 15-04-2013 4:00 GMT+10 + Date lastScheduledExecutionTime = cal.getTime(); + Date lastActualExecutionTime = cal.getTime(); + cal.add(Calendar.MINUTE, 30); // 4:30 + Date lastCompletionTime = cal.getTime(); + TriggerContext triggerContext = new SimpleTriggerContext( + lastScheduledExecutionTime, lastActualExecutionTime, lastCompletionTime); + cal.add(Calendar.MINUTE, 30); + cal.add(Calendar.HOUR_OF_DAY, 1); // 6:00 + Date nextExecutionTime = cronTrigger.nextExecutionTime(triggerContext); + // assert that 6:00 is next execution time + assertThat(nextExecutionTime).isEqualTo(cal.getTime()); + } + + @Test + public void cronTaskWithInvalidZone() { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(CronWithInvalidTimezoneTestBean.class); + context.registerBeanDefinition("postProcessor", processorDefinition); + context.registerBeanDefinition("target", targetDefinition); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy( + context::refresh); + } + + @Test + public void cronTaskWithMethodValidation() { + BeanDefinition validationDefinition = new RootBeanDefinition(MethodValidationPostProcessor.class); + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(CronTestBean.class); + context.registerBeanDefinition("methodValidation", validationDefinition); + context.registerBeanDefinition("postProcessor", processorDefinition); + context.registerBeanDefinition("target", targetDefinition); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy( + context::refresh); + } + + @Test + public void cronTaskWithScopedProxy() { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + context.registerBeanDefinition("postProcessor", processorDefinition); + new AnnotatedBeanDefinitionReader(context).register(ProxiedCronTestBean.class, ProxiedCronTestBeanDependent.class); + context.refresh(); + + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertThat(postProcessor.getScheduledTasks().size()).isEqualTo(1); + + ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) + new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); + @SuppressWarnings("unchecked") + List cronTasks = (List) + new DirectFieldAccessor(registrar).getPropertyValue("cronTasks"); + assertThat(cronTasks.size()).isEqualTo(1); + CronTask task = cronTasks.get(0); + ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); + Object targetObject = runnable.getTarget(); + Method targetMethod = runnable.getMethod(); + assertThat(targetObject).isEqualTo(context.getBean(ScopedProxyUtils.getTargetBeanName("target"))); + assertThat(targetMethod.getName()).isEqualTo("cron"); + assertThat(task.getExpression()).isEqualTo("*/7 * * * * ?"); + } + + @Test + public void metaAnnotationWithFixedRate() { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(MetaAnnotationFixedRateTestBean.class); + context.registerBeanDefinition("postProcessor", processorDefinition); + context.registerBeanDefinition("target", targetDefinition); + context.refresh(); + + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertThat(postProcessor.getScheduledTasks().size()).isEqualTo(1); + + Object target = context.getBean("target"); + ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) + new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); + @SuppressWarnings("unchecked") + List fixedRateTasks = (List) + new DirectFieldAccessor(registrar).getPropertyValue("fixedRateTasks"); + assertThat(fixedRateTasks.size()).isEqualTo(1); + IntervalTask task = fixedRateTasks.get(0); + ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); + Object targetObject = runnable.getTarget(); + Method targetMethod = runnable.getMethod(); + assertThat(targetObject).isEqualTo(target); + assertThat(targetMethod.getName()).isEqualTo("checkForUpdates"); + assertThat(task.getInterval()).isEqualTo(5000L); + } + + @Test + public void composedAnnotationWithInitialDelayAndFixedRate() { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(ComposedAnnotationFixedRateTestBean.class); + context.registerBeanDefinition("postProcessor", processorDefinition); + context.registerBeanDefinition("target", targetDefinition); + context.refresh(); + + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertThat(postProcessor.getScheduledTasks().size()).isEqualTo(1); + + Object target = context.getBean("target"); + ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) + new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); + @SuppressWarnings("unchecked") + List fixedRateTasks = (List) + new DirectFieldAccessor(registrar).getPropertyValue("fixedRateTasks"); + assertThat(fixedRateTasks.size()).isEqualTo(1); + IntervalTask task = fixedRateTasks.get(0); + ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); + Object targetObject = runnable.getTarget(); + Method targetMethod = runnable.getMethod(); + assertThat(targetObject).isEqualTo(target); + assertThat(targetMethod.getName()).isEqualTo("checkForUpdates"); + assertThat(task.getInterval()).isEqualTo(5000L); + assertThat(task.getInitialDelay()).isEqualTo(1000L); + } + + @Test + public void metaAnnotationWithCronExpression() { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(MetaAnnotationCronTestBean.class); + context.registerBeanDefinition("postProcessor", processorDefinition); + context.registerBeanDefinition("target", targetDefinition); + context.refresh(); + + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertThat(postProcessor.getScheduledTasks().size()).isEqualTo(1); + + Object target = context.getBean("target"); + ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) + new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); + @SuppressWarnings("unchecked") + List cronTasks = (List) + new DirectFieldAccessor(registrar).getPropertyValue("cronTasks"); + assertThat(cronTasks.size()).isEqualTo(1); + CronTask task = cronTasks.get(0); + ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); + Object targetObject = runnable.getTarget(); + Method targetMethod = runnable.getMethod(); + assertThat(targetObject).isEqualTo(target); + assertThat(targetMethod.getName()).isEqualTo("generateReport"); + assertThat(task.getExpression()).isEqualTo("0 0 * * * ?"); + } + + @Test + public void propertyPlaceholderWithCron() { + String businessHoursCronExpression = "0 0 9-17 * * MON-FRI"; + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition placeholderDefinition = new RootBeanDefinition(PropertySourcesPlaceholderConfigurer.class); + Properties properties = new Properties(); + properties.setProperty("schedules.businessHours", businessHoursCronExpression); + placeholderDefinition.getPropertyValues().addPropertyValue("properties", properties); + BeanDefinition targetDefinition = new RootBeanDefinition(PropertyPlaceholderWithCronTestBean.class); + context.registerBeanDefinition("postProcessor", processorDefinition); + context.registerBeanDefinition("placeholder", placeholderDefinition); + context.registerBeanDefinition("target", targetDefinition); + context.refresh(); + + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertThat(postProcessor.getScheduledTasks().size()).isEqualTo(1); + + Object target = context.getBean("target"); + ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) + new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); + @SuppressWarnings("unchecked") + List cronTasks = (List) + new DirectFieldAccessor(registrar).getPropertyValue("cronTasks"); + assertThat(cronTasks.size()).isEqualTo(1); + CronTask task = cronTasks.get(0); + ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); + Object targetObject = runnable.getTarget(); + Method targetMethod = runnable.getMethod(); + assertThat(targetObject).isEqualTo(target); + assertThat(targetMethod.getName()).isEqualTo("x"); + assertThat(task.getExpression()).isEqualTo(businessHoursCronExpression); + } + + @Test + public void propertyPlaceholderWithInactiveCron() { + String businessHoursCronExpression = "-"; + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition placeholderDefinition = new RootBeanDefinition(PropertySourcesPlaceholderConfigurer.class); + Properties properties = new Properties(); + properties.setProperty("schedules.businessHours", businessHoursCronExpression); + placeholderDefinition.getPropertyValues().addPropertyValue("properties", properties); + BeanDefinition targetDefinition = new RootBeanDefinition(PropertyPlaceholderWithCronTestBean.class); + context.registerBeanDefinition("postProcessor", processorDefinition); + context.registerBeanDefinition("placeholder", placeholderDefinition); + context.registerBeanDefinition("target", targetDefinition); + context.refresh(); + + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertThat(postProcessor.getScheduledTasks().isEmpty()).isTrue(); + } + + @Test + public void propertyPlaceholderWithFixedDelayInMillis() { + propertyPlaceholderWithFixedDelay(false); + } + + @Test + public void propertyPlaceholderWithFixedDelayInDuration() { + propertyPlaceholderWithFixedDelay(true); + } + + private void propertyPlaceholderWithFixedDelay(boolean durationFormat) { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition placeholderDefinition = new RootBeanDefinition(PropertySourcesPlaceholderConfigurer.class); + Properties properties = new Properties(); + properties.setProperty("fixedDelay", (durationFormat ? "PT5S" : "5000")); + properties.setProperty("initialDelay", (durationFormat ? "PT1S" : "1000")); + placeholderDefinition.getPropertyValues().addPropertyValue("properties", properties); + BeanDefinition targetDefinition = new RootBeanDefinition(PropertyPlaceholderWithFixedDelayTestBean.class); + context.registerBeanDefinition("postProcessor", processorDefinition); + context.registerBeanDefinition("placeholder", placeholderDefinition); + context.registerBeanDefinition("target", targetDefinition); + context.refresh(); + + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertThat(postProcessor.getScheduledTasks().size()).isEqualTo(1); + + Object target = context.getBean("target"); + ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) + new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); + @SuppressWarnings("unchecked") + List fixedDelayTasks = (List) + new DirectFieldAccessor(registrar).getPropertyValue("fixedDelayTasks"); + assertThat(fixedDelayTasks.size()).isEqualTo(1); + IntervalTask task = fixedDelayTasks.get(0); + ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); + Object targetObject = runnable.getTarget(); + Method targetMethod = runnable.getMethod(); + assertThat(targetObject).isEqualTo(target); + assertThat(targetMethod.getName()).isEqualTo("fixedDelay"); + assertThat(task.getInitialDelay()).isEqualTo(1000L); + assertThat(task.getInterval()).isEqualTo(5000L); + } + + @Test + public void propertyPlaceholderWithFixedRateInMillis() { + propertyPlaceholderWithFixedRate(false); + } + + @Test + public void propertyPlaceholderWithFixedRateInDuration() { + propertyPlaceholderWithFixedRate(true); + } + + private void propertyPlaceholderWithFixedRate(boolean durationFormat) { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition placeholderDefinition = new RootBeanDefinition(PropertySourcesPlaceholderConfigurer.class); + Properties properties = new Properties(); + properties.setProperty("fixedRate", (durationFormat ? "PT3S" : "3000")); + properties.setProperty("initialDelay", (durationFormat ? "PT1S" : "1000")); + placeholderDefinition.getPropertyValues().addPropertyValue("properties", properties); + BeanDefinition targetDefinition = new RootBeanDefinition(PropertyPlaceholderWithFixedRateTestBean.class); + context.registerBeanDefinition("postProcessor", processorDefinition); + context.registerBeanDefinition("placeholder", placeholderDefinition); + context.registerBeanDefinition("target", targetDefinition); + context.refresh(); + + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertThat(postProcessor.getScheduledTasks().size()).isEqualTo(1); + + Object target = context.getBean("target"); + ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) + new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); + @SuppressWarnings("unchecked") + List fixedRateTasks = (List) + new DirectFieldAccessor(registrar).getPropertyValue("fixedRateTasks"); + assertThat(fixedRateTasks.size()).isEqualTo(1); + IntervalTask task = fixedRateTasks.get(0); + ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); + Object targetObject = runnable.getTarget(); + Method targetMethod = runnable.getMethod(); + assertThat(targetObject).isEqualTo(target); + assertThat(targetMethod.getName()).isEqualTo("fixedRate"); + assertThat(task.getInitialDelay()).isEqualTo(1000L); + assertThat(task.getInterval()).isEqualTo(3000L); + } + + @Test + public void expressionWithCron() { + String businessHoursCronExpression = "0 0 9-17 * * MON-FRI"; + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(ExpressionWithCronTestBean.class); + context.registerBeanDefinition("postProcessor", processorDefinition); + context.registerBeanDefinition("target", targetDefinition); + Map schedules = new HashMap<>(); + schedules.put("businessHours", businessHoursCronExpression); + context.getBeanFactory().registerSingleton("schedules", schedules); + context.refresh(); + + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertThat(postProcessor.getScheduledTasks().size()).isEqualTo(1); + + Object target = context.getBean("target"); + ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) + new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); + @SuppressWarnings("unchecked") + List cronTasks = (List) + new DirectFieldAccessor(registrar).getPropertyValue("cronTasks"); + assertThat(cronTasks.size()).isEqualTo(1); + CronTask task = cronTasks.get(0); + ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); + Object targetObject = runnable.getTarget(); + Method targetMethod = runnable.getMethod(); + assertThat(targetObject).isEqualTo(target); + assertThat(targetMethod.getName()).isEqualTo("x"); + assertThat(task.getExpression()).isEqualTo(businessHoursCronExpression); + } + + @Test + public void propertyPlaceholderForMetaAnnotation() { + String businessHoursCronExpression = "0 0 9-17 * * MON-FRI"; + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition placeholderDefinition = new RootBeanDefinition(PropertySourcesPlaceholderConfigurer.class); + Properties properties = new Properties(); + properties.setProperty("schedules.businessHours", businessHoursCronExpression); + placeholderDefinition.getPropertyValues().addPropertyValue("properties", properties); + BeanDefinition targetDefinition = new RootBeanDefinition(PropertyPlaceholderMetaAnnotationTestBean.class); + context.registerBeanDefinition("postProcessor", processorDefinition); + context.registerBeanDefinition("placeholder", placeholderDefinition); + context.registerBeanDefinition("target", targetDefinition); + context.refresh(); + + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertThat(postProcessor.getScheduledTasks().size()).isEqualTo(1); + + Object target = context.getBean("target"); + ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) + new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); + @SuppressWarnings("unchecked") + List cronTasks = (List) + new DirectFieldAccessor(registrar).getPropertyValue("cronTasks"); + assertThat(cronTasks.size()).isEqualTo(1); + CronTask task = cronTasks.get(0); + ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); + Object targetObject = runnable.getTarget(); + Method targetMethod = runnable.getMethod(); + assertThat(targetObject).isEqualTo(target); + assertThat(targetMethod.getName()).isEqualTo("y"); + assertThat(task.getExpression()).isEqualTo(businessHoursCronExpression); + } + + @Test + public void nonVoidReturnType() { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(NonVoidReturnTypeTestBean.class); + context.registerBeanDefinition("postProcessor", processorDefinition); + context.registerBeanDefinition("target", targetDefinition); + context.refresh(); + + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertThat(postProcessor.getScheduledTasks().size()).isEqualTo(1); + + Object target = context.getBean("target"); + ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) + new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); + @SuppressWarnings("unchecked") + List cronTasks = (List) + new DirectFieldAccessor(registrar).getPropertyValue("cronTasks"); + assertThat(cronTasks.size()).isEqualTo(1); + CronTask task = cronTasks.get(0); + ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); + Object targetObject = runnable.getTarget(); + Method targetMethod = runnable.getMethod(); + assertThat(targetObject).isEqualTo(target); + assertThat(targetMethod.getName()).isEqualTo("cron"); + assertThat(task.getExpression()).isEqualTo("0 0 9-17 * * MON-FRI"); + } + + @Test + public void emptyAnnotation() { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(EmptyAnnotationTestBean.class); + context.registerBeanDefinition("postProcessor", processorDefinition); + context.registerBeanDefinition("target", targetDefinition); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy( + context::refresh); + } + + @Test + public void invalidCron() throws Throwable { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(InvalidCronTestBean.class); + context.registerBeanDefinition("postProcessor", processorDefinition); + context.registerBeanDefinition("target", targetDefinition); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy( + context::refresh); + } + + @Test + public void nonEmptyParamList() { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(NonEmptyParamListTestBean.class); + context.registerBeanDefinition("postProcessor", processorDefinition); + context.registerBeanDefinition("target", targetDefinition); + assertThatExceptionOfType(BeanCreationException.class).isThrownBy( + context::refresh); + } + + + static class FixedDelayTestBean { + + @Scheduled(fixedDelay = 5000) + public void fixedDelay() { + } + } + + + static class FixedRateTestBean { + + @Scheduled(fixedRate = 3000) + public void fixedRate() { + } + } + + + static class FixedRateWithInitialDelayTestBean { + + @Scheduled(fixedRate = 3000, initialDelay = 1000) + public void fixedRate() { + } + } + + + static class SeveralFixedRatesWithSchedulesContainerAnnotationTestBean { + + @Schedules({@Scheduled(fixedRate = 4000), @Scheduled(fixedRate = 4000, initialDelay = 2000)}) + public void fixedRate() { + } + } + + + static class SeveralFixedRatesWithRepeatedScheduledAnnotationTestBean { + + @Scheduled(fixedRate = 4000) + @Scheduled(fixedRate = 4000, initialDelay = 2000) + public void fixedRate() { + } + + static SeveralFixedRatesWithRepeatedScheduledAnnotationTestBean nestedProxy() { + ProxyFactory pf1 = new ProxyFactory(new SeveralFixedRatesWithRepeatedScheduledAnnotationTestBean()); + pf1.setProxyTargetClass(true); + ProxyFactory pf2 = new ProxyFactory(pf1.getProxy()); + pf2.setProxyTargetClass(true); + return (SeveralFixedRatesWithRepeatedScheduledAnnotationTestBean) pf2.getProxy(); + } + } + + + static class FixedRatesBaseBean { + + @Scheduled(fixedRate = 4000) + @Scheduled(fixedRate = 4000, initialDelay = 2000) + public void fixedRate() { + } + } + + + static class FixedRatesSubBean extends FixedRatesBaseBean { + } + + + interface FixedRatesDefaultMethod { + + @Scheduled(fixedRate = 4000) + @Scheduled(fixedRate = 4000, initialDelay = 2000) + default void fixedRate() { + } + } + + + static class FixedRatesDefaultBean implements FixedRatesDefaultMethod { + } + + + @Validated + static class CronTestBean { + + @Scheduled(cron = "*/7 * * * * ?") + private void cron() throws IOException { + throw new IOException("no no no"); + } + } + + + static class CronWithTimezoneTestBean { + + @Scheduled(cron = "0 0 0-4,6-23 * * ?", zone = "GMT+10") + protected void cron() throws IOException { + throw new IOException("no no no"); + } + } + + + static class CronWithInvalidTimezoneTestBean { + + @Scheduled(cron = "0 0 0-4,6-23 * * ?", zone = "FOO") + public void cron() throws IOException { + throw new IOException("no no no"); + } + } + + + @Component("target") + @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) + static class ProxiedCronTestBean { + + @Scheduled(cron = "*/7 * * * * ?") + public void cron() throws IOException { + throw new IOException("no no no"); + } + } + + + static class ProxiedCronTestBeanDependent { + + public ProxiedCronTestBeanDependent(ProxiedCronTestBean testBean) { + } + } + + + static class NonVoidReturnTypeTestBean { + + @Scheduled(cron = "0 0 9-17 * * MON-FRI") + public String cron() { + return "oops"; + } + } + + + static class EmptyAnnotationTestBean { + + @Scheduled + public void invalid() { + } + } + + + static class InvalidCronTestBean { + + @Scheduled(cron = "abc") + public void invalid() { + } + } + + + static class NonEmptyParamListTestBean { + + @Scheduled(fixedRate = 3000) + public void invalid(String oops) { + } + } + + + @Scheduled(fixedRate = 5000) + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + private @interface EveryFiveSeconds { + } + + @Scheduled(cron = "0 0 * * * ?") + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + private @interface Hourly { + } + + @Scheduled(initialDelay = 1000) + @Retention(RetentionPolicy.RUNTIME) + private @interface WaitASec { + + @AliasFor(annotation = Scheduled.class) + long fixedDelay() default -1; + + @AliasFor(annotation = Scheduled.class) + long fixedRate() default -1; + } + + + static class MetaAnnotationFixedRateTestBean { + + @EveryFiveSeconds + public void checkForUpdates() { + } + } + + + static class ComposedAnnotationFixedRateTestBean { + + @WaitASec(fixedRate = 5000) + public void checkForUpdates() { + } + } + + + static class MetaAnnotationCronTestBean { + + @Hourly + public void generateReport() { + } + } + + + static class PropertyPlaceholderWithCronTestBean { + + @Scheduled(cron = "${schedules.businessHours}") + public void x() { + } + } + + + static class PropertyPlaceholderWithFixedDelayTestBean { + + @Scheduled(fixedDelayString = "${fixedDelay}", initialDelayString = "${initialDelay}") + public void fixedDelay() { + } + } + + + static class PropertyPlaceholderWithFixedRateTestBean { + + @Scheduled(fixedRateString = "${fixedRate}", initialDelayString = "${initialDelay}") + public void fixedRate() { + } + } + + + static class ExpressionWithCronTestBean { + + @Scheduled(cron = "#{schedules.businessHours}") + public void x() { + } + } + + + @Scheduled(cron = "${schedules.businessHours}") + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + private @interface BusinessHours { + } + + + static class PropertyPlaceholderMetaAnnotationTestBean { + + @BusinessHours + public void y() { + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/TestableAsyncUncaughtExceptionHandler.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/TestableAsyncUncaughtExceptionHandler.java new file mode 100644 index 0000000..9ff3b8c --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/TestableAsyncUncaughtExceptionHandler.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.annotation; + +import java.lang.reflect.Method; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * An {@link AsyncUncaughtExceptionHandler} implementation used for testing purposes. + * @author Stephane Nicoll + */ +class TestableAsyncUncaughtExceptionHandler + implements AsyncUncaughtExceptionHandler { + + private final CountDownLatch latch = new CountDownLatch(1); + + private UncaughtExceptionDescriptor descriptor; + + private final boolean throwUnexpectedException; + + TestableAsyncUncaughtExceptionHandler() { + this(false); + } + + TestableAsyncUncaughtExceptionHandler(boolean throwUnexpectedException) { + this.throwUnexpectedException = throwUnexpectedException; + } + + @Override + public void handleUncaughtException(Throwable ex, Method method, Object... params) { + descriptor = new UncaughtExceptionDescriptor(ex, method); + this.latch.countDown(); + if (throwUnexpectedException) { + throw new IllegalStateException("Test exception"); + } + } + + public boolean isCalled() { + return descriptor != null; + } + + public void assertCalledWith(Method expectedMethod, Class expectedExceptionType) { + assertThat(descriptor).as("Handler not called").isNotNull(); + assertThat(descriptor.ex.getClass()).as("Wrong exception type").isEqualTo(expectedExceptionType); + assertThat(descriptor.method).as("Wrong method").isEqualTo(expectedMethod); + } + + public void await(long timeout) { + try { + this.latch.await(timeout, TimeUnit.MILLISECONDS); + } + catch (Exception e) { + Thread.currentThread().interrupt(); + } + } + + private static class UncaughtExceptionDescriptor { + private final Throwable ex; + + private final Method method; + + private UncaughtExceptionDescriptor(Throwable ex, Method method) { + this.ex = ex; + this.method = method; + } + } +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/AbstractSchedulingTaskExecutorTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/AbstractSchedulingTaskExecutorTests.java new file mode 100644 index 0000000..e2bd987 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/AbstractSchedulingTaskExecutorTests.java @@ -0,0 +1,340 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.concurrent; + +import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.core.task.AsyncListenableTaskExecutor; +import org.springframework.util.concurrent.ListenableFuture; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Juergen Hoeller + * @author Sam Brannen + * @since 5.0.5 + */ +abstract class AbstractSchedulingTaskExecutorTests { + + private AsyncListenableTaskExecutor executor; + + protected String testName; + + protected String threadNamePrefix; + + private volatile Object outcome; + + + @BeforeEach + void setUp(TestInfo testInfo) { + this.testName = testInfo.getTestMethod().get().getName(); + this.threadNamePrefix = this.testName + "-"; + this.executor = buildExecutor(); + } + + protected abstract AsyncListenableTaskExecutor buildExecutor(); + + @AfterEach + void shutdownExecutor() throws Exception { + if (executor instanceof DisposableBean) { + ((DisposableBean) executor).destroy(); + } + } + + + @Test + void executeRunnable() { + TestTask task = new TestTask(this.testName, 1); + executor.execute(task); + await(task); + assertThreadNamePrefix(task); + } + + @Test + void executeFailingRunnable() { + TestTask task = new TestTask(this.testName, 0); + executor.execute(task); + Awaitility.await() + .dontCatchUncaughtExceptions() + .atMost(1, TimeUnit.SECONDS) + .pollInterval(10, TimeUnit.MILLISECONDS) + .until(() -> task.exception.get() != null && task.exception.get().getMessage().equals( + "TestTask failure for test 'executeFailingRunnable': expectedRunCount:<0>, actualRunCount:<1>")); + } + + @Test + void submitRunnable() throws Exception { + TestTask task = new TestTask(this.testName, 1); + Future future = executor.submit(task); + Object result = future.get(1000, TimeUnit.MILLISECONDS); + assertThat(result).isNull(); + assertThreadNamePrefix(task); + } + + @Test + void submitFailingRunnable() throws Exception { + TestTask task = new TestTask(this.testName, 0); + Future future = executor.submit(task); + assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> + future.get(1000, TimeUnit.MILLISECONDS)); + assertThat(future.isDone()).isTrue(); + } + + @Test + void submitRunnableWithGetAfterShutdown() throws Exception { + Future future1 = executor.submit(new TestTask(this.testName, -1)); + Future future2 = executor.submit(new TestTask(this.testName, -1)); + shutdownExecutor(); + assertThatExceptionOfType(CancellationException.class).isThrownBy(() -> { + future1.get(1000, TimeUnit.MILLISECONDS); + future2.get(1000, TimeUnit.MILLISECONDS); + }); + } + + @Test + void submitListenableRunnable() throws Exception { + TestTask task = new TestTask(this.testName, 1); + // Act + ListenableFuture future = executor.submitListenable(task); + future.addCallback(result -> outcome = result, ex -> outcome = ex); + // Assert + Awaitility.await() + .atMost(1, TimeUnit.SECONDS) + .pollInterval(10, TimeUnit.MILLISECONDS) + .until(future::isDone); + assertThat(outcome).isNull(); + assertThreadNamePrefix(task); + } + + @Test + void submitFailingListenableRunnable() throws Exception { + TestTask task = new TestTask(this.testName, 0); + ListenableFuture future = executor.submitListenable(task); + future.addCallback(result -> outcome = result, ex -> outcome = ex); + + Awaitility.await() + .dontCatchUncaughtExceptions() + .atMost(1, TimeUnit.SECONDS) + .pollInterval(10, TimeUnit.MILLISECONDS) + .until(() -> future.isDone() && outcome != null); + assertThat(outcome.getClass()).isSameAs(RuntimeException.class); + } + + @Test + void submitListenableRunnableWithGetAfterShutdown() throws Exception { + ListenableFuture future1 = executor.submitListenable(new TestTask(this.testName, -1)); + ListenableFuture future2 = executor.submitListenable(new TestTask(this.testName, -1)); + shutdownExecutor(); + + try { + future1.get(1000, TimeUnit.MILLISECONDS); + } + catch (Exception ex) { + /* ignore */ + } + Awaitility.await() + .atMost(4, TimeUnit.SECONDS) + .pollInterval(10, TimeUnit.MILLISECONDS) + .untilAsserted(() -> + assertThatExceptionOfType(CancellationException.class).isThrownBy(() -> + future2.get(1000, TimeUnit.MILLISECONDS))); + } + + @Test + void submitCallable() throws Exception { + TestCallable task = new TestCallable(this.testName, 1); + Future future = executor.submit(task); + String result = future.get(1000, TimeUnit.MILLISECONDS); + assertThat(result.substring(0, this.threadNamePrefix.length())).isEqualTo(this.threadNamePrefix); + } + + @Test + void submitFailingCallable() throws Exception { + TestCallable task = new TestCallable(this.testName, 0); + Future future = executor.submit(task); + assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> + future.get(1000, TimeUnit.MILLISECONDS)); + assertThat(future.isDone()).isTrue(); + } + + @Test + void submitCallableWithGetAfterShutdown() throws Exception { + Future future1 = executor.submit(new TestCallable(this.testName, -1)); + Future future2 = executor.submit(new TestCallable(this.testName, -1)); + shutdownExecutor(); + + try { + future1.get(1000, TimeUnit.MILLISECONDS); + } + catch (Exception ex) { + /* ignore */ + } + Awaitility.await() + .atMost(4, TimeUnit.SECONDS) + .pollInterval(10, TimeUnit.MILLISECONDS) + .untilAsserted(() -> + assertThatExceptionOfType(CancellationException.class).isThrownBy(() -> + future2.get(1000, TimeUnit.MILLISECONDS))); + } + + @Test + void submitListenableCallable() throws Exception { + TestCallable task = new TestCallable(this.testName, 1); + // Act + ListenableFuture future = executor.submitListenable(task); + future.addCallback(result -> outcome = result, ex -> outcome = ex); + // Assert + Awaitility.await() + .atMost(1, TimeUnit.SECONDS) + .pollInterval(10, TimeUnit.MILLISECONDS) + .until(() -> future.isDone() && outcome != null); + assertThat(outcome.toString().substring(0, this.threadNamePrefix.length())).isEqualTo(this.threadNamePrefix); + } + + @Test + void submitFailingListenableCallable() throws Exception { + TestCallable task = new TestCallable(this.testName, 0); + // Act + ListenableFuture future = executor.submitListenable(task); + future.addCallback(result -> outcome = result, ex -> outcome = ex); + // Assert + Awaitility.await() + .dontCatchUncaughtExceptions() + .atMost(1, TimeUnit.SECONDS) + .pollInterval(10, TimeUnit.MILLISECONDS) + .until(() -> future.isDone() && outcome != null); + assertThat(outcome.getClass()).isSameAs(RuntimeException.class); + } + + @Test + void submitListenableCallableWithGetAfterShutdown() throws Exception { + ListenableFuture future1 = executor.submitListenable(new TestCallable(this.testName, -1)); + ListenableFuture future2 = executor.submitListenable(new TestCallable(this.testName, -1)); + shutdownExecutor(); + assertThatExceptionOfType(CancellationException.class).isThrownBy(() -> { + future1.get(1000, TimeUnit.MILLISECONDS); + future2.get(1000, TimeUnit.MILLISECONDS); + }); + } + + + protected void assertThreadNamePrefix(TestTask task) { + assertThat(task.lastThread.getName().substring(0, this.threadNamePrefix.length())).isEqualTo(this.threadNamePrefix); + } + + private void await(TestTask task) { + await(task.latch); + } + + private void await(CountDownLatch latch) { + try { + latch.await(1000, TimeUnit.MILLISECONDS); + } + catch (InterruptedException ex) { + throw new IllegalStateException(ex); + } + assertThat(latch.getCount()).as("latch did not count down,").isEqualTo(0); + } + + + static class TestTask implements Runnable { + + private final int expectedRunCount; + + private final String testName; + + private final AtomicInteger actualRunCount = new AtomicInteger(); + + private final AtomicReference exception = new AtomicReference<>(); + + final CountDownLatch latch; + + Thread lastThread; + + TestTask(String testName, int expectedRunCount) { + this.testName = testName; + this.expectedRunCount = expectedRunCount; + this.latch = (expectedRunCount > 0 ? new CountDownLatch(expectedRunCount) : null); + } + + @Override + public void run() { + lastThread = Thread.currentThread(); + try { + Thread.sleep(10); + } + catch (InterruptedException ex) { + } + if (expectedRunCount >= 0) { + if (actualRunCount.incrementAndGet() > expectedRunCount) { + RuntimeException exception = new RuntimeException(String.format("%s failure for test '%s': expectedRunCount:<%d>, actualRunCount:<%d>", + getClass().getSimpleName(), this.testName, expectedRunCount, actualRunCount.get())); + this.exception.set(exception); + throw exception; + } + latch.countDown(); + } + } + } + + + static class TestCallable implements Callable { + + private final String testName; + + private final int expectedRunCount; + + private final AtomicInteger actualRunCount = new AtomicInteger(); + + TestCallable(String testName, int expectedRunCount) { + this.testName = testName; + this.expectedRunCount = expectedRunCount; + } + + @Override + public String call() throws Exception { + try { + Thread.sleep(10); + } + catch (InterruptedException ex) { + } + if (expectedRunCount >= 0) { + if (actualRunCount.incrementAndGet() > expectedRunCount) { + throw new RuntimeException(String.format("%s failure for test '%s': expectedRunCount:<%d>, actualRunCount:<%d>", + getClass().getSimpleName(), this.testName, expectedRunCount, actualRunCount.get())); + } + } + return Thread.currentThread().getName(); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutorTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutorTests.java new file mode 100644 index 0000000..ca4a79d --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutorTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.concurrent; + +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RunnableFuture; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.core.task.AsyncListenableTaskExecutor; +import org.springframework.core.task.NoOpRunnable; + +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * @author Rick Evans + * @author Juergen Hoeller + */ +class ConcurrentTaskExecutorTests extends AbstractSchedulingTaskExecutorTests { + + private final ThreadPoolExecutor concurrentExecutor = + new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); + + + @Override + protected AsyncListenableTaskExecutor buildExecutor() { + concurrentExecutor.setThreadFactory(new CustomizableThreadFactory(this.threadNamePrefix)); + return new ConcurrentTaskExecutor(concurrentExecutor); + } + + @Override + @AfterEach + void shutdownExecutor() { + for (Runnable task : concurrentExecutor.shutdownNow()) { + if (task instanceof RunnableFuture) { + ((RunnableFuture) task).cancel(true); + } + } + } + + + @Test + void zeroArgCtorResultsInDefaultTaskExecutorBeingUsed() { + ConcurrentTaskExecutor executor = new ConcurrentTaskExecutor(); + assertThatCode(() -> executor.execute(new NoOpRunnable())).doesNotThrowAnyException(); + } + + @Test + void passingNullExecutorToCtorResultsInDefaultTaskExecutorBeingUsed() { + ConcurrentTaskExecutor executor = new ConcurrentTaskExecutor(null); + assertThatCode(() -> executor.execute(new NoOpRunnable())).doesNotThrowAnyException(); + } + + @Test + void passingNullExecutorToSetterResultsInDefaultTaskExecutorBeingUsed() { + ConcurrentTaskExecutor executor = new ConcurrentTaskExecutor(); + executor.setConcurrentExecutor(null); + assertThatCode(() -> executor.execute(new NoOpRunnable())).doesNotThrowAnyException(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/DecoratedThreadPoolTaskExecutorTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/DecoratedThreadPoolTaskExecutorTests.java new file mode 100644 index 0000000..6484b2c --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/DecoratedThreadPoolTaskExecutorTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.concurrent; + +import org.springframework.core.task.AsyncListenableTaskExecutor; +import org.springframework.scheduling.support.DelegatingErrorHandlingRunnable; +import org.springframework.scheduling.support.TaskUtils; + +/** + * @author Juergen Hoeller + * @since 5.0.5 + */ +class DecoratedThreadPoolTaskExecutorTests extends AbstractSchedulingTaskExecutorTests { + + @Override + protected AsyncListenableTaskExecutor buildExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setTaskDecorator(runnable -> + new DelegatingErrorHandlingRunnable(runnable, TaskUtils.LOG_AND_PROPAGATE_ERROR_HANDLER)); + executor.setThreadNamePrefix(this.threadNamePrefix); + executor.setMaxPoolSize(1); + executor.afterPropertiesSet(); + return executor; + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ScheduledExecutorFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ScheduledExecutorFactoryBeanTests.java new file mode 100644 index 0000000..140fe6e --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ScheduledExecutorFactoryBeanTests.java @@ -0,0 +1,235 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.concurrent; + +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.task.NoOpRunnable; +import org.springframework.core.testfixture.EnabledForTestGroups; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; + +/** + * @author Rick Evans + * @author Juergen Hoeller + */ +class ScheduledExecutorFactoryBeanTests { + + @Test + void throwsExceptionIfPoolSizeIsLessThanZero() throws Exception { + ScheduledExecutorFactoryBean factory = new ScheduledExecutorFactoryBean(); + assertThatIllegalArgumentException().isThrownBy(() -> factory.setPoolSize(-1)); + } + + @Test + @SuppressWarnings("serial") + void shutdownNowIsPropagatedToTheExecutorOnDestroy() throws Exception { + final ScheduledExecutorService executor = mock(ScheduledExecutorService.class); + + ScheduledExecutorFactoryBean factory = new ScheduledExecutorFactoryBean() { + @Override + protected ScheduledExecutorService createExecutor(int poolSize, ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) { + return executor; + } + }; + factory.setScheduledExecutorTasks(new NoOpScheduledExecutorTask()); + factory.afterPropertiesSet(); + factory.destroy(); + + verify(executor).shutdownNow(); + } + + @Test + @SuppressWarnings("serial") + void shutdownIsPropagatedToTheExecutorOnDestroy() throws Exception { + final ScheduledExecutorService executor = mock(ScheduledExecutorService.class); + + ScheduledExecutorFactoryBean factory = new ScheduledExecutorFactoryBean() { + @Override + protected ScheduledExecutorService createExecutor(int poolSize, ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) { + return executor; + } + }; + factory.setScheduledExecutorTasks(new NoOpScheduledExecutorTask()); + factory.setWaitForTasksToCompleteOnShutdown(true); + factory.afterPropertiesSet(); + factory.destroy(); + + verify(executor).shutdown(); + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + void oneTimeExecutionIsSetUpAndFiresCorrectly() throws Exception { + Runnable runnable = mock(Runnable.class); + + ScheduledExecutorFactoryBean factory = new ScheduledExecutorFactoryBean(); + factory.setScheduledExecutorTasks(new ScheduledExecutorTask(runnable)); + factory.afterPropertiesSet(); + pauseToLetTaskStart(1); + factory.destroy(); + + verify(runnable).run(); + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + void fixedRepeatedExecutionIsSetUpAndFiresCorrectly() throws Exception { + Runnable runnable = mock(Runnable.class); + + ScheduledExecutorTask task = new ScheduledExecutorTask(runnable); + task.setPeriod(500); + task.setFixedRate(true); + + ScheduledExecutorFactoryBean factory = new ScheduledExecutorFactoryBean(); + factory.setScheduledExecutorTasks(task); + factory.afterPropertiesSet(); + pauseToLetTaskStart(2); + factory.destroy(); + + verify(runnable, atLeast(2)).run(); + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + void fixedRepeatedExecutionIsSetUpAndFiresCorrectlyAfterException() throws Exception { + Runnable runnable = mock(Runnable.class); + willThrow(new IllegalStateException()).given(runnable).run(); + + ScheduledExecutorTask task = new ScheduledExecutorTask(runnable); + task.setPeriod(500); + task.setFixedRate(true); + + ScheduledExecutorFactoryBean factory = new ScheduledExecutorFactoryBean(); + factory.setScheduledExecutorTasks(task); + factory.setContinueScheduledExecutionAfterException(true); + factory.afterPropertiesSet(); + pauseToLetTaskStart(2); + factory.destroy(); + + verify(runnable, atLeast(2)).run(); + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + void withInitialDelayRepeatedExecutionIsSetUpAndFiresCorrectly() throws Exception { + Runnable runnable = mock(Runnable.class); + + ScheduledExecutorTask task = new ScheduledExecutorTask(runnable); + task.setPeriod(500); + task.setDelay(3000); // nice long wait... + + ScheduledExecutorFactoryBean factory = new ScheduledExecutorFactoryBean(); + factory.setScheduledExecutorTasks(task); + factory.afterPropertiesSet(); + pauseToLetTaskStart(1); + // invoke destroy before tasks have even been scheduled... + factory.destroy(); + + // Mock must never have been called + verify(runnable, never()).run(); + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + void withInitialDelayRepeatedExecutionIsSetUpAndFiresCorrectlyAfterException() throws Exception { + Runnable runnable = mock(Runnable.class); + willThrow(new IllegalStateException()).given(runnable).run(); + + ScheduledExecutorTask task = new ScheduledExecutorTask(runnable); + task.setPeriod(500); + task.setDelay(3000); // nice long wait... + + ScheduledExecutorFactoryBean factory = new ScheduledExecutorFactoryBean(); + factory.setScheduledExecutorTasks(task); + factory.setContinueScheduledExecutionAfterException(true); + factory.afterPropertiesSet(); + pauseToLetTaskStart(1); + // invoke destroy before tasks have even been scheduled... + factory.destroy(); + + // Mock must never have been called + verify(runnable, never()).run(); + } + + @Test + @SuppressWarnings("serial") + void settingThreadFactoryToNullForcesUseOfDefaultButIsOtherwiseCool() throws Exception { + ScheduledExecutorFactoryBean factory = new ScheduledExecutorFactoryBean() { + @Override + protected ScheduledExecutorService createExecutor(int poolSize, ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) { + assertThat("Bah; the setThreadFactory(..) method must use a default ThreadFactory if a null arg is passed in.").isNotNull(); + return super.createExecutor(poolSize, threadFactory, rejectedExecutionHandler); + } + }; + factory.setScheduledExecutorTasks(new NoOpScheduledExecutorTask()); + factory.setThreadFactory(null); // the null must not propagate + factory.afterPropertiesSet(); + factory.destroy(); + } + + @Test + @SuppressWarnings("serial") + void settingRejectedExecutionHandlerToNullForcesUseOfDefaultButIsOtherwiseCool() throws Exception { + ScheduledExecutorFactoryBean factory = new ScheduledExecutorFactoryBean() { + @Override + protected ScheduledExecutorService createExecutor(int poolSize, ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) { + assertThat("Bah; the setRejectedExecutionHandler(..) method must use a default RejectedExecutionHandler if a null arg is passed in.").isNotNull(); + return super.createExecutor(poolSize, threadFactory, rejectedExecutionHandler); + } + }; + factory.setScheduledExecutorTasks(new NoOpScheduledExecutorTask()); + factory.setRejectedExecutionHandler(null); // the null must not propagate + factory.afterPropertiesSet(); + factory.destroy(); + } + + @Test + void objectTypeReportsCorrectType() throws Exception { + ScheduledExecutorFactoryBean factory = new ScheduledExecutorFactoryBean(); + assertThat(factory.getObjectType()).isEqualTo(ScheduledExecutorService.class); + } + + + private static void pauseToLetTaskStart(int seconds) { + try { + Thread.sleep(seconds * 1000); + } + catch (InterruptedException ignored) { + } + } + + + private static class NoOpScheduledExecutorTask extends ScheduledExecutorTask { + + NoOpScheduledExecutorTask() { + super(new NoOpRunnable()); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolExecutorFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolExecutorFactoryBeanTests.java new file mode 100644 index 0000000..abba6cf --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolExecutorFactoryBeanTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.concurrent; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.FutureTask; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + */ +class ThreadPoolExecutorFactoryBeanTests { + + @Test + void defaultExecutor() throws Exception { + ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(ExecutorConfig.class); + ExecutorService executor = context.getBean(ExecutorService.class); + + FutureTask task = new FutureTask<>(() -> "foo"); + executor.execute(task); + assertThat(task.get()).isEqualTo("foo"); + context.close(); + } + + + @Configuration + static class ExecutorConfig { + + @Bean + ThreadPoolExecutorFactoryBean executor() { + return new ThreadPoolExecutorFactoryBean(); + } + + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutorTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutorTests.java new file mode 100644 index 0000000..11159af --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutorTests.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.concurrent; + +import org.springframework.core.task.AsyncListenableTaskExecutor; + +/** + * @author Juergen Hoeller + * @since 5.0.5 + */ +class ThreadPoolTaskExecutorTests extends AbstractSchedulingTaskExecutorTests { + + @Override + protected AsyncListenableTaskExecutor buildExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setThreadNamePrefix(this.threadNamePrefix); + executor.setMaxPoolSize(1); + executor.afterPropertiesSet(); + return executor; + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskSchedulerTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskSchedulerTests.java new file mode 100644 index 0000000..a965446 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskSchedulerTests.java @@ -0,0 +1,193 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.concurrent; + +import java.util.Date; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.task.AsyncListenableTaskExecutor; +import org.springframework.scheduling.Trigger; +import org.springframework.scheduling.TriggerContext; +import org.springframework.util.ErrorHandler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Mark Fisher + * @author Juergen Hoeller + * @author Sam Brannen + * @since 3.0 + */ +public class ThreadPoolTaskSchedulerTests extends AbstractSchedulingTaskExecutorTests { + + private final ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + + + @Override + protected AsyncListenableTaskExecutor buildExecutor() { + scheduler.setThreadNamePrefix(this.threadNamePrefix); + scheduler.afterPropertiesSet(); + return scheduler; + } + + + @Test + void executeFailingRunnableWithErrorHandler() { + TestTask task = new TestTask(this.testName, 0); + TestErrorHandler errorHandler = new TestErrorHandler(1); + scheduler.setErrorHandler(errorHandler); + scheduler.execute(task); + await(errorHandler); + assertThat(errorHandler.lastError).isNotNull(); + } + + @Test + void submitFailingRunnableWithErrorHandler() throws Exception { + TestTask task = new TestTask(this.testName, 0); + TestErrorHandler errorHandler = new TestErrorHandler(1); + scheduler.setErrorHandler(errorHandler); + Future future = scheduler.submit(task); + Object result = future.get(1000, TimeUnit.MILLISECONDS); + assertThat(future.isDone()).isTrue(); + assertThat(result).isNull(); + assertThat(errorHandler.lastError).isNotNull(); + } + + @Test + void submitFailingCallableWithErrorHandler() throws Exception { + TestCallable task = new TestCallable(this.testName, 0); + TestErrorHandler errorHandler = new TestErrorHandler(1); + scheduler.setErrorHandler(errorHandler); + Future future = scheduler.submit(task); + Object result = future.get(1000, TimeUnit.MILLISECONDS); + assertThat(future.isDone()).isTrue(); + assertThat(result).isNull(); + assertThat(errorHandler.lastError).isNotNull(); + } + + @Test + void scheduleOneTimeTask() throws Exception { + TestTask task = new TestTask(this.testName, 1); + Future future = scheduler.schedule(task, new Date()); + Object result = future.get(1000, TimeUnit.MILLISECONDS); + assertThat(result).isNull(); + assertThat(future.isDone()).isTrue(); + assertThreadNamePrefix(task); + } + + @Test + void scheduleOneTimeFailingTaskWithoutErrorHandler() throws Exception { + TestTask task = new TestTask(this.testName, 0); + Future future = scheduler.schedule(task, new Date()); + assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> future.get(1000, TimeUnit.MILLISECONDS)); + assertThat(future.isDone()).isTrue(); + } + + @Test + void scheduleOneTimeFailingTaskWithErrorHandler() throws Exception { + TestTask task = new TestTask(this.testName, 0); + TestErrorHandler errorHandler = new TestErrorHandler(1); + scheduler.setErrorHandler(errorHandler); + Future future = scheduler.schedule(task, new Date()); + Object result = future.get(1000, TimeUnit.MILLISECONDS); + assertThat(future.isDone()).isTrue(); + assertThat(result).isNull(); + assertThat(errorHandler.lastError).isNotNull(); + } + + @Test + void scheduleTriggerTask() throws Exception { + TestTask task = new TestTask(this.testName, 3); + Future future = scheduler.schedule(task, new TestTrigger(3)); + Object result = future.get(1000, TimeUnit.MILLISECONDS); + assertThat(result).isNull(); + await(task); + assertThreadNamePrefix(task); + } + + @Test + void scheduleMultipleTriggerTasks() throws Exception { + for (int i = 0; i < 100; i++) { + scheduleTriggerTask(); + } + } + + + private void await(TestTask task) { + await(task.latch); + } + + private void await(TestErrorHandler errorHandler) { + await(errorHandler.latch); + } + + private void await(CountDownLatch latch) { + try { + latch.await(1000, TimeUnit.MILLISECONDS); + } + catch (InterruptedException ex) { + throw new IllegalStateException(ex); + } + assertThat(latch.getCount()).as("latch did not count down,").isEqualTo(0); + } + + + private static class TestErrorHandler implements ErrorHandler { + + private final CountDownLatch latch; + + private volatile Throwable lastError; + + TestErrorHandler(int expectedErrorCount) { + this.latch = new CountDownLatch(expectedErrorCount); + } + + @Override + public void handleError(Throwable t) { + this.lastError = t; + this.latch.countDown(); + } + } + + + private static class TestTrigger implements Trigger { + + private final int maxRunCount; + + private final AtomicInteger actualRunCount = new AtomicInteger(); + + TestTrigger(int maxRunCount) { + this.maxRunCount = maxRunCount; + } + + @Override + public Date nextExecutionTime(TriggerContext triggerContext) { + if (this.actualRunCount.incrementAndGet() > this.maxRunCount) { + return null; + } + return new Date(); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/config/AnnotationDrivenBeanDefinitionParserTests.java b/spring-context/src/test/java/org/springframework/scheduling/config/AnnotationDrivenBeanDefinitionParserTests.java new file mode 100644 index 0000000..cd7e0d8 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/config/AnnotationDrivenBeanDefinitionParserTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.config; + +import java.util.function.Supplier; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Mark Fisher + * @author Stephane Nicoll + */ +public class AnnotationDrivenBeanDefinitionParserTests { + + private ConfigurableApplicationContext context = new ClassPathXmlApplicationContext( + "annotationDrivenContext.xml", AnnotationDrivenBeanDefinitionParserTests.class); + + + @AfterEach + public void closeApplicationContext() { + context.close(); + } + + + @Test + public void asyncPostProcessorRegistered() { + assertThat(context.containsBean(TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue(); + } + + @Test + public void scheduledPostProcessorRegistered() { + assertThat(context.containsBean(TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue(); + } + + @Test + public void asyncPostProcessorExecutorReference() { + Object executor = context.getBean("testExecutor"); + Object postProcessor = context.getBean(TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME); + assertThat(((Supplier) new DirectFieldAccessor(postProcessor).getPropertyValue("executor")).get()).isSameAs(executor); + } + + @Test + public void scheduledPostProcessorSchedulerReference() { + Object scheduler = context.getBean("testScheduler"); + Object postProcessor = context.getBean(TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME); + assertThat(new DirectFieldAccessor(postProcessor).getPropertyValue("scheduler")).isSameAs(scheduler); + } + + @Test + public void asyncPostProcessorExceptionHandlerReference() { + Object exceptionHandler = context.getBean("testExceptionHandler"); + Object postProcessor = context.getBean(TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME); + assertThat(((Supplier) new DirectFieldAccessor(postProcessor).getPropertyValue("exceptionHandler")).get()).isSameAs(exceptionHandler); + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/config/ExecutorBeanDefinitionParserTests.java b/spring-context/src/test/java/org/springframework/scheduling/config/ExecutorBeanDefinitionParserTests.java new file mode 100644 index 0000000..a52f768 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/config/ExecutorBeanDefinitionParserTests.java @@ -0,0 +1,170 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.config; + +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; +import java.util.concurrent.FutureTask; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.core.task.TaskExecutor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.util.CustomizableThreadCreator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Mark Fisher + * @author Juergen Hoeller + */ +public class ExecutorBeanDefinitionParserTests { + + private ApplicationContext context; + + + @BeforeEach + public void setup() { + this.context = new ClassPathXmlApplicationContext( + "executorContext.xml", ExecutorBeanDefinitionParserTests.class); + } + + + @Test + public void defaultExecutor() throws Exception { + ThreadPoolTaskExecutor executor = this.context.getBean("default", ThreadPoolTaskExecutor.class); + assertThat(getCorePoolSize(executor)).isEqualTo(1); + assertThat(getMaxPoolSize(executor)).isEqualTo(Integer.MAX_VALUE); + assertThat(getQueueCapacity(executor)).isEqualTo(Integer.MAX_VALUE); + assertThat(getKeepAliveSeconds(executor)).isEqualTo(60); + assertThat(getAllowCoreThreadTimeOut(executor)).isEqualTo(false); + + FutureTask task = new FutureTask<>(new Callable() { + @Override + public String call() throws Exception { + return "foo"; + } + }); + executor.execute(task); + assertThat(task.get()).isEqualTo("foo"); + } + + @Test + public void singleSize() { + Object executor = this.context.getBean("singleSize"); + assertThat(getCorePoolSize(executor)).isEqualTo(42); + assertThat(getMaxPoolSize(executor)).isEqualTo(42); + } + + @Test + public void invalidPoolSize() { + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + this.context.getBean("invalidPoolSize")); + } + + @Test + public void rangeWithBoundedQueue() { + Object executor = this.context.getBean("rangeWithBoundedQueue"); + assertThat(getCorePoolSize(executor)).isEqualTo(7); + assertThat(getMaxPoolSize(executor)).isEqualTo(42); + assertThat(getQueueCapacity(executor)).isEqualTo(11); + } + + @Test + public void rangeWithUnboundedQueue() { + Object executor = this.context.getBean("rangeWithUnboundedQueue"); + assertThat(getCorePoolSize(executor)).isEqualTo(9); + assertThat(getMaxPoolSize(executor)).isEqualTo(9); + assertThat(getKeepAliveSeconds(executor)).isEqualTo(37); + assertThat(getAllowCoreThreadTimeOut(executor)).isEqualTo(true); + assertThat(getQueueCapacity(executor)).isEqualTo(Integer.MAX_VALUE); + } + + @Test + public void propertyPlaceholderWithSingleSize() { + Object executor = this.context.getBean("propertyPlaceholderWithSingleSize"); + assertThat(getCorePoolSize(executor)).isEqualTo(123); + assertThat(getMaxPoolSize(executor)).isEqualTo(123); + assertThat(getKeepAliveSeconds(executor)).isEqualTo(60); + assertThat(getAllowCoreThreadTimeOut(executor)).isEqualTo(false); + assertThat(getQueueCapacity(executor)).isEqualTo(Integer.MAX_VALUE); + } + + @Test + public void propertyPlaceholderWithRange() { + Object executor = this.context.getBean("propertyPlaceholderWithRange"); + assertThat(getCorePoolSize(executor)).isEqualTo(5); + assertThat(getMaxPoolSize(executor)).isEqualTo(25); + assertThat(getAllowCoreThreadTimeOut(executor)).isEqualTo(false); + assertThat(getQueueCapacity(executor)).isEqualTo(10); + } + + @Test + public void propertyPlaceholderWithRangeAndCoreThreadTimeout() { + Object executor = this.context.getBean("propertyPlaceholderWithRangeAndCoreThreadTimeout"); + assertThat(getCorePoolSize(executor)).isEqualTo(99); + assertThat(getMaxPoolSize(executor)).isEqualTo(99); + assertThat(getAllowCoreThreadTimeOut(executor)).isEqualTo(true); + } + + @Test + public void propertyPlaceholderWithInvalidPoolSize() { + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> + this.context.getBean("propertyPlaceholderWithInvalidPoolSize")); + } + + @Test + public void threadNamePrefix() { + CustomizableThreadCreator executor = this.context.getBean("default", CustomizableThreadCreator.class); + assertThat(executor.getThreadNamePrefix()).isEqualTo("default-"); + } + + @Test + public void typeCheck() { + assertThat(this.context.isTypeMatch("default", Executor.class)).isTrue(); + assertThat(this.context.isTypeMatch("default", TaskExecutor.class)).isTrue(); + assertThat(this.context.isTypeMatch("default", ThreadPoolTaskExecutor.class)).isTrue(); + } + + + private int getCorePoolSize(Object executor) { + return (Integer) new DirectFieldAccessor(executor).getPropertyValue("corePoolSize"); + } + + private int getMaxPoolSize(Object executor) { + return (Integer) new DirectFieldAccessor(executor).getPropertyValue("maxPoolSize"); + } + + private int getQueueCapacity(Object executor) { + return (Integer) new DirectFieldAccessor(executor).getPropertyValue("queueCapacity"); + } + + private int getKeepAliveSeconds(Object executor) { + return (Integer) new DirectFieldAccessor(executor).getPropertyValue("keepAliveSeconds"); + } + + private boolean getAllowCoreThreadTimeOut(Object executor) { + return (Boolean) new DirectFieldAccessor(executor).getPropertyValue("allowCoreThreadTimeOut"); + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/config/LazyScheduledTasksBeanDefinitionParserTests.java b/spring-context/src/test/java/org/springframework/scheduling/config/LazyScheduledTasksBeanDefinitionParserTests.java new file mode 100644 index 0000000..4a7f9d7 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/config/LazyScheduledTasksBeanDefinitionParserTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.config; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.GenericXmlApplicationContext; + +/** + * Tests ensuring that tasks scheduled using the <task:scheduled> element + * are never marked lazy, even if the enclosing <beans> element declares + * default-lazy-init="true". See SPR-8498 + * + * @author Mike Youngstrom + * @author Chris Beams + * @author Sam Brannen + */ +class LazyScheduledTasksBeanDefinitionParserTests { + + @Test + @Timeout(5) + void checkTarget() { + try (ConfigurableApplicationContext applicationContext = + new GenericXmlApplicationContext(getClass(), "lazyScheduledTasksContext.xml")) { + + Task task = applicationContext.getBean(Task.class); + + while (!task.executed) { + try { + Thread.sleep(10); + } + catch (Exception ex) { + /* Do Nothing */ + } + } + } + } + + + static class Task { + + volatile boolean executed = false; + + public void doWork() { + executed = true; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/config/ScheduledTaskRegistrarTests.java b/spring-context/src/test/java/org/springframework/scheduling/config/ScheduledTaskRegistrarTests.java new file mode 100644 index 0000000..9b9828c --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/config/ScheduledTaskRegistrarTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.config; + +import java.util.Collections; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link ScheduledTaskRegistrar}. + * + * @author Tobias Montagna-Hay + * @author Juergen Hoeller + * @author Sam Brannen + * @since 4.2 + */ +class ScheduledTaskRegistrarTests { + + private static final Runnable no_op = () -> {}; + + private final ScheduledTaskRegistrar taskRegistrar = new ScheduledTaskRegistrar(); + + + @BeforeEach + void preconditions() { + assertThat(this.taskRegistrar.getTriggerTaskList()).isEmpty(); + assertThat(this.taskRegistrar.getCronTaskList()).isEmpty(); + assertThat(this.taskRegistrar.getFixedRateTaskList()).isEmpty(); + assertThat(this.taskRegistrar.getFixedDelayTaskList()).isEmpty(); + } + + @Test + void getTriggerTasks() { + TriggerTask mockTriggerTask = mock(TriggerTask.class); + this.taskRegistrar.setTriggerTasksList(Collections.singletonList(mockTriggerTask)); + assertThat(this.taskRegistrar.getTriggerTaskList()).containsExactly(mockTriggerTask); + } + + @Test + void getCronTasks() { + CronTask mockCronTask = mock(CronTask.class); + this.taskRegistrar.setCronTasksList(Collections.singletonList(mockCronTask)); + assertThat(this.taskRegistrar.getCronTaskList()).containsExactly(mockCronTask); + } + + @Test + void getFixedRateTasks() { + IntervalTask mockFixedRateTask = mock(IntervalTask.class); + this.taskRegistrar.setFixedRateTasksList(Collections.singletonList(mockFixedRateTask)); + assertThat(this.taskRegistrar.getFixedRateTaskList()).containsExactly(mockFixedRateTask); + } + + @Test + void getFixedDelayTasks() { + IntervalTask mockFixedDelayTask = mock(IntervalTask.class); + this.taskRegistrar.setFixedDelayTasksList(Collections.singletonList(mockFixedDelayTask)); + assertThat(this.taskRegistrar.getFixedDelayTaskList()).containsExactly(mockFixedDelayTask); + } + + @Test + void addCronTaskWithValidExpression() { + this.taskRegistrar.addCronTask(no_op, "* * * * * ?"); + assertThat(this.taskRegistrar.getCronTaskList()).hasSize(1); + } + + @Test + void addCronTaskWithInvalidExpression() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.taskRegistrar.addCronTask(no_op, "* * *")) + .withMessage("Cron expression must consist of 6 fields (found 3 in \"* * *\")"); + } + + @Test + void addCronTaskWithDisabledExpression() { + this.taskRegistrar.addCronTask(no_op, ScheduledTaskRegistrar.CRON_DISABLED); + assertThat(this.taskRegistrar.getCronTaskList()).isEmpty(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/config/ScheduledTasksBeanDefinitionParserTests.java b/spring-context/src/test/java/org/springframework/scheduling/config/ScheduledTasksBeanDefinitionParserTests.java new file mode 100644 index 0000000..16879f1 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/config/ScheduledTasksBeanDefinitionParserTests.java @@ -0,0 +1,130 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.config; + +import java.lang.reflect.Method; +import java.util.Date; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.scheduling.Trigger; +import org.springframework.scheduling.TriggerContext; +import org.springframework.scheduling.support.ScheduledMethodRunnable; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Mark Fisher + * @author Chris Beams + */ +@SuppressWarnings("unchecked") +public class ScheduledTasksBeanDefinitionParserTests { + + private ApplicationContext context; + + private ScheduledTaskRegistrar registrar; + + private Object testBean; + + + @BeforeEach + public void setup() { + this.context = new ClassPathXmlApplicationContext( + "scheduledTasksContext.xml", ScheduledTasksBeanDefinitionParserTests.class); + this.registrar = this.context.getBeansOfType( + ScheduledTaskRegistrar.class).values().iterator().next(); + this.testBean = this.context.getBean("testBean"); + } + + @Test + public void checkScheduler() { + Object schedulerBean = this.context.getBean("testScheduler"); + Object schedulerRef = new DirectFieldAccessor(this.registrar).getPropertyValue("taskScheduler"); + assertThat(schedulerRef).isEqualTo(schedulerBean); + } + + @Test + public void checkTarget() { + List tasks = (List) new DirectFieldAccessor( + this.registrar).getPropertyValue("fixedRateTasks"); + Runnable runnable = tasks.get(0).getRunnable(); + assertThat(runnable.getClass()).isEqualTo(ScheduledMethodRunnable.class); + Object targetObject = ((ScheduledMethodRunnable) runnable).getTarget(); + Method targetMethod = ((ScheduledMethodRunnable) runnable).getMethod(); + assertThat(targetObject).isEqualTo(this.testBean); + assertThat(targetMethod.getName()).isEqualTo("test"); + } + + @Test + public void fixedRateTasks() { + List tasks = (List) new DirectFieldAccessor( + this.registrar).getPropertyValue("fixedRateTasks"); + assertThat(tasks.size()).isEqualTo(3); + assertThat(tasks.get(0).getInterval()).isEqualTo(1000L); + assertThat(tasks.get(1).getInterval()).isEqualTo(2000L); + assertThat(tasks.get(2).getInterval()).isEqualTo(4000L); + assertThat(tasks.get(2).getInitialDelay()).isEqualTo(500); + } + + @Test + public void fixedDelayTasks() { + List tasks = (List) new DirectFieldAccessor( + this.registrar).getPropertyValue("fixedDelayTasks"); + assertThat(tasks.size()).isEqualTo(2); + assertThat(tasks.get(0).getInterval()).isEqualTo(3000L); + assertThat(tasks.get(1).getInterval()).isEqualTo(3500L); + assertThat(tasks.get(1).getInitialDelay()).isEqualTo(250); + } + + @Test + public void cronTasks() { + List tasks = (List) new DirectFieldAccessor( + this.registrar).getPropertyValue("cronTasks"); + assertThat(tasks.size()).isEqualTo(1); + assertThat(tasks.get(0).getExpression()).isEqualTo("*/4 * 9-17 * * MON-FRI"); + } + + @Test + public void triggerTasks() { + List tasks = (List) new DirectFieldAccessor( + this.registrar).getPropertyValue("triggerTasks"); + assertThat(tasks.size()).isEqualTo(1); + assertThat(tasks.get(0).getTrigger()).isInstanceOf(TestTrigger.class); + } + + + static class TestBean { + + public void test() { + } + } + + + static class TestTrigger implements Trigger { + + @Override + public Date nextExecutionTime(TriggerContext triggerContext) { + return null; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/config/SchedulerBeanDefinitionParserTests.java b/spring-context/src/test/java/org/springframework/scheduling/config/SchedulerBeanDefinitionParserTests.java new file mode 100644 index 0000000..6bf38e4 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/config/SchedulerBeanDefinitionParserTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.config; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Mark Fisher + */ +public class SchedulerBeanDefinitionParserTests { + + private ApplicationContext context; + + + @BeforeEach + public void setup() { + this.context = new ClassPathXmlApplicationContext( + "schedulerContext.xml", SchedulerBeanDefinitionParserTests.class); + } + + @Test + public void defaultScheduler() { + ThreadPoolTaskScheduler scheduler = (ThreadPoolTaskScheduler) this.context.getBean("defaultScheduler"); + Integer size = (Integer) new DirectFieldAccessor(scheduler).getPropertyValue("poolSize"); + assertThat(size).isEqualTo(1); + } + + @Test + public void customScheduler() { + ThreadPoolTaskScheduler scheduler = (ThreadPoolTaskScheduler) this.context.getBean("customScheduler"); + Integer size = (Integer) new DirectFieldAccessor(scheduler).getPropertyValue("poolSize"); + assertThat(size).isEqualTo(42); + } + + @Test + public void threadNamePrefix() { + ThreadPoolTaskScheduler scheduler = (ThreadPoolTaskScheduler) this.context.getBean("customScheduler"); + assertThat(scheduler.getThreadNamePrefix()).isEqualTo("customScheduler-"); + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/BitsCronFieldTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/BitsCronFieldTests.java new file mode 100644 index 0000000..e05fa73 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/support/BitsCronFieldTests.java @@ -0,0 +1,135 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.support; + +import java.util.Arrays; + +import org.assertj.core.api.Condition; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Arjen Poutsma + */ +public class BitsCronFieldTests { + + @Test + void parse() { + assertThat(BitsCronField.parseSeconds("42")).has(clearRange(0, 41)).has(set(42)).has(clearRange(43, 59)); + assertThat(BitsCronField.parseMinutes("1,2,5,9")).has(clear(0)).has(set(1, 2)).has(clearRange(3,4)).has(set(5)).has(clearRange(6,8)).has(set(9)).has(clearRange(10,59)); + assertThat(BitsCronField.parseSeconds("0-4,8-12")).has(setRange(0, 4)).has(clearRange(5,7)).has(setRange(8, 12)).has(clearRange(13,59)); + assertThat(BitsCronField.parseHours("0-23/2")).has(set(0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22)).has(clear(1,3,5,7,9,11,13,15,17,19,21,23)); + assertThat(BitsCronField.parseDaysOfWeek("0")).has(clearRange(0, 6)).has(set(7, 7)); + assertThat(BitsCronField.parseSeconds("57/2")).has(clearRange(0, 56)).has(set(57)).has(clear(58)).has(set(59)); + } + + @Test + void invalidRange() { + assertThatIllegalArgumentException().isThrownBy(() -> BitsCronField.parseSeconds("")); + assertThatIllegalArgumentException().isThrownBy(() -> BitsCronField.parseSeconds("0-12/0")); + assertThatIllegalArgumentException().isThrownBy(() -> BitsCronField.parseSeconds("60")); + assertThatIllegalArgumentException().isThrownBy(() -> BitsCronField.parseMinutes("60")); + assertThatIllegalArgumentException().isThrownBy(() -> BitsCronField.parseDaysOfMonth("0")); + assertThatIllegalArgumentException().isThrownBy(() -> BitsCronField.parseDaysOfMonth("32")); + assertThatIllegalArgumentException().isThrownBy(() -> BitsCronField.parseMonth("0")); + assertThatIllegalArgumentException().isThrownBy(() -> BitsCronField.parseMonth("13")); + assertThatIllegalArgumentException().isThrownBy(() -> BitsCronField.parseDaysOfWeek("8")); + assertThatIllegalArgumentException().isThrownBy(() -> BitsCronField.parseSeconds("20-10")); + assertThatIllegalArgumentException().isThrownBy(() -> BitsCronField.parseDaysOfWeek("*SUN")); + assertThatIllegalArgumentException().isThrownBy(() -> BitsCronField.parseDaysOfWeek("SUN*")); + assertThatIllegalArgumentException().isThrownBy(() -> BitsCronField.parseHours("*ANYTHING_HERE")); + } + + @Test + void parseWildcards() { + assertThat(BitsCronField.parseSeconds("*")).has(setRange(0, 60)); + assertThat(BitsCronField.parseMinutes("*")).has(setRange(0, 60)); + assertThat(BitsCronField.parseHours("*")).has(setRange(0, 23)); + assertThat(BitsCronField.parseDaysOfMonth("*")).has(clear(0)).has(setRange(1, 31)); + assertThat(BitsCronField.parseDaysOfMonth("?")).has(clear(0)).has(setRange(1, 31)); + assertThat(BitsCronField.parseMonth("*")).has(clear(0)).has(setRange(1, 12)); + assertThat(BitsCronField.parseDaysOfWeek("*")).has(clear(0)).has(setRange(1, 7)); + assertThat(BitsCronField.parseDaysOfWeek("?")).has(clear(0)).has(setRange(1, 7)); + } + + @Test + void names() { + assertThat(((BitsCronField)CronField.parseMonth("JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC"))) + .has(clear(0)).has(setRange(1, 12)); + assertThat(((BitsCronField)CronField.parseDaysOfWeek("SUN,MON,TUE,WED,THU,FRI,SAT"))) + .has(clear(0)).has(setRange(1, 7)); + } + + private static Condition set(int... indices) { + return new Condition(String.format("set bits %s", Arrays.toString(indices))) { + @Override + public boolean matches(BitsCronField value) { + for (int index : indices) { + if (!value.getBit(index)) { + return false; + } + } + return true; + } + }; + } + + private static Condition setRange(int min, int max) { + return new Condition(String.format("set range %d-%d", min, max)) { + @Override + public boolean matches(BitsCronField value) { + for (int i = min; i < max; i++) { + if (!value.getBit(i)) { + return false; + } + } + return true; + } + }; + } + + private static Condition clear(int... indices) { + return new Condition(String.format("clear bits %s", Arrays.toString(indices))) { + @Override + public boolean matches(BitsCronField value) { + for (int index : indices) { + if (value.getBit(index)) { + return false; + } + } + return true; + } + }; + } + + private static Condition clearRange(int min, int max) { + return new Condition(String.format("clear range %d-%d", min, max)) { + @Override + public boolean matches(BitsCronField value) { + for (int i = min; i < max; i++) { + if (value.getBit(i)) { + return false; + } + } + return true; + } + }; + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java new file mode 100644 index 0000000..9d25010 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java @@ -0,0 +1,1045 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.support; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Year; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoField; +import java.time.temporal.Temporal; + +import org.assertj.core.api.Condition; +import org.junit.jupiter.api.Test; + +import static java.time.DayOfWeek.FRIDAY; +import static java.time.DayOfWeek.MONDAY; +import static java.time.DayOfWeek.SUNDAY; +import static java.time.DayOfWeek.TUESDAY; +import static java.time.DayOfWeek.WEDNESDAY; +import static java.time.temporal.TemporalAdjusters.next; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + */ +class CronExpressionTests { + + private static final Condition weekday = new Condition("weekday") { + + @Override + public boolean matches(Temporal value) { + int dayOfWeek = value.get(ChronoField.DAY_OF_WEEK); + return dayOfWeek != 6 && dayOfWeek != 7; + } + }; + + + @Test + void matchAll() { + CronExpression expression = CronExpression.parse("* * * * * *"); + + LocalDateTime last = LocalDateTime.now(); + LocalDateTime expected = last.plusSeconds(1).withNano(0); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void matchLastSecond() { + CronExpression expression = CronExpression.parse("* * * * * *"); + + LocalDateTime last = LocalDateTime.now().withSecond(58); + LocalDateTime expected = last.plusSeconds(1).withNano(0); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void matchSpecificSecond() { + CronExpression expression = CronExpression.parse("10 * * * * *"); + + LocalDateTime now = LocalDateTime.now(); + LocalDateTime last = now.withSecond(9); + LocalDateTime expected = last.withSecond(10).withNano(0); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void incrementSecondByOne() { + CronExpression expression = CronExpression.parse("11 * * * * *"); + + LocalDateTime last = LocalDateTime.now().withSecond(10); + LocalDateTime expected = last.plusSeconds(1).withNano(0); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void incrementSecondAndRollover() { + CronExpression expression = CronExpression.parse("10 * * * * *"); + + LocalDateTime last = LocalDateTime.now().withSecond(11); + LocalDateTime expected = last.plusMinutes(1).withSecond(10).withNano(0); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void secondRange() { + CronExpression expression = CronExpression.parse("10-15 * * * * *"); + LocalDateTime now = LocalDateTime.now(); + + for (int i = 9; i < 15; i++) { + LocalDateTime last = now.withSecond(i); + LocalDateTime expected = last.plusSeconds(1).withNano(0); + assertThat(expression.next(last)).isEqualTo(expected); + } + } + + @Test + void incrementMinute() { + CronExpression expression = CronExpression.parse("0 * * * * *"); + + LocalDateTime last = LocalDateTime.now().withMinute(10); + LocalDateTime expected = last.plusMinutes(1).withSecond(0).withNano(0); + LocalDateTime actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusMinutes(1); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void incrementMinuteByOne() { + CronExpression expression = CronExpression.parse("0 11 * * * *"); + + LocalDateTime last = LocalDateTime.now().withMinute(10); + LocalDateTime expected = last.plusMinutes(1).withSecond(0).withNano(0); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void incrementMinuteAndRollover() { + CronExpression expression = CronExpression.parse("0 10 * * * *"); + + LocalDateTime last = LocalDateTime.now().withMinute(11).withSecond(0); + LocalDateTime expected = last.plusMinutes(59).withNano(0); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void incrementHour() { + CronExpression expression = CronExpression.parse("0 0 * * * *"); + + int year = Year.now().getValue(); + LocalDateTime last = LocalDateTime.of(year, 10, 30, 11, 1); + LocalDateTime expected = last.withHour(12).withMinute(0); + LocalDateTime actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.withHour(13); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void incrementHourAndRollover() { + CronExpression expression = CronExpression.parse("0 0 * * * *"); + + int year = Year.now().getValue(); + LocalDateTime last = LocalDateTime.of(year, 9, 10, 23, 1); + LocalDateTime expected = last.withDayOfMonth(11).withHour(0).withMinute(0); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void incrementDayOfMonth() { + CronExpression expression = CronExpression.parse("0 0 0 * * *"); + + LocalDateTime last = LocalDateTime.now().withDayOfMonth(1); + LocalDateTime expected = last.plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0); + LocalDateTime actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusDays(1); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void incrementDayOfMonthByOne() { + CronExpression expression = CronExpression.parse("* * * 10 * *"); + + LocalDateTime last = LocalDateTime.now().withDayOfMonth(9); + LocalDateTime expected = last.plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void incrementDayOfMonthAndRollover() { + CronExpression expression = CronExpression.parse("* * * 10 * *"); + + LocalDateTime last = LocalDateTime.now().withDayOfMonth(11); + LocalDateTime expected = + last.plusMonths(1).withDayOfMonth(10).withHour(0).withMinute(0).withSecond(0).withNano(0); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void dailyTriggerInShortMonth() { + CronExpression expression = CronExpression.parse("0 0 0 * * *"); + + // September: 30 days + LocalDateTime last = LocalDateTime.now().withMonth(9).withDayOfMonth(30); + LocalDateTime expected = LocalDateTime.of(last.getYear(), 10, 1, 0, 0); + LocalDateTime actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.withDayOfMonth(2); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void dailyTriggerInLongMonth() { + CronExpression expression = CronExpression.parse("0 0 0 * * *"); + + // August: 31 days and not a daylight saving boundary + LocalDateTime last = LocalDateTime.now().withMonth(8).withDayOfMonth(30); + LocalDateTime expected = last.plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0); + LocalDateTime actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusDays(1); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void dailyTriggerOnDaylightSavingBoundary() { + CronExpression expression = CronExpression.parse("0 0 0 * * *"); + + // October: 31 days and a daylight saving boundary in CET + ZonedDateTime last = ZonedDateTime.now(ZoneId.of("CET")).withMonth(10).withDayOfMonth(30); + ZonedDateTime expected = last.withDayOfMonth(31).withHour(0).withMinute(0).withSecond(0).withNano(0); + ZonedDateTime actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.withMonth(11).withDayOfMonth(1); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void incrementMonth() { + CronExpression expression = CronExpression.parse("0 0 0 1 * *"); + + LocalDateTime last = LocalDateTime.now().withMonth(10).withDayOfMonth(30); + LocalDateTime expected = LocalDateTime.of(last.getYear(), 11, 1, 0, 0); + LocalDateTime actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.withMonth(12); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void incrementMonthAndRollover() { + CronExpression expression = CronExpression.parse("0 0 0 1 * *"); + + LocalDateTime last = LocalDateTime.now().withYear(2010).withMonth(12).withDayOfMonth(31); + LocalDateTime expected = LocalDateTime.of(2011, 1, 1, 0, 0); + LocalDateTime actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusMonths(1); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void monthlyTriggerInLongMonth() { + CronExpression expression = CronExpression.parse("0 0 0 31 * *"); + + LocalDateTime last = LocalDateTime.now().withMonth(10).withDayOfMonth(30); + LocalDateTime expected = last.withDayOfMonth(31).withHour(0).withMinute(0).withSecond(0).withNano(0); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void monthlyTriggerInShortMonth() { + CronExpression expression = CronExpression.parse("0 0 0 1 * *"); + + LocalDateTime last = LocalDateTime.now().withMonth(10).withDayOfMonth(30); + LocalDateTime expected = LocalDateTime.of(last.getYear(), 11, 1, 0, 0); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void incrementDayOfWeekByOne() { + CronExpression expression = CronExpression.parse("* * * * * 2"); + + LocalDateTime last = LocalDateTime.now().with(next(MONDAY)); + LocalDateTime expected = last.plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0); + LocalDateTime actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + assertThat(actual.getDayOfWeek()).isEqualTo(TUESDAY); + } + + @Test + void incrementDayOfWeekAndRollover() { + CronExpression expression = CronExpression.parse("* * * * * 2"); + + LocalDateTime last = LocalDateTime.now().with(next(WEDNESDAY)); + LocalDateTime expected = last.plusDays(6).withHour(0).withMinute(0).withSecond(0).withNano(0); + LocalDateTime actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + assertThat(actual.getDayOfWeek()).isEqualTo(TUESDAY); + } + + @Test + void specificMinuteSecond() { + CronExpression expression = CronExpression.parse("55 5 * * * *"); + + LocalDateTime last = LocalDateTime.now().withMinute(4).withSecond(54); + LocalDateTime expected = last.plusMinutes(1).withSecond(55).withNano(0); + LocalDateTime actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusHours(1); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void specificHourSecond() { + CronExpression expression = CronExpression.parse("55 * 10 * * *"); + + LocalDateTime last = LocalDateTime.now().withHour(9).withSecond(54); + LocalDateTime expected = last.plusHours(1).withMinute(0).withSecond(55).withNano(0); + LocalDateTime actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusMinutes(1); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void specificMinuteHour() { + CronExpression expression = CronExpression.parse("* 5 10 * * *"); + + LocalDateTime last = LocalDateTime.now().withHour(9).withMinute(4); + LocalDateTime expected = last.plusHours(1).plusMinutes(1).withSecond(0).withNano(0); + LocalDateTime actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + // next trigger is in one second because second is wildcard + expected = expected.plusSeconds(1); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void specificDayOfMonthSecond() { + CronExpression expression = CronExpression.parse("55 * * 3 * *"); + + LocalDateTime last = LocalDateTime.now().withDayOfMonth(2).withSecond(54); + LocalDateTime expected = last.plusDays(1).withHour(0).withMinute(0).withSecond(55).withNano(0); + LocalDateTime actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusMinutes(1); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void specificDate() { + CronExpression expression = CronExpression.parse("* * * 3 11 *"); + + LocalDateTime last = LocalDateTime.now().withMonth(10).withDayOfMonth(2); + LocalDateTime expected = LocalDateTime.of(last.getYear(), 11, 3, 0, 0); + LocalDateTime actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusSeconds(1); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void nonExistentSpecificDate() { + CronExpression expression = CronExpression.parse("0 0 0 31 6 *"); + + LocalDateTime last = LocalDateTime.now().withMonth(3).withDayOfMonth(10); + assertThat(expression.next(last)).isNull(); + } + + @Test + void leapYearSpecificDate() { + CronExpression expression = CronExpression.parse("0 0 0 29 2 *"); + + LocalDateTime last = LocalDateTime.now().withYear(2007).withMonth(2).withDayOfMonth(10); + LocalDateTime expected = LocalDateTime.of(2008, 2, 29, 0, 0); + LocalDateTime actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusYears(4); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void weekDaySequence() { + CronExpression expression = CronExpression.parse("0 0 7 ? * MON-FRI"); + + // This is a Saturday + LocalDateTime last = LocalDateTime.of(LocalDate.of(2009, 9, 26), LocalTime.now()); + LocalDateTime expected = last.plusDays(2).withHour(7).withMinute(0).withSecond(0).withNano(0); + LocalDateTime actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + // Next day is a week day so add one + last = actual; + expected = expected.plusDays(1); + actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusDays(1); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void monthSequence() { + CronExpression expression = CronExpression.parse("0 30 23 30 1/3 ?"); + + LocalDateTime last = LocalDateTime.of(LocalDate.of(2010, 12, 30), LocalTime.now()); + LocalDateTime expected = last.plusMonths(1).withHour(23).withMinute(30).withSecond(0).withSecond(0).withNano(0); + LocalDateTime actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + // Next trigger is 3 months later + last = actual; + expected = expected.plusMonths(3); + actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusMonths(3); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + public void fixedDays() { + CronExpression expression = CronExpression.parse("0 0 0 29 2 WED"); + + LocalDateTime last = LocalDateTime.of(2012, 2, 29, 1, 0); + assertThat(last.getDayOfWeek()).isEqualTo(WEDNESDAY); + + LocalDateTime actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual.getDayOfMonth()).isEqualTo(29); + assertThat(actual.getDayOfWeek()).isEqualTo(WEDNESDAY); + } + + @Test + void friday13th() { + CronExpression expression = CronExpression.parse("0 0 0 13 * FRI"); + + LocalDateTime last = LocalDateTime.of(2018, 7, 31, 11, 47, 14); + LocalDateTime actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY); + assertThat(actual.getDayOfMonth()).isEqualTo(13); + + last = actual; + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY); + assertThat(actual.getDayOfMonth()).isEqualTo(13); + } + + @Test + void yearly() { + CronExpression expression = CronExpression.parse("@yearly"); + assertThat(expression).isEqualTo(CronExpression.parse("0 0 0 1 1 *")); + + LocalDateTime last = LocalDateTime.now().withMonth(10).withDayOfMonth(10); + LocalDateTime expected = LocalDateTime.of(last.getYear() + 1, 1, 1, 0, 0); + + LocalDateTime actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusYears(1); + actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusYears(1); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void annually() { + CronExpression expression = CronExpression.parse("@annually"); + assertThat(expression).isEqualTo(CronExpression.parse("0 0 0 1 1 *")); + assertThat(expression).isEqualTo(CronExpression.parse("@yearly")); + } + + @Test + void monthly() { + CronExpression expression = CronExpression.parse("@monthly"); + assertThat(expression).isEqualTo(CronExpression.parse("0 0 0 1 * *")); + + LocalDateTime last = LocalDateTime.now().withMonth(10).withDayOfMonth(10); + LocalDateTime expected = LocalDateTime.of(last.getYear(), 11, 1, 0, 0); + + LocalDateTime actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusMonths(1); + actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusMonths(1); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void weekly() { + CronExpression expression = CronExpression.parse("@weekly"); + assertThat(expression).isEqualTo(CronExpression.parse("0 0 0 * * 0")); + + LocalDateTime last = LocalDateTime.now(); + LocalDateTime expected = last.with(next(SUNDAY)).withHour(0).withMinute(0).withSecond(0).withNano(0); + + LocalDateTime actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusWeeks(1); + actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusWeeks(1); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void daily() { + CronExpression expression = CronExpression.parse("@daily"); + assertThat(expression).isEqualTo(CronExpression.parse("0 0 0 * * *")); + + LocalDateTime last = LocalDateTime.now(); + LocalDateTime expected = last.plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0); + + LocalDateTime actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusDays(1); + actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusDays(1); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void midnight() { + CronExpression expression = CronExpression.parse("@midnight"); + assertThat(expression).isEqualTo(CronExpression.parse("0 0 0 * * *")); + assertThat(expression).isEqualTo(CronExpression.parse("@daily")); + } + + @Test + void hourly() { + CronExpression expression = CronExpression.parse("@hourly"); + assertThat(expression).isEqualTo(CronExpression.parse("0 0 * * * *")); + + LocalDateTime last = LocalDateTime.now(); + LocalDateTime expected = last.plusHours(1).withMinute(0).withSecond(0).withNano(0); + + LocalDateTime actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusHours(1); + actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = expected.plusHours(1); + assertThat(expression.next(last)).isEqualTo(expected); + } + + + @Test + void quartzLastDayOfMonth() { + CronExpression expression = CronExpression.parse("0 0 0 L * *"); + + LocalDateTime last = LocalDateTime.of(LocalDate.of(2008, 1, 4), LocalTime.now()); + LocalDateTime expected = LocalDateTime.of(2008, 1, 31, 0, 0); + LocalDateTime actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = LocalDateTime.of(2008, 2, 29, 0, 0); + actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = LocalDateTime.of(2008, 3, 31, 0, 0); + actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = LocalDateTime.of(2008, 4, 30, 0, 0); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void quartzLastDayOfMonthOffset() { + // L-3 = third-to-last day of the month + CronExpression expression = CronExpression.parse("0 0 0 L-3 * *"); + + LocalDateTime last = LocalDateTime.of(LocalDate.of(2008, 1, 4), LocalTime.now()); + LocalDateTime expected = LocalDateTime.of(2008, 1, 28, 0, 0); + LocalDateTime actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = LocalDateTime.of(2008, 2, 26, 0, 0); + actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = LocalDateTime.of(2008, 3, 28, 0, 0); + actual = expression.next(last); + assertThat(actual).isEqualTo(expected); + + last = actual; + expected = LocalDateTime.of(2008, 4, 27, 0, 0); + assertThat(expression.next(last)).isEqualTo(expected); + } + + @Test + void quartzLastWeekdayOfMonth() { + CronExpression expression = CronExpression.parse("0 0 0 LW * *"); + + LocalDateTime last = LocalDateTime.of(LocalDate.of(2008, 1, 4), LocalTime.now()); + LocalDateTime expected = LocalDateTime.of(2008, 1, 31, 0, 0); + LocalDateTime actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual).is(weekday); + + last = actual; + expected = LocalDateTime.of(2008, 2, 29, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual).is(weekday); + + last = actual; + expected = LocalDateTime.of(2008, 3, 31, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual).is(weekday); + + last = actual; + expected = LocalDateTime.of(2008, 4, 30, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual).is(weekday); + + last = actual; + expected = LocalDateTime.of(2008, 5, 30, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual).is(weekday); + + last = actual; + expected = LocalDateTime.of(2008, 6, 30, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual).is(weekday); + + last = actual; + expected = LocalDateTime.of(2008, 7, 31, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual).is(weekday); + + last = actual; + expected = LocalDateTime.of(2008, 8, 29, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual).is(weekday); + + last = actual; + expected = LocalDateTime.of(2008, 9, 30, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual).is(weekday); + + last = actual; + expected = LocalDateTime.of(2008, 10, 31, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual).is(weekday); + + last = actual; + expected = LocalDateTime.of(2008, 11, 28, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual).is(weekday); + + last = actual; + expected = LocalDateTime.of(2008, 12, 31, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual).is(weekday); + } + + @Test + public void quartzLastDayOfWeekOffset() { + // last Friday (5) of the month + CronExpression expression = CronExpression.parse("0 0 0 * * 5L"); + + LocalDateTime last = LocalDateTime.of(LocalDate.of(2008, 1, 4), LocalTime.now()); + LocalDateTime expected = LocalDateTime.of(2008, 1, 25, 0, 0); + LocalDateTime actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY); + + last = actual; + expected = LocalDateTime.of(2008, 2, 29, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY); + + last = actual; + expected = LocalDateTime.of(2008, 3, 28, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY); + + last = actual; + expected = LocalDateTime.of(2008, 4, 25, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY); + + last = actual; + expected = LocalDateTime.of(2008, 5, 30, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY); + + last = actual; + expected = LocalDateTime.of(2008, 6, 27, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY); + + last = actual; + expected = LocalDateTime.of(2008, 7, 25, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY); + + last = actual; + expected = LocalDateTime.of(2008, 8, 29, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY); + + last = actual; + expected = LocalDateTime.of(2008, 9, 26, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY); + + last = actual; + expected = LocalDateTime.of(2008, 10, 31, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY); + + last = actual; + expected = LocalDateTime.of(2008, 11, 28, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY); + + last = actual; + expected = LocalDateTime.of(2008, 12, 26, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY); + } + + @Test + void quartzWeekdayNearestTo15() { + CronExpression expression = CronExpression.parse("0 0 0 15W * ?"); + + LocalDateTime last = LocalDateTime.of(2020, 1, 1, 0, 0); + LocalDateTime expected = LocalDateTime.of(2020, 1, 15, 0, 0); + LocalDateTime actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual).is(weekday); + + last = actual; + expected = LocalDateTime.of(2020, 2, 14, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual).is(weekday); + + last = actual; + expected = LocalDateTime.of(2020, 3, 16, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual).is(weekday); + + last = actual; + expected = LocalDateTime.of(2020, 4, 15, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual).is(weekday); + } + + @Test + void quartzWeekdayNearestTo1() { + CronExpression expression = CronExpression.parse("0 0 0 1W * ?"); + + LocalDateTime last = LocalDateTime.of(2019, 12, 31, 0, 0); + LocalDateTime expected = LocalDateTime.of(2020, 1, 1, 0, 0); + LocalDateTime actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual).is(weekday); + + last = actual; + expected = LocalDateTime.of(2020, 2, 3, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual).is(weekday); + + last = actual; + expected = LocalDateTime.of(2020, 3, 2, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual).is(weekday); + + last = actual; + expected = LocalDateTime.of(2020, 4, 1, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual).is(weekday); + } + + @Test + void quartzWeekdayNearestTo31() { + CronExpression expression = CronExpression.parse("0 0 0 31W * ?"); + + LocalDateTime last = LocalDateTime.of(2020, 1, 1, 0, 0); + LocalDateTime expected = LocalDateTime.of(2020, 1, 31, 0, 0); + LocalDateTime actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual).is(weekday); + + last = actual; + expected = LocalDateTime.of(2020, 3, 31, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual).is(weekday); + + last = actual; + expected = LocalDateTime.of(2020, 7, 31, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual).is(weekday); + + last = actual; + expected = LocalDateTime.of(2020, 8, 31, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual).is(weekday); + + last = actual; + expected = LocalDateTime.of(2020, 10, 30, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual).is(weekday); + + last = actual; + expected = LocalDateTime.of(2020, 12, 31, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual).is(weekday); + } + + @Test + void quartz2ndFridayOfTheMonth() { + CronExpression expression = CronExpression.parse("0 0 0 ? * 5#2"); + + LocalDateTime last = LocalDateTime.of(2020, 1, 1, 0, 0); + LocalDateTime expected = LocalDateTime.of(2020, 1, 10, 0, 0); + LocalDateTime actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY); + + last = actual; + expected = LocalDateTime.of(2020, 2, 14, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY); + + last = actual; + expected = LocalDateTime.of(2020, 3, 13, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY); + + last = actual; + expected = LocalDateTime.of(2020, 4, 10, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY); + } + + @Test + void quartz2ndFridayOfTheMonthDayName() { + CronExpression expression = CronExpression.parse("0 0 0 ? * FRI#2"); + + LocalDateTime last = LocalDateTime.of(2020, 1, 1, 0, 0); + LocalDateTime expected = LocalDateTime.of(2020, 1, 10, 0, 0); + LocalDateTime actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY); + + last = actual; + expected = LocalDateTime.of(2020, 2, 14, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY); + + last = actual; + expected = LocalDateTime.of(2020, 3, 13, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY); + + last = actual; + expected = LocalDateTime.of(2020, 4, 10, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY); + } + + @Test + void quartzFifthWednesdayOfTheMonth() { + CronExpression expression = CronExpression.parse("0 0 0 ? * 3#5"); + + LocalDateTime last = LocalDateTime.of(2020, 1, 1, 0, 0); + LocalDateTime expected = LocalDateTime.of(2020, 1, 29, 0, 0); + LocalDateTime actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual.getDayOfWeek()).isEqualTo(WEDNESDAY); + + last = actual; + expected = LocalDateTime.of(2020, 4, 29, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual.getDayOfWeek()).isEqualTo(WEDNESDAY); + + last = actual; + expected = LocalDateTime.of(2020, 7, 29, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual.getDayOfWeek()).isEqualTo(WEDNESDAY); + + last = actual; + expected = LocalDateTime.of(2020, 9, 30, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual.getDayOfWeek()).isEqualTo(WEDNESDAY); + + last = actual; + expected = LocalDateTime.of(2020, 12, 30, 0, 0); + actual = expression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); + assertThat(actual.getDayOfWeek()).isEqualTo(WEDNESDAY); + } +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/CronSequenceGeneratorTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/CronSequenceGeneratorTests.java new file mode 100644 index 0000000..fff8b6e --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/support/CronSequenceGeneratorTests.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.support; + +import java.util.Date; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Juergen Hoeller + * @author Ruslan Sibgatullin + */ +@SuppressWarnings("deprecation") +public class CronSequenceGeneratorTests { + + @Test + public void at50Seconds() { + assertThat(new CronSequenceGenerator("*/15 * 1-4 * * *").next(new Date(2012, 6, 1, 9, 53, 50))).isEqualTo(new Date(2012, 6, 2, 1, 0)); + } + + @Test + public void at0Seconds() { + assertThat(new CronSequenceGenerator("*/15 * 1-4 * * *").next(new Date(2012, 6, 1, 9, 53))).isEqualTo(new Date(2012, 6, 2, 1, 0)); + } + + @Test + public void at0Minutes() { + assertThat(new CronSequenceGenerator("0 */2 1-4 * * *").next(new Date(2012, 6, 1, 9, 0))).isEqualTo(new Date(2012, 6, 2, 1, 0)); + } + + @Test + public void with0Increment() { + assertThatIllegalArgumentException().isThrownBy(() -> + new CronSequenceGenerator("*/0 * * * * *").next(new Date(2012, 6, 1, 9, 0))); + } + + @Test + public void withNegativeIncrement() { + assertThatIllegalArgumentException().isThrownBy(() -> + new CronSequenceGenerator("*/-1 * * * * *").next(new Date(2012, 6, 1, 9, 0))); + } + + @Test + public void withInvertedMinuteRange() { + assertThatIllegalArgumentException().isThrownBy(() -> + new CronSequenceGenerator("* 6-5 * * * *").next(new Date(2012, 6, 1, 9, 0))); + } + + @Test + public void withInvertedHourRange() { + assertThatIllegalArgumentException().isThrownBy(() -> + new CronSequenceGenerator("* * 6-5 * * *").next(new Date(2012, 6, 1, 9, 0))); + } + + @Test + public void withSameMinuteRange() { + new CronSequenceGenerator("* 6-6 * * * *").next(new Date(2012, 6, 1, 9, 0)); + } + + @Test + public void withSameHourRange() { + new CronSequenceGenerator("* * 6-6 * * *").next(new Date(2012, 6, 1, 9, 0)); + } + + @Test + public void validExpression() { + assertThat(CronSequenceGenerator.isValidExpression("0 */2 1-4 * * *")).isTrue(); + } + + @Test + public void invalidExpressionWithLength() { + assertThat(CronSequenceGenerator.isValidExpression("0 */2 1-4 * * * *")).isFalse(); + } + + @Test + public void invalidExpressionWithSeconds() { + assertThat(CronSequenceGenerator.isValidExpression("100 */2 1-4 * * *")).isFalse(); + } + + @Test + public void invalidExpressionWithMonths() { + assertThat(CronSequenceGenerator.isValidExpression("0 */2 1-4 * INVALID *")).isFalse(); + } + + @Test + public void nullExpression() { + assertThat(CronSequenceGenerator.isValidExpression(null)).isFalse(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java new file mode 100644 index 0000000..119b5bd --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java @@ -0,0 +1,872 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.support; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; +import java.util.stream.Stream; + +import org.joda.time.LocalDateTime; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import org.springframework.scheduling.TriggerContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +/** + * Unit tests for {@link CronTrigger}. + * + * @author Dave Syer + * @author Mark Fisher + * @author Juergen Hoeller + * @author Sam Brannen + */ +class CronTriggerTests { + + private final Calendar calendar = new GregorianCalendar(); + + private void setUp(LocalDateTime localDateTime, TimeZone timeZone) { + this.calendar.setTimeZone(timeZone); + this.calendar.setTime(localDateTime.toDate()); + roundup(this.calendar); + } + + + @ParameterizedCronTriggerTest + void testMatchAll(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("* * * * * *", timeZone); + TriggerContext context = getTriggerContext(localDateTime.toDate()); + assertThat(trigger.nextExecutionTime(context)).isEqualTo(this.calendar.getTime()); + } + + @ParameterizedCronTriggerTest + void testMatchLastSecond(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("* * * * * *", timeZone); + GregorianCalendar calendar = new GregorianCalendar(); + calendar.set(Calendar.SECOND, 58); + assertMatchesNextSecond(trigger, calendar); + } + + @ParameterizedCronTriggerTest + void testMatchSpecificSecond(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("10 * * * * *", timeZone); + GregorianCalendar calendar = new GregorianCalendar(); + calendar.set(Calendar.SECOND, 9); + assertMatchesNextSecond(trigger, calendar); + } + + @ParameterizedCronTriggerTest + void testIncrementSecondByOne(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("11 * * * * *", timeZone); + this.calendar.set(Calendar.SECOND, 10); + Date localDate = this.calendar.getTime(); + this.calendar.add(Calendar.SECOND, 1); + TriggerContext context = getTriggerContext(localDate); + assertThat(trigger.nextExecutionTime(context)).isEqualTo(this.calendar.getTime()); + } + + @ParameterizedCronTriggerTest + void testIncrementSecondWithPreviousExecutionTooEarly(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("11 * * * * *", timeZone); + this.calendar.set(Calendar.SECOND, 11); + SimpleTriggerContext context = new SimpleTriggerContext(); + context.update(this.calendar.getTime(), new Date(this.calendar.getTimeInMillis() - 100), + new Date(this.calendar.getTimeInMillis() - 90)); + this.calendar.add(Calendar.MINUTE, 1); + assertThat(trigger.nextExecutionTime(context)).isEqualTo(this.calendar.getTime()); + } + + @ParameterizedCronTriggerTest + void testIncrementSecondAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("10 * * * * *", timeZone); + this.calendar.set(Calendar.SECOND, 11); + Date localDate = this.calendar.getTime(); + this.calendar.add(Calendar.SECOND, 59); + TriggerContext context = getTriggerContext(localDate); + assertThat(trigger.nextExecutionTime(context)).isEqualTo(this.calendar.getTime()); + } + + @ParameterizedCronTriggerTest + void testSecondRange(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("10-15 * * * * *", timeZone); + this.calendar.set(Calendar.SECOND, 9); + assertMatchesNextSecond(trigger, this.calendar); + this.calendar.set(Calendar.SECOND, 14); + assertMatchesNextSecond(trigger, this.calendar); + } + + @ParameterizedCronTriggerTest + void testIncrementMinute(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("0 * * * * *", timeZone); + this.calendar.set(Calendar.MINUTE, 10); + Date localDate = this.calendar.getTime(); + this.calendar.add(Calendar.MINUTE, 1); + this.calendar.set(Calendar.SECOND, 0); + TriggerContext context1 = getTriggerContext(localDate); + localDate = trigger.nextExecutionTime(context1); + assertThat(localDate).isEqualTo(this.calendar.getTime()); + this.calendar.add(Calendar.MINUTE, 1); + TriggerContext context2 = getTriggerContext(localDate); + localDate = trigger.nextExecutionTime(context2); + assertThat(localDate).isEqualTo(this.calendar.getTime()); + } + + @ParameterizedCronTriggerTest + void testIncrementMinuteByOne(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("0 11 * * * *", timeZone); + this.calendar.set(Calendar.MINUTE, 10); + TriggerContext context = getTriggerContext(this.calendar.getTime()); + this.calendar.add(Calendar.MINUTE, 1); + this.calendar.set(Calendar.SECOND, 0); + assertThat(trigger.nextExecutionTime(context)).isEqualTo(this.calendar.getTime()); + } + + @ParameterizedCronTriggerTest + void testIncrementMinuteAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("0 10 * * * *", timeZone); + this.calendar.set(Calendar.MINUTE, 11); + this.calendar.set(Calendar.SECOND, 0); + Date localDate = this.calendar.getTime(); + this.calendar.add(Calendar.MINUTE, 59); + TriggerContext context = getTriggerContext(localDate); + assertThat(trigger.nextExecutionTime(context)).isEqualTo(this.calendar.getTime()); + } + + @ParameterizedCronTriggerTest + void testIncrementHour(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("0 0 * * * *", timeZone); + this.calendar.set(Calendar.MONTH, 9); + this.calendar.set(Calendar.DAY_OF_MONTH, 30); + this.calendar.set(Calendar.HOUR_OF_DAY, 11); + this.calendar.set(Calendar.MINUTE, 1); + this.calendar.set(Calendar.SECOND, 0); + Date localDate = this.calendar.getTime(); + this.calendar.set(Calendar.MINUTE, 0); + this.calendar.set(Calendar.HOUR_OF_DAY, 12); + TriggerContext context1 = getTriggerContext(localDate); + Object actual = localDate = trigger.nextExecutionTime(context1); + assertThat(actual).isEqualTo(this.calendar.getTime()); + this.calendar.set(Calendar.HOUR_OF_DAY, 13); + TriggerContext context2 = getTriggerContext(localDate); + assertThat(trigger.nextExecutionTime(context2)).isEqualTo(this.calendar.getTime()); + } + + @ParameterizedCronTriggerTest + void testIncrementHourAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("0 0 * * * *", timeZone); + this.calendar.set(Calendar.MONTH, 9); + this.calendar.set(Calendar.DAY_OF_MONTH, 10); + this.calendar.set(Calendar.HOUR_OF_DAY, 23); + this.calendar.set(Calendar.MINUTE, 1); + this.calendar.set(Calendar.SECOND, 0); + Date localDate = this.calendar.getTime(); + this.calendar.set(Calendar.MINUTE, 0); + this.calendar.set(Calendar.HOUR_OF_DAY, 0); + this.calendar.set(Calendar.DAY_OF_MONTH, 11); + TriggerContext context1 = getTriggerContext(localDate); + Object actual = localDate = trigger.nextExecutionTime(context1); + assertThat(actual).isEqualTo(this.calendar.getTime()); + this.calendar.set(Calendar.HOUR_OF_DAY, 1); + TriggerContext context2 = getTriggerContext(localDate); + assertThat(trigger.nextExecutionTime(context2)).isEqualTo(this.calendar.getTime()); + } + + @ParameterizedCronTriggerTest + void testIncrementDayOfMonth(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("0 0 0 * * *", timeZone); + this.calendar.set(Calendar.DAY_OF_MONTH, 1); + Date localDate = this.calendar.getTime(); + this.calendar.add(Calendar.DAY_OF_MONTH, 1); + this.calendar.set(Calendar.HOUR_OF_DAY, 0); + this.calendar.set(Calendar.MINUTE, 0); + this.calendar.set(Calendar.SECOND, 0); + TriggerContext context1 = getTriggerContext(localDate); + Object actual1 = localDate = trigger.nextExecutionTime(context1); + assertThat(actual1).isEqualTo(this.calendar.getTime()); + assertThat(this.calendar.get(Calendar.DAY_OF_MONTH)).isEqualTo(2); + this.calendar.add(Calendar.DAY_OF_MONTH, 1); + TriggerContext context2 = getTriggerContext(localDate); + Object actual = localDate = trigger.nextExecutionTime(context2); + assertThat(actual).isEqualTo(this.calendar.getTime()); + assertThat(this.calendar.get(Calendar.DAY_OF_MONTH)).isEqualTo(3); + } + + @ParameterizedCronTriggerTest + void testIncrementDayOfMonthByOne(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("* * * 10 * *", timeZone); + this.calendar.set(Calendar.DAY_OF_MONTH, 9); + Date localDate = this.calendar.getTime(); + this.calendar.add(Calendar.DAY_OF_MONTH, 1); + this.calendar.set(Calendar.HOUR_OF_DAY, 0); + this.calendar.set(Calendar.MINUTE, 0); + this.calendar.set(Calendar.SECOND, 0); + TriggerContext context = getTriggerContext(localDate); + assertThat(trigger.nextExecutionTime(context)).isEqualTo(this.calendar.getTime()); + } + + @ParameterizedCronTriggerTest + void testIncrementDayOfMonthAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("* * * 10 * *", timeZone); + this.calendar.set(Calendar.DAY_OF_MONTH, 11); + Date localDate = this.calendar.getTime(); + this.calendar.add(Calendar.MONTH, 1); + this.calendar.set(Calendar.DAY_OF_MONTH, 10); + this.calendar.set(Calendar.HOUR_OF_DAY, 0); + this.calendar.set(Calendar.MINUTE, 0); + this.calendar.set(Calendar.SECOND, 0); + TriggerContext context = getTriggerContext(localDate); + assertThat(trigger.nextExecutionTime(context)).isEqualTo(this.calendar.getTime()); + } + + @ParameterizedCronTriggerTest + void testDailyTriggerInShortMonth(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("0 0 0 * * *", timeZone); + this.calendar.set(Calendar.MONTH, 8); // September: 30 days + this.calendar.set(Calendar.DAY_OF_MONTH, 30); + Date localDate = this.calendar.getTime(); + this.calendar.set(Calendar.MONTH, 9); // October + this.calendar.set(Calendar.HOUR_OF_DAY, 0); + this.calendar.set(Calendar.MINUTE, 0); + this.calendar.set(Calendar.SECOND, 0); + this.calendar.set(Calendar.DAY_OF_MONTH, 1); + TriggerContext context1 = getTriggerContext(localDate); + Object actual = localDate = trigger.nextExecutionTime(context1); + assertThat(actual).isEqualTo(this.calendar.getTime()); + this.calendar.set(Calendar.DAY_OF_MONTH, 2); + TriggerContext context2 = getTriggerContext(localDate); + assertThat(trigger.nextExecutionTime(context2)).isEqualTo(this.calendar.getTime()); + } + + @ParameterizedCronTriggerTest + void testDailyTriggerInLongMonth(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("0 0 0 * * *", timeZone); + this.calendar.set(Calendar.MONTH, 7); // August: 31 days and not a daylight saving boundary + this.calendar.set(Calendar.DAY_OF_MONTH, 30); + Date localDate = this.calendar.getTime(); + this.calendar.set(Calendar.HOUR_OF_DAY, 0); + this.calendar.set(Calendar.MINUTE, 0); + this.calendar.set(Calendar.SECOND, 0); + this.calendar.set(Calendar.DAY_OF_MONTH, 31); + TriggerContext context1 = getTriggerContext(localDate); + Object actual = localDate = trigger.nextExecutionTime(context1); + assertThat(actual).isEqualTo(this.calendar.getTime()); + this.calendar.set(Calendar.MONTH, 8); // September + this.calendar.set(Calendar.DAY_OF_MONTH, 1); + TriggerContext context2 = getTriggerContext(localDate); + assertThat(trigger.nextExecutionTime(context2)).isEqualTo(this.calendar.getTime()); + } + + @ParameterizedCronTriggerTest + void testDailyTriggerOnDaylightSavingBoundary(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("0 0 0 * * *", timeZone); + this.calendar.set(Calendar.MONTH, 9); // October: 31 days and a daylight saving boundary in CET + this.calendar.set(Calendar.DAY_OF_MONTH, 30); + Date localDate = this.calendar.getTime(); + this.calendar.set(Calendar.HOUR_OF_DAY, 0); + this.calendar.set(Calendar.MINUTE, 0); + this.calendar.set(Calendar.SECOND, 0); + this.calendar.set(Calendar.DAY_OF_MONTH, 31); + TriggerContext context1 = getTriggerContext(localDate); + Object actual = localDate = trigger.nextExecutionTime(context1); + assertThat(actual).isEqualTo(this.calendar.getTime()); + this.calendar.set(Calendar.MONTH, 10); // November + this.calendar.set(Calendar.DAY_OF_MONTH, 1); + TriggerContext context2 = getTriggerContext(localDate); + assertThat(trigger.nextExecutionTime(context2)).isEqualTo(this.calendar.getTime()); + } + + @ParameterizedCronTriggerTest + void testIncrementMonth(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("0 0 0 1 * *", timeZone); + this.calendar.set(Calendar.MONTH, 9); + this.calendar.set(Calendar.DAY_OF_MONTH, 30); + Date localDate = this.calendar.getTime(); + this.calendar.set(Calendar.DAY_OF_MONTH, 1); + this.calendar.set(Calendar.HOUR_OF_DAY, 0); + this.calendar.set(Calendar.MINUTE, 0); + this.calendar.set(Calendar.SECOND, 0); + this.calendar.set(Calendar.MONTH, 10); + TriggerContext context1 = getTriggerContext(localDate); + Object actual = localDate = trigger.nextExecutionTime(context1); + assertThat(actual).isEqualTo(this.calendar.getTime()); + this.calendar.set(Calendar.MONTH, 11); + TriggerContext context2 = getTriggerContext(localDate); + assertThat(trigger.nextExecutionTime(context2)).isEqualTo(this.calendar.getTime()); + } + + @ParameterizedCronTriggerTest + void testIncrementMonthAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("0 0 0 1 * *", timeZone); + this.calendar.set(Calendar.MONTH, 11); + this.calendar.set(Calendar.DAY_OF_MONTH, 31); + this.calendar.set(Calendar.YEAR, 2010); + Date localDate = this.calendar.getTime(); + this.calendar.set(Calendar.DAY_OF_MONTH, 1); + this.calendar.set(Calendar.HOUR_OF_DAY, 0); + this.calendar.set(Calendar.MINUTE, 0); + this.calendar.set(Calendar.SECOND, 0); + this.calendar.set(Calendar.MONTH, 0); + this.calendar.set(Calendar.YEAR, 2011); + TriggerContext context1 = getTriggerContext(localDate); + Object actual = localDate = trigger.nextExecutionTime(context1); + assertThat(actual).isEqualTo(this.calendar.getTime()); + this.calendar.set(Calendar.MONTH, 1); + TriggerContext context2 = getTriggerContext(localDate); + assertThat(trigger.nextExecutionTime(context2)).isEqualTo(this.calendar.getTime()); + } + + @ParameterizedCronTriggerTest + void testMonthlyTriggerInLongMonth(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("0 0 0 31 * *", timeZone); + this.calendar.set(Calendar.MONTH, 9); + this.calendar.set(Calendar.DAY_OF_MONTH, 30); + Date localDate = this.calendar.getTime(); + this.calendar.set(Calendar.DAY_OF_MONTH, 31); + this.calendar.set(Calendar.HOUR_OF_DAY, 0); + this.calendar.set(Calendar.MINUTE, 0); + this.calendar.set(Calendar.SECOND, 0); + TriggerContext context = getTriggerContext(localDate); + assertThat(trigger.nextExecutionTime(context)).isEqualTo(this.calendar.getTime()); + } + + @ParameterizedCronTriggerTest + void testMonthlyTriggerInShortMonth(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("0 0 0 1 * *", timeZone); + this.calendar.set(Calendar.MONTH, 9); + this.calendar.set(Calendar.DAY_OF_MONTH, 30); + Date localDate = this.calendar.getTime(); + this.calendar.set(Calendar.MONTH, 10); + this.calendar.set(Calendar.DAY_OF_MONTH, 1); + this.calendar.set(Calendar.HOUR_OF_DAY, 0); + this.calendar.set(Calendar.MINUTE, 0); + this.calendar.set(Calendar.SECOND, 0); + TriggerContext context = getTriggerContext(localDate); + assertThat(trigger.nextExecutionTime(context)).isEqualTo(this.calendar.getTime()); + } + + @ParameterizedCronTriggerTest + void testIncrementDayOfWeekByOne(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("* * * * * 2", timeZone); + this.calendar.set(Calendar.DAY_OF_WEEK, 2); + Date localDate = this.calendar.getTime(); + this.calendar.add(Calendar.DAY_OF_WEEK, 1); + this.calendar.set(Calendar.HOUR_OF_DAY, 0); + this.calendar.set(Calendar.MINUTE, 0); + this.calendar.set(Calendar.SECOND, 0); + TriggerContext context = getTriggerContext(localDate); + assertThat(trigger.nextExecutionTime(context)).isEqualTo(this.calendar.getTime()); + assertThat(this.calendar.get(Calendar.DAY_OF_WEEK)).isEqualTo(Calendar.TUESDAY); + } + + @ParameterizedCronTriggerTest + void testIncrementDayOfWeekAndRollover(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("* * * * * 2", timeZone); + this.calendar.set(Calendar.DAY_OF_WEEK, 4); + Date localDate = this.calendar.getTime(); + this.calendar.add(Calendar.DAY_OF_MONTH, 6); + this.calendar.set(Calendar.HOUR_OF_DAY, 0); + this.calendar.set(Calendar.MINUTE, 0); + this.calendar.set(Calendar.SECOND, 0); + TriggerContext context = getTriggerContext(localDate); + assertThat(trigger.nextExecutionTime(context)).isEqualTo(this.calendar.getTime()); + assertThat(this.calendar.get(Calendar.DAY_OF_WEEK)).isEqualTo(Calendar.TUESDAY); + } + + @ParameterizedCronTriggerTest + void testSpecificMinuteSecond(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("55 5 * * * *", timeZone); + this.calendar.set(Calendar.MINUTE, 4); + this.calendar.set(Calendar.SECOND, 54); + Date localDate = this.calendar.getTime(); + TriggerContext context1 = getTriggerContext(localDate); + this.calendar.add(Calendar.MINUTE, 1); + this.calendar.set(Calendar.SECOND, 55); + Object actual1 = localDate = trigger.nextExecutionTime(context1); + assertThat(actual1).isEqualTo(this.calendar.getTime()); + this.calendar.add(Calendar.HOUR, 1); + TriggerContext context2 = getTriggerContext(localDate); + Object actual = localDate = trigger.nextExecutionTime(context2); + assertThat(actual).isEqualTo(this.calendar.getTime()); + } + + @ParameterizedCronTriggerTest + void testSpecificHourSecond(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("55 * 10 * * *", timeZone); + this.calendar.set(Calendar.HOUR_OF_DAY, 9); + this.calendar.set(Calendar.SECOND, 54); + Date localDate = this.calendar.getTime(); + TriggerContext context1 = getTriggerContext(localDate); + this.calendar.add(Calendar.HOUR_OF_DAY, 1); + this.calendar.set(Calendar.MINUTE, 0); + this.calendar.set(Calendar.SECOND, 55); + Object actual1 = localDate = trigger.nextExecutionTime(context1); + assertThat(actual1).isEqualTo(this.calendar.getTime()); + this.calendar.add(Calendar.MINUTE, 1); + TriggerContext context2 = getTriggerContext(localDate); + Object actual = localDate = trigger.nextExecutionTime(context2); + assertThat(actual).isEqualTo(this.calendar.getTime()); + } + + @ParameterizedCronTriggerTest + void testSpecificMinuteHour(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("* 5 10 * * *", timeZone); + this.calendar.set(Calendar.MINUTE, 4); + this.calendar.set(Calendar.HOUR_OF_DAY, 9); + Date localDate = this.calendar.getTime(); + this.calendar.add(Calendar.MINUTE, 1); + this.calendar.add(Calendar.HOUR_OF_DAY, 1); + this.calendar.set(Calendar.SECOND, 0); + TriggerContext context1 = getTriggerContext(localDate); + Object actual1 = localDate = trigger.nextExecutionTime(context1); + assertThat(actual1).isEqualTo(this.calendar.getTime()); + // next trigger is in one second because second is wildcard + this.calendar.add(Calendar.SECOND, 1); + TriggerContext context2 = getTriggerContext(localDate); + Object actual = localDate = trigger.nextExecutionTime(context2); + assertThat(actual).isEqualTo(this.calendar.getTime()); + } + + @ParameterizedCronTriggerTest + void testSpecificDayOfMonthSecond(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("55 * * 3 * *", timeZone); + this.calendar.set(Calendar.DAY_OF_MONTH, 2); + this.calendar.set(Calendar.SECOND, 54); + Date localDate = this.calendar.getTime(); + TriggerContext context1 = getTriggerContext(localDate); + this.calendar.add(Calendar.DAY_OF_MONTH, 1); + this.calendar.set(Calendar.HOUR_OF_DAY, 0); + this.calendar.set(Calendar.MINUTE, 0); + this.calendar.set(Calendar.SECOND, 55); + Object actual1 = localDate = trigger.nextExecutionTime(context1); + assertThat(actual1).isEqualTo(this.calendar.getTime()); + this.calendar.add(Calendar.MINUTE, 1); + TriggerContext context2 = getTriggerContext(localDate); + Object actual = localDate = trigger.nextExecutionTime(context2); + assertThat(actual).isEqualTo(this.calendar.getTime()); + } + + @ParameterizedCronTriggerTest + void testSpecificDate(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("* * * 3 11 *", timeZone); + this.calendar.set(Calendar.DAY_OF_MONTH, 2); + this.calendar.set(Calendar.MONTH, 9); + Date localDate = this.calendar.getTime(); + TriggerContext context1 = getTriggerContext(localDate); + this.calendar.add(Calendar.DAY_OF_MONTH, 1); + this.calendar.set(Calendar.HOUR_OF_DAY, 0); + this.calendar.set(Calendar.MONTH, 10); // 10=November + this.calendar.set(Calendar.MINUTE, 0); + this.calendar.set(Calendar.SECOND, 0); + Object actual1 = localDate = trigger.nextExecutionTime(context1); + assertThat(actual1).isEqualTo(this.calendar.getTime()); + this.calendar.add(Calendar.SECOND, 1); + TriggerContext context2 = getTriggerContext(localDate); + Object actual = localDate = trigger.nextExecutionTime(context2); + assertThat(actual).isEqualTo(this.calendar.getTime()); + } + + @ParameterizedCronTriggerTest + void testNonExistentSpecificDate(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + // TODO: maybe try and detect this as a special case in parser? + CronTrigger trigger = new CronTrigger("0 0 0 31 6 *", timeZone); + this.calendar.set(Calendar.DAY_OF_MONTH, 10); + this.calendar.set(Calendar.MONTH, 2); + Date localDate = this.calendar.getTime(); + TriggerContext context1 = getTriggerContext(localDate); + assertThat(trigger.nextExecutionTime(context1)).isNull(); + } + + @ParameterizedCronTriggerTest + void testLeapYearSpecificDate(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("0 0 0 29 2 *", timeZone); + this.calendar.set(Calendar.YEAR, 2007); + this.calendar.set(Calendar.DAY_OF_MONTH, 10); + this.calendar.set(Calendar.MONTH, 1); // 2=February + Date localDate = this.calendar.getTime(); + TriggerContext context1 = getTriggerContext(localDate); + this.calendar.set(Calendar.YEAR, 2008); + this.calendar.set(Calendar.DAY_OF_MONTH, 29); + this.calendar.set(Calendar.HOUR_OF_DAY, 0); + this.calendar.set(Calendar.MINUTE, 0); + this.calendar.set(Calendar.SECOND, 0); + Object actual1 = localDate = trigger.nextExecutionTime(context1); + assertThat(actual1).isEqualTo(this.calendar.getTime()); + this.calendar.add(Calendar.YEAR, 4); + TriggerContext context2 = getTriggerContext(localDate); + Object actual = localDate = trigger.nextExecutionTime(context2); + assertThat(actual).isEqualTo(this.calendar.getTime()); + } + + @ParameterizedCronTriggerTest + void testWeekDaySequence(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("0 0 7 ? * MON-FRI", timeZone); + // This is a Saturday + this.calendar.set(2009, 8, 26); + Date localDate = this.calendar.getTime(); + // 7 am is the trigger time + this.calendar.set(Calendar.HOUR_OF_DAY, 7); + this.calendar.set(Calendar.MINUTE, 0); + this.calendar.set(Calendar.SECOND, 0); + // Add two days because we start on Saturday + this.calendar.add(Calendar.DAY_OF_MONTH, 2); + TriggerContext context1 = getTriggerContext(localDate); + Object actual2 = localDate = trigger.nextExecutionTime(context1); + assertThat(actual2).isEqualTo(this.calendar.getTime()); + // Next day is a week day so add one + this.calendar.add(Calendar.DAY_OF_MONTH, 1); + TriggerContext context2 = getTriggerContext(localDate); + Object actual1 = localDate = trigger.nextExecutionTime(context2); + assertThat(actual1).isEqualTo(this.calendar.getTime()); + this.calendar.add(Calendar.DAY_OF_MONTH, 1); + TriggerContext context3 = getTriggerContext(localDate); + Object actual = localDate = trigger.nextExecutionTime(context3); + assertThat(actual).isEqualTo(this.calendar.getTime()); + } + + @ParameterizedCronTriggerTest + void testDayOfWeekIndifferent(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger1 = new CronTrigger("* * * 2 * *", timeZone); + CronTrigger trigger2 = new CronTrigger("* * * 2 * ?", timeZone); + assertThat(trigger2).isEqualTo(trigger1); + } + + @ParameterizedCronTriggerTest + void testSecondIncrementer(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger1 = new CronTrigger("57,59 * * * * *", timeZone); + CronTrigger trigger2 = new CronTrigger("57/2 * * * * *", timeZone); + assertThat(trigger2).isEqualTo(trigger1); + } + + @ParameterizedCronTriggerTest + void testSecondIncrementerWithRange(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger1 = new CronTrigger("1,3,5 * * * * *", timeZone); + CronTrigger trigger2 = new CronTrigger("1-6/2 * * * * *", timeZone); + assertThat(trigger2).isEqualTo(trigger1); + } + + @ParameterizedCronTriggerTest + void testHourIncrementer(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger1 = new CronTrigger("* * 4,8,12,16,20 * * *", timeZone); + CronTrigger trigger2 = new CronTrigger("* * 4/4 * * *", timeZone); + assertThat(trigger2).isEqualTo(trigger1); + } + + @ParameterizedCronTriggerTest + void testDayNames(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger1 = new CronTrigger("* * * * * 0-6", timeZone); + CronTrigger trigger2 = new CronTrigger("* * * * * TUE,WED,THU,FRI,SAT,SUN,MON", timeZone); + assertThat(trigger2).isEqualTo(trigger1); + } + + @ParameterizedCronTriggerTest + void testSundayIsZero(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger1 = new CronTrigger("* * * * * 0", timeZone); + CronTrigger trigger2 = new CronTrigger("* * * * * SUN", timeZone); + assertThat(trigger2).isEqualTo(trigger1); + } + + @ParameterizedCronTriggerTest + void testSundaySynonym(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger1 = new CronTrigger("* * * * * 0", timeZone); + CronTrigger trigger2 = new CronTrigger("* * * * * 7", timeZone); + assertThat(trigger2).isEqualTo(trigger1); + } + + @ParameterizedCronTriggerTest + void testMonthNames(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger1 = new CronTrigger("* * * * 1-12 *", timeZone); + CronTrigger trigger2 = new CronTrigger("* * * * FEB,JAN,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC *", timeZone); + assertThat(trigger2).isEqualTo(trigger1); + } + + @ParameterizedCronTriggerTest + void testMonthNamesMixedCase(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger1 = new CronTrigger("* * * * 2 *", timeZone); + CronTrigger trigger2 = new CronTrigger("* * * * Feb *", timeZone); + assertThat(trigger2).isEqualTo(trigger1); + } + + @ParameterizedCronTriggerTest + void testSecondInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("77 * * * * *", timeZone)); + } + + @ParameterizedCronTriggerTest + void testSecondRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("44-77 * * * * *", timeZone)); + } + + @ParameterizedCronTriggerTest + void testMinuteInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* 77 * * * *", timeZone)); + } + + @ParameterizedCronTriggerTest + void testMinuteRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* 44-77 * * * *", timeZone)); + } + + @ParameterizedCronTriggerTest + void testHourInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* * 27 * * *", timeZone)); + } + + @ParameterizedCronTriggerTest + void testHourRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* * 23-28 * * *", timeZone)); + } + + @ParameterizedCronTriggerTest + void testDayInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* * * 45 * *", timeZone)); + } + + @ParameterizedCronTriggerTest + void testDayRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* * * 28-45 * *", timeZone)); + } + + @ParameterizedCronTriggerTest + void testMonthInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("0 0 0 25 13 ?", timeZone)); + } + + @ParameterizedCronTriggerTest + void testMonthInvalidTooSmall(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("0 0 0 25 0 ?", timeZone)); + } + + @ParameterizedCronTriggerTest + void testDayOfMonthInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("0 0 0 32 12 ?", timeZone)); + } + + @ParameterizedCronTriggerTest + void testMonthRangeInvalid(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + assertThatIllegalArgumentException().isThrownBy(() -> new CronTrigger("* * * * 11-13 *", timeZone)); + } + + @ParameterizedCronTriggerTest + void testWhitespace(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger1 = new CronTrigger("* * * * 1 *", timeZone); + CronTrigger trigger2 = new CronTrigger("* * * * 1 *", timeZone); + assertThat(trigger2).isEqualTo(trigger1); + } + + @ParameterizedCronTriggerTest + void testMonthSequence(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + CronTrigger trigger = new CronTrigger("0 30 23 30 1/3 ?", timeZone); + this.calendar.set(2010, 11, 30); + Date localDate = this.calendar.getTime(); + // set expected next trigger time + this.calendar.set(Calendar.HOUR_OF_DAY, 23); + this.calendar.set(Calendar.MINUTE, 30); + this.calendar.set(Calendar.SECOND, 0); + this.calendar.add(Calendar.MONTH, 1); + TriggerContext context1 = getTriggerContext(localDate); + Object actual2 = localDate = trigger.nextExecutionTime(context1); + assertThat(actual2).isEqualTo(this.calendar.getTime()); + // Next trigger is 3 months latter + this.calendar.add(Calendar.MONTH, 3); + TriggerContext context2 = getTriggerContext(localDate); + Object actual1 = localDate = trigger.nextExecutionTime(context2); + assertThat(actual1).isEqualTo(this.calendar.getTime()); + // Next trigger is 3 months latter + this.calendar.add(Calendar.MONTH, 3); + TriggerContext context3 = getTriggerContext(localDate); + Object actual = localDate = trigger.nextExecutionTime(context3); + assertThat(actual).isEqualTo(this.calendar.getTime()); + } + + @ParameterizedCronTriggerTest + void testDaylightSavingMissingHour(LocalDateTime localDateTime, TimeZone timeZone) { + setUp(localDateTime, timeZone); + + // This trigger has to be somewhere in between 2am and 3am + CronTrigger trigger = new CronTrigger("0 10 2 * * *", timeZone); + this.calendar.set(Calendar.DAY_OF_MONTH, 31); + this.calendar.set(Calendar.MONTH, Calendar.MARCH); + this.calendar.set(Calendar.YEAR, 2013); + this.calendar.set(Calendar.HOUR_OF_DAY, 1); + this.calendar.set(Calendar.SECOND, 54); + Date localDate = this.calendar.getTime(); + TriggerContext context1 = getTriggerContext(localDate); + if (timeZone.equals(TimeZone.getTimeZone("CET"))) { + // Clocks go forward an hour so 2am doesn't exist in CET for this localDateTime + this.calendar.add(Calendar.DAY_OF_MONTH, 1); + } + this.calendar.add(Calendar.HOUR_OF_DAY, 1); + this.calendar.set(Calendar.MINUTE, 10); + this.calendar.set(Calendar.SECOND, 0); + Object actual = localDate = trigger.nextExecutionTime(context1); + assertThat(actual).isEqualTo(this.calendar.getTime()); + } + + private static void roundup(Calendar calendar) { + calendar.add(Calendar.SECOND, 1); + calendar.set(Calendar.MILLISECOND, 0); + } + + private static void assertMatchesNextSecond(CronTrigger trigger, Calendar calendar) { + Date localDateTime = calendar.getTime(); + roundup(calendar); + TriggerContext context = getTriggerContext(localDateTime); + assertThat(trigger.nextExecutionTime(context)).isEqualTo(calendar.getTime()); + } + + private static TriggerContext getTriggerContext(Date lastCompletionTime) { + SimpleTriggerContext context = new SimpleTriggerContext(); + context.update(null, null, lastCompletionTime); + return context; + } + + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + @ParameterizedTest(name = "[{index}] localDateTime[{0}], time zone[{1}]") + @MethodSource("parameters") + @interface ParameterizedCronTriggerTest { + } + + static Stream parameters() { + return Stream.of( + arguments(LocalDateTime.now(), TimeZone.getTimeZone("PST")), + arguments(LocalDateTime.now(), TimeZone.getTimeZone("CET")) + ); + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/PeriodicTriggerTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/PeriodicTriggerTests.java new file mode 100644 index 0000000..60e09f0 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/support/PeriodicTriggerTests.java @@ -0,0 +1,271 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.support; + +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; + +import org.springframework.scheduling.TriggerContext; +import org.springframework.util.NumberUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Mark Fisher + * @since 3.0 + */ +public class PeriodicTriggerTests { + + @Test + public void fixedDelayFirstExecution() { + Date now = new Date(); + PeriodicTrigger trigger = new PeriodicTrigger(5000); + Date next = trigger.nextExecutionTime(context(null, null, null)); + assertNegligibleDifference(now, next); + } + + @Test + public void fixedDelayWithInitialDelayFirstExecution() { + Date now = new Date(); + long period = 5000; + long initialDelay = 30000; + PeriodicTrigger trigger = new PeriodicTrigger(period); + trigger.setInitialDelay(initialDelay); + Date next = trigger.nextExecutionTime(context(null, null, null)); + assertApproximateDifference(now, next, initialDelay); + } + + @Test + public void fixedDelayWithTimeUnitFirstExecution() { + Date now = new Date(); + PeriodicTrigger trigger = new PeriodicTrigger(5, TimeUnit.SECONDS); + Date next = trigger.nextExecutionTime(context(null, null, null)); + assertNegligibleDifference(now, next); + } + + @Test + public void fixedDelayWithTimeUnitAndInitialDelayFirstExecution() { + Date now = new Date(); + long period = 5; + long initialDelay = 30; + PeriodicTrigger trigger = new PeriodicTrigger(period, TimeUnit.SECONDS); + trigger.setInitialDelay(initialDelay); + Date next = trigger.nextExecutionTime(context(null, null, null)); + assertApproximateDifference(now, next, initialDelay * 1000); + } + + @Test + public void fixedDelaySubsequentExecution() { + Date now = new Date(); + long period = 5000; + PeriodicTrigger trigger = new PeriodicTrigger(period); + Date next = trigger.nextExecutionTime(context(now, 500, 3000)); + assertApproximateDifference(now, next, period + 3000); + } + + @Test + public void fixedDelayWithInitialDelaySubsequentExecution() { + Date now = new Date(); + long period = 5000; + long initialDelay = 30000; + PeriodicTrigger trigger = new PeriodicTrigger(period); + trigger.setInitialDelay(initialDelay); + Date next = trigger.nextExecutionTime(context(now, 500, 3000)); + assertApproximateDifference(now, next, period + 3000); + } + + @Test + public void fixedDelayWithTimeUnitSubsequentExecution() { + Date now = new Date(); + long period = 5; + PeriodicTrigger trigger = new PeriodicTrigger(period, TimeUnit.SECONDS); + Date next = trigger.nextExecutionTime(context(now, 500, 3000)); + assertApproximateDifference(now, next, (period * 1000) + 3000); + } + + @Test + public void fixedRateFirstExecution() { + Date now = new Date(); + PeriodicTrigger trigger = new PeriodicTrigger(5000); + trigger.setFixedRate(true); + Date next = trigger.nextExecutionTime(context(null, null, null)); + assertNegligibleDifference(now, next); + } + + @Test + public void fixedRateWithTimeUnitFirstExecution() { + Date now = new Date(); + PeriodicTrigger trigger = new PeriodicTrigger(5, TimeUnit.SECONDS); + trigger.setFixedRate(true); + Date next = trigger.nextExecutionTime(context(null, null, null)); + assertNegligibleDifference(now, next); + } + + @Test + public void fixedRateWithInitialDelayFirstExecution() { + Date now = new Date(); + long period = 5000; + long initialDelay = 30000; + PeriodicTrigger trigger = new PeriodicTrigger(period); + trigger.setFixedRate(true); + trigger.setInitialDelay(initialDelay); + Date next = trigger.nextExecutionTime(context(null, null, null)); + assertApproximateDifference(now, next, initialDelay); + } + + @Test + public void fixedRateWithTimeUnitAndInitialDelayFirstExecution() { + Date now = new Date(); + long period = 5; + long initialDelay = 30; + PeriodicTrigger trigger = new PeriodicTrigger(period, TimeUnit.MINUTES); + trigger.setFixedRate(true); + trigger.setInitialDelay(initialDelay); + Date next = trigger.nextExecutionTime(context(null, null, null)); + assertApproximateDifference(now, next, (initialDelay * 60 * 1000)); + } + + @Test + public void fixedRateSubsequentExecution() { + Date now = new Date(); + long period = 5000; + PeriodicTrigger trigger = new PeriodicTrigger(period); + trigger.setFixedRate(true); + Date next = trigger.nextExecutionTime(context(now, 500, 3000)); + assertApproximateDifference(now, next, period); + } + + @Test + public void fixedRateWithInitialDelaySubsequentExecution() { + Date now = new Date(); + long period = 5000; + long initialDelay = 30000; + PeriodicTrigger trigger = new PeriodicTrigger(period); + trigger.setFixedRate(true); + trigger.setInitialDelay(initialDelay); + Date next = trigger.nextExecutionTime(context(now, 500, 3000)); + assertApproximateDifference(now, next, period); + } + + @Test + public void fixedRateWithTimeUnitSubsequentExecution() { + Date now = new Date(); + long period = 5; + PeriodicTrigger trigger = new PeriodicTrigger(period, TimeUnit.HOURS); + trigger.setFixedRate(true); + Date next = trigger.nextExecutionTime(context(now, 500, 3000)); + assertApproximateDifference(now, next, (period * 60 * 60 * 1000)); + } + + @Test + public void equalsVerification() { + PeriodicTrigger trigger1 = new PeriodicTrigger(3000); + PeriodicTrigger trigger2 = new PeriodicTrigger(3000); + assertThat(trigger1.equals(new String("not a trigger"))).isFalse(); + assertThat(trigger1.equals(null)).isFalse(); + assertThat(trigger1).isEqualTo(trigger1); + assertThat(trigger2).isEqualTo(trigger2); + assertThat(trigger2).isEqualTo(trigger1); + trigger2.setInitialDelay(1234); + assertThat(trigger1.equals(trigger2)).isFalse(); + assertThat(trigger2.equals(trigger1)).isFalse(); + trigger1.setInitialDelay(1234); + assertThat(trigger2).isEqualTo(trigger1); + trigger2.setFixedRate(true); + assertThat(trigger1.equals(trigger2)).isFalse(); + assertThat(trigger2.equals(trigger1)).isFalse(); + trigger1.setFixedRate(true); + assertThat(trigger2).isEqualTo(trigger1); + PeriodicTrigger trigger3 = new PeriodicTrigger(3, TimeUnit.SECONDS); + trigger3.setInitialDelay(7); + trigger3.setFixedRate(true); + assertThat(trigger1.equals(trigger3)).isFalse(); + assertThat(trigger3.equals(trigger1)).isFalse(); + trigger1.setInitialDelay(7000); + assertThat(trigger3).isEqualTo(trigger1); + } + + + // utility methods + + private static void assertNegligibleDifference(Date d1, Date d2) { + long diff = Math.abs(d1.getTime() - d2.getTime()); + assertThat(diff < 100).as("difference exceeds threshold: " + diff).isTrue(); + } + + private static void assertApproximateDifference(Date lesser, Date greater, long expected) { + long diff = greater.getTime() - lesser.getTime(); + long variance = Math.abs(expected - diff); + assertThat(variance < 100).as("expected approximate difference of " + expected + + ", but actual difference was " + diff).isTrue(); + } + + private static TriggerContext context(Object scheduled, Object actual, Object completion) { + return new TestTriggerContext(asDate(scheduled), asDate(actual), asDate(completion)); + } + + private static Date asDate(Object o) { + if (o == null) { + return null; + } + if (o instanceof Date) { + return (Date) o; + } + if (o instanceof Number) { + return new Date(System.currentTimeMillis() + + NumberUtils.convertNumberToTargetClass((Number) o, Long.class)); + } + throw new IllegalArgumentException( + "expected Date or Number, but actual type was: " + o.getClass()); + } + + + // helper class + + private static class TestTriggerContext implements TriggerContext { + + private final Date scheduled; + + private final Date actual; + + private final Date completion; + + TestTriggerContext(Date scheduled, Date actual, Date completion) { + this.scheduled = scheduled; + this.actual = actual; + this.completion = completion; + } + + @Override + public Date lastActualExecutionTime() { + return this.actual; + } + + @Override + public Date lastCompletionTime() { + return this.completion; + } + + @Override + public Date lastScheduledExecutionTime() { + return this.scheduled; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/QuartzCronFieldTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/QuartzCronFieldTests.java new file mode 100644 index 0000000..bd16243 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scheduling/support/QuartzCronFieldTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scheduling.support; + +import java.time.DayOfWeek; +import java.time.LocalDate; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Arjen Poutsma + */ +class QuartzCronFieldTests { + + @Test + void lastDayOfMonth() { + QuartzCronField field = QuartzCronField.parseDaysOfMonth("L"); + + LocalDate last = LocalDate.of(2020, 6, 16); + LocalDate expected = LocalDate.of(2020, 6, 30); + assertThat(field.nextOrSame(last)).isEqualTo(expected); + } + + @Test + void lastDayOfMonthOffset() { + QuartzCronField field = QuartzCronField.parseDaysOfMonth("L-3"); + + LocalDate last = LocalDate.of(2020, 6, 16); + LocalDate expected = LocalDate.of(2020, 6, 27); + assertThat(field.nextOrSame(last)).isEqualTo(expected); + } + + @Test + void lastWeekdayOfMonth() { + QuartzCronField field = QuartzCronField.parseDaysOfMonth("LW"); + + LocalDate last = LocalDate.of(2020, 6, 16); + LocalDate expected = LocalDate.of(2020, 6, 30); + LocalDate actual = field.nextOrSame(last); + assertThat(actual).isNotNull(); + assertThat(actual.getDayOfWeek()).isEqualTo(DayOfWeek.TUESDAY); + assertThat(actual).isEqualTo(expected); + } + + @Test + void lastDayOfWeekOffset() { + // last Thursday (4) of the month + QuartzCronField field = QuartzCronField.parseDaysOfWeek("4L"); + + LocalDate last = LocalDate.of(2020, 6, 16); + LocalDate expected = LocalDate.of(2020, 6, 25); + assertThat(field.nextOrSame(last)).isEqualTo(expected); + } + + @Test + void invalidValues() { + assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfMonth("")); + assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfMonth("1")); + assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfMonth("1L")); + assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfMonth("LL")); + assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfMonth("4L")); + assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfMonth("0L")); + assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfMonth("W")); + assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfMonth("W1")); + assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfMonth("WW")); + assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfMonth("32W")); + + assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfWeek("")); + assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfWeek("1")); + assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfWeek("L")); + assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfWeek("L1")); + assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfWeek("LL")); + assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfWeek("-4L")); + assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfWeek("8L")); + assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfWeek("#")); + assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfWeek("1#")); + assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfWeek("#1")); + assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfWeek("1#L")); + assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfWeek("L#1")); + assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfWeek("8#1")); + } + +} diff --git a/spring-context/src/test/java/org/springframework/scripting/Calculator.java b/spring-context/src/test/java/org/springframework/scripting/Calculator.java new file mode 100644 index 0000000..073c4b3 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/Calculator.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2006 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting; + +/** + * @author Rob Harrop + */ +public interface Calculator { + + int add(int x, int y); + +} diff --git a/spring-context/src/test/java/org/springframework/scripting/CallCounter.java b/spring-context/src/test/java/org/springframework/scripting/CallCounter.java new file mode 100644 index 0000000..93bee28 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/CallCounter.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2007 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting; + +/** + * @author Juergen Hoeller + */ +public interface CallCounter { + + void before(); + + int getCalls(); + +} diff --git a/spring-context/src/test/java/org/springframework/scripting/ConfigurableMessenger.java b/spring-context/src/test/java/org/springframework/scripting/ConfigurableMessenger.java new file mode 100644 index 0000000..684bb6f --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/ConfigurableMessenger.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2006 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting; + +/** + * @author Juergen Hoeller + */ +public interface ConfigurableMessenger extends Messenger { + + void setMessage(String message); + +} diff --git a/spring-context/src/test/java/org/springframework/scripting/ContextScriptBean.java b/spring-context/src/test/java/org/springframework/scripting/ContextScriptBean.java new file mode 100644 index 0000000..b503366 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/ContextScriptBean.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting; + +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.ApplicationContext; + +/** + * @author Juergen Hoeller + * @since 08.08.2006 + */ +public interface ContextScriptBean extends ScriptBean { + + TestBean getTestBean(); + + ApplicationContext getApplicationContext(); + +} diff --git a/spring-context/src/test/java/org/springframework/scripting/Messenger.java b/spring-context/src/test/java/org/springframework/scripting/Messenger.java new file mode 100644 index 0000000..11988d3 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/Messenger.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2006 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting; + +/** + * @author Rob Harrop + */ +public interface Messenger { + + String getMessage(); + +} diff --git a/spring-context/src/test/java/org/springframework/scripting/MessengerScrambler.java b/spring-context/src/test/java/org/springframework/scripting/MessengerScrambler.java new file mode 100644 index 0000000..36320d0 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/MessengerScrambler.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2007 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting; + +import org.aspectj.lang.ProceedingJoinPoint; + +/** + * Twee advice that 'scrambles' the return value + * of a {@link Messenger} invocation. + * + * @author Rick Evans + */ +public class MessengerScrambler { + + public String scramble(ProceedingJoinPoint pjp) throws Throwable { + String message = (String) pjp.proceed(); + return new StringBuilder(message).reverse().toString(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/scripting/ScriptBean.java b/spring-context/src/test/java/org/springframework/scripting/ScriptBean.java new file mode 100644 index 0000000..1676d45 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/ScriptBean.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting; + +/** + * Simple interface used in testing the scripted beans support. + * + * @author Rick Evans + */ +public interface ScriptBean { + + String getName(); + + void setName(String name); + + int getAge(); + + void setAge(int age); + +} diff --git a/spring-context/src/test/java/org/springframework/scripting/TestBeanAwareMessenger.java b/spring-context/src/test/java/org/springframework/scripting/TestBeanAwareMessenger.java new file mode 100644 index 0000000..5edefe5 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/TestBeanAwareMessenger.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting; + +import org.springframework.beans.testfixture.beans.TestBean; + +/** + * @author Juergen Hoeller + */ +public interface TestBeanAwareMessenger extends ConfigurableMessenger { + + TestBean getTestBean(); + + void setTestBean(TestBean testBean); + +} diff --git a/spring-context/src/test/java/org/springframework/scripting/bsh/BshScriptEvaluatorTests.java b/spring-context/src/test/java/org/springframework/scripting/bsh/BshScriptEvaluatorTests.java new file mode 100644 index 0000000..dff1f64 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/bsh/BshScriptEvaluatorTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.bsh; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.scripting.ScriptEvaluator; +import org.springframework.scripting.support.ResourceScriptSource; +import org.springframework.scripting.support.StaticScriptSource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + */ +public class BshScriptEvaluatorTests { + + @Test + public void testBshScriptFromString() { + ScriptEvaluator evaluator = new BshScriptEvaluator(); + Object result = evaluator.evaluate(new StaticScriptSource("return 3 * 2;")); + assertThat(result).isEqualTo(6); + } + + @Test + public void testBshScriptFromFile() { + ScriptEvaluator evaluator = new BshScriptEvaluator(); + Object result = evaluator.evaluate(new ResourceScriptSource(new ClassPathResource("simple.bsh", getClass()))); + assertThat(result).isEqualTo(6); + } + + @Test + public void testGroovyScriptWithArguments() { + ScriptEvaluator evaluator = new BshScriptEvaluator(); + Map arguments = new HashMap<>(); + arguments.put("a", 3); + arguments.put("b", 2); + Object result = evaluator.evaluate(new StaticScriptSource("return a * b;"), arguments); + assertThat(result).isEqualTo(6); + } + +} diff --git a/spring-context/src/test/java/org/springframework/scripting/bsh/BshScriptFactoryTests.java b/spring-context/src/test/java/org/springframework/scripting/bsh/BshScriptFactoryTests.java new file mode 100644 index 0000000..3945971 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/bsh/BshScriptFactoryTests.java @@ -0,0 +1,332 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.bsh; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.support.AopUtils; +import org.springframework.aop.target.dynamic.Refreshable; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.core.NestedRuntimeException; +import org.springframework.scripting.Calculator; +import org.springframework.scripting.ConfigurableMessenger; +import org.springframework.scripting.Messenger; +import org.springframework.scripting.ScriptCompilationException; +import org.springframework.scripting.ScriptSource; +import org.springframework.scripting.TestBeanAwareMessenger; +import org.springframework.scripting.support.ScriptFactoryPostProcessor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * @author Rob Harrop + * @author Rick Evans + * @author Juergen Hoeller + */ +public class BshScriptFactoryTests { + + @Test + public void staticScript() { + ApplicationContext ctx = new ClassPathXmlApplicationContext("bshContext.xml", getClass()); + + assertThat(Arrays.asList(ctx.getBeanNamesForType(Calculator.class)).contains("calculator")).isTrue(); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("messenger")).isTrue(); + + Calculator calc = (Calculator) ctx.getBean("calculator"); + Messenger messenger = (Messenger) ctx.getBean("messenger"); + + boolean condition3 = calc instanceof Refreshable; + assertThat(condition3).as("Scripted object should not be instance of Refreshable").isFalse(); + boolean condition2 = messenger instanceof Refreshable; + assertThat(condition2).as("Scripted object should not be instance of Refreshable").isFalse(); + + assertThat(calc).isEqualTo(calc); + assertThat(messenger).isEqualTo(messenger); + boolean condition1 = !messenger.equals(calc); + assertThat(condition1).isTrue(); + assertThat(messenger.hashCode() != calc.hashCode()).isTrue(); + boolean condition = !messenger.toString().equals(calc.toString()); + assertThat(condition).isTrue(); + + assertThat(calc.add(2, 3)).isEqualTo(5); + + String desiredMessage = "Hello World!"; + assertThat(messenger.getMessage()).as("Message is incorrect").isEqualTo(desiredMessage); + + assertThat(ctx.getBeansOfType(Calculator.class).values().contains(calc)).isTrue(); + assertThat(ctx.getBeansOfType(Messenger.class).values().contains(messenger)).isTrue(); + } + + @Test + public void staticScriptWithNullReturnValue() { + ApplicationContext ctx = new ClassPathXmlApplicationContext("bshContext.xml", getClass()); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("messengerWithConfig")).isTrue(); + + ConfigurableMessenger messenger = (ConfigurableMessenger) ctx.getBean("messengerWithConfig"); + messenger.setMessage(null); + assertThat(messenger.getMessage()).isNull(); + assertThat(ctx.getBeansOfType(Messenger.class).values().contains(messenger)).isTrue(); + } + + @Test + public void staticScriptWithTwoInterfacesSpecified() { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("bshContext.xml", getClass()); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("messengerWithConfigExtra")).isTrue(); + + ConfigurableMessenger messenger = (ConfigurableMessenger) ctx.getBean("messengerWithConfigExtra"); + messenger.setMessage(null); + assertThat(messenger.getMessage()).isNull(); + assertThat(ctx.getBeansOfType(Messenger.class).values().contains(messenger)).isTrue(); + + ctx.close(); + assertThat(messenger.getMessage()).isNull(); + } + + @Test + public void staticWithScriptReturningInstance() { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("bshContext.xml", getClass()); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("messengerInstance")).isTrue(); + + Messenger messenger = (Messenger) ctx.getBean("messengerInstance"); + String desiredMessage = "Hello World!"; + assertThat(messenger.getMessage()).as("Message is incorrect").isEqualTo(desiredMessage); + assertThat(ctx.getBeansOfType(Messenger.class).values().contains(messenger)).isTrue(); + + ctx.close(); + assertThat(messenger.getMessage()).isNull(); + } + + @Test + public void staticScriptImplementingInterface() { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("bshContext.xml", getClass()); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("messengerImpl")).isTrue(); + + Messenger messenger = (Messenger) ctx.getBean("messengerImpl"); + String desiredMessage = "Hello World!"; + assertThat(messenger.getMessage()).as("Message is incorrect").isEqualTo(desiredMessage); + assertThat(ctx.getBeansOfType(Messenger.class).values().contains(messenger)).isTrue(); + + ctx.close(); + assertThat(messenger.getMessage()).isNull(); + } + + @Test + public void staticPrototypeScript() { + ApplicationContext ctx = new ClassPathXmlApplicationContext("bshContext.xml", getClass()); + ConfigurableMessenger messenger = (ConfigurableMessenger) ctx.getBean("messengerPrototype"); + ConfigurableMessenger messenger2 = (ConfigurableMessenger) ctx.getBean("messengerPrototype"); + + assertThat(AopUtils.isAopProxy(messenger)).as("Shouldn't get proxy when refresh is disabled").isFalse(); + boolean condition = messenger instanceof Refreshable; + assertThat(condition).as("Scripted object should not be instance of Refreshable").isFalse(); + + assertThat(messenger2).isNotSameAs(messenger); + assertThat(messenger2.getClass()).isSameAs(messenger.getClass()); + assertThat(messenger.getMessage()).isEqualTo("Hello World!"); + assertThat(messenger2.getMessage()).isEqualTo("Hello World!"); + messenger.setMessage("Bye World!"); + messenger2.setMessage("Byebye World!"); + assertThat(messenger.getMessage()).isEqualTo("Bye World!"); + assertThat(messenger2.getMessage()).isEqualTo("Byebye World!"); + } + + @Test + public void nonStaticScript() { + ApplicationContext ctx = new ClassPathXmlApplicationContext("bshRefreshableContext.xml", getClass()); + Messenger messenger = (Messenger) ctx.getBean("messenger"); + + assertThat(AopUtils.isAopProxy(messenger)).as("Should be a proxy for refreshable scripts").isTrue(); + boolean condition = messenger instanceof Refreshable; + assertThat(condition).as("Should be an instance of Refreshable").isTrue(); + + String desiredMessage = "Hello World!"; + assertThat(messenger.getMessage()).as("Message is incorrect").isEqualTo(desiredMessage); + + Refreshable refreshable = (Refreshable) messenger; + refreshable.refresh(); + + assertThat(messenger.getMessage()).as("Message is incorrect after refresh").isEqualTo(desiredMessage); + assertThat(refreshable.getRefreshCount()).as("Incorrect refresh count").isEqualTo(2); + } + + @Test + public void nonStaticPrototypeScript() { + ApplicationContext ctx = new ClassPathXmlApplicationContext("bshRefreshableContext.xml", getClass()); + ConfigurableMessenger messenger = (ConfigurableMessenger) ctx.getBean("messengerPrototype"); + ConfigurableMessenger messenger2 = (ConfigurableMessenger) ctx.getBean("messengerPrototype"); + + assertThat(AopUtils.isAopProxy(messenger)).as("Should be a proxy for refreshable scripts").isTrue(); + boolean condition = messenger instanceof Refreshable; + assertThat(condition).as("Should be an instance of Refreshable").isTrue(); + + assertThat(messenger.getMessage()).isEqualTo("Hello World!"); + assertThat(messenger2.getMessage()).isEqualTo("Hello World!"); + messenger.setMessage("Bye World!"); + messenger2.setMessage("Byebye World!"); + assertThat(messenger.getMessage()).isEqualTo("Bye World!"); + assertThat(messenger2.getMessage()).isEqualTo("Byebye World!"); + + Refreshable refreshable = (Refreshable) messenger; + refreshable.refresh(); + + assertThat(messenger.getMessage()).isEqualTo("Hello World!"); + assertThat(messenger2.getMessage()).isEqualTo("Byebye World!"); + assertThat(refreshable.getRefreshCount()).as("Incorrect refresh count").isEqualTo(2); + } + + @Test + public void scriptCompilationException() { + assertThatExceptionOfType(NestedRuntimeException.class).isThrownBy(() -> + new ClassPathXmlApplicationContext("org/springframework/scripting/bsh/bshBrokenContext.xml")) + .matches(ex -> ex.contains(ScriptCompilationException.class)); + } + + @Test + public void scriptThatCompilesButIsJustPlainBad() throws IOException { + ScriptSource script = mock(ScriptSource.class); + final String badScript = "String getMessage() { throw new IllegalArgumentException(); }"; + given(script.getScriptAsString()).willReturn(badScript); + given(script.isModified()).willReturn(true); + BshScriptFactory factory = new BshScriptFactory( + ScriptFactoryPostProcessor.INLINE_SCRIPT_PREFIX + badScript, Messenger.class); + assertThatExceptionOfType(BshScriptUtils.BshExecutionException.class).isThrownBy(() -> { + Messenger messenger = (Messenger) factory.getScriptedObject(script, Messenger.class); + messenger.getMessage(); + }); + } + + @Test + public void ctorWithNullScriptSourceLocator() { + assertThatIllegalArgumentException().isThrownBy(() -> + new BshScriptFactory(null, Messenger.class)); + } + + @Test + public void ctorWithEmptyScriptSourceLocator() { + assertThatIllegalArgumentException().isThrownBy(() -> + new BshScriptFactory("", Messenger.class)); + } + + @Test + public void ctorWithWhitespacedScriptSourceLocator() { + assertThatIllegalArgumentException().isThrownBy(() -> + new BshScriptFactory("\n ", Messenger.class)); + } + + @Test + public void resourceScriptFromTag() { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("bsh-with-xsd.xml", getClass()); + TestBean testBean = (TestBean) ctx.getBean("testBean"); + + Collection beanNames = Arrays.asList(ctx.getBeanNamesForType(Messenger.class)); + assertThat(beanNames.contains("messenger")).isTrue(); + assertThat(beanNames.contains("messengerImpl")).isTrue(); + assertThat(beanNames.contains("messengerInstance")).isTrue(); + + Messenger messenger = (Messenger) ctx.getBean("messenger"); + assertThat(messenger.getMessage()).isEqualTo("Hello World!"); + boolean condition = messenger instanceof Refreshable; + assertThat(condition).isFalse(); + + Messenger messengerImpl = (Messenger) ctx.getBean("messengerImpl"); + assertThat(messengerImpl.getMessage()).isEqualTo("Hello World!"); + + Messenger messengerInstance = (Messenger) ctx.getBean("messengerInstance"); + assertThat(messengerInstance.getMessage()).isEqualTo("Hello World!"); + + TestBeanAwareMessenger messengerByType = (TestBeanAwareMessenger) ctx.getBean("messengerByType"); + assertThat(messengerByType.getTestBean()).isEqualTo(testBean); + + TestBeanAwareMessenger messengerByName = (TestBeanAwareMessenger) ctx.getBean("messengerByName"); + assertThat(messengerByName.getTestBean()).isEqualTo(testBean); + + Collection beans = ctx.getBeansOfType(Messenger.class).values(); + assertThat(beans.contains(messenger)).isTrue(); + assertThat(beans.contains(messengerImpl)).isTrue(); + assertThat(beans.contains(messengerInstance)).isTrue(); + assertThat(beans.contains(messengerByType)).isTrue(); + assertThat(beans.contains(messengerByName)).isTrue(); + + ctx.close(); + assertThat(messenger.getMessage()).isNull(); + assertThat(messengerImpl.getMessage()).isNull(); + assertThat(messengerInstance.getMessage()).isNull(); + } + + @Test + public void prototypeScriptFromTag() { + ApplicationContext ctx = new ClassPathXmlApplicationContext("bsh-with-xsd.xml", getClass()); + ConfigurableMessenger messenger = (ConfigurableMessenger) ctx.getBean("messengerPrototype"); + ConfigurableMessenger messenger2 = (ConfigurableMessenger) ctx.getBean("messengerPrototype"); + + assertThat(messenger2).isNotSameAs(messenger); + assertThat(messenger2.getClass()).isSameAs(messenger.getClass()); + assertThat(messenger.getMessage()).isEqualTo("Hello World!"); + assertThat(messenger2.getMessage()).isEqualTo("Hello World!"); + messenger.setMessage("Bye World!"); + messenger2.setMessage("Byebye World!"); + assertThat(messenger.getMessage()).isEqualTo("Bye World!"); + assertThat(messenger2.getMessage()).isEqualTo("Byebye World!"); + } + + @Test + public void inlineScriptFromTag() { + ApplicationContext ctx = new ClassPathXmlApplicationContext("bsh-with-xsd.xml", getClass()); + Calculator calculator = (Calculator) ctx.getBean("calculator"); + assertThat(calculator).isNotNull(); + boolean condition = calculator instanceof Refreshable; + assertThat(condition).isFalse(); + } + + @Test + public void refreshableFromTag() { + ApplicationContext ctx = new ClassPathXmlApplicationContext("bsh-with-xsd.xml", getClass()); + Messenger messenger = (Messenger) ctx.getBean("refreshableMessenger"); + assertThat(messenger.getMessage()).isEqualTo("Hello World!"); + boolean condition = messenger instanceof Refreshable; + assertThat(condition).as("Messenger should be Refreshable").isTrue(); + } + + @Test + public void applicationEventListener() { + ApplicationContext ctx = new ClassPathXmlApplicationContext("bsh-with-xsd.xml", getClass()); + Messenger eventListener = (Messenger) ctx.getBean("eventListener"); + ctx.publishEvent(new MyEvent(ctx)); + assertThat(eventListener.getMessage()).isEqualTo("count=2"); + } + + + @SuppressWarnings("serial") + private static class MyEvent extends ApplicationEvent { + + public MyEvent(Object source) { + super(source); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/scripting/config/ITestBean.java b/spring-context/src/test/java/org/springframework/scripting/config/ITestBean.java new file mode 100644 index 0000000..138dc01 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/config/ITestBean.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2007 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.config; + +/** + * @author Mark Fisher + */ +public interface ITestBean { + + boolean isInitialized(); + + boolean isDestroyed(); + + ITestBean getOtherBean(); + +} diff --git a/spring-context/src/test/java/org/springframework/scripting/config/OtherTestBean.java b/spring-context/src/test/java/org/springframework/scripting/config/OtherTestBean.java new file mode 100644 index 0000000..b4f5805 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/config/OtherTestBean.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.config; + +/** + * @author Mark Fisher + */ +public class OtherTestBean implements ITestBean { + + @Override + public ITestBean getOtherBean() { + return null; + } + + @Override + public boolean isInitialized() { + return false; + } + + @Override + public boolean isDestroyed() { + return false; + } + +} diff --git a/spring-context/src/test/java/org/springframework/scripting/config/ScriptingDefaultsTests.java b/spring-context/src/test/java/org/springframework/scripting/config/ScriptingDefaultsTests.java new file mode 100644 index 0000000..07f9892 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/config/ScriptingDefaultsTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.config; + +import java.lang.reflect.Field; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.Advised; +import org.springframework.aop.support.AopUtils; +import org.springframework.aop.target.dynamic.AbstractRefreshableTargetSource; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Mark Fisher + * @author Dave Syer + */ +@SuppressWarnings("resource") +public class ScriptingDefaultsTests { + + private static final String CONFIG = + "org/springframework/scripting/config/scriptingDefaultsTests.xml"; + + private static final String PROXY_CONFIG = + "org/springframework/scripting/config/scriptingDefaultsProxyTargetClassTests.xml"; + + + @Test + public void defaultRefreshCheckDelay() throws Exception { + ApplicationContext context = new ClassPathXmlApplicationContext(CONFIG); + Advised advised = (Advised) context.getBean("testBean"); + AbstractRefreshableTargetSource targetSource = + ((AbstractRefreshableTargetSource) advised.getTargetSource()); + Field field = AbstractRefreshableTargetSource.class.getDeclaredField("refreshCheckDelay"); + field.setAccessible(true); + long delay = ((Long) field.get(targetSource)).longValue(); + assertThat(delay).isEqualTo(5000L); + } + + @Test + public void defaultInitMethod() { + ApplicationContext context = new ClassPathXmlApplicationContext(CONFIG); + ITestBean testBean = (ITestBean) context.getBean("testBean"); + assertThat(testBean.isInitialized()).isTrue(); + } + + @Test + public void nameAsAlias() { + ApplicationContext context = new ClassPathXmlApplicationContext(CONFIG); + ITestBean testBean = (ITestBean) context.getBean("/url"); + assertThat(testBean.isInitialized()).isTrue(); + } + + @Test + public void defaultDestroyMethod() { + ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(CONFIG); + ITestBean testBean = (ITestBean) context.getBean("nonRefreshableTestBean"); + assertThat(testBean.isDestroyed()).isFalse(); + context.close(); + assertThat(testBean.isDestroyed()).isTrue(); + } + + @Test + public void defaultAutowire() { + ApplicationContext context = new ClassPathXmlApplicationContext(CONFIG); + ITestBean testBean = (ITestBean) context.getBean("testBean"); + ITestBean otherBean = (ITestBean) context.getBean("otherBean"); + assertThat(testBean.getOtherBean()).isEqualTo(otherBean); + } + + @Test + public void defaultProxyTargetClass() { + ApplicationContext context = new ClassPathXmlApplicationContext(PROXY_CONFIG); + Object testBean = context.getBean("testBean"); + assertThat(AopUtils.isCglibProxy(testBean)).isTrue(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/scripting/groovy/ConcreteMessenger.java b/spring-context/src/test/java/org/springframework/scripting/groovy/ConcreteMessenger.java new file mode 100644 index 0000000..bde2b4e --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/groovy/ConcreteMessenger.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.groovy; + +import org.springframework.scripting.ConfigurableMessenger; + +/** + * @author Dave Syer + * + */ +public class ConcreteMessenger implements ConfigurableMessenger { + + private String message; + + @Override + public String getMessage() { + return message; + } + + @Override + public void setMessage(String message) { + this.message = message; + } + +} diff --git a/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyAspectIntegrationTests.java b/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyAspectIntegrationTests.java new file mode 100644 index 0000000..2e0a6f4 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyAspectIntegrationTests.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.groovy; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.context.support.GenericXmlApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Dave Syer + */ +public class GroovyAspectIntegrationTests { + + private GenericXmlApplicationContext context; + + @Test + public void testJavaBean() { + context = new GenericXmlApplicationContext(getClass(), getClass().getSimpleName()+"-java-context.xml"); + TestService bean = context.getBean("javaBean", TestService.class); + LogUserAdvice logAdvice = context.getBean(LogUserAdvice.class); + + assertThat(logAdvice.getCountThrows()).isEqualTo(0); + assertThatExceptionOfType(RuntimeException.class).isThrownBy( + bean::sayHello) + .withMessage("TestServiceImpl"); + assertThat(logAdvice.getCountThrows()).isEqualTo(1); + } + + @Test + public void testGroovyBeanInterface() { + context = new GenericXmlApplicationContext(getClass(), getClass().getSimpleName()+"-groovy-interface-context.xml"); + TestService bean = context.getBean("groovyBean", TestService.class); + LogUserAdvice logAdvice = context.getBean(LogUserAdvice.class); + + assertThat(logAdvice.getCountThrows()).isEqualTo(0); + assertThatExceptionOfType(RuntimeException.class).isThrownBy( + bean::sayHello) + .withMessage("GroovyServiceImpl"); + assertThat(logAdvice.getCountThrows()).isEqualTo(1); + } + + + @Test + public void testGroovyBeanDynamic() { + context = new GenericXmlApplicationContext(getClass(), getClass().getSimpleName()+"-groovy-dynamic-context.xml"); + TestService bean = context.getBean("groovyBean", TestService.class); + LogUserAdvice logAdvice = context.getBean(LogUserAdvice.class); + + assertThat(logAdvice.getCountThrows()).isEqualTo(0); + assertThatExceptionOfType(RuntimeException.class).isThrownBy( + bean::sayHello) + .withMessage("GroovyServiceImpl"); + // No proxy here because the pointcut only applies to the concrete class, not the interface + assertThat(logAdvice.getCountThrows()).isEqualTo(0); + assertThat(logAdvice.getCountBefore()).isEqualTo(0); + } + + @Test + public void testGroovyBeanProxyTargetClass() { + context = new GenericXmlApplicationContext(getClass(), getClass().getSimpleName()+"-groovy-proxy-target-class-context.xml"); + TestService bean = context.getBean("groovyBean", TestService.class); + LogUserAdvice logAdvice = context.getBean(LogUserAdvice.class); + + assertThat(logAdvice.getCountThrows()).isEqualTo(0); + assertThatExceptionOfType(RuntimeException.class).isThrownBy( + bean::sayHello) + .withMessage("GroovyServiceImpl"); + assertThat(logAdvice.getCountBefore()).isEqualTo(1); + assertThat(logAdvice.getCountThrows()).isEqualTo(1); + } + + @AfterEach + public void close() { + if (context != null) { + context.close(); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyAspectTests.java b/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyAspectTests.java new file mode 100644 index 0000000..6f1550f --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyAspectTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.groovy; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.Advisor; +import org.springframework.aop.aspectj.AspectJExpressionPointcut; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.core.io.ClassPathResource; +import org.springframework.scripting.support.ResourceScriptSource; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Dave Syer + * @author Sam Brannen + */ +public class GroovyAspectTests { + + private final LogUserAdvice logAdvice = new LogUserAdvice(); + + private final GroovyScriptFactory scriptFactory = new GroovyScriptFactory("GroovyServiceImpl.grv"); + + + @Test + public void manualGroovyBeanWithUnconditionalPointcut() throws Exception { + TestService target = (TestService) scriptFactory.getScriptedObject(new ResourceScriptSource( + new ClassPathResource("GroovyServiceImpl.grv", getClass()))); + + testAdvice(new DefaultPointcutAdvisor(logAdvice), logAdvice, target, "GroovyServiceImpl"); + } + + @Test + public void manualGroovyBeanWithStaticPointcut() throws Exception { + TestService target = (TestService) scriptFactory.getScriptedObject(new ResourceScriptSource( + new ClassPathResource("GroovyServiceImpl.grv", getClass()))); + + AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); + pointcut.setExpression(String.format("execution(* %s.TestService+.*(..))", ClassUtils.getPackageName(getClass()))); + testAdvice(new DefaultPointcutAdvisor(pointcut, logAdvice), logAdvice, target, "GroovyServiceImpl", true); + } + + @Test + public void manualGroovyBeanWithDynamicPointcut() throws Exception { + TestService target = (TestService) scriptFactory.getScriptedObject(new ResourceScriptSource( + new ClassPathResource("GroovyServiceImpl.grv", getClass()))); + + AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); + pointcut.setExpression(String.format("@within(%s.Log)", ClassUtils.getPackageName(getClass()))); + testAdvice(new DefaultPointcutAdvisor(pointcut, logAdvice), logAdvice, target, "GroovyServiceImpl", false); + } + + @Test + public void manualGroovyBeanWithDynamicPointcutProxyTargetClass() throws Exception { + TestService target = (TestService) scriptFactory.getScriptedObject(new ResourceScriptSource( + new ClassPathResource("GroovyServiceImpl.grv", getClass()))); + + AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); + pointcut.setExpression(String.format("@within(%s.Log)", ClassUtils.getPackageName(getClass()))); + testAdvice(new DefaultPointcutAdvisor(pointcut, logAdvice), logAdvice, target, "GroovyServiceImpl", true); + } + + private void testAdvice(Advisor advisor, LogUserAdvice logAdvice, TestService target, String message) + throws Exception { + + testAdvice(advisor, logAdvice, target, message, false); + } + + private void testAdvice(Advisor advisor, LogUserAdvice logAdvice, TestService target, String message, + boolean proxyTargetClass) throws Exception { + + logAdvice.reset(); + + ProxyFactory factory = new ProxyFactory(target); + factory.setProxyTargetClass(proxyTargetClass); + factory.addAdvisor(advisor); + TestService bean = (TestService) factory.getProxy(); + + assertThat(logAdvice.getCountThrows()).isEqualTo(0); + assertThatExceptionOfType(TestException.class).isThrownBy( + bean::sayHello) + .withMessage(message); + assertThat(logAdvice.getCountThrows()).isEqualTo(1); + } + +} diff --git a/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyClassLoadingTests.java b/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyClassLoadingTests.java new file mode 100644 index 0000000..5bf307f --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyClassLoadingTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.groovy; + +import java.lang.reflect.Method; + +import groovy.lang.GroovyClassLoader; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Mark Fisher + */ +public class GroovyClassLoadingTests { + + @Test + @SuppressWarnings("resource") + public void classLoading() throws Exception { + StaticApplicationContext context = new StaticApplicationContext(); + + GroovyClassLoader gcl = new GroovyClassLoader(); + Class class1 = gcl.parseClass("class TestBean { def myMethod() { \"foo\" } }"); + Class class2 = gcl.parseClass("class TestBean { def myMethod() { \"bar\" } }"); + + context.registerBeanDefinition("testBean", new RootBeanDefinition(class1)); + Object testBean1 = context.getBean("testBean"); + Method method1 = class1.getDeclaredMethod("myMethod", new Class[0]); + Object result1 = ReflectionUtils.invokeMethod(method1, testBean1); + assertThat(result1).isEqualTo("foo"); + + context.removeBeanDefinition("testBean"); + context.registerBeanDefinition("testBean", new RootBeanDefinition(class2)); + Object testBean2 = context.getBean("testBean"); + Method method2 = class2.getDeclaredMethod("myMethod", new Class[0]); + Object result2 = ReflectionUtils.invokeMethod(method2, testBean2); + assertThat(result2).isEqualTo("bar"); + } + +} diff --git a/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyScriptEvaluatorTests.java b/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyScriptEvaluatorTests.java new file mode 100644 index 0000000..4b584f0 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyScriptEvaluatorTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.groovy; + +import java.util.HashMap; +import java.util.Map; + +import org.codehaus.groovy.control.customizers.ImportCustomizer; +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.scripting.ScriptEvaluator; +import org.springframework.scripting.support.ResourceScriptSource; +import org.springframework.scripting.support.StandardScriptEvaluator; +import org.springframework.scripting.support.StaticScriptSource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + */ +public class GroovyScriptEvaluatorTests { + + @Test + public void testGroovyScriptFromString() { + ScriptEvaluator evaluator = new GroovyScriptEvaluator(); + Object result = evaluator.evaluate(new StaticScriptSource("return 3 * 2")); + assertThat(result).isEqualTo(6); + } + + @Test + public void testGroovyScriptFromFile() { + ScriptEvaluator evaluator = new GroovyScriptEvaluator(); + Object result = evaluator.evaluate(new ResourceScriptSource(new ClassPathResource("simple.groovy", getClass()))); + assertThat(result).isEqualTo(6); + } + + @Test + public void testGroovyScriptWithArguments() { + ScriptEvaluator evaluator = new GroovyScriptEvaluator(); + Map arguments = new HashMap<>(); + arguments.put("a", 3); + arguments.put("b", 2); + Object result = evaluator.evaluate(new StaticScriptSource("return a * b"), arguments); + assertThat(result).isEqualTo(6); + } + + @Test + public void testGroovyScriptWithCompilerConfiguration() { + GroovyScriptEvaluator evaluator = new GroovyScriptEvaluator(); + MyBytecodeProcessor processor = new MyBytecodeProcessor(); + evaluator.getCompilerConfiguration().setBytecodePostprocessor(processor); + Object result = evaluator.evaluate(new StaticScriptSource("return 3 * 2")); + assertThat(result).isEqualTo(6); + assertThat(processor.processed.contains("Script1")).isTrue(); + } + + @Test + public void testGroovyScriptWithImportCustomizer() { + GroovyScriptEvaluator evaluator = new GroovyScriptEvaluator(); + ImportCustomizer importCustomizer = new ImportCustomizer(); + importCustomizer.addStarImports("org.springframework.util"); + evaluator.setCompilationCustomizers(importCustomizer); + Object result = evaluator.evaluate(new StaticScriptSource("return ResourceUtils.CLASSPATH_URL_PREFIX")); + assertThat(result).isEqualTo("classpath:"); + } + + @Test + public void testGroovyScriptFromStringUsingJsr223() { + StandardScriptEvaluator evaluator = new StandardScriptEvaluator(); + evaluator.setLanguage("Groovy"); + Object result = evaluator.evaluate(new StaticScriptSource("return 3 * 2")); + assertThat(result).isEqualTo(6); + } + + @Test + public void testGroovyScriptFromFileUsingJsr223() { + ScriptEvaluator evaluator = new StandardScriptEvaluator(); + Object result = evaluator.evaluate(new ResourceScriptSource(new ClassPathResource("simple.groovy", getClass()))); + assertThat(result).isEqualTo(6); + } + + @Test + public void testGroovyScriptWithArgumentsUsingJsr223() { + StandardScriptEvaluator evaluator = new StandardScriptEvaluator(); + evaluator.setLanguage("Groovy"); + Map arguments = new HashMap<>(); + arguments.put("a", 3); + arguments.put("b", 2); + Object result = evaluator.evaluate(new StaticScriptSource("return a * b"), arguments); + assertThat(result).isEqualTo(6); + } + +} diff --git a/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyScriptFactoryTests.java b/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyScriptFactoryTests.java new file mode 100644 index 0000000..c657c9e --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyScriptFactoryTests.java @@ -0,0 +1,598 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.groovy; + +import java.io.FileNotFoundException; +import java.util.Arrays; +import java.util.Map; + +import groovy.lang.DelegatingMetaClass; +import groovy.lang.GroovyObject; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.support.AopUtils; +import org.springframework.aop.target.dynamic.Refreshable; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.core.NestedRuntimeException; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.scripting.Calculator; +import org.springframework.scripting.CallCounter; +import org.springframework.scripting.ConfigurableMessenger; +import org.springframework.scripting.ContextScriptBean; +import org.springframework.scripting.Messenger; +import org.springframework.scripting.ScriptCompilationException; +import org.springframework.scripting.ScriptSource; +import org.springframework.scripting.support.ScriptFactoryPostProcessor; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * @author Rob Harrop + * @author Rick Evans + * @author Rod Johnson + * @author Juergen Hoeller + * @author Mark Fisher + * @author Chris Beams + */ +@SuppressWarnings("resource") +public class GroovyScriptFactoryTests { + + @Test + public void testStaticScript() throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("groovyContext.xml", getClass()); + + assertThat(Arrays.asList(ctx.getBeanNamesForType(Calculator.class)).contains("calculator")).isTrue(); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("messenger")).isTrue(); + + Calculator calc = (Calculator) ctx.getBean("calculator"); + Messenger messenger = (Messenger) ctx.getBean("messenger"); + + assertThat(AopUtils.isAopProxy(calc)).as("Shouldn't get proxy when refresh is disabled").isFalse(); + assertThat(AopUtils.isAopProxy(messenger)).as("Shouldn't get proxy when refresh is disabled").isFalse(); + + boolean condition3 = calc instanceof Refreshable; + assertThat(condition3).as("Scripted object should not be instance of Refreshable").isFalse(); + boolean condition2 = messenger instanceof Refreshable; + assertThat(condition2).as("Scripted object should not be instance of Refreshable").isFalse(); + + assertThat(calc).isEqualTo(calc); + assertThat(messenger).isEqualTo(messenger); + boolean condition1 = !messenger.equals(calc); + assertThat(condition1).isTrue(); + assertThat(messenger.hashCode() != calc.hashCode()).isTrue(); + boolean condition = !messenger.toString().equals(calc.toString()); + assertThat(condition).isTrue(); + + String desiredMessage = "Hello World!"; + assertThat(messenger.getMessage()).as("Message is incorrect").isEqualTo(desiredMessage); + + assertThat(ctx.getBeansOfType(Calculator.class).values().contains(calc)).isTrue(); + assertThat(ctx.getBeansOfType(Messenger.class).values().contains(messenger)).isTrue(); + } + + @Test + public void testStaticScriptUsingJsr223() throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("groovyContextWithJsr223.xml", getClass()); + + assertThat(Arrays.asList(ctx.getBeanNamesForType(Calculator.class)).contains("calculator")).isTrue(); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("messenger")).isTrue(); + + Calculator calc = (Calculator) ctx.getBean("calculator"); + Messenger messenger = (Messenger) ctx.getBean("messenger"); + + assertThat(AopUtils.isAopProxy(calc)).as("Shouldn't get proxy when refresh is disabled").isFalse(); + assertThat(AopUtils.isAopProxy(messenger)).as("Shouldn't get proxy when refresh is disabled").isFalse(); + + boolean condition3 = calc instanceof Refreshable; + assertThat(condition3).as("Scripted object should not be instance of Refreshable").isFalse(); + boolean condition2 = messenger instanceof Refreshable; + assertThat(condition2).as("Scripted object should not be instance of Refreshable").isFalse(); + + assertThat(calc).isEqualTo(calc); + assertThat(messenger).isEqualTo(messenger); + boolean condition1 = !messenger.equals(calc); + assertThat(condition1).isTrue(); + assertThat(messenger.hashCode() != calc.hashCode()).isTrue(); + boolean condition = !messenger.toString().equals(calc.toString()); + assertThat(condition).isTrue(); + + String desiredMessage = "Hello World!"; + assertThat(messenger.getMessage()).as("Message is incorrect").isEqualTo(desiredMessage); + + assertThat(ctx.getBeansOfType(Calculator.class).values().contains(calc)).isTrue(); + assertThat(ctx.getBeansOfType(Messenger.class).values().contains(messenger)).isTrue(); + } + + @Test + public void testStaticPrototypeScript() throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("groovyContext.xml", getClass()); + ConfigurableMessenger messenger = (ConfigurableMessenger) ctx.getBean("messengerPrototype"); + ConfigurableMessenger messenger2 = (ConfigurableMessenger) ctx.getBean("messengerPrototype"); + + assertThat(AopUtils.isAopProxy(messenger)).as("Shouldn't get proxy when refresh is disabled").isFalse(); + boolean condition = messenger instanceof Refreshable; + assertThat(condition).as("Scripted object should not be instance of Refreshable").isFalse(); + + assertThat(messenger2).isNotSameAs(messenger); + assertThat(messenger2.getClass()).isSameAs(messenger.getClass()); + assertThat(messenger.getMessage()).isEqualTo("Hello World!"); + assertThat(messenger2.getMessage()).isEqualTo("Hello World!"); + messenger.setMessage("Bye World!"); + messenger2.setMessage("Byebye World!"); + assertThat(messenger.getMessage()).isEqualTo("Bye World!"); + assertThat(messenger2.getMessage()).isEqualTo("Byebye World!"); + } + + @Test + public void testStaticPrototypeScriptUsingJsr223() throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("groovyContextWithJsr223.xml", getClass()); + ConfigurableMessenger messenger = (ConfigurableMessenger) ctx.getBean("messengerPrototype"); + ConfigurableMessenger messenger2 = (ConfigurableMessenger) ctx.getBean("messengerPrototype"); + + assertThat(AopUtils.isAopProxy(messenger)).as("Shouldn't get proxy when refresh is disabled").isFalse(); + boolean condition = messenger instanceof Refreshable; + assertThat(condition).as("Scripted object should not be instance of Refreshable").isFalse(); + + assertThat(messenger2).isNotSameAs(messenger); + assertThat(messenger2.getClass()).isSameAs(messenger.getClass()); + assertThat(messenger.getMessage()).isEqualTo("Hello World!"); + assertThat(messenger2.getMessage()).isEqualTo("Hello World!"); + messenger.setMessage("Bye World!"); + messenger2.setMessage("Byebye World!"); + assertThat(messenger.getMessage()).isEqualTo("Bye World!"); + assertThat(messenger2.getMessage()).isEqualTo("Byebye World!"); + } + + @Test + public void testStaticScriptWithInstance() throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("groovyContext.xml", getClass()); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("messengerInstance")).isTrue(); + Messenger messenger = (Messenger) ctx.getBean("messengerInstance"); + + assertThat(AopUtils.isAopProxy(messenger)).as("Shouldn't get proxy when refresh is disabled").isFalse(); + boolean condition = messenger instanceof Refreshable; + assertThat(condition).as("Scripted object should not be instance of Refreshable").isFalse(); + + String desiredMessage = "Hello World!"; + assertThat(messenger.getMessage()).as("Message is incorrect").isEqualTo(desiredMessage); + assertThat(ctx.getBeansOfType(Messenger.class).values().contains(messenger)).isTrue(); + } + + @Test + public void testStaticScriptWithInstanceUsingJsr223() throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("groovyContextWithJsr223.xml", getClass()); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("messengerInstance")).isTrue(); + Messenger messenger = (Messenger) ctx.getBean("messengerInstance"); + + assertThat(AopUtils.isAopProxy(messenger)).as("Shouldn't get proxy when refresh is disabled").isFalse(); + boolean condition = messenger instanceof Refreshable; + assertThat(condition).as("Scripted object should not be instance of Refreshable").isFalse(); + + String desiredMessage = "Hello World!"; + assertThat(messenger.getMessage()).as("Message is incorrect").isEqualTo(desiredMessage); + assertThat(ctx.getBeansOfType(Messenger.class).values().contains(messenger)).isTrue(); + } + + @Test + public void testStaticScriptWithInlineDefinedInstance() throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("groovyContext.xml", getClass()); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("messengerInstanceInline")).isTrue(); + Messenger messenger = (Messenger) ctx.getBean("messengerInstanceInline"); + + assertThat(AopUtils.isAopProxy(messenger)).as("Shouldn't get proxy when refresh is disabled").isFalse(); + boolean condition = messenger instanceof Refreshable; + assertThat(condition).as("Scripted object should not be instance of Refreshable").isFalse(); + + String desiredMessage = "Hello World!"; + assertThat(messenger.getMessage()).as("Message is incorrect").isEqualTo(desiredMessage); + assertThat(ctx.getBeansOfType(Messenger.class).values().contains(messenger)).isTrue(); + } + + @Test + public void testStaticScriptWithInlineDefinedInstanceUsingJsr223() throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("groovyContextWithJsr223.xml", getClass()); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("messengerInstanceInline")).isTrue(); + Messenger messenger = (Messenger) ctx.getBean("messengerInstanceInline"); + + assertThat(AopUtils.isAopProxy(messenger)).as("Shouldn't get proxy when refresh is disabled").isFalse(); + boolean condition = messenger instanceof Refreshable; + assertThat(condition).as("Scripted object should not be instance of Refreshable").isFalse(); + + String desiredMessage = "Hello World!"; + assertThat(messenger.getMessage()).as("Message is incorrect").isEqualTo(desiredMessage); + assertThat(ctx.getBeansOfType(Messenger.class).values().contains(messenger)).isTrue(); + } + + @Test + public void testNonStaticScript() throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("groovyRefreshableContext.xml", getClass()); + Messenger messenger = (Messenger) ctx.getBean("messenger"); + + assertThat(AopUtils.isAopProxy(messenger)).as("Should be a proxy for refreshable scripts").isTrue(); + boolean condition = messenger instanceof Refreshable; + assertThat(condition).as("Should be an instance of Refreshable").isTrue(); + + String desiredMessage = "Hello World!"; + assertThat(messenger.getMessage()).as("Message is incorrect").isEqualTo(desiredMessage); + + Refreshable refreshable = (Refreshable) messenger; + refreshable.refresh(); + + assertThat(messenger.getMessage()).as("Message is incorrect after refresh.").isEqualTo(desiredMessage); + assertThat(refreshable.getRefreshCount()).as("Incorrect refresh count").isEqualTo(2); + } + + @Test + public void testNonStaticPrototypeScript() throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("groovyRefreshableContext.xml", getClass()); + ConfigurableMessenger messenger = (ConfigurableMessenger) ctx.getBean("messengerPrototype"); + ConfigurableMessenger messenger2 = (ConfigurableMessenger) ctx.getBean("messengerPrototype"); + + assertThat(AopUtils.isAopProxy(messenger)).as("Should be a proxy for refreshable scripts").isTrue(); + boolean condition = messenger instanceof Refreshable; + assertThat(condition).as("Should be an instance of Refreshable").isTrue(); + + assertThat(messenger.getMessage()).isEqualTo("Hello World!"); + assertThat(messenger2.getMessage()).isEqualTo("Hello World!"); + messenger.setMessage("Bye World!"); + messenger2.setMessage("Byebye World!"); + assertThat(messenger.getMessage()).isEqualTo("Bye World!"); + assertThat(messenger2.getMessage()).isEqualTo("Byebye World!"); + + Refreshable refreshable = (Refreshable) messenger; + refreshable.refresh(); + + assertThat(messenger.getMessage()).isEqualTo("Hello World!"); + assertThat(messenger2.getMessage()).isEqualTo("Byebye World!"); + assertThat(refreshable.getRefreshCount()).as("Incorrect refresh count").isEqualTo(2); + } + + @Test + public void testScriptCompilationException() throws Exception { + assertThatExceptionOfType(NestedRuntimeException.class).isThrownBy(() -> + new ClassPathXmlApplicationContext("org/springframework/scripting/groovy/groovyBrokenContext.xml")) + .matches(ex -> ex.contains(ScriptCompilationException.class)); + } + + @Test + public void testScriptedClassThatDoesNotHaveANoArgCtor() throws Exception { + ScriptSource script = mock(ScriptSource.class); + String badScript = "class Foo { public Foo(String foo) {}}"; + given(script.getScriptAsString()).willReturn(badScript); + given(script.suggestedClassName()).willReturn("someName"); + GroovyScriptFactory factory = new GroovyScriptFactory(ScriptFactoryPostProcessor.INLINE_SCRIPT_PREFIX + + badScript); + assertThatExceptionOfType(ScriptCompilationException.class).isThrownBy(() -> + factory.getScriptedObject(script)) + .matches(ex -> ex.contains(NoSuchMethodException.class)); + } + + @Test + public void testScriptedClassThatHasNoPublicNoArgCtor() throws Exception { + ScriptSource script = mock(ScriptSource.class); + String badScript = "class Foo { protected Foo() {} \n String toString() { 'X' }}"; + given(script.getScriptAsString()).willReturn(badScript); + given(script.suggestedClassName()).willReturn("someName"); + GroovyScriptFactory factory = new GroovyScriptFactory(ScriptFactoryPostProcessor.INLINE_SCRIPT_PREFIX + badScript); + assertThat(factory.getScriptedObject(script).toString()).isEqualTo("X"); + } + + @Test + public void testWithTwoClassesDefinedInTheOneGroovyFile_CorrectClassFirst() throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("twoClassesCorrectOneFirst.xml", getClass()); + Messenger messenger = (Messenger) ctx.getBean("messenger"); + assertThat(messenger).isNotNull(); + assertThat(messenger.getMessage()).isEqualTo("Hello World!"); + + // Check can cast to GroovyObject + GroovyObject goo = (GroovyObject) messenger; + assertThat(goo).isNotNull(); + } + + @Test + public void testWithTwoClassesDefinedInTheOneGroovyFile_WrongClassFirst() throws Exception { + assertThatExceptionOfType(Exception.class).as("two classes defined in GroovyScriptFactory source, non-Messenger class defined first").isThrownBy(() -> { + ApplicationContext ctx = new ClassPathXmlApplicationContext("twoClassesWrongOneFirst.xml", getClass()); + ctx.getBean("messenger", Messenger.class); + }); + } + + @Test + public void testCtorWithNullScriptSourceLocator() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new GroovyScriptFactory(null)); + } + + @Test + public void testCtorWithEmptyScriptSourceLocator() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new GroovyScriptFactory("")); + } + + @Test + public void testCtorWithWhitespacedScriptSourceLocator() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new GroovyScriptFactory("\n ")); + } + + @Test + public void testWithInlineScriptWithLeadingWhitespace() throws Exception { + assertThatExceptionOfType(BeanCreationException.class).as("'inline:' prefix was preceded by whitespace").isThrownBy(() -> + new ClassPathXmlApplicationContext("lwspBadGroovyContext.xml", getClass())) + .matches(ex -> ex.contains(FileNotFoundException.class)); + } + + @Test + public void testGetScriptedObjectDoesNotChokeOnNullInterfacesBeingPassedIn() throws Exception { + ScriptSource script = mock(ScriptSource.class); + given(script.getScriptAsString()).willReturn("class Bar {}"); + given(script.suggestedClassName()).willReturn("someName"); + + GroovyScriptFactory factory = new GroovyScriptFactory("a script source locator (doesn't matter here)"); + Object scriptedObject = factory.getScriptedObject(script); + assertThat(scriptedObject).isNotNull(); + } + + @Test + public void testGetScriptedObjectDoesChokeOnNullScriptSourceBeingPassedIn() throws Exception { + GroovyScriptFactory factory = new GroovyScriptFactory("a script source locator (doesn't matter here)"); + assertThatNullPointerException().as("NullPointerException as per contract ('null' ScriptSource supplied)").isThrownBy(() -> + factory.getScriptedObject(null)); + } + + @Test + public void testResourceScriptFromTag() throws Exception { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("groovy-with-xsd.xml", getClass()); + Messenger messenger = (Messenger) ctx.getBean("messenger"); + CallCounter countingAspect = (CallCounter) ctx.getBean("getMessageAspect"); + + assertThat(AopUtils.isAopProxy(messenger)).isTrue(); + boolean condition = messenger instanceof Refreshable; + assertThat(condition).isFalse(); + assertThat(countingAspect.getCalls()).isEqualTo(0); + assertThat(messenger.getMessage()).isEqualTo("Hello World!"); + assertThat(countingAspect.getCalls()).isEqualTo(1); + + ctx.close(); + assertThat(countingAspect.getCalls()).isEqualTo(-200); + } + + @Test + public void testPrototypeScriptFromTag() throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("groovy-with-xsd.xml", getClass()); + ConfigurableMessenger messenger = (ConfigurableMessenger) ctx.getBean("messengerPrototype"); + ConfigurableMessenger messenger2 = (ConfigurableMessenger) ctx.getBean("messengerPrototype"); + + assertThat(messenger2).isNotSameAs(messenger); + assertThat(messenger2.getClass()).isSameAs(messenger.getClass()); + assertThat(messenger.getMessage()).isEqualTo("Hello World!"); + assertThat(messenger2.getMessage()).isEqualTo("Hello World!"); + messenger.setMessage("Bye World!"); + messenger2.setMessage("Byebye World!"); + assertThat(messenger.getMessage()).isEqualTo("Bye World!"); + assertThat(messenger2.getMessage()).isEqualTo("Byebye World!"); + } + + @Test + public void testInlineScriptFromTag() throws Exception { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("groovy-with-xsd.xml", getClass()); + BeanDefinition bd = ctx.getBeanFactory().getBeanDefinition("calculator"); + assertThat(ObjectUtils.containsElement(bd.getDependsOn(), "messenger")).isTrue(); + Calculator calculator = (Calculator) ctx.getBean("calculator"); + assertThat(calculator).isNotNull(); + boolean condition = calculator instanceof Refreshable; + assertThat(condition).isFalse(); + } + + @Test + public void testRefreshableFromTag() throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("groovy-with-xsd.xml", getClass()); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("refreshableMessenger")).isTrue(); + + Messenger messenger = (Messenger) ctx.getBean("refreshableMessenger"); + CallCounter countingAspect = (CallCounter) ctx.getBean("getMessageAspect"); + + assertThat(AopUtils.isAopProxy(messenger)).isTrue(); + boolean condition = messenger instanceof Refreshable; + assertThat(condition).isTrue(); + assertThat(countingAspect.getCalls()).isEqualTo(0); + assertThat(messenger.getMessage()).isEqualTo("Hello World!"); + assertThat(countingAspect.getCalls()).isEqualTo(1); + + assertThat(ctx.getBeansOfType(Messenger.class).values().contains(messenger)).isTrue(); + } + + @Test // SPR-6268 + public void testRefreshableFromTagProxyTargetClass() throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("groovy-with-xsd-proxy-target-class.xml", + getClass()); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("refreshableMessenger")).isTrue(); + + Messenger messenger = (Messenger) ctx.getBean("refreshableMessenger"); + + assertThat(AopUtils.isAopProxy(messenger)).isTrue(); + boolean condition = messenger instanceof Refreshable; + assertThat(condition).isTrue(); + assertThat(messenger.getMessage()).isEqualTo("Hello World!"); + + assertThat(ctx.getBeansOfType(ConcreteMessenger.class).values().contains(messenger)).isTrue(); + + // Check that AnnotationUtils works with concrete proxied script classes + assertThat(AnnotationUtils.findAnnotation(messenger.getClass(), Component.class)).isNotNull(); + } + + @Test // SPR-6268 + public void testProxyTargetClassNotAllowedIfNotGroovy() throws Exception { + try { + new ClassPathXmlApplicationContext("groovy-with-xsd-proxy-target-class.xml", getClass()); + } + catch (BeanCreationException ex) { + assertThat(ex.getMessage().contains("Cannot use proxyTargetClass=true")).isTrue(); + } + } + + @Test + public void testAnonymousScriptDetected() throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("groovy-with-xsd.xml", getClass()); + Map beans = ctx.getBeansOfType(Messenger.class); + assertThat(beans.size()).isEqualTo(4); + assertThat(ctx.getBean(MyBytecodeProcessor.class).processed.contains( + "org.springframework.scripting.groovy.GroovyMessenger2")).isTrue(); + } + + @Test + public void testJsr223FromTag() throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("groovy-with-xsd-jsr223.xml", getClass()); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("messenger")).isTrue(); + Messenger messenger = (Messenger) ctx.getBean("messenger"); + assertThat(AopUtils.isAopProxy(messenger)).isFalse(); + assertThat(messenger.getMessage()).isEqualTo("Hello World!"); + } + + @Test + public void testJsr223FromTagWithInterface() throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("groovy-with-xsd-jsr223.xml", getClass()); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("messengerWithInterface")).isTrue(); + Messenger messenger = (Messenger) ctx.getBean("messengerWithInterface"); + assertThat(AopUtils.isAopProxy(messenger)).isFalse(); + } + + @Test + public void testRefreshableJsr223FromTag() throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("groovy-with-xsd-jsr223.xml", getClass()); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("refreshableMessenger")).isTrue(); + Messenger messenger = (Messenger) ctx.getBean("refreshableMessenger"); + assertThat(AopUtils.isAopProxy(messenger)).isTrue(); + boolean condition = messenger instanceof Refreshable; + assertThat(condition).isTrue(); + assertThat(messenger.getMessage()).isEqualTo("Hello World!"); + } + + @Test + public void testInlineJsr223FromTag() throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("groovy-with-xsd-jsr223.xml", getClass()); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("inlineMessenger")).isTrue(); + Messenger messenger = (Messenger) ctx.getBean("inlineMessenger"); + assertThat(AopUtils.isAopProxy(messenger)).isFalse(); + } + + @Test + public void testInlineJsr223FromTagWithInterface() throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("groovy-with-xsd-jsr223.xml", getClass()); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("inlineMessengerWithInterface")).isTrue(); + Messenger messenger = (Messenger) ctx.getBean("inlineMessengerWithInterface"); + assertThat(AopUtils.isAopProxy(messenger)).isFalse(); + } + + /** + * Tests the SPR-2098 bug whereby no more than 1 property element could be + * passed to a scripted bean :( + */ + @Test + public void testCanPassInMoreThanOneProperty() { + ApplicationContext ctx = new ClassPathXmlApplicationContext("groovy-multiple-properties.xml", getClass()); + TestBean tb = (TestBean) ctx.getBean("testBean"); + + ContextScriptBean bean = (ContextScriptBean) ctx.getBean("bean"); + assertThat(bean.getName()).as("The first property ain't bein' injected.").isEqualTo("Sophie Marceau"); + assertThat(bean.getAge()).as("The second property ain't bein' injected.").isEqualTo(31); + assertThat(bean.getTestBean()).isEqualTo(tb); + assertThat(bean.getApplicationContext()).isEqualTo(ctx); + + ContextScriptBean bean2 = (ContextScriptBean) ctx.getBean("bean2"); + assertThat(bean2.getTestBean()).isEqualTo(tb); + assertThat(bean2.getApplicationContext()).isEqualTo(ctx); + } + + @Test + public void testMetaClassWithBeans() { + testMetaClass("org/springframework/scripting/groovy/calculators.xml"); + } + + @Test + public void testMetaClassWithXsd() { + testMetaClass("org/springframework/scripting/groovy/calculators-with-xsd.xml"); + } + + private void testMetaClass(String xmlFile) { + // expect the exception we threw in the custom metaclass to show it got invoked + ApplicationContext ctx = new ClassPathXmlApplicationContext(xmlFile); + Calculator calc = (Calculator) ctx.getBean("delegatingCalculator"); + assertThatIllegalStateException().isThrownBy(() -> + calc.add(1, 2)) + .withMessage("Gotcha"); + } + + @Test + public void testFactoryBean() { + ApplicationContext context = new ClassPathXmlApplicationContext("groovyContext.xml", getClass()); + Object factory = context.getBean("&factory"); + boolean condition1 = factory instanceof FactoryBean; + assertThat(condition1).isTrue(); + Object result = context.getBean("factory"); + boolean condition = result instanceof String; + assertThat(condition).isTrue(); + assertThat(result).isEqualTo("test"); + } + + @Test + public void testRefreshableFactoryBean() { + ApplicationContext context = new ClassPathXmlApplicationContext("groovyContext.xml", getClass()); + Object factory = context.getBean("&refreshableFactory"); + boolean condition1 = factory instanceof FactoryBean; + assertThat(condition1).isTrue(); + Object result = context.getBean("refreshableFactory"); + boolean condition = result instanceof String; + assertThat(condition).isTrue(); + assertThat(result).isEqualTo("test"); + } + + + public static class TestCustomizer implements GroovyObjectCustomizer { + + @Override + public void customize(GroovyObject goo) { + DelegatingMetaClass dmc = new DelegatingMetaClass(goo.getMetaClass()) { + @Override + public Object invokeMethod(Object arg0, String mName, Object[] arg2) { + if (mName.contains("Missing")) { + throw new IllegalStateException("Gotcha"); + } + else { + return super.invokeMethod(arg0, mName, arg2); + } + } + }; + dmc.initialize(); + goo.setMetaClass(dmc); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/scripting/groovy/Log.java b/spring-context/src/test/java/org/springframework/scripting/groovy/Log.java new file mode 100644 index 0000000..905d651 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/groovy/Log.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.groovy; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface Log { +} diff --git a/spring-context/src/test/java/org/springframework/scripting/groovy/LogUserAdvice.java b/spring-context/src/test/java/org/springframework/scripting/groovy/LogUserAdvice.java new file mode 100644 index 0000000..841605e --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/groovy/LogUserAdvice.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.groovy; + +import java.lang.reflect.Method; + +import org.springframework.aop.MethodBeforeAdvice; +import org.springframework.aop.ThrowsAdvice; +import org.springframework.lang.Nullable; + +public class LogUserAdvice implements MethodBeforeAdvice, ThrowsAdvice { + + private int countBefore = 0; + + private int countThrows = 0; + + @Override + public void before(Method method, Object[] objects, @Nullable Object o) throws Throwable { + countBefore++; + // System.out.println("Method:" + method.getName()); + } + + public void afterThrowing(Exception e) throws Throwable { + countThrows++; + // System.out.println("***********************************************************************************"); + // System.out.println("Exception caught:"); + // System.out.println("***********************************************************************************"); + // e.printStackTrace(); + throw e; + } + + public int getCountBefore() { + return countBefore; + } + + public int getCountThrows() { + return countThrows; + } + + public void reset() { + countThrows = 0; + countBefore = 0; + } + +} diff --git a/spring-context/src/test/java/org/springframework/scripting/groovy/MyBytecodeProcessor.java b/spring-context/src/test/java/org/springframework/scripting/groovy/MyBytecodeProcessor.java new file mode 100644 index 0000000..fc73d71 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/groovy/MyBytecodeProcessor.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.groovy; + +import java.util.HashSet; +import java.util.Set; + +import org.codehaus.groovy.control.BytecodeProcessor; + +/** + * @author Juergen Hoeller + */ +public class MyBytecodeProcessor implements BytecodeProcessor { + + public final Set processed = new HashSet(); + + @Override + public byte[] processBytecode(String name, byte[] original) { + this.processed.add(name); + return original; + } + +} diff --git a/spring-context/src/test/java/org/springframework/scripting/groovy/MyImportCustomizer.java b/spring-context/src/test/java/org/springframework/scripting/groovy/MyImportCustomizer.java new file mode 100644 index 0000000..c99c85c --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/groovy/MyImportCustomizer.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.groovy; + +import org.codehaus.groovy.control.customizers.ImportCustomizer; + +/** + * @author Juergen Hoeller + */ +public class MyImportCustomizer extends ImportCustomizer { + + public MyImportCustomizer() { + addStarImports("org.springframework.scripting.groovy"); + } + +} diff --git a/spring-context/src/test/java/org/springframework/scripting/groovy/TestException.java b/spring-context/src/test/java/org/springframework/scripting/groovy/TestException.java new file mode 100644 index 0000000..3279e08 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/groovy/TestException.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.groovy; + +/** + * @author Dave Syer + * + */ +@SuppressWarnings("serial") +public class TestException extends RuntimeException { + + public TestException(String string) { + super(string); + } + +} diff --git a/spring-context/src/test/java/org/springframework/scripting/groovy/TestService.java b/spring-context/src/test/java/org/springframework/scripting/groovy/TestService.java new file mode 100644 index 0000000..aedebb2 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/groovy/TestService.java @@ -0,0 +1,22 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.groovy; + +public interface TestService { + + public String sayHello(); +} diff --git a/spring-context/src/test/java/org/springframework/scripting/groovy/TestServiceImpl.java b/spring-context/src/test/java/org/springframework/scripting/groovy/TestServiceImpl.java new file mode 100644 index 0000000..2e20636 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/groovy/TestServiceImpl.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.groovy; + +@Log +public class TestServiceImpl implements TestService{ + + @Override + public String sayHello() { + throw new TestException("TestServiceImpl"); + } +} diff --git a/spring-context/src/test/java/org/springframework/scripting/support/RefreshableScriptTargetSourceTests.java b/spring-context/src/test/java/org/springframework/scripting/support/RefreshableScriptTargetSourceTests.java new file mode 100644 index 0000000..2d81f5d --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/support/RefreshableScriptTargetSourceTests.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.support; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanFactory; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.mock; + +/** + * @author Rick Evans + */ +public class RefreshableScriptTargetSourceTests { + + @Test + public void createWithNullScriptSource() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new RefreshableScriptTargetSource(mock(BeanFactory.class), "a.bean", null, null, false)); + } + +} diff --git a/spring-context/src/test/java/org/springframework/scripting/support/ResourceScriptSourceTests.java b/spring-context/src/test/java/org/springframework/scripting/support/ResourceScriptSourceTests.java new file mode 100644 index 0000000..a5ab6c5 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/support/ResourceScriptSourceTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.support; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; +import org.springframework.util.StreamUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * @author Rick Evans + * @author Juergen Hoeller + */ +public class ResourceScriptSourceTests { + + @Test + public void doesNotPropagateFatalExceptionOnResourceThatCannotBeResolvedToAFile() throws Exception { + Resource resource = mock(Resource.class); + given(resource.lastModified()).willThrow(new IOException()); + + ResourceScriptSource scriptSource = new ResourceScriptSource(resource); + long lastModified = scriptSource.retrieveLastModifiedTime(); + assertThat(lastModified).isEqualTo(0); + } + + @Test + public void beginsInModifiedState() throws Exception { + Resource resource = mock(Resource.class); + ResourceScriptSource scriptSource = new ResourceScriptSource(resource); + assertThat(scriptSource.isModified()).isTrue(); + } + + @Test + public void lastModifiedWorksWithResourceThatDoesNotSupportFileBasedReading() throws Exception { + Resource resource = mock(Resource.class); + // underlying File is asked for so that the last modified time can be checked... + // And then mock the file changing; i.e. the File says it has been modified + given(resource.lastModified()).willReturn(100L, 100L, 200L); + // does not support File-based reading; delegates to InputStream-style reading... + //resource.getFile(); + //mock.setThrowable(new FileNotFoundException()); + given(resource.getInputStream()).willReturn(StreamUtils.emptyInput()); + + ResourceScriptSource scriptSource = new ResourceScriptSource(resource); + assertThat(scriptSource.isModified()).as("ResourceScriptSource must start off in the 'isModified' state (it obviously isn't).").isTrue(); + scriptSource.getScriptAsString(); + assertThat(scriptSource.isModified()).as("ResourceScriptSource must not report back as being modified if the underlying File resource is not reporting a changed lastModified time.").isFalse(); + // Must now report back as having been modified + assertThat(scriptSource.isModified()).as("ResourceScriptSource must report back as being modified if the underlying File resource is reporting a changed lastModified time.").isTrue(); + } + + @Test + public void lastModifiedWorksWithResourceThatDoesNotSupportFileBasedAccessAtAll() throws Exception { + Resource resource = new ByteArrayResource(new byte[0]); + ResourceScriptSource scriptSource = new ResourceScriptSource(resource); + assertThat(scriptSource.isModified()).as("ResourceScriptSource must start off in the 'isModified' state (it obviously isn't).").isTrue(); + scriptSource.getScriptAsString(); + assertThat(scriptSource.isModified()).as("ResourceScriptSource must not report back as being modified if the underlying File resource is not reporting a changed lastModified time.").isFalse(); + // Must now continue to report back as not having been modified 'cos the Resource does not support access as a File (and so the lastModified date cannot be determined). + assertThat(scriptSource.isModified()).as("ResourceScriptSource must not report back as being modified if the underlying File resource is not reporting a changed lastModified time.").isFalse(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/scripting/support/ScriptFactoryPostProcessorTests.java b/spring-context/src/test/java/org/springframework/scripting/support/ScriptFactoryPostProcessorTests.java new file mode 100644 index 0000000..1625323 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/support/ScriptFactoryPostProcessorTests.java @@ -0,0 +1,282 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.support; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.FatalBeanException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.testfixture.EnabledForTestGroups; +import org.springframework.scripting.Messenger; +import org.springframework.scripting.ScriptCompilationException; +import org.springframework.scripting.groovy.GroovyScriptFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.Mockito.mock; +import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; + +/** + * @author Rick Evans + * @author Juergen Hoeller + * @author Chris Beams + */ +@EnabledForTestGroups(LONG_RUNNING) +public class ScriptFactoryPostProcessorTests { + + private static final String MESSAGE_TEXT = "Bingo"; + + private static final String MESSENGER_BEAN_NAME = "messenger"; + + private static final String PROCESSOR_BEAN_NAME = "processor"; + + private static final String CHANGED_SCRIPT = "package org.springframework.scripting.groovy\n" + + "import org.springframework.scripting.Messenger\n" + + "class GroovyMessenger implements Messenger {\n" + + " private String message = \"Bingo\"\n" + + " public String getMessage() {\n" + + // quote the returned message (this is the change)... + " return \"'\" + this.message + \"'\"\n" + + " }\n" + + " public void setMessage(String message) {\n" + + " this.message = message\n" + + " }\n" + + "}"; + + private static final String EXPECTED_CHANGED_MESSAGE_TEXT = "'" + MESSAGE_TEXT + "'"; + + private static final int DEFAULT_SECONDS_TO_PAUSE = 1; + + private static final String DELEGATING_SCRIPT = "inline:package org.springframework.scripting;\n" + + "class DelegatingMessenger implements Messenger {\n" + + " private Messenger wrappedMessenger;\n" + + " public String getMessage() {\n" + + " return this.wrappedMessenger.getMessage()\n" + + " }\n" + + " public void setMessenger(Messenger wrappedMessenger) {\n" + + " this.wrappedMessenger = wrappedMessenger\n" + + " }\n" + + "}"; + + + @Test + public void testDoesNothingWhenPostProcessingNonScriptFactoryTypeBeforeInstantiation() throws Exception { + assertThat(new ScriptFactoryPostProcessor().postProcessBeforeInstantiation(getClass(), "a.bean")).isNull(); + } + + @Test + public void testThrowsExceptionIfGivenNonAbstractBeanFactoryImplementation() throws Exception { + assertThatIllegalStateException().isThrownBy(() -> + new ScriptFactoryPostProcessor().setBeanFactory(mock(BeanFactory.class))); + } + + @Test + public void testChangeScriptWithRefreshableBeanFunctionality() throws Exception { + BeanDefinition processorBeanDefinition = createScriptFactoryPostProcessor(true); + BeanDefinition scriptedBeanDefinition = createScriptedGroovyBean(); + + GenericApplicationContext ctx = new GenericApplicationContext(); + ctx.registerBeanDefinition(PROCESSOR_BEAN_NAME, processorBeanDefinition); + ctx.registerBeanDefinition(MESSENGER_BEAN_NAME, scriptedBeanDefinition); + ctx.refresh(); + + Messenger messenger = (Messenger) ctx.getBean(MESSENGER_BEAN_NAME); + assertThat(messenger.getMessage()).isEqualTo(MESSAGE_TEXT); + // cool; now let's change the script and check the refresh behaviour... + pauseToLetRefreshDelayKickIn(DEFAULT_SECONDS_TO_PAUSE); + StaticScriptSource source = getScriptSource(ctx); + source.setScript(CHANGED_SCRIPT); + Messenger refreshedMessenger = (Messenger) ctx.getBean(MESSENGER_BEAN_NAME); + // the updated script surrounds the message in quotes before returning... + assertThat(refreshedMessenger.getMessage()).isEqualTo(EXPECTED_CHANGED_MESSAGE_TEXT); + } + + @Test + public void testChangeScriptWithNoRefreshableBeanFunctionality() throws Exception { + BeanDefinition processorBeanDefinition = createScriptFactoryPostProcessor(false); + BeanDefinition scriptedBeanDefinition = createScriptedGroovyBean(); + + GenericApplicationContext ctx = new GenericApplicationContext(); + ctx.registerBeanDefinition(PROCESSOR_BEAN_NAME, processorBeanDefinition); + ctx.registerBeanDefinition(MESSENGER_BEAN_NAME, scriptedBeanDefinition); + ctx.refresh(); + + Messenger messenger = (Messenger) ctx.getBean(MESSENGER_BEAN_NAME); + assertThat(messenger.getMessage()).isEqualTo(MESSAGE_TEXT); + // cool; now let's change the script and check the refresh behaviour... + pauseToLetRefreshDelayKickIn(DEFAULT_SECONDS_TO_PAUSE); + StaticScriptSource source = getScriptSource(ctx); + source.setScript(CHANGED_SCRIPT); + Messenger refreshedMessenger = (Messenger) ctx.getBean(MESSENGER_BEAN_NAME); + assertThat(refreshedMessenger.getMessage()).as("Script seems to have been refreshed (must not be as no refreshCheckDelay set on ScriptFactoryPostProcessor)").isEqualTo(MESSAGE_TEXT); + } + + @Test + public void testRefreshedScriptReferencePropagatesToCollaborators() throws Exception { + BeanDefinition processorBeanDefinition = createScriptFactoryPostProcessor(true); + BeanDefinition scriptedBeanDefinition = createScriptedGroovyBean(); + BeanDefinitionBuilder collaboratorBuilder = BeanDefinitionBuilder.rootBeanDefinition(DefaultMessengerService.class); + collaboratorBuilder.addPropertyReference(MESSENGER_BEAN_NAME, MESSENGER_BEAN_NAME); + + GenericApplicationContext ctx = new GenericApplicationContext(); + ctx.registerBeanDefinition(PROCESSOR_BEAN_NAME, processorBeanDefinition); + ctx.registerBeanDefinition(MESSENGER_BEAN_NAME, scriptedBeanDefinition); + final String collaboratorBeanName = "collaborator"; + ctx.registerBeanDefinition(collaboratorBeanName, collaboratorBuilder.getBeanDefinition()); + ctx.refresh(); + + Messenger messenger = (Messenger) ctx.getBean(MESSENGER_BEAN_NAME); + assertThat(messenger.getMessage()).isEqualTo(MESSAGE_TEXT); + // cool; now let's change the script and check the refresh behaviour... + pauseToLetRefreshDelayKickIn(DEFAULT_SECONDS_TO_PAUSE); + StaticScriptSource source = getScriptSource(ctx); + source.setScript(CHANGED_SCRIPT); + Messenger refreshedMessenger = (Messenger) ctx.getBean(MESSENGER_BEAN_NAME); + // the updated script surrounds the message in quotes before returning... + assertThat(refreshedMessenger.getMessage()).isEqualTo(EXPECTED_CHANGED_MESSAGE_TEXT); + // ok, is this change reflected in the reference that the collaborator has? + DefaultMessengerService collaborator = (DefaultMessengerService) ctx.getBean(collaboratorBeanName); + assertThat(collaborator.getMessage()).isEqualTo(EXPECTED_CHANGED_MESSAGE_TEXT); + } + + @Test + public void testReferencesAcrossAContainerHierarchy() throws Exception { + GenericApplicationContext businessContext = new GenericApplicationContext(); + businessContext.registerBeanDefinition("messenger", BeanDefinitionBuilder.rootBeanDefinition(StubMessenger.class).getBeanDefinition()); + businessContext.refresh(); + + BeanDefinitionBuilder scriptedBeanBuilder = BeanDefinitionBuilder.rootBeanDefinition(GroovyScriptFactory.class); + scriptedBeanBuilder.addConstructorArgValue(DELEGATING_SCRIPT); + scriptedBeanBuilder.addPropertyReference("messenger", "messenger"); + + GenericApplicationContext presentationCtx = new GenericApplicationContext(businessContext); + presentationCtx.registerBeanDefinition("needsMessenger", scriptedBeanBuilder.getBeanDefinition()); + presentationCtx.registerBeanDefinition("scriptProcessor", createScriptFactoryPostProcessor(true)); + presentationCtx.refresh(); + } + + @Test + public void testScriptHavingAReferenceToAnotherBean() throws Exception { + // just tests that the (singleton) script-backed bean is able to be instantiated with references to its collaborators + new ClassPathXmlApplicationContext("org/springframework/scripting/support/groovyReferences.xml"); + } + + @Test + public void testForRefreshedScriptHavingErrorPickedUpOnFirstCall() throws Exception { + BeanDefinition processorBeanDefinition = createScriptFactoryPostProcessor(true); + BeanDefinition scriptedBeanDefinition = createScriptedGroovyBean(); + BeanDefinitionBuilder collaboratorBuilder = BeanDefinitionBuilder.rootBeanDefinition(DefaultMessengerService.class); + collaboratorBuilder.addPropertyReference(MESSENGER_BEAN_NAME, MESSENGER_BEAN_NAME); + + GenericApplicationContext ctx = new GenericApplicationContext(); + ctx.registerBeanDefinition(PROCESSOR_BEAN_NAME, processorBeanDefinition); + ctx.registerBeanDefinition(MESSENGER_BEAN_NAME, scriptedBeanDefinition); + final String collaboratorBeanName = "collaborator"; + ctx.registerBeanDefinition(collaboratorBeanName, collaboratorBuilder.getBeanDefinition()); + ctx.refresh(); + + Messenger messenger = (Messenger) ctx.getBean(MESSENGER_BEAN_NAME); + assertThat(messenger.getMessage()).isEqualTo(MESSAGE_TEXT); + // cool; now let's change the script and check the refresh behaviour... + pauseToLetRefreshDelayKickIn(DEFAULT_SECONDS_TO_PAUSE); + StaticScriptSource source = getScriptSource(ctx); + // needs The Sundays compiler; must NOT throw any exception here... + source.setScript("I keep hoping you are the same as me, and I'll send you letters and come to your house for tea"); + Messenger refreshedMessenger = (Messenger) ctx.getBean(MESSENGER_BEAN_NAME); + assertThatExceptionOfType(FatalBeanException.class).isThrownBy(() -> + refreshedMessenger.getMessage()) + .matches(ex -> ex.contains(ScriptCompilationException.class)); + } + + @Test + public void testPrototypeScriptedBean() throws Exception { + GenericApplicationContext ctx = new GenericApplicationContext(); + ctx.registerBeanDefinition("messenger", BeanDefinitionBuilder.rootBeanDefinition(StubMessenger.class).getBeanDefinition()); + + BeanDefinitionBuilder scriptedBeanBuilder = BeanDefinitionBuilder.rootBeanDefinition(GroovyScriptFactory.class); + scriptedBeanBuilder.setScope(BeanDefinition.SCOPE_PROTOTYPE); + scriptedBeanBuilder.addConstructorArgValue(DELEGATING_SCRIPT); + scriptedBeanBuilder.addPropertyReference("messenger", "messenger"); + + final String BEAN_WITH_DEPENDENCY_NAME = "needsMessenger"; + ctx.registerBeanDefinition(BEAN_WITH_DEPENDENCY_NAME, scriptedBeanBuilder.getBeanDefinition()); + ctx.registerBeanDefinition("scriptProcessor", createScriptFactoryPostProcessor(true)); + ctx.refresh(); + + Messenger messenger1 = (Messenger) ctx.getBean(BEAN_WITH_DEPENDENCY_NAME); + Messenger messenger2 = (Messenger) ctx.getBean(BEAN_WITH_DEPENDENCY_NAME); + assertThat(messenger2).isNotSameAs(messenger1); + } + + private static StaticScriptSource getScriptSource(GenericApplicationContext ctx) throws Exception { + ScriptFactoryPostProcessor processor = (ScriptFactoryPostProcessor) ctx.getBean(PROCESSOR_BEAN_NAME); + BeanDefinition bd = processor.scriptBeanFactory.getBeanDefinition("scriptedObject.messenger"); + return (StaticScriptSource) bd.getConstructorArgumentValues().getIndexedArgumentValue(0, StaticScriptSource.class).getValue(); + } + + private static BeanDefinition createScriptFactoryPostProcessor(boolean isRefreshable) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(ScriptFactoryPostProcessor.class); + if (isRefreshable) { + builder.addPropertyValue("defaultRefreshCheckDelay", new Long(1)); + } + return builder.getBeanDefinition(); + } + + private static BeanDefinition createScriptedGroovyBean() { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(GroovyScriptFactory.class); + builder.addConstructorArgValue("inline:package org.springframework.scripting;\n" + + "class GroovyMessenger implements Messenger {\n" + + " private String message = \"Bingo\"\n" + + " public String getMessage() {\n" + + " return this.message\n" + + " }\n" + + " public void setMessage(String message) {\n" + + " this.message = message\n" + + " }\n" + + "}"); + builder.addPropertyValue("message", MESSAGE_TEXT); + return builder.getBeanDefinition(); + } + + private static void pauseToLetRefreshDelayKickIn(int secondsToPause) { + try { + Thread.sleep(secondsToPause * 1000); + } + catch (InterruptedException ignored) { + } + } + + + public static class DefaultMessengerService { + + private Messenger messenger; + + public void setMessenger(Messenger messenger) { + this.messenger = messenger; + } + + public String getMessage() { + return this.messenger.getMessage(); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/scripting/support/StandardScriptFactoryTests.java b/spring-context/src/test/java/org/springframework/scripting/support/StandardScriptFactoryTests.java new file mode 100644 index 0000000..a3679ef --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/support/StandardScriptFactoryTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.support; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledForJreRange; + +import org.springframework.aop.support.AopUtils; +import org.springframework.aop.target.dynamic.Refreshable; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.scripting.Messenger; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.condition.JRE.JAVA_15; + +/** + * {@link StandardScriptFactory} (lang:std) tests for JavaScript. + * + * @author Juergen Hoeller + * @since 4.2 + */ +@DisabledForJreRange(min = JAVA_15) // Nashorn JavaScript engine removed in Java 15 +public class StandardScriptFactoryTests { + + @Test + public void testJsr223FromTagWithInterface() throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("jsr223-with-xsd.xml", getClass()); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("messengerWithInterface")).isTrue(); + Messenger messenger = (Messenger) ctx.getBean("messengerWithInterface"); + assertThat(AopUtils.isAopProxy(messenger)).isFalse(); + assertThat(messenger.getMessage()).isEqualTo("Hello World!"); + } + + @Test + public void testRefreshableJsr223FromTagWithInterface() throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("jsr223-with-xsd.xml", getClass()); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("refreshableMessengerWithInterface")).isTrue(); + Messenger messenger = (Messenger) ctx.getBean("refreshableMessengerWithInterface"); + assertThat(AopUtils.isAopProxy(messenger)).isTrue(); + boolean condition = messenger instanceof Refreshable; + assertThat(condition).isTrue(); + assertThat(messenger.getMessage()).isEqualTo("Hello World!"); + } + + @Test + public void testInlineJsr223FromTagWithInterface() throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("jsr223-with-xsd.xml", getClass()); + assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class)).contains("inlineMessengerWithInterface")).isTrue(); + Messenger messenger = (Messenger) ctx.getBean("inlineMessengerWithInterface"); + assertThat(AopUtils.isAopProxy(messenger)).isFalse(); + assertThat(messenger.getMessage()).isEqualTo("Hello World!"); + } + +} diff --git a/spring-context/src/test/java/org/springframework/scripting/support/StaticScriptSourceTests.java b/spring-context/src/test/java/org/springframework/scripting/support/StaticScriptSourceTests.java new file mode 100644 index 0000000..a9bddab --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/support/StaticScriptSourceTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.support; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Unit tests for the StaticScriptSource class. + * + * @author Rick Evans + * @author Sam Brannen + */ +public class StaticScriptSourceTests { + + private static final String SCRIPT_TEXT = "print($hello) if $true;"; + + private final StaticScriptSource source = new StaticScriptSource(SCRIPT_TEXT); + + + @Test + public void createWithNullScript() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new StaticScriptSource(null)); + } + + @Test + public void createWithEmptyScript() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new StaticScriptSource("")); + } + + @Test + public void createWithWhitespaceOnlyScript() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new StaticScriptSource(" \n\n\t \t\n")); + } + + @Test + public void isModifiedIsTrueByDefault() throws Exception { + assertThat(source.isModified()).as("Script must be flagged as 'modified' when first created.").isTrue(); + } + + @Test + public void gettingScriptTogglesIsModified() throws Exception { + source.getScriptAsString(); + assertThat(source.isModified()).as("Script must be flagged as 'not modified' after script is read.").isFalse(); + } + + @Test + public void gettingScriptViaToStringDoesNotToggleIsModified() throws Exception { + boolean isModifiedState = source.isModified(); + source.toString(); + assertThat(source.isModified()).as("Script's 'modified' flag must not change after script is read via toString().").isEqualTo(isModifiedState); + } + + @Test + public void isModifiedToggledWhenDifferentScriptIsSet() throws Exception { + source.setScript("use warnings;"); + assertThat(source.isModified()).as("Script must be flagged as 'modified' when different script is passed in.").isTrue(); + } + + @Test + public void isModifiedNotToggledWhenSameScriptIsSet() throws Exception { + source.setScript(SCRIPT_TEXT); + assertThat(source.isModified()).as("Script must not be flagged as 'modified' when same script is passed in.").isFalse(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/scripting/support/StubMessenger.java b/spring-context/src/test/java/org/springframework/scripting/support/StubMessenger.java new file mode 100644 index 0000000..e002e2d --- /dev/null +++ b/spring-context/src/test/java/org/springframework/scripting/support/StubMessenger.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.scripting.support; + +import org.springframework.scripting.ConfigurableMessenger; + +/** + * @author Rick Evans + */ +public class StubMessenger implements ConfigurableMessenger { + + private String message = "I used to be smart... now I'm just stupid."; + + @Override + public void setMessage(String message) { + this.message = message; + } + + @Override + public String getMessage() { + return message; + } + +} diff --git a/spring-context/src/test/java/org/springframework/tests/sample/beans/BeanWithObjectProperty.java b/spring-context/src/test/java/org/springframework/tests/sample/beans/BeanWithObjectProperty.java new file mode 100644 index 0000000..79e6fa4 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/tests/sample/beans/BeanWithObjectProperty.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.tests.sample.beans; + +/** + * @author Juergen Hoeller + * @since 17.08.2004 + */ +public class BeanWithObjectProperty { + + private Object object; + + public Object getObject() { + return object; + } + + public void setObject(Object object) { + this.object = object; + } + +} diff --git a/spring-context/src/test/java/org/springframework/tests/sample/beans/FieldAccessBean.java b/spring-context/src/test/java/org/springframework/tests/sample/beans/FieldAccessBean.java new file mode 100644 index 0000000..e9a2ea5 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/tests/sample/beans/FieldAccessBean.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.tests.sample.beans; + +import org.springframework.beans.testfixture.beans.TestBean; + +/** + * @author Juergen Hoeller + * @since 07.03.2006 + */ +public class FieldAccessBean { + + public String name; + + protected int age; + + private TestBean spouse; + + public String getName() { + return name; + } + + public int getAge() { + return age; + } + + public TestBean getSpouse() { + return spouse; + } + +} diff --git a/spring-context/src/test/java/org/springframework/tests/sample/beans/ResourceTestBean.java b/spring-context/src/test/java/org/springframework/tests/sample/beans/ResourceTestBean.java new file mode 100644 index 0000000..e62ded7 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/tests/sample/beans/ResourceTestBean.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2011 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. + * You may obtain a copy of the License at + * + * https://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.springframework.tests.sample.beans; + +import java.io.InputStream; +import java.util.Map; + +import org.springframework.core.io.ContextResource; +import org.springframework.core.io.Resource; + +/** + * @author Juergen Hoeller + * @since 01.04.2004 + */ +public class ResourceTestBean { + + private Resource resource; + + private ContextResource contextResource; + + private InputStream inputStream; + + private Resource[] resourceArray; + + private Map resourceMap; + + private Map resourceArrayMap; + + + public ResourceTestBean() { + } + + public ResourceTestBean(Resource resource, InputStream inputStream) { + this.resource = resource; + this.inputStream = inputStream; + } + + + public Resource getResource() { + return resource; + } + + public void setResource(Resource resource) { + this.resource = resource; + } + + public ContextResource getContextResource() { + return contextResource; + } + + public void setContextResource(ContextResource contextResource) { + this.contextResource = contextResource; + } + + public InputStream getInputStream() { + return inputStream; + } + + public void setInputStream(InputStream inputStream) { + this.inputStream = inputStream; + } + + public Resource[] getResourceArray() { + return resourceArray; + } + + public void setResourceArray(Resource[] resourceArray) { + this.resourceArray = resourceArray; + } + + public Map getResourceMap() { + return resourceMap; + } + + public void setResourceMap(Map resourceMap) { + this.resourceMap = resourceMap; + } + + public Map getResourceArrayMap() { + return resourceArrayMap; + } + + public void setResourceArrayMap(Map resourceArrayMap) { + this.resourceArrayMap = resourceArrayMap; + } + +} diff --git a/spring-context/src/test/java/org/springframework/ui/ModelMapTests.java b/spring-context/src/test/java/org/springframework/ui/ModelMapTests.java new file mode 100644 index 0000000..376d8a1 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/ui/ModelMapTests.java @@ -0,0 +1,312 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ui; + +import java.io.Serializable; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Rick Evans + * @author Juergen Hoeller + * @author Chris Beams + */ +public class ModelMapTests { + + @Test + public void testNoArgCtorYieldsEmptyModel() throws Exception { + assertThat(new ModelMap().size()).isEqualTo(0); + } + + /* + * SPR-2185 - Null model assertion causes backwards compatibility issue + */ + @Test + public void testAddNullObjectWithExplicitKey() throws Exception { + ModelMap model = new ModelMap(); + model.addAttribute("foo", null); + assertThat(model.containsKey("foo")).isTrue(); + assertThat(model.get("foo")).isNull(); + } + + /* + * SPR-2185 - Null model assertion causes backwards compatibility issue + */ + @Test + public void testAddNullObjectViaCtorWithExplicitKey() throws Exception { + ModelMap model = new ModelMap("foo", null); + assertThat(model.containsKey("foo")).isTrue(); + assertThat(model.get("foo")).isNull(); + } + + @Test + public void testNamedObjectCtor() throws Exception { + ModelMap model = new ModelMap("foo", "bing"); + assertThat(model.size()).isEqualTo(1); + String bing = (String) model.get("foo"); + assertThat(bing).isNotNull(); + assertThat(bing).isEqualTo("bing"); + } + + @Test + public void testUnnamedCtorScalar() throws Exception { + ModelMap model = new ModelMap("foo", "bing"); + assertThat(model.size()).isEqualTo(1); + String bing = (String) model.get("foo"); + assertThat(bing).isNotNull(); + assertThat(bing).isEqualTo("bing"); + } + + @Test + public void testOneArgCtorWithScalar() throws Exception { + ModelMap model = new ModelMap("bing"); + assertThat(model.size()).isEqualTo(1); + String string = (String) model.get("string"); + assertThat(string).isNotNull(); + assertThat(string).isEqualTo("bing"); + } + + @Test + public void testOneArgCtorWithNull() { + //Null model arguments added without a name being explicitly supplied are not allowed + assertThatIllegalArgumentException().isThrownBy(() -> + new ModelMap(null)); + } + + @Test + public void testOneArgCtorWithCollection() throws Exception { + ModelMap model = new ModelMap(new String[]{"foo", "boing"}); + assertThat(model.size()).isEqualTo(1); + String[] strings = (String[]) model.get("stringList"); + assertThat(strings).isNotNull(); + assertThat(strings.length).isEqualTo(2); + assertThat(strings[0]).isEqualTo("foo"); + assertThat(strings[1]).isEqualTo("boing"); + } + + @Test + public void testOneArgCtorWithEmptyCollection() throws Exception { + ModelMap model = new ModelMap(new HashSet<>()); + // must not add if collection is empty... + assertThat(model.size()).isEqualTo(0); + } + + @Test + public void testAddObjectWithNull() throws Exception { + // Null model arguments added without a name being explicitly supplied are not allowed + ModelMap model = new ModelMap(); + assertThatIllegalArgumentException().isThrownBy(() -> + model.addAttribute(null)); + } + + @Test + public void testAddObjectWithEmptyArray() throws Exception { + ModelMap model = new ModelMap(new int[]{}); + assertThat(model.size()).isEqualTo(1); + int[] ints = (int[]) model.get("intList"); + assertThat(ints).isNotNull(); + assertThat(ints.length).isEqualTo(0); + } + + @Test + public void testAddAllObjectsWithNullMap() throws Exception { + ModelMap model = new ModelMap(); + model.addAllAttributes((Map) null); + assertThat(model.size()).isEqualTo(0); + } + + @Test + public void testAddAllObjectsWithNullCollection() throws Exception { + ModelMap model = new ModelMap(); + model.addAllAttributes((Collection) null); + assertThat(model.size()).isEqualTo(0); + } + + @Test + public void testAddAllObjectsWithSparseArrayList() throws Exception { + // Null model arguments added without a name being explicitly supplied are not allowed + ModelMap model = new ModelMap(); + ArrayList list = new ArrayList<>(); + list.add("bing"); + list.add(null); + assertThatIllegalArgumentException().isThrownBy(() -> + model.addAllAttributes(list)); + } + + @Test + public void testAddMap() throws Exception { + Map map = new HashMap<>(); + map.put("one", "one-value"); + map.put("two", "two-value"); + ModelMap model = new ModelMap(); + model.addAttribute(map); + assertThat(model.size()).isEqualTo(1); + String key = StringUtils.uncapitalize(ClassUtils.getShortName(map.getClass())); + assertThat(model.containsKey(key)).isTrue(); + } + + @Test + public void testAddObjectNoKeyOfSameTypeOverrides() throws Exception { + ModelMap model = new ModelMap(); + model.addAttribute("foo"); + model.addAttribute("bar"); + assertThat(model.size()).isEqualTo(1); + String bar = (String) model.get("string"); + assertThat(bar).isEqualTo("bar"); + } + + @Test + public void testAddListOfTheSameObjects() throws Exception { + List beans = new ArrayList<>(); + beans.add(new TestBean("one")); + beans.add(new TestBean("two")); + beans.add(new TestBean("three")); + ModelMap model = new ModelMap(); + model.addAllAttributes(beans); + assertThat(model.size()).isEqualTo(1); + } + + @Test + public void testMergeMapWithOverriding() throws Exception { + Map beans = new HashMap<>(); + beans.put("one", new TestBean("one")); + beans.put("two", new TestBean("two")); + beans.put("three", new TestBean("three")); + ModelMap model = new ModelMap(); + model.put("one", new TestBean("oneOld")); + model.mergeAttributes(beans); + assertThat(model.size()).isEqualTo(3); + assertThat(((TestBean) model.get("one")).getName()).isEqualTo("oneOld"); + } + + @Test + public void testInnerClass() throws Exception { + ModelMap map = new ModelMap(); + SomeInnerClass inner = new SomeInnerClass(); + map.addAttribute(inner); + assertThat(map.get("someInnerClass")).isSameAs(inner); + } + + @Test + public void testInnerClassWithTwoUpperCaseLetters() throws Exception { + ModelMap map = new ModelMap(); + UKInnerClass inner = new UKInnerClass(); + map.addAttribute(inner); + assertThat(map.get("UKInnerClass")).isSameAs(inner); + } + + @Test + public void testAopCglibProxy() throws Exception { + ModelMap map = new ModelMap(); + ProxyFactory factory = new ProxyFactory(); + SomeInnerClass val = new SomeInnerClass(); + factory.setTarget(val); + factory.setProxyTargetClass(true); + map.addAttribute(factory.getProxy()); + assertThat(map.containsKey("someInnerClass")).isTrue(); + assertThat(val).isEqualTo(map.get("someInnerClass")); + } + + @Test + public void testAopJdkProxy() throws Exception { + ModelMap map = new ModelMap(); + ProxyFactory factory = new ProxyFactory(); + Map target = new HashMap<>(); + factory.setTarget(target); + factory.addInterface(Map.class); + Object proxy = factory.getProxy(); + map.addAttribute(proxy); + assertThat(map.get("map")).isSameAs(proxy); + } + + @Test + public void testAopJdkProxyWithMultipleInterfaces() throws Exception { + ModelMap map = new ModelMap(); + Map target = new HashMap<>(); + ProxyFactory factory = new ProxyFactory(); + factory.setTarget(target); + factory.addInterface(Serializable.class); + factory.addInterface(Cloneable.class); + factory.addInterface(Comparable.class); + factory.addInterface(Map.class); + Object proxy = factory.getProxy(); + map.addAttribute(proxy); + assertThat(map.get("map")).isSameAs(proxy); + } + + @Test + public void testAopJdkProxyWithDetectedInterfaces() throws Exception { + ModelMap map = new ModelMap(); + Map target = new HashMap<>(); + ProxyFactory factory = new ProxyFactory(target); + Object proxy = factory.getProxy(); + map.addAttribute(proxy); + assertThat(map.get("map")).isSameAs(proxy); + } + + @Test + public void testRawJdkProxy() throws Exception { + ModelMap map = new ModelMap(); + Object proxy = Proxy.newProxyInstance( + getClass().getClassLoader(), + new Class[] {Map.class}, + new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) { + return "proxy"; + } + }); + map.addAttribute(proxy); + assertThat(map.get("map")).isSameAs(proxy); + } + + + public static class SomeInnerClass { + + @Override + public boolean equals(Object obj) { + return (obj instanceof SomeInnerClass); + } + + @Override + public int hashCode() { + return SomeInnerClass.class.hashCode(); + } + } + + + public static class UKInnerClass { + } + +} diff --git a/spring-context/src/test/java/org/springframework/util/MBeanTestUtils.java b/spring-context/src/test/java/org/springframework/util/MBeanTestUtils.java new file mode 100644 index 0000000..b96c560 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/util/MBeanTestUtils.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.lang.management.ManagementFactory; +import java.lang.reflect.Field; + +import javax.management.MBeanServer; +import javax.management.MBeanServerFactory; + +/** + * Utilities for MBean tests. + * + * @author Phillip Webb + */ +public class MBeanTestUtils { + + /** + * Resets MBeanServerFactory and ManagementFactory to a known consistent state. + *

    This involves releasing all currently registered MBeanServers and resetting + * the platformMBeanServer to null. + */ + public static synchronized void resetMBeanServers() throws Exception { + for (MBeanServer server : MBeanServerFactory.findMBeanServer(null)) { + try { + MBeanServerFactory.releaseMBeanServer(server); + } + catch (IllegalArgumentException ex) { + if (!ex.getMessage().contains("not in list")) { + throw ex; + } + } + } + + Field field = ManagementFactory.class.getDeclaredField("platformMBeanServer"); + field.setAccessible(true); + field.set(null, null); + } + +} diff --git a/spring-context/src/test/java/org/springframework/validation/DataBinderFieldAccessTests.java b/spring-context/src/test/java/org/springframework/validation/DataBinderFieldAccessTests.java new file mode 100644 index 0000000..6024547 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/validation/DataBinderFieldAccessTests.java @@ -0,0 +1,179 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation; + +import java.beans.PropertyEditorSupport; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.NotWritablePropertyException; +import org.springframework.beans.NullValueInNestedPathException; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.tests.sample.beans.FieldAccessBean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Juergen Hoeller + * @author Stephane Nicoll + * @since 07.03.2006 + */ +public class DataBinderFieldAccessTests { + + @Test + public void bindingNoErrors() throws Exception { + FieldAccessBean rod = new FieldAccessBean(); + DataBinder binder = new DataBinder(rod, "person"); + assertThat(binder.isIgnoreUnknownFields()).isTrue(); + binder.initDirectFieldAccess(); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.addPropertyValue(new PropertyValue("name", "Rod")); + pvs.addPropertyValue(new PropertyValue("age", new Integer(32))); + pvs.addPropertyValue(new PropertyValue("nonExisting", "someValue")); + + binder.bind(pvs); + binder.close(); + + assertThat(rod.getName().equals("Rod")).as("changed name correctly").isTrue(); + assertThat(rod.getAge() == 32).as("changed age correctly").isTrue(); + + Map m = binder.getBindingResult().getModel(); + assertThat(m.size() == 2).as("There is one element in map").isTrue(); + FieldAccessBean tb = (FieldAccessBean) m.get("person"); + assertThat(tb.equals(rod)).as("Same object").isTrue(); + } + + @Test + public void bindingNoErrorsNotIgnoreUnknown() throws Exception { + FieldAccessBean rod = new FieldAccessBean(); + DataBinder binder = new DataBinder(rod, "person"); + binder.initDirectFieldAccess(); + binder.setIgnoreUnknownFields(false); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.addPropertyValue(new PropertyValue("name", "Rod")); + pvs.addPropertyValue(new PropertyValue("age", new Integer(32))); + pvs.addPropertyValue(new PropertyValue("nonExisting", "someValue")); + assertThatExceptionOfType(NotWritablePropertyException.class).isThrownBy(() -> + binder.bind(pvs)); + } + + @Test + public void bindingWithErrors() throws Exception { + FieldAccessBean rod = new FieldAccessBean(); + DataBinder binder = new DataBinder(rod, "person"); + binder.initDirectFieldAccess(); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.addPropertyValue(new PropertyValue("name", "Rod")); + pvs.addPropertyValue(new PropertyValue("age", "32x")); + binder.bind(pvs); + assertThatExceptionOfType(BindException.class).isThrownBy( + binder::close) + .satisfies(ex -> { + assertThat(rod.getName()).isEqualTo("Rod"); + Map map = binder.getBindingResult().getModel(); + FieldAccessBean tb = (FieldAccessBean) map.get("person"); + assertThat(tb).isEqualTo(rod); + + BindingResult br = (BindingResult) map.get(BindingResult.MODEL_KEY_PREFIX + "person"); + assertThat(br).isSameAs(binder.getBindingResult()); + assertThat(br.hasErrors()).isTrue(); + assertThat(br.getErrorCount()).isEqualTo(1); + assertThat(br.hasFieldErrors()).isTrue(); + assertThat(br.getFieldErrorCount("age")).isEqualTo(1); + assertThat(binder.getBindingResult().getFieldValue("age")).isEqualTo("32x"); + assertThat(binder.getBindingResult().getFieldError("age").getRejectedValue()).isEqualTo("32x"); + assertThat(tb.getAge()).isEqualTo(0); + }); + } + + @Test + public void nestedBindingWithDefaultConversionNoErrors() throws Exception { + FieldAccessBean rod = new FieldAccessBean(); + DataBinder binder = new DataBinder(rod, "person"); + assertThat(binder.isIgnoreUnknownFields()).isTrue(); + binder.initDirectFieldAccess(); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.addPropertyValue(new PropertyValue("spouse.name", "Kerry")); + pvs.addPropertyValue(new PropertyValue("spouse.jedi", "on")); + + binder.bind(pvs); + binder.close(); + + assertThat(rod.getSpouse().getName()).isEqualTo("Kerry"); + assertThat((rod.getSpouse()).isJedi()).isTrue(); + } + + @Test + public void nestedBindingWithDisabledAutoGrow() throws Exception { + FieldAccessBean rod = new FieldAccessBean(); + DataBinder binder = new DataBinder(rod, "person"); + binder.setAutoGrowNestedPaths(false); + binder.initDirectFieldAccess(); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.addPropertyValue(new PropertyValue("spouse.name", "Kerry")); + + assertThatExceptionOfType(NullValueInNestedPathException.class).isThrownBy(() -> + binder.bind(pvs)); + } + + @Test + public void bindingWithErrorsAndCustomEditors() throws Exception { + FieldAccessBean rod = new FieldAccessBean(); + DataBinder binder = new DataBinder(rod, "person"); + binder.initDirectFieldAccess(); + binder.registerCustomEditor(TestBean.class, "spouse", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue(new TestBean(text, 0)); + } + @Override + public String getAsText() { + return ((TestBean) getValue()).getName(); + } + }); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.addPropertyValue(new PropertyValue("name", "Rod")); + pvs.addPropertyValue(new PropertyValue("age", "32x")); + pvs.addPropertyValue(new PropertyValue("spouse", "Kerry")); + binder.bind(pvs); + + assertThatExceptionOfType(BindException.class).isThrownBy( + binder::close) + .satisfies(ex -> { + assertThat(rod.getName()).isEqualTo("Rod"); + Map model = binder.getBindingResult().getModel(); + FieldAccessBean tb = (FieldAccessBean) model.get("person"); + assertThat(tb).isEqualTo(rod); + BindingResult br = (BindingResult) model.get(BindingResult.MODEL_KEY_PREFIX + "person"); + assertThat(br).isSameAs(binder.getBindingResult()); + assertThat(br.hasErrors()).isTrue(); + assertThat(br.getErrorCount()).isEqualTo(1); + assertThat(br.hasFieldErrors("age")).isTrue(); + assertThat(br.getFieldErrorCount("age")).isEqualTo(1); + assertThat(binder.getBindingResult().getFieldValue("age")).isEqualTo("32x"); + assertThat(binder.getBindingResult().getFieldError("age").getRejectedValue()).isEqualTo("32x"); + assertThat(tb.getAge()).isEqualTo(0); + assertThat(br.hasFieldErrors("spouse")).isFalse(); + assertThat(binder.getBindingResult().getFieldValue("spouse")).isEqualTo("Kerry"); + assertThat(tb.getSpouse()).isNotNull(); + }); + } +} diff --git a/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java b/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java new file mode 100644 index 0000000..420e17d --- /dev/null +++ b/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java @@ -0,0 +1,2322 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation; + +import java.beans.PropertyEditor; +import java.beans.PropertyEditorSupport; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.text.ParseException; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.InvalidPropertyException; +import org.springframework.beans.MethodInvocationException; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.NotWritablePropertyException; +import org.springframework.beans.NullValueInNestedPathException; +import org.springframework.beans.TypeMismatchException; +import org.springframework.beans.propertyeditors.CustomCollectionEditor; +import org.springframework.beans.propertyeditors.CustomNumberEditor; +import org.springframework.beans.propertyeditors.StringTrimmerEditor; +import org.springframework.beans.testfixture.beans.DerivedTestBean; +import org.springframework.beans.testfixture.beans.ITestBean; +import org.springframework.beans.testfixture.beans.IndexedTestBean; +import org.springframework.beans.testfixture.beans.SerializablePerson; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.context.support.ResourceBundleMessageSource; +import org.springframework.context.support.StaticMessageSource; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.format.Formatter; +import org.springframework.format.number.NumberStyleFormatter; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.format.support.FormattingConversionService; +import org.springframework.lang.Nullable; +import org.springframework.tests.sample.beans.BeanWithObjectProperty; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * @author Rod Johnson + * @author Juergen Hoeller + * @author Rob Harrop + * @author Kazuki Shimizu + */ +public class DataBinderTests { + + @Test + public void testBindingNoErrors() throws BindException { + TestBean rod = new TestBean(); + DataBinder binder = new DataBinder(rod, "person"); + assertThat(binder.isIgnoreUnknownFields()).isTrue(); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "Rod"); + pvs.add("age", "032"); + pvs.add("nonExisting", "someValue"); + + binder.bind(pvs); + binder.close(); + + assertThat(rod.getName().equals("Rod")).as("changed name correctly").isTrue(); + assertThat(rod.getAge() == 32).as("changed age correctly").isTrue(); + + Map map = binder.getBindingResult().getModel(); + assertThat(map.size() == 2).as("There is one element in map").isTrue(); + TestBean tb = (TestBean) map.get("person"); + assertThat(tb.equals(rod)).as("Same object").isTrue(); + + BindingResult other = new BeanPropertyBindingResult(rod, "person"); + assertThat(binder.getBindingResult()).isEqualTo(other); + assertThat(other).isEqualTo(binder.getBindingResult()); + BindException ex = new BindException(other); + assertThat(other).isEqualTo(ex); + assertThat(ex).isEqualTo(other); + assertThat(binder.getBindingResult()).isEqualTo(ex); + assertThat(ex).isEqualTo(binder.getBindingResult()); + + other.reject("xxx"); + boolean condition = !other.equals(binder.getBindingResult()); + assertThat(condition).isTrue(); + } + + @Test + public void testBindingWithDefaultConversionNoErrors() throws BindException { + TestBean rod = new TestBean(); + DataBinder binder = new DataBinder(rod, "person"); + assertThat(binder.isIgnoreUnknownFields()).isTrue(); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "Rod"); + pvs.add("jedi", "on"); + + binder.bind(pvs); + binder.close(); + + assertThat(rod.getName()).isEqualTo("Rod"); + assertThat(rod.isJedi()).isTrue(); + } + + @Test + public void testNestedBindingWithDefaultConversionNoErrors() throws BindException { + TestBean rod = new TestBean(new TestBean()); + DataBinder binder = new DataBinder(rod, "person"); + assertThat(binder.isIgnoreUnknownFields()).isTrue(); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("spouse.name", "Kerry"); + pvs.add("spouse.jedi", "on"); + + binder.bind(pvs); + binder.close(); + + assertThat(rod.getSpouse().getName()).isEqualTo("Kerry"); + assertThat(((TestBean) rod.getSpouse()).isJedi()).isTrue(); + } + + @Test + public void testBindingNoErrorsNotIgnoreUnknown() { + TestBean rod = new TestBean(); + DataBinder binder = new DataBinder(rod, "person"); + binder.setIgnoreUnknownFields(false); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "Rod"); + pvs.add("age", 32); + pvs.add("nonExisting", "someValue"); + assertThatExceptionOfType(NotWritablePropertyException.class).isThrownBy(() -> + binder.bind(pvs)); + } + + @Test + public void testBindingNoErrorsWithInvalidField() { + TestBean rod = new TestBean(); + DataBinder binder = new DataBinder(rod, "person"); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "Rod"); + pvs.add("spouse.age", 32); + assertThatExceptionOfType(NullValueInNestedPathException.class).isThrownBy(() -> + binder.bind(pvs)); + } + + @Test + public void testBindingNoErrorsWithIgnoreInvalid() { + TestBean rod = new TestBean(); + DataBinder binder = new DataBinder(rod, "person"); + binder.setIgnoreInvalidFields(true); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "Rod"); + pvs.add("spouse.age", 32); + + binder.bind(pvs); + } + + @Test + public void testBindingWithErrors() { + TestBean rod = new TestBean(); + DataBinder binder = new DataBinder(rod, "person"); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "Rod"); + pvs.add("age", "32x"); + pvs.add("touchy", "m.y"); + binder.bind(pvs); + assertThatExceptionOfType(BindException.class).isThrownBy( + binder::close) + .satisfies(ex -> { + assertThat(rod.getName()).isEqualTo("Rod"); + Map map = binder.getBindingResult().getModel(); + TestBean tb = (TestBean) map.get("person"); + assertThat(tb).isSameAs(rod); + + BindingResult br = (BindingResult) map.get(BindingResult.MODEL_KEY_PREFIX + "person"); + assertThat(BindingResultUtils.getBindingResult(map, "person")).isEqualTo(br); + assertThat(BindingResultUtils.getRequiredBindingResult(map, "person")).isEqualTo(br); + + assertThat(BindingResultUtils.getBindingResult(map, "someOtherName")).isNull(); + assertThatIllegalStateException().isThrownBy(() -> + BindingResultUtils.getRequiredBindingResult(map, "someOtherName")); + + assertThat(binder.getBindingResult()).as("Added itself to map").isSameAs(br); + assertThat(br.hasErrors()).isTrue(); + assertThat(br.getErrorCount()).isEqualTo(2); + + assertThat(br.hasFieldErrors("age")).isTrue(); + assertThat(br.getFieldErrorCount("age")).isEqualTo(1); + assertThat(binder.getBindingResult().getFieldValue("age")).isEqualTo("32x"); + FieldError ageError = binder.getBindingResult().getFieldError("age"); + assertThat(ageError).isNotNull(); + assertThat(ageError.getCode()).isEqualTo("typeMismatch"); + assertThat(ageError.getRejectedValue()).isEqualTo("32x"); + assertThat(ageError.contains(TypeMismatchException.class)).isTrue(); + assertThat(ageError.contains(NumberFormatException.class)).isTrue(); + assertThat(ageError.unwrap(NumberFormatException.class).getMessage()).contains("32x"); + assertThat(tb.getAge()).isEqualTo(0); + + assertThat(br.hasFieldErrors("touchy")).isTrue(); + assertThat(br.getFieldErrorCount("touchy")).isEqualTo(1); + assertThat(binder.getBindingResult().getFieldValue("touchy")).isEqualTo("m.y"); + FieldError touchyError = binder.getBindingResult().getFieldError("touchy"); + assertThat(touchyError).isNotNull(); + assertThat(touchyError.getCode()).isEqualTo("methodInvocation"); + assertThat(touchyError.getRejectedValue()).isEqualTo("m.y"); + assertThat(touchyError.contains(MethodInvocationException.class)).isTrue(); + assertThat(touchyError.unwrap(MethodInvocationException.class).getCause().getMessage()).contains("a ."); + assertThat(tb.getTouchy()).isNull(); + + DataBinder binder2 = new DataBinder(new TestBean(), "person"); + MutablePropertyValues pvs2 = new MutablePropertyValues(); + pvs2.add("name", "Rod"); + pvs2.add("age", "32x"); + pvs2.add("touchy", "m.y"); + binder2.bind(pvs2); + assertThat(ex.getBindingResult()).isEqualTo(binder2.getBindingResult()); + }); + } + + @Test + public void testBindingWithSystemFieldError() { + TestBean rod = new TestBean(); + DataBinder binder = new DataBinder(rod, "person"); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("class.classLoader.URLs[0]", "https://myserver"); + binder.setIgnoreUnknownFields(false); + assertThatExceptionOfType(NotWritablePropertyException.class).isThrownBy(() -> + binder.bind(pvs)) + .withMessageContaining("classLoader"); + } + + @Test + public void testBindingWithErrorsAndCustomEditors() { + TestBean rod = new TestBean(); + DataBinder binder = new DataBinder(rod, "person"); + binder.registerCustomEditor(String.class, "touchy", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue("prefix_" + text); + } + @Override + public String getAsText() { + return getValue().toString().substring(7); + } + }); + binder.registerCustomEditor(TestBean.class, "spouse", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue(new TestBean(text, 0)); + } + @Override + public String getAsText() { + return ((TestBean) getValue()).getName(); + } + }); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "Rod"); + pvs.add("age", "32x"); + pvs.add("touchy", "m.y"); + pvs.add("spouse", "Kerry"); + binder.bind(pvs); + + assertThatExceptionOfType(BindException.class).isThrownBy( + binder::close) + .satisfies(ex -> { + assertThat(rod.getName()).isEqualTo("Rod"); + Map model = binder.getBindingResult().getModel(); + TestBean tb = (TestBean) model.get("person"); + assertThat(tb).isEqualTo(rod); + + BindingResult br = (BindingResult) model.get(BindingResult.MODEL_KEY_PREFIX + "person"); + assertThat(binder.getBindingResult()).isSameAs(br); + assertThat(br.hasErrors()).isTrue(); + assertThat(br.getErrorCount()).isEqualTo(2); + + assertThat(br.hasFieldErrors("age")).isTrue(); + assertThat(br.getFieldErrorCount("age")).isEqualTo(1); + assertThat(binder.getBindingResult().getFieldValue("age")).isEqualTo("32x"); + FieldError ageError = binder.getBindingResult().getFieldError("age"); + assertThat(ageError).isNotNull(); + assertThat(ageError.getCode()).isEqualTo("typeMismatch"); + assertThat(ageError.getRejectedValue()).isEqualTo("32x"); + assertThat(tb.getAge()).isEqualTo(0); + + assertThat(br.hasFieldErrors("touchy")).isTrue(); + assertThat(br.getFieldErrorCount("touchy")).isEqualTo(1); + assertThat(binder.getBindingResult().getFieldValue("touchy")).isEqualTo("m.y"); + FieldError touchyError = binder.getBindingResult().getFieldError("touchy"); + assertThat(touchyError).isNotNull(); + assertThat(touchyError.getCode()).isEqualTo("methodInvocation"); + assertThat(touchyError.getRejectedValue()).isEqualTo("m.y"); + assertThat(tb.getTouchy()).isNull(); + + assertThat(br.hasFieldErrors("spouse")).isFalse(); + assertThat(binder.getBindingResult().getFieldValue("spouse")).isEqualTo("Kerry"); + assertThat(tb.getSpouse()).isNotNull(); + }); + } + + @Test + public void testBindingWithCustomEditorOnObjectField() { + BeanWithObjectProperty tb = new BeanWithObjectProperty(); + DataBinder binder = new DataBinder(tb); + binder.registerCustomEditor(Integer.class, "object", new CustomNumberEditor(Integer.class, true)); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("object", "1"); + binder.bind(pvs); + assertThat(tb.getObject()).isEqualTo(1); + } + + @Test + public void testBindingWithFormatter() { + TestBean tb = new TestBean(); + DataBinder binder = new DataBinder(tb); + FormattingConversionService conversionService = new FormattingConversionService(); + DefaultConversionService.addDefaultConverters(conversionService); + conversionService.addFormatterForFieldType(Float.class, new NumberStyleFormatter()); + binder.setConversionService(conversionService); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("myFloat", "1,2"); + + LocaleContextHolder.setLocale(Locale.GERMAN); + try { + binder.bind(pvs); + assertThat(tb.getMyFloat()).isEqualTo(new Float(1.2)); + assertThat(binder.getBindingResult().getFieldValue("myFloat")).isEqualTo("1,2"); + + PropertyEditor editor = binder.getBindingResult().findEditor("myFloat", Float.class); + assertThat(editor).isNotNull(); + editor.setValue(new Float(1.4)); + assertThat(editor.getAsText()).isEqualTo("1,4"); + + editor = binder.getBindingResult().findEditor("myFloat", null); + assertThat(editor).isNotNull(); + editor.setAsText("1,6"); + assertThat(editor.getValue()).isEqualTo(new Float(1.6)); + } + finally { + LocaleContextHolder.resetLocaleContext(); + } + } + + @Test + public void testBindingErrorWithFormatter() { + TestBean tb = new TestBean(); + DataBinder binder = new DataBinder(tb); + FormattingConversionService conversionService = new FormattingConversionService(); + DefaultConversionService.addDefaultConverters(conversionService); + conversionService.addFormatterForFieldType(Float.class, new NumberStyleFormatter()); + binder.setConversionService(conversionService); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("myFloat", "1x2"); + + LocaleContextHolder.setLocale(Locale.GERMAN); + try { + binder.bind(pvs); + assertThat(tb.getMyFloat()).isEqualTo(new Float(0.0)); + assertThat(binder.getBindingResult().getFieldValue("myFloat")).isEqualTo("1x2"); + assertThat(binder.getBindingResult().hasFieldErrors("myFloat")).isTrue(); + } + finally { + LocaleContextHolder.resetLocaleContext(); + } + } + + @Test + public void testBindingErrorWithParseExceptionFromFormatter() { + TestBean tb = new TestBean(); + DataBinder binder = new DataBinder(tb); + FormattingConversionService conversionService = new FormattingConversionService(); + DefaultConversionService.addDefaultConverters(conversionService); + + conversionService.addFormatter(new Formatter() { + @Override + public String parse(String text, Locale locale) throws ParseException { + throw new ParseException(text, 0); + } + @Override + public String print(String object, Locale locale) { + return object; + } + }); + + binder.setConversionService(conversionService); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "test"); + + binder.bind(pvs); + assertThat(binder.getBindingResult().hasFieldErrors("name")).isTrue(); + assertThat(binder.getBindingResult().getFieldError("name").getCode()).isEqualTo("typeMismatch"); + assertThat(binder.getBindingResult().getFieldValue("name")).isEqualTo("test"); + } + + @Test + public void testBindingErrorWithRuntimeExceptionFromFormatter() { + TestBean tb = new TestBean(); + DataBinder binder = new DataBinder(tb); + FormattingConversionService conversionService = new FormattingConversionService(); + DefaultConversionService.addDefaultConverters(conversionService); + + conversionService.addFormatter(new Formatter() { + @Override + public String parse(String text, Locale locale) throws ParseException { + throw new RuntimeException(text); + } + @Override + public String print(String object, Locale locale) { + return object; + } + }); + + binder.setConversionService(conversionService); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "test"); + + binder.bind(pvs); + assertThat(binder.getBindingResult().hasFieldErrors("name")).isTrue(); + assertThat(binder.getBindingResult().getFieldError("name").getCode()).isEqualTo("typeMismatch"); + assertThat(binder.getBindingResult().getFieldValue("name")).isEqualTo("test"); + } + + @Test + public void testBindingWithFormatterAgainstList() { + BeanWithIntegerList tb = new BeanWithIntegerList(); + DataBinder binder = new DataBinder(tb); + FormattingConversionService conversionService = new FormattingConversionService(); + DefaultConversionService.addDefaultConverters(conversionService); + conversionService.addFormatterForFieldType(Float.class, new NumberStyleFormatter()); + binder.setConversionService(conversionService); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("integerList[0]", "1"); + + LocaleContextHolder.setLocale(Locale.GERMAN); + try { + binder.bind(pvs); + assertThat(tb.getIntegerList().get(0)).isEqualTo(1); + assertThat(binder.getBindingResult().getFieldValue("integerList[0]")).isEqualTo("1"); + } + finally { + LocaleContextHolder.resetLocaleContext(); + } + } + + @Test + public void testBindingErrorWithFormatterAgainstList() { + BeanWithIntegerList tb = new BeanWithIntegerList(); + DataBinder binder = new DataBinder(tb); + FormattingConversionService conversionService = new FormattingConversionService(); + DefaultConversionService.addDefaultConverters(conversionService); + conversionService.addFormatterForFieldType(Float.class, new NumberStyleFormatter()); + binder.setConversionService(conversionService); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("integerList[0]", "1x2"); + + LocaleContextHolder.setLocale(Locale.GERMAN); + try { + binder.bind(pvs); + assertThat(tb.getIntegerList().isEmpty()).isTrue(); + assertThat(binder.getBindingResult().getFieldValue("integerList[0]")).isEqualTo("1x2"); + assertThat(binder.getBindingResult().hasFieldErrors("integerList[0]")).isTrue(); + } + finally { + LocaleContextHolder.resetLocaleContext(); + } + } + + @Test + public void testBindingWithFormatterAgainstFields() { + TestBean tb = new TestBean(); + DataBinder binder = new DataBinder(tb); + FormattingConversionService conversionService = new FormattingConversionService(); + DefaultConversionService.addDefaultConverters(conversionService); + conversionService.addFormatterForFieldType(Float.class, new NumberStyleFormatter()); + binder.setConversionService(conversionService); + binder.initDirectFieldAccess(); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("myFloat", "1,2"); + + LocaleContextHolder.setLocale(Locale.GERMAN); + try { + binder.bind(pvs); + assertThat(tb.getMyFloat()).isEqualTo(new Float(1.2)); + assertThat(binder.getBindingResult().getFieldValue("myFloat")).isEqualTo("1,2"); + + PropertyEditor editor = binder.getBindingResult().findEditor("myFloat", Float.class); + assertThat(editor).isNotNull(); + editor.setValue(new Float(1.4)); + assertThat(editor.getAsText()).isEqualTo("1,4"); + + editor = binder.getBindingResult().findEditor("myFloat", null); + assertThat(editor).isNotNull(); + editor.setAsText("1,6"); + assertThat(editor.getValue()).isEqualTo(new Float(1.6)); + } + finally { + LocaleContextHolder.resetLocaleContext(); + } + } + + @Test + public void testBindingErrorWithFormatterAgainstFields() { + TestBean tb = new TestBean(); + DataBinder binder = new DataBinder(tb); + binder.initDirectFieldAccess(); + FormattingConversionService conversionService = new FormattingConversionService(); + DefaultConversionService.addDefaultConverters(conversionService); + conversionService.addFormatterForFieldType(Float.class, new NumberStyleFormatter()); + binder.setConversionService(conversionService); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("myFloat", "1x2"); + + LocaleContextHolder.setLocale(Locale.GERMAN); + try { + binder.bind(pvs); + assertThat(tb.getMyFloat()).isEqualTo(new Float(0.0)); + assertThat(binder.getBindingResult().getFieldValue("myFloat")).isEqualTo("1x2"); + assertThat(binder.getBindingResult().hasFieldErrors("myFloat")).isTrue(); + } + finally { + LocaleContextHolder.resetLocaleContext(); + } + } + + @Test + public void testBindingWithCustomFormatter() { + TestBean tb = new TestBean(); + DataBinder binder = new DataBinder(tb); + binder.addCustomFormatter(new NumberStyleFormatter(), Float.class); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("myFloat", "1,2"); + + LocaleContextHolder.setLocale(Locale.GERMAN); + try { + binder.bind(pvs); + assertThat(tb.getMyFloat()).isEqualTo(new Float(1.2)); + assertThat(binder.getBindingResult().getFieldValue("myFloat")).isEqualTo("1,2"); + + PropertyEditor editor = binder.getBindingResult().findEditor("myFloat", Float.class); + assertThat(editor).isNotNull(); + editor.setValue(new Float(1.4)); + assertThat(editor.getAsText()).isEqualTo("1,4"); + + editor = binder.getBindingResult().findEditor("myFloat", null); + assertThat(editor).isNotNull(); + editor.setAsText("1,6"); + assertThat(((Number) editor.getValue()).floatValue() == 1.6f).isTrue(); + } + finally { + LocaleContextHolder.resetLocaleContext(); + } + } + + @Test + public void testBindingErrorWithCustomFormatter() { + TestBean tb = new TestBean(); + DataBinder binder = new DataBinder(tb); + binder.addCustomFormatter(new NumberStyleFormatter()); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("myFloat", "1x2"); + + LocaleContextHolder.setLocale(Locale.GERMAN); + try { + binder.bind(pvs); + assertThat(tb.getMyFloat()).isEqualTo(new Float(0.0)); + assertThat(binder.getBindingResult().getFieldValue("myFloat")).isEqualTo("1x2"); + assertThat(binder.getBindingResult().hasFieldErrors("myFloat")).isTrue(); + assertThat(binder.getBindingResult().getFieldError("myFloat").getCode()).isEqualTo("typeMismatch"); + } + finally { + LocaleContextHolder.resetLocaleContext(); + } + } + + @Test + public void testBindingErrorWithParseExceptionFromCustomFormatter() { + TestBean tb = new TestBean(); + DataBinder binder = new DataBinder(tb); + + binder.addCustomFormatter(new Formatter() { + @Override + public String parse(String text, Locale locale) throws ParseException { + throw new ParseException(text, 0); + } + @Override + public String print(String object, Locale locale) { + return object; + } + }); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "test"); + + binder.bind(pvs); + assertThat(binder.getBindingResult().hasFieldErrors("name")).isTrue(); + assertThat(binder.getBindingResult().getFieldValue("name")).isEqualTo("test"); + assertThat(binder.getBindingResult().getFieldError("name").getCode()).isEqualTo("typeMismatch"); + } + + @Test + public void testBindingErrorWithRuntimeExceptionFromCustomFormatter() { + TestBean tb = new TestBean(); + DataBinder binder = new DataBinder(tb); + + binder.addCustomFormatter(new Formatter() { + @Override + public String parse(String text, Locale locale) throws ParseException { + throw new RuntimeException(text); + } + @Override + public String print(String object, Locale locale) { + return object; + } + }); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "test"); + + binder.bind(pvs); + assertThat(binder.getBindingResult().hasFieldErrors("name")).isTrue(); + assertThat(binder.getBindingResult().getFieldValue("name")).isEqualTo("test"); + assertThat(binder.getBindingResult().getFieldError("name").getCode()).isEqualTo("typeMismatch"); + } + + @Test + public void testConversionWithInappropriateStringEditor() { + DataBinder dataBinder = new DataBinder(null); + DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(); + dataBinder.setConversionService(conversionService); + dataBinder.registerCustomEditor(String.class, new StringTrimmerEditor(true)); + + NameBean bean = new NameBean("Fred"); + assertThat(dataBinder.convertIfNecessary(bean, String.class)).as("ConversionService should have invoked toString()").isEqualTo("Fred"); + conversionService.addConverter(new NameBeanConverter()); + assertThat(dataBinder.convertIfNecessary(bean, String.class)).as("Type converter should have been used").isEqualTo("[Fred]"); + } + + @Test + public void testBindingWithAllowedFields() throws BindException { + TestBean rod = new TestBean(); + DataBinder binder = new DataBinder(rod); + binder.setAllowedFields("name", "myparam"); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "Rod"); + pvs.add("age", "32x"); + + binder.bind(pvs); + binder.close(); + assertThat(rod.getName().equals("Rod")).as("changed name correctly").isTrue(); + assertThat(rod.getAge() == 0).as("did not change age").isTrue(); + } + + @Test + public void testBindingWithDisallowedFields() throws BindException { + TestBean rod = new TestBean(); + DataBinder binder = new DataBinder(rod); + binder.setDisallowedFields("age"); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "Rod"); + pvs.add("age", "32x"); + + binder.bind(pvs); + binder.close(); + assertThat(rod.getName().equals("Rod")).as("changed name correctly").isTrue(); + assertThat(rod.getAge() == 0).as("did not change age").isTrue(); + String[] disallowedFields = binder.getBindingResult().getSuppressedFields(); + assertThat(disallowedFields.length).isEqualTo(1); + assertThat(disallowedFields[0]).isEqualTo("age"); + } + + @Test + public void testBindingWithAllowedAndDisallowedFields() throws BindException { + TestBean rod = new TestBean(); + DataBinder binder = new DataBinder(rod); + binder.setAllowedFields("name", "myparam"); + binder.setDisallowedFields("age"); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "Rod"); + pvs.add("age", "32x"); + + binder.bind(pvs); + binder.close(); + assertThat(rod.getName().equals("Rod")).as("changed name correctly").isTrue(); + assertThat(rod.getAge() == 0).as("did not change age").isTrue(); + String[] disallowedFields = binder.getBindingResult().getSuppressedFields(); + assertThat(disallowedFields.length).isEqualTo(1); + assertThat(disallowedFields[0]).isEqualTo("age"); + } + + @Test + public void testBindingWithOverlappingAllowedAndDisallowedFields() throws BindException { + TestBean rod = new TestBean(); + DataBinder binder = new DataBinder(rod); + binder.setAllowedFields("name", "age"); + binder.setDisallowedFields("age"); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "Rod"); + pvs.add("age", "32x"); + + binder.bind(pvs); + binder.close(); + assertThat(rod.getName().equals("Rod")).as("changed name correctly").isTrue(); + assertThat(rod.getAge() == 0).as("did not change age").isTrue(); + String[] disallowedFields = binder.getBindingResult().getSuppressedFields(); + assertThat(disallowedFields.length).isEqualTo(1); + assertThat(disallowedFields[0]).isEqualTo("age"); + } + + @Test + public void testBindingWithAllowedFieldsUsingAsterisks() throws BindException { + TestBean rod = new TestBean(); + DataBinder binder = new DataBinder(rod, "person"); + binder.setAllowedFields("nam*", "*ouchy"); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "Rod"); + pvs.add("touchy", "Rod"); + pvs.add("age", "32x"); + + binder.bind(pvs); + binder.close(); + + assertThat("Rod".equals(rod.getName())).as("changed name correctly").isTrue(); + assertThat("Rod".equals(rod.getTouchy())).as("changed touchy correctly").isTrue(); + assertThat(rod.getAge() == 0).as("did not change age").isTrue(); + String[] disallowedFields = binder.getBindingResult().getSuppressedFields(); + assertThat(disallowedFields.length).isEqualTo(1); + assertThat(disallowedFields[0]).isEqualTo("age"); + + Map m = binder.getBindingResult().getModel(); + assertThat(m.size() == 2).as("There is one element in map").isTrue(); + TestBean tb = (TestBean) m.get("person"); + assertThat(tb.equals(rod)).as("Same object").isTrue(); + } + + @Test + public void testBindingWithAllowedAndDisallowedMapFields() throws BindException { + TestBean rod = new TestBean(); + DataBinder binder = new DataBinder(rod); + binder.setAllowedFields("someMap[key1]", "someMap[key2]"); + binder.setDisallowedFields("someMap['key3']", "someMap[key4]"); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("someMap[key1]", "value1"); + pvs.add("someMap['key2']", "value2"); + pvs.add("someMap[key3]", "value3"); + pvs.add("someMap['key4']", "value4"); + + binder.bind(pvs); + binder.close(); + assertThat(rod.getSomeMap().get("key1")).isEqualTo("value1"); + assertThat(rod.getSomeMap().get("key2")).isEqualTo("value2"); + assertThat(rod.getSomeMap().get("key3")).isNull(); + assertThat(rod.getSomeMap().get("key4")).isNull(); + String[] disallowedFields = binder.getBindingResult().getSuppressedFields(); + assertThat(disallowedFields.length).isEqualTo(2); + assertThat(ObjectUtils.containsElement(disallowedFields, "someMap[key3]")).isTrue(); + assertThat(ObjectUtils.containsElement(disallowedFields, "someMap[key4]")).isTrue(); + } + + /** + * Tests for required field, both null, non-existing and empty strings. + */ + @Test + public void testBindingWithRequiredFields() { + TestBean tb = new TestBean(); + tb.setSpouse(new TestBean()); + + DataBinder binder = new DataBinder(tb, "person"); + binder.setRequiredFields("touchy", "name", "age", "date", "spouse.name"); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("touchy", ""); + pvs.add("name", null); + pvs.add("age", null); + pvs.add("spouse.name", " "); + + binder.bind(pvs); + + BindingResult br = binder.getBindingResult(); + assertThat(br.getErrorCount()).as("Wrong number of errors").isEqualTo(5); + + assertThat(br.getFieldError("touchy").getCode()).isEqualTo("required"); + assertThat(br.getFieldValue("touchy")).isEqualTo(""); + assertThat(br.getFieldError("name").getCode()).isEqualTo("required"); + assertThat(br.getFieldValue("name")).isEqualTo(""); + assertThat(br.getFieldError("age").getCode()).isEqualTo("required"); + assertThat(br.getFieldValue("age")).isEqualTo(""); + assertThat(br.getFieldError("date").getCode()).isEqualTo("required"); + assertThat(br.getFieldValue("date")).isEqualTo(""); + assertThat(br.getFieldError("spouse.name").getCode()).isEqualTo("required"); + assertThat(br.getFieldValue("spouse.name")).isEqualTo(""); + } + + @Test + public void testBindingWithRequiredMapFields() { + TestBean tb = new TestBean(); + tb.setSpouse(new TestBean()); + + DataBinder binder = new DataBinder(tb, "person"); + binder.setRequiredFields("someMap[key1]", "someMap[key2]", "someMap['key3']", "someMap[key4]"); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("someMap[key1]", "value1"); + pvs.add("someMap['key2']", "value2"); + pvs.add("someMap[key3]", "value3"); + + binder.bind(pvs); + + BindingResult br = binder.getBindingResult(); + assertThat(br.getErrorCount()).as("Wrong number of errors").isEqualTo(1); + assertThat(br.getFieldError("someMap[key4]").getCode()).isEqualTo("required"); + } + + @Test + public void testBindingWithNestedObjectCreation() { + TestBean tb = new TestBean(); + + DataBinder binder = new DataBinder(tb, "person"); + binder.registerCustomEditor(ITestBean.class, new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue(new TestBean()); + } + }); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("spouse", "someValue"); + pvs.add("spouse.name", "test"); + binder.bind(pvs); + + assertThat(tb.getSpouse()).isNotNull(); + assertThat(tb.getSpouse().getName()).isEqualTo("test"); + } + + @Test + public void testCustomEditorWithOldValueAccess() { + TestBean tb = new TestBean(); + DataBinder binder = new DataBinder(tb, "tb"); + + binder.registerCustomEditor(String.class, null, new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + if (getValue() == null || !text.equalsIgnoreCase(getValue().toString())) { + setValue(text); + } + } + }); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "value"); + binder.bind(pvs); + assertThat(tb.getName()).isEqualTo("value"); + + pvs = new MutablePropertyValues(); + pvs.add("name", "vaLue"); + binder.bind(pvs); + assertThat(tb.getName()).isEqualTo("value"); + } + + @Test + public void testCustomEditorForSingleProperty() { + TestBean tb = new TestBean(); + tb.setSpouse(new TestBean()); + DataBinder binder = new DataBinder(tb, "tb"); + + binder.registerCustomEditor(String.class, "name", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue("prefix" + text); + } + @Override + public String getAsText() { + return ((String) getValue()).substring(6); + } + }); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "value"); + pvs.add("touchy", "value"); + pvs.add("spouse.name", "sue"); + binder.bind(pvs); + + binder.getBindingResult().rejectValue("name", "someCode", "someMessage"); + binder.getBindingResult().rejectValue("touchy", "someCode", "someMessage"); + binder.getBindingResult().rejectValue("spouse.name", "someCode", "someMessage"); + + assertThat(binder.getBindingResult().getNestedPath()).isEqualTo(""); + assertThat(binder.getBindingResult().getFieldValue("name")).isEqualTo("value"); + assertThat(binder.getBindingResult().getFieldError("name").getRejectedValue()).isEqualTo("prefixvalue"); + assertThat(tb.getName()).isEqualTo("prefixvalue"); + assertThat(binder.getBindingResult().getFieldValue("touchy")).isEqualTo("value"); + assertThat(binder.getBindingResult().getFieldError("touchy").getRejectedValue()).isEqualTo("value"); + assertThat(tb.getTouchy()).isEqualTo("value"); + + assertThat(binder.getBindingResult().hasFieldErrors("spouse.*")).isTrue(); + assertThat(binder.getBindingResult().getFieldErrorCount("spouse.*")).isEqualTo(1); + assertThat(binder.getBindingResult().getFieldError("spouse.*").getField()).isEqualTo("spouse.name"); + } + + @Test + public void testCustomEditorForPrimitiveProperty() { + TestBean tb = new TestBean(); + DataBinder binder = new DataBinder(tb, "tb"); + + binder.registerCustomEditor(int.class, "age", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue(99); + } + @Override + public String getAsText() { + return "argh"; + } + }); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("age", ""); + binder.bind(pvs); + + assertThat(binder.getBindingResult().getFieldValue("age")).isEqualTo("argh"); + assertThat(tb.getAge()).isEqualTo(99); + } + + @Test + public void testCustomEditorForAllStringProperties() { + TestBean tb = new TestBean(); + DataBinder binder = new DataBinder(tb, "tb"); + + binder.registerCustomEditor(String.class, null, new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue("prefix" + text); + } + @Override + public String getAsText() { + return ((String) getValue()).substring(6); + } + }); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "value"); + pvs.add("touchy", "value"); + binder.bind(pvs); + + binder.getBindingResult().rejectValue("name", "someCode", "someMessage"); + binder.getBindingResult().rejectValue("touchy", "someCode", "someMessage"); + + assertThat(binder.getBindingResult().getFieldValue("name")).isEqualTo("value"); + assertThat(binder.getBindingResult().getFieldError("name").getRejectedValue()).isEqualTo("prefixvalue"); + assertThat(tb.getName()).isEqualTo("prefixvalue"); + assertThat(binder.getBindingResult().getFieldValue("touchy")).isEqualTo("value"); + assertThat(binder.getBindingResult().getFieldError("touchy").getRejectedValue()).isEqualTo("prefixvalue"); + assertThat(tb.getTouchy()).isEqualTo("prefixvalue"); + } + + @Test + public void testCustomFormatterForSingleProperty() { + TestBean tb = new TestBean(); + tb.setSpouse(new TestBean()); + DataBinder binder = new DataBinder(tb, "tb"); + + binder.addCustomFormatter(new Formatter() { + @Override + public String parse(String text, Locale locale) throws ParseException { + return "prefix" + text; + } + @Override + public String print(String object, Locale locale) { + return object.substring(6); + } + }, "name"); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "value"); + pvs.add("touchy", "value"); + pvs.add("spouse.name", "sue"); + binder.bind(pvs); + + binder.getBindingResult().rejectValue("name", "someCode", "someMessage"); + binder.getBindingResult().rejectValue("touchy", "someCode", "someMessage"); + binder.getBindingResult().rejectValue("spouse.name", "someCode", "someMessage"); + + assertThat(binder.getBindingResult().getNestedPath()).isEqualTo(""); + assertThat(binder.getBindingResult().getFieldValue("name")).isEqualTo("value"); + assertThat(binder.getBindingResult().getFieldError("name").getRejectedValue()).isEqualTo("prefixvalue"); + assertThat(tb.getName()).isEqualTo("prefixvalue"); + assertThat(binder.getBindingResult().getFieldValue("touchy")).isEqualTo("value"); + assertThat(binder.getBindingResult().getFieldError("touchy").getRejectedValue()).isEqualTo("value"); + assertThat(tb.getTouchy()).isEqualTo("value"); + + assertThat(binder.getBindingResult().hasFieldErrors("spouse.*")).isTrue(); + assertThat(binder.getBindingResult().getFieldErrorCount("spouse.*")).isEqualTo(1); + assertThat(binder.getBindingResult().getFieldError("spouse.*").getField()).isEqualTo("spouse.name"); + } + + @Test + public void testCustomFormatterForPrimitiveProperty() { + TestBean tb = new TestBean(); + DataBinder binder = new DataBinder(tb, "tb"); + + binder.addCustomFormatter(new Formatter() { + @Override + public Integer parse(String text, Locale locale) throws ParseException { + return 99; + } + @Override + public String print(Integer object, Locale locale) { + return "argh"; + } + }, "age"); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("age", "x"); + binder.bind(pvs); + + assertThat(binder.getBindingResult().getFieldValue("age")).isEqualTo("argh"); + assertThat(tb.getAge()).isEqualTo(99); + } + + @Test + public void testCustomFormatterForAllStringProperties() { + TestBean tb = new TestBean(); + DataBinder binder = new DataBinder(tb, "tb"); + + binder.addCustomFormatter(new Formatter() { + @Override + public String parse(String text, Locale locale) throws ParseException { + return "prefix" + text; + } + @Override + public String print(String object, Locale locale) { + return object.substring(6); + } + }); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "value"); + pvs.add("touchy", "value"); + binder.bind(pvs); + + binder.getBindingResult().rejectValue("name", "someCode", "someMessage"); + binder.getBindingResult().rejectValue("touchy", "someCode", "someMessage"); + + assertThat(binder.getBindingResult().getFieldValue("name")).isEqualTo("value"); + assertThat(binder.getBindingResult().getFieldError("name").getRejectedValue()).isEqualTo("prefixvalue"); + assertThat(tb.getName()).isEqualTo("prefixvalue"); + assertThat(binder.getBindingResult().getFieldValue("touchy")).isEqualTo("value"); + assertThat(binder.getBindingResult().getFieldError("touchy").getRejectedValue()).isEqualTo("prefixvalue"); + assertThat(tb.getTouchy()).isEqualTo("prefixvalue"); + } + + @Test + public void testJavaBeanPropertyConventions() { + Book book = new Book(); + DataBinder binder = new DataBinder(book); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("title", "my book"); + pvs.add("ISBN", "1234"); + pvs.add("NInStock", "5"); + binder.bind(pvs); + assertThat(book.getTitle()).isEqualTo("my book"); + assertThat(book.getISBN()).isEqualTo("1234"); + assertThat(book.getNInStock()).isEqualTo(5); + + pvs = new MutablePropertyValues(); + pvs.add("Title", "my other book"); + pvs.add("iSBN", "6789"); + pvs.add("nInStock", "0"); + binder.bind(pvs); + assertThat(book.getTitle()).isEqualTo("my other book"); + assertThat(book.getISBN()).isEqualTo("6789"); + assertThat(book.getNInStock()).isEqualTo(0); + } + + @Test + public void testOptionalProperty() { + OptionalHolder bean = new OptionalHolder(); + DataBinder binder = new DataBinder(bean); + binder.setConversionService(new DefaultConversionService()); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("id", "1"); + pvs.add("name", null); + binder.bind(pvs); + assertThat(bean.getId()).isEqualTo("1"); + assertThat(bean.getName().isPresent()).isFalse(); + + pvs = new MutablePropertyValues(); + pvs.add("id", "2"); + pvs.add("name", "myName"); + binder.bind(pvs); + assertThat(bean.getId()).isEqualTo("2"); + assertThat(bean.getName().get()).isEqualTo("myName"); + } + + @Test + public void testValidatorNoErrors() throws Exception { + TestBean tb = new TestBean(); + tb.setAge(33); + tb.setName("Rod"); + tb.setTouchy("Rod"); // Should not throw + TestBean tb2 = new TestBean(); + tb2.setAge(34); + tb.setSpouse(tb2); + DataBinder db = new DataBinder(tb, "tb"); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("spouse.age", "argh"); + db.bind(pvs); + Errors errors = db.getBindingResult(); + Validator testValidator = new TestBeanValidator(); + testValidator.validate(tb, errors); + + errors.setNestedPath("spouse"); + assertThat(errors.getNestedPath()).isEqualTo("spouse."); + assertThat(errors.getFieldValue("age")).isEqualTo("argh"); + Validator spouseValidator = new SpouseValidator(); + spouseValidator.validate(tb.getSpouse(), errors); + + errors.setNestedPath(""); + assertThat(errors.getNestedPath()).isEqualTo(""); + errors.pushNestedPath("spouse"); + assertThat(errors.getNestedPath()).isEqualTo("spouse."); + errors.pushNestedPath("spouse"); + assertThat(errors.getNestedPath()).isEqualTo("spouse.spouse."); + errors.popNestedPath(); + assertThat(errors.getNestedPath()).isEqualTo("spouse."); + errors.popNestedPath(); + assertThat(errors.getNestedPath()).isEqualTo(""); + try { + errors.popNestedPath(); + } + catch (IllegalStateException ex) { + // expected, because stack was empty + } + errors.pushNestedPath("spouse"); + assertThat(errors.getNestedPath()).isEqualTo("spouse."); + errors.setNestedPath(""); + assertThat(errors.getNestedPath()).isEqualTo(""); + try { + errors.popNestedPath(); + } + catch (IllegalStateException ex) { + // expected, because stack was reset by setNestedPath + } + + errors.pushNestedPath("spouse"); + assertThat(errors.getNestedPath()).isEqualTo("spouse."); + + assertThat(errors.getErrorCount()).isEqualTo(1); + boolean condition1 = !errors.hasGlobalErrors(); + assertThat(condition1).isTrue(); + assertThat(errors.getFieldErrorCount("age")).isEqualTo(1); + boolean condition = !errors.hasFieldErrors("name"); + assertThat(condition).isTrue(); + } + + @Test + public void testValidatorWithErrors() { + TestBean tb = new TestBean(); + tb.setSpouse(new TestBean()); + + Errors errors = new BeanPropertyBindingResult(tb, "tb"); + + Validator testValidator = new TestBeanValidator(); + testValidator.validate(tb, errors); + + errors.setNestedPath("spouse."); + assertThat(errors.getNestedPath()).isEqualTo("spouse."); + Validator spouseValidator = new SpouseValidator(); + spouseValidator.validate(tb.getSpouse(), errors); + + errors.setNestedPath(""); + assertThat(errors.hasErrors()).isTrue(); + assertThat(errors.getErrorCount()).isEqualTo(6); + + assertThat(errors.getGlobalErrorCount()).isEqualTo(2); + assertThat(errors.getGlobalError().getCode()).isEqualTo("NAME_TOUCHY_MISMATCH"); + assertThat((errors.getGlobalErrors().get(0)).getCode()).isEqualTo("NAME_TOUCHY_MISMATCH"); + assertThat((errors.getGlobalErrors().get(0)).getCodes()[0]).isEqualTo("NAME_TOUCHY_MISMATCH.tb"); + assertThat((errors.getGlobalErrors().get(0)).getCodes()[1]).isEqualTo("NAME_TOUCHY_MISMATCH"); + assertThat((errors.getGlobalErrors().get(0)).getObjectName()).isEqualTo("tb"); + assertThat((errors.getGlobalErrors().get(1)).getCode()).isEqualTo("GENERAL_ERROR"); + assertThat((errors.getGlobalErrors().get(1)).getCodes()[0]).isEqualTo("GENERAL_ERROR.tb"); + assertThat((errors.getGlobalErrors().get(1)).getCodes()[1]).isEqualTo("GENERAL_ERROR"); + assertThat((errors.getGlobalErrors().get(1)).getDefaultMessage()).isEqualTo("msg"); + assertThat((errors.getGlobalErrors().get(1)).getArguments()[0]).isEqualTo("arg"); + + assertThat(errors.hasFieldErrors()).isTrue(); + assertThat(errors.getFieldErrorCount()).isEqualTo(4); + assertThat(errors.getFieldError().getCode()).isEqualTo("TOO_YOUNG"); + assertThat((errors.getFieldErrors().get(0)).getCode()).isEqualTo("TOO_YOUNG"); + assertThat((errors.getFieldErrors().get(0)).getField()).isEqualTo("age"); + assertThat((errors.getFieldErrors().get(1)).getCode()).isEqualTo("AGE_NOT_ODD"); + assertThat((errors.getFieldErrors().get(1)).getField()).isEqualTo("age"); + assertThat((errors.getFieldErrors().get(2)).getCode()).isEqualTo("NOT_ROD"); + assertThat((errors.getFieldErrors().get(2)).getField()).isEqualTo("name"); + assertThat((errors.getFieldErrors().get(3)).getCode()).isEqualTo("TOO_YOUNG"); + assertThat((errors.getFieldErrors().get(3)).getField()).isEqualTo("spouse.age"); + + assertThat(errors.hasFieldErrors("age")).isTrue(); + assertThat(errors.getFieldErrorCount("age")).isEqualTo(2); + assertThat(errors.getFieldError("age").getCode()).isEqualTo("TOO_YOUNG"); + assertThat((errors.getFieldErrors("age").get(0)).getCode()).isEqualTo("TOO_YOUNG"); + assertThat((errors.getFieldErrors("age").get(0)).getObjectName()).isEqualTo("tb"); + assertThat((errors.getFieldErrors("age").get(0)).getField()).isEqualTo("age"); + assertThat((errors.getFieldErrors("age").get(0)).getRejectedValue()).isEqualTo(0); + assertThat((errors.getFieldErrors("age").get(1)).getCode()).isEqualTo("AGE_NOT_ODD"); + + assertThat(errors.hasFieldErrors("name")).isTrue(); + assertThat(errors.getFieldErrorCount("name")).isEqualTo(1); + assertThat(errors.getFieldError("name").getCode()).isEqualTo("NOT_ROD"); + assertThat(errors.getFieldError("name").getCodes()[0]).isEqualTo("NOT_ROD.tb.name"); + assertThat(errors.getFieldError("name").getCodes()[1]).isEqualTo("NOT_ROD.name"); + assertThat(errors.getFieldError("name").getCodes()[2]).isEqualTo("NOT_ROD.java.lang.String"); + assertThat(errors.getFieldError("name").getCodes()[3]).isEqualTo("NOT_ROD"); + assertThat((errors.getFieldErrors("name").get(0)).getField()).isEqualTo("name"); + assertThat((errors.getFieldErrors("name").get(0)).getRejectedValue()).isEqualTo(null); + + assertThat(errors.hasFieldErrors("spouse.age")).isTrue(); + assertThat(errors.getFieldErrorCount("spouse.age")).isEqualTo(1); + assertThat(errors.getFieldError("spouse.age").getCode()).isEqualTo("TOO_YOUNG"); + assertThat((errors.getFieldErrors("spouse.age").get(0)).getObjectName()).isEqualTo("tb"); + assertThat((errors.getFieldErrors("spouse.age").get(0)).getRejectedValue()).isEqualTo(0); + } + + @Test + public void testValidatorWithErrorsAndCodesPrefix() { + TestBean tb = new TestBean(); + tb.setSpouse(new TestBean()); + + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(tb, "tb"); + DefaultMessageCodesResolver codesResolver = new DefaultMessageCodesResolver(); + codesResolver.setPrefix("validation."); + errors.setMessageCodesResolver(codesResolver); + + Validator testValidator = new TestBeanValidator(); + testValidator.validate(tb, errors); + + errors.setNestedPath("spouse."); + assertThat(errors.getNestedPath()).isEqualTo("spouse."); + Validator spouseValidator = new SpouseValidator(); + spouseValidator.validate(tb.getSpouse(), errors); + + errors.setNestedPath(""); + assertThat(errors.hasErrors()).isTrue(); + assertThat(errors.getErrorCount()).isEqualTo(6); + + assertThat(errors.getGlobalErrorCount()).isEqualTo(2); + assertThat(errors.getGlobalError().getCode()).isEqualTo("validation.NAME_TOUCHY_MISMATCH"); + assertThat((errors.getGlobalErrors().get(0)).getCode()).isEqualTo("validation.NAME_TOUCHY_MISMATCH"); + assertThat((errors.getGlobalErrors().get(0)).getCodes()[0]).isEqualTo("validation.NAME_TOUCHY_MISMATCH.tb"); + assertThat((errors.getGlobalErrors().get(0)).getCodes()[1]).isEqualTo("validation.NAME_TOUCHY_MISMATCH"); + assertThat((errors.getGlobalErrors().get(0)).getObjectName()).isEqualTo("tb"); + assertThat((errors.getGlobalErrors().get(1)).getCode()).isEqualTo("validation.GENERAL_ERROR"); + assertThat((errors.getGlobalErrors().get(1)).getCodes()[0]).isEqualTo("validation.GENERAL_ERROR.tb"); + assertThat((errors.getGlobalErrors().get(1)).getCodes()[1]).isEqualTo("validation.GENERAL_ERROR"); + assertThat((errors.getGlobalErrors().get(1)).getDefaultMessage()).isEqualTo("msg"); + assertThat((errors.getGlobalErrors().get(1)).getArguments()[0]).isEqualTo("arg"); + + assertThat(errors.hasFieldErrors()).isTrue(); + assertThat(errors.getFieldErrorCount()).isEqualTo(4); + assertThat(errors.getFieldError().getCode()).isEqualTo("validation.TOO_YOUNG"); + assertThat((errors.getFieldErrors().get(0)).getCode()).isEqualTo("validation.TOO_YOUNG"); + assertThat((errors.getFieldErrors().get(0)).getField()).isEqualTo("age"); + assertThat((errors.getFieldErrors().get(1)).getCode()).isEqualTo("validation.AGE_NOT_ODD"); + assertThat((errors.getFieldErrors().get(1)).getField()).isEqualTo("age"); + assertThat((errors.getFieldErrors().get(2)).getCode()).isEqualTo("validation.NOT_ROD"); + assertThat((errors.getFieldErrors().get(2)).getField()).isEqualTo("name"); + assertThat((errors.getFieldErrors().get(3)).getCode()).isEqualTo("validation.TOO_YOUNG"); + assertThat((errors.getFieldErrors().get(3)).getField()).isEqualTo("spouse.age"); + + assertThat(errors.hasFieldErrors("age")).isTrue(); + assertThat(errors.getFieldErrorCount("age")).isEqualTo(2); + assertThat(errors.getFieldError("age").getCode()).isEqualTo("validation.TOO_YOUNG"); + assertThat((errors.getFieldErrors("age").get(0)).getCode()).isEqualTo("validation.TOO_YOUNG"); + assertThat((errors.getFieldErrors("age").get(0)).getObjectName()).isEqualTo("tb"); + assertThat((errors.getFieldErrors("age").get(0)).getField()).isEqualTo("age"); + assertThat((errors.getFieldErrors("age").get(0)).getRejectedValue()).isEqualTo(0); + assertThat((errors.getFieldErrors("age").get(1)).getCode()).isEqualTo("validation.AGE_NOT_ODD"); + + assertThat(errors.hasFieldErrors("name")).isTrue(); + assertThat(errors.getFieldErrorCount("name")).isEqualTo(1); + assertThat(errors.getFieldError("name").getCode()).isEqualTo("validation.NOT_ROD"); + assertThat(errors.getFieldError("name").getCodes()[0]).isEqualTo("validation.NOT_ROD.tb.name"); + assertThat(errors.getFieldError("name").getCodes()[1]).isEqualTo("validation.NOT_ROD.name"); + assertThat(errors.getFieldError("name").getCodes()[2]).isEqualTo("validation.NOT_ROD.java.lang.String"); + assertThat(errors.getFieldError("name").getCodes()[3]).isEqualTo("validation.NOT_ROD"); + assertThat((errors.getFieldErrors("name").get(0)).getField()).isEqualTo("name"); + assertThat((errors.getFieldErrors("name").get(0)).getRejectedValue()).isEqualTo(null); + + assertThat(errors.hasFieldErrors("spouse.age")).isTrue(); + assertThat(errors.getFieldErrorCount("spouse.age")).isEqualTo(1); + assertThat(errors.getFieldError("spouse.age").getCode()).isEqualTo("validation.TOO_YOUNG"); + assertThat((errors.getFieldErrors("spouse.age").get(0)).getObjectName()).isEqualTo("tb"); + assertThat((errors.getFieldErrors("spouse.age").get(0)).getRejectedValue()).isEqualTo(0); + } + + @Test + public void testValidatorWithNestedObjectNull() { + TestBean tb = new TestBean(); + Errors errors = new BeanPropertyBindingResult(tb, "tb"); + Validator testValidator = new TestBeanValidator(); + testValidator.validate(tb, errors); + errors.setNestedPath("spouse."); + assertThat(errors.getNestedPath()).isEqualTo("spouse."); + Validator spouseValidator = new SpouseValidator(); + spouseValidator.validate(tb.getSpouse(), errors); + errors.setNestedPath(""); + + assertThat(errors.hasFieldErrors("spouse")).isTrue(); + assertThat(errors.getFieldErrorCount("spouse")).isEqualTo(1); + assertThat(errors.getFieldError("spouse").getCode()).isEqualTo("SPOUSE_NOT_AVAILABLE"); + assertThat((errors.getFieldErrors("spouse").get(0)).getObjectName()).isEqualTo("tb"); + assertThat((errors.getFieldErrors("spouse").get(0)).getRejectedValue()).isEqualTo(null); + } + + @Test + public void testNestedValidatorWithoutNestedPath() { + TestBean tb = new TestBean(); + tb.setName("XXX"); + Errors errors = new BeanPropertyBindingResult(tb, "tb"); + Validator spouseValidator = new SpouseValidator(); + spouseValidator.validate(tb, errors); + + assertThat(errors.hasGlobalErrors()).isTrue(); + assertThat(errors.getGlobalErrorCount()).isEqualTo(1); + assertThat(errors.getGlobalError().getCode()).isEqualTo("SPOUSE_NOT_AVAILABLE"); + assertThat((errors.getGlobalErrors().get(0)).getObjectName()).isEqualTo("tb"); + } + + @Test + public void testBindingStringArrayToIntegerSet() { + IndexedTestBean tb = new IndexedTestBean(); + DataBinder binder = new DataBinder(tb, "tb"); + binder.registerCustomEditor(Set.class, new CustomCollectionEditor(TreeSet.class) { + @Override + protected Object convertElement(Object element) { + return new Integer(element.toString()); + } + }); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("set", new String[] {"10", "20", "30"}); + binder.bind(pvs); + + assertThat(binder.getBindingResult().getFieldValue("set")).isEqualTo(tb.getSet()); + boolean condition = tb.getSet() instanceof TreeSet; + assertThat(condition).isTrue(); + assertThat(tb.getSet().size()).isEqualTo(3); + assertThat(tb.getSet().contains(10)).isTrue(); + assertThat(tb.getSet().contains(20)).isTrue(); + assertThat(tb.getSet().contains(30)).isTrue(); + + pvs = new MutablePropertyValues(); + pvs.add("set", null); + binder.bind(pvs); + + assertThat(tb.getSet()).isNull(); + } + + @Test + public void testBindingNullToEmptyCollection() { + IndexedTestBean tb = new IndexedTestBean(); + DataBinder binder = new DataBinder(tb, "tb"); + binder.registerCustomEditor(Set.class, new CustomCollectionEditor(TreeSet.class, true)); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("set", null); + binder.bind(pvs); + + boolean condition = tb.getSet() instanceof TreeSet; + assertThat(condition).isTrue(); + assertThat(tb.getSet().isEmpty()).isTrue(); + } + + @Test + public void testBindingToIndexedField() { + IndexedTestBean tb = new IndexedTestBean(); + DataBinder binder = new DataBinder(tb, "tb"); + binder.registerCustomEditor(String.class, "array.name", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue("array" + text); + } + }); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("array[0]", "a"); + binder.bind(pvs); + Errors errors = binder.getBindingResult(); + errors.rejectValue("array[0].name", "NOT_ROD", "are you sure you're not Rod?"); + errors.rejectValue("map[key1].name", "NOT_ROD", "are you sure you're not Rod?"); + + assertThat(errors.getFieldErrorCount("array[0].name")).isEqualTo(1); + assertThat(errors.getFieldError("array[0].name").getCode()).isEqualTo("NOT_ROD"); + assertThat(errors.getFieldError("array[0].name").getCodes()[0]).isEqualTo("NOT_ROD.tb.array[0].name"); + assertThat(errors.getFieldError("array[0].name").getCodes()[1]).isEqualTo("NOT_ROD.tb.array.name"); + assertThat(errors.getFieldError("array[0].name").getCodes()[2]).isEqualTo("NOT_ROD.array[0].name"); + assertThat(errors.getFieldError("array[0].name").getCodes()[3]).isEqualTo("NOT_ROD.array.name"); + assertThat(errors.getFieldError("array[0].name").getCodes()[4]).isEqualTo("NOT_ROD.name"); + assertThat(errors.getFieldError("array[0].name").getCodes()[5]).isEqualTo("NOT_ROD.java.lang.String"); + assertThat(errors.getFieldError("array[0].name").getCodes()[6]).isEqualTo("NOT_ROD"); + assertThat(errors.getFieldErrorCount("map[key1].name")).isEqualTo(1); + assertThat(errors.getFieldErrorCount("map['key1'].name")).isEqualTo(1); + assertThat(errors.getFieldErrorCount("map[\"key1\"].name")).isEqualTo(1); + assertThat(errors.getFieldError("map[key1].name").getCode()).isEqualTo("NOT_ROD"); + assertThat(errors.getFieldError("map[key1].name").getCodes()[0]).isEqualTo("NOT_ROD.tb.map[key1].name"); + assertThat(errors.getFieldError("map[key1].name").getCodes()[1]).isEqualTo("NOT_ROD.tb.map.name"); + assertThat(errors.getFieldError("map[key1].name").getCodes()[2]).isEqualTo("NOT_ROD.map[key1].name"); + assertThat(errors.getFieldError("map[key1].name").getCodes()[3]).isEqualTo("NOT_ROD.map.name"); + assertThat(errors.getFieldError("map[key1].name").getCodes()[4]).isEqualTo("NOT_ROD.name"); + assertThat(errors.getFieldError("map[key1].name").getCodes()[5]).isEqualTo("NOT_ROD.java.lang.String"); + assertThat(errors.getFieldError("map[key1].name").getCodes()[6]).isEqualTo("NOT_ROD"); + } + + @Test + public void testBindingToNestedIndexedField() { + IndexedTestBean tb = new IndexedTestBean(); + tb.getArray()[0].setNestedIndexedBean(new IndexedTestBean()); + tb.getArray()[1].setNestedIndexedBean(new IndexedTestBean()); + DataBinder binder = new DataBinder(tb, "tb"); + binder.registerCustomEditor(String.class, "array.nestedIndexedBean.list.name", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue("list" + text); + } + }); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("array[0].nestedIndexedBean.list[0].name", "a"); + binder.bind(pvs); + Errors errors = binder.getBindingResult(); + errors.rejectValue("array[0].nestedIndexedBean.list[0].name", "NOT_ROD", "are you sure you're not Rod?"); + + assertThat(errors.getFieldErrorCount("array[0].nestedIndexedBean.list[0].name")).isEqualTo(1); + assertThat(errors.getFieldError("array[0].nestedIndexedBean.list[0].name").getCode()).isEqualTo("NOT_ROD"); + assertThat(errors.getFieldError("array[0].nestedIndexedBean.list[0].name").getCodes()[0]).isEqualTo("NOT_ROD.tb.array[0].nestedIndexedBean.list[0].name"); + assertThat(errors.getFieldError("array[0].nestedIndexedBean.list[0].name").getCodes()[1]).isEqualTo("NOT_ROD.tb.array[0].nestedIndexedBean.list.name"); + assertThat(errors.getFieldError("array[0].nestedIndexedBean.list[0].name").getCodes()[2]).isEqualTo("NOT_ROD.tb.array.nestedIndexedBean.list.name"); + assertThat(errors.getFieldError("array[0].nestedIndexedBean.list[0].name").getCodes()[3]).isEqualTo("NOT_ROD.array[0].nestedIndexedBean.list[0].name"); + assertThat(errors.getFieldError("array[0].nestedIndexedBean.list[0].name").getCodes()[4]).isEqualTo("NOT_ROD.array[0].nestedIndexedBean.list.name"); + assertThat(errors.getFieldError("array[0].nestedIndexedBean.list[0].name").getCodes()[5]).isEqualTo("NOT_ROD.array.nestedIndexedBean.list.name"); + assertThat(errors.getFieldError("array[0].nestedIndexedBean.list[0].name").getCodes()[6]).isEqualTo("NOT_ROD.name"); + assertThat(errors.getFieldError("array[0].nestedIndexedBean.list[0].name").getCodes()[7]).isEqualTo("NOT_ROD.java.lang.String"); + assertThat(errors.getFieldError("array[0].nestedIndexedBean.list[0].name").getCodes()[8]).isEqualTo("NOT_ROD"); + } + + @Test + public void testEditorForNestedIndexedField() { + IndexedTestBean tb = new IndexedTestBean(); + tb.getArray()[0].setNestedIndexedBean(new IndexedTestBean()); + tb.getArray()[1].setNestedIndexedBean(new IndexedTestBean()); + DataBinder binder = new DataBinder(tb, "tb"); + binder.registerCustomEditor(String.class, "array.nestedIndexedBean.list.name", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue("list" + text); + } + @Override + public String getAsText() { + return ((String) getValue()).substring(4); + } + }); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("array[0].nestedIndexedBean.list[0].name", "test1"); + pvs.add("array[1].nestedIndexedBean.list[1].name", "test2"); + binder.bind(pvs); + assertThat(((TestBean) tb.getArray()[0].getNestedIndexedBean().getList().get(0)).getName()).isEqualTo("listtest1"); + assertThat(((TestBean) tb.getArray()[1].getNestedIndexedBean().getList().get(1)).getName()).isEqualTo("listtest2"); + assertThat(binder.getBindingResult().getFieldValue("array[0].nestedIndexedBean.list[0].name")).isEqualTo("test1"); + assertThat(binder.getBindingResult().getFieldValue("array[1].nestedIndexedBean.list[1].name")).isEqualTo("test2"); + } + + @Test + public void testSpecificEditorForNestedIndexedField() { + IndexedTestBean tb = new IndexedTestBean(); + tb.getArray()[0].setNestedIndexedBean(new IndexedTestBean()); + tb.getArray()[1].setNestedIndexedBean(new IndexedTestBean()); + DataBinder binder = new DataBinder(tb, "tb"); + binder.registerCustomEditor(String.class, "array[0].nestedIndexedBean.list.name", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue("list" + text); + } + @Override + public String getAsText() { + return ((String) getValue()).substring(4); + } + }); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("array[0].nestedIndexedBean.list[0].name", "test1"); + pvs.add("array[1].nestedIndexedBean.list[1].name", "test2"); + binder.bind(pvs); + assertThat(((TestBean) tb.getArray()[0].getNestedIndexedBean().getList().get(0)).getName()).isEqualTo("listtest1"); + assertThat(((TestBean) tb.getArray()[1].getNestedIndexedBean().getList().get(1)).getName()).isEqualTo("test2"); + assertThat(binder.getBindingResult().getFieldValue("array[0].nestedIndexedBean.list[0].name")).isEqualTo("test1"); + assertThat(binder.getBindingResult().getFieldValue("array[1].nestedIndexedBean.list[1].name")).isEqualTo("test2"); + } + + @Test + public void testInnerSpecificEditorForNestedIndexedField() { + IndexedTestBean tb = new IndexedTestBean(); + tb.getArray()[0].setNestedIndexedBean(new IndexedTestBean()); + tb.getArray()[1].setNestedIndexedBean(new IndexedTestBean()); + DataBinder binder = new DataBinder(tb, "tb"); + binder.registerCustomEditor(String.class, "array.nestedIndexedBean.list[0].name", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue("list" + text); + } + @Override + public String getAsText() { + return ((String) getValue()).substring(4); + } + }); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("array[0].nestedIndexedBean.list[0].name", "test1"); + pvs.add("array[1].nestedIndexedBean.list[1].name", "test2"); + binder.bind(pvs); + assertThat(((TestBean) tb.getArray()[0].getNestedIndexedBean().getList().get(0)).getName()).isEqualTo("listtest1"); + assertThat(((TestBean) tb.getArray()[1].getNestedIndexedBean().getList().get(1)).getName()).isEqualTo("test2"); + assertThat(binder.getBindingResult().getFieldValue("array[0].nestedIndexedBean.list[0].name")).isEqualTo("test1"); + assertThat(binder.getBindingResult().getFieldValue("array[1].nestedIndexedBean.list[1].name")).isEqualTo("test2"); + } + + @Test + public void testDirectBindingToIndexedField() { + IndexedTestBean tb = new IndexedTestBean(); + DataBinder binder = new DataBinder(tb, "tb"); + binder.registerCustomEditor(TestBean.class, "array", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + DerivedTestBean tb = new DerivedTestBean(); + tb.setName("array" + text); + setValue(tb); + } + @Override + public String getAsText() { + return ((TestBean) getValue()).getName(); + } + }); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("array[0]", "a"); + binder.bind(pvs); + Errors errors = binder.getBindingResult(); + errors.rejectValue("array[0]", "NOT_ROD", "are you sure you're not Rod?"); + errors.rejectValue("map[key1]", "NOT_ROD", "are you sure you're not Rod?"); + errors.rejectValue("map[key0]", "NOT_NULL", "should not be null"); + + assertThat(errors.getFieldValue("array[0]")).isEqualTo("arraya"); + assertThat(errors.getFieldErrorCount("array[0]")).isEqualTo(1); + assertThat(errors.getFieldError("array[0]").getCode()).isEqualTo("NOT_ROD"); + assertThat(errors.getFieldError("array[0]").getCodes()[0]).isEqualTo("NOT_ROD.tb.array[0]"); + assertThat(errors.getFieldError("array[0]").getCodes()[1]).isEqualTo("NOT_ROD.tb.array"); + assertThat(errors.getFieldError("array[0]").getCodes()[2]).isEqualTo("NOT_ROD.array[0]"); + assertThat(errors.getFieldError("array[0]").getCodes()[3]).isEqualTo("NOT_ROD.array"); + assertThat(errors.getFieldError("array[0]").getCodes()[4]).isEqualTo("NOT_ROD.org.springframework.beans.testfixture.beans.DerivedTestBean"); + assertThat(errors.getFieldError("array[0]").getCodes()[5]).isEqualTo("NOT_ROD"); + assertThat(errors.getFieldValue("array[0]")).isEqualTo("arraya"); + + assertThat(errors.getFieldErrorCount("map[key1]")).isEqualTo(1); + assertThat(errors.getFieldError("map[key1]").getCode()).isEqualTo("NOT_ROD"); + assertThat(errors.getFieldError("map[key1]").getCodes()[0]).isEqualTo("NOT_ROD.tb.map[key1]"); + assertThat(errors.getFieldError("map[key1]").getCodes()[1]).isEqualTo("NOT_ROD.tb.map"); + assertThat(errors.getFieldError("map[key1]").getCodes()[2]).isEqualTo("NOT_ROD.map[key1]"); + assertThat(errors.getFieldError("map[key1]").getCodes()[3]).isEqualTo("NOT_ROD.map"); + assertThat(errors.getFieldError("map[key1]").getCodes()[4]).isEqualTo("NOT_ROD.org.springframework.beans.testfixture.beans.TestBean"); + assertThat(errors.getFieldError("map[key1]").getCodes()[5]).isEqualTo("NOT_ROD"); + + assertThat(errors.getFieldErrorCount("map[key0]")).isEqualTo(1); + assertThat(errors.getFieldError("map[key0]").getCode()).isEqualTo("NOT_NULL"); + assertThat(errors.getFieldError("map[key0]").getCodes()[0]).isEqualTo("NOT_NULL.tb.map[key0]"); + assertThat(errors.getFieldError("map[key0]").getCodes()[1]).isEqualTo("NOT_NULL.tb.map"); + assertThat(errors.getFieldError("map[key0]").getCodes()[2]).isEqualTo("NOT_NULL.map[key0]"); + assertThat(errors.getFieldError("map[key0]").getCodes()[3]).isEqualTo("NOT_NULL.map"); + assertThat(errors.getFieldError("map[key0]").getCodes()[4]).isEqualTo("NOT_NULL"); + } + + @Test + public void testDirectBindingToEmptyIndexedFieldWithRegisteredSpecificEditor() { + IndexedTestBean tb = new IndexedTestBean(); + DataBinder binder = new DataBinder(tb, "tb"); + binder.registerCustomEditor(TestBean.class, "map[key0]", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + DerivedTestBean tb = new DerivedTestBean(); + tb.setName("array" + text); + setValue(tb); + } + @Override + public String getAsText() { + return ((TestBean) getValue()).getName(); + } + }); + Errors errors = binder.getBindingResult(); + errors.rejectValue("map[key0]", "NOT_NULL", "should not be null"); + + assertThat(errors.getFieldErrorCount("map[key0]")).isEqualTo(1); + assertThat(errors.getFieldError("map[key0]").getCode()).isEqualTo("NOT_NULL"); + assertThat(errors.getFieldError("map[key0]").getCodes()[0]).isEqualTo("NOT_NULL.tb.map[key0]"); + assertThat(errors.getFieldError("map[key0]").getCodes()[1]).isEqualTo("NOT_NULL.tb.map"); + assertThat(errors.getFieldError("map[key0]").getCodes()[2]).isEqualTo("NOT_NULL.map[key0]"); + assertThat(errors.getFieldError("map[key0]").getCodes()[3]).isEqualTo("NOT_NULL.map"); + // This next code is only generated because of the registered editor, using the + // registered type of the editor as guess for the content type of the collection. + assertThat(errors.getFieldError("map[key0]").getCodes()[4]).isEqualTo("NOT_NULL.org.springframework.beans.testfixture.beans.TestBean"); + assertThat(errors.getFieldError("map[key0]").getCodes()[5]).isEqualTo("NOT_NULL"); + } + + @Test + public void testDirectBindingToEmptyIndexedFieldWithRegisteredGenericEditor() { + IndexedTestBean tb = new IndexedTestBean(); + DataBinder binder = new DataBinder(tb, "tb"); + binder.registerCustomEditor(TestBean.class, "map", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + DerivedTestBean tb = new DerivedTestBean(); + tb.setName("array" + text); + setValue(tb); + } + @Override + public String getAsText() { + return ((TestBean) getValue()).getName(); + } + }); + Errors errors = binder.getBindingResult(); + errors.rejectValue("map[key0]", "NOT_NULL", "should not be null"); + + assertThat(errors.getFieldErrorCount("map[key0]")).isEqualTo(1); + assertThat(errors.getFieldError("map[key0]").getCode()).isEqualTo("NOT_NULL"); + assertThat(errors.getFieldError("map[key0]").getCodes()[0]).isEqualTo("NOT_NULL.tb.map[key0]"); + assertThat(errors.getFieldError("map[key0]").getCodes()[1]).isEqualTo("NOT_NULL.tb.map"); + assertThat(errors.getFieldError("map[key0]").getCodes()[2]).isEqualTo("NOT_NULL.map[key0]"); + assertThat(errors.getFieldError("map[key0]").getCodes()[3]).isEqualTo("NOT_NULL.map"); + // This next code is only generated because of the registered editor, using the + // registered type of the editor as guess for the content type of the collection. + assertThat(errors.getFieldError("map[key0]").getCodes()[4]).isEqualTo("NOT_NULL.org.springframework.beans.testfixture.beans.TestBean"); + assertThat(errors.getFieldError("map[key0]").getCodes()[5]).isEqualTo("NOT_NULL"); + } + + @Test + public void testCustomEditorWithSubclass() { + IndexedTestBean tb = new IndexedTestBean(); + DataBinder binder = new DataBinder(tb, "tb"); + binder.registerCustomEditor(TestBean.class, new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + DerivedTestBean tb = new DerivedTestBean(); + tb.setName("array" + text); + setValue(tb); + } + @Override + public String getAsText() { + return ((TestBean) getValue()).getName(); + } + }); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("array[0]", "a"); + binder.bind(pvs); + Errors errors = binder.getBindingResult(); + errors.rejectValue("array[0]", "NOT_ROD", "are you sure you're not Rod?"); + + assertThat(errors.getFieldValue("array[0]")).isEqualTo("arraya"); + assertThat(errors.getFieldErrorCount("array[0]")).isEqualTo(1); + assertThat(errors.getFieldError("array[0]").getCode()).isEqualTo("NOT_ROD"); + assertThat(errors.getFieldError("array[0]").getCodes()[0]).isEqualTo("NOT_ROD.tb.array[0]"); + assertThat(errors.getFieldError("array[0]").getCodes()[1]).isEqualTo("NOT_ROD.tb.array"); + assertThat(errors.getFieldError("array[0]").getCodes()[2]).isEqualTo("NOT_ROD.array[0]"); + assertThat(errors.getFieldError("array[0]").getCodes()[3]).isEqualTo("NOT_ROD.array"); + assertThat(errors.getFieldError("array[0]").getCodes()[4]).isEqualTo("NOT_ROD.org.springframework.beans.testfixture.beans.DerivedTestBean"); + assertThat(errors.getFieldError("array[0]").getCodes()[5]).isEqualTo("NOT_ROD"); + assertThat(errors.getFieldValue("array[0]")).isEqualTo("arraya"); + } + + @Test + public void testBindToStringArrayWithArrayEditor() { + TestBean tb = new TestBean(); + DataBinder binder = new DataBinder(tb, "tb"); + binder.registerCustomEditor(String[].class, "stringArray", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue(StringUtils.delimitedListToStringArray(text, "-")); + } + }); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("stringArray", "a1-b2"); + binder.bind(pvs); + boolean condition = !binder.getBindingResult().hasErrors(); + assertThat(condition).isTrue(); + assertThat(tb.getStringArray().length).isEqualTo(2); + assertThat(tb.getStringArray()[0]).isEqualTo("a1"); + assertThat(tb.getStringArray()[1]).isEqualTo("b2"); + } + + @Test + public void testBindToStringArrayWithComponentEditor() { + TestBean tb = new TestBean(); + DataBinder binder = new DataBinder(tb, "tb"); + binder.registerCustomEditor(String.class, "stringArray", new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue("X" + text); + } + }); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("stringArray", new String[] {"a1", "b2"}); + binder.bind(pvs); + boolean condition = !binder.getBindingResult().hasErrors(); + assertThat(condition).isTrue(); + assertThat(tb.getStringArray().length).isEqualTo(2); + assertThat(tb.getStringArray()[0]).isEqualTo("Xa1"); + assertThat(tb.getStringArray()[1]).isEqualTo("Xb2"); + } + + @Test + public void testBindingErrors() { + TestBean rod = new TestBean(); + DataBinder binder = new DataBinder(rod, "person"); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("age", "32x"); + binder.bind(pvs); + Errors errors = binder.getBindingResult(); + FieldError ageError = errors.getFieldError("age"); + assertThat(ageError.getCode()).isEqualTo("typeMismatch"); + + ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); + messageSource.setBasename("org.springframework.validation.messages1"); + String msg = messageSource.getMessage(ageError, Locale.getDefault()); + assertThat(msg).isEqualTo("Field age did not have correct type"); + + messageSource = new ResourceBundleMessageSource(); + messageSource.setBasename("org.springframework.validation.messages2"); + msg = messageSource.getMessage(ageError, Locale.getDefault()); + assertThat(msg).isEqualTo("Field Age did not have correct type"); + + messageSource = new ResourceBundleMessageSource(); + messageSource.setBasename("org.springframework.validation.messages3"); + msg = messageSource.getMessage(ageError, Locale.getDefault()); + assertThat(msg).isEqualTo("Field Person Age did not have correct type"); + } + + @Test + public void testAddAllErrors() { + TestBean rod = new TestBean(); + DataBinder binder = new DataBinder(rod, "person"); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("age", "32x"); + binder.bind(pvs); + Errors errors = binder.getBindingResult(); + + BeanPropertyBindingResult errors2 = new BeanPropertyBindingResult(rod, "person"); + errors.rejectValue("name", "badName"); + errors.addAllErrors(errors2); + + FieldError ageError = errors.getFieldError("age"); + assertThat(ageError.getCode()).isEqualTo("typeMismatch"); + FieldError nameError = errors.getFieldError("name"); + assertThat(nameError.getCode()).isEqualTo("badName"); + } + + @Test + @SuppressWarnings("unchecked") + public void testBindingWithResortedList() { + IndexedTestBean tb = new IndexedTestBean(); + DataBinder binder = new DataBinder(tb, "tb"); + MutablePropertyValues pvs = new MutablePropertyValues(); + TestBean tb1 = new TestBean("tb1", 99); + TestBean tb2 = new TestBean("tb2", 99); + pvs.add("list[0]", tb1); + pvs.add("list[1]", tb2); + binder.bind(pvs); + assertThat(binder.getBindingResult().getFieldValue("list[0].name")).isEqualTo(tb1.getName()); + assertThat(binder.getBindingResult().getFieldValue("list[1].name")).isEqualTo(tb2.getName()); + tb.getList().set(0, tb2); + tb.getList().set(1, tb1); + assertThat(binder.getBindingResult().getFieldValue("list[0].name")).isEqualTo(tb2.getName()); + assertThat(binder.getBindingResult().getFieldValue("list[1].name")).isEqualTo(tb1.getName()); + } + + @Test + public void testRejectWithoutDefaultMessage() { + TestBean tb = new TestBean(); + tb.setName("myName"); + tb.setAge(99); + + BeanPropertyBindingResult ex = new BeanPropertyBindingResult(tb, "tb"); + ex.reject("invalid"); + ex.rejectValue("age", "invalidField"); + + StaticMessageSource ms = new StaticMessageSource(); + ms.addMessage("invalid", Locale.US, "general error"); + ms.addMessage("invalidField", Locale.US, "invalid field"); + + assertThat(ms.getMessage(ex.getGlobalError(), Locale.US)).isEqualTo("general error"); + assertThat(ms.getMessage(ex.getFieldError("age"), Locale.US)).isEqualTo("invalid field"); + } + + @Test + public void testBindExceptionSerializable() throws Exception { + SerializablePerson tb = new SerializablePerson(); + tb.setName("myName"); + tb.setAge(99); + + BindException ex = new BindException(tb, "tb"); + ex.reject("invalid", "someMessage"); + ex.rejectValue("age", "invalidField", "someMessage"); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(ex); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(bais); + + BindException ex2 = (BindException) ois.readObject(); + assertThat(ex2.hasGlobalErrors()).isTrue(); + assertThat(ex2.getGlobalError().getCode()).isEqualTo("invalid"); + assertThat(ex2.hasFieldErrors("age")).isTrue(); + assertThat(ex2.getFieldError("age").getCode()).isEqualTo("invalidField"); + assertThat(ex2.getFieldValue("age")).isEqualTo(99); + + ex2.rejectValue("name", "invalidField", "someMessage"); + assertThat(ex2.hasFieldErrors("name")).isTrue(); + assertThat(ex2.getFieldError("name").getCode()).isEqualTo("invalidField"); + assertThat(ex2.getFieldValue("name")).isEqualTo("myName"); + } + + @Test + public void testTrackDisallowedFields() { + TestBean testBean = new TestBean(); + DataBinder binder = new DataBinder(testBean, "testBean"); + binder.setAllowedFields("name", "age"); + + String name = "Rob Harrop"; + String beanName = "foobar"; + + MutablePropertyValues mpvs = new MutablePropertyValues(); + mpvs.add("name", name); + mpvs.add("beanName", beanName); + binder.bind(mpvs); + + assertThat(testBean.getName()).isEqualTo(name); + String[] disallowedFields = binder.getBindingResult().getSuppressedFields(); + assertThat(disallowedFields.length).isEqualTo(1); + assertThat(disallowedFields[0]).isEqualTo("beanName"); + } + + @Test + public void testAutoGrowWithinDefaultLimit() { + TestBean testBean = new TestBean(); + DataBinder binder = new DataBinder(testBean, "testBean"); + + MutablePropertyValues mpvs = new MutablePropertyValues(); + mpvs.add("friends[4]", ""); + binder.bind(mpvs); + + assertThat(testBean.getFriends().size()).isEqualTo(5); + } + + @Test + public void testAutoGrowBeyondDefaultLimit() { + TestBean testBean = new TestBean(); + DataBinder binder = new DataBinder(testBean, "testBean"); + + MutablePropertyValues mpvs = new MutablePropertyValues(); + mpvs.add("friends[256]", ""); + assertThatExceptionOfType(InvalidPropertyException.class) + .isThrownBy(() -> binder.bind(mpvs)) + .havingRootCause() + .isInstanceOf(IndexOutOfBoundsException.class); + } + + @Test + public void testAutoGrowWithinCustomLimit() { + TestBean testBean = new TestBean(); + DataBinder binder = new DataBinder(testBean, "testBean"); + binder.setAutoGrowCollectionLimit(10); + + MutablePropertyValues mpvs = new MutablePropertyValues(); + mpvs.add("friends[4]", ""); + binder.bind(mpvs); + + assertThat(testBean.getFriends().size()).isEqualTo(5); + } + + @Test + public void testAutoGrowBeyondCustomLimit() { + TestBean testBean = new TestBean(); + DataBinder binder = new DataBinder(testBean, "testBean"); + binder.setAutoGrowCollectionLimit(10); + + MutablePropertyValues mpvs = new MutablePropertyValues(); + mpvs.add("friends[16]", ""); + assertThatExceptionOfType(InvalidPropertyException.class) + .isThrownBy(() -> binder.bind(mpvs)) + .havingRootCause() + .isInstanceOf(IndexOutOfBoundsException.class); + } + + @Test + public void testNestedGrowingList() { + Form form = new Form(); + DataBinder binder = new DataBinder(form, "form"); + MutablePropertyValues mpv = new MutablePropertyValues(); + mpv.add("f[list][0]", "firstValue"); + mpv.add("f[list][1]", "secondValue"); + binder.bind(mpv); + assertThat(binder.getBindingResult().hasErrors()).isFalse(); + @SuppressWarnings("unchecked") + List list = (List) form.getF().get("list"); + assertThat(list.get(0)).isEqualTo("firstValue"); + assertThat(list.get(1)).isEqualTo("secondValue"); + assertThat(list.size()).isEqualTo(2); + } + + @Test + public void testFieldErrorAccessVariations() { + TestBean testBean = new TestBean(); + DataBinder binder = new DataBinder(testBean, "testBean"); + assertThat(binder.getBindingResult().getGlobalError()).isNull(); + assertThat(binder.getBindingResult().getFieldError()).isNull(); + assertThat(binder.getBindingResult().getFieldError("")).isNull(); + + MutablePropertyValues mpv = new MutablePropertyValues(); + mpv.add("age", "invalid"); + binder.bind(mpv); + assertThat(binder.getBindingResult().getGlobalError()).isNull(); + assertThat(binder.getBindingResult().getFieldError("")).isNull(); + assertThat(binder.getBindingResult().getFieldError("b*")).isNull(); + assertThat(binder.getBindingResult().getFieldError().getField()).isEqualTo("age"); + assertThat(binder.getBindingResult().getFieldError("*").getField()).isEqualTo("age"); + assertThat(binder.getBindingResult().getFieldError("a*").getField()).isEqualTo("age"); + assertThat(binder.getBindingResult().getFieldError("ag*").getField()).isEqualTo("age"); + assertThat(binder.getBindingResult().getFieldError("age").getField()).isEqualTo("age"); + } + + @Test // SPR-14888 + public void testSetAutoGrowCollectionLimit() { + BeanWithIntegerList tb = new BeanWithIntegerList(); + DataBinder binder = new DataBinder(tb); + binder.setAutoGrowCollectionLimit(257); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("integerList[256]", "1"); + + binder.bind(pvs); + assertThat(tb.getIntegerList().size()).isEqualTo(257); + assertThat(tb.getIntegerList().get(256)).isEqualTo(Integer.valueOf(1)); + assertThat(binder.getBindingResult().getFieldValue("integerList[256]")).isEqualTo(Integer.valueOf(1)); + } + + @Test // SPR-14888 + public void testSetAutoGrowCollectionLimitAfterInitialization() { + DataBinder binder = new DataBinder(new BeanWithIntegerList()); + binder.registerCustomEditor(String.class, new StringTrimmerEditor(true)); + assertThatIllegalStateException().isThrownBy(() -> + binder.setAutoGrowCollectionLimit(257)) + .withMessageContaining("DataBinder is already initialized - call setAutoGrowCollectionLimit before other configuration methods"); + } + + @Test // SPR-15009 + public void testSetCustomMessageCodesResolverBeforeInitializeBindingResultForBeanPropertyAccess() { + TestBean testBean = new TestBean(); + DataBinder binder = new DataBinder(testBean, "testBean"); + DefaultMessageCodesResolver messageCodesResolver = new DefaultMessageCodesResolver(); + messageCodesResolver.setPrefix("errors."); + binder.setMessageCodesResolver(messageCodesResolver); + binder.setAutoGrowCollectionLimit(512); // allow configuration after set a MessageCodesResolver + binder.initBeanPropertyAccess(); + + MutablePropertyValues mpv = new MutablePropertyValues(); + mpv.add("age", "invalid"); + binder.bind(mpv); + assertThat(binder.getBindingResult().getFieldError("age").getCode()).isEqualTo("errors.typeMismatch"); + assertThat(((BeanWrapper) binder.getInternalBindingResult().getPropertyAccessor()).getAutoGrowCollectionLimit()).isEqualTo(512); + } + + @Test // SPR-15009 + public void testSetCustomMessageCodesResolverBeforeInitializeBindingResultForDirectFieldAccess() { + TestBean testBean = new TestBean(); + DataBinder binder = new DataBinder(testBean, "testBean"); + DefaultMessageCodesResolver messageCodesResolver = new DefaultMessageCodesResolver(); + messageCodesResolver.setPrefix("errors."); + binder.setMessageCodesResolver(messageCodesResolver); + binder.initDirectFieldAccess(); + + MutablePropertyValues mpv = new MutablePropertyValues(); + mpv.add("age", "invalid"); + binder.bind(mpv); + assertThat(binder.getBindingResult().getFieldError("age").getCode()).isEqualTo("errors.typeMismatch"); + } + + @Test // SPR-15009 + public void testSetCustomMessageCodesResolverAfterInitializeBindingResult() { + TestBean testBean = new TestBean(); + DataBinder binder = new DataBinder(testBean, "testBean"); + binder.initBeanPropertyAccess(); + DefaultMessageCodesResolver messageCodesResolver = new DefaultMessageCodesResolver(); + messageCodesResolver.setPrefix("errors."); + binder.setMessageCodesResolver(messageCodesResolver); + + MutablePropertyValues mpv = new MutablePropertyValues(); + mpv.add("age", "invalid"); + binder.bind(mpv); + assertThat(binder.getBindingResult().getFieldError("age").getCode()).isEqualTo("errors.typeMismatch"); + } + + @Test // SPR-15009 + public void testSetMessageCodesResolverIsNullAfterInitializeBindingResult() { + TestBean testBean = new TestBean(); + DataBinder binder = new DataBinder(testBean, "testBean"); + binder.initBeanPropertyAccess(); + binder.setMessageCodesResolver(null); + + MutablePropertyValues mpv = new MutablePropertyValues(); + mpv.add("age", "invalid"); + binder.bind(mpv); + // Keep a default MessageCodesResolver + assertThat(binder.getBindingResult().getFieldError("age").getCode()).isEqualTo("typeMismatch"); + } + + @Test // SPR-15009 + public void testCallSetMessageCodesResolverTwice() { + + TestBean testBean = new TestBean(); + DataBinder binder = new DataBinder(testBean, "testBean"); + binder.setMessageCodesResolver(new DefaultMessageCodesResolver()); + assertThatIllegalStateException().isThrownBy(() -> + binder.setMessageCodesResolver(new DefaultMessageCodesResolver())) + .withMessageContaining("DataBinder is already initialized with MessageCodesResolver"); + } + + @Test // gh-24347 + public void overrideBindingResultType() { + TestBean testBean = new TestBean(); + DataBinder binder = new DataBinder(testBean, "testBean"); + binder.initDirectFieldAccess(); + binder.initBeanPropertyAccess(); + assertThat(binder.getBindingResult()).isInstanceOf(BeanPropertyBindingResult.class); + } + + + @SuppressWarnings("unused") + private static class BeanWithIntegerList { + + private List integerList; + + public List getIntegerList() { + return integerList; + } + + public void setIntegerList(List integerList) { + this.integerList = integerList; + } + } + + + private static class Book { + + private String Title; + + private String ISBN; + + private int nInStock; + + public String getTitle() { + return Title; + } + + @SuppressWarnings("unused") + public void setTitle(String title) { + Title = title; + } + + public String getISBN() { + return ISBN; + } + + @SuppressWarnings("unused") + public void setISBN(String ISBN) { + this.ISBN = ISBN; + } + + public int getNInStock() { + return nInStock; + } + + @SuppressWarnings("unused") + public void setNInStock(int nInStock) { + this.nInStock = nInStock; + } + } + + + private static class OptionalHolder { + + private String id; + + private Optional name; + + public String getId() { + return id; + } + + @SuppressWarnings("unused") + public void setId(String id) { + this.id = id; + } + + public Optional getName() { + return name; + } + + @SuppressWarnings("unused") + public void setName(Optional name) { + this.name = name; + } + } + + + private static class TestBeanValidator implements Validator { + + @Override + public boolean supports(Class clazz) { + return TestBean.class.isAssignableFrom(clazz); + } + + @Override + public void validate(@Nullable Object obj, Errors errors) { + TestBean tb = (TestBean) obj; + if (tb.getAge() < 32) { + errors.rejectValue("age", "TOO_YOUNG", "simply too young"); + } + if (tb.getAge() % 2 == 0) { + errors.rejectValue("age", "AGE_NOT_ODD", "your age isn't odd"); + } + if (tb.getName() == null || !tb.getName().equals("Rod")) { + errors.rejectValue("name", "NOT_ROD", "are you sure you're not Rod?"); + } + if (tb.getTouchy() == null || !tb.getTouchy().equals(tb.getName())) { + errors.reject("NAME_TOUCHY_MISMATCH", "name and touchy do not match"); + } + if (tb.getAge() == 0) { + errors.reject("GENERAL_ERROR", new String[] {"arg"}, "msg"); + } + } + } + + + private static class SpouseValidator implements Validator { + + @Override + public boolean supports(Class clazz) { + return TestBean.class.isAssignableFrom(clazz); + } + + @Override + public void validate(@Nullable Object obj, Errors errors) { + TestBean tb = (TestBean) obj; + if (tb == null || "XXX".equals(tb.getName())) { + errors.rejectValue("", "SPOUSE_NOT_AVAILABLE"); + return; + } + if (tb.getAge() < 32) { + errors.rejectValue("age", "TOO_YOUNG", "simply too young"); + } + } + } + + + @SuppressWarnings("unused") + private static class GrowingList extends AbstractList { + + private List list; + + public GrowingList() { + this.list = new ArrayList<>(); + } + + public List getWrappedList() { + return list; + } + + @Override + public E get(int index) { + if (index >= list.size()) { + for (int i = list.size(); i < index; i++) { + list.add(null); + } + list.add(null); + return null; + } + else { + return list.get(index); + } + } + + @Override + public int size() { + return list.size(); + } + + @Override + public boolean add(E o) { + return list.add(o); + } + + @Override + public void add(int index, E element) { + list.add(index, element); + } + + @Override + public boolean addAll(int index, Collection c) { + return list.addAll(index, c); + } + + @Override + public void clear() { + list.clear(); + } + + @Override + public int indexOf(Object o) { + return list.indexOf(o); + } + + @Override + public Iterator iterator() { + return list.iterator(); + } + + @Override + public int lastIndexOf(Object o) { + return list.lastIndexOf(o); + } + + @Override + public ListIterator listIterator() { + return list.listIterator(); + } + + @Override + public ListIterator listIterator(int index) { + return list.listIterator(index); + } + + @Override + public E remove(int index) { + return list.remove(index); + } + + @Override + public E set(int index, E element) { + return list.set(index, element); + } + } + + + private static class Form { + + private final Map f; + + public Form() { + f = new HashMap<>(); + f.put("list", new GrowingList<>()); + } + + public Map getF() { + return f; + } + } + + + public static class NameBean { + + private final String name; + + public NameBean(String name) { + this.name = name; + } + public String getName() { + return name; + } + @Override + public String toString() { + return name; + } + } + + + public static class NameBeanConverter implements Converter { + + @Override + public String convert(NameBean source) { + return "[" + source.getName() + "]"; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/validation/DefaultMessageCodesResolverTests.java b/spring-context/src/test/java/org/springframework/validation/DefaultMessageCodesResolverTests.java new file mode 100644 index 0000000..962ea0c --- /dev/null +++ b/spring-context/src/test/java/org/springframework/validation/DefaultMessageCodesResolverTests.java @@ -0,0 +1,141 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.validation.DefaultMessageCodesResolver.Format; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DefaultMessageCodesResolver}. + * + * @author Phillip Webb + */ +class DefaultMessageCodesResolverTests { + + private final DefaultMessageCodesResolver resolver = new DefaultMessageCodesResolver(); + + + @Test + void shouldResolveMessageCode() throws Exception { + String[] codes = resolver.resolveMessageCodes("errorCode", "objectName"); + assertThat(codes).containsExactly("errorCode.objectName", "errorCode"); + } + + @Test + void shouldResolveFieldMessageCode() throws Exception { + String[] codes = resolver.resolveMessageCodes("errorCode", "objectName", "field", TestBean.class); + assertThat(codes).containsExactly( + "errorCode.objectName.field", + "errorCode.field", + "errorCode.org.springframework.beans.testfixture.beans.TestBean", + "errorCode"); + } + + @Test + void shouldResolveIndexedFieldMessageCode() throws Exception { + String[] codes = resolver.resolveMessageCodes("errorCode", "objectName", "a.b[3].c[5].d", TestBean.class); + assertThat(codes).containsExactly( + "errorCode.objectName.a.b[3].c[5].d", + "errorCode.objectName.a.b[3].c.d", + "errorCode.objectName.a.b.c.d", + "errorCode.a.b[3].c[5].d", + "errorCode.a.b[3].c.d", + "errorCode.a.b.c.d", + "errorCode.d", + "errorCode.org.springframework.beans.testfixture.beans.TestBean", + "errorCode"); + } + + @Test + void shouldResolveMessageCodeWithPrefix() throws Exception { + resolver.setPrefix("prefix."); + String[] codes = resolver.resolveMessageCodes("errorCode", "objectName"); + assertThat(codes).containsExactly("prefix.errorCode.objectName", "prefix.errorCode"); + } + + @Test + void shouldResolveFieldMessageCodeWithPrefix() throws Exception { + resolver.setPrefix("prefix."); + String[] codes = resolver.resolveMessageCodes("errorCode", "objectName", "field", TestBean.class); + assertThat(codes).containsExactly( + "prefix.errorCode.objectName.field", + "prefix.errorCode.field", + "prefix.errorCode.org.springframework.beans.testfixture.beans.TestBean", + "prefix.errorCode"); + } + + @Test + void shouldSupportNullPrefix() throws Exception { + resolver.setPrefix(null); + String[] codes = resolver.resolveMessageCodes("errorCode", "objectName", "field", TestBean.class); + assertThat(codes).containsExactly( + "errorCode.objectName.field", + "errorCode.field", + "errorCode.org.springframework.beans.testfixture.beans.TestBean", + "errorCode"); + } + + @Test + void shouldSupportMalformedIndexField() throws Exception { + String[] codes = resolver.resolveMessageCodes("errorCode", "objectName", "field[", TestBean.class); + assertThat(codes).containsExactly( + "errorCode.objectName.field[", + "errorCode.field[", + "errorCode.org.springframework.beans.testfixture.beans.TestBean", + "errorCode"); + } + + @Test + void shouldSupportNullFieldType() throws Exception { + String[] codes = resolver.resolveMessageCodes("errorCode", "objectName", "field", null); + assertThat(codes).containsExactly( + "errorCode.objectName.field", + "errorCode.field", + "errorCode"); + } + + @Test + void shouldSupportPostfixFormat() throws Exception { + resolver.setMessageCodeFormatter(Format.POSTFIX_ERROR_CODE); + String[] codes = resolver.resolveMessageCodes("errorCode", "objectName"); + assertThat(codes).containsExactly("objectName.errorCode", "errorCode"); + } + + @Test + void shouldSupportFieldPostfixFormat() throws Exception { + resolver.setMessageCodeFormatter(Format.POSTFIX_ERROR_CODE); + String[] codes = resolver.resolveMessageCodes("errorCode", "objectName", "field", TestBean.class); + assertThat(codes).containsExactly( + "objectName.field.errorCode", + "field.errorCode", + "org.springframework.beans.testfixture.beans.TestBean.errorCode", + "errorCode"); + } + + @Test + void shouldSupportCustomFormat() throws Exception { + resolver.setMessageCodeFormatter((errorCode, objectName, field) -> + DefaultMessageCodesResolver.Format.toDelimitedString("CUSTOM-" + errorCode, objectName, field)); + String[] codes = resolver.resolveMessageCodes("errorCode", "objectName"); + assertThat(codes).containsExactly("CUSTOM-errorCode.objectName", "CUSTOM-errorCode"); + } + +} diff --git a/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java b/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java new file mode 100644 index 0000000..0a027b9 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/validation/ValidationUtilsTests.java @@ -0,0 +1,194 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Unit tests for {@link ValidationUtils}. + * + * @author Juergen Hoeller + * @author Rick Evans + * @author Chris Beams + * @since 08.10.2004 + */ +public class ValidationUtilsTests { + + @Test + public void testInvokeValidatorWithNullValidator() throws Exception { + TestBean tb = new TestBean(); + Errors errors = new BeanPropertyBindingResult(tb, "tb"); + assertThatIllegalArgumentException().isThrownBy(() -> + ValidationUtils.invokeValidator(null, tb, errors)); + } + + @Test + public void testInvokeValidatorWithNullErrors() throws Exception { + TestBean tb = new TestBean(); + assertThatIllegalArgumentException().isThrownBy(() -> + ValidationUtils.invokeValidator(new EmptyValidator(), tb, null)); + } + + @Test + public void testInvokeValidatorSunnyDay() throws Exception { + TestBean tb = new TestBean(); + Errors errors = new BeanPropertyBindingResult(tb, "tb"); + ValidationUtils.invokeValidator(new EmptyValidator(), tb, errors); + assertThat(errors.hasFieldErrors("name")).isTrue(); + assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY"); + } + + @Test + public void testValidationUtilsSunnyDay() throws Exception { + TestBean tb = new TestBean(""); + + Validator testValidator = new EmptyValidator(); + tb.setName(" "); + Errors errors = new BeanPropertyBindingResult(tb, "tb"); + testValidator.validate(tb, errors); + assertThat(errors.hasFieldErrors("name")).isFalse(); + + tb.setName("Roddy"); + errors = new BeanPropertyBindingResult(tb, "tb"); + testValidator.validate(tb, errors); + assertThat(errors.hasFieldErrors("name")).isFalse(); + } + + @Test + public void testValidationUtilsNull() throws Exception { + TestBean tb = new TestBean(); + Errors errors = new BeanPropertyBindingResult(tb, "tb"); + Validator testValidator = new EmptyValidator(); + testValidator.validate(tb, errors); + assertThat(errors.hasFieldErrors("name")).isTrue(); + assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY"); + } + + @Test + public void testValidationUtilsEmpty() throws Exception { + TestBean tb = new TestBean(""); + Errors errors = new BeanPropertyBindingResult(tb, "tb"); + Validator testValidator = new EmptyValidator(); + testValidator.validate(tb, errors); + assertThat(errors.hasFieldErrors("name")).isTrue(); + assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY"); + } + + @Test + public void testValidationUtilsEmptyVariants() { + TestBean tb = new TestBean(); + + Errors errors = new BeanPropertyBindingResult(tb, "tb"); + ValidationUtils.rejectIfEmpty(errors, "name", "EMPTY_OR_WHITESPACE", new Object[] {"arg"}); + assertThat(errors.hasFieldErrors("name")).isTrue(); + assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY_OR_WHITESPACE"); + assertThat(errors.getFieldError("name").getArguments()[0]).isEqualTo("arg"); + + errors = new BeanPropertyBindingResult(tb, "tb"); + ValidationUtils.rejectIfEmpty(errors, "name", "EMPTY_OR_WHITESPACE", new Object[] {"arg"}, "msg"); + assertThat(errors.hasFieldErrors("name")).isTrue(); + assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY_OR_WHITESPACE"); + assertThat(errors.getFieldError("name").getArguments()[0]).isEqualTo("arg"); + assertThat(errors.getFieldError("name").getDefaultMessage()).isEqualTo("msg"); + } + + @Test + public void testValidationUtilsEmptyOrWhitespace() throws Exception { + TestBean tb = new TestBean(); + Validator testValidator = new EmptyOrWhitespaceValidator(); + + // Test null + Errors errors = new BeanPropertyBindingResult(tb, "tb"); + testValidator.validate(tb, errors); + assertThat(errors.hasFieldErrors("name")).isTrue(); + assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY_OR_WHITESPACE"); + + // Test empty String + tb.setName(""); + errors = new BeanPropertyBindingResult(tb, "tb"); + testValidator.validate(tb, errors); + assertThat(errors.hasFieldErrors("name")).isTrue(); + assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY_OR_WHITESPACE"); + + // Test whitespace String + tb.setName(" "); + errors = new BeanPropertyBindingResult(tb, "tb"); + testValidator.validate(tb, errors); + assertThat(errors.hasFieldErrors("name")).isTrue(); + assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY_OR_WHITESPACE"); + + // Test OK + tb.setName("Roddy"); + errors = new BeanPropertyBindingResult(tb, "tb"); + testValidator.validate(tb, errors); + assertThat(errors.hasFieldErrors("name")).isFalse(); + } + + @Test + public void testValidationUtilsEmptyOrWhitespaceVariants() { + TestBean tb = new TestBean(); + tb.setName(" "); + + Errors errors = new BeanPropertyBindingResult(tb, "tb"); + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "EMPTY_OR_WHITESPACE", new Object[] {"arg"}); + assertThat(errors.hasFieldErrors("name")).isTrue(); + assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY_OR_WHITESPACE"); + assertThat(errors.getFieldError("name").getArguments()[0]).isEqualTo("arg"); + + errors = new BeanPropertyBindingResult(tb, "tb"); + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "EMPTY_OR_WHITESPACE", new Object[] {"arg"}, "msg"); + assertThat(errors.hasFieldErrors("name")).isTrue(); + assertThat(errors.getFieldError("name").getCode()).isEqualTo("EMPTY_OR_WHITESPACE"); + assertThat(errors.getFieldError("name").getArguments()[0]).isEqualTo("arg"); + assertThat(errors.getFieldError("name").getDefaultMessage()).isEqualTo("msg"); + } + + + private static class EmptyValidator implements Validator { + + @Override + public boolean supports(Class clazz) { + return TestBean.class.isAssignableFrom(clazz); + } + + @Override + public void validate(@Nullable Object obj, Errors errors) { + ValidationUtils.rejectIfEmpty(errors, "name", "EMPTY", "You must enter a name!"); + } + } + + + private static class EmptyOrWhitespaceValidator implements Validator { + + @Override + public boolean supports(Class clazz) { + return TestBean.class.isAssignableFrom(clazz); + } + + @Override + public void validate(@Nullable Object obj, Errors errors) { + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "EMPTY_OR_WHITESPACE", "You must enter a name!"); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/BeanValidationPostProcessorTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/BeanValidationPostProcessorTests.java new file mode 100644 index 0000000..4ec8ce0 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/BeanValidationPostProcessorTests.java @@ -0,0 +1,175 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation.beanvalidation; + +import javax.annotation.PostConstruct; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.annotation.CommonAnnotationBeanPostProcessor; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.AsyncAnnotationAdvisor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Juergen Hoeller + */ +public class BeanValidationPostProcessorTests { + + @Test + public void testNotNullConstraint() { + GenericApplicationContext ac = new GenericApplicationContext(); + ac.registerBeanDefinition("bvpp", new RootBeanDefinition(BeanValidationPostProcessor.class)); + ac.registerBeanDefinition("capp", new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class)); + ac.registerBeanDefinition("bean", new RootBeanDefinition(NotNullConstrainedBean.class)); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(ac::refresh) + .havingRootCause() + .withMessageContainingAll("testBean", "invalid"); + ac.close(); + } + + @Test + public void testNotNullConstraintSatisfied() { + GenericApplicationContext ac = new GenericApplicationContext(); + ac.registerBeanDefinition("bvpp", new RootBeanDefinition(BeanValidationPostProcessor.class)); + ac.registerBeanDefinition("capp", new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class)); + RootBeanDefinition bd = new RootBeanDefinition(NotNullConstrainedBean.class); + bd.getPropertyValues().add("testBean", new TestBean()); + ac.registerBeanDefinition("bean", bd); + ac.refresh(); + ac.close(); + } + + @Test + public void testNotNullConstraintAfterInitialization() { + GenericApplicationContext ac = new GenericApplicationContext(); + RootBeanDefinition bvpp = new RootBeanDefinition(BeanValidationPostProcessor.class); + bvpp.getPropertyValues().add("afterInitialization", true); + ac.registerBeanDefinition("bvpp", bvpp); + ac.registerBeanDefinition("capp", new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class)); + ac.registerBeanDefinition("bean", new RootBeanDefinition(AfterInitConstraintBean.class)); + ac.refresh(); + ac.close(); + } + + @Test + public void testNotNullConstraintAfterInitializationWithProxy() { + GenericApplicationContext ac = new GenericApplicationContext(); + RootBeanDefinition bvpp = new RootBeanDefinition(BeanValidationPostProcessor.class); + bvpp.getPropertyValues().add("afterInitialization", true); + ac.registerBeanDefinition("bvpp", bvpp); + ac.registerBeanDefinition("capp", new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class)); + ac.registerBeanDefinition("bean", new RootBeanDefinition(AfterInitConstraintBean.class)); + ac.registerBeanDefinition("autoProxyCreator", new RootBeanDefinition(DefaultAdvisorAutoProxyCreator.class)); + ac.registerBeanDefinition("asyncAdvisor", new RootBeanDefinition(AsyncAnnotationAdvisor.class)); + ac.refresh(); + ac.close(); + } + + @Test + public void testSizeConstraint() { + GenericApplicationContext ac = new GenericApplicationContext(); + ac.registerBeanDefinition("bvpp", new RootBeanDefinition(BeanValidationPostProcessor.class)); + RootBeanDefinition bd = new RootBeanDefinition(NotNullConstrainedBean.class); + bd.getPropertyValues().add("testBean", new TestBean()); + bd.getPropertyValues().add("stringValue", "s"); + ac.registerBeanDefinition("bean", bd); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> ac.refresh()) + .havingRootCause() + .withMessageContainingAll("stringValue", "invalid"); + ac.close(); + } + + @Test + public void testSizeConstraintSatisfied() { + GenericApplicationContext ac = new GenericApplicationContext(); + ac.registerBeanDefinition("bvpp", new RootBeanDefinition(BeanValidationPostProcessor.class)); + RootBeanDefinition bd = new RootBeanDefinition(NotNullConstrainedBean.class); + bd.getPropertyValues().add("testBean", new TestBean()); + bd.getPropertyValues().add("stringValue", "ss"); + ac.registerBeanDefinition("bean", bd); + ac.refresh(); + ac.close(); + } + + + public static class NotNullConstrainedBean { + + @NotNull + private TestBean testBean; + + @Size(min = 2) + private String stringValue; + + public TestBean getTestBean() { + return testBean; + } + + public void setTestBean(TestBean testBean) { + this.testBean = testBean; + } + + public String getStringValue() { + return stringValue; + } + + public void setStringValue(String stringValue) { + this.stringValue = stringValue; + } + + @PostConstruct + public void init() { + assertThat(this.testBean).as("Shouldn't be here after constraint checking").isNotNull(); + } + } + + + public static class AfterInitConstraintBean { + + @NotNull + private TestBean testBean; + + public TestBean getTestBean() { + return testBean; + } + + public void setTestBean(TestBean testBean) { + this.testBean = testBean; + } + + @PostConstruct + public void init() { + this.testBean = new TestBean(); + } + + @Async + void asyncMethod() { + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationTests.java new file mode 100644 index 0000000..bdfa870 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationTests.java @@ -0,0 +1,214 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation.beanvalidation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.validation.ValidationException; +import javax.validation.Validator; +import javax.validation.constraints.Max; +import javax.validation.constraints.NotNull; +import javax.validation.groups.Default; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.AsyncAnnotationAdvisor; +import org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor; +import org.springframework.validation.annotation.Validated; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Juergen Hoeller + */ +public class MethodValidationTests { + + @Test + public void testMethodValidationInterceptor() { + MyValidBean bean = new MyValidBean(); + ProxyFactory proxyFactory = new ProxyFactory(bean); + proxyFactory.addAdvice(new MethodValidationInterceptor()); + proxyFactory.addAdvisor(new AsyncAnnotationAdvisor()); + doTestProxyValidation((MyValidInterface) proxyFactory.getProxy()); + } + + @Test + public void testMethodValidationPostProcessor() { + StaticApplicationContext ac = new StaticApplicationContext(); + ac.registerSingleton("mvpp", MethodValidationPostProcessor.class); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("beforeExistingAdvisors", false); + ac.registerSingleton("aapp", AsyncAnnotationBeanPostProcessor.class, pvs); + ac.registerSingleton("bean", MyValidBean.class); + ac.refresh(); + doTestProxyValidation(ac.getBean("bean", MyValidInterface.class)); + ac.close(); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private void doTestProxyValidation(MyValidInterface proxy) { + assertThat(proxy.myValidMethod("value", 5)).isNotNull(); + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> + proxy.myValidMethod("value", 15)); + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> + proxy.myValidMethod(null, 5)); + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> + proxy.myValidMethod("value", 0)); + proxy.myValidAsyncMethod("value", 5); + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> + proxy.myValidAsyncMethod("value", 15)); + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> + proxy.myValidAsyncMethod(null, 5)); + assertThat(proxy.myGenericMethod("myValue")).isEqualTo("myValue"); + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> + proxy.myGenericMethod(null)); + } + + @Test + @SuppressWarnings("resource") + public void testLazyValidatorForMethodValidation() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext( + LazyMethodValidationConfig.class, CustomValidatorBean.class, + MyValidBean.class, MyValidFactoryBean.class); + ctx.getBeansOfType(MyValidInterface.class).values().forEach(bean -> bean.myValidMethod("value", 5)); + } + + @Test + @SuppressWarnings("resource") + public void testLazyValidatorForMethodValidationWithProxyTargetClass() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext( + LazyMethodValidationConfigWithProxyTargetClass.class, CustomValidatorBean.class, + MyValidBean.class, MyValidFactoryBean.class); + ctx.getBeansOfType(MyValidInterface.class).values().forEach(bean -> bean.myValidMethod("value", 5)); + } + + + @MyStereotype + public static class MyValidBean implements MyValidInterface { + + @Override + public Object myValidMethod(String arg1, int arg2) { + return (arg2 == 0 ? null : "value"); + } + + @Override + public void myValidAsyncMethod(String arg1, int arg2) { + } + + @Override + public String myGenericMethod(String value) { + return value; + } + } + + + @MyStereotype + public static class MyValidFactoryBean implements FactoryBean, MyValidInterface { + + @Override + public String getObject() { + return null; + } + + @Override + public Class getObjectType() { + return String.class; + } + + @Override + public Object myValidMethod(String arg1, int arg2) { + return (arg2 == 0 ? null : "value"); + } + + @Override + public void myValidAsyncMethod(String arg1, int arg2) { + } + + @Override + public String myGenericMethod(String value) { + return value; + } + } + + + public interface MyValidInterface { + + @NotNull Object myValidMethod(@NotNull(groups = MyGroup.class) String arg1, @Max(10) int arg2); + + @MyValid + @Async void myValidAsyncMethod(@NotNull(groups = OtherGroup.class) String arg1, @Max(10) int arg2); + + T myGenericMethod(@NotNull T value); + } + + + public interface MyGroup { + } + + + public interface OtherGroup { + } + + + @Validated({MyGroup.class, Default.class}) + @Retention(RetentionPolicy.RUNTIME) + public @interface MyStereotype { + } + + + @Validated({OtherGroup.class, Default.class}) + @Retention(RetentionPolicy.RUNTIME) + public @interface MyValid { + } + + + @Configuration + public static class LazyMethodValidationConfig { + + @Bean + public static MethodValidationPostProcessor methodValidationPostProcessor(@Lazy Validator validator) { + MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor(); + postProcessor.setValidator(validator); + return postProcessor; + } + } + + + @Configuration + public static class LazyMethodValidationConfigWithProxyTargetClass { + + @Bean + public static MethodValidationPostProcessor methodValidationPostProcessor(@Lazy Validator validator) { + MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor(); + postProcessor.setValidator(validator); + postProcessor.setProxyTargetClass(true); + return postProcessor; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/SpringValidatorAdapterTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/SpringValidatorAdapterTests.java new file mode 100644 index 0000000..fe5f62b --- /dev/null +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/SpringValidatorAdapterTests.java @@ -0,0 +1,488 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation.beanvalidation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import javax.validation.Constraint; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import javax.validation.ConstraintViolation; +import javax.validation.Payload; +import javax.validation.Valid; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.BeanWrapperImpl; +import org.springframework.context.support.StaticMessageSource; +import org.springframework.core.testfixture.io.SerializationTestUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.FieldError; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Kazuki Shimizu + * @author Juergen Hoeller + */ +public class SpringValidatorAdapterTests { + + private final Validator nativeValidator = Validation.buildDefaultValidatorFactory().getValidator(); + + private final SpringValidatorAdapter validatorAdapter = new SpringValidatorAdapter(nativeValidator); + + private final StaticMessageSource messageSource = new StaticMessageSource(); + + + @BeforeEach + public void setupSpringValidatorAdapter() { + messageSource.addMessage("Size", Locale.ENGLISH, "Size of {0} must be between {2} and {1}"); + messageSource.addMessage("Same", Locale.ENGLISH, "{2} must be same value as {1}"); + messageSource.addMessage("password", Locale.ENGLISH, "Password"); + messageSource.addMessage("confirmPassword", Locale.ENGLISH, "Password(Confirm)"); + } + + + @Test + public void testUnwrap() { + Validator nativeValidator = validatorAdapter.unwrap(Validator.class); + assertThat(nativeValidator).isSameAs(this.nativeValidator); + } + + @Test // SPR-13406 + public void testNoStringArgumentValue() throws Exception { + TestBean testBean = new TestBean(); + testBean.setPassword("pass"); + testBean.setConfirmPassword("pass"); + + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(testBean, "testBean"); + validatorAdapter.validate(testBean, errors); + + assertThat(errors.getFieldErrorCount("password")).isEqualTo(1); + assertThat(errors.getFieldValue("password")).isEqualTo("pass"); + FieldError error = errors.getFieldError("password"); + assertThat(error).isNotNull(); + assertThat(messageSource.getMessage(error, Locale.ENGLISH)).isEqualTo("Size of Password must be between 8 and 128"); + assertThat(error.contains(ConstraintViolation.class)).isTrue(); + assertThat(error.unwrap(ConstraintViolation.class).getPropertyPath().toString()).isEqualTo("password"); + assertThat(SerializationTestUtils.serializeAndDeserialize(error.toString())).isEqualTo(error.toString()); + } + + @Test // SPR-13406 + public void testApplyMessageSourceResolvableToStringArgumentValueWithResolvedLogicalFieldName() throws Exception { + TestBean testBean = new TestBean(); + testBean.setPassword("password"); + testBean.setConfirmPassword("PASSWORD"); + + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(testBean, "testBean"); + validatorAdapter.validate(testBean, errors); + + assertThat(errors.getFieldErrorCount("password")).isEqualTo(1); + assertThat(errors.getFieldValue("password")).isEqualTo("password"); + FieldError error = errors.getFieldError("password"); + assertThat(error).isNotNull(); + assertThat(messageSource.getMessage(error, Locale.ENGLISH)).isEqualTo("Password must be same value as Password(Confirm)"); + assertThat(error.contains(ConstraintViolation.class)).isTrue(); + assertThat(error.unwrap(ConstraintViolation.class).getPropertyPath().toString()).isEqualTo("password"); + assertThat(SerializationTestUtils.serializeAndDeserialize(error.toString())).isEqualTo(error.toString()); + } + + @Test // SPR-13406 + public void testApplyMessageSourceResolvableToStringArgumentValueWithUnresolvedLogicalFieldName() { + TestBean testBean = new TestBean(); + testBean.setEmail("test@example.com"); + testBean.setConfirmEmail("TEST@EXAMPLE.IO"); + + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(testBean, "testBean"); + validatorAdapter.validate(testBean, errors); + + assertThat(errors.getFieldErrorCount("email")).isEqualTo(1); + assertThat(errors.getFieldValue("email")).isEqualTo("test@example.com"); + assertThat(errors.getFieldErrorCount("confirmEmail")).isEqualTo(1); + FieldError error1 = errors.getFieldError("email"); + FieldError error2 = errors.getFieldError("confirmEmail"); + assertThat(error1).isNotNull(); + assertThat(error2).isNotNull(); + assertThat(messageSource.getMessage(error1, Locale.ENGLISH)).isEqualTo("email must be same value as confirmEmail"); + assertThat(messageSource.getMessage(error2, Locale.ENGLISH)).isEqualTo("Email required"); + assertThat(error1.contains(ConstraintViolation.class)).isTrue(); + assertThat(error1.unwrap(ConstraintViolation.class).getPropertyPath().toString()).isEqualTo("email"); + assertThat(error2.contains(ConstraintViolation.class)).isTrue(); + assertThat(error2.unwrap(ConstraintViolation.class).getPropertyPath().toString()).isEqualTo("confirmEmail"); + } + + @Test // SPR-15123 + public void testApplyMessageSourceResolvableToStringArgumentValueWithAlwaysUseMessageFormat() { + messageSource.setAlwaysUseMessageFormat(true); + + TestBean testBean = new TestBean(); + testBean.setEmail("test@example.com"); + testBean.setConfirmEmail("TEST@EXAMPLE.IO"); + + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(testBean, "testBean"); + validatorAdapter.validate(testBean, errors); + + assertThat(errors.getFieldErrorCount("email")).isEqualTo(1); + assertThat(errors.getFieldValue("email")).isEqualTo("test@example.com"); + assertThat(errors.getFieldErrorCount("confirmEmail")).isEqualTo(1); + FieldError error1 = errors.getFieldError("email"); + FieldError error2 = errors.getFieldError("confirmEmail"); + assertThat(error1).isNotNull(); + assertThat(error2).isNotNull(); + assertThat(messageSource.getMessage(error1, Locale.ENGLISH)).isEqualTo("email must be same value as confirmEmail"); + assertThat(messageSource.getMessage(error2, Locale.ENGLISH)).isEqualTo("Email required"); + assertThat(error1.contains(ConstraintViolation.class)).isTrue(); + assertThat(error1.unwrap(ConstraintViolation.class).getPropertyPath().toString()).isEqualTo("email"); + assertThat(error2.contains(ConstraintViolation.class)).isTrue(); + assertThat(error2.unwrap(ConstraintViolation.class).getPropertyPath().toString()).isEqualTo("confirmEmail"); + } + + @Test + public void testPatternMessage() { + TestBean testBean = new TestBean(); + testBean.setEmail("X"); + testBean.setConfirmEmail("X"); + + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(testBean, "testBean"); + validatorAdapter.validate(testBean, errors); + + assertThat(errors.getFieldErrorCount("email")).isEqualTo(1); + assertThat(errors.getFieldValue("email")).isEqualTo("X"); + FieldError error = errors.getFieldError("email"); + assertThat(error).isNotNull(); + assertThat(messageSource.getMessage(error, Locale.ENGLISH)).contains("[\\w.'-]{1,}@[\\w.'-]{1,}"); + assertThat(error.contains(ConstraintViolation.class)).isTrue(); + assertThat(error.unwrap(ConstraintViolation.class).getPropertyPath().toString()).isEqualTo("email"); + } + + @Test // SPR-16177 + public void testWithList() { + Parent parent = new Parent(); + parent.setName("Parent whit list"); + parent.getChildList().addAll(createChildren(parent)); + + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(parent, "parent"); + validatorAdapter.validate(parent, errors); + + assertThat(errors.getErrorCount() > 0).isTrue(); + } + + @Test // SPR-16177 + public void testWithSet() { + Parent parent = new Parent(); + parent.setName("Parent with set"); + parent.getChildSet().addAll(createChildren(parent)); + + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(parent, "parent"); + validatorAdapter.validate(parent, errors); + + assertThat(errors.getErrorCount() > 0).isTrue(); + } + + private List createChildren(Parent parent) { + Child child1 = new Child(); + child1.setName("Child1"); + child1.setAge(null); + child1.setParent(parent); + + Child child2 = new Child(); + child2.setName(null); + child2.setAge(17); + child2.setParent(parent); + + return Arrays.asList(child1, child2); + } + + + @Same(field = "password", comparingField = "confirmPassword") + @Same(field = "email", comparingField = "confirmEmail") + static class TestBean { + + @Size(min = 8, max = 128) + private String password; + + private String confirmPassword; + + @Pattern(regexp = "[\\w.'-]{1,}@[\\w.'-]{1,}") + private String email; + + @Pattern(regexp = "[\\p{L} -]*", message = "Email required") + private String confirmEmail; + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getConfirmPassword() { + return confirmPassword; + } + + public void setConfirmPassword(String confirmPassword) { + this.confirmPassword = confirmPassword; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getConfirmEmail() { + return confirmEmail; + } + + public void setConfirmEmail(String confirmEmail) { + this.confirmEmail = confirmEmail; + } + } + + + @Documented + @Constraint(validatedBy = {SameValidator.class}) + @Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @Repeatable(SameGroup.class) + @interface Same { + + String message() default "{org.springframework.validation.beanvalidation.Same.message}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + String field(); + + String comparingField(); + + @Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @interface List { + Same[] value(); + } + } + + + @Documented + @Inherited + @Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @interface SameGroup { + + Same[] value(); + } + + + public static class SameValidator implements ConstraintValidator { + + private String field; + + private String comparingField; + + private String message; + + @Override + public void initialize(Same constraintAnnotation) { + field = constraintAnnotation.field(); + comparingField = constraintAnnotation.comparingField(); + message = constraintAnnotation.message(); + } + + @Override + public boolean isValid(Object value, ConstraintValidatorContext context) { + BeanWrapper beanWrapper = new BeanWrapperImpl(value); + Object fieldValue = beanWrapper.getPropertyValue(field); + Object comparingFieldValue = beanWrapper.getPropertyValue(comparingField); + boolean matched = ObjectUtils.nullSafeEquals(fieldValue, comparingFieldValue); + if (matched) { + return true; + } + else { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(message) + .addPropertyNode(field) + .addConstraintViolation(); + return false; + } + } + } + + + public static class Parent { + + private Integer id; + + @NotNull + private String name; + + @Valid + private Set childSet = new LinkedHashSet<>(); + + @Valid + private List childList = new ArrayList<>(); + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Set getChildSet() { + return childSet; + } + + public void setChildSet(Set childSet) { + this.childSet = childSet; + } + + public List getChildList() { + return childList; + } + + public void setChildList(List childList) { + this.childList = childList; + } + } + + + @AnythingValid + public static class Child { + + private Integer id; + + @NotNull + private String name; + + @NotNull + private Integer age; + + @NotNull + private Parent parent; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + + public Parent getParent() { + return parent; + } + + public void setParent(Parent parent) { + this.parent = parent; + } + } + + + @Constraint(validatedBy = AnythingValidator.class) + @Retention(RetentionPolicy.RUNTIME) + public @interface AnythingValid { + + String message() default "{AnythingValid.message}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + } + + + public static class AnythingValidator implements ConstraintValidator { + + private static final String ID = "id"; + + @Override + public void initialize(AnythingValid constraintAnnotation) { + } + + @Override + public boolean isValid(Object value, ConstraintValidatorContext context) { + List fieldsErrors = new ArrayList<>(); + Arrays.asList(value.getClass().getDeclaredFields()).forEach(field -> { + field.setAccessible(true); + try { + if (!field.getName().equals(ID) && field.get(value) == null) { + fieldsErrors.add(field); + context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()) + .addPropertyNode(field.getName()) + .addConstraintViolation(); + } + } + catch (IllegalAccessException ex) { + throw new IllegalStateException(ex); + } + }); + return fieldsErrors.isEmpty(); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java new file mode 100644 index 0000000..a8ae011 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java @@ -0,0 +1,503 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.validation.beanvalidation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import javax.validation.Constraint; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import javax.validation.ConstraintViolation; +import javax.validation.Payload; +import javax.validation.Valid; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; +import javax.validation.constraints.NotNull; + +import org.hibernate.validator.HibernateValidator; +import org.hibernate.validator.HibernateValidatorFactory; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.env.Environment; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.Errors; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + */ +public class ValidatorFactoryTests { + + @Test + public void testSimpleValidation() { + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.afterPropertiesSet(); + + ValidPerson person = new ValidPerson(); + Set> result = validator.validate(person); + assertThat(result.size()).isEqualTo(2); + for (ConstraintViolation cv : result) { + String path = cv.getPropertyPath().toString(); + assertThat(path).matches(actual -> "name".equals(actual) || "address.street".equals(actual)); + assertThat(cv.getConstraintDescriptor().getAnnotation()).isInstanceOf(NotNull.class); + } + + Validator nativeValidator = validator.unwrap(Validator.class); + assertThat(nativeValidator.getClass().getName().startsWith("org.hibernate")).isTrue(); + assertThat(validator.unwrap(ValidatorFactory.class)).isInstanceOf(HibernateValidatorFactory.class); + assertThat(validator.unwrap(HibernateValidatorFactory.class)).isInstanceOf(HibernateValidatorFactory.class); + + validator.destroy(); + } + + @Test + public void testSimpleValidationWithCustomProvider() { + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.setProviderClass(HibernateValidator.class); + validator.afterPropertiesSet(); + + ValidPerson person = new ValidPerson(); + Set> result = validator.validate(person); + assertThat(result.size()).isEqualTo(2); + for (ConstraintViolation cv : result) { + String path = cv.getPropertyPath().toString(); + assertThat(path).matches(actual -> "name".equals(actual) || "address.street".equals(actual)); + assertThat(cv.getConstraintDescriptor().getAnnotation()).isInstanceOf(NotNull.class); + } + + Validator nativeValidator = validator.unwrap(Validator.class); + assertThat(nativeValidator.getClass().getName().startsWith("org.hibernate")).isTrue(); + assertThat(validator.unwrap(ValidatorFactory.class)).isInstanceOf(HibernateValidatorFactory.class); + assertThat(validator.unwrap(HibernateValidatorFactory.class)).isInstanceOf(HibernateValidatorFactory.class); + + validator.destroy(); + } + + @Test + public void testSimpleValidationWithClassLevel() { + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.afterPropertiesSet(); + + ValidPerson person = new ValidPerson(); + person.setName("Juergen"); + person.getAddress().setStreet("Juergen's Street"); + Set> result = validator.validate(person); + assertThat(result.size()).isEqualTo(1); + Iterator> iterator = result.iterator(); + ConstraintViolation cv = iterator.next(); + assertThat(cv.getPropertyPath().toString()).isEqualTo(""); + assertThat(cv.getConstraintDescriptor().getAnnotation() instanceof NameAddressValid).isTrue(); + } + + @Test + public void testSpringValidationFieldType() { + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.afterPropertiesSet(); + + ValidPerson person = new ValidPerson(); + person.setName("Phil"); + person.getAddress().setStreet("Phil's Street"); + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(person, "person"); + validator.validate(person, errors); + assertThat(errors.getErrorCount()).isEqualTo(1); + assertThat(errors.getFieldError("address").getRejectedValue()) + .as("Field/Value type mismatch") + .isInstanceOf(ValidAddress.class); + } + + @Test + public void testSpringValidation() { + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.afterPropertiesSet(); + + ValidPerson person = new ValidPerson(); + BeanPropertyBindingResult result = new BeanPropertyBindingResult(person, "person"); + validator.validate(person, result); + assertThat(result.getErrorCount()).isEqualTo(2); + FieldError fieldError = result.getFieldError("name"); + assertThat(fieldError.getField()).isEqualTo("name"); + List errorCodes = Arrays.asList(fieldError.getCodes()); + assertThat(errorCodes.size()).isEqualTo(4); + assertThat(errorCodes.contains("NotNull.person.name")).isTrue(); + assertThat(errorCodes.contains("NotNull.name")).isTrue(); + assertThat(errorCodes.contains("NotNull.java.lang.String")).isTrue(); + assertThat(errorCodes.contains("NotNull")).isTrue(); + fieldError = result.getFieldError("address.street"); + assertThat(fieldError.getField()).isEqualTo("address.street"); + errorCodes = Arrays.asList(fieldError.getCodes()); + assertThat(errorCodes.size()).isEqualTo(5); + assertThat(errorCodes.contains("NotNull.person.address.street")).isTrue(); + assertThat(errorCodes.contains("NotNull.address.street")).isTrue(); + assertThat(errorCodes.contains("NotNull.street")).isTrue(); + assertThat(errorCodes.contains("NotNull.java.lang.String")).isTrue(); + assertThat(errorCodes.contains("NotNull")).isTrue(); + } + + @Test + public void testSpringValidationWithClassLevel() { + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.afterPropertiesSet(); + + ValidPerson person = new ValidPerson(); + person.setName("Juergen"); + person.getAddress().setStreet("Juergen's Street"); + BeanPropertyBindingResult result = new BeanPropertyBindingResult(person, "person"); + validator.validate(person, result); + assertThat(result.getErrorCount()).isEqualTo(1); + ObjectError globalError = result.getGlobalError(); + List errorCodes = Arrays.asList(globalError.getCodes()); + assertThat(errorCodes.size()).isEqualTo(2); + assertThat(errorCodes.contains("NameAddressValid.person")).isTrue(); + assertThat(errorCodes.contains("NameAddressValid")).isTrue(); + } + + @Test + public void testSpringValidationWithAutowiredValidator() { + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext( + LocalValidatorFactoryBean.class); + LocalValidatorFactoryBean validator = ctx.getBean(LocalValidatorFactoryBean.class); + + ValidPerson person = new ValidPerson(); + person.expectsAutowiredValidator = true; + person.setName("Juergen"); + person.getAddress().setStreet("Juergen's Street"); + BeanPropertyBindingResult result = new BeanPropertyBindingResult(person, "person"); + validator.validate(person, result); + assertThat(result.getErrorCount()).isEqualTo(1); + ObjectError globalError = result.getGlobalError(); + List errorCodes = Arrays.asList(globalError.getCodes()); + assertThat(errorCodes.size()).isEqualTo(2); + assertThat(errorCodes.contains("NameAddressValid.person")).isTrue(); + assertThat(errorCodes.contains("NameAddressValid")).isTrue(); + ctx.close(); + } + + @Test + public void testSpringValidationWithErrorInListElement() { + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.afterPropertiesSet(); + + ValidPerson person = new ValidPerson(); + person.getAddressList().add(new ValidAddress()); + BeanPropertyBindingResult result = new BeanPropertyBindingResult(person, "person"); + validator.validate(person, result); + assertThat(result.getErrorCount()).isEqualTo(3); + FieldError fieldError = result.getFieldError("name"); + assertThat(fieldError.getField()).isEqualTo("name"); + fieldError = result.getFieldError("address.street"); + assertThat(fieldError.getField()).isEqualTo("address.street"); + fieldError = result.getFieldError("addressList[0].street"); + assertThat(fieldError.getField()).isEqualTo("addressList[0].street"); + } + + @Test + public void testSpringValidationWithErrorInSetElement() { + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.afterPropertiesSet(); + + ValidPerson person = new ValidPerson(); + person.getAddressSet().add(new ValidAddress()); + BeanPropertyBindingResult result = new BeanPropertyBindingResult(person, "person"); + validator.validate(person, result); + assertThat(result.getErrorCount()).isEqualTo(3); + FieldError fieldError = result.getFieldError("name"); + assertThat(fieldError.getField()).isEqualTo("name"); + fieldError = result.getFieldError("address.street"); + assertThat(fieldError.getField()).isEqualTo("address.street"); + fieldError = result.getFieldError("addressSet[].street"); + assertThat(fieldError.getField()).isEqualTo("addressSet[].street"); + } + + @Test + public void testInnerBeanValidation() { + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.afterPropertiesSet(); + + MainBean mainBean = new MainBean(); + Errors errors = new BeanPropertyBindingResult(mainBean, "mainBean"); + validator.validate(mainBean, errors); + Object rejected = errors.getFieldValue("inner.value"); + assertThat(rejected).isNull(); + } + + @Test + public void testValidationWithOptionalField() { + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.afterPropertiesSet(); + + MainBeanWithOptional mainBean = new MainBeanWithOptional(); + Errors errors = new BeanPropertyBindingResult(mainBean, "mainBean"); + validator.validate(mainBean, errors); + Object rejected = errors.getFieldValue("inner.value"); + assertThat(rejected).isNull(); + } + + @Test + public void testListValidation() { + LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); + validator.afterPropertiesSet(); + + ListContainer listContainer = new ListContainer(); + listContainer.addString("A"); + listContainer.addString("X"); + + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(listContainer, "listContainer"); + errors.initConversion(new DefaultConversionService()); + validator.validate(listContainer, errors); + + FieldError fieldError = errors.getFieldError("list[1]"); + assertThat(fieldError).isNotNull(); + assertThat(fieldError.getRejectedValue()).isEqualTo("X"); + assertThat(errors.getFieldValue("list[1]")).isEqualTo("X"); + } + + + @NameAddressValid + public static class ValidPerson { + + @NotNull + private String name; + + @Valid + private ValidAddress address = new ValidAddress(); + + @Valid + private List addressList = new ArrayList<>(); + + @Valid + private Set addressSet = new LinkedHashSet<>(); + + public boolean expectsAutowiredValidator = false; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public ValidAddress getAddress() { + return address; + } + + public void setAddress(ValidAddress address) { + this.address = address; + } + + public List getAddressList() { + return addressList; + } + + public void setAddressList(List addressList) { + this.addressList = addressList; + } + + public Set getAddressSet() { + return addressSet; + } + + public void setAddressSet(Set addressSet) { + this.addressSet = addressSet; + } + } + + + public static class ValidAddress { + + @NotNull + private String street; + + public String getStreet() { + return street; + } + + public void setStreet(String street) { + this.street = street; + } + } + + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Constraint(validatedBy = NameAddressValidator.class) + public @interface NameAddressValid { + + String message() default "Street must not contain name"; + + Class[] groups() default {}; + + Class[] payload() default {}; + } + + + public static class NameAddressValidator implements ConstraintValidator { + + @Autowired + private Environment environment; + + @Override + public void initialize(NameAddressValid constraintAnnotation) { + } + + @Override + public boolean isValid(ValidPerson value, ConstraintValidatorContext context) { + if (value.expectsAutowiredValidator) { + assertThat(this.environment).isNotNull(); + } + boolean valid = (value.name == null || !value.address.street.contains(value.name)); + if (!valid && "Phil".equals(value.name)) { + context.buildConstraintViolationWithTemplate( + context.getDefaultConstraintMessageTemplate()).addPropertyNode("address").addConstraintViolation().disableDefaultConstraintViolation(); + } + return valid; + } + } + + + public static class MainBean { + + @InnerValid + private InnerBean inner = new InnerBean(); + + public InnerBean getInner() { + return inner; + } + } + + + public static class MainBeanWithOptional { + + @InnerValid + private InnerBean inner = new InnerBean(); + + public Optional getInner() { + return Optional.ofNullable(inner); + } + } + + + public static class InnerBean { + + private String value; + + public String getValue() { + return value; + } + public void setValue(String value) { + this.value = value; + } + } + + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @Constraint(validatedBy=InnerValidator.class) + public @interface InnerValid { + + String message() default "NOT VALID"; + + Class[] groups() default { }; + + Class[] payload() default {}; + } + + + public static class InnerValidator implements ConstraintValidator { + + @Override + public void initialize(InnerValid constraintAnnotation) { + } + + @Override + public boolean isValid(InnerBean bean, ConstraintValidatorContext context) { + context.disableDefaultConstraintViolation(); + if (bean.getValue() == null) { + context.buildConstraintViolationWithTemplate("NULL").addPropertyNode("value").addConstraintViolation(); + return false; + } + return true; + } + } + + + public static class ListContainer { + + @NotXList + private List list = new ArrayList<>(); + + public void addString(String value) { + list.add(value); + } + + public List getList() { + return list; + } + } + + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @Constraint(validatedBy = NotXListValidator.class) + public @interface NotXList { + + String message() default "Should not be X"; + + Class[] groups() default {}; + + Class[] payload() default {}; + } + + + public static class NotXListValidator implements ConstraintValidator> { + + @Override + public void initialize(NotXList constraintAnnotation) { + } + + @Override + public boolean isValid(List list, ConstraintValidatorContext context) { + context.disableDefaultConstraintViolation(); + boolean valid = true; + for (int i = 0; i < list.size(); i++) { + if ("X".equals(list.get(i))) { + context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()).addBeanNode().inIterable().atIndex(i).addConstraintViolation(); + valid = false; + } + } + return valid; + } + } + +} diff --git a/spring-context/src/test/java/test/aspect/PerTargetAspect.java b/spring-context/src/test/java/test/aspect/PerTargetAspect.java new file mode 100644 index 0000000..cb93dc4 --- /dev/null +++ b/spring-context/src/test/java/test/aspect/PerTargetAspect.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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 test.aspect; + +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; + +import org.springframework.core.Ordered; + +@Aspect("pertarget(execution(* *.getSpouse()))") +public class PerTargetAspect implements Ordered { + + public int count; + + private int order = Ordered.LOWEST_PRECEDENCE; + + @Around("execution(int *.getAge())") + public int returnCountAsAge() { + return count++; + } + + @Before("execution(void *.set*(int))") + public void countSetter() { + ++count; + } + + @Override + public int getOrder() { + return this.order; + } + + public void setOrder(int order) { + this.order = order; + } +} diff --git a/spring-context/src/test/java/test/aspect/PerThisAspect.java b/spring-context/src/test/java/test/aspect/PerThisAspect.java new file mode 100644 index 0000000..d70c8e7 --- /dev/null +++ b/spring-context/src/test/java/test/aspect/PerThisAspect.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2005 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. + * You may obtain a copy of the License at + * + * https://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 test.aspect; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; + +@Aspect("perthis(execution(* getAge()))") +public class PerThisAspect { + + private int invocations = 0; + + public int getInvocations() { + return this.invocations; + } + + @Around("execution(* getAge())") + public int changeAge(ProceedingJoinPoint pjp) throws Throwable { + return invocations++; + } + +} diff --git a/spring-context/src/test/java/test/aspect/TwoAdviceAspect.java b/spring-context/src/test/java/test/aspect/TwoAdviceAspect.java new file mode 100644 index 0000000..3e54407 --- /dev/null +++ b/spring-context/src/test/java/test/aspect/TwoAdviceAspect.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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 test.aspect; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; + +@Aspect +public class TwoAdviceAspect { + + private int totalCalls; + + @Around("execution(* org.springframework.beans.testfixture.beans.ITestBean.age())") + public int returnCallCount(ProceedingJoinPoint pjp) throws Exception { + return totalCalls; + } + + @Before("execution(* org.springframework.beans.testfixture.beans.ITestBean.setAge(int)) && args(newAge)") + public void countSet(int newAge) throws Exception { + ++totalCalls; + } + +} diff --git a/spring-context/src/test/java/test/mixin/DefaultLockable.java b/spring-context/src/test/java/test/mixin/DefaultLockable.java new file mode 100644 index 0000000..0482eb0 --- /dev/null +++ b/spring-context/src/test/java/test/mixin/DefaultLockable.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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 test.mixin; + + +/** + * Simple implementation of Lockable interface for use in mixins. + * + * @author Rod Johnson + */ +public class DefaultLockable implements Lockable { + + private boolean locked; + + @Override + public void lock() { + this.locked = true; + } + + @Override + public void unlock() { + this.locked = false; + } + + @Override + public boolean locked() { + return this.locked; + } + +} diff --git a/spring-context/src/test/java/test/mixin/LockMixin.java b/spring-context/src/test/java/test/mixin/LockMixin.java new file mode 100644 index 0000000..96a78ad --- /dev/null +++ b/spring-context/src/test/java/test/mixin/LockMixin.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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 test.mixin; + +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.aop.support.DelegatingIntroductionInterceptor; + +/** + * Mixin to provide stateful locking functionality. + * Test/demonstration of AOP mixin support rather than a + * useful interceptor in its own right. + * + * @author Rod Johnson + * @since 10.07.2003 + */ +@SuppressWarnings("serial") +public class LockMixin extends DelegatingIntroductionInterceptor implements Lockable { + + /** This field demonstrates additional state in the mixin */ + private boolean locked; + + @Override + public void lock() { + this.locked = true; + } + + @Override + public void unlock() { + this.locked = false; + } + + /** + * @see test.mixin.AopProxyTests.Lockable#locked() + */ + @Override + public boolean locked() { + return this.locked; + } + + /** + * Note that we need to override around advice. + * If the method is a setter and we're locked, prevent execution. + * Otherwise let super.invoke() handle it, and do normal + * Lockable(this) then target behaviour. + * @see org.aopalliance.MethodInterceptor#invoke(org.aopalliance.MethodInvocation) + */ + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + if (locked() && invocation.getMethod().getName().indexOf("set") == 0) + throw new LockedException(); + return super.invoke(invocation); + } + +} diff --git a/spring-context/src/test/java/test/mixin/LockMixinAdvisor.java b/spring-context/src/test/java/test/mixin/LockMixinAdvisor.java new file mode 100644 index 0000000..6ec81a7 --- /dev/null +++ b/spring-context/src/test/java/test/mixin/LockMixinAdvisor.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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 test.mixin; + +import org.springframework.aop.support.DefaultIntroductionAdvisor; + +/** + * Advisor for use with a LockMixin. Applies to all classes. + * + * @author Rod Johnson + */ +@SuppressWarnings("serial") +public class LockMixinAdvisor extends DefaultIntroductionAdvisor { + + public LockMixinAdvisor() { + super(new LockMixin(), Lockable.class); + } + +} diff --git a/spring-context/src/test/java/test/mixin/Lockable.java b/spring-context/src/test/java/test/mixin/Lockable.java new file mode 100644 index 0000000..41abef3 --- /dev/null +++ b/spring-context/src/test/java/test/mixin/Lockable.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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 test.mixin; + + +/** + * Simple interface to use for mixins + * + * @author Rod Johnson + * + */ +public interface Lockable { + + void lock(); + + void unlock(); + + boolean locked(); +} diff --git a/spring-context/src/test/java/test/mixin/LockedException.java b/spring-context/src/test/java/test/mixin/LockedException.java new file mode 100644 index 0000000..6f8d853 --- /dev/null +++ b/spring-context/src/test/java/test/mixin/LockedException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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 test.mixin; + + +@SuppressWarnings("serial") +public class LockedException extends RuntimeException { + + public LockedException() { + } +} diff --git a/spring-context/src/test/kotlin/org/springframework/context/annotation/AnnotationConfigApplicationContextExtensionsTests.kt b/spring-context/src/test/kotlin/org/springframework/context/annotation/AnnotationConfigApplicationContextExtensionsTests.kt new file mode 100644 index 0000000..98a10fa --- /dev/null +++ b/spring-context/src/test/kotlin/org/springframework/context/annotation/AnnotationConfigApplicationContextExtensionsTests.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.getBean +import org.springframework.context.support.registerBean + +/** + * Tests for [AnnotationConfigApplicationContext] Kotlin extensions. + * + * @author Sebastien Deleuze + */ +class AnnotationConfigApplicationContextExtensionsTests { + + @Test + @Suppress("DEPRECATION") + fun `Instantiate AnnotationConfigApplicationContext`() { + val applicationContext = AnnotationConfigApplicationContext { + registerBean() + } + assertThat(applicationContext).isNotNull() + applicationContext.refresh() + applicationContext.getBean() + } + + class Foo +} diff --git a/spring-context/src/test/kotlin/org/springframework/context/annotation/KotlinConfigurationClassTests.kt b/spring-context/src/test/kotlin/org/springframework/context/annotation/KotlinConfigurationClassTests.kt new file mode 100644 index 0000000..72d411e --- /dev/null +++ b/spring-context/src/test/kotlin/org/springframework/context/annotation/KotlinConfigurationClassTests.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation + + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.getBean +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException + +class KotlinConfigurationClassTests { + + @Test + fun `Final configuration with default proxyBeanMethods value`() { + assertThatExceptionOfType(BeanDefinitionParsingException::class.java).isThrownBy { + AnnotationConfigApplicationContext(FinalConfigurationWithProxy::class.java) + } + } + + @Test + fun `Final configuration with proxyBeanMethods set to false`() { + val context = AnnotationConfigApplicationContext(FinalConfigurationWithoutProxy::class.java) + val foo = context.getBean() + assertThat(context.getBean().foo).isEqualTo(foo) + } + + + @Configuration + class FinalConfigurationWithProxy { + + @Bean + fun foo() = Foo() + + @Bean + fun bar(foo: Foo) = Bar(foo) + } + + @Configuration(proxyBeanMethods = false) + class FinalConfigurationWithoutProxy { + + @Bean + fun foo() = Foo() + + @Bean + fun bar(foo: Foo) = Bar(foo) + } + + class Foo + + class Bar(val foo: Foo) +} diff --git a/spring-context/src/test/kotlin/org/springframework/context/annotation/Spr16022Tests.kt b/spring-context/src/test/kotlin/org/springframework/context/annotation/Spr16022Tests.kt new file mode 100644 index 0000000..a65bf31 --- /dev/null +++ b/spring-context/src/test/kotlin/org/springframework/context/annotation/Spr16022Tests.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.annotation + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.BeanFactory +import org.springframework.beans.factory.getBean +import org.springframework.context.support.ClassPathXmlApplicationContext + +/** + * @author Sebastien Deleuze + */ +class Spr16022Tests { + + @Test + fun `Register beans with multiple constructors with AnnotationConfigApplicationContext`() { + assert(AnnotationConfigApplicationContext(Config::class.java)) + } + + @Test + fun `Register beans with multiple constructors with ClassPathXmlApplicationContext`() { + assert(ClassPathXmlApplicationContext(CONTEXT)) + } + + private fun assert(context: BeanFactory) { + val bean1 = context.getBean("bean1") + assertThat(bean1.foo).isEqualTo(0) + val bean2 = context.getBean("bean2") + assertThat(bean2.foo).isEqualTo(1) + val bean3 = context.getBean("bean3") + assertThat(bean3.foo).isEqualTo(3) + + } + + @Suppress("unused") + class MultipleConstructorsTestBean(val foo: Int) { + constructor(bar: String) : this(bar.length) + constructor(foo: Int, bar: String) : this(foo + bar.length) + } + + @Configuration @ImportResource(CONTEXT) + open class Config +} + +private const val CONTEXT = "org/springframework/context/annotation/multipleConstructors.xml" \ No newline at end of file diff --git a/spring-context/src/test/kotlin/org/springframework/context/support/BeanDefinitionDslTests.kt b/spring-context/src/test/kotlin/org/springframework/context/support/BeanDefinitionDslTests.kt new file mode 100644 index 0000000..5ac22c8 --- /dev/null +++ b/spring-context/src/test/kotlin/org/springframework/context/support/BeanDefinitionDslTests.kt @@ -0,0 +1,209 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.junit.jupiter.api.fail +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.NoSuchBeanDefinitionException +import org.springframework.beans.factory.getBean +import org.springframework.context.support.BeanDefinitionDsl.* +import org.springframework.core.env.SimpleCommandLinePropertySource +import org.springframework.core.env.get +import org.springframework.core.testfixture.env.MockPropertySource +import java.util.stream.Collectors + +@Suppress("UNUSED_EXPRESSION") +class BeanDefinitionDslTests { + + @Test + fun `Declare beans with the functional Kotlin DSL`() { + val beans = beans { + bean() + bean("bar", scope = Scope.PROTOTYPE) + bean { Baz(ref("bar")) } + } + + val context = GenericApplicationContext().apply { + beans.initialize(this) + refresh() + } + + context.getBean() + context.getBean("bar") + assertThat(context.isPrototype("bar")).isTrue() + context.getBean() + } + + @Test + fun `Declare beans using profile condition with the functional Kotlin DSL`() { + val beans = beans { + profile("foo") { + bean() + profile("bar") { + bean("bar") + } + } + profile("baz") { + bean { Baz(ref("bar")) } + } + } + + val context = GenericApplicationContext().apply { + environment.addActiveProfile("foo") + environment.addActiveProfile("bar") + beans.initialize(this) + refresh() + } + + context.getBean() + context.getBean("bar") + assertThatExceptionOfType(NoSuchBeanDefinitionException::class.java).isThrownBy { context.getBean() } + } + + @Test + fun `Declare beans using environment condition with the functional Kotlin DSL`() { + val beans = beans { + bean() + bean("bar") + environment( { env["name"].equals("foofoo") } ) { + bean { FooFoo(env["name"]!!) } + } + environment( { activeProfiles.contains("baz") } ) { + bean { Baz(ref("bar")) } + } + } + + val context = GenericApplicationContext().apply { + environment.propertySources.addFirst(SimpleCommandLinePropertySource("--name=foofoo")) + beans.initialize(this) + refresh() + } + + context.getBean() + context.getBean("bar") + assertThat(context.getBean().name).isEqualTo("foofoo") + assertThatExceptionOfType(NoSuchBeanDefinitionException::class.java).isThrownBy { context.getBean() } + } + + @Test // SPR-16412 + fun `Declare beans depending on environment properties`() { + val beans = beans { + val n = env["number-of-beans"]!!.toInt() + for (i in 1..n) { + bean("string$i") { Foo() } + } + } + + val context = GenericApplicationContext().apply { + environment.propertySources.addLast(MockPropertySource().withProperty("number-of-beans", 5)) + beans.initialize(this) + refresh() + } + + for (i in 1..5) { + context.getBean("string$i") + } + } + + @Test // SPR-17352 + fun `Retrieve multiple beans via a bean provider`() { + val beans = beans { + bean() + bean() + bean { BarBar(provider().stream().collect(Collectors.toList())) } + } + + val context = GenericApplicationContext().apply { + beans.initialize(this) + refresh() + } + + val barbar = context.getBean() + assertThat(barbar.foos.size).isEqualTo(2) + } + + @Test // SPR-17292 + fun `Declare beans leveraging constructor injection`() { + val beans = beans { + bean() + bean() + } + val context = GenericApplicationContext().apply { + beans.initialize(this) + refresh() + } + context.getBean() + } + + @Test // gh-21845 + fun `Declare beans leveraging callable reference`() { + val beans = beans { + bean() + bean(::baz) + bean(::foo) + } + val context = GenericApplicationContext().apply { + beans.initialize(this) + refresh() + } + context.getBean() + } + + + @Test + fun `Declare beans with accepted profiles`() { + val beans = beans { + profile("foo") { bean() } + profile("!bar") { bean() } + profile("bar | barbar") { bean() } + profile("baz & buz") { bean() } + profile("baz & foo") { bean() } + } + val context = GenericApplicationContext().apply { + environment.addActiveProfile("barbar") + environment.addActiveProfile("baz") + environment.addActiveProfile("buz") + beans.initialize(this) + refresh() + } + context.getBean() + context.getBean() + context.getBean() + + try { + context.getBean() + fail("should have thrown an Exception") + } catch (ignored: Exception) { + } + try { + context.getBean() + fail("should have thrown an Exception") + } catch (ignored: Exception) { + } + } +} + +class Foo +class Bar +class Baz(val bar: Bar) +class FooFoo(val name: String) +class BarBar(val foos: Collection) + +fun baz(bar: Bar) = Baz(bar) +fun foo() = Foo() diff --git a/spring-context/src/test/kotlin/org/springframework/context/support/GenericApplicationContextExtensionsTests.kt b/spring-context/src/test/kotlin/org/springframework/context/support/GenericApplicationContextExtensionsTests.kt new file mode 100644 index 0000000..f39108e --- /dev/null +++ b/spring-context/src/test/kotlin/org/springframework/context/support/GenericApplicationContextExtensionsTests.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.support + +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.getBean + +/** + * Tests for [GenericApplicationContext] Kotlin extensions + * + * @author Sebastien Deleuze + */ +class GenericApplicationContextExtensionsTests { + + @Test + fun registerBeanWithClass() { + val context = GenericApplicationContext() + context.registerBean() + context.refresh() + context.getBean() + } + + @Test + fun registerBeanWithNameAndClass() { + val context = GenericApplicationContext() + context.registerBean("a") + context.refresh() + context.getBean("a") + } + + @Test + fun registerBeanWithSupplier() { + val context = GenericApplicationContext() + context.registerBean { BeanA() } + context.refresh() + context.getBean() + } + + @Test + fun registerBeanWithNameAndSupplier() { + val context = GenericApplicationContext() + context.registerBean("a") { BeanA() } + context.refresh() + context.getBean("a") + } + + @Test + fun registerBeanWithFunction() { + val context = GenericApplicationContext() + context.registerBean() + context.registerBean { BeanB(it.getBean()) } + context.refresh() + context.getBean() + context.getBean() + } + + @Test + fun registerBeanWithNameAndFunction() { + val context = GenericApplicationContext() + context.registerBean("a") + context.registerBean("b") { BeanB(it.getBean()) } + context.refresh() + context.getBean("a") + context.getBean("b") + } + + class BeanA + + class BeanB(val a: BeanA) + +} diff --git a/spring-context/src/test/kotlin/org/springframework/ui/ModelExtensionsTests.kt b/spring-context/src/test/kotlin/org/springframework/ui/ModelExtensionsTests.kt new file mode 100644 index 0000000..6ef6b32 --- /dev/null +++ b/spring-context/src/test/kotlin/org/springframework/ui/ModelExtensionsTests.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ui + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +/** + * Tests for [Model] Kotlin extensions. + * + * @author Sebastien Deleuze + */ +class ModelExtensionsTests { + + @Test + fun setAttribute() { + val model:Model = ConcurrentModel() + model["foo"] = "bing" + assertThat(model.containsAttribute("foo")).isTrue() + assertThat(model.asMap()["foo"]).isEqualTo("bing") + } +} diff --git a/spring-context/src/test/kotlin/org/springframework/ui/ModelMapExtensionsTests.kt b/spring-context/src/test/kotlin/org/springframework/ui/ModelMapExtensionsTests.kt new file mode 100644 index 0000000..23eb3c4 --- /dev/null +++ b/spring-context/src/test/kotlin/org/springframework/ui/ModelMapExtensionsTests.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.ui + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +/** + * Tests for [ModelMap] Kotlin extensions. + * + * @author Sebastien Deleuze + */ +class ModelMapExtensionsTests { + + @Test + fun setAttribute() { + val model = ModelMap() + model["foo"] = "bing" + assertThat(model.containsAttribute("foo")) + assertThat(model["foo"]).isEqualTo("bing") + } + +} diff --git a/spring-context/src/test/resources/do_not_delete_me.txt b/spring-context/src/test/resources/do_not_delete_me.txt new file mode 100644 index 0000000..25f06fe --- /dev/null +++ b/spring-context/src/test/resources/do_not_delete_me.txt @@ -0,0 +1 @@ +Please do not delete me; otherwise, you'll break some tests. \ No newline at end of file diff --git a/spring-context/src/test/resources/example/scannable/spring.components b/spring-context/src/test/resources/example/scannable/spring.components new file mode 100644 index 0000000..9e7303d --- /dev/null +++ b/spring-context/src/test/resources/example/scannable/spring.components @@ -0,0 +1,10 @@ +example.scannable.AutowiredQualifierFooService=example.scannable.FooService +example.scannable.DefaultNamedComponent=org.springframework.stereotype.Component +example.scannable.NamedComponent=org.springframework.stereotype.Component +example.scannable.FooService=example.scannable.FooService +example.scannable.FooServiceImpl=org.springframework.stereotype.Component,example.scannable.FooService +example.scannable.ScopedProxyTestBean=example.scannable.FooService +example.scannable.StubFooDao=org.springframework.stereotype.Component +example.scannable.NamedStubDao=org.springframework.stereotype.Component +example.scannable.ServiceInvocationCounter=org.springframework.stereotype.Component +example.scannable.sub.BarComponent=org.springframework.stereotype.Component \ No newline at end of file diff --git a/spring-context/src/test/resources/log4j2-test.xml b/spring-context/src/test/resources/log4j2-test.xml new file mode 100644 index 0000000..e17ae99 --- /dev/null +++ b/spring-context/src/test/resources/log4j2-test.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/AfterAdviceBindingTests.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/AfterAdviceBindingTests.xml new file mode 100644 index 0000000..7b7b2f8 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/AfterAdviceBindingTests.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/AfterReturningAdviceBindingTests.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/AfterReturningAdviceBindingTests.xml new file mode 100644 index 0000000..796fcd4 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/AfterReturningAdviceBindingTests.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/AfterThrowingAdviceBindingTests.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/AfterThrowingAdviceBindingTests.xml new file mode 100644 index 0000000..8e82146 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/AfterThrowingAdviceBindingTests.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/AroundAdviceBindingTests.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/AroundAdviceBindingTests.xml new file mode 100644 index 0000000..2aa4754 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/AroundAdviceBindingTests.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/AroundAdviceCircularTests.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/AroundAdviceCircularTests.xml new file mode 100644 index 0000000..e2380f0 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/AroundAdviceCircularTests.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/AspectAndAdvicePrecedenceTests.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/AspectAndAdvicePrecedenceTests.xml new file mode 100644 index 0000000..424716d --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/AspectAndAdvicePrecedenceTests.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/AspectJExpressionPointcutAdvisorTests.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/AspectJExpressionPointcutAdvisorTests.xml new file mode 100644 index 0000000..056c8ef --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/AspectJExpressionPointcutAdvisorTests.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/BeanNamePointcutAtAspectTests.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/BeanNamePointcutAtAspectTests.xml new file mode 100644 index 0000000..3e3e9a6 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/BeanNamePointcutAtAspectTests.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/BeanNamePointcutTests.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/BeanNamePointcutTests.xml new file mode 100644 index 0000000..a60d6c0 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/BeanNamePointcutTests.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/BeforeAdviceBindingTests.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/BeforeAdviceBindingTests.xml new file mode 100644 index 0000000..bd0da18 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/BeforeAdviceBindingTests.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/DeclarationOrderIndependenceTests.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/DeclarationOrderIndependenceTests.xml new file mode 100644 index 0000000..1f60985 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/DeclarationOrderIndependenceTests.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/DeclareParentsDelegateRefTests.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/DeclareParentsDelegateRefTests.xml new file mode 100644 index 0000000..52bc482 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/DeclareParentsDelegateRefTests.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/DeclareParentsTests.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/DeclareParentsTests.xml new file mode 100644 index 0000000..96779df --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/DeclareParentsTests.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/ImplicitJPArgumentMatchingAtAspectJTests.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/ImplicitJPArgumentMatchingAtAspectJTests.xml new file mode 100644 index 0000000..ade5ce1 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/ImplicitJPArgumentMatchingAtAspectJTests.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/ImplicitJPArgumentMatchingTests.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/ImplicitJPArgumentMatchingTests.xml new file mode 100644 index 0000000..b146694 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/ImplicitJPArgumentMatchingTests.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/OverloadedAdviceTests-ambiguous.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/OverloadedAdviceTests-ambiguous.xml new file mode 100644 index 0000000..ff428b8 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/OverloadedAdviceTests-ambiguous.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/OverloadedAdviceTests.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/OverloadedAdviceTests.xml new file mode 100644 index 0000000..df9bfad --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/OverloadedAdviceTests.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/ProceedTests.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/ProceedTests.xml new file mode 100644 index 0000000..c2a5a18 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/ProceedTests.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/PropertyDependentAspectTests-after.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/PropertyDependentAspectTests-after.xml new file mode 100644 index 0000000..15ec581 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/PropertyDependentAspectTests-after.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/PropertyDependentAspectTests-atAspectJ-after.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/PropertyDependentAspectTests-atAspectJ-after.xml new file mode 100644 index 0000000..d8d1e4d --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/PropertyDependentAspectTests-atAspectJ-after.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/PropertyDependentAspectTests-atAspectJ-before.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/PropertyDependentAspectTests-atAspectJ-before.xml new file mode 100644 index 0000000..6db7143 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/PropertyDependentAspectTests-atAspectJ-before.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/PropertyDependentAspectTests-before.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/PropertyDependentAspectTests-before.xml new file mode 100644 index 0000000..9628565 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/PropertyDependentAspectTests-before.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/SharedPointcutWithArgsMismatchTests.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/SharedPointcutWithArgsMismatchTests.xml new file mode 100644 index 0000000..af301fd --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/SharedPointcutWithArgsMismatchTests.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/SubtypeSensitiveMatchingTests.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/SubtypeSensitiveMatchingTests.xml new file mode 100644 index 0000000..809f1cf --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/SubtypeSensitiveMatchingTests.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/TargetPointcutSelectionTests.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/TargetPointcutSelectionTests.xml new file mode 100644 index 0000000..5d488c3 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/TargetPointcutSelectionTests.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/ThisAndTargetSelectionOnlyPointcutsAtAspectJTests.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/ThisAndTargetSelectionOnlyPointcutsAtAspectJTests.xml new file mode 100644 index 0000000..fd6e9bd --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/ThisAndTargetSelectionOnlyPointcutsAtAspectJTests.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/ThisAndTargetSelectionOnlyPointcutsTests.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/ThisAndTargetSelectionOnlyPointcutsTests.xml new file mode 100644 index 0000000..7f61226 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/ThisAndTargetSelectionOnlyPointcutsTests.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AnnotationBindingTests-context.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AnnotationBindingTests-context.xml new file mode 100644 index 0000000..c943b18 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AnnotationBindingTests-context.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AnnotationPointcutTests-context.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AnnotationPointcutTests-context.xml new file mode 100644 index 0000000..2eb828e --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AnnotationPointcutTests-context.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectImplementingInterfaceTests-context.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectImplementingInterfaceTests-context.xml new file mode 100644 index 0000000..ffe4eac --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectImplementingInterfaceTests-context.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorAndLazyInitTargetSourceTests-context.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorAndLazyInitTargetSourceTests-context.xml new file mode 100644 index 0000000..66b46a8 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorAndLazyInitTargetSourceTests-context.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-aspects.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-aspects.xml new file mode 100644 index 0000000..e390f07 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-aspects.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-aspectsPlusAdvisor.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-aspectsPlusAdvisor.xml new file mode 100644 index 0000000..62515c2 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-aspectsPlusAdvisor.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-aspectsWithAbstractBean.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-aspectsWithAbstractBean.xml new file mode 100644 index 0000000..d4d66d8 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-aspectsWithAbstractBean.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-aspectsWithCGLIB.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-aspectsWithCGLIB.xml new file mode 100644 index 0000000..51fc2d3 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-aspectsWithCGLIB.xml @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-aspectsWithOrdering.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-aspectsWithOrdering.xml new file mode 100644 index 0000000..7760c65 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-aspectsWithOrdering.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-pertarget.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-pertarget.xml new file mode 100644 index 0000000..8f9d9bd --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-pertarget.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-perthis.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-perthis.xml new file mode 100644 index 0000000..231038c --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-perthis.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-retryAspect.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-retryAspect.xml new file mode 100644 index 0000000..953c959 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-retryAspect.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-twoAdviceAspect.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-twoAdviceAspect.xml new file mode 100644 index 0000000..c8dcfe9 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-twoAdviceAspect.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-twoAdviceAspectPrototype.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-twoAdviceAspectPrototype.xml new file mode 100644 index 0000000..5bb3453 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-twoAdviceAspectPrototype.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-twoAdviceAspectSingleton.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-twoAdviceAspectSingleton.xml new file mode 100644 index 0000000..9202f08 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-twoAdviceAspectSingleton.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-usesInclude.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-usesInclude.xml new file mode 100644 index 0000000..b63bf5f --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-usesInclude.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-usesJoinPointAspect.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-usesJoinPointAspect.xml new file mode 100644 index 0000000..1babdf8 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-usesJoinPointAspect.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-withBeanNameAutoProxyCreator.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-withBeanNameAutoProxyCreator.xml new file mode 100644 index 0000000..624c06e --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests-withBeanNameAutoProxyCreator.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AtAspectJAfterThrowingTests-context.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AtAspectJAfterThrowingTests-context.xml new file mode 100644 index 0000000..4e5a8f8 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AtAspectJAfterThrowingTests-context.xml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AtAspectJAnnotationBindingTests-context.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AtAspectJAnnotationBindingTests-context.xml new file mode 100644 index 0000000..d76967e --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/AtAspectJAnnotationBindingTests-context.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/benchmark/BenchmarkTests-aspectj.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/benchmark/BenchmarkTests-aspectj.xml new file mode 100644 index 0000000..aec8745 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/benchmark/BenchmarkTests-aspectj.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/benchmark/BenchmarkTests-springAop.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/benchmark/BenchmarkTests-springAop.xml new file mode 100644 index 0000000..ec543f9 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/benchmark/BenchmarkTests-springAop.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/spr3064/SPR3064Tests.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/spr3064/SPR3064Tests.xml new file mode 100644 index 0000000..6a5cc63 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/autoproxy/spr3064/SPR3064Tests.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/generic/AfterReturningGenericTypeMatchingTests-context.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/generic/AfterReturningGenericTypeMatchingTests-context.xml new file mode 100644 index 0000000..5c1cf83 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/generic/AfterReturningGenericTypeMatchingTests-context.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/generic/GenericBridgeMethodMatchingClassProxyTests-context.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/generic/GenericBridgeMethodMatchingClassProxyTests-context.xml new file mode 100644 index 0000000..c9ddeb3 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/generic/GenericBridgeMethodMatchingClassProxyTests-context.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/generic/GenericBridgeMethodMatchingTests-context.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/generic/GenericBridgeMethodMatchingTests-context.xml new file mode 100644 index 0000000..46f8ba8 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/generic/GenericBridgeMethodMatchingTests-context.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/generic/GenericParameterMatchingTests-context.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/generic/GenericParameterMatchingTests-context.xml new file mode 100644 index 0000000..066e722 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/generic/GenericParameterMatchingTests-context.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerAdviceTypeTests-error.xml b/spring-context/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerAdviceTypeTests-error.xml new file mode 100644 index 0000000..681b8f2 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerAdviceTypeTests-error.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerAdviceTypeTests-ok.xml b/spring-context/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerAdviceTypeTests-ok.xml new file mode 100644 index 0000000..6acd17a --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerAdviceTypeTests-ok.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerArgNamesTests-error.xml b/spring-context/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerArgNamesTests-error.xml new file mode 100644 index 0000000..1b73003 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerArgNamesTests-error.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerArgNamesTests-ok.xml b/spring-context/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerArgNamesTests-ok.xml new file mode 100644 index 0000000..d07c160 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerArgNamesTests-ok.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerProxyTargetClassTests-context.xml b/spring-context/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerProxyTargetClassTests-context.xml new file mode 100644 index 0000000..bbb0ca2 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerProxyTargetClassTests-context.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerReturningTests-error.xml b/spring-context/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerReturningTests-error.xml new file mode 100644 index 0000000..185736d --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerReturningTests-error.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerReturningTests-ok.xml b/spring-context/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerReturningTests-ok.xml new file mode 100644 index 0000000..23b6d47 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerReturningTests-ok.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerTests-context.xml b/spring-context/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerTests-context.xml new file mode 100644 index 0000000..ef13614 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerTests-context.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerThrowingTests-error.xml b/spring-context/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerThrowingTests-error.xml new file mode 100644 index 0000000..1aef0e6 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerThrowingTests-error.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerThrowingTests-ok.xml b/spring-context/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerThrowingTests-ok.xml new file mode 100644 index 0000000..4abf24d --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/config/AopNamespaceHandlerThrowingTests-ok.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/config/PrototypeProxyTests-context.xml b/spring-context/src/test/resources/org/springframework/aop/config/PrototypeProxyTests-context.xml new file mode 100644 index 0000000..62900b1 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/config/PrototypeProxyTests-context.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/framework/CglibProxyTests-with-dependency-checking.xml b/spring-context/src/test/resources/org/springframework/aop/framework/CglibProxyTests-with-dependency-checking.xml new file mode 100644 index 0000000..d232ec9 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/framework/CglibProxyTests-with-dependency-checking.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/framework/ObjenesisProxyTests-context.xml b/spring-context/src/test/resources/org/springframework/aop/framework/ObjenesisProxyTests-context.xml new file mode 100644 index 0000000..7cc3f28 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/framework/ObjenesisProxyTests-context.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-autowiring.xml b/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-autowiring.xml new file mode 100644 index 0000000..2d39973 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-autowiring.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-context.xml b/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-context.xml new file mode 100644 index 0000000..a8a7734 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-context.xml @@ -0,0 +1,171 @@ + + + + + + + + custom + 666 + + + + + + org.springframework.beans.testfixture.beans.ITestBean + + debugInterceptor + + + + + + + org.springframework.beans.testfixture.beans.ITestBean + + + global*,test + + + + + + + org.springframework.beans.testfixture.beans.ITestBean + + + false + test + + + + + + org.springframework.beans.testfixture.beans.ITestBean + + + false + test + + + + true + + debugInterceptor + + + + true + false + test + + + + true + testCircleTarget1 + + + + custom + 666 + + + + + true + testCircleTarget2 + + + + custom + 666 + + + + + org.springframework.beans.testfixture.beans.ITestBean + pointcutForVoid + test + + + + + + + + + org.springframework.context.ApplicationListener + debugInterceptor,global*,target2 + + + + + + + + + + + + + + org.springframework.beans.testfixture.beans.ITestBean + false + + + prototypeLockMixinAdvisor + prototypeTestBean + + + + + + + + + + org.springframework.beans.testfixture.beans.ITestBean + test.mixin.Lockable + + + false + + + + prototypeLockMixinInterceptor + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-double-targetsource.xml b/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-double-targetsource.xml new file mode 100644 index 0000000..4738c1a --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-double-targetsource.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + Eve + + + + + + Adam + + + + + + + + + + + + org.springframework.beans.testfixture.beans.ITestBean + + countingBeforeAdvice,adamTargetSource + + + + + + + org.springframework.beans.testfixture.beans.ITestBean + + adam + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-frozen.xml b/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-frozen.xml new file mode 100644 index 0000000..a40e264 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-frozen.xml @@ -0,0 +1,28 @@ + + + + + + + + custom + 666 + + + + + + + + org.springframework.beans.testfixture.beans.ITestBean + + + debugInterceptor + true + true + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-inner-bean-target.xml b/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-inner-bean-target.xml new file mode 100644 index 0000000..b8e6f5e --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-inner-bean-target.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + innerBeanTarget + + + + + nopInterceptor + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-invalid.xml b/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-invalid.xml new file mode 100644 index 0000000..0224d4c --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-invalid.xml @@ -0,0 +1,21 @@ + + + + + + + + org.springframework.beans.testfixture.beans.ITestBean + + + + + + org.springframework.beans.testfixture.beans.ITestBean + global* + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-notlast-targetsource.xml b/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-notlast-targetsource.xml new file mode 100644 index 0000000..fe55a5e --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-notlast-targetsource.xml @@ -0,0 +1,34 @@ + + + + + + + + + + Adam + + + + + + + org.springframework.beans.testfixture.beans.ITestBean + + adam,countingBeforeAdvice + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-prototype.xml b/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-prototype.xml new file mode 100644 index 0000000..aecb904 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-prototype.xml @@ -0,0 +1,37 @@ + + + + + + + + + 10 + + + + 10 + + + + + + debugInterceptor,test + + + + debugInterceptor,prototypeTarget + false + + + + debugInterceptor,prototypeTarget + false + + true + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-serialization.xml b/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-serialization.xml new file mode 100644 index 0000000..1d0b6ff --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-serialization.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + serializableNopInterceptor + org.springframework.beans.testfixture.beans.Person + + + serializableSingleton + + + + + + serializablePrototype + + + + serializableNopInterceptor,prototypeTarget + org.springframework.beans.testfixture.beans.Person + false + + + + nopInterceptor + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-targetsource.xml b/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-targetsource.xml new file mode 100644 index 0000000..0bc1a3e --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-targetsource.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + Adam + + + + + + + + + + + + + + + + + + + nopInterceptor,targetSource + + + + + + + + org.springframework.beans.testfixture.beans.ITestBean + nopInterceptor,unsupportedInterceptor + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-throws-advice.xml b/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-throws-advice.xml new file mode 100644 index 0000000..8bc633a --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/framework/ProxyFactoryBeanTests-throws-advice.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + countingBeforeAdvice,nopInterceptor,throwsAdvice,target + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/framework/adapter/AdvisorAdapterRegistrationTests-with-bpp.xml b/spring-context/src/test/resources/org/springframework/aop/framework/adapter/AdvisorAdapterRegistrationTests-with-bpp.xml new file mode 100644 index 0000000..358f830 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/framework/adapter/AdvisorAdapterRegistrationTests-with-bpp.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + org.springframework.beans.testfixture.beans.ITestBean + simpleBeforeAdviceAdvisor,testBeanTarget + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/framework/adapter/AdvisorAdapterRegistrationTests-without-bpp.xml b/spring-context/src/test/resources/org/springframework/aop/framework/adapter/AdvisorAdapterRegistrationTests-without-bpp.xml new file mode 100644 index 0000000..418c895 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/framework/adapter/AdvisorAdapterRegistrationTests-without-bpp.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + org.springframework.beans.testfixture.beans.ITestBean + simpleBeforeAdviceAdvisor,testBeanTarget + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorTests-common-interceptors.xml b/spring-context/src/test/resources/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorTests-common-interceptors.xml new file mode 100644 index 0000000..7819d2c --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorTests-common-interceptors.xml @@ -0,0 +1,47 @@ + + + + + + + + Matches all Advisors in the factory: we don't use a prefix + + + + + + nopInterceptor + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorTests-custom-targetsource.xml b/spring-context/src/test/resources/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorTests-custom-targetsource.xml new file mode 100644 index 0000000..a596c40 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorTests-custom-targetsource.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + Rod + + + + + Rod + + + + + Rod + + + + + Kerry + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorTests-optimized.xml b/spring-context/src/test/resources/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorTests-optimized.xml new file mode 100644 index 0000000..a3da2c1 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorTests-optimized.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + .*beans.I?TestBean.* + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorTests-quick-targetsource.xml b/spring-context/src/test/resources/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorTests-quick-targetsource.xml new file mode 100644 index 0000000..6b34f79 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/framework/autoproxy/AdvisorAutoProxyCreatorTests-quick-targetsource.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + Rod + + + + + Kerry + + + + + Rod + + + + + + Rod + + + + + + Rod + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorInitTests-context.xml b/spring-context/src/test/resources/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorInitTests-context.xml new file mode 100644 index 0000000..f6d2096 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorInitTests-context.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + .*\.set[a-zA-Z]*(.*) + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorTests-context.xml b/spring-context/src/test/resources/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorTests-context.xml new file mode 100644 index 0000000..6327f5e --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorTests-context.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + Automatically proxies using JDK dynamic proxies + + + + + + + + + + + + + Use the inherited ProxyConfig property to force CGLIB proxying + true + + + Interceptors and Advisors to apply automatically + + nopInterceptor + countingBeforeAdvice + + + + + + + Illustrates a JDK introduction + + + + + introductionNopInterceptor + timestampIntroduction + lockableAdvisor + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/scope/ScopedProxyTests-list.xml b/spring-context/src/test/resources/org/springframework/aop/scope/ScopedProxyTests-list.xml new file mode 100644 index 0000000..011fad2 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/scope/ScopedProxyTests-list.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/scope/ScopedProxyTests-map.xml b/spring-context/src/test/resources/org/springframework/aop/scope/ScopedProxyTests-map.xml new file mode 100644 index 0000000..e335461 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/scope/ScopedProxyTests-map.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/scope/ScopedProxyTests-override.xml b/spring-context/src/test/resources/org/springframework/aop/scope/ScopedProxyTests-override.xml new file mode 100644 index 0000000..2689268 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/scope/ScopedProxyTests-override.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/aop/scope/ScopedProxyTests-testbean.xml b/spring-context/src/test/resources/org/springframework/aop/scope/ScopedProxyTests-testbean.xml new file mode 100644 index 0000000..62e23b5 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/aop/scope/ScopedProxyTests-testbean.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/LookupMethodWrappedByCglibProxyTests-context.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/LookupMethodWrappedByCglibProxyTests-context.xml new file mode 100644 index 0000000..9f753da --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/LookupMethodWrappedByCglibProxyTests-context.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + true + interceptor + + + + Jenny + 30 + + + + + autoProxiedOverload + true + interceptor + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/QualifierAnnotationTests-context.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/QualifierAnnotationTests-context.xml new file mode 100644 index 0000000..b69324f --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/QualifierAnnotationTests-context.xml @@ -0,0 +1,62 @@ + + + + + + + + + larry + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-autowire.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-autowire.xml new file mode 100644 index 0000000..54c25f5 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-autowire.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + /org/springframework/beans/factory/xml/XmlBeanFactoryTests-collections.xml + + + + + + + /org/springframework/beans/factory/xml/XmlBeanFactoryTests-constructorArg.xml + /org/springframework/beans/factory/xml/XmlBeanFactoryTests-initializers.xml + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-child.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-child.xml new file mode 100644 index 0000000..e2e43b3 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-child.xml @@ -0,0 +1,51 @@ + + + + + + + override + + + + + override + + + + + override + + + + + prototypeOverridesInheritedSingleton + + + + + prototype-override + + + + + prototype-override + + + + + overrideParentBean + + + + + + + myname + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-classNotFound.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-classNotFound.xml new file mode 100644 index 0000000..97ba826 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-classNotFound.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-collections.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-collections.xml new file mode 100644 index 0000000..c1af44a --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-collections.xml @@ -0,0 +1,376 @@ + + + + + Jenny + 30 + + + + + + + + + Simple bean, without any collections. + + + The name of the user + David + + 27 + + + + Rod + 32 + + List of Rod's friends + + + + + + + + + Jenny + 30 + + + + + + + + David + 27 + + + + Rod + 32 + + + + + + + + + + + loner + 26 + + + My List + + + + + + + + + + literal + + + + + + + + + + + literal + + + + + + + + + verbose + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 10 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + bar + + + + + zero + + bar + + + + + ba + + + + + + + bar + + + + + + + + + + + + + + + + + + + bar + + + + + + + + + + + + + + + + + + bar + TWO + + + + + + + + + + + + + + + + + one + + + + + + + + + java.lang.String + java.lang.Exception + + + + + + + + 0 + 1 + 2 + + + + + + + + bar + jenny + + + + java.util.LinkedList + + + + + + + bar + jenny + + + + java.util.LinkedList + + + true + + + + + + + bar + jenny + + + + java.util.TreeSet + + + + + + + bar + jenny + + + + java.util.TreeSet + + + true + + + + + + + bar + jenny + + + + java.util.TreeMap + + + + + + + bar + jenny + + + + java.util.TreeMap + + + true + + + + + + + My Map + + + + + + + + + + + My Set + ONE + TWO + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-complexFactoryCircle.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-complexFactoryCircle.xml new file mode 100644 index 0000000..b5ad92e --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-complexFactoryCircle.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-constructorArg.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-constructorArg.xml new file mode 100644 index 0000000..099305b --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-constructorArg.xml @@ -0,0 +1,238 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + wife + + + + + + + + wife + + + + + magic int value: 99 is the number of aliens who can dance on the tip of pin + + 99 + + + myname + + + + + + + + + + + + + + + + + + + + + + + 99 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 29 + + + + + + + + + + + + + Kerry1 + + + 33 + + + + + + Kerry2 + + + 32 + + + + + + + /test + + + + + + + + + + + + true + A String + + + + + + + + + + + + + + + + + true + + + + true + + + + false + true + + + + false + true + + + + + + + + + + + + + + + + test + + + + + + + + + + 1 + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-constructorOverrides.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-constructorOverrides.xml new file mode 100644 index 0000000..a39f9c0 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-constructorOverrides.xml @@ -0,0 +1,34 @@ + + + + + + + + + from property element + + + + + + + + Jenny + 30 + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-defaultAutowire.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-defaultAutowire.xml new file mode 100644 index 0000000..778369a --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-defaultAutowire.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + Kerry + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-defaultLazyInit.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-defaultLazyInit.xml new file mode 100644 index 0000000..8266a05 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-defaultLazyInit.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-delegationOverrides.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-delegationOverrides.xml new file mode 100644 index 0000000..175408a --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-delegationOverrides.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + String + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + String + + + + + + Jenny + 30 + + + + + + + + + Simple bean, without any collections. + + + The name of the user + David + + 27 + + + + + + + + + + + String + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depCarg.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depCarg.xml new file mode 100644 index 0000000..232f2fc --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depCarg.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depCargAutowire.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depCargAutowire.xml new file mode 100644 index 0000000..50e1325 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depCargAutowire.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depCargInner.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depCargInner.xml new file mode 100644 index 0000000..444c038 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depCargInner.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depDependsOn.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depDependsOn.xml new file mode 100644 index 0000000..413c4ca --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depDependsOn.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depDependsOnInner.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depDependsOnInner.xml new file mode 100644 index 0000000..429751e --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depDependsOnInner.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depMaterializeThis.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depMaterializeThis.xml new file mode 100644 index 0000000..e099f82 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depMaterializeThis.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + boPrototype + + + + + org.springframework.beans.factory.xml.DummyBo + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depProp.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depProp.xml new file mode 100644 index 0000000..efa5f1d --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depProp.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depPropAutowireByName.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depPropAutowireByName.xml new file mode 100644 index 0000000..3238643 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depPropAutowireByName.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depPropAutowireByType.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depPropAutowireByType.xml new file mode 100644 index 0000000..2854aa8 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depPropAutowireByType.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depPropInTheMiddle.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depPropInTheMiddle.xml new file mode 100644 index 0000000..83e215c --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depPropInTheMiddle.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depPropInner.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depPropInner.xml new file mode 100644 index 0000000..cafbea4 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-depPropInner.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-factoryCircle.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-factoryCircle.xml new file mode 100644 index 0000000..77fab13 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-factoryCircle.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-initializers.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-initializers.xml new file mode 100644 index 0000000..3c3bc61 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-initializers.xml @@ -0,0 +1,23 @@ + + + + + + + 7 + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-invalid.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-invalid.xml new file mode 100644 index 0000000..a749bba --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-invalid.xml @@ -0,0 +1,19 @@ + + + + + + + + Jenny + 30 + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-invalidOverridesNoSuchMethod.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-invalidOverridesNoSuchMethod.xml new file mode 100644 index 0000000..02f14e6 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-invalidOverridesNoSuchMethod.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + Jenny + 30 + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-localCollectionsUsingXsd.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-localCollectionsUsingXsd.xml new file mode 100644 index 0000000..51a9be9 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-localCollectionsUsingXsd.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-noSuchFactoryMethod.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-noSuchFactoryMethod.xml new file mode 100644 index 0000000..40ac430 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-noSuchFactoryMethod.xml @@ -0,0 +1,17 @@ + + + + + + + + + setterString + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-overrides.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-overrides.xml new file mode 100644 index 0000000..2105381 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-overrides.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Jenny + 30 + + + + + + + + + + Jenny + 30 + + + + + + + + + + + + + + Simple bean, without any collections. + + + The name of the user + David + + 27 + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-parent.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-parent.xml new file mode 100644 index 0000000..3f6f14f --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-parent.xml @@ -0,0 +1,23 @@ + + + + + + + parent + 1 + + + + parent + 1 + + + + parent + 2 + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-recursiveImport.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-recursiveImport.xml new file mode 100644 index 0000000..cb73bab --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-recursiveImport.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-reftypes.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-reftypes.xml new file mode 100644 index 0000000..d293346 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-reftypes.xml @@ -0,0 +1,214 @@ + + + + + Jenny + 30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Andrew + 36 + + + + + + + + + + + Georgia + 33 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + outer + 0 + + + + hasInner + 5 + + + inner1 + 6 + + + + + + inner2 + 7 + + + + inner5 + 6 + + + + + + + + inner3 + 8 + + + + + inner4 + 9 + + + + + + + + + + inner1 + 6 + + + + + + + + inner1 + 6 + + + + + + hasInner + 5 + + + inner1 + 6 + + + + + + inner2 + 7 + + + + inner5 + 6 + + + + + + + + + + + inner3 + 8 + + + + + + inner4 + 9 + + + + + + + + + + inner1 + 6 + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-resource.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-resource.xml new file mode 100644 index 0000000..d03cdec --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-resource.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-resourceImport.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-resourceImport.xml new file mode 100644 index 0000000..92e233b --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-resourceImport.xml @@ -0,0 +1,15 @@ + + + + + + + + classpath:org/springframework/beans/factory/xml/test.properties + + + classpath:org/springframework/beans/factory/xml/test.properties + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-testWithDuplicateNameInAlias.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-testWithDuplicateNameInAlias.xml new file mode 100644 index 0000000..333f448 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-testWithDuplicateNameInAlias.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-testWithDuplicateNames.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-testWithDuplicateNames.xml new file mode 100644 index 0000000..84f4ca2 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-testWithDuplicateNames.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/simplePropertyNamespaceHandlerWithExpressionLanguageTests.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/simplePropertyNamespaceHandlerWithExpressionLanguageTests.xml new file mode 100644 index 0000000..fcb7178 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/simplePropertyNamespaceHandlerWithExpressionLanguageTests.xml @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/support/CustomNamespaceHandlerTests-context.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/support/CustomNamespaceHandlerTests-context.xml new file mode 100644 index 0000000..8962b36 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/support/CustomNamespaceHandlerTests-context.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/support/CustomNamespaceHandlerTests.properties b/spring-context/src/test/resources/org/springframework/beans/factory/xml/support/CustomNamespaceHandlerTests.properties new file mode 100644 index 0000000..fe0eca6 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/support/CustomNamespaceHandlerTests.properties @@ -0,0 +1,2 @@ +http\://www.springframework.org/schema/beans/test=org.springframework.beans.factory.xml.support.TestNamespaceHandler +http\://www.springframework.org/schema/util=org.springframework.beans.factory.xml.UtilNamespaceHandler \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/support/CustomNamespaceHandlerTests.xsd b/spring-context/src/test/resources/org/springframework/beans/factory/xml/support/CustomNamespaceHandlerTests.xsd new file mode 100644 index 0000000..ccf71f6 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/support/CustomNamespaceHandlerTests.xsd @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/test.properties b/spring-context/src/test/resources/org/springframework/beans/factory/xml/test.properties new file mode 100644 index 0000000..30d74d2 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/test.properties @@ -0,0 +1 @@ +test \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/cache/config/annotationDrivenCacheConfig.xml b/spring-context/src/test/resources/org/springframework/cache/config/annotationDrivenCacheConfig.xml new file mode 100644 index 0000000..0057278 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/cache/config/annotationDrivenCacheConfig.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/cache/config/annotationDrivenCacheNamespace-manager-resolver.xml b/spring-context/src/test/resources/org/springframework/cache/config/annotationDrivenCacheNamespace-manager-resolver.xml new file mode 100644 index 0000000..4043efb --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/cache/config/annotationDrivenCacheNamespace-manager-resolver.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/cache/config/annotationDrivenCacheNamespace-resolver.xml b/spring-context/src/test/resources/org/springframework/cache/config/annotationDrivenCacheNamespace-resolver.xml new file mode 100644 index 0000000..1b9baab --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/cache/config/annotationDrivenCacheNamespace-resolver.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/cache/config/annotationDrivenCacheNamespace.xml b/spring-context/src/test/resources/org/springframework/cache/config/annotationDrivenCacheNamespace.xml new file mode 100644 index 0000000..9cffaef --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/cache/config/annotationDrivenCacheNamespace.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/cache/config/cache-advice-invalid.xml b/spring-context/src/test/resources/org/springframework/cache/config/cache-advice-invalid.xml new file mode 100644 index 0000000..06d6eb9 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/cache/config/cache-advice-invalid.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/cache/config/cache-advice.xml b/spring-context/src/test/resources/org/springframework/cache/config/cache-advice.xml new file mode 100644 index 0000000..ea6901d --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/cache/config/cache-advice.xml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/access/ContextJndiBeanFactoryLocatorTests-collections.xml b/spring-context/src/test/resources/org/springframework/context/access/ContextJndiBeanFactoryLocatorTests-collections.xml new file mode 100644 index 0000000..7430fc4 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/access/ContextJndiBeanFactoryLocatorTests-collections.xml @@ -0,0 +1,374 @@ + + + + + Jenny + 30 + + + + + + + + + Simple bean, without any collections. + + + The name of the user + David + + 27 + + + + Rod + 32 + + List of Rod's friends + + + + + + + + + Jenny + 30 + + + + + + + + David + 27 + + + + Rod + 32 + + + + + + + + + + + loner + 26 + + + My List + + + + + + + + + + literal + + + + + + + + + + + literal + + + + + + + + + verbose + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 10 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + bar + + + + + zero + + bar + + + + + ba + + + + + + + bar + + + + + + + + + + + + + + + + + + + bar + + + + + + + + + + + + + + + + + + bar + TWO + + + + + + + + + + + + + + + + + one + + + + + + + + + java.lang.String + java.lang.Exception + + + + + + + + 0 + 1 + 2 + + + + + + + + bar + jenny + + + + java.util.LinkedList + + + + + + + bar + jenny + + + + java.util.LinkedList + + + true + + + + + + + bar + jenny + + + + java.util.TreeSet + + + + + + + bar + jenny + + + + java.util.TreeSet + + + true + + + + + + + bar + jenny + + + + java.util.TreeMap + + + + + + + bar + jenny + + + + java.util.TreeMap + + + true + + + + + + + My Map + + + + + + + + + + + My Set + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/access/ContextJndiBeanFactoryLocatorTests-parent.xml b/spring-context/src/test/resources/org/springframework/context/access/ContextJndiBeanFactoryLocatorTests-parent.xml new file mode 100644 index 0000000..83fc1bc --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/access/ContextJndiBeanFactoryLocatorTests-parent.xml @@ -0,0 +1,23 @@ + + + + + + + parent + 1 + + + + parent + 1 + + + + parent + 2 + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/access/ContextSingletonBeanFactoryLocatorTests-context.xml b/spring-context/src/test/resources/org/springframework/context/access/ContextSingletonBeanFactoryLocatorTests-context.xml new file mode 100644 index 0000000..d203ea3 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/access/ContextSingletonBeanFactoryLocatorTests-context.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/DestroyMethodInferenceTests-context.xml b/spring-context/src/test/resources/org/springframework/context/annotation/DestroyMethodInferenceTests-context.xml new file mode 100644 index 0000000..5c66a6d --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/DestroyMethodInferenceTests-context.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/EnableLoadTimeWeavingTests-context.xml b/spring-context/src/test/resources/org/springframework/context/annotation/EnableLoadTimeWeavingTests-context.xml new file mode 100644 index 0000000..453fa97 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/EnableLoadTimeWeavingTests-context.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/Spr6602Tests-context.xml b/spring-context/src/test/resources/org/springframework/context/annotation/Spr6602Tests-context.xml new file mode 100644 index 0000000..69ff95f --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/Spr6602Tests-context.xml @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/aspectjTypeFilterTests.xml b/spring-context/src/test/resources/org/springframework/context/annotation/aspectjTypeFilterTests.xml new file mode 100644 index 0000000..234b205 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/aspectjTypeFilterTests.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/aspectjTypeFilterTestsWithPlaceholders.xml b/spring-context/src/test/resources/org/springframework/context/annotation/aspectjTypeFilterTestsWithPlaceholders.xml new file mode 100644 index 0000000..1193f33 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/aspectjTypeFilterTestsWithPlaceholders.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/componentScanRespectsProfileAnnotationTests.xml b/spring-context/src/test/resources/org/springframework/context/annotation/componentScanRespectsProfileAnnotationTests.xml new file mode 100644 index 0000000..c58935a --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/componentScanRespectsProfileAnnotationTests.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/componentScanWithAutowiredQualifierTests.xml b/spring-context/src/test/resources/org/springframework/context/annotation/componentScanWithAutowiredQualifierTests.xml new file mode 100644 index 0000000..a910e11 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/componentScanWithAutowiredQualifierTests.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/configuration/AutowiredConfigurationTests-custom.properties b/spring-context/src/test/resources/org/springframework/context/annotation/configuration/AutowiredConfigurationTests-custom.properties new file mode 100644 index 0000000..17a2bc6 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/configuration/AutowiredConfigurationTests-custom.properties @@ -0,0 +1,3 @@ +hostname=localhost +foo=a +bar=b \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/configuration/AutowiredConfigurationTests-custom.xml b/spring-context/src/test/resources/org/springframework/context/annotation/configuration/AutowiredConfigurationTests-custom.xml new file mode 100644 index 0000000..e19f905 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/configuration/AutowiredConfigurationTests-custom.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/configuration/AutowiredConfigurationTests.xml b/spring-context/src/test/resources/org/springframework/context/annotation/configuration/AutowiredConfigurationTests.xml new file mode 100644 index 0000000..2ea3203 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/configuration/AutowiredConfigurationTests.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/configuration/ImportNonXmlResourceConfig-context.properties b/spring-context/src/test/resources/org/springframework/context/annotation/configuration/ImportNonXmlResourceConfig-context.properties new file mode 100644 index 0000000..6c65636 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/configuration/ImportNonXmlResourceConfig-context.properties @@ -0,0 +1 @@ +propertiesDeclaredBean.(class)=org.springframework.beans.testfixture.beans.TestBean \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/configuration/ImportXmlConfig-context.xml b/spring-context/src/test/resources/org/springframework/context/annotation/configuration/ImportXmlConfig-context.xml new file mode 100644 index 0000000..015651e --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/configuration/ImportXmlConfig-context.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/configuration/ImportXmlWithAopNamespace-context.xml b/spring-context/src/test/resources/org/springframework/context/annotation/configuration/ImportXmlWithAopNamespace-context.xml new file mode 100644 index 0000000..85d3501 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/configuration/ImportXmlWithAopNamespace-context.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/configuration/ImportXmlWithConfigurationClass-context.xml b/spring-context/src/test/resources/org/springframework/context/annotation/configuration/ImportXmlWithConfigurationClass-context.xml new file mode 100644 index 0000000..2f555d9 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/configuration/ImportXmlWithConfigurationClass-context.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/configuration/SecondLevelSubConfig-context.xml b/spring-context/src/test/resources/org/springframework/context/annotation/configuration/SecondLevelSubConfig-context.xml new file mode 100644 index 0000000..78d050b --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/configuration/SecondLevelSubConfig-context.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/configuration/ValueInjectionTests.xml b/spring-context/src/test/resources/org/springframework/context/annotation/configuration/ValueInjectionTests.xml new file mode 100644 index 0000000..60fff9b --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/configuration/ValueInjectionTests.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/configuration/annotation-config.xml b/spring-context/src/test/resources/org/springframework/context/annotation/configuration/annotation-config.xml new file mode 100644 index 0000000..5eb5fc1 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/configuration/annotation-config.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/configuration/aspectj-autoproxy-config.xml b/spring-context/src/test/resources/org/springframework/context/annotation/configuration/aspectj-autoproxy-config.xml new file mode 100644 index 0000000..5ce8818 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/configuration/aspectj-autoproxy-config.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/customAnnotationUsedForBothComponentScanAndQualifierTests.xml b/spring-context/src/test/resources/org/springframework/context/annotation/customAnnotationUsedForBothComponentScanAndQualifierTests.xml new file mode 100644 index 0000000..abbb1f1 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/customAnnotationUsedForBothComponentScanAndQualifierTests.xml @@ -0,0 +1,22 @@ + + + + + + + + + + org.springframework.context.annotation.ComponentScanParserTests$CustomAnnotation + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/customNameGeneratorTests.xml b/spring-context/src/test/resources/org/springframework/context/annotation/customNameGeneratorTests.xml new file mode 100644 index 0000000..c6215b7 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/customNameGeneratorTests.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/customScopeResolverTests.xml b/spring-context/src/test/resources/org/springframework/context/annotation/customScopeResolverTests.xml new file mode 100644 index 0000000..32ca89d --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/customScopeResolverTests.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/customTypeFilterTests.xml b/spring-context/src/test/resources/org/springframework/context/annotation/customTypeFilterTests.xml new file mode 100644 index 0000000..d477a0e --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/customTypeFilterTests.xml @@ -0,0 +1,22 @@ + + + + + + + + + + org.springframework.context.annotation.ComponentScanParserTests$CustomAnnotation + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/defaultAutowireByNameTests.xml b/spring-context/src/test/resources/org/springframework/context/annotation/defaultAutowireByNameTests.xml new file mode 100644 index 0000000..3fb5f98 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/defaultAutowireByNameTests.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/defaultAutowireByTypeTests.xml b/spring-context/src/test/resources/org/springframework/context/annotation/defaultAutowireByTypeTests.xml new file mode 100644 index 0000000..66e1479 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/defaultAutowireByTypeTests.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/defaultAutowireConstructorTests.xml b/spring-context/src/test/resources/org/springframework/context/annotation/defaultAutowireConstructorTests.xml new file mode 100644 index 0000000..127a9d2 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/defaultAutowireConstructorTests.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/defaultAutowireNoTests.xml b/spring-context/src/test/resources/org/springframework/context/annotation/defaultAutowireNoTests.xml new file mode 100644 index 0000000..9a53dce --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/defaultAutowireNoTests.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/defaultInitAndDestroyMethodsTests.xml b/spring-context/src/test/resources/org/springframework/context/annotation/defaultInitAndDestroyMethodsTests.xml new file mode 100644 index 0000000..e74eca9 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/defaultInitAndDestroyMethodsTests.xml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/defaultLazyInitFalseTests.xml b/spring-context/src/test/resources/org/springframework/context/annotation/defaultLazyInitFalseTests.xml new file mode 100644 index 0000000..f827a05 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/defaultLazyInitFalseTests.xml @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/defaultLazyInitTrueTests.xml b/spring-context/src/test/resources/org/springframework/context/annotation/defaultLazyInitTrueTests.xml new file mode 100644 index 0000000..f61cb69 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/defaultLazyInitTrueTests.xml @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/defaultNonExistingInitAndDestroyMethodsTests.xml b/spring-context/src/test/resources/org/springframework/context/annotation/defaultNonExistingInitAndDestroyMethodsTests.xml new file mode 100644 index 0000000..3107e6a --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/defaultNonExistingInitAndDestroyMethodsTests.xml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/defaultWithNoOverridesTests.xml b/spring-context/src/test/resources/org/springframework/context/annotation/defaultWithNoOverridesTests.xml new file mode 100644 index 0000000..4182ffa --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/defaultWithNoOverridesTests.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/doubleScanTests.xml b/spring-context/src/test/resources/org/springframework/context/annotation/doubleScanTests.xml new file mode 100644 index 0000000..2bff6fd --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/doubleScanTests.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/invalidClassNameScopeResolverTests.xml b/spring-context/src/test/resources/org/springframework/context/annotation/invalidClassNameScopeResolverTests.xml new file mode 100644 index 0000000..9980545 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/invalidClassNameScopeResolverTests.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/invalidConstructorNameGeneratorTests.xml b/spring-context/src/test/resources/org/springframework/context/annotation/invalidConstructorNameGeneratorTests.xml new file mode 100644 index 0000000..c8a4056 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/invalidConstructorNameGeneratorTests.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/matchingResourcePatternTests.xml b/spring-context/src/test/resources/org/springframework/context/annotation/matchingResourcePatternTests.xml new file mode 100644 index 0000000..4919942 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/matchingResourcePatternTests.xml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/multipleConstructors.xml b/spring-context/src/test/resources/org/springframework/context/annotation/multipleConstructors.xml new file mode 100644 index 0000000..1571f75 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/multipleConstructors.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/nonMatchingResourcePatternTests.xml b/spring-context/src/test/resources/org/springframework/context/annotation/nonMatchingResourcePatternTests.xml new file mode 100644 index 0000000..5b4ea6e --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/nonMatchingResourcePatternTests.xml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/p1.properties b/spring-context/src/test/resources/org/springframework/context/annotation/p1.properties new file mode 100644 index 0000000..16140f6 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/p1.properties @@ -0,0 +1,4 @@ +testbean.name=p1TestBean +from.p1=p1Value +base.package=org/springframework/context/annotation +spring.profiles.active=test diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/p2.properties b/spring-context/src/test/resources/org/springframework/context/annotation/p2.properties new file mode 100644 index 0000000..34049d5 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/p2.properties @@ -0,0 +1,2 @@ +testbean.name=p2TestBean +from.p2=p2Value \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/p3.properties b/spring-context/src/test/resources/org/springframework/context/annotation/p3.properties new file mode 100644 index 0000000..5165c44 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/p3.properties @@ -0,0 +1 @@ +testbean.name=p3TestBean diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/p4.properties b/spring-context/src/test/resources/org/springframework/context/annotation/p4.properties new file mode 100644 index 0000000..a9fbccd --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/p4.properties @@ -0,0 +1 @@ +testbean.name=p4TestBean diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/scopedProxyDefaultTests.xml b/spring-context/src/test/resources/org/springframework/context/annotation/scopedProxyDefaultTests.xml new file mode 100644 index 0000000..2bce1ac --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/scopedProxyDefaultTests.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/scopedProxyInterfacesTests.xml b/spring-context/src/test/resources/org/springframework/context/annotation/scopedProxyInterfacesTests.xml new file mode 100644 index 0000000..e9d15fa --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/scopedProxyInterfacesTests.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/scopedProxyInvalidConfigTests.xml b/spring-context/src/test/resources/org/springframework/context/annotation/scopedProxyInvalidConfigTests.xml new file mode 100644 index 0000000..d06d20f --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/scopedProxyInvalidConfigTests.xml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/scopedProxyNoTests.xml b/spring-context/src/test/resources/org/springframework/context/annotation/scopedProxyNoTests.xml new file mode 100644 index 0000000..1b4ce15 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/scopedProxyNoTests.xml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/scopedProxyTargetClassTests.xml b/spring-context/src/test/resources/org/springframework/context/annotation/scopedProxyTargetClassTests.xml new file mode 100644 index 0000000..79bebe3 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/scopedProxyTargetClassTests.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/simpleConfigTests.xml b/spring-context/src/test/resources/org/springframework/context/annotation/simpleConfigTests.xml new file mode 100644 index 0000000..f0229b6 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/simpleConfigTests.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/simpleScanTests.xml b/spring-context/src/test/resources/org/springframework/context/annotation/simpleScanTests.xml new file mode 100644 index 0000000..81d8e7a --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/simpleScanTests.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/spr10546/importedResource.xml b/spring-context/src/test/resources/org/springframework/context/annotation/spr10546/importedResource.xml new file mode 100644 index 0000000..78e6147 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/spr10546/importedResource.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/config/contextNamespaceHandlerTests-location-placeholder.xml b/spring-context/src/test/resources/org/springframework/context/config/contextNamespaceHandlerTests-location-placeholder.xml new file mode 100644 index 0000000..958b3ed --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/config/contextNamespaceHandlerTests-location-placeholder.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/config/contextNamespaceHandlerTests-location.xml b/spring-context/src/test/resources/org/springframework/context/config/contextNamespaceHandlerTests-location.xml new file mode 100644 index 0000000..d6f292f --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/config/contextNamespaceHandlerTests-location.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/config/contextNamespaceHandlerTests-override.xml b/spring-context/src/test/resources/org/springframework/context/config/contextNamespaceHandlerTests-override.xml new file mode 100644 index 0000000..c34f324 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/config/contextNamespaceHandlerTests-override.xml @@ -0,0 +1,20 @@ + + + + + 42 + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/config/contextNamespaceHandlerTests-replace-ignore.xml b/spring-context/src/test/resources/org/springframework/context/config/contextNamespaceHandlerTests-replace-ignore.xml new file mode 100644 index 0000000..a736050 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/config/contextNamespaceHandlerTests-replace-ignore.xml @@ -0,0 +1,24 @@ + + + + + bar + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/config/contextNamespaceHandlerTests-replace.xml b/spring-context/src/test/resources/org/springframework/context/config/contextNamespaceHandlerTests-replace.xml new file mode 100644 index 0000000..8ff5d01 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/config/contextNamespaceHandlerTests-replace.xml @@ -0,0 +1,30 @@ + + + + + bar + MYNULL + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/config/contextNamespaceHandlerTests-simple.xml b/spring-context/src/test/resources/org/springframework/context/config/contextNamespaceHandlerTests-simple.xml new file mode 100644 index 0000000..b8f2aa8 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/config/contextNamespaceHandlerTests-simple.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/config/contextNamespaceHandlerTests-system.xml b/spring-context/src/test/resources/org/springframework/context/config/contextNamespaceHandlerTests-system.xml new file mode 100644 index 0000000..989d717 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/config/contextNamespaceHandlerTests-system.xml @@ -0,0 +1,23 @@ + + + + + bar + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/config/empty-foo.properties b/spring-context/src/test/resources/org/springframework/context/config/empty-foo.properties new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/config/empty-foo.properties @@ -0,0 +1 @@ + diff --git a/spring-context/src/test/resources/org/springframework/context/config/test-bar.properties b/spring-context/src/test/resources/org/springframework/context/config/test-bar.properties new file mode 100644 index 0000000..6d7afb4 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/config/test-bar.properties @@ -0,0 +1,2 @@ +bar= foo\t +spam=\tmaps diff --git a/spring-context/src/test/resources/org/springframework/context/config/test-foo.properties b/spring-context/src/test/resources/org/springframework/context/config/test-foo.properties new file mode 100644 index 0000000..74d0a43 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/config/test-foo.properties @@ -0,0 +1 @@ +foo=bar diff --git a/spring-context/src/test/resources/org/springframework/context/conversionservice/conversionService.xml b/spring-context/src/test/resources/org/springframework/context/conversionservice/conversionService.xml new file mode 100644 index 0000000..72dd63f --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/conversionservice/conversionService.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + #{'test-' + strValue + '-end'} + #{'test-' + strValue} + #{'test-' + numValue+ '-end'} + #{'test-' + numValue} + + + + classpath:test.xml + + + + classpath:test.xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/event/simple-event-configuration.xml b/spring-context/src/test/resources/org/springframework/context/event/simple-event-configuration.xml new file mode 100644 index 0000000..dd88324 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/event/simple-event-configuration.xml @@ -0,0 +1,17 @@ + + + + + + + + + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/context/groovy/applicationContext-error.groovy b/spring-context/src/test/resources/org/springframework/context/groovy/applicationContext-error.groovy new file mode 100644 index 0000000..85a13f0 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/groovy/applicationContext-error.groovy @@ -0,0 +1,6 @@ +package org.springframework.context.groovy + +beans = { + framework String, 'Grails' + foo String, 'hello' +} diff --git a/spring-context/src/test/resources/org/springframework/context/groovy/applicationContext.groovy b/spring-context/src/test/resources/org/springframework/context/groovy/applicationContext.groovy new file mode 100644 index 0000000..d5abf3d --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/groovy/applicationContext.groovy @@ -0,0 +1,6 @@ +package org.springframework.context.groovy + +beans { + framework String, 'Grails' + foo String, 'hello' +} diff --git a/spring-context/src/test/resources/org/springframework/context/groovy/applicationContext2.groovy b/spring-context/src/test/resources/org/springframework/context/groovy/applicationContext2.groovy new file mode 100644 index 0000000..e47f667 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/groovy/applicationContext2.groovy @@ -0,0 +1,5 @@ +package org.springframework.context.groovy + +beans { + company String, 'SpringSource' +} diff --git a/spring-context/src/test/resources/org/springframework/context/groovy/test.xml b/spring-context/src/test/resources/org/springframework/context/groovy/test.xml new file mode 100644 index 0000000..2228797 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/groovy/test.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/index/empty-spring.components b/spring-context/src/test/resources/org/springframework/context/index/empty-spring.components new file mode 100644 index 0000000..405817f --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/index/empty-spring.components @@ -0,0 +1,3 @@ +# +# Empty file to validate that if there is no entry we get a "null" index. +# \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/context/index/spring.components b/spring-context/src/test/resources/org/springframework/context/index/spring.components new file mode 100644 index 0000000..c03f2f0 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/index/spring.components @@ -0,0 +1,3 @@ +org.springframework.context.index.Sample1=foo +org.springframework.context.index.Sample2=bar,foo +org.springframework.context.index.Sample3=biz \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/context/support/ClassPathXmlApplicationContextTests-resource.xml b/spring-context/src/test/resources/org/springframework/context/support/ClassPathXmlApplicationContextTests-resource.xml new file mode 100644 index 0000000..4a85982 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/ClassPathXmlApplicationContextTests-resource.xml @@ -0,0 +1,17 @@ + + + + + + + + classpath:org/springframework/beans/factory/xml/test.properties + + + test.properties + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/support/ClassPathXmlApplicationContextTests-resourceImport.xml b/spring-context/src/test/resources/org/springframework/context/support/ClassPathXmlApplicationContextTests-resourceImport.xml new file mode 100644 index 0000000..442a334 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/ClassPathXmlApplicationContextTests-resourceImport.xml @@ -0,0 +1,18 @@ + + + + + + + + test.properties + + + testBeans.properties + + + classpath:org/springframework/beans/factory/xml/test.properties + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/support/GenericXmlApplicationContextTests-context.xml b/spring-context/src/test/resources/org/springframework/context/support/GenericXmlApplicationContextTests-context.xml new file mode 100644 index 0000000..5420411 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/GenericXmlApplicationContextTests-context.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/support/PropertySourcesPlaceholderConfigurerTests.properties b/spring-context/src/test/resources/org/springframework/context/support/PropertySourcesPlaceholderConfigurerTests.properties new file mode 100644 index 0000000..b8f6978 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/PropertySourcesPlaceholderConfigurerTests.properties @@ -0,0 +1 @@ +my.name=foo \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/context/support/aliasForParent.xml b/spring-context/src/test/resources/org/springframework/context/support/aliasForParent.xml new file mode 100644 index 0000000..423ac8e --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/aliasForParent.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/support/aliasThatOverridesParent.xml b/spring-context/src/test/resources/org/springframework/context/support/aliasThatOverridesParent.xml new file mode 100644 index 0000000..89ec371 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/aliasThatOverridesParent.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/support/childWithProxy.xml b/spring-context/src/test/resources/org/springframework/context/support/childWithProxy.xml new file mode 100644 index 0000000..2f818aa --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/childWithProxy.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/support/classWithPlaceholder.xml b/spring-context/src/test/resources/org/springframework/context/support/classWithPlaceholder.xml new file mode 100644 index 0000000..829b409 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/classWithPlaceholder.xml @@ -0,0 +1,17 @@ + + + + + + + + + StaticMessageSource + singleton + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/support/conversionService.xml b/spring-context/src/test/resources/org/springframework/context/support/conversionService.xml new file mode 100644 index 0000000..97570ac --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/conversionService.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/support/conversionServiceWithResourceOverriding.xml b/spring-context/src/test/resources/org/springframework/context/support/conversionServiceWithResourceOverriding.xml new file mode 100644 index 0000000..848dfbf --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/conversionServiceWithResourceOverriding.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/support/invalidClass.xml b/spring-context/src/test/resources/org/springframework/context/support/invalidClass.xml new file mode 100644 index 0000000..0b87e24 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/invalidClass.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/support/invalidValueType.xml b/spring-context/src/test/resources/org/springframework/context/support/invalidValueType.xml new file mode 100644 index 0000000..544451e --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/invalidValueType.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/support/lifecycleTests.xml b/spring-context/src/test/resources/org/springframework/context/support/lifecycleTests.xml new file mode 100644 index 0000000..4b4b450 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/lifecycleTests.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/support/messages.properties b/spring-context/src/test/resources/org/springframework/context/support/messages.properties new file mode 100644 index 0000000..90fb2bb --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/messages.properties @@ -0,0 +1,5 @@ + code1 = mess\ + age1 +code2=message2 +hello={0}, {1} +escaped=I''m diff --git a/spring-context/src/test/resources/org/springframework/context/support/messages_de.properties b/spring-context/src/test/resources/org/springframework/context/support/messages_de.properties new file mode 100644 index 0000000..a9a00b1 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/messages_de.properties @@ -0,0 +1 @@ +code2=nachricht2 diff --git a/spring-context/src/test/resources/org/springframework/context/support/messages_de_AT.properties b/spring-context/src/test/resources/org/springframework/context/support/messages_de_AT.properties new file mode 100644 index 0000000..1f363cc --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/messages_de_AT.properties @@ -0,0 +1 @@ +code2=nochricht2 diff --git a/spring-context/src/test/resources/org/springframework/context/support/messages_de_AT_oo.properties b/spring-context/src/test/resources/org/springframework/context/support/messages_de_AT_oo.properties new file mode 100644 index 0000000..b0a9428 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/messages_de_AT_oo.properties @@ -0,0 +1 @@ +code2=noochricht2 diff --git a/spring-context/src/test/resources/org/springframework/context/support/messages_de_DE.xml b/spring-context/src/test/resources/org/springframework/context/support/messages_de_DE.xml new file mode 100644 index 0000000..fe84234 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/messages_de_DE.xml @@ -0,0 +1,8 @@ + + + + + + nachricht2xml + + diff --git a/spring-context/src/test/resources/org/springframework/context/support/more-messages.properties b/spring-context/src/test/resources/org/springframework/context/support/more-messages.properties new file mode 100644 index 0000000..1a76f24 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/more-messages.properties @@ -0,0 +1 @@ +code3=message3 diff --git a/spring-context/src/test/resources/org/springframework/context/support/override.properties b/spring-context/src/test/resources/org/springframework/context/support/override.properties new file mode 100644 index 0000000..37e7f13 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/override.properties @@ -0,0 +1,2 @@ +wrappedAssemblerOne.proxyTargetClass=true +wrappedAssemblerTwo.proxyTargetClass=true diff --git a/spring-context/src/test/resources/org/springframework/context/support/placeholder.properties b/spring-context/src/test/resources/org/springframework/context/support/placeholder.properties new file mode 100644 index 0000000..11f0d7f --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/placeholder.properties @@ -0,0 +1,4 @@ +targetName=wrappedAssemblerOne +logicName=logicTwo +realLogicName=realLogic +jedi=true diff --git a/spring-context/src/test/resources/org/springframework/context/support/simpleContext.xml b/spring-context/src/test/resources/org/springframework/context/support/simpleContext.xml new file mode 100644 index 0000000..133d5c3 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/simpleContext.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/support/simpleThreadScopeTests.xml b/spring-context/src/test/resources/org/springframework/context/support/simpleThreadScopeTests.xml new file mode 100644 index 0000000..cb25ac9 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/simpleThreadScopeTests.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/support/spr7283.xml b/spring-context/src/test/resources/org/springframework/context/support/spr7283.xml new file mode 100644 index 0000000..926a23f --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/spr7283.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/support/spr7816.xml b/spring-context/src/test/resources/org/springframework/context/support/spr7816.xml new file mode 100644 index 0000000..1998eeb --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/spr7816.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/support/test.properties b/spring-context/src/test/resources/org/springframework/context/support/test.properties new file mode 100644 index 0000000..6cfa0ea --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/test.properties @@ -0,0 +1 @@ +contexttest \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/context/support/test/aliased-contextC.xml b/spring-context/src/test/resources/org/springframework/context/support/test/aliased-contextC.xml new file mode 100644 index 0000000..5cdfe64 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/test/aliased-contextC.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/support/test/contextA.xml b/spring-context/src/test/resources/org/springframework/context/support/test/contextA.xml new file mode 100644 index 0000000..0ac88a5 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/test/contextA.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/support/test/contextB.xml b/spring-context/src/test/resources/org/springframework/context/support/test/contextB.xml new file mode 100644 index 0000000..eb18246 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/test/contextB.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/support/test/contextC.xml b/spring-context/src/test/resources/org/springframework/context/support/test/contextC.xml new file mode 100644 index 0000000..2546c12 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/test/contextC.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/support/test/import1.xml b/spring-context/src/test/resources/org/springframework/context/support/test/import1.xml new file mode 100644 index 0000000..9d647df --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/test/import1.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/support/test/subtest/import2.xml b/spring-context/src/test/resources/org/springframework/context/support/test/subtest/import2.xml new file mode 100644 index 0000000..35afe2a --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/test/subtest/import2.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/context/support/testBeans.properties b/spring-context/src/test/resources/org/springframework/context/support/testBeans.properties new file mode 100644 index 0000000..eb02a14 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/support/testBeans.properties @@ -0,0 +1,42 @@ +# this must only be used for ApplicationContexts, some classes are only appropriate for application contexts + +rod.(class)=org.springframework.beans.testfixture.beans.TestBean +rod.name=Rod +rod.age=31 + +roderick.(parent)=rod +roderick.name=Roderick + +kerry.(class)=org.springframework.beans.testfixture.beans.TestBean +kerry.name=Kerry +kerry.age=34 +kerry.spouse(ref)=rod + +kathy.(class)=org.springframework.beans.testfixture.beans.TestBean +kathy.(singleton)=false + +typeMismatch.(class)=org.springframework.beans.testfixture.beans.TestBean +typeMismatch.name=typeMismatch +typeMismatch.age=34x +typeMismatch.spouse(ref)=rod +typeMismatch.(singleton)=false + +validEmpty.(class)=org.springframework.beans.testfixture.beans.TestBean + +listenerVeto.(class)=org.springframework.beans.testfixture.beans.TestBean + +typeMismatch.name=typeMismatch +typeMismatch.age=34x +typeMismatch.spouse(ref)=rod + +singletonFactory.(class)=org.springframework.beans.testfixture.beans.factory.DummyFactory +singletonFactory.singleton=true + +prototypeFactory.(class)=org.springframework.beans.testfixture.beans.factory.DummyFactory +prototypeFactory.singleton=false + +mustBeInitialized.(class)=org.springframework.beans.testfixture.beans.MustBeInitialized + +lifecycle.(class)=org.springframework.context.LifecycleContextBean + +lifecyclePostProcessor.(class)=org.springframework.beans.testfixture.beans.LifecycleBean$PostProcessor diff --git a/spring-context/src/test/resources/org/springframework/ejb/config/jeeNamespaceHandlerTests.xml b/spring-context/src/test/resources/org/springframework/ejb/config/jeeNamespaceHandlerTests.xml new file mode 100644 index 0000000..6d0dc55 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/ejb/config/jeeNamespaceHandlerTests.xml @@ -0,0 +1,78 @@ + + + + + + + jdbc/MyDataSource + + + + + + + + + + + foo=bar + + + + + + bar + + + + + + + foo=bar + + + + + + + foo=bar + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/instrument/classloading/testResource.xml b/spring-context/src/test/resources/org/springframework/instrument/classloading/testResource.xml new file mode 100644 index 0000000..9de559d --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/instrument/classloading/testResource.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/jmx/applicationContext.xml b/spring-context/src/test/resources/org/springframework/jmx/applicationContext.xml new file mode 100644 index 0000000..4c38618 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/jmx/applicationContext.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + TEST + + + 100 + + + + diff --git a/spring-context/src/test/resources/org/springframework/jmx/export/annotation/annotations.xml b/spring-context/src/test/resources/org/springframework/jmx/export/annotation/annotations.xml new file mode 100644 index 0000000..fbf2dc6 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/jmx/export/annotation/annotations.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/jmx/export/annotation/componentScan.xml b/spring-context/src/test/resources/org/springframework/jmx/export/annotation/componentScan.xml new file mode 100644 index 0000000..2d2dd2d --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/jmx/export/annotation/componentScan.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/jmx/export/annotation/lazyAssembling.xml b/spring-context/src/test/resources/org/springframework/jmx/export/annotation/lazyAssembling.xml new file mode 100644 index 0000000..7359d87 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/jmx/export/annotation/lazyAssembling.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/jmx/export/annotation/lazyNaming.xml b/spring-context/src/test/resources/org/springframework/jmx/export/annotation/lazyNaming.xml new file mode 100644 index 0000000..79651d1 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/jmx/export/annotation/lazyNaming.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/jmx/export/assembler/interfaceAssembler.xml b/spring-context/src/test/resources/org/springframework/jmx/export/assembler/interfaceAssembler.xml new file mode 100644 index 0000000..55736ca --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/jmx/export/assembler/interfaceAssembler.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TEST + + + 100 + + + + diff --git a/spring-context/src/test/resources/org/springframework/jmx/export/assembler/interfaceAssemblerCustom.xml b/spring-context/src/test/resources/org/springframework/jmx/export/assembler/interfaceAssemblerCustom.xml new file mode 100644 index 0000000..7a5af96 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/jmx/export/assembler/interfaceAssemblerCustom.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + org.springframework.jmx.export.assembler.ICustomJmxBean + + + + + + + + + + + + + + + TEST + + + 100 + + + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/jmx/export/assembler/interfaceAssemblerMapped.xml b/spring-context/src/test/resources/org/springframework/jmx/export/assembler/interfaceAssemblerMapped.xml new file mode 100644 index 0000000..9c4703b --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/jmx/export/assembler/interfaceAssemblerMapped.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + org.springframework.jmx.export.assembler.IAdditionalTestMethods, + org.springframework.jmx.export.assembler.ICustomJmxBean + + + + + + + + + + + + + + + + + TEST + + + 100 + + + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/jmx/export/assembler/metadata-autodetect.xml b/spring-context/src/test/resources/org/springframework/jmx/export/assembler/metadata-autodetect.xml new file mode 100644 index 0000000..10485da --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/jmx/export/assembler/metadata-autodetect.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + TEST + + + 100 + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/jmx/export/assembler/metadataAssembler.xml b/spring-context/src/test/resources/org/springframework/jmx/export/assembler/metadataAssembler.xml new file mode 100644 index 0000000..ab432b5 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/jmx/export/assembler/metadataAssembler.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + TEST + + + 100 + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/jmx/export/assembler/methodExclusionAssembler.xml b/spring-context/src/test/resources/org/springframework/jmx/export/assembler/methodExclusionAssembler.xml new file mode 100644 index 0000000..1c74cdf --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/jmx/export/assembler/methodExclusionAssembler.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/jmx/export/assembler/methodExclusionAssemblerCombo.xml b/spring-context/src/test/resources/org/springframework/jmx/export/assembler/methodExclusionAssemblerCombo.xml new file mode 100644 index 0000000..6618f94 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/jmx/export/assembler/methodExclusionAssemblerCombo.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + setAge,isSuperman,setSuperman,dontExposeMe + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/jmx/export/assembler/methodExclusionAssemblerMapped.xml b/spring-context/src/test/resources/org/springframework/jmx/export/assembler/methodExclusionAssemblerMapped.xml new file mode 100644 index 0000000..6c765a2 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/jmx/export/assembler/methodExclusionAssemblerMapped.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + setAge,isSuperman,setSuperman,dontExposeMe + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/jmx/export/assembler/methodExclusionAssemblerNotMapped.xml b/spring-context/src/test/resources/org/springframework/jmx/export/assembler/methodExclusionAssemblerNotMapped.xml new file mode 100644 index 0000000..b67ba36 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/jmx/export/assembler/methodExclusionAssemblerNotMapped.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + setAge,isSuperman,setSuperman,dontExposeMe + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/jmx/export/assembler/methodNameAssembler.xml b/spring-context/src/test/resources/org/springframework/jmx/export/assembler/methodNameAssembler.xml new file mode 100644 index 0000000..e431cd7 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/jmx/export/assembler/methodNameAssembler.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + add,myOperation,getName,setName,getAge + + + + + + + + + + + + + + + TEST + + + 100 + + + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/jmx/export/assembler/methodNameAssemblerMapped.xml b/spring-context/src/test/resources/org/springframework/jmx/export/assembler/methodNameAssemblerMapped.xml new file mode 100644 index 0000000..195cacc --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/jmx/export/assembler/methodNameAssemblerMapped.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + getNickName,setNickName,add,myOperation,getName,setName,getAge + + + + + + + + + + + + + + + + + + + + TEST + + + 100 + + + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/jmx/export/assembler/reflectiveAssembler.xml b/spring-context/src/test/resources/org/springframework/jmx/export/assembler/reflectiveAssembler.xml new file mode 100644 index 0000000..797d207 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/jmx/export/assembler/reflectiveAssembler.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + TEST + + + 100 + + + + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/jmx/export/autodetectLazyMBeans.xml b/spring-context/src/test/resources/org/springframework/jmx/export/autodetectLazyMBeans.xml new file mode 100644 index 0000000..e82e678 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/jmx/export/autodetectLazyMBeans.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/jmx/export/autodetectMBeans.xml b/spring-context/src/test/resources/org/springframework/jmx/export/autodetectMBeans.xml new file mode 100644 index 0000000..05f7c00 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/jmx/export/autodetectMBeans.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/jmx/export/autodetectNoMBeans.xml b/spring-context/src/test/resources/org/springframework/jmx/export/autodetectNoMBeans.xml new file mode 100644 index 0000000..e569169 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/jmx/export/autodetectNoMBeans.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/jmx/export/customConfigurer.xml b/spring-context/src/test/resources/org/springframework/jmx/export/customConfigurer.xml new file mode 100644 index 0000000..1d8ac9c --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/jmx/export/customConfigurer.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/jmx/export/excludedBeans.xml b/spring-context/src/test/resources/org/springframework/jmx/export/excludedBeans.xml new file mode 100644 index 0000000..010c57a --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/jmx/export/excludedBeans.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + true + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/jmx/export/lazyInit.xml b/spring-context/src/test/resources/org/springframework/jmx/export/lazyInit.xml new file mode 100644 index 0000000..7b5b8b8 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/jmx/export/lazyInit.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/jmx/export/naming/jmx-names.properties b/spring-context/src/test/resources/org/springframework/jmx/export/naming/jmx-names.properties new file mode 100644 index 0000000..9239236 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/jmx/export/naming/jmx-names.properties @@ -0,0 +1 @@ +namingTest = bean:name=namingTest diff --git a/spring-context/src/test/resources/org/springframework/jmx/export/notificationPublisherLazyTests.xml b/spring-context/src/test/resources/org/springframework/jmx/export/notificationPublisherLazyTests.xml new file mode 100644 index 0000000..2a3513a --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/jmx/export/notificationPublisherLazyTests.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/jmx/export/notificationPublisherTests.xml b/spring-context/src/test/resources/org/springframework/jmx/export/notificationPublisherTests.xml new file mode 100644 index 0000000..8b8699a --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/jmx/export/notificationPublisherTests.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/jmx/export/propertyPlaceholderConfigurer.xml b/spring-context/src/test/resources/org/springframework/jmx/export/propertyPlaceholderConfigurer.xml new file mode 100644 index 0000000..40d1791 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/jmx/export/propertyPlaceholderConfigurer.xml @@ -0,0 +1,49 @@ + + + + + + + + + Rob Harrop + 100 + myScope + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${testBean.name} + + + ${testBean.age} + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/scheduling/annotation/taskNamespaceTests.xml b/spring-context/src/test/resources/org/springframework/scheduling/annotation/taskNamespaceTests.xml new file mode 100644 index 0000000..4575462 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scheduling/annotation/taskNamespaceTests.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/scheduling/config/annotationDrivenContext.xml b/spring-context/src/test/resources/org/springframework/scheduling/config/annotationDrivenContext.xml new file mode 100644 index 0000000..02353c8 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scheduling/config/annotationDrivenContext.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/scheduling/config/executorContext.xml b/spring-context/src/test/resources/org/springframework/scheduling/config/executorContext.xml new file mode 100644 index 0000000..67c401d --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scheduling/config/executorContext.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 123 + 5-25 + 0-99 + 22-abc + + + diff --git a/spring-context/src/test/resources/org/springframework/scheduling/config/lazyScheduledTasksContext.xml b/spring-context/src/test/resources/org/springframework/scheduling/config/lazyScheduledTasksContext.xml new file mode 100644 index 0000000..cbf5953 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scheduling/config/lazyScheduledTasksContext.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/scheduling/config/scheduledTasksContext.xml b/spring-context/src/test/resources/org/springframework/scheduling/config/scheduledTasksContext.xml new file mode 100644 index 0000000..f51d1ee --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scheduling/config/scheduledTasksContext.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/scheduling/config/schedulerContext.xml b/spring-context/src/test/resources/org/springframework/scheduling/config/schedulerContext.xml new file mode 100644 index 0000000..5ce62f6 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scheduling/config/schedulerContext.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/scripting/bsh/Broken.bsh b/spring-context/src/test/resources/org/springframework/scripting/bsh/Broken.bsh new file mode 100644 index 0000000..5b1af16 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/bsh/Broken.bsh @@ -0,0 +1 @@ +one sure is the loneliest number... that i'll ever know \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/scripting/bsh/Calculator.bsh b/spring-context/src/test/resources/org/springframework/scripting/bsh/Calculator.bsh new file mode 100644 index 0000000..cc88f79 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/bsh/Calculator.bsh @@ -0,0 +1,3 @@ +int add(int x, int y) { + return x + y; +} \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/scripting/bsh/Messenger.bsh b/spring-context/src/test/resources/org/springframework/scripting/bsh/Messenger.bsh new file mode 100644 index 0000000..b15af62 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/bsh/Messenger.bsh @@ -0,0 +1,21 @@ +String message; + +boolean active; + +void init() { + active = true; +} + +String getMessage() { + if (!active && message != null) throw new java.lang.IllegalStateException(); + return message; +} + +void setMessage(String aMessage) { + message = aMessage; +} + +void destroy() { + message = null; + active = false; +} diff --git a/spring-context/src/test/resources/org/springframework/scripting/bsh/MessengerImpl.bsh b/spring-context/src/test/resources/org/springframework/scripting/bsh/MessengerImpl.bsh new file mode 100644 index 0000000..2878b71 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/bsh/MessengerImpl.bsh @@ -0,0 +1,38 @@ +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.scripting.TestBeanAwareMessenger; + +public class MyMessenger implements TestBeanAwareMessenger { + + private String message; + + private TestBean testBean; + + private boolean active; + + public void init() { + active = true; + } + + public String getMessage() { + if (!active && message != null) throw new java.lang.IllegalStateException(); + return message; + } + + public void setMessage(String aMessage) { + message = aMessage; + } + + public TestBean getTestBean() { + return testBean; + } + + public void setTestBean(TestBean tb) { + testBean = tb; + } + + public void destroy() { + message = null; + active = false; + } + +} diff --git a/spring-context/src/test/resources/org/springframework/scripting/bsh/MessengerInstance.bsh b/spring-context/src/test/resources/org/springframework/scripting/bsh/MessengerInstance.bsh new file mode 100644 index 0000000..e211b1b --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/bsh/MessengerInstance.bsh @@ -0,0 +1,28 @@ +import org.springframework.scripting.Messenger; + +public class MyMessenger implements Messenger { + + private String message; + + private boolean active; + + public void init() { + active = true; + } + + public String getMessage() { + if (!active && message != null) throw new java.lang.IllegalStateException(); + return message; + } + + public void setMessage(String aMessage) { + message = aMessage; + } + + public void destroy() { + message = null; + active = false; + } +} + +return new MyMessenger() ; diff --git a/spring-context/src/test/resources/org/springframework/scripting/bsh/bsh-with-xsd.xml b/spring-context/src/test/resources/org/springframework/scripting/bsh/bsh-with-xsd.xml new file mode 100644 index 0000000..44311c2 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/bsh/bsh-with-xsd.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + int add(int x, int y) { + return x + y; + } + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/scripting/bsh/bshBrokenContext.xml b/spring-context/src/test/resources/org/springframework/scripting/bsh/bshBrokenContext.xml new file mode 100644 index 0000000..2f17aae --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/bsh/bshBrokenContext.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/scripting/bsh/bshContext.xml b/spring-context/src/test/resources/org/springframework/scripting/bsh/bshContext.xml new file mode 100644 index 0000000..75479b9 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/bsh/bshContext.xml @@ -0,0 +1,71 @@ + + + + + + + + + + inline: +int add(int x, int y) { + return x + y; +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/scripting/bsh/bshRefreshableContext.xml b/spring-context/src/test/resources/org/springframework/scripting/bsh/bshRefreshableContext.xml new file mode 100644 index 0000000..be11d75 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/bsh/bshRefreshableContext.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/scripting/bsh/simple.bsh b/spring-context/src/test/resources/org/springframework/scripting/bsh/simple.bsh new file mode 100644 index 0000000..9f5e0b6 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/bsh/simple.bsh @@ -0,0 +1 @@ +return 3 * 2; diff --git a/spring-context/src/test/resources/org/springframework/scripting/config/TestBean.groovy b/spring-context/src/test/resources/org/springframework/scripting/config/TestBean.groovy new file mode 100644 index 0000000..74188ad --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/config/TestBean.groovy @@ -0,0 +1,19 @@ +package org.springframework.scripting.config + +class TestBean implements ITestBean { + + ITestBean otherBean + + boolean initialized + + boolean destroyed + + void setOtherBean(ITestBean otherBean) { + this.otherBean = otherBean; + } + + void startup() { this.initialized = true } + + void shutdown() { this.destroyed = true } + +} \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/scripting/config/scriptingDefaultsProxyTargetClassTests.xml b/spring-context/src/test/resources/org/springframework/scripting/config/scriptingDefaultsProxyTargetClassTests.xml new file mode 100644 index 0000000..46b746c --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/config/scriptingDefaultsProxyTargetClassTests.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/scripting/config/scriptingDefaultsTests.xml b/spring-context/src/test/resources/org/springframework/scripting/config/scriptingDefaultsTests.xml new file mode 100644 index 0000000..6c9a4f1 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/config/scriptingDefaultsTests.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/scripting/groovy/Broken.groovyb b/spring-context/src/test/resources/org/springframework/scripting/groovy/Broken.groovyb new file mode 100644 index 0000000..b9e6515 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/groovy/Broken.groovyb @@ -0,0 +1,14 @@ +I have eaten +the plums +that were in +the icebox + +and which +you were probably +saving +for breakfast + +Forgive me +they were delicious +so sweet +and so cold \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/scripting/groovy/Calculator.groovy b/spring-context/src/test/resources/org/springframework/scripting/groovy/Calculator.groovy new file mode 100644 index 0000000..2817bb9 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/groovy/Calculator.groovy @@ -0,0 +1,12 @@ +package org.springframework.scripting.groovy; + +import org.springframework.scripting.Calculator + +class GroovyCalculator implements Calculator { + + @Override + int add(int x, int y) { + return x + y; + } + +} diff --git a/spring-context/src/test/resources/org/springframework/scripting/groovy/CallCounter.groovy b/spring-context/src/test/resources/org/springframework/scripting/groovy/CallCounter.groovy new file mode 100644 index 0000000..18712db --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/groovy/CallCounter.groovy @@ -0,0 +1,27 @@ +package org.springframework.scripting.groovy; + +import org.springframework.scripting.CallCounter; + +class GroovyCallCounter implements CallCounter { + + int count = -100; + + void init() { + count = 0; + } + + @Override + void before() { + count++; + } + + @Override + int getCalls() { + return count; + } + + void destroy() { + count = -200; + } + +} diff --git a/spring-context/src/test/resources/org/springframework/scripting/groovy/DelegatingCalculator.groovy b/spring-context/src/test/resources/org/springframework/scripting/groovy/DelegatingCalculator.groovy new file mode 100644 index 0000000..02e8c8c --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/groovy/DelegatingCalculator.groovy @@ -0,0 +1,21 @@ +package org.springframework.scripting.groovy; + +import org.springframework.scripting.Calculator + +class DelegatingCalculator implements Calculator { + + def Calculator delegate; + + @Override + int add(int x, int y) { + //println "hello" + //println this.metaClass.getClass() + //println delegate.metaClass.getClass() + //delegate.metaClass.invokeMethod("add", [x,y]) + + delegate.callMissingMethod() + + return delegate.add(x,y) + } + +} diff --git a/spring-context/src/test/resources/org/springframework/scripting/groovy/GroovyAspectIntegrationTests-groovy-dynamic-context.xml b/spring-context/src/test/resources/org/springframework/scripting/groovy/GroovyAspectIntegrationTests-groovy-dynamic-context.xml new file mode 100644 index 0000000..f54649b --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/groovy/GroovyAspectIntegrationTests-groovy-dynamic-context.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/scripting/groovy/GroovyAspectIntegrationTests-groovy-interface-context.xml b/spring-context/src/test/resources/org/springframework/scripting/groovy/GroovyAspectIntegrationTests-groovy-interface-context.xml new file mode 100644 index 0000000..4f0cfbd --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/groovy/GroovyAspectIntegrationTests-groovy-interface-context.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/scripting/groovy/GroovyAspectIntegrationTests-groovy-proxy-target-class-context.xml b/spring-context/src/test/resources/org/springframework/scripting/groovy/GroovyAspectIntegrationTests-groovy-proxy-target-class-context.xml new file mode 100644 index 0000000..1c32d49 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/groovy/GroovyAspectIntegrationTests-groovy-proxy-target-class-context.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/scripting/groovy/GroovyAspectIntegrationTests-java-context.xml b/spring-context/src/test/resources/org/springframework/scripting/groovy/GroovyAspectIntegrationTests-java-context.xml new file mode 100644 index 0000000..6a403f7 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/groovy/GroovyAspectIntegrationTests-java-context.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/scripting/groovy/GroovyServiceImpl.grv b/spring-context/src/test/resources/org/springframework/scripting/groovy/GroovyServiceImpl.grv new file mode 100644 index 0000000..dadb2bc --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/groovy/GroovyServiceImpl.grv @@ -0,0 +1,11 @@ +package org.springframework.scripting.groovy; + +@Log +public class GroovyServiceImpl implements TestService { + + public String sayHello() { + throw new TestException("GroovyServiceImpl"); + } + + +} \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/scripting/groovy/Messenger.groovy b/spring-context/src/test/resources/org/springframework/scripting/groovy/Messenger.groovy new file mode 100644 index 0000000..002aca8 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/groovy/Messenger.groovy @@ -0,0 +1,7 @@ +package org.springframework.scripting.groovy; + +import org.springframework.stereotype.Component; + +@Component +class GroovyMessenger2 extends ConcreteMessenger { +} diff --git a/spring-context/src/test/resources/org/springframework/scripting/groovy/MessengerInstance.groovy b/spring-context/src/test/resources/org/springframework/scripting/groovy/MessengerInstance.groovy new file mode 100644 index 0000000..8f0a864 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/groovy/MessengerInstance.groovy @@ -0,0 +1,14 @@ +package org.springframework.scripting.groovy; + +import org.springframework.scripting.Messenger + +class GroovyMessenger implements Messenger { + + GroovyMessenger() { + println "GroovyMessenger" + } + + def String message; +} + +return new GroovyMessenger(); diff --git a/spring-context/src/test/resources/org/springframework/scripting/groovy/ScriptBean.groovy b/spring-context/src/test/resources/org/springframework/scripting/groovy/ScriptBean.groovy new file mode 100644 index 0000000..a3d64aa --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/groovy/ScriptBean.groovy @@ -0,0 +1,27 @@ +package org.springframework.scripting.groovy; + +import org.springframework.beans.testfixture.beans.TestBean +import org.springframework.context.ApplicationContext +import org.springframework.context.ApplicationContextAware +import org.springframework.scripting.ContextScriptBean + +class GroovyScriptBean implements ContextScriptBean, ApplicationContextAware { + + private int age + + @Override + int getAge() { + return this.age + } + + @Override + void setAge(int age) { + this.age = age + } + + def String name + + def TestBean testBean; + + def ApplicationContext applicationContext +} diff --git a/spring-context/src/test/resources/org/springframework/scripting/groovy/TestFactoryBean.groovy b/spring-context/src/test/resources/org/springframework/scripting/groovy/TestFactoryBean.groovy new file mode 100644 index 0000000..51c0bda --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/groovy/TestFactoryBean.groovy @@ -0,0 +1,21 @@ +package org.springframework.scripting.groovy; + +import org.springframework.beans.factory.FactoryBean + +class TestFactoryBean implements FactoryBean { + + @Override + public boolean isSingleton() { + true + } + + @Override + public Class getObjectType() { + String.class + } + + @Override + public Object getObject() { + "test" + } +} diff --git a/spring-context/src/test/resources/org/springframework/scripting/groovy/calculators-with-xsd.xml b/spring-context/src/test/resources/org/springframework/scripting/groovy/calculators-with-xsd.xml new file mode 100644 index 0000000..5ac8f7e --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/groovy/calculators-with-xsd.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/scripting/groovy/calculators.xml b/spring-context/src/test/resources/org/springframework/scripting/groovy/calculators.xml new file mode 100644 index 0000000..4909bbd --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/groovy/calculators.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/scripting/groovy/groovy-multiple-properties.xml b/spring-context/src/test/resources/org/springframework/scripting/groovy/groovy-multiple-properties.xml new file mode 100644 index 0000000..4f663dd --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/groovy/groovy-multiple-properties.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/scripting/groovy/groovy-with-xsd-jsr223.xml b/spring-context/src/test/resources/org/springframework/scripting/groovy/groovy-with-xsd-jsr223.xml new file mode 100644 index 0000000..1762200 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/groovy/groovy-with-xsd-jsr223.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + package org.springframework.scripting.groovy; + import org.springframework.scripting.Messenger + class GroovyMessenger implements Messenger { + def String message; + } + return new GroovyMessenger(); + + + + + + package org.springframework.scripting.groovy; + import org.springframework.scripting.Messenger + class GroovyMessenger implements Messenger { + def String message; + } + return new GroovyMessenger(); + + + + diff --git a/spring-context/src/test/resources/org/springframework/scripting/groovy/groovy-with-xsd-proxy-target-class.xml b/spring-context/src/test/resources/org/springframework/scripting/groovy/groovy-with-xsd-proxy-target-class.xml new file mode 100644 index 0000000..5163e19 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/groovy/groovy-with-xsd-proxy-target-class.xml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/scripting/groovy/groovy-with-xsd.xml b/spring-context/src/test/resources/org/springframework/scripting/groovy/groovy-with-xsd.xml new file mode 100644 index 0000000..3fdfe26 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/groovy/groovy-with-xsd.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + package org.springframework.scripting.groovy; +import org.springframework.scripting.Calculator +class GroovyCalculator implements Calculator { + int add(int x, int y) { + return x + y; + } +} + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/scripting/groovy/groovyBrokenContext.xml b/spring-context/src/test/resources/org/springframework/scripting/groovy/groovyBrokenContext.xml new file mode 100644 index 0000000..bc4eb22 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/groovy/groovyBrokenContext.xml @@ -0,0 +1,11 @@ + + + + + + + + + + \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/scripting/groovy/groovyContext.xml b/spring-context/src/test/resources/org/springframework/scripting/groovy/groovyContext.xml new file mode 100644 index 0000000..f565a26 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/groovy/groovyContext.xml @@ -0,0 +1,66 @@ + + + + + + + + inline: +package org.springframework.scripting.groovy; +import org.springframework.scripting.Calculator +class GroovyCalculator implements Calculator { + int add(int x, int y) { + return x + y; + } +} + + + + + + + + + + + + + + + + + + + + + + inline: +package org.springframework.scripting.groovy; +import org.springframework.scripting.Messenger +class GroovyMessenger implements Messenger { + def String message; +} +return new GroovyMessenger(); + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/scripting/groovy/groovyContextWithJsr223.xml b/spring-context/src/test/resources/org/springframework/scripting/groovy/groovyContextWithJsr223.xml new file mode 100644 index 0000000..3964609 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/groovy/groovyContextWithJsr223.xml @@ -0,0 +1,61 @@ + + + + + + + + + inline: +package org.springframework.scripting.groovy; +import org.springframework.scripting.Calculator +class GroovyCalculator implements Calculator { + int add(int x, int y) { + return x + y; + } +} + + + + + + + + + + + + + + + + + + + + + + + inline: +package org.springframework.scripting.groovy; +import org.springframework.scripting.Messenger +class GroovyMessenger implements Messenger { + def String message; +} +return new GroovyMessenger(); + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/scripting/groovy/groovyRefreshableContext.xml b/spring-context/src/test/resources/org/springframework/scripting/groovy/groovyRefreshableContext.xml new file mode 100644 index 0000000..1728779 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/groovy/groovyRefreshableContext.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/scripting/groovy/lwspBadGroovyContext.xml b/spring-context/src/test/resources/org/springframework/scripting/groovy/lwspBadGroovyContext.xml new file mode 100644 index 0000000..d935ae0 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/groovy/lwspBadGroovyContext.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + inline: + + class Bingo { + + @Property String message; + } + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/scripting/groovy/simple.groovy b/spring-context/src/test/resources/org/springframework/scripting/groovy/simple.groovy new file mode 100644 index 0000000..b999147 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/groovy/simple.groovy @@ -0,0 +1,3 @@ +package org.springframework.scripting.groovy; + +return 3 * 2 diff --git a/spring-context/src/test/resources/org/springframework/scripting/groovy/twoClassesCorrectOneFirst.xml b/spring-context/src/test/resources/org/springframework/scripting/groovy/twoClassesCorrectOneFirst.xml new file mode 100644 index 0000000..8aee2de --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/groovy/twoClassesCorrectOneFirst.xml @@ -0,0 +1,28 @@ + + + + + + + + + inline: + package org.springframework.scripting.groovy; + + import org.springframework.scripting.Messenger; + + class GroovyMessenger implements Messenger { + + def String message; + } + + class Bingo { + + def String message; + } + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/scripting/groovy/twoClassesWrongOneFirst.xml b/spring-context/src/test/resources/org/springframework/scripting/groovy/twoClassesWrongOneFirst.xml new file mode 100644 index 0000000..5115838 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/groovy/twoClassesWrongOneFirst.xml @@ -0,0 +1,28 @@ + + + + + + + + + inline: + package org.springframework.scripting.groovy; + + import org.springframework.scripting.Messenger; + + class Bingo { + + @Property String message; + } + + class GroovyMessenger implements Messenger { + + @Property String message; + } + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/scripting/support/Messenger.js b/spring-context/src/test/resources/org/springframework/scripting/support/Messenger.js new file mode 100644 index 0000000..5277c3c --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/support/Messenger.js @@ -0,0 +1 @@ +function getMessage() { return "Hello World!" } diff --git a/spring-context/src/test/resources/org/springframework/scripting/support/groovyReferences.xml b/spring-context/src/test/resources/org/springframework/scripting/support/groovyReferences.xml new file mode 100644 index 0000000..10b56e6 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/support/groovyReferences.xml @@ -0,0 +1,32 @@ + + + + + + + + + + inline:package org.springframework.scripting; + +import org.springframework.scripting.Messenger + +class DelegatingMessenger implements Messenger { + + private Messenger wrappedMessenger; + + public String getMessage() { + this.wrappedMessenger.getMessage(); + } + + public void setMessenger(Messenger wrappedMessenger) { + this.wrappedMessenger = wrappedMessenger; + } +} + + + + + + + diff --git a/spring-context/src/test/resources/org/springframework/scripting/support/jsr223-with-xsd.xml b/spring-context/src/test/resources/org/springframework/scripting/support/jsr223-with-xsd.xml new file mode 100644 index 0000000..3346aa0 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/scripting/support/jsr223-with-xsd.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + function getMessage() { return "Hello World!" } + + + + diff --git a/spring-context/src/test/resources/org/springframework/validation/messages1.properties b/spring-context/src/test/resources/org/springframework/validation/messages1.properties new file mode 100644 index 0000000..2b2adb2 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/validation/messages1.properties @@ -0,0 +1 @@ +typeMismatch=Field {0} did not have correct type diff --git a/spring-context/src/test/resources/org/springframework/validation/messages2.properties b/spring-context/src/test/resources/org/springframework/validation/messages2.properties new file mode 100644 index 0000000..69f0191 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/validation/messages2.properties @@ -0,0 +1,2 @@ +typeMismatch=Field {0} did not have correct type +age=Age diff --git a/spring-context/src/test/resources/org/springframework/validation/messages3.properties b/spring-context/src/test/resources/org/springframework/validation/messages3.properties new file mode 100644 index 0000000..7b82992 --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/validation/messages3.properties @@ -0,0 +1,2 @@ +typeMismatch=Field {0} did not have correct type +person.age=Person Age diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/AbstractApplicationContextTests.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/AbstractApplicationContextTests.java new file mode 100644 index 0000000..e3ba64f --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/AbstractApplicationContextTests.java @@ -0,0 +1,203 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.testfixture; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.Locale; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.testfixture.beans.LifecycleBean; +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.beans.testfixture.factory.xml.AbstractListableBeanFactoryTests; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.NoSuchMessageException; +import org.springframework.context.testfixture.beans.ACATester; +import org.springframework.context.testfixture.beans.BeanThatListens; +import org.springframework.context.testfixture.beans.TestApplicationListener; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Rod Johnson + * @author Juergen Hoeller + * @author Sam Brannen + */ +public abstract class AbstractApplicationContextTests extends AbstractListableBeanFactoryTests { + + /** Must be supplied as XML */ + public static final String TEST_NAMESPACE = "testNamespace"; + + protected ConfigurableApplicationContext applicationContext; + + /** Subclass must register this */ + protected TestApplicationListener listener = new TestApplicationListener(); + + protected TestApplicationListener parentListener = new TestApplicationListener(); + + @BeforeEach + public void setUp() throws Exception { + this.applicationContext = createContext(); + } + + @Override + protected BeanFactory getBeanFactory() { + return applicationContext; + } + + protected ApplicationContext getApplicationContext() { + return applicationContext; + } + + /** + * Must register a TestListener. + * Must register standard beans. + * Parent must register rod with name Roderick + * and father with name Albert. + */ + protected abstract ConfigurableApplicationContext createContext() throws Exception; + + @Test + public void contextAwareSingletonWasCalledBack() throws Exception { + ACATester aca = (ACATester) applicationContext.getBean("aca"); + assertThat(aca.getApplicationContext() == applicationContext).as("has had context set").isTrue(); + Object aca2 = applicationContext.getBean("aca"); + assertThat(aca == aca2).as("Same instance").isTrue(); + assertThat(applicationContext.isSingleton("aca")).as("Says is singleton").isTrue(); + } + + @Test + public void contextAwarePrototypeWasCalledBack() throws Exception { + ACATester aca = (ACATester) applicationContext.getBean("aca-prototype"); + assertThat(aca.getApplicationContext() == applicationContext).as("has had context set").isTrue(); + Object aca2 = applicationContext.getBean("aca-prototype"); + assertThat(aca != aca2).as("NOT Same instance").isTrue(); + boolean condition = !applicationContext.isSingleton("aca-prototype"); + assertThat(condition).as("Says is prototype").isTrue(); + } + + @Test + public void parentNonNull() { + assertThat(applicationContext.getParent() != null).as("parent isn't null").isTrue(); + } + + @Test + public void grandparentNull() { + assertThat(applicationContext.getParent().getParent() == null).as("grandparent is null").isTrue(); + } + + @Test + public void overrideWorked() throws Exception { + TestBean rod = (TestBean) applicationContext.getParent().getBean("rod"); + assertThat(rod.getName().equals("Roderick")).as("Parent's name differs").isTrue(); + } + + @Test + public void grandparentDefinitionFound() throws Exception { + TestBean dad = (TestBean) applicationContext.getBean("father"); + assertThat(dad.getName().equals("Albert")).as("Dad has correct name").isTrue(); + } + + @Test + public void grandparentTypedDefinitionFound() throws Exception { + TestBean dad = applicationContext.getBean("father", TestBean.class); + assertThat(dad.getName().equals("Albert")).as("Dad has correct name").isTrue(); + } + + @Test + public void closeTriggersDestroy() { + LifecycleBean lb = (LifecycleBean) applicationContext.getBean("lifecycle"); + boolean condition = !lb.isDestroyed(); + assertThat(condition).as("Not destroyed").isTrue(); + applicationContext.close(); + if (applicationContext.getParent() != null) { + ((ConfigurableApplicationContext) applicationContext.getParent()).close(); + } + assertThat(lb.isDestroyed()).as("Destroyed").isTrue(); + applicationContext.close(); + if (applicationContext.getParent() != null) { + ((ConfigurableApplicationContext) applicationContext.getParent()).close(); + } + assertThat(lb.isDestroyed()).as("Destroyed").isTrue(); + } + + @Test + public void messageSource() throws NoSuchMessageException { + assertThat(applicationContext.getMessage("code1", null, Locale.getDefault())).isEqualTo("message1"); + assertThat(applicationContext.getMessage("code2", null, Locale.getDefault())).isEqualTo("message2"); + assertThatExceptionOfType(NoSuchMessageException.class).isThrownBy(() -> + applicationContext.getMessage("code0", null, Locale.getDefault())); + } + + @Test + public void events() throws Exception { + doTestEvents(this.listener, this.parentListener, new MyEvent(this)); + } + + @Test + public void eventsWithNoSource() throws Exception { + // See SPR-10945 Serialized events result in a null source + MyEvent event = new MyEvent(this); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(bos); + oos.writeObject(event); + oos.close(); + event = (MyEvent) new ObjectInputStream(new ByteArrayInputStream( + bos.toByteArray())).readObject(); + doTestEvents(this.listener, this.parentListener, event); + } + + protected void doTestEvents(TestApplicationListener listener, TestApplicationListener parentListener, + MyEvent event) { + listener.zeroCounter(); + parentListener.zeroCounter(); + assertThat(listener.getEventCount() == 0).as("0 events before publication").isTrue(); + assertThat(parentListener.getEventCount() == 0).as("0 parent events before publication").isTrue(); + this.applicationContext.publishEvent(event); + assertThat(listener.getEventCount() == 1).as("1 events after publication, not " + listener.getEventCount()).isTrue(); + assertThat(parentListener.getEventCount() == 1).as("1 parent events after publication").isTrue(); + } + + @Test + public void beanAutomaticallyHearsEvents() throws Exception { + //String[] listenerNames = ((ListableBeanFactory) applicationContext).getBeanDefinitionNames(ApplicationListener.class); + //assertTrue("listeners include beanThatListens", Arrays.asList(listenerNames).contains("beanThatListens")); + BeanThatListens b = (BeanThatListens) applicationContext.getBean("beanThatListens"); + b.zero(); + assertThat(b.getEventCount() == 0).as("0 events before publication").isTrue(); + this.applicationContext.publishEvent(new MyEvent(this)); + assertThat(b.getEventCount() == 1).as("1 events after publication, not " + b.getEventCount()).isTrue(); + } + + + @SuppressWarnings("serial") + public static class MyEvent extends ApplicationEvent { + + public MyEvent(Object source) { + super(source); + } + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/SimpleMapScope.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/SimpleMapScope.java new file mode 100644 index 0000000..3b29b78 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/SimpleMapScope.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.testfixture; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.config.Scope; + +/** + * @author Juergen Hoeller + */ +@SuppressWarnings("serial") +public class SimpleMapScope implements Scope, Serializable { + + private final Map map = new HashMap<>(); + + private final List callbacks = new ArrayList<>(); + + + public SimpleMapScope() { + } + + public final Map getMap() { + return this.map; + } + + + @Override + public Object get(String name, ObjectFactory objectFactory) { + synchronized (this.map) { + Object scopedObject = this.map.get(name); + if (scopedObject == null) { + scopedObject = objectFactory.getObject(); + this.map.put(name, scopedObject); + } + return scopedObject; + } + } + + @Override + public Object remove(String name) { + synchronized (this.map) { + return this.map.remove(name); + } + } + + @Override + public void registerDestructionCallback(String name, Runnable callback) { + this.callbacks.add(callback); + } + + @Override + public Object resolveContextualObject(String key) { + return null; + } + + public void close() { + for (Iterator it = this.callbacks.iterator(); it.hasNext();) { + Runnable runnable = it.next(); + runnable.run(); + } + } + + @Override + public String getConversationId() { + return null; + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/beans/ACATester.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/beans/ACATester.java new file mode 100644 index 0000000..4d0fbd5 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/beans/ACATester.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.testfixture.beans; + +import java.util.Locale; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationContextException; +import org.springframework.context.NoSuchMessageException; + +public class ACATester implements ApplicationContextAware { + + private ApplicationContext ac; + + @Override + public void setApplicationContext(ApplicationContext ctx) throws ApplicationContextException { + // check re-initialization + if (this.ac != null) { + throw new IllegalStateException("Already initialized"); + } + + // check message source availability + if (ctx != null) { + try { + ctx.getMessage("code1", null, Locale.getDefault()); + } + catch (NoSuchMessageException ex) { + // expected + } + } + + this.ac = ctx; + } + + public ApplicationContext getApplicationContext() { + return ac; + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/beans/BeanThatBroadcasts.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/beans/BeanThatBroadcasts.java new file mode 100644 index 0000000..cc8e138 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/beans/BeanThatBroadcasts.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.testfixture.beans; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +/** + * @author Juergen Hoeller + */ +public class BeanThatBroadcasts implements ApplicationContextAware { + + public ApplicationContext applicationContext; + + public int receivedCount; + + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + if (applicationContext.getDisplayName().contains("listener")) { + applicationContext.getBean("listener"); + } + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/beans/BeanThatListens.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/beans/BeanThatListens.java new file mode 100644 index 0000000..f54e74c --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/beans/BeanThatListens.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.testfixture.beans; + +import java.util.Map; + +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; + +/** + * A stub {@link ApplicationListener}. + * + * @author Thomas Risberg + * @author Juergen Hoeller + */ +public class BeanThatListens implements ApplicationListener { + + private BeanThatBroadcasts beanThatBroadcasts; + + private int eventCount; + + + public BeanThatListens() { + } + + public BeanThatListens(BeanThatBroadcasts beanThatBroadcasts) { + this.beanThatBroadcasts = beanThatBroadcasts; + Map beans = beanThatBroadcasts.applicationContext.getBeansOfType(BeanThatListens.class); + if (!beans.isEmpty()) { + throw new IllegalStateException("Shouldn't have found any BeanThatListens instances"); + } + } + + + @Override + public void onApplicationEvent(ApplicationEvent event) { + eventCount++; + if (beanThatBroadcasts != null) { + beanThatBroadcasts.receivedCount++; + } + } + + public int getEventCount() { + return eventCount; + } + + public void zero() { + eventCount = 0; + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/beans/TestApplicationListener.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/beans/TestApplicationListener.java new file mode 100644 index 0000000..63749d3 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/beans/TestApplicationListener.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.testfixture.beans; + +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; + +/** + * Listener that maintains a global count of events. + * + * @author Rod Johnson + * @since January 21, 2001 + */ +public class TestApplicationListener implements ApplicationListener { + + private int eventCount; + + public int getEventCount() { + return eventCount; + } + + public void zeroCounter() { + eventCount = 0; + } + + @Override + public void onApplicationEvent(ApplicationEvent e) { + ++eventCount; + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/AbstractCacheAnnotationTests.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/AbstractCacheAnnotationTests.java new file mode 100644 index 0000000..66c3a66 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/AbstractCacheAnnotationTests.java @@ -0,0 +1,877 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.testfixture.cache; + +import java.util.Collection; +import java.util.UUID; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.AopProxyUtils; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.testfixture.cache.beans.AnnotatedClassCacheableService; +import org.springframework.context.testfixture.cache.beans.CacheableService; +import org.springframework.context.testfixture.cache.beans.TestEntity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIOException; + +/** + * Abstract cache annotation tests (containing several reusable methods). + * + * @author Costin Leau + * @author Chris Beams + * @author Phillip Webb + * @author Stephane Nicoll + */ +public abstract class AbstractCacheAnnotationTests { + + protected ConfigurableApplicationContext ctx; + + protected CacheableService cs; + + protected CacheableService ccs; + + protected CacheManager cm; + + + /** + * @return a refreshed application context + */ + protected abstract ConfigurableApplicationContext getApplicationContext(); + + + @BeforeEach + public void setup() { + this.ctx = getApplicationContext(); + this.cs = ctx.getBean("service", CacheableService.class); + this.ccs = ctx.getBean("classService", CacheableService.class); + this.cm = ctx.getBean("cacheManager", CacheManager.class); + + Collection cn = this.cm.getCacheNames(); + assertThat(cn).containsOnly("testCache", "secondary", "primary"); + } + + @AfterEach + public void close() { + if (this.ctx != null) { + this.ctx.close(); + } + } + + + protected void testCacheable(CacheableService service) { + Object o1 = new Object(); + + Object r1 = service.cache(o1); + Object r2 = service.cache(o1); + Object r3 = service.cache(o1); + + assertThat(r2).isSameAs(r1); + assertThat(r3).isSameAs(r1); + } + + protected void testCacheableNull(CacheableService service) { + Object o1 = new Object(); + assertThat(this.cm.getCache("testCache").get(o1)).isNull(); + + Object r1 = service.cacheNull(o1); + Object r2 = service.cacheNull(o1); + Object r3 = service.cacheNull(o1); + + assertThat(r2).isSameAs(r1); + assertThat(r3).isSameAs(r1); + + assertThat(this.cm.getCache("testCache")).as("testCache").isNotNull(); + assertThat(this.cm.getCache("testCache").get(o1)).as("cached object").isNotNull(); + assertThat(this.cm.getCache("testCache").get(o1).get()).isEqualTo(r3); + assertThat(r3).as("Cached value should be null").isNull(); + } + + protected void testCacheableSync(CacheableService service) { + Object o1 = new Object(); + + Object r1 = service.cacheSync(o1); + Object r2 = service.cacheSync(o1); + Object r3 = service.cacheSync(o1); + + assertThat(r2).isSameAs(r1); + assertThat(r3).isSameAs(r1); + } + + protected void testCacheableSyncNull(CacheableService service) { + Object o1 = new Object(); + assertThat(this.cm.getCache("testCache").get(o1)).isNull(); + + Object r1 = service.cacheSyncNull(o1); + Object r2 = service.cacheSyncNull(o1); + Object r3 = service.cacheSyncNull(o1); + + assertThat(r2).isSameAs(r1); + assertThat(r3).isSameAs(r1); + + assertThat(this.cm.getCache("testCache").get(o1).get()).isEqualTo(r3); + assertThat(r3).as("Cached value should be null").isNull(); + } + + protected void testEvict(CacheableService service, boolean successExpected) { + Cache cache = this.cm.getCache("testCache"); + + Object o1 = new Object(); + cache.putIfAbsent(o1, -1L); + Object r1 = service.cache(o1); + + service.evict(o1, null); + if (successExpected) { + assertThat(cache.get(o1)).isNull(); + } + else { + assertThat(cache.get(o1)).isNotNull(); + } + + Object r2 = service.cache(o1); + if (successExpected) { + assertThat(r2).isNotSameAs(r1); + } + else { + assertThat(r2).isSameAs(r1); + } + } + + protected void testEvictEarly(CacheableService service) { + Cache cache = this.cm.getCache("testCache"); + + Object o1 = new Object(); + cache.putIfAbsent(o1, -1L); + Object r1 = service.cache(o1); + + try { + service.evictEarly(o1); + } + catch (RuntimeException ex) { + // expected + } + assertThat(cache.get(o1)).isNull(); + + Object r2 = service.cache(o1); + assertThat(r2).isNotSameAs(r1); + } + + protected void testEvictException(CacheableService service) { + Object o1 = new Object(); + Object r1 = service.cache(o1); + + try { + service.evictWithException(o1); + } + catch (RuntimeException ex) { + // expected + } + // exception occurred, eviction skipped, data should still be in the cache + Object r2 = service.cache(o1); + assertThat(r2).isSameAs(r1); + } + + protected void testEvictWithKey(CacheableService service) { + Object o1 = new Object(); + Object r1 = service.cache(o1); + + service.evict(o1, null); + Object r2 = service.cache(o1); + assertThat(r2).isNotSameAs(r1); + } + + protected void testEvictWithKeyEarly(CacheableService service) { + Object o1 = new Object(); + Object r1 = service.cache(o1); + + try { + service.evictEarly(o1); + } + catch (Exception ex) { + // expected + } + Object r2 = service.cache(o1); + assertThat(r2).isNotSameAs(r1); + } + + protected void testEvictAll(CacheableService service, boolean successExpected) { + Cache cache = this.cm.getCache("testCache"); + + Object o1 = new Object(); + Object o2 = new Object(); + cache.putIfAbsent(o1, -1L); + cache.putIfAbsent(o2, -2L); + + Object r1 = service.cache(o1); + Object r2 = service.cache(o2); + assertThat(r2).isNotSameAs(r1); + + service.evictAll(new Object()); + if (successExpected) { + assertThat(cache.get(o1)).isNull(); + assertThat(cache.get(o2)).isNull(); + } + else { + assertThat(cache.get(o1)).isNotNull(); + assertThat(cache.get(o2)).isNotNull(); + } + + Object r3 = service.cache(o1); + Object r4 = service.cache(o2); + if (successExpected) { + assertThat(r3).isNotSameAs(r1); + assertThat(r4).isNotSameAs(r2); + } + else { + assertThat(r3).isSameAs(r1); + assertThat(r4).isSameAs(r2); + } + } + + protected void testEvictAllEarly(CacheableService service) { + Cache cache = this.cm.getCache("testCache"); + + Object o1 = new Object(); + Object o2 = new Object(); + cache.putIfAbsent(o1, -1L); + cache.putIfAbsent(o2, -2L); + + Object r1 = service.cache(o1); + Object r2 = service.cache(o2); + assertThat(r2).isNotSameAs(r1); + + try { + service.evictAllEarly(new Object()); + } + catch (Exception ex) { + // expected + } + assertThat(cache.get(o1)).isNull(); + assertThat(cache.get(o2)).isNull(); + + Object r3 = service.cache(o1); + Object r4 = service.cache(o2); + assertThat(r3).isNotSameAs(r1); + assertThat(r4).isNotSameAs(r2); + } + + protected void testConditionalExpression(CacheableService service) { + Object r1 = service.conditional(4); + Object r2 = service.conditional(4); + + assertThat(r2).isNotSameAs(r1); + + Object r3 = service.conditional(3); + Object r4 = service.conditional(3); + + assertThat(r4).isSameAs(r3); + } + + protected void testConditionalExpressionSync(CacheableService service) { + Object r1 = service.conditionalSync(4); + Object r2 = service.conditionalSync(4); + + assertThat(r2).isNotSameAs(r1); + + Object r3 = service.conditionalSync(3); + Object r4 = service.conditionalSync(3); + + assertThat(r4).isSameAs(r3); + } + + protected void testUnlessExpression(CacheableService service) { + Cache cache = this.cm.getCache("testCache"); + cache.clear(); + service.unless(10); + service.unless(11); + assertThat(cache.get(10).get()).isEqualTo(10L); + assertThat(cache.get(11)).isNull(); + } + + protected void testKeyExpression(CacheableService service) { + Object r1 = service.key(5, 1); + Object r2 = service.key(5, 2); + + assertThat(r2).isSameAs(r1); + + Object r3 = service.key(1, 5); + Object r4 = service.key(2, 5); + + assertThat(r4).isNotSameAs(r3); + } + + protected void testVarArgsKey(CacheableService service) { + Object r1 = service.varArgsKey(1, 2, 3); + Object r2 = service.varArgsKey(1, 2, 3); + + assertThat(r2).isSameAs(r1); + + Object r3 = service.varArgsKey(1, 2, 3); + Object r4 = service.varArgsKey(1, 2); + + assertThat(r4).isNotSameAs(r3); + } + + protected void testNullValue(CacheableService service) { + Object key = new Object(); + assertThat(service.nullValue(key)).isNull(); + int nr = service.nullInvocations().intValue(); + assertThat(service.nullValue(key)).isNull(); + assertThat(service.nullInvocations().intValue()).isEqualTo(nr); + assertThat(service.nullValue(new Object())).isNull(); + assertThat(service.nullInvocations().intValue()).isEqualTo(nr + 1); + } + + protected void testMethodName(CacheableService service, String keyName) { + Object key = new Object(); + Object r1 = service.name(key); + assertThat(service.name(key)).isSameAs(r1); + Cache cache = this.cm.getCache("testCache"); + // assert the method name is used + assertThat(cache.get(keyName)).isNotNull(); + } + + protected void testRootVars(CacheableService service) { + Object key = new Object(); + Object r1 = service.rootVars(key); + assertThat(service.rootVars(key)).isSameAs(r1); + Cache cache = this.cm.getCache("testCache"); + // assert the method name is used + String expectedKey = "rootVarsrootVars" + AopProxyUtils.ultimateTargetClass(service) + service; + assertThat(cache.get(expectedKey)).isNotNull(); + } + + protected void testCheckedThrowable(CacheableService service) { + String arg = UUID.randomUUID().toString(); + assertThatIOException().isThrownBy(() -> + service.throwChecked(arg)) + .withMessage(arg); + } + + protected void testUncheckedThrowable(CacheableService service) { + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + service.throwUnchecked(1L)) + .withMessage("1"); + } + + protected void testCheckedThrowableSync(CacheableService service) { + String arg = UUID.randomUUID().toString(); + assertThatIOException().isThrownBy(() -> + service.throwCheckedSync(arg)) + .withMessage(arg); + } + + protected void testUncheckedThrowableSync(CacheableService service) { + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + service.throwUncheckedSync(1L)) + .withMessage("1"); + } + + protected void testNullArg(CacheableService service) { + Object r1 = service.cache(null); + assertThat(service.cache(null)).isSameAs(r1); + } + + protected void testCacheUpdate(CacheableService service) { + Object o = new Object(); + Cache cache = this.cm.getCache("testCache"); + assertThat(cache.get(o)).isNull(); + Object r1 = service.update(o); + assertThat(cache.get(o).get()).isSameAs(r1); + + o = new Object(); + assertThat(cache.get(o)).isNull(); + Object r2 = service.update(o); + assertThat(cache.get(o).get()).isSameAs(r2); + } + + protected void testConditionalCacheUpdate(CacheableService service) { + int one = 1; + int three = 3; + + Cache cache = this.cm.getCache("testCache"); + assertThat(Integer.parseInt(service.conditionalUpdate(one).toString())).isEqualTo(one); + assertThat(cache.get(one)).isNull(); + + assertThat(Integer.parseInt(service.conditionalUpdate(three).toString())).isEqualTo(three); + assertThat(Integer.parseInt(cache.get(three).get().toString())).isEqualTo(three); + } + + protected void testMultiCache(CacheableService service) { + Object o1 = new Object(); + Object o2 = new Object(); + + Cache primary = this.cm.getCache("primary"); + Cache secondary = this.cm.getCache("secondary"); + + assertThat(primary.get(o1)).isNull(); + assertThat(secondary.get(o1)).isNull(); + Object r1 = service.multiCache(o1); + assertThat(primary.get(o1).get()).isSameAs(r1); + assertThat(secondary.get(o1).get()).isSameAs(r1); + + Object r2 = service.multiCache(o1); + Object r3 = service.multiCache(o1); + + assertThat(r2).isSameAs(r1); + assertThat(r3).isSameAs(r1); + + assertThat(primary.get(o2)).isNull(); + assertThat(secondary.get(o2)).isNull(); + Object r4 = service.multiCache(o2); + assertThat(primary.get(o2).get()).isSameAs(r4); + assertThat(secondary.get(o2).get()).isSameAs(r4); + } + + protected void testMultiEvict(CacheableService service) { + Object o1 = new Object(); + Object o2 = o1.toString() + "A"; + + + Object r1 = service.multiCache(o1); + Object r2 = service.multiCache(o1); + + Cache primary = this.cm.getCache("primary"); + Cache secondary = this.cm.getCache("secondary"); + + primary.put(o2, o2); + assertThat(r2).isSameAs(r1); + assertThat(primary.get(o1).get()).isSameAs(r1); + assertThat(secondary.get(o1).get()).isSameAs(r1); + + service.multiEvict(o1); + assertThat(primary.get(o1)).isNull(); + assertThat(secondary.get(o1)).isNull(); + assertThat(primary.get(o2)).isNull(); + + Object r3 = service.multiCache(o1); + Object r4 = service.multiCache(o1); + assertThat(r3).isNotSameAs(r1); + assertThat(r4).isSameAs(r3); + + assertThat(primary.get(o1).get()).isSameAs(r3); + assertThat(secondary.get(o1).get()).isSameAs(r4); + } + + protected void testMultiPut(CacheableService service) { + Object o = 1; + + Cache primary = this.cm.getCache("primary"); + Cache secondary = this.cm.getCache("secondary"); + + assertThat(primary.get(o)).isNull(); + assertThat(secondary.get(o)).isNull(); + Object r1 = service.multiUpdate(o); + assertThat(primary.get(o).get()).isSameAs(r1); + assertThat(secondary.get(o).get()).isSameAs(r1); + + o = 2; + assertThat(primary.get(o)).isNull(); + assertThat(secondary.get(o)).isNull(); + Object r2 = service.multiUpdate(o); + assertThat(primary.get(o).get()).isSameAs(r2); + assertThat(secondary.get(o).get()).isSameAs(r2); + } + + protected void testPutRefersToResult(CacheableService service) { + Long id = Long.MIN_VALUE; + TestEntity entity = new TestEntity(); + Cache primary = this.cm.getCache("primary"); + assertThat(primary.get(id)).isNull(); + assertThat(entity.getId()).isNull(); + service.putRefersToResult(entity); + assertThat(primary.get(id).get()).isSameAs(entity); + } + + protected void testMultiCacheAndEvict(CacheableService service) { + String methodName = "multiCacheAndEvict"; + + Cache primary = this.cm.getCache("primary"); + Cache secondary = this.cm.getCache("secondary"); + Object key = 1; + + secondary.put(key, key); + + assertThat(secondary.get(methodName)).isNull(); + assertThat(secondary.get(key).get()).isSameAs(key); + + Object r1 = service.multiCacheAndEvict(key); + assertThat(service.multiCacheAndEvict(key)).isSameAs(r1); + + // assert the method name is used + assertThat(primary.get(methodName).get()).isSameAs(r1); + assertThat(secondary.get(methodName)).isNull(); + assertThat(secondary.get(key)).isNull(); + } + + protected void testMultiConditionalCacheAndEvict(CacheableService service) { + Cache primary = this.cm.getCache("primary"); + Cache secondary = this.cm.getCache("secondary"); + Object key = 1; + + secondary.put(key, key); + + assertThat(primary.get(key)).isNull(); + assertThat(secondary.get(key).get()).isSameAs(key); + + Object r1 = service.multiConditionalCacheAndEvict(key); + Object r3 = service.multiConditionalCacheAndEvict(key); + + assertThat(!r1.equals(r3)).isTrue(); + assertThat(primary.get(key)).isNull(); + + Object key2 = 3; + Object r2 = service.multiConditionalCacheAndEvict(key2); + assertThat(service.multiConditionalCacheAndEvict(key2)).isSameAs(r2); + + // assert the method name is used + assertThat(primary.get(key2).get()).isSameAs(r2); + assertThat(secondary.get(key2)).isNull(); + } + + @Test + public void testCacheable() { + testCacheable(this.cs); + } + + @Test + public void testCacheableNull() { + testCacheableNull(this.cs); + } + + @Test + public void testCacheableSync() { + testCacheableSync(this.cs); + } + + @Test + public void testCacheableSyncNull() { + testCacheableSyncNull(this.cs); + } + + @Test + public void testEvict() { + testEvict(this.cs, true); + } + + @Test + public void testEvictEarly() { + testEvictEarly(this.cs); + } + + @Test + public void testEvictWithException() { + testEvictException(this.cs); + } + + @Test + public void testEvictAll() { + testEvictAll(this.cs, true); + } + + @Test + public void testEvictAllEarly() { + testEvictAllEarly(this.cs); + } + + @Test + public void testEvictWithKey() { + testEvictWithKey(this.cs); + } + + @Test + public void testEvictWithKeyEarly() { + testEvictWithKeyEarly(this.cs); + } + + @Test + public void testConditionalExpression() { + testConditionalExpression(this.cs); + } + + @Test + public void testConditionalExpressionSync() { + testConditionalExpressionSync(this.cs); + } + + @Test + public void testUnlessExpression() { + testUnlessExpression(this.cs); + } + + @Test + public void testClassCacheUnlessExpression() { + testUnlessExpression(this.cs); + } + + @Test + public void testKeyExpression() { + testKeyExpression(this.cs); + } + + @Test + public void testVarArgsKey() { + testVarArgsKey(this.cs); + } + + @Test + public void testClassCacheCacheable() { + testCacheable(this.ccs); + } + + @Test + public void testClassCacheEvict() { + testEvict(this.ccs, true); + } + + @Test + public void testClassEvictEarly() { + testEvictEarly(this.ccs); + } + + @Test + public void testClassEvictAll() { + testEvictAll(this.ccs, true); + } + + @Test + public void testClassEvictWithException() { + testEvictException(this.ccs); + } + + @Test + public void testClassCacheEvictWithWKey() { + testEvictWithKey(this.ccs); + } + + @Test + public void testClassEvictWithKeyEarly() { + testEvictWithKeyEarly(this.ccs); + } + + @Test + public void testNullValue() { + testNullValue(this.cs); + } + + @Test + public void testClassNullValue() { + Object key = new Object(); + assertThat(this.ccs.nullValue(key)).isNull(); + int nr = this.ccs.nullInvocations().intValue(); + assertThat(this.ccs.nullValue(key)).isNull(); + assertThat(this.ccs.nullInvocations().intValue()).isEqualTo(nr); + assertThat(this.ccs.nullValue(new Object())).isNull(); + // the check method is also cached + assertThat(this.ccs.nullInvocations().intValue()).isEqualTo(nr); + assertThat(AnnotatedClassCacheableService.nullInvocations.intValue()).isEqualTo(nr + 1); + } + + @Test + public void testMethodName() { + testMethodName(this.cs, "name"); + } + + @Test + public void testClassMethodName() { + testMethodName(this.ccs, "nametestCache"); + } + + @Test + public void testRootVars() { + testRootVars(this.cs); + } + + @Test + public void testClassRootVars() { + testRootVars(this.ccs); + } + + @Test + public void testCustomKeyGenerator() { + Object param = new Object(); + Object r1 = this.cs.customKeyGenerator(param); + assertThat(this.cs.customKeyGenerator(param)).isSameAs(r1); + Cache cache = this.cm.getCache("testCache"); + // Checks that the custom keyGenerator was used + Object expectedKey = SomeCustomKeyGenerator.generateKey("customKeyGenerator", param); + assertThat(cache.get(expectedKey)).isNotNull(); + } + + @Test + public void testUnknownCustomKeyGenerator() { + Object param = new Object(); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> + this.cs.unknownCustomKeyGenerator(param)); + } + + @Test + public void testCustomCacheManager() { + CacheManager customCm = this.ctx.getBean("customCacheManager", CacheManager.class); + Object key = new Object(); + Object r1 = this.cs.customCacheManager(key); + assertThat(this.cs.customCacheManager(key)).isSameAs(r1); + + Cache cache = customCm.getCache("testCache"); + assertThat(cache.get(key)).isNotNull(); + } + + @Test + public void testUnknownCustomCacheManager() { + Object param = new Object(); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> + this.cs.unknownCustomCacheManager(param)); + } + + @Test + public void testNullArg() { + testNullArg(this.cs); + } + + @Test + public void testClassNullArg() { + testNullArg(this.ccs); + } + + @Test + public void testCheckedException() { + testCheckedThrowable(this.cs); + } + + @Test + public void testClassCheckedException() { + testCheckedThrowable(this.ccs); + } + + @Test + public void testCheckedExceptionSync() { + testCheckedThrowableSync(this.cs); + } + + @Test + public void testClassCheckedExceptionSync() { + testCheckedThrowableSync(this.ccs); + } + + @Test + public void testUncheckedException() { + testUncheckedThrowable(this.cs); + } + + @Test + public void testClassUncheckedException() { + testUncheckedThrowable(this.ccs); + } + + @Test + public void testUncheckedExceptionSync() { + testUncheckedThrowableSync(this.cs); + } + + @Test + public void testClassUncheckedExceptionSync() { + testUncheckedThrowableSync(this.ccs); + } + + @Test + public void testUpdate() { + testCacheUpdate(this.cs); + } + + @Test + public void testClassUpdate() { + testCacheUpdate(this.ccs); + } + + @Test + public void testConditionalUpdate() { + testConditionalCacheUpdate(this.cs); + } + + @Test + public void testClassConditionalUpdate() { + testConditionalCacheUpdate(this.ccs); + } + + @Test + public void testMultiCache() { + testMultiCache(this.cs); + } + + @Test + public void testClassMultiCache() { + testMultiCache(this.ccs); + } + + @Test + public void testMultiEvict() { + testMultiEvict(this.cs); + } + + @Test + public void testClassMultiEvict() { + testMultiEvict(this.ccs); + } + + @Test + public void testMultiPut() { + testMultiPut(this.cs); + } + + @Test + public void testClassMultiPut() { + testMultiPut(this.ccs); + } + + @Test + public void testPutRefersToResult() { + testPutRefersToResult(this.cs); + } + + @Test + public void testClassPutRefersToResult() { + testPutRefersToResult(this.ccs); + } + + @Test + public void testMultiCacheAndEvict() { + testMultiCacheAndEvict(this.cs); + } + + @Test + public void testClassMultiCacheAndEvict() { + testMultiCacheAndEvict(this.ccs); + } + + @Test + public void testMultiConditionalCacheAndEvict() { + testMultiConditionalCacheAndEvict(this.cs); + } + + @Test + public void testClassMultiConditionalCacheAndEvict() { + testMultiConditionalCacheAndEvict(this.ccs); + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/AbstractCacheTests.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/AbstractCacheTests.java new file mode 100644 index 0000000..e010744 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/AbstractCacheTests.java @@ -0,0 +1,215 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.testfixture.cache; + +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; + +import org.springframework.cache.Cache; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Stephane Nicoll + */ +public abstract class AbstractCacheTests { + + protected final static String CACHE_NAME = "testCache"; + + protected abstract T getCache(); + + protected abstract Object getNativeCache(); + + + @Test + public void testCacheName() throws Exception { + assertThat(getCache().getName()).isEqualTo(CACHE_NAME); + } + + @Test + public void testNativeCache() throws Exception { + assertThat(getCache().getNativeCache()).isSameAs(getNativeCache()); + } + + @Test + public void testCachePut() throws Exception { + T cache = getCache(); + + String key = createRandomKey(); + Object value = "george"; + + assertThat((Object) cache.get(key)).isNull(); + assertThat(cache.get(key, String.class)).isNull(); + assertThat(cache.get(key, Object.class)).isNull(); + + cache.put(key, value); + assertThat(cache.get(key).get()).isEqualTo(value); + assertThat(cache.get(key, String.class)).isEqualTo(value); + assertThat(cache.get(key, Object.class)).isEqualTo(value); + assertThat(cache.get(key, (Class) null)).isEqualTo(value); + + cache.put(key, null); + assertThat(cache.get(key)).isNotNull(); + assertThat(cache.get(key).get()).isNull(); + assertThat(cache.get(key, String.class)).isNull(); + assertThat(cache.get(key, Object.class)).isNull(); + } + + @Test + public void testCachePutIfAbsent() throws Exception { + T cache = getCache(); + + String key = createRandomKey(); + Object value = "initialValue"; + + assertThat(cache.get(key)).isNull(); + assertThat(cache.putIfAbsent(key, value)).isNull(); + assertThat(cache.get(key).get()).isEqualTo(value); + assertThat(cache.putIfAbsent(key, "anotherValue").get()).isEqualTo("initialValue"); + // not changed + assertThat(cache.get(key).get()).isEqualTo(value); + } + + @Test + public void testCacheRemove() throws Exception { + T cache = getCache(); + + String key = createRandomKey(); + Object value = "george"; + + assertThat((Object) cache.get(key)).isNull(); + cache.put(key, value); + } + + @Test + public void testCacheClear() throws Exception { + T cache = getCache(); + + assertThat((Object) cache.get("enescu")).isNull(); + cache.put("enescu", "george"); + assertThat((Object) cache.get("vlaicu")).isNull(); + cache.put("vlaicu", "aurel"); + cache.clear(); + assertThat((Object) cache.get("vlaicu")).isNull(); + assertThat((Object) cache.get("enescu")).isNull(); + } + + @Test + public void testCacheGetCallable() { + doTestCacheGetCallable("test"); + } + + @Test + public void testCacheGetCallableWithNull() { + doTestCacheGetCallable(null); + } + + private void doTestCacheGetCallable(Object returnValue) { + T cache = getCache(); + + String key = createRandomKey(); + + assertThat((Object) cache.get(key)).isNull(); + Object value = cache.get(key, () -> returnValue); + assertThat(value).isEqualTo(returnValue); + assertThat(cache.get(key).get()).isEqualTo(value); + } + + @Test + public void testCacheGetCallableNotInvokedWithHit() { + doTestCacheGetCallableNotInvokedWithHit("existing"); + } + + @Test + public void testCacheGetCallableNotInvokedWithHitNull() { + doTestCacheGetCallableNotInvokedWithHit(null); + } + + private void doTestCacheGetCallableNotInvokedWithHit(Object initialValue) { + T cache = getCache(); + + String key = createRandomKey(); + cache.put(key, initialValue); + + Object value = cache.get(key, () -> { + throw new IllegalStateException("Should not have been invoked"); + }); + assertThat(value).isEqualTo(initialValue); + } + + @Test + public void testCacheGetCallableFail() { + T cache = getCache(); + + String key = createRandomKey(); + assertThat((Object) cache.get(key)).isNull(); + + try { + cache.get(key, () -> { + throw new UnsupportedOperationException("Expected exception"); + }); + } + catch (Cache.ValueRetrievalException ex) { + assertThat(ex.getCause()).isNotNull(); + assertThat(ex.getCause().getClass()).isEqualTo(UnsupportedOperationException.class); + } + } + + /** + * Test that a call to get with a Callable concurrently properly synchronize the + * invocations. + */ + @Test + public void testCacheGetSynchronized() throws InterruptedException { + T cache = getCache(); + final AtomicInteger counter = new AtomicInteger(); + final List results = new CopyOnWriteArrayList<>(); + final CountDownLatch latch = new CountDownLatch(10); + + String key = createRandomKey(); + Runnable run = () -> { + try { + Integer value = cache.get(key, () -> { + Thread.sleep(50); // make sure the thread will overlap + return counter.incrementAndGet(); + }); + results.add(value); + } + finally { + latch.countDown(); + } + }; + + for (int i = 0; i < 10; i++) { + new Thread(run).start(); + } + latch.await(); + + assertThat(results.size()).isEqualTo(10); + results.forEach(r -> assertThat(r).isEqualTo(1)); // Only one method got invoked + } + + protected String createRandomKey() { + return UUID.randomUUID().toString(); + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/AbstractValueAdaptingCacheTests.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/AbstractValueAdaptingCacheTests.java new file mode 100644 index 0000000..5f0809d --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/AbstractValueAdaptingCacheTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.testfixture.cache; + +import org.junit.jupiter.api.Test; + +import org.springframework.cache.support.AbstractValueAdaptingCache; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Stephane Nicoll + */ +public abstract class AbstractValueAdaptingCacheTests + extends AbstractCacheTests { + + protected final static String CACHE_NAME_NO_NULL = "testCacheNoNull"; + + protected abstract T getCache(boolean allowNull); + + @Test + public void testCachePutNullValueAllowNullFalse() { + T cache = getCache(false); + String key = createRandomKey(); + assertThatIllegalArgumentException().isThrownBy(() -> + cache.put(key, null)) + .withMessageContaining(CACHE_NAME_NO_NULL) + .withMessageContaining("is configured to not allow null values but null was provided"); + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/CacheTestUtils.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/CacheTestUtils.java new file mode 100644 index 0000000..459c3bf --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/CacheTestUtils.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.testfixture.cache; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.concurrent.ConcurrentMapCache; +import org.springframework.cache.support.SimpleCacheManager; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * General cache-related test utilities. + * + * @author Stephane Nicoll + */ +public class CacheTestUtils { + + /** + * Create a {@link SimpleCacheManager} with the specified cache(s). + * @param cacheNames the names of the caches to create + */ + public static CacheManager createSimpleCacheManager(String... cacheNames) { + SimpleCacheManager result = new SimpleCacheManager(); + List caches = new ArrayList<>(); + for (String cacheName : cacheNames) { + caches.add(new ConcurrentMapCache(cacheName)); + } + result.setCaches(caches); + result.afterPropertiesSet(); + return result; + } + + + /** + * Assert the following key is not held within the specified cache(s). + */ + public static void assertCacheMiss(Object key, Cache... caches) { + for (Cache cache : caches) { + assertThat(cache.get(key)).as("No entry in " + cache + " should have been found with key " + key).isNull(); + } + } + + /** + * Assert the following key has a matching value within the specified cache(s). + */ + public static void assertCacheHit(Object key, Object value, Cache... caches) { + for (Cache cache : caches) { + Cache.ValueWrapper wrapper = cache.get(key); + assertThat(wrapper).as("An entry in " + cache + " should have been found with key " + key).isNotNull(); + assertThat(wrapper.get()).as("Wrong value in " + cache + " for entry with key " + key).isEqualTo(value); + } + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/SomeCustomKeyGenerator.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/SomeCustomKeyGenerator.java new file mode 100644 index 0000000..c9c4763 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/SomeCustomKeyGenerator.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.testfixture.cache; + +import java.lang.reflect.Method; + +import org.springframework.cache.interceptor.KeyGenerator; + +/** + * A custom {@link KeyGenerator} that exposes the algorithm used to compute the key + * for convenience in test scenarios. + * + * @author Stephane Nicoll + */ +public class SomeCustomKeyGenerator implements KeyGenerator { + + @Override + public Object generate(Object target, Method method, Object... params) { + return generateKey(method.getName(), params); + } + + /** + * @see #generate(Object, java.lang.reflect.Method, Object...) + */ + public static Object generateKey(String methodName, Object... params) { + final StringBuilder sb = new StringBuilder(methodName); + for (Object param : params) { + sb.append(param); + } + return sb.toString(); + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/SomeKeyGenerator.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/SomeKeyGenerator.java new file mode 100644 index 0000000..791d9f7 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/SomeKeyGenerator.java @@ -0,0 +1,22 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.testfixture.cache; + +import org.springframework.cache.interceptor.SimpleKeyGenerator; + +public class SomeKeyGenerator extends SimpleKeyGenerator { +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/AnnotatedClassCacheableService.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/AnnotatedClassCacheableService.java new file mode 100644 index 0000000..aaf8fb6 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/AnnotatedClassCacheableService.java @@ -0,0 +1,238 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.testfixture.cache.beans; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicLong; + +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.annotation.Caching; + +/** + * @author Costin Leau + * @author Phillip Webb + * @author Stephane Nicoll + */ +@Cacheable("testCache") +public class AnnotatedClassCacheableService implements CacheableService { + + private final AtomicLong counter = new AtomicLong(); + + public static final AtomicLong nullInvocations = new AtomicLong(); + + + @Override + public Object cache(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + public Object cacheNull(Object arg1) { + return null; + } + + @Override + @Cacheable(cacheNames = "testCache", sync = true) + public Object cacheSync(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", sync = true) + public Object cacheSyncNull(Object arg1) { + return null; + } + + @Override + public Object conditional(int field) { + return null; + } + + @Override + public Object conditionalSync(int field) { + return null; + } + + @Override + @Cacheable(cacheNames = "testCache", unless = "#result > 10") + public Object unless(int arg) { + return arg; + } + + @Override + @CacheEvict(cacheNames = "testCache", key = "#p0") + public void evict(Object arg1, Object arg2) { + } + + @Override + @CacheEvict("testCache") + public void evictWithException(Object arg1) { + throw new RuntimeException("exception thrown - evict should NOT occur"); + } + + @Override + @CacheEvict(cacheNames = "testCache", beforeInvocation = true) + public void evictEarly(Object arg1) { + throw new RuntimeException("exception thrown - evict should still occur"); + } + + @Override + @CacheEvict(cacheNames = "testCache", allEntries = true) + public void evictAll(Object arg1) { + } + + @Override + @CacheEvict(cacheNames = "testCache", allEntries = true, beforeInvocation = true) + public void evictAllEarly(Object arg1) { + throw new RuntimeException("exception thrown - evict should still occur"); + } + + @Override + @Cacheable(cacheNames = "testCache", key = "#p0") + public Object key(Object arg1, Object arg2) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable("testCache") + public Object varArgsKey(Object... args) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", key = "#root.methodName + #root.caches[0].name") + public Object name(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", key = "#root.methodName + #root.method.name + #root.targetClass + #root.target") + public Object rootVars(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", keyGenerator = "customKyeGenerator") + public Object customKeyGenerator(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", keyGenerator = "unknownBeanName") + public Object unknownCustomKeyGenerator(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", cacheManager = "customCacheManager") + public Object customCacheManager(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", cacheManager = "unknownBeanName") + public Object unknownCustomCacheManager(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @CachePut("testCache") + public Object update(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @CachePut(cacheNames = "testCache", condition = "#arg.equals(3)") + public Object conditionalUpdate(Object arg) { + return arg; + } + + @Override + public Object nullValue(Object arg1) { + nullInvocations.incrementAndGet(); + return null; + } + + @Override + public Number nullInvocations() { + return nullInvocations.get(); + } + + @Override + public Long throwChecked(Object arg1) throws Exception { + throw new IOException(arg1.toString()); + } + + @Override + public Long throwUnchecked(Object arg1) { + throw new UnsupportedOperationException(arg1.toString()); + } + + @Override + @Cacheable(cacheNames = "testCache", sync = true) + public Object throwCheckedSync(Object arg1) throws Exception { + throw new IOException(arg1.toString()); + } + + @Override + @Cacheable(cacheNames = "testCache", sync = true) + public Object throwUncheckedSync(Object arg1) { + throw new UnsupportedOperationException(arg1.toString()); + } + + // multi annotations + + @Override + @Caching(cacheable = { @Cacheable("primary"), @Cacheable("secondary") }) + public Object multiCache(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames = "secondary", key = "#a0"), @CacheEvict(cacheNames = "primary", key = "#p0 + 'A'") }) + public Object multiEvict(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Caching(cacheable = { @Cacheable(cacheNames = "primary", key = "#root.methodName") }, evict = { @CacheEvict("secondary") }) + public Object multiCacheAndEvict(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Caching(cacheable = { @Cacheable(cacheNames = "primary", condition = "#a0 == 3") }, evict = { @CacheEvict("secondary") }) + public Object multiConditionalCacheAndEvict(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Caching(put = { @CachePut("primary"), @CachePut("secondary") }) + public Object multiUpdate(Object arg1) { + return arg1; + } + + @Override + @CachePut(cacheNames = "primary", key = "#result.id") + public TestEntity putRefersToResult(TestEntity arg1) { + arg1.setId(Long.MIN_VALUE); + return arg1; + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/CacheableService.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/CacheableService.java new file mode 100644 index 0000000..06afcea --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/CacheableService.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.testfixture.cache.beans; + +/** + * Basic service interface for caching tests. + * + * @author Costin Leau + * @author Phillip Webb + * @author Stephane Nicoll + */ +public interface CacheableService { + + T cache(Object arg1); + + T cacheNull(Object arg1); + + T cacheSync(Object arg1); + + T cacheSyncNull(Object arg1); + + void evict(Object arg1, Object arg2); + + void evictWithException(Object arg1); + + void evictEarly(Object arg1); + + void evictAll(Object arg1); + + void evictAllEarly(Object arg1); + + T conditional(int field); + + T conditionalSync(int field); + + T unless(int arg); + + T key(Object arg1, Object arg2); + + T varArgsKey(Object... args); + + T name(Object arg1); + + T nullValue(Object arg1); + + T update(Object arg1); + + T conditionalUpdate(Object arg2); + + Number nullInvocations(); + + T rootVars(Object arg1); + + T customKeyGenerator(Object arg1); + + T unknownCustomKeyGenerator(Object arg1); + + T customCacheManager(Object arg1); + + T unknownCustomCacheManager(Object arg1); + + T throwChecked(Object arg1) throws Exception; + + T throwUnchecked(Object arg1); + + T throwCheckedSync(Object arg1) throws Exception; + + T throwUncheckedSync(Object arg1); + + T multiCache(Object arg1); + + T multiEvict(Object arg1); + + T multiCacheAndEvict(Object arg1); + + T multiConditionalCacheAndEvict(Object arg1); + + T multiUpdate(Object arg1); + + TestEntity putRefersToResult(TestEntity arg1); + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/DefaultCacheableService.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/DefaultCacheableService.java new file mode 100644 index 0000000..017a719 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/DefaultCacheableService.java @@ -0,0 +1,246 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.testfixture.cache.beans; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicLong; + +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.annotation.Caching; + +/** + * Simple cacheable service. + * + * @author Costin Leau + * @author Phillip Webb + * @author Stephane Nicoll + */ +public class DefaultCacheableService implements CacheableService { + + private final AtomicLong counter = new AtomicLong(); + + private final AtomicLong nullInvocations = new AtomicLong(); + + + @Override + @Cacheable("testCache") + public Long cache(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable("testCache") + public Long cacheNull(Object arg1) { + return null; + } + + @Override + @Cacheable(cacheNames = "testCache", sync = true) + public Long cacheSync(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", sync = true) + public Long cacheSyncNull(Object arg1) { + return null; + } + + @Override + @CacheEvict(cacheNames = "testCache", key = "#p0") + public void evict(Object arg1, Object arg2) { + } + + @Override + @CacheEvict("testCache") + public void evictWithException(Object arg1) { + throw new RuntimeException("exception thrown - evict should NOT occur"); + } + + @Override + @CacheEvict(cacheNames = "testCache", beforeInvocation = true) + public void evictEarly(Object arg1) { + throw new RuntimeException("exception thrown - evict should still occur"); + } + + @Override + @CacheEvict(cacheNames = "testCache", allEntries = true) + public void evictAll(Object arg1) { + } + + @Override + @CacheEvict(cacheNames = "testCache", allEntries = true, beforeInvocation = true) + public void evictAllEarly(Object arg1) { + throw new RuntimeException("exception thrown - evict should still occur"); + } + + @Override + @Cacheable(cacheNames = "testCache", condition = "#p0 == 3") + public Long conditional(int classField) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", sync = true, condition = "#p0 == 3") + public Long conditionalSync(int classField) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", unless = "#result > 10") + public Long unless(int arg) { + return (long) arg; + } + + @Override + @Cacheable(cacheNames = "testCache", key = "#p0") + public Long key(Object arg1, Object arg2) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache") + public Long varArgsKey(Object... args) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", key = "#root.methodName") + public Long name(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", key = "#root.methodName + #root.method.name + #root.targetClass + #root.target") + public Long rootVars(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", keyGenerator = "customKeyGenerator") + public Long customKeyGenerator(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", keyGenerator = "unknownBeanName") + public Long unknownCustomKeyGenerator(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", cacheManager = "customCacheManager") + public Long customCacheManager(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Cacheable(cacheNames = "testCache", cacheManager = "unknownBeanName") + public Long unknownCustomCacheManager(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @CachePut("testCache") + public Long update(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @CachePut(cacheNames = "testCache", condition = "#arg.equals(3)") + public Long conditionalUpdate(Object arg) { + return Long.valueOf(arg.toString()); + } + + @Override + @Cacheable("testCache") + public Long nullValue(Object arg1) { + this.nullInvocations.incrementAndGet(); + return null; + } + + @Override + public Number nullInvocations() { + return this.nullInvocations.get(); + } + + @Override + @Cacheable("testCache") + public Long throwChecked(Object arg1) throws Exception { + throw new IOException(arg1.toString()); + } + + @Override + @Cacheable("testCache") + public Long throwUnchecked(Object arg1) { + throw new UnsupportedOperationException(arg1.toString()); + } + + @Override + @Cacheable(cacheNames = "testCache", sync = true) + public Long throwCheckedSync(Object arg1) throws Exception { + throw new IOException(arg1.toString()); + } + + @Override + @Cacheable(cacheNames = "testCache", sync = true) + public Long throwUncheckedSync(Object arg1) { + throw new UnsupportedOperationException(arg1.toString()); + } + + // multi annotations + + @Override + @Caching(cacheable = { @Cacheable("primary"), @Cacheable("secondary") }) + public Long multiCache(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames = "secondary", key = "#p0"), @CacheEvict(cacheNames = "primary", key = "#p0 + 'A'") }) + public Long multiEvict(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Caching(cacheable = { @Cacheable(cacheNames = "primary", key = "#root.methodName") }, evict = { @CacheEvict("secondary") }) + public Long multiCacheAndEvict(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Caching(cacheable = { @Cacheable(cacheNames = "primary", condition = "#p0 == 3") }, evict = { @CacheEvict("secondary") }) + public Long multiConditionalCacheAndEvict(Object arg1) { + return this.counter.getAndIncrement(); + } + + @Override + @Caching(put = { @CachePut("primary"), @CachePut("secondary") }) + public Long multiUpdate(Object arg1) { + return Long.valueOf(arg1.toString()); + } + + @Override + @CachePut(cacheNames = "primary", key = "#result.id") + public TestEntity putRefersToResult(TestEntity arg1) { + arg1.setId(Long.MIN_VALUE); + return arg1; + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/TestEntity.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/TestEntity.java new file mode 100644 index 0000000..77c9e06 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/TestEntity.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.testfixture.cache.beans; + +import org.springframework.util.ObjectUtils; + +/** + * Simple test entity for use with caching tests. + * + * @author Michael Plod + */ +public class TestEntity { + + private Long id; + + public Long getId() { + return this.id; + } + + public void setId(Long id) { + this.id = id; + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.id); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null) { + return false; + } + if (obj instanceof TestEntity) { + return ObjectUtils.nullSafeEquals(this.id, ((TestEntity) obj).id); + } + return false; + } +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/index/CandidateComponentsTestClassLoader.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/index/CandidateComponentsTestClassLoader.java new file mode 100644 index 0000000..7b6ad75 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/index/CandidateComponentsTestClassLoader.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.testfixture.index; + +import java.io.IOException; +import java.net.URL; +import java.util.Collections; +import java.util.Enumeration; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.context.index.CandidateComponentsIndexLoader; +import org.springframework.core.io.Resource; + +/** + * A test {@link ClassLoader} that can be used in a testing context to control the + * {@code spring.components} resource that should be loaded. Can also simulate a failure + * by throwing a configurable {@link IOException}. + * + * @author Stephane Nicoll + */ +public class CandidateComponentsTestClassLoader extends ClassLoader { + + /** + * Create a test {@link ClassLoader} that disable the use of the index, even + * if resources are present at the standard location. + * @param classLoader the classloader to use for all other operations + * @return a test {@link ClassLoader} that has no index + * @see CandidateComponentsIndexLoader#COMPONENTS_RESOURCE_LOCATION + */ + public static ClassLoader disableIndex(ClassLoader classLoader) { + return new CandidateComponentsTestClassLoader(classLoader, + Collections.enumeration(Collections.emptyList())); + } + + /** + * Create a test {@link ClassLoader} that creates an index with the + * specified {@link Resource} instances + * @param classLoader the classloader to use for all other operations + * @return a test {@link ClassLoader} with an index built based on the + * specified resources. + */ + public static ClassLoader index(ClassLoader classLoader, Resource... resources) { + return new CandidateComponentsTestClassLoader(classLoader, + Collections.enumeration(Stream.of(resources).map(r -> { + try { + return r.getURL(); + } + catch (Exception ex) { + throw new IllegalArgumentException("Invalid resource " + r, ex); + } + }).collect(Collectors.toList()))); + } + + + private final Enumeration resourceUrls; + + private final IOException cause; + + public CandidateComponentsTestClassLoader(ClassLoader classLoader, Enumeration resourceUrls) { + super(classLoader); + this.resourceUrls = resourceUrls; + this.cause = null; + } + + public CandidateComponentsTestClassLoader(ClassLoader parent, IOException cause) { + super(parent); + this.resourceUrls = null; + this.cause = cause; + } + + @Override + public Enumeration getResources(String name) throws IOException { + if (CandidateComponentsIndexLoader.COMPONENTS_RESOURCE_LOCATION.equals(name)) { + if (this.resourceUrls != null) { + return this.resourceUrls; + } + throw this.cause; + } + return super.getResources(name); + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/jndi/ExpectedLookupTemplate.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/jndi/ExpectedLookupTemplate.java new file mode 100644 index 0000000..007f1e5 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/jndi/ExpectedLookupTemplate.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.testfixture.jndi; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.naming.NamingException; + +import org.springframework.jndi.JndiTemplate; + +/** + * Copy of the standard {@link org.springframework.context.testfixture.jndi.jndi.ExpectedLookupTemplate} + * for testing purposes. + * + *

    Simple extension of the JndiTemplate class that always returns a given object. + * + *

    Very useful for testing. Effectively a mock object. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +public class ExpectedLookupTemplate extends JndiTemplate { + + private final Map jndiObjects = new ConcurrentHashMap<>(16); + + + /** + * Construct a new JndiTemplate that will always return given objects for + * given names. To be populated through {@code addObject} calls. + * @see #addObject(String, Object) + */ + public ExpectedLookupTemplate() { + } + + /** + * Construct a new JndiTemplate that will always return the given object, + * but honour only requests for the given name. + * @param name the name the client is expected to look up + * @param object the object that will be returned + */ + public ExpectedLookupTemplate(String name, Object object) { + addObject(name, object); + } + + + /** + * Add the given object to the list of JNDI objects that this template will expose. + * @param name the name the client is expected to look up + * @param object the object that will be returned + */ + public void addObject(String name, Object object) { + this.jndiObjects.put(name, object); + } + + /** + * If the name is the expected name specified in the constructor, return the + * object provided in the constructor. If the name is unexpected, a + * respective NamingException gets thrown. + */ + @Override + public Object lookup(String name) throws NamingException { + Object object = this.jndiObjects.get(name); + if (object == null) { + throw new NamingException("Unexpected JNDI name '" + name + "': expecting " + this.jndiObjects.keySet()); + } + return object; + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/jndi/SimpleNamingContext.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/jndi/SimpleNamingContext.java new file mode 100644 index 0000000..92b0e3f --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/jndi/SimpleNamingContext.java @@ -0,0 +1,389 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.testfixture.jndi; + +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.Map; + +import javax.naming.Binding; +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.NameClassPair; +import javax.naming.NameNotFoundException; +import javax.naming.NameParser; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.OperationNotSupportedException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Copy of the standard {@link org.springframework.mock.jndi.SimpleNamingContext} + * for testing purposes. + * + *

    Simple implementation of a JNDI naming context. + * Only supports binding plain Objects to String names. + * Mainly for test environments, but also usable for standalone applications. + * + *

    This class is not intended for direct usage by applications, although it + * can be used for example to override JndiTemplate's {@code createInitialContext} + * method in unit tests. Typically, SimpleNamingContextBuilder will be used to + * set up a JVM-level JNDI environment. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see SimpleNamingContextBuilder + * @see org.springframework.jndi.JndiTemplate#createInitialContext + */ +public class SimpleNamingContext implements Context { + + private final Log logger = LogFactory.getLog(getClass()); + + private final String root; + + private final Hashtable boundObjects; + + private final Hashtable environment = new Hashtable<>(); + + + /** + * Create a new naming context. + */ + public SimpleNamingContext() { + this(""); + } + + /** + * Create a new naming context with the given naming root. + */ + public SimpleNamingContext(String root) { + this.root = root; + this.boundObjects = new Hashtable<>(); + } + + /** + * Create a new naming context with the given naming root, + * the given name/object map, and the JNDI environment entries. + */ + public SimpleNamingContext( + String root, Hashtable boundObjects, @Nullable Hashtable env) { + + this.root = root; + this.boundObjects = boundObjects; + if (env != null) { + this.environment.putAll(env); + } + } + + + // Actual implementations of Context methods follow + + @Override + public NamingEnumeration list(String root) throws NamingException { + if (logger.isDebugEnabled()) { + logger.debug("Listing name/class pairs under [" + root + "]"); + } + return new NameClassPairEnumeration(this, root); + } + + @Override + public NamingEnumeration listBindings(String root) throws NamingException { + if (logger.isDebugEnabled()) { + logger.debug("Listing bindings under [" + root + "]"); + } + return new BindingEnumeration(this, root); + } + + /** + * Look up the object with the given name. + *

    Note: Not intended for direct use by applications. + * Will be used by any standard InitialContext JNDI lookups. + * @throws javax.naming.NameNotFoundException if the object could not be found + */ + @Override + public Object lookup(String lookupName) throws NameNotFoundException { + String name = this.root + lookupName; + if (logger.isDebugEnabled()) { + logger.debug("Static JNDI lookup: [" + name + "]"); + } + if (name.isEmpty()) { + return new SimpleNamingContext(this.root, this.boundObjects, this.environment); + } + Object found = this.boundObjects.get(name); + if (found == null) { + if (!name.endsWith("/")) { + name = name + "/"; + } + for (String boundName : this.boundObjects.keySet()) { + if (boundName.startsWith(name)) { + return new SimpleNamingContext(name, this.boundObjects, this.environment); + } + } + throw new NameNotFoundException( + "Name [" + this.root + lookupName + "] not bound; " + this.boundObjects.size() + " bindings: [" + + StringUtils.collectionToDelimitedString(this.boundObjects.keySet(), ",") + "]"); + } + return found; + } + + @Override + public Object lookupLink(String name) throws NameNotFoundException { + return lookup(name); + } + + /** + * Bind the given object to the given name. + * Note: Not intended for direct use by applications + * if setting up a JVM-level JNDI environment. + * Use SimpleNamingContextBuilder to set up JNDI bindings then. + * @see org.springframework.context.testfixture.jndi.SimpleNamingContextBuilder#bind + */ + @Override + public void bind(String name, Object obj) { + if (logger.isInfoEnabled()) { + logger.info("Static JNDI binding: [" + this.root + name + "] = [" + obj + "]"); + } + this.boundObjects.put(this.root + name, obj); + } + + @Override + public void unbind(String name) { + if (logger.isInfoEnabled()) { + logger.info("Static JNDI remove: [" + this.root + name + "]"); + } + this.boundObjects.remove(this.root + name); + } + + @Override + public void rebind(String name, Object obj) { + bind(name, obj); + } + + @Override + public void rename(String oldName, String newName) throws NameNotFoundException { + Object obj = lookup(oldName); + unbind(oldName); + bind(newName, obj); + } + + @Override + public Context createSubcontext(String name) { + String subcontextName = this.root + name; + if (!subcontextName.endsWith("/")) { + subcontextName += "/"; + } + Context subcontext = new SimpleNamingContext(subcontextName, this.boundObjects, this.environment); + bind(name, subcontext); + return subcontext; + } + + @Override + public void destroySubcontext(String name) { + unbind(name); + } + + @Override + public String composeName(String name, String prefix) { + return prefix + name; + } + + @Override + public Hashtable getEnvironment() { + return this.environment; + } + + @Override + @Nullable + public Object addToEnvironment(String propName, Object propVal) { + return this.environment.put(propName, propVal); + } + + @Override + public Object removeFromEnvironment(String propName) { + return this.environment.remove(propName); + } + + @Override + public void close() { + } + + + // Unsupported methods follow: no support for javax.naming.Name + + @Override + public NamingEnumeration list(Name name) throws NamingException { + throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]"); + } + + @Override + public NamingEnumeration listBindings(Name name) throws NamingException { + throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]"); + } + + @Override + public Object lookup(Name name) throws NamingException { + throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]"); + } + + @Override + public Object lookupLink(Name name) throws NamingException { + throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]"); + } + + @Override + public void bind(Name name, Object obj) throws NamingException { + throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]"); + } + + @Override + public void unbind(Name name) throws NamingException { + throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]"); + } + + @Override + public void rebind(Name name, Object obj) throws NamingException { + throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]"); + } + + @Override + public void rename(Name oldName, Name newName) throws NamingException { + throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]"); + } + + @Override + public Context createSubcontext(Name name) throws NamingException { + throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]"); + } + + @Override + public void destroySubcontext(Name name) throws NamingException { + throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]"); + } + + @Override + public String getNameInNamespace() throws NamingException { + throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]"); + } + + @Override + public NameParser getNameParser(Name name) throws NamingException { + throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]"); + } + + @Override + public NameParser getNameParser(String name) throws NamingException { + throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]"); + } + + @Override + public Name composeName(Name name, Name prefix) throws NamingException { + throw new OperationNotSupportedException("SimpleNamingContext does not support [javax.naming.Name]"); + } + + + private abstract static class AbstractNamingEnumeration implements NamingEnumeration { + + private final Iterator iterator; + + private AbstractNamingEnumeration(SimpleNamingContext context, String proot) throws NamingException { + if (!proot.isEmpty() && !proot.endsWith("/")) { + proot = proot + "/"; + } + String root = context.root + proot; + Map contents = new HashMap<>(); + for (String boundName : context.boundObjects.keySet()) { + if (boundName.startsWith(root)) { + int startIndex = root.length(); + int endIndex = boundName.indexOf('/', startIndex); + String strippedName = + (endIndex != -1 ? boundName.substring(startIndex, endIndex) : boundName.substring(startIndex)); + if (!contents.containsKey(strippedName)) { + try { + contents.put(strippedName, createObject(strippedName, context.lookup(proot + strippedName))); + } + catch (NameNotFoundException ex) { + // cannot happen + } + } + } + } + if (contents.size() == 0) { + throw new NamingException("Invalid root: [" + context.root + proot + "]"); + } + this.iterator = contents.values().iterator(); + } + + protected abstract T createObject(String strippedName, Object obj); + + @Override + public boolean hasMore() { + return this.iterator.hasNext(); + } + + @Override + public T next() { + return this.iterator.next(); + } + + @Override + public boolean hasMoreElements() { + return this.iterator.hasNext(); + } + + @Override + public T nextElement() { + return this.iterator.next(); + } + + @Override + public void close() { + } + } + + + private static final class NameClassPairEnumeration extends AbstractNamingEnumeration { + + private NameClassPairEnumeration(SimpleNamingContext context, String root) throws NamingException { + super(context, root); + } + + @Override + protected NameClassPair createObject(String strippedName, Object obj) { + return new NameClassPair(strippedName, obj.getClass().getName()); + } + } + + + private static final class BindingEnumeration extends AbstractNamingEnumeration { + + private BindingEnumeration(SimpleNamingContext context, String root) throws NamingException { + super(context, root); + } + + @Override + protected Binding createObject(String strippedName, Object obj) { + return new Binding(strippedName, obj); + } + } + +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/jndi/SimpleNamingContextBuilder.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/jndi/SimpleNamingContextBuilder.java new file mode 100644 index 0000000..d686261 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/jndi/SimpleNamingContextBuilder.java @@ -0,0 +1,236 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.context.testfixture.jndi; + +import java.util.Hashtable; + +import javax.naming.Context; +import javax.naming.NamingException; +import javax.naming.spi.InitialContextFactory; +import javax.naming.spi.InitialContextFactoryBuilder; +import javax.naming.spi.NamingManager; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * Copy of the standard {@link org.springframework.mock.jndi.SimpleNamingContextBuilder} + * for testing purposes. + * + *

    Simple implementation of a JNDI naming context builder. + * + *

    Mainly targeted at test environments, where each test case can + * configure JNDI appropriately, so that {@code new InitialContext()} + * will expose the required objects. Also usable for standalone applications, + * e.g. for binding a JDBC DataSource to a well-known JNDI location, to be + * able to use traditional Java EE data access code outside of a Java EE + * container. + * + *

    There are various choices for DataSource implementations: + *

      + *
    • {@code SingleConnectionDataSource} (using the same Connection for all getConnection calls) + *
    • {@code DriverManagerDataSource} (creating a new Connection on each getConnection call) + *
    • Apache's Commons DBCP offers {@code org.apache.commons.dbcp.BasicDataSource} (a real pool) + *
    + * + *

    Typical usage in bootstrap code: + * + *

    + * SimpleNamingContextBuilder builder = new SimpleNamingContextBuilder();
    + * DataSource ds = new DriverManagerDataSource(...);
    + * builder.bind("java:comp/env/jdbc/myds", ds);
    + * builder.activate();
    + * + * Note that it's impossible to activate multiple builders within the same JVM, + * due to JNDI restrictions. Thus to configure a fresh builder repeatedly, use + * the following code to get a reference to either an already activated builder + * or a newly activated one: + * + *
    + * SimpleNamingContextBuilder builder = SimpleNamingContextBuilder.emptyActivatedContextBuilder();
    + * DataSource ds = new DriverManagerDataSource(...);
    + * builder.bind("java:comp/env/jdbc/myds", ds);
    + * + * Note that you should not call {@code activate()} on a builder from + * this factory method, as there will already be an activated one in any case. + * + *

    An instance of this class is only necessary at setup time. + * An application does not need to keep a reference to it after activation. + * + * @author Juergen Hoeller + * @author Rod Johnson + * @see #emptyActivatedContextBuilder() + * @see #bind(String, Object) + * @see #activate() + * @see SimpleNamingContext + * @see org.springframework.jdbc.datasource.SingleConnectionDataSource + * @see org.springframework.jdbc.datasource.DriverManagerDataSource + */ +public class SimpleNamingContextBuilder implements InitialContextFactoryBuilder { + + /** An instance of this class bound to JNDI. */ + @Nullable + private static volatile SimpleNamingContextBuilder activated; + + private static boolean initialized = false; + + private static final Object initializationLock = new Object(); + + + /** + * Checks if a SimpleNamingContextBuilder is active. + * @return the current SimpleNamingContextBuilder instance, + * or {@code null} if none + */ + @Nullable + public static SimpleNamingContextBuilder getCurrentContextBuilder() { + return activated; + } + + /** + * If no SimpleNamingContextBuilder is already configuring JNDI, + * create and activate one. Otherwise take the existing activated + * SimpleNamingContextBuilder, clear it and return it. + *

    This is mainly intended for test suites that want to + * reinitialize JNDI bindings from scratch repeatedly. + * @return an empty SimpleNamingContextBuilder that can be used + * to control JNDI bindings + */ + public static SimpleNamingContextBuilder emptyActivatedContextBuilder() throws NamingException { + SimpleNamingContextBuilder builder = activated; + if (builder != null) { + // Clear already activated context builder. + builder.clear(); + } + else { + // Create and activate new context builder. + builder = new SimpleNamingContextBuilder(); + // The activate() call will cause an assignment to the activated variable. + builder.activate(); + } + return builder; + } + + + private final Log logger = LogFactory.getLog(getClass()); + + private final Hashtable boundObjects = new Hashtable<>(); + + + /** + * Register the context builder by registering it with the JNDI NamingManager. + * Note that once this has been done, {@code new InitialContext()} will always + * return a context from this factory. Use the {@code emptyActivatedContextBuilder()} + * static method to get an empty context (for example, in test methods). + * @throws IllegalStateException if there's already a naming context builder + * registered with the JNDI NamingManager + */ + public void activate() throws IllegalStateException, NamingException { + logger.info("Activating simple JNDI environment"); + synchronized (initializationLock) { + if (!initialized) { + Assert.state(!NamingManager.hasInitialContextFactoryBuilder(), + "Cannot activate SimpleNamingContextBuilder: there is already a JNDI provider registered. " + + "Note that JNDI is a JVM-wide service, shared at the JVM system class loader level, " + + "with no reset option. As a consequence, a JNDI provider must only be registered once per JVM."); + NamingManager.setInitialContextFactoryBuilder(this); + initialized = true; + } + } + activated = this; + } + + /** + * Temporarily deactivate this context builder. It will remain registered with + * the JNDI NamingManager but will delegate to the standard JNDI InitialContextFactory + * (if configured) instead of exposing its own bound objects. + *

    Call {@code activate()} again in order to expose this context builder's own + * bound objects again. Such activate/deactivate sequences can be applied any number + * of times (e.g. within a larger integration test suite running in the same VM). + * @see #activate() + */ + public void deactivate() { + logger.info("Deactivating simple JNDI environment"); + activated = null; + } + + /** + * Clear all bindings in this context builder, while keeping it active. + */ + public void clear() { + this.boundObjects.clear(); + } + + /** + * Bind the given object under the given name, for all naming contexts + * that this context builder will generate. + * @param name the JNDI name of the object (e.g. "java:comp/env/jdbc/myds") + * @param obj the object to bind (e.g. a DataSource implementation) + */ + public void bind(String name, Object obj) { + if (logger.isInfoEnabled()) { + logger.info("Static JNDI binding: [" + name + "] = [" + obj + "]"); + } + this.boundObjects.put(name, obj); + } + + + /** + * Simple InitialContextFactoryBuilder implementation, + * creating a new SimpleNamingContext instance. + * @see SimpleNamingContext + */ + @Override + @SuppressWarnings("unchecked") + public InitialContextFactory createInitialContextFactory(@Nullable Hashtable environment) { + if (activated == null && environment != null) { + Object icf = environment.get(Context.INITIAL_CONTEXT_FACTORY); + if (icf != null) { + Class icfClass; + if (icf instanceof Class) { + icfClass = (Class) icf; + } + else if (icf instanceof String) { + icfClass = ClassUtils.resolveClassName((String) icf, getClass().getClassLoader()); + } + else { + throw new IllegalArgumentException("Invalid value type for environment key [" + + Context.INITIAL_CONTEXT_FACTORY + "]: " + icf.getClass().getName()); + } + if (!InitialContextFactory.class.isAssignableFrom(icfClass)) { + throw new IllegalArgumentException( + "Specified class does not implement [" + InitialContextFactory.class.getName() + "]: " + icf); + } + try { + return (InitialContextFactory) ReflectionUtils.accessibleConstructor(icfClass).newInstance(); + } + catch (Throwable ex) { + throw new IllegalStateException("Unable to instantiate specified InitialContextFactory: " + icf, ex); + } + } + } + + // Default case... + return env -> new SimpleNamingContext("", this.boundObjects, (Hashtable) env); + } + +} diff --git a/spring-core/kotlin-coroutines/kotlin-coroutines.gradle b/spring-core/kotlin-coroutines/kotlin-coroutines.gradle new file mode 100644 index 0000000..24efbb4 --- /dev/null +++ b/spring-core/kotlin-coroutines/kotlin-coroutines.gradle @@ -0,0 +1,35 @@ +description = "Spring Core Coroutines support" + +apply plugin: "kotlin" + +configurations { + classesOnlyElements { + canBeConsumed = true + canBeResolved = false + } +} + +artifacts { + classesOnlyElements(compileKotlin.destinationDir) +} + +dependencies { + compile("org.jetbrains.kotlin:kotlin-reflect") + compile("org.jetbrains.kotlin:kotlin-stdlib") + compile("io.projectreactor:reactor-core") + compile("org.jetbrains.kotlinx:kotlinx-coroutines-core") + compile("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") +} + +eclipse { + project { + buildCommand "org.jetbrains.kotlin.ui.kotlinBuilder" + buildCommand "org.eclipse.jdt.core.javabuilder" + natures "org.jetbrains.kotlin.core.kotlinNature" + natures "org.eclipse.jdt.core.javanature" + linkedResource name: "kotlin_bin", type: "2", locationUri: "org.jetbrains.kotlin.core.filesystem:/" + project.name + "/kotlin_bin" + } + classpath { + containers "org.jetbrains.kotlin.core.KOTLIN_CONTAINER" + } +} diff --git a/spring-core/kotlin-coroutines/src/main/kotlin/org/springframework/core/CoroutinesUtils.kt b/spring-core/kotlin-coroutines/src/main/kotlin/org/springframework/core/CoroutinesUtils.kt new file mode 100644 index 0000000..e62281d --- /dev/null +++ b/spring-core/kotlin-coroutines/src/main/kotlin/org/springframework/core/CoroutinesUtils.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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. + */ + +@file:JvmName("CoroutinesUtils") +package org.springframework.core + +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.reactive.awaitSingleOrNull +import kotlinx.coroutines.reactor.asFlux + +import kotlinx.coroutines.reactor.mono +import org.reactivestreams.Publisher +import reactor.core.publisher.Mono +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method +import kotlin.reflect.full.callSuspend +import kotlin.reflect.jvm.kotlinFunction + +/** + * Convert a [Deferred] instance to a [Mono] one. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +internal fun deferredToMono(source: Deferred) = + mono(Dispatchers.Unconfined) { source.await() } + +/** + * Convert a [Mono] instance to a [Deferred] one. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +internal fun monoToDeferred(source: Mono) = + GlobalScope.async(Dispatchers.Unconfined) { source.awaitSingleOrNull() } + +/** + * Invoke a suspending function and converts it to [Mono] or [reactor.core.publisher.Flux]. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +@Suppress("UNCHECKED_CAST") +fun invokeSuspendingFunction(method: Method, target: Any, vararg args: Any?): Publisher<*> { + val function = method.kotlinFunction!! + val mono = mono(Dispatchers.Unconfined) { + function.callSuspend(target, *args.sliceArray(0..(args.size-2))).let { if (it == Unit) null else it } + }.onErrorMap(InvocationTargetException::class.java) { it.targetException } + return if (function.returnType.classifier == Flow::class) { + mono.flatMapMany { (it as Flow).asFlux() } + } + else { + mono + } +} diff --git a/spring-core/spring-core.gradle b/spring-core/spring-core.gradle new file mode 100644 index 0000000..6c72e4d --- /dev/null +++ b/spring-core/spring-core.gradle @@ -0,0 +1,105 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +plugins { + id "com.github.johnrengelman.shadow" version "5.2.0" +} + +description = "Spring Core" + +apply plugin: "kotlin" + +// spring-core includes asm and repackages cglib, inlining both into the spring-core jar. +// cglib itself depends on asm and is therefore further transformed by the JarJar task to +// depend on org.springframework.asm; this avoids including two different copies of asm. +def cglibVersion = "3.3.0" +def objenesisVersion = "3.1" + +configurations { + cglib + objenesis + coroutines +} + +task cglibRepackJar(type: ShadowJar) { + archiveBaseName = 'spring-cglib-repack' + archiveVersion = cglibVersion + configurations = [project.configurations.cglib] + relocate 'net.sf.cglib', 'org.springframework.cglib' + relocate 'org.objectweb.asm', 'org.springframework.asm' +} + +task objenesisRepackJar(type: ShadowJar) { + archiveBaseName = 'spring-objenesis-repack' + archiveVersion = objenesisVersion + configurations = [project.configurations.objenesis] + relocate 'org.objenesis', 'org.springframework.objenesis' +} + +dependencies { + cglib("cglib:cglib:${cglibVersion}@jar") + objenesis("org.objenesis:objenesis:${objenesisVersion}@jar") + coroutines(project(path: ":kotlin-coroutines", configuration: 'classesOnlyElements')) + compile(files(cglibRepackJar)) + compile(files(objenesisRepackJar)) + compile(project(":spring-jcl")) + compileOnly(project(":kotlin-coroutines")) + compileOnly("io.projectreactor.tools:blockhound") + optional("net.sf.jopt-simple:jopt-simple") + optional("org.aspectj:aspectjweaver") + optional("org.jetbrains.kotlin:kotlin-reflect") + optional("org.jetbrains.kotlin:kotlin-stdlib") + optional("io.projectreactor:reactor-core") + optional("io.reactivex:rxjava") + optional("io.reactivex:rxjava-reactive-streams") + optional("io.reactivex.rxjava2:rxjava") + optional("io.reactivex.rxjava3:rxjava") + optional("io.netty:netty-buffer") + testCompile("io.projectreactor:reactor-test") + testCompile("com.google.code.findbugs:jsr305") + testCompile("javax.annotation:javax.annotation-api") + testCompile("javax.xml.bind:jaxb-api") + testCompile("com.fasterxml.woodstox:woodstox-core") + testCompile("org.xmlunit:xmlunit-assertj") + testCompile("org.xmlunit:xmlunit-matchers") + testCompile(project(":kotlin-coroutines")) + testCompile("io.projectreactor.tools:blockhound") + testFixturesImplementation("io.projectreactor:reactor-test") + testFixturesImplementation("com.google.code.findbugs:jsr305") + testFixturesImplementation("org.junit.platform:junit-platform-launcher") + testFixturesImplementation("org.junit.jupiter:junit-jupiter-api") + testFixturesImplementation("org.junit.jupiter:junit-jupiter-params") + testFixturesImplementation("org.assertj:assertj-core") + testFixturesImplementation("org.xmlunit:xmlunit-assertj") +} + +jar { + reproducibleFileOrder = true + preserveFileTimestamps = false // maybe not necessary here, but good for reproducibility + manifest.attributes["Dependencies"] = "jdk.unsupported" // JBoss modules + + // Inline repackaged cglib classes directly into spring-core jar + dependsOn cglibRepackJar + from(zipTree(cglibRepackJar.archivePath)) { + include "org/springframework/cglib/**" + exclude "org/springframework/cglib/core/AbstractClassGenerator*.class" + exclude "org/springframework/cglib/core/AsmApi*.class" + exclude "org/springframework/cglib/core/KeyFactory.class" + exclude "org/springframework/cglib/core/KeyFactory\$*.class" + exclude "org/springframework/cglib/core/ReflectUtils*.class" + exclude "org/springframework/cglib/proxy/Enhancer*.class" + exclude "org/springframework/cglib/proxy/MethodProxy*.class" + } + + dependsOn objenesisRepackJar + from(zipTree(objenesisRepackJar.archivePath)) { + include "org/springframework/objenesis/**" + } + + from configurations.coroutines +} + +test { + // Make sure the classes dir is used on the test classpath (required by ResourceTests) + // When test fixtures are involved, the JAR is used by default + classpath = sourceSets.main.output.classesDirs + classpath - files(jar.archiveFile) +} diff --git a/spring-core/src/jmh/java/org/springframework/core/codec/StringDecoderBenchmark.java b/spring-core/src/jmh/java/org/springframework/core/codec/StringDecoderBenchmark.java new file mode 100644 index 0000000..cbf36d0 --- /dev/null +++ b/spring-core/src/jmh/java/org/springframework/core/codec/StringDecoderBenchmark.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.infra.Blackhole; +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.util.MimeType; + +/** + * Benchmarks for {@link DataBufferUtils}. + * + * @author Rossen Stoyanchev + */ +@BenchmarkMode(Mode.Throughput) +public class StringDecoderBenchmark { + + @Benchmark + public void parseSseLines(SseLinesState state, Blackhole blackhole) { + blackhole.consume(state.parseLines().blockLast()); + } + + + @State(Scope.Benchmark) + @SuppressWarnings({"NotNullFieldNotInitialized", "ConstantConditions"}) + public static class SseLinesState { + + private static final Charset CHARSET = StandardCharsets.UTF_8; + + private static final ResolvableType ELEMENT_TYPE = ResolvableType.forClass(String.class); + + + @Param("10240") + int totalSize; + + @Param("2000") + int chunkSize; + + List chunks; + + StringDecoder decoder = StringDecoder.textPlainOnly(Arrays.asList("\r\n", "\n"), false); + + MimeType mimeType = new MimeType("text", "plain", CHARSET); + + + @Setup(Level.Trial) + public void setup() { + String eventTemplate = "id:$1\n" + + "event:some-event\n" + + ":some-comment-$1-aa\n" + + ":some-comment-$1-bb\n" + + "data:abcdefg-$1-hijklmnop-$1-qrstuvw-$1-xyz-$1\n\n"; + + int eventLength = String.format(eventTemplate, String.format("%05d", 1)).length(); + int eventCount = this.totalSize / eventLength; + DataBufferFactory bufferFactory = new DefaultDataBufferFactory(); + + this.chunks = Flux.range(1, eventCount) + .map(index -> String.format(eventTemplate, String.format("%05d", index))) + .buffer(this.chunkSize > eventLength ? this.chunkSize / eventLength : 1) + .map(strings -> String.join("", strings)) + .map(chunk -> { + byte[] bytes = chunk.getBytes(CHARSET); + DataBuffer buffer = bufferFactory.allocateBuffer(bytes.length); + buffer.write(bytes); + return buffer; + }) + .collectList() + .block(); + } + + public Flux parseLines() { + Flux input = Flux.fromIterable(this.chunks).doOnNext(DataBufferUtils::retain); + return this.decoder.decode(input, ELEMENT_TYPE, this.mimeType, Collections.emptyMap()); + } + } + +} diff --git a/spring-core/src/jmh/java/org/springframework/core/convert/support/GenericConversionServiceBenchmark.java b/spring-core/src/jmh/java/org/springframework/core/convert/support/GenericConversionServiceBenchmark.java new file mode 100644 index 0000000..b31e69d --- /dev/null +++ b/spring-core/src/jmh/java/org/springframework/core/convert/support/GenericConversionServiceBenchmark.java @@ -0,0 +1,116 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.infra.Blackhole; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.util.CollectionUtils; + +/** + * Benchmarks for {@link GenericConversionService}. + * + * @author Brian Clozel + */ +@BenchmarkMode(Mode.Throughput) +public class GenericConversionServiceBenchmark { + + @Benchmark + public void convertListOfStringToListOfIntegerWithConversionService(ListBenchmarkState state, Blackhole bh) { + TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(state.source); + bh.consume(state.conversionService.convert(state.source, sourceTypeDesc, state.targetTypeDesc)); + } + + @Benchmark + public void convertListOfStringToListOfIntegerBaseline(ListBenchmarkState state, Blackhole bh) { + List target = new ArrayList<>(state.source.size()); + for (String element : state.source) { + target.add(Integer.valueOf(element)); + } + bh.consume(target); + } + + @State(Scope.Benchmark) + public static class ListBenchmarkState extends BenchmarkState { + + List source; + + @Setup(Level.Trial) + public void setup() throws Exception { + this.source = IntStream.rangeClosed(1, collectionSize).mapToObj(String::valueOf).collect(Collectors.toList()); + List target = new ArrayList<>(); + this.targetTypeDesc = TypeDescriptor.forObject(target); + } + } + + @Benchmark + public void convertMapOfStringToListOfIntegerWithConversionService(MapBenchmarkState state, Blackhole bh) { + TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(state.source); + bh.consume(state.conversionService.convert(state.source, sourceTypeDesc, state.targetTypeDesc)); + } + + @Benchmark + public void convertMapOfStringToListOfIntegerBaseline(MapBenchmarkState state, Blackhole bh) { + Map target = CollectionUtils.newHashMap(state.source.size()); + state.source.forEach((k, v) -> target.put(k, Integer.valueOf(v))); + bh.consume(target); + } + + + @State(Scope.Benchmark) + public static class MapBenchmarkState extends BenchmarkState { + + Map source; + + @Setup(Level.Trial) + public void setup() throws Exception { + this.source = CollectionUtils.newHashMap(this.collectionSize); + Map target = new HashMap<>(); + this.targetTypeDesc = TypeDescriptor.forObject(target); + this.source = IntStream.rangeClosed(1, collectionSize).mapToObj(String::valueOf) + .collect(Collectors.toMap(String::valueOf, String::valueOf)); + } + } + + + @State(Scope.Benchmark) + public static class BenchmarkState { + + GenericConversionService conversionService = new GenericConversionService(); + + @Param({"10"}) + int collectionSize; + + TypeDescriptor targetTypeDesc; + } + +} diff --git a/spring-core/src/jmh/java/org/springframework/util/ReflectionUtilsUniqueDeclaredMethodsBenchmark.java b/spring-core/src/jmh/java/org/springframework/util/ReflectionUtilsUniqueDeclaredMethodsBenchmark.java new file mode 100644 index 0000000..6f401d1 --- /dev/null +++ b/spring-core/src/jmh/java/org/springframework/util/ReflectionUtilsUniqueDeclaredMethodsBenchmark.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.lang.reflect.Method; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Mode; + +/** + * Benchmark for finding declared methods on a class using {@link ReflectionUtils}. + * This benchmark is using {@link Mode#SingleShotTime} since we want to benchmark + * cold JVM iterations. + * @author Brian Clozel + */ +@BenchmarkMode(Mode.SingleShotTime) +public class ReflectionUtilsUniqueDeclaredMethodsBenchmark { + + @Benchmark + public Method[] findMethods() { + return ReflectionUtils.getUniqueDeclaredMethods(C.class); + } + + @SuppressWarnings("unused") + class C { + void m00() { } void m01() { } void m02() { } void m03() { } void m04() { } + void m05() { } void m06() { } void m07() { } void m08() { } void m09() { } + void m10() { } void m11() { } void m12() { } void m13() { } void m14() { } + void m15() { } void m16() { } void m17() { } void m18() { } void m19() { } + void m20() { } void m21() { } void m22() { } void m23() { } void m24() { } + void m25() { } void m26() { } void m27() { } void m28() { } void m29() { } + void m30() { } void m31() { } void m32() { } void m33() { } void m34() { } + void m35() { } void m36() { } void m37() { } void m38() { } void m39() { } + void m40() { } void m41() { } void m42() { } void m43() { } void m44() { } + void m45() { } void m46() { } void m47() { } void m48() { } void m49() { } + void m50() { } void m51() { } void m52() { } void m53() { } void m54() { } + void m55() { } void m56() { } void m57() { } void m58() { } void m59() { } + void m60() { } void m61() { } void m62() { } void m63() { } void m64() { } + void m65() { } void m66() { } void m67() { } void m68() { } void m69() { } + void m70() { } void m71() { } void m72() { } void m73() { } void m74() { } + void m75() { } void m76() { } void m77() { } void m78() { } void m79() { } + void m80() { } void m81() { } void m82() { } void m83() { } void m84() { } + void m85() { } void m86() { } void m87() { } void m88() { } void m89() { } + void m90() { } void m91() { } void m92() { } void m93() { } void m94() { } + void m95() { } void m96() { } void m97() { } void m98() { } void m99() { } + } + +} diff --git a/spring-core/src/main/java/org/springframework/asm/AnnotationVisitor.java b/spring-core/src/main/java/org/springframework/asm/AnnotationVisitor.java new file mode 100644 index 0000000..6294ece --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/AnnotationVisitor.java @@ -0,0 +1,155 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +package org.springframework.asm; + +/** + * A visitor to visit a Java annotation. The methods of this class must be called in the following + * order: ( {@code visit} | {@code visitEnum} | {@code visitAnnotation} | {@code visitArray} )* + * {@code visitEnd}. + * + * @author Eric Bruneton + * @author Eugene Kuleshov + */ +public abstract class AnnotationVisitor { + + /** + * The ASM API version implemented by this visitor. The value of this field must be one of {@link + * Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6} or {@link Opcodes#ASM7}. + */ + protected final int api; + + /** + * The annotation visitor to which this visitor must delegate method calls. May be {@literal + * null}. + */ + protected AnnotationVisitor av; + + /** + * Constructs a new {@link AnnotationVisitor}. + * + * @param api the ASM API version implemented by this visitor. Must be one of {@link + * Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6} or {@link Opcodes#ASM7}. + */ + public AnnotationVisitor(final int api) { + this(api, null); + } + + /** + * Constructs a new {@link AnnotationVisitor}. + * + * @param api the ASM API version implemented by this visitor. Must be one of {@link + * Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6} or {@link Opcodes#ASM7}. + * @param annotationVisitor the annotation visitor to which this visitor must delegate method + * calls. May be {@literal null}. + */ + public AnnotationVisitor(final int api, final AnnotationVisitor annotationVisitor) { + if (api != Opcodes.ASM9 + && api != Opcodes.ASM8 + && api != Opcodes.ASM7 + && api != Opcodes.ASM6 + && api != Opcodes.ASM5 + && api != Opcodes.ASM4 + && api != Opcodes.ASM10_EXPERIMENTAL) { + throw new IllegalArgumentException("Unsupported api " + api); + } + // SPRING PATCH: no preview mode check for ASM 9 experimental + this.api = api; + this.av = annotationVisitor; + } + + /** + * Visits a primitive value of the annotation. + * + * @param name the value name. + * @param value the actual value, whose type must be {@link Byte}, {@link Boolean}, {@link + * Character}, {@link Short}, {@link Integer} , {@link Long}, {@link Float}, {@link Double}, + * {@link String} or {@link Type} of {@link Type#OBJECT} or {@link Type#ARRAY} sort. This + * value can also be an array of byte, boolean, short, char, int, long, float or double values + * (this is equivalent to using {@link #visitArray} and visiting each array element in turn, + * but is more convenient). + */ + public void visit(final String name, final Object value) { + if (av != null) { + av.visit(name, value); + } + } + + /** + * Visits an enumeration value of the annotation. + * + * @param name the value name. + * @param descriptor the class descriptor of the enumeration class. + * @param value the actual enumeration value. + */ + public void visitEnum(final String name, final String descriptor, final String value) { + if (av != null) { + av.visitEnum(name, descriptor, value); + } + } + + /** + * Visits a nested annotation value of the annotation. + * + * @param name the value name. + * @param descriptor the class descriptor of the nested annotation class. + * @return a visitor to visit the actual nested annotation value, or {@literal null} if this + * visitor is not interested in visiting this nested annotation. The nested annotation + * value must be fully visited before calling other methods on this annotation visitor. + */ + public AnnotationVisitor visitAnnotation(final String name, final String descriptor) { + if (av != null) { + return av.visitAnnotation(name, descriptor); + } + return null; + } + + /** + * Visits an array value of the annotation. Note that arrays of primitive types (such as byte, + * boolean, short, char, int, long, float or double) can be passed as value to {@link #visit + * visit}. This is what {@link ClassReader} does. + * + * @param name the value name. + * @return a visitor to visit the actual array value elements, or {@literal null} if this visitor + * is not interested in visiting these values. The 'name' parameters passed to the methods of + * this visitor are ignored. All the array values must be visited before calling other + * methods on this annotation visitor. + */ + public AnnotationVisitor visitArray(final String name) { + if (av != null) { + return av.visitArray(name); + } + return null; + } + + /** Visits the end of the annotation. */ + public void visitEnd() { + if (av != null) { + av.visitEnd(); + } + } +} diff --git a/spring-core/src/main/java/org/springframework/asm/AnnotationWriter.java b/spring-core/src/main/java/org/springframework/asm/AnnotationWriter.java new file mode 100644 index 0000000..6494a79 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/AnnotationWriter.java @@ -0,0 +1,553 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +package org.springframework.asm; + +/** + * An {@link AnnotationVisitor} that generates a corresponding 'annotation' or 'type_annotation' + * structure, as defined in the Java Virtual Machine Specification (JVMS). AnnotationWriter + * instances can be chained in a doubly linked list, from which Runtime[In]Visible[Type]Annotations + * attributes can be generated with the {@link #putAnnotations} method. Similarly, arrays of such + * lists can be used to generate Runtime[In]VisibleParameterAnnotations attributes. + * + * @see JVMS + * 4.7.16 + * @see JVMS + * 4.7.20 + * @author Eric Bruneton + * @author Eugene Kuleshov + */ +final class AnnotationWriter extends AnnotationVisitor { + + /** Where the constants used in this AnnotationWriter must be stored. */ + private final SymbolTable symbolTable; + + /** + * Whether values are named or not. AnnotationWriter instances used for annotation default and + * annotation arrays use unnamed values (i.e. they generate an 'element_value' structure for each + * value, instead of an element_name_index followed by an element_value). + */ + private final boolean useNamedValues; + + /** + * The 'annotation' or 'type_annotation' JVMS structure corresponding to the annotation values + * visited so far. All the fields of these structures, except the last one - the + * element_value_pairs array, must be set before this ByteVector is passed to the constructor + * (num_element_value_pairs can be set to 0, it is reset to the correct value in {@link + * #visitEnd()}). The element_value_pairs array is filled incrementally in the various visit() + * methods. + * + *

    Note: as an exception to the above rules, for AnnotationDefault attributes (which contain a + * single element_value by definition), this ByteVector is initially empty when passed to the + * constructor, and {@link #numElementValuePairsOffset} is set to -1. + */ + private final ByteVector annotation; + + /** + * The offset in {@link #annotation} where {@link #numElementValuePairs} must be stored (or -1 for + * the case of AnnotationDefault attributes). + */ + private final int numElementValuePairsOffset; + + /** The number of element value pairs visited so far. */ + private int numElementValuePairs; + + /** + * The previous AnnotationWriter. This field is used to store the list of annotations of a + * Runtime[In]Visible[Type]Annotations attribute. It is unused for nested or array annotations + * (annotation values of annotation type), or for AnnotationDefault attributes. + */ + private final AnnotationWriter previousAnnotation; + + /** + * The next AnnotationWriter. This field is used to store the list of annotations of a + * Runtime[In]Visible[Type]Annotations attribute. It is unused for nested or array annotations + * (annotation values of annotation type), or for AnnotationDefault attributes. + */ + private AnnotationWriter nextAnnotation; + + // ----------------------------------------------------------------------------------------------- + // Constructors and factories + // ----------------------------------------------------------------------------------------------- + + /** + * Constructs a new {@link AnnotationWriter}. + * + * @param symbolTable where the constants used in this AnnotationWriter must be stored. + * @param useNamedValues whether values are named or not. AnnotationDefault and annotation arrays + * use unnamed values. + * @param annotation where the 'annotation' or 'type_annotation' JVMS structure corresponding to + * the visited content must be stored. This ByteVector must already contain all the fields of + * the structure except the last one (the element_value_pairs array). + * @param previousAnnotation the previously visited annotation of the + * Runtime[In]Visible[Type]Annotations attribute to which this annotation belongs, or + * {@literal null} in other cases (e.g. nested or array annotations). + */ + AnnotationWriter( + final SymbolTable symbolTable, + final boolean useNamedValues, + final ByteVector annotation, + final AnnotationWriter previousAnnotation) { + super(/* latest api = */ Opcodes.ASM9); + this.symbolTable = symbolTable; + this.useNamedValues = useNamedValues; + this.annotation = annotation; + // By hypothesis, num_element_value_pairs is stored in the last unsigned short of 'annotation'. + this.numElementValuePairsOffset = annotation.length == 0 ? -1 : annotation.length - 2; + this.previousAnnotation = previousAnnotation; + if (previousAnnotation != null) { + previousAnnotation.nextAnnotation = this; + } + } + + /** + * Creates a new {@link AnnotationWriter} using named values. + * + * @param symbolTable where the constants used in this AnnotationWriter must be stored. + * @param descriptor the class descriptor of the annotation class. + * @param previousAnnotation the previously visited annotation of the + * Runtime[In]Visible[Type]Annotations attribute to which this annotation belongs, or + * {@literal null} in other cases (e.g. nested or array annotations). + * @return a new {@link AnnotationWriter} for the given annotation descriptor. + */ + static AnnotationWriter create( + final SymbolTable symbolTable, + final String descriptor, + final AnnotationWriter previousAnnotation) { + // Create a ByteVector to hold an 'annotation' JVMS structure. + // See https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.7.16. + ByteVector annotation = new ByteVector(); + // Write type_index and reserve space for num_element_value_pairs. + annotation.putShort(symbolTable.addConstantUtf8(descriptor)).putShort(0); + return new AnnotationWriter( + symbolTable, /* useNamedValues = */ true, annotation, previousAnnotation); + } + + /** + * Creates a new {@link AnnotationWriter} using named values. + * + * @param symbolTable where the constants used in this AnnotationWriter must be stored. + * @param typeRef a reference to the annotated type. The sort of this type reference must be + * {@link TypeReference#CLASS_TYPE_PARAMETER}, {@link + * TypeReference#CLASS_TYPE_PARAMETER_BOUND} or {@link TypeReference#CLASS_EXTENDS}. See + * {@link TypeReference}. + * @param typePath the path to the annotated type argument, wildcard bound, array element type, or + * static inner type within 'typeRef'. May be {@literal null} if the annotation targets + * 'typeRef' as a whole. + * @param descriptor the class descriptor of the annotation class. + * @param previousAnnotation the previously visited annotation of the + * Runtime[In]Visible[Type]Annotations attribute to which this annotation belongs, or + * {@literal null} in other cases (e.g. nested or array annotations). + * @return a new {@link AnnotationWriter} for the given type annotation reference and descriptor. + */ + static AnnotationWriter create( + final SymbolTable symbolTable, + final int typeRef, + final TypePath typePath, + final String descriptor, + final AnnotationWriter previousAnnotation) { + // Create a ByteVector to hold a 'type_annotation' JVMS structure. + // See https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.7.20. + ByteVector typeAnnotation = new ByteVector(); + // Write target_type, target_info, and target_path. + TypeReference.putTarget(typeRef, typeAnnotation); + TypePath.put(typePath, typeAnnotation); + // Write type_index and reserve space for num_element_value_pairs. + typeAnnotation.putShort(symbolTable.addConstantUtf8(descriptor)).putShort(0); + return new AnnotationWriter( + symbolTable, /* useNamedValues = */ true, typeAnnotation, previousAnnotation); + } + + // ----------------------------------------------------------------------------------------------- + // Implementation of the AnnotationVisitor abstract class + // ----------------------------------------------------------------------------------------------- + + @Override + public void visit(final String name, final Object value) { + // Case of an element_value with a const_value_index, class_info_index or array_index field. + // See https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.7.16.1. + ++numElementValuePairs; + if (useNamedValues) { + annotation.putShort(symbolTable.addConstantUtf8(name)); + } + if (value instanceof String) { + annotation.put12('s', symbolTable.addConstantUtf8((String) value)); + } else if (value instanceof Byte) { + annotation.put12('B', symbolTable.addConstantInteger(((Byte) value).byteValue()).index); + } else if (value instanceof Boolean) { + int booleanValue = ((Boolean) value).booleanValue() ? 1 : 0; + annotation.put12('Z', symbolTable.addConstantInteger(booleanValue).index); + } else if (value instanceof Character) { + annotation.put12('C', symbolTable.addConstantInteger(((Character) value).charValue()).index); + } else if (value instanceof Short) { + annotation.put12('S', symbolTable.addConstantInteger(((Short) value).shortValue()).index); + } else if (value instanceof Type) { + annotation.put12('c', symbolTable.addConstantUtf8(((Type) value).getDescriptor())); + } else if (value instanceof byte[]) { + byte[] byteArray = (byte[]) value; + annotation.put12('[', byteArray.length); + for (byte byteValue : byteArray) { + annotation.put12('B', symbolTable.addConstantInteger(byteValue).index); + } + } else if (value instanceof boolean[]) { + boolean[] booleanArray = (boolean[]) value; + annotation.put12('[', booleanArray.length); + for (boolean booleanValue : booleanArray) { + annotation.put12('Z', symbolTable.addConstantInteger(booleanValue ? 1 : 0).index); + } + } else if (value instanceof short[]) { + short[] shortArray = (short[]) value; + annotation.put12('[', shortArray.length); + for (short shortValue : shortArray) { + annotation.put12('S', symbolTable.addConstantInteger(shortValue).index); + } + } else if (value instanceof char[]) { + char[] charArray = (char[]) value; + annotation.put12('[', charArray.length); + for (char charValue : charArray) { + annotation.put12('C', symbolTable.addConstantInteger(charValue).index); + } + } else if (value instanceof int[]) { + int[] intArray = (int[]) value; + annotation.put12('[', intArray.length); + for (int intValue : intArray) { + annotation.put12('I', symbolTable.addConstantInteger(intValue).index); + } + } else if (value instanceof long[]) { + long[] longArray = (long[]) value; + annotation.put12('[', longArray.length); + for (long longValue : longArray) { + annotation.put12('J', symbolTable.addConstantLong(longValue).index); + } + } else if (value instanceof float[]) { + float[] floatArray = (float[]) value; + annotation.put12('[', floatArray.length); + for (float floatValue : floatArray) { + annotation.put12('F', symbolTable.addConstantFloat(floatValue).index); + } + } else if (value instanceof double[]) { + double[] doubleArray = (double[]) value; + annotation.put12('[', doubleArray.length); + for (double doubleValue : doubleArray) { + annotation.put12('D', symbolTable.addConstantDouble(doubleValue).index); + } + } else { + Symbol symbol = symbolTable.addConstant(value); + annotation.put12(".s.IFJDCS".charAt(symbol.tag), symbol.index); + } + } + + @Override + public void visitEnum(final String name, final String descriptor, final String value) { + // Case of an element_value with an enum_const_value field. + // See https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.7.16.1. + ++numElementValuePairs; + if (useNamedValues) { + annotation.putShort(symbolTable.addConstantUtf8(name)); + } + annotation + .put12('e', symbolTable.addConstantUtf8(descriptor)) + .putShort(symbolTable.addConstantUtf8(value)); + } + + @Override + public AnnotationVisitor visitAnnotation(final String name, final String descriptor) { + // Case of an element_value with an annotation_value field. + // See https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.7.16.1. + ++numElementValuePairs; + if (useNamedValues) { + annotation.putShort(symbolTable.addConstantUtf8(name)); + } + // Write tag and type_index, and reserve 2 bytes for num_element_value_pairs. + annotation.put12('@', symbolTable.addConstantUtf8(descriptor)).putShort(0); + return new AnnotationWriter(symbolTable, /* useNamedValues = */ true, annotation, null); + } + + @Override + public AnnotationVisitor visitArray(final String name) { + // Case of an element_value with an array_value field. + // https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.7.16.1 + ++numElementValuePairs; + if (useNamedValues) { + annotation.putShort(symbolTable.addConstantUtf8(name)); + } + // Write tag, and reserve 2 bytes for num_values. Here we take advantage of the fact that the + // end of an element_value of array type is similar to the end of an 'annotation' structure: an + // unsigned short num_values followed by num_values element_value, versus an unsigned short + // num_element_value_pairs, followed by num_element_value_pairs { element_name_index, + // element_value } tuples. This allows us to use an AnnotationWriter with unnamed values to + // visit the array elements. Its num_element_value_pairs will correspond to the number of array + // elements and will be stored in what is in fact num_values. + annotation.put12('[', 0); + return new AnnotationWriter(symbolTable, /* useNamedValues = */ false, annotation, null); + } + + @Override + public void visitEnd() { + if (numElementValuePairsOffset != -1) { + byte[] data = annotation.data; + data[numElementValuePairsOffset] = (byte) (numElementValuePairs >>> 8); + data[numElementValuePairsOffset + 1] = (byte) numElementValuePairs; + } + } + + // ----------------------------------------------------------------------------------------------- + // Utility methods + // ----------------------------------------------------------------------------------------------- + + /** + * Returns the size of a Runtime[In]Visible[Type]Annotations attribute containing this annotation + * and all its predecessors (see {@link #previousAnnotation}. Also adds the attribute name + * to the constant pool of the class (if not null). + * + * @param attributeName one of "Runtime[In]Visible[Type]Annotations", or {@literal null}. + * @return the size in bytes of a Runtime[In]Visible[Type]Annotations attribute containing this + * annotation and all its predecessors. This includes the size of the attribute_name_index and + * attribute_length fields. + */ + int computeAnnotationsSize(final String attributeName) { + if (attributeName != null) { + symbolTable.addConstantUtf8(attributeName); + } + // The attribute_name_index, attribute_length and num_annotations fields use 8 bytes. + int attributeSize = 8; + AnnotationWriter annotationWriter = this; + while (annotationWriter != null) { + attributeSize += annotationWriter.annotation.length; + annotationWriter = annotationWriter.previousAnnotation; + } + return attributeSize; + } + + /** + * Returns the size of the Runtime[In]Visible[Type]Annotations attributes containing the given + * annotations and all their predecessors (see {@link #previousAnnotation}. Also adds the + * attribute names to the constant pool of the class (if not null). + * + * @param lastRuntimeVisibleAnnotation The last runtime visible annotation of a field, method or + * class. The previous ones can be accessed with the {@link #previousAnnotation} field. May be + * {@literal null}. + * @param lastRuntimeInvisibleAnnotation The last runtime invisible annotation of this a field, + * method or class. The previous ones can be accessed with the {@link #previousAnnotation} + * field. May be {@literal null}. + * @param lastRuntimeVisibleTypeAnnotation The last runtime visible type annotation of this a + * field, method or class. The previous ones can be accessed with the {@link + * #previousAnnotation} field. May be {@literal null}. + * @param lastRuntimeInvisibleTypeAnnotation The last runtime invisible type annotation of a + * field, method or class field. The previous ones can be accessed with the {@link + * #previousAnnotation} field. May be {@literal null}. + * @return the size in bytes of a Runtime[In]Visible[Type]Annotations attribute containing the + * given annotations and all their predecessors. This includes the size of the + * attribute_name_index and attribute_length fields. + */ + static int computeAnnotationsSize( + final AnnotationWriter lastRuntimeVisibleAnnotation, + final AnnotationWriter lastRuntimeInvisibleAnnotation, + final AnnotationWriter lastRuntimeVisibleTypeAnnotation, + final AnnotationWriter lastRuntimeInvisibleTypeAnnotation) { + int size = 0; + if (lastRuntimeVisibleAnnotation != null) { + size += + lastRuntimeVisibleAnnotation.computeAnnotationsSize( + Constants.RUNTIME_VISIBLE_ANNOTATIONS); + } + if (lastRuntimeInvisibleAnnotation != null) { + size += + lastRuntimeInvisibleAnnotation.computeAnnotationsSize( + Constants.RUNTIME_INVISIBLE_ANNOTATIONS); + } + if (lastRuntimeVisibleTypeAnnotation != null) { + size += + lastRuntimeVisibleTypeAnnotation.computeAnnotationsSize( + Constants.RUNTIME_VISIBLE_TYPE_ANNOTATIONS); + } + if (lastRuntimeInvisibleTypeAnnotation != null) { + size += + lastRuntimeInvisibleTypeAnnotation.computeAnnotationsSize( + Constants.RUNTIME_INVISIBLE_TYPE_ANNOTATIONS); + } + return size; + } + + /** + * Puts a Runtime[In]Visible[Type]Annotations attribute containing this annotations and all its + * predecessors (see {@link #previousAnnotation} in the given ByteVector. Annotations are + * put in the same order they have been visited. + * + * @param attributeNameIndex the constant pool index of the attribute name (one of + * "Runtime[In]Visible[Type]Annotations"). + * @param output where the attribute must be put. + */ + void putAnnotations(final int attributeNameIndex, final ByteVector output) { + int attributeLength = 2; // For num_annotations. + int numAnnotations = 0; + AnnotationWriter annotationWriter = this; + AnnotationWriter firstAnnotation = null; + while (annotationWriter != null) { + // In case the user forgot to call visitEnd(). + annotationWriter.visitEnd(); + attributeLength += annotationWriter.annotation.length; + numAnnotations++; + firstAnnotation = annotationWriter; + annotationWriter = annotationWriter.previousAnnotation; + } + output.putShort(attributeNameIndex); + output.putInt(attributeLength); + output.putShort(numAnnotations); + annotationWriter = firstAnnotation; + while (annotationWriter != null) { + output.putByteArray(annotationWriter.annotation.data, 0, annotationWriter.annotation.length); + annotationWriter = annotationWriter.nextAnnotation; + } + } + + /** + * Puts the Runtime[In]Visible[Type]Annotations attributes containing the given annotations and + * all their predecessors (see {@link #previousAnnotation} in the given ByteVector. + * Annotations are put in the same order they have been visited. + * + * @param symbolTable where the constants used in the AnnotationWriter instances are stored. + * @param lastRuntimeVisibleAnnotation The last runtime visible annotation of a field, method or + * class. The previous ones can be accessed with the {@link #previousAnnotation} field. May be + * {@literal null}. + * @param lastRuntimeInvisibleAnnotation The last runtime invisible annotation of this a field, + * method or class. The previous ones can be accessed with the {@link #previousAnnotation} + * field. May be {@literal null}. + * @param lastRuntimeVisibleTypeAnnotation The last runtime visible type annotation of this a + * field, method or class. The previous ones can be accessed with the {@link + * #previousAnnotation} field. May be {@literal null}. + * @param lastRuntimeInvisibleTypeAnnotation The last runtime invisible type annotation of a + * field, method or class field. The previous ones can be accessed with the {@link + * #previousAnnotation} field. May be {@literal null}. + * @param output where the attributes must be put. + */ + static void putAnnotations( + final SymbolTable symbolTable, + final AnnotationWriter lastRuntimeVisibleAnnotation, + final AnnotationWriter lastRuntimeInvisibleAnnotation, + final AnnotationWriter lastRuntimeVisibleTypeAnnotation, + final AnnotationWriter lastRuntimeInvisibleTypeAnnotation, + final ByteVector output) { + if (lastRuntimeVisibleAnnotation != null) { + lastRuntimeVisibleAnnotation.putAnnotations( + symbolTable.addConstantUtf8(Constants.RUNTIME_VISIBLE_ANNOTATIONS), output); + } + if (lastRuntimeInvisibleAnnotation != null) { + lastRuntimeInvisibleAnnotation.putAnnotations( + symbolTable.addConstantUtf8(Constants.RUNTIME_INVISIBLE_ANNOTATIONS), output); + } + if (lastRuntimeVisibleTypeAnnotation != null) { + lastRuntimeVisibleTypeAnnotation.putAnnotations( + symbolTable.addConstantUtf8(Constants.RUNTIME_VISIBLE_TYPE_ANNOTATIONS), output); + } + if (lastRuntimeInvisibleTypeAnnotation != null) { + lastRuntimeInvisibleTypeAnnotation.putAnnotations( + symbolTable.addConstantUtf8(Constants.RUNTIME_INVISIBLE_TYPE_ANNOTATIONS), output); + } + } + + /** + * Returns the size of a Runtime[In]VisibleParameterAnnotations attribute containing all the + * annotation lists from the given AnnotationWriter sub-array. Also adds the attribute name to the + * constant pool of the class. + * + * @param attributeName one of "Runtime[In]VisibleParameterAnnotations". + * @param annotationWriters an array of AnnotationWriter lists (designated by their last + * element). + * @param annotableParameterCount the number of elements in annotationWriters to take into account + * (elements [0..annotableParameterCount[ are taken into account). + * @return the size in bytes of a Runtime[In]VisibleParameterAnnotations attribute corresponding + * to the given sub-array of AnnotationWriter lists. This includes the size of the + * attribute_name_index and attribute_length fields. + */ + static int computeParameterAnnotationsSize( + final String attributeName, + final AnnotationWriter[] annotationWriters, + final int annotableParameterCount) { + // Note: attributeName is added to the constant pool by the call to computeAnnotationsSize + // below. This assumes that there is at least one non-null element in the annotationWriters + // sub-array (which is ensured by the lazy instantiation of this array in MethodWriter). + // The attribute_name_index, attribute_length and num_parameters fields use 7 bytes, and each + // element of the parameter_annotations array uses 2 bytes for its num_annotations field. + int attributeSize = 7 + 2 * annotableParameterCount; + for (int i = 0; i < annotableParameterCount; ++i) { + AnnotationWriter annotationWriter = annotationWriters[i]; + attributeSize += + annotationWriter == null ? 0 : annotationWriter.computeAnnotationsSize(attributeName) - 8; + } + return attributeSize; + } + + /** + * Puts a Runtime[In]VisibleParameterAnnotations attribute containing all the annotation lists + * from the given AnnotationWriter sub-array in the given ByteVector. + * + * @param attributeNameIndex constant pool index of the attribute name (one of + * Runtime[In]VisibleParameterAnnotations). + * @param annotationWriters an array of AnnotationWriter lists (designated by their last + * element). + * @param annotableParameterCount the number of elements in annotationWriters to put (elements + * [0..annotableParameterCount[ are put). + * @param output where the attribute must be put. + */ + static void putParameterAnnotations( + final int attributeNameIndex, + final AnnotationWriter[] annotationWriters, + final int annotableParameterCount, + final ByteVector output) { + // The num_parameters field uses 1 byte, and each element of the parameter_annotations array + // uses 2 bytes for its num_annotations field. + int attributeLength = 1 + 2 * annotableParameterCount; + for (int i = 0; i < annotableParameterCount; ++i) { + AnnotationWriter annotationWriter = annotationWriters[i]; + attributeLength += + annotationWriter == null ? 0 : annotationWriter.computeAnnotationsSize(null) - 8; + } + output.putShort(attributeNameIndex); + output.putInt(attributeLength); + output.putByte(annotableParameterCount); + for (int i = 0; i < annotableParameterCount; ++i) { + AnnotationWriter annotationWriter = annotationWriters[i]; + AnnotationWriter firstAnnotation = null; + int numAnnotations = 0; + while (annotationWriter != null) { + // In case user the forgot to call visitEnd(). + annotationWriter.visitEnd(); + numAnnotations++; + firstAnnotation = annotationWriter; + annotationWriter = annotationWriter.previousAnnotation; + } + output.putShort(numAnnotations); + annotationWriter = firstAnnotation; + while (annotationWriter != null) { + output.putByteArray( + annotationWriter.annotation.data, 0, annotationWriter.annotation.length); + annotationWriter = annotationWriter.nextAnnotation; + } + } + } +} diff --git a/spring-core/src/main/java/org/springframework/asm/Attribute.java b/spring-core/src/main/java/org/springframework/asm/Attribute.java new file mode 100644 index 0000000..e40c42f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/Attribute.java @@ -0,0 +1,392 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +package org.springframework.asm; + +/** + * A non standard class, field, method or Code attribute, as defined in the Java Virtual Machine + * Specification (JVMS). + * + * @see JVMS + * 4.7 + * @see JVMS + * 4.7.3 + * @author Eric Bruneton + * @author Eugene Kuleshov + */ +public class Attribute { + + /** The type of this attribute, also called its name in the JVMS. */ + public final String type; + + /** + * The raw content of this attribute, only used for unknown attributes (see {@link #isUnknown()}). + * The 6 header bytes of the attribute (attribute_name_index and attribute_length) are not + * included. + */ + private byte[] content; + + /** + * The next attribute in this attribute list (Attribute instances can be linked via this field to + * store a list of class, field, method or Code attributes). May be {@literal null}. + */ + Attribute nextAttribute; + + /** + * Constructs a new empty attribute. + * + * @param type the type of the attribute. + */ + protected Attribute(final String type) { + this.type = type; + } + + /** + * Returns {@literal true} if this type of attribute is unknown. This means that the attribute + * content can't be parsed to extract constant pool references, labels, etc. Instead, the + * attribute content is read as an opaque byte array, and written back as is. This can lead to + * invalid attributes, if the content actually contains constant pool references, labels, or other + * symbolic references that need to be updated when there are changes to the constant pool, the + * method bytecode, etc. The default implementation of this method always returns {@literal true}. + * + * @return {@literal true} if this type of attribute is unknown. + */ + public boolean isUnknown() { + return true; + } + + /** + * Returns {@literal true} if this type of attribute is a Code attribute. + * + * @return {@literal true} if this type of attribute is a Code attribute. + */ + public boolean isCodeAttribute() { + return false; + } + + /** + * Returns the labels corresponding to this attribute. + * + * @return the labels corresponding to this attribute, or {@literal null} if this attribute is not + * a Code attribute that contains labels. + */ + protected Label[] getLabels() { + return new Label[0]; + } + + /** + * Reads a {@link #type} attribute. This method must return a new {@link Attribute} object, + * of type {@link #type}, corresponding to the 'length' bytes starting at 'offset', in the given + * ClassReader. + * + * @param classReader the class that contains the attribute to be read. + * @param offset index of the first byte of the attribute's content in {@link ClassReader}. The 6 + * attribute header bytes (attribute_name_index and attribute_length) are not taken into + * account here. + * @param length the length of the attribute's content (excluding the 6 attribute header bytes). + * @param charBuffer the buffer to be used to call the ClassReader methods requiring a + * 'charBuffer' parameter. + * @param codeAttributeOffset index of the first byte of content of the enclosing Code attribute + * in {@link ClassReader}, or -1 if the attribute to be read is not a Code attribute. The 6 + * attribute header bytes (attribute_name_index and attribute_length) are not taken into + * account here. + * @param labels the labels of the method's code, or {@literal null} if the attribute to be read + * is not a Code attribute. + * @return a new {@link Attribute} object corresponding to the specified bytes. + */ + protected Attribute read( + final ClassReader classReader, + final int offset, + final int length, + final char[] charBuffer, + final int codeAttributeOffset, + final Label[] labels) { + Attribute attribute = new Attribute(type); + attribute.content = new byte[length]; + System.arraycopy(classReader.classFileBuffer, offset, attribute.content, 0, length); + return attribute; + } + + /** + * Returns the byte array form of the content of this attribute. The 6 header bytes + * (attribute_name_index and attribute_length) must not be added in the returned + * ByteVector. + * + * @param classWriter the class to which this attribute must be added. This parameter can be used + * to add the items that corresponds to this attribute to the constant pool of this class. + * @param code the bytecode of the method corresponding to this Code attribute, or {@literal null} + * if this attribute is not a Code attribute. Corresponds to the 'code' field of the Code + * attribute. + * @param codeLength the length of the bytecode of the method corresponding to this code + * attribute, or 0 if this attribute is not a Code attribute. Corresponds to the 'code_length' + * field of the Code attribute. + * @param maxStack the maximum stack size of the method corresponding to this Code attribute, or + * -1 if this attribute is not a Code attribute. + * @param maxLocals the maximum number of local variables of the method corresponding to this code + * attribute, or -1 if this attribute is not a Code attribute. + * @return the byte array form of this attribute. + */ + protected ByteVector write( + final ClassWriter classWriter, + final byte[] code, + final int codeLength, + final int maxStack, + final int maxLocals) { + return new ByteVector(content); + } + + /** + * Returns the number of attributes of the attribute list that begins with this attribute. + * + * @return the number of attributes of the attribute list that begins with this attribute. + */ + final int getAttributeCount() { + int count = 0; + Attribute attribute = this; + while (attribute != null) { + count += 1; + attribute = attribute.nextAttribute; + } + return count; + } + + /** + * Returns the total size in bytes of all the attributes in the attribute list that begins with + * this attribute. This size includes the 6 header bytes (attribute_name_index and + * attribute_length) per attribute. Also adds the attribute type names to the constant pool. + * + * @param symbolTable where the constants used in the attributes must be stored. + * @return the size of all the attributes in this attribute list. This size includes the size of + * the attribute headers. + */ + final int computeAttributesSize(final SymbolTable symbolTable) { + final byte[] code = null; + final int codeLength = 0; + final int maxStack = -1; + final int maxLocals = -1; + return computeAttributesSize(symbolTable, code, codeLength, maxStack, maxLocals); + } + + /** + * Returns the total size in bytes of all the attributes in the attribute list that begins with + * this attribute. This size includes the 6 header bytes (attribute_name_index and + * attribute_length) per attribute. Also adds the attribute type names to the constant pool. + * + * @param symbolTable where the constants used in the attributes must be stored. + * @param code the bytecode of the method corresponding to these Code attributes, or {@literal + * null} if they are not Code attributes. Corresponds to the 'code' field of the Code + * attribute. + * @param codeLength the length of the bytecode of the method corresponding to these code + * attributes, or 0 if they are not Code attributes. Corresponds to the 'code_length' field of + * the Code attribute. + * @param maxStack the maximum stack size of the method corresponding to these Code attributes, or + * -1 if they are not Code attributes. + * @param maxLocals the maximum number of local variables of the method corresponding to these + * Code attributes, or -1 if they are not Code attribute. + * @return the size of all the attributes in this attribute list. This size includes the size of + * the attribute headers. + */ + final int computeAttributesSize( + final SymbolTable symbolTable, + final byte[] code, + final int codeLength, + final int maxStack, + final int maxLocals) { + final ClassWriter classWriter = symbolTable.classWriter; + int size = 0; + Attribute attribute = this; + while (attribute != null) { + symbolTable.addConstantUtf8(attribute.type); + size += 6 + attribute.write(classWriter, code, codeLength, maxStack, maxLocals).length; + attribute = attribute.nextAttribute; + } + return size; + } + + /** + * Returns the total size in bytes of all the attributes that correspond to the given field, + * method or class access flags and signature. This size includes the 6 header bytes + * (attribute_name_index and attribute_length) per attribute. Also adds the attribute type names + * to the constant pool. + * + * @param symbolTable where the constants used in the attributes must be stored. + * @param accessFlags some field, method or class access flags. + * @param signatureIndex the constant pool index of a field, method of class signature. + * @return the size of all the attributes in bytes. This size includes the size of the attribute + * headers. + */ + static int computeAttributesSize( + final SymbolTable symbolTable, final int accessFlags, final int signatureIndex) { + int size = 0; + // Before Java 1.5, synthetic fields are represented with a Synthetic attribute. + if ((accessFlags & Opcodes.ACC_SYNTHETIC) != 0 + && symbolTable.getMajorVersion() < Opcodes.V1_5) { + // Synthetic attributes always use 6 bytes. + symbolTable.addConstantUtf8(Constants.SYNTHETIC); + size += 6; + } + if (signatureIndex != 0) { + // Signature attributes always use 8 bytes. + symbolTable.addConstantUtf8(Constants.SIGNATURE); + size += 8; + } + // ACC_DEPRECATED is ASM specific, the ClassFile format uses a Deprecated attribute instead. + if ((accessFlags & Opcodes.ACC_DEPRECATED) != 0) { + // Deprecated attributes always use 6 bytes. + symbolTable.addConstantUtf8(Constants.DEPRECATED); + size += 6; + } + return size; + } + + /** + * Puts all the attributes of the attribute list that begins with this attribute, in the given + * byte vector. This includes the 6 header bytes (attribute_name_index and attribute_length) per + * attribute. + * + * @param symbolTable where the constants used in the attributes must be stored. + * @param output where the attributes must be written. + */ + final void putAttributes(final SymbolTable symbolTable, final ByteVector output) { + final byte[] code = null; + final int codeLength = 0; + final int maxStack = -1; + final int maxLocals = -1; + putAttributes(symbolTable, code, codeLength, maxStack, maxLocals, output); + } + + /** + * Puts all the attributes of the attribute list that begins with this attribute, in the given + * byte vector. This includes the 6 header bytes (attribute_name_index and attribute_length) per + * attribute. + * + * @param symbolTable where the constants used in the attributes must be stored. + * @param code the bytecode of the method corresponding to these Code attributes, or {@literal + * null} if they are not Code attributes. Corresponds to the 'code' field of the Code + * attribute. + * @param codeLength the length of the bytecode of the method corresponding to these code + * attributes, or 0 if they are not Code attributes. Corresponds to the 'code_length' field of + * the Code attribute. + * @param maxStack the maximum stack size of the method corresponding to these Code attributes, or + * -1 if they are not Code attributes. + * @param maxLocals the maximum number of local variables of the method corresponding to these + * Code attributes, or -1 if they are not Code attribute. + * @param output where the attributes must be written. + */ + final void putAttributes( + final SymbolTable symbolTable, + final byte[] code, + final int codeLength, + final int maxStack, + final int maxLocals, + final ByteVector output) { + final ClassWriter classWriter = symbolTable.classWriter; + Attribute attribute = this; + while (attribute != null) { + ByteVector attributeContent = + attribute.write(classWriter, code, codeLength, maxStack, maxLocals); + // Put attribute_name_index and attribute_length. + output.putShort(symbolTable.addConstantUtf8(attribute.type)).putInt(attributeContent.length); + output.putByteArray(attributeContent.data, 0, attributeContent.length); + attribute = attribute.nextAttribute; + } + } + + /** + * Puts all the attributes that correspond to the given field, method or class access flags and + * signature, in the given byte vector. This includes the 6 header bytes (attribute_name_index and + * attribute_length) per attribute. + * + * @param symbolTable where the constants used in the attributes must be stored. + * @param accessFlags some field, method or class access flags. + * @param signatureIndex the constant pool index of a field, method of class signature. + * @param output where the attributes must be written. + */ + static void putAttributes( + final SymbolTable symbolTable, + final int accessFlags, + final int signatureIndex, + final ByteVector output) { + // Before Java 1.5, synthetic fields are represented with a Synthetic attribute. + if ((accessFlags & Opcodes.ACC_SYNTHETIC) != 0 + && symbolTable.getMajorVersion() < Opcodes.V1_5) { + output.putShort(symbolTable.addConstantUtf8(Constants.SYNTHETIC)).putInt(0); + } + if (signatureIndex != 0) { + output + .putShort(symbolTable.addConstantUtf8(Constants.SIGNATURE)) + .putInt(2) + .putShort(signatureIndex); + } + if ((accessFlags & Opcodes.ACC_DEPRECATED) != 0) { + output.putShort(symbolTable.addConstantUtf8(Constants.DEPRECATED)).putInt(0); + } + } + + /** A set of attribute prototypes (attributes with the same type are considered equal). */ + static final class Set { + + private static final int SIZE_INCREMENT = 6; + + private int size; + private Attribute[] data = new Attribute[SIZE_INCREMENT]; + + void addAttributes(final Attribute attributeList) { + Attribute attribute = attributeList; + while (attribute != null) { + if (!contains(attribute)) { + add(attribute); + } + attribute = attribute.nextAttribute; + } + } + + Attribute[] toArray() { + Attribute[] result = new Attribute[size]; + System.arraycopy(data, 0, result, 0, size); + return result; + } + + private boolean contains(final Attribute attribute) { + for (int i = 0; i < size; ++i) { + if (data[i].type.equals(attribute.type)) { + return true; + } + } + return false; + } + + private void add(final Attribute attribute) { + if (size >= data.length) { + Attribute[] newData = new Attribute[data.length + SIZE_INCREMENT]; + System.arraycopy(data, 0, newData, 0, size); + data = newData; + } + data[size++] = attribute; + } + } +} diff --git a/spring-core/src/main/java/org/springframework/asm/ByteVector.java b/spring-core/src/main/java/org/springframework/asm/ByteVector.java new file mode 100644 index 0000000..3789eec --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/ByteVector.java @@ -0,0 +1,361 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +package org.springframework.asm; + +/** + * A dynamically extensible vector of bytes. This class is roughly equivalent to a DataOutputStream + * on top of a ByteArrayOutputStream, but is more efficient. + * + * @author Eric Bruneton + */ +public class ByteVector { + + /** The content of this vector. Only the first {@link #length} bytes contain real data. */ + byte[] data; + + /** The actual number of bytes in this vector. */ + int length; + + /** Constructs a new {@link ByteVector} with a default initial capacity. */ + public ByteVector() { + data = new byte[64]; + } + + /** + * Constructs a new {@link ByteVector} with the given initial capacity. + * + * @param initialCapacity the initial capacity of the byte vector to be constructed. + */ + public ByteVector(final int initialCapacity) { + data = new byte[initialCapacity]; + } + + /** + * Constructs a new {@link ByteVector} from the given initial data. + * + * @param data the initial data of the new byte vector. + */ + ByteVector(final byte[] data) { + this.data = data; + this.length = data.length; + } + + /** + * Puts a byte into this byte vector. The byte vector is automatically enlarged if necessary. + * + * @param byteValue a byte. + * @return this byte vector. + */ + public ByteVector putByte(final int byteValue) { + int currentLength = length; + if (currentLength + 1 > data.length) { + enlarge(1); + } + data[currentLength++] = (byte) byteValue; + length = currentLength; + return this; + } + + /** + * Puts two bytes into this byte vector. The byte vector is automatically enlarged if necessary. + * + * @param byteValue1 a byte. + * @param byteValue2 another byte. + * @return this byte vector. + */ + final ByteVector put11(final int byteValue1, final int byteValue2) { + int currentLength = length; + if (currentLength + 2 > data.length) { + enlarge(2); + } + byte[] currentData = data; + currentData[currentLength++] = (byte) byteValue1; + currentData[currentLength++] = (byte) byteValue2; + length = currentLength; + return this; + } + + /** + * Puts a short into this byte vector. The byte vector is automatically enlarged if necessary. + * + * @param shortValue a short. + * @return this byte vector. + */ + public ByteVector putShort(final int shortValue) { + int currentLength = length; + if (currentLength + 2 > data.length) { + enlarge(2); + } + byte[] currentData = data; + currentData[currentLength++] = (byte) (shortValue >>> 8); + currentData[currentLength++] = (byte) shortValue; + length = currentLength; + return this; + } + + /** + * Puts a byte and a short into this byte vector. The byte vector is automatically enlarged if + * necessary. + * + * @param byteValue a byte. + * @param shortValue a short. + * @return this byte vector. + */ + final ByteVector put12(final int byteValue, final int shortValue) { + int currentLength = length; + if (currentLength + 3 > data.length) { + enlarge(3); + } + byte[] currentData = data; + currentData[currentLength++] = (byte) byteValue; + currentData[currentLength++] = (byte) (shortValue >>> 8); + currentData[currentLength++] = (byte) shortValue; + length = currentLength; + return this; + } + + /** + * Puts two bytes and a short into this byte vector. The byte vector is automatically enlarged if + * necessary. + * + * @param byteValue1 a byte. + * @param byteValue2 another byte. + * @param shortValue a short. + * @return this byte vector. + */ + final ByteVector put112(final int byteValue1, final int byteValue2, final int shortValue) { + int currentLength = length; + if (currentLength + 4 > data.length) { + enlarge(4); + } + byte[] currentData = data; + currentData[currentLength++] = (byte) byteValue1; + currentData[currentLength++] = (byte) byteValue2; + currentData[currentLength++] = (byte) (shortValue >>> 8); + currentData[currentLength++] = (byte) shortValue; + length = currentLength; + return this; + } + + /** + * Puts an int into this byte vector. The byte vector is automatically enlarged if necessary. + * + * @param intValue an int. + * @return this byte vector. + */ + public ByteVector putInt(final int intValue) { + int currentLength = length; + if (currentLength + 4 > data.length) { + enlarge(4); + } + byte[] currentData = data; + currentData[currentLength++] = (byte) (intValue >>> 24); + currentData[currentLength++] = (byte) (intValue >>> 16); + currentData[currentLength++] = (byte) (intValue >>> 8); + currentData[currentLength++] = (byte) intValue; + length = currentLength; + return this; + } + + /** + * Puts one byte and two shorts into this byte vector. The byte vector is automatically enlarged + * if necessary. + * + * @param byteValue a byte. + * @param shortValue1 a short. + * @param shortValue2 another short. + * @return this byte vector. + */ + final ByteVector put122(final int byteValue, final int shortValue1, final int shortValue2) { + int currentLength = length; + if (currentLength + 5 > data.length) { + enlarge(5); + } + byte[] currentData = data; + currentData[currentLength++] = (byte) byteValue; + currentData[currentLength++] = (byte) (shortValue1 >>> 8); + currentData[currentLength++] = (byte) shortValue1; + currentData[currentLength++] = (byte) (shortValue2 >>> 8); + currentData[currentLength++] = (byte) shortValue2; + length = currentLength; + return this; + } + + /** + * Puts a long into this byte vector. The byte vector is automatically enlarged if necessary. + * + * @param longValue a long. + * @return this byte vector. + */ + public ByteVector putLong(final long longValue) { + int currentLength = length; + if (currentLength + 8 > data.length) { + enlarge(8); + } + byte[] currentData = data; + int intValue = (int) (longValue >>> 32); + currentData[currentLength++] = (byte) (intValue >>> 24); + currentData[currentLength++] = (byte) (intValue >>> 16); + currentData[currentLength++] = (byte) (intValue >>> 8); + currentData[currentLength++] = (byte) intValue; + intValue = (int) longValue; + currentData[currentLength++] = (byte) (intValue >>> 24); + currentData[currentLength++] = (byte) (intValue >>> 16); + currentData[currentLength++] = (byte) (intValue >>> 8); + currentData[currentLength++] = (byte) intValue; + length = currentLength; + return this; + } + + /** + * Puts an UTF8 string into this byte vector. The byte vector is automatically enlarged if + * necessary. + * + * @param stringValue a String whose UTF8 encoded length must be less than 65536. + * @return this byte vector. + */ + // DontCheck(AbbreviationAsWordInName): can't be renamed (for backward binary compatibility). + public ByteVector putUTF8(final String stringValue) { + int charLength = stringValue.length(); + if (charLength > 65535) { + throw new IllegalArgumentException("UTF8 string too large"); + } + int currentLength = length; + if (currentLength + 2 + charLength > data.length) { + enlarge(2 + charLength); + } + byte[] currentData = data; + // Optimistic algorithm: instead of computing the byte length and then serializing the string + // (which requires two loops), we assume the byte length is equal to char length (which is the + // most frequent case), and we start serializing the string right away. During the + // serialization, if we find that this assumption is wrong, we continue with the general method. + currentData[currentLength++] = (byte) (charLength >>> 8); + currentData[currentLength++] = (byte) charLength; + for (int i = 0; i < charLength; ++i) { + char charValue = stringValue.charAt(i); + if (charValue >= '\u0001' && charValue <= '\u007F') { + currentData[currentLength++] = (byte) charValue; + } else { + length = currentLength; + return encodeUtf8(stringValue, i, 65535); + } + } + length = currentLength; + return this; + } + + /** + * Puts an UTF8 string into this byte vector. The byte vector is automatically enlarged if + * necessary. The string length is encoded in two bytes before the encoded characters, if there is + * space for that (i.e. if this.length - offset - 2 >= 0). + * + * @param stringValue the String to encode. + * @param offset the index of the first character to encode. The previous characters are supposed + * to have already been encoded, using only one byte per character. + * @param maxByteLength the maximum byte length of the encoded string, including the already + * encoded characters. + * @return this byte vector. + */ + final ByteVector encodeUtf8(final String stringValue, final int offset, final int maxByteLength) { + int charLength = stringValue.length(); + int byteLength = offset; + for (int i = offset; i < charLength; ++i) { + char charValue = stringValue.charAt(i); + if (charValue >= 0x0001 && charValue <= 0x007F) { + byteLength++; + } else if (charValue <= 0x07FF) { + byteLength += 2; + } else { + byteLength += 3; + } + } + if (byteLength > maxByteLength) { + throw new IllegalArgumentException("UTF8 string too large"); + } + // Compute where 'byteLength' must be stored in 'data', and store it at this location. + int byteLengthOffset = length - offset - 2; + if (byteLengthOffset >= 0) { + data[byteLengthOffset] = (byte) (byteLength >>> 8); + data[byteLengthOffset + 1] = (byte) byteLength; + } + if (length + byteLength - offset > data.length) { + enlarge(byteLength - offset); + } + int currentLength = length; + for (int i = offset; i < charLength; ++i) { + char charValue = stringValue.charAt(i); + if (charValue >= 0x0001 && charValue <= 0x007F) { + data[currentLength++] = (byte) charValue; + } else if (charValue <= 0x07FF) { + data[currentLength++] = (byte) (0xC0 | charValue >> 6 & 0x1F); + data[currentLength++] = (byte) (0x80 | charValue & 0x3F); + } else { + data[currentLength++] = (byte) (0xE0 | charValue >> 12 & 0xF); + data[currentLength++] = (byte) (0x80 | charValue >> 6 & 0x3F); + data[currentLength++] = (byte) (0x80 | charValue & 0x3F); + } + } + length = currentLength; + return this; + } + + /** + * Puts an array of bytes into this byte vector. The byte vector is automatically enlarged if + * necessary. + * + * @param byteArrayValue an array of bytes. May be {@literal null} to put {@code byteLength} null + * bytes into this byte vector. + * @param byteOffset index of the first byte of byteArrayValue that must be copied. + * @param byteLength number of bytes of byteArrayValue that must be copied. + * @return this byte vector. + */ + public ByteVector putByteArray( + final byte[] byteArrayValue, final int byteOffset, final int byteLength) { + if (length + byteLength > data.length) { + enlarge(byteLength); + } + if (byteArrayValue != null) { + System.arraycopy(byteArrayValue, byteOffset, data, length, byteLength); + } + length += byteLength; + return this; + } + + /** + * Enlarges this byte vector so that it can receive 'size' more bytes. + * + * @param size number of additional bytes that this byte vector should be able to receive. + */ + private void enlarge(final int size) { + int doubleCapacity = 2 * data.length; + int minimalCapacity = length + size; + byte[] newData = new byte[doubleCapacity > minimalCapacity ? doubleCapacity : minimalCapacity]; + System.arraycopy(data, 0, newData, 0, length); + data = newData; + } +} diff --git a/spring-core/src/main/java/org/springframework/asm/ClassReader.java b/spring-core/src/main/java/org/springframework/asm/ClassReader.java new file mode 100644 index 0000000..308f504 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/ClassReader.java @@ -0,0 +1,3822 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +package org.springframework.asm; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * A parser to make a {@link ClassVisitor} visit a ClassFile structure, as defined in the Java + * Virtual Machine Specification (JVMS). This class parses the ClassFile content and calls the + * appropriate visit methods of a given {@link ClassVisitor} for each field, method and bytecode + * instruction encountered. + * + * @see JVMS 4 + * @author Eric Bruneton + * @author Eugene Kuleshov + */ +public class ClassReader { + + /** + * A flag to skip the Code attributes. If this flag is set the Code attributes are neither parsed + * nor visited. + */ + public static final int SKIP_CODE = 1; + + /** + * A flag to skip the SourceFile, SourceDebugExtension, LocalVariableTable, + * LocalVariableTypeTable, LineNumberTable and MethodParameters attributes. If this flag is set + * these attributes are neither parsed nor visited (i.e. {@link ClassVisitor#visitSource}, {@link + * MethodVisitor#visitLocalVariable}, {@link MethodVisitor#visitLineNumber} and {@link + * MethodVisitor#visitParameter} are not called). + */ + public static final int SKIP_DEBUG = 2; + + /** + * A flag to skip the StackMap and StackMapTable attributes. If this flag is set these attributes + * are neither parsed nor visited (i.e. {@link MethodVisitor#visitFrame} is not called). This flag + * is useful when the {@link ClassWriter#COMPUTE_FRAMES} option is used: it avoids visiting frames + * that will be ignored and recomputed from scratch. + */ + public static final int SKIP_FRAMES = 4; + + /** + * A flag to expand the stack map frames. By default stack map frames are visited in their + * original format (i.e. "expanded" for classes whose version is less than V1_6, and "compressed" + * for the other classes). If this flag is set, stack map frames are always visited in expanded + * format (this option adds a decompression/compression step in ClassReader and ClassWriter which + * degrades performance quite a lot). + */ + public static final int EXPAND_FRAMES = 8; + + /** + * A flag to expand the ASM specific instructions into an equivalent sequence of standard bytecode + * instructions. When resolving a forward jump it may happen that the signed 2 bytes offset + * reserved for it is not sufficient to store the bytecode offset. In this case the jump + * instruction is replaced with a temporary ASM specific instruction using an unsigned 2 bytes + * offset (see {@link Label#resolve}). This internal flag is used to re-read classes containing + * such instructions, in order to replace them with standard instructions. In addition, when this + * flag is used, goto_w and jsr_w are not converted into goto and jsr, to make sure that + * infinite loops where a goto_w is replaced with a goto in ClassReader and converted back to a + * goto_w in ClassWriter cannot occur. + */ + static final int EXPAND_ASM_INSNS = 256; + + /** The size of the temporary byte array used to read class input streams chunk by chunk. */ + private static final int INPUT_STREAM_DATA_CHUNK_SIZE = 4096; + + /** + * A byte array containing the JVMS ClassFile structure to be parsed. + * + * @deprecated Use {@link #readByte(int)} and the other read methods instead. This field will + * eventually be deleted. + */ + @Deprecated + // DontCheck(MemberName): can't be renamed (for backward binary compatibility). + public final byte[] b; + /** The offset in bytes of the ClassFile's access_flags field. */ + public final int header; + /** + * A byte array containing the JVMS ClassFile structure to be parsed. The content of this array + * must not be modified. This field is intended for {@link Attribute} sub classes, and is normally + * not needed by class visitors. + * + *

    NOTE: the ClassFile structure can start at any offset within this array, i.e. it does not + * necessarily start at offset 0. Use {@link #getItem} and {@link #header} to get correct + * ClassFile element offsets within this byte array. + */ + final byte[] classFileBuffer; + /** + * The offset in bytes, in {@link #classFileBuffer}, of each cp_info entry of the ClassFile's + * constant_pool array, plus one. In other words, the offset of constant pool entry i is + * given by cpInfoOffsets[i] - 1, i.e. its cp_info's tag field is given by b[cpInfoOffsets[i] - + * 1]. + */ + private final int[] cpInfoOffsets; + /** + * The String objects corresponding to the CONSTANT_Utf8 constant pool items. This cache avoids + * multiple parsing of a given CONSTANT_Utf8 constant pool item. + */ + private final String[] constantUtf8Values; + /** + * The ConstantDynamic objects corresponding to the CONSTANT_Dynamic constant pool items. This + * cache avoids multiple parsing of a given CONSTANT_Dynamic constant pool item. + */ + private final ConstantDynamic[] constantDynamicValues; + /** + * The start offsets in {@link #classFileBuffer} of each element of the bootstrap_methods array + * (in the BootstrapMethods attribute). + * + * @see JVMS + * 4.7.23 + */ + private final int[] bootstrapMethodOffsets; + /** + * A conservative estimate of the maximum length of the strings contained in the constant pool of + * the class. + */ + private final int maxStringLength; + + // ----------------------------------------------------------------------------------------------- + // Constructors + // ----------------------------------------------------------------------------------------------- + + /** + * Constructs a new {@link ClassReader} object. + * + * @param classFile the JVMS ClassFile structure to be read. + */ + public ClassReader(final byte[] classFile) { + this(classFile, 0, classFile.length); + } + + /** + * Constructs a new {@link ClassReader} object. + * + * @param classFileBuffer a byte array containing the JVMS ClassFile structure to be read. + * @param classFileOffset the offset in byteBuffer of the first byte of the ClassFile to be read. + * @param classFileLength the length in bytes of the ClassFile to be read. + */ + public ClassReader( + final byte[] classFileBuffer, + final int classFileOffset, + final int classFileLength) { // NOPMD(UnusedFormalParameter) used for backward compatibility. + this(classFileBuffer, classFileOffset, /* checkClassVersion = */ true); + } + + /** + * Constructs a new {@link ClassReader} object. This internal constructor must not be exposed + * as a public API. + * + * @param classFileBuffer a byte array containing the JVMS ClassFile structure to be read. + * @param classFileOffset the offset in byteBuffer of the first byte of the ClassFile to be read. + * @param checkClassVersion whether to check the class version or not. + */ + ClassReader( + final byte[] classFileBuffer, final int classFileOffset, final boolean checkClassVersion) { + this.classFileBuffer = classFileBuffer; + this.b = classFileBuffer; + // Check the class' major_version. This field is after the magic and minor_version fields, which + // use 4 and 2 bytes respectively. + if (checkClassVersion && readShort(classFileOffset + 6) > Opcodes.V16) { + throw new IllegalArgumentException( + "Unsupported class file major version " + readShort(classFileOffset + 6)); + } + // Create the constant pool arrays. The constant_pool_count field is after the magic, + // minor_version and major_version fields, which use 4, 2 and 2 bytes respectively. + int constantPoolCount = readUnsignedShort(classFileOffset + 8); + cpInfoOffsets = new int[constantPoolCount]; + constantUtf8Values = new String[constantPoolCount]; + // Compute the offset of each constant pool entry, as well as a conservative estimate of the + // maximum length of the constant pool strings. The first constant pool entry is after the + // magic, minor_version, major_version and constant_pool_count fields, which use 4, 2, 2 and 2 + // bytes respectively. + int currentCpInfoIndex = 1; + int currentCpInfoOffset = classFileOffset + 10; + int currentMaxStringLength = 0; + boolean hasBootstrapMethods = false; + boolean hasConstantDynamic = false; + // The offset of the other entries depend on the total size of all the previous entries. + while (currentCpInfoIndex < constantPoolCount) { + cpInfoOffsets[currentCpInfoIndex++] = currentCpInfoOffset + 1; + int cpInfoSize; + switch (classFileBuffer[currentCpInfoOffset]) { + case Symbol.CONSTANT_FIELDREF_TAG: + case Symbol.CONSTANT_METHODREF_TAG: + case Symbol.CONSTANT_INTERFACE_METHODREF_TAG: + case Symbol.CONSTANT_INTEGER_TAG: + case Symbol.CONSTANT_FLOAT_TAG: + case Symbol.CONSTANT_NAME_AND_TYPE_TAG: + cpInfoSize = 5; + break; + case Symbol.CONSTANT_DYNAMIC_TAG: + cpInfoSize = 5; + hasBootstrapMethods = true; + hasConstantDynamic = true; + break; + case Symbol.CONSTANT_INVOKE_DYNAMIC_TAG: + cpInfoSize = 5; + hasBootstrapMethods = true; + break; + case Symbol.CONSTANT_LONG_TAG: + case Symbol.CONSTANT_DOUBLE_TAG: + cpInfoSize = 9; + currentCpInfoIndex++; + break; + case Symbol.CONSTANT_UTF8_TAG: + cpInfoSize = 3 + readUnsignedShort(currentCpInfoOffset + 1); + if (cpInfoSize > currentMaxStringLength) { + // The size in bytes of this CONSTANT_Utf8 structure provides a conservative estimate + // of the length in characters of the corresponding string, and is much cheaper to + // compute than this exact length. + currentMaxStringLength = cpInfoSize; + } + break; + case Symbol.CONSTANT_METHOD_HANDLE_TAG: + cpInfoSize = 4; + break; + case Symbol.CONSTANT_CLASS_TAG: + case Symbol.CONSTANT_STRING_TAG: + case Symbol.CONSTANT_METHOD_TYPE_TAG: + case Symbol.CONSTANT_PACKAGE_TAG: + case Symbol.CONSTANT_MODULE_TAG: + cpInfoSize = 3; + break; + default: + throw new IllegalArgumentException(); + } + currentCpInfoOffset += cpInfoSize; + } + maxStringLength = currentMaxStringLength; + // The Classfile's access_flags field is just after the last constant pool entry. + header = currentCpInfoOffset; + + // Allocate the cache of ConstantDynamic values, if there is at least one. + constantDynamicValues = hasConstantDynamic ? new ConstantDynamic[constantPoolCount] : null; + + // Read the BootstrapMethods attribute, if any (only get the offset of each method). + bootstrapMethodOffsets = + hasBootstrapMethods ? readBootstrapMethodsAttribute(currentMaxStringLength) : null; + } + + /** + * Constructs a new {@link ClassReader} object. + * + * @param inputStream an input stream of the JVMS ClassFile structure to be read. This input + * stream must contain nothing more than the ClassFile structure itself. It is read from its + * current position to its end. + * @throws IOException if a problem occurs during reading. + */ + public ClassReader(final InputStream inputStream) throws IOException { + this(readStream(inputStream, false)); + } + + /** + * Constructs a new {@link ClassReader} object. + * + * @param className the fully qualified name of the class to be read. The ClassFile structure is + * retrieved with the current class loader's {@link ClassLoader#getSystemResourceAsStream}. + * @throws IOException if an exception occurs during reading. + */ + public ClassReader(final String className) throws IOException { + this( + readStream( + ClassLoader.getSystemResourceAsStream(className.replace('.', '/') + ".class"), true)); + } + + /** + * Reads the given input stream and returns its content as a byte array. + * + * @param inputStream an input stream. + * @param close true to close the input stream after reading. + * @return the content of the given input stream. + * @throws IOException if a problem occurs during reading. + */ + private static byte[] readStream(final InputStream inputStream, final boolean close) + throws IOException { + if (inputStream == null) { + throw new IOException("Class not found"); + } + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + byte[] data = new byte[INPUT_STREAM_DATA_CHUNK_SIZE]; + int bytesRead; + while ((bytesRead = inputStream.read(data, 0, data.length)) != -1) { + outputStream.write(data, 0, bytesRead); + } + outputStream.flush(); + return outputStream.toByteArray(); + } finally { + if (close) { + inputStream.close(); + } + } + } + + // ----------------------------------------------------------------------------------------------- + // Accessors + // ----------------------------------------------------------------------------------------------- + + /** + * Returns the class's access flags (see {@link Opcodes}). This value may not reflect Deprecated + * and Synthetic flags when bytecode is before 1.5 and those flags are represented by attributes. + * + * @return the class access flags. + * @see ClassVisitor#visit(int, int, String, String, String, String[]) + */ + public int getAccess() { + return readUnsignedShort(header); + } + + /** + * Returns the internal name of the class (see {@link Type#getInternalName()}). + * + * @return the internal class name. + * @see ClassVisitor#visit(int, int, String, String, String, String[]) + */ + public String getClassName() { + // this_class is just after the access_flags field (using 2 bytes). + return readClass(header + 2, new char[maxStringLength]); + } + + /** + * Returns the internal of name of the super class (see {@link Type#getInternalName()}). For + * interfaces, the super class is {@link Object}. + * + * @return the internal name of the super class, or {@literal null} for {@link Object} class. + * @see ClassVisitor#visit(int, int, String, String, String, String[]) + */ + public String getSuperName() { + // super_class is after the access_flags and this_class fields (2 bytes each). + return readClass(header + 4, new char[maxStringLength]); + } + + /** + * Returns the internal names of the implemented interfaces (see {@link Type#getInternalName()}). + * + * @return the internal names of the directly implemented interfaces. Inherited implemented + * interfaces are not returned. + * @see ClassVisitor#visit(int, int, String, String, String, String[]) + */ + public String[] getInterfaces() { + // interfaces_count is after the access_flags, this_class and super_class fields (2 bytes each). + int currentOffset = header + 6; + int interfacesCount = readUnsignedShort(currentOffset); + String[] interfaces = new String[interfacesCount]; + if (interfacesCount > 0) { + char[] charBuffer = new char[maxStringLength]; + for (int i = 0; i < interfacesCount; ++i) { + currentOffset += 2; + interfaces[i] = readClass(currentOffset, charBuffer); + } + } + return interfaces; + } + + // ----------------------------------------------------------------------------------------------- + // Public methods + // ----------------------------------------------------------------------------------------------- + + /** + * Makes the given visitor visit the JVMS ClassFile structure passed to the constructor of this + * {@link ClassReader}. + * + * @param classVisitor the visitor that must visit this class. + * @param parsingOptions the options to use to parse this class. One or more of {@link + * #SKIP_CODE}, {@link #SKIP_DEBUG}, {@link #SKIP_FRAMES} or {@link #EXPAND_FRAMES}. + */ + public void accept(final ClassVisitor classVisitor, final int parsingOptions) { + accept(classVisitor, new Attribute[0], parsingOptions); + } + + /** + * Makes the given visitor visit the JVMS ClassFile structure passed to the constructor of this + * {@link ClassReader}. + * + * @param classVisitor the visitor that must visit this class. + * @param attributePrototypes prototypes of the attributes that must be parsed during the visit of + * the class. Any attribute whose type is not equal to the type of one the prototypes will not + * be parsed: its byte array value will be passed unchanged to the ClassWriter. This may + * corrupt it if this value contains references to the constant pool, or has syntactic or + * semantic links with a class element that has been transformed by a class adapter between + * the reader and the writer. + * @param parsingOptions the options to use to parse this class. One or more of {@link + * #SKIP_CODE}, {@link #SKIP_DEBUG}, {@link #SKIP_FRAMES} or {@link #EXPAND_FRAMES}. + */ + public void accept( + final ClassVisitor classVisitor, + final Attribute[] attributePrototypes, + final int parsingOptions) { + Context context = new Context(); + context.attributePrototypes = attributePrototypes; + context.parsingOptions = parsingOptions; + context.charBuffer = new char[maxStringLength]; + + // Read the access_flags, this_class, super_class, interface_count and interfaces fields. + char[] charBuffer = context.charBuffer; + int currentOffset = header; + int accessFlags = readUnsignedShort(currentOffset); + String thisClass = readClass(currentOffset + 2, charBuffer); + String superClass = readClass(currentOffset + 4, charBuffer); + String[] interfaces = new String[readUnsignedShort(currentOffset + 6)]; + currentOffset += 8; + for (int i = 0; i < interfaces.length; ++i) { + interfaces[i] = readClass(currentOffset, charBuffer); + currentOffset += 2; + } + + // Read the class attributes (the variables are ordered as in Section 4.7 of the JVMS). + // Attribute offsets exclude the attribute_name_index and attribute_length fields. + // - The offset of the InnerClasses attribute, or 0. + int innerClassesOffset = 0; + // - The offset of the EnclosingMethod attribute, or 0. + int enclosingMethodOffset = 0; + // - The string corresponding to the Signature attribute, or null. + String signature = null; + // - The string corresponding to the SourceFile attribute, or null. + String sourceFile = null; + // - The string corresponding to the SourceDebugExtension attribute, or null. + String sourceDebugExtension = null; + // - The offset of the RuntimeVisibleAnnotations attribute, or 0. + int runtimeVisibleAnnotationsOffset = 0; + // - The offset of the RuntimeInvisibleAnnotations attribute, or 0. + int runtimeInvisibleAnnotationsOffset = 0; + // - The offset of the RuntimeVisibleTypeAnnotations attribute, or 0. + int runtimeVisibleTypeAnnotationsOffset = 0; + // - The offset of the RuntimeInvisibleTypeAnnotations attribute, or 0. + int runtimeInvisibleTypeAnnotationsOffset = 0; + // - The offset of the Module attribute, or 0. + int moduleOffset = 0; + // - The offset of the ModulePackages attribute, or 0. + int modulePackagesOffset = 0; + // - The string corresponding to the ModuleMainClass attribute, or null. + String moduleMainClass = null; + // - The string corresponding to the NestHost attribute, or null. + String nestHostClass = null; + // - The offset of the NestMembers attribute, or 0. + int nestMembersOffset = 0; + // - The offset of the PermittedSubclasses attribute, or 0 + int permittedSubclassesOffset = 0; + // - The offset of the Record attribute, or 0. + int recordOffset = 0; + // - The non standard attributes (linked with their {@link Attribute#nextAttribute} field). + // This list in the reverse order or their order in the ClassFile structure. + Attribute attributes = null; + + int currentAttributeOffset = getFirstAttributeOffset(); + for (int i = readUnsignedShort(currentAttributeOffset - 2); i > 0; --i) { + // Read the attribute_info's attribute_name and attribute_length fields. + String attributeName = readUTF8(currentAttributeOffset, charBuffer); + int attributeLength = readInt(currentAttributeOffset + 2); + currentAttributeOffset += 6; + // The tests are sorted in decreasing frequency order (based on frequencies observed on + // typical classes). + if (Constants.SOURCE_FILE.equals(attributeName)) { + sourceFile = readUTF8(currentAttributeOffset, charBuffer); + } else if (Constants.INNER_CLASSES.equals(attributeName)) { + innerClassesOffset = currentAttributeOffset; + } else if (Constants.ENCLOSING_METHOD.equals(attributeName)) { + enclosingMethodOffset = currentAttributeOffset; + } else if (Constants.NEST_HOST.equals(attributeName)) { + nestHostClass = readClass(currentAttributeOffset, charBuffer); + } else if (Constants.NEST_MEMBERS.equals(attributeName)) { + nestMembersOffset = currentAttributeOffset; + } else if (Constants.PERMITTED_SUBCLASSES.equals(attributeName)) { + permittedSubclassesOffset = currentAttributeOffset; + } else if (Constants.SIGNATURE.equals(attributeName)) { + signature = readUTF8(currentAttributeOffset, charBuffer); + } else if (Constants.RUNTIME_VISIBLE_ANNOTATIONS.equals(attributeName)) { + runtimeVisibleAnnotationsOffset = currentAttributeOffset; + } else if (Constants.RUNTIME_VISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) { + runtimeVisibleTypeAnnotationsOffset = currentAttributeOffset; + } else if (Constants.DEPRECATED.equals(attributeName)) { + accessFlags |= Opcodes.ACC_DEPRECATED; + } else if (Constants.SYNTHETIC.equals(attributeName)) { + accessFlags |= Opcodes.ACC_SYNTHETIC; + } else if (Constants.SOURCE_DEBUG_EXTENSION.equals(attributeName)) { + sourceDebugExtension = + readUtf(currentAttributeOffset, attributeLength, new char[attributeLength]); + } else if (Constants.RUNTIME_INVISIBLE_ANNOTATIONS.equals(attributeName)) { + runtimeInvisibleAnnotationsOffset = currentAttributeOffset; + } else if (Constants.RUNTIME_INVISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) { + runtimeInvisibleTypeAnnotationsOffset = currentAttributeOffset; + } else if (Constants.RECORD.equals(attributeName)) { + recordOffset = currentAttributeOffset; + accessFlags |= Opcodes.ACC_RECORD; + } else if (Constants.MODULE.equals(attributeName)) { + moduleOffset = currentAttributeOffset; + } else if (Constants.MODULE_MAIN_CLASS.equals(attributeName)) { + moduleMainClass = readClass(currentAttributeOffset, charBuffer); + } else if (Constants.MODULE_PACKAGES.equals(attributeName)) { + modulePackagesOffset = currentAttributeOffset; + } else if (!Constants.BOOTSTRAP_METHODS.equals(attributeName)) { + // The BootstrapMethods attribute is read in the constructor. + Attribute attribute = + readAttribute( + attributePrototypes, + attributeName, + currentAttributeOffset, + attributeLength, + charBuffer, + -1, + null); + attribute.nextAttribute = attributes; + attributes = attribute; + } + currentAttributeOffset += attributeLength; + } + + // Visit the class declaration. The minor_version and major_version fields start 6 bytes before + // the first constant pool entry, which itself starts at cpInfoOffsets[1] - 1 (by definition). + classVisitor.visit( + readInt(cpInfoOffsets[1] - 7), accessFlags, thisClass, signature, superClass, interfaces); + + // Visit the SourceFile and SourceDebugExtenstion attributes. + if ((parsingOptions & SKIP_DEBUG) == 0 + && (sourceFile != null || sourceDebugExtension != null)) { + classVisitor.visitSource(sourceFile, sourceDebugExtension); + } + + // Visit the Module, ModulePackages and ModuleMainClass attributes. + if (moduleOffset != 0) { + readModuleAttributes( + classVisitor, context, moduleOffset, modulePackagesOffset, moduleMainClass); + } + + // Visit the NestHost attribute. + if (nestHostClass != null) { + classVisitor.visitNestHost(nestHostClass); + } + + // Visit the EnclosingMethod attribute. + if (enclosingMethodOffset != 0) { + String className = readClass(enclosingMethodOffset, charBuffer); + int methodIndex = readUnsignedShort(enclosingMethodOffset + 2); + String name = methodIndex == 0 ? null : readUTF8(cpInfoOffsets[methodIndex], charBuffer); + String type = methodIndex == 0 ? null : readUTF8(cpInfoOffsets[methodIndex] + 2, charBuffer); + classVisitor.visitOuterClass(className, name, type); + } + + // Visit the RuntimeVisibleAnnotations attribute. + if (runtimeVisibleAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeVisibleAnnotationsOffset); + int currentAnnotationOffset = runtimeVisibleAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + classVisitor.visitAnnotation(annotationDescriptor, /* visible = */ true), + currentAnnotationOffset, + /* named = */ true, + charBuffer); + } + } + + // Visit the RuntimeInvisibleAnnotations attribute. + if (runtimeInvisibleAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeInvisibleAnnotationsOffset); + int currentAnnotationOffset = runtimeInvisibleAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + classVisitor.visitAnnotation(annotationDescriptor, /* visible = */ false), + currentAnnotationOffset, + /* named = */ true, + charBuffer); + } + } + + // Visit the RuntimeVisibleTypeAnnotations attribute. + if (runtimeVisibleTypeAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeVisibleTypeAnnotationsOffset); + int currentAnnotationOffset = runtimeVisibleTypeAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the target_type, target_info and target_path fields. + currentAnnotationOffset = readTypeAnnotationTarget(context, currentAnnotationOffset); + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + classVisitor.visitTypeAnnotation( + context.currentTypeAnnotationTarget, + context.currentTypeAnnotationTargetPath, + annotationDescriptor, + /* visible = */ true), + currentAnnotationOffset, + /* named = */ true, + charBuffer); + } + } + + // Visit the RuntimeInvisibleTypeAnnotations attribute. + if (runtimeInvisibleTypeAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeInvisibleTypeAnnotationsOffset); + int currentAnnotationOffset = runtimeInvisibleTypeAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the target_type, target_info and target_path fields. + currentAnnotationOffset = readTypeAnnotationTarget(context, currentAnnotationOffset); + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + classVisitor.visitTypeAnnotation( + context.currentTypeAnnotationTarget, + context.currentTypeAnnotationTargetPath, + annotationDescriptor, + /* visible = */ false), + currentAnnotationOffset, + /* named = */ true, + charBuffer); + } + } + + // Visit the non standard attributes. + while (attributes != null) { + // Copy and reset the nextAttribute field so that it can also be used in ClassWriter. + Attribute nextAttribute = attributes.nextAttribute; + attributes.nextAttribute = null; + classVisitor.visitAttribute(attributes); + attributes = nextAttribute; + } + + // Visit the NestedMembers attribute. + if (nestMembersOffset != 0) { + int numberOfNestMembers = readUnsignedShort(nestMembersOffset); + int currentNestMemberOffset = nestMembersOffset + 2; + while (numberOfNestMembers-- > 0) { + classVisitor.visitNestMember(readClass(currentNestMemberOffset, charBuffer)); + currentNestMemberOffset += 2; + } + } + + // Visit the PermittedSubclasses attribute. + if (permittedSubclassesOffset != 0) { + int numberOfPermittedSubclasses = readUnsignedShort(permittedSubclassesOffset); + int currentPermittedSubclassesOffset = permittedSubclassesOffset + 2; + while (numberOfPermittedSubclasses-- > 0) { + classVisitor.visitPermittedSubclass( + readClass(currentPermittedSubclassesOffset, charBuffer)); + currentPermittedSubclassesOffset += 2; + } + } + + // Visit the InnerClasses attribute. + if (innerClassesOffset != 0) { + int numberOfClasses = readUnsignedShort(innerClassesOffset); + int currentClassesOffset = innerClassesOffset + 2; + while (numberOfClasses-- > 0) { + classVisitor.visitInnerClass( + readClass(currentClassesOffset, charBuffer), + readClass(currentClassesOffset + 2, charBuffer), + readUTF8(currentClassesOffset + 4, charBuffer), + readUnsignedShort(currentClassesOffset + 6)); + currentClassesOffset += 8; + } + } + + // Visit Record components. + if (recordOffset != 0) { + int recordComponentsCount = readUnsignedShort(recordOffset); + recordOffset += 2; + while (recordComponentsCount-- > 0) { + recordOffset = readRecordComponent(classVisitor, context, recordOffset); + } + } + + // Visit the fields and methods. + int fieldsCount = readUnsignedShort(currentOffset); + currentOffset += 2; + while (fieldsCount-- > 0) { + currentOffset = readField(classVisitor, context, currentOffset); + } + int methodsCount = readUnsignedShort(currentOffset); + currentOffset += 2; + while (methodsCount-- > 0) { + currentOffset = readMethod(classVisitor, context, currentOffset); + } + + // Visit the end of the class. + classVisitor.visitEnd(); + } + + // ---------------------------------------------------------------------------------------------- + // Methods to parse modules, fields and methods + // ---------------------------------------------------------------------------------------------- + + /** + * Reads the Module, ModulePackages and ModuleMainClass attributes and visit them. + * + * @param classVisitor the current class visitor + * @param context information about the class being parsed. + * @param moduleOffset the offset of the Module attribute (excluding the attribute_info's + * attribute_name_index and attribute_length fields). + * @param modulePackagesOffset the offset of the ModulePackages attribute (excluding the + * attribute_info's attribute_name_index and attribute_length fields), or 0. + * @param moduleMainClass the string corresponding to the ModuleMainClass attribute, or {@literal + * null}. + */ + private void readModuleAttributes( + final ClassVisitor classVisitor, + final Context context, + final int moduleOffset, + final int modulePackagesOffset, + final String moduleMainClass) { + char[] buffer = context.charBuffer; + + // Read the module_name_index, module_flags and module_version_index fields and visit them. + int currentOffset = moduleOffset; + String moduleName = readModule(currentOffset, buffer); + int moduleFlags = readUnsignedShort(currentOffset + 2); + String moduleVersion = readUTF8(currentOffset + 4, buffer); + currentOffset += 6; + ModuleVisitor moduleVisitor = classVisitor.visitModule(moduleName, moduleFlags, moduleVersion); + if (moduleVisitor == null) { + return; + } + + // Visit the ModuleMainClass attribute. + if (moduleMainClass != null) { + moduleVisitor.visitMainClass(moduleMainClass); + } + + // Visit the ModulePackages attribute. + if (modulePackagesOffset != 0) { + int packageCount = readUnsignedShort(modulePackagesOffset); + int currentPackageOffset = modulePackagesOffset + 2; + while (packageCount-- > 0) { + moduleVisitor.visitPackage(readPackage(currentPackageOffset, buffer)); + currentPackageOffset += 2; + } + } + + // Read the 'requires_count' and 'requires' fields. + int requiresCount = readUnsignedShort(currentOffset); + currentOffset += 2; + while (requiresCount-- > 0) { + // Read the requires_index, requires_flags and requires_version fields and visit them. + String requires = readModule(currentOffset, buffer); + int requiresFlags = readUnsignedShort(currentOffset + 2); + String requiresVersion = readUTF8(currentOffset + 4, buffer); + currentOffset += 6; + moduleVisitor.visitRequire(requires, requiresFlags, requiresVersion); + } + + // Read the 'exports_count' and 'exports' fields. + int exportsCount = readUnsignedShort(currentOffset); + currentOffset += 2; + while (exportsCount-- > 0) { + // Read the exports_index, exports_flags, exports_to_count and exports_to_index fields + // and visit them. + String exports = readPackage(currentOffset, buffer); + int exportsFlags = readUnsignedShort(currentOffset + 2); + int exportsToCount = readUnsignedShort(currentOffset + 4); + currentOffset += 6; + String[] exportsTo = null; + if (exportsToCount != 0) { + exportsTo = new String[exportsToCount]; + for (int i = 0; i < exportsToCount; ++i) { + exportsTo[i] = readModule(currentOffset, buffer); + currentOffset += 2; + } + } + moduleVisitor.visitExport(exports, exportsFlags, exportsTo); + } + + // Reads the 'opens_count' and 'opens' fields. + int opensCount = readUnsignedShort(currentOffset); + currentOffset += 2; + while (opensCount-- > 0) { + // Read the opens_index, opens_flags, opens_to_count and opens_to_index fields and visit them. + String opens = readPackage(currentOffset, buffer); + int opensFlags = readUnsignedShort(currentOffset + 2); + int opensToCount = readUnsignedShort(currentOffset + 4); + currentOffset += 6; + String[] opensTo = null; + if (opensToCount != 0) { + opensTo = new String[opensToCount]; + for (int i = 0; i < opensToCount; ++i) { + opensTo[i] = readModule(currentOffset, buffer); + currentOffset += 2; + } + } + moduleVisitor.visitOpen(opens, opensFlags, opensTo); + } + + // Read the 'uses_count' and 'uses' fields. + int usesCount = readUnsignedShort(currentOffset); + currentOffset += 2; + while (usesCount-- > 0) { + moduleVisitor.visitUse(readClass(currentOffset, buffer)); + currentOffset += 2; + } + + // Read the 'provides_count' and 'provides' fields. + int providesCount = readUnsignedShort(currentOffset); + currentOffset += 2; + while (providesCount-- > 0) { + // Read the provides_index, provides_with_count and provides_with_index fields and visit them. + String provides = readClass(currentOffset, buffer); + int providesWithCount = readUnsignedShort(currentOffset + 2); + currentOffset += 4; + String[] providesWith = new String[providesWithCount]; + for (int i = 0; i < providesWithCount; ++i) { + providesWith[i] = readClass(currentOffset, buffer); + currentOffset += 2; + } + moduleVisitor.visitProvide(provides, providesWith); + } + + // Visit the end of the module attributes. + moduleVisitor.visitEnd(); + } + + /** + * Reads a record component and visit it. + * + * @param classVisitor the current class visitor + * @param context information about the class being parsed. + * @param recordComponentOffset the offset of the current record component. + * @return the offset of the first byte following the record component. + */ + private int readRecordComponent( + final ClassVisitor classVisitor, final Context context, final int recordComponentOffset) { + char[] charBuffer = context.charBuffer; + + int currentOffset = recordComponentOffset; + String name = readUTF8(currentOffset, charBuffer); + String descriptor = readUTF8(currentOffset + 2, charBuffer); + currentOffset += 4; + + // Read the record component attributes (the variables are ordered as in Section 4.7 of the + // JVMS). + + // Attribute offsets exclude the attribute_name_index and attribute_length fields. + // - The string corresponding to the Signature attribute, or null. + String signature = null; + // - The offset of the RuntimeVisibleAnnotations attribute, or 0. + int runtimeVisibleAnnotationsOffset = 0; + // - The offset of the RuntimeInvisibleAnnotations attribute, or 0. + int runtimeInvisibleAnnotationsOffset = 0; + // - The offset of the RuntimeVisibleTypeAnnotations attribute, or 0. + int runtimeVisibleTypeAnnotationsOffset = 0; + // - The offset of the RuntimeInvisibleTypeAnnotations attribute, or 0. + int runtimeInvisibleTypeAnnotationsOffset = 0; + // - The non standard attributes (linked with their {@link Attribute#nextAttribute} field). + // This list in the reverse order or their order in the ClassFile structure. + Attribute attributes = null; + + int attributesCount = readUnsignedShort(currentOffset); + currentOffset += 2; + while (attributesCount-- > 0) { + // Read the attribute_info's attribute_name and attribute_length fields. + String attributeName = readUTF8(currentOffset, charBuffer); + int attributeLength = readInt(currentOffset + 2); + currentOffset += 6; + // The tests are sorted in decreasing frequency order (based on frequencies observed on + // typical classes). + if (Constants.SIGNATURE.equals(attributeName)) { + signature = readUTF8(currentOffset, charBuffer); + } else if (Constants.RUNTIME_VISIBLE_ANNOTATIONS.equals(attributeName)) { + runtimeVisibleAnnotationsOffset = currentOffset; + } else if (Constants.RUNTIME_VISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) { + runtimeVisibleTypeAnnotationsOffset = currentOffset; + } else if (Constants.RUNTIME_INVISIBLE_ANNOTATIONS.equals(attributeName)) { + runtimeInvisibleAnnotationsOffset = currentOffset; + } else if (Constants.RUNTIME_INVISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) { + runtimeInvisibleTypeAnnotationsOffset = currentOffset; + } else { + Attribute attribute = + readAttribute( + context.attributePrototypes, + attributeName, + currentOffset, + attributeLength, + charBuffer, + -1, + null); + attribute.nextAttribute = attributes; + attributes = attribute; + } + currentOffset += attributeLength; + } + + RecordComponentVisitor recordComponentVisitor = + classVisitor.visitRecordComponent(name, descriptor, signature); + if (recordComponentVisitor == null) { + return currentOffset; + } + + // Visit the RuntimeVisibleAnnotations attribute. + if (runtimeVisibleAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeVisibleAnnotationsOffset); + int currentAnnotationOffset = runtimeVisibleAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + recordComponentVisitor.visitAnnotation(annotationDescriptor, /* visible = */ true), + currentAnnotationOffset, + /* named = */ true, + charBuffer); + } + } + + // Visit the RuntimeInvisibleAnnotations attribute. + if (runtimeInvisibleAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeInvisibleAnnotationsOffset); + int currentAnnotationOffset = runtimeInvisibleAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + recordComponentVisitor.visitAnnotation(annotationDescriptor, /* visible = */ false), + currentAnnotationOffset, + /* named = */ true, + charBuffer); + } + } + + // Visit the RuntimeVisibleTypeAnnotations attribute. + if (runtimeVisibleTypeAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeVisibleTypeAnnotationsOffset); + int currentAnnotationOffset = runtimeVisibleTypeAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the target_type, target_info and target_path fields. + currentAnnotationOffset = readTypeAnnotationTarget(context, currentAnnotationOffset); + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + recordComponentVisitor.visitTypeAnnotation( + context.currentTypeAnnotationTarget, + context.currentTypeAnnotationTargetPath, + annotationDescriptor, + /* visible = */ true), + currentAnnotationOffset, + /* named = */ true, + charBuffer); + } + } + + // Visit the RuntimeInvisibleTypeAnnotations attribute. + if (runtimeInvisibleTypeAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeInvisibleTypeAnnotationsOffset); + int currentAnnotationOffset = runtimeInvisibleTypeAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the target_type, target_info and target_path fields. + currentAnnotationOffset = readTypeAnnotationTarget(context, currentAnnotationOffset); + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + recordComponentVisitor.visitTypeAnnotation( + context.currentTypeAnnotationTarget, + context.currentTypeAnnotationTargetPath, + annotationDescriptor, + /* visible = */ false), + currentAnnotationOffset, + /* named = */ true, + charBuffer); + } + } + + // Visit the non standard attributes. + while (attributes != null) { + // Copy and reset the nextAttribute field so that it can also be used in FieldWriter. + Attribute nextAttribute = attributes.nextAttribute; + attributes.nextAttribute = null; + recordComponentVisitor.visitAttribute(attributes); + attributes = nextAttribute; + } + + // Visit the end of the field. + recordComponentVisitor.visitEnd(); + return currentOffset; + } + + /** + * Reads a JVMS field_info structure and makes the given visitor visit it. + * + * @param classVisitor the visitor that must visit the field. + * @param context information about the class being parsed. + * @param fieldInfoOffset the start offset of the field_info structure. + * @return the offset of the first byte following the field_info structure. + */ + private int readField( + final ClassVisitor classVisitor, final Context context, final int fieldInfoOffset) { + char[] charBuffer = context.charBuffer; + + // Read the access_flags, name_index and descriptor_index fields. + int currentOffset = fieldInfoOffset; + int accessFlags = readUnsignedShort(currentOffset); + String name = readUTF8(currentOffset + 2, charBuffer); + String descriptor = readUTF8(currentOffset + 4, charBuffer); + currentOffset += 6; + + // Read the field attributes (the variables are ordered as in Section 4.7 of the JVMS). + // Attribute offsets exclude the attribute_name_index and attribute_length fields. + // - The value corresponding to the ConstantValue attribute, or null. + Object constantValue = null; + // - The string corresponding to the Signature attribute, or null. + String signature = null; + // - The offset of the RuntimeVisibleAnnotations attribute, or 0. + int runtimeVisibleAnnotationsOffset = 0; + // - The offset of the RuntimeInvisibleAnnotations attribute, or 0. + int runtimeInvisibleAnnotationsOffset = 0; + // - The offset of the RuntimeVisibleTypeAnnotations attribute, or 0. + int runtimeVisibleTypeAnnotationsOffset = 0; + // - The offset of the RuntimeInvisibleTypeAnnotations attribute, or 0. + int runtimeInvisibleTypeAnnotationsOffset = 0; + // - The non standard attributes (linked with their {@link Attribute#nextAttribute} field). + // This list in the reverse order or their order in the ClassFile structure. + Attribute attributes = null; + + int attributesCount = readUnsignedShort(currentOffset); + currentOffset += 2; + while (attributesCount-- > 0) { + // Read the attribute_info's attribute_name and attribute_length fields. + String attributeName = readUTF8(currentOffset, charBuffer); + int attributeLength = readInt(currentOffset + 2); + currentOffset += 6; + // The tests are sorted in decreasing frequency order (based on frequencies observed on + // typical classes). + if (Constants.CONSTANT_VALUE.equals(attributeName)) { + int constantvalueIndex = readUnsignedShort(currentOffset); + constantValue = constantvalueIndex == 0 ? null : readConst(constantvalueIndex, charBuffer); + } else if (Constants.SIGNATURE.equals(attributeName)) { + signature = readUTF8(currentOffset, charBuffer); + } else if (Constants.DEPRECATED.equals(attributeName)) { + accessFlags |= Opcodes.ACC_DEPRECATED; + } else if (Constants.SYNTHETIC.equals(attributeName)) { + accessFlags |= Opcodes.ACC_SYNTHETIC; + } else if (Constants.RUNTIME_VISIBLE_ANNOTATIONS.equals(attributeName)) { + runtimeVisibleAnnotationsOffset = currentOffset; + } else if (Constants.RUNTIME_VISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) { + runtimeVisibleTypeAnnotationsOffset = currentOffset; + } else if (Constants.RUNTIME_INVISIBLE_ANNOTATIONS.equals(attributeName)) { + runtimeInvisibleAnnotationsOffset = currentOffset; + } else if (Constants.RUNTIME_INVISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) { + runtimeInvisibleTypeAnnotationsOffset = currentOffset; + } else { + Attribute attribute = + readAttribute( + context.attributePrototypes, + attributeName, + currentOffset, + attributeLength, + charBuffer, + -1, + null); + attribute.nextAttribute = attributes; + attributes = attribute; + } + currentOffset += attributeLength; + } + + // Visit the field declaration. + FieldVisitor fieldVisitor = + classVisitor.visitField(accessFlags, name, descriptor, signature, constantValue); + if (fieldVisitor == null) { + return currentOffset; + } + + // Visit the RuntimeVisibleAnnotations attribute. + if (runtimeVisibleAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeVisibleAnnotationsOffset); + int currentAnnotationOffset = runtimeVisibleAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + fieldVisitor.visitAnnotation(annotationDescriptor, /* visible = */ true), + currentAnnotationOffset, + /* named = */ true, + charBuffer); + } + } + + // Visit the RuntimeInvisibleAnnotations attribute. + if (runtimeInvisibleAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeInvisibleAnnotationsOffset); + int currentAnnotationOffset = runtimeInvisibleAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + fieldVisitor.visitAnnotation(annotationDescriptor, /* visible = */ false), + currentAnnotationOffset, + /* named = */ true, + charBuffer); + } + } + + // Visit the RuntimeVisibleTypeAnnotations attribute. + if (runtimeVisibleTypeAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeVisibleTypeAnnotationsOffset); + int currentAnnotationOffset = runtimeVisibleTypeAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the target_type, target_info and target_path fields. + currentAnnotationOffset = readTypeAnnotationTarget(context, currentAnnotationOffset); + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + fieldVisitor.visitTypeAnnotation( + context.currentTypeAnnotationTarget, + context.currentTypeAnnotationTargetPath, + annotationDescriptor, + /* visible = */ true), + currentAnnotationOffset, + /* named = */ true, + charBuffer); + } + } + + // Visit the RuntimeInvisibleTypeAnnotations attribute. + if (runtimeInvisibleTypeAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeInvisibleTypeAnnotationsOffset); + int currentAnnotationOffset = runtimeInvisibleTypeAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the target_type, target_info and target_path fields. + currentAnnotationOffset = readTypeAnnotationTarget(context, currentAnnotationOffset); + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + fieldVisitor.visitTypeAnnotation( + context.currentTypeAnnotationTarget, + context.currentTypeAnnotationTargetPath, + annotationDescriptor, + /* visible = */ false), + currentAnnotationOffset, + /* named = */ true, + charBuffer); + } + } + + // Visit the non standard attributes. + while (attributes != null) { + // Copy and reset the nextAttribute field so that it can also be used in FieldWriter. + Attribute nextAttribute = attributes.nextAttribute; + attributes.nextAttribute = null; + fieldVisitor.visitAttribute(attributes); + attributes = nextAttribute; + } + + // Visit the end of the field. + fieldVisitor.visitEnd(); + return currentOffset; + } + + /** + * Reads a JVMS method_info structure and makes the given visitor visit it. + * + * @param classVisitor the visitor that must visit the method. + * @param context information about the class being parsed. + * @param methodInfoOffset the start offset of the method_info structure. + * @return the offset of the first byte following the method_info structure. + */ + private int readMethod( + final ClassVisitor classVisitor, final Context context, final int methodInfoOffset) { + char[] charBuffer = context.charBuffer; + + // Read the access_flags, name_index and descriptor_index fields. + int currentOffset = methodInfoOffset; + context.currentMethodAccessFlags = readUnsignedShort(currentOffset); + context.currentMethodName = readUTF8(currentOffset + 2, charBuffer); + context.currentMethodDescriptor = readUTF8(currentOffset + 4, charBuffer); + currentOffset += 6; + + // Read the method attributes (the variables are ordered as in Section 4.7 of the JVMS). + // Attribute offsets exclude the attribute_name_index and attribute_length fields. + // - The offset of the Code attribute, or 0. + int codeOffset = 0; + // - The offset of the Exceptions attribute, or 0. + int exceptionsOffset = 0; + // - The strings corresponding to the Exceptions attribute, or null. + String[] exceptions = null; + // - Whether the method has a Synthetic attribute. + boolean synthetic = false; + // - The constant pool index contained in the Signature attribute, or 0. + int signatureIndex = 0; + // - The offset of the RuntimeVisibleAnnotations attribute, or 0. + int runtimeVisibleAnnotationsOffset = 0; + // - The offset of the RuntimeInvisibleAnnotations attribute, or 0. + int runtimeInvisibleAnnotationsOffset = 0; + // - The offset of the RuntimeVisibleParameterAnnotations attribute, or 0. + int runtimeVisibleParameterAnnotationsOffset = 0; + // - The offset of the RuntimeInvisibleParameterAnnotations attribute, or 0. + int runtimeInvisibleParameterAnnotationsOffset = 0; + // - The offset of the RuntimeVisibleTypeAnnotations attribute, or 0. + int runtimeVisibleTypeAnnotationsOffset = 0; + // - The offset of the RuntimeInvisibleTypeAnnotations attribute, or 0. + int runtimeInvisibleTypeAnnotationsOffset = 0; + // - The offset of the AnnotationDefault attribute, or 0. + int annotationDefaultOffset = 0; + // - The offset of the MethodParameters attribute, or 0. + int methodParametersOffset = 0; + // - The non standard attributes (linked with their {@link Attribute#nextAttribute} field). + // This list in the reverse order or their order in the ClassFile structure. + Attribute attributes = null; + + int attributesCount = readUnsignedShort(currentOffset); + currentOffset += 2; + while (attributesCount-- > 0) { + // Read the attribute_info's attribute_name and attribute_length fields. + String attributeName = readUTF8(currentOffset, charBuffer); + int attributeLength = readInt(currentOffset + 2); + currentOffset += 6; + // The tests are sorted in decreasing frequency order (based on frequencies observed on + // typical classes). + if (Constants.CODE.equals(attributeName)) { + if ((context.parsingOptions & SKIP_CODE) == 0) { + codeOffset = currentOffset; + } + } else if (Constants.EXCEPTIONS.equals(attributeName)) { + exceptionsOffset = currentOffset; + exceptions = new String[readUnsignedShort(exceptionsOffset)]; + int currentExceptionOffset = exceptionsOffset + 2; + for (int i = 0; i < exceptions.length; ++i) { + exceptions[i] = readClass(currentExceptionOffset, charBuffer); + currentExceptionOffset += 2; + } + } else if (Constants.SIGNATURE.equals(attributeName)) { + signatureIndex = readUnsignedShort(currentOffset); + } else if (Constants.DEPRECATED.equals(attributeName)) { + context.currentMethodAccessFlags |= Opcodes.ACC_DEPRECATED; + } else if (Constants.RUNTIME_VISIBLE_ANNOTATIONS.equals(attributeName)) { + runtimeVisibleAnnotationsOffset = currentOffset; + } else if (Constants.RUNTIME_VISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) { + runtimeVisibleTypeAnnotationsOffset = currentOffset; + } else if (Constants.ANNOTATION_DEFAULT.equals(attributeName)) { + annotationDefaultOffset = currentOffset; + } else if (Constants.SYNTHETIC.equals(attributeName)) { + synthetic = true; + context.currentMethodAccessFlags |= Opcodes.ACC_SYNTHETIC; + } else if (Constants.RUNTIME_INVISIBLE_ANNOTATIONS.equals(attributeName)) { + runtimeInvisibleAnnotationsOffset = currentOffset; + } else if (Constants.RUNTIME_INVISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) { + runtimeInvisibleTypeAnnotationsOffset = currentOffset; + } else if (Constants.RUNTIME_VISIBLE_PARAMETER_ANNOTATIONS.equals(attributeName)) { + runtimeVisibleParameterAnnotationsOffset = currentOffset; + } else if (Constants.RUNTIME_INVISIBLE_PARAMETER_ANNOTATIONS.equals(attributeName)) { + runtimeInvisibleParameterAnnotationsOffset = currentOffset; + } else if (Constants.METHOD_PARAMETERS.equals(attributeName)) { + methodParametersOffset = currentOffset; + } else { + Attribute attribute = + readAttribute( + context.attributePrototypes, + attributeName, + currentOffset, + attributeLength, + charBuffer, + -1, + null); + attribute.nextAttribute = attributes; + attributes = attribute; + } + currentOffset += attributeLength; + } + + // Visit the method declaration. + MethodVisitor methodVisitor = + classVisitor.visitMethod( + context.currentMethodAccessFlags, + context.currentMethodName, + context.currentMethodDescriptor, + signatureIndex == 0 ? null : readUtf(signatureIndex, charBuffer), + exceptions); + if (methodVisitor == null) { + return currentOffset; + } + + // If the returned MethodVisitor is in fact a MethodWriter, it means there is no method + // adapter between the reader and the writer. In this case, it might be possible to copy + // the method attributes directly into the writer. If so, return early without visiting + // the content of these attributes. + if (methodVisitor instanceof MethodWriter) { + MethodWriter methodWriter = (MethodWriter) methodVisitor; + if (methodWriter.canCopyMethodAttributes( + this, + synthetic, + (context.currentMethodAccessFlags & Opcodes.ACC_DEPRECATED) != 0, + readUnsignedShort(methodInfoOffset + 4), + signatureIndex, + exceptionsOffset)) { + methodWriter.setMethodAttributesSource(methodInfoOffset, currentOffset - methodInfoOffset); + return currentOffset; + } + } + + // Visit the MethodParameters attribute. + if (methodParametersOffset != 0 && (context.parsingOptions & SKIP_DEBUG) == 0) { + int parametersCount = readByte(methodParametersOffset); + int currentParameterOffset = methodParametersOffset + 1; + while (parametersCount-- > 0) { + // Read the name_index and access_flags fields and visit them. + methodVisitor.visitParameter( + readUTF8(currentParameterOffset, charBuffer), + readUnsignedShort(currentParameterOffset + 2)); + currentParameterOffset += 4; + } + } + + // Visit the AnnotationDefault attribute. + if (annotationDefaultOffset != 0) { + AnnotationVisitor annotationVisitor = methodVisitor.visitAnnotationDefault(); + readElementValue(annotationVisitor, annotationDefaultOffset, null, charBuffer); + if (annotationVisitor != null) { + annotationVisitor.visitEnd(); + } + } + + // Visit the RuntimeVisibleAnnotations attribute. + if (runtimeVisibleAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeVisibleAnnotationsOffset); + int currentAnnotationOffset = runtimeVisibleAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + methodVisitor.visitAnnotation(annotationDescriptor, /* visible = */ true), + currentAnnotationOffset, + /* named = */ true, + charBuffer); + } + } + + // Visit the RuntimeInvisibleAnnotations attribute. + if (runtimeInvisibleAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeInvisibleAnnotationsOffset); + int currentAnnotationOffset = runtimeInvisibleAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + methodVisitor.visitAnnotation(annotationDescriptor, /* visible = */ false), + currentAnnotationOffset, + /* named = */ true, + charBuffer); + } + } + + // Visit the RuntimeVisibleTypeAnnotations attribute. + if (runtimeVisibleTypeAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeVisibleTypeAnnotationsOffset); + int currentAnnotationOffset = runtimeVisibleTypeAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the target_type, target_info and target_path fields. + currentAnnotationOffset = readTypeAnnotationTarget(context, currentAnnotationOffset); + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + methodVisitor.visitTypeAnnotation( + context.currentTypeAnnotationTarget, + context.currentTypeAnnotationTargetPath, + annotationDescriptor, + /* visible = */ true), + currentAnnotationOffset, + /* named = */ true, + charBuffer); + } + } + + // Visit the RuntimeInvisibleTypeAnnotations attribute. + if (runtimeInvisibleTypeAnnotationsOffset != 0) { + int numAnnotations = readUnsignedShort(runtimeInvisibleTypeAnnotationsOffset); + int currentAnnotationOffset = runtimeInvisibleTypeAnnotationsOffset + 2; + while (numAnnotations-- > 0) { + // Parse the target_type, target_info and target_path fields. + currentAnnotationOffset = readTypeAnnotationTarget(context, currentAnnotationOffset); + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentAnnotationOffset = + readElementValues( + methodVisitor.visitTypeAnnotation( + context.currentTypeAnnotationTarget, + context.currentTypeAnnotationTargetPath, + annotationDescriptor, + /* visible = */ false), + currentAnnotationOffset, + /* named = */ true, + charBuffer); + } + } + + // Visit the RuntimeVisibleParameterAnnotations attribute. + if (runtimeVisibleParameterAnnotationsOffset != 0) { + readParameterAnnotations( + methodVisitor, context, runtimeVisibleParameterAnnotationsOffset, /* visible = */ true); + } + + // Visit the RuntimeInvisibleParameterAnnotations attribute. + if (runtimeInvisibleParameterAnnotationsOffset != 0) { + readParameterAnnotations( + methodVisitor, + context, + runtimeInvisibleParameterAnnotationsOffset, + /* visible = */ false); + } + + // Visit the non standard attributes. + while (attributes != null) { + // Copy and reset the nextAttribute field so that it can also be used in MethodWriter. + Attribute nextAttribute = attributes.nextAttribute; + attributes.nextAttribute = null; + methodVisitor.visitAttribute(attributes); + attributes = nextAttribute; + } + + // Visit the Code attribute. + if (codeOffset != 0) { + methodVisitor.visitCode(); + readCode(methodVisitor, context, codeOffset); + } + + // Visit the end of the method. + methodVisitor.visitEnd(); + return currentOffset; + } + + // ---------------------------------------------------------------------------------------------- + // Methods to parse a Code attribute + // ---------------------------------------------------------------------------------------------- + + /** + * Reads a JVMS 'Code' attribute and makes the given visitor visit it. + * + * @param methodVisitor the visitor that must visit the Code attribute. + * @param context information about the class being parsed. + * @param codeOffset the start offset in {@link #classFileBuffer} of the Code attribute, excluding + * its attribute_name_index and attribute_length fields. + */ + private void readCode( + final MethodVisitor methodVisitor, final Context context, final int codeOffset) { + int currentOffset = codeOffset; + + // Read the max_stack, max_locals and code_length fields. + final byte[] classBuffer = classFileBuffer; + final char[] charBuffer = context.charBuffer; + final int maxStack = readUnsignedShort(currentOffset); + final int maxLocals = readUnsignedShort(currentOffset + 2); + final int codeLength = readInt(currentOffset + 4); + currentOffset += 8; + + // Read the bytecode 'code' array to create a label for each referenced instruction. + final int bytecodeStartOffset = currentOffset; + final int bytecodeEndOffset = currentOffset + codeLength; + final Label[] labels = context.currentMethodLabels = new Label[codeLength + 1]; + while (currentOffset < bytecodeEndOffset) { + final int bytecodeOffset = currentOffset - bytecodeStartOffset; + final int opcode = classBuffer[currentOffset] & 0xFF; + switch (opcode) { + case Opcodes.NOP: + case Opcodes.ACONST_NULL: + case Opcodes.ICONST_M1: + case Opcodes.ICONST_0: + case Opcodes.ICONST_1: + case Opcodes.ICONST_2: + case Opcodes.ICONST_3: + case Opcodes.ICONST_4: + case Opcodes.ICONST_5: + case Opcodes.LCONST_0: + case Opcodes.LCONST_1: + case Opcodes.FCONST_0: + case Opcodes.FCONST_1: + case Opcodes.FCONST_2: + case Opcodes.DCONST_0: + case Opcodes.DCONST_1: + case Opcodes.IALOAD: + case Opcodes.LALOAD: + case Opcodes.FALOAD: + case Opcodes.DALOAD: + case Opcodes.AALOAD: + case Opcodes.BALOAD: + case Opcodes.CALOAD: + case Opcodes.SALOAD: + case Opcodes.IASTORE: + case Opcodes.LASTORE: + case Opcodes.FASTORE: + case Opcodes.DASTORE: + case Opcodes.AASTORE: + case Opcodes.BASTORE: + case Opcodes.CASTORE: + case Opcodes.SASTORE: + case Opcodes.POP: + case Opcodes.POP2: + case Opcodes.DUP: + case Opcodes.DUP_X1: + case Opcodes.DUP_X2: + case Opcodes.DUP2: + case Opcodes.DUP2_X1: + case Opcodes.DUP2_X2: + case Opcodes.SWAP: + case Opcodes.IADD: + case Opcodes.LADD: + case Opcodes.FADD: + case Opcodes.DADD: + case Opcodes.ISUB: + case Opcodes.LSUB: + case Opcodes.FSUB: + case Opcodes.DSUB: + case Opcodes.IMUL: + case Opcodes.LMUL: + case Opcodes.FMUL: + case Opcodes.DMUL: + case Opcodes.IDIV: + case Opcodes.LDIV: + case Opcodes.FDIV: + case Opcodes.DDIV: + case Opcodes.IREM: + case Opcodes.LREM: + case Opcodes.FREM: + case Opcodes.DREM: + case Opcodes.INEG: + case Opcodes.LNEG: + case Opcodes.FNEG: + case Opcodes.DNEG: + case Opcodes.ISHL: + case Opcodes.LSHL: + case Opcodes.ISHR: + case Opcodes.LSHR: + case Opcodes.IUSHR: + case Opcodes.LUSHR: + case Opcodes.IAND: + case Opcodes.LAND: + case Opcodes.IOR: + case Opcodes.LOR: + case Opcodes.IXOR: + case Opcodes.LXOR: + case Opcodes.I2L: + case Opcodes.I2F: + case Opcodes.I2D: + case Opcodes.L2I: + case Opcodes.L2F: + case Opcodes.L2D: + case Opcodes.F2I: + case Opcodes.F2L: + case Opcodes.F2D: + case Opcodes.D2I: + case Opcodes.D2L: + case Opcodes.D2F: + case Opcodes.I2B: + case Opcodes.I2C: + case Opcodes.I2S: + case Opcodes.LCMP: + case Opcodes.FCMPL: + case Opcodes.FCMPG: + case Opcodes.DCMPL: + case Opcodes.DCMPG: + case Opcodes.IRETURN: + case Opcodes.LRETURN: + case Opcodes.FRETURN: + case Opcodes.DRETURN: + case Opcodes.ARETURN: + case Opcodes.RETURN: + case Opcodes.ARRAYLENGTH: + case Opcodes.ATHROW: + case Opcodes.MONITORENTER: + case Opcodes.MONITOREXIT: + case Constants.ILOAD_0: + case Constants.ILOAD_1: + case Constants.ILOAD_2: + case Constants.ILOAD_3: + case Constants.LLOAD_0: + case Constants.LLOAD_1: + case Constants.LLOAD_2: + case Constants.LLOAD_3: + case Constants.FLOAD_0: + case Constants.FLOAD_1: + case Constants.FLOAD_2: + case Constants.FLOAD_3: + case Constants.DLOAD_0: + case Constants.DLOAD_1: + case Constants.DLOAD_2: + case Constants.DLOAD_3: + case Constants.ALOAD_0: + case Constants.ALOAD_1: + case Constants.ALOAD_2: + case Constants.ALOAD_3: + case Constants.ISTORE_0: + case Constants.ISTORE_1: + case Constants.ISTORE_2: + case Constants.ISTORE_3: + case Constants.LSTORE_0: + case Constants.LSTORE_1: + case Constants.LSTORE_2: + case Constants.LSTORE_3: + case Constants.FSTORE_0: + case Constants.FSTORE_1: + case Constants.FSTORE_2: + case Constants.FSTORE_3: + case Constants.DSTORE_0: + case Constants.DSTORE_1: + case Constants.DSTORE_2: + case Constants.DSTORE_3: + case Constants.ASTORE_0: + case Constants.ASTORE_1: + case Constants.ASTORE_2: + case Constants.ASTORE_3: + currentOffset += 1; + break; + case Opcodes.IFEQ: + case Opcodes.IFNE: + case Opcodes.IFLT: + case Opcodes.IFGE: + case Opcodes.IFGT: + case Opcodes.IFLE: + case Opcodes.IF_ICMPEQ: + case Opcodes.IF_ICMPNE: + case Opcodes.IF_ICMPLT: + case Opcodes.IF_ICMPGE: + case Opcodes.IF_ICMPGT: + case Opcodes.IF_ICMPLE: + case Opcodes.IF_ACMPEQ: + case Opcodes.IF_ACMPNE: + case Opcodes.GOTO: + case Opcodes.JSR: + case Opcodes.IFNULL: + case Opcodes.IFNONNULL: + createLabel(bytecodeOffset + readShort(currentOffset + 1), labels); + currentOffset += 3; + break; + case Constants.ASM_IFEQ: + case Constants.ASM_IFNE: + case Constants.ASM_IFLT: + case Constants.ASM_IFGE: + case Constants.ASM_IFGT: + case Constants.ASM_IFLE: + case Constants.ASM_IF_ICMPEQ: + case Constants.ASM_IF_ICMPNE: + case Constants.ASM_IF_ICMPLT: + case Constants.ASM_IF_ICMPGE: + case Constants.ASM_IF_ICMPGT: + case Constants.ASM_IF_ICMPLE: + case Constants.ASM_IF_ACMPEQ: + case Constants.ASM_IF_ACMPNE: + case Constants.ASM_GOTO: + case Constants.ASM_JSR: + case Constants.ASM_IFNULL: + case Constants.ASM_IFNONNULL: + createLabel(bytecodeOffset + readUnsignedShort(currentOffset + 1), labels); + currentOffset += 3; + break; + case Constants.GOTO_W: + case Constants.JSR_W: + case Constants.ASM_GOTO_W: + createLabel(bytecodeOffset + readInt(currentOffset + 1), labels); + currentOffset += 5; + break; + case Constants.WIDE: + switch (classBuffer[currentOffset + 1] & 0xFF) { + case Opcodes.ILOAD: + case Opcodes.FLOAD: + case Opcodes.ALOAD: + case Opcodes.LLOAD: + case Opcodes.DLOAD: + case Opcodes.ISTORE: + case Opcodes.FSTORE: + case Opcodes.ASTORE: + case Opcodes.LSTORE: + case Opcodes.DSTORE: + case Opcodes.RET: + currentOffset += 4; + break; + case Opcodes.IINC: + currentOffset += 6; + break; + default: + throw new IllegalArgumentException(); + } + break; + case Opcodes.TABLESWITCH: + // Skip 0 to 3 padding bytes. + currentOffset += 4 - (bytecodeOffset & 3); + // Read the default label and the number of table entries. + createLabel(bytecodeOffset + readInt(currentOffset), labels); + int numTableEntries = readInt(currentOffset + 8) - readInt(currentOffset + 4) + 1; + currentOffset += 12; + // Read the table labels. + while (numTableEntries-- > 0) { + createLabel(bytecodeOffset + readInt(currentOffset), labels); + currentOffset += 4; + } + break; + case Opcodes.LOOKUPSWITCH: + // Skip 0 to 3 padding bytes. + currentOffset += 4 - (bytecodeOffset & 3); + // Read the default label and the number of switch cases. + createLabel(bytecodeOffset + readInt(currentOffset), labels); + int numSwitchCases = readInt(currentOffset + 4); + currentOffset += 8; + // Read the switch labels. + while (numSwitchCases-- > 0) { + createLabel(bytecodeOffset + readInt(currentOffset + 4), labels); + currentOffset += 8; + } + break; + case Opcodes.ILOAD: + case Opcodes.LLOAD: + case Opcodes.FLOAD: + case Opcodes.DLOAD: + case Opcodes.ALOAD: + case Opcodes.ISTORE: + case Opcodes.LSTORE: + case Opcodes.FSTORE: + case Opcodes.DSTORE: + case Opcodes.ASTORE: + case Opcodes.RET: + case Opcodes.BIPUSH: + case Opcodes.NEWARRAY: + case Opcodes.LDC: + currentOffset += 2; + break; + case Opcodes.SIPUSH: + case Constants.LDC_W: + case Constants.LDC2_W: + case Opcodes.GETSTATIC: + case Opcodes.PUTSTATIC: + case Opcodes.GETFIELD: + case Opcodes.PUTFIELD: + case Opcodes.INVOKEVIRTUAL: + case Opcodes.INVOKESPECIAL: + case Opcodes.INVOKESTATIC: + case Opcodes.NEW: + case Opcodes.ANEWARRAY: + case Opcodes.CHECKCAST: + case Opcodes.INSTANCEOF: + case Opcodes.IINC: + currentOffset += 3; + break; + case Opcodes.INVOKEINTERFACE: + case Opcodes.INVOKEDYNAMIC: + currentOffset += 5; + break; + case Opcodes.MULTIANEWARRAY: + currentOffset += 4; + break; + default: + throw new IllegalArgumentException(); + } + } + + // Read the 'exception_table_length' and 'exception_table' field to create a label for each + // referenced instruction, and to make methodVisitor visit the corresponding try catch blocks. + int exceptionTableLength = readUnsignedShort(currentOffset); + currentOffset += 2; + while (exceptionTableLength-- > 0) { + Label start = createLabel(readUnsignedShort(currentOffset), labels); + Label end = createLabel(readUnsignedShort(currentOffset + 2), labels); + Label handler = createLabel(readUnsignedShort(currentOffset + 4), labels); + String catchType = readUTF8(cpInfoOffsets[readUnsignedShort(currentOffset + 6)], charBuffer); + currentOffset += 8; + methodVisitor.visitTryCatchBlock(start, end, handler, catchType); + } + + // Read the Code attributes to create a label for each referenced instruction (the variables + // are ordered as in Section 4.7 of the JVMS). Attribute offsets exclude the + // attribute_name_index and attribute_length fields. + // - The offset of the current 'stack_map_frame' in the StackMap[Table] attribute, or 0. + // Initially, this is the offset of the first 'stack_map_frame' entry. Then this offset is + // updated after each stack_map_frame is read. + int stackMapFrameOffset = 0; + // - The end offset of the StackMap[Table] attribute, or 0. + int stackMapTableEndOffset = 0; + // - Whether the stack map frames are compressed (i.e. in a StackMapTable) or not. + boolean compressedFrames = true; + // - The offset of the LocalVariableTable attribute, or 0. + int localVariableTableOffset = 0; + // - The offset of the LocalVariableTypeTable attribute, or 0. + int localVariableTypeTableOffset = 0; + // - The offset of each 'type_annotation' entry in the RuntimeVisibleTypeAnnotations + // attribute, or null. + int[] visibleTypeAnnotationOffsets = null; + // - The offset of each 'type_annotation' entry in the RuntimeInvisibleTypeAnnotations + // attribute, or null. + int[] invisibleTypeAnnotationOffsets = null; + // - The non standard attributes (linked with their {@link Attribute#nextAttribute} field). + // This list in the reverse order or their order in the ClassFile structure. + Attribute attributes = null; + + int attributesCount = readUnsignedShort(currentOffset); + currentOffset += 2; + while (attributesCount-- > 0) { + // Read the attribute_info's attribute_name and attribute_length fields. + String attributeName = readUTF8(currentOffset, charBuffer); + int attributeLength = readInt(currentOffset + 2); + currentOffset += 6; + if (Constants.LOCAL_VARIABLE_TABLE.equals(attributeName)) { + if ((context.parsingOptions & SKIP_DEBUG) == 0) { + localVariableTableOffset = currentOffset; + // Parse the attribute to find the corresponding (debug only) labels. + int currentLocalVariableTableOffset = currentOffset; + int localVariableTableLength = readUnsignedShort(currentLocalVariableTableOffset); + currentLocalVariableTableOffset += 2; + while (localVariableTableLength-- > 0) { + int startPc = readUnsignedShort(currentLocalVariableTableOffset); + createDebugLabel(startPc, labels); + int length = readUnsignedShort(currentLocalVariableTableOffset + 2); + createDebugLabel(startPc + length, labels); + // Skip the name_index, descriptor_index and index fields (2 bytes each). + currentLocalVariableTableOffset += 10; + } + } + } else if (Constants.LOCAL_VARIABLE_TYPE_TABLE.equals(attributeName)) { + localVariableTypeTableOffset = currentOffset; + // Here we do not extract the labels corresponding to the attribute content. We assume they + // are the same or a subset of those of the LocalVariableTable attribute. + } else if (Constants.LINE_NUMBER_TABLE.equals(attributeName)) { + if ((context.parsingOptions & SKIP_DEBUG) == 0) { + // Parse the attribute to find the corresponding (debug only) labels. + int currentLineNumberTableOffset = currentOffset; + int lineNumberTableLength = readUnsignedShort(currentLineNumberTableOffset); + currentLineNumberTableOffset += 2; + while (lineNumberTableLength-- > 0) { + int startPc = readUnsignedShort(currentLineNumberTableOffset); + int lineNumber = readUnsignedShort(currentLineNumberTableOffset + 2); + currentLineNumberTableOffset += 4; + createDebugLabel(startPc, labels); + labels[startPc].addLineNumber(lineNumber); + } + } + } else if (Constants.RUNTIME_VISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) { + visibleTypeAnnotationOffsets = + readTypeAnnotations(methodVisitor, context, currentOffset, /* visible = */ true); + // Here we do not extract the labels corresponding to the attribute content. This would + // require a full parsing of the attribute, which would need to be repeated when parsing + // the bytecode instructions (see below). Instead, the content of the attribute is read one + // type annotation at a time (i.e. after a type annotation has been visited, the next type + // annotation is read), and the labels it contains are also extracted one annotation at a + // time. This assumes that type annotations are ordered by increasing bytecode offset. + } else if (Constants.RUNTIME_INVISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) { + invisibleTypeAnnotationOffsets = + readTypeAnnotations(methodVisitor, context, currentOffset, /* visible = */ false); + // Same comment as above for the RuntimeVisibleTypeAnnotations attribute. + } else if (Constants.STACK_MAP_TABLE.equals(attributeName)) { + if ((context.parsingOptions & SKIP_FRAMES) == 0) { + stackMapFrameOffset = currentOffset + 2; + stackMapTableEndOffset = currentOffset + attributeLength; + } + // Here we do not extract the labels corresponding to the attribute content. This would + // require a full parsing of the attribute, which would need to be repeated when parsing + // the bytecode instructions (see below). Instead, the content of the attribute is read one + // frame at a time (i.e. after a frame has been visited, the next frame is read), and the + // labels it contains are also extracted one frame at a time. Thanks to the ordering of + // frames, having only a "one frame lookahead" is not a problem, i.e. it is not possible to + // see an offset smaller than the offset of the current instruction and for which no Label + // exist. Except for UNINITIALIZED type offsets. We solve this by parsing the stack map + // table without a full decoding (see below). + } else if ("StackMap".equals(attributeName)) { + if ((context.parsingOptions & SKIP_FRAMES) == 0) { + stackMapFrameOffset = currentOffset + 2; + stackMapTableEndOffset = currentOffset + attributeLength; + compressedFrames = false; + } + // IMPORTANT! Here we assume that the frames are ordered, as in the StackMapTable attribute, + // although this is not guaranteed by the attribute format. This allows an incremental + // extraction of the labels corresponding to this attribute (see the comment above for the + // StackMapTable attribute). + } else { + Attribute attribute = + readAttribute( + context.attributePrototypes, + attributeName, + currentOffset, + attributeLength, + charBuffer, + codeOffset, + labels); + attribute.nextAttribute = attributes; + attributes = attribute; + } + currentOffset += attributeLength; + } + + // Initialize the context fields related to stack map frames, and generate the first + // (implicit) stack map frame, if needed. + final boolean expandFrames = (context.parsingOptions & EXPAND_FRAMES) != 0; + if (stackMapFrameOffset != 0) { + // The bytecode offset of the first explicit frame is not offset_delta + 1 but only + // offset_delta. Setting the implicit frame offset to -1 allows us to use of the + // "offset_delta + 1" rule in all cases. + context.currentFrameOffset = -1; + context.currentFrameType = 0; + context.currentFrameLocalCount = 0; + context.currentFrameLocalCountDelta = 0; + context.currentFrameLocalTypes = new Object[maxLocals]; + context.currentFrameStackCount = 0; + context.currentFrameStackTypes = new Object[maxStack]; + if (expandFrames) { + computeImplicitFrame(context); + } + // Find the labels for UNINITIALIZED frame types. Instead of decoding each element of the + // stack map table, we look for 3 consecutive bytes that "look like" an UNINITIALIZED type + // (tag ITEM_Uninitialized, offset within bytecode bounds, NEW instruction at this offset). + // We may find false positives (i.e. not real UNINITIALIZED types), but this should be rare, + // and the only consequence will be the creation of an unneeded label. This is better than + // creating a label for each NEW instruction, and faster than fully decoding the whole stack + // map table. + for (int offset = stackMapFrameOffset; offset < stackMapTableEndOffset - 2; ++offset) { + if (classBuffer[offset] == Frame.ITEM_UNINITIALIZED) { + int potentialBytecodeOffset = readUnsignedShort(offset + 1); + if (potentialBytecodeOffset >= 0 + && potentialBytecodeOffset < codeLength + && (classBuffer[bytecodeStartOffset + potentialBytecodeOffset] & 0xFF) + == Opcodes.NEW) { + createLabel(potentialBytecodeOffset, labels); + } + } + } + } + if (expandFrames && (context.parsingOptions & EXPAND_ASM_INSNS) != 0) { + // Expanding the ASM specific instructions can introduce F_INSERT frames, even if the method + // does not currently have any frame. These inserted frames must be computed by simulating the + // effect of the bytecode instructions, one by one, starting from the implicit first frame. + // For this, MethodWriter needs to know maxLocals before the first instruction is visited. To + // ensure this, we visit the implicit first frame here (passing only maxLocals - the rest is + // computed in MethodWriter). + methodVisitor.visitFrame(Opcodes.F_NEW, maxLocals, null, 0, null); + } + + // Visit the bytecode instructions. First, introduce state variables for the incremental parsing + // of the type annotations. + + // Index of the next runtime visible type annotation to read (in the + // visibleTypeAnnotationOffsets array). + int currentVisibleTypeAnnotationIndex = 0; + // The bytecode offset of the next runtime visible type annotation to read, or -1. + int currentVisibleTypeAnnotationBytecodeOffset = + getTypeAnnotationBytecodeOffset(visibleTypeAnnotationOffsets, 0); + // Index of the next runtime invisible type annotation to read (in the + // invisibleTypeAnnotationOffsets array). + int currentInvisibleTypeAnnotationIndex = 0; + // The bytecode offset of the next runtime invisible type annotation to read, or -1. + int currentInvisibleTypeAnnotationBytecodeOffset = + getTypeAnnotationBytecodeOffset(invisibleTypeAnnotationOffsets, 0); + + // Whether a F_INSERT stack map frame must be inserted before the current instruction. + boolean insertFrame = false; + + // The delta to subtract from a goto_w or jsr_w opcode to get the corresponding goto or jsr + // opcode, or 0 if goto_w and jsr_w must be left unchanged (i.e. when expanding ASM specific + // instructions). + final int wideJumpOpcodeDelta = + (context.parsingOptions & EXPAND_ASM_INSNS) == 0 ? Constants.WIDE_JUMP_OPCODE_DELTA : 0; + + currentOffset = bytecodeStartOffset; + while (currentOffset < bytecodeEndOffset) { + final int currentBytecodeOffset = currentOffset - bytecodeStartOffset; + + // Visit the label and the line number(s) for this bytecode offset, if any. + Label currentLabel = labels[currentBytecodeOffset]; + if (currentLabel != null) { + currentLabel.accept(methodVisitor, (context.parsingOptions & SKIP_DEBUG) == 0); + } + + // Visit the stack map frame for this bytecode offset, if any. + while (stackMapFrameOffset != 0 + && (context.currentFrameOffset == currentBytecodeOffset + || context.currentFrameOffset == -1)) { + // If there is a stack map frame for this offset, make methodVisitor visit it, and read the + // next stack map frame if there is one. + if (context.currentFrameOffset != -1) { + if (!compressedFrames || expandFrames) { + methodVisitor.visitFrame( + Opcodes.F_NEW, + context.currentFrameLocalCount, + context.currentFrameLocalTypes, + context.currentFrameStackCount, + context.currentFrameStackTypes); + } else { + methodVisitor.visitFrame( + context.currentFrameType, + context.currentFrameLocalCountDelta, + context.currentFrameLocalTypes, + context.currentFrameStackCount, + context.currentFrameStackTypes); + } + // Since there is already a stack map frame for this bytecode offset, there is no need to + // insert a new one. + insertFrame = false; + } + if (stackMapFrameOffset < stackMapTableEndOffset) { + stackMapFrameOffset = + readStackMapFrame(stackMapFrameOffset, compressedFrames, expandFrames, context); + } else { + stackMapFrameOffset = 0; + } + } + + // Insert a stack map frame for this bytecode offset, if requested by setting insertFrame to + // true during the previous iteration. The actual frame content is computed in MethodWriter. + if (insertFrame) { + if ((context.parsingOptions & EXPAND_FRAMES) != 0) { + methodVisitor.visitFrame(Constants.F_INSERT, 0, null, 0, null); + } + insertFrame = false; + } + + // Visit the instruction at this bytecode offset. + int opcode = classBuffer[currentOffset] & 0xFF; + switch (opcode) { + case Opcodes.NOP: + case Opcodes.ACONST_NULL: + case Opcodes.ICONST_M1: + case Opcodes.ICONST_0: + case Opcodes.ICONST_1: + case Opcodes.ICONST_2: + case Opcodes.ICONST_3: + case Opcodes.ICONST_4: + case Opcodes.ICONST_5: + case Opcodes.LCONST_0: + case Opcodes.LCONST_1: + case Opcodes.FCONST_0: + case Opcodes.FCONST_1: + case Opcodes.FCONST_2: + case Opcodes.DCONST_0: + case Opcodes.DCONST_1: + case Opcodes.IALOAD: + case Opcodes.LALOAD: + case Opcodes.FALOAD: + case Opcodes.DALOAD: + case Opcodes.AALOAD: + case Opcodes.BALOAD: + case Opcodes.CALOAD: + case Opcodes.SALOAD: + case Opcodes.IASTORE: + case Opcodes.LASTORE: + case Opcodes.FASTORE: + case Opcodes.DASTORE: + case Opcodes.AASTORE: + case Opcodes.BASTORE: + case Opcodes.CASTORE: + case Opcodes.SASTORE: + case Opcodes.POP: + case Opcodes.POP2: + case Opcodes.DUP: + case Opcodes.DUP_X1: + case Opcodes.DUP_X2: + case Opcodes.DUP2: + case Opcodes.DUP2_X1: + case Opcodes.DUP2_X2: + case Opcodes.SWAP: + case Opcodes.IADD: + case Opcodes.LADD: + case Opcodes.FADD: + case Opcodes.DADD: + case Opcodes.ISUB: + case Opcodes.LSUB: + case Opcodes.FSUB: + case Opcodes.DSUB: + case Opcodes.IMUL: + case Opcodes.LMUL: + case Opcodes.FMUL: + case Opcodes.DMUL: + case Opcodes.IDIV: + case Opcodes.LDIV: + case Opcodes.FDIV: + case Opcodes.DDIV: + case Opcodes.IREM: + case Opcodes.LREM: + case Opcodes.FREM: + case Opcodes.DREM: + case Opcodes.INEG: + case Opcodes.LNEG: + case Opcodes.FNEG: + case Opcodes.DNEG: + case Opcodes.ISHL: + case Opcodes.LSHL: + case Opcodes.ISHR: + case Opcodes.LSHR: + case Opcodes.IUSHR: + case Opcodes.LUSHR: + case Opcodes.IAND: + case Opcodes.LAND: + case Opcodes.IOR: + case Opcodes.LOR: + case Opcodes.IXOR: + case Opcodes.LXOR: + case Opcodes.I2L: + case Opcodes.I2F: + case Opcodes.I2D: + case Opcodes.L2I: + case Opcodes.L2F: + case Opcodes.L2D: + case Opcodes.F2I: + case Opcodes.F2L: + case Opcodes.F2D: + case Opcodes.D2I: + case Opcodes.D2L: + case Opcodes.D2F: + case Opcodes.I2B: + case Opcodes.I2C: + case Opcodes.I2S: + case Opcodes.LCMP: + case Opcodes.FCMPL: + case Opcodes.FCMPG: + case Opcodes.DCMPL: + case Opcodes.DCMPG: + case Opcodes.IRETURN: + case Opcodes.LRETURN: + case Opcodes.FRETURN: + case Opcodes.DRETURN: + case Opcodes.ARETURN: + case Opcodes.RETURN: + case Opcodes.ARRAYLENGTH: + case Opcodes.ATHROW: + case Opcodes.MONITORENTER: + case Opcodes.MONITOREXIT: + methodVisitor.visitInsn(opcode); + currentOffset += 1; + break; + case Constants.ILOAD_0: + case Constants.ILOAD_1: + case Constants.ILOAD_2: + case Constants.ILOAD_3: + case Constants.LLOAD_0: + case Constants.LLOAD_1: + case Constants.LLOAD_2: + case Constants.LLOAD_3: + case Constants.FLOAD_0: + case Constants.FLOAD_1: + case Constants.FLOAD_2: + case Constants.FLOAD_3: + case Constants.DLOAD_0: + case Constants.DLOAD_1: + case Constants.DLOAD_2: + case Constants.DLOAD_3: + case Constants.ALOAD_0: + case Constants.ALOAD_1: + case Constants.ALOAD_2: + case Constants.ALOAD_3: + opcode -= Constants.ILOAD_0; + methodVisitor.visitVarInsn(Opcodes.ILOAD + (opcode >> 2), opcode & 0x3); + currentOffset += 1; + break; + case Constants.ISTORE_0: + case Constants.ISTORE_1: + case Constants.ISTORE_2: + case Constants.ISTORE_3: + case Constants.LSTORE_0: + case Constants.LSTORE_1: + case Constants.LSTORE_2: + case Constants.LSTORE_3: + case Constants.FSTORE_0: + case Constants.FSTORE_1: + case Constants.FSTORE_2: + case Constants.FSTORE_3: + case Constants.DSTORE_0: + case Constants.DSTORE_1: + case Constants.DSTORE_2: + case Constants.DSTORE_3: + case Constants.ASTORE_0: + case Constants.ASTORE_1: + case Constants.ASTORE_2: + case Constants.ASTORE_3: + opcode -= Constants.ISTORE_0; + methodVisitor.visitVarInsn(Opcodes.ISTORE + (opcode >> 2), opcode & 0x3); + currentOffset += 1; + break; + case Opcodes.IFEQ: + case Opcodes.IFNE: + case Opcodes.IFLT: + case Opcodes.IFGE: + case Opcodes.IFGT: + case Opcodes.IFLE: + case Opcodes.IF_ICMPEQ: + case Opcodes.IF_ICMPNE: + case Opcodes.IF_ICMPLT: + case Opcodes.IF_ICMPGE: + case Opcodes.IF_ICMPGT: + case Opcodes.IF_ICMPLE: + case Opcodes.IF_ACMPEQ: + case Opcodes.IF_ACMPNE: + case Opcodes.GOTO: + case Opcodes.JSR: + case Opcodes.IFNULL: + case Opcodes.IFNONNULL: + methodVisitor.visitJumpInsn( + opcode, labels[currentBytecodeOffset + readShort(currentOffset + 1)]); + currentOffset += 3; + break; + case Constants.GOTO_W: + case Constants.JSR_W: + methodVisitor.visitJumpInsn( + opcode - wideJumpOpcodeDelta, + labels[currentBytecodeOffset + readInt(currentOffset + 1)]); + currentOffset += 5; + break; + case Constants.ASM_IFEQ: + case Constants.ASM_IFNE: + case Constants.ASM_IFLT: + case Constants.ASM_IFGE: + case Constants.ASM_IFGT: + case Constants.ASM_IFLE: + case Constants.ASM_IF_ICMPEQ: + case Constants.ASM_IF_ICMPNE: + case Constants.ASM_IF_ICMPLT: + case Constants.ASM_IF_ICMPGE: + case Constants.ASM_IF_ICMPGT: + case Constants.ASM_IF_ICMPLE: + case Constants.ASM_IF_ACMPEQ: + case Constants.ASM_IF_ACMPNE: + case Constants.ASM_GOTO: + case Constants.ASM_JSR: + case Constants.ASM_IFNULL: + case Constants.ASM_IFNONNULL: + { + // A forward jump with an offset > 32767. In this case we automatically replace ASM_GOTO + // with GOTO_W, ASM_JSR with JSR_W and ASM_IFxxx with IFNOTxxx GOTO_W L:..., + // where IFNOTxxx is the "opposite" opcode of ASMS_IFxxx (e.g. IFNE for ASM_IFEQ) and + // where designates the instruction just after the GOTO_W. + // First, change the ASM specific opcodes ASM_IFEQ ... ASM_JSR, ASM_IFNULL and + // ASM_IFNONNULL to IFEQ ... JSR, IFNULL and IFNONNULL. + opcode = + opcode < Constants.ASM_IFNULL + ? opcode - Constants.ASM_OPCODE_DELTA + : opcode - Constants.ASM_IFNULL_OPCODE_DELTA; + Label target = labels[currentBytecodeOffset + readUnsignedShort(currentOffset + 1)]; + if (opcode == Opcodes.GOTO || opcode == Opcodes.JSR) { + // Replace GOTO with GOTO_W and JSR with JSR_W. + methodVisitor.visitJumpInsn(opcode + Constants.WIDE_JUMP_OPCODE_DELTA, target); + } else { + // Compute the "opposite" of opcode. This can be done by flipping the least + // significant bit for IFNULL and IFNONNULL, and similarly for IFEQ ... IF_ACMPEQ + // (with a pre and post offset by 1). + opcode = opcode < Opcodes.GOTO ? ((opcode + 1) ^ 1) - 1 : opcode ^ 1; + Label endif = createLabel(currentBytecodeOffset + 3, labels); + methodVisitor.visitJumpInsn(opcode, endif); + methodVisitor.visitJumpInsn(Constants.GOTO_W, target); + // endif designates the instruction just after GOTO_W, and is visited as part of the + // next instruction. Since it is a jump target, we need to insert a frame here. + insertFrame = true; + } + currentOffset += 3; + break; + } + case Constants.ASM_GOTO_W: + // Replace ASM_GOTO_W with GOTO_W. + methodVisitor.visitJumpInsn( + Constants.GOTO_W, labels[currentBytecodeOffset + readInt(currentOffset + 1)]); + // The instruction just after is a jump target (because ASM_GOTO_W is used in patterns + // IFNOTxxx ASM_GOTO_W L:..., see MethodWriter), so we need to insert a frame + // here. + insertFrame = true; + currentOffset += 5; + break; + case Constants.WIDE: + opcode = classBuffer[currentOffset + 1] & 0xFF; + if (opcode == Opcodes.IINC) { + methodVisitor.visitIincInsn( + readUnsignedShort(currentOffset + 2), readShort(currentOffset + 4)); + currentOffset += 6; + } else { + methodVisitor.visitVarInsn(opcode, readUnsignedShort(currentOffset + 2)); + currentOffset += 4; + } + break; + case Opcodes.TABLESWITCH: + { + // Skip 0 to 3 padding bytes. + currentOffset += 4 - (currentBytecodeOffset & 3); + // Read the instruction. + Label defaultLabel = labels[currentBytecodeOffset + readInt(currentOffset)]; + int low = readInt(currentOffset + 4); + int high = readInt(currentOffset + 8); + currentOffset += 12; + Label[] table = new Label[high - low + 1]; + for (int i = 0; i < table.length; ++i) { + table[i] = labels[currentBytecodeOffset + readInt(currentOffset)]; + currentOffset += 4; + } + methodVisitor.visitTableSwitchInsn(low, high, defaultLabel, table); + break; + } + case Opcodes.LOOKUPSWITCH: + { + // Skip 0 to 3 padding bytes. + currentOffset += 4 - (currentBytecodeOffset & 3); + // Read the instruction. + Label defaultLabel = labels[currentBytecodeOffset + readInt(currentOffset)]; + int numPairs = readInt(currentOffset + 4); + currentOffset += 8; + int[] keys = new int[numPairs]; + Label[] values = new Label[numPairs]; + for (int i = 0; i < numPairs; ++i) { + keys[i] = readInt(currentOffset); + values[i] = labels[currentBytecodeOffset + readInt(currentOffset + 4)]; + currentOffset += 8; + } + methodVisitor.visitLookupSwitchInsn(defaultLabel, keys, values); + break; + } + case Opcodes.ILOAD: + case Opcodes.LLOAD: + case Opcodes.FLOAD: + case Opcodes.DLOAD: + case Opcodes.ALOAD: + case Opcodes.ISTORE: + case Opcodes.LSTORE: + case Opcodes.FSTORE: + case Opcodes.DSTORE: + case Opcodes.ASTORE: + case Opcodes.RET: + methodVisitor.visitVarInsn(opcode, classBuffer[currentOffset + 1] & 0xFF); + currentOffset += 2; + break; + case Opcodes.BIPUSH: + case Opcodes.NEWARRAY: + methodVisitor.visitIntInsn(opcode, classBuffer[currentOffset + 1]); + currentOffset += 2; + break; + case Opcodes.SIPUSH: + methodVisitor.visitIntInsn(opcode, readShort(currentOffset + 1)); + currentOffset += 3; + break; + case Opcodes.LDC: + methodVisitor.visitLdcInsn(readConst(classBuffer[currentOffset + 1] & 0xFF, charBuffer)); + currentOffset += 2; + break; + case Constants.LDC_W: + case Constants.LDC2_W: + methodVisitor.visitLdcInsn(readConst(readUnsignedShort(currentOffset + 1), charBuffer)); + currentOffset += 3; + break; + case Opcodes.GETSTATIC: + case Opcodes.PUTSTATIC: + case Opcodes.GETFIELD: + case Opcodes.PUTFIELD: + case Opcodes.INVOKEVIRTUAL: + case Opcodes.INVOKESPECIAL: + case Opcodes.INVOKESTATIC: + case Opcodes.INVOKEINTERFACE: + { + int cpInfoOffset = cpInfoOffsets[readUnsignedShort(currentOffset + 1)]; + int nameAndTypeCpInfoOffset = cpInfoOffsets[readUnsignedShort(cpInfoOffset + 2)]; + String owner = readClass(cpInfoOffset, charBuffer); + String name = readUTF8(nameAndTypeCpInfoOffset, charBuffer); + String descriptor = readUTF8(nameAndTypeCpInfoOffset + 2, charBuffer); + if (opcode < Opcodes.INVOKEVIRTUAL) { + methodVisitor.visitFieldInsn(opcode, owner, name, descriptor); + } else { + boolean isInterface = + classBuffer[cpInfoOffset - 1] == Symbol.CONSTANT_INTERFACE_METHODREF_TAG; + methodVisitor.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } + if (opcode == Opcodes.INVOKEINTERFACE) { + currentOffset += 5; + } else { + currentOffset += 3; + } + break; + } + case Opcodes.INVOKEDYNAMIC: + { + int cpInfoOffset = cpInfoOffsets[readUnsignedShort(currentOffset + 1)]; + int nameAndTypeCpInfoOffset = cpInfoOffsets[readUnsignedShort(cpInfoOffset + 2)]; + String name = readUTF8(nameAndTypeCpInfoOffset, charBuffer); + String descriptor = readUTF8(nameAndTypeCpInfoOffset + 2, charBuffer); + int bootstrapMethodOffset = bootstrapMethodOffsets[readUnsignedShort(cpInfoOffset)]; + Handle handle = + (Handle) readConst(readUnsignedShort(bootstrapMethodOffset), charBuffer); + Object[] bootstrapMethodArguments = + new Object[readUnsignedShort(bootstrapMethodOffset + 2)]; + bootstrapMethodOffset += 4; + for (int i = 0; i < bootstrapMethodArguments.length; i++) { + bootstrapMethodArguments[i] = + readConst(readUnsignedShort(bootstrapMethodOffset), charBuffer); + bootstrapMethodOffset += 2; + } + methodVisitor.visitInvokeDynamicInsn( + name, descriptor, handle, bootstrapMethodArguments); + currentOffset += 5; + break; + } + case Opcodes.NEW: + case Opcodes.ANEWARRAY: + case Opcodes.CHECKCAST: + case Opcodes.INSTANCEOF: + methodVisitor.visitTypeInsn(opcode, readClass(currentOffset + 1, charBuffer)); + currentOffset += 3; + break; + case Opcodes.IINC: + methodVisitor.visitIincInsn( + classBuffer[currentOffset + 1] & 0xFF, classBuffer[currentOffset + 2]); + currentOffset += 3; + break; + case Opcodes.MULTIANEWARRAY: + methodVisitor.visitMultiANewArrayInsn( + readClass(currentOffset + 1, charBuffer), classBuffer[currentOffset + 3] & 0xFF); + currentOffset += 4; + break; + default: + throw new AssertionError(); + } + + // Visit the runtime visible instruction annotations, if any. + while (visibleTypeAnnotationOffsets != null + && currentVisibleTypeAnnotationIndex < visibleTypeAnnotationOffsets.length + && currentVisibleTypeAnnotationBytecodeOffset <= currentBytecodeOffset) { + if (currentVisibleTypeAnnotationBytecodeOffset == currentBytecodeOffset) { + // Parse the target_type, target_info and target_path fields. + int currentAnnotationOffset = + readTypeAnnotationTarget( + context, visibleTypeAnnotationOffsets[currentVisibleTypeAnnotationIndex]); + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + readElementValues( + methodVisitor.visitInsnAnnotation( + context.currentTypeAnnotationTarget, + context.currentTypeAnnotationTargetPath, + annotationDescriptor, + /* visible = */ true), + currentAnnotationOffset, + /* named = */ true, + charBuffer); + } + currentVisibleTypeAnnotationBytecodeOffset = + getTypeAnnotationBytecodeOffset( + visibleTypeAnnotationOffsets, ++currentVisibleTypeAnnotationIndex); + } + + // Visit the runtime invisible instruction annotations, if any. + while (invisibleTypeAnnotationOffsets != null + && currentInvisibleTypeAnnotationIndex < invisibleTypeAnnotationOffsets.length + && currentInvisibleTypeAnnotationBytecodeOffset <= currentBytecodeOffset) { + if (currentInvisibleTypeAnnotationBytecodeOffset == currentBytecodeOffset) { + // Parse the target_type, target_info and target_path fields. + int currentAnnotationOffset = + readTypeAnnotationTarget( + context, invisibleTypeAnnotationOffsets[currentInvisibleTypeAnnotationIndex]); + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer); + currentAnnotationOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + readElementValues( + methodVisitor.visitInsnAnnotation( + context.currentTypeAnnotationTarget, + context.currentTypeAnnotationTargetPath, + annotationDescriptor, + /* visible = */ false), + currentAnnotationOffset, + /* named = */ true, + charBuffer); + } + currentInvisibleTypeAnnotationBytecodeOffset = + getTypeAnnotationBytecodeOffset( + invisibleTypeAnnotationOffsets, ++currentInvisibleTypeAnnotationIndex); + } + } + if (labels[codeLength] != null) { + methodVisitor.visitLabel(labels[codeLength]); + } + + // Visit LocalVariableTable and LocalVariableTypeTable attributes. + if (localVariableTableOffset != 0 && (context.parsingOptions & SKIP_DEBUG) == 0) { + // The (start_pc, index, signature_index) fields of each entry of the LocalVariableTypeTable. + int[] typeTable = null; + if (localVariableTypeTableOffset != 0) { + typeTable = new int[readUnsignedShort(localVariableTypeTableOffset) * 3]; + currentOffset = localVariableTypeTableOffset + 2; + int typeTableIndex = typeTable.length; + while (typeTableIndex > 0) { + // Store the offset of 'signature_index', and the value of 'index' and 'start_pc'. + typeTable[--typeTableIndex] = currentOffset + 6; + typeTable[--typeTableIndex] = readUnsignedShort(currentOffset + 8); + typeTable[--typeTableIndex] = readUnsignedShort(currentOffset); + currentOffset += 10; + } + } + int localVariableTableLength = readUnsignedShort(localVariableTableOffset); + currentOffset = localVariableTableOffset + 2; + while (localVariableTableLength-- > 0) { + int startPc = readUnsignedShort(currentOffset); + int length = readUnsignedShort(currentOffset + 2); + String name = readUTF8(currentOffset + 4, charBuffer); + String descriptor = readUTF8(currentOffset + 6, charBuffer); + int index = readUnsignedShort(currentOffset + 8); + currentOffset += 10; + String signature = null; + if (typeTable != null) { + for (int i = 0; i < typeTable.length; i += 3) { + if (typeTable[i] == startPc && typeTable[i + 1] == index) { + signature = readUTF8(typeTable[i + 2], charBuffer); + break; + } + } + } + methodVisitor.visitLocalVariable( + name, descriptor, signature, labels[startPc], labels[startPc + length], index); + } + } + + // Visit the local variable type annotations of the RuntimeVisibleTypeAnnotations attribute. + if (visibleTypeAnnotationOffsets != null) { + for (int typeAnnotationOffset : visibleTypeAnnotationOffsets) { + int targetType = readByte(typeAnnotationOffset); + if (targetType == TypeReference.LOCAL_VARIABLE + || targetType == TypeReference.RESOURCE_VARIABLE) { + // Parse the target_type, target_info and target_path fields. + currentOffset = readTypeAnnotationTarget(context, typeAnnotationOffset); + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentOffset, charBuffer); + currentOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + readElementValues( + methodVisitor.visitLocalVariableAnnotation( + context.currentTypeAnnotationTarget, + context.currentTypeAnnotationTargetPath, + context.currentLocalVariableAnnotationRangeStarts, + context.currentLocalVariableAnnotationRangeEnds, + context.currentLocalVariableAnnotationRangeIndices, + annotationDescriptor, + /* visible = */ true), + currentOffset, + /* named = */ true, + charBuffer); + } + } + } + + // Visit the local variable type annotations of the RuntimeInvisibleTypeAnnotations attribute. + if (invisibleTypeAnnotationOffsets != null) { + for (int typeAnnotationOffset : invisibleTypeAnnotationOffsets) { + int targetType = readByte(typeAnnotationOffset); + if (targetType == TypeReference.LOCAL_VARIABLE + || targetType == TypeReference.RESOURCE_VARIABLE) { + // Parse the target_type, target_info and target_path fields. + currentOffset = readTypeAnnotationTarget(context, typeAnnotationOffset); + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentOffset, charBuffer); + currentOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + readElementValues( + methodVisitor.visitLocalVariableAnnotation( + context.currentTypeAnnotationTarget, + context.currentTypeAnnotationTargetPath, + context.currentLocalVariableAnnotationRangeStarts, + context.currentLocalVariableAnnotationRangeEnds, + context.currentLocalVariableAnnotationRangeIndices, + annotationDescriptor, + /* visible = */ false), + currentOffset, + /* named = */ true, + charBuffer); + } + } + } + + // Visit the non standard attributes. + while (attributes != null) { + // Copy and reset the nextAttribute field so that it can also be used in MethodWriter. + Attribute nextAttribute = attributes.nextAttribute; + attributes.nextAttribute = null; + methodVisitor.visitAttribute(attributes); + attributes = nextAttribute; + } + + // Visit the max stack and max locals values. + methodVisitor.visitMaxs(maxStack, maxLocals); + } + + /** + * Returns the label corresponding to the given bytecode offset. The default implementation of + * this method creates a label for the given offset if it has not been already created. + * + * @param bytecodeOffset a bytecode offset in a method. + * @param labels the already created labels, indexed by their offset. If a label already exists + * for bytecodeOffset this method must not create a new one. Otherwise it must store the new + * label in this array. + * @return a non null Label, which must be equal to labels[bytecodeOffset]. + */ + protected Label readLabel(final int bytecodeOffset, final Label[] labels) { + // SPRING PATCH: leniently handle offset mismatch + if (bytecodeOffset >= labels.length) { + return new Label(); + } + // END OF PATCH + if (labels[bytecodeOffset] == null) { + labels[bytecodeOffset] = new Label(); + } + return labels[bytecodeOffset]; + } + + /** + * Creates a label without the {@link Label#FLAG_DEBUG_ONLY} flag set, for the given bytecode + * offset. The label is created with a call to {@link #readLabel} and its {@link + * Label#FLAG_DEBUG_ONLY} flag is cleared. + * + * @param bytecodeOffset a bytecode offset in a method. + * @param labels the already created labels, indexed by their offset. + * @return a Label without the {@link Label#FLAG_DEBUG_ONLY} flag set. + */ + private Label createLabel(final int bytecodeOffset, final Label[] labels) { + Label label = readLabel(bytecodeOffset, labels); + label.flags &= ~Label.FLAG_DEBUG_ONLY; + return label; + } + + /** + * Creates a label with the {@link Label#FLAG_DEBUG_ONLY} flag set, if there is no already + * existing label for the given bytecode offset (otherwise does nothing). The label is created + * with a call to {@link #readLabel}. + * + * @param bytecodeOffset a bytecode offset in a method. + * @param labels the already created labels, indexed by their offset. + */ + private void createDebugLabel(final int bytecodeOffset, final Label[] labels) { + if (labels[bytecodeOffset] == null) { + readLabel(bytecodeOffset, labels).flags |= Label.FLAG_DEBUG_ONLY; + } + } + + // ---------------------------------------------------------------------------------------------- + // Methods to parse annotations, type annotations and parameter annotations + // ---------------------------------------------------------------------------------------------- + + /** + * Parses a Runtime[In]VisibleTypeAnnotations attribute to find the offset of each type_annotation + * entry it contains, to find the corresponding labels, and to visit the try catch block + * annotations. + * + * @param methodVisitor the method visitor to be used to visit the try catch block annotations. + * @param context information about the class being parsed. + * @param runtimeTypeAnnotationsOffset the start offset of a Runtime[In]VisibleTypeAnnotations + * attribute, excluding the attribute_info's attribute_name_index and attribute_length fields. + * @param visible true if the attribute to parse is a RuntimeVisibleTypeAnnotations attribute, + * false it is a RuntimeInvisibleTypeAnnotations attribute. + * @return the start offset of each entry of the Runtime[In]VisibleTypeAnnotations_attribute's + * 'annotations' array field. + */ + private int[] readTypeAnnotations( + final MethodVisitor methodVisitor, + final Context context, + final int runtimeTypeAnnotationsOffset, + final boolean visible) { + char[] charBuffer = context.charBuffer; + int currentOffset = runtimeTypeAnnotationsOffset; + // Read the num_annotations field and create an array to store the type_annotation offsets. + int[] typeAnnotationsOffsets = new int[readUnsignedShort(currentOffset)]; + currentOffset += 2; + // Parse the 'annotations' array field. + for (int i = 0; i < typeAnnotationsOffsets.length; ++i) { + typeAnnotationsOffsets[i] = currentOffset; + // Parse the type_annotation's target_type and the target_info fields. The size of the + // target_info field depends on the value of target_type. + int targetType = readInt(currentOffset); + switch (targetType >>> 24) { + case TypeReference.LOCAL_VARIABLE: + case TypeReference.RESOURCE_VARIABLE: + // A localvar_target has a variable size, which depends on the value of their table_length + // field. It also references bytecode offsets, for which we need labels. + int tableLength = readUnsignedShort(currentOffset + 1); + currentOffset += 3; + while (tableLength-- > 0) { + int startPc = readUnsignedShort(currentOffset); + int length = readUnsignedShort(currentOffset + 2); + // Skip the index field (2 bytes). + currentOffset += 6; + createLabel(startPc, context.currentMethodLabels); + createLabel(startPc + length, context.currentMethodLabels); + } + break; + case TypeReference.CAST: + case TypeReference.CONSTRUCTOR_INVOCATION_TYPE_ARGUMENT: + case TypeReference.METHOD_INVOCATION_TYPE_ARGUMENT: + case TypeReference.CONSTRUCTOR_REFERENCE_TYPE_ARGUMENT: + case TypeReference.METHOD_REFERENCE_TYPE_ARGUMENT: + currentOffset += 4; + break; + case TypeReference.CLASS_EXTENDS: + case TypeReference.CLASS_TYPE_PARAMETER_BOUND: + case TypeReference.METHOD_TYPE_PARAMETER_BOUND: + case TypeReference.THROWS: + case TypeReference.EXCEPTION_PARAMETER: + case TypeReference.INSTANCEOF: + case TypeReference.NEW: + case TypeReference.CONSTRUCTOR_REFERENCE: + case TypeReference.METHOD_REFERENCE: + currentOffset += 3; + break; + case TypeReference.CLASS_TYPE_PARAMETER: + case TypeReference.METHOD_TYPE_PARAMETER: + case TypeReference.METHOD_FORMAL_PARAMETER: + case TypeReference.FIELD: + case TypeReference.METHOD_RETURN: + case TypeReference.METHOD_RECEIVER: + default: + // TypeReference type which can't be used in Code attribute, or which is unknown. + throw new IllegalArgumentException(); + } + // Parse the rest of the type_annotation structure, starting with the target_path structure + // (whose size depends on its path_length field). + int pathLength = readByte(currentOffset); + if ((targetType >>> 24) == TypeReference.EXCEPTION_PARAMETER) { + // Parse the target_path structure and create a corresponding TypePath. + TypePath path = pathLength == 0 ? null : new TypePath(classFileBuffer, currentOffset); + currentOffset += 1 + 2 * pathLength; + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentOffset, charBuffer); + currentOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentOffset = + readElementValues( + methodVisitor.visitTryCatchAnnotation( + targetType & 0xFFFFFF00, path, annotationDescriptor, visible), + currentOffset, + /* named = */ true, + charBuffer); + } else { + // We don't want to visit the other target_type annotations, so we just skip them (which + // requires some parsing because the element_value_pairs array has a variable size). First, + // skip the target_path structure: + currentOffset += 3 + 2 * pathLength; + // Then skip the num_element_value_pairs and element_value_pairs fields (by reading them + // with a null AnnotationVisitor). + currentOffset = + readElementValues( + /* annotationVisitor = */ null, currentOffset, /* named = */ true, charBuffer); + } + } + return typeAnnotationsOffsets; + } + + /** + * Returns the bytecode offset corresponding to the specified JVMS 'type_annotation' structure, or + * -1 if there is no such type_annotation of if it does not have a bytecode offset. + * + * @param typeAnnotationOffsets the offset of each 'type_annotation' entry in a + * Runtime[In]VisibleTypeAnnotations attribute, or {@literal null}. + * @param typeAnnotationIndex the index a 'type_annotation' entry in typeAnnotationOffsets. + * @return bytecode offset corresponding to the specified JVMS 'type_annotation' structure, or -1 + * if there is no such type_annotation of if it does not have a bytecode offset. + */ + private int getTypeAnnotationBytecodeOffset( + final int[] typeAnnotationOffsets, final int typeAnnotationIndex) { + if (typeAnnotationOffsets == null + || typeAnnotationIndex >= typeAnnotationOffsets.length + || readByte(typeAnnotationOffsets[typeAnnotationIndex]) < TypeReference.INSTANCEOF) { + return -1; + } + return readUnsignedShort(typeAnnotationOffsets[typeAnnotationIndex] + 1); + } + + /** + * Parses the header of a JVMS type_annotation structure to extract its target_type, target_info + * and target_path (the result is stored in the given context), and returns the start offset of + * the rest of the type_annotation structure. + * + * @param context information about the class being parsed. This is where the extracted + * target_type and target_path must be stored. + * @param typeAnnotationOffset the start offset of a type_annotation structure. + * @return the start offset of the rest of the type_annotation structure. + */ + private int readTypeAnnotationTarget(final Context context, final int typeAnnotationOffset) { + int currentOffset = typeAnnotationOffset; + // Parse and store the target_type structure. + int targetType = readInt(typeAnnotationOffset); + switch (targetType >>> 24) { + case TypeReference.CLASS_TYPE_PARAMETER: + case TypeReference.METHOD_TYPE_PARAMETER: + case TypeReference.METHOD_FORMAL_PARAMETER: + targetType &= 0xFFFF0000; + currentOffset += 2; + break; + case TypeReference.FIELD: + case TypeReference.METHOD_RETURN: + case TypeReference.METHOD_RECEIVER: + targetType &= 0xFF000000; + currentOffset += 1; + break; + case TypeReference.LOCAL_VARIABLE: + case TypeReference.RESOURCE_VARIABLE: + targetType &= 0xFF000000; + int tableLength = readUnsignedShort(currentOffset + 1); + currentOffset += 3; + context.currentLocalVariableAnnotationRangeStarts = new Label[tableLength]; + context.currentLocalVariableAnnotationRangeEnds = new Label[tableLength]; + context.currentLocalVariableAnnotationRangeIndices = new int[tableLength]; + for (int i = 0; i < tableLength; ++i) { + int startPc = readUnsignedShort(currentOffset); + int length = readUnsignedShort(currentOffset + 2); + int index = readUnsignedShort(currentOffset + 4); + currentOffset += 6; + context.currentLocalVariableAnnotationRangeStarts[i] = + createLabel(startPc, context.currentMethodLabels); + context.currentLocalVariableAnnotationRangeEnds[i] = + createLabel(startPc + length, context.currentMethodLabels); + context.currentLocalVariableAnnotationRangeIndices[i] = index; + } + break; + case TypeReference.CAST: + case TypeReference.CONSTRUCTOR_INVOCATION_TYPE_ARGUMENT: + case TypeReference.METHOD_INVOCATION_TYPE_ARGUMENT: + case TypeReference.CONSTRUCTOR_REFERENCE_TYPE_ARGUMENT: + case TypeReference.METHOD_REFERENCE_TYPE_ARGUMENT: + targetType &= 0xFF0000FF; + currentOffset += 4; + break; + case TypeReference.CLASS_EXTENDS: + case TypeReference.CLASS_TYPE_PARAMETER_BOUND: + case TypeReference.METHOD_TYPE_PARAMETER_BOUND: + case TypeReference.THROWS: + case TypeReference.EXCEPTION_PARAMETER: + targetType &= 0xFFFFFF00; + currentOffset += 3; + break; + case TypeReference.INSTANCEOF: + case TypeReference.NEW: + case TypeReference.CONSTRUCTOR_REFERENCE: + case TypeReference.METHOD_REFERENCE: + targetType &= 0xFF000000; + currentOffset += 3; + break; + default: + throw new IllegalArgumentException(); + } + context.currentTypeAnnotationTarget = targetType; + // Parse and store the target_path structure. + int pathLength = readByte(currentOffset); + context.currentTypeAnnotationTargetPath = + pathLength == 0 ? null : new TypePath(classFileBuffer, currentOffset); + // Return the start offset of the rest of the type_annotation structure. + return currentOffset + 1 + 2 * pathLength; + } + + /** + * Reads a Runtime[In]VisibleParameterAnnotations attribute and makes the given visitor visit it. + * + * @param methodVisitor the visitor that must visit the parameter annotations. + * @param context information about the class being parsed. + * @param runtimeParameterAnnotationsOffset the start offset of a + * Runtime[In]VisibleParameterAnnotations attribute, excluding the attribute_info's + * attribute_name_index and attribute_length fields. + * @param visible true if the attribute to parse is a RuntimeVisibleParameterAnnotations + * attribute, false it is a RuntimeInvisibleParameterAnnotations attribute. + */ + private void readParameterAnnotations( + final MethodVisitor methodVisitor, + final Context context, + final int runtimeParameterAnnotationsOffset, + final boolean visible) { + int currentOffset = runtimeParameterAnnotationsOffset; + int numParameters = classFileBuffer[currentOffset++] & 0xFF; + methodVisitor.visitAnnotableParameterCount(numParameters, visible); + char[] charBuffer = context.charBuffer; + for (int i = 0; i < numParameters; ++i) { + int numAnnotations = readUnsignedShort(currentOffset); + currentOffset += 2; + while (numAnnotations-- > 0) { + // Parse the type_index field. + String annotationDescriptor = readUTF8(currentOffset, charBuffer); + currentOffset += 2; + // Parse num_element_value_pairs and element_value_pairs and visit these values. + currentOffset = + readElementValues( + methodVisitor.visitParameterAnnotation(i, annotationDescriptor, visible), + currentOffset, + /* named = */ true, + charBuffer); + } + } + } + + /** + * Reads the element values of a JVMS 'annotation' structure and makes the given visitor visit + * them. This method can also be used to read the values of the JVMS 'array_value' field of an + * annotation's 'element_value'. + * + * @param annotationVisitor the visitor that must visit the values. + * @param annotationOffset the start offset of an 'annotation' structure (excluding its type_index + * field) or of an 'array_value' structure. + * @param named if the annotation values are named or not. This should be true to parse the values + * of a JVMS 'annotation' structure, and false to parse the JVMS 'array_value' of an + * annotation's element_value. + * @param charBuffer the buffer used to read strings in the constant pool. + * @return the end offset of the JVMS 'annotation' or 'array_value' structure. + */ + private int readElementValues( + final AnnotationVisitor annotationVisitor, + final int annotationOffset, + final boolean named, + final char[] charBuffer) { + int currentOffset = annotationOffset; + // Read the num_element_value_pairs field (or num_values field for an array_value). + int numElementValuePairs = readUnsignedShort(currentOffset); + currentOffset += 2; + if (named) { + // Parse the element_value_pairs array. + while (numElementValuePairs-- > 0) { + String elementName = readUTF8(currentOffset, charBuffer); + currentOffset = + readElementValue(annotationVisitor, currentOffset + 2, elementName, charBuffer); + } + } else { + // Parse the array_value array. + while (numElementValuePairs-- > 0) { + currentOffset = + readElementValue(annotationVisitor, currentOffset, /* elementName = */ null, charBuffer); + } + } + if (annotationVisitor != null) { + annotationVisitor.visitEnd(); + } + return currentOffset; + } + + /** + * Reads a JVMS 'element_value' structure and makes the given visitor visit it. + * + * @param annotationVisitor the visitor that must visit the element_value structure. + * @param elementValueOffset the start offset in {@link #classFileBuffer} of the element_value + * structure to be read. + * @param elementName the name of the element_value structure to be read, or {@literal null}. + * @param charBuffer the buffer used to read strings in the constant pool. + * @return the end offset of the JVMS 'element_value' structure. + */ + private int readElementValue( + final AnnotationVisitor annotationVisitor, + final int elementValueOffset, + final String elementName, + final char[] charBuffer) { + int currentOffset = elementValueOffset; + if (annotationVisitor == null) { + switch (classFileBuffer[currentOffset] & 0xFF) { + case 'e': // enum_const_value + return currentOffset + 5; + case '@': // annotation_value + return readElementValues(null, currentOffset + 3, /* named = */ true, charBuffer); + case '[': // array_value + return readElementValues(null, currentOffset + 1, /* named = */ false, charBuffer); + default: + return currentOffset + 3; + } + } + switch (classFileBuffer[currentOffset++] & 0xFF) { + case 'B': // const_value_index, CONSTANT_Integer + annotationVisitor.visit( + elementName, (byte) readInt(cpInfoOffsets[readUnsignedShort(currentOffset)])); + currentOffset += 2; + break; + case 'C': // const_value_index, CONSTANT_Integer + annotationVisitor.visit( + elementName, (char) readInt(cpInfoOffsets[readUnsignedShort(currentOffset)])); + currentOffset += 2; + break; + case 'D': // const_value_index, CONSTANT_Double + case 'F': // const_value_index, CONSTANT_Float + case 'I': // const_value_index, CONSTANT_Integer + case 'J': // const_value_index, CONSTANT_Long + annotationVisitor.visit( + elementName, readConst(readUnsignedShort(currentOffset), charBuffer)); + currentOffset += 2; + break; + case 'S': // const_value_index, CONSTANT_Integer + annotationVisitor.visit( + elementName, (short) readInt(cpInfoOffsets[readUnsignedShort(currentOffset)])); + currentOffset += 2; + break; + + case 'Z': // const_value_index, CONSTANT_Integer + annotationVisitor.visit( + elementName, + readInt(cpInfoOffsets[readUnsignedShort(currentOffset)]) == 0 + ? Boolean.FALSE + : Boolean.TRUE); + currentOffset += 2; + break; + case 's': // const_value_index, CONSTANT_Utf8 + annotationVisitor.visit(elementName, readUTF8(currentOffset, charBuffer)); + currentOffset += 2; + break; + case 'e': // enum_const_value + annotationVisitor.visitEnum( + elementName, + readUTF8(currentOffset, charBuffer), + readUTF8(currentOffset + 2, charBuffer)); + currentOffset += 4; + break; + case 'c': // class_info + annotationVisitor.visit(elementName, Type.getType(readUTF8(currentOffset, charBuffer))); + currentOffset += 2; + break; + case '@': // annotation_value + currentOffset = + readElementValues( + annotationVisitor.visitAnnotation(elementName, readUTF8(currentOffset, charBuffer)), + currentOffset + 2, + true, + charBuffer); + break; + case '[': // array_value + int numValues = readUnsignedShort(currentOffset); + currentOffset += 2; + if (numValues == 0) { + return readElementValues( + annotationVisitor.visitArray(elementName), + currentOffset - 2, + /* named = */ false, + charBuffer); + } + switch (classFileBuffer[currentOffset] & 0xFF) { + case 'B': + byte[] byteValues = new byte[numValues]; + for (int i = 0; i < numValues; i++) { + byteValues[i] = (byte) readInt(cpInfoOffsets[readUnsignedShort(currentOffset + 1)]); + currentOffset += 3; + } + annotationVisitor.visit(elementName, byteValues); + break; + case 'Z': + boolean[] booleanValues = new boolean[numValues]; + for (int i = 0; i < numValues; i++) { + booleanValues[i] = readInt(cpInfoOffsets[readUnsignedShort(currentOffset + 1)]) != 0; + currentOffset += 3; + } + annotationVisitor.visit(elementName, booleanValues); + break; + case 'S': + short[] shortValues = new short[numValues]; + for (int i = 0; i < numValues; i++) { + shortValues[i] = (short) readInt(cpInfoOffsets[readUnsignedShort(currentOffset + 1)]); + currentOffset += 3; + } + annotationVisitor.visit(elementName, shortValues); + break; + case 'C': + char[] charValues = new char[numValues]; + for (int i = 0; i < numValues; i++) { + charValues[i] = (char) readInt(cpInfoOffsets[readUnsignedShort(currentOffset + 1)]); + currentOffset += 3; + } + annotationVisitor.visit(elementName, charValues); + break; + case 'I': + int[] intValues = new int[numValues]; + for (int i = 0; i < numValues; i++) { + intValues[i] = readInt(cpInfoOffsets[readUnsignedShort(currentOffset + 1)]); + currentOffset += 3; + } + annotationVisitor.visit(elementName, intValues); + break; + case 'J': + long[] longValues = new long[numValues]; + for (int i = 0; i < numValues; i++) { + longValues[i] = readLong(cpInfoOffsets[readUnsignedShort(currentOffset + 1)]); + currentOffset += 3; + } + annotationVisitor.visit(elementName, longValues); + break; + case 'F': + float[] floatValues = new float[numValues]; + for (int i = 0; i < numValues; i++) { + floatValues[i] = + Float.intBitsToFloat( + readInt(cpInfoOffsets[readUnsignedShort(currentOffset + 1)])); + currentOffset += 3; + } + annotationVisitor.visit(elementName, floatValues); + break; + case 'D': + double[] doubleValues = new double[numValues]; + for (int i = 0; i < numValues; i++) { + doubleValues[i] = + Double.longBitsToDouble( + readLong(cpInfoOffsets[readUnsignedShort(currentOffset + 1)])); + currentOffset += 3; + } + annotationVisitor.visit(elementName, doubleValues); + break; + default: + currentOffset = + readElementValues( + annotationVisitor.visitArray(elementName), + currentOffset - 2, + /* named = */ false, + charBuffer); + break; + } + break; + default: + throw new IllegalArgumentException(); + } + return currentOffset; + } + + // ---------------------------------------------------------------------------------------------- + // Methods to parse stack map frames + // ---------------------------------------------------------------------------------------------- + + /** + * Computes the implicit frame of the method currently being parsed (as defined in the given + * {@link Context}) and stores it in the given context. + * + * @param context information about the class being parsed. + */ + private void computeImplicitFrame(final Context context) { + String methodDescriptor = context.currentMethodDescriptor; + Object[] locals = context.currentFrameLocalTypes; + int numLocal = 0; + if ((context.currentMethodAccessFlags & Opcodes.ACC_STATIC) == 0) { + if ("".equals(context.currentMethodName)) { + locals[numLocal++] = Opcodes.UNINITIALIZED_THIS; + } else { + locals[numLocal++] = readClass(header + 2, context.charBuffer); + } + } + // Parse the method descriptor, one argument type descriptor at each iteration. Start by + // skipping the first method descriptor character, which is always '('. + int currentMethodDescritorOffset = 1; + while (true) { + int currentArgumentDescriptorStartOffset = currentMethodDescritorOffset; + switch (methodDescriptor.charAt(currentMethodDescritorOffset++)) { + case 'Z': + case 'C': + case 'B': + case 'S': + case 'I': + locals[numLocal++] = Opcodes.INTEGER; + break; + case 'F': + locals[numLocal++] = Opcodes.FLOAT; + break; + case 'J': + locals[numLocal++] = Opcodes.LONG; + break; + case 'D': + locals[numLocal++] = Opcodes.DOUBLE; + break; + case '[': + while (methodDescriptor.charAt(currentMethodDescritorOffset) == '[') { + ++currentMethodDescritorOffset; + } + if (methodDescriptor.charAt(currentMethodDescritorOffset) == 'L') { + ++currentMethodDescritorOffset; + while (methodDescriptor.charAt(currentMethodDescritorOffset) != ';') { + ++currentMethodDescritorOffset; + } + } + locals[numLocal++] = + methodDescriptor.substring( + currentArgumentDescriptorStartOffset, ++currentMethodDescritorOffset); + break; + case 'L': + while (methodDescriptor.charAt(currentMethodDescritorOffset) != ';') { + ++currentMethodDescritorOffset; + } + locals[numLocal++] = + methodDescriptor.substring( + currentArgumentDescriptorStartOffset + 1, currentMethodDescritorOffset++); + break; + default: + context.currentFrameLocalCount = numLocal; + return; + } + } + } + + /** + * Reads a JVMS 'stack_map_frame' structure and stores the result in the given {@link Context} + * object. This method can also be used to read a full_frame structure, excluding its frame_type + * field (this is used to parse the legacy StackMap attributes). + * + * @param stackMapFrameOffset the start offset in {@link #classFileBuffer} of the + * stack_map_frame_value structure to be read, or the start offset of a full_frame structure + * (excluding its frame_type field). + * @param compressed true to read a 'stack_map_frame' structure, false to read a 'full_frame' + * structure without its frame_type field. + * @param expand if the stack map frame must be expanded. See {@link #EXPAND_FRAMES}. + * @param context where the parsed stack map frame must be stored. + * @return the end offset of the JVMS 'stack_map_frame' or 'full_frame' structure. + */ + private int readStackMapFrame( + final int stackMapFrameOffset, + final boolean compressed, + final boolean expand, + final Context context) { + int currentOffset = stackMapFrameOffset; + final char[] charBuffer = context.charBuffer; + final Label[] labels = context.currentMethodLabels; + int frameType; + if (compressed) { + // Read the frame_type field. + frameType = classFileBuffer[currentOffset++] & 0xFF; + } else { + frameType = Frame.FULL_FRAME; + context.currentFrameOffset = -1; + } + int offsetDelta; + context.currentFrameLocalCountDelta = 0; + if (frameType < Frame.SAME_LOCALS_1_STACK_ITEM_FRAME) { + offsetDelta = frameType; + context.currentFrameType = Opcodes.F_SAME; + context.currentFrameStackCount = 0; + } else if (frameType < Frame.RESERVED) { + offsetDelta = frameType - Frame.SAME_LOCALS_1_STACK_ITEM_FRAME; + currentOffset = + readVerificationTypeInfo( + currentOffset, context.currentFrameStackTypes, 0, charBuffer, labels); + context.currentFrameType = Opcodes.F_SAME1; + context.currentFrameStackCount = 1; + } else if (frameType >= Frame.SAME_LOCALS_1_STACK_ITEM_FRAME_EXTENDED) { + offsetDelta = readUnsignedShort(currentOffset); + currentOffset += 2; + if (frameType == Frame.SAME_LOCALS_1_STACK_ITEM_FRAME_EXTENDED) { + currentOffset = + readVerificationTypeInfo( + currentOffset, context.currentFrameStackTypes, 0, charBuffer, labels); + context.currentFrameType = Opcodes.F_SAME1; + context.currentFrameStackCount = 1; + } else if (frameType >= Frame.CHOP_FRAME && frameType < Frame.SAME_FRAME_EXTENDED) { + context.currentFrameType = Opcodes.F_CHOP; + context.currentFrameLocalCountDelta = Frame.SAME_FRAME_EXTENDED - frameType; + context.currentFrameLocalCount -= context.currentFrameLocalCountDelta; + context.currentFrameStackCount = 0; + } else if (frameType == Frame.SAME_FRAME_EXTENDED) { + context.currentFrameType = Opcodes.F_SAME; + context.currentFrameStackCount = 0; + } else if (frameType < Frame.FULL_FRAME) { + int local = expand ? context.currentFrameLocalCount : 0; + for (int k = frameType - Frame.SAME_FRAME_EXTENDED; k > 0; k--) { + currentOffset = + readVerificationTypeInfo( + currentOffset, context.currentFrameLocalTypes, local++, charBuffer, labels); + } + context.currentFrameType = Opcodes.F_APPEND; + context.currentFrameLocalCountDelta = frameType - Frame.SAME_FRAME_EXTENDED; + context.currentFrameLocalCount += context.currentFrameLocalCountDelta; + context.currentFrameStackCount = 0; + } else { + final int numberOfLocals = readUnsignedShort(currentOffset); + currentOffset += 2; + context.currentFrameType = Opcodes.F_FULL; + context.currentFrameLocalCountDelta = numberOfLocals; + context.currentFrameLocalCount = numberOfLocals; + for (int local = 0; local < numberOfLocals; ++local) { + currentOffset = + readVerificationTypeInfo( + currentOffset, context.currentFrameLocalTypes, local, charBuffer, labels); + } + final int numberOfStackItems = readUnsignedShort(currentOffset); + currentOffset += 2; + context.currentFrameStackCount = numberOfStackItems; + for (int stack = 0; stack < numberOfStackItems; ++stack) { + currentOffset = + readVerificationTypeInfo( + currentOffset, context.currentFrameStackTypes, stack, charBuffer, labels); + } + } + } else { + throw new IllegalArgumentException(); + } + context.currentFrameOffset += offsetDelta + 1; + createLabel(context.currentFrameOffset, labels); + return currentOffset; + } + + /** + * Reads a JVMS 'verification_type_info' structure and stores it at the given index in the given + * array. + * + * @param verificationTypeInfoOffset the start offset of the 'verification_type_info' structure to + * read. + * @param frame the array where the parsed type must be stored. + * @param index the index in 'frame' where the parsed type must be stored. + * @param charBuffer the buffer used to read strings in the constant pool. + * @param labels the labels of the method currently being parsed, indexed by their offset. If the + * parsed type is an ITEM_Uninitialized, a new label for the corresponding NEW instruction is + * stored in this array if it does not already exist. + * @return the end offset of the JVMS 'verification_type_info' structure. + */ + private int readVerificationTypeInfo( + final int verificationTypeInfoOffset, + final Object[] frame, + final int index, + final char[] charBuffer, + final Label[] labels) { + int currentOffset = verificationTypeInfoOffset; + int tag = classFileBuffer[currentOffset++] & 0xFF; + switch (tag) { + case Frame.ITEM_TOP: + frame[index] = Opcodes.TOP; + break; + case Frame.ITEM_INTEGER: + frame[index] = Opcodes.INTEGER; + break; + case Frame.ITEM_FLOAT: + frame[index] = Opcodes.FLOAT; + break; + case Frame.ITEM_DOUBLE: + frame[index] = Opcodes.DOUBLE; + break; + case Frame.ITEM_LONG: + frame[index] = Opcodes.LONG; + break; + case Frame.ITEM_NULL: + frame[index] = Opcodes.NULL; + break; + case Frame.ITEM_UNINITIALIZED_THIS: + frame[index] = Opcodes.UNINITIALIZED_THIS; + break; + case Frame.ITEM_OBJECT: + frame[index] = readClass(currentOffset, charBuffer); + currentOffset += 2; + break; + case Frame.ITEM_UNINITIALIZED: + frame[index] = createLabel(readUnsignedShort(currentOffset), labels); + currentOffset += 2; + break; + default: + throw new IllegalArgumentException(); + } + return currentOffset; + } + + // ---------------------------------------------------------------------------------------------- + // Methods to parse attributes + // ---------------------------------------------------------------------------------------------- + + /** + * Returns the offset in {@link #classFileBuffer} of the first ClassFile's 'attributes' array + * field entry. + * + * @return the offset in {@link #classFileBuffer} of the first ClassFile's 'attributes' array + * field entry. + */ + final int getFirstAttributeOffset() { + // Skip the access_flags, this_class, super_class, and interfaces_count fields (using 2 bytes + // each), as well as the interfaces array field (2 bytes per interface). + int currentOffset = header + 8 + readUnsignedShort(header + 6) * 2; + + // Read the fields_count field. + int fieldsCount = readUnsignedShort(currentOffset); + currentOffset += 2; + // Skip the 'fields' array field. + while (fieldsCount-- > 0) { + // Invariant: currentOffset is the offset of a field_info structure. + // Skip the access_flags, name_index and descriptor_index fields (2 bytes each), and read the + // attributes_count field. + int attributesCount = readUnsignedShort(currentOffset + 6); + currentOffset += 8; + // Skip the 'attributes' array field. + while (attributesCount-- > 0) { + // Invariant: currentOffset is the offset of an attribute_info structure. + // Read the attribute_length field (2 bytes after the start of the attribute_info) and skip + // this many bytes, plus 6 for the attribute_name_index and attribute_length fields + // (yielding the total size of the attribute_info structure). + currentOffset += 6 + readInt(currentOffset + 2); + } + } + + // Skip the methods_count and 'methods' fields, using the same method as above. + int methodsCount = readUnsignedShort(currentOffset); + currentOffset += 2; + while (methodsCount-- > 0) { + int attributesCount = readUnsignedShort(currentOffset + 6); + currentOffset += 8; + while (attributesCount-- > 0) { + currentOffset += 6 + readInt(currentOffset + 2); + } + } + + // Skip the ClassFile's attributes_count field. + return currentOffset + 2; + } + + /** + * Reads the BootstrapMethods attribute to compute the offset of each bootstrap method. + * + * @param maxStringLength a conservative estimate of the maximum length of the strings contained + * in the constant pool of the class. + * @return the offsets of the bootstrap methods. + */ + private int[] readBootstrapMethodsAttribute(final int maxStringLength) { + char[] charBuffer = new char[maxStringLength]; + int currentAttributeOffset = getFirstAttributeOffset(); + int[] currentBootstrapMethodOffsets = null; + for (int i = readUnsignedShort(currentAttributeOffset - 2); i > 0; --i) { + // Read the attribute_info's attribute_name and attribute_length fields. + String attributeName = readUTF8(currentAttributeOffset, charBuffer); + int attributeLength = readInt(currentAttributeOffset + 2); + currentAttributeOffset += 6; + if (Constants.BOOTSTRAP_METHODS.equals(attributeName)) { + // Read the num_bootstrap_methods field and create an array of this size. + currentBootstrapMethodOffsets = new int[readUnsignedShort(currentAttributeOffset)]; + // Compute and store the offset of each 'bootstrap_methods' array field entry. + int currentBootstrapMethodOffset = currentAttributeOffset + 2; + for (int j = 0; j < currentBootstrapMethodOffsets.length; ++j) { + currentBootstrapMethodOffsets[j] = currentBootstrapMethodOffset; + // Skip the bootstrap_method_ref and num_bootstrap_arguments fields (2 bytes each), + // as well as the bootstrap_arguments array field (of size num_bootstrap_arguments * 2). + currentBootstrapMethodOffset += + 4 + readUnsignedShort(currentBootstrapMethodOffset + 2) * 2; + } + return currentBootstrapMethodOffsets; + } + currentAttributeOffset += attributeLength; + } + throw new IllegalArgumentException(); + } + + /** + * Reads a non standard JVMS 'attribute' structure in {@link #classFileBuffer}. + * + * @param attributePrototypes prototypes of the attributes that must be parsed during the visit of + * the class. Any attribute whose type is not equal to the type of one the prototypes will not + * be parsed: its byte array value will be passed unchanged to the ClassWriter. + * @param type the type of the attribute. + * @param offset the start offset of the JVMS 'attribute' structure in {@link #classFileBuffer}. + * The 6 attribute header bytes (attribute_name_index and attribute_length) are not taken into + * account here. + * @param length the length of the attribute's content (excluding the 6 attribute header bytes). + * @param charBuffer the buffer to be used to read strings in the constant pool. + * @param codeAttributeOffset the start offset of the enclosing Code attribute in {@link + * #classFileBuffer}, or -1 if the attribute to be read is not a code attribute. The 6 + * attribute header bytes (attribute_name_index and attribute_length) are not taken into + * account here. + * @param labels the labels of the method's code, or {@literal null} if the attribute to be read + * is not a code attribute. + * @return the attribute that has been read. + */ + private Attribute readAttribute( + final Attribute[] attributePrototypes, + final String type, + final int offset, + final int length, + final char[] charBuffer, + final int codeAttributeOffset, + final Label[] labels) { + for (Attribute attributePrototype : attributePrototypes) { + if (attributePrototype.type.equals(type)) { + return attributePrototype.read( + this, offset, length, charBuffer, codeAttributeOffset, labels); + } + } + return new Attribute(type).read(this, offset, length, null, -1, null); + } + + // ----------------------------------------------------------------------------------------------- + // Utility methods: low level parsing + // ----------------------------------------------------------------------------------------------- + + /** + * Returns the number of entries in the class's constant pool table. + * + * @return the number of entries in the class's constant pool table. + */ + public int getItemCount() { + return cpInfoOffsets.length; + } + + /** + * Returns the start offset in this {@link ClassReader} of a JVMS 'cp_info' structure (i.e. a + * constant pool entry), plus one. This method is intended for {@link Attribute} sub classes, + * and is normally not needed by class generators or adapters. + * + * @param constantPoolEntryIndex the index a constant pool entry in the class's constant pool + * table. + * @return the start offset in this {@link ClassReader} of the corresponding JVMS 'cp_info' + * structure, plus one. + */ + public int getItem(final int constantPoolEntryIndex) { + return cpInfoOffsets[constantPoolEntryIndex]; + } + + /** + * Returns a conservative estimate of the maximum length of the strings contained in the class's + * constant pool table. + * + * @return a conservative estimate of the maximum length of the strings contained in the class's + * constant pool table. + */ + public int getMaxStringLength() { + return maxStringLength; + } + + /** + * Reads a byte value in this {@link ClassReader}. This method is intended for {@link + * Attribute} sub classes, and is normally not needed by class generators or adapters. + * + * @param offset the start offset of the value to be read in this {@link ClassReader}. + * @return the read value. + */ + public int readByte(final int offset) { + return classFileBuffer[offset] & 0xFF; + } + + /** + * Reads an unsigned short value in this {@link ClassReader}. This method is intended for + * {@link Attribute} sub classes, and is normally not needed by class generators or adapters. + * + * @param offset the start index of the value to be read in this {@link ClassReader}. + * @return the read value. + */ + public int readUnsignedShort(final int offset) { + byte[] classBuffer = classFileBuffer; + return ((classBuffer[offset] & 0xFF) << 8) | (classBuffer[offset + 1] & 0xFF); + } + + /** + * Reads a signed short value in this {@link ClassReader}. This method is intended for {@link + * Attribute} sub classes, and is normally not needed by class generators or adapters. + * + * @param offset the start offset of the value to be read in this {@link ClassReader}. + * @return the read value. + */ + public short readShort(final int offset) { + byte[] classBuffer = classFileBuffer; + return (short) (((classBuffer[offset] & 0xFF) << 8) | (classBuffer[offset + 1] & 0xFF)); + } + + /** + * Reads a signed int value in this {@link ClassReader}. This method is intended for {@link + * Attribute} sub classes, and is normally not needed by class generators or adapters. + * + * @param offset the start offset of the value to be read in this {@link ClassReader}. + * @return the read value. + */ + public int readInt(final int offset) { + byte[] classBuffer = classFileBuffer; + return ((classBuffer[offset] & 0xFF) << 24) + | ((classBuffer[offset + 1] & 0xFF) << 16) + | ((classBuffer[offset + 2] & 0xFF) << 8) + | (classBuffer[offset + 3] & 0xFF); + } + + /** + * Reads a signed long value in this {@link ClassReader}. This method is intended for {@link + * Attribute} sub classes, and is normally not needed by class generators or adapters. + * + * @param offset the start offset of the value to be read in this {@link ClassReader}. + * @return the read value. + */ + public long readLong(final int offset) { + long l1 = readInt(offset); + long l0 = readInt(offset + 4) & 0xFFFFFFFFL; + return (l1 << 32) | l0; + } + + /** + * Reads a CONSTANT_Utf8 constant pool entry in this {@link ClassReader}. This method is + * intended for {@link Attribute} sub classes, and is normally not needed by class generators or + * adapters. + * + * @param offset the start offset of an unsigned short value in this {@link ClassReader}, whose + * value is the index of a CONSTANT_Utf8 entry in the class's constant pool table. + * @param charBuffer the buffer to be used to read the string. This buffer must be sufficiently + * large. It is not automatically resized. + * @return the String corresponding to the specified CONSTANT_Utf8 entry. + */ + // DontCheck(AbbreviationAsWordInName): can't be renamed (for backward binary compatibility). + public String readUTF8(final int offset, final char[] charBuffer) { + int constantPoolEntryIndex = readUnsignedShort(offset); + if (offset == 0 || constantPoolEntryIndex == 0) { + return null; + } + return readUtf(constantPoolEntryIndex, charBuffer); + } + + /** + * Reads a CONSTANT_Utf8 constant pool entry in {@link #classFileBuffer}. + * + * @param constantPoolEntryIndex the index of a CONSTANT_Utf8 entry in the class's constant pool + * table. + * @param charBuffer the buffer to be used to read the string. This buffer must be sufficiently + * large. It is not automatically resized. + * @return the String corresponding to the specified CONSTANT_Utf8 entry. + */ + final String readUtf(final int constantPoolEntryIndex, final char[] charBuffer) { + String value = constantUtf8Values[constantPoolEntryIndex]; + if (value != null) { + return value; + } + int cpInfoOffset = cpInfoOffsets[constantPoolEntryIndex]; + return constantUtf8Values[constantPoolEntryIndex] = + readUtf(cpInfoOffset + 2, readUnsignedShort(cpInfoOffset), charBuffer); + } + + /** + * Reads an UTF8 string in {@link #classFileBuffer}. + * + * @param utfOffset the start offset of the UTF8 string to be read. + * @param utfLength the length of the UTF8 string to be read. + * @param charBuffer the buffer to be used to read the string. This buffer must be sufficiently + * large. It is not automatically resized. + * @return the String corresponding to the specified UTF8 string. + */ + private String readUtf(final int utfOffset, final int utfLength, final char[] charBuffer) { + int currentOffset = utfOffset; + int endOffset = currentOffset + utfLength; + int strLength = 0; + byte[] classBuffer = classFileBuffer; + while (currentOffset < endOffset) { + int currentByte = classBuffer[currentOffset++]; + if ((currentByte & 0x80) == 0) { + charBuffer[strLength++] = (char) (currentByte & 0x7F); + } else if ((currentByte & 0xE0) == 0xC0) { + charBuffer[strLength++] = + (char) (((currentByte & 0x1F) << 6) + (classBuffer[currentOffset++] & 0x3F)); + } else { + charBuffer[strLength++] = + (char) + (((currentByte & 0xF) << 12) + + ((classBuffer[currentOffset++] & 0x3F) << 6) + + (classBuffer[currentOffset++] & 0x3F)); + } + } + return new String(charBuffer, 0, strLength); + } + + /** + * Reads a CONSTANT_Class, CONSTANT_String, CONSTANT_MethodType, CONSTANT_Module or + * CONSTANT_Package constant pool entry in {@link #classFileBuffer}. This method is intended + * for {@link Attribute} sub classes, and is normally not needed by class generators or + * adapters. + * + * @param offset the start offset of an unsigned short value in {@link #classFileBuffer}, whose + * value is the index of a CONSTANT_Class, CONSTANT_String, CONSTANT_MethodType, + * CONSTANT_Module or CONSTANT_Package entry in class's constant pool table. + * @param charBuffer the buffer to be used to read the item. This buffer must be sufficiently + * large. It is not automatically resized. + * @return the String corresponding to the specified constant pool entry. + */ + private String readStringish(final int offset, final char[] charBuffer) { + // Get the start offset of the cp_info structure (plus one), and read the CONSTANT_Utf8 entry + // designated by the first two bytes of this cp_info. + return readUTF8(cpInfoOffsets[readUnsignedShort(offset)], charBuffer); + } + + /** + * Reads a CONSTANT_Class constant pool entry in this {@link ClassReader}. This method is + * intended for {@link Attribute} sub classes, and is normally not needed by class generators or + * adapters. + * + * @param offset the start offset of an unsigned short value in this {@link ClassReader}, whose + * value is the index of a CONSTANT_Class entry in class's constant pool table. + * @param charBuffer the buffer to be used to read the item. This buffer must be sufficiently + * large. It is not automatically resized. + * @return the String corresponding to the specified CONSTANT_Class entry. + */ + public String readClass(final int offset, final char[] charBuffer) { + return readStringish(offset, charBuffer); + } + + /** + * Reads a CONSTANT_Module constant pool entry in this {@link ClassReader}. This method is + * intended for {@link Attribute} sub classes, and is normally not needed by class generators or + * adapters. + * + * @param offset the start offset of an unsigned short value in this {@link ClassReader}, whose + * value is the index of a CONSTANT_Module entry in class's constant pool table. + * @param charBuffer the buffer to be used to read the item. This buffer must be sufficiently + * large. It is not automatically resized. + * @return the String corresponding to the specified CONSTANT_Module entry. + */ + public String readModule(final int offset, final char[] charBuffer) { + return readStringish(offset, charBuffer); + } + + /** + * Reads a CONSTANT_Package constant pool entry in this {@link ClassReader}. This method is + * intended for {@link Attribute} sub classes, and is normally not needed by class generators or + * adapters. + * + * @param offset the start offset of an unsigned short value in this {@link ClassReader}, whose + * value is the index of a CONSTANT_Package entry in class's constant pool table. + * @param charBuffer the buffer to be used to read the item. This buffer must be sufficiently + * large. It is not automatically resized. + * @return the String corresponding to the specified CONSTANT_Package entry. + */ + public String readPackage(final int offset, final char[] charBuffer) { + return readStringish(offset, charBuffer); + } + + /** + * Reads a CONSTANT_Dynamic constant pool entry in {@link #classFileBuffer}. + * + * @param constantPoolEntryIndex the index of a CONSTANT_Dynamic entry in the class's constant + * pool table. + * @param charBuffer the buffer to be used to read the string. This buffer must be sufficiently + * large. It is not automatically resized. + * @return the ConstantDynamic corresponding to the specified CONSTANT_Dynamic entry. + */ + private ConstantDynamic readConstantDynamic( + final int constantPoolEntryIndex, final char[] charBuffer) { + ConstantDynamic constantDynamic = constantDynamicValues[constantPoolEntryIndex]; + if (constantDynamic != null) { + return constantDynamic; + } + int cpInfoOffset = cpInfoOffsets[constantPoolEntryIndex]; + int nameAndTypeCpInfoOffset = cpInfoOffsets[readUnsignedShort(cpInfoOffset + 2)]; + String name = readUTF8(nameAndTypeCpInfoOffset, charBuffer); + String descriptor = readUTF8(nameAndTypeCpInfoOffset + 2, charBuffer); + int bootstrapMethodOffset = bootstrapMethodOffsets[readUnsignedShort(cpInfoOffset)]; + Handle handle = (Handle) readConst(readUnsignedShort(bootstrapMethodOffset), charBuffer); + Object[] bootstrapMethodArguments = new Object[readUnsignedShort(bootstrapMethodOffset + 2)]; + bootstrapMethodOffset += 4; + for (int i = 0; i < bootstrapMethodArguments.length; i++) { + bootstrapMethodArguments[i] = readConst(readUnsignedShort(bootstrapMethodOffset), charBuffer); + bootstrapMethodOffset += 2; + } + return constantDynamicValues[constantPoolEntryIndex] = + new ConstantDynamic(name, descriptor, handle, bootstrapMethodArguments); + } + + /** + * Reads a numeric or string constant pool entry in this {@link ClassReader}. This method is + * intended for {@link Attribute} sub classes, and is normally not needed by class generators or + * adapters. + * + * @param constantPoolEntryIndex the index of a CONSTANT_Integer, CONSTANT_Float, CONSTANT_Long, + * CONSTANT_Double, CONSTANT_Class, CONSTANT_String, CONSTANT_MethodType, + * CONSTANT_MethodHandle or CONSTANT_Dynamic entry in the class's constant pool. + * @param charBuffer the buffer to be used to read strings. This buffer must be sufficiently + * large. It is not automatically resized. + * @return the {@link Integer}, {@link Float}, {@link Long}, {@link Double}, {@link String}, + * {@link Type}, {@link Handle} or {@link ConstantDynamic} corresponding to the specified + * constant pool entry. + */ + public Object readConst(final int constantPoolEntryIndex, final char[] charBuffer) { + int cpInfoOffset = cpInfoOffsets[constantPoolEntryIndex]; + switch (classFileBuffer[cpInfoOffset - 1]) { + case Symbol.CONSTANT_INTEGER_TAG: + return readInt(cpInfoOffset); + case Symbol.CONSTANT_FLOAT_TAG: + return Float.intBitsToFloat(readInt(cpInfoOffset)); + case Symbol.CONSTANT_LONG_TAG: + return readLong(cpInfoOffset); + case Symbol.CONSTANT_DOUBLE_TAG: + return Double.longBitsToDouble(readLong(cpInfoOffset)); + case Symbol.CONSTANT_CLASS_TAG: + return Type.getObjectType(readUTF8(cpInfoOffset, charBuffer)); + case Symbol.CONSTANT_STRING_TAG: + return readUTF8(cpInfoOffset, charBuffer); + case Symbol.CONSTANT_METHOD_TYPE_TAG: + return Type.getMethodType(readUTF8(cpInfoOffset, charBuffer)); + case Symbol.CONSTANT_METHOD_HANDLE_TAG: + int referenceKind = readByte(cpInfoOffset); + int referenceCpInfoOffset = cpInfoOffsets[readUnsignedShort(cpInfoOffset + 1)]; + int nameAndTypeCpInfoOffset = cpInfoOffsets[readUnsignedShort(referenceCpInfoOffset + 2)]; + String owner = readClass(referenceCpInfoOffset, charBuffer); + String name = readUTF8(nameAndTypeCpInfoOffset, charBuffer); + String descriptor = readUTF8(nameAndTypeCpInfoOffset + 2, charBuffer); + boolean isInterface = + classFileBuffer[referenceCpInfoOffset - 1] == Symbol.CONSTANT_INTERFACE_METHODREF_TAG; + return new Handle(referenceKind, owner, name, descriptor, isInterface); + case Symbol.CONSTANT_DYNAMIC_TAG: + return readConstantDynamic(constantPoolEntryIndex, charBuffer); + default: + throw new IllegalArgumentException(); + } + } +} diff --git a/spring-core/src/main/java/org/springframework/asm/ClassTooLargeException.java b/spring-core/src/main/java/org/springframework/asm/ClassTooLargeException.java new file mode 100644 index 0000000..f679328 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/ClassTooLargeException.java @@ -0,0 +1,71 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +package org.springframework.asm; + +/** + * Exception thrown when the constant pool of a class produced by a {@link ClassWriter} is too + * large. + * + * @author Jason Zaugg + */ +public final class ClassTooLargeException extends IndexOutOfBoundsException { + private static final long serialVersionUID = 160715609518896765L; + + private final String className; + private final int constantPoolCount; + + /** + * Constructs a new {@link ClassTooLargeException}. + * + * @param className the internal name of the class. + * @param constantPoolCount the number of constant pool items of the class. + */ + public ClassTooLargeException(final String className, final int constantPoolCount) { + super("Class too large: " + className); + this.className = className; + this.constantPoolCount = constantPoolCount; + } + + /** + * Returns the internal name of the class. + * + * @return the internal name of the class. + */ + public String getClassName() { + return className; + } + + /** + * Returns the number of constant pool items of the class. + * + * @return the number of constant pool items of the class. + */ + public int getConstantPoolCount() { + return constantPoolCount; + } +} diff --git a/spring-core/src/main/java/org/springframework/asm/ClassVisitor.java b/spring-core/src/main/java/org/springframework/asm/ClassVisitor.java new file mode 100644 index 0000000..10a549a --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/ClassVisitor.java @@ -0,0 +1,378 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +package org.springframework.asm; + +/** + * A visitor to visit a Java class. The methods of this class must be called in the following order: + * {@code visit} [ {@code visitSource} ] [ {@code visitModule} ][ {@code visitNestHost} ][ {@code + * visitPermittedSubclass} ][ {@code visitOuterClass} ] ( {@code visitAnnotation} | {@code + * visitTypeAnnotation} | {@code visitAttribute} )* ( {@code visitNestMember} | {@code + * visitInnerClass} | {@code visitRecordComponent} | {@code visitField} | {@code visitMethod} )* + * {@code visitEnd}. + * + * @author Eric Bruneton + */ +public abstract class ClassVisitor { + + /** + * The ASM API version implemented by this visitor. The value of this field must be one of {@link + * Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6} or {@link Opcodes#ASM7}. + */ + protected final int api; + + /** The class visitor to which this visitor must delegate method calls. May be {@literal null}. */ + protected ClassVisitor cv; + + /** + * Constructs a new {@link ClassVisitor}. + * + * @param api the ASM API version implemented by this visitor. Must be one of {@link + * Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6} or {@link Opcodes#ASM7}. + */ + public ClassVisitor(final int api) { + this(api, null); + } + + /** + * Constructs a new {@link ClassVisitor}. + * + * @param api the ASM API version implemented by this visitor. Must be one of {@link + * Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6}, {@link Opcodes#ASM7}, {@link + * Opcodes#ASM8} or {@link Opcodes#ASM9}. + * @param classVisitor the class visitor to which this visitor must delegate method calls. May be + * null. + */ + public ClassVisitor(final int api, final ClassVisitor classVisitor) { + if (api != Opcodes.ASM9 + && api != Opcodes.ASM8 + && api != Opcodes.ASM7 + && api != Opcodes.ASM6 + && api != Opcodes.ASM5 + && api != Opcodes.ASM4 + && api != Opcodes.ASM10_EXPERIMENTAL) { + throw new IllegalArgumentException("Unsupported api " + api); + } + // SPRING PATCH: no preview mode check for ASM 9 experimental + this.api = api; + this.cv = classVisitor; + } + + /** + * Visits the header of the class. + * + * @param version the class version. The minor version is stored in the 16 most significant bits, + * and the major version in the 16 least significant bits. + * @param access the class's access flags (see {@link Opcodes}). This parameter also indicates if + * the class is deprecated {@link Opcodes#ACC_DEPRECATED} or a record {@link + * Opcodes#ACC_RECORD}. + * @param name the internal name of the class (see {@link Type#getInternalName()}). + * @param signature the signature of this class. May be {@literal null} if the class is not a + * generic one, and does not extend or implement generic classes or interfaces. + * @param superName the internal of name of the super class (see {@link Type#getInternalName()}). + * For interfaces, the super class is {@link Object}. May be {@literal null}, but only for the + * {@link Object} class. + * @param interfaces the internal names of the class's interfaces (see {@link + * Type#getInternalName()}). May be {@literal null}. + */ + public void visit( + final int version, + final int access, + final String name, + final String signature, + final String superName, + final String[] interfaces) { + if (api < Opcodes.ASM8 && (access & Opcodes.ACC_RECORD) != 0) { + throw new UnsupportedOperationException("Records requires ASM8"); + } + if (cv != null) { + cv.visit(version, access, name, signature, superName, interfaces); + } + } + + /** + * Visits the source of the class. + * + * @param source the name of the source file from which the class was compiled. May be {@literal + * null}. + * @param debug additional debug information to compute the correspondence between source and + * compiled elements of the class. May be {@literal null}. + */ + public void visitSource(final String source, final String debug) { + if (cv != null) { + cv.visitSource(source, debug); + } + } + + /** + * Visit the module corresponding to the class. + * + * @param name the fully qualified name (using dots) of the module. + * @param access the module access flags, among {@code ACC_OPEN}, {@code ACC_SYNTHETIC} and {@code + * ACC_MANDATED}. + * @param version the module version, or {@literal null}. + * @return a visitor to visit the module values, or {@literal null} if this visitor is not + * interested in visiting this module. + */ + public ModuleVisitor visitModule(final String name, final int access, final String version) { + if (api < Opcodes.ASM6) { + throw new UnsupportedOperationException("Module requires ASM6"); + } + if (cv != null) { + return cv.visitModule(name, access, version); + } + return null; + } + + /** + * Visits the nest host class of the class. A nest is a set of classes of the same package that + * share access to their private members. One of these classes, called the host, lists the other + * members of the nest, which in turn should link to the host of their nest. This method must be + * called only once and only if the visited class is a non-host member of a nest. A class is + * implicitly its own nest, so it's invalid to call this method with the visited class name as + * argument. + * + * @param nestHost the internal name of the host class of the nest. + */ + public void visitNestHost(final String nestHost) { + if (api < Opcodes.ASM7) { + throw new UnsupportedOperationException("NestHost requires ASM7"); + } + if (cv != null) { + cv.visitNestHost(nestHost); + } + } + + /** + * Visits the enclosing class of the class. This method must be called only if the class has an + * enclosing class. + * + * @param owner internal name of the enclosing class of the class. + * @param name the name of the method that contains the class, or {@literal null} if the class is + * not enclosed in a method of its enclosing class. + * @param descriptor the descriptor of the method that contains the class, or {@literal null} if + * the class is not enclosed in a method of its enclosing class. + */ + public void visitOuterClass(final String owner, final String name, final String descriptor) { + if (cv != null) { + cv.visitOuterClass(owner, name, descriptor); + } + } + + /** + * Visits an annotation of the class. + * + * @param descriptor the class descriptor of the annotation class. + * @param visible {@literal true} if the annotation is visible at runtime. + * @return a visitor to visit the annotation values, or {@literal null} if this visitor is not + * interested in visiting this annotation. + */ + public AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) { + if (cv != null) { + return cv.visitAnnotation(descriptor, visible); + } + return null; + } + + /** + * Visits an annotation on a type in the class signature. + * + * @param typeRef a reference to the annotated type. The sort of this type reference must be + * {@link TypeReference#CLASS_TYPE_PARAMETER}, {@link + * TypeReference#CLASS_TYPE_PARAMETER_BOUND} or {@link TypeReference#CLASS_EXTENDS}. See + * {@link TypeReference}. + * @param typePath the path to the annotated type argument, wildcard bound, array element type, or + * static inner type within 'typeRef'. May be {@literal null} if the annotation targets + * 'typeRef' as a whole. + * @param descriptor the class descriptor of the annotation class. + * @param visible {@literal true} if the annotation is visible at runtime. + * @return a visitor to visit the annotation values, or {@literal null} if this visitor is not + * interested in visiting this annotation. + */ + public AnnotationVisitor visitTypeAnnotation( + final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) { + if (api < Opcodes.ASM5) { + throw new UnsupportedOperationException("TypeAnnotation requires ASM5"); + } + if (cv != null) { + return cv.visitTypeAnnotation(typeRef, typePath, descriptor, visible); + } + return null; + } + + /** + * Visits a non standard attribute of the class. + * + * @param attribute an attribute. + */ + public void visitAttribute(final Attribute attribute) { + if (cv != null) { + cv.visitAttribute(attribute); + } + } + + /** + * Visits a member of the nest. A nest is a set of classes of the same package that share access + * to their private members. One of these classes, called the host, lists the other members of the + * nest, which in turn should link to the host of their nest. This method must be called only if + * the visited class is the host of a nest. A nest host is implicitly a member of its own nest, so + * it's invalid to call this method with the visited class name as argument. + * + * @param nestMember the internal name of a nest member. + */ + public void visitNestMember(final String nestMember) { + if (api < Opcodes.ASM7) { + throw new UnsupportedOperationException("NestMember requires ASM7"); + } + if (cv != null) { + cv.visitNestMember(nestMember); + } + } + + /** + * Visits a permitted subclasses. A permitted subclass is one of the allowed subclasses of the + * current class. + * + * @param permittedSubclass the internal name of a permitted subclass. + */ + public void visitPermittedSubclass(final String permittedSubclass) { + if (api < Opcodes.ASM9) { + throw new UnsupportedOperationException("PermittedSubclasses requires ASM9"); + } + if (cv != null) { + cv.visitPermittedSubclass(permittedSubclass); + } + } + + /** + * Visits information about an inner class. This inner class is not necessarily a member of the + * class being visited. + * + * @param name the internal name of an inner class (see {@link Type#getInternalName()}). + * @param outerName the internal name of the class to which the inner class belongs (see {@link + * Type#getInternalName()}). May be {@literal null} for not member classes. + * @param innerName the (simple) name of the inner class inside its enclosing class. May be + * {@literal null} for anonymous inner classes. + * @param access the access flags of the inner class as originally declared in the enclosing + * class. + */ + public void visitInnerClass( + final String name, final String outerName, final String innerName, final int access) { + if (cv != null) { + cv.visitInnerClass(name, outerName, innerName, access); + } + } + + /** + * Visits a record component of the class. + * + * @param name the record component name. + * @param descriptor the record component descriptor (see {@link Type}). + * @param signature the record component signature. May be {@literal null} if the record component + * type does not use generic types. + * @return a visitor to visit this record component annotations and attributes, or {@literal null} + * if this class visitor is not interested in visiting these annotations and attributes. + */ + public RecordComponentVisitor visitRecordComponent( + final String name, final String descriptor, final String signature) { + if (api < Opcodes.ASM8) { + throw new UnsupportedOperationException("Record requires ASM8"); + } + if (cv != null) { + return cv.visitRecordComponent(name, descriptor, signature); + } + return null; + } + + /** + * Visits a field of the class. + * + * @param access the field's access flags (see {@link Opcodes}). This parameter also indicates if + * the field is synthetic and/or deprecated. + * @param name the field's name. + * @param descriptor the field's descriptor (see {@link Type}). + * @param signature the field's signature. May be {@literal null} if the field's type does not use + * generic types. + * @param value the field's initial value. This parameter, which may be {@literal null} if the + * field does not have an initial value, must be an {@link Integer}, a {@link Float}, a {@link + * Long}, a {@link Double} or a {@link String} (for {@code int}, {@code float}, {@code long} + * or {@code String} fields respectively). This parameter is only used for static + * fields. Its value is ignored for non static fields, which must be initialized through + * bytecode instructions in constructors or methods. + * @return a visitor to visit field annotations and attributes, or {@literal null} if this class + * visitor is not interested in visiting these annotations and attributes. + */ + public FieldVisitor visitField( + final int access, + final String name, + final String descriptor, + final String signature, + final Object value) { + if (cv != null) { + return cv.visitField(access, name, descriptor, signature, value); + } + return null; + } + + /** + * Visits a method of the class. This method must return a new {@link MethodVisitor} + * instance (or {@literal null}) each time it is called, i.e., it should not return a previously + * returned visitor. + * + * @param access the method's access flags (see {@link Opcodes}). This parameter also indicates if + * the method is synthetic and/or deprecated. + * @param name the method's name. + * @param descriptor the method's descriptor (see {@link Type}). + * @param signature the method's signature. May be {@literal null} if the method parameters, + * return type and exceptions do not use generic types. + * @param exceptions the internal names of the method's exception classes (see {@link + * Type#getInternalName()}). May be {@literal null}. + * @return an object to visit the byte code of the method, or {@literal null} if this class + * visitor is not interested in visiting the code of this method. + */ + public MethodVisitor visitMethod( + final int access, + final String name, + final String descriptor, + final String signature, + final String[] exceptions) { + if (cv != null) { + return cv.visitMethod(access, name, descriptor, signature, exceptions); + } + return null; + } + + /** + * Visits the end of the class. This method, which is the last one to be called, is used to inform + * the visitor that all the fields and methods of the class have been visited. + */ + public void visitEnd() { + if (cv != null) { + cv.visitEnd(); + } + } +} diff --git a/spring-core/src/main/java/org/springframework/asm/ClassWriter.java b/spring-core/src/main/java/org/springframework/asm/ClassWriter.java new file mode 100644 index 0000000..8730027 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/ClassWriter.java @@ -0,0 +1,1060 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +package org.springframework.asm; + +/** + * A {@link ClassVisitor} that generates a corresponding ClassFile structure, as defined in the Java + * Virtual Machine Specification (JVMS). It can be used alone, to generate a Java class "from + * scratch", or with one or more {@link ClassReader} and adapter {@link ClassVisitor} to generate a + * modified class from one or more existing Java classes. + * + * @see JVMS 4 + * @author Eric Bruneton + */ +public class ClassWriter extends ClassVisitor { + + /** + * A flag to automatically compute the maximum stack size and the maximum number of local + * variables of methods. If this flag is set, then the arguments of the {@link + * MethodVisitor#visitMaxs} method of the {@link MethodVisitor} returned by the {@link + * #visitMethod} method will be ignored, and computed automatically from the signature and the + * bytecode of each method. + * + *

    Note: for classes whose version is {@link Opcodes#V1_7} of more, this option requires + * valid stack map frames. The maximum stack size is then computed from these frames, and from the + * bytecode instructions in between. If stack map frames are not present or must be recomputed, + * used {@link #COMPUTE_FRAMES} instead. + * + * @see #ClassWriter(int) + */ + public static final int COMPUTE_MAXS = 1; + + /** + * A flag to automatically compute the stack map frames of methods from scratch. If this flag is + * set, then the calls to the {@link MethodVisitor#visitFrame} method are ignored, and the stack + * map frames are recomputed from the methods bytecode. The arguments of the {@link + * MethodVisitor#visitMaxs} method are also ignored and recomputed from the bytecode. In other + * words, {@link #COMPUTE_FRAMES} implies {@link #COMPUTE_MAXS}. + * + * @see #ClassWriter(int) + */ + public static final int COMPUTE_FRAMES = 2; + + // Note: fields are ordered as in the ClassFile structure, and those related to attributes are + // ordered as in Section 4.7 of the JVMS. + + /** + * The minor_version and major_version fields of the JVMS ClassFile structure. minor_version is + * stored in the 16 most significant bits, and major_version in the 16 least significant bits. + */ + private int version; + + /** The symbol table for this class (contains the constant_pool and the BootstrapMethods). */ + private final SymbolTable symbolTable; + + /** + * The access_flags field of the JVMS ClassFile structure. This field can contain ASM specific + * access flags, such as {@link Opcodes#ACC_DEPRECATED} or {}@link Opcodes#ACC_RECORD}, which are + * removed when generating the ClassFile structure. + */ + private int accessFlags; + + /** The this_class field of the JVMS ClassFile structure. */ + private int thisClass; + + /** The super_class field of the JVMS ClassFile structure. */ + private int superClass; + + /** The interface_count field of the JVMS ClassFile structure. */ + private int interfaceCount; + + /** The 'interfaces' array of the JVMS ClassFile structure. */ + private int[] interfaces; + + /** + * The fields of this class, stored in a linked list of {@link FieldWriter} linked via their + * {@link FieldWriter#fv} field. This field stores the first element of this list. + */ + private FieldWriter firstField; + + /** + * The fields of this class, stored in a linked list of {@link FieldWriter} linked via their + * {@link FieldWriter#fv} field. This field stores the last element of this list. + */ + private FieldWriter lastField; + + /** + * The methods of this class, stored in a linked list of {@link MethodWriter} linked via their + * {@link MethodWriter#mv} field. This field stores the first element of this list. + */ + private MethodWriter firstMethod; + + /** + * The methods of this class, stored in a linked list of {@link MethodWriter} linked via their + * {@link MethodWriter#mv} field. This field stores the last element of this list. + */ + private MethodWriter lastMethod; + + /** The number_of_classes field of the InnerClasses attribute, or 0. */ + private int numberOfInnerClasses; + + /** The 'classes' array of the InnerClasses attribute, or {@literal null}. */ + private ByteVector innerClasses; + + /** The class_index field of the EnclosingMethod attribute, or 0. */ + private int enclosingClassIndex; + + /** The method_index field of the EnclosingMethod attribute. */ + private int enclosingMethodIndex; + + /** The signature_index field of the Signature attribute, or 0. */ + private int signatureIndex; + + /** The source_file_index field of the SourceFile attribute, or 0. */ + private int sourceFileIndex; + + /** The debug_extension field of the SourceDebugExtension attribute, or {@literal null}. */ + private ByteVector debugExtension; + + /** + * The last runtime visible annotation of this class. The previous ones can be accessed with the + * {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeVisibleAnnotation; + + /** + * The last runtime invisible annotation of this class. The previous ones can be accessed with the + * {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeInvisibleAnnotation; + + /** + * The last runtime visible type annotation of this class. The previous ones can be accessed with + * the {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeVisibleTypeAnnotation; + + /** + * The last runtime invisible type annotation of this class. The previous ones can be accessed + * with the {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeInvisibleTypeAnnotation; + + /** The Module attribute of this class, or {@literal null}. */ + private ModuleWriter moduleWriter; + + /** The host_class_index field of the NestHost attribute, or 0. */ + private int nestHostClassIndex; + + /** The number_of_classes field of the NestMembers attribute, or 0. */ + private int numberOfNestMemberClasses; + + /** The 'classes' array of the NestMembers attribute, or {@literal null}. */ + private ByteVector nestMemberClasses; + + /** The number_of_classes field of the PermittedSubclasses attribute, or 0. */ + private int numberOfPermittedSubclasses; + + /** The 'classes' array of the PermittedSubclasses attribute, or {@literal null}. */ + private ByteVector permittedSubclasses; + + /** + * The record components of this class, stored in a linked list of {@link RecordComponentWriter} + * linked via their {@link RecordComponentWriter#delegate} field. This field stores the first + * element of this list. + */ + private RecordComponentWriter firstRecordComponent; + + /** + * The record components of this class, stored in a linked list of {@link RecordComponentWriter} + * linked via their {@link RecordComponentWriter#delegate} field. This field stores the last + * element of this list. + */ + private RecordComponentWriter lastRecordComponent; + + /** + * The first non standard attribute of this class. The next ones can be accessed with the {@link + * Attribute#nextAttribute} field. May be {@literal null}. + * + *

    WARNING: this list stores the attributes in the reverse order of their visit. + * firstAttribute is actually the last attribute visited in {@link #visitAttribute}. The {@link + * #toByteArray} method writes the attributes in the order defined by this list, i.e. in the + * reverse order specified by the user. + */ + private Attribute firstAttribute; + + /** + * Indicates what must be automatically computed in {@link MethodWriter}. Must be one of {@link + * MethodWriter#COMPUTE_NOTHING}, {@link MethodWriter#COMPUTE_MAX_STACK_AND_LOCAL}, {@link + * MethodWriter#COMPUTE_INSERTED_FRAMES}, or {@link MethodWriter#COMPUTE_ALL_FRAMES}. + */ + private int compute; + + // ----------------------------------------------------------------------------------------------- + // Constructor + // ----------------------------------------------------------------------------------------------- + + /** + * Constructs a new {@link ClassWriter} object. + * + * @param flags option flags that can be used to modify the default behavior of this class. Must + * be zero or more of {@link #COMPUTE_MAXS} and {@link #COMPUTE_FRAMES}. + */ + public ClassWriter(final int flags) { + this(null, flags); + } + + /** + * Constructs a new {@link ClassWriter} object and enables optimizations for "mostly add" bytecode + * transformations. These optimizations are the following: + * + *

      + *
    • The constant pool and bootstrap methods from the original class are copied as is in the + * new class, which saves time. New constant pool entries and new bootstrap methods will be + * added at the end if necessary, but unused constant pool entries or bootstrap methods + * won't be removed. + *
    • Methods that are not transformed are copied as is in the new class, directly from the + * original class bytecode (i.e. without emitting visit events for all the method + * instructions), which saves a lot of time. Untransformed methods are detected by + * the fact that the {@link ClassReader} receives {@link MethodVisitor} objects that come + * from a {@link ClassWriter} (and not from any other {@link ClassVisitor} instance). + *
    + * + * @param classReader the {@link ClassReader} used to read the original class. It will be used to + * copy the entire constant pool and bootstrap methods from the original class and also to + * copy other fragments of original bytecode where applicable. + * @param flags option flags that can be used to modify the default behavior of this class.Must be + * zero or more of {@link #COMPUTE_MAXS} and {@link #COMPUTE_FRAMES}. These option flags do + * not affect methods that are copied as is in the new class. This means that neither the + * maximum stack size nor the stack frames will be computed for these methods. + */ + public ClassWriter(final ClassReader classReader, final int flags) { + super(/* latest api = */ Opcodes.ASM9); + symbolTable = classReader == null ? new SymbolTable(this) : new SymbolTable(this, classReader); + if ((flags & COMPUTE_FRAMES) != 0) { + this.compute = MethodWriter.COMPUTE_ALL_FRAMES; + } else if ((flags & COMPUTE_MAXS) != 0) { + this.compute = MethodWriter.COMPUTE_MAX_STACK_AND_LOCAL; + } else { + this.compute = MethodWriter.COMPUTE_NOTHING; + } + } + + // ----------------------------------------------------------------------------------------------- + // Implementation of the ClassVisitor abstract class + // ----------------------------------------------------------------------------------------------- + + @Override + public final void visit( + final int version, + final int access, + final String name, + final String signature, + final String superName, + final String[] interfaces) { + this.version = version; + this.accessFlags = access; + this.thisClass = symbolTable.setMajorVersionAndClassName(version & 0xFFFF, name); + if (signature != null) { + this.signatureIndex = symbolTable.addConstantUtf8(signature); + } + this.superClass = superName == null ? 0 : symbolTable.addConstantClass(superName).index; + if (interfaces != null && interfaces.length > 0) { + interfaceCount = interfaces.length; + this.interfaces = new int[interfaceCount]; + for (int i = 0; i < interfaceCount; ++i) { + this.interfaces[i] = symbolTable.addConstantClass(interfaces[i]).index; + } + } + if (compute == MethodWriter.COMPUTE_MAX_STACK_AND_LOCAL && (version & 0xFFFF) >= Opcodes.V1_7) { + compute = MethodWriter.COMPUTE_MAX_STACK_AND_LOCAL_FROM_FRAMES; + } + } + + @Override + public final void visitSource(final String file, final String debug) { + if (file != null) { + sourceFileIndex = symbolTable.addConstantUtf8(file); + } + if (debug != null) { + debugExtension = new ByteVector().encodeUtf8(debug, 0, Integer.MAX_VALUE); + } + } + + @Override + public final ModuleVisitor visitModule( + final String name, final int access, final String version) { + return moduleWriter = + new ModuleWriter( + symbolTable, + symbolTable.addConstantModule(name).index, + access, + version == null ? 0 : symbolTable.addConstantUtf8(version)); + } + + @Override + public final void visitNestHost(final String nestHost) { + nestHostClassIndex = symbolTable.addConstantClass(nestHost).index; + } + + @Override + public final void visitOuterClass( + final String owner, final String name, final String descriptor) { + enclosingClassIndex = symbolTable.addConstantClass(owner).index; + if (name != null && descriptor != null) { + enclosingMethodIndex = symbolTable.addConstantNameAndType(name, descriptor); + } + } + + @Override + public final AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) { + if (visible) { + return lastRuntimeVisibleAnnotation = + AnnotationWriter.create(symbolTable, descriptor, lastRuntimeVisibleAnnotation); + } else { + return lastRuntimeInvisibleAnnotation = + AnnotationWriter.create(symbolTable, descriptor, lastRuntimeInvisibleAnnotation); + } + } + + @Override + public final AnnotationVisitor visitTypeAnnotation( + final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) { + if (visible) { + return lastRuntimeVisibleTypeAnnotation = + AnnotationWriter.create( + symbolTable, typeRef, typePath, descriptor, lastRuntimeVisibleTypeAnnotation); + } else { + return lastRuntimeInvisibleTypeAnnotation = + AnnotationWriter.create( + symbolTable, typeRef, typePath, descriptor, lastRuntimeInvisibleTypeAnnotation); + } + } + + @Override + public final void visitAttribute(final Attribute attribute) { + // Store the attributes in the reverse order of their visit by this method. + attribute.nextAttribute = firstAttribute; + firstAttribute = attribute; + } + + @Override + public final void visitNestMember(final String nestMember) { + if (nestMemberClasses == null) { + nestMemberClasses = new ByteVector(); + } + ++numberOfNestMemberClasses; + nestMemberClasses.putShort(symbolTable.addConstantClass(nestMember).index); + } + + @Override + public final void visitPermittedSubclass(final String permittedSubclass) { + if (permittedSubclasses == null) { + permittedSubclasses = new ByteVector(); + } + ++numberOfPermittedSubclasses; + permittedSubclasses.putShort(symbolTable.addConstantClass(permittedSubclass).index); + } + + @Override + public final void visitInnerClass( + final String name, final String outerName, final String innerName, final int access) { + if (innerClasses == null) { + innerClasses = new ByteVector(); + } + // Section 4.7.6 of the JVMS states "Every CONSTANT_Class_info entry in the constant_pool table + // which represents a class or interface C that is not a package member must have exactly one + // corresponding entry in the classes array". To avoid duplicates we keep track in the info + // field of the Symbol of each CONSTANT_Class_info entry C whether an inner class entry has + // already been added for C. If so, we store the index of this inner class entry (plus one) in + // the info field. This trick allows duplicate detection in O(1) time. + Symbol nameSymbol = symbolTable.addConstantClass(name); + if (nameSymbol.info == 0) { + ++numberOfInnerClasses; + innerClasses.putShort(nameSymbol.index); + innerClasses.putShort(outerName == null ? 0 : symbolTable.addConstantClass(outerName).index); + innerClasses.putShort(innerName == null ? 0 : symbolTable.addConstantUtf8(innerName)); + innerClasses.putShort(access); + nameSymbol.info = numberOfInnerClasses; + } + // Else, compare the inner classes entry nameSymbol.info - 1 with the arguments of this method + // and throw an exception if there is a difference? + } + + @Override + public final RecordComponentVisitor visitRecordComponent( + final String name, final String descriptor, final String signature) { + RecordComponentWriter recordComponentWriter = + new RecordComponentWriter(symbolTable, name, descriptor, signature); + if (firstRecordComponent == null) { + firstRecordComponent = recordComponentWriter; + } else { + lastRecordComponent.delegate = recordComponentWriter; + } + return lastRecordComponent = recordComponentWriter; + } + + @Override + public final FieldVisitor visitField( + final int access, + final String name, + final String descriptor, + final String signature, + final Object value) { + FieldWriter fieldWriter = + new FieldWriter(symbolTable, access, name, descriptor, signature, value); + if (firstField == null) { + firstField = fieldWriter; + } else { + lastField.fv = fieldWriter; + } + return lastField = fieldWriter; + } + + @Override + public final MethodVisitor visitMethod( + final int access, + final String name, + final String descriptor, + final String signature, + final String[] exceptions) { + MethodWriter methodWriter = + new MethodWriter(symbolTable, access, name, descriptor, signature, exceptions, compute); + if (firstMethod == null) { + firstMethod = methodWriter; + } else { + lastMethod.mv = methodWriter; + } + return lastMethod = methodWriter; + } + + @Override + public final void visitEnd() { + // Nothing to do. + } + + // ----------------------------------------------------------------------------------------------- + // Other public methods + // ----------------------------------------------------------------------------------------------- + + /** + * Returns the content of the class file that was built by this ClassWriter. + * + * @return the binary content of the JVMS ClassFile structure that was built by this ClassWriter. + * @throws ClassTooLargeException if the constant pool of the class is too large. + * @throws MethodTooLargeException if the Code attribute of a method is too large. + */ + public byte[] toByteArray() { + // First step: compute the size in bytes of the ClassFile structure. + // The magic field uses 4 bytes, 10 mandatory fields (minor_version, major_version, + // constant_pool_count, access_flags, this_class, super_class, interfaces_count, fields_count, + // methods_count and attributes_count) use 2 bytes each, and each interface uses 2 bytes too. + int size = 24 + 2 * interfaceCount; + int fieldsCount = 0; + FieldWriter fieldWriter = firstField; + while (fieldWriter != null) { + ++fieldsCount; + size += fieldWriter.computeFieldInfoSize(); + fieldWriter = (FieldWriter) fieldWriter.fv; + } + int methodsCount = 0; + MethodWriter methodWriter = firstMethod; + while (methodWriter != null) { + ++methodsCount; + size += methodWriter.computeMethodInfoSize(); + methodWriter = (MethodWriter) methodWriter.mv; + } + + // For ease of reference, we use here the same attribute order as in Section 4.7 of the JVMS. + int attributesCount = 0; + if (innerClasses != null) { + ++attributesCount; + size += 8 + innerClasses.length; + symbolTable.addConstantUtf8(Constants.INNER_CLASSES); + } + if (enclosingClassIndex != 0) { + ++attributesCount; + size += 10; + symbolTable.addConstantUtf8(Constants.ENCLOSING_METHOD); + } + if ((accessFlags & Opcodes.ACC_SYNTHETIC) != 0 && (version & 0xFFFF) < Opcodes.V1_5) { + ++attributesCount; + size += 6; + symbolTable.addConstantUtf8(Constants.SYNTHETIC); + } + if (signatureIndex != 0) { + ++attributesCount; + size += 8; + symbolTable.addConstantUtf8(Constants.SIGNATURE); + } + if (sourceFileIndex != 0) { + ++attributesCount; + size += 8; + symbolTable.addConstantUtf8(Constants.SOURCE_FILE); + } + if (debugExtension != null) { + ++attributesCount; + size += 6 + debugExtension.length; + symbolTable.addConstantUtf8(Constants.SOURCE_DEBUG_EXTENSION); + } + if ((accessFlags & Opcodes.ACC_DEPRECATED) != 0) { + ++attributesCount; + size += 6; + symbolTable.addConstantUtf8(Constants.DEPRECATED); + } + if (lastRuntimeVisibleAnnotation != null) { + ++attributesCount; + size += + lastRuntimeVisibleAnnotation.computeAnnotationsSize( + Constants.RUNTIME_VISIBLE_ANNOTATIONS); + } + if (lastRuntimeInvisibleAnnotation != null) { + ++attributesCount; + size += + lastRuntimeInvisibleAnnotation.computeAnnotationsSize( + Constants.RUNTIME_INVISIBLE_ANNOTATIONS); + } + if (lastRuntimeVisibleTypeAnnotation != null) { + ++attributesCount; + size += + lastRuntimeVisibleTypeAnnotation.computeAnnotationsSize( + Constants.RUNTIME_VISIBLE_TYPE_ANNOTATIONS); + } + if (lastRuntimeInvisibleTypeAnnotation != null) { + ++attributesCount; + size += + lastRuntimeInvisibleTypeAnnotation.computeAnnotationsSize( + Constants.RUNTIME_INVISIBLE_TYPE_ANNOTATIONS); + } + if (symbolTable.computeBootstrapMethodsSize() > 0) { + ++attributesCount; + size += symbolTable.computeBootstrapMethodsSize(); + } + if (moduleWriter != null) { + attributesCount += moduleWriter.getAttributeCount(); + size += moduleWriter.computeAttributesSize(); + } + if (nestHostClassIndex != 0) { + ++attributesCount; + size += 8; + symbolTable.addConstantUtf8(Constants.NEST_HOST); + } + if (nestMemberClasses != null) { + ++attributesCount; + size += 8 + nestMemberClasses.length; + symbolTable.addConstantUtf8(Constants.NEST_MEMBERS); + } + if (permittedSubclasses != null) { + ++attributesCount; + size += 8 + permittedSubclasses.length; + symbolTable.addConstantUtf8(Constants.PERMITTED_SUBCLASSES); + } + int recordComponentCount = 0; + int recordSize = 0; + if ((accessFlags & Opcodes.ACC_RECORD) != 0 || firstRecordComponent != null) { + RecordComponentWriter recordComponentWriter = firstRecordComponent; + while (recordComponentWriter != null) { + ++recordComponentCount; + recordSize += recordComponentWriter.computeRecordComponentInfoSize(); + recordComponentWriter = (RecordComponentWriter) recordComponentWriter.delegate; + } + ++attributesCount; + size += 8 + recordSize; + symbolTable.addConstantUtf8(Constants.RECORD); + } + if (firstAttribute != null) { + attributesCount += firstAttribute.getAttributeCount(); + size += firstAttribute.computeAttributesSize(symbolTable); + } + // IMPORTANT: this must be the last part of the ClassFile size computation, because the previous + // statements can add attribute names to the constant pool, thereby changing its size! + size += symbolTable.getConstantPoolLength(); + int constantPoolCount = symbolTable.getConstantPoolCount(); + if (constantPoolCount > 0xFFFF) { + throw new ClassTooLargeException(symbolTable.getClassName(), constantPoolCount); + } + + // Second step: allocate a ByteVector of the correct size (in order to avoid any array copy in + // dynamic resizes) and fill it with the ClassFile content. + ByteVector result = new ByteVector(size); + result.putInt(0xCAFEBABE).putInt(version); + symbolTable.putConstantPool(result); + int mask = (version & 0xFFFF) < Opcodes.V1_5 ? Opcodes.ACC_SYNTHETIC : 0; + result.putShort(accessFlags & ~mask).putShort(thisClass).putShort(superClass); + result.putShort(interfaceCount); + for (int i = 0; i < interfaceCount; ++i) { + result.putShort(interfaces[i]); + } + result.putShort(fieldsCount); + fieldWriter = firstField; + while (fieldWriter != null) { + fieldWriter.putFieldInfo(result); + fieldWriter = (FieldWriter) fieldWriter.fv; + } + result.putShort(methodsCount); + boolean hasFrames = false; + boolean hasAsmInstructions = false; + methodWriter = firstMethod; + while (methodWriter != null) { + hasFrames |= methodWriter.hasFrames(); + hasAsmInstructions |= methodWriter.hasAsmInstructions(); + methodWriter.putMethodInfo(result); + methodWriter = (MethodWriter) methodWriter.mv; + } + // For ease of reference, we use here the same attribute order as in Section 4.7 of the JVMS. + result.putShort(attributesCount); + if (innerClasses != null) { + result + .putShort(symbolTable.addConstantUtf8(Constants.INNER_CLASSES)) + .putInt(innerClasses.length + 2) + .putShort(numberOfInnerClasses) + .putByteArray(innerClasses.data, 0, innerClasses.length); + } + if (enclosingClassIndex != 0) { + result + .putShort(symbolTable.addConstantUtf8(Constants.ENCLOSING_METHOD)) + .putInt(4) + .putShort(enclosingClassIndex) + .putShort(enclosingMethodIndex); + } + if ((accessFlags & Opcodes.ACC_SYNTHETIC) != 0 && (version & 0xFFFF) < Opcodes.V1_5) { + result.putShort(symbolTable.addConstantUtf8(Constants.SYNTHETIC)).putInt(0); + } + if (signatureIndex != 0) { + result + .putShort(symbolTable.addConstantUtf8(Constants.SIGNATURE)) + .putInt(2) + .putShort(signatureIndex); + } + if (sourceFileIndex != 0) { + result + .putShort(symbolTable.addConstantUtf8(Constants.SOURCE_FILE)) + .putInt(2) + .putShort(sourceFileIndex); + } + if (debugExtension != null) { + int length = debugExtension.length; + result + .putShort(symbolTable.addConstantUtf8(Constants.SOURCE_DEBUG_EXTENSION)) + .putInt(length) + .putByteArray(debugExtension.data, 0, length); + } + if ((accessFlags & Opcodes.ACC_DEPRECATED) != 0) { + result.putShort(symbolTable.addConstantUtf8(Constants.DEPRECATED)).putInt(0); + } + AnnotationWriter.putAnnotations( + symbolTable, + lastRuntimeVisibleAnnotation, + lastRuntimeInvisibleAnnotation, + lastRuntimeVisibleTypeAnnotation, + lastRuntimeInvisibleTypeAnnotation, + result); + symbolTable.putBootstrapMethods(result); + if (moduleWriter != null) { + moduleWriter.putAttributes(result); + } + if (nestHostClassIndex != 0) { + result + .putShort(symbolTable.addConstantUtf8(Constants.NEST_HOST)) + .putInt(2) + .putShort(nestHostClassIndex); + } + if (nestMemberClasses != null) { + result + .putShort(symbolTable.addConstantUtf8(Constants.NEST_MEMBERS)) + .putInt(nestMemberClasses.length + 2) + .putShort(numberOfNestMemberClasses) + .putByteArray(nestMemberClasses.data, 0, nestMemberClasses.length); + } + if (permittedSubclasses != null) { + result + .putShort(symbolTable.addConstantUtf8(Constants.PERMITTED_SUBCLASSES)) + .putInt(permittedSubclasses.length + 2) + .putShort(numberOfPermittedSubclasses) + .putByteArray(permittedSubclasses.data, 0, permittedSubclasses.length); + } + if ((accessFlags & Opcodes.ACC_RECORD) != 0 || firstRecordComponent != null) { + result + .putShort(symbolTable.addConstantUtf8(Constants.RECORD)) + .putInt(recordSize + 2) + .putShort(recordComponentCount); + RecordComponentWriter recordComponentWriter = firstRecordComponent; + while (recordComponentWriter != null) { + recordComponentWriter.putRecordComponentInfo(result); + recordComponentWriter = (RecordComponentWriter) recordComponentWriter.delegate; + } + } + if (firstAttribute != null) { + firstAttribute.putAttributes(symbolTable, result); + } + + // Third step: replace the ASM specific instructions, if any. + if (hasAsmInstructions) { + return replaceAsmInstructions(result.data, hasFrames); + } else { + return result.data; + } + } + + /** + * Returns the equivalent of the given class file, with the ASM specific instructions replaced + * with standard ones. This is done with a ClassReader -> ClassWriter round trip. + * + * @param classFile a class file containing ASM specific instructions, generated by this + * ClassWriter. + * @param hasFrames whether there is at least one stack map frames in 'classFile'. + * @return an equivalent of 'classFile', with the ASM specific instructions replaced with standard + * ones. + */ + private byte[] replaceAsmInstructions(final byte[] classFile, final boolean hasFrames) { + final Attribute[] attributes = getAttributePrototypes(); + firstField = null; + lastField = null; + firstMethod = null; + lastMethod = null; + lastRuntimeVisibleAnnotation = null; + lastRuntimeInvisibleAnnotation = null; + lastRuntimeVisibleTypeAnnotation = null; + lastRuntimeInvisibleTypeAnnotation = null; + moduleWriter = null; + nestHostClassIndex = 0; + numberOfNestMemberClasses = 0; + nestMemberClasses = null; + numberOfPermittedSubclasses = 0; + permittedSubclasses = null; + firstRecordComponent = null; + lastRecordComponent = null; + firstAttribute = null; + compute = hasFrames ? MethodWriter.COMPUTE_INSERTED_FRAMES : MethodWriter.COMPUTE_NOTHING; + new ClassReader(classFile, 0, /* checkClassVersion = */ false) + .accept( + this, + attributes, + (hasFrames ? ClassReader.EXPAND_FRAMES : 0) | ClassReader.EXPAND_ASM_INSNS); + return toByteArray(); + } + + /** + * Returns the prototypes of the attributes used by this class, its fields and its methods. + * + * @return the prototypes of the attributes used by this class, its fields and its methods. + */ + private Attribute[] getAttributePrototypes() { + Attribute.Set attributePrototypes = new Attribute.Set(); + attributePrototypes.addAttributes(firstAttribute); + FieldWriter fieldWriter = firstField; + while (fieldWriter != null) { + fieldWriter.collectAttributePrototypes(attributePrototypes); + fieldWriter = (FieldWriter) fieldWriter.fv; + } + MethodWriter methodWriter = firstMethod; + while (methodWriter != null) { + methodWriter.collectAttributePrototypes(attributePrototypes); + methodWriter = (MethodWriter) methodWriter.mv; + } + RecordComponentWriter recordComponentWriter = firstRecordComponent; + while (recordComponentWriter != null) { + recordComponentWriter.collectAttributePrototypes(attributePrototypes); + recordComponentWriter = (RecordComponentWriter) recordComponentWriter.delegate; + } + return attributePrototypes.toArray(); + } + + // ----------------------------------------------------------------------------------------------- + // Utility methods: constant pool management for Attribute sub classes + // ----------------------------------------------------------------------------------------------- + + /** + * Adds a number or string constant to the constant pool of the class being build. Does nothing if + * the constant pool already contains a similar item. This method is intended for {@link + * Attribute} sub classes, and is normally not needed by class generators or adapters. + * + * @param value the value of the constant to be added to the constant pool. This parameter must be + * an {@link Integer}, a {@link Float}, a {@link Long}, a {@link Double} or a {@link String}. + * @return the index of a new or already existing constant item with the given value. + */ + public int newConst(final Object value) { + return symbolTable.addConstant(value).index; + } + + /** + * Adds an UTF8 string to the constant pool of the class being build. Does nothing if the constant + * pool already contains a similar item. This method is intended for {@link Attribute} sub + * classes, and is normally not needed by class generators or adapters. + * + * @param value the String value. + * @return the index of a new or already existing UTF8 item. + */ + // DontCheck(AbbreviationAsWordInName): can't be renamed (for backward binary compatibility). + public int newUTF8(final String value) { + return symbolTable.addConstantUtf8(value); + } + + /** + * Adds a class reference to the constant pool of the class being build. Does nothing if the + * constant pool already contains a similar item. This method is intended for {@link Attribute} + * sub classes, and is normally not needed by class generators or adapters. + * + * @param value the internal name of the class. + * @return the index of a new or already existing class reference item. + */ + public int newClass(final String value) { + return symbolTable.addConstantClass(value).index; + } + + /** + * Adds a method type reference to the constant pool of the class being build. Does nothing if the + * constant pool already contains a similar item. This method is intended for {@link Attribute} + * sub classes, and is normally not needed by class generators or adapters. + * + * @param methodDescriptor method descriptor of the method type. + * @return the index of a new or already existing method type reference item. + */ + public int newMethodType(final String methodDescriptor) { + return symbolTable.addConstantMethodType(methodDescriptor).index; + } + + /** + * Adds a module reference to the constant pool of the class being build. Does nothing if the + * constant pool already contains a similar item. This method is intended for {@link Attribute} + * sub classes, and is normally not needed by class generators or adapters. + * + * @param moduleName name of the module. + * @return the index of a new or already existing module reference item. + */ + public int newModule(final String moduleName) { + return symbolTable.addConstantModule(moduleName).index; + } + + /** + * Adds a package reference to the constant pool of the class being build. Does nothing if the + * constant pool already contains a similar item. This method is intended for {@link Attribute} + * sub classes, and is normally not needed by class generators or adapters. + * + * @param packageName name of the package in its internal form. + * @return the index of a new or already existing module reference item. + */ + public int newPackage(final String packageName) { + return symbolTable.addConstantPackage(packageName).index; + } + + /** + * Adds a handle to the constant pool of the class being build. Does nothing if the constant pool + * already contains a similar item. This method is intended for {@link Attribute} sub classes, + * and is normally not needed by class generators or adapters. + * + * @param tag the kind of this handle. Must be {@link Opcodes#H_GETFIELD}, {@link + * Opcodes#H_GETSTATIC}, {@link Opcodes#H_PUTFIELD}, {@link Opcodes#H_PUTSTATIC}, {@link + * Opcodes#H_INVOKEVIRTUAL}, {@link Opcodes#H_INVOKESTATIC}, {@link Opcodes#H_INVOKESPECIAL}, + * {@link Opcodes#H_NEWINVOKESPECIAL} or {@link Opcodes#H_INVOKEINTERFACE}. + * @param owner the internal name of the field or method owner class. + * @param name the name of the field or method. + * @param descriptor the descriptor of the field or method. + * @return the index of a new or already existing method type reference item. + * @deprecated this method is superseded by {@link #newHandle(int, String, String, String, + * boolean)}. + */ + @Deprecated + public int newHandle( + final int tag, final String owner, final String name, final String descriptor) { + return newHandle(tag, owner, name, descriptor, tag == Opcodes.H_INVOKEINTERFACE); + } + + /** + * Adds a handle to the constant pool of the class being build. Does nothing if the constant pool + * already contains a similar item. This method is intended for {@link Attribute} sub classes, + * and is normally not needed by class generators or adapters. + * + * @param tag the kind of this handle. Must be {@link Opcodes#H_GETFIELD}, {@link + * Opcodes#H_GETSTATIC}, {@link Opcodes#H_PUTFIELD}, {@link Opcodes#H_PUTSTATIC}, {@link + * Opcodes#H_INVOKEVIRTUAL}, {@link Opcodes#H_INVOKESTATIC}, {@link Opcodes#H_INVOKESPECIAL}, + * {@link Opcodes#H_NEWINVOKESPECIAL} or {@link Opcodes#H_INVOKEINTERFACE}. + * @param owner the internal name of the field or method owner class. + * @param name the name of the field or method. + * @param descriptor the descriptor of the field or method. + * @param isInterface true if the owner is an interface. + * @return the index of a new or already existing method type reference item. + */ + public int newHandle( + final int tag, + final String owner, + final String name, + final String descriptor, + final boolean isInterface) { + return symbolTable.addConstantMethodHandle(tag, owner, name, descriptor, isInterface).index; + } + + /** + * Adds a dynamic constant reference to the constant pool of the class being build. Does nothing + * if the constant pool already contains a similar item. This method is intended for {@link + * Attribute} sub classes, and is normally not needed by class generators or adapters. + * + * @param name name of the invoked method. + * @param descriptor field descriptor of the constant type. + * @param bootstrapMethodHandle the bootstrap method. + * @param bootstrapMethodArguments the bootstrap method constant arguments. + * @return the index of a new or already existing dynamic constant reference item. + */ + public int newConstantDynamic( + final String name, + final String descriptor, + final Handle bootstrapMethodHandle, + final Object... bootstrapMethodArguments) { + return symbolTable.addConstantDynamic( + name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments) + .index; + } + + /** + * Adds an invokedynamic reference to the constant pool of the class being build. Does nothing if + * the constant pool already contains a similar item. This method is intended for {@link + * Attribute} sub classes, and is normally not needed by class generators or adapters. + * + * @param name name of the invoked method. + * @param descriptor descriptor of the invoke method. + * @param bootstrapMethodHandle the bootstrap method. + * @param bootstrapMethodArguments the bootstrap method constant arguments. + * @return the index of a new or already existing invokedynamic reference item. + */ + public int newInvokeDynamic( + final String name, + final String descriptor, + final Handle bootstrapMethodHandle, + final Object... bootstrapMethodArguments) { + return symbolTable.addConstantInvokeDynamic( + name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments) + .index; + } + + /** + * Adds a field reference to the constant pool of the class being build. Does nothing if the + * constant pool already contains a similar item. This method is intended for {@link Attribute} + * sub classes, and is normally not needed by class generators or adapters. + * + * @param owner the internal name of the field's owner class. + * @param name the field's name. + * @param descriptor the field's descriptor. + * @return the index of a new or already existing field reference item. + */ + public int newField(final String owner, final String name, final String descriptor) { + return symbolTable.addConstantFieldref(owner, name, descriptor).index; + } + + /** + * Adds a method reference to the constant pool of the class being build. Does nothing if the + * constant pool already contains a similar item. This method is intended for {@link Attribute} + * sub classes, and is normally not needed by class generators or adapters. + * + * @param owner the internal name of the method's owner class. + * @param name the method's name. + * @param descriptor the method's descriptor. + * @param isInterface {@literal true} if {@code owner} is an interface. + * @return the index of a new or already existing method reference item. + */ + public int newMethod( + final String owner, final String name, final String descriptor, final boolean isInterface) { + return symbolTable.addConstantMethodref(owner, name, descriptor, isInterface).index; + } + + /** + * Adds a name and type to the constant pool of the class being build. Does nothing if the + * constant pool already contains a similar item. This method is intended for {@link Attribute} + * sub classes, and is normally not needed by class generators or adapters. + * + * @param name a name. + * @param descriptor a type descriptor. + * @return the index of a new or already existing name and type item. + */ + public int newNameType(final String name, final String descriptor) { + return symbolTable.addConstantNameAndType(name, descriptor); + } + + // ----------------------------------------------------------------------------------------------- + // Default method to compute common super classes when computing stack map frames + // ----------------------------------------------------------------------------------------------- + + /** + * Returns the common super type of the two given types. The default implementation of this method + * loads the two given classes and uses the java.lang.Class methods to find the common + * super class. It can be overridden to compute this common super type in other ways, in + * particular without actually loading any class, or to take into account the class that is + * currently being generated by this ClassWriter, which can of course not be loaded since it is + * under construction. + * + * @param type1 the internal name of a class. + * @param type2 the internal name of another class. + * @return the internal name of the common super class of the two given classes. + */ + protected String getCommonSuperClass(final String type1, final String type2) { + ClassLoader classLoader = getClassLoader(); + Class class1; + try { + class1 = Class.forName(type1.replace('/', '.'), false, classLoader); + } catch (ClassNotFoundException e) { + throw new TypeNotPresentException(type1, e); + } + Class class2; + try { + class2 = Class.forName(type2.replace('/', '.'), false, classLoader); + } catch (ClassNotFoundException e) { + throw new TypeNotPresentException(type2, e); + } + if (class1.isAssignableFrom(class2)) { + return type1; + } + if (class2.isAssignableFrom(class1)) { + return type2; + } + if (class1.isInterface() || class2.isInterface()) { + return "java/lang/Object"; + } else { + do { + class1 = class1.getSuperclass(); + } while (!class1.isAssignableFrom(class2)); + return class1.getName().replace('.', '/'); + } + } + + /** + * Returns the {@link ClassLoader} to be used by the default implementation of {@link + * #getCommonSuperClass(String, String)}, that of this {@link ClassWriter}'s runtime type by + * default. + * + * @return ClassLoader + */ + protected ClassLoader getClassLoader() { + // SPRING PATCH: prefer thread context ClassLoader for application classes + ClassLoader classLoader = null; + try { + classLoader = Thread.currentThread().getContextClassLoader(); + } catch (Throwable ex) { + // Cannot access thread context ClassLoader - falling back... + } + return (classLoader != null ? classLoader : getClass().getClassLoader()); + } +} diff --git a/spring-core/src/main/java/org/springframework/asm/ConstantDynamic.java b/spring-core/src/main/java/org/springframework/asm/ConstantDynamic.java new file mode 100644 index 0000000..9174dba --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/ConstantDynamic.java @@ -0,0 +1,178 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +package org.springframework.asm; + +import java.util.Arrays; + +/** + * A constant whose value is computed at runtime, with a bootstrap method. + * + * @author Remi Forax + */ +public final class ConstantDynamic { + + /** The constant name (can be arbitrary). */ + private final String name; + + /** The constant type (must be a field descriptor). */ + private final String descriptor; + + /** The bootstrap method to use to compute the constant value at runtime. */ + private final Handle bootstrapMethod; + + /** + * The arguments to pass to the bootstrap method, in order to compute the constant value at + * runtime. + */ + private final Object[] bootstrapMethodArguments; + + /** + * Constructs a new {@link ConstantDynamic}. + * + * @param name the constant name (can be arbitrary). + * @param descriptor the constant type (must be a field descriptor). + * @param bootstrapMethod the bootstrap method to use to compute the constant value at runtime. + * @param bootstrapMethodArguments the arguments to pass to the bootstrap method, in order to + * compute the constant value at runtime. + */ + public ConstantDynamic( + final String name, + final String descriptor, + final Handle bootstrapMethod, + final Object... bootstrapMethodArguments) { + this.name = name; + this.descriptor = descriptor; + this.bootstrapMethod = bootstrapMethod; + this.bootstrapMethodArguments = bootstrapMethodArguments; + } + + /** + * Returns the name of this constant. + * + * @return the name of this constant. + */ + public String getName() { + return name; + } + + /** + * Returns the type of this constant. + * + * @return the type of this constant, as a field descriptor. + */ + public String getDescriptor() { + return descriptor; + } + + /** + * Returns the bootstrap method used to compute the value of this constant. + * + * @return the bootstrap method used to compute the value of this constant. + */ + public Handle getBootstrapMethod() { + return bootstrapMethod; + } + + /** + * Returns the number of arguments passed to the bootstrap method, in order to compute the value + * of this constant. + * + * @return the number of arguments passed to the bootstrap method, in order to compute the value + * of this constant. + */ + public int getBootstrapMethodArgumentCount() { + return bootstrapMethodArguments.length; + } + + /** + * Returns an argument passed to the bootstrap method, in order to compute the value of this + * constant. + * + * @param index an argument index, between 0 and {@link #getBootstrapMethodArgumentCount()} + * (exclusive). + * @return the argument passed to the bootstrap method, with the given index. + */ + public Object getBootstrapMethodArgument(final int index) { + return bootstrapMethodArguments[index]; + } + + /** + * Returns the arguments to pass to the bootstrap method, in order to compute the value of this + * constant. WARNING: this array must not be modified, and must not be returned to the user. + * + * @return the arguments to pass to the bootstrap method, in order to compute the value of this + * constant. + */ + Object[] getBootstrapMethodArgumentsUnsafe() { + return bootstrapMethodArguments; + } + + /** + * Returns the size of this constant. + * + * @return the size of this constant, i.e., 2 for {@code long} and {@code double}, 1 otherwise. + */ + public int getSize() { + char firstCharOfDescriptor = descriptor.charAt(0); + return (firstCharOfDescriptor == 'J' || firstCharOfDescriptor == 'D') ? 2 : 1; + } + + @Override + public boolean equals(final Object object) { + if (object == this) { + return true; + } + if (!(object instanceof ConstantDynamic)) { + return false; + } + ConstantDynamic constantDynamic = (ConstantDynamic) object; + return name.equals(constantDynamic.name) + && descriptor.equals(constantDynamic.descriptor) + && bootstrapMethod.equals(constantDynamic.bootstrapMethod) + && Arrays.equals(bootstrapMethodArguments, constantDynamic.bootstrapMethodArguments); + } + + @Override + public int hashCode() { + return name.hashCode() + ^ Integer.rotateLeft(descriptor.hashCode(), 8) + ^ Integer.rotateLeft(bootstrapMethod.hashCode(), 16) + ^ Integer.rotateLeft(Arrays.hashCode(bootstrapMethodArguments), 24); + } + + @Override + public String toString() { + return name + + " : " + + descriptor + + ' ' + + bootstrapMethod + + ' ' + + Arrays.toString(bootstrapMethodArguments); + } +} diff --git a/spring-core/src/main/java/org/springframework/asm/Constants.java b/spring-core/src/main/java/org/springframework/asm/Constants.java new file mode 100644 index 0000000..a0e250d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/Constants.java @@ -0,0 +1,178 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +package org.springframework.asm; + +/** + * Defines additional JVM opcodes, access flags and constants which are not part of the ASM public + * API. + * + * @see JVMS 6 + * @author Eric Bruneton + */ +final class Constants { + + // The ClassFile attribute names, in the order they are defined in + // https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html#jvms-4.7-300. + + static final String CONSTANT_VALUE = "ConstantValue"; + static final String CODE = "Code"; + static final String STACK_MAP_TABLE = "StackMapTable"; + static final String EXCEPTIONS = "Exceptions"; + static final String INNER_CLASSES = "InnerClasses"; + static final String ENCLOSING_METHOD = "EnclosingMethod"; + static final String SYNTHETIC = "Synthetic"; + static final String SIGNATURE = "Signature"; + static final String SOURCE_FILE = "SourceFile"; + static final String SOURCE_DEBUG_EXTENSION = "SourceDebugExtension"; + static final String LINE_NUMBER_TABLE = "LineNumberTable"; + static final String LOCAL_VARIABLE_TABLE = "LocalVariableTable"; + static final String LOCAL_VARIABLE_TYPE_TABLE = "LocalVariableTypeTable"; + static final String DEPRECATED = "Deprecated"; + static final String RUNTIME_VISIBLE_ANNOTATIONS = "RuntimeVisibleAnnotations"; + static final String RUNTIME_INVISIBLE_ANNOTATIONS = "RuntimeInvisibleAnnotations"; + static final String RUNTIME_VISIBLE_PARAMETER_ANNOTATIONS = "RuntimeVisibleParameterAnnotations"; + static final String RUNTIME_INVISIBLE_PARAMETER_ANNOTATIONS = "RuntimeInvisibleParameterAnnotations"; + static final String RUNTIME_VISIBLE_TYPE_ANNOTATIONS = "RuntimeVisibleTypeAnnotations"; + static final String RUNTIME_INVISIBLE_TYPE_ANNOTATIONS = "RuntimeInvisibleTypeAnnotations"; + static final String ANNOTATION_DEFAULT = "AnnotationDefault"; + static final String BOOTSTRAP_METHODS = "BootstrapMethods"; + static final String METHOD_PARAMETERS = "MethodParameters"; + static final String MODULE = "Module"; + static final String MODULE_PACKAGES = "ModulePackages"; + static final String MODULE_MAIN_CLASS = "ModuleMainClass"; + static final String NEST_HOST = "NestHost"; + static final String NEST_MEMBERS = "NestMembers"; + static final String PERMITTED_SUBCLASSES = "PermittedSubclasses"; + static final String RECORD = "Record"; + + // ASM specific access flags. + // WARNING: the 16 least significant bits must NOT be used, to avoid conflicts with standard + // access flags, and also to make sure that these flags are automatically filtered out when + // written in class files (because access flags are stored using 16 bits only). + + static final int ACC_CONSTRUCTOR = 0x40000; // method access flag. + + // ASM specific stack map frame types, used in {@link ClassVisitor#visitFrame}. + + /** + * A frame inserted between already existing frames. This internal stack map frame type (in + * addition to the ones declared in {@link Opcodes}) can only be used if the frame content can be + * computed from the previous existing frame and from the instructions between this existing frame + * and the inserted one, without any knowledge of the type hierarchy. This kind of frame is only + * used when an unconditional jump is inserted in a method while expanding an ASM specific + * instruction. Keep in sync with Opcodes.java. + */ + static final int F_INSERT = 256; + + // The JVM opcode values which are not part of the ASM public API. + // See https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-6.html. + + static final int LDC_W = 19; + static final int LDC2_W = 20; + static final int ILOAD_0 = 26; + static final int ILOAD_1 = 27; + static final int ILOAD_2 = 28; + static final int ILOAD_3 = 29; + static final int LLOAD_0 = 30; + static final int LLOAD_1 = 31; + static final int LLOAD_2 = 32; + static final int LLOAD_3 = 33; + static final int FLOAD_0 = 34; + static final int FLOAD_1 = 35; + static final int FLOAD_2 = 36; + static final int FLOAD_3 = 37; + static final int DLOAD_0 = 38; + static final int DLOAD_1 = 39; + static final int DLOAD_2 = 40; + static final int DLOAD_3 = 41; + static final int ALOAD_0 = 42; + static final int ALOAD_1 = 43; + static final int ALOAD_2 = 44; + static final int ALOAD_3 = 45; + static final int ISTORE_0 = 59; + static final int ISTORE_1 = 60; + static final int ISTORE_2 = 61; + static final int ISTORE_3 = 62; + static final int LSTORE_0 = 63; + static final int LSTORE_1 = 64; + static final int LSTORE_2 = 65; + static final int LSTORE_3 = 66; + static final int FSTORE_0 = 67; + static final int FSTORE_1 = 68; + static final int FSTORE_2 = 69; + static final int FSTORE_3 = 70; + static final int DSTORE_0 = 71; + static final int DSTORE_1 = 72; + static final int DSTORE_2 = 73; + static final int DSTORE_3 = 74; + static final int ASTORE_0 = 75; + static final int ASTORE_1 = 76; + static final int ASTORE_2 = 77; + static final int ASTORE_3 = 78; + static final int WIDE = 196; + static final int GOTO_W = 200; + static final int JSR_W = 201; + + // Constants to convert between normal and wide jump instructions. + + // The delta between the GOTO_W and JSR_W opcodes and GOTO and JUMP. + static final int WIDE_JUMP_OPCODE_DELTA = GOTO_W - Opcodes.GOTO; + + // Constants to convert JVM opcodes to the equivalent ASM specific opcodes, and vice versa. + + // The delta between the ASM_IFEQ, ..., ASM_IF_ACMPNE, ASM_GOTO and ASM_JSR opcodes + // and IFEQ, ..., IF_ACMPNE, GOTO and JSR. + static final int ASM_OPCODE_DELTA = 49; + + // The delta between the ASM_IFNULL and ASM_IFNONNULL opcodes and IFNULL and IFNONNULL. + static final int ASM_IFNULL_OPCODE_DELTA = 20; + + // ASM specific opcodes, used for long forward jump instructions. + + static final int ASM_IFEQ = Opcodes.IFEQ + ASM_OPCODE_DELTA; + static final int ASM_IFNE = Opcodes.IFNE + ASM_OPCODE_DELTA; + static final int ASM_IFLT = Opcodes.IFLT + ASM_OPCODE_DELTA; + static final int ASM_IFGE = Opcodes.IFGE + ASM_OPCODE_DELTA; + static final int ASM_IFGT = Opcodes.IFGT + ASM_OPCODE_DELTA; + static final int ASM_IFLE = Opcodes.IFLE + ASM_OPCODE_DELTA; + static final int ASM_IF_ICMPEQ = Opcodes.IF_ICMPEQ + ASM_OPCODE_DELTA; + static final int ASM_IF_ICMPNE = Opcodes.IF_ICMPNE + ASM_OPCODE_DELTA; + static final int ASM_IF_ICMPLT = Opcodes.IF_ICMPLT + ASM_OPCODE_DELTA; + static final int ASM_IF_ICMPGE = Opcodes.IF_ICMPGE + ASM_OPCODE_DELTA; + static final int ASM_IF_ICMPGT = Opcodes.IF_ICMPGT + ASM_OPCODE_DELTA; + static final int ASM_IF_ICMPLE = Opcodes.IF_ICMPLE + ASM_OPCODE_DELTA; + static final int ASM_IF_ACMPEQ = Opcodes.IF_ACMPEQ + ASM_OPCODE_DELTA; + static final int ASM_IF_ACMPNE = Opcodes.IF_ACMPNE + ASM_OPCODE_DELTA; + static final int ASM_GOTO = Opcodes.GOTO + ASM_OPCODE_DELTA; + static final int ASM_JSR = Opcodes.JSR + ASM_OPCODE_DELTA; + static final int ASM_IFNULL = Opcodes.IFNULL + ASM_IFNULL_OPCODE_DELTA; + static final int ASM_IFNONNULL = Opcodes.IFNONNULL + ASM_IFNULL_OPCODE_DELTA; + static final int ASM_GOTO_W = 220; + + private Constants() {} +} diff --git a/spring-core/src/main/java/org/springframework/asm/Context.java b/spring-core/src/main/java/org/springframework/asm/Context.java new file mode 100644 index 0000000..9915471 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/Context.java @@ -0,0 +1,137 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. + +package org.springframework.asm; + +/** + * Information about a class being parsed in a {@link ClassReader}. + * + * @author Eric Bruneton + */ +final class Context { + + /** The prototypes of the attributes that must be parsed in this class. */ + Attribute[] attributePrototypes; + + /** + * The options used to parse this class. One or more of {@link ClassReader#SKIP_CODE}, {@link + * ClassReader#SKIP_DEBUG}, {@link ClassReader#SKIP_FRAMES}, {@link ClassReader#EXPAND_FRAMES} or + * {@link ClassReader#EXPAND_ASM_INSNS}. + */ + int parsingOptions; + + /** The buffer used to read strings in the constant pool. */ + char[] charBuffer; + + // Information about the current method, i.e. the one read in the current (or latest) call + // to {@link ClassReader#readMethod()}. + + /** The access flags of the current method. */ + int currentMethodAccessFlags; + + /** The name of the current method. */ + String currentMethodName; + + /** The descriptor of the current method. */ + String currentMethodDescriptor; + + /** + * The labels of the current method, indexed by bytecode offset (only bytecode offsets for which a + * label is needed have a non null associated Label). + */ + Label[] currentMethodLabels; + + // Information about the current type annotation target, i.e. the one read in the current + // (or latest) call to {@link ClassReader#readAnnotationTarget()}. + + /** + * The target_type and target_info of the current type annotation target, encoded as described in + * {@link TypeReference}. + */ + int currentTypeAnnotationTarget; + + /** The target_path of the current type annotation target. */ + TypePath currentTypeAnnotationTargetPath; + + /** The start of each local variable range in the current local variable annotation. */ + Label[] currentLocalVariableAnnotationRangeStarts; + + /** The end of each local variable range in the current local variable annotation. */ + Label[] currentLocalVariableAnnotationRangeEnds; + + /** + * The local variable index of each local variable range in the current local variable annotation. + */ + int[] currentLocalVariableAnnotationRangeIndices; + + // Information about the current stack map frame, i.e. the one read in the current (or latest) + // call to {@link ClassReader#readFrame()}. + + /** The bytecode offset of the current stack map frame. */ + int currentFrameOffset; + + /** + * The type of the current stack map frame. One of {@link Opcodes#F_FULL}, {@link + * Opcodes#F_APPEND}, {@link Opcodes#F_CHOP}, {@link Opcodes#F_SAME} or {@link Opcodes#F_SAME1}. + */ + int currentFrameType; + + /** + * The number of local variable types in the current stack map frame. Each type is represented + * with a single array element (even long and double). + */ + int currentFrameLocalCount; + + /** + * The delta number of local variable types in the current stack map frame (each type is + * represented with a single array element - even long and double). This is the number of local + * variable types in this frame, minus the number of local variable types in the previous frame. + */ + int currentFrameLocalCountDelta; + + /** + * The types of the local variables in the current stack map frame. Each type is represented with + * a single array element (even long and double), using the format described in {@link + * MethodVisitor#visitFrame}. Depending on {@link #currentFrameType}, this contains the types of + * all the local variables, or only those of the additional ones (compared to the previous frame). + */ + Object[] currentFrameLocalTypes; + + /** + * The number stack element types in the current stack map frame. Each type is represented with a + * single array element (even long and double). + */ + int currentFrameStackCount; + + /** + * The types of the stack elements in the current stack map frame. Each type is represented with a + * single array element (even long and double), using the format described in {@link + * MethodVisitor#visitFrame}. + */ + Object[] currentFrameStackTypes; +} diff --git a/spring-core/src/main/java/org/springframework/asm/CurrentFrame.java b/spring-core/src/main/java/org/springframework/asm/CurrentFrame.java new file mode 100644 index 0000000..4610ad2 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/CurrentFrame.java @@ -0,0 +1,56 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. + +package org.springframework.asm; + +/** + * Information about the input stack map frame at the "current" instruction of a method. This is + * implemented as a Frame subclass for a "basic block" containing only one instruction. + * + * @author Eric Bruneton + */ +final class CurrentFrame extends Frame { + + CurrentFrame(final Label owner) { + super(owner); + } + + /** + * Sets this CurrentFrame to the input stack map frame of the next "current" instruction, i.e. the + * instruction just after the given one. It is assumed that the value of this object when this + * method is called is the stack map frame status just before the given instruction is executed. + */ + @Override + void execute( + final int opcode, final int arg, final Symbol symbolArg, final SymbolTable symbolTable) { + super.execute(opcode, arg, symbolArg, symbolTable); + Frame successor = new Frame(null); + merge(symbolTable, successor, 0); + copyFrom(successor); + } +} diff --git a/spring-core/src/main/java/org/springframework/asm/Edge.java b/spring-core/src/main/java/org/springframework/asm/Edge.java new file mode 100644 index 0000000..f9e5316 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/Edge.java @@ -0,0 +1,91 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +package org.springframework.asm; + +/** + * An edge in the control flow graph of a method. Each node of this graph is a basic block, + * represented with the Label corresponding to its first instruction. Each edge goes from one node + * to another, i.e. from one basic block to another (called the predecessor and successor blocks, + * respectively). An edge corresponds either to a jump or ret instruction or to an exception + * handler. + * + * @see Label + * @author Eric Bruneton + */ +final class Edge { + + /** + * A control flow graph edge corresponding to a jump or ret instruction. Only used with {@link + * ClassWriter#COMPUTE_FRAMES}. + */ + static final int JUMP = 0; + + /** + * A control flow graph edge corresponding to an exception handler. Only used with {@link + * ClassWriter#COMPUTE_MAXS}. + */ + static final int EXCEPTION = 0x7FFFFFFF; + + /** + * Information about this control flow graph edge. + * + *
      + *
    • If {@link ClassWriter#COMPUTE_MAXS} is used, this field contains either a stack size + * delta (for an edge corresponding to a jump instruction), or the value EXCEPTION (for an + * edge corresponding to an exception handler). The stack size delta is the stack size just + * after the jump instruction, minus the stack size at the beginning of the predecessor + * basic block, i.e. the one containing the jump instruction. + *
    • If {@link ClassWriter#COMPUTE_FRAMES} is used, this field contains either the value JUMP + * (for an edge corresponding to a jump instruction), or the index, in the {@link + * ClassWriter} type table, of the exception type that is handled (for an edge corresponding + * to an exception handler). + *
    + */ + final int info; + + /** The successor block of this control flow graph edge. */ + final Label successor; + + /** + * The next edge in the list of outgoing edges of a basic block. See {@link Label#outgoingEdges}. + */ + Edge nextEdge; + + /** + * Constructs a new Edge. + * + * @param info see {@link #info}. + * @param successor see {@link #successor}. + * @param nextEdge see {@link #nextEdge}. + */ + Edge(final int info, final Label successor, final Edge nextEdge) { + this.info = info; + this.successor = successor; + this.nextEdge = nextEdge; + } +} diff --git a/spring-core/src/main/java/org/springframework/asm/FieldVisitor.java b/spring-core/src/main/java/org/springframework/asm/FieldVisitor.java new file mode 100644 index 0000000..c828e60 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/FieldVisitor.java @@ -0,0 +1,143 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +package org.springframework.asm; + +/** + * A visitor to visit a Java field. The methods of this class must be called in the following order: + * ( {@code visitAnnotation} | {@code visitTypeAnnotation} | {@code visitAttribute} )* {@code + * visitEnd}. + * + * @author Eric Bruneton + */ +public abstract class FieldVisitor { + + /** + * The ASM API version implemented by this visitor. The value of this field must be one of {@link + * Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6}, {@link Opcodes#ASM7}, {@link + * Opcodes#ASM8} or {@link Opcodes#ASM9}. + */ + protected final int api; + + /** The field visitor to which this visitor must delegate method calls. May be {@literal null}. */ + protected FieldVisitor fv; + + /** + * Constructs a new {@link FieldVisitor}. + * + * @param api the ASM API version implemented by this visitor. Must be one of {@link + * Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6}, {@link Opcodes#ASM7}, {@link + * Opcodes#ASM8} or {@link Opcodes#ASM9}. + */ + public FieldVisitor(final int api) { + this(api, null); + } + + /** + * Constructs a new {@link FieldVisitor}. + * + * @param api the ASM API version implemented by this visitor. Must be one of {@link + * Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6}, {@link Opcodes#ASM7} or {@link + * Opcodes#ASM8}. + * @param fieldVisitor the field visitor to which this visitor must delegate method calls. May be + * null. + */ + public FieldVisitor(final int api, final FieldVisitor fieldVisitor) { + if (api != Opcodes.ASM9 + && api != Opcodes.ASM8 + && api != Opcodes.ASM7 + && api != Opcodes.ASM6 + && api != Opcodes.ASM5 + && api != Opcodes.ASM4 + && api != Opcodes.ASM10_EXPERIMENTAL) { + throw new IllegalArgumentException("Unsupported api " + api); + } + // SPRING PATCH: no preview mode check for ASM 9 experimental + this.api = api; + this.fv = fieldVisitor; + } + + /** + * Visits an annotation of the field. + * + * @param descriptor the class descriptor of the annotation class. + * @param visible {@literal true} if the annotation is visible at runtime. + * @return a visitor to visit the annotation values, or {@literal null} if this visitor is not + * interested in visiting this annotation. + */ + public AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) { + if (fv != null) { + return fv.visitAnnotation(descriptor, visible); + } + return null; + } + + /** + * Visits an annotation on the type of the field. + * + * @param typeRef a reference to the annotated type. The sort of this type reference must be + * {@link TypeReference#FIELD}. See {@link TypeReference}. + * @param typePath the path to the annotated type argument, wildcard bound, array element type, or + * static inner type within 'typeRef'. May be {@literal null} if the annotation targets + * 'typeRef' as a whole. + * @param descriptor the class descriptor of the annotation class. + * @param visible {@literal true} if the annotation is visible at runtime. + * @return a visitor to visit the annotation values, or {@literal null} if this visitor is not + * interested in visiting this annotation. + */ + public AnnotationVisitor visitTypeAnnotation( + final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) { + if (api < Opcodes.ASM5) { + throw new UnsupportedOperationException("This feature requires ASM5"); + } + if (fv != null) { + return fv.visitTypeAnnotation(typeRef, typePath, descriptor, visible); + } + return null; + } + + /** + * Visits a non standard attribute of the field. + * + * @param attribute an attribute. + */ + public void visitAttribute(final Attribute attribute) { + if (fv != null) { + fv.visitAttribute(attribute); + } + } + + /** + * Visits the end of the field. This method, which is the last one to be called, is used to inform + * the visitor that all the annotations and attributes of the field have been visited. + */ + public void visitEnd() { + if (fv != null) { + fv.visitEnd(); + } + } +} diff --git a/spring-core/src/main/java/org/springframework/asm/FieldWriter.java b/spring-core/src/main/java/org/springframework/asm/FieldWriter.java new file mode 100644 index 0000000..a638a36 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/FieldWriter.java @@ -0,0 +1,284 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +package org.springframework.asm; + +/** + * A {@link FieldVisitor} that generates a corresponding 'field_info' structure, as defined in the + * Java Virtual Machine Specification (JVMS). + * + * @see JVMS + * 4.5 + * @author Eric Bruneton + */ +final class FieldWriter extends FieldVisitor { + + /** Where the constants used in this FieldWriter must be stored. */ + private final SymbolTable symbolTable; + + // Note: fields are ordered as in the field_info structure, and those related to attributes are + // ordered as in Section 4.7 of the JVMS. + + /** + * The access_flags field of the field_info JVMS structure. This field can contain ASM specific + * access flags, such as {@link Opcodes#ACC_DEPRECATED}, which are removed when generating the + * ClassFile structure. + */ + private final int accessFlags; + + /** The name_index field of the field_info JVMS structure. */ + private final int nameIndex; + + /** The descriptor_index field of the field_info JVMS structure. */ + private final int descriptorIndex; + + /** + * The signature_index field of the Signature attribute of this field_info, or 0 if there is no + * Signature attribute. + */ + private int signatureIndex; + + /** + * The constantvalue_index field of the ConstantValue attribute of this field_info, or 0 if there + * is no ConstantValue attribute. + */ + private int constantValueIndex; + + /** + * The last runtime visible annotation of this field. The previous ones can be accessed with the + * {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeVisibleAnnotation; + + /** + * The last runtime invisible annotation of this field. The previous ones can be accessed with the + * {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeInvisibleAnnotation; + + /** + * The last runtime visible type annotation of this field. The previous ones can be accessed with + * the {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeVisibleTypeAnnotation; + + /** + * The last runtime invisible type annotation of this field. The previous ones can be accessed + * with the {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeInvisibleTypeAnnotation; + + /** + * The first non standard attribute of this field. The next ones can be accessed with the {@link + * Attribute#nextAttribute} field. May be {@literal null}. + * + *

    WARNING: this list stores the attributes in the reverse order of their visit. + * firstAttribute is actually the last attribute visited in {@link #visitAttribute}. The {@link + * #putFieldInfo} method writes the attributes in the order defined by this list, i.e. in the + * reverse order specified by the user. + */ + private Attribute firstAttribute; + + // ----------------------------------------------------------------------------------------------- + // Constructor + // ----------------------------------------------------------------------------------------------- + + /** + * Constructs a new {@link FieldWriter}. + * + * @param symbolTable where the constants used in this FieldWriter must be stored. + * @param access the field's access flags (see {@link Opcodes}). + * @param name the field's name. + * @param descriptor the field's descriptor (see {@link Type}). + * @param signature the field's signature. May be {@literal null}. + * @param constantValue the field's constant value. May be {@literal null}. + */ + FieldWriter( + final SymbolTable symbolTable, + final int access, + final String name, + final String descriptor, + final String signature, + final Object constantValue) { + super(/* latest api = */ Opcodes.ASM9); + this.symbolTable = symbolTable; + this.accessFlags = access; + this.nameIndex = symbolTable.addConstantUtf8(name); + this.descriptorIndex = symbolTable.addConstantUtf8(descriptor); + if (signature != null) { + this.signatureIndex = symbolTable.addConstantUtf8(signature); + } + if (constantValue != null) { + this.constantValueIndex = symbolTable.addConstant(constantValue).index; + } + } + + // ----------------------------------------------------------------------------------------------- + // Implementation of the FieldVisitor abstract class + // ----------------------------------------------------------------------------------------------- + + @Override + public AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) { + if (visible) { + return lastRuntimeVisibleAnnotation = + AnnotationWriter.create(symbolTable, descriptor, lastRuntimeVisibleAnnotation); + } else { + return lastRuntimeInvisibleAnnotation = + AnnotationWriter.create(symbolTable, descriptor, lastRuntimeInvisibleAnnotation); + } + } + + @Override + public AnnotationVisitor visitTypeAnnotation( + final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) { + if (visible) { + return lastRuntimeVisibleTypeAnnotation = + AnnotationWriter.create( + symbolTable, typeRef, typePath, descriptor, lastRuntimeVisibleTypeAnnotation); + } else { + return lastRuntimeInvisibleTypeAnnotation = + AnnotationWriter.create( + symbolTable, typeRef, typePath, descriptor, lastRuntimeInvisibleTypeAnnotation); + } + } + + @Override + public void visitAttribute(final Attribute attribute) { + // Store the attributes in the reverse order of their visit by this method. + attribute.nextAttribute = firstAttribute; + firstAttribute = attribute; + } + + @Override + public void visitEnd() { + // Nothing to do. + } + + // ----------------------------------------------------------------------------------------------- + // Utility methods + // ----------------------------------------------------------------------------------------------- + + /** + * Returns the size of the field_info JVMS structure generated by this FieldWriter. Also adds the + * names of the attributes of this field in the constant pool. + * + * @return the size in bytes of the field_info JVMS structure. + */ + int computeFieldInfoSize() { + // The access_flags, name_index, descriptor_index and attributes_count fields use 8 bytes. + int size = 8; + // For ease of reference, we use here the same attribute order as in Section 4.7 of the JVMS. + if (constantValueIndex != 0) { + // ConstantValue attributes always use 8 bytes. + symbolTable.addConstantUtf8(Constants.CONSTANT_VALUE); + size += 8; + } + size += Attribute.computeAttributesSize(symbolTable, accessFlags, signatureIndex); + size += + AnnotationWriter.computeAnnotationsSize( + lastRuntimeVisibleAnnotation, + lastRuntimeInvisibleAnnotation, + lastRuntimeVisibleTypeAnnotation, + lastRuntimeInvisibleTypeAnnotation); + if (firstAttribute != null) { + size += firstAttribute.computeAttributesSize(symbolTable); + } + return size; + } + + /** + * Puts the content of the field_info JVMS structure generated by this FieldWriter into the given + * ByteVector. + * + * @param output where the field_info structure must be put. + */ + void putFieldInfo(final ByteVector output) { + boolean useSyntheticAttribute = symbolTable.getMajorVersion() < Opcodes.V1_5; + // Put the access_flags, name_index and descriptor_index fields. + int mask = useSyntheticAttribute ? Opcodes.ACC_SYNTHETIC : 0; + output.putShort(accessFlags & ~mask).putShort(nameIndex).putShort(descriptorIndex); + // Compute and put the attributes_count field. + // For ease of reference, we use here the same attribute order as in Section 4.7 of the JVMS. + int attributesCount = 0; + if (constantValueIndex != 0) { + ++attributesCount; + } + if ((accessFlags & Opcodes.ACC_SYNTHETIC) != 0 && useSyntheticAttribute) { + ++attributesCount; + } + if (signatureIndex != 0) { + ++attributesCount; + } + if ((accessFlags & Opcodes.ACC_DEPRECATED) != 0) { + ++attributesCount; + } + if (lastRuntimeVisibleAnnotation != null) { + ++attributesCount; + } + if (lastRuntimeInvisibleAnnotation != null) { + ++attributesCount; + } + if (lastRuntimeVisibleTypeAnnotation != null) { + ++attributesCount; + } + if (lastRuntimeInvisibleTypeAnnotation != null) { + ++attributesCount; + } + if (firstAttribute != null) { + attributesCount += firstAttribute.getAttributeCount(); + } + output.putShort(attributesCount); + // Put the field_info attributes. + // For ease of reference, we use here the same attribute order as in Section 4.7 of the JVMS. + if (constantValueIndex != 0) { + output + .putShort(symbolTable.addConstantUtf8(Constants.CONSTANT_VALUE)) + .putInt(2) + .putShort(constantValueIndex); + } + Attribute.putAttributes(symbolTable, accessFlags, signatureIndex, output); + AnnotationWriter.putAnnotations( + symbolTable, + lastRuntimeVisibleAnnotation, + lastRuntimeInvisibleAnnotation, + lastRuntimeVisibleTypeAnnotation, + lastRuntimeInvisibleTypeAnnotation, + output); + if (firstAttribute != null) { + firstAttribute.putAttributes(symbolTable, output); + } + } + + /** + * Collects the attributes of this field into the given set of attribute prototypes. + * + * @param attributePrototypes a set of attribute prototypes. + */ + final void collectAttributePrototypes(final Attribute.Set attributePrototypes) { + attributePrototypes.addAttributes(firstAttribute); + } +} diff --git a/spring-core/src/main/java/org/springframework/asm/Frame.java b/spring-core/src/main/java/org/springframework/asm/Frame.java new file mode 100644 index 0000000..5a1dc2f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/Frame.java @@ -0,0 +1,1473 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +package org.springframework.asm; + +/** + * The input and output stack map frames of a basic block. + * + *

    Stack map frames are computed in two steps: + * + *

      + *
    • During the visit of each instruction in MethodWriter, the state of the frame at the end of + * the current basic block is updated by simulating the action of the instruction on the + * previous state of this so called "output frame". + *
    • After all instructions have been visited, a fix point algorithm is used in MethodWriter to + * compute the "input frame" of each basic block (i.e. the stack map frame at the beginning of + * the basic block). See {@link MethodWriter#computeAllFrames}. + *
    + * + *

    Output stack map frames are computed relatively to the input frame of the basic block, which + * is not yet known when output frames are computed. It is therefore necessary to be able to + * represent abstract types such as "the type at position x in the input frame locals" or "the type + * at position x from the top of the input frame stack" or even "the type at position x in the input + * frame, with y more (or less) array dimensions". This explains the rather complicated type format + * used in this class, explained below. + * + *

    The local variables and the operand stack of input and output frames contain values called + * "abstract types" hereafter. An abstract type is represented with 4 fields named DIM, KIND, FLAGS + * and VALUE, packed in a single int value for better performance and memory efficiency: + * + *

    + *   =====================================
    + *   |...DIM|KIND|.F|...............VALUE|
    + *   =====================================
    + * 
    + * + *
      + *
    • the DIM field, stored in the 6 most significant bits, is a signed number of array + * dimensions (from -32 to 31, included). It can be retrieved with {@link #DIM_MASK} and a + * right shift of {@link #DIM_SHIFT}. + *
    • the KIND field, stored in 4 bits, indicates the kind of VALUE used. These 4 bits can be + * retrieved with {@link #KIND_MASK} and, without any shift, must be equal to {@link + * #CONSTANT_KIND}, {@link #REFERENCE_KIND}, {@link #UNINITIALIZED_KIND}, {@link #LOCAL_KIND} + * or {@link #STACK_KIND}. + *
    • the FLAGS field, stored in 2 bits, contains up to 2 boolean flags. Currently only one flag + * is defined, namely {@link #TOP_IF_LONG_OR_DOUBLE_FLAG}. + *
    • the VALUE field, stored in the remaining 20 bits, contains either + *
        + *
      • one of the constants {@link #ITEM_TOP}, {@link #ITEM_ASM_BOOLEAN}, {@link + * #ITEM_ASM_BYTE}, {@link #ITEM_ASM_CHAR} or {@link #ITEM_ASM_SHORT}, {@link + * #ITEM_INTEGER}, {@link #ITEM_FLOAT}, {@link #ITEM_LONG}, {@link #ITEM_DOUBLE}, {@link + * #ITEM_NULL} or {@link #ITEM_UNINITIALIZED_THIS}, if KIND is equal to {@link + * #CONSTANT_KIND}. + *
      • the index of a {@link Symbol#TYPE_TAG} {@link Symbol} in the type table of a {@link + * SymbolTable}, if KIND is equal to {@link #REFERENCE_KIND}. + *
      • the index of an {@link Symbol#UNINITIALIZED_TYPE_TAG} {@link Symbol} in the type + * table of a SymbolTable, if KIND is equal to {@link #UNINITIALIZED_KIND}. + *
      • the index of a local variable in the input stack frame, if KIND is equal to {@link + * #LOCAL_KIND}. + *
      • a position relatively to the top of the stack of the input stack frame, if KIND is + * equal to {@link #STACK_KIND}, + *
      + *
    + * + *

    Output frames can contain abstract types of any kind and with a positive or negative array + * dimension (and even unassigned types, represented by 0 - which does not correspond to any valid + * abstract type value). Input frames can only contain CONSTANT_KIND, REFERENCE_KIND or + * UNINITIALIZED_KIND abstract types of positive or {@literal null} array dimension. In all cases + * the type table contains only internal type names (array type descriptors are forbidden - array + * dimensions must be represented through the DIM field). + * + *

    The LONG and DOUBLE types are always represented by using two slots (LONG + TOP or DOUBLE + + * TOP), for local variables as well as in the operand stack. This is necessary to be able to + * simulate DUPx_y instructions, whose effect would be dependent on the concrete types represented + * by the abstract types in the stack (which are not always known). + * + * @author Eric Bruneton + */ +class Frame { + + // Constants used in the StackMapTable attribute. + // See https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.7.4. + + static final int SAME_FRAME = 0; + static final int SAME_LOCALS_1_STACK_ITEM_FRAME = 64; + static final int RESERVED = 128; + static final int SAME_LOCALS_1_STACK_ITEM_FRAME_EXTENDED = 247; + static final int CHOP_FRAME = 248; + static final int SAME_FRAME_EXTENDED = 251; + static final int APPEND_FRAME = 252; + static final int FULL_FRAME = 255; + + static final int ITEM_TOP = 0; + static final int ITEM_INTEGER = 1; + static final int ITEM_FLOAT = 2; + static final int ITEM_DOUBLE = 3; + static final int ITEM_LONG = 4; + static final int ITEM_NULL = 5; + static final int ITEM_UNINITIALIZED_THIS = 6; + static final int ITEM_OBJECT = 7; + static final int ITEM_UNINITIALIZED = 8; + // Additional, ASM specific constants used in abstract types below. + private static final int ITEM_ASM_BOOLEAN = 9; + private static final int ITEM_ASM_BYTE = 10; + private static final int ITEM_ASM_CHAR = 11; + private static final int ITEM_ASM_SHORT = 12; + + // The size and offset in bits of each field of an abstract type. + + private static final int DIM_SIZE = 6; + private static final int KIND_SIZE = 4; + private static final int FLAGS_SIZE = 2; + private static final int VALUE_SIZE = 32 - DIM_SIZE - KIND_SIZE - FLAGS_SIZE; + + private static final int DIM_SHIFT = KIND_SIZE + FLAGS_SIZE + VALUE_SIZE; + private static final int KIND_SHIFT = FLAGS_SIZE + VALUE_SIZE; + private static final int FLAGS_SHIFT = VALUE_SIZE; + + // Bitmasks to get each field of an abstract type. + + private static final int DIM_MASK = ((1 << DIM_SIZE) - 1) << DIM_SHIFT; + private static final int KIND_MASK = ((1 << KIND_SIZE) - 1) << KIND_SHIFT; + private static final int VALUE_MASK = (1 << VALUE_SIZE) - 1; + + // Constants to manipulate the DIM field of an abstract type. + + /** The constant to be added to an abstract type to get one with one more array dimension. */ + private static final int ARRAY_OF = +1 << DIM_SHIFT; + + /** The constant to be added to an abstract type to get one with one less array dimension. */ + private static final int ELEMENT_OF = -1 << DIM_SHIFT; + + // Possible values for the KIND field of an abstract type. + + private static final int CONSTANT_KIND = 1 << KIND_SHIFT; + private static final int REFERENCE_KIND = 2 << KIND_SHIFT; + private static final int UNINITIALIZED_KIND = 3 << KIND_SHIFT; + private static final int LOCAL_KIND = 4 << KIND_SHIFT; + private static final int STACK_KIND = 5 << KIND_SHIFT; + + // Possible flags for the FLAGS field of an abstract type. + + /** + * A flag used for LOCAL_KIND and STACK_KIND abstract types, indicating that if the resolved, + * concrete type is LONG or DOUBLE, TOP should be used instead (because the value has been + * partially overridden with an xSTORE instruction). + */ + private static final int TOP_IF_LONG_OR_DOUBLE_FLAG = 1 << FLAGS_SHIFT; + + // Useful predefined abstract types (all the possible CONSTANT_KIND types). + + private static final int TOP = CONSTANT_KIND | ITEM_TOP; + private static final int BOOLEAN = CONSTANT_KIND | ITEM_ASM_BOOLEAN; + private static final int BYTE = CONSTANT_KIND | ITEM_ASM_BYTE; + private static final int CHAR = CONSTANT_KIND | ITEM_ASM_CHAR; + private static final int SHORT = CONSTANT_KIND | ITEM_ASM_SHORT; + private static final int INTEGER = CONSTANT_KIND | ITEM_INTEGER; + private static final int FLOAT = CONSTANT_KIND | ITEM_FLOAT; + private static final int LONG = CONSTANT_KIND | ITEM_LONG; + private static final int DOUBLE = CONSTANT_KIND | ITEM_DOUBLE; + private static final int NULL = CONSTANT_KIND | ITEM_NULL; + private static final int UNINITIALIZED_THIS = CONSTANT_KIND | ITEM_UNINITIALIZED_THIS; + + // ----------------------------------------------------------------------------------------------- + // Instance fields + // ----------------------------------------------------------------------------------------------- + + /** The basic block to which these input and output stack map frames correspond. */ + Label owner; + + /** The input stack map frame locals. This is an array of abstract types. */ + private int[] inputLocals; + + /** The input stack map frame stack. This is an array of abstract types. */ + private int[] inputStack; + + /** The output stack map frame locals. This is an array of abstract types. */ + private int[] outputLocals; + + /** The output stack map frame stack. This is an array of abstract types. */ + private int[] outputStack; + + /** + * The start of the output stack, relatively to the input stack. This offset is always negative or + * null. A null offset means that the output stack must be appended to the input stack. A -n + * offset means that the first n output stack elements must replace the top n input stack + * elements, and that the other elements must be appended to the input stack. + */ + private short outputStackStart; + + /** The index of the top stack element in {@link #outputStack}. */ + private short outputStackTop; + + /** The number of types that are initialized in the basic block. See {@link #initializations}. */ + private int initializationCount; + + /** + * The abstract types that are initialized in the basic block. A constructor invocation on an + * UNINITIALIZED or UNINITIALIZED_THIS abstract type must replace every occurrence of this + * type in the local variables and in the operand stack. This cannot be done during the first step + * of the algorithm since, during this step, the local variables and the operand stack types are + * still abstract. It is therefore necessary to store the abstract types of the constructors which + * are invoked in the basic block, in order to do this replacement during the second step of the + * algorithm, where the frames are fully computed. Note that this array can contain abstract types + * that are relative to the input locals or to the input stack. + */ + private int[] initializations; + + // ----------------------------------------------------------------------------------------------- + // Constructor + // ----------------------------------------------------------------------------------------------- + + /** + * Constructs a new Frame. + * + * @param owner the basic block to which these input and output stack map frames correspond. + */ + Frame(final Label owner) { + this.owner = owner; + } + + /** + * Sets this frame to the value of the given frame. + * + *

    WARNING: after this method is called the two frames share the same data structures. It is + * recommended to discard the given frame to avoid unexpected side effects. + * + * @param frame The new frame value. + */ + final void copyFrom(final Frame frame) { + inputLocals = frame.inputLocals; + inputStack = frame.inputStack; + outputStackStart = 0; + outputLocals = frame.outputLocals; + outputStack = frame.outputStack; + outputStackTop = frame.outputStackTop; + initializationCount = frame.initializationCount; + initializations = frame.initializations; + } + + // ----------------------------------------------------------------------------------------------- + // Static methods to get abstract types from other type formats + // ----------------------------------------------------------------------------------------------- + + /** + * Returns the abstract type corresponding to the given public API frame element type. + * + * @param symbolTable the type table to use to lookup and store type {@link Symbol}. + * @param type a frame element type described using the same format as in {@link + * MethodVisitor#visitFrame}, i.e. either {@link Opcodes#TOP}, {@link Opcodes#INTEGER}, {@link + * Opcodes#FLOAT}, {@link Opcodes#LONG}, {@link Opcodes#DOUBLE}, {@link Opcodes#NULL}, or + * {@link Opcodes#UNINITIALIZED_THIS}, or the internal name of a class, or a Label designating + * a NEW instruction (for uninitialized types). + * @return the abstract type corresponding to the given frame element type. + */ + static int getAbstractTypeFromApiFormat(final SymbolTable symbolTable, final Object type) { + if (type instanceof Integer) { + return CONSTANT_KIND | ((Integer) type).intValue(); + } else if (type instanceof String) { + String descriptor = Type.getObjectType((String) type).getDescriptor(); + return getAbstractTypeFromDescriptor(symbolTable, descriptor, 0); + } else { + return UNINITIALIZED_KIND + | symbolTable.addUninitializedType("", ((Label) type).bytecodeOffset); + } + } + + /** + * Returns the abstract type corresponding to the internal name of a class. + * + * @param symbolTable the type table to use to lookup and store type {@link Symbol}. + * @param internalName the internal name of a class. This must not be an array type + * descriptor. + * @return the abstract type value corresponding to the given internal name. + */ + static int getAbstractTypeFromInternalName( + final SymbolTable symbolTable, final String internalName) { + return REFERENCE_KIND | symbolTable.addType(internalName); + } + + /** + * Returns the abstract type corresponding to the given type descriptor. + * + * @param symbolTable the type table to use to lookup and store type {@link Symbol}. + * @param buffer a string ending with a type descriptor. + * @param offset the start offset of the type descriptor in buffer. + * @return the abstract type corresponding to the given type descriptor. + */ + private static int getAbstractTypeFromDescriptor( + final SymbolTable symbolTable, final String buffer, final int offset) { + String internalName; + switch (buffer.charAt(offset)) { + case 'V': + return 0; + case 'Z': + case 'C': + case 'B': + case 'S': + case 'I': + return INTEGER; + case 'F': + return FLOAT; + case 'J': + return LONG; + case 'D': + return DOUBLE; + case 'L': + internalName = buffer.substring(offset + 1, buffer.length() - 1); + return REFERENCE_KIND | symbolTable.addType(internalName); + case '[': + int elementDescriptorOffset = offset + 1; + while (buffer.charAt(elementDescriptorOffset) == '[') { + ++elementDescriptorOffset; + } + int typeValue; + switch (buffer.charAt(elementDescriptorOffset)) { + case 'Z': + typeValue = BOOLEAN; + break; + case 'C': + typeValue = CHAR; + break; + case 'B': + typeValue = BYTE; + break; + case 'S': + typeValue = SHORT; + break; + case 'I': + typeValue = INTEGER; + break; + case 'F': + typeValue = FLOAT; + break; + case 'J': + typeValue = LONG; + break; + case 'D': + typeValue = DOUBLE; + break; + case 'L': + internalName = buffer.substring(elementDescriptorOffset + 1, buffer.length() - 1); + typeValue = REFERENCE_KIND | symbolTable.addType(internalName); + break; + default: + throw new IllegalArgumentException(); + } + return ((elementDescriptorOffset - offset) << DIM_SHIFT) | typeValue; + default: + throw new IllegalArgumentException(); + } + } + + // ----------------------------------------------------------------------------------------------- + // Methods related to the input frame + // ----------------------------------------------------------------------------------------------- + + /** + * Sets the input frame from the given method description. This method is used to initialize the + * first frame of a method, which is implicit (i.e. not stored explicitly in the StackMapTable + * attribute). + * + * @param symbolTable the type table to use to lookup and store type {@link Symbol}. + * @param access the method's access flags. + * @param descriptor the method descriptor. + * @param maxLocals the maximum number of local variables of the method. + */ + final void setInputFrameFromDescriptor( + final SymbolTable symbolTable, + final int access, + final String descriptor, + final int maxLocals) { + inputLocals = new int[maxLocals]; + inputStack = new int[0]; + int inputLocalIndex = 0; + if ((access & Opcodes.ACC_STATIC) == 0) { + if ((access & Constants.ACC_CONSTRUCTOR) == 0) { + inputLocals[inputLocalIndex++] = + REFERENCE_KIND | symbolTable.addType(symbolTable.getClassName()); + } else { + inputLocals[inputLocalIndex++] = UNINITIALIZED_THIS; + } + } + for (Type argumentType : Type.getArgumentTypes(descriptor)) { + int abstractType = + getAbstractTypeFromDescriptor(symbolTable, argumentType.getDescriptor(), 0); + inputLocals[inputLocalIndex++] = abstractType; + if (abstractType == LONG || abstractType == DOUBLE) { + inputLocals[inputLocalIndex++] = TOP; + } + } + while (inputLocalIndex < maxLocals) { + inputLocals[inputLocalIndex++] = TOP; + } + } + + /** + * Sets the input frame from the given public API frame description. + * + * @param symbolTable the type table to use to lookup and store type {@link Symbol}. + * @param numLocal the number of local variables. + * @param local the local variable types, described using the same format as in {@link + * MethodVisitor#visitFrame}. + * @param numStack the number of operand stack elements. + * @param stack the operand stack types, described using the same format as in {@link + * MethodVisitor#visitFrame}. + */ + final void setInputFrameFromApiFormat( + final SymbolTable symbolTable, + final int numLocal, + final Object[] local, + final int numStack, + final Object[] stack) { + int inputLocalIndex = 0; + for (int i = 0; i < numLocal; ++i) { + inputLocals[inputLocalIndex++] = getAbstractTypeFromApiFormat(symbolTable, local[i]); + if (local[i] == Opcodes.LONG || local[i] == Opcodes.DOUBLE) { + inputLocals[inputLocalIndex++] = TOP; + } + } + while (inputLocalIndex < inputLocals.length) { + inputLocals[inputLocalIndex++] = TOP; + } + int numStackTop = 0; + for (int i = 0; i < numStack; ++i) { + if (stack[i] == Opcodes.LONG || stack[i] == Opcodes.DOUBLE) { + ++numStackTop; + } + } + inputStack = new int[numStack + numStackTop]; + int inputStackIndex = 0; + for (int i = 0; i < numStack; ++i) { + inputStack[inputStackIndex++] = getAbstractTypeFromApiFormat(symbolTable, stack[i]); + if (stack[i] == Opcodes.LONG || stack[i] == Opcodes.DOUBLE) { + inputStack[inputStackIndex++] = TOP; + } + } + outputStackTop = 0; + initializationCount = 0; + } + + final int getInputStackSize() { + return inputStack.length; + } + + // ----------------------------------------------------------------------------------------------- + // Methods related to the output frame + // ----------------------------------------------------------------------------------------------- + + /** + * Returns the abstract type stored at the given local variable index in the output frame. + * + * @param localIndex the index of the local variable whose value must be returned. + * @return the abstract type stored at the given local variable index in the output frame. + */ + private int getLocal(final int localIndex) { + if (outputLocals == null || localIndex >= outputLocals.length) { + // If this local has never been assigned in this basic block, it is still equal to its value + // in the input frame. + return LOCAL_KIND | localIndex; + } else { + int abstractType = outputLocals[localIndex]; + if (abstractType == 0) { + // If this local has never been assigned in this basic block, so it is still equal to its + // value in the input frame. + abstractType = outputLocals[localIndex] = LOCAL_KIND | localIndex; + } + return abstractType; + } + } + + /** + * Replaces the abstract type stored at the given local variable index in the output frame. + * + * @param localIndex the index of the output frame local variable that must be set. + * @param abstractType the value that must be set. + */ + private void setLocal(final int localIndex, final int abstractType) { + // Create and/or resize the output local variables array if necessary. + if (outputLocals == null) { + outputLocals = new int[10]; + } + int outputLocalsLength = outputLocals.length; + if (localIndex >= outputLocalsLength) { + int[] newOutputLocals = new int[Math.max(localIndex + 1, 2 * outputLocalsLength)]; + System.arraycopy(outputLocals, 0, newOutputLocals, 0, outputLocalsLength); + outputLocals = newOutputLocals; + } + // Set the local variable. + outputLocals[localIndex] = abstractType; + } + + /** + * Pushes the given abstract type on the output frame stack. + * + * @param abstractType an abstract type. + */ + private void push(final int abstractType) { + // Create and/or resize the output stack array if necessary. + if (outputStack == null) { + outputStack = new int[10]; + } + int outputStackLength = outputStack.length; + if (outputStackTop >= outputStackLength) { + int[] newOutputStack = new int[Math.max(outputStackTop + 1, 2 * outputStackLength)]; + System.arraycopy(outputStack, 0, newOutputStack, 0, outputStackLength); + outputStack = newOutputStack; + } + // Pushes the abstract type on the output stack. + outputStack[outputStackTop++] = abstractType; + // Updates the maximum size reached by the output stack, if needed (note that this size is + // relative to the input stack size, which is not known yet). + short outputStackSize = (short) (outputStackStart + outputStackTop); + if (outputStackSize > owner.outputStackMax) { + owner.outputStackMax = outputStackSize; + } + } + + /** + * Pushes the abstract type corresponding to the given descriptor on the output frame stack. + * + * @param symbolTable the type table to use to lookup and store type {@link Symbol}. + * @param descriptor a type or method descriptor (in which case its return type is pushed). + */ + private void push(final SymbolTable symbolTable, final String descriptor) { + int typeDescriptorOffset = + descriptor.charAt(0) == '(' ? Type.getReturnTypeOffset(descriptor) : 0; + int abstractType = getAbstractTypeFromDescriptor(symbolTable, descriptor, typeDescriptorOffset); + if (abstractType != 0) { + push(abstractType); + if (abstractType == LONG || abstractType == DOUBLE) { + push(TOP); + } + } + } + + /** + * Pops an abstract type from the output frame stack and returns its value. + * + * @return the abstract type that has been popped from the output frame stack. + */ + private int pop() { + if (outputStackTop > 0) { + return outputStack[--outputStackTop]; + } else { + // If the output frame stack is empty, pop from the input stack. + return STACK_KIND | -(--outputStackStart); + } + } + + /** + * Pops the given number of abstract types from the output frame stack. + * + * @param elements the number of abstract types that must be popped. + */ + private void pop(final int elements) { + if (outputStackTop >= elements) { + outputStackTop -= elements; + } else { + // If the number of elements to be popped is greater than the number of elements in the output + // stack, clear it, and pop the remaining elements from the input stack. + outputStackStart -= elements - outputStackTop; + outputStackTop = 0; + } + } + + /** + * Pops as many abstract types from the output frame stack as described by the given descriptor. + * + * @param descriptor a type or method descriptor (in which case its argument types are popped). + */ + private void pop(final String descriptor) { + char firstDescriptorChar = descriptor.charAt(0); + if (firstDescriptorChar == '(') { + pop((Type.getArgumentsAndReturnSizes(descriptor) >> 2) - 1); + } else if (firstDescriptorChar == 'J' || firstDescriptorChar == 'D') { + pop(2); + } else { + pop(1); + } + } + + // ----------------------------------------------------------------------------------------------- + // Methods to handle uninitialized types + // ----------------------------------------------------------------------------------------------- + + /** + * Adds an abstract type to the list of types on which a constructor is invoked in the basic + * block. + * + * @param abstractType an abstract type on a which a constructor is invoked. + */ + private void addInitializedType(final int abstractType) { + // Create and/or resize the initializations array if necessary. + if (initializations == null) { + initializations = new int[2]; + } + int initializationsLength = initializations.length; + if (initializationCount >= initializationsLength) { + int[] newInitializations = + new int[Math.max(initializationCount + 1, 2 * initializationsLength)]; + System.arraycopy(initializations, 0, newInitializations, 0, initializationsLength); + initializations = newInitializations; + } + // Store the abstract type. + initializations[initializationCount++] = abstractType; + } + + /** + * Returns the "initialized" abstract type corresponding to the given abstract type. + * + * @param symbolTable the type table to use to lookup and store type {@link Symbol}. + * @param abstractType an abstract type. + * @return the REFERENCE_KIND abstract type corresponding to abstractType if it is + * UNINITIALIZED_THIS or an UNINITIALIZED_KIND abstract type for one of the types on which a + * constructor is invoked in the basic block. Otherwise returns abstractType. + */ + private int getInitializedType(final SymbolTable symbolTable, final int abstractType) { + if (abstractType == UNINITIALIZED_THIS + || (abstractType & (DIM_MASK | KIND_MASK)) == UNINITIALIZED_KIND) { + for (int i = 0; i < initializationCount; ++i) { + int initializedType = initializations[i]; + int dim = initializedType & DIM_MASK; + int kind = initializedType & KIND_MASK; + int value = initializedType & VALUE_MASK; + if (kind == LOCAL_KIND) { + initializedType = dim + inputLocals[value]; + } else if (kind == STACK_KIND) { + initializedType = dim + inputStack[inputStack.length - value]; + } + if (abstractType == initializedType) { + if (abstractType == UNINITIALIZED_THIS) { + return REFERENCE_KIND | symbolTable.addType(symbolTable.getClassName()); + } else { + return REFERENCE_KIND + | symbolTable.addType(symbolTable.getType(abstractType & VALUE_MASK).value); + } + } + } + } + return abstractType; + } + + // ----------------------------------------------------------------------------------------------- + // Main method, to simulate the execution of each instruction on the output frame + // ----------------------------------------------------------------------------------------------- + + /** + * Simulates the action of the given instruction on the output stack frame. + * + * @param opcode the opcode of the instruction. + * @param arg the numeric operand of the instruction, if any. + * @param argSymbol the Symbol operand of the instruction, if any. + * @param symbolTable the type table to use to lookup and store type {@link Symbol}. + */ + void execute( + final int opcode, final int arg, final Symbol argSymbol, final SymbolTable symbolTable) { + // Abstract types popped from the stack or read from local variables. + int abstractType1; + int abstractType2; + int abstractType3; + int abstractType4; + switch (opcode) { + case Opcodes.NOP: + case Opcodes.INEG: + case Opcodes.LNEG: + case Opcodes.FNEG: + case Opcodes.DNEG: + case Opcodes.I2B: + case Opcodes.I2C: + case Opcodes.I2S: + case Opcodes.GOTO: + case Opcodes.RETURN: + break; + case Opcodes.ACONST_NULL: + push(NULL); + break; + case Opcodes.ICONST_M1: + case Opcodes.ICONST_0: + case Opcodes.ICONST_1: + case Opcodes.ICONST_2: + case Opcodes.ICONST_3: + case Opcodes.ICONST_4: + case Opcodes.ICONST_5: + case Opcodes.BIPUSH: + case Opcodes.SIPUSH: + case Opcodes.ILOAD: + push(INTEGER); + break; + case Opcodes.LCONST_0: + case Opcodes.LCONST_1: + case Opcodes.LLOAD: + push(LONG); + push(TOP); + break; + case Opcodes.FCONST_0: + case Opcodes.FCONST_1: + case Opcodes.FCONST_2: + case Opcodes.FLOAD: + push(FLOAT); + break; + case Opcodes.DCONST_0: + case Opcodes.DCONST_1: + case Opcodes.DLOAD: + push(DOUBLE); + push(TOP); + break; + case Opcodes.LDC: + switch (argSymbol.tag) { + case Symbol.CONSTANT_INTEGER_TAG: + push(INTEGER); + break; + case Symbol.CONSTANT_LONG_TAG: + push(LONG); + push(TOP); + break; + case Symbol.CONSTANT_FLOAT_TAG: + push(FLOAT); + break; + case Symbol.CONSTANT_DOUBLE_TAG: + push(DOUBLE); + push(TOP); + break; + case Symbol.CONSTANT_CLASS_TAG: + push(REFERENCE_KIND | symbolTable.addType("java/lang/Class")); + break; + case Symbol.CONSTANT_STRING_TAG: + push(REFERENCE_KIND | symbolTable.addType("java/lang/String")); + break; + case Symbol.CONSTANT_METHOD_TYPE_TAG: + push(REFERENCE_KIND | symbolTable.addType("java/lang/invoke/MethodType")); + break; + case Symbol.CONSTANT_METHOD_HANDLE_TAG: + push(REFERENCE_KIND | symbolTable.addType("java/lang/invoke/MethodHandle")); + break; + case Symbol.CONSTANT_DYNAMIC_TAG: + push(symbolTable, argSymbol.value); + break; + default: + throw new AssertionError(); + } + break; + case Opcodes.ALOAD: + push(getLocal(arg)); + break; + case Opcodes.LALOAD: + case Opcodes.D2L: + pop(2); + push(LONG); + push(TOP); + break; + case Opcodes.DALOAD: + case Opcodes.L2D: + pop(2); + push(DOUBLE); + push(TOP); + break; + case Opcodes.AALOAD: + pop(1); + abstractType1 = pop(); + push(abstractType1 == NULL ? abstractType1 : ELEMENT_OF + abstractType1); + break; + case Opcodes.ISTORE: + case Opcodes.FSTORE: + case Opcodes.ASTORE: + abstractType1 = pop(); + setLocal(arg, abstractType1); + if (arg > 0) { + int previousLocalType = getLocal(arg - 1); + if (previousLocalType == LONG || previousLocalType == DOUBLE) { + setLocal(arg - 1, TOP); + } else if ((previousLocalType & KIND_MASK) == LOCAL_KIND + || (previousLocalType & KIND_MASK) == STACK_KIND) { + // The type of the previous local variable is not known yet, but if it later appears + // to be LONG or DOUBLE, we should then use TOP instead. + setLocal(arg - 1, previousLocalType | TOP_IF_LONG_OR_DOUBLE_FLAG); + } + } + break; + case Opcodes.LSTORE: + case Opcodes.DSTORE: + pop(1); + abstractType1 = pop(); + setLocal(arg, abstractType1); + setLocal(arg + 1, TOP); + if (arg > 0) { + int previousLocalType = getLocal(arg - 1); + if (previousLocalType == LONG || previousLocalType == DOUBLE) { + setLocal(arg - 1, TOP); + } else if ((previousLocalType & KIND_MASK) == LOCAL_KIND + || (previousLocalType & KIND_MASK) == STACK_KIND) { + // The type of the previous local variable is not known yet, but if it later appears + // to be LONG or DOUBLE, we should then use TOP instead. + setLocal(arg - 1, previousLocalType | TOP_IF_LONG_OR_DOUBLE_FLAG); + } + } + break; + case Opcodes.IASTORE: + case Opcodes.BASTORE: + case Opcodes.CASTORE: + case Opcodes.SASTORE: + case Opcodes.FASTORE: + case Opcodes.AASTORE: + pop(3); + break; + case Opcodes.LASTORE: + case Opcodes.DASTORE: + pop(4); + break; + case Opcodes.POP: + case Opcodes.IFEQ: + case Opcodes.IFNE: + case Opcodes.IFLT: + case Opcodes.IFGE: + case Opcodes.IFGT: + case Opcodes.IFLE: + case Opcodes.IRETURN: + case Opcodes.FRETURN: + case Opcodes.ARETURN: + case Opcodes.TABLESWITCH: + case Opcodes.LOOKUPSWITCH: + case Opcodes.ATHROW: + case Opcodes.MONITORENTER: + case Opcodes.MONITOREXIT: + case Opcodes.IFNULL: + case Opcodes.IFNONNULL: + pop(1); + break; + case Opcodes.POP2: + case Opcodes.IF_ICMPEQ: + case Opcodes.IF_ICMPNE: + case Opcodes.IF_ICMPLT: + case Opcodes.IF_ICMPGE: + case Opcodes.IF_ICMPGT: + case Opcodes.IF_ICMPLE: + case Opcodes.IF_ACMPEQ: + case Opcodes.IF_ACMPNE: + case Opcodes.LRETURN: + case Opcodes.DRETURN: + pop(2); + break; + case Opcodes.DUP: + abstractType1 = pop(); + push(abstractType1); + push(abstractType1); + break; + case Opcodes.DUP_X1: + abstractType1 = pop(); + abstractType2 = pop(); + push(abstractType1); + push(abstractType2); + push(abstractType1); + break; + case Opcodes.DUP_X2: + abstractType1 = pop(); + abstractType2 = pop(); + abstractType3 = pop(); + push(abstractType1); + push(abstractType3); + push(abstractType2); + push(abstractType1); + break; + case Opcodes.DUP2: + abstractType1 = pop(); + abstractType2 = pop(); + push(abstractType2); + push(abstractType1); + push(abstractType2); + push(abstractType1); + break; + case Opcodes.DUP2_X1: + abstractType1 = pop(); + abstractType2 = pop(); + abstractType3 = pop(); + push(abstractType2); + push(abstractType1); + push(abstractType3); + push(abstractType2); + push(abstractType1); + break; + case Opcodes.DUP2_X2: + abstractType1 = pop(); + abstractType2 = pop(); + abstractType3 = pop(); + abstractType4 = pop(); + push(abstractType2); + push(abstractType1); + push(abstractType4); + push(abstractType3); + push(abstractType2); + push(abstractType1); + break; + case Opcodes.SWAP: + abstractType1 = pop(); + abstractType2 = pop(); + push(abstractType1); + push(abstractType2); + break; + case Opcodes.IALOAD: + case Opcodes.BALOAD: + case Opcodes.CALOAD: + case Opcodes.SALOAD: + case Opcodes.IADD: + case Opcodes.ISUB: + case Opcodes.IMUL: + case Opcodes.IDIV: + case Opcodes.IREM: + case Opcodes.IAND: + case Opcodes.IOR: + case Opcodes.IXOR: + case Opcodes.ISHL: + case Opcodes.ISHR: + case Opcodes.IUSHR: + case Opcodes.L2I: + case Opcodes.D2I: + case Opcodes.FCMPL: + case Opcodes.FCMPG: + pop(2); + push(INTEGER); + break; + case Opcodes.LADD: + case Opcodes.LSUB: + case Opcodes.LMUL: + case Opcodes.LDIV: + case Opcodes.LREM: + case Opcodes.LAND: + case Opcodes.LOR: + case Opcodes.LXOR: + pop(4); + push(LONG); + push(TOP); + break; + case Opcodes.FALOAD: + case Opcodes.FADD: + case Opcodes.FSUB: + case Opcodes.FMUL: + case Opcodes.FDIV: + case Opcodes.FREM: + case Opcodes.L2F: + case Opcodes.D2F: + pop(2); + push(FLOAT); + break; + case Opcodes.DADD: + case Opcodes.DSUB: + case Opcodes.DMUL: + case Opcodes.DDIV: + case Opcodes.DREM: + pop(4); + push(DOUBLE); + push(TOP); + break; + case Opcodes.LSHL: + case Opcodes.LSHR: + case Opcodes.LUSHR: + pop(3); + push(LONG); + push(TOP); + break; + case Opcodes.IINC: + setLocal(arg, INTEGER); + break; + case Opcodes.I2L: + case Opcodes.F2L: + pop(1); + push(LONG); + push(TOP); + break; + case Opcodes.I2F: + pop(1); + push(FLOAT); + break; + case Opcodes.I2D: + case Opcodes.F2D: + pop(1); + push(DOUBLE); + push(TOP); + break; + case Opcodes.F2I: + case Opcodes.ARRAYLENGTH: + case Opcodes.INSTANCEOF: + pop(1); + push(INTEGER); + break; + case Opcodes.LCMP: + case Opcodes.DCMPL: + case Opcodes.DCMPG: + pop(4); + push(INTEGER); + break; + case Opcodes.JSR: + case Opcodes.RET: + throw new IllegalArgumentException("JSR/RET are not supported with computeFrames option"); + case Opcodes.GETSTATIC: + push(symbolTable, argSymbol.value); + break; + case Opcodes.PUTSTATIC: + pop(argSymbol.value); + break; + case Opcodes.GETFIELD: + pop(1); + push(symbolTable, argSymbol.value); + break; + case Opcodes.PUTFIELD: + pop(argSymbol.value); + pop(); + break; + case Opcodes.INVOKEVIRTUAL: + case Opcodes.INVOKESPECIAL: + case Opcodes.INVOKESTATIC: + case Opcodes.INVOKEINTERFACE: + pop(argSymbol.value); + if (opcode != Opcodes.INVOKESTATIC) { + abstractType1 = pop(); + if (opcode == Opcodes.INVOKESPECIAL && argSymbol.name.charAt(0) == '<') { + addInitializedType(abstractType1); + } + } + push(symbolTable, argSymbol.value); + break; + case Opcodes.INVOKEDYNAMIC: + pop(argSymbol.value); + push(symbolTable, argSymbol.value); + break; + case Opcodes.NEW: + push(UNINITIALIZED_KIND | symbolTable.addUninitializedType(argSymbol.value, arg)); + break; + case Opcodes.NEWARRAY: + pop(); + switch (arg) { + case Opcodes.T_BOOLEAN: + push(ARRAY_OF | BOOLEAN); + break; + case Opcodes.T_CHAR: + push(ARRAY_OF | CHAR); + break; + case Opcodes.T_BYTE: + push(ARRAY_OF | BYTE); + break; + case Opcodes.T_SHORT: + push(ARRAY_OF | SHORT); + break; + case Opcodes.T_INT: + push(ARRAY_OF | INTEGER); + break; + case Opcodes.T_FLOAT: + push(ARRAY_OF | FLOAT); + break; + case Opcodes.T_DOUBLE: + push(ARRAY_OF | DOUBLE); + break; + case Opcodes.T_LONG: + push(ARRAY_OF | LONG); + break; + default: + throw new IllegalArgumentException(); + } + break; + case Opcodes.ANEWARRAY: + String arrayElementType = argSymbol.value; + pop(); + if (arrayElementType.charAt(0) == '[') { + push(symbolTable, '[' + arrayElementType); + } else { + push(ARRAY_OF | REFERENCE_KIND | symbolTable.addType(arrayElementType)); + } + break; + case Opcodes.CHECKCAST: + String castType = argSymbol.value; + pop(); + if (castType.charAt(0) == '[') { + push(symbolTable, castType); + } else { + push(REFERENCE_KIND | symbolTable.addType(castType)); + } + break; + case Opcodes.MULTIANEWARRAY: + pop(arg); + push(symbolTable, argSymbol.value); + break; + default: + throw new IllegalArgumentException(); + } + } + + // ----------------------------------------------------------------------------------------------- + // Frame merging methods, used in the second step of the stack map frame computation algorithm + // ----------------------------------------------------------------------------------------------- + + /** + * Computes the concrete output type corresponding to a given abstract output type. + * + * @param abstractOutputType an abstract output type. + * @param numStack the size of the input stack, used to resolve abstract output types of + * STACK_KIND kind. + * @return the concrete output type corresponding to 'abstractOutputType'. + */ + private int getConcreteOutputType(final int abstractOutputType, final int numStack) { + int dim = abstractOutputType & DIM_MASK; + int kind = abstractOutputType & KIND_MASK; + if (kind == LOCAL_KIND) { + // By definition, a LOCAL_KIND type designates the concrete type of a local variable at + // the beginning of the basic block corresponding to this frame (which is known when + // this method is called, but was not when the abstract type was computed). + int concreteOutputType = dim + inputLocals[abstractOutputType & VALUE_MASK]; + if ((abstractOutputType & TOP_IF_LONG_OR_DOUBLE_FLAG) != 0 + && (concreteOutputType == LONG || concreteOutputType == DOUBLE)) { + concreteOutputType = TOP; + } + return concreteOutputType; + } else if (kind == STACK_KIND) { + // By definition, a STACK_KIND type designates the concrete type of a local variable at + // the beginning of the basic block corresponding to this frame (which is known when + // this method is called, but was not when the abstract type was computed). + int concreteOutputType = dim + inputStack[numStack - (abstractOutputType & VALUE_MASK)]; + if ((abstractOutputType & TOP_IF_LONG_OR_DOUBLE_FLAG) != 0 + && (concreteOutputType == LONG || concreteOutputType == DOUBLE)) { + concreteOutputType = TOP; + } + return concreteOutputType; + } else { + return abstractOutputType; + } + } + + /** + * Merges the input frame of the given {@link Frame} with the input and output frames of this + * {@link Frame}. Returns {@literal true} if the given frame has been changed by this operation + * (the input and output frames of this {@link Frame} are never changed). + * + * @param symbolTable the type table to use to lookup and store type {@link Symbol}. + * @param dstFrame the {@link Frame} whose input frame must be updated. This should be the frame + * of a successor, in the control flow graph, of the basic block corresponding to this frame. + * @param catchTypeIndex if 'frame' corresponds to an exception handler basic block, the type + * table index of the caught exception type, otherwise 0. + * @return {@literal true} if the input frame of 'frame' has been changed by this operation. + */ + final boolean merge( + final SymbolTable symbolTable, final Frame dstFrame, final int catchTypeIndex) { + boolean frameChanged = false; + + // Compute the concrete types of the local variables at the end of the basic block corresponding + // to this frame, by resolving its abstract output types, and merge these concrete types with + // those of the local variables in the input frame of dstFrame. + int numLocal = inputLocals.length; + int numStack = inputStack.length; + if (dstFrame.inputLocals == null) { + dstFrame.inputLocals = new int[numLocal]; + frameChanged = true; + } + for (int i = 0; i < numLocal; ++i) { + int concreteOutputType; + if (outputLocals != null && i < outputLocals.length) { + int abstractOutputType = outputLocals[i]; + if (abstractOutputType == 0) { + // If the local variable has never been assigned in this basic block, it is equal to its + // value at the beginning of the block. + concreteOutputType = inputLocals[i]; + } else { + concreteOutputType = getConcreteOutputType(abstractOutputType, numStack); + } + } else { + // If the local variable has never been assigned in this basic block, it is equal to its + // value at the beginning of the block. + concreteOutputType = inputLocals[i]; + } + // concreteOutputType might be an uninitialized type from the input locals or from the input + // stack. However, if a constructor has been called for this class type in the basic block, + // then this type is no longer uninitialized at the end of basic block. + if (initializations != null) { + concreteOutputType = getInitializedType(symbolTable, concreteOutputType); + } + frameChanged |= merge(symbolTable, concreteOutputType, dstFrame.inputLocals, i); + } + + // If dstFrame is an exception handler block, it can be reached from any instruction of the + // basic block corresponding to this frame, in particular from the first one. Therefore, the + // input locals of dstFrame should be compatible (i.e. merged) with the input locals of this + // frame (and the input stack of dstFrame should be compatible, i.e. merged, with a one + // element stack containing the caught exception type). + if (catchTypeIndex > 0) { + for (int i = 0; i < numLocal; ++i) { + frameChanged |= merge(symbolTable, inputLocals[i], dstFrame.inputLocals, i); + } + if (dstFrame.inputStack == null) { + dstFrame.inputStack = new int[1]; + frameChanged = true; + } + frameChanged |= merge(symbolTable, catchTypeIndex, dstFrame.inputStack, 0); + return frameChanged; + } + + // Compute the concrete types of the stack operands at the end of the basic block corresponding + // to this frame, by resolving its abstract output types, and merge these concrete types with + // those of the stack operands in the input frame of dstFrame. + int numInputStack = inputStack.length + outputStackStart; + if (dstFrame.inputStack == null) { + dstFrame.inputStack = new int[numInputStack + outputStackTop]; + frameChanged = true; + } + // First, do this for the stack operands that have not been popped in the basic block + // corresponding to this frame, and which are therefore equal to their value in the input + // frame (except for uninitialized types, which may have been initialized). + for (int i = 0; i < numInputStack; ++i) { + int concreteOutputType = inputStack[i]; + if (initializations != null) { + concreteOutputType = getInitializedType(symbolTable, concreteOutputType); + } + frameChanged |= merge(symbolTable, concreteOutputType, dstFrame.inputStack, i); + } + // Then, do this for the stack operands that have pushed in the basic block (this code is the + // same as the one above for local variables). + for (int i = 0; i < outputStackTop; ++i) { + int abstractOutputType = outputStack[i]; + int concreteOutputType = getConcreteOutputType(abstractOutputType, numStack); + if (initializations != null) { + concreteOutputType = getInitializedType(symbolTable, concreteOutputType); + } + frameChanged |= + merge(symbolTable, concreteOutputType, dstFrame.inputStack, numInputStack + i); + } + return frameChanged; + } + + /** + * Merges the type at the given index in the given abstract type array with the given type. + * Returns {@literal true} if the type array has been modified by this operation. + * + * @param symbolTable the type table to use to lookup and store type {@link Symbol}. + * @param sourceType the abstract type with which the abstract type array element must be merged. + * This type should be of {@link #CONSTANT_KIND}, {@link #REFERENCE_KIND} or {@link + * #UNINITIALIZED_KIND} kind, with positive or {@literal null} array dimensions. + * @param dstTypes an array of abstract types. These types should be of {@link #CONSTANT_KIND}, + * {@link #REFERENCE_KIND} or {@link #UNINITIALIZED_KIND} kind, with positive or {@literal + * null} array dimensions. + * @param dstIndex the index of the type that must be merged in dstTypes. + * @return {@literal true} if the type array has been modified by this operation. + */ + private static boolean merge( + final SymbolTable symbolTable, + final int sourceType, + final int[] dstTypes, + final int dstIndex) { + int dstType = dstTypes[dstIndex]; + if (dstType == sourceType) { + // If the types are equal, merge(sourceType, dstType) = dstType, so there is no change. + return false; + } + int srcType = sourceType; + if ((sourceType & ~DIM_MASK) == NULL) { + if (dstType == NULL) { + return false; + } + srcType = NULL; + } + if (dstType == 0) { + // If dstTypes[dstIndex] has never been assigned, merge(srcType, dstType) = srcType. + dstTypes[dstIndex] = srcType; + return true; + } + int mergedType; + if ((dstType & DIM_MASK) != 0 || (dstType & KIND_MASK) == REFERENCE_KIND) { + // If dstType is a reference type of any array dimension. + if (srcType == NULL) { + // If srcType is the NULL type, merge(srcType, dstType) = dstType, so there is no change. + return false; + } else if ((srcType & (DIM_MASK | KIND_MASK)) == (dstType & (DIM_MASK | KIND_MASK))) { + // If srcType has the same array dimension and the same kind as dstType. + if ((dstType & KIND_MASK) == REFERENCE_KIND) { + // If srcType and dstType are reference types with the same array dimension, + // merge(srcType, dstType) = dim(srcType) | common super class of srcType and dstType. + mergedType = + (srcType & DIM_MASK) + | REFERENCE_KIND + | symbolTable.addMergedType(srcType & VALUE_MASK, dstType & VALUE_MASK); + } else { + // If srcType and dstType are array types of equal dimension but different element types, + // merge(srcType, dstType) = dim(srcType) - 1 | java/lang/Object. + int mergedDim = ELEMENT_OF + (srcType & DIM_MASK); + mergedType = mergedDim | REFERENCE_KIND | symbolTable.addType("java/lang/Object"); + } + } else if ((srcType & DIM_MASK) != 0 || (srcType & KIND_MASK) == REFERENCE_KIND) { + // If srcType is any other reference or array type, + // merge(srcType, dstType) = min(srcDdim, dstDim) | java/lang/Object + // where srcDim is the array dimension of srcType, minus 1 if srcType is an array type + // with a non reference element type (and similarly for dstDim). + int srcDim = srcType & DIM_MASK; + if (srcDim != 0 && (srcType & KIND_MASK) != REFERENCE_KIND) { + srcDim = ELEMENT_OF + srcDim; + } + int dstDim = dstType & DIM_MASK; + if (dstDim != 0 && (dstType & KIND_MASK) != REFERENCE_KIND) { + dstDim = ELEMENT_OF + dstDim; + } + mergedType = + Math.min(srcDim, dstDim) | REFERENCE_KIND | symbolTable.addType("java/lang/Object"); + } else { + // If srcType is any other type, merge(srcType, dstType) = TOP. + mergedType = TOP; + } + } else if (dstType == NULL) { + // If dstType is the NULL type, merge(srcType, dstType) = srcType, or TOP if srcType is not a + // an array type or a reference type. + mergedType = + (srcType & DIM_MASK) != 0 || (srcType & KIND_MASK) == REFERENCE_KIND ? srcType : TOP; + } else { + // If dstType is any other type, merge(srcType, dstType) = TOP whatever srcType. + mergedType = TOP; + } + if (mergedType != dstType) { + dstTypes[dstIndex] = mergedType; + return true; + } + return false; + } + + // ----------------------------------------------------------------------------------------------- + // Frame output methods, to generate StackMapFrame attributes + // ----------------------------------------------------------------------------------------------- + + /** + * Makes the given {@link MethodWriter} visit the input frame of this {@link Frame}. The visit is + * done with the {@link MethodWriter#visitFrameStart}, {@link MethodWriter#visitAbstractType} and + * {@link MethodWriter#visitFrameEnd} methods. + * + * @param methodWriter the {@link MethodWriter} that should visit the input frame of this {@link + * Frame}. + */ + final void accept(final MethodWriter methodWriter) { + // Compute the number of locals, ignoring TOP types that are just after a LONG or a DOUBLE, and + // all trailing TOP types. + int[] localTypes = inputLocals; + int numLocal = 0; + int numTrailingTop = 0; + int i = 0; + while (i < localTypes.length) { + int localType = localTypes[i]; + i += (localType == LONG || localType == DOUBLE) ? 2 : 1; + if (localType == TOP) { + numTrailingTop++; + } else { + numLocal += numTrailingTop + 1; + numTrailingTop = 0; + } + } + // Compute the stack size, ignoring TOP types that are just after a LONG or a DOUBLE. + int[] stackTypes = inputStack; + int numStack = 0; + i = 0; + while (i < stackTypes.length) { + int stackType = stackTypes[i]; + i += (stackType == LONG || stackType == DOUBLE) ? 2 : 1; + numStack++; + } + // Visit the frame and its content. + int frameIndex = methodWriter.visitFrameStart(owner.bytecodeOffset, numLocal, numStack); + i = 0; + while (numLocal-- > 0) { + int localType = localTypes[i]; + i += (localType == LONG || localType == DOUBLE) ? 2 : 1; + methodWriter.visitAbstractType(frameIndex++, localType); + } + i = 0; + while (numStack-- > 0) { + int stackType = stackTypes[i]; + i += (stackType == LONG || stackType == DOUBLE) ? 2 : 1; + methodWriter.visitAbstractType(frameIndex++, stackType); + } + methodWriter.visitFrameEnd(); + } + + /** + * Put the given abstract type in the given ByteVector, using the JVMS verification_type_info + * format used in StackMapTable attributes. + * + * @param symbolTable the type table to use to lookup and store type {@link Symbol}. + * @param abstractType an abstract type, restricted to {@link Frame#CONSTANT_KIND}, {@link + * Frame#REFERENCE_KIND} or {@link Frame#UNINITIALIZED_KIND} types. + * @param output where the abstract type must be put. + * @see JVMS + * 4.7.4 + */ + static void putAbstractType( + final SymbolTable symbolTable, final int abstractType, final ByteVector output) { + int arrayDimensions = (abstractType & Frame.DIM_MASK) >> DIM_SHIFT; + if (arrayDimensions == 0) { + int typeValue = abstractType & VALUE_MASK; + switch (abstractType & KIND_MASK) { + case CONSTANT_KIND: + output.putByte(typeValue); + break; + case REFERENCE_KIND: + output + .putByte(ITEM_OBJECT) + .putShort(symbolTable.addConstantClass(symbolTable.getType(typeValue).value).index); + break; + case UNINITIALIZED_KIND: + output.putByte(ITEM_UNINITIALIZED).putShort((int) symbolTable.getType(typeValue).data); + break; + default: + throw new AssertionError(); + } + } else { + // Case of an array type, we need to build its descriptor first. + StringBuilder typeDescriptor = new StringBuilder(32); // SPRING PATCH: larger initial size + while (arrayDimensions-- > 0) { + typeDescriptor.append('['); + } + if ((abstractType & KIND_MASK) == REFERENCE_KIND) { + typeDescriptor + .append('L') + .append(symbolTable.getType(abstractType & VALUE_MASK).value) + .append(';'); + } else { + switch (abstractType & VALUE_MASK) { + case Frame.ITEM_ASM_BOOLEAN: + typeDescriptor.append('Z'); + break; + case Frame.ITEM_ASM_BYTE: + typeDescriptor.append('B'); + break; + case Frame.ITEM_ASM_CHAR: + typeDescriptor.append('C'); + break; + case Frame.ITEM_ASM_SHORT: + typeDescriptor.append('S'); + break; + case Frame.ITEM_INTEGER: + typeDescriptor.append('I'); + break; + case Frame.ITEM_FLOAT: + typeDescriptor.append('F'); + break; + case Frame.ITEM_LONG: + typeDescriptor.append('J'); + break; + case Frame.ITEM_DOUBLE: + typeDescriptor.append('D'); + break; + default: + throw new AssertionError(); + } + } + output + .putByte(ITEM_OBJECT) + .putShort(symbolTable.addConstantClass(typeDescriptor.toString()).index); + } + } +} diff --git a/spring-core/src/main/java/org/springframework/asm/Handle.java b/spring-core/src/main/java/org/springframework/asm/Handle.java new file mode 100644 index 0000000..52a3e31 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/Handle.java @@ -0,0 +1,189 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. + +package org.springframework.asm; + +/** + * A reference to a field or a method. + * + * @author Remi Forax + * @author Eric Bruneton + */ +public final class Handle { + + /** + * The kind of field or method designated by this Handle. Should be {@link Opcodes#H_GETFIELD}, + * {@link Opcodes#H_GETSTATIC}, {@link Opcodes#H_PUTFIELD}, {@link Opcodes#H_PUTSTATIC}, {@link + * Opcodes#H_INVOKEVIRTUAL}, {@link Opcodes#H_INVOKESTATIC}, {@link Opcodes#H_INVOKESPECIAL}, + * {@link Opcodes#H_NEWINVOKESPECIAL} or {@link Opcodes#H_INVOKEINTERFACE}. + */ + private final int tag; + + /** The internal name of the class that owns the field or method designated by this handle. */ + private final String owner; + + /** The name of the field or method designated by this handle. */ + private final String name; + + /** The descriptor of the field or method designated by this handle. */ + private final String descriptor; + + /** Whether the owner is an interface or not. */ + private final boolean isInterface; + + /** + * Constructs a new field or method handle. + * + * @param tag the kind of field or method designated by this Handle. Must be {@link + * Opcodes#H_GETFIELD}, {@link Opcodes#H_GETSTATIC}, {@link Opcodes#H_PUTFIELD}, {@link + * Opcodes#H_PUTSTATIC}, {@link Opcodes#H_INVOKEVIRTUAL}, {@link Opcodes#H_INVOKESTATIC}, + * {@link Opcodes#H_INVOKESPECIAL}, {@link Opcodes#H_NEWINVOKESPECIAL} or {@link + * Opcodes#H_INVOKEINTERFACE}. + * @param owner the internal name of the class that owns the field or method designated by this + * handle. + * @param name the name of the field or method designated by this handle. + * @param descriptor the descriptor of the field or method designated by this handle. + * @deprecated this constructor has been superseded by {@link #Handle(int, String, String, String, + * boolean)}. + */ + @Deprecated + public Handle(final int tag, final String owner, final String name, final String descriptor) { + this(tag, owner, name, descriptor, tag == Opcodes.H_INVOKEINTERFACE); + } + + /** + * Constructs a new field or method handle. + * + * @param tag the kind of field or method designated by this Handle. Must be {@link + * Opcodes#H_GETFIELD}, {@link Opcodes#H_GETSTATIC}, {@link Opcodes#H_PUTFIELD}, {@link + * Opcodes#H_PUTSTATIC}, {@link Opcodes#H_INVOKEVIRTUAL}, {@link Opcodes#H_INVOKESTATIC}, + * {@link Opcodes#H_INVOKESPECIAL}, {@link Opcodes#H_NEWINVOKESPECIAL} or {@link + * Opcodes#H_INVOKEINTERFACE}. + * @param owner the internal name of the class that owns the field or method designated by this + * handle. + * @param name the name of the field or method designated by this handle. + * @param descriptor the descriptor of the field or method designated by this handle. + * @param isInterface whether the owner is an interface or not. + */ + public Handle( + final int tag, + final String owner, + final String name, + final String descriptor, + final boolean isInterface) { + this.tag = tag; + this.owner = owner; + this.name = name; + this.descriptor = descriptor; + this.isInterface = isInterface; + } + + /** + * Returns the kind of field or method designated by this handle. + * + * @return {@link Opcodes#H_GETFIELD}, {@link Opcodes#H_GETSTATIC}, {@link Opcodes#H_PUTFIELD}, + * {@link Opcodes#H_PUTSTATIC}, {@link Opcodes#H_INVOKEVIRTUAL}, {@link + * Opcodes#H_INVOKESTATIC}, {@link Opcodes#H_INVOKESPECIAL}, {@link + * Opcodes#H_NEWINVOKESPECIAL} or {@link Opcodes#H_INVOKEINTERFACE}. + */ + public int getTag() { + return tag; + } + + /** + * Returns the internal name of the class that owns the field or method designated by this handle. + * + * @return the internal name of the class that owns the field or method designated by this handle. + */ + public String getOwner() { + return owner; + } + + /** + * Returns the name of the field or method designated by this handle. + * + * @return the name of the field or method designated by this handle. + */ + public String getName() { + return name; + } + + /** + * Returns the descriptor of the field or method designated by this handle. + * + * @return the descriptor of the field or method designated by this handle. + */ + public String getDesc() { + return descriptor; + } + + /** + * Returns true if the owner of the field or method designated by this handle is an interface. + * + * @return true if the owner of the field or method designated by this handle is an interface. + */ + public boolean isInterface() { + return isInterface; + } + + @Override + public boolean equals(final Object object) { + if (object == this) { + return true; + } + if (!(object instanceof Handle)) { + return false; + } + Handle handle = (Handle) object; + return tag == handle.tag + && isInterface == handle.isInterface + && owner.equals(handle.owner) + && name.equals(handle.name) + && descriptor.equals(handle.descriptor); + } + + @Override + public int hashCode() { + return tag + + (isInterface ? 64 : 0) + + owner.hashCode() * name.hashCode() * descriptor.hashCode(); + } + + /** + * Returns the textual representation of this handle. The textual representation is: + * + *

      + *
    • for a reference to a class: owner "." name descriptor " (" tag ")", + *
    • for a reference to an interface: owner "." name descriptor " (" tag " itf)". + *
    + */ + @Override + public String toString() { + return owner + '.' + name + descriptor + " (" + tag + (isInterface ? " itf" : "") + ')'; + } +} diff --git a/spring-core/src/main/java/org/springframework/asm/Handler.java b/spring-core/src/main/java/org/springframework/asm/Handler.java new file mode 100644 index 0000000..5b7f6bc --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/Handler.java @@ -0,0 +1,198 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +package org.springframework.asm; + +/** + * Information about an exception handler. Corresponds to an element of the exception_table array of + * a Code attribute, as defined in the Java Virtual Machine Specification (JVMS). Handler instances + * can be chained together, with their {@link #nextHandler} field, to describe a full JVMS + * exception_table array. + * + * @see JVMS + * 4.7.3 + * @author Eric Bruneton + */ +final class Handler { + + /** + * The start_pc field of this JVMS exception_table entry. Corresponds to the beginning of the + * exception handler's scope (inclusive). + */ + final Label startPc; + + /** + * The end_pc field of this JVMS exception_table entry. Corresponds to the end of the exception + * handler's scope (exclusive). + */ + final Label endPc; + + /** + * The handler_pc field of this JVMS exception_table entry. Corresponding to the beginning of the + * exception handler's code. + */ + final Label handlerPc; + + /** + * The catch_type field of this JVMS exception_table entry. This is the constant pool index of the + * internal name of the type of exceptions handled by this handler, or 0 to catch any exceptions. + */ + final int catchType; + + /** + * The internal name of the type of exceptions handled by this handler, or {@literal null} to + * catch any exceptions. + */ + final String catchTypeDescriptor; + + /** The next exception handler. */ + Handler nextHandler; + + /** + * Constructs a new Handler. + * + * @param startPc the start_pc field of this JVMS exception_table entry. + * @param endPc the end_pc field of this JVMS exception_table entry. + * @param handlerPc the handler_pc field of this JVMS exception_table entry. + * @param catchType The catch_type field of this JVMS exception_table entry. + * @param catchTypeDescriptor The internal name of the type of exceptions handled by this handler, + * or {@literal null} to catch any exceptions. + */ + Handler( + final Label startPc, + final Label endPc, + final Label handlerPc, + final int catchType, + final String catchTypeDescriptor) { + this.startPc = startPc; + this.endPc = endPc; + this.handlerPc = handlerPc; + this.catchType = catchType; + this.catchTypeDescriptor = catchTypeDescriptor; + } + + /** + * Constructs a new Handler from the given one, with a different scope. + * + * @param handler an existing Handler. + * @param startPc the start_pc field of this JVMS exception_table entry. + * @param endPc the end_pc field of this JVMS exception_table entry. + */ + Handler(final Handler handler, final Label startPc, final Label endPc) { + this(startPc, endPc, handler.handlerPc, handler.catchType, handler.catchTypeDescriptor); + this.nextHandler = handler.nextHandler; + } + + /** + * Removes the range between start and end from the Handler list that begins with the given + * element. + * + * @param firstHandler the beginning of a Handler list. May be {@literal null}. + * @param start the start of the range to be removed. + * @param end the end of the range to be removed. Maybe {@literal null}. + * @return the exception handler list with the start-end range removed. + */ + static Handler removeRange(final Handler firstHandler, final Label start, final Label end) { + if (firstHandler == null) { + return null; + } else { + firstHandler.nextHandler = removeRange(firstHandler.nextHandler, start, end); + } + int handlerStart = firstHandler.startPc.bytecodeOffset; + int handlerEnd = firstHandler.endPc.bytecodeOffset; + int rangeStart = start.bytecodeOffset; + int rangeEnd = end == null ? Integer.MAX_VALUE : end.bytecodeOffset; + // Return early if [handlerStart,handlerEnd[ and [rangeStart,rangeEnd[ don't intersect. + if (rangeStart >= handlerEnd || rangeEnd <= handlerStart) { + return firstHandler; + } + if (rangeStart <= handlerStart) { + if (rangeEnd >= handlerEnd) { + // If [handlerStart,handlerEnd[ is included in [rangeStart,rangeEnd[, remove firstHandler. + return firstHandler.nextHandler; + } else { + // [handlerStart,handlerEnd[ - [rangeStart,rangeEnd[ = [rangeEnd,handlerEnd[ + return new Handler(firstHandler, end, firstHandler.endPc); + } + } else if (rangeEnd >= handlerEnd) { + // [handlerStart,handlerEnd[ - [rangeStart,rangeEnd[ = [handlerStart,rangeStart[ + return new Handler(firstHandler, firstHandler.startPc, start); + } else { + // [handlerStart,handlerEnd[ - [rangeStart,rangeEnd[ = + // [handlerStart,rangeStart[ + [rangeEnd,handerEnd[ + firstHandler.nextHandler = new Handler(firstHandler, end, firstHandler.endPc); + return new Handler(firstHandler, firstHandler.startPc, start); + } + } + + /** + * Returns the number of elements of the Handler list that begins with the given element. + * + * @param firstHandler the beginning of a Handler list. May be {@literal null}. + * @return the number of elements of the Handler list that begins with 'handler'. + */ + static int getExceptionTableLength(final Handler firstHandler) { + int length = 0; + Handler handler = firstHandler; + while (handler != null) { + length++; + handler = handler.nextHandler; + } + return length; + } + + /** + * Returns the size in bytes of the JVMS exception_table corresponding to the Handler list that + * begins with the given element. This includes the exception_table_length field. + * + * @param firstHandler the beginning of a Handler list. May be {@literal null}. + * @return the size in bytes of the exception_table_length and exception_table structures. + */ + static int getExceptionTableSize(final Handler firstHandler) { + return 2 + 8 * getExceptionTableLength(firstHandler); + } + + /** + * Puts the JVMS exception_table corresponding to the Handler list that begins with the given + * element. This includes the exception_table_length field. + * + * @param firstHandler the beginning of a Handler list. May be {@literal null}. + * @param output where the exception_table_length and exception_table structures must be put. + */ + static void putExceptionTable(final Handler firstHandler, final ByteVector output) { + output.putShort(getExceptionTableLength(firstHandler)); + Handler handler = firstHandler; + while (handler != null) { + output + .putShort(handler.startPc.bytecodeOffset) + .putShort(handler.endPc.bytecodeOffset) + .putShort(handler.handlerPc.bytecodeOffset) + .putShort(handler.catchType); + handler = handler.nextHandler; + } + } +} diff --git a/spring-core/src/main/java/org/springframework/asm/Label.java b/spring-core/src/main/java/org/springframework/asm/Label.java new file mode 100644 index 0000000..e9e2f9e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/Label.java @@ -0,0 +1,622 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +package org.springframework.asm; + +/** + * A position in the bytecode of a method. Labels are used for jump, goto, and switch instructions, + * and for try catch blocks. A label designates the instruction that is just after. Note + * however that there can be other elements between a label and the instruction it designates (such + * as other labels, stack map frames, line numbers, etc.). + * + * @author Eric Bruneton + */ +public class Label { + + /** + * A flag indicating that a label is only used for debug attributes. Such a label is not the start + * of a basic block, the target of a jump instruction, or an exception handler. It can be safely + * ignored in control flow graph analysis algorithms (for optimization purposes). + */ + static final int FLAG_DEBUG_ONLY = 1; + + /** + * A flag indicating that a label is the target of a jump instruction, or the start of an + * exception handler. + */ + static final int FLAG_JUMP_TARGET = 2; + + /** A flag indicating that the bytecode offset of a label is known. */ + static final int FLAG_RESOLVED = 4; + + /** A flag indicating that a label corresponds to a reachable basic block. */ + static final int FLAG_REACHABLE = 8; + + /** + * A flag indicating that the basic block corresponding to a label ends with a subroutine call. By + * construction in {@link MethodWriter#visitJumpInsn}, labels with this flag set have at least two + * outgoing edges: + * + *
      + *
    • the first one corresponds to the instruction that follows the jsr instruction in the + * bytecode, i.e. where execution continues when it returns from the jsr call. This is a + * virtual control flow edge, since execution never goes directly from the jsr to the next + * instruction. Instead, it goes to the subroutine and eventually returns to the instruction + * following the jsr. This virtual edge is used to compute the real outgoing edges of the + * basic blocks ending with a ret instruction, in {@link #addSubroutineRetSuccessors}. + *
    • the second one corresponds to the target of the jsr instruction, + *
    + */ + static final int FLAG_SUBROUTINE_CALLER = 16; + + /** + * A flag indicating that the basic block corresponding to a label is the start of a subroutine. + */ + static final int FLAG_SUBROUTINE_START = 32; + + /** A flag indicating that the basic block corresponding to a label is the end of a subroutine. */ + static final int FLAG_SUBROUTINE_END = 64; + + /** + * The number of elements to add to the {@link #otherLineNumbers} array when it needs to be + * resized to store a new source line number. + */ + static final int LINE_NUMBERS_CAPACITY_INCREMENT = 4; + + /** + * The number of elements to add to the {@link #forwardReferences} array when it needs to be + * resized to store a new forward reference. + */ + static final int FORWARD_REFERENCES_CAPACITY_INCREMENT = 6; + + /** + * The bit mask to extract the type of a forward reference to this label. The extracted type is + * either {@link #FORWARD_REFERENCE_TYPE_SHORT} or {@link #FORWARD_REFERENCE_TYPE_WIDE}. + * + * @see #forwardReferences + */ + static final int FORWARD_REFERENCE_TYPE_MASK = 0xF0000000; + + /** + * The type of forward references stored with two bytes in the bytecode. This is the case, for + * instance, of a forward reference from an ifnull instruction. + */ + static final int FORWARD_REFERENCE_TYPE_SHORT = 0x10000000; + + /** + * The type of forward references stored in four bytes in the bytecode. This is the case, for + * instance, of a forward reference from a lookupswitch instruction. + */ + static final int FORWARD_REFERENCE_TYPE_WIDE = 0x20000000; + + /** + * The bit mask to extract the 'handle' of a forward reference to this label. The extracted handle + * is the bytecode offset where the forward reference value is stored (using either 2 or 4 bytes, + * as indicated by the {@link #FORWARD_REFERENCE_TYPE_MASK}). + * + * @see #forwardReferences + */ + static final int FORWARD_REFERENCE_HANDLE_MASK = 0x0FFFFFFF; + + /** + * A sentinel element used to indicate the end of a list of labels. + * + * @see #nextListElement + */ + static final Label EMPTY_LIST = new Label(); + + /** + * A user managed state associated with this label. Warning: this field is used by the ASM tree + * package. In order to use it with the ASM tree package you must override the getLabelNode method + * in MethodNode. + */ + public Object info; + + /** + * The type and status of this label or its corresponding basic block. Must be zero or more of + * {@link #FLAG_DEBUG_ONLY}, {@link #FLAG_JUMP_TARGET}, {@link #FLAG_RESOLVED}, {@link + * #FLAG_REACHABLE}, {@link #FLAG_SUBROUTINE_CALLER}, {@link #FLAG_SUBROUTINE_START}, {@link + * #FLAG_SUBROUTINE_END}. + */ + short flags; + + /** + * The source line number corresponding to this label, or 0. If there are several source line + * numbers corresponding to this label, the first one is stored in this field, and the remaining + * ones are stored in {@link #otherLineNumbers}. + */ + private short lineNumber; + + /** + * The source line numbers corresponding to this label, in addition to {@link #lineNumber}, or + * null. The first element of this array is the number n of source line numbers it contains, which + * are stored between indices 1 and n (inclusive). + */ + private int[] otherLineNumbers; + + /** + * The offset of this label in the bytecode of its method, in bytes. This value is set if and only + * if the {@link #FLAG_RESOLVED} flag is set. + */ + int bytecodeOffset; + + /** + * The forward references to this label. The first element is the number of forward references, + * times 2 (this corresponds to the index of the last element actually used in this array). Then, + * each forward reference is described with two consecutive integers noted + * 'sourceInsnBytecodeOffset' and 'reference': + * + *
      + *
    • 'sourceInsnBytecodeOffset' is the bytecode offset of the instruction that contains the + * forward reference, + *
    • 'reference' contains the type and the offset in the bytecode where the forward reference + * value must be stored, which can be extracted with {@link #FORWARD_REFERENCE_TYPE_MASK} + * and {@link #FORWARD_REFERENCE_HANDLE_MASK}. + *
    + * + *

    For instance, for an ifnull instruction at bytecode offset x, 'sourceInsnBytecodeOffset' is + * equal to x, and 'reference' is of type {@link #FORWARD_REFERENCE_TYPE_SHORT} with value x + 1 + * (because the ifnull instruction uses a 2 bytes bytecode offset operand stored one byte after + * the start of the instruction itself). For the default case of a lookupswitch instruction at + * bytecode offset x, 'sourceInsnBytecodeOffset' is equal to x, and 'reference' is of type {@link + * #FORWARD_REFERENCE_TYPE_WIDE} with value between x + 1 and x + 4 (because the lookupswitch + * instruction uses a 4 bytes bytecode offset operand stored one to four bytes after the start of + * the instruction itself). + */ + private int[] forwardReferences; + + // ----------------------------------------------------------------------------------------------- + + // Fields for the control flow and data flow graph analysis algorithms (used to compute the + // maximum stack size or the stack map frames). A control flow graph contains one node per "basic + // block", and one edge per "jump" from one basic block to another. Each node (i.e., each basic + // block) is represented with the Label object that corresponds to the first instruction of this + // basic block. Each node also stores the list of its successors in the graph, as a linked list of + // Edge objects. + // + // The control flow analysis algorithms used to compute the maximum stack size or the stack map + // frames are similar and use two steps. The first step, during the visit of each instruction, + // builds information about the state of the local variables and the operand stack at the end of + // each basic block, called the "output frame", relatively to the frame state at the + // beginning of the basic block, which is called the "input frame", and which is unknown + // during this step. The second step, in {@link MethodWriter#computeAllFrames} and {@link + // MethodWriter#computeMaxStackAndLocal}, is a fix point algorithm + // that computes information about the input frame of each basic block, from the input state of + // the first basic block (known from the method signature), and by the using the previously + // computed relative output frames. + // + // The algorithm used to compute the maximum stack size only computes the relative output and + // absolute input stack heights, while the algorithm used to compute stack map frames computes + // relative output frames and absolute input frames. + + /** + * The number of elements in the input stack of the basic block corresponding to this label. This + * field is computed in {@link MethodWriter#computeMaxStackAndLocal}. + */ + short inputStackSize; + + /** + * The number of elements in the output stack, at the end of the basic block corresponding to this + * label. This field is only computed for basic blocks that end with a RET instruction. + */ + short outputStackSize; + + /** + * The maximum height reached by the output stack, relatively to the top of the input stack, in + * the basic block corresponding to this label. This maximum is always positive or {@literal + * null}. + */ + short outputStackMax; + + /** + * The id of the subroutine to which this basic block belongs, or 0. If the basic block belongs to + * several subroutines, this is the id of the "oldest" subroutine that contains it (with the + * convention that a subroutine calling another one is "older" than the callee). This field is + * computed in {@link MethodWriter#computeMaxStackAndLocal}, if the method contains JSR + * instructions. + */ + short subroutineId; + + /** + * The input and output stack map frames of the basic block corresponding to this label. This + * field is only used when the {@link MethodWriter#COMPUTE_ALL_FRAMES} or {@link + * MethodWriter#COMPUTE_INSERTED_FRAMES} option is used. + */ + Frame frame; + + /** + * The successor of this label, in the order they are visited in {@link MethodVisitor#visitLabel}. + * This linked list does not include labels used for debug info only. If the {@link + * MethodWriter#COMPUTE_ALL_FRAMES} or {@link MethodWriter#COMPUTE_INSERTED_FRAMES} option is used + * then it does not contain either successive labels that denote the same bytecode offset (in this + * case only the first label appears in this list). + */ + Label nextBasicBlock; + + /** + * The outgoing edges of the basic block corresponding to this label, in the control flow graph of + * its method. These edges are stored in a linked list of {@link Edge} objects, linked to each + * other by their {@link Edge#nextEdge} field. + */ + Edge outgoingEdges; + + /** + * The next element in the list of labels to which this label belongs, or {@literal null} if it + * does not belong to any list. All lists of labels must end with the {@link #EMPTY_LIST} + * sentinel, in order to ensure that this field is null if and only if this label does not belong + * to a list of labels. Note that there can be several lists of labels at the same time, but that + * a label can belong to at most one list at a time (unless some lists share a common tail, but + * this is not used in practice). + * + *

    List of labels are used in {@link MethodWriter#computeAllFrames} and {@link + * MethodWriter#computeMaxStackAndLocal} to compute stack map frames and the maximum stack size, + * respectively, as well as in {@link #markSubroutine} and {@link #addSubroutineRetSuccessors} to + * compute the basic blocks belonging to subroutines and their outgoing edges. Outside of these + * methods, this field should be null (this property is a precondition and a postcondition of + * these methods). + */ + Label nextListElement; + + // ----------------------------------------------------------------------------------------------- + // Constructor and accessors + // ----------------------------------------------------------------------------------------------- + + /** Constructs a new label. */ + public Label() { + // Nothing to do. + } + + /** + * Returns the bytecode offset corresponding to this label. This offset is computed from the start + * of the method's bytecode. This method is intended for {@link Attribute} sub classes, and is + * normally not needed by class generators or adapters. + * + * @return the bytecode offset corresponding to this label. + * @throws IllegalStateException if this label is not resolved yet. + */ + public int getOffset() { + if ((flags & FLAG_RESOLVED) == 0) { + throw new IllegalStateException("Label offset position has not been resolved yet"); + } + return bytecodeOffset; + } + + /** + * Returns the "canonical" {@link Label} instance corresponding to this label's bytecode offset, + * if known, otherwise the label itself. The canonical instance is the first label (in the order + * of their visit by {@link MethodVisitor#visitLabel}) corresponding to this bytecode offset. It + * cannot be known for labels which have not been visited yet. + * + *

    This method should only be used when the {@link MethodWriter#COMPUTE_ALL_FRAMES} option + * is used. + * + * @return the label itself if {@link #frame} is null, otherwise the Label's frame owner. This + * corresponds to the "canonical" label instance described above thanks to the way the label + * frame is set in {@link MethodWriter#visitLabel}. + */ + final Label getCanonicalInstance() { + return frame == null ? this : frame.owner; + } + + // ----------------------------------------------------------------------------------------------- + // Methods to manage line numbers + // ----------------------------------------------------------------------------------------------- + + /** + * Adds a source line number corresponding to this label. + * + * @param lineNumber a source line number (which should be strictly positive). + */ + final void addLineNumber(final int lineNumber) { + if (this.lineNumber == 0) { + this.lineNumber = (short) lineNumber; + } else { + if (otherLineNumbers == null) { + otherLineNumbers = new int[LINE_NUMBERS_CAPACITY_INCREMENT]; + } + int otherLineNumberIndex = ++otherLineNumbers[0]; + if (otherLineNumberIndex >= otherLineNumbers.length) { + int[] newLineNumbers = new int[otherLineNumbers.length + LINE_NUMBERS_CAPACITY_INCREMENT]; + System.arraycopy(otherLineNumbers, 0, newLineNumbers, 0, otherLineNumbers.length); + otherLineNumbers = newLineNumbers; + } + otherLineNumbers[otherLineNumberIndex] = lineNumber; + } + } + + /** + * Makes the given visitor visit this label and its source line numbers, if applicable. + * + * @param methodVisitor a method visitor. + * @param visitLineNumbers whether to visit of the label's source line numbers, if any. + */ + final void accept(final MethodVisitor methodVisitor, final boolean visitLineNumbers) { + methodVisitor.visitLabel(this); + if (visitLineNumbers && lineNumber != 0) { + methodVisitor.visitLineNumber(lineNumber & 0xFFFF, this); + if (otherLineNumbers != null) { + for (int i = 1; i <= otherLineNumbers[0]; ++i) { + methodVisitor.visitLineNumber(otherLineNumbers[i], this); + } + } + } + } + + // ----------------------------------------------------------------------------------------------- + // Methods to compute offsets and to manage forward references + // ----------------------------------------------------------------------------------------------- + + /** + * Puts a reference to this label in the bytecode of a method. If the bytecode offset of the label + * is known, the relative bytecode offset between the label and the instruction referencing it is + * computed and written directly. Otherwise, a null relative offset is written and a new forward + * reference is declared for this label. + * + * @param code the bytecode of the method. This is where the reference is appended. + * @param sourceInsnBytecodeOffset the bytecode offset of the instruction that contains the + * reference to be appended. + * @param wideReference whether the reference must be stored in 4 bytes (instead of 2 bytes). + */ + final void put( + final ByteVector code, final int sourceInsnBytecodeOffset, final boolean wideReference) { + if ((flags & FLAG_RESOLVED) == 0) { + if (wideReference) { + addForwardReference(sourceInsnBytecodeOffset, FORWARD_REFERENCE_TYPE_WIDE, code.length); + code.putInt(-1); + } else { + addForwardReference(sourceInsnBytecodeOffset, FORWARD_REFERENCE_TYPE_SHORT, code.length); + code.putShort(-1); + } + } else { + if (wideReference) { + code.putInt(bytecodeOffset - sourceInsnBytecodeOffset); + } else { + code.putShort(bytecodeOffset - sourceInsnBytecodeOffset); + } + } + } + + /** + * Adds a forward reference to this label. This method must be called only for a true forward + * reference, i.e. only if this label is not resolved yet. For backward references, the relative + * bytecode offset of the reference can be, and must be, computed and stored directly. + * + * @param sourceInsnBytecodeOffset the bytecode offset of the instruction that contains the + * reference stored at referenceHandle. + * @param referenceType either {@link #FORWARD_REFERENCE_TYPE_SHORT} or {@link + * #FORWARD_REFERENCE_TYPE_WIDE}. + * @param referenceHandle the offset in the bytecode where the forward reference value must be + * stored. + */ + private void addForwardReference( + final int sourceInsnBytecodeOffset, final int referenceType, final int referenceHandle) { + if (forwardReferences == null) { + forwardReferences = new int[FORWARD_REFERENCES_CAPACITY_INCREMENT]; + } + int lastElementIndex = forwardReferences[0]; + if (lastElementIndex + 2 >= forwardReferences.length) { + int[] newValues = new int[forwardReferences.length + FORWARD_REFERENCES_CAPACITY_INCREMENT]; + System.arraycopy(forwardReferences, 0, newValues, 0, forwardReferences.length); + forwardReferences = newValues; + } + forwardReferences[++lastElementIndex] = sourceInsnBytecodeOffset; + forwardReferences[++lastElementIndex] = referenceType | referenceHandle; + forwardReferences[0] = lastElementIndex; + } + + /** + * Sets the bytecode offset of this label to the given value and resolves the forward references + * to this label, if any. This method must be called when this label is added to the bytecode of + * the method, i.e. when its bytecode offset becomes known. This method fills in the blanks that + * where left in the bytecode by each forward reference previously added to this label. + * + * @param code the bytecode of the method. + * @param bytecodeOffset the bytecode offset of this label. + * @return {@literal true} if a blank that was left for this label was too small to store the + * offset. In such a case the corresponding jump instruction is replaced with an equivalent + * ASM specific instruction using an unsigned two bytes offset. These ASM specific + * instructions are later replaced with standard bytecode instructions with wider offsets (4 + * bytes instead of 2), in ClassReader. + */ + final boolean resolve(final byte[] code, final int bytecodeOffset) { + this.flags |= FLAG_RESOLVED; + this.bytecodeOffset = bytecodeOffset; + if (forwardReferences == null) { + return false; + } + boolean hasAsmInstructions = false; + for (int i = forwardReferences[0]; i > 0; i -= 2) { + final int sourceInsnBytecodeOffset = forwardReferences[i - 1]; + final int reference = forwardReferences[i]; + final int relativeOffset = bytecodeOffset - sourceInsnBytecodeOffset; + int handle = reference & FORWARD_REFERENCE_HANDLE_MASK; + if ((reference & FORWARD_REFERENCE_TYPE_MASK) == FORWARD_REFERENCE_TYPE_SHORT) { + if (relativeOffset < Short.MIN_VALUE || relativeOffset > Short.MAX_VALUE) { + // Change the opcode of the jump instruction, in order to be able to find it later in + // ClassReader. These ASM specific opcodes are similar to jump instruction opcodes, except + // that the 2 bytes offset is unsigned (and can therefore represent values from 0 to + // 65535, which is sufficient since the size of a method is limited to 65535 bytes). + int opcode = code[sourceInsnBytecodeOffset] & 0xFF; + if (opcode < Opcodes.IFNULL) { + // Change IFEQ ... JSR to ASM_IFEQ ... ASM_JSR. + code[sourceInsnBytecodeOffset] = (byte) (opcode + Constants.ASM_OPCODE_DELTA); + } else { + // Change IFNULL and IFNONNULL to ASM_IFNULL and ASM_IFNONNULL. + code[sourceInsnBytecodeOffset] = (byte) (opcode + Constants.ASM_IFNULL_OPCODE_DELTA); + } + hasAsmInstructions = true; + } + code[handle++] = (byte) (relativeOffset >>> 8); + code[handle] = (byte) relativeOffset; + } else { + code[handle++] = (byte) (relativeOffset >>> 24); + code[handle++] = (byte) (relativeOffset >>> 16); + code[handle++] = (byte) (relativeOffset >>> 8); + code[handle] = (byte) relativeOffset; + } + } + return hasAsmInstructions; + } + + // ----------------------------------------------------------------------------------------------- + // Methods related to subroutines + // ----------------------------------------------------------------------------------------------- + + /** + * Finds the basic blocks that belong to the subroutine starting with the basic block + * corresponding to this label, and marks these blocks as belonging to this subroutine. This + * method follows the control flow graph to find all the blocks that are reachable from the + * current basic block WITHOUT following any jsr target. + * + *

    Note: a precondition and postcondition of this method is that all labels must have a null + * {@link #nextListElement}. + * + * @param subroutineId the id of the subroutine starting with the basic block corresponding to + * this label. + */ + final void markSubroutine(final short subroutineId) { + // Data flow algorithm: put this basic block in a list of blocks to process (which are blocks + // belonging to subroutine subroutineId) and, while there are blocks to process, remove one from + // the list, mark it as belonging to the subroutine, and add its successor basic blocks in the + // control flow graph to the list of blocks to process (if not already done). + Label listOfBlocksToProcess = this; + listOfBlocksToProcess.nextListElement = EMPTY_LIST; + while (listOfBlocksToProcess != EMPTY_LIST) { + // Remove a basic block from the list of blocks to process. + Label basicBlock = listOfBlocksToProcess; + listOfBlocksToProcess = listOfBlocksToProcess.nextListElement; + basicBlock.nextListElement = null; + + // If it is not already marked as belonging to a subroutine, mark it as belonging to + // subroutineId and add its successors to the list of blocks to process (unless already done). + if (basicBlock.subroutineId == 0) { + basicBlock.subroutineId = subroutineId; + listOfBlocksToProcess = basicBlock.pushSuccessors(listOfBlocksToProcess); + } + } + } + + /** + * Finds the basic blocks that end a subroutine starting with the basic block corresponding to + * this label and, for each one of them, adds an outgoing edge to the basic block following the + * given subroutine call. In other words, completes the control flow graph by adding the edges + * corresponding to the return from this subroutine, when called from the given caller basic + * block. + * + *

    Note: a precondition and postcondition of this method is that all labels must have a null + * {@link #nextListElement}. + * + * @param subroutineCaller a basic block that ends with a jsr to the basic block corresponding to + * this label. This label is supposed to correspond to the start of a subroutine. + */ + final void addSubroutineRetSuccessors(final Label subroutineCaller) { + // Data flow algorithm: put this basic block in a list blocks to process (which are blocks + // belonging to a subroutine starting with this label) and, while there are blocks to process, + // remove one from the list, put it in a list of blocks that have been processed, add a return + // edge to the successor of subroutineCaller if applicable, and add its successor basic blocks + // in the control flow graph to the list of blocks to process (if not already done). + Label listOfProcessedBlocks = EMPTY_LIST; + Label listOfBlocksToProcess = this; + listOfBlocksToProcess.nextListElement = EMPTY_LIST; + while (listOfBlocksToProcess != EMPTY_LIST) { + // Move a basic block from the list of blocks to process to the list of processed blocks. + Label basicBlock = listOfBlocksToProcess; + listOfBlocksToProcess = basicBlock.nextListElement; + basicBlock.nextListElement = listOfProcessedBlocks; + listOfProcessedBlocks = basicBlock; + + // Add an edge from this block to the successor of the caller basic block, if this block is + // the end of a subroutine and if this block and subroutineCaller do not belong to the same + // subroutine. + if ((basicBlock.flags & FLAG_SUBROUTINE_END) != 0 + && basicBlock.subroutineId != subroutineCaller.subroutineId) { + basicBlock.outgoingEdges = + new Edge( + basicBlock.outputStackSize, + // By construction, the first outgoing edge of a basic block that ends with a jsr + // instruction leads to the jsr continuation block, i.e. where execution continues + // when ret is called (see {@link #FLAG_SUBROUTINE_CALLER}). + subroutineCaller.outgoingEdges.successor, + basicBlock.outgoingEdges); + } + // Add its successors to the list of blocks to process. Note that {@link #pushSuccessors} does + // not push basic blocks which are already in a list. Here this means either in the list of + // blocks to process, or in the list of already processed blocks. This second list is + // important to make sure we don't reprocess an already processed block. + listOfBlocksToProcess = basicBlock.pushSuccessors(listOfBlocksToProcess); + } + // Reset the {@link #nextListElement} of all the basic blocks that have been processed to null, + // so that this method can be called again with a different subroutine or subroutine caller. + while (listOfProcessedBlocks != EMPTY_LIST) { + Label newListOfProcessedBlocks = listOfProcessedBlocks.nextListElement; + listOfProcessedBlocks.nextListElement = null; + listOfProcessedBlocks = newListOfProcessedBlocks; + } + } + + /** + * Adds the successors of this label in the method's control flow graph (except those + * corresponding to a jsr target, and those already in a list of labels) to the given list of + * blocks to process, and returns the new list. + * + * @param listOfLabelsToProcess a list of basic blocks to process, linked together with their + * {@link #nextListElement} field. + * @return the new list of blocks to process. + */ + private Label pushSuccessors(final Label listOfLabelsToProcess) { + Label newListOfLabelsToProcess = listOfLabelsToProcess; + Edge outgoingEdge = outgoingEdges; + while (outgoingEdge != null) { + // By construction, the second outgoing edge of a basic block that ends with a jsr instruction + // leads to the jsr target (see {@link #FLAG_SUBROUTINE_CALLER}). + boolean isJsrTarget = + (flags & Label.FLAG_SUBROUTINE_CALLER) != 0 && outgoingEdge == outgoingEdges.nextEdge; + if (!isJsrTarget && outgoingEdge.successor.nextListElement == null) { + // Add this successor to the list of blocks to process, if it does not already belong to a + // list of labels. + outgoingEdge.successor.nextListElement = newListOfLabelsToProcess; + newListOfLabelsToProcess = outgoingEdge.successor; + } + outgoingEdge = outgoingEdge.nextEdge; + } + return newListOfLabelsToProcess; + } + + // ----------------------------------------------------------------------------------------------- + // Overridden Object methods + // ----------------------------------------------------------------------------------------------- + + /** + * Returns a string representation of this label. + * + * @return a string representation of this label. + */ + @Override + public String toString() { + return "L" + System.identityHashCode(this); + } +} diff --git a/spring-core/src/main/java/org/springframework/asm/MethodTooLargeException.java b/spring-core/src/main/java/org/springframework/asm/MethodTooLargeException.java new file mode 100644 index 0000000..ac263c4 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/MethodTooLargeException.java @@ -0,0 +1,99 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +package org.springframework.asm; + +/** + * Exception thrown when the Code attribute of a method produced by a {@link ClassWriter} is too + * large. + * + * @author Jason Zaugg + */ +public final class MethodTooLargeException extends IndexOutOfBoundsException { + private static final long serialVersionUID = 6807380416709738314L; + + private final String className; + private final String methodName; + private final String descriptor; + private final int codeSize; + + /** + * Constructs a new {@link MethodTooLargeException}. + * + * @param className the internal name of the owner class. + * @param methodName the name of the method. + * @param descriptor the descriptor of the method. + * @param codeSize the size of the method's Code attribute, in bytes. + */ + public MethodTooLargeException( + final String className, + final String methodName, + final String descriptor, + final int codeSize) { + super("Method too large: " + className + "." + methodName + " " + descriptor); + this.className = className; + this.methodName = methodName; + this.descriptor = descriptor; + this.codeSize = codeSize; + } + + /** + * Returns the internal name of the owner class. + * + * @return the internal name of the owner class. + */ + public String getClassName() { + return className; + } + + /** + * Returns the name of the method. + * + * @return the name of the method. + */ + public String getMethodName() { + return methodName; + } + + /** + * Returns the descriptor of the method. + * + * @return the descriptor of the method. + */ + public String getDescriptor() { + return descriptor; + } + + /** + * Returns the size of the method's Code attribute, in bytes. + * + * @return the size of the method's Code attribute, in bytes. + */ + public int getCodeSize() { + return codeSize; + } +} diff --git a/spring-core/src/main/java/org/springframework/asm/MethodVisitor.java b/spring-core/src/main/java/org/springframework/asm/MethodVisitor.java new file mode 100644 index 0000000..57eb209 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/MethodVisitor.java @@ -0,0 +1,784 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +package org.springframework.asm; + +/** + * A visitor to visit a Java method. The methods of this class must be called in the following + * order: ( {@code visitParameter} )* [ {@code visitAnnotationDefault} ] ( {@code visitAnnotation} | + * {@code visitAnnotableParameterCount} | {@code visitParameterAnnotation} {@code + * visitTypeAnnotation} | {@code visitAttribute} )* [ {@code visitCode} ( {@code visitFrame} | + * {@code visitXInsn} | {@code visitLabel} | {@code visitInsnAnnotation} | {@code + * visitTryCatchBlock} | {@code visitTryCatchAnnotation} | {@code visitLocalVariable} | {@code + * visitLocalVariableAnnotation} | {@code visitLineNumber} )* {@code visitMaxs} ] {@code visitEnd}. + * In addition, the {@code visitXInsn} and {@code visitLabel} methods must be called in the + * sequential order of the bytecode instructions of the visited code, {@code visitInsnAnnotation} + * must be called after the annotated instruction, {@code visitTryCatchBlock} must be called + * before the labels passed as arguments have been visited, {@code + * visitTryCatchBlockAnnotation} must be called after the corresponding try catch block has + * been visited, and the {@code visitLocalVariable}, {@code visitLocalVariableAnnotation} and {@code + * visitLineNumber} methods must be called after the labels passed as arguments have been + * visited. + * + * @author Eric Bruneton + */ +public abstract class MethodVisitor { + + private static final String REQUIRES_ASM5 = "This feature requires ASM5"; + + /** + * The ASM API version implemented by this visitor. The value of this field must be one of {@link + * Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6} or {@link Opcodes#ASM7}. + */ + protected final int api; + + /** + * The method visitor to which this visitor must delegate method calls. May be {@literal null}. + */ + protected MethodVisitor mv; + + /** + * Constructs a new {@link MethodVisitor}. + * + * @param api the ASM API version implemented by this visitor. Must be one of {@link + * Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6} or {@link Opcodes#ASM7}. + */ + public MethodVisitor(final int api) { + this(api, null); + } + + /** + * Constructs a new {@link MethodVisitor}. + * + * @param api the ASM API version implemented by this visitor. Must be one of {@link + * Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6} or {@link Opcodes#ASM7}. + * @param methodVisitor the method visitor to which this visitor must delegate method calls. May + * be null. + */ + public MethodVisitor(final int api, final MethodVisitor methodVisitor) { + if (api != Opcodes.ASM9 + && api != Opcodes.ASM8 + && api != Opcodes.ASM7 + && api != Opcodes.ASM6 + && api != Opcodes.ASM5 + && api != Opcodes.ASM4 + && api != Opcodes.ASM10_EXPERIMENTAL) { + throw new IllegalArgumentException("Unsupported api " + api); + } + // SPRING PATCH: no preview mode check for ASM 9 experimental + this.api = api; + this.mv = methodVisitor; + } + + // ----------------------------------------------------------------------------------------------- + // Parameters, annotations and non standard attributes + // ----------------------------------------------------------------------------------------------- + + /** + * Visits a parameter of this method. + * + * @param name parameter name or {@literal null} if none is provided. + * @param access the parameter's access flags, only {@code ACC_FINAL}, {@code ACC_SYNTHETIC} + * or/and {@code ACC_MANDATED} are allowed (see {@link Opcodes}). + */ + public void visitParameter(final String name, final int access) { + if (api < Opcodes.ASM5) { + throw new UnsupportedOperationException(REQUIRES_ASM5); + } + if (mv != null) { + mv.visitParameter(name, access); + } + } + + /** + * Visits the default value of this annotation interface method. + * + * @return a visitor to the visit the actual default value of this annotation interface method, or + * {@literal null} if this visitor is not interested in visiting this default value. The + * 'name' parameters passed to the methods of this annotation visitor are ignored. Moreover, + * exacly one visit method must be called on this annotation visitor, followed by visitEnd. + */ + public AnnotationVisitor visitAnnotationDefault() { + if (mv != null) { + return mv.visitAnnotationDefault(); + } + return null; + } + + /** + * Visits an annotation of this method. + * + * @param descriptor the class descriptor of the annotation class. + * @param visible {@literal true} if the annotation is visible at runtime. + * @return a visitor to visit the annotation values, or {@literal null} if this visitor is not + * interested in visiting this annotation. + */ + public AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) { + if (mv != null) { + return mv.visitAnnotation(descriptor, visible); + } + return null; + } + + /** + * Visits an annotation on a type in the method signature. + * + * @param typeRef a reference to the annotated type. The sort of this type reference must be + * {@link TypeReference#METHOD_TYPE_PARAMETER}, {@link + * TypeReference#METHOD_TYPE_PARAMETER_BOUND}, {@link TypeReference#METHOD_RETURN}, {@link + * TypeReference#METHOD_RECEIVER}, {@link TypeReference#METHOD_FORMAL_PARAMETER} or {@link + * TypeReference#THROWS}. See {@link TypeReference}. + * @param typePath the path to the annotated type argument, wildcard bound, array element type, or + * static inner type within 'typeRef'. May be {@literal null} if the annotation targets + * 'typeRef' as a whole. + * @param descriptor the class descriptor of the annotation class. + * @param visible {@literal true} if the annotation is visible at runtime. + * @return a visitor to visit the annotation values, or {@literal null} if this visitor is not + * interested in visiting this annotation. + */ + public AnnotationVisitor visitTypeAnnotation( + final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) { + if (api < Opcodes.ASM5) { + throw new UnsupportedOperationException(REQUIRES_ASM5); + } + if (mv != null) { + return mv.visitTypeAnnotation(typeRef, typePath, descriptor, visible); + } + return null; + } + + /** + * Visits the number of method parameters that can have annotations. By default (i.e. when this + * method is not called), all the method parameters defined by the method descriptor can have + * annotations. + * + * @param parameterCount the number of method parameters than can have annotations. This number + * must be less or equal than the number of parameter types in the method descriptor. It can + * be strictly less when a method has synthetic parameters and when these parameters are + * ignored when computing parameter indices for the purpose of parameter annotations (see + * https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.7.18). + * @param visible {@literal true} to define the number of method parameters that can have + * annotations visible at runtime, {@literal false} to define the number of method parameters + * that can have annotations invisible at runtime. + */ + public void visitAnnotableParameterCount(final int parameterCount, final boolean visible) { + if (mv != null) { + mv.visitAnnotableParameterCount(parameterCount, visible); + } + } + + /** + * Visits an annotation of a parameter this method. + * + * @param parameter the parameter index. This index must be strictly smaller than the number of + * parameters in the method descriptor, and strictly smaller than the parameter count + * specified in {@link #visitAnnotableParameterCount}. Important note: a parameter index i + * is not required to correspond to the i'th parameter descriptor in the method + * descriptor, in particular in case of synthetic parameters (see + * https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.7.18). + * @param descriptor the class descriptor of the annotation class. + * @param visible {@literal true} if the annotation is visible at runtime. + * @return a visitor to visit the annotation values, or {@literal null} if this visitor is not + * interested in visiting this annotation. + */ + public AnnotationVisitor visitParameterAnnotation( + final int parameter, final String descriptor, final boolean visible) { + if (mv != null) { + return mv.visitParameterAnnotation(parameter, descriptor, visible); + } + return null; + } + + /** + * Visits a non standard attribute of this method. + * + * @param attribute an attribute. + */ + public void visitAttribute(final Attribute attribute) { + if (mv != null) { + mv.visitAttribute(attribute); + } + } + + /** Starts the visit of the method's code, if any (i.e. non abstract method). */ + public void visitCode() { + if (mv != null) { + mv.visitCode(); + } + } + + /** + * Visits the current state of the local variables and operand stack elements. This method must(*) + * be called just before any instruction i that follows an unconditional branch + * instruction such as GOTO or THROW, that is the target of a jump instruction, or that starts an + * exception handler block. The visited types must describe the values of the local variables and + * of the operand stack elements just before i is executed.
    + *
    + * (*) this is mandatory only for classes whose version is greater than or equal to {@link + * Opcodes#V1_6}.
    + *
    + * The frames of a method must be given either in expanded form, or in compressed form (all frames + * must use the same format, i.e. you must not mix expanded and compressed frames within a single + * method): + * + *

      + *
    • In expanded form, all frames must have the F_NEW type. + *
    • In compressed form, frames are basically "deltas" from the state of the previous frame: + *
        + *
      • {@link Opcodes#F_SAME} representing frame with exactly the same locals as the + * previous frame and with the empty stack. + *
      • {@link Opcodes#F_SAME1} representing frame with exactly the same locals as the + * previous frame and with single value on the stack ( numStack is 1 and + * stack[0] contains value for the type of the stack item). + *
      • {@link Opcodes#F_APPEND} representing frame with current locals are the same as the + * locals in the previous frame, except that additional locals are defined ( + * numLocal is 1, 2 or 3 and local elements contains values + * representing added types). + *
      • {@link Opcodes#F_CHOP} representing frame with current locals are the same as the + * locals in the previous frame, except that the last 1-3 locals are absent and with + * the empty stack (numLocal is 1, 2 or 3). + *
      • {@link Opcodes#F_FULL} representing complete frame data. + *
      + *
    + * + *
    + * In both cases the first frame, corresponding to the method's parameters and access flags, is + * implicit and must not be visited. Also, it is illegal to visit two or more frames for the same + * code location (i.e., at least one instruction must be visited between two calls to visitFrame). + * + * @param type the type of this stack map frame. Must be {@link Opcodes#F_NEW} for expanded + * frames, or {@link Opcodes#F_FULL}, {@link Opcodes#F_APPEND}, {@link Opcodes#F_CHOP}, {@link + * Opcodes#F_SAME} or {@link Opcodes#F_APPEND}, {@link Opcodes#F_SAME1} for compressed frames. + * @param numLocal the number of local variables in the visited frame. + * @param local the local variable types in this frame. This array must not be modified. Primitive + * types are represented by {@link Opcodes#TOP}, {@link Opcodes#INTEGER}, {@link + * Opcodes#FLOAT}, {@link Opcodes#LONG}, {@link Opcodes#DOUBLE}, {@link Opcodes#NULL} or + * {@link Opcodes#UNINITIALIZED_THIS} (long and double are represented by a single element). + * Reference types are represented by String objects (representing internal names), and + * uninitialized types by Label objects (this label designates the NEW instruction that + * created this uninitialized value). + * @param numStack the number of operand stack elements in the visited frame. + * @param stack the operand stack types in this frame. This array must not be modified. Its + * content has the same format as the "local" array. + * @throws IllegalStateException if a frame is visited just after another one, without any + * instruction between the two (unless this frame is a Opcodes#F_SAME frame, in which case it + * is silently ignored). + */ + public void visitFrame( + final int type, + final int numLocal, + final Object[] local, + final int numStack, + final Object[] stack) { + if (mv != null) { + mv.visitFrame(type, numLocal, local, numStack, stack); + } + } + + // ----------------------------------------------------------------------------------------------- + // Normal instructions + // ----------------------------------------------------------------------------------------------- + + /** + * Visits a zero operand instruction. + * + * @param opcode the opcode of the instruction to be visited. This opcode is either NOP, + * ACONST_NULL, ICONST_M1, ICONST_0, ICONST_1, ICONST_2, ICONST_3, ICONST_4, ICONST_5, + * LCONST_0, LCONST_1, FCONST_0, FCONST_1, FCONST_2, DCONST_0, DCONST_1, IALOAD, LALOAD, + * FALOAD, DALOAD, AALOAD, BALOAD, CALOAD, SALOAD, IASTORE, LASTORE, FASTORE, DASTORE, + * AASTORE, BASTORE, CASTORE, SASTORE, POP, POP2, DUP, DUP_X1, DUP_X2, DUP2, DUP2_X1, DUP2_X2, + * SWAP, IADD, LADD, FADD, DADD, ISUB, LSUB, FSUB, DSUB, IMUL, LMUL, FMUL, DMUL, IDIV, LDIV, + * FDIV, DDIV, IREM, LREM, FREM, DREM, INEG, LNEG, FNEG, DNEG, ISHL, LSHL, ISHR, LSHR, IUSHR, + * LUSHR, IAND, LAND, IOR, LOR, IXOR, LXOR, I2L, I2F, I2D, L2I, L2F, L2D, F2I, F2L, F2D, D2I, + * D2L, D2F, I2B, I2C, I2S, LCMP, FCMPL, FCMPG, DCMPL, DCMPG, IRETURN, LRETURN, FRETURN, + * DRETURN, ARETURN, RETURN, ARRAYLENGTH, ATHROW, MONITORENTER, or MONITOREXIT. + */ + public void visitInsn(final int opcode) { + if (mv != null) { + mv.visitInsn(opcode); + } + } + + /** + * Visits an instruction with a single int operand. + * + * @param opcode the opcode of the instruction to be visited. This opcode is either BIPUSH, SIPUSH + * or NEWARRAY. + * @param operand the operand of the instruction to be visited.
    + * When opcode is BIPUSH, operand value should be between Byte.MIN_VALUE and Byte.MAX_VALUE. + *
    + * When opcode is SIPUSH, operand value should be between Short.MIN_VALUE and Short.MAX_VALUE. + *
    + * When opcode is NEWARRAY, operand value should be one of {@link Opcodes#T_BOOLEAN}, {@link + * Opcodes#T_CHAR}, {@link Opcodes#T_FLOAT}, {@link Opcodes#T_DOUBLE}, {@link Opcodes#T_BYTE}, + * {@link Opcodes#T_SHORT}, {@link Opcodes#T_INT} or {@link Opcodes#T_LONG}. + */ + public void visitIntInsn(final int opcode, final int operand) { + if (mv != null) { + mv.visitIntInsn(opcode, operand); + } + } + + /** + * Visits a local variable instruction. A local variable instruction is an instruction that loads + * or stores the value of a local variable. + * + * @param opcode the opcode of the local variable instruction to be visited. This opcode is either + * ILOAD, LLOAD, FLOAD, DLOAD, ALOAD, ISTORE, LSTORE, FSTORE, DSTORE, ASTORE or RET. + * @param var the operand of the instruction to be visited. This operand is the index of a local + * variable. + */ + public void visitVarInsn(final int opcode, final int var) { + if (mv != null) { + mv.visitVarInsn(opcode, var); + } + } + + /** + * Visits a type instruction. A type instruction is an instruction that takes the internal name of + * a class as parameter. + * + * @param opcode the opcode of the type instruction to be visited. This opcode is either NEW, + * ANEWARRAY, CHECKCAST or INSTANCEOF. + * @param type the operand of the instruction to be visited. This operand must be the internal + * name of an object or array class (see {@link Type#getInternalName()}). + */ + public void visitTypeInsn(final int opcode, final String type) { + if (mv != null) { + mv.visitTypeInsn(opcode, type); + } + } + + /** + * Visits a field instruction. A field instruction is an instruction that loads or stores the + * value of a field of an object. + * + * @param opcode the opcode of the type instruction to be visited. This opcode is either + * GETSTATIC, PUTSTATIC, GETFIELD or PUTFIELD. + * @param owner the internal name of the field's owner class (see {@link Type#getInternalName()}). + * @param name the field's name. + * @param descriptor the field's descriptor (see {@link Type}). + */ + public void visitFieldInsn( + final int opcode, final String owner, final String name, final String descriptor) { + if (mv != null) { + mv.visitFieldInsn(opcode, owner, name, descriptor); + } + } + + /** + * Visits a method instruction. A method instruction is an instruction that invokes a method. + * + * @param opcode the opcode of the type instruction to be visited. This opcode is either + * INVOKEVIRTUAL, INVOKESPECIAL, INVOKESTATIC or INVOKEINTERFACE. + * @param owner the internal name of the method's owner class (see {@link + * Type#getInternalName()}). + * @param name the method's name. + * @param descriptor the method's descriptor (see {@link Type}). + * @deprecated use {@link #visitMethodInsn(int, String, String, String, boolean)} instead. + */ + @Deprecated + public void visitMethodInsn( + final int opcode, final String owner, final String name, final String descriptor) { + int opcodeAndSource = opcode | (api < Opcodes.ASM5 ? Opcodes.SOURCE_DEPRECATED : 0); + visitMethodInsn(opcodeAndSource, owner, name, descriptor, opcode == Opcodes.INVOKEINTERFACE); + } + + /** + * Visits a method instruction. A method instruction is an instruction that invokes a method. + * + * @param opcode the opcode of the type instruction to be visited. This opcode is either + * INVOKEVIRTUAL, INVOKESPECIAL, INVOKESTATIC or INVOKEINTERFACE. + * @param owner the internal name of the method's owner class (see {@link + * Type#getInternalName()}). + * @param name the method's name. + * @param descriptor the method's descriptor (see {@link Type}). + * @param isInterface if the method's owner class is an interface. + */ + public void visitMethodInsn( + final int opcode, + final String owner, + final String name, + final String descriptor, + final boolean isInterface) { + if (api < Opcodes.ASM5 && (opcode & Opcodes.SOURCE_DEPRECATED) == 0) { + if (isInterface != (opcode == Opcodes.INVOKEINTERFACE)) { + throw new UnsupportedOperationException("INVOKESPECIAL/STATIC on interfaces requires ASM5"); + } + visitMethodInsn(opcode, owner, name, descriptor); + return; + } + if (mv != null) { + mv.visitMethodInsn(opcode & ~Opcodes.SOURCE_MASK, owner, name, descriptor, isInterface); + } + } + + /** + * Visits an invokedynamic instruction. + * + * @param name the method's name. + * @param descriptor the method's descriptor (see {@link Type}). + * @param bootstrapMethodHandle the bootstrap method. + * @param bootstrapMethodArguments the bootstrap method constant arguments. Each argument must be + * an {@link Integer}, {@link Float}, {@link Long}, {@link Double}, {@link String}, {@link + * Type}, {@link Handle} or {@link ConstantDynamic} value. This method is allowed to modify + * the content of the array so a caller should expect that this array may change. + */ + public void visitInvokeDynamicInsn( + final String name, + final String descriptor, + final Handle bootstrapMethodHandle, + final Object... bootstrapMethodArguments) { + if (api < Opcodes.ASM5) { + throw new UnsupportedOperationException(REQUIRES_ASM5); + } + if (mv != null) { + mv.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments); + } + } + + /** + * Visits a jump instruction. A jump instruction is an instruction that may jump to another + * instruction. + * + * @param opcode the opcode of the type instruction to be visited. This opcode is either IFEQ, + * IFNE, IFLT, IFGE, IFGT, IFLE, IF_ICMPEQ, IF_ICMPNE, IF_ICMPLT, IF_ICMPGE, IF_ICMPGT, + * IF_ICMPLE, IF_ACMPEQ, IF_ACMPNE, GOTO, JSR, IFNULL or IFNONNULL. + * @param label the operand of the instruction to be visited. This operand is a label that + * designates the instruction to which the jump instruction may jump. + */ + public void visitJumpInsn(final int opcode, final Label label) { + if (mv != null) { + mv.visitJumpInsn(opcode, label); + } + } + + /** + * Visits a label. A label designates the instruction that will be visited just after it. + * + * @param label a {@link Label} object. + */ + public void visitLabel(final Label label) { + if (mv != null) { + mv.visitLabel(label); + } + } + + // ----------------------------------------------------------------------------------------------- + // Special instructions + // ----------------------------------------------------------------------------------------------- + + /** + * Visits a LDC instruction. Note that new constant types may be added in future versions of the + * Java Virtual Machine. To easily detect new constant types, implementations of this method + * should check for unexpected constant types, like this: + * + *
    +   * if (cst instanceof Integer) {
    +   *     // ...
    +   * } else if (cst instanceof Float) {
    +   *     // ...
    +   * } else if (cst instanceof Long) {
    +   *     // ...
    +   * } else if (cst instanceof Double) {
    +   *     // ...
    +   * } else if (cst instanceof String) {
    +   *     // ...
    +   * } else if (cst instanceof Type) {
    +   *     int sort = ((Type) cst).getSort();
    +   *     if (sort == Type.OBJECT) {
    +   *         // ...
    +   *     } else if (sort == Type.ARRAY) {
    +   *         // ...
    +   *     } else if (sort == Type.METHOD) {
    +   *         // ...
    +   *     } else {
    +   *         // throw an exception
    +   *     }
    +   * } else if (cst instanceof Handle) {
    +   *     // ...
    +   * } else if (cst instanceof ConstantDynamic) {
    +   *     // ...
    +   * } else {
    +   *     // throw an exception
    +   * }
    +   * 
    + * + * @param value the constant to be loaded on the stack. This parameter must be a non null {@link + * Integer}, a {@link Float}, a {@link Long}, a {@link Double}, a {@link String}, a {@link + * Type} of OBJECT or ARRAY sort for {@code .class} constants, for classes whose version is + * 49, a {@link Type} of METHOD sort for MethodType, a {@link Handle} for MethodHandle + * constants, for classes whose version is 51 or a {@link ConstantDynamic} for a constant + * dynamic for classes whose version is 55. + */ + public void visitLdcInsn(final Object value) { + if (api < Opcodes.ASM5 + && (value instanceof Handle + || (value instanceof Type && ((Type) value).getSort() == Type.METHOD))) { + throw new UnsupportedOperationException(REQUIRES_ASM5); + } + if (api < Opcodes.ASM7 && value instanceof ConstantDynamic) { + throw new UnsupportedOperationException("This feature requires ASM7"); + } + if (mv != null) { + mv.visitLdcInsn(value); + } + } + + /** + * Visits an IINC instruction. + * + * @param var index of the local variable to be incremented. + * @param increment amount to increment the local variable by. + */ + public void visitIincInsn(final int var, final int increment) { + if (mv != null) { + mv.visitIincInsn(var, increment); + } + } + + /** + * Visits a TABLESWITCH instruction. + * + * @param min the minimum key value. + * @param max the maximum key value. + * @param dflt beginning of the default handler block. + * @param labels beginnings of the handler blocks. {@code labels[i]} is the beginning of the + * handler block for the {@code min + i} key. + */ + public void visitTableSwitchInsn( + final int min, final int max, final Label dflt, final Label... labels) { + if (mv != null) { + mv.visitTableSwitchInsn(min, max, dflt, labels); + } + } + + /** + * Visits a LOOKUPSWITCH instruction. + * + * @param dflt beginning of the default handler block. + * @param keys the values of the keys. + * @param labels beginnings of the handler blocks. {@code labels[i]} is the beginning of the + * handler block for the {@code keys[i]} key. + */ + public void visitLookupSwitchInsn(final Label dflt, final int[] keys, final Label[] labels) { + if (mv != null) { + mv.visitLookupSwitchInsn(dflt, keys, labels); + } + } + + /** + * Visits a MULTIANEWARRAY instruction. + * + * @param descriptor an array type descriptor (see {@link Type}). + * @param numDimensions the number of dimensions of the array to allocate. + */ + public void visitMultiANewArrayInsn(final String descriptor, final int numDimensions) { + if (mv != null) { + mv.visitMultiANewArrayInsn(descriptor, numDimensions); + } + } + + /** + * Visits an annotation on an instruction. This method must be called just after the + * annotated instruction. It can be called several times for the same instruction. + * + * @param typeRef a reference to the annotated type. The sort of this type reference must be + * {@link TypeReference#INSTANCEOF}, {@link TypeReference#NEW}, {@link + * TypeReference#CONSTRUCTOR_REFERENCE}, {@link TypeReference#METHOD_REFERENCE}, {@link + * TypeReference#CAST}, {@link TypeReference#CONSTRUCTOR_INVOCATION_TYPE_ARGUMENT}, {@link + * TypeReference#METHOD_INVOCATION_TYPE_ARGUMENT}, {@link + * TypeReference#CONSTRUCTOR_REFERENCE_TYPE_ARGUMENT}, or {@link + * TypeReference#METHOD_REFERENCE_TYPE_ARGUMENT}. See {@link TypeReference}. + * @param typePath the path to the annotated type argument, wildcard bound, array element type, or + * static inner type within 'typeRef'. May be {@literal null} if the annotation targets + * 'typeRef' as a whole. + * @param descriptor the class descriptor of the annotation class. + * @param visible {@literal true} if the annotation is visible at runtime. + * @return a visitor to visit the annotation values, or {@literal null} if this visitor is not + * interested in visiting this annotation. + */ + public AnnotationVisitor visitInsnAnnotation( + final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) { + if (api < Opcodes.ASM5) { + throw new UnsupportedOperationException(REQUIRES_ASM5); + } + if (mv != null) { + return mv.visitInsnAnnotation(typeRef, typePath, descriptor, visible); + } + return null; + } + + // ----------------------------------------------------------------------------------------------- + // Exceptions table entries, debug information, max stack and max locals + // ----------------------------------------------------------------------------------------------- + + /** + * Visits a try catch block. + * + * @param start the beginning of the exception handler's scope (inclusive). + * @param end the end of the exception handler's scope (exclusive). + * @param handler the beginning of the exception handler's code. + * @param type the internal name of the type of exceptions handled by the handler, or {@literal + * null} to catch any exceptions (for "finally" blocks). + * @throws IllegalArgumentException if one of the labels has already been visited by this visitor + * (by the {@link #visitLabel} method). + */ + public void visitTryCatchBlock( + final Label start, final Label end, final Label handler, final String type) { + if (mv != null) { + mv.visitTryCatchBlock(start, end, handler, type); + } + } + + /** + * Visits an annotation on an exception handler type. This method must be called after the + * {@link #visitTryCatchBlock} for the annotated exception handler. It can be called several times + * for the same exception handler. + * + * @param typeRef a reference to the annotated type. The sort of this type reference must be + * {@link TypeReference#EXCEPTION_PARAMETER}. See {@link TypeReference}. + * @param typePath the path to the annotated type argument, wildcard bound, array element type, or + * static inner type within 'typeRef'. May be {@literal null} if the annotation targets + * 'typeRef' as a whole. + * @param descriptor the class descriptor of the annotation class. + * @param visible {@literal true} if the annotation is visible at runtime. + * @return a visitor to visit the annotation values, or {@literal null} if this visitor is not + * interested in visiting this annotation. + */ + public AnnotationVisitor visitTryCatchAnnotation( + final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) { + if (api < Opcodes.ASM5) { + throw new UnsupportedOperationException(REQUIRES_ASM5); + } + if (mv != null) { + return mv.visitTryCatchAnnotation(typeRef, typePath, descriptor, visible); + } + return null; + } + + /** + * Visits a local variable declaration. + * + * @param name the name of a local variable. + * @param descriptor the type descriptor of this local variable. + * @param signature the type signature of this local variable. May be {@literal null} if the local + * variable type does not use generic types. + * @param start the first instruction corresponding to the scope of this local variable + * (inclusive). + * @param end the last instruction corresponding to the scope of this local variable (exclusive). + * @param index the local variable's index. + * @throws IllegalArgumentException if one of the labels has not already been visited by this + * visitor (by the {@link #visitLabel} method). + */ + public void visitLocalVariable( + final String name, + final String descriptor, + final String signature, + final Label start, + final Label end, + final int index) { + if (mv != null) { + mv.visitLocalVariable(name, descriptor, signature, start, end, index); + } + } + + /** + * Visits an annotation on a local variable type. + * + * @param typeRef a reference to the annotated type. The sort of this type reference must be + * {@link TypeReference#LOCAL_VARIABLE} or {@link TypeReference#RESOURCE_VARIABLE}. See {@link + * TypeReference}. + * @param typePath the path to the annotated type argument, wildcard bound, array element type, or + * static inner type within 'typeRef'. May be {@literal null} if the annotation targets + * 'typeRef' as a whole. + * @param start the fist instructions corresponding to the continuous ranges that make the scope + * of this local variable (inclusive). + * @param end the last instructions corresponding to the continuous ranges that make the scope of + * this local variable (exclusive). This array must have the same size as the 'start' array. + * @param index the local variable's index in each range. This array must have the same size as + * the 'start' array. + * @param descriptor the class descriptor of the annotation class. + * @param visible {@literal true} if the annotation is visible at runtime. + * @return a visitor to visit the annotation values, or {@literal null} if this visitor is not + * interested in visiting this annotation. + */ + public AnnotationVisitor visitLocalVariableAnnotation( + final int typeRef, + final TypePath typePath, + final Label[] start, + final Label[] end, + final int[] index, + final String descriptor, + final boolean visible) { + if (api < Opcodes.ASM5) { + throw new UnsupportedOperationException(REQUIRES_ASM5); + } + if (mv != null) { + return mv.visitLocalVariableAnnotation( + typeRef, typePath, start, end, index, descriptor, visible); + } + return null; + } + + /** + * Visits a line number declaration. + * + * @param line a line number. This number refers to the source file from which the class was + * compiled. + * @param start the first instruction corresponding to this line number. + * @throws IllegalArgumentException if {@code start} has not already been visited by this visitor + * (by the {@link #visitLabel} method). + */ + public void visitLineNumber(final int line, final Label start) { + if (mv != null) { + mv.visitLineNumber(line, start); + } + } + + /** + * Visits the maximum stack size and the maximum number of local variables of the method. + * + * @param maxStack maximum stack size of the method. + * @param maxLocals maximum number of local variables for the method. + */ + public void visitMaxs(final int maxStack, final int maxLocals) { + if (mv != null) { + mv.visitMaxs(maxStack, maxLocals); + } + } + + /** + * Visits the end of the method. This method, which is the last one to be called, is used to + * inform the visitor that all the annotations and attributes of the method have been visited. + */ + public void visitEnd() { + if (mv != null) { + mv.visitEnd(); + } + } +} diff --git a/spring-core/src/main/java/org/springframework/asm/MethodWriter.java b/spring-core/src/main/java/org/springframework/asm/MethodWriter.java new file mode 100644 index 0000000..54f9b1c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/MethodWriter.java @@ -0,0 +1,2393 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +package org.springframework.asm; + +/** + * A {@link MethodVisitor} that generates a corresponding 'method_info' structure, as defined in the + * Java Virtual Machine Specification (JVMS). + * + * @see JVMS + * 4.6 + * @author Eric Bruneton + * @author Eugene Kuleshov + */ +final class MethodWriter extends MethodVisitor { + + /** Indicates that nothing must be computed. */ + static final int COMPUTE_NOTHING = 0; + + /** + * Indicates that the maximum stack size and the maximum number of local variables must be + * computed, from scratch. + */ + static final int COMPUTE_MAX_STACK_AND_LOCAL = 1; + + /** + * Indicates that the maximum stack size and the maximum number of local variables must be + * computed, from the existing stack map frames. This can be done more efficiently than with the + * control flow graph algorithm used for {@link #COMPUTE_MAX_STACK_AND_LOCAL}, by using a linear + * scan of the bytecode instructions. + */ + static final int COMPUTE_MAX_STACK_AND_LOCAL_FROM_FRAMES = 2; + + /** + * Indicates that the stack map frames of type F_INSERT must be computed. The other frames are not + * computed. They should all be of type F_NEW and should be sufficient to compute the content of + * the F_INSERT frames, together with the bytecode instructions between a F_NEW and a F_INSERT + * frame - and without any knowledge of the type hierarchy (by definition of F_INSERT). + */ + static final int COMPUTE_INSERTED_FRAMES = 3; + + /** + * Indicates that all the stack map frames must be computed. In this case the maximum stack size + * and the maximum number of local variables is also computed. + */ + static final int COMPUTE_ALL_FRAMES = 4; + + /** Indicates that {@link #STACK_SIZE_DELTA} is not applicable (not constant or never used). */ + private static final int NA = 0; + + /** + * The stack size variation corresponding to each JVM opcode. The stack size variation for opcode + * 'o' is given by the array element at index 'o'. + * + * @see JVMS 6 + */ + private static final int[] STACK_SIZE_DELTA = { + 0, // nop = 0 (0x0) + 1, // aconst_null = 1 (0x1) + 1, // iconst_m1 = 2 (0x2) + 1, // iconst_0 = 3 (0x3) + 1, // iconst_1 = 4 (0x4) + 1, // iconst_2 = 5 (0x5) + 1, // iconst_3 = 6 (0x6) + 1, // iconst_4 = 7 (0x7) + 1, // iconst_5 = 8 (0x8) + 2, // lconst_0 = 9 (0x9) + 2, // lconst_1 = 10 (0xa) + 1, // fconst_0 = 11 (0xb) + 1, // fconst_1 = 12 (0xc) + 1, // fconst_2 = 13 (0xd) + 2, // dconst_0 = 14 (0xe) + 2, // dconst_1 = 15 (0xf) + 1, // bipush = 16 (0x10) + 1, // sipush = 17 (0x11) + 1, // ldc = 18 (0x12) + NA, // ldc_w = 19 (0x13) + NA, // ldc2_w = 20 (0x14) + 1, // iload = 21 (0x15) + 2, // lload = 22 (0x16) + 1, // fload = 23 (0x17) + 2, // dload = 24 (0x18) + 1, // aload = 25 (0x19) + NA, // iload_0 = 26 (0x1a) + NA, // iload_1 = 27 (0x1b) + NA, // iload_2 = 28 (0x1c) + NA, // iload_3 = 29 (0x1d) + NA, // lload_0 = 30 (0x1e) + NA, // lload_1 = 31 (0x1f) + NA, // lload_2 = 32 (0x20) + NA, // lload_3 = 33 (0x21) + NA, // fload_0 = 34 (0x22) + NA, // fload_1 = 35 (0x23) + NA, // fload_2 = 36 (0x24) + NA, // fload_3 = 37 (0x25) + NA, // dload_0 = 38 (0x26) + NA, // dload_1 = 39 (0x27) + NA, // dload_2 = 40 (0x28) + NA, // dload_3 = 41 (0x29) + NA, // aload_0 = 42 (0x2a) + NA, // aload_1 = 43 (0x2b) + NA, // aload_2 = 44 (0x2c) + NA, // aload_3 = 45 (0x2d) + -1, // iaload = 46 (0x2e) + 0, // laload = 47 (0x2f) + -1, // faload = 48 (0x30) + 0, // daload = 49 (0x31) + -1, // aaload = 50 (0x32) + -1, // baload = 51 (0x33) + -1, // caload = 52 (0x34) + -1, // saload = 53 (0x35) + -1, // istore = 54 (0x36) + -2, // lstore = 55 (0x37) + -1, // fstore = 56 (0x38) + -2, // dstore = 57 (0x39) + -1, // astore = 58 (0x3a) + NA, // istore_0 = 59 (0x3b) + NA, // istore_1 = 60 (0x3c) + NA, // istore_2 = 61 (0x3d) + NA, // istore_3 = 62 (0x3e) + NA, // lstore_0 = 63 (0x3f) + NA, // lstore_1 = 64 (0x40) + NA, // lstore_2 = 65 (0x41) + NA, // lstore_3 = 66 (0x42) + NA, // fstore_0 = 67 (0x43) + NA, // fstore_1 = 68 (0x44) + NA, // fstore_2 = 69 (0x45) + NA, // fstore_3 = 70 (0x46) + NA, // dstore_0 = 71 (0x47) + NA, // dstore_1 = 72 (0x48) + NA, // dstore_2 = 73 (0x49) + NA, // dstore_3 = 74 (0x4a) + NA, // astore_0 = 75 (0x4b) + NA, // astore_1 = 76 (0x4c) + NA, // astore_2 = 77 (0x4d) + NA, // astore_3 = 78 (0x4e) + -3, // iastore = 79 (0x4f) + -4, // lastore = 80 (0x50) + -3, // fastore = 81 (0x51) + -4, // dastore = 82 (0x52) + -3, // aastore = 83 (0x53) + -3, // bastore = 84 (0x54) + -3, // castore = 85 (0x55) + -3, // sastore = 86 (0x56) + -1, // pop = 87 (0x57) + -2, // pop2 = 88 (0x58) + 1, // dup = 89 (0x59) + 1, // dup_x1 = 90 (0x5a) + 1, // dup_x2 = 91 (0x5b) + 2, // dup2 = 92 (0x5c) + 2, // dup2_x1 = 93 (0x5d) + 2, // dup2_x2 = 94 (0x5e) + 0, // swap = 95 (0x5f) + -1, // iadd = 96 (0x60) + -2, // ladd = 97 (0x61) + -1, // fadd = 98 (0x62) + -2, // dadd = 99 (0x63) + -1, // isub = 100 (0x64) + -2, // lsub = 101 (0x65) + -1, // fsub = 102 (0x66) + -2, // dsub = 103 (0x67) + -1, // imul = 104 (0x68) + -2, // lmul = 105 (0x69) + -1, // fmul = 106 (0x6a) + -2, // dmul = 107 (0x6b) + -1, // idiv = 108 (0x6c) + -2, // ldiv = 109 (0x6d) + -1, // fdiv = 110 (0x6e) + -2, // ddiv = 111 (0x6f) + -1, // irem = 112 (0x70) + -2, // lrem = 113 (0x71) + -1, // frem = 114 (0x72) + -2, // drem = 115 (0x73) + 0, // ineg = 116 (0x74) + 0, // lneg = 117 (0x75) + 0, // fneg = 118 (0x76) + 0, // dneg = 119 (0x77) + -1, // ishl = 120 (0x78) + -1, // lshl = 121 (0x79) + -1, // ishr = 122 (0x7a) + -1, // lshr = 123 (0x7b) + -1, // iushr = 124 (0x7c) + -1, // lushr = 125 (0x7d) + -1, // iand = 126 (0x7e) + -2, // land = 127 (0x7f) + -1, // ior = 128 (0x80) + -2, // lor = 129 (0x81) + -1, // ixor = 130 (0x82) + -2, // lxor = 131 (0x83) + 0, // iinc = 132 (0x84) + 1, // i2l = 133 (0x85) + 0, // i2f = 134 (0x86) + 1, // i2d = 135 (0x87) + -1, // l2i = 136 (0x88) + -1, // l2f = 137 (0x89) + 0, // l2d = 138 (0x8a) + 0, // f2i = 139 (0x8b) + 1, // f2l = 140 (0x8c) + 1, // f2d = 141 (0x8d) + -1, // d2i = 142 (0x8e) + 0, // d2l = 143 (0x8f) + -1, // d2f = 144 (0x90) + 0, // i2b = 145 (0x91) + 0, // i2c = 146 (0x92) + 0, // i2s = 147 (0x93) + -3, // lcmp = 148 (0x94) + -1, // fcmpl = 149 (0x95) + -1, // fcmpg = 150 (0x96) + -3, // dcmpl = 151 (0x97) + -3, // dcmpg = 152 (0x98) + -1, // ifeq = 153 (0x99) + -1, // ifne = 154 (0x9a) + -1, // iflt = 155 (0x9b) + -1, // ifge = 156 (0x9c) + -1, // ifgt = 157 (0x9d) + -1, // ifle = 158 (0x9e) + -2, // if_icmpeq = 159 (0x9f) + -2, // if_icmpne = 160 (0xa0) + -2, // if_icmplt = 161 (0xa1) + -2, // if_icmpge = 162 (0xa2) + -2, // if_icmpgt = 163 (0xa3) + -2, // if_icmple = 164 (0xa4) + -2, // if_acmpeq = 165 (0xa5) + -2, // if_acmpne = 166 (0xa6) + 0, // goto = 167 (0xa7) + 1, // jsr = 168 (0xa8) + 0, // ret = 169 (0xa9) + -1, // tableswitch = 170 (0xaa) + -1, // lookupswitch = 171 (0xab) + -1, // ireturn = 172 (0xac) + -2, // lreturn = 173 (0xad) + -1, // freturn = 174 (0xae) + -2, // dreturn = 175 (0xaf) + -1, // areturn = 176 (0xb0) + 0, // return = 177 (0xb1) + NA, // getstatic = 178 (0xb2) + NA, // putstatic = 179 (0xb3) + NA, // getfield = 180 (0xb4) + NA, // putfield = 181 (0xb5) + NA, // invokevirtual = 182 (0xb6) + NA, // invokespecial = 183 (0xb7) + NA, // invokestatic = 184 (0xb8) + NA, // invokeinterface = 185 (0xb9) + NA, // invokedynamic = 186 (0xba) + 1, // new = 187 (0xbb) + 0, // newarray = 188 (0xbc) + 0, // anewarray = 189 (0xbd) + 0, // arraylength = 190 (0xbe) + NA, // athrow = 191 (0xbf) + 0, // checkcast = 192 (0xc0) + 0, // instanceof = 193 (0xc1) + -1, // monitorenter = 194 (0xc2) + -1, // monitorexit = 195 (0xc3) + NA, // wide = 196 (0xc4) + NA, // multianewarray = 197 (0xc5) + -1, // ifnull = 198 (0xc6) + -1, // ifnonnull = 199 (0xc7) + NA, // goto_w = 200 (0xc8) + NA // jsr_w = 201 (0xc9) + }; + + /** Where the constants used in this MethodWriter must be stored. */ + private final SymbolTable symbolTable; + + // Note: fields are ordered as in the method_info structure, and those related to attributes are + // ordered as in Section 4.7 of the JVMS. + + /** + * The access_flags field of the method_info JVMS structure. This field can contain ASM specific + * access flags, such as {@link Opcodes#ACC_DEPRECATED}, which are removed when generating the + * ClassFile structure. + */ + private final int accessFlags; + + /** The name_index field of the method_info JVMS structure. */ + private final int nameIndex; + + /** The name of this method. */ + private final String name; + + /** The descriptor_index field of the method_info JVMS structure. */ + private final int descriptorIndex; + + /** The descriptor of this method. */ + private final String descriptor; + + // Code attribute fields and sub attributes: + + /** The max_stack field of the Code attribute. */ + private int maxStack; + + /** The max_locals field of the Code attribute. */ + private int maxLocals; + + /** The 'code' field of the Code attribute. */ + private final ByteVector code = new ByteVector(); + + /** + * The first element in the exception handler list (used to generate the exception_table of the + * Code attribute). The next ones can be accessed with the {@link Handler#nextHandler} field. May + * be {@literal null}. + */ + private Handler firstHandler; + + /** + * The last element in the exception handler list (used to generate the exception_table of the + * Code attribute). The next ones can be accessed with the {@link Handler#nextHandler} field. May + * be {@literal null}. + */ + private Handler lastHandler; + + /** The line_number_table_length field of the LineNumberTable code attribute. */ + private int lineNumberTableLength; + + /** The line_number_table array of the LineNumberTable code attribute, or {@literal null}. */ + private ByteVector lineNumberTable; + + /** The local_variable_table_length field of the LocalVariableTable code attribute. */ + private int localVariableTableLength; + + /** + * The local_variable_table array of the LocalVariableTable code attribute, or {@literal null}. + */ + private ByteVector localVariableTable; + + /** The local_variable_type_table_length field of the LocalVariableTypeTable code attribute. */ + private int localVariableTypeTableLength; + + /** + * The local_variable_type_table array of the LocalVariableTypeTable code attribute, or {@literal + * null}. + */ + private ByteVector localVariableTypeTable; + + /** The number_of_entries field of the StackMapTable code attribute. */ + private int stackMapTableNumberOfEntries; + + /** The 'entries' array of the StackMapTable code attribute. */ + private ByteVector stackMapTableEntries; + + /** + * The last runtime visible type annotation of the Code attribute. The previous ones can be + * accessed with the {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastCodeRuntimeVisibleTypeAnnotation; + + /** + * The last runtime invisible type annotation of the Code attribute. The previous ones can be + * accessed with the {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastCodeRuntimeInvisibleTypeAnnotation; + + /** + * The first non standard attribute of the Code attribute. The next ones can be accessed with the + * {@link Attribute#nextAttribute} field. May be {@literal null}. + * + *

    WARNING: this list stores the attributes in the reverse order of their visit. + * firstAttribute is actually the last attribute visited in {@link #visitAttribute}. The {@link + * #putMethodInfo} method writes the attributes in the order defined by this list, i.e. in the + * reverse order specified by the user. + */ + private Attribute firstCodeAttribute; + + // Other method_info attributes: + + /** The number_of_exceptions field of the Exceptions attribute. */ + private final int numberOfExceptions; + + /** The exception_index_table array of the Exceptions attribute, or {@literal null}. */ + private final int[] exceptionIndexTable; + + /** The signature_index field of the Signature attribute. */ + private final int signatureIndex; + + /** + * The last runtime visible annotation of this method. The previous ones can be accessed with the + * {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeVisibleAnnotation; + + /** + * The last runtime invisible annotation of this method. The previous ones can be accessed with + * the {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeInvisibleAnnotation; + + /** The number of method parameters that can have runtime visible annotations, or 0. */ + private int visibleAnnotableParameterCount; + + /** + * The runtime visible parameter annotations of this method. Each array element contains the last + * annotation of a parameter (which can be {@literal null} - the previous ones can be accessed + * with the {@link AnnotationWriter#previousAnnotation} field). May be {@literal null}. + */ + private AnnotationWriter[] lastRuntimeVisibleParameterAnnotations; + + /** The number of method parameters that can have runtime visible annotations, or 0. */ + private int invisibleAnnotableParameterCount; + + /** + * The runtime invisible parameter annotations of this method. Each array element contains the + * last annotation of a parameter (which can be {@literal null} - the previous ones can be + * accessed with the {@link AnnotationWriter#previousAnnotation} field). May be {@literal null}. + */ + private AnnotationWriter[] lastRuntimeInvisibleParameterAnnotations; + + /** + * The last runtime visible type annotation of this method. The previous ones can be accessed with + * the {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeVisibleTypeAnnotation; + + /** + * The last runtime invisible type annotation of this method. The previous ones can be accessed + * with the {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeInvisibleTypeAnnotation; + + /** The default_value field of the AnnotationDefault attribute, or {@literal null}. */ + private ByteVector defaultValue; + + /** The parameters_count field of the MethodParameters attribute. */ + private int parametersCount; + + /** The 'parameters' array of the MethodParameters attribute, or {@literal null}. */ + private ByteVector parameters; + + /** + * The first non standard attribute of this method. The next ones can be accessed with the {@link + * Attribute#nextAttribute} field. May be {@literal null}. + * + *

    WARNING: this list stores the attributes in the reverse order of their visit. + * firstAttribute is actually the last attribute visited in {@link #visitAttribute}. The {@link + * #putMethodInfo} method writes the attributes in the order defined by this list, i.e. in the + * reverse order specified by the user. + */ + private Attribute firstAttribute; + + // ----------------------------------------------------------------------------------------------- + // Fields used to compute the maximum stack size and number of locals, and the stack map frames + // ----------------------------------------------------------------------------------------------- + + /** + * Indicates what must be computed. Must be one of {@link #COMPUTE_ALL_FRAMES}, {@link + * #COMPUTE_INSERTED_FRAMES}, {@link #COMPUTE_MAX_STACK_AND_LOCAL} or {@link #COMPUTE_NOTHING}. + */ + private final int compute; + + /** + * The first basic block of the method. The next ones (in bytecode offset order) can be accessed + * with the {@link Label#nextBasicBlock} field. + */ + private Label firstBasicBlock; + + /** + * The last basic block of the method (in bytecode offset order). This field is updated each time + * a basic block is encountered, and is used to append it at the end of the basic block list. + */ + private Label lastBasicBlock; + + /** + * The current basic block, i.e. the basic block of the last visited instruction. When {@link + * #compute} is equal to {@link #COMPUTE_MAX_STACK_AND_LOCAL} or {@link #COMPUTE_ALL_FRAMES}, this + * field is {@literal null} for unreachable code. When {@link #compute} is equal to {@link + * #COMPUTE_MAX_STACK_AND_LOCAL_FROM_FRAMES} or {@link #COMPUTE_INSERTED_FRAMES}, this field stays + * unchanged throughout the whole method (i.e. the whole code is seen as a single basic block; + * indeed, the existing frames are sufficient by hypothesis to compute any intermediate frame - + * and the maximum stack size as well - without using any control flow graph). + */ + private Label currentBasicBlock; + + /** + * The relative stack size after the last visited instruction. This size is relative to the + * beginning of {@link #currentBasicBlock}, i.e. the true stack size after the last visited + * instruction is equal to the {@link Label#inputStackSize} of the current basic block plus {@link + * #relativeStackSize}. When {@link #compute} is equal to {@link + * #COMPUTE_MAX_STACK_AND_LOCAL_FROM_FRAMES}, {@link #currentBasicBlock} is always the start of + * the method, so this relative size is also equal to the absolute stack size after the last + * visited instruction. + */ + private int relativeStackSize; + + /** + * The maximum relative stack size after the last visited instruction. This size is relative to + * the beginning of {@link #currentBasicBlock}, i.e. the true maximum stack size after the last + * visited instruction is equal to the {@link Label#inputStackSize} of the current basic block + * plus {@link #maxRelativeStackSize}.When {@link #compute} is equal to {@link + * #COMPUTE_MAX_STACK_AND_LOCAL_FROM_FRAMES}, {@link #currentBasicBlock} is always the start of + * the method, so this relative size is also equal to the absolute maximum stack size after the + * last visited instruction. + */ + private int maxRelativeStackSize; + + /** The number of local variables in the last visited stack map frame. */ + private int currentLocals; + + /** The bytecode offset of the last frame that was written in {@link #stackMapTableEntries}. */ + private int previousFrameOffset; + + /** + * The last frame that was written in {@link #stackMapTableEntries}. This field has the same + * format as {@link #currentFrame}. + */ + private int[] previousFrame; + + /** + * The current stack map frame. The first element contains the bytecode offset of the instruction + * to which the frame corresponds, the second element is the number of locals and the third one is + * the number of stack elements. The local variables start at index 3 and are followed by the + * operand stack elements. In summary frame[0] = offset, frame[1] = numLocal, frame[2] = numStack. + * Local variables and operand stack entries contain abstract types, as defined in {@link Frame}, + * but restricted to {@link Frame#CONSTANT_KIND}, {@link Frame#REFERENCE_KIND} or {@link + * Frame#UNINITIALIZED_KIND} abstract types. Long and double types use only one array entry. + */ + private int[] currentFrame; + + /** Whether this method contains subroutines. */ + private boolean hasSubroutines; + + // ----------------------------------------------------------------------------------------------- + // Other miscellaneous status fields + // ----------------------------------------------------------------------------------------------- + + /** Whether the bytecode of this method contains ASM specific instructions. */ + private boolean hasAsmInstructions; + + /** + * The start offset of the last visited instruction. Used to set the offset field of type + * annotations of type 'offset_target' (see JVMS + * 4.7.20.1). + */ + private int lastBytecodeOffset; + + /** + * The offset in bytes in {@link SymbolTable#getSource} from which the method_info for this method + * (excluding its first 6 bytes) must be copied, or 0. + */ + private int sourceOffset; + + /** + * The length in bytes in {@link SymbolTable#getSource} which must be copied to get the + * method_info for this method (excluding its first 6 bytes for access_flags, name_index and + * descriptor_index). + */ + private int sourceLength; + + // ----------------------------------------------------------------------------------------------- + // Constructor and accessors + // ----------------------------------------------------------------------------------------------- + + /** + * Constructs a new {@link MethodWriter}. + * + * @param symbolTable where the constants used in this AnnotationWriter must be stored. + * @param access the method's access flags (see {@link Opcodes}). + * @param name the method's name. + * @param descriptor the method's descriptor (see {@link Type}). + * @param signature the method's signature. May be {@literal null}. + * @param exceptions the internal names of the method's exceptions. May be {@literal null}. + * @param compute indicates what must be computed (see #compute). + */ + MethodWriter( + final SymbolTable symbolTable, + final int access, + final String name, + final String descriptor, + final String signature, + final String[] exceptions, + final int compute) { + super(/* latest api = */ Opcodes.ASM9); + this.symbolTable = symbolTable; + this.accessFlags = "".equals(name) ? access | Constants.ACC_CONSTRUCTOR : access; + this.nameIndex = symbolTable.addConstantUtf8(name); + this.name = name; + this.descriptorIndex = symbolTable.addConstantUtf8(descriptor); + this.descriptor = descriptor; + this.signatureIndex = signature == null ? 0 : symbolTable.addConstantUtf8(signature); + if (exceptions != null && exceptions.length > 0) { + numberOfExceptions = exceptions.length; + this.exceptionIndexTable = new int[numberOfExceptions]; + for (int i = 0; i < numberOfExceptions; ++i) { + this.exceptionIndexTable[i] = symbolTable.addConstantClass(exceptions[i]).index; + } + } else { + numberOfExceptions = 0; + this.exceptionIndexTable = null; + } + this.compute = compute; + if (compute != COMPUTE_NOTHING) { + // Update maxLocals and currentLocals. + int argumentsSize = Type.getArgumentsAndReturnSizes(descriptor) >> 2; + if ((access & Opcodes.ACC_STATIC) != 0) { + --argumentsSize; + } + maxLocals = argumentsSize; + currentLocals = argumentsSize; + // Create and visit the label for the first basic block. + firstBasicBlock = new Label(); + visitLabel(firstBasicBlock); + } + } + + boolean hasFrames() { + return stackMapTableNumberOfEntries > 0; + } + + boolean hasAsmInstructions() { + return hasAsmInstructions; + } + + // ----------------------------------------------------------------------------------------------- + // Implementation of the MethodVisitor abstract class + // ----------------------------------------------------------------------------------------------- + + @Override + public void visitParameter(final String name, final int access) { + if (parameters == null) { + parameters = new ByteVector(); + } + ++parametersCount; + parameters.putShort((name == null) ? 0 : symbolTable.addConstantUtf8(name)).putShort(access); + } + + @Override + public AnnotationVisitor visitAnnotationDefault() { + defaultValue = new ByteVector(); + return new AnnotationWriter(symbolTable, /* useNamedValues = */ false, defaultValue, null); + } + + @Override + public AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) { + if (visible) { + return lastRuntimeVisibleAnnotation = + AnnotationWriter.create(symbolTable, descriptor, lastRuntimeVisibleAnnotation); + } else { + return lastRuntimeInvisibleAnnotation = + AnnotationWriter.create(symbolTable, descriptor, lastRuntimeInvisibleAnnotation); + } + } + + @Override + public AnnotationVisitor visitTypeAnnotation( + final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) { + if (visible) { + return lastRuntimeVisibleTypeAnnotation = + AnnotationWriter.create( + symbolTable, typeRef, typePath, descriptor, lastRuntimeVisibleTypeAnnotation); + } else { + return lastRuntimeInvisibleTypeAnnotation = + AnnotationWriter.create( + symbolTable, typeRef, typePath, descriptor, lastRuntimeInvisibleTypeAnnotation); + } + } + + @Override + public void visitAnnotableParameterCount(final int parameterCount, final boolean visible) { + if (visible) { + visibleAnnotableParameterCount = parameterCount; + } else { + invisibleAnnotableParameterCount = parameterCount; + } + } + + @Override + public AnnotationVisitor visitParameterAnnotation( + final int parameter, final String annotationDescriptor, final boolean visible) { + if (visible) { + if (lastRuntimeVisibleParameterAnnotations == null) { + lastRuntimeVisibleParameterAnnotations = + new AnnotationWriter[Type.getArgumentTypes(descriptor).length]; + } + return lastRuntimeVisibleParameterAnnotations[parameter] = + AnnotationWriter.create( + symbolTable, annotationDescriptor, lastRuntimeVisibleParameterAnnotations[parameter]); + } else { + if (lastRuntimeInvisibleParameterAnnotations == null) { + lastRuntimeInvisibleParameterAnnotations = + new AnnotationWriter[Type.getArgumentTypes(descriptor).length]; + } + return lastRuntimeInvisibleParameterAnnotations[parameter] = + AnnotationWriter.create( + symbolTable, + annotationDescriptor, + lastRuntimeInvisibleParameterAnnotations[parameter]); + } + } + + @Override + public void visitAttribute(final Attribute attribute) { + // Store the attributes in the reverse order of their visit by this method. + if (attribute.isCodeAttribute()) { + attribute.nextAttribute = firstCodeAttribute; + firstCodeAttribute = attribute; + } else { + attribute.nextAttribute = firstAttribute; + firstAttribute = attribute; + } + } + + @Override + public void visitCode() { + // Nothing to do. + } + + @Override + public void visitFrame( + final int type, + final int numLocal, + final Object[] local, + final int numStack, + final Object[] stack) { + if (compute == COMPUTE_ALL_FRAMES) { + return; + } + + if (compute == COMPUTE_INSERTED_FRAMES) { + if (currentBasicBlock.frame == null) { + // This should happen only once, for the implicit first frame (which is explicitly visited + // in ClassReader if the EXPAND_ASM_INSNS option is used - and COMPUTE_INSERTED_FRAMES + // can't be set if EXPAND_ASM_INSNS is not used). + currentBasicBlock.frame = new CurrentFrame(currentBasicBlock); + currentBasicBlock.frame.setInputFrameFromDescriptor( + symbolTable, accessFlags, descriptor, numLocal); + currentBasicBlock.frame.accept(this); + } else { + if (type == Opcodes.F_NEW) { + currentBasicBlock.frame.setInputFrameFromApiFormat( + symbolTable, numLocal, local, numStack, stack); + } + // If type is not F_NEW then it is F_INSERT by hypothesis, and currentBlock.frame contains + // the stack map frame at the current instruction, computed from the last F_NEW frame and + // the bytecode instructions in between (via calls to CurrentFrame#execute). + currentBasicBlock.frame.accept(this); + } + } else if (type == Opcodes.F_NEW) { + if (previousFrame == null) { + int argumentsSize = Type.getArgumentsAndReturnSizes(descriptor) >> 2; + Frame implicitFirstFrame = new Frame(new Label()); + implicitFirstFrame.setInputFrameFromDescriptor( + symbolTable, accessFlags, descriptor, argumentsSize); + implicitFirstFrame.accept(this); + } + currentLocals = numLocal; + int frameIndex = visitFrameStart(code.length, numLocal, numStack); + for (int i = 0; i < numLocal; ++i) { + currentFrame[frameIndex++] = Frame.getAbstractTypeFromApiFormat(symbolTable, local[i]); + } + for (int i = 0; i < numStack; ++i) { + currentFrame[frameIndex++] = Frame.getAbstractTypeFromApiFormat(symbolTable, stack[i]); + } + visitFrameEnd(); + } else { + if (symbolTable.getMajorVersion() < Opcodes.V1_6) { + throw new IllegalArgumentException("Class versions V1_5 or less must use F_NEW frames."); + } + int offsetDelta; + if (stackMapTableEntries == null) { + stackMapTableEntries = new ByteVector(); + offsetDelta = code.length; + } else { + offsetDelta = code.length - previousFrameOffset - 1; + if (offsetDelta < 0) { + if (type == Opcodes.F_SAME) { + return; + } else { + throw new IllegalStateException(); + } + } + } + + switch (type) { + case Opcodes.F_FULL: + currentLocals = numLocal; + stackMapTableEntries.putByte(Frame.FULL_FRAME).putShort(offsetDelta).putShort(numLocal); + for (int i = 0; i < numLocal; ++i) { + putFrameType(local[i]); + } + stackMapTableEntries.putShort(numStack); + for (int i = 0; i < numStack; ++i) { + putFrameType(stack[i]); + } + break; + case Opcodes.F_APPEND: + currentLocals += numLocal; + stackMapTableEntries.putByte(Frame.SAME_FRAME_EXTENDED + numLocal).putShort(offsetDelta); + for (int i = 0; i < numLocal; ++i) { + putFrameType(local[i]); + } + break; + case Opcodes.F_CHOP: + currentLocals -= numLocal; + stackMapTableEntries.putByte(Frame.SAME_FRAME_EXTENDED - numLocal).putShort(offsetDelta); + break; + case Opcodes.F_SAME: + if (offsetDelta < 64) { + stackMapTableEntries.putByte(offsetDelta); + } else { + stackMapTableEntries.putByte(Frame.SAME_FRAME_EXTENDED).putShort(offsetDelta); + } + break; + case Opcodes.F_SAME1: + if (offsetDelta < 64) { + stackMapTableEntries.putByte(Frame.SAME_LOCALS_1_STACK_ITEM_FRAME + offsetDelta); + } else { + stackMapTableEntries + .putByte(Frame.SAME_LOCALS_1_STACK_ITEM_FRAME_EXTENDED) + .putShort(offsetDelta); + } + putFrameType(stack[0]); + break; + default: + throw new IllegalArgumentException(); + } + + previousFrameOffset = code.length; + ++stackMapTableNumberOfEntries; + } + + if (compute == COMPUTE_MAX_STACK_AND_LOCAL_FROM_FRAMES) { + relativeStackSize = numStack; + for (int i = 0; i < numStack; ++i) { + if (stack[i] == Opcodes.LONG || stack[i] == Opcodes.DOUBLE) { + relativeStackSize++; + } + } + if (relativeStackSize > maxRelativeStackSize) { + maxRelativeStackSize = relativeStackSize; + } + } + + maxStack = Math.max(maxStack, numStack); + maxLocals = Math.max(maxLocals, currentLocals); + } + + @Override + public void visitInsn(final int opcode) { + lastBytecodeOffset = code.length; + // Add the instruction to the bytecode of the method. + code.putByte(opcode); + // If needed, update the maximum stack size and number of locals, and stack map frames. + if (currentBasicBlock != null) { + if (compute == COMPUTE_ALL_FRAMES || compute == COMPUTE_INSERTED_FRAMES) { + currentBasicBlock.frame.execute(opcode, 0, null, null); + } else { + int size = relativeStackSize + STACK_SIZE_DELTA[opcode]; + if (size > maxRelativeStackSize) { + maxRelativeStackSize = size; + } + relativeStackSize = size; + } + if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) { + endCurrentBasicBlockWithNoSuccessor(); + } + } + } + + @Override + public void visitIntInsn(final int opcode, final int operand) { + lastBytecodeOffset = code.length; + // Add the instruction to the bytecode of the method. + if (opcode == Opcodes.SIPUSH) { + code.put12(opcode, operand); + } else { // BIPUSH or NEWARRAY + code.put11(opcode, operand); + } + // If needed, update the maximum stack size and number of locals, and stack map frames. + if (currentBasicBlock != null) { + if (compute == COMPUTE_ALL_FRAMES || compute == COMPUTE_INSERTED_FRAMES) { + currentBasicBlock.frame.execute(opcode, operand, null, null); + } else if (opcode != Opcodes.NEWARRAY) { + // The stack size delta is 1 for BIPUSH or SIPUSH, and 0 for NEWARRAY. + int size = relativeStackSize + 1; + if (size > maxRelativeStackSize) { + maxRelativeStackSize = size; + } + relativeStackSize = size; + } + } + } + + @Override + public void visitVarInsn(final int opcode, final int var) { + lastBytecodeOffset = code.length; + // Add the instruction to the bytecode of the method. + if (var < 4 && opcode != Opcodes.RET) { + int optimizedOpcode; + if (opcode < Opcodes.ISTORE) { + optimizedOpcode = Constants.ILOAD_0 + ((opcode - Opcodes.ILOAD) << 2) + var; + } else { + optimizedOpcode = Constants.ISTORE_0 + ((opcode - Opcodes.ISTORE) << 2) + var; + } + code.putByte(optimizedOpcode); + } else if (var >= 256) { + code.putByte(Constants.WIDE).put12(opcode, var); + } else { + code.put11(opcode, var); + } + // If needed, update the maximum stack size and number of locals, and stack map frames. + if (currentBasicBlock != null) { + if (compute == COMPUTE_ALL_FRAMES || compute == COMPUTE_INSERTED_FRAMES) { + currentBasicBlock.frame.execute(opcode, var, null, null); + } else { + if (opcode == Opcodes.RET) { + // No stack size delta. + currentBasicBlock.flags |= Label.FLAG_SUBROUTINE_END; + currentBasicBlock.outputStackSize = (short) relativeStackSize; + endCurrentBasicBlockWithNoSuccessor(); + } else { // xLOAD or xSTORE + int size = relativeStackSize + STACK_SIZE_DELTA[opcode]; + if (size > maxRelativeStackSize) { + maxRelativeStackSize = size; + } + relativeStackSize = size; + } + } + } + if (compute != COMPUTE_NOTHING) { + int currentMaxLocals; + if (opcode == Opcodes.LLOAD + || opcode == Opcodes.DLOAD + || opcode == Opcodes.LSTORE + || opcode == Opcodes.DSTORE) { + currentMaxLocals = var + 2; + } else { + currentMaxLocals = var + 1; + } + if (currentMaxLocals > maxLocals) { + maxLocals = currentMaxLocals; + } + } + if (opcode >= Opcodes.ISTORE && compute == COMPUTE_ALL_FRAMES && firstHandler != null) { + // If there are exception handler blocks, each instruction within a handler range is, in + // theory, a basic block (since execution can jump from this instruction to the exception + // handler). As a consequence, the local variable types at the beginning of the handler + // block should be the merge of the local variable types at all the instructions within the + // handler range. However, instead of creating a basic block for each instruction, we can + // get the same result in a more efficient way. Namely, by starting a new basic block after + // each xSTORE instruction, which is what we do here. + visitLabel(new Label()); + } + } + + @Override + public void visitTypeInsn(final int opcode, final String type) { + lastBytecodeOffset = code.length; + // Add the instruction to the bytecode of the method. + Symbol typeSymbol = symbolTable.addConstantClass(type); + code.put12(opcode, typeSymbol.index); + // If needed, update the maximum stack size and number of locals, and stack map frames. + if (currentBasicBlock != null) { + if (compute == COMPUTE_ALL_FRAMES || compute == COMPUTE_INSERTED_FRAMES) { + currentBasicBlock.frame.execute(opcode, lastBytecodeOffset, typeSymbol, symbolTable); + } else if (opcode == Opcodes.NEW) { + // The stack size delta is 1 for NEW, and 0 for ANEWARRAY, CHECKCAST, or INSTANCEOF. + int size = relativeStackSize + 1; + if (size > maxRelativeStackSize) { + maxRelativeStackSize = size; + } + relativeStackSize = size; + } + } + } + + @Override + public void visitFieldInsn( + final int opcode, final String owner, final String name, final String descriptor) { + lastBytecodeOffset = code.length; + // Add the instruction to the bytecode of the method. + Symbol fieldrefSymbol = symbolTable.addConstantFieldref(owner, name, descriptor); + code.put12(opcode, fieldrefSymbol.index); + // If needed, update the maximum stack size and number of locals, and stack map frames. + if (currentBasicBlock != null) { + if (compute == COMPUTE_ALL_FRAMES || compute == COMPUTE_INSERTED_FRAMES) { + currentBasicBlock.frame.execute(opcode, 0, fieldrefSymbol, symbolTable); + } else { + int size; + char firstDescChar = descriptor.charAt(0); + switch (opcode) { + case Opcodes.GETSTATIC: + size = relativeStackSize + (firstDescChar == 'D' || firstDescChar == 'J' ? 2 : 1); + break; + case Opcodes.PUTSTATIC: + size = relativeStackSize + (firstDescChar == 'D' || firstDescChar == 'J' ? -2 : -1); + break; + case Opcodes.GETFIELD: + size = relativeStackSize + (firstDescChar == 'D' || firstDescChar == 'J' ? 1 : 0); + break; + case Opcodes.PUTFIELD: + default: + size = relativeStackSize + (firstDescChar == 'D' || firstDescChar == 'J' ? -3 : -2); + break; + } + if (size > maxRelativeStackSize) { + maxRelativeStackSize = size; + } + relativeStackSize = size; + } + } + } + + @Override + public void visitMethodInsn( + final int opcode, + final String owner, + final String name, + final String descriptor, + final boolean isInterface) { + lastBytecodeOffset = code.length; + // Add the instruction to the bytecode of the method. + Symbol methodrefSymbol = symbolTable.addConstantMethodref(owner, name, descriptor, isInterface); + if (opcode == Opcodes.INVOKEINTERFACE) { + code.put12(Opcodes.INVOKEINTERFACE, methodrefSymbol.index) + .put11(methodrefSymbol.getArgumentsAndReturnSizes() >> 2, 0); + } else { + code.put12(opcode, methodrefSymbol.index); + } + // If needed, update the maximum stack size and number of locals, and stack map frames. + if (currentBasicBlock != null) { + if (compute == COMPUTE_ALL_FRAMES || compute == COMPUTE_INSERTED_FRAMES) { + currentBasicBlock.frame.execute(opcode, 0, methodrefSymbol, symbolTable); + } else { + int argumentsAndReturnSize = methodrefSymbol.getArgumentsAndReturnSizes(); + int stackSizeDelta = (argumentsAndReturnSize & 3) - (argumentsAndReturnSize >> 2); + int size; + if (opcode == Opcodes.INVOKESTATIC) { + size = relativeStackSize + stackSizeDelta + 1; + } else { + size = relativeStackSize + stackSizeDelta; + } + if (size > maxRelativeStackSize) { + maxRelativeStackSize = size; + } + relativeStackSize = size; + } + } + } + + @Override + public void visitInvokeDynamicInsn( + final String name, + final String descriptor, + final Handle bootstrapMethodHandle, + final Object... bootstrapMethodArguments) { + lastBytecodeOffset = code.length; + // Add the instruction to the bytecode of the method. + Symbol invokeDynamicSymbol = + symbolTable.addConstantInvokeDynamic( + name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments); + code.put12(Opcodes.INVOKEDYNAMIC, invokeDynamicSymbol.index); + code.putShort(0); + // If needed, update the maximum stack size and number of locals, and stack map frames. + if (currentBasicBlock != null) { + if (compute == COMPUTE_ALL_FRAMES || compute == COMPUTE_INSERTED_FRAMES) { + currentBasicBlock.frame.execute(Opcodes.INVOKEDYNAMIC, 0, invokeDynamicSymbol, symbolTable); + } else { + int argumentsAndReturnSize = invokeDynamicSymbol.getArgumentsAndReturnSizes(); + int stackSizeDelta = (argumentsAndReturnSize & 3) - (argumentsAndReturnSize >> 2) + 1; + int size = relativeStackSize + stackSizeDelta; + if (size > maxRelativeStackSize) { + maxRelativeStackSize = size; + } + relativeStackSize = size; + } + } + } + + @Override + public void visitJumpInsn(final int opcode, final Label label) { + lastBytecodeOffset = code.length; + // Add the instruction to the bytecode of the method. + // Compute the 'base' opcode, i.e. GOTO or JSR if opcode is GOTO_W or JSR_W, otherwise opcode. + int baseOpcode = + opcode >= Constants.GOTO_W ? opcode - Constants.WIDE_JUMP_OPCODE_DELTA : opcode; + boolean nextInsnIsJumpTarget = false; + if ((label.flags & Label.FLAG_RESOLVED) != 0 + && label.bytecodeOffset - code.length < Short.MIN_VALUE) { + // Case of a backward jump with an offset < -32768. In this case we automatically replace GOTO + // with GOTO_W, JSR with JSR_W and IFxxx with IFNOTxxx GOTO_W L:..., where + // IFNOTxxx is the "opposite" opcode of IFxxx (e.g. IFNE for IFEQ) and where designates + // the instruction just after the GOTO_W. + if (baseOpcode == Opcodes.GOTO) { + code.putByte(Constants.GOTO_W); + } else if (baseOpcode == Opcodes.JSR) { + code.putByte(Constants.JSR_W); + } else { + // Put the "opposite" opcode of baseOpcode. This can be done by flipping the least + // significant bit for IFNULL and IFNONNULL, and similarly for IFEQ ... IF_ACMPEQ (with a + // pre and post offset by 1). The jump offset is 8 bytes (3 for IFNOTxxx, 5 for GOTO_W). + code.putByte(baseOpcode >= Opcodes.IFNULL ? baseOpcode ^ 1 : ((baseOpcode + 1) ^ 1) - 1); + code.putShort(8); + // Here we could put a GOTO_W in theory, but if ASM specific instructions are used in this + // method or another one, and if the class has frames, we will need to insert a frame after + // this GOTO_W during the additional ClassReader -> ClassWriter round trip to remove the ASM + // specific instructions. To not miss this additional frame, we need to use an ASM_GOTO_W + // here, which has the unfortunate effect of forcing this additional round trip (which in + // some case would not have been really necessary, but we can't know this at this point). + code.putByte(Constants.ASM_GOTO_W); + hasAsmInstructions = true; + // The instruction after the GOTO_W becomes the target of the IFNOT instruction. + nextInsnIsJumpTarget = true; + } + label.put(code, code.length - 1, true); + } else if (baseOpcode != opcode) { + // Case of a GOTO_W or JSR_W specified by the user (normally ClassReader when used to remove + // ASM specific instructions). In this case we keep the original instruction. + code.putByte(opcode); + label.put(code, code.length - 1, true); + } else { + // Case of a jump with an offset >= -32768, or of a jump with an unknown offset. In these + // cases we store the offset in 2 bytes (which will be increased via a ClassReader -> + // ClassWriter round trip if it turns out that 2 bytes are not sufficient). + code.putByte(baseOpcode); + label.put(code, code.length - 1, false); + } + + // If needed, update the maximum stack size and number of locals, and stack map frames. + if (currentBasicBlock != null) { + Label nextBasicBlock = null; + if (compute == COMPUTE_ALL_FRAMES) { + currentBasicBlock.frame.execute(baseOpcode, 0, null, null); + // Record the fact that 'label' is the target of a jump instruction. + label.getCanonicalInstance().flags |= Label.FLAG_JUMP_TARGET; + // Add 'label' as a successor of the current basic block. + addSuccessorToCurrentBasicBlock(Edge.JUMP, label); + if (baseOpcode != Opcodes.GOTO) { + // The next instruction starts a new basic block (except for GOTO: by default the code + // following a goto is unreachable - unless there is an explicit label for it - and we + // should not compute stack frame types for its instructions). + nextBasicBlock = new Label(); + } + } else if (compute == COMPUTE_INSERTED_FRAMES) { + currentBasicBlock.frame.execute(baseOpcode, 0, null, null); + } else if (compute == COMPUTE_MAX_STACK_AND_LOCAL_FROM_FRAMES) { + // No need to update maxRelativeStackSize (the stack size delta is always negative). + relativeStackSize += STACK_SIZE_DELTA[baseOpcode]; + } else { + if (baseOpcode == Opcodes.JSR) { + // Record the fact that 'label' designates a subroutine, if not already done. + if ((label.flags & Label.FLAG_SUBROUTINE_START) == 0) { + label.flags |= Label.FLAG_SUBROUTINE_START; + hasSubroutines = true; + } + currentBasicBlock.flags |= Label.FLAG_SUBROUTINE_CALLER; + // Note that, by construction in this method, a block which calls a subroutine has at + // least two successors in the control flow graph: the first one (added below) leads to + // the instruction after the JSR, while the second one (added here) leads to the JSR + // target. Note that the first successor is virtual (it does not correspond to a possible + // execution path): it is only used to compute the successors of the basic blocks ending + // with a ret, in {@link Label#addSubroutineRetSuccessors}. + addSuccessorToCurrentBasicBlock(relativeStackSize + 1, label); + // The instruction after the JSR starts a new basic block. + nextBasicBlock = new Label(); + } else { + // No need to update maxRelativeStackSize (the stack size delta is always negative). + relativeStackSize += STACK_SIZE_DELTA[baseOpcode]; + addSuccessorToCurrentBasicBlock(relativeStackSize, label); + } + } + // If the next instruction starts a new basic block, call visitLabel to add the label of this + // instruction as a successor of the current block, and to start a new basic block. + if (nextBasicBlock != null) { + if (nextInsnIsJumpTarget) { + nextBasicBlock.flags |= Label.FLAG_JUMP_TARGET; + } + visitLabel(nextBasicBlock); + } + if (baseOpcode == Opcodes.GOTO) { + endCurrentBasicBlockWithNoSuccessor(); + } + } + } + + @Override + public void visitLabel(final Label label) { + // Resolve the forward references to this label, if any. + hasAsmInstructions |= label.resolve(code.data, code.length); + // visitLabel starts a new basic block (except for debug only labels), so we need to update the + // previous and current block references and list of successors. + if ((label.flags & Label.FLAG_DEBUG_ONLY) != 0) { + return; + } + if (compute == COMPUTE_ALL_FRAMES) { + if (currentBasicBlock != null) { + if (label.bytecodeOffset == currentBasicBlock.bytecodeOffset) { + // We use {@link Label#getCanonicalInstance} to store the state of a basic block in only + // one place, but this does not work for labels which have not been visited yet. + // Therefore, when we detect here two labels having the same bytecode offset, we need to + // - consolidate the state scattered in these two instances into the canonical instance: + currentBasicBlock.flags |= (label.flags & Label.FLAG_JUMP_TARGET); + // - make sure the two instances share the same Frame instance (the implementation of + // {@link Label#getCanonicalInstance} relies on this property; here label.frame should be + // null): + label.frame = currentBasicBlock.frame; + // - and make sure to NOT assign 'label' into 'currentBasicBlock' or 'lastBasicBlock', so + // that they still refer to the canonical instance for this bytecode offset. + return; + } + // End the current basic block (with one new successor). + addSuccessorToCurrentBasicBlock(Edge.JUMP, label); + } + // Append 'label' at the end of the basic block list. + if (lastBasicBlock != null) { + if (label.bytecodeOffset == lastBasicBlock.bytecodeOffset) { + // Same comment as above. + lastBasicBlock.flags |= (label.flags & Label.FLAG_JUMP_TARGET); + // Here label.frame should be null. + label.frame = lastBasicBlock.frame; + currentBasicBlock = lastBasicBlock; + return; + } + lastBasicBlock.nextBasicBlock = label; + } + lastBasicBlock = label; + // Make it the new current basic block. + currentBasicBlock = label; + // Here label.frame should be null. + label.frame = new Frame(label); + } else if (compute == COMPUTE_INSERTED_FRAMES) { + if (currentBasicBlock == null) { + // This case should happen only once, for the visitLabel call in the constructor. Indeed, if + // compute is equal to COMPUTE_INSERTED_FRAMES, currentBasicBlock stays unchanged. + currentBasicBlock = label; + } else { + // Update the frame owner so that a correct frame offset is computed in Frame.accept(). + currentBasicBlock.frame.owner = label; + } + } else if (compute == COMPUTE_MAX_STACK_AND_LOCAL) { + if (currentBasicBlock != null) { + // End the current basic block (with one new successor). + currentBasicBlock.outputStackMax = (short) maxRelativeStackSize; + addSuccessorToCurrentBasicBlock(relativeStackSize, label); + } + // Start a new current basic block, and reset the current and maximum relative stack sizes. + currentBasicBlock = label; + relativeStackSize = 0; + maxRelativeStackSize = 0; + // Append the new basic block at the end of the basic block list. + if (lastBasicBlock != null) { + lastBasicBlock.nextBasicBlock = label; + } + lastBasicBlock = label; + } else if (compute == COMPUTE_MAX_STACK_AND_LOCAL_FROM_FRAMES && currentBasicBlock == null) { + // This case should happen only once, for the visitLabel call in the constructor. Indeed, if + // compute is equal to COMPUTE_MAX_STACK_AND_LOCAL_FROM_FRAMES, currentBasicBlock stays + // unchanged. + currentBasicBlock = label; + } + } + + @Override + public void visitLdcInsn(final Object value) { + lastBytecodeOffset = code.length; + // Add the instruction to the bytecode of the method. + Symbol constantSymbol = symbolTable.addConstant(value); + int constantIndex = constantSymbol.index; + char firstDescriptorChar; + boolean isLongOrDouble = + constantSymbol.tag == Symbol.CONSTANT_LONG_TAG + || constantSymbol.tag == Symbol.CONSTANT_DOUBLE_TAG + || (constantSymbol.tag == Symbol.CONSTANT_DYNAMIC_TAG + && ((firstDescriptorChar = constantSymbol.value.charAt(0)) == 'J' + || firstDescriptorChar == 'D')); + if (isLongOrDouble) { + code.put12(Constants.LDC2_W, constantIndex); + } else if (constantIndex >= 256) { + code.put12(Constants.LDC_W, constantIndex); + } else { + code.put11(Opcodes.LDC, constantIndex); + } + // If needed, update the maximum stack size and number of locals, and stack map frames. + if (currentBasicBlock != null) { + if (compute == COMPUTE_ALL_FRAMES || compute == COMPUTE_INSERTED_FRAMES) { + currentBasicBlock.frame.execute(Opcodes.LDC, 0, constantSymbol, symbolTable); + } else { + int size = relativeStackSize + (isLongOrDouble ? 2 : 1); + if (size > maxRelativeStackSize) { + maxRelativeStackSize = size; + } + relativeStackSize = size; + } + } + } + + @Override + public void visitIincInsn(final int var, final int increment) { + lastBytecodeOffset = code.length; + // Add the instruction to the bytecode of the method. + if ((var > 255) || (increment > 127) || (increment < -128)) { + code.putByte(Constants.WIDE).put12(Opcodes.IINC, var).putShort(increment); + } else { + code.putByte(Opcodes.IINC).put11(var, increment); + } + // If needed, update the maximum stack size and number of locals, and stack map frames. + if (currentBasicBlock != null + && (compute == COMPUTE_ALL_FRAMES || compute == COMPUTE_INSERTED_FRAMES)) { + currentBasicBlock.frame.execute(Opcodes.IINC, var, null, null); + } + if (compute != COMPUTE_NOTHING) { + int currentMaxLocals = var + 1; + if (currentMaxLocals > maxLocals) { + maxLocals = currentMaxLocals; + } + } + } + + @Override + public void visitTableSwitchInsn( + final int min, final int max, final Label dflt, final Label... labels) { + lastBytecodeOffset = code.length; + // Add the instruction to the bytecode of the method. + code.putByte(Opcodes.TABLESWITCH).putByteArray(null, 0, (4 - code.length % 4) % 4); + dflt.put(code, lastBytecodeOffset, true); + code.putInt(min).putInt(max); + for (Label label : labels) { + label.put(code, lastBytecodeOffset, true); + } + // If needed, update the maximum stack size and number of locals, and stack map frames. + visitSwitchInsn(dflt, labels); + } + + @Override + public void visitLookupSwitchInsn(final Label dflt, final int[] keys, final Label[] labels) { + lastBytecodeOffset = code.length; + // Add the instruction to the bytecode of the method. + code.putByte(Opcodes.LOOKUPSWITCH).putByteArray(null, 0, (4 - code.length % 4) % 4); + dflt.put(code, lastBytecodeOffset, true); + code.putInt(labels.length); + for (int i = 0; i < labels.length; ++i) { + code.putInt(keys[i]); + labels[i].put(code, lastBytecodeOffset, true); + } + // If needed, update the maximum stack size and number of locals, and stack map frames. + visitSwitchInsn(dflt, labels); + } + + private void visitSwitchInsn(final Label dflt, final Label[] labels) { + if (currentBasicBlock != null) { + if (compute == COMPUTE_ALL_FRAMES) { + currentBasicBlock.frame.execute(Opcodes.LOOKUPSWITCH, 0, null, null); + // Add all the labels as successors of the current basic block. + addSuccessorToCurrentBasicBlock(Edge.JUMP, dflt); + dflt.getCanonicalInstance().flags |= Label.FLAG_JUMP_TARGET; + for (Label label : labels) { + addSuccessorToCurrentBasicBlock(Edge.JUMP, label); + label.getCanonicalInstance().flags |= Label.FLAG_JUMP_TARGET; + } + } else if (compute == COMPUTE_MAX_STACK_AND_LOCAL) { + // No need to update maxRelativeStackSize (the stack size delta is always negative). + --relativeStackSize; + // Add all the labels as successors of the current basic block. + addSuccessorToCurrentBasicBlock(relativeStackSize, dflt); + for (Label label : labels) { + addSuccessorToCurrentBasicBlock(relativeStackSize, label); + } + } + // End the current basic block. + endCurrentBasicBlockWithNoSuccessor(); + } + } + + @Override + public void visitMultiANewArrayInsn(final String descriptor, final int numDimensions) { + lastBytecodeOffset = code.length; + // Add the instruction to the bytecode of the method. + Symbol descSymbol = symbolTable.addConstantClass(descriptor); + code.put12(Opcodes.MULTIANEWARRAY, descSymbol.index).putByte(numDimensions); + // If needed, update the maximum stack size and number of locals, and stack map frames. + if (currentBasicBlock != null) { + if (compute == COMPUTE_ALL_FRAMES || compute == COMPUTE_INSERTED_FRAMES) { + currentBasicBlock.frame.execute( + Opcodes.MULTIANEWARRAY, numDimensions, descSymbol, symbolTable); + } else { + // No need to update maxRelativeStackSize (the stack size delta is always negative). + relativeStackSize += 1 - numDimensions; + } + } + } + + @Override + public AnnotationVisitor visitInsnAnnotation( + final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) { + if (visible) { + return lastCodeRuntimeVisibleTypeAnnotation = + AnnotationWriter.create( + symbolTable, + (typeRef & 0xFF0000FF) | (lastBytecodeOffset << 8), + typePath, + descriptor, + lastCodeRuntimeVisibleTypeAnnotation); + } else { + return lastCodeRuntimeInvisibleTypeAnnotation = + AnnotationWriter.create( + symbolTable, + (typeRef & 0xFF0000FF) | (lastBytecodeOffset << 8), + typePath, + descriptor, + lastCodeRuntimeInvisibleTypeAnnotation); + } + } + + @Override + public void visitTryCatchBlock( + final Label start, final Label end, final Label handler, final String type) { + Handler newHandler = + new Handler( + start, end, handler, type != null ? symbolTable.addConstantClass(type).index : 0, type); + if (firstHandler == null) { + firstHandler = newHandler; + } else { + lastHandler.nextHandler = newHandler; + } + lastHandler = newHandler; + } + + @Override + public AnnotationVisitor visitTryCatchAnnotation( + final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) { + if (visible) { + return lastCodeRuntimeVisibleTypeAnnotation = + AnnotationWriter.create( + symbolTable, typeRef, typePath, descriptor, lastCodeRuntimeVisibleTypeAnnotation); + } else { + return lastCodeRuntimeInvisibleTypeAnnotation = + AnnotationWriter.create( + symbolTable, typeRef, typePath, descriptor, lastCodeRuntimeInvisibleTypeAnnotation); + } + } + + @Override + public void visitLocalVariable( + final String name, + final String descriptor, + final String signature, + final Label start, + final Label end, + final int index) { + if (signature != null) { + if (localVariableTypeTable == null) { + localVariableTypeTable = new ByteVector(); + } + ++localVariableTypeTableLength; + localVariableTypeTable + .putShort(start.bytecodeOffset) + .putShort(end.bytecodeOffset - start.bytecodeOffset) + .putShort(symbolTable.addConstantUtf8(name)) + .putShort(symbolTable.addConstantUtf8(signature)) + .putShort(index); + } + if (localVariableTable == null) { + localVariableTable = new ByteVector(); + } + ++localVariableTableLength; + localVariableTable + .putShort(start.bytecodeOffset) + .putShort(end.bytecodeOffset - start.bytecodeOffset) + .putShort(symbolTable.addConstantUtf8(name)) + .putShort(symbolTable.addConstantUtf8(descriptor)) + .putShort(index); + if (compute != COMPUTE_NOTHING) { + char firstDescChar = descriptor.charAt(0); + int currentMaxLocals = index + (firstDescChar == 'J' || firstDescChar == 'D' ? 2 : 1); + if (currentMaxLocals > maxLocals) { + maxLocals = currentMaxLocals; + } + } + } + + @Override + public AnnotationVisitor visitLocalVariableAnnotation( + final int typeRef, + final TypePath typePath, + final Label[] start, + final Label[] end, + final int[] index, + final String descriptor, + final boolean visible) { + // Create a ByteVector to hold a 'type_annotation' JVMS structure. + // See https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.7.20. + ByteVector typeAnnotation = new ByteVector(); + // Write target_type, target_info, and target_path. + typeAnnotation.putByte(typeRef >>> 24).putShort(start.length); + for (int i = 0; i < start.length; ++i) { + typeAnnotation + .putShort(start[i].bytecodeOffset) + .putShort(end[i].bytecodeOffset - start[i].bytecodeOffset) + .putShort(index[i]); + } + TypePath.put(typePath, typeAnnotation); + // Write type_index and reserve space for num_element_value_pairs. + typeAnnotation.putShort(symbolTable.addConstantUtf8(descriptor)).putShort(0); + if (visible) { + return lastCodeRuntimeVisibleTypeAnnotation = + new AnnotationWriter( + symbolTable, + /* useNamedValues = */ true, + typeAnnotation, + lastCodeRuntimeVisibleTypeAnnotation); + } else { + return lastCodeRuntimeInvisibleTypeAnnotation = + new AnnotationWriter( + symbolTable, + /* useNamedValues = */ true, + typeAnnotation, + lastCodeRuntimeInvisibleTypeAnnotation); + } + } + + @Override + public void visitLineNumber(final int line, final Label start) { + if (lineNumberTable == null) { + lineNumberTable = new ByteVector(); + } + ++lineNumberTableLength; + lineNumberTable.putShort(start.bytecodeOffset); + lineNumberTable.putShort(line); + } + + @Override + public void visitMaxs(final int maxStack, final int maxLocals) { + if (compute == COMPUTE_ALL_FRAMES) { + computeAllFrames(); + } else if (compute == COMPUTE_MAX_STACK_AND_LOCAL) { + computeMaxStackAndLocal(); + } else if (compute == COMPUTE_MAX_STACK_AND_LOCAL_FROM_FRAMES) { + this.maxStack = maxRelativeStackSize; + } else { + this.maxStack = maxStack; + this.maxLocals = maxLocals; + } + } + + /** Computes all the stack map frames of the method, from scratch. */ + private void computeAllFrames() { + // Complete the control flow graph with exception handler blocks. + Handler handler = firstHandler; + while (handler != null) { + String catchTypeDescriptor = + handler.catchTypeDescriptor == null ? "java/lang/Throwable" : handler.catchTypeDescriptor; + int catchType = Frame.getAbstractTypeFromInternalName(symbolTable, catchTypeDescriptor); + // Mark handlerBlock as an exception handler. + Label handlerBlock = handler.handlerPc.getCanonicalInstance(); + handlerBlock.flags |= Label.FLAG_JUMP_TARGET; + // Add handlerBlock as a successor of all the basic blocks in the exception handler range. + Label handlerRangeBlock = handler.startPc.getCanonicalInstance(); + Label handlerRangeEnd = handler.endPc.getCanonicalInstance(); + while (handlerRangeBlock != handlerRangeEnd) { + handlerRangeBlock.outgoingEdges = + new Edge(catchType, handlerBlock, handlerRangeBlock.outgoingEdges); + handlerRangeBlock = handlerRangeBlock.nextBasicBlock; + } + handler = handler.nextHandler; + } + + // Create and visit the first (implicit) frame. + Frame firstFrame = firstBasicBlock.frame; + firstFrame.setInputFrameFromDescriptor(symbolTable, accessFlags, descriptor, this.maxLocals); + firstFrame.accept(this); + + // Fix point algorithm: add the first basic block to a list of blocks to process (i.e. blocks + // whose stack map frame has changed) and, while there are blocks to process, remove one from + // the list and update the stack map frames of its successor blocks in the control flow graph + // (which might change them, in which case these blocks must be processed too, and are thus + // added to the list of blocks to process). Also compute the maximum stack size of the method, + // as a by-product. + Label listOfBlocksToProcess = firstBasicBlock; + listOfBlocksToProcess.nextListElement = Label.EMPTY_LIST; + int maxStackSize = 0; + while (listOfBlocksToProcess != Label.EMPTY_LIST) { + // Remove a basic block from the list of blocks to process. + Label basicBlock = listOfBlocksToProcess; + listOfBlocksToProcess = listOfBlocksToProcess.nextListElement; + basicBlock.nextListElement = null; + // By definition, basicBlock is reachable. + basicBlock.flags |= Label.FLAG_REACHABLE; + // Update the (absolute) maximum stack size. + int maxBlockStackSize = basicBlock.frame.getInputStackSize() + basicBlock.outputStackMax; + if (maxBlockStackSize > maxStackSize) { + maxStackSize = maxBlockStackSize; + } + // Update the successor blocks of basicBlock in the control flow graph. + Edge outgoingEdge = basicBlock.outgoingEdges; + while (outgoingEdge != null) { + Label successorBlock = outgoingEdge.successor.getCanonicalInstance(); + boolean successorBlockChanged = + basicBlock.frame.merge(symbolTable, successorBlock.frame, outgoingEdge.info); + if (successorBlockChanged && successorBlock.nextListElement == null) { + // If successorBlock has changed it must be processed. Thus, if it is not already in the + // list of blocks to process, add it to this list. + successorBlock.nextListElement = listOfBlocksToProcess; + listOfBlocksToProcess = successorBlock; + } + outgoingEdge = outgoingEdge.nextEdge; + } + } + + // Loop over all the basic blocks and visit the stack map frames that must be stored in the + // StackMapTable attribute. Also replace unreachable code with NOP* ATHROW, and remove it from + // exception handler ranges. + Label basicBlock = firstBasicBlock; + while (basicBlock != null) { + if ((basicBlock.flags & (Label.FLAG_JUMP_TARGET | Label.FLAG_REACHABLE)) + == (Label.FLAG_JUMP_TARGET | Label.FLAG_REACHABLE)) { + basicBlock.frame.accept(this); + } + if ((basicBlock.flags & Label.FLAG_REACHABLE) == 0) { + // Find the start and end bytecode offsets of this unreachable block. + Label nextBasicBlock = basicBlock.nextBasicBlock; + int startOffset = basicBlock.bytecodeOffset; + int endOffset = (nextBasicBlock == null ? code.length : nextBasicBlock.bytecodeOffset) - 1; + if (endOffset >= startOffset) { + // Replace its instructions with NOP ... NOP ATHROW. + for (int i = startOffset; i < endOffset; ++i) { + code.data[i] = Opcodes.NOP; + } + code.data[endOffset] = (byte) Opcodes.ATHROW; + // Emit a frame for this unreachable block, with no local and a Throwable on the stack + // (so that the ATHROW could consume this Throwable if it were reachable). + int frameIndex = visitFrameStart(startOffset, /* numLocal = */ 0, /* numStack = */ 1); + currentFrame[frameIndex] = + Frame.getAbstractTypeFromInternalName(symbolTable, "java/lang/Throwable"); + visitFrameEnd(); + // Remove this unreachable basic block from the exception handler ranges. + firstHandler = Handler.removeRange(firstHandler, basicBlock, nextBasicBlock); + // The maximum stack size is now at least one, because of the Throwable declared above. + maxStackSize = Math.max(maxStackSize, 1); + } + } + basicBlock = basicBlock.nextBasicBlock; + } + + this.maxStack = maxStackSize; + } + + /** Computes the maximum stack size of the method. */ + private void computeMaxStackAndLocal() { + // Complete the control flow graph with exception handler blocks. + Handler handler = firstHandler; + while (handler != null) { + Label handlerBlock = handler.handlerPc; + Label handlerRangeBlock = handler.startPc; + Label handlerRangeEnd = handler.endPc; + // Add handlerBlock as a successor of all the basic blocks in the exception handler range. + while (handlerRangeBlock != handlerRangeEnd) { + if ((handlerRangeBlock.flags & Label.FLAG_SUBROUTINE_CALLER) == 0) { + handlerRangeBlock.outgoingEdges = + new Edge(Edge.EXCEPTION, handlerBlock, handlerRangeBlock.outgoingEdges); + } else { + // If handlerRangeBlock is a JSR block, add handlerBlock after the first two outgoing + // edges to preserve the hypothesis about JSR block successors order (see + // {@link #visitJumpInsn}). + handlerRangeBlock.outgoingEdges.nextEdge.nextEdge = + new Edge( + Edge.EXCEPTION, handlerBlock, handlerRangeBlock.outgoingEdges.nextEdge.nextEdge); + } + handlerRangeBlock = handlerRangeBlock.nextBasicBlock; + } + handler = handler.nextHandler; + } + + // Complete the control flow graph with the successor blocks of subroutines, if needed. + if (hasSubroutines) { + // First step: find the subroutines. This step determines, for each basic block, to which + // subroutine(s) it belongs. Start with the main "subroutine": + short numSubroutines = 1; + firstBasicBlock.markSubroutine(numSubroutines); + // Then, mark the subroutines called by the main subroutine, then the subroutines called by + // those called by the main subroutine, etc. + for (short currentSubroutine = 1; currentSubroutine <= numSubroutines; ++currentSubroutine) { + Label basicBlock = firstBasicBlock; + while (basicBlock != null) { + if ((basicBlock.flags & Label.FLAG_SUBROUTINE_CALLER) != 0 + && basicBlock.subroutineId == currentSubroutine) { + Label jsrTarget = basicBlock.outgoingEdges.nextEdge.successor; + if (jsrTarget.subroutineId == 0) { + // If this subroutine has not been marked yet, find its basic blocks. + jsrTarget.markSubroutine(++numSubroutines); + } + } + basicBlock = basicBlock.nextBasicBlock; + } + } + // Second step: find the successors in the control flow graph of each subroutine basic block + // 'r' ending with a RET instruction. These successors are the virtual successors of the basic + // blocks ending with JSR instructions (see {@link #visitJumpInsn)} that can reach 'r'. + Label basicBlock = firstBasicBlock; + while (basicBlock != null) { + if ((basicBlock.flags & Label.FLAG_SUBROUTINE_CALLER) != 0) { + // By construction, jsr targets are stored in the second outgoing edge of basic blocks + // that ends with a jsr instruction (see {@link #FLAG_SUBROUTINE_CALLER}). + Label subroutine = basicBlock.outgoingEdges.nextEdge.successor; + subroutine.addSubroutineRetSuccessors(basicBlock); + } + basicBlock = basicBlock.nextBasicBlock; + } + } + + // Data flow algorithm: put the first basic block in a list of blocks to process (i.e. blocks + // whose input stack size has changed) and, while there are blocks to process, remove one + // from the list, update the input stack size of its successor blocks in the control flow + // graph, and add these blocks to the list of blocks to process (if not already done). + Label listOfBlocksToProcess = firstBasicBlock; + listOfBlocksToProcess.nextListElement = Label.EMPTY_LIST; + int maxStackSize = maxStack; + while (listOfBlocksToProcess != Label.EMPTY_LIST) { + // Remove a basic block from the list of blocks to process. Note that we don't reset + // basicBlock.nextListElement to null on purpose, to make sure we don't reprocess already + // processed basic blocks. + Label basicBlock = listOfBlocksToProcess; + listOfBlocksToProcess = listOfBlocksToProcess.nextListElement; + // Compute the (absolute) input stack size and maximum stack size of this block. + int inputStackTop = basicBlock.inputStackSize; + int maxBlockStackSize = inputStackTop + basicBlock.outputStackMax; + // Update the absolute maximum stack size of the method. + if (maxBlockStackSize > maxStackSize) { + maxStackSize = maxBlockStackSize; + } + // Update the input stack size of the successor blocks of basicBlock in the control flow + // graph, and add these blocks to the list of blocks to process, if not already done. + Edge outgoingEdge = basicBlock.outgoingEdges; + if ((basicBlock.flags & Label.FLAG_SUBROUTINE_CALLER) != 0) { + // Ignore the first outgoing edge of the basic blocks ending with a jsr: these are virtual + // edges which lead to the instruction just after the jsr, and do not correspond to a + // possible execution path (see {@link #visitJumpInsn} and + // {@link Label#FLAG_SUBROUTINE_CALLER}). + outgoingEdge = outgoingEdge.nextEdge; + } + while (outgoingEdge != null) { + Label successorBlock = outgoingEdge.successor; + if (successorBlock.nextListElement == null) { + successorBlock.inputStackSize = + (short) (outgoingEdge.info == Edge.EXCEPTION ? 1 : inputStackTop + outgoingEdge.info); + successorBlock.nextListElement = listOfBlocksToProcess; + listOfBlocksToProcess = successorBlock; + } + outgoingEdge = outgoingEdge.nextEdge; + } + } + this.maxStack = maxStackSize; + } + + @Override + public void visitEnd() { + // Nothing to do. + } + + // ----------------------------------------------------------------------------------------------- + // Utility methods: control flow analysis algorithm + // ----------------------------------------------------------------------------------------------- + + /** + * Adds a successor to {@link #currentBasicBlock} in the control flow graph. + * + * @param info information about the control flow edge to be added. + * @param successor the successor block to be added to the current basic block. + */ + private void addSuccessorToCurrentBasicBlock(final int info, final Label successor) { + currentBasicBlock.outgoingEdges = new Edge(info, successor, currentBasicBlock.outgoingEdges); + } + + /** + * Ends the current basic block. This method must be used in the case where the current basic + * block does not have any successor. + * + *

    WARNING: this method must be called after the currently visited instruction has been put in + * {@link #code} (if frames are computed, this method inserts a new Label to start a new basic + * block after the current instruction). + */ + private void endCurrentBasicBlockWithNoSuccessor() { + if (compute == COMPUTE_ALL_FRAMES) { + Label nextBasicBlock = new Label(); + nextBasicBlock.frame = new Frame(nextBasicBlock); + nextBasicBlock.resolve(code.data, code.length); + lastBasicBlock.nextBasicBlock = nextBasicBlock; + lastBasicBlock = nextBasicBlock; + currentBasicBlock = null; + } else if (compute == COMPUTE_MAX_STACK_AND_LOCAL) { + currentBasicBlock.outputStackMax = (short) maxRelativeStackSize; + currentBasicBlock = null; + } + } + + // ----------------------------------------------------------------------------------------------- + // Utility methods: stack map frames + // ----------------------------------------------------------------------------------------------- + + /** + * Starts the visit of a new stack map frame, stored in {@link #currentFrame}. + * + * @param offset the bytecode offset of the instruction to which the frame corresponds. + * @param numLocal the number of local variables in the frame. + * @param numStack the number of stack elements in the frame. + * @return the index of the next element to be written in this frame. + */ + int visitFrameStart(final int offset, final int numLocal, final int numStack) { + int frameLength = 3 + numLocal + numStack; + if (currentFrame == null || currentFrame.length < frameLength) { + currentFrame = new int[frameLength]; + } + currentFrame[0] = offset; + currentFrame[1] = numLocal; + currentFrame[2] = numStack; + return 3; + } + + /** + * Sets an abstract type in {@link #currentFrame}. + * + * @param frameIndex the index of the element to be set in {@link #currentFrame}. + * @param abstractType an abstract type. + */ + void visitAbstractType(final int frameIndex, final int abstractType) { + currentFrame[frameIndex] = abstractType; + } + + /** + * Ends the visit of {@link #currentFrame} by writing it in the StackMapTable entries and by + * updating the StackMapTable number_of_entries (except if the current frame is the first one, + * which is implicit in StackMapTable). Then resets {@link #currentFrame} to {@literal null}. + */ + void visitFrameEnd() { + if (previousFrame != null) { + if (stackMapTableEntries == null) { + stackMapTableEntries = new ByteVector(); + } + putFrame(); + ++stackMapTableNumberOfEntries; + } + previousFrame = currentFrame; + currentFrame = null; + } + + /** Compresses and writes {@link #currentFrame} in a new StackMapTable entry. */ + private void putFrame() { + final int numLocal = currentFrame[1]; + final int numStack = currentFrame[2]; + if (symbolTable.getMajorVersion() < Opcodes.V1_6) { + // Generate a StackMap attribute entry, which are always uncompressed. + stackMapTableEntries.putShort(currentFrame[0]).putShort(numLocal); + putAbstractTypes(3, 3 + numLocal); + stackMapTableEntries.putShort(numStack); + putAbstractTypes(3 + numLocal, 3 + numLocal + numStack); + return; + } + final int offsetDelta = + stackMapTableNumberOfEntries == 0 + ? currentFrame[0] + : currentFrame[0] - previousFrame[0] - 1; + final int previousNumlocal = previousFrame[1]; + final int numLocalDelta = numLocal - previousNumlocal; + int type = Frame.FULL_FRAME; + if (numStack == 0) { + switch (numLocalDelta) { + case -3: + case -2: + case -1: + type = Frame.CHOP_FRAME; + break; + case 0: + type = offsetDelta < 64 ? Frame.SAME_FRAME : Frame.SAME_FRAME_EXTENDED; + break; + case 1: + case 2: + case 3: + type = Frame.APPEND_FRAME; + break; + default: + // Keep the FULL_FRAME type. + break; + } + } else if (numLocalDelta == 0 && numStack == 1) { + type = + offsetDelta < 63 + ? Frame.SAME_LOCALS_1_STACK_ITEM_FRAME + : Frame.SAME_LOCALS_1_STACK_ITEM_FRAME_EXTENDED; + } + if (type != Frame.FULL_FRAME) { + // Verify if locals are the same as in the previous frame. + int frameIndex = 3; + for (int i = 0; i < previousNumlocal && i < numLocal; i++) { + if (currentFrame[frameIndex] != previousFrame[frameIndex]) { + type = Frame.FULL_FRAME; + break; + } + frameIndex++; + } + } + switch (type) { + case Frame.SAME_FRAME: + stackMapTableEntries.putByte(offsetDelta); + break; + case Frame.SAME_LOCALS_1_STACK_ITEM_FRAME: + stackMapTableEntries.putByte(Frame.SAME_LOCALS_1_STACK_ITEM_FRAME + offsetDelta); + putAbstractTypes(3 + numLocal, 4 + numLocal); + break; + case Frame.SAME_LOCALS_1_STACK_ITEM_FRAME_EXTENDED: + stackMapTableEntries + .putByte(Frame.SAME_LOCALS_1_STACK_ITEM_FRAME_EXTENDED) + .putShort(offsetDelta); + putAbstractTypes(3 + numLocal, 4 + numLocal); + break; + case Frame.SAME_FRAME_EXTENDED: + stackMapTableEntries.putByte(Frame.SAME_FRAME_EXTENDED).putShort(offsetDelta); + break; + case Frame.CHOP_FRAME: + stackMapTableEntries + .putByte(Frame.SAME_FRAME_EXTENDED + numLocalDelta) + .putShort(offsetDelta); + break; + case Frame.APPEND_FRAME: + stackMapTableEntries + .putByte(Frame.SAME_FRAME_EXTENDED + numLocalDelta) + .putShort(offsetDelta); + putAbstractTypes(3 + previousNumlocal, 3 + numLocal); + break; + case Frame.FULL_FRAME: + default: + stackMapTableEntries.putByte(Frame.FULL_FRAME).putShort(offsetDelta).putShort(numLocal); + putAbstractTypes(3, 3 + numLocal); + stackMapTableEntries.putShort(numStack); + putAbstractTypes(3 + numLocal, 3 + numLocal + numStack); + break; + } + } + + /** + * Puts some abstract types of {@link #currentFrame} in {@link #stackMapTableEntries} , using the + * JVMS verification_type_info format used in StackMapTable attributes. + * + * @param start index of the first type in {@link #currentFrame} to write. + * @param end index of last type in {@link #currentFrame} to write (exclusive). + */ + private void putAbstractTypes(final int start, final int end) { + for (int i = start; i < end; ++i) { + Frame.putAbstractType(symbolTable, currentFrame[i], stackMapTableEntries); + } + } + + /** + * Puts the given public API frame element type in {@link #stackMapTableEntries} , using the JVMS + * verification_type_info format used in StackMapTable attributes. + * + * @param type a frame element type described using the same format as in {@link + * MethodVisitor#visitFrame}, i.e. either {@link Opcodes#TOP}, {@link Opcodes#INTEGER}, {@link + * Opcodes#FLOAT}, {@link Opcodes#LONG}, {@link Opcodes#DOUBLE}, {@link Opcodes#NULL}, or + * {@link Opcodes#UNINITIALIZED_THIS}, or the internal name of a class, or a Label designating + * a NEW instruction (for uninitialized types). + */ + private void putFrameType(final Object type) { + if (type instanceof Integer) { + stackMapTableEntries.putByte(((Integer) type).intValue()); + } else if (type instanceof String) { + stackMapTableEntries + .putByte(Frame.ITEM_OBJECT) + .putShort(symbolTable.addConstantClass((String) type).index); + } else { + stackMapTableEntries + .putByte(Frame.ITEM_UNINITIALIZED) + .putShort(((Label) type).bytecodeOffset); + } + } + + // ----------------------------------------------------------------------------------------------- + // Utility methods + // ----------------------------------------------------------------------------------------------- + + /** + * Returns whether the attributes of this method can be copied from the attributes of the given + * method (assuming there is no method visitor between the given ClassReader and this + * MethodWriter). This method should only be called just after this MethodWriter has been created, + * and before any content is visited. It returns true if the attributes corresponding to the + * constructor arguments (at most a Signature, an Exception, a Deprecated and a Synthetic + * attribute) are the same as the corresponding attributes in the given method. + * + * @param source the source ClassReader from which the attributes of this method might be copied. + * @param hasSyntheticAttribute whether the method_info JVMS structure from which the attributes + * of this method might be copied contains a Synthetic attribute. + * @param hasDeprecatedAttribute whether the method_info JVMS structure from which the attributes + * of this method might be copied contains a Deprecated attribute. + * @param descriptorIndex the descriptor_index field of the method_info JVMS structure from which + * the attributes of this method might be copied. + * @param signatureIndex the constant pool index contained in the Signature attribute of the + * method_info JVMS structure from which the attributes of this method might be copied, or 0. + * @param exceptionsOffset the offset in 'source.b' of the Exceptions attribute of the method_info + * JVMS structure from which the attributes of this method might be copied, or 0. + * @return whether the attributes of this method can be copied from the attributes of the + * method_info JVMS structure in 'source.b', between 'methodInfoOffset' and 'methodInfoOffset' + * + 'methodInfoLength'. + */ + boolean canCopyMethodAttributes( + final ClassReader source, + final boolean hasSyntheticAttribute, + final boolean hasDeprecatedAttribute, + final int descriptorIndex, + final int signatureIndex, + final int exceptionsOffset) { + // If the method descriptor has changed, with more locals than the max_locals field of the + // original Code attribute, if any, then the original method attributes can't be copied. A + // conservative check on the descriptor changes alone ensures this (being more precise is not + // worth the additional complexity, because these cases should be rare -- if a transform changes + // a method descriptor, most of the time it needs to change the method's code too). + if (source != symbolTable.getSource() + || descriptorIndex != this.descriptorIndex + || signatureIndex != this.signatureIndex + || hasDeprecatedAttribute != ((accessFlags & Opcodes.ACC_DEPRECATED) != 0)) { + return false; + } + boolean needSyntheticAttribute = + symbolTable.getMajorVersion() < Opcodes.V1_5 && (accessFlags & Opcodes.ACC_SYNTHETIC) != 0; + if (hasSyntheticAttribute != needSyntheticAttribute) { + return false; + } + if (exceptionsOffset == 0) { + if (numberOfExceptions != 0) { + return false; + } + } else if (source.readUnsignedShort(exceptionsOffset) == numberOfExceptions) { + int currentExceptionOffset = exceptionsOffset + 2; + for (int i = 0; i < numberOfExceptions; ++i) { + if (source.readUnsignedShort(currentExceptionOffset) != exceptionIndexTable[i]) { + return false; + } + currentExceptionOffset += 2; + } + } + return true; + } + + /** + * Sets the source from which the attributes of this method will be copied. + * + * @param methodInfoOffset the offset in 'symbolTable.getSource()' of the method_info JVMS + * structure from which the attributes of this method will be copied. + * @param methodInfoLength the length in 'symbolTable.getSource()' of the method_info JVMS + * structure from which the attributes of this method will be copied. + */ + void setMethodAttributesSource(final int methodInfoOffset, final int methodInfoLength) { + // Don't copy the attributes yet, instead store their location in the source class reader so + // they can be copied later, in {@link #putMethodInfo}. Note that we skip the 6 header bytes + // of the method_info JVMS structure. + this.sourceOffset = methodInfoOffset + 6; + this.sourceLength = methodInfoLength - 6; + } + + /** + * Returns the size of the method_info JVMS structure generated by this MethodWriter. Also add the + * names of the attributes of this method in the constant pool. + * + * @return the size in bytes of the method_info JVMS structure. + */ + int computeMethodInfoSize() { + // If this method_info must be copied from an existing one, the size computation is trivial. + if (sourceOffset != 0) { + // sourceLength excludes the first 6 bytes for access_flags, name_index and descriptor_index. + return 6 + sourceLength; + } + // 2 bytes each for access_flags, name_index, descriptor_index and attributes_count. + int size = 8; + // For ease of reference, we use here the same attribute order as in Section 4.7 of the JVMS. + if (code.length > 0) { + if (code.length > 65535) { + throw new MethodTooLargeException( + symbolTable.getClassName(), name, descriptor, code.length); + } + symbolTable.addConstantUtf8(Constants.CODE); + // The Code attribute has 6 header bytes, plus 2, 2, 4 and 2 bytes respectively for max_stack, + // max_locals, code_length and attributes_count, plus the bytecode and the exception table. + size += 16 + code.length + Handler.getExceptionTableSize(firstHandler); + if (stackMapTableEntries != null) { + boolean useStackMapTable = symbolTable.getMajorVersion() >= Opcodes.V1_6; + symbolTable.addConstantUtf8(useStackMapTable ? Constants.STACK_MAP_TABLE : "StackMap"); + // 6 header bytes and 2 bytes for number_of_entries. + size += 8 + stackMapTableEntries.length; + } + if (lineNumberTable != null) { + symbolTable.addConstantUtf8(Constants.LINE_NUMBER_TABLE); + // 6 header bytes and 2 bytes for line_number_table_length. + size += 8 + lineNumberTable.length; + } + if (localVariableTable != null) { + symbolTable.addConstantUtf8(Constants.LOCAL_VARIABLE_TABLE); + // 6 header bytes and 2 bytes for local_variable_table_length. + size += 8 + localVariableTable.length; + } + if (localVariableTypeTable != null) { + symbolTable.addConstantUtf8(Constants.LOCAL_VARIABLE_TYPE_TABLE); + // 6 header bytes and 2 bytes for local_variable_type_table_length. + size += 8 + localVariableTypeTable.length; + } + if (lastCodeRuntimeVisibleTypeAnnotation != null) { + size += + lastCodeRuntimeVisibleTypeAnnotation.computeAnnotationsSize( + Constants.RUNTIME_VISIBLE_TYPE_ANNOTATIONS); + } + if (lastCodeRuntimeInvisibleTypeAnnotation != null) { + size += + lastCodeRuntimeInvisibleTypeAnnotation.computeAnnotationsSize( + Constants.RUNTIME_INVISIBLE_TYPE_ANNOTATIONS); + } + if (firstCodeAttribute != null) { + size += + firstCodeAttribute.computeAttributesSize( + symbolTable, code.data, code.length, maxStack, maxLocals); + } + } + if (numberOfExceptions > 0) { + symbolTable.addConstantUtf8(Constants.EXCEPTIONS); + size += 8 + 2 * numberOfExceptions; + } + size += Attribute.computeAttributesSize(symbolTable, accessFlags, signatureIndex); + size += + AnnotationWriter.computeAnnotationsSize( + lastRuntimeVisibleAnnotation, + lastRuntimeInvisibleAnnotation, + lastRuntimeVisibleTypeAnnotation, + lastRuntimeInvisibleTypeAnnotation); + if (lastRuntimeVisibleParameterAnnotations != null) { + size += + AnnotationWriter.computeParameterAnnotationsSize( + Constants.RUNTIME_VISIBLE_PARAMETER_ANNOTATIONS, + lastRuntimeVisibleParameterAnnotations, + visibleAnnotableParameterCount == 0 + ? lastRuntimeVisibleParameterAnnotations.length + : visibleAnnotableParameterCount); + } + if (lastRuntimeInvisibleParameterAnnotations != null) { + size += + AnnotationWriter.computeParameterAnnotationsSize( + Constants.RUNTIME_INVISIBLE_PARAMETER_ANNOTATIONS, + lastRuntimeInvisibleParameterAnnotations, + invisibleAnnotableParameterCount == 0 + ? lastRuntimeInvisibleParameterAnnotations.length + : invisibleAnnotableParameterCount); + } + if (defaultValue != null) { + symbolTable.addConstantUtf8(Constants.ANNOTATION_DEFAULT); + size += 6 + defaultValue.length; + } + if (parameters != null) { + symbolTable.addConstantUtf8(Constants.METHOD_PARAMETERS); + // 6 header bytes and 1 byte for parameters_count. + size += 7 + parameters.length; + } + if (firstAttribute != null) { + size += firstAttribute.computeAttributesSize(symbolTable); + } + return size; + } + + /** + * Puts the content of the method_info JVMS structure generated by this MethodWriter into the + * given ByteVector. + * + * @param output where the method_info structure must be put. + */ + void putMethodInfo(final ByteVector output) { + boolean useSyntheticAttribute = symbolTable.getMajorVersion() < Opcodes.V1_5; + int mask = useSyntheticAttribute ? Opcodes.ACC_SYNTHETIC : 0; + output.putShort(accessFlags & ~mask).putShort(nameIndex).putShort(descriptorIndex); + // If this method_info must be copied from an existing one, copy it now and return early. + if (sourceOffset != 0) { + output.putByteArray(symbolTable.getSource().classFileBuffer, sourceOffset, sourceLength); + return; + } + // For ease of reference, we use here the same attribute order as in Section 4.7 of the JVMS. + int attributeCount = 0; + if (code.length > 0) { + ++attributeCount; + } + if (numberOfExceptions > 0) { + ++attributeCount; + } + if ((accessFlags & Opcodes.ACC_SYNTHETIC) != 0 && useSyntheticAttribute) { + ++attributeCount; + } + if (signatureIndex != 0) { + ++attributeCount; + } + if ((accessFlags & Opcodes.ACC_DEPRECATED) != 0) { + ++attributeCount; + } + if (lastRuntimeVisibleAnnotation != null) { + ++attributeCount; + } + if (lastRuntimeInvisibleAnnotation != null) { + ++attributeCount; + } + if (lastRuntimeVisibleParameterAnnotations != null) { + ++attributeCount; + } + if (lastRuntimeInvisibleParameterAnnotations != null) { + ++attributeCount; + } + if (lastRuntimeVisibleTypeAnnotation != null) { + ++attributeCount; + } + if (lastRuntimeInvisibleTypeAnnotation != null) { + ++attributeCount; + } + if (defaultValue != null) { + ++attributeCount; + } + if (parameters != null) { + ++attributeCount; + } + if (firstAttribute != null) { + attributeCount += firstAttribute.getAttributeCount(); + } + // For ease of reference, we use here the same attribute order as in Section 4.7 of the JVMS. + output.putShort(attributeCount); + if (code.length > 0) { + // 2, 2, 4 and 2 bytes respectively for max_stack, max_locals, code_length and + // attributes_count, plus the bytecode and the exception table. + int size = 10 + code.length + Handler.getExceptionTableSize(firstHandler); + int codeAttributeCount = 0; + if (stackMapTableEntries != null) { + // 6 header bytes and 2 bytes for number_of_entries. + size += 8 + stackMapTableEntries.length; + ++codeAttributeCount; + } + if (lineNumberTable != null) { + // 6 header bytes and 2 bytes for line_number_table_length. + size += 8 + lineNumberTable.length; + ++codeAttributeCount; + } + if (localVariableTable != null) { + // 6 header bytes and 2 bytes for local_variable_table_length. + size += 8 + localVariableTable.length; + ++codeAttributeCount; + } + if (localVariableTypeTable != null) { + // 6 header bytes and 2 bytes for local_variable_type_table_length. + size += 8 + localVariableTypeTable.length; + ++codeAttributeCount; + } + if (lastCodeRuntimeVisibleTypeAnnotation != null) { + size += + lastCodeRuntimeVisibleTypeAnnotation.computeAnnotationsSize( + Constants.RUNTIME_VISIBLE_TYPE_ANNOTATIONS); + ++codeAttributeCount; + } + if (lastCodeRuntimeInvisibleTypeAnnotation != null) { + size += + lastCodeRuntimeInvisibleTypeAnnotation.computeAnnotationsSize( + Constants.RUNTIME_INVISIBLE_TYPE_ANNOTATIONS); + ++codeAttributeCount; + } + if (firstCodeAttribute != null) { + size += + firstCodeAttribute.computeAttributesSize( + symbolTable, code.data, code.length, maxStack, maxLocals); + codeAttributeCount += firstCodeAttribute.getAttributeCount(); + } + output + .putShort(symbolTable.addConstantUtf8(Constants.CODE)) + .putInt(size) + .putShort(maxStack) + .putShort(maxLocals) + .putInt(code.length) + .putByteArray(code.data, 0, code.length); + Handler.putExceptionTable(firstHandler, output); + output.putShort(codeAttributeCount); + if (stackMapTableEntries != null) { + boolean useStackMapTable = symbolTable.getMajorVersion() >= Opcodes.V1_6; + output + .putShort( + symbolTable.addConstantUtf8( + useStackMapTable ? Constants.STACK_MAP_TABLE : "StackMap")) + .putInt(2 + stackMapTableEntries.length) + .putShort(stackMapTableNumberOfEntries) + .putByteArray(stackMapTableEntries.data, 0, stackMapTableEntries.length); + } + if (lineNumberTable != null) { + output + .putShort(symbolTable.addConstantUtf8(Constants.LINE_NUMBER_TABLE)) + .putInt(2 + lineNumberTable.length) + .putShort(lineNumberTableLength) + .putByteArray(lineNumberTable.data, 0, lineNumberTable.length); + } + if (localVariableTable != null) { + output + .putShort(symbolTable.addConstantUtf8(Constants.LOCAL_VARIABLE_TABLE)) + .putInt(2 + localVariableTable.length) + .putShort(localVariableTableLength) + .putByteArray(localVariableTable.data, 0, localVariableTable.length); + } + if (localVariableTypeTable != null) { + output + .putShort(symbolTable.addConstantUtf8(Constants.LOCAL_VARIABLE_TYPE_TABLE)) + .putInt(2 + localVariableTypeTable.length) + .putShort(localVariableTypeTableLength) + .putByteArray(localVariableTypeTable.data, 0, localVariableTypeTable.length); + } + if (lastCodeRuntimeVisibleTypeAnnotation != null) { + lastCodeRuntimeVisibleTypeAnnotation.putAnnotations( + symbolTable.addConstantUtf8(Constants.RUNTIME_VISIBLE_TYPE_ANNOTATIONS), output); + } + if (lastCodeRuntimeInvisibleTypeAnnotation != null) { + lastCodeRuntimeInvisibleTypeAnnotation.putAnnotations( + symbolTable.addConstantUtf8(Constants.RUNTIME_INVISIBLE_TYPE_ANNOTATIONS), output); + } + if (firstCodeAttribute != null) { + firstCodeAttribute.putAttributes( + symbolTable, code.data, code.length, maxStack, maxLocals, output); + } + } + if (numberOfExceptions > 0) { + output + .putShort(symbolTable.addConstantUtf8(Constants.EXCEPTIONS)) + .putInt(2 + 2 * numberOfExceptions) + .putShort(numberOfExceptions); + for (int exceptionIndex : exceptionIndexTable) { + output.putShort(exceptionIndex); + } + } + Attribute.putAttributes(symbolTable, accessFlags, signatureIndex, output); + AnnotationWriter.putAnnotations( + symbolTable, + lastRuntimeVisibleAnnotation, + lastRuntimeInvisibleAnnotation, + lastRuntimeVisibleTypeAnnotation, + lastRuntimeInvisibleTypeAnnotation, + output); + if (lastRuntimeVisibleParameterAnnotations != null) { + AnnotationWriter.putParameterAnnotations( + symbolTable.addConstantUtf8(Constants.RUNTIME_VISIBLE_PARAMETER_ANNOTATIONS), + lastRuntimeVisibleParameterAnnotations, + visibleAnnotableParameterCount == 0 + ? lastRuntimeVisibleParameterAnnotations.length + : visibleAnnotableParameterCount, + output); + } + if (lastRuntimeInvisibleParameterAnnotations != null) { + AnnotationWriter.putParameterAnnotations( + symbolTable.addConstantUtf8(Constants.RUNTIME_INVISIBLE_PARAMETER_ANNOTATIONS), + lastRuntimeInvisibleParameterAnnotations, + invisibleAnnotableParameterCount == 0 + ? lastRuntimeInvisibleParameterAnnotations.length + : invisibleAnnotableParameterCount, + output); + } + if (defaultValue != null) { + output + .putShort(symbolTable.addConstantUtf8(Constants.ANNOTATION_DEFAULT)) + .putInt(defaultValue.length) + .putByteArray(defaultValue.data, 0, defaultValue.length); + } + if (parameters != null) { + output + .putShort(symbolTable.addConstantUtf8(Constants.METHOD_PARAMETERS)) + .putInt(1 + parameters.length) + .putByte(parametersCount) + .putByteArray(parameters.data, 0, parameters.length); + } + if (firstAttribute != null) { + firstAttribute.putAttributes(symbolTable, output); + } + } + + /** + * Collects the attributes of this method into the given set of attribute prototypes. + * + * @param attributePrototypes a set of attribute prototypes. + */ + final void collectAttributePrototypes(final Attribute.Set attributePrototypes) { + attributePrototypes.addAttributes(firstAttribute); + attributePrototypes.addAttributes(firstCodeAttribute); + } +} diff --git a/spring-core/src/main/java/org/springframework/asm/ModuleVisitor.java b/spring-core/src/main/java/org/springframework/asm/ModuleVisitor.java new file mode 100644 index 0000000..a5b2eb9 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/ModuleVisitor.java @@ -0,0 +1,183 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +package org.springframework.asm; + +/** + * A visitor to visit a Java module. The methods of this class must be called in the following + * order: ( {@code visitMainClass} | ( {@code visitPackage} | {@code visitRequire} | {@code + * visitExport} | {@code visitOpen} | {@code visitUse} | {@code visitProvide} )* ) {@code visitEnd}. + * + * @author Remi Forax + * @author Eric Bruneton + */ +public abstract class ModuleVisitor { + /** + * The ASM API version implemented by this visitor. The value of this field must be one of {@link + * Opcodes#ASM6} or {@link Opcodes#ASM7}. + */ + protected final int api; + + /** + * The module visitor to which this visitor must delegate method calls. May be {@literal null}. + */ + protected ModuleVisitor mv; + + /** + * Constructs a new {@link ModuleVisitor}. + * + * @param api the ASM API version implemented by this visitor. Must be one of {@link Opcodes#ASM6} + * or {@link Opcodes#ASM7}. + */ + public ModuleVisitor(final int api) { + this(api, null); + } + + /** + * Constructs a new {@link ModuleVisitor}. + * + * @param api the ASM API version implemented by this visitor. Must be one of {@link Opcodes#ASM6} + * or {@link Opcodes#ASM7}. + * @param moduleVisitor the module visitor to which this visitor must delegate method calls. May + * be null. + */ + public ModuleVisitor(final int api, final ModuleVisitor moduleVisitor) { + if (api != Opcodes.ASM9 + && api != Opcodes.ASM8 + && api != Opcodes.ASM7 + && api != Opcodes.ASM6 + && api != Opcodes.ASM5 + && api != Opcodes.ASM4 + && api != Opcodes.ASM10_EXPERIMENTAL) { + throw new IllegalArgumentException("Unsupported api " + api); + } + // SPRING PATCH: no preview mode check for ASM 9 experimental + this.api = api; + this.mv = moduleVisitor; + } + + /** + * Visit the main class of the current module. + * + * @param mainClass the internal name of the main class of the current module. + */ + public void visitMainClass(final String mainClass) { + if (mv != null) { + mv.visitMainClass(mainClass); + } + } + + /** + * Visit a package of the current module. + * + * @param packaze the internal name of a package. + */ + public void visitPackage(final String packaze) { + if (mv != null) { + mv.visitPackage(packaze); + } + } + + /** + * Visits a dependence of the current module. + * + * @param module the fully qualified name (using dots) of the dependence. + * @param access the access flag of the dependence among {@code ACC_TRANSITIVE}, {@code + * ACC_STATIC_PHASE}, {@code ACC_SYNTHETIC} and {@code ACC_MANDATED}. + * @param version the module version at compile time, or {@literal null}. + */ + public void visitRequire(final String module, final int access, final String version) { + if (mv != null) { + mv.visitRequire(module, access, version); + } + } + + /** + * Visit an exported package of the current module. + * + * @param packaze the internal name of the exported package. + * @param access the access flag of the exported package, valid values are among {@code + * ACC_SYNTHETIC} and {@code ACC_MANDATED}. + * @param modules the fully qualified names (using dots) of the modules that can access the public + * classes of the exported package, or {@literal null}. + */ + public void visitExport(final String packaze, final int access, final String... modules) { + if (mv != null) { + mv.visitExport(packaze, access, modules); + } + } + + /** + * Visit an open package of the current module. + * + * @param packaze the internal name of the opened package. + * @param access the access flag of the opened package, valid values are among {@code + * ACC_SYNTHETIC} and {@code ACC_MANDATED}. + * @param modules the fully qualified names (using dots) of the modules that can use deep + * reflection to the classes of the open package, or {@literal null}. + */ + public void visitOpen(final String packaze, final int access, final String... modules) { + if (mv != null) { + mv.visitOpen(packaze, access, modules); + } + } + + /** + * Visit a service used by the current module. The name must be the internal name of an interface + * or a class. + * + * @param service the internal name of the service. + */ + public void visitUse(final String service) { + if (mv != null) { + mv.visitUse(service); + } + } + + /** + * Visit an implementation of a service. + * + * @param service the internal name of the service. + * @param providers the internal names of the implementations of the service (there is at least + * one provider). + */ + public void visitProvide(final String service, final String... providers) { + if (mv != null) { + mv.visitProvide(service, providers); + } + } + + /** + * Visits the end of the module. This method, which is the last one to be called, is used to + * inform the visitor that everything have been visited. + */ + public void visitEnd() { + if (mv != null) { + mv.visitEnd(); + } + } +} diff --git a/spring-core/src/main/java/org/springframework/asm/ModuleWriter.java b/spring-core/src/main/java/org/springframework/asm/ModuleWriter.java new file mode 100644 index 0000000..490f865 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/ModuleWriter.java @@ -0,0 +1,253 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +package org.springframework.asm; + +/** + * A {@link ModuleVisitor} that generates the corresponding Module, ModulePackages and + * ModuleMainClass attributes, as defined in the Java Virtual Machine Specification (JVMS). + * + * @see JVMS + * 4.7.25 + * @see JVMS + * 4.7.26 + * @see JVMS + * 4.7.27 + * @author Remi Forax + * @author Eric Bruneton + */ +final class ModuleWriter extends ModuleVisitor { + + /** Where the constants used in this AnnotationWriter must be stored. */ + private final SymbolTable symbolTable; + + /** The module_name_index field of the JVMS Module attribute. */ + private final int moduleNameIndex; + + /** The module_flags field of the JVMS Module attribute. */ + private final int moduleFlags; + + /** The module_version_index field of the JVMS Module attribute. */ + private final int moduleVersionIndex; + + /** The requires_count field of the JVMS Module attribute. */ + private int requiresCount; + + /** The binary content of the 'requires' array of the JVMS Module attribute. */ + private final ByteVector requires; + + /** The exports_count field of the JVMS Module attribute. */ + private int exportsCount; + + /** The binary content of the 'exports' array of the JVMS Module attribute. */ + private final ByteVector exports; + + /** The opens_count field of the JVMS Module attribute. */ + private int opensCount; + + /** The binary content of the 'opens' array of the JVMS Module attribute. */ + private final ByteVector opens; + + /** The uses_count field of the JVMS Module attribute. */ + private int usesCount; + + /** The binary content of the 'uses_index' array of the JVMS Module attribute. */ + private final ByteVector usesIndex; + + /** The provides_count field of the JVMS Module attribute. */ + private int providesCount; + + /** The binary content of the 'provides' array of the JVMS Module attribute. */ + private final ByteVector provides; + + /** The provides_count field of the JVMS ModulePackages attribute. */ + private int packageCount; + + /** The binary content of the 'package_index' array of the JVMS ModulePackages attribute. */ + private final ByteVector packageIndex; + + /** The main_class_index field of the JVMS ModuleMainClass attribute, or 0. */ + private int mainClassIndex; + + ModuleWriter(final SymbolTable symbolTable, final int name, final int access, final int version) { + super(/* latest api = */ Opcodes.ASM9); + this.symbolTable = symbolTable; + this.moduleNameIndex = name; + this.moduleFlags = access; + this.moduleVersionIndex = version; + this.requires = new ByteVector(); + this.exports = new ByteVector(); + this.opens = new ByteVector(); + this.usesIndex = new ByteVector(); + this.provides = new ByteVector(); + this.packageIndex = new ByteVector(); + } + + @Override + public void visitMainClass(final String mainClass) { + this.mainClassIndex = symbolTable.addConstantClass(mainClass).index; + } + + @Override + public void visitPackage(final String packaze) { + packageIndex.putShort(symbolTable.addConstantPackage(packaze).index); + packageCount++; + } + + @Override + public void visitRequire(final String module, final int access, final String version) { + requires + .putShort(symbolTable.addConstantModule(module).index) + .putShort(access) + .putShort(version == null ? 0 : symbolTable.addConstantUtf8(version)); + requiresCount++; + } + + @Override + public void visitExport(final String packaze, final int access, final String... modules) { + exports.putShort(symbolTable.addConstantPackage(packaze).index).putShort(access); + if (modules == null) { + exports.putShort(0); + } else { + exports.putShort(modules.length); + for (String module : modules) { + exports.putShort(symbolTable.addConstantModule(module).index); + } + } + exportsCount++; + } + + @Override + public void visitOpen(final String packaze, final int access, final String... modules) { + opens.putShort(symbolTable.addConstantPackage(packaze).index).putShort(access); + if (modules == null) { + opens.putShort(0); + } else { + opens.putShort(modules.length); + for (String module : modules) { + opens.putShort(symbolTable.addConstantModule(module).index); + } + } + opensCount++; + } + + @Override + public void visitUse(final String service) { + usesIndex.putShort(symbolTable.addConstantClass(service).index); + usesCount++; + } + + @Override + public void visitProvide(final String service, final String... providers) { + provides.putShort(symbolTable.addConstantClass(service).index); + provides.putShort(providers.length); + for (String provider : providers) { + provides.putShort(symbolTable.addConstantClass(provider).index); + } + providesCount++; + } + + @Override + public void visitEnd() { + // Nothing to do. + } + + /** + * Returns the number of Module, ModulePackages and ModuleMainClass attributes generated by this + * ModuleWriter. + * + * @return the number of Module, ModulePackages and ModuleMainClass attributes (between 1 and 3). + */ + int getAttributeCount() { + return 1 + (packageCount > 0 ? 1 : 0) + (mainClassIndex > 0 ? 1 : 0); + } + + /** + * Returns the size of the Module, ModulePackages and ModuleMainClass attributes generated by this + * ModuleWriter. Also add the names of these attributes in the constant pool. + * + * @return the size in bytes of the Module, ModulePackages and ModuleMainClass attributes. + */ + int computeAttributesSize() { + symbolTable.addConstantUtf8(Constants.MODULE); + // 6 attribute header bytes, 6 bytes for name, flags and version, and 5 * 2 bytes for counts. + int size = + 22 + requires.length + exports.length + opens.length + usesIndex.length + provides.length; + if (packageCount > 0) { + symbolTable.addConstantUtf8(Constants.MODULE_PACKAGES); + // 6 attribute header bytes, and 2 bytes for package_count. + size += 8 + packageIndex.length; + } + if (mainClassIndex > 0) { + symbolTable.addConstantUtf8(Constants.MODULE_MAIN_CLASS); + // 6 attribute header bytes, and 2 bytes for main_class_index. + size += 8; + } + return size; + } + + /** + * Puts the Module, ModulePackages and ModuleMainClass attributes generated by this ModuleWriter + * in the given ByteVector. + * + * @param output where the attributes must be put. + */ + void putAttributes(final ByteVector output) { + // 6 bytes for name, flags and version, and 5 * 2 bytes for counts. + int moduleAttributeLength = + 16 + requires.length + exports.length + opens.length + usesIndex.length + provides.length; + output + .putShort(symbolTable.addConstantUtf8(Constants.MODULE)) + .putInt(moduleAttributeLength) + .putShort(moduleNameIndex) + .putShort(moduleFlags) + .putShort(moduleVersionIndex) + .putShort(requiresCount) + .putByteArray(requires.data, 0, requires.length) + .putShort(exportsCount) + .putByteArray(exports.data, 0, exports.length) + .putShort(opensCount) + .putByteArray(opens.data, 0, opens.length) + .putShort(usesCount) + .putByteArray(usesIndex.data, 0, usesIndex.length) + .putShort(providesCount) + .putByteArray(provides.data, 0, provides.length); + if (packageCount > 0) { + output + .putShort(symbolTable.addConstantUtf8(Constants.MODULE_PACKAGES)) + .putInt(2 + packageIndex.length) + .putShort(packageCount) + .putByteArray(packageIndex.data, 0, packageIndex.length); + } + if (mainClassIndex > 0) { + output + .putShort(symbolTable.addConstantUtf8(Constants.MODULE_MAIN_CLASS)) + .putInt(2) + .putShort(mainClassIndex); + } + } +} diff --git a/spring-core/src/main/java/org/springframework/asm/Opcodes.java b/spring-core/src/main/java/org/springframework/asm/Opcodes.java new file mode 100644 index 0000000..52e35dd --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/Opcodes.java @@ -0,0 +1,558 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +package org.springframework.asm; + +/** + * The JVM opcodes, access flags and array type codes. This interface does not define all the JVM + * opcodes because some opcodes are automatically handled. For example, the xLOAD and xSTORE opcodes + * are automatically replaced by xLOAD_n and xSTORE_n opcodes when possible. The xLOAD_n and + * xSTORE_n opcodes are therefore not defined in this interface. Likewise for LDC, automatically + * replaced by LDC_W or LDC2_W when necessary, WIDE, GOTO_W and JSR_W. + * + * @see JVMS 6 + * @author Eric Bruneton + * @author Eugene Kuleshov + */ +// DontCheck(InterfaceIsType): can't be fixed (for backward binary compatibility). +public interface Opcodes { + + // ASM API versions. + + int ASM4 = 4 << 16 | 0 << 8; + int ASM5 = 5 << 16 | 0 << 8; + int ASM6 = 6 << 16 | 0 << 8; + int ASM7 = 7 << 16 | 0 << 8; + int ASM8 = 8 << 16 | 0 << 8; + int ASM9 = 9 << 16 | 0 << 8; + + /** + * Experimental, use at your own risk. This field will be renamed when it becomes stable, this + * will break existing code using it. Only code compiled with --enable-preview can use this. + *

    SPRING PATCH: no preview mode check for ASM 10 experimental, enabling it by default. + */ + int ASM10_EXPERIMENTAL = 1 << 24 | 10 << 16 | 0 << 8; + + /* + * Internal flags used to redirect calls to deprecated methods. For instance, if a visitOldStuff + * method in API_OLD is deprecated and replaced with visitNewStuff in API_NEW, then the + * redirection should be done as follows: + * + *

    +   * public class StuffVisitor {
    +   *   ...
    +   *
    +   *   @Deprecated public void visitOldStuff(int arg, ...) {
    +   *     // SOURCE_DEPRECATED means "a call from a deprecated method using the old 'api' value".
    +   *     visitNewStuf(arg | (api < API_NEW ? SOURCE_DEPRECATED : 0), ...);
    +   *   }
    +   *
    +   *   public void visitNewStuff(int argAndSource, ...) {
    +   *     if (api < API_NEW && (argAndSource & SOURCE_DEPRECATED) == 0) {
    +   *       visitOldStuff(argAndSource, ...);
    +   *     } else {
    +   *       int arg = argAndSource & ~SOURCE_MASK;
    +   *       [ do stuff ]
    +   *     }
    +   *   }
    +   * }
    +   * 
    + * + *

    If 'api' is equal to API_NEW, there are two cases: + * + *

      + *
    • call visitNewStuff: the redirection test is skipped and 'do stuff' is executed directly. + *
    • call visitOldSuff: the source is not set to SOURCE_DEPRECATED before calling + * visitNewStuff, but the redirection test is skipped anyway in visitNewStuff, which + * directly executes 'do stuff'. + *
    + * + *

    If 'api' is equal to API_OLD, there are two cases: + * + *

      + *
    • call visitOldSuff: the source is set to SOURCE_DEPRECATED before calling visitNewStuff. + * Because of this visitNewStuff does not redirect back to visitOldStuff, and instead + * executes 'do stuff'. + *
    • call visitNewStuff: the call is redirected to visitOldStuff because the source is 0. + * visitOldStuff now sets the source to SOURCE_DEPRECATED and calls visitNewStuff back. This + * time visitNewStuff does not redirect the call, and instead executes 'do stuff'. + *
    + * + *

    User subclasses

    + * + *

    If a user subclass overrides one of these methods, there are only two cases: either 'api' is + * API_OLD and visitOldStuff is overridden (and visitNewStuff is not), or 'api' is API_NEW or + * more, and visitNewStuff is overridden (and visitOldStuff is not). Any other case is a user + * programming error. + * + *

    If 'api' is equal to API_NEW, the class hierarchy is equivalent to + * + *

    +   * public class StuffVisitor {
    +   *   @Deprecated public void visitOldStuff(int arg, ...) { visitNewStuf(arg, ...); }
    +   *   public void visitNewStuff(int arg, ...) { [ do stuff ] }
    +   * }
    +   * class UserStuffVisitor extends StuffVisitor {
    +   *   @Override public void visitNewStuff(int arg, ...) {
    +   *     super.visitNewStuff(int arg, ...); // optional
    +   *     [ do user stuff ]
    +   *   }
    +   * }
    +   * 
    + * + *

    It is then obvious that whether visitNewStuff or visitOldStuff is called, 'do stuff' and 'do + * user stuff' will be executed, in this order. + * + *

    If 'api' is equal to API_OLD, the class hierarchy is equivalent to + * + *

    +   * public class StuffVisitor {
    +   *   @Deprecated public void visitOldStuff(int arg, ...) {
    +   *     visitNewStuf(arg | SOURCE_DEPRECATED, ...);
    +   *   }
    +   *   public void visitNewStuff(int argAndSource...) {
    +   *     if ((argAndSource & SOURCE_DEPRECATED) == 0) {
    +   *       visitOldStuff(argAndSource, ...);
    +   *     } else {
    +   *       int arg = argAndSource & ~SOURCE_MASK;
    +   *       [ do stuff ]
    +   *     }
    +   *   }
    +   * }
    +   * class UserStuffVisitor extends StuffVisitor {
    +   *   @Override public void visitOldStuff(int arg, ...) {
    +   *     super.visitOldStuff(int arg, ...); // optional
    +   *     [ do user stuff ]
    +   *   }
    +   * }
    +   * 
    + * + *

    and there are two cases: + * + *

      + *
    • call visitOldSuff: in the call to super.visitOldStuff, the source is set to + * SOURCE_DEPRECATED and visitNewStuff is called. Here 'do stuff' is run because the source + * was previously set to SOURCE_DEPRECATED, and execution eventually returns to + * UserStuffVisitor.visitOldStuff, where 'do user stuff' is run. + *
    • call visitNewStuff: the call is redirected to UserStuffVisitor.visitOldStuff because the + * source is 0. Execution continues as in the previous case, resulting in 'do stuff' and 'do + * user stuff' being executed, in this order. + *
    + * + *

    ASM subclasses

    + * + *

    In ASM packages, subclasses of StuffVisitor can typically be sub classed again by the user, + * and can be used with API_OLD or API_NEW. Because of this, if such a subclass must override + * visitNewStuff, it must do so in the following way (and must not override visitOldStuff): + * + *

    +   * public class AsmStuffVisitor extends StuffVisitor {
    +   *   @Override public void visitNewStuff(int argAndSource, ...) {
    +   *     if (api < API_NEW && (argAndSource & SOURCE_DEPRECATED) == 0) {
    +   *       super.visitNewStuff(argAndSource, ...);
    +   *       return;
    +   *     }
    +   *     super.visitNewStuff(argAndSource, ...); // optional
    +   *     int arg = argAndSource & ~SOURCE_MASK;
    +   *     [ do other stuff ]
    +   *   }
    +   * }
    +   * 
    + * + *

    If a user class extends this with 'api' equal to API_NEW, the class hierarchy is equivalent + * to + * + *

    +   * public class StuffVisitor {
    +   *   @Deprecated public void visitOldStuff(int arg, ...) { visitNewStuf(arg, ...); }
    +   *   public void visitNewStuff(int arg, ...) { [ do stuff ] }
    +   * }
    +   * public class AsmStuffVisitor extends StuffVisitor {
    +   *   @Override public void visitNewStuff(int arg, ...) {
    +   *     super.visitNewStuff(arg, ...);
    +   *     [ do other stuff ]
    +   *   }
    +   * }
    +   * class UserStuffVisitor extends StuffVisitor {
    +   *   @Override public void visitNewStuff(int arg, ...) {
    +   *     super.visitNewStuff(int arg, ...);
    +   *     [ do user stuff ]
    +   *   }
    +   * }
    +   * 
    + * + *

    It is then obvious that whether visitNewStuff or visitOldStuff is called, 'do stuff', 'do + * other stuff' and 'do user stuff' will be executed, in this order. If, on the other hand, a user + * class extends AsmStuffVisitor with 'api' equal to API_OLD, the class hierarchy is equivalent to + * + *

    +   * public class StuffVisitor {
    +   *   @Deprecated public void visitOldStuff(int arg, ...) {
    +   *     visitNewStuf(arg | SOURCE_DEPRECATED, ...);
    +   *   }
    +   *   public void visitNewStuff(int argAndSource, ...) {
    +   *     if ((argAndSource & SOURCE_DEPRECATED) == 0) {
    +   *       visitOldStuff(argAndSource, ...);
    +   *     } else {
    +   *       int arg = argAndSource & ~SOURCE_MASK;
    +   *       [ do stuff ]
    +   *     }
    +   *   }
    +   * }
    +   * public class AsmStuffVisitor extends StuffVisitor {
    +   *   @Override public void visitNewStuff(int argAndSource, ...) {
    +   *     if ((argAndSource & SOURCE_DEPRECATED) == 0) {
    +   *       super.visitNewStuff(argAndSource, ...);
    +   *       return;
    +   *     }
    +   *     super.visitNewStuff(argAndSource, ...); // optional
    +   *     int arg = argAndSource & ~SOURCE_MASK;
    +   *     [ do other stuff ]
    +   *   }
    +   * }
    +   * class UserStuffVisitor extends StuffVisitor {
    +   *   @Override public void visitOldStuff(int arg, ...) {
    +   *     super.visitOldStuff(arg, ...);
    +   *     [ do user stuff ]
    +   *   }
    +   * }
    +   * 
    + * + *

    and, here again, whether visitNewStuff or visitOldStuff is called, 'do stuff', 'do other + * stuff' and 'do user stuff' will be executed, in this order (exercise left to the reader). + * + *

    Notes

    + * + *
      + *
    • the SOURCE_DEPRECATED flag is set only if 'api' is API_OLD, just before calling + * visitNewStuff. By hypothesis, this method is not overridden by the user. Therefore, user + * classes can never see this flag. Only ASM subclasses must take care of extracting the + * actual argument value by clearing the source flags. + *
    • because the SOURCE_DEPRECATED flag is immediately cleared in the caller, the caller can + * call visitOldStuff or visitNewStuff (in 'do stuff' and 'do user stuff') on a delegate + * visitor without any risks (breaking the redirection logic, "leaking" the flag, etc). + *
    • all the scenarios discussed above are unit tested in MethodVisitorTest. + *
    + */ + + int SOURCE_DEPRECATED = 0x100; + int SOURCE_MASK = SOURCE_DEPRECATED; + + // Java ClassFile versions (the minor version is stored in the 16 most significant bits, and the + // major version in the 16 least significant bits). + + int V1_1 = 3 << 16 | 45; + int V1_2 = 0 << 16 | 46; + int V1_3 = 0 << 16 | 47; + int V1_4 = 0 << 16 | 48; + int V1_5 = 0 << 16 | 49; + int V1_6 = 0 << 16 | 50; + int V1_7 = 0 << 16 | 51; + int V1_8 = 0 << 16 | 52; + int V9 = 0 << 16 | 53; + int V10 = 0 << 16 | 54; + int V11 = 0 << 16 | 55; + int V12 = 0 << 16 | 56; + int V13 = 0 << 16 | 57; + int V14 = 0 << 16 | 58; + int V15 = 0 << 16 | 59; + int V16 = 0 << 16 | 60; + + /** + * Version flag indicating that the class is using 'preview' features. + * + *

    {@code version & V_PREVIEW == V_PREVIEW} tests if a version is flagged with {@code + * V_PREVIEW}. + */ + int V_PREVIEW = 0xFFFF0000; + + // Access flags values, defined in + // - https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.1-200-E.1 + // - https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.5-200-A.1 + // - https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.6-200-A.1 + // - https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.7.25 + + int ACC_PUBLIC = 0x0001; // class, field, method + int ACC_PRIVATE = 0x0002; // class, field, method + int ACC_PROTECTED = 0x0004; // class, field, method + int ACC_STATIC = 0x0008; // field, method + int ACC_FINAL = 0x0010; // class, field, method, parameter + int ACC_SUPER = 0x0020; // class + int ACC_SYNCHRONIZED = 0x0020; // method + int ACC_OPEN = 0x0020; // module + int ACC_TRANSITIVE = 0x0020; // module requires + int ACC_VOLATILE = 0x0040; // field + int ACC_BRIDGE = 0x0040; // method + int ACC_STATIC_PHASE = 0x0040; // module requires + int ACC_VARARGS = 0x0080; // method + int ACC_TRANSIENT = 0x0080; // field + int ACC_NATIVE = 0x0100; // method + int ACC_INTERFACE = 0x0200; // class + int ACC_ABSTRACT = 0x0400; // class, method + int ACC_STRICT = 0x0800; // method + int ACC_SYNTHETIC = 0x1000; // class, field, method, parameter, module * + int ACC_ANNOTATION = 0x2000; // class + int ACC_ENUM = 0x4000; // class(?) field inner + int ACC_MANDATED = 0x8000; // field, method, parameter, module, module * + int ACC_MODULE = 0x8000; // class + + // ASM specific access flags. + // WARNING: the 16 least significant bits must NOT be used, to avoid conflicts with standard + // access flags, and also to make sure that these flags are automatically filtered out when + // written in class files (because access flags are stored using 16 bits only). + + int ACC_RECORD = 0x10000; // class + int ACC_DEPRECATED = 0x20000; // class, field, method + + // Possible values for the type operand of the NEWARRAY instruction. + // See https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-6.html#jvms-6.5.newarray. + + int T_BOOLEAN = 4; + int T_CHAR = 5; + int T_FLOAT = 6; + int T_DOUBLE = 7; + int T_BYTE = 8; + int T_SHORT = 9; + int T_INT = 10; + int T_LONG = 11; + + // Possible values for the reference_kind field of CONSTANT_MethodHandle_info structures. + // See https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.4.8. + + int H_GETFIELD = 1; + int H_GETSTATIC = 2; + int H_PUTFIELD = 3; + int H_PUTSTATIC = 4; + int H_INVOKEVIRTUAL = 5; + int H_INVOKESTATIC = 6; + int H_INVOKESPECIAL = 7; + int H_NEWINVOKESPECIAL = 8; + int H_INVOKEINTERFACE = 9; + + // ASM specific stack map frame types, used in {@link ClassVisitor#visitFrame}. + + /** An expanded frame. See {@link ClassReader#EXPAND_FRAMES}. */ + int F_NEW = -1; + + /** A compressed frame with complete frame data. */ + int F_FULL = 0; + + /** + * A compressed frame where locals are the same as the locals in the previous frame, except that + * additional 1-3 locals are defined, and with an empty stack. + */ + int F_APPEND = 1; + + /** + * A compressed frame where locals are the same as the locals in the previous frame, except that + * the last 1-3 locals are absent and with an empty stack. + */ + int F_CHOP = 2; + + /** + * A compressed frame with exactly the same locals as the previous frame and with an empty stack. + */ + int F_SAME = 3; + + /** + * A compressed frame with exactly the same locals as the previous frame and with a single value + * on the stack. + */ + int F_SAME1 = 4; + + // Standard stack map frame element types, used in {@link ClassVisitor#visitFrame}. + + Integer TOP = Frame.ITEM_TOP; + Integer INTEGER = Frame.ITEM_INTEGER; + Integer FLOAT = Frame.ITEM_FLOAT; + Integer DOUBLE = Frame.ITEM_DOUBLE; + Integer LONG = Frame.ITEM_LONG; + Integer NULL = Frame.ITEM_NULL; + Integer UNINITIALIZED_THIS = Frame.ITEM_UNINITIALIZED_THIS; + + // The JVM opcode values (with the MethodVisitor method name used to visit them in comment, and + // where '-' means 'same method name as on the previous line'). + // See https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-6.html. + + int NOP = 0; // visitInsn + int ACONST_NULL = 1; // - + int ICONST_M1 = 2; // - + int ICONST_0 = 3; // - + int ICONST_1 = 4; // - + int ICONST_2 = 5; // - + int ICONST_3 = 6; // - + int ICONST_4 = 7; // - + int ICONST_5 = 8; // - + int LCONST_0 = 9; // - + int LCONST_1 = 10; // - + int FCONST_0 = 11; // - + int FCONST_1 = 12; // - + int FCONST_2 = 13; // - + int DCONST_0 = 14; // - + int DCONST_1 = 15; // - + int BIPUSH = 16; // visitIntInsn + int SIPUSH = 17; // - + int LDC = 18; // visitLdcInsn + int ILOAD = 21; // visitVarInsn + int LLOAD = 22; // - + int FLOAD = 23; // - + int DLOAD = 24; // - + int ALOAD = 25; // - + int IALOAD = 46; // visitInsn + int LALOAD = 47; // - + int FALOAD = 48; // - + int DALOAD = 49; // - + int AALOAD = 50; // - + int BALOAD = 51; // - + int CALOAD = 52; // - + int SALOAD = 53; // - + int ISTORE = 54; // visitVarInsn + int LSTORE = 55; // - + int FSTORE = 56; // - + int DSTORE = 57; // - + int ASTORE = 58; // - + int IASTORE = 79; // visitInsn + int LASTORE = 80; // - + int FASTORE = 81; // - + int DASTORE = 82; // - + int AASTORE = 83; // - + int BASTORE = 84; // - + int CASTORE = 85; // - + int SASTORE = 86; // - + int POP = 87; // - + int POP2 = 88; // - + int DUP = 89; // - + int DUP_X1 = 90; // - + int DUP_X2 = 91; // - + int DUP2 = 92; // - + int DUP2_X1 = 93; // - + int DUP2_X2 = 94; // - + int SWAP = 95; // - + int IADD = 96; // - + int LADD = 97; // - + int FADD = 98; // - + int DADD = 99; // - + int ISUB = 100; // - + int LSUB = 101; // - + int FSUB = 102; // - + int DSUB = 103; // - + int IMUL = 104; // - + int LMUL = 105; // - + int FMUL = 106; // - + int DMUL = 107; // - + int IDIV = 108; // - + int LDIV = 109; // - + int FDIV = 110; // - + int DDIV = 111; // - + int IREM = 112; // - + int LREM = 113; // - + int FREM = 114; // - + int DREM = 115; // - + int INEG = 116; // - + int LNEG = 117; // - + int FNEG = 118; // - + int DNEG = 119; // - + int ISHL = 120; // - + int LSHL = 121; // - + int ISHR = 122; // - + int LSHR = 123; // - + int IUSHR = 124; // - + int LUSHR = 125; // - + int IAND = 126; // - + int LAND = 127; // - + int IOR = 128; // - + int LOR = 129; // - + int IXOR = 130; // - + int LXOR = 131; // - + int IINC = 132; // visitIincInsn + int I2L = 133; // visitInsn + int I2F = 134; // - + int I2D = 135; // - + int L2I = 136; // - + int L2F = 137; // - + int L2D = 138; // - + int F2I = 139; // - + int F2L = 140; // - + int F2D = 141; // - + int D2I = 142; // - + int D2L = 143; // - + int D2F = 144; // - + int I2B = 145; // - + int I2C = 146; // - + int I2S = 147; // - + int LCMP = 148; // - + int FCMPL = 149; // - + int FCMPG = 150; // - + int DCMPL = 151; // - + int DCMPG = 152; // - + int IFEQ = 153; // visitJumpInsn + int IFNE = 154; // - + int IFLT = 155; // - + int IFGE = 156; // - + int IFGT = 157; // - + int IFLE = 158; // - + int IF_ICMPEQ = 159; // - + int IF_ICMPNE = 160; // - + int IF_ICMPLT = 161; // - + int IF_ICMPGE = 162; // - + int IF_ICMPGT = 163; // - + int IF_ICMPLE = 164; // - + int IF_ACMPEQ = 165; // - + int IF_ACMPNE = 166; // - + int GOTO = 167; // - + int JSR = 168; // - + int RET = 169; // visitVarInsn + int TABLESWITCH = 170; // visiTableSwitchInsn + int LOOKUPSWITCH = 171; // visitLookupSwitch + int IRETURN = 172; // visitInsn + int LRETURN = 173; // - + int FRETURN = 174; // - + int DRETURN = 175; // - + int ARETURN = 176; // - + int RETURN = 177; // - + int GETSTATIC = 178; // visitFieldInsn + int PUTSTATIC = 179; // - + int GETFIELD = 180; // - + int PUTFIELD = 181; // - + int INVOKEVIRTUAL = 182; // visitMethodInsn + int INVOKESPECIAL = 183; // - + int INVOKESTATIC = 184; // - + int INVOKEINTERFACE = 185; // - + int INVOKEDYNAMIC = 186; // visitInvokeDynamicInsn + int NEW = 187; // visitTypeInsn + int NEWARRAY = 188; // visitIntInsn + int ANEWARRAY = 189; // visitTypeInsn + int ARRAYLENGTH = 190; // visitInsn + int ATHROW = 191; // - + int CHECKCAST = 192; // visitTypeInsn + int INSTANCEOF = 193; // - + int MONITORENTER = 194; // visitInsn + int MONITOREXIT = 195; // - + int MULTIANEWARRAY = 197; // visitMultiANewArrayInsn + int IFNULL = 198; // visitJumpInsn + int IFNONNULL = 199; // - +} diff --git a/spring-core/src/main/java/org/springframework/asm/RecordComponentVisitor.java b/spring-core/src/main/java/org/springframework/asm/RecordComponentVisitor.java new file mode 100644 index 0000000..0a79e2c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/RecordComponentVisitor.java @@ -0,0 +1,150 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +package org.springframework.asm; + +/** + * A visitor to visit a record component. The methods of this class must be called in the following + * order: ( {@code visitAnnotation} | {@code visitTypeAnnotation} | {@code visitAttribute} )* {@code + * visitEnd}. + * + * @author Remi Forax + * @author Eric Bruneton + */ +public abstract class RecordComponentVisitor { + /** + * The ASM API version implemented by this visitor. The value of this field must be one of {@link + * Opcodes#ASM8} or {@link Opcodes#ASM9}. + */ + protected final int api; + + /** + * The record visitor to which this visitor must delegate method calls. May be {@literal null}. + */ + /*package-private*/ RecordComponentVisitor delegate; + + /** + * Constructs a new {@link RecordComponentVisitor}. + * + * @param api the ASM API version implemented by this visitor. Must be one of {@link Opcodes#ASM8} + * or {@link Opcodes#ASM9}. + */ + public RecordComponentVisitor(final int api) { + this(api, null); + } + + /** + * Constructs a new {@link RecordComponentVisitor}. + * + * @param api the ASM API version implemented by this visitor. Must be {@link Opcodes#ASM8}. + * @param recordComponentVisitor the record component visitor to which this visitor must delegate + * method calls. May be null. + */ + public RecordComponentVisitor( + final int api, final RecordComponentVisitor recordComponentVisitor) { + if (api != Opcodes.ASM9 + && api != Opcodes.ASM8 + && api != Opcodes.ASM7 + && api != Opcodes.ASM6 + && api != Opcodes.ASM5 + && api != Opcodes.ASM4 + && api != Opcodes.ASM10_EXPERIMENTAL) { + throw new IllegalArgumentException("Unsupported api " + api); + } + // SPRING PATCH: no preview mode check for ASM 9 experimental + this.api = api; + this.delegate = recordComponentVisitor; + } + + /** + * The record visitor to which this visitor must delegate method calls. May be {@literal null}. + * + * @return the record visitor to which this visitor must delegate method calls or {@literal null}. + */ + public RecordComponentVisitor getDelegate() { + return delegate; + } + + /** + * Visits an annotation of the record component. + * + * @param descriptor the class descriptor of the annotation class. + * @param visible {@literal true} if the annotation is visible at runtime. + * @return a visitor to visit the annotation values, or {@literal null} if this visitor is not + * interested in visiting this annotation. + */ + public AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) { + if (delegate != null) { + return delegate.visitAnnotation(descriptor, visible); + } + return null; + } + + /** + * Visits an annotation on a type in the record component signature. + * + * @param typeRef a reference to the annotated type. The sort of this type reference must be + * {@link TypeReference#CLASS_TYPE_PARAMETER}, {@link + * TypeReference#CLASS_TYPE_PARAMETER_BOUND} or {@link TypeReference#CLASS_EXTENDS}. See + * {@link TypeReference}. + * @param typePath the path to the annotated type argument, wildcard bound, array element type, or + * static inner type within 'typeRef'. May be {@literal null} if the annotation targets + * 'typeRef' as a whole. + * @param descriptor the class descriptor of the annotation class. + * @param visible {@literal true} if the annotation is visible at runtime. + * @return a visitor to visit the annotation values, or {@literal null} if this visitor is not + * interested in visiting this annotation. + */ + public AnnotationVisitor visitTypeAnnotation( + final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) { + if (delegate != null) { + return delegate.visitTypeAnnotation(typeRef, typePath, descriptor, visible); + } + return null; + } + + /** + * Visits a non standard attribute of the record component. + * + * @param attribute an attribute. + */ + public void visitAttribute(final Attribute attribute) { + if (delegate != null) { + delegate.visitAttribute(attribute); + } + } + + /** + * Visits the end of the record component. This method, which is the last one to be called, is + * used to inform the visitor that everything have been visited. + */ + public void visitEnd() { + if (delegate != null) { + delegate.visitEnd(); + } + } +} diff --git a/spring-core/src/main/java/org/springframework/asm/RecordComponentWriter.java b/spring-core/src/main/java/org/springframework/asm/RecordComponentWriter.java new file mode 100644 index 0000000..bd007ac --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/RecordComponentWriter.java @@ -0,0 +1,225 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +package org.springframework.asm; + +final class RecordComponentWriter extends RecordComponentVisitor { + /** Where the constants used in this RecordComponentWriter must be stored. */ + private final SymbolTable symbolTable; + + // Note: fields are ordered as in the record_component_info structure, and those related to + // attributes are ordered as in Section 4.7 of the JVMS. + + /** The name_index field of the Record attribute. */ + private final int nameIndex; + + /** The descriptor_index field of the the Record attribute. */ + private final int descriptorIndex; + + /** + * The signature_index field of the Signature attribute of this record component, or 0 if there is + * no Signature attribute. + */ + private int signatureIndex; + + /** + * The last runtime visible annotation of this record component. The previous ones can be accessed + * with the {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeVisibleAnnotation; + + /** + * The last runtime invisible annotation of this record component. The previous ones can be + * accessed with the {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeInvisibleAnnotation; + + /** + * The last runtime visible type annotation of this record component. The previous ones can be + * accessed with the {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeVisibleTypeAnnotation; + + /** + * The last runtime invisible type annotation of this record component. The previous ones can be + * accessed with the {@link AnnotationWriter#previousAnnotation} field. May be {@literal null}. + */ + private AnnotationWriter lastRuntimeInvisibleTypeAnnotation; + + /** + * The first non standard attribute of this record component. The next ones can be accessed with + * the {@link Attribute#nextAttribute} field. May be {@literal null}. + * + *

    WARNING: this list stores the attributes in the reverse order of their visit. + * firstAttribute is actually the last attribute visited in {@link #visitAttribute(Attribute)}. + * The {@link #putRecordComponentInfo(ByteVector)} method writes the attributes in the order + * defined by this list, i.e. in the reverse order specified by the user. + */ + private Attribute firstAttribute; + + /** + * Constructs a new {@link RecordComponentWriter}. + * + * @param symbolTable where the constants used in this RecordComponentWriter must be stored. + * @param name the record component name. + * @param descriptor the record component descriptor (see {@link Type}). + * @param signature the record component signature. May be {@literal null}. + */ + RecordComponentWriter( + final SymbolTable symbolTable, + final String name, + final String descriptor, + final String signature) { + super(/* latest api = */ Opcodes.ASM9); + this.symbolTable = symbolTable; + this.nameIndex = symbolTable.addConstantUtf8(name); + this.descriptorIndex = symbolTable.addConstantUtf8(descriptor); + if (signature != null) { + this.signatureIndex = symbolTable.addConstantUtf8(signature); + } + } + + // ----------------------------------------------------------------------------------------------- + // Implementation of the FieldVisitor abstract class + // ----------------------------------------------------------------------------------------------- + + @Override + public AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) { + if (visible) { + return lastRuntimeVisibleAnnotation = + AnnotationWriter.create(symbolTable, descriptor, lastRuntimeVisibleAnnotation); + } else { + return lastRuntimeInvisibleAnnotation = + AnnotationWriter.create(symbolTable, descriptor, lastRuntimeInvisibleAnnotation); + } + } + + @Override + public AnnotationVisitor visitTypeAnnotation( + final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) { + if (visible) { + return lastRuntimeVisibleTypeAnnotation = + AnnotationWriter.create( + symbolTable, typeRef, typePath, descriptor, lastRuntimeVisibleTypeAnnotation); + } else { + return lastRuntimeInvisibleTypeAnnotation = + AnnotationWriter.create( + symbolTable, typeRef, typePath, descriptor, lastRuntimeInvisibleTypeAnnotation); + } + } + + @Override + public void visitAttribute(final Attribute attribute) { + // Store the attributes in the reverse order of their visit by this method. + attribute.nextAttribute = firstAttribute; + firstAttribute = attribute; + } + + @Override + public void visitEnd() { + // Nothing to do. + } + + // ----------------------------------------------------------------------------------------------- + // Utility methods + // ----------------------------------------------------------------------------------------------- + + /** + * Returns the size of the record component JVMS structure generated by this + * RecordComponentWriter. Also adds the names of the attributes of this record component in the + * constant pool. + * + * @return the size in bytes of the record_component_info of the Record attribute. + */ + int computeRecordComponentInfoSize() { + // name_index, descriptor_index and attributes_count fields use 6 bytes. + int size = 6; + size += Attribute.computeAttributesSize(symbolTable, 0, signatureIndex); + size += + AnnotationWriter.computeAnnotationsSize( + lastRuntimeVisibleAnnotation, + lastRuntimeInvisibleAnnotation, + lastRuntimeVisibleTypeAnnotation, + lastRuntimeInvisibleTypeAnnotation); + if (firstAttribute != null) { + size += firstAttribute.computeAttributesSize(symbolTable); + } + return size; + } + + /** + * Puts the content of the record component generated by this RecordComponentWriter into the given + * ByteVector. + * + * @param output where the record_component_info structure must be put. + */ + void putRecordComponentInfo(final ByteVector output) { + output.putShort(nameIndex).putShort(descriptorIndex); + // Compute and put the attributes_count field. + // For ease of reference, we use here the same attribute order as in Section 4.7 of the JVMS. + int attributesCount = 0; + if (signatureIndex != 0) { + ++attributesCount; + } + if (lastRuntimeVisibleAnnotation != null) { + ++attributesCount; + } + if (lastRuntimeInvisibleAnnotation != null) { + ++attributesCount; + } + if (lastRuntimeVisibleTypeAnnotation != null) { + ++attributesCount; + } + if (lastRuntimeInvisibleTypeAnnotation != null) { + ++attributesCount; + } + if (firstAttribute != null) { + attributesCount += firstAttribute.getAttributeCount(); + } + output.putShort(attributesCount); + Attribute.putAttributes(symbolTable, 0, signatureIndex, output); + AnnotationWriter.putAnnotations( + symbolTable, + lastRuntimeVisibleAnnotation, + lastRuntimeInvisibleAnnotation, + lastRuntimeVisibleTypeAnnotation, + lastRuntimeInvisibleTypeAnnotation, + output); + if (firstAttribute != null) { + firstAttribute.putAttributes(symbolTable, output); + } + } + + /** + * Collects the attributes of this record component into the given set of attribute prototypes. + * + * @param attributePrototypes a set of attribute prototypes. + */ + final void collectAttributePrototypes(final Attribute.Set attributePrototypes) { + attributePrototypes.addAttributes(firstAttribute); + } +} diff --git a/spring-core/src/main/java/org/springframework/asm/SpringAsmInfo.java b/spring-core/src/main/java/org/springframework/asm/SpringAsmInfo.java new file mode 100644 index 0000000..609551f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/SpringAsmInfo.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.asm; + +/** + * Utility class exposing constants related to Spring's internal repackaging + * of the ASM bytecode library: currently based on ASM 9.0 plus minor patches. + * + *

    See package-level javadocs for more + * information on {@code org.springframework.asm}. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.2 + */ +public final class SpringAsmInfo { + + /** + * The ASM compatibility version for Spring's ASM visitor implementations: + * currently {@link Opcodes#ASM10_EXPERIMENTAL}, as of Spring Framework 5.3. + */ + public static final int ASM_VERSION = Opcodes.ASM10_EXPERIMENTAL; + +} diff --git a/spring-core/src/main/java/org/springframework/asm/Symbol.java b/spring-core/src/main/java/org/springframework/asm/Symbol.java new file mode 100644 index 0000000..1088bbf --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/Symbol.java @@ -0,0 +1,243 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +package org.springframework.asm; + +/** + * An entry of the constant pool, of the BootstrapMethods attribute, or of the (ASM specific) type + * table of a class. + * + * @see JVMS + * 4.4 + * @see JVMS + * 4.7.23 + * @author Eric Bruneton + */ +abstract class Symbol { + + // Tag values for the constant pool entries (using the same order as in the JVMS). + + /** The tag value of CONSTANT_Class_info JVMS structures. */ + static final int CONSTANT_CLASS_TAG = 7; + + /** The tag value of CONSTANT_Fieldref_info JVMS structures. */ + static final int CONSTANT_FIELDREF_TAG = 9; + + /** The tag value of CONSTANT_Methodref_info JVMS structures. */ + static final int CONSTANT_METHODREF_TAG = 10; + + /** The tag value of CONSTANT_InterfaceMethodref_info JVMS structures. */ + static final int CONSTANT_INTERFACE_METHODREF_TAG = 11; + + /** The tag value of CONSTANT_String_info JVMS structures. */ + static final int CONSTANT_STRING_TAG = 8; + + /** The tag value of CONSTANT_Integer_info JVMS structures. */ + static final int CONSTANT_INTEGER_TAG = 3; + + /** The tag value of CONSTANT_Float_info JVMS structures. */ + static final int CONSTANT_FLOAT_TAG = 4; + + /** The tag value of CONSTANT_Long_info JVMS structures. */ + static final int CONSTANT_LONG_TAG = 5; + + /** The tag value of CONSTANT_Double_info JVMS structures. */ + static final int CONSTANT_DOUBLE_TAG = 6; + + /** The tag value of CONSTANT_NameAndType_info JVMS structures. */ + static final int CONSTANT_NAME_AND_TYPE_TAG = 12; + + /** The tag value of CONSTANT_Utf8_info JVMS structures. */ + static final int CONSTANT_UTF8_TAG = 1; + + /** The tag value of CONSTANT_MethodHandle_info JVMS structures. */ + static final int CONSTANT_METHOD_HANDLE_TAG = 15; + + /** The tag value of CONSTANT_MethodType_info JVMS structures. */ + static final int CONSTANT_METHOD_TYPE_TAG = 16; + + /** The tag value of CONSTANT_Dynamic_info JVMS structures. */ + static final int CONSTANT_DYNAMIC_TAG = 17; + + /** The tag value of CONSTANT_InvokeDynamic_info JVMS structures. */ + static final int CONSTANT_INVOKE_DYNAMIC_TAG = 18; + + /** The tag value of CONSTANT_Module_info JVMS structures. */ + static final int CONSTANT_MODULE_TAG = 19; + + /** The tag value of CONSTANT_Package_info JVMS structures. */ + static final int CONSTANT_PACKAGE_TAG = 20; + + // Tag values for the BootstrapMethods attribute entries (ASM specific tag). + + /** The tag value of the BootstrapMethods attribute entries. */ + static final int BOOTSTRAP_METHOD_TAG = 64; + + // Tag values for the type table entries (ASM specific tags). + + /** The tag value of a normal type entry in the (ASM specific) type table of a class. */ + static final int TYPE_TAG = 128; + + /** + * The tag value of an {@link Frame#ITEM_UNINITIALIZED} type entry in the type table of a class. + */ + static final int UNINITIALIZED_TYPE_TAG = 129; + + /** The tag value of a merged type entry in the (ASM specific) type table of a class. */ + static final int MERGED_TYPE_TAG = 130; + + // Instance fields. + + /** + * The index of this symbol in the constant pool, in the BootstrapMethods attribute, or in the + * (ASM specific) type table of a class (depending on the {@link #tag} value). + */ + final int index; + + /** + * A tag indicating the type of this symbol. Must be one of the static tag values defined in this + * class. + */ + final int tag; + + /** + * The internal name of the owner class of this symbol. Only used for {@link + * #CONSTANT_FIELDREF_TAG}, {@link #CONSTANT_METHODREF_TAG}, {@link + * #CONSTANT_INTERFACE_METHODREF_TAG}, and {@link #CONSTANT_METHOD_HANDLE_TAG} symbols. + */ + final String owner; + + /** + * The name of the class field or method corresponding to this symbol. Only used for {@link + * #CONSTANT_FIELDREF_TAG}, {@link #CONSTANT_METHODREF_TAG}, {@link + * #CONSTANT_INTERFACE_METHODREF_TAG}, {@link #CONSTANT_NAME_AND_TYPE_TAG}, {@link + * #CONSTANT_METHOD_HANDLE_TAG}, {@link #CONSTANT_DYNAMIC_TAG} and {@link + * #CONSTANT_INVOKE_DYNAMIC_TAG} symbols. + */ + final String name; + + /** + * The string value of this symbol. This is: + * + *

      + *
    • a field or method descriptor for {@link #CONSTANT_FIELDREF_TAG}, {@link + * #CONSTANT_METHODREF_TAG}, {@link #CONSTANT_INTERFACE_METHODREF_TAG}, {@link + * #CONSTANT_NAME_AND_TYPE_TAG}, {@link #CONSTANT_METHOD_HANDLE_TAG}, {@link + * #CONSTANT_METHOD_TYPE_TAG}, {@link #CONSTANT_DYNAMIC_TAG} and {@link + * #CONSTANT_INVOKE_DYNAMIC_TAG} symbols, + *
    • an arbitrary string for {@link #CONSTANT_UTF8_TAG} and {@link #CONSTANT_STRING_TAG} + * symbols, + *
    • an internal class name for {@link #CONSTANT_CLASS_TAG}, {@link #TYPE_TAG} and {@link + * #UNINITIALIZED_TYPE_TAG} symbols, + *
    • {@literal null} for the other types of symbol. + *
    + */ + final String value; + + /** + * The numeric value of this symbol. This is: + * + *
      + *
    • the symbol's value for {@link #CONSTANT_INTEGER_TAG},{@link #CONSTANT_FLOAT_TAG}, {@link + * #CONSTANT_LONG_TAG}, {@link #CONSTANT_DOUBLE_TAG}, + *
    • the CONSTANT_MethodHandle_info reference_kind field value for {@link + * #CONSTANT_METHOD_HANDLE_TAG} symbols, + *
    • the CONSTANT_InvokeDynamic_info bootstrap_method_attr_index field value for {@link + * #CONSTANT_INVOKE_DYNAMIC_TAG} symbols, + *
    • the offset of a bootstrap method in the BootstrapMethods boostrap_methods array, for + * {@link #CONSTANT_DYNAMIC_TAG} or {@link #BOOTSTRAP_METHOD_TAG} symbols, + *
    • the bytecode offset of the NEW instruction that created an {@link + * Frame#ITEM_UNINITIALIZED} type for {@link #UNINITIALIZED_TYPE_TAG} symbols, + *
    • the indices (in the class' type table) of two {@link #TYPE_TAG} source types for {@link + * #MERGED_TYPE_TAG} symbols, + *
    • 0 for the other types of symbol. + *
    + */ + final long data; + + /** + * Additional information about this symbol, generally computed lazily. Warning: the value of + * this field is ignored when comparing Symbol instances (to avoid duplicate entries in a + * SymbolTable). Therefore, this field should only contain data that can be computed from the + * other fields of this class. It contains: + * + *
      + *
    • the {@link Type#getArgumentsAndReturnSizes} of the symbol's method descriptor for {@link + * #CONSTANT_METHODREF_TAG}, {@link #CONSTANT_INTERFACE_METHODREF_TAG} and {@link + * #CONSTANT_INVOKE_DYNAMIC_TAG} symbols, + *
    • the index in the InnerClasses_attribute 'classes' array (plus one) corresponding to this + * class, for {@link #CONSTANT_CLASS_TAG} symbols, + *
    • the index (in the class' type table) of the merged type of the two source types for + * {@link #MERGED_TYPE_TAG} symbols, + *
    • 0 for the other types of symbol, or if this field has not been computed yet. + *
    + */ + int info; + + /** + * Constructs a new Symbol. This constructor can't be used directly because the Symbol class is + * abstract. Instead, use the factory methods of the {@link SymbolTable} class. + * + * @param index the symbol index in the constant pool, in the BootstrapMethods attribute, or in + * the (ASM specific) type table of a class (depending on 'tag'). + * @param tag the symbol type. Must be one of the static tag values defined in this class. + * @param owner The internal name of the symbol's owner class. Maybe {@literal null}. + * @param name The name of the symbol's corresponding class field or method. Maybe {@literal + * null}. + * @param value The string value of this symbol. Maybe {@literal null}. + * @param data The numeric value of this symbol. + */ + Symbol( + final int index, + final int tag, + final String owner, + final String name, + final String value, + final long data) { + this.index = index; + this.tag = tag; + this.owner = owner; + this.name = name; + this.value = value; + this.data = data; + } + + /** + * Returns the result {@link Type#getArgumentsAndReturnSizes} on {@link #value}. + * + * @return the result {@link Type#getArgumentsAndReturnSizes} on {@link #value} (memoized in + * {@link #info} for efficiency). This should only be used for {@link + * #CONSTANT_METHODREF_TAG}, {@link #CONSTANT_INTERFACE_METHODREF_TAG} and {@link + * #CONSTANT_INVOKE_DYNAMIC_TAG} symbols. + */ + int getArgumentsAndReturnSizes() { + if (info == 0) { + info = Type.getArgumentsAndReturnSizes(value); + } + return info; + } +} diff --git a/spring-core/src/main/java/org/springframework/asm/SymbolTable.java b/spring-core/src/main/java/org/springframework/asm/SymbolTable.java new file mode 100644 index 0000000..e4c4a84 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/SymbolTable.java @@ -0,0 +1,1322 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +package org.springframework.asm; + +/** + * The constant pool entries, the BootstrapMethods attribute entries and the (ASM specific) type + * table entries of a class. + * + * @author Eric Bruneton + * @see JVMS + * 4.4 + * @see JVMS + * 4.7.23 + */ +final class SymbolTable { + + /** + * The ClassWriter to which this SymbolTable belongs. This is only used to get access to {@link + * ClassWriter#getCommonSuperClass} and to serialize custom attributes with {@link + * Attribute#write}. + */ + final ClassWriter classWriter; + + /** + * The ClassReader from which this SymbolTable was constructed, or {@literal null} if it was + * constructed from scratch. + */ + private final ClassReader sourceClassReader; + + /** The major version number of the class to which this symbol table belongs. */ + private int majorVersion; + + /** The internal name of the class to which this symbol table belongs. */ + private String className; + + /** + * The total number of {@link Entry} instances in {@link #entries}. This includes entries that are + * accessible (recursively) via {@link Entry#next}. + */ + private int entryCount; + + /** + * A hash set of all the entries in this SymbolTable (this includes the constant pool entries, the + * bootstrap method entries and the type table entries). Each {@link Entry} instance is stored at + * the array index given by its hash code modulo the array size. If several entries must be stored + * at the same array index, they are linked together via their {@link Entry#next} field. The + * factory methods of this class make sure that this table does not contain duplicated entries. + */ + private Entry[] entries; + + /** + * The number of constant pool items in {@link #constantPool}, plus 1. The first constant pool + * item has index 1, and long and double items count for two items. + */ + private int constantPoolCount; + + /** + * The content of the ClassFile's constant_pool JVMS structure corresponding to this SymbolTable. + * The ClassFile's constant_pool_count field is not included. + */ + private ByteVector constantPool; + + /** + * The number of bootstrap methods in {@link #bootstrapMethods}. Corresponds to the + * BootstrapMethods_attribute's num_bootstrap_methods field value. + */ + private int bootstrapMethodCount; + + /** + * The content of the BootstrapMethods attribute 'bootstrap_methods' array corresponding to this + * SymbolTable. Note that the first 6 bytes of the BootstrapMethods_attribute, and its + * num_bootstrap_methods field, are not included. + */ + private ByteVector bootstrapMethods; + + /** + * The actual number of elements in {@link #typeTable}. These elements are stored from index 0 to + * typeCount (excluded). The other array entries are empty. + */ + private int typeCount; + + /** + * An ASM specific type table used to temporarily store internal names that will not necessarily + * be stored in the constant pool. This type table is used by the control flow and data flow + * analysis algorithm used to compute stack map frames from scratch. This array stores {@link + * Symbol#TYPE_TAG} and {@link Symbol#UNINITIALIZED_TYPE_TAG}) Symbol. The type symbol at index + * {@code i} has its {@link Symbol#index} equal to {@code i} (and vice versa). + */ + private Entry[] typeTable; + + /** + * Constructs a new, empty SymbolTable for the given ClassWriter. + * + * @param classWriter a ClassWriter. + */ + SymbolTable(final ClassWriter classWriter) { + this.classWriter = classWriter; + this.sourceClassReader = null; + this.entries = new Entry[256]; + this.constantPoolCount = 1; + this.constantPool = new ByteVector(); + } + + /** + * Constructs a new SymbolTable for the given ClassWriter, initialized with the constant pool and + * bootstrap methods of the given ClassReader. + * + * @param classWriter a ClassWriter. + * @param classReader the ClassReader whose constant pool and bootstrap methods must be copied to + * initialize the SymbolTable. + */ + SymbolTable(final ClassWriter classWriter, final ClassReader classReader) { + this.classWriter = classWriter; + this.sourceClassReader = classReader; + + // Copy the constant pool binary content. + byte[] inputBytes = classReader.classFileBuffer; + int constantPoolOffset = classReader.getItem(1) - 1; + int constantPoolLength = classReader.header - constantPoolOffset; + constantPoolCount = classReader.getItemCount(); + constantPool = new ByteVector(constantPoolLength); + constantPool.putByteArray(inputBytes, constantPoolOffset, constantPoolLength); + + // Add the constant pool items in the symbol table entries. Reserve enough space in 'entries' to + // avoid too many hash set collisions (entries is not dynamically resized by the addConstant* + // method calls below), and to account for bootstrap method entries. + entries = new Entry[constantPoolCount * 2]; + char[] charBuffer = new char[classReader.getMaxStringLength()]; + boolean hasBootstrapMethods = false; + int itemIndex = 1; + while (itemIndex < constantPoolCount) { + int itemOffset = classReader.getItem(itemIndex); + int itemTag = inputBytes[itemOffset - 1]; + int nameAndTypeItemOffset; + switch (itemTag) { + case Symbol.CONSTANT_FIELDREF_TAG: + case Symbol.CONSTANT_METHODREF_TAG: + case Symbol.CONSTANT_INTERFACE_METHODREF_TAG: + nameAndTypeItemOffset = + classReader.getItem(classReader.readUnsignedShort(itemOffset + 2)); + addConstantMemberReference( + itemIndex, + itemTag, + classReader.readClass(itemOffset, charBuffer), + classReader.readUTF8(nameAndTypeItemOffset, charBuffer), + classReader.readUTF8(nameAndTypeItemOffset + 2, charBuffer)); + break; + case Symbol.CONSTANT_INTEGER_TAG: + case Symbol.CONSTANT_FLOAT_TAG: + addConstantIntegerOrFloat(itemIndex, itemTag, classReader.readInt(itemOffset)); + break; + case Symbol.CONSTANT_NAME_AND_TYPE_TAG: + addConstantNameAndType( + itemIndex, + classReader.readUTF8(itemOffset, charBuffer), + classReader.readUTF8(itemOffset + 2, charBuffer)); + break; + case Symbol.CONSTANT_LONG_TAG: + case Symbol.CONSTANT_DOUBLE_TAG: + addConstantLongOrDouble(itemIndex, itemTag, classReader.readLong(itemOffset)); + break; + case Symbol.CONSTANT_UTF8_TAG: + addConstantUtf8(itemIndex, classReader.readUtf(itemIndex, charBuffer)); + break; + case Symbol.CONSTANT_METHOD_HANDLE_TAG: + int memberRefItemOffset = + classReader.getItem(classReader.readUnsignedShort(itemOffset + 1)); + nameAndTypeItemOffset = + classReader.getItem(classReader.readUnsignedShort(memberRefItemOffset + 2)); + addConstantMethodHandle( + itemIndex, + classReader.readByte(itemOffset), + classReader.readClass(memberRefItemOffset, charBuffer), + classReader.readUTF8(nameAndTypeItemOffset, charBuffer), + classReader.readUTF8(nameAndTypeItemOffset + 2, charBuffer)); + break; + case Symbol.CONSTANT_DYNAMIC_TAG: + case Symbol.CONSTANT_INVOKE_DYNAMIC_TAG: + hasBootstrapMethods = true; + nameAndTypeItemOffset = + classReader.getItem(classReader.readUnsignedShort(itemOffset + 2)); + addConstantDynamicOrInvokeDynamicReference( + itemTag, + itemIndex, + classReader.readUTF8(nameAndTypeItemOffset, charBuffer), + classReader.readUTF8(nameAndTypeItemOffset + 2, charBuffer), + classReader.readUnsignedShort(itemOffset)); + break; + case Symbol.CONSTANT_STRING_TAG: + case Symbol.CONSTANT_CLASS_TAG: + case Symbol.CONSTANT_METHOD_TYPE_TAG: + case Symbol.CONSTANT_MODULE_TAG: + case Symbol.CONSTANT_PACKAGE_TAG: + addConstantUtf8Reference( + itemIndex, itemTag, classReader.readUTF8(itemOffset, charBuffer)); + break; + default: + throw new IllegalArgumentException(); + } + itemIndex += + (itemTag == Symbol.CONSTANT_LONG_TAG || itemTag == Symbol.CONSTANT_DOUBLE_TAG) ? 2 : 1; + } + + // Copy the BootstrapMethods, if any. + if (hasBootstrapMethods) { + copyBootstrapMethods(classReader, charBuffer); + } + } + + /** + * Read the BootstrapMethods 'bootstrap_methods' array binary content and add them as entries of + * the SymbolTable. + * + * @param classReader the ClassReader whose bootstrap methods must be copied to initialize the + * SymbolTable. + * @param charBuffer a buffer used to read strings in the constant pool. + */ + private void copyBootstrapMethods(final ClassReader classReader, final char[] charBuffer) { + // Find attributOffset of the 'bootstrap_methods' array. + byte[] inputBytes = classReader.classFileBuffer; + int currentAttributeOffset = classReader.getFirstAttributeOffset(); + for (int i = classReader.readUnsignedShort(currentAttributeOffset - 2); i > 0; --i) { + String attributeName = classReader.readUTF8(currentAttributeOffset, charBuffer); + if (Constants.BOOTSTRAP_METHODS.equals(attributeName)) { + bootstrapMethodCount = classReader.readUnsignedShort(currentAttributeOffset + 6); + break; + } + currentAttributeOffset += 6 + classReader.readInt(currentAttributeOffset + 2); + } + if (bootstrapMethodCount > 0) { + // Compute the offset and the length of the BootstrapMethods 'bootstrap_methods' array. + int bootstrapMethodsOffset = currentAttributeOffset + 8; + int bootstrapMethodsLength = classReader.readInt(currentAttributeOffset + 2) - 2; + bootstrapMethods = new ByteVector(bootstrapMethodsLength); + bootstrapMethods.putByteArray(inputBytes, bootstrapMethodsOffset, bootstrapMethodsLength); + + // Add each bootstrap method in the symbol table entries. + int currentOffset = bootstrapMethodsOffset; + for (int i = 0; i < bootstrapMethodCount; i++) { + int offset = currentOffset - bootstrapMethodsOffset; + int bootstrapMethodRef = classReader.readUnsignedShort(currentOffset); + currentOffset += 2; + int numBootstrapArguments = classReader.readUnsignedShort(currentOffset); + currentOffset += 2; + int hashCode = classReader.readConst(bootstrapMethodRef, charBuffer).hashCode(); + while (numBootstrapArguments-- > 0) { + int bootstrapArgument = classReader.readUnsignedShort(currentOffset); + currentOffset += 2; + hashCode ^= classReader.readConst(bootstrapArgument, charBuffer).hashCode(); + } + add(new Entry(i, Symbol.BOOTSTRAP_METHOD_TAG, offset, hashCode & 0x7FFFFFFF)); + } + } + } + + /** + * Returns the ClassReader from which this SymbolTable was constructed. + * + * @return the ClassReader from which this SymbolTable was constructed, or {@literal null} if it + * was constructed from scratch. + */ + ClassReader getSource() { + return sourceClassReader; + } + + /** + * Returns the major version of the class to which this symbol table belongs. + * + * @return the major version of the class to which this symbol table belongs. + */ + int getMajorVersion() { + return majorVersion; + } + + /** + * Returns the internal name of the class to which this symbol table belongs. + * + * @return the internal name of the class to which this symbol table belongs. + */ + String getClassName() { + return className; + } + + /** + * Sets the major version and the name of the class to which this symbol table belongs. Also adds + * the class name to the constant pool. + * + * @param majorVersion a major ClassFile version number. + * @param className an internal class name. + * @return the constant pool index of a new or already existing Symbol with the given class name. + */ + int setMajorVersionAndClassName(final int majorVersion, final String className) { + this.majorVersion = majorVersion; + this.className = className; + return addConstantClass(className).index; + } + + /** + * Returns the number of items in this symbol table's constant_pool array (plus 1). + * + * @return the number of items in this symbol table's constant_pool array (plus 1). + */ + int getConstantPoolCount() { + return constantPoolCount; + } + + /** + * Returns the length in bytes of this symbol table's constant_pool array. + * + * @return the length in bytes of this symbol table's constant_pool array. + */ + int getConstantPoolLength() { + return constantPool.length; + } + + /** + * Puts this symbol table's constant_pool array in the given ByteVector, preceded by the + * constant_pool_count value. + * + * @param output where the JVMS ClassFile's constant_pool array must be put. + */ + void putConstantPool(final ByteVector output) { + output.putShort(constantPoolCount).putByteArray(constantPool.data, 0, constantPool.length); + } + + /** + * Returns the size in bytes of this symbol table's BootstrapMethods attribute. Also adds the + * attribute name in the constant pool. + * + * @return the size in bytes of this symbol table's BootstrapMethods attribute. + */ + int computeBootstrapMethodsSize() { + if (bootstrapMethods != null) { + addConstantUtf8(Constants.BOOTSTRAP_METHODS); + return 8 + bootstrapMethods.length; + } else { + return 0; + } + } + + /** + * Puts this symbol table's BootstrapMethods attribute in the given ByteVector. This includes the + * 6 attribute header bytes and the num_bootstrap_methods value. + * + * @param output where the JVMS BootstrapMethods attribute must be put. + */ + void putBootstrapMethods(final ByteVector output) { + if (bootstrapMethods != null) { + output + .putShort(addConstantUtf8(Constants.BOOTSTRAP_METHODS)) + .putInt(bootstrapMethods.length + 2) + .putShort(bootstrapMethodCount) + .putByteArray(bootstrapMethods.data, 0, bootstrapMethods.length); + } + } + + // ----------------------------------------------------------------------------------------------- + // Generic symbol table entries management. + // ----------------------------------------------------------------------------------------------- + + /** + * Returns the list of entries which can potentially have the given hash code. + * + * @param hashCode a {@link Entry#hashCode} value. + * @return the list of entries which can potentially have the given hash code. The list is stored + * via the {@link Entry#next} field. + */ + private Entry get(final int hashCode) { + return entries[hashCode % entries.length]; + } + + /** + * Puts the given entry in the {@link #entries} hash set. This method does not check + * whether {@link #entries} already contains a similar entry or not. {@link #entries} is resized + * if necessary to avoid hash collisions (multiple entries needing to be stored at the same {@link + * #entries} array index) as much as possible, with reasonable memory usage. + * + * @param entry an Entry (which must not already be contained in {@link #entries}). + * @return the given entry + */ + private Entry put(final Entry entry) { + if (entryCount > (entries.length * 3) / 4) { + int currentCapacity = entries.length; + int newCapacity = currentCapacity * 2 + 1; + Entry[] newEntries = new Entry[newCapacity]; + for (int i = currentCapacity - 1; i >= 0; --i) { + Entry currentEntry = entries[i]; + while (currentEntry != null) { + int newCurrentEntryIndex = currentEntry.hashCode % newCapacity; + Entry nextEntry = currentEntry.next; + currentEntry.next = newEntries[newCurrentEntryIndex]; + newEntries[newCurrentEntryIndex] = currentEntry; + currentEntry = nextEntry; + } + } + entries = newEntries; + } + entryCount++; + int index = entry.hashCode % entries.length; + entry.next = entries[index]; + return entries[index] = entry; + } + + /** + * Adds the given entry in the {@link #entries} hash set. This method does not check + * whether {@link #entries} already contains a similar entry or not, and does not resize + * {@link #entries} if necessary. + * + * @param entry an Entry (which must not already be contained in {@link #entries}). + */ + private void add(final Entry entry) { + entryCount++; + int index = entry.hashCode % entries.length; + entry.next = entries[index]; + entries[index] = entry; + } + + // ----------------------------------------------------------------------------------------------- + // Constant pool entries management. + // ----------------------------------------------------------------------------------------------- + + /** + * Adds a number or string constant to the constant pool of this symbol table. Does nothing if the + * constant pool already contains a similar item. + * + * @param value the value of the constant to be added to the constant pool. This parameter must be + * an {@link Integer}, {@link Byte}, {@link Character}, {@link Short}, {@link Boolean}, {@link + * Float}, {@link Long}, {@link Double}, {@link String}, {@link Type} or {@link Handle}. + * @return a new or already existing Symbol with the given value. + */ + Symbol addConstant(final Object value) { + if (value instanceof Integer) { + return addConstantInteger(((Integer) value).intValue()); + } else if (value instanceof Byte) { + return addConstantInteger(((Byte) value).intValue()); + } else if (value instanceof Character) { + return addConstantInteger(((Character) value).charValue()); + } else if (value instanceof Short) { + return addConstantInteger(((Short) value).intValue()); + } else if (value instanceof Boolean) { + return addConstantInteger(((Boolean) value).booleanValue() ? 1 : 0); + } else if (value instanceof Float) { + return addConstantFloat(((Float) value).floatValue()); + } else if (value instanceof Long) { + return addConstantLong(((Long) value).longValue()); + } else if (value instanceof Double) { + return addConstantDouble(((Double) value).doubleValue()); + } else if (value instanceof String) { + return addConstantString((String) value); + } else if (value instanceof Type) { + Type type = (Type) value; + int typeSort = type.getSort(); + if (typeSort == Type.OBJECT) { + return addConstantClass(type.getInternalName()); + } else if (typeSort == Type.METHOD) { + return addConstantMethodType(type.getDescriptor()); + } else { // type is a primitive or array type. + return addConstantClass(type.getDescriptor()); + } + } else if (value instanceof Handle) { + Handle handle = (Handle) value; + return addConstantMethodHandle( + handle.getTag(), + handle.getOwner(), + handle.getName(), + handle.getDesc(), + handle.isInterface()); + } else if (value instanceof ConstantDynamic) { + ConstantDynamic constantDynamic = (ConstantDynamic) value; + return addConstantDynamic( + constantDynamic.getName(), + constantDynamic.getDescriptor(), + constantDynamic.getBootstrapMethod(), + constantDynamic.getBootstrapMethodArgumentsUnsafe()); + } else { + throw new IllegalArgumentException("value " + value); + } + } + + /** + * Adds a CONSTANT_Class_info to the constant pool of this symbol table. Does nothing if the + * constant pool already contains a similar item. + * + * @param value the internal name of a class. + * @return a new or already existing Symbol with the given value. + */ + Symbol addConstantClass(final String value) { + return addConstantUtf8Reference(Symbol.CONSTANT_CLASS_TAG, value); + } + + /** + * Adds a CONSTANT_Fieldref_info to the constant pool of this symbol table. Does nothing if the + * constant pool already contains a similar item. + * + * @param owner the internal name of a class. + * @param name a field name. + * @param descriptor a field descriptor. + * @return a new or already existing Symbol with the given value. + */ + Symbol addConstantFieldref(final String owner, final String name, final String descriptor) { + return addConstantMemberReference(Symbol.CONSTANT_FIELDREF_TAG, owner, name, descriptor); + } + + /** + * Adds a CONSTANT_Methodref_info or CONSTANT_InterfaceMethodref_info to the constant pool of this + * symbol table. Does nothing if the constant pool already contains a similar item. + * + * @param owner the internal name of a class. + * @param name a method name. + * @param descriptor a method descriptor. + * @param isInterface whether owner is an interface or not. + * @return a new or already existing Symbol with the given value. + */ + Symbol addConstantMethodref( + final String owner, final String name, final String descriptor, final boolean isInterface) { + int tag = isInterface ? Symbol.CONSTANT_INTERFACE_METHODREF_TAG : Symbol.CONSTANT_METHODREF_TAG; + return addConstantMemberReference(tag, owner, name, descriptor); + } + + /** + * Adds a CONSTANT_Fieldref_info, CONSTANT_Methodref_info or CONSTANT_InterfaceMethodref_info to + * the constant pool of this symbol table. Does nothing if the constant pool already contains a + * similar item. + * + * @param tag one of {@link Symbol#CONSTANT_FIELDREF_TAG}, {@link Symbol#CONSTANT_METHODREF_TAG} + * or {@link Symbol#CONSTANT_INTERFACE_METHODREF_TAG}. + * @param owner the internal name of a class. + * @param name a field or method name. + * @param descriptor a field or method descriptor. + * @return a new or already existing Symbol with the given value. + */ + private Entry addConstantMemberReference( + final int tag, final String owner, final String name, final String descriptor) { + int hashCode = hash(tag, owner, name, descriptor); + Entry entry = get(hashCode); + while (entry != null) { + if (entry.tag == tag + && entry.hashCode == hashCode + && entry.owner.equals(owner) + && entry.name.equals(name) + && entry.value.equals(descriptor)) { + return entry; + } + entry = entry.next; + } + constantPool.put122( + tag, addConstantClass(owner).index, addConstantNameAndType(name, descriptor)); + return put(new Entry(constantPoolCount++, tag, owner, name, descriptor, 0, hashCode)); + } + + /** + * Adds a new CONSTANT_Fieldref_info, CONSTANT_Methodref_info or CONSTANT_InterfaceMethodref_info + * to the constant pool of this symbol table. + * + * @param index the constant pool index of the new Symbol. + * @param tag one of {@link Symbol#CONSTANT_FIELDREF_TAG}, {@link Symbol#CONSTANT_METHODREF_TAG} + * or {@link Symbol#CONSTANT_INTERFACE_METHODREF_TAG}. + * @param owner the internal name of a class. + * @param name a field or method name. + * @param descriptor a field or method descriptor. + */ + private void addConstantMemberReference( + final int index, + final int tag, + final String owner, + final String name, + final String descriptor) { + add(new Entry(index, tag, owner, name, descriptor, 0, hash(tag, owner, name, descriptor))); + } + + /** + * Adds a CONSTANT_String_info to the constant pool of this symbol table. Does nothing if the + * constant pool already contains a similar item. + * + * @param value a string. + * @return a new or already existing Symbol with the given value. + */ + Symbol addConstantString(final String value) { + return addConstantUtf8Reference(Symbol.CONSTANT_STRING_TAG, value); + } + + /** + * Adds a CONSTANT_Integer_info to the constant pool of this symbol table. Does nothing if the + * constant pool already contains a similar item. + * + * @param value an int. + * @return a new or already existing Symbol with the given value. + */ + Symbol addConstantInteger(final int value) { + return addConstantIntegerOrFloat(Symbol.CONSTANT_INTEGER_TAG, value); + } + + /** + * Adds a CONSTANT_Float_info to the constant pool of this symbol table. Does nothing if the + * constant pool already contains a similar item. + * + * @param value a float. + * @return a new or already existing Symbol with the given value. + */ + Symbol addConstantFloat(final float value) { + return addConstantIntegerOrFloat(Symbol.CONSTANT_FLOAT_TAG, Float.floatToRawIntBits(value)); + } + + /** + * Adds a CONSTANT_Integer_info or CONSTANT_Float_info to the constant pool of this symbol table. + * Does nothing if the constant pool already contains a similar item. + * + * @param tag one of {@link Symbol#CONSTANT_INTEGER_TAG} or {@link Symbol#CONSTANT_FLOAT_TAG}. + * @param value an int or float. + * @return a constant pool constant with the given tag and primitive values. + */ + private Symbol addConstantIntegerOrFloat(final int tag, final int value) { + int hashCode = hash(tag, value); + Entry entry = get(hashCode); + while (entry != null) { + if (entry.tag == tag && entry.hashCode == hashCode && entry.data == value) { + return entry; + } + entry = entry.next; + } + constantPool.putByte(tag).putInt(value); + return put(new Entry(constantPoolCount++, tag, value, hashCode)); + } + + /** + * Adds a new CONSTANT_Integer_info or CONSTANT_Float_info to the constant pool of this symbol + * table. + * + * @param index the constant pool index of the new Symbol. + * @param tag one of {@link Symbol#CONSTANT_INTEGER_TAG} or {@link Symbol#CONSTANT_FLOAT_TAG}. + * @param value an int or float. + */ + private void addConstantIntegerOrFloat(final int index, final int tag, final int value) { + add(new Entry(index, tag, value, hash(tag, value))); + } + + /** + * Adds a CONSTANT_Long_info to the constant pool of this symbol table. Does nothing if the + * constant pool already contains a similar item. + * + * @param value a long. + * @return a new or already existing Symbol with the given value. + */ + Symbol addConstantLong(final long value) { + return addConstantLongOrDouble(Symbol.CONSTANT_LONG_TAG, value); + } + + /** + * Adds a CONSTANT_Double_info to the constant pool of this symbol table. Does nothing if the + * constant pool already contains a similar item. + * + * @param value a double. + * @return a new or already existing Symbol with the given value. + */ + Symbol addConstantDouble(final double value) { + return addConstantLongOrDouble(Symbol.CONSTANT_DOUBLE_TAG, Double.doubleToRawLongBits(value)); + } + + /** + * Adds a CONSTANT_Long_info or CONSTANT_Double_info to the constant pool of this symbol table. + * Does nothing if the constant pool already contains a similar item. + * + * @param tag one of {@link Symbol#CONSTANT_LONG_TAG} or {@link Symbol#CONSTANT_DOUBLE_TAG}. + * @param value a long or double. + * @return a constant pool constant with the given tag and primitive values. + */ + private Symbol addConstantLongOrDouble(final int tag, final long value) { + int hashCode = hash(tag, value); + Entry entry = get(hashCode); + while (entry != null) { + if (entry.tag == tag && entry.hashCode == hashCode && entry.data == value) { + return entry; + } + entry = entry.next; + } + int index = constantPoolCount; + constantPool.putByte(tag).putLong(value); + constantPoolCount += 2; + return put(new Entry(index, tag, value, hashCode)); + } + + /** + * Adds a new CONSTANT_Long_info or CONSTANT_Double_info to the constant pool of this symbol + * table. + * + * @param index the constant pool index of the new Symbol. + * @param tag one of {@link Symbol#CONSTANT_LONG_TAG} or {@link Symbol#CONSTANT_DOUBLE_TAG}. + * @param value a long or double. + */ + private void addConstantLongOrDouble(final int index, final int tag, final long value) { + add(new Entry(index, tag, value, hash(tag, value))); + } + + /** + * Adds a CONSTANT_NameAndType_info to the constant pool of this symbol table. Does nothing if the + * constant pool already contains a similar item. + * + * @param name a field or method name. + * @param descriptor a field or method descriptor. + * @return a new or already existing Symbol with the given value. + */ + int addConstantNameAndType(final String name, final String descriptor) { + final int tag = Symbol.CONSTANT_NAME_AND_TYPE_TAG; + int hashCode = hash(tag, name, descriptor); + Entry entry = get(hashCode); + while (entry != null) { + if (entry.tag == tag + && entry.hashCode == hashCode + && entry.name.equals(name) + && entry.value.equals(descriptor)) { + return entry.index; + } + entry = entry.next; + } + constantPool.put122(tag, addConstantUtf8(name), addConstantUtf8(descriptor)); + return put(new Entry(constantPoolCount++, tag, name, descriptor, hashCode)).index; + } + + /** + * Adds a new CONSTANT_NameAndType_info to the constant pool of this symbol table. + * + * @param index the constant pool index of the new Symbol. + * @param name a field or method name. + * @param descriptor a field or method descriptor. + */ + private void addConstantNameAndType(final int index, final String name, final String descriptor) { + final int tag = Symbol.CONSTANT_NAME_AND_TYPE_TAG; + add(new Entry(index, tag, name, descriptor, hash(tag, name, descriptor))); + } + + /** + * Adds a CONSTANT_Utf8_info to the constant pool of this symbol table. Does nothing if the + * constant pool already contains a similar item. + * + * @param value a string. + * @return a new or already existing Symbol with the given value. + */ + int addConstantUtf8(final String value) { + int hashCode = hash(Symbol.CONSTANT_UTF8_TAG, value); + Entry entry = get(hashCode); + while (entry != null) { + if (entry.tag == Symbol.CONSTANT_UTF8_TAG + && entry.hashCode == hashCode + && entry.value.equals(value)) { + return entry.index; + } + entry = entry.next; + } + constantPool.putByte(Symbol.CONSTANT_UTF8_TAG).putUTF8(value); + return put(new Entry(constantPoolCount++, Symbol.CONSTANT_UTF8_TAG, value, hashCode)).index; + } + + /** + * Adds a new CONSTANT_String_info to the constant pool of this symbol table. + * + * @param index the constant pool index of the new Symbol. + * @param value a string. + */ + private void addConstantUtf8(final int index, final String value) { + add(new Entry(index, Symbol.CONSTANT_UTF8_TAG, value, hash(Symbol.CONSTANT_UTF8_TAG, value))); + } + + /** + * Adds a CONSTANT_MethodHandle_info to the constant pool of this symbol table. Does nothing if + * the constant pool already contains a similar item. + * + * @param referenceKind one of {@link Opcodes#H_GETFIELD}, {@link Opcodes#H_GETSTATIC}, {@link + * Opcodes#H_PUTFIELD}, {@link Opcodes#H_PUTSTATIC}, {@link Opcodes#H_INVOKEVIRTUAL}, {@link + * Opcodes#H_INVOKESTATIC}, {@link Opcodes#H_INVOKESPECIAL}, {@link + * Opcodes#H_NEWINVOKESPECIAL} or {@link Opcodes#H_INVOKEINTERFACE}. + * @param owner the internal name of a class of interface. + * @param name a field or method name. + * @param descriptor a field or method descriptor. + * @param isInterface whether owner is an interface or not. + * @return a new or already existing Symbol with the given value. + */ + Symbol addConstantMethodHandle( + final int referenceKind, + final String owner, + final String name, + final String descriptor, + final boolean isInterface) { + final int tag = Symbol.CONSTANT_METHOD_HANDLE_TAG; + // Note that we don't need to include isInterface in the hash computation, because it is + // redundant with owner (we can't have the same owner with different isInterface values). + int hashCode = hash(tag, owner, name, descriptor, referenceKind); + Entry entry = get(hashCode); + while (entry != null) { + if (entry.tag == tag + && entry.hashCode == hashCode + && entry.data == referenceKind + && entry.owner.equals(owner) + && entry.name.equals(name) + && entry.value.equals(descriptor)) { + return entry; + } + entry = entry.next; + } + if (referenceKind <= Opcodes.H_PUTSTATIC) { + constantPool.put112(tag, referenceKind, addConstantFieldref(owner, name, descriptor).index); + } else { + constantPool.put112( + tag, referenceKind, addConstantMethodref(owner, name, descriptor, isInterface).index); + } + return put( + new Entry(constantPoolCount++, tag, owner, name, descriptor, referenceKind, hashCode)); + } + + /** + * Adds a new CONSTANT_MethodHandle_info to the constant pool of this symbol table. + * + * @param index the constant pool index of the new Symbol. + * @param referenceKind one of {@link Opcodes#H_GETFIELD}, {@link Opcodes#H_GETSTATIC}, {@link + * Opcodes#H_PUTFIELD}, {@link Opcodes#H_PUTSTATIC}, {@link Opcodes#H_INVOKEVIRTUAL}, {@link + * Opcodes#H_INVOKESTATIC}, {@link Opcodes#H_INVOKESPECIAL}, {@link + * Opcodes#H_NEWINVOKESPECIAL} or {@link Opcodes#H_INVOKEINTERFACE}. + * @param owner the internal name of a class of interface. + * @param name a field or method name. + * @param descriptor a field or method descriptor. + */ + private void addConstantMethodHandle( + final int index, + final int referenceKind, + final String owner, + final String name, + final String descriptor) { + final int tag = Symbol.CONSTANT_METHOD_HANDLE_TAG; + int hashCode = hash(tag, owner, name, descriptor, referenceKind); + add(new Entry(index, tag, owner, name, descriptor, referenceKind, hashCode)); + } + + /** + * Adds a CONSTANT_MethodType_info to the constant pool of this symbol table. Does nothing if the + * constant pool already contains a similar item. + * + * @param methodDescriptor a method descriptor. + * @return a new or already existing Symbol with the given value. + */ + Symbol addConstantMethodType(final String methodDescriptor) { + return addConstantUtf8Reference(Symbol.CONSTANT_METHOD_TYPE_TAG, methodDescriptor); + } + + /** + * Adds a CONSTANT_Dynamic_info to the constant pool of this symbol table. Also adds the related + * bootstrap method to the BootstrapMethods of this symbol table. Does nothing if the constant + * pool already contains a similar item. + * + * @param name a method name. + * @param descriptor a field descriptor. + * @param bootstrapMethodHandle a bootstrap method handle. + * @param bootstrapMethodArguments the bootstrap method arguments. + * @return a new or already existing Symbol with the given value. + */ + Symbol addConstantDynamic( + final String name, + final String descriptor, + final Handle bootstrapMethodHandle, + final Object... bootstrapMethodArguments) { + Symbol bootstrapMethod = addBootstrapMethod(bootstrapMethodHandle, bootstrapMethodArguments); + return addConstantDynamicOrInvokeDynamicReference( + Symbol.CONSTANT_DYNAMIC_TAG, name, descriptor, bootstrapMethod.index); + } + + /** + * Adds a CONSTANT_InvokeDynamic_info to the constant pool of this symbol table. Also adds the + * related bootstrap method to the BootstrapMethods of this symbol table. Does nothing if the + * constant pool already contains a similar item. + * + * @param name a method name. + * @param descriptor a method descriptor. + * @param bootstrapMethodHandle a bootstrap method handle. + * @param bootstrapMethodArguments the bootstrap method arguments. + * @return a new or already existing Symbol with the given value. + */ + Symbol addConstantInvokeDynamic( + final String name, + final String descriptor, + final Handle bootstrapMethodHandle, + final Object... bootstrapMethodArguments) { + Symbol bootstrapMethod = addBootstrapMethod(bootstrapMethodHandle, bootstrapMethodArguments); + return addConstantDynamicOrInvokeDynamicReference( + Symbol.CONSTANT_INVOKE_DYNAMIC_TAG, name, descriptor, bootstrapMethod.index); + } + + /** + * Adds a CONSTANT_Dynamic or a CONSTANT_InvokeDynamic_info to the constant pool of this symbol + * table. Does nothing if the constant pool already contains a similar item. + * + * @param tag one of {@link Symbol#CONSTANT_DYNAMIC_TAG} or {@link + * Symbol#CONSTANT_INVOKE_DYNAMIC_TAG}. + * @param name a method name. + * @param descriptor a field descriptor for CONSTANT_DYNAMIC_TAG) or a method descriptor for + * CONSTANT_INVOKE_DYNAMIC_TAG. + * @param bootstrapMethodIndex the index of a bootstrap method in the BootstrapMethods attribute. + * @return a new or already existing Symbol with the given value. + */ + private Symbol addConstantDynamicOrInvokeDynamicReference( + final int tag, final String name, final String descriptor, final int bootstrapMethodIndex) { + int hashCode = hash(tag, name, descriptor, bootstrapMethodIndex); + Entry entry = get(hashCode); + while (entry != null) { + if (entry.tag == tag + && entry.hashCode == hashCode + && entry.data == bootstrapMethodIndex + && entry.name.equals(name) + && entry.value.equals(descriptor)) { + return entry; + } + entry = entry.next; + } + constantPool.put122(tag, bootstrapMethodIndex, addConstantNameAndType(name, descriptor)); + return put( + new Entry( + constantPoolCount++, tag, null, name, descriptor, bootstrapMethodIndex, hashCode)); + } + + /** + * Adds a new CONSTANT_Dynamic_info or CONSTANT_InvokeDynamic_info to the constant pool of this + * symbol table. + * + * @param tag one of {@link Symbol#CONSTANT_DYNAMIC_TAG} or {@link + * Symbol#CONSTANT_INVOKE_DYNAMIC_TAG}. + * @param index the constant pool index of the new Symbol. + * @param name a method name. + * @param descriptor a field descriptor for CONSTANT_DYNAMIC_TAG or a method descriptor for + * CONSTANT_INVOKE_DYNAMIC_TAG. + * @param bootstrapMethodIndex the index of a bootstrap method in the BootstrapMethods attribute. + */ + private void addConstantDynamicOrInvokeDynamicReference( + final int tag, + final int index, + final String name, + final String descriptor, + final int bootstrapMethodIndex) { + int hashCode = hash(tag, name, descriptor, bootstrapMethodIndex); + add(new Entry(index, tag, null, name, descriptor, bootstrapMethodIndex, hashCode)); + } + + /** + * Adds a CONSTANT_Module_info to the constant pool of this symbol table. Does nothing if the + * constant pool already contains a similar item. + * + * @param moduleName a fully qualified name (using dots) of a module. + * @return a new or already existing Symbol with the given value. + */ + Symbol addConstantModule(final String moduleName) { + return addConstantUtf8Reference(Symbol.CONSTANT_MODULE_TAG, moduleName); + } + + /** + * Adds a CONSTANT_Package_info to the constant pool of this symbol table. Does nothing if the + * constant pool already contains a similar item. + * + * @param packageName the internal name of a package. + * @return a new or already existing Symbol with the given value. + */ + Symbol addConstantPackage(final String packageName) { + return addConstantUtf8Reference(Symbol.CONSTANT_PACKAGE_TAG, packageName); + } + + /** + * Adds a CONSTANT_Class_info, CONSTANT_String_info, CONSTANT_MethodType_info, + * CONSTANT_Module_info or CONSTANT_Package_info to the constant pool of this symbol table. Does + * nothing if the constant pool already contains a similar item. + * + * @param tag one of {@link Symbol#CONSTANT_CLASS_TAG}, {@link Symbol#CONSTANT_STRING_TAG}, {@link + * Symbol#CONSTANT_METHOD_TYPE_TAG}, {@link Symbol#CONSTANT_MODULE_TAG} or {@link + * Symbol#CONSTANT_PACKAGE_TAG}. + * @param value an internal class name, an arbitrary string, a method descriptor, a module or a + * package name, depending on tag. + * @return a new or already existing Symbol with the given value. + */ + private Symbol addConstantUtf8Reference(final int tag, final String value) { + int hashCode = hash(tag, value); + Entry entry = get(hashCode); + while (entry != null) { + if (entry.tag == tag && entry.hashCode == hashCode && entry.value.equals(value)) { + return entry; + } + entry = entry.next; + } + constantPool.put12(tag, addConstantUtf8(value)); + return put(new Entry(constantPoolCount++, tag, value, hashCode)); + } + + /** + * Adds a new CONSTANT_Class_info, CONSTANT_String_info, CONSTANT_MethodType_info, + * CONSTANT_Module_info or CONSTANT_Package_info to the constant pool of this symbol table. + * + * @param index the constant pool index of the new Symbol. + * @param tag one of {@link Symbol#CONSTANT_CLASS_TAG}, {@link Symbol#CONSTANT_STRING_TAG}, {@link + * Symbol#CONSTANT_METHOD_TYPE_TAG}, {@link Symbol#CONSTANT_MODULE_TAG} or {@link + * Symbol#CONSTANT_PACKAGE_TAG}. + * @param value an internal class name, an arbitrary string, a method descriptor, a module or a + * package name, depending on tag. + */ + private void addConstantUtf8Reference(final int index, final int tag, final String value) { + add(new Entry(index, tag, value, hash(tag, value))); + } + + // ----------------------------------------------------------------------------------------------- + // Bootstrap method entries management. + // ----------------------------------------------------------------------------------------------- + + /** + * Adds a bootstrap method to the BootstrapMethods attribute of this symbol table. Does nothing if + * the BootstrapMethods already contains a similar bootstrap method. + * + * @param bootstrapMethodHandle a bootstrap method handle. + * @param bootstrapMethodArguments the bootstrap method arguments. + * @return a new or already existing Symbol with the given value. + */ + Symbol addBootstrapMethod( + final Handle bootstrapMethodHandle, final Object... bootstrapMethodArguments) { + ByteVector bootstrapMethodsAttribute = bootstrapMethods; + if (bootstrapMethodsAttribute == null) { + bootstrapMethodsAttribute = bootstrapMethods = new ByteVector(); + } + + // The bootstrap method arguments can be Constant_Dynamic values, which reference other + // bootstrap methods. We must therefore add the bootstrap method arguments to the constant pool + // and BootstrapMethods attribute first, so that the BootstrapMethods attribute is not modified + // while adding the given bootstrap method to it, in the rest of this method. + int numBootstrapArguments = bootstrapMethodArguments.length; + int[] bootstrapMethodArgumentIndexes = new int[numBootstrapArguments]; + for (int i = 0; i < numBootstrapArguments; i++) { + bootstrapMethodArgumentIndexes[i] = addConstant(bootstrapMethodArguments[i]).index; + } + + // Write the bootstrap method in the BootstrapMethods table. This is necessary to be able to + // compare it with existing ones, and will be reverted below if there is already a similar + // bootstrap method. + int bootstrapMethodOffset = bootstrapMethodsAttribute.length; + bootstrapMethodsAttribute.putShort( + addConstantMethodHandle( + bootstrapMethodHandle.getTag(), + bootstrapMethodHandle.getOwner(), + bootstrapMethodHandle.getName(), + bootstrapMethodHandle.getDesc(), + bootstrapMethodHandle.isInterface()) + .index); + + bootstrapMethodsAttribute.putShort(numBootstrapArguments); + for (int i = 0; i < numBootstrapArguments; i++) { + bootstrapMethodsAttribute.putShort(bootstrapMethodArgumentIndexes[i]); + } + + // Compute the length and the hash code of the bootstrap method. + int bootstrapMethodlength = bootstrapMethodsAttribute.length - bootstrapMethodOffset; + int hashCode = bootstrapMethodHandle.hashCode(); + for (Object bootstrapMethodArgument : bootstrapMethodArguments) { + hashCode ^= bootstrapMethodArgument.hashCode(); + } + hashCode &= 0x7FFFFFFF; + + // Add the bootstrap method to the symbol table or revert the above changes. + return addBootstrapMethod(bootstrapMethodOffset, bootstrapMethodlength, hashCode); + } + + /** + * Adds a bootstrap method to the BootstrapMethods attribute of this symbol table. Does nothing if + * the BootstrapMethods already contains a similar bootstrap method (more precisely, reverts the + * content of {@link #bootstrapMethods} to remove the last, duplicate bootstrap method). + * + * @param offset the offset of the last bootstrap method in {@link #bootstrapMethods}, in bytes. + * @param length the length of this bootstrap method in {@link #bootstrapMethods}, in bytes. + * @param hashCode the hash code of this bootstrap method. + * @return a new or already existing Symbol with the given value. + */ + private Symbol addBootstrapMethod(final int offset, final int length, final int hashCode) { + final byte[] bootstrapMethodsData = bootstrapMethods.data; + Entry entry = get(hashCode); + while (entry != null) { + if (entry.tag == Symbol.BOOTSTRAP_METHOD_TAG && entry.hashCode == hashCode) { + int otherOffset = (int) entry.data; + boolean isSameBootstrapMethod = true; + for (int i = 0; i < length; ++i) { + if (bootstrapMethodsData[offset + i] != bootstrapMethodsData[otherOffset + i]) { + isSameBootstrapMethod = false; + break; + } + } + if (isSameBootstrapMethod) { + bootstrapMethods.length = offset; // Revert to old position. + return entry; + } + } + entry = entry.next; + } + return put(new Entry(bootstrapMethodCount++, Symbol.BOOTSTRAP_METHOD_TAG, offset, hashCode)); + } + + // ----------------------------------------------------------------------------------------------- + // Type table entries management. + // ----------------------------------------------------------------------------------------------- + + /** + * Returns the type table element whose index is given. + * + * @param typeIndex a type table index. + * @return the type table element whose index is given. + */ + Symbol getType(final int typeIndex) { + return typeTable[typeIndex]; + } + + /** + * Adds a type in the type table of this symbol table. Does nothing if the type table already + * contains a similar type. + * + * @param value an internal class name. + * @return the index of a new or already existing type Symbol with the given value. + */ + int addType(final String value) { + int hashCode = hash(Symbol.TYPE_TAG, value); + Entry entry = get(hashCode); + while (entry != null) { + if (entry.tag == Symbol.TYPE_TAG && entry.hashCode == hashCode && entry.value.equals(value)) { + return entry.index; + } + entry = entry.next; + } + return addTypeInternal(new Entry(typeCount, Symbol.TYPE_TAG, value, hashCode)); + } + + /** + * Adds an {@link Frame#ITEM_UNINITIALIZED} type in the type table of this symbol table. Does + * nothing if the type table already contains a similar type. + * + * @param value an internal class name. + * @param bytecodeOffset the bytecode offset of the NEW instruction that created this {@link + * Frame#ITEM_UNINITIALIZED} type value. + * @return the index of a new or already existing type Symbol with the given value. + */ + int addUninitializedType(final String value, final int bytecodeOffset) { + int hashCode = hash(Symbol.UNINITIALIZED_TYPE_TAG, value, bytecodeOffset); + Entry entry = get(hashCode); + while (entry != null) { + if (entry.tag == Symbol.UNINITIALIZED_TYPE_TAG + && entry.hashCode == hashCode + && entry.data == bytecodeOffset + && entry.value.equals(value)) { + return entry.index; + } + entry = entry.next; + } + return addTypeInternal( + new Entry(typeCount, Symbol.UNINITIALIZED_TYPE_TAG, value, bytecodeOffset, hashCode)); + } + + /** + * Adds a merged type in the type table of this symbol table. Does nothing if the type table + * already contains a similar type. + * + * @param typeTableIndex1 a {@link Symbol#TYPE_TAG} type, specified by its index in the type + * table. + * @param typeTableIndex2 another {@link Symbol#TYPE_TAG} type, specified by its index in the type + * table. + * @return the index of a new or already existing {@link Symbol#TYPE_TAG} type Symbol, + * corresponding to the common super class of the given types. + */ + int addMergedType(final int typeTableIndex1, final int typeTableIndex2) { + long data = + typeTableIndex1 < typeTableIndex2 + ? typeTableIndex1 | (((long) typeTableIndex2) << 32) + : typeTableIndex2 | (((long) typeTableIndex1) << 32); + int hashCode = hash(Symbol.MERGED_TYPE_TAG, typeTableIndex1 + typeTableIndex2); + Entry entry = get(hashCode); + while (entry != null) { + if (entry.tag == Symbol.MERGED_TYPE_TAG && entry.hashCode == hashCode && entry.data == data) { + return entry.info; + } + entry = entry.next; + } + String type1 = typeTable[typeTableIndex1].value; + String type2 = typeTable[typeTableIndex2].value; + int commonSuperTypeIndex = addType(classWriter.getCommonSuperClass(type1, type2)); + put(new Entry(typeCount, Symbol.MERGED_TYPE_TAG, data, hashCode)).info = commonSuperTypeIndex; + return commonSuperTypeIndex; + } + + /** + * Adds the given type Symbol to {@link #typeTable}. + * + * @param entry a {@link Symbol#TYPE_TAG} or {@link Symbol#UNINITIALIZED_TYPE_TAG} type symbol. + * The index of this Symbol must be equal to the current value of {@link #typeCount}. + * @return the index in {@link #typeTable} where the given type was added, which is also equal to + * entry's index by hypothesis. + */ + private int addTypeInternal(final Entry entry) { + if (typeTable == null) { + typeTable = new Entry[16]; + } + if (typeCount == typeTable.length) { + Entry[] newTypeTable = new Entry[2 * typeTable.length]; + System.arraycopy(typeTable, 0, newTypeTable, 0, typeTable.length); + typeTable = newTypeTable; + } + typeTable[typeCount++] = entry; + return put(entry).index; + } + + // ----------------------------------------------------------------------------------------------- + // Static helper methods to compute hash codes. + // ----------------------------------------------------------------------------------------------- + + private static int hash(final int tag, final int value) { + return 0x7FFFFFFF & (tag + value); + } + + private static int hash(final int tag, final long value) { + return 0x7FFFFFFF & (tag + (int) value + (int) (value >>> 32)); + } + + private static int hash(final int tag, final String value) { + return 0x7FFFFFFF & (tag + value.hashCode()); + } + + private static int hash(final int tag, final String value1, final int value2) { + return 0x7FFFFFFF & (tag + value1.hashCode() + value2); + } + + private static int hash(final int tag, final String value1, final String value2) { + return 0x7FFFFFFF & (tag + value1.hashCode() * value2.hashCode()); + } + + private static int hash( + final int tag, final String value1, final String value2, final int value3) { + return 0x7FFFFFFF & (tag + value1.hashCode() * value2.hashCode() * (value3 + 1)); + } + + private static int hash( + final int tag, final String value1, final String value2, final String value3) { + return 0x7FFFFFFF & (tag + value1.hashCode() * value2.hashCode() * value3.hashCode()); + } + + private static int hash( + final int tag, + final String value1, + final String value2, + final String value3, + final int value4) { + return 0x7FFFFFFF & (tag + value1.hashCode() * value2.hashCode() * value3.hashCode() * value4); + } + + /** + * An entry of a SymbolTable. This concrete and private subclass of {@link Symbol} adds two fields + * which are only used inside SymbolTable, to implement hash sets of symbols (in order to avoid + * duplicate symbols). See {@link #entries}. + * + * @author Eric Bruneton + */ + private static class Entry extends Symbol { + + /** The hash code of this entry. */ + final int hashCode; + + /** + * Another entry (and so on recursively) having the same hash code (modulo the size of {@link + * #entries}) as this one. + */ + Entry next; + + Entry( + final int index, + final int tag, + final String owner, + final String name, + final String value, + final long data, + final int hashCode) { + super(index, tag, owner, name, value, data); + this.hashCode = hashCode; + } + + Entry(final int index, final int tag, final String value, final int hashCode) { + super(index, tag, /* owner = */ null, /* name = */ null, value, /* data = */ 0); + this.hashCode = hashCode; + } + + Entry(final int index, final int tag, final String value, final long data, final int hashCode) { + super(index, tag, /* owner = */ null, /* name = */ null, value, data); + this.hashCode = hashCode; + } + + Entry( + final int index, final int tag, final String name, final String value, final int hashCode) { + super(index, tag, /* owner = */ null, name, value, /* data = */ 0); + this.hashCode = hashCode; + } + + Entry(final int index, final int tag, final long data, final int hashCode) { + super(index, tag, /* owner = */ null, /* name = */ null, /* value = */ null, data); + this.hashCode = hashCode; + } + } +} diff --git a/spring-core/src/main/java/org/springframework/asm/Type.java b/spring-core/src/main/java/org/springframework/asm/Type.java new file mode 100644 index 0000000..5850ffd --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/Type.java @@ -0,0 +1,895 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +package org.springframework.asm; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +/** + * A Java field or method type. This class can be used to make it easier to manipulate type and + * method descriptors. + * + * @author Eric Bruneton + * @author Chris Nokleberg + */ +public final class Type { + + /** The sort of the {@code void} type. See {@link #getSort}. */ + public static final int VOID = 0; + + /** The sort of the {@code boolean} type. See {@link #getSort}. */ + public static final int BOOLEAN = 1; + + /** The sort of the {@code char} type. See {@link #getSort}. */ + public static final int CHAR = 2; + + /** The sort of the {@code byte} type. See {@link #getSort}. */ + public static final int BYTE = 3; + + /** The sort of the {@code short} type. See {@link #getSort}. */ + public static final int SHORT = 4; + + /** The sort of the {@code int} type. See {@link #getSort}. */ + public static final int INT = 5; + + /** The sort of the {@code float} type. See {@link #getSort}. */ + public static final int FLOAT = 6; + + /** The sort of the {@code long} type. See {@link #getSort}. */ + public static final int LONG = 7; + + /** The sort of the {@code double} type. See {@link #getSort}. */ + public static final int DOUBLE = 8; + + /** The sort of array reference types. See {@link #getSort}. */ + public static final int ARRAY = 9; + + /** The sort of object reference types. See {@link #getSort}. */ + public static final int OBJECT = 10; + + /** The sort of method types. See {@link #getSort}. */ + public static final int METHOD = 11; + + /** The (private) sort of object reference types represented with an internal name. */ + private static final int INTERNAL = 12; + + /** The descriptors of the primitive types. */ + private static final String PRIMITIVE_DESCRIPTORS = "VZCBSIFJD"; + + /** The {@code void} type. */ + public static final Type VOID_TYPE = new Type(VOID, PRIMITIVE_DESCRIPTORS, VOID, VOID + 1); + + /** The {@code boolean} type. */ + public static final Type BOOLEAN_TYPE = + new Type(BOOLEAN, PRIMITIVE_DESCRIPTORS, BOOLEAN, BOOLEAN + 1); + + /** The {@code char} type. */ + public static final Type CHAR_TYPE = new Type(CHAR, PRIMITIVE_DESCRIPTORS, CHAR, CHAR + 1); + + /** The {@code byte} type. */ + public static final Type BYTE_TYPE = new Type(BYTE, PRIMITIVE_DESCRIPTORS, BYTE, BYTE + 1); + + /** The {@code short} type. */ + public static final Type SHORT_TYPE = new Type(SHORT, PRIMITIVE_DESCRIPTORS, SHORT, SHORT + 1); + + /** The {@code int} type. */ + public static final Type INT_TYPE = new Type(INT, PRIMITIVE_DESCRIPTORS, INT, INT + 1); + + /** The {@code float} type. */ + public static final Type FLOAT_TYPE = new Type(FLOAT, PRIMITIVE_DESCRIPTORS, FLOAT, FLOAT + 1); + + /** The {@code long} type. */ + public static final Type LONG_TYPE = new Type(LONG, PRIMITIVE_DESCRIPTORS, LONG, LONG + 1); + + /** The {@code double} type. */ + public static final Type DOUBLE_TYPE = + new Type(DOUBLE, PRIMITIVE_DESCRIPTORS, DOUBLE, DOUBLE + 1); + + // ----------------------------------------------------------------------------------------------- + // Fields + // ----------------------------------------------------------------------------------------------- + + /** + * The sort of this type. Either {@link #VOID}, {@link #BOOLEAN}, {@link #CHAR}, {@link #BYTE}, + * {@link #SHORT}, {@link #INT}, {@link #FLOAT}, {@link #LONG}, {@link #DOUBLE}, {@link #ARRAY}, + * {@link #OBJECT}, {@link #METHOD} or {@link #INTERNAL}. + */ + private final int sort; + + /** + * A buffer containing the value of this field or method type. This value is an internal name for + * {@link #OBJECT} and {@link #INTERNAL} types, and a field or method descriptor in the other + * cases. + * + *

    For {@link #OBJECT} types, this field also contains the descriptor: the characters in + * [{@link #valueBegin},{@link #valueEnd}) contain the internal name, and those in [{@link + * #valueBegin} - 1, {@link #valueEnd} + 1) contain the descriptor. + */ + private final String valueBuffer; + + /** + * The beginning index, inclusive, of the value of this Java field or method type in {@link + * #valueBuffer}. This value is an internal name for {@link #OBJECT} and {@link #INTERNAL} types, + * and a field or method descriptor in the other cases. + */ + private final int valueBegin; + + /** + * The end index, exclusive, of the value of this Java field or method type in {@link + * #valueBuffer}. This value is an internal name for {@link #OBJECT} and {@link #INTERNAL} types, + * and a field or method descriptor in the other cases. + */ + private final int valueEnd; + + /** + * Constructs a reference type. + * + * @param sort the sort of this type, see {@link #sort}. + * @param valueBuffer a buffer containing the value of this field or method type. + * @param valueBegin the beginning index, inclusive, of the value of this field or method type in + * valueBuffer. + * @param valueEnd the end index, exclusive, of the value of this field or method type in + * valueBuffer. + */ + private Type(final int sort, final String valueBuffer, final int valueBegin, final int valueEnd) { + this.sort = sort; + this.valueBuffer = valueBuffer; + this.valueBegin = valueBegin; + this.valueEnd = valueEnd; + } + + // ----------------------------------------------------------------------------------------------- + // Methods to get Type(s) from a descriptor, a reflected Method or Constructor, other types, etc. + // ----------------------------------------------------------------------------------------------- + + /** + * Returns the {@link Type} corresponding to the given type descriptor. + * + * @param typeDescriptor a field or method type descriptor. + * @return the {@link Type} corresponding to the given type descriptor. + */ + public static Type getType(final String typeDescriptor) { + return getTypeInternal(typeDescriptor, 0, typeDescriptor.length()); + } + + /** + * Returns the {@link Type} corresponding to the given class. + * + * @param clazz a class. + * @return the {@link Type} corresponding to the given class. + */ + public static Type getType(final Class clazz) { + if (clazz.isPrimitive()) { + if (clazz == Integer.TYPE) { + return INT_TYPE; + } else if (clazz == Void.TYPE) { + return VOID_TYPE; + } else if (clazz == Boolean.TYPE) { + return BOOLEAN_TYPE; + } else if (clazz == Byte.TYPE) { + return BYTE_TYPE; + } else if (clazz == Character.TYPE) { + return CHAR_TYPE; + } else if (clazz == Short.TYPE) { + return SHORT_TYPE; + } else if (clazz == Double.TYPE) { + return DOUBLE_TYPE; + } else if (clazz == Float.TYPE) { + return FLOAT_TYPE; + } else if (clazz == Long.TYPE) { + return LONG_TYPE; + } else { + throw new AssertionError(); + } + } else { + return getType(getDescriptor(clazz)); + } + } + + /** + * Returns the method {@link Type} corresponding to the given constructor. + * + * @param constructor a {@link Constructor} object. + * @return the method {@link Type} corresponding to the given constructor. + */ + public static Type getType(final Constructor constructor) { + return getType(getConstructorDescriptor(constructor)); + } + + /** + * Returns the method {@link Type} corresponding to the given method. + * + * @param method a {@link Method} object. + * @return the method {@link Type} corresponding to the given method. + */ + public static Type getType(final Method method) { + return getType(getMethodDescriptor(method)); + } + + /** + * Returns the type of the elements of this array type. This method should only be used for an + * array type. + * + * @return Returns the type of the elements of this array type. + */ + public Type getElementType() { + final int numDimensions = getDimensions(); + return getTypeInternal(valueBuffer, valueBegin + numDimensions, valueEnd); + } + + /** + * Returns the {@link Type} corresponding to the given internal name. + * + * @param internalName an internal name. + * @return the {@link Type} corresponding to the given internal name. + */ + public static Type getObjectType(final String internalName) { + return new Type( + internalName.charAt(0) == '[' ? ARRAY : INTERNAL, internalName, 0, internalName.length()); + } + + /** + * Returns the {@link Type} corresponding to the given method descriptor. Equivalent to + * Type.getType(methodDescriptor). + * + * @param methodDescriptor a method descriptor. + * @return the {@link Type} corresponding to the given method descriptor. + */ + public static Type getMethodType(final String methodDescriptor) { + return new Type(METHOD, methodDescriptor, 0, methodDescriptor.length()); + } + + /** + * Returns the method {@link Type} corresponding to the given argument and return types. + * + * @param returnType the return type of the method. + * @param argumentTypes the argument types of the method. + * @return the method {@link Type} corresponding to the given argument and return types. + */ + public static Type getMethodType(final Type returnType, final Type... argumentTypes) { + return getType(getMethodDescriptor(returnType, argumentTypes)); + } + + /** + * Returns the argument types of methods of this type. This method should only be used for method + * types. + * + * @return the argument types of methods of this type. + */ + public Type[] getArgumentTypes() { + return getArgumentTypes(getDescriptor()); + } + + /** + * Returns the {@link Type} values corresponding to the argument types of the given method + * descriptor. + * + * @param methodDescriptor a method descriptor. + * @return the {@link Type} values corresponding to the argument types of the given method + * descriptor. + */ + public static Type[] getArgumentTypes(final String methodDescriptor) { + // First step: compute the number of argument types in methodDescriptor. + int numArgumentTypes = 0; + // Skip the first character, which is always a '('. + int currentOffset = 1; + // Parse the argument types, one at a each loop iteration. + while (methodDescriptor.charAt(currentOffset) != ')') { + while (methodDescriptor.charAt(currentOffset) == '[') { + currentOffset++; + } + if (methodDescriptor.charAt(currentOffset++) == 'L') { + // Skip the argument descriptor content. + int semiColumnOffset = methodDescriptor.indexOf(';', currentOffset); + currentOffset = Math.max(currentOffset, semiColumnOffset + 1); + } + ++numArgumentTypes; + } + + // Second step: create a Type instance for each argument type. + Type[] argumentTypes = new Type[numArgumentTypes]; + // Skip the first character, which is always a '('. + currentOffset = 1; + // Parse and create the argument types, one at each loop iteration. + int currentArgumentTypeIndex = 0; + while (methodDescriptor.charAt(currentOffset) != ')') { + final int currentArgumentTypeOffset = currentOffset; + while (methodDescriptor.charAt(currentOffset) == '[') { + currentOffset++; + } + if (methodDescriptor.charAt(currentOffset++) == 'L') { + // Skip the argument descriptor content. + int semiColumnOffset = methodDescriptor.indexOf(';', currentOffset); + currentOffset = Math.max(currentOffset, semiColumnOffset + 1); + } + argumentTypes[currentArgumentTypeIndex++] = + getTypeInternal(methodDescriptor, currentArgumentTypeOffset, currentOffset); + } + return argumentTypes; + } + + /** + * Returns the {@link Type} values corresponding to the argument types of the given method. + * + * @param method a method. + * @return the {@link Type} values corresponding to the argument types of the given method. + */ + public static Type[] getArgumentTypes(final Method method) { + Class[] classes = method.getParameterTypes(); + Type[] types = new Type[classes.length]; + for (int i = classes.length - 1; i >= 0; --i) { + types[i] = getType(classes[i]); + } + return types; + } + + /** + * Returns the return type of methods of this type. This method should only be used for method + * types. + * + * @return the return type of methods of this type. + */ + public Type getReturnType() { + return getReturnType(getDescriptor()); + } + + /** + * Returns the {@link Type} corresponding to the return type of the given method descriptor. + * + * @param methodDescriptor a method descriptor. + * @return the {@link Type} corresponding to the return type of the given method descriptor. + */ + public static Type getReturnType(final String methodDescriptor) { + return getTypeInternal( + methodDescriptor, getReturnTypeOffset(methodDescriptor), methodDescriptor.length()); + } + + /** + * Returns the {@link Type} corresponding to the return type of the given method. + * + * @param method a method. + * @return the {@link Type} corresponding to the return type of the given method. + */ + public static Type getReturnType(final Method method) { + return getType(method.getReturnType()); + } + + /** + * Returns the start index of the return type of the given method descriptor. + * + * @param methodDescriptor a method descriptor. + * @return the start index of the return type of the given method descriptor. + */ + static int getReturnTypeOffset(final String methodDescriptor) { + // Skip the first character, which is always a '('. + int currentOffset = 1; + // Skip the argument types, one at a each loop iteration. + while (methodDescriptor.charAt(currentOffset) != ')') { + while (methodDescriptor.charAt(currentOffset) == '[') { + currentOffset++; + } + if (methodDescriptor.charAt(currentOffset++) == 'L') { + // Skip the argument descriptor content. + int semiColumnOffset = methodDescriptor.indexOf(';', currentOffset); + currentOffset = Math.max(currentOffset, semiColumnOffset + 1); + } + } + return currentOffset + 1; + } + + /** + * Returns the {@link Type} corresponding to the given field or method descriptor. + * + * @param descriptorBuffer a buffer containing the field or method descriptor. + * @param descriptorBegin the beginning index, inclusive, of the field or method descriptor in + * descriptorBuffer. + * @param descriptorEnd the end index, exclusive, of the field or method descriptor in + * descriptorBuffer. + * @return the {@link Type} corresponding to the given type descriptor. + */ + private static Type getTypeInternal( + final String descriptorBuffer, final int descriptorBegin, final int descriptorEnd) { + switch (descriptorBuffer.charAt(descriptorBegin)) { + case 'V': + return VOID_TYPE; + case 'Z': + return BOOLEAN_TYPE; + case 'C': + return CHAR_TYPE; + case 'B': + return BYTE_TYPE; + case 'S': + return SHORT_TYPE; + case 'I': + return INT_TYPE; + case 'F': + return FLOAT_TYPE; + case 'J': + return LONG_TYPE; + case 'D': + return DOUBLE_TYPE; + case '[': + return new Type(ARRAY, descriptorBuffer, descriptorBegin, descriptorEnd); + case 'L': + return new Type(OBJECT, descriptorBuffer, descriptorBegin + 1, descriptorEnd - 1); + case '(': + return new Type(METHOD, descriptorBuffer, descriptorBegin, descriptorEnd); + default: + throw new IllegalArgumentException(); + } + } + + // ----------------------------------------------------------------------------------------------- + // Methods to get class names, internal names or descriptors. + // ----------------------------------------------------------------------------------------------- + + /** + * Returns the binary name of the class corresponding to this type. This method must not be used + * on method types. + * + * @return the binary name of the class corresponding to this type. + */ + public String getClassName() { + switch (sort) { + case VOID: + return "void"; + case BOOLEAN: + return "boolean"; + case CHAR: + return "char"; + case BYTE: + return "byte"; + case SHORT: + return "short"; + case INT: + return "int"; + case FLOAT: + return "float"; + case LONG: + return "long"; + case DOUBLE: + return "double"; + case ARRAY: + StringBuilder stringBuilder = new StringBuilder(getElementType().getClassName()); + for (int i = getDimensions(); i > 0; --i) { + stringBuilder.append("[]"); + } + return stringBuilder.toString(); + case OBJECT: + case INTERNAL: + return valueBuffer.substring(valueBegin, valueEnd).replace('/', '.'); + default: + throw new AssertionError(); + } + } + + /** + * Returns the internal name of the class corresponding to this object or array type. The internal + * name of a class is its fully qualified name (as returned by Class.getName(), where '.' are + * replaced by '/'). This method should only be used for an object or array type. + * + * @return the internal name of the class corresponding to this object type. + */ + public String getInternalName() { + return valueBuffer.substring(valueBegin, valueEnd); + } + + /** + * Returns the internal name of the given class. The internal name of a class is its fully + * qualified name, as returned by Class.getName(), where '.' are replaced by '/'. + * + * @param clazz an object or array class. + * @return the internal name of the given class. + */ + public static String getInternalName(final Class clazz) { + return clazz.getName().replace('.', '/'); + } + + /** + * Returns the descriptor corresponding to this type. + * + * @return the descriptor corresponding to this type. + */ + public String getDescriptor() { + if (sort == OBJECT) { + return valueBuffer.substring(valueBegin - 1, valueEnd + 1); + } else if (sort == INTERNAL) { + return 'L' + valueBuffer.substring(valueBegin, valueEnd) + ';'; + } else { + return valueBuffer.substring(valueBegin, valueEnd); + } + } + + /** + * Returns the descriptor corresponding to the given class. + * + * @param clazz an object class, a primitive class or an array class. + * @return the descriptor corresponding to the given class. + */ + public static String getDescriptor(final Class clazz) { + StringBuilder stringBuilder = new StringBuilder(); + appendDescriptor(clazz, stringBuilder); + return stringBuilder.toString(); + } + + /** + * Returns the descriptor corresponding to the given constructor. + * + * @param constructor a {@link Constructor} object. + * @return the descriptor of the given constructor. + */ + public static String getConstructorDescriptor(final Constructor constructor) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append('('); + Class[] parameters = constructor.getParameterTypes(); + for (Class parameter : parameters) { + appendDescriptor(parameter, stringBuilder); + } + return stringBuilder.append(")V").toString(); + } + + /** + * Returns the descriptor corresponding to the given argument and return types. + * + * @param returnType the return type of the method. + * @param argumentTypes the argument types of the method. + * @return the descriptor corresponding to the given argument and return types. + */ + public static String getMethodDescriptor(final Type returnType, final Type... argumentTypes) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append('('); + for (Type argumentType : argumentTypes) { + argumentType.appendDescriptor(stringBuilder); + } + stringBuilder.append(')'); + returnType.appendDescriptor(stringBuilder); + return stringBuilder.toString(); + } + + /** + * Returns the descriptor corresponding to the given method. + * + * @param method a {@link Method} object. + * @return the descriptor of the given method. + */ + public static String getMethodDescriptor(final Method method) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append('('); + Class[] parameters = method.getParameterTypes(); + for (Class parameter : parameters) { + appendDescriptor(parameter, stringBuilder); + } + stringBuilder.append(')'); + appendDescriptor(method.getReturnType(), stringBuilder); + return stringBuilder.toString(); + } + + /** + * Appends the descriptor corresponding to this type to the given string buffer. + * + * @param stringBuilder the string builder to which the descriptor must be appended. + */ + private void appendDescriptor(final StringBuilder stringBuilder) { + if (sort == OBJECT) { + stringBuilder.append(valueBuffer, valueBegin - 1, valueEnd + 1); + } else if (sort == INTERNAL) { + stringBuilder.append('L').append(valueBuffer, valueBegin, valueEnd).append(';'); + } else { + stringBuilder.append(valueBuffer, valueBegin, valueEnd); + } + } + + /** + * Appends the descriptor of the given class to the given string builder. + * + * @param clazz the class whose descriptor must be computed. + * @param stringBuilder the string builder to which the descriptor must be appended. + */ + private static void appendDescriptor(final Class clazz, final StringBuilder stringBuilder) { + Class currentClass = clazz; + while (currentClass.isArray()) { + stringBuilder.append('['); + currentClass = currentClass.getComponentType(); + } + if (currentClass.isPrimitive()) { + char descriptor; + if (currentClass == Integer.TYPE) { + descriptor = 'I'; + } else if (currentClass == Void.TYPE) { + descriptor = 'V'; + } else if (currentClass == Boolean.TYPE) { + descriptor = 'Z'; + } else if (currentClass == Byte.TYPE) { + descriptor = 'B'; + } else if (currentClass == Character.TYPE) { + descriptor = 'C'; + } else if (currentClass == Short.TYPE) { + descriptor = 'S'; + } else if (currentClass == Double.TYPE) { + descriptor = 'D'; + } else if (currentClass == Float.TYPE) { + descriptor = 'F'; + } else if (currentClass == Long.TYPE) { + descriptor = 'J'; + } else { + throw new AssertionError(); + } + stringBuilder.append(descriptor); + } else { + stringBuilder.append('L').append(getInternalName(currentClass)).append(';'); + } + } + + // ----------------------------------------------------------------------------------------------- + // Methods to get the sort, dimension, size, and opcodes corresponding to a Type or descriptor. + // ----------------------------------------------------------------------------------------------- + + /** + * Returns the sort of this type. + * + * @return {@link #VOID}, {@link #BOOLEAN}, {@link #CHAR}, {@link #BYTE}, {@link #SHORT}, {@link + * #INT}, {@link #FLOAT}, {@link #LONG}, {@link #DOUBLE}, {@link #ARRAY}, {@link #OBJECT} or + * {@link #METHOD}. + */ + public int getSort() { + return sort == INTERNAL ? OBJECT : sort; + } + + /** + * Returns the number of dimensions of this array type. This method should only be used for an + * array type. + * + * @return the number of dimensions of this array type. + */ + public int getDimensions() { + int numDimensions = 1; + while (valueBuffer.charAt(valueBegin + numDimensions) == '[') { + numDimensions++; + } + return numDimensions; + } + + /** + * Returns the size of values of this type. This method must not be used for method types. + * + * @return the size of values of this type, i.e., 2 for {@code long} and {@code double}, 0 for + * {@code void} and 1 otherwise. + */ + public int getSize() { + switch (sort) { + case VOID: + return 0; + case BOOLEAN: + case CHAR: + case BYTE: + case SHORT: + case INT: + case FLOAT: + case ARRAY: + case OBJECT: + case INTERNAL: + return 1; + case LONG: + case DOUBLE: + return 2; + default: + throw new AssertionError(); + } + } + + /** + * Returns the size of the arguments and of the return value of methods of this type. This method + * should only be used for method types. + * + * @return the size of the arguments of the method (plus one for the implicit this argument), + * argumentsSize, and the size of its return value, returnSize, packed into a single int i = + * {@code (argumentsSize << 2) | returnSize} (argumentsSize is therefore equal to {@code + * i >> 2}, and returnSize to {@code i & 0x03}). + */ + public int getArgumentsAndReturnSizes() { + return getArgumentsAndReturnSizes(getDescriptor()); + } + + /** + * Computes the size of the arguments and of the return value of a method. + * + * @param methodDescriptor a method descriptor. + * @return the size of the arguments of the method (plus one for the implicit this argument), + * argumentsSize, and the size of its return value, returnSize, packed into a single int i = + * {@code (argumentsSize << 2) | returnSize} (argumentsSize is therefore equal to {@code + * i >> 2}, and returnSize to {@code i & 0x03}). + */ + public static int getArgumentsAndReturnSizes(final String methodDescriptor) { + int argumentsSize = 1; + // Skip the first character, which is always a '('. + int currentOffset = 1; + int currentChar = methodDescriptor.charAt(currentOffset); + // Parse the argument types and compute their size, one at a each loop iteration. + while (currentChar != ')') { + if (currentChar == 'J' || currentChar == 'D') { + currentOffset++; + argumentsSize += 2; + } else { + while (methodDescriptor.charAt(currentOffset) == '[') { + currentOffset++; + } + if (methodDescriptor.charAt(currentOffset++) == 'L') { + // Skip the argument descriptor content. + int semiColumnOffset = methodDescriptor.indexOf(';', currentOffset); + currentOffset = Math.max(currentOffset, semiColumnOffset + 1); + } + argumentsSize += 1; + } + currentChar = methodDescriptor.charAt(currentOffset); + } + currentChar = methodDescriptor.charAt(currentOffset + 1); + if (currentChar == 'V') { + return argumentsSize << 2; + } else { + int returnSize = (currentChar == 'J' || currentChar == 'D') ? 2 : 1; + return argumentsSize << 2 | returnSize; + } + } + + /** + * Returns a JVM instruction opcode adapted to this {@link Type}. This method must not be used for + * method types. + * + * @param opcode a JVM instruction opcode. This opcode must be one of ILOAD, ISTORE, IALOAD, + * IASTORE, IADD, ISUB, IMUL, IDIV, IREM, INEG, ISHL, ISHR, IUSHR, IAND, IOR, IXOR and + * IRETURN. + * @return an opcode that is similar to the given opcode, but adapted to this {@link Type}. For + * example, if this type is {@code float} and {@code opcode} is IRETURN, this method returns + * FRETURN. + */ + public int getOpcode(final int opcode) { + if (opcode == Opcodes.IALOAD || opcode == Opcodes.IASTORE) { + switch (sort) { + case BOOLEAN: + case BYTE: + return opcode + (Opcodes.BALOAD - Opcodes.IALOAD); + case CHAR: + return opcode + (Opcodes.CALOAD - Opcodes.IALOAD); + case SHORT: + return opcode + (Opcodes.SALOAD - Opcodes.IALOAD); + case INT: + return opcode; + case FLOAT: + return opcode + (Opcodes.FALOAD - Opcodes.IALOAD); + case LONG: + return opcode + (Opcodes.LALOAD - Opcodes.IALOAD); + case DOUBLE: + return opcode + (Opcodes.DALOAD - Opcodes.IALOAD); + case ARRAY: + case OBJECT: + case INTERNAL: + return opcode + (Opcodes.AALOAD - Opcodes.IALOAD); + case METHOD: + case VOID: + throw new UnsupportedOperationException(); + default: + throw new AssertionError(); + } + } else { + switch (sort) { + case VOID: + if (opcode != Opcodes.IRETURN) { + throw new UnsupportedOperationException(); + } + return Opcodes.RETURN; + case BOOLEAN: + case BYTE: + case CHAR: + case SHORT: + case INT: + return opcode; + case FLOAT: + return opcode + (Opcodes.FRETURN - Opcodes.IRETURN); + case LONG: + return opcode + (Opcodes.LRETURN - Opcodes.IRETURN); + case DOUBLE: + return opcode + (Opcodes.DRETURN - Opcodes.IRETURN); + case ARRAY: + case OBJECT: + case INTERNAL: + if (opcode != Opcodes.ILOAD && opcode != Opcodes.ISTORE && opcode != Opcodes.IRETURN) { + throw new UnsupportedOperationException(); + } + return opcode + (Opcodes.ARETURN - Opcodes.IRETURN); + case METHOD: + throw new UnsupportedOperationException(); + default: + throw new AssertionError(); + } + } + } + + // ----------------------------------------------------------------------------------------------- + // Equals, hashCode and toString. + // ----------------------------------------------------------------------------------------------- + + /** + * Tests if the given object is equal to this type. + * + * @param object the object to be compared to this type. + * @return {@literal true} if the given object is equal to this type. + */ + @Override + public boolean equals(final Object object) { + if (this == object) { + return true; + } + if (!(object instanceof Type)) { + return false; + } + Type other = (Type) object; + if ((sort == INTERNAL ? OBJECT : sort) != (other.sort == INTERNAL ? OBJECT : other.sort)) { + return false; + } + int begin = valueBegin; + int end = valueEnd; + int otherBegin = other.valueBegin; + int otherEnd = other.valueEnd; + // Compare the values. + if (end - begin != otherEnd - otherBegin) { + return false; + } + for (int i = begin, j = otherBegin; i < end; i++, j++) { + if (valueBuffer.charAt(i) != other.valueBuffer.charAt(j)) { + return false; + } + } + return true; + } + + /** + * Returns a hash code value for this type. + * + * @return a hash code value for this type. + */ + @Override + public int hashCode() { + int hashCode = 13 * (sort == INTERNAL ? OBJECT : sort); + if (sort >= ARRAY) { + for (int i = valueBegin, end = valueEnd; i < end; i++) { + hashCode = 17 * (hashCode + valueBuffer.charAt(i)); + } + } + return hashCode; + } + + /** + * Returns a string representation of this type. + * + * @return the descriptor of this type. + */ + @Override + public String toString() { + return getDescriptor(); + } +} diff --git a/spring-core/src/main/java/org/springframework/asm/TypePath.java b/spring-core/src/main/java/org/springframework/asm/TypePath.java new file mode 100644 index 0000000..b7cf845 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/TypePath.java @@ -0,0 +1,201 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. + +package org.springframework.asm; + +/** + * The path to a type argument, wildcard bound, array element type, or static inner type within an + * enclosing type. + * + * @author Eric Bruneton + */ +public final class TypePath { + + /** A type path step that steps into the element type of an array type. See {@link #getStep}. */ + public static final int ARRAY_ELEMENT = 0; + + /** A type path step that steps into the nested type of a class type. See {@link #getStep}. */ + public static final int INNER_TYPE = 1; + + /** A type path step that steps into the bound of a wildcard type. See {@link #getStep}. */ + public static final int WILDCARD_BOUND = 2; + + /** A type path step that steps into a type argument of a generic type. See {@link #getStep}. */ + public static final int TYPE_ARGUMENT = 3; + + /** + * The byte array where the 'type_path' structure - as defined in the Java Virtual Machine + * Specification (JVMS) - corresponding to this TypePath is stored. The first byte of the + * structure in this array is given by {@link #typePathOffset}. + * + * @see JVMS + * 4.7.20.2 + */ + private final byte[] typePathContainer; + + /** The offset of the first byte of the type_path JVMS structure in {@link #typePathContainer}. */ + private final int typePathOffset; + + /** + * Constructs a new TypePath. + * + * @param typePathContainer a byte array containing a type_path JVMS structure. + * @param typePathOffset the offset of the first byte of the type_path structure in + * typePathContainer. + */ + TypePath(final byte[] typePathContainer, final int typePathOffset) { + this.typePathContainer = typePathContainer; + this.typePathOffset = typePathOffset; + } + + /** + * Returns the length of this path, i.e. its number of steps. + * + * @return the length of this path. + */ + public int getLength() { + // path_length is stored in the first byte of a type_path. + return typePathContainer[typePathOffset]; + } + + /** + * Returns the value of the given step of this path. + * + * @param index an index between 0 and {@link #getLength()}, exclusive. + * @return one of {@link #ARRAY_ELEMENT}, {@link #INNER_TYPE}, {@link #WILDCARD_BOUND}, or {@link + * #TYPE_ARGUMENT}. + */ + public int getStep(final int index) { + // Returns the type_path_kind of the path element of the given index. + return typePathContainer[typePathOffset + 2 * index + 1]; + } + + /** + * Returns the index of the type argument that the given step is stepping into. This method should + * only be used for steps whose value is {@link #TYPE_ARGUMENT}. + * + * @param index an index between 0 and {@link #getLength()}, exclusive. + * @return the index of the type argument that the given step is stepping into. + */ + public int getStepArgument(final int index) { + // Returns the type_argument_index of the path element of the given index. + return typePathContainer[typePathOffset + 2 * index + 2]; + } + + /** + * Converts a type path in string form, in the format used by {@link #toString()}, into a TypePath + * object. + * + * @param typePath a type path in string form, in the format used by {@link #toString()}. May be + * {@literal null} or empty. + * @return the corresponding TypePath object, or {@literal null} if the path is empty. + */ + public static TypePath fromString(final String typePath) { + if (typePath == null || typePath.length() == 0) { + return null; + } + int typePathLength = typePath.length(); + ByteVector output = new ByteVector(typePathLength); + output.putByte(0); + int typePathIndex = 0; + while (typePathIndex < typePathLength) { + char c = typePath.charAt(typePathIndex++); + if (c == '[') { + output.put11(ARRAY_ELEMENT, 0); + } else if (c == '.') { + output.put11(INNER_TYPE, 0); + } else if (c == '*') { + output.put11(WILDCARD_BOUND, 0); + } else if (c >= '0' && c <= '9') { + int typeArg = c - '0'; + while (typePathIndex < typePathLength) { + c = typePath.charAt(typePathIndex++); + if (c >= '0' && c <= '9') { + typeArg = typeArg * 10 + c - '0'; + } else if (c == ';') { + break; + } else { + throw new IllegalArgumentException(); + } + } + output.put11(TYPE_ARGUMENT, typeArg); + } else { + throw new IllegalArgumentException(); + } + } + output.data[0] = (byte) (output.length / 2); + return new TypePath(output.data, 0); + } + + /** + * Returns a string representation of this type path. {@link #ARRAY_ELEMENT} steps are represented + * with '[', {@link #INNER_TYPE} steps with '.', {@link #WILDCARD_BOUND} steps with '*' and {@link + * #TYPE_ARGUMENT} steps with their type argument index in decimal form followed by ';'. + */ + @Override + public String toString() { + int length = getLength(); + StringBuilder result = new StringBuilder(length * 2); + for (int i = 0; i < length; ++i) { + switch (getStep(i)) { + case ARRAY_ELEMENT: + result.append('['); + break; + case INNER_TYPE: + result.append('.'); + break; + case WILDCARD_BOUND: + result.append('*'); + break; + case TYPE_ARGUMENT: + result.append(getStepArgument(i)).append(';'); + break; + default: + throw new AssertionError(); + } + } + return result.toString(); + } + + /** + * Puts the type_path JVMS structure corresponding to the given TypePath into the given + * ByteVector. + * + * @param typePath a TypePath instance, or {@literal null} for empty paths. + * @param output where the type path must be put. + */ + static void put(final TypePath typePath, final ByteVector output) { + if (typePath == null) { + output.putByte(0); + } else { + int length = typePath.typePathContainer[typePath.typePathOffset] * 2 + 1; + output.putByteArray(typePath.typePathContainer, typePath.typePathOffset, length); + } + } +} diff --git a/spring-core/src/main/java/org/springframework/asm/TypeReference.java b/spring-core/src/main/java/org/springframework/asm/TypeReference.java new file mode 100644 index 0000000..a565eff --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/TypeReference.java @@ -0,0 +1,436 @@ +// ASM: a very small and fast Java bytecode manipulation framework +// Copyright (c) 2000-2011 INRIA, France Telecom +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holders nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. + +package org.springframework.asm; + +/** + * A reference to a type appearing in a class, field or method declaration, or on an instruction. + * Such a reference designates the part of the class where the referenced type is appearing (e.g. an + * 'extends', 'implements' or 'throws' clause, a 'new' instruction, a 'catch' clause, a type cast, a + * local variable declaration, etc). + * + * @author Eric Bruneton + */ +public class TypeReference { + + /** + * The sort of type references that target a type parameter of a generic class. See {@link + * #getSort}. + */ + public static final int CLASS_TYPE_PARAMETER = 0x00; + + /** + * The sort of type references that target a type parameter of a generic method. See {@link + * #getSort}. + */ + public static final int METHOD_TYPE_PARAMETER = 0x01; + + /** + * The sort of type references that target the super class of a class or one of the interfaces it + * implements. See {@link #getSort}. + */ + public static final int CLASS_EXTENDS = 0x10; + + /** + * The sort of type references that target a bound of a type parameter of a generic class. See + * {@link #getSort}. + */ + public static final int CLASS_TYPE_PARAMETER_BOUND = 0x11; + + /** + * The sort of type references that target a bound of a type parameter of a generic method. See + * {@link #getSort}. + */ + public static final int METHOD_TYPE_PARAMETER_BOUND = 0x12; + + /** The sort of type references that target the type of a field. See {@link #getSort}. */ + public static final int FIELD = 0x13; + + /** The sort of type references that target the return type of a method. See {@link #getSort}. */ + public static final int METHOD_RETURN = 0x14; + + /** + * The sort of type references that target the receiver type of a method. See {@link #getSort}. + */ + public static final int METHOD_RECEIVER = 0x15; + + /** + * The sort of type references that target the type of a formal parameter of a method. See {@link + * #getSort}. + */ + public static final int METHOD_FORMAL_PARAMETER = 0x16; + + /** + * The sort of type references that target the type of an exception declared in the throws clause + * of a method. See {@link #getSort}. + */ + public static final int THROWS = 0x17; + + /** + * The sort of type references that target the type of a local variable in a method. See {@link + * #getSort}. + */ + public static final int LOCAL_VARIABLE = 0x40; + + /** + * The sort of type references that target the type of a resource variable in a method. See {@link + * #getSort}. + */ + public static final int RESOURCE_VARIABLE = 0x41; + + /** + * The sort of type references that target the type of the exception of a 'catch' clause in a + * method. See {@link #getSort}. + */ + public static final int EXCEPTION_PARAMETER = 0x42; + + /** + * The sort of type references that target the type declared in an 'instanceof' instruction. See + * {@link #getSort}. + */ + public static final int INSTANCEOF = 0x43; + + /** + * The sort of type references that target the type of the object created by a 'new' instruction. + * See {@link #getSort}. + */ + public static final int NEW = 0x44; + + /** + * The sort of type references that target the receiver type of a constructor reference. See + * {@link #getSort}. + */ + public static final int CONSTRUCTOR_REFERENCE = 0x45; + + /** + * The sort of type references that target the receiver type of a method reference. See {@link + * #getSort}. + */ + public static final int METHOD_REFERENCE = 0x46; + + /** + * The sort of type references that target the type declared in an explicit or implicit cast + * instruction. See {@link #getSort}. + */ + public static final int CAST = 0x47; + + /** + * The sort of type references that target a type parameter of a generic constructor in a + * constructor call. See {@link #getSort}. + */ + public static final int CONSTRUCTOR_INVOCATION_TYPE_ARGUMENT = 0x48; + + /** + * The sort of type references that target a type parameter of a generic method in a method call. + * See {@link #getSort}. + */ + public static final int METHOD_INVOCATION_TYPE_ARGUMENT = 0x49; + + /** + * The sort of type references that target a type parameter of a generic constructor in a + * constructor reference. See {@link #getSort}. + */ + public static final int CONSTRUCTOR_REFERENCE_TYPE_ARGUMENT = 0x4A; + + /** + * The sort of type references that target a type parameter of a generic method in a method + * reference. See {@link #getSort}. + */ + public static final int METHOD_REFERENCE_TYPE_ARGUMENT = 0x4B; + + /** + * The target_type and target_info structures - as defined in the Java Virtual Machine + * Specification (JVMS) - corresponding to this type reference. target_type uses one byte, and all + * the target_info union fields use up to 3 bytes (except localvar_target, handled with the + * specific method {@link MethodVisitor#visitLocalVariableAnnotation}). Thus, both structures can + * be stored in an int. + * + *

    This int field stores target_type (called the TypeReference 'sort' in the public API of this + * class) in its most significant byte, followed by the target_info fields. Depending on + * target_type, 1, 2 or even 3 least significant bytes of this field are unused. target_info + * fields which reference bytecode offsets are set to 0 (these offsets are ignored in ClassReader, + * and recomputed in MethodWriter). + * + * @see JVMS + * 4.7.20 + * @see JVMS + * 4.7.20.1 + */ + private final int targetTypeAndInfo; + + /** + * Constructs a new TypeReference. + * + * @param typeRef the int encoded value of the type reference, as received in a visit method + * related to type annotations, such as {@link ClassVisitor#visitTypeAnnotation}. + */ + public TypeReference(final int typeRef) { + this.targetTypeAndInfo = typeRef; + } + + /** + * Returns a type reference of the given sort. + * + * @param sort one of {@link #FIELD}, {@link #METHOD_RETURN}, {@link #METHOD_RECEIVER}, {@link + * #LOCAL_VARIABLE}, {@link #RESOURCE_VARIABLE}, {@link #INSTANCEOF}, {@link #NEW}, {@link + * #CONSTRUCTOR_REFERENCE}, or {@link #METHOD_REFERENCE}. + * @return a type reference of the given sort. + */ + public static TypeReference newTypeReference(final int sort) { + return new TypeReference(sort << 24); + } + + /** + * Returns a reference to a type parameter of a generic class or method. + * + * @param sort one of {@link #CLASS_TYPE_PARAMETER} or {@link #METHOD_TYPE_PARAMETER}. + * @param paramIndex the type parameter index. + * @return a reference to the given generic class or method type parameter. + */ + public static TypeReference newTypeParameterReference(final int sort, final int paramIndex) { + return new TypeReference((sort << 24) | (paramIndex << 16)); + } + + /** + * Returns a reference to a type parameter bound of a generic class or method. + * + * @param sort one of {@link #CLASS_TYPE_PARAMETER} or {@link #METHOD_TYPE_PARAMETER}. + * @param paramIndex the type parameter index. + * @param boundIndex the type bound index within the above type parameters. + * @return a reference to the given generic class or method type parameter bound. + */ + public static TypeReference newTypeParameterBoundReference( + final int sort, final int paramIndex, final int boundIndex) { + return new TypeReference((sort << 24) | (paramIndex << 16) | (boundIndex << 8)); + } + + /** + * Returns a reference to the super class or to an interface of the 'implements' clause of a + * class. + * + * @param itfIndex the index of an interface in the 'implements' clause of a class, or -1 to + * reference the super class of the class. + * @return a reference to the given super type of a class. + */ + public static TypeReference newSuperTypeReference(final int itfIndex) { + return new TypeReference((CLASS_EXTENDS << 24) | ((itfIndex & 0xFFFF) << 8)); + } + + /** + * Returns a reference to the type of a formal parameter of a method. + * + * @param paramIndex the formal parameter index. + * @return a reference to the type of the given method formal parameter. + */ + public static TypeReference newFormalParameterReference(final int paramIndex) { + return new TypeReference((METHOD_FORMAL_PARAMETER << 24) | (paramIndex << 16)); + } + + /** + * Returns a reference to the type of an exception, in a 'throws' clause of a method. + * + * @param exceptionIndex the index of an exception in a 'throws' clause of a method. + * @return a reference to the type of the given exception. + */ + public static TypeReference newExceptionReference(final int exceptionIndex) { + return new TypeReference((THROWS << 24) | (exceptionIndex << 8)); + } + + /** + * Returns a reference to the type of the exception declared in a 'catch' clause of a method. + * + * @param tryCatchBlockIndex the index of a try catch block (using the order in which they are + * visited with visitTryCatchBlock). + * @return a reference to the type of the given exception. + */ + public static TypeReference newTryCatchReference(final int tryCatchBlockIndex) { + return new TypeReference((EXCEPTION_PARAMETER << 24) | (tryCatchBlockIndex << 8)); + } + + /** + * Returns a reference to the type of a type argument in a constructor or method call or + * reference. + * + * @param sort one of {@link #CAST}, {@link #CONSTRUCTOR_INVOCATION_TYPE_ARGUMENT}, {@link + * #METHOD_INVOCATION_TYPE_ARGUMENT}, {@link #CONSTRUCTOR_REFERENCE_TYPE_ARGUMENT}, or {@link + * #METHOD_REFERENCE_TYPE_ARGUMENT}. + * @param argIndex the type argument index. + * @return a reference to the type of the given type argument. + */ + public static TypeReference newTypeArgumentReference(final int sort, final int argIndex) { + return new TypeReference((sort << 24) | argIndex); + } + + /** + * Returns the sort of this type reference. + * + * @return one of {@link #CLASS_TYPE_PARAMETER}, {@link #METHOD_TYPE_PARAMETER}, {@link + * #CLASS_EXTENDS}, {@link #CLASS_TYPE_PARAMETER_BOUND}, {@link #METHOD_TYPE_PARAMETER_BOUND}, + * {@link #FIELD}, {@link #METHOD_RETURN}, {@link #METHOD_RECEIVER}, {@link + * #METHOD_FORMAL_PARAMETER}, {@link #THROWS}, {@link #LOCAL_VARIABLE}, {@link + * #RESOURCE_VARIABLE}, {@link #EXCEPTION_PARAMETER}, {@link #INSTANCEOF}, {@link #NEW}, + * {@link #CONSTRUCTOR_REFERENCE}, {@link #METHOD_REFERENCE}, {@link #CAST}, {@link + * #CONSTRUCTOR_INVOCATION_TYPE_ARGUMENT}, {@link #METHOD_INVOCATION_TYPE_ARGUMENT}, {@link + * #CONSTRUCTOR_REFERENCE_TYPE_ARGUMENT}, or {@link #METHOD_REFERENCE_TYPE_ARGUMENT}. + */ + public int getSort() { + return targetTypeAndInfo >>> 24; + } + + /** + * Returns the index of the type parameter referenced by this type reference. This method must + * only be used for type references whose sort is {@link #CLASS_TYPE_PARAMETER}, {@link + * #METHOD_TYPE_PARAMETER}, {@link #CLASS_TYPE_PARAMETER_BOUND} or {@link + * #METHOD_TYPE_PARAMETER_BOUND}. + * + * @return a type parameter index. + */ + public int getTypeParameterIndex() { + return (targetTypeAndInfo & 0x00FF0000) >> 16; + } + + /** + * Returns the index of the type parameter bound, within the type parameter {@link + * #getTypeParameterIndex}, referenced by this type reference. This method must only be used for + * type references whose sort is {@link #CLASS_TYPE_PARAMETER_BOUND} or {@link + * #METHOD_TYPE_PARAMETER_BOUND}. + * + * @return a type parameter bound index. + */ + public int getTypeParameterBoundIndex() { + return (targetTypeAndInfo & 0x0000FF00) >> 8; + } + + /** + * Returns the index of the "super type" of a class that is referenced by this type reference. + * This method must only be used for type references whose sort is {@link #CLASS_EXTENDS}. + * + * @return the index of an interface in the 'implements' clause of a class, or -1 if this type + * reference references the type of the super class. + */ + public int getSuperTypeIndex() { + return (short) ((targetTypeAndInfo & 0x00FFFF00) >> 8); + } + + /** + * Returns the index of the formal parameter whose type is referenced by this type reference. This + * method must only be used for type references whose sort is {@link #METHOD_FORMAL_PARAMETER}. + * + * @return a formal parameter index. + */ + public int getFormalParameterIndex() { + return (targetTypeAndInfo & 0x00FF0000) >> 16; + } + + /** + * Returns the index of the exception, in a 'throws' clause of a method, whose type is referenced + * by this type reference. This method must only be used for type references whose sort is {@link + * #THROWS}. + * + * @return the index of an exception in the 'throws' clause of a method. + */ + public int getExceptionIndex() { + return (targetTypeAndInfo & 0x00FFFF00) >> 8; + } + + /** + * Returns the index of the try catch block (using the order in which they are visited with + * visitTryCatchBlock), whose 'catch' type is referenced by this type reference. This method must + * only be used for type references whose sort is {@link #EXCEPTION_PARAMETER} . + * + * @return the index of an exception in the 'throws' clause of a method. + */ + public int getTryCatchBlockIndex() { + return (targetTypeAndInfo & 0x00FFFF00) >> 8; + } + + /** + * Returns the index of the type argument referenced by this type reference. This method must only + * be used for type references whose sort is {@link #CAST}, {@link + * #CONSTRUCTOR_INVOCATION_TYPE_ARGUMENT}, {@link #METHOD_INVOCATION_TYPE_ARGUMENT}, {@link + * #CONSTRUCTOR_REFERENCE_TYPE_ARGUMENT}, or {@link #METHOD_REFERENCE_TYPE_ARGUMENT}. + * + * @return a type parameter index. + */ + public int getTypeArgumentIndex() { + return targetTypeAndInfo & 0xFF; + } + + /** + * Returns the int encoded value of this type reference, suitable for use in visit methods related + * to type annotations, like visitTypeAnnotation. + * + * @return the int encoded value of this type reference. + */ + public int getValue() { + return targetTypeAndInfo; + } + + /** + * Puts the given target_type and target_info JVMS structures into the given ByteVector. + * + * @param targetTypeAndInfo a target_type and a target_info structures encoded as in {@link + * #targetTypeAndInfo}. LOCAL_VARIABLE and RESOURCE_VARIABLE target types are not supported. + * @param output where the type reference must be put. + */ + static void putTarget(final int targetTypeAndInfo, final ByteVector output) { + switch (targetTypeAndInfo >>> 24) { + case CLASS_TYPE_PARAMETER: + case METHOD_TYPE_PARAMETER: + case METHOD_FORMAL_PARAMETER: + output.putShort(targetTypeAndInfo >>> 16); + break; + case FIELD: + case METHOD_RETURN: + case METHOD_RECEIVER: + output.putByte(targetTypeAndInfo >>> 24); + break; + case CAST: + case CONSTRUCTOR_INVOCATION_TYPE_ARGUMENT: + case METHOD_INVOCATION_TYPE_ARGUMENT: + case CONSTRUCTOR_REFERENCE_TYPE_ARGUMENT: + case METHOD_REFERENCE_TYPE_ARGUMENT: + output.putInt(targetTypeAndInfo); + break; + case CLASS_EXTENDS: + case CLASS_TYPE_PARAMETER_BOUND: + case METHOD_TYPE_PARAMETER_BOUND: + case THROWS: + case EXCEPTION_PARAMETER: + case INSTANCEOF: + case NEW: + case CONSTRUCTOR_REFERENCE: + case METHOD_REFERENCE: + output.put12(targetTypeAndInfo >>> 24, (targetTypeAndInfo & 0xFFFF00) >> 8); + break; + default: + throw new IllegalArgumentException(); + } + } +} diff --git a/spring-core/src/main/java/org/springframework/asm/package-info.java b/spring-core/src/main/java/org/springframework/asm/package-info.java new file mode 100644 index 0000000..18df410 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/asm/package-info.java @@ -0,0 +1,13 @@ +/** + * Spring's repackaging of + * ASM 9.0 + * (with Spring-specific patches; for internal use only). + * + *

    This repackaging technique avoids any potential conflicts with + * dependencies on ASM at the application level or from third-party + * libraries and frameworks. + * + *

    As this repackaging happens at the class file level, sources + * and javadocs are not available here. + */ +package org.springframework.asm; diff --git a/spring-core/src/main/java/org/springframework/cglib/SpringCglibInfo.java b/spring-core/src/main/java/org/springframework/cglib/SpringCglibInfo.java new file mode 100644 index 0000000..ca4f47a --- /dev/null +++ b/spring-core/src/main/java/org/springframework/cglib/SpringCglibInfo.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cglib; + +/** + * Empty class used to ensure that the {@code org.springframework.cglib} + * package is processed during javadoc generation. + * + *

    See package-level javadocs for more + * information on {@code org.springframework.cglib}. + * + * @author Chris Beams + * @since 3.2 + */ +public final class SpringCglibInfo { + +} diff --git a/spring-core/src/main/java/org/springframework/cglib/core/AbstractClassGenerator.java b/spring-core/src/main/java/org/springframework/cglib/core/AbstractClassGenerator.java new file mode 100644 index 0000000..4d05d05 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/cglib/core/AbstractClassGenerator.java @@ -0,0 +1,383 @@ +/* + * Copyright 2003,2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://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.springframework.cglib.core; + +import java.lang.ref.WeakReference; +import java.security.ProtectionDomain; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; + +import org.springframework.asm.ClassReader; +import org.springframework.cglib.core.internal.Function; +import org.springframework.cglib.core.internal.LoadingCache; + +/** + * Abstract class for all code-generating CGLIB utilities. + * In addition to caching generated classes for performance, it provides hooks for + * customizing the ClassLoader, name of the generated class, and transformations + * applied before generation. + */ +@SuppressWarnings({"rawtypes", "unchecked"}) +abstract public class AbstractClassGenerator implements ClassGenerator { + + private static final ThreadLocal CURRENT = new ThreadLocal(); + + private static volatile Map CACHE = new WeakHashMap(); + + private static final boolean DEFAULT_USE_CACHE = + Boolean.parseBoolean(System.getProperty("cglib.useCache", "true")); + + + private GeneratorStrategy strategy = DefaultGeneratorStrategy.INSTANCE; + + private NamingPolicy namingPolicy = DefaultNamingPolicy.INSTANCE; + + private Source source; + + private ClassLoader classLoader; + + private Class contextClass; + + private String namePrefix; + + private Object key; + + private boolean useCache = DEFAULT_USE_CACHE; + + private String className; + + private boolean attemptLoad; + + + protected static class ClassLoaderData { + + private final Set reservedClassNames = new HashSet(); + + /** + * {@link AbstractClassGenerator} here holds "cache key" (e.g. {@link org.springframework.cglib.proxy.Enhancer} + * configuration), and the value is the generated class plus some additional values + * (see {@link #unwrapCachedValue(Object)}. + *

    The generated classes can be reused as long as their classloader is reachable.

    + *

    Note: the only way to access a class is to find it through generatedClasses cache, thus + * the key should not expire as long as the class itself is alive (its classloader is alive).

    + */ + private final LoadingCache generatedClasses; + + /** + * Note: ClassLoaderData object is stored as a value of {@code WeakHashMap} thus + * this classLoader reference should be weak otherwise it would make classLoader strongly reachable + * and alive forever. + * Reference queue is not required since the cleanup is handled by {@link WeakHashMap}. + */ + private final WeakReference classLoader; + + private final Predicate uniqueNamePredicate = new Predicate() { + public boolean evaluate(Object name) { + return reservedClassNames.contains(name); + } + }; + + private static final Function GET_KEY = new Function() { + public Object apply(AbstractClassGenerator gen) { + return gen.key; + } + }; + + public ClassLoaderData(ClassLoader classLoader) { + if (classLoader == null) { + throw new IllegalArgumentException("classLoader == null is not yet supported"); + } + this.classLoader = new WeakReference(classLoader); + Function load = + new Function() { + public Object apply(AbstractClassGenerator gen) { + Class klass = gen.generate(ClassLoaderData.this); + return gen.wrapCachedClass(klass); + } + }; + generatedClasses = new LoadingCache(GET_KEY, load); + } + + public ClassLoader getClassLoader() { + return classLoader.get(); + } + + public void reserveName(String name) { + reservedClassNames.add(name); + } + + public Predicate getUniqueNamePredicate() { + return uniqueNamePredicate; + } + + public Object get(AbstractClassGenerator gen, boolean useCache) { + if (!useCache) { + return gen.generate(ClassLoaderData.this); + } + else { + Object cachedValue = generatedClasses.get(gen); + return gen.unwrapCachedValue(cachedValue); + } + } + } + + + protected T wrapCachedClass(Class klass) { + return (T) new WeakReference(klass); + } + + protected Object unwrapCachedValue(T cached) { + return ((WeakReference) cached).get(); + } + + + protected static class Source { + + String name; + + public Source(String name) { + this.name = name; + } + } + + + protected AbstractClassGenerator(Source source) { + this.source = source; + } + + protected void setNamePrefix(String namePrefix) { + this.namePrefix = namePrefix; + } + + final protected String getClassName() { + return className; + } + + private void setClassName(String className) { + this.className = className; + } + + private String generateClassName(Predicate nameTestPredicate) { + return namingPolicy.getClassName(namePrefix, source.name, key, nameTestPredicate); + } + + /** + * Set the ClassLoader in which the class will be generated. + * Concrete subclasses of AbstractClassGenerator (such as Enhancer) + * will try to choose an appropriate default if this is unset. + *

    + * Classes are cached per-ClassLoader using a WeakHashMap, to allow + * the generated classes to be removed when the associated loader is garbage collected. + * @param classLoader the loader to generate the new class with, or null to use the default + */ + public void setClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + // SPRING PATCH BEGIN + public void setContextClass(Class contextClass) { + this.contextClass = contextClass; + } + // SPRING PATCH END + + /** + * Override the default naming policy. + * @param namingPolicy the custom policy, or null to use the default + * @see DefaultNamingPolicy + */ + public void setNamingPolicy(NamingPolicy namingPolicy) { + if (namingPolicy == null) + namingPolicy = DefaultNamingPolicy.INSTANCE; + this.namingPolicy = namingPolicy; + } + + /** + * @see #setNamingPolicy + */ + public NamingPolicy getNamingPolicy() { + return namingPolicy; + } + + /** + * Whether use and update the static cache of generated classes + * for a class with the same properties. Default is true. + */ + public void setUseCache(boolean useCache) { + this.useCache = useCache; + } + + /** + * @see #setUseCache + */ + public boolean getUseCache() { + return useCache; + } + + /** + * If set, CGLIB will attempt to load classes from the specified + * ClassLoader before generating them. Because generated + * class names are not guaranteed to be unique, the default is false. + */ + public void setAttemptLoad(boolean attemptLoad) { + this.attemptLoad = attemptLoad; + } + + public boolean getAttemptLoad() { + return attemptLoad; + } + + /** + * Set the strategy to use to create the bytecode from this generator. + * By default an instance of {@link DefaultGeneratorStrategy} is used. + */ + public void setStrategy(GeneratorStrategy strategy) { + if (strategy == null) + strategy = DefaultGeneratorStrategy.INSTANCE; + this.strategy = strategy; + } + + /** + * @see #setStrategy + */ + public GeneratorStrategy getStrategy() { + return strategy; + } + + /** + * Used internally by CGLIB. Returns the AbstractClassGenerator + * that is being used to generate a class in the current thread. + */ + public static AbstractClassGenerator getCurrent() { + return (AbstractClassGenerator) CURRENT.get(); + } + + public ClassLoader getClassLoader() { + ClassLoader t = classLoader; + if (t == null) { + t = getDefaultClassLoader(); + } + if (t == null) { + t = getClass().getClassLoader(); + } + if (t == null) { + t = Thread.currentThread().getContextClassLoader(); + } + if (t == null) { + throw new IllegalStateException("Cannot determine classloader"); + } + return t; + } + + abstract protected ClassLoader getDefaultClassLoader(); + + /** + * Returns the protection domain to use when defining the class. + *

    + * Default implementation returns null for using a default protection domain. Sub-classes may + * override to use a more specific protection domain. + *

    + * @return the protection domain (null for using a default) + */ + protected ProtectionDomain getProtectionDomain() { + return null; + } + + protected Object create(Object key) { + try { + ClassLoader loader = getClassLoader(); + Map cache = CACHE; + ClassLoaderData data = cache.get(loader); + if (data == null) { + synchronized (AbstractClassGenerator.class) { + cache = CACHE; + data = cache.get(loader); + if (data == null) { + Map newCache = new WeakHashMap(cache); + data = new ClassLoaderData(loader); + newCache.put(loader, data); + CACHE = newCache; + } + } + } + this.key = key; + Object obj = data.get(this, getUseCache()); + if (obj instanceof Class) { + return firstInstance((Class) obj); + } + return nextInstance(obj); + } + catch (RuntimeException | Error ex) { + throw ex; + } + catch (Exception ex) { + throw new CodeGenerationException(ex); + } + } + + protected Class generate(ClassLoaderData data) { + Class gen; + Object save = CURRENT.get(); + CURRENT.set(this); + try { + ClassLoader classLoader = data.getClassLoader(); + if (classLoader == null) { + throw new IllegalStateException("ClassLoader is null while trying to define class " + + getClassName() + ". It seems that the loader has been expired from a weak reference somehow. " + + "Please file an issue at cglib's issue tracker."); + } + synchronized (classLoader) { + String name = generateClassName(data.getUniqueNamePredicate()); + data.reserveName(name); + this.setClassName(name); + } + if (attemptLoad) { + try { + gen = classLoader.loadClass(getClassName()); + return gen; + } + catch (ClassNotFoundException e) { + // ignore + } + } + byte[] b = strategy.generate(this); + String className = ClassNameReader.getClassName(new ClassReader(b)); + ProtectionDomain protectionDomain = getProtectionDomain(); + synchronized (classLoader) { // just in case + // SPRING PATCH BEGIN + gen = ReflectUtils.defineClass(className, b, classLoader, protectionDomain, contextClass); + // SPRING PATCH END + } + return gen; + } + catch (RuntimeException | Error ex) { + throw ex; + } + catch (Exception ex) { + throw new CodeGenerationException(ex); + } + finally { + CURRENT.set(save); + } + } + + abstract protected Object firstInstance(Class type) throws Exception; + + abstract protected Object nextInstance(Object instance) throws Exception; + +} diff --git a/spring-core/src/main/java/org/springframework/cglib/core/AsmApi.java b/spring-core/src/main/java/org/springframework/cglib/core/AsmApi.java new file mode 100644 index 0000000..bda25dc --- /dev/null +++ b/spring-core/src/main/java/org/springframework/cglib/core/AsmApi.java @@ -0,0 +1,33 @@ +/* + * Copyright 2003,2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://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.springframework.cglib.core; + +import org.springframework.asm.Opcodes; + +final class AsmApi { + + /** + * SPRING PATCH: always returns ASM9. + */ + static int value() { + return Opcodes.ASM9; + } + + private AsmApi() { + } + +} diff --git a/spring-core/src/main/java/org/springframework/cglib/core/ClassLoaderAwareGeneratorStrategy.java b/spring-core/src/main/java/org/springframework/cglib/core/ClassLoaderAwareGeneratorStrategy.java new file mode 100644 index 0000000..eb74644 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/cglib/core/ClassLoaderAwareGeneratorStrategy.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cglib.core; + +/** + * CGLIB GeneratorStrategy variant which exposes the application ClassLoader + * as current thread context ClassLoader for the time of class generation. + * The ASM ClassWriter in Spring's ASM variant will pick it up when doing + * common superclass resolution. + * + * @author Juergen Hoeller + * @since 5.2 + */ +public class ClassLoaderAwareGeneratorStrategy extends DefaultGeneratorStrategy { + + private final ClassLoader classLoader; + + public ClassLoaderAwareGeneratorStrategy(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + @Override + public byte[] generate(ClassGenerator cg) throws Exception { + if (this.classLoader == null) { + return super.generate(cg); + } + + Thread currentThread = Thread.currentThread(); + ClassLoader threadContextClassLoader; + try { + threadContextClassLoader = currentThread.getContextClassLoader(); + } + catch (Throwable ex) { + // Cannot access thread context ClassLoader - falling back... + return super.generate(cg); + } + + boolean overrideClassLoader = !this.classLoader.equals(threadContextClassLoader); + if (overrideClassLoader) { + currentThread.setContextClassLoader(this.classLoader); + } + try { + return super.generate(cg); + } + finally { + if (overrideClassLoader) { + // Reset original thread context ClassLoader. + currentThread.setContextClassLoader(threadContextClassLoader); + } + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/cglib/core/KeyFactory.java b/spring-core/src/main/java/org/springframework/cglib/core/KeyFactory.java new file mode 100644 index 0000000..e1503b8 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/cglib/core/KeyFactory.java @@ -0,0 +1,363 @@ +/* + * Copyright 2003,2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://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.springframework.cglib.core; + +import java.lang.reflect.Method; +import java.security.ProtectionDomain; +import java.util.Collections; +import java.util.List; + +import org.springframework.asm.ClassVisitor; +import org.springframework.asm.Label; +import org.springframework.asm.Type; +import org.springframework.cglib.core.internal.CustomizerRegistry; + +/** + * Generates classes to handle multi-valued keys, for use in things such as Maps and Sets. + * Code for equals and hashCode methods follow the + * the rules laid out in Effective Java by Joshua Bloch. + *

    + * To generate a KeyFactory, you need to supply an interface which + * describes the structure of the key. The interface should have a + * single method named newInstance, which returns an + * Object. The arguments array can be + * anything--Objects, primitive values, or single or + * multi-dimension arrays of either. For example: + *

    + *     private interface IntStringKey {
    + *         public Object newInstance(int i, String s);
    + *     }
    + * 

    + * Once you have made a KeyFactory, you generate a new key by calling + * the newInstance method defined by your interface. + *

    + *     IntStringKey factory = (IntStringKey)KeyFactory.create(IntStringKey.class);
    + *     Object key1 = factory.newInstance(4, "Hello");
    + *     Object key2 = factory.newInstance(4, "World");
    + * 

    + * Note: + * hashCode equality between two keys key1 and key2 is only guaranteed if + * key1.equals(key2) and the keys were produced by the same factory. + * @version $Id: KeyFactory.java,v 1.26 2006/03/05 02:43:19 herbyderby Exp $ + */ +@SuppressWarnings({"rawtypes", "unchecked"}) +abstract public class KeyFactory { + + private static final Signature GET_NAME = + TypeUtils.parseSignature("String getName()"); + + private static final Signature GET_CLASS = + TypeUtils.parseSignature("Class getClass()"); + + private static final Signature HASH_CODE = + TypeUtils.parseSignature("int hashCode()"); + + private static final Signature EQUALS = + TypeUtils.parseSignature("boolean equals(Object)"); + + private static final Signature TO_STRING = + TypeUtils.parseSignature("String toString()"); + + private static final Signature APPEND_STRING = + TypeUtils.parseSignature("StringBuffer append(String)"); + + private static final Type KEY_FACTORY = + TypeUtils.parseType("org.springframework.cglib.core.KeyFactory"); + + private static final Signature GET_SORT = + TypeUtils.parseSignature("int getSort()"); + + //generated numbers: + private final static int PRIMES[] = { + 11, 73, 179, 331, + 521, 787, 1213, 1823, + 2609, 3691, 5189, 7247, + 10037, 13931, 19289, 26627, + 36683, 50441, 69403, 95401, + 131129, 180179, 247501, 340057, + 467063, 641371, 880603, 1209107, + 1660097, 2279161, 3129011, 4295723, + 5897291, 8095873, 11114263, 15257791, + 20946017, 28754629, 39474179, 54189869, + 74391461, 102123817, 140194277, 192456917, + 264202273, 362693231, 497900099, 683510293, + 938313161, 1288102441, 1768288259}; + + + public static final Customizer CLASS_BY_NAME = new Customizer() { + public void customize(CodeEmitter e, Type type) { + if (type.equals(Constants.TYPE_CLASS)) { + e.invoke_virtual(Constants.TYPE_CLASS, GET_NAME); + } + } + }; + + public static final FieldTypeCustomizer STORE_CLASS_AS_STRING = new FieldTypeCustomizer() { + public void customize(CodeEmitter e, int index, Type type) { + if (type.equals(Constants.TYPE_CLASS)) { + e.invoke_virtual(Constants.TYPE_CLASS, GET_NAME); + } + } + public Type getOutType(int index, Type type) { + if (type.equals(Constants.TYPE_CLASS)) { + return Constants.TYPE_STRING; + } + return type; + } + }; + + /** + * {@link Type#hashCode()} is very expensive as it traverses full descriptor to calculate hash code. + * This customizer uses {@link Type#getSort()} as a hash code. + */ + public static final HashCodeCustomizer HASH_ASM_TYPE = new HashCodeCustomizer() { + public boolean customize(CodeEmitter e, Type type) { + if (Constants.TYPE_TYPE.equals(type)) { + e.invoke_virtual(type, GET_SORT); + return true; + } + return false; + } + }; + + /** + * @deprecated this customizer might result in unexpected class leak since key object still holds a strong reference to the Object and class. + * It is recommended to have pre-processing method that would strip Objects and represent Classes as Strings + */ + @Deprecated + public static final Customizer OBJECT_BY_CLASS = new Customizer() { + public void customize(CodeEmitter e, Type type) { + e.invoke_virtual(Constants.TYPE_OBJECT, GET_CLASS); + } + }; + + protected KeyFactory() { + } + + public static KeyFactory create(Class keyInterface) { + return create(keyInterface, null); + } + + public static KeyFactory create(Class keyInterface, Customizer customizer) { + return create(keyInterface.getClassLoader(), keyInterface, customizer); + } + + public static KeyFactory create(Class keyInterface, KeyFactoryCustomizer first, List next) { + return create(keyInterface.getClassLoader(), keyInterface, first, next); + } + + public static KeyFactory create(ClassLoader loader, Class keyInterface, Customizer customizer) { + return create(loader, keyInterface, customizer, Collections.emptyList()); + } + + public static KeyFactory create(ClassLoader loader, Class keyInterface, KeyFactoryCustomizer customizer, + List next) { + Generator gen = new Generator(); + gen.setInterface(keyInterface); + // SPRING PATCH BEGIN + gen.setContextClass(keyInterface); + // SPRING PATCH END + + if (customizer != null) { + gen.addCustomizer(customizer); + } + if (next != null && !next.isEmpty()) { + for (KeyFactoryCustomizer keyFactoryCustomizer : next) { + gen.addCustomizer(keyFactoryCustomizer); + } + } + gen.setClassLoader(loader); + return gen.create(); + } + + + public static class Generator extends AbstractClassGenerator { + + private static final Source SOURCE = new Source(KeyFactory.class.getName()); + + private static final Class[] KNOWN_CUSTOMIZER_TYPES = new Class[]{Customizer.class, FieldTypeCustomizer.class}; + + private Class keyInterface; + + // TODO: Make me final when deprecated methods are removed + private CustomizerRegistry customizers = new CustomizerRegistry(KNOWN_CUSTOMIZER_TYPES); + + private int constant; + + private int multiplier; + + public Generator() { + super(SOURCE); + } + + protected ClassLoader getDefaultClassLoader() { + return keyInterface.getClassLoader(); + } + + protected ProtectionDomain getProtectionDomain() { + return ReflectUtils.getProtectionDomain(keyInterface); + } + + /** + * @deprecated Use {@link #addCustomizer(KeyFactoryCustomizer)} instead. + */ + @Deprecated + public void setCustomizer(Customizer customizer) { + customizers = CustomizerRegistry.singleton(customizer); + } + + public void addCustomizer(KeyFactoryCustomizer customizer) { + customizers.add(customizer); + } + + public List getCustomizers(Class klass) { + return customizers.get(klass); + } + + public void setInterface(Class keyInterface) { + this.keyInterface = keyInterface; + } + + public KeyFactory create() { + setNamePrefix(keyInterface.getName()); + return (KeyFactory) super.create(keyInterface.getName()); + } + + public void setHashConstant(int constant) { + this.constant = constant; + } + + public void setHashMultiplier(int multiplier) { + this.multiplier = multiplier; + } + + protected Object firstInstance(Class type) { + return ReflectUtils.newInstance(type); + } + + protected Object nextInstance(Object instance) { + return instance; + } + + public void generateClass(ClassVisitor v) { + ClassEmitter ce = new ClassEmitter(v); + + Method newInstance = ReflectUtils.findNewInstance(keyInterface); + if (!newInstance.getReturnType().equals(Object.class)) { + throw new IllegalArgumentException("newInstance method must return Object"); + } + + Type[] parameterTypes = TypeUtils.getTypes(newInstance.getParameterTypes()); + ce.begin_class(Constants.V1_8, + Constants.ACC_PUBLIC, + getClassName(), + KEY_FACTORY, + new Type[]{Type.getType(keyInterface)}, + Constants.SOURCE_FILE); + EmitUtils.null_constructor(ce); + EmitUtils.factory_method(ce, ReflectUtils.getSignature(newInstance)); + + int seed = 0; + CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC, + TypeUtils.parseConstructor(parameterTypes), + null); + e.load_this(); + e.super_invoke_constructor(); + e.load_this(); + List fieldTypeCustomizers = getCustomizers(FieldTypeCustomizer.class); + for (int i = 0; i < parameterTypes.length; i++) { + Type parameterType = parameterTypes[i]; + Type fieldType = parameterType; + for (FieldTypeCustomizer customizer : fieldTypeCustomizers) { + fieldType = customizer.getOutType(i, fieldType); + } + seed += fieldType.hashCode(); + ce.declare_field(Constants.ACC_PRIVATE | Constants.ACC_FINAL, + getFieldName(i), + fieldType, + null); + e.dup(); + e.load_arg(i); + for (FieldTypeCustomizer customizer : fieldTypeCustomizers) { + customizer.customize(e, i, parameterType); + } + e.putfield(getFieldName(i)); + } + e.return_value(); + e.end_method(); + + // hash code + e = ce.begin_method(Constants.ACC_PUBLIC, HASH_CODE, null); + int hc = (constant != 0) ? constant : PRIMES[(Math.abs(seed) % PRIMES.length)]; + int hm = (multiplier != 0) ? multiplier : PRIMES[(Math.abs(seed * 13) % PRIMES.length)]; + e.push(hc); + for (int i = 0; i < parameterTypes.length; i++) { + e.load_this(); + e.getfield(getFieldName(i)); + EmitUtils.hash_code(e, parameterTypes[i], hm, customizers); + } + e.return_value(); + e.end_method(); + + // equals + e = ce.begin_method(Constants.ACC_PUBLIC, EQUALS, null); + Label fail = e.make_label(); + e.load_arg(0); + e.instance_of_this(); + e.if_jump(CodeEmitter.EQ, fail); + for (int i = 0; i < parameterTypes.length; i++) { + e.load_this(); + e.getfield(getFieldName(i)); + e.load_arg(0); + e.checkcast_this(); + e.getfield(getFieldName(i)); + EmitUtils.not_equals(e, parameterTypes[i], fail, customizers); + } + e.push(1); + e.return_value(); + e.mark(fail); + e.push(0); + e.return_value(); + e.end_method(); + + // toString + e = ce.begin_method(Constants.ACC_PUBLIC, TO_STRING, null); + e.new_instance(Constants.TYPE_STRING_BUFFER); + e.dup(); + e.invoke_constructor(Constants.TYPE_STRING_BUFFER); + for (int i = 0; i < parameterTypes.length; i++) { + if (i > 0) { + e.push(", "); + e.invoke_virtual(Constants.TYPE_STRING_BUFFER, APPEND_STRING); + } + e.load_this(); + e.getfield(getFieldName(i)); + EmitUtils.append_string(e, parameterTypes[i], EmitUtils.DEFAULT_DELIMITERS, customizers); + } + e.invoke_virtual(Constants.TYPE_STRING_BUFFER, TO_STRING); + e.return_value(); + e.end_method(); + + ce.end_class(); + } + + private String getFieldName(int arg) { + return "FIELD_" + arg; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java b/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java new file mode 100644 index 0000000..a8f4cb5 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java @@ -0,0 +1,654 @@ +/* + * Copyright 2003,2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://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.springframework.cglib.core; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.security.PrivilegedExceptionAction; +import java.security.ProtectionDomain; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.asm.Attribute; +import org.springframework.asm.Type; + +/** + * @version $Id: ReflectUtils.java,v 1.30 2009/01/11 19:47:49 herbyderby Exp $ + */ +@SuppressWarnings({"rawtypes", "unchecked"}) +public class ReflectUtils { + + private ReflectUtils() { + } + + private static final Map primitives = new HashMap(8); + + private static final Map transforms = new HashMap(8); + + private static final ClassLoader defaultLoader = ReflectUtils.class.getClassLoader(); + + // SPRING PATCH BEGIN + private static final Method privateLookupInMethod; + + private static final Method lookupDefineClassMethod; + + private static final Method classLoaderDefineClassMethod; + + private static final ProtectionDomain PROTECTION_DOMAIN; + + private static final Throwable THROWABLE; + + private static final List OBJECT_METHODS = new ArrayList(); + + static { + Method privateLookupIn; + Method lookupDefineClass; + Method classLoaderDefineClass; + ProtectionDomain protectionDomain; + Throwable throwable = null; + try { + privateLookupIn = (Method) AccessController.doPrivileged(new PrivilegedExceptionAction() { + public Object run() throws Exception { + try { + return MethodHandles.class.getMethod("privateLookupIn", Class.class, MethodHandles.Lookup.class); + } + catch (NoSuchMethodException ex) { + return null; + } + } + }); + lookupDefineClass = (Method) AccessController.doPrivileged(new PrivilegedExceptionAction() { + public Object run() throws Exception { + try { + return MethodHandles.Lookup.class.getMethod("defineClass", byte[].class); + } + catch (NoSuchMethodException ex) { + return null; + } + } + }); + classLoaderDefineClass = (Method) AccessController.doPrivileged(new PrivilegedExceptionAction() { + public Object run() throws Exception { + return ClassLoader.class.getDeclaredMethod("defineClass", + String.class, byte[].class, Integer.TYPE, Integer.TYPE, ProtectionDomain.class); + } + }); + protectionDomain = getProtectionDomain(ReflectUtils.class); + AccessController.doPrivileged(new PrivilegedExceptionAction() { + public Object run() throws Exception { + Method[] methods = Object.class.getDeclaredMethods(); + for (Method method : methods) { + if ("finalize".equals(method.getName()) + || (method.getModifiers() & (Modifier.FINAL | Modifier.STATIC)) > 0) { + continue; + } + OBJECT_METHODS.add(method); + } + return null; + } + }); + } + catch (Throwable t) { + privateLookupIn = null; + lookupDefineClass = null; + classLoaderDefineClass = null; + protectionDomain = null; + throwable = t; + } + privateLookupInMethod = privateLookupIn; + lookupDefineClassMethod = lookupDefineClass; + classLoaderDefineClassMethod = classLoaderDefineClass; + PROTECTION_DOMAIN = protectionDomain; + THROWABLE = throwable; + } + // SPRING PATCH END + + private static final String[] CGLIB_PACKAGES = { + "java.lang", + }; + + static { + primitives.put("byte", Byte.TYPE); + primitives.put("char", Character.TYPE); + primitives.put("double", Double.TYPE); + primitives.put("float", Float.TYPE); + primitives.put("int", Integer.TYPE); + primitives.put("long", Long.TYPE); + primitives.put("short", Short.TYPE); + primitives.put("boolean", Boolean.TYPE); + + transforms.put("byte", "B"); + transforms.put("char", "C"); + transforms.put("double", "D"); + transforms.put("float", "F"); + transforms.put("int", "I"); + transforms.put("long", "J"); + transforms.put("short", "S"); + transforms.put("boolean", "Z"); + } + + public static ProtectionDomain getProtectionDomain(final Class source) { + if (source == null) { + return null; + } + return (ProtectionDomain) AccessController.doPrivileged(new PrivilegedAction() { + public Object run() { + return source.getProtectionDomain(); + } + }); + } + + public static Type[] getExceptionTypes(Member member) { + if (member instanceof Method) { + return TypeUtils.getTypes(((Method) member).getExceptionTypes()); + } + else if (member instanceof Constructor) { + return TypeUtils.getTypes(((Constructor) member).getExceptionTypes()); + } + else { + throw new IllegalArgumentException("Cannot get exception types of a field"); + } + } + + public static Signature getSignature(Member member) { + if (member instanceof Method) { + return new Signature(member.getName(), Type.getMethodDescriptor((Method) member)); + } + else if (member instanceof Constructor) { + Type[] types = TypeUtils.getTypes(((Constructor) member).getParameterTypes()); + return new Signature(Constants.CONSTRUCTOR_NAME, + Type.getMethodDescriptor(Type.VOID_TYPE, types)); + + } + else { + throw new IllegalArgumentException("Cannot get signature of a field"); + } + } + + public static Constructor findConstructor(String desc) { + return findConstructor(desc, defaultLoader); + } + + public static Constructor findConstructor(String desc, ClassLoader loader) { + try { + int lparen = desc.indexOf('('); + String className = desc.substring(0, lparen).trim(); + return getClass(className, loader).getConstructor(parseTypes(desc, loader)); + } + catch (ClassNotFoundException | NoSuchMethodException ex) { + throw new CodeGenerationException(ex); + } + } + + public static Method findMethod(String desc) { + return findMethod(desc, defaultLoader); + } + + public static Method findMethod(String desc, ClassLoader loader) { + try { + int lparen = desc.indexOf('('); + int dot = desc.lastIndexOf('.', lparen); + String className = desc.substring(0, dot).trim(); + String methodName = desc.substring(dot + 1, lparen).trim(); + return getClass(className, loader).getDeclaredMethod(methodName, parseTypes(desc, loader)); + } + catch (ClassNotFoundException | NoSuchMethodException ex) { + throw new CodeGenerationException(ex); + } + } + + private static Class[] parseTypes(String desc, ClassLoader loader) throws ClassNotFoundException { + int lparen = desc.indexOf('('); + int rparen = desc.indexOf(')', lparen); + List params = new ArrayList(); + int start = lparen + 1; + for (; ; ) { + int comma = desc.indexOf(',', start); + if (comma < 0) { + break; + } + params.add(desc.substring(start, comma).trim()); + start = comma + 1; + } + if (start < rparen) { + params.add(desc.substring(start, rparen).trim()); + } + Class[] types = new Class[params.size()]; + for (int i = 0; i < types.length; i++) { + types[i] = getClass((String) params.get(i), loader); + } + return types; + } + + private static Class getClass(String className, ClassLoader loader) throws ClassNotFoundException { + return getClass(className, loader, CGLIB_PACKAGES); + } + + private static Class getClass(String className, ClassLoader loader, String[] packages) throws ClassNotFoundException { + String save = className; + int dimensions = 0; + int index = 0; + while ((index = className.indexOf("[]", index) + 1) > 0) { + dimensions++; + } + StringBuilder brackets = new StringBuilder(className.length() - dimensions); + for (int i = 0; i < dimensions; i++) { + brackets.append('['); + } + className = className.substring(0, className.length() - 2 * dimensions); + + String prefix = (dimensions > 0) ? brackets + "L" : ""; + String suffix = (dimensions > 0) ? ";" : ""; + try { + return Class.forName(prefix + className + suffix, false, loader); + } + catch (ClassNotFoundException ignore) { + } + for (int i = 0; i < packages.length; i++) { + try { + return Class.forName(prefix + packages[i] + '.' + className + suffix, false, loader); + } + catch (ClassNotFoundException ignore) { + } + } + if (dimensions == 0) { + Class c = (Class) primitives.get(className); + if (c != null) { + return c; + } + } + else { + String transform = (String) transforms.get(className); + if (transform != null) { + try { + return Class.forName(brackets + transform, false, loader); + } + catch (ClassNotFoundException ignore) { + } + } + } + throw new ClassNotFoundException(save); + } + + public static Object newInstance(Class type) { + return newInstance(type, Constants.EMPTY_CLASS_ARRAY, null); + } + + public static Object newInstance(Class type, Class[] parameterTypes, Object[] args) { + return newInstance(getConstructor(type, parameterTypes), args); + } + + @SuppressWarnings("deprecation") // on JDK 9 + public static Object newInstance(final Constructor cstruct, final Object[] args) { + boolean flag = cstruct.isAccessible(); + try { + if (!flag) { + cstruct.setAccessible(true); + } + Object result = cstruct.newInstance(args); + return result; + } + catch (InstantiationException e) { + throw new CodeGenerationException(e); + } + catch (IllegalAccessException e) { + throw new CodeGenerationException(e); + } + catch (InvocationTargetException e) { + throw new CodeGenerationException(e.getTargetException()); + } + finally { + if (!flag) { + cstruct.setAccessible(flag); + } + } + } + + public static Constructor getConstructor(Class type, Class[] parameterTypes) { + try { + Constructor constructor = type.getDeclaredConstructor(parameterTypes); + if (System.getSecurityManager() != null) { + AccessController.doPrivileged((PrivilegedAction) () -> { + constructor.setAccessible(true); + return null; + }); + } + else { + constructor.setAccessible(true); + } + return constructor; + } + catch (NoSuchMethodException e) { + throw new CodeGenerationException(e); + } + } + + public static String[] getNames(Class[] classes) { + if (classes == null) + return null; + String[] names = new String[classes.length]; + for (int i = 0; i < names.length; i++) { + names[i] = classes[i].getName(); + } + return names; + } + + public static Class[] getClasses(Object[] objects) { + Class[] classes = new Class[objects.length]; + for (int i = 0; i < objects.length; i++) { + classes[i] = objects[i].getClass(); + } + return classes; + } + + public static Method findNewInstance(Class iface) { + Method m = findInterfaceMethod(iface); + if (!m.getName().equals("newInstance")) { + throw new IllegalArgumentException(iface + " missing newInstance method"); + } + return m; + } + + public static Method[] getPropertyMethods(PropertyDescriptor[] properties, boolean read, boolean write) { + Set methods = new HashSet(); + for (int i = 0; i < properties.length; i++) { + PropertyDescriptor pd = properties[i]; + if (read) { + methods.add(pd.getReadMethod()); + } + if (write) { + methods.add(pd.getWriteMethod()); + } + } + methods.remove(null); + return (Method[]) methods.toArray(new Method[methods.size()]); + } + + public static PropertyDescriptor[] getBeanProperties(Class type) { + return getPropertiesHelper(type, true, true); + } + + public static PropertyDescriptor[] getBeanGetters(Class type) { + return getPropertiesHelper(type, true, false); + } + + public static PropertyDescriptor[] getBeanSetters(Class type) { + return getPropertiesHelper(type, false, true); + } + + private static PropertyDescriptor[] getPropertiesHelper(Class type, boolean read, boolean write) { + try { + BeanInfo info = Introspector.getBeanInfo(type, Object.class); + PropertyDescriptor[] all = info.getPropertyDescriptors(); + if (read && write) { + return all; + } + List properties = new ArrayList(all.length); + for (int i = 0; i < all.length; i++) { + PropertyDescriptor pd = all[i]; + if ((read && pd.getReadMethod() != null) || + (write && pd.getWriteMethod() != null)) { + properties.add(pd); + } + } + return (PropertyDescriptor[]) properties.toArray(new PropertyDescriptor[properties.size()]); + } + catch (IntrospectionException e) { + throw new CodeGenerationException(e); + } + } + + public static Method findDeclaredMethod(final Class type, + final String methodName, final Class[] parameterTypes) + throws NoSuchMethodException { + + Class cl = type; + while (cl != null) { + try { + return cl.getDeclaredMethod(methodName, parameterTypes); + } + catch (NoSuchMethodException e) { + cl = cl.getSuperclass(); + } + } + throw new NoSuchMethodException(methodName); + } + + public static List addAllMethods(final Class type, final List list) { + if (type == Object.class) { + list.addAll(OBJECT_METHODS); + } + else + list.addAll(java.util.Arrays.asList(type.getDeclaredMethods())); + + Class superclass = type.getSuperclass(); + if (superclass != null) { + addAllMethods(superclass, list); + } + Class[] interfaces = type.getInterfaces(); + for (int i = 0; i < interfaces.length; i++) { + addAllMethods(interfaces[i], list); + } + + return list; + } + + public static List addAllInterfaces(Class type, List list) { + Class superclass = type.getSuperclass(); + if (superclass != null) { + list.addAll(Arrays.asList(type.getInterfaces())); + addAllInterfaces(superclass, list); + } + return list; + } + + + public static Method findInterfaceMethod(Class iface) { + if (!iface.isInterface()) { + throw new IllegalArgumentException(iface + " is not an interface"); + } + Method[] methods = iface.getDeclaredMethods(); + if (methods.length != 1) { + throw new IllegalArgumentException("expecting exactly 1 method in " + iface); + } + return methods[0]; + } + + // SPRING PATCH BEGIN + public static Class defineClass(String className, byte[] b, ClassLoader loader) throws Exception { + return defineClass(className, b, loader, null, null); + } + + public static Class defineClass(String className, byte[] b, ClassLoader loader, + ProtectionDomain protectionDomain) throws Exception { + + return defineClass(className, b, loader, protectionDomain, null); + } + + @SuppressWarnings("deprecation") // on JDK 9 + public static Class defineClass(String className, byte[] b, ClassLoader loader, + ProtectionDomain protectionDomain, Class contextClass) throws Exception { + + Class c = null; + + // Preferred option: JDK 9+ Lookup.defineClass API if ClassLoader matches + if (contextClass != null && contextClass.getClassLoader() == loader && + privateLookupInMethod != null && lookupDefineClassMethod != null) { + try { + MethodHandles.Lookup lookup = (MethodHandles.Lookup) + privateLookupInMethod.invoke(null, contextClass, MethodHandles.lookup()); + c = (Class) lookupDefineClassMethod.invoke(lookup, b); + } + catch (InvocationTargetException ex) { + Throwable target = ex.getTargetException(); + if (target.getClass() != LinkageError.class && target.getClass() != IllegalArgumentException.class) { + throw new CodeGenerationException(target); + } + // in case of plain LinkageError (class already defined) + // or IllegalArgumentException (class in different package): + // fall through to traditional ClassLoader.defineClass below + } + catch (Throwable ex) { + throw new CodeGenerationException(ex); + } + } + + // Classic option: protected ClassLoader.defineClass method + if (c == null && classLoaderDefineClassMethod != null) { + if (protectionDomain == null) { + protectionDomain = PROTECTION_DOMAIN; + } + Object[] args = new Object[]{className, b, 0, b.length, protectionDomain}; + try { + if (!classLoaderDefineClassMethod.isAccessible()) { + classLoaderDefineClassMethod.setAccessible(true); + } + c = (Class) classLoaderDefineClassMethod.invoke(loader, args); + } + catch (InvocationTargetException ex) { + throw new CodeGenerationException(ex.getTargetException()); + } + catch (Throwable ex) { + // Fall through if setAccessible fails with InaccessibleObjectException on JDK 9+ + // (on the module path and/or with a JVM bootstrapped with --illegal-access=deny) + if (!ex.getClass().getName().endsWith("InaccessibleObjectException")) { + throw new CodeGenerationException(ex); + } + } + } + + // Fallback option: JDK 9+ Lookup.defineClass API even if ClassLoader does not match + if (c == null && contextClass != null && contextClass.getClassLoader() != loader && + privateLookupInMethod != null && lookupDefineClassMethod != null) { + try { + MethodHandles.Lookup lookup = (MethodHandles.Lookup) + privateLookupInMethod.invoke(null, contextClass, MethodHandles.lookup()); + c = (Class) lookupDefineClassMethod.invoke(lookup, b); + } + catch (InvocationTargetException ex) { + throw new CodeGenerationException(ex.getTargetException()); + } + catch (Throwable ex) { + throw new CodeGenerationException(ex); + } + } + + // No defineClass variant available at all? + if (c == null) { + throw new CodeGenerationException(THROWABLE); + } + + // Force static initializers to run. + Class.forName(className, true, loader); + return c; + } + // SPRING PATCH END + + public static int findPackageProtected(Class[] classes) { + for (int i = 0; i < classes.length; i++) { + if (!Modifier.isPublic(classes[i].getModifiers())) { + return i; + } + } + return 0; + } + + public static MethodInfo getMethodInfo(final Member member, final int modifiers) { + final Signature sig = getSignature(member); + return new MethodInfo() { + private ClassInfo ci; + + public ClassInfo getClassInfo() { + if (ci == null) + ci = ReflectUtils.getClassInfo(member.getDeclaringClass()); + return ci; + } + + public int getModifiers() { + return modifiers; + } + + public Signature getSignature() { + return sig; + } + + public Type[] getExceptionTypes() { + return ReflectUtils.getExceptionTypes(member); + } + + public Attribute getAttribute() { + return null; + } + }; + } + + public static MethodInfo getMethodInfo(Member member) { + return getMethodInfo(member, member.getModifiers()); + } + + public static ClassInfo getClassInfo(final Class clazz) { + final Type type = Type.getType(clazz); + final Type sc = (clazz.getSuperclass() == null) ? null : Type.getType(clazz.getSuperclass()); + return new ClassInfo() { + public Type getType() { + return type; + } + public Type getSuperType() { + return sc; + } + public Type[] getInterfaces() { + return TypeUtils.getTypes(clazz.getInterfaces()); + } + public int getModifiers() { + return clazz.getModifiers(); + } + }; + } + + // used by MethodInterceptorGenerated generated code + public static Method[] findMethods(String[] namesAndDescriptors, Method[] methods) { + Map map = new HashMap(); + for (int i = 0; i < methods.length; i++) { + Method method = methods[i]; + map.put(method.getName() + Type.getMethodDescriptor(method), method); + } + Method[] result = new Method[namesAndDescriptors.length / 2]; + for (int i = 0; i < result.length; i++) { + result[i] = (Method) map.get(namesAndDescriptors[i * 2] + namesAndDescriptors[i * 2 + 1]); + if (result[i] == null) { + // TODO: error? + } + } + return result; + } + +} diff --git a/spring-core/src/main/java/org/springframework/cglib/core/SpringNamingPolicy.java b/spring-core/src/main/java/org/springframework/cglib/core/SpringNamingPolicy.java new file mode 100644 index 0000000..0266a2a --- /dev/null +++ b/spring-core/src/main/java/org/springframework/cglib/core/SpringNamingPolicy.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.cglib.core; + +/** + * Custom extension of CGLIB's {@link DefaultNamingPolicy}, modifying + * the tag in generated class names from "ByCGLIB" to "BySpringCGLIB". + * + *

    This is primarily designed to avoid clashes between a regular CGLIB + * version (used by some other library) and Spring's embedded variant, + * in case the same class happens to get proxied for different purposes. + * + * @author Juergen Hoeller + * @since 3.2.8 + */ +public class SpringNamingPolicy extends DefaultNamingPolicy { + + public static final SpringNamingPolicy INSTANCE = new SpringNamingPolicy(); + + @Override + protected String getTag() { + return "BySpringCGLIB"; + } + +} diff --git a/spring-core/src/main/java/org/springframework/cglib/core/package-info.java b/spring-core/src/main/java/org/springframework/cglib/core/package-info.java new file mode 100644 index 0000000..a2ed94f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/cglib/core/package-info.java @@ -0,0 +1,10 @@ +/** + * Spring's repackaging of the + * CGLIB core package + * (for internal use only). + * + *

    As this repackaging happens at the class file level, sources + * and javadocs are not available here... except for a few files + * that have been patched for Spring's purposes on JDK 9/10/11. + */ +package org.springframework.cglib.core; diff --git a/spring-core/src/main/java/org/springframework/cglib/package-info.java b/spring-core/src/main/java/org/springframework/cglib/package-info.java new file mode 100644 index 0000000..638ce59 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/cglib/package-info.java @@ -0,0 +1,13 @@ +/** + * Spring's repackaging of + * CGLIB 3.3 + * (with Spring-specific patches; for internal use only). + * + *

    This repackaging technique avoids any potential conflicts with + * dependencies on CGLIB at the application level or from third-party + * libraries and frameworks. + * + *

    As this repackaging happens at the class file level, sources + * and javadocs are not available here. + */ +package org.springframework.cglib; diff --git a/spring-core/src/main/java/org/springframework/cglib/proxy/Enhancer.java b/spring-core/src/main/java/org/springframework/cglib/proxy/Enhancer.java new file mode 100644 index 0000000..71c6a37 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/cglib/proxy/Enhancer.java @@ -0,0 +1,1438 @@ +/* + * Copyright 2002,2003,2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://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.springframework.cglib.proxy; + +import java.lang.ref.WeakReference; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.security.ProtectionDomain; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.asm.ClassVisitor; +import org.springframework.asm.Label; +import org.springframework.asm.Type; +import org.springframework.cglib.core.AbstractClassGenerator; +import org.springframework.cglib.core.ClassEmitter; +import org.springframework.cglib.core.CodeEmitter; +import org.springframework.cglib.core.CodeGenerationException; +import org.springframework.cglib.core.CollectionUtils; +import org.springframework.cglib.core.Constants; +import org.springframework.cglib.core.DuplicatesPredicate; +import org.springframework.cglib.core.EmitUtils; +import org.springframework.cglib.core.KeyFactory; +import org.springframework.cglib.core.Local; +import org.springframework.cglib.core.MethodInfo; +import org.springframework.cglib.core.MethodInfoTransformer; +import org.springframework.cglib.core.MethodWrapper; +import org.springframework.cglib.core.ObjectSwitchCallback; +import org.springframework.cglib.core.ProcessSwitchCallback; +import org.springframework.cglib.core.ReflectUtils; +import org.springframework.cglib.core.RejectModifierPredicate; +import org.springframework.cglib.core.Signature; +import org.springframework.cglib.core.Transformer; +import org.springframework.cglib.core.TypeUtils; +import org.springframework.cglib.core.VisibilityPredicate; +import org.springframework.cglib.core.WeakCacheKey; + +/** + * Generates dynamic subclasses to enable method interception. This + * class started as a substitute for the standard Dynamic Proxy support + * included with JDK 1.3, but one that allowed the proxies to extend a + * concrete base class, in addition to implementing interfaces. The dynamically + * generated subclasses override the non-final methods of the superclass and + * have hooks which callback to user-defined interceptor + * implementations. + *

    + * The original and most general callback type is the {@link MethodInterceptor}, which + * in AOP terms enables "around advice"--that is, you can invoke custom code both before + * and after the invocation of the "super" method. In addition you can modify the + * arguments before calling the super method, or not call it at all. + *

    + * Although MethodInterceptor is generic enough to meet any + * interception need, it is often overkill. For simplicity and performance, additional + * specialized callback types, such as {@link LazyLoader} are also available. + * Often a single callback will be used per enhanced class, but you can control + * which callback is used on a per-method basis with a {@link CallbackFilter}. + *

    + * The most common uses of this class are embodied in the static helper methods. For + * advanced needs, such as customizing the ClassLoader to use, you should create + * a new instance of Enhancer. Other classes within CGLIB follow a similar pattern. + *

    + * All enhanced objects implement the {@link Factory} interface, unless {@link #setUseFactory} is + * used to explicitly disable this feature. The Factory interface provides an API + * to change the callbacks of an existing object, as well as a faster and easier way to create + * new instances of the same type. + *

    + * For an almost drop-in replacement for + * java.lang.reflect.Proxy, see the {@link Proxy} class. + */ +@SuppressWarnings({"rawtypes", "unchecked"}) +public class Enhancer extends AbstractClassGenerator { + + private static final CallbackFilter ALL_ZERO = new CallbackFilter() { + public int accept(Method method) { + return 0; + } + }; + + private static final Source SOURCE = new Source(Enhancer.class.getName()); + + private static final EnhancerKey KEY_FACTORY = + (EnhancerKey) KeyFactory.create(EnhancerKey.class, KeyFactory.HASH_ASM_TYPE, null); + + private static final String BOUND_FIELD = "CGLIB$BOUND"; + + private static final String FACTORY_DATA_FIELD = "CGLIB$FACTORY_DATA"; + + private static final String THREAD_CALLBACKS_FIELD = "CGLIB$THREAD_CALLBACKS"; + + private static final String STATIC_CALLBACKS_FIELD = "CGLIB$STATIC_CALLBACKS"; + + private static final String SET_THREAD_CALLBACKS_NAME = "CGLIB$SET_THREAD_CALLBACKS"; + + private static final String SET_STATIC_CALLBACKS_NAME = "CGLIB$SET_STATIC_CALLBACKS"; + + private static final String CONSTRUCTED_FIELD = "CGLIB$CONSTRUCTED"; + + /** + * {@link org.springframework.cglib.core.AbstractClassGenerator.ClassLoaderData#generatedClasses} requires to keep cache key + * in a good shape (the keys should be up and running if the proxy class is alive), and one of the cache keys is + * {@link CallbackFilter}. That is why the generated class contains static field that keeps strong reference to + * the {@link #filter}. + *

    This dance achieves two goals: ensures generated class is reusable and available through generatedClasses + * cache, and it enables to unload classloader and the related {@link CallbackFilter} in case user does not need + * that

    + */ + private static final String CALLBACK_FILTER_FIELD = "CGLIB$CALLBACK_FILTER"; + + private static final Type OBJECT_TYPE = + TypeUtils.parseType("Object"); + + private static final Type FACTORY = + TypeUtils.parseType("org.springframework.cglib.proxy.Factory"); + + private static final Type ILLEGAL_STATE_EXCEPTION = + TypeUtils.parseType("IllegalStateException"); + + private static final Type ILLEGAL_ARGUMENT_EXCEPTION = + TypeUtils.parseType("IllegalArgumentException"); + + private static final Type THREAD_LOCAL = + TypeUtils.parseType("ThreadLocal"); + + private static final Type CALLBACK = + TypeUtils.parseType("org.springframework.cglib.proxy.Callback"); + + private static final Type CALLBACK_ARRAY = + Type.getType(Callback[].class); + + private static final Signature CSTRUCT_NULL = + TypeUtils.parseConstructor(""); + + private static final Signature SET_THREAD_CALLBACKS = + new Signature(SET_THREAD_CALLBACKS_NAME, Type.VOID_TYPE, new Type[]{CALLBACK_ARRAY}); + + private static final Signature SET_STATIC_CALLBACKS = + new Signature(SET_STATIC_CALLBACKS_NAME, Type.VOID_TYPE, new Type[]{CALLBACK_ARRAY}); + + private static final Signature NEW_INSTANCE = + new Signature("newInstance", Constants.TYPE_OBJECT, new Type[]{CALLBACK_ARRAY}); + + private static final Signature MULTIARG_NEW_INSTANCE = + new Signature("newInstance", Constants.TYPE_OBJECT, new Type[]{ + Constants.TYPE_CLASS_ARRAY, + Constants.TYPE_OBJECT_ARRAY, + CALLBACK_ARRAY, + }); + + private static final Signature SINGLE_NEW_INSTANCE = + new Signature("newInstance", Constants.TYPE_OBJECT, new Type[]{CALLBACK}); + + private static final Signature SET_CALLBACK = + new Signature("setCallback", Type.VOID_TYPE, new Type[]{Type.INT_TYPE, CALLBACK}); + + private static final Signature GET_CALLBACK = + new Signature("getCallback", CALLBACK, new Type[]{Type.INT_TYPE}); + + private static final Signature SET_CALLBACKS = + new Signature("setCallbacks", Type.VOID_TYPE, new Type[]{CALLBACK_ARRAY}); + + private static final Signature GET_CALLBACKS = + new Signature("getCallbacks", CALLBACK_ARRAY, new Type[0]); + + private static final Signature THREAD_LOCAL_GET = + TypeUtils.parseSignature("Object get()"); + + private static final Signature THREAD_LOCAL_SET = + TypeUtils.parseSignature("void set(Object)"); + + private static final Signature BIND_CALLBACKS = + TypeUtils.parseSignature("void CGLIB$BIND_CALLBACKS(Object)"); + + private EnhancerFactoryData currentData; + + private Object currentKey; + + + /** + * Internal interface, only public due to ClassLoader issues. + */ + public interface EnhancerKey { + + public Object newInstance(String type, + String[] interfaces, + WeakCacheKey filter, + Type[] callbackTypes, + boolean useFactory, + boolean interceptDuringConstruction, + Long serialVersionUID); + } + + + private Class[] interfaces; + + private CallbackFilter filter; + + private Callback[] callbacks; + + private Type[] callbackTypes; + + private boolean validateCallbackTypes; + + private boolean classOnly; + + private Class superclass; + + private Class[] argumentTypes; + + private Object[] arguments; + + private boolean useFactory = true; + + private Long serialVersionUID; + + private boolean interceptDuringConstruction = true; + + /** + * Create a new Enhancer. A new Enhancer + * object should be used for each generated object, and should not + * be shared across threads. To create additional instances of a + * generated class, use the Factory interface. + * @see Factory + */ + public Enhancer() { + super(SOURCE); + } + + /** + * Set the class which the generated class will extend. As a convenience, + * if the supplied superclass is actually an interface, setInterfaces + * will be called with the appropriate argument instead. + * A non-interface argument must not be declared as final, and must have an + * accessible constructor. + * @param superclass class to extend or interface to implement + * @see #setInterfaces(Class[]) + */ + public void setSuperclass(Class superclass) { + if (superclass != null && superclass.isInterface()) { + setInterfaces(new Class[]{superclass}); + // SPRING PATCH BEGIN + setContextClass(superclass); + // SPRING PATCH END + } + else if (superclass != null && superclass.equals(Object.class)) { + // affects choice of ClassLoader + this.superclass = null; + } + else { + this.superclass = superclass; + // SPRING PATCH BEGIN + setContextClass(superclass); + // SPRING PATCH END + } + } + + /** + * Set the interfaces to implement. The Factory interface will + * always be implemented regardless of what is specified here. + * @param interfaces array of interfaces to implement, or null + * @see Factory + */ + public void setInterfaces(Class[] interfaces) { + this.interfaces = interfaces; + } + + /** + * Set the {@link CallbackFilter} used to map the generated class' methods + * to a particular callback index. + * New object instances will always use the same mapping, but may use different + * actual callback objects. + * @param filter the callback filter to use when generating a new class + * @see #setCallbacks + */ + public void setCallbackFilter(CallbackFilter filter) { + this.filter = filter; + } + + + /** + * Set the single {@link Callback} to use. + * Ignored if you use {@link #createClass}. + * @param callback the callback to use for all methods + * @see #setCallbacks + */ + public void setCallback(final Callback callback) { + setCallbacks(new Callback[]{callback}); + } + + /** + * Set the array of callbacks to use. + * Ignored if you use {@link #createClass}. + * You must use a {@link CallbackFilter} to specify the index into this + * array for each method in the proxied class. + * @param callbacks the callback array + * @see #setCallbackFilter + * @see #setCallback + */ + public void setCallbacks(Callback[] callbacks) { + if (callbacks != null && callbacks.length == 0) { + throw new IllegalArgumentException("Array cannot be empty"); + } + this.callbacks = callbacks; + } + + /** + * Set whether the enhanced object instances should implement + * the {@link Factory} interface. + * This was added for tools that need for proxies to be more + * indistinguishable from their targets. Also, in some cases it may + * be necessary to disable the Factory interface to + * prevent code from changing the underlying callbacks. + * @param useFactory whether to implement Factory; default is true + */ + public void setUseFactory(boolean useFactory) { + this.useFactory = useFactory; + } + + /** + * Set whether methods called from within the proxy's constructer + * will be intercepted. The default value is true. Unintercepted methods + * will call the method of the proxy's base class, if it exists. + * @param interceptDuringConstruction whether to intercept methods called from the constructor + */ + public void setInterceptDuringConstruction(boolean interceptDuringConstruction) { + this.interceptDuringConstruction = interceptDuringConstruction; + } + + /** + * Set the single type of {@link Callback} to use. + * This may be used instead of {@link #setCallback} when calling + * {@link #createClass}, since it may not be possible to have + * an array of actual callback instances. + * @param callbackType the type of callback to use for all methods + * @see #setCallbackTypes + */ + public void setCallbackType(Class callbackType) { + setCallbackTypes(new Class[]{callbackType}); + } + + /** + * Set the array of callback types to use. + * This may be used instead of {@link #setCallbacks} when calling + * {@link #createClass}, since it may not be possible to have + * an array of actual callback instances. + * You must use a {@link CallbackFilter} to specify the index into this + * array for each method in the proxied class. + * @param callbackTypes the array of callback types + */ + public void setCallbackTypes(Class[] callbackTypes) { + if (callbackTypes != null && callbackTypes.length == 0) { + throw new IllegalArgumentException("Array cannot be empty"); + } + this.callbackTypes = CallbackInfo.determineTypes(callbackTypes); + } + + /** + * Generate a new class if necessary and uses the specified + * callbacks (if any) to create a new object instance. + * Uses the no-arg constructor of the superclass. + * @return a new instance + */ + public Object create() { + classOnly = false; + argumentTypes = null; + return createHelper(); + } + + /** + * Generate a new class if necessary and uses the specified + * callbacks (if any) to create a new object instance. + * Uses the constructor of the superclass matching the argumentTypes + * parameter, with the given arguments. + * @param argumentTypes constructor signature + * @param arguments compatible wrapped arguments to pass to constructor + * @return a new instance + */ + public Object create(Class[] argumentTypes, Object[] arguments) { + classOnly = false; + if (argumentTypes == null || arguments == null || argumentTypes.length != arguments.length) { + throw new IllegalArgumentException("Arguments must be non-null and of equal length"); + } + this.argumentTypes = argumentTypes; + this.arguments = arguments; + return createHelper(); + } + + /** + * Generate a new class if necessary and return it without creating a new instance. + * This ignores any callbacks that have been set. + * To create a new instance you will have to use reflection, and methods + * called during the constructor will not be intercepted. To avoid this problem, + * use the multi-arg create method. + * @see #create(Class[], Object[]) + */ + public Class createClass() { + classOnly = true; + return (Class) createHelper(); + } + + /** + * Insert a static serialVersionUID field into the generated class. + * @param sUID the field value, or null to avoid generating field. + */ + public void setSerialVersionUID(Long sUID) { + serialVersionUID = sUID; + } + + private void preValidate() { + if (callbackTypes == null) { + callbackTypes = CallbackInfo.determineTypes(callbacks, false); + validateCallbackTypes = true; + } + if (filter == null) { + if (callbackTypes.length > 1) { + throw new IllegalStateException("Multiple callback types possible but no filter specified"); + } + filter = ALL_ZERO; + } + } + + private void validate() { + if (classOnly ^ (callbacks == null)) { + if (classOnly) { + throw new IllegalStateException("createClass does not accept callbacks"); + } + else { + throw new IllegalStateException("Callbacks are required"); + } + } + if (classOnly && (callbackTypes == null)) { + throw new IllegalStateException("Callback types are required"); + } + if (validateCallbackTypes) { + callbackTypes = null; + } + if (callbacks != null && callbackTypes != null) { + if (callbacks.length != callbackTypes.length) { + throw new IllegalStateException("Lengths of callback and callback types array must be the same"); + } + Type[] check = CallbackInfo.determineTypes(callbacks); + for (int i = 0; i < check.length; i++) { + if (!check[i].equals(callbackTypes[i])) { + throw new IllegalStateException("Callback " + check[i] + " is not assignable to " + callbackTypes[i]); + } + } + } + else if (callbacks != null) { + callbackTypes = CallbackInfo.determineTypes(callbacks); + } + if (interfaces != null) { + for (int i = 0; i < interfaces.length; i++) { + if (interfaces[i] == null) { + throw new IllegalStateException("Interfaces cannot be null"); + } + if (!interfaces[i].isInterface()) { + throw new IllegalStateException(interfaces[i] + " is not an interface"); + } + } + } + } + + /** + * The idea of the class is to cache relevant java.lang.reflect instances so + * proxy-class can be instantiated faster that when using {@link ReflectUtils#newInstance(Class, Class[], Object[])} + * and {@link Enhancer#setThreadCallbacks(Class, Callback[])} + */ + static class EnhancerFactoryData { + + public final Class generatedClass; + + private final Method setThreadCallbacks; + + private final Class[] primaryConstructorArgTypes; + + private final Constructor primaryConstructor; + + public EnhancerFactoryData(Class generatedClass, Class[] primaryConstructorArgTypes, boolean classOnly) { + this.generatedClass = generatedClass; + try { + setThreadCallbacks = getCallbacksSetter(generatedClass, SET_THREAD_CALLBACKS_NAME); + if (classOnly) { + this.primaryConstructorArgTypes = null; + this.primaryConstructor = null; + } + else { + this.primaryConstructorArgTypes = primaryConstructorArgTypes; + this.primaryConstructor = ReflectUtils.getConstructor(generatedClass, primaryConstructorArgTypes); + } + } + catch (NoSuchMethodException e) { + throw new CodeGenerationException(e); + } + } + + /** + * Creates proxy instance for given argument types, and assigns the callbacks. + * Ideally, for each proxy class, just one set of argument types should be used, + * otherwise it would have to spend time on constructor lookup. + * Technically, it is a re-implementation of {@link Enhancer#createUsingReflection(Class)}, + * with "cache {@link #setThreadCallbacks} and {@link #primaryConstructor}" + * @param argumentTypes constructor argument types + * @param arguments constructor arguments + * @param callbacks callbacks to set for the new instance + * @return newly created proxy + * @see #createUsingReflection(Class) + */ + public Object newInstance(Class[] argumentTypes, Object[] arguments, Callback[] callbacks) { + setThreadCallbacks(callbacks); + try { + // Explicit reference equality is added here just in case Arrays.equals does not have one + if (primaryConstructorArgTypes == argumentTypes || + Arrays.equals(primaryConstructorArgTypes, argumentTypes)) { + // If we have relevant Constructor instance at hand, just call it + // This skips "get constructors" machinery + return ReflectUtils.newInstance(primaryConstructor, arguments); + } + // Take a slow path if observing unexpected argument types + return ReflectUtils.newInstance(generatedClass, argumentTypes, arguments); + } + finally { + // clear thread callbacks to allow them to be gc'd + setThreadCallbacks(null); + } + + } + + private void setThreadCallbacks(Callback[] callbacks) { + try { + setThreadCallbacks.invoke(generatedClass, (Object) callbacks); + } + catch (IllegalAccessException e) { + throw new CodeGenerationException(e); + } + catch (InvocationTargetException e) { + throw new CodeGenerationException(e.getTargetException()); + } + } + } + + private Object createHelper() { + preValidate(); + Object key = KEY_FACTORY.newInstance((superclass != null) ? superclass.getName() : null, + ReflectUtils.getNames(interfaces), + filter == ALL_ZERO ? null : new WeakCacheKey(filter), + callbackTypes, + useFactory, + interceptDuringConstruction, + serialVersionUID); + this.currentKey = key; + Object result = super.create(key); + return result; + } + + @Override + protected Class generate(ClassLoaderData data) { + validate(); + if (superclass != null) { + setNamePrefix(superclass.getName()); + } + else if (interfaces != null) { + setNamePrefix(interfaces[ReflectUtils.findPackageProtected(interfaces)].getName()); + } + return super.generate(data); + } + + protected ClassLoader getDefaultClassLoader() { + if (superclass != null) { + return superclass.getClassLoader(); + } + else if (interfaces != null) { + return interfaces[0].getClassLoader(); + } + else { + return null; + } + } + + protected ProtectionDomain getProtectionDomain() { + if (superclass != null) { + return ReflectUtils.getProtectionDomain(superclass); + } + else if (interfaces != null) { + return ReflectUtils.getProtectionDomain(interfaces[0]); + } + else { + return null; + } + } + + private Signature rename(Signature sig, int index) { + return new Signature("CGLIB$" + sig.getName() + "$" + index, + sig.getDescriptor()); + } + + /** + * Finds all of the methods that will be extended by an + * Enhancer-generated class using the specified superclass and + * interfaces. This can be useful in building a list of Callback + * objects. The methods are added to the end of the given list. Due + * to the subclassing nature of the classes generated by Enhancer, + * the methods are guaranteed to be non-static, non-final, and + * non-private. Each method signature will only occur once, even if + * it occurs in multiple classes. + * @param superclass the class that will be extended, or null + * @param interfaces the list of interfaces that will be implemented, or null + * @param methods the list into which to copy the applicable methods + */ + public static void getMethods(Class superclass, Class[] interfaces, List methods) { + getMethods(superclass, interfaces, methods, null, null); + } + + private static void getMethods(Class superclass, Class[] interfaces, List methods, List interfaceMethods, Set forcePublic) { + ReflectUtils.addAllMethods(superclass, methods); + List target = (interfaceMethods != null) ? interfaceMethods : methods; + if (interfaces != null) { + for (int i = 0; i < interfaces.length; i++) { + if (interfaces[i] != Factory.class) { + ReflectUtils.addAllMethods(interfaces[i], target); + } + } + } + if (interfaceMethods != null) { + if (forcePublic != null) { + forcePublic.addAll(MethodWrapper.createSet(interfaceMethods)); + } + methods.addAll(interfaceMethods); + } + CollectionUtils.filter(methods, new RejectModifierPredicate(Constants.ACC_STATIC)); + CollectionUtils.filter(methods, new VisibilityPredicate(superclass, true)); + CollectionUtils.filter(methods, new DuplicatesPredicate()); + CollectionUtils.filter(methods, new RejectModifierPredicate(Constants.ACC_FINAL)); + } + + public void generateClass(ClassVisitor v) throws Exception { + Class sc = (superclass == null) ? Object.class : superclass; + + if (TypeUtils.isFinal(sc.getModifiers())) + throw new IllegalArgumentException("Cannot subclass final class " + sc.getName()); + List constructors = new ArrayList(Arrays.asList(sc.getDeclaredConstructors())); + filterConstructors(sc, constructors); + + // Order is very important: must add superclass, then + // its superclass chain, then each interface and + // its superinterfaces. + List actualMethods = new ArrayList(); + List interfaceMethods = new ArrayList(); + final Set forcePublic = new HashSet(); + getMethods(sc, interfaces, actualMethods, interfaceMethods, forcePublic); + + List methods = CollectionUtils.transform(actualMethods, new Transformer() { + public Object transform(Object value) { + Method method = (Method) value; + int modifiers = Constants.ACC_FINAL + | (method.getModifiers() + & ~Constants.ACC_ABSTRACT + & ~Constants.ACC_NATIVE + & ~Constants.ACC_SYNCHRONIZED); + if (forcePublic.contains(MethodWrapper.create(method))) { + modifiers = (modifiers & ~Constants.ACC_PROTECTED) | Constants.ACC_PUBLIC; + } + return ReflectUtils.getMethodInfo(method, modifiers); + } + }); + + ClassEmitter e = new ClassEmitter(v); + if (currentData == null) { + e.begin_class(Constants.V1_8, + Constants.ACC_PUBLIC, + getClassName(), + Type.getType(sc), + (useFactory ? + TypeUtils.add(TypeUtils.getTypes(interfaces), FACTORY) : + TypeUtils.getTypes(interfaces)), + Constants.SOURCE_FILE); + } + else { + e.begin_class(Constants.V1_8, + Constants.ACC_PUBLIC, + getClassName(), + null, + new Type[]{FACTORY}, + Constants.SOURCE_FILE); + } + List constructorInfo = CollectionUtils.transform(constructors, MethodInfoTransformer.getInstance()); + + e.declare_field(Constants.ACC_PRIVATE, BOUND_FIELD, Type.BOOLEAN_TYPE, null); + e.declare_field(Constants.ACC_PUBLIC | Constants.ACC_STATIC, FACTORY_DATA_FIELD, OBJECT_TYPE, null); + if (!interceptDuringConstruction) { + e.declare_field(Constants.ACC_PRIVATE, CONSTRUCTED_FIELD, Type.BOOLEAN_TYPE, null); + } + e.declare_field(Constants.PRIVATE_FINAL_STATIC, THREAD_CALLBACKS_FIELD, THREAD_LOCAL, null); + e.declare_field(Constants.PRIVATE_FINAL_STATIC, STATIC_CALLBACKS_FIELD, CALLBACK_ARRAY, null); + if (serialVersionUID != null) { + e.declare_field(Constants.PRIVATE_FINAL_STATIC, Constants.SUID_FIELD_NAME, Type.LONG_TYPE, serialVersionUID); + } + + for (int i = 0; i < callbackTypes.length; i++) { + e.declare_field(Constants.ACC_PRIVATE, getCallbackField(i), callbackTypes[i], null); + } + // This is declared private to avoid "public field" pollution + e.declare_field(Constants.ACC_PRIVATE | Constants.ACC_STATIC, CALLBACK_FILTER_FIELD, OBJECT_TYPE, null); + + if (currentData == null) { + emitMethods(e, methods, actualMethods); + emitConstructors(e, constructorInfo); + } + else { + emitDefaultConstructor(e); + } + emitSetThreadCallbacks(e); + emitSetStaticCallbacks(e); + emitBindCallbacks(e); + + if (useFactory || currentData != null) { + int[] keys = getCallbackKeys(); + emitNewInstanceCallbacks(e); + emitNewInstanceCallback(e); + emitNewInstanceMultiarg(e, constructorInfo); + emitGetCallback(e, keys); + emitSetCallback(e, keys); + emitGetCallbacks(e); + emitSetCallbacks(e); + } + + e.end_class(); + } + + /** + * Filter the list of constructors from the superclass. The + * constructors which remain will be included in the generated + * class. The default implementation is to filter out all private + * constructors, but subclasses may extend Enhancer to override this + * behavior. + * @param sc the superclass + * @param constructors the list of all declared constructors from the superclass + * @throws IllegalArgumentException if there are no non-private constructors + */ + protected void filterConstructors(Class sc, List constructors) { + CollectionUtils.filter(constructors, new VisibilityPredicate(sc, true)); + if (constructors.size() == 0) + throw new IllegalArgumentException("No visible constructors in " + sc); + } + + /** + * This method should not be called in regular flow. + * Technically speaking {@link #wrapCachedClass(Class)} uses {@link Enhancer.EnhancerFactoryData} as a cache value, + * and the latter enables faster instantiation than plain old reflection lookup and invoke. + * This method is left intact for backward compatibility reasons: just in case it was ever used. + * @param type class to instantiate + * @return newly created proxy instance + * @throws Exception if something goes wrong + */ + protected Object firstInstance(Class type) throws Exception { + if (classOnly) { + return type; + } + else { + return createUsingReflection(type); + } + } + + protected Object nextInstance(Object instance) { + EnhancerFactoryData data = (EnhancerFactoryData) instance; + + if (classOnly) { + return data.generatedClass; + } + + Class[] argumentTypes = this.argumentTypes; + Object[] arguments = this.arguments; + if (argumentTypes == null) { + argumentTypes = Constants.EMPTY_CLASS_ARRAY; + arguments = null; + } + return data.newInstance(argumentTypes, arguments, callbacks); + } + + @Override + protected Object wrapCachedClass(Class klass) { + Class[] argumentTypes = this.argumentTypes; + if (argumentTypes == null) { + argumentTypes = Constants.EMPTY_CLASS_ARRAY; + } + EnhancerFactoryData factoryData = new EnhancerFactoryData(klass, argumentTypes, classOnly); + Field factoryDataField = null; + try { + // The subsequent dance is performed just once for each class, + // so it does not matter much how fast it goes + factoryDataField = klass.getField(FACTORY_DATA_FIELD); + factoryDataField.set(null, factoryData); + Field callbackFilterField = klass.getDeclaredField(CALLBACK_FILTER_FIELD); + callbackFilterField.setAccessible(true); + callbackFilterField.set(null, this.filter); + } + catch (NoSuchFieldException e) { + throw new CodeGenerationException(e); + } + catch (IllegalAccessException e) { + throw new CodeGenerationException(e); + } + return new WeakReference(factoryData); + } + + @Override + protected Object unwrapCachedValue(Object cached) { + if (currentKey instanceof EnhancerKey) { + EnhancerFactoryData data = ((WeakReference) cached).get(); + return data; + } + return super.unwrapCachedValue(cached); + } + + /** + * Call this method to register the {@link Callback} array to use before + * creating a new instance of the generated class via reflection. If you are using + * an instance of Enhancer or the {@link Factory} interface to create + * new instances, this method is unnecessary. Its primary use is for when you want to + * cache and reuse a generated class yourself, and the generated class does + * not implement the {@link Factory} interface. + *

    + * Note that this method only registers the callbacks on the current thread. + * If you want to register callbacks for instances created by multiple threads, + * use {@link #registerStaticCallbacks}. + *

    + * The registered callbacks are overwritten and subsequently cleared + * when calling any of the create methods (such as + * {@link #create}), or any {@link Factory} newInstance method. + * Otherwise they are not cleared, and you should be careful to set them + * back to null after creating new instances via reflection if + * memory leakage is a concern. + * @param generatedClass a class previously created by {@link Enhancer} + * @param callbacks the array of callbacks to use when instances of the generated + * class are created + * @see #setUseFactory + */ + public static void registerCallbacks(Class generatedClass, Callback[] callbacks) { + setThreadCallbacks(generatedClass, callbacks); + } + + /** + * Similar to {@link #registerCallbacks}, but suitable for use + * when multiple threads will be creating instances of the generated class. + * The thread-level callbacks will always override the static callbacks. + * Static callbacks are never cleared. + * @param generatedClass a class previously created by {@link Enhancer} + * @param callbacks the array of callbacks to use when instances of the generated + * class are created + */ + public static void registerStaticCallbacks(Class generatedClass, Callback[] callbacks) { + setCallbacksHelper(generatedClass, callbacks, SET_STATIC_CALLBACKS_NAME); + } + + /** + * Determine if a class was generated using Enhancer. + * @param type any class + * @return whether the class was generated using Enhancer + */ + public static boolean isEnhanced(Class type) { + try { + getCallbacksSetter(type, SET_THREAD_CALLBACKS_NAME); + return true; + } + catch (NoSuchMethodException e) { + return false; + } + } + + private static void setThreadCallbacks(Class type, Callback[] callbacks) { + setCallbacksHelper(type, callbacks, SET_THREAD_CALLBACKS_NAME); + } + + private static void setCallbacksHelper(Class type, Callback[] callbacks, String methodName) { + // TODO: optimize + try { + Method setter = getCallbacksSetter(type, methodName); + setter.invoke(null, new Object[]{callbacks}); + } + catch (NoSuchMethodException e) { + throw new IllegalArgumentException(type + " is not an enhanced class"); + } + catch (IllegalAccessException e) { + throw new CodeGenerationException(e); + } + catch (InvocationTargetException e) { + throw new CodeGenerationException(e); + } + } + + private static Method getCallbacksSetter(Class type, String methodName) throws NoSuchMethodException { + return type.getDeclaredMethod(methodName, new Class[]{Callback[].class}); + } + + /** + * Instantiates a proxy instance and assigns callback values. + * Implementation detail: java.lang.reflect instances are not cached, so this method should not + * be used on a hot path. + * This method is used when {@link #setUseCache(boolean)} is set to {@code false}. + * @param type class to instantiate + * @return newly created instance + */ + private Object createUsingReflection(Class type) { + setThreadCallbacks(type, callbacks); + try { + + if (argumentTypes != null) { + + return ReflectUtils.newInstance(type, argumentTypes, arguments); + + } + else { + + return ReflectUtils.newInstance(type); + + } + } + finally { + // clear thread callbacks to allow them to be gc'd + setThreadCallbacks(type, null); + } + } + + /** + * Helper method to create an intercepted object. + * For finer control over the generated instance, use a new instance of Enhancer + * instead of this static method. + * @param type class to extend or interface to implement + * @param callback the callback to use for all methods + */ + public static Object create(Class type, Callback callback) { + Enhancer e = new Enhancer(); + e.setSuperclass(type); + e.setCallback(callback); + return e.create(); + } + + /** + * Helper method to create an intercepted object. + * For finer control over the generated instance, use a new instance of Enhancer + * instead of this static method. + * @param superclass class to extend or interface to implement + * @param interfaces array of interfaces to implement, or null + * @param callback the callback to use for all methods + */ + public static Object create(Class superclass, Class interfaces[], Callback callback) { + Enhancer e = new Enhancer(); + e.setSuperclass(superclass); + e.setInterfaces(interfaces); + e.setCallback(callback); + return e.create(); + } + + /** + * Helper method to create an intercepted object. + * For finer control over the generated instance, use a new instance of Enhancer + * instead of this static method. + * @param superclass class to extend or interface to implement + * @param interfaces array of interfaces to implement, or null + * @param filter the callback filter to use when generating a new class + * @param callbacks callback implementations to use for the enhanced object + */ + public static Object create(Class superclass, Class[] interfaces, CallbackFilter filter, Callback[] callbacks) { + Enhancer e = new Enhancer(); + e.setSuperclass(superclass); + e.setInterfaces(interfaces); + e.setCallbackFilter(filter); + e.setCallbacks(callbacks); + return e.create(); + } + + private void emitDefaultConstructor(ClassEmitter ce) { + Constructor declaredConstructor; + try { + declaredConstructor = Object.class.getDeclaredConstructor(); + } + catch (NoSuchMethodException e) { + throw new IllegalStateException("Object should have default constructor ", e); + } + MethodInfo constructor = (MethodInfo) MethodInfoTransformer.getInstance().transform(declaredConstructor); + CodeEmitter e = EmitUtils.begin_method(ce, constructor, Constants.ACC_PUBLIC); + e.load_this(); + e.dup(); + Signature sig = constructor.getSignature(); + e.super_invoke_constructor(sig); + e.return_value(); + e.end_method(); + } + + private void emitConstructors(ClassEmitter ce, List constructors) { + boolean seenNull = false; + for (Iterator it = constructors.iterator(); it.hasNext(); ) { + MethodInfo constructor = (MethodInfo) it.next(); + if (currentData != null && !"()V".equals(constructor.getSignature().getDescriptor())) { + continue; + } + CodeEmitter e = EmitUtils.begin_method(ce, constructor, Constants.ACC_PUBLIC); + e.load_this(); + e.dup(); + e.load_args(); + Signature sig = constructor.getSignature(); + seenNull = seenNull || sig.getDescriptor().equals("()V"); + e.super_invoke_constructor(sig); + if (currentData == null) { + e.invoke_static_this(BIND_CALLBACKS); + if (!interceptDuringConstruction) { + e.load_this(); + e.push(1); + e.putfield(CONSTRUCTED_FIELD); + } + } + e.return_value(); + e.end_method(); + } + if (!classOnly && !seenNull && arguments == null) + throw new IllegalArgumentException("Superclass has no null constructors but no arguments were given"); + } + + private int[] getCallbackKeys() { + int[] keys = new int[callbackTypes.length]; + for (int i = 0; i < callbackTypes.length; i++) { + keys[i] = i; + } + return keys; + } + + private void emitGetCallback(ClassEmitter ce, int[] keys) { + final CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC, GET_CALLBACK, null); + e.load_this(); + e.invoke_static_this(BIND_CALLBACKS); + e.load_this(); + e.load_arg(0); + e.process_switch(keys, new ProcessSwitchCallback() { + public void processCase(int key, Label end) { + e.getfield(getCallbackField(key)); + e.goTo(end); + } + + public void processDefault() { + e.pop(); // stack height + e.aconst_null(); + } + }); + e.return_value(); + e.end_method(); + } + + private void emitSetCallback(ClassEmitter ce, int[] keys) { + final CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC, SET_CALLBACK, null); + e.load_arg(0); + e.process_switch(keys, new ProcessSwitchCallback() { + public void processCase(int key, Label end) { + e.load_this(); + e.load_arg(1); + e.checkcast(callbackTypes[key]); + e.putfield(getCallbackField(key)); + e.goTo(end); + } + + public void processDefault() { + // TODO: error? + } + }); + e.return_value(); + e.end_method(); + } + + private void emitSetCallbacks(ClassEmitter ce) { + CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC, SET_CALLBACKS, null); + e.load_this(); + e.load_arg(0); + for (int i = 0; i < callbackTypes.length; i++) { + e.dup2(); + e.aaload(i); + e.checkcast(callbackTypes[i]); + e.putfield(getCallbackField(i)); + } + e.return_value(); + e.end_method(); + } + + private void emitGetCallbacks(ClassEmitter ce) { + CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC, GET_CALLBACKS, null); + e.load_this(); + e.invoke_static_this(BIND_CALLBACKS); + e.load_this(); + e.push(callbackTypes.length); + e.newarray(CALLBACK); + for (int i = 0; i < callbackTypes.length; i++) { + e.dup(); + e.push(i); + e.load_this(); + e.getfield(getCallbackField(i)); + e.aastore(); + } + e.return_value(); + e.end_method(); + } + + private void emitNewInstanceCallbacks(ClassEmitter ce) { + CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC, NEW_INSTANCE, null); + Type thisType = getThisType(e); + e.load_arg(0); + e.invoke_static(thisType, SET_THREAD_CALLBACKS, false); + emitCommonNewInstance(e); + } + + private Type getThisType(CodeEmitter e) { + if (currentData == null) { + return e.getClassEmitter().getClassType(); + } + else { + return Type.getType(currentData.generatedClass); + } + } + + private void emitCommonNewInstance(CodeEmitter e) { + Type thisType = getThisType(e); + e.new_instance(thisType); + e.dup(); + e.invoke_constructor(thisType); + e.aconst_null(); + e.invoke_static(thisType, SET_THREAD_CALLBACKS, false); + e.return_value(); + e.end_method(); + } + + private void emitNewInstanceCallback(ClassEmitter ce) { + CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC, SINGLE_NEW_INSTANCE, null); + switch (callbackTypes.length) { + case 0: + // TODO: make sure Callback is null + break; + case 1: + // for now just make a new array; TODO: optimize + e.push(1); + e.newarray(CALLBACK); + e.dup(); + e.push(0); + e.load_arg(0); + e.aastore(); + e.invoke_static(getThisType(e), SET_THREAD_CALLBACKS, false); + break; + default: + e.throw_exception(ILLEGAL_STATE_EXCEPTION, "More than one callback object required"); + } + emitCommonNewInstance(e); + } + + private void emitNewInstanceMultiarg(ClassEmitter ce, List constructors) { + final CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC, MULTIARG_NEW_INSTANCE, null); + final Type thisType = getThisType(e); + e.load_arg(2); + e.invoke_static(thisType, SET_THREAD_CALLBACKS, false); + e.new_instance(thisType); + e.dup(); + e.load_arg(0); + EmitUtils.constructor_switch(e, constructors, new ObjectSwitchCallback() { + public void processCase(Object key, Label end) { + MethodInfo constructor = (MethodInfo) key; + Type types[] = constructor.getSignature().getArgumentTypes(); + for (int i = 0; i < types.length; i++) { + e.load_arg(1); + e.push(i); + e.aaload(); + e.unbox(types[i]); + } + e.invoke_constructor(thisType, constructor.getSignature()); + e.goTo(end); + } + + public void processDefault() { + e.throw_exception(ILLEGAL_ARGUMENT_EXCEPTION, "Constructor not found"); + } + }); + e.aconst_null(); + e.invoke_static(thisType, SET_THREAD_CALLBACKS, false); + e.return_value(); + e.end_method(); + } + + private void emitMethods(final ClassEmitter ce, List methods, List actualMethods) { + CallbackGenerator[] generators = CallbackInfo.getGenerators(callbackTypes); + + Map groups = new HashMap(); + final Map indexes = new HashMap(); + final Map originalModifiers = new HashMap(); + final Map positions = CollectionUtils.getIndexMap(methods); + final Map declToBridge = new HashMap(); + + Iterator it1 = methods.iterator(); + Iterator it2 = (actualMethods != null) ? actualMethods.iterator() : null; + + while (it1.hasNext()) { + MethodInfo method = (MethodInfo) it1.next(); + Method actualMethod = (it2 != null) ? (Method) it2.next() : null; + int index = filter.accept(actualMethod); + if (index >= callbackTypes.length) { + throw new IllegalArgumentException("Callback filter returned an index that is too large: " + index); + } + originalModifiers.put(method, (actualMethod != null ? actualMethod.getModifiers() : method.getModifiers())); + indexes.put(method, index); + List group = (List) groups.get(generators[index]); + if (group == null) { + groups.put(generators[index], group = new ArrayList(methods.size())); + } + group.add(method); + + // Optimization: build up a map of Class -> bridge methods in class + // so that we can look up all the bridge methods in one pass for a class. + if (TypeUtils.isBridge(actualMethod.getModifiers())) { + Set bridges = (Set) declToBridge.get(actualMethod.getDeclaringClass()); + if (bridges == null) { + bridges = new HashSet(); + declToBridge.put(actualMethod.getDeclaringClass(), bridges); + } + bridges.add(method.getSignature()); + } + } + + final Map bridgeToTarget = new BridgeMethodResolver(declToBridge, getClassLoader()).resolveAll(); + + Set seenGen = new HashSet(); + CodeEmitter se = ce.getStaticHook(); + se.new_instance(THREAD_LOCAL); + se.dup(); + se.invoke_constructor(THREAD_LOCAL, CSTRUCT_NULL); + se.putfield(THREAD_CALLBACKS_FIELD); + + final Object[] state = new Object[1]; + CallbackGenerator.Context context = new CallbackGenerator.Context() { + public ClassLoader getClassLoader() { + return Enhancer.this.getClassLoader(); + } + + public int getOriginalModifiers(MethodInfo method) { + return ((Integer) originalModifiers.get(method)).intValue(); + } + + public int getIndex(MethodInfo method) { + return ((Integer) indexes.get(method)).intValue(); + } + + public void emitCallback(CodeEmitter e, int index) { + emitCurrentCallback(e, index); + } + + public Signature getImplSignature(MethodInfo method) { + return rename(method.getSignature(), ((Integer) positions.get(method)).intValue()); + } + + public void emitLoadArgsAndInvoke(CodeEmitter e, MethodInfo method) { + // If this is a bridge and we know the target was called from invokespecial, + // then we need to invoke_virtual w/ the bridge target instead of doing + // a super, because super may itself be using super, which would bypass + // any proxies on the target. + Signature bridgeTarget = (Signature) bridgeToTarget.get(method.getSignature()); + if (bridgeTarget != null) { + // checkcast each argument against the target's argument types + for (int i = 0; i < bridgeTarget.getArgumentTypes().length; i++) { + e.load_arg(i); + Type target = bridgeTarget.getArgumentTypes()[i]; + if (!target.equals(method.getSignature().getArgumentTypes()[i])) { + e.checkcast(target); + } + } + + e.invoke_virtual_this(bridgeTarget); + + Type retType = method.getSignature().getReturnType(); + // Not necessary to cast if the target & bridge have + // the same return type. + // (This conveniently includes void and primitive types, + // which would fail if casted. It's not possible to + // covariant from boxed to unbox (or vice versa), so no having + // to box/unbox for bridges). + // TODO: It also isn't necessary to checkcast if the return is + // assignable from the target. (This would happen if a subclass + // used covariant returns to narrow the return type within a bridge + // method.) + if (!retType.equals(bridgeTarget.getReturnType())) { + e.checkcast(retType); + } + } + else { + e.load_args(); + e.super_invoke(method.getSignature()); + } + } + + public CodeEmitter beginMethod(ClassEmitter ce, MethodInfo method) { + CodeEmitter e = EmitUtils.begin_method(ce, method); + if (!interceptDuringConstruction && + !TypeUtils.isAbstract(method.getModifiers())) { + Label constructed = e.make_label(); + e.load_this(); + e.getfield(CONSTRUCTED_FIELD); + e.if_jump(CodeEmitter.NE, constructed); + e.load_this(); + e.load_args(); + e.super_invoke(); + e.return_value(); + e.mark(constructed); + } + return e; + } + }; + for (int i = 0; i < callbackTypes.length; i++) { + CallbackGenerator gen = generators[i]; + if (!seenGen.contains(gen)) { + seenGen.add(gen); + final List fmethods = (List) groups.get(gen); + if (fmethods != null) { + try { + gen.generate(ce, context, fmethods); + gen.generateStatic(se, context, fmethods); + } + catch (RuntimeException x) { + throw x; + } + catch (Exception x) { + throw new CodeGenerationException(x); + } + } + } + } + se.return_value(); + se.end_method(); + } + + private void emitSetThreadCallbacks(ClassEmitter ce) { + CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC | Constants.ACC_STATIC, + SET_THREAD_CALLBACKS, + null); + e.getfield(THREAD_CALLBACKS_FIELD); + e.load_arg(0); + e.invoke_virtual(THREAD_LOCAL, THREAD_LOCAL_SET); + e.return_value(); + e.end_method(); + } + + private void emitSetStaticCallbacks(ClassEmitter ce) { + CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC | Constants.ACC_STATIC, + SET_STATIC_CALLBACKS, + null); + e.load_arg(0); + e.putfield(STATIC_CALLBACKS_FIELD); + e.return_value(); + e.end_method(); + } + + private void emitCurrentCallback(CodeEmitter e, int index) { + e.load_this(); + e.getfield(getCallbackField(index)); + e.dup(); + Label end = e.make_label(); + e.ifnonnull(end); + e.pop(); // stack height + e.load_this(); + e.invoke_static_this(BIND_CALLBACKS); + e.load_this(); + e.getfield(getCallbackField(index)); + e.mark(end); + } + + private void emitBindCallbacks(ClassEmitter ce) { + CodeEmitter e = ce.begin_method(Constants.PRIVATE_FINAL_STATIC, + BIND_CALLBACKS, + null); + Local me = e.make_local(); + e.load_arg(0); + e.checkcast_this(); + e.store_local(me); + + Label end = e.make_label(); + e.load_local(me); + e.getfield(BOUND_FIELD); + e.if_jump(CodeEmitter.NE, end); + e.load_local(me); + e.push(1); + e.putfield(BOUND_FIELD); + + e.getfield(THREAD_CALLBACKS_FIELD); + e.invoke_virtual(THREAD_LOCAL, THREAD_LOCAL_GET); + e.dup(); + Label found_callback = e.make_label(); + e.ifnonnull(found_callback); + e.pop(); + + e.getfield(STATIC_CALLBACKS_FIELD); + e.dup(); + e.ifnonnull(found_callback); + e.pop(); + e.goTo(end); + + e.mark(found_callback); + e.checkcast(CALLBACK_ARRAY); + e.load_local(me); + e.swap(); + for (int i = callbackTypes.length - 1; i >= 0; i--) { + if (i != 0) { + e.dup2(); + } + e.aaload(i); + e.checkcast(callbackTypes[i]); + e.putfield(getCallbackField(i)); + } + + e.mark(end); + e.return_value(); + e.end_method(); + } + + private static String getCallbackField(int index) { + return "CGLIB$CALLBACK_" + index; + } + +} diff --git a/spring-core/src/main/java/org/springframework/cglib/proxy/MethodProxy.java b/spring-core/src/main/java/org/springframework/cglib/proxy/MethodProxy.java new file mode 100644 index 0000000..fc0f63b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/cglib/proxy/MethodProxy.java @@ -0,0 +1,251 @@ +/* + * Copyright 2003,2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://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.springframework.cglib.proxy; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import org.springframework.cglib.core.AbstractClassGenerator; +import org.springframework.cglib.core.CodeGenerationException; +import org.springframework.cglib.core.GeneratorStrategy; +import org.springframework.cglib.core.NamingPolicy; +import org.springframework.cglib.core.Signature; +import org.springframework.cglib.reflect.FastClass; + +/** + * Classes generated by {@link Enhancer} pass this object to the + * registered {@link MethodInterceptor} objects when an intercepted method is invoked. It can + * be used to either invoke the original method, or call the same method on a different + * object of the same type. + * @version $Id: MethodProxy.java,v 1.16 2009/01/11 20:09:48 herbyderby Exp $ + */ +@SuppressWarnings({"rawtypes", "unchecked"}) +public class MethodProxy { + + private Signature sig1; + + private Signature sig2; + + private CreateInfo createInfo; + + private final Object initLock = new Object(); + + private volatile FastClassInfo fastClassInfo; + + /** + * For internal use by {@link Enhancer} only; see the {@link org.springframework.cglib.reflect.FastMethod} class + * for similar functionality. + */ + public static MethodProxy create(Class c1, Class c2, String desc, String name1, String name2) { + MethodProxy proxy = new MethodProxy(); + proxy.sig1 = new Signature(name1, desc); + proxy.sig2 = new Signature(name2, desc); + proxy.createInfo = new CreateInfo(c1, c2); + return proxy; + } + + private void init() { + /* + * Using a volatile invariant allows us to initialize the FastClass and + * method index pairs atomically. + * + * Double-checked locking is safe with volatile in Java 5. Before 1.5 this + * code could allow fastClassInfo to be instantiated more than once, which + * appears to be benign. + */ + if (fastClassInfo == null) { + synchronized (initLock) { + if (fastClassInfo == null) { + CreateInfo ci = createInfo; + + FastClassInfo fci = new FastClassInfo(); + fci.f1 = helper(ci, ci.c1); + fci.f2 = helper(ci, ci.c2); + fci.i1 = fci.f1.getIndex(sig1); + fci.i2 = fci.f2.getIndex(sig2); + fastClassInfo = fci; + createInfo = null; + } + } + } + } + + + private static class FastClassInfo { + + FastClass f1; + + FastClass f2; + + int i1; + + int i2; + } + + + private static class CreateInfo { + + Class c1; + + Class c2; + + NamingPolicy namingPolicy; + + GeneratorStrategy strategy; + + boolean attemptLoad; + + public CreateInfo(Class c1, Class c2) { + this.c1 = c1; + this.c2 = c2; + AbstractClassGenerator fromEnhancer = AbstractClassGenerator.getCurrent(); + if (fromEnhancer != null) { + namingPolicy = fromEnhancer.getNamingPolicy(); + strategy = fromEnhancer.getStrategy(); + attemptLoad = fromEnhancer.getAttemptLoad(); + } + } + } + + + private static FastClass helper(CreateInfo ci, Class type) { + FastClass.Generator g = new FastClass.Generator(); + g.setType(type); + // SPRING PATCH BEGIN + g.setContextClass(type); + // SPRING PATCH END + g.setClassLoader(ci.c2.getClassLoader()); + g.setNamingPolicy(ci.namingPolicy); + g.setStrategy(ci.strategy); + g.setAttemptLoad(ci.attemptLoad); + return g.create(); + } + + private MethodProxy() { + } + + /** + * Return the signature of the proxied method. + */ + public Signature getSignature() { + return sig1; + } + + /** + * Return the name of the synthetic method created by CGLIB which is + * used by {@link #invokeSuper} to invoke the superclass + * (non-intercepted) method implementation. The parameter types are + * the same as the proxied method. + */ + public String getSuperName() { + return sig2.getName(); + } + + /** + * Return the {@link org.springframework.cglib.reflect.FastClass} method index + * for the method used by {@link #invokeSuper}. This index uniquely + * identifies the method within the generated proxy, and therefore + * can be useful to reference external metadata. + * @see #getSuperName + */ + public int getSuperIndex() { + init(); + return fastClassInfo.i2; + } + + // For testing + FastClass getFastClass() { + init(); + return fastClassInfo.f1; + } + + // For testing + FastClass getSuperFastClass() { + init(); + return fastClassInfo.f2; + } + + /** + * Return the MethodProxy used when intercepting the method + * matching the given signature. + * @param type the class generated by Enhancer + * @param sig the signature to match + * @return the MethodProxy instance, or null if no applicable matching method is found + * @throws IllegalArgumentException if the Class was not created by Enhancer or does not use a MethodInterceptor + */ + public static MethodProxy find(Class type, Signature sig) { + try { + Method m = type.getDeclaredMethod(MethodInterceptorGenerator.FIND_PROXY_NAME, + MethodInterceptorGenerator.FIND_PROXY_TYPES); + return (MethodProxy) m.invoke(null, new Object[]{sig}); + } + catch (NoSuchMethodException ex) { + throw new IllegalArgumentException("Class " + type + " does not use a MethodInterceptor"); + } + catch (IllegalAccessException | InvocationTargetException ex) { + throw new CodeGenerationException(ex); + } + } + + /** + * Invoke the original method, on a different object of the same type. + * @param obj the compatible object; recursion will result if you use the object passed as the first + * argument to the MethodInterceptor (usually not what you want) + * @param args the arguments passed to the intercepted method; you may substitute a different + * argument array as long as the types are compatible + * @throws Throwable the bare exceptions thrown by the called method are passed through + * without wrapping in an InvocationTargetException + * @see MethodInterceptor#intercept + */ + public Object invoke(Object obj, Object[] args) throws Throwable { + try { + init(); + FastClassInfo fci = fastClassInfo; + return fci.f1.invoke(fci.i1, obj, args); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + catch (IllegalArgumentException ex) { + if (fastClassInfo.i1 < 0) + throw new IllegalArgumentException("Protected method: " + sig1); + throw ex; + } + } + + /** + * Invoke the original (super) method on the specified object. + * @param obj the enhanced object, must be the object passed as the first + * argument to the MethodInterceptor + * @param args the arguments passed to the intercepted method; you may substitute a different + * argument array as long as the types are compatible + * @throws Throwable the bare exceptions thrown by the called method are passed through + * without wrapping in an InvocationTargetException + * @see MethodInterceptor#intercept + */ + public Object invokeSuper(Object obj, Object[] args) throws Throwable { + try { + init(); + FastClassInfo fci = fastClassInfo; + return fci.f2.invoke(fci.i2, obj, args); + } + catch (InvocationTargetException e) { + throw e.getTargetException(); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/cglib/proxy/package-info.java b/spring-core/src/main/java/org/springframework/cglib/proxy/package-info.java new file mode 100644 index 0000000..0d651c8 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/cglib/proxy/package-info.java @@ -0,0 +1,10 @@ +/** + * Spring's repackaging of the + * CGLIB proxy package + * (for internal use only). + * + *

    As this repackaging happens at the class file level, sources + * and javadocs are not available here... except for a few files + * that have been patched for Spring's purposes on JDK 9/10/11. + */ +package org.springframework.cglib.proxy; diff --git a/spring-core/src/main/java/org/springframework/core/AliasRegistry.java b/spring-core/src/main/java/org/springframework/core/AliasRegistry.java new file mode 100644 index 0000000..689b3ae --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/AliasRegistry.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +/** + * Common interface for managing aliases. Serves as a super-interface for + * {@link org.springframework.beans.factory.support.BeanDefinitionRegistry}. + * + * @author Juergen Hoeller + * @since 2.5.2 + */ +public interface AliasRegistry { + + /** + * Given a name, register an alias for it. + * @param name the canonical name + * @param alias the alias to be registered + * @throws IllegalStateException if the alias is already in use + * and may not be overridden + */ + void registerAlias(String name, String alias); + + /** + * Remove the specified alias from this registry. + * @param alias the alias to remove + * @throws IllegalStateException if no such alias was found + */ + void removeAlias(String alias); + + /** + * Determine whether the given name is defined as an alias + * (as opposed to the name of an actually registered component). + * @param name the name to check + * @return whether the given name is an alias + */ + boolean isAlias(String name); + + /** + * Return the aliases for the given name, if defined. + * @param name the name to check for aliases + * @return the aliases, or an empty array if none + */ + String[] getAliases(String name); + +} diff --git a/spring-core/src/main/java/org/springframework/core/AttributeAccessor.java b/spring-core/src/main/java/org/springframework/core/AttributeAccessor.java new file mode 100644 index 0000000..bb81cca --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/AttributeAccessor.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import org.springframework.lang.Nullable; + +/** + * Interface defining a generic contract for attaching and accessing metadata + * to/from arbitrary objects. + * + * @author Rob Harrop + * @since 2.0 + */ +public interface AttributeAccessor { + + /** + * Set the attribute defined by {@code name} to the supplied {@code value}. + * If {@code value} is {@code null}, the attribute is {@link #removeAttribute removed}. + *

    In general, users should take care to prevent overlaps with other + * metadata attributes by using fully-qualified names, perhaps using + * class or package names as prefix. + * @param name the unique attribute key + * @param value the attribute value to be attached + */ + void setAttribute(String name, @Nullable Object value); + + /** + * Get the value of the attribute identified by {@code name}. + * Return {@code null} if the attribute doesn't exist. + * @param name the unique attribute key + * @return the current value of the attribute, if any + */ + @Nullable + Object getAttribute(String name); + + /** + * Remove the attribute identified by {@code name} and return its value. + * Return {@code null} if no attribute under {@code name} is found. + * @param name the unique attribute key + * @return the last value of the attribute, if any + */ + @Nullable + Object removeAttribute(String name); + + /** + * Return {@code true} if the attribute identified by {@code name} exists. + * Otherwise return {@code false}. + * @param name the unique attribute key + */ + boolean hasAttribute(String name); + + /** + * Return the names of all attributes. + */ + String[] attributeNames(); + +} diff --git a/spring-core/src/main/java/org/springframework/core/AttributeAccessorSupport.java b/spring-core/src/main/java/org/springframework/core/AttributeAccessorSupport.java new file mode 100644 index 0000000..df9c391 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/AttributeAccessorSupport.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.io.Serializable; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Support class for {@link AttributeAccessor AttributeAccessors}, providing + * a base implementation of all methods. To be extended by subclasses. + * + *

    {@link Serializable} if subclasses and all attribute values are {@link Serializable}. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + */ +@SuppressWarnings("serial") +public abstract class AttributeAccessorSupport implements AttributeAccessor, Serializable { + + /** Map with String keys and Object values. */ + private final Map attributes = new LinkedHashMap<>(); + + + @Override + public void setAttribute(String name, @Nullable Object value) { + Assert.notNull(name, "Name must not be null"); + if (value != null) { + this.attributes.put(name, value); + } + else { + removeAttribute(name); + } + } + + @Override + @Nullable + public Object getAttribute(String name) { + Assert.notNull(name, "Name must not be null"); + return this.attributes.get(name); + } + + @Override + @Nullable + public Object removeAttribute(String name) { + Assert.notNull(name, "Name must not be null"); + return this.attributes.remove(name); + } + + @Override + public boolean hasAttribute(String name) { + Assert.notNull(name, "Name must not be null"); + return this.attributes.containsKey(name); + } + + @Override + public String[] attributeNames() { + return StringUtils.toStringArray(this.attributes.keySet()); + } + + + /** + * Copy the attributes from the supplied AttributeAccessor to this accessor. + * @param source the AttributeAccessor to copy from + */ + protected void copyAttributesFrom(AttributeAccessor source) { + Assert.notNull(source, "Source must not be null"); + String[] attributeNames = source.attributeNames(); + for (String attributeName : attributeNames) { + setAttribute(attributeName, source.getAttribute(attributeName)); + } + } + + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof AttributeAccessorSupport && + this.attributes.equals(((AttributeAccessorSupport) other).attributes))); + } + + @Override + public int hashCode() { + return this.attributes.hashCode(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/BridgeMethodResolver.java b/spring-core/src/main/java/org/springframework/core/BridgeMethodResolver.java new file mode 100644 index 0000000..1615469 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/BridgeMethodResolver.java @@ -0,0 +1,242 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.ReflectionUtils.MethodFilter; + +/** + * Helper for resolving synthetic {@link Method#isBridge bridge Methods} to the + * {@link Method} being bridged. + * + *

    Given a synthetic {@link Method#isBridge bridge Method} returns the {@link Method} + * being bridged. A bridge method may be created by the compiler when extending a + * parameterized type whose methods have parameterized arguments. During runtime + * invocation the bridge {@link Method} may be invoked and/or used via reflection. + * When attempting to locate annotations on {@link Method Methods}, it is wise to check + * for bridge {@link Method Methods} as appropriate and find the bridged {@link Method}. + * + *

    See + * The Java Language Specification for more details on the use of bridge methods. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Phillip Webb + * @since 2.0 + */ +public final class BridgeMethodResolver { + + private static final Map cache = new ConcurrentReferenceHashMap<>(); + + private BridgeMethodResolver() { + } + + + /** + * Find the original method for the supplied {@link Method bridge Method}. + *

    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 not required to check for bridging before calling this method. + * @param bridgeMethod the method to introspect + * @return the original method (either the bridged method or the passed-in method + * if no more specific one could be found) + */ + public static Method findBridgedMethod(Method bridgeMethod) { + if (!bridgeMethod.isBridge()) { + return bridgeMethod; + } + Method bridgedMethod = cache.get(bridgeMethod); + if (bridgedMethod == null) { + // Gather all methods with matching name and parameter size. + List candidateMethods = new ArrayList<>(); + MethodFilter filter = candidateMethod -> + isBridgedCandidateFor(candidateMethod, bridgeMethod); + ReflectionUtils.doWithMethods(bridgeMethod.getDeclaringClass(), candidateMethods::add, filter); + if (!candidateMethods.isEmpty()) { + bridgedMethod = candidateMethods.size() == 1 ? + candidateMethods.get(0) : + searchCandidates(candidateMethods, bridgeMethod); + } + if (bridgedMethod == null) { + // A bridge method was passed in but we couldn't find the bridged method. + // Let's proceed with the passed-in method and hope for the best... + bridgedMethod = bridgeMethod; + } + cache.put(bridgeMethod, bridgedMethod); + } + return bridgedMethod; + } + + /** + * Returns {@code true} if the supplied '{@code candidateMethod}' can be + * consider a validate 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 filter for a set of possible matches. + */ + private static boolean isBridgedCandidateFor(Method candidateMethod, Method bridgeMethod) { + return (!candidateMethod.isBridge() && !candidateMethod.equals(bridgeMethod) && + candidateMethod.getName().equals(bridgeMethod.getName()) && + candidateMethod.getParameterCount() == bridgeMethod.getParameterCount()); + } + + /** + * Searches for the bridged method in the given candidates. + * @param candidateMethods the List of candidate Methods + * @param bridgeMethod the bridge method + * @return the bridged method, or {@code null} if none found + */ + @Nullable + private static Method searchCandidates(List candidateMethods, Method bridgeMethod) { + if (candidateMethods.isEmpty()) { + return null; + } + Method previousMethod = null; + boolean sameSig = true; + for (Method candidateMethod : candidateMethods) { + if (isBridgeMethodFor(bridgeMethod, candidateMethod, bridgeMethod.getDeclaringClass())) { + return candidateMethod; + } + else if (previousMethod != null) { + sameSig = sameSig && + Arrays.equals(candidateMethod.getGenericParameterTypes(), previousMethod.getGenericParameterTypes()); + } + previousMethod = candidateMethod; + } + return (sameSig ? candidateMethods.get(0) : null); + } + + /** + * Determines whether or not the bridge {@link Method} is the bridge for the + * supplied candidate {@link Method}. + */ + static boolean isBridgeMethodFor(Method bridgeMethod, Method candidateMethod, Class declaringClass) { + if (isResolvedTypeMatch(candidateMethod, bridgeMethod, declaringClass)) { + return true; + } + Method method = findGenericDeclaration(bridgeMethod); + return (method != null && isResolvedTypeMatch(method, candidateMethod, declaringClass)); + } + + /** + * Returns {@code true} if the {@link Type} signature of both the supplied + * {@link Method#getGenericParameterTypes() generic Method} and concrete {@link Method} + * are equal after resolving all types against the declaringType, otherwise + * returns {@code false}. + */ + private static boolean isResolvedTypeMatch(Method genericMethod, Method candidateMethod, Class declaringClass) { + Type[] genericParameters = genericMethod.getGenericParameterTypes(); + if (genericParameters.length != candidateMethod.getParameterCount()) { + return false; + } + Class[] candidateParameters = candidateMethod.getParameterTypes(); + for (int i = 0; i < candidateParameters.length; i++) { + ResolvableType genericParameter = ResolvableType.forMethodParameter(genericMethod, i, declaringClass); + Class candidateParameter = candidateParameters[i]; + if (candidateParameter.isArray()) { + // An array type: compare the component type. + if (!candidateParameter.getComponentType().equals(genericParameter.getComponentType().toClass())) { + return false; + } + } + // A non-array type: compare the type itself. + if (!candidateParameter.equals(genericParameter.toClass())) { + return false; + } + } + return true; + } + + /** + * Searches for the generic {@link Method} declaration whose erased signature + * matches that of the supplied bridge method. + * @throws IllegalStateException if the generic declaration cannot be found + */ + @Nullable + private static Method findGenericDeclaration(Method bridgeMethod) { + // Search parent types for method that has same signature as bridge. + Class superclass = bridgeMethod.getDeclaringClass().getSuperclass(); + while (superclass != null && Object.class != superclass) { + Method method = searchForMatch(superclass, bridgeMethod); + if (method != null && !method.isBridge()) { + return method; + } + superclass = superclass.getSuperclass(); + } + + Class[] interfaces = ClassUtils.getAllInterfacesForClass(bridgeMethod.getDeclaringClass()); + return searchInterfaces(interfaces, bridgeMethod); + } + + @Nullable + private static Method searchInterfaces(Class[] interfaces, Method bridgeMethod) { + for (Class ifc : interfaces) { + Method method = searchForMatch(ifc, bridgeMethod); + if (method != null && !method.isBridge()) { + return method; + } + else { + method = searchInterfaces(ifc.getInterfaces(), bridgeMethod); + if (method != null) { + return method; + } + } + } + return null; + } + + /** + * If the supplied {@link Class} has a declared {@link Method} whose signature matches + * that of the supplied {@link Method}, then this matching {@link Method} is returned, + * otherwise {@code null} is returned. + */ + @Nullable + private static Method searchForMatch(Class type, Method bridgeMethod) { + try { + return type.getDeclaredMethod(bridgeMethod.getName(), bridgeMethod.getParameterTypes()); + } + catch (NoSuchMethodException ex) { + return null; + } + } + + /** + * 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.java.com/view_bug.do?bug_id=6342411. + * See also https://stas-blogspot.blogspot.com/2010/03/java-bridge-methods-explained.html + * @return whether signatures match as described + */ + public static boolean isVisibilityBridgeMethodPair(Method bridgeMethod, Method bridgedMethod) { + if (bridgeMethod == bridgedMethod) { + return true; + } + return (bridgeMethod.getReturnType().equals(bridgedMethod.getReturnType()) && + bridgeMethod.getParameterCount() == bridgedMethod.getParameterCount() && + Arrays.equals(bridgeMethod.getParameterTypes(), bridgedMethod.getParameterTypes())); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/CollectionFactory.java b/spring-core/src/main/java/org/springframework/core/CollectionFactory.java new file mode 100644 index 0000000..30225a7 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/CollectionFactory.java @@ -0,0 +1,409 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.NavigableSet; +import java.util.Properties; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.ReflectionUtils; + +/** + * Factory for collections that is aware of common Java and Spring collection types. + * + *

    Mainly for internal use within the framework. + * + * @author Juergen Hoeller + * @author Arjen Poutsma + * @author Oliver Gierke + * @author Sam Brannen + * @since 1.1.1 + */ +public final class CollectionFactory { + + private static final Set> approximableCollectionTypes = new HashSet<>(); + + private static final Set> approximableMapTypes = new HashSet<>(); + + + static { + // Standard collection interfaces + approximableCollectionTypes.add(Collection.class); + approximableCollectionTypes.add(List.class); + approximableCollectionTypes.add(Set.class); + approximableCollectionTypes.add(SortedSet.class); + approximableCollectionTypes.add(NavigableSet.class); + approximableMapTypes.add(Map.class); + approximableMapTypes.add(SortedMap.class); + approximableMapTypes.add(NavigableMap.class); + + // Common concrete collection classes + approximableCollectionTypes.add(ArrayList.class); + approximableCollectionTypes.add(LinkedList.class); + approximableCollectionTypes.add(HashSet.class); + approximableCollectionTypes.add(LinkedHashSet.class); + approximableCollectionTypes.add(TreeSet.class); + approximableCollectionTypes.add(EnumSet.class); + approximableMapTypes.add(HashMap.class); + approximableMapTypes.add(LinkedHashMap.class); + approximableMapTypes.add(TreeMap.class); + approximableMapTypes.add(EnumMap.class); + } + + + private CollectionFactory() { + } + + + /** + * Determine whether the given collection type is an approximable type, + * i.e. a type that {@link #createApproximateCollection} can approximate. + * @param collectionType the collection type to check + * @return {@code true} if the type is approximable + */ + public static boolean isApproximableCollectionType(@Nullable Class collectionType) { + return (collectionType != null && approximableCollectionTypes.contains(collectionType)); + } + + /** + * Create the most approximate collection for the given collection. + *

    Warning: Since the parameterized type {@code E} is + * not bound to the type of elements contained in the supplied + * {@code collection}, type safety cannot be guaranteed if the supplied + * {@code collection} is an {@link EnumSet}. In such scenarios, the caller + * is responsible for ensuring that the element type for the supplied + * {@code collection} is an enum type matching type {@code E}. As an + * alternative, the caller may wish to treat the return value as a raw + * collection or collection of {@link Object}. + * @param collection the original collection object, potentially {@code null} + * @param capacity the initial capacity + * @return a new, empty collection instance + * @see #isApproximableCollectionType + * @see java.util.LinkedList + * @see java.util.ArrayList + * @see java.util.EnumSet + * @see java.util.TreeSet + * @see java.util.LinkedHashSet + */ + @SuppressWarnings({"rawtypes", "unchecked", "cast"}) + public static Collection createApproximateCollection(@Nullable Object collection, int capacity) { + if (collection instanceof LinkedList) { + return new LinkedList<>(); + } + else if (collection instanceof List) { + return new ArrayList<>(capacity); + } + else if (collection instanceof EnumSet) { + // Cast is necessary for compilation in Eclipse 4.4.1. + Collection enumSet = (Collection) EnumSet.copyOf((EnumSet) collection); + enumSet.clear(); + return enumSet; + } + else if (collection instanceof SortedSet) { + return new TreeSet<>(((SortedSet) collection).comparator()); + } + else { + return new LinkedHashSet<>(capacity); + } + } + + /** + * Create the most appropriate collection for the given collection type. + *

    Delegates to {@link #createCollection(Class, Class, int)} with a + * {@code null} element type. + * @param collectionType the desired type of the target collection (never {@code null}) + * @param capacity the initial capacity + * @return a new collection instance + * @throws IllegalArgumentException if the supplied {@code collectionType} + * is {@code null} or of type {@link EnumSet} + */ + public static Collection createCollection(Class collectionType, int capacity) { + return createCollection(collectionType, null, capacity); + } + + /** + * Create the most appropriate collection for the given collection type. + *

    Warning: Since the parameterized type {@code E} is + * not bound to the supplied {@code elementType}, type safety cannot be + * guaranteed if the desired {@code collectionType} is {@link EnumSet}. + * In such scenarios, the caller is responsible for ensuring that the + * supplied {@code elementType} is an enum type matching type {@code E}. + * As an alternative, the caller may wish to treat the return value as a + * raw collection or collection of {@link Object}. + * @param collectionType the desired type of the target collection (never {@code null}) + * @param elementType the collection's element type, or {@code null} if unknown + * (note: only relevant for {@link EnumSet} creation) + * @param capacity the initial capacity + * @return a new collection instance + * @since 4.1.3 + * @see java.util.LinkedHashSet + * @see java.util.ArrayList + * @see java.util.TreeSet + * @see java.util.EnumSet + * @throws IllegalArgumentException if the supplied {@code collectionType} is + * {@code null}; or if the desired {@code collectionType} is {@link EnumSet} and + * the supplied {@code elementType} is not a subtype of {@link Enum} + */ + @SuppressWarnings({"unchecked", "cast"}) + public static Collection createCollection(Class collectionType, @Nullable Class elementType, int capacity) { + Assert.notNull(collectionType, "Collection type must not be null"); + if (collectionType.isInterface()) { + if (Set.class == collectionType || Collection.class == collectionType) { + return new LinkedHashSet<>(capacity); + } + else if (List.class == collectionType) { + return new ArrayList<>(capacity); + } + else if (SortedSet.class == collectionType || NavigableSet.class == collectionType) { + return new TreeSet<>(); + } + else { + throw new IllegalArgumentException("Unsupported Collection interface: " + collectionType.getName()); + } + } + else if (EnumSet.class.isAssignableFrom(collectionType)) { + Assert.notNull(elementType, "Cannot create EnumSet for unknown element type"); + // Cast is necessary for compilation in Eclipse 4.4.1. + return (Collection) EnumSet.noneOf(asEnumType(elementType)); + } + else { + if (!Collection.class.isAssignableFrom(collectionType)) { + throw new IllegalArgumentException("Unsupported Collection type: " + collectionType.getName()); + } + try { + return (Collection) ReflectionUtils.accessibleConstructor(collectionType).newInstance(); + } + catch (Throwable ex) { + throw new IllegalArgumentException( + "Could not instantiate Collection type: " + collectionType.getName(), ex); + } + } + } + + /** + * Determine whether the given map type is an approximable type, + * i.e. a type that {@link #createApproximateMap} can approximate. + * @param mapType the map type to check + * @return {@code true} if the type is approximable + */ + public static boolean isApproximableMapType(@Nullable Class mapType) { + return (mapType != null && approximableMapTypes.contains(mapType)); + } + + /** + * Create the most approximate map for the given map. + *

    Warning: Since the parameterized type {@code K} is + * not bound to the type of keys contained in the supplied {@code map}, + * type safety cannot be guaranteed if the supplied {@code map} is an + * {@link EnumMap}. In such scenarios, the caller is responsible for + * ensuring that the key type in the supplied {@code map} is an enum type + * matching type {@code K}. As an alternative, the caller may wish to + * treat the return value as a raw map or map keyed by {@link Object}. + * @param map the original map object, potentially {@code null} + * @param capacity the initial capacity + * @return a new, empty map instance + * @see #isApproximableMapType + * @see java.util.EnumMap + * @see java.util.TreeMap + * @see java.util.LinkedHashMap + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + public static Map createApproximateMap(@Nullable Object map, int capacity) { + if (map instanceof EnumMap) { + EnumMap enumMap = new EnumMap((EnumMap) map); + enumMap.clear(); + return enumMap; + } + else if (map instanceof SortedMap) { + return new TreeMap<>(((SortedMap) map).comparator()); + } + else { + return new LinkedHashMap<>(capacity); + } + } + + /** + * Create the most appropriate map for the given map type. + *

    Delegates to {@link #createMap(Class, Class, int)} with a + * {@code null} key type. + * @param mapType the desired type of the target map + * @param capacity the initial capacity + * @return a new map instance + * @throws IllegalArgumentException if the supplied {@code mapType} is + * {@code null} or of type {@link EnumMap} + */ + public static Map createMap(Class mapType, int capacity) { + return createMap(mapType, null, capacity); + } + + /** + * Create the most appropriate map for the given map type. + *

    Warning: Since the parameterized type {@code K} + * is not bound to the supplied {@code keyType}, type safety cannot be + * guaranteed if the desired {@code mapType} is {@link EnumMap}. In such + * scenarios, the caller is responsible for ensuring that the {@code keyType} + * is an enum type matching type {@code K}. As an alternative, the caller + * may wish to treat the return value as a raw map or map keyed by + * {@link Object}. Similarly, type safety cannot be enforced if the + * desired {@code mapType} is {@link MultiValueMap}. + * @param mapType the desired type of the target map (never {@code null}) + * @param keyType the map's key type, or {@code null} if unknown + * (note: only relevant for {@link EnumMap} creation) + * @param capacity the initial capacity + * @return a new map instance + * @since 4.1.3 + * @see java.util.LinkedHashMap + * @see java.util.TreeMap + * @see org.springframework.util.LinkedMultiValueMap + * @see java.util.EnumMap + * @throws IllegalArgumentException if the supplied {@code mapType} is + * {@code null}; or if the desired {@code mapType} is {@link EnumMap} and + * the supplied {@code keyType} is not a subtype of {@link Enum} + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + public static Map createMap(Class mapType, @Nullable Class keyType, int capacity) { + Assert.notNull(mapType, "Map type must not be null"); + if (mapType.isInterface()) { + if (Map.class == mapType) { + return new LinkedHashMap<>(capacity); + } + else if (SortedMap.class == mapType || NavigableMap.class == mapType) { + return new TreeMap<>(); + } + else if (MultiValueMap.class == mapType) { + return new LinkedMultiValueMap(); + } + else { + throw new IllegalArgumentException("Unsupported Map interface: " + mapType.getName()); + } + } + else if (EnumMap.class == mapType) { + Assert.notNull(keyType, "Cannot create EnumMap for unknown key type"); + return new EnumMap(asEnumType(keyType)); + } + else { + if (!Map.class.isAssignableFrom(mapType)) { + throw new IllegalArgumentException("Unsupported Map type: " + mapType.getName()); + } + try { + return (Map) ReflectionUtils.accessibleConstructor(mapType).newInstance(); + } + catch (Throwable ex) { + throw new IllegalArgumentException("Could not instantiate Map type: " + mapType.getName(), ex); + } + } + } + + /** + * Create a variant of {@link java.util.Properties} that automatically adapts + * non-String values to String representations in {@link Properties#getProperty}. + *

    In addition, the returned {@code Properties} instance sorts properties + * alphanumerically based on their keys. + * @return a new {@code Properties} instance + * @since 4.3.4 + * @see #createSortedProperties(boolean) + * @see #createSortedProperties(Properties, boolean) + */ + @SuppressWarnings("serial") + public static Properties createStringAdaptingProperties() { + return new SortedProperties(false) { + @Override + @Nullable + public String getProperty(String key) { + Object value = get(key); + return (value != null ? value.toString() : null); + } + }; + } + + /** + * Create a variant of {@link java.util.Properties} that sorts properties + * alphanumerically based on their keys. + *

    This can be useful when storing the {@link Properties} instance in a + * properties file, since it allows such files to be generated in a repeatable + * manner with consistent ordering of properties. Comments in generated + * properties files can also be optionally omitted. + * @param omitComments {@code true} if comments should be omitted when + * storing properties in a file + * @return a new {@code Properties} instance + * @since 5.2 + * @see #createStringAdaptingProperties() + * @see #createSortedProperties(Properties, boolean) + */ + public static Properties createSortedProperties(boolean omitComments) { + return new SortedProperties(omitComments); + } + + /** + * Create a variant of {@link java.util.Properties} that sorts properties + * alphanumerically based on their keys. + *

    This can be useful when storing the {@code Properties} instance in a + * properties file, since it allows such files to be generated in a repeatable + * manner with consistent ordering of properties. Comments in generated + * properties files can also be optionally omitted. + *

    The returned {@code Properties} instance will be populated with + * properties from the supplied {@code properties} object, but default + * properties from the supplied {@code properties} object will not be copied. + * @param properties the {@code Properties} object from which to copy the + * initial properties + * @param omitComments {@code true} if comments should be omitted when + * storing properties in a file + * @return a new {@code Properties} instance + * @since 5.2 + * @see #createStringAdaptingProperties() + * @see #createSortedProperties(boolean) + */ + public static Properties createSortedProperties(Properties properties, boolean omitComments) { + return new SortedProperties(properties, omitComments); + } + + /** + * Cast the given type to a subtype of {@link Enum}. + * @param enumType the enum type, never {@code null} + * @return the given type as subtype of {@link Enum} + * @throws IllegalArgumentException if the given type is not a subtype of {@link Enum} + */ + @SuppressWarnings("rawtypes") + private static Class asEnumType(Class enumType) { + Assert.notNull(enumType, "Enum type must not be null"); + if (!Enum.class.isAssignableFrom(enumType)) { + throw new IllegalArgumentException("Supplied type is not an enum: " + enumType.getName()); + } + return enumType.asSubclass(Enum.class); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/ConfigurableObjectInputStream.java b/spring-core/src/main/java/org/springframework/core/ConfigurableObjectInputStream.java new file mode 100644 index 0000000..52c11ee --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/ConfigurableObjectInputStream.java @@ -0,0 +1,152 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.io.IOException; +import java.io.InputStream; +import java.io.NotSerializableException; +import java.io.ObjectInputStream; +import java.io.ObjectStreamClass; + +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * Special ObjectInputStream subclass that resolves class names + * against a specific ClassLoader. Serves as base class for + * {@link org.springframework.remoting.rmi.CodebaseAwareObjectInputStream}. + * + * @author Juergen Hoeller + * @since 2.5.5 + */ +public class ConfigurableObjectInputStream extends ObjectInputStream { + + @Nullable + private final ClassLoader classLoader; + + private final boolean acceptProxyClasses; + + + /** + * Create a new ConfigurableObjectInputStream for the given InputStream and ClassLoader. + * @param in the InputStream to read from + * @param classLoader the ClassLoader to use for loading local classes + * @see java.io.ObjectInputStream#ObjectInputStream(java.io.InputStream) + */ + public ConfigurableObjectInputStream(InputStream in, @Nullable ClassLoader classLoader) throws IOException { + this(in, classLoader, true); + } + + /** + * Create a new ConfigurableObjectInputStream for the given InputStream and ClassLoader. + * @param in the InputStream to read from + * @param classLoader the ClassLoader to use for loading local classes + * @param acceptProxyClasses whether to accept deserialization of proxy classes + * (may be deactivated as a security measure) + * @see java.io.ObjectInputStream#ObjectInputStream(java.io.InputStream) + */ + public ConfigurableObjectInputStream( + InputStream in, @Nullable ClassLoader classLoader, boolean acceptProxyClasses) throws IOException { + + super(in); + this.classLoader = classLoader; + this.acceptProxyClasses = acceptProxyClasses; + } + + + @Override + protected Class resolveClass(ObjectStreamClass classDesc) throws IOException, ClassNotFoundException { + try { + if (this.classLoader != null) { + // Use the specified ClassLoader to resolve local classes. + return ClassUtils.forName(classDesc.getName(), this.classLoader); + } + else { + // Use the default ClassLoader... + return super.resolveClass(classDesc); + } + } + catch (ClassNotFoundException ex) { + return resolveFallbackIfPossible(classDesc.getName(), ex); + } + } + + @Override + protected Class resolveProxyClass(String[] interfaces) throws IOException, ClassNotFoundException { + if (!this.acceptProxyClasses) { + throw new NotSerializableException("Not allowed to accept serialized proxy classes"); + } + if (this.classLoader != null) { + // Use the specified ClassLoader to resolve local proxy classes. + Class[] resolvedInterfaces = new Class[interfaces.length]; + for (int i = 0; i < interfaces.length; i++) { + try { + resolvedInterfaces[i] = ClassUtils.forName(interfaces[i], this.classLoader); + } + catch (ClassNotFoundException ex) { + resolvedInterfaces[i] = resolveFallbackIfPossible(interfaces[i], ex); + } + } + try { + return ClassUtils.createCompositeInterface(resolvedInterfaces, this.classLoader); + } + catch (IllegalArgumentException ex) { + throw new ClassNotFoundException(null, ex); + } + } + else { + // Use ObjectInputStream's default ClassLoader... + try { + return super.resolveProxyClass(interfaces); + } + catch (ClassNotFoundException ex) { + Class[] resolvedInterfaces = new Class[interfaces.length]; + for (int i = 0; i < interfaces.length; i++) { + resolvedInterfaces[i] = resolveFallbackIfPossible(interfaces[i], ex); + } + return ClassUtils.createCompositeInterface(resolvedInterfaces, getFallbackClassLoader()); + } + } + } + + + /** + * Resolve the given class name against a fallback class loader. + *

    The default implementation simply rethrows the original exception, + * since there is no fallback available. + * @param className the class name to resolve + * @param ex the original exception thrown when attempting to load the class + * @return the newly resolved class (never {@code null}) + */ + protected Class resolveFallbackIfPossible(String className, ClassNotFoundException ex) + throws IOException, ClassNotFoundException{ + + throw ex; + } + + /** + * Return the fallback ClassLoader to use when no ClassLoader was specified + * and ObjectInputStream's own default class loader failed. + *

    The default implementation simply returns {@code null}, indicating + * that no specific fallback is available. + */ + @Nullable + protected ClassLoader getFallbackClassLoader() throws IOException { + return null; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/Constants.java b/spring-core/src/main/java/org/springframework/core/Constants.java new file mode 100644 index 0000000..1515955 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/Constants.java @@ -0,0 +1,366 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +/** + * This class can be used to parse other classes containing constant definitions + * in public static final members. The {@code asXXXX} methods of this class + * allow these constant values to be accessed via their string names. + * + *

    Consider class Foo containing {@code public final static int CONSTANT1 = 66;} + * An instance of this class wrapping {@code Foo.class} will return the constant value + * of 66 from its {@code asNumber} method given the argument {@code "CONSTANT1"}. + * + *

    This class is ideal for use in PropertyEditors, enabling them to + * recognize the same names as the constants themselves, and freeing them + * from maintaining their own mapping. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 16.03.2003 + */ +public class Constants { + + /** The name of the introspected class. */ + private final String className; + + /** Map from String field name to object value. */ + private final Map fieldCache = new HashMap<>(); + + + /** + * Create a new Constants converter class wrapping the given class. + *

    All public static final variables will be exposed, whatever their type. + * @param clazz the class to analyze + * @throws IllegalArgumentException if the supplied {@code clazz} is {@code null} + */ + public Constants(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + this.className = clazz.getName(); + Field[] fields = clazz.getFields(); + for (Field field : fields) { + if (ReflectionUtils.isPublicStaticFinal(field)) { + String name = field.getName(); + try { + Object value = field.get(null); + this.fieldCache.put(name, value); + } + catch (IllegalAccessException ex) { + // just leave this field and continue + } + } + } + } + + + /** + * Return the name of the analyzed class. + */ + public final String getClassName() { + return this.className; + } + + /** + * Return the number of constants exposed. + */ + public final int getSize() { + return this.fieldCache.size(); + } + + /** + * Exposes the field cache to subclasses: + * a Map from String field name to object value. + */ + protected final Map getFieldCache() { + return this.fieldCache; + } + + + /** + * Return a constant value cast to a Number. + * @param code the name of the field (never {@code null}) + * @return the Number value + * @throws ConstantException if the field name wasn't found + * or if the type wasn't compatible with Number + * @see #asObject + */ + public Number asNumber(String code) throws ConstantException { + Object obj = asObject(code); + if (!(obj instanceof Number)) { + throw new ConstantException(this.className, code, "not a Number"); + } + return (Number) obj; + } + + /** + * Return a constant value as a String. + * @param code the name of the field (never {@code null}) + * @return the String value + * Works even if it's not a string (invokes {@code toString()}). + * @throws ConstantException if the field name wasn't found + * @see #asObject + */ + public String asString(String code) throws ConstantException { + return asObject(code).toString(); + } + + /** + * Parse the given String (upper or lower case accepted) and return + * the appropriate value if it's the name of a constant field in the + * class that we're analysing. + * @param code the name of the field (never {@code null}) + * @return the Object value + * @throws ConstantException if there's no such field + */ + public Object asObject(String code) throws ConstantException { + Assert.notNull(code, "Code must not be null"); + String codeToUse = code.toUpperCase(Locale.ENGLISH); + Object val = this.fieldCache.get(codeToUse); + if (val == null) { + throw new ConstantException(this.className, codeToUse, "not found"); + } + return val; + } + + + /** + * Return all names of the given group of constants. + *

    Note that this method assumes that constants are named + * in accordance with the standard Java convention for constant + * values (i.e. all uppercase). The supplied {@code namePrefix} + * will be uppercased (in a locale-insensitive fashion) prior to + * the main logic of this method kicking in. + * @param namePrefix prefix of the constant names to search (may be {@code null}) + * @return the set of constant names + */ + public Set getNames(@Nullable String namePrefix) { + String prefixToUse = (namePrefix != null ? namePrefix.trim().toUpperCase(Locale.ENGLISH) : ""); + Set names = new HashSet<>(); + for (String code : this.fieldCache.keySet()) { + if (code.startsWith(prefixToUse)) { + names.add(code); + } + } + return names; + } + + /** + * Return all names of the group of constants for the + * given bean property name. + * @param propertyName the name of the bean property + * @return the set of values + * @see #propertyToConstantNamePrefix + */ + public Set getNamesForProperty(String propertyName) { + return getNames(propertyToConstantNamePrefix(propertyName)); + } + + /** + * Return all names of the given group of constants. + *

    Note that this method assumes that constants are named + * in accordance with the standard Java convention for constant + * values (i.e. all uppercase). The supplied {@code nameSuffix} + * will be uppercased (in a locale-insensitive fashion) prior to + * the main logic of this method kicking in. + * @param nameSuffix suffix of the constant names to search (may be {@code null}) + * @return the set of constant names + */ + public Set getNamesForSuffix(@Nullable String nameSuffix) { + String suffixToUse = (nameSuffix != null ? nameSuffix.trim().toUpperCase(Locale.ENGLISH) : ""); + Set names = new HashSet<>(); + for (String code : this.fieldCache.keySet()) { + if (code.endsWith(suffixToUse)) { + names.add(code); + } + } + return names; + } + + + /** + * Return all values of the given group of constants. + *

    Note that this method assumes that constants are named + * in accordance with the standard Java convention for constant + * values (i.e. all uppercase). The supplied {@code namePrefix} + * will be uppercased (in a locale-insensitive fashion) prior to + * the main logic of this method kicking in. + * @param namePrefix prefix of the constant names to search (may be {@code null}) + * @return the set of values + */ + public Set getValues(@Nullable String namePrefix) { + String prefixToUse = (namePrefix != null ? namePrefix.trim().toUpperCase(Locale.ENGLISH) : ""); + Set values = new HashSet<>(); + this.fieldCache.forEach((code, value) -> { + if (code.startsWith(prefixToUse)) { + values.add(value); + } + }); + return values; + } + + /** + * Return all values of the group of constants for the + * given bean property name. + * @param propertyName the name of the bean property + * @return the set of values + * @see #propertyToConstantNamePrefix + */ + public Set getValuesForProperty(String propertyName) { + return getValues(propertyToConstantNamePrefix(propertyName)); + } + + /** + * Return all values of the given group of constants. + *

    Note that this method assumes that constants are named + * in accordance with the standard Java convention for constant + * values (i.e. all uppercase). The supplied {@code nameSuffix} + * will be uppercased (in a locale-insensitive fashion) prior to + * the main logic of this method kicking in. + * @param nameSuffix suffix of the constant names to search (may be {@code null}) + * @return the set of values + */ + public Set getValuesForSuffix(@Nullable String nameSuffix) { + String suffixToUse = (nameSuffix != null ? nameSuffix.trim().toUpperCase(Locale.ENGLISH) : ""); + Set values = new HashSet<>(); + this.fieldCache.forEach((code, value) -> { + if (code.endsWith(suffixToUse)) { + values.add(value); + } + }); + return values; + } + + + /** + * Look up the given value within the given group of constants. + *

    Will return the first match. + * @param value constant value to look up + * @param namePrefix prefix of the constant names to search (may be {@code null}) + * @return the name of the constant field + * @throws ConstantException if the value wasn't found + */ + public String toCode(Object value, @Nullable String namePrefix) throws ConstantException { + String prefixToUse = (namePrefix != null ? namePrefix.trim().toUpperCase(Locale.ENGLISH) : ""); + for (Map.Entry entry : this.fieldCache.entrySet()) { + if (entry.getKey().startsWith(prefixToUse) && entry.getValue().equals(value)) { + return entry.getKey(); + } + } + throw new ConstantException(this.className, prefixToUse, value); + } + + /** + * Look up the given value within the group of constants for + * the given bean property name. Will return the first match. + * @param value constant value to look up + * @param propertyName the name of the bean property + * @return the name of the constant field + * @throws ConstantException if the value wasn't found + * @see #propertyToConstantNamePrefix + */ + public String toCodeForProperty(Object value, String propertyName) throws ConstantException { + return toCode(value, propertyToConstantNamePrefix(propertyName)); + } + + /** + * Look up the given value within the given group of constants. + *

    Will return the first match. + * @param value constant value to look up + * @param nameSuffix suffix of the constant names to search (may be {@code null}) + * @return the name of the constant field + * @throws ConstantException if the value wasn't found + */ + public String toCodeForSuffix(Object value, @Nullable String nameSuffix) throws ConstantException { + String suffixToUse = (nameSuffix != null ? nameSuffix.trim().toUpperCase(Locale.ENGLISH) : ""); + for (Map.Entry entry : this.fieldCache.entrySet()) { + if (entry.getKey().endsWith(suffixToUse) && entry.getValue().equals(value)) { + return entry.getKey(); + } + } + throw new ConstantException(this.className, suffixToUse, value); + } + + + /** + * Convert the given bean property name to a constant name prefix. + *

    Uses a common naming idiom: turning all lower case characters to + * upper case, and prepending upper case characters with an underscore. + *

    Example: "imageSize" -> "IMAGE_SIZE"
    + * Example: "imagesize" -> "IMAGESIZE".
    + * Example: "ImageSize" -> "_IMAGE_SIZE".
    + * Example: "IMAGESIZE" -> "_I_M_A_G_E_S_I_Z_E" + * @param propertyName the name of the bean property + * @return the corresponding constant name prefix + * @see #getValuesForProperty + * @see #toCodeForProperty + */ + public String propertyToConstantNamePrefix(String propertyName) { + StringBuilder parsedPrefix = new StringBuilder(); + for (int i = 0; i < propertyName.length(); i++) { + char c = propertyName.charAt(i); + if (Character.isUpperCase(c)) { + parsedPrefix.append("_"); + parsedPrefix.append(c); + } + else { + parsedPrefix.append(Character.toUpperCase(c)); + } + } + return parsedPrefix.toString(); + } + + + /** + * Exception thrown when the {@link Constants} class is asked for + * an invalid constant name. + */ + @SuppressWarnings("serial") + public static class ConstantException extends IllegalArgumentException { + + /** + * Thrown when an invalid constant name is requested. + * @param className name of the class containing the constant definitions + * @param field invalid constant name + * @param message description of the problem + */ + public ConstantException(String className, String field, String message) { + super("Field '" + field + "' " + message + " in class [" + className + "]"); + } + + /** + * Thrown when an invalid constant value is looked up. + * @param className name of the class containing the constant definitions + * @param namePrefix prefix of the searched constant names + * @param value the looked up constant value + */ + public ConstantException(String className, String namePrefix, Object value) { + super("No '" + namePrefix + "' field with value '" + value + "' found in class [" + className + "]"); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/Conventions.java b/spring-core/src/main/java/org/springframework/core/Conventions.java new file mode 100644 index 0000000..70daea7 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/Conventions.java @@ -0,0 +1,311 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Collection; +import java.util.Iterator; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Provides methods to support various naming and other conventions used + * throughout the framework. Mainly for internal use within the framework. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Rossen Stoyanchev + * @since 2.0 + */ +public final class Conventions { + + /** + * Suffix added to names when using arrays. + */ + private static final String PLURAL_SUFFIX = "List"; + + + private Conventions() { + } + + + /** + * Determine the conventional variable name for the supplied {@code Object} + * based on its concrete type. The convention used is to return the + * un-capitalized short name of the {@code Class}, according to JavaBeans + * property naming rules. + *

    For example:
    + * {@code com.myapp.Product} becomes {@code "product"}
    + * {@code com.myapp.MyProduct} becomes {@code "myProduct"}
    + * {@code com.myapp.UKProduct} becomes {@code "UKProduct"}
    + *

    For arrays the pluralized version of the array component type is used. + * For {@code Collection}s an attempt is made to 'peek ahead' to determine + * the component type and return its pluralized version. + * @param value the value to generate a variable name for + * @return the generated variable name + */ + public static String getVariableName(Object value) { + Assert.notNull(value, "Value must not be null"); + Class valueClass; + boolean pluralize = false; + + if (value.getClass().isArray()) { + valueClass = value.getClass().getComponentType(); + pluralize = true; + } + else if (value instanceof Collection) { + Collection collection = (Collection) value; + if (collection.isEmpty()) { + throw new IllegalArgumentException( + "Cannot generate variable name for an empty Collection"); + } + Object valueToCheck = peekAhead(collection); + valueClass = getClassForValue(valueToCheck); + pluralize = true; + } + else { + valueClass = getClassForValue(value); + } + + String name = ClassUtils.getShortNameAsProperty(valueClass); + return (pluralize ? pluralize(name) : name); + } + + /** + * Determine the conventional variable name for the given parameter taking + * the generic collection type, if any, into account. + *

    As of 5.0 this method supports reactive types:
    + * {@code Mono} becomes {@code "productMono"}
    + * {@code Flux} becomes {@code "myProductFlux"}
    + * {@code Observable} becomes {@code "myProductObservable"}
    + * @param parameter the method or constructor parameter + * @return the generated variable name + */ + public static String getVariableNameForParameter(MethodParameter parameter) { + Assert.notNull(parameter, "MethodParameter must not be null"); + Class valueClass; + boolean pluralize = false; + String reactiveSuffix = ""; + + if (parameter.getParameterType().isArray()) { + valueClass = parameter.getParameterType().getComponentType(); + pluralize = true; + } + else if (Collection.class.isAssignableFrom(parameter.getParameterType())) { + valueClass = ResolvableType.forMethodParameter(parameter).asCollection().resolveGeneric(); + if (valueClass == null) { + throw new IllegalArgumentException( + "Cannot generate variable name for non-typed Collection parameter type"); + } + pluralize = true; + } + else { + valueClass = parameter.getParameterType(); + ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(valueClass); + if (adapter != null && !adapter.getDescriptor().isNoValue()) { + reactiveSuffix = ClassUtils.getShortName(valueClass); + valueClass = parameter.nested().getNestedParameterType(); + } + } + + String name = ClassUtils.getShortNameAsProperty(valueClass); + return (pluralize ? pluralize(name) : name + reactiveSuffix); + } + + /** + * Determine the conventional variable name for the return type of the + * given method, taking the generic collection type, if any, into account. + * @param method the method to generate a variable name for + * @return the generated variable name + */ + public static String getVariableNameForReturnType(Method method) { + return getVariableNameForReturnType(method, method.getReturnType(), null); + } + + /** + * Determine the conventional variable name for the return type of the given + * method, taking the generic collection type, if any, into account, falling + * back on the given actual return value if the method declaration is not + * specific enough, e.g. {@code Object} return type or untyped collection. + * @param method the method to generate a variable name for + * @param value the return value (may be {@code null} if not available) + * @return the generated variable name + */ + public static String getVariableNameForReturnType(Method method, @Nullable Object value) { + return getVariableNameForReturnType(method, method.getReturnType(), value); + } + + /** + * Determine the conventional variable name for the return type of the given + * method, taking the generic collection type, if any, into account, falling + * back on the given return value if the method declaration is not specific + * enough, e.g. {@code Object} return type or untyped collection. + *

    As of 5.0 this method supports reactive types:
    + * {@code Mono} becomes {@code "productMono"}
    + * {@code Flux} becomes {@code "myProductFlux"}
    + * {@code Observable} becomes {@code "myProductObservable"}
    + * @param method the method to generate a variable name for + * @param resolvedType the resolved return type of the method + * @param value the return value (may be {@code null} if not available) + * @return the generated variable name + */ + public static String getVariableNameForReturnType(Method method, Class resolvedType, @Nullable Object value) { + Assert.notNull(method, "Method must not be null"); + + if (Object.class == resolvedType) { + if (value == null) { + throw new IllegalArgumentException( + "Cannot generate variable name for an Object return type with null value"); + } + return getVariableName(value); + } + + Class valueClass; + boolean pluralize = false; + String reactiveSuffix = ""; + + if (resolvedType.isArray()) { + valueClass = resolvedType.getComponentType(); + pluralize = true; + } + else if (Collection.class.isAssignableFrom(resolvedType)) { + valueClass = ResolvableType.forMethodReturnType(method).asCollection().resolveGeneric(); + if (valueClass == null) { + if (!(value instanceof Collection)) { + throw new IllegalArgumentException("Cannot generate variable name " + + "for non-typed Collection return type and a non-Collection value"); + } + Collection collection = (Collection) value; + if (collection.isEmpty()) { + throw new IllegalArgumentException("Cannot generate variable name " + + "for non-typed Collection return type and an empty Collection value"); + } + Object valueToCheck = peekAhead(collection); + valueClass = getClassForValue(valueToCheck); + } + pluralize = true; + } + else { + valueClass = resolvedType; + ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(valueClass); + if (adapter != null && !adapter.getDescriptor().isNoValue()) { + reactiveSuffix = ClassUtils.getShortName(valueClass); + valueClass = ResolvableType.forMethodReturnType(method).getGeneric().toClass(); + } + } + + String name = ClassUtils.getShortNameAsProperty(valueClass); + return (pluralize ? pluralize(name) : name + reactiveSuffix); + } + + /** + * Convert {@code String}s in attribute name format (e.g. lowercase, hyphens + * separating words) into property name format (camel-case). For example + * {@code transaction-manager} becomes {@code "transactionManager"}. + */ + public static String attributeNameToPropertyName(String attributeName) { + Assert.notNull(attributeName, "'attributeName' must not be null"); + if (!attributeName.contains("-")) { + return attributeName; + } + char[] result = new char[attributeName.length() -1]; // not completely accurate but good guess + int currPos = 0; + boolean upperCaseNext = false; + for (int i = 0; i < attributeName.length(); i++ ) { + char c = attributeName.charAt(i); + if (c == '-') { + upperCaseNext = true; + } + else if (upperCaseNext) { + result[currPos++] = Character.toUpperCase(c); + upperCaseNext = false; + } + else { + result[currPos++] = c; + } + } + return new String(result, 0, currPos); + } + + /** + * Return an attribute name qualified by the given enclosing {@link Class}. + * For example the attribute name '{@code foo}' qualified by {@link Class} + * '{@code com.myapp.SomeClass}' would be '{@code com.myapp.SomeClass.foo}' + */ + public static String getQualifiedAttributeName(Class enclosingClass, String attributeName) { + Assert.notNull(enclosingClass, "'enclosingClass' must not be null"); + Assert.notNull(attributeName, "'attributeName' must not be null"); + return enclosingClass.getName() + '.' + attributeName; + } + + + /** + * Determine the class to use for naming a variable containing the given value. + *

    Will return the class of the given value, except when encountering a + * JDK proxy, in which case it will determine the 'primary' interface + * implemented by that proxy. + * @param value the value to check + * @return the class to use for naming a variable + */ + private static Class getClassForValue(Object value) { + Class valueClass = value.getClass(); + if (Proxy.isProxyClass(valueClass)) { + Class[] ifcs = valueClass.getInterfaces(); + for (Class ifc : ifcs) { + if (!ClassUtils.isJavaLanguageInterface(ifc)) { + return ifc; + } + } + } + else if (valueClass.getName().lastIndexOf('$') != -1 && valueClass.getDeclaringClass() == null) { + // '$' in the class name but no inner class - + // assuming it's a special subclass (e.g. by OpenJPA) + valueClass = valueClass.getSuperclass(); + } + return valueClass; + } + + /** + * Pluralize the given name. + */ + private static String pluralize(String name) { + return name + PLURAL_SUFFIX; + } + + /** + * Retrieve the {@code Class} of an element in the {@code Collection}. + * The exact element for which the {@code Class} is retrieved will depend + * on the concrete {@code Collection} implementation. + */ + private static E peekAhead(Collection collection) { + Iterator it = collection.iterator(); + if (!it.hasNext()) { + throw new IllegalStateException( + "Unable to peek ahead in non-empty collection - no element found"); + } + E value = it.next(); + if (value == null) { + throw new IllegalStateException( + "Unable to peek ahead in non-empty collection - only null element found"); + } + return value; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/DecoratingClassLoader.java b/spring-core/src/main/java/org/springframework/core/DecoratingClassLoader.java new file mode 100644 index 0000000..a8563ca --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/DecoratingClassLoader.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Base class for decorating ClassLoaders such as {@link OverridingClassLoader} + * and {@link org.springframework.instrument.classloading.ShadowingClassLoader}, + * providing common handling of excluded packages and classes. + * + * @author Juergen Hoeller + * @author Rod Johnson + * @since 2.5.2 + */ +public abstract class DecoratingClassLoader extends ClassLoader { + + static { + ClassLoader.registerAsParallelCapable(); + } + + + private final Set excludedPackages = Collections.newSetFromMap(new ConcurrentHashMap<>(8)); + + private final Set excludedClasses = Collections.newSetFromMap(new ConcurrentHashMap<>(8)); + + + /** + * Create a new DecoratingClassLoader with no parent ClassLoader. + */ + public DecoratingClassLoader() { + } + + /** + * Create a new DecoratingClassLoader using the given parent ClassLoader + * for delegation. + */ + public DecoratingClassLoader(@Nullable ClassLoader parent) { + super(parent); + } + + + /** + * Add a package name to exclude from decoration (e.g. overriding). + *

    Any class whose fully-qualified name starts with the name registered + * here will be handled by the parent ClassLoader in the usual fashion. + * @param packageName the package name to exclude + */ + public void excludePackage(String packageName) { + Assert.notNull(packageName, "Package name must not be null"); + this.excludedPackages.add(packageName); + } + + /** + * Add a class name to exclude from decoration (e.g. overriding). + *

    Any class name registered here will be handled by the parent + * ClassLoader in the usual fashion. + * @param className the class name to exclude + */ + public void excludeClass(String className) { + Assert.notNull(className, "Class name must not be null"); + this.excludedClasses.add(className); + } + + /** + * Determine whether the specified class is excluded from decoration + * by this class loader. + *

    The default implementation checks against excluded packages and classes. + * @param className the class name to check + * @return whether the specified class is eligible + * @see #excludePackage + * @see #excludeClass + */ + protected boolean isExcluded(String className) { + if (this.excludedClasses.contains(className)) { + return true; + } + for (String packageName : this.excludedPackages) { + if (className.startsWith(packageName)) { + return true; + } + } + return false; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/DecoratingProxy.java b/spring-core/src/main/java/org/springframework/core/DecoratingProxy.java new file mode 100644 index 0000000..c3fb621 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/DecoratingProxy.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +/** + * Interface to be implemented by decorating proxies, in particular Spring AOP + * proxies but potentially also custom proxies with decorator semantics. + * + *

    Note that this interface should just be implemented if the decorated class + * is not within the hierarchy of the proxy class to begin with. In particular, + * a "target-class" proxy such as a Spring AOP CGLIB proxy should not implement + * it since any lookup on the target class can simply be performed on the proxy + * class there anyway. + * + *

    Defined in the core module in order to allow + * {@link org.springframework.core.annotation.AnnotationAwareOrderComparator} + * (and potential other candidates without spring-aop dependencies) to use it + * for introspection purposes, in particular annotation lookups. + * + * @author Juergen Hoeller + * @since 4.3 + */ +public interface DecoratingProxy { + + /** + * Return the (ultimate) decorated class behind this proxy. + *

    In case of an AOP proxy, this will be the ultimate target class, + * not just the immediate target (in case of multiple nested proxies). + * @return the decorated class (never {@code null}) + */ + Class getDecoratedClass(); + +} diff --git a/spring-core/src/main/java/org/springframework/core/DefaultParameterNameDiscoverer.java b/spring-core/src/main/java/org/springframework/core/DefaultParameterNameDiscoverer.java new file mode 100644 index 0000000..283e5ba --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/DefaultParameterNameDiscoverer.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +/** + * Default implementation of the {@link ParameterNameDiscoverer} strategy interface, + * using the Java 8 standard reflection mechanism (if available), and falling back + * to the ASM-based {@link LocalVariableTableParameterNameDiscoverer} for checking + * debug information in the class file. + * + *

    If a Kotlin reflection implementation is present, + * {@link KotlinReflectionParameterNameDiscoverer} is added first in the list and + * used for Kotlin classes and interfaces. When compiling or running as a GraalVM + * native image, the {@code KotlinReflectionParameterNameDiscoverer} is not used. + * + *

    Further discoverers may be added through {@link #addDiscoverer(ParameterNameDiscoverer)}. + * + * @author Juergen Hoeller + * @author Sebastien Deleuze + * @author Sam Brannen + * @since 4.0 + * @see StandardReflectionParameterNameDiscoverer + * @see LocalVariableTableParameterNameDiscoverer + * @see KotlinReflectionParameterNameDiscoverer + */ +public class DefaultParameterNameDiscoverer extends PrioritizedParameterNameDiscoverer { + + /** + * Whether this environment lives within a native image. + * Exposed as a private static field rather than in a {@code NativeImageDetector.inNativeImage()} static method due to https://github.com/oracle/graal/issues/2594. + * @see ImageInfo.java + */ + private static final boolean IN_NATIVE_IMAGE = (System.getProperty("org.graalvm.nativeimage.imagecode") != null); + + public DefaultParameterNameDiscoverer() { + if (KotlinDetector.isKotlinReflectPresent() && !IN_NATIVE_IMAGE) { + addDiscoverer(new KotlinReflectionParameterNameDiscoverer()); + } + addDiscoverer(new StandardReflectionParameterNameDiscoverer()); + addDiscoverer(new LocalVariableTableParameterNameDiscoverer()); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/ExceptionDepthComparator.java b/spring-core/src/main/java/org/springframework/core/ExceptionDepthComparator.java new file mode 100644 index 0000000..e34d7f2 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/ExceptionDepthComparator.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; + +import org.springframework.util.Assert; + +/** + * Comparator capable of sorting exceptions based on their depth from the thrown exception type. + * + * @author Juergen Hoeller + * @author Arjen Poutsma + * @since 3.0.3 + */ +public class ExceptionDepthComparator implements Comparator> { + + private final Class targetException; + + + /** + * Create a new ExceptionDepthComparator for the given exception. + * @param exception the target exception to compare to when sorting by depth + */ + public ExceptionDepthComparator(Throwable exception) { + Assert.notNull(exception, "Target exception must not be null"); + this.targetException = exception.getClass(); + } + + /** + * Create a new ExceptionDepthComparator for the given exception type. + * @param exceptionType the target exception type to compare to when sorting by depth + */ + public ExceptionDepthComparator(Class exceptionType) { + Assert.notNull(exceptionType, "Target exception type must not be null"); + this.targetException = exceptionType; + } + + + @Override + public int compare(Class o1, Class o2) { + int depth1 = getDepth(o1, this.targetException, 0); + int depth2 = getDepth(o2, this.targetException, 0); + return (depth1 - depth2); + } + + private int getDepth(Class declaredException, Class exceptionToMatch, int depth) { + if (exceptionToMatch.equals(declaredException)) { + // Found it! + return depth; + } + // If we've gone as far as we can go and haven't found it... + if (exceptionToMatch == Throwable.class) { + return Integer.MAX_VALUE; + } + return getDepth(declaredException, exceptionToMatch.getSuperclass(), depth + 1); + } + + + /** + * Obtain the closest match from the given exception types for the given target exception. + * @param exceptionTypes the collection of exception types + * @param targetException the target exception to find a match for + * @return the closest matching exception type from the given collection + */ + public static Class findClosestMatch( + Collection> exceptionTypes, Throwable targetException) { + + Assert.notEmpty(exceptionTypes, "Exception types must not be empty"); + if (exceptionTypes.size() == 1) { + return exceptionTypes.iterator().next(); + } + List> handledExceptions = new ArrayList<>(exceptionTypes); + handledExceptions.sort(new ExceptionDepthComparator(targetException)); + return handledExceptions.get(0); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java b/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java new file mode 100644 index 0000000..353b88f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java @@ -0,0 +1,303 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ConcurrentReferenceHashMap; + +/** + * Helper class for resolving generic types against type variables. + * + *

    Mainly intended for usage within the framework, resolving method + * parameter types even when they are declared generically. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @author Sam Brannen + * @author Phillip Webb + * @since 2.5.2 + */ +public final class GenericTypeResolver { + + /** Cache from Class to TypeVariable Map. */ + @SuppressWarnings("rawtypes") + private static final Map, Map> typeVariableCache = new ConcurrentReferenceHashMap<>(); + + + private GenericTypeResolver() { + } + + + /** + * Determine the target type for the given generic parameter type. + * @param methodParameter the method parameter specification + * @param implementationClass the class to resolve type variables against + * @return the corresponding generic parameter or return type + * @deprecated since 5.2 in favor of {@code methodParameter.withContainingClass(implementationClass).getParameterType()} + */ + @Deprecated + public static Class resolveParameterType(MethodParameter methodParameter, Class implementationClass) { + Assert.notNull(methodParameter, "MethodParameter must not be null"); + Assert.notNull(implementationClass, "Class must not be null"); + methodParameter.setContainingClass(implementationClass); + return methodParameter.getParameterType(); + } + + /** + * Determine the target type for the generic return type of the given method, + * where formal type variables are declared on the given class. + * @param method the method to introspect + * @param clazz the class to resolve type variables against + * @return the corresponding generic parameter or return type + */ + public static Class resolveReturnType(Method method, Class clazz) { + Assert.notNull(method, "Method must not be null"); + Assert.notNull(clazz, "Class must not be null"); + return ResolvableType.forMethodReturnType(method, clazz).resolve(method.getReturnType()); + } + + /** + * Resolve the single type argument of the given generic interface against the given + * target method which is assumed to return the given interface or an implementation + * of it. + * @param method the target method to check the return type of + * @param genericIfc the generic interface or superclass to resolve the type argument from + * @return the resolved parameter type of the method return type, or {@code null} + * if not resolvable or if the single argument is of type {@link WildcardType}. + */ + @Nullable + public static Class resolveReturnTypeArgument(Method method, Class genericIfc) { + Assert.notNull(method, "Method must not be null"); + ResolvableType resolvableType = ResolvableType.forMethodReturnType(method).as(genericIfc); + if (!resolvableType.hasGenerics() || resolvableType.getType() instanceof WildcardType) { + return null; + } + return getSingleGeneric(resolvableType); + } + + /** + * Resolve the single type argument of the given generic interface against + * the given target class which is assumed to implement the generic interface + * and possibly declare a concrete type for its type variable. + * @param clazz the target class to check against + * @param genericIfc the generic interface or superclass to resolve the type argument from + * @return the resolved type of the argument, or {@code null} if not resolvable + */ + @Nullable + public static Class resolveTypeArgument(Class clazz, Class genericIfc) { + ResolvableType resolvableType = ResolvableType.forClass(clazz).as(genericIfc); + if (!resolvableType.hasGenerics()) { + return null; + } + return getSingleGeneric(resolvableType); + } + + @Nullable + private static Class getSingleGeneric(ResolvableType resolvableType) { + Assert.isTrue(resolvableType.getGenerics().length == 1, + () -> "Expected 1 type argument on generic interface [" + resolvableType + + "] but found " + resolvableType.getGenerics().length); + return resolvableType.getGeneric().resolve(); + } + + + /** + * Resolve the type arguments of the given generic interface against the given + * target class which is assumed to implement the generic interface and possibly + * declare concrete types for its type variables. + * @param clazz the target class to check against + * @param genericIfc the generic interface or superclass to resolve the type argument from + * @return the resolved type of each argument, with the array size matching the + * number of actual type arguments, or {@code null} if not resolvable + */ + @Nullable + public static Class[] resolveTypeArguments(Class clazz, Class genericIfc) { + ResolvableType type = ResolvableType.forClass(clazz).as(genericIfc); + if (!type.hasGenerics() || type.isEntirelyUnresolvable()) { + return null; + } + return type.resolveGenerics(Object.class); + } + + /** + * Resolve the given generic type against the given context class, + * substituting type variables as far as possible. + * @param genericType the (potentially) generic type + * @param contextClass a context class for the target type, for example a class + * in which the target type appears in a method signature (can be {@code null}) + * @return the resolved type (possibly the given generic type as-is) + * @since 5.0 + */ + public static Type resolveType(Type genericType, @Nullable Class contextClass) { + if (contextClass != null) { + if (genericType instanceof TypeVariable) { + ResolvableType resolvedTypeVariable = resolveVariable( + (TypeVariable) genericType, ResolvableType.forClass(contextClass)); + if (resolvedTypeVariable != ResolvableType.NONE) { + Class resolved = resolvedTypeVariable.resolve(); + if (resolved != null) { + return resolved; + } + } + } + else if (genericType instanceof ParameterizedType) { + ResolvableType resolvedType = ResolvableType.forType(genericType); + if (resolvedType.hasUnresolvableGenerics()) { + ParameterizedType parameterizedType = (ParameterizedType) genericType; + Class[] generics = new Class[parameterizedType.getActualTypeArguments().length]; + Type[] typeArguments = parameterizedType.getActualTypeArguments(); + ResolvableType contextType = ResolvableType.forClass(contextClass); + for (int i = 0; i < typeArguments.length; i++) { + Type typeArgument = typeArguments[i]; + if (typeArgument instanceof TypeVariable) { + ResolvableType resolvedTypeArgument = resolveVariable( + (TypeVariable) typeArgument, contextType); + if (resolvedTypeArgument != ResolvableType.NONE) { + generics[i] = resolvedTypeArgument.resolve(); + } + else { + generics[i] = ResolvableType.forType(typeArgument).resolve(); + } + } + else { + generics[i] = ResolvableType.forType(typeArgument).resolve(); + } + } + Class rawClass = resolvedType.getRawClass(); + if (rawClass != null) { + return ResolvableType.forClassWithGenerics(rawClass, generics).getType(); + } + } + } + } + return genericType; + } + + private static ResolvableType resolveVariable(TypeVariable typeVariable, ResolvableType contextType) { + ResolvableType resolvedType; + if (contextType.hasGenerics()) { + resolvedType = ResolvableType.forType(typeVariable, contextType); + if (resolvedType.resolve() != null) { + return resolvedType; + } + } + + ResolvableType superType = contextType.getSuperType(); + if (superType != ResolvableType.NONE) { + resolvedType = resolveVariable(typeVariable, superType); + if (resolvedType.resolve() != null) { + return resolvedType; + } + } + for (ResolvableType ifc : contextType.getInterfaces()) { + resolvedType = resolveVariable(typeVariable, ifc); + if (resolvedType.resolve() != null) { + return resolvedType; + } + } + return ResolvableType.NONE; + } + + /** + * Resolve the specified generic type against the given TypeVariable map. + *

    Used by Spring Data. + * @param genericType the generic type to resolve + * @param map the TypeVariable Map to resolved against + * @return the type if it resolves to a Class, or {@code Object.class} otherwise + */ + @SuppressWarnings("rawtypes") + public static Class resolveType(Type genericType, Map map) { + return ResolvableType.forType(genericType, new TypeVariableMapVariableResolver(map)).toClass(); + } + + /** + * Build a mapping of {@link TypeVariable#getName TypeVariable names} to + * {@link Class concrete classes} for the specified {@link Class}. + * Searches all super types, enclosing types and interfaces. + * @see #resolveType(Type, Map) + */ + @SuppressWarnings("rawtypes") + public static Map getTypeVariableMap(Class clazz) { + Map typeVariableMap = typeVariableCache.get(clazz); + if (typeVariableMap == null) { + typeVariableMap = new HashMap<>(); + buildTypeVariableMap(ResolvableType.forClass(clazz), typeVariableMap); + typeVariableCache.put(clazz, Collections.unmodifiableMap(typeVariableMap)); + } + return typeVariableMap; + } + + @SuppressWarnings("rawtypes") + private static void buildTypeVariableMap(ResolvableType type, Map typeVariableMap) { + if (type != ResolvableType.NONE) { + Class resolved = type.resolve(); + if (resolved != null && type.getType() instanceof ParameterizedType) { + TypeVariable[] variables = resolved.getTypeParameters(); + for (int i = 0; i < variables.length; i++) { + ResolvableType generic = type.getGeneric(i); + while (generic.getType() instanceof TypeVariable) { + generic = generic.resolveType(); + } + if (generic != ResolvableType.NONE) { + typeVariableMap.put(variables[i], generic.getType()); + } + } + } + buildTypeVariableMap(type.getSuperType(), typeVariableMap); + for (ResolvableType interfaceType : type.getInterfaces()) { + buildTypeVariableMap(interfaceType, typeVariableMap); + } + if (resolved != null && resolved.isMemberClass()) { + buildTypeVariableMap(ResolvableType.forClass(resolved.getEnclosingClass()), typeVariableMap); + } + } + } + + + @SuppressWarnings({"serial", "rawtypes"}) + private static class TypeVariableMapVariableResolver implements ResolvableType.VariableResolver { + + private final Map typeVariableMap; + + public TypeVariableMapVariableResolver(Map typeVariableMap) { + this.typeVariableMap = typeVariableMap; + } + + @Override + @Nullable + public ResolvableType resolveVariable(TypeVariable variable) { + Type type = this.typeVariableMap.get(variable); + return (type != null ? ResolvableType.forType(type) : null); + } + + @Override + public Object getSource() { + return this.typeVariableMap; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/InfrastructureProxy.java b/spring-core/src/main/java/org/springframework/core/InfrastructureProxy.java new file mode 100644 index 0000000..68f301d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/InfrastructureProxy.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +/** + * Interface to be implemented by transparent resource proxies that need to be + * considered as equal to the underlying resource, for example for consistent + * lookup key comparisons. Note that this interface does imply such special + * semantics and does not constitute a general-purpose mixin! + * + *

    Such wrappers will automatically be unwrapped for key comparisons in + * {@link org.springframework.transaction.support.TransactionSynchronizationManager}. + * + *

    Only fully transparent proxies, e.g. for redirection or service lookups, + * are supposed to implement this interface. Proxies that decorate the target + * object with new behavior, such as AOP proxies, do not qualify here! + * + * @author Juergen Hoeller + * @since 2.5.4 + * @see org.springframework.transaction.support.TransactionSynchronizationManager + */ +public interface InfrastructureProxy { + + /** + * Return the underlying resource (never {@code null}). + */ + Object getWrappedObject(); + +} diff --git a/spring-core/src/main/java/org/springframework/core/KotlinDetector.java b/spring-core/src/main/java/org/springframework/core/KotlinDetector.java new file mode 100644 index 0000000..8a6c072 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/KotlinDetector.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * A common delegate for detecting Kotlin's presence and for identifying Kotlin types. + * + * @author Juergen Hoeller + * @author Sebastien Deleuze + * @since 5.0 + */ +@SuppressWarnings("unchecked") +public abstract class KotlinDetector { + + @Nullable + private static final Class kotlinMetadata; + + private static final boolean kotlinReflectPresent; + + static { + Class metadata; + ClassLoader classLoader = KotlinDetector.class.getClassLoader(); + try { + metadata = ClassUtils.forName("kotlin.Metadata", classLoader); + } + catch (ClassNotFoundException ex) { + // Kotlin API not available - no Kotlin support + metadata = null; + } + kotlinMetadata = (Class) metadata; + kotlinReflectPresent = ClassUtils.isPresent("kotlin.reflect.full.KClasses", classLoader); + } + + + /** + * Determine whether Kotlin is present in general. + */ + public static boolean isKotlinPresent() { + return (kotlinMetadata != null); + } + + /** + * Determine whether Kotlin reflection is present. + * @since 5.1 + */ + public static boolean isKotlinReflectPresent() { + return kotlinReflectPresent; + } + + /** + * Determine whether the given {@code Class} is a Kotlin type + * (with Kotlin metadata present on it). + */ + public static boolean isKotlinType(Class clazz) { + return (kotlinMetadata != null && clazz.getDeclaredAnnotation(kotlinMetadata) != null); + } + + /** + * Return {@code true} if the method is a suspending function. + * @since 5.3 + */ + public static boolean isSuspendingFunction(Method method) { + if (KotlinDetector.isKotlinType(method.getDeclaringClass())) { + Class[] types = method.getParameterTypes(); + if (types.length > 0 && "kotlin.coroutines.Continuation".equals(types[types.length - 1].getName())) { + return true; + } + } + return false; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/KotlinReflectionParameterNameDiscoverer.java b/spring-core/src/main/java/org/springframework/core/KotlinReflectionParameterNameDiscoverer.java new file mode 100644 index 0000000..0170043 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/KotlinReflectionParameterNameDiscoverer.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.List; +import java.util.stream.Collectors; + +import kotlin.reflect.KFunction; +import kotlin.reflect.KParameter; +import kotlin.reflect.jvm.ReflectJvmMapping; + +import org.springframework.lang.Nullable; + +/** + * {@link ParameterNameDiscoverer} implementation which uses Kotlin's reflection facilities + * for introspecting parameter names. + * + * Compared to {@link StandardReflectionParameterNameDiscoverer}, it allows in addition to + * determine interface parameter names without requiring Java 8 -parameters compiler flag. + * + * @author Sebastien Deleuze + * @since 5.0 + */ +public class KotlinReflectionParameterNameDiscoverer implements ParameterNameDiscoverer { + + @Override + @Nullable + public String[] getParameterNames(Method method) { + if (!KotlinDetector.isKotlinType(method.getDeclaringClass())) { + return null; + } + + try { + KFunction function = ReflectJvmMapping.getKotlinFunction(method); + return (function != null ? getParameterNames(function.getParameters()) : null); + } + catch (UnsupportedOperationException ex) { + return null; + } + } + + @Override + @Nullable + public String[] getParameterNames(Constructor ctor) { + if (ctor.getDeclaringClass().isEnum() || !KotlinDetector.isKotlinType(ctor.getDeclaringClass())) { + return null; + } + + try { + KFunction function = ReflectJvmMapping.getKotlinFunction(ctor); + return (function != null ? getParameterNames(function.getParameters()) : null); + } + catch (UnsupportedOperationException ex) { + return null; + } + } + + @Nullable + private String[] getParameterNames(List parameters) { + List filteredParameters = parameters + .stream() + // Extension receivers of extension methods must be included as they appear as normal method parameters in Java + .filter(p -> KParameter.Kind.VALUE.equals(p.getKind()) || KParameter.Kind.EXTENSION_RECEIVER.equals(p.getKind())) + .collect(Collectors.toList()); + String[] parameterNames = new String[filteredParameters.size()]; + for (int i = 0; i < filteredParameters.size(); i++) { + KParameter parameter = filteredParameters.get(i); + // extension receivers are not explicitly named, but require a name for Java interoperability + // $receiver is not a valid Kotlin identifier, but valid in Java, so it can be used here + String name = KParameter.Kind.EXTENSION_RECEIVER.equals(parameter.getKind()) ? "$receiver" : parameter.getName(); + if (name == null) { + return null; + } + parameterNames[i] = name; + } + return parameterNames; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/LocalVariableTableParameterNameDiscoverer.java b/spring-core/src/main/java/org/springframework/core/LocalVariableTableParameterNameDiscoverer.java new file mode 100644 index 0000000..6498a35 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/LocalVariableTableParameterNameDiscoverer.java @@ -0,0 +1,268 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.asm.ClassReader; +import org.springframework.asm.ClassVisitor; +import org.springframework.asm.Label; +import org.springframework.asm.MethodVisitor; +import org.springframework.asm.Opcodes; +import org.springframework.asm.SpringAsmInfo; +import org.springframework.asm.Type; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * Implementation of {@link ParameterNameDiscoverer} that uses the LocalVariableTable + * information in the method attributes to discover parameter names. Returns + * {@code null} if the class file was compiled without debug information. + * + *

    Uses ObjectWeb's ASM library for analyzing class files. Each discoverer instance + * caches the ASM discovered information for each introspected Class, in a thread-safe + * manner. It is recommended to reuse ParameterNameDiscoverer instances as far as possible. + * + * @author Adrian Colyer + * @author Costin Leau + * @author Juergen Hoeller + * @author Chris Beams + * @author Sam Brannen + * @since 2.0 + */ +public class LocalVariableTableParameterNameDiscoverer implements ParameterNameDiscoverer { + + private static final Log logger = LogFactory.getLog(LocalVariableTableParameterNameDiscoverer.class); + + // marker object for classes that do not have any debug info + private static final Map NO_DEBUG_INFO_MAP = Collections.emptyMap(); + + // the cache uses a nested index (value is a map) to keep the top level cache relatively small in size + private final Map, Map> parameterNamesCache = new ConcurrentHashMap<>(32); + + + @Override + @Nullable + public String[] getParameterNames(Method method) { + Method originalMethod = BridgeMethodResolver.findBridgedMethod(method); + return doGetParameterNames(originalMethod); + } + + @Override + @Nullable + public String[] getParameterNames(Constructor ctor) { + return doGetParameterNames(ctor); + } + + @Nullable + private String[] doGetParameterNames(Executable executable) { + Class declaringClass = executable.getDeclaringClass(); + Map map = this.parameterNamesCache.computeIfAbsent(declaringClass, this::inspectClass); + return (map != NO_DEBUG_INFO_MAP ? map.get(executable) : null); + } + + /** + * Inspects the target class. + *

    Exceptions will be logged, and a marker map returned to indicate the + * lack of debug information. + */ + private Map inspectClass(Class clazz) { + InputStream is = clazz.getResourceAsStream(ClassUtils.getClassFileName(clazz)); + if (is == null) { + // We couldn't load the class file, which is not fatal as it + // simply means this method of discovering parameter names won't work. + if (logger.isDebugEnabled()) { + logger.debug("Cannot find '.class' file for class [" + clazz + + "] - unable to determine constructor/method parameter names"); + } + return NO_DEBUG_INFO_MAP; + } + try { + ClassReader classReader = new ClassReader(is); + Map map = new ConcurrentHashMap<>(32); + classReader.accept(new ParameterNameDiscoveringVisitor(clazz, map), 0); + return map; + } + catch (IOException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Exception thrown while reading '.class' file for class [" + clazz + + "] - unable to determine constructor/method parameter names", ex); + } + } + catch (IllegalArgumentException ex) { + if (logger.isDebugEnabled()) { + logger.debug("ASM ClassReader failed to parse class file [" + clazz + + "], probably due to a new Java class file version that isn't supported yet " + + "- unable to determine constructor/method parameter names", ex); + } + } + finally { + try { + is.close(); + } + catch (IOException ex) { + // ignore + } + } + return NO_DEBUG_INFO_MAP; + } + + + /** + * Helper class that inspects all methods and constructors and then + * attempts to find the parameter names for the given {@link Executable}. + */ + private static class ParameterNameDiscoveringVisitor extends ClassVisitor { + + private static final String STATIC_CLASS_INIT = ""; + + private final Class clazz; + + private final Map executableMap; + + public ParameterNameDiscoveringVisitor(Class clazz, Map executableMap) { + super(SpringAsmInfo.ASM_VERSION); + this.clazz = clazz; + this.executableMap = executableMap; + } + + @Override + @Nullable + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + // exclude synthetic + bridged && static class initialization + if (!isSyntheticOrBridged(access) && !STATIC_CLASS_INIT.equals(name)) { + return new LocalVariableTableVisitor(this.clazz, this.executableMap, name, desc, isStatic(access)); + } + return null; + } + + private static boolean isSyntheticOrBridged(int access) { + return (((access & Opcodes.ACC_SYNTHETIC) | (access & Opcodes.ACC_BRIDGE)) > 0); + } + + private static boolean isStatic(int access) { + return ((access & Opcodes.ACC_STATIC) > 0); + } + } + + + private static class LocalVariableTableVisitor extends MethodVisitor { + + private static final String CONSTRUCTOR = ""; + + private final Class clazz; + + private final Map executableMap; + + private final String name; + + private final Type[] args; + + private final String[] parameterNames; + + private final boolean isStatic; + + private boolean hasLvtInfo = false; + + /* + * The nth entry contains the slot index of the LVT table entry holding the + * argument name for the nth parameter. + */ + private final int[] lvtSlotIndex; + + public LocalVariableTableVisitor(Class clazz, Map map, String name, String desc, boolean isStatic) { + super(SpringAsmInfo.ASM_VERSION); + this.clazz = clazz; + this.executableMap = map; + this.name = name; + this.args = Type.getArgumentTypes(desc); + this.parameterNames = new String[this.args.length]; + this.isStatic = isStatic; + this.lvtSlotIndex = computeLvtSlotIndices(isStatic, this.args); + } + + @Override + public void visitLocalVariable(String name, String description, String signature, Label start, Label end, int index) { + this.hasLvtInfo = true; + for (int i = 0; i < this.lvtSlotIndex.length; i++) { + if (this.lvtSlotIndex[i] == index) { + this.parameterNames[i] = name; + } + } + } + + @Override + public void visitEnd() { + if (this.hasLvtInfo || (this.isStatic && this.parameterNames.length == 0)) { + // visitLocalVariable will never be called for static no args methods + // which doesn't use any local variables. + // This means that hasLvtInfo could be false for that kind of methods + // even if the class has local variable info. + this.executableMap.put(resolveExecutable(), this.parameterNames); + } + } + + private Executable resolveExecutable() { + ClassLoader loader = this.clazz.getClassLoader(); + Class[] argTypes = new Class[this.args.length]; + for (int i = 0; i < this.args.length; i++) { + argTypes[i] = ClassUtils.resolveClassName(this.args[i].getClassName(), loader); + } + try { + if (CONSTRUCTOR.equals(this.name)) { + return this.clazz.getDeclaredConstructor(argTypes); + } + return this.clazz.getDeclaredMethod(this.name, argTypes); + } + catch (NoSuchMethodException ex) { + throw new IllegalStateException("Method [" + this.name + + "] was discovered in the .class file but cannot be resolved in the class object", ex); + } + } + + private static int[] computeLvtSlotIndices(boolean isStatic, Type[] paramTypes) { + int[] lvtIndex = new int[paramTypes.length]; + int nextIndex = (isStatic ? 0 : 1); + for (int i = 0; i < paramTypes.length; i++) { + lvtIndex[i] = nextIndex; + if (isWideType(paramTypes[i])) { + nextIndex += 2; + } + else { + nextIndex++; + } + } + return lvtIndex; + } + + private static boolean isWideType(Type aType) { + // float is not a wide type + return (aType == Type.LONG_TYPE || aType == Type.DOUBLE_TYPE); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/MethodClassKey.java b/spring-core/src/main/java/org/springframework/core/MethodClassKey.java new file mode 100644 index 0000000..62ca1ea --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/MethodClassKey.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.lang.reflect.Method; + +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +/** + * A common key class for a method against a specific target class, + * including {@link #toString()} representation and {@link Comparable} + * support (as suggested for custom {@code HashMap} keys as of Java 8). + * + * @author Juergen Hoeller + * @since 4.3 + */ +public final class MethodClassKey implements Comparable { + + private final Method method; + + @Nullable + private final Class targetClass; + + + /** + * Create a key object for the given method and target class. + * @param method the method to wrap (must not be {@code null}) + * @param targetClass the target class that the method will be invoked + * on (may be {@code null} if identical to the declaring class) + */ + public MethodClassKey(Method method, @Nullable Class targetClass) { + this.method = method; + this.targetClass = targetClass; + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof MethodClassKey)) { + return false; + } + MethodClassKey otherKey = (MethodClassKey) other; + return (this.method.equals(otherKey.method) && + ObjectUtils.nullSafeEquals(this.targetClass, otherKey.targetClass)); + } + + @Override + public int hashCode() { + return this.method.hashCode() + (this.targetClass != null ? this.targetClass.hashCode() * 29 : 0); + } + + @Override + public String toString() { + return this.method + (this.targetClass != null ? " on " + this.targetClass : ""); + } + + @Override + public int compareTo(MethodClassKey other) { + int result = this.method.getName().compareTo(other.method.getName()); + if (result == 0) { + result = this.method.toString().compareTo(other.method.toString()); + if (result == 0 && this.targetClass != null && other.targetClass != null) { + result = this.targetClass.getName().compareTo(other.targetClass.getName()); + } + } + return result; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java b/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java new file mode 100644 index 0000000..2947945 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java @@ -0,0 +1,159 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * Defines the algorithm for searching for metadata-associated methods exhaustively + * including interfaces and parent classes while also dealing with parameterized methods + * as well as common scenarios encountered with interface and class-based proxies. + * + *

    Typically, but not necessarily, used for finding annotated handler methods. + * + * @author Juergen Hoeller + * @author Rossen Stoyanchev + * @since 4.2.3 + */ +public final class MethodIntrospector { + + private MethodIntrospector() { + } + + + /** + * Select methods on the given target type based on the lookup of associated metadata. + *

    Callers define methods of interest through the {@link MetadataLookup} parameter, + * allowing to collect the associated metadata into the result map. + * @param targetType the target type to search methods on + * @param metadataLookup a {@link MetadataLookup} callback to inspect methods of interest, + * returning non-null metadata to be associated with a given method if there is a match, + * or {@code null} for no match + * @return the selected methods associated with their metadata (in the order of retrieval), + * or an empty map in case of no match + */ + public static Map selectMethods(Class targetType, final MetadataLookup metadataLookup) { + final Map methodMap = new LinkedHashMap<>(); + Set> handlerTypes = new LinkedHashSet<>(); + Class specificHandlerType = null; + + if (!Proxy.isProxyClass(targetType)) { + specificHandlerType = ClassUtils.getUserClass(targetType); + handlerTypes.add(specificHandlerType); + } + handlerTypes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetType)); + + for (Class currentHandlerType : handlerTypes) { + final Class targetClass = (specificHandlerType != null ? specificHandlerType : currentHandlerType); + + ReflectionUtils.doWithMethods(currentHandlerType, method -> { + Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass); + T result = metadataLookup.inspect(specificMethod); + if (result != null) { + Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); + if (bridgedMethod == specificMethod || metadataLookup.inspect(bridgedMethod) == null) { + methodMap.put(specificMethod, result); + } + } + }, ReflectionUtils.USER_DECLARED_METHODS); + } + + return methodMap; + } + + /** + * Select methods on the given target type based on a filter. + *

    Callers define methods of interest through the {@code MethodFilter} parameter. + * @param targetType the target type to search methods on + * @param methodFilter a {@code MethodFilter} to help + * recognize handler methods of interest + * @return the selected methods, or an empty set in case of no match + */ + public static Set selectMethods(Class targetType, final ReflectionUtils.MethodFilter methodFilter) { + return selectMethods(targetType, + (MetadataLookup) method -> (methodFilter.matches(method) ? Boolean.TRUE : null)).keySet(); + } + + /** + * Select an invocable method on the target type: either the given method itself + * if actually exposed on the target type, or otherwise a corresponding method + * on one of the target type's interfaces or on the target type itself. + *

    Matches on user-declared interfaces will be preferred since they are likely + * to contain relevant metadata that corresponds to the method on the target class. + * @param method the method to check + * @param targetType the target type to search methods on + * (typically an interface-based JDK proxy) + * @return a corresponding invocable method on the target type + * @throws IllegalStateException if the given method is not invocable on the given + * target type (typically due to a proxy mismatch) + */ + public static Method selectInvocableMethod(Method method, Class targetType) { + if (method.getDeclaringClass().isAssignableFrom(targetType)) { + return method; + } + try { + String methodName = method.getName(); + Class[] parameterTypes = method.getParameterTypes(); + for (Class ifc : targetType.getInterfaces()) { + try { + return ifc.getMethod(methodName, parameterTypes); + } + catch (NoSuchMethodException ex) { + // Alright, not on this interface then... + } + } + // A final desperate attempt on the proxy class itself... + return targetType.getMethod(methodName, parameterTypes); + } + catch (NoSuchMethodException ex) { + throw new IllegalStateException(String.format( + "Need to invoke method '%s' declared on target class '%s', " + + "but not found in any interface(s) of the exposed proxy type. " + + "Either pull the method up to an interface or switch to CGLIB " + + "proxies by enforcing proxy-target-class mode in your configuration.", + method.getName(), method.getDeclaringClass().getSimpleName())); + } + } + + + /** + * A callback interface for metadata lookup on a given method. + * @param the type of metadata returned + */ + @FunctionalInterface + public interface MetadataLookup { + + /** + * Perform a lookup on the given method and return associated metadata, if any. + * @param method the method to inspect + * @return non-null metadata to be associated with a method if there is a match, + * or {@code null} for no match + */ + @Nullable + T inspect(Method method); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/MethodParameter.java b/spring-core/src/main/java/org/springframework/core/MethodParameter.java new file mode 100644 index 0000000..42a0dfc --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/MethodParameter.java @@ -0,0 +1,948 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.Predicate; + +import kotlin.Unit; +import kotlin.reflect.KFunction; +import kotlin.reflect.KParameter; +import kotlin.reflect.jvm.ReflectJvmMapping; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; + +/** + * Helper class that encapsulates the specification of a method parameter, i.e. a {@link Method} + * or {@link Constructor} plus a parameter index and a nested type index for a declared generic + * type. Useful as a specification object to pass along. + * + *

    As of 4.2, there is a {@link org.springframework.core.annotation.SynthesizingMethodParameter} + * subclass available which synthesizes annotations with attribute aliases. That subclass is used + * for web and message endpoint processing, in particular. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @author Andy Clement + * @author Sam Brannen + * @author Sebastien Deleuze + * @author Phillip Webb + * @since 2.0 + * @see org.springframework.core.annotation.SynthesizingMethodParameter + */ +public class MethodParameter { + + private static final Annotation[] EMPTY_ANNOTATION_ARRAY = new Annotation[0]; + + + private final Executable executable; + + private final int parameterIndex; + + @Nullable + private volatile Parameter parameter; + + private int nestingLevel; + + /** Map from Integer level to Integer type index. */ + @Nullable + Map typeIndexesPerLevel; + + /** The containing class. Could also be supplied by overriding {@link #getContainingClass()} */ + @Nullable + private volatile Class containingClass; + + @Nullable + private volatile Class parameterType; + + @Nullable + private volatile Type genericParameterType; + + @Nullable + private volatile Annotation[] parameterAnnotations; + + @Nullable + private volatile ParameterNameDiscoverer parameterNameDiscoverer; + + @Nullable + private volatile String parameterName; + + @Nullable + private volatile MethodParameter nestedMethodParameter; + + + /** + * Create a new {@code MethodParameter} for the given method, with nesting level 1. + * @param method the Method to specify a parameter for + * @param parameterIndex the index of the parameter: -1 for the method + * return type; 0 for the first method parameter; 1 for the second method + * parameter, etc. + */ + public MethodParameter(Method method, int parameterIndex) { + this(method, parameterIndex, 1); + } + + /** + * Create a new {@code MethodParameter} for the given method. + * @param method the Method to specify a parameter for + * @param parameterIndex the index of the parameter: -1 for the method + * return type; 0 for the first method parameter; 1 for the second method + * parameter, etc. + * @param nestingLevel the nesting level of the target type + * (typically 1; e.g. in case of a List of Lists, 1 would indicate the + * nested List, whereas 2 would indicate the element of the nested List) + */ + public MethodParameter(Method method, int parameterIndex, int nestingLevel) { + Assert.notNull(method, "Method must not be null"); + this.executable = method; + this.parameterIndex = validateIndex(method, parameterIndex); + this.nestingLevel = nestingLevel; + } + + /** + * Create a new MethodParameter for the given constructor, with nesting level 1. + * @param constructor the Constructor to specify a parameter for + * @param parameterIndex the index of the parameter + */ + public MethodParameter(Constructor constructor, int parameterIndex) { + this(constructor, parameterIndex, 1); + } + + /** + * Create a new MethodParameter for the given constructor. + * @param constructor the Constructor to specify a parameter for + * @param parameterIndex the index of the parameter + * @param nestingLevel the nesting level of the target type + * (typically 1; e.g. in case of a List of Lists, 1 would indicate the + * nested List, whereas 2 would indicate the element of the nested List) + */ + public MethodParameter(Constructor constructor, int parameterIndex, int nestingLevel) { + Assert.notNull(constructor, "Constructor must not be null"); + this.executable = constructor; + this.parameterIndex = validateIndex(constructor, parameterIndex); + this.nestingLevel = nestingLevel; + } + + /** + * Internal constructor used to create a {@link MethodParameter} with a + * containing class already set. + * @param executable the Executable to specify a parameter for + * @param parameterIndex the index of the parameter + * @param containingClass the containing class + * @since 5.2 + */ + MethodParameter(Executable executable, int parameterIndex, @Nullable Class containingClass) { + Assert.notNull(executable, "Executable must not be null"); + this.executable = executable; + this.parameterIndex = validateIndex(executable, parameterIndex); + this.nestingLevel = 1; + this.containingClass = containingClass; + } + + /** + * Copy constructor, resulting in an independent MethodParameter object + * based on the same metadata and cache state that the original object was in. + * @param original the original MethodParameter object to copy from + */ + public MethodParameter(MethodParameter original) { + Assert.notNull(original, "Original must not be null"); + this.executable = original.executable; + this.parameterIndex = original.parameterIndex; + this.parameter = original.parameter; + this.nestingLevel = original.nestingLevel; + this.typeIndexesPerLevel = original.typeIndexesPerLevel; + this.containingClass = original.containingClass; + this.parameterType = original.parameterType; + this.genericParameterType = original.genericParameterType; + this.parameterAnnotations = original.parameterAnnotations; + this.parameterNameDiscoverer = original.parameterNameDiscoverer; + this.parameterName = original.parameterName; + } + + + /** + * Return the wrapped Method, if any. + *

    Note: Either Method or Constructor is available. + * @return the Method, or {@code null} if none + */ + @Nullable + public Method getMethod() { + return (this.executable instanceof Method ? (Method) this.executable : null); + } + + /** + * Return the wrapped Constructor, if any. + *

    Note: Either Method or Constructor is available. + * @return the Constructor, or {@code null} if none + */ + @Nullable + public Constructor getConstructor() { + return (this.executable instanceof Constructor ? (Constructor) this.executable : null); + } + + /** + * Return the class that declares the underlying Method or Constructor. + */ + public Class getDeclaringClass() { + return this.executable.getDeclaringClass(); + } + + /** + * Return the wrapped member. + * @return the Method or Constructor as Member + */ + public Member getMember() { + return this.executable; + } + + /** + * Return the wrapped annotated element. + *

    Note: This method exposes the annotations declared on the method/constructor + * itself (i.e. at the method/constructor level, not at the parameter level). + * @return the Method or Constructor as AnnotatedElement + */ + public AnnotatedElement getAnnotatedElement() { + return this.executable; + } + + /** + * Return the wrapped executable. + * @return the Method or Constructor as Executable + * @since 5.0 + */ + public Executable getExecutable() { + return this.executable; + } + + /** + * Return the {@link Parameter} descriptor for method/constructor parameter. + * @since 5.0 + */ + public Parameter getParameter() { + if (this.parameterIndex < 0) { + throw new IllegalStateException("Cannot retrieve Parameter descriptor for method return type"); + } + Parameter parameter = this.parameter; + if (parameter == null) { + parameter = getExecutable().getParameters()[this.parameterIndex]; + this.parameter = parameter; + } + return parameter; + } + + /** + * Return the index of the method/constructor parameter. + * @return the parameter index (-1 in case of the return type) + */ + public int getParameterIndex() { + return this.parameterIndex; + } + + /** + * Increase this parameter's nesting level. + * @see #getNestingLevel() + * @deprecated since 5.2 in favor of {@link #nested(Integer)} + */ + @Deprecated + public void increaseNestingLevel() { + this.nestingLevel++; + } + + /** + * Decrease this parameter's nesting level. + * @see #getNestingLevel() + * @deprecated since 5.2 in favor of retaining the original MethodParameter and + * using {@link #nested(Integer)} if nesting is required + */ + @Deprecated + public void decreaseNestingLevel() { + getTypeIndexesPerLevel().remove(this.nestingLevel); + this.nestingLevel--; + } + + /** + * Return the nesting level of the target type + * (typically 1; e.g. in case of a List of Lists, 1 would indicate the + * nested List, whereas 2 would indicate the element of the nested List). + */ + public int getNestingLevel() { + return this.nestingLevel; + } + + /** + * Return a variant of this {@code MethodParameter} with the type + * for the current level set to the specified value. + * @param typeIndex the new type index + * @since 5.2 + */ + public MethodParameter withTypeIndex(int typeIndex) { + return nested(this.nestingLevel, typeIndex); + } + + /** + * Set the type index for the current nesting level. + * @param typeIndex the corresponding type index + * (or {@code null} for the default type index) + * @see #getNestingLevel() + * @deprecated since 5.2 in favor of {@link #withTypeIndex} + */ + @Deprecated + public void setTypeIndexForCurrentLevel(int typeIndex) { + getTypeIndexesPerLevel().put(this.nestingLevel, typeIndex); + } + + /** + * Return the type index for the current nesting level. + * @return the corresponding type index, or {@code null} + * if none specified (indicating the default type index) + * @see #getNestingLevel() + */ + @Nullable + public Integer getTypeIndexForCurrentLevel() { + return getTypeIndexForLevel(this.nestingLevel); + } + + /** + * Return the type index for the specified nesting level. + * @param nestingLevel the nesting level to check + * @return the corresponding type index, or {@code null} + * if none specified (indicating the default type index) + */ + @Nullable + public Integer getTypeIndexForLevel(int nestingLevel) { + return getTypeIndexesPerLevel().get(nestingLevel); + } + + /** + * Obtain the (lazily constructed) type-indexes-per-level Map. + */ + private Map getTypeIndexesPerLevel() { + if (this.typeIndexesPerLevel == null) { + this.typeIndexesPerLevel = new HashMap<>(4); + } + return this.typeIndexesPerLevel; + } + + /** + * Return a variant of this {@code MethodParameter} which points to the + * same parameter but one nesting level deeper. + * @since 4.3 + */ + public MethodParameter nested() { + return nested(null); + } + + /** + * Return a variant of this {@code MethodParameter} which points to the + * same parameter but one nesting level deeper. + * @param typeIndex the type index for the new nesting level + * @since 5.2 + */ + public MethodParameter nested(@Nullable Integer typeIndex) { + MethodParameter nestedParam = this.nestedMethodParameter; + if (nestedParam != null && typeIndex == null) { + return nestedParam; + } + nestedParam = nested(this.nestingLevel + 1, typeIndex); + if (typeIndex == null) { + this.nestedMethodParameter = nestedParam; + } + return nestedParam; + } + + private MethodParameter nested(int nestingLevel, @Nullable Integer typeIndex) { + MethodParameter copy = clone(); + copy.nestingLevel = nestingLevel; + if (this.typeIndexesPerLevel != null) { + copy.typeIndexesPerLevel = new HashMap<>(this.typeIndexesPerLevel); + } + if (typeIndex != null) { + copy.getTypeIndexesPerLevel().put(copy.nestingLevel, typeIndex); + } + copy.parameterType = null; + copy.genericParameterType = null; + return copy; + } + + /** + * Return whether this method indicates a parameter which is not required: + * either in the form of Java 8's {@link java.util.Optional}, any variant + * of a parameter-level {@code Nullable} annotation (such as from JSR-305 + * or the FindBugs set of annotations), or a language-level nullable type + * declaration or {@code Continuation} parameter in Kotlin. + * @since 4.3 + */ + public boolean isOptional() { + return (getParameterType() == Optional.class || hasNullableAnnotation() || + (KotlinDetector.isKotlinReflectPresent() && + KotlinDetector.isKotlinType(getContainingClass()) && + KotlinDelegate.isOptional(this))); + } + + /** + * Check whether this method parameter is annotated with any variant of a + * {@code Nullable} annotation, e.g. {@code javax.annotation.Nullable} or + * {@code edu.umd.cs.findbugs.annotations.Nullable}. + */ + private boolean hasNullableAnnotation() { + for (Annotation ann : getParameterAnnotations()) { + if ("Nullable".equals(ann.annotationType().getSimpleName())) { + return true; + } + } + return false; + } + + /** + * Return a variant of this {@code MethodParameter} which points to + * the same parameter but one nesting level deeper in case of a + * {@link java.util.Optional} declaration. + * @since 4.3 + * @see #isOptional() + * @see #nested() + */ + public MethodParameter nestedIfOptional() { + return (getParameterType() == Optional.class ? nested() : this); + } + + /** + * Return a variant of this {@code MethodParameter} which refers to the + * given containing class. + * @param containingClass a specific containing class (potentially a + * subclass of the declaring class, e.g. substituting a type variable) + * @since 5.2 + * @see #getParameterType() + */ + public MethodParameter withContainingClass(@Nullable Class containingClass) { + MethodParameter result = clone(); + result.containingClass = containingClass; + result.parameterType = null; + return result; + } + + /** + * Set a containing class to resolve the parameter type against. + */ + @Deprecated + void setContainingClass(Class containingClass) { + this.containingClass = containingClass; + this.parameterType = null; + } + + /** + * Return the containing class for this method parameter. + * @return a specific containing class (potentially a subclass of the + * declaring class), or otherwise simply the declaring class itself + * @see #getDeclaringClass() + */ + public Class getContainingClass() { + Class containingClass = this.containingClass; + return (containingClass != null ? containingClass : getDeclaringClass()); + } + + /** + * Set a resolved (generic) parameter type. + */ + @Deprecated + void setParameterType(@Nullable Class parameterType) { + this.parameterType = parameterType; + } + + /** + * Return the type of the method/constructor parameter. + * @return the parameter type (never {@code null}) + */ + public Class getParameterType() { + Class paramType = this.parameterType; + if (paramType != null) { + return paramType; + } + if (getContainingClass() != getDeclaringClass()) { + paramType = ResolvableType.forMethodParameter(this, null, 1).resolve(); + } + if (paramType == null) { + paramType = computeParameterType(); + } + this.parameterType = paramType; + return paramType; + } + + /** + * Return the generic type of the method/constructor parameter. + * @return the parameter type (never {@code null}) + * @since 3.0 + */ + public Type getGenericParameterType() { + Type paramType = this.genericParameterType; + if (paramType == null) { + if (this.parameterIndex < 0) { + Method method = getMethod(); + paramType = (method != null ? + (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(getContainingClass()) ? + KotlinDelegate.getGenericReturnType(method) : method.getGenericReturnType()) : void.class); + } + else { + Type[] genericParameterTypes = this.executable.getGenericParameterTypes(); + int index = this.parameterIndex; + if (this.executable instanceof Constructor && + ClassUtils.isInnerClass(this.executable.getDeclaringClass()) && + genericParameterTypes.length == this.executable.getParameterCount() - 1) { + // Bug in javac: type array excludes enclosing instance parameter + // for inner classes with at least one generic constructor parameter, + // so access it with the actual parameter index lowered by 1 + index = this.parameterIndex - 1; + } + paramType = (index >= 0 && index < genericParameterTypes.length ? + genericParameterTypes[index] : computeParameterType()); + } + this.genericParameterType = paramType; + } + return paramType; + } + + private Class computeParameterType() { + if (this.parameterIndex < 0) { + Method method = getMethod(); + if (method == null) { + return void.class; + } + if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(getContainingClass())) { + return KotlinDelegate.getReturnType(method); + } + return method.getReturnType(); + } + return this.executable.getParameterTypes()[this.parameterIndex]; + } + + /** + * Return the nested type of the method/constructor parameter. + * @return the parameter type (never {@code null}) + * @since 3.1 + * @see #getNestingLevel() + */ + public Class getNestedParameterType() { + if (this.nestingLevel > 1) { + Type type = getGenericParameterType(); + for (int i = 2; i <= this.nestingLevel; i++) { + if (type instanceof ParameterizedType) { + Type[] args = ((ParameterizedType) type).getActualTypeArguments(); + Integer index = getTypeIndexForLevel(i); + type = args[index != null ? index : args.length - 1]; + } + // TODO: Object.class if unresolvable + } + if (type instanceof Class) { + return (Class) type; + } + else if (type instanceof ParameterizedType) { + Type arg = ((ParameterizedType) type).getRawType(); + if (arg instanceof Class) { + return (Class) arg; + } + } + return Object.class; + } + else { + return getParameterType(); + } + } + + /** + * Return the nested generic type of the method/constructor parameter. + * @return the parameter type (never {@code null}) + * @since 4.2 + * @see #getNestingLevel() + */ + public Type getNestedGenericParameterType() { + if (this.nestingLevel > 1) { + Type type = getGenericParameterType(); + for (int i = 2; i <= this.nestingLevel; i++) { + if (type instanceof ParameterizedType) { + Type[] args = ((ParameterizedType) type).getActualTypeArguments(); + Integer index = getTypeIndexForLevel(i); + type = args[index != null ? index : args.length - 1]; + } + } + return type; + } + else { + return getGenericParameterType(); + } + } + + /** + * Return the annotations associated with the target method/constructor itself. + */ + public Annotation[] getMethodAnnotations() { + return adaptAnnotationArray(getAnnotatedElement().getAnnotations()); + } + + /** + * Return the method/constructor annotation of the given type, if available. + * @param annotationType the annotation type to look for + * @return the annotation object, or {@code null} if not found + */ + @Nullable + public A getMethodAnnotation(Class annotationType) { + A annotation = getAnnotatedElement().getAnnotation(annotationType); + return (annotation != null ? adaptAnnotation(annotation) : null); + } + + /** + * Return whether the method/constructor is annotated with the given type. + * @param annotationType the annotation type to look for + * @since 4.3 + * @see #getMethodAnnotation(Class) + */ + public boolean hasMethodAnnotation(Class annotationType) { + return getAnnotatedElement().isAnnotationPresent(annotationType); + } + + /** + * Return the annotations associated with the specific method/constructor parameter. + */ + public Annotation[] getParameterAnnotations() { + Annotation[] paramAnns = this.parameterAnnotations; + if (paramAnns == null) { + Annotation[][] annotationArray = this.executable.getParameterAnnotations(); + int index = this.parameterIndex; + if (this.executable instanceof Constructor && + ClassUtils.isInnerClass(this.executable.getDeclaringClass()) && + annotationArray.length == this.executable.getParameterCount() - 1) { + // Bug in javac in JDK <9: annotation array excludes enclosing instance parameter + // for inner classes, so access it with the actual parameter index lowered by 1 + index = this.parameterIndex - 1; + } + paramAnns = (index >= 0 && index < annotationArray.length ? + adaptAnnotationArray(annotationArray[index]) : EMPTY_ANNOTATION_ARRAY); + this.parameterAnnotations = paramAnns; + } + return paramAnns; + } + + /** + * Return {@code true} if the parameter has at least one annotation, + * {@code false} if it has none. + * @see #getParameterAnnotations() + */ + public boolean hasParameterAnnotations() { + return (getParameterAnnotations().length != 0); + } + + /** + * Return the parameter annotation of the given type, if available. + * @param annotationType the annotation type to look for + * @return the annotation object, or {@code null} if not found + */ + @SuppressWarnings("unchecked") + @Nullable + public A getParameterAnnotation(Class annotationType) { + Annotation[] anns = getParameterAnnotations(); + for (Annotation ann : anns) { + if (annotationType.isInstance(ann)) { + return (A) ann; + } + } + return null; + } + + /** + * Return whether the parameter is declared with the given annotation type. + * @param annotationType the annotation type to look for + * @see #getParameterAnnotation(Class) + */ + public boolean hasParameterAnnotation(Class annotationType) { + return (getParameterAnnotation(annotationType) != null); + } + + /** + * Initialize parameter name discovery for this method parameter. + *

    This method does not actually try to retrieve the parameter name at + * this point; it just allows discovery to happen when the application calls + * {@link #getParameterName()} (if ever). + */ + public void initParameterNameDiscovery(@Nullable ParameterNameDiscoverer parameterNameDiscoverer) { + this.parameterNameDiscoverer = parameterNameDiscoverer; + } + + /** + * Return the name of the method/constructor parameter. + * @return the parameter name (may be {@code null} if no + * parameter name metadata is contained in the class file or no + * {@link #initParameterNameDiscovery ParameterNameDiscoverer} + * has been set to begin with) + */ + @Nullable + public String getParameterName() { + if (this.parameterIndex < 0) { + return null; + } + ParameterNameDiscoverer discoverer = this.parameterNameDiscoverer; + if (discoverer != null) { + String[] parameterNames = null; + if (this.executable instanceof Method) { + parameterNames = discoverer.getParameterNames((Method) this.executable); + } + else if (this.executable instanceof Constructor) { + parameterNames = discoverer.getParameterNames((Constructor) this.executable); + } + if (parameterNames != null) { + this.parameterName = parameterNames[this.parameterIndex]; + } + this.parameterNameDiscoverer = null; + } + return this.parameterName; + } + + + /** + * A template method to post-process a given annotation instance before + * returning it to the caller. + *

    The default implementation simply returns the given annotation as-is. + * @param annotation the annotation about to be returned + * @return the post-processed annotation (or simply the original one) + * @since 4.2 + */ + protected A adaptAnnotation(A annotation) { + return annotation; + } + + /** + * A template method to post-process a given annotation array before + * returning it to the caller. + *

    The default implementation simply returns the given annotation array as-is. + * @param annotations the annotation array about to be returned + * @return the post-processed annotation array (or simply the original one) + * @since 4.2 + */ + protected Annotation[] adaptAnnotationArray(Annotation[] annotations) { + return annotations; + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof MethodParameter)) { + return false; + } + MethodParameter otherParam = (MethodParameter) other; + return (getContainingClass() == otherParam.getContainingClass() && + ObjectUtils.nullSafeEquals(this.typeIndexesPerLevel, otherParam.typeIndexesPerLevel) && + this.nestingLevel == otherParam.nestingLevel && + this.parameterIndex == otherParam.parameterIndex && + this.executable.equals(otherParam.executable)); + } + + @Override + public int hashCode() { + return (31 * this.executable.hashCode() + this.parameterIndex); + } + + @Override + public String toString() { + Method method = getMethod(); + return (method != null ? "method '" + method.getName() + "'" : "constructor") + + " parameter " + this.parameterIndex; + } + + @Override + public MethodParameter clone() { + return new MethodParameter(this); + } + + /** + * Create a new MethodParameter for the given method or constructor. + *

    This is a convenience factory method for scenarios where a + * Method or Constructor reference is treated in a generic fashion. + * @param methodOrConstructor the Method or Constructor to specify a parameter for + * @param parameterIndex the index of the parameter + * @return the corresponding MethodParameter instance + * @deprecated as of 5.0, in favor of {@link #forExecutable} + */ + @Deprecated + public static MethodParameter forMethodOrConstructor(Object methodOrConstructor, int parameterIndex) { + if (!(methodOrConstructor instanceof Executable)) { + throw new IllegalArgumentException( + "Given object [" + methodOrConstructor + "] is neither a Method nor a Constructor"); + } + return forExecutable((Executable) methodOrConstructor, parameterIndex); + } + + /** + * Create a new MethodParameter for the given method or constructor. + *

    This is a convenience factory method for scenarios where a + * Method or Constructor reference is treated in a generic fashion. + * @param executable the Method or Constructor to specify a parameter for + * @param parameterIndex the index of the parameter + * @return the corresponding MethodParameter instance + * @since 5.0 + */ + public static MethodParameter forExecutable(Executable executable, int parameterIndex) { + if (executable instanceof Method) { + return new MethodParameter((Method) executable, parameterIndex); + } + else if (executable instanceof Constructor) { + return new MethodParameter((Constructor) executable, parameterIndex); + } + else { + throw new IllegalArgumentException("Not a Method/Constructor: " + executable); + } + } + + /** + * Create a new MethodParameter for the given parameter descriptor. + *

    This is a convenience factory method for scenarios where a + * Java 8 {@link Parameter} descriptor is already available. + * @param parameter the parameter descriptor + * @return the corresponding MethodParameter instance + * @since 5.0 + */ + public static MethodParameter forParameter(Parameter parameter) { + return forExecutable(parameter.getDeclaringExecutable(), findParameterIndex(parameter)); + } + + protected static int findParameterIndex(Parameter parameter) { + Executable executable = parameter.getDeclaringExecutable(); + Parameter[] allParams = executable.getParameters(); + // Try first with identity checks for greater performance. + for (int i = 0; i < allParams.length; i++) { + if (parameter == allParams[i]) { + return i; + } + } + // Potentially try again with object equality checks in order to avoid race + // conditions while invoking java.lang.reflect.Executable.getParameters(). + for (int i = 0; i < allParams.length; i++) { + if (parameter.equals(allParams[i])) { + return i; + } + } + throw new IllegalArgumentException("Given parameter [" + parameter + + "] does not match any parameter in the declaring executable"); + } + + private static int validateIndex(Executable executable, int parameterIndex) { + int count = executable.getParameterCount(); + Assert.isTrue(parameterIndex >= -1 && parameterIndex < count, + () -> "Parameter index needs to be between -1 and " + (count - 1)); + return parameterIndex; + } + + + /** + * Inner class to avoid a hard dependency on Kotlin at runtime. + */ + private static class KotlinDelegate { + + /** + * Check whether the specified {@link MethodParameter} represents a nullable Kotlin type, + * an optional parameter (with a default value in the Kotlin declaration) or a + * {@code Continuation} parameter used in suspending functions. + */ + public static boolean isOptional(MethodParameter param) { + Method method = param.getMethod(); + int index = param.getParameterIndex(); + if (method != null && index == -1) { + KFunction function = ReflectJvmMapping.getKotlinFunction(method); + return (function != null && function.getReturnType().isMarkedNullable()); + } + KFunction function; + Predicate predicate; + if (method != null) { + if (param.getParameterType().getName().equals("kotlin.coroutines.Continuation")) { + return true; + } + function = ReflectJvmMapping.getKotlinFunction(method); + predicate = p -> KParameter.Kind.VALUE.equals(p.getKind()); + } + else { + Constructor ctor = param.getConstructor(); + Assert.state(ctor != null, "Neither method nor constructor found"); + function = ReflectJvmMapping.getKotlinFunction(ctor); + predicate = p -> (KParameter.Kind.VALUE.equals(p.getKind()) || + KParameter.Kind.INSTANCE.equals(p.getKind())); + } + if (function != null) { + int i = 0; + for (KParameter kParameter : function.getParameters()) { + if (predicate.test(kParameter)) { + if (index == i++) { + return (kParameter.getType().isMarkedNullable() || kParameter.isOptional()); + } + } + } + } + return false; + } + + /** + * Return the generic return type of the method, with support of suspending + * functions via Kotlin reflection. + */ + private static Type getGenericReturnType(Method method) { + try { + KFunction function = ReflectJvmMapping.getKotlinFunction(method); + if (function != null && function.isSuspend()) { + return ReflectJvmMapping.getJavaType(function.getReturnType()); + } + } + catch (UnsupportedOperationException ex) { + // probably a synthetic class - let's use java reflection instead + } + return method.getGenericReturnType(); + } + + /** + * Return the return type of the method, with support of suspending + * functions via Kotlin reflection. + */ + private static Class getReturnType(Method method) { + try { + KFunction function = ReflectJvmMapping.getKotlinFunction(method); + if (function != null && function.isSuspend()) { + Type paramType = ReflectJvmMapping.getJavaType(function.getReturnType()); + if (paramType == Unit.class) { + paramType = void.class; + } + return ResolvableType.forType(paramType).resolve(method.getReturnType()); + } + } + catch (UnsupportedOperationException ex) { + // probably a synthetic class - let's use java reflection instead + } + return method.getReturnType(); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/NamedInheritableThreadLocal.java b/spring-core/src/main/java/org/springframework/core/NamedInheritableThreadLocal.java new file mode 100644 index 0000000..e3b8963 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/NamedInheritableThreadLocal.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import org.springframework.util.Assert; + +/** + * {@link InheritableThreadLocal} subclass that exposes a specified name + * as {@link #toString()} result (allowing for introspection). + * + * @author Juergen Hoeller + * @since 2.5.2 + * @param the value type + * @see NamedThreadLocal + */ +public class NamedInheritableThreadLocal extends InheritableThreadLocal { + + private final String name; + + + /** + * Create a new NamedInheritableThreadLocal with the given name. + * @param name a descriptive name for this ThreadLocal + */ + public NamedInheritableThreadLocal(String name) { + Assert.hasText(name, "Name must not be empty"); + this.name = name; + } + + @Override + public String toString() { + return this.name; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/NamedThreadLocal.java b/spring-core/src/main/java/org/springframework/core/NamedThreadLocal.java new file mode 100644 index 0000000..3d211a8 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/NamedThreadLocal.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import org.springframework.util.Assert; + +/** + * {@link ThreadLocal} subclass that exposes a specified name + * as {@link #toString()} result (allowing for introspection). + * + * @author Juergen Hoeller + * @since 2.5.2 + * @param the value type + * @see NamedInheritableThreadLocal + */ +public class NamedThreadLocal extends ThreadLocal { + + private final String name; + + + /** + * Create a new NamedThreadLocal with the given name. + * @param name a descriptive name for this ThreadLocal + */ + public NamedThreadLocal(String name) { + Assert.hasText(name, "Name must not be empty"); + this.name = name; + } + + @Override + public String toString() { + return this.name; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/NestedCheckedException.java b/spring-core/src/main/java/org/springframework/core/NestedCheckedException.java new file mode 100644 index 0000000..18987ff --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/NestedCheckedException.java @@ -0,0 +1,138 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import org.springframework.lang.Nullable; + +/** + * Handy class for wrapping checked {@code Exceptions} with a root cause. + * + *

    This class is {@code abstract} to force the programmer to extend + * the class. {@code getMessage} will include nested exception + * information; {@code printStackTrace} and other like methods will + * delegate to the wrapped exception, if any. + * + *

    The similarity between this class and the {@link NestedRuntimeException} + * class is unavoidable, as Java forces these two classes to have different + * superclasses (ah, the inflexibility of concrete inheritance!). + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #getMessage + * @see #printStackTrace + * @see NestedRuntimeException + */ +public abstract class NestedCheckedException extends Exception { + + /** Use serialVersionUID from Spring 1.2 for interoperability. */ + private static final long serialVersionUID = 7100714597678207546L; + + static { + // Eagerly load the NestedExceptionUtils class to avoid classloader deadlock + // issues on OSGi when calling getMessage(). Reported by Don Brown; SPR-5607. + NestedExceptionUtils.class.getName(); + } + + + /** + * Construct a {@code NestedCheckedException} with the specified detail message. + * @param msg the detail message + */ + public NestedCheckedException(String msg) { + super(msg); + } + + /** + * Construct a {@code NestedCheckedException} with the specified detail message + * and nested exception. + * @param msg the detail message + * @param cause the nested exception + */ + public NestedCheckedException(@Nullable String msg, @Nullable Throwable cause) { + super(msg, cause); + } + + + /** + * Return the detail message, including the message from the nested exception + * if there is one. + */ + @Override + @Nullable + public String getMessage() { + return NestedExceptionUtils.buildMessage(super.getMessage(), getCause()); + } + + + /** + * Retrieve the innermost cause of this exception, if any. + * @return the innermost exception, or {@code null} if none + */ + @Nullable + public Throwable getRootCause() { + return NestedExceptionUtils.getRootCause(this); + } + + /** + * Retrieve the most specific cause of this exception, that is, + * either the innermost cause (root cause) or this exception itself. + *

    Differs from {@link #getRootCause()} in that it falls back + * to the present exception if there is no root cause. + * @return the most specific cause (never {@code null}) + * @since 2.0.3 + */ + public Throwable getMostSpecificCause() { + Throwable rootCause = getRootCause(); + return (rootCause != null ? rootCause : this); + } + + /** + * Check whether this exception contains an exception of the given type: + * either it is of the given class itself or it contains a nested cause + * of the given type. + * @param exType the exception type to look for + * @return whether there is a nested exception of the specified type + */ + public boolean contains(@Nullable Class exType) { + if (exType == null) { + return false; + } + if (exType.isInstance(this)) { + return true; + } + Throwable cause = getCause(); + if (cause == this) { + return false; + } + if (cause instanceof NestedCheckedException) { + return ((NestedCheckedException) cause).contains(exType); + } + else { + while (cause != null) { + if (exType.isInstance(cause)) { + return true; + } + if (cause.getCause() == cause) { + break; + } + cause = cause.getCause(); + } + return false; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/NestedExceptionUtils.java b/spring-core/src/main/java/org/springframework/core/NestedExceptionUtils.java new file mode 100644 index 0000000..db879c3 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/NestedExceptionUtils.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import org.springframework.lang.Nullable; + +/** + * Helper class for implementing exception classes which are capable of + * holding nested exceptions. Necessary because we can't share a base + * class among different exception types. + * + *

    Mainly for use within the framework. + * + * @author Juergen Hoeller + * @since 2.0 + * @see NestedRuntimeException + * @see NestedCheckedException + * @see NestedIOException + * @see org.springframework.web.util.NestedServletException + */ +public abstract class NestedExceptionUtils { + + /** + * Build a message for the given base message and root cause. + * @param message the base message + * @param cause the root cause + * @return the full exception message + */ + @Nullable + public static String buildMessage(@Nullable String message, @Nullable Throwable cause) { + if (cause == null) { + return message; + } + StringBuilder sb = new StringBuilder(64); + if (message != null) { + sb.append(message).append("; "); + } + sb.append("nested exception is ").append(cause); + return sb.toString(); + } + + /** + * Retrieve the innermost cause of the given exception, if any. + * @param original the original exception to introspect + * @return the innermost exception, or {@code null} if none + * @since 4.3.9 + */ + @Nullable + public static Throwable getRootCause(@Nullable Throwable original) { + if (original == null) { + return null; + } + Throwable rootCause = null; + Throwable cause = original.getCause(); + while (cause != null && cause != rootCause) { + rootCause = cause; + cause = cause.getCause(); + } + return rootCause; + } + + /** + * Retrieve the most specific cause of the given exception, that is, + * either the innermost cause (root cause) or the exception itself. + *

    Differs from {@link #getRootCause} in that it falls back + * to the original exception if there is no root cause. + * @param original the original exception to introspect + * @return the most specific cause (never {@code null}) + * @since 4.3.9 + */ + public static Throwable getMostSpecificCause(Throwable original) { + Throwable rootCause = getRootCause(original); + return (rootCause != null ? rootCause : original); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/NestedIOException.java b/spring-core/src/main/java/org/springframework/core/NestedIOException.java new file mode 100644 index 0000000..37dbcf5 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/NestedIOException.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.io.IOException; + +import org.springframework.lang.Nullable; + +/** + * Subclass of {@link IOException} that properly handles a root cause, + * exposing the root cause just like NestedChecked/RuntimeException does. + * + *

    Proper root cause handling has not been added to standard IOException before + * Java 6, which is why we need to do it ourselves for Java 5 compatibility purposes. + * + *

    The similarity between this class and the NestedChecked/RuntimeException + * class is unavoidable, as this class needs to derive from IOException. + * + * @author Juergen Hoeller + * @since 2.0 + * @see #getMessage + * @see #printStackTrace + * @see org.springframework.core.NestedCheckedException + * @see org.springframework.core.NestedRuntimeException + */ +@SuppressWarnings("serial") +public class NestedIOException extends IOException { + + static { + // Eagerly load the NestedExceptionUtils class to avoid classloader deadlock + // issues on OSGi when calling getMessage(). Reported by Don Brown; SPR-5607. + NestedExceptionUtils.class.getName(); + } + + + /** + * Construct a {@code NestedIOException} with the specified detail message. + * @param msg the detail message + */ + public NestedIOException(String msg) { + super(msg); + } + + /** + * Construct a {@code NestedIOException} with the specified detail message + * and nested exception. + * @param msg the detail message + * @param cause the nested exception + */ + public NestedIOException(@Nullable String msg, @Nullable Throwable cause) { + super(msg, cause); + } + + + /** + * Return the detail message, including the message from the nested exception + * if there is one. + */ + @Override + @Nullable + public String getMessage() { + return NestedExceptionUtils.buildMessage(super.getMessage(), getCause()); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/NestedRuntimeException.java b/spring-core/src/main/java/org/springframework/core/NestedRuntimeException.java new file mode 100644 index 0000000..0549266 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/NestedRuntimeException.java @@ -0,0 +1,139 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import org.springframework.lang.Nullable; + +/** + * Handy class for wrapping runtime {@code Exceptions} with a root cause. + * + *

    This class is {@code abstract} to force the programmer to extend + * the class. {@code getMessage} will include nested exception + * information; {@code printStackTrace} and other like methods will + * delegate to the wrapped exception, if any. + * + *

    The similarity between this class and the {@link NestedCheckedException} + * class is unavoidable, as Java forces these two classes to have different + * superclasses (ah, the inflexibility of concrete inheritance!). + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #getMessage + * @see #printStackTrace + * @see NestedCheckedException + */ +public abstract class NestedRuntimeException extends RuntimeException { + + /** Use serialVersionUID from Spring 1.2 for interoperability. */ + private static final long serialVersionUID = 5439915454935047936L; + + static { + // Eagerly load the NestedExceptionUtils class to avoid classloader deadlock + // issues on OSGi when calling getMessage(). Reported by Don Brown; SPR-5607. + NestedExceptionUtils.class.getName(); + } + + + /** + * Construct a {@code NestedRuntimeException} with the specified detail message. + * @param msg the detail message + */ + public NestedRuntimeException(String msg) { + super(msg); + } + + /** + * Construct a {@code NestedRuntimeException} with the specified detail message + * and nested exception. + * @param msg the detail message + * @param cause the nested exception + */ + public NestedRuntimeException(@Nullable String msg, @Nullable Throwable cause) { + super(msg, cause); + } + + + /** + * Return the detail message, including the message from the nested exception + * if there is one. + */ + @Override + @Nullable + public String getMessage() { + return NestedExceptionUtils.buildMessage(super.getMessage(), getCause()); + } + + + /** + * Retrieve the innermost cause of this exception, if any. + * @return the innermost exception, or {@code null} if none + * @since 2.0 + */ + @Nullable + public Throwable getRootCause() { + return NestedExceptionUtils.getRootCause(this); + } + + /** + * Retrieve the most specific cause of this exception, that is, + * either the innermost cause (root cause) or this exception itself. + *

    Differs from {@link #getRootCause()} in that it falls back + * to the present exception if there is no root cause. + * @return the most specific cause (never {@code null}) + * @since 2.0.3 + */ + public Throwable getMostSpecificCause() { + Throwable rootCause = getRootCause(); + return (rootCause != null ? rootCause : this); + } + + /** + * Check whether this exception contains an exception of the given type: + * either it is of the given class itself or it contains a nested cause + * of the given type. + * @param exType the exception type to look for + * @return whether there is a nested exception of the specified type + */ + public boolean contains(@Nullable Class exType) { + if (exType == null) { + return false; + } + if (exType.isInstance(this)) { + return true; + } + Throwable cause = getCause(); + if (cause == this) { + return false; + } + if (cause instanceof NestedRuntimeException) { + return ((NestedRuntimeException) cause).contains(exType); + } + else { + while (cause != null) { + if (exType.isInstance(cause)) { + return true; + } + if (cause.getCause() == cause) { + break; + } + cause = cause.getCause(); + } + return false; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/OrderComparator.java b/spring-core/src/main/java/org/springframework/core/OrderComparator.java new file mode 100644 index 0000000..a478d6e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/OrderComparator.java @@ -0,0 +1,229 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +/** + * {@link Comparator} implementation for {@link Ordered} objects, sorting + * by order value ascending, respectively by priority descending. + * + *

    {@code PriorityOrdered} Objects

    + *

    {@link PriorityOrdered} objects will be sorted with higher priority than + * plain {@code Ordered} objects. + * + *

    Same Order Objects

    + *

    Objects that have the same order value will be sorted with arbitrary + * ordering with respect to other objects with the same order value. + * + *

    Non-ordered Objects

    + *

    Any object that does not provide its own order value is implicitly + * assigned a value of {@link Ordered#LOWEST_PRECEDENCE}, thus ending up + * at the end of a sorted collection in arbitrary order with respect to + * other objects with the same order value. + * + * @author Juergen Hoeller + * @author Sam Brannen + * @since 07.04.2003 + * @see Ordered + * @see PriorityOrdered + * @see org.springframework.core.annotation.AnnotationAwareOrderComparator + * @see java.util.List#sort(java.util.Comparator) + * @see java.util.Arrays#sort(Object[], java.util.Comparator) + */ +public class OrderComparator implements Comparator { + + /** + * Shared default instance of {@code OrderComparator}. + */ + public static final OrderComparator INSTANCE = new OrderComparator(); + + + /** + * Build an adapted order comparator with the given source provider. + * @param sourceProvider the order source provider to use + * @return the adapted comparator + * @since 4.1 + */ + public Comparator withSourceProvider(OrderSourceProvider sourceProvider) { + return (o1, o2) -> doCompare(o1, o2, sourceProvider); + } + + @Override + public int compare(@Nullable Object o1, @Nullable Object o2) { + return doCompare(o1, o2, null); + } + + private int doCompare(@Nullable Object o1, @Nullable Object o2, @Nullable OrderSourceProvider sourceProvider) { + boolean p1 = (o1 instanceof PriorityOrdered); + boolean p2 = (o2 instanceof PriorityOrdered); + if (p1 && !p2) { + return -1; + } + else if (p2 && !p1) { + return 1; + } + + int i1 = getOrder(o1, sourceProvider); + int i2 = getOrder(o2, sourceProvider); + return Integer.compare(i1, i2); + } + + /** + * Determine the order value for the given object. + *

    The default implementation checks against the given {@link OrderSourceProvider} + * using {@link #findOrder} and falls back to a regular {@link #getOrder(Object)} call. + * @param obj the object to check + * @return the order value, or {@code Ordered.LOWEST_PRECEDENCE} as fallback + */ + private int getOrder(@Nullable Object obj, @Nullable OrderSourceProvider sourceProvider) { + Integer order = null; + if (obj != null && sourceProvider != null) { + Object orderSource = sourceProvider.getOrderSource(obj); + if (orderSource != null) { + if (orderSource.getClass().isArray()) { + for (Object source : ObjectUtils.toObjectArray(orderSource)) { + order = findOrder(source); + if (order != null) { + break; + } + } + } + else { + order = findOrder(orderSource); + } + } + } + return (order != null ? order : getOrder(obj)); + } + + /** + * Determine the order value for the given object. + *

    The default implementation checks against the {@link Ordered} interface + * through delegating to {@link #findOrder}. Can be overridden in subclasses. + * @param obj the object to check + * @return the order value, or {@code Ordered.LOWEST_PRECEDENCE} as fallback + */ + protected int getOrder(@Nullable Object obj) { + if (obj != null) { + Integer order = findOrder(obj); + if (order != null) { + return order; + } + } + return Ordered.LOWEST_PRECEDENCE; + } + + /** + * Find an order value indicated by the given object. + *

    The default implementation checks against the {@link Ordered} interface. + * Can be overridden in subclasses. + * @param obj the object to check + * @return the order value, or {@code null} if none found + */ + @Nullable + protected Integer findOrder(Object obj) { + return (obj instanceof Ordered ? ((Ordered) obj).getOrder() : null); + } + + /** + * Determine a priority value for the given object, if any. + *

    The default implementation always returns {@code null}. + * Subclasses may override this to give specific kinds of values a + * 'priority' characteristic, in addition to their 'order' semantics. + * A priority indicates that it may be used for selecting one object over + * another, in addition to serving for ordering purposes in a list/array. + * @param obj the object to check + * @return the priority value, or {@code null} if none + * @since 4.1 + */ + @Nullable + public Integer getPriority(Object obj) { + return null; + } + + + /** + * Sort the given List with a default OrderComparator. + *

    Optimized to skip sorting for lists with size 0 or 1, + * in order to avoid unnecessary array extraction. + * @param list the List to sort + * @see java.util.List#sort(java.util.Comparator) + */ + public static void sort(List list) { + if (list.size() > 1) { + list.sort(INSTANCE); + } + } + + /** + * Sort the given array with a default OrderComparator. + *

    Optimized to skip sorting for lists with size 0 or 1, + * in order to avoid unnecessary array extraction. + * @param array the array to sort + * @see java.util.Arrays#sort(Object[], java.util.Comparator) + */ + public static void sort(Object[] array) { + if (array.length > 1) { + Arrays.sort(array, INSTANCE); + } + } + + /** + * Sort the given array or List with a default OrderComparator, + * if necessary. Simply skips sorting when given any other value. + *

    Optimized to skip sorting for lists with size 0 or 1, + * in order to avoid unnecessary array extraction. + * @param value the array or List to sort + * @see java.util.Arrays#sort(Object[], java.util.Comparator) + */ + public static void sortIfNecessary(Object value) { + if (value instanceof Object[]) { + sort((Object[]) value); + } + else if (value instanceof List) { + sort((List) value); + } + } + + + /** + * Strategy interface to provide an order source for a given object. + * @since 4.1 + */ + @FunctionalInterface + public interface OrderSourceProvider { + + /** + * Return an order source for the specified object, i.e. an object that + * should be checked for an order value as a replacement to the given object. + *

    Can also be an array of order source objects. + *

    If the returned object does not indicate any order, the comparator + * will fall back to checking the original object. + * @param obj the object to find an order source for + * @return the order source for that object, or {@code null} if none found + */ + @Nullable + Object getOrderSource(Object obj); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/Ordered.java b/spring-core/src/main/java/org/springframework/core/Ordered.java new file mode 100644 index 0000000..0555909 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/Ordered.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +/** + * {@code Ordered} is an interface that can be implemented by objects that + * should be orderable, for example in a {@code Collection}. + * + *

    The actual {@link #getOrder() order} can be interpreted as prioritization, + * with the first object (with the lowest order value) having the highest + * priority. + * + *

    Note that there is also a priority marker for this interface: + * {@link PriorityOrdered}. Consult the Javadoc for {@code PriorityOrdered} for + * details on how {@code PriorityOrdered} objects are ordered relative to + * plain {@link Ordered} objects. + * + *

    Consult the Javadoc for {@link OrderComparator} for details on the + * sort semantics for non-ordered objects. + * + * @author Juergen Hoeller + * @author Sam Brannen + * @since 07.04.2003 + * @see PriorityOrdered + * @see OrderComparator + * @see org.springframework.core.annotation.Order + * @see org.springframework.core.annotation.AnnotationAwareOrderComparator + */ +public interface Ordered { + + /** + * Useful constant for the highest precedence value. + * @see java.lang.Integer#MIN_VALUE + */ + int HIGHEST_PRECEDENCE = Integer.MIN_VALUE; + + /** + * Useful constant for the lowest precedence value. + * @see java.lang.Integer#MAX_VALUE + */ + int LOWEST_PRECEDENCE = Integer.MAX_VALUE; + + + /** + * Get the order value of this object. + *

    Higher values are interpreted as lower priority. As a consequence, + * the object with the lowest value has the highest priority (somewhat + * analogous to Servlet {@code load-on-startup} values). + *

    Same order values will result in arbitrary sort positions for the + * affected objects. + * @return the order value + * @see #HIGHEST_PRECEDENCE + * @see #LOWEST_PRECEDENCE + */ + int getOrder(); + +} diff --git a/spring-core/src/main/java/org/springframework/core/OverridingClassLoader.java b/spring-core/src/main/java/org/springframework/core/OverridingClassLoader.java new file mode 100644 index 0000000..63aeab6 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/OverridingClassLoader.java @@ -0,0 +1,183 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.io.IOException; +import java.io.InputStream; + +import org.springframework.lang.Nullable; +import org.springframework.util.FileCopyUtils; + +/** + * {@code ClassLoader} that does not always delegate to the parent loader + * as normal class loaders do. This enables, for example, instrumentation to be + * forced in the overriding ClassLoader, or a "throwaway" class loading behavior + * where selected application classes are temporarily loaded in the overriding + * {@code ClassLoader} for introspection purposes before eventually loading an + * instrumented version of the class in the given parent {@code ClassLoader}. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0.1 + */ +public class OverridingClassLoader extends DecoratingClassLoader { + + /** Packages that are excluded by default. */ + public static final String[] DEFAULT_EXCLUDED_PACKAGES = new String[] + {"java.", "javax.", "sun.", "oracle.", "javassist.", "org.aspectj.", "net.sf.cglib."}; + + private static final String CLASS_FILE_SUFFIX = ".class"; + + static { + ClassLoader.registerAsParallelCapable(); + } + + + @Nullable + private final ClassLoader overrideDelegate; + + + /** + * Create a new OverridingClassLoader for the given ClassLoader. + * @param parent the ClassLoader to build an overriding ClassLoader for + */ + public OverridingClassLoader(@Nullable ClassLoader parent) { + this(parent, null); + } + + /** + * Create a new OverridingClassLoader for the given ClassLoader. + * @param parent the ClassLoader to build an overriding ClassLoader for + * @param overrideDelegate the ClassLoader to delegate to for overriding + * @since 4.3 + */ + public OverridingClassLoader(@Nullable ClassLoader parent, @Nullable ClassLoader overrideDelegate) { + super(parent); + this.overrideDelegate = overrideDelegate; + for (String packageName : DEFAULT_EXCLUDED_PACKAGES) { + excludePackage(packageName); + } + } + + + @Override + public Class loadClass(String name) throws ClassNotFoundException { + if (this.overrideDelegate != null && isEligibleForOverriding(name)) { + return this.overrideDelegate.loadClass(name); + } + return super.loadClass(name); + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (isEligibleForOverriding(name)) { + Class result = loadClassForOverriding(name); + if (result != null) { + if (resolve) { + resolveClass(result); + } + return result; + } + } + return super.loadClass(name, resolve); + } + + /** + * Determine whether the specified class is eligible for overriding + * by this class loader. + * @param className the class name to check + * @return whether the specified class is eligible + * @see #isExcluded + */ + protected boolean isEligibleForOverriding(String className) { + return !isExcluded(className); + } + + /** + * Load the specified class for overriding purposes in this ClassLoader. + *

    The default implementation delegates to {@link #findLoadedClass}, + * {@link #loadBytesForClass} and {@link #defineClass}. + * @param name the name of the class + * @return the Class object, or {@code null} if no class defined for that name + * @throws ClassNotFoundException if the class for the given name couldn't be loaded + */ + @Nullable + protected Class loadClassForOverriding(String name) throws ClassNotFoundException { + Class result = findLoadedClass(name); + if (result == null) { + byte[] bytes = loadBytesForClass(name); + if (bytes != null) { + result = defineClass(name, bytes, 0, bytes.length); + } + } + return result; + } + + /** + * Load the defining bytes for the given class, + * to be turned into a Class object through a {@link #defineClass} call. + *

    The default implementation delegates to {@link #openStreamForClass} + * and {@link #transformIfNecessary}. + * @param name the name of the class + * @return the byte content (with transformers already applied), + * or {@code null} if no class defined for that name + * @throws ClassNotFoundException if the class for the given name couldn't be loaded + */ + @Nullable + protected byte[] loadBytesForClass(String name) throws ClassNotFoundException { + InputStream is = openStreamForClass(name); + if (is == null) { + return null; + } + try { + // Load the raw bytes. + byte[] bytes = FileCopyUtils.copyToByteArray(is); + // Transform if necessary and use the potentially transformed bytes. + return transformIfNecessary(name, bytes); + } + catch (IOException ex) { + throw new ClassNotFoundException("Cannot load resource for class [" + name + "]", ex); + } + } + + /** + * Open an InputStream for the specified class. + *

    The default implementation loads a standard class file through + * the parent ClassLoader's {@code getResourceAsStream} method. + * @param name the name of the class + * @return the InputStream containing the byte code for the specified class + */ + @Nullable + protected InputStream openStreamForClass(String name) { + String internalName = name.replace('.', '/') + CLASS_FILE_SUFFIX; + return getParent().getResourceAsStream(internalName); + } + + + /** + * Transformation hook to be implemented by subclasses. + *

    The default implementation simply returns the given bytes as-is. + * @param name the fully-qualified name of the class being transformed + * @param bytes the raw bytes of the class + * @return the transformed bytes (never {@code null}; + * same as the input bytes if the transformation produced no changes) + */ + protected byte[] transformIfNecessary(String name, byte[] bytes) { + return bytes; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/ParameterNameDiscoverer.java b/spring-core/src/main/java/org/springframework/core/ParameterNameDiscoverer.java new file mode 100644 index 0000000..1679eb5 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/ParameterNameDiscoverer.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +import org.springframework.lang.Nullable; + +/** + * Interface to discover parameter names for methods and constructors. + * + *

    Parameter name discovery is not always possible, but various strategies are + * available to try, such as looking for debug information that may have been + * emitted at compile time, and looking for argname annotation values optionally + * accompanying AspectJ annotated methods. + * + * @author Rod Johnson + * @author Adrian Colyer + * @since 2.0 + */ +public interface ParameterNameDiscoverer { + + /** + * Return parameter names for a method, or {@code null} if they cannot be determined. + *

    Individual entries in the array may be {@code null} if parameter names are only + * available for some parameters of the given method but not for others. However, + * it is recommended to use stub parameter names instead wherever feasible. + * @param method the method to find parameter names for + * @return an array of parameter names if the names can be resolved, + * or {@code null} if they cannot + */ + @Nullable + String[] getParameterNames(Method method); + + /** + * Return parameter names for a constructor, or {@code null} if they cannot be determined. + *

    Individual entries in the array may be {@code null} if parameter names are only + * available for some parameters of the given constructor but not for others. However, + * it is recommended to use stub parameter names instead wherever feasible. + * @param ctor the constructor to find parameter names for + * @return an array of parameter names if the names can be resolved, + * or {@code null} if they cannot + */ + @Nullable + String[] getParameterNames(Constructor ctor); + +} diff --git a/spring-core/src/main/java/org/springframework/core/ParameterizedTypeReference.java b/spring-core/src/main/java/org/springframework/core/ParameterizedTypeReference.java new file mode 100644 index 0000000..c10dd89 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/ParameterizedTypeReference.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * The purpose of this class is to enable capturing and passing a generic + * {@link Type}. In order to capture the generic type and retain it at runtime, + * you need to create a subclass (ideally as anonymous inline class) as follows: + * + *

    + * ParameterizedTypeReference<List<String>> typeRef = new ParameterizedTypeReference<List<String>>() {};
    + * 
    + * + *

    The resulting {@code typeRef} instance can then be used to obtain a {@link Type} + * instance that carries the captured parameterized type information at runtime. + * For more information on "super type tokens" see the link to Neal Gafter's blog post. + * + * @author Arjen Poutsma + * @author Rossen Stoyanchev + * @since 3.2 + * @param the referenced type + * @see Neal Gafter on Super Type Tokens + */ +public abstract class ParameterizedTypeReference { + + private final Type type; + + + protected ParameterizedTypeReference() { + Class parameterizedTypeReferenceSubclass = findParameterizedTypeReferenceSubclass(getClass()); + Type type = parameterizedTypeReferenceSubclass.getGenericSuperclass(); + Assert.isInstanceOf(ParameterizedType.class, type, "Type must be a parameterized type"); + ParameterizedType parameterizedType = (ParameterizedType) type; + Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); + Assert.isTrue(actualTypeArguments.length == 1, "Number of type arguments must be 1"); + this.type = actualTypeArguments[0]; + } + + private ParameterizedTypeReference(Type type) { + this.type = type; + } + + + public Type getType() { + return this.type; + } + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof ParameterizedTypeReference && + this.type.equals(((ParameterizedTypeReference) other).type))); + } + + @Override + public int hashCode() { + return this.type.hashCode(); + } + + @Override + public String toString() { + return "ParameterizedTypeReference<" + this.type + ">"; + } + + + /** + * Build a {@code ParameterizedTypeReference} wrapping the given type. + * @param type a generic type (possibly obtained via reflection, + * e.g. from {@link java.lang.reflect.Method#getGenericReturnType()}) + * @return a corresponding reference which may be passed into + * {@code ParameterizedTypeReference}-accepting methods + * @since 4.3.12 + */ + public static ParameterizedTypeReference forType(Type type) { + return new ParameterizedTypeReference(type) { + }; + } + + private static Class findParameterizedTypeReferenceSubclass(Class child) { + Class parent = child.getSuperclass(); + if (Object.class == parent) { + throw new IllegalStateException("Expected ParameterizedTypeReference superclass"); + } + else if (ParameterizedTypeReference.class == parent) { + return child; + } + else { + return findParameterizedTypeReferenceSubclass(parent); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/PrioritizedParameterNameDiscoverer.java b/spring-core/src/main/java/org/springframework/core/PrioritizedParameterNameDiscoverer.java new file mode 100644 index 0000000..8701e36 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/PrioritizedParameterNameDiscoverer.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.lang.Nullable; + +/** + * {@link ParameterNameDiscoverer} implementation that tries several discoverer + * delegates in succession. Those added first in the {@code addDiscoverer} method + * have highest priority. If one returns {@code null}, the next will be tried. + * + *

    The default behavior is to return {@code null} if no discoverer matches. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + */ +public class PrioritizedParameterNameDiscoverer implements ParameterNameDiscoverer { + + private final List parameterNameDiscoverers = new ArrayList<>(2); + + + /** + * Add a further {@link ParameterNameDiscoverer} delegate to the list of + * discoverers that this {@code PrioritizedParameterNameDiscoverer} checks. + */ + public void addDiscoverer(ParameterNameDiscoverer pnd) { + this.parameterNameDiscoverers.add(pnd); + } + + + @Override + @Nullable + public String[] getParameterNames(Method method) { + for (ParameterNameDiscoverer pnd : this.parameterNameDiscoverers) { + String[] result = pnd.getParameterNames(method); + if (result != null) { + return result; + } + } + return null; + } + + @Override + @Nullable + public String[] getParameterNames(Constructor ctor) { + for (ParameterNameDiscoverer pnd : this.parameterNameDiscoverers) { + String[] result = pnd.getParameterNames(ctor); + if (result != null) { + return result; + } + } + return null; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/PriorityOrdered.java b/spring-core/src/main/java/org/springframework/core/PriorityOrdered.java new file mode 100644 index 0000000..2090cd3 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/PriorityOrdered.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +/** + * Extension of the {@link Ordered} interface, expressing a priority + * ordering: {@code PriorityOrdered} objects are always applied before + * plain {@link Ordered} objects regardless of their order values. + * + *

    When sorting a set of {@code Ordered} objects, {@code PriorityOrdered} + * objects and plain {@code Ordered} objects are effectively treated as + * two separate subsets, with the set of {@code PriorityOrdered} objects preceding + * the set of plain {@code Ordered} objects and with relative + * ordering applied within those subsets. + * + *

    This is primarily a special-purpose interface, used within the framework + * itself for objects where it is particularly important to recognize + * prioritized objects first, potentially without even obtaining the + * remaining objects. A typical example: prioritized post-processors in a Spring + * {@link org.springframework.context.ApplicationContext}. + * + *

    Note: {@code PriorityOrdered} post-processor beans are initialized in + * a special phase, ahead of other post-processor beans. This subtly + * affects their autowiring behavior: they will only be autowired against + * beans which do not require eager initialization for type matching. + * + * @author Juergen Hoeller + * @author Sam Brannen + * @since 2.5 + * @see org.springframework.beans.factory.config.PropertyOverrideConfigurer + * @see org.springframework.beans.factory.config.PropertyPlaceholderConfigurer + */ +public interface PriorityOrdered extends Ordered { +} diff --git a/spring-core/src/main/java/org/springframework/core/ReactiveAdapter.java b/spring-core/src/main/java/org/springframework/core/ReactiveAdapter.java new file mode 100644 index 0000000..0356f47 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/ReactiveAdapter.java @@ -0,0 +1,124 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.util.function.Function; + +import org.reactivestreams.Publisher; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Adapter for a Reactive Streams {@link Publisher} to and from an async/reactive + * type such as {@code CompletableFuture}, RxJava {@code Observable}, and others. + * + *

    An adapter is typically obtained via {@link ReactiveAdapterRegistry}. + * + * @author Rossen Stoyanchev + * @since 5.0 + */ +public class ReactiveAdapter { + + private final ReactiveTypeDescriptor descriptor; + + private final Function> toPublisherFunction; + + private final Function, Object> fromPublisherFunction; + + + /** + * Constructor for an adapter with functions to convert the target reactive + * or async type to and from a Reactive Streams Publisher. + * @param descriptor the reactive type descriptor + * @param toPublisherFunction adapter to a Publisher + * @param fromPublisherFunction adapter from a Publisher + */ + public ReactiveAdapter(ReactiveTypeDescriptor descriptor, + Function> toPublisherFunction, + Function, Object> fromPublisherFunction) { + + Assert.notNull(descriptor, "'descriptor' is required"); + Assert.notNull(toPublisherFunction, "'toPublisherFunction' is required"); + Assert.notNull(fromPublisherFunction, "'fromPublisherFunction' is required"); + + this.descriptor = descriptor; + this.toPublisherFunction = toPublisherFunction; + this.fromPublisherFunction = fromPublisherFunction; + } + + + /** + * Return the descriptor of the reactive type for the adapter. + */ + public ReactiveTypeDescriptor getDescriptor() { + return this.descriptor; + } + + /** + * Shortcut for {@code getDescriptor().getReactiveType()}. + */ + public Class getReactiveType() { + return getDescriptor().getReactiveType(); + } + + /** + * Shortcut for {@code getDescriptor().isMultiValue()}. + */ + public boolean isMultiValue() { + return getDescriptor().isMultiValue(); + } + + /** + * Shortcut for {@code getDescriptor().isNoValue()}. + */ + public boolean isNoValue() { + return getDescriptor().isNoValue(); + } + + /** + * Shortcut for {@code getDescriptor().supportsEmpty()}. + */ + public boolean supportsEmpty() { + return getDescriptor().supportsEmpty(); + } + + + /** + * Adapt the given instance to a Reactive Streams {@code Publisher}. + * @param source the source object to adapt from; if the given object is + * {@code null}, {@link ReactiveTypeDescriptor#getEmptyValue()} is used. + * @return the Publisher representing the adaptation + */ + @SuppressWarnings("unchecked") + public Publisher toPublisher(@Nullable Object source) { + if (source == null) { + source = getDescriptor().getEmptyValue(); + } + return (Publisher) this.toPublisherFunction.apply(source); + } + + /** + * Adapt from the given Reactive Streams Publisher. + * @param publisher the publisher to adapt from + * @return the reactive type instance representing the adapted publisher + */ + public Object fromPublisher(Publisher publisher) { + return this.fromPublisherFunction.apply(publisher); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java b/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java new file mode 100644 index 0000000..35d66ba --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java @@ -0,0 +1,433 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.Function; + +import kotlinx.coroutines.CompletableDeferredKt; +import kotlinx.coroutines.Deferred; +import org.reactivestreams.Publisher; +import reactor.blockhound.BlockHound; +import reactor.blockhound.integration.BlockHoundIntegration; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import rx.RxReactiveStreams; + +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ReflectionUtils; + +/** + * A registry of adapters to adapt Reactive Streams {@link Publisher} to/from + * various async/reactive types such as {@code CompletableFuture}, RxJava + * {@code Observable}, and others. + * + *

    By default, depending on classpath availability, adapters are registered + * for Reactor, RxJava 2/3, or RxJava 1 (+ RxJava Reactive Streams bridge), + * {@link CompletableFuture}, Java 9+ {@code Flow.Publisher}, and Kotlin + * Coroutines' {@code Deferred} and {@code Flow}. + * + *

    Note: As of Spring Framework 5.3, support for RxJava 1.x + * is deprecated in favor of RxJava 2 and 3. + * + * @author Rossen Stoyanchev + * @author Sebastien Deleuze + * @since 5.0 + */ +public class ReactiveAdapterRegistry { + + @Nullable + private static volatile ReactiveAdapterRegistry sharedInstance; + + private final boolean reactorPresent; + + private final List adapters = new ArrayList<>(); + + + /** + * Create a registry and auto-register default adapters. + * @see #getSharedInstance() + */ + public ReactiveAdapterRegistry() { + ClassLoader classLoader = ReactiveAdapterRegistry.class.getClassLoader(); + + // Reactor + boolean reactorRegistered = false; + if (ClassUtils.isPresent("reactor.core.publisher.Flux", classLoader)) { + new ReactorRegistrar().registerAdapters(this); + reactorRegistered = true; + } + this.reactorPresent = reactorRegistered; + + // RxJava1 (deprecated) + if (ClassUtils.isPresent("rx.Observable", classLoader) && + ClassUtils.isPresent("rx.RxReactiveStreams", classLoader)) { + new RxJava1Registrar().registerAdapters(this); + } + + // RxJava2 + if (ClassUtils.isPresent("io.reactivex.Flowable", classLoader)) { + new RxJava2Registrar().registerAdapters(this); + } + + // RxJava3 + if (ClassUtils.isPresent("io.reactivex.rxjava3.core.Flowable", classLoader)) { + new RxJava3Registrar().registerAdapters(this); + } + + // Java 9+ Flow.Publisher + if (ClassUtils.isPresent("java.util.concurrent.Flow.Publisher", classLoader)) { + new ReactorJdkFlowAdapterRegistrar().registerAdapter(this); + } + // If not present, do nothing for the time being... + // We can fall back on "reactive-streams-flow-bridge" (once released) + + // Coroutines + if (this.reactorPresent && ClassUtils.isPresent("kotlinx.coroutines.reactor.MonoKt", classLoader)) { + new CoroutinesRegistrar().registerAdapters(this); + } + } + + + /** + * Whether the registry has any adapters. + */ + public boolean hasAdapters() { + return !this.adapters.isEmpty(); + } + + /** + * Register a reactive type along with functions to adapt to and from a + * Reactive Streams {@link Publisher}. The function arguments assume that + * their input is neither {@code null} nor {@link Optional}. + */ + public void registerReactiveType(ReactiveTypeDescriptor descriptor, + Function> toAdapter, Function, Object> fromAdapter) { + + if (this.reactorPresent) { + this.adapters.add(new ReactorAdapter(descriptor, toAdapter, fromAdapter)); + } + else { + this.adapters.add(new ReactiveAdapter(descriptor, toAdapter, fromAdapter)); + } + } + + /** + * Get the adapter for the given reactive type. + * @return the corresponding adapter, or {@code null} if none available + */ + @Nullable + public ReactiveAdapter getAdapter(Class reactiveType) { + return getAdapter(reactiveType, null); + } + + /** + * Get the adapter for the given reactive type. Or if a "source" object is + * provided, its actual type is used instead. + * @param reactiveType the reactive type + * (may be {@code null} if a concrete source object is given) + * @param source an instance of the reactive type + * (i.e. to adapt from; may be {@code null} if the reactive type is specified) + * @return the corresponding adapter, or {@code null} if none available + */ + @Nullable + public ReactiveAdapter getAdapter(@Nullable Class reactiveType, @Nullable Object source) { + if (this.adapters.isEmpty()) { + return null; + } + + Object sourceToUse = (source instanceof Optional ? ((Optional) source).orElse(null) : source); + Class clazz = (sourceToUse != null ? sourceToUse.getClass() : reactiveType); + if (clazz == null) { + return null; + } + for (ReactiveAdapter adapter : this.adapters) { + if (adapter.getReactiveType() == clazz) { + return adapter; + } + } + for (ReactiveAdapter adapter : this.adapters) { + if (adapter.getReactiveType().isAssignableFrom(clazz)) { + return adapter; + } + } + return null; + } + + + /** + * Return a shared default {@code ReactiveAdapterRegistry} instance, + * lazily building it once needed. + *

    NOTE: We highly recommend passing a long-lived, pre-configured + * {@code ReactiveAdapterRegistry} instance for customization purposes. + * This accessor is only meant as a fallback for code paths that want to + * fall back on a default instance if one isn't provided. + * @return the shared {@code ReactiveAdapterRegistry} instance + * @since 5.0.2 + */ + public static ReactiveAdapterRegistry getSharedInstance() { + ReactiveAdapterRegistry registry = sharedInstance; + if (registry == null) { + synchronized (ReactiveAdapterRegistry.class) { + registry = sharedInstance; + if (registry == null) { + registry = new ReactiveAdapterRegistry(); + sharedInstance = registry; + } + } + } + return registry; + } + + + private static class ReactorRegistrar { + + void registerAdapters(ReactiveAdapterRegistry registry) { + // Register Flux and Mono before Publisher... + + registry.registerReactiveType( + ReactiveTypeDescriptor.singleOptionalValue(Mono.class, Mono::empty), + source -> (Mono) source, + Mono::from + ); + + registry.registerReactiveType( + ReactiveTypeDescriptor.multiValue(Flux.class, Flux::empty), + source -> (Flux) source, + Flux::from); + + registry.registerReactiveType( + ReactiveTypeDescriptor.multiValue(Publisher.class, Flux::empty), + source -> (Publisher) source, + source -> source); + + registry.registerReactiveType( + ReactiveTypeDescriptor.nonDeferredAsyncValue(CompletionStage.class, EmptyCompletableFuture::new), + source -> Mono.fromCompletionStage((CompletionStage) source), + source -> Mono.from(source).toFuture() + ); + } + } + + + private static class RxJava1Registrar { + + void registerAdapters(ReactiveAdapterRegistry registry) { + registry.registerReactiveType( + ReactiveTypeDescriptor.multiValue(rx.Observable.class, rx.Observable::empty), + source -> RxReactiveStreams.toPublisher((rx.Observable) source), + RxReactiveStreams::toObservable + ); + registry.registerReactiveType( + ReactiveTypeDescriptor.singleRequiredValue(rx.Single.class), + source -> RxReactiveStreams.toPublisher((rx.Single) source), + RxReactiveStreams::toSingle + ); + registry.registerReactiveType( + ReactiveTypeDescriptor.noValue(rx.Completable.class, rx.Completable::complete), + source -> RxReactiveStreams.toPublisher((rx.Completable) source), + RxReactiveStreams::toCompletable + ); + } + } + + + private static class RxJava2Registrar { + + void registerAdapters(ReactiveAdapterRegistry registry) { + registry.registerReactiveType( + ReactiveTypeDescriptor.multiValue(io.reactivex.Flowable.class, io.reactivex.Flowable::empty), + source -> (io.reactivex.Flowable) source, + io.reactivex.Flowable::fromPublisher + ); + registry.registerReactiveType( + ReactiveTypeDescriptor.multiValue(io.reactivex.Observable.class, io.reactivex.Observable::empty), + source -> ((io.reactivex.Observable) source).toFlowable(io.reactivex.BackpressureStrategy.BUFFER), + io.reactivex.Observable::fromPublisher + ); + registry.registerReactiveType( + ReactiveTypeDescriptor.singleRequiredValue(io.reactivex.Single.class), + source -> ((io.reactivex.Single) source).toFlowable(), + io.reactivex.Single::fromPublisher + ); + registry.registerReactiveType( + ReactiveTypeDescriptor.singleOptionalValue(io.reactivex.Maybe.class, io.reactivex.Maybe::empty), + source -> ((io.reactivex.Maybe) source).toFlowable(), + source -> io.reactivex.Flowable.fromPublisher(source) + .toObservable().singleElement() + ); + registry.registerReactiveType( + ReactiveTypeDescriptor.noValue(io.reactivex.Completable.class, io.reactivex.Completable::complete), + source -> ((io.reactivex.Completable) source).toFlowable(), + io.reactivex.Completable::fromPublisher + ); + } + } + + private static class RxJava3Registrar { + + void registerAdapters(ReactiveAdapterRegistry registry) { + registry.registerReactiveType( + ReactiveTypeDescriptor.multiValue( + io.reactivex.rxjava3.core.Flowable.class, + io.reactivex.rxjava3.core.Flowable::empty), + source -> (io.reactivex.rxjava3.core.Flowable) source, + io.reactivex.rxjava3.core.Flowable::fromPublisher + ); + registry.registerReactiveType( + ReactiveTypeDescriptor.multiValue( + io.reactivex.rxjava3.core.Observable.class, + io.reactivex.rxjava3.core.Observable::empty), + source -> ((io.reactivex.rxjava3.core.Observable) source).toFlowable( + io.reactivex.rxjava3.core.BackpressureStrategy.BUFFER), + io.reactivex.rxjava3.core.Observable::fromPublisher + ); + registry.registerReactiveType( + ReactiveTypeDescriptor.singleRequiredValue(io.reactivex.rxjava3.core.Single.class), + source -> ((io.reactivex.rxjava3.core.Single) source).toFlowable(), + io.reactivex.rxjava3.core.Single::fromPublisher + ); + registry.registerReactiveType( + ReactiveTypeDescriptor.singleOptionalValue( + io.reactivex.rxjava3.core.Maybe.class, + io.reactivex.rxjava3.core.Maybe::empty), + source -> ((io.reactivex.rxjava3.core.Maybe) source).toFlowable(), + io.reactivex.rxjava3.core.Maybe::fromPublisher + ); + registry.registerReactiveType( + ReactiveTypeDescriptor.noValue( + io.reactivex.rxjava3.core.Completable.class, + io.reactivex.rxjava3.core.Completable::complete), + source -> ((io.reactivex.rxjava3.core.Completable) source).toFlowable(), + io.reactivex.rxjava3.core.Completable::fromPublisher + ); + } + } + + private static class ReactorJdkFlowAdapterRegistrar { + + void registerAdapter(ReactiveAdapterRegistry registry) { + // TODO: remove reflection when build requires JDK 9+ + + try { + String publisherName = "java.util.concurrent.Flow.Publisher"; + Class publisherClass = ClassUtils.forName(publisherName, getClass().getClassLoader()); + + String adapterName = "reactor.adapter.JdkFlowAdapter"; + Class flowAdapterClass = ClassUtils.forName(adapterName, getClass().getClassLoader()); + + Method toFluxMethod = flowAdapterClass.getMethod("flowPublisherToFlux", publisherClass); + Method toFlowMethod = flowAdapterClass.getMethod("publisherToFlowPublisher", Publisher.class); + Object emptyFlow = ReflectionUtils.invokeMethod(toFlowMethod, null, Flux.empty()); + + registry.registerReactiveType( + ReactiveTypeDescriptor.multiValue(publisherClass, () -> emptyFlow), + source -> (Publisher) ReflectionUtils.invokeMethod(toFluxMethod, null, source), + publisher -> ReflectionUtils.invokeMethod(toFlowMethod, null, publisher) + ); + } + catch (Throwable ex) { + // Ignore + } + } + } + + + /** + * ReactiveAdapter variant that wraps adapted Publishers as {@link Flux} or + * {@link Mono} depending on {@link ReactiveTypeDescriptor#isMultiValue()}. + * This is important in places where only the stream and stream element type + * information is available like encoders and decoders. + */ + private static class ReactorAdapter extends ReactiveAdapter { + + ReactorAdapter(ReactiveTypeDescriptor descriptor, + Function> toPublisherFunction, + Function, Object> fromPublisherFunction) { + + super(descriptor, toPublisherFunction, fromPublisherFunction); + } + + @Override + public Publisher toPublisher(@Nullable Object source) { + Publisher publisher = super.toPublisher(source); + return (isMultiValue() ? Flux.from(publisher) : Mono.from(publisher)); + } + } + + + private static class EmptyCompletableFuture extends CompletableFuture { + + EmptyCompletableFuture() { + complete(null); + } + } + + + private static class CoroutinesRegistrar { + + @SuppressWarnings("KotlinInternalInJava") + void registerAdapters(ReactiveAdapterRegistry registry) { + registry.registerReactiveType( + ReactiveTypeDescriptor.singleOptionalValue(Deferred.class, + () -> CompletableDeferredKt.CompletableDeferred(null)), + source -> CoroutinesUtils.deferredToMono((Deferred) source), + source -> CoroutinesUtils.monoToDeferred(Mono.from(source))); + + registry.registerReactiveType( + ReactiveTypeDescriptor.multiValue(kotlinx.coroutines.flow.Flow.class, kotlinx.coroutines.flow.FlowKt::emptyFlow), + source -> kotlinx.coroutines.reactor.ReactorFlowKt.asFlux((kotlinx.coroutines.flow.Flow) source), + kotlinx.coroutines.reactive.ReactiveFlowKt::asFlow + ); + } + } + + + /** + * {@code BlockHoundIntegration} for spring-core classes. + *

    Explicitly allow the following: + *

      + *
    • Reading class info via {@link LocalVariableTableParameterNameDiscoverer}. + *
    • Locking within {@link ConcurrentReferenceHashMap}. + *
    + * @since 5.2.4 + */ + public static class SpringCoreBlockHoundIntegration implements BlockHoundIntegration { + + @Override + public void applyTo(BlockHound.Builder builder) { + + // Avoid hard references potentially anywhere in spring-core (no need for structural dependency) + + builder.allowBlockingCallsInside( + "org.springframework.core.LocalVariableTableParameterNameDiscoverer", "inspectClass"); + + String className = "org.springframework.util.ConcurrentReferenceHashMap$Segment"; + builder.allowBlockingCallsInside(className, "doTask"); + builder.allowBlockingCallsInside(className, "clear"); + builder.allowBlockingCallsInside(className, "restructure"); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/ReactiveTypeDescriptor.java b/spring-core/src/main/java/org/springframework/core/ReactiveTypeDescriptor.java new file mode 100644 index 0000000..035ac1b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/ReactiveTypeDescriptor.java @@ -0,0 +1,178 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.util.function.Supplier; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Describes the semantics of a reactive type including boolean checks for + * {@link #isMultiValue()}, {@link #isNoValue()}, and {@link #supportsEmpty()}. + * + * @author Rossen Stoyanchev + * @since 5.0 + */ +public final class ReactiveTypeDescriptor { + + private final Class reactiveType; + + private final boolean multiValue; + + private final boolean noValue; + + @Nullable + private final Supplier emptyValueSupplier; + + private final boolean deferred; + + + private ReactiveTypeDescriptor(Class reactiveType, boolean multiValue, boolean noValue, + @Nullable Supplier emptySupplier) { + + this(reactiveType, multiValue, noValue, emptySupplier, true); + } + + private ReactiveTypeDescriptor(Class reactiveType, boolean multiValue, boolean noValue, + @Nullable Supplier emptySupplier, boolean deferred) { + + Assert.notNull(reactiveType, "'reactiveType' must not be null"); + this.reactiveType = reactiveType; + this.multiValue = multiValue; + this.noValue = noValue; + this.emptyValueSupplier = emptySupplier; + this.deferred = deferred; + } + + + /** + * Return the reactive type for this descriptor. + */ + public Class getReactiveType() { + return this.reactiveType; + } + + /** + * Return {@code true} if the reactive type can produce more than 1 value + * can be produced and is therefore a good fit to adapt to {@code Flux}. + * A {@code false} return value implies the reactive type can produce 1 + * value at most and is therefore a good fit to adapt to {@code Mono}. + */ + public boolean isMultiValue() { + return this.multiValue; + } + + /** + * Return {@code true} if the reactive type does not produce any values and + * only provides completion and error signals. + */ + public boolean isNoValue() { + return this.noValue; + } + + /** + * Return {@code true} if the reactive type can complete with no values. + */ + public boolean supportsEmpty() { + return (this.emptyValueSupplier != null); + } + + /** + * Return an empty-value instance for the underlying reactive or async type. + * Use of this type implies {@link #supportsEmpty()} is true. + */ + public Object getEmptyValue() { + Assert.state(this.emptyValueSupplier != null, "Empty values not supported"); + return this.emptyValueSupplier.get(); + } + + /** + * Whether the underlying operation is deferred and needs to be started + * explicitly, e.g. via subscribing (or similar), or whether it is triggered + * without the consumer having any control. + * @since 5.2.7 + */ + public boolean isDeferred() { + return this.deferred; + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + return this.reactiveType.equals(((ReactiveTypeDescriptor) other).reactiveType); + } + + @Override + public int hashCode() { + return this.reactiveType.hashCode(); + } + + + /** + * Descriptor for a reactive type that can produce 0..N values. + * @param type the reactive type + * @param emptySupplier a supplier of an empty-value instance of the reactive type + */ + public static ReactiveTypeDescriptor multiValue(Class type, Supplier emptySupplier) { + return new ReactiveTypeDescriptor(type, true, false, emptySupplier); + } + + /** + * Descriptor for a reactive type that can produce 0..1 values. + * @param type the reactive type + * @param emptySupplier a supplier of an empty-value instance of the reactive type + */ + public static ReactiveTypeDescriptor singleOptionalValue(Class type, Supplier emptySupplier) { + return new ReactiveTypeDescriptor(type, false, false, emptySupplier); + } + + /** + * Descriptor for a reactive type that must produce 1 value to complete. + * @param type the reactive type + */ + public static ReactiveTypeDescriptor singleRequiredValue(Class type) { + return new ReactiveTypeDescriptor(type, false, false, null); + } + + /** + * Descriptor for a reactive type that does not produce any values. + * @param type the reactive type + * @param emptySupplier a supplier of an empty-value instance of the reactive type + */ + public static ReactiveTypeDescriptor noValue(Class type, Supplier emptySupplier) { + return new ReactiveTypeDescriptor(type, false, true, emptySupplier); + } + + /** + * The same as {@link #singleOptionalValue(Class, Supplier)} but for a + * non-deferred, async type such as {@link java.util.concurrent.CompletableFuture}. + * @param type the reactive type + * @param emptySupplier a supplier of an empty-value instance of the reactive type + * @since 5.2.7 + */ + public static ReactiveTypeDescriptor nonDeferredAsyncValue(Class type, Supplier emptySupplier) { + return new ReactiveTypeDescriptor(type, false, false, emptySupplier, false); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/ResolvableType.java b/spring-core/src/main/java/org/springframework/core/ResolvableType.java new file mode 100644 index 0000000..ae6fe4f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/ResolvableType.java @@ -0,0 +1,1695 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.io.Serializable; +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.Arrays; +import java.util.Collection; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.StringJoiner; + +import org.springframework.core.SerializableTypeWrapper.FieldTypeProvider; +import org.springframework.core.SerializableTypeWrapper.MethodParameterTypeProvider; +import org.springframework.core.SerializableTypeWrapper.TypeProvider; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Encapsulates a Java {@link java.lang.reflect.Type}, providing access to + * {@link #getSuperType() supertypes}, {@link #getInterfaces() interfaces}, and + * {@link #getGeneric(int...) generic parameters} along with the ability to ultimately + * {@link #resolve() resolve} to a {@link java.lang.Class}. + * + *

    {@code ResolvableTypes} may be obtained from {@link #forField(Field) fields}, + * {@link #forMethodParameter(Method, int) method parameters}, + * {@link #forMethodReturnType(Method) method returns} or + * {@link #forClass(Class) classes}. Most methods on this class will themselves return + * {@link ResolvableType ResolvableTypes}, allowing easy navigation. For example: + *

    + * private HashMap<Integer, List<String>> myMap;
    + *
    + * public void example() {
    + *     ResolvableType t = ResolvableType.forField(getClass().getDeclaredField("myMap"));
    + *     t.getSuperType(); // AbstractMap<Integer, List<String>>
    + *     t.asMap(); // Map<Integer, List<String>>
    + *     t.getGeneric(0).resolve(); // Integer
    + *     t.getGeneric(1).resolve(); // List
    + *     t.getGeneric(1); // List<String>
    + *     t.resolveGeneric(1, 0); // String
    + * }
    + * 
    + * + * @author Phillip Webb + * @author Juergen Hoeller + * @author Stephane Nicoll + * @since 4.0 + * @see #forField(Field) + * @see #forMethodParameter(Method, int) + * @see #forMethodReturnType(Method) + * @see #forConstructorParameter(Constructor, int) + * @see #forClass(Class) + * @see #forType(Type) + * @see #forInstance(Object) + * @see ResolvableTypeProvider + */ +@SuppressWarnings("serial") +public class ResolvableType implements Serializable { + + /** + * {@code ResolvableType} returned when no value is available. {@code NONE} is used + * in preference to {@code null} so that multiple method calls can be safely chained. + */ + public static final ResolvableType NONE = new ResolvableType(EmptyType.INSTANCE, null, null, 0); + + private static final ResolvableType[] EMPTY_TYPES_ARRAY = new ResolvableType[0]; + + private static final ConcurrentReferenceHashMap cache = + new ConcurrentReferenceHashMap<>(256); + + + /** + * The underlying Java type being managed. + */ + private final Type type; + + /** + * Optional provider for the type. + */ + @Nullable + private final TypeProvider typeProvider; + + /** + * The {@code VariableResolver} to use or {@code null} if no resolver is available. + */ + @Nullable + private final VariableResolver variableResolver; + + /** + * The component type for an array or {@code null} if the type should be deduced. + */ + @Nullable + private final ResolvableType componentType; + + @Nullable + private final Integer hash; + + @Nullable + private Class resolved; + + @Nullable + private volatile ResolvableType superType; + + @Nullable + private volatile ResolvableType[] interfaces; + + @Nullable + private volatile ResolvableType[] generics; + + + /** + * Private constructor used to create a new {@link ResolvableType} for cache key purposes, + * with no upfront resolution. + */ + private ResolvableType( + Type type, @Nullable TypeProvider typeProvider, @Nullable VariableResolver variableResolver) { + + this.type = type; + this.typeProvider = typeProvider; + this.variableResolver = variableResolver; + this.componentType = null; + this.hash = calculateHashCode(); + this.resolved = null; + } + + /** + * Private constructor used to create a new {@link ResolvableType} for cache value purposes, + * with upfront resolution and a pre-calculated hash. + * @since 4.2 + */ + private ResolvableType(Type type, @Nullable TypeProvider typeProvider, + @Nullable VariableResolver variableResolver, @Nullable Integer hash) { + + this.type = type; + this.typeProvider = typeProvider; + this.variableResolver = variableResolver; + this.componentType = null; + this.hash = hash; + this.resolved = resolveClass(); + } + + /** + * Private constructor used to create a new {@link ResolvableType} for uncached purposes, + * with upfront resolution but lazily calculated hash. + */ + private ResolvableType(Type type, @Nullable TypeProvider typeProvider, + @Nullable VariableResolver variableResolver, @Nullable ResolvableType componentType) { + + this.type = type; + this.typeProvider = typeProvider; + this.variableResolver = variableResolver; + this.componentType = componentType; + this.hash = null; + this.resolved = resolveClass(); + } + + /** + * Private constructor used to create a new {@link ResolvableType} on a {@link Class} basis. + * Avoids all {@code instanceof} checks in order to create a straight {@link Class} wrapper. + * @since 4.2 + */ + private ResolvableType(@Nullable Class clazz) { + this.resolved = (clazz != null ? clazz : Object.class); + this.type = this.resolved; + this.typeProvider = null; + this.variableResolver = null; + this.componentType = null; + this.hash = null; + } + + + /** + * Return the underling Java {@link Type} being managed. + */ + public Type getType() { + return SerializableTypeWrapper.unwrap(this.type); + } + + /** + * Return the underlying Java {@link Class} being managed, if available; + * otherwise {@code null}. + */ + @Nullable + public Class getRawClass() { + if (this.type == this.resolved) { + return this.resolved; + } + Type rawType = this.type; + if (rawType instanceof ParameterizedType) { + rawType = ((ParameterizedType) rawType).getRawType(); + } + return (rawType instanceof Class ? (Class) rawType : null); + } + + /** + * Return the underlying source of the resolvable type. Will return a {@link Field}, + * {@link MethodParameter} or {@link Type} depending on how the {@link ResolvableType} + * was constructed. With the exception of the {@link #NONE} constant, this method will + * never return {@code null}. This method is primarily to provide access to additional + * type information or meta-data that alternative JVM languages may provide. + */ + public Object getSource() { + Object source = (this.typeProvider != null ? this.typeProvider.getSource() : null); + return (source != null ? source : this.type); + } + + /** + * Return this type as a resolved {@code Class}, falling back to + * {@link java.lang.Object} if no specific class can be resolved. + * @return the resolved {@link Class} or the {@code Object} fallback + * @since 5.1 + * @see #getRawClass() + * @see #resolve(Class) + */ + public Class toClass() { + return resolve(Object.class); + } + + /** + * Determine whether the given object is an instance of this {@code ResolvableType}. + * @param obj the object to check + * @since 4.2 + * @see #isAssignableFrom(Class) + */ + public boolean isInstance(@Nullable Object obj) { + return (obj != null && isAssignableFrom(obj.getClass())); + } + + /** + * Determine whether this {@code ResolvableType} is assignable from the + * specified other type. + * @param other the type to be checked against (as a {@code Class}) + * @since 4.2 + * @see #isAssignableFrom(ResolvableType) + */ + public boolean isAssignableFrom(Class other) { + return isAssignableFrom(forClass(other), null); + } + + /** + * Determine whether this {@code ResolvableType} is assignable from the + * specified other type. + *

    Attempts to follow the same rules as the Java compiler, considering + * whether both the {@link #resolve() resolved} {@code Class} is + * {@link Class#isAssignableFrom(Class) assignable from} the given type + * as well as whether all {@link #getGenerics() generics} are assignable. + * @param other the type to be checked against (as a {@code ResolvableType}) + * @return {@code true} if the specified other type can be assigned to this + * {@code ResolvableType}; {@code false} otherwise + */ + public boolean isAssignableFrom(ResolvableType other) { + return isAssignableFrom(other, null); + } + + private boolean isAssignableFrom(ResolvableType other, @Nullable Map matchedBefore) { + Assert.notNull(other, "ResolvableType must not be null"); + + // If we cannot resolve types, we are not assignable + if (this == NONE || other == NONE) { + return false; + } + + // Deal with array by delegating to the component type + if (isArray()) { + return (other.isArray() && getComponentType().isAssignableFrom(other.getComponentType())); + } + + if (matchedBefore != null && matchedBefore.get(this.type) == other.type) { + return true; + } + + // Deal with wildcard bounds + WildcardBounds ourBounds = WildcardBounds.get(this); + WildcardBounds typeBounds = WildcardBounds.get(other); + + // In the form X is assignable to + if (typeBounds != null) { + return (ourBounds != null && ourBounds.isSameKind(typeBounds) && + ourBounds.isAssignableFrom(typeBounds.getBounds())); + } + + // In the form is assignable to X... + if (ourBounds != null) { + return ourBounds.isAssignableFrom(other); + } + + // Main assignability check about to follow + boolean exactMatch = (matchedBefore != null); // We're checking nested generic variables now... + boolean checkGenerics = true; + Class ourResolved = null; + if (this.type instanceof TypeVariable) { + TypeVariable variable = (TypeVariable) this.type; + // Try default variable resolution + if (this.variableResolver != null) { + ResolvableType resolved = this.variableResolver.resolveVariable(variable); + if (resolved != null) { + ourResolved = resolved.resolve(); + } + } + if (ourResolved == null) { + // Try variable resolution against target type + if (other.variableResolver != null) { + ResolvableType resolved = other.variableResolver.resolveVariable(variable); + if (resolved != null) { + ourResolved = resolved.resolve(); + checkGenerics = false; + } + } + } + if (ourResolved == null) { + // Unresolved type variable, potentially nested -> never insist on exact match + exactMatch = false; + } + } + if (ourResolved == null) { + ourResolved = resolve(Object.class); + } + Class otherResolved = other.toClass(); + + // We need an exact type match for generics + // List is not assignable from List + if (exactMatch ? !ourResolved.equals(otherResolved) : !ClassUtils.isAssignable(ourResolved, otherResolved)) { + return false; + } + + if (checkGenerics) { + // Recursively check each generic + ResolvableType[] ourGenerics = getGenerics(); + ResolvableType[] typeGenerics = other.as(ourResolved).getGenerics(); + if (ourGenerics.length != typeGenerics.length) { + return false; + } + if (matchedBefore == null) { + matchedBefore = new IdentityHashMap<>(1); + } + matchedBefore.put(this.type, other.type); + for (int i = 0; i < ourGenerics.length; i++) { + if (!ourGenerics[i].isAssignableFrom(typeGenerics[i], matchedBefore)) { + return false; + } + } + } + + return true; + } + + /** + * Return {@code true} if this type resolves to a Class that represents an array. + * @see #getComponentType() + */ + public boolean isArray() { + if (this == NONE) { + return false; + } + return ((this.type instanceof Class && ((Class) this.type).isArray()) || + this.type instanceof GenericArrayType || resolveType().isArray()); + } + + /** + * Return the ResolvableType representing the component type of the array or + * {@link #NONE} if this type does not represent an array. + * @see #isArray() + */ + public ResolvableType getComponentType() { + if (this == NONE) { + return NONE; + } + if (this.componentType != null) { + return this.componentType; + } + if (this.type instanceof Class) { + Class componentType = ((Class) this.type).getComponentType(); + return forType(componentType, this.variableResolver); + } + if (this.type instanceof GenericArrayType) { + return forType(((GenericArrayType) this.type).getGenericComponentType(), this.variableResolver); + } + return resolveType().getComponentType(); + } + + /** + * Convenience method to return this type as a resolvable {@link Collection} type. + * Returns {@link #NONE} if this type does not implement or extend + * {@link Collection}. + * @see #as(Class) + * @see #asMap() + */ + public ResolvableType asCollection() { + return as(Collection.class); + } + + /** + * Convenience method to return this type as a resolvable {@link Map} type. + * Returns {@link #NONE} if this type does not implement or extend + * {@link Map}. + * @see #as(Class) + * @see #asCollection() + */ + public ResolvableType asMap() { + return as(Map.class); + } + + /** + * Return this type as a {@link ResolvableType} of the specified class. Searches + * {@link #getSuperType() supertype} and {@link #getInterfaces() interface} + * hierarchies to find a match, returning {@link #NONE} if this type does not + * implement or extend the specified class. + * @param type the required type (typically narrowed) + * @return a {@link ResolvableType} representing this object as the specified + * type, or {@link #NONE} if not resolvable as that type + * @see #asCollection() + * @see #asMap() + * @see #getSuperType() + * @see #getInterfaces() + */ + public ResolvableType as(Class type) { + if (this == NONE) { + return NONE; + } + Class resolved = resolve(); + if (resolved == null || resolved == type) { + return this; + } + for (ResolvableType interfaceType : getInterfaces()) { + ResolvableType interfaceAsType = interfaceType.as(type); + if (interfaceAsType != NONE) { + return interfaceAsType; + } + } + return getSuperType().as(type); + } + + /** + * Return a {@link ResolvableType} representing the direct supertype of this type. + * If no supertype is available this method returns {@link #NONE}. + *

    Note: The resulting {@link ResolvableType} instance may not be {@link Serializable}. + * @see #getInterfaces() + */ + public ResolvableType getSuperType() { + Class resolved = resolve(); + if (resolved == null) { + return NONE; + } + try { + Type superclass = resolved.getGenericSuperclass(); + if (superclass == null) { + return NONE; + } + ResolvableType superType = this.superType; + if (superType == null) { + superType = forType(superclass, this); + this.superType = superType; + } + return superType; + } + catch (TypeNotPresentException ex) { + // Ignore non-present types in generic signature + return NONE; + } + } + + /** + * Return a {@link ResolvableType} array representing the direct interfaces + * implemented by this type. If this type does not implement any interfaces an + * empty array is returned. + *

    Note: The resulting {@link ResolvableType} instances may not be {@link Serializable}. + * @see #getSuperType() + */ + public ResolvableType[] getInterfaces() { + Class resolved = resolve(); + if (resolved == null) { + return EMPTY_TYPES_ARRAY; + } + ResolvableType[] interfaces = this.interfaces; + if (interfaces == null) { + Type[] genericIfcs = resolved.getGenericInterfaces(); + interfaces = new ResolvableType[genericIfcs.length]; + for (int i = 0; i < genericIfcs.length; i++) { + interfaces[i] = forType(genericIfcs[i], this); + } + this.interfaces = interfaces; + } + return interfaces; + } + + /** + * Return {@code true} if this type contains generic parameters. + * @see #getGeneric(int...) + * @see #getGenerics() + */ + public boolean hasGenerics() { + return (getGenerics().length > 0); + } + + /** + * Return {@code true} if this type contains unresolvable generics only, + * that is, no substitute for any of its declared type variables. + */ + boolean isEntirelyUnresolvable() { + if (this == NONE) { + return false; + } + ResolvableType[] generics = getGenerics(); + for (ResolvableType generic : generics) { + if (!generic.isUnresolvableTypeVariable() && !generic.isWildcardWithoutBounds()) { + return false; + } + } + return true; + } + + /** + * Determine whether the underlying type has any unresolvable generics: + * either through an unresolvable type variable on the type itself + * or through implementing a generic interface in a raw fashion, + * i.e. without substituting that interface's type variables. + * The result will be {@code true} only in those two scenarios. + */ + public boolean hasUnresolvableGenerics() { + if (this == NONE) { + return false; + } + ResolvableType[] generics = getGenerics(); + for (ResolvableType generic : generics) { + if (generic.isUnresolvableTypeVariable() || generic.isWildcardWithoutBounds()) { + return true; + } + } + Class resolved = resolve(); + if (resolved != null) { + try { + for (Type genericInterface : resolved.getGenericInterfaces()) { + if (genericInterface instanceof Class) { + if (forClass((Class) genericInterface).hasGenerics()) { + return true; + } + } + } + } + catch (TypeNotPresentException ex) { + // Ignore non-present types in generic signature + } + return getSuperType().hasUnresolvableGenerics(); + } + return false; + } + + /** + * Determine whether the underlying type is a type variable that + * cannot be resolved through the associated variable resolver. + */ + private boolean isUnresolvableTypeVariable() { + if (this.type instanceof TypeVariable) { + if (this.variableResolver == null) { + return true; + } + TypeVariable variable = (TypeVariable) this.type; + ResolvableType resolved = this.variableResolver.resolveVariable(variable); + if (resolved == null || resolved.isUnresolvableTypeVariable()) { + return true; + } + } + return false; + } + + /** + * Determine whether the underlying type represents a wildcard + * without specific bounds (i.e., equal to {@code ? extends Object}). + */ + private boolean isWildcardWithoutBounds() { + if (this.type instanceof WildcardType) { + WildcardType wt = (WildcardType) this.type; + if (wt.getLowerBounds().length == 0) { + Type[] upperBounds = wt.getUpperBounds(); + if (upperBounds.length == 0 || (upperBounds.length == 1 && Object.class == upperBounds[0])) { + return true; + } + } + } + return false; + } + + /** + * Return a {@link ResolvableType} for the specified nesting level. + * See {@link #getNested(int, Map)} for details. + * @param nestingLevel the nesting level + * @return the {@link ResolvableType} type, or {@code #NONE} + */ + public ResolvableType getNested(int nestingLevel) { + return getNested(nestingLevel, null); + } + + /** + * Return a {@link ResolvableType} for the specified nesting level. + *

    The nesting level refers to the specific generic parameter that should be returned. + * A nesting level of 1 indicates this type; 2 indicates the first nested generic; + * 3 the second; and so on. For example, given {@code List>} level 1 refers + * to the {@code List}, level 2 the {@code Set}, and level 3 the {@code Integer}. + *

    The {@code typeIndexesPerLevel} map can be used to reference a specific generic + * for the given level. For example, an index of 0 would refer to a {@code Map} key; + * whereas, 1 would refer to the value. If the map does not contain a value for a + * specific level the last generic will be used (e.g. a {@code Map} value). + *

    Nesting levels may also apply to array types; for example given + * {@code String[]}, a nesting level of 2 refers to {@code String}. + *

    If a type does not {@link #hasGenerics() contain} generics the + * {@link #getSuperType() supertype} hierarchy will be considered. + * @param nestingLevel the required nesting level, indexed from 1 for the + * current type, 2 for the first nested generic, 3 for the second and so on + * @param typeIndexesPerLevel a map containing the generic index for a given + * nesting level (may be {@code null}) + * @return a {@link ResolvableType} for the nested level, or {@link #NONE} + */ + public ResolvableType getNested(int nestingLevel, @Nullable Map typeIndexesPerLevel) { + ResolvableType result = this; + for (int i = 2; i <= nestingLevel; i++) { + if (result.isArray()) { + result = result.getComponentType(); + } + else { + // Handle derived types + while (result != ResolvableType.NONE && !result.hasGenerics()) { + result = result.getSuperType(); + } + Integer index = (typeIndexesPerLevel != null ? typeIndexesPerLevel.get(i) : null); + index = (index == null ? result.getGenerics().length - 1 : index); + result = result.getGeneric(index); + } + } + return result; + } + + /** + * Return a {@link ResolvableType} representing the generic parameter for the + * given indexes. Indexes are zero based; for example given the type + * {@code Map>}, {@code getGeneric(0)} will access the + * {@code Integer}. Nested generics can be accessed by specifying multiple indexes; + * for example {@code getGeneric(1, 0)} will access the {@code String} from the + * nested {@code List}. For convenience, if no indexes are specified the first + * generic is returned. + *

    If no generic is available at the specified indexes {@link #NONE} is returned. + * @param indexes the indexes that refer to the generic parameter + * (may be omitted to return the first generic) + * @return a {@link ResolvableType} for the specified generic, or {@link #NONE} + * @see #hasGenerics() + * @see #getGenerics() + * @see #resolveGeneric(int...) + * @see #resolveGenerics() + */ + public ResolvableType getGeneric(@Nullable int... indexes) { + ResolvableType[] generics = getGenerics(); + if (indexes == null || indexes.length == 0) { + return (generics.length == 0 ? NONE : generics[0]); + } + ResolvableType generic = this; + for (int index : indexes) { + generics = generic.getGenerics(); + if (index < 0 || index >= generics.length) { + return NONE; + } + generic = generics[index]; + } + return generic; + } + + /** + * Return an array of {@link ResolvableType ResolvableTypes} representing the generic parameters of + * this type. If no generics are available an empty array is returned. If you need to + * access a specific generic consider using the {@link #getGeneric(int...)} method as + * it allows access to nested generics and protects against + * {@code IndexOutOfBoundsExceptions}. + * @return an array of {@link ResolvableType ResolvableTypes} representing the generic parameters + * (never {@code null}) + * @see #hasGenerics() + * @see #getGeneric(int...) + * @see #resolveGeneric(int...) + * @see #resolveGenerics() + */ + public ResolvableType[] getGenerics() { + if (this == NONE) { + return EMPTY_TYPES_ARRAY; + } + ResolvableType[] generics = this.generics; + if (generics == null) { + if (this.type instanceof Class) { + Type[] typeParams = ((Class) this.type).getTypeParameters(); + generics = new ResolvableType[typeParams.length]; + for (int i = 0; i < generics.length; i++) { + generics[i] = ResolvableType.forType(typeParams[i], this); + } + } + else if (this.type instanceof ParameterizedType) { + Type[] actualTypeArguments = ((ParameterizedType) this.type).getActualTypeArguments(); + generics = new ResolvableType[actualTypeArguments.length]; + for (int i = 0; i < actualTypeArguments.length; i++) { + generics[i] = forType(actualTypeArguments[i], this.variableResolver); + } + } + else { + generics = resolveType().getGenerics(); + } + this.generics = generics; + } + return generics; + } + + /** + * Convenience method that will {@link #getGenerics() get} and + * {@link #resolve() resolve} generic parameters. + * @return an array of resolved generic parameters (the resulting array + * will never be {@code null}, but it may contain {@code null} elements}) + * @see #getGenerics() + * @see #resolve() + */ + public Class[] resolveGenerics() { + ResolvableType[] generics = getGenerics(); + Class[] resolvedGenerics = new Class[generics.length]; + for (int i = 0; i < generics.length; i++) { + resolvedGenerics[i] = generics[i].resolve(); + } + return resolvedGenerics; + } + + /** + * Convenience method that will {@link #getGenerics() get} and {@link #resolve() + * resolve} generic parameters, using the specified {@code fallback} if any type + * cannot be resolved. + * @param fallback the fallback class to use if resolution fails + * @return an array of resolved generic parameters + * @see #getGenerics() + * @see #resolve() + */ + public Class[] resolveGenerics(Class fallback) { + ResolvableType[] generics = getGenerics(); + Class[] resolvedGenerics = new Class[generics.length]; + for (int i = 0; i < generics.length; i++) { + resolvedGenerics[i] = generics[i].resolve(fallback); + } + return resolvedGenerics; + } + + /** + * Convenience method that will {@link #getGeneric(int...) get} and + * {@link #resolve() resolve} a specific generic parameters. + * @param indexes the indexes that refer to the generic parameter + * (may be omitted to return the first generic) + * @return a resolved {@link Class} or {@code null} + * @see #getGeneric(int...) + * @see #resolve() + */ + @Nullable + public Class resolveGeneric(int... indexes) { + return getGeneric(indexes).resolve(); + } + + /** + * Resolve this type to a {@link java.lang.Class}, returning {@code null} + * if the type cannot be resolved. This method will consider bounds of + * {@link TypeVariable TypeVariables} and {@link WildcardType WildcardTypes} if + * direct resolution fails; however, bounds of {@code Object.class} will be ignored. + *

    If this method returns a non-null {@code Class} and {@link #hasGenerics()} + * returns {@code false}, the given type effectively wraps a plain {@code Class}, + * allowing for plain {@code Class} processing if desirable. + * @return the resolved {@link Class}, or {@code null} if not resolvable + * @see #resolve(Class) + * @see #resolveGeneric(int...) + * @see #resolveGenerics() + */ + @Nullable + public Class resolve() { + return this.resolved; + } + + /** + * Resolve this type to a {@link java.lang.Class}, returning the specified + * {@code fallback} if the type cannot be resolved. This method will consider bounds + * of {@link TypeVariable TypeVariables} and {@link WildcardType WildcardTypes} if + * direct resolution fails; however, bounds of {@code Object.class} will be ignored. + * @param fallback the fallback class to use if resolution fails + * @return the resolved {@link Class} or the {@code fallback} + * @see #resolve() + * @see #resolveGeneric(int...) + * @see #resolveGenerics() + */ + public Class resolve(Class fallback) { + return (this.resolved != null ? this.resolved : fallback); + } + + @Nullable + private Class resolveClass() { + if (this.type == EmptyType.INSTANCE) { + return null; + } + if (this.type instanceof Class) { + return (Class) this.type; + } + if (this.type instanceof GenericArrayType) { + Class resolvedComponent = getComponentType().resolve(); + return (resolvedComponent != null ? Array.newInstance(resolvedComponent, 0).getClass() : null); + } + return resolveType().resolve(); + } + + /** + * Resolve this type by a single level, returning the resolved value or {@link #NONE}. + *

    Note: The returned {@link ResolvableType} should only be used as an intermediary + * as it cannot be serialized. + */ + ResolvableType resolveType() { + if (this.type instanceof ParameterizedType) { + return forType(((ParameterizedType) this.type).getRawType(), this.variableResolver); + } + if (this.type instanceof WildcardType) { + Type resolved = resolveBounds(((WildcardType) this.type).getUpperBounds()); + if (resolved == null) { + resolved = resolveBounds(((WildcardType) this.type).getLowerBounds()); + } + return forType(resolved, this.variableResolver); + } + if (this.type instanceof TypeVariable) { + TypeVariable variable = (TypeVariable) this.type; + // Try default variable resolution + if (this.variableResolver != null) { + ResolvableType resolved = this.variableResolver.resolveVariable(variable); + if (resolved != null) { + return resolved; + } + } + // Fallback to bounds + return forType(resolveBounds(variable.getBounds()), this.variableResolver); + } + return NONE; + } + + @Nullable + private Type resolveBounds(Type[] bounds) { + if (bounds.length == 0 || bounds[0] == Object.class) { + return null; + } + return bounds[0]; + } + + @Nullable + private ResolvableType resolveVariable(TypeVariable variable) { + if (this.type instanceof TypeVariable) { + return resolveType().resolveVariable(variable); + } + if (this.type instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) this.type; + Class resolved = resolve(); + if (resolved == null) { + return null; + } + TypeVariable[] variables = resolved.getTypeParameters(); + for (int i = 0; i < variables.length; i++) { + if (ObjectUtils.nullSafeEquals(variables[i].getName(), variable.getName())) { + Type actualType = parameterizedType.getActualTypeArguments()[i]; + return forType(actualType, this.variableResolver); + } + } + Type ownerType = parameterizedType.getOwnerType(); + if (ownerType != null) { + return forType(ownerType, this.variableResolver).resolveVariable(variable); + } + } + if (this.type instanceof WildcardType) { + ResolvableType resolved = resolveType().resolveVariable(variable); + if (resolved != null) { + return resolved; + } + } + if (this.variableResolver != null) { + return this.variableResolver.resolveVariable(variable); + } + return null; + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof ResolvableType)) { + return false; + } + + ResolvableType otherType = (ResolvableType) other; + if (!ObjectUtils.nullSafeEquals(this.type, otherType.type)) { + return false; + } + if (this.typeProvider != otherType.typeProvider && + (this.typeProvider == null || otherType.typeProvider == null || + !ObjectUtils.nullSafeEquals(this.typeProvider.getType(), otherType.typeProvider.getType()))) { + return false; + } + if (this.variableResolver != otherType.variableResolver && + (this.variableResolver == null || otherType.variableResolver == null || + !ObjectUtils.nullSafeEquals(this.variableResolver.getSource(), otherType.variableResolver.getSource()))) { + return false; + } + if (!ObjectUtils.nullSafeEquals(this.componentType, otherType.componentType)) { + return false; + } + return true; + } + + @Override + public int hashCode() { + return (this.hash != null ? this.hash : calculateHashCode()); + } + + private int calculateHashCode() { + int hashCode = ObjectUtils.nullSafeHashCode(this.type); + if (this.typeProvider != null) { + hashCode = 31 * hashCode + ObjectUtils.nullSafeHashCode(this.typeProvider.getType()); + } + if (this.variableResolver != null) { + hashCode = 31 * hashCode + ObjectUtils.nullSafeHashCode(this.variableResolver.getSource()); + } + if (this.componentType != null) { + hashCode = 31 * hashCode + ObjectUtils.nullSafeHashCode(this.componentType); + } + return hashCode; + } + + /** + * Adapts this {@link ResolvableType} to a {@link VariableResolver}. + */ + @Nullable + VariableResolver asVariableResolver() { + if (this == NONE) { + return null; + } + return new DefaultVariableResolver(this); + } + + /** + * Custom serialization support for {@link #NONE}. + */ + private Object readResolve() { + return (this.type == EmptyType.INSTANCE ? NONE : this); + } + + /** + * Return a String representation of this type in its fully resolved form + * (including any generic parameters). + */ + @Override + public String toString() { + if (isArray()) { + return getComponentType() + "[]"; + } + if (this.resolved == null) { + return "?"; + } + if (this.type instanceof TypeVariable) { + TypeVariable variable = (TypeVariable) this.type; + if (this.variableResolver == null || this.variableResolver.resolveVariable(variable) == null) { + // Don't bother with variable boundaries for toString()... + // Can cause infinite recursions in case of self-references + return "?"; + } + } + if (hasGenerics()) { + return this.resolved.getName() + '<' + StringUtils.arrayToDelimitedString(getGenerics(), ", ") + '>'; + } + return this.resolved.getName(); + } + + + // Factory methods + + /** + * Return a {@link ResolvableType} for the specified {@link Class}, + * using the full generic type information for assignability checks. + * For example: {@code ResolvableType.forClass(MyArrayList.class)}. + * @param clazz the class to introspect ({@code null} is semantically + * equivalent to {@code Object.class} for typical use cases here) + * @return a {@link ResolvableType} for the specified class + * @see #forClass(Class, Class) + * @see #forClassWithGenerics(Class, Class...) + */ + public static ResolvableType forClass(@Nullable Class clazz) { + return new ResolvableType(clazz); + } + + /** + * Return a {@link ResolvableType} for the specified {@link Class}, + * doing assignability checks against the raw class only (analogous to + * {@link Class#isAssignableFrom}, which this serves as a wrapper for. + * For example: {@code ResolvableType.forRawClass(List.class)}. + * @param clazz the class to introspect ({@code null} is semantically + * equivalent to {@code Object.class} for typical use cases here) + * @return a {@link ResolvableType} for the specified class + * @since 4.2 + * @see #forClass(Class) + * @see #getRawClass() + */ + public static ResolvableType forRawClass(@Nullable Class clazz) { + return new ResolvableType(clazz) { + @Override + public ResolvableType[] getGenerics() { + return EMPTY_TYPES_ARRAY; + } + @Override + public boolean isAssignableFrom(Class other) { + return (clazz == null || ClassUtils.isAssignable(clazz, other)); + } + @Override + public boolean isAssignableFrom(ResolvableType other) { + Class otherClass = other.resolve(); + return (otherClass != null && (clazz == null || ClassUtils.isAssignable(clazz, otherClass))); + } + }; + } + + /** + * Return a {@link ResolvableType} for the specified base type + * (interface or base class) with a given implementation class. + * For example: {@code ResolvableType.forClass(List.class, MyArrayList.class)}. + * @param baseType the base type (must not be {@code null}) + * @param implementationClass the implementation class + * @return a {@link ResolvableType} for the specified base type backed by the + * given implementation class + * @see #forClass(Class) + * @see #forClassWithGenerics(Class, Class...) + */ + public static ResolvableType forClass(Class baseType, Class implementationClass) { + Assert.notNull(baseType, "Base type must not be null"); + ResolvableType asType = forType(implementationClass).as(baseType); + return (asType == NONE ? forType(baseType) : asType); + } + + /** + * Return a {@link ResolvableType} for the specified {@link Class} with pre-declared generics. + * @param clazz the class (or interface) to introspect + * @param generics the generics of the class + * @return a {@link ResolvableType} for the specific class and generics + * @see #forClassWithGenerics(Class, ResolvableType...) + */ + public static ResolvableType forClassWithGenerics(Class clazz, Class... generics) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(generics, "Generics array must not be null"); + ResolvableType[] resolvableGenerics = new ResolvableType[generics.length]; + for (int i = 0; i < generics.length; i++) { + resolvableGenerics[i] = forClass(generics[i]); + } + return forClassWithGenerics(clazz, resolvableGenerics); + } + + /** + * Return a {@link ResolvableType} for the specified {@link Class} with pre-declared generics. + * @param clazz the class (or interface) to introspect + * @param generics the generics of the class + * @return a {@link ResolvableType} for the specific class and generics + * @see #forClassWithGenerics(Class, Class...) + */ + public static ResolvableType forClassWithGenerics(Class clazz, ResolvableType... generics) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(generics, "Generics array must not be null"); + TypeVariable[] variables = clazz.getTypeParameters(); + Assert.isTrue(variables.length == generics.length, "Mismatched number of generics specified"); + + Type[] arguments = new Type[generics.length]; + for (int i = 0; i < generics.length; i++) { + ResolvableType generic = generics[i]; + Type argument = (generic != null ? generic.getType() : null); + arguments[i] = (argument != null && !(argument instanceof TypeVariable) ? argument : variables[i]); + } + + ParameterizedType syntheticType = new SyntheticParameterizedType(clazz, arguments); + return forType(syntheticType, new TypeVariablesVariableResolver(variables, generics)); + } + + /** + * Return a {@link ResolvableType} for the specified instance. The instance does not + * convey generic information but if it implements {@link ResolvableTypeProvider} a + * more precise {@link ResolvableType} can be used than the simple one based on + * the {@link #forClass(Class) Class instance}. + * @param instance the instance + * @return a {@link ResolvableType} for the specified instance + * @since 4.2 + * @see ResolvableTypeProvider + */ + public static ResolvableType forInstance(Object instance) { + Assert.notNull(instance, "Instance must not be null"); + if (instance instanceof ResolvableTypeProvider) { + ResolvableType type = ((ResolvableTypeProvider) instance).getResolvableType(); + if (type != null) { + return type; + } + } + return ResolvableType.forClass(instance.getClass()); + } + + /** + * Return a {@link ResolvableType} for the specified {@link Field}. + * @param field the source field + * @return a {@link ResolvableType} for the specified field + * @see #forField(Field, Class) + */ + public static ResolvableType forField(Field field) { + Assert.notNull(field, "Field must not be null"); + return forType(null, new FieldTypeProvider(field), null); + } + + /** + * Return a {@link ResolvableType} for the specified {@link Field} with a given + * implementation. + *

    Use this variant when the class that declares the field includes generic + * parameter variables that are satisfied by the implementation class. + * @param field the source field + * @param implementationClass the implementation class + * @return a {@link ResolvableType} for the specified field + * @see #forField(Field) + */ + public static ResolvableType forField(Field field, Class implementationClass) { + Assert.notNull(field, "Field must not be null"); + ResolvableType owner = forType(implementationClass).as(field.getDeclaringClass()); + return forType(null, new FieldTypeProvider(field), owner.asVariableResolver()); + } + + /** + * Return a {@link ResolvableType} for the specified {@link Field} with a given + * implementation. + *

    Use this variant when the class that declares the field includes generic + * parameter variables that are satisfied by the implementation type. + * @param field the source field + * @param implementationType the implementation type + * @return a {@link ResolvableType} for the specified field + * @see #forField(Field) + */ + public static ResolvableType forField(Field field, @Nullable ResolvableType implementationType) { + Assert.notNull(field, "Field must not be null"); + ResolvableType owner = (implementationType != null ? implementationType : NONE); + owner = owner.as(field.getDeclaringClass()); + return forType(null, new FieldTypeProvider(field), owner.asVariableResolver()); + } + + /** + * Return a {@link ResolvableType} for the specified {@link Field} with the + * given nesting level. + * @param field the source field + * @param nestingLevel the nesting level (1 for the outer level; 2 for a nested + * generic type; etc) + * @see #forField(Field) + */ + public static ResolvableType forField(Field field, int nestingLevel) { + Assert.notNull(field, "Field must not be null"); + return forType(null, new FieldTypeProvider(field), null).getNested(nestingLevel); + } + + /** + * Return a {@link ResolvableType} for the specified {@link Field} with a given + * implementation and the given nesting level. + *

    Use this variant when the class that declares the field includes generic + * parameter variables that are satisfied by the implementation class. + * @param field the source field + * @param nestingLevel the nesting level (1 for the outer level; 2 for a nested + * generic type; etc) + * @param implementationClass the implementation class + * @return a {@link ResolvableType} for the specified field + * @see #forField(Field) + */ + public static ResolvableType forField(Field field, int nestingLevel, @Nullable Class implementationClass) { + Assert.notNull(field, "Field must not be null"); + ResolvableType owner = forType(implementationClass).as(field.getDeclaringClass()); + return forType(null, new FieldTypeProvider(field), owner.asVariableResolver()).getNested(nestingLevel); + } + + /** + * Return a {@link ResolvableType} for the specified {@link Constructor} parameter. + * @param constructor the source constructor (must not be {@code null}) + * @param parameterIndex the parameter index + * @return a {@link ResolvableType} for the specified constructor parameter + * @see #forConstructorParameter(Constructor, int, Class) + */ + public static ResolvableType forConstructorParameter(Constructor constructor, int parameterIndex) { + Assert.notNull(constructor, "Constructor must not be null"); + return forMethodParameter(new MethodParameter(constructor, parameterIndex)); + } + + /** + * Return a {@link ResolvableType} for the specified {@link Constructor} parameter + * with a given implementation. Use this variant when the class that declares the + * constructor includes generic parameter variables that are satisfied by the + * implementation class. + * @param constructor the source constructor (must not be {@code null}) + * @param parameterIndex the parameter index + * @param implementationClass the implementation class + * @return a {@link ResolvableType} for the specified constructor parameter + * @see #forConstructorParameter(Constructor, int) + */ + public static ResolvableType forConstructorParameter(Constructor constructor, int parameterIndex, + Class implementationClass) { + + Assert.notNull(constructor, "Constructor must not be null"); + MethodParameter methodParameter = new MethodParameter(constructor, parameterIndex, implementationClass); + return forMethodParameter(methodParameter); + } + + /** + * Return a {@link ResolvableType} for the specified {@link Method} return type. + * @param method the source for the method return type + * @return a {@link ResolvableType} for the specified method return + * @see #forMethodReturnType(Method, Class) + */ + public static ResolvableType forMethodReturnType(Method method) { + Assert.notNull(method, "Method must not be null"); + return forMethodParameter(new MethodParameter(method, -1)); + } + + /** + * Return a {@link ResolvableType} for the specified {@link Method} return type. + * Use this variant when the class that declares the method includes generic + * parameter variables that are satisfied by the implementation class. + * @param method the source for the method return type + * @param implementationClass the implementation class + * @return a {@link ResolvableType} for the specified method return + * @see #forMethodReturnType(Method) + */ + public static ResolvableType forMethodReturnType(Method method, Class implementationClass) { + Assert.notNull(method, "Method must not be null"); + MethodParameter methodParameter = new MethodParameter(method, -1, implementationClass); + return forMethodParameter(methodParameter); + } + + /** + * Return a {@link ResolvableType} for the specified {@link Method} parameter. + * @param method the source method (must not be {@code null}) + * @param parameterIndex the parameter index + * @return a {@link ResolvableType} for the specified method parameter + * @see #forMethodParameter(Method, int, Class) + * @see #forMethodParameter(MethodParameter) + */ + public static ResolvableType forMethodParameter(Method method, int parameterIndex) { + Assert.notNull(method, "Method must not be null"); + return forMethodParameter(new MethodParameter(method, parameterIndex)); + } + + /** + * Return a {@link ResolvableType} for the specified {@link Method} parameter with a + * given implementation. Use this variant when the class that declares the method + * includes generic parameter variables that are satisfied by the implementation class. + * @param method the source method (must not be {@code null}) + * @param parameterIndex the parameter index + * @param implementationClass the implementation class + * @return a {@link ResolvableType} for the specified method parameter + * @see #forMethodParameter(Method, int, Class) + * @see #forMethodParameter(MethodParameter) + */ + public static ResolvableType forMethodParameter(Method method, int parameterIndex, Class implementationClass) { + Assert.notNull(method, "Method must not be null"); + MethodParameter methodParameter = new MethodParameter(method, parameterIndex, implementationClass); + return forMethodParameter(methodParameter); + } + + /** + * Return a {@link ResolvableType} for the specified {@link MethodParameter}. + * @param methodParameter the source method parameter (must not be {@code null}) + * @return a {@link ResolvableType} for the specified method parameter + * @see #forMethodParameter(Method, int) + */ + public static ResolvableType forMethodParameter(MethodParameter methodParameter) { + return forMethodParameter(methodParameter, (Type) null); + } + + /** + * Return a {@link ResolvableType} for the specified {@link MethodParameter} with a + * given implementation type. Use this variant when the class that declares the method + * includes generic parameter variables that are satisfied by the implementation type. + * @param methodParameter the source method parameter (must not be {@code null}) + * @param implementationType the implementation type + * @return a {@link ResolvableType} for the specified method parameter + * @see #forMethodParameter(MethodParameter) + */ + public static ResolvableType forMethodParameter(MethodParameter methodParameter, + @Nullable ResolvableType implementationType) { + + Assert.notNull(methodParameter, "MethodParameter must not be null"); + implementationType = (implementationType != null ? implementationType : + forType(methodParameter.getContainingClass())); + ResolvableType owner = implementationType.as(methodParameter.getDeclaringClass()); + return forType(null, new MethodParameterTypeProvider(methodParameter), owner.asVariableResolver()). + getNested(methodParameter.getNestingLevel(), methodParameter.typeIndexesPerLevel); + } + + /** + * Return a {@link ResolvableType} for the specified {@link MethodParameter}, + * overriding the target type to resolve with a specific given type. + * @param methodParameter the source method parameter (must not be {@code null}) + * @param targetType the type to resolve (a part of the method parameter's type) + * @return a {@link ResolvableType} for the specified method parameter + * @see #forMethodParameter(Method, int) + */ + public static ResolvableType forMethodParameter(MethodParameter methodParameter, @Nullable Type targetType) { + Assert.notNull(methodParameter, "MethodParameter must not be null"); + return forMethodParameter(methodParameter, targetType, methodParameter.getNestingLevel()); + } + + /** + * Return a {@link ResolvableType} for the specified {@link MethodParameter} at + * a specific nesting level, overriding the target type to resolve with a specific + * given type. + * @param methodParameter the source method parameter (must not be {@code null}) + * @param targetType the type to resolve (a part of the method parameter's type) + * @param nestingLevel the nesting level to use + * @return a {@link ResolvableType} for the specified method parameter + * @since 5.2 + * @see #forMethodParameter(Method, int) + */ + static ResolvableType forMethodParameter( + MethodParameter methodParameter, @Nullable Type targetType, int nestingLevel) { + + ResolvableType owner = forType(methodParameter.getContainingClass()).as(methodParameter.getDeclaringClass()); + return forType(targetType, new MethodParameterTypeProvider(methodParameter), owner.asVariableResolver()). + getNested(nestingLevel, methodParameter.typeIndexesPerLevel); + } + + /** + * Return a {@link ResolvableType} as a array of the specified {@code componentType}. + * @param componentType the component type + * @return a {@link ResolvableType} as an array of the specified component type + */ + public static ResolvableType forArrayComponent(ResolvableType componentType) { + Assert.notNull(componentType, "Component type must not be null"); + Class arrayClass = Array.newInstance(componentType.resolve(), 0).getClass(); + return new ResolvableType(arrayClass, null, null, componentType); + } + + /** + * Return a {@link ResolvableType} for the specified {@link Type}. + *

    Note: The resulting {@link ResolvableType} instance may not be {@link Serializable}. + * @param type the source type (potentially {@code null}) + * @return a {@link ResolvableType} for the specified {@link Type} + * @see #forType(Type, ResolvableType) + */ + public static ResolvableType forType(@Nullable Type type) { + return forType(type, null, null); + } + + /** + * Return a {@link ResolvableType} for the specified {@link Type} backed by the given + * owner type. + *

    Note: The resulting {@link ResolvableType} instance may not be {@link Serializable}. + * @param type the source type or {@code null} + * @param owner the owner type used to resolve variables + * @return a {@link ResolvableType} for the specified {@link Type} and owner + * @see #forType(Type) + */ + public static ResolvableType forType(@Nullable Type type, @Nullable ResolvableType owner) { + VariableResolver variableResolver = null; + if (owner != null) { + variableResolver = owner.asVariableResolver(); + } + return forType(type, variableResolver); + } + + + /** + * Return a {@link ResolvableType} for the specified {@link ParameterizedTypeReference}. + *

    Note: The resulting {@link ResolvableType} instance may not be {@link Serializable}. + * @param typeReference the reference to obtain the source type from + * @return a {@link ResolvableType} for the specified {@link ParameterizedTypeReference} + * @since 4.3.12 + * @see #forType(Type) + */ + public static ResolvableType forType(ParameterizedTypeReference typeReference) { + return forType(typeReference.getType(), null, null); + } + + /** + * Return a {@link ResolvableType} for the specified {@link Type} backed by a given + * {@link VariableResolver}. + * @param type the source type or {@code null} + * @param variableResolver the variable resolver or {@code null} + * @return a {@link ResolvableType} for the specified {@link Type} and {@link VariableResolver} + */ + static ResolvableType forType(@Nullable Type type, @Nullable VariableResolver variableResolver) { + return forType(type, null, variableResolver); + } + + /** + * Return a {@link ResolvableType} for the specified {@link Type} backed by a given + * {@link VariableResolver}. + * @param type the source type or {@code null} + * @param typeProvider the type provider or {@code null} + * @param variableResolver the variable resolver or {@code null} + * @return a {@link ResolvableType} for the specified {@link Type} and {@link VariableResolver} + */ + static ResolvableType forType( + @Nullable Type type, @Nullable TypeProvider typeProvider, @Nullable VariableResolver variableResolver) { + + if (type == null && typeProvider != null) { + type = SerializableTypeWrapper.forTypeProvider(typeProvider); + } + if (type == null) { + return NONE; + } + + // For simple Class references, build the wrapper right away - + // no expensive resolution necessary, so not worth caching... + if (type instanceof Class) { + return new ResolvableType(type, typeProvider, variableResolver, (ResolvableType) null); + } + + // Purge empty entries on access since we don't have a clean-up thread or the like. + cache.purgeUnreferencedEntries(); + + // Check the cache - we may have a ResolvableType which has been resolved before... + ResolvableType resultType = new ResolvableType(type, typeProvider, variableResolver); + ResolvableType cachedType = cache.get(resultType); + if (cachedType == null) { + cachedType = new ResolvableType(type, typeProvider, variableResolver, resultType.hash); + cache.put(cachedType, cachedType); + } + resultType.resolved = cachedType.resolved; + return resultType; + } + + /** + * Clear the internal {@code ResolvableType}/{@code SerializableTypeWrapper} cache. + * @since 4.2 + */ + public static void clearCache() { + cache.clear(); + SerializableTypeWrapper.cache.clear(); + } + + + /** + * Strategy interface used to resolve {@link TypeVariable TypeVariables}. + */ + interface VariableResolver extends Serializable { + + /** + * Return the source of the resolver (used for hashCode and equals). + */ + Object getSource(); + + /** + * Resolve the specified variable. + * @param variable the variable to resolve + * @return the resolved variable, or {@code null} if not found + */ + @Nullable + ResolvableType resolveVariable(TypeVariable variable); + } + + + @SuppressWarnings("serial") + private static class DefaultVariableResolver implements VariableResolver { + + private final ResolvableType source; + + DefaultVariableResolver(ResolvableType resolvableType) { + this.source = resolvableType; + } + + @Override + @Nullable + public ResolvableType resolveVariable(TypeVariable variable) { + return this.source.resolveVariable(variable); + } + + @Override + public Object getSource() { + return this.source; + } + } + + + @SuppressWarnings("serial") + private static class TypeVariablesVariableResolver implements VariableResolver { + + private final TypeVariable[] variables; + + private final ResolvableType[] generics; + + public TypeVariablesVariableResolver(TypeVariable[] variables, ResolvableType[] generics) { + this.variables = variables; + this.generics = generics; + } + + @Override + @Nullable + public ResolvableType resolveVariable(TypeVariable variable) { + TypeVariable variableToCompare = SerializableTypeWrapper.unwrap(variable); + for (int i = 0; i < this.variables.length; i++) { + TypeVariable resolvedVariable = SerializableTypeWrapper.unwrap(this.variables[i]); + if (ObjectUtils.nullSafeEquals(resolvedVariable, variableToCompare)) { + return this.generics[i]; + } + } + return null; + } + + @Override + public Object getSource() { + return this.generics; + } + } + + + private static final class SyntheticParameterizedType implements ParameterizedType, Serializable { + + private final Type rawType; + + private final Type[] typeArguments; + + public SyntheticParameterizedType(Type rawType, Type[] typeArguments) { + this.rawType = rawType; + this.typeArguments = typeArguments; + } + + @Override + public String getTypeName() { + String typeName = this.rawType.getTypeName(); + if (this.typeArguments.length > 0) { + StringJoiner stringJoiner = new StringJoiner(", ", "<", ">"); + for (Type argument : this.typeArguments) { + stringJoiner.add(argument.getTypeName()); + } + return typeName + stringJoiner; + } + return typeName; + } + + @Override + @Nullable + public Type getOwnerType() { + return null; + } + + @Override + public Type getRawType() { + return this.rawType; + } + + @Override + public Type[] getActualTypeArguments() { + return this.typeArguments; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof ParameterizedType)) { + return false; + } + ParameterizedType otherType = (ParameterizedType) other; + return (otherType.getOwnerType() == null && this.rawType.equals(otherType.getRawType()) && + Arrays.equals(this.typeArguments, otherType.getActualTypeArguments())); + } + + @Override + public int hashCode() { + return (this.rawType.hashCode() * 31 + Arrays.hashCode(this.typeArguments)); + } + + @Override + public String toString() { + return getTypeName(); + } + } + + + /** + * Internal helper to handle bounds from {@link WildcardType WildcardTypes}. + */ + private static class WildcardBounds { + + private final Kind kind; + + private final ResolvableType[] bounds; + + /** + * Internal constructor to create a new {@link WildcardBounds} instance. + * @param kind the kind of bounds + * @param bounds the bounds + * @see #get(ResolvableType) + */ + public WildcardBounds(Kind kind, ResolvableType[] bounds) { + this.kind = kind; + this.bounds = bounds; + } + + /** + * Return {@code true} if this bounds is the same kind as the specified bounds. + */ + public boolean isSameKind(WildcardBounds bounds) { + return this.kind == bounds.kind; + } + + /** + * Return {@code true} if this bounds is assignable to all the specified types. + * @param types the types to test against + * @return {@code true} if this bounds is assignable to all types + */ + public boolean isAssignableFrom(ResolvableType... types) { + for (ResolvableType bound : this.bounds) { + for (ResolvableType type : types) { + if (!isAssignable(bound, type)) { + return false; + } + } + } + return true; + } + + private boolean isAssignable(ResolvableType source, ResolvableType from) { + return (this.kind == Kind.UPPER ? source.isAssignableFrom(from) : from.isAssignableFrom(source)); + } + + /** + * Return the underlying bounds. + */ + public ResolvableType[] getBounds() { + return this.bounds; + } + + /** + * Get a {@link WildcardBounds} instance for the specified type, returning + * {@code null} if the specified type cannot be resolved to a {@link WildcardType}. + * @param type the source type + * @return a {@link WildcardBounds} instance or {@code null} + */ + @Nullable + public static WildcardBounds get(ResolvableType type) { + ResolvableType resolveToWildcard = type; + while (!(resolveToWildcard.getType() instanceof WildcardType)) { + if (resolveToWildcard == NONE) { + return null; + } + resolveToWildcard = resolveToWildcard.resolveType(); + } + WildcardType wildcardType = (WildcardType) resolveToWildcard.type; + Kind boundsType = (wildcardType.getLowerBounds().length > 0 ? Kind.LOWER : Kind.UPPER); + Type[] bounds = (boundsType == Kind.UPPER ? wildcardType.getUpperBounds() : wildcardType.getLowerBounds()); + ResolvableType[] resolvableBounds = new ResolvableType[bounds.length]; + for (int i = 0; i < bounds.length; i++) { + resolvableBounds[i] = ResolvableType.forType(bounds[i], type.variableResolver); + } + return new WildcardBounds(boundsType, resolvableBounds); + } + + /** + * The various kinds of bounds. + */ + enum Kind {UPPER, LOWER} + } + + + /** + * Internal {@link Type} used to represent an empty value. + */ + @SuppressWarnings("serial") + static class EmptyType implements Type, Serializable { + + static final Type INSTANCE = new EmptyType(); + + Object readResolve() { + return INSTANCE; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/ResolvableTypeProvider.java b/spring-core/src/main/java/org/springframework/core/ResolvableTypeProvider.java new file mode 100644 index 0000000..93f7de9 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/ResolvableTypeProvider.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import org.springframework.lang.Nullable; + +/** + * Any object can implement this interface to provide its actual {@link ResolvableType}. + * + *

    Such information is very useful when figuring out if the instance matches a generic + * signature as Java does not convey the signature at runtime. + * + *

    Users of this interface should be careful in complex hierarchy scenarios, especially + * when the generic type signature of the class changes in sub-classes. It is always + * possible to return {@code null} to fallback on a default behavior. + * + * @author Stephane Nicoll + * @since 4.2 + */ +public interface ResolvableTypeProvider { + + /** + * Return the {@link ResolvableType} describing this instance + * (or {@code null} if some sort of default should be applied instead). + */ + @Nullable + ResolvableType getResolvableType(); + +} diff --git a/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.java b/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.java new file mode 100644 index 0000000..8a3a18e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.java @@ -0,0 +1,385 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.lang.reflect.Field; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Proxy; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; + +import org.springframework.lang.Nullable; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; + +/** + * Internal utility class that can be used to obtain wrapped {@link Serializable} + * variants of {@link java.lang.reflect.Type java.lang.reflect.Types}. + * + *

    {@link #forField(Field) Fields} or {@link #forMethodParameter(MethodParameter) + * MethodParameters} can be used as the root source for a serializable type. + * Alternatively, a regular {@link Class} can also be used as source. + * + *

    The returned type will either be a {@link Class} or a serializable proxy of + * {@link GenericArrayType}, {@link ParameterizedType}, {@link TypeVariable} or + * {@link WildcardType}. With the exception of {@link Class} (which is final) calls + * to methods that return further {@link Type Types} (for example + * {@link GenericArrayType#getGenericComponentType()}) will be automatically wrapped. + * + * @author Phillip Webb + * @author Juergen Hoeller + * @author Sam Brannen + * @since 4.0 + */ +final class SerializableTypeWrapper { + + private static final Class[] SUPPORTED_SERIALIZABLE_TYPES = { + GenericArrayType.class, ParameterizedType.class, TypeVariable.class, WildcardType.class}; + + /** + * Whether this environment lives within a native image. + * Exposed as a private static field rather than in a {@code NativeImageDetector.inNativeImage()} static method due to https://github.com/oracle/graal/issues/2594. + * @see ImageInfo.java + */ + private static final boolean IN_NATIVE_IMAGE = (System.getProperty("org.graalvm.nativeimage.imagecode") != null); + + static final ConcurrentReferenceHashMap cache = new ConcurrentReferenceHashMap<>(256); + + + private SerializableTypeWrapper() { + } + + + /** + * Return a {@link Serializable} variant of {@link Field#getGenericType()}. + */ + @Nullable + public static Type forField(Field field) { + return forTypeProvider(new FieldTypeProvider(field)); + } + + /** + * Return a {@link Serializable} variant of + * {@link MethodParameter#getGenericParameterType()}. + */ + @Nullable + public static Type forMethodParameter(MethodParameter methodParameter) { + return forTypeProvider(new MethodParameterTypeProvider(methodParameter)); + } + + /** + * Unwrap the given type, effectively returning the original non-serializable type. + * @param type the type to unwrap + * @return the original non-serializable type + */ + @SuppressWarnings("unchecked") + public static T unwrap(T type) { + Type unwrapped = null; + if (type instanceof SerializableTypeProxy) { + unwrapped = ((SerializableTypeProxy) type).getTypeProvider().getType(); + } + return (unwrapped != null ? (T) unwrapped : type); + } + + /** + * Return a {@link Serializable} {@link Type} backed by a {@link TypeProvider} . + *

    If type artifacts are generally not serializable in the current runtime + * environment, this delegate will simply return the original {@code Type} as-is. + */ + @Nullable + static Type forTypeProvider(TypeProvider provider) { + Type providedType = provider.getType(); + if (providedType == null || providedType instanceof Serializable) { + // No serializable type wrapping necessary (e.g. for java.lang.Class) + return providedType; + } + if (IN_NATIVE_IMAGE || !Serializable.class.isAssignableFrom(Class.class)) { + // Let's skip any wrapping attempts if types are generally not serializable in + // the current runtime environment (even java.lang.Class itself, e.g. on GraalVM native images) + return providedType; + } + + // Obtain a serializable type proxy for the given provider... + Type cached = cache.get(providedType); + if (cached != null) { + return cached; + } + for (Class type : SUPPORTED_SERIALIZABLE_TYPES) { + if (type.isInstance(providedType)) { + ClassLoader classLoader = provider.getClass().getClassLoader(); + Class[] interfaces = new Class[] {type, SerializableTypeProxy.class, Serializable.class}; + InvocationHandler handler = new TypeProxyInvocationHandler(provider); + cached = (Type) Proxy.newProxyInstance(classLoader, interfaces, handler); + cache.put(providedType, cached); + return cached; + } + } + throw new IllegalArgumentException("Unsupported Type class: " + providedType.getClass().getName()); + } + + + /** + * Additional interface implemented by the type proxy. + */ + interface SerializableTypeProxy { + + /** + * Return the underlying type provider. + */ + TypeProvider getTypeProvider(); + } + + + /** + * A {@link Serializable} interface providing access to a {@link Type}. + */ + @SuppressWarnings("serial") + interface TypeProvider extends Serializable { + + /** + * Return the (possibly non {@link Serializable}) {@link Type}. + */ + @Nullable + Type getType(); + + /** + * Return the source of the type, or {@code null} if not known. + *

    The default implementations returns {@code null}. + */ + @Nullable + default Object getSource() { + return null; + } + } + + + /** + * {@link Serializable} {@link InvocationHandler} used by the proxied {@link Type}. + * Provides serialization support and enhances any methods that return {@code Type} + * or {@code Type[]}. + */ + @SuppressWarnings("serial") + private static class TypeProxyInvocationHandler implements InvocationHandler, Serializable { + + private final TypeProvider provider; + + public TypeProxyInvocationHandler(TypeProvider provider) { + this.provider = provider; + } + + @Override + @Nullable + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + switch (method.getName()) { + case "equals": + Object other = args[0]; + // Unwrap proxies for speed + if (other instanceof Type) { + other = unwrap((Type) other); + } + return ObjectUtils.nullSafeEquals(this.provider.getType(), other); + case "hashCode": + return ObjectUtils.nullSafeHashCode(this.provider.getType()); + case "getTypeProvider": + return this.provider; + } + + if (Type.class == method.getReturnType() && ObjectUtils.isEmpty(args)) { + return forTypeProvider(new MethodInvokeTypeProvider(this.provider, method, -1)); + } + else if (Type[].class == method.getReturnType() && ObjectUtils.isEmpty(args)) { + Type[] result = new Type[((Type[]) method.invoke(this.provider.getType())).length]; + for (int i = 0; i < result.length; i++) { + result[i] = forTypeProvider(new MethodInvokeTypeProvider(this.provider, method, i)); + } + return result; + } + + try { + return method.invoke(this.provider.getType(), args); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + } + + + /** + * {@link TypeProvider} for {@link Type Types} obtained from a {@link Field}. + */ + @SuppressWarnings("serial") + static class FieldTypeProvider implements TypeProvider { + + private final String fieldName; + + private final Class declaringClass; + + private transient Field field; + + public FieldTypeProvider(Field field) { + this.fieldName = field.getName(); + this.declaringClass = field.getDeclaringClass(); + this.field = field; + } + + @Override + public Type getType() { + return this.field.getGenericType(); + } + + @Override + public Object getSource() { + return this.field; + } + + private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException { + inputStream.defaultReadObject(); + try { + this.field = this.declaringClass.getDeclaredField(this.fieldName); + } + catch (Throwable ex) { + throw new IllegalStateException("Could not find original class structure", ex); + } + } + } + + + /** + * {@link TypeProvider} for {@link Type Types} obtained from a {@link MethodParameter}. + */ + @SuppressWarnings("serial") + static class MethodParameterTypeProvider implements TypeProvider { + + @Nullable + private final String methodName; + + private final Class[] parameterTypes; + + private final Class declaringClass; + + private final int parameterIndex; + + private transient MethodParameter methodParameter; + + public MethodParameterTypeProvider(MethodParameter methodParameter) { + this.methodName = (methodParameter.getMethod() != null ? methodParameter.getMethod().getName() : null); + this.parameterTypes = methodParameter.getExecutable().getParameterTypes(); + this.declaringClass = methodParameter.getDeclaringClass(); + this.parameterIndex = methodParameter.getParameterIndex(); + this.methodParameter = methodParameter; + } + + @Override + public Type getType() { + return this.methodParameter.getGenericParameterType(); + } + + @Override + public Object getSource() { + return this.methodParameter; + } + + private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException { + inputStream.defaultReadObject(); + try { + if (this.methodName != null) { + this.methodParameter = new MethodParameter( + this.declaringClass.getDeclaredMethod(this.methodName, this.parameterTypes), this.parameterIndex); + } + else { + this.methodParameter = new MethodParameter( + this.declaringClass.getDeclaredConstructor(this.parameterTypes), this.parameterIndex); + } + } + catch (Throwable ex) { + throw new IllegalStateException("Could not find original class structure", ex); + } + } + } + + + /** + * {@link TypeProvider} for {@link Type Types} obtained by invoking a no-arg method. + */ + @SuppressWarnings("serial") + static class MethodInvokeTypeProvider implements TypeProvider { + + private final TypeProvider provider; + + private final String methodName; + + private final Class declaringClass; + + private final int index; + + private transient Method method; + + @Nullable + private transient volatile Object result; + + public MethodInvokeTypeProvider(TypeProvider provider, Method method, int index) { + this.provider = provider; + this.methodName = method.getName(); + this.declaringClass = method.getDeclaringClass(); + this.index = index; + this.method = method; + } + + @Override + @Nullable + public Type getType() { + Object result = this.result; + if (result == null) { + // Lazy invocation of the target method on the provided type + result = ReflectionUtils.invokeMethod(this.method, this.provider.getType()); + // Cache the result for further calls to getType() + this.result = result; + } + return (result instanceof Type[] ? ((Type[]) result)[this.index] : (Type) result); + } + + @Override + @Nullable + public Object getSource() { + return null; + } + + private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException { + inputStream.defaultReadObject(); + Method method = ReflectionUtils.findMethod(this.declaringClass, this.methodName); + if (method == null) { + throw new IllegalStateException("Cannot find method on deserialization: " + this.methodName); + } + if (method.getReturnType() != Type.class && method.getReturnType() != Type[].class) { + throw new IllegalStateException( + "Invalid return type on deserialized method - needs to be Type or Type[]: " + method); + } + this.method = method; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/SimpleAliasRegistry.java b/spring-core/src/main/java/org/springframework/core/SimpleAliasRegistry.java new file mode 100644 index 0000000..1eee8f2 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/SimpleAliasRegistry.java @@ -0,0 +1,223 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.util.ArrayList; +import java.util.HashMap; +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.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; +import org.springframework.util.StringValueResolver; + +/** + * Simple implementation of the {@link AliasRegistry} interface. + *

    Serves as base class for + * {@link org.springframework.beans.factory.support.BeanDefinitionRegistry} + * implementations. + * + * @author Juergen Hoeller + * @author Qimiao Chen + * @since 2.5.2 + */ +public class SimpleAliasRegistry implements AliasRegistry { + + /** Logger available to subclasses. */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** Map from alias to canonical name. */ + private final Map aliasMap = new ConcurrentHashMap<>(16); + + + @Override + public void registerAlias(String name, String alias) { + Assert.hasText(name, "'name' must not be empty"); + Assert.hasText(alias, "'alias' must not be empty"); + synchronized (this.aliasMap) { + if (alias.equals(name)) { + this.aliasMap.remove(alias); + if (logger.isDebugEnabled()) { + logger.debug("Alias definition '" + alias + "' ignored since it points to same name"); + } + } + else { + String registeredName = this.aliasMap.get(alias); + if (registeredName != null) { + if (registeredName.equals(name)) { + // An existing alias - no need to re-register + return; + } + if (!allowAliasOverriding()) { + throw new IllegalStateException("Cannot define alias '" + alias + "' for name '" + + name + "': It is already registered for name '" + registeredName + "'."); + } + if (logger.isDebugEnabled()) { + logger.debug("Overriding alias '" + alias + "' definition for registered name '" + + registeredName + "' with new target name '" + name + "'"); + } + } + checkForAliasCircle(name, alias); + this.aliasMap.put(alias, name); + if (logger.isTraceEnabled()) { + logger.trace("Alias definition '" + alias + "' registered for name '" + name + "'"); + } + } + } + } + + /** + * Determine whether alias overriding is allowed. + *

    Default is {@code true}. + */ + protected boolean allowAliasOverriding() { + return true; + } + + /** + * Determine whether the given name has the given alias registered. + * @param name the name to check + * @param alias the alias to look for + * @since 4.2.1 + */ + public boolean hasAlias(String name, String alias) { + String registeredName = this.aliasMap.get(alias); + return ObjectUtils.nullSafeEquals(registeredName, name) || (registeredName != null + && hasAlias(name, registeredName)); + } + + @Override + public void removeAlias(String alias) { + synchronized (this.aliasMap) { + String name = this.aliasMap.remove(alias); + if (name == null) { + throw new IllegalStateException("No alias '" + alias + "' registered"); + } + } + } + + @Override + public boolean isAlias(String name) { + return this.aliasMap.containsKey(name); + } + + @Override + public String[] getAliases(String name) { + List result = new ArrayList<>(); + synchronized (this.aliasMap) { + retrieveAliases(name, result); + } + return StringUtils.toStringArray(result); + } + + /** + * Transitively retrieve all aliases for the given name. + * @param name the target name to find aliases for + * @param result the resulting aliases list + */ + private void retrieveAliases(String name, List result) { + this.aliasMap.forEach((alias, registeredName) -> { + if (registeredName.equals(name)) { + result.add(alias); + retrieveAliases(alias, result); + } + }); + } + + /** + * Resolve all alias target names and aliases registered in this + * registry, applying the given {@link StringValueResolver} to them. + *

    The value resolver may for example resolve placeholders + * in target bean names and even in alias names. + * @param valueResolver the StringValueResolver to apply + */ + public void resolveAliases(StringValueResolver valueResolver) { + Assert.notNull(valueResolver, "StringValueResolver must not be null"); + synchronized (this.aliasMap) { + Map aliasCopy = new HashMap<>(this.aliasMap); + aliasCopy.forEach((alias, registeredName) -> { + String resolvedAlias = valueResolver.resolveStringValue(alias); + String resolvedName = valueResolver.resolveStringValue(registeredName); + if (resolvedAlias == null || resolvedName == null || resolvedAlias.equals(resolvedName)) { + this.aliasMap.remove(alias); + } + else if (!resolvedAlias.equals(alias)) { + String existingName = this.aliasMap.get(resolvedAlias); + if (existingName != null) { + if (existingName.equals(resolvedName)) { + // Pointing to existing alias - just remove placeholder + this.aliasMap.remove(alias); + return; + } + throw new IllegalStateException( + "Cannot register resolved alias '" + resolvedAlias + "' (original: '" + alias + + "') for name '" + resolvedName + "': It is already registered for name '" + + registeredName + "'."); + } + checkForAliasCircle(resolvedName, resolvedAlias); + this.aliasMap.remove(alias); + this.aliasMap.put(resolvedAlias, resolvedName); + } + else if (!registeredName.equals(resolvedName)) { + this.aliasMap.put(alias, resolvedName); + } + }); + } + } + + /** + * Check whether the given name points back to the given alias as an alias + * in the other direction already, catching a circular reference upfront + * and throwing a corresponding IllegalStateException. + * @param name the candidate name + * @param alias the candidate alias + * @see #registerAlias + * @see #hasAlias + */ + protected void checkForAliasCircle(String name, String alias) { + if (hasAlias(alias, name)) { + throw new IllegalStateException("Cannot register alias '" + alias + + "' for name '" + name + "': Circular reference - '" + + name + "' is a direct or indirect alias for '" + alias + "' already"); + } + } + + /** + * Determine the raw name, resolving aliases to canonical names. + * @param name the user-specified name + * @return the transformed name + */ + public String canonicalName(String name) { + String canonicalName = name; + // Handle aliasing... + String resolvedName; + do { + resolvedName = this.aliasMap.get(canonicalName); + if (resolvedName != null) { + canonicalName = resolvedName; + } + } + while (resolvedName != null); + return canonicalName; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/SmartClassLoader.java b/spring-core/src/main/java/org/springframework/core/SmartClassLoader.java new file mode 100644 index 0000000..21948da --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/SmartClassLoader.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +/** + * Interface to be implemented by a reloading-aware ClassLoader + * (e.g. a Groovy-based ClassLoader). Detected for example by + * Spring's CGLIB proxy factory for making a caching decision. + * + *

    If a ClassLoader does not implement this interface, + * then all of the classes obtained from it should be considered + * as not reloadable (i.e. cacheable). + * + * @author Juergen Hoeller + * @since 2.5.1 + */ +public interface SmartClassLoader { + + /** + * Determine whether the given class is reloadable (in this ClassLoader). + *

    Typically used to check whether the result may be cached (for this + * ClassLoader) or whether it should be reobtained every time. + * @param clazz the class to check (usually loaded from this ClassLoader) + * @return whether the class should be expected to appear in a reloaded + * version (with a different {@code Class} object) later on + */ + boolean isClassReloadable(Class clazz); + +} diff --git a/spring-core/src/main/java/org/springframework/core/SortedProperties.java b/spring-core/src/main/java/org/springframework/core/SortedProperties.java new file mode 100644 index 0000000..63f87ac --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/SortedProperties.java @@ -0,0 +1,158 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.StringWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.Set; +import java.util.TreeSet; + +import org.springframework.lang.Nullable; + +/** + * Specialization of {@link Properties} that sorts properties alphanumerically + * based on their keys. + * + *

    This can be useful when storing the {@link Properties} instance in a + * properties file, since it allows such files to be generated in a repeatable + * manner with consistent ordering of properties. + * + *

    Comments in generated properties files can also be optionally omitted. + * + * @author Sam Brannen + * @since 5.2 + * @see java.util.Properties + */ +@SuppressWarnings("serial") +class SortedProperties extends Properties { + + static final String EOL = System.lineSeparator(); + + private static final Comparator keyComparator = Comparator.comparing(String::valueOf); + + private static final Comparator> entryComparator = Entry.comparingByKey(keyComparator); + + + private final boolean omitComments; + + + /** + * Construct a new {@code SortedProperties} instance that honors the supplied + * {@code omitComments} flag. + * @param omitComments {@code true} if comments should be omitted when + * storing properties in a file + */ + SortedProperties(boolean omitComments) { + this.omitComments = omitComments; + } + + /** + * Construct a new {@code SortedProperties} instance with properties populated + * from the supplied {@link Properties} object and honoring the supplied + * {@code omitComments} flag. + *

    Default properties from the supplied {@code Properties} object will + * not be copied. + * @param properties the {@code Properties} object from which to copy the + * initial properties + * @param omitComments {@code true} if comments should be omitted when + * storing properties in a file + */ + SortedProperties(Properties properties, boolean omitComments) { + this(omitComments); + putAll(properties); + } + + + @Override + public void store(OutputStream out, @Nullable String comments) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + super.store(baos, (this.omitComments ? null : comments)); + String contents = baos.toString(StandardCharsets.ISO_8859_1.name()); + for (String line : contents.split(EOL)) { + if (!(this.omitComments && line.startsWith("#"))) { + out.write((line + EOL).getBytes(StandardCharsets.ISO_8859_1)); + } + } + } + + @Override + public void store(Writer writer, @Nullable String comments) throws IOException { + StringWriter stringWriter = new StringWriter(); + super.store(stringWriter, (this.omitComments ? null : comments)); + String contents = stringWriter.toString(); + for (String line : contents.split(EOL)) { + if (!(this.omitComments && line.startsWith("#"))) { + writer.write(line + EOL); + } + } + } + + @Override + public void storeToXML(OutputStream out, @Nullable String comments) throws IOException { + super.storeToXML(out, (this.omitComments ? null : comments)); + } + + @Override + public void storeToXML(OutputStream out, @Nullable String comments, String encoding) throws IOException { + super.storeToXML(out, (this.omitComments ? null : comments), encoding); + } + + /** + * Return a sorted enumeration of the keys in this {@link Properties} object. + * @see #keySet() + */ + @Override + public synchronized Enumeration keys() { + return Collections.enumeration(keySet()); + } + + /** + * Return a sorted set of the keys in this {@link Properties} object. + *

    The keys will be converted to strings if necessary using + * {@link String#valueOf(Object)} and sorted alphanumerically according to + * the natural order of strings. + */ + @Override + public Set keySet() { + Set sortedKeys = new TreeSet<>(keyComparator); + sortedKeys.addAll(super.keySet()); + return Collections.synchronizedSet(sortedKeys); + } + + /** + * Return a sorted set of the entries in this {@link Properties} object. + *

    The entries will be sorted based on their keys, and the keys will be + * converted to strings if necessary using {@link String#valueOf(Object)} + * and compared alphanumerically according to the natural order of strings. + */ + @Override + public Set> entrySet() { + Set> sortedEntries = new TreeSet<>(entryComparator); + sortedEntries.addAll(super.entrySet()); + return Collections.synchronizedSet(sortedEntries); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/SpringProperties.java b/spring-core/src/main/java/org/springframework/core/SpringProperties.java new file mode 100644 index 0000000..dc74704 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/SpringProperties.java @@ -0,0 +1,128 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Properties; + +import org.springframework.lang.Nullable; + +/** + * Static holder for local Spring properties, i.e. defined at the Spring library level. + * + *

    Reads a {@code spring.properties} file from the root of the Spring library classpath, + * and also allows for programmatically setting properties through {@link #setProperty}. + * When checking a property, local entries are being checked first, then falling back + * to JVM-level system properties through a {@link System#getProperty} check. + * + *

    This is an alternative way to set Spring-related system properties such as + * "spring.getenv.ignore" and "spring.beaninfo.ignore", in particular for scenarios + * where JVM system properties are locked on the target platform (e.g. WebSphere). + * See {@link #setFlag} for a convenient way to locally set such flags to "true". + * + * @author Juergen Hoeller + * @since 3.2.7 + * @see org.springframework.core.env.AbstractEnvironment#IGNORE_GETENV_PROPERTY_NAME + * @see org.springframework.beans.CachedIntrospectionResults#IGNORE_BEANINFO_PROPERTY_NAME + * @see org.springframework.jdbc.core.StatementCreatorUtils#IGNORE_GETPARAMETERTYPE_PROPERTY_NAME + * @see org.springframework.test.context.cache.ContextCache#MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME + */ +public final class SpringProperties { + + private static final String PROPERTIES_RESOURCE_LOCATION = "spring.properties"; + + private static final Properties localProperties = new Properties(); + + + static { + try { + ClassLoader cl = SpringProperties.class.getClassLoader(); + URL url = (cl != null ? cl.getResource(PROPERTIES_RESOURCE_LOCATION) : + ClassLoader.getSystemResource(PROPERTIES_RESOURCE_LOCATION)); + if (url != null) { + try (InputStream is = url.openStream()) { + localProperties.load(is); + } + } + } + catch (IOException ex) { + System.err.println("Could not load 'spring.properties' file from local classpath: " + ex); + } + } + + + private SpringProperties() { + } + + + /** + * Programmatically set a local property, overriding an entry in the + * {@code spring.properties} file (if any). + * @param key the property key + * @param value the associated property value, or {@code null} to reset it + */ + public static void setProperty(String key, @Nullable String value) { + if (value != null) { + localProperties.setProperty(key, value); + } + else { + localProperties.remove(key); + } + } + + /** + * Retrieve the property value for the given key, checking local Spring + * properties first and falling back to JVM-level system properties. + * @param key the property key + * @return the associated property value, or {@code null} if none found + */ + @Nullable + public static String getProperty(String key) { + String value = localProperties.getProperty(key); + if (value == null) { + try { + value = System.getProperty(key); + } + catch (Throwable ex) { + System.err.println("Could not retrieve system property '" + key + "': " + ex); + } + } + return value; + } + + /** + * Programmatically set a local flag to "true", overriding an + * entry in the {@code spring.properties} file (if any). + * @param key the property key + */ + public static void setFlag(String key) { + localProperties.put(key, Boolean.TRUE.toString()); + } + + /** + * Retrieve the flag for the given property key. + * @param key the property key + * @return {@code true} if the property is set to "true", + * {@code} false otherwise + */ + public static boolean getFlag(String key) { + return Boolean.parseBoolean(getProperty(key)); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/SpringVersion.java b/spring-core/src/main/java/org/springframework/core/SpringVersion.java new file mode 100644 index 0000000..962343e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/SpringVersion.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import org.springframework.lang.Nullable; + +/** + * Class that exposes the Spring version. Fetches the + * "Implementation-Version" manifest attribute from the jar file. + * + *

    Note that some ClassLoaders do not expose the package metadata, + * hence this class might not be able to determine the Spring version + * in all environments. Consider using a reflection-based check instead — + * for example, checking for the presence of a specific Spring 5.2 + * method that you intend to call. + * + * @author Juergen Hoeller + * @since 1.1 + */ +public final class SpringVersion { + + private SpringVersion() { + } + + + /** + * Return the full version string of the present Spring codebase, + * or {@code null} if it cannot be determined. + * @see Package#getImplementationVersion() + */ + @Nullable + public static String getVersion() { + Package pkg = SpringVersion.class.getPackage(); + return (pkg != null ? pkg.getImplementationVersion() : null); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/StandardReflectionParameterNameDiscoverer.java b/spring-core/src/main/java/org/springframework/core/StandardReflectionParameterNameDiscoverer.java new file mode 100644 index 0000000..665befa --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/StandardReflectionParameterNameDiscoverer.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; + +import org.springframework.lang.Nullable; + +/** + * {@link ParameterNameDiscoverer} implementation which uses JDK 8's reflection facilities + * for introspecting parameter names (based on the "-parameters" compiler flag). + * + * @author Juergen Hoeller + * @since 4.0 + * @see java.lang.reflect.Method#getParameters() + * @see java.lang.reflect.Parameter#getName() + */ +public class StandardReflectionParameterNameDiscoverer implements ParameterNameDiscoverer { + + @Override + @Nullable + public String[] getParameterNames(Method method) { + return getParameterNames(method.getParameters()); + } + + @Override + @Nullable + public String[] getParameterNames(Constructor ctor) { + return getParameterNames(ctor.getParameters()); + } + + @Nullable + private String[] getParameterNames(Parameter[] parameters) { + String[] parameterNames = new String[parameters.length]; + for (int i = 0; i < parameters.length; i++) { + Parameter param = parameters[i]; + if (!param.isNamePresent()) { + return null; + } + parameterNames[i] = param.getName(); + } + return parameterNames; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AbstractMergedAnnotation.java b/spring-core/src/main/java/org/springframework/core/annotation/AbstractMergedAnnotation.java new file mode 100644 index 0000000..45bf025 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AbstractMergedAnnotation.java @@ -0,0 +1,243 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.function.Predicate; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Abstract base class for {@link MergedAnnotation} implementations. + * + * @author Phillip Webb + * @author Juergen Hoeller + * @since 5.2 + * @param the annotation type + */ +abstract class AbstractMergedAnnotation implements MergedAnnotation { + + @Nullable + private volatile A synthesizedAnnotation; + + + @Override + public boolean isDirectlyPresent() { + return isPresent() && getDistance() == 0; + } + + @Override + public boolean isMetaPresent() { + return isPresent() && getDistance() > 0; + } + + @Override + public boolean hasNonDefaultValue(String attributeName) { + return !hasDefaultValue(attributeName); + } + + @Override + public byte getByte(String attributeName) { + return getRequiredAttributeValue(attributeName, Byte.class); + } + + @Override + public byte[] getByteArray(String attributeName) { + return getRequiredAttributeValue(attributeName, byte[].class); + } + + @Override + public boolean getBoolean(String attributeName) { + return getRequiredAttributeValue(attributeName, Boolean.class); + } + + @Override + public boolean[] getBooleanArray(String attributeName) { + return getRequiredAttributeValue(attributeName, boolean[].class); + } + + @Override + public char getChar(String attributeName) { + return getRequiredAttributeValue(attributeName, Character.class); + } + + @Override + public char[] getCharArray(String attributeName) { + return getRequiredAttributeValue(attributeName, char[].class); + } + + @Override + public short getShort(String attributeName) { + return getRequiredAttributeValue(attributeName, Short.class); + } + + @Override + public short[] getShortArray(String attributeName) { + return getRequiredAttributeValue(attributeName, short[].class); + } + + @Override + public int getInt(String attributeName) { + return getRequiredAttributeValue(attributeName, Integer.class); + } + + @Override + public int[] getIntArray(String attributeName) { + return getRequiredAttributeValue(attributeName, int[].class); + } + + @Override + public long getLong(String attributeName) { + return getRequiredAttributeValue(attributeName, Long.class); + } + + @Override + public long[] getLongArray(String attributeName) { + return getRequiredAttributeValue(attributeName, long[].class); + } + + @Override + public double getDouble(String attributeName) { + return getRequiredAttributeValue(attributeName, Double.class); + } + + @Override + public double[] getDoubleArray(String attributeName) { + return getRequiredAttributeValue(attributeName, double[].class); + } + + @Override + public float getFloat(String attributeName) { + return getRequiredAttributeValue(attributeName, Float.class); + } + + @Override + public float[] getFloatArray(String attributeName) { + return getRequiredAttributeValue(attributeName, float[].class); + } + + @Override + public String getString(String attributeName) { + return getRequiredAttributeValue(attributeName, String.class); + } + + @Override + public String[] getStringArray(String attributeName) { + return getRequiredAttributeValue(attributeName, String[].class); + } + + @Override + public Class getClass(String attributeName) { + return getRequiredAttributeValue(attributeName, Class.class); + } + + @Override + public Class[] getClassArray(String attributeName) { + return getRequiredAttributeValue(attributeName, Class[].class); + } + + @Override + public > E getEnum(String attributeName, Class type) { + Assert.notNull(type, "Type must not be null"); + return getRequiredAttributeValue(attributeName, type); + } + + @Override + @SuppressWarnings("unchecked") + public > E[] getEnumArray(String attributeName, Class type) { + Assert.notNull(type, "Type must not be null"); + Class arrayType = Array.newInstance(type, 0).getClass(); + return (E[]) getRequiredAttributeValue(attributeName, arrayType); + } + + @Override + public Optional getValue(String attributeName) { + return getValue(attributeName, Object.class); + } + + @Override + public Optional getValue(String attributeName, Class type) { + return Optional.ofNullable(getAttributeValue(attributeName, type)); + } + + @Override + public Optional getDefaultValue(String attributeName) { + return getDefaultValue(attributeName, Object.class); + } + + @Override + public MergedAnnotation filterDefaultValues() { + return filterAttributes(this::hasNonDefaultValue); + } + + @Override + public AnnotationAttributes asAnnotationAttributes(Adapt... adaptations) { + return asMap(mergedAnnotation -> new AnnotationAttributes(mergedAnnotation.getType()), adaptations); + } + + @Override + public Optional synthesize(Predicate> condition) + throws NoSuchElementException { + + return (condition.test(this) ? Optional.of(synthesize()) : Optional.empty()); + } + + @Override + public A synthesize() { + if (!isPresent()) { + throw new NoSuchElementException("Unable to synthesize missing annotation"); + } + A synthesized = this.synthesizedAnnotation; + if (synthesized == null) { + synthesized = createSynthesized(); + this.synthesizedAnnotation = synthesized; + } + return synthesized; + } + + private T getRequiredAttributeValue(String attributeName, Class type) { + T value = getAttributeValue(attributeName, type); + if (value == null) { + throw new NoSuchElementException("No attribute named '" + attributeName + + "' present in merged annotation " + getType().getName()); + } + return value; + } + + /** + * Get the underlying attribute value. + * @param attributeName the attribute name + * @param type the type to return (see {@link MergedAnnotation} class + * documentation for details) + * @return the attribute value or {@code null} if the value is not found and + * is not required + * @throws IllegalArgumentException if the source type is not compatible + * @throws NoSuchElementException if the value is required but not found + */ + @Nullable + protected abstract T getAttributeValue(String attributeName, Class type); + + /** + * Factory method used to create the synthesized annotation. + */ + protected abstract A createSynthesized(); + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AliasFor.java b/spring-core/src/main/java/org/springframework/core/annotation/AliasFor.java new file mode 100644 index 0000000..88d7d78 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AliasFor.java @@ -0,0 +1,206 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * {@code @AliasFor} is an annotation that is used to declare aliases for + * annotation attributes. + * + *

    Usage Scenarios

    + *
      + *
    • Explicit aliases within an annotation: within a single + * annotation, {@code @AliasFor} can be declared on a pair of attributes to + * signal that they are interchangeable aliases for each other.
    • + *
    • Explicit alias for attribute in meta-annotation: if the + * {@link #annotation} attribute of {@code @AliasFor} is set to a different + * annotation than the one that declares it, the {@link #attribute} is + * interpreted as an alias for an attribute in a meta-annotation (i.e., an + * explicit meta-annotation attribute override). This enables fine-grained + * control over exactly which attributes are overridden within an annotation + * hierarchy. In fact, with {@code @AliasFor} it is even possible to declare + * an alias for the {@code value} attribute of a meta-annotation.
    • + *
    • Implicit aliases within an annotation: if one or + * more attributes within an annotation are declared as attribute overrides + * for the same meta-annotation attribute (either directly or transitively), + * those attributes will be treated as a set of implicit aliases + * for each other, resulting in behavior analogous to that for explicit + * aliases within an annotation.
    • + *
    + * + *

    Usage Requirements

    + *

    Like with any annotation in Java, the mere presence of {@code @AliasFor} + * on its own will not enforce alias semantics. For alias semantics to be + * enforced, annotations must be loaded via {@link MergedAnnotations}. + * + *

    Implementation Requirements

    + *
      + *
    • Explicit aliases within an annotation: + *
        + *
      1. Each attribute that makes up an aliased pair should be annotated with + * {@code @AliasFor}, and either {@link #attribute} or {@link #value} must + * reference the other attribute in the pair. Since Spring Framework + * 5.2.1 it is technically possible to annotate only one of the attributes in an + * aliased pair; however, it is recommended to annotate both attributes in an + * aliased pair for better documentation as well as compatibility with previous + * versions of the Spring Framework.
      2. + *
      3. Aliased attributes must declare the same return type.
      4. + *
      5. Aliased attributes must declare a default value.
      6. + *
      7. Aliased attributes must declare the same default value.
      8. + *
      9. {@link #annotation} should not be declared.
      10. + *
      + *
    • + *
    • Explicit alias for attribute in meta-annotation: + *
        + *
      1. The attribute that is an alias for an attribute in a meta-annotation + * must be annotated with {@code @AliasFor}, and {@link #attribute} must + * reference the attribute in the meta-annotation.
      2. + *
      3. Aliased attributes must declare the same return type.
      4. + *
      5. {@link #annotation} must reference the meta-annotation.
      6. + *
      7. The referenced meta-annotation must be meta-present on the + * annotation class that declares {@code @AliasFor}.
      8. + *
      + *
    • + *
    • Implicit aliases within an annotation: + *
        + *
      1. Each attribute that belongs to a set of implicit aliases must be + * annotated with {@code @AliasFor}, and {@link #attribute} must reference + * the same attribute in the same meta-annotation (either directly or + * transitively via other explicit meta-annotation attribute overrides + * within the annotation hierarchy).
      2. + *
      3. Aliased attributes must declare the same return type.
      4. + *
      5. Aliased attributes must declare a default value.
      6. + *
      7. Aliased attributes must declare the same default value.
      8. + *
      9. {@link #annotation} must reference an appropriate meta-annotation.
      10. + *
      11. The referenced meta-annotation must be meta-present on the + * annotation class that declares {@code @AliasFor}.
      12. + *
      + *
    • + *
    + * + *

    Example: Explicit Aliases within an Annotation

    + *

    In {@code @ContextConfiguration}, {@code value} and {@code locations} + * are explicit aliases for each other. + * + *

     public @interface ContextConfiguration {
    + *
    + *    @AliasFor("locations")
    + *    String[] value() default {};
    + *
    + *    @AliasFor("value")
    + *    String[] locations() default {};
    + *
    + *    // ...
    + * }
    + * + *

    Example: Explicit Alias for Attribute in Meta-annotation

    + *

    In {@code @XmlTestConfig}, {@code xmlFiles} is an explicit alias for + * {@code locations} in {@code @ContextConfiguration}. In other words, + * {@code xmlFiles} overrides the {@code locations} attribute in + * {@code @ContextConfiguration}. + * + *

     @ContextConfiguration
    + * public @interface XmlTestConfig {
    + *
    + *    @AliasFor(annotation = ContextConfiguration.class, attribute = "locations")
    + *    String[] xmlFiles();
    + * }
    + * + *

    Example: Implicit Aliases within an Annotation

    + *

    In {@code @MyTestConfig}, {@code value}, {@code groovyScripts}, and + * {@code xmlFiles} are all explicit meta-annotation attribute overrides for + * the {@code locations} attribute in {@code @ContextConfiguration}. These + * three attributes are therefore also implicit aliases for each other. + * + *

     @ContextConfiguration
    + * public @interface MyTestConfig {
    + *
    + *    @AliasFor(annotation = ContextConfiguration.class, attribute = "locations")
    + *    String[] value() default {};
    + *
    + *    @AliasFor(annotation = ContextConfiguration.class, attribute = "locations")
    + *    String[] groovyScripts() default {};
    + *
    + *    @AliasFor(annotation = ContextConfiguration.class, attribute = "locations")
    + *    String[] xmlFiles() default {};
    + * }
    + * + *

    Example: Transitive Implicit Aliases within an Annotation

    + *

    In {@code @GroovyOrXmlTestConfig}, {@code groovy} is an explicit + * override for the {@code groovyScripts} attribute in {@code @MyTestConfig}; + * whereas, {@code xml} is an explicit override for the {@code locations} + * attribute in {@code @ContextConfiguration}. Furthermore, {@code groovy} + * and {@code xml} are transitive implicit aliases for each other, since they + * both effectively override the {@code locations} attribute in + * {@code @ContextConfiguration}. + * + *

     @MyTestConfig
    + * public @interface GroovyOrXmlTestConfig {
    + *
    + *    @AliasFor(annotation = MyTestConfig.class, attribute = "groovyScripts")
    + *    String[] groovy() default {};
    + *
    + *    @AliasFor(annotation = ContextConfiguration.class, attribute = "locations")
    + *    String[] xml() default {};
    + * }
    + * + *

    Spring Annotations Supporting Attribute Aliases

    + *

    As of Spring Framework 4.2, several annotations within core Spring + * have been updated to use {@code @AliasFor} to configure their internal + * attribute aliases. Consult the Javadoc for individual annotations as well + * as the reference manual for details. + * + * @author Sam Brannen + * @since 4.2 + * @see MergedAnnotations + * @see SynthesizedAnnotation + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface AliasFor { + + /** + * Alias for {@link #attribute}. + *

    Intended to be used instead of {@link #attribute} when {@link #annotation} + * is not declared — for example: {@code @AliasFor("value")} instead of + * {@code @AliasFor(attribute = "value")}. + */ + @AliasFor("attribute") + String value() default ""; + + /** + * The name of the attribute that this attribute is an alias for. + * @see #value + */ + @AliasFor("value") + String attribute() default ""; + + /** + * The type of annotation in which the aliased {@link #attribute} is declared. + *

    Defaults to {@link Annotation}, implying that the aliased attribute is + * declared in the same annotation as this attribute. + */ + Class annotation() default Annotation.class; + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java new file mode 100644 index 0000000..850cee0 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java @@ -0,0 +1,831 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.annotation.MergedAnnotation.Adapt; +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; +import org.springframework.lang.Nullable; +import org.springframework.util.MultiValueMap; + +/** + * General utility methods for finding annotations, meta-annotations, and + * repeatable annotations on {@link AnnotatedElement AnnotatedElements}. + * + *

    {@code AnnotatedElementUtils} defines the public API for Spring's + * meta-annotation programming model with support for annotation attribute + * overrides. If you do not need support for annotation attribute + * overrides, consider using {@link AnnotationUtils} instead. + * + *

    Note that the features of this class are not provided by the JDK's + * introspection facilities themselves. + * + *

    Annotation Attribute Overrides

    + *

    Support for meta-annotations with attribute overrides in + * composed annotations is provided by all variants of the + * {@code getMergedAnnotationAttributes()}, {@code getMergedAnnotation()}, + * {@code getAllMergedAnnotations()}, {@code getMergedRepeatableAnnotations()}, + * {@code findMergedAnnotationAttributes()}, {@code findMergedAnnotation()}, + * {@code findAllMergedAnnotations()}, and {@code findMergedRepeatableAnnotations()} + * methods. + * + *

    Find vs. Get Semantics

    + *

    The search algorithms used by methods in this class follow either + * find or get semantics. Consult the javadocs for each + * individual method for details on which search algorithm is used. + * + *

    Get semantics are limited to searching for annotations + * that are either present on an {@code AnnotatedElement} (i.e. declared + * locally or {@linkplain java.lang.annotation.Inherited inherited}) or declared + * within the annotation hierarchy above the {@code AnnotatedElement}. + * + *

    Find semantics are much more exhaustive, providing + * get semantics plus support for the following: + * + *

      + *
    • Searching on interfaces, if the annotated element is a class + *
    • Searching on superclasses, if the annotated element is a class + *
    • Resolving bridged methods, if the annotated element is a method + *
    • Searching on methods in interfaces, if the annotated element is a method + *
    • Searching on methods in superclasses, if the annotated element is a method + *
    + * + *

    Support for {@code @Inherited}

    + *

    Methods following get semantics will honor the contract of Java's + * {@link java.lang.annotation.Inherited @Inherited} annotation except that locally + * declared annotations (including custom composed annotations) will be favored over + * inherited annotations. In contrast, methods following find semantics + * will completely ignore the presence of {@code @Inherited} since the find + * search algorithm manually traverses type and method hierarchies and thereby + * implicitly supports annotation inheritance without a need for {@code @Inherited}. + * + * @author Phillip Webb + * @author Juergen Hoeller + * @author Sam Brannen + * @since 4.0 + * @see AliasFor + * @see AnnotationAttributes + * @see AnnotationUtils + * @see BridgeMethodResolver + */ +public abstract class AnnotatedElementUtils { + + /** + * Build an adapted {@link AnnotatedElement} for the given annotations, + * typically for use with other methods on {@link AnnotatedElementUtils}. + * @param annotations the annotations to expose through the {@code AnnotatedElement} + * @since 4.3 + */ + public static AnnotatedElement forAnnotations(Annotation... annotations) { + return new AnnotatedElementForAnnotations(annotations); + } + + /** + * Get the fully qualified class names of all meta-annotation types + * present on the annotation (of the specified {@code annotationType}) + * on the supplied {@link AnnotatedElement}. + *

    This method follows get semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. + * @param element the annotated element + * @param annotationType the annotation type on which to find meta-annotations + * @return the names of all meta-annotations present on the annotation, + * or an empty set if not found + * @since 4.2 + * @see #getMetaAnnotationTypes(AnnotatedElement, String) + * @see #hasMetaAnnotationTypes + */ + public static Set getMetaAnnotationTypes(AnnotatedElement element, + Class annotationType) { + + return getMetaAnnotationTypes(element, element.getAnnotation(annotationType)); + } + + /** + * Get the fully qualified class names of all meta-annotation + * types present on the annotation (of the specified + * {@code annotationName}) on the supplied {@link AnnotatedElement}. + *

    This method follows get semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. + * @param element the annotated element + * @param annotationName the fully qualified class name of the annotation + * type on which to find meta-annotations + * @return the names of all meta-annotations present on the annotation, + * or an empty set if none found + * @see #getMetaAnnotationTypes(AnnotatedElement, Class) + * @see #hasMetaAnnotationTypes + */ + public static Set getMetaAnnotationTypes(AnnotatedElement element, String annotationName) { + for (Annotation annotation : element.getAnnotations()) { + if (annotation.annotationType().getName().equals(annotationName)) { + return getMetaAnnotationTypes(element, annotation); + } + } + return Collections.emptySet(); + } + + private static Set getMetaAnnotationTypes(AnnotatedElement element, @Nullable Annotation annotation) { + if (annotation == null) { + return Collections.emptySet(); + } + return getAnnotations(annotation.annotationType()).stream() + .map(mergedAnnotation -> mergedAnnotation.getType().getName()) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + /** + * Determine if the supplied {@link AnnotatedElement} is annotated with + * a composed annotation that is meta-annotated with an + * annotation of the specified {@code annotationType}. + *

    This method follows get semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. + * @param element the annotated element + * @param annotationType the meta-annotation type to find + * @return {@code true} if a matching meta-annotation is present + * @since 4.2.3 + * @see #getMetaAnnotationTypes + */ + public static boolean hasMetaAnnotationTypes(AnnotatedElement element, Class annotationType) { + return getAnnotations(element).stream(annotationType).anyMatch(MergedAnnotation::isMetaPresent); + } + + /** + * Determine if the supplied {@link AnnotatedElement} is annotated with a + * composed annotation that is meta-annotated with an annotation + * of the specified {@code annotationName}. + *

    This method follows get semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. + * @param element the annotated element + * @param annotationName the fully qualified class name of the + * meta-annotation type to find + * @return {@code true} if a matching meta-annotation is present + * @see #getMetaAnnotationTypes + */ + public static boolean hasMetaAnnotationTypes(AnnotatedElement element, String annotationName) { + return getAnnotations(element).stream(annotationName).anyMatch(MergedAnnotation::isMetaPresent); + } + + /** + * Determine if an annotation of the specified {@code annotationType} + * is present on the supplied {@link AnnotatedElement} or + * within the annotation hierarchy above the specified element. + *

    If this method returns {@code true}, then {@link #getMergedAnnotationAttributes} + * will return a non-null value. + *

    This method follows get semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. + * @param element the annotated element + * @param annotationType the annotation type to find + * @return {@code true} if a matching annotation is present + * @since 4.2.3 + * @see #hasAnnotation(AnnotatedElement, Class) + */ + public static boolean isAnnotated(AnnotatedElement element, Class annotationType) { + // Shortcut: directly present on the element, with no merging needed? + if (AnnotationFilter.PLAIN.matches(annotationType) || + AnnotationsScanner.hasPlainJavaAnnotationsOnly(element)) { + return element.isAnnotationPresent(annotationType); + } + // Exhaustive retrieval of merged annotations... + return getAnnotations(element).isPresent(annotationType); + } + + /** + * Determine if an annotation of the specified {@code annotationName} is + * present on the supplied {@link AnnotatedElement} or within the + * annotation hierarchy above the specified element. + *

    If this method returns {@code true}, then {@link #getMergedAnnotationAttributes} + * will return a non-null value. + *

    This method follows get semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. + * @param element the annotated element + * @param annotationName the fully qualified class name of the annotation type to find + * @return {@code true} if a matching annotation is present + */ + public static boolean isAnnotated(AnnotatedElement element, String annotationName) { + return getAnnotations(element).isPresent(annotationName); + } + + /** + * Get the first annotation of the specified {@code annotationType} within + * the annotation hierarchy above the supplied {@code element} and + * merge that annotation's attributes with matching attributes from + * annotations in lower levels of the annotation hierarchy. + *

    {@link AliasFor @AliasFor} semantics are fully supported, both + * within a single annotation and within the annotation hierarchy. + *

    This method delegates to {@link #getMergedAnnotationAttributes(AnnotatedElement, String)}. + * @param element the annotated element + * @param annotationType the annotation type to find + * @return the merged {@code AnnotationAttributes}, or {@code null} if not found + * @since 4.2 + * @see #getMergedAnnotationAttributes(AnnotatedElement, String, boolean, boolean) + * @see #findMergedAnnotationAttributes(AnnotatedElement, String, boolean, boolean) + * @see #getMergedAnnotation(AnnotatedElement, Class) + * @see #findMergedAnnotation(AnnotatedElement, Class) + */ + @Nullable + public static AnnotationAttributes getMergedAnnotationAttributes( + AnnotatedElement element, Class annotationType) { + + MergedAnnotation mergedAnnotation = getAnnotations(element) + .get(annotationType, null, MergedAnnotationSelectors.firstDirectlyDeclared()); + return getAnnotationAttributes(mergedAnnotation, false, false); + } + + /** + * Get the first annotation of the specified {@code annotationName} within + * the annotation hierarchy above the supplied {@code element} and + * merge that annotation's attributes with matching attributes from + * annotations in lower levels of the annotation hierarchy. + *

    {@link AliasFor @AliasFor} semantics are fully supported, both + * within a single annotation and within the annotation hierarchy. + *

    This method delegates to {@link #getMergedAnnotationAttributes(AnnotatedElement, String, boolean, boolean)}, + * supplying {@code false} for {@code classValuesAsString} and {@code nestedAnnotationsAsMap}. + * @param element the annotated element + * @param annotationName the fully qualified class name of the annotation type to find + * @return the merged {@code AnnotationAttributes}, or {@code null} if not found + * @since 4.2 + * @see #getMergedAnnotationAttributes(AnnotatedElement, String, boolean, boolean) + * @see #findMergedAnnotationAttributes(AnnotatedElement, String, boolean, boolean) + * @see #findMergedAnnotation(AnnotatedElement, Class) + * @see #getAllAnnotationAttributes(AnnotatedElement, String) + */ + @Nullable + public static AnnotationAttributes getMergedAnnotationAttributes(AnnotatedElement element, + String annotationName) { + + return getMergedAnnotationAttributes(element, annotationName, false, false); + } + + /** + * Get the first annotation of the specified {@code annotationName} within + * the annotation hierarchy above the supplied {@code element} and + * merge that annotation's attributes with matching attributes from + * annotations in lower levels of the annotation hierarchy. + *

    Attributes from lower levels in the annotation hierarchy override attributes + * of the same name from higher levels, and {@link AliasFor @AliasFor} semantics are + * fully supported, both within a single annotation and within the annotation hierarchy. + *

    In contrast to {@link #getAllAnnotationAttributes}, the search algorithm used by + * this method will stop searching the annotation hierarchy once the first annotation + * of the specified {@code annotationName} has been found. As a consequence, + * additional annotations of the specified {@code annotationName} will be ignored. + *

    This method follows get semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. + * @param element the annotated element + * @param annotationName the fully qualified class name of the annotation type to find + * @param classValuesAsString whether to convert Class references into Strings or to + * preserve them as Class references + * @param nestedAnnotationsAsMap whether to convert nested Annotation instances + * into {@code AnnotationAttributes} maps or to preserve them as Annotation instances + * @return the merged {@code AnnotationAttributes}, or {@code null} if not found + * @since 4.2 + * @see #findMergedAnnotation(AnnotatedElement, Class) + * @see #findMergedAnnotationAttributes(AnnotatedElement, String, boolean, boolean) + * @see #getAllAnnotationAttributes(AnnotatedElement, String, boolean, boolean) + */ + @Nullable + public static AnnotationAttributes getMergedAnnotationAttributes(AnnotatedElement element, + String annotationName, boolean classValuesAsString, boolean nestedAnnotationsAsMap) { + + MergedAnnotation mergedAnnotation = getAnnotations(element) + .get(annotationName, null, MergedAnnotationSelectors.firstDirectlyDeclared()); + return getAnnotationAttributes(mergedAnnotation, classValuesAsString, nestedAnnotationsAsMap); + } + + /** + * Get the first annotation of the specified {@code annotationType} within + * the annotation hierarchy above the supplied {@code element}, + * merge that annotation's attributes with matching attributes from + * annotations in lower levels of the annotation hierarchy, and synthesize + * the result back into an annotation of the specified {@code annotationType}. + *

    {@link AliasFor @AliasFor} semantics are fully supported, both + * within a single annotation and within the annotation hierarchy. + * @param element the annotated element + * @param annotationType the annotation type to find + * @return the merged, synthesized {@code Annotation}, or {@code null} if not found + * @since 4.2 + * @see #findMergedAnnotation(AnnotatedElement, Class) + */ + @Nullable + public static A getMergedAnnotation(AnnotatedElement element, Class annotationType) { + // Shortcut: directly present on the element, with no merging needed? + if (AnnotationFilter.PLAIN.matches(annotationType) || + AnnotationsScanner.hasPlainJavaAnnotationsOnly(element)) { + return element.getDeclaredAnnotation(annotationType); + } + // Exhaustive retrieval of merged annotations... + return getAnnotations(element) + .get(annotationType, null, MergedAnnotationSelectors.firstDirectlyDeclared()) + .synthesize(MergedAnnotation::isPresent).orElse(null); + } + + /** + * Get all annotations of the specified {@code annotationType} + * within the annotation hierarchy above the supplied {@code element}; + * and for each annotation found, merge that annotation's attributes with + * matching attributes from annotations in lower levels of the annotation + * hierarchy and synthesize the results back into an annotation of the specified + * {@code annotationType}. + *

    {@link AliasFor @AliasFor} semantics are fully supported, both within a + * single annotation and within annotation hierarchies. + *

    This method follows get semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. + * @param element the annotated element (never {@code null}) + * @param annotationType the annotation type to find (never {@code null}) + * @return the set of all merged, synthesized {@code Annotations} found, + * or an empty set if none were found + * @since 4.3 + * @see #getMergedAnnotation(AnnotatedElement, Class) + * @see #getAllAnnotationAttributes(AnnotatedElement, String) + * @see #findAllMergedAnnotations(AnnotatedElement, Class) + */ + public static Set getAllMergedAnnotations( + AnnotatedElement element, Class annotationType) { + + return getAnnotations(element).stream(annotationType) + .collect(MergedAnnotationCollectors.toAnnotationSet()); + } + + /** + * Get all annotations of the specified {@code annotationTypes} + * within the annotation hierarchy above the supplied {@code element}; + * and for each annotation found, merge that annotation's attributes with + * matching attributes from annotations in lower levels of the + * annotation hierarchy and synthesize the results back into an annotation + * of the corresponding {@code annotationType}. + *

    {@link AliasFor @AliasFor} semantics are fully supported, both within a + * single annotation and within annotation hierarchies. + *

    This method follows get semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. + * @param element the annotated element (never {@code null}) + * @param annotationTypes the annotation types to find + * @return the set of all merged, synthesized {@code Annotations} found, + * or an empty set if none were found + * @since 5.1 + * @see #getAllMergedAnnotations(AnnotatedElement, Class) + */ + public static Set getAllMergedAnnotations(AnnotatedElement element, + Set> annotationTypes) { + + return getAnnotations(element).stream() + .filter(MergedAnnotationPredicates.typeIn(annotationTypes)) + .collect(MergedAnnotationCollectors.toAnnotationSet()); + } + + /** + * Get all repeatable annotations of the specified {@code annotationType} + * within the annotation hierarchy above the supplied {@code element}; + * and for each annotation found, merge that annotation's attributes with + * matching attributes from annotations in lower levels of the annotation + * hierarchy and synthesize the results back into an annotation of the specified + * {@code annotationType}. + *

    The container type that holds the repeatable annotations will be looked up + * via {@link java.lang.annotation.Repeatable}. + *

    {@link AliasFor @AliasFor} semantics are fully supported, both within a + * single annotation and within annotation hierarchies. + *

    This method follows get semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. + * @param element the annotated element (never {@code null}) + * @param annotationType the annotation type to find (never {@code null}) + * @return the set of all merged repeatable {@code Annotations} found, + * or an empty set if none were found + * @throws IllegalArgumentException if the {@code element} or {@code annotationType} + * is {@code null}, or if the container type cannot be resolved + * @since 4.3 + * @see #getMergedAnnotation(AnnotatedElement, Class) + * @see #getAllMergedAnnotations(AnnotatedElement, Class) + * @see #getMergedRepeatableAnnotations(AnnotatedElement, Class, Class) + */ + public static Set getMergedRepeatableAnnotations( + AnnotatedElement element, Class annotationType) { + + return getMergedRepeatableAnnotations(element, annotationType, null); + } + + /** + * Get all repeatable annotations of the specified {@code annotationType} + * within the annotation hierarchy above the supplied {@code element}; + * and for each annotation found, merge that annotation's attributes with + * matching attributes from annotations in lower levels of the annotation + * hierarchy and synthesize the results back into an annotation of the specified + * {@code annotationType}. + *

    {@link AliasFor @AliasFor} semantics are fully supported, both within a + * single annotation and within annotation hierarchies. + *

    This method follows get semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. + * @param element the annotated element (never {@code null}) + * @param annotationType the annotation type to find (never {@code null}) + * @param containerType the type of the container that holds the annotations; + * may be {@code null} if the container type should be looked up via + * {@link java.lang.annotation.Repeatable} + * @return the set of all merged repeatable {@code Annotations} found, + * or an empty set if none were found + * @throws IllegalArgumentException if the {@code element} or {@code annotationType} + * is {@code null}, or if the container type cannot be resolved + * @throws AnnotationConfigurationException if the supplied {@code containerType} + * is not a valid container annotation for the supplied {@code annotationType} + * @since 4.3 + * @see #getMergedAnnotation(AnnotatedElement, Class) + * @see #getAllMergedAnnotations(AnnotatedElement, Class) + */ + public static Set getMergedRepeatableAnnotations( + AnnotatedElement element, Class annotationType, + @Nullable Class containerType) { + + return getRepeatableAnnotations(element, containerType, annotationType) + .stream(annotationType) + .collect(MergedAnnotationCollectors.toAnnotationSet()); + } + + /** + * Get the annotation attributes of all annotations of the specified + * {@code annotationName} in the annotation hierarchy above the supplied + * {@link AnnotatedElement} and store the results in a {@link MultiValueMap}. + *

    Note: in contrast to {@link #getMergedAnnotationAttributes(AnnotatedElement, String)}, + * this method does not support attribute overrides. + *

    This method follows get semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. + * @param element the annotated element + * @param annotationName the fully qualified class name of the annotation type to find + * @return a {@link MultiValueMap} keyed by attribute name, containing the annotation + * attributes from all annotations found, or {@code null} if not found + * @see #getAllAnnotationAttributes(AnnotatedElement, String, boolean, boolean) + */ + @Nullable + public static MultiValueMap getAllAnnotationAttributes( + AnnotatedElement element, String annotationName) { + + return getAllAnnotationAttributes(element, annotationName, false, false); + } + + /** + * Get the annotation attributes of all annotations of + * the specified {@code annotationName} in the annotation hierarchy above + * the supplied {@link AnnotatedElement} and store the results in a + * {@link MultiValueMap}. + *

    Note: in contrast to {@link #getMergedAnnotationAttributes(AnnotatedElement, String)}, + * this method does not support attribute overrides. + *

    This method follows get semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. + * @param element the annotated element + * @param annotationName the fully qualified class name of the annotation type to find + * @param classValuesAsString whether to convert Class references into Strings or to + * preserve them as Class references + * @param nestedAnnotationsAsMap whether to convert nested Annotation instances into + * {@code AnnotationAttributes} maps or to preserve them as Annotation instances + * @return a {@link MultiValueMap} keyed by attribute name, containing the annotation + * attributes from all annotations found, or {@code null} if not found + */ + @Nullable + public static MultiValueMap getAllAnnotationAttributes(AnnotatedElement element, + String annotationName, final boolean classValuesAsString, final boolean nestedAnnotationsAsMap) { + + Adapt[] adaptations = Adapt.values(classValuesAsString, nestedAnnotationsAsMap); + return getAnnotations(element).stream(annotationName) + .filter(MergedAnnotationPredicates.unique(MergedAnnotation::getMetaTypes)) + .map(MergedAnnotation::withNonMergedAttributes) + .collect(MergedAnnotationCollectors.toMultiValueMap(AnnotatedElementUtils::nullIfEmpty, adaptations)); + } + + /** + * Determine if an annotation of the specified {@code annotationType} + * is available on the supplied {@link AnnotatedElement} or + * within the annotation hierarchy above the specified element. + *

    If this method returns {@code true}, then {@link #findMergedAnnotationAttributes} + * will return a non-null value. + *

    This method follows find semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. + * @param element the annotated element + * @param annotationType the annotation type to find + * @return {@code true} if a matching annotation is present + * @since 4.3 + * @see #isAnnotated(AnnotatedElement, Class) + */ + public static boolean hasAnnotation(AnnotatedElement element, Class annotationType) { + // Shortcut: directly present on the element, with no merging needed? + if (AnnotationFilter.PLAIN.matches(annotationType) || + AnnotationsScanner.hasPlainJavaAnnotationsOnly(element)) { + return element.isAnnotationPresent(annotationType); + } + // Exhaustive retrieval of merged annotations... + return findAnnotations(element).isPresent(annotationType); + } + + /** + * Find the first annotation of the specified {@code annotationType} within + * the annotation hierarchy above the supplied {@code element} and + * merge that annotation's attributes with matching attributes from + * annotations in lower levels of the annotation hierarchy. + *

    Attributes from lower levels in the annotation hierarchy override + * attributes of the same name from higher levels, and + * {@link AliasFor @AliasFor} semantics are fully supported, both + * within a single annotation and within the annotation hierarchy. + *

    In contrast to {@link #getAllAnnotationAttributes}, the search algorithm + * used by this method will stop searching the annotation hierarchy once the + * first annotation of the specified {@code annotationType} has been found. + * As a consequence, additional annotations of the specified + * {@code annotationType} will be ignored. + *

    This method follows find semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. + * @param element the annotated element + * @param annotationType the annotation type to find + * @param classValuesAsString whether to convert Class references into + * Strings or to preserve them as Class references + * @param nestedAnnotationsAsMap whether to convert nested Annotation instances into + * {@code AnnotationAttributes} maps or to preserve them as Annotation instances + * @return the merged {@code AnnotationAttributes}, or {@code null} if not found + * @since 4.2 + * @see #findMergedAnnotation(AnnotatedElement, Class) + * @see #getMergedAnnotationAttributes(AnnotatedElement, String, boolean, boolean) + */ + @Nullable + public static AnnotationAttributes findMergedAnnotationAttributes(AnnotatedElement element, + Class annotationType, boolean classValuesAsString, boolean nestedAnnotationsAsMap) { + + MergedAnnotation mergedAnnotation = findAnnotations(element) + .get(annotationType, null, MergedAnnotationSelectors.firstDirectlyDeclared()); + return getAnnotationAttributes(mergedAnnotation, classValuesAsString, nestedAnnotationsAsMap); + } + + /** + * Find the first annotation of the specified {@code annotationName} within + * the annotation hierarchy above the supplied {@code element} and + * merge that annotation's attributes with matching attributes from + * annotations in lower levels of the annotation hierarchy. + *

    Attributes from lower levels in the annotation hierarchy override + * attributes of the same name from higher levels, and + * {@link AliasFor @AliasFor} semantics are fully supported, both + * within a single annotation and within the annotation hierarchy. + *

    In contrast to {@link #getAllAnnotationAttributes}, the search + * algorithm used by this method will stop searching the annotation + * hierarchy once the first annotation of the specified + * {@code annotationName} has been found. As a consequence, additional + * annotations of the specified {@code annotationName} will be ignored. + *

    This method follows find semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. + * @param element the annotated element + * @param annotationName the fully qualified class name of the annotation type to find + * @param classValuesAsString whether to convert Class references into Strings or to + * preserve them as Class references + * @param nestedAnnotationsAsMap whether to convert nested Annotation instances into + * {@code AnnotationAttributes} maps or to preserve them as Annotation instances + * @return the merged {@code AnnotationAttributes}, or {@code null} if not found + * @since 4.2 + * @see #findMergedAnnotation(AnnotatedElement, Class) + * @see #getMergedAnnotationAttributes(AnnotatedElement, String, boolean, boolean) + */ + @Nullable + public static AnnotationAttributes findMergedAnnotationAttributes(AnnotatedElement element, + String annotationName, boolean classValuesAsString, boolean nestedAnnotationsAsMap) { + + MergedAnnotation mergedAnnotation = findAnnotations(element) + .get(annotationName, null, MergedAnnotationSelectors.firstDirectlyDeclared()); + return getAnnotationAttributes(mergedAnnotation, classValuesAsString, nestedAnnotationsAsMap); + } + + /** + * Find the first annotation of the specified {@code annotationType} within + * the annotation hierarchy above the supplied {@code element}, + * merge that annotation's attributes with matching attributes from + * annotations in lower levels of the annotation hierarchy, and synthesize + * the result back into an annotation of the specified {@code annotationType}. + *

    {@link AliasFor @AliasFor} semantics are fully supported, both + * within a single annotation and within the annotation hierarchy. + *

    This method follows find semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. + * @param element the annotated element + * @param annotationType the annotation type to find + * @return the merged, synthesized {@code Annotation}, or {@code null} if not found + * @since 4.2 + * @see #findAllMergedAnnotations(AnnotatedElement, Class) + * @see #findMergedAnnotationAttributes(AnnotatedElement, String, boolean, boolean) + * @see #getMergedAnnotationAttributes(AnnotatedElement, Class) + */ + @Nullable + public static A findMergedAnnotation(AnnotatedElement element, Class annotationType) { + // Shortcut: directly present on the element, with no merging needed? + if (AnnotationFilter.PLAIN.matches(annotationType) || + AnnotationsScanner.hasPlainJavaAnnotationsOnly(element)) { + return element.getDeclaredAnnotation(annotationType); + } + // Exhaustive retrieval of merged annotations... + return findAnnotations(element) + .get(annotationType, null, MergedAnnotationSelectors.firstDirectlyDeclared()) + .synthesize(MergedAnnotation::isPresent).orElse(null); + } + + /** + * Find all annotations of the specified {@code annotationType} + * within the annotation hierarchy above the supplied {@code element}; + * and for each annotation found, merge that annotation's attributes with + * matching attributes from annotations in lower levels of the annotation + * hierarchy and synthesize the results back into an annotation of the specified + * {@code annotationType}. + *

    {@link AliasFor @AliasFor} semantics are fully supported, both within a + * single annotation and within annotation hierarchies. + *

    This method follows find semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. + * @param element the annotated element (never {@code null}) + * @param annotationType the annotation type to find (never {@code null}) + * @return the set of all merged, synthesized {@code Annotations} found, + * or an empty set if none were found + * @since 4.3 + * @see #findMergedAnnotation(AnnotatedElement, Class) + * @see #getAllMergedAnnotations(AnnotatedElement, Class) + */ + public static Set findAllMergedAnnotations(AnnotatedElement element, Class annotationType) { + return findAnnotations(element).stream(annotationType) + .sorted(highAggregateIndexesFirst()) + .collect(MergedAnnotationCollectors.toAnnotationSet()); + } + + /** + * Find all annotations of the specified {@code annotationTypes} + * within the annotation hierarchy above the supplied {@code element}; + * and for each annotation found, merge that annotation's attributes with + * matching attributes from annotations in lower levels of the + * annotation hierarchy and synthesize the results back into an annotation + * of the corresponding {@code annotationType}. + *

    {@link AliasFor @AliasFor} semantics are fully supported, both within a + * single annotation and within annotation hierarchies. + *

    This method follows find semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. + * @param element the annotated element (never {@code null}) + * @param annotationTypes the annotation types to find + * @return the set of all merged, synthesized {@code Annotations} found, + * or an empty set if none were found + * @since 5.1 + * @see #findAllMergedAnnotations(AnnotatedElement, Class) + */ + public static Set findAllMergedAnnotations(AnnotatedElement element, Set> annotationTypes) { + return findAnnotations(element).stream() + .filter(MergedAnnotationPredicates.typeIn(annotationTypes)) + .sorted(highAggregateIndexesFirst()) + .collect(MergedAnnotationCollectors.toAnnotationSet()); + } + + /** + * Find all repeatable annotations of the specified {@code annotationType} + * within the annotation hierarchy above the supplied {@code element}; + * and for each annotation found, merge that annotation's attributes with + * matching attributes from annotations in lower levels of the annotation + * hierarchy and synthesize the results back into an annotation of the specified + * {@code annotationType}. + *

    The container type that holds the repeatable annotations will be looked up + * via {@link java.lang.annotation.Repeatable}. + *

    {@link AliasFor @AliasFor} semantics are fully supported, both within a + * single annotation and within annotation hierarchies. + *

    This method follows find semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. + * @param element the annotated element (never {@code null}) + * @param annotationType the annotation type to find (never {@code null}) + * @return the set of all merged repeatable {@code Annotations} found, + * or an empty set if none were found + * @throws IllegalArgumentException if the {@code element} or {@code annotationType} + * is {@code null}, or if the container type cannot be resolved + * @since 4.3 + * @see #findMergedAnnotation(AnnotatedElement, Class) + * @see #findAllMergedAnnotations(AnnotatedElement, Class) + * @see #findMergedRepeatableAnnotations(AnnotatedElement, Class, Class) + */ + public static Set findMergedRepeatableAnnotations(AnnotatedElement element, + Class annotationType) { + + return findMergedRepeatableAnnotations(element, annotationType, null); + } + + /** + * Find all repeatable annotations of the specified {@code annotationType} + * within the annotation hierarchy above the supplied {@code element}; + * and for each annotation found, merge that annotation's attributes with + * matching attributes from annotations in lower levels of the annotation + * hierarchy and synthesize the results back into an annotation of the specified + * {@code annotationType}. + *

    {@link AliasFor @AliasFor} semantics are fully supported, both within a + * single annotation and within annotation hierarchies. + *

    This method follows find semantics as described in the + * {@linkplain AnnotatedElementUtils class-level javadoc}. + * @param element the annotated element (never {@code null}) + * @param annotationType the annotation type to find (never {@code null}) + * @param containerType the type of the container that holds the annotations; + * may be {@code null} if the container type should be looked up via + * {@link java.lang.annotation.Repeatable} + * @return the set of all merged repeatable {@code Annotations} found, + * or an empty set if none were found + * @throws IllegalArgumentException if the {@code element} or {@code annotationType} + * is {@code null}, or if the container type cannot be resolved + * @throws AnnotationConfigurationException if the supplied {@code containerType} + * is not a valid container annotation for the supplied {@code annotationType} + * @since 4.3 + * @see #findMergedAnnotation(AnnotatedElement, Class) + * @see #findAllMergedAnnotations(AnnotatedElement, Class) + */ + public static Set findMergedRepeatableAnnotations(AnnotatedElement element, + Class annotationType, @Nullable Class containerType) { + + return findRepeatableAnnotations(element, containerType, annotationType) + .stream(annotationType) + .sorted(highAggregateIndexesFirst()) + .collect(MergedAnnotationCollectors.toAnnotationSet()); + } + + private static MergedAnnotations getAnnotations(AnnotatedElement element) { + return MergedAnnotations.from(element, SearchStrategy.INHERITED_ANNOTATIONS, RepeatableContainers.none()); + } + + private static MergedAnnotations getRepeatableAnnotations(AnnotatedElement element, + @Nullable Class containerType, Class annotationType) { + + RepeatableContainers repeatableContainers = RepeatableContainers.of(annotationType, containerType); + return MergedAnnotations.from(element, SearchStrategy.INHERITED_ANNOTATIONS, repeatableContainers); + } + + private static MergedAnnotations findAnnotations(AnnotatedElement element) { + return MergedAnnotations.from(element, SearchStrategy.TYPE_HIERARCHY, RepeatableContainers.none()); + } + + private static MergedAnnotations findRepeatableAnnotations(AnnotatedElement element, + @Nullable Class containerType, Class annotationType) { + + RepeatableContainers repeatableContainers = RepeatableContainers.of(annotationType, containerType); + return MergedAnnotations.from(element, SearchStrategy.TYPE_HIERARCHY, repeatableContainers); + } + + @Nullable + private static MultiValueMap nullIfEmpty(MultiValueMap map) { + return (map.isEmpty() ? null : map); + } + + private static Comparator> highAggregateIndexesFirst() { + return Comparator.> comparingInt( + MergedAnnotation::getAggregateIndex).reversed(); + } + + @Nullable + private static AnnotationAttributes getAnnotationAttributes(MergedAnnotation annotation, + boolean classValuesAsString, boolean nestedAnnotationsAsMap) { + + if (!annotation.isPresent()) { + return null; + } + return annotation.asAnnotationAttributes( + Adapt.values(classValuesAsString, nestedAnnotationsAsMap)); + } + + + /** + * Adapted {@link AnnotatedElement} that hold specific annotations. + */ + private static class AnnotatedElementForAnnotations implements AnnotatedElement { + + private final Annotation[] annotations; + + AnnotatedElementForAnnotations(Annotation... annotations) { + this.annotations = annotations; + } + + @Override + @SuppressWarnings("unchecked") + @Nullable + public T getAnnotation(Class annotationClass) { + for (Annotation annotation : this.annotations) { + if (annotation.annotationType() == annotationClass) { + return (T) annotation; + } + } + return null; + } + + @Override + public Annotation[] getAnnotations() { + return this.annotations.clone(); + } + + @Override + public Annotation[] getDeclaredAnnotations() { + return this.annotations.clone(); + } + + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java new file mode 100644 index 0000000..3a6f44f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java @@ -0,0 +1,433 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * {@link LinkedHashMap} subclass representing annotation attribute + * key-value pairs as read by {@link AnnotationUtils}, + * {@link AnnotatedElementUtils}, and Spring's reflection- and ASM-based + * {@link org.springframework.core.type.AnnotationMetadata} implementations. + * + *

    Provides 'pseudo-reification' to avoid noisy Map generics in the calling + * code as well as convenience methods for looking up annotation attributes + * in a type-safe fashion. + * + * @author Chris Beams + * @author Sam Brannen + * @author Juergen Hoeller + * @since 3.1.1 + * @see AnnotationUtils#getAnnotationAttributes + * @see AnnotatedElementUtils + */ +@SuppressWarnings("serial") +public class AnnotationAttributes extends LinkedHashMap { + + private static final String UNKNOWN = "unknown"; + + @Nullable + private final Class annotationType; + + final String displayName; + + boolean validated = false; + + + /** + * Create a new, empty {@link AnnotationAttributes} instance. + */ + public AnnotationAttributes() { + this.annotationType = null; + this.displayName = UNKNOWN; + } + + /** + * Create a new, empty {@link AnnotationAttributes} instance with the + * given initial capacity to optimize performance. + * @param initialCapacity initial size of the underlying map + */ + public AnnotationAttributes(int initialCapacity) { + super(initialCapacity); + this.annotationType = null; + this.displayName = UNKNOWN; + } + + /** + * Create a new {@link AnnotationAttributes} instance, wrapping the provided + * map and all its key-value pairs. + * @param map original source of annotation attribute key-value pairs + * @see #fromMap(Map) + */ + public AnnotationAttributes(Map map) { + super(map); + this.annotationType = null; + this.displayName = UNKNOWN; + } + + /** + * Create a new {@link AnnotationAttributes} instance, wrapping the provided + * map and all its key-value pairs. + * @param other original source of annotation attribute key-value pairs + * @see #fromMap(Map) + */ + public AnnotationAttributes(AnnotationAttributes other) { + super(other); + this.annotationType = other.annotationType; + this.displayName = other.displayName; + this.validated = other.validated; + } + + /** + * Create a new, empty {@link AnnotationAttributes} instance for the + * specified {@code annotationType}. + * @param annotationType the type of annotation represented by this + * {@code AnnotationAttributes} instance; never {@code null} + * @since 4.2 + */ + public AnnotationAttributes(Class annotationType) { + Assert.notNull(annotationType, "'annotationType' must not be null"); + this.annotationType = annotationType; + this.displayName = annotationType.getName(); + } + + /** + * Create a possibly already validated new, empty + * {@link AnnotationAttributes} instance for the specified + * {@code annotationType}. + * @param annotationType the type of annotation represented by this + * {@code AnnotationAttributes} instance; never {@code null} + * @param validated if the attributes are considered already validated + * @since 5.2 + */ + AnnotationAttributes(Class annotationType, boolean validated) { + Assert.notNull(annotationType, "'annotationType' must not be null"); + this.annotationType = annotationType; + this.displayName = annotationType.getName(); + this.validated = validated; + } + + /** + * Create a new, empty {@link AnnotationAttributes} instance for the + * specified {@code annotationType}. + * @param annotationType the annotation type name represented by this + * {@code AnnotationAttributes} instance; never {@code null} + * @param classLoader the ClassLoader to try to load the annotation type on, + * or {@code null} to just store the annotation type name + * @since 4.3.2 + */ + public AnnotationAttributes(String annotationType, @Nullable ClassLoader classLoader) { + Assert.notNull(annotationType, "'annotationType' must not be null"); + this.annotationType = getAnnotationType(annotationType, classLoader); + this.displayName = annotationType; + } + + @SuppressWarnings("unchecked") + @Nullable + private static Class getAnnotationType(String annotationType, @Nullable ClassLoader classLoader) { + if (classLoader != null) { + try { + return (Class) classLoader.loadClass(annotationType); + } + catch (ClassNotFoundException ex) { + // Annotation Class not resolvable + } + } + return null; + } + + + /** + * Get the type of annotation represented by this {@code AnnotationAttributes}. + * @return the annotation type, or {@code null} if unknown + * @since 4.2 + */ + @Nullable + public Class annotationType() { + return this.annotationType; + } + + /** + * Get the value stored under the specified {@code attributeName} as a string. + * @param attributeName the name of the attribute to get; + * never {@code null} or empty + * @return the value + * @throws IllegalArgumentException if the attribute does not exist or + * if it is not of the expected type + */ + public String getString(String attributeName) { + return getRequiredAttribute(attributeName, String.class); + } + + /** + * Get the value stored under the specified {@code attributeName} as an + * array of strings. + *

    If the value stored under the specified {@code attributeName} is + * a string, it will be wrapped in a single-element array before + * returning it. + * @param attributeName the name of the attribute to get; + * never {@code null} or empty + * @return the value + * @throws IllegalArgumentException if the attribute does not exist or + * if it is not of the expected type + */ + public String[] getStringArray(String attributeName) { + return getRequiredAttribute(attributeName, String[].class); + } + + /** + * Get the value stored under the specified {@code attributeName} as a boolean. + * @param attributeName the name of the attribute to get; + * never {@code null} or empty + * @return the value + * @throws IllegalArgumentException if the attribute does not exist or + * if it is not of the expected type + */ + public boolean getBoolean(String attributeName) { + return getRequiredAttribute(attributeName, Boolean.class); + } + + /** + * Get the value stored under the specified {@code attributeName} as a number. + * @param attributeName the name of the attribute to get; + * never {@code null} or empty + * @return the value + * @throws IllegalArgumentException if the attribute does not exist or + * if it is not of the expected type + */ + @SuppressWarnings("unchecked") + public N getNumber(String attributeName) { + return (N) getRequiredAttribute(attributeName, Number.class); + } + + /** + * Get the value stored under the specified {@code attributeName} as an enum. + * @param attributeName the name of the attribute to get; + * never {@code null} or empty + * @return the value + * @throws IllegalArgumentException if the attribute does not exist or + * if it is not of the expected type + */ + @SuppressWarnings("unchecked") + public > E getEnum(String attributeName) { + return (E) getRequiredAttribute(attributeName, Enum.class); + } + + /** + * Get the value stored under the specified {@code attributeName} as a class. + * @param attributeName the name of the attribute to get; + * never {@code null} or empty + * @return the value + * @throws IllegalArgumentException if the attribute does not exist or + * if it is not of the expected type + */ + @SuppressWarnings("unchecked") + public Class getClass(String attributeName) { + return getRequiredAttribute(attributeName, Class.class); + } + + /** + * Get the value stored under the specified {@code attributeName} as an + * array of classes. + *

    If the value stored under the specified {@code attributeName} is a class, + * it will be wrapped in a single-element array before returning it. + * @param attributeName the name of the attribute to get; + * never {@code null} or empty + * @return the value + * @throws IllegalArgumentException if the attribute does not exist or + * if it is not of the expected type + */ + public Class[] getClassArray(String attributeName) { + return getRequiredAttribute(attributeName, Class[].class); + } + + /** + * Get the {@link AnnotationAttributes} stored under the specified + * {@code attributeName}. + *

    Note: if you expect an actual annotation, invoke + * {@link #getAnnotation(String, Class)} instead. + * @param attributeName the name of the attribute to get; + * never {@code null} or empty + * @return the {@code AnnotationAttributes} + * @throws IllegalArgumentException if the attribute does not exist or + * if it is not of the expected type + */ + public AnnotationAttributes getAnnotation(String attributeName) { + return getRequiredAttribute(attributeName, AnnotationAttributes.class); + } + + /** + * Get the annotation of type {@code annotationType} stored under the + * specified {@code attributeName}. + * @param attributeName the name of the attribute to get; + * never {@code null} or empty + * @param annotationType the expected annotation type; never {@code null} + * @return the annotation + * @throws IllegalArgumentException if the attribute does not exist or + * if it is not of the expected type + * @since 4.2 + */ + public A getAnnotation(String attributeName, Class annotationType) { + return getRequiredAttribute(attributeName, annotationType); + } + + /** + * Get the array of {@link AnnotationAttributes} stored under the specified + * {@code attributeName}. + *

    If the value stored under the specified {@code attributeName} is + * an instance of {@code AnnotationAttributes}, it will be wrapped in + * a single-element array before returning it. + *

    Note: if you expect an actual array of annotations, invoke + * {@link #getAnnotationArray(String, Class)} instead. + * @param attributeName the name of the attribute to get; + * never {@code null} or empty + * @return the array of {@code AnnotationAttributes} + * @throws IllegalArgumentException if the attribute does not exist or + * if it is not of the expected type + */ + public AnnotationAttributes[] getAnnotationArray(String attributeName) { + return getRequiredAttribute(attributeName, AnnotationAttributes[].class); + } + + /** + * Get the array of type {@code annotationType} stored under the specified + * {@code attributeName}. + *

    If the value stored under the specified {@code attributeName} is + * an {@code Annotation}, it will be wrapped in a single-element array + * before returning it. + * @param attributeName the name of the attribute to get; + * never {@code null} or empty + * @param annotationType the expected annotation type; never {@code null} + * @return the annotation array + * @throws IllegalArgumentException if the attribute does not exist or + * if it is not of the expected type + * @since 4.2 + */ + @SuppressWarnings("unchecked") + public A[] getAnnotationArray(String attributeName, Class annotationType) { + Object array = Array.newInstance(annotationType, 0); + return (A[]) getRequiredAttribute(attributeName, array.getClass()); + } + + /** + * Get the value stored under the specified {@code attributeName}, + * ensuring that the value is of the {@code expectedType}. + *

    If the {@code expectedType} is an array and the value stored + * under the specified {@code attributeName} is a single element of the + * component type of the expected array type, the single element will be + * wrapped in a single-element array of the appropriate type before + * returning it. + * @param attributeName the name of the attribute to get; + * never {@code null} or empty + * @param expectedType the expected type; never {@code null} + * @return the value + * @throws IllegalArgumentException if the attribute does not exist or + * if it is not of the expected type + */ + @SuppressWarnings("unchecked") + private T getRequiredAttribute(String attributeName, Class expectedType) { + Assert.hasText(attributeName, "'attributeName' must not be null or empty"); + Object value = get(attributeName); + assertAttributePresence(attributeName, value); + assertNotException(attributeName, value); + if (!expectedType.isInstance(value) && expectedType.isArray() && + expectedType.getComponentType().isInstance(value)) { + Object array = Array.newInstance(expectedType.getComponentType(), 1); + Array.set(array, 0, value); + value = array; + } + assertAttributeType(attributeName, value, expectedType); + return (T) value; + } + + private void assertAttributePresence(String attributeName, Object attributeValue) { + Assert.notNull(attributeValue, () -> String.format( + "Attribute '%s' not found in attributes for annotation [%s]", + attributeName, this.displayName)); + } + + private void assertNotException(String attributeName, Object attributeValue) { + if (attributeValue instanceof Throwable) { + throw new IllegalArgumentException(String.format( + "Attribute '%s' for annotation [%s] was not resolvable due to exception [%s]", + attributeName, this.displayName, attributeValue), (Throwable) attributeValue); + } + } + + private void assertAttributeType(String attributeName, Object attributeValue, Class expectedType) { + if (!expectedType.isInstance(attributeValue)) { + throw new IllegalArgumentException(String.format( + "Attribute '%s' is of type %s, but %s was expected in attributes for annotation [%s]", + attributeName, attributeValue.getClass().getSimpleName(), expectedType.getSimpleName(), + this.displayName)); + } + } + + @Override + public String toString() { + Iterator> entries = entrySet().iterator(); + StringBuilder sb = new StringBuilder("{"); + while (entries.hasNext()) { + Map.Entry entry = entries.next(); + sb.append(entry.getKey()); + sb.append('='); + sb.append(valueToString(entry.getValue())); + sb.append(entries.hasNext() ? ", " : ""); + } + sb.append("}"); + return sb.toString(); + } + + private String valueToString(Object value) { + if (value == this) { + return "(this Map)"; + } + if (value instanceof Object[]) { + return "[" + StringUtils.arrayToDelimitedString((Object[]) value, ", ") + "]"; + } + return String.valueOf(value); + } + + + /** + * Return an {@link AnnotationAttributes} instance based on the given map. + *

    If the map is already an {@code AnnotationAttributes} instance, it + * will be cast and returned immediately without creating a new instance. + * Otherwise a new instance will be created by passing the supplied map + * to the {@link #AnnotationAttributes(Map)} constructor. + * @param map original source of annotation attribute key-value pairs + */ + @Nullable + public static AnnotationAttributes fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + if (map instanceof AnnotationAttributes) { + return (AnnotationAttributes) map; + } + return new AnnotationAttributes(map); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAwareOrderComparator.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAwareOrderComparator.java new file mode 100644 index 0000000..1fe06d4 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAwareOrderComparator.java @@ -0,0 +1,145 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.reflect.AnnotatedElement; +import java.util.Arrays; +import java.util.List; + +import org.springframework.core.DecoratingProxy; +import org.springframework.core.OrderComparator; +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; +import org.springframework.lang.Nullable; + +/** + * {@code AnnotationAwareOrderComparator} is an extension of + * {@link OrderComparator} that supports Spring's + * {@link org.springframework.core.Ordered} interface as well as the + * {@link Order @Order} and {@link javax.annotation.Priority @Priority} + * annotations, with an order value provided by an {@code Ordered} + * instance overriding a statically defined annotation value (if any). + * + *

    Consult the Javadoc for {@link OrderComparator} for details on the + * sort semantics for non-ordered objects. + * + * @author Juergen Hoeller + * @author Oliver Gierke + * @author Stephane Nicoll + * @since 2.0.1 + * @see org.springframework.core.Ordered + * @see org.springframework.core.annotation.Order + * @see javax.annotation.Priority + */ +public class AnnotationAwareOrderComparator extends OrderComparator { + + /** + * Shared default instance of {@code AnnotationAwareOrderComparator}. + */ + public static final AnnotationAwareOrderComparator INSTANCE = new AnnotationAwareOrderComparator(); + + + /** + * This implementation checks for {@link Order @Order} or + * {@link javax.annotation.Priority @Priority} on various kinds of + * elements, in addition to the {@link org.springframework.core.Ordered} + * check in the superclass. + */ + @Override + @Nullable + protected Integer findOrder(Object obj) { + Integer order = super.findOrder(obj); + if (order != null) { + return order; + } + return findOrderFromAnnotation(obj); + } + + @Nullable + private Integer findOrderFromAnnotation(Object obj) { + AnnotatedElement element = (obj instanceof AnnotatedElement ? (AnnotatedElement) obj : obj.getClass()); + MergedAnnotations annotations = MergedAnnotations.from(element, SearchStrategy.TYPE_HIERARCHY); + Integer order = OrderUtils.getOrderFromAnnotations(element, annotations); + if (order == null && obj instanceof DecoratingProxy) { + return findOrderFromAnnotation(((DecoratingProxy) obj).getDecoratedClass()); + } + return order; + } + + /** + * This implementation retrieves an @{@link javax.annotation.Priority} + * value, allowing for additional semantics over the regular @{@link Order} + * annotation: typically, selecting one object over another in case of + * multiple matches but only one object to be returned. + */ + @Override + @Nullable + public Integer getPriority(Object obj) { + if (obj instanceof Class) { + return OrderUtils.getPriority((Class) obj); + } + Integer priority = OrderUtils.getPriority(obj.getClass()); + if (priority == null && obj instanceof DecoratingProxy) { + return getPriority(((DecoratingProxy) obj).getDecoratedClass()); + } + return priority; + } + + + /** + * Sort the given list with a default {@link AnnotationAwareOrderComparator}. + *

    Optimized to skip sorting for lists with size 0 or 1, + * in order to avoid unnecessary array extraction. + * @param list the List to sort + * @see java.util.List#sort(java.util.Comparator) + */ + public static void sort(List list) { + if (list.size() > 1) { + list.sort(INSTANCE); + } + } + + /** + * Sort the given array with a default AnnotationAwareOrderComparator. + *

    Optimized to skip sorting for lists with size 0 or 1, + * in order to avoid unnecessary array extraction. + * @param array the array to sort + * @see java.util.Arrays#sort(Object[], java.util.Comparator) + */ + public static void sort(Object[] array) { + if (array.length > 1) { + Arrays.sort(array, INSTANCE); + } + } + + /** + * Sort the given array or List with a default AnnotationAwareOrderComparator, + * if necessary. Simply skips sorting when given any other value. + *

    Optimized to skip sorting for lists with size 0 or 1, + * in order to avoid unnecessary array extraction. + * @param value the array or List to sort + * @see java.util.Arrays#sort(Object[], java.util.Comparator) + */ + public static void sortIfNecessary(Object value) { + if (value instanceof Object[]) { + sort((Object[]) value); + } + else if (value instanceof List) { + sort((List) value); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationConfigurationException.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationConfigurationException.java new file mode 100644 index 0000000..4f4839d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationConfigurationException.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import org.springframework.core.NestedRuntimeException; + +/** + * Thrown by {@link AnnotationUtils} and synthesized annotations + * if an annotation is improperly configured. + * + * @author Sam Brannen + * @since 4.2 + * @see AnnotationUtils + * @see SynthesizedAnnotation + */ +@SuppressWarnings("serial") +public class AnnotationConfigurationException extends NestedRuntimeException { + + /** + * Construct a new {@code AnnotationConfigurationException} with the + * supplied message. + * @param message the detail message + */ + public AnnotationConfigurationException(String message) { + super(message); + } + + /** + * Construct a new {@code AnnotationConfigurationException} with the + * supplied message and cause. + * @param message the detail message + * @param cause the root cause + */ + public AnnotationConfigurationException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationFilter.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationFilter.java new file mode 100644 index 0000000..07963dc --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationFilter.java @@ -0,0 +1,139 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; + +/** + * Callback interface that can be used to filter specific annotation types. + * + *

    Note that the {@link MergedAnnotations} model (which this interface has been + * designed for) always ignores lang annotations according to the {@link #PLAIN} + * filter (for efficiency reasons). Any additional filters and even custom filter + * implementations apply within this boundary and may only narrow further from here. + * + * @author Phillip Webb + * @author Juergen Hoeller + * @since 5.2 + * @see MergedAnnotations + */ +@FunctionalInterface +public interface AnnotationFilter { + + /** + * {@link AnnotationFilter} that matches annotations in the + * {@code java.lang} and {@code org.springframework.lang} packages + * and their subpackages. + *

    This is the default filter in the {@link MergedAnnotations} model. + */ + AnnotationFilter PLAIN = packages("java.lang", "org.springframework.lang"); + + /** + * {@link AnnotationFilter} that matches annotations in the + * {@code java} and {@code javax} packages and their subpackages. + */ + AnnotationFilter JAVA = packages("java", "javax"); + + /** + * {@link AnnotationFilter} that always matches and can be used when no + * relevant annotation types are expected to be present at all. + */ + AnnotationFilter ALL = new AnnotationFilter() { + @Override + public boolean matches(Annotation annotation) { + return true; + } + @Override + public boolean matches(Class type) { + return true; + } + @Override + public boolean matches(String typeName) { + return true; + } + @Override + public String toString() { + return "All annotations filtered"; + } + }; + + /** + * {@link AnnotationFilter} that never matches and can be used when no + * filtering is needed (allowing for any annotation types to be present). + * @deprecated as of 5.2.6 since the {@link MergedAnnotations} model + * always ignores lang annotations according to the {@link #PLAIN} filter + * (for efficiency reasons) + * @see #PLAIN + */ + @Deprecated + AnnotationFilter NONE = new AnnotationFilter() { + @Override + public boolean matches(Annotation annotation) { + return false; + } + @Override + public boolean matches(Class type) { + return false; + } + @Override + public boolean matches(String typeName) { + return false; + } + @Override + public String toString() { + return "No annotation filtering"; + } + }; + + + /** + * Test if the given annotation matches the filter. + * @param annotation the annotation to test + * @return {@code true} if the annotation matches + */ + default boolean matches(Annotation annotation) { + return matches(annotation.annotationType()); + } + + /** + * Test if the given type matches the filter. + * @param type the annotation type to test + * @return {@code true} if the annotation matches + */ + default boolean matches(Class type) { + return matches(type.getName()); + } + + /** + * Test if the given type name matches the filter. + * @param typeName the fully qualified class name of the annotation type to test + * @return {@code true} if the annotation matches + */ + boolean matches(String typeName); + + + /** + * Create a new {@link AnnotationFilter} that matches annotations in the + * specified packages. + * @param packages the annotation packages that should match + * @return a new {@link AnnotationFilter} instance + */ + static AnnotationFilter packages(String... packages) { + return new PackagesAnnotationFilter(packages); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java new file mode 100644 index 0000000..ef87e2d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java @@ -0,0 +1,741 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +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; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.core.annotation.AnnotationTypeMapping.MirrorSets.MirrorSet; +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * Provides mapping information for a single annotation (or meta-annotation) in + * the context of a root annotation type. + * + * @author Phillip Webb + * @author Sam Brannen + * @since 5.2 + * @see AnnotationTypeMappings + */ +final class AnnotationTypeMapping { + + private static final MirrorSet[] EMPTY_MIRROR_SETS = new MirrorSet[0]; + + + @Nullable + private final AnnotationTypeMapping source; + + private final AnnotationTypeMapping root; + + private final int distance; + + private final Class annotationType; + + private final List> metaTypes; + + @Nullable + private final Annotation annotation; + + private final AttributeMethods attributes; + + private final MirrorSets mirrorSets; + + private final int[] aliasMappings; + + private final int[] conventionMappings; + + private final int[] annotationValueMappings; + + private final AnnotationTypeMapping[] annotationValueSource; + + private final Map> aliasedBy; + + private final boolean synthesizable; + + private final Set claimedAliases = new HashSet<>(); + + + AnnotationTypeMapping(@Nullable AnnotationTypeMapping source, + Class annotationType, @Nullable Annotation annotation) { + + this.source = source; + this.root = (source != null ? source.getRoot() : this); + this.distance = (source == null ? 0 : source.getDistance() + 1); + this.annotationType = annotationType; + this.metaTypes = merge( + source != null ? source.getMetaTypes() : null, + annotationType); + this.annotation = annotation; + this.attributes = AttributeMethods.forAnnotationType(annotationType); + this.mirrorSets = new MirrorSets(); + this.aliasMappings = filledIntArray(this.attributes.size()); + this.conventionMappings = filledIntArray(this.attributes.size()); + this.annotationValueMappings = filledIntArray(this.attributes.size()); + this.annotationValueSource = new AnnotationTypeMapping[this.attributes.size()]; + this.aliasedBy = resolveAliasedForTargets(); + processAliases(); + addConventionMappings(); + addConventionAnnotationValues(); + this.synthesizable = computeSynthesizableFlag(); + } + + + private static List merge(@Nullable List existing, T element) { + if (existing == null) { + return Collections.singletonList(element); + } + List merged = new ArrayList<>(existing.size() + 1); + merged.addAll(existing); + merged.add(element); + return Collections.unmodifiableList(merged); + } + + private Map> resolveAliasedForTargets() { + Map> aliasedBy = new HashMap<>(); + for (int i = 0; i < this.attributes.size(); i++) { + Method attribute = this.attributes.get(i); + AliasFor aliasFor = AnnotationsScanner.getDeclaredAnnotation(attribute, AliasFor.class); + if (aliasFor != null) { + Method target = resolveAliasTarget(attribute, aliasFor); + aliasedBy.computeIfAbsent(target, key -> new ArrayList<>()).add(attribute); + } + } + return Collections.unmodifiableMap(aliasedBy); + } + + private Method resolveAliasTarget(Method attribute, AliasFor aliasFor) { + return resolveAliasTarget(attribute, aliasFor, true); + } + + private Method resolveAliasTarget(Method attribute, AliasFor aliasFor, boolean checkAliasPair) { + if (StringUtils.hasText(aliasFor.value()) && StringUtils.hasText(aliasFor.attribute())) { + throw new AnnotationConfigurationException(String.format( + "In @AliasFor declared on %s, attribute 'attribute' and its alias 'value' " + + "are present with values of '%s' and '%s', but only one is permitted.", + AttributeMethods.describe(attribute), aliasFor.attribute(), + aliasFor.value())); + } + Class targetAnnotation = aliasFor.annotation(); + if (targetAnnotation == Annotation.class) { + targetAnnotation = this.annotationType; + } + String targetAttributeName = aliasFor.attribute(); + if (!StringUtils.hasLength(targetAttributeName)) { + targetAttributeName = aliasFor.value(); + } + if (!StringUtils.hasLength(targetAttributeName)) { + targetAttributeName = attribute.getName(); + } + Method target = AttributeMethods.forAnnotationType(targetAnnotation).get(targetAttributeName); + if (target == null) { + if (targetAnnotation == this.annotationType) { + throw new AnnotationConfigurationException(String.format( + "@AliasFor declaration on %s declares an alias for '%s' which is not present.", + AttributeMethods.describe(attribute), targetAttributeName)); + } + throw new AnnotationConfigurationException(String.format( + "%s is declared as an @AliasFor nonexistent %s.", + StringUtils.capitalize(AttributeMethods.describe(attribute)), + AttributeMethods.describe(targetAnnotation, targetAttributeName))); + } + if (target.equals(attribute)) { + throw new AnnotationConfigurationException(String.format( + "@AliasFor declaration on %s points to itself. " + + "Specify 'annotation' to point to a same-named attribute on a meta-annotation.", + AttributeMethods.describe(attribute))); + } + if (!isCompatibleReturnType(attribute.getReturnType(), target.getReturnType())) { + throw new AnnotationConfigurationException(String.format( + "Misconfigured aliases: %s and %s must declare the same return type.", + AttributeMethods.describe(attribute), + AttributeMethods.describe(target))); + } + if (isAliasPair(target) && checkAliasPair) { + AliasFor targetAliasFor = target.getAnnotation(AliasFor.class); + if (targetAliasFor != null) { + Method mirror = resolveAliasTarget(target, targetAliasFor, false); + if (!mirror.equals(attribute)) { + throw new AnnotationConfigurationException(String.format( + "%s must be declared as an @AliasFor %s, not %s.", + StringUtils.capitalize(AttributeMethods.describe(target)), + AttributeMethods.describe(attribute), AttributeMethods.describe(mirror))); + } + } + } + return target; + } + + private boolean isAliasPair(Method target) { + return (this.annotationType == target.getDeclaringClass()); + } + + private boolean isCompatibleReturnType(Class attributeType, Class targetType) { + return (attributeType == targetType || attributeType == targetType.getComponentType()); + } + + private void processAliases() { + List aliases = new ArrayList<>(); + for (int i = 0; i < this.attributes.size(); i++) { + aliases.clear(); + aliases.add(this.attributes.get(i)); + collectAliases(aliases); + if (aliases.size() > 1) { + processAliases(i, aliases); + } + } + } + + private void collectAliases(List aliases) { + AnnotationTypeMapping mapping = this; + while (mapping != null) { + int size = aliases.size(); + for (int j = 0; j < size; j++) { + List additional = mapping.aliasedBy.get(aliases.get(j)); + if (additional != null) { + aliases.addAll(additional); + } + } + mapping = mapping.source; + } + } + + private void processAliases(int attributeIndex, List aliases) { + int rootAttributeIndex = getFirstRootAttributeIndex(aliases); + AnnotationTypeMapping mapping = this; + while (mapping != null) { + if (rootAttributeIndex != -1 && mapping != this.root) { + for (int i = 0; i < mapping.attributes.size(); i++) { + if (aliases.contains(mapping.attributes.get(i))) { + mapping.aliasMappings[i] = rootAttributeIndex; + } + } + } + mapping.mirrorSets.updateFrom(aliases); + mapping.claimedAliases.addAll(aliases); + if (mapping.annotation != null) { + int[] resolvedMirrors = mapping.mirrorSets.resolve(null, + mapping.annotation, ReflectionUtils::invokeMethod); + for (int i = 0; i < mapping.attributes.size(); i++) { + if (aliases.contains(mapping.attributes.get(i))) { + this.annotationValueMappings[attributeIndex] = resolvedMirrors[i]; + this.annotationValueSource[attributeIndex] = mapping; + } + } + } + mapping = mapping.source; + } + } + + private int getFirstRootAttributeIndex(Collection aliases) { + AttributeMethods rootAttributes = this.root.getAttributes(); + for (int i = 0; i < rootAttributes.size(); i++) { + if (aliases.contains(rootAttributes.get(i))) { + return i; + } + } + return -1; + } + + private void addConventionMappings() { + if (this.distance == 0) { + return; + } + AttributeMethods rootAttributes = this.root.getAttributes(); + int[] mappings = this.conventionMappings; + for (int i = 0; i < mappings.length; i++) { + String name = this.attributes.get(i).getName(); + MirrorSet mirrors = getMirrorSets().getAssigned(i); + int mapped = rootAttributes.indexOf(name); + if (!MergedAnnotation.VALUE.equals(name) && mapped != -1) { + mappings[i] = mapped; + if (mirrors != null) { + for (int j = 0; j < mirrors.size(); j++) { + mappings[mirrors.getAttributeIndex(j)] = mapped; + } + } + } + } + } + + private void addConventionAnnotationValues() { + for (int i = 0; i < this.attributes.size(); i++) { + Method attribute = this.attributes.get(i); + boolean isValueAttribute = MergedAnnotation.VALUE.equals(attribute.getName()); + AnnotationTypeMapping mapping = this; + while (mapping != null && mapping.distance > 0) { + int mapped = mapping.getAttributes().indexOf(attribute.getName()); + if (mapped != -1 && isBetterConventionAnnotationValue(i, isValueAttribute, mapping)) { + this.annotationValueMappings[i] = mapped; + this.annotationValueSource[i] = mapping; + } + mapping = mapping.source; + } + } + } + + private boolean isBetterConventionAnnotationValue(int index, boolean isValueAttribute, + AnnotationTypeMapping mapping) { + + if (this.annotationValueMappings[index] == -1) { + return true; + } + int existingDistance = this.annotationValueSource[index].distance; + return !isValueAttribute && existingDistance > mapping.distance; + } + + @SuppressWarnings("unchecked") + private boolean computeSynthesizableFlag() { + // Uses @AliasFor for local aliases? + for (int index : this.aliasMappings) { + if (index != -1) { + return true; + } + } + + // Uses @AliasFor for attribute overrides in meta-annotations? + if (!this.aliasedBy.isEmpty()) { + return true; + } + + // Uses convention-based attribute overrides in meta-annotations? + for (int index : this.conventionMappings) { + if (index != -1) { + return true; + } + } + + // Has nested annotations or arrays of annotations that are synthesizable? + if (getAttributes().hasNestedAnnotation()) { + AttributeMethods attributeMethods = getAttributes(); + for (int i = 0; i < attributeMethods.size(); i++) { + Method method = attributeMethods.get(i); + Class type = method.getReturnType(); + if (type.isAnnotation() || (type.isArray() && type.getComponentType().isAnnotation())) { + Class annotationType = + (Class) (type.isAnnotation() ? type : type.getComponentType()); + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(annotationType).get(0); + if (mapping.isSynthesizable()) { + return true; + } + } + } + } + + return false; + } + + /** + * Method called after all mappings have been set. At this point no further + * lookups from child mappings will occur. + */ + void afterAllMappingsSet() { + validateAllAliasesClaimed(); + for (int i = 0; i < this.mirrorSets.size(); i++) { + validateMirrorSet(this.mirrorSets.get(i)); + } + this.claimedAliases.clear(); + } + + private void validateAllAliasesClaimed() { + for (int i = 0; i < this.attributes.size(); i++) { + Method attribute = this.attributes.get(i); + AliasFor aliasFor = AnnotationsScanner.getDeclaredAnnotation(attribute, AliasFor.class); + if (aliasFor != null && !this.claimedAliases.contains(attribute)) { + Method target = resolveAliasTarget(attribute, aliasFor); + throw new AnnotationConfigurationException(String.format( + "@AliasFor declaration on %s declares an alias for %s which is not meta-present.", + AttributeMethods.describe(attribute), AttributeMethods.describe(target))); + } + } + } + + private void validateMirrorSet(MirrorSet mirrorSet) { + Method firstAttribute = mirrorSet.get(0); + Object firstDefaultValue = firstAttribute.getDefaultValue(); + for (int i = 1; i <= mirrorSet.size() - 1; i++) { + Method mirrorAttribute = mirrorSet.get(i); + Object mirrorDefaultValue = mirrorAttribute.getDefaultValue(); + if (firstDefaultValue == null || mirrorDefaultValue == null) { + throw new AnnotationConfigurationException(String.format( + "Misconfigured aliases: %s and %s must declare default values.", + AttributeMethods.describe(firstAttribute), AttributeMethods.describe(mirrorAttribute))); + } + if (!ObjectUtils.nullSafeEquals(firstDefaultValue, mirrorDefaultValue)) { + throw new AnnotationConfigurationException(String.format( + "Misconfigured aliases: %s and %s must declare the same default value.", + AttributeMethods.describe(firstAttribute), AttributeMethods.describe(mirrorAttribute))); + } + } + } + + /** + * Get the root mapping. + * @return the root mapping + */ + AnnotationTypeMapping getRoot() { + return this.root; + } + + /** + * Get the source of the mapping or {@code null}. + * @return the source of the mapping + */ + @Nullable + AnnotationTypeMapping getSource() { + return this.source; + } + + /** + * Get the distance of this mapping. + * @return the distance of the mapping + */ + int getDistance() { + return this.distance; + } + + /** + * Get the type of the mapped annotation. + * @return the annotation type + */ + Class getAnnotationType() { + return this.annotationType; + } + + List> getMetaTypes() { + return this.metaTypes; + } + + /** + * Get the source annotation for this mapping. This will be the + * meta-annotation, or {@code null} if this is the root mapping. + * @return the source annotation of the mapping + */ + @Nullable + Annotation getAnnotation() { + return this.annotation; + } + + /** + * Get the annotation attributes for the mapping annotation type. + * @return the attribute methods + */ + AttributeMethods getAttributes() { + return this.attributes; + } + + /** + * Get the related index of an alias mapped attribute, or {@code -1} if + * there is no mapping. The resulting value is the index of the attribute on + * the root annotation that can be invoked in order to obtain the actual + * value. + * @param attributeIndex the attribute index of the source attribute + * @return the mapped attribute index or {@code -1} + */ + int getAliasMapping(int attributeIndex) { + return this.aliasMappings[attributeIndex]; + } + + /** + * Get the related index of a convention mapped attribute, or {@code -1} + * if there is no mapping. The resulting value is the index of the attribute + * on the root annotation that can be invoked in order to obtain the actual + * value. + * @param attributeIndex the attribute index of the source attribute + * @return the mapped attribute index or {@code -1} + */ + int getConventionMapping(int attributeIndex) { + return this.conventionMappings[attributeIndex]; + } + + /** + * Get a mapped attribute value from the most suitable + * {@link #getAnnotation() meta-annotation}. + *

    The resulting value is obtained from the closest meta-annotation, + * taking into consideration both convention and alias based mapping rules. + * For root mappings, this method will always return {@code null}. + * @param attributeIndex the attribute index of the source attribute + * @param metaAnnotationsOnly if only meta annotations should be considered. + * If this parameter is {@code false} then aliases within the annotation will + * also be considered. + * @return the mapped annotation value, or {@code null} + */ + @Nullable + Object getMappedAnnotationValue(int attributeIndex, boolean metaAnnotationsOnly) { + int mappedIndex = this.annotationValueMappings[attributeIndex]; + if (mappedIndex == -1) { + return null; + } + AnnotationTypeMapping source = this.annotationValueSource[attributeIndex]; + if (source == this && metaAnnotationsOnly) { + return null; + } + return ReflectionUtils.invokeMethod(source.attributes.get(mappedIndex), source.annotation); + } + + /** + * Determine if the specified value is equivalent to the default value of the + * attribute at the given index. + * @param attributeIndex the attribute index of the source attribute + * @param value the value to check + * @param valueExtractor the value extractor used to extract values from any + * nested annotations + * @return {@code true} if the value is equivalent to the default value + */ + boolean isEquivalentToDefaultValue(int attributeIndex, Object value, ValueExtractor valueExtractor) { + + Method attribute = this.attributes.get(attributeIndex); + return isEquivalentToDefaultValue(attribute, value, valueExtractor); + } + + /** + * Get the mirror sets for this type mapping. + * @return the attribute mirror sets + */ + MirrorSets getMirrorSets() { + return this.mirrorSets; + } + + /** + * Determine if the mapped annotation is synthesizable. + *

    Consult the documentation for {@link MergedAnnotation#synthesize()} + * for an explanation of what is considered synthesizable. + * @return {@code true} if the mapped annotation is synthesizable + * @since 5.2.6 + */ + boolean isSynthesizable() { + return this.synthesizable; + } + + + private static int[] filledIntArray(int size) { + int[] array = new int[size]; + Arrays.fill(array, -1); + return array; + } + + private static boolean isEquivalentToDefaultValue(Method attribute, Object value, + ValueExtractor valueExtractor) { + + return areEquivalent(attribute.getDefaultValue(), value, valueExtractor); + } + + private static boolean areEquivalent(@Nullable Object value, @Nullable Object extractedValue, + ValueExtractor valueExtractor) { + + if (ObjectUtils.nullSafeEquals(value, extractedValue)) { + return true; + } + if (value instanceof Class && extractedValue instanceof String) { + return areEquivalent((Class) value, (String) extractedValue); + } + if (value instanceof Class[] && extractedValue instanceof String[]) { + return areEquivalent((Class[]) value, (String[]) extractedValue); + } + if (value instanceof Annotation) { + return areEquivalent((Annotation) value, extractedValue, valueExtractor); + } + return false; + } + + private static boolean areEquivalent(Class[] value, String[] extractedValue) { + if (value.length != extractedValue.length) { + return false; + } + for (int i = 0; i < value.length; i++) { + if (!areEquivalent(value[i], extractedValue[i])) { + return false; + } + } + return true; + } + + private static boolean areEquivalent(Class value, String extractedValue) { + return value.getName().equals(extractedValue); + } + + private static boolean areEquivalent(Annotation annotation, @Nullable Object extractedValue, + ValueExtractor valueExtractor) { + + AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType()); + for (int i = 0; i < attributes.size(); i++) { + Method attribute = attributes.get(i); + Object value1 = ReflectionUtils.invokeMethod(attribute, annotation); + Object value2; + if (extractedValue instanceof TypeMappedAnnotation) { + value2 = ((TypeMappedAnnotation) extractedValue).getValue(attribute.getName()).orElse(null); + } + else { + value2 = valueExtractor.extract(attribute, extractedValue); + } + if (!areEquivalent(value1, value2, valueExtractor)) { + return false; + } + } + return true; + } + + + /** + * A collection of {@link MirrorSet} instances that provides details of all + * defined mirrors. + */ + class MirrorSets { + + private MirrorSet[] mirrorSets; + + private final MirrorSet[] assigned; + + MirrorSets() { + this.assigned = new MirrorSet[attributes.size()]; + this.mirrorSets = EMPTY_MIRROR_SETS; + } + + void updateFrom(Collection aliases) { + MirrorSet mirrorSet = null; + int size = 0; + int last = -1; + for (int i = 0; i < attributes.size(); i++) { + Method attribute = attributes.get(i); + if (aliases.contains(attribute)) { + size++; + if (size > 1) { + if (mirrorSet == null) { + mirrorSet = new MirrorSet(); + this.assigned[last] = mirrorSet; + } + this.assigned[i] = mirrorSet; + } + last = i; + } + } + if (mirrorSet != null) { + mirrorSet.update(); + Set unique = new LinkedHashSet<>(Arrays.asList(this.assigned)); + unique.remove(null); + this.mirrorSets = unique.toArray(EMPTY_MIRROR_SETS); + } + } + + int size() { + return this.mirrorSets.length; + } + + MirrorSet get(int index) { + return this.mirrorSets[index]; + } + + @Nullable + MirrorSet getAssigned(int attributeIndex) { + return this.assigned[attributeIndex]; + } + + int[] resolve(@Nullable Object source, @Nullable Object annotation, ValueExtractor valueExtractor) { + int[] result = new int[attributes.size()]; + for (int i = 0; i < result.length; i++) { + result[i] = i; + } + for (int i = 0; i < size(); i++) { + MirrorSet mirrorSet = get(i); + int resolved = mirrorSet.resolve(source, annotation, valueExtractor); + for (int j = 0; j < mirrorSet.size; j++) { + result[mirrorSet.indexes[j]] = resolved; + } + } + return result; + } + + + /** + * A single set of mirror attributes. + */ + class MirrorSet { + + private int size; + + private final int[] indexes = new int[attributes.size()]; + + void update() { + this.size = 0; + Arrays.fill(this.indexes, -1); + for (int i = 0; i < MirrorSets.this.assigned.length; i++) { + if (MirrorSets.this.assigned[i] == this) { + this.indexes[this.size] = i; + this.size++; + } + } + } + + int resolve(@Nullable Object source, @Nullable A annotation, ValueExtractor valueExtractor) { + int result = -1; + Object lastValue = null; + for (int i = 0; i < this.size; i++) { + Method attribute = attributes.get(this.indexes[i]); + Object value = valueExtractor.extract(attribute, annotation); + boolean isDefaultValue = (value == null || + isEquivalentToDefaultValue(attribute, value, valueExtractor)); + if (isDefaultValue || ObjectUtils.nullSafeEquals(lastValue, value)) { + if (result == -1) { + result = this.indexes[i]; + } + continue; + } + if (lastValue != null && !ObjectUtils.nullSafeEquals(lastValue, value)) { + String on = (source != null) ? " declared on " + source : ""; + throw new AnnotationConfigurationException(String.format( + "Different @AliasFor mirror values for annotation [%s]%s; attribute '%s' " + + "and its alias '%s' are declared with values of [%s] and [%s].", + getAnnotationType().getName(), on, + attributes.get(result).getName(), + attribute.getName(), + ObjectUtils.nullSafeToString(lastValue), + ObjectUtils.nullSafeToString(value))); + } + result = this.indexes[i]; + lastValue = value; + } + return result; + } + + int size() { + return this.size; + } + + Method get(int index) { + int attributeIndex = this.indexes[index]; + return attributes.get(attributeIndex); + } + + int getAttributeIndex(int index) { + return this.indexes[index]; + } + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMappings.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMappings.java new file mode 100644 index 0000000..7bd5350 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMappings.java @@ -0,0 +1,249 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; +import java.util.Map; + +import org.springframework.lang.Nullable; +import org.springframework.util.ConcurrentReferenceHashMap; + +/** + * Provides {@link AnnotationTypeMapping} information for a single source + * annotation type. Performs a recursive breadth first crawl of all + * meta-annotations to ultimately provide a quick way to map the attributes of + * a root {@link Annotation}. + * + *

    Supports convention based merging of meta-annotations as well as implicit + * and explicit {@link AliasFor @AliasFor} aliases. Also provides information + * about mirrored attributes. + * + *

    This class is designed to be cached so that meta-annotations only need to + * be searched once, regardless of how many times they are actually used. + * + * @author Phillip Webb + * @since 5.2 + * @see AnnotationTypeMapping + */ +final class AnnotationTypeMappings { + + private static final IntrospectionFailureLogger failureLogger = IntrospectionFailureLogger.DEBUG; + + private static final Map standardRepeatablesCache = new ConcurrentReferenceHashMap<>(); + + private static final Map noRepeatablesCache = new ConcurrentReferenceHashMap<>(); + + + private final RepeatableContainers repeatableContainers; + + private final AnnotationFilter filter; + + private final List mappings; + + + private AnnotationTypeMappings(RepeatableContainers repeatableContainers, + AnnotationFilter filter, Class annotationType) { + + this.repeatableContainers = repeatableContainers; + this.filter = filter; + this.mappings = new ArrayList<>(); + addAllMappings(annotationType); + this.mappings.forEach(AnnotationTypeMapping::afterAllMappingsSet); + } + + + private void addAllMappings(Class annotationType) { + Deque queue = new ArrayDeque<>(); + addIfPossible(queue, null, annotationType, null); + while (!queue.isEmpty()) { + AnnotationTypeMapping mapping = queue.removeFirst(); + this.mappings.add(mapping); + addMetaAnnotationsToQueue(queue, mapping); + } + } + + private void addMetaAnnotationsToQueue(Deque queue, AnnotationTypeMapping source) { + Annotation[] metaAnnotations = AnnotationsScanner.getDeclaredAnnotations(source.getAnnotationType(), false); + for (Annotation metaAnnotation : metaAnnotations) { + if (!isMappable(source, metaAnnotation)) { + continue; + } + Annotation[] repeatedAnnotations = this.repeatableContainers.findRepeatedAnnotations(metaAnnotation); + if (repeatedAnnotations != null) { + for (Annotation repeatedAnnotation : repeatedAnnotations) { + if (!isMappable(source, repeatedAnnotation)) { + continue; + } + addIfPossible(queue, source, repeatedAnnotation); + } + } + else { + addIfPossible(queue, source, metaAnnotation); + } + } + } + + private void addIfPossible(Deque queue, AnnotationTypeMapping source, Annotation ann) { + addIfPossible(queue, source, ann.annotationType(), ann); + } + + private void addIfPossible(Deque queue, @Nullable AnnotationTypeMapping source, + Class annotationType, @Nullable Annotation ann) { + + try { + queue.addLast(new AnnotationTypeMapping(source, annotationType, ann)); + } + catch (Exception ex) { + AnnotationUtils.rethrowAnnotationConfigurationException(ex); + if (failureLogger.isEnabled()) { + failureLogger.log("Failed to introspect meta-annotation " + annotationType.getName(), + (source != null ? source.getAnnotationType() : null), ex); + } + } + } + + private boolean isMappable(AnnotationTypeMapping source, @Nullable Annotation metaAnnotation) { + return (metaAnnotation != null && !this.filter.matches(metaAnnotation) && + !AnnotationFilter.PLAIN.matches(source.getAnnotationType()) && + !isAlreadyMapped(source, metaAnnotation)); + } + + private boolean isAlreadyMapped(AnnotationTypeMapping source, Annotation metaAnnotation) { + Class annotationType = metaAnnotation.annotationType(); + AnnotationTypeMapping mapping = source; + while (mapping != null) { + if (mapping.getAnnotationType() == annotationType) { + return true; + } + mapping = mapping.getSource(); + } + return false; + } + + /** + * Get the total number of contained mappings. + * @return the total number of mappings + */ + int size() { + return this.mappings.size(); + } + + /** + * Get an individual mapping from this instance. + *

    Index {@code 0} will always return the root mapping; higher indexes + * will return meta-annotation mappings. + * @param index the index to return + * @return the {@link AnnotationTypeMapping} + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index >= size()) + */ + AnnotationTypeMapping get(int index) { + return this.mappings.get(index); + } + + + /** + * Create {@link AnnotationTypeMappings} for the specified annotation type. + * @param annotationType the source annotation type + * @return type mappings for the annotation type + */ + static AnnotationTypeMappings forAnnotationType(Class annotationType) { + return forAnnotationType(annotationType, AnnotationFilter.PLAIN); + } + + /** + * Create {@link AnnotationTypeMappings} for the specified annotation type. + * @param annotationType the source annotation type + * @param annotationFilter the annotation filter used to limit which + * annotations are considered + * @return type mappings for the annotation type + */ + static AnnotationTypeMappings forAnnotationType( + Class annotationType, AnnotationFilter annotationFilter) { + + return forAnnotationType(annotationType, RepeatableContainers.standardRepeatables(), annotationFilter); + } + + /** + * Create {@link AnnotationTypeMappings} for the specified annotation type. + * @param annotationType the source annotation type + * @param repeatableContainers the repeatable containers that may be used by + * the meta-annotations + * @param annotationFilter the annotation filter used to limit which + * annotations are considered + * @return type mappings for the annotation type + */ + static AnnotationTypeMappings forAnnotationType(Class annotationType, + RepeatableContainers repeatableContainers, AnnotationFilter annotationFilter) { + + if (repeatableContainers == RepeatableContainers.standardRepeatables()) { + return standardRepeatablesCache.computeIfAbsent(annotationFilter, + key -> new Cache(repeatableContainers, key)).get(annotationType); + } + if (repeatableContainers == RepeatableContainers.none()) { + return noRepeatablesCache.computeIfAbsent(annotationFilter, + key -> new Cache(repeatableContainers, key)).get(annotationType); + } + return new AnnotationTypeMappings(repeatableContainers, annotationFilter, annotationType); + } + + static void clearCache() { + standardRepeatablesCache.clear(); + noRepeatablesCache.clear(); + } + + + /** + * Cache created per {@link AnnotationFilter}. + */ + private static class Cache { + + private final RepeatableContainers repeatableContainers; + + private final AnnotationFilter filter; + + private final Map, AnnotationTypeMappings> mappings; + + /** + * Create a cache instance with the specified filter. + * @param filter the annotation filter + */ + Cache(RepeatableContainers repeatableContainers, AnnotationFilter filter) { + this.repeatableContainers = repeatableContainers; + this.filter = filter; + this.mappings = new ConcurrentReferenceHashMap<>(); + } + + /** + * Get or create {@link AnnotationTypeMappings} for the specified annotation type. + * @param annotationType the annotation type + * @return a new or existing {@link AnnotationTypeMappings} instance + */ + AnnotationTypeMappings get(Class annotationType) { + return this.mappings.computeIfAbsent(annotationType, this::createMappings); + } + + AnnotationTypeMappings createMappings(Class annotationType) { + return new AnnotationTypeMappings(this.repeatableContainers, this.filter, annotationType); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java new file mode 100644 index 0000000..edd0996 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java @@ -0,0 +1,1312 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Array; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; + +import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.annotation.AnnotationTypeMapping.MirrorSets.MirrorSet; +import org.springframework.core.annotation.MergedAnnotation.Adapt; +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * General utility methods for working with annotations, handling meta-annotations, + * bridge methods (which the compiler generates for generic declarations) as well + * as super methods (for optional annotation inheritance). + * + *

    Note that most of the features of this class are not provided by the + * JDK's introspection facilities themselves. + * + *

    As a general rule for runtime-retained application annotations (e.g. for + * transaction control, authorization, or service exposure), always use the + * lookup methods on this class (e.g. {@link #findAnnotation(Method, Class)} or + * {@link #getAnnotation(Method, Class)}) instead of the plain annotation lookup + * methods in the JDK. You can still explicitly choose between a get + * lookup on the given class level only ({@link #getAnnotation(Method, Class)}) + * and a find lookup in the entire inheritance hierarchy of the given + * method ({@link #findAnnotation(Method, Class)}). + * + *

    Terminology

    + * The terms directly present, indirectly present, and + * present have the same meanings as defined in the class-level + * javadoc for {@link AnnotatedElement} (in Java 8). + * + *

    An annotation is meta-present on an element if the annotation + * is declared as a meta-annotation on some other annotation which is + * present on the element. Annotation {@code A} is meta-present + * on another annotation if {@code A} is either directly present or + * meta-present on the other annotation. + * + *

    Meta-annotation Support

    + *

    Most {@code find*()} methods and some {@code get*()} methods in this class + * provide support for finding annotations used as meta-annotations. Consult the + * javadoc for each method in this class for details. For fine-grained support for + * meta-annotations with attribute overrides in composed annotations, + * consider using {@link AnnotatedElementUtils}'s more specific methods instead. + * + *

    Attribute Aliases

    + *

    All public methods in this class that return annotations, arrays of + * annotations, or {@link AnnotationAttributes} transparently support attribute + * aliases configured via {@link AliasFor @AliasFor}. Consult the various + * {@code synthesizeAnnotation*(..)} methods for details. + * + *

    Search Scope

    + *

    The search algorithms used by methods in this class stop searching for + * an annotation once the first annotation of the specified type has been + * found. As a consequence, additional annotations of the specified type will + * be silently ignored. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Sam Brannen + * @author Mark Fisher + * @author Chris Beams + * @author Phillip Webb + * @author Oleg Zhurakousky + * @since 2.0 + * @see AliasFor + * @see AnnotationAttributes + * @see AnnotatedElementUtils + * @see BridgeMethodResolver + * @see java.lang.reflect.AnnotatedElement#getAnnotations() + * @see java.lang.reflect.AnnotatedElement#getAnnotation(Class) + * @see java.lang.reflect.AnnotatedElement#getDeclaredAnnotations() + */ +public abstract class AnnotationUtils { + + /** + * The attribute name for annotations with a single element. + */ + public static final String VALUE = MergedAnnotation.VALUE; + + private static final AnnotationFilter JAVA_LANG_ANNOTATION_FILTER = + AnnotationFilter.packages("java.lang.annotation"); + + private static final Map, Map> defaultValuesCache = + new ConcurrentReferenceHashMap<>(); + + + /** + * Determine whether the given class is a candidate for carrying one of the specified + * annotations (at type, method or field level). + * @param clazz the class to introspect + * @param annotationTypes the searchable annotation types + * @return {@code false} if the class is known to have no such annotations at any level; + * {@code true} otherwise. Callers will usually perform full method/field introspection + * if {@code true} is being returned here. + * @since 5.2 + * @see #isCandidateClass(Class, Class) + * @see #isCandidateClass(Class, String) + */ + public static boolean isCandidateClass(Class clazz, Collection> annotationTypes) { + for (Class annotationType : annotationTypes) { + if (isCandidateClass(clazz, annotationType)) { + return true; + } + } + return false; + } + + /** + * Determine whether the given class is a candidate for carrying the specified annotation + * (at type, method or field level). + * @param clazz the class to introspect + * @param annotationType the searchable annotation type + * @return {@code false} if the class is known to have no such annotations at any level; + * {@code true} otherwise. Callers will usually perform full method/field introspection + * if {@code true} is being returned here. + * @since 5.2 + * @see #isCandidateClass(Class, String) + */ + public static boolean isCandidateClass(Class clazz, Class annotationType) { + return isCandidateClass(clazz, annotationType.getName()); + } + + /** + * Determine whether the given class is a candidate for carrying the specified annotation + * (at type, method or field level). + * @param clazz the class to introspect + * @param annotationName the fully-qualified name of the searchable annotation type + * @return {@code false} if the class is known to have no such annotations at any level; + * {@code true} otherwise. Callers will usually perform full method/field introspection + * if {@code true} is being returned here. + * @since 5.2 + * @see #isCandidateClass(Class, Class) + */ + public static boolean isCandidateClass(Class clazz, String annotationName) { + if (annotationName.startsWith("java.")) { + return true; + } + if (AnnotationsScanner.hasPlainJavaAnnotationsOnly(clazz)) { + return false; + } + return true; + } + + /** + * Get a single {@link Annotation} of {@code annotationType} from the supplied + * annotation: either the given annotation itself or a direct meta-annotation + * thereof. + *

    Note that this method supports only a single level of meta-annotations. + * For support for arbitrary levels of meta-annotations, use one of the + * {@code find*()} methods instead. + * @param annotation the Annotation to check + * @param annotationType the annotation type to look for, both locally and as a meta-annotation + * @return the first matching annotation, or {@code null} if not found + * @since 4.0 + */ + @SuppressWarnings("unchecked") + @Nullable + public static A getAnnotation(Annotation annotation, Class annotationType) { + // Shortcut: directly present on the element, with no merging needed? + if (annotationType.isInstance(annotation)) { + return synthesizeAnnotation((A) annotation, annotationType); + } + // Shortcut: no searchable annotations to be found on plain Java classes and core Spring types... + if (AnnotationsScanner.hasPlainJavaAnnotationsOnly(annotation)) { + return null; + } + // Exhaustive retrieval of merged annotations... + return MergedAnnotations.from(annotation, new Annotation[] {annotation}, RepeatableContainers.none()) + .get(annotationType).withNonMergedAttributes() + .synthesize(AnnotationUtils::isSingleLevelPresent).orElse(null); + } + + /** + * Get a single {@link Annotation} of {@code annotationType} from the supplied + * {@link AnnotatedElement}, where the annotation is either present or + * meta-present on the {@code AnnotatedElement}. + *

    Note that this method supports only a single level of meta-annotations. + * For support for arbitrary levels of meta-annotations, use + * {@link #findAnnotation(AnnotatedElement, Class)} instead. + * @param annotatedElement the {@code AnnotatedElement} from which to get the annotation + * @param annotationType the annotation type to look for, both locally and as a meta-annotation + * @return the first matching annotation, or {@code null} if not found + * @since 3.1 + */ + @Nullable + public static A getAnnotation(AnnotatedElement annotatedElement, Class annotationType) { + // Shortcut: directly present on the element, with no merging needed? + if (AnnotationFilter.PLAIN.matches(annotationType) || + AnnotationsScanner.hasPlainJavaAnnotationsOnly(annotatedElement)) { + return annotatedElement.getAnnotation(annotationType); + } + // Exhaustive retrieval of merged annotations... + return MergedAnnotations.from(annotatedElement, SearchStrategy.INHERITED_ANNOTATIONS, RepeatableContainers.none()) + .get(annotationType).withNonMergedAttributes() + .synthesize(AnnotationUtils::isSingleLevelPresent).orElse(null); + } + + private static boolean isSingleLevelPresent(MergedAnnotation mergedAnnotation) { + int distance = mergedAnnotation.getDistance(); + return (distance == 0 || distance == 1); + } + + /** + * Get a single {@link Annotation} of {@code annotationType} from the + * supplied {@link Method}, where the annotation is either present + * or meta-present on the method. + *

    Correctly handles bridge {@link Method Methods} generated by the compiler. + *

    Note that this method supports only a single level of meta-annotations. + * For support for arbitrary levels of meta-annotations, use + * {@link #findAnnotation(Method, Class)} instead. + * @param method the method to look for annotations on + * @param annotationType the annotation type to look for + * @return the first matching annotation, or {@code null} if not found + * @see org.springframework.core.BridgeMethodResolver#findBridgedMethod(Method) + * @see #getAnnotation(AnnotatedElement, Class) + */ + @Nullable + public static A getAnnotation(Method method, Class annotationType) { + Method resolvedMethod = BridgeMethodResolver.findBridgedMethod(method); + return getAnnotation((AnnotatedElement) resolvedMethod, annotationType); + } + + /** + * Get all {@link Annotation Annotations} that are present on the + * supplied {@link AnnotatedElement}. + *

    Meta-annotations will not be searched. + * @param annotatedElement the Method, Constructor or Field to retrieve annotations from + * @return the annotations found, an empty array, or {@code null} if not + * resolvable (e.g. because nested Class values in annotation attributes + * failed to resolve at runtime) + * @since 4.0.8 + * @see AnnotatedElement#getAnnotations() + * @deprecated as of 5.2 since it is superseded by the {@link MergedAnnotations} API + */ + @Deprecated + @Nullable + public static Annotation[] getAnnotations(AnnotatedElement annotatedElement) { + try { + return synthesizeAnnotationArray(annotatedElement.getAnnotations(), annotatedElement); + } + catch (Throwable ex) { + handleIntrospectionFailure(annotatedElement, ex); + return null; + } + } + + /** + * Get all {@link Annotation Annotations} that are present on the + * supplied {@link Method}. + *

    Correctly handles bridge {@link Method Methods} generated by the compiler. + *

    Meta-annotations will not be searched. + * @param method the Method to retrieve annotations from + * @return the annotations found, an empty array, or {@code null} if not + * resolvable (e.g. because nested Class values in annotation attributes + * failed to resolve at runtime) + * @see org.springframework.core.BridgeMethodResolver#findBridgedMethod(Method) + * @see AnnotatedElement#getAnnotations() + * @deprecated as of 5.2 since it is superseded by the {@link MergedAnnotations} API + */ + @Deprecated + @Nullable + public static Annotation[] getAnnotations(Method method) { + try { + return synthesizeAnnotationArray(BridgeMethodResolver.findBridgedMethod(method).getAnnotations(), method); + } + catch (Throwable ex) { + handleIntrospectionFailure(method, ex); + return null; + } + } + + /** + * Get the repeatable {@linkplain Annotation annotations} of + * {@code annotationType} from the supplied {@link AnnotatedElement}, where + * such annotations are either present, indirectly present, + * or meta-present on the element. + *

    This method mimics the functionality of Java 8's + * {@link java.lang.reflect.AnnotatedElement#getAnnotationsByType(Class)} + * with support for automatic detection of a container annotation + * declared via @{@link java.lang.annotation.Repeatable} (when running on + * Java 8 or higher) and with additional support for meta-annotations. + *

    Handles both single annotations and annotations nested within a + * container annotation. + *

    Correctly handles bridge methods generated by the + * compiler if the supplied element is a {@link Method}. + *

    Meta-annotations will be searched if the annotation is not + * present on the supplied element. + * @param annotatedElement the element to look for annotations on + * @param annotationType the annotation type to look for + * @return the annotations found or an empty set (never {@code null}) + * @since 4.2 + * @see #getRepeatableAnnotations(AnnotatedElement, Class, Class) + * @see #getDeclaredRepeatableAnnotations(AnnotatedElement, Class, Class) + * @see AnnotatedElementUtils#getMergedRepeatableAnnotations(AnnotatedElement, Class) + * @see org.springframework.core.BridgeMethodResolver#findBridgedMethod + * @see java.lang.annotation.Repeatable + * @see java.lang.reflect.AnnotatedElement#getAnnotationsByType + * @deprecated as of 5.2 since it is superseded by the {@link MergedAnnotations} API + */ + @Deprecated + public static Set getRepeatableAnnotations(AnnotatedElement annotatedElement, + Class annotationType) { + + return getRepeatableAnnotations(annotatedElement, annotationType, null); + } + + /** + * Get the repeatable {@linkplain Annotation annotations} of + * {@code annotationType} from the supplied {@link AnnotatedElement}, where + * such annotations are either present, indirectly present, + * or meta-present on the element. + *

    This method mimics the functionality of Java 8's + * {@link java.lang.reflect.AnnotatedElement#getAnnotationsByType(Class)} + * with additional support for meta-annotations. + *

    Handles both single annotations and annotations nested within a + * container annotation. + *

    Correctly handles bridge methods generated by the + * compiler if the supplied element is a {@link Method}. + *

    Meta-annotations will be searched if the annotation is not + * present on the supplied element. + * @param annotatedElement the element to look for annotations on + * @param annotationType the annotation type to look for + * @param containerAnnotationType the type of the container that holds + * the annotations; may be {@code null} if a container is not supported + * or if it should be looked up via @{@link java.lang.annotation.Repeatable} + * when running on Java 8 or higher + * @return the annotations found or an empty set (never {@code null}) + * @since 4.2 + * @see #getRepeatableAnnotations(AnnotatedElement, Class) + * @see #getDeclaredRepeatableAnnotations(AnnotatedElement, Class) + * @see #getDeclaredRepeatableAnnotations(AnnotatedElement, Class, Class) + * @see AnnotatedElementUtils#getMergedRepeatableAnnotations(AnnotatedElement, Class, Class) + * @see org.springframework.core.BridgeMethodResolver#findBridgedMethod + * @see java.lang.annotation.Repeatable + * @see java.lang.reflect.AnnotatedElement#getAnnotationsByType + * @deprecated as of 5.2 since it is superseded by the {@link MergedAnnotations} API + */ + @Deprecated + public static Set getRepeatableAnnotations(AnnotatedElement annotatedElement, + Class annotationType, @Nullable Class containerAnnotationType) { + + RepeatableContainers repeatableContainers = (containerAnnotationType != null ? + RepeatableContainers.of(annotationType, containerAnnotationType) : + RepeatableContainers.standardRepeatables()); + + return MergedAnnotations.from(annotatedElement, SearchStrategy.SUPERCLASS, repeatableContainers) + .stream(annotationType) + .filter(MergedAnnotationPredicates.firstRunOf(MergedAnnotation::getAggregateIndex)) + .map(MergedAnnotation::withNonMergedAttributes) + .collect(MergedAnnotationCollectors.toAnnotationSet()); + } + + /** + * Get the declared repeatable {@linkplain Annotation annotations} + * of {@code annotationType} from the supplied {@link AnnotatedElement}, + * where such annotations are either directly present, + * indirectly present, or meta-present on the element. + *

    This method mimics the functionality of Java 8's + * {@link java.lang.reflect.AnnotatedElement#getDeclaredAnnotationsByType(Class)} + * with support for automatic detection of a container annotation + * declared via @{@link java.lang.annotation.Repeatable} (when running on + * Java 8 or higher) and with additional support for meta-annotations. + *

    Handles both single annotations and annotations nested within a + * container annotation. + *

    Correctly handles bridge methods generated by the + * compiler if the supplied element is a {@link Method}. + *

    Meta-annotations will be searched if the annotation is not + * present on the supplied element. + * @param annotatedElement the element to look for annotations on + * @param annotationType the annotation type to look for + * @return the annotations found or an empty set (never {@code null}) + * @since 4.2 + * @see #getRepeatableAnnotations(AnnotatedElement, Class) + * @see #getRepeatableAnnotations(AnnotatedElement, Class, Class) + * @see #getDeclaredRepeatableAnnotations(AnnotatedElement, Class, Class) + * @see AnnotatedElementUtils#getMergedRepeatableAnnotations(AnnotatedElement, Class) + * @see org.springframework.core.BridgeMethodResolver#findBridgedMethod + * @see java.lang.annotation.Repeatable + * @see java.lang.reflect.AnnotatedElement#getDeclaredAnnotationsByType + * @deprecated as of 5.2 since it is superseded by the {@link MergedAnnotations} API + */ + @Deprecated + public static Set getDeclaredRepeatableAnnotations(AnnotatedElement annotatedElement, + Class annotationType) { + + return getDeclaredRepeatableAnnotations(annotatedElement, annotationType, null); + } + + /** + * Get the declared repeatable {@linkplain Annotation annotations} + * of {@code annotationType} from the supplied {@link AnnotatedElement}, + * where such annotations are either directly present, + * indirectly present, or meta-present on the element. + *

    This method mimics the functionality of Java 8's + * {@link java.lang.reflect.AnnotatedElement#getDeclaredAnnotationsByType(Class)} + * with additional support for meta-annotations. + *

    Handles both single annotations and annotations nested within a + * container annotation. + *

    Correctly handles bridge methods generated by the + * compiler if the supplied element is a {@link Method}. + *

    Meta-annotations will be searched if the annotation is not + * present on the supplied element. + * @param annotatedElement the element to look for annotations on + * @param annotationType the annotation type to look for + * @param containerAnnotationType the type of the container that holds + * the annotations; may be {@code null} if a container is not supported + * or if it should be looked up via @{@link java.lang.annotation.Repeatable} + * when running on Java 8 or higher + * @return the annotations found or an empty set (never {@code null}) + * @since 4.2 + * @see #getRepeatableAnnotations(AnnotatedElement, Class) + * @see #getRepeatableAnnotations(AnnotatedElement, Class, Class) + * @see #getDeclaredRepeatableAnnotations(AnnotatedElement, Class) + * @see AnnotatedElementUtils#getMergedRepeatableAnnotations(AnnotatedElement, Class, Class) + * @see org.springframework.core.BridgeMethodResolver#findBridgedMethod + * @see java.lang.annotation.Repeatable + * @see java.lang.reflect.AnnotatedElement#getDeclaredAnnotationsByType + * @deprecated as of 5.2 since it is superseded by the {@link MergedAnnotations} API + */ + @Deprecated + public static Set getDeclaredRepeatableAnnotations(AnnotatedElement annotatedElement, + Class annotationType, @Nullable Class containerAnnotationType) { + + RepeatableContainers repeatableContainers = containerAnnotationType != null ? + RepeatableContainers.of(annotationType, containerAnnotationType) : + RepeatableContainers.standardRepeatables(); + + return MergedAnnotations.from(annotatedElement, SearchStrategy.DIRECT, repeatableContainers) + .stream(annotationType) + .map(MergedAnnotation::withNonMergedAttributes) + .collect(MergedAnnotationCollectors.toAnnotationSet()); + } + + /** + * Find a single {@link Annotation} of {@code annotationType} on the + * supplied {@link AnnotatedElement}. + *

    Meta-annotations will be searched if the annotation is not + * directly present on the supplied element. + *

    Warning: this method operates generically on + * annotated elements. In other words, this method does not execute + * specialized search algorithms for classes or methods. If you require + * the more specific semantics of {@link #findAnnotation(Class, Class)} + * or {@link #findAnnotation(Method, Class)}, invoke one of those methods + * instead. + * @param annotatedElement the {@code AnnotatedElement} on which to find the annotation + * @param annotationType the annotation type to look for, both locally and as a meta-annotation + * @return the first matching annotation, or {@code null} if not found + * @since 4.2 + */ + @Nullable + public static A findAnnotation( + AnnotatedElement annotatedElement, @Nullable Class annotationType) { + + if (annotationType == null) { + return null; + } + + // Shortcut: directly present on the element, with no merging needed? + if (AnnotationFilter.PLAIN.matches(annotationType) || + AnnotationsScanner.hasPlainJavaAnnotationsOnly(annotatedElement)) { + return annotatedElement.getDeclaredAnnotation(annotationType); + } + + // Exhaustive retrieval of merged annotations... + return MergedAnnotations.from(annotatedElement, SearchStrategy.INHERITED_ANNOTATIONS, RepeatableContainers.none()) + .get(annotationType).withNonMergedAttributes() + .synthesize(MergedAnnotation::isPresent).orElse(null); + } + + /** + * Find a single {@link Annotation} of {@code annotationType} on the supplied + * {@link Method}, traversing its super methods (i.e. from superclasses and + * interfaces) if the annotation is not directly present on the given + * method itself. + *

    Correctly handles bridge {@link Method Methods} generated by the compiler. + *

    Meta-annotations will be searched if the annotation is not + * directly present on the method. + *

    Annotations on methods are not inherited by default, so we need to handle + * this explicitly. + * @param method the method to look for annotations on + * @param annotationType the annotation type to look for + * @return the first matching annotation, or {@code null} if not found + * @see #getAnnotation(Method, Class) + */ + @Nullable + public static A findAnnotation(Method method, @Nullable Class annotationType) { + if (annotationType == null) { + return null; + } + + // Shortcut: directly present on the element, with no merging needed? + if (AnnotationFilter.PLAIN.matches(annotationType) || + AnnotationsScanner.hasPlainJavaAnnotationsOnly(method)) { + return method.getDeclaredAnnotation(annotationType); + } + + // Exhaustive retrieval of merged annotations... + return MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY, RepeatableContainers.none()) + .get(annotationType).withNonMergedAttributes() + .synthesize(MergedAnnotation::isPresent).orElse(null); + } + + /** + * Find a single {@link Annotation} of {@code annotationType} on the + * supplied {@link Class}, traversing its interfaces, annotations, and + * superclasses if the annotation is not directly present on + * the given class itself. + *

    This method explicitly handles class-level annotations which are not + * declared as {@link java.lang.annotation.Inherited inherited} as well + * as meta-annotations and annotations on interfaces. + *

    The algorithm operates as follows: + *

      + *
    1. Search for the annotation on the given class and return it if found. + *
    2. Recursively search through all annotations that the given class declares. + *
    3. Recursively search through all interfaces that the given class declares. + *
    4. Recursively search through the superclass hierarchy of the given class. + *
    + *

    Note: in this context, the term recursively means that the search + * process continues by returning to step #1 with the current interface, + * annotation, or superclass as the class to look for annotations on. + * @param clazz the class to look for annotations on + * @param annotationType the type of annotation to look for + * @return the first matching annotation, or {@code null} if not found + */ + @Nullable + public static A findAnnotation(Class clazz, @Nullable Class annotationType) { + if (annotationType == null) { + return null; + } + + // Shortcut: directly present on the element, with no merging needed? + if (AnnotationFilter.PLAIN.matches(annotationType) || + AnnotationsScanner.hasPlainJavaAnnotationsOnly(clazz)) { + A annotation = clazz.getDeclaredAnnotation(annotationType); + if (annotation != null) { + return annotation; + } + // For backwards compatibility, perform a superclass search with plain annotations + // even if not marked as @Inherited: e.g. a findAnnotation search for @Deprecated + Class superclass = clazz.getSuperclass(); + if (superclass == null || superclass == Object.class) { + return null; + } + return findAnnotation(superclass, annotationType); + } + + // Exhaustive retrieval of merged annotations... + return MergedAnnotations.from(clazz, SearchStrategy.TYPE_HIERARCHY, RepeatableContainers.none()) + .get(annotationType).withNonMergedAttributes() + .synthesize(MergedAnnotation::isPresent).orElse(null); + } + + /** + * Find the first {@link Class} in the inheritance hierarchy of the + * specified {@code clazz} (including the specified {@code clazz} itself) + * on which an annotation of the specified {@code annotationType} is + * directly present. + *

    If the supplied {@code clazz} is an interface, only the interface + * itself will be checked; the inheritance hierarchy for interfaces will + * not be traversed. + *

    Meta-annotations will not be searched. + *

    The standard {@link Class} API does not provide a mechanism for + * determining which class in an inheritance hierarchy actually declares + * an {@link Annotation}, so we need to handle this explicitly. + * @param annotationType the annotation type to look for + * @param clazz the class to check for the annotation on (may be {@code null}) + * @return the first {@link Class} in the inheritance hierarchy that + * declares an annotation of the specified {@code annotationType}, + * or {@code null} if not found + * @see Class#isAnnotationPresent(Class) + * @see Class#getDeclaredAnnotations() + * @deprecated as of 5.2 since it is superseded by the {@link MergedAnnotations} API + */ + @Deprecated + @Nullable + public static Class findAnnotationDeclaringClass( + Class annotationType, @Nullable Class clazz) { + + if (clazz == null) { + return null; + } + + return (Class) MergedAnnotations.from(clazz, SearchStrategy.SUPERCLASS) + .get(annotationType, MergedAnnotation::isDirectlyPresent) + .getSource(); + } + + /** + * Find the first {@link Class} in the inheritance hierarchy of the + * specified {@code clazz} (including the specified {@code clazz} itself) + * on which at least one of the specified {@code annotationTypes} is + * directly present. + *

    If the supplied {@code clazz} is an interface, only the interface + * itself will be checked; the inheritance hierarchy for interfaces will + * not be traversed. + *

    Meta-annotations will not be searched. + *

    The standard {@link Class} API does not provide a mechanism for + * determining which class in an inheritance hierarchy actually declares + * one of several candidate {@linkplain Annotation annotations}, so we + * need to handle this explicitly. + * @param annotationTypes the annotation types to look for + * @param clazz the class to check for the annotation on (may be {@code null}) + * @return the first {@link Class} in the inheritance hierarchy that + * declares an annotation of at least one of the specified + * {@code annotationTypes}, or {@code null} if not found + * @since 3.2.2 + * @see Class#isAnnotationPresent(Class) + * @see Class#getDeclaredAnnotations() + * @deprecated as of 5.2 since it is superseded by the {@link MergedAnnotations} API + */ + @Deprecated + @Nullable + public static Class findAnnotationDeclaringClassForTypes( + List> annotationTypes, @Nullable Class clazz) { + + if (clazz == null) { + return null; + } + + return (Class) MergedAnnotations.from(clazz, SearchStrategy.SUPERCLASS) + .stream() + .filter(MergedAnnotationPredicates.typeIn(annotationTypes).and(MergedAnnotation::isDirectlyPresent)) + .map(MergedAnnotation::getSource) + .findFirst().orElse(null); + } + + /** + * Determine whether an annotation of the specified {@code annotationType} + * is declared locally (i.e. directly present) on the supplied + * {@code clazz}. + *

    The supplied {@link Class} may represent any type. + *

    Meta-annotations will not be searched. + *

    Note: This method does not determine if the annotation + * is {@linkplain java.lang.annotation.Inherited inherited}. + * @param annotationType the annotation type to look for + * @param clazz the class to check for the annotation on + * @return {@code true} if an annotation of the specified {@code annotationType} + * is directly present + * @see java.lang.Class#getDeclaredAnnotations() + * @see java.lang.Class#getDeclaredAnnotation(Class) + */ + public static boolean isAnnotationDeclaredLocally(Class annotationType, Class clazz) { + return MergedAnnotations.from(clazz).get(annotationType).isDirectlyPresent(); + } + + /** + * Determine whether an annotation of the specified {@code annotationType} + * is present on the supplied {@code clazz} and is + * {@linkplain java.lang.annotation.Inherited inherited} + * (i.e. not directly present). + *

    Meta-annotations will not be searched. + *

    If the supplied {@code clazz} is an interface, only the interface + * itself will be checked. In accordance with standard meta-annotation + * semantics in Java, the inheritance hierarchy for interfaces will not + * be traversed. See the {@linkplain java.lang.annotation.Inherited javadoc} + * for the {@code @Inherited} meta-annotation for further details regarding + * annotation inheritance. + * @param annotationType the annotation type to look for + * @param clazz the class to check for the annotation on + * @return {@code true} if an annotation of the specified {@code annotationType} + * is present and inherited + * @see Class#isAnnotationPresent(Class) + * @see #isAnnotationDeclaredLocally(Class, Class) + * @deprecated as of 5.2 since it is superseded by the {@link MergedAnnotations} API + */ + @Deprecated + public static boolean isAnnotationInherited(Class annotationType, Class clazz) { + return MergedAnnotations.from(clazz, SearchStrategy.INHERITED_ANNOTATIONS) + .stream(annotationType) + .filter(MergedAnnotation::isDirectlyPresent) + .findFirst().orElseGet(MergedAnnotation::missing) + .getAggregateIndex() > 0; + } + + /** + * Determine if an annotation of type {@code metaAnnotationType} is + * meta-present on the supplied {@code annotationType}. + * @param annotationType the annotation type to search on + * @param metaAnnotationType the type of meta-annotation to search for + * @return {@code true} if such an annotation is meta-present + * @since 4.2.1 + * @deprecated as of 5.2 since it is superseded by the {@link MergedAnnotations} API + */ + @Deprecated + public static boolean isAnnotationMetaPresent(Class annotationType, + @Nullable Class metaAnnotationType) { + + if (metaAnnotationType == null) { + return false; + } + // Shortcut: directly present on the element, with no merging needed? + if (AnnotationFilter.PLAIN.matches(metaAnnotationType) || + AnnotationsScanner.hasPlainJavaAnnotationsOnly(annotationType)) { + return annotationType.isAnnotationPresent(metaAnnotationType); + } + // Exhaustive retrieval of merged annotations... + return MergedAnnotations.from(annotationType, SearchStrategy.INHERITED_ANNOTATIONS, + RepeatableContainers.none()).isPresent(metaAnnotationType); + } + + /** + * Determine if the supplied {@link Annotation} is defined in the core JDK + * {@code java.lang.annotation} package. + * @param annotation the annotation to check + * @return {@code true} if the annotation is in the {@code java.lang.annotation} package + */ + public static boolean isInJavaLangAnnotationPackage(@Nullable Annotation annotation) { + return (annotation != null && JAVA_LANG_ANNOTATION_FILTER.matches(annotation)); + } + + /** + * Determine if the {@link Annotation} with the supplied name is defined + * in the core JDK {@code java.lang.annotation} package. + * @param annotationType the name of the annotation type to check + * @return {@code true} if the annotation is in the {@code java.lang.annotation} package + * @since 4.2 + */ + public static boolean isInJavaLangAnnotationPackage(@Nullable String annotationType) { + return (annotationType != null && JAVA_LANG_ANNOTATION_FILTER.matches(annotationType)); + } + + /** + * Check the declared attributes of the given annotation, in particular covering + * Google App Engine's late arrival of {@code TypeNotPresentExceptionProxy} for + * {@code Class} values (instead of early {@code Class.getAnnotations() failure}. + *

    This method not failing indicates that {@link #getAnnotationAttributes(Annotation)} + * won't failure either (when attempted later on). + * @param annotation the annotation to validate + * @throws IllegalStateException if a declared {@code Class} attribute could not be read + * @since 4.3.15 + * @see Class#getAnnotations() + * @see #getAnnotationAttributes(Annotation) + */ + public static void validateAnnotation(Annotation annotation) { + AttributeMethods.forAnnotationType(annotation.annotationType()).validate(annotation); + } + + /** + * Retrieve the given annotation's attributes as a {@link Map}, preserving all + * attribute types. + *

    Equivalent to calling {@link #getAnnotationAttributes(Annotation, boolean, boolean)} + * with the {@code classValuesAsString} and {@code nestedAnnotationsAsMap} parameters + * set to {@code false}. + *

    Note: This method actually returns an {@link AnnotationAttributes} instance. + * However, the {@code Map} signature has been preserved for binary compatibility. + * @param annotation the annotation to retrieve the attributes for + * @return the Map of annotation attributes, with attribute names as keys and + * corresponding attribute values as values (never {@code null}) + * @see #getAnnotationAttributes(AnnotatedElement, Annotation) + * @see #getAnnotationAttributes(Annotation, boolean, boolean) + * @see #getAnnotationAttributes(AnnotatedElement, Annotation, boolean, boolean) + */ + public static Map getAnnotationAttributes(Annotation annotation) { + return getAnnotationAttributes(null, annotation); + } + + /** + * Retrieve the given annotation's attributes as a {@link Map}. + *

    Equivalent to calling {@link #getAnnotationAttributes(Annotation, boolean, boolean)} + * with the {@code nestedAnnotationsAsMap} parameter set to {@code false}. + *

    Note: This method actually returns an {@link AnnotationAttributes} instance. + * However, the {@code Map} signature has been preserved for binary compatibility. + * @param annotation the annotation to retrieve the attributes for + * @param classValuesAsString whether to convert Class references into Strings (for + * compatibility with {@link org.springframework.core.type.AnnotationMetadata}) + * or to preserve them as Class references + * @return the Map of annotation attributes, with attribute names as keys and + * corresponding attribute values as values (never {@code null}) + * @see #getAnnotationAttributes(Annotation, boolean, boolean) + */ + public static Map getAnnotationAttributes( + Annotation annotation, boolean classValuesAsString) { + + return getAnnotationAttributes(annotation, classValuesAsString, false); + } + + /** + * Retrieve the given annotation's attributes as an {@link AnnotationAttributes} map. + *

    This method provides fully recursive annotation reading capabilities on par with + * the reflection-based {@link org.springframework.core.type.StandardAnnotationMetadata}. + * @param annotation the annotation to retrieve the attributes for + * @param classValuesAsString whether to convert Class references into Strings (for + * compatibility with {@link org.springframework.core.type.AnnotationMetadata}) + * or to preserve them as Class references + * @param nestedAnnotationsAsMap whether to convert nested annotations into + * {@link AnnotationAttributes} maps (for compatibility with + * {@link org.springframework.core.type.AnnotationMetadata}) or to preserve them as + * {@code Annotation} instances + * @return the annotation attributes (a specialized Map) with attribute names as keys + * and corresponding attribute values as values (never {@code null}) + * @since 3.1.1 + */ + public static AnnotationAttributes getAnnotationAttributes( + Annotation annotation, boolean classValuesAsString, boolean nestedAnnotationsAsMap) { + + return getAnnotationAttributes(null, annotation, classValuesAsString, nestedAnnotationsAsMap); + } + + /** + * Retrieve the given annotation's attributes as an {@link AnnotationAttributes} map. + *

    Equivalent to calling {@link #getAnnotationAttributes(AnnotatedElement, Annotation, boolean, boolean)} + * with the {@code classValuesAsString} and {@code nestedAnnotationsAsMap} parameters + * set to {@code false}. + * @param annotatedElement the element that is annotated with the supplied annotation; + * may be {@code null} if unknown + * @param annotation the annotation to retrieve the attributes for + * @return the annotation attributes (a specialized Map) with attribute names as keys + * and corresponding attribute values as values (never {@code null}) + * @since 4.2 + * @see #getAnnotationAttributes(AnnotatedElement, Annotation, boolean, boolean) + */ + public static AnnotationAttributes getAnnotationAttributes( + @Nullable AnnotatedElement annotatedElement, Annotation annotation) { + + return getAnnotationAttributes(annotatedElement, annotation, false, false); + } + + /** + * Retrieve the given annotation's attributes as an {@link AnnotationAttributes} map. + *

    This method provides fully recursive annotation reading capabilities on par with + * the reflection-based {@link org.springframework.core.type.StandardAnnotationMetadata}. + * @param annotatedElement the element that is annotated with the supplied annotation; + * may be {@code null} if unknown + * @param annotation the annotation to retrieve the attributes for + * @param classValuesAsString whether to convert Class references into Strings (for + * compatibility with {@link org.springframework.core.type.AnnotationMetadata}) + * or to preserve them as Class references + * @param nestedAnnotationsAsMap whether to convert nested annotations into + * {@link AnnotationAttributes} maps (for compatibility with + * {@link org.springframework.core.type.AnnotationMetadata}) or to preserve them as + * {@code Annotation} instances + * @return the annotation attributes (a specialized Map) with attribute names as keys + * and corresponding attribute values as values (never {@code null}) + * @since 4.2 + */ + public static AnnotationAttributes getAnnotationAttributes( + @Nullable AnnotatedElement annotatedElement, Annotation annotation, + boolean classValuesAsString, boolean nestedAnnotationsAsMap) { + + Adapt[] adaptations = Adapt.values(classValuesAsString, nestedAnnotationsAsMap); + return MergedAnnotation.from(annotatedElement, annotation) + .withNonMergedAttributes() + .asMap(mergedAnnotation -> + new AnnotationAttributes(mergedAnnotation.getType(), true), adaptations); + } + + /** + * Register the annotation-declared default values for the given attributes, + * if available. + * @param attributes the annotation attributes to process + * @since 4.3.2 + */ + public static void registerDefaultValues(AnnotationAttributes attributes) { + Class annotationType = attributes.annotationType(); + if (annotationType != null && Modifier.isPublic(annotationType.getModifiers()) && + !AnnotationFilter.PLAIN.matches(annotationType)) { + Map defaultValues = getDefaultValues(annotationType); + defaultValues.forEach(attributes::putIfAbsent); + } + } + + private static Map getDefaultValues( + Class annotationType) { + + return defaultValuesCache.computeIfAbsent(annotationType, + AnnotationUtils::computeDefaultValues); + } + + private static Map computeDefaultValues( + Class annotationType) { + + AttributeMethods methods = AttributeMethods.forAnnotationType(annotationType); + if (!methods.hasDefaultValueMethod()) { + return Collections.emptyMap(); + } + Map result = CollectionUtils.newLinkedHashMap(methods.size()); + if (!methods.hasNestedAnnotation()) { + // Use simpler method if there are no nested annotations + for (int i = 0; i < methods.size(); i++) { + Method method = methods.get(i); + Object defaultValue = method.getDefaultValue(); + if (defaultValue != null) { + result.put(method.getName(), new DefaultValueHolder(defaultValue)); + } + } + } + else { + // If we have nested annotations, we need them as nested maps + AnnotationAttributes attributes = MergedAnnotation.of(annotationType) + .asMap(annotation -> + new AnnotationAttributes(annotation.getType(), true), Adapt.ANNOTATION_TO_MAP); + for (Map.Entry element : attributes.entrySet()) { + result.put(element.getKey(), new DefaultValueHolder(element.getValue())); + } + } + return result; + } + + /** + * Post-process the supplied {@link AnnotationAttributes}, preserving nested + * annotations as {@code Annotation} instances. + *

    Specifically, this method enforces attribute alias semantics + * for annotation attributes that are annotated with {@link AliasFor @AliasFor} + * and replaces default value placeholders with their original default values. + * @param annotatedElement the element that is annotated with an annotation or + * annotation hierarchy from which the supplied attributes were created; + * may be {@code null} if unknown + * @param attributes the annotation attributes to post-process + * @param classValuesAsString whether to convert Class references into Strings (for + * compatibility with {@link org.springframework.core.type.AnnotationMetadata}) + * or to preserve them as Class references + * @since 4.3.2 + * @see #getDefaultValue(Class, String) + */ + public static void postProcessAnnotationAttributes(@Nullable Object annotatedElement, + @Nullable AnnotationAttributes attributes, boolean classValuesAsString) { + + if (attributes == null) { + return; + } + if (!attributes.validated) { + Class annotationType = attributes.annotationType(); + if (annotationType == null) { + return; + } + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(annotationType).get(0); + for (int i = 0; i < mapping.getMirrorSets().size(); i++) { + MirrorSet mirrorSet = mapping.getMirrorSets().get(i); + int resolved = mirrorSet.resolve(attributes.displayName, attributes, + AnnotationUtils::getAttributeValueForMirrorResolution); + if (resolved != -1) { + Method attribute = mapping.getAttributes().get(resolved); + Object value = attributes.get(attribute.getName()); + for (int j = 0; j < mirrorSet.size(); j++) { + Method mirror = mirrorSet.get(j); + if (mirror != attribute) { + attributes.put(mirror.getName(), + adaptValue(annotatedElement, value, classValuesAsString)); + } + } + } + } + } + for (Map.Entry attributeEntry : attributes.entrySet()) { + String attributeName = attributeEntry.getKey(); + Object value = attributeEntry.getValue(); + if (value instanceof DefaultValueHolder) { + value = ((DefaultValueHolder) value).defaultValue; + attributes.put(attributeName, + adaptValue(annotatedElement, value, classValuesAsString)); + } + } + } + + private static Object getAttributeValueForMirrorResolution(Method attribute, Object attributes) { + Object result = ((AnnotationAttributes) attributes).get(attribute.getName()); + return (result instanceof DefaultValueHolder ? ((DefaultValueHolder) result).defaultValue : result); + } + + @Nullable + private static Object adaptValue( + @Nullable Object annotatedElement, @Nullable Object value, boolean classValuesAsString) { + + if (classValuesAsString) { + if (value instanceof Class) { + return ((Class) value).getName(); + } + if (value instanceof Class[]) { + Class[] classes = (Class[]) value; + String[] names = new String[classes.length]; + for (int i = 0; i < classes.length; i++) { + names[i] = classes[i].getName(); + } + return names; + } + } + if (value instanceof Annotation) { + Annotation annotation = (Annotation) value; + return MergedAnnotation.from(annotatedElement, annotation).synthesize(); + } + if (value instanceof Annotation[]) { + Annotation[] annotations = (Annotation[]) value; + Annotation[] synthesized = (Annotation[]) Array.newInstance( + annotations.getClass().getComponentType(), annotations.length); + for (int i = 0; i < annotations.length; i++) { + synthesized[i] = MergedAnnotation.from(annotatedElement, annotations[i]).synthesize(); + } + return synthesized; + } + return value; + } + + /** + * Retrieve the value of the {@code value} attribute of a + * single-element Annotation, given an annotation instance. + * @param annotation the annotation instance from which to retrieve the value + * @return the attribute value, or {@code null} if not found unless the attribute + * value cannot be retrieved due to an {@link AnnotationConfigurationException}, + * in which case such an exception will be rethrown + * @see #getValue(Annotation, String) + */ + @Nullable + public static Object getValue(Annotation annotation) { + return getValue(annotation, VALUE); + } + + /** + * Retrieve the value of a named attribute, given an annotation instance. + * @param annotation the annotation instance from which to retrieve the value + * @param attributeName the name of the attribute value to retrieve + * @return the attribute value, or {@code null} if not found unless the attribute + * value cannot be retrieved due to an {@link AnnotationConfigurationException}, + * in which case such an exception will be rethrown + * @see #getValue(Annotation) + */ + @Nullable + public static Object getValue(@Nullable Annotation annotation, @Nullable String attributeName) { + if (annotation == null || !StringUtils.hasText(attributeName)) { + return null; + } + try { + Method method = annotation.annotationType().getDeclaredMethod(attributeName); + ReflectionUtils.makeAccessible(method); + return method.invoke(annotation); + } + catch (NoSuchMethodException ex) { + return null; + } + catch (InvocationTargetException ex) { + rethrowAnnotationConfigurationException(ex.getTargetException()); + throw new IllegalStateException("Could not obtain value for annotation attribute '" + + attributeName + "' in " + annotation, ex); + } + catch (Throwable ex) { + handleIntrospectionFailure(annotation.getClass(), ex); + return null; + } + } + + /** + * If the supplied throwable is an {@link AnnotationConfigurationException}, + * it will be cast to an {@code AnnotationConfigurationException} and thrown, + * allowing it to propagate to the caller. + *

    Otherwise, this method does nothing. + * @param ex the throwable to inspect + */ + static void rethrowAnnotationConfigurationException(Throwable ex) { + if (ex instanceof AnnotationConfigurationException) { + throw (AnnotationConfigurationException) ex; + } + } + + /** + * Handle the supplied annotation introspection exception. + *

    If the supplied exception is an {@link AnnotationConfigurationException}, + * it will simply be thrown, allowing it to propagate to the caller, and + * nothing will be logged. + *

    Otherwise, this method logs an introspection failure (in particular for + * a {@link TypeNotPresentException}) before moving on, assuming nested + * {@code Class} values were not resolvable within annotation attributes and + * thereby effectively pretending there were no annotations on the specified + * element. + * @param element the element that we tried to introspect annotations on + * @param ex the exception that we encountered + * @see #rethrowAnnotationConfigurationException + * @see IntrospectionFailureLogger + */ + static void handleIntrospectionFailure(@Nullable AnnotatedElement element, Throwable ex) { + rethrowAnnotationConfigurationException(ex); + IntrospectionFailureLogger logger = IntrospectionFailureLogger.INFO; + boolean meta = false; + if (element instanceof Class && Annotation.class.isAssignableFrom((Class) element)) { + // Meta-annotation or (default) value lookup on an annotation type + logger = IntrospectionFailureLogger.DEBUG; + meta = true; + } + if (logger.isEnabled()) { + String message = meta ? + "Failed to meta-introspect annotation " : + "Failed to introspect annotations on "; + logger.log(message + element + ": " + ex); + } + } + + /** + * Retrieve the default value of the {@code value} attribute + * of a single-element Annotation, given an annotation instance. + * @param annotation the annotation instance from which to retrieve the default value + * @return the default value, or {@code null} if not found + * @see #getDefaultValue(Annotation, String) + */ + @Nullable + public static Object getDefaultValue(Annotation annotation) { + return getDefaultValue(annotation, VALUE); + } + + /** + * Retrieve the default value of a named attribute, given an annotation instance. + * @param annotation the annotation instance from which to retrieve the default value + * @param attributeName the name of the attribute value to retrieve + * @return the default value of the named attribute, or {@code null} if not found + * @see #getDefaultValue(Class, String) + */ + @Nullable + public static Object getDefaultValue(@Nullable Annotation annotation, @Nullable String attributeName) { + return (annotation != null ? getDefaultValue(annotation.annotationType(), attributeName) : null); + } + + /** + * Retrieve the default value of the {@code value} attribute + * of a single-element Annotation, given the {@link Class annotation type}. + * @param annotationType the annotation type for which the default value should be retrieved + * @return the default value, or {@code null} if not found + * @see #getDefaultValue(Class, String) + */ + @Nullable + public static Object getDefaultValue(Class annotationType) { + return getDefaultValue(annotationType, VALUE); + } + + /** + * Retrieve the default value of a named attribute, given the + * {@link Class annotation type}. + * @param annotationType the annotation type for which the default value should be retrieved + * @param attributeName the name of the attribute value to retrieve. + * @return the default value of the named attribute, or {@code null} if not found + * @see #getDefaultValue(Annotation, String) + */ + @Nullable + public static Object getDefaultValue( + @Nullable Class annotationType, @Nullable String attributeName) { + + if (annotationType == null || !StringUtils.hasText(attributeName)) { + return null; + } + return MergedAnnotation.of(annotationType).getDefaultValue(attributeName).orElse(null); + } + + /** + * Synthesize an annotation from the supplied {@code annotation} + * by wrapping it in a dynamic proxy that transparently enforces + * attribute alias semantics for annotation attributes that are + * annotated with {@link AliasFor @AliasFor}. + * @param annotation the annotation to synthesize + * @param annotatedElement the element that is annotated with the supplied + * annotation; may be {@code null} if unknown + * @return the synthesized annotation if the supplied annotation is + * synthesizable; {@code null} if the supplied annotation is + * {@code null}; otherwise the supplied annotation unmodified + * @throws AnnotationConfigurationException if invalid configuration of + * {@code @AliasFor} is detected + * @since 4.2 + * @see #synthesizeAnnotation(Map, Class, AnnotatedElement) + * @see #synthesizeAnnotation(Class) + */ + public static A synthesizeAnnotation( + A annotation, @Nullable AnnotatedElement annotatedElement) { + + if (annotation instanceof SynthesizedAnnotation || AnnotationFilter.PLAIN.matches(annotation)) { + return annotation; + } + return MergedAnnotation.from(annotatedElement, annotation).synthesize(); + } + + /** + * Synthesize an annotation from its default attributes values. + *

    This method simply delegates to + * {@link #synthesizeAnnotation(Map, Class, AnnotatedElement)}, + * supplying an empty map for the source attribute values and {@code null} + * for the {@link AnnotatedElement}. + * @param annotationType the type of annotation to synthesize + * @return the synthesized annotation + * @throws IllegalArgumentException if a required attribute is missing + * @throws AnnotationConfigurationException if invalid configuration of + * {@code @AliasFor} is detected + * @since 4.2 + * @see #synthesizeAnnotation(Map, Class, AnnotatedElement) + * @see #synthesizeAnnotation(Annotation, AnnotatedElement) + */ + public static A synthesizeAnnotation(Class annotationType) { + return synthesizeAnnotation(Collections.emptyMap(), annotationType, null); + } + + /** + * Synthesize an annotation from the supplied map of annotation + * attributes by wrapping the map in a dynamic proxy that implements an + * annotation of the specified {@code annotationType} and transparently + * enforces attribute alias semantics for annotation attributes + * that are annotated with {@link AliasFor @AliasFor}. + *

    The supplied map must contain a key-value pair for every attribute + * defined in the supplied {@code annotationType} that is not aliased or + * does not have a default value. Nested maps and nested arrays of maps + * will be recursively synthesized into nested annotations or nested + * arrays of annotations, respectively. + *

    Note that {@link AnnotationAttributes} is a specialized type of + * {@link Map} that is an ideal candidate for this method's + * {@code attributes} argument. + * @param attributes the map of annotation attributes to synthesize + * @param annotationType the type of annotation to synthesize + * @param annotatedElement the element that is annotated with the annotation + * corresponding to the supplied attributes; may be {@code null} if unknown + * @return the synthesized annotation + * @throws IllegalArgumentException if a required attribute is missing or if an + * attribute is not of the correct type + * @throws AnnotationConfigurationException if invalid configuration of + * {@code @AliasFor} is detected + * @since 4.2 + * @see #synthesizeAnnotation(Annotation, AnnotatedElement) + * @see #synthesizeAnnotation(Class) + * @see #getAnnotationAttributes(AnnotatedElement, Annotation) + * @see #getAnnotationAttributes(AnnotatedElement, Annotation, boolean, boolean) + */ + public static A synthesizeAnnotation(Map attributes, + Class annotationType, @Nullable AnnotatedElement annotatedElement) { + + try { + return MergedAnnotation.of(annotatedElement, annotationType, attributes).synthesize(); + } + catch (NoSuchElementException | IllegalStateException ex) { + throw new IllegalArgumentException(ex); + } + } + + /** + * Synthesize an array of annotations from the supplied array + * of {@code annotations} by creating a new array of the same size and + * type and populating it with {@linkplain #synthesizeAnnotation(Annotation, + * AnnotatedElement) synthesized} versions of the annotations from the input + * array. + * @param annotations the array of annotations to synthesize + * @param annotatedElement the element that is annotated with the supplied + * array of annotations; may be {@code null} if unknown + * @return a new array of synthesized annotations, or {@code null} if + * the supplied array is {@code null} + * @throws AnnotationConfigurationException if invalid configuration of + * {@code @AliasFor} is detected + * @since 4.2 + * @see #synthesizeAnnotation(Annotation, AnnotatedElement) + * @see #synthesizeAnnotation(Map, Class, AnnotatedElement) + */ + static Annotation[] synthesizeAnnotationArray(Annotation[] annotations, AnnotatedElement annotatedElement) { + if (AnnotationsScanner.hasPlainJavaAnnotationsOnly(annotatedElement)) { + return annotations; + } + Annotation[] synthesized = (Annotation[]) Array.newInstance( + annotations.getClass().getComponentType(), annotations.length); + for (int i = 0; i < annotations.length; i++) { + synthesized[i] = synthesizeAnnotation(annotations[i], annotatedElement); + } + return synthesized; + } + + /** + * Clear the internal annotation metadata cache. + * @since 4.3.15 + */ + public static void clearCache() { + AnnotationTypeMappings.clearCache(); + AnnotationsScanner.clearCache(); + } + + + /** + * Internal holder used to wrap default values. + */ + private static class DefaultValueHolder { + + final Object defaultValue; + + public DefaultValueHolder(Object defaultValue) { + this.defaultValue = defaultValue; + } + + @Override + public String toString() { + return "*" + this.defaultValue; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsProcessor.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsProcessor.java new file mode 100644 index 0000000..75552f3 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsProcessor.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; + +import org.springframework.lang.Nullable; + +/** + * Callback interface used to process annotations. + * + * @param the context type + * @param the result type + * @author Phillip Webb + * @since 5.2 + * @see AnnotationsScanner + * @see TypeMappedAnnotations + */ +@FunctionalInterface +interface AnnotationsProcessor { + + /** + * Called when an aggregate is about to be processed. This method may return + * a {@code non-null} result to short-circuit any further processing. + * @param context the context information relevant to the processor + * @param aggregateIndex the aggregate index about to be processed + * @return a {@code non-null} result if no further processing is required + */ + @Nullable + default R doWithAggregate(C context, int aggregateIndex) { + return null; + } + + /** + * Called when an array of annotations can be processed. This method may + * return a {@code non-null} result to short-circuit any further processing. + * @param context the context information relevant to the processor + * @param aggregateIndex the aggregate index of the provided annotations + * @param source the original source of the annotations, if known + * @param annotations the annotations to process (this array may contain + * {@code null} elements) + * @return a {@code non-null} result if no further processing is required + */ + @Nullable + R doWithAnnotations(C context, int aggregateIndex, @Nullable Object source, Annotation[] annotations); + + /** + * Get the final result to be returned. By default this method returns + * the last process result. + * @param result the last early exit result, or {@code null} if none + * @return the final result to be returned to the caller + */ + @Nullable + default R finish(@Nullable R result) { + return result; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java new file mode 100644 index 0000000..6264138 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java @@ -0,0 +1,537 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Map; + +import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.Ordered; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; +import org.springframework.lang.Nullable; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; + +/** + * Scanner to search for relevant annotations in the annotation hierarchy of an + * {@link AnnotatedElement}. + * + * @author Phillip Webb + * @author Sam Brannen + * @since 5.2 + * @see AnnotationsProcessor + */ +abstract class AnnotationsScanner { + + private static final Annotation[] NO_ANNOTATIONS = {}; + + private static final Method[] NO_METHODS = {}; + + + private static final Map declaredAnnotationCache = + new ConcurrentReferenceHashMap<>(256); + + private static final Map, Method[]> baseTypeMethodsCache = + new ConcurrentReferenceHashMap<>(256); + + + private AnnotationsScanner() { + } + + + /** + * Scan the hierarchy of the specified element for relevant annotations and + * call the processor as required. + * @param context an optional context object that will be passed back to the + * processor + * @param source the source element to scan + * @param searchStrategy the search strategy to use + * @param processor the processor that receives the annotations + * @return the result of {@link AnnotationsProcessor#finish(Object)} + */ + @Nullable + static R scan(C context, AnnotatedElement source, SearchStrategy searchStrategy, + AnnotationsProcessor processor) { + + R result = process(context, source, searchStrategy, processor); + return processor.finish(result); + } + + @Nullable + private static R process(C context, AnnotatedElement source, + SearchStrategy searchStrategy, AnnotationsProcessor processor) { + + if (source instanceof Class) { + return processClass(context, (Class) source, searchStrategy, processor); + } + if (source instanceof Method) { + return processMethod(context, (Method) source, searchStrategy, processor); + } + return processElement(context, source, processor); + } + + @Nullable + private static R processClass(C context, Class source, + SearchStrategy searchStrategy, AnnotationsProcessor processor) { + + switch (searchStrategy) { + case DIRECT: + return processElement(context, source, processor); + case INHERITED_ANNOTATIONS: + return processClassInheritedAnnotations(context, source, searchStrategy, processor); + case SUPERCLASS: + return processClassHierarchy(context, source, processor, false, false); + case TYPE_HIERARCHY: + return processClassHierarchy(context, source, processor, true, false); + case TYPE_HIERARCHY_AND_ENCLOSING_CLASSES: + return processClassHierarchy(context, source, processor, true, true); + } + throw new IllegalStateException("Unsupported search strategy " + searchStrategy); + } + + @Nullable + private static R processClassInheritedAnnotations(C context, Class source, + SearchStrategy searchStrategy, AnnotationsProcessor processor) { + + try { + if (isWithoutHierarchy(source, searchStrategy)) { + return processElement(context, source, processor); + } + Annotation[] relevant = null; + int remaining = Integer.MAX_VALUE; + int aggregateIndex = 0; + Class root = source; + while (source != null && source != Object.class && remaining > 0 && + !hasPlainJavaAnnotationsOnly(source)) { + R result = processor.doWithAggregate(context, aggregateIndex); + if (result != null) { + return result; + } + Annotation[] declaredAnnotations = getDeclaredAnnotations(source, true); + if (relevant == null && declaredAnnotations.length > 0) { + relevant = root.getAnnotations(); + remaining = relevant.length; + } + for (int i = 0; i < declaredAnnotations.length; i++) { + if (declaredAnnotations[i] != null) { + boolean isRelevant = false; + for (int relevantIndex = 0; relevantIndex < relevant.length; relevantIndex++) { + if (relevant[relevantIndex] != null && + declaredAnnotations[i].annotationType() == relevant[relevantIndex].annotationType()) { + isRelevant = true; + relevant[relevantIndex] = null; + remaining--; + break; + } + } + if (!isRelevant) { + declaredAnnotations[i] = null; + } + } + } + result = processor.doWithAnnotations(context, aggregateIndex, source, declaredAnnotations); + if (result != null) { + return result; + } + source = source.getSuperclass(); + aggregateIndex++; + } + } + catch (Throwable ex) { + AnnotationUtils.handleIntrospectionFailure(source, ex); + } + return null; + } + + @Nullable + private static R processClassHierarchy(C context, Class source, + AnnotationsProcessor processor, boolean includeInterfaces, boolean includeEnclosing) { + + return processClassHierarchy(context, new int[] {0}, source, processor, + includeInterfaces, includeEnclosing); + } + + @Nullable + private static R processClassHierarchy(C context, int[] aggregateIndex, Class source, + AnnotationsProcessor processor, boolean includeInterfaces, boolean includeEnclosing) { + + try { + R result = processor.doWithAggregate(context, aggregateIndex[0]); + if (result != null) { + return result; + } + if (hasPlainJavaAnnotationsOnly(source)) { + return null; + } + Annotation[] annotations = getDeclaredAnnotations(source, false); + result = processor.doWithAnnotations(context, aggregateIndex[0], source, annotations); + if (result != null) { + return result; + } + aggregateIndex[0]++; + if (includeInterfaces) { + for (Class interfaceType : source.getInterfaces()) { + R interfacesResult = processClassHierarchy(context, aggregateIndex, + interfaceType, processor, true, includeEnclosing); + if (interfacesResult != null) { + return interfacesResult; + } + } + } + Class superclass = source.getSuperclass(); + if (superclass != Object.class && superclass != null) { + R superclassResult = processClassHierarchy(context, aggregateIndex, + superclass, processor, includeInterfaces, includeEnclosing); + if (superclassResult != null) { + return superclassResult; + } + } + if (includeEnclosing) { + // Since merely attempting to load the enclosing class may result in + // automatic loading of sibling nested classes that in turn results + // in an exception such as NoClassDefFoundError, we wrap the following + // in its own dedicated try-catch block in order not to preemptively + // halt the annotation scanning process. + try { + Class enclosingClass = source.getEnclosingClass(); + if (enclosingClass != null) { + R enclosingResult = processClassHierarchy(context, aggregateIndex, + enclosingClass, processor, includeInterfaces, true); + if (enclosingResult != null) { + return enclosingResult; + } + } + } + catch (Throwable ex) { + AnnotationUtils.handleIntrospectionFailure(source, ex); + } + } + } + catch (Throwable ex) { + AnnotationUtils.handleIntrospectionFailure(source, ex); + } + return null; + } + + @Nullable + private static R processMethod(C context, Method source, + SearchStrategy searchStrategy, AnnotationsProcessor processor) { + + switch (searchStrategy) { + case DIRECT: + case INHERITED_ANNOTATIONS: + return processMethodInheritedAnnotations(context, source, processor); + case SUPERCLASS: + return processMethodHierarchy(context, new int[] {0}, source.getDeclaringClass(), + processor, source, false); + case TYPE_HIERARCHY: + case TYPE_HIERARCHY_AND_ENCLOSING_CLASSES: + return processMethodHierarchy(context, new int[] {0}, source.getDeclaringClass(), + processor, source, true); + } + throw new IllegalStateException("Unsupported search strategy " + searchStrategy); + } + + @Nullable + private static R processMethodInheritedAnnotations(C context, Method source, + AnnotationsProcessor processor) { + + try { + R result = processor.doWithAggregate(context, 0); + return (result != null ? result : + processMethodAnnotations(context, 0, source, processor)); + } + catch (Throwable ex) { + AnnotationUtils.handleIntrospectionFailure(source, ex); + } + return null; + } + + @Nullable + private static R processMethodHierarchy(C context, int[] aggregateIndex, + Class sourceClass, AnnotationsProcessor processor, Method rootMethod, + boolean includeInterfaces) { + + try { + R result = processor.doWithAggregate(context, aggregateIndex[0]); + if (result != null) { + return result; + } + if (hasPlainJavaAnnotationsOnly(sourceClass)) { + return null; + } + boolean calledProcessor = false; + if (sourceClass == rootMethod.getDeclaringClass()) { + result = processMethodAnnotations(context, aggregateIndex[0], + rootMethod, processor); + calledProcessor = true; + if (result != null) { + return result; + } + } + else { + for (Method candidateMethod : getBaseTypeMethods(context, sourceClass)) { + if (candidateMethod != null && isOverride(rootMethod, candidateMethod)) { + result = processMethodAnnotations(context, aggregateIndex[0], + candidateMethod, processor); + calledProcessor = true; + if (result != null) { + return result; + } + } + } + } + if (Modifier.isPrivate(rootMethod.getModifiers())) { + return null; + } + if (calledProcessor) { + aggregateIndex[0]++; + } + if (includeInterfaces) { + for (Class interfaceType : sourceClass.getInterfaces()) { + R interfacesResult = processMethodHierarchy(context, aggregateIndex, + interfaceType, processor, rootMethod, true); + if (interfacesResult != null) { + return interfacesResult; + } + } + } + Class superclass = sourceClass.getSuperclass(); + if (superclass != Object.class && superclass != null) { + R superclassResult = processMethodHierarchy(context, aggregateIndex, + superclass, processor, rootMethod, includeInterfaces); + if (superclassResult != null) { + return superclassResult; + } + } + } + catch (Throwable ex) { + AnnotationUtils.handleIntrospectionFailure(rootMethod, ex); + } + return null; + } + + private static Method[] getBaseTypeMethods(C context, Class baseType) { + if (baseType == Object.class || hasPlainJavaAnnotationsOnly(baseType)) { + return NO_METHODS; + } + + Method[] methods = baseTypeMethodsCache.get(baseType); + if (methods == null) { + boolean isInterface = baseType.isInterface(); + methods = isInterface ? baseType.getMethods() : ReflectionUtils.getDeclaredMethods(baseType); + int cleared = 0; + for (int i = 0; i < methods.length; i++) { + if ((!isInterface && Modifier.isPrivate(methods[i].getModifiers())) || + hasPlainJavaAnnotationsOnly(methods[i]) || + getDeclaredAnnotations(methods[i], false).length == 0) { + methods[i] = null; + cleared++; + } + } + if (cleared == methods.length) { + methods = NO_METHODS; + } + baseTypeMethodsCache.put(baseType, methods); + } + return methods; + } + + private static boolean isOverride(Method rootMethod, Method candidateMethod) { + return (!Modifier.isPrivate(candidateMethod.getModifiers()) && + candidateMethod.getName().equals(rootMethod.getName()) && + hasSameParameterTypes(rootMethod, candidateMethod)); + } + + private static boolean hasSameParameterTypes(Method rootMethod, Method candidateMethod) { + if (candidateMethod.getParameterCount() != rootMethod.getParameterCount()) { + return false; + } + Class[] rootParameterTypes = rootMethod.getParameterTypes(); + Class[] candidateParameterTypes = candidateMethod.getParameterTypes(); + if (Arrays.equals(candidateParameterTypes, rootParameterTypes)) { + return true; + } + return hasSameGenericTypeParameters(rootMethod, candidateMethod, + rootParameterTypes); + } + + private static boolean hasSameGenericTypeParameters( + Method rootMethod, Method candidateMethod, Class[] rootParameterTypes) { + + Class sourceDeclaringClass = rootMethod.getDeclaringClass(); + Class candidateDeclaringClass = candidateMethod.getDeclaringClass(); + if (!candidateDeclaringClass.isAssignableFrom(sourceDeclaringClass)) { + return false; + } + for (int i = 0; i < rootParameterTypes.length; i++) { + Class resolvedParameterType = ResolvableType.forMethodParameter( + candidateMethod, i, sourceDeclaringClass).resolve(); + if (rootParameterTypes[i] != resolvedParameterType) { + return false; + } + } + return true; + } + + @Nullable + private static R processMethodAnnotations(C context, int aggregateIndex, Method source, + AnnotationsProcessor processor) { + + Annotation[] annotations = getDeclaredAnnotations(source, false); + R result = processor.doWithAnnotations(context, aggregateIndex, source, annotations); + if (result != null) { + return result; + } + Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(source); + if (bridgedMethod != source) { + Annotation[] bridgedAnnotations = getDeclaredAnnotations(bridgedMethod, true); + for (int i = 0; i < bridgedAnnotations.length; i++) { + if (ObjectUtils.containsElement(annotations, bridgedAnnotations[i])) { + bridgedAnnotations[i] = null; + } + } + return processor.doWithAnnotations(context, aggregateIndex, source, bridgedAnnotations); + } + return null; + } + + @Nullable + private static R processElement(C context, AnnotatedElement source, + AnnotationsProcessor processor) { + + try { + R result = processor.doWithAggregate(context, 0); + return (result != null ? result : processor.doWithAnnotations( + context, 0, source, getDeclaredAnnotations(source, false))); + } + catch (Throwable ex) { + AnnotationUtils.handleIntrospectionFailure(source, ex); + } + return null; + } + + @SuppressWarnings("unchecked") + @Nullable + static A getDeclaredAnnotation(AnnotatedElement source, Class annotationType) { + Annotation[] annotations = getDeclaredAnnotations(source, false); + for (Annotation annotation : annotations) { + if (annotation != null && annotationType == annotation.annotationType()) { + return (A) annotation; + } + } + return null; + } + + static Annotation[] getDeclaredAnnotations(AnnotatedElement source, boolean defensive) { + boolean cached = false; + Annotation[] annotations = declaredAnnotationCache.get(source); + if (annotations != null) { + cached = true; + } + else { + annotations = source.getDeclaredAnnotations(); + if (annotations.length != 0) { + boolean allIgnored = true; + for (int i = 0; i < annotations.length; i++) { + Annotation annotation = annotations[i]; + if (isIgnorable(annotation.annotationType()) || + !AttributeMethods.forAnnotationType(annotation.annotationType()).isValid(annotation)) { + annotations[i] = null; + } + else { + allIgnored = false; + } + } + annotations = (allIgnored ? NO_ANNOTATIONS : annotations); + if (source instanceof Class || source instanceof Member) { + declaredAnnotationCache.put(source, annotations); + cached = true; + } + } + } + if (!defensive || annotations.length == 0 || !cached) { + return annotations; + } + return annotations.clone(); + } + + private static boolean isIgnorable(Class annotationType) { + return AnnotationFilter.PLAIN.matches(annotationType); + } + + static boolean isKnownEmpty(AnnotatedElement source, SearchStrategy searchStrategy) { + if (hasPlainJavaAnnotationsOnly(source)) { + return true; + } + if (searchStrategy == SearchStrategy.DIRECT || isWithoutHierarchy(source, searchStrategy)) { + if (source instanceof Method && ((Method) source).isBridge()) { + return false; + } + return getDeclaredAnnotations(source, false).length == 0; + } + return false; + } + + static boolean hasPlainJavaAnnotationsOnly(@Nullable Object annotatedElement) { + if (annotatedElement instanceof Class) { + return hasPlainJavaAnnotationsOnly((Class) annotatedElement); + } + else if (annotatedElement instanceof Member) { + return hasPlainJavaAnnotationsOnly(((Member) annotatedElement).getDeclaringClass()); + } + else { + return false; + } + } + + static boolean hasPlainJavaAnnotationsOnly(Class type) { + return (type.getName().startsWith("java.") || type == Ordered.class); + } + + private static boolean isWithoutHierarchy(AnnotatedElement source, SearchStrategy searchStrategy) { + if (source == Object.class) { + return true; + } + if (source instanceof Class) { + Class sourceClass = (Class) source; + boolean noSuperTypes = (sourceClass.getSuperclass() == Object.class && + sourceClass.getInterfaces().length == 0); + return (searchStrategy == SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES ? noSuperTypes && + sourceClass.getEnclosingClass() == null : noSuperTypes); + } + if (source instanceof Method) { + Method sourceMethod = (Method) source; + return (Modifier.isPrivate(sourceMethod.getModifiers()) || + isWithoutHierarchy(sourceMethod.getDeclaringClass(), searchStrategy)); + } + return true; + } + + static void clearCache() { + declaredAnnotationCache.clear(); + baseTypeMethodsCache.clear(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java b/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java new file mode 100644 index 0000000..f5c06ff --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java @@ -0,0 +1,304 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Map; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ReflectionUtils; + +/** + * Provides a quick way to access the attribute methods of an {@link Annotation} + * with consistent ordering as well as a few useful utility methods. + * + * @author Phillip Webb + * @since 5.2 + */ +final class AttributeMethods { + + static final AttributeMethods NONE = new AttributeMethods(null, new Method[0]); + + + private static final Map, AttributeMethods> cache = + new ConcurrentReferenceHashMap<>(); + + private static final Comparator methodComparator = (m1, m2) -> { + if (m1 != null && m2 != null) { + return m1.getName().compareTo(m2.getName()); + } + return m1 != null ? -1 : 1; + }; + + + @Nullable + private final Class annotationType; + + private final Method[] attributeMethods; + + private final boolean[] canThrowTypeNotPresentException; + + private final boolean hasDefaultValueMethod; + + private final boolean hasNestedAnnotation; + + + private AttributeMethods(@Nullable Class annotationType, Method[] attributeMethods) { + this.annotationType = annotationType; + this.attributeMethods = attributeMethods; + this.canThrowTypeNotPresentException = new boolean[attributeMethods.length]; + boolean foundDefaultValueMethod = false; + boolean foundNestedAnnotation = false; + for (int i = 0; i < attributeMethods.length; i++) { + Method method = this.attributeMethods[i]; + Class type = method.getReturnType(); + if (method.getDefaultValue() != null) { + foundDefaultValueMethod = true; + } + if (type.isAnnotation() || (type.isArray() && type.getComponentType().isAnnotation())) { + foundNestedAnnotation = true; + } + ReflectionUtils.makeAccessible(method); + this.canThrowTypeNotPresentException[i] = (type == Class.class || type == Class[].class || type.isEnum()); + } + this.hasDefaultValueMethod = foundDefaultValueMethod; + this.hasNestedAnnotation = foundNestedAnnotation; + } + + + /** + * Determine if this instance only contains a single attribute named + * {@code value}. + * @return {@code true} if there is only a value attribute + */ + boolean hasOnlyValueAttribute() { + return (this.attributeMethods.length == 1 && + MergedAnnotation.VALUE.equals(this.attributeMethods[0].getName())); + } + + + /** + * Determine if values from the given annotation can be safely accessed without + * causing any {@link TypeNotPresentException TypeNotPresentExceptions}. + * @param annotation the annotation to check + * @return {@code true} if all values are present + * @see #validate(Annotation) + */ + boolean isValid(Annotation annotation) { + assertAnnotation(annotation); + for (int i = 0; i < size(); i++) { + if (canThrowTypeNotPresentException(i)) { + try { + get(i).invoke(annotation); + } + catch (Throwable ex) { + return false; + } + } + } + return true; + } + + /** + * Check if values from the given annotation can be safely accessed without causing + * any {@link TypeNotPresentException TypeNotPresentExceptions}. In particular, + * this method is designed to cover Google App Engine's late arrival of such + * exceptions for {@code Class} values (instead of the more typical early + * {@code Class.getAnnotations() failure}. + * @param annotation the annotation to validate + * @throws IllegalStateException if a declared {@code Class} attribute could not be read + * @see #isValid(Annotation) + */ + void validate(Annotation annotation) { + assertAnnotation(annotation); + for (int i = 0; i < size(); i++) { + if (canThrowTypeNotPresentException(i)) { + try { + get(i).invoke(annotation); + } + catch (Throwable ex) { + throw new IllegalStateException("Could not obtain annotation attribute value for " + + get(i).getName() + " declared on " + annotation.annotationType(), ex); + } + } + } + } + + private void assertAnnotation(Annotation annotation) { + Assert.notNull(annotation, "Annotation must not be null"); + if (this.annotationType != null) { + Assert.isInstanceOf(this.annotationType, annotation); + } + } + + /** + * Get the attribute with the specified name or {@code null} if no + * matching attribute exists. + * @param name the attribute name to find + * @return the attribute method or {@code null} + */ + @Nullable + Method get(String name) { + int index = indexOf(name); + return index != -1 ? this.attributeMethods[index] : null; + } + + /** + * Get the attribute at the specified index. + * @param index the index of the attribute to return + * @return the attribute method + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index >= size()) + */ + Method get(int index) { + return this.attributeMethods[index]; + } + + /** + * Determine if the attribute at the specified index could throw a + * {@link TypeNotPresentException} when accessed. + * @param index the index of the attribute to check + * @return {@code true} if the attribute can throw a + * {@link TypeNotPresentException} + */ + boolean canThrowTypeNotPresentException(int index) { + return this.canThrowTypeNotPresentException[index]; + } + + /** + * Get the index of the attribute with the specified name, or {@code -1} + * if there is no attribute with the name. + * @param name the name to find + * @return the index of the attribute, or {@code -1} + */ + int indexOf(String name) { + for (int i = 0; i < this.attributeMethods.length; i++) { + if (this.attributeMethods[i].getName().equals(name)) { + return i; + } + } + return -1; + } + + /** + * Get the index of the specified attribute, or {@code -1} if the + * attribute is not in this collection. + * @param attribute the attribute to find + * @return the index of the attribute, or {@code -1} + */ + int indexOf(Method attribute) { + for (int i = 0; i < this.attributeMethods.length; i++) { + if (this.attributeMethods[i].equals(attribute)) { + return i; + } + } + return -1; + } + + /** + * Get the number of attributes in this collection. + * @return the number of attributes + */ + int size() { + return this.attributeMethods.length; + } + + /** + * Determine if at least one of the attribute methods has a default value. + * @return {@code true} if there is at least one attribute method with a default value + */ + boolean hasDefaultValueMethod() { + return this.hasDefaultValueMethod; + } + + /** + * Determine if at least one of the attribute methods is a nested annotation. + * @return {@code true} if there is at least one attribute method with a nested + * annotation type + */ + boolean hasNestedAnnotation() { + return this.hasNestedAnnotation; + } + + + /** + * Get the attribute methods for the given annotation type. + * @param annotationType the annotation type + * @return the attribute methods for the annotation type + */ + static AttributeMethods forAnnotationType(@Nullable Class annotationType) { + if (annotationType == null) { + return NONE; + } + return cache.computeIfAbsent(annotationType, AttributeMethods::compute); + } + + private static AttributeMethods compute(Class annotationType) { + Method[] methods = annotationType.getDeclaredMethods(); + int size = methods.length; + for (int i = 0; i < methods.length; i++) { + if (!isAttributeMethod(methods[i])) { + methods[i] = null; + size--; + } + } + if (size == 0) { + return NONE; + } + Arrays.sort(methods, methodComparator); + Method[] attributeMethods = Arrays.copyOf(methods, size); + return new AttributeMethods(annotationType, attributeMethods); + } + + private static boolean isAttributeMethod(Method method) { + return (method.getParameterCount() == 0 && method.getReturnType() != void.class); + } + + /** + * Create a description for the given attribute method suitable to use in + * exception messages and logs. + * @param attribute the attribute to describe + * @return a description of the attribute + */ + static String describe(@Nullable Method attribute) { + if (attribute == null) { + return "(none)"; + } + return describe(attribute.getDeclaringClass(), attribute.getName()); + } + + /** + * Create a description for the given attribute method suitable to use in + * exception messages and logs. + * @param annotationType the annotation type + * @param attributeName the attribute name + * @return a description of the attribute + */ + static String describe(@Nullable Class annotationType, @Nullable String attributeName) { + if (attributeName == null) { + return "(none)"; + } + String in = (annotationType != null ? " in annotation [" + annotationType.getName() + "]" : ""); + return "attribute '" + attributeName + "'" + in; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/IntrospectionFailureLogger.java b/spring-core/src/main/java/org/springframework/core/annotation/IntrospectionFailureLogger.java new file mode 100644 index 0000000..23178ce --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/IntrospectionFailureLogger.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.lang.Nullable; + +/** + * Log facade used to handle annotation introspection failures (in particular + * {@code TypeNotPresentExceptions}). Allows annotation processing to continue, + * assuming that when Class attribute values are not resolvable the annotation + * should effectively disappear. + * + * @author Phillip Webb + * @since 5.2 + */ +enum IntrospectionFailureLogger { + + DEBUG { + @Override + public boolean isEnabled() { + return getLogger().isDebugEnabled(); + } + @Override + public void log(String message) { + getLogger().debug(message); + } + }, + + INFO { + @Override + public boolean isEnabled() { + return getLogger().isInfoEnabled(); + } + @Override + public void log(String message) { + getLogger().info(message); + } + }; + + + @Nullable + private static Log logger; + + + void log(String message, @Nullable Object source, Exception ex) { + String on = (source != null ? " on " + source : ""); + log(message + on + ": " + ex); + } + + abstract boolean isEnabled(); + + abstract void log(String message); + + + private static Log getLogger() { + Log logger = IntrospectionFailureLogger.logger; + if (logger == null) { + logger = LogFactory.getLog(MergedAnnotation.class); + IntrospectionFailureLogger.logger = logger; + } + return logger; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotation.java b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotation.java new file mode 100644 index 0000000..ed60e36 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotation.java @@ -0,0 +1,661 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Inherited; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Proxy; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; + +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; +import org.springframework.lang.Nullable; + +/** + * A single merged annotation returned from a {@link MergedAnnotations} + * collection. Presents a view onto an annotation where attribute values may + * have been "merged" from different source values. + * + *

    Attribute values may be accessed using the various {@code get} methods. + * For example, to access an {@code int} attribute the {@link #getInt(String)} + * method would be used. + * + *

    Note that attribute values are not converted when accessed. + * For example, it is not possible to call {@link #getString(String)} if the + * underlying attribute is an {@code int}. The only exception to this rule is + * {@code Class} and {@code Class[]} values which may be accessed as + * {@code String} and {@code String[]} respectively to prevent potential early + * class initialization. + * + *

    If necessary, a {@code MergedAnnotation} can be {@linkplain #synthesize() + * synthesized} back into an actual {@link java.lang.annotation.Annotation}. + * + * @author Phillip Webb + * @author Juergen Hoeller + * @author Sam Brannen + * @since 5.2 + * @param the annotation type + * @see MergedAnnotations + * @see MergedAnnotationPredicates + */ +public interface MergedAnnotation { + + /** + * The attribute name for annotations with a single element. + */ + String VALUE = "value"; + + + /** + * Get the {@code Class} reference for the actual annotation type. + * @return the annotation type + */ + Class getType(); + + /** + * Determine if the annotation is present on the source. Considers + * {@linkplain #isDirectlyPresent() directly present} and + * {@linkplain #isMetaPresent() meta-present} annotations within the context + * of the {@link SearchStrategy} used. + * @return {@code true} if the annotation is present + */ + boolean isPresent(); + + /** + * Determine if the annotation is directly present on the source. + *

    A directly present annotation is one that the user has explicitly + * declared and not one that is {@linkplain #isMetaPresent() meta-present} + * or {@link Inherited @Inherited}. + * @return {@code true} if the annotation is directly present + */ + boolean isDirectlyPresent(); + + /** + * Determine if the annotation is meta-present on the source. + *

    A meta-present annotation is an annotation that the user hasn't + * explicitly declared, but has been used as a meta-annotation somewhere in + * the annotation hierarchy. + * @return {@code true} if the annotation is meta-present + */ + boolean isMetaPresent(); + + /** + * Get the distance of this annotation related to its use as a + * meta-annotation. + *

    A directly declared annotation has a distance of {@code 0}, a + * meta-annotation has a distance of {@code 1}, a meta-annotation on a + * meta-annotation has a distance of {@code 2}, etc. A {@linkplain #missing() + * missing} annotation will always return a distance of {@code -1}. + * @return the annotation distance or {@code -1} if the annotation is missing + */ + int getDistance(); + + /** + * Get the index of the aggregate collection containing this annotation. + *

    Can be used to reorder a stream of annotations, for example, to give a + * higher priority to annotations declared on a superclass or interface. A + * {@linkplain #missing() missing} annotation will always return an aggregate + * index of {@code -1}. + * @return the aggregate index (starting at {@code 0}) or {@code -1} if the + * annotation is missing + */ + int getAggregateIndex(); + + /** + * Get the source that ultimately declared the root annotation, or + * {@code null} if the source is not known. + *

    If this merged annotation was created + * {@link MergedAnnotations#from(AnnotatedElement) from} an + * {@link AnnotatedElement} then this source will be an element of the same + * type. If the annotation was loaded without using reflection, the source + * can be of any type, but should have a sensible {@code toString()}. + * Meta-annotations will always return the same source as the + * {@link #getRoot() root}. + * @return the source, or {@code null} + */ + @Nullable + Object getSource(); + + /** + * Get the source of the meta-annotation, or {@code null} if the + * annotation is not {@linkplain #isMetaPresent() meta-present}. + *

    The meta-source is the annotation that was meta-annotated with this + * annotation. + * @return the meta-annotation source or {@code null} + * @see #getRoot() + */ + @Nullable + MergedAnnotation getMetaSource(); + + /** + * Get the root annotation, i.e. the {@link #getDistance() distance} {@code 0} + * annotation as directly declared on the source. + * @return the root annotation + * @see #getMetaSource() + */ + MergedAnnotation getRoot(); + + /** + * Get the complete list of annotation types within the annotation hierarchy + * from this annotation to the {@link #getRoot() root}. + *

    Provides a useful way to uniquely identify a merged annotation instance. + * @return the meta types for the annotation + * @see MergedAnnotationPredicates#unique(Function) + * @see #getRoot() + * @see #getMetaSource() + */ + List> getMetaTypes(); + + + /** + * Determine if the specified attribute name has a non-default value when + * compared to the annotation declaration. + * @param attributeName the attribute name + * @return {@code true} if the attribute value is different from the default + * value + */ + boolean hasNonDefaultValue(String attributeName); + + /** + * Determine if the specified attribute name has a default value when compared + * to the annotation declaration. + * @param attributeName the attribute name + * @return {@code true} if the attribute value is the same as the default + * value + */ + boolean hasDefaultValue(String attributeName) throws NoSuchElementException; + + /** + * Get a required byte attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a byte + * @throws NoSuchElementException if there is no matching attribute + */ + byte getByte(String attributeName) throws NoSuchElementException; + + /** + * Get a required byte array attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a byte array + * @throws NoSuchElementException if there is no matching attribute + */ + byte[] getByteArray(String attributeName) throws NoSuchElementException; + + /** + * Get a required boolean attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a boolean + * @throws NoSuchElementException if there is no matching attribute + */ + boolean getBoolean(String attributeName) throws NoSuchElementException; + + /** + * Get a required boolean array attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a boolean array + * @throws NoSuchElementException if there is no matching attribute + */ + boolean[] getBooleanArray(String attributeName) throws NoSuchElementException; + + /** + * Get a required char attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a char + * @throws NoSuchElementException if there is no matching attribute + */ + char getChar(String attributeName) throws NoSuchElementException; + + /** + * Get a required char array attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a char array + * @throws NoSuchElementException if there is no matching attribute + */ + char[] getCharArray(String attributeName) throws NoSuchElementException; + + /** + * Get a required short attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a short + * @throws NoSuchElementException if there is no matching attribute + */ + short getShort(String attributeName) throws NoSuchElementException; + + /** + * Get a required short array attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a short array + * @throws NoSuchElementException if there is no matching attribute + */ + short[] getShortArray(String attributeName) throws NoSuchElementException; + + /** + * Get a required int attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as an int + * @throws NoSuchElementException if there is no matching attribute + */ + int getInt(String attributeName) throws NoSuchElementException; + + /** + * Get a required int array attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as an int array + * @throws NoSuchElementException if there is no matching attribute + */ + int[] getIntArray(String attributeName) throws NoSuchElementException; + + /** + * Get a required long attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a long + * @throws NoSuchElementException if there is no matching attribute + */ + long getLong(String attributeName) throws NoSuchElementException; + + /** + * Get a required long array attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a long array + * @throws NoSuchElementException if there is no matching attribute + */ + long[] getLongArray(String attributeName) throws NoSuchElementException; + + /** + * Get a required double attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a double + * @throws NoSuchElementException if there is no matching attribute + */ + double getDouble(String attributeName) throws NoSuchElementException; + + /** + * Get a required double array attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a double array + * @throws NoSuchElementException if there is no matching attribute + */ + double[] getDoubleArray(String attributeName) throws NoSuchElementException; + + /** + * Get a required float attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a float + * @throws NoSuchElementException if there is no matching attribute + */ + float getFloat(String attributeName) throws NoSuchElementException; + + /** + * Get a required float array attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a float array + * @throws NoSuchElementException if there is no matching attribute + */ + float[] getFloatArray(String attributeName) throws NoSuchElementException; + + /** + * Get a required string attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a string + * @throws NoSuchElementException if there is no matching attribute + */ + String getString(String attributeName) throws NoSuchElementException; + + /** + * Get a required string array attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a string array + * @throws NoSuchElementException if there is no matching attribute + */ + String[] getStringArray(String attributeName) throws NoSuchElementException; + + /** + * Get a required class attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a class + * @throws NoSuchElementException if there is no matching attribute + */ + Class getClass(String attributeName) throws NoSuchElementException; + + /** + * Get a required class array attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a class array + * @throws NoSuchElementException if there is no matching attribute + */ + Class[] getClassArray(String attributeName) throws NoSuchElementException; + + /** + * Get a required enum attribute value from the annotation. + * @param attributeName the attribute name + * @param type the enum type + * @return the value as a enum + * @throws NoSuchElementException if there is no matching attribute + */ + > E getEnum(String attributeName, Class type) throws NoSuchElementException; + + /** + * Get a required enum array attribute value from the annotation. + * @param attributeName the attribute name + * @param type the enum type + * @return the value as a enum array + * @throws NoSuchElementException if there is no matching attribute + */ + > E[] getEnumArray(String attributeName, Class type) throws NoSuchElementException; + + /** + * Get a required annotation attribute value from the annotation. + * @param attributeName the attribute name + * @param type the annotation type + * @return the value as a {@link MergedAnnotation} + * @throws NoSuchElementException if there is no matching attribute + */ + MergedAnnotation getAnnotation(String attributeName, Class type) + throws NoSuchElementException; + + /** + * Get a required annotation array attribute value from the annotation. + * @param attributeName the attribute name + * @param type the annotation type + * @return the value as a {@link MergedAnnotation} array + * @throws NoSuchElementException if there is no matching attribute + */ + MergedAnnotation[] getAnnotationArray(String attributeName, Class type) + throws NoSuchElementException; + + /** + * Get an optional attribute value from the annotation. + * @param attributeName the attribute name + * @return an optional value or {@link Optional#empty()} if there is no + * matching attribute + */ + Optional getValue(String attributeName); + + /** + * Get an optional attribute value from the annotation. + * @param attributeName the attribute name + * @param type the attribute type. Must be compatible with the underlying + * attribute type or {@code Object.class}. + * @return an optional value or {@link Optional#empty()} if there is no + * matching attribute + */ + Optional getValue(String attributeName, Class type); + + /** + * Get the default attribute value from the annotation as specified in + * the annotation declaration. + * @param attributeName the attribute name + * @return an optional of the default value or {@link Optional#empty()} if + * there is no matching attribute or no defined default + */ + Optional getDefaultValue(String attributeName); + + /** + * Get the default attribute value from the annotation as specified in + * the annotation declaration. + * @param attributeName the attribute name + * @param type the attribute type. Must be compatible with the underlying + * attribute type or {@code Object.class}. + * @return an optional of the default value or {@link Optional#empty()} if + * there is no matching attribute or no defined default + */ + Optional getDefaultValue(String attributeName, Class type); + + /** + * Create a new view of the annotation with all attributes that have default + * values removed. + * @return a filtered view of the annotation without any attributes that + * have a default value + * @see #filterAttributes(Predicate) + */ + MergedAnnotation filterDefaultValues(); + + /** + * Create a new view of the annotation with only attributes that match the + * given predicate. + * @param predicate a predicate used to filter attribute names + * @return a filtered view of the annotation + * @see #filterDefaultValues() + * @see MergedAnnotationPredicates + */ + MergedAnnotation filterAttributes(Predicate predicate); + + /** + * Create a new view of the annotation that exposes non-merged attribute values. + *

    Methods from this view will return attribute values with only alias mirroring + * rules applied. Aliases to {@link #getMetaSource() meta-source} attributes will + * not be applied. + * @return a non-merged view of the annotation + */ + MergedAnnotation withNonMergedAttributes(); + + /** + * Create a new mutable {@link AnnotationAttributes} instance from this + * merged annotation. + *

    The {@link Adapt adaptations} may be used to change the way that values + * are added. + * @param adaptations the adaptations that should be applied to the annotation values + * @return an immutable map containing the attributes and values + */ + AnnotationAttributes asAnnotationAttributes(Adapt... adaptations); + + /** + * Get an immutable {@link Map} that contains all the annotation attributes. + *

    The {@link Adapt adaptations} may be used to change the way that values are added. + * @param adaptations the adaptations that should be applied to the annotation values + * @return an immutable map containing the attributes and values + */ + Map asMap(Adapt... adaptations); + + /** + * Create a new {@link Map} instance of the given type that contains all the annotation + * attributes. + *

    The {@link Adapt adaptations} may be used to change the way that values are added. + * @param factory a map factory + * @param adaptations the adaptations that should be applied to the annotation values + * @return a map containing the attributes and values + */ + > T asMap(Function, T> factory, Adapt... adaptations); + + /** + * Create a type-safe synthesized version of this merged annotation that can + * be used directly in code. + *

    The result is synthesized using a JDK {@link Proxy} and as a result may + * incur a computational cost when first invoked. + *

    If this merged annotation was created {@linkplain #from(Annotation) from} + * an annotation instance, that annotation will be returned unmodified if it is + * not synthesizable. An annotation is considered synthesizable if + * one of the following is true. + *

    + * @return a synthesized version of the annotation or the original annotation + * unmodified + * @throws NoSuchElementException on a missing annotation + */ + A synthesize() throws NoSuchElementException; + + /** + * Optionally create a type-safe synthesized version of this annotation based + * on a condition predicate. + *

    The result is synthesized using a JDK {@link Proxy} and as a result may + * incur a computational cost when first invoked. + *

    Consult the documentation for {@link #synthesize()} for an explanation + * of what is considered synthesizable. + * @param condition the test to determine if the annotation can be synthesized + * @return an optional containing the synthesized version of the annotation or + * an empty optional if the condition doesn't match + * @throws NoSuchElementException on a missing annotation + * @see MergedAnnotationPredicates + */ + Optional synthesize(Predicate> condition) throws NoSuchElementException; + + + /** + * Create a {@link MergedAnnotation} that represents a missing annotation + * (i.e. one that is not present). + * @return an instance representing a missing annotation + */ + static MergedAnnotation missing() { + return MissingMergedAnnotation.getInstance(); + } + + /** + * Create a new {@link MergedAnnotation} instance from the specified + * annotation. + * @param annotation the annotation to include + * @return a {@link MergedAnnotation} instance containing the annotation + */ + static MergedAnnotation from(A annotation) { + return from(null, annotation); + } + + /** + * Create a new {@link MergedAnnotation} instance from the specified + * annotation. + * @param source the source for the annotation. This source is used only for + * information and logging. It does not need to actually contain + * the specified annotations, and it will not be searched. + * @param annotation the annotation to include + * @return a {@link MergedAnnotation} instance for the annotation + */ + static MergedAnnotation from(@Nullable Object source, A annotation) { + return TypeMappedAnnotation.from(source, annotation); + } + + /** + * Create a new {@link MergedAnnotation} instance of the specified + * annotation type. The resulting annotation will not have any attribute + * values but may still be used to query default values. + * @param annotationType the annotation type + * @return a {@link MergedAnnotation} instance for the annotation + */ + static MergedAnnotation of(Class annotationType) { + return of(null, annotationType, null); + } + + /** + * Create a new {@link MergedAnnotation} instance of the specified + * annotation type with attribute values supplied by a map. + * @param annotationType the annotation type + * @param attributes the annotation attributes or {@code null} if just default + * values should be used + * @return a {@link MergedAnnotation} instance for the annotation and attributes + * @see #of(AnnotatedElement, Class, Map) + */ + static MergedAnnotation of( + Class annotationType, @Nullable Map attributes) { + + return of(null, annotationType, attributes); + } + + /** + * Create a new {@link MergedAnnotation} instance of the specified + * annotation type with attribute values supplied by a map. + * @param source the source for the annotation. This source is used only for + * information and logging. It does not need to actually contain + * the specified annotations and it will not be searched. + * @param annotationType the annotation type + * @param attributes the annotation attributes or {@code null} if just default + * values should be used + * @return a {@link MergedAnnotation} instance for the annotation and attributes + */ + static MergedAnnotation of( + @Nullable AnnotatedElement source, Class annotationType, @Nullable Map attributes) { + + return of(null, source, annotationType, attributes); + } + + /** + * Create a new {@link MergedAnnotation} instance of the specified + * annotation type with attribute values supplied by a map. + * @param classLoader the class loader used to resolve class attributes + * @param source the source for the annotation. This source is used only for + * information and logging. It does not need to actually contain + * the specified annotations and it will not be searched. + * @param annotationType the annotation type + * @param attributes the annotation attributes or {@code null} if just default + * values should be used + * @return a {@link MergedAnnotation} instance for the annotation and attributes + */ + static MergedAnnotation of( + @Nullable ClassLoader classLoader, @Nullable Object source, + Class annotationType, @Nullable Map attributes) { + + return TypeMappedAnnotation.of(classLoader, source, annotationType, attributes); + } + + + /** + * Adaptations that can be applied to attribute values when creating + * {@linkplain MergedAnnotation#asMap(Adapt...) Maps} or + * {@link MergedAnnotation#asAnnotationAttributes(Adapt...) AnnotationAttributes}. + */ + enum Adapt { + + /** + * Adapt class or class array attributes to strings. + */ + CLASS_TO_STRING, + + /** + * Adapt nested annotation or annotation arrays to maps rather + * than synthesizing the values. + */ + ANNOTATION_TO_MAP; + + protected final boolean isIn(Adapt... adaptations) { + for (Adapt candidate : adaptations) { + if (candidate == this) { + return true; + } + } + return false; + } + + /** + * Factory method to create an {@link Adapt} array from a set of boolean flags. + * @param classToString if {@link Adapt#CLASS_TO_STRING} is included + * @param annotationsToMap if {@link Adapt#ANNOTATION_TO_MAP} is included + * @return a new {@link Adapt} array + */ + public static Adapt[] values(boolean classToString, boolean annotationsToMap) { + EnumSet result = EnumSet.noneOf(Adapt.class); + addIfTrue(result, Adapt.CLASS_TO_STRING, classToString); + addIfTrue(result, Adapt.ANNOTATION_TO_MAP, annotationsToMap); + return result.toArray(new Adapt[0]); + } + + private static void addIfTrue(Set result, T value, boolean test) { + if (test) { + result.add(value); + } + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationCollectors.java b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationCollectors.java new file mode 100644 index 0000000..d09369f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationCollectors.java @@ -0,0 +1,165 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.stream.Collector; +import java.util.stream.Collector.Characteristics; + +import org.springframework.core.annotation.MergedAnnotation.Adapt; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * {@link Collector} implementations that provide various reduction operations for + * {@link MergedAnnotation} instances. + * + * @author Phillip Webb + * @author Sam Brannen + * @since 5.2 + */ +public abstract class MergedAnnotationCollectors { + + private static final Characteristics[] NO_CHARACTERISTICS = {}; + + private static final Characteristics[] IDENTITY_FINISH_CHARACTERISTICS = {Characteristics.IDENTITY_FINISH}; + + + private MergedAnnotationCollectors() { + } + + + /** + * Create a new {@link Collector} that accumulates merged annotations to a + * {@link LinkedHashSet} containing {@linkplain MergedAnnotation#synthesize() + * synthesized} versions. + *

    The collector returned by this method is effectively equivalent to + * {@code Collectors.mapping(MergedAnnotation::synthesize, Collectors.toCollection(LinkedHashSet::new))} + * but avoids the creation of a composite collector. + * @param the annotation type + * @return a {@link Collector} which collects and synthesizes the + * annotations into a {@link Set} + */ + public static Collector, ?, Set> toAnnotationSet() { + return Collector.of(LinkedHashSet::new, (set, annotation) -> set.add(annotation.synthesize()), + MergedAnnotationCollectors::combiner); + } + + /** + * Create a new {@link Collector} that accumulates merged annotations to an + * {@link Annotation} array containing {@linkplain MergedAnnotation#synthesize() + * synthesized} versions. + * @param the annotation type + * @return a {@link Collector} which collects and synthesizes the + * annotations into an {@code Annotation[]} + * @see #toAnnotationArray(IntFunction) + */ + public static Collector, ?, Annotation[]> toAnnotationArray() { + return toAnnotationArray(Annotation[]::new); + } + + /** + * Create a new {@link Collector} that accumulates merged annotations to an + * {@link Annotation} array containing {@linkplain MergedAnnotation#synthesize() + * synthesized} versions. + * @param the annotation type + * @param the resulting array type + * @param generator a function which produces a new array of the desired + * type and the provided length + * @return a {@link Collector} which collects and synthesizes the + * annotations into an annotation array + * @see #toAnnotationArray + */ + public static Collector, ?, R[]> toAnnotationArray( + IntFunction generator) { + + return Collector.of(ArrayList::new, (list, annotation) -> list.add(annotation.synthesize()), + MergedAnnotationCollectors::combiner, list -> list.toArray(generator.apply(list.size()))); + } + + /** + * Create a new {@link Collector} that accumulates merged annotations to a + * {@link MultiValueMap} with items {@linkplain MultiValueMap#add(Object, Object) + * added} from each merged annotation + * {@linkplain MergedAnnotation#asMap(Adapt...) as a map}. + * @param the annotation type + * @param adaptations the adaptations that should be applied to the annotation values + * @return a {@link Collector} which collects and synthesizes the + * annotations into a {@link LinkedMultiValueMap} + * @see #toMultiValueMap(Function, MergedAnnotation.Adapt...) + */ + public static Collector, ?, MultiValueMap> toMultiValueMap( + Adapt... adaptations) { + + return toMultiValueMap(Function.identity(), adaptations); + } + + /** + * Create a new {@link Collector} that accumulates merged annotations to a + * {@link MultiValueMap} with items {@linkplain MultiValueMap#add(Object, Object) + * added} from each merged annotation + * {@linkplain MergedAnnotation#asMap(Adapt...) as a map}. + * @param the annotation type + * @param finisher the finisher function for the new {@link MultiValueMap} + * @param adaptations the adaptations that should be applied to the annotation values + * @return a {@link Collector} which collects and synthesizes the + * annotations into a {@link LinkedMultiValueMap} + * @see #toMultiValueMap(MergedAnnotation.Adapt...) + */ + public static Collector, ?, MultiValueMap> toMultiValueMap( + Function, MultiValueMap> finisher, + Adapt... adaptations) { + + Characteristics[] characteristics = (isSameInstance(finisher, Function.identity()) ? + IDENTITY_FINISH_CHARACTERISTICS : NO_CHARACTERISTICS); + return Collector.of(LinkedMultiValueMap::new, + (map, annotation) -> annotation.asMap(adaptations).forEach(map::add), + MergedAnnotationCollectors::combiner, finisher, characteristics); + } + + + private static boolean isSameInstance(Object instance, Object candidate) { + return instance == candidate; + } + + /** + * {@link Collector#combiner() Combiner} for collections. + *

    This method is only invoked if the {@link java.util.stream.Stream} is + * processed in {@linkplain java.util.stream.Stream#parallel() parallel}. + */ + private static > C combiner(C collection, C additions) { + collection.addAll(additions); + return collection; + } + + /** + * {@link Collector#combiner() Combiner} for multi-value maps. + *

    This method is only invoked if the {@link java.util.stream.Stream} is + * processed in {@linkplain java.util.stream.Stream#parallel() parallel}. + */ + private static MultiValueMap combiner(MultiValueMap map, MultiValueMap additions) { + map.addAll(additions); + return map; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationPredicates.java b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationPredicates.java new file mode 100644 index 0000000..9fafbc0 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationPredicates.java @@ -0,0 +1,171 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Predicate implementations that provide various test operations for + * {@link MergedAnnotation MergedAnnotations}. + * + * @author Phillip Webb + * @since 5.2 + */ +public abstract class MergedAnnotationPredicates { + + private MergedAnnotationPredicates() { + } + + + /** + * Create a new {@link Predicate} that evaluates to {@code true} if the name of the + * {@linkplain MergedAnnotation#getType() merged annotation type} is contained in + * the specified array. + * @param the annotation type + * @param typeNames the names that should be matched + * @return a {@link Predicate} to test the annotation type + */ + public static Predicate> typeIn(String... typeNames) { + return annotation -> ObjectUtils.containsElement(typeNames, annotation.getType().getName()); + } + + /** + * Create a new {@link Predicate} that evaluates to {@code true} if the + * {@linkplain MergedAnnotation#getType() merged annotation type} is contained in + * the specified array. + * @param the annotation type + * @param types the types that should be matched + * @return a {@link Predicate} to test the annotation type + */ + public static Predicate> typeIn(Class... types) { + return annotation -> ObjectUtils.containsElement(types, annotation.getType()); + } + + /** + * Create a new {@link Predicate} that evaluates to {@code true} if the + * {@linkplain MergedAnnotation#getType() merged annotation type} is contained in + * the specified collection. + * @param the annotation type + * @param types the type names or classes that should be matched + * @return a {@link Predicate} to test the annotation type + */ + public static Predicate> typeIn(Collection types) { + return annotation -> types.stream() + .map(type -> type instanceof Class ? ((Class) type).getName() : type.toString()) + .anyMatch(typeName -> typeName.equals(annotation.getType().getName())); + } + + /** + * Create a new stateful, single use {@link Predicate} that matches only + * the first run of an extracted value. For example, + * {@code MergedAnnotationPredicates.firstRunOf(MergedAnnotation::distance)} + * will match the first annotation, and any subsequent runs that have the + * same distance. + *

    NOTE: This predicate only matches the first run. Once the extracted + * value changes, the predicate always returns {@code false}. For example, + * if you have a set of annotations with distances {@code [1, 1, 2, 1]} then + * only the first two will match. + * @param valueExtractor function used to extract the value to check + * @return a {@link Predicate} that matches the first run of the extracted + * values + */ + public static Predicate> firstRunOf( + Function, ?> valueExtractor) { + + return new FirstRunOfPredicate<>(valueExtractor); + } + + /** + * Create a new stateful, single use {@link Predicate} that matches + * annotations that are unique based on the extracted key. For example + * {@code MergedAnnotationPredicates.unique(MergedAnnotation::getType)} will + * match the first time a unique type is encountered. + * @param keyExtractor function used to extract the key used to test for + * uniqueness + * @return a {@link Predicate} that matches a unique annotation based on the + * extracted key + */ + public static Predicate> unique( + Function, K> keyExtractor) { + + return new UniquePredicate<>(keyExtractor); + } + + + /** + * {@link Predicate} implementation used for + * {@link MergedAnnotationPredicates#firstRunOf(Function)}. + */ + private static class FirstRunOfPredicate implements Predicate> { + + private final Function, ?> valueExtractor; + + private boolean hasLastValue; + + @Nullable + private Object lastValue; + + FirstRunOfPredicate(Function, ?> valueExtractor) { + Assert.notNull(valueExtractor, "Value extractor must not be null"); + this.valueExtractor = valueExtractor; + } + + @Override + public boolean test(@Nullable MergedAnnotation annotation) { + if (!this.hasLastValue) { + this.hasLastValue = true; + this.lastValue = this.valueExtractor.apply(annotation); + } + Object value = this.valueExtractor.apply(annotation); + return ObjectUtils.nullSafeEquals(value, this.lastValue); + + } + } + + + /** + * {@link Predicate} implementation used for + * {@link MergedAnnotationPredicates#unique(Function)}. + */ + private static class UniquePredicate implements Predicate> { + + private final Function, K> keyExtractor; + + private final Set seen = new HashSet<>(); + + UniquePredicate(Function, K> keyExtractor) { + Assert.notNull(keyExtractor, "Key extractor must not be null"); + this.keyExtractor = keyExtractor; + } + + @Override + public boolean test(@Nullable MergedAnnotation annotation) { + K key = this.keyExtractor.apply(annotation); + return this.seen.add(key); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationSelector.java b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationSelector.java new file mode 100644 index 0000000..227a9ac --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationSelector.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; + +/** + * Strategy interface used to select between two {@link MergedAnnotation} + * instances. + * + * @author Phillip Webb + * @since 5.2 + * @param the annotation type + * @see MergedAnnotationSelectors + */ +@FunctionalInterface +public interface MergedAnnotationSelector { + + /** + * Determine if the existing annotation is known to be the best + * candidate and any subsequent selections may be skipped. + * @param annotation the annotation to check + * @return {@code true} if the annotation is known to be the best candidate + */ + default boolean isBestCandidate(MergedAnnotation annotation) { + return false; + } + + /** + * Select the annotation that should be used. + * @param existing an existing annotation returned from an earlier result + * @param candidate a candidate annotation that may be better suited + * @return the most appropriate annotation from the {@code existing} or + * {@code candidate} + */ + MergedAnnotation select(MergedAnnotation existing, MergedAnnotation candidate); + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationSelectors.java b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationSelectors.java new file mode 100644 index 0000000..0948161 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationSelectors.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.util.function.Predicate; + +/** + * {@link MergedAnnotationSelector} implementations that provide various options + * for {@link MergedAnnotation} instances. + * + * @author Phillip Webb + * @since 5.2 + * @see MergedAnnotations#get(Class, Predicate, MergedAnnotationSelector) + * @see MergedAnnotations#get(String, Predicate, MergedAnnotationSelector) + */ +public abstract class MergedAnnotationSelectors { + + private static final MergedAnnotationSelector NEAREST = new Nearest(); + + private static final MergedAnnotationSelector FIRST_DIRECTLY_DECLARED = new FirstDirectlyDeclared(); + + + private MergedAnnotationSelectors() { + } + + + /** + * Select the nearest annotation, i.e. the one with the lowest distance. + * @return a selector that picks the annotation with the lowest distance + */ + @SuppressWarnings("unchecked") + public static MergedAnnotationSelector nearest() { + return (MergedAnnotationSelector) NEAREST; + } + + /** + * Select the first directly declared annotation when possible. If no direct + * annotations are declared then the nearest annotation is selected. + * @return a selector that picks the first directly declared annotation whenever possible + */ + @SuppressWarnings("unchecked") + public static MergedAnnotationSelector firstDirectlyDeclared() { + return (MergedAnnotationSelector) FIRST_DIRECTLY_DECLARED; + } + + + /** + * {@link MergedAnnotationSelector} to select the nearest annotation. + */ + private static class Nearest implements MergedAnnotationSelector { + + @Override + public boolean isBestCandidate(MergedAnnotation annotation) { + return annotation.getDistance() == 0; + } + + @Override + public MergedAnnotation select( + MergedAnnotation existing, MergedAnnotation candidate) { + + if (candidate.getDistance() < existing.getDistance()) { + return candidate; + } + return existing; + } + + } + + + /** + * {@link MergedAnnotationSelector} to select the first directly declared + * annotation. + */ + private static class FirstDirectlyDeclared implements MergedAnnotationSelector { + + @Override + public boolean isBestCandidate(MergedAnnotation annotation) { + return annotation.getDistance() == 0; + } + + @Override + public MergedAnnotation select( + MergedAnnotation existing, MergedAnnotation candidate) { + + if (existing.getDistance() > 0 && candidate.getDistance() == 0) { + return candidate; + } + return existing; + } + + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java new file mode 100644 index 0000000..55dff90 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java @@ -0,0 +1,489 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Inherited; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Provides access to a collection of merged annotations, usually obtained + * from a source such as a {@link Class} or {@link Method}. + * + *

    Each merged annotation represents a view where the attribute values may be + * "merged" from different source values, typically: + * + *

    + * + *

    For example, a {@code @PostMapping} annotation might be defined as follows: + * + *

    + * @Retention(RetentionPolicy.RUNTIME)
    + * @RequestMapping(method = RequestMethod.POST)
    + * public @interface PostMapping {
    + *
    + *     @AliasFor(attribute = "path")
    + *     String[] value() default {};
    + *
    + *     @AliasFor(attribute = "value")
    + *     String[] path() default {};
    + * }
    + * 
    + * + *

    If a method is annotated with {@code @PostMapping("/home")} it will contain + * merged annotations for both {@code @PostMapping} and the meta-annotation + * {@code @RequestMapping}. The merged view of the {@code @RequestMapping} + * annotation will contain the following attributes: + * + *

    + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    NameValueSource
    value"/home"Declared in {@code @PostMapping}
    path"/home"Explicit {@code @AliasFor}
    methodRequestMethod.POSTDeclared in meta-annotation
    + * + *

    {@link MergedAnnotations} can be obtained {@linkplain #from(AnnotatedElement) + * from} any Java {@link AnnotatedElement}. They may also be used for sources that + * don't use reflection (such as those that directly parse bytecode). + * + *

    Different {@linkplain SearchStrategy search strategies} can be used to locate + * related source elements that contain the annotations to be aggregated. For + * example, {@link SearchStrategy#TYPE_HIERARCHY} will search both superclasses and + * implemented interfaces. + * + *

    From a {@link MergedAnnotations} instance you can either + * {@linkplain #get(String) get} a single annotation, or {@linkplain #stream() + * stream all annotations} or just those that match {@linkplain #stream(String) + * a specific type}. You can also quickly tell if an annotation + * {@linkplain #isPresent(String) is present}. + * + *

    Here are some typical examples: + * + *

    + * // is an annotation present or meta-present?
    + * mergedAnnotations.isPresent(ExampleAnnotation.class);
    + *
    + * // get the merged "value" attribute of ExampleAnnotation (either directly or
    + * // meta-present)
    + * mergedAnnotations.get(ExampleAnnotation.class).getString("value");
    + *
    + * // get all meta-annotations but no directly present annotations
    + * mergedAnnotations.stream().filter(MergedAnnotation::isMetaPresent);
    + *
    + * // get all ExampleAnnotation declarations (including any meta-annotations) and
    + * // print the merged "value" attributes
    + * mergedAnnotations.stream(ExampleAnnotation.class)
    + *     .map(mergedAnnotation -> mergedAnnotation.getString("value"))
    + *     .forEach(System.out::println);
    + * 
    + * + *

    NOTE: The {@code MergedAnnotations} API and its underlying model have + * been designed for composable annotations in Spring's common component model, + * with a focus on attribute aliasing and meta-annotation relationships. + * There is no support for retrieving plain Java annotations with this API; + * please use standard Java reflection or Spring's {@link AnnotationUtils} + * for simple annotation retrieval purposes. + * + * @author Phillip Webb + * @author Sam Brannen + * @since 5.2 + * @see MergedAnnotation + * @see MergedAnnotationCollectors + * @see MergedAnnotationPredicates + * @see MergedAnnotationSelectors + */ +public interface MergedAnnotations extends Iterable> { + + /** + * Determine if the specified annotation is either directly present or + * meta-present. + *

    Equivalent to calling {@code get(annotationType).isPresent()}. + * @param annotationType the annotation type to check + * @return {@code true} if the annotation is present + */ + boolean isPresent(Class annotationType); + + /** + * Determine if the specified annotation is either directly present or + * meta-present. + *

    Equivalent to calling {@code get(annotationType).isPresent()}. + * @param annotationType the fully qualified class name of the annotation type + * to check + * @return {@code true} if the annotation is present + */ + boolean isPresent(String annotationType); + + /** + * Determine if the specified annotation is directly present. + *

    Equivalent to calling {@code get(annotationType).isDirectlyPresent()}. + * @param annotationType the annotation type to check + * @return {@code true} if the annotation is directly present + */ + boolean isDirectlyPresent(Class annotationType); + + /** + * Determine if the specified annotation is directly present. + *

    Equivalent to calling {@code get(annotationType).isDirectlyPresent()}. + * @param annotationType the fully qualified class name of the annotation type + * to check + * @return {@code true} if the annotation is directly present + */ + boolean isDirectlyPresent(String annotationType); + + /** + * Get the {@linkplain MergedAnnotationSelectors#nearest() nearest} matching + * annotation or meta-annotation of the specified type, or + * {@link MergedAnnotation#missing()} if none is present. + * @param annotationType the annotation type to get + * @return a {@link MergedAnnotation} instance + */ + MergedAnnotation get(Class annotationType); + + /** + * Get the {@linkplain MergedAnnotationSelectors#nearest() nearest} matching + * annotation or meta-annotation of the specified type, or + * {@link MergedAnnotation#missing()} if none is present. + * @param annotationType the annotation type to get + * @param predicate a predicate that must match, or {@code null} if only + * type matching is required + * @return a {@link MergedAnnotation} instance + * @see MergedAnnotationPredicates + */ + MergedAnnotation get(Class annotationType, + @Nullable Predicate> predicate); + + /** + * Get a matching annotation or meta-annotation of the specified type, or + * {@link MergedAnnotation#missing()} if none is present. + * @param annotationType the annotation type to get + * @param predicate a predicate that must match, or {@code null} if only + * type matching is required + * @param selector a selector used to choose the most appropriate annotation + * within an aggregate, or {@code null} to select the + * {@linkplain MergedAnnotationSelectors#nearest() nearest} + * @return a {@link MergedAnnotation} instance + * @see MergedAnnotationPredicates + * @see MergedAnnotationSelectors + */ + MergedAnnotation get(Class annotationType, + @Nullable Predicate> predicate, + @Nullable MergedAnnotationSelector selector); + + /** + * Get the {@linkplain MergedAnnotationSelectors#nearest() nearest} matching + * annotation or meta-annotation of the specified type, or + * {@link MergedAnnotation#missing()} if none is present. + * @param annotationType the fully qualified class name of the annotation type + * to get + * @return a {@link MergedAnnotation} instance + */ + MergedAnnotation get(String annotationType); + + /** + * Get the {@linkplain MergedAnnotationSelectors#nearest() nearest} matching + * annotation or meta-annotation of the specified type, or + * {@link MergedAnnotation#missing()} if none is present. + * @param annotationType the fully qualified class name of the annotation type + * to get + * @param predicate a predicate that must match, or {@code null} if only + * type matching is required + * @return a {@link MergedAnnotation} instance + * @see MergedAnnotationPredicates + */ + MergedAnnotation get(String annotationType, + @Nullable Predicate> predicate); + + /** + * Get a matching annotation or meta-annotation of the specified type, or + * {@link MergedAnnotation#missing()} if none is present. + * @param annotationType the fully qualified class name of the annotation type + * to get + * @param predicate a predicate that must match, or {@code null} if only + * type matching is required + * @param selector a selector used to choose the most appropriate annotation + * within an aggregate, or {@code null} to select the + * {@linkplain MergedAnnotationSelectors#nearest() nearest} + * @return a {@link MergedAnnotation} instance + * @see MergedAnnotationPredicates + * @see MergedAnnotationSelectors + */ + MergedAnnotation get(String annotationType, + @Nullable Predicate> predicate, + @Nullable MergedAnnotationSelector selector); + + /** + * Stream all annotations and meta-annotations that match the specified + * type. The resulting stream follows the same ordering rules as + * {@link #stream()}. + * @param annotationType the annotation type to match + * @return a stream of matching annotations + */ + Stream> stream(Class annotationType); + + /** + * Stream all annotations and meta-annotations that match the specified + * type. The resulting stream follows the same ordering rules as + * {@link #stream()}. + * @param annotationType the fully qualified class name of the annotation type + * to match + * @return a stream of matching annotations + */ + Stream> stream(String annotationType); + + /** + * Stream all annotations and meta-annotations contained in this collection. + * The resulting stream is ordered first by the + * {@linkplain MergedAnnotation#getAggregateIndex() aggregate index} and then + * by the annotation distance (with the closest annotations first). This ordering + * means that, for most use-cases, the most suitable annotations appear + * earliest in the stream. + * @return a stream of annotations + */ + Stream> stream(); + + + /** + * Create a new {@link MergedAnnotations} instance containing all + * annotations and meta-annotations from the specified element. The + * resulting instance will not include any inherited annotations. If you + * want to include those as well you should use + * {@link #from(AnnotatedElement, SearchStrategy)} with an appropriate + * {@link SearchStrategy}. + * @param element the source element + * @return a {@link MergedAnnotations} instance containing the element's + * annotations + */ + static MergedAnnotations from(AnnotatedElement element) { + return from(element, SearchStrategy.DIRECT); + } + + /** + * Create a new {@link MergedAnnotations} instance containing all + * annotations and meta-annotations from the specified element and, + * depending on the {@link SearchStrategy}, related inherited elements. + * @param element the source element + * @param searchStrategy the search strategy to use + * @return a {@link MergedAnnotations} instance containing the merged + * element annotations + */ + static MergedAnnotations from(AnnotatedElement element, SearchStrategy searchStrategy) { + return from(element, searchStrategy, RepeatableContainers.standardRepeatables()); + } + + /** + * Create a new {@link MergedAnnotations} instance containing all + * annotations and meta-annotations from the specified element and, + * depending on the {@link SearchStrategy}, related inherited elements. + * @param element the source element + * @param searchStrategy the search strategy to use + * @param repeatableContainers the repeatable containers that may be used by + * the element annotations or the meta-annotations + * @return a {@link MergedAnnotations} instance containing the merged + * element annotations + */ + static MergedAnnotations from(AnnotatedElement element, SearchStrategy searchStrategy, + RepeatableContainers repeatableContainers) { + + return from(element, searchStrategy, repeatableContainers, AnnotationFilter.PLAIN); + } + + /** + * Create a new {@link MergedAnnotations} instance containing all + * annotations and meta-annotations from the specified element and, + * depending on the {@link SearchStrategy}, related inherited elements. + * @param element the source element + * @param searchStrategy the search strategy to use + * @param repeatableContainers the repeatable containers that may be used by + * the element annotations or the meta-annotations + * @param annotationFilter an annotation filter used to restrict the + * annotations considered + * @return a {@link MergedAnnotations} instance containing the merged + * annotations for the supplied element + */ + static MergedAnnotations from(AnnotatedElement element, SearchStrategy searchStrategy, + RepeatableContainers repeatableContainers, AnnotationFilter annotationFilter) { + + Assert.notNull(repeatableContainers, "RepeatableContainers must not be null"); + Assert.notNull(annotationFilter, "AnnotationFilter must not be null"); + return TypeMappedAnnotations.from(element, searchStrategy, repeatableContainers, annotationFilter); + } + + /** + * Create a new {@link MergedAnnotations} instance from the specified + * annotations. + * @param annotations the annotations to include + * @return a {@link MergedAnnotations} instance containing the annotations + * @see #from(Object, Annotation...) + */ + static MergedAnnotations from(Annotation... annotations) { + return from(annotations, annotations); + } + + /** + * Create a new {@link MergedAnnotations} instance from the specified + * annotations. + * @param source the source for the annotations. This source is used only + * for information and logging. It does not need to actually + * contain the specified annotations, and it will not be searched. + * @param annotations the annotations to include + * @return a {@link MergedAnnotations} instance containing the annotations + * @see #from(Annotation...) + * @see #from(AnnotatedElement) + */ + static MergedAnnotations from(Object source, Annotation... annotations) { + return from(source, annotations, RepeatableContainers.standardRepeatables()); + } + + /** + * Create a new {@link MergedAnnotations} instance from the specified + * annotations. + * @param source the source for the annotations. This source is used only + * for information and logging. It does not need to actually + * contain the specified annotations, and it will not be searched. + * @param annotations the annotations to include + * @param repeatableContainers the repeatable containers that may be used by + * meta-annotations + * @return a {@link MergedAnnotations} instance containing the annotations + */ + static MergedAnnotations from(Object source, Annotation[] annotations, RepeatableContainers repeatableContainers) { + return from(source, annotations, repeatableContainers, AnnotationFilter.PLAIN); + } + + /** + * Create a new {@link MergedAnnotations} instance from the specified + * annotations. + * @param source the source for the annotations. This source is used only + * for information and logging. It does not need to actually + * contain the specified annotations, and it will not be searched. + * @param annotations the annotations to include + * @param repeatableContainers the repeatable containers that may be used by + * meta-annotations + * @param annotationFilter an annotation filter used to restrict the + * annotations considered + * @return a {@link MergedAnnotations} instance containing the annotations + */ + static MergedAnnotations from(Object source, Annotation[] annotations, + RepeatableContainers repeatableContainers, AnnotationFilter annotationFilter) { + + Assert.notNull(repeatableContainers, "RepeatableContainers must not be null"); + Assert.notNull(annotationFilter, "AnnotationFilter must not be null"); + return TypeMappedAnnotations.from(source, annotations, repeatableContainers, annotationFilter); + } + + /** + * Create a new {@link MergedAnnotations} instance from the specified + * collection of directly present annotations. This method allows a + * {@link MergedAnnotations} instance to be created from annotations that + * are not necessarily loaded using reflection. The provided annotations + * must all be {@link MergedAnnotation#isDirectlyPresent() directly present} + * and must have an {@link MergedAnnotation#getAggregateIndex() aggregate + * index} of {@code 0}. + *

    The resulting {@link MergedAnnotations} instance will contain both the + * specified annotations, and any meta-annotations that can be read using + * reflection. + * @param annotations the annotations to include + * @return a {@link MergedAnnotations} instance containing the annotations + * @see MergedAnnotation#of(ClassLoader, Object, Class, java.util.Map) + */ + static MergedAnnotations of(Collection> annotations) { + return MergedAnnotationsCollection.of(annotations); + } + + + /** + * Search strategies supported by + * {@link MergedAnnotations#from(AnnotatedElement, SearchStrategy)}. + * + *

    Each strategy creates a different set of aggregates that will be + * combined to create the final {@link MergedAnnotations}. + */ + enum SearchStrategy { + + /** + * Find only directly declared annotations, without considering + * {@link Inherited @Inherited} annotations and without searching + * superclasses or implemented interfaces. + */ + DIRECT, + + /** + * Find all directly declared annotations as well as any + * {@link Inherited @Inherited} superclass annotations. This strategy + * is only really useful when used with {@link Class} types since the + * {@link Inherited @Inherited} annotation is ignored for all other + * {@linkplain AnnotatedElement annotated elements}. This strategy does + * not search implemented interfaces. + */ + INHERITED_ANNOTATIONS, + + /** + * Find all directly declared and superclass annotations. This strategy + * is similar to {@link #INHERITED_ANNOTATIONS} except the annotations + * do not need to be meta-annotated with {@link Inherited @Inherited}. + * This strategy does not search implemented interfaces. + */ + SUPERCLASS, + + /** + * Perform a full search of the entire type hierarchy, including + * superclasses and implemented interfaces. Superclass annotations do + * not need to be meta-annotated with {@link Inherited @Inherited}. + */ + TYPE_HIERARCHY, + + /** + * Perform a full search of the entire type hierarchy on the source + * and any enclosing classes. This strategy is similar to + * {@link #TYPE_HIERARCHY} except that {@linkplain Class#getEnclosingClass() + * enclosing classes} are also searched. Superclass annotations do not + * need to be meta-annotated with {@link Inherited @Inherited}. When + * searching a {@link Method} source, this strategy is identical to + * {@link #TYPE_HIERARCHY}. + */ + TYPE_HIERARCHY_AND_ENCLOSING_CLASSES + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationsCollection.java b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationsCollection.java new file mode 100644 index 0000000..9fc6cb5 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationsCollection.java @@ -0,0 +1,317 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.Iterator; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * {@link MergedAnnotations} implementation backed by a {@link Collection} of + * {@link MergedAnnotation} instances that represent direct annotations. + * + * @author Phillip Webb + * @since 5.2 + * @see MergedAnnotations#of(Collection) + */ +final class MergedAnnotationsCollection implements MergedAnnotations { + + private final MergedAnnotation[] annotations; + + private final AnnotationTypeMappings[] mappings; + + + private MergedAnnotationsCollection(Collection> annotations) { + Assert.notNull(annotations, "Annotations must not be null"); + this.annotations = annotations.toArray(new MergedAnnotation[0]); + this.mappings = new AnnotationTypeMappings[this.annotations.length]; + for (int i = 0; i < this.annotations.length; i++) { + MergedAnnotation annotation = this.annotations[i]; + Assert.notNull(annotation, "Annotation must not be null"); + Assert.isTrue(annotation.isDirectlyPresent(), "Annotation must be directly present"); + Assert.isTrue(annotation.getAggregateIndex() == 0, "Annotation must have aggregate index of zero"); + this.mappings[i] = AnnotationTypeMappings.forAnnotationType(annotation.getType()); + } + } + + + @Override + public Iterator> iterator() { + return Spliterators.iterator(spliterator()); + } + + @Override + public Spliterator> spliterator() { + return spliterator(null); + } + + private Spliterator> spliterator(@Nullable Object annotationType) { + return new AnnotationsSpliterator<>(annotationType); + } + + @Override + public boolean isPresent(Class annotationType) { + return isPresent(annotationType, false); + } + + @Override + public boolean isPresent(String annotationType) { + return isPresent(annotationType, false); + } + + @Override + public boolean isDirectlyPresent(Class annotationType) { + return isPresent(annotationType, true); + } + + @Override + public boolean isDirectlyPresent(String annotationType) { + return isPresent(annotationType, true); + } + + private boolean isPresent(Object requiredType, boolean directOnly) { + for (MergedAnnotation annotation : this.annotations) { + Class type = annotation.getType(); + if (type == requiredType || type.getName().equals(requiredType)) { + return true; + } + } + if (!directOnly) { + for (AnnotationTypeMappings mappings : this.mappings) { + for (int i = 1; i < mappings.size(); i++) { + AnnotationTypeMapping mapping = mappings.get(i); + if (isMappingForType(mapping, requiredType)) { + return true; + } + } + } + } + return false; + } + + @Override + public MergedAnnotation get(Class annotationType) { + return get(annotationType, null, null); + } + + @Override + public MergedAnnotation get(Class annotationType, + @Nullable Predicate> predicate) { + + return get(annotationType, predicate, null); + } + + @Override + public MergedAnnotation get(Class annotationType, + @Nullable Predicate> predicate, + @Nullable MergedAnnotationSelector selector) { + + MergedAnnotation result = find(annotationType, predicate, selector); + return (result != null ? result : MergedAnnotation.missing()); + } + + @Override + public MergedAnnotation get(String annotationType) { + return get(annotationType, null, null); + } + + @Override + public MergedAnnotation get(String annotationType, + @Nullable Predicate> predicate) { + + return get(annotationType, predicate, null); + } + + @Override + public MergedAnnotation get(String annotationType, + @Nullable Predicate> predicate, + @Nullable MergedAnnotationSelector selector) { + + MergedAnnotation result = find(annotationType, predicate, selector); + return (result != null ? result : MergedAnnotation.missing()); + } + + @SuppressWarnings("unchecked") + @Nullable + private MergedAnnotation find(Object requiredType, + @Nullable Predicate> predicate, + @Nullable MergedAnnotationSelector selector) { + + if (selector == null) { + selector = MergedAnnotationSelectors.nearest(); + } + + MergedAnnotation result = null; + for (int i = 0; i < this.annotations.length; i++) { + MergedAnnotation root = this.annotations[i]; + AnnotationTypeMappings mappings = this.mappings[i]; + for (int mappingIndex = 0; mappingIndex < mappings.size(); mappingIndex++) { + AnnotationTypeMapping mapping = mappings.get(mappingIndex); + if (!isMappingForType(mapping, requiredType)) { + continue; + } + MergedAnnotation candidate = (mappingIndex == 0 ? (MergedAnnotation) root : + TypeMappedAnnotation.createIfPossible(mapping, root, IntrospectionFailureLogger.INFO)); + if (candidate != null && (predicate == null || predicate.test(candidate))) { + if (selector.isBestCandidate(candidate)) { + return candidate; + } + result = (result != null ? selector.select(result, candidate) : candidate); + } + } + } + return result; + } + + @Override + public Stream> stream(Class annotationType) { + return StreamSupport.stream(spliterator(annotationType), false); + } + + @Override + public Stream> stream(String annotationType) { + return StreamSupport.stream(spliterator(annotationType), false); + } + + @Override + public Stream> stream() { + return StreamSupport.stream(spliterator(), false); + } + + private static boolean isMappingForType(AnnotationTypeMapping mapping, @Nullable Object requiredType) { + if (requiredType == null) { + return true; + } + Class actualType = mapping.getAnnotationType(); + return (actualType == requiredType || actualType.getName().equals(requiredType)); + } + + static MergedAnnotations of(Collection> annotations) { + Assert.notNull(annotations, "Annotations must not be null"); + if (annotations.isEmpty()) { + return TypeMappedAnnotations.NONE; + } + return new MergedAnnotationsCollection(annotations); + } + + + private class AnnotationsSpliterator implements Spliterator> { + + @Nullable + private Object requiredType; + + private final int[] mappingCursors; + + public AnnotationsSpliterator(@Nullable Object requiredType) { + this.mappingCursors = new int[annotations.length]; + this.requiredType = requiredType; + } + + @Override + public boolean tryAdvance(Consumer> action) { + int lowestDistance = Integer.MAX_VALUE; + int annotationResult = -1; + for (int annotationIndex = 0; annotationIndex < annotations.length; annotationIndex++) { + AnnotationTypeMapping mapping = getNextSuitableMapping(annotationIndex); + if (mapping != null && mapping.getDistance() < lowestDistance) { + annotationResult = annotationIndex; + lowestDistance = mapping.getDistance(); + } + if (lowestDistance == 0) { + break; + } + } + if (annotationResult != -1) { + MergedAnnotation mergedAnnotation = createMergedAnnotationIfPossible( + annotationResult, this.mappingCursors[annotationResult]); + this.mappingCursors[annotationResult]++; + if (mergedAnnotation == null) { + return tryAdvance(action); + } + action.accept(mergedAnnotation); + return true; + } + return false; + } + + @Nullable + private AnnotationTypeMapping getNextSuitableMapping(int annotationIndex) { + AnnotationTypeMapping mapping; + do { + mapping = getMapping(annotationIndex, this.mappingCursors[annotationIndex]); + if (mapping != null && isMappingForType(mapping, this.requiredType)) { + return mapping; + } + this.mappingCursors[annotationIndex]++; + } + while (mapping != null); + return null; + } + + @Nullable + private AnnotationTypeMapping getMapping(int annotationIndex, int mappingIndex) { + AnnotationTypeMappings mappings = MergedAnnotationsCollection.this.mappings[annotationIndex]; + return (mappingIndex < mappings.size() ? mappings.get(mappingIndex) : null); + } + + @Nullable + @SuppressWarnings("unchecked") + private MergedAnnotation createMergedAnnotationIfPossible(int annotationIndex, int mappingIndex) { + MergedAnnotation root = annotations[annotationIndex]; + if (mappingIndex == 0) { + return (MergedAnnotation) root; + } + IntrospectionFailureLogger logger = (this.requiredType != null ? + IntrospectionFailureLogger.INFO : IntrospectionFailureLogger.DEBUG); + return TypeMappedAnnotation.createIfPossible( + mappings[annotationIndex].get(mappingIndex), root, logger); + } + + @Override + @Nullable + public Spliterator> trySplit() { + return null; + } + + @Override + public long estimateSize() { + int size = 0; + for (int i = 0; i < annotations.length; i++) { + AnnotationTypeMappings mappings = MergedAnnotationsCollection.this.mappings[i]; + int numberOfMappings = mappings.size(); + numberOfMappings -= Math.min(this.mappingCursors[i], mappings.size()); + size += numberOfMappings; + } + return size; + } + + @Override + public int characteristics() { + return NONNULL | IMMUTABLE; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MissingMergedAnnotation.java b/spring-core/src/main/java/org/springframework/core/annotation/MissingMergedAnnotation.java new file mode 100644 index 0000000..caa764c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/MissingMergedAnnotation.java @@ -0,0 +1,175 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; + +import org.springframework.lang.Nullable; + +/** + * An {@link AbstractMergedAnnotation} used as the implementation of + * {@link MergedAnnotation#missing()}. + * + * @author Phillip Webb + * @author Juergen Hoeller + * @since 5.2 + * @param the annotation type + */ +final class MissingMergedAnnotation extends AbstractMergedAnnotation { + + private static final MissingMergedAnnotation INSTANCE = new MissingMergedAnnotation<>(); + + + private MissingMergedAnnotation() { + } + + + @Override + public Class getType() { + throw new NoSuchElementException("Unable to get type for missing annotation"); + } + + @Override + public boolean isPresent() { + return false; + } + + @Override + @Nullable + public Object getSource() { + return null; + } + + @Override + @Nullable + public MergedAnnotation getMetaSource() { + return null; + } + + @Override + public MergedAnnotation getRoot() { + return this; + } + + @Override + public List> getMetaTypes() { + return Collections.emptyList(); + } + + @Override + public int getDistance() { + return -1; + } + + @Override + public int getAggregateIndex() { + return -1; + } + + @Override + public boolean hasNonDefaultValue(String attributeName) { + throw new NoSuchElementException( + "Unable to check non-default value for missing annotation"); + } + + @Override + public boolean hasDefaultValue(String attributeName) { + throw new NoSuchElementException( + "Unable to check default value for missing annotation"); + } + + @Override + public Optional getValue(String attributeName, Class type) { + return Optional.empty(); + } + + @Override + public Optional getDefaultValue(@Nullable String attributeName, Class type) { + return Optional.empty(); + } + + @Override + public MergedAnnotation filterAttributes(Predicate predicate) { + return this; + } + + @Override + public MergedAnnotation withNonMergedAttributes() { + return this; + } + + @Override + public AnnotationAttributes asAnnotationAttributes(Adapt... adaptations) { + return new AnnotationAttributes(); + } + + @Override + public Map asMap(Adapt... adaptations) { + return Collections.emptyMap(); + } + + @Override + public > T asMap(Function, T> factory, Adapt... adaptations) { + return factory.apply(this); + } + + @Override + public String toString() { + return "(missing)"; + } + + @Override + public MergedAnnotation getAnnotation(String attributeName, + Class type) throws NoSuchElementException { + + throw new NoSuchElementException( + "Unable to get attribute value for missing annotation"); + } + + @Override + public MergedAnnotation[] getAnnotationArray( + String attributeName, Class type) throws NoSuchElementException { + + throw new NoSuchElementException( + "Unable to get attribute value for missing annotation"); + } + + @Override + protected T getAttributeValue(String attributeName, Class type) { + throw new NoSuchElementException( + "Unable to get attribute value for missing annotation"); + } + + @Override + protected A createSynthesized() { + throw new NoSuchElementException("Unable to synthesize missing annotation"); + } + + + @SuppressWarnings("unchecked") + static MergedAnnotation getInstance() { + return (MergedAnnotation) INSTANCE; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/Order.java b/spring-core/src/main/java/org/springframework/core/annotation/Order.java new file mode 100644 index 0000000..a60a439 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/Order.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.Ordered; + +/** + * {@code @Order} defines the sort order for an annotated component. + * + *

    The {@link #value} is optional and represents an order value as defined in the + * {@link Ordered} interface. Lower values have higher priority. The default value is + * {@code Ordered.LOWEST_PRECEDENCE}, indicating lowest priority (losing to any other + * specified order value). + * + *

    NOTE: Since Spring 4.0, annotation-based ordering is supported for many + * kinds of components in Spring, even for collection injection where the order values + * of the target components are taken into account (either from their target class or + * from their {@code @Bean} method). While such order values may influence priorities + * at injection points, please be aware that they do not influence singleton startup + * order which is an orthogonal concern determined by dependency relationships and + * {@code @DependsOn} declarations (influencing a runtime-determined dependency graph). + * + *

    Since Spring 4.1, the standard {@link javax.annotation.Priority} annotation + * can be used as a drop-in replacement for this annotation in ordering scenarios. + * Note that {@code @Priority} may have additional semantics when a single element + * has to be picked (see {@link AnnotationAwareOrderComparator#getPriority}). + * + *

    Alternatively, order values may also be determined on a per-instance basis + * through the {@link Ordered} interface, allowing for configuration-determined + * instance values instead of hard-coded values attached to a particular class. + * + *

    Consult the javadoc for {@link org.springframework.core.OrderComparator + * OrderComparator} for details on the sort semantics for non-ordered objects. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.core.Ordered + * @see AnnotationAwareOrderComparator + * @see OrderUtils + * @see javax.annotation.Priority + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD}) +@Documented +public @interface Order { + + /** + * The order value. + *

    Default is {@link Ordered#LOWEST_PRECEDENCE}. + * @see Ordered#getOrder() + */ + int value() default Ordered.LOWEST_PRECEDENCE; + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/OrderUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/OrderUtils.java new file mode 100644 index 0000000..25acbc8 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/OrderUtils.java @@ -0,0 +1,146 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.reflect.AnnotatedElement; +import java.util.Map; + +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; +import org.springframework.lang.Nullable; +import org.springframework.util.ConcurrentReferenceHashMap; + +/** + * General utility for determining the order of an object based on its type declaration. + * Handles Spring's {@link Order} annotation as well as {@link javax.annotation.Priority}. + * + * @author Stephane Nicoll + * @author Juergen Hoeller + * @since 4.1 + * @see Order + * @see javax.annotation.Priority + */ +public abstract class OrderUtils { + + /** Cache marker for a non-annotated Class. */ + private static final Object NOT_ANNOTATED = new Object(); + + private static final String JAVAX_PRIORITY_ANNOTATION = "javax.annotation.Priority"; + + /** Cache for @Order value (or NOT_ANNOTATED marker) per Class. */ + private static final Map orderCache = new ConcurrentReferenceHashMap<>(64); + + + /** + * Return the order on the specified {@code type}, or the specified + * default value if none can be found. + *

    Takes care of {@link Order @Order} and {@code @javax.annotation.Priority}. + * @param type the type to handle + * @return the priority value, or the specified default order if none can be found + * @since 5.0 + * @see #getPriority(Class) + */ + public static int getOrder(Class type, int defaultOrder) { + Integer order = getOrder(type); + return (order != null ? order : defaultOrder); + } + + /** + * Return the order on the specified {@code type}, or the specified + * default value if none can be found. + *

    Takes care of {@link Order @Order} and {@code @javax.annotation.Priority}. + * @param type the type to handle + * @return the priority value, or the specified default order if none can be found + * @see #getPriority(Class) + */ + @Nullable + public static Integer getOrder(Class type, @Nullable Integer defaultOrder) { + Integer order = getOrder(type); + return (order != null ? order : defaultOrder); + } + + /** + * Return the order on the specified {@code type}. + *

    Takes care of {@link Order @Order} and {@code @javax.annotation.Priority}. + * @param type the type to handle + * @return the order value, or {@code null} if none can be found + * @see #getPriority(Class) + */ + @Nullable + public static Integer getOrder(Class type) { + return getOrder((AnnotatedElement) type); + } + + /** + * Return the order declared on the specified {@code element}. + *

    Takes care of {@link Order @Order} and {@code @javax.annotation.Priority}. + * @param element the annotated element (e.g. type or method) + * @return the order value, or {@code null} if none can be found + * @since 5.3 + */ + @Nullable + public static Integer getOrder(AnnotatedElement element) { + return getOrderFromAnnotations(element, MergedAnnotations.from(element, SearchStrategy.TYPE_HIERARCHY)); + } + + /** + * Return the order from the specified annotations collection. + *

    Takes care of {@link Order @Order} and + * {@code @javax.annotation.Priority}. + * @param element the source element + * @param annotations the annotation to consider + * @return the order value, or {@code null} if none can be found + */ + @Nullable + static Integer getOrderFromAnnotations(AnnotatedElement element, MergedAnnotations annotations) { + if (!(element instanceof Class)) { + return findOrder(annotations); + } + Object cached = orderCache.get(element); + if (cached != null) { + return (cached instanceof Integer ? (Integer) cached : null); + } + Integer result = findOrder(annotations); + orderCache.put(element, result != null ? result : NOT_ANNOTATED); + return result; + } + + @Nullable + private static Integer findOrder(MergedAnnotations annotations) { + MergedAnnotation orderAnnotation = annotations.get(Order.class); + if (orderAnnotation.isPresent()) { + return orderAnnotation.getInt(MergedAnnotation.VALUE); + } + MergedAnnotation priorityAnnotation = annotations.get(JAVAX_PRIORITY_ANNOTATION); + if (priorityAnnotation.isPresent()) { + return priorityAnnotation.getInt(MergedAnnotation.VALUE); + } + return null; + } + + /** + * Return the value of the {@code javax.annotation.Priority} annotation + * declared on the specified type, or {@code null} if none. + * @param type the type to handle + * @return the priority value if the annotation is declared, or {@code null} if none + */ + @Nullable + public static Integer getPriority(Class type) { + return MergedAnnotations.from(type, SearchStrategy.TYPE_HIERARCHY).get(JAVAX_PRIORITY_ANNOTATION) + .getValue(MergedAnnotation.VALUE, Integer.class).orElse(null); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/PackagesAnnotationFilter.java b/spring-core/src/main/java/org/springframework/core/annotation/PackagesAnnotationFilter.java new file mode 100644 index 0000000..c050a69 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/PackagesAnnotationFilter.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.util.Arrays; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * {@link AnnotationFilter} implementation used for + * {@link AnnotationFilter#packages(String...)}. + * + * @author Phillip Webb + * @since 5.2 + */ +final class PackagesAnnotationFilter implements AnnotationFilter { + + private final String[] prefixes; + + private final int hashCode; + + + PackagesAnnotationFilter(String... packages) { + Assert.notNull(packages, "Packages array must not be null"); + this.prefixes = new String[packages.length]; + for (int i = 0; i < packages.length; i++) { + String pkg = packages[i]; + Assert.hasText(pkg, "Packages array must not have empty elements"); + this.prefixes[i] = pkg + "."; + } + Arrays.sort(this.prefixes); + this.hashCode = Arrays.hashCode(this.prefixes); + } + + + @Override + public boolean matches(String annotationType) { + for (String prefix : this.prefixes) { + if (annotationType.startsWith(prefix)) { + return true; + } + } + return false; + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + return Arrays.equals(this.prefixes, ((PackagesAnnotationFilter) other).prefixes); + } + + @Override + public int hashCode() { + return this.hashCode; + } + + @Override + public String toString() { + return "Packages annotation filter: " + + StringUtils.arrayToCommaDelimitedString(this.prefixes); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java b/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java new file mode 100644 index 0000000..a235a58 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java @@ -0,0 +1,274 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Repeatable; +import java.lang.reflect.Method; +import java.util.Map; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; + +/** + * Strategy used to determine annotations that act as containers for other + * annotations. The {@link #standardRepeatables()} method provides a default + * strategy that respects Java's {@link Repeatable @Repeatable} support and + * should be suitable for most situations. + * + *

    The {@link #of} method can be used to register relationships for + * annotations that do not wish to use {@link Repeatable @Repeatable}. + * + *

    To completely disable repeatable support use {@link #none()}. + * + * @author Phillip Webb + * @since 5.2 + */ +public abstract class RepeatableContainers { + + @Nullable + private final RepeatableContainers parent; + + + private RepeatableContainers(@Nullable RepeatableContainers parent) { + this.parent = parent; + } + + + /** + * Add an additional explicit relationship between a contained and + * repeatable annotation. + * @param container the container type + * @param repeatable the contained repeatable type + * @return a new {@link RepeatableContainers} instance + */ + public RepeatableContainers and(Class container, + Class repeatable) { + + return new ExplicitRepeatableContainer(this, repeatable, container); + } + + @Nullable + Annotation[] findRepeatedAnnotations(Annotation annotation) { + if (this.parent == null) { + return null; + } + return this.parent.findRepeatedAnnotations(annotation); + } + + + @Override + public boolean equals(@Nullable Object other) { + if (other == this) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + return ObjectUtils.nullSafeEquals(this.parent, ((RepeatableContainers) other).parent); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.parent); + } + + + /** + * Create a {@link RepeatableContainers} instance that searches using Java's + * {@link Repeatable @Repeatable} annotation. + * @return a {@link RepeatableContainers} instance + */ + public static RepeatableContainers standardRepeatables() { + return StandardRepeatableContainers.INSTANCE; + } + + /** + * Create a {@link RepeatableContainers} instance that uses a defined + * container and repeatable type. + * @param repeatable the contained repeatable annotation + * @param container the container annotation or {@code null}. If specified, + * this annotation must declare a {@code value} attribute returning an array + * of repeatable annotations. If not specified, the container will be + * deduced by inspecting the {@code @Repeatable} annotation on + * {@code repeatable}. + * @return a {@link RepeatableContainers} instance + */ + public static RepeatableContainers of( + Class repeatable, @Nullable Class container) { + + return new ExplicitRepeatableContainer(null, repeatable, container); + } + + /** + * Create a {@link RepeatableContainers} instance that does not expand any + * repeatable annotations. + * @return a {@link RepeatableContainers} instance + */ + public static RepeatableContainers none() { + return NoRepeatableContainers.INSTANCE; + } + + + /** + * Standard {@link RepeatableContainers} implementation that searches using + * Java's {@link Repeatable @Repeatable} annotation. + */ + private static class StandardRepeatableContainers extends RepeatableContainers { + + private static final Map, Object> cache = new ConcurrentReferenceHashMap<>(); + + private static final Object NONE = new Object(); + + private static StandardRepeatableContainers INSTANCE = new StandardRepeatableContainers(); + + StandardRepeatableContainers() { + super(null); + } + + @Override + @Nullable + Annotation[] findRepeatedAnnotations(Annotation annotation) { + Method method = getRepeatedAnnotationsMethod(annotation.annotationType()); + if (method != null) { + return (Annotation[]) ReflectionUtils.invokeMethod(method, annotation); + } + return super.findRepeatedAnnotations(annotation); + } + + @Nullable + private static Method getRepeatedAnnotationsMethod(Class annotationType) { + Object result = cache.computeIfAbsent(annotationType, + StandardRepeatableContainers::computeRepeatedAnnotationsMethod); + return (result != NONE ? (Method) result : null); + } + + private static Object computeRepeatedAnnotationsMethod(Class annotationType) { + AttributeMethods methods = AttributeMethods.forAnnotationType(annotationType); + if (methods.hasOnlyValueAttribute()) { + Method method = methods.get(0); + Class returnType = method.getReturnType(); + if (returnType.isArray()) { + Class componentType = returnType.getComponentType(); + if (Annotation.class.isAssignableFrom(componentType) && + componentType.isAnnotationPresent(Repeatable.class)) { + return method; + } + } + } + return NONE; + } + } + + + /** + * A single explicit mapping. + */ + private static class ExplicitRepeatableContainer extends RepeatableContainers { + + private final Class repeatable; + + private final Class container; + + private final Method valueMethod; + + ExplicitRepeatableContainer(@Nullable RepeatableContainers parent, + Class repeatable, @Nullable Class container) { + + super(parent); + Assert.notNull(repeatable, "Repeatable must not be null"); + if (container == null) { + container = deduceContainer(repeatable); + } + Method valueMethod = AttributeMethods.forAnnotationType(container).get(MergedAnnotation.VALUE); + try { + if (valueMethod == null) { + throw new NoSuchMethodException("No value method found"); + } + Class returnType = valueMethod.getReturnType(); + if (!returnType.isArray() || returnType.getComponentType() != repeatable) { + throw new AnnotationConfigurationException("Container type [" + + container.getName() + + "] must declare a 'value' attribute for an array of type [" + + repeatable.getName() + "]"); + } + } + catch (AnnotationConfigurationException ex) { + throw ex; + } + catch (Throwable ex) { + throw new AnnotationConfigurationException( + "Invalid declaration of container type [" + container.getName() + + "] for repeatable annotation [" + repeatable.getName() + "]", + ex); + } + this.repeatable = repeatable; + this.container = container; + this.valueMethod = valueMethod; + } + + private Class deduceContainer(Class repeatable) { + Repeatable annotation = repeatable.getAnnotation(Repeatable.class); + Assert.notNull(annotation, () -> "Annotation type must be a repeatable annotation: " + + "failed to resolve container type for " + repeatable.getName()); + return annotation.value(); + } + + @Override + @Nullable + Annotation[] findRepeatedAnnotations(Annotation annotation) { + if (this.container.isAssignableFrom(annotation.annotationType())) { + return (Annotation[]) ReflectionUtils.invokeMethod(this.valueMethod, annotation); + } + return super.findRepeatedAnnotations(annotation); + } + + @Override + public boolean equals(@Nullable Object other) { + if (!super.equals(other)) { + return false; + } + ExplicitRepeatableContainer otherErc = (ExplicitRepeatableContainer) other; + return (this.container.equals(otherErc.container) && this.repeatable.equals(otherErc.repeatable)); + } + + @Override + public int hashCode() { + int hashCode = super.hashCode(); + hashCode = 31 * hashCode + this.container.hashCode(); + hashCode = 31 * hashCode + this.repeatable.hashCode(); + return hashCode; + } + } + + + /** + * No repeatable containers. + */ + private static class NoRepeatableContainers extends RepeatableContainers { + + private static NoRepeatableContainers INSTANCE = new NoRepeatableContainers(); + + NoRepeatableContainers() { + super(null); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedAnnotation.java b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedAnnotation.java new file mode 100644 index 0000000..6fd52f7 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedAnnotation.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +/** + * Marker interface implemented by synthesized annotation proxies. + * + *

    Used to detect whether an annotation has already been synthesized. + * + * @author Sam Brannen + * @since 4.2 + */ +public interface SynthesizedAnnotation { +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedMergedAnnotationInvocationHandler.java b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedMergedAnnotationInvocationHandler.java new file mode 100644 index 0000000..16a5e82 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedMergedAnnotationInvocationHandler.java @@ -0,0 +1,287 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Array; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; + +/** + * {@link InvocationHandler} for an {@link Annotation} that Spring has + * synthesized (i.e. wrapped in a dynamic proxy) with additional + * functionality such as attribute alias handling. + * + * @author Sam Brannen + * @author Phillip Webb + * @since 5.2 + * @param the annotation type + * @see Annotation + * @see AnnotationUtils#synthesizeAnnotation(Annotation, AnnotatedElement) + */ +final class SynthesizedMergedAnnotationInvocationHandler implements InvocationHandler { + + private final MergedAnnotation annotation; + + private final Class type; + + private final AttributeMethods attributes; + + private final Map valueCache = new ConcurrentHashMap<>(8); + + @Nullable + private volatile Integer hashCode; + + @Nullable + private volatile String string; + + + private SynthesizedMergedAnnotationInvocationHandler(MergedAnnotation annotation, Class type) { + Assert.notNull(annotation, "MergedAnnotation must not be null"); + Assert.notNull(type, "Type must not be null"); + Assert.isTrue(type.isAnnotation(), "Type must be an annotation"); + this.annotation = annotation; + this.type = type; + this.attributes = AttributeMethods.forAnnotationType(type); + } + + + @Override + public Object invoke(Object proxy, Method method, Object[] args) { + if (ReflectionUtils.isEqualsMethod(method)) { + return annotationEquals(args[0]); + } + if (ReflectionUtils.isHashCodeMethod(method)) { + return annotationHashCode(); + } + if (ReflectionUtils.isToStringMethod(method)) { + return annotationToString(); + } + if (isAnnotationTypeMethod(method)) { + return this.type; + } + if (this.attributes.indexOf(method.getName()) != -1) { + return getAttributeValue(method); + } + throw new AnnotationConfigurationException(String.format( + "Method [%s] is unsupported for synthesized annotation type [%s]", method, this.type)); + } + + private boolean isAnnotationTypeMethod(Method method) { + return (method.getName().equals("annotationType") && method.getParameterCount() == 0); + } + + /** + * See {@link Annotation#equals(Object)} for a definition of the required algorithm. + * @param other the other object to compare against + */ + private boolean annotationEquals(Object other) { + if (this == other) { + return true; + } + if (!this.type.isInstance(other)) { + return false; + } + for (int i = 0; i < this.attributes.size(); i++) { + Method attribute = this.attributes.get(i); + Object thisValue = getAttributeValue(attribute); + Object otherValue = ReflectionUtils.invokeMethod(attribute, other); + if (!ObjectUtils.nullSafeEquals(thisValue, otherValue)) { + return false; + } + } + return true; + } + + /** + * See {@link Annotation#hashCode()} for a definition of the required algorithm. + */ + private int annotationHashCode() { + Integer hashCode = this.hashCode; + if (hashCode == null) { + hashCode = computeHashCode(); + this.hashCode = hashCode; + } + return hashCode; + } + + private Integer computeHashCode() { + int hashCode = 0; + for (int i = 0; i < this.attributes.size(); i++) { + Method attribute = this.attributes.get(i); + Object value = getAttributeValue(attribute); + hashCode += (127 * attribute.getName().hashCode()) ^ getValueHashCode(value); + } + return hashCode; + } + + private int getValueHashCode(Object value) { + // Use Arrays.hashCode(...) since Spring's ObjectUtils doesn't comply + // with the requirements specified in Annotation#hashCode(). + if (value instanceof boolean[]) { + return Arrays.hashCode((boolean[]) value); + } + if (value instanceof byte[]) { + return Arrays.hashCode((byte[]) value); + } + if (value instanceof char[]) { + return Arrays.hashCode((char[]) value); + } + if (value instanceof double[]) { + return Arrays.hashCode((double[]) value); + } + if (value instanceof float[]) { + return Arrays.hashCode((float[]) value); + } + if (value instanceof int[]) { + return Arrays.hashCode((int[]) value); + } + if (value instanceof long[]) { + return Arrays.hashCode((long[]) value); + } + if (value instanceof short[]) { + return Arrays.hashCode((short[]) value); + } + if (value instanceof Object[]) { + return Arrays.hashCode((Object[]) value); + } + return value.hashCode(); + } + + private String annotationToString() { + String string = this.string; + if (string == null) { + StringBuilder builder = new StringBuilder("@").append(this.type.getName()).append("("); + for (int i = 0; i < this.attributes.size(); i++) { + Method attribute = this.attributes.get(i); + if (i > 0) { + builder.append(", "); + } + builder.append(attribute.getName()); + builder.append("="); + builder.append(toString(getAttributeValue(attribute))); + } + builder.append(")"); + string = builder.toString(); + this.string = string; + } + return string; + } + + private String toString(Object value) { + if (value instanceof Class) { + return ((Class) value).getName(); + } + if (value.getClass().isArray()) { + StringBuilder builder = new StringBuilder("["); + for (int i = 0; i < Array.getLength(value); i++) { + if (i > 0) { + builder.append(", "); + } + builder.append(toString(Array.get(value, i))); + } + builder.append("]"); + return builder.toString(); + } + return String.valueOf(value); + } + + private Object getAttributeValue(Method method) { + Object value = this.valueCache.computeIfAbsent(method.getName(), attributeName -> { + Class type = ClassUtils.resolvePrimitiveIfNecessary(method.getReturnType()); + return this.annotation.getValue(attributeName, type).orElseThrow( + () -> new NoSuchElementException("No value found for attribute named '" + attributeName + + "' in merged annotation " + this.annotation.getType().getName())); + }); + + // Clone non-empty arrays so that users cannot alter the contents of values in our cache. + if (value.getClass().isArray() && Array.getLength(value) > 0) { + value = cloneArray(value); + } + + return value; + } + + /** + * Clone the provided array, ensuring that the original component type is retained. + * @param array the array to clone + */ + private Object cloneArray(Object array) { + if (array instanceof boolean[]) { + return ((boolean[]) array).clone(); + } + if (array instanceof byte[]) { + return ((byte[]) array).clone(); + } + if (array instanceof char[]) { + return ((char[]) array).clone(); + } + if (array instanceof double[]) { + return ((double[]) array).clone(); + } + if (array instanceof float[]) { + return ((float[]) array).clone(); + } + if (array instanceof int[]) { + return ((int[]) array).clone(); + } + if (array instanceof long[]) { + return ((long[]) array).clone(); + } + if (array instanceof short[]) { + return ((short[]) array).clone(); + } + + // else + return ((Object[]) array).clone(); + } + + @SuppressWarnings("unchecked") + static A createProxy(MergedAnnotation annotation, Class type) { + ClassLoader classLoader = type.getClassLoader(); + InvocationHandler handler = new SynthesizedMergedAnnotationInvocationHandler<>(annotation, type); + Class[] interfaces = isVisible(classLoader, SynthesizedAnnotation.class) ? + new Class[] {type, SynthesizedAnnotation.class} : new Class[] {type}; + return (A) Proxy.newProxyInstance(classLoader, interfaces, handler); + } + + + private static boolean isVisible(ClassLoader classLoader, Class interfaceClass) { + if (classLoader == interfaceClass.getClassLoader()) { + return true; + } + try { + return Class.forName(interfaceClass.getName(), false, classLoader) == interfaceClass; + } + catch (ClassNotFoundException ex) { + return false; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/SynthesizingMethodParameter.java b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizingMethodParameter.java new file mode 100644 index 0000000..efc95dc --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizingMethodParameter.java @@ -0,0 +1,146 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; + +import org.springframework.core.MethodParameter; + +/** + * A {@link MethodParameter} variant which synthesizes annotations that + * declare attribute aliases via {@link AliasFor @AliasFor}. + * + * @author Juergen Hoeller + * @author Sam Brannen + * @since 4.2 + * @see AnnotationUtils#synthesizeAnnotation + * @see AnnotationUtils#synthesizeAnnotationArray + */ +public class SynthesizingMethodParameter extends MethodParameter { + + /** + * Create a new {@code SynthesizingMethodParameter} for the given method, + * with nesting level 1. + * @param method the Method to specify a parameter for + * @param parameterIndex the index of the parameter: -1 for the method + * return type; 0 for the first method parameter; 1 for the second method + * parameter, etc. + */ + public SynthesizingMethodParameter(Method method, int parameterIndex) { + super(method, parameterIndex); + } + + /** + * Create a new {@code SynthesizingMethodParameter} for the given method. + * @param method the Method to specify a parameter for + * @param parameterIndex the index of the parameter: -1 for the method + * return type; 0 for the first method parameter; 1 for the second method + * parameter, etc. + * @param nestingLevel the nesting level of the target type + * (typically 1; e.g. in case of a List of Lists, 1 would indicate the + * nested List, whereas 2 would indicate the element of the nested List) + */ + public SynthesizingMethodParameter(Method method, int parameterIndex, int nestingLevel) { + super(method, parameterIndex, nestingLevel); + } + + /** + * Create a new {@code SynthesizingMethodParameter} for the given constructor, + * with nesting level 1. + * @param constructor the Constructor to specify a parameter for + * @param parameterIndex the index of the parameter + */ + public SynthesizingMethodParameter(Constructor constructor, int parameterIndex) { + super(constructor, parameterIndex); + } + + /** + * Create a new {@code SynthesizingMethodParameter} for the given constructor. + * @param constructor the Constructor to specify a parameter for + * @param parameterIndex the index of the parameter + * @param nestingLevel the nesting level of the target type + * (typically 1; e.g. in case of a List of Lists, 1 would indicate the + * nested List, whereas 2 would indicate the element of the nested List) + */ + public SynthesizingMethodParameter(Constructor constructor, int parameterIndex, int nestingLevel) { + super(constructor, parameterIndex, nestingLevel); + } + + /** + * Copy constructor, resulting in an independent {@code SynthesizingMethodParameter} + * based on the same metadata and cache state that the original object was in. + * @param original the original SynthesizingMethodParameter object to copy from + */ + protected SynthesizingMethodParameter(SynthesizingMethodParameter original) { + super(original); + } + + + @Override + protected A adaptAnnotation(A annotation) { + return AnnotationUtils.synthesizeAnnotation(annotation, getAnnotatedElement()); + } + + @Override + protected Annotation[] adaptAnnotationArray(Annotation[] annotations) { + return AnnotationUtils.synthesizeAnnotationArray(annotations, getAnnotatedElement()); + } + + @Override + public SynthesizingMethodParameter clone() { + return new SynthesizingMethodParameter(this); + } + + + /** + * Create a new SynthesizingMethodParameter for the given method or constructor. + *

    This is a convenience factory method for scenarios where a + * Method or Constructor reference is treated in a generic fashion. + * @param executable the Method or Constructor to specify a parameter for + * @param parameterIndex the index of the parameter + * @return the corresponding SynthesizingMethodParameter instance + * @since 5.0 + */ + public static SynthesizingMethodParameter forExecutable(Executable executable, int parameterIndex) { + if (executable instanceof Method) { + return new SynthesizingMethodParameter((Method) executable, parameterIndex); + } + else if (executable instanceof Constructor) { + return new SynthesizingMethodParameter((Constructor) executable, parameterIndex); + } + else { + throw new IllegalArgumentException("Not a Method/Constructor: " + executable); + } + } + + /** + * Create a new SynthesizingMethodParameter for the given parameter descriptor. + *

    This is a convenience factory method for scenarios where a + * Java 8 {@link Parameter} descriptor is already available. + * @param parameter the parameter descriptor + * @return the corresponding SynthesizingMethodParameter instance + * @since 5.0 + */ + public static SynthesizingMethodParameter forParameter(Parameter parameter) { + return forExecutable(parameter.getDeclaringExecutable(), findParameterIndex(parameter)); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java new file mode 100644 index 0000000..ede74c0 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java @@ -0,0 +1,653 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * {@link MergedAnnotation} that adapts attributes from a root annotation by + * applying the mapping and mirroring rules of an {@link AnnotationTypeMapping}. + * + *

    Root attribute values are extracted from a source object using a supplied + * {@code BiFunction}. This allows various different annotation models to be + * supported by the same class. For example, the attributes source might be an + * actual {@link Annotation} instance where methods on the annotation instance + * are {@linkplain ReflectionUtils#invokeMethod(Method, Object) invoked} to extract + * values. Equally, the source could be a simple {@link Map} with values + * extracted using {@link Map#get(Object)}. + * + *

    Extracted root attribute values must be compatible with the attribute + * return type, namely: + * + *

    + * + * + * + * + * + * + *
    Return TypeExtracted Type
    ClassClass or String
    Class[]Class[] or String[]
    AnnotationAnnotation, Map, or Object compatible with the value + * extractor
    Annotation[]Annotation[], Map[], or Object[] where elements are + * compatible with the value extractor
    Other typesAn exact match or the appropriate primitive wrapper
    + * + * @author Phillip Webb + * @author Juergen Hoeller + * @author Sam Brannen + * @since 5.2 + * @param the annotation type + * @see TypeMappedAnnotations + */ +final class TypeMappedAnnotation extends AbstractMergedAnnotation { + + private static final Map, Object> EMPTY_ARRAYS; + static { + Map, Object> emptyArrays = new HashMap<>(); + emptyArrays.put(boolean.class, new boolean[0]); + emptyArrays.put(byte.class, new byte[0]); + emptyArrays.put(char.class, new char[0]); + emptyArrays.put(double.class, new double[0]); + emptyArrays.put(float.class, new float[0]); + emptyArrays.put(int.class, new int[0]); + emptyArrays.put(long.class, new long[0]); + emptyArrays.put(short.class, new short[0]); + emptyArrays.put(String.class, new String[0]); + EMPTY_ARRAYS = Collections.unmodifiableMap(emptyArrays); + } + + + private final AnnotationTypeMapping mapping; + + @Nullable + private final ClassLoader classLoader; + + @Nullable + private final Object source; + + @Nullable + private final Object rootAttributes; + + private final ValueExtractor valueExtractor; + + private final int aggregateIndex; + + private final boolean useMergedValues; + + @Nullable + private final Predicate attributeFilter; + + private final int[] resolvedRootMirrors; + + private final int[] resolvedMirrors; + + + private TypeMappedAnnotation(AnnotationTypeMapping mapping, @Nullable ClassLoader classLoader, + @Nullable Object source, @Nullable Object rootAttributes, ValueExtractor valueExtractor, + int aggregateIndex) { + + this(mapping, classLoader, source, rootAttributes, valueExtractor, aggregateIndex, null); + } + + private TypeMappedAnnotation(AnnotationTypeMapping mapping, @Nullable ClassLoader classLoader, + @Nullable Object source, @Nullable Object rootAttributes, ValueExtractor valueExtractor, + int aggregateIndex, @Nullable int[] resolvedRootMirrors) { + + this.mapping = mapping; + this.classLoader = classLoader; + this.source = source; + this.rootAttributes = rootAttributes; + this.valueExtractor = valueExtractor; + this.aggregateIndex = aggregateIndex; + this.useMergedValues = true; + this.attributeFilter = null; + this.resolvedRootMirrors = (resolvedRootMirrors != null ? resolvedRootMirrors : + mapping.getRoot().getMirrorSets().resolve(source, rootAttributes, this.valueExtractor)); + this.resolvedMirrors = (getDistance() == 0 ? this.resolvedRootMirrors : + mapping.getMirrorSets().resolve(source, this, this::getValueForMirrorResolution)); + } + + private TypeMappedAnnotation(AnnotationTypeMapping mapping, @Nullable ClassLoader classLoader, + @Nullable Object source, @Nullable Object rootAnnotation, ValueExtractor valueExtractor, + int aggregateIndex, boolean useMergedValues, @Nullable Predicate attributeFilter, + int[] resolvedRootMirrors, int[] resolvedMirrors) { + + this.classLoader = classLoader; + this.source = source; + this.rootAttributes = rootAnnotation; + this.valueExtractor = valueExtractor; + this.mapping = mapping; + this.aggregateIndex = aggregateIndex; + this.useMergedValues = useMergedValues; + this.attributeFilter = attributeFilter; + this.resolvedRootMirrors = resolvedRootMirrors; + this.resolvedMirrors = resolvedMirrors; + } + + + @Override + @SuppressWarnings("unchecked") + public Class getType() { + return (Class) this.mapping.getAnnotationType(); + } + + @Override + public List> getMetaTypes() { + return this.mapping.getMetaTypes(); + } + + @Override + public boolean isPresent() { + return true; + } + + @Override + public int getDistance() { + return this.mapping.getDistance(); + } + + @Override + public int getAggregateIndex() { + return this.aggregateIndex; + } + + @Override + @Nullable + public Object getSource() { + return this.source; + } + + @Override + @Nullable + public MergedAnnotation getMetaSource() { + AnnotationTypeMapping metaSourceMapping = this.mapping.getSource(); + if (metaSourceMapping == null) { + return null; + } + return new TypeMappedAnnotation<>(metaSourceMapping, this.classLoader, this.source, + this.rootAttributes, this.valueExtractor, this.aggregateIndex, this.resolvedRootMirrors); + } + + @Override + public MergedAnnotation getRoot() { + if (getDistance() == 0) { + return this; + } + AnnotationTypeMapping rootMapping = this.mapping.getRoot(); + return new TypeMappedAnnotation<>(rootMapping, this.classLoader, this.source, + this.rootAttributes, this.valueExtractor, this.aggregateIndex, this.resolvedRootMirrors); + } + + @Override + public boolean hasDefaultValue(String attributeName) { + int attributeIndex = getAttributeIndex(attributeName, true); + Object value = getValue(attributeIndex, true, false); + return (value == null || this.mapping.isEquivalentToDefaultValue(attributeIndex, value, this.valueExtractor)); + } + + @Override + @SuppressWarnings("unchecked") + public MergedAnnotation getAnnotation(String attributeName, Class type) + throws NoSuchElementException { + + int attributeIndex = getAttributeIndex(attributeName, true); + Method attribute = this.mapping.getAttributes().get(attributeIndex); + Assert.notNull(type, "Type must not be null"); + Assert.isAssignable(type, attribute.getReturnType(), + () -> "Attribute " + attributeName + " type mismatch:"); + return (MergedAnnotation) getRequiredValue(attributeIndex, attributeName); + } + + @Override + @SuppressWarnings("unchecked") + public MergedAnnotation[] getAnnotationArray( + String attributeName, Class type) throws NoSuchElementException { + + int attributeIndex = getAttributeIndex(attributeName, true); + Method attribute = this.mapping.getAttributes().get(attributeIndex); + Class componentType = attribute.getReturnType().getComponentType(); + Assert.notNull(type, "Type must not be null"); + Assert.notNull(componentType, () -> "Attribute " + attributeName + " is not an array"); + Assert.isAssignable(type, componentType, () -> "Attribute " + attributeName + " component type mismatch:"); + return (MergedAnnotation[]) getRequiredValue(attributeIndex, attributeName); + } + + @Override + public Optional getDefaultValue(String attributeName, Class type) { + int attributeIndex = getAttributeIndex(attributeName, false); + if (attributeIndex == -1) { + return Optional.empty(); + } + Method attribute = this.mapping.getAttributes().get(attributeIndex); + return Optional.ofNullable(adapt(attribute, attribute.getDefaultValue(), type)); + } + + @Override + public MergedAnnotation filterAttributes(Predicate predicate) { + if (this.attributeFilter != null) { + predicate = this.attributeFilter.and(predicate); + } + return new TypeMappedAnnotation<>(this.mapping, this.classLoader, this.source, this.rootAttributes, + this.valueExtractor, this.aggregateIndex, this.useMergedValues, predicate, + this.resolvedRootMirrors, this.resolvedMirrors); + } + + @Override + public MergedAnnotation withNonMergedAttributes() { + return new TypeMappedAnnotation<>(this.mapping, this.classLoader, this.source, this.rootAttributes, + this.valueExtractor, this.aggregateIndex, false, this.attributeFilter, + this.resolvedRootMirrors, this.resolvedMirrors); + } + + @Override + public Map asMap(Adapt... adaptations) { + return Collections.unmodifiableMap(asMap(mergedAnnotation -> new LinkedHashMap<>(), adaptations)); + } + + @Override + public > T asMap(Function, T> factory, Adapt... adaptations) { + T map = factory.apply(this); + Assert.state(map != null, "Factory used to create MergedAnnotation Map must not return null"); + AttributeMethods attributes = this.mapping.getAttributes(); + for (int i = 0; i < attributes.size(); i++) { + Method attribute = attributes.get(i); + Object value = (isFiltered(attribute.getName()) ? null : + getValue(i, getTypeForMapOptions(attribute, adaptations))); + if (value != null) { + map.put(attribute.getName(), + adaptValueForMapOptions(attribute, value, map.getClass(), factory, adaptations)); + } + } + return map; + } + + private Class getTypeForMapOptions(Method attribute, Adapt[] adaptations) { + Class attributeType = attribute.getReturnType(); + Class componentType = (attributeType.isArray() ? attributeType.getComponentType() : attributeType); + if (Adapt.CLASS_TO_STRING.isIn(adaptations) && componentType == Class.class) { + return (attributeType.isArray() ? String[].class : String.class); + } + return Object.class; + } + + private > Object adaptValueForMapOptions(Method attribute, Object value, + Class mapType, Function, T> factory, Adapt[] adaptations) { + + if (value instanceof MergedAnnotation) { + MergedAnnotation annotation = (MergedAnnotation) value; + return (Adapt.ANNOTATION_TO_MAP.isIn(adaptations) ? + annotation.asMap(factory, adaptations) : annotation.synthesize()); + } + if (value instanceof MergedAnnotation[]) { + MergedAnnotation[] annotations = (MergedAnnotation[]) value; + if (Adapt.ANNOTATION_TO_MAP.isIn(adaptations)) { + Object result = Array.newInstance(mapType, annotations.length); + for (int i = 0; i < annotations.length; i++) { + Array.set(result, i, annotations[i].asMap(factory, adaptations)); + } + return result; + } + Object result = Array.newInstance( + attribute.getReturnType().getComponentType(), annotations.length); + for (int i = 0; i < annotations.length; i++) { + Array.set(result, i, annotations[i].synthesize()); + } + return result; + } + return value; + } + + @Override + @SuppressWarnings("unchecked") + protected A createSynthesized() { + if (getType().isInstance(this.rootAttributes) && !isSynthesizable()) { + return (A) this.rootAttributes; + } + return SynthesizedMergedAnnotationInvocationHandler.createProxy(this, getType()); + } + + private boolean isSynthesizable() { + // Already synthesized? + if (this.rootAttributes instanceof SynthesizedAnnotation) { + return false; + } + return this.mapping.isSynthesizable(); + } + + @Override + @Nullable + protected T getAttributeValue(String attributeName, Class type) { + int attributeIndex = getAttributeIndex(attributeName, false); + return (attributeIndex != -1 ? getValue(attributeIndex, type) : null); + } + + private Object getRequiredValue(int attributeIndex, String attributeName) { + Object value = getValue(attributeIndex, Object.class); + if (value == null) { + throw new NoSuchElementException("No element at attribute index " + + attributeIndex + " for name " + attributeName); + } + return value; + } + + @Nullable + private T getValue(int attributeIndex, Class type) { + Method attribute = this.mapping.getAttributes().get(attributeIndex); + Object value = getValue(attributeIndex, true, false); + if (value == null) { + value = attribute.getDefaultValue(); + } + return adapt(attribute, value, type); + } + + @Nullable + private Object getValue(int attributeIndex, boolean useConventionMapping, boolean forMirrorResolution) { + AnnotationTypeMapping mapping = this.mapping; + if (this.useMergedValues) { + int mappedIndex = this.mapping.getAliasMapping(attributeIndex); + if (mappedIndex == -1 && useConventionMapping) { + mappedIndex = this.mapping.getConventionMapping(attributeIndex); + } + if (mappedIndex != -1) { + mapping = mapping.getRoot(); + attributeIndex = mappedIndex; + } + } + if (!forMirrorResolution) { + attributeIndex = + (mapping.getDistance() != 0 ? this.resolvedMirrors : this.resolvedRootMirrors)[attributeIndex]; + } + if (attributeIndex == -1) { + return null; + } + if (mapping.getDistance() == 0) { + Method attribute = mapping.getAttributes().get(attributeIndex); + Object result = this.valueExtractor.extract(attribute, this.rootAttributes); + return (result != null ? result : attribute.getDefaultValue()); + } + return getValueFromMetaAnnotation(attributeIndex, forMirrorResolution); + } + + @Nullable + private Object getValueFromMetaAnnotation(int attributeIndex, boolean forMirrorResolution) { + Object value = null; + if (this.useMergedValues || forMirrorResolution) { + value = this.mapping.getMappedAnnotationValue(attributeIndex, forMirrorResolution); + } + if (value == null) { + Method attribute = this.mapping.getAttributes().get(attributeIndex); + value = ReflectionUtils.invokeMethod(attribute, this.mapping.getAnnotation()); + } + return value; + } + + @Nullable + private Object getValueForMirrorResolution(Method attribute, Object annotation) { + int attributeIndex = this.mapping.getAttributes().indexOf(attribute); + boolean valueAttribute = VALUE.equals(attribute.getName()); + return getValue(attributeIndex, !valueAttribute, true); + } + + @SuppressWarnings("unchecked") + @Nullable + private T adapt(Method attribute, @Nullable Object value, Class type) { + if (value == null) { + return null; + } + value = adaptForAttribute(attribute, value); + type = getAdaptType(attribute, type); + if (value instanceof Class && type == String.class) { + value = ((Class) value).getName(); + } + else if (value instanceof String && type == Class.class) { + value = ClassUtils.resolveClassName((String) value, getClassLoader()); + } + else if (value instanceof Class[] && type == String[].class) { + Class[] classes = (Class[]) value; + String[] names = new String[classes.length]; + for (int i = 0; i < classes.length; i++) { + names[i] = classes[i].getName(); + } + value = names; + } + else if (value instanceof String[] && type == Class[].class) { + String[] names = (String[]) value; + Class[] classes = new Class[names.length]; + for (int i = 0; i < names.length; i++) { + classes[i] = ClassUtils.resolveClassName(names[i], getClassLoader()); + } + value = classes; + } + else if (value instanceof MergedAnnotation && type.isAnnotation()) { + MergedAnnotation annotation = (MergedAnnotation) value; + value = annotation.synthesize(); + } + else if (value instanceof MergedAnnotation[] && type.isArray() && type.getComponentType().isAnnotation()) { + MergedAnnotation[] annotations = (MergedAnnotation[]) value; + Object array = Array.newInstance(type.getComponentType(), annotations.length); + for (int i = 0; i < annotations.length; i++) { + Array.set(array, i, annotations[i].synthesize()); + } + value = array; + } + if (!type.isInstance(value)) { + throw new IllegalArgumentException("Unable to adapt value of type " + + value.getClass().getName() + " to " + type.getName()); + } + return (T) value; + } + + @SuppressWarnings("unchecked") + private Object adaptForAttribute(Method attribute, Object value) { + Class attributeType = ClassUtils.resolvePrimitiveIfNecessary(attribute.getReturnType()); + if (attributeType.isArray() && !value.getClass().isArray()) { + Object array = Array.newInstance(value.getClass(), 1); + Array.set(array, 0, value); + return adaptForAttribute(attribute, array); + } + if (attributeType.isAnnotation()) { + return adaptToMergedAnnotation(value, (Class) attributeType); + } + if (attributeType.isArray() && attributeType.getComponentType().isAnnotation()) { + MergedAnnotation[] result = new MergedAnnotation[Array.getLength(value)]; + for (int i = 0; i < result.length; i++) { + result[i] = adaptToMergedAnnotation(Array.get(value, i), + (Class) attributeType.getComponentType()); + } + return result; + } + if ((attributeType == Class.class && value instanceof String) || + (attributeType == Class[].class && value instanceof String[]) || + (attributeType == String.class && value instanceof Class) || + (attributeType == String[].class && value instanceof Class[])) { + return value; + } + if (attributeType.isArray() && isEmptyObjectArray(value)) { + return emptyArray(attributeType.getComponentType()); + } + if (!attributeType.isInstance(value)) { + throw new IllegalStateException("Attribute '" + attribute.getName() + + "' in annotation " + getType().getName() + " should be compatible with " + + attributeType.getName() + " but a " + value.getClass().getName() + + " value was returned"); + } + return value; + } + + private boolean isEmptyObjectArray(Object value) { + return (value instanceof Object[] && ((Object[]) value).length == 0); + } + + private Object emptyArray(Class componentType) { + Object result = EMPTY_ARRAYS.get(componentType); + if (result == null) { + result = Array.newInstance(componentType, 0); + } + return result; + } + + private MergedAnnotation adaptToMergedAnnotation(Object value, Class annotationType) { + if (value instanceof MergedAnnotation) { + return (MergedAnnotation) value; + } + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(annotationType).get(0); + return new TypeMappedAnnotation<>( + mapping, null, this.source, value, getValueExtractor(value), this.aggregateIndex); + } + + private ValueExtractor getValueExtractor(Object value) { + if (value instanceof Annotation) { + return ReflectionUtils::invokeMethod; + } + if (value instanceof Map) { + return TypeMappedAnnotation::extractFromMap; + } + return this.valueExtractor; + } + + @SuppressWarnings("unchecked") + private Class getAdaptType(Method attribute, Class type) { + if (type != Object.class) { + return type; + } + Class attributeType = attribute.getReturnType(); + if (attributeType.isAnnotation()) { + return (Class) MergedAnnotation.class; + } + if (attributeType.isArray() && attributeType.getComponentType().isAnnotation()) { + return (Class) MergedAnnotation[].class; + } + return (Class) ClassUtils.resolvePrimitiveIfNecessary(attributeType); + } + + private int getAttributeIndex(String attributeName, boolean required) { + Assert.hasText(attributeName, "Attribute name must not be null"); + int attributeIndex = (isFiltered(attributeName) ? -1 : this.mapping.getAttributes().indexOf(attributeName)); + if (attributeIndex == -1 && required) { + throw new NoSuchElementException("No attribute named '" + attributeName + + "' present in merged annotation " + getType().getName()); + } + return attributeIndex; + } + + private boolean isFiltered(String attributeName) { + if (this.attributeFilter != null) { + return !this.attributeFilter.test(attributeName); + } + return false; + } + + @Nullable + private ClassLoader getClassLoader() { + if (this.classLoader != null) { + return this.classLoader; + } + if (this.source != null) { + if (this.source instanceof Class) { + return ((Class) source).getClassLoader(); + } + if (this.source instanceof Member) { + ((Member) this.source).getDeclaringClass().getClassLoader(); + } + } + return null; + } + + + static MergedAnnotation from(@Nullable Object source, A annotation) { + Assert.notNull(annotation, "Annotation must not be null"); + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(annotation.annotationType()); + return new TypeMappedAnnotation<>(mappings.get(0), null, source, annotation, ReflectionUtils::invokeMethod, 0); + } + + static MergedAnnotation of( + @Nullable ClassLoader classLoader, @Nullable Object source, + Class annotationType, @Nullable Map attributes) { + + Assert.notNull(annotationType, "Annotation type must not be null"); + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(annotationType); + return new TypeMappedAnnotation<>( + mappings.get(0), classLoader, source, attributes, TypeMappedAnnotation::extractFromMap, 0); + } + + @Nullable + static TypeMappedAnnotation createIfPossible( + AnnotationTypeMapping mapping, MergedAnnotation annotation, IntrospectionFailureLogger logger) { + + if (annotation instanceof TypeMappedAnnotation) { + TypeMappedAnnotation typeMappedAnnotation = (TypeMappedAnnotation) annotation; + return createIfPossible(mapping, typeMappedAnnotation.source, + typeMappedAnnotation.rootAttributes, + typeMappedAnnotation.valueExtractor, + typeMappedAnnotation.aggregateIndex, logger); + } + return createIfPossible(mapping, annotation.getSource(), annotation.synthesize(), + annotation.getAggregateIndex(), logger); + } + + @Nullable + static TypeMappedAnnotation createIfPossible( + AnnotationTypeMapping mapping, @Nullable Object source, Annotation annotation, + int aggregateIndex, IntrospectionFailureLogger logger) { + + return createIfPossible(mapping, source, annotation, + ReflectionUtils::invokeMethod, aggregateIndex, logger); + } + + @Nullable + private static TypeMappedAnnotation createIfPossible( + AnnotationTypeMapping mapping, @Nullable Object source, @Nullable Object rootAttribute, + ValueExtractor valueExtractor, int aggregateIndex, IntrospectionFailureLogger logger) { + + try { + return new TypeMappedAnnotation<>(mapping, null, source, rootAttribute, + valueExtractor, aggregateIndex); + } + catch (Exception ex) { + AnnotationUtils.rethrowAnnotationConfigurationException(ex); + if (logger.isEnabled()) { + String type = mapping.getAnnotationType().getName(); + String item = (mapping.getDistance() == 0 ? "annotation " + type : + "meta-annotation " + type + " from " + mapping.getRoot().getAnnotationType().getName()); + logger.log("Failed to introspect " + item, source, ex); + } + return null; + } + } + + @SuppressWarnings("unchecked") + @Nullable + static Object extractFromMap(Method attribute, @Nullable Object map) { + return (map != null ? ((Map) map).get(attribute.getName()) : null); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java new file mode 100644 index 0000000..ddd078b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java @@ -0,0 +1,652 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.springframework.lang.Nullable; + +/** + * {@link MergedAnnotations} implementation that searches for and adapts + * annotations and meta-annotations using {@link AnnotationTypeMappings}. + * + * @author Phillip Webb + * @since 5.2 + */ +final class TypeMappedAnnotations implements MergedAnnotations { + + /** + * Shared instance that can be used when there are no annotations. + */ + static final MergedAnnotations NONE = new TypeMappedAnnotations( + null, new Annotation[0], RepeatableContainers.none(), AnnotationFilter.ALL); + + + @Nullable + private final Object source; + + @Nullable + private final AnnotatedElement element; + + @Nullable + private final SearchStrategy searchStrategy; + + @Nullable + private final Annotation[] annotations; + + private final RepeatableContainers repeatableContainers; + + private final AnnotationFilter annotationFilter; + + @Nullable + private volatile List aggregates; + + + private TypeMappedAnnotations(AnnotatedElement element, SearchStrategy searchStrategy, + RepeatableContainers repeatableContainers, AnnotationFilter annotationFilter) { + + this.source = element; + this.element = element; + this.searchStrategy = searchStrategy; + this.annotations = null; + this.repeatableContainers = repeatableContainers; + this.annotationFilter = annotationFilter; + } + + private TypeMappedAnnotations(@Nullable Object source, Annotation[] annotations, + RepeatableContainers repeatableContainers, AnnotationFilter annotationFilter) { + + this.source = source; + this.element = null; + this.searchStrategy = null; + this.annotations = annotations; + this.repeatableContainers = repeatableContainers; + this.annotationFilter = annotationFilter; + } + + + @Override + public boolean isPresent(Class annotationType) { + if (this.annotationFilter.matches(annotationType)) { + return false; + } + return Boolean.TRUE.equals(scan(annotationType, + IsPresent.get(this.repeatableContainers, this.annotationFilter, false))); + } + + @Override + public boolean isPresent(String annotationType) { + if (this.annotationFilter.matches(annotationType)) { + return false; + } + return Boolean.TRUE.equals(scan(annotationType, + IsPresent.get(this.repeatableContainers, this.annotationFilter, false))); + } + + @Override + public boolean isDirectlyPresent(Class annotationType) { + if (this.annotationFilter.matches(annotationType)) { + return false; + } + return Boolean.TRUE.equals(scan(annotationType, + IsPresent.get(this.repeatableContainers, this.annotationFilter, true))); + } + + @Override + public boolean isDirectlyPresent(String annotationType) { + if (this.annotationFilter.matches(annotationType)) { + return false; + } + return Boolean.TRUE.equals(scan(annotationType, + IsPresent.get(this.repeatableContainers, this.annotationFilter, true))); + } + + @Override + public MergedAnnotation get(Class annotationType) { + return get(annotationType, null, null); + } + + @Override + public MergedAnnotation get(Class annotationType, + @Nullable Predicate> predicate) { + + return get(annotationType, predicate, null); + } + + @Override + public MergedAnnotation get(Class annotationType, + @Nullable Predicate> predicate, + @Nullable MergedAnnotationSelector selector) { + + if (this.annotationFilter.matches(annotationType)) { + return MergedAnnotation.missing(); + } + MergedAnnotation result = scan(annotationType, + new MergedAnnotationFinder<>(annotationType, predicate, selector)); + return (result != null ? result : MergedAnnotation.missing()); + } + + @Override + public MergedAnnotation get(String annotationType) { + return get(annotationType, null, null); + } + + @Override + public MergedAnnotation get(String annotationType, + @Nullable Predicate> predicate) { + + return get(annotationType, predicate, null); + } + + @Override + public MergedAnnotation get(String annotationType, + @Nullable Predicate> predicate, + @Nullable MergedAnnotationSelector selector) { + + if (this.annotationFilter.matches(annotationType)) { + return MergedAnnotation.missing(); + } + MergedAnnotation result = scan(annotationType, + new MergedAnnotationFinder<>(annotationType, predicate, selector)); + return (result != null ? result : MergedAnnotation.missing()); + } + + @Override + public Stream> stream(Class annotationType) { + if (this.annotationFilter == AnnotationFilter.ALL) { + return Stream.empty(); + } + return StreamSupport.stream(spliterator(annotationType), false); + } + + @Override + public Stream> stream(String annotationType) { + if (this.annotationFilter == AnnotationFilter.ALL) { + return Stream.empty(); + } + return StreamSupport.stream(spliterator(annotationType), false); + } + + @Override + public Stream> stream() { + if (this.annotationFilter == AnnotationFilter.ALL) { + return Stream.empty(); + } + return StreamSupport.stream(spliterator(), false); + } + + @Override + public Iterator> iterator() { + if (this.annotationFilter == AnnotationFilter.ALL) { + return Collections.emptyIterator(); + } + return Spliterators.iterator(spliterator()); + } + + @Override + public Spliterator> spliterator() { + if (this.annotationFilter == AnnotationFilter.ALL) { + return Spliterators.emptySpliterator(); + } + return spliterator(null); + } + + private Spliterator> spliterator(@Nullable Object annotationType) { + return new AggregatesSpliterator<>(annotationType, getAggregates()); + } + + private List getAggregates() { + List aggregates = this.aggregates; + if (aggregates == null) { + aggregates = scan(this, new AggregatesCollector()); + if (aggregates == null || aggregates.isEmpty()) { + aggregates = Collections.emptyList(); + } + this.aggregates = aggregates; + } + return aggregates; + } + + @Nullable + private R scan(C criteria, AnnotationsProcessor processor) { + if (this.annotations != null) { + R result = processor.doWithAnnotations(criteria, 0, this.source, this.annotations); + return processor.finish(result); + } + if (this.element != null && this.searchStrategy != null) { + return AnnotationsScanner.scan(criteria, this.element, this.searchStrategy, processor); + } + return null; + } + + + static MergedAnnotations from(AnnotatedElement element, SearchStrategy searchStrategy, + RepeatableContainers repeatableContainers, AnnotationFilter annotationFilter) { + + if (AnnotationsScanner.isKnownEmpty(element, searchStrategy)) { + return NONE; + } + return new TypeMappedAnnotations(element, searchStrategy, repeatableContainers, annotationFilter); + } + + static MergedAnnotations from(@Nullable Object source, Annotation[] annotations, + RepeatableContainers repeatableContainers, AnnotationFilter annotationFilter) { + + if (annotations.length == 0) { + return NONE; + } + return new TypeMappedAnnotations(source, annotations, repeatableContainers, annotationFilter); + } + + private static boolean isMappingForType(AnnotationTypeMapping mapping, + AnnotationFilter annotationFilter, @Nullable Object requiredType) { + + Class actualType = mapping.getAnnotationType(); + return (!annotationFilter.matches(actualType) && + (requiredType == null || actualType == requiredType || actualType.getName().equals(requiredType))); + } + + + /** + * {@link AnnotationsProcessor} used to detect if an annotation is directly + * present or meta-present. + */ + private static final class IsPresent implements AnnotationsProcessor { + + /** + * Shared instances that save us needing to create a new processor for + * the common combinations. + */ + private static final IsPresent[] SHARED; + static { + SHARED = new IsPresent[4]; + SHARED[0] = new IsPresent(RepeatableContainers.none(), AnnotationFilter.PLAIN, true); + SHARED[1] = new IsPresent(RepeatableContainers.none(), AnnotationFilter.PLAIN, false); + SHARED[2] = new IsPresent(RepeatableContainers.standardRepeatables(), AnnotationFilter.PLAIN, true); + SHARED[3] = new IsPresent(RepeatableContainers.standardRepeatables(), AnnotationFilter.PLAIN, false); + } + + private final RepeatableContainers repeatableContainers; + + private final AnnotationFilter annotationFilter; + + private final boolean directOnly; + + private IsPresent(RepeatableContainers repeatableContainers, + AnnotationFilter annotationFilter, boolean directOnly) { + + this.repeatableContainers = repeatableContainers; + this.annotationFilter = annotationFilter; + this.directOnly = directOnly; + } + + @Override + @Nullable + public Boolean doWithAnnotations(Object requiredType, int aggregateIndex, + @Nullable Object source, Annotation[] annotations) { + + for (Annotation annotation : annotations) { + if (annotation != null) { + Class type = annotation.annotationType(); + if (type != null && !this.annotationFilter.matches(type)) { + if (type == requiredType || type.getName().equals(requiredType)) { + return Boolean.TRUE; + } + Annotation[] repeatedAnnotations = + this.repeatableContainers.findRepeatedAnnotations(annotation); + if (repeatedAnnotations != null) { + Boolean result = doWithAnnotations( + requiredType, aggregateIndex, source, repeatedAnnotations); + if (result != null) { + return result; + } + } + if (!this.directOnly) { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(type); + for (int i = 0; i < mappings.size(); i++) { + AnnotationTypeMapping mapping = mappings.get(i); + if (isMappingForType(mapping, this.annotationFilter, requiredType)) { + return Boolean.TRUE; + } + } + } + } + } + } + return null; + } + + static IsPresent get(RepeatableContainers repeatableContainers, + AnnotationFilter annotationFilter, boolean directOnly) { + + // Use a single shared instance for common combinations + if (annotationFilter == AnnotationFilter.PLAIN) { + if (repeatableContainers == RepeatableContainers.none()) { + return SHARED[directOnly ? 0 : 1]; + } + if (repeatableContainers == RepeatableContainers.standardRepeatables()) { + return SHARED[directOnly ? 2 : 3]; + } + } + return new IsPresent(repeatableContainers, annotationFilter, directOnly); + } + } + + + /** + * {@link AnnotationsProcessor} that finds a single {@link MergedAnnotation}. + */ + private class MergedAnnotationFinder + implements AnnotationsProcessor> { + + private final Object requiredType; + + @Nullable + private final Predicate> predicate; + + private final MergedAnnotationSelector selector; + + @Nullable + private MergedAnnotation result; + + MergedAnnotationFinder(Object requiredType, @Nullable Predicate> predicate, + @Nullable MergedAnnotationSelector selector) { + + this.requiredType = requiredType; + this.predicate = predicate; + this.selector = (selector != null ? selector : MergedAnnotationSelectors.nearest()); + } + + @Override + @Nullable + public MergedAnnotation doWithAggregate(Object context, int aggregateIndex) { + return this.result; + } + + @Override + @Nullable + public MergedAnnotation doWithAnnotations(Object type, int aggregateIndex, + @Nullable Object source, Annotation[] annotations) { + + for (Annotation annotation : annotations) { + if (annotation != null && !annotationFilter.matches(annotation)) { + MergedAnnotation result = process(type, aggregateIndex, source, annotation); + if (result != null) { + return result; + } + } + } + return null; + } + + @Nullable + private MergedAnnotation process( + Object type, int aggregateIndex, @Nullable Object source, Annotation annotation) { + + Annotation[] repeatedAnnotations = repeatableContainers.findRepeatedAnnotations(annotation); + if (repeatedAnnotations != null) { + return doWithAnnotations(type, aggregateIndex, source, repeatedAnnotations); + } + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( + annotation.annotationType(), repeatableContainers, annotationFilter); + for (int i = 0; i < mappings.size(); i++) { + AnnotationTypeMapping mapping = mappings.get(i); + if (isMappingForType(mapping, annotationFilter, this.requiredType)) { + MergedAnnotation candidate = TypeMappedAnnotation.createIfPossible( + mapping, source, annotation, aggregateIndex, IntrospectionFailureLogger.INFO); + if (candidate != null && (this.predicate == null || this.predicate.test(candidate))) { + if (this.selector.isBestCandidate(candidate)) { + return candidate; + } + updateLastResult(candidate); + } + } + } + return null; + } + + private void updateLastResult(MergedAnnotation candidate) { + MergedAnnotation lastResult = this.result; + this.result = (lastResult != null ? this.selector.select(lastResult, candidate) : candidate); + } + + @Override + @Nullable + public MergedAnnotation finish(@Nullable MergedAnnotation result) { + return (result != null ? result : this.result); + } + } + + + /** + * {@link AnnotationsProcessor} that collects {@link Aggregate} instances. + */ + private class AggregatesCollector implements AnnotationsProcessor> { + + private final List aggregates = new ArrayList<>(); + + @Override + @Nullable + public List doWithAnnotations(Object criteria, int aggregateIndex, + @Nullable Object source, Annotation[] annotations) { + + this.aggregates.add(createAggregate(aggregateIndex, source, annotations)); + return null; + } + + private Aggregate createAggregate(int aggregateIndex, @Nullable Object source, Annotation[] annotations) { + List aggregateAnnotations = getAggregateAnnotations(annotations); + return new Aggregate(aggregateIndex, source, aggregateAnnotations); + } + + private List getAggregateAnnotations(Annotation[] annotations) { + List result = new ArrayList<>(annotations.length); + addAggregateAnnotations(result, annotations); + return result; + } + + private void addAggregateAnnotations(List aggregateAnnotations, Annotation[] annotations) { + for (Annotation annotation : annotations) { + if (annotation != null && !annotationFilter.matches(annotation)) { + Annotation[] repeatedAnnotations = repeatableContainers.findRepeatedAnnotations(annotation); + if (repeatedAnnotations != null) { + addAggregateAnnotations(aggregateAnnotations, repeatedAnnotations); + } + else { + aggregateAnnotations.add(annotation); + } + } + } + } + + @Override + public List finish(@Nullable List processResult) { + return this.aggregates; + } + } + + + private static class Aggregate { + + private final int aggregateIndex; + + @Nullable + private final Object source; + + private final List annotations; + + private final AnnotationTypeMappings[] mappings; + + Aggregate(int aggregateIndex, @Nullable Object source, List annotations) { + this.aggregateIndex = aggregateIndex; + this.source = source; + this.annotations = annotations; + this.mappings = new AnnotationTypeMappings[annotations.size()]; + for (int i = 0; i < annotations.size(); i++) { + this.mappings[i] = AnnotationTypeMappings.forAnnotationType(annotations.get(i).annotationType()); + } + } + + int size() { + return this.annotations.size(); + } + + @Nullable + AnnotationTypeMapping getMapping(int annotationIndex, int mappingIndex) { + AnnotationTypeMappings mappings = getMappings(annotationIndex); + return (mappingIndex < mappings.size() ? mappings.get(mappingIndex) : null); + } + + AnnotationTypeMappings getMappings(int annotationIndex) { + return this.mappings[annotationIndex]; + } + + @Nullable + MergedAnnotation createMergedAnnotationIfPossible( + int annotationIndex, int mappingIndex, IntrospectionFailureLogger logger) { + + return TypeMappedAnnotation.createIfPossible( + this.mappings[annotationIndex].get(mappingIndex), this.source, + this.annotations.get(annotationIndex), this.aggregateIndex, logger); + } + } + + + /** + * {@link Spliterator} used to consume merged annotations from the + * aggregates in distance fist order. + */ + private class AggregatesSpliterator implements Spliterator> { + + @Nullable + private final Object requiredType; + + private final List aggregates; + + private int aggregateCursor; + + @Nullable + private int[] mappingCursors; + + AggregatesSpliterator(@Nullable Object requiredType, List aggregates) { + this.requiredType = requiredType; + this.aggregates = aggregates; + this.aggregateCursor = 0; + } + + @Override + public boolean tryAdvance(Consumer> action) { + while (this.aggregateCursor < this.aggregates.size()) { + Aggregate aggregate = this.aggregates.get(this.aggregateCursor); + if (tryAdvance(aggregate, action)) { + return true; + } + this.aggregateCursor++; + this.mappingCursors = null; + } + return false; + } + + private boolean tryAdvance(Aggregate aggregate, Consumer> action) { + if (this.mappingCursors == null) { + this.mappingCursors = new int[aggregate.size()]; + } + int lowestDistance = Integer.MAX_VALUE; + int annotationResult = -1; + for (int annotationIndex = 0; annotationIndex < aggregate.size(); annotationIndex++) { + AnnotationTypeMapping mapping = getNextSuitableMapping(aggregate, annotationIndex); + if (mapping != null && mapping.getDistance() < lowestDistance) { + annotationResult = annotationIndex; + lowestDistance = mapping.getDistance(); + } + if (lowestDistance == 0) { + break; + } + } + if (annotationResult != -1) { + MergedAnnotation mergedAnnotation = aggregate.createMergedAnnotationIfPossible( + annotationResult, this.mappingCursors[annotationResult], + this.requiredType != null ? IntrospectionFailureLogger.INFO : IntrospectionFailureLogger.DEBUG); + this.mappingCursors[annotationResult]++; + if (mergedAnnotation == null) { + return tryAdvance(aggregate, action); + } + action.accept(mergedAnnotation); + return true; + } + return false; + } + + @Nullable + private AnnotationTypeMapping getNextSuitableMapping(Aggregate aggregate, int annotationIndex) { + int[] cursors = this.mappingCursors; + if (cursors != null) { + AnnotationTypeMapping mapping; + do { + mapping = aggregate.getMapping(annotationIndex, cursors[annotationIndex]); + if (mapping != null && isMappingForType(mapping, annotationFilter, this.requiredType)) { + return mapping; + } + cursors[annotationIndex]++; + } + while (mapping != null); + } + return null; + } + + @Override + @Nullable + public Spliterator> trySplit() { + return null; + } + + @Override + public long estimateSize() { + int size = 0; + for (int aggregateIndex = this.aggregateCursor; + aggregateIndex < this.aggregates.size(); aggregateIndex++) { + Aggregate aggregate = this.aggregates.get(aggregateIndex); + for (int annotationIndex = 0; annotationIndex < aggregate.size(); annotationIndex++) { + AnnotationTypeMappings mappings = aggregate.getMappings(annotationIndex); + int numberOfMappings = mappings.size(); + if (aggregateIndex == this.aggregateCursor && this.mappingCursors != null) { + numberOfMappings -= Math.min(this.mappingCursors[annotationIndex], mappings.size()); + } + size += numberOfMappings; + } + } + return size; + } + + @Override + public int characteristics() { + return NONNULL | IMMUTABLE; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/ValueExtractor.java b/spring-core/src/main/java/org/springframework/core/annotation/ValueExtractor.java new file mode 100644 index 0000000..62438f6 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/ValueExtractor.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Map; + +import org.springframework.lang.Nullable; + +/** + * Strategy API for extracting a value for an annotation attribute from a given + * source object which is typically an {@link Annotation}, {@link Map}, or + * {@link TypeMappedAnnotation}. + * + * @since 5.2.4 + * @author Sam Brannen + */ +@FunctionalInterface +interface ValueExtractor { + + /** + * Extract the annotation attribute represented by the supplied {@link Method} + * from the supplied source {@link Object}. + */ + @Nullable + Object extract(Method attribute, @Nullable Object object); + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/package-info.java b/spring-core/src/main/java/org/springframework/core/annotation/package-info.java new file mode 100644 index 0000000..af9d8e1 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/package-info.java @@ -0,0 +1,10 @@ +/** + * Core support package for annotations, meta-annotations, and merged + * annotations with attribute overrides. + */ +@NonNullApi +@NonNullFields +package org.springframework.core.annotation; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/main/java/org/springframework/core/codec/AbstractDataBufferDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/AbstractDataBufferDecoder.java new file mode 100644 index 0000000..0fd5870 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/codec/AbstractDataBufferDecoder.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.util.Map; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.MimeType; + +/** + * Abstract base class for {@code Decoder} implementations that can decode + * a {@code DataBuffer} directly to the target element type. + * + *

    Sub-classes must implement {@link #decodeDataBuffer} to provide a way to + * transform a {@code DataBuffer} to the target data type. The default + * {@link #decode} implementation transforms each individual data buffer while + * {@link #decodeToMono} applies "reduce" and transforms the aggregated buffer. + * + *

    Sub-classes can override {@link #decode} in order to split the input stream + * along different boundaries (e.g. on new line characters for {@code String}) + * or always reduce to a single data buffer (e.g. {@code Resource}). + * + * @author Rossen Stoyanchev + * @since 5.0 + * @param the element type + */ +@SuppressWarnings("deprecation") +public abstract class AbstractDataBufferDecoder extends AbstractDecoder { + + private int maxInMemorySize = 256 * 1024; + + + protected AbstractDataBufferDecoder(MimeType... supportedMimeTypes) { + super(supportedMimeTypes); + } + + + /** + * Configure a limit on the number of bytes that can be buffered whenever + * the input stream needs to be aggregated. This can be a result of + * decoding to a single {@code DataBuffer}, + * {@link java.nio.ByteBuffer ByteBuffer}, {@code byte[]}, + * {@link org.springframework.core.io.Resource Resource}, {@code String}, etc. + * It can also occur when splitting the input stream, e.g. delimited text, + * in which case the limit applies to data buffered between delimiters. + *

    By default this is set to 256K. + * @param byteCount the max number of bytes to buffer, or -1 for unlimited + * @since 5.1.11 + */ + public void setMaxInMemorySize(int byteCount) { + this.maxInMemorySize = byteCount; + } + + /** + * Return the {@link #setMaxInMemorySize configured} byte count limit. + * @since 5.1.11 + */ + public int getMaxInMemorySize() { + return this.maxInMemorySize; + } + + + @Override + public Flux decode(Publisher input, ResolvableType elementType, + @Nullable MimeType mimeType, @Nullable Map hints) { + + return Flux.from(input).map(buffer -> decodeDataBuffer(buffer, elementType, mimeType, hints)); + } + + @Override + public Mono decodeToMono(Publisher input, ResolvableType elementType, + @Nullable MimeType mimeType, @Nullable Map hints) { + + return DataBufferUtils.join(input, this.maxInMemorySize) + .map(buffer -> decodeDataBuffer(buffer, elementType, mimeType, hints)); + } + + /** + * How to decode a {@code DataBuffer} to the target element type. + * @deprecated as of 5.2, please implement + * {@link #decode(DataBuffer, ResolvableType, MimeType, Map)} instead + */ + @Deprecated + @Nullable + protected T decodeDataBuffer(DataBuffer buffer, ResolvableType elementType, + @Nullable MimeType mimeType, @Nullable Map hints) { + + return decode(buffer, elementType, mimeType, hints); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/codec/AbstractDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/AbstractDecoder.java new file mode 100644 index 0000000..7810d20 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/codec/AbstractDecoder.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.lang.Nullable; +import org.springframework.util.MimeType; + +/** + * Abstract base class for {@link Decoder} implementations. + * + * @author Sebastien Deleuze + * @author Arjen Poutsma + * @since 5.0 + * @param the element type + */ +public abstract class AbstractDecoder implements Decoder { + + private final List decodableMimeTypes; + + protected Log logger = LogFactory.getLog(getClass()); + + + protected AbstractDecoder(MimeType... supportedMimeTypes) { + this.decodableMimeTypes = Arrays.asList(supportedMimeTypes); + } + + + /** + * Set an alternative logger to use than the one based on the class name. + * @param logger the logger to use + * @since 5.1 + */ + public void setLogger(Log logger) { + this.logger = logger; + } + + /** + * Return the currently configured Logger. + * @since 5.1 + */ + public Log getLogger() { + return logger; + } + + + @Override + public List getDecodableMimeTypes() { + return this.decodableMimeTypes; + } + + @Override + public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) { + if (mimeType == null) { + return true; + } + for (MimeType candidate : this.decodableMimeTypes) { + if (candidate.isCompatibleWith(mimeType)) { + return true; + } + } + return false; + } + + @Override + public Mono decodeToMono(Publisher inputStream, ResolvableType elementType, + @Nullable MimeType mimeType, @Nullable Map hints) { + + throw new UnsupportedOperationException(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/codec/AbstractEncoder.java b/spring-core/src/main/java/org/springframework/core/codec/AbstractEncoder.java new file mode 100644 index 0000000..5a2633d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/codec/AbstractEncoder.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.ResolvableType; +import org.springframework.lang.Nullable; +import org.springframework.util.MimeType; + +/** + * Abstract base class for {@link Decoder} implementations. + * + * @author Sebastien Deleuze + * @author Arjen Poutsma + * @since 5.0 + * @param the element type + */ +public abstract class AbstractEncoder implements Encoder { + + private final List encodableMimeTypes; + + protected Log logger = LogFactory.getLog(getClass()); + + + protected AbstractEncoder(MimeType... supportedMimeTypes) { + this.encodableMimeTypes = Arrays.asList(supportedMimeTypes); + } + + + /** + * Set an alternative logger to use than the one based on the class name. + * @param logger the logger to use + * @since 5.1 + */ + public void setLogger(Log logger) { + this.logger = logger; + } + + /** + * Return the currently configured Logger. + * @since 5.1 + */ + public Log getLogger() { + return logger; + } + + + @Override + public List getEncodableMimeTypes() { + return this.encodableMimeTypes; + } + + @Override + public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) { + if (mimeType == null) { + return true; + } + for (MimeType candidate : this.encodableMimeTypes) { + if (candidate.isCompatibleWith(mimeType)) { + return true; + } + } + return false; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/codec/AbstractSingleValueEncoder.java b/spring-core/src/main/java/org/springframework/core/codec/AbstractSingleValueEncoder.java new file mode 100644 index 0000000..cdaba36 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/codec/AbstractSingleValueEncoder.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.util.Map; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.core.io.buffer.PooledDataBuffer; +import org.springframework.lang.Nullable; +import org.springframework.util.MimeType; + +/** + * Abstract base class for {@link org.springframework.core.codec.Encoder} + * classes that can only deal with a single value. + * + * @author Arjen Poutsma + * @since 5.0 + * @param the element type + */ +public abstract class AbstractSingleValueEncoder extends AbstractEncoder { + + + public AbstractSingleValueEncoder(MimeType... supportedMimeTypes) { + super(supportedMimeTypes); + } + + + @Override + public final Flux encode(Publisher inputStream, DataBufferFactory bufferFactory, + ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map hints) { + + return Flux.from(inputStream) + .take(1) + .concatMap(value -> encode(value, bufferFactory, elementType, mimeType, hints)) + .doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release); + } + + /** + * Encode {@code T} to an output {@link DataBuffer} stream. + * @param t the value to process + * @param dataBufferFactory a buffer factory used to create the output + * @param type the stream element type to process + * @param mimeType the mime type to process + * @param hints additional information about how to do decode, optional + * @return the output stream + */ + protected abstract Flux encode(T t, DataBufferFactory dataBufferFactory, + ResolvableType type, @Nullable MimeType mimeType, @Nullable Map hints); + +} diff --git a/spring-core/src/main/java/org/springframework/core/codec/ByteArrayDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/ByteArrayDecoder.java new file mode 100644 index 0000000..65f8222 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/codec/ByteArrayDecoder.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.util.Map; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +/** + * Decoder for {@code byte} arrays. + * + * @author Arjen Poutsma + * @author Rossen Stoyanchev + * @since 5.0 + */ +public class ByteArrayDecoder extends AbstractDataBufferDecoder { + + public ByteArrayDecoder() { + super(MimeTypeUtils.ALL); + } + + + @Override + public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) { + return (elementType.resolve() == byte[].class && super.canDecode(elementType, mimeType)); + } + + @Override + public byte[] decode(DataBuffer dataBuffer, ResolvableType elementType, + @Nullable MimeType mimeType, @Nullable Map hints) { + + byte[] result = new byte[dataBuffer.readableByteCount()]; + dataBuffer.read(result); + DataBufferUtils.release(dataBuffer); + if (logger.isDebugEnabled()) { + logger.debug(Hints.getLogPrefix(hints) + "Read " + result.length + " bytes"); + } + return result; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/codec/ByteArrayEncoder.java b/spring-core/src/main/java/org/springframework/core/codec/ByteArrayEncoder.java new file mode 100644 index 0000000..6eef1a1 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/codec/ByteArrayEncoder.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.util.Map; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.lang.Nullable; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +/** + * Encoder for {@code byte} arrays. + * + * @author Arjen Poutsma + * @since 5.0 + */ +public class ByteArrayEncoder extends AbstractEncoder { + + public ByteArrayEncoder() { + super(MimeTypeUtils.ALL); + } + + + @Override + public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) { + Class clazz = elementType.toClass(); + return super.canEncode(elementType, mimeType) && byte[].class.isAssignableFrom(clazz); + } + + @Override + public Flux encode(Publisher inputStream, + DataBufferFactory bufferFactory, ResolvableType elementType, @Nullable MimeType mimeType, + @Nullable Map hints) { + + // Use (byte[] bytes) for Eclipse + return Flux.from(inputStream).map((byte[] bytes) -> + encodeValue(bytes, bufferFactory, elementType, mimeType, hints)); + } + + @Override + public DataBuffer encodeValue(byte[] bytes, DataBufferFactory bufferFactory, + ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map hints) { + + DataBuffer dataBuffer = bufferFactory.wrap(bytes); + if (logger.isDebugEnabled() && !Hints.isLoggingSuppressed(hints)) { + String logPrefix = Hints.getLogPrefix(hints); + logger.debug(logPrefix + "Writing " + dataBuffer.readableByteCount() + " bytes"); + } + return dataBuffer; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/codec/ByteBufferDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/ByteBufferDecoder.java new file mode 100644 index 0000000..9c1133f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/codec/ByteBufferDecoder.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.nio.ByteBuffer; +import java.util.Map; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +/** + * Decoder for {@link ByteBuffer ByteBuffers}. + * + * @author Sebastien Deleuze + * @author Arjen Poutsma + * @author Rossen Stoyanchev + * @since 5.0 + */ +public class ByteBufferDecoder extends AbstractDataBufferDecoder { + + public ByteBufferDecoder() { + super(MimeTypeUtils.ALL); + } + + + @Override + public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) { + return (ByteBuffer.class.isAssignableFrom(elementType.toClass()) && + super.canDecode(elementType, mimeType)); + } + + @Override + public ByteBuffer decode(DataBuffer dataBuffer, ResolvableType elementType, + @Nullable MimeType mimeType, @Nullable Map hints) { + + int byteCount = dataBuffer.readableByteCount(); + ByteBuffer copy = ByteBuffer.allocate(byteCount); + copy.put(dataBuffer.asByteBuffer()); + copy.flip(); + DataBufferUtils.release(dataBuffer); + if (logger.isDebugEnabled()) { + logger.debug(Hints.getLogPrefix(hints) + "Read " + byteCount + " bytes"); + } + return copy; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/codec/ByteBufferEncoder.java b/spring-core/src/main/java/org/springframework/core/codec/ByteBufferEncoder.java new file mode 100644 index 0000000..8d60066 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/codec/ByteBufferEncoder.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.nio.ByteBuffer; +import java.util.Map; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.lang.Nullable; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +/** + * Encoder for {@link ByteBuffer ByteBuffers}. + * + * @author Sebastien Deleuze + * @since 5.0 + */ +public class ByteBufferEncoder extends AbstractEncoder { + + public ByteBufferEncoder() { + super(MimeTypeUtils.ALL); + } + + + @Override + public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) { + Class clazz = elementType.toClass(); + return super.canEncode(elementType, mimeType) && ByteBuffer.class.isAssignableFrom(clazz); + } + + @Override + public Flux encode(Publisher inputStream, + DataBufferFactory bufferFactory, ResolvableType elementType, @Nullable MimeType mimeType, + @Nullable Map hints) { + + return Flux.from(inputStream).map(byteBuffer -> + encodeValue(byteBuffer, bufferFactory, elementType, mimeType, hints)); + } + + @Override + public DataBuffer encodeValue(ByteBuffer byteBuffer, DataBufferFactory bufferFactory, + ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map hints) { + + DataBuffer dataBuffer = bufferFactory.wrap(byteBuffer); + if (logger.isDebugEnabled() && !Hints.isLoggingSuppressed(hints)) { + String logPrefix = Hints.getLogPrefix(hints); + logger.debug(logPrefix + "Writing " + dataBuffer.readableByteCount() + " bytes"); + } + return dataBuffer; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/codec/CharSequenceEncoder.java b/spring-core/src/main/java/org/springframework/core/codec/CharSequenceEncoder.java new file mode 100644 index 0000000..267a684 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/codec/CharSequenceEncoder.java @@ -0,0 +1,138 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.nio.charset.Charset; +import java.nio.charset.CoderMalfunctionError; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.core.log.LogFormatUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +/** + * Encode from a {@code CharSequence} stream to a bytes stream. + * + * @author Sebastien Deleuze + * @author Arjen Poutsma + * @author Rossen Stoyanchev + * @since 5.0 + * @see StringDecoder + */ +public final class CharSequenceEncoder extends AbstractEncoder { + + /** + * The default charset used by the encoder. + */ + public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + + private final ConcurrentMap charsetToMaxBytesPerChar = + new ConcurrentHashMap<>(3); + + + private CharSequenceEncoder(MimeType... mimeTypes) { + super(mimeTypes); + } + + + @Override + public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) { + Class clazz = elementType.toClass(); + return super.canEncode(elementType, mimeType) && CharSequence.class.isAssignableFrom(clazz); + } + + @Override + public Flux encode(Publisher inputStream, + DataBufferFactory bufferFactory, ResolvableType elementType, + @Nullable MimeType mimeType, @Nullable Map hints) { + + return Flux.from(inputStream).map(charSequence -> + encodeValue(charSequence, bufferFactory, elementType, mimeType, hints)); + } + + @Override + public DataBuffer encodeValue(CharSequence charSequence, DataBufferFactory bufferFactory, + ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map hints) { + + if (!Hints.isLoggingSuppressed(hints)) { + LogFormatUtils.traceDebug(logger, traceOn -> { + String formatted = LogFormatUtils.formatValue(charSequence, !traceOn); + return Hints.getLogPrefix(hints) + "Writing " + formatted; + }); + } + boolean release = true; + Charset charset = getCharset(mimeType); + int capacity = calculateCapacity(charSequence, charset); + DataBuffer dataBuffer = bufferFactory.allocateBuffer(capacity); + try { + dataBuffer.write(charSequence, charset); + release = false; + } + catch (CoderMalfunctionError ex) { + throw new EncodingException("String encoding error: " + ex.getMessage(), ex); + } + finally { + if (release) { + DataBufferUtils.release(dataBuffer); + } + } + return dataBuffer; + } + + int calculateCapacity(CharSequence sequence, Charset charset) { + float maxBytesPerChar = this.charsetToMaxBytesPerChar + .computeIfAbsent(charset, cs -> cs.newEncoder().maxBytesPerChar()); + float maxBytesForSequence = sequence.length() * maxBytesPerChar; + return (int) Math.ceil(maxBytesForSequence); + } + + private Charset getCharset(@Nullable MimeType mimeType) { + if (mimeType != null && mimeType.getCharset() != null) { + return mimeType.getCharset(); + } + else { + return DEFAULT_CHARSET; + } + } + + + /** + * Create a {@code CharSequenceEncoder} that supports only "text/plain". + */ + public static CharSequenceEncoder textPlainOnly() { + return new CharSequenceEncoder(new MimeType("text", "plain", DEFAULT_CHARSET)); + } + + /** + * Create a {@code CharSequenceEncoder} that supports all MIME types. + */ + public static CharSequenceEncoder allMimeTypes() { + return new CharSequenceEncoder(new MimeType("text", "plain", DEFAULT_CHARSET), MimeTypeUtils.ALL); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/codec/CodecException.java b/spring-core/src/main/java/org/springframework/core/codec/CodecException.java new file mode 100644 index 0000000..0cdd1de --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/codec/CodecException.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import org.springframework.core.NestedRuntimeException; +import org.springframework.lang.Nullable; + +/** + * General error that indicates a problem while encoding and decoding to and + * from an Object stream. + * + * @author Sebastien Deleuze + * @author Rossen Stoyanchev + * @since 5.0 + */ +@SuppressWarnings("serial") +public class CodecException extends NestedRuntimeException { + + /** + * Create a new CodecException. + * @param msg the detail message + */ + public CodecException(String msg) { + super(msg); + } + + /** + * Create a new CodecException. + * @param msg the detail message + * @param cause root cause for the exception, if any + */ + public CodecException(String msg, @Nullable Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/codec/DataBufferDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/DataBufferDecoder.java new file mode 100644 index 0000000..34b1503 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/codec/DataBufferDecoder.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.util.Map; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.lang.Nullable; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +/** + * Simple pass-through decoder for {@link DataBuffer DataBuffers}. + * + *

    Note: The data buffers should be released via + * {@link org.springframework.core.io.buffer.DataBufferUtils#release(DataBuffer)} + * after they have been consumed. In addition, if using {@code Flux} or + * {@code Mono} operators such as flatMap, reduce, and others that prefetch, + * cache, and skip or filter out data items internally, please add + * {@code doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release)} to the + * composition chain to ensure cached data buffers are released prior to an + * error or cancellation signal. + * + * @author Arjen Poutsma + * @author Rossen Stoyanchev + * @since 5.0 + */ +public class DataBufferDecoder extends AbstractDataBufferDecoder { + + public DataBufferDecoder() { + super(MimeTypeUtils.ALL); + } + + + @Override + public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) { + return (DataBuffer.class.isAssignableFrom(elementType.toClass()) && + super.canDecode(elementType, mimeType)); + } + + @Override + public Flux decode(Publisher input, ResolvableType elementType, + @Nullable MimeType mimeType, @Nullable Map hints) { + + return Flux.from(input); + } + + @Override + public DataBuffer decode(DataBuffer buffer, ResolvableType elementType, + @Nullable MimeType mimeType, @Nullable Map hints) { + + if (logger.isDebugEnabled()) { + logger.debug(Hints.getLogPrefix(hints) + "Read " + buffer.readableByteCount() + " bytes"); + } + return buffer; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/codec/DataBufferEncoder.java b/spring-core/src/main/java/org/springframework/core/codec/DataBufferEncoder.java new file mode 100644 index 0000000..88e8f1c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/codec/DataBufferEncoder.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.util.Map; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.lang.Nullable; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +/** + * Simple pass-through encoder for {@link DataBuffer DataBuffers}. + * + * @author Arjen Poutsma + * @since 5.0 + */ +public class DataBufferEncoder extends AbstractEncoder { + + public DataBufferEncoder() { + super(MimeTypeUtils.ALL); + } + + + @Override + public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) { + Class clazz = elementType.toClass(); + return super.canEncode(elementType, mimeType) && DataBuffer.class.isAssignableFrom(clazz); + } + + @Override + public Flux encode(Publisher inputStream, + DataBufferFactory bufferFactory, ResolvableType elementType, @Nullable MimeType mimeType, + @Nullable Map hints) { + + Flux flux = Flux.from(inputStream); + if (logger.isDebugEnabled() && !Hints.isLoggingSuppressed(hints)) { + flux = flux.doOnNext(buffer -> logValue(buffer, hints)); + } + return flux; + } + + @Override + public DataBuffer encodeValue(DataBuffer buffer, DataBufferFactory bufferFactory, + ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map hints) { + + if (logger.isDebugEnabled() && !Hints.isLoggingSuppressed(hints)) { + logValue(buffer, hints); + } + return buffer; + } + + private void logValue(DataBuffer buffer, @Nullable Map hints) { + String logPrefix = Hints.getLogPrefix(hints); + logger.debug(logPrefix + "Writing " + buffer.readableByteCount() + " bytes"); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/codec/Decoder.java b/spring-core/src/main/java/org/springframework/core/codec/Decoder.java new file mode 100644 index 0000000..7370a14 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/codec/Decoder.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.MimeType; + +/** + * Strategy for decoding a {@link DataBuffer} input stream into an output stream + * of elements of type {@code }. + * + * @author Sebastien Deleuze + * @author Rossen Stoyanchev + * @since 5.0 + * @param the type of elements in the output stream + */ +public interface Decoder { + + /** + * Whether the decoder supports the given target element type and the MIME + * type of the source stream. + * @param elementType the target element type for the output stream + * @param mimeType the mime type associated with the stream to decode + * (can be {@code null} if not specified) + * @return {@code true} if supported, {@code false} otherwise + */ + boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType); + + /** + * Decode a {@link DataBuffer} input stream into a Flux of {@code T}. + * @param inputStream the {@code DataBuffer} input stream to decode + * @param elementType the expected type of elements in the output stream; + * this type must have been previously passed to the {@link #canDecode} + * method and it must have returned {@code true}. + * @param mimeType the MIME type associated with the input stream (optional) + * @param hints additional information about how to do encode + * @return the output stream with decoded elements + */ + Flux decode(Publisher inputStream, ResolvableType elementType, + @Nullable MimeType mimeType, @Nullable Map hints); + + /** + * Decode a {@link DataBuffer} input stream into a Mono of {@code T}. + * @param inputStream the {@code DataBuffer} input stream to decode + * @param elementType the expected type of elements in the output stream; + * this type must have been previously passed to the {@link #canDecode} + * method and it must have returned {@code true}. + * @param mimeType the MIME type associated with the input stream (optional) + * @param hints additional information about how to do encode + * @return the output stream with the decoded element + */ + Mono decodeToMono(Publisher inputStream, ResolvableType elementType, + @Nullable MimeType mimeType, @Nullable Map hints); + + /** + * Decode a data buffer to an Object of type T. This is useful for scenarios, + * that distinct messages (or events) are decoded and handled individually, + * in fully aggregated form. + * @param buffer the {@code DataBuffer} to decode + * @param targetType the expected output type + * @param mimeType the MIME type associated with the data + * @param hints additional information about how to do encode + * @return the decoded value, possibly {@code null} + * @since 5.2 + */ + @Nullable + default T decode(DataBuffer buffer, ResolvableType targetType, + @Nullable MimeType mimeType, @Nullable Map hints) throws DecodingException { + + CompletableFuture future = decodeToMono(Mono.just(buffer), targetType, mimeType, hints).toFuture(); + Assert.state(future.isDone(), "DataBuffer decoding should have completed."); + + Throwable failure; + try { + return future.get(); + } + catch (ExecutionException ex) { + failure = ex.getCause(); + } + catch (InterruptedException ex) { + failure = ex; + } + throw (failure instanceof CodecException ? (CodecException) failure : + new DecodingException("Failed to decode: " + failure.getMessage(), failure)); + } + + /** + * Return the list of MIME types this decoder supports. + */ + List getDecodableMimeTypes(); + +} diff --git a/spring-core/src/main/java/org/springframework/core/codec/DecodingException.java b/spring-core/src/main/java/org/springframework/core/codec/DecodingException.java new file mode 100644 index 0000000..f2ec58a --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/codec/DecodingException.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import org.springframework.lang.Nullable; + +/** + * Indicates an issue with decoding the input stream with a focus on content + * related issues such as a parse failure. As opposed to more general I/O + * errors, illegal state, or a {@link CodecException} such as a configuration + * issue that a {@link Decoder} may choose to raise. + * + *

    For example in server web application, a {@code DecodingException} would + * translate to a response with a 400 (bad input) status while + * {@code CodecException} would translate to 500 (server error) status. + * + * @author Rossen Stoyanchev + * @since 5.0 + * @see Decoder + */ +@SuppressWarnings("serial") +public class DecodingException extends CodecException { + + /** + * Create a new DecodingException. + * @param msg the detail message + */ + public DecodingException(String msg) { + super(msg); + } + + /** + * Create a new DecodingException. + * @param msg the detail message + * @param cause root cause for the exception, if any + */ + public DecodingException(String msg, @Nullable Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/codec/Encoder.java b/spring-core/src/main/java/org/springframework/core/codec/Encoder.java new file mode 100644 index 0000000..aa4f5d8 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/codec/Encoder.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.util.List; +import java.util.Map; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.lang.Nullable; +import org.springframework.util.MimeType; + +/** + * Strategy to encode a stream of Objects of type {@code } into an output + * stream of bytes. + * + * @author Sebastien Deleuze + * @author Rossen Stoyanchev + * @since 5.0 + * @param the type of elements in the input stream + */ +public interface Encoder { + + /** + * Whether the encoder supports the given source element type and the MIME + * type for the output stream. + * @param elementType the type of elements in the source stream + * @param mimeType the MIME type for the output stream + * (can be {@code null} if not specified) + * @return {@code true} if supported, {@code false} otherwise + */ + boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType); + + /** + * Encode a stream of Objects of type {@code T} into a {@link DataBuffer} + * output stream. + * @param inputStream the input stream of Objects to encode. If the input should be + * encoded as a single value rather than as a stream of elements, an instance of + * {@link Mono} should be used. + * @param bufferFactory for creating output stream {@code DataBuffer}'s + * @param elementType the expected type of elements in the input stream; + * this type must have been previously passed to the {@link #canEncode} + * method and it must have returned {@code true}. + * @param mimeType the MIME type for the output content (optional) + * @param hints additional information about how to encode + * @return the output stream + */ + Flux encode(Publisher inputStream, DataBufferFactory bufferFactory, + ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map hints); + + /** + * Encode an Object of type T to a data buffer. This is useful for scenarios, + * that distinct messages (or events) are encoded and handled individually, + * in fully aggregated form. + *

    By default this method raises {@link UnsupportedOperationException} + * and it is expected that some encoders cannot produce a single buffer or + * cannot do so synchronously (e.g. encoding a {@code Resource}). + * @param value the value to be encoded + * @param bufferFactory for creating the output {@code DataBuffer} + * @param valueType the type for the value being encoded + * @param mimeType the MIME type for the output content (optional) + * @param hints additional information about how to encode + * @return the encoded content + * @since 5.2 + */ + default DataBuffer encodeValue(T value, DataBufferFactory bufferFactory, + ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map hints) { + + // It may not be possible to produce a single DataBuffer synchronously + throw new UnsupportedOperationException(); + } + + /** + * Return the list of mime types this encoder supports. + */ + List getEncodableMimeTypes(); + +} diff --git a/spring-core/src/main/java/org/springframework/core/codec/EncodingException.java b/spring-core/src/main/java/org/springframework/core/codec/EncodingException.java new file mode 100644 index 0000000..015c013 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/codec/EncodingException.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import org.springframework.lang.Nullable; + +/** + * Indicates an issue with encoding the input Object stream with a focus on + * not being able to encode Objects. As opposed to a more general I/O errors + * or a {@link CodecException} such as a configuration issue that an + * {@link Encoder} may also choose to raise. + * + * @author Rossen Stoyanchev + * @since 5.0 + * @see Encoder + */ +@SuppressWarnings("serial") +public class EncodingException extends CodecException { + + /** + * Create a new EncodingException. + * @param msg the detail message + */ + public EncodingException(String msg) { + super(msg); + } + + /** + * Create a new EncodingException. + * @param msg the detail message + * @param cause root cause for the exception, if any + */ + public EncodingException(String msg, @Nullable Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/codec/Hints.java b/spring-core/src/main/java/org/springframework/core/codec/Hints.java new file mode 100644 index 0000000..a731b01 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/codec/Hints.java @@ -0,0 +1,171 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.util.Collections; +import java.util.Map; + +import org.apache.commons.logging.Log; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; + +/** + * Constants and convenience methods for working with hints. + * + * @author Rossen Stoyanchev + * @since 5.1 + * @see ResourceRegionEncoder#BOUNDARY_STRING_HINT + */ +public abstract class Hints { + + /** + * Name of hint exposing a prefix to use for correlating log messages. + */ + public static final String LOG_PREFIX_HINT = Log.class.getName() + ".PREFIX"; + + /** + * Name of boolean hint whether to avoid logging data either because it's + * potentially sensitive, or because it has been logged by a composite + * encoder, e.g. for multipart requests. + */ + public static final String SUPPRESS_LOGGING_HINT = Log.class.getName() + ".SUPPRESS_LOGGING"; + + + /** + * Create a map wit a single hint via {@link Collections#singletonMap}. + * @param hintName the hint name + * @param value the hint value + * @return the created map + */ + public static Map from(String hintName, Object value) { + return Collections.singletonMap(hintName, value); + } + + /** + * Return an empty map of hints via {@link Collections#emptyMap()}. + * @return the empty map + */ + public static Map none() { + return Collections.emptyMap(); + } + + /** + * Obtain the value for a required hint. + * @param hints the hints map + * @param hintName the required hint name + * @param the hint type to cast to + * @return the hint value + * @throws IllegalArgumentException if the hint is not found + */ + @SuppressWarnings("unchecked") + public static T getRequiredHint(@Nullable Map hints, String hintName) { + if (hints == null) { + throw new IllegalArgumentException("No hints map for required hint '" + hintName + "'"); + } + T hint = (T) hints.get(hintName); + if (hint == null) { + throw new IllegalArgumentException("Hints map must contain the hint '" + hintName + "'"); + } + return hint; + } + + /** + * Obtain the hint {@link #LOG_PREFIX_HINT}, if present, or an empty String. + * @param hints the hints passed to the encode method + * @return the log prefix + */ + public static String getLogPrefix(@Nullable Map hints) { + return (hints != null ? (String) hints.getOrDefault(LOG_PREFIX_HINT, "") : ""); + } + + /** + * Whether to suppress logging based on the hint {@link #SUPPRESS_LOGGING_HINT}. + * @param hints the hints map + * @return whether logging of data is allowed + */ + public static boolean isLoggingSuppressed(@Nullable Map hints) { + return (hints != null && (boolean) hints.getOrDefault(SUPPRESS_LOGGING_HINT, false)); + } + + /** + * Merge two maps of hints, creating and copying into a new map if both have + * values, or returning the non-empty map, or an empty map if both are empty. + * @param hints1 1st map of hints + * @param hints2 2nd map of hints + * @return a single map with hints from both + */ + public static Map merge(Map hints1, Map hints2) { + if (hints1.isEmpty() && hints2.isEmpty()) { + return Collections.emptyMap(); + } + else if (hints2.isEmpty()) { + return hints1; + } + else if (hints1.isEmpty()) { + return hints2; + } + else { + Map result = CollectionUtils.newHashMap(hints1.size() + hints2.size()); + result.putAll(hints1); + result.putAll(hints2); + return result; + } + } + + /** + * Merge a single hint into a map of hints, possibly creating and copying + * all hints into a new map, or otherwise if the map of hints is empty, + * creating a new single entry map. + * @param hints a map of hints to be merge + * @param hintName the hint name to merge + * @param hintValue the hint value to merge + * @return a single map with all hints + */ + public static Map merge(Map hints, String hintName, Object hintValue) { + if (hints.isEmpty()) { + return Collections.singletonMap(hintName, hintValue); + } + else { + Map result = CollectionUtils.newHashMap(hints.size() + 1); + result.putAll(hints); + result.put(hintName, hintValue); + return result; + } + } + + /** + * If the hints contain a {@link #LOG_PREFIX_HINT} and the given logger has + * DEBUG level enabled, apply the log prefix as a hint to the given buffer + * via {@link DataBufferUtils#touch(DataBuffer, Object)}. + * @param buffer the buffer to touch + * @param hints the hints map to check for a log prefix + * @param logger the logger whose level to check + * @since 5.3.2 + */ + public static void touchDataBuffer(DataBuffer buffer, @Nullable Map hints, Log logger) { + if (logger.isDebugEnabled() && hints != null) { + Object logPrefix = hints.get(LOG_PREFIX_HINT); + if (logPrefix != null) { + DataBufferUtils.touch(buffer, logPrefix); + } + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/codec/NettyByteBufDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/NettyByteBufDecoder.java new file mode 100644 index 0000000..ebd9f8d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/codec/NettyByteBufDecoder.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.util.Map; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.core.io.buffer.NettyDataBuffer; +import org.springframework.lang.Nullable; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +/** + * Decoder for {@link ByteBuf ByteBufs}. + * + * @author Vladislav Kisel + * @since 5.3 + */ +public class NettyByteBufDecoder extends AbstractDataBufferDecoder { + + public NettyByteBufDecoder() { + super(MimeTypeUtils.ALL); + } + + + @Override + public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) { + return (ByteBuf.class.isAssignableFrom(elementType.toClass()) && + super.canDecode(elementType, mimeType)); + } + + @Override + public ByteBuf decode(DataBuffer dataBuffer, ResolvableType elementType, + @Nullable MimeType mimeType, @Nullable Map hints) { + + if (logger.isDebugEnabled()) { + logger.debug(Hints.getLogPrefix(hints) + "Read " + dataBuffer.readableByteCount() + " bytes"); + } + if (dataBuffer instanceof NettyDataBuffer) { + return ((NettyDataBuffer) dataBuffer).getNativeBuffer(); + } + ByteBuf byteBuf; + byte[] bytes = new byte[dataBuffer.readableByteCount()]; + dataBuffer.read(bytes); + byteBuf = Unpooled.wrappedBuffer(bytes); + DataBufferUtils.release(dataBuffer); + return byteBuf; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/codec/NettyByteBufEncoder.java b/spring-core/src/main/java/org/springframework/core/codec/NettyByteBufEncoder.java new file mode 100644 index 0000000..dca9002 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/codec/NettyByteBufEncoder.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.util.Map; + +import io.netty.buffer.ByteBuf; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.NettyDataBufferFactory; +import org.springframework.lang.Nullable; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +/** + * Encoder for {@link ByteBuf ByteBufs}. + * + * @author Vladislav Kisel + * @since 5.3 + */ +public class NettyByteBufEncoder extends AbstractEncoder { + + public NettyByteBufEncoder() { + super(MimeTypeUtils.ALL); + } + + + @Override + public boolean canEncode(ResolvableType type, @Nullable MimeType mimeType) { + Class clazz = type.toClass(); + return super.canEncode(type, mimeType) && ByteBuf.class.isAssignableFrom(clazz); + } + + @Override + public Flux encode(Publisher inputStream, + DataBufferFactory bufferFactory, ResolvableType elementType, @Nullable MimeType mimeType, + @Nullable Map hints) { + + return Flux.from(inputStream).map(byteBuffer -> + encodeValue(byteBuffer, bufferFactory, elementType, mimeType, hints)); + } + + @Override + public DataBuffer encodeValue(ByteBuf byteBuf, DataBufferFactory bufferFactory, + ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map hints) { + + if (logger.isDebugEnabled() && !Hints.isLoggingSuppressed(hints)) { + String logPrefix = Hints.getLogPrefix(hints); + logger.debug(logPrefix + "Writing " + byteBuf.readableBytes() + " bytes"); + } + if (bufferFactory instanceof NettyDataBufferFactory) { + return ((NettyDataBufferFactory) bufferFactory).wrap(byteBuf); + } + byte[] bytes = new byte[byteBuf.readableBytes()]; + byteBuf.readBytes(bytes); + byteBuf.release(); + return bufferFactory.wrap(bytes); + } +} diff --git a/spring-core/src/main/java/org/springframework/core/codec/ResourceDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/ResourceDecoder.java new file mode 100644 index 0000000..4e9552a --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/codec/ResourceDecoder.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.io.ByteArrayInputStream; +import java.util.Map; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +/** + * Decoder for {@link Resource Resources}. + * + * @author Arjen Poutsma + * @author Rossen Stoyanchev + * @since 5.0 + */ +public class ResourceDecoder extends AbstractDataBufferDecoder { + + /** Name of hint with a filename for the resource(e.g. from "Content-Disposition" HTTP header). */ + public static String FILENAME_HINT = ResourceDecoder.class.getName() + ".filename"; + + + public ResourceDecoder() { + super(MimeTypeUtils.ALL); + } + + + @Override + public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) { + return (Resource.class.isAssignableFrom(elementType.toClass()) && + super.canDecode(elementType, mimeType)); + } + + @Override + public Flux decode(Publisher inputStream, ResolvableType elementType, + @Nullable MimeType mimeType, @Nullable Map hints) { + + return Flux.from(decodeToMono(inputStream, elementType, mimeType, hints)); + } + + @Override + public Resource decode(DataBuffer dataBuffer, ResolvableType elementType, + @Nullable MimeType mimeType, @Nullable Map hints) { + + byte[] bytes = new byte[dataBuffer.readableByteCount()]; + dataBuffer.read(bytes); + DataBufferUtils.release(dataBuffer); + + if (logger.isDebugEnabled()) { + logger.debug(Hints.getLogPrefix(hints) + "Read " + bytes.length + " bytes"); + } + + Class clazz = elementType.toClass(); + String filename = hints != null ? (String) hints.get(FILENAME_HINT) : null; + if (clazz == InputStreamResource.class) { + return new InputStreamResource(new ByteArrayInputStream(bytes)) { + @Override + public String getFilename() { + return filename; + } + @Override + public long contentLength() { + return bytes.length; + } + }; + } + else if (Resource.class.isAssignableFrom(clazz)) { + return new ByteArrayResource(bytes) { + @Override + public String getFilename() { + return filename; + } + }; + } + else { + throw new IllegalStateException("Unsupported resource class: " + clazz); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/codec/ResourceEncoder.java b/spring-core/src/main/java/org/springframework/core/codec/ResourceEncoder.java new file mode 100644 index 0000000..58b0a09 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/codec/ResourceEncoder.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.util.Map; + +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.Resource; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; +import org.springframework.util.StreamUtils; + +/** + * Encoder for {@link Resource Resources}. + * + * @author Arjen Poutsma + * @since 5.0 + */ +public class ResourceEncoder extends AbstractSingleValueEncoder { + + /** + * The default buffer size used by the encoder. + */ + public static final int DEFAULT_BUFFER_SIZE = StreamUtils.BUFFER_SIZE; + + private final int bufferSize; + + + public ResourceEncoder() { + this(DEFAULT_BUFFER_SIZE); + } + + public ResourceEncoder(int bufferSize) { + super(MimeTypeUtils.APPLICATION_OCTET_STREAM, MimeTypeUtils.ALL); + Assert.isTrue(bufferSize > 0, "'bufferSize' must be larger than 0"); + this.bufferSize = bufferSize; + } + + + @Override + public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) { + Class clazz = elementType.toClass(); + return (super.canEncode(elementType, mimeType) && Resource.class.isAssignableFrom(clazz)); + } + + @Override + protected Flux encode(Resource resource, DataBufferFactory bufferFactory, + ResolvableType type, @Nullable MimeType mimeType, @Nullable Map hints) { + + if (logger.isDebugEnabled() && !Hints.isLoggingSuppressed(hints)) { + String logPrefix = Hints.getLogPrefix(hints); + logger.debug(logPrefix + "Writing [" + resource + "]"); + } + return DataBufferUtils.read(resource, bufferFactory, this.bufferSize); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/codec/ResourceRegionEncoder.java b/spring-core/src/main/java/org/springframework/core/codec/ResourceRegionEncoder.java new file mode 100644 index 0000000..3330ac6 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/codec/ResourceRegionEncoder.java @@ -0,0 +1,179 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.OptionalLong; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.core.io.support.ResourceRegion; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; +import org.springframework.util.StreamUtils; + +/** + * Encoder for {@link ResourceRegion ResourceRegions}. + * + * @author Brian Clozel + * @since 5.0 + */ +public class ResourceRegionEncoder extends AbstractEncoder { + + /** + * The default buffer size used by the encoder. + */ + public static final int DEFAULT_BUFFER_SIZE = StreamUtils.BUFFER_SIZE; + + /** + * The hint key that contains the boundary string. + */ + public static final String BOUNDARY_STRING_HINT = ResourceRegionEncoder.class.getName() + ".boundaryString"; + + private final int bufferSize; + + + public ResourceRegionEncoder() { + this(DEFAULT_BUFFER_SIZE); + } + + public ResourceRegionEncoder(int bufferSize) { + super(MimeTypeUtils.APPLICATION_OCTET_STREAM, MimeTypeUtils.ALL); + Assert.isTrue(bufferSize > 0, "'bufferSize' must be larger than 0"); + this.bufferSize = bufferSize; + } + + @Override + public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) { + return super.canEncode(elementType, mimeType) + && ResourceRegion.class.isAssignableFrom(elementType.toClass()); + } + + @Override + public Flux encode(Publisher input, + DataBufferFactory bufferFactory, ResolvableType elementType, @Nullable MimeType mimeType, + @Nullable Map hints) { + + Assert.notNull(input, "'inputStream' must not be null"); + Assert.notNull(bufferFactory, "'bufferFactory' must not be null"); + Assert.notNull(elementType, "'elementType' must not be null"); + + if (input instanceof Mono) { + return Mono.from(input) + .flatMapMany(region -> { + if (!region.getResource().isReadable()) { + return Flux.error(new EncodingException( + "Resource " + region.getResource() + " is not readable")); + } + return writeResourceRegion(region, bufferFactory, hints); + }); + } + else { + final String boundaryString = Hints.getRequiredHint(hints, BOUNDARY_STRING_HINT); + byte[] startBoundary = toAsciiBytes("\r\n--" + boundaryString + "\r\n"); + byte[] contentType = mimeType != null ? toAsciiBytes("Content-Type: " + mimeType + "\r\n") : new byte[0]; + + return Flux.from(input) + .concatMap(region -> { + if (!region.getResource().isReadable()) { + return Flux.error(new EncodingException( + "Resource " + region.getResource() + " is not readable")); + } + Flux prefix = Flux.just( + bufferFactory.wrap(startBoundary), + bufferFactory.wrap(contentType), + bufferFactory.wrap(getContentRangeHeader(region))); // only wrapping, no allocation + + return prefix.concatWith(writeResourceRegion(region, bufferFactory, hints)); + }) + .concatWithValues(getRegionSuffix(bufferFactory, boundaryString)); + } + // No doOnDiscard (no caching after DataBufferUtils#read) + } + + private Flux writeResourceRegion( + ResourceRegion region, DataBufferFactory bufferFactory, @Nullable Map hints) { + + Resource resource = region.getResource(); + long position = region.getPosition(); + long count = region.getCount(); + + if (logger.isDebugEnabled() && !Hints.isLoggingSuppressed(hints)) { + logger.debug(Hints.getLogPrefix(hints) + + "Writing region " + position + "-" + (position + count) + " of [" + resource + "]"); + } + + Flux in = DataBufferUtils.read(resource, position, bufferFactory, this.bufferSize); + if (logger.isDebugEnabled()) { + in = in.doOnNext(buffer -> Hints.touchDataBuffer(buffer, hints, logger)); + } + return DataBufferUtils.takeUntilByteCount(in, count); + } + + private DataBuffer getRegionSuffix(DataBufferFactory bufferFactory, String boundaryString) { + byte[] endBoundary = toAsciiBytes("\r\n--" + boundaryString + "--"); + return bufferFactory.wrap(endBoundary); + } + + private byte[] toAsciiBytes(String in) { + return in.getBytes(StandardCharsets.US_ASCII); + } + + private byte[] getContentRangeHeader(ResourceRegion region) { + long start = region.getPosition(); + long end = start + region.getCount() - 1; + OptionalLong contentLength = contentLength(region.getResource()); + if (contentLength.isPresent()) { + long length = contentLength.getAsLong(); + return toAsciiBytes("Content-Range: bytes " + start + '-' + end + '/' + length + "\r\n\r\n"); + } + else { + return toAsciiBytes("Content-Range: bytes " + start + '-' + end + "\r\n\r\n"); + } + } + + /** + * Determine, if possible, the contentLength of the given resource without reading it. + * @param resource the resource instance + * @return the contentLength of the resource + */ + private OptionalLong contentLength(Resource resource) { + // Don't try to determine contentLength on InputStreamResource - cannot be read afterwards... + // Note: custom InputStreamResource subclasses could provide a pre-calculated content length! + if (InputStreamResource.class != resource.getClass()) { + try { + return OptionalLong.of(resource.contentLength()); + } + catch (IOException ignored) { + } + } + return OptionalLong.empty(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/codec/StringDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/StringDecoder.java new file mode 100644 index 0000000..48c0da6 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/codec/StringDecoder.java @@ -0,0 +1,263 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.core.io.buffer.LimitedDataBufferList; +import org.springframework.core.io.buffer.PooledDataBuffer; +import org.springframework.core.log.LogFormatUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +/** + * Decode from a data buffer stream to a {@code String} stream, either splitting + * or aggregating incoming data chunks to realign along newlines delimiters + * and produce a stream of strings. This is useful for streaming but is also + * necessary to ensure that that multibyte characters can be decoded correctly, + * avoiding split-character issues. The default delimiters used by default are + * {@code \n} and {@code \r\n} but that can be customized. + * + * @author Sebastien Deleuze + * @author Brian Clozel + * @author Arjen Poutsma + * @author Mark Paluch + * @since 5.0 + * @see CharSequenceEncoder + */ +public final class StringDecoder extends AbstractDataBufferDecoder { + + /** The default charset to use, i.e. "UTF-8". */ + public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + + /** The default delimiter strings to use, i.e. {@code \r\n} and {@code \n}. */ + public static final List DEFAULT_DELIMITERS = Arrays.asList("\r\n", "\n"); + + + private final List delimiters; + + private final boolean stripDelimiter; + + private Charset defaultCharset = DEFAULT_CHARSET; + + private final ConcurrentMap delimitersCache = new ConcurrentHashMap<>(); + + + private StringDecoder(List delimiters, boolean stripDelimiter, MimeType... mimeTypes) { + super(mimeTypes); + Assert.notEmpty(delimiters, "'delimiters' must not be empty"); + this.delimiters = new ArrayList<>(delimiters); + this.stripDelimiter = stripDelimiter; + } + + + /** + * Set the default character set to fall back on if the MimeType does not specify any. + *

    By default this is {@code UTF-8}. + * @param defaultCharset the charset to fall back on + * @since 5.2.9 + */ + public void setDefaultCharset(Charset defaultCharset) { + this.defaultCharset = defaultCharset; + } + + /** + * Return the configured {@link #setDefaultCharset(Charset) defaultCharset}. + * @since 5.2.9 + */ + public Charset getDefaultCharset() { + return this.defaultCharset; + } + + + @Override + public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) { + return (elementType.resolve() == String.class && super.canDecode(elementType, mimeType)); + } + + @Override + public Flux decode(Publisher input, ResolvableType elementType, + @Nullable MimeType mimeType, @Nullable Map hints) { + + byte[][] delimiterBytes = getDelimiterBytes(mimeType); + + LimitedDataBufferList chunks = new LimitedDataBufferList(getMaxInMemorySize()); + DataBufferUtils.Matcher matcher = DataBufferUtils.matcher(delimiterBytes); + + return Flux.from(input) + .concatMapIterable(buffer -> processDataBuffer(buffer, matcher, chunks)) + .concatWith(Mono.defer(() -> { + if (chunks.isEmpty()) { + return Mono.empty(); + } + DataBuffer lastBuffer = chunks.get(0).factory().join(chunks); + chunks.clear(); + return Mono.just(lastBuffer); + })) + .doOnTerminate(chunks::releaseAndClear) + .doOnDiscard(PooledDataBuffer.class, PooledDataBuffer::release) + .map(buffer -> decode(buffer, elementType, mimeType, hints)); + } + + private byte[][] getDelimiterBytes(@Nullable MimeType mimeType) { + return this.delimitersCache.computeIfAbsent(getCharset(mimeType), charset -> { + byte[][] result = new byte[this.delimiters.size()][]; + for (int i = 0; i < this.delimiters.size(); i++) { + result[i] = this.delimiters.get(i).getBytes(charset); + } + return result; + }); + } + + private Collection processDataBuffer( + DataBuffer buffer, DataBufferUtils.Matcher matcher, LimitedDataBufferList chunks) { + + try { + List result = null; + do { + int endIndex = matcher.match(buffer); + if (endIndex == -1) { + chunks.add(buffer); + DataBufferUtils.retain(buffer); // retain after add (may raise DataBufferLimitException) + break; + } + int startIndex = buffer.readPosition(); + int length = (endIndex - startIndex + 1); + DataBuffer slice = buffer.retainedSlice(startIndex, length); + if (this.stripDelimiter) { + slice.writePosition(slice.writePosition() - matcher.delimiter().length); + } + result = (result != null ? result : new ArrayList<>()); + if (chunks.isEmpty()) { + result.add(slice); + } + else { + chunks.add(slice); + result.add(buffer.factory().join(chunks)); + chunks.clear(); + } + buffer.readPosition(endIndex + 1); + } + while (buffer.readableByteCount() > 0); + return (result != null ? result : Collections.emptyList()); + } + finally { + DataBufferUtils.release(buffer); + } + } + + @Override + public String decode(DataBuffer dataBuffer, ResolvableType elementType, + @Nullable MimeType mimeType, @Nullable Map hints) { + + Charset charset = getCharset(mimeType); + CharBuffer charBuffer = charset.decode(dataBuffer.asByteBuffer()); + DataBufferUtils.release(dataBuffer); + String value = charBuffer.toString(); + LogFormatUtils.traceDebug(logger, traceOn -> { + String formatted = LogFormatUtils.formatValue(value, !traceOn); + return Hints.getLogPrefix(hints) + "Decoded " + formatted; + }); + return value; + } + + private Charset getCharset(@Nullable MimeType mimeType) { + if (mimeType != null && mimeType.getCharset() != null) { + return mimeType.getCharset(); + } + else { + return getDefaultCharset(); + } + } + + /** + * Create a {@code StringDecoder} for {@code "text/plain"}. + * @param stripDelimiter this flag is ignored + * @deprecated as of Spring 5.0.4, in favor of {@link #textPlainOnly()} or + * {@link #textPlainOnly(List, boolean)} + */ + @Deprecated + public static StringDecoder textPlainOnly(boolean stripDelimiter) { + return textPlainOnly(); + } + + /** + * Create a {@code StringDecoder} for {@code "text/plain"}. + */ + public static StringDecoder textPlainOnly() { + return textPlainOnly(DEFAULT_DELIMITERS, true); + } + + /** + * Create a {@code StringDecoder} for {@code "text/plain"}. + * @param delimiters delimiter strings to use to split the input stream + * @param stripDelimiter whether to remove delimiters from the resulting + * input strings + */ + public static StringDecoder textPlainOnly(List delimiters, boolean stripDelimiter) { + return new StringDecoder(delimiters, stripDelimiter, new MimeType("text", "plain", DEFAULT_CHARSET)); + } + + /** + * Create a {@code StringDecoder} that supports all MIME types. + * @param stripDelimiter this flag is ignored + * @deprecated as of Spring 5.0.4, in favor of {@link #allMimeTypes()} or + * {@link #allMimeTypes(List, boolean)} + */ + @Deprecated + public static StringDecoder allMimeTypes(boolean stripDelimiter) { + return allMimeTypes(); + } + + /** + * Create a {@code StringDecoder} that supports all MIME types. + */ + public static StringDecoder allMimeTypes() { + return allMimeTypes(DEFAULT_DELIMITERS, true); + } + + /** + * Create a {@code StringDecoder} that supports all MIME types. + * @param delimiters delimiter strings to use to split the input stream + * @param stripDelimiter whether to remove delimiters from the resulting + * input strings + */ + public static StringDecoder allMimeTypes(List delimiters, boolean stripDelimiter) { + return new StringDecoder(delimiters, stripDelimiter, + new MimeType("text", "plain", DEFAULT_CHARSET), MimeTypeUtils.ALL); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/codec/package-info.java b/spring-core/src/main/java/org/springframework/core/codec/package-info.java new file mode 100644 index 0000000..acf70ed --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/codec/package-info.java @@ -0,0 +1,11 @@ +/** + * {@link org.springframework.core.codec.Encoder} and + * {@link org.springframework.core.codec.Decoder} abstractions to convert + * between a reactive stream of bytes and Java objects. + */ +@NonNullApi +@NonNullFields +package org.springframework.core.codec; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/main/java/org/springframework/core/convert/ConversionException.java b/spring-core/src/main/java/org/springframework/core/convert/ConversionException.java new file mode 100644 index 0000000..17550b0 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/ConversionException.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2010 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert; + +import org.springframework.core.NestedRuntimeException; + +/** + * Base class for exceptions thrown by the conversion system. + * + * @author Keith Donald + * @since 3.0 + */ +@SuppressWarnings("serial") +public abstract class ConversionException extends NestedRuntimeException { + + /** + * Construct a new conversion exception. + * @param message the exception message + */ + public ConversionException(String message) { + super(message); + } + + /** + * Construct a new conversion exception. + * @param message the exception message + * @param cause the cause + */ + public ConversionException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/ConversionFailedException.java b/spring-core/src/main/java/org/springframework/core/convert/ConversionFailedException.java new file mode 100644 index 0000000..8cb7b4e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/ConversionFailedException.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert; + +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +/** + * Exception to be thrown when an actual type conversion attempt fails. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + */ +@SuppressWarnings("serial") +public class ConversionFailedException extends ConversionException { + + @Nullable + private final TypeDescriptor sourceType; + + private final TypeDescriptor targetType; + + @Nullable + private final Object value; + + + /** + * Create a new conversion exception. + * @param sourceType the value's original type + * @param targetType the value's target type + * @param value the value we tried to convert + * @param cause the cause of the conversion failure + */ + public ConversionFailedException(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType, + @Nullable Object value, Throwable cause) { + + super("Failed to convert from type [" + sourceType + "] to type [" + targetType + + "] for value '" + ObjectUtils.nullSafeToString(value) + "'", cause); + this.sourceType = sourceType; + this.targetType = targetType; + this.value = value; + } + + + /** + * Return the source type we tried to convert the value from. + */ + @Nullable + public TypeDescriptor getSourceType() { + return this.sourceType; + } + + /** + * Return the target type we tried to convert the value to. + */ + public TypeDescriptor getTargetType() { + return this.targetType; + } + + /** + * Return the offending value. + */ + @Nullable + public Object getValue() { + return this.value; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/ConversionService.java b/spring-core/src/main/java/org/springframework/core/convert/ConversionService.java new file mode 100644 index 0000000..92b33cd --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/ConversionService.java @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert; + +import org.springframework.lang.Nullable; + +/** + * A service interface for type conversion. This is the entry point into the convert system. + * Call {@link #convert(Object, Class)} to perform a thread-safe type conversion using this system. + * + * @author Keith Donald + * @author Phillip Webb + * @since 3.0 + */ +public interface ConversionService { + + /** + * Return {@code true} if objects of {@code sourceType} can be converted to the {@code targetType}. + *

    If this method returns {@code true}, it means {@link #convert(Object, Class)} is capable + * of converting an instance of {@code sourceType} to {@code targetType}. + *

    Special note on collections, arrays, and maps types: + * For conversion between collection, array, and map types, this method will return {@code true} + * even though a convert invocation may still generate a {@link ConversionException} if the + * underlying elements are not convertible. Callers are expected to handle this exceptional case + * when working with collections and maps. + * @param sourceType the source type to convert from (may be {@code null} if source is {@code null}) + * @param targetType the target type to convert to (required) + * @return {@code true} if a conversion can be performed, {@code false} if not + * @throws IllegalArgumentException if {@code targetType} is {@code null} + */ + boolean canConvert(@Nullable Class sourceType, Class targetType); + + /** + * Return {@code true} if objects of {@code sourceType} can be converted to the {@code targetType}. + * The TypeDescriptors provide additional context about the source and target locations + * where conversion would occur, often object fields or property locations. + *

    If this method returns {@code true}, it means {@link #convert(Object, TypeDescriptor, TypeDescriptor)} + * is capable of converting an instance of {@code sourceType} to {@code targetType}. + *

    Special note on collections, arrays, and maps types: + * For conversion between collection, array, and map types, this method will return {@code true} + * even though a convert invocation may still generate a {@link ConversionException} if the + * underlying elements are not convertible. Callers are expected to handle this exceptional case + * when working with collections and maps. + * @param sourceType context about the source type to convert from + * (may be {@code null} if source is {@code null}) + * @param targetType context about the target type to convert to (required) + * @return {@code true} if a conversion can be performed between the source and target types, + * {@code false} if not + * @throws IllegalArgumentException if {@code targetType} is {@code null} + */ + boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType); + + /** + * Convert the given {@code source} to the specified {@code targetType}. + * @param source the source object to convert (may be {@code null}) + * @param targetType the target type to convert to (required) + * @return the converted object, an instance of targetType + * @throws ConversionException if a conversion exception occurred + * @throws IllegalArgumentException if targetType is {@code null} + */ + @Nullable + T convert(@Nullable Object source, Class targetType); + + /** + * Convert the given {@code source} to the specified {@code targetType}. + * The TypeDescriptors provide additional context about the source and target locations + * where conversion will occur, often object fields or property locations. + * @param source the source object to convert (may be {@code null}) + * @param sourceType context about the source type to convert from + * (may be {@code null} if source is {@code null}) + * @param targetType context about the target type to convert to (required) + * @return the converted object, an instance of {@link TypeDescriptor#getObjectType() targetType} + * @throws ConversionException if a conversion exception occurred + * @throws IllegalArgumentException if targetType is {@code null}, + * or {@code sourceType} is {@code null} but source is not {@code null} + */ + @Nullable + Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType); + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/ConverterNotFoundException.java b/spring-core/src/main/java/org/springframework/core/convert/ConverterNotFoundException.java new file mode 100644 index 0000000..e4dd3c5 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/ConverterNotFoundException.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert; + +import org.springframework.lang.Nullable; + +/** + * Exception to be thrown when a suitable converter could not be found + * in a given conversion service. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + */ +@SuppressWarnings("serial") +public class ConverterNotFoundException extends ConversionException { + + @Nullable + private final TypeDescriptor sourceType; + + private final TypeDescriptor targetType; + + + /** + * Create a new conversion executor not found exception. + * @param sourceType the source type requested to convert from + * @param targetType the target type requested to convert to + */ + public ConverterNotFoundException(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType) { + super("No converter found capable of converting from type [" + sourceType + "] to type [" + targetType + "]"); + this.sourceType = sourceType; + this.targetType = targetType; + } + + + /** + * Return the source type that was requested to convert from. + */ + @Nullable + public TypeDescriptor getSourceType() { + return this.sourceType; + } + + /** + * Return the target type that was requested to convert to. + */ + public TypeDescriptor getTargetType() { + return this.targetType; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/Property.java b/spring-core/src/main/java/org/springframework/core/convert/Property.java new file mode 100644 index 0000000..5cddaea --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/Property.java @@ -0,0 +1,281 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.core.MethodParameter; +import org.springframework.lang.Nullable; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * A description of a JavaBeans Property that allows us to avoid a dependency on + * {@code java.beans.PropertyDescriptor}. The {@code java.beans} package + * is not available in a number of environments (e.g. Android, Java ME), so this is + * desirable for portability of Spring's core conversion facility. + * + *

    Used to build a {@link TypeDescriptor} from a property location. The built + * {@code TypeDescriptor} can then be used to convert from/to the property type. + * + * @author Keith Donald + * @author Phillip Webb + * @since 3.1 + * @see TypeDescriptor#TypeDescriptor(Property) + * @see TypeDescriptor#nested(Property, int) + */ +public final class Property { + + private static Map annotationCache = new ConcurrentReferenceHashMap<>(); + + private final Class objectType; + + @Nullable + private final Method readMethod; + + @Nullable + private final Method writeMethod; + + private final String name; + + private final MethodParameter methodParameter; + + @Nullable + private Annotation[] annotations; + + + public Property(Class objectType, @Nullable Method readMethod, @Nullable Method writeMethod) { + this(objectType, readMethod, writeMethod, null); + } + + public Property( + Class objectType, @Nullable Method readMethod, @Nullable Method writeMethod, @Nullable String name) { + + this.objectType = objectType; + this.readMethod = readMethod; + this.writeMethod = writeMethod; + this.methodParameter = resolveMethodParameter(); + this.name = (name != null ? name : resolveName()); + } + + + /** + * The object declaring this property, either directly or in a superclass the object extends. + */ + public Class getObjectType() { + return this.objectType; + } + + /** + * The name of the property: e.g. 'foo' + */ + public String getName() { + return this.name; + } + + /** + * The property type: e.g. {@code java.lang.String} + */ + public Class getType() { + return this.methodParameter.getParameterType(); + } + + /** + * The property getter method: e.g. {@code getFoo()} + */ + @Nullable + public Method getReadMethod() { + return this.readMethod; + } + + /** + * The property setter method: e.g. {@code setFoo(String)} + */ + @Nullable + public Method getWriteMethod() { + return this.writeMethod; + } + + + // Package private + + MethodParameter getMethodParameter() { + return this.methodParameter; + } + + Annotation[] getAnnotations() { + if (this.annotations == null) { + this.annotations = resolveAnnotations(); + } + return this.annotations; + } + + + // Internal helpers + + private String resolveName() { + if (this.readMethod != null) { + int index = this.readMethod.getName().indexOf("get"); + if (index != -1) { + index += 3; + } + else { + index = this.readMethod.getName().indexOf("is"); + if (index != -1) { + index += 2; + } + else { + // Record-style plain accessor method, e.g. name() + index = 0; + } + } + return StringUtils.uncapitalize(this.readMethod.getName().substring(index)); + } + else if (this.writeMethod != null) { + int index = this.writeMethod.getName().indexOf("set"); + if (index == -1) { + throw new IllegalArgumentException("Not a setter method"); + } + index += 3; + return StringUtils.uncapitalize(this.writeMethod.getName().substring(index)); + } + else { + throw new IllegalStateException("Property is neither readable nor writeable"); + } + } + + private MethodParameter resolveMethodParameter() { + MethodParameter read = resolveReadMethodParameter(); + MethodParameter write = resolveWriteMethodParameter(); + if (write == null) { + if (read == null) { + throw new IllegalStateException("Property is neither readable nor writeable"); + } + return read; + } + if (read != null) { + Class readType = read.getParameterType(); + Class writeType = write.getParameterType(); + if (!writeType.equals(readType) && writeType.isAssignableFrom(readType)) { + return read; + } + } + return write; + } + + @Nullable + private MethodParameter resolveReadMethodParameter() { + if (getReadMethod() == null) { + return null; + } + return new MethodParameter(getReadMethod(), -1).withContainingClass(getObjectType()); + } + + @Nullable + private MethodParameter resolveWriteMethodParameter() { + if (getWriteMethod() == null) { + return null; + } + return new MethodParameter(getWriteMethod(), 0).withContainingClass(getObjectType()); + } + + private Annotation[] resolveAnnotations() { + Annotation[] annotations = annotationCache.get(this); + if (annotations == null) { + Map, Annotation> annotationMap = new LinkedHashMap<>(); + addAnnotationsToMap(annotationMap, getReadMethod()); + addAnnotationsToMap(annotationMap, getWriteMethod()); + addAnnotationsToMap(annotationMap, getField()); + annotations = annotationMap.values().toArray(new Annotation[0]); + annotationCache.put(this, annotations); + } + return annotations; + } + + private void addAnnotationsToMap( + Map, Annotation> annotationMap, @Nullable AnnotatedElement object) { + + if (object != null) { + for (Annotation annotation : object.getAnnotations()) { + annotationMap.put(annotation.annotationType(), annotation); + } + } + } + + @Nullable + private Field getField() { + String name = getName(); + if (!StringUtils.hasLength(name)) { + return null; + } + Field field = null; + Class declaringClass = declaringClass(); + if (declaringClass != null) { + field = ReflectionUtils.findField(declaringClass, name); + if (field == null) { + // Same lenient fallback checking as in CachedIntrospectionResults... + field = ReflectionUtils.findField(declaringClass, StringUtils.uncapitalize(name)); + if (field == null) { + field = ReflectionUtils.findField(declaringClass, StringUtils.capitalize(name)); + } + } + } + return field; + } + + @Nullable + private Class declaringClass() { + if (getReadMethod() != null) { + return getReadMethod().getDeclaringClass(); + } + else if (getWriteMethod() != null) { + return getWriteMethod().getDeclaringClass(); + } + else { + return null; + } + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof Property)) { + return false; + } + Property otherProperty = (Property) other; + return (ObjectUtils.nullSafeEquals(this.objectType, otherProperty.objectType) && + ObjectUtils.nullSafeEquals(this.name, otherProperty.name) && + ObjectUtils.nullSafeEquals(this.readMethod, otherProperty.readMethod) && + ObjectUtils.nullSafeEquals(this.writeMethod, otherProperty.writeMethod)); + } + + @Override + public int hashCode() { + return (ObjectUtils.nullSafeHashCode(this.objectType) * 31 + ObjectUtils.nullSafeHashCode(this.name)); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java new file mode 100644 index 0000000..3e2aee8 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java @@ -0,0 +1,799 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert; + +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Field; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; + +/** + * Contextual descriptor about a type to convert from or to. + * Capable of representing arrays and generic collection types. + * + * @author Keith Donald + * @author Andy Clement + * @author Juergen Hoeller + * @author Phillip Webb + * @author Sam Brannen + * @author Stephane Nicoll + * @since 3.0 + * @see ConversionService#canConvert(TypeDescriptor, TypeDescriptor) + * @see ConversionService#convert(Object, TypeDescriptor, TypeDescriptor) + */ +@SuppressWarnings("serial") +public class TypeDescriptor implements Serializable { + + private static final Annotation[] EMPTY_ANNOTATION_ARRAY = new Annotation[0]; + + private static final Map, TypeDescriptor> commonTypesCache = new HashMap<>(32); + + private static final Class[] CACHED_COMMON_TYPES = { + boolean.class, Boolean.class, byte.class, Byte.class, char.class, Character.class, + double.class, Double.class, float.class, Float.class, int.class, Integer.class, + long.class, Long.class, short.class, Short.class, String.class, Object.class}; + + static { + for (Class preCachedClass : CACHED_COMMON_TYPES) { + commonTypesCache.put(preCachedClass, valueOf(preCachedClass)); + } + } + + + private final Class type; + + private final ResolvableType resolvableType; + + private final AnnotatedElementAdapter annotatedElement; + + + /** + * Create a new type descriptor from a {@link MethodParameter}. + *

    Use this constructor when a source or target conversion point is a + * constructor parameter, method parameter, or method return value. + * @param methodParameter the method parameter + */ + public TypeDescriptor(MethodParameter methodParameter) { + this.resolvableType = ResolvableType.forMethodParameter(methodParameter); + this.type = this.resolvableType.resolve(methodParameter.getNestedParameterType()); + this.annotatedElement = new AnnotatedElementAdapter(methodParameter.getParameterIndex() == -1 ? + methodParameter.getMethodAnnotations() : methodParameter.getParameterAnnotations()); + } + + /** + * Create a new type descriptor from a {@link Field}. + *

    Use this constructor when a source or target conversion point is a field. + * @param field the field + */ + public TypeDescriptor(Field field) { + this.resolvableType = ResolvableType.forField(field); + this.type = this.resolvableType.resolve(field.getType()); + this.annotatedElement = new AnnotatedElementAdapter(field.getAnnotations()); + } + + /** + * Create a new type descriptor from a {@link Property}. + *

    Use this constructor when a source or target conversion point is a + * property on a Java class. + * @param property the property + */ + public TypeDescriptor(Property property) { + Assert.notNull(property, "Property must not be null"); + this.resolvableType = ResolvableType.forMethodParameter(property.getMethodParameter()); + this.type = this.resolvableType.resolve(property.getType()); + this.annotatedElement = new AnnotatedElementAdapter(property.getAnnotations()); + } + + /** + * Create a new type descriptor from a {@link ResolvableType}. + *

    This constructor is used internally and may also be used by subclasses + * that support non-Java languages with extended type systems. It is public + * as of 5.1.4 whereas it was protected before. + * @param resolvableType the resolvable type + * @param type the backing type (or {@code null} if it should get resolved) + * @param annotations the type annotations + * @since 4.0 + */ + public TypeDescriptor(ResolvableType resolvableType, @Nullable Class type, @Nullable Annotation[] annotations) { + this.resolvableType = resolvableType; + this.type = (type != null ? type : resolvableType.toClass()); + this.annotatedElement = new AnnotatedElementAdapter(annotations); + } + + + /** + * Variation of {@link #getType()} that accounts for a primitive type by + * returning its object wrapper type. + *

    This is useful for conversion service implementations that wish to + * normalize to object-based types and not work with primitive types directly. + */ + public Class getObjectType() { + return ClassUtils.resolvePrimitiveIfNecessary(getType()); + } + + /** + * The type of the backing class, method parameter, field, or property + * described by this TypeDescriptor. + *

    Returns primitive types as-is. See {@link #getObjectType()} for a + * variation of this operation that resolves primitive types to their + * corresponding Object types if necessary. + * @see #getObjectType() + */ + public Class getType() { + return this.type; + } + + /** + * Return the underlying {@link ResolvableType}. + * @since 4.0 + */ + public ResolvableType getResolvableType() { + return this.resolvableType; + } + + /** + * Return the underlying source of the descriptor. Will return a {@link Field}, + * {@link MethodParameter} or {@link Type} depending on how the {@link TypeDescriptor} + * was constructed. This method is primarily to provide access to additional + * type information or meta-data that alternative JVM languages may provide. + * @since 4.0 + */ + public Object getSource() { + return this.resolvableType.getSource(); + } + + /** + * Narrows this {@link TypeDescriptor} by setting its type to the class of the + * provided value. + *

    If the value is {@code null}, no narrowing is performed and this TypeDescriptor + * is returned unchanged. + *

    Designed to be called by binding frameworks when they read property, field, + * or method return values. Allows such frameworks to narrow a TypeDescriptor built + * from a declared property, field, or method return value type. For example, a field + * declared as {@code java.lang.Object} would be narrowed to {@code java.util.HashMap} + * if it was set to a {@code java.util.HashMap} value. The narrowed TypeDescriptor + * can then be used to convert the HashMap to some other type. Annotation and nested + * type context is preserved by the narrowed copy. + * @param value the value to use for narrowing this type descriptor + * @return this TypeDescriptor narrowed (returns a copy with its type updated to the + * class of the provided value) + */ + public TypeDescriptor narrow(@Nullable Object value) { + if (value == null) { + return this; + } + ResolvableType narrowed = ResolvableType.forType(value.getClass(), getResolvableType()); + return new TypeDescriptor(narrowed, value.getClass(), getAnnotations()); + } + + /** + * Cast this {@link TypeDescriptor} to a superclass or implemented interface + * preserving annotations and nested type context. + * @param superType the super type to cast to (can be {@code null}) + * @return a new TypeDescriptor for the up-cast type + * @throws IllegalArgumentException if this type is not assignable to the super-type + * @since 3.2 + */ + @Nullable + public TypeDescriptor upcast(@Nullable Class superType) { + if (superType == null) { + return null; + } + Assert.isAssignable(superType, getType()); + return new TypeDescriptor(getResolvableType().as(superType), superType, getAnnotations()); + } + + /** + * Return the name of this type: the fully qualified class name. + */ + public String getName() { + return ClassUtils.getQualifiedName(getType()); + } + + /** + * Is this type a primitive type? + */ + public boolean isPrimitive() { + return getType().isPrimitive(); + } + + /** + * Return the annotations associated with this type descriptor, if any. + * @return the annotations, or an empty array if none + */ + public Annotation[] getAnnotations() { + return this.annotatedElement.getAnnotations(); + } + + /** + * Determine if this type descriptor has the specified annotation. + *

    As of Spring Framework 4.2, this method supports arbitrary levels + * of meta-annotations. + * @param annotationType the annotation type + * @return true if the annotation is present + */ + public boolean hasAnnotation(Class annotationType) { + if (this.annotatedElement.isEmpty()) { + // Shortcut: AnnotatedElementUtils would have to expect AnnotatedElement.getAnnotations() + // to return a copy of the array, whereas we can do it more efficiently here. + return false; + } + return AnnotatedElementUtils.isAnnotated(this.annotatedElement, annotationType); + } + + /** + * Obtain the annotation of the specified {@code annotationType} that is on this type descriptor. + *

    As of Spring Framework 4.2, this method supports arbitrary levels of meta-annotations. + * @param annotationType the annotation type + * @return the annotation, or {@code null} if no such annotation exists on this type descriptor + */ + @Nullable + public T getAnnotation(Class annotationType) { + if (this.annotatedElement.isEmpty()) { + // Shortcut: AnnotatedElementUtils would have to expect AnnotatedElement.getAnnotations() + // to return a copy of the array, whereas we can do it more efficiently here. + return null; + } + return AnnotatedElementUtils.getMergedAnnotation(this.annotatedElement, annotationType); + } + + /** + * Returns true if an object of this type descriptor can be assigned to the location + * described by the given type descriptor. + *

    For example, {@code valueOf(String.class).isAssignableTo(valueOf(CharSequence.class))} + * returns {@code true} because a String value can be assigned to a CharSequence variable. + * On the other hand, {@code valueOf(Number.class).isAssignableTo(valueOf(Integer.class))} + * returns {@code false} because, while all Integers are Numbers, not all Numbers are Integers. + *

    For arrays, collections, and maps, element and key/value types are checked if declared. + * For example, a List<String> field value is assignable to a Collection<CharSequence> + * field, but List<Number> is not assignable to List<Integer>. + * @return {@code true} if this type is assignable to the type represented by the provided + * type descriptor + * @see #getObjectType() + */ + public boolean isAssignableTo(TypeDescriptor typeDescriptor) { + boolean typesAssignable = typeDescriptor.getObjectType().isAssignableFrom(getObjectType()); + if (!typesAssignable) { + return false; + } + if (isArray() && typeDescriptor.isArray()) { + return isNestedAssignable(getElementTypeDescriptor(), typeDescriptor.getElementTypeDescriptor()); + } + else if (isCollection() && typeDescriptor.isCollection()) { + return isNestedAssignable(getElementTypeDescriptor(), typeDescriptor.getElementTypeDescriptor()); + } + else if (isMap() && typeDescriptor.isMap()) { + return isNestedAssignable(getMapKeyTypeDescriptor(), typeDescriptor.getMapKeyTypeDescriptor()) && + isNestedAssignable(getMapValueTypeDescriptor(), typeDescriptor.getMapValueTypeDescriptor()); + } + else { + return true; + } + } + + private boolean isNestedAssignable(@Nullable TypeDescriptor nestedTypeDescriptor, + @Nullable TypeDescriptor otherNestedTypeDescriptor) { + + return (nestedTypeDescriptor == null || otherNestedTypeDescriptor == null || + nestedTypeDescriptor.isAssignableTo(otherNestedTypeDescriptor)); + } + + /** + * Is this type a {@link Collection} type? + */ + public boolean isCollection() { + return Collection.class.isAssignableFrom(getType()); + } + + /** + * Is this type an array type? + */ + public boolean isArray() { + return getType().isArray(); + } + + /** + * If this type is an array, returns the array's component type. + * If this type is a {@code Stream}, returns the stream's component type. + * If this type is a {@link Collection} and it is parameterized, returns the Collection's element type. + * If the Collection is not parameterized, returns {@code null} indicating the element type is not declared. + * @return the array component type or Collection element type, or {@code null} if this type is not + * an array type or a {@code java.util.Collection} or if its element type is not parameterized + * @see #elementTypeDescriptor(Object) + */ + @Nullable + public TypeDescriptor getElementTypeDescriptor() { + if (getResolvableType().isArray()) { + return new TypeDescriptor(getResolvableType().getComponentType(), null, getAnnotations()); + } + if (Stream.class.isAssignableFrom(getType())) { + return getRelatedIfResolvable(this, getResolvableType().as(Stream.class).getGeneric(0)); + } + return getRelatedIfResolvable(this, getResolvableType().asCollection().getGeneric(0)); + } + + /** + * If this type is a {@link Collection} or an array, creates a element TypeDescriptor + * from the provided collection or array element. + *

    Narrows the {@link #getElementTypeDescriptor() elementType} property to the class + * of the provided collection or array element. For example, if this describes a + * {@code java.util.List<java.lang.Number<} and the element argument is an + * {@code java.lang.Integer}, the returned TypeDescriptor will be {@code java.lang.Integer}. + * If this describes a {@code java.util.List<?>} and the element argument is an + * {@code java.lang.Integer}, the returned TypeDescriptor will be {@code java.lang.Integer} + * as well. + *

    Annotation and nested type context will be preserved in the narrowed + * TypeDescriptor that is returned. + * @param element the collection or array element + * @return a element type descriptor, narrowed to the type of the provided element + * @see #getElementTypeDescriptor() + * @see #narrow(Object) + */ + @Nullable + public TypeDescriptor elementTypeDescriptor(Object element) { + return narrow(element, getElementTypeDescriptor()); + } + + /** + * Is this type a {@link Map} type? + */ + public boolean isMap() { + return Map.class.isAssignableFrom(getType()); + } + + /** + * If this type is a {@link Map} and its key type is parameterized, + * returns the map's key type. If the Map's key type is not parameterized, + * returns {@code null} indicating the key type is not declared. + * @return the Map key type, or {@code null} if this type is a Map + * but its key type is not parameterized + * @throws IllegalStateException if this type is not a {@code java.util.Map} + */ + @Nullable + public TypeDescriptor getMapKeyTypeDescriptor() { + Assert.state(isMap(), "Not a [java.util.Map]"); + return getRelatedIfResolvable(this, getResolvableType().asMap().getGeneric(0)); + } + + /** + * If this type is a {@link Map}, creates a mapKey {@link TypeDescriptor} + * from the provided map key. + *

    Narrows the {@link #getMapKeyTypeDescriptor() mapKeyType} property + * to the class of the provided map key. For example, if this describes a + * {@code java.util.Map<java.lang.Number, java.lang.String<} and the key + * argument is a {@code java.lang.Integer}, the returned TypeDescriptor will be + * {@code java.lang.Integer}. If this describes a {@code java.util.Map<?, ?>} + * and the key argument is a {@code java.lang.Integer}, the returned + * TypeDescriptor will be {@code java.lang.Integer} as well. + *

    Annotation and nested type context will be preserved in the narrowed + * TypeDescriptor that is returned. + * @param mapKey the map key + * @return the map key type descriptor + * @throws IllegalStateException if this type is not a {@code java.util.Map} + * @see #narrow(Object) + */ + @Nullable + public TypeDescriptor getMapKeyTypeDescriptor(Object mapKey) { + return narrow(mapKey, getMapKeyTypeDescriptor()); + } + + /** + * If this type is a {@link Map} and its value type is parameterized, + * returns the map's value type. + *

    If the Map's value type is not parameterized, returns {@code null} + * indicating the value type is not declared. + * @return the Map value type, or {@code null} if this type is a Map + * but its value type is not parameterized + * @throws IllegalStateException if this type is not a {@code java.util.Map} + */ + @Nullable + public TypeDescriptor getMapValueTypeDescriptor() { + Assert.state(isMap(), "Not a [java.util.Map]"); + return getRelatedIfResolvable(this, getResolvableType().asMap().getGeneric(1)); + } + + /** + * If this type is a {@link Map}, creates a mapValue {@link TypeDescriptor} + * from the provided map value. + *

    Narrows the {@link #getMapValueTypeDescriptor() mapValueType} property + * to the class of the provided map value. For example, if this describes a + * {@code java.util.Map<java.lang.String, java.lang.Number<} and the value + * argument is a {@code java.lang.Integer}, the returned TypeDescriptor will be + * {@code java.lang.Integer}. If this describes a {@code java.util.Map<?, ?>} + * and the value argument is a {@code java.lang.Integer}, the returned + * TypeDescriptor will be {@code java.lang.Integer} as well. + *

    Annotation and nested type context will be preserved in the narrowed + * TypeDescriptor that is returned. + * @param mapValue the map value + * @return the map value type descriptor + * @throws IllegalStateException if this type is not a {@code java.util.Map} + * @see #narrow(Object) + */ + @Nullable + public TypeDescriptor getMapValueTypeDescriptor(Object mapValue) { + return narrow(mapValue, getMapValueTypeDescriptor()); + } + + @Nullable + private TypeDescriptor narrow(@Nullable Object value, @Nullable TypeDescriptor typeDescriptor) { + if (typeDescriptor != null) { + return typeDescriptor.narrow(value); + } + if (value != null) { + return narrow(value); + } + return null; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof TypeDescriptor)) { + return false; + } + TypeDescriptor otherDesc = (TypeDescriptor) other; + if (getType() != otherDesc.getType()) { + return false; + } + if (!annotationsMatch(otherDesc)) { + return false; + } + if (isCollection() || isArray()) { + return ObjectUtils.nullSafeEquals(getElementTypeDescriptor(), otherDesc.getElementTypeDescriptor()); + } + else if (isMap()) { + return (ObjectUtils.nullSafeEquals(getMapKeyTypeDescriptor(), otherDesc.getMapKeyTypeDescriptor()) && + ObjectUtils.nullSafeEquals(getMapValueTypeDescriptor(), otherDesc.getMapValueTypeDescriptor())); + } + else { + return true; + } + } + + private boolean annotationsMatch(TypeDescriptor otherDesc) { + Annotation[] anns = getAnnotations(); + Annotation[] otherAnns = otherDesc.getAnnotations(); + if (anns == otherAnns) { + return true; + } + if (anns.length != otherAnns.length) { + return false; + } + if (anns.length > 0) { + for (int i = 0; i < anns.length; i++) { + if (!annotationEquals(anns[i], otherAnns[i])) { + return false; + } + } + } + return true; + } + + private boolean annotationEquals(Annotation ann, Annotation otherAnn) { + // Annotation.equals is reflective and pretty slow, so let's check identity and proxy type first. + return (ann == otherAnn || (ann.getClass() == otherAnn.getClass() && ann.equals(otherAnn))); + } + + @Override + public int hashCode() { + return getType().hashCode(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + for (Annotation ann : getAnnotations()) { + builder.append("@").append(ann.annotationType().getName()).append(' '); + } + builder.append(getResolvableType().toString()); + return builder.toString(); + } + + + /** + * Create a new type descriptor for an object. + *

    Use this factory method to introspect a source object before asking the + * conversion system to convert it to some another type. + *

    If the provided object is {@code null}, returns {@code null}, else calls + * {@link #valueOf(Class)} to build a TypeDescriptor from the object's class. + * @param source the source object + * @return the type descriptor + */ + @Nullable + public static TypeDescriptor forObject(@Nullable Object source) { + return (source != null ? valueOf(source.getClass()) : null); + } + + /** + * Create a new type descriptor from the given type. + *

    Use this to instruct the conversion system to convert an object to a + * specific target type, when no type location such as a method parameter or + * field is available to provide additional conversion context. + *

    Generally prefer use of {@link #forObject(Object)} for constructing type + * descriptors from source objects, as it handles the {@code null} object case. + * @param type the class (may be {@code null} to indicate {@code Object.class}) + * @return the corresponding type descriptor + */ + public static TypeDescriptor valueOf(@Nullable Class type) { + if (type == null) { + type = Object.class; + } + TypeDescriptor desc = commonTypesCache.get(type); + return (desc != null ? desc : new TypeDescriptor(ResolvableType.forClass(type), null, null)); + } + + /** + * Create a new type descriptor from a {@link java.util.Collection} type. + *

    Useful for converting to typed Collections. + *

    For example, a {@code List} could be converted to a + * {@code List} by converting to a targetType built with this method. + * The method call to construct such a {@code TypeDescriptor} would look something + * like: {@code collection(List.class, TypeDescriptor.valueOf(EmailAddress.class));} + * @param collectionType the collection type, which must implement {@link Collection}. + * @param elementTypeDescriptor a descriptor for the collection's element type, + * used to convert collection elements + * @return the collection type descriptor + */ + public static TypeDescriptor collection(Class collectionType, @Nullable TypeDescriptor elementTypeDescriptor) { + Assert.notNull(collectionType, "Collection type must not be null"); + if (!Collection.class.isAssignableFrom(collectionType)) { + throw new IllegalArgumentException("Collection type must be a [java.util.Collection]"); + } + ResolvableType element = (elementTypeDescriptor != null ? elementTypeDescriptor.resolvableType : null); + return new TypeDescriptor(ResolvableType.forClassWithGenerics(collectionType, element), null, null); + } + + /** + * Create a new type descriptor from a {@link java.util.Map} type. + *

    Useful for converting to typed Maps. + *

    For example, a Map<String, String> could be converted to a Map<Id, EmailAddress> + * by converting to a targetType built with this method: + * The method call to construct such a TypeDescriptor would look something like: + *

    +	 * map(Map.class, TypeDescriptor.valueOf(Id.class), TypeDescriptor.valueOf(EmailAddress.class));
    +	 * 
    + * @param mapType the map type, which must implement {@link Map} + * @param keyTypeDescriptor a descriptor for the map's key type, used to convert map keys + * @param valueTypeDescriptor the map's value type, used to convert map values + * @return the map type descriptor + */ + public static TypeDescriptor map(Class mapType, @Nullable TypeDescriptor keyTypeDescriptor, + @Nullable TypeDescriptor valueTypeDescriptor) { + + Assert.notNull(mapType, "Map type must not be null"); + if (!Map.class.isAssignableFrom(mapType)) { + throw new IllegalArgumentException("Map type must be a [java.util.Map]"); + } + ResolvableType key = (keyTypeDescriptor != null ? keyTypeDescriptor.resolvableType : null); + ResolvableType value = (valueTypeDescriptor != null ? valueTypeDescriptor.resolvableType : null); + return new TypeDescriptor(ResolvableType.forClassWithGenerics(mapType, key, value), null, null); + } + + /** + * Create a new type descriptor as an array of the specified type. + *

    For example to create a {@code Map[]} use: + *

    +	 * TypeDescriptor.array(TypeDescriptor.map(Map.class, TypeDescriptor.value(String.class), TypeDescriptor.value(String.class)));
    +	 * 
    + * @param elementTypeDescriptor the {@link TypeDescriptor} of the array element or {@code null} + * @return an array {@link TypeDescriptor} or {@code null} if {@code elementTypeDescriptor} is {@code null} + * @since 3.2.1 + */ + @Nullable + public static TypeDescriptor array(@Nullable TypeDescriptor elementTypeDescriptor) { + if (elementTypeDescriptor == null) { + return null; + } + return new TypeDescriptor(ResolvableType.forArrayComponent(elementTypeDescriptor.resolvableType), + null, elementTypeDescriptor.getAnnotations()); + } + + /** + * Create a type descriptor for a nested type declared within the method parameter. + *

    For example, if the methodParameter is a {@code List} and the + * nesting level is 1, the nested type descriptor will be String.class. + *

    If the methodParameter is a {@code List>} and the nesting + * level is 2, the nested type descriptor will also be a String.class. + *

    If the methodParameter is a {@code Map} and the nesting + * level is 1, the nested type descriptor will be String, derived from the map value. + *

    If the methodParameter is a {@code List>} and the + * nesting level is 2, the nested type descriptor will be String, derived from the map value. + *

    Returns {@code null} if a nested type cannot be obtained because it was not declared. + * For example, if the method parameter is a {@code List}, the nested type + * descriptor returned will be {@code null}. + * @param methodParameter the method parameter with a nestingLevel of 1 + * @param nestingLevel the nesting level of the collection/array element or + * map key/value declaration within the method parameter + * @return the nested type descriptor at the specified nesting level, + * or {@code null} if it could not be obtained + * @throws IllegalArgumentException if the nesting level of the input + * {@link MethodParameter} argument is not 1, or if the types up to the + * specified nesting level are not of collection, array, or map types + */ + @Nullable + public static TypeDescriptor nested(MethodParameter methodParameter, int nestingLevel) { + if (methodParameter.getNestingLevel() != 1) { + throw new IllegalArgumentException("MethodParameter nesting level must be 1: " + + "use the nestingLevel parameter to specify the desired nestingLevel for nested type traversal"); + } + return nested(new TypeDescriptor(methodParameter), nestingLevel); + } + + /** + * Create a type descriptor for a nested type declared within the field. + *

    For example, if the field is a {@code List} and the nesting + * level is 1, the nested type descriptor will be {@code String.class}. + *

    If the field is a {@code List>} and the nesting level is + * 2, the nested type descriptor will also be a {@code String.class}. + *

    If the field is a {@code Map} and the nesting level + * is 1, the nested type descriptor will be String, derived from the map value. + *

    If the field is a {@code List>} and the nesting + * level is 2, the nested type descriptor will be String, derived from the map value. + *

    Returns {@code null} if a nested type cannot be obtained because it was not + * declared. For example, if the field is a {@code List}, the nested type + * descriptor returned will be {@code null}. + * @param field the field + * @param nestingLevel the nesting level of the collection/array element or + * map key/value declaration within the field + * @return the nested type descriptor at the specified nesting level, + * or {@code null} if it could not be obtained + * @throws IllegalArgumentException if the types up to the specified nesting + * level are not of collection, array, or map types + */ + @Nullable + public static TypeDescriptor nested(Field field, int nestingLevel) { + return nested(new TypeDescriptor(field), nestingLevel); + } + + /** + * Create a type descriptor for a nested type declared within the property. + *

    For example, if the property is a {@code List} and the nesting + * level is 1, the nested type descriptor will be {@code String.class}. + *

    If the property is a {@code List>} and the nesting level + * is 2, the nested type descriptor will also be a {@code String.class}. + *

    If the property is a {@code Map} and the nesting level + * is 1, the nested type descriptor will be String, derived from the map value. + *

    If the property is a {@code List>} and the nesting + * level is 2, the nested type descriptor will be String, derived from the map value. + *

    Returns {@code null} if a nested type cannot be obtained because it was not + * declared. For example, if the property is a {@code List}, the nested type + * descriptor returned will be {@code null}. + * @param property the property + * @param nestingLevel the nesting level of the collection/array element or + * map key/value declaration within the property + * @return the nested type descriptor at the specified nesting level, or + * {@code null} if it could not be obtained + * @throws IllegalArgumentException if the types up to the specified nesting + * level are not of collection, array, or map types + */ + @Nullable + public static TypeDescriptor nested(Property property, int nestingLevel) { + return nested(new TypeDescriptor(property), nestingLevel); + } + + @Nullable + private static TypeDescriptor nested(TypeDescriptor typeDescriptor, int nestingLevel) { + ResolvableType nested = typeDescriptor.resolvableType; + for (int i = 0; i < nestingLevel; i++) { + if (Object.class == nested.getType()) { + // Could be a collection type but we don't know about its element type, + // so let's just assume there is an element type of type Object... + } + else { + nested = nested.getNested(2); + } + } + if (nested == ResolvableType.NONE) { + return null; + } + return getRelatedIfResolvable(typeDescriptor, nested); + } + + @Nullable + private static TypeDescriptor getRelatedIfResolvable(TypeDescriptor source, ResolvableType type) { + if (type.resolve() == null) { + return null; + } + return new TypeDescriptor(type, null, source.getAnnotations()); + } + + + /** + * Adapter class for exposing a {@code TypeDescriptor}'s annotations as an + * {@link AnnotatedElement}, in particular to {@link AnnotatedElementUtils}. + * @see AnnotatedElementUtils#isAnnotated(AnnotatedElement, Class) + * @see AnnotatedElementUtils#getMergedAnnotation(AnnotatedElement, Class) + */ + private class AnnotatedElementAdapter implements AnnotatedElement, Serializable { + + @Nullable + private final Annotation[] annotations; + + public AnnotatedElementAdapter(@Nullable Annotation[] annotations) { + this.annotations = annotations; + } + + @Override + public boolean isAnnotationPresent(Class annotationClass) { + for (Annotation annotation : getAnnotations()) { + if (annotation.annotationType() == annotationClass) { + return true; + } + } + return false; + } + + @Override + @Nullable + @SuppressWarnings("unchecked") + public T getAnnotation(Class annotationClass) { + for (Annotation annotation : getAnnotations()) { + if (annotation.annotationType() == annotationClass) { + return (T) annotation; + } + } + return null; + } + + @Override + public Annotation[] getAnnotations() { + return (this.annotations != null ? this.annotations.clone() : EMPTY_ANNOTATION_ARRAY); + } + + @Override + public Annotation[] getDeclaredAnnotations() { + return getAnnotations(); + } + + public boolean isEmpty() { + return ObjectUtils.isEmpty(this.annotations); + } + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof AnnotatedElementAdapter && + Arrays.equals(this.annotations, ((AnnotatedElementAdapter) other).annotations))); + } + + @Override + public int hashCode() { + return Arrays.hashCode(this.annotations); + } + + @Override + public String toString() { + return TypeDescriptor.this.toString(); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/converter/ConditionalConverter.java b/spring-core/src/main/java/org/springframework/core/convert/converter/ConditionalConverter.java new file mode 100644 index 0000000..3a29353 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/converter/ConditionalConverter.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.converter; + +import org.springframework.core.convert.TypeDescriptor; + +/** + * Allows a {@link Converter}, {@link GenericConverter} or {@link ConverterFactory} to + * conditionally execute based on attributes of the {@code source} and {@code target} + * {@link TypeDescriptor}. + * + *

    Often used to selectively match custom conversion logic based on the presence of a + * field or class-level characteristic, such as an annotation or method. For example, when + * converting from a String field to a Date field, an implementation might return + * {@code true} if the target field has also been annotated with {@code @DateTimeFormat}. + * + *

    As another example, when converting from a String field to an {@code Account} field, + * an implementation might return {@code true} if the target Account class defines a + * {@code public static findAccount(String)} method. + * + * @author Phillip Webb + * @author Keith Donald + * @since 3.2 + * @see Converter + * @see GenericConverter + * @see ConverterFactory + * @see ConditionalGenericConverter + */ +public interface ConditionalConverter { + + /** + * Should the conversion from {@code sourceType} to {@code targetType} currently under + * consideration be selected? + * @param sourceType the type descriptor of the field we are converting from + * @param targetType the type descriptor of the field we are converting to + * @return true if conversion should be performed, false otherwise + */ + boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType); + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/converter/ConditionalGenericConverter.java b/spring-core/src/main/java/org/springframework/core/convert/converter/ConditionalGenericConverter.java new file mode 100644 index 0000000..a4e7791 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/converter/ConditionalGenericConverter.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.converter; + +import org.springframework.core.convert.TypeDescriptor; + +/** + * A {@link GenericConverter} that may conditionally execute based on attributes + * of the {@code source} and {@code target} {@link TypeDescriptor}. + * + *

    See {@link ConditionalConverter} for details. + * + * @author Keith Donald + * @author Phillip Webb + * @since 3.0 + * @see GenericConverter + * @see ConditionalConverter + */ +public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter { + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/converter/Converter.java b/spring-core/src/main/java/org/springframework/core/convert/converter/Converter.java new file mode 100644 index 0000000..c1bc0d2 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/converter/Converter.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.converter; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * A converter converts a source object of type {@code S} to a target of type {@code T}. + * + *

    Implementations of this interface are thread-safe and can be shared. + * + *

    Implementations may additionally implement {@link ConditionalConverter}. + * + * @author Keith Donald + * @author Josh Cummings + * @since 3.0 + * @param the source type + * @param the target type + */ +@FunctionalInterface +public interface Converter { + + /** + * Convert the source object of type {@code S} to target type {@code T}. + * @param source the source object to convert, which must be an instance of {@code S} (never {@code null}) + * @return the converted object, which must be an instance of {@code T} (potentially {@code null}) + * @throws IllegalArgumentException if the source cannot be converted to the desired target type + */ + @Nullable + T convert(S source); + + /** + * Construct a composed {@link Converter} that first applies this {@link Converter} + * to its input, and then applies the {@code after} {@link Converter} to the + * result. + * @param after the {@link Converter} to apply after this {@link Converter} + * is applied + * @param the type of output of both the {@code after} {@link Converter} + * and the composed {@link Converter} + * @return a composed {@link Converter} that first applies this {@link Converter} + * and then applies the {@code after} {@link Converter} + * @since 5.3 + */ + default Converter andThen(Converter after) { + Assert.notNull(after, "After Converter must not be null"); + return (S s) -> { + T initialResult = convert(s); + return (initialResult != null ? after.convert(initialResult) : null); + }; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/converter/ConverterFactory.java b/spring-core/src/main/java/org/springframework/core/convert/converter/ConverterFactory.java new file mode 100644 index 0000000..2863224 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/converter/ConverterFactory.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.converter; + +/** + * A factory for "ranged" converters that can convert objects from S to subtypes of R. + * + *

    Implementations may additionally implement {@link ConditionalConverter}. + * + * @author Keith Donald + * @since 3.0 + * @param the source type converters created by this factory can convert from + * @param the target range (or base) type converters created by this factory can convert to; + * for example {@link Number} for a set of number subtypes. + * @see ConditionalConverter + */ +public interface ConverterFactory { + + /** + * Get the converter to convert from S to target type T, where T is also an instance of R. + * @param the target type + * @param targetType the target type to convert to + * @return a converter from S to T + */ + Converter getConverter(Class targetType); + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/converter/ConverterRegistry.java b/spring-core/src/main/java/org/springframework/core/convert/converter/ConverterRegistry.java new file mode 100644 index 0000000..f70209d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/converter/ConverterRegistry.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.converter; + +/** + * For registering converters with a type conversion system. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + */ +public interface ConverterRegistry { + + /** + * Add a plain converter to this registry. + * The convertible source/target type pair is derived from the Converter's parameterized types. + * @throws IllegalArgumentException if the parameterized types could not be resolved + */ + void addConverter(Converter converter); + + /** + * Add a plain converter to this registry. + * The convertible source/target type pair is specified explicitly. + *

    Allows for a Converter to be reused for multiple distinct pairs without + * having to create a Converter class for each pair. + * @since 3.1 + */ + void addConverter(Class sourceType, Class targetType, Converter converter); + + /** + * Add a generic converter to this registry. + */ + void addConverter(GenericConverter converter); + + /** + * Add a ranged converter factory to this registry. + * The convertible source/target type pair is derived from the ConverterFactory's parameterized types. + * @throws IllegalArgumentException if the parameterized types could not be resolved + */ + void addConverterFactory(ConverterFactory factory); + + /** + * Remove any converters from {@code sourceType} to {@code targetType}. + * @param sourceType the source type + * @param targetType the target type + */ + void removeConvertible(Class sourceType, Class targetType); + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/converter/ConvertingComparator.java b/spring-core/src/main/java/org/springframework/core/convert/converter/ConvertingComparator.java new file mode 100644 index 0000000..dadf4cd --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/converter/ConvertingComparator.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.converter; + +import java.util.Comparator; +import java.util.Map; + +import org.springframework.core.convert.ConversionService; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.comparator.Comparators; + +/** + * A {@link Comparator} that converts values before they are compared. + * + *

    The specified {@link Converter} will be used to convert each value + * before it is passed to the underlying {@code Comparator}. + * + * @author Phillip Webb + * @since 3.2 + * @param the source type + * @param the target type + */ +public class ConvertingComparator implements Comparator { + + private final Comparator comparator; + + private final Converter converter; + + + /** + * Create a new {@link ConvertingComparator} instance. + * @param converter the converter + */ + public ConvertingComparator(Converter converter) { + this(Comparators.comparable(), converter); + } + + /** + * Create a new {@link ConvertingComparator} instance. + * @param comparator the underlying comparator used to compare the converted values + * @param converter the converter + */ + public ConvertingComparator(Comparator comparator, Converter converter) { + Assert.notNull(comparator, "Comparator must not be null"); + Assert.notNull(converter, "Converter must not be null"); + this.comparator = comparator; + this.converter = converter; + } + + /** + * Create a new {@code ConvertingComparator} instance. + * @param comparator the underlying comparator + * @param conversionService the conversion service + * @param targetType the target type + */ + public ConvertingComparator( + Comparator comparator, ConversionService conversionService, Class targetType) { + + this(comparator, new ConversionServiceConverter<>(conversionService, targetType)); + } + + + @Override + public int compare(S o1, S o2) { + T c1 = this.converter.convert(o1); + T c2 = this.converter.convert(o2); + return this.comparator.compare(c1, c2); + } + + /** + * Create a new {@link ConvertingComparator} that compares {@linkplain java.util.Map.Entry + * map entries} based on their {@linkplain java.util.Map.Entry#getKey() keys}. + * @param comparator the underlying comparator used to compare keys + * @return a new {@link ConvertingComparator} instance + */ + public static ConvertingComparator, K> mapEntryKeys(Comparator comparator) { + return new ConvertingComparator<>(comparator, Map.Entry::getKey); + } + + /** + * Create a new {@link ConvertingComparator} that compares {@linkplain java.util.Map.Entry + * map entries} based on their {@linkplain java.util.Map.Entry#getValue() values}. + * @param comparator the underlying comparator used to compare values + * @return a new {@link ConvertingComparator} instance + */ + public static ConvertingComparator, V> mapEntryValues(Comparator comparator) { + return new ConvertingComparator<>(comparator, Map.Entry::getValue); + } + + + /** + * Adapts a {@link ConversionService} and targetType to a {@link Converter}. + */ + private static class ConversionServiceConverter implements Converter { + + private final ConversionService conversionService; + + private final Class targetType; + + public ConversionServiceConverter(ConversionService conversionService, Class targetType) { + Assert.notNull(conversionService, "ConversionService must not be null"); + Assert.notNull(targetType, "TargetType must not be null"); + this.conversionService = conversionService; + this.targetType = targetType; + } + + @Override + @Nullable + public T convert(S source) { + return this.conversionService.convert(source, this.targetType); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/converter/GenericConverter.java b/spring-core/src/main/java/org/springframework/core/convert/converter/GenericConverter.java new file mode 100644 index 0000000..0472337 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/converter/GenericConverter.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.converter; + +import java.util.Set; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Generic converter interface for converting between two or more types. + * + *

    This is the most flexible of the Converter SPI interfaces, but also the most complex. + * It is flexible in that a GenericConverter may support converting between multiple source/target + * type pairs (see {@link #getConvertibleTypes()}. In addition, GenericConverter implementations + * have access to source/target {@link TypeDescriptor field context} during the type conversion + * process. This allows for resolving source and target field metadata such as annotations and + * generics information, which can be used to influence the conversion logic. + * + *

    This interface should generally not be used when the simpler {@link Converter} or + * {@link ConverterFactory} interface is sufficient. + * + *

    Implementations may additionally implement {@link ConditionalConverter}. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + * @see TypeDescriptor + * @see Converter + * @see ConverterFactory + * @see ConditionalConverter + */ +public interface GenericConverter { + + /** + * Return the source and target types that this converter can convert between. + *

    Each entry is a convertible source-to-target type pair. + *

    For {@link ConditionalConverter conditional converters} this method may return + * {@code null} to indicate all source-to-target pairs should be considered. + */ + @Nullable + Set getConvertibleTypes(); + + /** + * Convert the source object to the targetType described by the {@code TypeDescriptor}. + * @param source the source object to convert (may be {@code null}) + * @param sourceType the type descriptor of the field we are converting from + * @param targetType the type descriptor of the field we are converting to + * @return the converted object + */ + @Nullable + Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType); + + + /** + * Holder for a source-to-target class pair. + */ + final class ConvertiblePair { + + private final Class sourceType; + + private final Class targetType; + + /** + * Create a new source-to-target pair. + * @param sourceType the source type + * @param targetType the target type + */ + public ConvertiblePair(Class sourceType, Class targetType) { + Assert.notNull(sourceType, "Source type must not be null"); + Assert.notNull(targetType, "Target type must not be null"); + this.sourceType = sourceType; + this.targetType = targetType; + } + + public Class getSourceType() { + return this.sourceType; + } + + public Class getTargetType() { + return this.targetType; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null || other.getClass() != ConvertiblePair.class) { + return false; + } + ConvertiblePair otherPair = (ConvertiblePair) other; + return (this.sourceType == otherPair.sourceType && this.targetType == otherPair.targetType); + } + + @Override + public int hashCode() { + return (this.sourceType.hashCode() * 31 + this.targetType.hashCode()); + } + + @Override + public String toString() { + return (this.sourceType.getName() + " -> " + this.targetType.getName()); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/converter/package-info.java b/spring-core/src/main/java/org/springframework/core/convert/converter/package-info.java new file mode 100644 index 0000000..c7a057d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/converter/package-info.java @@ -0,0 +1,9 @@ +/** + * SPI to implement Converters for the type conversion system. + */ +@NonNullApi +@NonNullFields +package org.springframework.core.convert.converter; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/main/java/org/springframework/core/convert/package-info.java b/spring-core/src/main/java/org/springframework/core/convert/package-info.java new file mode 100644 index 0000000..7cef726 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/package-info.java @@ -0,0 +1,9 @@ +/** + * Type conversion system API. + */ +@NonNullApi +@NonNullFields +package org.springframework.core.convert; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/AbstractConditionalEnumConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/AbstractConditionalEnumConverter.java new file mode 100644 index 0000000..50cf843 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/AbstractConditionalEnumConverter.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalConverter; +import org.springframework.util.ClassUtils; + +/** + * A {@link ConditionalConverter} base implementation for enum-based converters. + * + * @author Stephane Nicoll + * @since 4.3 + */ +abstract class AbstractConditionalEnumConverter implements ConditionalConverter { + + private final ConversionService conversionService; + + + protected AbstractConditionalEnumConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + for (Class interfaceType : ClassUtils.getAllInterfacesForClassAsSet(sourceType.getType())) { + if (this.conversionService.canConvert(TypeDescriptor.valueOf(interfaceType), targetType)) { + return false; + } + } + return true; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToArrayConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToArrayConverter.java new file mode 100644 index 0000000..a511447 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToArrayConverter.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +/** + * Converts an array to another array. First adapts the source array to a List, + * then delegates to {@link CollectionToArrayConverter} to perform the target + * array conversion. + * + * @author Keith Donald + * @author Phillip Webb + * @since 3.0 + */ +final class ArrayToArrayConverter implements ConditionalGenericConverter { + + private final CollectionToArrayConverter helperConverter; + + private final ConversionService conversionService; + + + public ArrayToArrayConverter(ConversionService conversionService) { + this.helperConverter = new CollectionToArrayConverter(conversionService); + this.conversionService = conversionService; + } + + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Object[].class, Object[].class)); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return this.helperConverter.matches(sourceType, targetType); + } + + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (this.conversionService instanceof GenericConversionService) { + TypeDescriptor targetElement = targetType.getElementTypeDescriptor(); + if (targetElement != null && + ((GenericConversionService) this.conversionService).canBypassConvert( + sourceType.getElementTypeDescriptor(), targetElement)) { + return source; + } + } + List sourceList = Arrays.asList(ObjectUtils.toObjectArray(source)); + return this.helperConverter.convert(sourceList, sourceType, targetType); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToCollectionConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToCollectionConverter.java new file mode 100644 index 0000000..ea96d76 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToCollectionConverter.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.CollectionFactory; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.lang.Nullable; + +/** + * Converts an array to a Collection. + * + *

    First, creates a new Collection of the requested target type. + * Then adds each array element to the target collection. + * Will perform an element conversion from the source component type + * to the collection's parameterized type if necessary. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + */ +final class ArrayToCollectionConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + + public ArrayToCollectionConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Object[].class, Collection.class)); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return ConversionUtils.canConvertElements( + sourceType.getElementTypeDescriptor(), targetType.getElementTypeDescriptor(), this.conversionService); + } + + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + + int length = Array.getLength(source); + TypeDescriptor elementDesc = targetType.getElementTypeDescriptor(); + Collection target = CollectionFactory.createCollection(targetType.getType(), + (elementDesc != null ? elementDesc.getType() : null), length); + + if (elementDesc == null) { + for (int i = 0; i < length; i++) { + Object sourceElement = Array.get(source, i); + target.add(sourceElement); + } + } + else { + for (int i = 0; i < length; i++) { + Object sourceElement = Array.get(source, i); + Object targetElement = this.conversionService.convert(sourceElement, + sourceType.elementTypeDescriptor(sourceElement), elementDesc); + target.add(targetElement); + } + } + return target; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToObjectConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToObjectConverter.java new file mode 100644 index 0000000..4547d18 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToObjectConverter.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.lang.reflect.Array; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.lang.Nullable; + +/** + * Converts an array to an Object by returning the first array element + * after converting it to the desired target type. + * + * @author Keith Donald + * @since 3.0 + */ +final class ArrayToObjectConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + + public ArrayToObjectConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Object[].class, Object.class)); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return ConversionUtils.canConvertElements(sourceType.getElementTypeDescriptor(), targetType, this.conversionService); + } + + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + if (sourceType.isAssignableTo(targetType)) { + return source; + } + if (Array.getLength(source) == 0) { + return null; + } + Object firstElement = Array.get(source, 0); + return this.conversionService.convert(firstElement, sourceType.elementTypeDescriptor(firstElement), targetType); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToStringConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToStringConverter.java new file mode 100644 index 0000000..fad85e5 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToStringConverter.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +/** + * Converts an array to a comma-delimited String. First adapts the source array + * to a List, then delegates to {@link CollectionToStringConverter} to perform + * the target String conversion. + * + * @author Keith Donald + * @since 3.0 + */ +final class ArrayToStringConverter implements ConditionalGenericConverter { + + private final CollectionToStringConverter helperConverter; + + + public ArrayToStringConverter(ConversionService conversionService) { + this.helperConverter = new CollectionToStringConverter(conversionService); + } + + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Object[].class, String.class)); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return this.helperConverter.matches(sourceType, targetType); + } + + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + return this.helperConverter.convert(Arrays.asList(ObjectUtils.toObjectArray(source)), sourceType, targetType); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ByteBufferConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ByteBufferConverter.java new file mode 100644 index 0000000..55a1c07 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ByteBufferConverter.java @@ -0,0 +1,131 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.lang.Nullable; + +/** + * Converts a {@link ByteBuffer} directly to and from {@code byte[] ByteBuffer} directly to and from {@code byte[]s} and indirectly + * to any type that the {@link ConversionService} support via {@code byte[]}. + * + * @author Phillip Webb + * @author Juergen Hoeller + * @since 4.0 + */ +final class ByteBufferConverter implements ConditionalGenericConverter { + + private static final TypeDescriptor BYTE_BUFFER_TYPE = TypeDescriptor.valueOf(ByteBuffer.class); + + private static final TypeDescriptor BYTE_ARRAY_TYPE = TypeDescriptor.valueOf(byte[].class); + + private static final Set CONVERTIBLE_PAIRS; + + static { + Set convertiblePairs = new HashSet<>(4); + convertiblePairs.add(new ConvertiblePair(ByteBuffer.class, byte[].class)); + convertiblePairs.add(new ConvertiblePair(byte[].class, ByteBuffer.class)); + convertiblePairs.add(new ConvertiblePair(ByteBuffer.class, Object.class)); + convertiblePairs.add(new ConvertiblePair(Object.class, ByteBuffer.class)); + CONVERTIBLE_PAIRS = Collections.unmodifiableSet(convertiblePairs); + } + + + private final ConversionService conversionService; + + + public ByteBufferConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + + @Override + public Set getConvertibleTypes() { + return CONVERTIBLE_PAIRS; + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + boolean byteBufferTarget = targetType.isAssignableTo(BYTE_BUFFER_TYPE); + if (sourceType.isAssignableTo(BYTE_BUFFER_TYPE)) { + return (byteBufferTarget || matchesFromByteBuffer(targetType)); + } + return (byteBufferTarget && matchesToByteBuffer(sourceType)); + } + + private boolean matchesFromByteBuffer(TypeDescriptor targetType) { + return (targetType.isAssignableTo(BYTE_ARRAY_TYPE) || + this.conversionService.canConvert(BYTE_ARRAY_TYPE, targetType)); + } + + private boolean matchesToByteBuffer(TypeDescriptor sourceType) { + return (sourceType.isAssignableTo(BYTE_ARRAY_TYPE) || + this.conversionService.canConvert(sourceType, BYTE_ARRAY_TYPE)); + } + + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + boolean byteBufferTarget = targetType.isAssignableTo(BYTE_BUFFER_TYPE); + if (source instanceof ByteBuffer) { + ByteBuffer buffer = (ByteBuffer) source; + return (byteBufferTarget ? buffer.duplicate() : convertFromByteBuffer(buffer, targetType)); + } + if (byteBufferTarget) { + return convertToByteBuffer(source, sourceType); + } + // Should not happen + throw new IllegalStateException("Unexpected source/target types"); + } + + @Nullable + private Object convertFromByteBuffer(ByteBuffer source, TypeDescriptor targetType) { + byte[] bytes = new byte[source.remaining()]; + source.get(bytes); + + if (targetType.isAssignableTo(BYTE_ARRAY_TYPE)) { + return bytes; + } + return this.conversionService.convert(bytes, BYTE_ARRAY_TYPE, targetType); + } + + private Object convertToByteBuffer(@Nullable Object source, TypeDescriptor sourceType) { + byte[] bytes = (byte[]) (source instanceof byte[] ? source : + this.conversionService.convert(source, sourceType, BYTE_ARRAY_TYPE)); + + if (bytes == null) { + return ByteBuffer.wrap(new byte[0]); + } + + ByteBuffer byteBuffer = ByteBuffer.allocate(bytes.length); + byteBuffer.put(bytes); + + // Extra cast necessary for compiling on JDK 9 plus running on JDK 8, since + // otherwise the overridden ByteBuffer-returning rewind method would be chosen + // which isn't available on JDK 8. + return ((Buffer) byteBuffer).rewind(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/CharacterToNumberFactory.java b/spring-core/src/main/java/org/springframework/core/convert/support/CharacterToNumberFactory.java new file mode 100644 index 0000000..2a420aa --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/CharacterToNumberFactory.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.util.NumberUtils; + +/** + * Converts from a Character to any JDK-standard Number implementation. + * + *

    Support Number classes including Byte, Short, Integer, Float, Double, Long, BigInteger, BigDecimal. This class + * delegates to {@link NumberUtils#convertNumberToTargetClass(Number, Class)} to perform the conversion. + * + * @author Keith Donald + * @since 3.0 + * @see java.lang.Byte + * @see java.lang.Short + * @see java.lang.Integer + * @see java.lang.Long + * @see java.math.BigInteger + * @see java.lang.Float + * @see java.lang.Double + * @see java.math.BigDecimal + * @see NumberUtils + */ +final class CharacterToNumberFactory implements ConverterFactory { + + @Override + public Converter getConverter(Class targetType) { + return new CharacterToNumber<>(targetType); + } + + private static final class CharacterToNumber implements Converter { + + private final Class targetType; + + public CharacterToNumber(Class targetType) { + this.targetType = targetType; + } + + @Override + public T convert(Character source) { + return NumberUtils.convertNumberToTargetClass((short) source.charValue(), this.targetType); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToArrayConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToArrayConverter.java new file mode 100644 index 0000000..026587a --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToArrayConverter.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Converts a Collection to an array. + * + *

    First, creates a new array of the requested targetType with a length equal to the + * size of the source Collection. Then sets each collection element into the array. + * Will perform an element conversion from the collection's parameterized type to the + * array's component type if necessary. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + */ +final class CollectionToArrayConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + + public CollectionToArrayConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Collection.class, Object[].class)); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return ConversionUtils.canConvertElements(sourceType.getElementTypeDescriptor(), + targetType.getElementTypeDescriptor(), this.conversionService); + } + + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + Collection sourceCollection = (Collection) source; + TypeDescriptor targetElementType = targetType.getElementTypeDescriptor(); + Assert.state(targetElementType != null, "No target element type"); + Object array = Array.newInstance(targetElementType.getType(), sourceCollection.size()); + int i = 0; + for (Object sourceElement : sourceCollection) { + Object targetElement = this.conversionService.convert(sourceElement, + sourceType.elementTypeDescriptor(sourceElement), targetElementType); + Array.set(array, i++, targetElement); + } + return array; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToCollectionConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToCollectionConverter.java new file mode 100644 index 0000000..f20c950 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToCollectionConverter.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.CollectionFactory; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.lang.Nullable; + +/** + * Converts from a Collection to another Collection. + * + *

    First, creates a new Collection of the requested targetType with a size equal to the + * size of the source Collection. Then copies each element in the source collection to the + * target collection. Will perform an element conversion from the source collection's + * parameterized type to the target collection's parameterized type if necessary. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + */ +final class CollectionToCollectionConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + + public CollectionToCollectionConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Collection.class, Collection.class)); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return ConversionUtils.canConvertElements( + sourceType.getElementTypeDescriptor(), targetType.getElementTypeDescriptor(), this.conversionService); + } + + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + Collection sourceCollection = (Collection) source; + + // Shortcut if possible... + boolean copyRequired = !targetType.getType().isInstance(source); + if (!copyRequired && sourceCollection.isEmpty()) { + return source; + } + TypeDescriptor elementDesc = targetType.getElementTypeDescriptor(); + if (elementDesc == null && !copyRequired) { + return source; + } + + // At this point, we need a collection copy in any case, even if just for finding out about element copies... + Collection target = CollectionFactory.createCollection(targetType.getType(), + (elementDesc != null ? elementDesc.getType() : null), sourceCollection.size()); + + if (elementDesc == null) { + target.addAll(sourceCollection); + } + else { + for (Object sourceElement : sourceCollection) { + Object targetElement = this.conversionService.convert(sourceElement, + sourceType.elementTypeDescriptor(sourceElement), elementDesc); + target.add(targetElement); + if (sourceElement != targetElement) { + copyRequired = true; + } + } + } + + return (copyRequired ? target : source); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToObjectConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToObjectConverter.java new file mode 100644 index 0000000..81b4a41 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToObjectConverter.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.lang.Nullable; + +/** + * Converts a Collection to an Object by returning the first collection element after converting it to the desired targetType. + * + * @author Keith Donald + * @since 3.0 + */ +final class CollectionToObjectConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + public CollectionToObjectConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Collection.class, Object.class)); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return ConversionUtils.canConvertElements(sourceType.getElementTypeDescriptor(), targetType, this.conversionService); + } + + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + if (sourceType.isAssignableTo(targetType)) { + return source; + } + Collection sourceCollection = (Collection) source; + if (sourceCollection.isEmpty()) { + return null; + } + Object firstElement = sourceCollection.iterator().next(); + return this.conversionService.convert(firstElement, sourceType.elementTypeDescriptor(firstElement), targetType); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToStringConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToStringConverter.java new file mode 100644 index 0000000..2bef4df --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToStringConverter.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; +import java.util.StringJoiner; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.lang.Nullable; + +/** + * Converts a Collection to a comma-delimited String. + * + * @author Keith Donald + * @since 3.0 + */ +final class CollectionToStringConverter implements ConditionalGenericConverter { + + private static final String DELIMITER = ","; + + private final ConversionService conversionService; + + + public CollectionToStringConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Collection.class, String.class)); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return ConversionUtils.canConvertElements( + sourceType.getElementTypeDescriptor(), targetType, this.conversionService); + } + + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + Collection sourceCollection = (Collection) source; + if (sourceCollection.isEmpty()) { + return ""; + } + StringJoiner sj = new StringJoiner(DELIMITER); + for (Object sourceElement : sourceCollection) { + Object targetElement = this.conversionService.convert( + sourceElement, sourceType.elementTypeDescriptor(sourceElement), targetType); + sj.add(String.valueOf(targetElement)); + } + return sj.toString(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ConfigurableConversionService.java b/spring-core/src/main/java/org/springframework/core/convert/support/ConfigurableConversionService.java new file mode 100644 index 0000000..f384762 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ConfigurableConversionService.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2011 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.converter.ConverterRegistry; + +/** + * Configuration interface to be implemented by most if not all {@link ConversionService} + * types. Consolidates the read-only operations exposed by {@link ConversionService} and + * the mutating operations of {@link ConverterRegistry} to allow for convenient ad-hoc + * addition and removal of {@link org.springframework.core.convert.converter.Converter + * Converters} through. The latter is particularly useful when working against a + * {@link org.springframework.core.env.ConfigurableEnvironment ConfigurableEnvironment} + * instance in application context bootstrapping code. + * + * @author Chris Beams + * @since 3.1 + * @see org.springframework.core.env.ConfigurablePropertyResolver#getConversionService() + * @see org.springframework.core.env.ConfigurableEnvironment + * @see org.springframework.context.ConfigurableApplicationContext#getEnvironment() + */ +public interface ConfigurableConversionService extends ConversionService, ConverterRegistry { + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ConversionServiceFactory.java b/spring-core/src/main/java/org/springframework/core/convert/support/ConversionServiceFactory.java new file mode 100644 index 0000000..3f8ba59 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ConversionServiceFactory.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.util.Set; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.core.convert.converter.ConverterRegistry; +import org.springframework.core.convert.converter.GenericConverter; +import org.springframework.lang.Nullable; + +/** + * A factory for common {@link org.springframework.core.convert.ConversionService} + * configurations. + * + * @author Keith Donald + * @author Juergen Hoeller + * @author Chris Beams + * @since 3.0 + */ +public final class ConversionServiceFactory { + + private ConversionServiceFactory() { + } + + + /** + * Register the given Converter objects with the given target ConverterRegistry. + * @param converters the converter objects: implementing {@link Converter}, + * {@link ConverterFactory}, or {@link GenericConverter} + * @param registry the target registry + */ + public static void registerConverters(@Nullable Set converters, ConverterRegistry registry) { + if (converters != null) { + for (Object converter : converters) { + if (converter instanceof GenericConverter) { + registry.addConverter((GenericConverter) converter); + } + else if (converter instanceof Converter) { + registry.addConverter((Converter) converter); + } + else if (converter instanceof ConverterFactory) { + registry.addConverterFactory((ConverterFactory) converter); + } + else { + throw new IllegalArgumentException("Each converter object must implement one of the " + + "Converter, ConverterFactory, or GenericConverter interfaces"); + } + } + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ConversionUtils.java b/spring-core/src/main/java/org/springframework/core/convert/support/ConversionUtils.java new file mode 100644 index 0000000..f2f400d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ConversionUtils.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.GenericConverter; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Internal utilities for the conversion package. + * + * @author Keith Donald + * @author Stephane Nicoll + * @since 3.0 + */ +abstract class ConversionUtils { + + @Nullable + public static Object invokeConverter(GenericConverter converter, @Nullable Object source, + TypeDescriptor sourceType, TypeDescriptor targetType) { + + try { + return converter.convert(source, sourceType, targetType); + } + catch (ConversionFailedException ex) { + throw ex; + } + catch (Throwable ex) { + throw new ConversionFailedException(sourceType, targetType, source, ex); + } + } + + public static boolean canConvertElements(@Nullable TypeDescriptor sourceElementType, + @Nullable TypeDescriptor targetElementType, ConversionService conversionService) { + + if (targetElementType == null) { + // yes + return true; + } + if (sourceElementType == null) { + // maybe + return true; + } + if (conversionService.canConvert(sourceElementType, targetElementType)) { + // yes + return true; + } + if (ClassUtils.isAssignable(sourceElementType.getType(), targetElementType.getType())) { + // maybe + return true; + } + // no + return false; + } + + public static Class getEnumType(Class targetType) { + Class enumType = targetType; + while (enumType != null && !enumType.isEnum()) { + enumType = enumType.getSuperclass(); + } + Assert.notNull(enumType, () -> "The target type " + targetType.getName() + " does not refer to an enum"); + return enumType; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ConvertingPropertyEditorAdapter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ConvertingPropertyEditorAdapter.java new file mode 100644 index 0000000..dcf4c8b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ConvertingPropertyEditorAdapter.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.beans.PropertyEditorSupport; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Adapter that exposes a {@link java.beans.PropertyEditor} for any given + * {@link org.springframework.core.convert.ConversionService} and specific target type. + * + * @author Juergen Hoeller + * @since 3.0 + */ +public class ConvertingPropertyEditorAdapter extends PropertyEditorSupport { + + private final ConversionService conversionService; + + private final TypeDescriptor targetDescriptor; + + private final boolean canConvertToString; + + + /** + * Create a new ConvertingPropertyEditorAdapter for a given + * {@link org.springframework.core.convert.ConversionService} + * and the given target type. + * @param conversionService the ConversionService to delegate to + * @param targetDescriptor the target type to convert to + */ + public ConvertingPropertyEditorAdapter(ConversionService conversionService, TypeDescriptor targetDescriptor) { + Assert.notNull(conversionService, "ConversionService must not be null"); + Assert.notNull(targetDescriptor, "TypeDescriptor must not be null"); + this.conversionService = conversionService; + this.targetDescriptor = targetDescriptor; + this.canConvertToString = conversionService.canConvert(this.targetDescriptor, TypeDescriptor.valueOf(String.class)); + } + + + @Override + public void setAsText(@Nullable String text) throws IllegalArgumentException { + setValue(this.conversionService.convert(text, TypeDescriptor.valueOf(String.class), this.targetDescriptor)); + } + + @Override + @Nullable + public String getAsText() { + if (this.canConvertToString) { + return (String) this.conversionService.convert(getValue(), this.targetDescriptor, TypeDescriptor.valueOf(String.class)); + } + else { + return null; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java b/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java new file mode 100644 index 0000000..dfd95ca --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java @@ -0,0 +1,171 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.nio.charset.Charset; +import java.util.Currency; +import java.util.Locale; +import java.util.UUID; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.converter.ConverterRegistry; +import org.springframework.lang.Nullable; + +/** + * A specialization of {@link GenericConversionService} configured by default + * with converters appropriate for most environments. + * + *

    Designed for direct instantiation but also exposes the static + * {@link #addDefaultConverters(ConverterRegistry)} utility method for ad-hoc + * use against any {@code ConverterRegistry} instance. + * + * @author Chris Beams + * @author Juergen Hoeller + * @author Stephane Nicoll + * @since 3.1 + */ +public class DefaultConversionService extends GenericConversionService { + + @Nullable + private static volatile DefaultConversionService sharedInstance; + + + /** + * Create a new {@code DefaultConversionService} with the set of + * {@linkplain DefaultConversionService#addDefaultConverters(ConverterRegistry) default converters}. + */ + public DefaultConversionService() { + addDefaultConverters(this); + } + + + /** + * Return a shared default {@code ConversionService} instance, + * lazily building it once needed. + *

    NOTE: We highly recommend constructing individual + * {@code ConversionService} instances for customization purposes. + * This accessor is only meant as a fallback for code paths which + * need simple type coercion but cannot access a longer-lived + * {@code ConversionService} instance any other way. + * @return the shared {@code ConversionService} instance (never {@code null}) + * @since 4.3.5 + */ + public static ConversionService getSharedInstance() { + DefaultConversionService cs = sharedInstance; + if (cs == null) { + synchronized (DefaultConversionService.class) { + cs = sharedInstance; + if (cs == null) { + cs = new DefaultConversionService(); + sharedInstance = cs; + } + } + } + return cs; + } + + /** + * Add converters appropriate for most environments. + * @param converterRegistry the registry of converters to add to + * (must also be castable to ConversionService, e.g. being a {@link ConfigurableConversionService}) + * @throws ClassCastException if the given ConverterRegistry could not be cast to a ConversionService + */ + public static void addDefaultConverters(ConverterRegistry converterRegistry) { + addScalarConverters(converterRegistry); + addCollectionConverters(converterRegistry); + + converterRegistry.addConverter(new ByteBufferConverter((ConversionService) converterRegistry)); + converterRegistry.addConverter(new StringToTimeZoneConverter()); + converterRegistry.addConverter(new ZoneIdToTimeZoneConverter()); + converterRegistry.addConverter(new ZonedDateTimeToCalendarConverter()); + + converterRegistry.addConverter(new ObjectToObjectConverter()); + converterRegistry.addConverter(new IdToEntityConverter((ConversionService) converterRegistry)); + converterRegistry.addConverter(new FallbackObjectToStringConverter()); + converterRegistry.addConverter(new ObjectToOptionalConverter((ConversionService) converterRegistry)); + } + + /** + * Add common collection converters. + * @param converterRegistry the registry of converters to add to + * (must also be castable to ConversionService, e.g. being a {@link ConfigurableConversionService}) + * @throws ClassCastException if the given ConverterRegistry could not be cast to a ConversionService + * @since 4.2.3 + */ + public static void addCollectionConverters(ConverterRegistry converterRegistry) { + ConversionService conversionService = (ConversionService) converterRegistry; + + converterRegistry.addConverter(new ArrayToCollectionConverter(conversionService)); + converterRegistry.addConverter(new CollectionToArrayConverter(conversionService)); + + converterRegistry.addConverter(new ArrayToArrayConverter(conversionService)); + converterRegistry.addConverter(new CollectionToCollectionConverter(conversionService)); + converterRegistry.addConverter(new MapToMapConverter(conversionService)); + + converterRegistry.addConverter(new ArrayToStringConverter(conversionService)); + converterRegistry.addConverter(new StringToArrayConverter(conversionService)); + + converterRegistry.addConverter(new ArrayToObjectConverter(conversionService)); + converterRegistry.addConverter(new ObjectToArrayConverter(conversionService)); + + converterRegistry.addConverter(new CollectionToStringConverter(conversionService)); + converterRegistry.addConverter(new StringToCollectionConverter(conversionService)); + + converterRegistry.addConverter(new CollectionToObjectConverter(conversionService)); + converterRegistry.addConverter(new ObjectToCollectionConverter(conversionService)); + + converterRegistry.addConverter(new StreamConverter(conversionService)); + } + + private static void addScalarConverters(ConverterRegistry converterRegistry) { + converterRegistry.addConverterFactory(new NumberToNumberConverterFactory()); + + converterRegistry.addConverterFactory(new StringToNumberConverterFactory()); + converterRegistry.addConverter(Number.class, String.class, new ObjectToStringConverter()); + + converterRegistry.addConverter(new StringToCharacterConverter()); + converterRegistry.addConverter(Character.class, String.class, new ObjectToStringConverter()); + + converterRegistry.addConverter(new NumberToCharacterConverter()); + converterRegistry.addConverterFactory(new CharacterToNumberFactory()); + + converterRegistry.addConverter(new StringToBooleanConverter()); + converterRegistry.addConverter(Boolean.class, String.class, new ObjectToStringConverter()); + + converterRegistry.addConverterFactory(new StringToEnumConverterFactory()); + converterRegistry.addConverter(new EnumToStringConverter((ConversionService) converterRegistry)); + + converterRegistry.addConverterFactory(new IntegerToEnumConverterFactory()); + converterRegistry.addConverter(new EnumToIntegerConverter((ConversionService) converterRegistry)); + + converterRegistry.addConverter(new StringToLocaleConverter()); + converterRegistry.addConverter(Locale.class, String.class, new ObjectToStringConverter()); + + converterRegistry.addConverter(new StringToCharsetConverter()); + converterRegistry.addConverter(Charset.class, String.class, new ObjectToStringConverter()); + + converterRegistry.addConverter(new StringToCurrencyConverter()); + converterRegistry.addConverter(Currency.class, String.class, new ObjectToStringConverter()); + + converterRegistry.addConverter(new StringToPropertiesConverter()); + converterRegistry.addConverter(new PropertiesToStringConverter()); + + converterRegistry.addConverter(new StringToUUIDConverter()); + converterRegistry.addConverter(UUID.class, String.class, new ObjectToStringConverter()); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/EnumToIntegerConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/EnumToIntegerConverter.java new file mode 100644 index 0000000..0239f74 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/EnumToIntegerConverter.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.converter.Converter; + +/** + * Calls {@link Enum#ordinal()} to convert a source Enum to a Integer. + * This converter will not match enums with interfaces that can be converted. + * + * @author Yanming Zhou + * @since 4.3 + */ +final class EnumToIntegerConverter extends AbstractConditionalEnumConverter implements Converter, Integer> { + + public EnumToIntegerConverter(ConversionService conversionService) { + super(conversionService); + } + + @Override + public Integer convert(Enum source) { + return source.ordinal(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/EnumToStringConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/EnumToStringConverter.java new file mode 100644 index 0000000..76c14c9 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/EnumToStringConverter.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.converter.Converter; + +/** + * Calls {@link Enum#name()} to convert a source Enum to a String. + * This converter will not match enums with interfaces that can be converted. + * + * @author Keith Donald + * @author Phillip Webb + * @since 3.0 + */ +final class EnumToStringConverter extends AbstractConditionalEnumConverter implements Converter, String> { + + public EnumToStringConverter(ConversionService conversionService) { + super(conversionService); + } + + @Override + public String convert(Enum source) { + return source.name(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/FallbackObjectToStringConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/FallbackObjectToStringConverter.java new file mode 100644 index 0000000..63313d3 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/FallbackObjectToStringConverter.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.io.StringWriter; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.lang.Nullable; + +/** + * Simply calls {@link Object#toString()} to convert any supported object + * to a {@link String}. + * + *

    Supports {@link CharSequence}, {@link StringWriter}, and any class + * with a String constructor or one of the following static factory methods: + * {@code valueOf(String)}, {@code of(String)}, {@code from(String)}. + * + *

    Used by the {@link DefaultConversionService} as a fallback if there + * are no other explicit to-String converters registered. + * + * @author Keith Donald + * @author Juergen Hoeller + * @author Sam Brannen + * @since 3.0 + * @see ObjectToObjectConverter + */ +final class FallbackObjectToStringConverter implements ConditionalGenericConverter { + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Object.class, String.class)); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + Class sourceClass = sourceType.getObjectType(); + if (String.class == sourceClass) { + // no conversion required + return false; + } + return (CharSequence.class.isAssignableFrom(sourceClass) || + StringWriter.class.isAssignableFrom(sourceClass) || + ObjectToObjectConverter.hasConversionMethodOrConstructor(sourceClass, String.class)); + } + + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + return (source != null ? source.toString() : null); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java b/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java new file mode 100644 index 0000000..1134483 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java @@ -0,0 +1,707 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.CopyOnWriteArraySet; + +import org.springframework.core.DecoratingProxy; +import org.springframework.core.ResolvableType; +import org.springframework.core.convert.ConversionException; +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.ConverterNotFoundException; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalConverter; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.core.convert.converter.ConverterRegistry; +import org.springframework.core.convert.converter.GenericConverter; +import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.StringUtils; + +/** + * Base {@link ConversionService} implementation suitable for use in most environments. + * Indirectly implements {@link ConverterRegistry} as registration API through the + * {@link ConfigurableConversionService} interface. + * + * @author Keith Donald + * @author Juergen Hoeller + * @author Chris Beams + * @author Phillip Webb + * @author David Haraburda + * @since 3.0 + */ +public class GenericConversionService implements ConfigurableConversionService { + + /** + * General NO-OP converter used when conversion is not required. + */ + private static final GenericConverter NO_OP_CONVERTER = new NoOpConverter("NO_OP"); + + /** + * Used as a cache entry when no converter is available. + * This converter is never returned. + */ + private static final GenericConverter NO_MATCH = new NoOpConverter("NO_MATCH"); + + + private final Converters converters = new Converters(); + + private final Map converterCache = new ConcurrentReferenceHashMap<>(64); + + + // ConverterRegistry implementation + + @Override + public void addConverter(Converter converter) { + ResolvableType[] typeInfo = getRequiredTypeInfo(converter.getClass(), Converter.class); + if (typeInfo == null && converter instanceof DecoratingProxy) { + typeInfo = getRequiredTypeInfo(((DecoratingProxy) converter).getDecoratedClass(), Converter.class); + } + if (typeInfo == null) { + throw new IllegalArgumentException("Unable to determine source type and target type for your " + + "Converter [" + converter.getClass().getName() + "]; does the class parameterize those types?"); + } + addConverter(new ConverterAdapter(converter, typeInfo[0], typeInfo[1])); + } + + @Override + public void addConverter(Class sourceType, Class targetType, Converter converter) { + addConverter(new ConverterAdapter( + converter, ResolvableType.forClass(sourceType), ResolvableType.forClass(targetType))); + } + + @Override + public void addConverter(GenericConverter converter) { + this.converters.add(converter); + invalidateCache(); + } + + @Override + public void addConverterFactory(ConverterFactory factory) { + ResolvableType[] typeInfo = getRequiredTypeInfo(factory.getClass(), ConverterFactory.class); + if (typeInfo == null && factory instanceof DecoratingProxy) { + typeInfo = getRequiredTypeInfo(((DecoratingProxy) factory).getDecoratedClass(), ConverterFactory.class); + } + if (typeInfo == null) { + throw new IllegalArgumentException("Unable to determine source type and target type for your " + + "ConverterFactory [" + factory.getClass().getName() + "]; does the class parameterize those types?"); + } + addConverter(new ConverterFactoryAdapter(factory, + new ConvertiblePair(typeInfo[0].toClass(), typeInfo[1].toClass()))); + } + + @Override + public void removeConvertible(Class sourceType, Class targetType) { + this.converters.remove(sourceType, targetType); + invalidateCache(); + } + + + // ConversionService implementation + + @Override + public boolean canConvert(@Nullable Class sourceType, Class targetType) { + Assert.notNull(targetType, "Target type to convert to cannot be null"); + return canConvert((sourceType != null ? TypeDescriptor.valueOf(sourceType) : null), + TypeDescriptor.valueOf(targetType)); + } + + @Override + public boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType) { + Assert.notNull(targetType, "Target type to convert to cannot be null"); + if (sourceType == null) { + return true; + } + GenericConverter converter = getConverter(sourceType, targetType); + return (converter != null); + } + + /** + * Return whether conversion between the source type and the target type can be bypassed. + *

    More precisely, this method will return true if objects of sourceType can be + * converted to the target type by returning the source object unchanged. + * @param sourceType context about the source type to convert from + * (may be {@code null} if source is {@code null}) + * @param targetType context about the target type to convert to (required) + * @return {@code true} if conversion can be bypassed; {@code false} otherwise + * @throws IllegalArgumentException if targetType is {@code null} + * @since 3.2 + */ + public boolean canBypassConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType) { + Assert.notNull(targetType, "Target type to convert to cannot be null"); + if (sourceType == null) { + return true; + } + GenericConverter converter = getConverter(sourceType, targetType); + return (converter == NO_OP_CONVERTER); + } + + @Override + @SuppressWarnings("unchecked") + @Nullable + public T convert(@Nullable Object source, Class targetType) { + Assert.notNull(targetType, "Target type to convert to cannot be null"); + return (T) convert(source, TypeDescriptor.forObject(source), TypeDescriptor.valueOf(targetType)); + } + + @Override + @Nullable + public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) { + Assert.notNull(targetType, "Target type to convert to cannot be null"); + if (sourceType == null) { + Assert.isTrue(source == null, "Source must be [null] if source type == [null]"); + return handleResult(null, targetType, convertNullSource(null, targetType)); + } + if (source != null && !sourceType.getObjectType().isInstance(source)) { + throw new IllegalArgumentException("Source to convert from must be an instance of [" + + sourceType + "]; instead it was a [" + source.getClass().getName() + "]"); + } + GenericConverter converter = getConverter(sourceType, targetType); + if (converter != null) { + Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType); + return handleResult(sourceType, targetType, result); + } + return handleConverterNotFound(source, sourceType, targetType); + } + + /** + * Convenience operation for converting a source object to the specified targetType, + * where the target type is a descriptor that provides additional conversion context. + * Simply delegates to {@link #convert(Object, TypeDescriptor, TypeDescriptor)} and + * encapsulates the construction of the source type descriptor using + * {@link TypeDescriptor#forObject(Object)}. + * @param source the source object + * @param targetType the target type + * @return the converted value + * @throws ConversionException if a conversion exception occurred + * @throws IllegalArgumentException if targetType is {@code null}, + * or sourceType is {@code null} but source is not {@code null} + */ + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor targetType) { + return convert(source, TypeDescriptor.forObject(source), targetType); + } + + @Override + public String toString() { + return this.converters.toString(); + } + + + // Protected template methods + + /** + * Template method to convert a {@code null} source. + *

    The default implementation returns {@code null} or the Java 8 + * {@link java.util.Optional#empty()} instance if the target type is + * {@code java.util.Optional}. Subclasses may override this to return + * custom {@code null} objects for specific target types. + * @param sourceType the source type to convert from + * @param targetType the target type to convert to + * @return the converted null object + */ + @Nullable + protected Object convertNullSource(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType) { + if (targetType.getObjectType() == Optional.class) { + return Optional.empty(); + } + return null; + } + + /** + * Hook method to lookup the converter for a given sourceType/targetType pair. + * First queries this ConversionService's converter cache. + * On a cache miss, then performs an exhaustive search for a matching converter. + * If no converter matches, returns the default converter. + * @param sourceType the source type to convert from + * @param targetType the target type to convert to + * @return the generic converter that will perform the conversion, + * or {@code null} if no suitable converter was found + * @see #getDefaultConverter(TypeDescriptor, TypeDescriptor) + */ + @Nullable + protected GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) { + ConverterCacheKey key = new ConverterCacheKey(sourceType, targetType); + GenericConverter converter = this.converterCache.get(key); + if (converter != null) { + return (converter != NO_MATCH ? converter : null); + } + + converter = this.converters.find(sourceType, targetType); + if (converter == null) { + converter = getDefaultConverter(sourceType, targetType); + } + + if (converter != null) { + this.converterCache.put(key, converter); + return converter; + } + + this.converterCache.put(key, NO_MATCH); + return null; + } + + /** + * Return the default converter if no converter is found for the given sourceType/targetType pair. + *

    Returns a NO_OP Converter if the source type is assignable to the target type. + * Returns {@code null} otherwise, indicating no suitable converter could be found. + * @param sourceType the source type to convert from + * @param targetType the target type to convert to + * @return the default generic converter that will perform the conversion + */ + @Nullable + protected GenericConverter getDefaultConverter(TypeDescriptor sourceType, TypeDescriptor targetType) { + return (sourceType.isAssignableTo(targetType) ? NO_OP_CONVERTER : null); + } + + + // Internal helpers + + @Nullable + private ResolvableType[] getRequiredTypeInfo(Class converterClass, Class genericIfc) { + ResolvableType resolvableType = ResolvableType.forClass(converterClass).as(genericIfc); + ResolvableType[] generics = resolvableType.getGenerics(); + if (generics.length < 2) { + return null; + } + Class sourceType = generics[0].resolve(); + Class targetType = generics[1].resolve(); + if (sourceType == null || targetType == null) { + return null; + } + return generics; + } + + private void invalidateCache() { + this.converterCache.clear(); + } + + @Nullable + private Object handleConverterNotFound( + @Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) { + + if (source == null) { + assertNotPrimitiveTargetType(sourceType, targetType); + return null; + } + if ((sourceType == null || sourceType.isAssignableTo(targetType)) && + targetType.getObjectType().isInstance(source)) { + return source; + } + throw new ConverterNotFoundException(sourceType, targetType); + } + + @Nullable + private Object handleResult(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType, @Nullable Object result) { + if (result == null) { + assertNotPrimitiveTargetType(sourceType, targetType); + } + return result; + } + + private void assertNotPrimitiveTargetType(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType) { + if (targetType.isPrimitive()) { + throw new ConversionFailedException(sourceType, targetType, null, + new IllegalArgumentException("A null value cannot be assigned to a primitive type")); + } + } + + + /** + * Adapts a {@link Converter} to a {@link GenericConverter}. + */ + @SuppressWarnings("unchecked") + private final class ConverterAdapter implements ConditionalGenericConverter { + + private final Converter converter; + + private final ConvertiblePair typeInfo; + + private final ResolvableType targetType; + + public ConverterAdapter(Converter converter, ResolvableType sourceType, ResolvableType targetType) { + this.converter = (Converter) converter; + this.typeInfo = new ConvertiblePair(sourceType.toClass(), targetType.toClass()); + this.targetType = targetType; + } + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(this.typeInfo); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + // Check raw type first... + if (this.typeInfo.getTargetType() != targetType.getObjectType()) { + return false; + } + // Full check for complex generic type match required? + ResolvableType rt = targetType.getResolvableType(); + if (!(rt.getType() instanceof Class) && !rt.isAssignableFrom(this.targetType) && + !this.targetType.hasUnresolvableGenerics()) { + return false; + } + return !(this.converter instanceof ConditionalConverter) || + ((ConditionalConverter) this.converter).matches(sourceType, targetType); + } + + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return convertNullSource(sourceType, targetType); + } + return this.converter.convert(source); + } + + @Override + public String toString() { + return (this.typeInfo + " : " + this.converter); + } + } + + + /** + * Adapts a {@link ConverterFactory} to a {@link GenericConverter}. + */ + @SuppressWarnings("unchecked") + private final class ConverterFactoryAdapter implements ConditionalGenericConverter { + + private final ConverterFactory converterFactory; + + private final ConvertiblePair typeInfo; + + public ConverterFactoryAdapter(ConverterFactory converterFactory, ConvertiblePair typeInfo) { + this.converterFactory = (ConverterFactory) converterFactory; + this.typeInfo = typeInfo; + } + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(this.typeInfo); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + boolean matches = true; + if (this.converterFactory instanceof ConditionalConverter) { + matches = ((ConditionalConverter) this.converterFactory).matches(sourceType, targetType); + } + if (matches) { + Converter converter = this.converterFactory.getConverter(targetType.getType()); + if (converter instanceof ConditionalConverter) { + matches = ((ConditionalConverter) converter).matches(sourceType, targetType); + } + } + return matches; + } + + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return convertNullSource(sourceType, targetType); + } + return this.converterFactory.getConverter(targetType.getObjectType()).convert(source); + } + + @Override + public String toString() { + return (this.typeInfo + " : " + this.converterFactory); + } + } + + + /** + * Key for use with the converter cache. + */ + private static final class ConverterCacheKey implements Comparable { + + private final TypeDescriptor sourceType; + + private final TypeDescriptor targetType; + + public ConverterCacheKey(TypeDescriptor sourceType, TypeDescriptor targetType) { + this.sourceType = sourceType; + this.targetType = targetType; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof ConverterCacheKey)) { + return false; + } + ConverterCacheKey otherKey = (ConverterCacheKey) other; + return (this.sourceType.equals(otherKey.sourceType)) && + this.targetType.equals(otherKey.targetType); + } + + @Override + public int hashCode() { + return (this.sourceType.hashCode() * 29 + this.targetType.hashCode()); + } + + @Override + public String toString() { + return ("ConverterCacheKey [sourceType = " + this.sourceType + + ", targetType = " + this.targetType + "]"); + } + + @Override + public int compareTo(ConverterCacheKey other) { + int result = this.sourceType.getResolvableType().toString().compareTo( + other.sourceType.getResolvableType().toString()); + if (result == 0) { + result = this.targetType.getResolvableType().toString().compareTo( + other.targetType.getResolvableType().toString()); + } + return result; + } + } + + + /** + * Manages all converters registered with the service. + */ + private static class Converters { + + private final Set globalConverters = new CopyOnWriteArraySet<>(); + + private final Map converters = new ConcurrentHashMap<>(256); + + public void add(GenericConverter converter) { + Set convertibleTypes = converter.getConvertibleTypes(); + if (convertibleTypes == null) { + Assert.state(converter instanceof ConditionalConverter, + "Only conditional converters may return null convertible types"); + this.globalConverters.add(converter); + } + else { + for (ConvertiblePair convertiblePair : convertibleTypes) { + getMatchableConverters(convertiblePair).add(converter); + } + } + } + + private ConvertersForPair getMatchableConverters(ConvertiblePair convertiblePair) { + return this.converters.computeIfAbsent(convertiblePair, k -> new ConvertersForPair()); + } + + public void remove(Class sourceType, Class targetType) { + this.converters.remove(new ConvertiblePair(sourceType, targetType)); + } + + /** + * Find a {@link GenericConverter} given a source and target type. + *

    This method will attempt to match all possible converters by working + * through the class and interface hierarchy of the types. + * @param sourceType the source type + * @param targetType the target type + * @return a matching {@link GenericConverter}, or {@code null} if none found + */ + @Nullable + public GenericConverter find(TypeDescriptor sourceType, TypeDescriptor targetType) { + // Search the full type hierarchy + List> sourceCandidates = getClassHierarchy(sourceType.getType()); + List> targetCandidates = getClassHierarchy(targetType.getType()); + for (Class sourceCandidate : sourceCandidates) { + for (Class targetCandidate : targetCandidates) { + ConvertiblePair convertiblePair = new ConvertiblePair(sourceCandidate, targetCandidate); + GenericConverter converter = getRegisteredConverter(sourceType, targetType, convertiblePair); + if (converter != null) { + return converter; + } + } + } + return null; + } + + @Nullable + private GenericConverter getRegisteredConverter(TypeDescriptor sourceType, + TypeDescriptor targetType, ConvertiblePair convertiblePair) { + + // Check specifically registered converters + ConvertersForPair convertersForPair = this.converters.get(convertiblePair); + if (convertersForPair != null) { + GenericConverter converter = convertersForPair.getConverter(sourceType, targetType); + if (converter != null) { + return converter; + } + } + // Check ConditionalConverters for a dynamic match + for (GenericConverter globalConverter : this.globalConverters) { + if (((ConditionalConverter) globalConverter).matches(sourceType, targetType)) { + return globalConverter; + } + } + return null; + } + + /** + * Returns an ordered class hierarchy for the given type. + * @param type the type + * @return an ordered list of all classes that the given type extends or implements + */ + private List> getClassHierarchy(Class type) { + List> hierarchy = new ArrayList<>(20); + Set> visited = new HashSet<>(20); + addToClassHierarchy(0, ClassUtils.resolvePrimitiveIfNecessary(type), false, hierarchy, visited); + boolean array = type.isArray(); + + int i = 0; + while (i < hierarchy.size()) { + Class candidate = hierarchy.get(i); + candidate = (array ? candidate.getComponentType() : ClassUtils.resolvePrimitiveIfNecessary(candidate)); + Class superclass = candidate.getSuperclass(); + if (superclass != null && superclass != Object.class && superclass != Enum.class) { + addToClassHierarchy(i + 1, candidate.getSuperclass(), array, hierarchy, visited); + } + addInterfacesToClassHierarchy(candidate, array, hierarchy, visited); + i++; + } + + if (Enum.class.isAssignableFrom(type)) { + addToClassHierarchy(hierarchy.size(), Enum.class, array, hierarchy, visited); + addToClassHierarchy(hierarchy.size(), Enum.class, false, hierarchy, visited); + addInterfacesToClassHierarchy(Enum.class, array, hierarchy, visited); + } + + addToClassHierarchy(hierarchy.size(), Object.class, array, hierarchy, visited); + addToClassHierarchy(hierarchy.size(), Object.class, false, hierarchy, visited); + return hierarchy; + } + + private void addInterfacesToClassHierarchy(Class type, boolean asArray, + List> hierarchy, Set> visited) { + + for (Class implementedInterface : type.getInterfaces()) { + addToClassHierarchy(hierarchy.size(), implementedInterface, asArray, hierarchy, visited); + } + } + + private void addToClassHierarchy(int index, Class type, boolean asArray, + List> hierarchy, Set> visited) { + + if (asArray) { + type = Array.newInstance(type, 0).getClass(); + } + if (visited.add(type)) { + hierarchy.add(index, type); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("ConversionService converters =\n"); + for (String converterString : getConverterStrings()) { + builder.append('\t').append(converterString).append('\n'); + } + return builder.toString(); + } + + private List getConverterStrings() { + List converterStrings = new ArrayList<>(); + for (ConvertersForPair convertersForPair : this.converters.values()) { + converterStrings.add(convertersForPair.toString()); + } + Collections.sort(converterStrings); + return converterStrings; + } + } + + + /** + * Manages converters registered with a specific {@link ConvertiblePair}. + */ + private static class ConvertersForPair { + + private final Deque converters = new ConcurrentLinkedDeque<>(); + + public void add(GenericConverter converter) { + this.converters.addFirst(converter); + } + + @Nullable + public GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) { + for (GenericConverter converter : this.converters) { + if (!(converter instanceof ConditionalGenericConverter) || + ((ConditionalGenericConverter) converter).matches(sourceType, targetType)) { + return converter; + } + } + return null; + } + + @Override + public String toString() { + return StringUtils.collectionToCommaDelimitedString(this.converters); + } + } + + + /** + * Internal converter that performs no operation. + */ + private static class NoOpConverter implements GenericConverter { + + private final String name; + + public NoOpConverter(String name) { + this.name = name; + } + + @Override + @Nullable + public Set getConvertibleTypes() { + return null; + } + + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + return source; + } + + @Override + public String toString() { + return this.name; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/IdToEntityConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/IdToEntityConverter.java new file mode 100644 index 0000000..238c848 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/IdToEntityConverter.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * Converts an entity identifier to a entity reference by calling a static finder method + * on the target entity type. + * + *

    For this converter to match, the finder method must be static, have the signature + * {@code find[EntityName]([IdType])}, and return an instance of the desired entity type. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + */ +final class IdToEntityConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + + public IdToEntityConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Object.class, Object.class)); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + Method finder = getFinder(targetType.getType()); + return (finder != null && + this.conversionService.canConvert(sourceType, TypeDescriptor.valueOf(finder.getParameterTypes()[0]))); + } + + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + Method finder = getFinder(targetType.getType()); + Assert.state(finder != null, "No finder method"); + Object id = this.conversionService.convert( + source, sourceType, TypeDescriptor.valueOf(finder.getParameterTypes()[0])); + return ReflectionUtils.invokeMethod(finder, source, id); + } + + @Nullable + private Method getFinder(Class entityClass) { + String finderMethod = "find" + getEntityName(entityClass); + Method[] methods; + boolean localOnlyFiltered; + try { + methods = entityClass.getDeclaredMethods(); + localOnlyFiltered = true; + } + catch (SecurityException ex) { + // Not allowed to access non-public methods... + // Fallback: check locally declared public methods only. + methods = entityClass.getMethods(); + localOnlyFiltered = false; + } + for (Method method : methods) { + if (Modifier.isStatic(method.getModifiers()) && method.getName().equals(finderMethod) && + method.getParameterCount() == 1 && method.getReturnType().equals(entityClass) && + (localOnlyFiltered || method.getDeclaringClass().equals(entityClass))) { + return method; + } + } + return null; + } + + private String getEntityName(Class entityClass) { + String shortName = ClassUtils.getShortName(entityClass); + int lastDot = shortName.lastIndexOf('.'); + if (lastDot != -1) { + return shortName.substring(lastDot + 1); + } + else { + return shortName; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/IntegerToEnumConverterFactory.java b/spring-core/src/main/java/org/springframework/core/convert/support/IntegerToEnumConverterFactory.java new file mode 100644 index 0000000..4322d8e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/IntegerToEnumConverterFactory.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; + +/** + * Converts from a Integer to a {@link java.lang.Enum} by calling {@link Class#getEnumConstants()}. + * + * @author Yanming Zhou + * @author Stephane Nicoll + * @since 4.3 + */ +@SuppressWarnings({"rawtypes", "unchecked"}) +final class IntegerToEnumConverterFactory implements ConverterFactory { + + @Override + public Converter getConverter(Class targetType) { + return new IntegerToEnum(ConversionUtils.getEnumType(targetType)); + } + + + private static class IntegerToEnum implements Converter { + + private final Class enumType; + + public IntegerToEnum(Class enumType) { + this.enumType = enumType; + } + + @Override + public T convert(Integer source) { + return this.enumType.getEnumConstants()[source]; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/MapToMapConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/MapToMapConverter.java new file mode 100644 index 0000000..5b791f9 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/MapToMapConverter.java @@ -0,0 +1,152 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.core.CollectionFactory; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.lang.Nullable; + +/** + * Converts a Map to another Map. + * + *

    First, creates a new Map of the requested targetType with a size equal to the + * size of the source Map. Then copies each element in the source map to the target map. + * Will perform a conversion from the source maps's parameterized K,V types to the target + * map's parameterized types K,V if necessary. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + */ +final class MapToMapConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + + public MapToMapConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Map.class, Map.class)); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return canConvertKey(sourceType, targetType) && canConvertValue(sourceType, targetType); + } + + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + @SuppressWarnings("unchecked") + Map sourceMap = (Map) source; + + // Shortcut if possible... + boolean copyRequired = !targetType.getType().isInstance(source); + if (!copyRequired && sourceMap.isEmpty()) { + return sourceMap; + } + TypeDescriptor keyDesc = targetType.getMapKeyTypeDescriptor(); + TypeDescriptor valueDesc = targetType.getMapValueTypeDescriptor(); + + List targetEntries = new ArrayList<>(sourceMap.size()); + for (Map.Entry entry : sourceMap.entrySet()) { + Object sourceKey = entry.getKey(); + Object sourceValue = entry.getValue(); + Object targetKey = convertKey(sourceKey, sourceType, keyDesc); + Object targetValue = convertValue(sourceValue, sourceType, valueDesc); + targetEntries.add(new MapEntry(targetKey, targetValue)); + if (sourceKey != targetKey || sourceValue != targetValue) { + copyRequired = true; + } + } + if (!copyRequired) { + return sourceMap; + } + + Map targetMap = CollectionFactory.createMap(targetType.getType(), + (keyDesc != null ? keyDesc.getType() : null), sourceMap.size()); + + for (MapEntry entry : targetEntries) { + entry.addToMap(targetMap); + } + return targetMap; + } + + + // internal helpers + + private boolean canConvertKey(TypeDescriptor sourceType, TypeDescriptor targetType) { + return ConversionUtils.canConvertElements(sourceType.getMapKeyTypeDescriptor(), + targetType.getMapKeyTypeDescriptor(), this.conversionService); + } + + private boolean canConvertValue(TypeDescriptor sourceType, TypeDescriptor targetType) { + return ConversionUtils.canConvertElements(sourceType.getMapValueTypeDescriptor(), + targetType.getMapValueTypeDescriptor(), this.conversionService); + } + + @Nullable + private Object convertKey(Object sourceKey, TypeDescriptor sourceType, @Nullable TypeDescriptor targetType) { + if (targetType == null) { + return sourceKey; + } + return this.conversionService.convert(sourceKey, sourceType.getMapKeyTypeDescriptor(sourceKey), targetType); + } + + @Nullable + private Object convertValue(Object sourceValue, TypeDescriptor sourceType, @Nullable TypeDescriptor targetType) { + if (targetType == null) { + return sourceValue; + } + return this.conversionService.convert(sourceValue, sourceType.getMapValueTypeDescriptor(sourceValue), targetType); + } + + + private static class MapEntry { + + @Nullable + private final Object key; + + @Nullable + private final Object value; + + public MapEntry(@Nullable Object key, @Nullable Object value) { + this.key = key; + this.value = value; + } + + public void addToMap(Map map) { + map.put(this.key, this.value); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/NumberToCharacterConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/NumberToCharacterConverter.java new file mode 100644 index 0000000..2863a9c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/NumberToCharacterConverter.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import org.springframework.core.convert.converter.Converter; + +/** + * Converts from any JDK-standard Number implementation to a Character. + * + * @author Keith Donald + * @since 3.0 + * @see java.lang.Character + * @see java.lang.Short + * @see java.lang.Integer + * @see java.lang.Long + * @see java.math.BigInteger + * @see java.lang.Float + * @see java.lang.Double + * @see java.math.BigDecimal + */ +final class NumberToCharacterConverter implements Converter { + + @Override + public Character convert(Number source) { + return (char) source.shortValue(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/NumberToNumberConverterFactory.java b/spring-core/src/main/java/org/springframework/core/convert/support/NumberToNumberConverterFactory.java new file mode 100644 index 0000000..846d487 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/NumberToNumberConverterFactory.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalConverter; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.util.NumberUtils; + +/** + * Converts from any JDK-standard Number implementation to any other JDK-standard Number implementation. + * + *

    Support Number classes including Byte, Short, Integer, Float, Double, Long, BigInteger, BigDecimal. This class + * delegates to {@link NumberUtils#convertNumberToTargetClass(Number, Class)} to perform the conversion. + * + * @author Keith Donald + * @since 3.0 + * @see java.lang.Byte + * @see java.lang.Short + * @see java.lang.Integer + * @see java.lang.Long + * @see java.math.BigInteger + * @see java.lang.Float + * @see java.lang.Double + * @see java.math.BigDecimal + * @see NumberUtils + */ +final class NumberToNumberConverterFactory implements ConverterFactory, ConditionalConverter { + + @Override + public Converter getConverter(Class targetType) { + return new NumberToNumber<>(targetType); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return !sourceType.equals(targetType); + } + + + private static final class NumberToNumber implements Converter { + + private final Class targetType; + + NumberToNumber(Class targetType) { + this.targetType = targetType; + } + + @Override + public T convert(Number source) { + return NumberUtils.convertNumberToTargetClass(source, this.targetType); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToArrayConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToArrayConverter.java new file mode 100644 index 0000000..e57a7b8 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToArrayConverter.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.lang.reflect.Array; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Converts an Object to a single-element array containing the Object. + * Will convert the Object to the target array's component type if necessary. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + */ +final class ObjectToArrayConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + + public ObjectToArrayConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Object.class, Object[].class)); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return ConversionUtils.canConvertElements(sourceType, targetType.getElementTypeDescriptor(), + this.conversionService); + } + + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + TypeDescriptor targetElementType = targetType.getElementTypeDescriptor(); + Assert.state(targetElementType != null, "No target element type"); + Object target = Array.newInstance(targetElementType.getType(), 1); + Object targetElement = this.conversionService.convert(source, sourceType, targetElementType); + Array.set(target, 0, targetElement); + return target; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToCollectionConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToCollectionConverter.java new file mode 100644 index 0000000..28cf07c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToCollectionConverter.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.CollectionFactory; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.lang.Nullable; + +/** + * Converts an Object to a single-element Collection containing the Object. + * Will convert the Object to the target Collection's parameterized type if necessary. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + */ +final class ObjectToCollectionConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + + public ObjectToCollectionConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Object.class, Collection.class)); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return ConversionUtils.canConvertElements(sourceType, targetType.getElementTypeDescriptor(), this.conversionService); + } + + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + + TypeDescriptor elementDesc = targetType.getElementTypeDescriptor(); + Collection target = CollectionFactory.createCollection(targetType.getType(), + (elementDesc != null ? elementDesc.getType() : null), 1); + + if (elementDesc == null || elementDesc.isCollection()) { + target.add(source); + } + else { + Object singleElement = this.conversionService.convert(source, sourceType, elementDesc); + target.add(singleElement); + } + return target; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToObjectConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToObjectConverter.java new file mode 100644 index 0000000..802e097 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToObjectConverter.java @@ -0,0 +1,204 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ReflectionUtils; + +/** + * Generic converter that uses conventions to convert a source object to a + * {@code targetType} by delegating to a method on the source object or to + * a static factory method or constructor on the {@code targetType}. + * + *

    Conversion Algorithm

    + *
      + *
    1. Invoke a non-static {@code to[targetType.simpleName]()} method on the + * source object that has a return type equal to {@code targetType}, if such + * a method exists. For example, {@code org.example.Bar Foo#toBar()} is a + * method that follows this convention. + *
    2. Otherwise invoke a static {@code valueOf(sourceType)} or Java + * 8 style static {@code of(sourceType)} or {@code from(sourceType)} + * method on the {@code targetType}, if such a method exists. + *
    3. Otherwise invoke a constructor on the {@code targetType} that accepts + * a single {@code sourceType} argument, if such a constructor exists. + *
    4. Otherwise throw a {@link ConversionFailedException}. + *
    + * + *

    Warning: this converter does not support the + * {@link Object#toString()} method for converting from a {@code sourceType} + * to {@code java.lang.String}. For {@code toString()} support, use + * {@link FallbackObjectToStringConverter} instead. + * + * @author Keith Donald + * @author Juergen Hoeller + * @author Sam Brannen + * @since 3.0 + * @see FallbackObjectToStringConverter + */ +final class ObjectToObjectConverter implements ConditionalGenericConverter { + + // Cache for the latest to-method resolved on a given Class + private static final Map, Member> conversionMemberCache = + new ConcurrentReferenceHashMap<>(32); + + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Object.class, Object.class)); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return (sourceType.getType() != targetType.getType() && + hasConversionMethodOrConstructor(targetType.getType(), sourceType.getType())); + } + + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + Class sourceClass = sourceType.getType(); + Class targetClass = targetType.getType(); + Member member = getValidatedMember(targetClass, sourceClass); + + try { + if (member instanceof Method) { + Method method = (Method) member; + ReflectionUtils.makeAccessible(method); + if (!Modifier.isStatic(method.getModifiers())) { + return method.invoke(source); + } + else { + return method.invoke(null, source); + } + } + else if (member instanceof Constructor) { + Constructor ctor = (Constructor) member; + ReflectionUtils.makeAccessible(ctor); + return ctor.newInstance(source); + } + } + catch (InvocationTargetException ex) { + throw new ConversionFailedException(sourceType, targetType, source, ex.getTargetException()); + } + catch (Throwable ex) { + throw new ConversionFailedException(sourceType, targetType, source, ex); + } + + // If sourceClass is Number and targetClass is Integer, the following message should expand to: + // No toInteger() method exists on java.lang.Number, and no static valueOf/of/from(java.lang.Number) + // method or Integer(java.lang.Number) constructor exists on java.lang.Integer. + throw new IllegalStateException(String.format("No to%3$s() method exists on %1$s, " + + "and no static valueOf/of/from(%1$s) method or %3$s(%1$s) constructor exists on %2$s.", + sourceClass.getName(), targetClass.getName(), targetClass.getSimpleName())); + } + + + + static boolean hasConversionMethodOrConstructor(Class targetClass, Class sourceClass) { + return (getValidatedMember(targetClass, sourceClass) != null); + } + + @Nullable + private static Member getValidatedMember(Class targetClass, Class sourceClass) { + Member member = conversionMemberCache.get(targetClass); + if (isApplicable(member, sourceClass)) { + return member; + } + + member = determineToMethod(targetClass, sourceClass); + if (member == null) { + member = determineFactoryMethod(targetClass, sourceClass); + if (member == null) { + member = determineFactoryConstructor(targetClass, sourceClass); + if (member == null) { + return null; + } + } + } + + conversionMemberCache.put(targetClass, member); + return member; + } + + private static boolean isApplicable(Member member, Class sourceClass) { + if (member instanceof Method) { + Method method = (Method) member; + return (!Modifier.isStatic(method.getModifiers()) ? + ClassUtils.isAssignable(method.getDeclaringClass(), sourceClass) : + method.getParameterTypes()[0] == sourceClass); + } + else if (member instanceof Constructor) { + Constructor ctor = (Constructor) member; + return (ctor.getParameterTypes()[0] == sourceClass); + } + else { + return false; + } + } + + @Nullable + private static Method determineToMethod(Class targetClass, Class sourceClass) { + if (String.class == targetClass || String.class == sourceClass) { + // Do not accept a toString() method or any to methods on String itself + return null; + } + + Method method = ClassUtils.getMethodIfAvailable(sourceClass, "to" + targetClass.getSimpleName()); + return (method != null && !Modifier.isStatic(method.getModifiers()) && + ClassUtils.isAssignable(targetClass, method.getReturnType()) ? method : null); + } + + @Nullable + private static Method determineFactoryMethod(Class targetClass, Class sourceClass) { + if (String.class == targetClass) { + // Do not accept the String.valueOf(Object) method + return null; + } + + Method method = ClassUtils.getStaticMethod(targetClass, "valueOf", sourceClass); + if (method == null) { + method = ClassUtils.getStaticMethod(targetClass, "of", sourceClass); + if (method == null) { + method = ClassUtils.getStaticMethod(targetClass, "from", sourceClass); + } + } + return method; + } + + @Nullable + private static Constructor determineFactoryConstructor(Class targetClass, Class sourceClass) { + return ClassUtils.getConstructorIfAvailable(targetClass, sourceClass); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToOptionalConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToOptionalConverter.java new file mode 100644 index 0000000..830530d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToOptionalConverter.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Optional; +import java.util.Set; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.lang.Nullable; + +/** + * Convert an Object to {@code java.util.Optional} if necessary using the + * {@code ConversionService} to convert the source Object to the generic type + * of Optional when known. + * + * @author Rossen Stoyanchev + * @author Juergen Hoeller + * @since 4.1 + */ +final class ObjectToOptionalConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + + public ObjectToOptionalConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + + @Override + public Set getConvertibleTypes() { + Set convertibleTypes = new LinkedHashSet<>(4); + convertibleTypes.add(new ConvertiblePair(Collection.class, Optional.class)); + convertibleTypes.add(new ConvertiblePair(Object[].class, Optional.class)); + convertibleTypes.add(new ConvertiblePair(Object.class, Optional.class)); + return convertibleTypes; + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + if (targetType.getResolvableType().hasGenerics()) { + return this.conversionService.canConvert(sourceType, new GenericTypeDescriptor(targetType)); + } + else { + return true; + } + } + + @Override + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return Optional.empty(); + } + else if (source instanceof Optional) { + return source; + } + else if (targetType.getResolvableType().hasGenerics()) { + Object target = this.conversionService.convert(source, sourceType, new GenericTypeDescriptor(targetType)); + if (target == null || (target.getClass().isArray() && Array.getLength(target) == 0) || + (target instanceof Collection && ((Collection) target).isEmpty())) { + return Optional.empty(); + } + return Optional.of(target); + } + else { + return Optional.of(source); + } + } + + + @SuppressWarnings("serial") + private static class GenericTypeDescriptor extends TypeDescriptor { + + public GenericTypeDescriptor(TypeDescriptor typeDescriptor) { + super(typeDescriptor.getResolvableType().getGeneric(), null, typeDescriptor.getAnnotations()); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToStringConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToStringConverter.java new file mode 100644 index 0000000..2229061 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToStringConverter.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import org.springframework.core.convert.converter.Converter; + +/** + * Simply calls {@link Object#toString()} to convert a source Object to a String. + * + * @author Keith Donald + * @since 3.0 + */ +final class ObjectToStringConverter implements Converter { + + @Override + public String convert(Object source) { + return source.toString(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/PropertiesToStringConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/PropertiesToStringConverter.java new file mode 100644 index 0000000..c953280 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/PropertiesToStringConverter.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Properties; + +import org.springframework.core.convert.converter.Converter; + +/** + * Converts from a Properties to a String by calling {@link Properties#store(java.io.OutputStream, String)}. + * Decodes with the ISO-8859-1 charset before returning the String. + * + * @author Keith Donald + * @since 3.0 + */ +final class PropertiesToStringConverter implements Converter { + + @Override + public String convert(Properties source) { + try { + ByteArrayOutputStream os = new ByteArrayOutputStream(256); + source.store(os, null); + return os.toString("ISO-8859-1"); + } + catch (IOException ex) { + // Should never happen. + throw new IllegalArgumentException("Failed to store [" + source + "] into String", ex); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StreamConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StreamConverter.java new file mode 100644 index 0000000..438d58c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StreamConverter.java @@ -0,0 +1,130 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.lang.Nullable; + +/** + * Converts a {@link Stream} to and from a collection or array, converting the + * element type if necessary. + * + * @author Stephane Nicoll + * @since 4.2 + */ +class StreamConverter implements ConditionalGenericConverter { + + private static final TypeDescriptor STREAM_TYPE = TypeDescriptor.valueOf(Stream.class); + + private static final Set CONVERTIBLE_TYPES = createConvertibleTypes(); + + private final ConversionService conversionService; + + + public StreamConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + + @Override + public Set getConvertibleTypes() { + return CONVERTIBLE_TYPES; + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + if (sourceType.isAssignableTo(STREAM_TYPE)) { + return matchesFromStream(sourceType.getElementTypeDescriptor(), targetType); + } + if (targetType.isAssignableTo(STREAM_TYPE)) { + return matchesToStream(targetType.getElementTypeDescriptor(), sourceType); + } + return false; + } + + /** + * Validate that a {@link Collection} of the elements held within the stream can be + * converted to the specified {@code targetType}. + * @param elementType the type of the stream elements + * @param targetType the type to convert to + */ + public boolean matchesFromStream(@Nullable TypeDescriptor elementType, TypeDescriptor targetType) { + TypeDescriptor collectionOfElement = TypeDescriptor.collection(Collection.class, elementType); + return this.conversionService.canConvert(collectionOfElement, targetType); + } + + /** + * Validate that the specified {@code sourceType} can be converted to a {@link Collection} of + * the type of the stream elements. + * @param elementType the type of the stream elements + * @param sourceType the type to convert from + */ + public boolean matchesToStream(@Nullable TypeDescriptor elementType, TypeDescriptor sourceType) { + TypeDescriptor collectionOfElement = TypeDescriptor.collection(Collection.class, elementType); + return this.conversionService.canConvert(sourceType, collectionOfElement); + } + + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (sourceType.isAssignableTo(STREAM_TYPE)) { + return convertFromStream((Stream) source, sourceType, targetType); + } + if (targetType.isAssignableTo(STREAM_TYPE)) { + return convertToStream(source, sourceType, targetType); + } + // Should not happen + throw new IllegalStateException("Unexpected source/target types"); + } + + @Nullable + private Object convertFromStream(@Nullable Stream source, TypeDescriptor streamType, TypeDescriptor targetType) { + List content = (source != null ? source.collect(Collectors.toList()) : Collections.emptyList()); + TypeDescriptor listType = TypeDescriptor.collection(List.class, streamType.getElementTypeDescriptor()); + return this.conversionService.convert(content, listType, targetType); + } + + private Object convertToStream(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor streamType) { + TypeDescriptor targetCollection = TypeDescriptor.collection(List.class, streamType.getElementTypeDescriptor()); + List target = (List) this.conversionService.convert(source, sourceType, targetCollection); + if (target == null) { + target = Collections.emptyList(); + } + return target.stream(); + } + + + private static Set createConvertibleTypes() { + Set convertiblePairs = new HashSet<>(); + convertiblePairs.add(new ConvertiblePair(Stream.class, Collection.class)); + convertiblePairs.add(new ConvertiblePair(Stream.class, Object[].class)); + convertiblePairs.add(new ConvertiblePair(Collection.class, Stream.class)); + convertiblePairs.add(new ConvertiblePair(Object[].class, Stream.class)); + return convertiblePairs; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToArrayConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToArrayConverter.java new file mode 100644 index 0000000..038a82d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToArrayConverter.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.lang.reflect.Array; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Converts a comma-delimited String to an Array. + * Only matches if String.class can be converted to the target array element type. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + */ +final class StringToArrayConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + + public StringToArrayConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(String.class, Object[].class)); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return ConversionUtils.canConvertElements(sourceType, targetType.getElementTypeDescriptor(), + this.conversionService); + } + + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + String string = (String) source; + String[] fields = StringUtils.commaDelimitedListToStringArray(string); + TypeDescriptor targetElementType = targetType.getElementTypeDescriptor(); + Assert.state(targetElementType != null, "No target element type"); + Object target = Array.newInstance(targetElementType.getType(), fields.length); + for (int i = 0; i < fields.length; i++) { + String sourceElement = fields[i]; + Object targetElement = this.conversionService.convert(sourceElement.trim(), sourceType, targetElementType); + Array.set(target, i, targetElement); + } + return target; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToBooleanConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToBooleanConverter.java new file mode 100644 index 0000000..4b1c0fd --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToBooleanConverter.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.lang.Nullable; + +/** + * Converts String to a Boolean. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + */ +final class StringToBooleanConverter implements Converter { + + private static final Set trueValues = new HashSet<>(8); + + private static final Set falseValues = new HashSet<>(8); + + static { + trueValues.add("true"); + trueValues.add("on"); + trueValues.add("yes"); + trueValues.add("1"); + + falseValues.add("false"); + falseValues.add("off"); + falseValues.add("no"); + falseValues.add("0"); + } + + + @Override + @Nullable + public Boolean convert(String source) { + String value = source.trim(); + if (value.isEmpty()) { + return null; + } + value = value.toLowerCase(); + if (trueValues.contains(value)) { + return Boolean.TRUE; + } + else if (falseValues.contains(value)) { + return Boolean.FALSE; + } + else { + throw new IllegalArgumentException("Invalid boolean value '" + source + "'"); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToCharacterConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToCharacterConverter.java new file mode 100644 index 0000000..97374fb --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToCharacterConverter.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.lang.Nullable; + +/** + * Converts a String to a Character. + * + * @author Keith Donald + * @since 3.0 + */ +final class StringToCharacterConverter implements Converter { + + @Override + @Nullable + public Character convert(String source) { + if (source.isEmpty()) { + return null; + } + if (source.length() > 1) { + throw new IllegalArgumentException( + "Can only convert a [String] with length of 1 to a [Character]; string value '" + source + "' has length of " + source.length()); + } + return source.charAt(0); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToCharsetConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToCharsetConverter.java new file mode 100644 index 0000000..728ea71 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToCharsetConverter.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.nio.charset.Charset; + +import org.springframework.core.convert.converter.Converter; + +/** + * Convert a String to a {@link Charset}. + * + * @author Stephane Nicoll + * @since 4.2 + */ +class StringToCharsetConverter implements Converter { + + @Override + public Charset convert(String source) { + return Charset.forName(source); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToCollectionConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToCollectionConverter.java new file mode 100644 index 0000000..df565b4 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToCollectionConverter.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.CollectionFactory; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Converts a comma-delimited String to a Collection. + * If the target collection element type is declared, only matches if + * {@code String.class} can be converted to it. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + */ +final class StringToCollectionConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + + public StringToCollectionConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(String.class, Collection.class)); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return (targetType.getElementTypeDescriptor() == null || + this.conversionService.canConvert(sourceType, targetType.getElementTypeDescriptor())); + } + + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + String string = (String) source; + + String[] fields = StringUtils.commaDelimitedListToStringArray(string); + TypeDescriptor elementDesc = targetType.getElementTypeDescriptor(); + Collection target = CollectionFactory.createCollection(targetType.getType(), + (elementDesc != null ? elementDesc.getType() : null), fields.length); + + if (elementDesc == null) { + for (String field : fields) { + target.add(field.trim()); + } + } + else { + for (String field : fields) { + Object targetElement = this.conversionService.convert(field.trim(), sourceType, elementDesc); + target.add(targetElement); + } + } + return target; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToCurrencyConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToCurrencyConverter.java new file mode 100644 index 0000000..33897e3 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToCurrencyConverter.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.util.Currency; + +import org.springframework.core.convert.converter.Converter; + +/** + * Convert a String to a {@link Currency}. + * + * @author Stephane Nicoll + * @since 4.2 + */ +class StringToCurrencyConverter implements Converter { + + @Override + public Currency convert(String source) { + return Currency.getInstance(source); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToEnumConverterFactory.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToEnumConverterFactory.java new file mode 100644 index 0000000..887690b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToEnumConverterFactory.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.lang.Nullable; + +/** + * Converts from a String to a {@link java.lang.Enum} by calling {@link Enum#valueOf(Class, String)}. + * + * @author Keith Donald + * @author Stephane Nicoll + * @since 3.0 + */ +@SuppressWarnings({"rawtypes", "unchecked"}) +final class StringToEnumConverterFactory implements ConverterFactory { + + @Override + public Converter getConverter(Class targetType) { + return new StringToEnum(ConversionUtils.getEnumType(targetType)); + } + + + private static class StringToEnum implements Converter { + + private final Class enumType; + + StringToEnum(Class enumType) { + this.enumType = enumType; + } + + @Override + @Nullable + public T convert(String source) { + if (source.isEmpty()) { + // It's an empty enum identifier: reset the enum value to null. + return null; + } + return (T) Enum.valueOf(this.enumType, source.trim()); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToLocaleConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToLocaleConverter.java new file mode 100644 index 0000000..d68fe56 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToLocaleConverter.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.util.Locale; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Converts from a String to a {@link java.util.Locale}. + * + *

    Accepts the classic {@link Locale} String format ({@link Locale#toString()}) + * as well as BCP 47 language tags ({@link Locale#forLanguageTag} on Java 7+). + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + * @see StringUtils#parseLocale + */ +final class StringToLocaleConverter implements Converter { + + @Override + @Nullable + public Locale convert(String source) { + return StringUtils.parseLocale(source); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToNumberConverterFactory.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToNumberConverterFactory.java new file mode 100644 index 0000000..082d4c9 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToNumberConverterFactory.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.lang.Nullable; +import org.springframework.util.NumberUtils; + +/** + * Converts from a String any JDK-standard Number implementation. + * + *

    Support Number classes including Byte, Short, Integer, Float, Double, Long, BigInteger, BigDecimal. This class + * delegates to {@link NumberUtils#parseNumber(String, Class)} to perform the conversion. + * + * @author Keith Donald + * @since 3.0 + * @see java.lang.Byte + * @see java.lang.Short + * @see java.lang.Integer + * @see java.lang.Long + * @see java.math.BigInteger + * @see java.lang.Float + * @see java.lang.Double + * @see java.math.BigDecimal + * @see NumberUtils + */ +final class StringToNumberConverterFactory implements ConverterFactory { + + @Override + public Converter getConverter(Class targetType) { + return new StringToNumber<>(targetType); + } + + + private static final class StringToNumber implements Converter { + + private final Class targetType; + + public StringToNumber(Class targetType) { + this.targetType = targetType; + } + + @Override + @Nullable + public T convert(String source) { + if (source.isEmpty()) { + return null; + } + return NumberUtils.parseNumber(source, this.targetType); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToPropertiesConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToPropertiesConverter.java new file mode 100644 index 0000000..f8c48e5 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToPropertiesConverter.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.Properties; + +import org.springframework.core.convert.converter.Converter; + +/** + * Converts a String to a Properties by calling Properties#load(java.io.InputStream). + * Uses ISO-8559-1 encoding required by Properties. + * + * @author Keith Donald + * @since 3.0 + */ +final class StringToPropertiesConverter implements Converter { + + @Override + public Properties convert(String source) { + try { + Properties props = new Properties(); + // Must use the ISO-8859-1 encoding because Properties.load(stream) expects it. + props.load(new ByteArrayInputStream(source.getBytes(StandardCharsets.ISO_8859_1))); + return props; + } + catch (Exception ex) { + // Should never happen. + throw new IllegalArgumentException("Failed to parse [" + source + "] into Properties", ex); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToTimeZoneConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToTimeZoneConverter.java new file mode 100644 index 0000000..fd7404f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToTimeZoneConverter.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.util.TimeZone; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.util.StringUtils; + +/** + * Convert a String to a {@link TimeZone}. + * + * @author Stephane Nicoll + * @since 4.2 + */ +class StringToTimeZoneConverter implements Converter { + + @Override + public TimeZone convert(String source) { + return StringUtils.parseTimeZoneString(source); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToUUIDConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToUUIDConverter.java new file mode 100644 index 0000000..cb63290 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToUUIDConverter.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.util.UUID; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Converts from a String to a {@link java.util.UUID}. + * + * @author Phillip Webb + * @since 3.2 + * @see UUID#fromString + */ +final class StringToUUIDConverter implements Converter { + + @Override + @Nullable + public UUID convert(String source) { + return (StringUtils.hasText(source) ? UUID.fromString(source.trim()) : null); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ZoneIdToTimeZoneConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ZoneIdToTimeZoneConverter.java new file mode 100644 index 0000000..1da6a1c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ZoneIdToTimeZoneConverter.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.time.ZoneId; +import java.util.TimeZone; + +import org.springframework.core.convert.converter.Converter; + +/** + * Simple converter from Java 8's {@link java.time.ZoneId} to {@link java.util.TimeZone}. + * + *

    Note that Spring's default ConversionService setup understands the 'from'/'to' convention + * that the JSR-310 {@code java.time} package consistently uses. That convention is implemented + * reflectively in {@link ObjectToObjectConverter}, not in specific JSR-310 converters. + * It covers {@link java.util.TimeZone#toZoneId()} as well, and also + * {@link java.util.Date#from(java.time.Instant)} and {@link java.util.Date#toInstant()}. + * + * @author Juergen Hoeller + * @since 4.0 + * @see TimeZone#getTimeZone(java.time.ZoneId) + */ +final class ZoneIdToTimeZoneConverter implements Converter { + + @Override + public TimeZone convert(ZoneId source) { + return TimeZone.getTimeZone(source); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ZonedDateTimeToCalendarConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ZonedDateTimeToCalendarConverter.java new file mode 100644 index 0000000..2541d1c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ZonedDateTimeToCalendarConverter.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.time.ZonedDateTime; +import java.util.Calendar; +import java.util.GregorianCalendar; + +import org.springframework.core.convert.converter.Converter; + +/** + * Simple converter from Java 8's {@link java.time.ZonedDateTime} to {@link java.util.Calendar}. + * + *

    Note that Spring's default ConversionService setup understands the 'from'/'to' convention + * that the JSR-310 {@code java.time} package consistently uses. That convention is implemented + * reflectively in {@link ObjectToObjectConverter}, not in specific JSR-310 converters. + * It covers {@link java.util.GregorianCalendar#toZonedDateTime()} as well, and also + * {@link java.util.Date#from(java.time.Instant)} and {@link java.util.Date#toInstant()}. + * + * @author Juergen Hoeller + * @since 4.0.1 + * @see java.util.GregorianCalendar#from(java.time.ZonedDateTime) + */ +final class ZonedDateTimeToCalendarConverter implements Converter { + + @Override + public Calendar convert(ZonedDateTime source) { + return GregorianCalendar.from(source); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/package-info.java b/spring-core/src/main/java/org/springframework/core/convert/support/package-info.java new file mode 100644 index 0000000..c516a7e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/convert/support/package-info.java @@ -0,0 +1,9 @@ +/** + * Default implementation of the type conversion system. + */ +@NonNullApi +@NonNullFields +package org.springframework.core.convert.support; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java b/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java new file mode 100644 index 0000000..56c894f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java @@ -0,0 +1,581 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import java.security.AccessControlException; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.SpringProperties; +import org.springframework.core.convert.support.ConfigurableConversionService; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Abstract base class for {@link Environment} implementations. Supports the notion of + * reserved default profile names and enables specifying active and default profiles + * through the {@link #ACTIVE_PROFILES_PROPERTY_NAME} and + * {@link #DEFAULT_PROFILES_PROPERTY_NAME} properties. + * + *

    Concrete subclasses differ primarily on which {@link PropertySource} objects they + * add by default. {@code AbstractEnvironment} adds none. Subclasses should contribute + * property sources through the protected {@link #customizePropertySources(MutablePropertySources)} + * hook, while clients should customize using {@link ConfigurableEnvironment#getPropertySources()} + * and working against the {@link MutablePropertySources} API. + * See {@link ConfigurableEnvironment} javadoc for usage examples. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + * @see ConfigurableEnvironment + * @see StandardEnvironment + */ +public abstract class AbstractEnvironment implements ConfigurableEnvironment { + + /** + * System property that instructs Spring to ignore system environment variables, + * i.e. to never attempt to retrieve such a variable via {@link System#getenv()}. + *

    The default is "false", falling back to system environment variable checks if a + * Spring environment property (e.g. a placeholder in a configuration String) isn't + * resolvable otherwise. Consider switching this flag to "true" if you experience + * log warnings from {@code getenv} calls coming from Spring, e.g. on WebSphere + * with strict SecurityManager settings and AccessControlExceptions warnings. + * @see #suppressGetenvAccess() + */ + public static final String IGNORE_GETENV_PROPERTY_NAME = "spring.getenv.ignore"; + + /** + * Name of property to set to specify active profiles: {@value}. Value may be comma + * delimited. + *

    Note that certain shell environments such as Bash disallow the use of the period + * character in variable names. Assuming that Spring's {@link SystemEnvironmentPropertySource} + * is in use, this property may be specified as an environment variable as + * {@code SPRING_PROFILES_ACTIVE}. + * @see ConfigurableEnvironment#setActiveProfiles + */ + public static final String ACTIVE_PROFILES_PROPERTY_NAME = "spring.profiles.active"; + + /** + * Name of property to set to specify profiles active by default: {@value}. Value may + * be comma delimited. + *

    Note that certain shell environments such as Bash disallow the use of the period + * character in variable names. Assuming that Spring's {@link SystemEnvironmentPropertySource} + * is in use, this property may be specified as an environment variable as + * {@code SPRING_PROFILES_DEFAULT}. + * @see ConfigurableEnvironment#setDefaultProfiles + */ + public static final String DEFAULT_PROFILES_PROPERTY_NAME = "spring.profiles.default"; + + /** + * Name of reserved default profile name: {@value}. If no default profile names are + * explicitly and no active profile names are explicitly set, this profile will + * automatically be activated by default. + * @see #getReservedDefaultProfiles + * @see ConfigurableEnvironment#setDefaultProfiles + * @see ConfigurableEnvironment#setActiveProfiles + * @see AbstractEnvironment#DEFAULT_PROFILES_PROPERTY_NAME + * @see AbstractEnvironment#ACTIVE_PROFILES_PROPERTY_NAME + */ + protected static final String RESERVED_DEFAULT_PROFILE_NAME = "default"; + + + protected final Log logger = LogFactory.getLog(getClass()); + + private final Set activeProfiles = new LinkedHashSet<>(); + + private final Set defaultProfiles = new LinkedHashSet<>(getReservedDefaultProfiles()); + + private final MutablePropertySources propertySources = new MutablePropertySources(); + + private final ConfigurablePropertyResolver propertyResolver = + new PropertySourcesPropertyResolver(this.propertySources); + + + /** + * Create a new {@code Environment} instance, calling back to + * {@link #customizePropertySources(MutablePropertySources)} during construction to + * allow subclasses to contribute or manipulate {@link PropertySource} instances as + * appropriate. + * @see #customizePropertySources(MutablePropertySources) + */ + public AbstractEnvironment() { + customizePropertySources(this.propertySources); + } + + + /** + * Customize the set of {@link PropertySource} objects to be searched by this + * {@code Environment} during calls to {@link #getProperty(String)} and related + * methods. + * + *

    Subclasses that override this method are encouraged to add property + * sources using {@link MutablePropertySources#addLast(PropertySource)} such that + * further subclasses may call {@code super.customizePropertySources()} with + * predictable results. For example: + *

    +	 * public class Level1Environment extends AbstractEnvironment {
    +	 *     @Override
    +	 *     protected void customizePropertySources(MutablePropertySources propertySources) {
    +	 *         super.customizePropertySources(propertySources); // no-op from base class
    +	 *         propertySources.addLast(new PropertySourceA(...));
    +	 *         propertySources.addLast(new PropertySourceB(...));
    +	 *     }
    +	 * }
    +	 *
    +	 * public class Level2Environment extends Level1Environment {
    +	 *     @Override
    +	 *     protected void customizePropertySources(MutablePropertySources propertySources) {
    +	 *         super.customizePropertySources(propertySources); // add all from superclass
    +	 *         propertySources.addLast(new PropertySourceC(...));
    +	 *         propertySources.addLast(new PropertySourceD(...));
    +	 *     }
    +	 * }
    +	 * 
    + * In this arrangement, properties will be resolved against sources A, B, C, D in that + * order. That is to say that property source "A" has precedence over property source + * "D". If the {@code Level2Environment} subclass wished to give property sources C + * and D higher precedence than A and B, it could simply call + * {@code super.customizePropertySources} after, rather than before adding its own: + *
    +	 * public class Level2Environment extends Level1Environment {
    +	 *     @Override
    +	 *     protected void customizePropertySources(MutablePropertySources propertySources) {
    +	 *         propertySources.addLast(new PropertySourceC(...));
    +	 *         propertySources.addLast(new PropertySourceD(...));
    +	 *         super.customizePropertySources(propertySources); // add all from superclass
    +	 *     }
    +	 * }
    +	 * 
    + * The search order is now C, D, A, B as desired. + * + *

    Beyond these recommendations, subclasses may use any of the {@code add*}, + * {@code remove}, or {@code replace} methods exposed by {@link MutablePropertySources} + * in order to create the exact arrangement of property sources desired. + * + *

    The base implementation registers no property sources. + * + *

    Note that clients of any {@link ConfigurableEnvironment} may further customize + * property sources via the {@link #getPropertySources()} accessor, typically within + * an {@link org.springframework.context.ApplicationContextInitializer + * ApplicationContextInitializer}. For example: + *

    +	 * ConfigurableEnvironment env = new StandardEnvironment();
    +	 * env.getPropertySources().addLast(new PropertySourceX(...));
    +	 * 
    + * + *

    A warning about instance variable access

    + * Instance variables declared in subclasses and having default initial values should + * not be accessed from within this method. Due to Java object creation + * lifecycle constraints, any initial value will not yet be assigned when this + * callback is invoked by the {@link #AbstractEnvironment()} constructor, which may + * lead to a {@code NullPointerException} or other problems. If you need to access + * default values of instance variables, leave this method as a no-op and perform + * property source manipulation and instance variable access directly within the + * subclass constructor. Note that assigning values to instance variables is + * not problematic; it is only attempting to read default values that must be avoided. + * + * @see MutablePropertySources + * @see PropertySourcesPropertyResolver + * @see org.springframework.context.ApplicationContextInitializer + */ + protected void customizePropertySources(MutablePropertySources propertySources) { + } + + /** + * Return the set of reserved default profile names. This implementation returns + * {@value #RESERVED_DEFAULT_PROFILE_NAME}. Subclasses may override in order to + * customize the set of reserved names. + * @see #RESERVED_DEFAULT_PROFILE_NAME + * @see #doGetDefaultProfiles() + */ + protected Set getReservedDefaultProfiles() { + return Collections.singleton(RESERVED_DEFAULT_PROFILE_NAME); + } + + + //--------------------------------------------------------------------- + // Implementation of ConfigurableEnvironment interface + //--------------------------------------------------------------------- + + @Override + public String[] getActiveProfiles() { + return StringUtils.toStringArray(doGetActiveProfiles()); + } + + /** + * Return the set of active profiles as explicitly set through + * {@link #setActiveProfiles} or if the current set of active profiles + * is empty, check for the presence of the {@value #ACTIVE_PROFILES_PROPERTY_NAME} + * property and assign its value to the set of active profiles. + * @see #getActiveProfiles() + * @see #ACTIVE_PROFILES_PROPERTY_NAME + */ + protected Set doGetActiveProfiles() { + synchronized (this.activeProfiles) { + if (this.activeProfiles.isEmpty()) { + String profiles = getProperty(ACTIVE_PROFILES_PROPERTY_NAME); + if (StringUtils.hasText(profiles)) { + setActiveProfiles(StringUtils.commaDelimitedListToStringArray( + StringUtils.trimAllWhitespace(profiles))); + } + } + return this.activeProfiles; + } + } + + @Override + public void setActiveProfiles(String... profiles) { + Assert.notNull(profiles, "Profile array must not be null"); + if (logger.isDebugEnabled()) { + logger.debug("Activating profiles " + Arrays.asList(profiles)); + } + synchronized (this.activeProfiles) { + this.activeProfiles.clear(); + for (String profile : profiles) { + validateProfile(profile); + this.activeProfiles.add(profile); + } + } + } + + @Override + public void addActiveProfile(String profile) { + if (logger.isDebugEnabled()) { + logger.debug("Activating profile '" + profile + "'"); + } + validateProfile(profile); + doGetActiveProfiles(); + synchronized (this.activeProfiles) { + this.activeProfiles.add(profile); + } + } + + + @Override + public String[] getDefaultProfiles() { + return StringUtils.toStringArray(doGetDefaultProfiles()); + } + + /** + * Return the set of default profiles explicitly set via + * {@link #setDefaultProfiles(String...)} or if the current set of default profiles + * consists only of {@linkplain #getReservedDefaultProfiles() reserved default + * profiles}, then check for the presence of the + * {@value #DEFAULT_PROFILES_PROPERTY_NAME} property and assign its value (if any) + * to the set of default profiles. + * @see #AbstractEnvironment() + * @see #getDefaultProfiles() + * @see #DEFAULT_PROFILES_PROPERTY_NAME + * @see #getReservedDefaultProfiles() + */ + protected Set doGetDefaultProfiles() { + synchronized (this.defaultProfiles) { + if (this.defaultProfiles.equals(getReservedDefaultProfiles())) { + String profiles = getProperty(DEFAULT_PROFILES_PROPERTY_NAME); + if (StringUtils.hasText(profiles)) { + setDefaultProfiles(StringUtils.commaDelimitedListToStringArray( + StringUtils.trimAllWhitespace(profiles))); + } + } + return this.defaultProfiles; + } + } + + /** + * Specify the set of profiles to be made active by default if no other profiles + * are explicitly made active through {@link #setActiveProfiles}. + *

    Calling this method removes overrides any reserved default profiles + * that may have been added during construction of the environment. + * @see #AbstractEnvironment() + * @see #getReservedDefaultProfiles() + */ + @Override + public void setDefaultProfiles(String... profiles) { + Assert.notNull(profiles, "Profile array must not be null"); + synchronized (this.defaultProfiles) { + this.defaultProfiles.clear(); + for (String profile : profiles) { + validateProfile(profile); + this.defaultProfiles.add(profile); + } + } + } + + @Override + @Deprecated + public boolean acceptsProfiles(String... profiles) { + Assert.notEmpty(profiles, "Must specify at least one profile"); + for (String profile : profiles) { + if (StringUtils.hasLength(profile) && profile.charAt(0) == '!') { + if (!isProfileActive(profile.substring(1))) { + return true; + } + } + else if (isProfileActive(profile)) { + return true; + } + } + return false; + } + + @Override + public boolean acceptsProfiles(Profiles profiles) { + Assert.notNull(profiles, "Profiles must not be null"); + return profiles.matches(this::isProfileActive); + } + + /** + * Return whether the given profile is active, or if active profiles are empty + * whether the profile should be active by default. + * @throws IllegalArgumentException per {@link #validateProfile(String)} + */ + protected boolean isProfileActive(String profile) { + validateProfile(profile); + Set currentActiveProfiles = doGetActiveProfiles(); + return (currentActiveProfiles.contains(profile) || + (currentActiveProfiles.isEmpty() && doGetDefaultProfiles().contains(profile))); + } + + /** + * Validate the given profile, called internally prior to adding to the set of + * active or default profiles. + *

    Subclasses may override to impose further restrictions on profile syntax. + * @throws IllegalArgumentException if the profile is null, empty, whitespace-only or + * begins with the profile NOT operator (!). + * @see #acceptsProfiles + * @see #addActiveProfile + * @see #setDefaultProfiles + */ + protected void validateProfile(String profile) { + if (!StringUtils.hasText(profile)) { + throw new IllegalArgumentException("Invalid profile [" + profile + "]: must contain text"); + } + if (profile.charAt(0) == '!') { + throw new IllegalArgumentException("Invalid profile [" + profile + "]: must not begin with ! operator"); + } + } + + @Override + public MutablePropertySources getPropertySources() { + return this.propertySources; + } + + @Override + @SuppressWarnings({"rawtypes", "unchecked"}) + public Map getSystemProperties() { + try { + return (Map) System.getProperties(); + } + catch (AccessControlException ex) { + return (Map) new ReadOnlySystemAttributesMap() { + @Override + @Nullable + protected String getSystemAttribute(String attributeName) { + try { + return System.getProperty(attributeName); + } + catch (AccessControlException ex) { + if (logger.isInfoEnabled()) { + logger.info("Caught AccessControlException when accessing system property '" + + attributeName + "'; its value will be returned [null]. Reason: " + ex.getMessage()); + } + return null; + } + } + }; + } + } + + @Override + @SuppressWarnings({"rawtypes", "unchecked"}) + public Map getSystemEnvironment() { + if (suppressGetenvAccess()) { + return Collections.emptyMap(); + } + try { + return (Map) System.getenv(); + } + catch (AccessControlException ex) { + return (Map) new ReadOnlySystemAttributesMap() { + @Override + @Nullable + protected String getSystemAttribute(String attributeName) { + try { + return System.getenv(attributeName); + } + catch (AccessControlException ex) { + if (logger.isInfoEnabled()) { + logger.info("Caught AccessControlException when accessing system environment variable '" + + attributeName + "'; its value will be returned [null]. Reason: " + ex.getMessage()); + } + return null; + } + } + }; + } + } + + /** + * Determine whether to suppress {@link System#getenv()}/{@link System#getenv(String)} + * access for the purposes of {@link #getSystemEnvironment()}. + *

    If this method returns {@code true}, an empty dummy Map will be used instead + * of the regular system environment Map, never even trying to call {@code getenv} + * and therefore avoiding security manager warnings (if any). + *

    The default implementation checks for the "spring.getenv.ignore" system property, + * returning {@code true} if its value equals "true" in any case. + * @see #IGNORE_GETENV_PROPERTY_NAME + * @see SpringProperties#getFlag + */ + protected boolean suppressGetenvAccess() { + return SpringProperties.getFlag(IGNORE_GETENV_PROPERTY_NAME); + } + + @Override + public void merge(ConfigurableEnvironment parent) { + for (PropertySource ps : parent.getPropertySources()) { + if (!this.propertySources.contains(ps.getName())) { + this.propertySources.addLast(ps); + } + } + String[] parentActiveProfiles = parent.getActiveProfiles(); + if (!ObjectUtils.isEmpty(parentActiveProfiles)) { + synchronized (this.activeProfiles) { + Collections.addAll(this.activeProfiles, parentActiveProfiles); + } + } + String[] parentDefaultProfiles = parent.getDefaultProfiles(); + if (!ObjectUtils.isEmpty(parentDefaultProfiles)) { + synchronized (this.defaultProfiles) { + this.defaultProfiles.remove(RESERVED_DEFAULT_PROFILE_NAME); + Collections.addAll(this.defaultProfiles, parentDefaultProfiles); + } + } + } + + + //--------------------------------------------------------------------- + // Implementation of ConfigurablePropertyResolver interface + //--------------------------------------------------------------------- + + @Override + public ConfigurableConversionService getConversionService() { + return this.propertyResolver.getConversionService(); + } + + @Override + public void setConversionService(ConfigurableConversionService conversionService) { + this.propertyResolver.setConversionService(conversionService); + } + + @Override + public void setPlaceholderPrefix(String placeholderPrefix) { + this.propertyResolver.setPlaceholderPrefix(placeholderPrefix); + } + + @Override + public void setPlaceholderSuffix(String placeholderSuffix) { + this.propertyResolver.setPlaceholderSuffix(placeholderSuffix); + } + + @Override + public void setValueSeparator(@Nullable String valueSeparator) { + this.propertyResolver.setValueSeparator(valueSeparator); + } + + @Override + public void setIgnoreUnresolvableNestedPlaceholders(boolean ignoreUnresolvableNestedPlaceholders) { + this.propertyResolver.setIgnoreUnresolvableNestedPlaceholders(ignoreUnresolvableNestedPlaceholders); + } + + @Override + public void setRequiredProperties(String... requiredProperties) { + this.propertyResolver.setRequiredProperties(requiredProperties); + } + + @Override + public void validateRequiredProperties() throws MissingRequiredPropertiesException { + this.propertyResolver.validateRequiredProperties(); + } + + + //--------------------------------------------------------------------- + // Implementation of PropertyResolver interface + //--------------------------------------------------------------------- + + @Override + public boolean containsProperty(String key) { + return this.propertyResolver.containsProperty(key); + } + + @Override + @Nullable + public String getProperty(String key) { + return this.propertyResolver.getProperty(key); + } + + @Override + public String getProperty(String key, String defaultValue) { + return this.propertyResolver.getProperty(key, defaultValue); + } + + @Override + @Nullable + public T getProperty(String key, Class targetType) { + return this.propertyResolver.getProperty(key, targetType); + } + + @Override + public T getProperty(String key, Class targetType, T defaultValue) { + return this.propertyResolver.getProperty(key, targetType, defaultValue); + } + + @Override + public String getRequiredProperty(String key) throws IllegalStateException { + return this.propertyResolver.getRequiredProperty(key); + } + + @Override + public T getRequiredProperty(String key, Class targetType) throws IllegalStateException { + return this.propertyResolver.getRequiredProperty(key, targetType); + } + + @Override + public String resolvePlaceholders(String text) { + return this.propertyResolver.resolvePlaceholders(text); + } + + @Override + public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException { + return this.propertyResolver.resolveRequiredPlaceholders(text); + } + + + @Override + public String toString() { + return getClass().getSimpleName() + " {activeProfiles=" + this.activeProfiles + + ", defaultProfiles=" + this.defaultProfiles + ", propertySources=" + this.propertySources + "}"; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java b/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java new file mode 100644 index 0000000..c3f29e1 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java @@ -0,0 +1,278 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.ConfigurableConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.PropertyPlaceholderHelper; +import org.springframework.util.SystemPropertyUtils; + +/** + * Abstract base class for resolving properties against any underlying source. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + */ +public abstract class AbstractPropertyResolver implements ConfigurablePropertyResolver { + + protected final Log logger = LogFactory.getLog(getClass()); + + @Nullable + private volatile ConfigurableConversionService conversionService; + + @Nullable + private PropertyPlaceholderHelper nonStrictHelper; + + @Nullable + private PropertyPlaceholderHelper strictHelper; + + private boolean ignoreUnresolvableNestedPlaceholders = false; + + private String placeholderPrefix = SystemPropertyUtils.PLACEHOLDER_PREFIX; + + private String placeholderSuffix = SystemPropertyUtils.PLACEHOLDER_SUFFIX; + + @Nullable + private String valueSeparator = SystemPropertyUtils.VALUE_SEPARATOR; + + private final Set requiredProperties = new LinkedHashSet<>(); + + + @Override + public ConfigurableConversionService getConversionService() { + // Need to provide an independent DefaultConversionService, not the + // shared DefaultConversionService used by PropertySourcesPropertyResolver. + ConfigurableConversionService cs = this.conversionService; + if (cs == null) { + synchronized (this) { + cs = this.conversionService; + if (cs == null) { + cs = new DefaultConversionService(); + this.conversionService = cs; + } + } + } + return cs; + } + + @Override + public void setConversionService(ConfigurableConversionService conversionService) { + Assert.notNull(conversionService, "ConversionService must not be null"); + this.conversionService = conversionService; + } + + /** + * Set the prefix that placeholders replaced by this resolver must begin with. + *

    The default is "${". + * @see org.springframework.util.SystemPropertyUtils#PLACEHOLDER_PREFIX + */ + @Override + public void setPlaceholderPrefix(String placeholderPrefix) { + Assert.notNull(placeholderPrefix, "'placeholderPrefix' must not be null"); + this.placeholderPrefix = placeholderPrefix; + } + + /** + * Set the suffix that placeholders replaced by this resolver must end with. + *

    The default is "}". + * @see org.springframework.util.SystemPropertyUtils#PLACEHOLDER_SUFFIX + */ + @Override + public void setPlaceholderSuffix(String placeholderSuffix) { + Assert.notNull(placeholderSuffix, "'placeholderSuffix' must not be null"); + this.placeholderSuffix = placeholderSuffix; + } + + /** + * Specify the separating character between the placeholders replaced by this + * resolver and their associated default value, or {@code null} if no such + * special character should be processed as a value separator. + *

    The default is ":". + * @see org.springframework.util.SystemPropertyUtils#VALUE_SEPARATOR + */ + @Override + public void setValueSeparator(@Nullable String valueSeparator) { + this.valueSeparator = valueSeparator; + } + + /** + * Set whether to throw an exception when encountering an unresolvable placeholder + * nested within the value of a given property. A {@code false} value indicates strict + * resolution, i.e. that an exception will be thrown. A {@code true} value indicates + * that unresolvable nested placeholders should be passed through in their unresolved + * ${...} form. + *

    The default is {@code false}. + * @since 3.2 + */ + @Override + public void setIgnoreUnresolvableNestedPlaceholders(boolean ignoreUnresolvableNestedPlaceholders) { + this.ignoreUnresolvableNestedPlaceholders = ignoreUnresolvableNestedPlaceholders; + } + + @Override + public void setRequiredProperties(String... requiredProperties) { + Collections.addAll(this.requiredProperties, requiredProperties); + } + + @Override + public void validateRequiredProperties() { + MissingRequiredPropertiesException ex = new MissingRequiredPropertiesException(); + for (String key : this.requiredProperties) { + if (this.getProperty(key) == null) { + ex.addMissingRequiredProperty(key); + } + } + if (!ex.getMissingRequiredProperties().isEmpty()) { + throw ex; + } + } + + @Override + public boolean containsProperty(String key) { + return (getProperty(key) != null); + } + + @Override + @Nullable + public String getProperty(String key) { + return getProperty(key, String.class); + } + + @Override + public String getProperty(String key, String defaultValue) { + String value = getProperty(key); + return (value != null ? value : defaultValue); + } + + @Override + public T getProperty(String key, Class targetType, T defaultValue) { + T value = getProperty(key, targetType); + return (value != null ? value : defaultValue); + } + + @Override + public String getRequiredProperty(String key) throws IllegalStateException { + String value = getProperty(key); + if (value == null) { + throw new IllegalStateException("Required key '" + key + "' not found"); + } + return value; + } + + @Override + public T getRequiredProperty(String key, Class valueType) throws IllegalStateException { + T value = getProperty(key, valueType); + if (value == null) { + throw new IllegalStateException("Required key '" + key + "' not found"); + } + return value; + } + + @Override + public String resolvePlaceholders(String text) { + if (this.nonStrictHelper == null) { + this.nonStrictHelper = createPlaceholderHelper(true); + } + return doResolvePlaceholders(text, this.nonStrictHelper); + } + + @Override + public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException { + if (this.strictHelper == null) { + this.strictHelper = createPlaceholderHelper(false); + } + return doResolvePlaceholders(text, this.strictHelper); + } + + /** + * Resolve placeholders within the given string, deferring to the value of + * {@link #setIgnoreUnresolvableNestedPlaceholders} to determine whether any + * unresolvable placeholders should raise an exception or be ignored. + *

    Invoked from {@link #getProperty} and its variants, implicitly resolving + * nested placeholders. In contrast, {@link #resolvePlaceholders} and + * {@link #resolveRequiredPlaceholders} do not delegate + * to this method but rather perform their own handling of unresolvable + * placeholders, as specified by each of those methods. + * @since 3.2 + * @see #setIgnoreUnresolvableNestedPlaceholders + */ + protected String resolveNestedPlaceholders(String value) { + if (value.isEmpty()) { + return value; + } + return (this.ignoreUnresolvableNestedPlaceholders ? + resolvePlaceholders(value) : resolveRequiredPlaceholders(value)); + } + + private PropertyPlaceholderHelper createPlaceholderHelper(boolean ignoreUnresolvablePlaceholders) { + return new PropertyPlaceholderHelper(this.placeholderPrefix, this.placeholderSuffix, + this.valueSeparator, ignoreUnresolvablePlaceholders); + } + + private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) { + return helper.replacePlaceholders(text, this::getPropertyAsRawString); + } + + /** + * Convert the given value to the specified target type, if necessary. + * @param value the original property value + * @param targetType the specified target type for property retrieval + * @return the converted value, or the original value if no conversion + * is necessary + * @since 4.3.5 + */ + @SuppressWarnings("unchecked") + @Nullable + protected T convertValueIfNecessary(Object value, @Nullable Class targetType) { + if (targetType == null) { + return (T) value; + } + ConversionService conversionServiceToUse = this.conversionService; + if (conversionServiceToUse == null) { + // Avoid initialization of shared DefaultConversionService if + // no standard type conversion is needed in the first place... + if (ClassUtils.isAssignableValue(targetType, value)) { + return (T) value; + } + conversionServiceToUse = DefaultConversionService.getSharedInstance(); + } + return conversionServiceToUse.convert(value, targetType); + } + + + /** + * Retrieve the specified property as a raw String, + * i.e. without resolution of nested placeholders. + * @param key the property name to resolve + * @return the property value or {@code null} if none found + */ + @Nullable + protected abstract String getPropertyAsRawString(String key); + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/CommandLineArgs.java b/spring-core/src/main/java/org/springframework/core/env/CommandLineArgs.java new file mode 100644 index 0000000..9e24676 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/CommandLineArgs.java @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.lang.Nullable; + +/** + * A simple representation of command line arguments, broken into "option arguments" and + * "non-option arguments". + * + * @author Chris Beams + * @since 3.1 + * @see SimpleCommandLineArgsParser + */ +class CommandLineArgs { + + private final Map> optionArgs = new HashMap<>(); + private final List nonOptionArgs = new ArrayList<>(); + + /** + * Add an option argument for the given option name and add the given value to the + * list of values associated with this option (of which there may be zero or more). + * The given value may be {@code null}, indicating that the option was specified + * without an associated value (e.g. "--foo" vs. "--foo=bar"). + */ + public void addOptionArg(String optionName, @Nullable String optionValue) { + if (!this.optionArgs.containsKey(optionName)) { + this.optionArgs.put(optionName, new ArrayList<>()); + } + if (optionValue != null) { + this.optionArgs.get(optionName).add(optionValue); + } + } + + /** + * Return the set of all option arguments present on the command line. + */ + public Set getOptionNames() { + return Collections.unmodifiableSet(this.optionArgs.keySet()); + } + + /** + * Return whether the option with the given name was present on the command line. + */ + public boolean containsOption(String optionName) { + return this.optionArgs.containsKey(optionName); + } + + /** + * Return the list of values associated with the given option. {@code null} signifies + * that the option was not present; empty list signifies that no values were associated + * with this option. + */ + @Nullable + public List getOptionValues(String optionName) { + return this.optionArgs.get(optionName); + } + + /** + * Add the given value to the list of non-option arguments. + */ + public void addNonOptionArg(String value) { + this.nonOptionArgs.add(value); + } + + /** + * Return the list of non-option arguments specified on the command line. + */ + public List getNonOptionArgs() { + return Collections.unmodifiableList(this.nonOptionArgs); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/CommandLinePropertySource.java b/spring-core/src/main/java/org/springframework/core/env/CommandLinePropertySource.java new file mode 100644 index 0000000..3216299 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/CommandLinePropertySource.java @@ -0,0 +1,320 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import java.util.Collection; +import java.util.List; + +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Abstract base class for {@link PropertySource} implementations backed by command line + * arguments. The parameterized type {@code T} represents the underlying source of command + * line options. This may be as simple as a String array in the case of + * {@link SimpleCommandLinePropertySource}, or specific to a particular API such as JOpt's + * {@code OptionSet} in the case of {@link JOptCommandLinePropertySource}. + * + *

    Purpose and General Usage

    + * + * For use in standalone Spring-based applications, i.e. those that are bootstrapped via + * a traditional {@code main} method accepting a {@code String[]} of arguments from the + * command line. In many cases, processing command-line arguments directly within the + * {@code main} method may be sufficient, but in other cases, it may be desirable to + * inject arguments as values into Spring beans. It is this latter set of cases in which + * a {@code CommandLinePropertySource} becomes useful. A {@code CommandLinePropertySource} + * will typically be added to the {@link Environment} of the Spring + * {@code ApplicationContext}, at which point all command line arguments become available + * through the {@link Environment#getProperty(String)} family of methods. For example: + * + *
    + * public static void main(String[] args) {
    + *     CommandLinePropertySource clps = ...;
    + *     AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    + *     ctx.getEnvironment().getPropertySources().addFirst(clps);
    + *     ctx.register(AppConfig.class);
    + *     ctx.refresh();
    + * }
    + * + * With the bootstrap logic above, the {@code AppConfig} class may {@code @Inject} the + * Spring {@code Environment} and query it directly for properties: + * + *
    + * @Configuration
    + * public class AppConfig {
    + *
    + *     @Inject Environment env;
    + *
    + *     @Bean
    + *     public void DataSource dataSource() {
    + *         MyVendorDataSource dataSource = new MyVendorDataSource();
    + *         dataSource.setHostname(env.getProperty("db.hostname", "localhost"));
    + *         dataSource.setUsername(env.getRequiredProperty("db.username"));
    + *         dataSource.setPassword(env.getRequiredProperty("db.password"));
    + *         // ...
    + *         return dataSource;
    + *     }
    + * }
    + * + * Because the {@code CommandLinePropertySource} was added to the {@code Environment}'s + * set of {@link MutablePropertySources} using the {@code #addFirst} method, it has + * highest search precedence, meaning that while "db.hostname" and other properties may + * exist in other property sources such as the system environment variables, it will be + * chosen from the command line property source first. This is a reasonable approach + * given that arguments specified on the command line are naturally more specific than + * those specified as environment variables. + * + *

    As an alternative to injecting the {@code Environment}, Spring's {@code @Value} + * annotation may be used to inject these properties, given that a {@link + * PropertySourcesPropertyResolver} bean has been registered, either directly or through + * using the {@code } element. For example: + * + *

    + * @Component
    + * public class MyComponent {
    + *
    + *     @Value("my.property:defaultVal")
    + *     private String myProperty;
    + *
    + *     public void getMyProperty() {
    + *         return this.myProperty;
    + *     }
    + *
    + *     // ...
    + * }
    + * + *

    Working with option arguments

    + * + *

    Individual command line arguments are represented as properties through the usual + * {@link PropertySource#getProperty(String)} and + * {@link PropertySource#containsProperty(String)} methods. For example, given the + * following command line: + * + *

    --o1=v1 --o2
    + * + * 'o1' and 'o2' are treated as "option arguments", and the following assertions would + * evaluate true: + * + *
    + * CommandLinePropertySource ps = ...
    + * assert ps.containsProperty("o1") == true;
    + * assert ps.containsProperty("o2") == true;
    + * assert ps.containsProperty("o3") == false;
    + * assert ps.getProperty("o1").equals("v1");
    + * assert ps.getProperty("o2").equals("");
    + * assert ps.getProperty("o3") == null;
    + * 
    + * + * Note that the 'o2' option has no argument, but {@code getProperty("o2")} resolves to + * empty string ({@code ""}) as opposed to {@code null}, while {@code getProperty("o3")} + * resolves to {@code null} because it was not specified. This behavior is consistent with + * the general contract to be followed by all {@code PropertySource} implementations. + * + *

    Note also that while "--" was used in the examples above to denote an option + * argument, this syntax may vary across individual command line argument libraries. For + * example, a JOpt- or Commons CLI-based implementation may allow for single dash ("-") + * "short" option arguments, etc. + * + *

    Working with non-option arguments

    + * + *

    Non-option arguments are also supported through this abstraction. Any arguments + * supplied without an option-style prefix such as "-" or "--" are considered "non-option + * arguments" and available through the special {@linkplain + * #DEFAULT_NON_OPTION_ARGS_PROPERTY_NAME "nonOptionArgs"} property. If multiple + * non-option arguments are specified, the value of this property will be a + * comma-delimited string containing all of the arguments. This approach ensures a simple + * and consistent return type (String) for all properties from a {@code + * CommandLinePropertySource} and at the same time lends itself to conversion when used + * in conjunction with the Spring {@link Environment} and its built-in {@code + * ConversionService}. Consider the following example: + * + *

    --o1=v1 --o2=v2 /path/to/file1 /path/to/file2
    + * + * In this example, "o1" and "o2" would be considered "option arguments", while the two + * filesystem paths qualify as "non-option arguments". As such, the following assertions + * will evaluate true: + * + *
    + * CommandLinePropertySource ps = ...
    + * assert ps.containsProperty("o1") == true;
    + * assert ps.containsProperty("o2") == true;
    + * assert ps.containsProperty("nonOptionArgs") == true;
    + * assert ps.getProperty("o1").equals("v1");
    + * assert ps.getProperty("o2").equals("v2");
    + * assert ps.getProperty("nonOptionArgs").equals("/path/to/file1,/path/to/file2");
    + * 
    + * + *

    As mentioned above, when used in conjunction with the Spring {@code Environment} + * abstraction, this comma-delimited string may easily be converted to a String array or + * list: + * + *

    + * Environment env = applicationContext.getEnvironment();
    + * String[] nonOptionArgs = env.getProperty("nonOptionArgs", String[].class);
    + * assert nonOptionArgs[0].equals("/path/to/file1");
    + * assert nonOptionArgs[1].equals("/path/to/file2");
    + * 
    + * + *

    The name of the special "non-option arguments" property may be customized through + * the {@link #setNonOptionArgsPropertyName(String)} method. Doing so is recommended as + * it gives proper semantic value to non-option arguments. For example, if filesystem + * paths are being specified as non-option arguments, it is likely preferable to refer to + * these as something like "file.locations" than the default of "nonOptionArgs": + * + *

    + * public static void main(String[] args) {
    + *     CommandLinePropertySource clps = ...;
    + *     clps.setNonOptionArgsPropertyName("file.locations");
    + *
    + *     AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    + *     ctx.getEnvironment().getPropertySources().addFirst(clps);
    + *     ctx.register(AppConfig.class);
    + *     ctx.refresh();
    + * }
    + * + *

    Limitations

    + * + * This abstraction is not intended to expose the full power of underlying command line + * parsing APIs such as JOpt or Commons CLI. It's intent is rather just the opposite: to + * provide the simplest possible abstraction for accessing command line arguments + * after they have been parsed. So the typical case will involve fully configuring + * the underlying command line parsing API, parsing the {@code String[]} of arguments + * coming into the main method, and then simply providing the parsing results to an + * implementation of {@code CommandLinePropertySource}. At that point, all arguments can + * be considered either 'option' or 'non-option' arguments and as described above can be + * accessed through the normal {@code PropertySource} and {@code Environment} APIs. + * + * @author Chris Beams + * @since 3.1 + * @param the source type + * @see PropertySource + * @see SimpleCommandLinePropertySource + * @see JOptCommandLinePropertySource + */ +public abstract class CommandLinePropertySource extends EnumerablePropertySource { + + /** The default name given to {@link CommandLinePropertySource} instances: {@value}. */ + public static final String COMMAND_LINE_PROPERTY_SOURCE_NAME = "commandLineArgs"; + + /** The default name of the property representing non-option arguments: {@value}. */ + public static final String DEFAULT_NON_OPTION_ARGS_PROPERTY_NAME = "nonOptionArgs"; + + + private String nonOptionArgsPropertyName = DEFAULT_NON_OPTION_ARGS_PROPERTY_NAME; + + + /** + * Create a new {@code CommandLinePropertySource} having the default name + * {@value #COMMAND_LINE_PROPERTY_SOURCE_NAME} and backed by the given source object. + */ + public CommandLinePropertySource(T source) { + super(COMMAND_LINE_PROPERTY_SOURCE_NAME, source); + } + + /** + * Create a new {@link CommandLinePropertySource} having the given name + * and backed by the given source object. + */ + public CommandLinePropertySource(String name, T source) { + super(name, source); + } + + + /** + * Specify the name of the special "non-option arguments" property. + * The default is {@value #DEFAULT_NON_OPTION_ARGS_PROPERTY_NAME}. + */ + public void setNonOptionArgsPropertyName(String nonOptionArgsPropertyName) { + this.nonOptionArgsPropertyName = nonOptionArgsPropertyName; + } + + /** + * This implementation first checks to see if the name specified is the special + * {@linkplain #setNonOptionArgsPropertyName(String) "non-option arguments" property}, + * and if so delegates to the abstract {@link #getNonOptionArgs()} method + * checking to see whether it returns an empty collection. Otherwise delegates to and + * returns the value of the abstract {@link #containsOption(String)} method. + */ + @Override + public final boolean containsProperty(String name) { + if (this.nonOptionArgsPropertyName.equals(name)) { + return !this.getNonOptionArgs().isEmpty(); + } + return this.containsOption(name); + } + + /** + * This implementation first checks to see if the name specified is the special + * {@linkplain #setNonOptionArgsPropertyName(String) "non-option arguments" property}, + * and if so delegates to the abstract {@link #getNonOptionArgs()} method. If so + * and the collection of non-option arguments is empty, this method returns {@code + * null}. If not empty, it returns a comma-separated String of all non-option + * arguments. Otherwise delegates to and returns the result of the abstract {@link + * #getOptionValues(String)} method. + */ + @Override + @Nullable + public final String getProperty(String name) { + if (this.nonOptionArgsPropertyName.equals(name)) { + Collection nonOptionArguments = this.getNonOptionArgs(); + if (nonOptionArguments.isEmpty()) { + return null; + } + else { + return StringUtils.collectionToCommaDelimitedString(nonOptionArguments); + } + } + Collection optionValues = this.getOptionValues(name); + if (optionValues == null) { + return null; + } + else { + return StringUtils.collectionToCommaDelimitedString(optionValues); + } + } + + + /** + * Return whether the set of option arguments parsed from the command line contains + * an option with the given name. + */ + protected abstract boolean containsOption(String name); + + /** + * Return the collection of values associated with the command line option having the + * given name. + *
      + *
    • if the option is present and has no argument (e.g.: "--foo"), return an empty + * collection ({@code []})
    • + *
    • if the option is present and has a single value (e.g. "--foo=bar"), return a + * collection having one element ({@code ["bar"]})
    • + *
    • if the option is present and the underlying command line parsing library + * supports multiple arguments (e.g. "--foo=bar --foo=baz"), return a collection + * having elements for each value ({@code ["bar", "baz"]})
    • + *
    • if the option is not present, return {@code null}
    • + *
    + */ + @Nullable + protected abstract List getOptionValues(String name); + + /** + * Return the collection of non-option arguments parsed from the command line. + * Never {@code null}. + */ + protected abstract List getNonOptionArgs(); + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/CompositePropertySource.java b/spring-core/src/main/java/org/springframework/core/env/CompositePropertySource.java new file mode 100644 index 0000000..ef6b764 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/CompositePropertySource.java @@ -0,0 +1,127 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Composite {@link PropertySource} implementation that iterates over a set of + * {@link PropertySource} instances. Necessary in cases where multiple property sources + * share the same name, e.g. when multiple values are supplied to {@code @PropertySource}. + * + *

    As of Spring 4.1.2, this class extends {@link EnumerablePropertySource} instead + * of plain {@link PropertySource}, exposing {@link #getPropertyNames()} based on the + * accumulated property names from all contained sources (as far as possible). + * + * @author Chris Beams + * @author Juergen Hoeller + * @author Phillip Webb + * @since 3.1.1 + */ +public class CompositePropertySource extends EnumerablePropertySource { + + private final Set> propertySources = new LinkedHashSet<>(); + + + /** + * Create a new {@code CompositePropertySource}. + * @param name the name of the property source + */ + public CompositePropertySource(String name) { + super(name); + } + + + @Override + @Nullable + public Object getProperty(String name) { + for (PropertySource propertySource : this.propertySources) { + Object candidate = propertySource.getProperty(name); + if (candidate != null) { + return candidate; + } + } + return null; + } + + @Override + public boolean containsProperty(String name) { + for (PropertySource propertySource : this.propertySources) { + if (propertySource.containsProperty(name)) { + return true; + } + } + return false; + } + + @Override + public String[] getPropertyNames() { + Set names = new LinkedHashSet<>(); + for (PropertySource propertySource : this.propertySources) { + if (!(propertySource instanceof EnumerablePropertySource)) { + throw new IllegalStateException( + "Failed to enumerate property names due to non-enumerable property source: " + propertySource); + } + names.addAll(Arrays.asList(((EnumerablePropertySource) propertySource).getPropertyNames())); + } + return StringUtils.toStringArray(names); + } + + + /** + * Add the given {@link PropertySource} to the end of the chain. + * @param propertySource the PropertySource to add + */ + public void addPropertySource(PropertySource propertySource) { + this.propertySources.add(propertySource); + } + + /** + * Add the given {@link PropertySource} to the start of the chain. + * @param propertySource the PropertySource to add + * @since 4.1 + */ + public void addFirstPropertySource(PropertySource propertySource) { + List> existing = new ArrayList<>(this.propertySources); + this.propertySources.clear(); + this.propertySources.add(propertySource); + this.propertySources.addAll(existing); + } + + /** + * Return all property sources that this composite source holds. + * @since 4.1.1 + */ + public Collection> getPropertySources() { + return this.propertySources; + } + + + @Override + public String toString() { + return getClass().getSimpleName() + " {name='" + this.name + "', propertySources=" + this.propertySources + "}"; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/ConfigurableEnvironment.java b/spring-core/src/main/java/org/springframework/core/env/ConfigurableEnvironment.java new file mode 100644 index 0000000..b174ca0 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/ConfigurableEnvironment.java @@ -0,0 +1,171 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import java.util.Map; + +/** + * Configuration interface to be implemented by most if not all {@link Environment} types. + * Provides facilities for setting active and default profiles and manipulating underlying + * property sources. Allows clients to set and validate required properties, customize the + * conversion service and more through the {@link ConfigurablePropertyResolver} + * superinterface. + * + *

    Manipulating property sources

    + *

    Property sources may be removed, reordered, or replaced; and additional + * property sources may be added using the {@link MutablePropertySources} + * instance returned from {@link #getPropertySources()}. The following examples + * are against the {@link StandardEnvironment} implementation of + * {@code ConfigurableEnvironment}, but are generally applicable to any implementation, + * though particular default property sources may differ. + * + *

    Example: adding a new property source with highest search priority

    + *
    + * ConfigurableEnvironment environment = new StandardEnvironment();
    + * MutablePropertySources propertySources = environment.getPropertySources();
    + * Map<String, String> myMap = new HashMap<>();
    + * myMap.put("xyz", "myValue");
    + * propertySources.addFirst(new MapPropertySource("MY_MAP", myMap));
    + * 
    + * + *

    Example: removing the default system properties property source

    + *
    + * MutablePropertySources propertySources = environment.getPropertySources();
    + * propertySources.remove(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME)
    + * 
    + * + *

    Example: mocking the system environment for testing purposes

    + *
    + * MutablePropertySources propertySources = environment.getPropertySources();
    + * MockPropertySource mockEnvVars = new MockPropertySource().withProperty("xyz", "myValue");
    + * propertySources.replace(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, mockEnvVars);
    + * 
    + * + * When an {@link Environment} is being used by an {@code ApplicationContext}, it is + * important that any such {@code PropertySource} manipulations be performed + * before the context's {@link + * org.springframework.context.support.AbstractApplicationContext#refresh() refresh()} + * method is called. This ensures that all property sources are available during the + * container bootstrap process, including use by {@linkplain + * org.springframework.context.support.PropertySourcesPlaceholderConfigurer property + * placeholder configurers}. + * + * @author Chris Beams + * @since 3.1 + * @see StandardEnvironment + * @see org.springframework.context.ConfigurableApplicationContext#getEnvironment + */ +public interface ConfigurableEnvironment extends Environment, ConfigurablePropertyResolver { + + /** + * Specify the set of profiles active for this {@code Environment}. Profiles are + * evaluated during container bootstrap to determine whether bean definitions + * should be registered with the container. + *

    Any existing active profiles will be replaced with the given arguments; call + * with zero arguments to clear the current set of active profiles. Use + * {@link #addActiveProfile} to add a profile while preserving the existing set. + * @throws IllegalArgumentException if any profile is null, empty or whitespace-only + * @see #addActiveProfile + * @see #setDefaultProfiles + * @see org.springframework.context.annotation.Profile + * @see AbstractEnvironment#ACTIVE_PROFILES_PROPERTY_NAME + */ + void setActiveProfiles(String... profiles); + + /** + * Add a profile to the current set of active profiles. + * @throws IllegalArgumentException if the profile is null, empty or whitespace-only + * @see #setActiveProfiles + */ + void addActiveProfile(String profile); + + /** + * Specify the set of profiles to be made active by default if no other profiles + * are explicitly made active through {@link #setActiveProfiles}. + * @throws IllegalArgumentException if any profile is null, empty or whitespace-only + * @see AbstractEnvironment#DEFAULT_PROFILES_PROPERTY_NAME + */ + void setDefaultProfiles(String... profiles); + + /** + * Return the {@link PropertySources} for this {@code Environment} in mutable form, + * allowing for manipulation of the set of {@link PropertySource} objects that should + * be searched when resolving properties against this {@code Environment} object. + * The various {@link MutablePropertySources} methods such as + * {@link MutablePropertySources#addFirst addFirst}, + * {@link MutablePropertySources#addLast addLast}, + * {@link MutablePropertySources#addBefore addBefore} and + * {@link MutablePropertySources#addAfter addAfter} allow for fine-grained control + * over property source ordering. This is useful, for example, in ensuring that + * certain user-defined property sources have search precedence over default property + * sources such as the set of system properties or the set of system environment + * variables. + * @see AbstractEnvironment#customizePropertySources + */ + MutablePropertySources getPropertySources(); + + /** + * Return the value of {@link System#getProperties()} if allowed by the current + * {@link SecurityManager}, otherwise return a map implementation that will attempt + * to access individual keys using calls to {@link System#getProperty(String)}. + *

    Note that most {@code Environment} implementations will include this system + * properties map as a default {@link PropertySource} to be searched. Therefore, it is + * recommended that this method not be used directly unless bypassing other property + * sources is expressly intended. + *

    Calls to {@link Map#get(Object)} on the Map returned will never throw + * {@link IllegalAccessException}; in cases where the SecurityManager forbids access + * to a property, {@code null} will be returned and an INFO-level log message will be + * issued noting the exception. + */ + Map getSystemProperties(); + + /** + * Return the value of {@link System#getenv()} if allowed by the current + * {@link SecurityManager}, otherwise return a map implementation that will attempt + * to access individual keys using calls to {@link System#getenv(String)}. + *

    Note that most {@link Environment} implementations will include this system + * environment map as a default {@link PropertySource} to be searched. Therefore, it + * is recommended that this method not be used directly unless bypassing other + * property sources is expressly intended. + *

    Calls to {@link Map#get(Object)} on the Map returned will never throw + * {@link IllegalAccessException}; in cases where the SecurityManager forbids access + * to a property, {@code null} will be returned and an INFO-level log message will be + * issued noting the exception. + */ + Map getSystemEnvironment(); + + /** + * Append the given parent environment's active profiles, default profiles and + * property sources to this (child) environment's respective collections of each. + *

    For any identically-named {@code PropertySource} instance existing in both + * parent and child, the child instance is to be preserved and the parent instance + * discarded. This has the effect of allowing overriding of property sources by the + * child as well as avoiding redundant searches through common property source types, + * e.g. system environment and system properties. + *

    Active and default profile names are also filtered for duplicates, to avoid + * confusion and redundant storage. + *

    The parent environment remains unmodified in any case. Note that any changes to + * the parent environment occurring after the call to {@code merge} will not be + * reflected in the child. Therefore, care should be taken to configure parent + * property sources and profile information prior to calling {@code merge}. + * @param parent the environment to merge with + * @since 3.1.2 + * @see org.springframework.context.support.AbstractApplicationContext#setParent + */ + void merge(ConfigurableEnvironment parent); + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/ConfigurablePropertyResolver.java b/spring-core/src/main/java/org/springframework/core/env/ConfigurablePropertyResolver.java new file mode 100644 index 0000000..bb2f9bc --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/ConfigurablePropertyResolver.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import org.springframework.core.convert.support.ConfigurableConversionService; +import org.springframework.lang.Nullable; + +/** + * Configuration interface to be implemented by most if not all {@link PropertyResolver} + * types. Provides facilities for accessing and customizing the + * {@link org.springframework.core.convert.ConversionService ConversionService} + * used when converting property values from one type to another. + * + * @author Chris Beams + * @since 3.1 + */ +public interface ConfigurablePropertyResolver extends PropertyResolver { + + /** + * Return the {@link ConfigurableConversionService} used when performing type + * conversions on properties. + *

    The configurable nature of the returned conversion service allows for + * the convenient addition and removal of individual {@code Converter} instances: + *

    +	 * ConfigurableConversionService cs = env.getConversionService();
    +	 * cs.addConverter(new FooConverter());
    +	 * 
    + * @see PropertyResolver#getProperty(String, Class) + * @see org.springframework.core.convert.converter.ConverterRegistry#addConverter + */ + ConfigurableConversionService getConversionService(); + + /** + * Set the {@link ConfigurableConversionService} to be used when performing type + * conversions on properties. + *

    Note: as an alternative to fully replacing the + * {@code ConversionService}, consider adding or removing individual + * {@code Converter} instances by drilling into {@link #getConversionService()} + * and calling methods such as {@code #addConverter}. + * @see PropertyResolver#getProperty(String, Class) + * @see #getConversionService() + * @see org.springframework.core.convert.converter.ConverterRegistry#addConverter + */ + void setConversionService(ConfigurableConversionService conversionService); + + /** + * Set the prefix that placeholders replaced by this resolver must begin with. + */ + void setPlaceholderPrefix(String placeholderPrefix); + + /** + * Set the suffix that placeholders replaced by this resolver must end with. + */ + void setPlaceholderSuffix(String placeholderSuffix); + + /** + * Specify the separating character between the placeholders replaced by this + * resolver and their associated default value, or {@code null} if no such + * special character should be processed as a value separator. + */ + void setValueSeparator(@Nullable String valueSeparator); + + /** + * Set whether to throw an exception when encountering an unresolvable placeholder + * nested within the value of a given property. A {@code false} value indicates strict + * resolution, i.e. that an exception will be thrown. A {@code true} value indicates + * that unresolvable nested placeholders should be passed through in their unresolved + * ${...} form. + *

    Implementations of {@link #getProperty(String)} and its variants must inspect + * the value set here to determine correct behavior when property values contain + * unresolvable placeholders. + * @since 3.2 + */ + void setIgnoreUnresolvableNestedPlaceholders(boolean ignoreUnresolvableNestedPlaceholders); + + /** + * Specify which properties must be present, to be verified by + * {@link #validateRequiredProperties()}. + */ + void setRequiredProperties(String... requiredProperties); + + /** + * Validate that each of the properties specified by + * {@link #setRequiredProperties} is present and resolves to a + * non-{@code null} value. + * @throws MissingRequiredPropertiesException if any of the required + * properties are not resolvable. + */ + void validateRequiredProperties() throws MissingRequiredPropertiesException; + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/EnumerablePropertySource.java b/spring-core/src/main/java/org/springframework/core/env/EnumerablePropertySource.java new file mode 100644 index 0000000..2c1386d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/EnumerablePropertySource.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import org.springframework.util.ObjectUtils; + +/** + * A {@link PropertySource} implementation capable of interrogating its + * underlying source object to enumerate all possible property name/value + * pairs. Exposes the {@link #getPropertyNames()} method to allow callers + * to introspect available properties without having to access the underlying + * source object. This also facilitates a more efficient implementation of + * {@link #containsProperty(String)}, in that it can call {@link #getPropertyNames()} + * and iterate through the returned array rather than attempting a call to + * {@link #getProperty(String)} which may be more expensive. Implementations may + * consider caching the result of {@link #getPropertyNames()} to fully exploit this + * performance opportunity. + * + *

    Most framework-provided {@code PropertySource} implementations are enumerable; + * a counter-example would be {@code JndiPropertySource} where, due to the + * nature of JNDI it is not possible to determine all possible property names at + * any given time; rather it is only possible to try to access a property + * (via {@link #getProperty(String)}) in order to evaluate whether it is present + * or not. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + * @param the source type + */ +public abstract class EnumerablePropertySource extends PropertySource { + + /** + * Create a new {@code EnumerablePropertySource} with the given name and source object. + * @param name the associated name + * @param source the source object + */ + public EnumerablePropertySource(String name, T source) { + super(name, source); + } + + /** + * Create a new {@code EnumerablePropertySource} with the given name and with a new + * {@code Object} instance as the underlying source. + * @param name the associated name + */ + protected EnumerablePropertySource(String name) { + super(name); + } + + + /** + * Return whether this {@code PropertySource} contains a property with the given name. + *

    This implementation checks for the presence of the given name within the + * {@link #getPropertyNames()} array. + * @param name the name of the property to find + */ + @Override + public boolean containsProperty(String name) { + return ObjectUtils.containsElement(getPropertyNames(), name); + } + + /** + * Return the names of all properties contained by the + * {@linkplain #getSource() source} object (never {@code null}). + */ + public abstract String[] getPropertyNames(); + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/Environment.java b/spring-core/src/main/java/org/springframework/core/env/Environment.java new file mode 100644 index 0000000..37b52fb --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/Environment.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +/** + * Interface representing the environment in which the current application is running. + * Models two key aspects of the application environment: profiles and + * properties. Methods related to property access are exposed via the + * {@link PropertyResolver} superinterface. + * + *

    A profile is a named, logical group of bean definitions to be registered + * with the container only if the given profile is active. Beans may be assigned + * to a profile whether defined in XML or via annotations; see the spring-beans 3.1 schema + * or the {@link org.springframework.context.annotation.Profile @Profile} annotation for + * syntax details. The role of the {@code Environment} object with relation to profiles is + * in determining which profiles (if any) are currently {@linkplain #getActiveProfiles + * active}, and which profiles (if any) should be {@linkplain #getDefaultProfiles active + * by default}. + * + *

    Properties play an important role in almost all applications, and may + * originate from a variety of sources: properties files, JVM system properties, system + * environment variables, JNDI, servlet context parameters, ad-hoc Properties objects, + * Maps, and so on. The role of the environment object with relation to properties is to + * provide the user with a convenient service interface for configuring property sources + * and resolving properties from them. + * + *

    Beans managed within an {@code ApplicationContext} may register to be {@link + * org.springframework.context.EnvironmentAware EnvironmentAware} or {@code @Inject} the + * {@code Environment} in order to query profile state or resolve properties directly. + * + *

    In most cases, however, application-level beans should not need to interact with the + * {@code Environment} directly but instead may have to have {@code ${...}} property + * values replaced by a property placeholder configurer such as + * {@link org.springframework.context.support.PropertySourcesPlaceholderConfigurer + * PropertySourcesPlaceholderConfigurer}, which itself is {@code EnvironmentAware} and + * as of Spring 3.1 is registered by default when using + * {@code }. + * + *

    Configuration of the environment object must be done through the + * {@code ConfigurableEnvironment} interface, returned from all + * {@code AbstractApplicationContext} subclass {@code getEnvironment()} methods. See + * {@link ConfigurableEnvironment} Javadoc for usage examples demonstrating manipulation + * of property sources prior to application context {@code refresh()}. + * + * @author Chris Beams + * @since 3.1 + * @see PropertyResolver + * @see EnvironmentCapable + * @see ConfigurableEnvironment + * @see AbstractEnvironment + * @see StandardEnvironment + * @see org.springframework.context.EnvironmentAware + * @see org.springframework.context.ConfigurableApplicationContext#getEnvironment + * @see org.springframework.context.ConfigurableApplicationContext#setEnvironment + * @see org.springframework.context.support.AbstractApplicationContext#createEnvironment + */ +public interface Environment extends PropertyResolver { + + /** + * Return the set of profiles explicitly made active for this environment. Profiles + * are used for creating logical groupings of bean definitions to be registered + * conditionally, for example based on deployment environment. Profiles can be + * activated by setting {@linkplain AbstractEnvironment#ACTIVE_PROFILES_PROPERTY_NAME + * "spring.profiles.active"} as a system property or by calling + * {@link ConfigurableEnvironment#setActiveProfiles(String...)}. + *

    If no profiles have explicitly been specified as active, then any + * {@linkplain #getDefaultProfiles() default profiles} will automatically be activated. + * @see #getDefaultProfiles + * @see ConfigurableEnvironment#setActiveProfiles + * @see AbstractEnvironment#ACTIVE_PROFILES_PROPERTY_NAME + */ + String[] getActiveProfiles(); + + /** + * Return the set of profiles to be active by default when no active profiles have + * been set explicitly. + * @see #getActiveProfiles + * @see ConfigurableEnvironment#setDefaultProfiles + * @see AbstractEnvironment#DEFAULT_PROFILES_PROPERTY_NAME + */ + String[] getDefaultProfiles(); + + /** + * Return whether one or more of the given profiles is active or, in the case of no + * explicit active profiles, whether one or more of the given profiles is included in + * the set of default profiles. If a profile begins with '!' the logic is inverted, + * i.e. the method will return {@code true} if the given profile is not active. + * For example, {@code env.acceptsProfiles("p1", "!p2")} will return {@code true} if + * profile 'p1' is active or 'p2' is not active. + * @throws IllegalArgumentException if called with zero arguments + * or if any profile is {@code null}, empty, or whitespace only + * @see #getActiveProfiles + * @see #getDefaultProfiles + * @see #acceptsProfiles(Profiles) + * @deprecated as of 5.1 in favor of {@link #acceptsProfiles(Profiles)} + */ + @Deprecated + boolean acceptsProfiles(String... profiles); + + /** + * Return whether the {@linkplain #getActiveProfiles() active profiles} + * match the given {@link Profiles} predicate. + */ + boolean acceptsProfiles(Profiles profiles); + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/EnvironmentCapable.java b/spring-core/src/main/java/org/springframework/core/env/EnvironmentCapable.java new file mode 100644 index 0000000..1b7b96b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/EnvironmentCapable.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +/** + * Interface indicating a component that contains and exposes an {@link Environment} reference. + * + *

    All Spring application contexts are EnvironmentCapable, and the interface is used primarily + * for performing {@code instanceof} checks in framework methods that accept BeanFactory + * instances that may or may not actually be ApplicationContext instances in order to interact + * with the environment if indeed it is available. + * + *

    As mentioned, {@link org.springframework.context.ApplicationContext ApplicationContext} + * extends EnvironmentCapable, and thus exposes a {@link #getEnvironment()} method; however, + * {@link org.springframework.context.ConfigurableApplicationContext ConfigurableApplicationContext} + * redefines {@link org.springframework.context.ConfigurableApplicationContext#getEnvironment + * getEnvironment()} and narrows the signature to return a {@link ConfigurableEnvironment}. + * The effect is that an Environment object is 'read-only' until it is being accessed from + * a ConfigurableApplicationContext, at which point it too may be configured. + * + * @author Chris Beams + * @since 3.1 + * @see Environment + * @see ConfigurableEnvironment + * @see org.springframework.context.ConfigurableApplicationContext#getEnvironment() + */ +public interface EnvironmentCapable { + + /** + * Return the {@link Environment} associated with this component. + */ + Environment getEnvironment(); + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/JOptCommandLinePropertySource.java b/spring-core/src/main/java/org/springframework/core/env/JOptCommandLinePropertySource.java new file mode 100644 index 0000000..64f67e3 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/JOptCommandLinePropertySource.java @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import joptsimple.OptionSet; +import joptsimple.OptionSpec; + +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +/** + * {@link CommandLinePropertySource} implementation backed by a JOpt {@link OptionSet}. + * + *

    Typical usage

    + * + * Configure and execute an {@code OptionParser} against the {@code String[]} of arguments + * supplied to the {@code main} method, and create a {@link JOptCommandLinePropertySource} + * using the resulting {@code OptionSet} object: + * + *
    + * public static void main(String[] args) {
    + *     OptionParser parser = new OptionParser();
    + *     parser.accepts("option1");
    + *     parser.accepts("option2").withRequiredArg();
    + *     OptionSet options = parser.parse(args);
    + *     PropertySource ps = new JOptCommandLinePropertySource(options);
    + *     // ...
    + * }
    + * + * See {@link CommandLinePropertySource} for complete general usage examples. + * + *

    Requires JOpt Simple version 4.3 or higher. Tested against JOpt up until 5.0. + * + * @author Chris Beams + * @author Juergen Hoeller + * @author Dave Syer + * @since 3.1 + * @see CommandLinePropertySource + * @see joptsimple.OptionParser + * @see joptsimple.OptionSet + */ +public class JOptCommandLinePropertySource extends CommandLinePropertySource { + + /** + * Create a new {@code JOptCommandLinePropertySource} having the default name + * and backed by the given {@code OptionSet}. + * @see CommandLinePropertySource#COMMAND_LINE_PROPERTY_SOURCE_NAME + * @see CommandLinePropertySource#CommandLinePropertySource(Object) + */ + public JOptCommandLinePropertySource(OptionSet options) { + super(options); + } + + /** + * Create a new {@code JOptCommandLinePropertySource} having the given name + * and backed by the given {@code OptionSet}. + */ + public JOptCommandLinePropertySource(String name, OptionSet options) { + super(name, options); + } + + + @Override + protected boolean containsOption(String name) { + return this.source.has(name); + } + + @Override + public String[] getPropertyNames() { + List names = new ArrayList<>(); + for (OptionSpec spec : this.source.specs()) { + String lastOption = CollectionUtils.lastElement(spec.options()); + if (lastOption != null) { + // Only the longest name is used for enumerating + names.add(lastOption); + } + } + return StringUtils.toStringArray(names); + } + + @Override + @Nullable + public List getOptionValues(String name) { + List argValues = this.source.valuesOf(name); + List stringArgValues = new ArrayList<>(); + for (Object argValue : argValues) { + stringArgValues.add(argValue.toString()); + } + if (stringArgValues.isEmpty()) { + return (this.source.has(name) ? Collections.emptyList() : null); + } + return Collections.unmodifiableList(stringArgValues); + } + + @Override + protected List getNonOptionArgs() { + List argValues = this.source.nonOptionArguments(); + List stringArgValues = new ArrayList<>(); + for (Object argValue : argValues) { + stringArgValues.add(argValue.toString()); + } + return (stringArgValues.isEmpty() ? Collections.emptyList() : + Collections.unmodifiableList(stringArgValues)); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/MapPropertySource.java b/spring-core/src/main/java/org/springframework/core/env/MapPropertySource.java new file mode 100644 index 0000000..36597a5 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/MapPropertySource.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import java.util.Map; + +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * {@link PropertySource} that reads keys and values from a {@code Map} object. + * The underlying map should not contain any {@code null} values in order to + * comply with {@link #getProperty} and {@link #containsProperty} semantics. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + * @see PropertiesPropertySource + */ +public class MapPropertySource extends EnumerablePropertySource> { + + /** + * Create a new {@code MapPropertySource} with the given name and {@code Map}. + * @param name the associated name + * @param source the Map source (without {@code null} values in order to get + * consistent {@link #getProperty} and {@link #containsProperty} behavior) + */ + public MapPropertySource(String name, Map source) { + super(name, source); + } + + + @Override + @Nullable + public Object getProperty(String name) { + return this.source.get(name); + } + + @Override + public boolean containsProperty(String name) { + return this.source.containsKey(name); + } + + @Override + public String[] getPropertyNames() { + return StringUtils.toStringArray(this.source.keySet()); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/MissingRequiredPropertiesException.java b/spring-core/src/main/java/org/springframework/core/env/MissingRequiredPropertiesException.java new file mode 100644 index 0000000..6733058 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/MissingRequiredPropertiesException.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Exception thrown when required properties are not found. + * + * @author Chris Beams + * @since 3.1 + * @see ConfigurablePropertyResolver#setRequiredProperties(String...) + * @see ConfigurablePropertyResolver#validateRequiredProperties() + * @see org.springframework.context.support.AbstractApplicationContext#prepareRefresh() + */ +@SuppressWarnings("serial") +public class MissingRequiredPropertiesException extends IllegalStateException { + + private final Set missingRequiredProperties = new LinkedHashSet<>(); + + + void addMissingRequiredProperty(String key) { + this.missingRequiredProperties.add(key); + } + + @Override + public String getMessage() { + return "The following properties were declared as required but could not be resolved: " + + getMissingRequiredProperties(); + } + + /** + * Return the set of properties marked as required but not present + * upon validation. + * @see ConfigurablePropertyResolver#setRequiredProperties(String...) + * @see ConfigurablePropertyResolver#validateRequiredProperties() + */ + public Set getMissingRequiredProperties() { + return this.missingRequiredProperties; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/MutablePropertySources.java b/spring-core/src/main/java/org/springframework/core/env/MutablePropertySources.java new file mode 100644 index 0000000..1bc07bc --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/MutablePropertySources.java @@ -0,0 +1,233 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import java.util.Iterator; +import java.util.List; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Stream; + +import org.springframework.lang.Nullable; + +/** + * The default implementation of the {@link PropertySources} interface. + * Allows manipulation of contained property sources and provides a constructor + * for copying an existing {@code PropertySources} instance. + * + *

    Where precedence is mentioned in methods such as {@link #addFirst} + * and {@link #addLast}, this is with regard to the order in which property sources + * will be searched when resolving a given property with a {@link PropertyResolver}. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + * @see PropertySourcesPropertyResolver + */ +public class MutablePropertySources implements PropertySources { + + private final List> propertySourceList = new CopyOnWriteArrayList<>(); + + + /** + * Create a new {@link MutablePropertySources} object. + */ + public MutablePropertySources() { + } + + /** + * Create a new {@code MutablePropertySources} from the given propertySources + * object, preserving the original order of contained {@code PropertySource} objects. + */ + public MutablePropertySources(PropertySources propertySources) { + this(); + for (PropertySource propertySource : propertySources) { + addLast(propertySource); + } + } + + + @Override + public Iterator> iterator() { + return this.propertySourceList.iterator(); + } + + @Override + public Spliterator> spliterator() { + return Spliterators.spliterator(this.propertySourceList, 0); + } + + @Override + public Stream> stream() { + return this.propertySourceList.stream(); + } + + @Override + public boolean contains(String name) { + for (PropertySource propertySource : this.propertySourceList) { + if (propertySource.getName().equals(name)) { + return true; + } + } + return false; + } + + @Override + @Nullable + public PropertySource get(String name) { + for (PropertySource propertySource : this.propertySourceList) { + if (propertySource.getName().equals(name)) { + return propertySource; + } + } + return null; + } + + + /** + * Add the given property source object with highest precedence. + */ + public void addFirst(PropertySource propertySource) { + synchronized (this.propertySourceList) { + removeIfPresent(propertySource); + this.propertySourceList.add(0, propertySource); + } + } + + /** + * Add the given property source object with lowest precedence. + */ + public void addLast(PropertySource propertySource) { + synchronized (this.propertySourceList) { + removeIfPresent(propertySource); + this.propertySourceList.add(propertySource); + } + } + + /** + * Add the given property source object with precedence immediately higher + * than the named relative property source. + */ + public void addBefore(String relativePropertySourceName, PropertySource propertySource) { + assertLegalRelativeAddition(relativePropertySourceName, propertySource); + synchronized (this.propertySourceList) { + removeIfPresent(propertySource); + int index = assertPresentAndGetIndex(relativePropertySourceName); + addAtIndex(index, propertySource); + } + } + + /** + * Add the given property source object with precedence immediately lower + * than the named relative property source. + */ + public void addAfter(String relativePropertySourceName, PropertySource propertySource) { + assertLegalRelativeAddition(relativePropertySourceName, propertySource); + synchronized (this.propertySourceList) { + removeIfPresent(propertySource); + int index = assertPresentAndGetIndex(relativePropertySourceName); + addAtIndex(index + 1, propertySource); + } + } + + /** + * Return the precedence of the given property source, {@code -1} if not found. + */ + public int precedenceOf(PropertySource propertySource) { + return this.propertySourceList.indexOf(propertySource); + } + + /** + * Remove and return the property source with the given name, {@code null} if not found. + * @param name the name of the property source to find and remove + */ + @Nullable + public PropertySource remove(String name) { + synchronized (this.propertySourceList) { + int index = this.propertySourceList.indexOf(PropertySource.named(name)); + return (index != -1 ? this.propertySourceList.remove(index) : null); + } + } + + /** + * Replace the property source with the given name with the given property source object. + * @param name the name of the property source to find and replace + * @param propertySource the replacement property source + * @throws IllegalArgumentException if no property source with the given name is present + * @see #contains + */ + public void replace(String name, PropertySource propertySource) { + synchronized (this.propertySourceList) { + int index = assertPresentAndGetIndex(name); + this.propertySourceList.set(index, propertySource); + } + } + + /** + * Return the number of {@link PropertySource} objects contained. + */ + public int size() { + return this.propertySourceList.size(); + } + + @Override + public String toString() { + return this.propertySourceList.toString(); + } + + + /** + * Ensure that the given property source is not being added relative to itself. + */ + protected void assertLegalRelativeAddition(String relativePropertySourceName, PropertySource propertySource) { + String newPropertySourceName = propertySource.getName(); + if (relativePropertySourceName.equals(newPropertySourceName)) { + throw new IllegalArgumentException( + "PropertySource named '" + newPropertySourceName + "' cannot be added relative to itself"); + } + } + + /** + * Remove the given property source if it is present. + */ + protected void removeIfPresent(PropertySource propertySource) { + this.propertySourceList.remove(propertySource); + } + + /** + * Add the given property source at a particular index in the list. + */ + private void addAtIndex(int index, PropertySource propertySource) { + removeIfPresent(propertySource); + this.propertySourceList.add(index, propertySource); + } + + /** + * Assert that the named property source is present and return its index. + * @param name {@linkplain PropertySource#getName() name of the property source} to find + * @throws IllegalArgumentException if the named property source is not present + */ + private int assertPresentAndGetIndex(String name) { + int index = this.propertySourceList.indexOf(PropertySource.named(name)); + if (index == -1) { + throw new IllegalArgumentException("PropertySource named '" + name + "' does not exist"); + } + return index; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/Profiles.java b/spring-core/src/main/java/org/springframework/core/env/Profiles.java new file mode 100644 index 0000000..7f4fba7 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/Profiles.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import java.util.function.Predicate; + +/** + * Profile predicate that may be {@linkplain Environment#acceptsProfiles(Profiles) + * accepted} by an {@link Environment}. + * + *

    May be implemented directly or, more usually, created using the + * {@link #of(String...) of(...)} factory method. + * + * @author Phillip Webb + * @author Sam Brannen + * @since 5.1 + */ +@FunctionalInterface +public interface Profiles { + + /** + * Test if this {@code Profiles} instance matches against the given + * active profiles predicate. + * @param activeProfiles a predicate that tests whether a given profile is + * currently active + */ + boolean matches(Predicate activeProfiles); + + + /** + * Create a new {@link Profiles} instance that checks for matches against + * the given profile strings. + *

    The returned instance will {@linkplain Profiles#matches(Predicate) match} + * if any one of the given profile strings matches. + *

    A profile string may contain a simple profile name (for example + * {@code "production"}) or a profile expression. A profile expression allows + * for more complicated profile logic to be expressed, for example + * {@code "production & cloud"}. + *

    The following operators are supported in profile expressions. + *

      + *
    • {@code !} - A logical NOT of the profile or profile expression
    • + *
    • {@code &} - A logical AND of the profiles or profile expressions
    • + *
    • {@code |} - A logical OR of the profiles or profile expressions
    • + *
    + *

    Please note that the {@code &} and {@code |} operators may not be mixed + * without using parentheses. For example {@code "a & b | c"} is not a valid + * expression; it must be expressed as {@code "(a & b) | c"} or + * {@code "a & (b | c)"}. + *

    As of Spring Framework 5.1.17, two {@code Profiles} instances returned + * by this method are considered equivalent to each other (in terms of + * {@code equals()} and {@code hashCode()} semantics) if they are created + * with identical profile strings. + * @param profiles the profile strings to include + * @return a new {@link Profiles} instance + */ + static Profiles of(String... profiles) { + return ProfilesParser.parse(profiles); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/ProfilesParser.java b/spring-core/src/main/java/org/springframework/core/env/ProfilesParser.java new file mode 100644 index 0000000..5d4dd0c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/ProfilesParser.java @@ -0,0 +1,199 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.function.Predicate; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Internal parser used by {@link Profiles#of}. + * + * @author Phillip Webb + * @author Sam Brannen + * @since 5.1 + */ +final class ProfilesParser { + + private ProfilesParser() { + } + + + static Profiles parse(String... expressions) { + Assert.notEmpty(expressions, "Must specify at least one profile"); + Profiles[] parsed = new Profiles[expressions.length]; + for (int i = 0; i < expressions.length; i++) { + parsed[i] = parseExpression(expressions[i]); + } + return new ParsedProfiles(expressions, parsed); + } + + private static Profiles parseExpression(String expression) { + Assert.hasText(expression, () -> "Invalid profile expression [" + expression + "]: must contain text"); + StringTokenizer tokens = new StringTokenizer(expression, "()&|!", true); + return parseTokens(expression, tokens); + } + + private static Profiles parseTokens(String expression, StringTokenizer tokens) { + return parseTokens(expression, tokens, Context.NONE); + } + + private static Profiles parseTokens(String expression, StringTokenizer tokens, Context context) { + List elements = new ArrayList<>(); + Operator operator = null; + while (tokens.hasMoreTokens()) { + String token = tokens.nextToken().trim(); + if (token.isEmpty()) { + continue; + } + switch (token) { + case "(": + Profiles contents = parseTokens(expression, tokens, Context.BRACKET); + if (context == Context.INVERT) { + return contents; + } + elements.add(contents); + break; + case "&": + assertWellFormed(expression, operator == null || operator == Operator.AND); + operator = Operator.AND; + break; + case "|": + assertWellFormed(expression, operator == null || operator == Operator.OR); + operator = Operator.OR; + break; + case "!": + elements.add(not(parseTokens(expression, tokens, Context.INVERT))); + break; + case ")": + Profiles merged = merge(expression, elements, operator); + if (context == Context.BRACKET) { + return merged; + } + elements.clear(); + elements.add(merged); + operator = null; + break; + default: + Profiles value = equals(token); + if (context == Context.INVERT) { + return value; + } + elements.add(value); + } + } + return merge(expression, elements, operator); + } + + private static Profiles merge(String expression, List elements, @Nullable Operator operator) { + assertWellFormed(expression, !elements.isEmpty()); + if (elements.size() == 1) { + return elements.get(0); + } + Profiles[] profiles = elements.toArray(new Profiles[0]); + return (operator == Operator.AND ? and(profiles) : or(profiles)); + } + + private static void assertWellFormed(String expression, boolean wellFormed) { + Assert.isTrue(wellFormed, () -> "Malformed profile expression [" + expression + "]"); + } + + private static Profiles or(Profiles... profiles) { + return activeProfile -> Arrays.stream(profiles).anyMatch(isMatch(activeProfile)); + } + + private static Profiles and(Profiles... profiles) { + return activeProfile -> Arrays.stream(profiles).allMatch(isMatch(activeProfile)); + } + + private static Profiles not(Profiles profiles) { + return activeProfile -> !profiles.matches(activeProfile); + } + + private static Profiles equals(String profile) { + return activeProfile -> activeProfile.test(profile); + } + + private static Predicate isMatch(Predicate activeProfile) { + return profiles -> profiles.matches(activeProfile); + } + + + private enum Operator {AND, OR} + + + private enum Context {NONE, INVERT, BRACKET} + + + private static class ParsedProfiles implements Profiles { + + private final Set expressions = new LinkedHashSet<>(); + + private final Profiles[] parsed; + + ParsedProfiles(String[] expressions, Profiles[] parsed) { + Collections.addAll(this.expressions, expressions); + this.parsed = parsed; + } + + @Override + public boolean matches(Predicate activeProfiles) { + for (Profiles candidate : this.parsed) { + if (candidate.matches(activeProfiles)) { + return true; + } + } + return false; + } + + @Override + public int hashCode() { + return this.expressions.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ParsedProfiles that = (ParsedProfiles) obj; + return this.expressions.equals(that.expressions); + } + + @Override + public String toString() { + return StringUtils.collectionToDelimitedString(this.expressions, " or "); + } + + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/PropertiesPropertySource.java b/spring-core/src/main/java/org/springframework/core/env/PropertiesPropertySource.java new file mode 100644 index 0000000..d09c368 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/PropertiesPropertySource.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import java.util.Map; +import java.util.Properties; + +/** + * {@link PropertySource} implementation that extracts properties from a + * {@link java.util.Properties} object. + * + *

    Note that because a {@code Properties} object is technically an + * {@code } {@link java.util.Hashtable Hashtable}, one may contain + * non-{@code String} keys or values. This implementation, however is restricted to + * accessing only {@code String}-based keys and values, in the same fashion as + * {@link Properties#getProperty} and {@link Properties#setProperty}. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + */ +public class PropertiesPropertySource extends MapPropertySource { + + @SuppressWarnings({"rawtypes", "unchecked"}) + public PropertiesPropertySource(String name, Properties source) { + super(name, (Map) source); + } + + protected PropertiesPropertySource(String name, Map source) { + super(name, source); + } + + + @Override + public String[] getPropertyNames() { + synchronized (this.source) { + return super.getPropertyNames(); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/PropertyResolver.java b/spring-core/src/main/java/org/springframework/core/env/PropertyResolver.java new file mode 100644 index 0000000..173a1a3 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/PropertyResolver.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import org.springframework.lang.Nullable; + +/** + * Interface for resolving properties against any underlying source. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + * @see Environment + * @see PropertySourcesPropertyResolver + */ +public interface PropertyResolver { + + /** + * Return whether the given property key is available for resolution, + * i.e. if the value for the given key is not {@code null}. + */ + boolean containsProperty(String key); + + /** + * Return the property value associated with the given key, + * or {@code null} if the key cannot be resolved. + * @param key the property name to resolve + * @see #getProperty(String, String) + * @see #getProperty(String, Class) + * @see #getRequiredProperty(String) + */ + @Nullable + String getProperty(String key); + + /** + * Return the property value associated with the given key, or + * {@code defaultValue} if the key cannot be resolved. + * @param key the property name to resolve + * @param defaultValue the default value to return if no value is found + * @see #getRequiredProperty(String) + * @see #getProperty(String, Class) + */ + String getProperty(String key, String defaultValue); + + /** + * Return the property value associated with the given key, + * or {@code null} if the key cannot be resolved. + * @param key the property name to resolve + * @param targetType the expected type of the property value + * @see #getRequiredProperty(String, Class) + */ + @Nullable + T getProperty(String key, Class targetType); + + /** + * Return the property value associated with the given key, + * or {@code defaultValue} if the key cannot be resolved. + * @param key the property name to resolve + * @param targetType the expected type of the property value + * @param defaultValue the default value to return if no value is found + * @see #getRequiredProperty(String, Class) + */ + T getProperty(String key, Class targetType, T defaultValue); + + /** + * Return the property value associated with the given key (never {@code null}). + * @throws IllegalStateException if the key cannot be resolved + * @see #getRequiredProperty(String, Class) + */ + String getRequiredProperty(String key) throws IllegalStateException; + + /** + * Return the property value associated with the given key, converted to the given + * targetType (never {@code null}). + * @throws IllegalStateException if the given key cannot be resolved + */ + T getRequiredProperty(String key, Class targetType) throws IllegalStateException; + + /** + * Resolve ${...} placeholders in the given text, replacing them with corresponding + * property values as resolved by {@link #getProperty}. Unresolvable placeholders with + * no default value are ignored and passed through unchanged. + * @param text the String to resolve + * @return the resolved String (never {@code null}) + * @throws IllegalArgumentException if given text is {@code null} + * @see #resolveRequiredPlaceholders + */ + String resolvePlaceholders(String text); + + /** + * Resolve ${...} placeholders in the given text, replacing them with corresponding + * property values as resolved by {@link #getProperty}. Unresolvable placeholders with + * no default value will cause an IllegalArgumentException to be thrown. + * @return the resolved String (never {@code null}) + * @throws IllegalArgumentException if given text is {@code null} + * or if any placeholders are unresolvable + */ + String resolveRequiredPlaceholders(String text) throws IllegalArgumentException; + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/PropertySource.java b/spring-core/src/main/java/org/springframework/core/env/PropertySource.java new file mode 100644 index 0000000..24a71dd --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/PropertySource.java @@ -0,0 +1,255 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Abstract base class representing a source of name/value property pairs. The underlying + * {@linkplain #getSource() source object} may be of any type {@code T} that encapsulates + * properties. Examples include {@link java.util.Properties} objects, {@link java.util.Map} + * objects, {@code ServletContext} and {@code ServletConfig} objects (for access to init + * parameters). Explore the {@code PropertySource} type hierarchy to see provided + * implementations. + * + *

    {@code PropertySource} objects are not typically used in isolation, but rather + * through a {@link PropertySources} object, which aggregates property sources and in + * conjunction with a {@link PropertyResolver} implementation that can perform + * precedence-based searches across the set of {@code PropertySources}. + * + *

    {@code PropertySource} identity is determined not based on the content of + * encapsulated properties, but rather based on the {@link #getName() name} of the + * {@code PropertySource} alone. This is useful for manipulating {@code PropertySource} + * objects when in collection contexts. See operations in {@link MutablePropertySources} + * as well as the {@link #named(String)} and {@link #toString()} methods for details. + * + *

    Note that when working with @{@link + * org.springframework.context.annotation.Configuration Configuration} classes that + * the @{@link org.springframework.context.annotation.PropertySource PropertySource} + * annotation provides a convenient and declarative way of adding property sources to the + * enclosing {@code Environment}. + * + * @author Chris Beams + * @since 3.1 + * @param the source type + * @see PropertySources + * @see PropertyResolver + * @see PropertySourcesPropertyResolver + * @see MutablePropertySources + * @see org.springframework.context.annotation.PropertySource + */ +public abstract class PropertySource { + + protected final Log logger = LogFactory.getLog(getClass()); + + protected final String name; + + protected final T source; + + + /** + * Create a new {@code PropertySource} with the given name and source object. + * @param name the associated name + * @param source the source object + */ + public PropertySource(String name, T source) { + Assert.hasText(name, "Property source name must contain at least one character"); + Assert.notNull(source, "Property source must not be null"); + this.name = name; + this.source = source; + } + + /** + * Create a new {@code PropertySource} with the given name and with a new + * {@code Object} instance as the underlying source. + *

    Often useful in testing scenarios when creating anonymous implementations + * that never query an actual source but rather return hard-coded values. + */ + @SuppressWarnings("unchecked") + public PropertySource(String name) { + this(name, (T) new Object()); + } + + + /** + * Return the name of this {@code PropertySource}. + */ + public String getName() { + return this.name; + } + + /** + * Return the underlying source object for this {@code PropertySource}. + */ + public T getSource() { + return this.source; + } + + /** + * Return whether this {@code PropertySource} contains the given name. + *

    This implementation simply checks for a {@code null} return value + * from {@link #getProperty(String)}. Subclasses may wish to implement + * a more efficient algorithm if possible. + * @param name the property name to find + */ + public boolean containsProperty(String name) { + return (getProperty(name) != null); + } + + /** + * Return the value associated with the given name, + * or {@code null} if not found. + * @param name the property to find + * @see PropertyResolver#getRequiredProperty(String) + */ + @Nullable + public abstract Object getProperty(String name); + + + /** + * This {@code PropertySource} object is equal to the given object if: + *

      + *
    • they are the same instance + *
    • the {@code name} properties for both objects are equal + *
    + *

    No properties other than {@code name} are evaluated. + */ + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof PropertySource && + ObjectUtils.nullSafeEquals(getName(), ((PropertySource) other).getName()))); + } + + /** + * Return a hash code derived from the {@code name} property + * of this {@code PropertySource} object. + */ + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(getName()); + } + + /** + * Produce concise output (type and name) if the current log level does not include + * debug. If debug is enabled, produce verbose output including the hash code of the + * PropertySource instance and every name/value property pair. + *

    This variable verbosity is useful as a property source such as system properties + * or environment variables may contain an arbitrary number of property pairs, + * potentially leading to difficult to read exception and log messages. + * @see Log#isDebugEnabled() + */ + @Override + public String toString() { + if (logger.isDebugEnabled()) { + return getClass().getSimpleName() + "@" + System.identityHashCode(this) + + " {name='" + getName() + "', properties=" + getSource() + "}"; + } + else { + return getClass().getSimpleName() + " {name='" + getName() + "'}"; + } + } + + + /** + * Return a {@code PropertySource} implementation intended for collection comparison purposes only. + *

    Primarily for internal use, but given a collection of {@code PropertySource} objects, may be + * used as follows: + *

    +	 * {@code List> sources = new ArrayList>();
    +	 * sources.add(new MapPropertySource("sourceA", mapA));
    +	 * sources.add(new MapPropertySource("sourceB", mapB));
    +	 * assert sources.contains(PropertySource.named("sourceA"));
    +	 * assert sources.contains(PropertySource.named("sourceB"));
    +	 * assert !sources.contains(PropertySource.named("sourceC"));
    +	 * }
    + * The returned {@code PropertySource} will throw {@code UnsupportedOperationException} + * if any methods other than {@code equals(Object)}, {@code hashCode()}, and {@code toString()} + * are called. + * @param name the name of the comparison {@code PropertySource} to be created and returned. + */ + public static PropertySource named(String name) { + return new ComparisonPropertySource(name); + } + + + /** + * {@code PropertySource} to be used as a placeholder in cases where an actual + * property source cannot be eagerly initialized at application context + * creation time. For example, a {@code ServletContext}-based property source + * must wait until the {@code ServletContext} object is available to its enclosing + * {@code ApplicationContext}. In such cases, a stub should be used to hold the + * intended default position/order of the property source, then be replaced + * during context refresh. + * @see org.springframework.context.support.AbstractApplicationContext#initPropertySources() + * @see org.springframework.web.context.support.StandardServletEnvironment + * @see org.springframework.web.context.support.ServletContextPropertySource + */ + public static class StubPropertySource extends PropertySource { + + public StubPropertySource(String name) { + super(name, new Object()); + } + + /** + * Always returns {@code null}. + */ + @Override + @Nullable + public String getProperty(String name) { + return null; + } + } + + + /** + * A {@code PropertySource} implementation intended for collection comparison + * purposes. + * + * @see PropertySource#named(String) + */ + static class ComparisonPropertySource extends StubPropertySource { + + private static final String USAGE_ERROR = + "ComparisonPropertySource instances are for use with collection comparison only"; + + public ComparisonPropertySource(String name) { + super(name); + } + + @Override + public Object getSource() { + throw new UnsupportedOperationException(USAGE_ERROR); + } + + @Override + public boolean containsProperty(String name) { + throw new UnsupportedOperationException(USAGE_ERROR); + } + + @Override + @Nullable + public String getProperty(String name) { + throw new UnsupportedOperationException(USAGE_ERROR); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/PropertySources.java b/spring-core/src/main/java/org/springframework/core/env/PropertySources.java new file mode 100644 index 0000000..0296ea1 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/PropertySources.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.springframework.lang.Nullable; + +/** + * Holder containing one or more {@link PropertySource} objects. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + * @see PropertySource + */ +public interface PropertySources extends Iterable> { + + /** + * Return a sequential {@link Stream} containing the property sources. + * @since 5.1 + */ + default Stream> stream() { + return StreamSupport.stream(spliterator(), false); + } + + /** + * Return whether a property source with the given name is contained. + * @param name the {@linkplain PropertySource#getName() name of the property source} to find + */ + boolean contains(String name); + + /** + * Return the property source with the given name, {@code null} if not found. + * @param name the {@linkplain PropertySource#getName() name of the property source} to find + */ + @Nullable + PropertySource get(String name); + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/PropertySourcesPropertyResolver.java b/spring-core/src/main/java/org/springframework/core/env/PropertySourcesPropertyResolver.java new file mode 100644 index 0000000..aa07751 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/PropertySourcesPropertyResolver.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import org.springframework.lang.Nullable; + +/** + * {@link PropertyResolver} implementation that resolves property values against + * an underlying set of {@link PropertySources}. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + * @see PropertySource + * @see PropertySources + * @see AbstractEnvironment + */ +public class PropertySourcesPropertyResolver extends AbstractPropertyResolver { + + @Nullable + private final PropertySources propertySources; + + + /** + * Create a new resolver against the given property sources. + * @param propertySources the set of {@link PropertySource} objects to use + */ + public PropertySourcesPropertyResolver(@Nullable PropertySources propertySources) { + this.propertySources = propertySources; + } + + + @Override + public boolean containsProperty(String key) { + if (this.propertySources != null) { + for (PropertySource propertySource : this.propertySources) { + if (propertySource.containsProperty(key)) { + return true; + } + } + } + return false; + } + + @Override + @Nullable + public String getProperty(String key) { + return getProperty(key, String.class, true); + } + + @Override + @Nullable + public T getProperty(String key, Class targetValueType) { + return getProperty(key, targetValueType, true); + } + + @Override + @Nullable + protected String getPropertyAsRawString(String key) { + return getProperty(key, String.class, false); + } + + @Nullable + protected T getProperty(String key, Class targetValueType, boolean resolveNestedPlaceholders) { + if (this.propertySources != null) { + for (PropertySource propertySource : this.propertySources) { + if (logger.isTraceEnabled()) { + logger.trace("Searching for key '" + key + "' in PropertySource '" + + propertySource.getName() + "'"); + } + Object value = propertySource.getProperty(key); + if (value != null) { + if (resolveNestedPlaceholders && value instanceof String) { + value = resolveNestedPlaceholders((String) value); + } + logKeyFound(key, propertySource, value); + return convertValueIfNecessary(value, targetValueType); + } + } + } + if (logger.isTraceEnabled()) { + logger.trace("Could not find key '" + key + "' in any property source"); + } + return null; + } + + /** + * Log the given key as found in the given {@link PropertySource}, resulting in + * the given value. + *

    The default implementation writes a debug log message with key and source. + * As of 4.3.3, this does not log the value anymore in order to avoid accidental + * logging of sensitive settings. Subclasses may override this method to change + * the log level and/or log message, including the property's value if desired. + * @param key the key found + * @param propertySource the {@code PropertySource} that the key has been found in + * @param value the corresponding value + * @since 4.3.1 + */ + protected void logKeyFound(String key, PropertySource propertySource, Object value) { + if (logger.isDebugEnabled()) { + logger.debug("Found key '" + key + "' in PropertySource '" + propertySource.getName() + + "' with value of type " + value.getClass().getSimpleName()); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/ReadOnlySystemAttributesMap.java b/spring-core/src/main/java/org/springframework/core/env/ReadOnlySystemAttributesMap.java new file mode 100644 index 0000000..37867bd --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/ReadOnlySystemAttributesMap.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import org.springframework.lang.Nullable; + +/** + * Read-only {@code Map} implementation that is backed by system + * properties or environment variables. + * + *

    Used by {@link AbstractEnvironment} when a {@link SecurityManager} prohibits + * access to {@link System#getProperties()} or {@link System#getenv()}. It is for this + * reason that the implementations of {@link #keySet()}, {@link #entrySet()}, and + * {@link #values()} always return empty even though {@link #get(Object)} may in fact + * return non-null if the current security manager allows access to individual keys. + * + * @author Arjen Poutsma + * @author Chris Beams + * @since 3.0 + */ +abstract class ReadOnlySystemAttributesMap implements Map { + + @Override + public boolean containsKey(Object key) { + return (get(key) != null); + } + + /** + * Returns the value to which the specified key is mapped, or {@code null} if this map + * contains no mapping for the key. + * @param key the name of the system attribute to retrieve + * @throws IllegalArgumentException if given key is non-String + */ + @Override + @Nullable + public String get(Object key) { + if (!(key instanceof String)) { + throw new IllegalArgumentException( + "Type of key [" + key.getClass().getName() + "] must be java.lang.String"); + } + return getSystemAttribute((String) key); + } + + @Override + public boolean isEmpty() { + return false; + } + + /** + * Template method that returns the underlying system attribute. + *

    Implementations typically call {@link System#getProperty(String)} or {@link System#getenv(String)} here. + */ + @Nullable + protected abstract String getSystemAttribute(String attributeName); + + + // Unsupported + + @Override + public int size() { + throw new UnsupportedOperationException(); + } + + @Override + public String put(String key, String value) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsValue(Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public String remove(Object key) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @Override + public Set keySet() { + return Collections.emptySet(); + } + + @Override + public void putAll(Map map) { + throw new UnsupportedOperationException(); + } + + @Override + public Collection values() { + return Collections.emptySet(); + } + + @Override + public Set> entrySet() { + return Collections.emptySet(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLineArgsParser.java b/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLineArgsParser.java new file mode 100644 index 0000000..bcf8d07 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLineArgsParser.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +/** + * Parses a {@code String[]} of command line arguments in order to populate a + * {@link CommandLineArgs} object. + * + *

    Working with option arguments

    + *

    Option arguments must adhere to the exact syntax: + * + *

    --optName[=optValue]
    + * + *

    That is, options must be prefixed with "{@code --}" and may or may not + * specify a value. If a value is specified, the name and value must be separated + * without spaces by an equals sign ("="). The value may optionally be + * an empty string. + * + *

    Valid examples of option arguments

    + *
    + * --foo
    + * --foo=
    + * --foo=""
    + * --foo=bar
    + * --foo="bar then baz"
    + * --foo=bar,baz,biz
    + * + *

    Invalid examples of option arguments

    + *
    + * -foo
    + * --foo bar
    + * --foo = bar
    + * --foo=bar --foo=baz --foo=biz
    + * + *

    Working with non-option arguments

    + *

    Any and all arguments specified at the command line without the "{@code --}" + * option prefix will be considered as "non-option arguments" and made available + * through the {@link CommandLineArgs#getNonOptionArgs()} method. + * + * @author Chris Beams + * @author Sam Brannen + * @since 3.1 + */ +class SimpleCommandLineArgsParser { + + /** + * Parse the given {@code String} array based on the rules described {@linkplain + * SimpleCommandLineArgsParser above}, returning a fully-populated + * {@link CommandLineArgs} object. + * @param args command line arguments, typically from a {@code main()} method + */ + public CommandLineArgs parse(String... args) { + CommandLineArgs commandLineArgs = new CommandLineArgs(); + for (String arg : args) { + if (arg.startsWith("--")) { + String optionText = arg.substring(2); + String optionName; + String optionValue = null; + int indexOfEqualsSign = optionText.indexOf('='); + if (indexOfEqualsSign > -1) { + optionName = optionText.substring(0, indexOfEqualsSign); + optionValue = optionText.substring(indexOfEqualsSign + 1); + } + else { + optionName = optionText; + } + if (optionName.isEmpty()) { + throw new IllegalArgumentException("Invalid argument syntax: " + arg); + } + commandLineArgs.addOptionArg(optionName, optionValue); + } + else { + commandLineArgs.addNonOptionArg(arg); + } + } + return commandLineArgs; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLinePropertySource.java b/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLinePropertySource.java new file mode 100644 index 0000000..a4a2539 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLinePropertySource.java @@ -0,0 +1,131 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import java.util.List; + +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * {@link CommandLinePropertySource} implementation backed by a simple String array. + * + *

    Purpose

    + *

    This {@code CommandLinePropertySource} implementation aims to provide the simplest + * possible approach to parsing command line arguments. As with all {@code + * CommandLinePropertySource} implementations, command line arguments are broken into two + * distinct groups: option arguments and non-option arguments, as + * described below (some sections copied from Javadoc for + * {@link SimpleCommandLineArgsParser}): + * + *

    Working with option arguments

    + *

    Option arguments must adhere to the exact syntax: + * + *

    --optName[=optValue]
    + * + *

    That is, options must be prefixed with "{@code --}" and may or may not + * specify a value. If a value is specified, the name and value must be separated + * without spaces by an equals sign ("="). The value may optionally be + * an empty string. + * + *

    Valid examples of option arguments

    + *
    + * --foo
    + * --foo=
    + * --foo=""
    + * --foo=bar
    + * --foo="bar then baz"
    + * --foo=bar,baz,biz
    + * + *

    Invalid examples of option arguments

    + *
    + * -foo
    + * --foo bar
    + * --foo = bar
    + * --foo=bar --foo=baz --foo=biz
    + * + *

    Working with non-option arguments

    + *

    Any and all arguments specified at the command line without the "{@code --}" + * option prefix will be considered as "non-option arguments" and made available + * through the {@link CommandLineArgs#getNonOptionArgs()} method. + * + *

    Typical usage

    + *
    + * public static void main(String[] args) {
    + *     PropertySource ps = new SimpleCommandLinePropertySource(args);
    + *     // ...
    + * }
    + * + * See {@link CommandLinePropertySource} for complete general usage examples. + * + *

    Beyond the basics

    + * + *

    When more fully-featured command line parsing is necessary, consider using + * the provided {@link JOptCommandLinePropertySource}, or implement your own + * {@code CommandLinePropertySource} against the command line parsing library of your + * choice. + * + * @author Chris Beams + * @since 3.1 + * @see CommandLinePropertySource + * @see JOptCommandLinePropertySource + */ +public class SimpleCommandLinePropertySource extends CommandLinePropertySource { + + /** + * Create a new {@code SimpleCommandLinePropertySource} having the default name + * and backed by the given {@code String[]} of command line arguments. + * @see CommandLinePropertySource#COMMAND_LINE_PROPERTY_SOURCE_NAME + * @see CommandLinePropertySource#CommandLinePropertySource(Object) + */ + public SimpleCommandLinePropertySource(String... args) { + super(new SimpleCommandLineArgsParser().parse(args)); + } + + /** + * Create a new {@code SimpleCommandLinePropertySource} having the given name + * and backed by the given {@code String[]} of command line arguments. + */ + public SimpleCommandLinePropertySource(String name, String[] args) { + super(name, new SimpleCommandLineArgsParser().parse(args)); + } + + /** + * Get the property names for the option arguments. + */ + @Override + public String[] getPropertyNames() { + return StringUtils.toStringArray(this.source.getOptionNames()); + } + + @Override + protected boolean containsOption(String name) { + return this.source.containsOption(name); + } + + @Override + @Nullable + protected List getOptionValues(String name) { + return this.source.getOptionValues(name); + } + + @Override + protected List getNonOptionArgs() { + return this.source.getNonOptionArgs(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/StandardEnvironment.java b/spring-core/src/main/java/org/springframework/core/env/StandardEnvironment.java new file mode 100644 index 0000000..fc45706 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/StandardEnvironment.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +/** + * {@link Environment} implementation suitable for use in 'standard' (i.e. non-web) + * applications. + * + *

    In addition to the usual functions of a {@link ConfigurableEnvironment} such as + * property resolution and profile-related operations, this implementation configures two + * default property sources, to be searched in the following order: + *

      + *
    • {@linkplain AbstractEnvironment#getSystemProperties() system properties} + *
    • {@linkplain AbstractEnvironment#getSystemEnvironment() system environment variables} + *
    + * + * That is, if the key "xyz" is present both in the JVM system properties as well as in + * the set of environment variables for the current process, the value of key "xyz" from + * system properties will return from a call to {@code environment.getProperty("xyz")}. + * This ordering is chosen by default because system properties are per-JVM, while + * environment variables may be the same across many JVMs on a given system. Giving + * system properties precedence allows for overriding of environment variables on a + * per-JVM basis. + * + *

    These default property sources may be removed, reordered, or replaced; and + * additional property sources may be added using the {@link MutablePropertySources} + * instance available from {@link #getPropertySources()}. See + * {@link ConfigurableEnvironment} Javadoc for usage examples. + * + *

    See {@link SystemEnvironmentPropertySource} javadoc for details on special handling + * of property names in shell environments (e.g. Bash) that disallow period characters in + * variable names. + * + * @author Chris Beams + * @since 3.1 + * @see ConfigurableEnvironment + * @see SystemEnvironmentPropertySource + * @see org.springframework.web.context.support.StandardServletEnvironment + */ +public class StandardEnvironment extends AbstractEnvironment { + + /** System environment property source name: {@value}. */ + public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment"; + + /** JVM system properties property source name: {@value}. */ + public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties"; + + + /** + * Customize the set of property sources with those appropriate for any standard + * Java environment: + *

      + *
    • {@value #SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME} + *
    • {@value #SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME} + *
    + *

    Properties present in {@value #SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME} will + * take precedence over those in {@value #SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME}. + * @see AbstractEnvironment#customizePropertySources(MutablePropertySources) + * @see #getSystemProperties() + * @see #getSystemEnvironment() + */ + @Override + protected void customizePropertySources(MutablePropertySources propertySources) { + propertySources.addLast( + new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties())); + propertySources.addLast( + new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment())); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/SystemEnvironmentPropertySource.java b/spring-core/src/main/java/org/springframework/core/env/SystemEnvironmentPropertySource.java new file mode 100644 index 0000000..06dd2b7 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/SystemEnvironmentPropertySource.java @@ -0,0 +1,155 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import java.util.Map; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Specialization of {@link MapPropertySource} designed for use with + * {@linkplain AbstractEnvironment#getSystemEnvironment() system environment variables}. + * Compensates for constraints in Bash and other shells that do not allow for variables + * containing the period character and/or hyphen character; also allows for uppercase + * variations on property names for more idiomatic shell use. + * + *

    For example, a call to {@code getProperty("foo.bar")} will attempt to find a value + * for the original property or any 'equivalent' property, returning the first found: + *

      + *
    • {@code foo.bar} - the original name
    • + *
    • {@code foo_bar} - with underscores for periods (if any)
    • + *
    • {@code FOO.BAR} - original, with upper case
    • + *
    • {@code FOO_BAR} - with underscores and upper case
    • + *
    + * Any hyphen variant of the above would work as well, or even mix dot/hyphen variants. + * + *

    The same applies for calls to {@link #containsProperty(String)}, which returns + * {@code true} if any of the above properties are present, otherwise {@code false}. + * + *

    This feature is particularly useful when specifying active or default profiles as + * environment variables. The following is not allowable under Bash: + * + *

    spring.profiles.active=p1 java -classpath ... MyApp
    + * + * However, the following syntax is permitted and is also more conventional: + * + *
    SPRING_PROFILES_ACTIVE=p1 java -classpath ... MyApp
    + * + *

    Enable debug- or trace-level logging for this class (or package) for messages + * explaining when these 'property name resolutions' occur. + * + *

    This property source is included by default in {@link StandardEnvironment} + * and all its subclasses. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + * @see StandardEnvironment + * @see AbstractEnvironment#getSystemEnvironment() + * @see AbstractEnvironment#ACTIVE_PROFILES_PROPERTY_NAME + */ +public class SystemEnvironmentPropertySource extends MapPropertySource { + + /** + * Create a new {@code SystemEnvironmentPropertySource} with the given name and + * delegating to the given {@code MapPropertySource}. + */ + public SystemEnvironmentPropertySource(String name, Map source) { + super(name, source); + } + + + /** + * Return {@code true} if a property with the given name or any underscore/uppercase variant + * thereof exists in this property source. + */ + @Override + public boolean containsProperty(String name) { + return (getProperty(name) != null); + } + + /** + * This implementation returns {@code true} if a property with the given name or + * any underscore/uppercase variant thereof exists in this property source. + */ + @Override + @Nullable + public Object getProperty(String name) { + String actualName = resolvePropertyName(name); + if (logger.isDebugEnabled() && !name.equals(actualName)) { + logger.debug("PropertySource '" + getName() + "' does not contain property '" + name + + "', but found equivalent '" + actualName + "'"); + } + return super.getProperty(actualName); + } + + /** + * Check to see if this property source contains a property with the given name, or + * any underscore / uppercase variation thereof. Return the resolved name if one is + * found or otherwise the original name. Never returns {@code null}. + */ + protected final String resolvePropertyName(String name) { + Assert.notNull(name, "Property name must not be null"); + String resolvedName = checkPropertyName(name); + if (resolvedName != null) { + return resolvedName; + } + String uppercasedName = name.toUpperCase(); + if (!name.equals(uppercasedName)) { + resolvedName = checkPropertyName(uppercasedName); + if (resolvedName != null) { + return resolvedName; + } + } + return name; + } + + @Nullable + private String checkPropertyName(String name) { + // Check name as-is + if (containsKey(name)) { + return name; + } + // Check name with just dots replaced + String noDotName = name.replace('.', '_'); + if (!name.equals(noDotName) && containsKey(noDotName)) { + return noDotName; + } + // Check name with just hyphens replaced + String noHyphenName = name.replace('-', '_'); + if (!name.equals(noHyphenName) && containsKey(noHyphenName)) { + return noHyphenName; + } + // Check name with dots and hyphens replaced + String noDotNoHyphenName = noDotName.replace('-', '_'); + if (!noDotName.equals(noDotNoHyphenName) && containsKey(noDotNoHyphenName)) { + return noDotNoHyphenName; + } + // Give up + return null; + } + + private boolean containsKey(String name) { + return (isSecurityManagerPresent() ? this.source.keySet().contains(name) : this.source.containsKey(name)); + } + + protected boolean isSecurityManagerPresent() { + return (System.getSecurityManager() != null); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/env/package-info.java b/spring-core/src/main/java/org/springframework/core/env/package-info.java new file mode 100644 index 0000000..a0784c4 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/env/package-info.java @@ -0,0 +1,10 @@ +/** + * Spring's environment abstraction consisting of bean definition + * profile and hierarchical property source support. + */ +@NonNullApi +@NonNullFields +package org.springframework.core.env; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java b/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java new file mode 100644 index 0000000..958b3ba --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java @@ -0,0 +1,316 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.net.URLConnection; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; +import java.nio.file.NoSuchFileException; +import java.nio.file.StandardOpenOption; + +import org.springframework.util.ResourceUtils; + +/** + * Abstract base class for resources which resolve URLs into File references, + * such as {@link UrlResource} or {@link ClassPathResource}. + * + *

    Detects the "file" protocol as well as the JBoss "vfs" protocol in URLs, + * resolving file system references accordingly. + * + * @author Juergen Hoeller + * @since 3.0 + */ +public abstract class AbstractFileResolvingResource extends AbstractResource { + + @Override + public boolean exists() { + try { + URL url = getURL(); + if (ResourceUtils.isFileURL(url)) { + // Proceed with file system resolution + return getFile().exists(); + } + else { + // Try a URL connection content-length header + URLConnection con = url.openConnection(); + customizeConnection(con); + HttpURLConnection httpCon = + (con instanceof HttpURLConnection ? (HttpURLConnection) con : null); + if (httpCon != null) { + int code = httpCon.getResponseCode(); + if (code == HttpURLConnection.HTTP_OK) { + return true; + } + else if (code == HttpURLConnection.HTTP_NOT_FOUND) { + return false; + } + } + if (con.getContentLengthLong() > 0) { + return true; + } + if (httpCon != null) { + // No HTTP OK status, and no content-length header: give up + httpCon.disconnect(); + return false; + } + else { + // Fall back to stream existence: can we open the stream? + getInputStream().close(); + return true; + } + } + } + catch (IOException ex) { + return false; + } + } + + @Override + public boolean isReadable() { + try { + URL url = getURL(); + if (ResourceUtils.isFileURL(url)) { + // Proceed with file system resolution + File file = getFile(); + return (file.canRead() && !file.isDirectory()); + } + else { + // Try InputStream resolution for jar resources + URLConnection con = url.openConnection(); + customizeConnection(con); + if (con instanceof HttpURLConnection) { + HttpURLConnection httpCon = (HttpURLConnection) con; + int code = httpCon.getResponseCode(); + if (code != HttpURLConnection.HTTP_OK) { + httpCon.disconnect(); + return false; + } + } + long contentLength = con.getContentLengthLong(); + if (contentLength > 0) { + return true; + } + else if (contentLength == 0) { + // Empty file or directory -> not considered readable... + return false; + } + else { + // Fall back to stream existence: can we open the stream? + getInputStream().close(); + return true; + } + } + } + catch (IOException ex) { + return false; + } + } + + @Override + public boolean isFile() { + try { + URL url = getURL(); + if (url.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) { + return VfsResourceDelegate.getResource(url).isFile(); + } + return ResourceUtils.URL_PROTOCOL_FILE.equals(url.getProtocol()); + } + catch (IOException ex) { + return false; + } + } + + /** + * This implementation returns a File reference for the underlying class path + * resource, provided that it refers to a file in the file system. + * @see org.springframework.util.ResourceUtils#getFile(java.net.URL, String) + */ + @Override + public File getFile() throws IOException { + URL url = getURL(); + if (url.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) { + return VfsResourceDelegate.getResource(url).getFile(); + } + return ResourceUtils.getFile(url, getDescription()); + } + + /** + * This implementation determines the underlying File + * (or jar file, in case of a resource in a jar/zip). + */ + @Override + protected File getFileForLastModifiedCheck() throws IOException { + URL url = getURL(); + if (ResourceUtils.isJarURL(url)) { + URL actualUrl = ResourceUtils.extractArchiveURL(url); + if (actualUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) { + return VfsResourceDelegate.getResource(actualUrl).getFile(); + } + return ResourceUtils.getFile(actualUrl, "Jar URL"); + } + else { + return getFile(); + } + } + + /** + * This implementation returns a File reference for the given URI-identified + * resource, provided that it refers to a file in the file system. + * @since 5.0 + * @see #getFile(URI) + */ + protected boolean isFile(URI uri) { + try { + if (uri.getScheme().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) { + return VfsResourceDelegate.getResource(uri).isFile(); + } + return ResourceUtils.URL_PROTOCOL_FILE.equals(uri.getScheme()); + } + catch (IOException ex) { + return false; + } + } + + /** + * This implementation returns a File reference for the given URI-identified + * resource, provided that it refers to a file in the file system. + * @see org.springframework.util.ResourceUtils#getFile(java.net.URI, String) + */ + protected File getFile(URI uri) throws IOException { + if (uri.getScheme().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) { + return VfsResourceDelegate.getResource(uri).getFile(); + } + return ResourceUtils.getFile(uri, getDescription()); + } + + /** + * This implementation returns a FileChannel for the given URI-identified + * resource, provided that it refers to a file in the file system. + * @since 5.0 + * @see #getFile() + */ + @Override + public ReadableByteChannel readableChannel() throws IOException { + try { + // Try file system channel + return FileChannel.open(getFile().toPath(), StandardOpenOption.READ); + } + catch (FileNotFoundException | NoSuchFileException ex) { + // Fall back to InputStream adaptation in superclass + return super.readableChannel(); + } + } + + @Override + public long contentLength() throws IOException { + URL url = getURL(); + if (ResourceUtils.isFileURL(url)) { + // Proceed with file system resolution + File file = getFile(); + long length = file.length(); + if (length == 0L && !file.exists()) { + throw new FileNotFoundException(getDescription() + + " cannot be resolved in the file system for checking its content length"); + } + return length; + } + else { + // Try a URL connection content-length header + URLConnection con = url.openConnection(); + customizeConnection(con); + return con.getContentLengthLong(); + } + } + + @Override + public long lastModified() throws IOException { + URL url = getURL(); + boolean fileCheck = false; + if (ResourceUtils.isFileURL(url) || ResourceUtils.isJarURL(url)) { + // Proceed with file system resolution + fileCheck = true; + try { + File fileToCheck = getFileForLastModifiedCheck(); + long lastModified = fileToCheck.lastModified(); + if (lastModified > 0L || fileToCheck.exists()) { + return lastModified; + } + } + catch (FileNotFoundException ex) { + // Defensively fall back to URL connection check instead + } + } + // Try a URL connection last-modified header + URLConnection con = url.openConnection(); + customizeConnection(con); + long lastModified = con.getLastModified(); + if (fileCheck && lastModified == 0 && con.getContentLengthLong() <= 0) { + throw new FileNotFoundException(getDescription() + + " cannot be resolved in the file system for checking its last-modified timestamp"); + } + return lastModified; + } + + /** + * Customize the given {@link URLConnection}, obtained in the course of an + * {@link #exists()}, {@link #contentLength()} or {@link #lastModified()} call. + *

    Calls {@link ResourceUtils#useCachesIfNecessary(URLConnection)} and + * delegates to {@link #customizeConnection(HttpURLConnection)} if possible. + * Can be overridden in subclasses. + * @param con the URLConnection to customize + * @throws IOException if thrown from URLConnection methods + */ + protected void customizeConnection(URLConnection con) throws IOException { + ResourceUtils.useCachesIfNecessary(con); + if (con instanceof HttpURLConnection) { + customizeConnection((HttpURLConnection) con); + } + } + + /** + * Customize the given {@link HttpURLConnection}, obtained in the course of an + * {@link #exists()}, {@link #contentLength()} or {@link #lastModified()} call. + *

    Sets request method "HEAD" by default. Can be overridden in subclasses. + * @param con the HttpURLConnection to customize + * @throws IOException if thrown from HttpURLConnection methods + */ + protected void customizeConnection(HttpURLConnection con) throws IOException { + con.setRequestMethod("HEAD"); + } + + + /** + * Inner delegate class, avoiding a hard JBoss VFS API dependency at runtime. + */ + private static class VfsResourceDelegate { + + public static Resource getResource(URL url) throws IOException { + return new VfsResource(VfsUtils.getRoot(url)); + } + + public static Resource getResource(URI uri) throws IOException { + return new VfsResource(VfsUtils.getRoot(uri)); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/AbstractResource.java b/spring-core/src/main/java/org/springframework/core/io/AbstractResource.java new file mode 100644 index 0000000..de327d7 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/AbstractResource.java @@ -0,0 +1,261 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.NestedIOException; +import org.springframework.lang.Nullable; +import org.springframework.util.ResourceUtils; + +/** + * Convenience base class for {@link Resource} implementations, + * pre-implementing typical behavior. + * + *

    The "exists" method will check whether a File or InputStream can + * be opened; "isOpen" will always return false; "getURL" and "getFile" + * throw an exception; and "toString" will return the description. + * + * @author Juergen Hoeller + * @author Sam Brannen + * @since 28.12.2003 + */ +public abstract class AbstractResource implements Resource { + + /** + * This implementation checks whether a File can be opened, + * falling back to whether an InputStream can be opened. + * This will cover both directories and content resources. + */ + @Override + public boolean exists() { + // Try file existence: can we find the file in the file system? + if (isFile()) { + try { + return getFile().exists(); + } + catch (IOException ex) { + Log logger = LogFactory.getLog(getClass()); + if (logger.isDebugEnabled()) { + logger.debug("Could not retrieve File for existence check of " + getDescription(), ex); + } + } + } + // Fall back to stream existence: can we open the stream? + try { + getInputStream().close(); + return true; + } + catch (Throwable ex) { + Log logger = LogFactory.getLog(getClass()); + if (logger.isDebugEnabled()) { + logger.debug("Could not retrieve InputStream for existence check of " + getDescription(), ex); + } + return false; + } + } + + /** + * This implementation always returns {@code true} for a resource + * that {@link #exists() exists} (revised as of 5.1). + */ + @Override + public boolean isReadable() { + return exists(); + } + + /** + * This implementation always returns {@code false}. + */ + @Override + public boolean isOpen() { + return false; + } + + /** + * This implementation always returns {@code false}. + */ + @Override + public boolean isFile() { + return false; + } + + /** + * This implementation throws a FileNotFoundException, assuming + * that the resource cannot be resolved to a URL. + */ + @Override + public URL getURL() throws IOException { + throw new FileNotFoundException(getDescription() + " cannot be resolved to URL"); + } + + /** + * This implementation builds a URI based on the URL returned + * by {@link #getURL()}. + */ + @Override + public URI getURI() throws IOException { + URL url = getURL(); + try { + return ResourceUtils.toURI(url); + } + catch (URISyntaxException ex) { + throw new NestedIOException("Invalid URI [" + url + "]", ex); + } + } + + /** + * This implementation throws a FileNotFoundException, assuming + * that the resource cannot be resolved to an absolute file path. + */ + @Override + public File getFile() throws IOException { + throw new FileNotFoundException(getDescription() + " cannot be resolved to absolute file path"); + } + + /** + * This implementation returns {@link Channels#newChannel(InputStream)} + * with the result of {@link #getInputStream()}. + *

    This is the same as in {@link Resource}'s corresponding default method + * but mirrored here for efficient JVM-level dispatching in a class hierarchy. + */ + @Override + public ReadableByteChannel readableChannel() throws IOException { + return Channels.newChannel(getInputStream()); + } + + /** + * This method reads the entire InputStream to determine the content length. + *

    For a custom sub-class of {@code InputStreamResource}, we strongly + * recommend overriding this method with a more optimal implementation, e.g. + * checking File length, or possibly simply returning -1 if the stream can + * only be read once. + * @see #getInputStream() + */ + @Override + public long contentLength() throws IOException { + InputStream is = getInputStream(); + try { + long size = 0; + byte[] buf = new byte[256]; + int read; + while ((read = is.read(buf)) != -1) { + size += read; + } + return size; + } + finally { + try { + is.close(); + } + catch (IOException ex) { + Log logger = LogFactory.getLog(getClass()); + if (logger.isDebugEnabled()) { + logger.debug("Could not close content-length InputStream for " + getDescription(), ex); + } + } + } + } + + /** + * This implementation checks the timestamp of the underlying File, + * if available. + * @see #getFileForLastModifiedCheck() + */ + @Override + public long lastModified() throws IOException { + File fileToCheck = getFileForLastModifiedCheck(); + long lastModified = fileToCheck.lastModified(); + if (lastModified == 0L && !fileToCheck.exists()) { + throw new FileNotFoundException(getDescription() + + " cannot be resolved in the file system for checking its last-modified timestamp"); + } + return lastModified; + } + + /** + * Determine the File to use for timestamp checking. + *

    The default implementation delegates to {@link #getFile()}. + * @return the File to use for timestamp checking (never {@code null}) + * @throws FileNotFoundException if the resource cannot be resolved as + * an absolute file path, i.e. is not available in a file system + * @throws IOException in case of general resolution/reading failures + */ + protected File getFileForLastModifiedCheck() throws IOException { + return getFile(); + } + + /** + * This implementation throws a FileNotFoundException, assuming + * that relative resources cannot be created for this resource. + */ + @Override + public Resource createRelative(String relativePath) throws IOException { + throw new FileNotFoundException("Cannot create a relative resource for " + getDescription()); + } + + /** + * This implementation always returns {@code null}, + * assuming that this resource type does not have a filename. + */ + @Override + @Nullable + public String getFilename() { + return null; + } + + + /** + * This implementation compares description strings. + * @see #getDescription() + */ + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof Resource && + ((Resource) other).getDescription().equals(getDescription()))); + } + + /** + * This implementation returns the description's hash code. + * @see #getDescription() + */ + @Override + public int hashCode() { + return getDescription().hashCode(); + } + + /** + * This implementation returns the description of this resource. + * @see #getDescription() + */ + @Override + public String toString() { + return getDescription(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/ByteArrayResource.java b/spring-core/src/main/java/org/springframework/core/io/ByteArrayResource.java new file mode 100644 index 0000000..359d6b8 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/ByteArrayResource.java @@ -0,0 +1,132 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * {@link Resource} implementation for a given byte array. + *

    Creates a {@link ByteArrayInputStream} for the given byte array. + * + *

    Useful for loading content from any given byte array, + * without having to resort to a single-use {@link InputStreamResource}. + * Particularly useful for creating mail attachments from local content, + * where JavaMail needs to be able to read the stream multiple times. + * + * @author Juergen Hoeller + * @author Sam Brannen + * @since 1.2.3 + * @see java.io.ByteArrayInputStream + * @see InputStreamResource + * @see org.springframework.mail.javamail.MimeMessageHelper#addAttachment(String, InputStreamSource) + */ +public class ByteArrayResource extends AbstractResource { + + private final byte[] byteArray; + + private final String description; + + + /** + * Create a new {@code ByteArrayResource}. + * @param byteArray the byte array to wrap + */ + public ByteArrayResource(byte[] byteArray) { + this(byteArray, "resource loaded from byte array"); + } + + /** + * Create a new {@code ByteArrayResource} with a description. + * @param byteArray the byte array to wrap + * @param description where the byte array comes from + */ + public ByteArrayResource(byte[] byteArray, @Nullable String description) { + Assert.notNull(byteArray, "Byte array must not be null"); + this.byteArray = byteArray; + this.description = (description != null ? description : ""); + } + + + /** + * Return the underlying byte array. + */ + public final byte[] getByteArray() { + return this.byteArray; + } + + /** + * This implementation always returns {@code true}. + */ + @Override + public boolean exists() { + return true; + } + + /** + * This implementation returns the length of the underlying byte array. + */ + @Override + public long contentLength() { + return this.byteArray.length; + } + + /** + * This implementation returns a ByteArrayInputStream for the + * underlying byte array. + * @see java.io.ByteArrayInputStream + */ + @Override + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream(this.byteArray); + } + + /** + * This implementation returns a description that includes the passed-in + * {@code description}, if any. + */ + @Override + public String getDescription() { + return "Byte array resource [" + this.description + "]"; + } + + + /** + * This implementation compares the underlying byte array. + * @see java.util.Arrays#equals(byte[], byte[]) + */ + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof ByteArrayResource && + Arrays.equals(((ByteArrayResource) other).byteArray, this.byteArray))); + } + + /** + * This implementation returns the hash code based on the + * underlying byte array. + */ + @Override + public int hashCode() { + return (byte[].class.hashCode() * 29 * this.byteArray.length); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/ClassPathResource.java b/spring-core/src/main/java/org/springframework/core/io/ClassPathResource.java new file mode 100644 index 0000000..c618dfd --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/ClassPathResource.java @@ -0,0 +1,269 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * {@link Resource} implementation for class path resources. Uses either a + * given {@link ClassLoader} or a given {@link Class} for loading resources. + * + *

    Supports resolution as {@code java.io.File} if the class path + * resource resides in the file system, but not for resources in a JAR. + * Always supports resolution as URL. + * + * @author Juergen Hoeller + * @author Sam Brannen + * @since 28.12.2003 + * @see ClassLoader#getResourceAsStream(String) + * @see Class#getResourceAsStream(String) + */ +public class ClassPathResource extends AbstractFileResolvingResource { + + private final String path; + + @Nullable + private ClassLoader classLoader; + + @Nullable + private Class clazz; + + + /** + * Create a new {@code ClassPathResource} for {@code ClassLoader} usage. + * A leading slash will be removed, as the ClassLoader resource access + * methods will not accept it. + *

    The thread context class loader will be used for + * loading the resource. + * @param path the absolute path within the class path + * @see java.lang.ClassLoader#getResourceAsStream(String) + * @see org.springframework.util.ClassUtils#getDefaultClassLoader() + */ + public ClassPathResource(String path) { + this(path, (ClassLoader) null); + } + + /** + * Create a new {@code ClassPathResource} for {@code ClassLoader} usage. + * A leading slash will be removed, as the ClassLoader resource access + * methods will not accept it. + * @param path the absolute path within the classpath + * @param classLoader the class loader to load the resource with, + * or {@code null} for the thread context class loader + * @see ClassLoader#getResourceAsStream(String) + */ + public ClassPathResource(String path, @Nullable ClassLoader classLoader) { + Assert.notNull(path, "Path must not be null"); + String pathToUse = StringUtils.cleanPath(path); + if (pathToUse.startsWith("/")) { + pathToUse = pathToUse.substring(1); + } + this.path = pathToUse; + this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader()); + } + + /** + * Create a new {@code ClassPathResource} for {@code Class} usage. + * The path can be relative to the given class, or absolute within + * the classpath via a leading slash. + * @param path relative or absolute path within the class path + * @param clazz the class to load resources with + * @see java.lang.Class#getResourceAsStream + */ + public ClassPathResource(String path, @Nullable Class clazz) { + Assert.notNull(path, "Path must not be null"); + this.path = StringUtils.cleanPath(path); + this.clazz = clazz; + } + + /** + * Create a new {@code ClassPathResource} with optional {@code ClassLoader} + * and {@code Class}. Only for internal usage. + * @param path relative or absolute path within the classpath + * @param classLoader the class loader to load the resource with, if any + * @param clazz the class to load resources with, if any + * @deprecated as of 4.3.13, in favor of selective use of + * {@link #ClassPathResource(String, ClassLoader)} vs {@link #ClassPathResource(String, Class)} + */ + @Deprecated + protected ClassPathResource(String path, @Nullable ClassLoader classLoader, @Nullable Class clazz) { + this.path = StringUtils.cleanPath(path); + this.classLoader = classLoader; + this.clazz = clazz; + } + + + /** + * Return the path for this resource (as resource path within the class path). + */ + public final String getPath() { + return this.path; + } + + /** + * Return the ClassLoader that this resource will be obtained from. + */ + @Nullable + public final ClassLoader getClassLoader() { + return (this.clazz != null ? this.clazz.getClassLoader() : this.classLoader); + } + + + /** + * This implementation checks for the resolution of a resource URL. + * @see java.lang.ClassLoader#getResource(String) + * @see java.lang.Class#getResource(String) + */ + @Override + public boolean exists() { + return (resolveURL() != null); + } + + /** + * Resolves a URL for the underlying class path resource. + * @return the resolved URL, or {@code null} if not resolvable + */ + @Nullable + protected URL resolveURL() { + if (this.clazz != null) { + return this.clazz.getResource(this.path); + } + else if (this.classLoader != null) { + return this.classLoader.getResource(this.path); + } + else { + return ClassLoader.getSystemResource(this.path); + } + } + + /** + * This implementation opens an InputStream for the given class path resource. + * @see java.lang.ClassLoader#getResourceAsStream(String) + * @see java.lang.Class#getResourceAsStream(String) + */ + @Override + public InputStream getInputStream() throws IOException { + InputStream is; + if (this.clazz != null) { + is = this.clazz.getResourceAsStream(this.path); + } + else if (this.classLoader != null) { + is = this.classLoader.getResourceAsStream(this.path); + } + else { + is = ClassLoader.getSystemResourceAsStream(this.path); + } + if (is == null) { + throw new FileNotFoundException(getDescription() + " cannot be opened because it does not exist"); + } + return is; + } + + /** + * This implementation returns a URL for the underlying class path resource, + * if available. + * @see java.lang.ClassLoader#getResource(String) + * @see java.lang.Class#getResource(String) + */ + @Override + public URL getURL() throws IOException { + URL url = resolveURL(); + if (url == null) { + throw new FileNotFoundException(getDescription() + " cannot be resolved to URL because it does not exist"); + } + return url; + } + + /** + * This implementation creates a ClassPathResource, applying the given path + * relative to the path of the underlying resource of this descriptor. + * @see org.springframework.util.StringUtils#applyRelativePath(String, String) + */ + @Override + public Resource createRelative(String relativePath) { + String pathToUse = StringUtils.applyRelativePath(this.path, relativePath); + return (this.clazz != null ? new ClassPathResource(pathToUse, this.clazz) : + new ClassPathResource(pathToUse, this.classLoader)); + } + + /** + * This implementation returns the name of the file that this class path + * resource refers to. + * @see org.springframework.util.StringUtils#getFilename(String) + */ + @Override + @Nullable + public String getFilename() { + return StringUtils.getFilename(this.path); + } + + /** + * This implementation returns a description that includes the class path location. + */ + @Override + public String getDescription() { + StringBuilder builder = new StringBuilder("class path resource ["); + String pathToUse = this.path; + if (this.clazz != null && !pathToUse.startsWith("/")) { + builder.append(ClassUtils.classPackageAsResourcePath(this.clazz)); + builder.append('/'); + } + if (pathToUse.startsWith("/")) { + pathToUse = pathToUse.substring(1); + } + builder.append(pathToUse); + builder.append(']'); + return builder.toString(); + } + + + /** + * This implementation compares the underlying class path locations. + */ + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof ClassPathResource)) { + return false; + } + ClassPathResource otherRes = (ClassPathResource) other; + return (this.path.equals(otherRes.path) && + ObjectUtils.nullSafeEquals(this.classLoader, otherRes.classLoader) && + ObjectUtils.nullSafeEquals(this.clazz, otherRes.clazz)); + } + + /** + * This implementation returns the hash code of the underlying + * class path location. + */ + @Override + public int hashCode() { + return this.path.hashCode(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/ClassRelativeResourceLoader.java b/spring-core/src/main/java/org/springframework/core/io/ClassRelativeResourceLoader.java new file mode 100644 index 0000000..ddda6d4 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/ClassRelativeResourceLoader.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * {@link ResourceLoader} implementation that interprets plain resource paths + * as relative to a given {@code java.lang.Class}. + * + * @author Juergen Hoeller + * @since 3.0 + * @see Class#getResource(String) + * @see ClassPathResource#ClassPathResource(String, Class) + */ +public class ClassRelativeResourceLoader extends DefaultResourceLoader { + + private final Class clazz; + + + /** + * Create a new ClassRelativeResourceLoader for the given class. + * @param clazz the class to load resources through + */ + public ClassRelativeResourceLoader(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + this.clazz = clazz; + setClassLoader(clazz.getClassLoader()); + } + + @Override + protected Resource getResourceByPath(String path) { + return new ClassRelativeContextResource(path, this.clazz); + } + + + /** + * ClassPathResource that explicitly expresses a context-relative path + * through implementing the ContextResource interface. + */ + private static class ClassRelativeContextResource extends ClassPathResource implements ContextResource { + + private final Class clazz; + + public ClassRelativeContextResource(String path, Class clazz) { + super(path, clazz); + this.clazz = clazz; + } + + @Override + public String getPathWithinContext() { + return getPath(); + } + + @Override + public Resource createRelative(String relativePath) { + String pathToUse = StringUtils.applyRelativePath(getPath(), relativePath); + return new ClassRelativeContextResource(pathToUse, this.clazz); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/ContextResource.java b/spring-core/src/main/java/org/springframework/core/io/ContextResource.java new file mode 100644 index 0000000..f5df629 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/ContextResource.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2007 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io; + +/** + * Extended interface for a resource that is loaded from an enclosing + * 'context', e.g. from a {@link javax.servlet.ServletContext} but also + * from plain classpath paths or relative file system paths (specified + * without an explicit prefix, hence applying relative to the local + * {@link ResourceLoader}'s context). + * + * @author Juergen Hoeller + * @since 2.5 + * @see org.springframework.web.context.support.ServletContextResource + */ +public interface ContextResource extends Resource { + + /** + * Return the path within the enclosing 'context'. + *

    This is typically path relative to a context-specific root directory, + * e.g. a ServletContext root or a PortletContext root. + */ + String getPathWithinContext(); + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/DefaultResourceLoader.java b/spring-core/src/main/java/org/springframework/core/io/DefaultResourceLoader.java new file mode 100644 index 0000000..27ed3bf --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/DefaultResourceLoader.java @@ -0,0 +1,211 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ResourceUtils; +import org.springframework.util.StringUtils; + +/** + * Default implementation of the {@link ResourceLoader} interface. + * Used by {@link ResourceEditor}, and serves as base class for + * {@link org.springframework.context.support.AbstractApplicationContext}. + * Can also be used standalone. + * + *

    Will return a {@link UrlResource} if the location value is a URL, + * and a {@link ClassPathResource} if it is a non-URL path or a + * "classpath:" pseudo-URL. + * + * @author Juergen Hoeller + * @since 10.03.2004 + * @see FileSystemResourceLoader + * @see org.springframework.context.support.ClassPathXmlApplicationContext + */ +public class DefaultResourceLoader implements ResourceLoader { + + @Nullable + private ClassLoader classLoader; + + private final Set protocolResolvers = new LinkedHashSet<>(4); + + private final Map, Map> resourceCaches = new ConcurrentHashMap<>(4); + + + /** + * Create a new DefaultResourceLoader. + *

    ClassLoader access will happen using the thread context class loader + * at the time of actual resource access (since 5.3). For more control, pass + * a specific ClassLoader to {@link #DefaultResourceLoader(ClassLoader)}. + * @see java.lang.Thread#getContextClassLoader() + */ + public DefaultResourceLoader() { + } + + /** + * Create a new DefaultResourceLoader. + * @param classLoader the ClassLoader to load class path resources with, or {@code null} + * for using the thread context class loader at the time of actual resource access + */ + public DefaultResourceLoader(@Nullable ClassLoader classLoader) { + this.classLoader = classLoader; + } + + + /** + * Specify the ClassLoader to load class path resources with, or {@code null} + * for using the thread context class loader at the time of actual resource access. + *

    The default is that ClassLoader access will happen using the thread context + * class loader at the time of actual resource access (since 5.3). + */ + public void setClassLoader(@Nullable ClassLoader classLoader) { + this.classLoader = classLoader; + } + + /** + * Return the ClassLoader to load class path resources with. + *

    Will get passed to ClassPathResource's constructor for all + * ClassPathResource objects created by this resource loader. + * @see ClassPathResource + */ + @Override + @Nullable + public ClassLoader getClassLoader() { + return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader()); + } + + /** + * Register the given resolver with this resource loader, allowing for + * additional protocols to be handled. + *

    Any such resolver will be invoked ahead of this loader's standard + * resolution rules. It may therefore also override any default rules. + * @since 4.3 + * @see #getProtocolResolvers() + */ + public void addProtocolResolver(ProtocolResolver resolver) { + Assert.notNull(resolver, "ProtocolResolver must not be null"); + this.protocolResolvers.add(resolver); + } + + /** + * Return the collection of currently registered protocol resolvers, + * allowing for introspection as well as modification. + * @since 4.3 + */ + public Collection getProtocolResolvers() { + return this.protocolResolvers; + } + + /** + * Obtain a cache for the given value type, keyed by {@link Resource}. + * @param valueType the value type, e.g. an ASM {@code MetadataReader} + * @return the cache {@link Map}, shared at the {@code ResourceLoader} level + * @since 5.0 + */ + @SuppressWarnings("unchecked") + public Map getResourceCache(Class valueType) { + return (Map) this.resourceCaches.computeIfAbsent(valueType, key -> new ConcurrentHashMap<>()); + } + + /** + * Clear all resource caches in this resource loader. + * @since 5.0 + * @see #getResourceCache + */ + public void clearResourceCaches() { + this.resourceCaches.clear(); + } + + + @Override + public Resource getResource(String location) { + Assert.notNull(location, "Location must not be null"); + + for (ProtocolResolver protocolResolver : getProtocolResolvers()) { + Resource resource = protocolResolver.resolve(location, this); + if (resource != null) { + return resource; + } + } + + if (location.startsWith("/")) { + return getResourceByPath(location); + } + else if (location.startsWith(CLASSPATH_URL_PREFIX)) { + return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader()); + } + else { + try { + // Try to parse the location as a URL... + URL url = new URL(location); + return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url)); + } + catch (MalformedURLException ex) { + // No URL -> resolve as resource path. + return getResourceByPath(location); + } + } + } + + /** + * Return a Resource handle for the resource at the given path. + *

    The default implementation supports class path locations. This should + * be appropriate for standalone implementations but can be overridden, + * e.g. for implementations targeted at a Servlet container. + * @param path the path to the resource + * @return the corresponding Resource handle + * @see ClassPathResource + * @see org.springframework.context.support.FileSystemXmlApplicationContext#getResourceByPath + * @see org.springframework.web.context.support.XmlWebApplicationContext#getResourceByPath + */ + protected Resource getResourceByPath(String path) { + return new ClassPathContextResource(path, getClassLoader()); + } + + + /** + * ClassPathResource that explicitly expresses a context-relative path + * through implementing the ContextResource interface. + */ + protected static class ClassPathContextResource extends ClassPathResource implements ContextResource { + + public ClassPathContextResource(String path, @Nullable ClassLoader classLoader) { + super(path, classLoader); + } + + @Override + public String getPathWithinContext() { + return getPath(); + } + + @Override + public Resource createRelative(String relativePath) { + String pathToUse = StringUtils.applyRelativePath(getPath(), relativePath); + return new ClassPathContextResource(pathToUse, getClassLoader()); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/DescriptiveResource.java b/spring-core/src/main/java/org/springframework/core/io/DescriptiveResource.java new file mode 100644 index 0000000..3082732 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/DescriptiveResource.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +import org.springframework.lang.Nullable; + +/** + * Simple {@link Resource} implementation that holds a resource description + * but does not point to an actually readable resource. + * + *

    To be used as placeholder if a {@code Resource} argument is + * expected by an API but not necessarily used for actual reading. + * + * @author Juergen Hoeller + * @since 1.2.6 + */ +public class DescriptiveResource extends AbstractResource { + + private final String description; + + + /** + * Create a new DescriptiveResource. + * @param description the resource description + */ + public DescriptiveResource(@Nullable String description) { + this.description = (description != null ? description : ""); + } + + + @Override + public boolean exists() { + return false; + } + + @Override + public boolean isReadable() { + return false; + } + + @Override + public InputStream getInputStream() throws IOException { + throw new FileNotFoundException( + getDescription() + " cannot be opened because it does not point to a readable resource"); + } + + @Override + public String getDescription() { + return this.description; + } + + + /** + * This implementation compares the underlying description String. + */ + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof DescriptiveResource && + ((DescriptiveResource) other).description.equals(this.description))); + } + + /** + * This implementation returns the hash code of the underlying description String. + */ + @Override + public int hashCode() { + return this.description.hashCode(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/FileSystemResource.java b/spring-core/src/main/java/org/springframework/core/io/FileSystemResource.java new file mode 100644 index 0000000..d0f7d39 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/FileSystemResource.java @@ -0,0 +1,361 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.URL; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * {@link Resource} implementation for {@code java.io.File} and + * {@code java.nio.file.Path} handles with a file system target. + * Supports resolution as a {@code File} and also as a {@code URL}. + * Implements the extended {@link WritableResource} interface. + * + *

    Note: As of Spring Framework 5.0, this {@link Resource} implementation uses + * NIO.2 API for read/write interactions. As of 5.1, it may be constructed with a + * {@link java.nio.file.Path} handle in which case it will perform all file system + * interactions via NIO.2, only resorting to {@link File} on {@link #getFile()}. + * + * @author Juergen Hoeller + * @since 28.12.2003 + * @see #FileSystemResource(String) + * @see #FileSystemResource(File) + * @see #FileSystemResource(Path) + * @see java.io.File + * @see java.nio.file.Files + */ +public class FileSystemResource extends AbstractResource implements WritableResource { + + private final String path; + + @Nullable + private final File file; + + private final Path filePath; + + + /** + * Create a new {@code FileSystemResource} from a file path. + *

    Note: When building relative resources via {@link #createRelative}, + * it makes a difference whether the specified resource base path here + * ends with a slash or not. In the case of "C:/dir1/", relative paths + * will be built underneath that root: e.g. relative path "dir2" -> + * "C:/dir1/dir2". In the case of "C:/dir1", relative paths will apply + * at the same directory level: relative path "dir2" -> "C:/dir2". + * @param path a file path + * @see #FileSystemResource(Path) + */ + public FileSystemResource(String path) { + Assert.notNull(path, "Path must not be null"); + this.path = StringUtils.cleanPath(path); + this.file = new File(path); + this.filePath = this.file.toPath(); + } + + /** + * Create a new {@code FileSystemResource} from a {@link File} handle. + *

    Note: When building relative resources via {@link #createRelative}, + * the relative path will apply at the same directory level: + * e.g. new File("C:/dir1"), relative path "dir2" -> "C:/dir2"! + * If you prefer to have relative paths built underneath the given root directory, + * use the {@link #FileSystemResource(String) constructor with a file path} + * to append a trailing slash to the root path: "C:/dir1/", which indicates + * this directory as root for all relative paths. + * @param file a File handle + * @see #FileSystemResource(Path) + * @see #getFile() + */ + public FileSystemResource(File file) { + Assert.notNull(file, "File must not be null"); + this.path = StringUtils.cleanPath(file.getPath()); + this.file = file; + this.filePath = file.toPath(); + } + + /** + * Create a new {@code FileSystemResource} from a {@link Path} handle, + * performing all file system interactions via NIO.2 instead of {@link File}. + *

    In contrast to {@link PathResource}, this variant strictly follows the + * general {@link FileSystemResource} conventions, in particular in terms of + * path cleaning and {@link #createRelative(String)} handling. + *

    Note: When building relative resources via {@link #createRelative}, + * the relative path will apply at the same directory level: + * e.g. Paths.get("C:/dir1"), relative path "dir2" -> "C:/dir2"! + * If you prefer to have relative paths built underneath the given root directory, + * use the {@link #FileSystemResource(String) constructor with a file path} + * to append a trailing slash to the root path: "C:/dir1/", which indicates + * this directory as root for all relative paths. Alternatively, consider + * using {@link PathResource#PathResource(Path)} for {@code java.nio.path.Path} + * resolution in {@code createRelative}, always nesting relative paths. + * @param filePath a Path handle to a file + * @since 5.1 + * @see #FileSystemResource(File) + */ + public FileSystemResource(Path filePath) { + Assert.notNull(filePath, "Path must not be null"); + this.path = StringUtils.cleanPath(filePath.toString()); + this.file = null; + this.filePath = filePath; + } + + /** + * Create a new {@code FileSystemResource} from a {@link FileSystem} handle, + * locating the specified path. + *

    This is an alternative to {@link #FileSystemResource(String)}, + * performing all file system interactions via NIO.2 instead of {@link File}. + * @param fileSystem the FileSystem to locate the path within + * @param path a file path + * @since 5.1.1 + * @see #FileSystemResource(File) + */ + public FileSystemResource(FileSystem fileSystem, String path) { + Assert.notNull(fileSystem, "FileSystem must not be null"); + Assert.notNull(path, "Path must not be null"); + this.path = StringUtils.cleanPath(path); + this.file = null; + this.filePath = fileSystem.getPath(this.path).normalize(); + } + + + /** + * Return the file path for this resource. + */ + public final String getPath() { + return this.path; + } + + /** + * This implementation returns whether the underlying file exists. + * @see java.io.File#exists() + */ + @Override + public boolean exists() { + return (this.file != null ? this.file.exists() : Files.exists(this.filePath)); + } + + /** + * This implementation checks whether the underlying file is marked as readable + * (and corresponds to an actual file with content, not to a directory). + * @see java.io.File#canRead() + * @see java.io.File#isDirectory() + */ + @Override + public boolean isReadable() { + return (this.file != null ? this.file.canRead() && !this.file.isDirectory() : + Files.isReadable(this.filePath) && !Files.isDirectory(this.filePath)); + } + + /** + * This implementation opens a NIO file stream for the underlying file. + * @see java.io.FileInputStream + */ + @Override + public InputStream getInputStream() throws IOException { + try { + return Files.newInputStream(this.filePath); + } + catch (NoSuchFileException ex) { + throw new FileNotFoundException(ex.getMessage()); + } + } + + /** + * This implementation checks whether the underlying file is marked as writable + * (and corresponds to an actual file with content, not to a directory). + * @see java.io.File#canWrite() + * @see java.io.File#isDirectory() + */ + @Override + public boolean isWritable() { + return (this.file != null ? this.file.canWrite() && !this.file.isDirectory() : + Files.isWritable(this.filePath) && !Files.isDirectory(this.filePath)); + } + + /** + * This implementation opens a FileOutputStream for the underlying file. + * @see java.io.FileOutputStream + */ + @Override + public OutputStream getOutputStream() throws IOException { + return Files.newOutputStream(this.filePath); + } + + /** + * This implementation returns a URL for the underlying file. + * @see java.io.File#toURI() + */ + @Override + public URL getURL() throws IOException { + return (this.file != null ? this.file.toURI().toURL() : this.filePath.toUri().toURL()); + } + + /** + * This implementation returns a URI for the underlying file. + * @see java.io.File#toURI() + */ + @Override + public URI getURI() throws IOException { + return (this.file != null ? this.file.toURI() : this.filePath.toUri()); + } + + /** + * This implementation always indicates a file. + */ + @Override + public boolean isFile() { + return true; + } + + /** + * This implementation returns the underlying File reference. + */ + @Override + public File getFile() { + return (this.file != null ? this.file : this.filePath.toFile()); + } + + /** + * This implementation opens a FileChannel for the underlying file. + * @see java.nio.channels.FileChannel + */ + @Override + public ReadableByteChannel readableChannel() throws IOException { + try { + return FileChannel.open(this.filePath, StandardOpenOption.READ); + } + catch (NoSuchFileException ex) { + throw new FileNotFoundException(ex.getMessage()); + } + } + + /** + * This implementation opens a FileChannel for the underlying file. + * @see java.nio.channels.FileChannel + */ + @Override + public WritableByteChannel writableChannel() throws IOException { + return FileChannel.open(this.filePath, StandardOpenOption.WRITE); + } + + /** + * This implementation returns the underlying File/Path length. + */ + @Override + public long contentLength() throws IOException { + if (this.file != null) { + long length = this.file.length(); + if (length == 0L && !this.file.exists()) { + throw new FileNotFoundException(getDescription() + + " cannot be resolved in the file system for checking its content length"); + } + return length; + } + else { + try { + return Files.size(this.filePath); + } + catch (NoSuchFileException ex) { + throw new FileNotFoundException(ex.getMessage()); + } + } + } + + /** + * This implementation returns the underlying File/Path last-modified time. + */ + @Override + public long lastModified() throws IOException { + if (this.file != null) { + return super.lastModified(); + } + else { + try { + return Files.getLastModifiedTime(this.filePath).toMillis(); + } + catch (NoSuchFileException ex) { + throw new FileNotFoundException(ex.getMessage()); + } + } + } + + /** + * This implementation creates a FileSystemResource, applying the given path + * relative to the path of the underlying file of this resource descriptor. + * @see org.springframework.util.StringUtils#applyRelativePath(String, String) + */ + @Override + public Resource createRelative(String relativePath) { + String pathToUse = StringUtils.applyRelativePath(this.path, relativePath); + return (this.file != null ? new FileSystemResource(pathToUse) : + new FileSystemResource(this.filePath.getFileSystem(), pathToUse)); + } + + /** + * This implementation returns the name of the file. + * @see java.io.File#getName() + */ + @Override + public String getFilename() { + return (this.file != null ? this.file.getName() : this.filePath.getFileName().toString()); + } + + /** + * This implementation returns a description that includes the absolute + * path of the file. + * @see java.io.File#getAbsolutePath() + */ + @Override + public String getDescription() { + return "file [" + (this.file != null ? this.file.getAbsolutePath() : this.filePath.toAbsolutePath()) + "]"; + } + + + /** + * This implementation compares the underlying File references. + */ + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof FileSystemResource && + this.path.equals(((FileSystemResource) other).path))); + } + + /** + * This implementation returns the hash code of the underlying File reference. + */ + @Override + public int hashCode() { + return this.path.hashCode(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/FileSystemResourceLoader.java b/spring-core/src/main/java/org/springframework/core/io/FileSystemResourceLoader.java new file mode 100644 index 0000000..1cf38e3 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/FileSystemResourceLoader.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io; + +/** + * {@link ResourceLoader} implementation that resolves plain paths as + * file system resources rather than as class path resources + * (the latter is {@link DefaultResourceLoader}'s default strategy). + * + *

    NOTE: Plain paths will always be interpreted as relative + * to the current VM working directory, even if they start with a slash. + * (This is consistent with the semantics in a Servlet container.) + * Use an explicit "file:" prefix to enforce an absolute file path. + * + *

    {@link org.springframework.context.support.FileSystemXmlApplicationContext} + * is a full-fledged ApplicationContext implementation that provides + * the same resource path resolution strategy. + * + * @author Juergen Hoeller + * @since 1.1.3 + * @see DefaultResourceLoader + * @see org.springframework.context.support.FileSystemXmlApplicationContext + */ +public class FileSystemResourceLoader extends DefaultResourceLoader { + + /** + * Resolve resource paths as file system paths. + *

    Note: Even if a given path starts with a slash, it will get + * interpreted as relative to the current VM working directory. + * @param path the path to the resource + * @return the corresponding Resource handle + * @see FileSystemResource + * @see org.springframework.web.context.support.ServletContextResourceLoader#getResourceByPath + */ + @Override + protected Resource getResourceByPath(String path) { + if (path.startsWith("/")) { + path = path.substring(1); + } + return new FileSystemContextResource(path); + } + + + /** + * FileSystemResource that explicitly expresses a context-relative path + * through implementing the ContextResource interface. + */ + private static class FileSystemContextResource extends FileSystemResource implements ContextResource { + + public FileSystemContextResource(String path) { + super(path); + } + + @Override + public String getPathWithinContext() { + return getPath(); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/FileUrlResource.java b/spring-core/src/main/java/org/springframework/core/io/FileUrlResource.java new file mode 100644 index 0000000..b910bbc --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/FileUrlResource.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.channels.FileChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; + +import org.springframework.lang.Nullable; +import org.springframework.util.ResourceUtils; + +/** + * Subclass of {@link UrlResource} which assumes file resolution, to the degree + * of implementing the {@link WritableResource} interface for it. This resource + * variant also caches resolved {@link File} handles from {@link #getFile()}. + * + *

    This is the class resolved by {@link DefaultResourceLoader} for a "file:..." + * URL location, allowing a downcast to {@link WritableResource} for it. + * + *

    Alternatively, for direct construction from a {@link java.io.File} handle + * or NIO {@link java.nio.file.Path}, consider using {@link FileSystemResource}. + * + * @author Juergen Hoeller + * @since 5.0.2 + */ +public class FileUrlResource extends UrlResource implements WritableResource { + + @Nullable + private volatile File file; + + + /** + * Create a new {@code FileUrlResource} based on the given URL object. + *

    Note that this does not enforce "file" as URL protocol. If a protocol + * is known to be resolvable to a file, it is acceptable for this purpose. + * @param url a URL + * @see ResourceUtils#isFileURL(URL) + * @see #getFile() + */ + public FileUrlResource(URL url) { + super(url); + } + + /** + * Create a new {@code FileUrlResource} based on the given file location, + * using the URL protocol "file". + *

    The given parts will automatically get encoded if necessary. + * @param location the location (i.e. the file path within that protocol) + * @throws MalformedURLException if the given URL specification is not valid + * @see UrlResource#UrlResource(String, String) + * @see ResourceUtils#URL_PROTOCOL_FILE + */ + public FileUrlResource(String location) throws MalformedURLException { + super(ResourceUtils.URL_PROTOCOL_FILE, location); + } + + + @Override + public File getFile() throws IOException { + File file = this.file; + if (file != null) { + return file; + } + file = super.getFile(); + this.file = file; + return file; + } + + @Override + public boolean isWritable() { + try { + File file = getFile(); + return (file.canWrite() && !file.isDirectory()); + } + catch (IOException ex) { + return false; + } + } + + @Override + public OutputStream getOutputStream() throws IOException { + return Files.newOutputStream(getFile().toPath()); + } + + @Override + public WritableByteChannel writableChannel() throws IOException { + return FileChannel.open(getFile().toPath(), StandardOpenOption.WRITE); + } + + @Override + public Resource createRelative(String relativePath) throws MalformedURLException { + return new FileUrlResource(createRelativeURL(relativePath)); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/InputStreamResource.java b/spring-core/src/main/java/org/springframework/core/io/InputStreamResource.java new file mode 100644 index 0000000..5a5eaa9 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/InputStreamResource.java @@ -0,0 +1,131 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io; + +import java.io.IOException; +import java.io.InputStream; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * {@link Resource} implementation for a given {@link InputStream}. + *

    Should only be used if no other specific {@code Resource} implementation + * is applicable. In particular, prefer {@link ByteArrayResource} or any of the + * file-based {@code Resource} implementations where possible. + * + *

    In contrast to other {@code Resource} implementations, this is a descriptor + * for an already opened resource - therefore returning {@code true} from + * {@link #isOpen()}. Do not use an {@code InputStreamResource} if you need to + * keep the resource descriptor somewhere, or if you need to read from a stream + * multiple times. + * + * @author Juergen Hoeller + * @author Sam Brannen + * @since 28.12.2003 + * @see ByteArrayResource + * @see ClassPathResource + * @see FileSystemResource + * @see UrlResource + */ +public class InputStreamResource extends AbstractResource { + + private final InputStream inputStream; + + private final String description; + + private boolean read = false; + + + /** + * Create a new InputStreamResource. + * @param inputStream the InputStream to use + */ + public InputStreamResource(InputStream inputStream) { + this(inputStream, "resource loaded through InputStream"); + } + + /** + * Create a new InputStreamResource. + * @param inputStream the InputStream to use + * @param description where the InputStream comes from + */ + public InputStreamResource(InputStream inputStream, @Nullable String description) { + Assert.notNull(inputStream, "InputStream must not be null"); + this.inputStream = inputStream; + this.description = (description != null ? description : ""); + } + + + /** + * This implementation always returns {@code true}. + */ + @Override + public boolean exists() { + return true; + } + + /** + * This implementation always returns {@code true}. + */ + @Override + public boolean isOpen() { + return true; + } + + /** + * This implementation throws IllegalStateException if attempting to + * read the underlying stream multiple times. + */ + @Override + public InputStream getInputStream() throws IOException, IllegalStateException { + if (this.read) { + throw new IllegalStateException("InputStream has already been read - " + + "do not use InputStreamResource if a stream needs to be read multiple times"); + } + this.read = true; + return this.inputStream; + } + + /** + * This implementation returns a description that includes the passed-in + * description, if any. + */ + @Override + public String getDescription() { + return "InputStream resource [" + this.description + "]"; + } + + + /** + * This implementation compares the underlying InputStream. + */ + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof InputStreamResource && + ((InputStreamResource) other).inputStream.equals(this.inputStream))); + } + + /** + * This implementation returns the hash code of the underlying InputStream. + */ + @Override + public int hashCode() { + return this.inputStream.hashCode(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/InputStreamSource.java b/spring-core/src/main/java/org/springframework/core/io/InputStreamSource.java new file mode 100644 index 0000000..b769895 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/InputStreamSource.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Simple interface for objects that are sources for an {@link InputStream}. + * + *

    This is the base interface for Spring's more extensive {@link Resource} interface. + * + *

    For single-use streams, {@link InputStreamResource} can be used for any + * given {@code InputStream}. Spring's {@link ByteArrayResource} or any + * file-based {@code Resource} implementation can be used as a concrete + * instance, allowing one to read the underlying content stream multiple times. + * This makes this interface useful as an abstract content source for mail + * attachments, for example. + * + * @author Juergen Hoeller + * @since 20.01.2004 + * @see java.io.InputStream + * @see Resource + * @see InputStreamResource + * @see ByteArrayResource + */ +public interface InputStreamSource { + + /** + * Return an {@link InputStream} for the content of an underlying resource. + *

    It is expected that each call creates a fresh stream. + *

    This requirement is particularly important when you consider an API such + * as JavaMail, which needs to be able to read the stream multiple times when + * creating mail attachments. For such a use case, it is required + * that each {@code getInputStream()} call returns a fresh stream. + * @return the input stream for the underlying resource (must not be {@code null}) + * @throws java.io.FileNotFoundException if the underlying resource doesn't exist + * @throws IOException if the content stream could not be opened + */ + InputStream getInputStream() throws IOException; + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/PathResource.java b/spring-core/src/main/java/org/springframework/core/io/PathResource.java new file mode 100644 index 0000000..bd334cc --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/PathResource.java @@ -0,0 +1,293 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.URL; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * {@link Resource} implementation for {@link java.nio.file.Path} handles, + * performing all operations and transformations via the {@code Path} API. + * Supports resolution as a {@link File} and also as a {@link URL}. + * Implements the extended {@link WritableResource} interface. + * + *

    Note: As of 5.1, {@link java.nio.file.Path} support is also available + * in {@link FileSystemResource#FileSystemResource(Path) FileSystemResource}, + * applying Spring's standard String-based path transformations but + * performing all operations via the {@link java.nio.file.Files} API. + * This {@code PathResource} is effectively a pure {@code java.nio.path.Path} + * based alternative with different {@code createRelative} behavior. + * + * @author Philippe Marschall + * @author Juergen Hoeller + * @since 4.0 + * @see java.nio.file.Path + * @see java.nio.file.Files + * @see FileSystemResource + */ +public class PathResource extends AbstractResource implements WritableResource { + + private final Path path; + + + /** + * Create a new PathResource from a Path handle. + *

    Note: Unlike {@link FileSystemResource}, when building relative resources + * via {@link #createRelative}, the relative path will be built underneath + * the given root: e.g. Paths.get("C:/dir1/"), relative path "dir2" -> "C:/dir1/dir2"! + * @param path a Path handle + */ + public PathResource(Path path) { + Assert.notNull(path, "Path must not be null"); + this.path = path.normalize(); + } + + /** + * Create a new PathResource from a Path handle. + *

    Note: Unlike {@link FileSystemResource}, when building relative resources + * via {@link #createRelative}, the relative path will be built underneath + * the given root: e.g. Paths.get("C:/dir1/"), relative path "dir2" -> "C:/dir1/dir2"! + * @param path a path + * @see java.nio.file.Paths#get(String, String...) + */ + public PathResource(String path) { + Assert.notNull(path, "Path must not be null"); + this.path = Paths.get(path).normalize(); + } + + /** + * Create a new PathResource from a Path handle. + *

    Note: Unlike {@link FileSystemResource}, when building relative resources + * via {@link #createRelative}, the relative path will be built underneath + * the given root: e.g. Paths.get("C:/dir1/"), relative path "dir2" -> "C:/dir1/dir2"! + * @param uri a path URI + * @see java.nio.file.Paths#get(URI) + */ + public PathResource(URI uri) { + Assert.notNull(uri, "URI must not be null"); + this.path = Paths.get(uri).normalize(); + } + + + /** + * Return the file path for this resource. + */ + public final String getPath() { + return this.path.toString(); + } + + /** + * This implementation returns whether the underlying file exists. + * @see java.nio.file.Files#exists(Path, java.nio.file.LinkOption...) + */ + @Override + public boolean exists() { + return Files.exists(this.path); + } + + /** + * This implementation checks whether the underlying file is marked as readable + * (and corresponds to an actual file with content, not to a directory). + * @see java.nio.file.Files#isReadable(Path) + * @see java.nio.file.Files#isDirectory(Path, java.nio.file.LinkOption...) + */ + @Override + public boolean isReadable() { + return (Files.isReadable(this.path) && !Files.isDirectory(this.path)); + } + + /** + * This implementation opens a InputStream for the underlying file. + * @see java.nio.file.spi.FileSystemProvider#newInputStream(Path, OpenOption...) + */ + @Override + public InputStream getInputStream() throws IOException { + if (!exists()) { + throw new FileNotFoundException(getPath() + " (no such file or directory)"); + } + if (Files.isDirectory(this.path)) { + throw new FileNotFoundException(getPath() + " (is a directory)"); + } + return Files.newInputStream(this.path); + } + + /** + * This implementation checks whether the underlying file is marked as writable + * (and corresponds to an actual file with content, not to a directory). + * @see java.nio.file.Files#isWritable(Path) + * @see java.nio.file.Files#isDirectory(Path, java.nio.file.LinkOption...) + */ + @Override + public boolean isWritable() { + return (Files.isWritable(this.path) && !Files.isDirectory(this.path)); + } + + /** + * This implementation opens a OutputStream for the underlying file. + * @see java.nio.file.spi.FileSystemProvider#newOutputStream(Path, OpenOption...) + */ + @Override + public OutputStream getOutputStream() throws IOException { + if (Files.isDirectory(this.path)) { + throw new FileNotFoundException(getPath() + " (is a directory)"); + } + return Files.newOutputStream(this.path); + } + + /** + * This implementation returns a URL for the underlying file. + * @see java.nio.file.Path#toUri() + * @see java.net.URI#toURL() + */ + @Override + public URL getURL() throws IOException { + return this.path.toUri().toURL(); + } + + /** + * This implementation returns a URI for the underlying file. + * @see java.nio.file.Path#toUri() + */ + @Override + public URI getURI() throws IOException { + return this.path.toUri(); + } + + /** + * This implementation always indicates a file. + */ + @Override + public boolean isFile() { + return true; + } + + /** + * This implementation returns the underlying File reference. + */ + @Override + public File getFile() throws IOException { + try { + return this.path.toFile(); + } + catch (UnsupportedOperationException ex) { + // Only paths on the default file system can be converted to a File: + // Do exception translation for cases where conversion is not possible. + throw new FileNotFoundException(this.path + " cannot be resolved to absolute file path"); + } + } + + /** + * This implementation opens a Channel for the underlying file. + * @see Files#newByteChannel(Path, OpenOption...) + */ + @Override + public ReadableByteChannel readableChannel() throws IOException { + try { + return Files.newByteChannel(this.path, StandardOpenOption.READ); + } + catch (NoSuchFileException ex) { + throw new FileNotFoundException(ex.getMessage()); + } + } + + /** + * This implementation opens a Channel for the underlying file. + * @see Files#newByteChannel(Path, OpenOption...) + */ + @Override + public WritableByteChannel writableChannel() throws IOException { + return Files.newByteChannel(this.path, StandardOpenOption.WRITE); + } + + /** + * This implementation returns the underlying file's length. + */ + @Override + public long contentLength() throws IOException { + return Files.size(this.path); + } + + /** + * This implementation returns the underlying File's timestamp. + * @see java.nio.file.Files#getLastModifiedTime(Path, java.nio.file.LinkOption...) + */ + @Override + public long lastModified() throws IOException { + // We can not use the superclass method since it uses conversion to a File and + // only a Path on the default file system can be converted to a File... + return Files.getLastModifiedTime(this.path).toMillis(); + } + + /** + * This implementation creates a PathResource, applying the given path + * relative to the path of the underlying file of this resource descriptor. + * @see java.nio.file.Path#resolve(String) + */ + @Override + public Resource createRelative(String relativePath) { + return new PathResource(this.path.resolve(relativePath)); + } + + /** + * This implementation returns the name of the file. + * @see java.nio.file.Path#getFileName() + */ + @Override + public String getFilename() { + return this.path.getFileName().toString(); + } + + @Override + public String getDescription() { + return "path [" + this.path.toAbsolutePath() + "]"; + } + + + /** + * This implementation compares the underlying Path references. + */ + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof PathResource && + this.path.equals(((PathResource) other).path))); + } + + /** + * This implementation returns the hash code of the underlying Path reference. + */ + @Override + public int hashCode() { + return this.path.hashCode(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/ProtocolResolver.java b/spring-core/src/main/java/org/springframework/core/io/ProtocolResolver.java new file mode 100644 index 0000000..53cc930 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/ProtocolResolver.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io; + +import org.springframework.lang.Nullable; + +/** + * A resolution strategy for protocol-specific resource handles. + * + *

    Used as an SPI for {@link DefaultResourceLoader}, allowing for + * custom protocols to be handled without subclassing the loader + * implementation (or application context implementation). + * + * @author Juergen Hoeller + * @since 4.3 + * @see DefaultResourceLoader#addProtocolResolver + */ +@FunctionalInterface +public interface ProtocolResolver { + + /** + * Resolve the given location against the given resource loader + * if this implementation's protocol matches. + * @param location the user-specified resource location + * @param resourceLoader the associated resource loader + * @return a corresponding {@code Resource} handle if the given location + * matches this resolver's protocol, or {@code null} otherwise + */ + @Nullable + Resource resolve(String location, ResourceLoader resourceLoader); + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/Resource.java b/spring-core/src/main/java/org/springframework/core/io/Resource.java new file mode 100644 index 0000000..1995ee7 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/Resource.java @@ -0,0 +1,178 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; + +import org.springframework.lang.Nullable; + +/** + * Interface for a resource descriptor that abstracts from the actual + * type of underlying resource, such as a file or class path resource. + * + *

    An InputStream can be opened for every resource if it exists in + * physical form, but a URL or File handle can just be returned for + * certain resources. The actual behavior is implementation-specific. + * + * @author Juergen Hoeller + * @since 28.12.2003 + * @see #getInputStream() + * @see #getURL() + * @see #getURI() + * @see #getFile() + * @see WritableResource + * @see ContextResource + * @see UrlResource + * @see FileUrlResource + * @see FileSystemResource + * @see ClassPathResource + * @see ByteArrayResource + * @see InputStreamResource + */ +public interface Resource extends InputStreamSource { + + /** + * Determine whether this resource actually exists in physical form. + *

    This method performs a definitive existence check, whereas the + * existence of a {@code Resource} handle only guarantees a valid + * descriptor handle. + */ + boolean exists(); + + /** + * Indicate whether non-empty contents of this resource can be read via + * {@link #getInputStream()}. + *

    Will be {@code true} for typical resource descriptors that exist + * since it strictly implies {@link #exists()} semantics as of 5.1. + * Note that actual content reading may still fail when attempted. + * However, a value of {@code false} is a definitive indication + * that the resource content cannot be read. + * @see #getInputStream() + * @see #exists() + */ + default boolean isReadable() { + return exists(); + } + + /** + * Indicate whether this resource represents a handle with an open stream. + * If {@code true}, the InputStream cannot be read multiple times, + * and must be read and closed to avoid resource leaks. + *

    Will be {@code false} for typical resource descriptors. + */ + default boolean isOpen() { + return false; + } + + /** + * Determine whether this resource represents a file in a file system. + * A value of {@code true} strongly suggests (but does not guarantee) + * that a {@link #getFile()} call will succeed. + *

    This is conservatively {@code false} by default. + * @since 5.0 + * @see #getFile() + */ + default boolean isFile() { + return false; + } + + /** + * Return a URL handle for this resource. + * @throws IOException if the resource cannot be resolved as URL, + * i.e. if the resource is not available as descriptor + */ + URL getURL() throws IOException; + + /** + * Return a URI handle for this resource. + * @throws IOException if the resource cannot be resolved as URI, + * i.e. if the resource is not available as descriptor + * @since 2.5 + */ + URI getURI() throws IOException; + + /** + * Return a File handle for this resource. + * @throws java.io.FileNotFoundException if the resource cannot be resolved as + * absolute file path, i.e. if the resource is not available in a file system + * @throws IOException in case of general resolution/reading failures + * @see #getInputStream() + */ + File getFile() throws IOException; + + /** + * Return a {@link ReadableByteChannel}. + *

    It is expected that each call creates a fresh channel. + *

    The default implementation returns {@link Channels#newChannel(InputStream)} + * with the result of {@link #getInputStream()}. + * @return the byte channel for the underlying resource (must not be {@code null}) + * @throws java.io.FileNotFoundException if the underlying resource doesn't exist + * @throws IOException if the content channel could not be opened + * @since 5.0 + * @see #getInputStream() + */ + default ReadableByteChannel readableChannel() throws IOException { + return Channels.newChannel(getInputStream()); + } + + /** + * Determine the content length for this resource. + * @throws IOException if the resource cannot be resolved + * (in the file system or as some other known physical resource type) + */ + long contentLength() throws IOException; + + /** + * Determine the last-modified timestamp for this resource. + * @throws IOException if the resource cannot be resolved + * (in the file system or as some other known physical resource type) + */ + long lastModified() throws IOException; + + /** + * Create a resource relative to this resource. + * @param relativePath the relative path (relative to this resource) + * @return the resource handle for the relative resource + * @throws IOException if the relative resource cannot be determined + */ + Resource createRelative(String relativePath) throws IOException; + + /** + * Determine a filename for this resource, i.e. typically the last + * part of the path: for example, "myfile.txt". + *

    Returns {@code null} if this type of resource does not + * have a filename. + */ + @Nullable + String getFilename(); + + /** + * Return a description for this resource, + * to be used for error output when working with the resource. + *

    Implementations are also encouraged to return this value + * from their {@code toString} method. + * @see Object#toString() + */ + String getDescription(); + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/ResourceEditor.java b/spring-core/src/main/java/org/springframework/core/io/ResourceEditor.java new file mode 100644 index 0000000..ffc5165 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/ResourceEditor.java @@ -0,0 +1,139 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io; + +import java.beans.PropertyEditorSupport; +import java.io.IOException; + +import org.springframework.core.env.PropertyResolver; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * {@link java.beans.PropertyEditor Editor} for {@link Resource} + * descriptors, to automatically convert {@code String} locations + * e.g. {@code file:C:/myfile.txt} or {@code classpath:myfile.txt} to + * {@code Resource} properties instead of using a {@code String} location property. + * + *

    The path may contain {@code ${...}} placeholders, to be + * resolved as {@link org.springframework.core.env.Environment} properties: + * e.g. {@code ${user.dir}}. Unresolvable placeholders are ignored by default. + * + *

    Delegates to a {@link ResourceLoader} to do the heavy lifting, + * by default using a {@link DefaultResourceLoader}. + * + * @author Juergen Hoeller + * @author Dave Syer + * @author Chris Beams + * @since 28.12.2003 + * @see Resource + * @see ResourceLoader + * @see DefaultResourceLoader + * @see PropertyResolver#resolvePlaceholders + */ +public class ResourceEditor extends PropertyEditorSupport { + + private final ResourceLoader resourceLoader; + + @Nullable + private PropertyResolver propertyResolver; + + private final boolean ignoreUnresolvablePlaceholders; + + + /** + * Create a new instance of the {@link ResourceEditor} class + * using a {@link DefaultResourceLoader} and {@link StandardEnvironment}. + */ + public ResourceEditor() { + this(new DefaultResourceLoader(), null); + } + + /** + * Create a new instance of the {@link ResourceEditor} class + * using the given {@link ResourceLoader} and {@link PropertyResolver}. + * @param resourceLoader the {@code ResourceLoader} to use + * @param propertyResolver the {@code PropertyResolver} to use + */ + public ResourceEditor(ResourceLoader resourceLoader, @Nullable PropertyResolver propertyResolver) { + this(resourceLoader, propertyResolver, true); + } + + /** + * Create a new instance of the {@link ResourceEditor} class + * using the given {@link ResourceLoader}. + * @param resourceLoader the {@code ResourceLoader} to use + * @param propertyResolver the {@code PropertyResolver} to use + * @param ignoreUnresolvablePlaceholders whether to ignore unresolvable placeholders + * if no corresponding property could be found in the given {@code propertyResolver} + */ + public ResourceEditor(ResourceLoader resourceLoader, @Nullable PropertyResolver propertyResolver, + boolean ignoreUnresolvablePlaceholders) { + + Assert.notNull(resourceLoader, "ResourceLoader must not be null"); + this.resourceLoader = resourceLoader; + this.propertyResolver = propertyResolver; + this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders; + } + + + @Override + public void setAsText(String text) { + if (StringUtils.hasText(text)) { + String locationToUse = resolvePath(text).trim(); + setValue(this.resourceLoader.getResource(locationToUse)); + } + else { + setValue(null); + } + } + + /** + * Resolve the given path, replacing placeholders with corresponding + * property values from the {@code environment} if necessary. + * @param path the original file path + * @return the resolved file path + * @see PropertyResolver#resolvePlaceholders + * @see PropertyResolver#resolveRequiredPlaceholders + */ + protected String resolvePath(String path) { + if (this.propertyResolver == null) { + this.propertyResolver = new StandardEnvironment(); + } + return (this.ignoreUnresolvablePlaceholders ? this.propertyResolver.resolvePlaceholders(path) : + this.propertyResolver.resolveRequiredPlaceholders(path)); + } + + + @Override + @Nullable + public String getAsText() { + Resource value = (Resource) getValue(); + try { + // Try to determine URL for resource. + return (value != null ? value.getURL().toExternalForm() : ""); + } + catch (IOException ex) { + // Couldn't determine resource URL - return null to indicate + // that there is no appropriate text representation. + return null; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/ResourceLoader.java b/spring-core/src/main/java/org/springframework/core/io/ResourceLoader.java new file mode 100644 index 0000000..9a19fda --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/ResourceLoader.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io; + +import org.springframework.lang.Nullable; +import org.springframework.util.ResourceUtils; + +/** + * Strategy interface for loading resources (e.. class path or file system + * resources). An {@link org.springframework.context.ApplicationContext} + * is required to provide this functionality, plus extended + * {@link org.springframework.core.io.support.ResourcePatternResolver} support. + * + *

    {@link DefaultResourceLoader} is a standalone implementation that is + * usable outside an ApplicationContext, also used by {@link ResourceEditor}. + * + *

    Bean properties of type Resource and Resource array can be populated + * from Strings when running in an ApplicationContext, using the particular + * context's resource loading strategy. + * + * @author Juergen Hoeller + * @since 10.03.2004 + * @see Resource + * @see org.springframework.core.io.support.ResourcePatternResolver + * @see org.springframework.context.ApplicationContext + * @see org.springframework.context.ResourceLoaderAware + */ +public interface ResourceLoader { + + /** Pseudo URL prefix for loading from the class path: "classpath:". */ + String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX; + + + /** + * Return a Resource handle for the specified resource location. + *

    The handle should always be a reusable resource descriptor, + * allowing for multiple {@link Resource#getInputStream()} calls. + *

      + *
    • Must support fully qualified URLs, e.g. "file:C:/test.dat". + *
    • Must support classpath pseudo-URLs, e.g. "classpath:test.dat". + *
    • Should support relative file paths, e.g. "WEB-INF/test.dat". + * (This will be implementation-specific, typically provided by an + * ApplicationContext implementation.) + *
    + *

    Note that a Resource handle does not imply an existing resource; + * you need to invoke {@link Resource#exists} to check for existence. + * @param location the resource location + * @return a corresponding Resource handle (never {@code null}) + * @see #CLASSPATH_URL_PREFIX + * @see Resource#exists() + * @see Resource#getInputStream() + */ + Resource getResource(String location); + + /** + * Expose the ClassLoader used by this ResourceLoader. + *

    Clients which need to access the ClassLoader directly can do so + * in a uniform manner with the ResourceLoader, rather than relying + * on the thread context ClassLoader. + * @return the ClassLoader + * (only {@code null} if even the system ClassLoader isn't accessible) + * @see org.springframework.util.ClassUtils#getDefaultClassLoader() + * @see org.springframework.util.ClassUtils#forName(String, ClassLoader) + */ + @Nullable + ClassLoader getClassLoader(); + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/UrlResource.java b/spring-core/src/main/java/org/springframework/core/io/UrlResource.java new file mode 100644 index 0000000..3f1dcc0 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/UrlResource.java @@ -0,0 +1,307 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ResourceUtils; +import org.springframework.util.StringUtils; + +/** + * {@link Resource} implementation for {@code java.net.URL} locators. + * Supports resolution as a {@code URL} and also as a {@code File} in + * case of the {@code "file:"} protocol. + * + * @author Juergen Hoeller + * @since 28.12.2003 + * @see java.net.URL + */ +public class UrlResource extends AbstractFileResolvingResource { + + /** + * Original URI, if available; used for URI and File access. + */ + @Nullable + private final URI uri; + + /** + * Original URL, used for actual access. + */ + private final URL url; + + /** + * Cleaned URL (with normalized path), used for comparisons. + */ + @Nullable + private volatile URL cleanedUrl; + + + /** + * Create a new {@code UrlResource} based on the given URI object. + * @param uri a URI + * @throws MalformedURLException if the given URL path is not valid + * @since 2.5 + */ + public UrlResource(URI uri) throws MalformedURLException { + Assert.notNull(uri, "URI must not be null"); + this.uri = uri; + this.url = uri.toURL(); + } + + /** + * Create a new {@code UrlResource} based on the given URL object. + * @param url a URL + */ + public UrlResource(URL url) { + Assert.notNull(url, "URL must not be null"); + this.uri = null; + this.url = url; + } + + /** + * Create a new {@code UrlResource} based on a URL path. + *

    Note: The given path needs to be pre-encoded if necessary. + * @param path a URL path + * @throws MalformedURLException if the given URL path is not valid + * @see java.net.URL#URL(String) + */ + public UrlResource(String path) throws MalformedURLException { + Assert.notNull(path, "Path must not be null"); + this.uri = null; + this.url = new URL(path); + this.cleanedUrl = getCleanedUrl(this.url, path); + } + + /** + * Create a new {@code UrlResource} based on a URI specification. + *

    The given parts will automatically get encoded if necessary. + * @param protocol the URL protocol to use (e.g. "jar" or "file" - without colon); + * also known as "scheme" + * @param location the location (e.g. the file path within that protocol); + * also known as "scheme-specific part" + * @throws MalformedURLException if the given URL specification is not valid + * @see java.net.URI#URI(String, String, String) + */ + public UrlResource(String protocol, String location) throws MalformedURLException { + this(protocol, location, null); + } + + /** + * Create a new {@code UrlResource} based on a URI specification. + *

    The given parts will automatically get encoded if necessary. + * @param protocol the URL protocol to use (e.g. "jar" or "file" - without colon); + * also known as "scheme" + * @param location the location (e.g. the file path within that protocol); + * also known as "scheme-specific part" + * @param fragment the fragment within that location (e.g. anchor on an HTML page, + * as following after a "#" separator) + * @throws MalformedURLException if the given URL specification is not valid + * @see java.net.URI#URI(String, String, String) + */ + public UrlResource(String protocol, String location, @Nullable String fragment) throws MalformedURLException { + try { + this.uri = new URI(protocol, location, fragment); + this.url = this.uri.toURL(); + } + catch (URISyntaxException ex) { + MalformedURLException exToThrow = new MalformedURLException(ex.getMessage()); + exToThrow.initCause(ex); + throw exToThrow; + } + } + + + /** + * Determine a cleaned URL for the given original URL. + * @param originalUrl the original URL + * @param originalPath the original URL path + * @return the cleaned URL (possibly the original URL as-is) + * @see org.springframework.util.StringUtils#cleanPath + */ + private static URL getCleanedUrl(URL originalUrl, String originalPath) { + String cleanedPath = StringUtils.cleanPath(originalPath); + if (!cleanedPath.equals(originalPath)) { + try { + return new URL(cleanedPath); + } + catch (MalformedURLException ex) { + // Cleaned URL path cannot be converted to URL -> take original URL. + } + } + return originalUrl; + } + + /** + * Lazily determine a cleaned URL for the given original URL. + * @see #getCleanedUrl(URL, String) + */ + private URL getCleanedUrl() { + URL cleanedUrl = this.cleanedUrl; + if (cleanedUrl != null) { + return cleanedUrl; + } + cleanedUrl = getCleanedUrl(this.url, (this.uri != null ? this.uri : this.url).toString()); + this.cleanedUrl = cleanedUrl; + return cleanedUrl; + } + + + /** + * This implementation opens an InputStream for the given URL. + *

    It sets the {@code useCaches} flag to {@code false}, + * mainly to avoid jar file locking on Windows. + * @see java.net.URL#openConnection() + * @see java.net.URLConnection#setUseCaches(boolean) + * @see java.net.URLConnection#getInputStream() + */ + @Override + public InputStream getInputStream() throws IOException { + URLConnection con = this.url.openConnection(); + ResourceUtils.useCachesIfNecessary(con); + try { + return con.getInputStream(); + } + catch (IOException ex) { + // Close the HTTP connection (if applicable). + if (con instanceof HttpURLConnection) { + ((HttpURLConnection) con).disconnect(); + } + throw ex; + } + } + + /** + * This implementation returns the underlying URL reference. + */ + @Override + public URL getURL() { + return this.url; + } + + /** + * This implementation returns the underlying URI directly, + * if possible. + */ + @Override + public URI getURI() throws IOException { + if (this.uri != null) { + return this.uri; + } + else { + return super.getURI(); + } + } + + @Override + public boolean isFile() { + if (this.uri != null) { + return super.isFile(this.uri); + } + else { + return super.isFile(); + } + } + + /** + * This implementation returns a File reference for the underlying URL/URI, + * provided that it refers to a file in the file system. + * @see org.springframework.util.ResourceUtils#getFile(java.net.URL, String) + */ + @Override + public File getFile() throws IOException { + if (this.uri != null) { + return super.getFile(this.uri); + } + else { + return super.getFile(); + } + } + + /** + * This implementation creates a {@code UrlResource}, delegating to + * {@link #createRelativeURL(String)} for adapting the relative path. + * @see #createRelativeURL(String) + */ + @Override + public Resource createRelative(String relativePath) throws MalformedURLException { + return new UrlResource(createRelativeURL(relativePath)); + } + + /** + * This delegate creates a {@code java.net.URL}, applying the given path + * relative to the path of the underlying URL of this resource descriptor. + * A leading slash will get dropped; a "#" symbol will get encoded. + * @since 5.2 + * @see #createRelative(String) + * @see java.net.URL#URL(java.net.URL, String) + */ + protected URL createRelativeURL(String relativePath) throws MalformedURLException { + if (relativePath.startsWith("/")) { + relativePath = relativePath.substring(1); + } + // # can appear in filenames, java.net.URL should not treat it as a fragment + relativePath = StringUtils.replace(relativePath, "#", "%23"); + // Use the URL constructor for applying the relative path as a URL spec + return new URL(this.url, relativePath); + } + + /** + * This implementation returns the name of the file that this URL refers to. + * @see java.net.URL#getPath() + */ + @Override + public String getFilename() { + return StringUtils.getFilename(getCleanedUrl().getPath()); + } + + /** + * This implementation returns a description that includes the URL. + */ + @Override + public String getDescription() { + return "URL [" + this.url + "]"; + } + + + /** + * This implementation compares the underlying URL references. + */ + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof UrlResource && + getCleanedUrl().equals(((UrlResource) other).getCleanedUrl()))); + } + + /** + * This implementation returns the hash code of the underlying URL reference. + */ + @Override + public int hashCode() { + return getCleanedUrl().hashCode(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/VfsResource.java b/spring-core/src/main/java/org/springframework/core/io/VfsResource.java new file mode 100644 index 0000000..6751d60 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/VfsResource.java @@ -0,0 +1,144 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; + +import org.springframework.core.NestedIOException; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * JBoss VFS based {@link Resource} implementation. + * + *

    As of Spring 4.0, this class supports VFS 3.x on JBoss AS 6+ + * (package {@code org.jboss.vfs}) and is in particular compatible with + * JBoss AS 7 and WildFly 8+. + * + * @author Ales Justin + * @author Juergen Hoeller + * @author Costin Leau + * @author Sam Brannen + * @since 3.0 + * @see org.jboss.vfs.VirtualFile + */ +public class VfsResource extends AbstractResource { + + private final Object resource; + + + /** + * Create a new {@code VfsResource} wrapping the given resource handle. + * @param resource a {@code org.jboss.vfs.VirtualFile} instance + * (untyped in order to avoid a static dependency on the VFS API) + */ + public VfsResource(Object resource) { + Assert.notNull(resource, "VirtualFile must not be null"); + this.resource = resource; + } + + + @Override + public InputStream getInputStream() throws IOException { + return VfsUtils.getInputStream(this.resource); + } + + @Override + public boolean exists() { + return VfsUtils.exists(this.resource); + } + + @Override + public boolean isReadable() { + return VfsUtils.isReadable(this.resource); + } + + @Override + public URL getURL() throws IOException { + try { + return VfsUtils.getURL(this.resource); + } + catch (Exception ex) { + throw new NestedIOException("Failed to obtain URL for file " + this.resource, ex); + } + } + + @Override + public URI getURI() throws IOException { + try { + return VfsUtils.getURI(this.resource); + } + catch (Exception ex) { + throw new NestedIOException("Failed to obtain URI for " + this.resource, ex); + } + } + + @Override + public File getFile() throws IOException { + return VfsUtils.getFile(this.resource); + } + + @Override + public long contentLength() throws IOException { + return VfsUtils.getSize(this.resource); + } + + @Override + public long lastModified() throws IOException { + return VfsUtils.getLastModified(this.resource); + } + + @Override + public Resource createRelative(String relativePath) throws IOException { + if (!relativePath.startsWith(".") && relativePath.contains("/")) { + try { + return new VfsResource(VfsUtils.getChild(this.resource, relativePath)); + } + catch (IOException ex) { + // fall back to getRelative + } + } + + return new VfsResource(VfsUtils.getRelative(new URL(getURL(), relativePath))); + } + + @Override + public String getFilename() { + return VfsUtils.getName(this.resource); + } + + @Override + public String getDescription() { + return "VFS resource [" + this.resource + "]"; + } + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof VfsResource && + this.resource.equals(((VfsResource) other).resource))); + } + + @Override + public int hashCode() { + return this.resource.hashCode(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/VfsUtils.java b/spring-core/src/main/java/org/springframework/core/io/VfsUtils.java new file mode 100644 index 0000000..c2e3c35 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/VfsUtils.java @@ -0,0 +1,196 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URI; +import java.net.URL; + +import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; + +/** + * Utility for detecting and accessing JBoss VFS in the classpath. + * + *

    As of Spring 4.0, this class supports VFS 3.x on JBoss AS 6+ + * (package {@code org.jboss.vfs}) and is in particular compatible with + * JBoss AS 7 and WildFly 8+. + * + *

    Thanks go to Marius Bogoevici for the initial patch. + * Note: This is an internal class and should not be used outside the framework. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 3.0.3 + */ +public abstract class VfsUtils { + + private static final String VFS3_PKG = "org.jboss.vfs."; + private static final String VFS_NAME = "VFS"; + + private static final Method VFS_METHOD_GET_ROOT_URL; + private static final Method VFS_METHOD_GET_ROOT_URI; + + private static final Method VIRTUAL_FILE_METHOD_EXISTS; + private static final Method VIRTUAL_FILE_METHOD_GET_INPUT_STREAM; + private static final Method VIRTUAL_FILE_METHOD_GET_SIZE; + private static final Method VIRTUAL_FILE_METHOD_GET_LAST_MODIFIED; + private static final Method VIRTUAL_FILE_METHOD_TO_URL; + private static final Method VIRTUAL_FILE_METHOD_TO_URI; + private static final Method VIRTUAL_FILE_METHOD_GET_NAME; + private static final Method VIRTUAL_FILE_METHOD_GET_PATH_NAME; + private static final Method VIRTUAL_FILE_METHOD_GET_PHYSICAL_FILE; + private static final Method VIRTUAL_FILE_METHOD_GET_CHILD; + + protected static final Class VIRTUAL_FILE_VISITOR_INTERFACE; + protected static final Method VIRTUAL_FILE_METHOD_VISIT; + + private static final Field VISITOR_ATTRIBUTES_FIELD_RECURSE; + + static { + ClassLoader loader = VfsUtils.class.getClassLoader(); + try { + Class vfsClass = loader.loadClass(VFS3_PKG + VFS_NAME); + VFS_METHOD_GET_ROOT_URL = vfsClass.getMethod("getChild", URL.class); + VFS_METHOD_GET_ROOT_URI = vfsClass.getMethod("getChild", URI.class); + + Class virtualFile = loader.loadClass(VFS3_PKG + "VirtualFile"); + VIRTUAL_FILE_METHOD_EXISTS = virtualFile.getMethod("exists"); + VIRTUAL_FILE_METHOD_GET_INPUT_STREAM = virtualFile.getMethod("openStream"); + VIRTUAL_FILE_METHOD_GET_SIZE = virtualFile.getMethod("getSize"); + VIRTUAL_FILE_METHOD_GET_LAST_MODIFIED = virtualFile.getMethod("getLastModified"); + VIRTUAL_FILE_METHOD_TO_URI = virtualFile.getMethod("toURI"); + VIRTUAL_FILE_METHOD_TO_URL = virtualFile.getMethod("toURL"); + VIRTUAL_FILE_METHOD_GET_NAME = virtualFile.getMethod("getName"); + VIRTUAL_FILE_METHOD_GET_PATH_NAME = virtualFile.getMethod("getPathName"); + VIRTUAL_FILE_METHOD_GET_PHYSICAL_FILE = virtualFile.getMethod("getPhysicalFile"); + VIRTUAL_FILE_METHOD_GET_CHILD = virtualFile.getMethod("getChild", String.class); + + VIRTUAL_FILE_VISITOR_INTERFACE = loader.loadClass(VFS3_PKG + "VirtualFileVisitor"); + VIRTUAL_FILE_METHOD_VISIT = virtualFile.getMethod("visit", VIRTUAL_FILE_VISITOR_INTERFACE); + + Class visitorAttributesClass = loader.loadClass(VFS3_PKG + "VisitorAttributes"); + VISITOR_ATTRIBUTES_FIELD_RECURSE = visitorAttributesClass.getField("RECURSE"); + } + catch (Throwable ex) { + throw new IllegalStateException("Could not detect JBoss VFS infrastructure", ex); + } + } + + protected static Object invokeVfsMethod(Method method, @Nullable Object target, Object... args) throws IOException { + try { + return method.invoke(target, args); + } + catch (InvocationTargetException ex) { + Throwable targetEx = ex.getTargetException(); + if (targetEx instanceof IOException) { + throw (IOException) targetEx; + } + ReflectionUtils.handleInvocationTargetException(ex); + } + catch (Exception ex) { + ReflectionUtils.handleReflectionException(ex); + } + + throw new IllegalStateException("Invalid code path reached"); + } + + static boolean exists(Object vfsResource) { + try { + return (Boolean) invokeVfsMethod(VIRTUAL_FILE_METHOD_EXISTS, vfsResource); + } + catch (IOException ex) { + return false; + } + } + + static boolean isReadable(Object vfsResource) { + try { + return (Long) invokeVfsMethod(VIRTUAL_FILE_METHOD_GET_SIZE, vfsResource) > 0; + } + catch (IOException ex) { + return false; + } + } + + static long getSize(Object vfsResource) throws IOException { + return (Long) invokeVfsMethod(VIRTUAL_FILE_METHOD_GET_SIZE, vfsResource); + } + + static long getLastModified(Object vfsResource) throws IOException { + return (Long) invokeVfsMethod(VIRTUAL_FILE_METHOD_GET_LAST_MODIFIED, vfsResource); + } + + static InputStream getInputStream(Object vfsResource) throws IOException { + return (InputStream) invokeVfsMethod(VIRTUAL_FILE_METHOD_GET_INPUT_STREAM, vfsResource); + } + + static URL getURL(Object vfsResource) throws IOException { + return (URL) invokeVfsMethod(VIRTUAL_FILE_METHOD_TO_URL, vfsResource); + } + + static URI getURI(Object vfsResource) throws IOException { + return (URI) invokeVfsMethod(VIRTUAL_FILE_METHOD_TO_URI, vfsResource); + } + + static String getName(Object vfsResource) { + try { + return (String) invokeVfsMethod(VIRTUAL_FILE_METHOD_GET_NAME, vfsResource); + } + catch (IOException ex) { + throw new IllegalStateException("Cannot get resource name", ex); + } + } + + static Object getRelative(URL url) throws IOException { + return invokeVfsMethod(VFS_METHOD_GET_ROOT_URL, null, url); + } + + static Object getChild(Object vfsResource, String path) throws IOException { + return invokeVfsMethod(VIRTUAL_FILE_METHOD_GET_CHILD, vfsResource, path); + } + + static File getFile(Object vfsResource) throws IOException { + return (File) invokeVfsMethod(VIRTUAL_FILE_METHOD_GET_PHYSICAL_FILE, vfsResource); + } + + static Object getRoot(URI url) throws IOException { + return invokeVfsMethod(VFS_METHOD_GET_ROOT_URI, null, url); + } + + // protected methods used by the support sub-package + + protected static Object getRoot(URL url) throws IOException { + return invokeVfsMethod(VFS_METHOD_GET_ROOT_URL, null, url); + } + + @Nullable + protected static Object doGetVisitorAttributes() { + return ReflectionUtils.getField(VISITOR_ATTRIBUTES_FIELD_RECURSE, null); + } + + @Nullable + protected static String doGetPath(Object resource) { + return (String) ReflectionUtils.invokeMethod(VIRTUAL_FILE_METHOD_GET_PATH_NAME, resource); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/WritableResource.java b/spring-core/src/main/java/org/springframework/core/io/WritableResource.java new file mode 100644 index 0000000..3fd8037 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/WritableResource.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.channels.Channels; +import java.nio.channels.WritableByteChannel; + +/** + * Extended interface for a resource that supports writing to it. + * Provides an {@link #getOutputStream() OutputStream accessor}. + * + * @author Juergen Hoeller + * @since 3.1 + * @see java.io.OutputStream + */ +public interface WritableResource extends Resource { + + /** + * Indicate whether the contents of this resource can be written + * via {@link #getOutputStream()}. + *

    Will be {@code true} for typical resource descriptors; + * note that actual content writing may still fail when attempted. + * However, a value of {@code false} is a definitive indication + * that the resource content cannot be modified. + * @see #getOutputStream() + * @see #isReadable() + */ + default boolean isWritable() { + return true; + } + + /** + * Return an {@link OutputStream} for the underlying resource, + * allowing to (over-)write its content. + * @throws IOException if the stream could not be opened + * @see #getInputStream() + */ + OutputStream getOutputStream() throws IOException; + + /** + * Return a {@link WritableByteChannel}. + *

    It is expected that each call creates a fresh channel. + *

    The default implementation returns {@link Channels#newChannel(OutputStream)} + * with the result of {@link #getOutputStream()}. + * @return the byte channel for the underlying resource (must not be {@code null}) + * @throws java.io.FileNotFoundException if the underlying resource doesn't exist + * @throws IOException if the content channel could not be opened + * @since 5.0 + * @see #getOutputStream() + */ + default WritableByteChannel writableChannel() throws IOException { + return Channels.newChannel(getOutputStream()); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBuffer.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBuffer.java new file mode 100644 index 0000000..6f5a38b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBuffer.java @@ -0,0 +1,380 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.buffer; + +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; +import java.util.function.IntPredicate; + +import org.springframework.util.Assert; + +/** + * Basic abstraction over byte buffers. + * + *

    {@code DataBuffer}s has a separate {@linkplain #readPosition() read} and + * {@linkplain #writePosition() write} position, as opposed to {@code ByteBuffer}'s + * single {@linkplain ByteBuffer#position() position}. As such, the {@code DataBuffer} + * does not require a {@linkplain ByteBuffer#flip() flip} to read after writing. In general, + * the following invariant holds for the read and write positions, and the capacity: + * + *

    + * 0 <= + * readPosition <= + * writePosition <= + * capacity + *
    + * + *

    The {@linkplain #capacity() capacity} of a {@code DataBuffer} is expanded on demand, + * similar to {@code StringBuilder}. + * + *

    The main purpose of the {@code DataBuffer} abstraction is to provide a convenient wrapper + * around {@link ByteBuffer} which is similar to Netty's {@link io.netty.buffer.ByteBuf} but + * can also be used on non-Netty platforms (i.e. Servlet containers). + * + * @author Arjen Poutsma + * @author Brian Clozel + * @since 5.0 + * @see DataBufferFactory + */ +public interface DataBuffer { + + /** + * Return the {@link DataBufferFactory} that created this buffer. + * @return the creating buffer factory + */ + DataBufferFactory factory(); + + /** + * Return the index of the first byte in this buffer that matches + * the given predicate. + * @param predicate the predicate to match + * @param fromIndex the index to start the search from + * @return the index of the first byte that matches {@code predicate}; + * or {@code -1} if none match + */ + int indexOf(IntPredicate predicate, int fromIndex); + + /** + * Return the index of the last byte in this buffer that matches + * the given predicate. + * @param predicate the predicate to match + * @param fromIndex the index to start the search from + * @return the index of the last byte that matches {@code predicate}; + * or {@code -1} if none match + */ + int lastIndexOf(IntPredicate predicate, int fromIndex); + + /** + * Return the number of bytes that can be read from this data buffer. + * @return the readable byte count + */ + int readableByteCount(); + + /** + * Return the number of bytes that can be written to this data buffer. + * @return the writable byte count + * @since 5.0.1 + */ + int writableByteCount(); + + /** + * Return the number of bytes that this buffer can contain. + * @return the capacity + * @since 5.0.1 + */ + int capacity(); + + /** + * Set the number of bytes that this buffer can contain. + *

    If the new capacity is lower than the current capacity, the contents + * of this buffer will be truncated. If the new capacity is higher than + * the current capacity, it will be expanded. + * @param capacity the new capacity + * @return this buffer + */ + DataBuffer capacity(int capacity); + + /** + * Ensure that the current buffer has enough {@link #writableByteCount()} + * to write the amount of data given as an argument. If not, the missing + * capacity will be added to the buffer. + * @param capacity the writable capacity to check for + * @return this buffer + * @since 5.1.4 + */ + default DataBuffer ensureCapacity(int capacity) { + return this; + } + + /** + * Return the position from which this buffer will read. + * @return the read position + * @since 5.0.1 + */ + int readPosition(); + + /** + * Set the position from which this buffer will read. + * @param readPosition the new read position + * @return this buffer + * @throws IndexOutOfBoundsException if {@code readPosition} is smaller than 0 + * or greater than {@link #writePosition()} + * @since 5.0.1 + */ + DataBuffer readPosition(int readPosition); + + /** + * Return the position to which this buffer will write. + * @return the write position + * @since 5.0.1 + */ + int writePosition(); + + /** + * Set the position to which this buffer will write. + * @param writePosition the new write position + * @return this buffer + * @throws IndexOutOfBoundsException if {@code writePosition} is smaller than + * {@link #readPosition()} or greater than {@link #capacity()} + * @since 5.0.1 + */ + DataBuffer writePosition(int writePosition); + + /** + * Read a single byte at the given index from this data buffer. + * @param index the index at which the byte will be read + * @return the byte at the given index + * @throws IndexOutOfBoundsException when {@code index} is out of bounds + * @since 5.0.4 + */ + byte getByte(int index); + + /** + * Read a single byte from the current reading position from this data buffer. + * @return the byte at this buffer's current reading position + */ + byte read(); + + /** + * Read this buffer's data into the specified destination, starting at the current + * reading position of this buffer. + * @param destination the array into which the bytes are to be written + * @return this buffer + */ + DataBuffer read(byte[] destination); + + /** + * Read at most {@code length} bytes of this buffer into the specified destination, + * starting at the current reading position of this buffer. + * @param destination the array into which the bytes are to be written + * @param offset the index within {@code destination} of the first byte to be written + * @param length the maximum number of bytes to be written in {@code destination} + * @return this buffer + */ + DataBuffer read(byte[] destination, int offset, int length); + + /** + * Write a single byte into this buffer at the current writing position. + * @param b the byte to be written + * @return this buffer + */ + DataBuffer write(byte b); + + /** + * Write the given source into this buffer, starting at the current writing position + * of this buffer. + * @param source the bytes to be written into this buffer + * @return this buffer + */ + DataBuffer write(byte[] source); + + /** + * Write at most {@code length} bytes of the given source into this buffer, starting + * at the current writing position of this buffer. + * @param source the bytes to be written into this buffer + * @param offset the index within {@code source} to start writing from + * @param length the maximum number of bytes to be written from {@code source} + * @return this buffer + */ + DataBuffer write(byte[] source, int offset, int length); + + /** + * Write one or more {@code DataBuffer}s to this buffer, starting at the current + * writing position. It is the responsibility of the caller to + * {@linkplain DataBufferUtils#release(DataBuffer) release} the given data buffers. + * @param buffers the byte buffers to write into this buffer + * @return this buffer + */ + DataBuffer write(DataBuffer... buffers); + + /** + * Write one or more {@link ByteBuffer} to this buffer, starting at the current + * writing position. + * @param buffers the byte buffers to write into this buffer + * @return this buffer + */ + DataBuffer write(ByteBuffer... buffers); + + /** + * Write the given {@code CharSequence} using the given {@code Charset}, + * starting at the current writing position. + * @param charSequence the char sequence to write into this buffer + * @param charset the charset to encode the char sequence with + * @return this buffer + * @since 5.1.4 + */ + default DataBuffer write(CharSequence charSequence, Charset charset) { + Assert.notNull(charSequence, "CharSequence must not be null"); + Assert.notNull(charset, "Charset must not be null"); + if (charSequence.length() != 0) { + CharsetEncoder charsetEncoder = charset.newEncoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE); + CharBuffer inBuffer = CharBuffer.wrap(charSequence); + int estimatedSize = (int) (inBuffer.remaining() * charsetEncoder.averageBytesPerChar()); + ByteBuffer outBuffer = ensureCapacity(estimatedSize) + .asByteBuffer(writePosition(), writableByteCount()); + while (true) { + CoderResult cr = (inBuffer.hasRemaining() ? + charsetEncoder.encode(inBuffer, outBuffer, true) : CoderResult.UNDERFLOW); + if (cr.isUnderflow()) { + cr = charsetEncoder.flush(outBuffer); + } + if (cr.isUnderflow()) { + break; + } + if (cr.isOverflow()) { + writePosition(writePosition() + outBuffer.position()); + int maximumSize = (int) (inBuffer.remaining() * charsetEncoder.maxBytesPerChar()); + ensureCapacity(maximumSize); + outBuffer = asByteBuffer(writePosition(), writableByteCount()); + } + } + writePosition(writePosition() + outBuffer.position()); + } + return this; + } + + /** + * Create a new {@code DataBuffer} whose contents is a shared subsequence of this + * data buffer's content. Data between this data buffer and the returned buffer is + * shared; though changes in the returned buffer's position will not be reflected + * in the reading nor writing position of this data buffer. + *

    Note that this method will not call + * {@link DataBufferUtils#retain(DataBuffer)} on the resulting slice: the reference + * count will not be increased. + * @param index the index at which to start the slice + * @param length the length of the slice + * @return the specified slice of this data buffer + */ + DataBuffer slice(int index, int length); + + /** + * Create a new {@code DataBuffer} whose contents is a shared, retained subsequence of this + * data buffer's content. Data between this data buffer and the returned buffer is + * shared; though changes in the returned buffer's position will not be reflected + * in the reading nor writing position of this data buffer. + *

    Note that unlike {@link #slice(int, int)}, this method + * will call {@link DataBufferUtils#retain(DataBuffer)} (or equivalent) on the + * resulting slice. + * @param index the index at which to start the slice + * @param length the length of the slice + * @return the specified, retained slice of this data buffer + * @since 5.2 + */ + default DataBuffer retainedSlice(int index, int length) { + return DataBufferUtils.retain(slice(index, length)); + } + + /** + * Expose this buffer's bytes as a {@link ByteBuffer}. Data between this + * {@code DataBuffer} and the returned {@code ByteBuffer} is shared; though + * changes in the returned buffer's {@linkplain ByteBuffer#position() position} + * will not be reflected in the reading nor writing position of this data buffer. + * @return this data buffer as a byte buffer + */ + ByteBuffer asByteBuffer(); + + /** + * Expose a subsequence of this buffer's bytes as a {@link ByteBuffer}. Data between + * this {@code DataBuffer} and the returned {@code ByteBuffer} is shared; though + * changes in the returned buffer's {@linkplain ByteBuffer#position() position} + * will not be reflected in the reading nor writing position of this data buffer. + * @param index the index at which to start the byte buffer + * @param length the length of the returned byte buffer + * @return this data buffer as a byte buffer + * @since 5.0.1 + */ + ByteBuffer asByteBuffer(int index, int length); + + /** + * Expose this buffer's data as an {@link InputStream}. Both data and read position are + * shared between the returned stream and this data buffer. The underlying buffer will + * not be {@linkplain DataBufferUtils#release(DataBuffer) released} + * when the input stream is {@linkplain InputStream#close() closed}. + * @return this data buffer as an input stream + * @see #asInputStream(boolean) + */ + InputStream asInputStream(); + + /** + * Expose this buffer's data as an {@link InputStream}. Both data and read position are + * shared between the returned stream and this data buffer. + * @param releaseOnClose whether the underlying buffer will be + * {@linkplain DataBufferUtils#release(DataBuffer) released} when the input stream is + * {@linkplain InputStream#close() closed}. + * @return this data buffer as an input stream + * @since 5.0.4 + */ + InputStream asInputStream(boolean releaseOnClose); + + /** + * Expose this buffer's data as an {@link OutputStream}. Both data and write position are + * shared between the returned stream and this data buffer. + * @return this data buffer as an output stream + */ + OutputStream asOutputStream(); + + /** + * Return this buffer's data a String using the specified charset. Default implementation + * delegates to {@code toString(readPosition(), readableByteCount(), charset)}. + * @param charset the character set to use + * @return a string representation of all this buffers data + * @since 5.2 + */ + default String toString(Charset charset) { + Assert.notNull(charset, "Charset must not be null"); + return toString(readPosition(), readableByteCount(), charset); + } + + /** + * Return a part of this buffer's data as a String using the specified charset. + * @param index the index at which to start the string + * @param length the number of bytes to use for the string + * @param charset the charset to use + * @return a string representation of a part of this buffers data + * @since 5.2 + */ + String toString(int index, int length, Charset charset); + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferFactory.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferFactory.java new file mode 100644 index 0000000..4352449 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferFactory.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.buffer; + +import java.nio.ByteBuffer; +import java.util.List; + +/** + * A factory for {@link DataBuffer DataBuffers}, allowing for allocation and + * wrapping of data buffers. + * + * @author Arjen Poutsma + * @since 5.0 + * @see DataBuffer + */ +public interface DataBufferFactory { + + /** + * Allocate a data buffer of a default initial capacity. Depending on the + * underlying implementation and its configuration, this will be heap-based + * or direct buffer. + * @return the allocated buffer + */ + DataBuffer allocateBuffer(); + + /** + * Allocate a data buffer of the given initial capacity. Depending on the + * underlying implementation and its configuration, this will be heap-based + * or direct buffer. + * @param initialCapacity the initial capacity of the buffer to allocate + * @return the allocated buffer + */ + DataBuffer allocateBuffer(int initialCapacity); + + /** + * Wrap the given {@link ByteBuffer} in a {@code DataBuffer}. Unlike + * {@linkplain #allocateBuffer(int) allocating}, wrapping does not use new memory. + * @param byteBuffer the NIO byte buffer to wrap + * @return the wrapped buffer + */ + DataBuffer wrap(ByteBuffer byteBuffer); + + /** + * Wrap the given {@code byte} array in a {@code DataBuffer}. Unlike + * {@linkplain #allocateBuffer(int) allocating}, wrapping does not use new memory. + * @param bytes the byte array to wrap + * @return the wrapped buffer + */ + DataBuffer wrap(byte[] bytes); + + /** + * Return a new {@code DataBuffer} composed of the {@code dataBuffers} elements joined together. + * Depending on the implementation, the returned buffer may be a single buffer containing all + * data of the provided buffers, or it may be a true composite that contains references to the + * buffers. + *

    Note that the given data buffers do not have to be released, as they are + * released as part of the returned composite. + * @param dataBuffers the data buffers to be composed + * @return a buffer that is composed from the {@code dataBuffers} argument + * @since 5.0.3 + */ + DataBuffer join(List dataBuffers); + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferLimitException.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferLimitException.java new file mode 100644 index 0000000..c038390 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferLimitException.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.buffer; + +/** + * Exception that indicates the cumulative number of bytes consumed from a + * stream of {@link DataBuffer DataBuffer}'s exceeded some pre-configured limit. + * This can be raised when data buffers are cached and aggregated, e.g. + * {@link DataBufferUtils#join}. Or it could also be raised when data buffers + * have been released but a parsed representation is being aggregated, e.g. async + * parsing with Jackson, SSE parsing and aggregating lines per event. + * + * @author Rossen Stoyanchev + * @since 5.1.11 + */ +@SuppressWarnings("serial") +public class DataBufferLimitException extends IllegalStateException { + + + public DataBufferLimitException(String message) { + super(message); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java new file mode 100644 index 0000000..41a3760 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java @@ -0,0 +1,1153 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.buffer; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousFileChannel; +import java.nio.channels.Channel; +import java.nio.channels.Channels; +import java.nio.channels.CompletionHandler; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscription; +import reactor.core.publisher.BaseSubscriber; +import reactor.core.publisher.Flux; +import reactor.core.publisher.FluxSink; +import reactor.core.publisher.Mono; +import reactor.core.publisher.SynchronousSink; + +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Utility class for working with {@link DataBuffer DataBuffers}. + * + * @author Arjen Poutsma + * @author Brian Clozel + * @since 5.0 + */ +public abstract class DataBufferUtils { + + private final static Log logger = LogFactory.getLog(DataBufferUtils.class); + + private static final Consumer RELEASE_CONSUMER = DataBufferUtils::release; + + + //--------------------------------------------------------------------- + // Reading + //--------------------------------------------------------------------- + + /** + * Obtain a {@link InputStream} from the given supplier, and read it into a + * {@code Flux} of {@code DataBuffer}s. Closes the input stream when the + * Flux is terminated. + * @param inputStreamSupplier the supplier for the input stream to read from + * @param bufferFactory the factory to create data buffers with + * @param bufferSize the maximum size of the data buffers + * @return a Flux of data buffers read from the given channel + */ + public static Flux readInputStream( + Callable inputStreamSupplier, DataBufferFactory bufferFactory, int bufferSize) { + + Assert.notNull(inputStreamSupplier, "'inputStreamSupplier' must not be null"); + return readByteChannel(() -> Channels.newChannel(inputStreamSupplier.call()), bufferFactory, bufferSize); + } + + /** + * Obtain a {@link ReadableByteChannel} from the given supplier, and read + * it into a {@code Flux} of {@code DataBuffer}s. Closes the channel when + * the Flux is terminated. + * @param channelSupplier the supplier for the channel to read from + * @param bufferFactory the factory to create data buffers with + * @param bufferSize the maximum size of the data buffers + * @return a Flux of data buffers read from the given channel + */ + public static Flux readByteChannel( + Callable channelSupplier, DataBufferFactory bufferFactory, int bufferSize) { + + Assert.notNull(channelSupplier, "'channelSupplier' must not be null"); + Assert.notNull(bufferFactory, "'dataBufferFactory' must not be null"); + Assert.isTrue(bufferSize > 0, "'bufferSize' must be > 0"); + + return Flux.using(channelSupplier, + channel -> Flux.generate(new ReadableByteChannelGenerator(channel, bufferFactory, bufferSize)), + DataBufferUtils::closeChannel); + + // No doOnDiscard as operators used do not cache + } + + /** + * Obtain a {@code AsynchronousFileChannel} from the given supplier, and read + * it into a {@code Flux} of {@code DataBuffer}s. Closes the channel when + * the Flux is terminated. + * @param channelSupplier the supplier for the channel to read from + * @param bufferFactory the factory to create data buffers with + * @param bufferSize the maximum size of the data buffers + * @return a Flux of data buffers read from the given channel + */ + public static Flux readAsynchronousFileChannel( + Callable channelSupplier, DataBufferFactory bufferFactory, int bufferSize) { + + return readAsynchronousFileChannel(channelSupplier, 0, bufferFactory, bufferSize); + } + + /** + * Obtain a {@code AsynchronousFileChannel} from the given supplier, and + * read it into a {@code Flux} of {@code DataBuffer}s, starting at the given + * position. Closes the channel when the Flux is terminated. + * @param channelSupplier the supplier for the channel to read from + * @param position the position to start reading from + * @param bufferFactory the factory to create data buffers with + * @param bufferSize the maximum size of the data buffers + * @return a Flux of data buffers read from the given channel + */ + public static Flux readAsynchronousFileChannel( + Callable channelSupplier, long position, + DataBufferFactory bufferFactory, int bufferSize) { + + Assert.notNull(channelSupplier, "'channelSupplier' must not be null"); + Assert.notNull(bufferFactory, "'dataBufferFactory' must not be null"); + Assert.isTrue(position >= 0, "'position' must be >= 0"); + Assert.isTrue(bufferSize > 0, "'bufferSize' must be > 0"); + + Flux flux = Flux.using(channelSupplier, + channel -> Flux.create(sink -> { + ReadCompletionHandler handler = + new ReadCompletionHandler(channel, sink, position, bufferFactory, bufferSize); + sink.onCancel(handler::cancel); + sink.onRequest(handler::request); + }), + channel -> { + // Do not close channel from here, rather wait for the current read callback + // and then complete after releasing the DataBuffer. + }); + + return flux.doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release); + } + + /** + * Read bytes from the given file {@code Path} into a {@code Flux} of {@code DataBuffer}s. + * The method ensures that the file is closed when the flux is terminated. + * @param path the path to read bytes from + * @param bufferFactory the factory to create data buffers with + * @param bufferSize the maximum size of the data buffers + * @return a Flux of data buffers read from the given channel + * @since 5.2 + */ + public static Flux read( + Path path, DataBufferFactory bufferFactory, int bufferSize, OpenOption... options) { + + Assert.notNull(path, "Path must not be null"); + Assert.notNull(bufferFactory, "BufferFactory must not be null"); + Assert.isTrue(bufferSize > 0, "'bufferSize' must be > 0"); + if (options.length > 0) { + for (OpenOption option : options) { + Assert.isTrue(!(option == StandardOpenOption.APPEND || option == StandardOpenOption.WRITE), + "'" + option + "' not allowed"); + } + } + + return readAsynchronousFileChannel(() -> AsynchronousFileChannel.open(path, options), + bufferFactory, bufferSize); + } + + /** + * Read the given {@code Resource} into a {@code Flux} of {@code DataBuffer}s. + *

    If the resource is a file, it is read into an + * {@code AsynchronousFileChannel} and turned to {@code Flux} via + * {@link #readAsynchronousFileChannel(Callable, DataBufferFactory, int)} or else + * fall back to {@link #readByteChannel(Callable, DataBufferFactory, int)}. + * Closes the channel when the flux is terminated. + * @param resource the resource to read from + * @param bufferFactory the factory to create data buffers with + * @param bufferSize the maximum size of the data buffers + * @return a Flux of data buffers read from the given channel + */ + public static Flux read(Resource resource, DataBufferFactory bufferFactory, int bufferSize) { + return read(resource, 0, bufferFactory, bufferSize); + } + + /** + * Read the given {@code Resource} into a {@code Flux} of {@code DataBuffer}s + * starting at the given position. + *

    If the resource is a file, it is read into an + * {@code AsynchronousFileChannel} and turned to {@code Flux} via + * {@link #readAsynchronousFileChannel(Callable, DataBufferFactory, int)} or else + * fall back on {@link #readByteChannel(Callable, DataBufferFactory, int)}. + * Closes the channel when the flux is terminated. + * @param resource the resource to read from + * @param position the position to start reading from + * @param bufferFactory the factory to create data buffers with + * @param bufferSize the maximum size of the data buffers + * @return a Flux of data buffers read from the given channel + */ + public static Flux read( + Resource resource, long position, DataBufferFactory bufferFactory, int bufferSize) { + + try { + if (resource.isFile()) { + File file = resource.getFile(); + return readAsynchronousFileChannel( + () -> AsynchronousFileChannel.open(file.toPath(), StandardOpenOption.READ), + position, bufferFactory, bufferSize); + } + } + catch (IOException ignore) { + // fallback to resource.readableChannel(), below + } + Flux result = readByteChannel(resource::readableChannel, bufferFactory, bufferSize); + return position == 0 ? result : skipUntilByteCount(result, position); + } + + + //--------------------------------------------------------------------- + // Writing + //--------------------------------------------------------------------- + + /** + * Write the given stream of {@link DataBuffer DataBuffers} to the given + * {@code OutputStream}. Does not close the output stream + * when the flux is terminated, and does not + * {@linkplain #release(DataBuffer) release} the data buffers in the source. + * If releasing is required, then subscribe to the returned {@code Flux} + * with a {@link #releaseConsumer()}. + *

    Note that the writing process does not start until the returned + * {@code Flux} is subscribed to. + * @param source the stream of data buffers to be written + * @param outputStream the output stream to write to + * @return a Flux containing the same buffers as in {@code source}, that + * starts the writing process when subscribed to, and that publishes any + * writing errors and the completion signal + */ + public static Flux write(Publisher source, OutputStream outputStream) { + Assert.notNull(source, "'source' must not be null"); + Assert.notNull(outputStream, "'outputStream' must not be null"); + + WritableByteChannel channel = Channels.newChannel(outputStream); + return write(source, channel); + } + + /** + * Write the given stream of {@link DataBuffer DataBuffers} to the given + * {@code WritableByteChannel}. Does not close the channel + * when the flux is terminated, and does not + * {@linkplain #release(DataBuffer) release} the data buffers in the source. + * If releasing is required, then subscribe to the returned {@code Flux} + * with a {@link #releaseConsumer()}. + *

    Note that the writing process does not start until the returned + * {@code Flux} is subscribed to. + * @param source the stream of data buffers to be written + * @param channel the channel to write to + * @return a Flux containing the same buffers as in {@code source}, that + * starts the writing process when subscribed to, and that publishes any + * writing errors and the completion signal + */ + public static Flux write(Publisher source, WritableByteChannel channel) { + Assert.notNull(source, "'source' must not be null"); + Assert.notNull(channel, "'channel' must not be null"); + + Flux flux = Flux.from(source); + return Flux.create(sink -> { + WritableByteChannelSubscriber subscriber = new WritableByteChannelSubscriber(sink, channel); + sink.onDispose(subscriber); + flux.subscribe(subscriber); + }); + } + + /** + * Write the given stream of {@link DataBuffer DataBuffers} to the given + * {@code AsynchronousFileChannel}. Does not close the + * channel when the flux is terminated, and does not + * {@linkplain #release(DataBuffer) release} the data buffers in the source. + * If releasing is required, then subscribe to the returned {@code Flux} + * with a {@link #releaseConsumer()}. + *

    Note that the writing process does not start until the returned + * {@code Flux} is subscribed to. + * @param source the stream of data buffers to be written + * @param channel the channel to write to + * @return a Flux containing the same buffers as in {@code source}, that + * starts the writing process when subscribed to, and that publishes any + * writing errors and the completion signal + * @since 5.0.10 + */ + public static Flux write(Publisher source, AsynchronousFileChannel channel) { + return write(source, channel, 0); + } + + /** + * Write the given stream of {@link DataBuffer DataBuffers} to the given + * {@code AsynchronousFileChannel}. Does not close the channel + * when the flux is terminated, and does not + * {@linkplain #release(DataBuffer) release} the data buffers in the source. + * If releasing is required, then subscribe to the returned {@code Flux} with a + * {@link #releaseConsumer()}. + *

    Note that the writing process does not start until the returned + * {@code Flux} is subscribed to. + * @param source the stream of data buffers to be written + * @param channel the channel to write to + * @param position the file position where writing is to begin; must be non-negative + * @return a flux containing the same buffers as in {@code source}, that + * starts the writing process when subscribed to, and that publishes any + * writing errors and the completion signal + */ + public static Flux write( + Publisher source, AsynchronousFileChannel channel, long position) { + + Assert.notNull(source, "'source' must not be null"); + Assert.notNull(channel, "'channel' must not be null"); + Assert.isTrue(position >= 0, "'position' must be >= 0"); + + Flux flux = Flux.from(source); + return Flux.create(sink -> { + WriteCompletionHandler handler = new WriteCompletionHandler(sink, channel, position); + sink.onDispose(handler); + flux.subscribe(handler); + }); + + + } + + /** + * Write the given stream of {@link DataBuffer DataBuffers} to the given + * file {@link Path}. The optional {@code options} parameter specifies + * how the file is created or opened (defaults to + * {@link StandardOpenOption#CREATE CREATE}, + * {@link StandardOpenOption#TRUNCATE_EXISTING TRUNCATE_EXISTING}, and + * {@link StandardOpenOption#WRITE WRITE}). + * @param source the stream of data buffers to be written + * @param destination the path to the file + * @param options the options specifying how the file is opened + * @return a {@link Mono} that indicates completion or error + * @since 5.2 + */ + public static Mono write(Publisher source, Path destination, OpenOption... options) { + Assert.notNull(source, "Source must not be null"); + Assert.notNull(destination, "Destination must not be null"); + + Set optionSet = checkWriteOptions(options); + + return Mono.create(sink -> { + try { + AsynchronousFileChannel channel = AsynchronousFileChannel.open(destination, optionSet, null); + sink.onDispose(() -> closeChannel(channel)); + write(source, channel).subscribe(DataBufferUtils::release, + sink::error, + sink::success); + } + catch (IOException ex) { + sink.error(ex); + } + }); + } + + private static Set checkWriteOptions(OpenOption[] options) { + int length = options.length; + Set result = new HashSet<>(length + 3); + if (length == 0) { + result.add(StandardOpenOption.CREATE); + result.add(StandardOpenOption.TRUNCATE_EXISTING); + } + else { + for (OpenOption opt : options) { + if (opt == StandardOpenOption.READ) { + throw new IllegalArgumentException("READ not allowed"); + } + result.add(opt); + } + } + result.add(StandardOpenOption.WRITE); + return result; + } + + static void closeChannel(@Nullable Channel channel) { + if (channel != null && channel.isOpen()) { + try { + channel.close(); + } + catch (IOException ignored) { + } + } + } + + + //--------------------------------------------------------------------- + // Various + //--------------------------------------------------------------------- + + /** + * Relay buffers from the given {@link Publisher} until the total + * {@linkplain DataBuffer#readableByteCount() byte count} reaches + * the given maximum byte count, or until the publisher is complete. + * @param publisher the publisher to filter + * @param maxByteCount the maximum byte count + * @return a flux whose maximum byte count is {@code maxByteCount} + */ + public static Flux takeUntilByteCount(Publisher publisher, long maxByteCount) { + Assert.notNull(publisher, "Publisher must not be null"); + Assert.isTrue(maxByteCount >= 0, "'maxByteCount' must be a positive number"); + + return Flux.defer(() -> { + AtomicLong countDown = new AtomicLong(maxByteCount); + return Flux.from(publisher) + .map(buffer -> { + long remainder = countDown.addAndGet(-buffer.readableByteCount()); + if (remainder < 0) { + int length = buffer.readableByteCount() + (int) remainder; + return buffer.slice(0, length); + } + else { + return buffer; + } + }) + .takeUntil(buffer -> countDown.get() <= 0); + }); + + // No doOnDiscard as operators used do not cache (and drop) buffers + } + + /** + * Skip buffers from the given {@link Publisher} until the total + * {@linkplain DataBuffer#readableByteCount() byte count} reaches + * the given maximum byte count, or until the publisher is complete. + * @param publisher the publisher to filter + * @param maxByteCount the maximum byte count + * @return a flux with the remaining part of the given publisher + */ + public static Flux skipUntilByteCount(Publisher publisher, long maxByteCount) { + Assert.notNull(publisher, "Publisher must not be null"); + Assert.isTrue(maxByteCount >= 0, "'maxByteCount' must be a positive number"); + + return Flux.defer(() -> { + AtomicLong countDown = new AtomicLong(maxByteCount); + return Flux.from(publisher) + .skipUntil(buffer -> { + long remainder = countDown.addAndGet(-buffer.readableByteCount()); + return remainder < 0; + }) + .map(buffer -> { + long remainder = countDown.get(); + if (remainder < 0) { + countDown.set(0); + int start = buffer.readableByteCount() + (int)remainder; + int length = (int) -remainder; + return buffer.slice(start, length); + } + else { + return buffer; + } + }); + }).doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release); + } + + /** + * Retain the given data buffer, if it is a {@link PooledDataBuffer}. + * @param dataBuffer the data buffer to retain + * @return the retained buffer + */ + @SuppressWarnings("unchecked") + public static T retain(T dataBuffer) { + if (dataBuffer instanceof PooledDataBuffer) { + return (T) ((PooledDataBuffer) dataBuffer).retain(); + } + else { + return dataBuffer; + } + } + + /** + * Associate the given hint with the data buffer if it is a pooled buffer + * and supports leak tracking. + * @param dataBuffer the data buffer to attach the hint to + * @param hint the hint to attach + * @return the input buffer + * @since 5.3.2 + */ + @SuppressWarnings("unchecked") + public static T touch(T dataBuffer, Object hint) { + if (dataBuffer instanceof PooledDataBuffer) { + return (T) ((PooledDataBuffer) dataBuffer).touch(hint); + } + else { + return dataBuffer; + } + } + + /** + * Release the given data buffer, if it is a {@link PooledDataBuffer} and + * has been {@linkplain PooledDataBuffer#isAllocated() allocated}. + * @param dataBuffer the data buffer to release + * @return {@code true} if the buffer was released; {@code false} otherwise. + */ + public static boolean release(@Nullable DataBuffer dataBuffer) { + if (dataBuffer instanceof PooledDataBuffer) { + PooledDataBuffer pooledDataBuffer = (PooledDataBuffer) dataBuffer; + if (pooledDataBuffer.isAllocated()) { + try { + return pooledDataBuffer.release(); + } + catch (IllegalStateException ex) { + // Avoid dependency on Netty: IllegalReferenceCountException + if (logger.isDebugEnabled()) { + logger.debug("Failed to release PooledDataBuffer: " + dataBuffer, ex); + } + return false; + } + } + } + return false; + } + + /** + * Return a consumer that calls {@link #release(DataBuffer)} on all + * passed data buffers. + */ + public static Consumer releaseConsumer() { + return RELEASE_CONSUMER; + } + + /** + * Return a new {@code DataBuffer} composed from joining together the given + * {@code dataBuffers} elements. Depending on the {@link DataBuffer} type, + * the returned buffer may be a single buffer containing all data of the + * provided buffers, or it may be a zero-copy, composite with references to + * the given buffers. + *

    If {@code dataBuffers} produces an error or if there is a cancel + * signal, then all accumulated buffers will be + * {@linkplain #release(DataBuffer) released}. + *

    Note that the given data buffers do not have to be + * released. They will be released as part of the returned composite. + * @param dataBuffers the data buffers that are to be composed + * @return a buffer that is composed from the {@code dataBuffers} argument + * @since 5.0.3 + */ + public static Mono join(Publisher dataBuffers) { + return join(dataBuffers, -1); + } + + /** + * Variant of {@link #join(Publisher)} that behaves the same way up until + * the specified max number of bytes to buffer. Once the limit is exceeded, + * {@link DataBufferLimitException} is raised. + * @param buffers the data buffers that are to be composed + * @param maxByteCount the max number of bytes to buffer, or -1 for unlimited + * @return a buffer with the aggregated content, possibly an empty Mono if + * the max number of bytes to buffer is exceeded. + * @throws DataBufferLimitException if maxByteCount is exceeded + * @since 5.1.11 + */ + @SuppressWarnings("unchecked") + public static Mono join(Publisher buffers, int maxByteCount) { + Assert.notNull(buffers, "'dataBuffers' must not be null"); + + if (buffers instanceof Mono) { + return (Mono) buffers; + } + + return Flux.from(buffers) + .collect(() -> new LimitedDataBufferList(maxByteCount), LimitedDataBufferList::add) + .filter(list -> !list.isEmpty()) + .map(list -> list.get(0).factory().join(list)) + .doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release); + } + + /** + * Return a {@link Matcher} for the given delimiter. + * The matcher can be used to find the delimiters in a stream of data buffers. + * @param delimiter the delimiter bytes to find + * @return the matcher + * @since 5.2 + */ + public static Matcher matcher(byte[] delimiter) { + return createMatcher(delimiter); + } + + /** + * Return a {@link Matcher} for the given delimiters. + * The matcher can be used to find the delimiters in a stream of data buffers. + * @param delimiters the delimiters bytes to find + * @return the matcher + * @since 5.2 + */ + public static Matcher matcher(byte[]... delimiters) { + Assert.isTrue(delimiters.length > 0, "Delimiters must not be empty"); + return (delimiters.length == 1 ? createMatcher(delimiters[0]) : new CompositeMatcher(delimiters)); + } + + private static NestedMatcher createMatcher(byte[] delimiter) { + Assert.isTrue(delimiter.length > 0, "Delimiter must not be empty"); + switch (delimiter.length) { + case 1: + return (delimiter[0] == 10 ? SingleByteMatcher.NEWLINE_MATCHER : new SingleByteMatcher(delimiter)); + case 2: + return new TwoByteMatcher(delimiter); + default: + return new KnuthMorrisPrattMatcher(delimiter); + } + } + + + /** + * Contract to find delimiter(s) against one or more data buffers that can + * be passed one at a time to the {@link #match(DataBuffer)} method. + * + * @since 5.2 + * @see #match(DataBuffer) + */ + public interface Matcher { + + /** + * Find the first matching delimiter and return the index of the last + * byte of the delimiter, or {@code -1} if not found. + */ + int match(DataBuffer dataBuffer); + + /** + * Return the delimiter from the last invocation of {@link #match(DataBuffer)}. + */ + byte[] delimiter(); + + /** + * Reset the state of this matcher. + */ + void reset(); + } + + + /** + * Matcher that supports searching for multiple delimiters. + */ + private static class CompositeMatcher implements Matcher { + + private static final byte[] NO_DELIMITER = new byte[0]; + + + private final NestedMatcher[] matchers; + + byte[] longestDelimiter = NO_DELIMITER; + + CompositeMatcher(byte[][] delimiters) { + this.matchers = initMatchers(delimiters); + } + + private static NestedMatcher[] initMatchers(byte[][] delimiters) { + NestedMatcher[] matchers = new NestedMatcher[delimiters.length]; + for (int i = 0; i < delimiters.length; i++) { + matchers[i] = createMatcher(delimiters[i]); + } + return matchers; + } + + @Override + public int match(DataBuffer dataBuffer) { + this.longestDelimiter = NO_DELIMITER; + + for (int pos = dataBuffer.readPosition(); pos < dataBuffer.writePosition(); pos++) { + byte b = dataBuffer.getByte(pos); + + for (NestedMatcher matcher : this.matchers) { + if (matcher.match(b) && matcher.delimiter().length > this.longestDelimiter.length) { + this.longestDelimiter = matcher.delimiter(); + } + } + + if (this.longestDelimiter != NO_DELIMITER) { + reset(); + return pos; + } + } + return -1; + } + + @Override + public byte[] delimiter() { + Assert.state(this.longestDelimiter != NO_DELIMITER, "Illegal state!"); + return this.longestDelimiter; + } + + @Override + public void reset() { + for (NestedMatcher matcher : this.matchers) { + matcher.reset(); + } + } + } + + + /** + * Matcher that can be nested within {@link CompositeMatcher} where multiple + * matchers advance together using the same index, one byte at a time. + */ + private interface NestedMatcher extends Matcher { + + /** + * Perform a match against the next byte of the stream and return true + * if the delimiter is fully matched. + */ + boolean match(byte b); + + } + + + /** + * Matcher for a single byte delimiter. + */ + private static class SingleByteMatcher implements NestedMatcher { + + static SingleByteMatcher NEWLINE_MATCHER = new SingleByteMatcher(new byte[] {10}); + + private final byte[] delimiter; + + SingleByteMatcher(byte[] delimiter) { + Assert.isTrue(delimiter.length == 1, "Expected a 1 byte delimiter"); + this.delimiter = delimiter; + } + + @Override + public int match(DataBuffer dataBuffer) { + for (int pos = dataBuffer.readPosition(); pos < dataBuffer.writePosition(); pos++) { + byte b = dataBuffer.getByte(pos); + if (match(b)) { + return pos; + } + } + return -1; + } + + @Override + public boolean match(byte b) { + return this.delimiter[0] == b; + } + + @Override + public byte[] delimiter() { + return this.delimiter; + } + + @Override + public void reset() { + } + } + + + /** + * Base class for a {@link NestedMatcher}. + */ + private static abstract class AbstractNestedMatcher implements NestedMatcher { + + private final byte[] delimiter; + + private int matches = 0; + + + protected AbstractNestedMatcher(byte[] delimiter) { + this.delimiter = delimiter; + } + + protected void setMatches(int index) { + this.matches = index; + } + + protected int getMatches() { + return this.matches; + } + + @Override + public int match(DataBuffer dataBuffer) { + for (int pos = dataBuffer.readPosition(); pos < dataBuffer.writePosition(); pos++) { + byte b = dataBuffer.getByte(pos); + if (match(b)) { + reset(); + return pos; + } + } + return -1; + } + + @Override + public boolean match(byte b) { + if (b == this.delimiter[this.matches]) { + this.matches++; + return (this.matches == delimiter().length); + } + return false; + } + + @Override + public byte[] delimiter() { + return this.delimiter; + } + + @Override + public void reset() { + this.matches = 0; + } + } + + + /** + * Matcher with a 2 byte delimiter that does not benefit from a + * Knuth-Morris-Pratt suffix-prefix table. + */ + private static class TwoByteMatcher extends AbstractNestedMatcher { + + protected TwoByteMatcher(byte[] delimiter) { + super(delimiter); + Assert.isTrue(delimiter.length == 2, "Expected a 2-byte delimiter"); + } + } + + + /** + * Implementation of {@link Matcher} that uses the Knuth-Morris-Pratt algorithm. + * @see Knuth-Morris-Pratt string matching + */ + private static class KnuthMorrisPrattMatcher extends AbstractNestedMatcher { + + private final int[] table; + + public KnuthMorrisPrattMatcher(byte[] delimiter) { + super(delimiter); + this.table = longestSuffixPrefixTable(delimiter); + } + + private static int[] longestSuffixPrefixTable(byte[] delimiter) { + int[] result = new int[delimiter.length]; + result[0] = 0; + for (int i = 1; i < delimiter.length; i++) { + int j = result[i - 1]; + while (j > 0 && delimiter[i] != delimiter[j]) { + j = result[j - 1]; + } + if (delimiter[i] == delimiter[j]) { + j++; + } + result[i] = j; + } + return result; + } + + @Override + public boolean match(byte b) { + while (getMatches() > 0 && b != delimiter()[getMatches()]) { + setMatches(this.table[getMatches() - 1]); + } + return super.match(b); + } + } + + + private static class ReadableByteChannelGenerator implements Consumer> { + + private final ReadableByteChannel channel; + + private final DataBufferFactory dataBufferFactory; + + private final int bufferSize; + + public ReadableByteChannelGenerator( + ReadableByteChannel channel, DataBufferFactory dataBufferFactory, int bufferSize) { + + this.channel = channel; + this.dataBufferFactory = dataBufferFactory; + this.bufferSize = bufferSize; + } + + @Override + public void accept(SynchronousSink sink) { + boolean release = true; + DataBuffer dataBuffer = this.dataBufferFactory.allocateBuffer(this.bufferSize); + try { + int read; + ByteBuffer byteBuffer = dataBuffer.asByteBuffer(0, dataBuffer.capacity()); + if ((read = this.channel.read(byteBuffer)) >= 0) { + dataBuffer.writePosition(read); + release = false; + sink.next(dataBuffer); + } + else { + sink.complete(); + } + } + catch (IOException ex) { + sink.error(ex); + } + finally { + if (release) { + release(dataBuffer); + } + } + } + } + + + private static class ReadCompletionHandler implements CompletionHandler { + + private final AsynchronousFileChannel channel; + + private final FluxSink sink; + + private final DataBufferFactory dataBufferFactory; + + private final int bufferSize; + + private final AtomicLong position; + + private final AtomicReference state = new AtomicReference<>(State.IDLE); + + public ReadCompletionHandler(AsynchronousFileChannel channel, + FluxSink sink, long position, DataBufferFactory dataBufferFactory, int bufferSize) { + + this.channel = channel; + this.sink = sink; + this.position = new AtomicLong(position); + this.dataBufferFactory = dataBufferFactory; + this.bufferSize = bufferSize; + } + + /** + * Invoked when Reactive Streams consumer signals demand. + */ + public void request(long n) { + tryRead(); + } + + /** + * Invoked when Reactive Streams consumer cancels. + */ + public void cancel() { + this.state.getAndSet(State.DISPOSED); + + // According java.nio.channels.AsynchronousChannel "if an I/O operation is outstanding + // on the channel and the channel's close method is invoked, then the I/O operation + // fails with the exception AsynchronousCloseException". That should invoke the failed + // callback below and the current DataBuffer should be released. + + closeChannel(this.channel); + } + + private void tryRead() { + if (this.sink.requestedFromDownstream() > 0 && this.state.compareAndSet(State.IDLE, State.READING)) { + read(); + } + } + + private void read() { + DataBuffer dataBuffer = this.dataBufferFactory.allocateBuffer(this.bufferSize); + ByteBuffer byteBuffer = dataBuffer.asByteBuffer(0, this.bufferSize); + this.channel.read(byteBuffer, this.position.get(), dataBuffer, this); + } + + @Override + public void completed(Integer read, DataBuffer dataBuffer) { + if (this.state.get().equals(State.DISPOSED)) { + release(dataBuffer); + closeChannel(this.channel); + return; + } + + if (read == -1) { + release(dataBuffer); + closeChannel(this.channel); + this.state.set(State.DISPOSED); + this.sink.complete(); + return; + } + + this.position.addAndGet(read); + dataBuffer.writePosition(read); + this.sink.next(dataBuffer); + + // Stay in READING mode if there is demand + if (this.sink.requestedFromDownstream() > 0) { + read(); + return; + } + + // Release READING mode and then try again in case of concurrent "request" + if (this.state.compareAndSet(State.READING, State.IDLE)) { + tryRead(); + } + } + + @Override + public void failed(Throwable exc, DataBuffer dataBuffer) { + release(dataBuffer); + closeChannel(this.channel); + this.state.set(State.DISPOSED); + this.sink.error(exc); + } + + private enum State { + IDLE, READING, DISPOSED + } + } + + + private static class WritableByteChannelSubscriber extends BaseSubscriber { + + private final FluxSink sink; + + private final WritableByteChannel channel; + + public WritableByteChannelSubscriber(FluxSink sink, WritableByteChannel channel) { + this.sink = sink; + this.channel = channel; + } + + @Override + protected void hookOnSubscribe(Subscription subscription) { + request(1); + } + + @Override + protected void hookOnNext(DataBuffer dataBuffer) { + try { + ByteBuffer byteBuffer = dataBuffer.asByteBuffer(); + while (byteBuffer.hasRemaining()) { + this.channel.write(byteBuffer); + } + this.sink.next(dataBuffer); + request(1); + } + catch (IOException ex) { + this.sink.next(dataBuffer); + this.sink.error(ex); + } + } + + @Override + protected void hookOnError(Throwable throwable) { + this.sink.error(throwable); + } + + @Override + protected void hookOnComplete() { + this.sink.complete(); + } + } + + + private static class WriteCompletionHandler extends BaseSubscriber + implements CompletionHandler { + + private final FluxSink sink; + + private final AsynchronousFileChannel channel; + + private final AtomicBoolean completed = new AtomicBoolean(); + + private final AtomicReference error = new AtomicReference<>(); + + private final AtomicLong position; + + private final AtomicReference dataBuffer = new AtomicReference<>(); + + public WriteCompletionHandler( + FluxSink sink, AsynchronousFileChannel channel, long position) { + + this.sink = sink; + this.channel = channel; + this.position = new AtomicLong(position); + } + + @Override + protected void hookOnSubscribe(Subscription subscription) { + request(1); + } + + @Override + protected void hookOnNext(DataBuffer value) { + if (!this.dataBuffer.compareAndSet(null, value)) { + throw new IllegalStateException(); + } + ByteBuffer byteBuffer = value.asByteBuffer(); + this.channel.write(byteBuffer, this.position.get(), byteBuffer, this); + } + + @Override + protected void hookOnError(Throwable throwable) { + this.error.set(throwable); + + if (this.dataBuffer.get() == null) { + this.sink.error(throwable); + } + } + + @Override + protected void hookOnComplete() { + this.completed.set(true); + + if (this.dataBuffer.get() == null) { + this.sink.complete(); + } + } + + @Override + public void completed(Integer written, ByteBuffer byteBuffer) { + long pos = this.position.addAndGet(written); + if (byteBuffer.hasRemaining()) { + this.channel.write(byteBuffer, pos, byteBuffer, this); + return; + } + sinkDataBuffer(); + + Throwable throwable = this.error.get(); + if (throwable != null) { + this.sink.error(throwable); + } + else if (this.completed.get()) { + this.sink.complete(); + } + else { + request(1); + } + } + + @Override + public void failed(Throwable exc, ByteBuffer byteBuffer) { + sinkDataBuffer(); + this.sink.error(exc); + } + + private void sinkDataBuffer() { + DataBuffer dataBuffer = this.dataBuffer.get(); + Assert.state(dataBuffer != null, "DataBuffer should not be null"); + this.sink.next(dataBuffer); + this.dataBuffer.set(null); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferWrapper.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferWrapper.java new file mode 100644 index 0000000..d4084ec --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferWrapper.java @@ -0,0 +1,213 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.buffer; + +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.function.IntPredicate; + +import org.springframework.util.Assert; + +/** + * Provides a convenient implementation of the {@link DataBuffer} interface + * that can be overridden to adapt the delegate. + * + *

    These methods default to calling through to the wrapped delegate object. + * + * @author Arjen Poutsma + * @since 5.2 + */ +public class DataBufferWrapper implements DataBuffer { + + private final DataBuffer delegate; + + + /** + * Create a new {@code DataBufferWrapper} that wraps the given buffer. + * @param delegate the buffer to wrap + */ + public DataBufferWrapper(DataBuffer delegate) { + Assert.notNull(delegate, "Delegate must not be null"); + this.delegate = delegate; + } + + /** + * Return the wrapped delegate. + */ + public DataBuffer dataBuffer() { + return this.delegate; + } + + @Override + public DataBufferFactory factory() { + return this.delegate.factory(); + } + + @Override + public int indexOf(IntPredicate predicate, int fromIndex) { + return this.delegate.indexOf(predicate, fromIndex); + } + + @Override + public int lastIndexOf(IntPredicate predicate, int fromIndex) { + return this.delegate.lastIndexOf(predicate, fromIndex); + } + + @Override + public int readableByteCount() { + return this.delegate.readableByteCount(); + } + + @Override + public int writableByteCount() { + return this.delegate.writableByteCount(); + } + + @Override + public int capacity() { + return this.delegate.capacity(); + } + + @Override + public DataBuffer capacity(int capacity) { + return this.delegate.capacity(capacity); + } + + @Override + public DataBuffer ensureCapacity(int capacity) { + return this.delegate.ensureCapacity(capacity); + } + + @Override + public int readPosition() { + return this.delegate.readPosition(); + } + + @Override + public DataBuffer readPosition(int readPosition) { + return this.delegate.readPosition(readPosition); + } + + @Override + public int writePosition() { + return this.delegate.writePosition(); + } + + @Override + public DataBuffer writePosition(int writePosition) { + return this.delegate.writePosition(writePosition); + } + + @Override + public byte getByte(int index) { + return this.delegate.getByte(index); + } + + @Override + public byte read() { + return this.delegate.read(); + } + + @Override + public DataBuffer read(byte[] destination) { + return this.delegate.read(destination); + } + + @Override + public DataBuffer read(byte[] destination, int offset, int length) { + return this.delegate.read(destination, offset, length); + } + + @Override + public DataBuffer write(byte b) { + return this.delegate.write(b); + } + + @Override + public DataBuffer write(byte[] source) { + return this.delegate.write(source); + } + + @Override + public DataBuffer write(byte[] source, int offset, int length) { + return this.delegate.write(source, offset, length); + } + + @Override + public DataBuffer write(DataBuffer... buffers) { + return this.delegate.write(buffers); + } + + @Override + public DataBuffer write(ByteBuffer... buffers) { + return this.delegate.write(buffers); + } + + @Override + public DataBuffer write(CharSequence charSequence, + Charset charset) { + return this.delegate.write(charSequence, charset); + } + + @Override + public DataBuffer slice(int index, int length) { + return this.delegate.slice(index, length); + } + + @Override + public DataBuffer retainedSlice(int index, int length) { + return this.delegate.retainedSlice(index, length); + } + + @Override + public ByteBuffer asByteBuffer() { + return this.delegate.asByteBuffer(); + } + + @Override + public ByteBuffer asByteBuffer(int index, int length) { + return this.delegate.asByteBuffer(index, length); + } + + @Override + public InputStream asInputStream() { + return this.delegate.asInputStream(); + } + + @Override + public InputStream asInputStream(boolean releaseOnClose) { + return this.delegate.asInputStream(releaseOnClose); + } + + @Override + public OutputStream asOutputStream() { + return this.delegate.asOutputStream(); + } + + @Override + public String toString(Charset charset) { + return this.delegate.toString(charset); + } + + @Override + public String toString(int index, int length, Charset charset) { + return this.delegate.toString(index, length, charset); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java new file mode 100644 index 0000000..be2155f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java @@ -0,0 +1,532 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.buffer; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.function.IntPredicate; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Default implementation of the {@link DataBuffer} interface that uses a + * {@link ByteBuffer} internally. with separate read and write positions. + * Constructed using the {@link DefaultDataBufferFactory}. + * + *

    Inspired by Netty's {@code ByteBuf}. Introduced so that non-Netty runtimes + * (i.e. Servlet) do not require Netty on the classpath. + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @author Brian Clozel + * @since 5.0 + * @see DefaultDataBufferFactory + */ +public class DefaultDataBuffer implements DataBuffer { + + private static final int MAX_CAPACITY = Integer.MAX_VALUE; + + private static final int CAPACITY_THRESHOLD = 1024 * 1024 * 4; + + + private final DefaultDataBufferFactory dataBufferFactory; + + private ByteBuffer byteBuffer; + + private int capacity; + + private int readPosition; + + private int writePosition; + + + private DefaultDataBuffer(DefaultDataBufferFactory dataBufferFactory, ByteBuffer byteBuffer) { + Assert.notNull(dataBufferFactory, "DefaultDataBufferFactory must not be null"); + Assert.notNull(byteBuffer, "ByteBuffer must not be null"); + this.dataBufferFactory = dataBufferFactory; + ByteBuffer slice = byteBuffer.slice(); + this.byteBuffer = slice; + this.capacity = slice.remaining(); + } + + static DefaultDataBuffer fromFilledByteBuffer(DefaultDataBufferFactory dataBufferFactory, ByteBuffer byteBuffer) { + DefaultDataBuffer dataBuffer = new DefaultDataBuffer(dataBufferFactory, byteBuffer); + dataBuffer.writePosition(byteBuffer.remaining()); + return dataBuffer; + } + + static DefaultDataBuffer fromEmptyByteBuffer(DefaultDataBufferFactory dataBufferFactory, ByteBuffer byteBuffer) { + return new DefaultDataBuffer(dataBufferFactory, byteBuffer); + } + + + /** + * Directly exposes the native {@code ByteBuffer} that this buffer is based + * on also updating the {@code ByteBuffer's} position and limit to match + * the current {@link #readPosition()} and {@link #readableByteCount()}. + * @return the wrapped byte buffer + */ + public ByteBuffer getNativeBuffer() { + this.byteBuffer.position(this.readPosition); + this.byteBuffer.limit(readableByteCount()); + return this.byteBuffer; + } + + private void setNativeBuffer(ByteBuffer byteBuffer) { + this.byteBuffer = byteBuffer; + this.capacity = byteBuffer.remaining(); + } + + + @Override + public DefaultDataBufferFactory factory() { + return this.dataBufferFactory; + } + + @Override + public int indexOf(IntPredicate predicate, int fromIndex) { + Assert.notNull(predicate, "IntPredicate must not be null"); + if (fromIndex < 0) { + fromIndex = 0; + } + else if (fromIndex >= this.writePosition) { + return -1; + } + for (int i = fromIndex; i < this.writePosition; i++) { + byte b = this.byteBuffer.get(i); + if (predicate.test(b)) { + return i; + } + } + return -1; + } + + @Override + public int lastIndexOf(IntPredicate predicate, int fromIndex) { + Assert.notNull(predicate, "IntPredicate must not be null"); + int i = Math.min(fromIndex, this.writePosition - 1); + for (; i >= 0; i--) { + byte b = this.byteBuffer.get(i); + if (predicate.test(b)) { + return i; + } + } + return -1; + } + + @Override + public int readableByteCount() { + return this.writePosition - this.readPosition; + } + + @Override + public int writableByteCount() { + return this.capacity - this.writePosition; + } + + @Override + public int readPosition() { + return this.readPosition; + } + + @Override + public DefaultDataBuffer readPosition(int readPosition) { + assertIndex(readPosition >= 0, "'readPosition' %d must be >= 0", readPosition); + assertIndex(readPosition <= this.writePosition, "'readPosition' %d must be <= %d", + readPosition, this.writePosition); + this.readPosition = readPosition; + return this; + } + + @Override + public int writePosition() { + return this.writePosition; + } + + @Override + public DefaultDataBuffer writePosition(int writePosition) { + assertIndex(writePosition >= this.readPosition, "'writePosition' %d must be >= %d", + writePosition, this.readPosition); + assertIndex(writePosition <= this.capacity, "'writePosition' %d must be <= %d", + writePosition, this.capacity); + this.writePosition = writePosition; + return this; + } + + @Override + public int capacity() { + return this.capacity; + } + + @Override + public DefaultDataBuffer capacity(int newCapacity) { + if (newCapacity <= 0) { + throw new IllegalArgumentException(String.format("'newCapacity' %d must be higher than 0", newCapacity)); + } + int readPosition = readPosition(); + int writePosition = writePosition(); + int oldCapacity = capacity(); + + if (newCapacity > oldCapacity) { + ByteBuffer oldBuffer = this.byteBuffer; + ByteBuffer newBuffer = allocate(newCapacity, oldBuffer.isDirect()); + oldBuffer.position(0).limit(oldBuffer.capacity()); + newBuffer.position(0).limit(oldBuffer.capacity()); + newBuffer.put(oldBuffer); + newBuffer.clear(); + setNativeBuffer(newBuffer); + } + else if (newCapacity < oldCapacity) { + ByteBuffer oldBuffer = this.byteBuffer; + ByteBuffer newBuffer = allocate(newCapacity, oldBuffer.isDirect()); + if (readPosition < newCapacity) { + if (writePosition > newCapacity) { + writePosition = newCapacity; + writePosition(writePosition); + } + oldBuffer.position(readPosition).limit(writePosition); + newBuffer.position(readPosition).limit(writePosition); + newBuffer.put(oldBuffer); + newBuffer.clear(); + } + else { + readPosition(newCapacity); + writePosition(newCapacity); + } + setNativeBuffer(newBuffer); + } + return this; + } + + @Override + public DataBuffer ensureCapacity(int length) { + if (length > writableByteCount()) { + int newCapacity = calculateCapacity(this.writePosition + length); + capacity(newCapacity); + } + return this; + } + + private static ByteBuffer allocate(int capacity, boolean direct) { + return (direct ? ByteBuffer.allocateDirect(capacity) : ByteBuffer.allocate(capacity)); + } + + @Override + public byte getByte(int index) { + assertIndex(index >= 0, "index %d must be >= 0", index); + assertIndex(index <= this.writePosition - 1, "index %d must be <= %d", index, this.writePosition - 1); + return this.byteBuffer.get(index); + } + + @Override + public byte read() { + assertIndex(this.readPosition <= this.writePosition - 1, "readPosition %d must be <= %d", + this.readPosition, this.writePosition - 1); + int pos = this.readPosition; + byte b = this.byteBuffer.get(pos); + this.readPosition = pos + 1; + return b; + } + + @Override + public DefaultDataBuffer read(byte[] destination) { + Assert.notNull(destination, "Byte array must not be null"); + read(destination, 0, destination.length); + return this; + } + + @Override + public DefaultDataBuffer read(byte[] destination, int offset, int length) { + Assert.notNull(destination, "Byte array must not be null"); + assertIndex(this.readPosition <= this.writePosition - length, + "readPosition %d and length %d should be smaller than writePosition %d", + this.readPosition, length, this.writePosition); + + ByteBuffer tmp = this.byteBuffer.duplicate(); + int limit = this.readPosition + length; + tmp.clear().position(this.readPosition).limit(limit); + tmp.get(destination, offset, length); + + this.readPosition += length; + return this; + } + + @Override + public DefaultDataBuffer write(byte b) { + ensureCapacity(1); + int pos = this.writePosition; + this.byteBuffer.put(pos, b); + this.writePosition = pos + 1; + return this; + } + + @Override + public DefaultDataBuffer write(byte[] source) { + Assert.notNull(source, "Byte array must not be null"); + write(source, 0, source.length); + return this; + } + + @Override + public DefaultDataBuffer write(byte[] source, int offset, int length) { + Assert.notNull(source, "Byte array must not be null"); + ensureCapacity(length); + + ByteBuffer tmp = this.byteBuffer.duplicate(); + int limit = this.writePosition + length; + tmp.clear().position(this.writePosition).limit(limit); + tmp.put(source, offset, length); + + this.writePosition += length; + return this; + } + + @Override + public DefaultDataBuffer write(DataBuffer... buffers) { + if (!ObjectUtils.isEmpty(buffers)) { + write(Arrays.stream(buffers).map(DataBuffer::asByteBuffer).toArray(ByteBuffer[]::new)); + } + return this; + } + + @Override + public DefaultDataBuffer write(ByteBuffer... buffers) { + if (!ObjectUtils.isEmpty(buffers)) { + int capacity = Arrays.stream(buffers).mapToInt(ByteBuffer::remaining).sum(); + ensureCapacity(capacity); + Arrays.stream(buffers).forEach(this::write); + } + return this; + } + + private void write(ByteBuffer source) { + int length = source.remaining(); + ByteBuffer tmp = this.byteBuffer.duplicate(); + int limit = this.writePosition + source.remaining(); + tmp.clear().position(this.writePosition).limit(limit); + tmp.put(source); + this.writePosition += length; + } + + @Override + public DefaultDataBuffer slice(int index, int length) { + checkIndex(index, length); + int oldPosition = this.byteBuffer.position(); + // Explicit access via Buffer base type for compatibility + // with covariant return type on JDK 9's ByteBuffer... + Buffer buffer = this.byteBuffer; + try { + buffer.position(index); + ByteBuffer slice = this.byteBuffer.slice(); + // Explicit cast for compatibility with covariant return type on JDK 9's ByteBuffer + slice.limit(length); + return new SlicedDefaultDataBuffer(slice, this.dataBufferFactory, length); + } + finally { + buffer.position(oldPosition); + } + } + + @Override + public ByteBuffer asByteBuffer() { + return asByteBuffer(this.readPosition, readableByteCount()); + } + + @Override + public ByteBuffer asByteBuffer(int index, int length) { + checkIndex(index, length); + + ByteBuffer duplicate = this.byteBuffer.duplicate(); + // Explicit access via Buffer base type for compatibility + // with covariant return type on JDK 9's ByteBuffer... + Buffer buffer = duplicate; + buffer.position(index); + buffer.limit(index + length); + return duplicate.slice(); + } + + @Override + public InputStream asInputStream() { + return new DefaultDataBufferInputStream(); + } + + @Override + public InputStream asInputStream(boolean releaseOnClose) { + return new DefaultDataBufferInputStream(); + } + + @Override + public OutputStream asOutputStream() { + return new DefaultDataBufferOutputStream(); + } + + + @Override + public String toString(int index, int length, Charset charset) { + checkIndex(index, length); + Assert.notNull(charset, "Charset must not be null"); + + byte[] bytes; + int offset; + + if (this.byteBuffer.hasArray()) { + bytes = this.byteBuffer.array(); + offset = this.byteBuffer.arrayOffset() + index; + } + else { + bytes = new byte[length]; + offset = 0; + ByteBuffer duplicate = this.byteBuffer.duplicate(); + duplicate.clear().position(index).limit(index + length); + duplicate.get(bytes, 0, length); + } + return new String(bytes, offset, length, charset); + } + + /** + * Calculate the capacity of the buffer. + * @see io.netty.buffer.AbstractByteBufAllocator#calculateNewCapacity(int, int) + */ + private int calculateCapacity(int neededCapacity) { + Assert.isTrue(neededCapacity >= 0, "'neededCapacity' must >= 0"); + + if (neededCapacity == CAPACITY_THRESHOLD) { + return CAPACITY_THRESHOLD; + } + else if (neededCapacity > CAPACITY_THRESHOLD) { + int newCapacity = neededCapacity / CAPACITY_THRESHOLD * CAPACITY_THRESHOLD; + if (newCapacity > MAX_CAPACITY - CAPACITY_THRESHOLD) { + newCapacity = MAX_CAPACITY; + } + else { + newCapacity += CAPACITY_THRESHOLD; + } + return newCapacity; + } + else { + int newCapacity = 64; + while (newCapacity < neededCapacity) { + newCapacity <<= 1; + } + return Math.min(newCapacity, MAX_CAPACITY); + } + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof DefaultDataBuffer)) { + return false; + } + DefaultDataBuffer otherBuffer = (DefaultDataBuffer) other; + return (this.readPosition == otherBuffer.readPosition && + this.writePosition == otherBuffer.writePosition && + this.byteBuffer.equals(otherBuffer.byteBuffer)); + } + + @Override + public int hashCode() { + return this.byteBuffer.hashCode(); + } + + @Override + public String toString() { + return String.format("DefaultDataBuffer (r: %d, w: %d, c: %d)", + this.readPosition, this.writePosition, this.capacity); + } + + + private void checkIndex(int index, int length) { + assertIndex(index >= 0, "index %d must be >= 0", index); + assertIndex(length >= 0, "length %d must be >= 0", index); + assertIndex(index <= this.capacity, "index %d must be <= %d", index, this.capacity); + assertIndex(length <= this.capacity, "length %d must be <= %d", index, this.capacity); + } + + private void assertIndex(boolean expression, String format, Object... args) { + if (!expression) { + String message = String.format(format, args); + throw new IndexOutOfBoundsException(message); + } + } + + + private class DefaultDataBufferInputStream extends InputStream { + + @Override + public int available() { + return readableByteCount(); + } + + @Override + public int read() { + return available() > 0 ? DefaultDataBuffer.this.read() & 0xFF : -1; + } + + @Override + public int read(byte[] bytes, int off, int len) throws IOException { + int available = available(); + if (available > 0) { + len = Math.min(len, available); + DefaultDataBuffer.this.read(bytes, off, len); + return len; + } + else { + return -1; + } + } + } + + + private class DefaultDataBufferOutputStream extends OutputStream { + + @Override + public void write(int b) throws IOException { + DefaultDataBuffer.this.write((byte) b); + } + + @Override + public void write(byte[] bytes, int off, int len) throws IOException { + DefaultDataBuffer.this.write(bytes, off, len); + } + } + + + private static class SlicedDefaultDataBuffer extends DefaultDataBuffer { + + SlicedDefaultDataBuffer(ByteBuffer byteBuffer, DefaultDataBufferFactory dataBufferFactory, int length) { + super(dataBufferFactory, byteBuffer); + writePosition(length); + } + + @Override + public DefaultDataBuffer capacity(int newCapacity) { + throw new UnsupportedOperationException("Changing the capacity of a sliced buffer is not supported"); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferFactory.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferFactory.java new file mode 100644 index 0000000..9da24e5 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBufferFactory.java @@ -0,0 +1,131 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.buffer; + +import java.nio.ByteBuffer; +import java.util.List; + +import org.springframework.util.Assert; + +/** + * Default implementation of the {@code DataBufferFactory} interface. Allows for + * specification of the default initial capacity at construction time, as well + * as whether heap-based or direct buffers are to be preferred. + * + * @author Arjen Poutsma + * @since 5.0 + */ +public class DefaultDataBufferFactory implements DataBufferFactory { + + /** + * The default capacity when none is specified. + * @see #DefaultDataBufferFactory() + * @see #DefaultDataBufferFactory(boolean) + */ + public static final int DEFAULT_INITIAL_CAPACITY = 256; + + /** + * Shared instance based on the default constructor. + * @since 5.3 + */ + public static final DefaultDataBufferFactory sharedInstance = new DefaultDataBufferFactory(); + + + private final boolean preferDirect; + + private final int defaultInitialCapacity; + + + /** + * Creates a new {@code DefaultDataBufferFactory} with default settings. + * @see #sharedInstance + */ + public DefaultDataBufferFactory() { + this(false); + } + + /** + * Creates a new {@code DefaultDataBufferFactory}, indicating whether direct + * buffers should be created by {@link #allocateBuffer()} and + * {@link #allocateBuffer(int)}. + * @param preferDirect {@code true} if direct buffers are to be preferred; + * {@code false} otherwise + */ + public DefaultDataBufferFactory(boolean preferDirect) { + this(preferDirect, DEFAULT_INITIAL_CAPACITY); + } + + /** + * Creates a new {@code DefaultDataBufferFactory}, indicating whether direct + * buffers should be created by {@link #allocateBuffer()} and + * {@link #allocateBuffer(int)}, and what the capacity is to be used for + * {@link #allocateBuffer()}. + * @param preferDirect {@code true} if direct buffers are to be preferred; + * {@code false} otherwise + */ + public DefaultDataBufferFactory(boolean preferDirect, int defaultInitialCapacity) { + Assert.isTrue(defaultInitialCapacity > 0, "'defaultInitialCapacity' should be larger than 0"); + this.preferDirect = preferDirect; + this.defaultInitialCapacity = defaultInitialCapacity; + } + + + @Override + public DefaultDataBuffer allocateBuffer() { + return allocateBuffer(this.defaultInitialCapacity); + } + + @Override + public DefaultDataBuffer allocateBuffer(int initialCapacity) { + ByteBuffer byteBuffer = (this.preferDirect ? + ByteBuffer.allocateDirect(initialCapacity) : + ByteBuffer.allocate(initialCapacity)); + return DefaultDataBuffer.fromEmptyByteBuffer(this, byteBuffer); + } + + @Override + public DefaultDataBuffer wrap(ByteBuffer byteBuffer) { + return DefaultDataBuffer.fromFilledByteBuffer(this, byteBuffer.slice()); + } + + @Override + public DefaultDataBuffer wrap(byte[] bytes) { + return DefaultDataBuffer.fromFilledByteBuffer(this, ByteBuffer.wrap(bytes)); + } + + /** + * {@inheritDoc} + *

    This implementation creates a single {@link DefaultDataBuffer} + * to contain the data in {@code dataBuffers}. + */ + @Override + public DefaultDataBuffer join(List dataBuffers) { + Assert.notEmpty(dataBuffers, "DataBuffer List must not be empty"); + int capacity = dataBuffers.stream().mapToInt(DataBuffer::readableByteCount).sum(); + DefaultDataBuffer result = allocateBuffer(capacity); + dataBuffers.forEach(result::write); + dataBuffers.forEach(DataBufferUtils::release); + return result; + } + + + @Override + public String toString() { + return "DefaultDataBufferFactory (preferDirect=" + this.preferDirect + ")"; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/LimitedDataBufferList.java b/spring-core/src/main/java/org/springframework/core/io/buffer/LimitedDataBufferList.java new file mode 100644 index 0000000..d95e426 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/LimitedDataBufferList.java @@ -0,0 +1,154 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.buffer; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Predicate; + +import reactor.core.publisher.Flux; + +/** + * Custom {@link List} to collect data buffers with and enforce a + * limit on the total number of bytes buffered. For use with "collect" or + * other buffering operators in declarative APIs, e.g. {@link Flux}. + * + *

    Adding elements increases the byte count and if the limit is exceeded, + * {@link DataBufferLimitException} is raised. {@link #clear()} resets the + * count. Remove and set are not supported. + * + *

    Note: This class does not automatically release the + * buffers it contains. It is usually preferable to use hooks such as + * {@link Flux#doOnDiscard} that also take care of cancel and error signals, + * or otherwise {@link #releaseAndClear()} can be used. + * + * @author Rossen Stoyanchev + * @since 5.1.11 + */ +@SuppressWarnings("serial") +public class LimitedDataBufferList extends ArrayList { + + private final int maxByteCount; + + private int byteCount; + + + public LimitedDataBufferList(int maxByteCount) { + this.maxByteCount = maxByteCount; + } + + + @Override + public boolean add(DataBuffer buffer) { + updateCount(buffer.readableByteCount()); + return super.add(buffer); + } + + @Override + public void add(int index, DataBuffer buffer) { + super.add(index, buffer); + updateCount(buffer.readableByteCount()); + } + + @Override + public boolean addAll(Collection collection) { + boolean result = super.addAll(collection); + collection.forEach(buffer -> updateCount(buffer.readableByteCount())); + return result; + } + + @Override + public boolean addAll(int index, Collection collection) { + boolean result = super.addAll(index, collection); + collection.forEach(buffer -> updateCount(buffer.readableByteCount())); + return result; + } + + private void updateCount(int bytesToAdd) { + if (this.maxByteCount < 0) { + return; + } + if (bytesToAdd > Integer.MAX_VALUE - this.byteCount) { + raiseLimitException(); + } + else { + this.byteCount += bytesToAdd; + if (this.byteCount > this.maxByteCount) { + raiseLimitException(); + } + } + } + + private void raiseLimitException() { + // Do not release here, it's likely down via doOnDiscard.. + throw new DataBufferLimitException( + "Exceeded limit on max bytes to buffer : " + this.maxByteCount); + } + + @Override + public DataBuffer remove(int index) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + protected void removeRange(int fromIndex, int toIndex) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeIf(Predicate filter) { + throw new UnsupportedOperationException(); + } + + @Override + public DataBuffer set(int index, DataBuffer element) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + this.byteCount = 0; + super.clear(); + } + + /** + * Shortcut to {@link DataBufferUtils#release release} all data buffers and + * then {@link #clear()}. + */ + public void releaseAndClear() { + forEach(buf -> { + try { + DataBufferUtils.release(buf); + } + catch (Throwable ex) { + // Keep going.. + } + }); + clear(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java b/spring-core/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java new file mode 100644 index 0000000..7809c65 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java @@ -0,0 +1,346 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.buffer; + +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.function.IntPredicate; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufInputStream; +import io.netty.buffer.ByteBufOutputStream; +import io.netty.buffer.ByteBufUtil; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Implementation of the {@code DataBuffer} interface that wraps a Netty + * {@link ByteBuf}. Typically constructed with {@link NettyDataBufferFactory}. + * + * @author Arjen Poutsma + * @author Brian Clozel + * @since 5.0 + */ +public class NettyDataBuffer implements PooledDataBuffer { + + private final ByteBuf byteBuf; + + private final NettyDataBufferFactory dataBufferFactory; + + + /** + * Create a new {@code NettyDataBuffer} based on the given {@code ByteBuff}. + * @param byteBuf the buffer to base this buffer on + */ + NettyDataBuffer(ByteBuf byteBuf, NettyDataBufferFactory dataBufferFactory) { + Assert.notNull(byteBuf, "ByteBuf must not be null"); + Assert.notNull(dataBufferFactory, "NettyDataBufferFactory must not be null"); + this.byteBuf = byteBuf; + this.dataBufferFactory = dataBufferFactory; + } + + + /** + * Directly exposes the native {@code ByteBuf} that this buffer is based on. + * @return the wrapped byte buffer + */ + public ByteBuf getNativeBuffer() { + return this.byteBuf; + } + + @Override + public NettyDataBufferFactory factory() { + return this.dataBufferFactory; + } + + @Override + public int indexOf(IntPredicate predicate, int fromIndex) { + Assert.notNull(predicate, "IntPredicate must not be null"); + if (fromIndex < 0) { + fromIndex = 0; + } + else if (fromIndex >= this.byteBuf.writerIndex()) { + return -1; + } + int length = this.byteBuf.writerIndex() - fromIndex; + return this.byteBuf.forEachByte(fromIndex, length, predicate.negate()::test); + } + + @Override + public int lastIndexOf(IntPredicate predicate, int fromIndex) { + Assert.notNull(predicate, "IntPredicate must not be null"); + if (fromIndex < 0) { + return -1; + } + fromIndex = Math.min(fromIndex, this.byteBuf.writerIndex() - 1); + return this.byteBuf.forEachByteDesc(0, fromIndex + 1, predicate.negate()::test); + } + + @Override + public int readableByteCount() { + return this.byteBuf.readableBytes(); + } + + @Override + public int writableByteCount() { + return this.byteBuf.writableBytes(); + } + + @Override + public int readPosition() { + return this.byteBuf.readerIndex(); + } + + @Override + public NettyDataBuffer readPosition(int readPosition) { + this.byteBuf.readerIndex(readPosition); + return this; + } + + @Override + public int writePosition() { + return this.byteBuf.writerIndex(); + } + + @Override + public NettyDataBuffer writePosition(int writePosition) { + this.byteBuf.writerIndex(writePosition); + return this; + } + + @Override + public byte getByte(int index) { + return this.byteBuf.getByte(index); + } + + @Override + public int capacity() { + return this.byteBuf.capacity(); + } + + @Override + public NettyDataBuffer capacity(int capacity) { + this.byteBuf.capacity(capacity); + return this; + } + + @Override + public DataBuffer ensureCapacity(int capacity) { + this.byteBuf.ensureWritable(capacity); + return this; + } + + @Override + public byte read() { + return this.byteBuf.readByte(); + } + + @Override + public NettyDataBuffer read(byte[] destination) { + this.byteBuf.readBytes(destination); + return this; + } + + @Override + public NettyDataBuffer read(byte[] destination, int offset, int length) { + this.byteBuf.readBytes(destination, offset, length); + return this; + } + + @Override + public NettyDataBuffer write(byte b) { + this.byteBuf.writeByte(b); + return this; + } + + @Override + public NettyDataBuffer write(byte[] source) { + this.byteBuf.writeBytes(source); + return this; + } + + @Override + public NettyDataBuffer write(byte[] source, int offset, int length) { + this.byteBuf.writeBytes(source, offset, length); + return this; + } + + @Override + public NettyDataBuffer write(DataBuffer... buffers) { + if (!ObjectUtils.isEmpty(buffers)) { + if (hasNettyDataBuffers(buffers)) { + ByteBuf[] nativeBuffers = new ByteBuf[buffers.length]; + for (int i = 0; i < buffers.length; i++) { + nativeBuffers[i] = ((NettyDataBuffer) buffers[i]).getNativeBuffer(); + } + write(nativeBuffers); + } + else { + ByteBuffer[] byteBuffers = new ByteBuffer[buffers.length]; + for (int i = 0; i < buffers.length; i++) { + byteBuffers[i] = buffers[i].asByteBuffer(); + + } + write(byteBuffers); + } + } + return this; + } + + private static boolean hasNettyDataBuffers(DataBuffer[] buffers) { + for (DataBuffer buffer : buffers) { + if (!(buffer instanceof NettyDataBuffer)) { + return false; + } + } + return true; + } + + @Override + public NettyDataBuffer write(ByteBuffer... buffers) { + if (!ObjectUtils.isEmpty(buffers)) { + for (ByteBuffer buffer : buffers) { + this.byteBuf.writeBytes(buffer); + } + } + return this; + } + + /** + * Writes one or more Netty {@link ByteBuf ByteBufs} to this buffer, + * starting at the current writing position. + * @param byteBufs the buffers to write into this buffer + * @return this buffer + */ + public NettyDataBuffer write(ByteBuf... byteBufs) { + if (!ObjectUtils.isEmpty(byteBufs)) { + for (ByteBuf byteBuf : byteBufs) { + this.byteBuf.writeBytes(byteBuf); + } + } + return this; + } + + @Override + public DataBuffer write(CharSequence charSequence, Charset charset) { + Assert.notNull(charSequence, "CharSequence must not be null"); + Assert.notNull(charset, "Charset must not be null"); + if (StandardCharsets.UTF_8.equals(charset)) { + ByteBufUtil.writeUtf8(this.byteBuf, charSequence); + } + else if (StandardCharsets.US_ASCII.equals(charset)) { + ByteBufUtil.writeAscii(this.byteBuf, charSequence); + } + else { + return PooledDataBuffer.super.write(charSequence, charset); + } + return this; + } + + @Override + public NettyDataBuffer slice(int index, int length) { + ByteBuf slice = this.byteBuf.slice(index, length); + return new NettyDataBuffer(slice, this.dataBufferFactory); + } + + @Override + public NettyDataBuffer retainedSlice(int index, int length) { + ByteBuf slice = this.byteBuf.retainedSlice(index, length); + return new NettyDataBuffer(slice, this.dataBufferFactory); + } + + @Override + public ByteBuffer asByteBuffer() { + return this.byteBuf.nioBuffer(); + } + + @Override + public ByteBuffer asByteBuffer(int index, int length) { + return this.byteBuf.nioBuffer(index, length); + } + + @Override + public InputStream asInputStream() { + return new ByteBufInputStream(this.byteBuf); + } + + @Override + public InputStream asInputStream(boolean releaseOnClose) { + return new ByteBufInputStream(this.byteBuf, releaseOnClose); + } + + @Override + public OutputStream asOutputStream() { + return new ByteBufOutputStream(this.byteBuf); + } + + @Override + public String toString(Charset charset) { + Assert.notNull(charset, "Charset must not be null"); + return this.byteBuf.toString(charset); + } + + @Override + public String toString(int index, int length, Charset charset) { + Assert.notNull(charset, "Charset must not be null"); + return this.byteBuf.toString(index, length, charset); + } + + @Override + public boolean isAllocated() { + return this.byteBuf.refCnt() > 0; + } + + @Override + public PooledDataBuffer retain() { + return new NettyDataBuffer(this.byteBuf.retain(), this.dataBufferFactory); + } + + @Override + public PooledDataBuffer touch(Object hint) { + this.byteBuf.touch(hint); + return this; + } + + @Override + public boolean release() { + return this.byteBuf.release(); + } + + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof NettyDataBuffer && + this.byteBuf.equals(((NettyDataBuffer) other).byteBuf))); + } + + @Override + public int hashCode() { + return this.byteBuf.hashCode(); + } + + @Override + public String toString() { + return this.byteBuf.toString(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/NettyDataBufferFactory.java b/spring-core/src/main/java/org/springframework/core/io/buffer/NettyDataBufferFactory.java new file mode 100644 index 0000000..14931d4 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/NettyDataBufferFactory.java @@ -0,0 +1,139 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.buffer; + +import java.nio.ByteBuffer; +import java.util.List; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.Unpooled; + +import org.springframework.util.Assert; + +/** + * Implementation of the {@code DataBufferFactory} interface based on a + * Netty {@link ByteBufAllocator}. + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @since 5.0 + * @see io.netty.buffer.PooledByteBufAllocator + * @see io.netty.buffer.UnpooledByteBufAllocator + */ +public class NettyDataBufferFactory implements DataBufferFactory { + + private final ByteBufAllocator byteBufAllocator; + + + /** + * Create a new {@code NettyDataBufferFactory} based on the given factory. + * @param byteBufAllocator the factory to use + * @see io.netty.buffer.PooledByteBufAllocator + * @see io.netty.buffer.UnpooledByteBufAllocator + */ + public NettyDataBufferFactory(ByteBufAllocator byteBufAllocator) { + Assert.notNull(byteBufAllocator, "ByteBufAllocator must not be null"); + this.byteBufAllocator = byteBufAllocator; + } + + + /** + * Return the {@code ByteBufAllocator} used by this factory. + */ + public ByteBufAllocator getByteBufAllocator() { + return this.byteBufAllocator; + } + + @Override + public NettyDataBuffer allocateBuffer() { + ByteBuf byteBuf = this.byteBufAllocator.buffer(); + return new NettyDataBuffer(byteBuf, this); + } + + @Override + public NettyDataBuffer allocateBuffer(int initialCapacity) { + ByteBuf byteBuf = this.byteBufAllocator.buffer(initialCapacity); + return new NettyDataBuffer(byteBuf, this); + } + + @Override + public NettyDataBuffer wrap(ByteBuffer byteBuffer) { + ByteBuf byteBuf = Unpooled.wrappedBuffer(byteBuffer); + return new NettyDataBuffer(byteBuf, this); + } + + @Override + public DataBuffer wrap(byte[] bytes) { + ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes); + return new NettyDataBuffer(byteBuf, this); + } + + /** + * Wrap the given Netty {@link ByteBuf} in a {@code NettyDataBuffer}. + * @param byteBuf the Netty byte buffer to wrap + * @return the wrapped buffer + */ + public NettyDataBuffer wrap(ByteBuf byteBuf) { + byteBuf.touch(); + return new NettyDataBuffer(byteBuf, this); + } + + /** + * {@inheritDoc} + *

    This implementation uses Netty's {@link CompositeByteBuf}. + */ + @Override + public DataBuffer join(List dataBuffers) { + Assert.notEmpty(dataBuffers, "DataBuffer List must not be empty"); + int bufferCount = dataBuffers.size(); + if (bufferCount == 1) { + return dataBuffers.get(0); + } + CompositeByteBuf composite = this.byteBufAllocator.compositeBuffer(bufferCount); + for (DataBuffer dataBuffer : dataBuffers) { + Assert.isInstanceOf(NettyDataBuffer.class, dataBuffer); + composite.addComponent(true, ((NettyDataBuffer) dataBuffer).getNativeBuffer()); + } + return new NettyDataBuffer(composite, this); + } + + /** + * Return the given Netty {@link DataBuffer} as a {@link ByteBuf}. + *

    Returns the {@linkplain NettyDataBuffer#getNativeBuffer() native buffer} + * if {@code buffer} is a {@link NettyDataBuffer}; returns + * {@link Unpooled#wrappedBuffer(ByteBuffer)} otherwise. + * @param buffer the {@code DataBuffer} to return a {@code ByteBuf} for + * @return the netty {@code ByteBuf} + */ + public static ByteBuf toByteBuf(DataBuffer buffer) { + if (buffer instanceof NettyDataBuffer) { + return ((NettyDataBuffer) buffer).getNativeBuffer(); + } + else { + return Unpooled.wrappedBuffer(buffer.asByteBuffer()); + } + } + + + @Override + public String toString() { + return "NettyDataBufferFactory (" + this.byteBufAllocator + ")"; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/PooledDataBuffer.java b/spring-core/src/main/java/org/springframework/core/io/buffer/PooledDataBuffer.java new file mode 100644 index 0000000..e3e7942 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/PooledDataBuffer.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.buffer; + +/** + * Extension of {@link DataBuffer} that allows for buffer that share + * a memory pool. Introduces methods for reference counting. + * + * @author Arjen Poutsma + * @since 5.0 + */ +public interface PooledDataBuffer extends DataBuffer { + + /** + * Return {@code true} if this buffer is allocated; + * {@code false} if it has been deallocated. + * @since 5.1 + */ + boolean isAllocated(); + + /** + * Increase the reference count for this buffer by one. + * @return this buffer + */ + PooledDataBuffer retain(); + + /** + * Associate the given hint with the data buffer for debugging purposes. + * @return this buffer + * @since 5.3.2 + */ + PooledDataBuffer touch(Object hint); + + /** + * Decrease the reference count for this buffer by one, + * and deallocate it once the count reaches zero. + * @return {@code true} if the buffer was deallocated; + * {@code false} otherwise + */ + boolean release(); + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/package-info.java b/spring-core/src/main/java/org/springframework/core/io/buffer/package-info.java new file mode 100644 index 0000000..51f02e9 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/package-info.java @@ -0,0 +1,9 @@ +/** + * Generic abstraction for working with byte buffer implementations. + */ +@NonNullApi +@NonNullFields +package org.springframework.core.io.buffer; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/main/java/org/springframework/core/io/package-info.java b/spring-core/src/main/java/org/springframework/core/io/package-info.java new file mode 100644 index 0000000..632855a --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/package-info.java @@ -0,0 +1,9 @@ +/** + * Generic abstraction for (file-based) resources, used throughout the framework. + */ +@NonNullApi +@NonNullFields +package org.springframework.core.io; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/main/java/org/springframework/core/io/support/DefaultPropertySourceFactory.java b/spring-core/src/main/java/org/springframework/core/io/support/DefaultPropertySourceFactory.java new file mode 100644 index 0000000..45ca69b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/DefaultPropertySourceFactory.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.support; + +import java.io.IOException; + +import org.springframework.core.env.PropertySource; +import org.springframework.lang.Nullable; + +/** + * The default implementation for {@link PropertySourceFactory}, + * wrapping every resource in a {@link ResourcePropertySource}. + * + * @author Juergen Hoeller + * @since 4.3 + * @see PropertySourceFactory + * @see ResourcePropertySource + */ +public class DefaultPropertySourceFactory implements PropertySourceFactory { + + @Override + public PropertySource createPropertySource(@Nullable String name, EncodedResource resource) throws IOException { + return (name != null ? new ResourcePropertySource(name, resource) : new ResourcePropertySource(resource)); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/support/EncodedResource.java b/spring-core/src/main/java/org/springframework/core/io/support/EncodedResource.java new file mode 100644 index 0000000..f718fe7 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/EncodedResource.java @@ -0,0 +1,187 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.support; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.Charset; + +import org.springframework.core.io.InputStreamSource; +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Holder that combines a {@link Resource} descriptor with a specific encoding + * or {@code Charset} to be used for reading from the resource. + * + *

    Used as an argument for operations that support reading content with + * a specific encoding, typically via a {@code java.io.Reader}. + * + * @author Juergen Hoeller + * @author Sam Brannen + * @since 1.2.6 + * @see Resource#getInputStream() + * @see java.io.Reader + * @see java.nio.charset.Charset + */ +public class EncodedResource implements InputStreamSource { + + private final Resource resource; + + @Nullable + private final String encoding; + + @Nullable + private final Charset charset; + + + /** + * Create a new {@code EncodedResource} for the given {@code Resource}, + * not specifying an explicit encoding or {@code Charset}. + * @param resource the {@code Resource} to hold (never {@code null}) + */ + public EncodedResource(Resource resource) { + this(resource, null, null); + } + + /** + * Create a new {@code EncodedResource} for the given {@code Resource}, + * using the specified {@code encoding}. + * @param resource the {@code Resource} to hold (never {@code null}) + * @param encoding the encoding to use for reading from the resource + */ + public EncodedResource(Resource resource, @Nullable String encoding) { + this(resource, encoding, null); + } + + /** + * Create a new {@code EncodedResource} for the given {@code Resource}, + * using the specified {@code Charset}. + * @param resource the {@code Resource} to hold (never {@code null}) + * @param charset the {@code Charset} to use for reading from the resource + */ + public EncodedResource(Resource resource, @Nullable Charset charset) { + this(resource, null, charset); + } + + private EncodedResource(Resource resource, @Nullable String encoding, @Nullable Charset charset) { + super(); + Assert.notNull(resource, "Resource must not be null"); + this.resource = resource; + this.encoding = encoding; + this.charset = charset; + } + + + /** + * Return the {@code Resource} held by this {@code EncodedResource}. + */ + public final Resource getResource() { + return this.resource; + } + + /** + * Return the encoding to use for reading from the {@linkplain #getResource() resource}, + * or {@code null} if none specified. + */ + @Nullable + public final String getEncoding() { + return this.encoding; + } + + /** + * Return the {@code Charset} to use for reading from the {@linkplain #getResource() resource}, + * or {@code null} if none specified. + */ + @Nullable + public final Charset getCharset() { + return this.charset; + } + + /** + * Determine whether a {@link Reader} is required as opposed to an {@link InputStream}, + * i.e. whether an {@linkplain #getEncoding() encoding} or a {@link #getCharset() Charset} + * has been specified. + * @see #getReader() + * @see #getInputStream() + */ + public boolean requiresReader() { + return (this.encoding != null || this.charset != null); + } + + /** + * Open a {@code java.io.Reader} for the specified resource, using the specified + * {@link #getCharset() Charset} or {@linkplain #getEncoding() encoding} + * (if any). + * @throws IOException if opening the Reader failed + * @see #requiresReader() + * @see #getInputStream() + */ + public Reader getReader() throws IOException { + if (this.charset != null) { + return new InputStreamReader(this.resource.getInputStream(), this.charset); + } + else if (this.encoding != null) { + return new InputStreamReader(this.resource.getInputStream(), this.encoding); + } + else { + return new InputStreamReader(this.resource.getInputStream()); + } + } + + /** + * Open an {@code InputStream} for the specified resource, ignoring any specified + * {@link #getCharset() Charset} or {@linkplain #getEncoding() encoding}. + * @throws IOException if opening the InputStream failed + * @see #requiresReader() + * @see #getReader() + */ + @Override + public InputStream getInputStream() throws IOException { + return this.resource.getInputStream(); + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof EncodedResource)) { + return false; + } + EncodedResource otherResource = (EncodedResource) other; + return (this.resource.equals(otherResource.resource) && + ObjectUtils.nullSafeEquals(this.charset, otherResource.charset) && + ObjectUtils.nullSafeEquals(this.encoding, otherResource.encoding)); + } + + @Override + public int hashCode() { + return this.resource.hashCode(); + } + + @Override + public String toString() { + return this.resource.toString(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/support/LocalizedResourceHelper.java b/spring-core/src/main/java/org/springframework/core/io/support/LocalizedResourceHelper.java new file mode 100644 index 0000000..58021b0 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/LocalizedResourceHelper.java @@ -0,0 +1,130 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.support; + +import java.util.Locale; + +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Helper class for loading a localized resource, + * specified through name, extension and current locale. + * + * @author Juergen Hoeller + * @since 1.2.5 + */ +public class LocalizedResourceHelper { + + /** The default separator to use in-between file name parts: an underscore. */ + public static final String DEFAULT_SEPARATOR = "_"; + + + private final ResourceLoader resourceLoader; + + private String separator = DEFAULT_SEPARATOR; + + + /** + * Create a new LocalizedResourceHelper with a DefaultResourceLoader. + * @see org.springframework.core.io.DefaultResourceLoader + */ + public LocalizedResourceHelper() { + this.resourceLoader = new DefaultResourceLoader(); + } + + /** + * Create a new LocalizedResourceHelper with the given ResourceLoader. + * @param resourceLoader the ResourceLoader to use + */ + public LocalizedResourceHelper(ResourceLoader resourceLoader) { + Assert.notNull(resourceLoader, "ResourceLoader must not be null"); + this.resourceLoader = resourceLoader; + } + + /** + * Set the separator to use in-between file name parts. + * Default is an underscore ("_"). + */ + public void setSeparator(@Nullable String separator) { + this.separator = (separator != null ? separator : DEFAULT_SEPARATOR); + } + + + /** + * Find the most specific localized resource for the given name, + * extension and locale: + *

    The file will be searched with locations in the following order, + * similar to {@code java.util.ResourceBundle}'s search order: + *

      + *
    • [name]_[language]_[country]_[variant][extension] + *
    • [name]_[language]_[country][extension] + *
    • [name]_[language][extension] + *
    • [name][extension] + *
    + *

    If none of the specific files can be found, a resource + * descriptor for the default location will be returned. + * @param name the name of the file, without localization part nor extension + * @param extension the file extension (e.g. ".xls") + * @param locale the current locale (may be {@code null}) + * @return the most specific localized resource found + * @see java.util.ResourceBundle + */ + public Resource findLocalizedResource(String name, String extension, @Nullable Locale locale) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(extension, "Extension must not be null"); + + Resource resource = null; + + if (locale != null) { + String lang = locale.getLanguage(); + String country = locale.getCountry(); + String variant = locale.getVariant(); + + // Check for file with language, country and variant localization. + if (variant.length() > 0) { + String location = + name + this.separator + lang + this.separator + country + this.separator + variant + extension; + resource = this.resourceLoader.getResource(location); + } + + // Check for file with language and country localization. + if ((resource == null || !resource.exists()) && country.length() > 0) { + String location = name + this.separator + lang + this.separator + country + extension; + resource = this.resourceLoader.getResource(location); + } + + // Check for document with language localization. + if ((resource == null || !resource.exists()) && lang.length() > 0) { + String location = name + this.separator + lang + extension; + resource = this.resourceLoader.getResource(location); + } + } + + // Check for document without localization. + if (resource == null || !resource.exists()) { + String location = name + extension; + resource = this.resourceLoader.getResource(location); + } + + return resource; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java new file mode 100644 index 0000000..69cd661 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java @@ -0,0 +1,927 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.support; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.net.JarURLConnection; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; +import java.net.URLConnection; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.zip.ZipException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.UrlResource; +import org.springframework.core.io.VfsResource; +import org.springframework.lang.Nullable; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.PathMatcher; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.ResourceUtils; +import org.springframework.util.StringUtils; + +/** + * A {@link ResourcePatternResolver} implementation that is able to resolve a + * specified resource location path into one or more matching Resources. + * The source path may be a simple path which has a one-to-one mapping to a + * target {@link org.springframework.core.io.Resource}, or alternatively + * may contain the special "{@code classpath*:}" prefix and/or + * internal Ant-style regular expressions (matched using Spring's + * {@link org.springframework.util.AntPathMatcher} utility). + * Both of the latter are effectively wildcards. + * + *

    No Wildcards: + * + *

    In the simple case, if the specified location path does not start with the + * {@code "classpath*:}" prefix, and does not contain a PathMatcher pattern, + * this resolver will simply return a single resource via a + * {@code getResource()} call on the underlying {@code ResourceLoader}. + * Examples are real URLs such as "{@code file:C:/context.xml}", pseudo-URLs + * such as "{@code classpath:/context.xml}", and simple unprefixed paths + * such as "{@code /WEB-INF/context.xml}". The latter will resolve in a + * fashion specific to the underlying {@code ResourceLoader} (e.g. + * {@code ServletContextResource} for a {@code WebApplicationContext}). + * + *

    Ant-style Patterns: + * + *

    When the path location contains an Ant-style pattern, e.g.: + *

    + * /WEB-INF/*-context.xml
    + * com/mycompany/**/applicationContext.xml
    + * file:C:/some/path/*-context.xml
    + * classpath:com/mycompany/**/applicationContext.xml
    + * the resolver follows a more complex but defined procedure to try to resolve + * the wildcard. It produces a {@code Resource} for the path up to the last + * non-wildcard segment and obtains a {@code URL} from it. If this URL is + * not a "{@code jar:}" URL or container-specific variant (e.g. + * "{@code zip:}" in WebLogic, "{@code wsjar}" in WebSphere", etc.), + * then a {@code java.io.File} is obtained from it, and used to resolve the + * wildcard by walking the filesystem. In the case of a jar URL, the resolver + * either gets a {@code java.net.JarURLConnection} from it, or manually parses + * the jar URL, and then traverses the contents of the jar file, to resolve the + * wildcards. + * + *

    Implications on portability: + * + *

    If the specified path is already a file URL (either explicitly, or + * implicitly because the base {@code ResourceLoader} is a filesystem one, + * then wildcarding is guaranteed to work in a completely portable fashion. + * + *

    If the specified path is a classpath location, then the resolver must + * obtain the last non-wildcard path segment URL via a + * {@code Classloader.getResource()} call. Since this is just a + * node of the path (not the file at the end) it is actually undefined + * (in the ClassLoader Javadocs) exactly what sort of a URL is returned in + * this case. In practice, it is usually a {@code java.io.File} representing + * the directory, where the classpath resource resolves to a filesystem + * location, or a jar URL of some sort, where the classpath resource resolves + * to a jar location. Still, there is a portability concern on this operation. + * + *

    If a jar URL is obtained for the last non-wildcard segment, the resolver + * must be able to get a {@code java.net.JarURLConnection} from it, or + * manually parse the jar URL, to be able to walk the contents of the jar, + * and resolve the wildcard. This will work in most environments, but will + * fail in others, and it is strongly recommended that the wildcard + * resolution of resources coming from jars be thoroughly tested in your + * specific environment before you rely on it. + * + *

    {@code classpath*:} Prefix: + * + *

    There is special support for retrieving multiple class path resources with + * the same name, via the "{@code classpath*:}" prefix. For example, + * "{@code classpath*:META-INF/beans.xml}" will find all "beans.xml" + * files in the class path, be it in "classes" directories or in JAR files. + * This is particularly useful for autodetecting config files of the same name + * at the same location within each jar file. Internally, this happens via a + * {@code ClassLoader.getResources()} call, and is completely portable. + * + *

    The "classpath*:" prefix can also be combined with a PathMatcher pattern in + * the rest of the location path, for example "classpath*:META-INF/*-beans.xml". + * In this case, the resolution strategy is fairly simple: a + * {@code ClassLoader.getResources()} call is used on the last non-wildcard + * path segment to get all the matching resources in the class loader hierarchy, + * and then off each resource the same PathMatcher resolution strategy described + * above is used for the wildcard subpath. + * + *

    Other notes: + * + *

    WARNING: Note that "{@code classpath*:}" when combined with + * Ant-style patterns will only work reliably with at least one root directory + * before the pattern starts, unless the actual target files reside in the file + * system. This means that a pattern like "{@code classpath*:*.xml}" will + * not retrieve files from the root of jar files but rather only from the + * root of expanded directories. This originates from a limitation in the JDK's + * {@code ClassLoader.getResources()} method which only returns file system + * locations for a passed-in empty String (indicating potential roots to search). + * This {@code ResourcePatternResolver} implementation is trying to mitigate the + * jar root lookup limitation through {@link URLClassLoader} introspection and + * "java.class.path" manifest evaluation; however, without portability guarantees. + * + *

    WARNING: Ant-style patterns with "classpath:" resources are not + * guaranteed to find matching resources if the root package to search is available + * in multiple class path locations. This is because a resource such as + *

    + *     com/mycompany/package1/service-context.xml
    + * 
    + * may be in only one location, but when a path such as + *
    + *     classpath:com/mycompany/**/service-context.xml
    + * 
    + * is used to try to resolve it, the resolver will work off the (first) URL + * returned by {@code getResource("com/mycompany");}. If this base package node + * exists in multiple classloader locations, the actual end resource may not be + * underneath. Therefore, preferably, use "{@code classpath*:}" with the same + * Ant-style pattern in such a case, which will search all class path + * locations that contain the root package. + * + * @author Juergen Hoeller + * @author Colin Sampaleanu + * @author Marius Bogoevici + * @author Costin Leau + * @author Phillip Webb + * @since 1.0.2 + * @see #CLASSPATH_ALL_URL_PREFIX + * @see org.springframework.util.AntPathMatcher + * @see org.springframework.core.io.ResourceLoader#getResource(String) + * @see ClassLoader#getResources(String) + */ +public class PathMatchingResourcePatternResolver implements ResourcePatternResolver { + + private static final Log logger = LogFactory.getLog(PathMatchingResourcePatternResolver.class); + + @Nullable + private static Method equinoxResolveMethod; + + static { + try { + // Detect Equinox OSGi (e.g. on WebSphere 6.1) + Class fileLocatorClass = ClassUtils.forName("org.eclipse.core.runtime.FileLocator", + PathMatchingResourcePatternResolver.class.getClassLoader()); + equinoxResolveMethod = fileLocatorClass.getMethod("resolve", URL.class); + logger.trace("Found Equinox FileLocator for OSGi bundle URL resolution"); + } + catch (Throwable ex) { + equinoxResolveMethod = null; + } + } + + + private final ResourceLoader resourceLoader; + + private PathMatcher pathMatcher = new AntPathMatcher(); + + + /** + * Create a new PathMatchingResourcePatternResolver with a DefaultResourceLoader. + *

    ClassLoader access will happen via the thread context class loader. + * @see org.springframework.core.io.DefaultResourceLoader + */ + public PathMatchingResourcePatternResolver() { + this.resourceLoader = new DefaultResourceLoader(); + } + + /** + * Create a new PathMatchingResourcePatternResolver. + *

    ClassLoader access will happen via the thread context class loader. + * @param resourceLoader the ResourceLoader to load root directories and + * actual resources with + */ + public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) { + Assert.notNull(resourceLoader, "ResourceLoader must not be null"); + this.resourceLoader = resourceLoader; + } + + /** + * Create a new PathMatchingResourcePatternResolver with a DefaultResourceLoader. + * @param classLoader the ClassLoader to load classpath resources with, + * or {@code null} for using the thread context class loader + * at the time of actual resource access + * @see org.springframework.core.io.DefaultResourceLoader + */ + public PathMatchingResourcePatternResolver(@Nullable ClassLoader classLoader) { + this.resourceLoader = new DefaultResourceLoader(classLoader); + } + + + /** + * Return the ResourceLoader that this pattern resolver works with. + */ + public ResourceLoader getResourceLoader() { + return this.resourceLoader; + } + + @Override + @Nullable + public ClassLoader getClassLoader() { + return getResourceLoader().getClassLoader(); + } + + /** + * Set the PathMatcher implementation to use for this + * resource pattern resolver. Default is AntPathMatcher. + * @see org.springframework.util.AntPathMatcher + */ + public void setPathMatcher(PathMatcher pathMatcher) { + Assert.notNull(pathMatcher, "PathMatcher must not be null"); + this.pathMatcher = pathMatcher; + } + + /** + * Return the PathMatcher that this resource pattern resolver uses. + */ + public PathMatcher getPathMatcher() { + return this.pathMatcher; + } + + + @Override + public Resource getResource(String location) { + return getResourceLoader().getResource(location); + } + + @Override + public Resource[] getResources(String locationPattern) throws IOException { + Assert.notNull(locationPattern, "Location pattern must not be null"); + if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) { + // a class path resource (multiple resources for same name possible) + if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) { + // a class path resource pattern + return findPathMatchingResources(locationPattern); + } + else { + // all class path resources with the given name + return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length())); + } + } + else { + // Generally only look for a pattern after a prefix here, + // and on Tomcat only after the "*/" separator for its "war:" protocol. + int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 : + locationPattern.indexOf(':') + 1); + if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) { + // a file pattern + return findPathMatchingResources(locationPattern); + } + else { + // a single resource with the given name + return new Resource[] {getResourceLoader().getResource(locationPattern)}; + } + } + } + + /** + * Find all class location resources with the given location via the ClassLoader. + * Delegates to {@link #doFindAllClassPathResources(String)}. + * @param location the absolute path within the classpath + * @return the result as Resource array + * @throws IOException in case of I/O errors + * @see java.lang.ClassLoader#getResources + * @see #convertClassLoaderURL + */ + protected Resource[] findAllClassPathResources(String location) throws IOException { + String path = location; + if (path.startsWith("/")) { + path = path.substring(1); + } + Set result = doFindAllClassPathResources(path); + if (logger.isTraceEnabled()) { + logger.trace("Resolved classpath location [" + location + "] to resources " + result); + } + return result.toArray(new Resource[0]); + } + + /** + * Find all class location resources with the given path via the ClassLoader. + * Called by {@link #findAllClassPathResources(String)}. + * @param path the absolute path within the classpath (never a leading slash) + * @return a mutable Set of matching Resource instances + * @since 4.1.1 + */ + protected Set doFindAllClassPathResources(String path) throws IOException { + Set result = new LinkedHashSet<>(16); + ClassLoader cl = getClassLoader(); + Enumeration resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path)); + while (resourceUrls.hasMoreElements()) { + URL url = resourceUrls.nextElement(); + result.add(convertClassLoaderURL(url)); + } + if (!StringUtils.hasLength(path)) { + // The above result is likely to be incomplete, i.e. only containing file system references. + // We need to have pointers to each of the jar files on the classpath as well... + addAllClassLoaderJarRoots(cl, result); + } + return result; + } + + /** + * Convert the given URL as returned from the ClassLoader into a {@link Resource}. + *

    The default implementation simply creates a {@link UrlResource} instance. + * @param url a URL as returned from the ClassLoader + * @return the corresponding Resource object + * @see java.lang.ClassLoader#getResources + * @see org.springframework.core.io.Resource + */ + protected Resource convertClassLoaderURL(URL url) { + return new UrlResource(url); + } + + /** + * Search all {@link URLClassLoader} URLs for jar file references and add them to the + * given set of resources in the form of pointers to the root of the jar file content. + * @param classLoader the ClassLoader to search (including its ancestors) + * @param result the set of resources to add jar roots to + * @since 4.1.1 + */ + protected void addAllClassLoaderJarRoots(@Nullable ClassLoader classLoader, Set result) { + if (classLoader instanceof URLClassLoader) { + try { + for (URL url : ((URLClassLoader) classLoader).getURLs()) { + try { + UrlResource jarResource = (ResourceUtils.URL_PROTOCOL_JAR.equals(url.getProtocol()) ? + new UrlResource(url) : + new UrlResource(ResourceUtils.JAR_URL_PREFIX + url + ResourceUtils.JAR_URL_SEPARATOR)); + if (jarResource.exists()) { + result.add(jarResource); + } + } + catch (MalformedURLException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Cannot search for matching files underneath [" + url + + "] because it cannot be converted to a valid 'jar:' URL: " + ex.getMessage()); + } + } + } + } + catch (Exception ex) { + if (logger.isDebugEnabled()) { + logger.debug("Cannot introspect jar files since ClassLoader [" + classLoader + + "] does not support 'getURLs()': " + ex); + } + } + } + + if (classLoader == ClassLoader.getSystemClassLoader()) { + // "java.class.path" manifest evaluation... + addClassPathManifestEntries(result); + } + + if (classLoader != null) { + try { + // Hierarchy traversal... + addAllClassLoaderJarRoots(classLoader.getParent(), result); + } + catch (Exception ex) { + if (logger.isDebugEnabled()) { + logger.debug("Cannot introspect jar files in parent ClassLoader since [" + classLoader + + "] does not support 'getParent()': " + ex); + } + } + } + } + + /** + * Determine jar file references from the "java.class.path." manifest property and add them + * to the given set of resources in the form of pointers to the root of the jar file content. + * @param result the set of resources to add jar roots to + * @since 4.3 + */ + protected void addClassPathManifestEntries(Set result) { + try { + String javaClassPathProperty = System.getProperty("java.class.path"); + for (String path : StringUtils.delimitedListToStringArray( + javaClassPathProperty, System.getProperty("path.separator"))) { + try { + String filePath = new File(path).getAbsolutePath(); + int prefixIndex = filePath.indexOf(':'); + if (prefixIndex == 1) { + // Possibly "c:" drive prefix on Windows, to be upper-cased for proper duplicate detection + filePath = StringUtils.capitalize(filePath); + } + // # can appear in directories/filenames, java.net.URL should not treat it as a fragment + filePath = StringUtils.replace(filePath, "#", "%23"); + // Build URL that points to the root of the jar file + UrlResource jarResource = new UrlResource(ResourceUtils.JAR_URL_PREFIX + + ResourceUtils.FILE_URL_PREFIX + filePath + ResourceUtils.JAR_URL_SEPARATOR); + // Potentially overlapping with URLClassLoader.getURLs() result above! + if (!result.contains(jarResource) && !hasDuplicate(filePath, result) && jarResource.exists()) { + result.add(jarResource); + } + } + catch (MalformedURLException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Cannot search for matching files underneath [" + path + + "] because it cannot be converted to a valid 'jar:' URL: " + ex.getMessage()); + } + } + } + } + catch (Exception ex) { + if (logger.isDebugEnabled()) { + logger.debug("Failed to evaluate 'java.class.path' manifest entries: " + ex); + } + } + } + + /** + * Check whether the given file path has a duplicate but differently structured entry + * in the existing result, i.e. with or without a leading slash. + * @param filePath the file path (with or without a leading slash) + * @param result the current result + * @return {@code true} if there is a duplicate (i.e. to ignore the given file path), + * {@code false} to proceed with adding a corresponding resource to the current result + */ + private boolean hasDuplicate(String filePath, Set result) { + if (result.isEmpty()) { + return false; + } + String duplicatePath = (filePath.startsWith("/") ? filePath.substring(1) : "/" + filePath); + try { + return result.contains(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + + duplicatePath + ResourceUtils.JAR_URL_SEPARATOR)); + } + catch (MalformedURLException ex) { + // Ignore: just for testing against duplicate. + return false; + } + } + + /** + * Find all resources that match the given location pattern via the + * Ant-style PathMatcher. Supports resources in jar files and zip files + * and in the file system. + * @param locationPattern the location pattern to match + * @return the result as Resource array + * @throws IOException in case of I/O errors + * @see #doFindPathMatchingJarResources + * @see #doFindPathMatchingFileResources + * @see org.springframework.util.PathMatcher + */ + protected Resource[] findPathMatchingResources(String locationPattern) throws IOException { + String rootDirPath = determineRootDir(locationPattern); + String subPattern = locationPattern.substring(rootDirPath.length()); + Resource[] rootDirResources = getResources(rootDirPath); + Set result = new LinkedHashSet<>(16); + for (Resource rootDirResource : rootDirResources) { + rootDirResource = resolveRootDirResource(rootDirResource); + URL rootDirUrl = rootDirResource.getURL(); + if (equinoxResolveMethod != null && rootDirUrl.getProtocol().startsWith("bundle")) { + URL resolvedUrl = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirUrl); + if (resolvedUrl != null) { + rootDirUrl = resolvedUrl; + } + rootDirResource = new UrlResource(rootDirUrl); + } + if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) { + result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher())); + } + else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) { + result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern)); + } + else { + result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern)); + } + } + if (logger.isTraceEnabled()) { + logger.trace("Resolved location pattern [" + locationPattern + "] to resources " + result); + } + return result.toArray(new Resource[0]); + } + + /** + * Determine the root directory for the given location. + *

    Used for determining the starting point for file matching, + * resolving the root directory location to a {@code java.io.File} + * and passing it into {@code retrieveMatchingFiles}, with the + * remainder of the location as pattern. + *

    Will return "/WEB-INF/" for the pattern "/WEB-INF/*.xml", + * for example. + * @param location the location to check + * @return the part of the location that denotes the root directory + * @see #retrieveMatchingFiles + */ + protected String determineRootDir(String location) { + int prefixEnd = location.indexOf(':') + 1; + int rootDirEnd = location.length(); + while (rootDirEnd > prefixEnd && getPathMatcher().isPattern(location.substring(prefixEnd, rootDirEnd))) { + rootDirEnd = location.lastIndexOf('/', rootDirEnd - 2) + 1; + } + if (rootDirEnd == 0) { + rootDirEnd = prefixEnd; + } + return location.substring(0, rootDirEnd); + } + + /** + * Resolve the specified resource for path matching. + *

    By default, Equinox OSGi "bundleresource:" / "bundleentry:" URL will be + * resolved into a standard jar file URL that be traversed using Spring's + * standard jar file traversal algorithm. For any preceding custom resolution, + * override this method and replace the resource handle accordingly. + * @param original the resource to resolve + * @return the resolved resource (may be identical to the passed-in resource) + * @throws IOException in case of resolution failure + */ + protected Resource resolveRootDirResource(Resource original) throws IOException { + return original; + } + + /** + * Return whether the given resource handle indicates a jar resource + * that the {@code doFindPathMatchingJarResources} method can handle. + *

    By default, the URL protocols "jar", "zip", "vfszip and "wsjar" + * will be treated as jar resources. This template method allows for + * detecting further kinds of jar-like resources, e.g. through + * {@code instanceof} checks on the resource handle type. + * @param resource the resource handle to check + * (usually the root directory to start path matching from) + * @see #doFindPathMatchingJarResources + * @see org.springframework.util.ResourceUtils#isJarURL + */ + protected boolean isJarResource(Resource resource) throws IOException { + return false; + } + + /** + * Find all resources in jar files that match the given location pattern + * via the Ant-style PathMatcher. + * @param rootDirResource the root directory as Resource + * @param rootDirURL the pre-resolved root directory URL + * @param subPattern the sub pattern to match (below the root directory) + * @return a mutable Set of matching Resource instances + * @throws IOException in case of I/O errors + * @since 4.3 + * @see java.net.JarURLConnection + * @see org.springframework.util.PathMatcher + */ + protected Set doFindPathMatchingJarResources(Resource rootDirResource, URL rootDirURL, String subPattern) + throws IOException { + + URLConnection con = rootDirURL.openConnection(); + JarFile jarFile; + String jarFileUrl; + String rootEntryPath; + boolean closeJarFile; + + if (con instanceof JarURLConnection) { + // Should usually be the case for traditional JAR files. + JarURLConnection jarCon = (JarURLConnection) con; + ResourceUtils.useCachesIfNecessary(jarCon); + jarFile = jarCon.getJarFile(); + jarFileUrl = jarCon.getJarFileURL().toExternalForm(); + JarEntry jarEntry = jarCon.getJarEntry(); + rootEntryPath = (jarEntry != null ? jarEntry.getName() : ""); + closeJarFile = !jarCon.getUseCaches(); + } + else { + // No JarURLConnection -> need to resort to URL file parsing. + // We'll assume URLs of the format "jar:path!/entry", with the protocol + // being arbitrary as long as following the entry format. + // We'll also handle paths with and without leading "file:" prefix. + String urlFile = rootDirURL.getFile(); + try { + int separatorIndex = urlFile.indexOf(ResourceUtils.WAR_URL_SEPARATOR); + if (separatorIndex == -1) { + separatorIndex = urlFile.indexOf(ResourceUtils.JAR_URL_SEPARATOR); + } + if (separatorIndex != -1) { + jarFileUrl = urlFile.substring(0, separatorIndex); + rootEntryPath = urlFile.substring(separatorIndex + 2); // both separators are 2 chars + jarFile = getJarFile(jarFileUrl); + } + else { + jarFile = new JarFile(urlFile); + jarFileUrl = urlFile; + rootEntryPath = ""; + } + closeJarFile = true; + } + catch (ZipException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Skipping invalid jar classpath entry [" + urlFile + "]"); + } + return Collections.emptySet(); + } + } + + try { + if (logger.isTraceEnabled()) { + logger.trace("Looking for matching resources in jar file [" + jarFileUrl + "]"); + } + if (StringUtils.hasLength(rootEntryPath) && !rootEntryPath.endsWith("/")) { + // Root entry path must end with slash to allow for proper matching. + // The Sun JRE does not return a slash here, but BEA JRockit does. + rootEntryPath = rootEntryPath + "/"; + } + Set result = new LinkedHashSet<>(8); + for (Enumeration entries = jarFile.entries(); entries.hasMoreElements();) { + JarEntry entry = entries.nextElement(); + String entryPath = entry.getName(); + if (entryPath.startsWith(rootEntryPath)) { + String relativePath = entryPath.substring(rootEntryPath.length()); + if (getPathMatcher().match(subPattern, relativePath)) { + result.add(rootDirResource.createRelative(relativePath)); + } + } + } + return result; + } + finally { + if (closeJarFile) { + jarFile.close(); + } + } + } + + /** + * Resolve the given jar file URL into a JarFile object. + */ + protected JarFile getJarFile(String jarFileUrl) throws IOException { + if (jarFileUrl.startsWith(ResourceUtils.FILE_URL_PREFIX)) { + try { + return new JarFile(ResourceUtils.toURI(jarFileUrl).getSchemeSpecificPart()); + } + catch (URISyntaxException ex) { + // Fallback for URLs that are not valid URIs (should hardly ever happen). + return new JarFile(jarFileUrl.substring(ResourceUtils.FILE_URL_PREFIX.length())); + } + } + else { + return new JarFile(jarFileUrl); + } + } + + /** + * Find all resources in the file system that match the given location pattern + * via the Ant-style PathMatcher. + * @param rootDirResource the root directory as Resource + * @param subPattern the sub pattern to match (below the root directory) + * @return a mutable Set of matching Resource instances + * @throws IOException in case of I/O errors + * @see #retrieveMatchingFiles + * @see org.springframework.util.PathMatcher + */ + protected Set doFindPathMatchingFileResources(Resource rootDirResource, String subPattern) + throws IOException { + + File rootDir; + try { + rootDir = rootDirResource.getFile().getAbsoluteFile(); + } + catch (FileNotFoundException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Cannot search for matching files underneath " + rootDirResource + + " in the file system: " + ex.getMessage()); + } + return Collections.emptySet(); + } + catch (Exception ex) { + if (logger.isInfoEnabled()) { + logger.info("Failed to resolve " + rootDirResource + " in the file system: " + ex); + } + return Collections.emptySet(); + } + return doFindMatchingFileSystemResources(rootDir, subPattern); + } + + /** + * Find all resources in the file system that match the given location pattern + * via the Ant-style PathMatcher. + * @param rootDir the root directory in the file system + * @param subPattern the sub pattern to match (below the root directory) + * @return a mutable Set of matching Resource instances + * @throws IOException in case of I/O errors + * @see #retrieveMatchingFiles + * @see org.springframework.util.PathMatcher + */ + protected Set doFindMatchingFileSystemResources(File rootDir, String subPattern) throws IOException { + if (logger.isTraceEnabled()) { + logger.trace("Looking for matching resources in directory tree [" + rootDir.getPath() + "]"); + } + Set matchingFiles = retrieveMatchingFiles(rootDir, subPattern); + Set result = new LinkedHashSet<>(matchingFiles.size()); + for (File file : matchingFiles) { + result.add(new FileSystemResource(file)); + } + return result; + } + + /** + * Retrieve files that match the given path pattern, + * checking the given directory and its subdirectories. + * @param rootDir the directory to start from + * @param pattern the pattern to match against, + * relative to the root directory + * @return a mutable Set of matching Resource instances + * @throws IOException if directory contents could not be retrieved + */ + protected Set retrieveMatchingFiles(File rootDir, String pattern) throws IOException { + if (!rootDir.exists()) { + // Silently skip non-existing directories. + if (logger.isDebugEnabled()) { + logger.debug("Skipping [" + rootDir.getAbsolutePath() + "] because it does not exist"); + } + return Collections.emptySet(); + } + if (!rootDir.isDirectory()) { + // Complain louder if it exists but is no directory. + if (logger.isInfoEnabled()) { + logger.info("Skipping [" + rootDir.getAbsolutePath() + "] because it does not denote a directory"); + } + return Collections.emptySet(); + } + if (!rootDir.canRead()) { + if (logger.isInfoEnabled()) { + logger.info("Skipping search for matching files underneath directory [" + rootDir.getAbsolutePath() + + "] because the application is not allowed to read the directory"); + } + return Collections.emptySet(); + } + String fullPattern = StringUtils.replace(rootDir.getAbsolutePath(), File.separator, "/"); + if (!pattern.startsWith("/")) { + fullPattern += "/"; + } + fullPattern = fullPattern + StringUtils.replace(pattern, File.separator, "/"); + Set result = new LinkedHashSet<>(8); + doRetrieveMatchingFiles(fullPattern, rootDir, result); + return result; + } + + /** + * Recursively retrieve files that match the given pattern, + * adding them to the given result list. + * @param fullPattern the pattern to match against, + * with prepended root directory path + * @param dir the current directory + * @param result the Set of matching File instances to add to + * @throws IOException if directory contents could not be retrieved + */ + protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set result) throws IOException { + if (logger.isTraceEnabled()) { + logger.trace("Searching directory [" + dir.getAbsolutePath() + + "] for files matching pattern [" + fullPattern + "]"); + } + for (File content : listDirectory(dir)) { + String currPath = StringUtils.replace(content.getAbsolutePath(), File.separator, "/"); + if (content.isDirectory() && getPathMatcher().matchStart(fullPattern, currPath + "/")) { + if (!content.canRead()) { + if (logger.isDebugEnabled()) { + logger.debug("Skipping subdirectory [" + dir.getAbsolutePath() + + "] because the application is not allowed to read the directory"); + } + } + else { + doRetrieveMatchingFiles(fullPattern, content, result); + } + } + if (getPathMatcher().match(fullPattern, currPath)) { + result.add(content); + } + } + } + + /** + * Determine a sorted list of files in the given directory. + * @param dir the directory to introspect + * @return the sorted list of files (by default in alphabetical order) + * @since 5.1 + * @see File#listFiles() + */ + protected File[] listDirectory(File dir) { + File[] files = dir.listFiles(); + if (files == null) { + if (logger.isInfoEnabled()) { + logger.info("Could not retrieve contents of directory [" + dir.getAbsolutePath() + "]"); + } + return new File[0]; + } + Arrays.sort(files, Comparator.comparing(File::getName)); + return files; + } + + + /** + * Inner delegate class, avoiding a hard JBoss VFS API dependency at runtime. + */ + private static class VfsResourceMatchingDelegate { + + public static Set findMatchingResources( + URL rootDirURL, String locationPattern, PathMatcher pathMatcher) throws IOException { + + Object root = VfsPatternUtils.findRoot(rootDirURL); + PatternVirtualFileVisitor visitor = + new PatternVirtualFileVisitor(VfsPatternUtils.getPath(root), locationPattern, pathMatcher); + VfsPatternUtils.visit(root, visitor); + return visitor.getResources(); + } + } + + + /** + * VFS visitor for path matching purposes. + */ + @SuppressWarnings("unused") + private static class PatternVirtualFileVisitor implements InvocationHandler { + + private final String subPattern; + + private final PathMatcher pathMatcher; + + private final String rootPath; + + private final Set resources = new LinkedHashSet<>(); + + public PatternVirtualFileVisitor(String rootPath, String subPattern, PathMatcher pathMatcher) { + this.subPattern = subPattern; + this.pathMatcher = pathMatcher; + this.rootPath = (rootPath.isEmpty() || rootPath.endsWith("/") ? rootPath : rootPath + "/"); + } + + @Override + @Nullable + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + String methodName = method.getName(); + if (Object.class == method.getDeclaringClass()) { + if (methodName.equals("equals")) { + // Only consider equal when proxies are identical. + return (proxy == args[0]); + } + else if (methodName.equals("hashCode")) { + return System.identityHashCode(proxy); + } + } + else if ("getAttributes".equals(methodName)) { + return getAttributes(); + } + else if ("visit".equals(methodName)) { + visit(args[0]); + return null; + } + else if ("toString".equals(methodName)) { + return toString(); + } + + throw new IllegalStateException("Unexpected method invocation: " + method); + } + + public void visit(Object vfsResource) { + if (this.pathMatcher.match(this.subPattern, + VfsPatternUtils.getPath(vfsResource).substring(this.rootPath.length()))) { + this.resources.add(new VfsResource(vfsResource)); + } + } + + @Nullable + public Object getAttributes() { + return VfsPatternUtils.getVisitorAttributes(); + } + + public Set getResources() { + return this.resources; + } + + public int size() { + return this.resources.size(); + } + + @Override + public String toString() { + return "sub-pattern: " + this.subPattern + ", resources: " + this.resources; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PropertiesLoaderSupport.java b/spring-core/src/main/java/org/springframework/core/io/support/PropertiesLoaderSupport.java new file mode 100644 index 0000000..0f33f9b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/PropertiesLoaderSupport.java @@ -0,0 +1,198 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.support; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.Properties; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; +import org.springframework.util.PropertiesPersister; + +/** + * Base class for JavaBean-style components that need to load properties + * from one or more resources. Supports local properties as well, with + * configurable overriding. + * + * @author Juergen Hoeller + * @since 1.2.2 + */ +public abstract class PropertiesLoaderSupport { + + /** Logger available to subclasses. */ + protected final Log logger = LogFactory.getLog(getClass()); + + @Nullable + protected Properties[] localProperties; + + protected boolean localOverride = false; + + @Nullable + private Resource[] locations; + + private boolean ignoreResourceNotFound = false; + + @Nullable + private String fileEncoding; + + private PropertiesPersister propertiesPersister = ResourcePropertiesPersister.INSTANCE; + + + /** + * Set local properties, e.g. via the "props" tag in XML bean definitions. + * These can be considered defaults, to be overridden by properties + * loaded from files. + */ + public void setProperties(Properties properties) { + this.localProperties = new Properties[] {properties}; + } + + /** + * Set local properties, e.g. via the "props" tag in XML bean definitions, + * allowing for merging multiple properties sets into one. + */ + public void setPropertiesArray(Properties... propertiesArray) { + this.localProperties = propertiesArray; + } + + /** + * Set a location of a properties file to be loaded. + *

    Can point to a classic properties file or to an XML file + * that follows JDK 1.5's properties XML format. + */ + public void setLocation(Resource location) { + this.locations = new Resource[] {location}; + } + + /** + * Set locations of properties files to be loaded. + *

    Can point to classic properties files or to XML files + * that follow JDK 1.5's properties XML format. + *

    Note: Properties defined in later files will override + * properties defined earlier files, in case of overlapping keys. + * Hence, make sure that the most specific files are the last + * ones in the given list of locations. + */ + public void setLocations(Resource... locations) { + this.locations = locations; + } + + /** + * Set whether local properties override properties from files. + *

    Default is "false": Properties from files override local defaults. + * Can be switched to "true" to let local properties override defaults + * from files. + */ + public void setLocalOverride(boolean localOverride) { + this.localOverride = localOverride; + } + + /** + * Set if failure to find the property resource should be ignored. + *

    "true" is appropriate if the properties file is completely optional. + * Default is "false". + */ + public void setIgnoreResourceNotFound(boolean ignoreResourceNotFound) { + this.ignoreResourceNotFound = ignoreResourceNotFound; + } + + /** + * Set the encoding to use for parsing properties files. + *

    Default is none, using the {@code java.util.Properties} + * default encoding. + *

    Only applies to classic properties files, not to XML files. + * @see org.springframework.util.PropertiesPersister#load + */ + public void setFileEncoding(String encoding) { + this.fileEncoding = encoding; + } + + /** + * Set the PropertiesPersister to use for parsing properties files. + * The default is ResourcePropertiesPersister. + * @see ResourcePropertiesPersister#INSTANCE + */ + public void setPropertiesPersister(@Nullable PropertiesPersister propertiesPersister) { + this.propertiesPersister = + (propertiesPersister != null ? propertiesPersister : ResourcePropertiesPersister.INSTANCE); + } + + + /** + * Return a merged Properties instance containing both the + * loaded properties and properties set on this FactoryBean. + */ + protected Properties mergeProperties() throws IOException { + Properties result = new Properties(); + + if (this.localOverride) { + // Load properties from file upfront, to let local properties override. + loadProperties(result); + } + + if (this.localProperties != null) { + for (Properties localProp : this.localProperties) { + CollectionUtils.mergePropertiesIntoMap(localProp, result); + } + } + + if (!this.localOverride) { + // Load properties from file afterwards, to let those properties override. + loadProperties(result); + } + + return result; + } + + /** + * Load properties into the given instance. + * @param props the Properties instance to load into + * @throws IOException in case of I/O errors + * @see #setLocations + */ + protected void loadProperties(Properties props) throws IOException { + if (this.locations != null) { + for (Resource location : this.locations) { + if (logger.isTraceEnabled()) { + logger.trace("Loading properties file from " + location); + } + try { + PropertiesLoaderUtils.fillProperties( + props, new EncodedResource(location, this.fileEncoding), this.propertiesPersister); + } + catch (FileNotFoundException | UnknownHostException | SocketException ex) { + if (this.ignoreResourceNotFound) { + if (logger.isDebugEnabled()) { + logger.debug("Properties resource not found: " + ex.getMessage()); + } + } + else { + throw ex; + } + } + } + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PropertiesLoaderUtils.java b/spring-core/src/main/java/org/springframework/core/io/support/PropertiesLoaderUtils.java new file mode 100644 index 0000000..a464f6b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/PropertiesLoaderUtils.java @@ -0,0 +1,210 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.support; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.net.URL; +import java.net.URLConnection; +import java.util.Enumeration; +import java.util.Properties; + +import org.springframework.core.SpringProperties; +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.PropertiesPersister; +import org.springframework.util.ResourceUtils; + +/** + * Convenient utility methods for loading of {@code java.util.Properties}, + * performing standard handling of input streams. + * + *

    For more configurable properties loading, including the option of a + * customized encoding, consider using the PropertiesLoaderSupport class. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @author Sebastien Deleuze + * @since 2.0 + * @see PropertiesLoaderSupport + */ +public abstract class PropertiesLoaderUtils { + + private static final String XML_FILE_EXTENSION = ".xml"; + + /** + * Boolean flag controlled by a {@code spring.xml.ignore} system property that instructs Spring to + * ignore XML, i.e. to not initialize the XML-related infrastructure. + *

    The default is "false". + */ + private static final boolean shouldIgnoreXml = SpringProperties.getFlag("spring.xml.ignore"); + + + /** + * Load properties from the given EncodedResource, + * potentially defining a specific encoding for the properties file. + * @see #fillProperties(java.util.Properties, EncodedResource) + */ + public static Properties loadProperties(EncodedResource resource) throws IOException { + Properties props = new Properties(); + fillProperties(props, resource); + return props; + } + + /** + * Fill the given properties from the given EncodedResource, + * potentially defining a specific encoding for the properties file. + * @param props the Properties instance to load into + * @param resource the resource to load from + * @throws IOException in case of I/O errors + */ + public static void fillProperties(Properties props, EncodedResource resource) + throws IOException { + + fillProperties(props, resource, ResourcePropertiesPersister.INSTANCE); + } + + /** + * Actually load properties from the given EncodedResource into the given Properties instance. + * @param props the Properties instance to load into + * @param resource the resource to load from + * @param persister the PropertiesPersister to use + * @throws IOException in case of I/O errors + */ + static void fillProperties(Properties props, EncodedResource resource, PropertiesPersister persister) + throws IOException { + + InputStream stream = null; + Reader reader = null; + try { + String filename = resource.getResource().getFilename(); + if (filename != null && filename.endsWith(XML_FILE_EXTENSION)) { + if (shouldIgnoreXml) { + throw new UnsupportedOperationException("XML support disabled"); + } + stream = resource.getInputStream(); + persister.loadFromXml(props, stream); + } + else if (resource.requiresReader()) { + reader = resource.getReader(); + persister.load(props, reader); + } + else { + stream = resource.getInputStream(); + persister.load(props, stream); + } + } + finally { + if (stream != null) { + stream.close(); + } + if (reader != null) { + reader.close(); + } + } + } + + /** + * Load properties from the given resource (in ISO-8859-1 encoding). + * @param resource the resource to load from + * @return the populated Properties instance + * @throws IOException if loading failed + * @see #fillProperties(java.util.Properties, Resource) + */ + public static Properties loadProperties(Resource resource) throws IOException { + Properties props = new Properties(); + fillProperties(props, resource); + return props; + } + + /** + * Fill the given properties from the given resource (in ISO-8859-1 encoding). + * @param props the Properties instance to fill + * @param resource the resource to load from + * @throws IOException if loading failed + */ + public static void fillProperties(Properties props, Resource resource) throws IOException { + try (InputStream is = resource.getInputStream()) { + String filename = resource.getFilename(); + if (filename != null && filename.endsWith(XML_FILE_EXTENSION)) { + if (shouldIgnoreXml) { + throw new UnsupportedOperationException("XML support disabled"); + } + props.loadFromXML(is); + } + else { + props.load(is); + } + } + } + + /** + * Load all properties from the specified class path resource + * (in ISO-8859-1 encoding), using the default class loader. + *

    Merges properties if more than one resource of the same name + * found in the class path. + * @param resourceName the name of the class path resource + * @return the populated Properties instance + * @throws IOException if loading failed + */ + public static Properties loadAllProperties(String resourceName) throws IOException { + return loadAllProperties(resourceName, null); + } + + /** + * Load all properties from the specified class path resource + * (in ISO-8859-1 encoding), using the given class loader. + *

    Merges properties if more than one resource of the same name + * found in the class path. + * @param resourceName the name of the class path resource + * @param classLoader the ClassLoader to use for loading + * (or {@code null} to use the default class loader) + * @return the populated Properties instance + * @throws IOException if loading failed + */ + public static Properties loadAllProperties(String resourceName, @Nullable ClassLoader classLoader) throws IOException { + Assert.notNull(resourceName, "Resource name must not be null"); + ClassLoader classLoaderToUse = classLoader; + if (classLoaderToUse == null) { + classLoaderToUse = ClassUtils.getDefaultClassLoader(); + } + Enumeration urls = (classLoaderToUse != null ? classLoaderToUse.getResources(resourceName) : + ClassLoader.getSystemResources(resourceName)); + Properties props = new Properties(); + while (urls.hasMoreElements()) { + URL url = urls.nextElement(); + URLConnection con = url.openConnection(); + ResourceUtils.useCachesIfNecessary(con); + try (InputStream is = con.getInputStream()) { + if (resourceName.endsWith(XML_FILE_EXTENSION)) { + if (shouldIgnoreXml) { + throw new UnsupportedOperationException("XML support disabled"); + } + props.loadFromXML(is); + } + else { + props.load(is); + } + } + } + return props; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceFactory.java b/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceFactory.java new file mode 100644 index 0000000..21cba75 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceFactory.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.support; + +import java.io.IOException; + +import org.springframework.core.env.PropertySource; +import org.springframework.lang.Nullable; + +/** + * Strategy interface for creating resource-based {@link PropertySource} wrappers. + * + * @author Juergen Hoeller + * @since 4.3 + * @see DefaultPropertySourceFactory + */ +public interface PropertySourceFactory { + + /** + * Create a {@link PropertySource} that wraps the given resource. + * @param name the name of the property source + * (can be {@code null} in which case the factory implementation + * will have to generate a name based on the given resource) + * @param resource the resource (potentially encoded) to wrap + * @return the new {@link PropertySource} (never {@code null}) + * @throws IOException if resource resolution failed + */ + PropertySource createPropertySource(@Nullable String name, EncodedResource resource) throws IOException; + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/support/ResourceArrayPropertyEditor.java b/spring-core/src/main/java/org/springframework/core/io/support/ResourceArrayPropertyEditor.java new file mode 100644 index 0000000..ec1c00c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/ResourceArrayPropertyEditor.java @@ -0,0 +1,185 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.support; + +import java.beans.PropertyEditorSupport; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.env.Environment; +import org.springframework.core.env.PropertyResolver; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Editor for {@link org.springframework.core.io.Resource} arrays, to + * automatically convert {@code String} location patterns + * (e.g. {@code "file:C:/my*.txt"} or {@code "classpath*:myfile.txt"}) + * to {@code Resource} array properties. Can also translate a collection + * or array of location patterns into a merged Resource array. + * + *

    A path may contain {@code ${...}} placeholders, to be + * resolved as {@link org.springframework.core.env.Environment} properties: + * e.g. {@code ${user.dir}}. Unresolvable placeholders are ignored by default. + * + *

    Delegates to a {@link ResourcePatternResolver}, + * by default using a {@link PathMatchingResourcePatternResolver}. + * + * @author Juergen Hoeller + * @author Chris Beams + * @since 1.1.2 + * @see org.springframework.core.io.Resource + * @see ResourcePatternResolver + * @see PathMatchingResourcePatternResolver + */ +public class ResourceArrayPropertyEditor extends PropertyEditorSupport { + + private static final Log logger = LogFactory.getLog(ResourceArrayPropertyEditor.class); + + private final ResourcePatternResolver resourcePatternResolver; + + @Nullable + private PropertyResolver propertyResolver; + + private final boolean ignoreUnresolvablePlaceholders; + + + /** + * Create a new ResourceArrayPropertyEditor with a default + * {@link PathMatchingResourcePatternResolver} and {@link StandardEnvironment}. + * @see PathMatchingResourcePatternResolver + * @see Environment + */ + public ResourceArrayPropertyEditor() { + this(new PathMatchingResourcePatternResolver(), null, true); + } + + /** + * Create a new ResourceArrayPropertyEditor with the given {@link ResourcePatternResolver} + * and {@link PropertyResolver} (typically an {@link Environment}). + * @param resourcePatternResolver the ResourcePatternResolver to use + * @param propertyResolver the PropertyResolver to use + */ + public ResourceArrayPropertyEditor( + ResourcePatternResolver resourcePatternResolver, @Nullable PropertyResolver propertyResolver) { + + this(resourcePatternResolver, propertyResolver, true); + } + + /** + * Create a new ResourceArrayPropertyEditor with the given {@link ResourcePatternResolver} + * and {@link PropertyResolver} (typically an {@link Environment}). + * @param resourcePatternResolver the ResourcePatternResolver to use + * @param propertyResolver the PropertyResolver to use + * @param ignoreUnresolvablePlaceholders whether to ignore unresolvable placeholders + * if no corresponding system property could be found + */ + public ResourceArrayPropertyEditor(ResourcePatternResolver resourcePatternResolver, + @Nullable PropertyResolver propertyResolver, boolean ignoreUnresolvablePlaceholders) { + + Assert.notNull(resourcePatternResolver, "ResourcePatternResolver must not be null"); + this.resourcePatternResolver = resourcePatternResolver; + this.propertyResolver = propertyResolver; + this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders; + } + + + /** + * Treat the given text as a location pattern and convert it to a Resource array. + */ + @Override + public void setAsText(String text) { + String pattern = resolvePath(text).trim(); + try { + setValue(this.resourcePatternResolver.getResources(pattern)); + } + catch (IOException ex) { + throw new IllegalArgumentException( + "Could not resolve resource location pattern [" + pattern + "]: " + ex.getMessage()); + } + } + + /** + * Treat the given value as a collection or array and convert it to a Resource array. + * Considers String elements as location patterns and takes Resource elements as-is. + */ + @Override + public void setValue(Object value) throws IllegalArgumentException { + if (value instanceof Collection || (value instanceof Object[] && !(value instanceof Resource[]))) { + Collection input = (value instanceof Collection ? (Collection) value : Arrays.asList((Object[]) value)); + Set merged = new LinkedHashSet<>(); + for (Object element : input) { + if (element instanceof String) { + // A location pattern: resolve it into a Resource array. + // Might point to a single resource or to multiple resources. + String pattern = resolvePath((String) element).trim(); + try { + Resource[] resources = this.resourcePatternResolver.getResources(pattern); + Collections.addAll(merged, resources); + } + catch (IOException ex) { + // ignore - might be an unresolved placeholder or non-existing base directory + if (logger.isDebugEnabled()) { + logger.debug("Could not retrieve resources for pattern '" + pattern + "'", ex); + } + } + } + else if (element instanceof Resource) { + // A Resource object: add it to the result. + merged.add((Resource) element); + } + else { + throw new IllegalArgumentException("Cannot convert element [" + element + "] to [" + + Resource.class.getName() + "]: only location String and Resource object supported"); + } + } + super.setValue(merged.toArray(new Resource[0])); + } + + else { + // An arbitrary value: probably a String or a Resource array. + // setAsText will be called for a String; a Resource array will be used as-is. + super.setValue(value); + } + } + + /** + * Resolve the given path, replacing placeholders with + * corresponding system property values if necessary. + * @param path the original file path + * @return the resolved file path + * @see PropertyResolver#resolvePlaceholders + * @see PropertyResolver#resolveRequiredPlaceholders(String) + */ + protected String resolvePath(String path) { + if (this.propertyResolver == null) { + this.propertyResolver = new StandardEnvironment(); + } + return (this.ignoreUnresolvablePlaceholders ? this.propertyResolver.resolvePlaceholders(path) : + this.propertyResolver.resolveRequiredPlaceholders(path)); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/support/ResourcePatternResolver.java b/spring-core/src/main/java/org/springframework/core/io/support/ResourcePatternResolver.java new file mode 100644 index 0000000..b079382 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/ResourcePatternResolver.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2007 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.support; + +import java.io.IOException; + +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; + +/** + * Strategy interface for resolving a location pattern (for example, + * an Ant-style path pattern) into Resource objects. + * + *

    This is an extension to the {@link org.springframework.core.io.ResourceLoader} + * interface. A passed-in ResourceLoader (for example, an + * {@link org.springframework.context.ApplicationContext} passed in via + * {@link org.springframework.context.ResourceLoaderAware} when running in a context) + * can be checked whether it implements this extended interface too. + * + *

    {@link PathMatchingResourcePatternResolver} is a standalone implementation + * that is usable outside an ApplicationContext, also used by + * {@link ResourceArrayPropertyEditor} for populating Resource array bean properties. + * + *

    Can be used with any sort of location pattern (e.g. "/WEB-INF/*-context.xml"): + * Input patterns have to match the strategy implementation. This interface just + * specifies the conversion method rather than a specific pattern format. + * + *

    This interface also suggests a new resource prefix "classpath*:" for all + * matching resources from the class path. Note that the resource location is + * expected to be a path without placeholders in this case (e.g. "/beans.xml"); + * JAR files or classes directories can contain multiple files of the same name. + * + * @author Juergen Hoeller + * @since 1.0.2 + * @see org.springframework.core.io.Resource + * @see org.springframework.core.io.ResourceLoader + * @see org.springframework.context.ApplicationContext + * @see org.springframework.context.ResourceLoaderAware + */ +public interface ResourcePatternResolver extends ResourceLoader { + + /** + * Pseudo URL prefix for all matching resources from the class path: "classpath*:" + * This differs from ResourceLoader's classpath URL prefix in that it + * retrieves all matching resources for a given name (e.g. "/beans.xml"), + * for example in the root of all deployed JAR files. + * @see org.springframework.core.io.ResourceLoader#CLASSPATH_URL_PREFIX + */ + String CLASSPATH_ALL_URL_PREFIX = "classpath*:"; + + /** + * Resolve the given location pattern into Resource objects. + *

    Overlapping resource entries that point to the same physical + * resource should be avoided, as far as possible. The result should + * have set semantics. + * @param locationPattern the location pattern to resolve + * @return the corresponding Resource objects + * @throws IOException in case of I/O errors + */ + Resource[] getResources(String locationPattern) throws IOException; + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/support/ResourcePatternUtils.java b/spring-core/src/main/java/org/springframework/core/io/support/ResourcePatternUtils.java new file mode 100644 index 0000000..3fb8350 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/ResourcePatternUtils.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.support; + +import org.springframework.core.io.ResourceLoader; +import org.springframework.lang.Nullable; +import org.springframework.util.ResourceUtils; + +/** + * Utility class for determining whether a given URL is a resource + * location that can be loaded via a {@link ResourcePatternResolver}. + * + *

    Callers will usually assume that a location is a relative path + * if the {@link #isUrl(String)} method returns {@code false}. + * + * @author Juergen Hoeller + * @since 1.2.3 + */ +public abstract class ResourcePatternUtils { + + /** + * Return whether the given resource location is a URL: either a + * special "classpath" or "classpath*" pseudo URL or a standard URL. + * @param resourceLocation the location String to check + * @return whether the location qualifies as a URL + * @see ResourcePatternResolver#CLASSPATH_ALL_URL_PREFIX + * @see org.springframework.util.ResourceUtils#CLASSPATH_URL_PREFIX + * @see org.springframework.util.ResourceUtils#isUrl(String) + * @see java.net.URL + */ + public static boolean isUrl(@Nullable String resourceLocation) { + return (resourceLocation != null && + (resourceLocation.startsWith(ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX) || + ResourceUtils.isUrl(resourceLocation))); + } + + /** + * Return a default {@link ResourcePatternResolver} for the given {@link ResourceLoader}. + *

    This might be the {@code ResourceLoader} itself, if it implements the + * {@code ResourcePatternResolver} extension, or a default + * {@link PathMatchingResourcePatternResolver} built on the given {@code ResourceLoader}. + * @param resourceLoader the ResourceLoader to build a pattern resolver for + * (may be {@code null} to indicate a default ResourceLoader) + * @return the ResourcePatternResolver + * @see PathMatchingResourcePatternResolver + */ + public static ResourcePatternResolver getResourcePatternResolver(@Nullable ResourceLoader resourceLoader) { + if (resourceLoader instanceof ResourcePatternResolver) { + return (ResourcePatternResolver) resourceLoader; + } + else if (resourceLoader != null) { + return new PathMatchingResourcePatternResolver(resourceLoader); + } + else { + return new PathMatchingResourcePatternResolver(); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/support/ResourcePropertiesPersister.java b/spring-core/src/main/java/org/springframework/core/io/support/ResourcePropertiesPersister.java new file mode 100644 index 0000000..1388c1c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/ResourcePropertiesPersister.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.support; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Properties; + +import org.springframework.core.SpringProperties; +import org.springframework.util.DefaultPropertiesPersister; + +/** + * Spring-aware subclass of the plain {@link DefaultPropertiesPersister}, + * adding a conditional check for disabled XML support through the shared + * "spring.xml.ignore" property. + * + *

    This is the standard implementation used in Spring's resource support. + * + * @author Juergen Hoeller + * @author Sebastien Deleuze + * @since 5.3 + */ +public class ResourcePropertiesPersister extends DefaultPropertiesPersister { + + /** + * A convenient constant for a default {@code ResourcePropertiesPersister} instance, + * as used in Spring's common resource support. + * @since 5.3 + */ + public static final ResourcePropertiesPersister INSTANCE = new ResourcePropertiesPersister(); + + /** + * Boolean flag controlled by a {@code spring.xml.ignore} system property that instructs Spring to + * ignore XML, i.e. to not initialize the XML-related infrastructure. + *

    The default is "false". + */ + private static final boolean shouldIgnoreXml = SpringProperties.getFlag("spring.xml.ignore"); + + + @Override + public void loadFromXml(Properties props, InputStream is) throws IOException { + if (shouldIgnoreXml) { + throw new UnsupportedOperationException("XML support disabled"); + } + super.loadFromXml(props, is); + } + + @Override + public void storeToXml(Properties props, OutputStream os, String header) throws IOException { + if (shouldIgnoreXml) { + throw new UnsupportedOperationException("XML support disabled"); + } + super.storeToXml(props, os, header); + } + + @Override + public void storeToXml(Properties props, OutputStream os, String header, String encoding) throws IOException { + if (shouldIgnoreXml) { + throw new UnsupportedOperationException("XML support disabled"); + } + super.storeToXml(props, os, header, encoding); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/support/ResourcePropertySource.java b/spring-core/src/main/java/org/springframework/core/io/support/ResourcePropertySource.java new file mode 100644 index 0000000..0ae66c7 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/ResourcePropertySource.java @@ -0,0 +1,186 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.support; + +import java.io.IOException; +import java.util.Map; +import java.util.Properties; + +import org.springframework.core.env.PropertiesPropertySource; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Subclass of {@link PropertiesPropertySource} that loads a {@link Properties} object + * from a given {@link org.springframework.core.io.Resource} or resource location such as + * {@code "classpath:/com/myco/foo.properties"} or {@code "file:/path/to/file.xml"}. + * + *

    Both traditional and XML-based properties file formats are supported; however, in + * order for XML processing to take effect, the underlying {@code Resource}'s + * {@link org.springframework.core.io.Resource#getFilename() getFilename()} method must + * return a non-{@code null} value that ends in {@code ".xml"}. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + * @see org.springframework.core.io.Resource + * @see org.springframework.core.io.support.EncodedResource + */ +public class ResourcePropertySource extends PropertiesPropertySource { + + /** The original resource name, if different from the given name. */ + @Nullable + private final String resourceName; + + + /** + * Create a PropertySource having the given name based on Properties + * loaded from the given encoded resource. + */ + public ResourcePropertySource(String name, EncodedResource resource) throws IOException { + super(name, PropertiesLoaderUtils.loadProperties(resource)); + this.resourceName = getNameForResource(resource.getResource()); + } + + /** + * Create a PropertySource based on Properties loaded from the given resource. + * The name of the PropertySource will be generated based on the + * {@link Resource#getDescription() description} of the given resource. + */ + public ResourcePropertySource(EncodedResource resource) throws IOException { + super(getNameForResource(resource.getResource()), PropertiesLoaderUtils.loadProperties(resource)); + this.resourceName = null; + } + + /** + * Create a PropertySource having the given name based on Properties + * loaded from the given encoded resource. + */ + public ResourcePropertySource(String name, Resource resource) throws IOException { + super(name, PropertiesLoaderUtils.loadProperties(new EncodedResource(resource))); + this.resourceName = getNameForResource(resource); + } + + /** + * Create a PropertySource based on Properties loaded from the given resource. + * The name of the PropertySource will be generated based on the + * {@link Resource#getDescription() description} of the given resource. + */ + public ResourcePropertySource(Resource resource) throws IOException { + super(getNameForResource(resource), PropertiesLoaderUtils.loadProperties(new EncodedResource(resource))); + this.resourceName = null; + } + + /** + * Create a PropertySource having the given name based on Properties loaded from + * the given resource location and using the given class loader to load the + * resource (assuming it is prefixed with {@code classpath:}). + */ + public ResourcePropertySource(String name, String location, ClassLoader classLoader) throws IOException { + this(name, new DefaultResourceLoader(classLoader).getResource(location)); + } + + /** + * Create a PropertySource based on Properties loaded from the given resource + * location and use the given class loader to load the resource, assuming it is + * prefixed with {@code classpath:}. The name of the PropertySource will be + * generated based on the {@link Resource#getDescription() description} of the + * resource. + */ + public ResourcePropertySource(String location, ClassLoader classLoader) throws IOException { + this(new DefaultResourceLoader(classLoader).getResource(location)); + } + + /** + * Create a PropertySource having the given name based on Properties loaded from + * the given resource location. The default thread context class loader will be + * used to load the resource (assuming the location string is prefixed with + * {@code classpath:}. + */ + public ResourcePropertySource(String name, String location) throws IOException { + this(name, new DefaultResourceLoader().getResource(location)); + } + + /** + * Create a PropertySource based on Properties loaded from the given resource + * location. The name of the PropertySource will be generated based on the + * {@link Resource#getDescription() description} of the resource. + */ + public ResourcePropertySource(String location) throws IOException { + this(new DefaultResourceLoader().getResource(location)); + } + + private ResourcePropertySource(String name, @Nullable String resourceName, Map source) { + super(name, source); + this.resourceName = resourceName; + } + + + /** + * Return a potentially adapted variant of this {@link ResourcePropertySource}, + * overriding the previously given (or derived) name with the specified name. + * @since 4.0.4 + */ + public ResourcePropertySource withName(String name) { + if (this.name.equals(name)) { + return this; + } + // Store the original resource name if necessary... + if (this.resourceName != null) { + if (this.resourceName.equals(name)) { + return new ResourcePropertySource(this.resourceName, null, this.source); + } + else { + return new ResourcePropertySource(name, this.resourceName, this.source); + } + } + else { + // Current name is resource name -> preserve it in the extra field... + return new ResourcePropertySource(name, this.name, this.source); + } + } + + /** + * Return a potentially adapted variant of this {@link ResourcePropertySource}, + * overriding the previously given name (if any) with the original resource name + * (equivalent to the name generated by the name-less constructor variants). + * @since 4.1 + */ + public ResourcePropertySource withResourceName() { + if (this.resourceName == null) { + return this; + } + return new ResourcePropertySource(this.resourceName, null, this.source); + } + + + /** + * Return the description for the given Resource; if the description is + * empty, return the class name of the resource plus its identity hash code. + * @see org.springframework.core.io.Resource#getDescription() + */ + private static String getNameForResource(Resource resource) { + String name = resource.getDescription(); + if (!StringUtils.hasText(name)) { + name = resource.getClass().getSimpleName() + "@" + System.identityHashCode(resource); + } + return name; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/support/ResourceRegion.java b/spring-core/src/main/java/org/springframework/core/io/support/ResourceRegion.java new file mode 100644 index 0000000..dfa7b67 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/ResourceRegion.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.support; + +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; + +/** + * Region of a {@link Resource} implementation, materialized by a {@code position} + * within the {@link Resource} and a byte {@code count} for the length of that region. + * + * @author Arjen Poutsma + * @since 4.3 + */ +public class ResourceRegion { + + private final Resource resource; + + private final long position; + + private final long count; + + + /** + * Create a new {@code ResourceRegion} from a given {@link Resource}. + * This region of a resource is represented by a start {@code position} + * and a byte {@code count} within the given {@code Resource}. + * @param resource a Resource + * @param position the start position of the region in that resource + * @param count the byte count of the region in that resource + */ + public ResourceRegion(Resource resource, long position, long count) { + Assert.notNull(resource, "Resource must not be null"); + Assert.isTrue(position >= 0, "'position' must be larger than or equal to 0"); + Assert.isTrue(count >= 0, "'count' must be larger than or equal to 0"); + this.resource = resource; + this.position = position; + this.count = count; + } + + + /** + * Return the underlying {@link Resource} for this {@code ResourceRegion}. + */ + public Resource getResource() { + return this.resource; + } + + /** + * Return the start position of this region in the underlying {@link Resource}. + */ + public long getPosition() { + return this.position; + } + + /** + * Return the byte count of this region in the underlying {@link Resource}. + */ + public long getCount() { + return this.count; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/support/SpringFactoriesLoader.java b/spring-core/src/main/java/org/springframework/core/io/support/SpringFactoriesLoader.java new file mode 100644 index 0000000..c73e775 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/SpringFactoriesLoader.java @@ -0,0 +1,188 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.support; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.stream.Collectors; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.io.UrlResource; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * General purpose factory loading mechanism for internal use within the framework. + * + *

    {@code SpringFactoriesLoader} {@linkplain #loadFactories loads} and instantiates + * factories of a given type from {@value #FACTORIES_RESOURCE_LOCATION} files which + * may be present in multiple JAR files in the classpath. The {@code spring.factories} + * file must be in {@link Properties} format, where the key is the fully qualified + * name of the interface or abstract class, and the value is a comma-separated list of + * implementation class names. For example: + * + *

    example.MyService=example.MyServiceImpl1,example.MyServiceImpl2
    + * + * where {@code example.MyService} is the name of the interface, and {@code MyServiceImpl1} + * and {@code MyServiceImpl2} are two implementations. + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @author Sam Brannen + * @since 3.2 + */ +public final class SpringFactoriesLoader { + + /** + * The location to look for factories. + *

    Can be present in multiple JAR files. + */ + public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"; + + + private static final Log logger = LogFactory.getLog(SpringFactoriesLoader.class); + + static final Map>> cache = new ConcurrentReferenceHashMap<>(); + + + private SpringFactoriesLoader() { + } + + + /** + * Load and instantiate the factory implementations of the given type from + * {@value #FACTORIES_RESOURCE_LOCATION}, using the given class loader. + *

    The returned factories are sorted through {@link AnnotationAwareOrderComparator}. + *

    If a custom instantiation strategy is required, use {@link #loadFactoryNames} + * to obtain all registered factory names. + *

    As of Spring Framework 5.3, if duplicate implementation class names are + * discovered for a given factory type, only one instance of the duplicated + * implementation type will be instantiated. + * @param factoryType the interface or abstract class representing the factory + * @param classLoader the ClassLoader to use for loading (can be {@code null} to use the default) + * @throws IllegalArgumentException if any factory implementation class cannot + * be loaded or if an error occurs while instantiating any factory + * @see #loadFactoryNames + */ + public static List loadFactories(Class factoryType, @Nullable ClassLoader classLoader) { + Assert.notNull(factoryType, "'factoryType' must not be null"); + ClassLoader classLoaderToUse = classLoader; + if (classLoaderToUse == null) { + classLoaderToUse = SpringFactoriesLoader.class.getClassLoader(); + } + List factoryImplementationNames = loadFactoryNames(factoryType, classLoaderToUse); + if (logger.isTraceEnabled()) { + logger.trace("Loaded [" + factoryType.getName() + "] names: " + factoryImplementationNames); + } + List result = new ArrayList<>(factoryImplementationNames.size()); + for (String factoryImplementationName : factoryImplementationNames) { + result.add(instantiateFactory(factoryImplementationName, factoryType, classLoaderToUse)); + } + AnnotationAwareOrderComparator.sort(result); + return result; + } + + /** + * Load the fully qualified class names of factory implementations of the + * given type from {@value #FACTORIES_RESOURCE_LOCATION}, using the given + * class loader. + *

    As of Spring Framework 5.3, if a particular implementation class name + * is discovered more than once for the given factory type, duplicates will + * be ignored. + * @param factoryType the interface or abstract class representing the factory + * @param classLoader the ClassLoader to use for loading resources; can be + * {@code null} to use the default + * @throws IllegalArgumentException if an error occurs while loading factory names + * @see #loadFactories + */ + public static List loadFactoryNames(Class factoryType, @Nullable ClassLoader classLoader) { + ClassLoader classLoaderToUse = classLoader; + if (classLoaderToUse == null) { + classLoaderToUse = SpringFactoriesLoader.class.getClassLoader(); + } + String factoryTypeName = factoryType.getName(); + return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList()); + } + + private static Map> loadSpringFactories(ClassLoader classLoader) { + Map> result = cache.get(classLoader); + if (result != null) { + return result; + } + + result = new HashMap<>(); + try { + Enumeration urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION); + while (urls.hasMoreElements()) { + URL url = urls.nextElement(); + UrlResource resource = new UrlResource(url); + Properties properties = PropertiesLoaderUtils.loadProperties(resource); + for (Map.Entry entry : properties.entrySet()) { + String factoryTypeName = ((String) entry.getKey()).trim(); + String[] factoryImplementationNames = + StringUtils.commaDelimitedListToStringArray((String) entry.getValue()); + for (String factoryImplementationName : factoryImplementationNames) { + result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>()) + .add(factoryImplementationName.trim()); + } + } + } + + // Replace all lists with unmodifiable lists containing unique elements + result.replaceAll((factoryType, implementations) -> implementations.stream().distinct() + .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList))); + cache.put(classLoader, result); + } + catch (IOException ex) { + throw new IllegalArgumentException("Unable to load factories from location [" + + FACTORIES_RESOURCE_LOCATION + "]", ex); + } + return result; + } + + @SuppressWarnings("unchecked") + private static T instantiateFactory(String factoryImplementationName, Class factoryType, ClassLoader classLoader) { + try { + Class factoryImplementationClass = ClassUtils.forName(factoryImplementationName, classLoader); + if (!factoryType.isAssignableFrom(factoryImplementationClass)) { + throw new IllegalArgumentException( + "Class [" + factoryImplementationName + "] is not assignable to factory type [" + factoryType.getName() + "]"); + } + return (T) ReflectionUtils.accessibleConstructor(factoryImplementationClass).newInstance(); + } + catch (Throwable ex) { + throw new IllegalArgumentException( + "Unable to instantiate factory class [" + factoryImplementationName + "] for factory type [" + factoryType.getName() + "]", + ex); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/support/VfsPatternUtils.java b/spring-core/src/main/java/org/springframework/core/io/support/VfsPatternUtils.java new file mode 100644 index 0000000..5fcaf8a --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/VfsPatternUtils.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.support; + +import java.io.IOException; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; +import java.net.URL; + +import org.springframework.core.io.VfsUtils; +import org.springframework.lang.Nullable; + +/** + * Artificial class used for accessing the {@link VfsUtils} methods + * without exposing them to the entire world. + * + * @author Costin Leau + * @since 3.0.3 + */ +abstract class VfsPatternUtils extends VfsUtils { + + @Nullable + static Object getVisitorAttributes() { + return doGetVisitorAttributes(); + } + + static String getPath(Object resource) { + String path = doGetPath(resource); + return (path != null ? path : ""); + } + + static Object findRoot(URL url) throws IOException { + return getRoot(url); + } + + static void visit(Object resource, InvocationHandler visitor) throws IOException { + Object visitorProxy = Proxy.newProxyInstance( + VIRTUAL_FILE_VISITOR_INTERFACE.getClassLoader(), + new Class[] {VIRTUAL_FILE_VISITOR_INTERFACE}, visitor); + invokeVfsMethod(VIRTUAL_FILE_METHOD_VISIT, resource, visitorProxy); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/support/package-info.java b/spring-core/src/main/java/org/springframework/core/io/support/package-info.java new file mode 100644 index 0000000..2b1395f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/package-info.java @@ -0,0 +1,10 @@ +/** + * Support classes for Spring's resource abstraction. + * Includes a ResourcePatternResolver mechanism. + */ +@NonNullApi +@NonNullFields +package org.springframework.core.io.support; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/main/java/org/springframework/core/log/CompositeLog.java b/spring-core/src/main/java/org/springframework/core/log/CompositeLog.java new file mode 100644 index 0000000..9279180 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/log/CompositeLog.java @@ -0,0 +1,165 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.log; + +import java.util.List; +import java.util.function.Predicate; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.impl.NoOpLog; + +/** + * Implementation of {@link Log} that wraps a list of loggers and delegates + * to the first one for which logging is enabled at the given level. + * + * @author Rossen Stoyanchev + * @since 5.1 + * @see LogDelegateFactory#getCompositeLog + */ +final class CompositeLog implements Log { + + private static final Log NO_OP_LOG = new NoOpLog(); + + + private final Log fatalLogger; + + private final Log errorLogger; + + private final Log warnLogger; + + private final Log infoLogger; + + private final Log debugLogger; + + private final Log traceLogger; + + + /** + * Constructor with list of loggers. For optimal performance, the constructor + * checks and remembers which logger is on for each log category. + * @param loggers the loggers to use + */ + public CompositeLog(List loggers) { + this.fatalLogger = initLogger(loggers, Log::isFatalEnabled); + this.errorLogger = initLogger(loggers, Log::isErrorEnabled); + this.warnLogger = initLogger(loggers, Log::isWarnEnabled); + this.infoLogger = initLogger(loggers, Log::isInfoEnabled); + this.debugLogger = initLogger(loggers, Log::isDebugEnabled); + this.traceLogger = initLogger(loggers, Log::isTraceEnabled); + } + + private static Log initLogger(List loggers, Predicate predicate) { + for (Log logger : loggers) { + if (predicate.test(logger)) { + return logger; + } + } + return NO_OP_LOG; + } + + + @Override + public boolean isFatalEnabled() { + return (this.fatalLogger != NO_OP_LOG); + } + + @Override + public boolean isErrorEnabled() { + return (this.errorLogger != NO_OP_LOG); + } + + @Override + public boolean isWarnEnabled() { + return (this.warnLogger != NO_OP_LOG); + } + + @Override + public boolean isInfoEnabled() { + return (this.infoLogger != NO_OP_LOG); + } + + @Override + public boolean isDebugEnabled() { + return (this.debugLogger != NO_OP_LOG); + } + + @Override + public boolean isTraceEnabled() { + return (this.traceLogger != NO_OP_LOG); + } + + @Override + public void fatal(Object message) { + this.fatalLogger.fatal(message); + } + + @Override + public void fatal(Object message, Throwable ex) { + this.fatalLogger.fatal(message, ex); + } + + @Override + public void error(Object message) { + this.errorLogger.error(message); + } + + @Override + public void error(Object message, Throwable ex) { + this.errorLogger.error(message, ex); + } + + @Override + public void warn(Object message) { + this.warnLogger.warn(message); + } + + @Override + public void warn(Object message, Throwable ex) { + this.warnLogger.warn(message, ex); + } + + @Override + public void info(Object message) { + this.infoLogger.info(message); + } + + @Override + public void info(Object message, Throwable ex) { + this.infoLogger.info(message, ex); + } + + @Override + public void debug(Object message) { + this.debugLogger.debug(message); + } + + @Override + public void debug(Object message, Throwable ex) { + this.debugLogger.debug(message, ex); + } + + @Override + public void trace(Object message) { + this.traceLogger.trace(message); + } + + @Override + public void trace(Object message, Throwable ex) { + this.traceLogger.trace(message, ex); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/log/LogAccessor.java b/spring-core/src/main/java/org/springframework/core/log/LogAccessor.java new file mode 100644 index 0000000..1ad7701 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/log/LogAccessor.java @@ -0,0 +1,349 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.log; + +import java.util.function.Supplier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * A convenient accessor for Commons Logging, providing not only + * {@code CharSequence} based log methods but also {@code Supplier} + * based variants for use with Java 8 lambda expressions. + * + * @author Juergen Hoeller + * @since 5.2 + */ +public class LogAccessor { + + private final Log log; + + + /** + * Create a new accessor for the given Commons Log. + * @see LogFactory#getLog(Class) + * @see LogFactory#getLog(String) + */ + public LogAccessor(Log log) { + this.log = log; + } + + /** + * Create a new accessor for the specified Commons Log category. + * @see LogFactory#getLog(Class) + */ + public LogAccessor(Class logCategory) { + this.log = LogFactory.getLog(logCategory); + } + + /** + * Create a new accessor for the specified Commons Log category. + * @see LogFactory#getLog(String) + */ + public LogAccessor(String logCategory) { + this.log = LogFactory.getLog(logCategory); + } + + + /** + * Return the target Commons Log. + */ + public final Log getLog() { + return this.log; + } + + + // Log level checks + + /** + * Is fatal logging currently enabled? + */ + public boolean isFatalEnabled() { + return this.log.isFatalEnabled(); + } + + /** + * Is error logging currently enabled? + */ + public boolean isErrorEnabled() { + return this.log.isErrorEnabled(); + } + + /** + * Is warn logging currently enabled? + */ + public boolean isWarnEnabled() { + return this.log.isWarnEnabled(); + } + + /** + * Is info logging currently enabled? + */ + public boolean isInfoEnabled() { + return this.log.isInfoEnabled(); + } + + /** + * Is debug logging currently enabled? + */ + public boolean isDebugEnabled() { + return this.log.isDebugEnabled(); + } + + /** + * Is trace logging currently enabled? + */ + public boolean isTraceEnabled() { + return this.log.isTraceEnabled(); + } + + + // Plain log methods + + /** + * Log a message with fatal log level. + * @param message the message to log + */ + public void fatal(CharSequence message) { + this.log.fatal(message); + } + + /** + * Log an error with fatal log level. + * @param cause the exception to log + * @param message the message to log + */ + public void fatal(Throwable cause, CharSequence message) { + this.log.fatal(message, cause); + } + + /** + * Log a message with error log level. + * @param message the message to log + */ + public void error(CharSequence message) { + this.log.error(message); + } + + /** + * Log an error with error log level. + * @param cause the exception to log + * @param message the message to log + */ + public void error(Throwable cause, CharSequence message) { + this.log.error(message, cause); + } + + /** + * Log a message with warn log level. + * @param message the message to log + */ + public void warn(CharSequence message) { + this.log.warn(message); + } + + /** + * Log an error with warn log level. + * @param cause the exception to log + * @param message the message to log + */ + public void warn(Throwable cause, CharSequence message) { + this.log.warn(message, cause); + } + + /** + * Log a message with info log level. + * @param message the message to log + */ + public void info(CharSequence message) { + this.log.info(message); + } + + /** + * Log an error with info log level. + * @param cause the exception to log + * @param message the message to log + */ + public void info(Throwable cause, CharSequence message) { + this.log.info(message, cause); + } + + /** + * Log a message with debug log level. + * @param message the message to log + */ + public void debug(CharSequence message) { + this.log.debug(message); + } + + /** + * Log an error with debug log level. + * @param cause the exception to log + * @param message the message to log + */ + public void debug(Throwable cause, CharSequence message) { + this.log.debug(message, cause); + } + + /** + * Log a message with trace log level. + * @param message the message to log + */ + public void trace(CharSequence message) { + this.log.trace(message); + } + + /** + * Log an error with trace log level. + * @param cause the exception to log + * @param message the message to log + */ + public void trace(Throwable cause, CharSequence message) { + this.log.trace(message, cause); + } + + + // Supplier-based log methods + + /** + * Log a message with fatal log level. + * @param messageSupplier a lazy supplier for the message to log + */ + public void fatal(Supplier messageSupplier) { + if (this.log.isFatalEnabled()) { + this.log.fatal(LogMessage.of(messageSupplier)); + } + } + + /** + * Log an error with fatal log level. + * @param cause the exception to log + * @param messageSupplier a lazy supplier for the message to log + */ + public void fatal(Throwable cause, Supplier messageSupplier) { + if (this.log.isFatalEnabled()) { + this.log.fatal(LogMessage.of(messageSupplier), cause); + } + } + + /** + * Log a message with error log level. + * @param messageSupplier a lazy supplier for the message to log + */ + public void error(Supplier messageSupplier) { + if (this.log.isErrorEnabled()) { + this.log.error(LogMessage.of(messageSupplier)); + } + } + + /** + * Log an error with error log level. + * @param cause the exception to log + * @param messageSupplier a lazy supplier for the message to log + */ + public void error(Throwable cause, Supplier messageSupplier) { + if (this.log.isErrorEnabled()) { + this.log.error(LogMessage.of(messageSupplier), cause); + } + } + + /** + * Log a message with warn log level. + * @param messageSupplier a lazy supplier for the message to log + */ + public void warn(Supplier messageSupplier) { + if (this.log.isWarnEnabled()) { + this.log.warn(LogMessage.of(messageSupplier)); + } + } + + /** + * Log an error with warn log level. + * @param cause the exception to log + * @param messageSupplier a lazy supplier for the message to log + */ + public void warn(Throwable cause, Supplier messageSupplier) { + if (this.log.isWarnEnabled()) { + this.log.warn(LogMessage.of(messageSupplier), cause); + } + } + + /** + * Log a message with info log level. + * @param messageSupplier a lazy supplier for the message to log + */ + public void info(Supplier messageSupplier) { + if (this.log.isInfoEnabled()) { + this.log.info(LogMessage.of(messageSupplier)); + } + } + + /** + * Log an error with info log level. + * @param cause the exception to log + * @param messageSupplier a lazy supplier for the message to log + */ + public void info(Throwable cause, Supplier messageSupplier) { + if (this.log.isInfoEnabled()) { + this.log.info(LogMessage.of(messageSupplier), cause); + } + } + + /** + * Log a message with debug log level. + * @param messageSupplier a lazy supplier for the message to log + */ + public void debug(Supplier messageSupplier) { + if (this.log.isDebugEnabled()) { + this.log.debug(LogMessage.of(messageSupplier)); + } + } + + /** + * Log an error with debug log level. + * @param cause the exception to log + * @param messageSupplier a lazy supplier for the message to log + */ + public void debug(Throwable cause, Supplier messageSupplier) { + if (this.log.isDebugEnabled()) { + this.log.debug(LogMessage.of(messageSupplier), cause); + } + } + + /** + * Log a message with trace log level. + * @param messageSupplier a lazy supplier for the message to log + */ + public void trace(Supplier messageSupplier) { + if (this.log.isTraceEnabled()) { + this.log.trace(LogMessage.of(messageSupplier)); + } + } + + /** + * Log an error with trace log level. + * @param cause the exception to log + * @param messageSupplier a lazy supplier for the message to log + */ + public void trace(Throwable cause, Supplier messageSupplier) { + if (this.log.isTraceEnabled()) { + this.log.trace(LogMessage.of(messageSupplier), cause); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/log/LogDelegateFactory.java b/spring-core/src/main/java/org/springframework/core/log/LogDelegateFactory.java new file mode 100644 index 0000000..6097a7f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/log/LogDelegateFactory.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.log; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Factory for common {@link Log} delegates with Spring's logging conventions. + * + *

    Mainly for internal use within the framework with Apache Commons Logging, + * typically in the form of the {@code spring-jcl} bridge but also compatible + * with other Commons Logging bridges. + * + * @author Rossen Stoyanchev + * @author Juergen Hoeller + * @since 5.1 + * @see org.apache.commons.logging.LogFactory + */ +public final class LogDelegateFactory { + + private LogDelegateFactory() { + } + + + /** + * Create a composite logger that delegates to a primary or falls back on a + * secondary logger if logging for the primary logger is not enabled. + *

    This may be used for fallback logging from lower-level packages that + * logically should log together with some higher-level package but the two + * don't happen to share a suitable parent package (e.g. logging for the web + * and lower-level http and codec packages). For such cases the primary + * (class-based) logger can be wrapped with a shared fallback logger. + * @param primaryLogger primary logger to try first + * @param secondaryLogger secondary logger + * @param tertiaryLoggers optional vararg of further fallback loggers + * @return the resulting composite logger for the related categories + */ + public static Log getCompositeLog(Log primaryLogger, Log secondaryLogger, Log... tertiaryLoggers) { + List loggers = new ArrayList<>(2 + tertiaryLoggers.length); + loggers.add(primaryLogger); + loggers.add(secondaryLogger); + Collections.addAll(loggers, tertiaryLoggers); + return new CompositeLog(loggers); + } + + /** + * Create a "hidden" logger whose name is intentionally prefixed with "_" + * because its output is either too verbose or otherwise deemed as optional + * or unnecessary to see at any log level by default under the normal package + * based log hierarchy. + * @param clazz the class for which to create a logger + * @return a logger for the hidden category ("_" + fully-qualified class name) + */ + public static Log getHiddenLog(Class clazz) { + return LogFactory.getLog("_" + clazz.getName()); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/log/LogFormatUtils.java b/spring-core/src/main/java/org/springframework/core/log/LogFormatUtils.java new file mode 100644 index 0000000..c00c334 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/log/LogFormatUtils.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.log; + +import java.util.function.Function; + +import org.apache.commons.logging.Log; + +import org.springframework.lang.Nullable; + +/** + * Utility methods for formatting and logging messages. + * + *

    Mainly for internal use within the framework with Apache Commons Logging, + * typically in the form of the {@code spring-jcl} bridge but also compatible + * with other Commons Logging bridges. + * + * @author Rossen Stoyanchev + * @author Juergen Hoeller + * @since 5.1 + */ +public abstract class LogFormatUtils { + + /** + * Format the given value via {@code toString()}, quoting it if it is a + * {@link CharSequence}, and possibly truncating at 100 if limitLength is + * set to true. + * @param value the value to format + * @param limitLength whether to truncate large formatted values (over 100) + * @return the formatted value + */ + public static String formatValue(@Nullable Object value, boolean limitLength) { + if (value == null) { + return ""; + } + String str; + if (value instanceof CharSequence) { + str = "\"" + value + "\""; + } + else { + try { + str = value.toString(); + } + catch (Throwable ex) { + str = ex.toString(); + } + } + return (limitLength && str.length() > 100 ? str.substring(0, 100) + " (truncated)..." : str); + } + + /** + * Use this to log a message with different levels of detail (or different + * messages) at TRACE vs DEBUG log levels. Effectively, a substitute for: + *

    +	 * if (logger.isDebugEnabled()) {
    +	 *   String str = logger.isTraceEnabled() ? "..." : "...";
    +	 *   if (logger.isTraceEnabled()) {
    +	 *     logger.trace(str);
    +	 *   }
    +	 *   else {
    +	 *     logger.debug(str);
    +	 *   }
    +	 * }
    +	 * 
    + * @param logger the logger to use to log the message + * @param messageFactory function that accepts a boolean set to the value + * of {@link Log#isTraceEnabled()} + */ + public static void traceDebug(Log logger, Function messageFactory) { + if (logger.isDebugEnabled()) { + boolean traceEnabled = logger.isTraceEnabled(); + String logMessage = messageFactory.apply(traceEnabled); + if (traceEnabled) { + logger.trace(logMessage); + } + else { + logger.debug(logMessage); + } + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/log/LogMessage.java b/spring-core/src/main/java/org/springframework/core/log/LogMessage.java new file mode 100644 index 0000000..ee7dd65 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/log/LogMessage.java @@ -0,0 +1,268 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.log; + +import java.util.function.Supplier; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * A simple log message type for use with Commons Logging, allowing + * for convenient lazy resolution of a given {@link Supplier} instance + * (typically bound to a Java 8 lambda expression) or a printf-style + * format string ({@link String#format}) in its {@link #toString()}. + * + * @author Juergen Hoeller + * @since 5.2 + * @see #of(Supplier) + * @see #format(String, Object) + * @see #format(String, Object...) + * @see org.apache.commons.logging.Log#fatal(Object) + * @see org.apache.commons.logging.Log#error(Object) + * @see org.apache.commons.logging.Log#warn(Object) + * @see org.apache.commons.logging.Log#info(Object) + * @see org.apache.commons.logging.Log#debug(Object) + * @see org.apache.commons.logging.Log#trace(Object) + */ +public abstract class LogMessage implements CharSequence { + + @Nullable + private String result; + + + @Override + public int length() { + return toString().length(); + } + + @Override + public char charAt(int index) { + return toString().charAt(index); + } + + @Override + public CharSequence subSequence(int start, int end) { + return toString().subSequence(start, end); + } + + /** + * This will be called by the logging provider, potentially once + * per log target (therefore locally caching the result here). + */ + @Override + public String toString() { + if (this.result == null) { + this.result = buildString(); + } + return this.result; + } + + abstract String buildString(); + + + /** + * Build a lazily resolving message from the given supplier. + * @param supplier the supplier (typically bound to a Java 8 lambda expression) + * @see #toString() + */ + public static LogMessage of(Supplier supplier) { + return new SupplierMessage(supplier); + } + + /** + * Build a lazily formatted message from the given format string and argument. + * @param format the format string (following {@link String#format} rules) + * @param arg1 the argument + * @see String#format(String, Object...) + */ + public static LogMessage format(String format, Object arg1) { + return new FormatMessage1(format, arg1); + } + + /** + * Build a lazily formatted message from the given format string and arguments. + * @param format the format string (following {@link String#format} rules) + * @param arg1 the first argument + * @param arg2 the second argument + * @see String#format(String, Object...) + */ + public static LogMessage format(String format, Object arg1, Object arg2) { + return new FormatMessage2(format, arg1, arg2); + } + + /** + * Build a lazily formatted message from the given format string and arguments. + * @param format the format string (following {@link String#format} rules) + * @param arg1 the first argument + * @param arg2 the second argument + * @param arg3 the third argument + * @see String#format(String, Object...) + */ + public static LogMessage format(String format, Object arg1, Object arg2, Object arg3) { + return new FormatMessage3(format, arg1, arg2, arg3); + } + + /** + * Build a lazily formatted message from the given format string and arguments. + * @param format the format string (following {@link String#format} rules) + * @param arg1 the first argument + * @param arg2 the second argument + * @param arg3 the third argument + * @param arg4 the fourth argument + * @see String#format(String, Object...) + */ + public static LogMessage format(String format, Object arg1, Object arg2, Object arg3, Object arg4) { + return new FormatMessage4(format, arg1, arg2, arg3, arg4); + } + + /** + * Build a lazily formatted message from the given format string and varargs. + * @param format the format string (following {@link String#format} rules) + * @param args the varargs array (costly, prefer individual arguments) + * @see String#format(String, Object...) + */ + public static LogMessage format(String format, Object... args) { + return new FormatMessageX(format, args); + } + + + private static final class SupplierMessage extends LogMessage { + + private Supplier supplier; + + SupplierMessage(Supplier supplier) { + Assert.notNull(supplier, "Supplier must not be null"); + this.supplier = supplier; + } + + @Override + String buildString() { + return this.supplier.get().toString(); + } + } + + + private static abstract class FormatMessage extends LogMessage { + + protected final String format; + + FormatMessage(String format) { + Assert.notNull(format, "Format must not be null"); + this.format = format; + } + } + + + private static final class FormatMessage1 extends FormatMessage { + + private final Object arg1; + + FormatMessage1(String format, Object arg1) { + super(format); + this.arg1 = arg1; + } + + @Override + protected String buildString() { + return String.format(this.format, this.arg1); + } + } + + + private static final class FormatMessage2 extends FormatMessage { + + private final Object arg1; + + private final Object arg2; + + FormatMessage2(String format, Object arg1, Object arg2) { + super(format); + this.arg1 = arg1; + this.arg2 = arg2; + } + + @Override + String buildString() { + return String.format(this.format, this.arg1, this.arg2); + } + } + + + private static final class FormatMessage3 extends FormatMessage { + + private final Object arg1; + + private final Object arg2; + + private final Object arg3; + + FormatMessage3(String format, Object arg1, Object arg2, Object arg3) { + super(format); + this.arg1 = arg1; + this.arg2 = arg2; + this.arg3 = arg3; + } + + @Override + String buildString() { + return String.format(this.format, this.arg1, this.arg2, this.arg3); + } + } + + + private static final class FormatMessage4 extends FormatMessage { + + private final Object arg1; + + private final Object arg2; + + private final Object arg3; + + private final Object arg4; + + FormatMessage4(String format, Object arg1, Object arg2, Object arg3, Object arg4) { + super(format); + this.arg1 = arg1; + this.arg2 = arg2; + this.arg3 = arg3; + this.arg4 = arg4; + } + + @Override + String buildString() { + return String.format(this.format, this.arg1, this.arg2, this.arg3, this.arg4); + } + } + + + private static final class FormatMessageX extends FormatMessage { + + private final Object[] args; + + FormatMessageX(String format, Object... args) { + super(format); + this.args = args; + } + + @Override + String buildString() { + return String.format(this.format, this.args); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/log/package-info.java b/spring-core/src/main/java/org/springframework/core/log/package-info.java new file mode 100644 index 0000000..b14fac1 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/log/package-info.java @@ -0,0 +1,9 @@ +/** + * Useful delegates for Spring's logging conventions. + */ +@NonNullApi +@NonNullFields +package org.springframework.core.log; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/main/java/org/springframework/core/metrics/ApplicationStartup.java b/spring-core/src/main/java/org/springframework/core/metrics/ApplicationStartup.java new file mode 100644 index 0000000..ed72682 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/metrics/ApplicationStartup.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.metrics; + +/** + * Instruments the application startup phase using {@link StartupStep steps}. + *

    The core container and its infrastructure components can use the {@code ApplicationStartup} + * to mark steps during the application startup and collect data about the execution context + * or their processing time. + * + * @author Brian Clozel + * @since 5.3 + */ +public interface ApplicationStartup { + + /** + * Default "no op" {@code ApplicationStartup} implementation. + *

    This variant is designed for minimal overhead and does not record data. + */ + ApplicationStartup DEFAULT = new DefaultApplicationStartup(); + + /** + * Create a new step and marks its beginning. + *

    A step name describes the current action or phase. This technical + * name should be "." namespaced and can be reused to describe other instances of + * the same step during application startup. + * @param name the step name + */ + StartupStep start(String name); + +} diff --git a/spring-core/src/main/java/org/springframework/core/metrics/DefaultApplicationStartup.java b/spring-core/src/main/java/org/springframework/core/metrics/DefaultApplicationStartup.java new file mode 100644 index 0000000..9b464ad --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/metrics/DefaultApplicationStartup.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.metrics; + +import java.util.Collections; +import java.util.Iterator; +import java.util.function.Supplier; + +/** + * Default "no op" {@code ApplicationStartup} implementation. + * + *

    This variant is designed for minimal overhead and does not record events. + * + * @author Brian Clozel + */ +class DefaultApplicationStartup implements ApplicationStartup { + + @Override + public DefaultStartupStep start(String name) { + return new DefaultStartupStep(); + } + + + static class DefaultStartupStep implements StartupStep { + + boolean recorded = false; + + private final DefaultTags TAGS = new DefaultTags(); + + @Override + public String getName() { + return "default"; + } + + @Override + public long getId() { + return 0L; + } + + @Override + public Long getParentId() { + return null; + } + + @Override + public Tags getTags() { + return this.TAGS; + } + + @Override + public StartupStep tag(String key, String value) { + if (this.recorded) { + throw new IllegalArgumentException(); + } + return this; + } + + @Override + public StartupStep tag(String key, Supplier value) { + if (this.recorded) { + throw new IllegalArgumentException(); + } + return this; + } + + @Override + public void end() { + this.recorded = true; + } + + + static class DefaultTags implements StartupStep.Tags { + + @Override + public Iterator iterator() { + return Collections.emptyIterator(); + } + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/metrics/StartupStep.java b/spring-core/src/main/java/org/springframework/core/metrics/StartupStep.java new file mode 100644 index 0000000..e9061cb --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/metrics/StartupStep.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.metrics; + +import java.util.function.Supplier; + +import org.springframework.lang.Nullable; + +/** + * Step recording metrics about a particular phase or action happening during the {@link ApplicationStartup}. + * + *

    The lifecycle of a {@code StartupStep} goes as follows: + *

      + *
    1. the step is created and starts by calling {@link ApplicationStartup#start(String) the application startup} + * and is assigned a unique {@link StartupStep#getId() id}. + *
    2. we can then attach information with {@link Tags} during processing + *
    3. we then need to mark the {@link #end()} of the step + *
    + * + *

    Implementations can track the "execution time" or other metrics for steps. + * + * @author Brian Clozel + * @since 5.3 + */ +public interface StartupStep { + + /** + * Return the name of the startup step. + *

    A step name describes the current action or phase. This technical + * name should be "." namespaced and can be reused to describe other instances of + * similar steps during application startup. + */ + String getName(); + + /** + * Return the unique id for this step within the application startup. + */ + long getId(); + + /** + * Return, if available, the id of the parent step. + *

    The parent step is the step that was started the most recently + * when the current step was created. + */ + @Nullable + Long getParentId(); + + /** + * Add a {@link Tag} to the step. + * @param key tag key + * @param value tag value + */ + StartupStep tag(String key, String value); + + /** + * Add a {@link Tag} to the step. + * @param key tag key + * @param value {@link Supplier} for the tag value + */ + StartupStep tag(String key, Supplier value); + + /** + * Return the {@link Tag} collection for this step. + */ + Tags getTags(); + + /** + * Record the state of the step and possibly other metrics like execution time. + *

    Once ended, changes on the step state are not allowed. + */ + void end(); + + + /** + * Immutable collection of {@link Tag}. + */ + interface Tags extends Iterable { + } + + + /** + * Simple key/value association for storing step metadata. + */ + interface Tag { + + /** + * Return the {@code Tag} name. + */ + String getKey(); + + /** + * Return the {@code Tag} value. + */ + String getValue(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/metrics/jfr/FlightRecorderApplicationStartup.java b/spring-core/src/main/java/org/springframework/core/metrics/jfr/FlightRecorderApplicationStartup.java new file mode 100644 index 0000000..3959017 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/metrics/jfr/FlightRecorderApplicationStartup.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.metrics.jfr; + +import java.util.ArrayDeque; +import java.util.Deque; + +import org.springframework.core.metrics.ApplicationStartup; +import org.springframework.core.metrics.StartupStep; + +/** + * {@link ApplicationStartup} implementation for the Java Flight Recorder. + *

    This variant records {@link StartupStep} as Flight Recorder events. Because + * such events only support base types, the + * {@link org.springframework.core.metrics.StartupStep.Tags} are serialized as a + * single String attribute. + *

    Once this is configured on the application context, you can record data by + * launching the application with recording enabled: + * {@code java -XX:StartFlightRecording:filename=recording.jfr,duration=10s -jar app.jar}. + * + * @author Brian Clozel + * @since 5.3 + */ +public class FlightRecorderApplicationStartup implements ApplicationStartup { + + private long currentSequenceId; + + private final Deque currentSteps; + + + public FlightRecorderApplicationStartup() { + this.currentSequenceId = 0; + this.currentSteps = new ArrayDeque<>(); + this.currentSteps.offerFirst(0L); + } + + + @Override + public StartupStep start(String name) { + FlightRecorderStartupStep step = new FlightRecorderStartupStep(++this.currentSequenceId, name, + this.currentSteps.getFirst(), committedStep -> this.currentSteps.removeFirst()); + this.currentSteps.offerFirst(this.currentSequenceId); + return step; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/metrics/jfr/FlightRecorderStartupEvent.java b/spring-core/src/main/java/org/springframework/core/metrics/jfr/FlightRecorderStartupEvent.java new file mode 100644 index 0000000..7057daa --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/metrics/jfr/FlightRecorderStartupEvent.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.metrics.jfr; + +import jdk.jfr.Category; +import jdk.jfr.Description; +import jdk.jfr.Event; +import jdk.jfr.Label; + +/** + * {@link Event} extension for recording {@link FlightRecorderStartupStep} + * in Java Flight Recorder. + * + *

    {@link org.springframework.core.metrics.StartupStep.Tags} are serialized + * as a single {@code String}, since Flight Recorder events do not support complex types. + * + * @author Brian Clozel + * @since 5.3 + */ +@Category("Spring Application") +@Label("Startup Step") +@Description("Spring Application Startup") +class FlightRecorderStartupEvent extends Event { + + public final long eventId; + + public final long parentId; + + @Label("Name") + public final String name; + + @Label("Tags") + String tags = ""; + + public FlightRecorderStartupEvent(long eventId, String name, long parentId) { + this.name = name; + this.eventId = eventId; + this.parentId = parentId; + } + + public void setTags(String tags) { + this.tags = tags; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/metrics/jfr/FlightRecorderStartupStep.java b/spring-core/src/main/java/org/springframework/core/metrics/jfr/FlightRecorderStartupStep.java new file mode 100644 index 0000000..f9bfdb7 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/metrics/jfr/FlightRecorderStartupStep.java @@ -0,0 +1,168 @@ +/* + * Copyright 2012-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.metrics.jfr; + +import java.util.Iterator; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import org.jetbrains.annotations.NotNull; + +import org.springframework.core.metrics.StartupStep; + +/** + * {@link StartupStep} implementation for the Java Flight Recorder. + *

    This variant delegates to a {@link FlightRecorderStartupEvent JFR event extension} + * to collect and record data in Java Flight Recorder. + * + * @author Brian Clozel + */ +class FlightRecorderStartupStep implements StartupStep { + + private final FlightRecorderStartupEvent event; + + private final FlightRecorderTags tags = new FlightRecorderTags(); + + private final Consumer recordingCallback; + + + public FlightRecorderStartupStep(long id, String name, long parentId, + Consumer recordingCallback) { + + this.event = new FlightRecorderStartupEvent(id, name, parentId); + this.event.begin(); + this.recordingCallback = recordingCallback; + } + + + @Override + public String getName() { + return this.event.name; + } + + @Override + public long getId() { + return this.event.eventId; + } + + @Override + public Long getParentId() { + return this.event.parentId; + } + + @Override + public StartupStep tag(String key, String value) { + this.tags.add(key, value); + return this; + } + + @Override + public StartupStep tag(String key, Supplier value) { + this.tags.add(key, value.get()); + return this; + } + + @Override + public Tags getTags() { + return this.tags; + } + + @Override + public void end() { + this.event.end(); + if (this.event.shouldCommit()) { + StringBuilder builder = new StringBuilder(); + this.tags.forEach(tag -> + builder.append(tag.getKey()).append('=').append(tag.getValue()).append(',') + ); + this.event.setTags(builder.toString()); + } + this.event.commit(); + this.recordingCallback.accept(this); + } + + protected FlightRecorderStartupEvent getEvent() { + return this.event; + } + + + static class FlightRecorderTags implements Tags { + + private Tag[] tags = new Tag[0]; + + public void add(String key, String value) { + Tag[] newTags = new Tag[this.tags.length + 1]; + System.arraycopy(this.tags, 0, newTags, 0, this.tags.length); + newTags[newTags.length - 1] = new FlightRecorderTag(key, value); + this.tags = newTags; + } + + public void add(String key, Supplier value) { + add(key, value.get()); + } + + @NotNull + @Override + public Iterator iterator() { + return new TagsIterator(); + } + + private class TagsIterator implements Iterator { + + private int idx = 0; + + @Override + public boolean hasNext() { + return this.idx < tags.length; + } + + @Override + public Tag next() { + return tags[this.idx++]; + } + + @Override + public void remove() { + throw new UnsupportedOperationException("tags are append only"); + } + } + } + + + static class FlightRecorderTag implements Tag { + + private final String key; + + private final String value; + + public FlightRecorderTag(String key, String value) { + this.key = key; + this.value = value; + } + + @Override + public String getKey() { + return this.key; + } + + @Override + public String getValue() { + return this.value; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/metrics/jfr/package-info.java b/spring-core/src/main/java/org/springframework/core/metrics/jfr/package-info.java new file mode 100644 index 0000000..54b7c8d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/metrics/jfr/package-info.java @@ -0,0 +1,9 @@ +/** + * Support package for recording startup metrics using Java Flight Recorder. + */ +@NonNullApi +@NonNullFields +package org.springframework.core.metrics.jfr; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/main/java/org/springframework/core/metrics/package-info.java b/spring-core/src/main/java/org/springframework/core/metrics/package-info.java new file mode 100644 index 0000000..8774a4b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/metrics/package-info.java @@ -0,0 +1,9 @@ +/** + * Support package for recording metrics during application startup. + */ +@NonNullApi +@NonNullFields +package org.springframework.core.metrics; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/main/java/org/springframework/core/package-info.java b/spring-core/src/main/java/org/springframework/core/package-info.java new file mode 100644 index 0000000..703f29b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/package-info.java @@ -0,0 +1,10 @@ +/** + * Provides basic classes for exception handling and version detection, + * and other core helpers that are not specific to any part of the framework. + */ +@NonNullApi +@NonNullFields +package org.springframework.core; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/main/java/org/springframework/core/serializer/DefaultDeserializer.java b/spring-core/src/main/java/org/springframework/core/serializer/DefaultDeserializer.java new file mode 100644 index 0000000..3fbe57f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/serializer/DefaultDeserializer.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.serializer; + +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; + +import org.springframework.core.ConfigurableObjectInputStream; +import org.springframework.core.NestedIOException; +import org.springframework.lang.Nullable; + +/** + * A default {@link Deserializer} implementation that reads an input stream + * using Java serialization. + * + * @author Gary Russell + * @author Mark Fisher + * @author Juergen Hoeller + * @since 3.0.5 + * @see ObjectInputStream + */ +public class DefaultDeserializer implements Deserializer { + + @Nullable + private final ClassLoader classLoader; + + + /** + * Create a {@code DefaultDeserializer} with default {@link ObjectInputStream} + * configuration, using the "latest user-defined ClassLoader". + */ + public DefaultDeserializer() { + this.classLoader = null; + } + + /** + * Create a {@code DefaultDeserializer} for using an {@link ObjectInputStream} + * with the given {@code ClassLoader}. + * @since 4.2.1 + * @see ConfigurableObjectInputStream#ConfigurableObjectInputStream(InputStream, ClassLoader) + */ + public DefaultDeserializer(@Nullable ClassLoader classLoader) { + this.classLoader = classLoader; + } + + + /** + * Read from the supplied {@code InputStream} and deserialize the contents + * into an object. + * @see ObjectInputStream#readObject() + */ + @Override + @SuppressWarnings("resource") + public Object deserialize(InputStream inputStream) throws IOException { + ObjectInputStream objectInputStream = new ConfigurableObjectInputStream(inputStream, this.classLoader); + try { + return objectInputStream.readObject(); + } + catch (ClassNotFoundException ex) { + throw new NestedIOException("Failed to deserialize object type", ex); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/serializer/DefaultSerializer.java b/spring-core/src/main/java/org/springframework/core/serializer/DefaultSerializer.java new file mode 100644 index 0000000..923b8a1 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/serializer/DefaultSerializer.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.serializer; + +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.io.Serializable; + +/** + * A {@link Serializer} implementation that writes an object to an output stream + * using Java serialization. + * + * @author Gary Russell + * @author Mark Fisher + * @since 3.0.5 + */ +public class DefaultSerializer implements Serializer { + + /** + * Writes the source object to an output stream using Java serialization. + * The source object must implement {@link Serializable}. + * @see ObjectOutputStream#writeObject(Object) + */ + @Override + public void serialize(Object object, OutputStream outputStream) throws IOException { + if (!(object instanceof Serializable)) { + throw new IllegalArgumentException(getClass().getSimpleName() + " requires a Serializable payload " + + "but received an object of type [" + object.getClass().getName() + "]"); + } + ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream); + objectOutputStream.writeObject(object); + objectOutputStream.flush(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/serializer/Deserializer.java b/spring-core/src/main/java/org/springframework/core/serializer/Deserializer.java new file mode 100644 index 0000000..afa173a --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/serializer/Deserializer.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.serializer; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * A strategy interface for converting from data in an InputStream to an Object. + * + * @author Gary Russell + * @author Mark Fisher + * @author Juergen Hoeller + * @since 3.0.5 + * @param the object type + * @see Serializer + */ +@FunctionalInterface +public interface Deserializer { + + /** + * Read (assemble) an object of type T from the given InputStream. + *

    Note: Implementations should not close the given InputStream + * (or any decorators of that InputStream) but rather leave this up + * to the caller. + * @param inputStream the input stream + * @return the deserialized object + * @throws IOException in case of errors reading from the stream + */ + T deserialize(InputStream inputStream) throws IOException; + + /** + * Read (assemble) an object of type T from the given byte array. + * @param serialized the byte array + * @return the deserialized object + * @throws IOException in case of deserialization failure + * @since 5.2.7 + */ + default T deserializeFromByteArray(byte[] serialized) throws IOException { + return deserialize(new ByteArrayInputStream(serialized)); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/serializer/Serializer.java b/spring-core/src/main/java/org/springframework/core/serializer/Serializer.java new file mode 100644 index 0000000..1436f99 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/serializer/Serializer.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.serializer; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * A strategy interface for streaming an object to an OutputStream. + * + * @author Gary Russell + * @author Mark Fisher + * @author Juergen Hoeller + * @since 3.0.5 + * @param the object type + * @see Deserializer + */ +@FunctionalInterface +public interface Serializer { + + /** + * Write an object of type T to the given OutputStream. + *

    Note: Implementations should not close the given OutputStream + * (or any decorators of that OutputStream) but rather leave this up + * to the caller. + * @param object the object to serialize + * @param outputStream the output stream + * @throws IOException in case of errors writing to the stream + */ + void serialize(T object, OutputStream outputStream) throws IOException; + + /** + * Turn an object of type T into a serialized byte array. + * @param object the object to serialize + * @return the resulting byte array + * @throws IOException in case of serialization failure + * @since 5.2.7 + */ + default byte[] serializeToByteArray(T object) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(1024); + serialize(object, out); + return out.toByteArray(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/serializer/package-info.java b/spring-core/src/main/java/org/springframework/core/serializer/package-info.java new file mode 100644 index 0000000..88f4d3b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/serializer/package-info.java @@ -0,0 +1,11 @@ +/** + * Root package for Spring's serializer interfaces and implementations. + * Provides an abstraction over various serialization techniques. + * Includes exceptions for serialization and deserialization failures. + */ +@NonNullApi +@NonNullFields +package org.springframework.core.serializer; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/main/java/org/springframework/core/serializer/support/DeserializingConverter.java b/spring-core/src/main/java/org/springframework/core/serializer/support/DeserializingConverter.java new file mode 100644 index 0000000..7412907 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/serializer/support/DeserializingConverter.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.serializer.support; + +import java.io.ByteArrayInputStream; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.serializer.DefaultDeserializer; +import org.springframework.core.serializer.Deserializer; +import org.springframework.util.Assert; + +/** + * A {@link Converter} that delegates to a + * {@link org.springframework.core.serializer.Deserializer} + * to convert data in a byte array to an object. + * + * @author Gary Russell + * @author Mark Fisher + * @author Juergen Hoeller + * @since 3.0.5 + */ +public class DeserializingConverter implements Converter { + + private final Deserializer deserializer; + + + /** + * Create a {@code DeserializingConverter} with default {@link java.io.ObjectInputStream} + * configuration, using the "latest user-defined ClassLoader". + * @see DefaultDeserializer#DefaultDeserializer() + */ + public DeserializingConverter() { + this.deserializer = new DefaultDeserializer(); + } + + /** + * Create a {@code DeserializingConverter} for using an {@link java.io.ObjectInputStream} + * with the given {@code ClassLoader}. + * @since 4.2.1 + * @see DefaultDeserializer#DefaultDeserializer(ClassLoader) + */ + public DeserializingConverter(ClassLoader classLoader) { + this.deserializer = new DefaultDeserializer(classLoader); + } + + /** + * Create a {@code DeserializingConverter} that delegates to the provided {@link Deserializer}. + */ + public DeserializingConverter(Deserializer deserializer) { + Assert.notNull(deserializer, "Deserializer must not be null"); + this.deserializer = deserializer; + } + + + @Override + public Object convert(byte[] source) { + ByteArrayInputStream byteStream = new ByteArrayInputStream(source); + try { + return this.deserializer.deserialize(byteStream); + } + catch (Throwable ex) { + throw new SerializationFailedException("Failed to deserialize payload. " + + "Is the byte array a result of corresponding serialization for " + + this.deserializer.getClass().getSimpleName() + "?", ex); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/serializer/support/SerializationDelegate.java b/spring-core/src/main/java/org/springframework/core/serializer/support/SerializationDelegate.java new file mode 100644 index 0000000..05d5dc1 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/serializer/support/SerializationDelegate.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.serializer.support; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.springframework.core.serializer.DefaultDeserializer; +import org.springframework.core.serializer.DefaultSerializer; +import org.springframework.core.serializer.Deserializer; +import org.springframework.core.serializer.Serializer; +import org.springframework.util.Assert; + +/** + * A convenient delegate with pre-arranged configuration state for common + * serialization needs. Implements {@link Serializer} and {@link Deserializer} + * itself, so can also be passed into such more specific callback methods. + * + * @author Juergen Hoeller + * @since 4.3 + */ +public class SerializationDelegate implements Serializer, Deserializer { + + private final Serializer serializer; + + private final Deserializer deserializer; + + + /** + * Create a {@code SerializationDelegate} with a default serializer/deserializer + * for the given {@code ClassLoader}. + * @see DefaultDeserializer + * @see DefaultDeserializer#DefaultDeserializer(ClassLoader) + */ + public SerializationDelegate(ClassLoader classLoader) { + this.serializer = new DefaultSerializer(); + this.deserializer = new DefaultDeserializer(classLoader); + } + + /** + * Create a {@code SerializationDelegate} with the given serializer/deserializer. + * @param serializer the {@link Serializer} to use (never {@code null)} + * @param deserializer the {@link Deserializer} to use (never {@code null)} + */ + public SerializationDelegate(Serializer serializer, Deserializer deserializer) { + Assert.notNull(serializer, "Serializer must not be null"); + Assert.notNull(deserializer, "Deserializer must not be null"); + this.serializer = serializer; + this.deserializer = deserializer; + } + + + @Override + public void serialize(Object object, OutputStream outputStream) throws IOException { + this.serializer.serialize(object, outputStream); + } + + @Override + public Object deserialize(InputStream inputStream) throws IOException { + return this.deserializer.deserialize(inputStream); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/serializer/support/SerializationFailedException.java b/spring-core/src/main/java/org/springframework/core/serializer/support/SerializationFailedException.java new file mode 100644 index 0000000..b6354de --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/serializer/support/SerializationFailedException.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.serializer.support; + +import org.springframework.core.NestedRuntimeException; + +/** + * Wrapper for the native IOException (or similar) when a + * {@link org.springframework.core.serializer.Serializer} or + * {@link org.springframework.core.serializer.Deserializer} failed. + * Thrown by {@link SerializingConverter} and {@link DeserializingConverter}. + * + * @author Gary Russell + * @author Juergen Hoeller + * @since 3.0.5 + */ +@SuppressWarnings("serial") +public class SerializationFailedException extends NestedRuntimeException { + + /** + * Construct a {@code SerializationException} with the specified detail message. + * @param message the detail message + */ + public SerializationFailedException(String message) { + super(message); + } + + /** + * Construct a {@code SerializationException} with the specified detail message + * and nested exception. + * @param message the detail message + * @param cause the nested exception + */ + public SerializationFailedException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/serializer/support/SerializingConverter.java b/spring-core/src/main/java/org/springframework/core/serializer/support/SerializingConverter.java new file mode 100644 index 0000000..1f0fa8f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/serializer/support/SerializingConverter.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.serializer.support; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.serializer.DefaultSerializer; +import org.springframework.core.serializer.Serializer; +import org.springframework.util.Assert; + +/** + * A {@link Converter} that delegates to a + * {@link org.springframework.core.serializer.Serializer} + * to convert an object to a byte array. + * + * @author Gary Russell + * @author Mark Fisher + * @since 3.0.5 + */ +public class SerializingConverter implements Converter { + + private final Serializer serializer; + + + /** + * Create a default {@code SerializingConverter} that uses standard Java serialization. + */ + public SerializingConverter() { + this.serializer = new DefaultSerializer(); + } + + /** + * Create a {@code SerializingConverter} that delegates to the provided {@link Serializer}. + */ + public SerializingConverter(Serializer serializer) { + Assert.notNull(serializer, "Serializer must not be null"); + this.serializer = serializer; + } + + + /** + * Serializes the source object and returns the byte array result. + */ + @Override + public byte[] convert(Object source) { + try { + return this.serializer.serializeToByteArray(source); + } + catch (Throwable ex) { + throw new SerializationFailedException("Failed to serialize object using " + + this.serializer.getClass().getSimpleName(), ex); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/serializer/support/package-info.java b/spring-core/src/main/java/org/springframework/core/serializer/support/package-info.java new file mode 100644 index 0000000..4e46ea8 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/serializer/support/package-info.java @@ -0,0 +1,10 @@ +/** + * Support classes for Spring's serializer abstraction. + * Includes adapters to the Converter SPI. + */ +@NonNullApi +@NonNullFields +package org.springframework.core.serializer.support; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/main/java/org/springframework/core/style/DefaultToStringStyler.java b/spring-core/src/main/java/org/springframework/core/style/DefaultToStringStyler.java new file mode 100644 index 0000000..9eb0d37 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/style/DefaultToStringStyler.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.style; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; + +/** + * Spring's default {@code toString()} styler. + * + *

    This class is used by {@link ToStringCreator} to style {@code toString()} + * output in a consistent manner according to Spring conventions. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 1.2.2 + */ +public class DefaultToStringStyler implements ToStringStyler { + + private final ValueStyler valueStyler; + + + /** + * Create a new DefaultToStringStyler. + * @param valueStyler the ValueStyler to use + */ + public DefaultToStringStyler(ValueStyler valueStyler) { + Assert.notNull(valueStyler, "ValueStyler must not be null"); + this.valueStyler = valueStyler; + } + + /** + * Return the ValueStyler used by this ToStringStyler. + */ + protected final ValueStyler getValueStyler() { + return this.valueStyler; + } + + + @Override + public void styleStart(StringBuilder buffer, Object obj) { + if (!obj.getClass().isArray()) { + buffer.append('[').append(ClassUtils.getShortName(obj.getClass())); + styleIdentityHashCode(buffer, obj); + } + else { + buffer.append('['); + styleIdentityHashCode(buffer, obj); + buffer.append(' '); + styleValue(buffer, obj); + } + } + + private void styleIdentityHashCode(StringBuilder buffer, Object obj) { + buffer.append('@'); + buffer.append(ObjectUtils.getIdentityHexString(obj)); + } + + @Override + public void styleEnd(StringBuilder buffer, Object o) { + buffer.append(']'); + } + + @Override + public void styleField(StringBuilder buffer, String fieldName, @Nullable Object value) { + styleFieldStart(buffer, fieldName); + styleValue(buffer, value); + styleFieldEnd(buffer, fieldName); + } + + protected void styleFieldStart(StringBuilder buffer, String fieldName) { + buffer.append(' ').append(fieldName).append(" = "); + } + + protected void styleFieldEnd(StringBuilder buffer, String fieldName) { + } + + @Override + public void styleValue(StringBuilder buffer, @Nullable Object value) { + buffer.append(this.valueStyler.style(value)); + } + + @Override + public void styleFieldSeparator(StringBuilder buffer) { + buffer.append(','); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/style/DefaultValueStyler.java b/spring-core/src/main/java/org/springframework/core/style/DefaultValueStyler.java new file mode 100644 index 0000000..f56ac82 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/style/DefaultValueStyler.java @@ -0,0 +1,139 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.style; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringJoiner; + +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; + +/** + * Converts objects to String form, generally for debugging purposes, + * using Spring's {@code toString} styling conventions. + * + *

    Uses the reflective visitor pattern underneath the hood to nicely + * encapsulate styling algorithms for each type of styled object. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 1.2.2 + */ +public class DefaultValueStyler implements ValueStyler { + + private static final String EMPTY = "[[empty]]"; + private static final String NULL = "[null]"; + private static final String COLLECTION = "collection"; + private static final String SET = "set"; + private static final String LIST = "list"; + private static final String MAP = "map"; + private static final String EMPTY_MAP = MAP + EMPTY; + private static final String ARRAY = "array"; + + + @Override + public String style(@Nullable Object value) { + if (value == null) { + return NULL; + } + else if (value instanceof String) { + return "\'" + value + "\'"; + } + else if (value instanceof Class) { + return ClassUtils.getShortName((Class) value); + } + else if (value instanceof Method) { + Method method = (Method) value; + return method.getName() + "@" + ClassUtils.getShortName(method.getDeclaringClass()); + } + else if (value instanceof Map) { + return style((Map) value); + } + else if (value instanceof Map.Entry) { + return style((Map.Entry) value); + } + else if (value instanceof Collection) { + return style((Collection) value); + } + else if (value.getClass().isArray()) { + return styleArray(ObjectUtils.toObjectArray(value)); + } + else { + return String.valueOf(value); + } + } + + private String style(Map value) { + if (value.isEmpty()) { + return EMPTY_MAP; + } + + StringJoiner result = new StringJoiner(", ", "[", "]"); + for (Map.Entry entry : value.entrySet()) { + result.add(style(entry)); + } + return MAP + result; + } + + private String style(Map.Entry value) { + return style(value.getKey()) + " -> " + style(value.getValue()); + } + + private String style(Collection value) { + String collectionType = getCollectionTypeString(value); + + if (value.isEmpty()) { + return collectionType + EMPTY; + } + + StringJoiner result = new StringJoiner(", ", "[", "]"); + for (Object o : value) { + result.add(style(o)); + } + return collectionType + result; + } + + private String getCollectionTypeString(Collection value) { + if (value instanceof List) { + return LIST; + } + else if (value instanceof Set) { + return SET; + } + else { + return COLLECTION; + } + } + + private String styleArray(Object[] array) { + if (array.length == 0) { + return ARRAY + '<' + ClassUtils.getShortName(array.getClass().getComponentType()) + '>' + EMPTY; + } + + StringJoiner result = new StringJoiner(", ", "[", "]"); + for (Object o : array) { + result.add(style(o)); + } + return ARRAY + '<' + ClassUtils.getShortName(array.getClass().getComponentType()) + '>' + result; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/style/StylerUtils.java b/spring-core/src/main/java/org/springframework/core/style/StylerUtils.java new file mode 100644 index 0000000..4d58489 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/style/StylerUtils.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.style; + +/** + * Simple utility class to allow for convenient access to value + * styling logic, mainly to support descriptive logging messages. + * + *

    For more sophisticated needs, use the {@link ValueStyler} abstraction + * directly. This class simply uses a shared {@link DefaultValueStyler} + * instance underneath. + * + * @author Keith Donald + * @since 1.2.2 + * @see ValueStyler + * @see DefaultValueStyler + */ +public abstract class StylerUtils { + + /** + * Default ValueStyler instance used by the {@code style} method. + * Also available for the {@link ToStringCreator} class in this package. + */ + static final ValueStyler DEFAULT_VALUE_STYLER = new DefaultValueStyler(); + + /** + * Style the specified value according to default conventions. + * @param value the Object value to style + * @return the styled String + * @see DefaultValueStyler + */ + public static String style(Object value) { + return DEFAULT_VALUE_STYLER.style(value); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/style/ToStringCreator.java b/spring-core/src/main/java/org/springframework/core/style/ToStringCreator.java new file mode 100644 index 0000000..fbb5cc9 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/style/ToStringCreator.java @@ -0,0 +1,190 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.style; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Utility class that builds pretty-printing {@code toString()} methods + * with pluggable styling conventions. By default, ToStringCreator adheres + * to Spring's {@code toString()} styling conventions. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 1.2.2 + */ +public class ToStringCreator { + + /** + * Default ToStringStyler instance used by this ToStringCreator. + */ + private static final ToStringStyler DEFAULT_TO_STRING_STYLER = + new DefaultToStringStyler(StylerUtils.DEFAULT_VALUE_STYLER); + + + private final StringBuilder buffer = new StringBuilder(256); + + private final ToStringStyler styler; + + private final Object object; + + private boolean styledFirstField; + + + /** + * Create a ToStringCreator for the given object. + * @param obj the object to be stringified + */ + public ToStringCreator(Object obj) { + this(obj, (ToStringStyler) null); + } + + /** + * Create a ToStringCreator for the given object, using the provided style. + * @param obj the object to be stringified + * @param styler the ValueStyler encapsulating pretty-print instructions + */ + public ToStringCreator(Object obj, @Nullable ValueStyler styler) { + this(obj, new DefaultToStringStyler(styler != null ? styler : StylerUtils.DEFAULT_VALUE_STYLER)); + } + + /** + * Create a ToStringCreator for the given object, using the provided style. + * @param obj the object to be stringified + * @param styler the ToStringStyler encapsulating pretty-print instructions + */ + public ToStringCreator(Object obj, @Nullable ToStringStyler styler) { + Assert.notNull(obj, "The object to be styled must not be null"); + this.object = obj; + this.styler = (styler != null ? styler : DEFAULT_TO_STRING_STYLER); + this.styler.styleStart(this.buffer, this.object); + } + + + /** + * Append a byte field value. + * @param fieldName the name of the field, usually the member variable name + * @param value the field value + * @return this, to support call-chaining + */ + public ToStringCreator append(String fieldName, byte value) { + return append(fieldName, Byte.valueOf(value)); + } + + /** + * Append a short field value. + * @param fieldName the name of the field, usually the member variable name + * @param value the field value + * @return this, to support call-chaining + */ + public ToStringCreator append(String fieldName, short value) { + return append(fieldName, Short.valueOf(value)); + } + + /** + * Append a integer field value. + * @param fieldName the name of the field, usually the member variable name + * @param value the field value + * @return this, to support call-chaining + */ + public ToStringCreator append(String fieldName, int value) { + return append(fieldName, Integer.valueOf(value)); + } + + /** + * Append a long field value. + * @param fieldName the name of the field, usually the member variable name + * @param value the field value + * @return this, to support call-chaining + */ + public ToStringCreator append(String fieldName, long value) { + return append(fieldName, Long.valueOf(value)); + } + + /** + * Append a float field value. + * @param fieldName the name of the field, usually the member variable name + * @param value the field value + * @return this, to support call-chaining + */ + public ToStringCreator append(String fieldName, float value) { + return append(fieldName, Float.valueOf(value)); + } + + /** + * Append a double field value. + * @param fieldName the name of the field, usually the member variable name + * @param value the field value + * @return this, to support call-chaining + */ + public ToStringCreator append(String fieldName, double value) { + return append(fieldName, Double.valueOf(value)); + } + + /** + * Append a boolean field value. + * @param fieldName the name of the field, usually the member variable name + * @param value the field value + * @return this, to support call-chaining + */ + public ToStringCreator append(String fieldName, boolean value) { + return append(fieldName, Boolean.valueOf(value)); + } + + /** + * Append a field value. + * @param fieldName the name of the field, usually the member variable name + * @param value the field value + * @return this, to support call-chaining + */ + public ToStringCreator append(String fieldName, @Nullable Object value) { + printFieldSeparatorIfNecessary(); + this.styler.styleField(this.buffer, fieldName, value); + return this; + } + + private void printFieldSeparatorIfNecessary() { + if (this.styledFirstField) { + this.styler.styleFieldSeparator(this.buffer); + } + else { + this.styledFirstField = true; + } + } + + /** + * Append the provided value. + * @param value the value to append + * @return this, to support call-chaining. + */ + public ToStringCreator append(Object value) { + this.styler.styleValue(this.buffer, value); + return this; + } + + + /** + * Return the String representation that this ToStringCreator built. + */ + @Override + public String toString() { + this.styler.styleEnd(this.buffer, this.object); + return this.buffer.toString(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/style/ToStringStyler.java b/spring-core/src/main/java/org/springframework/core/style/ToStringStyler.java new file mode 100644 index 0000000..dab5fe9 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/style/ToStringStyler.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.style; + +import org.springframework.lang.Nullable; + +/** + * A strategy interface for pretty-printing {@code toString()} methods. + * Encapsulates the print algorithms; some other object such as a builder + * should provide the workflow. + * + * @author Keith Donald + * @since 1.2.2 + */ +public interface ToStringStyler { + + /** + * Style a {@code toString()}'ed object before its fields are styled. + * @param buffer the buffer to print to + * @param obj the object to style + */ + void styleStart(StringBuilder buffer, Object obj); + + /** + * Style a {@code toString()}'ed object after it's fields are styled. + * @param buffer the buffer to print to + * @param obj the object to style + */ + void styleEnd(StringBuilder buffer, Object obj); + + /** + * Style a field value as a string. + * @param buffer the buffer to print to + * @param fieldName the he name of the field + * @param value the field value + */ + void styleField(StringBuilder buffer, String fieldName, @Nullable Object value); + + /** + * Style the given value. + * @param buffer the buffer to print to + * @param value the field value + */ + void styleValue(StringBuilder buffer, Object value); + + /** + * Style the field separator. + * @param buffer the buffer to print to + */ + void styleFieldSeparator(StringBuilder buffer); + +} diff --git a/spring-core/src/main/java/org/springframework/core/style/ValueStyler.java b/spring-core/src/main/java/org/springframework/core/style/ValueStyler.java new file mode 100644 index 0000000..974a72d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/style/ValueStyler.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2007 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.style; + +import org.springframework.lang.Nullable; + +/** + * Strategy that encapsulates value String styling algorithms + * according to Spring conventions. + * + * @author Keith Donald + * @since 1.2.2 + */ +public interface ValueStyler { + + /** + * Style the given value, returning a String representation. + * @param value the Object value to style + * @return the styled String + */ + String style(@Nullable Object value); + +} diff --git a/spring-core/src/main/java/org/springframework/core/style/package-info.java b/spring-core/src/main/java/org/springframework/core/style/package-info.java new file mode 100644 index 0000000..c3fe9bf --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/style/package-info.java @@ -0,0 +1,9 @@ +/** + * Support for styling values as Strings, with ToStringCreator as central class. + */ +@NonNullApi +@NonNullFields +package org.springframework.core.style; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/main/java/org/springframework/core/task/AsyncListenableTaskExecutor.java b/spring-core/src/main/java/org/springframework/core/task/AsyncListenableTaskExecutor.java new file mode 100644 index 0000000..026da8e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/AsyncListenableTaskExecutor.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.task; + +import java.util.concurrent.Callable; + +import org.springframework.util.concurrent.ListenableFuture; + +/** + * Extension of the {@link AsyncTaskExecutor} interface, adding the capability to submit + * tasks for {@link ListenableFuture ListenableFutures}. + * + * @author Arjen Poutsma + * @since 4.0 + * @see ListenableFuture + */ +public interface AsyncListenableTaskExecutor extends AsyncTaskExecutor { + + /** + * Submit a {@code Runnable} task for execution, receiving a {@code ListenableFuture} + * representing that task. The Future will return a {@code null} result upon completion. + * @param task the {@code Runnable} to execute (never {@code null}) + * @return a {@code ListenableFuture} representing pending completion of the task + * @throws TaskRejectedException if the given task was not accepted + */ + ListenableFuture submitListenable(Runnable task); + + /** + * Submit a {@code Callable} task for execution, receiving a {@code ListenableFuture} + * representing that task. The Future will return the Callable's result upon + * completion. + * @param task the {@code Callable} to execute (never {@code null}) + * @return a {@code ListenableFuture} representing pending completion of the task + * @throws TaskRejectedException if the given task was not accepted + */ + ListenableFuture submitListenable(Callable task); + +} diff --git a/spring-core/src/main/java/org/springframework/core/task/AsyncTaskExecutor.java b/spring-core/src/main/java/org/springframework/core/task/AsyncTaskExecutor.java new file mode 100644 index 0000000..12ddd76 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/AsyncTaskExecutor.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.task; + +import java.util.concurrent.Callable; +import java.util.concurrent.Future; + +/** + * Extended interface for asynchronous {@link TaskExecutor} implementations, + * offering an overloaded {@link #execute(Runnable, long)} variant with a start + * timeout parameter as well support for {@link java.util.concurrent.Callable}. + * + *

    Note: The {@link java.util.concurrent.Executors} class includes a set of + * methods that can convert some other common closure-like objects, for example, + * {@link java.security.PrivilegedAction} to {@link Callable} before executing them. + * + *

    Implementing this interface also indicates that the {@link #execute(Runnable)} + * method will not execute its Runnable in the caller's thread but rather + * asynchronously in some other thread. + * + * @author Juergen Hoeller + * @since 2.0.3 + * @see SimpleAsyncTaskExecutor + * @see org.springframework.scheduling.SchedulingTaskExecutor + * @see java.util.concurrent.Callable + * @see java.util.concurrent.Executors + */ +public interface AsyncTaskExecutor extends TaskExecutor { + + /** Constant that indicates immediate execution. */ + long TIMEOUT_IMMEDIATE = 0; + + /** Constant that indicates no time limit. */ + long TIMEOUT_INDEFINITE = Long.MAX_VALUE; + + + /** + * Execute the given {@code task}. + * @param task the {@code Runnable} to execute (never {@code null}) + * @param startTimeout the time duration (milliseconds) within which the task is + * supposed to start. This is intended as a hint to the executor, allowing for + * preferred handling of immediate tasks. Typical values are {@link #TIMEOUT_IMMEDIATE} + * or {@link #TIMEOUT_INDEFINITE} (the default as used by {@link #execute(Runnable)}). + * @throws TaskTimeoutException in case of the task being rejected because + * of the timeout (i.e. it cannot be started in time) + * @throws TaskRejectedException if the given task was not accepted + */ + void execute(Runnable task, long startTimeout); + + /** + * Submit a Runnable task for execution, receiving a Future representing that task. + * The Future will return a {@code null} result upon completion. + * @param task the {@code Runnable} to execute (never {@code null}) + * @return a Future representing pending completion of the task + * @throws TaskRejectedException if the given task was not accepted + * @since 3.0 + */ + Future submit(Runnable task); + + /** + * Submit a Callable task for execution, receiving a Future representing that task. + * The Future will return the Callable's result upon completion. + * @param task the {@code Callable} to execute (never {@code null}) + * @return a Future representing pending completion of the task + * @throws TaskRejectedException if the given task was not accepted + * @since 3.0 + */ + Future submit(Callable task); + +} diff --git a/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java b/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java new file mode 100644 index 0000000..7d96032 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java @@ -0,0 +1,288 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.task; + +import java.io.Serializable; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.concurrent.ThreadFactory; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ConcurrencyThrottleSupport; +import org.springframework.util.CustomizableThreadCreator; +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.util.concurrent.ListenableFutureTask; + +/** + * {@link TaskExecutor} implementation that fires up a new Thread for each task, + * executing it asynchronously. + * + *

    Supports limiting concurrent threads through the "concurrencyLimit" + * bean property. By default, the number of concurrent threads is unlimited. + * + *

    NOTE: This implementation does not reuse threads! Consider a + * thread-pooling TaskExecutor implementation instead, in particular for + * executing a large number of short-lived tasks. + * + * @author Juergen Hoeller + * @since 2.0 + * @see #setConcurrencyLimit + * @see SyncTaskExecutor + * @see org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor + * @see org.springframework.scheduling.commonj.WorkManagerTaskExecutor + */ +@SuppressWarnings("serial") +public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator + implements AsyncListenableTaskExecutor, Serializable { + + /** + * Permit any number of concurrent invocations: that is, don't throttle concurrency. + * @see ConcurrencyThrottleSupport#UNBOUNDED_CONCURRENCY + */ + public static final int UNBOUNDED_CONCURRENCY = ConcurrencyThrottleSupport.UNBOUNDED_CONCURRENCY; + + /** + * Switch concurrency 'off': that is, don't allow any concurrent invocations. + * @see ConcurrencyThrottleSupport#NO_CONCURRENCY + */ + public static final int NO_CONCURRENCY = ConcurrencyThrottleSupport.NO_CONCURRENCY; + + + /** Internal concurrency throttle used by this executor. */ + private final ConcurrencyThrottleAdapter concurrencyThrottle = new ConcurrencyThrottleAdapter(); + + @Nullable + private ThreadFactory threadFactory; + + @Nullable + private TaskDecorator taskDecorator; + + + /** + * Create a new SimpleAsyncTaskExecutor with default thread name prefix. + */ + public SimpleAsyncTaskExecutor() { + super(); + } + + /** + * Create a new SimpleAsyncTaskExecutor with the given thread name prefix. + * @param threadNamePrefix the prefix to use for the names of newly created threads + */ + public SimpleAsyncTaskExecutor(String threadNamePrefix) { + super(threadNamePrefix); + } + + /** + * Create a new SimpleAsyncTaskExecutor with the given external thread factory. + * @param threadFactory the factory to use for creating new Threads + */ + public SimpleAsyncTaskExecutor(ThreadFactory threadFactory) { + this.threadFactory = threadFactory; + } + + + /** + * Specify an external factory to use for creating new Threads, + * instead of relying on the local properties of this executor. + *

    You may specify an inner ThreadFactory bean or also a ThreadFactory reference + * obtained from JNDI (on a Java EE 6 server) or some other lookup mechanism. + * @see #setThreadNamePrefix + * @see #setThreadPriority + */ + public void setThreadFactory(@Nullable ThreadFactory threadFactory) { + this.threadFactory = threadFactory; + } + + /** + * Return the external factory to use for creating new Threads, if any. + */ + @Nullable + public final ThreadFactory getThreadFactory() { + return this.threadFactory; + } + + /** + * Specify a custom {@link TaskDecorator} to be applied to any {@link Runnable} + * about to be executed. + *

    Note that such a decorator is not necessarily being applied to the + * user-supplied {@code Runnable}/{@code Callable} but rather to the actual + * execution callback (which may be a wrapper around the user-supplied task). + *

    The primary use case is to set some execution context around the task's + * invocation, or to provide some monitoring/statistics for task execution. + *

    NOTE: Exception handling in {@code TaskDecorator} implementations + * is limited to plain {@code Runnable} execution via {@code execute} calls. + * In case of {@code #submit} calls, the exposed {@code Runnable} will be a + * {@code FutureTask} which does not propagate any exceptions; you might + * have to cast it and call {@code Future#get} to evaluate exceptions. + * @since 4.3 + */ + public final void setTaskDecorator(TaskDecorator taskDecorator) { + this.taskDecorator = taskDecorator; + } + + /** + * Set the maximum number of parallel accesses allowed. + * -1 indicates no concurrency limit at all. + *

    In principle, this limit can be changed at runtime, + * although it is generally designed as a config time setting. + * NOTE: Do not switch between -1 and any concrete limit at runtime, + * as this will lead to inconsistent concurrency counts: A limit + * of -1 effectively turns off concurrency counting completely. + * @see #UNBOUNDED_CONCURRENCY + */ + public void setConcurrencyLimit(int concurrencyLimit) { + this.concurrencyThrottle.setConcurrencyLimit(concurrencyLimit); + } + + /** + * Return the maximum number of parallel accesses allowed. + */ + public final int getConcurrencyLimit() { + return this.concurrencyThrottle.getConcurrencyLimit(); + } + + /** + * Return whether this throttle is currently active. + * @return {@code true} if the concurrency limit for this instance is active + * @see #getConcurrencyLimit() + * @see #setConcurrencyLimit + */ + public final boolean isThrottleActive() { + return this.concurrencyThrottle.isThrottleActive(); + } + + + /** + * Executes the given task, within a concurrency throttle + * if configured (through the superclass's settings). + * @see #doExecute(Runnable) + */ + @Override + public void execute(Runnable task) { + execute(task, TIMEOUT_INDEFINITE); + } + + /** + * Executes the given task, within a concurrency throttle + * if configured (through the superclass's settings). + *

    Executes urgent tasks (with 'immediate' timeout) directly, + * bypassing the concurrency throttle (if active). All other + * tasks are subject to throttling. + * @see #TIMEOUT_IMMEDIATE + * @see #doExecute(Runnable) + */ + @Override + public void execute(Runnable task, long startTimeout) { + Assert.notNull(task, "Runnable must not be null"); + Runnable taskToUse = (this.taskDecorator != null ? this.taskDecorator.decorate(task) : task); + if (isThrottleActive() && startTimeout > TIMEOUT_IMMEDIATE) { + this.concurrencyThrottle.beforeAccess(); + doExecute(new ConcurrencyThrottlingRunnable(taskToUse)); + } + else { + doExecute(taskToUse); + } + } + + @Override + public Future submit(Runnable task) { + FutureTask future = new FutureTask<>(task, null); + execute(future, TIMEOUT_INDEFINITE); + return future; + } + + @Override + public Future submit(Callable task) { + FutureTask future = new FutureTask<>(task); + execute(future, TIMEOUT_INDEFINITE); + return future; + } + + @Override + public ListenableFuture submitListenable(Runnable task) { + ListenableFutureTask future = new ListenableFutureTask<>(task, null); + execute(future, TIMEOUT_INDEFINITE); + return future; + } + + @Override + public ListenableFuture submitListenable(Callable task) { + ListenableFutureTask future = new ListenableFutureTask<>(task); + execute(future, TIMEOUT_INDEFINITE); + return future; + } + + /** + * Template method for the actual execution of a task. + *

    The default implementation creates a new Thread and starts it. + * @param task the Runnable to execute + * @see #setThreadFactory + * @see #createThread + * @see java.lang.Thread#start() + */ + protected void doExecute(Runnable task) { + Thread thread = (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task)); + thread.start(); + } + + + /** + * Subclass of the general ConcurrencyThrottleSupport class, + * making {@code beforeAccess()} and {@code afterAccess()} + * visible to the surrounding class. + */ + private static class ConcurrencyThrottleAdapter extends ConcurrencyThrottleSupport { + + @Override + protected void beforeAccess() { + super.beforeAccess(); + } + + @Override + protected void afterAccess() { + super.afterAccess(); + } + } + + + /** + * This Runnable calls {@code afterAccess()} after the + * target Runnable has finished its execution. + */ + private class ConcurrencyThrottlingRunnable implements Runnable { + + private final Runnable target; + + public ConcurrencyThrottlingRunnable(Runnable target) { + this.target = target; + } + + @Override + public void run() { + try { + this.target.run(); + } + finally { + concurrencyThrottle.afterAccess(); + } + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/task/SyncTaskExecutor.java b/spring-core/src/main/java/org/springframework/core/task/SyncTaskExecutor.java new file mode 100644 index 0000000..90fca52 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/SyncTaskExecutor.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.task; + +import java.io.Serializable; + +import org.springframework.util.Assert; + +/** + * {@link TaskExecutor} implementation that executes each task synchronously + * in the calling thread. + * + *

    Mainly intended for testing scenarios. + * + *

    Execution in the calling thread does have the advantage of participating + * in it's thread context, for example the thread context class loader or the + * thread's current transaction association. That said, in many cases, + * asynchronous execution will be preferable: choose an asynchronous + * {@code TaskExecutor} instead for such scenarios. + * + * @author Juergen Hoeller + * @since 2.0 + * @see SimpleAsyncTaskExecutor + */ +@SuppressWarnings("serial") +public class SyncTaskExecutor implements TaskExecutor, Serializable { + + /** + * Executes the given {@code task} synchronously, through direct + * invocation of it's {@link Runnable#run() run()} method. + * @throws IllegalArgumentException if the given {@code task} is {@code null} + */ + @Override + public void execute(Runnable task) { + Assert.notNull(task, "Runnable must not be null"); + task.run(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/task/TaskDecorator.java b/spring-core/src/main/java/org/springframework/core/task/TaskDecorator.java new file mode 100644 index 0000000..753bf2c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/TaskDecorator.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.task; + +/** + * A callback interface for a decorator to be applied to any {@link Runnable} + * about to be executed. + * + *

    Note that such a decorator is not necessarily being applied to the + * user-supplied {@code Runnable}/{@code Callable} but rather to the actual + * execution callback (which may be a wrapper around the user-supplied task). + * + *

    The primary use case is to set some execution context around the task's + * invocation, or to provide some monitoring/statistics for task execution. + * + *

    NOTE: Exception handling in {@code TaskDecorator} implementations + * may be limited. Specifically in case of a {@code Future}-based operation, + * the exposed {@code Runnable} will be a wrapper which does not propagate + * any exceptions from its {@code run} method. + * + * @author Juergen Hoeller + * @since 4.3 + * @see TaskExecutor#execute(Runnable) + * @see SimpleAsyncTaskExecutor#setTaskDecorator + * @see org.springframework.core.task.support.TaskExecutorAdapter#setTaskDecorator + */ +@FunctionalInterface +public interface TaskDecorator { + + /** + * Decorate the given {@code Runnable}, returning a potentially wrapped + * {@code Runnable} for actual execution, internally delegating to the + * original {@link Runnable#run()} implementation. + * @param runnable the original {@code Runnable} + * @return the decorated {@code Runnable} + */ + Runnable decorate(Runnable runnable); + +} diff --git a/spring-core/src/main/java/org/springframework/core/task/TaskExecutor.java b/spring-core/src/main/java/org/springframework/core/task/TaskExecutor.java new file mode 100644 index 0000000..9bd2f6b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/TaskExecutor.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.task; + +import java.util.concurrent.Executor; + +/** + * Simple task executor interface that abstracts the execution + * of a {@link Runnable}. + * + *

    Implementations can use all sorts of different execution strategies, + * such as: synchronous, asynchronous, using a thread pool, and more. + * + *

    Equivalent to JDK 1.5's {@link java.util.concurrent.Executor} + * interface; extending it now in Spring 3.0, so that clients may declare + * a dependency on an Executor and receive any TaskExecutor implementation. + * This interface remains separate from the standard Executor interface + * mainly for backwards compatibility with JDK 1.4 in Spring 2.x. + * + * @author Juergen Hoeller + * @since 2.0 + * @see java.util.concurrent.Executor + */ +@FunctionalInterface +public interface TaskExecutor extends Executor { + + /** + * Execute the given {@code task}. + *

    The call might return immediately if the implementation uses + * an asynchronous execution strategy, or might block in the case + * of synchronous execution. + * @param task the {@code Runnable} to execute (never {@code null}) + * @throws TaskRejectedException if the given task was not accepted + */ + @Override + void execute(Runnable task); + +} diff --git a/spring-core/src/main/java/org/springframework/core/task/TaskRejectedException.java b/spring-core/src/main/java/org/springframework/core/task/TaskRejectedException.java new file mode 100644 index 0000000..f6294c5 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/TaskRejectedException.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.task; + +import java.util.concurrent.RejectedExecutionException; + +/** + * Exception thrown when a {@link TaskExecutor} rejects to accept + * a given task for execution. + * + * @author Juergen Hoeller + * @since 2.0.1 + * @see TaskExecutor#execute(Runnable) + * @see TaskTimeoutException + */ +@SuppressWarnings("serial") +public class TaskRejectedException extends RejectedExecutionException { + + /** + * Create a new {@code TaskRejectedException} + * with the specified detail message and no root cause. + * @param msg the detail message + */ + public TaskRejectedException(String msg) { + super(msg); + } + + /** + * Create a new {@code TaskRejectedException} + * with the specified detail message and the given root cause. + * @param msg the detail message + * @param cause the root cause (usually from using an underlying + * API such as the {@code java.util.concurrent} package) + * @see java.util.concurrent.RejectedExecutionException + */ + public TaskRejectedException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/task/TaskTimeoutException.java b/spring-core/src/main/java/org/springframework/core/task/TaskTimeoutException.java new file mode 100644 index 0000000..3352a62 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/TaskTimeoutException.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.task; + +/** + * Exception thrown when a {@link AsyncTaskExecutor} rejects to accept + * a given task for execution because of the specified timeout. + * + * @author Juergen Hoeller + * @since 2.0.3 + * @see AsyncTaskExecutor#execute(Runnable, long) + * @see TaskRejectedException + */ +@SuppressWarnings("serial") +public class TaskTimeoutException extends TaskRejectedException { + + /** + * Create a new {@code TaskTimeoutException} + * with the specified detail message and no root cause. + * @param msg the detail message + */ + public TaskTimeoutException(String msg) { + super(msg); + } + + /** + * Create a new {@code TaskTimeoutException} + * with the specified detail message and the given root cause. + * @param msg the detail message + * @param cause the root cause (usually from using an underlying + * API such as the {@code java.util.concurrent} package) + * @see java.util.concurrent.RejectedExecutionException + */ + public TaskTimeoutException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/task/package-info.java b/spring-core/src/main/java/org/springframework/core/task/package-info.java new file mode 100644 index 0000000..099867d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/package-info.java @@ -0,0 +1,10 @@ +/** + * This package defines Spring's core TaskExecutor abstraction, + * and provides SyncTaskExecutor and SimpleAsyncTaskExecutor implementations. + */ +@NonNullApi +@NonNullFields +package org.springframework.core.task; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/main/java/org/springframework/core/task/support/ConcurrentExecutorAdapter.java b/spring-core/src/main/java/org/springframework/core/task/support/ConcurrentExecutorAdapter.java new file mode 100644 index 0000000..6897a3c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/support/ConcurrentExecutorAdapter.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.task.support; + +import java.util.concurrent.Executor; + +import org.springframework.core.task.TaskExecutor; +import org.springframework.util.Assert; + +/** + * Adapter that exposes the {@link java.util.concurrent.Executor} interface + * for any Spring {@link org.springframework.core.task.TaskExecutor}. + * + *

    This is less useful as of Spring 3.0, since TaskExecutor itself + * extends the Executor interface. The adapter is only relevant for + * hiding the TaskExecutor nature of a given object now, + * solely exposing the standard Executor interface to a client. + * + * @author Juergen Hoeller + * @since 2.5 + * @see java.util.concurrent.Executor + * @see org.springframework.core.task.TaskExecutor + */ +public class ConcurrentExecutorAdapter implements Executor { + + private final TaskExecutor taskExecutor; + + + /** + * Create a new ConcurrentExecutorAdapter for the given Spring TaskExecutor. + * @param taskExecutor the Spring TaskExecutor to wrap + */ + public ConcurrentExecutorAdapter(TaskExecutor taskExecutor) { + Assert.notNull(taskExecutor, "TaskExecutor must not be null"); + this.taskExecutor = taskExecutor; + } + + + @Override + public void execute(Runnable command) { + this.taskExecutor.execute(command); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/task/support/ExecutorServiceAdapter.java b/spring-core/src/main/java/org/springframework/core/task/support/ExecutorServiceAdapter.java new file mode 100644 index 0000000..3297ff1 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/support/ExecutorServiceAdapter.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.task.support; + +import java.util.List; +import java.util.concurrent.AbstractExecutorService; +import java.util.concurrent.TimeUnit; + +import org.springframework.core.task.TaskExecutor; +import org.springframework.util.Assert; + +/** + * Adapter that takes a Spring {@link org.springframework.core.task.TaskExecutor} + * and exposes a full {@code java.util.concurrent.ExecutorService} for it. + * + *

    This is primarily for adapting to client components that communicate via the + * {@code java.util.concurrent.ExecutorService} API. It can also be used as + * common ground between a local Spring {@code TaskExecutor} backend and a + * JNDI-located {@code ManagedExecutorService} in a Java EE 7 environment. + * + *

    NOTE: This ExecutorService adapter does not support the + * lifecycle methods in the {@code java.util.concurrent.ExecutorService} API + * ("shutdown()" etc), similar to a server-wide {@code ManagedExecutorService} + * in a Java EE 7 environment. The lifecycle is always up to the backend pool, + * with this adapter acting as an access-only proxy for that target pool. + * + * @author Juergen Hoeller + * @since 3.0 + * @see java.util.concurrent.ExecutorService + */ +public class ExecutorServiceAdapter extends AbstractExecutorService { + + private final TaskExecutor taskExecutor; + + + /** + * Create a new ExecutorServiceAdapter, using the given target executor. + * @param taskExecutor the target executor to delegate to + */ + public ExecutorServiceAdapter(TaskExecutor taskExecutor) { + Assert.notNull(taskExecutor, "TaskExecutor must not be null"); + this.taskExecutor = taskExecutor; + } + + + @Override + public void execute(Runnable task) { + this.taskExecutor.execute(task); + } + + @Override + public void shutdown() { + throw new IllegalStateException( + "Manual shutdown not supported - ExecutorServiceAdapter is dependent on an external lifecycle"); + } + + @Override + public List shutdownNow() { + throw new IllegalStateException( + "Manual shutdown not supported - ExecutorServiceAdapter is dependent on an external lifecycle"); + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + throw new IllegalStateException( + "Manual shutdown not supported - ExecutorServiceAdapter is dependent on an external lifecycle"); + } + + @Override + public boolean isShutdown() { + return false; + } + + @Override + public boolean isTerminated() { + return false; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/task/support/TaskExecutorAdapter.java b/spring-core/src/main/java/org/springframework/core/task/support/TaskExecutorAdapter.java new file mode 100644 index 0000000..81da48d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/support/TaskExecutorAdapter.java @@ -0,0 +1,183 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.task.support; + +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.concurrent.RejectedExecutionException; + +import org.springframework.core.task.AsyncListenableTaskExecutor; +import org.springframework.core.task.TaskDecorator; +import org.springframework.core.task.TaskRejectedException; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.util.concurrent.ListenableFutureTask; + +/** + * Adapter that takes a JDK {@code java.util.concurrent.Executor} and + * exposes a Spring {@link org.springframework.core.task.TaskExecutor} for it. + * Also detects an extended {@code java.util.concurrent.ExecutorService}, adapting + * the {@link org.springframework.core.task.AsyncTaskExecutor} interface accordingly. + * + * @author Juergen Hoeller + * @since 3.0 + * @see java.util.concurrent.Executor + * @see java.util.concurrent.ExecutorService + * @see java.util.concurrent.Executors + */ +public class TaskExecutorAdapter implements AsyncListenableTaskExecutor { + + private final Executor concurrentExecutor; + + @Nullable + private TaskDecorator taskDecorator; + + + /** + * Create a new TaskExecutorAdapter, + * using the given JDK concurrent executor. + * @param concurrentExecutor the JDK concurrent executor to delegate to + */ + public TaskExecutorAdapter(Executor concurrentExecutor) { + Assert.notNull(concurrentExecutor, "Executor must not be null"); + this.concurrentExecutor = concurrentExecutor; + } + + + /** + * Specify a custom {@link TaskDecorator} to be applied to any {@link Runnable} + * about to be executed. + *

    Note that such a decorator is not necessarily being applied to the + * user-supplied {@code Runnable}/{@code Callable} but rather to the actual + * execution callback (which may be a wrapper around the user-supplied task). + *

    The primary use case is to set some execution context around the task's + * invocation, or to provide some monitoring/statistics for task execution. + *

    NOTE: Exception handling in {@code TaskDecorator} implementations + * is limited to plain {@code Runnable} execution via {@code execute} calls. + * In case of {@code #submit} calls, the exposed {@code Runnable} will be a + * {@code FutureTask} which does not propagate any exceptions; you might + * have to cast it and call {@code Future#get} to evaluate exceptions. + * @since 4.3 + */ + public final void setTaskDecorator(TaskDecorator taskDecorator) { + this.taskDecorator = taskDecorator; + } + + + /** + * Delegates to the specified JDK concurrent executor. + * @see java.util.concurrent.Executor#execute(Runnable) + */ + @Override + public void execute(Runnable task) { + try { + doExecute(this.concurrentExecutor, this.taskDecorator, task); + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException( + "Executor [" + this.concurrentExecutor + "] did not accept task: " + task, ex); + } + } + + @Override + public void execute(Runnable task, long startTimeout) { + execute(task); + } + + @Override + public Future submit(Runnable task) { + try { + if (this.taskDecorator == null && this.concurrentExecutor instanceof ExecutorService) { + return ((ExecutorService) this.concurrentExecutor).submit(task); + } + else { + FutureTask future = new FutureTask<>(task, null); + doExecute(this.concurrentExecutor, this.taskDecorator, future); + return future; + } + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException( + "Executor [" + this.concurrentExecutor + "] did not accept task: " + task, ex); + } + } + + @Override + public Future submit(Callable task) { + try { + if (this.taskDecorator == null && this.concurrentExecutor instanceof ExecutorService) { + return ((ExecutorService) this.concurrentExecutor).submit(task); + } + else { + FutureTask future = new FutureTask<>(task); + doExecute(this.concurrentExecutor, this.taskDecorator, future); + return future; + } + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException( + "Executor [" + this.concurrentExecutor + "] did not accept task: " + task, ex); + } + } + + @Override + public ListenableFuture submitListenable(Runnable task) { + try { + ListenableFutureTask future = new ListenableFutureTask<>(task, null); + doExecute(this.concurrentExecutor, this.taskDecorator, future); + return future; + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException( + "Executor [" + this.concurrentExecutor + "] did not accept task: " + task, ex); + } + } + + @Override + public ListenableFuture submitListenable(Callable task) { + try { + ListenableFutureTask future = new ListenableFutureTask<>(task); + doExecute(this.concurrentExecutor, this.taskDecorator, future); + return future; + } + catch (RejectedExecutionException ex) { + throw new TaskRejectedException( + "Executor [" + this.concurrentExecutor + "] did not accept task: " + task, ex); + } + } + + + /** + * Actually execute the given {@code Runnable} (which may be a user-supplied task + * or a wrapper around a user-supplied task) with the given executor. + * @param concurrentExecutor the underlying JDK concurrent executor to delegate to + * @param taskDecorator the specified decorator to be applied, if any + * @param runnable the runnable to execute + * @throws RejectedExecutionException if the given runnable cannot be accepted + * @since 4.3 + */ + protected void doExecute(Executor concurrentExecutor, @Nullable TaskDecorator taskDecorator, Runnable runnable) + throws RejectedExecutionException{ + + concurrentExecutor.execute(taskDecorator != null ? taskDecorator.decorate(runnable) : runnable); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/task/support/package-info.java b/spring-core/src/main/java/org/springframework/core/task/support/package-info.java new file mode 100644 index 0000000..e9dfcd9 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/task/support/package-info.java @@ -0,0 +1,10 @@ +/** + * Support classes for Spring's TaskExecutor abstraction. + * Includes an adapter for the standard ExecutorService interface. + */ +@NonNullApi +@NonNullFields +package org.springframework.core.task.support; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/main/java/org/springframework/core/type/AnnotatedTypeMetadata.java b/spring-core/src/main/java/org/springframework/core/type/AnnotatedTypeMetadata.java new file mode 100644 index 0000000..b32e552 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/AnnotatedTypeMetadata.java @@ -0,0 +1,149 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type; + +import java.lang.annotation.Annotation; +import java.util.Map; + +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotation.Adapt; +import org.springframework.core.annotation.MergedAnnotationCollectors; +import org.springframework.core.annotation.MergedAnnotationPredicates; +import org.springframework.core.annotation.MergedAnnotationSelectors; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.lang.Nullable; +import org.springframework.util.MultiValueMap; + +/** + * Defines access to the annotations of a specific type ({@link AnnotationMetadata class} + * or {@link MethodMetadata method}), in a form that does not necessarily require the + * class-loading. + * + * @author Juergen Hoeller + * @author Mark Fisher + * @author Mark Pollack + * @author Chris Beams + * @author Phillip Webb + * @author Sam Brannen + * @since 4.0 + * @see AnnotationMetadata + * @see MethodMetadata + */ +public interface AnnotatedTypeMetadata { + + /** + * Return annotation details based on the direct annotations of the + * underlying element. + * @return merged annotations based on the direct annotations + * @since 5.2 + */ + MergedAnnotations getAnnotations(); + + /** + * Determine whether the underlying element has an annotation or meta-annotation + * of the given type defined. + *

    If this method returns {@code true}, then + * {@link #getAnnotationAttributes} will return a non-null Map. + * @param annotationName the fully qualified class name of the annotation + * type to look for + * @return whether a matching annotation is defined + */ + default boolean isAnnotated(String annotationName) { + return getAnnotations().isPresent(annotationName); + } + + /** + * Retrieve the attributes of the annotation of the given type, if any (i.e. if + * defined on the underlying element, as direct annotation or meta-annotation), + * also taking attribute overrides on composed annotations into account. + * @param annotationName the fully qualified class name of the annotation + * type to look for + * @return a Map of attributes, with the attribute name as key (e.g. "value") + * and the defined attribute value as Map value. This return value will be + * {@code null} if no matching annotation is defined. + */ + @Nullable + default Map getAnnotationAttributes(String annotationName) { + return getAnnotationAttributes(annotationName, false); + } + + /** + * Retrieve the attributes of the annotation of the given type, if any (i.e. if + * defined on the underlying element, as direct annotation or meta-annotation), + * also taking attribute overrides on composed annotations into account. + * @param annotationName the fully qualified class name of the annotation + * type to look for + * @param classValuesAsString whether to convert class references to String + * class names for exposure as values in the returned Map, instead of Class + * references which might potentially have to be loaded first + * @return a Map of attributes, with the attribute name as key (e.g. "value") + * and the defined attribute value as Map value. This return value will be + * {@code null} if no matching annotation is defined. + */ + @Nullable + default Map getAnnotationAttributes(String annotationName, + boolean classValuesAsString) { + + MergedAnnotation annotation = getAnnotations().get(annotationName, + null, MergedAnnotationSelectors.firstDirectlyDeclared()); + if (!annotation.isPresent()) { + return null; + } + return annotation.asAnnotationAttributes(Adapt.values(classValuesAsString, true)); + } + + /** + * Retrieve all attributes of all annotations of the given type, if any (i.e. if + * defined on the underlying element, as direct annotation or meta-annotation). + * Note that this variant does not take attribute overrides into account. + * @param annotationName the fully qualified class name of the annotation + * type to look for + * @return a MultiMap of attributes, with the attribute name as key (e.g. "value") + * and a list of the defined attribute values as Map value. This return value will + * be {@code null} if no matching annotation is defined. + * @see #getAllAnnotationAttributes(String, boolean) + */ + @Nullable + default MultiValueMap getAllAnnotationAttributes(String annotationName) { + return getAllAnnotationAttributes(annotationName, false); + } + + /** + * Retrieve all attributes of all annotations of the given type, if any (i.e. if + * defined on the underlying element, as direct annotation or meta-annotation). + * Note that this variant does not take attribute overrides into account. + * @param annotationName the fully qualified class name of the annotation + * type to look for + * @param classValuesAsString whether to convert class references to String + * @return a MultiMap of attributes, with the attribute name as key (e.g. "value") + * and a list of the defined attribute values as Map value. This return value will + * be {@code null} if no matching annotation is defined. + * @see #getAllAnnotationAttributes(String) + */ + @Nullable + default MultiValueMap getAllAnnotationAttributes( + String annotationName, boolean classValuesAsString) { + + Adapt[] adaptations = Adapt.values(classValuesAsString, true); + return getAnnotations().stream(annotationName) + .filter(MergedAnnotationPredicates.unique(MergedAnnotation::getMetaTypes)) + .map(MergedAnnotation::withNonMergedAttributes) + .collect(MergedAnnotationCollectors.toMultiValueMap(map -> + map.isEmpty() ? null : map, adaptations)); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/AnnotationMetadata.java b/spring-core/src/main/java/org/springframework/core/type/AnnotationMetadata.java new file mode 100644 index 0000000..66592ba --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/AnnotationMetadata.java @@ -0,0 +1,130 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; + +/** + * Interface that defines abstract access to the annotations of a specific + * class, in a form that does not require that class to be loaded yet. + * + * @author Juergen Hoeller + * @author Mark Fisher + * @author Phillip Webb + * @author Sam Brannen + * @since 2.5 + * @see StandardAnnotationMetadata + * @see org.springframework.core.type.classreading.MetadataReader#getAnnotationMetadata() + * @see AnnotatedTypeMetadata + */ +public interface AnnotationMetadata extends ClassMetadata, AnnotatedTypeMetadata { + + /** + * Get the fully qualified class names of all annotation types that + * are present on the underlying class. + * @return the annotation type names + */ + default Set getAnnotationTypes() { + return getAnnotations().stream() + .filter(MergedAnnotation::isDirectlyPresent) + .map(annotation -> annotation.getType().getName()) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + /** + * Get the fully qualified class names of all meta-annotation types that + * are present on the given annotation type on the underlying class. + * @param annotationName the fully qualified class name of the meta-annotation + * type to look for + * @return the meta-annotation type names, or an empty set if none found + */ + default Set getMetaAnnotationTypes(String annotationName) { + MergedAnnotation annotation = getAnnotations().get(annotationName, MergedAnnotation::isDirectlyPresent); + if (!annotation.isPresent()) { + return Collections.emptySet(); + } + return MergedAnnotations.from(annotation.getType(), SearchStrategy.INHERITED_ANNOTATIONS).stream() + .map(mergedAnnotation -> mergedAnnotation.getType().getName()) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + /** + * Determine whether an annotation of the given type is present on + * the underlying class. + * @param annotationName the fully qualified class name of the annotation + * type to look for + * @return {@code true} if a matching annotation is present + */ + default boolean hasAnnotation(String annotationName) { + return getAnnotations().isDirectlyPresent(annotationName); + } + + /** + * Determine whether the underlying class has an annotation that is itself + * annotated with the meta-annotation of the given type. + * @param metaAnnotationName the fully qualified class name of the + * meta-annotation type to look for + * @return {@code true} if a matching meta-annotation is present + */ + default boolean hasMetaAnnotation(String metaAnnotationName) { + return getAnnotations().get(metaAnnotationName, + MergedAnnotation::isMetaPresent).isPresent(); + } + + /** + * Determine whether the underlying class has any methods that are + * annotated (or meta-annotated) with the given annotation type. + * @param annotationName the fully qualified class name of the annotation + * type to look for + */ + default boolean hasAnnotatedMethods(String annotationName) { + return !getAnnotatedMethods(annotationName).isEmpty(); + } + + /** + * Retrieve the method metadata for all methods that are annotated + * (or meta-annotated) with the given annotation type. + *

    For any returned method, {@link MethodMetadata#isAnnotated} will + * return {@code true} for the given annotation type. + * @param annotationName the fully qualified class name of the annotation + * type to look for + * @return a set of {@link MethodMetadata} for methods that have a matching + * annotation. The return value will be an empty set if no methods match + * the annotation type. + */ + Set getAnnotatedMethods(String annotationName); + + + /** + * Factory method to create a new {@link AnnotationMetadata} instance + * for the given class using standard reflection. + * @param type the class to introspect + * @return a new {@link AnnotationMetadata} instance + * @since 5.2 + */ + static AnnotationMetadata introspect(Class type) { + return StandardAnnotationMetadata.from(type); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/ClassMetadata.java b/spring-core/src/main/java/org/springframework/core/type/ClassMetadata.java new file mode 100644 index 0000000..adb6853 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/ClassMetadata.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type; + +import org.springframework.lang.Nullable; + +/** + * Interface that defines abstract metadata of a specific class, + * in a form that does not require that class to be loaded yet. + * + * @author Juergen Hoeller + * @since 2.5 + * @see StandardClassMetadata + * @see org.springframework.core.type.classreading.MetadataReader#getClassMetadata() + * @see AnnotationMetadata + */ +public interface ClassMetadata { + + /** + * Return the name of the underlying class. + */ + String getClassName(); + + /** + * Return whether the underlying class represents an interface. + */ + boolean isInterface(); + + /** + * Return whether the underlying class represents an annotation. + * @since 4.1 + */ + boolean isAnnotation(); + + /** + * Return whether the underlying class is marked as abstract. + */ + boolean isAbstract(); + + /** + * Return whether the underlying class represents a concrete class, + * i.e. neither an interface nor an abstract class. + */ + default boolean isConcrete() { + return !(isInterface() || isAbstract()); + } + + /** + * Return whether the underlying class is marked as 'final'. + */ + boolean isFinal(); + + /** + * Determine whether the underlying class is independent, i.e. whether + * it is a top-level class or a nested class (static inner class) that + * can be constructed independently from an enclosing class. + */ + boolean isIndependent(); + + /** + * Return whether the underlying class is declared within an enclosing + * class (i.e. the underlying class is an inner/nested class or a + * local class within a method). + *

    If this method returns {@code false}, then the underlying + * class is a top-level class. + */ + default boolean hasEnclosingClass() { + return (getEnclosingClassName() != null); + } + + /** + * Return the name of the enclosing class of the underlying class, + * or {@code null} if the underlying class is a top-level class. + */ + @Nullable + String getEnclosingClassName(); + + /** + * Return whether the underlying class has a super class. + */ + default boolean hasSuperClass() { + return (getSuperClassName() != null); + } + + /** + * Return the name of the super class of the underlying class, + * or {@code null} if there is no super class defined. + */ + @Nullable + String getSuperClassName(); + + /** + * Return the names of all interfaces that the underlying class + * implements, or an empty array if there are none. + */ + String[] getInterfaceNames(); + + /** + * Return the names of all classes declared as members of the class represented by + * this ClassMetadata object. This includes public, protected, default (package) + * access, and private classes and interfaces declared by the class, but excludes + * inherited classes and interfaces. An empty array is returned if no member classes + * or interfaces exist. + * @since 3.1 + */ + String[] getMemberClassNames(); + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/MethodMetadata.java b/spring-core/src/main/java/org/springframework/core/type/MethodMetadata.java new file mode 100644 index 0000000..4cba04e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/MethodMetadata.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type; + +/** + * Interface that defines abstract access to the annotations of a specific + * class, in a form that does not require that class to be loaded yet. + * + * @author Juergen Hoeller + * @author Mark Pollack + * @author Chris Beams + * @author Phillip Webb + * @since 3.0 + * @see StandardMethodMetadata + * @see AnnotationMetadata#getAnnotatedMethods + * @see AnnotatedTypeMetadata + */ +public interface MethodMetadata extends AnnotatedTypeMetadata { + + /** + * Return the name of the method. + */ + String getMethodName(); + + /** + * Return the fully-qualified name of the class that declares this method. + */ + String getDeclaringClassName(); + + /** + * Return the fully-qualified name of this method's declared return type. + * @since 4.2 + */ + String getReturnTypeName(); + + /** + * Return whether the underlying method is effectively abstract: + * i.e. marked as abstract on a class or declared as a regular, + * non-default method in an interface. + * @since 4.2 + */ + boolean isAbstract(); + + /** + * Return whether the underlying method is declared as 'static'. + */ + boolean isStatic(); + + /** + * Return whether the underlying method is marked as 'final'. + */ + boolean isFinal(); + + /** + * Return whether the underlying method is overridable, + * i.e. not marked as static, final or private. + */ + boolean isOverridable(); + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/StandardAnnotationMetadata.java b/spring-core/src/main/java/org/springframework/core/type/StandardAnnotationMetadata.java new file mode 100644 index 0000000..2c5347f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/StandardAnnotationMetadata.java @@ -0,0 +1,178 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; +import org.springframework.core.annotation.RepeatableContainers; +import org.springframework.lang.Nullable; +import org.springframework.util.MultiValueMap; +import org.springframework.util.ReflectionUtils; + +/** + * {@link AnnotationMetadata} implementation that uses standard reflection + * to introspect a given {@link Class}. + * + * @author Juergen Hoeller + * @author Mark Fisher + * @author Chris Beams + * @author Phillip Webb + * @author Sam Brannen + * @since 2.5 + */ +public class StandardAnnotationMetadata extends StandardClassMetadata implements AnnotationMetadata { + + private final MergedAnnotations mergedAnnotations; + + private final boolean nestedAnnotationsAsMap; + + @Nullable + private Set annotationTypes; + + + /** + * Create a new {@code StandardAnnotationMetadata} wrapper for the given Class. + * @param introspectedClass the Class to introspect + * @see #StandardAnnotationMetadata(Class, boolean) + * @deprecated since 5.2 in favor of the factory method {@link AnnotationMetadata#introspect(Class)} + */ + @Deprecated + public StandardAnnotationMetadata(Class introspectedClass) { + this(introspectedClass, false); + } + + /** + * Create a new {@link StandardAnnotationMetadata} wrapper for the given Class, + * providing the option to return any nested annotations or annotation arrays in the + * form of {@link org.springframework.core.annotation.AnnotationAttributes} instead + * of actual {@link Annotation} instances. + * @param introspectedClass the Class to introspect + * @param nestedAnnotationsAsMap return nested annotations and annotation arrays as + * {@link org.springframework.core.annotation.AnnotationAttributes} for compatibility + * with ASM-based {@link AnnotationMetadata} implementations + * @since 3.1.1 + * @deprecated since 5.2 in favor of the factory method {@link AnnotationMetadata#introspect(Class)}. + * Use {@link MergedAnnotation#asMap(org.springframework.core.annotation.MergedAnnotation.Adapt...) MergedAnnotation.asMap} + * from {@link #getAnnotations()} rather than {@link #getAnnotationAttributes(String)} + * if {@code nestedAnnotationsAsMap} is {@code false} + */ + @Deprecated + public StandardAnnotationMetadata(Class introspectedClass, boolean nestedAnnotationsAsMap) { + super(introspectedClass); + this.mergedAnnotations = MergedAnnotations.from(introspectedClass, + SearchStrategy.INHERITED_ANNOTATIONS, RepeatableContainers.none()); + this.nestedAnnotationsAsMap = nestedAnnotationsAsMap; + } + + + @Override + public MergedAnnotations getAnnotations() { + return this.mergedAnnotations; + } + + @Override + public Set getAnnotationTypes() { + Set annotationTypes = this.annotationTypes; + if (annotationTypes == null) { + annotationTypes = Collections.unmodifiableSet(AnnotationMetadata.super.getAnnotationTypes()); + this.annotationTypes = annotationTypes; + } + return annotationTypes; + } + + @Override + @Nullable + public Map getAnnotationAttributes(String annotationName, boolean classValuesAsString) { + if (this.nestedAnnotationsAsMap) { + return AnnotationMetadata.super.getAnnotationAttributes(annotationName, classValuesAsString); + } + return AnnotatedElementUtils.getMergedAnnotationAttributes( + getIntrospectedClass(), annotationName, classValuesAsString, false); + } + + @Override + @Nullable + public MultiValueMap getAllAnnotationAttributes(String annotationName, boolean classValuesAsString) { + if (this.nestedAnnotationsAsMap) { + return AnnotationMetadata.super.getAllAnnotationAttributes(annotationName, classValuesAsString); + } + return AnnotatedElementUtils.getAllAnnotationAttributes( + getIntrospectedClass(), annotationName, classValuesAsString, false); + } + + @Override + public boolean hasAnnotatedMethods(String annotationName) { + if (AnnotationUtils.isCandidateClass(getIntrospectedClass(), annotationName)) { + try { + Method[] methods = ReflectionUtils.getDeclaredMethods(getIntrospectedClass()); + for (Method method : methods) { + if (isAnnotatedMethod(method, annotationName)) { + return true; + } + } + } + catch (Throwable ex) { + throw new IllegalStateException("Failed to introspect annotated methods on " + getIntrospectedClass(), ex); + } + } + return false; + } + + @Override + @SuppressWarnings("deprecation") + public Set getAnnotatedMethods(String annotationName) { + Set annotatedMethods = null; + if (AnnotationUtils.isCandidateClass(getIntrospectedClass(), annotationName)) { + try { + Method[] methods = ReflectionUtils.getDeclaredMethods(getIntrospectedClass()); + for (Method method : methods) { + if (isAnnotatedMethod(method, annotationName)) { + if (annotatedMethods == null) { + annotatedMethods = new LinkedHashSet<>(4); + } + annotatedMethods.add(new StandardMethodMetadata(method, this.nestedAnnotationsAsMap)); + } + } + } + catch (Throwable ex) { + throw new IllegalStateException("Failed to introspect annotated methods on " + getIntrospectedClass(), ex); + } + } + return annotatedMethods != null ? annotatedMethods : Collections.emptySet(); + } + + private boolean isAnnotatedMethod(Method method, String annotationName) { + return !method.isBridge() && method.getAnnotations().length > 0 && + AnnotatedElementUtils.isAnnotated(method, annotationName); + } + + + static AnnotationMetadata from(Class introspectedClass) { + return new StandardAnnotationMetadata(introspectedClass, true); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/StandardClassMetadata.java b/spring-core/src/main/java/org/springframework/core/type/StandardClassMetadata.java new file mode 100644 index 0000000..ae9c296 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/StandardClassMetadata.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type; + +import java.lang.reflect.Modifier; +import java.util.LinkedHashSet; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * {@link ClassMetadata} implementation that uses standard reflection + * to introspect a given {@code Class}. + * + * @author Juergen Hoeller + * @since 2.5 + */ +public class StandardClassMetadata implements ClassMetadata { + + private final Class introspectedClass; + + + /** + * Create a new StandardClassMetadata wrapper for the given Class. + * @param introspectedClass the Class to introspect + * @deprecated since 5.2 in favor of {@link StandardAnnotationMetadata} + */ + @Deprecated + public StandardClassMetadata(Class introspectedClass) { + Assert.notNull(introspectedClass, "Class must not be null"); + this.introspectedClass = introspectedClass; + } + + /** + * Return the underlying Class. + */ + public final Class getIntrospectedClass() { + return this.introspectedClass; + } + + + @Override + public String getClassName() { + return this.introspectedClass.getName(); + } + + @Override + public boolean isInterface() { + return this.introspectedClass.isInterface(); + } + + @Override + public boolean isAnnotation() { + return this.introspectedClass.isAnnotation(); + } + + @Override + public boolean isAbstract() { + return Modifier.isAbstract(this.introspectedClass.getModifiers()); + } + + @Override + public boolean isFinal() { + return Modifier.isFinal(this.introspectedClass.getModifiers()); + } + + @Override + public boolean isIndependent() { + return (!hasEnclosingClass() || + (this.introspectedClass.getDeclaringClass() != null && + Modifier.isStatic(this.introspectedClass.getModifiers()))); + } + + @Override + @Nullable + public String getEnclosingClassName() { + Class enclosingClass = this.introspectedClass.getEnclosingClass(); + return (enclosingClass != null ? enclosingClass.getName() : null); + } + + @Override + @Nullable + public String getSuperClassName() { + Class superClass = this.introspectedClass.getSuperclass(); + return (superClass != null ? superClass.getName() : null); + } + + @Override + public String[] getInterfaceNames() { + Class[] ifcs = this.introspectedClass.getInterfaces(); + String[] ifcNames = new String[ifcs.length]; + for (int i = 0; i < ifcs.length; i++) { + ifcNames[i] = ifcs[i].getName(); + } + return ifcNames; + } + + @Override + public String[] getMemberClassNames() { + LinkedHashSet memberClassNames = new LinkedHashSet<>(4); + for (Class nestedClass : this.introspectedClass.getDeclaredClasses()) { + memberClassNames.add(nestedClass.getName()); + } + return StringUtils.toStringArray(memberClassNames); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java b/spring-core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java new file mode 100644 index 0000000..9678dc4 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java @@ -0,0 +1,153 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Map; + +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; +import org.springframework.core.annotation.RepeatableContainers; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.MultiValueMap; + +/** + * {@link MethodMetadata} implementation that uses standard reflection + * to introspect a given {@code Method}. + * + * @author Juergen Hoeller + * @author Mark Pollack + * @author Chris Beams + * @author Phillip Webb + * @since 3.0 + */ +public class StandardMethodMetadata implements MethodMetadata { + + private final Method introspectedMethod; + + private final boolean nestedAnnotationsAsMap; + + private final MergedAnnotations mergedAnnotations; + + + /** + * Create a new StandardMethodMetadata wrapper for the given Method. + * @param introspectedMethod the Method to introspect + * @deprecated since 5.2 in favor of obtaining instances via {@link AnnotationMetadata} + */ + @Deprecated + public StandardMethodMetadata(Method introspectedMethod) { + this(introspectedMethod, false); + } + + /** + * Create a new StandardMethodMetadata wrapper for the given Method, + * providing the option to return any nested annotations or annotation arrays in the + * form of {@link org.springframework.core.annotation.AnnotationAttributes} instead + * of actual {@link java.lang.annotation.Annotation} instances. + * @param introspectedMethod the Method to introspect + * @param nestedAnnotationsAsMap return nested annotations and annotation arrays as + * {@link org.springframework.core.annotation.AnnotationAttributes} for compatibility + * with ASM-based {@link AnnotationMetadata} implementations + * @since 3.1.1 + * @deprecated since 5.2 in favor of obtaining instances via {@link AnnotationMetadata} + */ + @Deprecated + public StandardMethodMetadata(Method introspectedMethod, boolean nestedAnnotationsAsMap) { + Assert.notNull(introspectedMethod, "Method must not be null"); + this.introspectedMethod = introspectedMethod; + this.nestedAnnotationsAsMap = nestedAnnotationsAsMap; + this.mergedAnnotations = MergedAnnotations.from( + introspectedMethod, SearchStrategy.DIRECT, RepeatableContainers.none()); + } + + + @Override + public MergedAnnotations getAnnotations() { + return this.mergedAnnotations; + } + + /** + * Return the underlying Method. + */ + public final Method getIntrospectedMethod() { + return this.introspectedMethod; + } + + @Override + public String getMethodName() { + return this.introspectedMethod.getName(); + } + + @Override + public String getDeclaringClassName() { + return this.introspectedMethod.getDeclaringClass().getName(); + } + + @Override + public String getReturnTypeName() { + return this.introspectedMethod.getReturnType().getName(); + } + + @Override + public boolean isAbstract() { + return Modifier.isAbstract(this.introspectedMethod.getModifiers()); + } + + @Override + public boolean isStatic() { + return Modifier.isStatic(this.introspectedMethod.getModifiers()); + } + + @Override + public boolean isFinal() { + return Modifier.isFinal(this.introspectedMethod.getModifiers()); + } + + @Override + public boolean isOverridable() { + return !isStatic() && !isFinal() && !isPrivate(); + } + + private boolean isPrivate() { + return Modifier.isPrivate(this.introspectedMethod.getModifiers()); + } + + @Override + @Nullable + public Map getAnnotationAttributes(String annotationName, boolean classValuesAsString) { + if (this.nestedAnnotationsAsMap) { + return MethodMetadata.super.getAnnotationAttributes(annotationName, classValuesAsString); + } + return AnnotatedElementUtils.getMergedAnnotationAttributes(this.introspectedMethod, + annotationName, classValuesAsString, false); + } + + @Override + @Nullable + public MultiValueMap getAllAnnotationAttributes(String annotationName, boolean classValuesAsString) { + if (this.nestedAnnotationsAsMap) { + return MethodMetadata.super.getAllAnnotationAttributes(annotationName, classValuesAsString); + } + return AnnotatedElementUtils.getAllAnnotationAttributes(this.introspectedMethod, + annotationName, classValuesAsString, false); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/AbstractRecursiveAnnotationVisitor.java b/spring-core/src/main/java/org/springframework/core/type/classreading/AbstractRecursiveAnnotationVisitor.java new file mode 100644 index 0000000..fa23ceb --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/AbstractRecursiveAnnotationVisitor.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.classreading; + +import java.lang.reflect.Field; +import java.security.AccessControlException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.asm.AnnotationVisitor; +import org.springframework.asm.SpringAsmInfo; +import org.springframework.asm.Type; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * {@link AnnotationVisitor} to recursively visit annotations. + * + * @author Chris Beams + * @author Juergen Hoeller + * @author Phillip Webb + * @author Sam Brannen + * @since 3.1.1 + * @deprecated As of Spring Framework 5.2, this class and related classes in this + * package have been replaced by {@link SimpleAnnotationMetadataReadingVisitor} + * and related classes for internal use within the framework. + */ +@Deprecated +abstract class AbstractRecursiveAnnotationVisitor extends AnnotationVisitor { + + protected final Log logger = LogFactory.getLog(getClass()); + + protected final AnnotationAttributes attributes; + + @Nullable + protected final ClassLoader classLoader; + + + public AbstractRecursiveAnnotationVisitor(@Nullable ClassLoader classLoader, AnnotationAttributes attributes) { + super(SpringAsmInfo.ASM_VERSION); + this.classLoader = classLoader; + this.attributes = attributes; + } + + + @Override + public void visit(String attributeName, Object attributeValue) { + this.attributes.put(attributeName, attributeValue); + } + + @Override + public AnnotationVisitor visitAnnotation(String attributeName, String asmTypeDescriptor) { + String annotationType = Type.getType(asmTypeDescriptor).getClassName(); + AnnotationAttributes nestedAttributes = new AnnotationAttributes(annotationType, this.classLoader); + this.attributes.put(attributeName, nestedAttributes); + return new RecursiveAnnotationAttributesVisitor(annotationType, nestedAttributes, this.classLoader); + } + + @Override + public AnnotationVisitor visitArray(String attributeName) { + return new RecursiveAnnotationArrayVisitor(attributeName, this.attributes, this.classLoader); + } + + @Override + public void visitEnum(String attributeName, String asmTypeDescriptor, String attributeValue) { + Object newValue = getEnumValue(asmTypeDescriptor, attributeValue); + visit(attributeName, newValue); + } + + protected Object getEnumValue(String asmTypeDescriptor, String attributeValue) { + Object valueToUse = attributeValue; + try { + Class enumType = ClassUtils.forName(Type.getType(asmTypeDescriptor).getClassName(), this.classLoader); + Field enumConstant = ReflectionUtils.findField(enumType, attributeValue); + if (enumConstant != null) { + ReflectionUtils.makeAccessible(enumConstant); + valueToUse = enumConstant.get(null); + } + } + catch (ClassNotFoundException | NoClassDefFoundError ex) { + logger.debug("Failed to classload enum type while reading annotation metadata", ex); + } + catch (IllegalAccessException | AccessControlException ex) { + logger.debug("Could not access enum value while reading annotation metadata", ex); + } + return valueToUse; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationAttributesReadingVisitor.java b/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationAttributesReadingVisitor.java new file mode 100644 index 0000000..e9f1a15 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationAttributesReadingVisitor.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.classreading; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Modifier; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.MultiValueMap; +import org.springframework.util.ObjectUtils; + +/** + * ASM visitor which looks for annotations defined on a class or method, + * including meta-annotations. + * + *

    This visitor is fully recursive, taking into account any nested + * annotations or nested annotation arrays. + * + * @author Juergen Hoeller + * @author Chris Beams + * @author Phillip Webb + * @author Sam Brannen + * @since 3.0 + * @deprecated As of Spring Framework 5.2, this class and related classes in this + * package have been replaced by {@link SimpleAnnotationMetadataReadingVisitor} + * and related classes for internal use within the framework. + */ +@Deprecated +final class AnnotationAttributesReadingVisitor extends RecursiveAnnotationAttributesVisitor { + + private final MultiValueMap attributesMap; + + private final Map> metaAnnotationMap; + + + public AnnotationAttributesReadingVisitor(String annotationType, + MultiValueMap attributesMap, Map> metaAnnotationMap, + @Nullable ClassLoader classLoader) { + + super(annotationType, new AnnotationAttributes(annotationType, classLoader), classLoader); + this.attributesMap = attributesMap; + this.metaAnnotationMap = metaAnnotationMap; + } + + + @Override + public void visitEnd() { + super.visitEnd(); + + Class annotationClass = this.attributes.annotationType(); + if (annotationClass != null) { + List attributeList = this.attributesMap.get(this.annotationType); + if (attributeList == null) { + this.attributesMap.add(this.annotationType, this.attributes); + } + else { + attributeList.add(0, this.attributes); + } + if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotationClass.getName())) { + try { + Annotation[] metaAnnotations = annotationClass.getAnnotations(); + if (!ObjectUtils.isEmpty(metaAnnotations)) { + Set visited = new LinkedHashSet<>(); + for (Annotation metaAnnotation : metaAnnotations) { + recursivelyCollectMetaAnnotations(visited, metaAnnotation); + } + if (!visited.isEmpty()) { + Set metaAnnotationTypeNames = new LinkedHashSet<>(visited.size()); + for (Annotation ann : visited) { + metaAnnotationTypeNames.add(ann.annotationType().getName()); + } + this.metaAnnotationMap.put(annotationClass.getName(), metaAnnotationTypeNames); + } + } + } + catch (Throwable ex) { + if (logger.isDebugEnabled()) { + logger.debug("Failed to introspect meta-annotations on " + annotationClass + ": " + ex); + } + } + } + } + } + + private void recursivelyCollectMetaAnnotations(Set visited, Annotation annotation) { + Class annotationType = annotation.annotationType(); + String annotationName = annotationType.getName(); + if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotationName) && visited.add(annotation)) { + try { + // Only do attribute scanning for public annotations; we'd run into + // IllegalAccessExceptions otherwise, and we don't want to mess with + // accessibility in a SecurityManager environment. + if (Modifier.isPublic(annotationType.getModifiers())) { + this.attributesMap.add(annotationName, + AnnotationUtils.getAnnotationAttributes(annotation, false, true)); + } + for (Annotation metaMetaAnnotation : annotationType.getAnnotations()) { + recursivelyCollectMetaAnnotations(visited, metaMetaAnnotation); + } + } + catch (Throwable ex) { + if (logger.isDebugEnabled()) { + logger.debug("Failed to introspect meta-annotations on " + annotation + ": " + ex); + } + } + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitor.java b/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitor.java new file mode 100644 index 0000000..9435af7 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitor.java @@ -0,0 +1,200 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.classreading; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.asm.AnnotationVisitor; +import org.springframework.asm.MethodVisitor; +import org.springframework.asm.Opcodes; +import org.springframework.asm.Type; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.MethodMetadata; +import org.springframework.lang.Nullable; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * ASM class visitor which looks for the class name and implemented types as + * well as for the annotations defined on the class, exposing them through + * the {@link org.springframework.core.type.AnnotationMetadata} interface. + * + * @author Juergen Hoeller + * @author Mark Fisher + * @author Costin Leau + * @author Phillip Webb + * @author Sam Brannen + * @since 2.5 + * @deprecated As of Spring Framework 5.2, this class has been replaced by + * {@link SimpleAnnotationMetadataReadingVisitor} for internal use within the + * framework, but there is no public replacement for + * {@code AnnotationMetadataReadingVisitor}. + */ +@Deprecated +public class AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisitor implements AnnotationMetadata { + + @Nullable + protected final ClassLoader classLoader; + + protected final Set annotationSet = new LinkedHashSet<>(4); + + protected final Map> metaAnnotationMap = new LinkedHashMap<>(4); + + /** + * Declared as a {@link LinkedMultiValueMap} instead of a {@link MultiValueMap} + * to ensure that the hierarchical ordering of the entries is preserved. + * @see AnnotationReadingVisitorUtils#getMergedAnnotationAttributes + */ + protected final LinkedMultiValueMap attributesMap = new LinkedMultiValueMap<>(3); + + protected final Set methodMetadataSet = new LinkedHashSet<>(4); + + + public AnnotationMetadataReadingVisitor(@Nullable ClassLoader classLoader) { + this.classLoader = classLoader; + } + + + @Override + public MergedAnnotations getAnnotations() { + throw new UnsupportedOperationException(); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + // Skip bridge methods - we're only interested in original annotation-defining user methods. + // On JDK 8, we'd otherwise run into double detection of the same annotated method... + if ((access & Opcodes.ACC_BRIDGE) != 0) { + return super.visitMethod(access, name, desc, signature, exceptions); + } + return new MethodMetadataReadingVisitor(name, access, getClassName(), + Type.getReturnType(desc).getClassName(), this.classLoader, this.methodMetadataSet); + } + + @Override + @Nullable + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + if (!visible) { + return null; + } + String className = Type.getType(desc).getClassName(); + if (AnnotationUtils.isInJavaLangAnnotationPackage(className)) { + return null; + } + this.annotationSet.add(className); + return new AnnotationAttributesReadingVisitor( + className, this.attributesMap, this.metaAnnotationMap, this.classLoader); + } + + + @Override + public Set getAnnotationTypes() { + return this.annotationSet; + } + + @Override + public Set getMetaAnnotationTypes(String annotationName) { + Set metaAnnotationTypes = this.metaAnnotationMap.get(annotationName); + return (metaAnnotationTypes != null ? metaAnnotationTypes : Collections.emptySet()); + } + + @Override + public boolean hasMetaAnnotation(String metaAnnotationType) { + if (AnnotationUtils.isInJavaLangAnnotationPackage(metaAnnotationType)) { + return false; + } + Collection> allMetaTypes = this.metaAnnotationMap.values(); + for (Set metaTypes : allMetaTypes) { + if (metaTypes.contains(metaAnnotationType)) { + return true; + } + } + return false; + } + + @Override + public boolean isAnnotated(String annotationName) { + return (!AnnotationUtils.isInJavaLangAnnotationPackage(annotationName) && + this.attributesMap.containsKey(annotationName)); + } + + @Override + public boolean hasAnnotation(String annotationName) { + return getAnnotationTypes().contains(annotationName); + } + + @Override + @Nullable + public AnnotationAttributes getAnnotationAttributes(String annotationName, boolean classValuesAsString) { + AnnotationAttributes raw = AnnotationReadingVisitorUtils.getMergedAnnotationAttributes( + this.attributesMap, this.metaAnnotationMap, annotationName); + if (raw == null) { + return null; + } + return AnnotationReadingVisitorUtils.convertClassValues( + "class '" + getClassName() + "'", this.classLoader, raw, classValuesAsString); + } + + @Override + @Nullable + public MultiValueMap getAllAnnotationAttributes(String annotationName, boolean classValuesAsString) { + MultiValueMap allAttributes = new LinkedMultiValueMap<>(); + List attributes = this.attributesMap.get(annotationName); + if (attributes == null) { + return null; + } + String annotatedElement = "class '" + getClassName() + "'"; + for (AnnotationAttributes raw : attributes) { + for (Map.Entry entry : AnnotationReadingVisitorUtils.convertClassValues( + annotatedElement, this.classLoader, raw, classValuesAsString).entrySet()) { + allAttributes.add(entry.getKey(), entry.getValue()); + } + } + return allAttributes; + } + + @Override + public boolean hasAnnotatedMethods(String annotationName) { + for (MethodMetadata methodMetadata : this.methodMetadataSet) { + if (methodMetadata.isAnnotated(annotationName)) { + return true; + } + } + return false; + } + + @Override + public Set getAnnotatedMethods(String annotationName) { + Set annotatedMethods = new LinkedHashSet<>(4); + for (MethodMetadata methodMetadata : this.methodMetadataSet) { + if (methodMetadata.isAnnotated(annotationName)) { + annotatedMethods.add(methodMetadata); + } + } + return annotatedMethods; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationReadingVisitorUtils.java b/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationReadingVisitorUtils.java new file mode 100644 index 0000000..d7c47ea --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/AnnotationReadingVisitorUtils.java @@ -0,0 +1,174 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.classreading; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.asm.Type; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.ObjectUtils; + +/** + * Internal utility class used when reading annotations via ASM. + * + * @author Juergen Hoeller + * @author Mark Fisher + * @author Costin Leau + * @author Phillip Webb + * @author Sam Brannen + * @since 4.0 + * @deprecated As of Spring Framework 5.2, this class and related classes in this + * package have been replaced by {@link SimpleAnnotationMetadataReadingVisitor} + * and related classes for internal use within the framework. + */ +@Deprecated +abstract class AnnotationReadingVisitorUtils { + + public static AnnotationAttributes convertClassValues(Object annotatedElement, + @Nullable ClassLoader classLoader, AnnotationAttributes original, boolean classValuesAsString) { + + AnnotationAttributes result = new AnnotationAttributes(original); + AnnotationUtils.postProcessAnnotationAttributes(annotatedElement, result, classValuesAsString); + + for (Map.Entry entry : result.entrySet()) { + try { + Object value = entry.getValue(); + if (value instanceof AnnotationAttributes) { + value = convertClassValues( + annotatedElement, classLoader, (AnnotationAttributes) value, classValuesAsString); + } + else if (value instanceof AnnotationAttributes[]) { + AnnotationAttributes[] values = (AnnotationAttributes[]) value; + for (int i = 0; i < values.length; i++) { + values[i] = convertClassValues(annotatedElement, classLoader, values[i], classValuesAsString); + } + value = values; + } + else if (value instanceof Type) { + value = (classValuesAsString ? ((Type) value).getClassName() : + ClassUtils.forName(((Type) value).getClassName(), classLoader)); + } + else if (value instanceof Type[]) { + Type[] array = (Type[]) value; + Object[] convArray = + (classValuesAsString ? new String[array.length] : new Class[array.length]); + for (int i = 0; i < array.length; i++) { + convArray[i] = (classValuesAsString ? array[i].getClassName() : + ClassUtils.forName(array[i].getClassName(), classLoader)); + } + value = convArray; + } + else if (classValuesAsString) { + if (value instanceof Class) { + value = ((Class) value).getName(); + } + else if (value instanceof Class[]) { + Class[] clazzArray = (Class[]) value; + String[] newValue = new String[clazzArray.length]; + for (int i = 0; i < clazzArray.length; i++) { + newValue[i] = clazzArray[i].getName(); + } + value = newValue; + } + } + entry.setValue(value); + } + catch (Throwable ex) { + // Class not found - can't resolve class reference in annotation attribute. + result.put(entry.getKey(), ex); + } + } + + return result; + } + + /** + * Retrieve the merged attributes of the annotation of the given type, + * if any, from the supplied {@code attributesMap}. + *

    Annotation attribute values appearing lower in the annotation + * hierarchy (i.e., closer to the declaring class) will override those + * defined higher in the annotation hierarchy. + * @param attributesMap the map of annotation attribute lists, keyed by + * annotation type name + * @param metaAnnotationMap the map of meta annotation relationships, + * keyed by annotation type name + * @param annotationName the fully qualified class name of the annotation + * type to look for + * @return the merged annotation attributes, or {@code null} if no + * matching annotation is present in the {@code attributesMap} + * @since 4.0.3 + */ + @Nullable + public static AnnotationAttributes getMergedAnnotationAttributes( + LinkedMultiValueMap attributesMap, + Map> metaAnnotationMap, String annotationName) { + + // Get the unmerged list of attributes for the target annotation. + List attributesList = attributesMap.get(annotationName); + if (CollectionUtils.isEmpty(attributesList)) { + return null; + } + + // To start with, we populate the result with a copy of all attribute values + // from the target annotation. A copy is necessary so that we do not + // inadvertently mutate the state of the metadata passed to this method. + AnnotationAttributes result = new AnnotationAttributes(attributesList.get(0)); + + Set overridableAttributeNames = new HashSet<>(result.keySet()); + overridableAttributeNames.remove(AnnotationUtils.VALUE); + + // Since the map is a LinkedMultiValueMap, we depend on the ordering of + // elements in the map and reverse the order of the keys in order to traverse + // "down" the annotation hierarchy. + List annotationTypes = new ArrayList<>(attributesMap.keySet()); + Collections.reverse(annotationTypes); + + // No need to revisit the target annotation type: + annotationTypes.remove(annotationName); + + for (String currentAnnotationType : annotationTypes) { + List currentAttributesList = attributesMap.get(currentAnnotationType); + if (!ObjectUtils.isEmpty(currentAttributesList)) { + Set metaAnns = metaAnnotationMap.get(currentAnnotationType); + if (metaAnns != null && metaAnns.contains(annotationName)) { + AnnotationAttributes currentAttributes = currentAttributesList.get(0); + for (String overridableAttributeName : overridableAttributeNames) { + Object value = currentAttributes.get(overridableAttributeName); + if (value != null) { + // Store the value, potentially overriding a value from an attribute + // of the same name found higher in the annotation hierarchy. + result.put(overridableAttributeName, value); + } + } + } + } + } + + return result; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/CachingMetadataReaderFactory.java b/spring-core/src/main/java/org/springframework/core/type/classreading/CachingMetadataReaderFactory.java new file mode 100644 index 0000000..193f7b3 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/CachingMetadataReaderFactory.java @@ -0,0 +1,183 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.classreading; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentMap; + +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.lang.Nullable; + +/** + * Caching implementation of the {@link MetadataReaderFactory} interface, + * caching a {@link MetadataReader} instance per Spring {@link Resource} handle + * (i.e. per ".class" file). + * + * @author Juergen Hoeller + * @author Costin Leau + * @since 2.5 + */ +public class CachingMetadataReaderFactory extends SimpleMetadataReaderFactory { + + /** Default maximum number of entries for a local MetadataReader cache: 256. */ + public static final int DEFAULT_CACHE_LIMIT = 256; + + /** MetadataReader cache: either local or shared at the ResourceLoader level. */ + @Nullable + private Map metadataReaderCache; + + + /** + * Create a new CachingMetadataReaderFactory for the default class loader, + * using a local resource cache. + */ + public CachingMetadataReaderFactory() { + super(); + setCacheLimit(DEFAULT_CACHE_LIMIT); + } + + /** + * Create a new CachingMetadataReaderFactory for the given {@link ClassLoader}, + * using a local resource cache. + * @param classLoader the ClassLoader to use + */ + public CachingMetadataReaderFactory(@Nullable ClassLoader classLoader) { + super(classLoader); + setCacheLimit(DEFAULT_CACHE_LIMIT); + } + + /** + * Create a new CachingMetadataReaderFactory for the given {@link ResourceLoader}, + * using a shared resource cache if supported or a local resource cache otherwise. + * @param resourceLoader the Spring ResourceLoader to use + * (also determines the ClassLoader to use) + * @see DefaultResourceLoader#getResourceCache + */ + public CachingMetadataReaderFactory(@Nullable ResourceLoader resourceLoader) { + super(resourceLoader); + if (resourceLoader instanceof DefaultResourceLoader) { + this.metadataReaderCache = + ((DefaultResourceLoader) resourceLoader).getResourceCache(MetadataReader.class); + } + else { + setCacheLimit(DEFAULT_CACHE_LIMIT); + } + } + + + /** + * Specify the maximum number of entries for the MetadataReader cache. + *

    Default is 256 for a local cache, whereas a shared cache is + * typically unbounded. This method enforces a local resource cache, + * even if the {@link ResourceLoader} supports a shared resource cache. + */ + public void setCacheLimit(int cacheLimit) { + if (cacheLimit <= 0) { + this.metadataReaderCache = null; + } + else if (this.metadataReaderCache instanceof LocalResourceCache) { + ((LocalResourceCache) this.metadataReaderCache).setCacheLimit(cacheLimit); + } + else { + this.metadataReaderCache = new LocalResourceCache(cacheLimit); + } + } + + /** + * Return the maximum number of entries for the MetadataReader cache. + */ + public int getCacheLimit() { + if (this.metadataReaderCache instanceof LocalResourceCache) { + return ((LocalResourceCache) this.metadataReaderCache).getCacheLimit(); + } + else { + return (this.metadataReaderCache != null ? Integer.MAX_VALUE : 0); + } + } + + + @Override + public MetadataReader getMetadataReader(Resource resource) throws IOException { + if (this.metadataReaderCache instanceof ConcurrentMap) { + // No synchronization necessary... + MetadataReader metadataReader = this.metadataReaderCache.get(resource); + if (metadataReader == null) { + metadataReader = super.getMetadataReader(resource); + this.metadataReaderCache.put(resource, metadataReader); + } + return metadataReader; + } + else if (this.metadataReaderCache != null) { + synchronized (this.metadataReaderCache) { + MetadataReader metadataReader = this.metadataReaderCache.get(resource); + if (metadataReader == null) { + metadataReader = super.getMetadataReader(resource); + this.metadataReaderCache.put(resource, metadataReader); + } + return metadataReader; + } + } + else { + return super.getMetadataReader(resource); + } + } + + /** + * Clear the local MetadataReader cache, if any, removing all cached class metadata. + */ + public void clearCache() { + if (this.metadataReaderCache instanceof LocalResourceCache) { + synchronized (this.metadataReaderCache) { + this.metadataReaderCache.clear(); + } + } + else if (this.metadataReaderCache != null) { + // Shared resource cache -> reset to local cache. + setCacheLimit(DEFAULT_CACHE_LIMIT); + } + } + + + @SuppressWarnings("serial") + private static class LocalResourceCache extends LinkedHashMap { + + private volatile int cacheLimit; + + public LocalResourceCache(int cacheLimit) { + super(cacheLimit, 0.75f, true); + this.cacheLimit = cacheLimit; + } + + public void setCacheLimit(int cacheLimit) { + this.cacheLimit = cacheLimit; + } + + public int getCacheLimit() { + return this.cacheLimit; + } + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > this.cacheLimit; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/ClassMetadataReadingVisitor.java b/spring-core/src/main/java/org/springframework/core/type/classreading/ClassMetadataReadingVisitor.java new file mode 100644 index 0000000..aca2ecd --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/ClassMetadataReadingVisitor.java @@ -0,0 +1,244 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.classreading; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.asm.AnnotationVisitor; +import org.springframework.asm.Attribute; +import org.springframework.asm.ClassVisitor; +import org.springframework.asm.FieldVisitor; +import org.springframework.asm.MethodVisitor; +import org.springframework.asm.Opcodes; +import org.springframework.asm.SpringAsmInfo; +import org.springframework.core.type.ClassMetadata; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * ASM class visitor which looks only for the class name and implemented types, + * exposing them through the {@link org.springframework.core.type.ClassMetadata} + * interface. + * + * @author Rod Johnson + * @author Costin Leau + * @author Mark Fisher + * @author Ramnivas Laddad + * @author Chris Beams + * @since 2.5 + * @deprecated As of Spring Framework 5.2, this class and related classes in this + * package have been replaced by {@link SimpleAnnotationMetadataReadingVisitor} + * and related classes for internal use within the framework. + */ +@Deprecated +class ClassMetadataReadingVisitor extends ClassVisitor implements ClassMetadata { + + private String className = ""; + + private boolean isInterface; + + private boolean isAnnotation; + + private boolean isAbstract; + + private boolean isFinal; + + @Nullable + private String enclosingClassName; + + private boolean independentInnerClass; + + @Nullable + private String superClassName; + + private String[] interfaces = new String[0]; + + private Set memberClassNames = new LinkedHashSet<>(4); + + + public ClassMetadataReadingVisitor() { + super(SpringAsmInfo.ASM_VERSION); + } + + + @Override + public void visit( + int version, int access, String name, String signature, @Nullable String supername, String[] interfaces) { + + this.className = ClassUtils.convertResourcePathToClassName(name); + this.isInterface = ((access & Opcodes.ACC_INTERFACE) != 0); + this.isAnnotation = ((access & Opcodes.ACC_ANNOTATION) != 0); + this.isAbstract = ((access & Opcodes.ACC_ABSTRACT) != 0); + this.isFinal = ((access & Opcodes.ACC_FINAL) != 0); + if (supername != null && !this.isInterface) { + this.superClassName = ClassUtils.convertResourcePathToClassName(supername); + } + this.interfaces = new String[interfaces.length]; + for (int i = 0; i < interfaces.length; i++) { + this.interfaces[i] = ClassUtils.convertResourcePathToClassName(interfaces[i]); + } + } + + @Override + public void visitOuterClass(String owner, String name, String desc) { + this.enclosingClassName = ClassUtils.convertResourcePathToClassName(owner); + } + + @Override + public void visitInnerClass(String name, @Nullable String outerName, String innerName, int access) { + if (outerName != null) { + String fqName = ClassUtils.convertResourcePathToClassName(name); + String fqOuterName = ClassUtils.convertResourcePathToClassName(outerName); + if (this.className.equals(fqName)) { + this.enclosingClassName = fqOuterName; + this.independentInnerClass = ((access & Opcodes.ACC_STATIC) != 0); + } + else if (this.className.equals(fqOuterName)) { + this.memberClassNames.add(fqName); + } + } + } + + @Override + public void visitSource(String source, String debug) { + // no-op + } + + @Override + @Nullable + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + // no-op + return new EmptyAnnotationVisitor(); + } + + @Override + public void visitAttribute(Attribute attr) { + // no-op + } + + @Override + public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { + // no-op + return new EmptyFieldVisitor(); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + // no-op + return new EmptyMethodVisitor(); + } + + @Override + public void visitEnd() { + // no-op + } + + + @Override + public String getClassName() { + return this.className; + } + + @Override + public boolean isInterface() { + return this.isInterface; + } + + @Override + public boolean isAnnotation() { + return this.isAnnotation; + } + + @Override + public boolean isAbstract() { + return this.isAbstract; + } + + @Override + public boolean isFinal() { + return this.isFinal; + } + + @Override + public boolean isIndependent() { + return (this.enclosingClassName == null || this.independentInnerClass); + } + + @Override + public boolean hasEnclosingClass() { + return (this.enclosingClassName != null); + } + + @Override + @Nullable + public String getEnclosingClassName() { + return this.enclosingClassName; + } + + @Override + @Nullable + public String getSuperClassName() { + return this.superClassName; + } + + @Override + public String[] getInterfaceNames() { + return this.interfaces; + } + + @Override + public String[] getMemberClassNames() { + return StringUtils.toStringArray(this.memberClassNames); + } + + + private static class EmptyAnnotationVisitor extends AnnotationVisitor { + + public EmptyAnnotationVisitor() { + super(SpringAsmInfo.ASM_VERSION); + } + + @Override + public AnnotationVisitor visitAnnotation(String name, String desc) { + return this; + } + + @Override + public AnnotationVisitor visitArray(String name) { + return this; + } + } + + + private static class EmptyMethodVisitor extends MethodVisitor { + + public EmptyMethodVisitor() { + super(SpringAsmInfo.ASM_VERSION); + } + } + + + private static class EmptyFieldVisitor extends FieldVisitor { + + public EmptyFieldVisitor() { + super(SpringAsmInfo.ASM_VERSION); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/MergedAnnotationReadingVisitor.java b/spring-core/src/main/java/org/springframework/core/type/classreading/MergedAnnotationReadingVisitor.java new file mode 100644 index 0000000..4a86118 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/MergedAnnotationReadingVisitor.java @@ -0,0 +1,199 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.classreading; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import org.springframework.asm.AnnotationVisitor; +import org.springframework.asm.SpringAsmInfo; +import org.springframework.asm.Type; +import org.springframework.core.annotation.AnnotationFilter; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * {@link AnnotationVisitor} that can be used to construct a + * {@link MergedAnnotation}. + * + * @author Phillip Webb + * @since 5.2 + * @param the annotation type + */ +class MergedAnnotationReadingVisitor extends AnnotationVisitor { + + @Nullable + private final ClassLoader classLoader; + + @Nullable + private final Object source; + + private final Class annotationType; + + private final Consumer> consumer; + + private final Map attributes = new LinkedHashMap<>(4); + + + public MergedAnnotationReadingVisitor(@Nullable ClassLoader classLoader, @Nullable Object source, + Class annotationType, Consumer> consumer) { + + super(SpringAsmInfo.ASM_VERSION); + this.classLoader = classLoader; + this.source = source; + this.annotationType = annotationType; + this.consumer = consumer; + } + + + @Override + public void visit(String name, Object value) { + if (value instanceof Type) { + value = ((Type) value).getClassName(); + } + this.attributes.put(name, value); + } + + @Override + public void visitEnum(String name, String descriptor, String value) { + visitEnum(descriptor, value, enumValue -> this.attributes.put(name, enumValue)); + } + + @Override + @Nullable + public AnnotationVisitor visitAnnotation(String name, String descriptor) { + return visitAnnotation(descriptor, annotation -> this.attributes.put(name, annotation)); + } + + @Override + public AnnotationVisitor visitArray(String name) { + return new ArrayVisitor(value -> this.attributes.put(name, value)); + } + + @Override + public void visitEnd() { + MergedAnnotation annotation = MergedAnnotation.of( + this.classLoader, this.source, this.annotationType, this.attributes); + this.consumer.accept(annotation); + } + + @SuppressWarnings("unchecked") + public > void visitEnum(String descriptor, String value, Consumer consumer) { + String className = Type.getType(descriptor).getClassName(); + Class type = (Class) ClassUtils.resolveClassName(className, this.classLoader); + consumer.accept(Enum.valueOf(type, value)); + } + + @SuppressWarnings("unchecked") + @Nullable + private AnnotationVisitor visitAnnotation( + String descriptor, Consumer> consumer) { + + String className = Type.getType(descriptor).getClassName(); + if (AnnotationFilter.PLAIN.matches(className)) { + return null; + } + Class type = (Class) ClassUtils.resolveClassName(className, this.classLoader); + return new MergedAnnotationReadingVisitor<>(this.classLoader, this.source, type, consumer); + } + + @SuppressWarnings("unchecked") + @Nullable + static AnnotationVisitor get(@Nullable ClassLoader classLoader, + @Nullable Supplier sourceSupplier, String descriptor, boolean visible, + Consumer> consumer) { + + if (!visible) { + return null; + } + + String typeName = Type.getType(descriptor).getClassName(); + if (AnnotationFilter.PLAIN.matches(typeName)) { + return null; + } + + Object source = (sourceSupplier != null ? sourceSupplier.get() : null); + try { + Class annotationType = (Class) ClassUtils.forName(typeName, classLoader); + return new MergedAnnotationReadingVisitor<>(classLoader, source, annotationType, consumer); + } + catch (ClassNotFoundException | LinkageError ex) { + return null; + } + } + + + /** + * {@link AnnotationVisitor} to deal with array attributes. + */ + private class ArrayVisitor extends AnnotationVisitor { + + private final List elements = new ArrayList<>(); + + private final Consumer consumer; + + ArrayVisitor(Consumer consumer) { + super(SpringAsmInfo.ASM_VERSION); + this.consumer = consumer; + } + + @Override + public void visit(String name, Object value) { + if (value instanceof Type) { + value = ((Type) value).getClassName(); + } + this.elements.add(value); + } + + @Override + public void visitEnum(String name, String descriptor, String value) { + MergedAnnotationReadingVisitor.this.visitEnum(descriptor, value, this.elements::add); + } + + @Override + @Nullable + public AnnotationVisitor visitAnnotation(String name, String descriptor) { + return MergedAnnotationReadingVisitor.this.visitAnnotation(descriptor, this.elements::add); + } + + @Override + public void visitEnd() { + Class componentType = getComponentType(); + Object[] array = (Object[]) Array.newInstance(componentType, this.elements.size()); + this.consumer.accept(this.elements.toArray(array)); + } + + private Class getComponentType() { + if (this.elements.isEmpty()) { + return Object.class; + } + Object firstElement = this.elements.get(0); + if (firstElement instanceof Enum) { + return ((Enum) firstElement).getDeclaringClass(); + } + return firstElement.getClass(); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReader.java b/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReader.java new file mode 100644 index 0000000..3b2beb7 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReader.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2009 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.classreading; + +import org.springframework.core.io.Resource; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.ClassMetadata; + +/** + * Simple facade for accessing class metadata, + * as read by an ASM {@link org.springframework.asm.ClassReader}. + * + * @author Juergen Hoeller + * @since 2.5 + */ +public interface MetadataReader { + + /** + * Return the resource reference for the class file. + */ + Resource getResource(); + + /** + * Read basic class metadata for the underlying class. + */ + ClassMetadata getClassMetadata(); + + /** + * Read full annotation metadata for the underlying class, + * including metadata for annotated methods. + */ + AnnotationMetadata getAnnotationMetadata(); + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReaderFactory.java b/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReaderFactory.java new file mode 100644 index 0000000..555b3ac --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReaderFactory.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.classreading; + +import java.io.IOException; + +import org.springframework.core.io.Resource; + +/** + * Factory interface for {@link MetadataReader} instances. + * Allows for caching a MetadataReader per original resource. + * + * @author Juergen Hoeller + * @since 2.5 + * @see SimpleMetadataReaderFactory + * @see CachingMetadataReaderFactory + */ +public interface MetadataReaderFactory { + + /** + * Obtain a MetadataReader for the given class name. + * @param className the class name (to be resolved to a ".class" file) + * @return a holder for the ClassReader instance (never {@code null}) + * @throws IOException in case of I/O failure + */ + MetadataReader getMetadataReader(String className) throws IOException; + + /** + * Obtain a MetadataReader for the given resource. + * @param resource the resource (pointing to a ".class" file) + * @return a holder for the ClassReader instance (never {@code null}) + * @throws IOException in case of I/O failure + */ + MetadataReader getMetadataReader(Resource resource) throws IOException; + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitor.java b/spring-core/src/main/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitor.java new file mode 100644 index 0000000..f352428 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitor.java @@ -0,0 +1,174 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.classreading; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.asm.AnnotationVisitor; +import org.springframework.asm.MethodVisitor; +import org.springframework.asm.Opcodes; +import org.springframework.asm.SpringAsmInfo; +import org.springframework.asm.Type; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.type.MethodMetadata; +import org.springframework.lang.Nullable; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * ASM method visitor which looks for the annotations defined on a method, + * exposing them through the {@link org.springframework.core.type.MethodMetadata} + * interface. + * + * @author Juergen Hoeller + * @author Mark Pollack + * @author Costin Leau + * @author Chris Beams + * @author Phillip Webb + * @since 3.0 + * @deprecated As of Spring Framework 5.2, this class and related classes in this + * package have been replaced by {@link SimpleAnnotationMetadataReadingVisitor} + * and related classes for internal use within the framework. + */ +@Deprecated +public class MethodMetadataReadingVisitor extends MethodVisitor implements MethodMetadata { + + protected final String methodName; + + protected final int access; + + protected final String declaringClassName; + + protected final String returnTypeName; + + @Nullable + protected final ClassLoader classLoader; + + protected final Set methodMetadataSet; + + protected final Map> metaAnnotationMap = new LinkedHashMap<>(4); + + protected final LinkedMultiValueMap attributesMap = new LinkedMultiValueMap<>(3); + + + public MethodMetadataReadingVisitor(String methodName, int access, String declaringClassName, + String returnTypeName, @Nullable ClassLoader classLoader, Set methodMetadataSet) { + + super(SpringAsmInfo.ASM_VERSION); + this.methodName = methodName; + this.access = access; + this.declaringClassName = declaringClassName; + this.returnTypeName = returnTypeName; + this.classLoader = classLoader; + this.methodMetadataSet = methodMetadataSet; + } + + + @Override + public MergedAnnotations getAnnotations() { + throw new UnsupportedOperationException(); + } + + @Override + @Nullable + public AnnotationVisitor visitAnnotation(final String desc, boolean visible) { + if (!visible) { + return null; + } + this.methodMetadataSet.add(this); + String className = Type.getType(desc).getClassName(); + return new AnnotationAttributesReadingVisitor( + className, this.attributesMap, this.metaAnnotationMap, this.classLoader); + } + + + @Override + public String getMethodName() { + return this.methodName; + } + + @Override + public boolean isAbstract() { + return ((this.access & Opcodes.ACC_ABSTRACT) != 0); + } + + @Override + public boolean isStatic() { + return ((this.access & Opcodes.ACC_STATIC) != 0); + } + + @Override + public boolean isFinal() { + return ((this.access & Opcodes.ACC_FINAL) != 0); + } + + @Override + public boolean isOverridable() { + return (!isStatic() && !isFinal() && ((this.access & Opcodes.ACC_PRIVATE) == 0)); + } + + @Override + public boolean isAnnotated(String annotationName) { + return this.attributesMap.containsKey(annotationName); + } + + @Override + @Nullable + public AnnotationAttributes getAnnotationAttributes(String annotationName, boolean classValuesAsString) { + AnnotationAttributes raw = AnnotationReadingVisitorUtils.getMergedAnnotationAttributes( + this.attributesMap, this.metaAnnotationMap, annotationName); + if (raw == null) { + return null; + } + return AnnotationReadingVisitorUtils.convertClassValues( + "method '" + getMethodName() + "'", this.classLoader, raw, classValuesAsString); + } + + @Override + @Nullable + public MultiValueMap getAllAnnotationAttributes(String annotationName, boolean classValuesAsString) { + if (!this.attributesMap.containsKey(annotationName)) { + return null; + } + MultiValueMap allAttributes = new LinkedMultiValueMap<>(); + List attributesList = this.attributesMap.get(annotationName); + if (attributesList != null) { + String annotatedElement = "method '" + getMethodName() + '\''; + for (AnnotationAttributes annotationAttributes : attributesList) { + AnnotationAttributes convertedAttributes = AnnotationReadingVisitorUtils.convertClassValues( + annotatedElement, this.classLoader, annotationAttributes, classValuesAsString); + convertedAttributes.forEach(allAttributes::add); + } + } + return allAttributes; + } + + @Override + public String getDeclaringClassName() { + return this.declaringClassName; + } + + @Override + public String getReturnTypeName() { + return this.returnTypeName; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/RecursiveAnnotationArrayVisitor.java b/spring-core/src/main/java/org/springframework/core/type/classreading/RecursiveAnnotationArrayVisitor.java new file mode 100644 index 0000000..68bc025 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/RecursiveAnnotationArrayVisitor.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.classreading; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.asm.AnnotationVisitor; +import org.springframework.asm.Type; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +/** + * {@link AnnotationVisitor} to recursively visit annotation arrays. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1.1 + * @deprecated As of Spring Framework 5.2, this class and related classes in this + * package have been replaced by {@link SimpleAnnotationMetadataReadingVisitor} + * and related classes for internal use within the framework. + */ +@Deprecated +class RecursiveAnnotationArrayVisitor extends AbstractRecursiveAnnotationVisitor { + + private final String attributeName; + + private final List allNestedAttributes = new ArrayList<>(); + + + public RecursiveAnnotationArrayVisitor( + String attributeName, AnnotationAttributes attributes, @Nullable ClassLoader classLoader) { + + super(classLoader, attributes); + this.attributeName = attributeName; + } + + + @Override + public void visit(String attributeName, Object attributeValue) { + Object newValue = attributeValue; + Object existingValue = this.attributes.get(this.attributeName); + if (existingValue != null) { + newValue = ObjectUtils.addObjectToArray((Object[]) existingValue, newValue); + } + else { + Class arrayClass = newValue.getClass(); + if (Enum.class.isAssignableFrom(arrayClass)) { + while (arrayClass.getSuperclass() != null && !arrayClass.isEnum()) { + arrayClass = arrayClass.getSuperclass(); + } + } + Object[] newArray = (Object[]) Array.newInstance(arrayClass, 1); + newArray[0] = newValue; + newValue = newArray; + } + this.attributes.put(this.attributeName, newValue); + } + + @Override + public AnnotationVisitor visitAnnotation(String attributeName, String asmTypeDescriptor) { + String annotationType = Type.getType(asmTypeDescriptor).getClassName(); + AnnotationAttributes nestedAttributes = new AnnotationAttributes(annotationType, this.classLoader); + this.allNestedAttributes.add(nestedAttributes); + return new RecursiveAnnotationAttributesVisitor(annotationType, nestedAttributes, this.classLoader); + } + + @Override + public void visitEnd() { + if (!this.allNestedAttributes.isEmpty()) { + this.attributes.put(this.attributeName, this.allNestedAttributes.toArray(new AnnotationAttributes[0])); + } + else if (!this.attributes.containsKey(this.attributeName)) { + Class annotationType = this.attributes.annotationType(); + if (annotationType != null) { + try { + Class attributeType = annotationType.getMethod(this.attributeName).getReturnType(); + if (attributeType.isArray()) { + Class elementType = attributeType.getComponentType(); + if (elementType.isAnnotation()) { + elementType = AnnotationAttributes.class; + } + this.attributes.put(this.attributeName, Array.newInstance(elementType, 0)); + } + } + catch (NoSuchMethodException ex) { + // Corresponding attribute method not found: cannot expose empty array. + } + } + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/RecursiveAnnotationAttributesVisitor.java b/spring-core/src/main/java/org/springframework/core/type/classreading/RecursiveAnnotationAttributesVisitor.java new file mode 100644 index 0000000..b5953c8 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/RecursiveAnnotationAttributesVisitor.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.classreading; + +import org.springframework.asm.AnnotationVisitor; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.lang.Nullable; + +/** + * {@link AnnotationVisitor} to recursively visit annotation attributes. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1.1 + * @deprecated As of Spring Framework 5.2, this class and related classes in this + * package have been replaced by {@link SimpleAnnotationMetadataReadingVisitor} + * and related classes for internal use within the framework. + */ +@Deprecated +class RecursiveAnnotationAttributesVisitor extends AbstractRecursiveAnnotationVisitor { + + protected final String annotationType; + + + public RecursiveAnnotationAttributesVisitor( + String annotationType, AnnotationAttributes attributes, @Nullable ClassLoader classLoader) { + + super(classLoader, attributes); + this.annotationType = annotationType; + } + + + @Override + public void visitEnd() { + AnnotationUtils.registerDefaultValues(this.attributes); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleAnnotationMetadata.java b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleAnnotationMetadata.java new file mode 100644 index 0000000..1f4c0ba --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleAnnotationMetadata.java @@ -0,0 +1,159 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.classreading; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.asm.Opcodes; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.MethodMetadata; +import org.springframework.lang.Nullable; + +/** + * {@link AnnotationMetadata} created from a + * {@link SimpleAnnotationMetadataReadingVisitor}. + * + * @author Phillip Webb + * @since 5.2 + */ +final class SimpleAnnotationMetadata implements AnnotationMetadata { + + private final String className; + + private final int access; + + @Nullable + private final String enclosingClassName; + + @Nullable + private final String superClassName; + + private final boolean independentInnerClass; + + private final String[] interfaceNames; + + private final String[] memberClassNames; + + private final MethodMetadata[] annotatedMethods; + + private final MergedAnnotations annotations; + + @Nullable + private Set annotationTypes; + + + SimpleAnnotationMetadata(String className, int access, @Nullable String enclosingClassName, + @Nullable String superClassName, boolean independentInnerClass, String[] interfaceNames, + String[] memberClassNames, MethodMetadata[] annotatedMethods, MergedAnnotations annotations) { + + this.className = className; + this.access = access; + this.enclosingClassName = enclosingClassName; + this.superClassName = superClassName; + this.independentInnerClass = independentInnerClass; + this.interfaceNames = interfaceNames; + this.memberClassNames = memberClassNames; + this.annotatedMethods = annotatedMethods; + this.annotations = annotations; + } + + @Override + public String getClassName() { + return this.className; + } + + @Override + public boolean isInterface() { + return (this.access & Opcodes.ACC_INTERFACE) != 0; + } + + @Override + public boolean isAnnotation() { + return (this.access & Opcodes.ACC_ANNOTATION) != 0; + } + + @Override + public boolean isAbstract() { + return (this.access & Opcodes.ACC_ABSTRACT) != 0; + } + + @Override + public boolean isFinal() { + return (this.access & Opcodes.ACC_FINAL) != 0; + } + + @Override + public boolean isIndependent() { + return (this.enclosingClassName == null || this.independentInnerClass); + } + + @Override + @Nullable + public String getEnclosingClassName() { + return this.enclosingClassName; + } + + @Override + @Nullable + public String getSuperClassName() { + return this.superClassName; + } + + @Override + public String[] getInterfaceNames() { + return this.interfaceNames.clone(); + } + + @Override + public String[] getMemberClassNames() { + return this.memberClassNames.clone(); + } + + @Override + public Set getAnnotationTypes() { + Set annotationTypes = this.annotationTypes; + if (annotationTypes == null) { + annotationTypes = Collections.unmodifiableSet( + AnnotationMetadata.super.getAnnotationTypes()); + this.annotationTypes = annotationTypes; + } + return annotationTypes; + } + + @Override + public Set getAnnotatedMethods(String annotationName) { + Set annotatedMethods = null; + for (MethodMetadata annotatedMethod : this.annotatedMethods) { + if (annotatedMethod.isAnnotated(annotationName)) { + if (annotatedMethods == null) { + annotatedMethods = new LinkedHashSet<>(4); + } + annotatedMethods.add(annotatedMethod); + } + } + return annotatedMethods != null ? annotatedMethods : Collections.emptySet(); + } + + @Override + public MergedAnnotations getAnnotations() { + return this.annotations; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleAnnotationMetadataReadingVisitor.java b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleAnnotationMetadataReadingVisitor.java new file mode 100644 index 0000000..eb11235 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleAnnotationMetadataReadingVisitor.java @@ -0,0 +1,209 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.classreading; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.asm.AnnotationVisitor; +import org.springframework.asm.ClassVisitor; +import org.springframework.asm.MethodVisitor; +import org.springframework.asm.Opcodes; +import org.springframework.asm.SpringAsmInfo; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.type.MethodMetadata; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * ASM class visitor that creates {@link SimpleAnnotationMetadata}. + * + * @author Phillip Webb + * @since 5.2 + */ +final class SimpleAnnotationMetadataReadingVisitor extends ClassVisitor { + + @Nullable + private final ClassLoader classLoader; + + private String className = ""; + + private int access; + + @Nullable + private String superClassName; + + private String[] interfaceNames = new String[0]; + + @Nullable + private String enclosingClassName; + + private boolean independentInnerClass; + + private Set memberClassNames = new LinkedHashSet<>(4); + + private List> annotations = new ArrayList<>(); + + private List annotatedMethods = new ArrayList<>(); + + @Nullable + private SimpleAnnotationMetadata metadata; + + @Nullable + private Source source; + + + SimpleAnnotationMetadataReadingVisitor(@Nullable ClassLoader classLoader) { + super(SpringAsmInfo.ASM_VERSION); + this.classLoader = classLoader; + } + + + @Override + public void visit(int version, int access, String name, String signature, + @Nullable String supername, String[] interfaces) { + + this.className = toClassName(name); + this.access = access; + if (supername != null && !isInterface(access)) { + this.superClassName = toClassName(supername); + } + this.interfaceNames = new String[interfaces.length]; + for (int i = 0; i < interfaces.length; i++) { + this.interfaceNames[i] = toClassName(interfaces[i]); + } + } + + @Override + public void visitOuterClass(String owner, String name, String desc) { + this.enclosingClassName = toClassName(owner); + } + + @Override + public void visitInnerClass(String name, @Nullable String outerName, String innerName, + int access) { + if (outerName != null) { + String className = toClassName(name); + String outerClassName = toClassName(outerName); + if (this.className.equals(className)) { + this.enclosingClassName = outerClassName; + this.independentInnerClass = ((access & Opcodes.ACC_STATIC) != 0); + } + else if (this.className.equals(outerClassName)) { + this.memberClassNames.add(className); + } + } + } + + @Override + @Nullable + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + return MergedAnnotationReadingVisitor.get(this.classLoader, this::getSource, + descriptor, visible, this.annotations::add); + } + + @Override + @Nullable + public MethodVisitor visitMethod( + int access, String name, String descriptor, String signature, String[] exceptions) { + + // Skip bridge methods - we're only interested in original + // annotation-defining user methods. On JDK 8, we'd otherwise run into + // double detection of the same annotated method... + if (isBridge(access)) { + return null; + } + return new SimpleMethodMetadataReadingVisitor(this.classLoader, this.className, + access, name, descriptor, this.annotatedMethods::add); + } + + @Override + public void visitEnd() { + String[] memberClassNames = StringUtils.toStringArray(this.memberClassNames); + MethodMetadata[] annotatedMethods = this.annotatedMethods.toArray(new MethodMetadata[0]); + MergedAnnotations annotations = MergedAnnotations.of(this.annotations); + this.metadata = new SimpleAnnotationMetadata(this.className, this.access, + this.enclosingClassName, this.superClassName, this.independentInnerClass, + this.interfaceNames, memberClassNames, annotatedMethods, annotations); + } + + public SimpleAnnotationMetadata getMetadata() { + Assert.state(this.metadata != null, "AnnotationMetadata not initialized"); + return this.metadata; + } + + private Source getSource() { + Source source = this.source; + if (source == null) { + source = new Source(this.className); + this.source = source; + } + return source; + } + + private String toClassName(String name) { + return ClassUtils.convertResourcePathToClassName(name); + } + + private boolean isBridge(int access) { + return (access & Opcodes.ACC_BRIDGE) != 0; + } + + private boolean isInterface(int access) { + return (access & Opcodes.ACC_INTERFACE) != 0; + } + + /** + * {@link MergedAnnotation} source. + */ + private static final class Source { + + private final String className; + + Source(String className) { + this.className = className; + } + + @Override + public int hashCode() { + return this.className.hashCode(); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + return this.className.equals(((Source) obj).className); + } + + @Override + public String toString() { + return this.className; + } + + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMetadataReader.java b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMetadataReader.java new file mode 100644 index 0000000..7298d62 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMetadataReader.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.classreading; + +import java.io.IOException; +import java.io.InputStream; + +import org.springframework.asm.ClassReader; +import org.springframework.core.NestedIOException; +import org.springframework.core.io.Resource; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.ClassMetadata; +import org.springframework.lang.Nullable; + +/** + * {@link MetadataReader} implementation based on an ASM + * {@link org.springframework.asm.ClassReader}. + * + * @author Juergen Hoeller + * @author Costin Leau + * @since 2.5 + */ +final class SimpleMetadataReader implements MetadataReader { + + private static final int PARSING_OPTIONS = ClassReader.SKIP_DEBUG + | ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES; + + private final Resource resource; + + private final AnnotationMetadata annotationMetadata; + + + SimpleMetadataReader(Resource resource, @Nullable ClassLoader classLoader) throws IOException { + SimpleAnnotationMetadataReadingVisitor visitor = new SimpleAnnotationMetadataReadingVisitor(classLoader); + getClassReader(resource).accept(visitor, PARSING_OPTIONS); + this.resource = resource; + this.annotationMetadata = visitor.getMetadata(); + } + + private static ClassReader getClassReader(Resource resource) throws IOException { + try (InputStream is = resource.getInputStream()) { + try { + return new ClassReader(is); + } + catch (IllegalArgumentException ex) { + throw new NestedIOException("ASM ClassReader failed to parse class file - " + + "probably due to a new Java class file version that isn't supported yet: " + resource, ex); + } + } + } + + + @Override + public Resource getResource() { + return this.resource; + } + + @Override + public ClassMetadata getClassMetadata() { + return this.annotationMetadata; + } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + return this.annotationMetadata; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMetadataReaderFactory.java b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMetadataReaderFactory.java new file mode 100644 index 0000000..46aa8f4 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMetadataReaderFactory.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.classreading; + +import java.io.FileNotFoundException; +import java.io.IOException; + +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * Simple implementation of the {@link MetadataReaderFactory} interface, + * creating a new ASM {@link org.springframework.asm.ClassReader} for every request. + * + * @author Juergen Hoeller + * @since 2.5 + */ +public class SimpleMetadataReaderFactory implements MetadataReaderFactory { + + private final ResourceLoader resourceLoader; + + + /** + * Create a new SimpleMetadataReaderFactory for the default class loader. + */ + public SimpleMetadataReaderFactory() { + this.resourceLoader = new DefaultResourceLoader(); + } + + /** + * Create a new SimpleMetadataReaderFactory for the given resource loader. + * @param resourceLoader the Spring ResourceLoader to use + * (also determines the ClassLoader to use) + */ + public SimpleMetadataReaderFactory(@Nullable ResourceLoader resourceLoader) { + this.resourceLoader = (resourceLoader != null ? resourceLoader : new DefaultResourceLoader()); + } + + /** + * Create a new SimpleMetadataReaderFactory for the given class loader. + * @param classLoader the ClassLoader to use + */ + public SimpleMetadataReaderFactory(@Nullable ClassLoader classLoader) { + this.resourceLoader = + (classLoader != null ? new DefaultResourceLoader(classLoader) : new DefaultResourceLoader()); + } + + + /** + * Return the ResourceLoader that this MetadataReaderFactory has been + * constructed with. + */ + public final ResourceLoader getResourceLoader() { + return this.resourceLoader; + } + + + @Override + public MetadataReader getMetadataReader(String className) throws IOException { + try { + String resourcePath = ResourceLoader.CLASSPATH_URL_PREFIX + + ClassUtils.convertClassNameToResourcePath(className) + ClassUtils.CLASS_FILE_SUFFIX; + Resource resource = this.resourceLoader.getResource(resourcePath); + return getMetadataReader(resource); + } + catch (FileNotFoundException ex) { + // Maybe an inner class name using the dot name syntax? Need to use the dollar syntax here... + // ClassUtils.forName has an equivalent check for resolution into Class references later on. + int lastDotIndex = className.lastIndexOf('.'); + if (lastDotIndex != -1) { + String innerClassName = + className.substring(0, lastDotIndex) + '$' + className.substring(lastDotIndex + 1); + String innerClassResourcePath = ResourceLoader.CLASSPATH_URL_PREFIX + + ClassUtils.convertClassNameToResourcePath(innerClassName) + ClassUtils.CLASS_FILE_SUFFIX; + Resource innerClassResource = this.resourceLoader.getResource(innerClassResourcePath); + if (innerClassResource.exists()) { + return getMetadataReader(innerClassResource); + } + } + throw ex; + } + } + + @Override + public MetadataReader getMetadataReader(Resource resource) throws IOException { + return new SimpleMetadataReader(resource, this.resourceLoader.getClassLoader()); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMethodMetadata.java b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMethodMetadata.java new file mode 100644 index 0000000..5d1b21d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMethodMetadata.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.classreading; + +import org.springframework.asm.Opcodes; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.type.MethodMetadata; + +/** + * {@link MethodMetadata} created from a + * {@link SimpleMethodMetadataReadingVisitor}. + * + * @author Phillip Webb + * @since 5.2 + */ +final class SimpleMethodMetadata implements MethodMetadata { + + private final String methodName; + + private final int access; + + private final String declaringClassName; + + private final String returnTypeName; + + private final MergedAnnotations annotations; + + + public SimpleMethodMetadata(String methodName, int access, String declaringClassName, + String returnTypeName, MergedAnnotations annotations) { + + this.methodName = methodName; + this.access = access; + this.declaringClassName = declaringClassName; + this.returnTypeName = returnTypeName; + this.annotations = annotations; + } + + + @Override + public String getMethodName() { + return this.methodName; + } + + @Override + public String getDeclaringClassName() { + return this.declaringClassName; + } + + @Override + public String getReturnTypeName() { + return this.returnTypeName; + } + + @Override + public boolean isAbstract() { + return (this.access & Opcodes.ACC_ABSTRACT) != 0; + } + + @Override + public boolean isStatic() { + return (this.access & Opcodes.ACC_STATIC) != 0; + } + + @Override + public boolean isFinal() { + return (this.access & Opcodes.ACC_FINAL) != 0; + } + + @Override + public boolean isOverridable() { + return !isStatic() && !isFinal() && !isPrivate(); + } + + public boolean isPrivate() { + return (this.access & Opcodes.ACC_PRIVATE) != 0; + } + + @Override + public MergedAnnotations getAnnotations() { + return this.annotations; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMethodMetadataReadingVisitor.java b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMethodMetadataReadingVisitor.java new file mode 100644 index 0000000..f8af7e8 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMethodMetadataReadingVisitor.java @@ -0,0 +1,162 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.classreading; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import org.springframework.asm.AnnotationVisitor; +import org.springframework.asm.MethodVisitor; +import org.springframework.asm.SpringAsmInfo; +import org.springframework.asm.Type; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.lang.Nullable; + +/** + * ASM method visitor that creates {@link SimpleMethodMetadata}. + * + * @author Phillip Webb + * @since 5.2 + */ +final class SimpleMethodMetadataReadingVisitor extends MethodVisitor { + + @Nullable + private final ClassLoader classLoader; + + private final String declaringClassName; + + private final int access; + + private final String name; + + private final String descriptor; + + private final List> annotations = new ArrayList<>(4); + + private final Consumer consumer; + + @Nullable + private Source source; + + + SimpleMethodMetadataReadingVisitor(@Nullable ClassLoader classLoader, String declaringClassName, + int access, String name, String descriptor, Consumer consumer) { + + super(SpringAsmInfo.ASM_VERSION); + this.classLoader = classLoader; + this.declaringClassName = declaringClassName; + this.access = access; + this.name = name; + this.descriptor = descriptor; + this.consumer = consumer; + } + + + @Override + @Nullable + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + return MergedAnnotationReadingVisitor.get(this.classLoader, this::getSource, + descriptor, visible, this.annotations::add); + } + + @Override + public void visitEnd() { + if (!this.annotations.isEmpty()) { + String returnTypeName = Type.getReturnType(this.descriptor).getClassName(); + MergedAnnotations annotations = MergedAnnotations.of(this.annotations); + SimpleMethodMetadata metadata = new SimpleMethodMetadata(this.name, + this.access, this.declaringClassName, returnTypeName, annotations); + this.consumer.accept(metadata); + } + } + + private Object getSource() { + Source source = this.source; + if (source == null) { + source = new Source(this.declaringClassName, this.name, this.descriptor); + this.source = source; + } + return source; + } + + + /** + * {@link MergedAnnotation} source. + */ + static final class Source { + + private final String declaringClassName; + + private final String name; + + private final String descriptor; + + @Nullable + private String toStringValue; + + Source(String declaringClassName, String name, String descriptor) { + this.declaringClassName = declaringClassName; + this.name = name; + this.descriptor = descriptor; + } + + @Override + public int hashCode() { + int result = 1; + result = 31 * result + this.declaringClassName.hashCode(); + result = 31 * result + this.name.hashCode(); + result = 31 * result + this.descriptor.hashCode(); + return result; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + Source otherSource = (Source) other; + return (this.declaringClassName.equals(otherSource.declaringClassName) && + this.name.equals(otherSource.name) && this.descriptor.equals(otherSource.descriptor)); + } + + @Override + public String toString() { + String value = this.toStringValue; + if (value == null) { + StringBuilder builder = new StringBuilder(); + builder.append(this.declaringClassName); + builder.append("."); + builder.append(this.name); + Type[] argumentTypes = Type.getArgumentTypes(this.descriptor); + builder.append("("); + for (Type type : argumentTypes) { + builder.append(type.getClassName()); + } + builder.append(")"); + value = builder.toString(); + this.toStringValue = value; + } + return value; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/package-info.java b/spring-core/src/main/java/org/springframework/core/type/classreading/package-info.java new file mode 100644 index 0000000..37a94af --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/package-info.java @@ -0,0 +1,9 @@ +/** + * Support classes for reading annotation and class-level metadata. + */ +@NonNullApi +@NonNullFields +package org.springframework.core.type.classreading; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/AbstractClassTestingTypeFilter.java b/spring-core/src/main/java/org/springframework/core/type/filter/AbstractClassTestingTypeFilter.java new file mode 100644 index 0000000..c9edc9d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/filter/AbstractClassTestingTypeFilter.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.filter; + +import java.io.IOException; + +import org.springframework.core.type.ClassMetadata; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; + +/** + * Type filter that exposes a + * {@link org.springframework.core.type.ClassMetadata} object + * to subclasses, for class testing purposes. + * + * @author Rod Johnson + * @author Costin Leau + * @author Juergen Hoeller + * @since 2.5 + * @see #match(org.springframework.core.type.ClassMetadata) + */ +public abstract class AbstractClassTestingTypeFilter implements TypeFilter { + + @Override + public final boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) + throws IOException { + + return match(metadataReader.getClassMetadata()); + } + + /** + * Determine a match based on the given ClassMetadata object. + * @param metadata the ClassMetadata object + * @return whether this filter matches on the specified type + */ + protected abstract boolean match(ClassMetadata metadata); + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/AbstractTypeHierarchyTraversingFilter.java b/spring-core/src/main/java/org/springframework/core/type/filter/AbstractTypeHierarchyTraversingFilter.java new file mode 100644 index 0000000..cfa9ba2 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/filter/AbstractTypeHierarchyTraversingFilter.java @@ -0,0 +1,162 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.filter; + +import java.io.IOException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.type.ClassMetadata; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.lang.Nullable; + +/** + * Type filter that is aware of traversing over hierarchy. + * + *

    This filter is useful when matching needs to be made based on potentially the + * whole class/interface hierarchy. The algorithm employed uses a succeed-fast + * strategy: if at any time a match is declared, no further processing is + * carried out. + * + * @author Ramnivas Laddad + * @author Mark Fisher + * @since 2.5 + */ +public abstract class AbstractTypeHierarchyTraversingFilter implements TypeFilter { + + protected final Log logger = LogFactory.getLog(getClass()); + + private final boolean considerInherited; + + private final boolean considerInterfaces; + + + protected AbstractTypeHierarchyTraversingFilter(boolean considerInherited, boolean considerInterfaces) { + this.considerInherited = considerInherited; + this.considerInterfaces = considerInterfaces; + } + + + @Override + public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) + throws IOException { + + // This method optimizes avoiding unnecessary creation of ClassReaders + // as well as visiting over those readers. + if (matchSelf(metadataReader)) { + return true; + } + ClassMetadata metadata = metadataReader.getClassMetadata(); + if (matchClassName(metadata.getClassName())) { + return true; + } + + if (this.considerInherited) { + String superClassName = metadata.getSuperClassName(); + if (superClassName != null) { + // Optimization to avoid creating ClassReader for super class. + Boolean superClassMatch = matchSuperClass(superClassName); + if (superClassMatch != null) { + if (superClassMatch.booleanValue()) { + return true; + } + } + else { + // Need to read super class to determine a match... + try { + if (match(metadata.getSuperClassName(), metadataReaderFactory)) { + return true; + } + } + catch (IOException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Could not read super class [" + metadata.getSuperClassName() + + "] of type-filtered class [" + metadata.getClassName() + "]"); + } + } + } + } + } + + if (this.considerInterfaces) { + for (String ifc : metadata.getInterfaceNames()) { + // Optimization to avoid creating ClassReader for super class + Boolean interfaceMatch = matchInterface(ifc); + if (interfaceMatch != null) { + if (interfaceMatch.booleanValue()) { + return true; + } + } + else { + // Need to read interface to determine a match... + try { + if (match(ifc, metadataReaderFactory)) { + return true; + } + } + catch (IOException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Could not read interface [" + ifc + "] for type-filtered class [" + + metadata.getClassName() + "]"); + } + } + } + } + } + + return false; + } + + private boolean match(String className, MetadataReaderFactory metadataReaderFactory) throws IOException { + return match(metadataReaderFactory.getMetadataReader(className), metadataReaderFactory); + } + + /** + * Override this to match self characteristics alone. Typically, + * the implementation will use a visitor to extract information + * to perform matching. + */ + protected boolean matchSelf(MetadataReader metadataReader) { + return false; + } + + /** + * Override this to match on type name. + */ + protected boolean matchClassName(String className) { + return false; + } + + /** + * Override this to match on super type name. + */ + @Nullable + protected Boolean matchSuperClass(String superClassName) { + return null; + } + + /** + * Override this to match on interface type name. + */ + @Nullable + protected Boolean matchInterface(String interfaceName) { + return null; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/AnnotationTypeFilter.java b/spring-core/src/main/java/org/springframework/core/type/filter/AnnotationTypeFilter.java new file mode 100644 index 0000000..bff626e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/filter/AnnotationTypeFilter.java @@ -0,0 +1,139 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.filter; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Inherited; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * A simple {@link TypeFilter} which matches classes with a given annotation, + * checking inherited annotations as well. + * + *

    By default, the matching logic mirrors that of + * {@link AnnotationUtils#getAnnotation(java.lang.reflect.AnnotatedElement, Class)}, + * supporting annotations that are present or meta-present for a + * single level of meta-annotations. The search for meta-annotations my be disabled. + * Similarly, the search for annotations on interfaces may optionally be enabled. + * Consult the various constructors in this class for details. + * + * @author Mark Fisher + * @author Ramnivas Laddad + * @author Juergen Hoeller + * @author Sam Brannen + * @since 2.5 + */ +public class AnnotationTypeFilter extends AbstractTypeHierarchyTraversingFilter { + + private final Class annotationType; + + private final boolean considerMetaAnnotations; + + + /** + * Create a new {@code AnnotationTypeFilter} for the given annotation type. + *

    The filter will also match meta-annotations. To disable the + * meta-annotation matching, use the constructor that accepts a + * '{@code considerMetaAnnotations}' argument. + *

    The filter will not match interfaces. + * @param annotationType the annotation type to match + */ + public AnnotationTypeFilter(Class annotationType) { + this(annotationType, true, false); + } + + /** + * Create a new {@code AnnotationTypeFilter} for the given annotation type. + *

    The filter will not match interfaces. + * @param annotationType the annotation type to match + * @param considerMetaAnnotations whether to also match on meta-annotations + */ + public AnnotationTypeFilter(Class annotationType, boolean considerMetaAnnotations) { + this(annotationType, considerMetaAnnotations, false); + } + + /** + * Create a new {@code AnnotationTypeFilter} for the given annotation type. + * @param annotationType the annotation type to match + * @param considerMetaAnnotations whether to also match on meta-annotations + * @param considerInterfaces whether to also match interfaces + */ + public AnnotationTypeFilter( + Class annotationType, boolean considerMetaAnnotations, boolean considerInterfaces) { + + super(annotationType.isAnnotationPresent(Inherited.class), considerInterfaces); + this.annotationType = annotationType; + this.considerMetaAnnotations = considerMetaAnnotations; + } + + /** + * Return the {@link Annotation} that this instance is using to filter + * candidates. + * @since 5.0 + */ + public final Class getAnnotationType() { + return this.annotationType; + } + + @Override + protected boolean matchSelf(MetadataReader metadataReader) { + AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); + return metadata.hasAnnotation(this.annotationType.getName()) || + (this.considerMetaAnnotations && metadata.hasMetaAnnotation(this.annotationType.getName())); + } + + @Override + @Nullable + protected Boolean matchSuperClass(String superClassName) { + return hasAnnotation(superClassName); + } + + @Override + @Nullable + protected Boolean matchInterface(String interfaceName) { + return hasAnnotation(interfaceName); + } + + @Nullable + protected Boolean hasAnnotation(String typeName) { + if (Object.class.getName().equals(typeName)) { + return false; + } + else if (typeName.startsWith("java")) { + if (!this.annotationType.getName().startsWith("java")) { + // Standard Java types do not have non-standard annotations on them -> + // skip any load attempt, in particular for Java language interfaces. + return false; + } + try { + Class clazz = ClassUtils.forName(typeName, getClass().getClassLoader()); + return ((this.considerMetaAnnotations ? AnnotationUtils.getAnnotation(clazz, this.annotationType) : + clazz.getAnnotation(this.annotationType)) != null); + } + catch (Throwable ex) { + // Class not regularly loadable - can't determine a match that way. + } + } + return null; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/AspectJTypeFilter.java b/spring-core/src/main/java/org/springframework/core/type/filter/AspectJTypeFilter.java new file mode 100644 index 0000000..86fbde4 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/filter/AspectJTypeFilter.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.filter; + +import java.io.IOException; + +import org.aspectj.bridge.IMessageHandler; +import org.aspectj.weaver.ResolvedType; +import org.aspectj.weaver.World; +import org.aspectj.weaver.bcel.BcelWorld; +import org.aspectj.weaver.patterns.Bindings; +import org.aspectj.weaver.patterns.FormalBinding; +import org.aspectj.weaver.patterns.IScope; +import org.aspectj.weaver.patterns.PatternParser; +import org.aspectj.weaver.patterns.SimpleScope; +import org.aspectj.weaver.patterns.TypePattern; + +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.lang.Nullable; + +/** + * Type filter that uses AspectJ type pattern for matching. + * + *

    A critical implementation details of this type filter is that it does not + * load the class being examined to match with a type pattern. + * + * @author Ramnivas Laddad + * @author Juergen Hoeller + * @since 2.5 + */ +public class AspectJTypeFilter implements TypeFilter { + + private final World world; + + private final TypePattern typePattern; + + + public AspectJTypeFilter(String typePatternExpression, @Nullable ClassLoader classLoader) { + this.world = new BcelWorld(classLoader, IMessageHandler.THROW, null); + this.world.setBehaveInJava5Way(true); + PatternParser patternParser = new PatternParser(typePatternExpression); + TypePattern typePattern = patternParser.parseTypePattern(); + typePattern.resolve(this.world); + IScope scope = new SimpleScope(this.world, new FormalBinding[0]); + this.typePattern = typePattern.resolveBindings(scope, Bindings.NONE, false, false); + } + + + @Override + public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) + throws IOException { + + String className = metadataReader.getClassMetadata().getClassName(); + ResolvedType resolvedType = this.world.resolve(className); + return this.typePattern.matchesStatically(resolvedType); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/AssignableTypeFilter.java b/spring-core/src/main/java/org/springframework/core/type/filter/AssignableTypeFilter.java new file mode 100644 index 0000000..53566f6 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/filter/AssignableTypeFilter.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.filter; + +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * A simple filter which matches classes that are assignable to a given type. + * + * @author Rod Johnson + * @author Mark Fisher + * @author Ramnivas Laddad + * @since 2.5 + */ +public class AssignableTypeFilter extends AbstractTypeHierarchyTraversingFilter { + + private final Class targetType; + + + /** + * Create a new AssignableTypeFilter for the given type. + * @param targetType the type to match + */ + public AssignableTypeFilter(Class targetType) { + super(true, true); + this.targetType = targetType; + } + + /** + * Return the {@code type} that this instance is using to filter candidates. + * @since 5.0 + */ + public final Class getTargetType() { + return this.targetType; + } + + @Override + protected boolean matchClassName(String className) { + return this.targetType.getName().equals(className); + } + + @Override + @Nullable + protected Boolean matchSuperClass(String superClassName) { + return matchTargetType(superClassName); + } + + @Override + @Nullable + protected Boolean matchInterface(String interfaceName) { + return matchTargetType(interfaceName); + } + + @Nullable + protected Boolean matchTargetType(String typeName) { + if (this.targetType.getName().equals(typeName)) { + return true; + } + else if (Object.class.getName().equals(typeName)) { + return false; + } + else if (typeName.startsWith("java")) { + try { + Class clazz = ClassUtils.forName(typeName, getClass().getClassLoader()); + return this.targetType.isAssignableFrom(clazz); + } + catch (Throwable ex) { + // Class not regularly loadable - can't determine a match that way. + } + } + return null; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/RegexPatternTypeFilter.java b/spring-core/src/main/java/org/springframework/core/type/filter/RegexPatternTypeFilter.java new file mode 100644 index 0000000..67db532 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/filter/RegexPatternTypeFilter.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2007 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.filter; + +import java.util.regex.Pattern; + +import org.springframework.core.type.ClassMetadata; +import org.springframework.util.Assert; + +/** + * A simple filter for matching a fully-qualified class name with a regex {@link Pattern}. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @since 2.5 + */ +public class RegexPatternTypeFilter extends AbstractClassTestingTypeFilter { + + private final Pattern pattern; + + + public RegexPatternTypeFilter(Pattern pattern) { + Assert.notNull(pattern, "Pattern must not be null"); + this.pattern = pattern; + } + + + @Override + protected boolean match(ClassMetadata metadata) { + return this.pattern.matcher(metadata.getClassName()).matches(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/TypeFilter.java b/spring-core/src/main/java/org/springframework/core/type/filter/TypeFilter.java new file mode 100644 index 0000000..422cbe4 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/filter/TypeFilter.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.filter; + +import java.io.IOException; + +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; + +/** + * Base interface for type filters using a + * {@link org.springframework.core.type.classreading.MetadataReader}. + * + * @author Costin Leau + * @author Juergen Hoeller + * @author Mark Fisher + * @since 2.5 + */ +@FunctionalInterface +public interface TypeFilter { + + /** + * Determine whether this filter matches for the class described by + * the given metadata. + * @param metadataReader the metadata reader for the target class + * @param metadataReaderFactory a factory for obtaining metadata readers + * for other classes (such as superclasses and interfaces) + * @return whether this filter matches + * @throws IOException in case of I/O failure when reading metadata + */ + boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) + throws IOException; + +} diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/package-info.java b/spring-core/src/main/java/org/springframework/core/type/filter/package-info.java new file mode 100644 index 0000000..7c60af6 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/filter/package-info.java @@ -0,0 +1,9 @@ +/** + * Core support package for type filtering (e.g. for classpath scanning). + */ +@NonNullApi +@NonNullFields +package org.springframework.core.type.filter; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/main/java/org/springframework/core/type/package-info.java b/spring-core/src/main/java/org/springframework/core/type/package-info.java new file mode 100644 index 0000000..f63ddb1 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/package-info.java @@ -0,0 +1,9 @@ +/** + * Core support package for type introspection. + */ +@NonNullApi +@NonNullFields +package org.springframework.core.type; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/main/java/org/springframework/lang/NonNull.java b/spring-core/src/main/java/org/springframework/lang/NonNull.java new file mode 100644 index 0000000..cd4ecde --- /dev/null +++ b/spring-core/src/main/java/org/springframework/lang/NonNull.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.lang; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.annotation.Nonnull; +import javax.annotation.meta.TypeQualifierNickname; + +/** + * A common Spring annotation to declare that annotated elements cannot be {@code null}. + * + *

    Leverages JSR-305 meta-annotations to indicate nullability in Java to common + * tools with JSR-305 support and used by Kotlin to infer nullability of Spring API. + * + *

    Should be used at parameter, return value, and field level. Method overrides should + * repeat parent {@code @NonNull} annotations unless they behave differently. + * + *

    Use {@code @NonNullApi} (scope = parameters + return values) and/or {@code @NonNullFields} + * (scope = fields) to set the default behavior to non-nullable in order to avoid annotating + * your whole codebase with {@code @NonNull}. + * + * @author Sebastien Deleuze + * @author Juergen Hoeller + * @since 5.0 + * @see NonNullApi + * @see NonNullFields + * @see Nullable + */ +@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Nonnull +@TypeQualifierNickname +public @interface NonNull { +} diff --git a/spring-core/src/main/java/org/springframework/lang/NonNullApi.java b/spring-core/src/main/java/org/springframework/lang/NonNullApi.java new file mode 100644 index 0000000..7bf99bd --- /dev/null +++ b/spring-core/src/main/java/org/springframework/lang/NonNullApi.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.lang; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.annotation.Nonnull; +import javax.annotation.meta.TypeQualifierDefault; + +/** + * A common Spring annotation to declare that parameters and return values + * are to be considered as non-nullable by default for a given package. + * + *

    Leverages JSR-305 meta-annotations to indicate nullability in Java to common + * tools with JSR-305 support and used by Kotlin to infer nullability of Spring API. + * + *

    Should be used at package level in association with {@link Nullable} + * annotations at parameter and return value level. + * + * @author Sebastien Deleuze + * @author Juergen Hoeller + * @since 5.0 + * @see NonNullFields + * @see Nullable + * @see NonNull + */ +@Target(ElementType.PACKAGE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Nonnull +@TypeQualifierDefault({ElementType.METHOD, ElementType.PARAMETER}) +public @interface NonNullApi { +} diff --git a/spring-core/src/main/java/org/springframework/lang/NonNullFields.java b/spring-core/src/main/java/org/springframework/lang/NonNullFields.java new file mode 100644 index 0000000..49968b9 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/lang/NonNullFields.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.lang; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.annotation.Nonnull; +import javax.annotation.meta.TypeQualifierDefault; + +/** + * A common Spring annotation to declare that fields are to be considered as + * non-nullable by default for a given package. + * + *

    Leverages JSR-305 meta-annotations to indicate nullability in Java to common + * tools with JSR-305 support and used by Kotlin to infer nullability of Spring API. + * + *

    Should be used at package level in association with {@link Nullable} + * annotations at field level. + * + * @author Sebastien Deleuze + * @since 5.0 + * @see NonNullApi + * @see Nullable + * @see NonNull + */ +@Target(ElementType.PACKAGE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Nonnull +@TypeQualifierDefault(ElementType.FIELD) +public @interface NonNullFields { +} diff --git a/spring-core/src/main/java/org/springframework/lang/Nullable.java b/spring-core/src/main/java/org/springframework/lang/Nullable.java new file mode 100644 index 0000000..3abedbd --- /dev/null +++ b/spring-core/src/main/java/org/springframework/lang/Nullable.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.lang; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.annotation.Nonnull; +import javax.annotation.meta.TypeQualifierNickname; +import javax.annotation.meta.When; + +/** + * A common Spring annotation to declare that annotated elements can be {@code null} under + * some circumstance. + * + *

    Leverages JSR-305 meta-annotations to indicate nullability in Java to common + * tools with JSR-305 support and used by Kotlin to infer nullability of Spring API. + * + *

    Should be used at parameter, return value, and field level. Methods override should + * repeat parent {@code @Nullable} annotations unless they behave differently. + * + *

    Can be used in association with {@code @NonNullApi} or {@code @NonNullFields} to + * override the default non-nullable semantic to nullable. + * + * @author Sebastien Deleuze + * @author Juergen Hoeller + * @since 5.0 + * @see NonNullApi + * @see NonNullFields + * @see NonNull + */ +@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Nonnull(when = When.MAYBE) +@TypeQualifierNickname +public @interface Nullable { +} diff --git a/spring-core/src/main/java/org/springframework/lang/UsesJava7.java b/spring-core/src/main/java/org/springframework/lang/UsesJava7.java new file mode 100644 index 0000000..c59159c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/lang/UsesJava7.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.lang; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that the annotated element uses Java 7 specific API constructs, + * without implying that it strictly requires Java 7. + * + * @author Stephane Nicoll + * @since 4.1 + * @deprecated as of 5.0 since the framework is based on Java 8+ now + */ +@Retention(RetentionPolicy.CLASS) +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE}) +@Documented +@Deprecated +public @interface UsesJava7 { +} diff --git a/spring-core/src/main/java/org/springframework/lang/UsesJava8.java b/spring-core/src/main/java/org/springframework/lang/UsesJava8.java new file mode 100644 index 0000000..1c2416b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/lang/UsesJava8.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.lang; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that the annotated element uses Java 8 specific API constructs, + * without implying that it strictly requires Java 8. + * + * @author Stephane Nicoll + * @since 4.1 + * @deprecated as of 5.0 since the framework is based on Java 8+ now + */ +@Retention(RetentionPolicy.CLASS) +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE}) +@Documented +@Deprecated +public @interface UsesJava8 { +} diff --git a/spring-core/src/main/java/org/springframework/lang/UsesSunHttpServer.java b/spring-core/src/main/java/org/springframework/lang/UsesSunHttpServer.java new file mode 100644 index 0000000..2147079 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/lang/UsesSunHttpServer.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.lang; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that the annotated element uses the Http Server available in + * {@code com.sun.*} classes, which is only available on a Sun/Oracle JVM. + * + * @author Stephane Nicoll + * @since 4.1 + * @deprecated as of 5.1, along with Spring's Sun HTTP Server support classes + */ +@Deprecated +@Retention(RetentionPolicy.CLASS) +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE}) +@Documented +public @interface UsesSunHttpServer { +} diff --git a/spring-core/src/main/java/org/springframework/lang/UsesSunMisc.java b/spring-core/src/main/java/org/springframework/lang/UsesSunMisc.java new file mode 100644 index 0000000..f86ccd9 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/lang/UsesSunMisc.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.lang; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that the annotated element uses an API from the {@code sun.misc} + * package. + * + * @author Stephane Nicoll + * @since 4.3 + */ +@Retention(RetentionPolicy.CLASS) +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE}) +@Documented +public @interface UsesSunMisc { +} diff --git a/spring-core/src/main/java/org/springframework/lang/package-info.java b/spring-core/src/main/java/org/springframework/lang/package-info.java new file mode 100644 index 0000000..267363e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/lang/package-info.java @@ -0,0 +1,10 @@ +/** + * Common annotations with language-level semantics: nullability as well as JDK API indications. + * These annotations sit at the lowest level of Spring's package dependency arrangement, even + * lower than {@code org.springframework.util}, with no Spring-specific concepts implied. + * + *

    Used descriptively within the framework codebase. Can be validated by build-time tools + * (e.g. FindBugs or Animal Sniffer), alternative JVM languages (e.g. Kotlin), as well as IDEs + * (e.g. IntelliJ IDEA or Eclipse with corresponding project setup). + */ +package org.springframework.lang; diff --git a/spring-core/src/main/java/org/springframework/objenesis/SpringObjenesis.java b/spring-core/src/main/java/org/springframework/objenesis/SpringObjenesis.java new file mode 100644 index 0000000..9984211 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/objenesis/SpringObjenesis.java @@ -0,0 +1,153 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.objenesis; + +import org.springframework.core.SpringProperties; +import org.springframework.objenesis.instantiator.ObjectInstantiator; +import org.springframework.objenesis.strategy.InstantiatorStrategy; +import org.springframework.objenesis.strategy.StdInstantiatorStrategy; +import org.springframework.util.ConcurrentReferenceHashMap; + +/** + * Spring-specific variant of {@link ObjenesisStd} / {@link ObjenesisBase}, + * providing a cache based on {@code Class} keys instead of class names, + * and allowing for selective use of the cache. + * + * @author Juergen Hoeller + * @since 4.2 + * @see #isWorthTrying() + * @see #newInstance(Class, boolean) + */ +public class SpringObjenesis implements Objenesis { + + /** + * System property that instructs Spring to ignore Objenesis, not even attempting + * to use it. Setting this flag to "true" is equivalent to letting Spring find + * out that Objenesis isn't working at runtime, triggering the fallback code path + * immediately: Most importantly, this means that all CGLIB AOP proxies will be + * created through regular instantiation via a default constructor. + */ + public static final String IGNORE_OBJENESIS_PROPERTY_NAME = "spring.objenesis.ignore"; + + + private final InstantiatorStrategy strategy; + + private final ConcurrentReferenceHashMap, ObjectInstantiator> cache = + new ConcurrentReferenceHashMap<>(); + + private volatile Boolean worthTrying; + + + /** + * Create a new {@code SpringObjenesis} instance with the + * standard instantiator strategy. + */ + public SpringObjenesis() { + this(null); + } + + /** + * Create a new {@code SpringObjenesis} instance with the + * given standard instantiator strategy. + * @param strategy the instantiator strategy to use + */ + public SpringObjenesis(InstantiatorStrategy strategy) { + this.strategy = (strategy != null ? strategy : new StdInstantiatorStrategy()); + + // Evaluate the "spring.objenesis.ignore" property upfront... + if (SpringProperties.getFlag(SpringObjenesis.IGNORE_OBJENESIS_PROPERTY_NAME)) { + this.worthTrying = Boolean.FALSE; + } + } + + + /** + * Return whether this Objenesis instance is worth trying for instance creation, + * i.e. whether it hasn't been used yet or is known to work. + *

    If the configured Objenesis instantiator strategy has been identified to not + * work on the current JVM at all or if the "spring.objenesis.ignore" property has + * been set to "true", this method returns {@code false}. + */ + public boolean isWorthTrying() { + return (this.worthTrying != Boolean.FALSE); + } + + /** + * Create a new instance of the given class via Objenesis. + * @param clazz the class to create an instance of + * @param useCache whether to use the instantiator cache + * (typically {@code true} but can be set to {@code false} + * e.g. for reloadable classes) + * @return the new instance (never {@code null}) + * @throws ObjenesisException if instance creation failed + */ + public T newInstance(Class clazz, boolean useCache) { + if (!useCache) { + return newInstantiatorOf(clazz).newInstance(); + } + return getInstantiatorOf(clazz).newInstance(); + } + + public T newInstance(Class clazz) { + return getInstantiatorOf(clazz).newInstance(); + } + + @SuppressWarnings("unchecked") + public ObjectInstantiator getInstantiatorOf(Class clazz) { + ObjectInstantiator instantiator = this.cache.get(clazz); + if (instantiator == null) { + ObjectInstantiator newInstantiator = newInstantiatorOf(clazz); + instantiator = this.cache.putIfAbsent(clazz, newInstantiator); + if (instantiator == null) { + instantiator = newInstantiator; + } + } + return (ObjectInstantiator) instantiator; + } + + protected ObjectInstantiator newInstantiatorOf(Class clazz) { + Boolean currentWorthTrying = this.worthTrying; + try { + ObjectInstantiator instantiator = this.strategy.newInstantiatorOf(clazz); + if (currentWorthTrying == null) { + this.worthTrying = Boolean.TRUE; + } + return instantiator; + } + catch (ObjenesisException ex) { + if (currentWorthTrying == null) { + Throwable cause = ex.getCause(); + if (cause instanceof ClassNotFoundException || cause instanceof IllegalAccessException) { + // Indicates that the chosen instantiation strategy does not work on the given JVM. + // Typically a failure to initialize the default SunReflectionFactoryInstantiator. + // Let's assume that any subsequent attempts to use Objenesis will fail as well... + this.worthTrying = Boolean.FALSE; + } + } + throw ex; + } + catch (NoClassDefFoundError err) { + // Happening on the production version of Google App Engine, coming out of the + // restricted "sun.reflect.ReflectionFactory" class... + if (currentWorthTrying == null) { + this.worthTrying = Boolean.FALSE; + } + throw new ObjenesisException(err); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/objenesis/package-info.java b/spring-core/src/main/java/org/springframework/objenesis/package-info.java new file mode 100644 index 0000000..8decb0e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/objenesis/package-info.java @@ -0,0 +1,15 @@ +/** + * Spring's repackaging of + * Objenesis 3.0 + * (with SpringObjenesis entry point; for internal use only). + * + *

    This repackaging technique avoids any potential conflicts with + * dependencies on different Objenesis versions at the application + * level or from third-party libraries and frameworks. + * + *

    As this repackaging happens at the class file level, sources + * and javadocs are not available here. See the original + * Objenesis docs + * for details when working with these classes. + */ +package org.springframework.objenesis; diff --git a/spring-core/src/main/java/org/springframework/util/AlternativeJdkIdGenerator.java b/spring-core/src/main/java/org/springframework/util/AlternativeJdkIdGenerator.java new file mode 100644 index 0000000..24e936c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/AlternativeJdkIdGenerator.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.Random; +import java.util.UUID; + +/** + * An {@link IdGenerator} that uses {@link SecureRandom} for the initial seed and + * {@link Random} thereafter, instead of calling {@link UUID#randomUUID()} every + * time as {@link org.springframework.util.JdkIdGenerator JdkIdGenerator} does. + * This provides a better balance between securely random ids and performance. + * + * @author Rossen Stoyanchev + * @author Rob Winch + * @since 4.0 + */ +public class AlternativeJdkIdGenerator implements IdGenerator { + + private final Random random; + + + public AlternativeJdkIdGenerator() { + SecureRandom secureRandom = new SecureRandom(); + byte[] seed = new byte[8]; + secureRandom.nextBytes(seed); + this.random = new Random(new BigInteger(seed).longValue()); + } + + + @Override + public UUID generateId() { + byte[] randomBytes = new byte[16]; + this.random.nextBytes(randomBytes); + + long mostSigBits = 0; + for (int i = 0; i < 8; i++) { + mostSigBits = (mostSigBits << 8) | (randomBytes[i] & 0xff); + } + + long leastSigBits = 0; + for (int i = 8; i < 16; i++) { + leastSigBits = (leastSigBits << 8) | (randomBytes[i] & 0xff); + } + + return new UUID(mostSigBits, leastSigBits); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/AntPathMatcher.java b/spring-core/src/main/java/org/springframework/util/AntPathMatcher.java new file mode 100644 index 0000000..5908731 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/AntPathMatcher.java @@ -0,0 +1,962 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.lang.Nullable; + +/** + * {@link PathMatcher} implementation for Ant-style path patterns. + * + *

    Part of this mapping code has been kindly borrowed from Apache Ant. + * + *

    The mapping matches URLs using the following rules:
    + *

      + *
    • {@code ?} matches one character
    • + *
    • {@code *} matches zero or more characters
    • + *
    • {@code **} matches zero or more directories in a path
    • + *
    • {@code {spring:[a-z]+}} matches the regexp {@code [a-z]+} as a path variable named "spring"
    • + *
    + * + *

    Examples

    + *
      + *
    • {@code com/t?st.jsp} — matches {@code com/test.jsp} but also + * {@code com/tast.jsp} or {@code com/txst.jsp}
    • + *
    • {@code com/*.jsp} — matches all {@code .jsp} files in the + * {@code com} directory
    • + *
    • com/**/test.jsp — matches all {@code test.jsp} + * files underneath the {@code com} path
    • + *
    • org/springframework/**/*.jsp — matches all + * {@code .jsp} files underneath the {@code org/springframework} path
    • + *
    • org/**/servlet/bla.jsp — matches + * {@code org/springframework/servlet/bla.jsp} but also + * {@code org/springframework/testing/servlet/bla.jsp} and {@code org/servlet/bla.jsp}
    • + *
    • {@code com/{filename:\\w+}.jsp} will match {@code com/test.jsp} and assign the value {@code test} + * to the {@code filename} variable
    • + *
    + * + *

    Note: a pattern and a path must both be absolute or must + * both be relative in order for the two to match. Therefore it is recommended + * that users of this implementation to sanitize patterns in order to prefix + * them with "/" as it makes sense in the context in which they're used. + * + * @author Alef Arendsen + * @author Juergen Hoeller + * @author Rob Harrop + * @author Arjen Poutsma + * @author Rossen Stoyanchev + * @author Sam Brannen + * @author Vladislav Kisel + * @since 16.07.2003 + */ +public class AntPathMatcher implements PathMatcher { + + /** Default path separator: "/". */ + public static final String DEFAULT_PATH_SEPARATOR = "/"; + + private static final int CACHE_TURNOFF_THRESHOLD = 65536; + + private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{[^/]+?}"); + + private static final char[] WILDCARD_CHARS = {'*', '?', '{'}; + + + private String pathSeparator; + + private PathSeparatorPatternCache pathSeparatorPatternCache; + + private boolean caseSensitive = true; + + private boolean trimTokens = false; + + @Nullable + private volatile Boolean cachePatterns; + + private final Map tokenizedPatternCache = new ConcurrentHashMap<>(256); + + final Map stringMatcherCache = new ConcurrentHashMap<>(256); + + + /** + * Create a new instance with the {@link #DEFAULT_PATH_SEPARATOR}. + */ + public AntPathMatcher() { + this.pathSeparator = DEFAULT_PATH_SEPARATOR; + this.pathSeparatorPatternCache = new PathSeparatorPatternCache(DEFAULT_PATH_SEPARATOR); + } + + /** + * A convenient, alternative constructor to use with a custom path separator. + * @param pathSeparator the path separator to use, must not be {@code null}. + * @since 4.1 + */ + public AntPathMatcher(String pathSeparator) { + Assert.notNull(pathSeparator, "'pathSeparator' is required"); + this.pathSeparator = pathSeparator; + this.pathSeparatorPatternCache = new PathSeparatorPatternCache(pathSeparator); + } + + + /** + * Set the path separator to use for pattern parsing. + *

    Default is "/", as in Ant. + */ + public void setPathSeparator(@Nullable String pathSeparator) { + this.pathSeparator = (pathSeparator != null ? pathSeparator : DEFAULT_PATH_SEPARATOR); + this.pathSeparatorPatternCache = new PathSeparatorPatternCache(this.pathSeparator); + } + + /** + * Specify whether to perform pattern matching in a case-sensitive fashion. + *

    Default is {@code true}. Switch this to {@code false} for case-insensitive matching. + * @since 4.2 + */ + public void setCaseSensitive(boolean caseSensitive) { + this.caseSensitive = caseSensitive; + } + + /** + * Specify whether to trim tokenized paths and patterns. + *

    Default is {@code false}. + */ + public void setTrimTokens(boolean trimTokens) { + this.trimTokens = trimTokens; + } + + /** + * Specify whether to cache parsed pattern metadata for patterns passed + * into this matcher's {@link #match} method. A value of {@code true} + * activates an unlimited pattern cache; a value of {@code false} turns + * the pattern cache off completely. + *

    Default is for the cache to be on, but with the variant to automatically + * turn it off when encountering too many patterns to cache at runtime + * (the threshold is 65536), assuming that arbitrary permutations of patterns + * are coming in, with little chance for encountering a recurring pattern. + * @since 4.0.1 + * @see #getStringMatcher(String) + */ + public void setCachePatterns(boolean cachePatterns) { + this.cachePatterns = cachePatterns; + } + + private void deactivatePatternCache() { + this.cachePatterns = false; + this.tokenizedPatternCache.clear(); + this.stringMatcherCache.clear(); + } + + + @Override + public boolean isPattern(@Nullable String path) { + if (path == null) { + return false; + } + boolean uriVar = false; + for (int i = 0; i < path.length(); i++) { + char c = path.charAt(i); + if (c == '*' || c == '?') { + return true; + } + if (c == '{') { + uriVar = true; + continue; + } + if (c == '}' && uriVar) { + return true; + } + } + return false; + } + + @Override + public boolean match(String pattern, String path) { + return doMatch(pattern, path, true, null); + } + + @Override + public boolean matchStart(String pattern, String path) { + return doMatch(pattern, path, false, null); + } + + /** + * Actually match the given {@code path} against the given {@code pattern}. + * @param pattern the pattern to match against + * @param path the path to test + * @param fullMatch whether a full pattern match is required (else a pattern match + * as far as the given base path goes is sufficient) + * @return {@code true} if the supplied {@code path} matched, {@code false} if it didn't + */ + protected boolean doMatch(String pattern, @Nullable String path, boolean fullMatch, + @Nullable Map uriTemplateVariables) { + + if (path == null || path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) { + return false; + } + + String[] pattDirs = tokenizePattern(pattern); + if (fullMatch && this.caseSensitive && !isPotentialMatch(path, pattDirs)) { + return false; + } + + String[] pathDirs = tokenizePath(path); + int pattIdxStart = 0; + int pattIdxEnd = pattDirs.length - 1; + int pathIdxStart = 0; + int pathIdxEnd = pathDirs.length - 1; + + // Match all elements up to the first ** + while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { + String pattDir = pattDirs[pattIdxStart]; + if ("**".equals(pattDir)) { + break; + } + if (!matchStrings(pattDir, pathDirs[pathIdxStart], uriTemplateVariables)) { + return false; + } + pattIdxStart++; + pathIdxStart++; + } + + if (pathIdxStart > pathIdxEnd) { + // Path is exhausted, only match if rest of pattern is * or **'s + if (pattIdxStart > pattIdxEnd) { + return (pattern.endsWith(this.pathSeparator) == path.endsWith(this.pathSeparator)); + } + if (!fullMatch) { + return true; + } + if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) { + return true; + } + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!pattDirs[i].equals("**")) { + return false; + } + } + return true; + } + else if (pattIdxStart > pattIdxEnd) { + // String not exhausted, but pattern is. Failure. + return false; + } + else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) { + // Path start definitely matches due to "**" part in pattern. + return true; + } + + // up to last '**' + while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { + String pattDir = pattDirs[pattIdxEnd]; + if (pattDir.equals("**")) { + break; + } + if (!matchStrings(pattDir, pathDirs[pathIdxEnd], uriTemplateVariables)) { + return false; + } + pattIdxEnd--; + pathIdxEnd--; + } + if (pathIdxStart > pathIdxEnd) { + // String is exhausted + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!pattDirs[i].equals("**")) { + return false; + } + } + return true; + } + + while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) { + int patIdxTmp = -1; + for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) { + if (pattDirs[i].equals("**")) { + patIdxTmp = i; + break; + } + } + if (patIdxTmp == pattIdxStart + 1) { + // '**/**' situation, so skip one + pattIdxStart++; + continue; + } + // Find the pattern between padIdxStart & padIdxTmp in str between + // strIdxStart & strIdxEnd + int patLength = (patIdxTmp - pattIdxStart - 1); + int strLength = (pathIdxEnd - pathIdxStart + 1); + int foundIdx = -1; + + strLoop: + for (int i = 0; i <= strLength - patLength; i++) { + for (int j = 0; j < patLength; j++) { + String subPat = pattDirs[pattIdxStart + j + 1]; + String subStr = pathDirs[pathIdxStart + i + j]; + if (!matchStrings(subPat, subStr, uriTemplateVariables)) { + continue strLoop; + } + } + foundIdx = pathIdxStart + i; + break; + } + + if (foundIdx == -1) { + return false; + } + + pattIdxStart = patIdxTmp; + pathIdxStart = foundIdx + patLength; + } + + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!pattDirs[i].equals("**")) { + return false; + } + } + + return true; + } + + private boolean isPotentialMatch(String path, String[] pattDirs) { + if (!this.trimTokens) { + int pos = 0; + for (String pattDir : pattDirs) { + int skipped = skipSeparator(path, pos, this.pathSeparator); + pos += skipped; + skipped = skipSegment(path, pos, pattDir); + if (skipped < pattDir.length()) { + return (skipped > 0 || (pattDir.length() > 0 && isWildcardChar(pattDir.charAt(0)))); + } + pos += skipped; + } + } + return true; + } + + private int skipSegment(String path, int pos, String prefix) { + int skipped = 0; + for (int i = 0; i < prefix.length(); i++) { + char c = prefix.charAt(i); + if (isWildcardChar(c)) { + return skipped; + } + int currPos = pos + skipped; + if (currPos >= path.length()) { + return 0; + } + if (c == path.charAt(currPos)) { + skipped++; + } + } + return skipped; + } + + private int skipSeparator(String path, int pos, String separator) { + int skipped = 0; + while (path.startsWith(separator, pos + skipped)) { + skipped += separator.length(); + } + return skipped; + } + + private boolean isWildcardChar(char c) { + for (char candidate : WILDCARD_CHARS) { + if (c == candidate) { + return true; + } + } + return false; + } + + /** + * Tokenize the given path pattern into parts, based on this matcher's settings. + *

    Performs caching based on {@link #setCachePatterns}, delegating to + * {@link #tokenizePath(String)} for the actual tokenization algorithm. + * @param pattern the pattern to tokenize + * @return the tokenized pattern parts + */ + protected String[] tokenizePattern(String pattern) { + String[] tokenized = null; + Boolean cachePatterns = this.cachePatterns; + if (cachePatterns == null || cachePatterns.booleanValue()) { + tokenized = this.tokenizedPatternCache.get(pattern); + } + if (tokenized == null) { + tokenized = tokenizePath(pattern); + if (cachePatterns == null && this.tokenizedPatternCache.size() >= CACHE_TURNOFF_THRESHOLD) { + // Try to adapt to the runtime situation that we're encountering: + // There are obviously too many different patterns coming in here... + // So let's turn off the cache since the patterns are unlikely to be reoccurring. + deactivatePatternCache(); + return tokenized; + } + if (cachePatterns == null || cachePatterns.booleanValue()) { + this.tokenizedPatternCache.put(pattern, tokenized); + } + } + return tokenized; + } + + /** + * Tokenize the given path into parts, based on this matcher's settings. + * @param path the path to tokenize + * @return the tokenized path parts + */ + protected String[] tokenizePath(String path) { + return StringUtils.tokenizeToStringArray(path, this.pathSeparator, this.trimTokens, true); + } + + /** + * Test whether or not a string matches against a pattern. + * @param pattern the pattern to match against (never {@code null}) + * @param str the String which must be matched against the pattern (never {@code null}) + * @return {@code true} if the string matches against the pattern, or {@code false} otherwise + */ + private boolean matchStrings(String pattern, String str, + @Nullable Map uriTemplateVariables) { + + return getStringMatcher(pattern).matchStrings(str, uriTemplateVariables); + } + + /** + * Build or retrieve an {@link AntPathStringMatcher} for the given pattern. + *

    The default implementation checks this AntPathMatcher's internal cache + * (see {@link #setCachePatterns}), creating a new AntPathStringMatcher instance + * if no cached copy is found. + *

    When encountering too many patterns to cache at runtime (the threshold is 65536), + * it turns the default cache off, assuming that arbitrary permutations of patterns + * are coming in, with little chance for encountering a recurring pattern. + *

    This method may be overridden to implement a custom cache strategy. + * @param pattern the pattern to match against (never {@code null}) + * @return a corresponding AntPathStringMatcher (never {@code null}) + * @see #setCachePatterns + */ + protected AntPathStringMatcher getStringMatcher(String pattern) { + AntPathStringMatcher matcher = null; + Boolean cachePatterns = this.cachePatterns; + if (cachePatterns == null || cachePatterns.booleanValue()) { + matcher = this.stringMatcherCache.get(pattern); + } + if (matcher == null) { + matcher = new AntPathStringMatcher(pattern, this.caseSensitive); + if (cachePatterns == null && this.stringMatcherCache.size() >= CACHE_TURNOFF_THRESHOLD) { + // Try to adapt to the runtime situation that we're encountering: + // There are obviously too many different patterns coming in here... + // So let's turn off the cache since the patterns are unlikely to be reoccurring. + deactivatePatternCache(); + return matcher; + } + if (cachePatterns == null || cachePatterns.booleanValue()) { + this.stringMatcherCache.put(pattern, matcher); + } + } + return matcher; + } + + /** + * Given a pattern and a full path, determine the pattern-mapped part.

    For example:

      + *
    • '{@code /docs/cvs/commit.html}' and '{@code /docs/cvs/commit.html} -> ''
    • + *
    • '{@code /docs/*}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'
    • + *
    • '{@code /docs/cvs/*.html}' and '{@code /docs/cvs/commit.html} -> '{@code commit.html}'
    • + *
    • '{@code /docs/**}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'
    • + *
    • '{@code /docs/**\/*.html}' and '{@code /docs/cvs/commit.html} -> '{@code cvs/commit.html}'
    • + *
    • '{@code /*.html}' and '{@code /docs/cvs/commit.html} -> '{@code docs/cvs/commit.html}'
    • + *
    • '{@code *.html}' and '{@code /docs/cvs/commit.html} -> '{@code /docs/cvs/commit.html}'
    • + *
    • '{@code *}' and '{@code /docs/cvs/commit.html} -> '{@code /docs/cvs/commit.html}'
    + *

    Assumes that {@link #match} returns {@code true} for '{@code pattern}' and '{@code path}', but + * does not enforce this. + */ + @Override + public String extractPathWithinPattern(String pattern, String path) { + String[] patternParts = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator, this.trimTokens, true); + String[] pathParts = StringUtils.tokenizeToStringArray(path, this.pathSeparator, this.trimTokens, true); + StringBuilder builder = new StringBuilder(); + boolean pathStarted = false; + + for (int segment = 0; segment < patternParts.length; segment++) { + String patternPart = patternParts[segment]; + if (patternPart.indexOf('*') > -1 || patternPart.indexOf('?') > -1) { + for (; segment < pathParts.length; segment++) { + if (pathStarted || (segment == 0 && !pattern.startsWith(this.pathSeparator))) { + builder.append(this.pathSeparator); + } + builder.append(pathParts[segment]); + pathStarted = true; + } + } + } + + return builder.toString(); + } + + @Override + public Map extractUriTemplateVariables(String pattern, String path) { + Map variables = new LinkedHashMap<>(); + boolean result = doMatch(pattern, path, true, variables); + if (!result) { + throw new IllegalStateException("Pattern \"" + pattern + "\" is not a match for \"" + path + "\""); + } + return variables; + } + + /** + * Combine two patterns into a new pattern. + *

    This implementation simply concatenates the two patterns, unless + * the first pattern contains a file extension match (e.g., {@code *.html}). + * In that case, the second pattern will be merged into the first. Otherwise, + * an {@code IllegalArgumentException} will be thrown. + *

    Examples

    + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    Pattern 1Pattern 2Result
    {@code null}{@code null} 
    /hotels{@code null}/hotels
    {@code null}/hotels/hotels
    /hotels/bookings/hotels/bookings
    /hotelsbookings/hotels/bookings
    /hotels/*/bookings/hotels/bookings
    /hotels/**/bookings/hotels/**/bookings
    /hotels{hotel}/hotels/{hotel}
    /hotels/*{hotel}/hotels/{hotel}
    /hotels/**{hotel}/hotels/**/{hotel}
    /*.html/hotels.html/hotels.html
    /*.html/hotels/hotels.html
    /*.html/*.txt{@code IllegalArgumentException}
    + * @param pattern1 the first pattern + * @param pattern2 the second pattern + * @return the combination of the two patterns + * @throws IllegalArgumentException if the two patterns cannot be combined + */ + @Override + public String combine(String pattern1, String pattern2) { + if (!StringUtils.hasText(pattern1) && !StringUtils.hasText(pattern2)) { + return ""; + } + if (!StringUtils.hasText(pattern1)) { + return pattern2; + } + if (!StringUtils.hasText(pattern2)) { + return pattern1; + } + + boolean pattern1ContainsUriVar = (pattern1.indexOf('{') != -1); + if (!pattern1.equals(pattern2) && !pattern1ContainsUriVar && match(pattern1, pattern2)) { + // /* + /hotel -> /hotel ; "/*.*" + "/*.html" -> /*.html + // However /user + /user -> /usr/user ; /{foo} + /bar -> /{foo}/bar + return pattern2; + } + + // /hotels/* + /booking -> /hotels/booking + // /hotels/* + booking -> /hotels/booking + if (pattern1.endsWith(this.pathSeparatorPatternCache.getEndsOnWildCard())) { + return concat(pattern1.substring(0, pattern1.length() - 2), pattern2); + } + + // /hotels/** + /booking -> /hotels/**/booking + // /hotels/** + booking -> /hotels/**/booking + if (pattern1.endsWith(this.pathSeparatorPatternCache.getEndsOnDoubleWildCard())) { + return concat(pattern1, pattern2); + } + + int starDotPos1 = pattern1.indexOf("*."); + if (pattern1ContainsUriVar || starDotPos1 == -1 || this.pathSeparator.equals(".")) { + // simply concatenate the two patterns + return concat(pattern1, pattern2); + } + + String ext1 = pattern1.substring(starDotPos1 + 1); + int dotPos2 = pattern2.indexOf('.'); + String file2 = (dotPos2 == -1 ? pattern2 : pattern2.substring(0, dotPos2)); + String ext2 = (dotPos2 == -1 ? "" : pattern2.substring(dotPos2)); + boolean ext1All = (ext1.equals(".*") || ext1.isEmpty()); + boolean ext2All = (ext2.equals(".*") || ext2.isEmpty()); + if (!ext1All && !ext2All) { + throw new IllegalArgumentException("Cannot combine patterns: " + pattern1 + " vs " + pattern2); + } + String ext = (ext1All ? ext2 : ext1); + return file2 + ext; + } + + private String concat(String path1, String path2) { + boolean path1EndsWithSeparator = path1.endsWith(this.pathSeparator); + boolean path2StartsWithSeparator = path2.startsWith(this.pathSeparator); + + if (path1EndsWithSeparator && path2StartsWithSeparator) { + return path1 + path2.substring(1); + } + else if (path1EndsWithSeparator || path2StartsWithSeparator) { + return path1 + path2; + } + else { + return path1 + this.pathSeparator + path2; + } + } + + /** + * Given a full path, returns a {@link Comparator} suitable for sorting patterns in order of + * explicitness. + *

    This {@code Comparator} will {@linkplain java.util.List#sort(Comparator) sort} + * a list so that more specific patterns (without URI templates or wild cards) come before + * generic patterns. So given a list with the following patterns, the returned comparator + * will sort this list so that the order will be as indicated. + *

      + *
    1. {@code /hotels/new}
    2. + *
    3. {@code /hotels/{hotel}}
    4. + *
    5. {@code /hotels/*}
    6. + *
    + *

    The full path given as parameter is used to test for exact matches. So when the given path + * is {@code /hotels/2}, the pattern {@code /hotels/2} will be sorted before {@code /hotels/1}. + * @param path the full path to use for comparison + * @return a comparator capable of sorting patterns in order of explicitness + */ + @Override + public Comparator getPatternComparator(String path) { + return new AntPatternComparator(path); + } + + + /** + * Tests whether or not a string matches against a pattern via a {@link Pattern}. + *

    The pattern may contain special characters: '*' means zero or more characters; '?' means one and + * only one character; '{' and '}' indicate a URI template pattern. For example /users/{user}. + */ + protected static class AntPathStringMatcher { + + private static final Pattern GLOB_PATTERN = Pattern.compile("\\?|\\*|\\{((?:\\{[^/]+?}|[^/{}]|\\\\[{}])+?)}"); + + private static final String DEFAULT_VARIABLE_PATTERN = "((?s).*)"; + + private final String rawPattern; + + private final boolean caseSensitive; + + private final boolean exactMatch; + + @Nullable + private final Pattern pattern; + + private final List variableNames = new ArrayList<>(); + + public AntPathStringMatcher(String pattern) { + this(pattern, true); + } + + public AntPathStringMatcher(String pattern, boolean caseSensitive) { + this.rawPattern = pattern; + this.caseSensitive = caseSensitive; + StringBuilder patternBuilder = new StringBuilder(); + Matcher matcher = GLOB_PATTERN.matcher(pattern); + int end = 0; + while (matcher.find()) { + patternBuilder.append(quote(pattern, end, matcher.start())); + String match = matcher.group(); + if ("?".equals(match)) { + patternBuilder.append('.'); + } + else if ("*".equals(match)) { + patternBuilder.append(".*"); + } + else if (match.startsWith("{") && match.endsWith("}")) { + int colonIdx = match.indexOf(':'); + if (colonIdx == -1) { + patternBuilder.append(DEFAULT_VARIABLE_PATTERN); + this.variableNames.add(matcher.group(1)); + } + else { + String variablePattern = match.substring(colonIdx + 1, match.length() - 1); + patternBuilder.append('('); + patternBuilder.append(variablePattern); + patternBuilder.append(')'); + String variableName = match.substring(1, colonIdx); + this.variableNames.add(variableName); + } + } + end = matcher.end(); + } + // No glob pattern was found, this is an exact String match + if (end == 0) { + this.exactMatch = true; + this.pattern = null; + } + else { + this.exactMatch = false; + patternBuilder.append(quote(pattern, end, pattern.length())); + this.pattern = (this.caseSensitive ? Pattern.compile(patternBuilder.toString()) : + Pattern.compile(patternBuilder.toString(), Pattern.CASE_INSENSITIVE)); + } + } + + private String quote(String s, int start, int end) { + if (start == end) { + return ""; + } + return Pattern.quote(s.substring(start, end)); + } + + /** + * Main entry point. + * @return {@code true} if the string matches against the pattern, or {@code false} otherwise. + */ + public boolean matchStrings(String str, @Nullable Map uriTemplateVariables) { + if (this.exactMatch) { + return this.caseSensitive ? this.rawPattern.equals(str) : this.rawPattern.equalsIgnoreCase(str); + } + else if (this.pattern != null) { + Matcher matcher = this.pattern.matcher(str); + if (matcher.matches()) { + if (uriTemplateVariables != null) { + if (this.variableNames.size() != matcher.groupCount()) { + throw new IllegalArgumentException("The number of capturing groups in the pattern segment " + + this.pattern + " does not match the number of URI template variables it defines, " + + "which can occur if capturing groups are used in a URI template regex. " + + "Use non-capturing groups instead."); + } + for (int i = 1; i <= matcher.groupCount(); i++) { + String name = this.variableNames.get(i - 1); + String value = matcher.group(i); + uriTemplateVariables.put(name, value); + } + } + return true; + } + } + return false; + } + + } + + + /** + * The default {@link Comparator} implementation returned by + * {@link #getPatternComparator(String)}. + *

    In order, the most "generic" pattern is determined by the following: + *

      + *
    • if it's null or a capture all pattern (i.e. it is equal to "/**")
    • + *
    • if the other pattern is an actual match
    • + *
    • if it's a catch-all pattern (i.e. it ends with "**"
    • + *
    • if it's got more "*" than the other pattern
    • + *
    • if it's got more "{foo}" than the other pattern
    • + *
    • if it's shorter than the other pattern
    • + *
    + */ + protected static class AntPatternComparator implements Comparator { + + private final String path; + + public AntPatternComparator(String path) { + this.path = path; + } + + /** + * Compare two patterns to determine which should match first, i.e. which + * is the most specific regarding the current path. + * @return a negative integer, zero, or a positive integer as pattern1 is + * more specific, equally specific, or less specific than pattern2. + */ + @Override + public int compare(String pattern1, String pattern2) { + PatternInfo info1 = new PatternInfo(pattern1); + PatternInfo info2 = new PatternInfo(pattern2); + + if (info1.isLeastSpecific() && info2.isLeastSpecific()) { + return 0; + } + else if (info1.isLeastSpecific()) { + return 1; + } + else if (info2.isLeastSpecific()) { + return -1; + } + + boolean pattern1EqualsPath = pattern1.equals(this.path); + boolean pattern2EqualsPath = pattern2.equals(this.path); + if (pattern1EqualsPath && pattern2EqualsPath) { + return 0; + } + else if (pattern1EqualsPath) { + return -1; + } + else if (pattern2EqualsPath) { + return 1; + } + + if (info1.isPrefixPattern() && info2.isPrefixPattern()) { + return info2.getLength() - info1.getLength(); + } + else if (info1.isPrefixPattern() && info2.getDoubleWildcards() == 0) { + return 1; + } + else if (info2.isPrefixPattern() && info1.getDoubleWildcards() == 0) { + return -1; + } + + if (info1.getTotalCount() != info2.getTotalCount()) { + return info1.getTotalCount() - info2.getTotalCount(); + } + + if (info1.getLength() != info2.getLength()) { + return info2.getLength() - info1.getLength(); + } + + if (info1.getSingleWildcards() < info2.getSingleWildcards()) { + return -1; + } + else if (info2.getSingleWildcards() < info1.getSingleWildcards()) { + return 1; + } + + if (info1.getUriVars() < info2.getUriVars()) { + return -1; + } + else if (info2.getUriVars() < info1.getUriVars()) { + return 1; + } + + return 0; + } + + + /** + * Value class that holds information about the pattern, e.g. number of + * occurrences of "*", "**", and "{" pattern elements. + */ + private static class PatternInfo { + + @Nullable + private final String pattern; + + private int uriVars; + + private int singleWildcards; + + private int doubleWildcards; + + private boolean catchAllPattern; + + private boolean prefixPattern; + + @Nullable + private Integer length; + + public PatternInfo(@Nullable String pattern) { + this.pattern = pattern; + if (this.pattern != null) { + initCounters(); + this.catchAllPattern = this.pattern.equals("/**"); + this.prefixPattern = !this.catchAllPattern && this.pattern.endsWith("/**"); + } + if (this.uriVars == 0) { + this.length = (this.pattern != null ? this.pattern.length() : 0); + } + } + + protected void initCounters() { + int pos = 0; + if (this.pattern != null) { + while (pos < this.pattern.length()) { + if (this.pattern.charAt(pos) == '{') { + this.uriVars++; + pos++; + } + else if (this.pattern.charAt(pos) == '*') { + if (pos + 1 < this.pattern.length() && this.pattern.charAt(pos + 1) == '*') { + this.doubleWildcards++; + pos += 2; + } + else if (pos > 0 && !this.pattern.substring(pos - 1).equals(".*")) { + this.singleWildcards++; + pos++; + } + else { + pos++; + } + } + else { + pos++; + } + } + } + } + + public int getUriVars() { + return this.uriVars; + } + + public int getSingleWildcards() { + return this.singleWildcards; + } + + public int getDoubleWildcards() { + return this.doubleWildcards; + } + + public boolean isLeastSpecific() { + return (this.pattern == null || this.catchAllPattern); + } + + public boolean isPrefixPattern() { + return this.prefixPattern; + } + + public int getTotalCount() { + return this.uriVars + this.singleWildcards + (2 * this.doubleWildcards); + } + + /** + * Returns the length of the given pattern, where template variables are considered to be 1 long. + */ + public int getLength() { + if (this.length == null) { + this.length = (this.pattern != null ? + VARIABLE_PATTERN.matcher(this.pattern).replaceAll("#").length() : 0); + } + return this.length; + } + } + } + + + /** + * A simple cache for patterns that depend on the configured path separator. + */ + private static class PathSeparatorPatternCache { + + private final String endsOnWildCard; + + private final String endsOnDoubleWildCard; + + public PathSeparatorPatternCache(String pathSeparator) { + this.endsOnWildCard = pathSeparator + "*"; + this.endsOnDoubleWildCard = pathSeparator + "**"; + } + + public String getEndsOnWildCard() { + return this.endsOnWildCard; + } + + public String getEndsOnDoubleWildCard() { + return this.endsOnDoubleWildCard; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/Assert.java b/spring-core/src/main/java/org/springframework/util/Assert.java new file mode 100644 index 0000000..fbba65f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/Assert.java @@ -0,0 +1,736 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.util.Collection; +import java.util.Map; +import java.util.function.Supplier; + +import org.springframework.lang.Nullable; + +/** + * Assertion utility class that assists in validating arguments. + * + *

    Useful for identifying programmer errors early and clearly at runtime. + * + *

    For example, if the contract of a public method states it does not + * allow {@code null} arguments, {@code Assert} can be used to validate that + * contract. Doing this clearly indicates a contract violation when it + * occurs and protects the class's invariants. + * + *

    Typically used to validate method arguments rather than configuration + * properties, to check for cases that are usually programmer errors rather + * than configuration errors. In contrast to configuration initialization + * code, there is usually no point in falling back to defaults in such methods. + * + *

    This class is similar to JUnit's assertion library. If an argument value is + * deemed invalid, an {@link IllegalArgumentException} is thrown (typically). + * For example: + * + *

    + * Assert.notNull(clazz, "The class must not be null");
    + * Assert.isTrue(i > 0, "The value must be greater than zero");
    + * + *

    Mainly for internal use within the framework; for a more comprehensive suite + * of assertion utilities consider {@code org.apache.commons.lang3.Validate} from + * Apache Commons Lang, + * Google Guava's + * Preconditions, + * or similar third-party libraries. + * + * @author Keith Donald + * @author Juergen Hoeller + * @author Sam Brannen + * @author Colin Sampaleanu + * @author Rob Harrop + * @since 1.1.2 + */ +public abstract class Assert { + + /** + * Assert a boolean expression, throwing an {@code IllegalStateException} + * if the expression evaluates to {@code false}. + *

    Call {@link #isTrue} if you wish to throw an {@code IllegalArgumentException} + * on an assertion failure. + *

    Assert.state(id == null, "The id property must not already be initialized");
    + * @param expression a boolean expression + * @param message the exception message to use if the assertion fails + * @throws IllegalStateException if {@code expression} is {@code false} + */ + public static void state(boolean expression, String message) { + if (!expression) { + throw new IllegalStateException(message); + } + } + + /** + * Assert a boolean expression, throwing an {@code IllegalStateException} + * if the expression evaluates to {@code false}. + *

    Call {@link #isTrue} if you wish to throw an {@code IllegalArgumentException} + * on an assertion failure. + *

    +	 * Assert.state(entity.getId() == null,
    +	 *     () -> "ID for entity " + entity.getName() + " must not already be initialized");
    +	 * 
    + * @param expression a boolean expression + * @param messageSupplier a supplier for the exception message to use if the + * assertion fails + * @throws IllegalStateException if {@code expression} is {@code false} + * @since 5.0 + */ + public static void state(boolean expression, Supplier messageSupplier) { + if (!expression) { + throw new IllegalStateException(nullSafeGet(messageSupplier)); + } + } + + /** + * Assert a boolean expression, throwing an {@code IllegalStateException} + * if the expression evaluates to {@code false}. + * @deprecated as of 4.3.7, in favor of {@link #state(boolean, String)} + */ + @Deprecated + public static void state(boolean expression) { + state(expression, "[Assertion failed] - this state invariant must be true"); + } + + /** + * Assert a boolean expression, throwing an {@code IllegalArgumentException} + * if the expression evaluates to {@code false}. + *
    Assert.isTrue(i > 0, "The value must be greater than zero");
    + * @param expression a boolean expression + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if {@code expression} is {@code false} + */ + public static void isTrue(boolean expression, String message) { + if (!expression) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert a boolean expression, throwing an {@code IllegalArgumentException} + * if the expression evaluates to {@code false}. + *
    +	 * Assert.isTrue(i > 0, () -> "The value '" + i + "' must be greater than zero");
    +	 * 
    + * @param expression a boolean expression + * @param messageSupplier a supplier for the exception message to use if the + * assertion fails + * @throws IllegalArgumentException if {@code expression} is {@code false} + * @since 5.0 + */ + public static void isTrue(boolean expression, Supplier messageSupplier) { + if (!expression) { + throw new IllegalArgumentException(nullSafeGet(messageSupplier)); + } + } + + /** + * Assert a boolean expression, throwing an {@code IllegalArgumentException} + * if the expression evaluates to {@code false}. + * @deprecated as of 4.3.7, in favor of {@link #isTrue(boolean, String)} + */ + @Deprecated + public static void isTrue(boolean expression) { + isTrue(expression, "[Assertion failed] - this expression must be true"); + } + + /** + * Assert that an object is {@code null}. + *
    Assert.isNull(value, "The value must be null");
    + * @param object the object to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the object is not {@code null} + */ + public static void isNull(@Nullable Object object, String message) { + if (object != null) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that an object is {@code null}. + *
    +	 * Assert.isNull(value, () -> "The value '" + value + "' must be null");
    +	 * 
    + * @param object the object to check + * @param messageSupplier a supplier for the exception message to use if the + * assertion fails + * @throws IllegalArgumentException if the object is not {@code null} + * @since 5.0 + */ + public static void isNull(@Nullable Object object, Supplier messageSupplier) { + if (object != null) { + throw new IllegalArgumentException(nullSafeGet(messageSupplier)); + } + } + + /** + * Assert that an object is {@code null}. + * @deprecated as of 4.3.7, in favor of {@link #isNull(Object, String)} + */ + @Deprecated + public static void isNull(@Nullable Object object) { + isNull(object, "[Assertion failed] - the object argument must be null"); + } + + /** + * Assert that an object is not {@code null}. + *
    Assert.notNull(clazz, "The class must not be null");
    + * @param object the object to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the object is {@code null} + */ + public static void notNull(@Nullable Object object, String message) { + if (object == null) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that an object is not {@code null}. + *
    +	 * Assert.notNull(entity.getId(),
    +	 *     () -> "ID for entity " + entity.getName() + " must not be null");
    +	 * 
    + * @param object the object to check + * @param messageSupplier a supplier for the exception message to use if the + * assertion fails + * @throws IllegalArgumentException if the object is {@code null} + * @since 5.0 + */ + public static void notNull(@Nullable Object object, Supplier messageSupplier) { + if (object == null) { + throw new IllegalArgumentException(nullSafeGet(messageSupplier)); + } + } + + /** + * Assert that an object is not {@code null}. + * @deprecated as of 4.3.7, in favor of {@link #notNull(Object, String)} + */ + @Deprecated + public static void notNull(@Nullable Object object) { + notNull(object, "[Assertion failed] - this argument is required; it must not be null"); + } + + /** + * Assert that the given String is not empty; that is, + * it must not be {@code null} and not the empty String. + *
    Assert.hasLength(name, "Name must not be empty");
    + * @param text the String to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the text is empty + * @see StringUtils#hasLength + */ + public static void hasLength(@Nullable String text, String message) { + if (!StringUtils.hasLength(text)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that the given String is not empty; that is, + * it must not be {@code null} and not the empty String. + *
    +	 * Assert.hasLength(account.getName(),
    +	 *     () -> "Name for account '" + account.getId() + "' must not be empty");
    +	 * 
    + * @param text the String to check + * @param messageSupplier a supplier for the exception message to use if the + * assertion fails + * @throws IllegalArgumentException if the text is empty + * @since 5.0 + * @see StringUtils#hasLength + */ + public static void hasLength(@Nullable String text, Supplier messageSupplier) { + if (!StringUtils.hasLength(text)) { + throw new IllegalArgumentException(nullSafeGet(messageSupplier)); + } + } + + /** + * Assert that the given String is not empty; that is, + * it must not be {@code null} and not the empty String. + * @deprecated as of 4.3.7, in favor of {@link #hasLength(String, String)} + */ + @Deprecated + public static void hasLength(@Nullable String text) { + hasLength(text, + "[Assertion failed] - this String argument must have length; it must not be null or empty"); + } + + /** + * Assert that the given String contains valid text content; that is, it must not + * be {@code null} and must contain at least one non-whitespace character. + *
    Assert.hasText(name, "'name' must not be empty");
    + * @param text the String to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the text does not contain valid text content + * @see StringUtils#hasText + */ + public static void hasText(@Nullable String text, String message) { + if (!StringUtils.hasText(text)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that the given String contains valid text content; that is, it must not + * be {@code null} and must contain at least one non-whitespace character. + *
    +	 * Assert.hasText(account.getName(),
    +	 *     () -> "Name for account '" + account.getId() + "' must not be empty");
    +	 * 
    + * @param text the String to check + * @param messageSupplier a supplier for the exception message to use if the + * assertion fails + * @throws IllegalArgumentException if the text does not contain valid text content + * @since 5.0 + * @see StringUtils#hasText + */ + public static void hasText(@Nullable String text, Supplier messageSupplier) { + if (!StringUtils.hasText(text)) { + throw new IllegalArgumentException(nullSafeGet(messageSupplier)); + } + } + + /** + * Assert that the given String contains valid text content; that is, it must not + * be {@code null} and must contain at least one non-whitespace character. + * @deprecated as of 4.3.7, in favor of {@link #hasText(String, String)} + */ + @Deprecated + public static void hasText(@Nullable String text) { + hasText(text, + "[Assertion failed] - this String argument must have text; it must not be null, empty, or blank"); + } + + /** + * Assert that the given text does not contain the given substring. + *
    Assert.doesNotContain(name, "rod", "Name must not contain 'rod'");
    + * @param textToSearch the text to search + * @param substring the substring to find within the text + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the text contains the substring + */ + public static void doesNotContain(@Nullable String textToSearch, String substring, String message) { + if (StringUtils.hasLength(textToSearch) && StringUtils.hasLength(substring) && + textToSearch.contains(substring)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that the given text does not contain the given substring. + *
    +	 * Assert.doesNotContain(name, forbidden, () -> "Name must not contain '" + forbidden + "'");
    +	 * 
    + * @param textToSearch the text to search + * @param substring the substring to find within the text + * @param messageSupplier a supplier for the exception message to use if the + * assertion fails + * @throws IllegalArgumentException if the text contains the substring + * @since 5.0 + */ + public static void doesNotContain(@Nullable String textToSearch, String substring, Supplier messageSupplier) { + if (StringUtils.hasLength(textToSearch) && StringUtils.hasLength(substring) && + textToSearch.contains(substring)) { + throw new IllegalArgumentException(nullSafeGet(messageSupplier)); + } + } + + /** + * Assert that the given text does not contain the given substring. + * @deprecated as of 4.3.7, in favor of {@link #doesNotContain(String, String, String)} + */ + @Deprecated + public static void doesNotContain(@Nullable String textToSearch, String substring) { + doesNotContain(textToSearch, substring, + () -> "[Assertion failed] - this String argument must not contain the substring [" + substring + "]"); + } + + /** + * Assert that an array contains elements; that is, it must not be + * {@code null} and must contain at least one element. + *
    Assert.notEmpty(array, "The array must contain elements");
    + * @param array the array to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the object array is {@code null} or contains no elements + */ + public static void notEmpty(@Nullable Object[] array, String message) { + if (ObjectUtils.isEmpty(array)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that an array contains elements; that is, it must not be + * {@code null} and must contain at least one element. + *
    +	 * Assert.notEmpty(array, () -> "The " + arrayType + " array must contain elements");
    +	 * 
    + * @param array the array to check + * @param messageSupplier a supplier for the exception message to use if the + * assertion fails + * @throws IllegalArgumentException if the object array is {@code null} or contains no elements + * @since 5.0 + */ + public static void notEmpty(@Nullable Object[] array, Supplier messageSupplier) { + if (ObjectUtils.isEmpty(array)) { + throw new IllegalArgumentException(nullSafeGet(messageSupplier)); + } + } + + /** + * Assert that an array contains elements; that is, it must not be + * {@code null} and must contain at least one element. + * @deprecated as of 4.3.7, in favor of {@link #notEmpty(Object[], String)} + */ + @Deprecated + public static void notEmpty(@Nullable Object[] array) { + notEmpty(array, "[Assertion failed] - this array must not be empty: it must contain at least 1 element"); + } + + /** + * Assert that an array contains no {@code null} elements. + *

    Note: Does not complain if the array is empty! + *

    Assert.noNullElements(array, "The array must contain non-null elements");
    + * @param array the array to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the object array contains a {@code null} element + */ + public static void noNullElements(@Nullable Object[] array, String message) { + if (array != null) { + for (Object element : array) { + if (element == null) { + throw new IllegalArgumentException(message); + } + } + } + } + + /** + * Assert that an array contains no {@code null} elements. + *

    Note: Does not complain if the array is empty! + *

    +	 * Assert.noNullElements(array, () -> "The " + arrayType + " array must contain non-null elements");
    +	 * 
    + * @param array the array to check + * @param messageSupplier a supplier for the exception message to use if the + * assertion fails + * @throws IllegalArgumentException if the object array contains a {@code null} element + * @since 5.0 + */ + public static void noNullElements(@Nullable Object[] array, Supplier messageSupplier) { + if (array != null) { + for (Object element : array) { + if (element == null) { + throw new IllegalArgumentException(nullSafeGet(messageSupplier)); + } + } + } + } + + /** + * Assert that an array contains no {@code null} elements. + * @deprecated as of 4.3.7, in favor of {@link #noNullElements(Object[], String)} + */ + @Deprecated + public static void noNullElements(@Nullable Object[] array) { + noNullElements(array, "[Assertion failed] - this array must not contain any null elements"); + } + + /** + * Assert that a collection contains elements; that is, it must not be + * {@code null} and must contain at least one element. + *
    Assert.notEmpty(collection, "Collection must contain elements");
    + * @param collection the collection to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the collection is {@code null} or + * contains no elements + */ + public static void notEmpty(@Nullable Collection collection, String message) { + if (CollectionUtils.isEmpty(collection)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that a collection contains elements; that is, it must not be + * {@code null} and must contain at least one element. + *
    +	 * Assert.notEmpty(collection, () -> "The " + collectionType + " collection must contain elements");
    +	 * 
    + * @param collection the collection to check + * @param messageSupplier a supplier for the exception message to use if the + * assertion fails + * @throws IllegalArgumentException if the collection is {@code null} or + * contains no elements + * @since 5.0 + */ + public static void notEmpty(@Nullable Collection collection, Supplier messageSupplier) { + if (CollectionUtils.isEmpty(collection)) { + throw new IllegalArgumentException(nullSafeGet(messageSupplier)); + } + } + + /** + * Assert that a collection contains elements; that is, it must not be + * {@code null} and must contain at least one element. + * @deprecated as of 4.3.7, in favor of {@link #notEmpty(Collection, String)} + */ + @Deprecated + public static void notEmpty(@Nullable Collection collection) { + notEmpty(collection, + "[Assertion failed] - this collection must not be empty: it must contain at least 1 element"); + } + + /** + * Assert that a collection contains no {@code null} elements. + *

    Note: Does not complain if the collection is empty! + *

    Assert.noNullElements(collection, "Collection must contain non-null elements");
    + * @param collection the collection to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the collection contains a {@code null} element + * @since 5.2 + */ + public static void noNullElements(@Nullable Collection collection, String message) { + if (collection != null) { + for (Object element : collection) { + if (element == null) { + throw new IllegalArgumentException(message); + } + } + } + } + + /** + * Assert that a collection contains no {@code null} elements. + *

    Note: Does not complain if the collection is empty! + *

    +	 * Assert.noNullElements(collection, () -> "Collection " + collectionName + " must contain non-null elements");
    +	 * 
    + * @param collection the collection to check + * @param messageSupplier a supplier for the exception message to use if the + * assertion fails + * @throws IllegalArgumentException if the collection contains a {@code null} element + * @since 5.2 + */ + public static void noNullElements(@Nullable Collection collection, Supplier messageSupplier) { + if (collection != null) { + for (Object element : collection) { + if (element == null) { + throw new IllegalArgumentException(nullSafeGet(messageSupplier)); + } + } + } + } + + /** + * Assert that a Map contains entries; that is, it must not be {@code null} + * and must contain at least one entry. + *
    Assert.notEmpty(map, "Map must contain entries");
    + * @param map the map to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the map is {@code null} or contains no entries + */ + public static void notEmpty(@Nullable Map map, String message) { + if (CollectionUtils.isEmpty(map)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that a Map contains entries; that is, it must not be {@code null} + * and must contain at least one entry. + *
    +	 * Assert.notEmpty(map, () -> "The " + mapType + " map must contain entries");
    +	 * 
    + * @param map the map to check + * @param messageSupplier a supplier for the exception message to use if the + * assertion fails + * @throws IllegalArgumentException if the map is {@code null} or contains no entries + * @since 5.0 + */ + public static void notEmpty(@Nullable Map map, Supplier messageSupplier) { + if (CollectionUtils.isEmpty(map)) { + throw new IllegalArgumentException(nullSafeGet(messageSupplier)); + } + } + + /** + * Assert that a Map contains entries; that is, it must not be {@code null} + * and must contain at least one entry. + * @deprecated as of 4.3.7, in favor of {@link #notEmpty(Map, String)} + */ + @Deprecated + public static void notEmpty(@Nullable Map map) { + notEmpty(map, "[Assertion failed] - this map must not be empty; it must contain at least one entry"); + } + + /** + * Assert that the provided object is an instance of the provided class. + *
    Assert.instanceOf(Foo.class, foo, "Foo expected");
    + * @param type the type to check against + * @param obj the object to check + * @param message a message which will be prepended to provide further context. + * If it is empty or ends in ":" or ";" or "," or ".", a full exception message + * will be appended. If it ends in a space, the name of the offending object's + * type will be appended. In any other case, a ":" with a space and the name + * of the offending object's type will be appended. + * @throws IllegalArgumentException if the object is not an instance of type + */ + public static void isInstanceOf(Class type, @Nullable Object obj, String message) { + notNull(type, "Type to check against must not be null"); + if (!type.isInstance(obj)) { + instanceCheckFailed(type, obj, message); + } + } + + /** + * Assert that the provided object is an instance of the provided class. + *
    +	 * Assert.instanceOf(Foo.class, foo, () -> "Processing " + Foo.class.getSimpleName() + ":");
    +	 * 
    + * @param type the type to check against + * @param obj the object to check + * @param messageSupplier a supplier for the exception message to use if the + * assertion fails. See {@link #isInstanceOf(Class, Object, String)} for details. + * @throws IllegalArgumentException if the object is not an instance of type + * @since 5.0 + */ + public static void isInstanceOf(Class type, @Nullable Object obj, Supplier messageSupplier) { + notNull(type, "Type to check against must not be null"); + if (!type.isInstance(obj)) { + instanceCheckFailed(type, obj, nullSafeGet(messageSupplier)); + } + } + + /** + * Assert that the provided object is an instance of the provided class. + *
    Assert.instanceOf(Foo.class, foo);
    + * @param type the type to check against + * @param obj the object to check + * @throws IllegalArgumentException if the object is not an instance of type + */ + public static void isInstanceOf(Class type, @Nullable Object obj) { + isInstanceOf(type, obj, ""); + } + + /** + * Assert that {@code superType.isAssignableFrom(subType)} is {@code true}. + *
    Assert.isAssignable(Number.class, myClass, "Number expected");
    + * @param superType the super type to check against + * @param subType the sub type to check + * @param message a message which will be prepended to provide further context. + * If it is empty or ends in ":" or ";" or "," or ".", a full exception message + * will be appended. If it ends in a space, the name of the offending sub type + * will be appended. In any other case, a ":" with a space and the name of the + * offending sub type will be appended. + * @throws IllegalArgumentException if the classes are not assignable + */ + public static void isAssignable(Class superType, @Nullable Class subType, String message) { + notNull(superType, "Super type to check against must not be null"); + if (subType == null || !superType.isAssignableFrom(subType)) { + assignableCheckFailed(superType, subType, message); + } + } + + /** + * Assert that {@code superType.isAssignableFrom(subType)} is {@code true}. + *
    +	 * Assert.isAssignable(Number.class, myClass, () -> "Processing " + myAttributeName + ":");
    +	 * 
    + * @param superType the super type to check against + * @param subType the sub type to check + * @param messageSupplier a supplier for the exception message to use if the + * assertion fails. See {@link #isAssignable(Class, Class, String)} for details. + * @throws IllegalArgumentException if the classes are not assignable + * @since 5.0 + */ + public static void isAssignable(Class superType, @Nullable Class subType, Supplier messageSupplier) { + notNull(superType, "Super type to check against must not be null"); + if (subType == null || !superType.isAssignableFrom(subType)) { + assignableCheckFailed(superType, subType, nullSafeGet(messageSupplier)); + } + } + + /** + * Assert that {@code superType.isAssignableFrom(subType)} is {@code true}. + *
    Assert.isAssignable(Number.class, myClass);
    + * @param superType the super type to check + * @param subType the sub type to check + * @throws IllegalArgumentException if the classes are not assignable + */ + public static void isAssignable(Class superType, Class subType) { + isAssignable(superType, subType, ""); + } + + + private static void instanceCheckFailed(Class type, @Nullable Object obj, @Nullable String msg) { + String className = (obj != null ? obj.getClass().getName() : "null"); + String result = ""; + boolean defaultMessage = true; + if (StringUtils.hasLength(msg)) { + if (endsWithSeparator(msg)) { + result = msg + " "; + } + else { + result = messageWithTypeName(msg, className); + defaultMessage = false; + } + } + if (defaultMessage) { + result = result + ("Object of class [" + className + "] must be an instance of " + type); + } + throw new IllegalArgumentException(result); + } + + private static void assignableCheckFailed(Class superType, @Nullable Class subType, @Nullable String msg) { + String result = ""; + boolean defaultMessage = true; + if (StringUtils.hasLength(msg)) { + if (endsWithSeparator(msg)) { + result = msg + " "; + } + else { + result = messageWithTypeName(msg, subType); + defaultMessage = false; + } + } + if (defaultMessage) { + result = result + (subType + " is not assignable to " + superType); + } + throw new IllegalArgumentException(result); + } + + private static boolean endsWithSeparator(String msg) { + return (msg.endsWith(":") || msg.endsWith(";") || msg.endsWith(",") || msg.endsWith(".")); + } + + private static String messageWithTypeName(String msg, @Nullable Object typeName) { + return msg + (msg.endsWith(" ") ? "" : ": ") + typeName; + } + + @Nullable + private static String nullSafeGet(@Nullable Supplier messageSupplier) { + return (messageSupplier != null ? messageSupplier.get() : null); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/AutoPopulatingList.java b/spring-core/src/main/java/org/springframework/util/AutoPopulatingList.java new file mode 100644 index 0000000..ff966cd --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/AutoPopulatingList.java @@ -0,0 +1,319 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +import org.springframework.lang.Nullable; + +/** + * Simple {@link List} wrapper class that allows for elements to be + * automatically populated as they are requested. This is particularly + * useful for data binding to {@link List Lists}, allowing for elements + * to be created and added to the {@link List} in a "just in time" fashion. + * + *

    Note: This class is not thread-safe. To create a thread-safe version, + * use the {@link java.util.Collections#synchronizedList} utility methods. + * + *

    Inspired by {@code LazyList} from Commons Collections. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.0 + * @param the element type + */ +@SuppressWarnings("serial") +public class AutoPopulatingList implements List, Serializable { + + /** + * The {@link List} that all operations are eventually delegated to. + */ + private final List backingList; + + /** + * The {@link ElementFactory} to use to create new {@link List} elements + * on demand. + */ + private final ElementFactory elementFactory; + + + /** + * Creates a new {@code AutoPopulatingList} that is backed by a standard + * {@link ArrayList} and adds new instances of the supplied {@link Class element Class} + * to the backing {@link List} on demand. + */ + public AutoPopulatingList(Class elementClass) { + this(new ArrayList<>(), elementClass); + } + + /** + * Creates a new {@code AutoPopulatingList} that is backed by the supplied {@link List} + * and adds new instances of the supplied {@link Class element Class} to the backing + * {@link List} on demand. + */ + public AutoPopulatingList(List backingList, Class elementClass) { + this(backingList, new ReflectiveElementFactory<>(elementClass)); + } + + /** + * Creates a new {@code AutoPopulatingList} that is backed by a standard + * {@link ArrayList} and creates new elements on demand using the supplied {@link ElementFactory}. + */ + public AutoPopulatingList(ElementFactory elementFactory) { + this(new ArrayList<>(), elementFactory); + } + + /** + * Creates a new {@code AutoPopulatingList} that is backed by the supplied {@link List} + * and creates new elements on demand using the supplied {@link ElementFactory}. + */ + public AutoPopulatingList(List backingList, ElementFactory elementFactory) { + Assert.notNull(backingList, "Backing List must not be null"); + Assert.notNull(elementFactory, "Element factory must not be null"); + this.backingList = backingList; + this.elementFactory = elementFactory; + } + + + @Override + public void add(int index, E element) { + this.backingList.add(index, element); + } + + @Override + public boolean add(E o) { + return this.backingList.add(o); + } + + @Override + public boolean addAll(Collection c) { + return this.backingList.addAll(c); + } + + @Override + public boolean addAll(int index, Collection c) { + return this.backingList.addAll(index, c); + } + + @Override + public void clear() { + this.backingList.clear(); + } + + @Override + public boolean contains(Object o) { + return this.backingList.contains(o); + } + + @Override + public boolean containsAll(Collection c) { + return this.backingList.containsAll(c); + } + + /** + * Get the element at the supplied index, creating it if there is + * no element at that index. + */ + @Override + public E get(int index) { + int backingListSize = this.backingList.size(); + E element = null; + if (index < backingListSize) { + element = this.backingList.get(index); + if (element == null) { + element = this.elementFactory.createElement(index); + this.backingList.set(index, element); + } + } + else { + for (int x = backingListSize; x < index; x++) { + this.backingList.add(null); + } + element = this.elementFactory.createElement(index); + this.backingList.add(element); + } + return element; + } + + @Override + public int indexOf(Object o) { + return this.backingList.indexOf(o); + } + + @Override + public boolean isEmpty() { + return this.backingList.isEmpty(); + } + + @Override + public Iterator iterator() { + return this.backingList.iterator(); + } + + @Override + public int lastIndexOf(Object o) { + return this.backingList.lastIndexOf(o); + } + + @Override + public ListIterator listIterator() { + return this.backingList.listIterator(); + } + + @Override + public ListIterator listIterator(int index) { + return this.backingList.listIterator(index); + } + + @Override + public E remove(int index) { + return this.backingList.remove(index); + } + + @Override + public boolean remove(Object o) { + return this.backingList.remove(o); + } + + @Override + public boolean removeAll(Collection c) { + return this.backingList.removeAll(c); + } + + @Override + public boolean retainAll(Collection c) { + return this.backingList.retainAll(c); + } + + @Override + public E set(int index, E element) { + return this.backingList.set(index, element); + } + + @Override + public int size() { + return this.backingList.size(); + } + + @Override + public List subList(int fromIndex, int toIndex) { + return this.backingList.subList(fromIndex, toIndex); + } + + @Override + public Object[] toArray() { + return this.backingList.toArray(); + } + + @Override + public T[] toArray(T[] a) { + return this.backingList.toArray(a); + } + + + @Override + public boolean equals(@Nullable Object other) { + return this.backingList.equals(other); + } + + @Override + public int hashCode() { + return this.backingList.hashCode(); + } + + + /** + * Factory interface for creating elements for an index-based access + * data structure such as a {@link java.util.List}. + * + * @param the element type + */ + @FunctionalInterface + public interface ElementFactory { + + /** + * Create the element for the supplied index. + * @return the element object + * @throws ElementInstantiationException if the instantiation process failed + * (any exception thrown by a target constructor should be propagated as-is) + */ + E createElement(int index) throws ElementInstantiationException; + } + + + /** + * Exception to be thrown from ElementFactory. + */ + public static class ElementInstantiationException extends RuntimeException { + + public ElementInstantiationException(String msg) { + super(msg); + } + + public ElementInstantiationException(String message, Throwable cause) { + super(message, cause); + } + } + + + /** + * Reflective implementation of the ElementFactory interface, using + * {@code Class.getDeclaredConstructor().newInstance()} on a given element class. + */ + private static class ReflectiveElementFactory implements ElementFactory, Serializable { + + private final Class elementClass; + + public ReflectiveElementFactory(Class elementClass) { + Assert.notNull(elementClass, "Element class must not be null"); + Assert.isTrue(!elementClass.isInterface(), "Element class must not be an interface type"); + Assert.isTrue(!Modifier.isAbstract(elementClass.getModifiers()), "Element class cannot be an abstract class"); + this.elementClass = elementClass; + } + + @Override + public E createElement(int index) { + try { + return ReflectionUtils.accessibleConstructor(this.elementClass).newInstance(); + } + catch (NoSuchMethodException ex) { + throw new ElementInstantiationException( + "No default constructor on element class: " + this.elementClass.getName(), ex); + } + catch (InstantiationException ex) { + throw new ElementInstantiationException( + "Unable to instantiate element class: " + this.elementClass.getName(), ex); + } + catch (IllegalAccessException ex) { + throw new ElementInstantiationException( + "Could not access element constructor: " + this.elementClass.getName(), ex); + } + catch (InvocationTargetException ex) { + throw new ElementInstantiationException( + "Failed to invoke element constructor: " + this.elementClass.getName(), ex.getTargetException()); + } + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/Base64Utils.java b/spring-core/src/main/java/org/springframework/util/Base64Utils.java new file mode 100644 index 0000000..64121e2 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/Base64Utils.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * A simple utility class for Base64 encoding and decoding. + * + *

    Adapts to Java 8's {@link java.util.Base64} in a convenience fashion. + * + * @author Juergen Hoeller + * @author Gary Russell + * @since 4.1 + * @see java.util.Base64 + */ +public abstract class Base64Utils { + + private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + + + /** + * Base64-encode the given byte array. + * @param src the original byte array + * @return the encoded byte array + */ + public static byte[] encode(byte[] src) { + if (src.length == 0) { + return src; + } + return Base64.getEncoder().encode(src); + } + + /** + * Base64-decode the given byte array. + * @param src the encoded byte array + * @return the original byte array + */ + public static byte[] decode(byte[] src) { + if (src.length == 0) { + return src; + } + return Base64.getDecoder().decode(src); + } + + /** + * Base64-encode the given byte array using the RFC 4648 + * "URL and Filename Safe Alphabet". + * @param src the original byte array + * @return the encoded byte array + * @since 4.2.4 + */ + public static byte[] encodeUrlSafe(byte[] src) { + if (src.length == 0) { + return src; + } + return Base64.getUrlEncoder().encode(src); + } + + /** + * Base64-decode the given byte array using the RFC 4648 + * "URL and Filename Safe Alphabet". + * @param src the encoded byte array + * @return the original byte array + * @since 4.2.4 + */ + public static byte[] decodeUrlSafe(byte[] src) { + if (src.length == 0) { + return src; + } + return Base64.getUrlDecoder().decode(src); + } + + /** + * Base64-encode the given byte array to a String. + * @param src the original byte array + * @return the encoded byte array as a UTF-8 String + */ + public static String encodeToString(byte[] src) { + if (src.length == 0) { + return ""; + } + return new String(encode(src), DEFAULT_CHARSET); + } + + /** + * Base64-decode the given byte array from an UTF-8 String. + * @param src the encoded UTF-8 String + * @return the original byte array + */ + public static byte[] decodeFromString(String src) { + if (src.isEmpty()) { + return new byte[0]; + } + return decode(src.getBytes(DEFAULT_CHARSET)); + } + + /** + * Base64-encode the given byte array to a String using the RFC 4648 + * "URL and Filename Safe Alphabet". + * @param src the original byte array + * @return the encoded byte array as a UTF-8 String + */ + public static String encodeToUrlSafeString(byte[] src) { + return new String(encodeUrlSafe(src), DEFAULT_CHARSET); + } + + /** + * Base64-decode the given byte array from an UTF-8 String using the RFC 4648 + * "URL and Filename Safe Alphabet". + * @param src the encoded UTF-8 String + * @return the original byte array + */ + public static byte[] decodeFromUrlSafeString(String src) { + return decodeUrlSafe(src.getBytes(DEFAULT_CHARSET)); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/ClassUtils.java b/spring-core/src/main/java/org/springframework/util/ClassUtils.java new file mode 100644 index 0000000..e570646 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/ClassUtils.java @@ -0,0 +1,1395 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.beans.Introspector; +import java.io.Closeable; +import java.io.Externalizable; +import java.io.Serializable; +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.StringJoiner; + +import org.springframework.lang.Nullable; + +/** + * Miscellaneous {@code java.lang.Class} utility methods. + * Mainly for internal use within the framework. + * + * @author Juergen Hoeller + * @author Keith Donald + * @author Rob Harrop + * @author Sam Brannen + * @since 1.1 + * @see TypeUtils + * @see ReflectionUtils + */ +public abstract class ClassUtils { + + /** Suffix for array class names: {@code "[]"}. */ + public static final String ARRAY_SUFFIX = "[]"; + + /** Prefix for internal array class names: {@code "["}. */ + private static final String INTERNAL_ARRAY_PREFIX = "["; + + /** Prefix for internal non-primitive array class names: {@code "[L"}. */ + private static final String NON_PRIMITIVE_ARRAY_PREFIX = "[L"; + + /** A reusable empty class array constant. */ + private static final Class[] EMPTY_CLASS_ARRAY = {}; + + /** The package separator character: {@code '.'}. */ + private static final char PACKAGE_SEPARATOR = '.'; + + /** The path separator character: {@code '/'}. */ + private static final char PATH_SEPARATOR = '/'; + + /** The inner class separator character: {@code '$'}. */ + private static final char INNER_CLASS_SEPARATOR = '$'; + + /** The CGLIB class separator: {@code "$$"}. */ + public static final String CGLIB_CLASS_SEPARATOR = "$$"; + + /** The ".class" file suffix. */ + public static final String CLASS_FILE_SUFFIX = ".class"; + + + /** + * Map with primitive wrapper type as key and corresponding primitive + * type as value, for example: Integer.class -> int.class. + */ + private static final Map, Class> primitiveWrapperTypeMap = new IdentityHashMap<>(8); + + /** + * Map with primitive type as key and corresponding wrapper + * type as value, for example: int.class -> Integer.class. + */ + private static final Map, Class> primitiveTypeToWrapperMap = new IdentityHashMap<>(8); + + /** + * Map with primitive type name as key and corresponding primitive + * type as value, for example: "int" -> "int.class". + */ + private static final Map> primitiveTypeNameMap = new HashMap<>(32); + + /** + * Map with common Java language class name as key and corresponding Class as value. + * Primarily for efficient deserialization of remote invocations. + */ + private static final Map> commonClassCache = new HashMap<>(64); + + /** + * Common Java language interfaces which are supposed to be ignored + * when searching for 'primary' user-level interfaces. + */ + private static final Set> javaLanguageInterfaces; + + /** + * Cache for equivalent methods on an interface implemented by the declaring class. + */ + private static final Map interfaceMethodCache = new ConcurrentReferenceHashMap<>(256); + + + static { + primitiveWrapperTypeMap.put(Boolean.class, boolean.class); + primitiveWrapperTypeMap.put(Byte.class, byte.class); + primitiveWrapperTypeMap.put(Character.class, char.class); + primitiveWrapperTypeMap.put(Double.class, double.class); + primitiveWrapperTypeMap.put(Float.class, float.class); + primitiveWrapperTypeMap.put(Integer.class, int.class); + primitiveWrapperTypeMap.put(Long.class, long.class); + primitiveWrapperTypeMap.put(Short.class, short.class); + primitiveWrapperTypeMap.put(Void.class, void.class); + + // Map entry iteration is less expensive to initialize than forEach with lambdas + for (Map.Entry, Class> entry : primitiveWrapperTypeMap.entrySet()) { + primitiveTypeToWrapperMap.put(entry.getValue(), entry.getKey()); + registerCommonClasses(entry.getKey()); + } + + Set> primitiveTypes = new HashSet<>(32); + primitiveTypes.addAll(primitiveWrapperTypeMap.values()); + Collections.addAll(primitiveTypes, boolean[].class, byte[].class, char[].class, + double[].class, float[].class, int[].class, long[].class, short[].class); + for (Class primitiveType : primitiveTypes) { + primitiveTypeNameMap.put(primitiveType.getName(), primitiveType); + } + + registerCommonClasses(Boolean[].class, Byte[].class, Character[].class, Double[].class, + Float[].class, Integer[].class, Long[].class, Short[].class); + registerCommonClasses(Number.class, Number[].class, String.class, String[].class, + Class.class, Class[].class, Object.class, Object[].class); + registerCommonClasses(Throwable.class, Exception.class, RuntimeException.class, + Error.class, StackTraceElement.class, StackTraceElement[].class); + registerCommonClasses(Enum.class, Iterable.class, Iterator.class, Enumeration.class, + Collection.class, List.class, Set.class, Map.class, Map.Entry.class, Optional.class); + + Class[] javaLanguageInterfaceArray = {Serializable.class, Externalizable.class, + Closeable.class, AutoCloseable.class, Cloneable.class, Comparable.class}; + registerCommonClasses(javaLanguageInterfaceArray); + javaLanguageInterfaces = new HashSet<>(Arrays.asList(javaLanguageInterfaceArray)); + } + + + /** + * Register the given common classes with the ClassUtils cache. + */ + private static void registerCommonClasses(Class... commonClasses) { + for (Class clazz : commonClasses) { + commonClassCache.put(clazz.getName(), clazz); + } + } + + /** + * Return the default ClassLoader to use: typically the thread context + * ClassLoader, if available; the ClassLoader that loaded the ClassUtils + * class will be used as fallback. + *

    Call this method if you intend to use the thread context ClassLoader + * in a scenario where you clearly prefer a non-null ClassLoader reference: + * for example, for class path resource loading (but not necessarily for + * {@code Class.forName}, which accepts a {@code null} ClassLoader + * reference as well). + * @return the default ClassLoader (only {@code null} if even the system + * ClassLoader isn't accessible) + * @see Thread#getContextClassLoader() + * @see ClassLoader#getSystemClassLoader() + */ + @Nullable + public static ClassLoader getDefaultClassLoader() { + ClassLoader cl = null; + try { + cl = Thread.currentThread().getContextClassLoader(); + } + catch (Throwable ex) { + // Cannot access thread context ClassLoader - falling back... + } + if (cl == null) { + // No thread context class loader -> use class loader of this class. + cl = ClassUtils.class.getClassLoader(); + if (cl == null) { + // getClassLoader() returning null indicates the bootstrap ClassLoader + try { + cl = ClassLoader.getSystemClassLoader(); + } + catch (Throwable ex) { + // Cannot access system ClassLoader - oh well, maybe the caller can live with null... + } + } + } + return cl; + } + + /** + * Override the thread context ClassLoader with the environment's bean ClassLoader + * if necessary, i.e. if the bean ClassLoader is not equivalent to the thread + * context ClassLoader already. + * @param classLoaderToUse the actual ClassLoader to use for the thread context + * @return the original thread context ClassLoader, or {@code null} if not overridden + */ + @Nullable + public static ClassLoader overrideThreadContextClassLoader(@Nullable ClassLoader classLoaderToUse) { + Thread currentThread = Thread.currentThread(); + ClassLoader threadContextClassLoader = currentThread.getContextClassLoader(); + if (classLoaderToUse != null && !classLoaderToUse.equals(threadContextClassLoader)) { + currentThread.setContextClassLoader(classLoaderToUse); + return threadContextClassLoader; + } + else { + return null; + } + } + + /** + * Replacement for {@code Class.forName()} that also returns Class instances + * for primitives (e.g. "int") and array class names (e.g. "String[]"). + * Furthermore, it is also capable of resolving inner class names in Java source + * style (e.g. "java.lang.Thread.State" instead of "java.lang.Thread$State"). + * @param name the name of the Class + * @param classLoader the class loader to use + * (may be {@code null}, which indicates the default class loader) + * @return a class instance for the supplied name + * @throws ClassNotFoundException if the class was not found + * @throws LinkageError if the class file could not be loaded + * @see Class#forName(String, boolean, ClassLoader) + */ + public static Class forName(String name, @Nullable ClassLoader classLoader) + throws ClassNotFoundException, LinkageError { + + Assert.notNull(name, "Name must not be null"); + + Class clazz = resolvePrimitiveClassName(name); + if (clazz == null) { + clazz = commonClassCache.get(name); + } + if (clazz != null) { + return clazz; + } + + // "java.lang.String[]" style arrays + if (name.endsWith(ARRAY_SUFFIX)) { + String elementClassName = name.substring(0, name.length() - ARRAY_SUFFIX.length()); + Class elementClass = forName(elementClassName, classLoader); + return Array.newInstance(elementClass, 0).getClass(); + } + + // "[Ljava.lang.String;" style arrays + if (name.startsWith(NON_PRIMITIVE_ARRAY_PREFIX) && name.endsWith(";")) { + String elementName = name.substring(NON_PRIMITIVE_ARRAY_PREFIX.length(), name.length() - 1); + Class elementClass = forName(elementName, classLoader); + return Array.newInstance(elementClass, 0).getClass(); + } + + // "[[I" or "[[Ljava.lang.String;" style arrays + if (name.startsWith(INTERNAL_ARRAY_PREFIX)) { + String elementName = name.substring(INTERNAL_ARRAY_PREFIX.length()); + Class elementClass = forName(elementName, classLoader); + return Array.newInstance(elementClass, 0).getClass(); + } + + ClassLoader clToUse = classLoader; + if (clToUse == null) { + clToUse = getDefaultClassLoader(); + } + try { + return Class.forName(name, false, clToUse); + } + catch (ClassNotFoundException ex) { + int lastDotIndex = name.lastIndexOf(PACKAGE_SEPARATOR); + if (lastDotIndex != -1) { + String innerClassName = + name.substring(0, lastDotIndex) + INNER_CLASS_SEPARATOR + name.substring(lastDotIndex + 1); + try { + return Class.forName(innerClassName, false, clToUse); + } + catch (ClassNotFoundException ex2) { + // Swallow - let original exception get through + } + } + throw ex; + } + } + + /** + * Resolve the given class name into a Class instance. Supports + * primitives (like "int") and array class names (like "String[]"). + *

    This is effectively equivalent to the {@code forName} + * method with the same arguments, with the only difference being + * the exceptions thrown in case of class loading failure. + * @param className the name of the Class + * @param classLoader the class loader to use + * (may be {@code null}, which indicates the default class loader) + * @return a class instance for the supplied name + * @throws IllegalArgumentException if the class name was not resolvable + * (that is, the class could not be found or the class file could not be loaded) + * @throws IllegalStateException if the corresponding class is resolvable but + * there was a readability mismatch in the inheritance hierarchy of the class + * (typically a missing dependency declaration in a Jigsaw module definition + * for a superclass or interface implemented by the class to be loaded here) + * @see #forName(String, ClassLoader) + */ + public static Class resolveClassName(String className, @Nullable ClassLoader classLoader) + throws IllegalArgumentException { + + try { + return forName(className, classLoader); + } + catch (IllegalAccessError err) { + throw new IllegalStateException("Readability mismatch in inheritance hierarchy of class [" + + className + "]: " + err.getMessage(), err); + } + catch (LinkageError err) { + throw new IllegalArgumentException("Unresolvable class definition for class [" + className + "]", err); + } + catch (ClassNotFoundException ex) { + throw new IllegalArgumentException("Could not find class [" + className + "]", ex); + } + } + + /** + * Determine whether the {@link Class} identified by the supplied name is present + * and can be loaded. Will return {@code false} if either the class or + * one of its dependencies is not present or cannot be loaded. + * @param className the name of the class to check + * @param classLoader the class loader to use + * (may be {@code null} which indicates the default class loader) + * @return whether the specified class is present (including all of its + * superclasses and interfaces) + * @throws IllegalStateException if the corresponding class is resolvable but + * there was a readability mismatch in the inheritance hierarchy of the class + * (typically a missing dependency declaration in a Jigsaw module definition + * for a superclass or interface implemented by the class to be checked here) + */ + public static boolean isPresent(String className, @Nullable ClassLoader classLoader) { + try { + forName(className, classLoader); + return true; + } + catch (IllegalAccessError err) { + throw new IllegalStateException("Readability mismatch in inheritance hierarchy of class [" + + className + "]: " + err.getMessage(), err); + } + catch (Throwable ex) { + // Typically ClassNotFoundException or NoClassDefFoundError... + return false; + } + } + + /** + * Check whether the given class is visible in the given ClassLoader. + * @param clazz the class to check (typically an interface) + * @param classLoader the ClassLoader to check against + * (may be {@code null} in which case this method will always return {@code true}) + */ + public static boolean isVisible(Class clazz, @Nullable ClassLoader classLoader) { + if (classLoader == null) { + return true; + } + try { + if (clazz.getClassLoader() == classLoader) { + return true; + } + } + catch (SecurityException ex) { + // Fall through to loadable check below + } + + // Visible if same Class can be loaded from given ClassLoader + return isLoadable(clazz, classLoader); + } + + /** + * Check whether the given class is cache-safe in the given context, + * i.e. whether it is loaded by the given ClassLoader or a parent of it. + * @param clazz the class to analyze + * @param classLoader the ClassLoader to potentially cache metadata in + * (may be {@code null} which indicates the system class loader) + */ + public static boolean isCacheSafe(Class clazz, @Nullable ClassLoader classLoader) { + Assert.notNull(clazz, "Class must not be null"); + try { + ClassLoader target = clazz.getClassLoader(); + // Common cases + if (target == classLoader || target == null) { + return true; + } + if (classLoader == null) { + return false; + } + // Check for match in ancestors -> positive + ClassLoader current = classLoader; + while (current != null) { + current = current.getParent(); + if (current == target) { + return true; + } + } + // Check for match in children -> negative + while (target != null) { + target = target.getParent(); + if (target == classLoader) { + return false; + } + } + } + catch (SecurityException ex) { + // Fall through to loadable check below + } + + // Fallback for ClassLoaders without parent/child relationship: + // safe if same Class can be loaded from given ClassLoader + return (classLoader != null && isLoadable(clazz, classLoader)); + } + + /** + * Check whether the given class is loadable in the given ClassLoader. + * @param clazz the class to check (typically an interface) + * @param classLoader the ClassLoader to check against + * @since 5.0.6 + */ + private static boolean isLoadable(Class clazz, ClassLoader classLoader) { + try { + return (clazz == classLoader.loadClass(clazz.getName())); + // Else: different class with same name found + } + catch (ClassNotFoundException ex) { + // No corresponding class found at all + return false; + } + } + + /** + * Resolve the given class name as primitive class, if appropriate, + * according to the JVM's naming rules for primitive classes. + *

    Also supports the JVM's internal class names for primitive arrays. + * Does not support the "[]" suffix notation for primitive arrays; + * this is only supported by {@link #forName(String, ClassLoader)}. + * @param name the name of the potentially primitive class + * @return the primitive class, or {@code null} if the name does not denote + * a primitive class or primitive array class + */ + @Nullable + public static Class resolvePrimitiveClassName(@Nullable String name) { + Class result = null; + // Most class names will be quite long, considering that they + // SHOULD sit in a package, so a length check is worthwhile. + if (name != null && name.length() <= 7) { + // Could be a primitive - likely. + result = primitiveTypeNameMap.get(name); + } + return result; + } + + /** + * Check if the given class represents a primitive wrapper, + * i.e. Boolean, Byte, Character, Short, Integer, Long, Float, Double, or + * Void. + * @param clazz the class to check + * @return whether the given class is a primitive wrapper class + */ + public static boolean isPrimitiveWrapper(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + return primitiveWrapperTypeMap.containsKey(clazz); + } + + /** + * Check if the given class represents a primitive (i.e. boolean, byte, + * char, short, int, long, float, or double), {@code void}, or a wrapper for + * those types (i.e. Boolean, Byte, Character, Short, Integer, Long, Float, + * Double, or Void). + * @param clazz the class to check + * @return {@code true} if the given class represents a primitive, void, or + * a wrapper class + */ + public static boolean isPrimitiveOrWrapper(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + return (clazz.isPrimitive() || isPrimitiveWrapper(clazz)); + } + + /** + * Check if the given class represents an array of primitives, + * i.e. boolean, byte, char, short, int, long, float, or double. + * @param clazz the class to check + * @return whether the given class is a primitive array class + */ + public static boolean isPrimitiveArray(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + return (clazz.isArray() && clazz.getComponentType().isPrimitive()); + } + + /** + * Check if the given class represents an array of primitive wrappers, + * i.e. Boolean, Byte, Character, Short, Integer, Long, Float, or Double. + * @param clazz the class to check + * @return whether the given class is a primitive wrapper array class + */ + public static boolean isPrimitiveWrapperArray(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + return (clazz.isArray() && isPrimitiveWrapper(clazz.getComponentType())); + } + + /** + * Resolve the given class if it is a primitive class, + * returning the corresponding primitive wrapper type instead. + * @param clazz the class to check + * @return the original class, or a primitive wrapper for the original primitive type + */ + public static Class resolvePrimitiveIfNecessary(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + return (clazz.isPrimitive() && clazz != void.class ? primitiveTypeToWrapperMap.get(clazz) : clazz); + } + + /** + * Check if the right-hand side type may be assigned to the left-hand side + * type, assuming setting by reflection. Considers primitive wrapper + * classes as assignable to the corresponding primitive types. + * @param lhsType the target type + * @param rhsType the value type that should be assigned to the target type + * @return if the target type is assignable from the value type + * @see TypeUtils#isAssignable(java.lang.reflect.Type, java.lang.reflect.Type) + */ + public static boolean isAssignable(Class lhsType, Class rhsType) { + Assert.notNull(lhsType, "Left-hand side type must not be null"); + Assert.notNull(rhsType, "Right-hand side type must not be null"); + if (lhsType.isAssignableFrom(rhsType)) { + return true; + } + if (lhsType.isPrimitive()) { + Class resolvedPrimitive = primitiveWrapperTypeMap.get(rhsType); + return (lhsType == resolvedPrimitive); + } + else { + Class resolvedWrapper = primitiveTypeToWrapperMap.get(rhsType); + return (resolvedWrapper != null && lhsType.isAssignableFrom(resolvedWrapper)); + } + } + + /** + * Determine if the given type is assignable from the given value, + * assuming setting by reflection. Considers primitive wrapper classes + * as assignable to the corresponding primitive types. + * @param type the target type + * @param value the value that should be assigned to the type + * @return if the type is assignable from the value + */ + public static boolean isAssignableValue(Class type, @Nullable Object value) { + Assert.notNull(type, "Type must not be null"); + return (value != null ? isAssignable(type, value.getClass()) : !type.isPrimitive()); + } + + /** + * Convert a "/"-based resource path to a "."-based fully qualified class name. + * @param resourcePath the resource path pointing to a class + * @return the corresponding fully qualified class name + */ + public static String convertResourcePathToClassName(String resourcePath) { + Assert.notNull(resourcePath, "Resource path must not be null"); + return resourcePath.replace(PATH_SEPARATOR, PACKAGE_SEPARATOR); + } + + /** + * Convert a "."-based fully qualified class name to a "/"-based resource path. + * @param className the fully qualified class name + * @return the corresponding resource path, pointing to the class + */ + public static String convertClassNameToResourcePath(String className) { + Assert.notNull(className, "Class name must not be null"); + return className.replace(PACKAGE_SEPARATOR, PATH_SEPARATOR); + } + + /** + * Return a path suitable for use with {@code ClassLoader.getResource} + * (also suitable for use with {@code Class.getResource} by prepending a + * slash ('/') to the return value). Built by taking the package of the specified + * class file, converting all dots ('.') to slashes ('/'), adding a trailing slash + * if necessary, and concatenating the specified resource name to this. + *
    As such, this function may be used to build a path suitable for + * loading a resource file that is in the same package as a class file, + * although {@link org.springframework.core.io.ClassPathResource} is usually + * even more convenient. + * @param clazz the Class whose package will be used as the base + * @param resourceName the resource name to append. A leading slash is optional. + * @return the built-up resource path + * @see ClassLoader#getResource + * @see Class#getResource + */ + public static String addResourcePathToPackagePath(Class clazz, String resourceName) { + Assert.notNull(resourceName, "Resource name must not be null"); + if (!resourceName.startsWith("/")) { + return classPackageAsResourcePath(clazz) + '/' + resourceName; + } + return classPackageAsResourcePath(clazz) + resourceName; + } + + /** + * Given an input class object, return a string which consists of the + * class's package name as a pathname, i.e., all dots ('.') are replaced by + * slashes ('/'). Neither a leading nor trailing slash is added. The result + * could be concatenated with a slash and the name of a resource and fed + * directly to {@code ClassLoader.getResource()}. For it to be fed to + * {@code Class.getResource} instead, a leading slash would also have + * to be prepended to the returned value. + * @param clazz the input class. A {@code null} value or the default + * (empty) package will result in an empty string ("") being returned. + * @return a path which represents the package name + * @see ClassLoader#getResource + * @see Class#getResource + */ + public static String classPackageAsResourcePath(@Nullable Class clazz) { + if (clazz == null) { + return ""; + } + String className = clazz.getName(); + int packageEndIndex = className.lastIndexOf(PACKAGE_SEPARATOR); + if (packageEndIndex == -1) { + return ""; + } + String packageName = className.substring(0, packageEndIndex); + return packageName.replace(PACKAGE_SEPARATOR, PATH_SEPARATOR); + } + + /** + * Build a String that consists of the names of the classes/interfaces + * in the given array. + *

    Basically like {@code AbstractCollection.toString()}, but stripping + * the "class "/"interface " prefix before every class name. + * @param classes an array of Class objects + * @return a String of form "[com.foo.Bar, com.foo.Baz]" + * @see java.util.AbstractCollection#toString() + */ + public static String classNamesToString(Class... classes) { + return classNamesToString(Arrays.asList(classes)); + } + + /** + * Build a String that consists of the names of the classes/interfaces + * in the given collection. + *

    Basically like {@code AbstractCollection.toString()}, but stripping + * the "class "/"interface " prefix before every class name. + * @param classes a Collection of Class objects (may be {@code null}) + * @return a String of form "[com.foo.Bar, com.foo.Baz]" + * @see java.util.AbstractCollection#toString() + */ + public static String classNamesToString(@Nullable Collection> classes) { + if (CollectionUtils.isEmpty(classes)) { + return "[]"; + } + StringJoiner stringJoiner = new StringJoiner(", ", "[", "]"); + for (Class clazz : classes) { + stringJoiner.add(clazz.getName()); + } + return stringJoiner.toString(); + } + + /** + * Copy the given {@code Collection} into a {@code Class} array. + *

    The {@code Collection} must contain {@code Class} elements only. + * @param collection the {@code Collection} to copy + * @return the {@code Class} array + * @since 3.1 + * @see StringUtils#toStringArray + */ + public static Class[] toClassArray(@Nullable Collection> collection) { + return (!CollectionUtils.isEmpty(collection) ? collection.toArray(EMPTY_CLASS_ARRAY) : EMPTY_CLASS_ARRAY); + } + + /** + * Return all interfaces that the given instance implements as an array, + * including ones implemented by superclasses. + * @param instance the instance to analyze for interfaces + * @return all interfaces that the given instance implements as an array + */ + public static Class[] getAllInterfaces(Object instance) { + Assert.notNull(instance, "Instance must not be null"); + return getAllInterfacesForClass(instance.getClass()); + } + + /** + * Return all interfaces that the given class implements as an array, + * including ones implemented by superclasses. + *

    If the class itself is an interface, it gets returned as sole interface. + * @param clazz the class to analyze for interfaces + * @return all interfaces that the given object implements as an array + */ + public static Class[] getAllInterfacesForClass(Class clazz) { + return getAllInterfacesForClass(clazz, null); + } + + /** + * Return all interfaces that the given class implements as an array, + * including ones implemented by superclasses. + *

    If the class itself is an interface, it gets returned as sole interface. + * @param clazz the class to analyze for interfaces + * @param classLoader the ClassLoader that the interfaces need to be visible in + * (may be {@code null} when accepting all declared interfaces) + * @return all interfaces that the given object implements as an array + */ + public static Class[] getAllInterfacesForClass(Class clazz, @Nullable ClassLoader classLoader) { + return toClassArray(getAllInterfacesForClassAsSet(clazz, classLoader)); + } + + /** + * Return all interfaces that the given instance implements as a Set, + * including ones implemented by superclasses. + * @param instance the instance to analyze for interfaces + * @return all interfaces that the given instance implements as a Set + */ + public static Set> getAllInterfacesAsSet(Object instance) { + Assert.notNull(instance, "Instance must not be null"); + return getAllInterfacesForClassAsSet(instance.getClass()); + } + + /** + * Return all interfaces that the given class implements as a Set, + * including ones implemented by superclasses. + *

    If the class itself is an interface, it gets returned as sole interface. + * @param clazz the class to analyze for interfaces + * @return all interfaces that the given object implements as a Set + */ + public static Set> getAllInterfacesForClassAsSet(Class clazz) { + return getAllInterfacesForClassAsSet(clazz, null); + } + + /** + * Return all interfaces that the given class implements as a Set, + * including ones implemented by superclasses. + *

    If the class itself is an interface, it gets returned as sole interface. + * @param clazz the class to analyze for interfaces + * @param classLoader the ClassLoader that the interfaces need to be visible in + * (may be {@code null} when accepting all declared interfaces) + * @return all interfaces that the given object implements as a Set + */ + public static Set> getAllInterfacesForClassAsSet(Class clazz, @Nullable ClassLoader classLoader) { + Assert.notNull(clazz, "Class must not be null"); + if (clazz.isInterface() && isVisible(clazz, classLoader)) { + return Collections.singleton(clazz); + } + Set> interfaces = new LinkedHashSet<>(); + Class current = clazz; + while (current != null) { + Class[] ifcs = current.getInterfaces(); + for (Class ifc : ifcs) { + if (isVisible(ifc, classLoader)) { + interfaces.add(ifc); + } + } + current = current.getSuperclass(); + } + return interfaces; + } + + /** + * Create a composite interface Class for the given interfaces, + * implementing the given interfaces in one single Class. + *

    This implementation builds a JDK proxy class for the given interfaces. + * @param interfaces the interfaces to merge + * @param classLoader the ClassLoader to create the composite Class in + * @return the merged interface as Class + * @throws IllegalArgumentException if the specified interfaces expose + * conflicting method signatures (or a similar constraint is violated) + * @see java.lang.reflect.Proxy#getProxyClass + */ + @SuppressWarnings("deprecation") // on JDK 9 + public static Class createCompositeInterface(Class[] interfaces, @Nullable ClassLoader classLoader) { + Assert.notEmpty(interfaces, "Interface array must not be empty"); + return Proxy.getProxyClass(classLoader, interfaces); + } + + /** + * Determine the common ancestor of the given classes, if any. + * @param clazz1 the class to introspect + * @param clazz2 the other class to introspect + * @return the common ancestor (i.e. common superclass, one interface + * extending the other), or {@code null} if none found. If any of the + * given classes is {@code null}, the other class will be returned. + * @since 3.2.6 + */ + @Nullable + public static Class determineCommonAncestor(@Nullable Class clazz1, @Nullable Class clazz2) { + if (clazz1 == null) { + return clazz2; + } + if (clazz2 == null) { + return clazz1; + } + if (clazz1.isAssignableFrom(clazz2)) { + return clazz1; + } + if (clazz2.isAssignableFrom(clazz1)) { + return clazz2; + } + Class ancestor = clazz1; + do { + ancestor = ancestor.getSuperclass(); + if (ancestor == null || Object.class == ancestor) { + return null; + } + } + while (!ancestor.isAssignableFrom(clazz2)); + return ancestor; + } + + /** + * Determine whether the given interface is a common Java language interface: + * {@link Serializable}, {@link Externalizable}, {@link Closeable}, {@link AutoCloseable}, + * {@link Cloneable}, {@link Comparable} - all of which can be ignored when looking + * for 'primary' user-level interfaces. Common characteristics: no service-level + * operations, no bean property methods, no default methods. + * @param ifc the interface to check + * @since 5.0.3 + */ + public static boolean isJavaLanguageInterface(Class ifc) { + return javaLanguageInterfaces.contains(ifc); + } + + /** + * Determine if the supplied class is an inner class, + * i.e. a non-static member of an enclosing class. + * @return {@code true} if the supplied class is an inner class + * @since 5.0.5 + * @see Class#isMemberClass() + */ + public static boolean isInnerClass(Class clazz) { + return (clazz.isMemberClass() && !Modifier.isStatic(clazz.getModifiers())); + } + + /** + * Check whether the given object is a CGLIB proxy. + * @param object the object to check + * @see #isCglibProxyClass(Class) + * @see org.springframework.aop.support.AopUtils#isCglibProxy(Object) + * @deprecated as of 5.2, in favor of custom (possibly narrower) checks + */ + @Deprecated + public static boolean isCglibProxy(Object object) { + return isCglibProxyClass(object.getClass()); + } + + /** + * Check whether the specified class is a CGLIB-generated class. + * @param clazz the class to check + * @see #isCglibProxyClassName(String) + * @deprecated as of 5.2, in favor of custom (possibly narrower) checks + */ + @Deprecated + public static boolean isCglibProxyClass(@Nullable Class clazz) { + return (clazz != null && isCglibProxyClassName(clazz.getName())); + } + + /** + * Check whether the specified class name is a CGLIB-generated class. + * @param className the class name to check + * @deprecated as of 5.2, in favor of custom (possibly narrower) checks + */ + @Deprecated + public static boolean isCglibProxyClassName(@Nullable String className) { + return (className != null && className.contains(CGLIB_CLASS_SEPARATOR)); + } + + /** + * Return the user-defined class for the given instance: usually simply + * the class of the given instance, but the original class in case of a + * CGLIB-generated subclass. + * @param instance the instance to check + * @return the user-defined class + */ + public static Class getUserClass(Object instance) { + Assert.notNull(instance, "Instance must not be null"); + return getUserClass(instance.getClass()); + } + + /** + * Return the user-defined class for the given class: usually simply the given + * class, but the original class in case of a CGLIB-generated subclass. + * @param clazz the class to check + * @return the user-defined class + */ + public static Class getUserClass(Class clazz) { + if (clazz.getName().contains(CGLIB_CLASS_SEPARATOR)) { + Class superclass = clazz.getSuperclass(); + if (superclass != null && superclass != Object.class) { + return superclass; + } + } + return clazz; + } + + /** + * Return a descriptive name for the given object's type: usually simply + * the class name, but component type class name + "[]" for arrays, + * and an appended list of implemented interfaces for JDK proxies. + * @param value the value to introspect + * @return the qualified name of the class + */ + @Nullable + public static String getDescriptiveType(@Nullable Object value) { + if (value == null) { + return null; + } + Class clazz = value.getClass(); + if (Proxy.isProxyClass(clazz)) { + String prefix = clazz.getName() + " implementing "; + StringJoiner result = new StringJoiner(",", prefix, ""); + for (Class ifc : clazz.getInterfaces()) { + result.add(ifc.getName()); + } + return result.toString(); + } + else { + return clazz.getTypeName(); + } + } + + /** + * Check whether the given class matches the user-specified type name. + * @param clazz the class to check + * @param typeName the type name to match + */ + public static boolean matchesTypeName(Class clazz, @Nullable String typeName) { + return (typeName != null && + (typeName.equals(clazz.getTypeName()) || typeName.equals(clazz.getSimpleName()))); + } + + /** + * Get the class name without the qualified package name. + * @param className the className to get the short name for + * @return the class name of the class without the package name + * @throws IllegalArgumentException if the className is empty + */ + public static String getShortName(String className) { + Assert.hasLength(className, "Class name must not be empty"); + int lastDotIndex = className.lastIndexOf(PACKAGE_SEPARATOR); + int nameEndIndex = className.indexOf(CGLIB_CLASS_SEPARATOR); + if (nameEndIndex == -1) { + nameEndIndex = className.length(); + } + String shortName = className.substring(lastDotIndex + 1, nameEndIndex); + shortName = shortName.replace(INNER_CLASS_SEPARATOR, PACKAGE_SEPARATOR); + return shortName; + } + + /** + * Get the class name without the qualified package name. + * @param clazz the class to get the short name for + * @return the class name of the class without the package name + */ + public static String getShortName(Class clazz) { + return getShortName(getQualifiedName(clazz)); + } + + /** + * Return the short string name of a Java class in uncapitalized JavaBeans + * property format. Strips the outer class name in case of an inner class. + * @param clazz the class + * @return the short name rendered in a standard JavaBeans property format + * @see java.beans.Introspector#decapitalize(String) + */ + public static String getShortNameAsProperty(Class clazz) { + String shortName = getShortName(clazz); + int dotIndex = shortName.lastIndexOf(PACKAGE_SEPARATOR); + shortName = (dotIndex != -1 ? shortName.substring(dotIndex + 1) : shortName); + return Introspector.decapitalize(shortName); + } + + /** + * Determine the name of the class file, relative to the containing + * package: e.g. "String.class" + * @param clazz the class + * @return the file name of the ".class" file + */ + public static String getClassFileName(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + String className = clazz.getName(); + int lastDotIndex = className.lastIndexOf(PACKAGE_SEPARATOR); + return className.substring(lastDotIndex + 1) + CLASS_FILE_SUFFIX; + } + + /** + * Determine the name of the package of the given class, + * e.g. "java.lang" for the {@code java.lang.String} class. + * @param clazz the class + * @return the package name, or the empty String if the class + * is defined in the default package + */ + public static String getPackageName(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + return getPackageName(clazz.getName()); + } + + /** + * Determine the name of the package of the given fully-qualified class name, + * e.g. "java.lang" for the {@code java.lang.String} class name. + * @param fqClassName the fully-qualified class name + * @return the package name, or the empty String if the class + * is defined in the default package + */ + public static String getPackageName(String fqClassName) { + Assert.notNull(fqClassName, "Class name must not be null"); + int lastDotIndex = fqClassName.lastIndexOf(PACKAGE_SEPARATOR); + return (lastDotIndex != -1 ? fqClassName.substring(0, lastDotIndex) : ""); + } + + /** + * Return the qualified name of the given class: usually simply + * the class name, but component type class name + "[]" for arrays. + * @param clazz the class + * @return the qualified name of the class + */ + public static String getQualifiedName(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + return clazz.getTypeName(); + } + + /** + * Return the qualified name of the given method, consisting of + * fully qualified interface/class name + "." + method name. + * @param method the method + * @return the qualified name of the method + */ + public static String getQualifiedMethodName(Method method) { + return getQualifiedMethodName(method, null); + } + + /** + * Return the qualified name of the given method, consisting of + * fully qualified interface/class name + "." + method name. + * @param method the method + * @param clazz the clazz that the method is being invoked on + * (may be {@code null} to indicate the method's declaring class) + * @return the qualified name of the method + * @since 4.3.4 + */ + public static String getQualifiedMethodName(Method method, @Nullable Class clazz) { + Assert.notNull(method, "Method must not be null"); + return (clazz != null ? clazz : method.getDeclaringClass()).getName() + '.' + method.getName(); + } + + /** + * Determine whether the given class has a public constructor with the given signature. + *

    Essentially translates {@code NoSuchMethodException} to "false". + * @param clazz the clazz to analyze + * @param paramTypes the parameter types of the method + * @return whether the class has a corresponding constructor + * @see Class#getConstructor + */ + public static boolean hasConstructor(Class clazz, Class... paramTypes) { + return (getConstructorIfAvailable(clazz, paramTypes) != null); + } + + /** + * Determine whether the given class has a public constructor with the given signature, + * and return it if available (else return {@code null}). + *

    Essentially translates {@code NoSuchMethodException} to {@code null}. + * @param clazz the clazz to analyze + * @param paramTypes the parameter types of the method + * @return the constructor, or {@code null} if not found + * @see Class#getConstructor + */ + @Nullable + public static Constructor getConstructorIfAvailable(Class clazz, Class... paramTypes) { + Assert.notNull(clazz, "Class must not be null"); + try { + return clazz.getConstructor(paramTypes); + } + catch (NoSuchMethodException ex) { + return null; + } + } + + /** + * Determine whether the given class has a public method with the given signature. + * @param clazz the clazz to analyze + * @param method the method to look for + * @return whether the class has a corresponding method + * @since 5.2.3 + */ + public static boolean hasMethod(Class clazz, Method method) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(method, "Method must not be null"); + if (clazz == method.getDeclaringClass()) { + return true; + } + String methodName = method.getName(); + Class[] paramTypes = method.getParameterTypes(); + return getMethodOrNull(clazz, methodName, paramTypes) != null; + } + + /** + * Determine whether the given class has a public method with the given signature. + *

    Essentially translates {@code NoSuchMethodException} to "false". + * @param clazz the clazz to analyze + * @param methodName the name of the method + * @param paramTypes the parameter types of the method + * @return whether the class has a corresponding method + * @see Class#getMethod + */ + public static boolean hasMethod(Class clazz, String methodName, Class... paramTypes) { + return (getMethodIfAvailable(clazz, methodName, paramTypes) != null); + } + + /** + * Determine whether the given class has a public method with the given signature, + * and return it if available (else throws an {@code IllegalStateException}). + *

    In case of any signature specified, only returns the method if there is a + * unique candidate, i.e. a single public method with the specified name. + *

    Essentially translates {@code NoSuchMethodException} to {@code IllegalStateException}. + * @param clazz the clazz to analyze + * @param methodName the name of the method + * @param paramTypes the parameter types of the method + * (may be {@code null} to indicate any signature) + * @return the method (never {@code null}) + * @throws IllegalStateException if the method has not been found + * @see Class#getMethod + */ + public static Method getMethod(Class clazz, String methodName, @Nullable Class... paramTypes) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(methodName, "Method name must not be null"); + if (paramTypes != null) { + try { + return clazz.getMethod(methodName, paramTypes); + } + catch (NoSuchMethodException ex) { + throw new IllegalStateException("Expected method not found: " + ex); + } + } + else { + Set candidates = findMethodCandidatesByName(clazz, methodName); + if (candidates.size() == 1) { + return candidates.iterator().next(); + } + else if (candidates.isEmpty()) { + throw new IllegalStateException("Expected method not found: " + clazz.getName() + '.' + methodName); + } + else { + throw new IllegalStateException("No unique method found: " + clazz.getName() + '.' + methodName); + } + } + } + + /** + * Determine whether the given class has a public method with the given signature, + * and return it if available (else return {@code null}). + *

    In case of any signature specified, only returns the method if there is a + * unique candidate, i.e. a single public method with the specified name. + *

    Essentially translates {@code NoSuchMethodException} to {@code null}. + * @param clazz the clazz to analyze + * @param methodName the name of the method + * @param paramTypes the parameter types of the method + * (may be {@code null} to indicate any signature) + * @return the method, or {@code null} if not found + * @see Class#getMethod + */ + @Nullable + public static Method getMethodIfAvailable(Class clazz, String methodName, @Nullable Class... paramTypes) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(methodName, "Method name must not be null"); + if (paramTypes != null) { + return getMethodOrNull(clazz, methodName, paramTypes); + } + else { + Set candidates = findMethodCandidatesByName(clazz, methodName); + if (candidates.size() == 1) { + return candidates.iterator().next(); + } + return null; + } + } + + /** + * Return the number of methods with a given name (with any argument types), + * for the given class and/or its superclasses. Includes non-public methods. + * @param clazz the clazz to check + * @param methodName the name of the method + * @return the number of methods with the given name + */ + public static int getMethodCountForName(Class clazz, String methodName) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(methodName, "Method name must not be null"); + int count = 0; + Method[] declaredMethods = clazz.getDeclaredMethods(); + for (Method method : declaredMethods) { + if (methodName.equals(method.getName())) { + count++; + } + } + Class[] ifcs = clazz.getInterfaces(); + for (Class ifc : ifcs) { + count += getMethodCountForName(ifc, methodName); + } + if (clazz.getSuperclass() != null) { + count += getMethodCountForName(clazz.getSuperclass(), methodName); + } + return count; + } + + /** + * Does the given class or one of its superclasses at least have one or more + * methods with the supplied name (with any argument types)? + * Includes non-public methods. + * @param clazz the clazz to check + * @param methodName the name of the method + * @return whether there is at least one method with the given name + */ + public static boolean hasAtLeastOneMethodWithName(Class clazz, String methodName) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(methodName, "Method name must not be null"); + Method[] declaredMethods = clazz.getDeclaredMethods(); + for (Method method : declaredMethods) { + if (method.getName().equals(methodName)) { + return true; + } + } + Class[] ifcs = clazz.getInterfaces(); + for (Class ifc : ifcs) { + if (hasAtLeastOneMethodWithName(ifc, methodName)) { + return true; + } + } + return (clazz.getSuperclass() != null && hasAtLeastOneMethodWithName(clazz.getSuperclass(), methodName)); + } + + /** + * Given a method, which may come from an interface, and a target class used + * in the current reflective invocation, find the corresponding target method + * if there is one. E.g. the method may be {@code IFoo.bar()} and the + * target class may be {@code DefaultFoo}. In this case, the method may be + * {@code DefaultFoo.bar()}. This enables attributes on that method to be found. + *

    NOTE: In contrast to {@link org.springframework.aop.support.AopUtils#getMostSpecificMethod}, + * this method does not resolve Java 5 bridge methods automatically. + * Call {@link org.springframework.core.BridgeMethodResolver#findBridgedMethod} + * if bridge method resolution is desirable (e.g. for obtaining metadata from + * the original method definition). + *

    NOTE: Since Spring 3.1.1, if Java security settings disallow reflective + * access (e.g. calls to {@code Class#getDeclaredMethods} etc, this implementation + * will fall back to returning the originally provided method. + * @param method the method to be invoked, which may come from an interface + * @param targetClass the target class for the current invocation + * (may be {@code null} or may not even implement the method) + * @return the specific target method, or the original method if the + * {@code targetClass} does not implement it + * @see #getInterfaceMethodIfPossible + */ + public static Method getMostSpecificMethod(Method method, @Nullable Class targetClass) { + if (targetClass != null && targetClass != method.getDeclaringClass() && isOverridable(method, targetClass)) { + try { + if (Modifier.isPublic(method.getModifiers())) { + try { + return targetClass.getMethod(method.getName(), method.getParameterTypes()); + } + catch (NoSuchMethodException ex) { + return method; + } + } + else { + Method specificMethod = + ReflectionUtils.findMethod(targetClass, method.getName(), method.getParameterTypes()); + return (specificMethod != null ? specificMethod : method); + } + } + catch (SecurityException ex) { + // Security settings are disallowing reflective access; fall back to 'method' below. + } + } + return method; + } + + /** + * Determine a corresponding interface method for the given method handle, if possible. + *

    This is particularly useful for arriving at a public exported type on Jigsaw + * which can be reflectively invoked without an illegal access warning. + * @param method the method to be invoked, potentially from an implementation class + * @return the corresponding interface method, or the original method if none found + * @since 5.1 + * @see #getMostSpecificMethod + */ + public static Method getInterfaceMethodIfPossible(Method method) { + if (!Modifier.isPublic(method.getModifiers()) || method.getDeclaringClass().isInterface()) { + return method; + } + return interfaceMethodCache.computeIfAbsent(method, key -> { + Class current = key.getDeclaringClass(); + while (current != null && current != Object.class) { + Class[] ifcs = current.getInterfaces(); + for (Class ifc : ifcs) { + try { + return ifc.getMethod(key.getName(), key.getParameterTypes()); + } + catch (NoSuchMethodException ex) { + // ignore + } + } + current = current.getSuperclass(); + } + return key; + }); + } + + /** + * Determine whether the given method is declared by the user or at least pointing to + * a user-declared method. + *

    Checks {@link Method#isSynthetic()} (for implementation methods) as well as the + * {@code GroovyObject} interface (for interface methods; on an implementation class, + * implementations of the {@code GroovyObject} methods will be marked as synthetic anyway). + * Note that, despite being synthetic, bridge methods ({@link Method#isBridge()}) are considered + * as user-level methods since they are eventually pointing to a user-declared generic method. + * @param method the method to check + * @return {@code true} if the method can be considered as user-declared; [@code false} otherwise + */ + public static boolean isUserLevelMethod(Method method) { + Assert.notNull(method, "Method must not be null"); + return (method.isBridge() || (!method.isSynthetic() && !isGroovyObjectMethod(method))); + } + + private static boolean isGroovyObjectMethod(Method method) { + return method.getDeclaringClass().getName().equals("groovy.lang.GroovyObject"); + } + + /** + * Determine whether the given method is overridable in the given target class. + * @param method the method to check + * @param targetClass the target class to check against + */ + private static boolean isOverridable(Method method, @Nullable Class targetClass) { + if (Modifier.isPrivate(method.getModifiers())) { + return false; + } + if (Modifier.isPublic(method.getModifiers()) || Modifier.isProtected(method.getModifiers())) { + return true; + } + return (targetClass == null || + getPackageName(method.getDeclaringClass()).equals(getPackageName(targetClass))); + } + + /** + * Return a public static method of a class. + * @param clazz the class which defines the method + * @param methodName the static method name + * @param args the parameter types to the method + * @return the static method, or {@code null} if no static method was found + * @throws IllegalArgumentException if the method name is blank or the clazz is null + */ + @Nullable + public static Method getStaticMethod(Class clazz, String methodName, Class... args) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(methodName, "Method name must not be null"); + try { + Method method = clazz.getMethod(methodName, args); + return Modifier.isStatic(method.getModifiers()) ? method : null; + } + catch (NoSuchMethodException ex) { + return null; + } + } + + + @Nullable + private static Method getMethodOrNull(Class clazz, String methodName, Class[] paramTypes) { + try { + return clazz.getMethod(methodName, paramTypes); + } + catch (NoSuchMethodException ex) { + return null; + } + } + + private static Set findMethodCandidatesByName(Class clazz, String methodName) { + Set candidates = new HashSet<>(1); + Method[] methods = clazz.getMethods(); + for (Method method : methods) { + if (methodName.equals(method.getName())) { + candidates.add(method); + } + } + return candidates; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/CollectionUtils.java b/spring-core/src/main/java/org/springframework/util/CollectionUtils.java new file mode 100644 index 0000000..d1e9354 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/CollectionUtils.java @@ -0,0 +1,512 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.SortedSet; + +import org.springframework.lang.Nullable; + +/** + * Miscellaneous collection utility methods. + * Mainly for internal use within the framework. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @author Arjen Poutsma + * @since 1.1.3 + */ +public abstract class CollectionUtils { + + /** + * Default load factor for {@link HashMap}/{@link LinkedHashMap} variants. + * @see #newHashMap(int) + * @see #newLinkedHashMap(int) + */ + static final float DEFAULT_LOAD_FACTOR = 0.75f; + + + /** + * Return {@code true} if the supplied Collection is {@code null} or empty. + * Otherwise, return {@code false}. + * @param collection the Collection to check + * @return whether the given Collection is empty + */ + public static boolean isEmpty(@Nullable Collection collection) { + return (collection == null || collection.isEmpty()); + } + + /** + * Return {@code true} if the supplied Map is {@code null} or empty. + * Otherwise, return {@code false}. + * @param map the Map to check + * @return whether the given Map is empty + */ + public static boolean isEmpty(@Nullable Map map) { + return (map == null || map.isEmpty()); + } + + /** + * Instantiate a new {@link HashMap} with an initial capacity + * that can accommodate the specified number of elements without + * any immediate resize/rehash operations to be expected. + *

    This differs from the regular {@link HashMap} constructor + * which takes an initial capacity relative to a load factor + * but is effectively aligned with the JDK's + * {@link java.util.concurrent.ConcurrentHashMap#ConcurrentHashMap(int)}. + * @param expectedSize the expected number of elements (with a corresponding + * capacity to be derived so that no resize/rehash operations are needed) + * @since 5.3 + * @see #newLinkedHashMap(int) + */ + public static HashMap newHashMap(int expectedSize) { + return new HashMap<>((int) (expectedSize / DEFAULT_LOAD_FACTOR), DEFAULT_LOAD_FACTOR); + } + + /** + * Instantiate a new {@link LinkedHashMap} with an initial capacity + * that can accommodate the specified number of elements without + * any immediate resize/rehash operations to be expected. + *

    This differs from the regular {@link LinkedHashMap} constructor + * which takes an initial capacity relative to a load factor but is + * aligned with Spring's own {@link LinkedCaseInsensitiveMap} and + * {@link LinkedMultiValueMap} constructor semantics as of 5.3. + * @param expectedSize the expected number of elements (with a corresponding + * capacity to be derived so that no resize/rehash operations are needed) + * @since 5.3 + * @see #newHashMap(int) + */ + public static LinkedHashMap newLinkedHashMap(int expectedSize) { + return new LinkedHashMap<>((int) (expectedSize / DEFAULT_LOAD_FACTOR), DEFAULT_LOAD_FACTOR); + } + + /** + * Convert the supplied array into a List. A primitive array gets converted + * into a List of the appropriate wrapper type. + *

    NOTE: Generally prefer the standard {@link Arrays#asList} method. + * This {@code arrayToList} method is just meant to deal with an incoming Object + * value that might be an {@code Object[]} or a primitive array at runtime. + *

    A {@code null} source value will be converted to an empty List. + * @param source the (potentially primitive) array + * @return the converted List result + * @see ObjectUtils#toObjectArray(Object) + * @see Arrays#asList(Object[]) + */ + public static List arrayToList(@Nullable Object source) { + return Arrays.asList(ObjectUtils.toObjectArray(source)); + } + + /** + * Merge the given array into the given Collection. + * @param array the array to merge (may be {@code null}) + * @param collection the target Collection to merge the array into + */ + @SuppressWarnings("unchecked") + public static void mergeArrayIntoCollection(@Nullable Object array, Collection collection) { + Object[] arr = ObjectUtils.toObjectArray(array); + for (Object elem : arr) { + collection.add((E) elem); + } + } + + /** + * Merge the given Properties instance into the given Map, + * copying all properties (key-value pairs) over. + *

    Uses {@code Properties.propertyNames()} to even catch + * default properties linked into the original Properties instance. + * @param props the Properties instance to merge (may be {@code null}) + * @param map the target Map to merge the properties into + */ + @SuppressWarnings("unchecked") + public static void mergePropertiesIntoMap(@Nullable Properties props, Map map) { + if (props != null) { + for (Enumeration en = props.propertyNames(); en.hasMoreElements();) { + String key = (String) en.nextElement(); + Object value = props.get(key); + if (value == null) { + // Allow for defaults fallback or potentially overridden accessor... + value = props.getProperty(key); + } + map.put((K) key, (V) value); + } + } + } + + + /** + * Check whether the given Iterator contains the given element. + * @param iterator the Iterator to check + * @param element the element to look for + * @return {@code true} if found, {@code false} otherwise + */ + public static boolean contains(@Nullable Iterator iterator, Object element) { + if (iterator != null) { + while (iterator.hasNext()) { + Object candidate = iterator.next(); + if (ObjectUtils.nullSafeEquals(candidate, element)) { + return true; + } + } + } + return false; + } + + /** + * Check whether the given Enumeration contains the given element. + * @param enumeration the Enumeration to check + * @param element the element to look for + * @return {@code true} if found, {@code false} otherwise + */ + public static boolean contains(@Nullable Enumeration enumeration, Object element) { + if (enumeration != null) { + while (enumeration.hasMoreElements()) { + Object candidate = enumeration.nextElement(); + if (ObjectUtils.nullSafeEquals(candidate, element)) { + return true; + } + } + } + return false; + } + + /** + * Check whether the given Collection contains the given element instance. + *

    Enforces the given instance to be present, rather than returning + * {@code true} for an equal element as well. + * @param collection the Collection to check + * @param element the element to look for + * @return {@code true} if found, {@code false} otherwise + */ + public static boolean containsInstance(@Nullable Collection collection, Object element) { + if (collection != null) { + for (Object candidate : collection) { + if (candidate == element) { + return true; + } + } + } + return false; + } + + /** + * Return {@code true} if any element in '{@code candidates}' is + * contained in '{@code source}'; otherwise returns {@code false}. + * @param source the source Collection + * @param candidates the candidates to search for + * @return whether any of the candidates has been found + */ + public static boolean containsAny(Collection source, Collection candidates) { + return findFirstMatch(source, candidates) != null; + } + + /** + * Return the first element in '{@code candidates}' that is contained in + * '{@code source}'. If no element in '{@code candidates}' is present in + * '{@code source}' returns {@code null}. Iteration order is + * {@link Collection} implementation specific. + * @param source the source Collection + * @param candidates the candidates to search for + * @return the first present object, or {@code null} if not found + */ + @SuppressWarnings("unchecked") + @Nullable + public static E findFirstMatch(Collection source, Collection candidates) { + if (isEmpty(source) || isEmpty(candidates)) { + return null; + } + for (Object candidate : candidates) { + if (source.contains(candidate)) { + return (E) candidate; + } + } + return null; + } + + /** + * Find a single value of the given type in the given Collection. + * @param collection the Collection to search + * @param type the type to look for + * @return a value of the given type found if there is a clear match, + * or {@code null} if none or more than one such value found + */ + @SuppressWarnings("unchecked") + @Nullable + public static T findValueOfType(Collection collection, @Nullable Class type) { + if (isEmpty(collection)) { + return null; + } + T value = null; + for (Object element : collection) { + if (type == null || type.isInstance(element)) { + if (value != null) { + // More than one value found... no clear single value. + return null; + } + value = (T) element; + } + } + return value; + } + + /** + * Find a single value of one of the given types in the given Collection: + * searching the Collection for a value of the first type, then + * searching for a value of the second type, etc. + * @param collection the collection to search + * @param types the types to look for, in prioritized order + * @return a value of one of the given types found if there is a clear match, + * or {@code null} if none or more than one such value found + */ + @Nullable + public static Object findValueOfType(Collection collection, Class[] types) { + if (isEmpty(collection) || ObjectUtils.isEmpty(types)) { + return null; + } + for (Class type : types) { + Object value = findValueOfType(collection, type); + if (value != null) { + return value; + } + } + return null; + } + + /** + * Determine whether the given Collection only contains a single unique object. + * @param collection the Collection to check + * @return {@code true} if the collection contains a single reference or + * multiple references to the same instance, {@code false} otherwise + */ + public static boolean hasUniqueObject(Collection collection) { + if (isEmpty(collection)) { + return false; + } + boolean hasCandidate = false; + Object candidate = null; + for (Object elem : collection) { + if (!hasCandidate) { + hasCandidate = true; + candidate = elem; + } + else if (candidate != elem) { + return false; + } + } + return true; + } + + /** + * Find the common element type of the given Collection, if any. + * @param collection the Collection to check + * @return the common element type, or {@code null} if no clear + * common type has been found (or the collection was empty) + */ + @Nullable + public static Class findCommonElementType(Collection collection) { + if (isEmpty(collection)) { + return null; + } + Class candidate = null; + for (Object val : collection) { + if (val != null) { + if (candidate == null) { + candidate = val.getClass(); + } + else if (candidate != val.getClass()) { + return null; + } + } + } + return candidate; + } + + /** + * Retrieve the first element of the given Set, using {@link SortedSet#first()} + * or otherwise using the iterator. + * @param set the Set to check (may be {@code null} or empty) + * @return the first element, or {@code null} if none + * @since 5.2.3 + * @see SortedSet + * @see LinkedHashMap#keySet() + * @see java.util.LinkedHashSet + */ + @Nullable + public static T firstElement(@Nullable Set set) { + if (isEmpty(set)) { + return null; + } + if (set instanceof SortedSet) { + return ((SortedSet) set).first(); + } + + Iterator it = set.iterator(); + T first = null; + if (it.hasNext()) { + first = it.next(); + } + return first; + } + + /** + * Retrieve the first element of the given List, accessing the zero index. + * @param list the List to check (may be {@code null} or empty) + * @return the first element, or {@code null} if none + * @since 5.2.3 + */ + @Nullable + public static T firstElement(@Nullable List list) { + if (isEmpty(list)) { + return null; + } + return list.get(0); + } + + /** + * Retrieve the last element of the given Set, using {@link SortedSet#last()} + * or otherwise iterating over all elements (assuming a linked set). + * @param set the Set to check (may be {@code null} or empty) + * @return the last element, or {@code null} if none + * @since 5.0.3 + * @see SortedSet + * @see LinkedHashMap#keySet() + * @see java.util.LinkedHashSet + */ + @Nullable + public static T lastElement(@Nullable Set set) { + if (isEmpty(set)) { + return null; + } + if (set instanceof SortedSet) { + return ((SortedSet) set).last(); + } + + // Full iteration necessary... + Iterator it = set.iterator(); + T last = null; + while (it.hasNext()) { + last = it.next(); + } + return last; + } + + /** + * Retrieve the last element of the given List, accessing the highest index. + * @param list the List to check (may be {@code null} or empty) + * @return the last element, or {@code null} if none + * @since 5.0.3 + */ + @Nullable + public static T lastElement(@Nullable List list) { + if (isEmpty(list)) { + return null; + } + return list.get(list.size() - 1); + } + + /** + * Marshal the elements from the given enumeration into an array of the given type. + * Enumeration elements must be assignable to the type of the given array. The array + * returned will be a different instance than the array given. + */ + public static A[] toArray(Enumeration enumeration, A[] array) { + ArrayList elements = new ArrayList<>(); + while (enumeration.hasMoreElements()) { + elements.add(enumeration.nextElement()); + } + return elements.toArray(array); + } + + /** + * Adapt an {@link Enumeration} to an {@link Iterator}. + * @param enumeration the original {@code Enumeration} + * @return the adapted {@code Iterator} + */ + public static Iterator toIterator(@Nullable Enumeration enumeration) { + return (enumeration != null ? new EnumerationIterator<>(enumeration) : Collections.emptyIterator()); + } + + /** + * Adapt a {@code Map>} to an {@code MultiValueMap}. + * @param targetMap the original map + * @return the adapted multi-value map (wrapping the original map) + * @since 3.1 + */ + public static MultiValueMap toMultiValueMap(Map> targetMap) { + return new MultiValueMapAdapter<>(targetMap); + } + + /** + * Return an unmodifiable view of the specified multi-value map. + * @param targetMap the map for which an unmodifiable view is to be returned. + * @return an unmodifiable view of the specified multi-value map + * @since 3.1 + */ + @SuppressWarnings("unchecked") + public static MultiValueMap unmodifiableMultiValueMap( + MultiValueMap targetMap) { + + Assert.notNull(targetMap, "'targetMap' must not be null"); + Map> result = newLinkedHashMap(targetMap.size()); + targetMap.forEach((key, value) -> { + List values = Collections.unmodifiableList(value); + result.put(key, (List) values); + }); + Map> unmodifiableMap = Collections.unmodifiableMap(result); + return toMultiValueMap(unmodifiableMap); + } + + + /** + * Iterator wrapping an Enumeration. + */ + private static class EnumerationIterator implements Iterator { + + private final Enumeration enumeration; + + public EnumerationIterator(Enumeration enumeration) { + this.enumeration = enumeration; + } + + @Override + public boolean hasNext() { + return this.enumeration.hasMoreElements(); + } + + @Override + public E next() { + return this.enumeration.nextElement(); + } + + @Override + public void remove() throws UnsupportedOperationException { + throw new UnsupportedOperationException("Not supported"); + } + } + + +} diff --git a/spring-core/src/main/java/org/springframework/util/CommonsLogWriter.java b/spring-core/src/main/java/org/springframework/util/CommonsLogWriter.java new file mode 100644 index 0000000..c3c5744 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/CommonsLogWriter.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.io.Writer; + +import org.apache.commons.logging.Log; + +/** + * {@code java.io.Writer} adapter for a Commons Logging {@code Log}. + * + * @author Juergen Hoeller + * @since 2.5.1 + */ +public class CommonsLogWriter extends Writer { + + private final Log logger; + + private final StringBuilder buffer = new StringBuilder(); + + + /** + * Create a new CommonsLogWriter for the given Commons Logging logger. + * @param logger the Commons Logging logger to write to + */ + public CommonsLogWriter(Log logger) { + Assert.notNull(logger, "Logger must not be null"); + this.logger = logger; + } + + + public void write(char ch) { + if (ch == '\n' && this.buffer.length() > 0) { + logger.debug(this.buffer.toString()); + this.buffer.setLength(0); + } + else { + this.buffer.append(ch); + } + } + + @Override + public void write(char[] buffer, int offset, int length) { + for (int i = 0; i < length; i++) { + write(buffer[offset + i]); + } + } + + @Override + public void flush() { + } + + @Override + public void close() { + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/CompositeIterator.java b/spring-core/src/main/java/org/springframework/util/CompositeIterator.java new file mode 100644 index 0000000..58ab425 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/CompositeIterator.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.NoSuchElementException; +import java.util.Set; + +/** + * Composite iterator that combines multiple other iterators, + * as registered via {@link #add(Iterator)}. + * + *

    This implementation maintains a linked set of iterators + * which are invoked in sequence until all iterators are exhausted. + * + * @author Erwin Vervaet + * @author Juergen Hoeller + * @since 3.0 + * @param the element type + */ +public class CompositeIterator implements Iterator { + + private final Set> iterators = new LinkedHashSet<>(); + + private boolean inUse = false; + + + /** + * Add given iterator to this composite. + */ + public void add(Iterator iterator) { + Assert.state(!this.inUse, "You can no longer add iterators to a composite iterator that's already in use"); + if (this.iterators.contains(iterator)) { + throw new IllegalArgumentException("You cannot add the same iterator twice"); + } + this.iterators.add(iterator); + } + + @Override + public boolean hasNext() { + this.inUse = true; + for (Iterator iterator : this.iterators) { + if (iterator.hasNext()) { + return true; + } + } + return false; + } + + @Override + public E next() { + this.inUse = true; + for (Iterator iterator : this.iterators) { + if (iterator.hasNext()) { + return iterator.next(); + } + } + throw new NoSuchElementException("All iterators exhausted"); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("CompositeIterator does not support remove()"); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/ConcurrencyThrottleSupport.java b/spring-core/src/main/java/org/springframework/util/ConcurrencyThrottleSupport.java new file mode 100644 index 0000000..58fd12b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/ConcurrencyThrottleSupport.java @@ -0,0 +1,170 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Support class for throttling concurrent access to a specific resource. + * + *

    Designed for use as a base class, with the subclass invoking + * the {@link #beforeAccess()} and {@link #afterAccess()} methods at + * appropriate points of its workflow. Note that {@code afterAccess} + * should usually be called in a finally block! + * + *

    The default concurrency limit of this support class is -1 + * ("unbounded concurrency"). Subclasses may override this default; + * check the javadoc of the concrete class that you're using. + * + * @author Juergen Hoeller + * @since 1.2.5 + * @see #setConcurrencyLimit + * @see #beforeAccess() + * @see #afterAccess() + * @see org.springframework.aop.interceptor.ConcurrencyThrottleInterceptor + * @see java.io.Serializable + */ +@SuppressWarnings("serial") +public abstract class ConcurrencyThrottleSupport implements Serializable { + + /** + * Permit any number of concurrent invocations: that is, don't throttle concurrency. + */ + public static final int UNBOUNDED_CONCURRENCY = -1; + + /** + * Switch concurrency 'off': that is, don't allow any concurrent invocations. + */ + public static final int NO_CONCURRENCY = 0; + + + /** Transient to optimize serialization. */ + protected transient Log logger = LogFactory.getLog(getClass()); + + private transient Object monitor = new Object(); + + private int concurrencyLimit = UNBOUNDED_CONCURRENCY; + + private int concurrencyCount = 0; + + + /** + * Set the maximum number of concurrent access attempts allowed. + * -1 indicates unbounded concurrency. + *

    In principle, this limit can be changed at runtime, + * although it is generally designed as a config time setting. + *

    NOTE: Do not switch between -1 and any concrete limit at runtime, + * as this will lead to inconsistent concurrency counts: A limit + * of -1 effectively turns off concurrency counting completely. + */ + public void setConcurrencyLimit(int concurrencyLimit) { + this.concurrencyLimit = concurrencyLimit; + } + + /** + * Return the maximum number of concurrent access attempts allowed. + */ + public int getConcurrencyLimit() { + return this.concurrencyLimit; + } + + /** + * Return whether this throttle is currently active. + * @return {@code true} if the concurrency limit for this instance is active + * @see #getConcurrencyLimit() + */ + public boolean isThrottleActive() { + return (this.concurrencyLimit >= 0); + } + + + /** + * To be invoked before the main execution logic of concrete subclasses. + *

    This implementation applies the concurrency throttle. + * @see #afterAccess() + */ + protected void beforeAccess() { + if (this.concurrencyLimit == NO_CONCURRENCY) { + throw new IllegalStateException( + "Currently no invocations allowed - concurrency limit set to NO_CONCURRENCY"); + } + if (this.concurrencyLimit > 0) { + boolean debug = logger.isDebugEnabled(); + synchronized (this.monitor) { + boolean interrupted = false; + while (this.concurrencyCount >= this.concurrencyLimit) { + if (interrupted) { + throw new IllegalStateException("Thread was interrupted while waiting for invocation access, " + + "but concurrency limit still does not allow for entering"); + } + if (debug) { + logger.debug("Concurrency count " + this.concurrencyCount + + " has reached limit " + this.concurrencyLimit + " - blocking"); + } + try { + this.monitor.wait(); + } + catch (InterruptedException ex) { + // Re-interrupt current thread, to allow other threads to react. + Thread.currentThread().interrupt(); + interrupted = true; + } + } + if (debug) { + logger.debug("Entering throttle at concurrency count " + this.concurrencyCount); + } + this.concurrencyCount++; + } + } + } + + /** + * To be invoked after the main execution logic of concrete subclasses. + * @see #beforeAccess() + */ + protected void afterAccess() { + if (this.concurrencyLimit >= 0) { + synchronized (this.monitor) { + this.concurrencyCount--; + if (logger.isDebugEnabled()) { + logger.debug("Returning from throttle at concurrency count " + this.concurrencyCount); + } + this.monitor.notify(); + } + } + } + + + //--------------------------------------------------------------------- + // Serialization support + //--------------------------------------------------------------------- + + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + // Rely on default serialization, just initialize state after deserialization. + ois.defaultReadObject(); + + // Initialize transient fields. + this.logger = LogFactory.getLog(getClass()); + this.monitor = new Object(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/ConcurrentLruCache.java b/spring-core/src/main/java/org/springframework/util/ConcurrentLruCache.java new file mode 100644 index 0000000..2f1f2e0 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/ConcurrentLruCache.java @@ -0,0 +1,185 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Function; + +/** + * Simple LRU (Least Recently Used) cache, bounded by a specified cache limit. + * + *

    This implementation is backed by a {@code ConcurrentHashMap} for storing + * the cached values and a {@code ConcurrentLinkedDeque} for ordering the keys + * and choosing the least recently used key when the cache is at full capacity. + * + * @author Brian Clozel + * @author Juergen Hoeller + * @since 5.3 + * @param the type of the key used for cache retrieval + * @param the type of the cached values + * @see #get + */ +public class ConcurrentLruCache { + + private final int sizeLimit; + + private final Function generator; + + private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); + + private final ConcurrentLinkedDeque queue = new ConcurrentLinkedDeque<>(); + + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + + private volatile int size; + + + /** + * Create a new cache instance with the given limit and generator function. + * @param sizeLimit the maximum number of entries in the cache + * (0 indicates no caching, always generating a new value) + * @param generator a function to generate a new value for a given key + */ + public ConcurrentLruCache(int sizeLimit, Function generator) { + Assert.isTrue(sizeLimit >= 0, "Cache size limit must not be negative"); + Assert.notNull(generator, "Generator function must not be null"); + this.sizeLimit = sizeLimit; + this.generator = generator; + } + + + /** + * Retrieve an entry from the cache, potentially triggering generation + * of the value. + * @param key the key to retrieve the entry for + * @return the cached or newly generated value + */ + public V get(K key) { + if (this.sizeLimit == 0) { + return this.generator.apply(key); + } + + V cached = this.cache.get(key); + if (cached != null) { + if (this.size < this.sizeLimit) { + return cached; + } + this.lock.readLock().lock(); + try { + if (this.queue.removeLastOccurrence(key)) { + this.queue.offer(key); + } + return cached; + } + finally { + this.lock.readLock().unlock(); + } + } + + this.lock.writeLock().lock(); + try { + // Retrying in case of concurrent reads on the same key + cached = this.cache.get(key); + if (cached != null) { + if (this.queue.removeLastOccurrence(key)) { + this.queue.offer(key); + } + return cached; + } + // Generate value first, to prevent size inconsistency + V value = this.generator.apply(key); + if (this.size == this.sizeLimit) { + K leastUsed = this.queue.poll(); + if (leastUsed != null) { + this.cache.remove(leastUsed); + } + } + this.queue.offer(key); + this.cache.put(key, value); + this.size = this.cache.size(); + return value; + } + finally { + this.lock.writeLock().unlock(); + } + } + + /** + * Determine whether the given key is present in this cache. + * @param key the key to check for + * @return {@code true} if the key is present, + * {@code false} if there was no matching key + */ + public boolean contains(K key) { + return this.cache.containsKey(key); + } + + /** + * Immediately remove the given key and any associated value. + * @param key the key to evict the entry for + * @return {@code true} if the key was present before, + * {@code false} if there was no matching key + */ + public boolean remove(K key) { + this.lock.writeLock().lock(); + try { + boolean wasPresent = (this.cache.remove(key) != null); + this.queue.remove(key); + this.size = this.cache.size(); + return wasPresent; + } + finally { + this.lock.writeLock().unlock(); + } + } + + /** + * Immediately remove all entries from this cache. + */ + public void clear() { + this.lock.writeLock().lock(); + try { + this.cache.clear(); + this.queue.clear(); + this.size = 0; + } + finally { + this.lock.writeLock().unlock(); + } + } + + /** + * Return the current size of the cache. + * @see #sizeLimit() + */ + public int size() { + return this.size; + } + + /** + * Return the the maximum number of entries in the cache + * (0 indicates no caching, always generating a new value). + * @see #size() + */ + public int sizeLimit() { + return this.sizeLimit; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java new file mode 100644 index 0000000..4cec61e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java @@ -0,0 +1,1092 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.lang.ref.ReferenceQueue; +import java.lang.ref.SoftReference; +import java.lang.ref.WeakReference; +import java.lang.reflect.Array; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; + +import org.springframework.lang.Nullable; + +/** + * A {@link ConcurrentHashMap} that uses {@link ReferenceType#SOFT soft} or + * {@linkplain ReferenceType#WEAK weak} references for both {@code keys} and {@code values}. + * + *

    This class can be used as an alternative to + * {@code Collections.synchronizedMap(new WeakHashMap>())} in order to + * support better performance when accessed concurrently. This implementation follows the + * same design constraints as {@link ConcurrentHashMap} with the exception that + * {@code null} values and {@code null} keys are supported. + * + *

    NOTE: The use of references means that there is no guarantee that items + * placed into the map will be subsequently available. The garbage collector may discard + * references at any time, so it may appear that an unknown thread is silently removing + * entries. + * + *

    If not explicitly specified, this implementation will use + * {@linkplain SoftReference soft entry references}. + * + * @author Phillip Webb + * @author Juergen Hoeller + * @since 3.2 + * @param the key type + * @param the value type + */ +public class ConcurrentReferenceHashMap extends AbstractMap implements ConcurrentMap { + + private static final int DEFAULT_INITIAL_CAPACITY = 16; + + private static final float DEFAULT_LOAD_FACTOR = 0.75f; + + private static final int DEFAULT_CONCURRENCY_LEVEL = 16; + + private static final ReferenceType DEFAULT_REFERENCE_TYPE = ReferenceType.SOFT; + + private static final int MAXIMUM_CONCURRENCY_LEVEL = 1 << 16; + + private static final int MAXIMUM_SEGMENT_SIZE = 1 << 30; + + + /** + * Array of segments indexed using the high order bits from the hash. + */ + private final Segment[] segments; + + /** + * When the average number of references per table exceeds this value resize will be attempted. + */ + private final float loadFactor; + + /** + * The reference type: SOFT or WEAK. + */ + private final ReferenceType referenceType; + + /** + * The shift value used to calculate the size of the segments array and an index from the hash. + */ + private final int shift; + + /** + * Late binding entry set. + */ + @Nullable + private volatile Set> entrySet; + + + /** + * Create a new {@code ConcurrentReferenceHashMap} instance. + */ + public ConcurrentReferenceHashMap() { + this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL, DEFAULT_REFERENCE_TYPE); + } + + /** + * Create a new {@code ConcurrentReferenceHashMap} instance. + * @param initialCapacity the initial capacity of the map + */ + public ConcurrentReferenceHashMap(int initialCapacity) { + this(initialCapacity, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL, DEFAULT_REFERENCE_TYPE); + } + + /** + * Create a new {@code ConcurrentReferenceHashMap} instance. + * @param initialCapacity the initial capacity of the map + * @param loadFactor the load factor. When the average number of references per table + * exceeds this value resize will be attempted + */ + public ConcurrentReferenceHashMap(int initialCapacity, float loadFactor) { + this(initialCapacity, loadFactor, DEFAULT_CONCURRENCY_LEVEL, DEFAULT_REFERENCE_TYPE); + } + + /** + * Create a new {@code ConcurrentReferenceHashMap} instance. + * @param initialCapacity the initial capacity of the map + * @param concurrencyLevel the expected number of threads that will concurrently + * write to the map + */ + public ConcurrentReferenceHashMap(int initialCapacity, int concurrencyLevel) { + this(initialCapacity, DEFAULT_LOAD_FACTOR, concurrencyLevel, DEFAULT_REFERENCE_TYPE); + } + + /** + * Create a new {@code ConcurrentReferenceHashMap} instance. + * @param initialCapacity the initial capacity of the map + * @param referenceType the reference type used for entries (soft or weak) + */ + public ConcurrentReferenceHashMap(int initialCapacity, ReferenceType referenceType) { + this(initialCapacity, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL, referenceType); + } + + /** + * Create a new {@code ConcurrentReferenceHashMap} instance. + * @param initialCapacity the initial capacity of the map + * @param loadFactor the load factor. When the average number of references per + * table exceeds this value, resize will be attempted. + * @param concurrencyLevel the expected number of threads that will concurrently + * write to the map + */ + public ConcurrentReferenceHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) { + this(initialCapacity, loadFactor, concurrencyLevel, DEFAULT_REFERENCE_TYPE); + } + + /** + * Create a new {@code ConcurrentReferenceHashMap} instance. + * @param initialCapacity the initial capacity of the map + * @param loadFactor the load factor. When the average number of references per + * table exceeds this value, resize will be attempted. + * @param concurrencyLevel the expected number of threads that will concurrently + * write to the map + * @param referenceType the reference type used for entries (soft or weak) + */ + @SuppressWarnings("unchecked") + public ConcurrentReferenceHashMap( + int initialCapacity, float loadFactor, int concurrencyLevel, ReferenceType referenceType) { + + Assert.isTrue(initialCapacity >= 0, "Initial capacity must not be negative"); + Assert.isTrue(loadFactor > 0f, "Load factor must be positive"); + Assert.isTrue(concurrencyLevel > 0, "Concurrency level must be positive"); + Assert.notNull(referenceType, "Reference type must not be null"); + this.loadFactor = loadFactor; + this.shift = calculateShift(concurrencyLevel, MAXIMUM_CONCURRENCY_LEVEL); + int size = 1 << this.shift; + this.referenceType = referenceType; + int roundedUpSegmentCapacity = (int) ((initialCapacity + size - 1L) / size); + int initialSize = 1 << calculateShift(roundedUpSegmentCapacity, MAXIMUM_SEGMENT_SIZE); + Segment[] segments = (Segment[]) Array.newInstance(Segment.class, size); + int resizeThreshold = (int) (initialSize * getLoadFactor()); + for (int i = 0; i < segments.length; i++) { + segments[i] = new Segment(initialSize, resizeThreshold); + } + this.segments = segments; + } + + + protected final float getLoadFactor() { + return this.loadFactor; + } + + protected final int getSegmentsSize() { + return this.segments.length; + } + + protected final Segment getSegment(int index) { + return this.segments[index]; + } + + /** + * Factory method that returns the {@link ReferenceManager}. + * This method will be called once for each {@link Segment}. + * @return a new reference manager + */ + protected ReferenceManager createReferenceManager() { + return new ReferenceManager(); + } + + /** + * Get the hash for a given object, apply an additional hash function to reduce + * collisions. This implementation uses the same Wang/Jenkins algorithm as + * {@link ConcurrentHashMap}. Subclasses can override to provide alternative hashing. + * @param o the object to hash (may be null) + * @return the resulting hash code + */ + protected int getHash(@Nullable Object o) { + int hash = (o != null ? o.hashCode() : 0); + hash += (hash << 15) ^ 0xffffcd7d; + hash ^= (hash >>> 10); + hash += (hash << 3); + hash ^= (hash >>> 6); + hash += (hash << 2) + (hash << 14); + hash ^= (hash >>> 16); + return hash; + } + + @Override + @Nullable + public V get(@Nullable Object key) { + Reference ref = getReference(key, Restructure.WHEN_NECESSARY); + Entry entry = (ref != null ? ref.get() : null); + return (entry != null ? entry.getValue() : null); + } + + @Override + @Nullable + public V getOrDefault(@Nullable Object key, @Nullable V defaultValue) { + Reference ref = getReference(key, Restructure.WHEN_NECESSARY); + Entry entry = (ref != null ? ref.get() : null); + return (entry != null ? entry.getValue() : defaultValue); + } + + @Override + public boolean containsKey(@Nullable Object key) { + Reference ref = getReference(key, Restructure.WHEN_NECESSARY); + Entry entry = (ref != null ? ref.get() : null); + return (entry != null && ObjectUtils.nullSafeEquals(entry.getKey(), key)); + } + + /** + * Return a {@link Reference} to the {@link Entry} for the specified {@code key}, + * or {@code null} if not found. + * @param key the key (can be {@code null}) + * @param restructure types of restructure allowed during this call + * @return the reference, or {@code null} if not found + */ + @Nullable + protected final Reference getReference(@Nullable Object key, Restructure restructure) { + int hash = getHash(key); + return getSegmentForHash(hash).getReference(key, hash, restructure); + } + + @Override + @Nullable + public V put(@Nullable K key, @Nullable V value) { + return put(key, value, true); + } + + @Override + @Nullable + public V putIfAbsent(@Nullable K key, @Nullable V value) { + return put(key, value, false); + } + + @Nullable + private V put(@Nullable final K key, @Nullable final V value, final boolean overwriteExisting) { + return doTask(key, new Task(TaskOption.RESTRUCTURE_BEFORE, TaskOption.RESIZE) { + @Override + @Nullable + protected V execute(@Nullable Reference ref, @Nullable Entry entry, @Nullable Entries entries) { + if (entry != null) { + V oldValue = entry.getValue(); + if (overwriteExisting) { + entry.setValue(value); + } + return oldValue; + } + Assert.state(entries != null, "No entries segment"); + entries.add(value); + return null; + } + }); + } + + @Override + @Nullable + public V remove(Object key) { + return doTask(key, new Task(TaskOption.RESTRUCTURE_AFTER, TaskOption.SKIP_IF_EMPTY) { + @Override + @Nullable + protected V execute(@Nullable Reference ref, @Nullable Entry entry) { + if (entry != null) { + if (ref != null) { + ref.release(); + } + return entry.value; + } + return null; + } + }); + } + + @Override + public boolean remove(Object key, final Object value) { + Boolean result = doTask(key, new Task(TaskOption.RESTRUCTURE_AFTER, TaskOption.SKIP_IF_EMPTY) { + @Override + protected Boolean execute(@Nullable Reference ref, @Nullable Entry entry) { + if (entry != null && ObjectUtils.nullSafeEquals(entry.getValue(), value)) { + if (ref != null) { + ref.release(); + } + return true; + } + return false; + } + }); + return (Boolean.TRUE.equals(result)); + } + + @Override + public boolean replace(K key, final V oldValue, final V newValue) { + Boolean result = doTask(key, new Task(TaskOption.RESTRUCTURE_BEFORE, TaskOption.SKIP_IF_EMPTY) { + @Override + protected Boolean execute(@Nullable Reference ref, @Nullable Entry entry) { + if (entry != null && ObjectUtils.nullSafeEquals(entry.getValue(), oldValue)) { + entry.setValue(newValue); + return true; + } + return false; + } + }); + return (Boolean.TRUE.equals(result)); + } + + @Override + @Nullable + public V replace(K key, final V value) { + return doTask(key, new Task(TaskOption.RESTRUCTURE_BEFORE, TaskOption.SKIP_IF_EMPTY) { + @Override + @Nullable + protected V execute(@Nullable Reference ref, @Nullable Entry entry) { + if (entry != null) { + V oldValue = entry.getValue(); + entry.setValue(value); + return oldValue; + } + return null; + } + }); + } + + @Override + public void clear() { + for (Segment segment : this.segments) { + segment.clear(); + } + } + + /** + * Remove any entries that have been garbage collected and are no longer referenced. + * Under normal circumstances garbage collected entries are automatically purged as + * items are added or removed from the Map. This method can be used to force a purge, + * and is useful when the Map is read frequently but updated less often. + */ + public void purgeUnreferencedEntries() { + for (Segment segment : this.segments) { + segment.restructureIfNecessary(false); + } + } + + + @Override + public int size() { + int size = 0; + for (Segment segment : this.segments) { + size += segment.getCount(); + } + return size; + } + + @Override + public boolean isEmpty() { + for (Segment segment : this.segments) { + if (segment.getCount() > 0) { + return false; + } + } + return true; + } + + @Override + public Set> entrySet() { + Set> entrySet = this.entrySet; + if (entrySet == null) { + entrySet = new EntrySet(); + this.entrySet = entrySet; + } + return entrySet; + } + + @Nullable + private T doTask(@Nullable Object key, Task task) { + int hash = getHash(key); + return getSegmentForHash(hash).doTask(hash, key, task); + } + + private Segment getSegmentForHash(int hash) { + return this.segments[(hash >>> (32 - this.shift)) & (this.segments.length - 1)]; + } + + /** + * Calculate a shift value that can be used to create a power-of-two value between + * the specified maximum and minimum values. + * @param minimumValue the minimum value + * @param maximumValue the maximum value + * @return the calculated shift (use {@code 1 << shift} to obtain a value) + */ + protected static int calculateShift(int minimumValue, int maximumValue) { + int shift = 0; + int value = 1; + while (value < minimumValue && value < maximumValue) { + value <<= 1; + shift++; + } + return shift; + } + + + /** + * Various reference types supported by this map. + */ + public enum ReferenceType { + + /** Use {@link SoftReference SoftReferences}. */ + SOFT, + + /** Use {@link WeakReference WeakReferences}. */ + WEAK + } + + + /** + * A single segment used to divide the map to allow better concurrent performance. + */ + @SuppressWarnings("serial") + protected final class Segment extends ReentrantLock { + + private final ReferenceManager referenceManager; + + private final int initialSize; + + /** + * Array of references indexed using the low order bits from the hash. + * This property should only be set along with {@code resizeThreshold}. + */ + private volatile Reference[] references; + + /** + * The total number of references contained in this segment. This includes chained + * references and references that have been garbage collected but not purged. + */ + private final AtomicInteger count = new AtomicInteger(); + + /** + * The threshold when resizing of the references should occur. When {@code count} + * exceeds this value references will be resized. + */ + private int resizeThreshold; + + public Segment(int initialSize, int resizeThreshold) { + this.referenceManager = createReferenceManager(); + this.initialSize = initialSize; + this.references = createReferenceArray(initialSize); + this.resizeThreshold = resizeThreshold; + } + + @Nullable + public Reference getReference(@Nullable Object key, int hash, Restructure restructure) { + if (restructure == Restructure.WHEN_NECESSARY) { + restructureIfNecessary(false); + } + if (this.count.get() == 0) { + return null; + } + // Use a local copy to protect against other threads writing + Reference[] references = this.references; + int index = getIndex(hash, references); + Reference head = references[index]; + return findInChain(head, key, hash); + } + + /** + * Apply an update operation to this segment. + * The segment will be locked during the update. + * @param hash the hash of the key + * @param key the key + * @param task the update operation + * @return the result of the operation + */ + @Nullable + public T doTask(final int hash, @Nullable final Object key, final Task task) { + boolean resize = task.hasOption(TaskOption.RESIZE); + if (task.hasOption(TaskOption.RESTRUCTURE_BEFORE)) { + restructureIfNecessary(resize); + } + if (task.hasOption(TaskOption.SKIP_IF_EMPTY) && this.count.get() == 0) { + return task.execute(null, null, null); + } + lock(); + try { + final int index = getIndex(hash, this.references); + final Reference head = this.references[index]; + Reference ref = findInChain(head, key, hash); + Entry entry = (ref != null ? ref.get() : null); + Entries entries = value -> { + @SuppressWarnings("unchecked") + Entry newEntry = new Entry<>((K) key, value); + Reference newReference = Segment.this.referenceManager.createReference(newEntry, hash, head); + Segment.this.references[index] = newReference; + Segment.this.count.incrementAndGet(); + }; + return task.execute(ref, entry, entries); + } + finally { + unlock(); + if (task.hasOption(TaskOption.RESTRUCTURE_AFTER)) { + restructureIfNecessary(resize); + } + } + } + + /** + * Clear all items from this segment. + */ + public void clear() { + if (this.count.get() == 0) { + return; + } + lock(); + try { + this.references = createReferenceArray(this.initialSize); + this.resizeThreshold = (int) (this.references.length * getLoadFactor()); + this.count.set(0); + } + finally { + unlock(); + } + } + + /** + * Restructure the underlying data structure when it becomes necessary. This + * method can increase the size of the references table as well as purge any + * references that have been garbage collected. + * @param allowResize if resizing is permitted + */ + protected final void restructureIfNecessary(boolean allowResize) { + int currCount = this.count.get(); + boolean needsResize = allowResize && (currCount > 0 && currCount >= this.resizeThreshold); + Reference ref = this.referenceManager.pollForPurge(); + if (ref != null || (needsResize)) { + restructure(allowResize, ref); + } + } + + private void restructure(boolean allowResize, @Nullable Reference ref) { + boolean needsResize; + lock(); + try { + int countAfterRestructure = this.count.get(); + Set> toPurge = Collections.emptySet(); + if (ref != null) { + toPurge = new HashSet<>(); + while (ref != null) { + toPurge.add(ref); + ref = this.referenceManager.pollForPurge(); + } + } + countAfterRestructure -= toPurge.size(); + + // Recalculate taking into account count inside lock and items that + // will be purged + needsResize = (countAfterRestructure > 0 && countAfterRestructure >= this.resizeThreshold); + boolean resizing = false; + int restructureSize = this.references.length; + if (allowResize && needsResize && restructureSize < MAXIMUM_SEGMENT_SIZE) { + restructureSize <<= 1; + resizing = true; + } + + // Either create a new table or reuse the existing one + Reference[] restructured = + (resizing ? createReferenceArray(restructureSize) : this.references); + + // Restructure + for (int i = 0; i < this.references.length; i++) { + ref = this.references[i]; + if (!resizing) { + restructured[i] = null; + } + while (ref != null) { + if (!toPurge.contains(ref)) { + Entry entry = ref.get(); + if (entry != null) { + int index = getIndex(ref.getHash(), restructured); + restructured[index] = this.referenceManager.createReference( + entry, ref.getHash(), restructured[index]); + } + } + ref = ref.getNext(); + } + } + + // Replace volatile members + if (resizing) { + this.references = restructured; + this.resizeThreshold = (int) (this.references.length * getLoadFactor()); + } + this.count.set(Math.max(countAfterRestructure, 0)); + } + finally { + unlock(); + } + } + + @Nullable + private Reference findInChain(Reference ref, @Nullable Object key, int hash) { + Reference currRef = ref; + while (currRef != null) { + if (currRef.getHash() == hash) { + Entry entry = currRef.get(); + if (entry != null) { + K entryKey = entry.getKey(); + if (ObjectUtils.nullSafeEquals(entryKey, key)) { + return currRef; + } + } + } + currRef = currRef.getNext(); + } + return null; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private Reference[] createReferenceArray(int size) { + return new Reference[size]; + } + + private int getIndex(int hash, Reference[] references) { + return (hash & (references.length - 1)); + } + + /** + * Return the size of the current references array. + */ + public final int getSize() { + return this.references.length; + } + + /** + * Return the total number of references in this segment. + */ + public final int getCount() { + return this.count.get(); + } + } + + + /** + * A reference to an {@link Entry} contained in the map. Implementations are usually + * wrappers around specific Java reference implementations (e.g., {@link SoftReference}). + * @param the key type + * @param the value type + */ + protected interface Reference { + + /** + * Return the referenced entry, or {@code null} if the entry is no longer available. + */ + @Nullable + Entry get(); + + /** + * Return the hash for the reference. + */ + int getHash(); + + /** + * Return the next reference in the chain, or {@code null} if none. + */ + @Nullable + Reference getNext(); + + /** + * Release this entry and ensure that it will be returned from + * {@code ReferenceManager#pollForPurge()}. + */ + void release(); + } + + + /** + * A single map entry. + * @param the key type + * @param the value type + */ + protected static final class Entry implements Map.Entry { + + @Nullable + private final K key; + + @Nullable + private volatile V value; + + public Entry(@Nullable K key, @Nullable V value) { + this.key = key; + this.value = value; + } + + @Override + @Nullable + public K getKey() { + return this.key; + } + + @Override + @Nullable + public V getValue() { + return this.value; + } + + @Override + @Nullable + public V setValue(@Nullable V value) { + V previous = this.value; + this.value = value; + return previous; + } + + @Override + public String toString() { + return (this.key + "=" + this.value); + } + + @Override + @SuppressWarnings("rawtypes") + public final boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof Map.Entry)) { + return false; + } + Map.Entry otherEntry = (Map.Entry) other; + return (ObjectUtils.nullSafeEquals(getKey(), otherEntry.getKey()) && + ObjectUtils.nullSafeEquals(getValue(), otherEntry.getValue())); + } + + @Override + public final int hashCode() { + return (ObjectUtils.nullSafeHashCode(this.key) ^ ObjectUtils.nullSafeHashCode(this.value)); + } + } + + + /** + * A task that can be {@link Segment#doTask run} against a {@link Segment}. + */ + private abstract class Task { + + private final EnumSet options; + + public Task(TaskOption... options) { + this.options = (options.length == 0 ? EnumSet.noneOf(TaskOption.class) : EnumSet.of(options[0], options)); + } + + public boolean hasOption(TaskOption option) { + return this.options.contains(option); + } + + /** + * Execute the task. + * @param ref the found reference (or {@code null}) + * @param entry the found entry (or {@code null}) + * @param entries access to the underlying entries + * @return the result of the task + * @see #execute(Reference, Entry) + */ + @Nullable + protected T execute(@Nullable Reference ref, @Nullable Entry entry, @Nullable Entries entries) { + return execute(ref, entry); + } + + /** + * Convenience method that can be used for tasks that do not need access to {@link Entries}. + * @param ref the found reference (or {@code null}) + * @param entry the found entry (or {@code null}) + * @return the result of the task + * @see #execute(Reference, Entry, Entries) + */ + @Nullable + protected T execute(@Nullable Reference ref, @Nullable Entry entry) { + return null; + } + } + + + /** + * Various options supported by a {@code Task}. + */ + private enum TaskOption { + + RESTRUCTURE_BEFORE, RESTRUCTURE_AFTER, SKIP_IF_EMPTY, RESIZE + } + + + /** + * Allows a task access to {@link ConcurrentReferenceHashMap.Segment} entries. + */ + private interface Entries { + + /** + * Add a new entry with the specified value. + * @param value the value to add + */ + void add(@Nullable V value); + } + + + /** + * Internal entry-set implementation. + */ + private class EntrySet extends AbstractSet> { + + @Override + public Iterator> iterator() { + return new EntryIterator(); + } + + @Override + public boolean contains(@Nullable Object o) { + if (o instanceof Map.Entry) { + Map.Entry entry = (Map.Entry) o; + Reference ref = ConcurrentReferenceHashMap.this.getReference(entry.getKey(), Restructure.NEVER); + Entry otherEntry = (ref != null ? ref.get() : null); + if (otherEntry != null) { + return ObjectUtils.nullSafeEquals(otherEntry.getValue(), otherEntry.getValue()); + } + } + return false; + } + + @Override + public boolean remove(Object o) { + if (o instanceof Map.Entry) { + Map.Entry entry = (Map.Entry) o; + return ConcurrentReferenceHashMap.this.remove(entry.getKey(), entry.getValue()); + } + return false; + } + + @Override + public int size() { + return ConcurrentReferenceHashMap.this.size(); + } + + @Override + public void clear() { + ConcurrentReferenceHashMap.this.clear(); + } + } + + + /** + * Internal entry iterator implementation. + */ + private class EntryIterator implements Iterator> { + + private int segmentIndex; + + private int referenceIndex; + + @Nullable + private Reference[] references; + + @Nullable + private Reference reference; + + @Nullable + private Entry next; + + @Nullable + private Entry last; + + public EntryIterator() { + moveToNextSegment(); + } + + @Override + public boolean hasNext() { + getNextIfNecessary(); + return (this.next != null); + } + + @Override + public Entry next() { + getNextIfNecessary(); + if (this.next == null) { + throw new NoSuchElementException(); + } + this.last = this.next; + this.next = null; + return this.last; + } + + private void getNextIfNecessary() { + while (this.next == null) { + moveToNextReference(); + if (this.reference == null) { + return; + } + this.next = this.reference.get(); + } + } + + private void moveToNextReference() { + if (this.reference != null) { + this.reference = this.reference.getNext(); + } + while (this.reference == null && this.references != null) { + if (this.referenceIndex >= this.references.length) { + moveToNextSegment(); + this.referenceIndex = 0; + } + else { + this.reference = this.references[this.referenceIndex]; + this.referenceIndex++; + } + } + } + + private void moveToNextSegment() { + this.reference = null; + this.references = null; + if (this.segmentIndex < ConcurrentReferenceHashMap.this.segments.length) { + this.references = ConcurrentReferenceHashMap.this.segments[this.segmentIndex].references; + this.segmentIndex++; + } + } + + @Override + public void remove() { + Assert.state(this.last != null, "No element to remove"); + ConcurrentReferenceHashMap.this.remove(this.last.getKey()); + } + } + + + /** + * The types of restructuring that can be performed. + */ + protected enum Restructure { + + WHEN_NECESSARY, NEVER + } + + + /** + * Strategy class used to manage {@link Reference References}. + * This class can be overridden if alternative reference types need to be supported. + */ + protected class ReferenceManager { + + private final ReferenceQueue> queue = new ReferenceQueue<>(); + + /** + * Factory method used to create a new {@link Reference}. + * @param entry the entry contained in the reference + * @param hash the hash + * @param next the next reference in the chain, or {@code null} if none + * @return a new {@link Reference} + */ + public Reference createReference(Entry entry, int hash, @Nullable Reference next) { + if (ConcurrentReferenceHashMap.this.referenceType == ReferenceType.WEAK) { + return new WeakEntryReference<>(entry, hash, next, this.queue); + } + return new SoftEntryReference<>(entry, hash, next, this.queue); + } + + /** + * Return any reference that has been garbage collected and can be purged from the + * underlying structure or {@code null} if no references need purging. This + * method must be thread safe and ideally should not block when returning + * {@code null}. References should be returned once and only once. + * @return a reference to purge or {@code null} + */ + @SuppressWarnings("unchecked") + @Nullable + public Reference pollForPurge() { + return (Reference) this.queue.poll(); + } + } + + + /** + * Internal {@link Reference} implementation for {@link SoftReference SoftReferences}. + */ + private static final class SoftEntryReference extends SoftReference> implements Reference { + + private final int hash; + + @Nullable + private final Reference nextReference; + + public SoftEntryReference(Entry entry, int hash, @Nullable Reference next, + ReferenceQueue> queue) { + + super(entry, queue); + this.hash = hash; + this.nextReference = next; + } + + @Override + public int getHash() { + return this.hash; + } + + @Override + @Nullable + public Reference getNext() { + return this.nextReference; + } + + @Override + public void release() { + enqueue(); + clear(); + } + } + + + /** + * Internal {@link Reference} implementation for {@link WeakReference WeakReferences}. + */ + private static final class WeakEntryReference extends WeakReference> implements Reference { + + private final int hash; + + @Nullable + private final Reference nextReference; + + public WeakEntryReference(Entry entry, int hash, @Nullable Reference next, + ReferenceQueue> queue) { + + super(entry, queue); + this.hash = hash; + this.nextReference = next; + } + + @Override + public int getHash() { + return this.hash; + } + + @Override + @Nullable + public Reference getNext() { + return this.nextReference; + } + + @Override + public void release() { + enqueue(); + clear(); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/CustomizableThreadCreator.java b/spring-core/src/main/java/org/springframework/util/CustomizableThreadCreator.java new file mode 100644 index 0000000..bb56e0a --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/CustomizableThreadCreator.java @@ -0,0 +1,177 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.io.Serializable; +import java.util.concurrent.atomic.AtomicInteger; + +import org.springframework.lang.Nullable; + +/** + * Simple customizable helper class for creating new {@link Thread} instances. + * Provides various bean properties: thread name prefix, thread priority, etc. + * + *

    Serves as base class for thread factories such as + * {@link org.springframework.scheduling.concurrent.CustomizableThreadFactory}. + * + * @author Juergen Hoeller + * @since 2.0.3 + * @see org.springframework.scheduling.concurrent.CustomizableThreadFactory + */ +@SuppressWarnings("serial") +public class CustomizableThreadCreator implements Serializable { + + private String threadNamePrefix; + + private int threadPriority = Thread.NORM_PRIORITY; + + private boolean daemon = false; + + @Nullable + private ThreadGroup threadGroup; + + private final AtomicInteger threadCount = new AtomicInteger(); + + + /** + * Create a new CustomizableThreadCreator with default thread name prefix. + */ + public CustomizableThreadCreator() { + this.threadNamePrefix = getDefaultThreadNamePrefix(); + } + + /** + * Create a new CustomizableThreadCreator with the given thread name prefix. + * @param threadNamePrefix the prefix to use for the names of newly created threads + */ + public CustomizableThreadCreator(@Nullable String threadNamePrefix) { + this.threadNamePrefix = (threadNamePrefix != null ? threadNamePrefix : getDefaultThreadNamePrefix()); + } + + + /** + * Specify the prefix to use for the names of newly created threads. + * Default is "SimpleAsyncTaskExecutor-". + */ + public void setThreadNamePrefix(@Nullable String threadNamePrefix) { + this.threadNamePrefix = (threadNamePrefix != null ? threadNamePrefix : getDefaultThreadNamePrefix()); + } + + /** + * Return the thread name prefix to use for the names of newly + * created threads. + */ + public String getThreadNamePrefix() { + return this.threadNamePrefix; + } + + /** + * Set the priority of the threads that this factory creates. + * Default is 5. + * @see java.lang.Thread#NORM_PRIORITY + */ + public void setThreadPriority(int threadPriority) { + this.threadPriority = threadPriority; + } + + /** + * Return the priority of the threads that this factory creates. + */ + public int getThreadPriority() { + return this.threadPriority; + } + + /** + * Set whether this factory is supposed to create daemon threads, + * just executing as long as the application itself is running. + *

    Default is "false": Concrete factories usually support explicit cancelling. + * Hence, if the application shuts down, Runnables will by default finish their + * execution. + *

    Specify "true" for eager shutdown of threads which still actively execute + * a {@link Runnable} at the time that the application itself shuts down. + * @see java.lang.Thread#setDaemon + */ + public void setDaemon(boolean daemon) { + this.daemon = daemon; + } + + /** + * Return whether this factory should create daemon threads. + */ + public boolean isDaemon() { + return this.daemon; + } + + /** + * Specify the name of the thread group that threads should be created in. + * @see #setThreadGroup + */ + public void setThreadGroupName(String name) { + this.threadGroup = new ThreadGroup(name); + } + + /** + * Specify the thread group that threads should be created in. + * @see #setThreadGroupName + */ + public void setThreadGroup(@Nullable ThreadGroup threadGroup) { + this.threadGroup = threadGroup; + } + + /** + * Return the thread group that threads should be created in + * (or {@code null} for the default group). + */ + @Nullable + public ThreadGroup getThreadGroup() { + return this.threadGroup; + } + + + /** + * Template method for the creation of a new {@link Thread}. + *

    The default implementation creates a new Thread for the given + * {@link Runnable}, applying an appropriate thread name. + * @param runnable the Runnable to execute + * @see #nextThreadName() + */ + public Thread createThread(Runnable runnable) { + Thread thread = new Thread(getThreadGroup(), runnable, nextThreadName()); + thread.setPriority(getThreadPriority()); + thread.setDaemon(isDaemon()); + return thread; + } + + /** + * Return the thread name to use for a newly created {@link Thread}. + *

    The default implementation returns the specified thread name prefix + * with an increasing thread count appended: e.g. "SimpleAsyncTaskExecutor-0". + * @see #getThreadNamePrefix() + */ + protected String nextThreadName() { + return getThreadNamePrefix() + this.threadCount.incrementAndGet(); + } + + /** + * Build the default thread name prefix for this factory. + * @return the default thread name prefix (never {@code null}) + */ + protected String getDefaultThreadNamePrefix() { + return ClassUtils.getShortName(getClass()) + "-"; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/DefaultPropertiesPersister.java b/spring-core/src/main/java/org/springframework/util/DefaultPropertiesPersister.java new file mode 100644 index 0000000..a369e33 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/DefaultPropertiesPersister.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; +import java.util.Properties; + +/** + * Default implementation of the {@link PropertiesPersister} interface. + * Follows the native parsing of {@code java.util.Properties}. + * + *

    Allows for reading from any Reader and writing to any Writer, for example + * to specify a charset for a properties file. This is a capability that standard + * {@code java.util.Properties} unfortunately lacked up until JDK 5: + * You were only able to load files using the ISO-8859-1 charset there. + * + *

    Loading from and storing to a stream delegates to {@code Properties.load} + * and {@code Properties.store}, respectively, to be fully compatible with + * the Unicode conversion as implemented by the JDK Properties class. As of JDK 6, + * {@code Properties.load/store} will also be used for readers/writers, + * effectively turning this class into a plain backwards compatibility adapter. + * + *

    The persistence code that works with Reader/Writer follows the JDK's parsing + * strategy but does not implement Unicode conversion, because the Reader/Writer + * should already apply proper decoding/encoding of characters. If you prefer + * to escape unicode characters in your properties files, do not specify + * an encoding for a Reader/Writer (like ReloadableResourceBundleMessageSource's + * "defaultEncoding" and "fileEncodings" properties). + * + * @author Juergen Hoeller + * @since 10.03.2004 + * @see java.util.Properties + * @see java.util.Properties#load + * @see java.util.Properties#store + * @see org.springframework.core.io.support.ResourcePropertiesPersister + */ +public class DefaultPropertiesPersister implements PropertiesPersister { + + @Override + public void load(Properties props, InputStream is) throws IOException { + props.load(is); + } + + @Override + public void load(Properties props, Reader reader) throws IOException { + props.load(reader); + } + + @Override + public void store(Properties props, OutputStream os, String header) throws IOException { + props.store(os, header); + } + + @Override + public void store(Properties props, Writer writer, String header) throws IOException { + props.store(writer, header); + } + + @Override + public void loadFromXml(Properties props, InputStream is) throws IOException { + props.loadFromXML(is); + } + + @Override + public void storeToXml(Properties props, OutputStream os, String header) throws IOException { + props.storeToXML(os, header); + } + + @Override + public void storeToXml(Properties props, OutputStream os, String header, String encoding) throws IOException { + props.storeToXML(os, header, encoding); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/DigestUtils.java b/spring-core/src/main/java/org/springframework/util/DigestUtils.java new file mode 100644 index 0000000..b76a2b4 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/DigestUtils.java @@ -0,0 +1,184 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.io.IOException; +import java.io.InputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Miscellaneous methods for calculating digests. + * + *

    Mainly for internal use within the framework; consider + * Apache Commons Codec + * for a more comprehensive suite of digest utilities. + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @author Craig Andrews + * @since 3.0 + */ +public abstract class DigestUtils { + + private static final String MD5_ALGORITHM_NAME = "MD5"; + + private static final char[] HEX_CHARS = + {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + + /** + * Calculate the MD5 digest of the given bytes. + * @param bytes the bytes to calculate the digest over + * @return the digest + */ + public static byte[] md5Digest(byte[] bytes) { + return digest(MD5_ALGORITHM_NAME, bytes); + } + + /** + * Calculate the MD5 digest of the given stream. + *

    This method does not close the input stream. + * @param inputStream the InputStream to calculate the digest over + * @return the digest + * @since 4.2 + */ + public static byte[] md5Digest(InputStream inputStream) throws IOException { + return digest(MD5_ALGORITHM_NAME, inputStream); + } + + /** + * Return a hexadecimal string representation of the MD5 digest of the given bytes. + * @param bytes the bytes to calculate the digest over + * @return a hexadecimal digest string + */ + public static String md5DigestAsHex(byte[] bytes) { + return digestAsHexString(MD5_ALGORITHM_NAME, bytes); + } + + /** + * Return a hexadecimal string representation of the MD5 digest of the given stream. + *

    This method does not close the input stream. + * @param inputStream the InputStream to calculate the digest over + * @return a hexadecimal digest string + * @since 4.2 + */ + public static String md5DigestAsHex(InputStream inputStream) throws IOException { + return digestAsHexString(MD5_ALGORITHM_NAME, inputStream); + } + + /** + * Append a hexadecimal string representation of the MD5 digest of the given + * bytes to the given {@link StringBuilder}. + * @param bytes the bytes to calculate the digest over + * @param builder the string builder to append the digest to + * @return the given string builder + */ + public static StringBuilder appendMd5DigestAsHex(byte[] bytes, StringBuilder builder) { + return appendDigestAsHex(MD5_ALGORITHM_NAME, bytes, builder); + } + + /** + * Append a hexadecimal string representation of the MD5 digest of the given + * inputStream to the given {@link StringBuilder}. + *

    This method does not close the input stream. + * @param inputStream the inputStream to calculate the digest over + * @param builder the string builder to append the digest to + * @return the given string builder + * @since 4.2 + */ + public static StringBuilder appendMd5DigestAsHex(InputStream inputStream, StringBuilder builder) throws IOException { + return appendDigestAsHex(MD5_ALGORITHM_NAME, inputStream, builder); + } + + + /** + * Create a new {@link MessageDigest} with the given algorithm. + *

    Necessary because {@code MessageDigest} is not thread-safe. + */ + private static MessageDigest getDigest(String algorithm) { + try { + return MessageDigest.getInstance(algorithm); + } + catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException("Could not find MessageDigest with algorithm \"" + algorithm + "\"", ex); + } + } + + private static byte[] digest(String algorithm, byte[] bytes) { + return getDigest(algorithm).digest(bytes); + } + + private static byte[] digest(String algorithm, InputStream inputStream) throws IOException { + MessageDigest messageDigest = getDigest(algorithm); + if (inputStream instanceof UpdateMessageDigestInputStream){ + ((UpdateMessageDigestInputStream) inputStream).updateMessageDigest(messageDigest); + return messageDigest.digest(); + } + else { + final byte[] buffer = new byte[StreamUtils.BUFFER_SIZE]; + int bytesRead = -1; + while ((bytesRead = inputStream.read(buffer)) != -1) { + messageDigest.update(buffer, 0, bytesRead); + } + return messageDigest.digest(); + } + } + + private static String digestAsHexString(String algorithm, byte[] bytes) { + char[] hexDigest = digestAsHexChars(algorithm, bytes); + return new String(hexDigest); + } + + private static String digestAsHexString(String algorithm, InputStream inputStream) throws IOException { + char[] hexDigest = digestAsHexChars(algorithm, inputStream); + return new String(hexDigest); + } + + private static StringBuilder appendDigestAsHex(String algorithm, byte[] bytes, StringBuilder builder) { + char[] hexDigest = digestAsHexChars(algorithm, bytes); + return builder.append(hexDigest); + } + + private static StringBuilder appendDigestAsHex(String algorithm, InputStream inputStream, StringBuilder builder) + throws IOException { + + char[] hexDigest = digestAsHexChars(algorithm, inputStream); + return builder.append(hexDigest); + } + + private static char[] digestAsHexChars(String algorithm, byte[] bytes) { + byte[] digest = digest(algorithm, bytes); + return encodeHex(digest); + } + + private static char[] digestAsHexChars(String algorithm, InputStream inputStream) throws IOException { + byte[] digest = digest(algorithm, inputStream); + return encodeHex(digest); + } + + private static char[] encodeHex(byte[] bytes) { + char[] chars = new char[32]; + for (int i = 0; i < chars.length; i = i + 2) { + byte b = bytes[i / 2]; + chars[i] = HEX_CHARS[(b >>> 0x4) & 0xf]; + chars[i + 1] = HEX_CHARS[b & 0xf]; + } + return chars; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/ErrorHandler.java b/spring-core/src/main/java/org/springframework/util/ErrorHandler.java new file mode 100644 index 0000000..19b32b4 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/ErrorHandler.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +/** + * A strategy for handling errors. This is especially useful for handling + * errors that occur during asynchronous execution of tasks that have been + * submitted to a TaskScheduler. In such cases, it may not be possible to + * throw the error to the original caller. + * + * @author Mark Fisher + * @since 3.0 + */ +@FunctionalInterface +public interface ErrorHandler { + + /** + * Handle the given error, possibly rethrowing it as a fatal exception. + */ + void handleError(Throwable t); + +} diff --git a/spring-core/src/main/java/org/springframework/util/ExceptionTypeFilter.java b/spring-core/src/main/java/org/springframework/util/ExceptionTypeFilter.java new file mode 100644 index 0000000..6686924 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/ExceptionTypeFilter.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.util.Collection; + +/** + * An {@link InstanceFilter} implementation that handles exception types. A type + * will match against a given candidate if it is assignable to that candidate. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public class ExceptionTypeFilter extends InstanceFilter> { + + public ExceptionTypeFilter(Collection> includes, + Collection> excludes, boolean matchIfEmpty) { + + super(includes, excludes, matchIfEmpty); + } + + @Override + protected boolean match(Class instance, Class candidate) { + return candidate.isAssignableFrom(instance); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/FastByteArrayOutputStream.java b/spring-core/src/main/java/org/springframework/util/FastByteArrayOutputStream.java new file mode 100644 index 0000000..d52e336 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/FastByteArrayOutputStream.java @@ -0,0 +1,527 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.MessageDigest; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Iterator; + +import org.springframework.lang.Nullable; + +/** + * A speedy alternative to {@link java.io.ByteArrayOutputStream}. Note that + * this variant does not extend {@code ByteArrayOutputStream}, unlike + * its sibling {@link ResizableByteArrayOutputStream}. + * + *

    Unlike {@link java.io.ByteArrayOutputStream}, this implementation is backed + * by an {@link java.util.ArrayDeque} of {@code byte[]} instead of 1 constantly + * resizing {@code byte[]}. It does not copy buffers when it gets expanded. + * + *

    The initial buffer is only created when the stream is first written. + * There is also no copying of the internal buffer if its contents is extracted + * with the {@link #writeTo(OutputStream)} method. + * + * @author Craig Andrews + * @author Juergen Hoeller + * @since 4.2 + * @see #resize + * @see ResizableByteArrayOutputStream + */ +public class FastByteArrayOutputStream extends OutputStream { + + private static final int DEFAULT_BLOCK_SIZE = 256; + + + // The buffers used to store the content bytes + private final Deque buffers = new ArrayDeque<>(); + + // The size, in bytes, to use when allocating the first byte[] + private final int initialBlockSize; + + // The size, in bytes, to use when allocating the next byte[] + private int nextBlockSize = 0; + + // The number of bytes in previous buffers. + // (The number of bytes in the current buffer is in 'index'.) + private int alreadyBufferedSize = 0; + + // The index in the byte[] found at buffers.getLast() to be written next + private int index = 0; + + // Is the stream closed? + private boolean closed = false; + + + /** + * Create a new FastByteArrayOutputStream + * with the default initial capacity of 256 bytes. + */ + public FastByteArrayOutputStream() { + this(DEFAULT_BLOCK_SIZE); + } + + /** + * Create a new FastByteArrayOutputStream + * with the specified initial capacity. + * @param initialBlockSize the initial buffer size in bytes + */ + public FastByteArrayOutputStream(int initialBlockSize) { + Assert.isTrue(initialBlockSize > 0, "Initial block size must be greater than 0"); + this.initialBlockSize = initialBlockSize; + this.nextBlockSize = initialBlockSize; + } + + + // Overridden methods + + @Override + public void write(int datum) throws IOException { + if (this.closed) { + throw new IOException("Stream closed"); + } + else { + if (this.buffers.peekLast() == null || this.buffers.getLast().length == this.index) { + addBuffer(1); + } + // store the byte + this.buffers.getLast()[this.index++] = (byte) datum; + } + } + + @Override + public void write(byte[] data, int offset, int length) throws IOException { + if (offset < 0 || offset + length > data.length || length < 0) { + throw new IndexOutOfBoundsException(); + } + else if (this.closed) { + throw new IOException("Stream closed"); + } + else { + if (this.buffers.peekLast() == null || this.buffers.getLast().length == this.index) { + addBuffer(length); + } + if (this.index + length > this.buffers.getLast().length) { + int pos = offset; + do { + if (this.index == this.buffers.getLast().length) { + addBuffer(length); + } + int copyLength = this.buffers.getLast().length - this.index; + if (length < copyLength) { + copyLength = length; + } + System.arraycopy(data, pos, this.buffers.getLast(), this.index, copyLength); + pos += copyLength; + this.index += copyLength; + length -= copyLength; + } + while (length > 0); + } + else { + // copy in the sub-array + System.arraycopy(data, offset, this.buffers.getLast(), this.index, length); + this.index += length; + } + } + } + + @Override + public void close() { + this.closed = true; + } + + /** + * Convert the buffer's contents into a string decoding bytes using the + * platform's default character set. The length of the new String + * is a function of the character set, and hence may not be equal to the + * size of the buffer. + *

    This method always replaces malformed-input and unmappable-character + * sequences with the default replacement string for the platform's + * default character set. The {@linkplain java.nio.charset.CharsetDecoder} + * class should be used when more control over the decoding process is + * required. + * @return a String decoded from the buffer's contents + */ + @Override + public String toString() { + return new String(toByteArrayUnsafe()); + } + + + // Custom methods + + /** + * Return the number of bytes stored in this FastByteArrayOutputStream. + */ + public int size() { + return (this.alreadyBufferedSize + this.index); + } + + /** + * Convert the stream's data to a byte array and return the byte array. + *

    Also replaces the internal structures with the byte array to conserve memory: + * if the byte array is being made anyways, mind as well as use it. This approach + * also means that if this method is called twice without any writes in between, + * the second call is a no-op. + *

    This method is "unsafe" as it returns the internal buffer. + * Callers should not modify the returned buffer. + * @return the current contents of this output stream, as a byte array. + * @see #size() + * @see #toByteArray() + */ + public byte[] toByteArrayUnsafe() { + int totalSize = size(); + if (totalSize == 0) { + return new byte[0]; + } + resize(totalSize); + return this.buffers.getFirst(); + } + + /** + * Creates a newly allocated byte array. + *

    Its size is the current + * size of this output stream and the valid contents of the buffer + * have been copied into it.

    + * @return the current contents of this output stream, as a byte array. + * @see #size() + * @see #toByteArrayUnsafe() + */ + public byte[] toByteArray() { + byte[] bytesUnsafe = toByteArrayUnsafe(); + return bytesUnsafe.clone(); + } + + /** + * Reset the contents of this FastByteArrayOutputStream. + *

    All currently accumulated output in the output stream is discarded. + * The output stream can be used again. + */ + public void reset() { + this.buffers.clear(); + this.nextBlockSize = this.initialBlockSize; + this.closed = false; + this.index = 0; + this.alreadyBufferedSize = 0; + } + + /** + * Get an {@link InputStream} to retrieve the data in this OutputStream. + *

    Note that if any methods are called on the OutputStream + * (including, but not limited to, any of the write methods, {@link #reset()}, + * {@link #toByteArray()}, and {@link #toByteArrayUnsafe()}) then the + * {@link java.io.InputStream}'s behavior is undefined. + * @return {@link InputStream} of the contents of this OutputStream + */ + public InputStream getInputStream() { + return new FastByteArrayInputStream(this); + } + + /** + * Write the buffers content to the given OutputStream. + * @param out the OutputStream to write to + */ + public void writeTo(OutputStream out) throws IOException { + Iterator it = this.buffers.iterator(); + while (it.hasNext()) { + byte[] bytes = it.next(); + if (it.hasNext()) { + out.write(bytes, 0, bytes.length); + } + else { + out.write(bytes, 0, this.index); + } + } + } + + /** + * Resize the internal buffer size to a specified capacity. + * @param targetCapacity the desired size of the buffer + * @throws IllegalArgumentException if the given capacity is smaller than + * the actual size of the content stored in the buffer already + * @see FastByteArrayOutputStream#size() + */ + public void resize(int targetCapacity) { + Assert.isTrue(targetCapacity >= size(), "New capacity must not be smaller than current size"); + if (this.buffers.peekFirst() == null) { + this.nextBlockSize = targetCapacity - size(); + } + else if (size() == targetCapacity && this.buffers.getFirst().length == targetCapacity) { + // do nothing - already at the targetCapacity + } + else { + int totalSize = size(); + byte[] data = new byte[targetCapacity]; + int pos = 0; + Iterator it = this.buffers.iterator(); + while (it.hasNext()) { + byte[] bytes = it.next(); + if (it.hasNext()) { + System.arraycopy(bytes, 0, data, pos, bytes.length); + pos += bytes.length; + } + else { + System.arraycopy(bytes, 0, data, pos, this.index); + } + } + this.buffers.clear(); + this.buffers.add(data); + this.index = totalSize; + this.alreadyBufferedSize = 0; + } + } + + /** + * Create a new buffer and store it in the ArrayDeque. + *

    Adds a new buffer that can store at least {@code minCapacity} bytes. + */ + private void addBuffer(int minCapacity) { + if (this.buffers.peekLast() != null) { + this.alreadyBufferedSize += this.index; + this.index = 0; + } + if (this.nextBlockSize < minCapacity) { + this.nextBlockSize = nextPowerOf2(minCapacity); + } + this.buffers.add(new byte[this.nextBlockSize]); + this.nextBlockSize *= 2; // block size doubles each time + } + + /** + * Get the next power of 2 of a number (ex, the next power of 2 of 119 is 128). + */ + private static int nextPowerOf2(int val) { + val--; + val = (val >> 1) | val; + val = (val >> 2) | val; + val = (val >> 4) | val; + val = (val >> 8) | val; + val = (val >> 16) | val; + val++; + return val; + } + + + /** + * An implementation of {@link java.io.InputStream} that reads from a given + * FastByteArrayOutputStream. + */ + private static final class FastByteArrayInputStream extends UpdateMessageDigestInputStream { + + private final FastByteArrayOutputStream fastByteArrayOutputStream; + + private final Iterator buffersIterator; + + @Nullable + private byte[] currentBuffer; + + private int currentBufferLength = 0; + + private int nextIndexInCurrentBuffer = 0; + + private int totalBytesRead = 0; + + /** + * Create a new FastByteArrayOutputStreamInputStream backed + * by the given FastByteArrayOutputStream. + */ + public FastByteArrayInputStream(FastByteArrayOutputStream fastByteArrayOutputStream) { + this.fastByteArrayOutputStream = fastByteArrayOutputStream; + this.buffersIterator = fastByteArrayOutputStream.buffers.iterator(); + if (this.buffersIterator.hasNext()) { + this.currentBuffer = this.buffersIterator.next(); + if (this.currentBuffer == fastByteArrayOutputStream.buffers.getLast()) { + this.currentBufferLength = fastByteArrayOutputStream.index; + } + else { + this.currentBufferLength = (this.currentBuffer != null ? this.currentBuffer.length : 0); + } + } + } + + @Override + public int read() { + if (this.currentBuffer == null) { + // This stream doesn't have any data in it... + return -1; + } + else { + if (this.nextIndexInCurrentBuffer < this.currentBufferLength) { + this.totalBytesRead++; + return this.currentBuffer[this.nextIndexInCurrentBuffer++] & 0xFF; + } + else { + if (this.buffersIterator.hasNext()) { + this.currentBuffer = this.buffersIterator.next(); + updateCurrentBufferLength(); + this.nextIndexInCurrentBuffer = 0; + } + else { + this.currentBuffer = null; + } + return read(); + } + } + } + + @Override + public int read(byte[] b) { + return read(b, 0, b.length); + } + + @Override + public int read(byte[] b, int off, int len) { + if (off < 0 || len < 0 || len > b.length - off) { + throw new IndexOutOfBoundsException(); + } + else if (len == 0) { + return 0; + } + else { + if (this.currentBuffer == null) { + // This stream doesn't have any data in it... + return -1; + } + else { + if (this.nextIndexInCurrentBuffer < this.currentBufferLength) { + int bytesToCopy = Math.min(len, this.currentBufferLength - this.nextIndexInCurrentBuffer); + System.arraycopy(this.currentBuffer, this.nextIndexInCurrentBuffer, b, off, bytesToCopy); + this.totalBytesRead += bytesToCopy; + this.nextIndexInCurrentBuffer += bytesToCopy; + int remaining = read(b, off + bytesToCopy, len - bytesToCopy); + return bytesToCopy + Math.max(remaining, 0); + } + else { + if (this.buffersIterator.hasNext()) { + this.currentBuffer = this.buffersIterator.next(); + updateCurrentBufferLength(); + this.nextIndexInCurrentBuffer = 0; + } + else { + this.currentBuffer = null; + } + return read(b, off, len); + } + } + } + } + + @Override + public long skip(long n) throws IOException { + if (n > Integer.MAX_VALUE) { + throw new IllegalArgumentException("n exceeds maximum (" + Integer.MAX_VALUE + "): " + n); + } + else if (n == 0) { + return 0; + } + else if (n < 0) { + throw new IllegalArgumentException("n must be 0 or greater: " + n); + } + int len = (int) n; + if (this.currentBuffer == null) { + // This stream doesn't have any data in it... + return 0; + } + else { + if (this.nextIndexInCurrentBuffer < this.currentBufferLength) { + int bytesToSkip = Math.min(len, this.currentBufferLength - this.nextIndexInCurrentBuffer); + this.totalBytesRead += bytesToSkip; + this.nextIndexInCurrentBuffer += bytesToSkip; + return (bytesToSkip + skip(len - bytesToSkip)); + } + else { + if (this.buffersIterator.hasNext()) { + this.currentBuffer = this.buffersIterator.next(); + updateCurrentBufferLength(); + this.nextIndexInCurrentBuffer = 0; + } + else { + this.currentBuffer = null; + } + return skip(len); + } + } + } + + @Override + public int available() { + return (this.fastByteArrayOutputStream.size() - this.totalBytesRead); + } + + /** + * Update the message digest with the remaining bytes in this stream. + * @param messageDigest the message digest to update + */ + @Override + public void updateMessageDigest(MessageDigest messageDigest) { + updateMessageDigest(messageDigest, available()); + } + + /** + * Update the message digest with the next len bytes in this stream. + * Avoids creating new byte arrays and use internal buffers for performance. + * @param messageDigest the message digest to update + * @param len how many bytes to read from this stream and use to update the message digest + */ + @Override + public void updateMessageDigest(MessageDigest messageDigest, int len) { + if (this.currentBuffer == null) { + // This stream doesn't have any data in it... + return; + } + else if (len == 0) { + return; + } + else if (len < 0) { + throw new IllegalArgumentException("len must be 0 or greater: " + len); + } + else { + if (this.nextIndexInCurrentBuffer < this.currentBufferLength) { + int bytesToCopy = Math.min(len, this.currentBufferLength - this.nextIndexInCurrentBuffer); + messageDigest.update(this.currentBuffer, this.nextIndexInCurrentBuffer, bytesToCopy); + this.nextIndexInCurrentBuffer += bytesToCopy; + updateMessageDigest(messageDigest, len - bytesToCopy); + } + else { + if (this.buffersIterator.hasNext()) { + this.currentBuffer = this.buffersIterator.next(); + updateCurrentBufferLength(); + this.nextIndexInCurrentBuffer = 0; + } + else { + this.currentBuffer = null; + } + updateMessageDigest(messageDigest, len); + } + } + } + + private void updateCurrentBufferLength() { + if (this.currentBuffer == this.fastByteArrayOutputStream.buffers.getLast()) { + this.currentBufferLength = this.fastByteArrayOutputStream.index; + } + else { + this.currentBufferLength = (this.currentBuffer != null ? this.currentBuffer.length : 0); + } + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/FileCopyUtils.java b/spring-core/src/main/java/org/springframework/util/FileCopyUtils.java new file mode 100644 index 0000000..37fb1a9 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/FileCopyUtils.java @@ -0,0 +1,240 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; +import java.nio.file.Files; + +import org.springframework.lang.Nullable; + +/** + * Simple utility methods for file and stream copying. All copy methods use a block size + * of 4096 bytes, and close all affected streams when done. A variation of the copy + * methods from this class that leave streams open can be found in {@link StreamUtils}. + * + *

    Mainly for use within the framework, but also useful for application code. + * + * @author Juergen Hoeller + * @author Hyunjin Choi + * @since 06.10.2003 + * @see StreamUtils + * @see FileSystemUtils + */ +public abstract class FileCopyUtils { + + /** + * The default buffer size used when copying bytes. + */ + public static final int BUFFER_SIZE = StreamUtils.BUFFER_SIZE; + + + //--------------------------------------------------------------------- + // Copy methods for java.io.File + //--------------------------------------------------------------------- + + /** + * Copy the contents of the given input File to the given output File. + * @param in the file to copy from + * @param out the file to copy to + * @return the number of bytes copied + * @throws IOException in case of I/O errors + */ + public static int copy(File in, File out) throws IOException { + Assert.notNull(in, "No input File specified"); + Assert.notNull(out, "No output File specified"); + return copy(Files.newInputStream(in.toPath()), Files.newOutputStream(out.toPath())); + } + + /** + * Copy the contents of the given byte array to the given output File. + * @param in the byte array to copy from + * @param out the file to copy to + * @throws IOException in case of I/O errors + */ + public static void copy(byte[] in, File out) throws IOException { + Assert.notNull(in, "No input byte array specified"); + Assert.notNull(out, "No output File specified"); + copy(new ByteArrayInputStream(in), Files.newOutputStream(out.toPath())); + } + + /** + * Copy the contents of the given input File into a new byte array. + * @param in the file to copy from + * @return the new byte array that has been copied to + * @throws IOException in case of I/O errors + */ + public static byte[] copyToByteArray(File in) throws IOException { + Assert.notNull(in, "No input File specified"); + return copyToByteArray(Files.newInputStream(in.toPath())); + } + + + //--------------------------------------------------------------------- + // Copy methods for java.io.InputStream / java.io.OutputStream + //--------------------------------------------------------------------- + + /** + * Copy the contents of the given InputStream to the given OutputStream. + * Closes both streams when done. + * @param in the stream to copy from + * @param out the stream to copy to + * @return the number of bytes copied + * @throws IOException in case of I/O errors + */ + public static int copy(InputStream in, OutputStream out) throws IOException { + Assert.notNull(in, "No InputStream specified"); + Assert.notNull(out, "No OutputStream specified"); + + try { + return StreamUtils.copy(in, out); + } + finally { + close(in); + close(out); + } + } + + /** + * Copy the contents of the given byte array to the given OutputStream. + * Closes the stream when done. + * @param in the byte array to copy from + * @param out the OutputStream to copy to + * @throws IOException in case of I/O errors + */ + public static void copy(byte[] in, OutputStream out) throws IOException { + Assert.notNull(in, "No input byte array specified"); + Assert.notNull(out, "No OutputStream specified"); + + try { + out.write(in); + } + finally { + close(out); + } + } + + /** + * Copy the contents of the given InputStream into a new byte array. + * Closes the stream when done. + * @param in the stream to copy from (may be {@code null} or empty) + * @return the new byte array that has been copied to (possibly empty) + * @throws IOException in case of I/O errors + */ + public static byte[] copyToByteArray(@Nullable InputStream in) throws IOException { + if (in == null) { + return new byte[0]; + } + + ByteArrayOutputStream out = new ByteArrayOutputStream(BUFFER_SIZE); + copy(in, out); + return out.toByteArray(); + } + + + //--------------------------------------------------------------------- + // Copy methods for java.io.Reader / java.io.Writer + //--------------------------------------------------------------------- + + /** + * Copy the contents of the given Reader to the given Writer. + * Closes both when done. + * @param in the Reader to copy from + * @param out the Writer to copy to + * @return the number of characters copied + * @throws IOException in case of I/O errors + */ + public static int copy(Reader in, Writer out) throws IOException { + Assert.notNull(in, "No Reader specified"); + Assert.notNull(out, "No Writer specified"); + + try { + int charCount = 0; + char[] buffer = new char[BUFFER_SIZE]; + int charsRead; + while ((charsRead = in.read(buffer)) != -1) { + out.write(buffer, 0, charsRead); + charCount += charsRead; + } + out.flush(); + return charCount; + } + finally { + close(in); + close(out); + } + } + + /** + * Copy the contents of the given String to the given Writer. + * Closes the writer when done. + * @param in the String to copy from + * @param out the Writer to copy to + * @throws IOException in case of I/O errors + */ + public static void copy(String in, Writer out) throws IOException { + Assert.notNull(in, "No input String specified"); + Assert.notNull(out, "No Writer specified"); + + try { + out.write(in); + } + finally { + close(out); + } + } + + /** + * Copy the contents of the given Reader into a String. + * Closes the reader when done. + * @param in the reader to copy from (may be {@code null} or empty) + * @return the String that has been copied to (possibly empty) + * @throws IOException in case of I/O errors + */ + public static String copyToString(@Nullable Reader in) throws IOException { + if (in == null) { + return ""; + } + + StringWriter out = new StringWriter(BUFFER_SIZE); + copy(in, out); + return out.toString(); + } + + /** + * Attempt to close the supplied {@link Closeable}, silently swallowing any + * exceptions. + * @param closeable the {@code Closeable} to close + */ + private static void close(Closeable closeable) { + try { + closeable.close(); + } + catch (IOException ex) { + // ignore + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/FileSystemUtils.java b/spring-core/src/main/java/org/springframework/util/FileSystemUtils.java new file mode 100644 index 0000000..1a532aa --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/FileSystemUtils.java @@ -0,0 +1,150 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.EnumSet; + +import org.springframework.lang.Nullable; + +import static java.nio.file.FileVisitOption.FOLLOW_LINKS; + +/** + * Utility methods for working with the file system. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 2.5.3 + * @see java.io.File + * @see java.nio.file.Path + * @see java.nio.file.Files + */ +public abstract class FileSystemUtils { + + /** + * Delete the supplied {@link File} - for directories, + * recursively delete any nested directories or files as well. + *

    Note: Like {@link File#delete()}, this method does not throw any + * exception but rather silently returns {@code false} in case of I/O + * errors. Consider using {@link #deleteRecursively(Path)} for NIO-style + * handling of I/O errors, clearly differentiating between non-existence + * and failure to delete an existing file. + * @param root the root {@code File} to delete + * @return {@code true} if the {@code File} was successfully deleted, + * otherwise {@code false} + */ + public static boolean deleteRecursively(@Nullable File root) { + if (root == null) { + return false; + } + + try { + return deleteRecursively(root.toPath()); + } + catch (IOException ex) { + return false; + } + } + + /** + * Delete the supplied {@link File} — for directories, + * recursively delete any nested directories or files as well. + * @param root the root {@code File} to delete + * @return {@code true} if the {@code File} existed and was deleted, + * or {@code false} if it did not exist + * @throws IOException in the case of I/O errors + * @since 5.0 + */ + public static boolean deleteRecursively(@Nullable Path root) throws IOException { + if (root == null) { + return false; + } + if (!Files.exists(root)) { + return false; + } + + Files.walkFileTree(root, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + return true; + } + + /** + * Recursively copy the contents of the {@code src} file/directory + * to the {@code dest} file/directory. + * @param src the source directory + * @param dest the destination directory + * @throws IOException in the case of I/O errors + */ + public static void copyRecursively(File src, File dest) throws IOException { + Assert.notNull(src, "Source File must not be null"); + Assert.notNull(dest, "Destination File must not be null"); + copyRecursively(src.toPath(), dest.toPath()); + } + + /** + * Recursively copy the contents of the {@code src} file/directory + * to the {@code dest} file/directory. + * @param src the source directory + * @param dest the destination directory + * @throws IOException in the case of I/O errors + * @since 5.0 + */ + public static void copyRecursively(Path src, Path dest) throws IOException { + Assert.notNull(src, "Source Path must not be null"); + Assert.notNull(dest, "Destination Path must not be null"); + BasicFileAttributes srcAttr = Files.readAttributes(src, BasicFileAttributes.class); + + if (srcAttr.isDirectory()) { + Files.walkFileTree(src, EnumSet.of(FOLLOW_LINKS), Integer.MAX_VALUE, new SimpleFileVisitor() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + Files.createDirectories(dest.resolve(src.relativize(dir))); + return FileVisitResult.CONTINUE; + } + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.copy(file, dest.resolve(src.relativize(file)), StandardCopyOption.REPLACE_EXISTING); + return FileVisitResult.CONTINUE; + } + }); + } + else if (srcAttr.isRegularFile()) { + Files.copy(src, dest); + } + else { + throw new IllegalArgumentException("Source File must denote a directory or file"); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/IdGenerator.java b/spring-core/src/main/java/org/springframework/util/IdGenerator.java new file mode 100644 index 0000000..0c79730 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/IdGenerator.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.util.UUID; + +/** + * Contract for generating universally unique identifiers ({@link UUID UUIDs}). + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +@FunctionalInterface +public interface IdGenerator { + + /** + * Generate a new identifier. + * @return the generated identifier + */ + UUID generateId(); + +} diff --git a/spring-core/src/main/java/org/springframework/util/InstanceFilter.java b/spring-core/src/main/java/org/springframework/util/InstanceFilter.java new file mode 100644 index 0000000..3eb6914 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/InstanceFilter.java @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.util.Collection; +import java.util.Collections; + +import org.springframework.lang.Nullable; + +/** + * A simple instance filter that checks if a given instance match based on + * a collection of includes and excludes element. + * + *

    Subclasses may want to override {@link #match(Object, Object)} to provide + * a custom matching algorithm. + * + * @author Stephane Nicoll + * @since 4.1 + * @param the instance type + */ +public class InstanceFilter { + + private final Collection includes; + + private final Collection excludes; + + private final boolean matchIfEmpty; + + + /** + * Create a new instance based on includes/excludes collections. + *

    A particular element will match if it "matches" the one of the element in the + * includes list and does not match one of the element in the excludes list. + *

    Subclasses may redefine what matching means. By default, an element match with + * another if it is equals according to {@link Object#equals(Object)} + *

    If both collections are empty, {@code matchIfEmpty} defines if + * an element matches or not. + * @param includes the collection of includes + * @param excludes the collection of excludes + * @param matchIfEmpty the matching result if both the includes and the excludes + * collections are empty + */ + public InstanceFilter(@Nullable Collection includes, + @Nullable Collection excludes, boolean matchIfEmpty) { + + this.includes = (includes != null ? includes : Collections.emptyList()); + this.excludes = (excludes != null ? excludes : Collections.emptyList()); + this.matchIfEmpty = matchIfEmpty; + } + + + /** + * Determine if the specified {code instance} matches this filter. + */ + public boolean match(T instance) { + Assert.notNull(instance, "Instance to match must not be null"); + + boolean includesSet = !this.includes.isEmpty(); + boolean excludesSet = !this.excludes.isEmpty(); + if (!includesSet && !excludesSet) { + return this.matchIfEmpty; + } + + boolean matchIncludes = match(instance, this.includes); + boolean matchExcludes = match(instance, this.excludes); + if (!includesSet) { + return !matchExcludes; + } + if (!excludesSet) { + return matchIncludes; + } + return matchIncludes && !matchExcludes; + } + + /** + * Determine if the specified {@code instance} is equal to the + * specified {@code candidate}. + * @param instance the instance to handle + * @param candidate a candidate defined by this filter + * @return {@code true} if the instance matches the candidate + */ + protected boolean match(T instance, T candidate) { + return instance.equals(candidate); + } + + /** + * Determine if the specified {@code instance} matches one of the candidates. + *

    If the candidates collection is {@code null}, returns {@code false}. + * @param instance the instance to check + * @param candidates a list of candidates + * @return {@code true} if the instance match or the candidates collection is null + */ + protected boolean match(T instance, Collection candidates) { + for (T candidate : candidates) { + if (match(instance, candidate)) { + return true; + } + } + return false; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(getClass().getSimpleName()); + sb.append(": includes=").append(this.includes); + sb.append(", excludes=").append(this.excludes); + sb.append(", matchIfEmpty=").append(this.matchIfEmpty); + return sb.toString(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/InvalidMimeTypeException.java b/spring-core/src/main/java/org/springframework/util/InvalidMimeTypeException.java new file mode 100644 index 0000000..57783de --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/InvalidMimeTypeException.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +/** + * Exception thrown from {@link MimeTypeUtils#parseMimeType(String)} in case of + * encountering an invalid content type specification String. + * + * @author Juergen Hoeller + * @author Rossen Stoyanchev + * @since 4.0 + */ +@SuppressWarnings("serial") +public class InvalidMimeTypeException extends IllegalArgumentException { + + private final String mimeType; + + + /** + * Create a new InvalidContentTypeException for the given content type. + * @param mimeType the offending media type + * @param message a detail message indicating the invalid part + */ + public InvalidMimeTypeException(String mimeType, String message) { + super("Invalid mime type \"" + mimeType + "\": " + message); + this.mimeType = mimeType; + } + + + /** + * Return the offending content type. + */ + public String getMimeType() { + return this.mimeType; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/JdkIdGenerator.java b/spring-core/src/main/java/org/springframework/util/JdkIdGenerator.java new file mode 100644 index 0000000..fd7c47f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/JdkIdGenerator.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.util.UUID; + +/** + * An {@link IdGenerator} that calls {@link java.util.UUID#randomUUID()}. + * + * @author Rossen Stoyanchev + * @since 4.1.5 + */ +public class JdkIdGenerator implements IdGenerator { + + @Override + public UUID generateId() { + return UUID.randomUUID(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java b/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java new file mode 100644 index 0000000..a3db322 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java @@ -0,0 +1,534 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.io.Serializable; +import java.util.AbstractCollection; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.Spliterator; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.springframework.lang.Nullable; + +/** + * {@link LinkedHashMap} variant that stores String keys in a case-insensitive + * manner, for example for key-based access in a results table. + * + *

    Preserves the original order as well as the original casing of keys, + * while allowing for contains, get and remove calls with any case of key. + * + *

    Does not support {@code null} keys. + * + * @author Juergen Hoeller + * @author Phillip Webb + * @since 3.0 + * @param the value type + */ +@SuppressWarnings("serial") +public class LinkedCaseInsensitiveMap implements Map, Serializable, Cloneable { + + private final LinkedHashMap targetMap; + + private final HashMap caseInsensitiveKeys; + + private final Locale locale; + + @Nullable + private transient volatile Set keySet; + + @Nullable + private transient volatile Collection values; + + @Nullable + private transient volatile Set> entrySet; + + + /** + * Create a new LinkedCaseInsensitiveMap that stores case-insensitive keys + * according to the default Locale (by default in lower case). + * @see #convertKey(String) + */ + public LinkedCaseInsensitiveMap() { + this((Locale) null); + } + + /** + * Create a new LinkedCaseInsensitiveMap that stores case-insensitive keys + * according to the given Locale (in lower case). + * @param locale the Locale to use for case-insensitive key conversion + * @see #convertKey(String) + */ + public LinkedCaseInsensitiveMap(@Nullable Locale locale) { + this(12, locale); // equivalent to LinkedHashMap's initial capacity of 16 + } + + /** + * Create a new LinkedCaseInsensitiveMap that wraps a {@link LinkedHashMap} + * with an initial capacity that can accommodate the specified number of + * elements without any immediate resize/rehash operations to be expected, + * storing case-insensitive keys according to the default Locale (in lower case). + * @param expectedSize the expected number of elements (with a corresponding + * capacity to be derived so that no resize/rehash operations are needed) + * @see CollectionUtils#newHashMap(int) + * @see #convertKey(String) + */ + public LinkedCaseInsensitiveMap(int expectedSize) { + this(expectedSize, null); + } + + /** + * Create a new LinkedCaseInsensitiveMap that wraps a {@link LinkedHashMap} + * with an initial capacity that can accommodate the specified number of + * elements without any immediate resize/rehash operations to be expected, + * storing case-insensitive keys according to the given Locale (in lower case). + * @param expectedSize the expected number of elements (with a corresponding + * capacity to be derived so that no resize/rehash operations are needed) + * @param locale the Locale to use for case-insensitive key conversion + * @see CollectionUtils#newHashMap(int) + * @see #convertKey(String) + */ + public LinkedCaseInsensitiveMap(int expectedSize, @Nullable Locale locale) { + this.targetMap = new LinkedHashMap( + (int) (expectedSize / CollectionUtils.DEFAULT_LOAD_FACTOR), CollectionUtils.DEFAULT_LOAD_FACTOR) { + @Override + public boolean containsKey(Object key) { + return LinkedCaseInsensitiveMap.this.containsKey(key); + } + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + boolean doRemove = LinkedCaseInsensitiveMap.this.removeEldestEntry(eldest); + if (doRemove) { + removeCaseInsensitiveKey(eldest.getKey()); + } + return doRemove; + } + }; + this.caseInsensitiveKeys = CollectionUtils.newHashMap(expectedSize); + this.locale = (locale != null ? locale : Locale.getDefault()); + } + + /** + * Copy constructor. + */ + @SuppressWarnings("unchecked") + private LinkedCaseInsensitiveMap(LinkedCaseInsensitiveMap other) { + this.targetMap = (LinkedHashMap) other.targetMap.clone(); + this.caseInsensitiveKeys = (HashMap) other.caseInsensitiveKeys.clone(); + this.locale = other.locale; + } + + + // Implementation of java.util.Map + + @Override + public int size() { + return this.targetMap.size(); + } + + @Override + public boolean isEmpty() { + return this.targetMap.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return (key instanceof String && this.caseInsensitiveKeys.containsKey(convertKey((String) key))); + } + + @Override + public boolean containsValue(Object value) { + return this.targetMap.containsValue(value); + } + + @Override + @Nullable + public V get(Object key) { + if (key instanceof String) { + String caseInsensitiveKey = this.caseInsensitiveKeys.get(convertKey((String) key)); + if (caseInsensitiveKey != null) { + return this.targetMap.get(caseInsensitiveKey); + } + } + return null; + } + + @Override + @Nullable + public V getOrDefault(Object key, V defaultValue) { + if (key instanceof String) { + String caseInsensitiveKey = this.caseInsensitiveKeys.get(convertKey((String) key)); + if (caseInsensitiveKey != null) { + return this.targetMap.get(caseInsensitiveKey); + } + } + return defaultValue; + } + + @Override + @Nullable + public V put(String key, @Nullable V value) { + String oldKey = this.caseInsensitiveKeys.put(convertKey(key), key); + V oldKeyValue = null; + if (oldKey != null && !oldKey.equals(key)) { + oldKeyValue = this.targetMap.remove(oldKey); + } + V oldValue = this.targetMap.put(key, value); + return (oldKeyValue != null ? oldKeyValue : oldValue); + } + + @Override + public void putAll(Map map) { + if (map.isEmpty()) { + return; + } + map.forEach(this::put); + } + + @Override + @Nullable + public V putIfAbsent(String key, @Nullable V value) { + String oldKey = this.caseInsensitiveKeys.putIfAbsent(convertKey(key), key); + if (oldKey != null) { + return this.targetMap.get(oldKey); + } + return this.targetMap.putIfAbsent(key, value); + } + + @Override + @Nullable + public V computeIfAbsent(String key, Function mappingFunction) { + String oldKey = this.caseInsensitiveKeys.putIfAbsent(convertKey(key), key); + if (oldKey != null) { + return this.targetMap.get(oldKey); + } + return this.targetMap.computeIfAbsent(key, mappingFunction); + } + + @Override + @Nullable + public V remove(Object key) { + if (key instanceof String) { + String caseInsensitiveKey = removeCaseInsensitiveKey((String) key); + if (caseInsensitiveKey != null) { + return this.targetMap.remove(caseInsensitiveKey); + } + } + return null; + } + + @Override + public void clear() { + this.caseInsensitiveKeys.clear(); + this.targetMap.clear(); + } + + @Override + public Set keySet() { + Set keySet = this.keySet; + if (keySet == null) { + keySet = new KeySet(this.targetMap.keySet()); + this.keySet = keySet; + } + return keySet; + } + + @Override + public Collection values() { + Collection values = this.values; + if (values == null) { + values = new Values(this.targetMap.values()); + this.values = values; + } + return values; + } + + @Override + public Set> entrySet() { + Set> entrySet = this.entrySet; + if (entrySet == null) { + entrySet = new EntrySet(this.targetMap.entrySet()); + this.entrySet = entrySet; + } + return entrySet; + } + + @Override + public LinkedCaseInsensitiveMap clone() { + return new LinkedCaseInsensitiveMap<>(this); + } + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || this.targetMap.equals(other)); + } + + @Override + public int hashCode() { + return this.targetMap.hashCode(); + } + + @Override + public String toString() { + return this.targetMap.toString(); + } + + + // Specific to LinkedCaseInsensitiveMap + + /** + * Return the locale used by this {@code LinkedCaseInsensitiveMap}. + * Used for case-insensitive key conversion. + * @since 4.3.10 + * @see #LinkedCaseInsensitiveMap(Locale) + * @see #convertKey(String) + */ + public Locale getLocale() { + return this.locale; + } + + /** + * Convert the given key to a case-insensitive key. + *

    The default implementation converts the key + * to lower-case according to this Map's Locale. + * @param key the user-specified key + * @return the key to use for storing + * @see String#toLowerCase(Locale) + */ + protected String convertKey(String key) { + return key.toLowerCase(getLocale()); + } + + /** + * Determine whether this map should remove the given eldest entry. + * @param eldest the candidate entry + * @return {@code true} for removing it, {@code false} for keeping it + * @see LinkedHashMap#removeEldestEntry + */ + protected boolean removeEldestEntry(Map.Entry eldest) { + return false; + } + + @Nullable + private String removeCaseInsensitiveKey(String key) { + return this.caseInsensitiveKeys.remove(convertKey(key)); + } + + + private class KeySet extends AbstractSet { + + private final Set delegate; + + KeySet(Set delegate) { + this.delegate = delegate; + } + + @Override + public int size() { + return this.delegate.size(); + } + + @Override + public boolean contains(Object o) { + return this.delegate.contains(o); + } + + @Override + public Iterator iterator() { + return new KeySetIterator(); + } + + @Override + public boolean remove(Object o) { + return LinkedCaseInsensitiveMap.this.remove(o) != null; + } + + @Override + public void clear() { + LinkedCaseInsensitiveMap.this.clear(); + } + + @Override + public Spliterator spliterator() { + return this.delegate.spliterator(); + } + + @Override + public void forEach(Consumer action) { + this.delegate.forEach(action); + } + } + + + private class Values extends AbstractCollection { + + private final Collection delegate; + + Values(Collection delegate) { + this.delegate = delegate; + } + + @Override + public int size() { + return this.delegate.size(); + } + + @Override + public boolean contains(Object o) { + return this.delegate.contains(o); + } + + @Override + public Iterator iterator() { + return new ValuesIterator(); + } + + @Override + public void clear() { + LinkedCaseInsensitiveMap.this.clear(); + } + + @Override + public Spliterator spliterator() { + return this.delegate.spliterator(); + } + + @Override + public void forEach(Consumer action) { + this.delegate.forEach(action); + } + } + + + private class EntrySet extends AbstractSet> { + + private final Set> delegate; + + public EntrySet(Set> delegate) { + this.delegate = delegate; + } + + @Override + public int size() { + return this.delegate.size(); + } + + @Override + public boolean contains(Object o) { + return this.delegate.contains(o); + } + + @Override + public Iterator> iterator() { + return new EntrySetIterator(); + } + + @Override + @SuppressWarnings("unchecked") + public boolean remove(Object o) { + if (this.delegate.remove(o)) { + removeCaseInsensitiveKey(((Map.Entry) o).getKey()); + return true; + } + return false; + } + + @Override + public void clear() { + this.delegate.clear(); + caseInsensitiveKeys.clear(); + } + + @Override + public Spliterator> spliterator() { + return this.delegate.spliterator(); + } + + @Override + public void forEach(Consumer> action) { + this.delegate.forEach(action); + } + } + + + private abstract class EntryIterator implements Iterator { + + private final Iterator> delegate; + + @Nullable + private Entry last; + + public EntryIterator() { + this.delegate = targetMap.entrySet().iterator(); + } + + protected Entry nextEntry() { + Entry entry = this.delegate.next(); + this.last = entry; + return entry; + } + + @Override + public boolean hasNext() { + return this.delegate.hasNext(); + } + + @Override + public void remove() { + this.delegate.remove(); + if (this.last != null) { + removeCaseInsensitiveKey(this.last.getKey()); + this.last = null; + } + } + } + + + private class KeySetIterator extends EntryIterator { + + @Override + public String next() { + return nextEntry().getKey(); + } + } + + + private class ValuesIterator extends EntryIterator { + + @Override + public V next() { + return nextEntry().getValue(); + } + } + + + private class EntrySetIterator extends EntryIterator> { + + @Override + public Entry next() { + return nextEntry(); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/LinkedMultiValueMap.java b/spring-core/src/main/java/org/springframework/util/LinkedMultiValueMap.java new file mode 100644 index 0000000..8faf71e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/LinkedMultiValueMap.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Simple implementation of {@link MultiValueMap} that wraps a {@link LinkedHashMap}, + * storing multiple values in an {@link ArrayList}. + * + *

    This Map implementation is generally not thread-safe. It is primarily designed + * for data structures exposed from request objects, for use in a single thread only. + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @since 3.0 + * @param the key type + * @param the value element type + */ +public class LinkedMultiValueMap extends MultiValueMapAdapter // new public base class in 5.3 + implements Serializable, Cloneable { + + private static final long serialVersionUID = 3801124242820219131L; + + + /** + * Create a new LinkedMultiValueMap that wraps a {@link LinkedHashMap}. + */ + public LinkedMultiValueMap() { + super(new LinkedHashMap<>()); + } + + /** + * Create a new LinkedMultiValueMap that wraps a {@link LinkedHashMap} + * with an initial capacity that can accommodate the specified number of + * elements without any immediate resize/rehash operations to be expected. + * @param expectedSize the expected number of elements (with a corresponding + * capacity to be derived so that no resize/rehash operations are needed) + * @see CollectionUtils#newLinkedHashMap(int) + */ + public LinkedMultiValueMap(int expectedSize) { + super(CollectionUtils.newLinkedHashMap(expectedSize)); + } + + /** + * Copy constructor: Create a new LinkedMultiValueMap with the same mappings as + * the specified Map. Note that this will be a shallow copy; its value-holding + * List entries will get reused and therefore cannot get modified independently. + * @param otherMap the Map whose mappings are to be placed in this Map + * @see #clone() + * @see #deepCopy() + */ + public LinkedMultiValueMap(Map> otherMap) { + super(new LinkedHashMap<>(otherMap)); + } + + + /** + * Create a deep copy of this Map. + * @return a copy of this Map, including a copy of each value-holding List entry + * (consistently using an independent modifiable {@link ArrayList} for each entry) + * along the lines of {@code MultiValueMap.addAll} semantics + * @since 4.2 + * @see #addAll(MultiValueMap) + * @see #clone() + */ + public LinkedMultiValueMap deepCopy() { + LinkedMultiValueMap copy = new LinkedMultiValueMap<>(size()); + forEach((key, values) -> copy.put(key, new ArrayList<>(values))); + return copy; + } + + /** + * Create a regular copy of this Map. + * @return a shallow copy of this Map, reusing this Map's value-holding List entries + * (even if some entries are shared or unmodifiable) along the lines of standard + * {@code Map.put} semantics + * @since 4.2 + * @see #put(Object, List) + * @see #putAll(Map) + * @see LinkedMultiValueMap#LinkedMultiValueMap(Map) + * @see #deepCopy() + */ + @Override + public LinkedMultiValueMap clone() { + return new LinkedMultiValueMap<>(this); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/MethodInvoker.java b/spring-core/src/main/java/org/springframework/util/MethodInvoker.java new file mode 100644 index 0000000..8672b01 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/MethodInvoker.java @@ -0,0 +1,337 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import org.springframework.lang.Nullable; + +/** + * Helper class that allows for specifying a method to invoke in a declarative + * fashion, be it static or non-static. + * + *

    Usage: Specify "targetClass"/"targetMethod" or "targetObject"/"targetMethod", + * optionally specify arguments, prepare the invoker. Afterwards, you may + * invoke the method any number of times, obtaining the invocation result. + * + * @author Colin Sampaleanu + * @author Juergen Hoeller + * @since 19.02.2004 + * @see #prepare + * @see #invoke + */ +public class MethodInvoker { + + private static final Object[] EMPTY_ARGUMENTS = new Object[0]; + + + @Nullable + protected Class targetClass; + + @Nullable + private Object targetObject; + + @Nullable + private String targetMethod; + + @Nullable + private String staticMethod; + + @Nullable + private Object[] arguments; + + /** The method we will call. */ + @Nullable + private Method methodObject; + + + /** + * Set the target class on which to call the target method. + * Only necessary when the target method is static; else, + * a target object needs to be specified anyway. + * @see #setTargetObject + * @see #setTargetMethod + */ + public void setTargetClass(@Nullable Class targetClass) { + this.targetClass = targetClass; + } + + /** + * Return the target class on which to call the target method. + */ + @Nullable + public Class getTargetClass() { + return this.targetClass; + } + + /** + * Set the target object on which to call the target method. + * Only necessary when the target method is not static; + * else, a target class is sufficient. + * @see #setTargetClass + * @see #setTargetMethod + */ + public void setTargetObject(@Nullable Object targetObject) { + this.targetObject = targetObject; + if (targetObject != null) { + this.targetClass = targetObject.getClass(); + } + } + + /** + * Return the target object on which to call the target method. + */ + @Nullable + public Object getTargetObject() { + return this.targetObject; + } + + /** + * Set the name of the method to be invoked. + * Refers to either a static method or a non-static method, + * depending on a target object being set. + * @see #setTargetClass + * @see #setTargetObject + */ + public void setTargetMethod(@Nullable String targetMethod) { + this.targetMethod = targetMethod; + } + + /** + * Return the name of the method to be invoked. + */ + @Nullable + public String getTargetMethod() { + return this.targetMethod; + } + + /** + * Set a fully qualified static method name to invoke, + * e.g. "example.MyExampleClass.myExampleMethod". + * Convenient alternative to specifying targetClass and targetMethod. + * @see #setTargetClass + * @see #setTargetMethod + */ + public void setStaticMethod(String staticMethod) { + this.staticMethod = staticMethod; + } + + /** + * Set arguments for the method invocation. If this property is not set, + * or the Object array is of length 0, a method with no arguments is assumed. + */ + public void setArguments(Object... arguments) { + this.arguments = arguments; + } + + /** + * Return the arguments for the method invocation. + */ + public Object[] getArguments() { + return (this.arguments != null ? this.arguments : EMPTY_ARGUMENTS); + } + + + /** + * Prepare the specified method. + * The method can be invoked any number of times afterwards. + * @see #getPreparedMethod + * @see #invoke + */ + public void prepare() throws ClassNotFoundException, NoSuchMethodException { + if (this.staticMethod != null) { + int lastDotIndex = this.staticMethod.lastIndexOf('.'); + if (lastDotIndex == -1 || lastDotIndex == this.staticMethod.length()) { + throw new IllegalArgumentException( + "staticMethod must be a fully qualified class plus method name: " + + "e.g. 'example.MyExampleClass.myExampleMethod'"); + } + String className = this.staticMethod.substring(0, lastDotIndex); + String methodName = this.staticMethod.substring(lastDotIndex + 1); + this.targetClass = resolveClassName(className); + this.targetMethod = methodName; + } + + Class targetClass = getTargetClass(); + String targetMethod = getTargetMethod(); + Assert.notNull(targetClass, "Either 'targetClass' or 'targetObject' is required"); + Assert.notNull(targetMethod, "Property 'targetMethod' is required"); + + Object[] arguments = getArguments(); + Class[] argTypes = new Class[arguments.length]; + for (int i = 0; i < arguments.length; ++i) { + argTypes[i] = (arguments[i] != null ? arguments[i].getClass() : Object.class); + } + + // Try to get the exact method first. + try { + this.methodObject = targetClass.getMethod(targetMethod, argTypes); + } + catch (NoSuchMethodException ex) { + // Just rethrow exception if we can't get any match. + this.methodObject = findMatchingMethod(); + if (this.methodObject == null) { + throw ex; + } + } + } + + /** + * Resolve the given class name into a Class. + *

    The default implementations uses {@code ClassUtils.forName}, + * using the thread context class loader. + * @param className the class name to resolve + * @return the resolved Class + * @throws ClassNotFoundException if the class name was invalid + */ + protected Class resolveClassName(String className) throws ClassNotFoundException { + return ClassUtils.forName(className, ClassUtils.getDefaultClassLoader()); + } + + /** + * Find a matching method with the specified name for the specified arguments. + * @return a matching method, or {@code null} if none + * @see #getTargetClass() + * @see #getTargetMethod() + * @see #getArguments() + */ + @Nullable + protected Method findMatchingMethod() { + String targetMethod = getTargetMethod(); + Object[] arguments = getArguments(); + int argCount = arguments.length; + + Class targetClass = getTargetClass(); + Assert.state(targetClass != null, "No target class set"); + Method[] candidates = ReflectionUtils.getAllDeclaredMethods(targetClass); + int minTypeDiffWeight = Integer.MAX_VALUE; + Method matchingMethod = null; + + for (Method candidate : candidates) { + if (candidate.getName().equals(targetMethod)) { + if (candidate.getParameterCount() == argCount) { + Class[] paramTypes = candidate.getParameterTypes(); + int typeDiffWeight = getTypeDifferenceWeight(paramTypes, arguments); + if (typeDiffWeight < minTypeDiffWeight) { + minTypeDiffWeight = typeDiffWeight; + matchingMethod = candidate; + } + } + } + } + + return matchingMethod; + } + + /** + * Return the prepared Method object that will be invoked. + *

    Can for example be used to determine the return type. + * @return the prepared Method object (never {@code null}) + * @throws IllegalStateException if the invoker hasn't been prepared yet + * @see #prepare + * @see #invoke + */ + public Method getPreparedMethod() throws IllegalStateException { + if (this.methodObject == null) { + throw new IllegalStateException("prepare() must be called prior to invoke() on MethodInvoker"); + } + return this.methodObject; + } + + /** + * Return whether this invoker has been prepared already, + * i.e. whether it allows access to {@link #getPreparedMethod()} already. + */ + public boolean isPrepared() { + return (this.methodObject != null); + } + + /** + * Invoke the specified method. + *

    The invoker needs to have been prepared before. + * @return the object (possibly null) returned by the method invocation, + * or {@code null} if the method has a void return type + * @throws InvocationTargetException if the target method threw an exception + * @throws IllegalAccessException if the target method couldn't be accessed + * @see #prepare + */ + @Nullable + public Object invoke() throws InvocationTargetException, IllegalAccessException { + // In the static case, target will simply be {@code null}. + Object targetObject = getTargetObject(); + Method preparedMethod = getPreparedMethod(); + if (targetObject == null && !Modifier.isStatic(preparedMethod.getModifiers())) { + throw new IllegalArgumentException("Target method must not be non-static without a target"); + } + ReflectionUtils.makeAccessible(preparedMethod); + return preparedMethod.invoke(targetObject, getArguments()); + } + + + /** + * Algorithm that judges the match between the declared parameter types of a candidate method + * and a specific list of arguments that this method is supposed to be invoked with. + *

    Determines a weight that represents the class hierarchy difference between types and + * arguments. A direct match, i.e. type Integer -> arg of class Integer, does not increase + * the result - all direct matches means weight 0. A match between type Object and arg of + * class Integer would increase the weight by 2, due to the superclass 2 steps up in the + * hierarchy (i.e. Object) being the last one that still matches the required type Object. + * Type Number and class Integer would increase the weight by 1 accordingly, due to the + * superclass 1 step up the hierarchy (i.e. Number) still matching the required type Number. + * Therefore, with an arg of type Integer, a constructor (Integer) would be preferred to a + * constructor (Number) which would in turn be preferred to a constructor (Object). + * All argument weights get accumulated. + *

    Note: This is the algorithm used by MethodInvoker itself and also the algorithm + * used for constructor and factory method selection in Spring's bean container (in case + * of lenient constructor resolution which is the default for regular bean definitions). + * @param paramTypes the parameter types to match + * @param args the arguments to match + * @return the accumulated weight for all arguments + */ + public static int getTypeDifferenceWeight(Class[] paramTypes, Object[] args) { + int result = 0; + for (int i = 0; i < paramTypes.length; i++) { + if (!ClassUtils.isAssignableValue(paramTypes[i], args[i])) { + return Integer.MAX_VALUE; + } + if (args[i] != null) { + Class paramType = paramTypes[i]; + Class superClass = args[i].getClass().getSuperclass(); + while (superClass != null) { + if (paramType.equals(superClass)) { + result = result + 2; + superClass = null; + } + else if (ClassUtils.isAssignable(paramType, superClass)) { + result = result + 2; + superClass = superClass.getSuperclass(); + } + else { + superClass = null; + } + } + if (paramType.isInterface()) { + result = result + 1; + } + } + } + return result; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/MimeType.java b/spring-core/src/main/java/org/springframework/util/MimeType.java new file mode 100644 index 0000000..de29040 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/MimeType.java @@ -0,0 +1,666 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.nio.charset.Charset; +import java.util.BitSet; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TreeSet; + +import org.springframework.lang.Nullable; + +/** + * Represents a MIME Type, as originally defined in RFC 2046 and subsequently + * used in other Internet protocols including HTTP. + * + *

    This class, however, does not contain support for the q-parameters used + * in HTTP content negotiation. Those can be found in the subclass + * {@code org.springframework.http.MediaType} in the {@code spring-web} module. + * + *

    Consists of a {@linkplain #getType() type} and a {@linkplain #getSubtype() subtype}. + * Also has functionality to parse MIME Type values from a {@code String} using + * {@link #valueOf(String)}. For more parsing options see {@link MimeTypeUtils}. + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @author Rossen Stoyanchev + * @author Sam Brannen + * @since 4.0 + * @see MimeTypeUtils + */ +public class MimeType implements Comparable, Serializable { + + private static final long serialVersionUID = 4085923477777865903L; + + + protected static final String WILDCARD_TYPE = "*"; + + private static final String PARAM_CHARSET = "charset"; + + private static final BitSet TOKEN; + + static { + // variable names refer to RFC 2616, section 2.2 + BitSet ctl = new BitSet(128); + for (int i = 0; i <= 31; i++) { + ctl.set(i); + } + ctl.set(127); + + BitSet separators = new BitSet(128); + separators.set('('); + separators.set(')'); + separators.set('<'); + separators.set('>'); + separators.set('@'); + separators.set(','); + separators.set(';'); + separators.set(':'); + separators.set('\\'); + separators.set('\"'); + separators.set('/'); + separators.set('['); + separators.set(']'); + separators.set('?'); + separators.set('='); + separators.set('{'); + separators.set('}'); + separators.set(' '); + separators.set('\t'); + + TOKEN = new BitSet(128); + TOKEN.set(0, 128); + TOKEN.andNot(ctl); + TOKEN.andNot(separators); + } + + + private final String type; + + private final String subtype; + + private final Map parameters; + + @Nullable + private transient Charset resolvedCharset; + + @Nullable + private volatile String toStringValue; + + + /** + * Create a new {@code MimeType} for the given primary type. + *

    The {@linkplain #getSubtype() subtype} is set to "*", + * and the parameters are empty. + * @param type the primary type + * @throws IllegalArgumentException if any of the parameters contains illegal characters + */ + public MimeType(String type) { + this(type, WILDCARD_TYPE); + } + + /** + * Create a new {@code MimeType} for the given primary type and subtype. + *

    The parameters are empty. + * @param type the primary type + * @param subtype the subtype + * @throws IllegalArgumentException if any of the parameters contains illegal characters + */ + public MimeType(String type, String subtype) { + this(type, subtype, Collections.emptyMap()); + } + + /** + * Create a new {@code MimeType} for the given type, subtype, and character set. + * @param type the primary type + * @param subtype the subtype + * @param charset the character set + * @throws IllegalArgumentException if any of the parameters contains illegal characters + */ + public MimeType(String type, String subtype, Charset charset) { + this(type, subtype, Collections.singletonMap(PARAM_CHARSET, charset.name())); + this.resolvedCharset = charset; + } + + /** + * Copy-constructor that copies the type, subtype, parameters of the given {@code MimeType}, + * and allows to set the specified character set. + * @param other the other MimeType + * @param charset the character set + * @throws IllegalArgumentException if any of the parameters contains illegal characters + * @since 4.3 + */ + public MimeType(MimeType other, Charset charset) { + this(other.getType(), other.getSubtype(), addCharsetParameter(charset, other.getParameters())); + this.resolvedCharset = charset; + } + + /** + * Copy-constructor that copies the type and subtype of the given {@code MimeType}, + * and allows for different parameter. + * @param other the other MimeType + * @param parameters the parameters (may be {@code null}) + * @throws IllegalArgumentException if any of the parameters contains illegal characters + */ + public MimeType(MimeType other, @Nullable Map parameters) { + this(other.getType(), other.getSubtype(), parameters); + } + + /** + * Create a new {@code MimeType} for the given type, subtype, and parameters. + * @param type the primary type + * @param subtype the subtype + * @param parameters the parameters (may be {@code null}) + * @throws IllegalArgumentException if any of the parameters contains illegal characters + */ + public MimeType(String type, String subtype, @Nullable Map parameters) { + Assert.hasLength(type, "'type' must not be empty"); + Assert.hasLength(subtype, "'subtype' must not be empty"); + checkToken(type); + checkToken(subtype); + this.type = type.toLowerCase(Locale.ENGLISH); + this.subtype = subtype.toLowerCase(Locale.ENGLISH); + if (!CollectionUtils.isEmpty(parameters)) { + Map map = new LinkedCaseInsensitiveMap<>(parameters.size(), Locale.ENGLISH); + parameters.forEach((parameter, value) -> { + checkParameters(parameter, value); + map.put(parameter, value); + }); + this.parameters = Collections.unmodifiableMap(map); + } + else { + this.parameters = Collections.emptyMap(); + } + } + + /** + * Copy-constructor that copies the type, subtype and parameters of the given {@code MimeType}, + * skipping checks performed in other constructors. + * @param other the other MimeType + * @since 5.3 + */ + protected MimeType(MimeType other) { + this.type = other.type; + this.subtype = other.subtype; + this.parameters = other.parameters; + this.resolvedCharset = other.resolvedCharset; + this.toStringValue = other.toStringValue; + } + + /** + * Checks the given token string for illegal characters, as defined in RFC 2616, + * section 2.2. + * @throws IllegalArgumentException in case of illegal characters + * @see HTTP 1.1, section 2.2 + */ + private void checkToken(String token) { + for (int i = 0; i < token.length(); i++) { + char ch = token.charAt(i); + if (!TOKEN.get(ch)) { + throw new IllegalArgumentException("Invalid token character '" + ch + "' in token \"" + token + "\""); + } + } + } + + protected void checkParameters(String parameter, String value) { + Assert.hasLength(parameter, "'parameter' must not be empty"); + Assert.hasLength(value, "'value' must not be empty"); + checkToken(parameter); + if (PARAM_CHARSET.equals(parameter)) { + if (this.resolvedCharset == null) { + this.resolvedCharset = Charset.forName(unquote(value)); + } + } + else if (!isQuotedString(value)) { + checkToken(value); + } + } + + private boolean isQuotedString(String s) { + if (s.length() < 2) { + return false; + } + else { + return ((s.startsWith("\"") && s.endsWith("\"")) || (s.startsWith("'") && s.endsWith("'"))); + } + } + + protected String unquote(String s) { + return (isQuotedString(s) ? s.substring(1, s.length() - 1) : s); + } + + /** + * Indicates whether the {@linkplain #getType() type} is the wildcard character + * * or not. + */ + public boolean isWildcardType() { + return WILDCARD_TYPE.equals(getType()); + } + + /** + * Indicates whether the {@linkplain #getSubtype() subtype} is the wildcard + * character * or the wildcard character followed by a suffix + * (e.g. *+xml). + * @return whether the subtype is a wildcard + */ + public boolean isWildcardSubtype() { + return WILDCARD_TYPE.equals(getSubtype()) || getSubtype().startsWith("*+"); + } + + /** + * Indicates whether this MIME Type is concrete, i.e. whether neither the type + * nor the subtype is a wildcard character *. + * @return whether this MIME Type is concrete + */ + public boolean isConcrete() { + return !isWildcardType() && !isWildcardSubtype(); + } + + /** + * Return the primary type. + */ + public String getType() { + return this.type; + } + + /** + * Return the subtype. + */ + public String getSubtype() { + return this.subtype; + } + + /** + * Return the subtype suffix as defined in RFC 6839. + * @since 5.3 + */ + @Nullable + public String getSubtypeSuffix() { + int suffixIndex = this.subtype.lastIndexOf('+'); + if (suffixIndex != -1 && this.subtype.length() > suffixIndex) { + return this.subtype.substring(suffixIndex + 1); + } + return null; + } + + /** + * Return the character set, as indicated by a {@code charset} parameter, if any. + * @return the character set, or {@code null} if not available + * @since 4.3 + */ + @Nullable + public Charset getCharset() { + return this.resolvedCharset; + } + + /** + * Return a generic parameter value, given a parameter name. + * @param name the parameter name + * @return the parameter value, or {@code null} if not present + */ + @Nullable + public String getParameter(String name) { + return this.parameters.get(name); + } + + /** + * Return all generic parameter values. + * @return a read-only map (possibly empty, never {@code null}) + */ + public Map getParameters() { + return this.parameters; + } + + /** + * Indicate whether this MIME Type includes the given MIME Type. + *

    For instance, {@code text/*} includes {@code text/plain} and {@code text/html}, + * and {@code application/*+xml} includes {@code application/soap+xml}, etc. + * This method is not symmetric. + * @param other the reference MIME Type with which to compare + * @return {@code true} if this MIME Type includes the given MIME Type; + * {@code false} otherwise + */ + public boolean includes(@Nullable MimeType other) { + if (other == null) { + return false; + } + if (isWildcardType()) { + // */* includes anything + return true; + } + else if (getType().equals(other.getType())) { + if (getSubtype().equals(other.getSubtype())) { + return true; + } + if (isWildcardSubtype()) { + // Wildcard with suffix, e.g. application/*+xml + int thisPlusIdx = getSubtype().lastIndexOf('+'); + if (thisPlusIdx == -1) { + return true; + } + else { + // application/*+xml includes application/soap+xml + int otherPlusIdx = other.getSubtype().lastIndexOf('+'); + if (otherPlusIdx != -1) { + String thisSubtypeNoSuffix = getSubtype().substring(0, thisPlusIdx); + String thisSubtypeSuffix = getSubtype().substring(thisPlusIdx + 1); + String otherSubtypeSuffix = other.getSubtype().substring(otherPlusIdx + 1); + if (thisSubtypeSuffix.equals(otherSubtypeSuffix) && WILDCARD_TYPE.equals(thisSubtypeNoSuffix)) { + return true; + } + } + } + } + } + return false; + } + + /** + * Indicate whether this MIME Type is compatible with the given MIME Type. + *

    For instance, {@code text/*} is compatible with {@code text/plain}, + * {@code text/html}, and vice versa. In effect, this method is similar to + * {@link #includes}, except that it is symmetric. + * @param other the reference MIME Type with which to compare + * @return {@code true} if this MIME Type is compatible with the given MIME Type; + * {@code false} otherwise + */ + public boolean isCompatibleWith(@Nullable MimeType other) { + if (other == null) { + return false; + } + if (isWildcardType() || other.isWildcardType()) { + return true; + } + else if (getType().equals(other.getType())) { + if (getSubtype().equals(other.getSubtype())) { + return true; + } + if (isWildcardSubtype() || other.isWildcardSubtype()) { + String thisSuffix = getSubtypeSuffix(); + String otherSuffix = other.getSubtypeSuffix(); + if (getSubtype().equals(WILDCARD_TYPE) || other.getSubtype().equals(WILDCARD_TYPE)) { + return true; + } + else if (isWildcardSubtype() && thisSuffix != null) { + return (thisSuffix.equals(other.getSubtype()) || thisSuffix.equals(otherSuffix)); + } + else if (other.isWildcardSubtype() && otherSuffix != null) { + return (this.getSubtype().equals(otherSuffix) || otherSuffix.equals(thisSuffix)); + } + } + } + return false; + } + + /** + * Similar to {@link #equals(Object)} but based on the type and subtype + * only, i.e. ignoring parameters. + * @param other the other mime type to compare to + * @return whether the two mime types have the same type and subtype + * @since 5.1.4 + */ + public boolean equalsTypeAndSubtype(@Nullable MimeType other) { + if (other == null) { + return false; + } + return this.type.equalsIgnoreCase(other.type) && this.subtype.equalsIgnoreCase(other.subtype); + } + + /** + * Unlike {@link Collection#contains(Object)} which relies on + * {@link MimeType#equals(Object)}, this method only checks the type and the + * subtype, but otherwise ignores parameters. + * @param mimeTypes the list of mime types to perform the check against + * @return whether the list contains the given mime type + * @since 5.1.4 + */ + public boolean isPresentIn(Collection mimeTypes) { + for (MimeType mimeType : mimeTypes) { + if (mimeType.equalsTypeAndSubtype(this)) { + return true; + } + } + return false; + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof MimeType)) { + return false; + } + MimeType otherType = (MimeType) other; + return (this.type.equalsIgnoreCase(otherType.type) && + this.subtype.equalsIgnoreCase(otherType.subtype) && + parametersAreEqual(otherType)); + } + + /** + * Determine if the parameters in this {@code MimeType} and the supplied + * {@code MimeType} are equal, performing case-insensitive comparisons + * for {@link Charset Charsets}. + * @since 4.2 + */ + private boolean parametersAreEqual(MimeType other) { + if (this.parameters.size() != other.parameters.size()) { + return false; + } + + for (Map.Entry entry : this.parameters.entrySet()) { + String key = entry.getKey(); + if (!other.parameters.containsKey(key)) { + return false; + } + if (PARAM_CHARSET.equals(key)) { + if (!ObjectUtils.nullSafeEquals(getCharset(), other.getCharset())) { + return false; + } + } + else if (!ObjectUtils.nullSafeEquals(entry.getValue(), other.parameters.get(key))) { + return false; + } + } + + return true; + } + + @Override + public int hashCode() { + int result = this.type.hashCode(); + result = 31 * result + this.subtype.hashCode(); + result = 31 * result + this.parameters.hashCode(); + return result; + } + + @Override + public String toString() { + String value = this.toStringValue; + if (value == null) { + StringBuilder builder = new StringBuilder(); + appendTo(builder); + value = builder.toString(); + this.toStringValue = value; + } + return value; + } + + protected void appendTo(StringBuilder builder) { + builder.append(this.type); + builder.append('/'); + builder.append(this.subtype); + appendTo(this.parameters, builder); + } + + private void appendTo(Map map, StringBuilder builder) { + map.forEach((key, val) -> { + builder.append(';'); + builder.append(key); + builder.append('='); + builder.append(val); + }); + } + + /** + * Compares this MIME Type to another alphabetically. + * @param other the MIME Type to compare to + * @see MimeTypeUtils#sortBySpecificity(List) + */ + @Override + public int compareTo(MimeType other) { + int comp = getType().compareToIgnoreCase(other.getType()); + if (comp != 0) { + return comp; + } + comp = getSubtype().compareToIgnoreCase(other.getSubtype()); + if (comp != 0) { + return comp; + } + comp = getParameters().size() - other.getParameters().size(); + if (comp != 0) { + return comp; + } + + TreeSet thisAttributes = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + thisAttributes.addAll(getParameters().keySet()); + TreeSet otherAttributes = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + otherAttributes.addAll(other.getParameters().keySet()); + Iterator thisAttributesIterator = thisAttributes.iterator(); + Iterator otherAttributesIterator = otherAttributes.iterator(); + + while (thisAttributesIterator.hasNext()) { + String thisAttribute = thisAttributesIterator.next(); + String otherAttribute = otherAttributesIterator.next(); + comp = thisAttribute.compareToIgnoreCase(otherAttribute); + if (comp != 0) { + return comp; + } + if (PARAM_CHARSET.equals(thisAttribute)) { + Charset thisCharset = getCharset(); + Charset otherCharset = other.getCharset(); + if (thisCharset != otherCharset) { + if (thisCharset == null) { + return -1; + } + if (otherCharset == null) { + return 1; + } + comp = thisCharset.compareTo(otherCharset); + if (comp != 0) { + return comp; + } + } + } + else { + String thisValue = getParameters().get(thisAttribute); + String otherValue = other.getParameters().get(otherAttribute); + if (otherValue == null) { + otherValue = ""; + } + comp = thisValue.compareTo(otherValue); + if (comp != 0) { + return comp; + } + } + } + + return 0; + } + + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + // Rely on default serialization, just initialize state after deserialization. + ois.defaultReadObject(); + + // Initialize transient fields. + String charsetName = getParameter(PARAM_CHARSET); + if (charsetName != null) { + this.resolvedCharset = Charset.forName(unquote(charsetName)); + } + } + + + /** + * Parse the given String value into a {@code MimeType} object, + * with this method name following the 'valueOf' naming convention + * (as supported by {@link org.springframework.core.convert.ConversionService}. + * @see MimeTypeUtils#parseMimeType(String) + */ + public static MimeType valueOf(String value) { + return MimeTypeUtils.parseMimeType(value); + } + + private static Map addCharsetParameter(Charset charset, Map parameters) { + Map map = new LinkedHashMap<>(parameters); + map.put(PARAM_CHARSET, charset.name()); + return map; + } + + + /** + * Comparator to sort {@link MimeType MimeTypes} in order of specificity. + * + * @param the type of mime types that may be compared by this comparator + */ + public static class SpecificityComparator implements Comparator { + + @Override + public int compare(T mimeType1, T mimeType2) { + if (mimeType1.isWildcardType() && !mimeType2.isWildcardType()) { // */* < audio/* + return 1; + } + else if (mimeType2.isWildcardType() && !mimeType1.isWildcardType()) { // audio/* > */* + return -1; + } + else if (!mimeType1.getType().equals(mimeType2.getType())) { // audio/basic == text/html + return 0; + } + else { // mediaType1.getType().equals(mediaType2.getType()) + if (mimeType1.isWildcardSubtype() && !mimeType2.isWildcardSubtype()) { // audio/* < audio/basic + return 1; + } + else if (mimeType2.isWildcardSubtype() && !mimeType1.isWildcardSubtype()) { // audio/basic > audio/* + return -1; + } + else if (!mimeType1.getSubtype().equals(mimeType2.getSubtype())) { // audio/basic == audio/wave + return 0; + } + else { // mediaType2.getSubtype().equals(mediaType2.getSubtype()) + return compareParameters(mimeType1, mimeType2); + } + } + } + + protected int compareParameters(T mimeType1, T mimeType2) { + int paramsSize1 = mimeType1.getParameters().size(); + int paramsSize2 = mimeType2.getParameters().size(); + return Integer.compare(paramsSize2, paramsSize1); // audio/basic;level=1 < audio/basic + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java b/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java new file mode 100644 index 0000000..05809bc --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java @@ -0,0 +1,404 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnsupportedCharsetException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.stream.Collectors; + +import org.springframework.lang.Nullable; + +/** + * Miscellaneous {@link MimeType} utility methods. + * + * @author Arjen Poutsma + * @author Rossen Stoyanchev + * @author Dimitrios Liapis + * @author Brian Clozel + * @author Sam Brannen + * @since 4.0 + */ +public abstract class MimeTypeUtils { + + private static final byte[] BOUNDARY_CHARS = + new byte[] {'-', '_', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f', 'g', + 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', + 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', + 'V', 'W', 'X', 'Y', 'Z'}; + + /** + * Comparator used by {@link #sortBySpecificity(List)}. + */ + public static final Comparator SPECIFICITY_COMPARATOR = new MimeType.SpecificityComparator<>(); + + /** + * Public constant mime type that includes all media ranges (i.e. "*/*"). + */ + public static final MimeType ALL; + + /** + * A String equivalent of {@link MimeTypeUtils#ALL}. + */ + public static final String ALL_VALUE = "*/*"; + + /** + * Public constant mime type for {@code application/json}. + * */ + public static final MimeType APPLICATION_JSON; + + /** + * A String equivalent of {@link MimeTypeUtils#APPLICATION_JSON}. + */ + public static final String APPLICATION_JSON_VALUE = "application/json"; + + /** + * Public constant mime type for {@code application/octet-stream}. + * */ + public static final MimeType APPLICATION_OCTET_STREAM; + + /** + * A String equivalent of {@link MimeTypeUtils#APPLICATION_OCTET_STREAM}. + */ + public static final String APPLICATION_OCTET_STREAM_VALUE = "application/octet-stream"; + + /** + * Public constant mime type for {@code application/xml}. + */ + public static final MimeType APPLICATION_XML; + + /** + * A String equivalent of {@link MimeTypeUtils#APPLICATION_XML}. + */ + public static final String APPLICATION_XML_VALUE = "application/xml"; + + /** + * Public constant mime type for {@code image/gif}. + */ + public static final MimeType IMAGE_GIF; + + /** + * A String equivalent of {@link MimeTypeUtils#IMAGE_GIF}. + */ + public static final String IMAGE_GIF_VALUE = "image/gif"; + + /** + * Public constant mime type for {@code image/jpeg}. + */ + public static final MimeType IMAGE_JPEG; + + /** + * A String equivalent of {@link MimeTypeUtils#IMAGE_JPEG}. + */ + public static final String IMAGE_JPEG_VALUE = "image/jpeg"; + + /** + * Public constant mime type for {@code image/png}. + */ + public static final MimeType IMAGE_PNG; + + /** + * A String equivalent of {@link MimeTypeUtils#IMAGE_PNG}. + */ + public static final String IMAGE_PNG_VALUE = "image/png"; + + /** + * Public constant mime type for {@code text/html}. + * */ + public static final MimeType TEXT_HTML; + + /** + * A String equivalent of {@link MimeTypeUtils#TEXT_HTML}. + */ + public static final String TEXT_HTML_VALUE = "text/html"; + + /** + * Public constant mime type for {@code text/plain}. + * */ + public static final MimeType TEXT_PLAIN; + + /** + * A String equivalent of {@link MimeTypeUtils#TEXT_PLAIN}. + */ + public static final String TEXT_PLAIN_VALUE = "text/plain"; + + /** + * Public constant mime type for {@code text/xml}. + * */ + public static final MimeType TEXT_XML; + + /** + * A String equivalent of {@link MimeTypeUtils#TEXT_XML}. + */ + public static final String TEXT_XML_VALUE = "text/xml"; + + + private static final ConcurrentLruCache cachedMimeTypes = + new ConcurrentLruCache<>(64, MimeTypeUtils::parseMimeTypeInternal); + + @Nullable + private static volatile Random random; + + static { + // Not using "parseMimeType" to avoid static init cost + ALL = new MimeType("*", "*"); + APPLICATION_JSON = new MimeType("application", "json"); + APPLICATION_OCTET_STREAM = new MimeType("application", "octet-stream"); + APPLICATION_XML = new MimeType("application", "xml"); + IMAGE_GIF = new MimeType("image", "gif"); + IMAGE_JPEG = new MimeType("image", "jpeg"); + IMAGE_PNG = new MimeType("image", "png"); + TEXT_HTML = new MimeType("text", "html"); + TEXT_PLAIN = new MimeType("text", "plain"); + TEXT_XML = new MimeType("text", "xml"); + } + + + /** + * Parse the given String into a single {@code MimeType}. + * Recently parsed {@code MimeType} are cached for further retrieval. + * @param mimeType the string to parse + * @return the mime type + * @throws InvalidMimeTypeException if the string cannot be parsed + */ + public static MimeType parseMimeType(String mimeType) { + if (!StringUtils.hasLength(mimeType)) { + throw new InvalidMimeTypeException(mimeType, "'mimeType' must not be empty"); + } + // do not cache multipart mime types with random boundaries + if (mimeType.startsWith("multipart")) { + return parseMimeTypeInternal(mimeType); + } + return cachedMimeTypes.get(mimeType); + } + + private static MimeType parseMimeTypeInternal(String mimeType) { + int index = mimeType.indexOf(';'); + String fullType = (index >= 0 ? mimeType.substring(0, index) : mimeType).trim(); + if (fullType.isEmpty()) { + throw new InvalidMimeTypeException(mimeType, "'mimeType' must not be empty"); + } + + // java.net.HttpURLConnection returns a *; q=.2 Accept header + if (MimeType.WILDCARD_TYPE.equals(fullType)) { + fullType = "*/*"; + } + int subIndex = fullType.indexOf('/'); + if (subIndex == -1) { + throw new InvalidMimeTypeException(mimeType, "does not contain '/'"); + } + if (subIndex == fullType.length() - 1) { + throw new InvalidMimeTypeException(mimeType, "does not contain subtype after '/'"); + } + String type = fullType.substring(0, subIndex); + String subtype = fullType.substring(subIndex + 1); + if (MimeType.WILDCARD_TYPE.equals(type) && !MimeType.WILDCARD_TYPE.equals(subtype)) { + throw new InvalidMimeTypeException(mimeType, "wildcard type is legal only in '*/*' (all mime types)"); + } + + Map parameters = null; + do { + int nextIndex = index + 1; + boolean quoted = false; + while (nextIndex < mimeType.length()) { + char ch = mimeType.charAt(nextIndex); + if (ch == ';') { + if (!quoted) { + break; + } + } + else if (ch == '"') { + quoted = !quoted; + } + nextIndex++; + } + String parameter = mimeType.substring(index + 1, nextIndex).trim(); + if (parameter.length() > 0) { + if (parameters == null) { + parameters = new LinkedHashMap<>(4); + } + int eqIndex = parameter.indexOf('='); + if (eqIndex >= 0) { + String attribute = parameter.substring(0, eqIndex).trim(); + String value = parameter.substring(eqIndex + 1).trim(); + parameters.put(attribute, value); + } + } + index = nextIndex; + } + while (index < mimeType.length()); + + try { + return new MimeType(type, subtype, parameters); + } + catch (UnsupportedCharsetException ex) { + throw new InvalidMimeTypeException(mimeType, "unsupported charset '" + ex.getCharsetName() + "'"); + } + catch (IllegalArgumentException ex) { + throw new InvalidMimeTypeException(mimeType, ex.getMessage()); + } + } + + /** + * Parse the comma-separated string into a list of {@code MimeType} objects. + * @param mimeTypes the string to parse + * @return the list of mime types + * @throws InvalidMimeTypeException if the string cannot be parsed + */ + public static List parseMimeTypes(String mimeTypes) { + if (!StringUtils.hasLength(mimeTypes)) { + return Collections.emptyList(); + } + return tokenize(mimeTypes).stream() + .filter(StringUtils::hasText) + .map(MimeTypeUtils::parseMimeType) + .collect(Collectors.toList()); + } + + /** + * Tokenize the given comma-separated string of {@code MimeType} objects + * into a {@code List}. Unlike simple tokenization by ",", this + * method takes into account quoted parameters. + * @param mimeTypes the string to tokenize + * @return the list of tokens + * @since 5.1.3 + */ + public static List tokenize(String mimeTypes) { + if (!StringUtils.hasLength(mimeTypes)) { + return Collections.emptyList(); + } + List tokens = new ArrayList<>(); + boolean inQuotes = false; + int startIndex = 0; + int i = 0; + while (i < mimeTypes.length()) { + switch (mimeTypes.charAt(i)) { + case '"': + inQuotes = !inQuotes; + break; + case ',': + if (!inQuotes) { + tokens.add(mimeTypes.substring(startIndex, i)); + startIndex = i + 1; + } + break; + case '\\': + i++; + break; + } + i++; + } + tokens.add(mimeTypes.substring(startIndex)); + return tokens; + } + + /** + * Return a string representation of the given list of {@code MimeType} objects. + * @param mimeTypes the string to parse + * @return the list of mime types + * @throws IllegalArgumentException if the String cannot be parsed + */ + public static String toString(Collection mimeTypes) { + StringBuilder builder = new StringBuilder(); + for (Iterator iterator = mimeTypes.iterator(); iterator.hasNext();) { + MimeType mimeType = iterator.next(); + mimeType.appendTo(builder); + if (iterator.hasNext()) { + builder.append(", "); + } + } + return builder.toString(); + } + + /** + * Sorts the given list of {@code MimeType} objects by specificity. + *

    Given two mime types: + *

      + *
    1. if either mime type has a {@linkplain MimeType#isWildcardType() wildcard type}, + * then the mime type without the wildcard is ordered before the other.
    2. + *
    3. if the two mime types have different {@linkplain MimeType#getType() types}, + * then they are considered equal and remain their current order.
    4. + *
    5. if either mime type has a {@linkplain MimeType#isWildcardSubtype() wildcard subtype} + * , then the mime type without the wildcard is sorted before the other.
    6. + *
    7. if the two mime types have different {@linkplain MimeType#getSubtype() subtypes}, + * then they are considered equal and remain their current order.
    8. + *
    9. if the two mime types have a different amount of + * {@linkplain MimeType#getParameter(String) parameters}, then the mime type with the most + * parameters is ordered before the other.
    10. + *
    + *

    For example:

    audio/basic < audio/* < */*
    + *
    audio/basic;level=1 < audio/basic
    + *
    audio/basic == text/html
    audio/basic == + * audio/wave
    + * @param mimeTypes the list of mime types to be sorted + * @see HTTP 1.1: Semantics + * and Content, section 5.3.2 + */ + public static void sortBySpecificity(List mimeTypes) { + Assert.notNull(mimeTypes, "'mimeTypes' must not be null"); + if (mimeTypes.size() > 1) { + mimeTypes.sort(SPECIFICITY_COMPARATOR); + } + } + + + /** + * Lazily initialize the {@link SecureRandom} for {@link #generateMultipartBoundary()}. + */ + private static Random initRandom() { + Random randomToUse = random; + if (randomToUse == null) { + synchronized (MimeTypeUtils.class) { + randomToUse = random; + if (randomToUse == null) { + randomToUse = new SecureRandom(); + random = randomToUse; + } + } + } + return randomToUse; + } + + /** + * Generate a random MIME boundary as bytes, often used in multipart mime types. + */ + public static byte[] generateMultipartBoundary() { + Random randomToUse = initRandom(); + byte[] boundary = new byte[randomToUse.nextInt(11) + 30]; + for (int i = 0; i < boundary.length; i++) { + boundary[i] = BOUNDARY_CHARS[randomToUse.nextInt(BOUNDARY_CHARS.length)]; + } + return boundary; + } + + /** + * Generate a random MIME boundary as String, often used in multipart mime types. + */ + public static String generateMultipartBoundaryString() { + return new String(generateMultipartBoundary(), StandardCharsets.US_ASCII); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/MultiValueMap.java b/spring-core/src/main/java/org/springframework/util/MultiValueMap.java new file mode 100644 index 0000000..2429f55 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/MultiValueMap.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.util.List; +import java.util.Map; + +import org.springframework.lang.Nullable; + +/** + * Extension of the {@code Map} interface that stores multiple values. + * + * @author Arjen Poutsma + * @since 3.0 + * @param the key type + * @param the value element type + */ +public interface MultiValueMap extends Map> { + + /** + * Return the first value for the given key. + * @param key the key + * @return the first value for the specified key, or {@code null} if none + */ + @Nullable + V getFirst(K key); + + /** + * Add the given single value to the current list of values for the given key. + * @param key the key + * @param value the value to be added + */ + void add(K key, @Nullable V value); + + /** + * Add all the values of the given list to the current list of values for the given key. + * @param key they key + * @param values the values to be added + * @since 5.0 + */ + void addAll(K key, List values); + + /** + * Add all the values of the given {@code MultiValueMap} to the current values. + * @param values the values to be added + * @since 5.0 + */ + void addAll(MultiValueMap values); + + /** + * {@link #add(Object, Object) Add} the given value, only when the map does not + * {@link #containsKey(Object) contain} the given key. + * @param key the key + * @param value the value to be added + * @since 5.2 + */ + default void addIfAbsent(K key, @Nullable V value) { + if (!containsKey(key)) { + add(key, value); + } + } + + /** + * Set the given single value under the given key. + * @param key the key + * @param value the value to set + */ + void set(K key, @Nullable V value); + + /** + * Set the given values under. + * @param values the values. + */ + void setAll(Map values); + + /** + * Return a {@code Map} with the first values contained in this {@code MultiValueMap}. + * @return a single value representation of this map + */ + Map toSingleValueMap(); + +} diff --git a/spring-core/src/main/java/org/springframework/util/MultiValueMapAdapter.java b/spring-core/src/main/java/org/springframework/util/MultiValueMapAdapter.java new file mode 100644 index 0000000..0d51b56 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/MultiValueMapAdapter.java @@ -0,0 +1,187 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.lang.Nullable; + +/** + * Adapts a given {@link Map} to the {@link MultiValueMap} contract. + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @since 5.3 + * @param the key type + * @param the value element type + * @see CollectionUtils#toMultiValueMap + * @see LinkedMultiValueMap + */ +@SuppressWarnings("serial") +public class MultiValueMapAdapter implements MultiValueMap, Serializable { + + private final Map> targetMap; + + + /** + * Wrap the given target {@link Map} as a {@link MultiValueMap} adapter. + * @param targetMap the plain target {@code Map} + */ + public MultiValueMapAdapter(Map> targetMap) { + Assert.notNull(targetMap, "'targetMap' must not be null"); + this.targetMap = targetMap; + } + + + // MultiValueMap implementation + + @Override + @Nullable + public V getFirst(K key) { + List values = this.targetMap.get(key); + return (values != null && !values.isEmpty() ? values.get(0) : null); + } + + @Override + public void add(K key, @Nullable V value) { + List values = this.targetMap.computeIfAbsent(key, k -> new ArrayList<>(1)); + values.add(value); + } + + @Override + public void addAll(K key, List values) { + List currentValues = this.targetMap.computeIfAbsent(key, k -> new ArrayList<>(1)); + currentValues.addAll(values); + } + + @Override + public void addAll(MultiValueMap values) { + for (Entry> entry : values.entrySet()) { + addAll(entry.getKey(), entry.getValue()); + } + } + + @Override + public void set(K key, @Nullable V value) { + List values = new ArrayList<>(1); + values.add(value); + this.targetMap.put(key, values); + } + + @Override + public void setAll(Map values) { + values.forEach(this::set); + } + + @Override + public Map toSingleValueMap() { + Map singleValueMap = CollectionUtils.newLinkedHashMap(this.targetMap.size()); + this.targetMap.forEach((key, values) -> { + if (values != null && !values.isEmpty()) { + singleValueMap.put(key, values.get(0)); + } + }); + return singleValueMap; + } + + + // Map implementation + + @Override + public int size() { + return this.targetMap.size(); + } + + @Override + public boolean isEmpty() { + return this.targetMap.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return this.targetMap.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return this.targetMap.containsValue(value); + } + + @Override + @Nullable + public List get(Object key) { + return this.targetMap.get(key); + } + + @Override + @Nullable + public List put(K key, List value) { + return this.targetMap.put(key, value); + } + + @Override + @Nullable + public List remove(Object key) { + return this.targetMap.remove(key); + } + + @Override + public void putAll(Map> map) { + this.targetMap.putAll(map); + } + + @Override + public void clear() { + this.targetMap.clear(); + } + + @Override + public Set keySet() { + return this.targetMap.keySet(); + } + + @Override + public Collection> values() { + return this.targetMap.values(); + } + + @Override + public Set>> entrySet() { + return this.targetMap.entrySet(); + } + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || this.targetMap.equals(other)); + } + + @Override + public int hashCode() { + return this.targetMap.hashCode(); + } + + @Override + public String toString() { + return this.targetMap.toString(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/NumberUtils.java b/spring-core/src/main/java/org/springframework/util/NumberUtils.java new file mode 100644 index 0000000..1857d5b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/NumberUtils.java @@ -0,0 +1,326 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.text.ParseException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.lang.Nullable; + +/** + * Miscellaneous utility methods for number conversion and parsing. + *

    Mainly for internal use within the framework; consider Apache's + * Commons Lang for a more comprehensive suite of number utilities. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @since 1.1.2 + */ +public abstract class NumberUtils { + + private static final BigInteger LONG_MIN = BigInteger.valueOf(Long.MIN_VALUE); + + private static final BigInteger LONG_MAX = BigInteger.valueOf(Long.MAX_VALUE); + + /** + * Standard number types (all immutable): + * Byte, Short, Integer, Long, BigInteger, Float, Double, BigDecimal. + */ + public static final Set> STANDARD_NUMBER_TYPES; + + static { + Set> numberTypes = new HashSet<>(8); + numberTypes.add(Byte.class); + numberTypes.add(Short.class); + numberTypes.add(Integer.class); + numberTypes.add(Long.class); + numberTypes.add(BigInteger.class); + numberTypes.add(Float.class); + numberTypes.add(Double.class); + numberTypes.add(BigDecimal.class); + STANDARD_NUMBER_TYPES = Collections.unmodifiableSet(numberTypes); + } + + + /** + * Convert the given number into an instance of the given target class. + * @param number the number to convert + * @param targetClass the target class to convert to + * @return the converted number + * @throws IllegalArgumentException if the target class is not supported + * (i.e. not a standard Number subclass as included in the JDK) + * @see java.lang.Byte + * @see java.lang.Short + * @see java.lang.Integer + * @see java.lang.Long + * @see java.math.BigInteger + * @see java.lang.Float + * @see java.lang.Double + * @see java.math.BigDecimal + */ + @SuppressWarnings("unchecked") + public static T convertNumberToTargetClass(Number number, Class targetClass) + throws IllegalArgumentException { + + Assert.notNull(number, "Number must not be null"); + Assert.notNull(targetClass, "Target class must not be null"); + + if (targetClass.isInstance(number)) { + return (T) number; + } + else if (Byte.class == targetClass) { + long value = checkedLongValue(number, targetClass); + if (value < Byte.MIN_VALUE || value > Byte.MAX_VALUE) { + raiseOverflowException(number, targetClass); + } + return (T) Byte.valueOf(number.byteValue()); + } + else if (Short.class == targetClass) { + long value = checkedLongValue(number, targetClass); + if (value < Short.MIN_VALUE || value > Short.MAX_VALUE) { + raiseOverflowException(number, targetClass); + } + return (T) Short.valueOf(number.shortValue()); + } + else if (Integer.class == targetClass) { + long value = checkedLongValue(number, targetClass); + if (value < Integer.MIN_VALUE || value > Integer.MAX_VALUE) { + raiseOverflowException(number, targetClass); + } + return (T) Integer.valueOf(number.intValue()); + } + else if (Long.class == targetClass) { + long value = checkedLongValue(number, targetClass); + return (T) Long.valueOf(value); + } + else if (BigInteger.class == targetClass) { + if (number instanceof BigDecimal) { + // do not lose precision - use BigDecimal's own conversion + return (T) ((BigDecimal) number).toBigInteger(); + } + else { + // original value is not a Big* number - use standard long conversion + return (T) BigInteger.valueOf(number.longValue()); + } + } + else if (Float.class == targetClass) { + return (T) Float.valueOf(number.floatValue()); + } + else if (Double.class == targetClass) { + return (T) Double.valueOf(number.doubleValue()); + } + else if (BigDecimal.class == targetClass) { + // always use BigDecimal(String) here to avoid unpredictability of BigDecimal(double) + // (see BigDecimal javadoc for details) + return (T) new BigDecimal(number.toString()); + } + else { + throw new IllegalArgumentException("Could not convert number [" + number + "] of type [" + + number.getClass().getName() + "] to unsupported target class [" + targetClass.getName() + "]"); + } + } + + /** + * Check for a {@code BigInteger}/{@code BigDecimal} long overflow + * before returning the given number as a long value. + * @param number the number to convert + * @param targetClass the target class to convert to + * @return the long value, if convertible without overflow + * @throws IllegalArgumentException if there is an overflow + * @see #raiseOverflowException + */ + private static long checkedLongValue(Number number, Class targetClass) { + BigInteger bigInt = null; + if (number instanceof BigInteger) { + bigInt = (BigInteger) number; + } + else if (number instanceof BigDecimal) { + bigInt = ((BigDecimal) number).toBigInteger(); + } + // Effectively analogous to JDK 8's BigInteger.longValueExact() + if (bigInt != null && (bigInt.compareTo(LONG_MIN) < 0 || bigInt.compareTo(LONG_MAX) > 0)) { + raiseOverflowException(number, targetClass); + } + return number.longValue(); + } + + /** + * Raise an overflow exception for the given number and target class. + * @param number the number we tried to convert + * @param targetClass the target class we tried to convert to + * @throws IllegalArgumentException if there is an overflow + */ + private static void raiseOverflowException(Number number, Class targetClass) { + throw new IllegalArgumentException("Could not convert number [" + number + "] of type [" + + number.getClass().getName() + "] to target class [" + targetClass.getName() + "]: overflow"); + } + + /** + * Parse the given {@code text} into a {@link Number} instance of the given + * target class, using the corresponding {@code decode} / {@code valueOf} method. + *

    Trims all whitespace (leading, trailing, and in between characters) from + * the input {@code String} before attempting to parse the number. + *

    Supports numbers in hex format (with leading "0x", "0X", or "#") as well. + * @param text the text to convert + * @param targetClass the target class to parse into + * @return the parsed number + * @throws IllegalArgumentException if the target class is not supported + * (i.e. not a standard Number subclass as included in the JDK) + * @see Byte#decode + * @see Short#decode + * @see Integer#decode + * @see Long#decode + * @see #decodeBigInteger(String) + * @see Float#valueOf + * @see Double#valueOf + * @see java.math.BigDecimal#BigDecimal(String) + */ + @SuppressWarnings("unchecked") + public static T parseNumber(String text, Class targetClass) { + Assert.notNull(text, "Text must not be null"); + Assert.notNull(targetClass, "Target class must not be null"); + String trimmed = StringUtils.trimAllWhitespace(text); + + if (Byte.class == targetClass) { + return (T) (isHexNumber(trimmed) ? Byte.decode(trimmed) : Byte.valueOf(trimmed)); + } + else if (Short.class == targetClass) { + return (T) (isHexNumber(trimmed) ? Short.decode(trimmed) : Short.valueOf(trimmed)); + } + else if (Integer.class == targetClass) { + return (T) (isHexNumber(trimmed) ? Integer.decode(trimmed) : Integer.valueOf(trimmed)); + } + else if (Long.class == targetClass) { + return (T) (isHexNumber(trimmed) ? Long.decode(trimmed) : Long.valueOf(trimmed)); + } + else if (BigInteger.class == targetClass) { + return (T) (isHexNumber(trimmed) ? decodeBigInteger(trimmed) : new BigInteger(trimmed)); + } + else if (Float.class == targetClass) { + return (T) Float.valueOf(trimmed); + } + else if (Double.class == targetClass) { + return (T) Double.valueOf(trimmed); + } + else if (BigDecimal.class == targetClass || Number.class == targetClass) { + return (T) new BigDecimal(trimmed); + } + else { + throw new IllegalArgumentException( + "Cannot convert String [" + text + "] to target class [" + targetClass.getName() + "]"); + } + } + + /** + * Parse the given {@code text} into a {@link Number} instance of the + * given target class, using the supplied {@link NumberFormat}. + *

    Trims the input {@code String} before attempting to parse the number. + * @param text the text to convert + * @param targetClass the target class to parse into + * @param numberFormat the {@code NumberFormat} to use for parsing (if + * {@code null}, this method falls back to {@link #parseNumber(String, Class)}) + * @return the parsed number + * @throws IllegalArgumentException if the target class is not supported + * (i.e. not a standard Number subclass as included in the JDK) + * @see java.text.NumberFormat#parse + * @see #convertNumberToTargetClass + * @see #parseNumber(String, Class) + */ + public static T parseNumber( + String text, Class targetClass, @Nullable NumberFormat numberFormat) { + + if (numberFormat != null) { + Assert.notNull(text, "Text must not be null"); + Assert.notNull(targetClass, "Target class must not be null"); + DecimalFormat decimalFormat = null; + boolean resetBigDecimal = false; + if (numberFormat instanceof DecimalFormat) { + decimalFormat = (DecimalFormat) numberFormat; + if (BigDecimal.class == targetClass && !decimalFormat.isParseBigDecimal()) { + decimalFormat.setParseBigDecimal(true); + resetBigDecimal = true; + } + } + try { + Number number = numberFormat.parse(StringUtils.trimAllWhitespace(text)); + return convertNumberToTargetClass(number, targetClass); + } + catch (ParseException ex) { + throw new IllegalArgumentException("Could not parse number: " + ex.getMessage()); + } + finally { + if (resetBigDecimal) { + decimalFormat.setParseBigDecimal(false); + } + } + } + else { + return parseNumber(text, targetClass); + } + } + + /** + * Determine whether the given {@code value} String indicates a hex number, + * i.e. needs to be passed into {@code Integer.decode} instead of + * {@code Integer.valueOf}, etc. + */ + private static boolean isHexNumber(String value) { + int index = (value.startsWith("-") ? 1 : 0); + return (value.startsWith("0x", index) || value.startsWith("0X", index) || value.startsWith("#", index)); + } + + /** + * Decode a {@link java.math.BigInteger} from the supplied {@link String} value. + *

    Supports decimal, hex, and octal notation. + * @see BigInteger#BigInteger(String, int) + */ + private static BigInteger decodeBigInteger(String value) { + int radix = 10; + int index = 0; + boolean negative = false; + + // Handle minus sign, if present. + if (value.startsWith("-")) { + negative = true; + index++; + } + + // Handle radix specifier, if present. + if (value.startsWith("0x", index) || value.startsWith("0X", index)) { + index += 2; + radix = 16; + } + else if (value.startsWith("#", index)) { + index++; + radix = 16; + } + else if (value.startsWith("0", index) && value.length() > 1 + index) { + index++; + radix = 8; + } + + BigInteger result = new BigInteger(value.substring(index), radix); + return (negative ? result.negate() : result); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/ObjectUtils.java b/spring-core/src/main/java/org/springframework/util/ObjectUtils.java new file mode 100644 index 0000000..8089a13 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/ObjectUtils.java @@ -0,0 +1,911 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.StringJoiner; + +import org.springframework.lang.Nullable; + +/** + * Miscellaneous object utility methods. + * + *

    Mainly for internal use within the framework. + * + *

    Thanks to Alex Ruiz for contributing several enhancements to this class! + * + * @author Juergen Hoeller + * @author Keith Donald + * @author Rod Johnson + * @author Rob Harrop + * @author Chris Beams + * @author Sam Brannen + * @since 19.03.2004 + * @see ClassUtils + * @see CollectionUtils + * @see StringUtils + */ +public abstract class ObjectUtils { + + private static final int INITIAL_HASH = 7; + private static final int MULTIPLIER = 31; + + private static final String EMPTY_STRING = ""; + private static final String NULL_STRING = "null"; + private static final String ARRAY_START = "{"; + private static final String ARRAY_END = "}"; + private static final String EMPTY_ARRAY = ARRAY_START + ARRAY_END; + private static final String ARRAY_ELEMENT_SEPARATOR = ", "; + private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; + + + /** + * Return whether the given throwable is a checked exception: + * that is, neither a RuntimeException nor an Error. + * @param ex the throwable to check + * @return whether the throwable is a checked exception + * @see java.lang.Exception + * @see java.lang.RuntimeException + * @see java.lang.Error + */ + public static boolean isCheckedException(Throwable ex) { + return !(ex instanceof RuntimeException || ex instanceof Error); + } + + /** + * Check whether the given exception is compatible with the specified + * exception types, as declared in a throws clause. + * @param ex the exception to check + * @param declaredExceptions the exception types declared in the throws clause + * @return whether the given exception is compatible + */ + public static boolean isCompatibleWithThrowsClause(Throwable ex, @Nullable Class... declaredExceptions) { + if (!isCheckedException(ex)) { + return true; + } + if (declaredExceptions != null) { + for (Class declaredException : declaredExceptions) { + if (declaredException.isInstance(ex)) { + return true; + } + } + } + return false; + } + + /** + * Determine whether the given object is an array: + * either an Object array or a primitive array. + * @param obj the object to check + */ + public static boolean isArray(@Nullable Object obj) { + return (obj != null && obj.getClass().isArray()); + } + + /** + * Determine whether the given array is empty: + * i.e. {@code null} or of zero length. + * @param array the array to check + * @see #isEmpty(Object) + */ + public static boolean isEmpty(@Nullable Object[] array) { + return (array == null || array.length == 0); + } + + /** + * Determine whether the given object is empty. + *

    This method supports the following object types. + *

      + *
    • {@code Optional}: considered empty if {@link Optional#empty()}
    • + *
    • {@code Array}: considered empty if its length is zero
    • + *
    • {@link CharSequence}: considered empty if its length is zero
    • + *
    • {@link Collection}: delegates to {@link Collection#isEmpty()}
    • + *
    • {@link Map}: delegates to {@link Map#isEmpty()}
    • + *
    + *

    If the given object is non-null and not one of the aforementioned + * supported types, this method returns {@code false}. + * @param obj the object to check + * @return {@code true} if the object is {@code null} or empty + * @since 4.2 + * @see Optional#isPresent() + * @see ObjectUtils#isEmpty(Object[]) + * @see StringUtils#hasLength(CharSequence) + * @see CollectionUtils#isEmpty(java.util.Collection) + * @see CollectionUtils#isEmpty(java.util.Map) + */ + public static boolean isEmpty(@Nullable Object obj) { + if (obj == null) { + return true; + } + + if (obj instanceof Optional) { + return !((Optional) obj).isPresent(); + } + if (obj instanceof CharSequence) { + return ((CharSequence) obj).length() == 0; + } + if (obj.getClass().isArray()) { + return Array.getLength(obj) == 0; + } + if (obj instanceof Collection) { + return ((Collection) obj).isEmpty(); + } + if (obj instanceof Map) { + return ((Map) obj).isEmpty(); + } + + // else + return false; + } + + /** + * Unwrap the given object which is potentially a {@link java.util.Optional}. + * @param obj the candidate object + * @return either the value held within the {@code Optional}, {@code null} + * if the {@code Optional} is empty, or simply the given object as-is + * @since 5.0 + */ + @Nullable + public static Object unwrapOptional(@Nullable Object obj) { + if (obj instanceof Optional) { + Optional optional = (Optional) obj; + if (!optional.isPresent()) { + return null; + } + Object result = optional.get(); + Assert.isTrue(!(result instanceof Optional), "Multi-level Optional usage not supported"); + return result; + } + return obj; + } + + /** + * Check whether the given array contains the given element. + * @param array the array to check (may be {@code null}, + * in which case the return value will always be {@code false}) + * @param element the element to check for + * @return whether the element has been found in the given array + */ + public static boolean containsElement(@Nullable Object[] array, Object element) { + if (array == null) { + return false; + } + for (Object arrayEle : array) { + if (nullSafeEquals(arrayEle, element)) { + return true; + } + } + return false; + } + + /** + * Check whether the given array of enum constants contains a constant with the given name, + * ignoring case when determining a match. + * @param enumValues the enum values to check, typically obtained via {@code MyEnum.values()} + * @param constant the constant name to find (must not be null or empty string) + * @return whether the constant has been found in the given array + */ + public static boolean containsConstant(Enum[] enumValues, String constant) { + return containsConstant(enumValues, constant, false); + } + + /** + * Check whether the given array of enum constants contains a constant with the given name. + * @param enumValues the enum values to check, typically obtained via {@code MyEnum.values()} + * @param constant the constant name to find (must not be null or empty string) + * @param caseSensitive whether case is significant in determining a match + * @return whether the constant has been found in the given array + */ + public static boolean containsConstant(Enum[] enumValues, String constant, boolean caseSensitive) { + for (Enum candidate : enumValues) { + if (caseSensitive ? candidate.toString().equals(constant) : + candidate.toString().equalsIgnoreCase(constant)) { + return true; + } + } + return false; + } + + /** + * Case insensitive alternative to {@link Enum#valueOf(Class, String)}. + * @param the concrete Enum type + * @param enumValues the array of all Enum constants in question, usually per {@code Enum.values()} + * @param constant the constant to get the enum value of + * @throws IllegalArgumentException if the given constant is not found in the given array + * of enum values. Use {@link #containsConstant(Enum[], String)} as a guard to avoid this exception. + */ + public static > E caseInsensitiveValueOf(E[] enumValues, String constant) { + for (E candidate : enumValues) { + if (candidate.toString().equalsIgnoreCase(constant)) { + return candidate; + } + } + throw new IllegalArgumentException("Constant [" + constant + "] does not exist in enum type " + + enumValues.getClass().getComponentType().getName()); + } + + /** + * Append the given object to the given array, returning a new array + * consisting of the input array contents plus the given object. + * @param array the array to append to (can be {@code null}) + * @param obj the object to append + * @return the new array (of the same component type; never {@code null}) + */ + public static A[] addObjectToArray(@Nullable A[] array, @Nullable O obj) { + Class compType = Object.class; + if (array != null) { + compType = array.getClass().getComponentType(); + } + else if (obj != null) { + compType = obj.getClass(); + } + int newArrLength = (array != null ? array.length + 1 : 1); + @SuppressWarnings("unchecked") + A[] newArr = (A[]) Array.newInstance(compType, newArrLength); + if (array != null) { + System.arraycopy(array, 0, newArr, 0, array.length); + } + newArr[newArr.length - 1] = obj; + return newArr; + } + + /** + * Convert the given array (which may be a primitive array) to an + * object array (if necessary of primitive wrapper objects). + *

    A {@code null} source value will be converted to an + * empty Object array. + * @param source the (potentially primitive) array + * @return the corresponding object array (never {@code null}) + * @throws IllegalArgumentException if the parameter is not an array + */ + public static Object[] toObjectArray(@Nullable Object source) { + if (source instanceof Object[]) { + return (Object[]) source; + } + if (source == null) { + return EMPTY_OBJECT_ARRAY; + } + if (!source.getClass().isArray()) { + throw new IllegalArgumentException("Source is not an array: " + source); + } + int length = Array.getLength(source); + if (length == 0) { + return EMPTY_OBJECT_ARRAY; + } + Class wrapperType = Array.get(source, 0).getClass(); + Object[] newArray = (Object[]) Array.newInstance(wrapperType, length); + for (int i = 0; i < length; i++) { + newArray[i] = Array.get(source, i); + } + return newArray; + } + + + //--------------------------------------------------------------------- + // Convenience methods for content-based equality/hash-code handling + //--------------------------------------------------------------------- + + /** + * Determine if the given objects are equal, returning {@code true} if + * both are {@code null} or {@code false} if only one is {@code null}. + *

    Compares arrays with {@code Arrays.equals}, performing an equality + * check based on the array elements rather than the array reference. + * @param o1 first Object to compare + * @param o2 second Object to compare + * @return whether the given objects are equal + * @see Object#equals(Object) + * @see java.util.Arrays#equals + */ + public static boolean nullSafeEquals(@Nullable Object o1, @Nullable Object o2) { + if (o1 == o2) { + return true; + } + if (o1 == null || o2 == null) { + return false; + } + if (o1.equals(o2)) { + return true; + } + if (o1.getClass().isArray() && o2.getClass().isArray()) { + return arrayEquals(o1, o2); + } + return false; + } + + /** + * Compare the given arrays with {@code Arrays.equals}, performing an equality + * check based on the array elements rather than the array reference. + * @param o1 first array to compare + * @param o2 second array to compare + * @return whether the given objects are equal + * @see #nullSafeEquals(Object, Object) + * @see java.util.Arrays#equals + */ + private static boolean arrayEquals(Object o1, Object o2) { + if (o1 instanceof Object[] && o2 instanceof Object[]) { + return Arrays.equals((Object[]) o1, (Object[]) o2); + } + if (o1 instanceof boolean[] && o2 instanceof boolean[]) { + return Arrays.equals((boolean[]) o1, (boolean[]) o2); + } + if (o1 instanceof byte[] && o2 instanceof byte[]) { + return Arrays.equals((byte[]) o1, (byte[]) o2); + } + if (o1 instanceof char[] && o2 instanceof char[]) { + return Arrays.equals((char[]) o1, (char[]) o2); + } + if (o1 instanceof double[] && o2 instanceof double[]) { + return Arrays.equals((double[]) o1, (double[]) o2); + } + if (o1 instanceof float[] && o2 instanceof float[]) { + return Arrays.equals((float[]) o1, (float[]) o2); + } + if (o1 instanceof int[] && o2 instanceof int[]) { + return Arrays.equals((int[]) o1, (int[]) o2); + } + if (o1 instanceof long[] && o2 instanceof long[]) { + return Arrays.equals((long[]) o1, (long[]) o2); + } + if (o1 instanceof short[] && o2 instanceof short[]) { + return Arrays.equals((short[]) o1, (short[]) o2); + } + return false; + } + + /** + * Return as hash code for the given object; typically the value of + * {@code Object#hashCode()}}. If the object is an array, + * this method will delegate to any of the {@code nullSafeHashCode} + * methods for arrays in this class. If the object is {@code null}, + * this method returns 0. + * @see Object#hashCode() + * @see #nullSafeHashCode(Object[]) + * @see #nullSafeHashCode(boolean[]) + * @see #nullSafeHashCode(byte[]) + * @see #nullSafeHashCode(char[]) + * @see #nullSafeHashCode(double[]) + * @see #nullSafeHashCode(float[]) + * @see #nullSafeHashCode(int[]) + * @see #nullSafeHashCode(long[]) + * @see #nullSafeHashCode(short[]) + */ + public static int nullSafeHashCode(@Nullable Object obj) { + if (obj == null) { + return 0; + } + if (obj.getClass().isArray()) { + if (obj instanceof Object[]) { + return nullSafeHashCode((Object[]) obj); + } + if (obj instanceof boolean[]) { + return nullSafeHashCode((boolean[]) obj); + } + if (obj instanceof byte[]) { + return nullSafeHashCode((byte[]) obj); + } + if (obj instanceof char[]) { + return nullSafeHashCode((char[]) obj); + } + if (obj instanceof double[]) { + return nullSafeHashCode((double[]) obj); + } + if (obj instanceof float[]) { + return nullSafeHashCode((float[]) obj); + } + if (obj instanceof int[]) { + return nullSafeHashCode((int[]) obj); + } + if (obj instanceof long[]) { + return nullSafeHashCode((long[]) obj); + } + if (obj instanceof short[]) { + return nullSafeHashCode((short[]) obj); + } + } + return obj.hashCode(); + } + + /** + * Return a hash code based on the contents of the specified array. + * If {@code array} is {@code null}, this method returns 0. + */ + public static int nullSafeHashCode(@Nullable Object[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + for (Object element : array) { + hash = MULTIPLIER * hash + nullSafeHashCode(element); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If {@code array} is {@code null}, this method returns 0. + */ + public static int nullSafeHashCode(@Nullable boolean[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + for (boolean element : array) { + hash = MULTIPLIER * hash + Boolean.hashCode(element); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If {@code array} is {@code null}, this method returns 0. + */ + public static int nullSafeHashCode(@Nullable byte[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + for (byte element : array) { + hash = MULTIPLIER * hash + element; + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If {@code array} is {@code null}, this method returns 0. + */ + public static int nullSafeHashCode(@Nullable char[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + for (char element : array) { + hash = MULTIPLIER * hash + element; + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If {@code array} is {@code null}, this method returns 0. + */ + public static int nullSafeHashCode(@Nullable double[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + for (double element : array) { + hash = MULTIPLIER * hash + Double.hashCode(element); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If {@code array} is {@code null}, this method returns 0. + */ + public static int nullSafeHashCode(@Nullable float[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + for (float element : array) { + hash = MULTIPLIER * hash + Float.hashCode(element); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If {@code array} is {@code null}, this method returns 0. + */ + public static int nullSafeHashCode(@Nullable int[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + for (int element : array) { + hash = MULTIPLIER * hash + element; + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If {@code array} is {@code null}, this method returns 0. + */ + public static int nullSafeHashCode(@Nullable long[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + for (long element : array) { + hash = MULTIPLIER * hash + Long.hashCode(element); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If {@code array} is {@code null}, this method returns 0. + */ + public static int nullSafeHashCode(@Nullable short[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + for (short element : array) { + hash = MULTIPLIER * hash + element; + } + return hash; + } + + /** + * Return the same value as {@link Boolean#hashCode(boolean)}}. + * @deprecated as of Spring Framework 5.0, in favor of the native JDK 8 variant + */ + @Deprecated + public static int hashCode(boolean bool) { + return Boolean.hashCode(bool); + } + + /** + * Return the same value as {@link Double#hashCode(double)}}. + * @deprecated as of Spring Framework 5.0, in favor of the native JDK 8 variant + */ + @Deprecated + public static int hashCode(double dbl) { + return Double.hashCode(dbl); + } + + /** + * Return the same value as {@link Float#hashCode(float)}}. + * @deprecated as of Spring Framework 5.0, in favor of the native JDK 8 variant + */ + @Deprecated + public static int hashCode(float flt) { + return Float.hashCode(flt); + } + + /** + * Return the same value as {@link Long#hashCode(long)}}. + * @deprecated as of Spring Framework 5.0, in favor of the native JDK 8 variant + */ + @Deprecated + public static int hashCode(long lng) { + return Long.hashCode(lng); + } + + + //--------------------------------------------------------------------- + // Convenience methods for toString output + //--------------------------------------------------------------------- + + /** + * Return a String representation of an object's overall identity. + * @param obj the object (may be {@code null}) + * @return the object's identity as String representation, + * or an empty String if the object was {@code null} + */ + public static String identityToString(@Nullable Object obj) { + if (obj == null) { + return EMPTY_STRING; + } + return obj.getClass().getName() + "@" + getIdentityHexString(obj); + } + + /** + * Return a hex String form of an object's identity hash code. + * @param obj the object + * @return the object's identity code in hex notation + */ + public static String getIdentityHexString(Object obj) { + return Integer.toHexString(System.identityHashCode(obj)); + } + + /** + * Return a content-based String representation if {@code obj} is + * not {@code null}; otherwise returns an empty String. + *

    Differs from {@link #nullSafeToString(Object)} in that it returns + * an empty String rather than "null" for a {@code null} value. + * @param obj the object to build a display String for + * @return a display String representation of {@code obj} + * @see #nullSafeToString(Object) + */ + public static String getDisplayString(@Nullable Object obj) { + if (obj == null) { + return EMPTY_STRING; + } + return nullSafeToString(obj); + } + + /** + * Determine the class name for the given object. + *

    Returns a {@code "null"} String if {@code obj} is {@code null}. + * @param obj the object to introspect (may be {@code null}) + * @return the corresponding class name + */ + public static String nullSafeClassName(@Nullable Object obj) { + return (obj != null ? obj.getClass().getName() : NULL_STRING); + } + + /** + * Return a String representation of the specified Object. + *

    Builds a String representation of the contents in case of an array. + * Returns a {@code "null"} String if {@code obj} is {@code null}. + * @param obj the object to build a String representation for + * @return a String representation of {@code obj} + */ + public static String nullSafeToString(@Nullable Object obj) { + if (obj == null) { + return NULL_STRING; + } + if (obj instanceof String) { + return (String) obj; + } + if (obj instanceof Object[]) { + return nullSafeToString((Object[]) obj); + } + if (obj instanceof boolean[]) { + return nullSafeToString((boolean[]) obj); + } + if (obj instanceof byte[]) { + return nullSafeToString((byte[]) obj); + } + if (obj instanceof char[]) { + return nullSafeToString((char[]) obj); + } + if (obj instanceof double[]) { + return nullSafeToString((double[]) obj); + } + if (obj instanceof float[]) { + return nullSafeToString((float[]) obj); + } + if (obj instanceof int[]) { + return nullSafeToString((int[]) obj); + } + if (obj instanceof long[]) { + return nullSafeToString((long[]) obj); + } + if (obj instanceof short[]) { + return nullSafeToString((short[]) obj); + } + String str = obj.toString(); + return (str != null ? str : EMPTY_STRING); + } + + /** + * Return a String representation of the contents of the specified array. + *

    The String representation consists of a list of the array's elements, + * enclosed in curly braces ({@code "{}"}). Adjacent elements are separated + * by the characters {@code ", "} (a comma followed by a space). + * Returns a {@code "null"} String if {@code array} is {@code null}. + * @param array the array to build a String representation for + * @return a String representation of {@code array} + */ + public static String nullSafeToString(@Nullable Object[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringJoiner stringJoiner = new StringJoiner(ARRAY_ELEMENT_SEPARATOR, ARRAY_START, ARRAY_END); + for (Object o : array) { + stringJoiner.add(String.valueOf(o)); + } + return stringJoiner.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

    The String representation consists of a list of the array's elements, + * enclosed in curly braces ({@code "{}"}). Adjacent elements are separated + * by the characters {@code ", "} (a comma followed by a space). + * Returns a {@code "null"} String if {@code array} is {@code null}. + * @param array the array to build a String representation for + * @return a String representation of {@code array} + */ + public static String nullSafeToString(@Nullable boolean[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringJoiner stringJoiner = new StringJoiner(ARRAY_ELEMENT_SEPARATOR, ARRAY_START, ARRAY_END); + for (boolean b : array) { + stringJoiner.add(String.valueOf(b)); + } + return stringJoiner.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

    The String representation consists of a list of the array's elements, + * enclosed in curly braces ({@code "{}"}). Adjacent elements are separated + * by the characters {@code ", "} (a comma followed by a space). + * Returns a {@code "null"} String if {@code array} is {@code null}. + * @param array the array to build a String representation for + * @return a String representation of {@code array} + */ + public static String nullSafeToString(@Nullable byte[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringJoiner stringJoiner = new StringJoiner(ARRAY_ELEMENT_SEPARATOR, ARRAY_START, ARRAY_END); + for (byte b : array) { + stringJoiner.add(String.valueOf(b)); + } + return stringJoiner.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

    The String representation consists of a list of the array's elements, + * enclosed in curly braces ({@code "{}"}). Adjacent elements are separated + * by the characters {@code ", "} (a comma followed by a space). + * Returns a {@code "null"} String if {@code array} is {@code null}. + * @param array the array to build a String representation for + * @return a String representation of {@code array} + */ + public static String nullSafeToString(@Nullable char[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringJoiner stringJoiner = new StringJoiner(ARRAY_ELEMENT_SEPARATOR, ARRAY_START, ARRAY_END); + for (char c : array) { + stringJoiner.add('\'' + String.valueOf(c) + '\''); + } + return stringJoiner.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

    The String representation consists of a list of the array's elements, + * enclosed in curly braces ({@code "{}"}). Adjacent elements are separated + * by the characters {@code ", "} (a comma followed by a space). + * Returns a {@code "null"} String if {@code array} is {@code null}. + * @param array the array to build a String representation for + * @return a String representation of {@code array} + */ + public static String nullSafeToString(@Nullable double[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringJoiner stringJoiner = new StringJoiner(ARRAY_ELEMENT_SEPARATOR, ARRAY_START, ARRAY_END); + for (double d : array) { + stringJoiner.add(String.valueOf(d)); + } + return stringJoiner.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

    The String representation consists of a list of the array's elements, + * enclosed in curly braces ({@code "{}"}). Adjacent elements are separated + * by the characters {@code ", "} (a comma followed by a space). + * Returns a {@code "null"} String if {@code array} is {@code null}. + * @param array the array to build a String representation for + * @return a String representation of {@code array} + */ + public static String nullSafeToString(@Nullable float[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringJoiner stringJoiner = new StringJoiner(ARRAY_ELEMENT_SEPARATOR, ARRAY_START, ARRAY_END); + for (float f : array) { + stringJoiner.add(String.valueOf(f)); + } + return stringJoiner.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

    The String representation consists of a list of the array's elements, + * enclosed in curly braces ({@code "{}"}). Adjacent elements are separated + * by the characters {@code ", "} (a comma followed by a space). + * Returns a {@code "null"} String if {@code array} is {@code null}. + * @param array the array to build a String representation for + * @return a String representation of {@code array} + */ + public static String nullSafeToString(@Nullable int[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringJoiner stringJoiner = new StringJoiner(ARRAY_ELEMENT_SEPARATOR, ARRAY_START, ARRAY_END); + for (int i : array) { + stringJoiner.add(String.valueOf(i)); + } + return stringJoiner.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

    The String representation consists of a list of the array's elements, + * enclosed in curly braces ({@code "{}"}). Adjacent elements are separated + * by the characters {@code ", "} (a comma followed by a space). + * Returns a {@code "null"} String if {@code array} is {@code null}. + * @param array the array to build a String representation for + * @return a String representation of {@code array} + */ + public static String nullSafeToString(@Nullable long[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringJoiner stringJoiner = new StringJoiner(ARRAY_ELEMENT_SEPARATOR, ARRAY_START, ARRAY_END); + for (long l : array) { + stringJoiner.add(String.valueOf(l)); + } + return stringJoiner.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

    The String representation consists of a list of the array's elements, + * enclosed in curly braces ({@code "{}"}). Adjacent elements are separated + * by the characters {@code ", "} (a comma followed by a space). + * Returns a {@code "null"} String if {@code array} is {@code null}. + * @param array the array to build a String representation for + * @return a String representation of {@code array} + */ + public static String nullSafeToString(@Nullable short[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringJoiner stringJoiner = new StringJoiner(ARRAY_ELEMENT_SEPARATOR, ARRAY_START, ARRAY_END); + for (short s : array) { + stringJoiner.add(String.valueOf(s)); + } + return stringJoiner.toString(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/PathMatcher.java b/spring-core/src/main/java/org/springframework/util/PathMatcher.java new file mode 100644 index 0000000..5d711d0 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/PathMatcher.java @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.util.Comparator; +import java.util.Map; + +/** + * Strategy interface for {@code String}-based path matching. + * + *

    Used by {@link org.springframework.core.io.support.PathMatchingResourcePatternResolver}, + * {@link org.springframework.web.servlet.handler.AbstractUrlHandlerMapping}, + * and {@link org.springframework.web.servlet.mvc.WebContentInterceptor}. + * + *

    The default implementation is {@link AntPathMatcher}, supporting the + * Ant-style pattern syntax. + * + * @author Juergen Hoeller + * @since 1.2 + * @see AntPathMatcher + */ +public interface PathMatcher { + + /** + * Does the given {@code path} represent a pattern that can be matched + * by an implementation of this interface? + *

    If the return value is {@code false}, then the {@link #match} + * method does not have to be used because direct equality comparisons + * on the static path Strings will lead to the same result. + * @param path the path to check + * @return {@code true} if the given {@code path} represents a pattern + */ + boolean isPattern(String path); + + /** + * Match the given {@code path} against the given {@code pattern}, + * according to this PathMatcher's matching strategy. + * @param pattern the pattern to match against + * @param path the path to test + * @return {@code true} if the supplied {@code path} matched, + * {@code false} if it didn't + */ + boolean match(String pattern, String path); + + /** + * Match the given {@code path} against the corresponding part of the given + * {@code pattern}, according to this PathMatcher's matching strategy. + *

    Determines whether the pattern at least matches as far as the given base + * path goes, assuming that a full path may then match as well. + * @param pattern the pattern to match against + * @param path the path to test + * @return {@code true} if the supplied {@code path} matched, + * {@code false} if it didn't + */ + boolean matchStart(String pattern, String path); + + /** + * Given a pattern and a full path, determine the pattern-mapped part. + *

    This method is supposed to find out which part of the path is matched + * dynamically through an actual pattern, that is, it strips off a statically + * defined leading path from the given full path, returning only the actually + * pattern-matched part of the path. + *

    For example: For "myroot/*.html" as pattern and "myroot/myfile.html" + * as full path, this method should return "myfile.html". The detailed + * determination rules are specified to this PathMatcher's matching strategy. + *

    A simple implementation may return the given full path as-is in case + * of an actual pattern, and the empty String in case of the pattern not + * containing any dynamic parts (i.e. the {@code pattern} parameter being + * a static path that wouldn't qualify as an actual {@link #isPattern pattern}). + * A sophisticated implementation will differentiate between the static parts + * and the dynamic parts of the given path pattern. + * @param pattern the path pattern + * @param path the full path to introspect + * @return the pattern-mapped part of the given {@code path} + * (never {@code null}) + */ + String extractPathWithinPattern(String pattern, String path); + + /** + * Given a pattern and a full path, extract the URI template variables. URI template + * variables are expressed through curly brackets ('{' and '}'). + *

    For example: For pattern "/hotels/{hotel}" and path "/hotels/1", this method will + * return a map containing "hotel"->"1". + * @param pattern the path pattern, possibly containing URI templates + * @param path the full path to extract template variables from + * @return a map, containing variable names as keys; variables values as values + */ + Map extractUriTemplateVariables(String pattern, String path); + + /** + * Given a full path, returns a {@link Comparator} suitable for sorting patterns + * in order of explicitness for that path. + *

    The full algorithm used depends on the underlying implementation, + * but generally, the returned {@code Comparator} will + * {@linkplain java.util.List#sort(java.util.Comparator) sort} + * a list so that more specific patterns come before generic patterns. + * @param path the full path to use for comparison + * @return a comparator capable of sorting patterns in order of explicitness + */ + Comparator getPatternComparator(String path); + + /** + * Combines two patterns into a new pattern that is returned. + *

    The full algorithm used for combining the two pattern depends on the underlying implementation. + * @param pattern1 the first pattern + * @param pattern2 the second pattern + * @return the combination of the two patterns + * @throws IllegalArgumentException when the two patterns cannot be combined + */ + String combine(String pattern1, String pattern2); + +} diff --git a/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java b/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java new file mode 100644 index 0000000..0430128 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import org.springframework.lang.Nullable; + +/** + * Utility methods for simple pattern matching, in particular for + * Spring's typical "xxx*", "*xxx" and "*xxx*" pattern styles. + * + * @author Juergen Hoeller + * @since 2.0 + */ +public abstract class PatternMatchUtils { + + /** + * Match a String against the given pattern, supporting the following simple + * pattern styles: "xxx*", "*xxx", "*xxx*" and "xxx*yyy" matches (with an + * arbitrary number of pattern parts), as well as direct equality. + * @param pattern the pattern to match against + * @param str the String to match + * @return whether the String matches the given pattern + */ + public static boolean simpleMatch(@Nullable String pattern, @Nullable String str) { + if (pattern == null || str == null) { + return false; + } + + int firstIndex = pattern.indexOf('*'); + if (firstIndex == -1) { + return pattern.equals(str); + } + + if (firstIndex == 0) { + if (pattern.length() == 1) { + return true; + } + int nextIndex = pattern.indexOf('*', 1); + if (nextIndex == -1) { + return str.endsWith(pattern.substring(1)); + } + String part = pattern.substring(1, nextIndex); + if (part.isEmpty()) { + return simpleMatch(pattern.substring(nextIndex), str); + } + int partIndex = str.indexOf(part); + while (partIndex != -1) { + if (simpleMatch(pattern.substring(nextIndex), str.substring(partIndex + part.length()))) { + return true; + } + partIndex = str.indexOf(part, partIndex + 1); + } + return false; + } + + return (str.length() >= firstIndex && + pattern.substring(0, firstIndex).equals(str.substring(0, firstIndex)) && + simpleMatch(pattern.substring(firstIndex), str.substring(firstIndex))); + } + + /** + * Match a String against the given patterns, supporting the following simple + * pattern styles: "xxx*", "*xxx", "*xxx*" and "xxx*yyy" matches (with an + * arbitrary number of pattern parts), as well as direct equality. + * @param patterns the patterns to match against + * @param str the String to match + * @return whether the String matches any of the given patterns + */ + public static boolean simpleMatch(@Nullable String[] patterns, String str) { + if (patterns != null) { + for (String pattern : patterns) { + if (simpleMatch(pattern, str)) { + return true; + } + } + } + return false; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/PropertiesPersister.java b/spring-core/src/main/java/org/springframework/util/PropertiesPersister.java new file mode 100644 index 0000000..6b6fcfd --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/PropertiesPersister.java @@ -0,0 +1,116 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; +import java.util.Properties; + +/** + * Strategy interface for persisting {@code java.util.Properties}, + * allowing for pluggable parsing strategies. + * + *

    The default implementation is DefaultPropertiesPersister, + * providing the native parsing of {@code java.util.Properties}, + * but allowing for reading from any Reader and writing to any Writer + * (which allows to specify an encoding for a properties file). + * + * @author Juergen Hoeller + * @since 10.03.2004 + * @see DefaultPropertiesPersister + * @see org.springframework.core.io.support.ResourcePropertiesPersister + * @see java.util.Properties + */ +public interface PropertiesPersister { + + /** + * Load properties from the given InputStream into the given + * Properties object. + * @param props the Properties object to load into + * @param is the InputStream to load from + * @throws IOException in case of I/O errors + * @see java.util.Properties#load + */ + void load(Properties props, InputStream is) throws IOException; + + /** + * Load properties from the given Reader into the given + * Properties object. + * @param props the Properties object to load into + * @param reader the Reader to load from + * @throws IOException in case of I/O errors + */ + void load(Properties props, Reader reader) throws IOException; + + /** + * Write the contents of the given Properties object to the + * given OutputStream. + * @param props the Properties object to store + * @param os the OutputStream to write to + * @param header the description of the property list + * @throws IOException in case of I/O errors + * @see java.util.Properties#store + */ + void store(Properties props, OutputStream os, String header) throws IOException; + + /** + * Write the contents of the given Properties object to the + * given Writer. + * @param props the Properties object to store + * @param writer the Writer to write to + * @param header the description of the property list + * @throws IOException in case of I/O errors + */ + void store(Properties props, Writer writer, String header) throws IOException; + + /** + * Load properties from the given XML InputStream into the + * given Properties object. + * @param props the Properties object to load into + * @param is the InputStream to load from + * @throws IOException in case of I/O errors + * @see java.util.Properties#loadFromXML(java.io.InputStream) + */ + void loadFromXml(Properties props, InputStream is) throws IOException; + + /** + * Write the contents of the given Properties object to the + * given XML OutputStream. + * @param props the Properties object to store + * @param os the OutputStream to write to + * @param header the description of the property list + * @throws IOException in case of I/O errors + * @see java.util.Properties#storeToXML(java.io.OutputStream, String) + */ + void storeToXml(Properties props, OutputStream os, String header) throws IOException; + + /** + * Write the contents of the given Properties object to the + * given XML OutputStream. + * @param props the Properties object to store + * @param os the OutputStream to write to + * @param encoding the encoding to use + * @param header the description of the property list + * @throws IOException in case of I/O errors + * @see java.util.Properties#storeToXML(java.io.OutputStream, String, String) + */ + void storeToXml(Properties props, OutputStream os, String header, String encoding) throws IOException; + +} diff --git a/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java b/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java new file mode 100644 index 0000000..b17d6f8 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java @@ -0,0 +1,230 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.lang.Nullable; + +/** + * Utility class for working with Strings that have placeholder values in them. A placeholder takes the form + * {@code ${name}}. Using {@code PropertyPlaceholderHelper} these placeholders can be substituted for + * user-supplied values.

    Values for substitution can be supplied using a {@link Properties} instance or + * using a {@link PlaceholderResolver}. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @since 3.0 + */ +public class PropertyPlaceholderHelper { + + private static final Log logger = LogFactory.getLog(PropertyPlaceholderHelper.class); + + private static final Map wellKnownSimplePrefixes = new HashMap<>(4); + + static { + wellKnownSimplePrefixes.put("}", "{"); + wellKnownSimplePrefixes.put("]", "["); + wellKnownSimplePrefixes.put(")", "("); + } + + + private final String placeholderPrefix; + + private final String placeholderSuffix; + + private final String simplePrefix; + + @Nullable + private final String valueSeparator; + + private final boolean ignoreUnresolvablePlaceholders; + + + /** + * Creates a new {@code PropertyPlaceholderHelper} that uses the supplied prefix and suffix. + * Unresolvable placeholders are ignored. + * @param placeholderPrefix the prefix that denotes the start of a placeholder + * @param placeholderSuffix the suffix that denotes the end of a placeholder + */ + public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix) { + this(placeholderPrefix, placeholderSuffix, null, true); + } + + /** + * Creates a new {@code PropertyPlaceholderHelper} that uses the supplied prefix and suffix. + * @param placeholderPrefix the prefix that denotes the start of a placeholder + * @param placeholderSuffix the suffix that denotes the end of a placeholder + * @param valueSeparator the separating character between the placeholder variable + * and the associated default value, if any + * @param ignoreUnresolvablePlaceholders indicates whether unresolvable placeholders should + * be ignored ({@code true}) or cause an exception ({@code false}) + */ + public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix, + @Nullable String valueSeparator, boolean ignoreUnresolvablePlaceholders) { + + Assert.notNull(placeholderPrefix, "'placeholderPrefix' must not be null"); + Assert.notNull(placeholderSuffix, "'placeholderSuffix' must not be null"); + this.placeholderPrefix = placeholderPrefix; + this.placeholderSuffix = placeholderSuffix; + String simplePrefixForSuffix = wellKnownSimplePrefixes.get(this.placeholderSuffix); + if (simplePrefixForSuffix != null && this.placeholderPrefix.endsWith(simplePrefixForSuffix)) { + this.simplePrefix = simplePrefixForSuffix; + } + else { + this.simplePrefix = this.placeholderPrefix; + } + this.valueSeparator = valueSeparator; + this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders; + } + + + /** + * Replaces all placeholders of format {@code ${name}} with the corresponding + * property from the supplied {@link Properties}. + * @param value the value containing the placeholders to be replaced + * @param properties the {@code Properties} to use for replacement + * @return the supplied value with placeholders replaced inline + */ + public String replacePlaceholders(String value, final Properties properties) { + Assert.notNull(properties, "'properties' must not be null"); + return replacePlaceholders(value, properties::getProperty); + } + + /** + * Replaces all placeholders of format {@code ${name}} with the value returned + * from the supplied {@link PlaceholderResolver}. + * @param value the value containing the placeholders to be replaced + * @param placeholderResolver the {@code PlaceholderResolver} to use for replacement + * @return the supplied value with placeholders replaced inline + */ + public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) { + Assert.notNull(value, "'value' must not be null"); + return parseStringValue(value, placeholderResolver, null); + } + + protected String parseStringValue( + String value, PlaceholderResolver placeholderResolver, @Nullable Set visitedPlaceholders) { + + int startIndex = value.indexOf(this.placeholderPrefix); + if (startIndex == -1) { + return value; + } + + StringBuilder result = new StringBuilder(value); + while (startIndex != -1) { + int endIndex = findPlaceholderEndIndex(result, startIndex); + if (endIndex != -1) { + String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex); + String originalPlaceholder = placeholder; + if (visitedPlaceholders == null) { + visitedPlaceholders = new HashSet<>(4); + } + if (!visitedPlaceholders.add(originalPlaceholder)) { + throw new IllegalArgumentException( + "Circular placeholder reference '" + originalPlaceholder + "' in property definitions"); + } + // Recursive invocation, parsing placeholders contained in the placeholder key. + placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders); + // Now obtain the value for the fully resolved key... + String propVal = placeholderResolver.resolvePlaceholder(placeholder); + if (propVal == null && this.valueSeparator != null) { + int separatorIndex = placeholder.indexOf(this.valueSeparator); + if (separatorIndex != -1) { + String actualPlaceholder = placeholder.substring(0, separatorIndex); + String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length()); + propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder); + if (propVal == null) { + propVal = defaultValue; + } + } + } + if (propVal != null) { + // Recursive invocation, parsing placeholders contained in the + // previously resolved placeholder value. + propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders); + result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal); + if (logger.isTraceEnabled()) { + logger.trace("Resolved placeholder '" + placeholder + "'"); + } + startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length()); + } + else if (this.ignoreUnresolvablePlaceholders) { + // Proceed with unprocessed value. + startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length()); + } + else { + throw new IllegalArgumentException("Could not resolve placeholder '" + + placeholder + "'" + " in value \"" + value + "\""); + } + visitedPlaceholders.remove(originalPlaceholder); + } + else { + startIndex = -1; + } + } + return result.toString(); + } + + private int findPlaceholderEndIndex(CharSequence buf, int startIndex) { + int index = startIndex + this.placeholderPrefix.length(); + int withinNestedPlaceholder = 0; + while (index < buf.length()) { + if (StringUtils.substringMatch(buf, index, this.placeholderSuffix)) { + if (withinNestedPlaceholder > 0) { + withinNestedPlaceholder--; + index = index + this.placeholderSuffix.length(); + } + else { + return index; + } + } + else if (StringUtils.substringMatch(buf, index, this.simplePrefix)) { + withinNestedPlaceholder++; + index = index + this.simplePrefix.length(); + } + else { + index++; + } + } + return -1; + } + + + /** + * Strategy interface used to resolve replacement values for placeholders contained in Strings. + */ + @FunctionalInterface + public interface PlaceholderResolver { + + /** + * Resolve the supplied placeholder name to the replacement value. + * @param placeholderName the name of the placeholder to resolve + * @return the replacement value, or {@code null} if no replacement is to be made + */ + @Nullable + String resolvePlaceholder(String placeholderName); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java b/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java new file mode 100644 index 0000000..6b457da --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java @@ -0,0 +1,883 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.UndeclaredThrowableException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.springframework.lang.Nullable; + +/** + * Simple utility class for working with the reflection API and handling + * reflection exceptions. + * + *

    Only intended for internal use. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @author Rod Johnson + * @author Costin Leau + * @author Sam Brannen + * @author Chris Beams + * @since 1.2.2 + */ +public abstract class ReflectionUtils { + + /** + * Pre-built MethodFilter that matches all non-bridge non-synthetic methods + * which are not declared on {@code java.lang.Object}. + * @since 3.0.5 + */ + public static final MethodFilter USER_DECLARED_METHODS = + (method -> !method.isBridge() && !method.isSynthetic()); + + /** + * Pre-built FieldFilter that matches all non-static, non-final fields. + */ + public static final FieldFilter COPYABLE_FIELDS = + (field -> !(Modifier.isStatic(field.getModifiers()) || Modifier.isFinal(field.getModifiers()))); + + + /** + * Naming prefix for CGLIB-renamed methods. + * @see #isCglibRenamedMethod + */ + private static final String CGLIB_RENAMED_METHOD_PREFIX = "CGLIB$"; + + private static final Class[] EMPTY_CLASS_ARRAY = new Class[0]; + + private static final Method[] EMPTY_METHOD_ARRAY = new Method[0]; + + private static final Field[] EMPTY_FIELD_ARRAY = new Field[0]; + + private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; + + + /** + * Cache for {@link Class#getDeclaredMethods()} plus equivalent default methods + * from Java 8 based interfaces, allowing for fast iteration. + */ + private static final Map, Method[]> declaredMethodsCache = new ConcurrentReferenceHashMap<>(256); + + /** + * Cache for {@link Class#getDeclaredFields()}, allowing for fast iteration. + */ + private static final Map, Field[]> declaredFieldsCache = new ConcurrentReferenceHashMap<>(256); + + + // Exception handling + + /** + * Handle the given reflection exception. + *

    Should only be called if no checked exception is expected to be thrown + * by a target method, or if an error occurs while accessing a method or field. + *

    Throws the underlying RuntimeException or Error in case of an + * InvocationTargetException with such a root cause. Throws an + * IllegalStateException with an appropriate message or + * UndeclaredThrowableException otherwise. + * @param ex the reflection exception to handle + */ + public static void handleReflectionException(Exception ex) { + if (ex instanceof NoSuchMethodException) { + throw new IllegalStateException("Method not found: " + ex.getMessage()); + } + if (ex instanceof IllegalAccessException) { + throw new IllegalStateException("Could not access method or field: " + ex.getMessage()); + } + if (ex instanceof InvocationTargetException) { + handleInvocationTargetException((InvocationTargetException) ex); + } + if (ex instanceof RuntimeException) { + throw (RuntimeException) ex; + } + throw new UndeclaredThrowableException(ex); + } + + /** + * Handle the given invocation target exception. Should only be called if no + * checked exception is expected to be thrown by the target method. + *

    Throws the underlying RuntimeException or Error in case of such a root + * cause. Throws an UndeclaredThrowableException otherwise. + * @param ex the invocation target exception to handle + */ + public static void handleInvocationTargetException(InvocationTargetException ex) { + rethrowRuntimeException(ex.getTargetException()); + } + + /** + * Rethrow the given {@link Throwable exception}, which is presumably the + * target exception of an {@link InvocationTargetException}. + * Should only be called if no checked exception is expected to be thrown + * by the target method. + *

    Rethrows the underlying exception cast to a {@link RuntimeException} or + * {@link Error} if appropriate; otherwise, throws an + * {@link UndeclaredThrowableException}. + * @param ex the exception to rethrow + * @throws RuntimeException the rethrown exception + */ + public static void rethrowRuntimeException(Throwable ex) { + if (ex instanceof RuntimeException) { + throw (RuntimeException) ex; + } + if (ex instanceof Error) { + throw (Error) ex; + } + throw new UndeclaredThrowableException(ex); + } + + /** + * Rethrow the given {@link Throwable exception}, which is presumably the + * target exception of an {@link InvocationTargetException}. + * Should only be called if no checked exception is expected to be thrown + * by the target method. + *

    Rethrows the underlying exception cast to an {@link Exception} or + * {@link Error} if appropriate; otherwise, throws an + * {@link UndeclaredThrowableException}. + * @param ex the exception to rethrow + * @throws Exception the rethrown exception (in case of a checked exception) + */ + public static void rethrowException(Throwable ex) throws Exception { + if (ex instanceof Exception) { + throw (Exception) ex; + } + if (ex instanceof Error) { + throw (Error) ex; + } + throw new UndeclaredThrowableException(ex); + } + + + // Constructor handling + + /** + * Obtain an accessible constructor for the given class and parameters. + * @param clazz the clazz to check + * @param parameterTypes the parameter types of the desired constructor + * @return the constructor reference + * @throws NoSuchMethodException if no such constructor exists + * @since 5.0 + */ + public static Constructor accessibleConstructor(Class clazz, Class... parameterTypes) + throws NoSuchMethodException { + + Constructor ctor = clazz.getDeclaredConstructor(parameterTypes); + makeAccessible(ctor); + return ctor; + } + + /** + * Make the given constructor accessible, explicitly setting it accessible + * if necessary. The {@code setAccessible(true)} method is only called + * when actually necessary, to avoid unnecessary conflicts with a JVM + * SecurityManager (if active). + * @param ctor the constructor to make accessible + * @see java.lang.reflect.Constructor#setAccessible + */ + @SuppressWarnings("deprecation") // on JDK 9 + public static void makeAccessible(Constructor ctor) { + if ((!Modifier.isPublic(ctor.getModifiers()) || + !Modifier.isPublic(ctor.getDeclaringClass().getModifiers())) && !ctor.isAccessible()) { + ctor.setAccessible(true); + } + } + + + // Method handling + + /** + * Attempt to find a {@link Method} on the supplied class with the supplied name + * and no parameters. Searches all superclasses up to {@code Object}. + *

    Returns {@code null} if no {@link Method} can be found. + * @param clazz the class to introspect + * @param name the name of the method + * @return the Method object, or {@code null} if none found + */ + @Nullable + public static Method findMethod(Class clazz, String name) { + return findMethod(clazz, name, EMPTY_CLASS_ARRAY); + } + + /** + * Attempt to find a {@link Method} on the supplied class with the supplied name + * and parameter types. Searches all superclasses up to {@code Object}. + *

    Returns {@code null} if no {@link Method} can be found. + * @param clazz the class to introspect + * @param name the name of the method + * @param paramTypes the parameter types of the method + * (may be {@code null} to indicate any signature) + * @return the Method object, or {@code null} if none found + */ + @Nullable + public static Method findMethod(Class clazz, String name, @Nullable Class... paramTypes) { + Assert.notNull(clazz, "Class must not be null"); + Assert.notNull(name, "Method name must not be null"); + Class searchType = clazz; + while (searchType != null) { + Method[] methods = (searchType.isInterface() ? searchType.getMethods() : + getDeclaredMethods(searchType, false)); + for (Method method : methods) { + if (name.equals(method.getName()) && (paramTypes == null || hasSameParams(method, paramTypes))) { + return method; + } + } + searchType = searchType.getSuperclass(); + } + return null; + } + + private static boolean hasSameParams(Method method, Class[] paramTypes) { + return (paramTypes.length == method.getParameterCount() && + Arrays.equals(paramTypes, method.getParameterTypes())); + } + + /** + * Invoke the specified {@link Method} against the supplied target object with no arguments. + * The target object can be {@code null} when invoking a static {@link Method}. + *

    Thrown exceptions are handled via a call to {@link #handleReflectionException}. + * @param method the method to invoke + * @param target the target object to invoke the method on + * @return the invocation result, if any + * @see #invokeMethod(java.lang.reflect.Method, Object, Object[]) + */ + @Nullable + public static Object invokeMethod(Method method, @Nullable Object target) { + return invokeMethod(method, target, EMPTY_OBJECT_ARRAY); + } + + /** + * Invoke the specified {@link Method} against the supplied target object with the + * supplied arguments. The target object can be {@code null} when invoking a + * static {@link Method}. + *

    Thrown exceptions are handled via a call to {@link #handleReflectionException}. + * @param method the method to invoke + * @param target the target object to invoke the method on + * @param args the invocation arguments (may be {@code null}) + * @return the invocation result, if any + */ + @Nullable + public static Object invokeMethod(Method method, @Nullable Object target, @Nullable Object... args) { + try { + return method.invoke(target, args); + } + catch (Exception ex) { + handleReflectionException(ex); + } + throw new IllegalStateException("Should never get here"); + } + + /** + * Determine whether the given method explicitly declares the given + * exception or one of its superclasses, which means that an exception + * of that type can be propagated as-is within a reflective invocation. + * @param method the declaring method + * @param exceptionType the exception to throw + * @return {@code true} if the exception can be thrown as-is; + * {@code false} if it needs to be wrapped + */ + public static boolean declaresException(Method method, Class exceptionType) { + Assert.notNull(method, "Method must not be null"); + Class[] declaredExceptions = method.getExceptionTypes(); + for (Class declaredException : declaredExceptions) { + if (declaredException.isAssignableFrom(exceptionType)) { + return true; + } + } + return false; + } + + /** + * Perform the given callback operation on all matching methods of the given + * class, as locally declared or equivalent thereof (such as default methods + * on Java 8 based interfaces that the given class implements). + * @param clazz the class to introspect + * @param mc the callback to invoke for each method + * @throws IllegalStateException if introspection fails + * @since 4.2 + * @see #doWithMethods + */ + public static void doWithLocalMethods(Class clazz, MethodCallback mc) { + Method[] methods = getDeclaredMethods(clazz, false); + for (Method method : methods) { + try { + mc.doWith(method); + } + catch (IllegalAccessException ex) { + throw new IllegalStateException("Not allowed to access method '" + method.getName() + "': " + ex); + } + } + } + + /** + * Perform the given callback operation on all matching methods of the given + * class and superclasses. + *

    The same named method occurring on subclass and superclass will appear + * twice, unless excluded by a {@link MethodFilter}. + * @param clazz the class to introspect + * @param mc the callback to invoke for each method + * @throws IllegalStateException if introspection fails + * @see #doWithMethods(Class, MethodCallback, MethodFilter) + */ + public static void doWithMethods(Class clazz, MethodCallback mc) { + doWithMethods(clazz, mc, null); + } + + /** + * Perform the given callback operation on all matching methods of the given + * class and superclasses (or given interface and super-interfaces). + *

    The same named method occurring on subclass and superclass will appear + * twice, unless excluded by the specified {@link MethodFilter}. + * @param clazz the class to introspect + * @param mc the callback to invoke for each method + * @param mf the filter that determines the methods to apply the callback to + * @throws IllegalStateException if introspection fails + */ + public static void doWithMethods(Class clazz, MethodCallback mc, @Nullable MethodFilter mf) { + // Keep backing up the inheritance hierarchy. + Method[] methods = getDeclaredMethods(clazz, false); + for (Method method : methods) { + if (mf != null && !mf.matches(method)) { + continue; + } + try { + mc.doWith(method); + } + catch (IllegalAccessException ex) { + throw new IllegalStateException("Not allowed to access method '" + method.getName() + "': " + ex); + } + } + if (clazz.getSuperclass() != null && (mf != USER_DECLARED_METHODS || clazz.getSuperclass() != Object.class)) { + doWithMethods(clazz.getSuperclass(), mc, mf); + } + else if (clazz.isInterface()) { + for (Class superIfc : clazz.getInterfaces()) { + doWithMethods(superIfc, mc, mf); + } + } + } + + /** + * Get all declared methods on the leaf class and all superclasses. + * Leaf class methods are included first. + * @param leafClass the class to introspect + * @throws IllegalStateException if introspection fails + */ + public static Method[] getAllDeclaredMethods(Class leafClass) { + final List methods = new ArrayList<>(32); + doWithMethods(leafClass, methods::add); + return methods.toArray(EMPTY_METHOD_ARRAY); + } + + /** + * Get the unique set of declared methods on the leaf class and all superclasses. + * Leaf class methods are included first and while traversing the superclass hierarchy + * any methods found with signatures matching a method already included are filtered out. + * @param leafClass the class to introspect + * @throws IllegalStateException if introspection fails + */ + public static Method[] getUniqueDeclaredMethods(Class leafClass) { + return getUniqueDeclaredMethods(leafClass, null); + } + + /** + * Get the unique set of declared methods on the leaf class and all superclasses. + * Leaf class methods are included first and while traversing the superclass hierarchy + * any methods found with signatures matching a method already included are filtered out. + * @param leafClass the class to introspect + * @param mf the filter that determines the methods to take into account + * @throws IllegalStateException if introspection fails + * @since 5.2 + */ + public static Method[] getUniqueDeclaredMethods(Class leafClass, @Nullable MethodFilter mf) { + final List methods = new ArrayList<>(32); + doWithMethods(leafClass, method -> { + boolean knownSignature = false; + Method methodBeingOverriddenWithCovariantReturnType = null; + for (Method existingMethod : methods) { + if (method.getName().equals(existingMethod.getName()) && + method.getParameterCount() == existingMethod.getParameterCount() && + Arrays.equals(method.getParameterTypes(), existingMethod.getParameterTypes())) { + // Is this a covariant return type situation? + if (existingMethod.getReturnType() != method.getReturnType() && + existingMethod.getReturnType().isAssignableFrom(method.getReturnType())) { + methodBeingOverriddenWithCovariantReturnType = existingMethod; + } + else { + knownSignature = true; + } + break; + } + } + if (methodBeingOverriddenWithCovariantReturnType != null) { + methods.remove(methodBeingOverriddenWithCovariantReturnType); + } + if (!knownSignature && !isCglibRenamedMethod(method)) { + methods.add(method); + } + }, mf); + return methods.toArray(EMPTY_METHOD_ARRAY); + } + + /** + * Variant of {@link Class#getDeclaredMethods()} that uses a local cache in + * order to avoid the JVM's SecurityManager check and new Method instances. + * In addition, it also includes Java 8 default methods from locally + * implemented interfaces, since those are effectively to be treated just + * like declared methods. + * @param clazz the class to introspect + * @return the cached array of methods + * @throws IllegalStateException if introspection fails + * @since 5.2 + * @see Class#getDeclaredMethods() + */ + public static Method[] getDeclaredMethods(Class clazz) { + return getDeclaredMethods(clazz, true); + } + + private static Method[] getDeclaredMethods(Class clazz, boolean defensive) { + Assert.notNull(clazz, "Class must not be null"); + Method[] result = declaredMethodsCache.get(clazz); + if (result == null) { + try { + Method[] declaredMethods = clazz.getDeclaredMethods(); + List defaultMethods = findConcreteMethodsOnInterfaces(clazz); + if (defaultMethods != null) { + result = new Method[declaredMethods.length + defaultMethods.size()]; + System.arraycopy(declaredMethods, 0, result, 0, declaredMethods.length); + int index = declaredMethods.length; + for (Method defaultMethod : defaultMethods) { + result[index] = defaultMethod; + index++; + } + } + else { + result = declaredMethods; + } + declaredMethodsCache.put(clazz, (result.length == 0 ? EMPTY_METHOD_ARRAY : result)); + } + catch (Throwable ex) { + throw new IllegalStateException("Failed to introspect Class [" + clazz.getName() + + "] from ClassLoader [" + clazz.getClassLoader() + "]", ex); + } + } + return (result.length == 0 || !defensive) ? result : result.clone(); + } + + @Nullable + private static List findConcreteMethodsOnInterfaces(Class clazz) { + List result = null; + for (Class ifc : clazz.getInterfaces()) { + for (Method ifcMethod : ifc.getMethods()) { + if (!Modifier.isAbstract(ifcMethod.getModifiers())) { + if (result == null) { + result = new ArrayList<>(); + } + result.add(ifcMethod); + } + } + } + return result; + } + + /** + * Determine whether the given method is an "equals" method. + * @see java.lang.Object#equals(Object) + */ + public static boolean isEqualsMethod(@Nullable Method method) { + if (method == null) { + return false; + } + if (method.getParameterCount() != 1) { + return false; + } + if (!method.getName().equals("equals")) { + return false; + } + return method.getParameterTypes()[0] == Object.class; + } + + /** + * Determine whether the given method is a "hashCode" method. + * @see java.lang.Object#hashCode() + */ + public static boolean isHashCodeMethod(@Nullable Method method) { + return method != null && method.getParameterCount() == 0 && method.getName().equals("hashCode"); + } + + /** + * Determine whether the given method is a "toString" method. + * @see java.lang.Object#toString() + */ + public static boolean isToStringMethod(@Nullable Method method) { + return (method != null && method.getParameterCount() == 0 && method.getName().equals("toString")); + } + + /** + * Determine whether the given method is originally declared by {@link java.lang.Object}. + */ + public static boolean isObjectMethod(@Nullable Method method) { + return (method != null && (method.getDeclaringClass() == Object.class || + isEqualsMethod(method) || isHashCodeMethod(method) || isToStringMethod(method))); + } + + /** + * Determine whether the given method is a CGLIB 'renamed' method, + * following the pattern "CGLIB$methodName$0". + * @param renamedMethod the method to check + */ + public static boolean isCglibRenamedMethod(Method renamedMethod) { + String name = renamedMethod.getName(); + if (name.startsWith(CGLIB_RENAMED_METHOD_PREFIX)) { + int i = name.length() - 1; + while (i >= 0 && Character.isDigit(name.charAt(i))) { + i--; + } + return (i > CGLIB_RENAMED_METHOD_PREFIX.length() && (i < name.length() - 1) && name.charAt(i) == '$'); + } + return false; + } + + /** + * Make the given method accessible, explicitly setting it accessible if + * necessary. The {@code setAccessible(true)} method is only called + * when actually necessary, to avoid unnecessary conflicts with a JVM + * SecurityManager (if active). + * @param method the method to make accessible + * @see java.lang.reflect.Method#setAccessible + */ + @SuppressWarnings("deprecation") // on JDK 9 + public static void makeAccessible(Method method) { + if ((!Modifier.isPublic(method.getModifiers()) || + !Modifier.isPublic(method.getDeclaringClass().getModifiers())) && !method.isAccessible()) { + method.setAccessible(true); + } + } + + + // Field handling + + /** + * Attempt to find a {@link Field field} on the supplied {@link Class} with the + * supplied {@code name}. Searches all superclasses up to {@link Object}. + * @param clazz the class to introspect + * @param name the name of the field + * @return the corresponding Field object, or {@code null} if not found + */ + @Nullable + public static Field findField(Class clazz, String name) { + return findField(clazz, name, null); + } + + /** + * Attempt to find a {@link Field field} on the supplied {@link Class} with the + * supplied {@code name} and/or {@link Class type}. Searches all superclasses + * up to {@link Object}. + * @param clazz the class to introspect + * @param name the name of the field (may be {@code null} if type is specified) + * @param type the type of the field (may be {@code null} if name is specified) + * @return the corresponding Field object, or {@code null} if not found + */ + @Nullable + public static Field findField(Class clazz, @Nullable String name, @Nullable Class type) { + Assert.notNull(clazz, "Class must not be null"); + Assert.isTrue(name != null || type != null, "Either name or type of the field must be specified"); + Class searchType = clazz; + while (Object.class != searchType && searchType != null) { + Field[] fields = getDeclaredFields(searchType); + for (Field field : fields) { + if ((name == null || name.equals(field.getName())) && + (type == null || type.equals(field.getType()))) { + return field; + } + } + searchType = searchType.getSuperclass(); + } + return null; + } + + /** + * Set the field represented by the supplied {@linkplain Field field object} on + * the specified {@linkplain Object target object} to the specified {@code value}. + *

    In accordance with {@link Field#set(Object, Object)} semantics, the new value + * is automatically unwrapped if the underlying field has a primitive type. + *

    This method does not support setting {@code static final} fields. + *

    Thrown exceptions are handled via a call to {@link #handleReflectionException(Exception)}. + * @param field the field to set + * @param target the target object on which to set the field + * @param value the value to set (may be {@code null}) + */ + public static void setField(Field field, @Nullable Object target, @Nullable Object value) { + try { + field.set(target, value); + } + catch (IllegalAccessException ex) { + handleReflectionException(ex); + } + } + + /** + * Get the field represented by the supplied {@link Field field object} on the + * specified {@link Object target object}. In accordance with {@link Field#get(Object)} + * semantics, the returned value is automatically wrapped if the underlying field + * has a primitive type. + *

    Thrown exceptions are handled via a call to {@link #handleReflectionException(Exception)}. + * @param field the field to get + * @param target the target object from which to get the field + * @return the field's current value + */ + @Nullable + public static Object getField(Field field, @Nullable Object target) { + try { + return field.get(target); + } + catch (IllegalAccessException ex) { + handleReflectionException(ex); + } + throw new IllegalStateException("Should never get here"); + } + + /** + * Invoke the given callback on all locally declared fields in the given class. + * @param clazz the target class to analyze + * @param fc the callback to invoke for each field + * @throws IllegalStateException if introspection fails + * @since 4.2 + * @see #doWithFields + */ + public static void doWithLocalFields(Class clazz, FieldCallback fc) { + for (Field field : getDeclaredFields(clazz)) { + try { + fc.doWith(field); + } + catch (IllegalAccessException ex) { + throw new IllegalStateException("Not allowed to access field '" + field.getName() + "': " + ex); + } + } + } + + /** + * Invoke the given callback on all fields in the target class, going up the + * class hierarchy to get all declared fields. + * @param clazz the target class to analyze + * @param fc the callback to invoke for each field + * @throws IllegalStateException if introspection fails + */ + public static void doWithFields(Class clazz, FieldCallback fc) { + doWithFields(clazz, fc, null); + } + + /** + * Invoke the given callback on all fields in the target class, going up the + * class hierarchy to get all declared fields. + * @param clazz the target class to analyze + * @param fc the callback to invoke for each field + * @param ff the filter that determines the fields to apply the callback to + * @throws IllegalStateException if introspection fails + */ + public static void doWithFields(Class clazz, FieldCallback fc, @Nullable FieldFilter ff) { + // Keep backing up the inheritance hierarchy. + Class targetClass = clazz; + do { + Field[] fields = getDeclaredFields(targetClass); + for (Field field : fields) { + if (ff != null && !ff.matches(field)) { + continue; + } + try { + fc.doWith(field); + } + catch (IllegalAccessException ex) { + throw new IllegalStateException("Not allowed to access field '" + field.getName() + "': " + ex); + } + } + targetClass = targetClass.getSuperclass(); + } + while (targetClass != null && targetClass != Object.class); + } + + /** + * This variant retrieves {@link Class#getDeclaredFields()} from a local cache + * in order to avoid the JVM's SecurityManager check and defensive array copying. + * @param clazz the class to introspect + * @return the cached array of fields + * @throws IllegalStateException if introspection fails + * @see Class#getDeclaredFields() + */ + private static Field[] getDeclaredFields(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + Field[] result = declaredFieldsCache.get(clazz); + if (result == null) { + try { + result = clazz.getDeclaredFields(); + declaredFieldsCache.put(clazz, (result.length == 0 ? EMPTY_FIELD_ARRAY : result)); + } + catch (Throwable ex) { + throw new IllegalStateException("Failed to introspect Class [" + clazz.getName() + + "] from ClassLoader [" + clazz.getClassLoader() + "]", ex); + } + } + return result; + } + + /** + * Given the source object and the destination, which must be the same class + * or a subclass, copy all fields, including inherited fields. Designed to + * work on objects with public no-arg constructors. + * @throws IllegalStateException if introspection fails + */ + public static void shallowCopyFieldState(final Object src, final Object dest) { + Assert.notNull(src, "Source for field copy cannot be null"); + Assert.notNull(dest, "Destination for field copy cannot be null"); + if (!src.getClass().isAssignableFrom(dest.getClass())) { + throw new IllegalArgumentException("Destination class [" + dest.getClass().getName() + + "] must be same or subclass as source class [" + src.getClass().getName() + "]"); + } + doWithFields(src.getClass(), field -> { + makeAccessible(field); + Object srcValue = field.get(src); + field.set(dest, srcValue); + }, COPYABLE_FIELDS); + } + + /** + * Determine whether the given field is a "public static final" constant. + * @param field the field to check + */ + public static boolean isPublicStaticFinal(Field field) { + int modifiers = field.getModifiers(); + return (Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers)); + } + + /** + * Make the given field accessible, explicitly setting it accessible if + * necessary. The {@code setAccessible(true)} method is only called + * when actually necessary, to avoid unnecessary conflicts with a JVM + * SecurityManager (if active). + * @param field the field to make accessible + * @see java.lang.reflect.Field#setAccessible + */ + @SuppressWarnings("deprecation") // on JDK 9 + public static void makeAccessible(Field field) { + if ((!Modifier.isPublic(field.getModifiers()) || + !Modifier.isPublic(field.getDeclaringClass().getModifiers()) || + Modifier.isFinal(field.getModifiers())) && !field.isAccessible()) { + field.setAccessible(true); + } + } + + + // Cache handling + + /** + * Clear the internal method/field cache. + * @since 4.2.4 + */ + public static void clearCache() { + declaredMethodsCache.clear(); + declaredFieldsCache.clear(); + } + + + /** + * Action to take on each method. + */ + @FunctionalInterface + public interface MethodCallback { + + /** + * Perform an operation using the given method. + * @param method the method to operate on + */ + void doWith(Method method) throws IllegalArgumentException, IllegalAccessException; + } + + + /** + * Callback optionally used to filter methods to be operated on by a method callback. + */ + @FunctionalInterface + public interface MethodFilter { + + /** + * Determine whether the given method matches. + * @param method the method to check + */ + boolean matches(Method method); + + /** + * Create a composite filter based on this filter and the provided filter. + *

    If this filter does not match, the next filter will not be applied. + * @param next the next {@code MethodFilter} + * @return a composite {@code MethodFilter} + * @throws IllegalArgumentException if the MethodFilter argument is {@code null} + * @since 5.3.2 + */ + default MethodFilter and(MethodFilter next) { + Assert.notNull(next, "Next MethodFilter must not be null"); + return method -> matches(method) && next.matches(method); + } + } + + + /** + * Callback interface invoked on each field in the hierarchy. + */ + @FunctionalInterface + public interface FieldCallback { + + /** + * Perform an operation using the given field. + * @param field the field to operate on + */ + void doWith(Field field) throws IllegalArgumentException, IllegalAccessException; + } + + + /** + * Callback optionally used to filter fields to be operated on by a field callback. + */ + @FunctionalInterface + public interface FieldFilter { + + /** + * Determine whether the given field matches. + * @param field the field to check + */ + boolean matches(Field field); + + /** + * Create a composite filter based on this filter and the provided filter. + *

    If this filter does not match, the next filter will not be applied. + * @param next the next {@code FieldFilter} + * @return a composite {@code FieldFilter} + * @throws IllegalArgumentException if the FieldFilter argument is {@code null} + * @since 5.3.2 + */ + default FieldFilter and(FieldFilter next) { + Assert.notNull(next, "Next FieldFilter must not be null"); + return field -> matches(field) && next.matches(field); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/ResizableByteArrayOutputStream.java b/spring-core/src/main/java/org/springframework/util/ResizableByteArrayOutputStream.java new file mode 100644 index 0000000..bbbc0d7 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/ResizableByteArrayOutputStream.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.io.ByteArrayOutputStream; + +/** + * An extension of {@link java.io.ByteArrayOutputStream} that: + *

      + *
    • has public {@link org.springframework.util.ResizableByteArrayOutputStream#grow(int)} + * and {@link org.springframework.util.ResizableByteArrayOutputStream#resize(int)} methods + * to get more control over the size of the internal buffer
    • + *
    • has a higher initial capacity (256) by default
    • + *
    + * + *

    As of 4.2, this class has been superseded by {@link FastByteArrayOutputStream} + * for Spring's internal use where no assignability to {@link ByteArrayOutputStream} + * is needed (since {@link FastByteArrayOutputStream} is more efficient with buffer + * resize management but doesn't extend the standard {@link ByteArrayOutputStream}). + * + * @author Brian Clozel + * @author Juergen Hoeller + * @since 4.0.3 + * @see #resize + * @see FastByteArrayOutputStream + */ +public class ResizableByteArrayOutputStream extends ByteArrayOutputStream { + + private static final int DEFAULT_INITIAL_CAPACITY = 256; + + + /** + * Create a new ResizableByteArrayOutputStream + * with the default initial capacity of 256 bytes. + */ + public ResizableByteArrayOutputStream() { + super(DEFAULT_INITIAL_CAPACITY); + } + + /** + * Create a new ResizableByteArrayOutputStream + * with the specified initial capacity. + * @param initialCapacity the initial buffer size in bytes + */ + public ResizableByteArrayOutputStream(int initialCapacity) { + super(initialCapacity); + } + + + /** + * Resize the internal buffer size to a specified capacity. + * @param targetCapacity the desired size of the buffer + * @throws IllegalArgumentException if the given capacity is smaller than + * the actual size of the content stored in the buffer already + * @see ResizableByteArrayOutputStream#size() + */ + public synchronized void resize(int targetCapacity) { + Assert.isTrue(targetCapacity >= this.count, "New capacity must not be smaller than current size"); + byte[] resizedBuffer = new byte[targetCapacity]; + System.arraycopy(this.buf, 0, resizedBuffer, 0, this.count); + this.buf = resizedBuffer; + } + + /** + * Grow the internal buffer size. + * @param additionalCapacity the number of bytes to add to the current buffer size + * @see ResizableByteArrayOutputStream#size() + */ + public synchronized void grow(int additionalCapacity) { + Assert.isTrue(additionalCapacity >= 0, "Additional capacity must be 0 or higher"); + if (this.count + additionalCapacity > this.buf.length) { + int newCapacity = Math.max(this.buf.length * 2, this.count + additionalCapacity); + resize(newCapacity); + } + } + + /** + * Return the current size of this stream's internal buffer. + */ + public synchronized int capacity() { + return this.buf.length; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/ResourceUtils.java b/spring-core/src/main/java/org/springframework/util/ResourceUtils.java new file mode 100644 index 0000000..2caaf33 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/ResourceUtils.java @@ -0,0 +1,394 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.io.File; +import java.io.FileNotFoundException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; + +import org.springframework.lang.Nullable; + +/** + * Utility methods for resolving resource locations to files in the + * file system. Mainly for internal use within the framework. + * + *

    Consider using Spring's Resource abstraction in the core package + * for handling all kinds of file resources in a uniform manner. + * {@link org.springframework.core.io.ResourceLoader}'s {@code getResource()} + * method can resolve any location to a {@link org.springframework.core.io.Resource} + * object, which in turn allows one to obtain a {@code java.io.File} in the + * file system through its {@code getFile()} method. + * + * @author Juergen Hoeller + * @since 1.1.5 + * @see org.springframework.core.io.Resource + * @see org.springframework.core.io.ClassPathResource + * @see org.springframework.core.io.FileSystemResource + * @see org.springframework.core.io.UrlResource + * @see org.springframework.core.io.ResourceLoader + */ +public abstract class ResourceUtils { + + /** Pseudo URL prefix for loading from the class path: "classpath:". */ + public static final String CLASSPATH_URL_PREFIX = "classpath:"; + + /** URL prefix for loading from the file system: "file:". */ + public static final String FILE_URL_PREFIX = "file:"; + + /** URL prefix for loading from a jar file: "jar:". */ + public static final String JAR_URL_PREFIX = "jar:"; + + /** URL prefix for loading from a war file on Tomcat: "war:". */ + public static final String WAR_URL_PREFIX = "war:"; + + /** URL protocol for a file in the file system: "file". */ + public static final String URL_PROTOCOL_FILE = "file"; + + /** URL protocol for an entry from a jar file: "jar". */ + public static final String URL_PROTOCOL_JAR = "jar"; + + /** URL protocol for an entry from a war file: "war". */ + public static final String URL_PROTOCOL_WAR = "war"; + + /** URL protocol for an entry from a zip file: "zip". */ + public static final String URL_PROTOCOL_ZIP = "zip"; + + /** URL protocol for an entry from a WebSphere jar file: "wsjar". */ + public static final String URL_PROTOCOL_WSJAR = "wsjar"; + + /** URL protocol for an entry from a JBoss jar file: "vfszip". */ + public static final String URL_PROTOCOL_VFSZIP = "vfszip"; + + /** URL protocol for a JBoss file system resource: "vfsfile". */ + public static final String URL_PROTOCOL_VFSFILE = "vfsfile"; + + /** URL protocol for a general JBoss VFS resource: "vfs". */ + public static final String URL_PROTOCOL_VFS = "vfs"; + + /** File extension for a regular jar file: ".jar". */ + public static final String JAR_FILE_EXTENSION = ".jar"; + + /** Separator between JAR URL and file path within the JAR: "!/". */ + public static final String JAR_URL_SEPARATOR = "!/"; + + /** Special separator between WAR URL and jar part on Tomcat. */ + public static final String WAR_URL_SEPARATOR = "*/"; + + + /** + * Return whether the given resource location is a URL: + * either a special "classpath" pseudo URL or a standard URL. + * @param resourceLocation the location String to check + * @return whether the location qualifies as a URL + * @see #CLASSPATH_URL_PREFIX + * @see java.net.URL + */ + public static boolean isUrl(@Nullable String resourceLocation) { + if (resourceLocation == null) { + return false; + } + if (resourceLocation.startsWith(CLASSPATH_URL_PREFIX)) { + return true; + } + try { + new URL(resourceLocation); + return true; + } + catch (MalformedURLException ex) { + return false; + } + } + + /** + * Resolve the given resource location to a {@code java.net.URL}. + *

    Does not check whether the URL actually exists; simply returns + * the URL that the given location would correspond to. + * @param resourceLocation the resource location to resolve: either a + * "classpath:" pseudo URL, a "file:" URL, or a plain file path + * @return a corresponding URL object + * @throws FileNotFoundException if the resource cannot be resolved to a URL + */ + public static URL getURL(String resourceLocation) throws FileNotFoundException { + Assert.notNull(resourceLocation, "Resource location must not be null"); + if (resourceLocation.startsWith(CLASSPATH_URL_PREFIX)) { + String path = resourceLocation.substring(CLASSPATH_URL_PREFIX.length()); + ClassLoader cl = ClassUtils.getDefaultClassLoader(); + URL url = (cl != null ? cl.getResource(path) : ClassLoader.getSystemResource(path)); + if (url == null) { + String description = "class path resource [" + path + "]"; + throw new FileNotFoundException(description + + " cannot be resolved to URL because it does not exist"); + } + return url; + } + try { + // try URL + return new URL(resourceLocation); + } + catch (MalformedURLException ex) { + // no URL -> treat as file path + try { + return new File(resourceLocation).toURI().toURL(); + } + catch (MalformedURLException ex2) { + throw new FileNotFoundException("Resource location [" + resourceLocation + + "] is neither a URL not a well-formed file path"); + } + } + } + + /** + * Resolve the given resource location to a {@code java.io.File}, + * i.e. to a file in the file system. + *

    Does not check whether the file actually exists; simply returns + * the File that the given location would correspond to. + * @param resourceLocation the resource location to resolve: either a + * "classpath:" pseudo URL, a "file:" URL, or a plain file path + * @return a corresponding File object + * @throws FileNotFoundException if the resource cannot be resolved to + * a file in the file system + */ + public static File getFile(String resourceLocation) throws FileNotFoundException { + Assert.notNull(resourceLocation, "Resource location must not be null"); + if (resourceLocation.startsWith(CLASSPATH_URL_PREFIX)) { + String path = resourceLocation.substring(CLASSPATH_URL_PREFIX.length()); + String description = "class path resource [" + path + "]"; + ClassLoader cl = ClassUtils.getDefaultClassLoader(); + URL url = (cl != null ? cl.getResource(path) : ClassLoader.getSystemResource(path)); + if (url == null) { + throw new FileNotFoundException(description + + " cannot be resolved to absolute file path because it does not exist"); + } + return getFile(url, description); + } + try { + // try URL + return getFile(new URL(resourceLocation)); + } + catch (MalformedURLException ex) { + // no URL -> treat as file path + return new File(resourceLocation); + } + } + + /** + * Resolve the given resource URL to a {@code java.io.File}, + * i.e. to a file in the file system. + * @param resourceUrl the resource URL to resolve + * @return a corresponding File object + * @throws FileNotFoundException if the URL cannot be resolved to + * a file in the file system + */ + public static File getFile(URL resourceUrl) throws FileNotFoundException { + return getFile(resourceUrl, "URL"); + } + + /** + * Resolve the given resource URL to a {@code java.io.File}, + * i.e. to a file in the file system. + * @param resourceUrl the resource URL to resolve + * @param description a description of the original resource that + * the URL was created for (for example, a class path location) + * @return a corresponding File object + * @throws FileNotFoundException if the URL cannot be resolved to + * a file in the file system + */ + public static File getFile(URL resourceUrl, String description) throws FileNotFoundException { + Assert.notNull(resourceUrl, "Resource URL must not be null"); + if (!URL_PROTOCOL_FILE.equals(resourceUrl.getProtocol())) { + throw new FileNotFoundException( + description + " cannot be resolved to absolute file path " + + "because it does not reside in the file system: " + resourceUrl); + } + try { + return new File(toURI(resourceUrl).getSchemeSpecificPart()); + } + catch (URISyntaxException ex) { + // Fallback for URLs that are not valid URIs (should hardly ever happen). + return new File(resourceUrl.getFile()); + } + } + + /** + * Resolve the given resource URI to a {@code java.io.File}, + * i.e. to a file in the file system. + * @param resourceUri the resource URI to resolve + * @return a corresponding File object + * @throws FileNotFoundException if the URL cannot be resolved to + * a file in the file system + * @since 2.5 + */ + public static File getFile(URI resourceUri) throws FileNotFoundException { + return getFile(resourceUri, "URI"); + } + + /** + * Resolve the given resource URI to a {@code java.io.File}, + * i.e. to a file in the file system. + * @param resourceUri the resource URI to resolve + * @param description a description of the original resource that + * the URI was created for (for example, a class path location) + * @return a corresponding File object + * @throws FileNotFoundException if the URL cannot be resolved to + * a file in the file system + * @since 2.5 + */ + public static File getFile(URI resourceUri, String description) throws FileNotFoundException { + Assert.notNull(resourceUri, "Resource URI must not be null"); + if (!URL_PROTOCOL_FILE.equals(resourceUri.getScheme())) { + throw new FileNotFoundException( + description + " cannot be resolved to absolute file path " + + "because it does not reside in the file system: " + resourceUri); + } + return new File(resourceUri.getSchemeSpecificPart()); + } + + /** + * Determine whether the given URL points to a resource in the file system, + * i.e. has protocol "file", "vfsfile" or "vfs". + * @param url the URL to check + * @return whether the URL has been identified as a file system URL + */ + public static boolean isFileURL(URL url) { + String protocol = url.getProtocol(); + return (URL_PROTOCOL_FILE.equals(protocol) || URL_PROTOCOL_VFSFILE.equals(protocol) || + URL_PROTOCOL_VFS.equals(protocol)); + } + + /** + * Determine whether the given URL points to a resource in a jar file. + * i.e. has protocol "jar", "war, ""zip", "vfszip" or "wsjar". + * @param url the URL to check + * @return whether the URL has been identified as a JAR URL + */ + public static boolean isJarURL(URL url) { + String protocol = url.getProtocol(); + return (URL_PROTOCOL_JAR.equals(protocol) || URL_PROTOCOL_WAR.equals(protocol) || + URL_PROTOCOL_ZIP.equals(protocol) || URL_PROTOCOL_VFSZIP.equals(protocol) || + URL_PROTOCOL_WSJAR.equals(protocol)); + } + + /** + * Determine whether the given URL points to a jar file itself, + * that is, has protocol "file" and ends with the ".jar" extension. + * @param url the URL to check + * @return whether the URL has been identified as a JAR file URL + * @since 4.1 + */ + public static boolean isJarFileURL(URL url) { + return (URL_PROTOCOL_FILE.equals(url.getProtocol()) && + url.getPath().toLowerCase().endsWith(JAR_FILE_EXTENSION)); + } + + /** + * Extract the URL for the actual jar file from the given URL + * (which may point to a resource in a jar file or to a jar file itself). + * @param jarUrl the original URL + * @return the URL for the actual jar file + * @throws MalformedURLException if no valid jar file URL could be extracted + */ + public static URL extractJarFileURL(URL jarUrl) throws MalformedURLException { + String urlFile = jarUrl.getFile(); + int separatorIndex = urlFile.indexOf(JAR_URL_SEPARATOR); + if (separatorIndex != -1) { + String jarFile = urlFile.substring(0, separatorIndex); + try { + return new URL(jarFile); + } + catch (MalformedURLException ex) { + // Probably no protocol in original jar URL, like "jar:C:/mypath/myjar.jar". + // This usually indicates that the jar file resides in the file system. + if (!jarFile.startsWith("/")) { + jarFile = "/" + jarFile; + } + return new URL(FILE_URL_PREFIX + jarFile); + } + } + else { + return jarUrl; + } + } + + /** + * Extract the URL for the outermost archive from the given jar/war URL + * (which may point to a resource in a jar file or to a jar file itself). + *

    In the case of a jar file nested within a war file, this will return + * a URL to the war file since that is the one resolvable in the file system. + * @param jarUrl the original URL + * @return the URL for the actual jar file + * @throws MalformedURLException if no valid jar file URL could be extracted + * @since 4.1.8 + * @see #extractJarFileURL(URL) + */ + public static URL extractArchiveURL(URL jarUrl) throws MalformedURLException { + String urlFile = jarUrl.getFile(); + + int endIndex = urlFile.indexOf(WAR_URL_SEPARATOR); + if (endIndex != -1) { + // Tomcat's "war:file:...mywar.war*/WEB-INF/lib/myjar.jar!/myentry.txt" + String warFile = urlFile.substring(0, endIndex); + if (URL_PROTOCOL_WAR.equals(jarUrl.getProtocol())) { + return new URL(warFile); + } + int startIndex = warFile.indexOf(WAR_URL_PREFIX); + if (startIndex != -1) { + return new URL(warFile.substring(startIndex + WAR_URL_PREFIX.length())); + } + } + + // Regular "jar:file:...myjar.jar!/myentry.txt" + return extractJarFileURL(jarUrl); + } + + /** + * Create a URI instance for the given URL, + * replacing spaces with "%20" URI encoding first. + * @param url the URL to convert into a URI instance + * @return the URI instance + * @throws URISyntaxException if the URL wasn't a valid URI + * @see java.net.URL#toURI() + */ + public static URI toURI(URL url) throws URISyntaxException { + return toURI(url.toString()); + } + + /** + * Create a URI instance for the given location String, + * replacing spaces with "%20" URI encoding first. + * @param location the location String to convert into a URI instance + * @return the URI instance + * @throws URISyntaxException if the location wasn't a valid URI + */ + public static URI toURI(String location) throws URISyntaxException { + return new URI(StringUtils.replace(location, " ", "%20")); + } + + /** + * Set the {@link URLConnection#setUseCaches "useCaches"} flag on the + * given connection, preferring {@code false} but leaving the + * flag at {@code true} for JNLP based resources. + * @param con the URLConnection to set the flag on + */ + public static void useCachesIfNecessary(URLConnection con) { + con.setUseCaches(con.getClass().getSimpleName().startsWith("JNLP")); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/RouteMatcher.java b/spring-core/src/main/java/org/springframework/util/RouteMatcher.java new file mode 100644 index 0000000..166f9e6 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/RouteMatcher.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.util.Comparator; +import java.util.Map; + +import org.springframework.lang.Nullable; + +/** + * Contract for matching routes to patterns. + * + *

    Equivalent to {@link PathMatcher}, but enables use of parsed representations + * of routes and patterns for efficiency reasons in scenarios where routes from + * incoming messages are continuously matched against a large number of message + * handler patterns. + * + * @author Rossen Stoyanchev + * @since 5.2 + * @see PathMatcher + */ +public interface RouteMatcher { + + /** + * Return a parsed representation of the given route. + * @param routeValue the route to parse + * @return the parsed representation of the route + */ + Route parseRoute(String routeValue); + + /** + * Whether the given {@code route} contains pattern syntax which requires + * the {@link #match(String, Route)} method, or if it is a regular String + * that could be compared directly to others. + * @param route the route to check + * @return {@code true} if the given {@code route} represents a pattern + */ + boolean isPattern(String route); + + /** + * Combines two patterns into a single pattern. + * @param pattern1 the first pattern + * @param pattern2 the second pattern + * @return the combination of the two patterns + * @throws IllegalArgumentException when the two patterns cannot be combined + */ + String combine(String pattern1, String pattern2); + + /** + * Match the given route against the given pattern. + * @param pattern the pattern to try to match + * @param route the route to test against + * @return {@code true} if there is a match, {@code false} otherwise + */ + boolean match(String pattern, Route route); + + /** + * Match the pattern to the route and extract template variables. + * @param pattern the pattern, possibly containing templates variables + * @param route the route to extract template variables from + * @return a map with template variables and values + */ + @Nullable + Map matchAndExtract(String pattern, Route route); + + /** + * Given a route, return a {@link Comparator} suitable for sorting patterns + * in order of explicitness for that route, so that more specific patterns + * come before more generic ones. + * @param route the full path to use for comparison + * @return a comparator capable of sorting patterns in order of explicitness + */ + Comparator getPatternComparator(Route route); + + + /** + * A parsed representation of a route. + */ + interface Route { + + /** + * The original route value. + */ + String value(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/SerializationUtils.java b/spring-core/src/main/java/org/springframework/util/SerializationUtils.java new file mode 100644 index 0000000..1f15a16 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/SerializationUtils.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import org.springframework.lang.Nullable; + +/** + * Static utilities for serialization and deserialization. + * + * @author Dave Syer + * @since 3.0.5 + */ +public abstract class SerializationUtils { + + /** + * Serialize the given object to a byte array. + * @param object the object to serialize + * @return an array of bytes representing the object in a portable fashion + */ + @Nullable + public static byte[] serialize(@Nullable Object object) { + if (object == null) { + return null; + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); + try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(object); + oos.flush(); + } + catch (IOException ex) { + throw new IllegalArgumentException("Failed to serialize object of type: " + object.getClass(), ex); + } + return baos.toByteArray(); + } + + /** + * Deserialize the byte array into an object. + * @param bytes a serialized object + * @return the result of deserializing the bytes + */ + @Nullable + public static Object deserialize(@Nullable byte[] bytes) { + if (bytes == null) { + return null; + } + try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) { + return ois.readObject(); + } + catch (IOException ex) { + throw new IllegalArgumentException("Failed to deserialize object", ex); + } + catch (ClassNotFoundException ex) { + throw new IllegalStateException("Failed to deserialize object type", ex); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/SimpleIdGenerator.java b/spring-core/src/main/java/org/springframework/util/SimpleIdGenerator.java new file mode 100644 index 0000000..e4c89f4 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/SimpleIdGenerator.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; + +/** + * A simple {@link IdGenerator} that starts at 1, increments up to + * {@link Long#MAX_VALUE}, and then rolls over. + * + * @author Rossen Stoyanchev + * @since 4.1.5 + */ +public class SimpleIdGenerator implements IdGenerator { + + private final AtomicLong leastSigBits = new AtomicLong(); + + + @Override + public UUID generateId() { + return new UUID(0, this.leastSigBits.incrementAndGet()); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/SimpleRouteMatcher.java b/spring-core/src/main/java/org/springframework/util/SimpleRouteMatcher.java new file mode 100644 index 0000000..ead414e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/SimpleRouteMatcher.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.util.Comparator; +import java.util.Map; + +import org.springframework.lang.Nullable; + +/** + * {@code RouteMatcher} that delegates to a {@link PathMatcher}. + * + *

    Note: This implementation is not efficient since + * {@code PathMatcher} treats paths and patterns as Strings. For more optimized + * performance use the {@code PathPatternRouteMatcher} from {@code spring-web} + * which enables use of parsed routes and patterns. + * + * @author Rossen Stoyanchev + * @since 5.2 + */ +public class SimpleRouteMatcher implements RouteMatcher { + + private final PathMatcher pathMatcher; + + + /** + * Create a new {@code SimpleRouteMatcher} for the given + * {@link PathMatcher} delegate. + */ + public SimpleRouteMatcher(PathMatcher pathMatcher) { + Assert.notNull(pathMatcher, "PathMatcher is required"); + this.pathMatcher = pathMatcher; + } + + /** + * Return the underlying {@link PathMatcher} delegate. + */ + public PathMatcher getPathMatcher() { + return this.pathMatcher; + } + + + @Override + public Route parseRoute(String route) { + return new DefaultRoute(route); + } + + @Override + public boolean isPattern(String route) { + return this.pathMatcher.isPattern(route); + } + + @Override + public String combine(String pattern1, String pattern2) { + return this.pathMatcher.combine(pattern1, pattern2); + } + + @Override + public boolean match(String pattern, Route route) { + return this.pathMatcher.match(pattern, route.value()); + } + + @Override + @Nullable + public Map matchAndExtract(String pattern, Route route) { + if (!match(pattern, route)) { + return null; + } + return this.pathMatcher.extractUriTemplateVariables(pattern, route.value()); + } + + @Override + public Comparator getPatternComparator(Route route) { + return this.pathMatcher.getPatternComparator(route.value()); + } + + + private static class DefaultRoute implements Route { + + private final String path; + + DefaultRoute(String path) { + this.path = path; + } + + @Override + public String value() { + return this.path; + } + + @Override + public String toString() { + return value(); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/SocketUtils.java b/spring-core/src/main/java/org/springframework/util/SocketUtils.java new file mode 100644 index 0000000..557173e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/SocketUtils.java @@ -0,0 +1,307 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.util.Random; +import java.util.SortedSet; +import java.util.TreeSet; + +import javax.net.ServerSocketFactory; + +/** + * Simple utility methods for working with network sockets — for example, + * for finding available ports on {@code localhost}. + * + *

    Within this class, a TCP port refers to a port for a {@link ServerSocket}; + * whereas, a UDP port refers to a port for a {@link DatagramSocket}. + * + * @author Sam Brannen + * @author Ben Hale + * @author Arjen Poutsma + * @author Gunnar Hillert + * @author Gary Russell + * @since 4.0 + */ +public class SocketUtils { + + /** + * The default minimum value for port ranges used when finding an available + * socket port. + */ + public static final int PORT_RANGE_MIN = 1024; + + /** + * The default maximum value for port ranges used when finding an available + * socket port. + */ + public static final int PORT_RANGE_MAX = 65535; + + + private static final Random random = new Random(System.nanoTime()); + + + /** + * Although {@code SocketUtils} consists solely of static utility methods, + * this constructor is intentionally {@code public}. + *

    Rationale

    + *

    Static methods from this class may be invoked from within XML + * configuration files using the Spring Expression Language (SpEL) and the + * following syntax. + *

    <bean id="bean1" ... p:port="#{T(org.springframework.util.SocketUtils).findAvailableTcpPort(12000)}" />
    + * If this constructor were {@code private}, you would be required to supply + * the fully qualified class name to SpEL's {@code T()} function for each usage. + * Thus, the fact that this constructor is {@code public} allows you to reduce + * boilerplate configuration with SpEL as can be seen in the following example. + *
    <bean id="socketUtils" class="org.springframework.util.SocketUtils" />
    +	 * <bean id="bean1" ... p:port="#{socketUtils.findAvailableTcpPort(12000)}" />
    +	 * <bean id="bean2" ... p:port="#{socketUtils.findAvailableTcpPort(30000)}" />
    + */ + public SocketUtils() { + } + + + /** + * Find an available TCP port randomly selected from the range + * [{@value #PORT_RANGE_MIN}, {@value #PORT_RANGE_MAX}]. + * @return an available TCP port number + * @throws IllegalStateException if no available port could be found + */ + public static int findAvailableTcpPort() { + return findAvailableTcpPort(PORT_RANGE_MIN); + } + + /** + * Find an available TCP port randomly selected from the range + * [{@code minPort}, {@value #PORT_RANGE_MAX}]. + * @param minPort the minimum port number + * @return an available TCP port number + * @throws IllegalStateException if no available port could be found + */ + public static int findAvailableTcpPort(int minPort) { + return findAvailableTcpPort(minPort, PORT_RANGE_MAX); + } + + /** + * Find an available TCP port randomly selected from the range + * [{@code minPort}, {@code maxPort}]. + * @param minPort the minimum port number + * @param maxPort the maximum port number + * @return an available TCP port number + * @throws IllegalStateException if no available port could be found + */ + public static int findAvailableTcpPort(int minPort, int maxPort) { + return SocketType.TCP.findAvailablePort(minPort, maxPort); + } + + /** + * Find the requested number of available TCP ports, each randomly selected + * from the range [{@value #PORT_RANGE_MIN}, {@value #PORT_RANGE_MAX}]. + * @param numRequested the number of available ports to find + * @return a sorted set of available TCP port numbers + * @throws IllegalStateException if the requested number of available ports could not be found + */ + public static SortedSet findAvailableTcpPorts(int numRequested) { + return findAvailableTcpPorts(numRequested, PORT_RANGE_MIN, PORT_RANGE_MAX); + } + + /** + * Find the requested number of available TCP ports, each randomly selected + * from the range [{@code minPort}, {@code maxPort}]. + * @param numRequested the number of available ports to find + * @param minPort the minimum port number + * @param maxPort the maximum port number + * @return a sorted set of available TCP port numbers + * @throws IllegalStateException if the requested number of available ports could not be found + */ + public static SortedSet findAvailableTcpPorts(int numRequested, int minPort, int maxPort) { + return SocketType.TCP.findAvailablePorts(numRequested, minPort, maxPort); + } + + /** + * Find an available UDP port randomly selected from the range + * [{@value #PORT_RANGE_MIN}, {@value #PORT_RANGE_MAX}]. + * @return an available UDP port number + * @throws IllegalStateException if no available port could be found + */ + public static int findAvailableUdpPort() { + return findAvailableUdpPort(PORT_RANGE_MIN); + } + + /** + * Find an available UDP port randomly selected from the range + * [{@code minPort}, {@value #PORT_RANGE_MAX}]. + * @param minPort the minimum port number + * @return an available UDP port number + * @throws IllegalStateException if no available port could be found + */ + public static int findAvailableUdpPort(int minPort) { + return findAvailableUdpPort(minPort, PORT_RANGE_MAX); + } + + /** + * Find an available UDP port randomly selected from the range + * [{@code minPort}, {@code maxPort}]. + * @param minPort the minimum port number + * @param maxPort the maximum port number + * @return an available UDP port number + * @throws IllegalStateException if no available port could be found + */ + public static int findAvailableUdpPort(int minPort, int maxPort) { + return SocketType.UDP.findAvailablePort(minPort, maxPort); + } + + /** + * Find the requested number of available UDP ports, each randomly selected + * from the range [{@value #PORT_RANGE_MIN}, {@value #PORT_RANGE_MAX}]. + * @param numRequested the number of available ports to find + * @return a sorted set of available UDP port numbers + * @throws IllegalStateException if the requested number of available ports could not be found + */ + public static SortedSet findAvailableUdpPorts(int numRequested) { + return findAvailableUdpPorts(numRequested, PORT_RANGE_MIN, PORT_RANGE_MAX); + } + + /** + * Find the requested number of available UDP ports, each randomly selected + * from the range [{@code minPort}, {@code maxPort}]. + * @param numRequested the number of available ports to find + * @param minPort the minimum port number + * @param maxPort the maximum port number + * @return a sorted set of available UDP port numbers + * @throws IllegalStateException if the requested number of available ports could not be found + */ + public static SortedSet findAvailableUdpPorts(int numRequested, int minPort, int maxPort) { + return SocketType.UDP.findAvailablePorts(numRequested, minPort, maxPort); + } + + + private enum SocketType { + + TCP { + @Override + protected boolean isPortAvailable(int port) { + try { + ServerSocket serverSocket = ServerSocketFactory.getDefault().createServerSocket( + port, 1, InetAddress.getByName("localhost")); + serverSocket.close(); + return true; + } + catch (Exception ex) { + return false; + } + } + }, + + UDP { + @Override + protected boolean isPortAvailable(int port) { + try { + DatagramSocket socket = new DatagramSocket(port, InetAddress.getByName("localhost")); + socket.close(); + return true; + } + catch (Exception ex) { + return false; + } + } + }; + + /** + * Determine if the specified port for this {@code SocketType} is + * currently available on {@code localhost}. + */ + protected abstract boolean isPortAvailable(int port); + + /** + * Find a pseudo-random port number within the range + * [{@code minPort}, {@code maxPort}]. + * @param minPort the minimum port number + * @param maxPort the maximum port number + * @return a random port number within the specified range + */ + private int findRandomPort(int minPort, int maxPort) { + int portRange = maxPort - minPort; + return minPort + random.nextInt(portRange + 1); + } + + /** + * Find an available port for this {@code SocketType}, randomly selected + * from the range [{@code minPort}, {@code maxPort}]. + * @param minPort the minimum port number + * @param maxPort the maximum port number + * @return an available port number for this socket type + * @throws IllegalStateException if no available port could be found + */ + int findAvailablePort(int minPort, int maxPort) { + Assert.isTrue(minPort > 0, "'minPort' must be greater than 0"); + Assert.isTrue(maxPort >= minPort, "'maxPort' must be greater than or equal to 'minPort'"); + Assert.isTrue(maxPort <= PORT_RANGE_MAX, "'maxPort' must be less than or equal to " + PORT_RANGE_MAX); + + int portRange = maxPort - minPort; + int candidatePort; + int searchCounter = 0; + do { + if (searchCounter > portRange) { + throw new IllegalStateException(String.format( + "Could not find an available %s port in the range [%d, %d] after %d attempts", + name(), minPort, maxPort, searchCounter)); + } + candidatePort = findRandomPort(minPort, maxPort); + searchCounter++; + } + while (!isPortAvailable(candidatePort)); + + return candidatePort; + } + + /** + * Find the requested number of available ports for this {@code SocketType}, + * each randomly selected from the range [{@code minPort}, {@code maxPort}]. + * @param numRequested the number of available ports to find + * @param minPort the minimum port number + * @param maxPort the maximum port number + * @return a sorted set of available port numbers for this socket type + * @throws IllegalStateException if the requested number of available ports could not be found + */ + SortedSet findAvailablePorts(int numRequested, int minPort, int maxPort) { + Assert.isTrue(minPort > 0, "'minPort' must be greater than 0"); + Assert.isTrue(maxPort > minPort, "'maxPort' must be greater than 'minPort'"); + Assert.isTrue(maxPort <= PORT_RANGE_MAX, "'maxPort' must be less than or equal to " + PORT_RANGE_MAX); + Assert.isTrue(numRequested > 0, "'numRequested' must be greater than 0"); + Assert.isTrue((maxPort - minPort) >= numRequested, + "'numRequested' must not be greater than 'maxPort' - 'minPort'"); + + SortedSet availablePorts = new TreeSet<>(); + int attemptCount = 0; + while ((++attemptCount <= numRequested + 100) && availablePorts.size() < numRequested) { + availablePorts.add(findAvailablePort(minPort, maxPort)); + } + + if (availablePorts.size() != numRequested) { + throw new IllegalStateException(String.format( + "Could not find %d available %s ports in the range [%d, %d]", + numRequested, name(), minPort, maxPort)); + } + + return availablePorts; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/StopWatch.java b/spring-core/src/main/java/org/springframework/util/StopWatch.java new file mode 100644 index 0000000..8076ed0 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/StopWatch.java @@ -0,0 +1,393 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.springframework.lang.Nullable; + +/** + * Simple stop watch, allowing for timing of a number of tasks, exposing total + * running time and running time for each named task. + * + *

    Conceals use of {@link System#nanoTime()}, improving the readability of + * application code and reducing the likelihood of calculation errors. + * + *

    Note that this object is not designed to be thread-safe and does not use + * synchronization. + * + *

    This class is normally used to verify performance during proof-of-concept + * work and in development, rather than as part of production applications. + * + *

    As of Spring Framework 5.2, running time is tracked and reported in + * nanoseconds. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Sam Brannen + * @since May 2, 2001 + */ +public class StopWatch { + + /** + * Identifier of this {@code StopWatch}. + *

    Handy when we have output from multiple stop watches and need to + * distinguish between them in log or console output. + */ + private final String id; + + private boolean keepTaskList = true; + + private final List taskList = new ArrayList<>(1); + + /** Start time of the current task. */ + private long startTimeNanos; + + /** Name of the current task. */ + @Nullable + private String currentTaskName; + + @Nullable + private TaskInfo lastTaskInfo; + + private int taskCount; + + /** Total running time. */ + private long totalTimeNanos; + + + /** + * Construct a new {@code StopWatch}. + *

    Does not start any task. + */ + public StopWatch() { + this(""); + } + + /** + * Construct a new {@code StopWatch} with the given ID. + *

    The ID is handy when we have output from multiple stop watches and need + * to distinguish between them. + *

    Does not start any task. + * @param id identifier for this stop watch + */ + public StopWatch(String id) { + this.id = id; + } + + + /** + * Get the ID of this {@code StopWatch}, as specified on construction. + * @return the ID (empty String by default) + * @since 4.2.2 + * @see #StopWatch(String) + */ + public String getId() { + return this.id; + } + + /** + * Configure whether the {@link TaskInfo} array is built over time. + *

    Set this to {@code false} when using a {@code StopWatch} for millions + * of intervals; otherwise, the {@code TaskInfo} structure will consume + * excessive memory. + *

    Default is {@code true}. + */ + public void setKeepTaskList(boolean keepTaskList) { + this.keepTaskList = keepTaskList; + } + + + /** + * Start an unnamed task. + *

    The results are undefined if {@link #stop()} or timing methods are + * called without invoking this method first. + * @see #start(String) + * @see #stop() + */ + public void start() throws IllegalStateException { + start(""); + } + + /** + * Start a named task. + *

    The results are undefined if {@link #stop()} or timing methods are + * called without invoking this method first. + * @param taskName the name of the task to start + * @see #start() + * @see #stop() + */ + public void start(String taskName) throws IllegalStateException { + if (this.currentTaskName != null) { + throw new IllegalStateException("Can't start StopWatch: it's already running"); + } + this.currentTaskName = taskName; + this.startTimeNanos = System.nanoTime(); + } + + /** + * Stop the current task. + *

    The results are undefined if timing methods are called without invoking + * at least one pair of {@code start()} / {@code stop()} methods. + * @see #start() + * @see #start(String) + */ + public void stop() throws IllegalStateException { + if (this.currentTaskName == null) { + throw new IllegalStateException("Can't stop StopWatch: it's not running"); + } + long lastTime = System.nanoTime() - this.startTimeNanos; + this.totalTimeNanos += lastTime; + this.lastTaskInfo = new TaskInfo(this.currentTaskName, lastTime); + if (this.keepTaskList) { + this.taskList.add(this.lastTaskInfo); + } + ++this.taskCount; + this.currentTaskName = null; + } + + /** + * Determine whether this {@code StopWatch} is currently running. + * @see #currentTaskName() + */ + public boolean isRunning() { + return (this.currentTaskName != null); + } + + /** + * Get the name of the currently running task, if any. + * @since 4.2.2 + * @see #isRunning() + */ + @Nullable + public String currentTaskName() { + return this.currentTaskName; + } + + /** + * Get the time taken by the last task in nanoseconds. + * @since 5.2 + * @see #getLastTaskTimeMillis() + */ + public long getLastTaskTimeNanos() throws IllegalStateException { + if (this.lastTaskInfo == null) { + throw new IllegalStateException("No tasks run: can't get last task interval"); + } + return this.lastTaskInfo.getTimeNanos(); + } + + /** + * Get the time taken by the last task in milliseconds. + * @see #getLastTaskTimeNanos() + */ + public long getLastTaskTimeMillis() throws IllegalStateException { + if (this.lastTaskInfo == null) { + throw new IllegalStateException("No tasks run: can't get last task interval"); + } + return this.lastTaskInfo.getTimeMillis(); + } + + /** + * Get the name of the last task. + */ + public String getLastTaskName() throws IllegalStateException { + if (this.lastTaskInfo == null) { + throw new IllegalStateException("No tasks run: can't get last task name"); + } + return this.lastTaskInfo.getTaskName(); + } + + /** + * Get the last task as a {@link TaskInfo} object. + */ + public TaskInfo getLastTaskInfo() throws IllegalStateException { + if (this.lastTaskInfo == null) { + throw new IllegalStateException("No tasks run: can't get last task info"); + } + return this.lastTaskInfo; + } + + + /** + * Get the total time in nanoseconds for all tasks. + * @since 5.2 + * @see #getTotalTimeMillis() + * @see #getTotalTimeSeconds() + */ + public long getTotalTimeNanos() { + return this.totalTimeNanos; + } + + /** + * Get the total time in milliseconds for all tasks. + * @see #getTotalTimeNanos() + * @see #getTotalTimeSeconds() + */ + public long getTotalTimeMillis() { + return nanosToMillis(this.totalTimeNanos); + } + + /** + * Get the total time in seconds for all tasks. + * @see #getTotalTimeNanos() + * @see #getTotalTimeMillis() + */ + public double getTotalTimeSeconds() { + return nanosToSeconds(this.totalTimeNanos); + } + + /** + * Get the number of tasks timed. + */ + public int getTaskCount() { + return this.taskCount; + } + + /** + * Get an array of the data for tasks performed. + */ + public TaskInfo[] getTaskInfo() { + if (!this.keepTaskList) { + throw new UnsupportedOperationException("Task info is not being kept!"); + } + return this.taskList.toArray(new TaskInfo[0]); + } + + + /** + * Get a short description of the total running time. + */ + public String shortSummary() { + return "StopWatch '" + getId() + "': running time = " + getTotalTimeNanos() + " ns"; + } + + /** + * Generate a string with a table describing all tasks performed. + *

    For custom reporting, call {@link #getTaskInfo()} and use the task info + * directly. + */ + public String prettyPrint() { + StringBuilder sb = new StringBuilder(shortSummary()); + sb.append('\n'); + if (!this.keepTaskList) { + sb.append("No task info kept"); + } + else { + sb.append("---------------------------------------------\n"); + sb.append("ns % Task name\n"); + sb.append("---------------------------------------------\n"); + NumberFormat nf = NumberFormat.getNumberInstance(); + nf.setMinimumIntegerDigits(9); + nf.setGroupingUsed(false); + NumberFormat pf = NumberFormat.getPercentInstance(); + pf.setMinimumIntegerDigits(3); + pf.setGroupingUsed(false); + for (TaskInfo task : getTaskInfo()) { + sb.append(nf.format(task.getTimeNanos())).append(" "); + sb.append(pf.format((double) task.getTimeNanos() / getTotalTimeNanos())).append(" "); + sb.append(task.getTaskName()).append("\n"); + } + } + return sb.toString(); + } + + /** + * Generate an informative string describing all tasks performed + *

    For custom reporting, call {@link #getTaskInfo()} and use the task info + * directly. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(shortSummary()); + if (this.keepTaskList) { + for (TaskInfo task : getTaskInfo()) { + sb.append("; [").append(task.getTaskName()).append("] took ").append(task.getTimeNanos()).append(" ns"); + long percent = Math.round(100.0 * task.getTimeNanos() / getTotalTimeNanos()); + sb.append(" = ").append(percent).append("%"); + } + } + else { + sb.append("; no task info kept"); + } + return sb.toString(); + } + + + private static long nanosToMillis(long duration) { + return TimeUnit.NANOSECONDS.toMillis(duration); + } + + private static double nanosToSeconds(long duration) { + return duration / 1_000_000_000.0; + } + + + /** + * Nested class to hold data about one task executed within the {@code StopWatch}. + */ + public static final class TaskInfo { + + private final String taskName; + + private final long timeNanos; + + TaskInfo(String taskName, long timeNanos) { + this.taskName = taskName; + this.timeNanos = timeNanos; + } + + /** + * Get the name of this task. + */ + public String getTaskName() { + return this.taskName; + } + + /** + * Get the time in nanoseconds this task took. + * @since 5.2 + * @see #getTimeMillis() + * @see #getTimeSeconds() + */ + public long getTimeNanos() { + return this.timeNanos; + } + + /** + * Get the time in milliseconds this task took. + * @see #getTimeNanos() + * @see #getTimeSeconds() + */ + public long getTimeMillis() { + return nanosToMillis(this.timeNanos); + } + + /** + * Get the time in seconds this task took. + * @see #getTimeMillis() + * @see #getTimeNanos() + */ + public double getTimeSeconds() { + return nanosToSeconds(this.timeNanos); + } + + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/StreamUtils.java b/spring-core/src/main/java/org/springframework/util/StreamUtils.java new file mode 100644 index 0000000..272abd9 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/StreamUtils.java @@ -0,0 +1,295 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.nio.charset.Charset; + +import org.springframework.lang.Nullable; + +/** + * Simple utility methods for dealing with streams. The copy methods of this class are + * similar to those defined in {@link FileCopyUtils} except that all affected streams are + * left open when done. All copy methods use a block size of 4096 bytes. + * + *

    Mainly for use within the framework, but also useful for application code. + * + * @author Juergen Hoeller + * @author Phillip Webb + * @author Brian Clozel + * @since 3.2.2 + * @see FileCopyUtils + */ +public abstract class StreamUtils { + + /** + * The default buffer size used when copying bytes. + */ + public static final int BUFFER_SIZE = 4096; + + private static final byte[] EMPTY_CONTENT = new byte[0]; + + + /** + * Copy the contents of the given InputStream into a new byte array. + *

    Leaves the stream open when done. + * @param in the stream to copy from (may be {@code null} or empty) + * @return the new byte array that has been copied to (possibly empty) + * @throws IOException in case of I/O errors + */ + public static byte[] copyToByteArray(@Nullable InputStream in) throws IOException { + if (in == null) { + return new byte[0]; + } + + ByteArrayOutputStream out = new ByteArrayOutputStream(BUFFER_SIZE); + copy(in, out); + return out.toByteArray(); + } + + /** + * Copy the contents of the given InputStream into a String. + *

    Leaves the stream open when done. + * @param in the InputStream to copy from (may be {@code null} or empty) + * @param charset the {@link Charset} to use to decode the bytes + * @return the String that has been copied to (possibly empty) + * @throws IOException in case of I/O errors + */ + public static String copyToString(@Nullable InputStream in, Charset charset) throws IOException { + if (in == null) { + return ""; + } + + StringBuilder out = new StringBuilder(BUFFER_SIZE); + InputStreamReader reader = new InputStreamReader(in, charset); + char[] buffer = new char[BUFFER_SIZE]; + int charsRead; + while ((charsRead = reader.read(buffer)) != -1) { + out.append(buffer, 0, charsRead); + } + return out.toString(); + } + + /** + * Copy the contents of the given {@link ByteArrayOutputStream} into a {@link String}. + *

    This is a more effective equivalent of {@code new String(baos.toByteArray(), charset)}. + * @param baos the {@code ByteArrayOutputStream} to be copied into a String + * @param charset the {@link Charset} to use to decode the bytes + * @return the String that has been copied to (possibly empty) + * @since 5.2.6 + */ + public static String copyToString(ByteArrayOutputStream baos, Charset charset) { + Assert.notNull(baos, "No ByteArrayOutputStream specified"); + Assert.notNull(charset, "No Charset specified"); + try { + // Can be replaced with toString(Charset) call in Java 10+ + return baos.toString(charset.name()); + } + catch (UnsupportedEncodingException ex) { + // Should never happen + throw new IllegalArgumentException("Invalid charset name: " + charset, ex); + } + } + + /** + * Copy the contents of the given byte array to the given OutputStream. + *

    Leaves the stream open when done. + * @param in the byte array to copy from + * @param out the OutputStream to copy to + * @throws IOException in case of I/O errors + */ + public static void copy(byte[] in, OutputStream out) throws IOException { + Assert.notNull(in, "No input byte array specified"); + Assert.notNull(out, "No OutputStream specified"); + + out.write(in); + out.flush(); + } + + /** + * Copy the contents of the given String to the given OutputStream. + *

    Leaves the stream open when done. + * @param in the String to copy from + * @param charset the Charset + * @param out the OutputStream to copy to + * @throws IOException in case of I/O errors + */ + public static void copy(String in, Charset charset, OutputStream out) throws IOException { + Assert.notNull(in, "No input String specified"); + Assert.notNull(charset, "No Charset specified"); + Assert.notNull(out, "No OutputStream specified"); + + Writer writer = new OutputStreamWriter(out, charset); + writer.write(in); + writer.flush(); + } + + /** + * Copy the contents of the given InputStream to the given OutputStream. + *

    Leaves both streams open when done. + * @param in the InputStream to copy from + * @param out the OutputStream to copy to + * @return the number of bytes copied + * @throws IOException in case of I/O errors + */ + public static int copy(InputStream in, OutputStream out) throws IOException { + Assert.notNull(in, "No InputStream specified"); + Assert.notNull(out, "No OutputStream specified"); + + int byteCount = 0; + byte[] buffer = new byte[BUFFER_SIZE]; + int bytesRead; + while ((bytesRead = in.read(buffer)) != -1) { + out.write(buffer, 0, bytesRead); + byteCount += bytesRead; + } + out.flush(); + return byteCount; + } + + /** + * Copy a range of content of the given InputStream to the given OutputStream. + *

    If the specified range exceeds the length of the InputStream, this copies + * up to the end of the stream and returns the actual number of copied bytes. + *

    Leaves both streams open when done. + * @param in the InputStream to copy from + * @param out the OutputStream to copy to + * @param start the position to start copying from + * @param end the position to end copying + * @return the number of bytes copied + * @throws IOException in case of I/O errors + * @since 4.3 + */ + public static long copyRange(InputStream in, OutputStream out, long start, long end) throws IOException { + Assert.notNull(in, "No InputStream specified"); + Assert.notNull(out, "No OutputStream specified"); + + long skipped = in.skip(start); + if (skipped < start) { + throw new IOException("Skipped only " + skipped + " bytes out of " + start + " required"); + } + + long bytesToCopy = end - start + 1; + byte[] buffer = new byte[(int) Math.min(StreamUtils.BUFFER_SIZE, bytesToCopy)]; + while (bytesToCopy > 0) { + int bytesRead = in.read(buffer); + if (bytesRead == -1) { + break; + } + else if (bytesRead <= bytesToCopy) { + out.write(buffer, 0, bytesRead); + bytesToCopy -= bytesRead; + } + else { + out.write(buffer, 0, (int) bytesToCopy); + bytesToCopy = 0; + } + } + return (end - start + 1 - bytesToCopy); + } + + /** + * Drain the remaining content of the given InputStream. + *

    Leaves the InputStream open when done. + * @param in the InputStream to drain + * @return the number of bytes read + * @throws IOException in case of I/O errors + * @since 4.3 + */ + public static int drain(InputStream in) throws IOException { + Assert.notNull(in, "No InputStream specified"); + byte[] buffer = new byte[BUFFER_SIZE]; + int bytesRead = -1; + int byteCount = 0; + while ((bytesRead = in.read(buffer)) != -1) { + byteCount += bytesRead; + } + return byteCount; + } + + /** + * Return an efficient empty {@link InputStream}. + * @return a {@link ByteArrayInputStream} based on an empty byte array + * @since 4.2.2 + */ + public static InputStream emptyInput() { + return new ByteArrayInputStream(EMPTY_CONTENT); + } + + /** + * Return a variant of the given {@link InputStream} where calling + * {@link InputStream#close() close()} has no effect. + * @param in the InputStream to decorate + * @return a version of the InputStream that ignores calls to close + */ + public static InputStream nonClosing(InputStream in) { + Assert.notNull(in, "No InputStream specified"); + return new NonClosingInputStream(in); + } + + /** + * Return a variant of the given {@link OutputStream} where calling + * {@link OutputStream#close() close()} has no effect. + * @param out the OutputStream to decorate + * @return a version of the OutputStream that ignores calls to close + */ + public static OutputStream nonClosing(OutputStream out) { + Assert.notNull(out, "No OutputStream specified"); + return new NonClosingOutputStream(out); + } + + + private static class NonClosingInputStream extends FilterInputStream { + + public NonClosingInputStream(InputStream in) { + super(in); + } + + @Override + public void close() throws IOException { + } + } + + + private static class NonClosingOutputStream extends FilterOutputStream { + + public NonClosingOutputStream(OutputStream out) { + super(out); + } + + @Override + public void write(byte[] b, int off, int let) throws IOException { + // It is critical that we override this method for performance + this.out.write(b, off, let); + } + + @Override + public void close() throws IOException { + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/StringUtils.java b/spring-core/src/main/java/org/springframework/util/StringUtils.java new file mode 100644 index 0000000..6d6d577 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/StringUtils.java @@ -0,0 +1,1362 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.Charset; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Properties; +import java.util.Set; +import java.util.StringJoiner; +import java.util.StringTokenizer; +import java.util.TimeZone; + +import org.springframework.lang.Nullable; + +/** + * Miscellaneous {@link String} utility methods. + * + *

    Mainly for internal use within the framework; consider + * Apache's Commons Lang + * for a more comprehensive suite of {@code String} utilities. + * + *

    This class delivers some simple functionality that should really be + * provided by the core Java {@link String} and {@link StringBuilder} + * classes. It also provides easy-to-use methods to convert between + * delimited strings, such as CSV strings, and collections and arrays. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Keith Donald + * @author Rob Harrop + * @author Rick Evans + * @author Arjen Poutsma + * @author Sam Brannen + * @author Brian Clozel + * @since 16 April 2001 + */ +public abstract class StringUtils { + + private static final String[] EMPTY_STRING_ARRAY = {}; + + private static final String FOLDER_SEPARATOR = "/"; + + private static final String WINDOWS_FOLDER_SEPARATOR = "\\"; + + private static final String TOP_PATH = ".."; + + private static final String CURRENT_PATH = "."; + + private static final char EXTENSION_SEPARATOR = '.'; + + + //--------------------------------------------------------------------- + // General convenience methods for working with Strings + //--------------------------------------------------------------------- + + /** + * Check whether the given object (possibly a {@code String}) is empty. + * This is effectively a shortcut for {@code !hasLength(String)}. + *

    This method accepts any Object as an argument, comparing it to + * {@code null} and the empty String. As a consequence, this method + * will never return {@code true} for a non-null non-String object. + *

    The Object signature is useful for general attribute handling code + * that commonly deals with Strings but generally has to iterate over + * Objects since attributes may e.g. be primitive value objects as well. + *

    Note: If the object is typed to {@code String} upfront, prefer + * {@link #hasLength(String)} or {@link #hasText(String)} instead. + * @param str the candidate object (possibly a {@code String}) + * @since 3.2.1 + * @deprecated as of 5.3, in favor of {@link #hasLength(String)} and + * {@link #hasText(String)} (or {@link ObjectUtils#isEmpty(Object)}) + */ + @Deprecated + public static boolean isEmpty(@Nullable Object str) { + return (str == null || "".equals(str)); + } + + /** + * Check that the given {@code CharSequence} is neither {@code null} nor + * of length 0. + *

    Note: this method returns {@code true} for a {@code CharSequence} + * that purely consists of whitespace. + *

    +	 * StringUtils.hasLength(null) = false
    +	 * StringUtils.hasLength("") = false
    +	 * StringUtils.hasLength(" ") = true
    +	 * StringUtils.hasLength("Hello") = true
    +	 * 
    + * @param str the {@code CharSequence} to check (may be {@code null}) + * @return {@code true} if the {@code CharSequence} is not {@code null} and has length + * @see #hasLength(String) + * @see #hasText(CharSequence) + */ + public static boolean hasLength(@Nullable CharSequence str) { + return (str != null && str.length() > 0); + } + + /** + * Check that the given {@code String} is neither {@code null} nor of length 0. + *

    Note: this method returns {@code true} for a {@code String} that + * purely consists of whitespace. + * @param str the {@code String} to check (may be {@code null}) + * @return {@code true} if the {@code String} is not {@code null} and has length + * @see #hasLength(CharSequence) + * @see #hasText(String) + */ + public static boolean hasLength(@Nullable String str) { + return (str != null && !str.isEmpty()); + } + + /** + * Check whether the given {@code CharSequence} contains actual text. + *

    More specifically, this method returns {@code true} if the + * {@code CharSequence} is not {@code null}, its length is greater than + * 0, and it contains at least one non-whitespace character. + *

    +	 * StringUtils.hasText(null) = false
    +	 * StringUtils.hasText("") = false
    +	 * StringUtils.hasText(" ") = false
    +	 * StringUtils.hasText("12345") = true
    +	 * StringUtils.hasText(" 12345 ") = true
    +	 * 
    + * @param str the {@code CharSequence} to check (may be {@code null}) + * @return {@code true} if the {@code CharSequence} is not {@code null}, + * its length is greater than 0, and it does not contain whitespace only + * @see #hasText(String) + * @see #hasLength(CharSequence) + * @see Character#isWhitespace + */ + public static boolean hasText(@Nullable CharSequence str) { + return (str != null && str.length() > 0 && containsText(str)); + } + + /** + * Check whether the given {@code String} contains actual text. + *

    More specifically, this method returns {@code true} if the + * {@code String} is not {@code null}, its length is greater than 0, + * and it contains at least one non-whitespace character. + * @param str the {@code String} to check (may be {@code null}) + * @return {@code true} if the {@code String} is not {@code null}, its + * length is greater than 0, and it does not contain whitespace only + * @see #hasText(CharSequence) + * @see #hasLength(String) + * @see Character#isWhitespace + */ + public static boolean hasText(@Nullable String str) { + return (str != null && !str.isEmpty() && containsText(str)); + } + + private static boolean containsText(CharSequence str) { + int strLen = str.length(); + for (int i = 0; i < strLen; i++) { + if (!Character.isWhitespace(str.charAt(i))) { + return true; + } + } + return false; + } + + /** + * Check whether the given {@code CharSequence} contains any whitespace characters. + * @param str the {@code CharSequence} to check (may be {@code null}) + * @return {@code true} if the {@code CharSequence} is not empty and + * contains at least 1 whitespace character + * @see Character#isWhitespace + */ + public static boolean containsWhitespace(@Nullable CharSequence str) { + if (!hasLength(str)) { + return false; + } + + int strLen = str.length(); + for (int i = 0; i < strLen; i++) { + if (Character.isWhitespace(str.charAt(i))) { + return true; + } + } + return false; + } + + /** + * Check whether the given {@code String} contains any whitespace characters. + * @param str the {@code String} to check (may be {@code null}) + * @return {@code true} if the {@code String} is not empty and + * contains at least 1 whitespace character + * @see #containsWhitespace(CharSequence) + */ + public static boolean containsWhitespace(@Nullable String str) { + return containsWhitespace((CharSequence) str); + } + + /** + * Trim leading and trailing whitespace from the given {@code String}. + * @param str the {@code String} to check + * @return the trimmed {@code String} + * @see java.lang.Character#isWhitespace + */ + public static String trimWhitespace(String str) { + if (!hasLength(str)) { + return str; + } + + int beginIndex = 0; + int endIndex = str.length() - 1; + + while (beginIndex <= endIndex && Character.isWhitespace(str.charAt(beginIndex))) { + beginIndex++; + } + + while (endIndex > beginIndex && Character.isWhitespace(str.charAt(endIndex))) { + endIndex--; + } + + return str.substring(beginIndex, endIndex + 1); + } + + /** + * Trim all whitespace from the given {@code String}: + * leading, trailing, and in between characters. + * @param str the {@code String} to check + * @return the trimmed {@code String} + * @see java.lang.Character#isWhitespace + */ + public static String trimAllWhitespace(String str) { + if (!hasLength(str)) { + return str; + } + + int len = str.length(); + StringBuilder sb = new StringBuilder(str.length()); + for (int i = 0; i < len; i++) { + char c = str.charAt(i); + if (!Character.isWhitespace(c)) { + sb.append(c); + } + } + return sb.toString(); + } + + /** + * Trim leading whitespace from the given {@code String}. + * @param str the {@code String} to check + * @return the trimmed {@code String} + * @see java.lang.Character#isWhitespace + */ + public static String trimLeadingWhitespace(String str) { + if (!hasLength(str)) { + return str; + } + + int beginIdx = 0; + while (beginIdx < str.length() && Character.isWhitespace(str.charAt(beginIdx))) { + beginIdx++; + } + return str.substring(beginIdx); + } + + /** + * Trim trailing whitespace from the given {@code String}. + * @param str the {@code String} to check + * @return the trimmed {@code String} + * @see java.lang.Character#isWhitespace + */ + public static String trimTrailingWhitespace(String str) { + if (!hasLength(str)) { + return str; + } + + int endIdx = str.length() - 1; + while (endIdx >= 0 && Character.isWhitespace(str.charAt(endIdx))) { + endIdx--; + } + return str.substring(0, endIdx + 1); + } + + /** + * Trim all occurrences of the supplied leading character from the given {@code String}. + * @param str the {@code String} to check + * @param leadingCharacter the leading character to be trimmed + * @return the trimmed {@code String} + */ + public static String trimLeadingCharacter(String str, char leadingCharacter) { + if (!hasLength(str)) { + return str; + } + + int beginIdx = 0; + while (beginIdx < str.length() && leadingCharacter == str.charAt(beginIdx)) { + beginIdx++; + } + return str.substring(beginIdx); + } + + /** + * Trim all occurrences of the supplied trailing character from the given {@code String}. + * @param str the {@code String} to check + * @param trailingCharacter the trailing character to be trimmed + * @return the trimmed {@code String} + */ + public static String trimTrailingCharacter(String str, char trailingCharacter) { + if (!hasLength(str)) { + return str; + } + + int endIdx = str.length() - 1; + while (endIdx >= 0 && trailingCharacter == str.charAt(endIdx)) { + endIdx--; + } + return str.substring(0, endIdx + 1); + } + + /** + * Test if the given {@code String} matches the given single character. + * @param str the {@code String} to check + * @param singleCharacter the character to compare to + * @since 5.2.9 + */ + public static boolean matchesCharacter(@Nullable String str, char singleCharacter) { + return (str != null && str.length() == 1 && str.charAt(0) == singleCharacter); + } + + /** + * Test if the given {@code String} starts with the specified prefix, + * ignoring upper/lower case. + * @param str the {@code String} to check + * @param prefix the prefix to look for + * @see java.lang.String#startsWith + */ + public static boolean startsWithIgnoreCase(@Nullable String str, @Nullable String prefix) { + return (str != null && prefix != null && str.length() >= prefix.length() && + str.regionMatches(true, 0, prefix, 0, prefix.length())); + } + + /** + * Test if the given {@code String} ends with the specified suffix, + * ignoring upper/lower case. + * @param str the {@code String} to check + * @param suffix the suffix to look for + * @see java.lang.String#endsWith + */ + public static boolean endsWithIgnoreCase(@Nullable String str, @Nullable String suffix) { + return (str != null && suffix != null && str.length() >= suffix.length() && + str.regionMatches(true, str.length() - suffix.length(), suffix, 0, suffix.length())); + } + + /** + * Test whether the given string matches the given substring + * at the given index. + * @param str the original string (or StringBuilder) + * @param index the index in the original string to start matching against + * @param substring the substring to match at the given index + */ + public static boolean substringMatch(CharSequence str, int index, CharSequence substring) { + if (index + substring.length() > str.length()) { + return false; + } + for (int i = 0; i < substring.length(); i++) { + if (str.charAt(index + i) != substring.charAt(i)) { + return false; + } + } + return true; + } + + /** + * Count the occurrences of the substring {@code sub} in string {@code str}. + * @param str string to search in + * @param sub string to search for + */ + public static int countOccurrencesOf(String str, String sub) { + if (!hasLength(str) || !hasLength(sub)) { + return 0; + } + + int count = 0; + int pos = 0; + int idx; + while ((idx = str.indexOf(sub, pos)) != -1) { + ++count; + pos = idx + sub.length(); + } + return count; + } + + /** + * Replace all occurrences of a substring within a string with another string. + * @param inString {@code String} to examine + * @param oldPattern {@code String} to replace + * @param newPattern {@code String} to insert + * @return a {@code String} with the replacements + */ + public static String replace(String inString, String oldPattern, @Nullable String newPattern) { + if (!hasLength(inString) || !hasLength(oldPattern) || newPattern == null) { + return inString; + } + int index = inString.indexOf(oldPattern); + if (index == -1) { + // no occurrence -> can return input as-is + return inString; + } + + int capacity = inString.length(); + if (newPattern.length() > oldPattern.length()) { + capacity += 16; + } + StringBuilder sb = new StringBuilder(capacity); + + int pos = 0; // our position in the old string + int patLen = oldPattern.length(); + while (index >= 0) { + sb.append(inString, pos, index); + sb.append(newPattern); + pos = index + patLen; + index = inString.indexOf(oldPattern, pos); + } + + // append any characters to the right of a match + sb.append(inString, pos, inString.length()); + return sb.toString(); + } + + /** + * Delete all occurrences of the given substring. + * @param inString the original {@code String} + * @param pattern the pattern to delete all occurrences of + * @return the resulting {@code String} + */ + public static String delete(String inString, String pattern) { + return replace(inString, pattern, ""); + } + + /** + * Delete any character in a given {@code String}. + * @param inString the original {@code String} + * @param charsToDelete a set of characters to delete. + * E.g. "az\n" will delete 'a's, 'z's and new lines. + * @return the resulting {@code String} + */ + public static String deleteAny(String inString, @Nullable String charsToDelete) { + if (!hasLength(inString) || !hasLength(charsToDelete)) { + return inString; + } + + int lastCharIndex = 0; + char[] result = new char[inString.length()]; + for (int i = 0; i < inString.length(); i++) { + char c = inString.charAt(i); + if (charsToDelete.indexOf(c) == -1) { + result[lastCharIndex++] = c; + } + } + if (lastCharIndex == inString.length()) { + return inString; + } + return new String(result, 0, lastCharIndex); + } + + //--------------------------------------------------------------------- + // Convenience methods for working with formatted Strings + //--------------------------------------------------------------------- + + /** + * Quote the given {@code String} with single quotes. + * @param str the input {@code String} (e.g. "myString") + * @return the quoted {@code String} (e.g. "'myString'"), + * or {@code null} if the input was {@code null} + */ + @Nullable + public static String quote(@Nullable String str) { + return (str != null ? "'" + str + "'" : null); + } + + /** + * Turn the given Object into a {@code String} with single quotes + * if it is a {@code String}; keeping the Object as-is else. + * @param obj the input Object (e.g. "myString") + * @return the quoted {@code String} (e.g. "'myString'"), + * or the input object as-is if not a {@code String} + */ + @Nullable + public static Object quoteIfString(@Nullable Object obj) { + return (obj instanceof String ? quote((String) obj) : obj); + } + + /** + * Unqualify a string qualified by a '.' dot character. For example, + * "this.name.is.qualified", returns "qualified". + * @param qualifiedName the qualified name + */ + public static String unqualify(String qualifiedName) { + return unqualify(qualifiedName, '.'); + } + + /** + * Unqualify a string qualified by a separator character. For example, + * "this:name:is:qualified" returns "qualified" if using a ':' separator. + * @param qualifiedName the qualified name + * @param separator the separator + */ + public static String unqualify(String qualifiedName, char separator) { + return qualifiedName.substring(qualifiedName.lastIndexOf(separator) + 1); + } + + /** + * Capitalize a {@code String}, changing the first letter to + * upper case as per {@link Character#toUpperCase(char)}. + * No other letters are changed. + * @param str the {@code String} to capitalize + * @return the capitalized {@code String} + */ + public static String capitalize(String str) { + return changeFirstCharacterCase(str, true); + } + + /** + * Uncapitalize a {@code String}, changing the first letter to + * lower case as per {@link Character#toLowerCase(char)}. + * No other letters are changed. + * @param str the {@code String} to uncapitalize + * @return the uncapitalized {@code String} + */ + public static String uncapitalize(String str) { + return changeFirstCharacterCase(str, false); + } + + private static String changeFirstCharacterCase(String str, boolean capitalize) { + if (!hasLength(str)) { + return str; + } + + char baseChar = str.charAt(0); + char updatedChar; + if (capitalize) { + updatedChar = Character.toUpperCase(baseChar); + } + else { + updatedChar = Character.toLowerCase(baseChar); + } + if (baseChar == updatedChar) { + return str; + } + + char[] chars = str.toCharArray(); + chars[0] = updatedChar; + return new String(chars, 0, chars.length); + } + + /** + * Extract the filename from the given Java resource path, + * e.g. {@code "mypath/myfile.txt" -> "myfile.txt"}. + * @param path the file path (may be {@code null}) + * @return the extracted filename, or {@code null} if none + */ + @Nullable + public static String getFilename(@Nullable String path) { + if (path == null) { + return null; + } + + int separatorIndex = path.lastIndexOf(FOLDER_SEPARATOR); + return (separatorIndex != -1 ? path.substring(separatorIndex + 1) : path); + } + + /** + * Extract the filename extension from the given Java resource path, + * e.g. "mypath/myfile.txt" -> "txt". + * @param path the file path (may be {@code null}) + * @return the extracted filename extension, or {@code null} if none + */ + @Nullable + public static String getFilenameExtension(@Nullable String path) { + if (path == null) { + return null; + } + + int extIndex = path.lastIndexOf(EXTENSION_SEPARATOR); + if (extIndex == -1) { + return null; + } + + int folderIndex = path.lastIndexOf(FOLDER_SEPARATOR); + if (folderIndex > extIndex) { + return null; + } + + return path.substring(extIndex + 1); + } + + /** + * Strip the filename extension from the given Java resource path, + * e.g. "mypath/myfile.txt" -> "mypath/myfile". + * @param path the file path + * @return the path with stripped filename extension + */ + public static String stripFilenameExtension(String path) { + int extIndex = path.lastIndexOf(EXTENSION_SEPARATOR); + if (extIndex == -1) { + return path; + } + + int folderIndex = path.lastIndexOf(FOLDER_SEPARATOR); + if (folderIndex > extIndex) { + return path; + } + + return path.substring(0, extIndex); + } + + /** + * Apply the given relative path to the given Java resource path, + * assuming standard Java folder separation (i.e. "/" separators). + * @param path the path to start from (usually a full file path) + * @param relativePath the relative path to apply + * (relative to the full file path above) + * @return the full file path that results from applying the relative path + */ + public static String applyRelativePath(String path, String relativePath) { + int separatorIndex = path.lastIndexOf(FOLDER_SEPARATOR); + if (separatorIndex != -1) { + String newPath = path.substring(0, separatorIndex); + if (!relativePath.startsWith(FOLDER_SEPARATOR)) { + newPath += FOLDER_SEPARATOR; + } + return newPath + relativePath; + } + else { + return relativePath; + } + } + + /** + * Normalize the path by suppressing sequences like "path/.." and + * inner simple dots. + *

    The result is convenient for path comparison. For other uses, + * notice that Windows separators ("\") are replaced by simple slashes. + *

    NOTE that {@code cleanPath} should not be depended + * upon in a security context. Other mechanisms should be used to prevent + * path-traversal issues. + * @param path the original path + * @return the normalized path + */ + public static String cleanPath(String path) { + if (!hasLength(path)) { + return path; + } + String pathToUse = replace(path, WINDOWS_FOLDER_SEPARATOR, FOLDER_SEPARATOR); + + // Shortcut if there is no work to do + if (pathToUse.indexOf('.') == -1) { + return pathToUse; + } + + // Strip prefix from path to analyze, to not treat it as part of the + // first path element. This is necessary to correctly parse paths like + // "file:core/../core/io/Resource.class", where the ".." should just + // strip the first "core" directory while keeping the "file:" prefix. + int prefixIndex = pathToUse.indexOf(':'); + String prefix = ""; + if (prefixIndex != -1) { + prefix = pathToUse.substring(0, prefixIndex + 1); + if (prefix.contains(FOLDER_SEPARATOR)) { + prefix = ""; + } + else { + pathToUse = pathToUse.substring(prefixIndex + 1); + } + } + if (pathToUse.startsWith(FOLDER_SEPARATOR)) { + prefix = prefix + FOLDER_SEPARATOR; + pathToUse = pathToUse.substring(1); + } + + String[] pathArray = delimitedListToStringArray(pathToUse, FOLDER_SEPARATOR); + Deque pathElements = new ArrayDeque<>(); + int tops = 0; + + for (int i = pathArray.length - 1; i >= 0; i--) { + String element = pathArray[i]; + if (CURRENT_PATH.equals(element)) { + // Points to current directory - drop it. + } + else if (TOP_PATH.equals(element)) { + // Registering top path found. + tops++; + } + else { + if (tops > 0) { + // Merging path element with element corresponding to top path. + tops--; + } + else { + // Normal path element found. + pathElements.addFirst(element); + } + } + } + + // All path elements stayed the same - shortcut + if (pathArray.length == pathElements.size()) { + return prefix + pathToUse; + } + // Remaining top paths need to be retained. + for (int i = 0; i < tops; i++) { + pathElements.addFirst(TOP_PATH); + } + // If nothing else left, at least explicitly point to current path. + if (pathElements.size() == 1 && pathElements.getLast().isEmpty() && !prefix.endsWith(FOLDER_SEPARATOR)) { + pathElements.addFirst(CURRENT_PATH); + } + + return prefix + collectionToDelimitedString(pathElements, FOLDER_SEPARATOR); + } + + /** + * Compare two paths after normalization of them. + * @param path1 first path for comparison + * @param path2 second path for comparison + * @return whether the two paths are equivalent after normalization + */ + public static boolean pathEquals(String path1, String path2) { + return cleanPath(path1).equals(cleanPath(path2)); + } + + /** + * Decode the given encoded URI component value. Based on the following rules: + *

      + *
    • Alphanumeric characters {@code "a"} through {@code "z"}, {@code "A"} through {@code "Z"}, + * and {@code "0"} through {@code "9"} stay the same.
    • + *
    • Special characters {@code "-"}, {@code "_"}, {@code "."}, and {@code "*"} stay the same.
    • + *
    • A sequence "{@code %xy}" is interpreted as a hexadecimal representation of the character.
    • + *
    + * @param source the encoded String + * @param charset the character set + * @return the decoded value + * @throws IllegalArgumentException when the given source contains invalid encoded sequences + * @since 5.0 + * @see java.net.URLDecoder#decode(String, String) + */ + public static String uriDecode(String source, Charset charset) { + int length = source.length(); + if (length == 0) { + return source; + } + Assert.notNull(charset, "Charset must not be null"); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(length); + boolean changed = false; + for (int i = 0; i < length; i++) { + int ch = source.charAt(i); + if (ch == '%') { + if (i + 2 < length) { + char hex1 = source.charAt(i + 1); + char hex2 = source.charAt(i + 2); + int u = Character.digit(hex1, 16); + int l = Character.digit(hex2, 16); + if (u == -1 || l == -1) { + throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\""); + } + baos.write((char) ((u << 4) + l)); + i += 2; + changed = true; + } + else { + throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\""); + } + } + else { + baos.write(ch); + } + } + return (changed ? StreamUtils.copyToString(baos, charset) : source); + } + + /** + * Parse the given {@code String} value into a {@link Locale}, accepting + * the {@link Locale#toString} format as well as BCP 47 language tags. + * @param localeValue the locale value: following either {@code Locale's} + * {@code toString()} format ("en", "en_UK", etc), also accepting spaces as + * separators (as an alternative to underscores), or BCP 47 (e.g. "en-UK") + * as specified by {@link Locale#forLanguageTag} on Java 7+ + * @return a corresponding {@code Locale} instance, or {@code null} if none + * @throws IllegalArgumentException in case of an invalid locale specification + * @since 5.0.4 + * @see #parseLocaleString + * @see Locale#forLanguageTag + */ + @Nullable + public static Locale parseLocale(String localeValue) { + String[] tokens = tokenizeLocaleSource(localeValue); + if (tokens.length == 1) { + validateLocalePart(localeValue); + Locale resolved = Locale.forLanguageTag(localeValue); + if (resolved.getLanguage().length() > 0) { + return resolved; + } + } + return parseLocaleTokens(localeValue, tokens); + } + + /** + * Parse the given {@code String} representation into a {@link Locale}. + *

    For many parsing scenarios, this is an inverse operation of + * {@link Locale#toString Locale's toString}, in a lenient sense. + * This method does not aim for strict {@code Locale} design compliance; + * it is rather specifically tailored for typical Spring parsing needs. + *

    Note: This delegate does not accept the BCP 47 language tag format. + * Please use {@link #parseLocale} for lenient parsing of both formats. + * @param localeString the locale {@code String}: following {@code Locale's} + * {@code toString()} format ("en", "en_UK", etc), also accepting spaces as + * separators (as an alternative to underscores) + * @return a corresponding {@code Locale} instance, or {@code null} if none + * @throws IllegalArgumentException in case of an invalid locale specification + */ + @Nullable + public static Locale parseLocaleString(String localeString) { + return parseLocaleTokens(localeString, tokenizeLocaleSource(localeString)); + } + + private static String[] tokenizeLocaleSource(String localeSource) { + return tokenizeToStringArray(localeSource, "_ ", false, false); + } + + @Nullable + private static Locale parseLocaleTokens(String localeString, String[] tokens) { + String language = (tokens.length > 0 ? tokens[0] : ""); + String country = (tokens.length > 1 ? tokens[1] : ""); + validateLocalePart(language); + validateLocalePart(country); + + String variant = ""; + if (tokens.length > 2) { + // There is definitely a variant, and it is everything after the country + // code sans the separator between the country code and the variant. + int endIndexOfCountryCode = localeString.indexOf(country, language.length()) + country.length(); + // Strip off any leading '_' and whitespace, what's left is the variant. + variant = trimLeadingWhitespace(localeString.substring(endIndexOfCountryCode)); + if (variant.startsWith("_")) { + variant = trimLeadingCharacter(variant, '_'); + } + } + + if (variant.isEmpty() && country.startsWith("#")) { + variant = country; + country = ""; + } + + return (language.length() > 0 ? new Locale(language, country, variant) : null); + } + + private static void validateLocalePart(String localePart) { + for (int i = 0; i < localePart.length(); i++) { + char ch = localePart.charAt(i); + if (ch != ' ' && ch != '_' && ch != '-' && ch != '#' && !Character.isLetterOrDigit(ch)) { + throw new IllegalArgumentException( + "Locale part \"" + localePart + "\" contains invalid characters"); + } + } + } + + /** + * Determine the RFC 3066 compliant language tag, + * as used for the HTTP "Accept-Language" header. + * @param locale the Locale to transform to a language tag + * @return the RFC 3066 compliant language tag as {@code String} + * @deprecated as of 5.0.4, in favor of {@link Locale#toLanguageTag()} + */ + @Deprecated + public static String toLanguageTag(Locale locale) { + return locale.getLanguage() + (hasText(locale.getCountry()) ? "-" + locale.getCountry() : ""); + } + + /** + * Parse the given {@code timeZoneString} value into a {@link TimeZone}. + * @param timeZoneString the time zone {@code String}, following {@link TimeZone#getTimeZone(String)} + * but throwing {@link IllegalArgumentException} in case of an invalid time zone specification + * @return a corresponding {@link TimeZone} instance + * @throws IllegalArgumentException in case of an invalid time zone specification + */ + public static TimeZone parseTimeZoneString(String timeZoneString) { + TimeZone timeZone = TimeZone.getTimeZone(timeZoneString); + if ("GMT".equals(timeZone.getID()) && !timeZoneString.startsWith("GMT")) { + // We don't want that GMT fallback... + throw new IllegalArgumentException("Invalid time zone specification '" + timeZoneString + "'"); + } + return timeZone; + } + + + //--------------------------------------------------------------------- + // Convenience methods for working with String arrays + //--------------------------------------------------------------------- + + /** + * Copy the given {@link Collection} into a {@code String} array. + *

    The {@code Collection} must contain {@code String} elements only. + * @param collection the {@code Collection} to copy + * (potentially {@code null} or empty) + * @return the resulting {@code String} array + */ + public static String[] toStringArray(@Nullable Collection collection) { + return (!CollectionUtils.isEmpty(collection) ? collection.toArray(EMPTY_STRING_ARRAY) : EMPTY_STRING_ARRAY); + } + + /** + * Copy the given {@link Enumeration} into a {@code String} array. + *

    The {@code Enumeration} must contain {@code String} elements only. + * @param enumeration the {@code Enumeration} to copy + * (potentially {@code null} or empty) + * @return the resulting {@code String} array + */ + public static String[] toStringArray(@Nullable Enumeration enumeration) { + return (enumeration != null ? toStringArray(Collections.list(enumeration)) : EMPTY_STRING_ARRAY); + } + + /** + * Append the given {@code String} to the given {@code String} array, + * returning a new array consisting of the input array contents plus + * the given {@code String}. + * @param array the array to append to (can be {@code null}) + * @param str the {@code String} to append + * @return the new array (never {@code null}) + */ + public static String[] addStringToArray(@Nullable String[] array, String str) { + if (ObjectUtils.isEmpty(array)) { + return new String[] {str}; + } + + String[] newArr = new String[array.length + 1]; + System.arraycopy(array, 0, newArr, 0, array.length); + newArr[array.length] = str; + return newArr; + } + + /** + * Concatenate the given {@code String} arrays into one, + * with overlapping array elements included twice. + *

    The order of elements in the original arrays is preserved. + * @param array1 the first array (can be {@code null}) + * @param array2 the second array (can be {@code null}) + * @return the new array ({@code null} if both given arrays were {@code null}) + */ + @Nullable + public static String[] concatenateStringArrays(@Nullable String[] array1, @Nullable String[] array2) { + if (ObjectUtils.isEmpty(array1)) { + return array2; + } + if (ObjectUtils.isEmpty(array2)) { + return array1; + } + + String[] newArr = new String[array1.length + array2.length]; + System.arraycopy(array1, 0, newArr, 0, array1.length); + System.arraycopy(array2, 0, newArr, array1.length, array2.length); + return newArr; + } + + /** + * Merge the given {@code String} arrays into one, with overlapping + * array elements only included once. + *

    The order of elements in the original arrays is preserved + * (with the exception of overlapping elements, which are only + * included on their first occurrence). + * @param array1 the first array (can be {@code null}) + * @param array2 the second array (can be {@code null}) + * @return the new array ({@code null} if both given arrays were {@code null}) + * @deprecated as of 4.3.15, in favor of manual merging via {@link LinkedHashSet} + * (with every entry included at most once, even entries within the first array) + */ + @Deprecated + @Nullable + public static String[] mergeStringArrays(@Nullable String[] array1, @Nullable String[] array2) { + if (ObjectUtils.isEmpty(array1)) { + return array2; + } + if (ObjectUtils.isEmpty(array2)) { + return array1; + } + + List result = new ArrayList<>(Arrays.asList(array1)); + for (String str : array2) { + if (!result.contains(str)) { + result.add(str); + } + } + return toStringArray(result); + } + + /** + * Sort the given {@code String} array if necessary. + * @param array the original array (potentially empty) + * @return the array in sorted form (never {@code null}) + */ + public static String[] sortStringArray(String[] array) { + if (ObjectUtils.isEmpty(array)) { + return array; + } + + Arrays.sort(array); + return array; + } + + /** + * Trim the elements of the given {@code String} array, calling + * {@code String.trim()} on each non-null element. + * @param array the original {@code String} array (potentially empty) + * @return the resulting array (of the same size) with trimmed elements + */ + public static String[] trimArrayElements(String[] array) { + if (ObjectUtils.isEmpty(array)) { + return array; + } + + String[] result = new String[array.length]; + for (int i = 0; i < array.length; i++) { + String element = array[i]; + result[i] = (element != null ? element.trim() : null); + } + return result; + } + + /** + * Remove duplicate strings from the given array. + *

    As of 4.2, it preserves the original order, as it uses a {@link LinkedHashSet}. + * @param array the {@code String} array (potentially empty) + * @return an array without duplicates, in natural sort order + */ + public static String[] removeDuplicateStrings(String[] array) { + if (ObjectUtils.isEmpty(array)) { + return array; + } + + Set set = new LinkedHashSet<>(Arrays.asList(array)); + return toStringArray(set); + } + + /** + * Split a {@code String} at the first occurrence of the delimiter. + * Does not include the delimiter in the result. + * @param toSplit the string to split (potentially {@code null} or empty) + * @param delimiter to split the string up with (potentially {@code null} or empty) + * @return a two element array with index 0 being before the delimiter, and + * index 1 being after the delimiter (neither element includes the delimiter); + * or {@code null} if the delimiter wasn't found in the given input {@code String} + */ + @Nullable + public static String[] split(@Nullable String toSplit, @Nullable String delimiter) { + if (!hasLength(toSplit) || !hasLength(delimiter)) { + return null; + } + int offset = toSplit.indexOf(delimiter); + if (offset < 0) { + return null; + } + + String beforeDelimiter = toSplit.substring(0, offset); + String afterDelimiter = toSplit.substring(offset + delimiter.length()); + return new String[] {beforeDelimiter, afterDelimiter}; + } + + /** + * Take an array of strings and split each element based on the given delimiter. + * A {@code Properties} instance is then generated, with the left of the delimiter + * providing the key, and the right of the delimiter providing the value. + *

    Will trim both the key and value before adding them to the {@code Properties}. + * @param array the array to process + * @param delimiter to split each element using (typically the equals symbol) + * @return a {@code Properties} instance representing the array contents, + * or {@code null} if the array to process was {@code null} or empty + */ + @Nullable + public static Properties splitArrayElementsIntoProperties(String[] array, String delimiter) { + return splitArrayElementsIntoProperties(array, delimiter, null); + } + + /** + * Take an array of strings and split each element based on the given delimiter. + * A {@code Properties} instance is then generated, with the left of the + * delimiter providing the key, and the right of the delimiter providing the value. + *

    Will trim both the key and value before adding them to the + * {@code Properties} instance. + * @param array the array to process + * @param delimiter to split each element using (typically the equals symbol) + * @param charsToDelete one or more characters to remove from each element + * prior to attempting the split operation (typically the quotation mark + * symbol), or {@code null} if no removal should occur + * @return a {@code Properties} instance representing the array contents, + * or {@code null} if the array to process was {@code null} or empty + */ + @Nullable + public static Properties splitArrayElementsIntoProperties( + String[] array, String delimiter, @Nullable String charsToDelete) { + + if (ObjectUtils.isEmpty(array)) { + return null; + } + + Properties result = new Properties(); + for (String element : array) { + if (charsToDelete != null) { + element = deleteAny(element, charsToDelete); + } + String[] splittedElement = split(element, delimiter); + if (splittedElement == null) { + continue; + } + result.setProperty(splittedElement[0].trim(), splittedElement[1].trim()); + } + return result; + } + + /** + * Tokenize the given {@code String} into a {@code String} array via a + * {@link StringTokenizer}. + *

    Trims tokens and omits empty tokens. + *

    The given {@code delimiters} string can consist of any number of + * delimiter characters. Each of those characters can be used to separate + * tokens. A delimiter is always a single character; for multi-character + * delimiters, consider using {@link #delimitedListToStringArray}. + * @param str the {@code String} to tokenize (potentially {@code null} or empty) + * @param delimiters the delimiter characters, assembled as a {@code String} + * (each of the characters is individually considered as a delimiter) + * @return an array of the tokens + * @see java.util.StringTokenizer + * @see String#trim() + * @see #delimitedListToStringArray + */ + public static String[] tokenizeToStringArray(@Nullable String str, String delimiters) { + return tokenizeToStringArray(str, delimiters, true, true); + } + + /** + * Tokenize the given {@code String} into a {@code String} array via a + * {@link StringTokenizer}. + *

    The given {@code delimiters} string can consist of any number of + * delimiter characters. Each of those characters can be used to separate + * tokens. A delimiter is always a single character; for multi-character + * delimiters, consider using {@link #delimitedListToStringArray}. + * @param str the {@code String} to tokenize (potentially {@code null} or empty) + * @param delimiters the delimiter characters, assembled as a {@code String} + * (each of the characters is individually considered as a delimiter) + * @param trimTokens trim the tokens via {@link String#trim()} + * @param ignoreEmptyTokens omit empty tokens from the result array + * (only applies to tokens that are empty after trimming; StringTokenizer + * will not consider subsequent delimiters as token in the first place). + * @return an array of the tokens + * @see java.util.StringTokenizer + * @see String#trim() + * @see #delimitedListToStringArray + */ + public static String[] tokenizeToStringArray( + @Nullable String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) { + + if (str == null) { + return EMPTY_STRING_ARRAY; + } + + StringTokenizer st = new StringTokenizer(str, delimiters); + List tokens = new ArrayList<>(); + while (st.hasMoreTokens()) { + String token = st.nextToken(); + if (trimTokens) { + token = token.trim(); + } + if (!ignoreEmptyTokens || token.length() > 0) { + tokens.add(token); + } + } + return toStringArray(tokens); + } + + /** + * Take a {@code String} that is a delimited list and convert it into a + * {@code String} array. + *

    A single {@code delimiter} may consist of more than one character, + * but it will still be considered as a single delimiter string, rather + * than as bunch of potential delimiter characters, in contrast to + * {@link #tokenizeToStringArray}. + * @param str the input {@code String} (potentially {@code null} or empty) + * @param delimiter the delimiter between elements (this is a single delimiter, + * rather than a bunch individual delimiter characters) + * @return an array of the tokens in the list + * @see #tokenizeToStringArray + */ + public static String[] delimitedListToStringArray(@Nullable String str, @Nullable String delimiter) { + return delimitedListToStringArray(str, delimiter, null); + } + + /** + * Take a {@code String} that is a delimited list and convert it into + * a {@code String} array. + *

    A single {@code delimiter} may consist of more than one character, + * but it will still be considered as a single delimiter string, rather + * than as bunch of potential delimiter characters, in contrast to + * {@link #tokenizeToStringArray}. + * @param str the input {@code String} (potentially {@code null} or empty) + * @param delimiter the delimiter between elements (this is a single delimiter, + * rather than a bunch individual delimiter characters) + * @param charsToDelete a set of characters to delete; useful for deleting unwanted + * line breaks: e.g. "\r\n\f" will delete all new lines and line feeds in a {@code String} + * @return an array of the tokens in the list + * @see #tokenizeToStringArray + */ + public static String[] delimitedListToStringArray( + @Nullable String str, @Nullable String delimiter, @Nullable String charsToDelete) { + + if (str == null) { + return EMPTY_STRING_ARRAY; + } + if (delimiter == null) { + return new String[] {str}; + } + + List result = new ArrayList<>(); + if (delimiter.isEmpty()) { + for (int i = 0; i < str.length(); i++) { + result.add(deleteAny(str.substring(i, i + 1), charsToDelete)); + } + } + else { + int pos = 0; + int delPos; + while ((delPos = str.indexOf(delimiter, pos)) != -1) { + result.add(deleteAny(str.substring(pos, delPos), charsToDelete)); + pos = delPos + delimiter.length(); + } + if (str.length() > 0 && pos <= str.length()) { + // Add rest of String, but not in case of empty input. + result.add(deleteAny(str.substring(pos), charsToDelete)); + } + } + return toStringArray(result); + } + + /** + * Convert a comma delimited list (e.g., a row from a CSV file) into an + * array of strings. + * @param str the input {@code String} (potentially {@code null} or empty) + * @return an array of strings, or the empty array in case of empty input + */ + public static String[] commaDelimitedListToStringArray(@Nullable String str) { + return delimitedListToStringArray(str, ","); + } + + /** + * Convert a comma delimited list (e.g., a row from a CSV file) into a set. + *

    Note that this will suppress duplicates, and as of 4.2, the elements in + * the returned set will preserve the original order in a {@link LinkedHashSet}. + * @param str the input {@code String} (potentially {@code null} or empty) + * @return a set of {@code String} entries in the list + * @see #removeDuplicateStrings(String[]) + */ + public static Set commaDelimitedListToSet(@Nullable String str) { + String[] tokens = commaDelimitedListToStringArray(str); + return new LinkedHashSet<>(Arrays.asList(tokens)); + } + + /** + * Convert a {@link Collection} to a delimited {@code String} (e.g. CSV). + *

    Useful for {@code toString()} implementations. + * @param coll the {@code Collection} to convert (potentially {@code null} or empty) + * @param delim the delimiter to use (typically a ",") + * @param prefix the {@code String} to start each element with + * @param suffix the {@code String} to end each element with + * @return the delimited {@code String} + */ + public static String collectionToDelimitedString( + @Nullable Collection coll, String delim, String prefix, String suffix) { + + if (CollectionUtils.isEmpty(coll)) { + return ""; + } + + StringBuilder sb = new StringBuilder(); + Iterator it = coll.iterator(); + while (it.hasNext()) { + sb.append(prefix).append(it.next()).append(suffix); + if (it.hasNext()) { + sb.append(delim); + } + } + return sb.toString(); + } + + /** + * Convert a {@code Collection} into a delimited {@code String} (e.g. CSV). + *

    Useful for {@code toString()} implementations. + * @param coll the {@code Collection} to convert (potentially {@code null} or empty) + * @param delim the delimiter to use (typically a ",") + * @return the delimited {@code String} + */ + public static String collectionToDelimitedString(@Nullable Collection coll, String delim) { + return collectionToDelimitedString(coll, delim, "", ""); + } + + /** + * Convert a {@code Collection} into a delimited {@code String} (e.g., CSV). + *

    Useful for {@code toString()} implementations. + * @param coll the {@code Collection} to convert (potentially {@code null} or empty) + * @return the delimited {@code String} + */ + public static String collectionToCommaDelimitedString(@Nullable Collection coll) { + return collectionToDelimitedString(coll, ","); + } + + /** + * Convert a {@code String} array into a delimited {@code String} (e.g. CSV). + *

    Useful for {@code toString()} implementations. + * @param arr the array to display (potentially {@code null} or empty) + * @param delim the delimiter to use (typically a ",") + * @return the delimited {@code String} + */ + public static String arrayToDelimitedString(@Nullable Object[] arr, String delim) { + if (ObjectUtils.isEmpty(arr)) { + return ""; + } + if (arr.length == 1) { + return ObjectUtils.nullSafeToString(arr[0]); + } + + StringJoiner sj = new StringJoiner(delim); + for (Object o : arr) { + sj.add(String.valueOf(o)); + } + return sj.toString(); + } + + /** + * Convert a {@code String} array into a comma delimited {@code String} + * (i.e., CSV). + *

    Useful for {@code toString()} implementations. + * @param arr the array to display (potentially {@code null} or empty) + * @return the delimited {@code String} + */ + public static String arrayToCommaDelimitedString(@Nullable Object[] arr) { + return arrayToDelimitedString(arr, ","); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/StringValueResolver.java b/spring-core/src/main/java/org/springframework/util/StringValueResolver.java new file mode 100644 index 0000000..72ba1ec --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/StringValueResolver.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import org.springframework.lang.Nullable; + +/** + * Simple strategy interface for resolving a String value. + * Used by {@link org.springframework.beans.factory.config.ConfigurableBeanFactory}. + * + * @author Juergen Hoeller + * @since 2.5 + * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#resolveAliases + * @see org.springframework.beans.factory.config.BeanDefinitionVisitor#BeanDefinitionVisitor(StringValueResolver) + * @see org.springframework.beans.factory.config.PropertyPlaceholderConfigurer + */ +@FunctionalInterface +public interface StringValueResolver { + + /** + * Resolve the given String value, for example parsing placeholders. + * @param strVal the original String value (never {@code null}) + * @return the resolved String value (may be {@code null} when resolved to a null + * value), possibly the original String value itself (in case of no placeholders + * to resolve or when ignoring unresolvable placeholders) + * @throws IllegalArgumentException in case of an unresolvable String value + */ + @Nullable + String resolveStringValue(String strVal); + +} diff --git a/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java b/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java new file mode 100644 index 0000000..7c5ac6b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import org.springframework.lang.Nullable; + +/** + * Helper class for resolving placeholders in texts. Usually applied to file paths. + * + *

    A text may contain {@code ${...}} placeholders, to be resolved as system properties: + * e.g. {@code ${user.dir}}. Default values can be supplied using the ":" separator + * between key and value. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @author Dave Syer + * @since 1.2.5 + * @see #PLACEHOLDER_PREFIX + * @see #PLACEHOLDER_SUFFIX + * @see System#getProperty(String) + */ +public abstract class SystemPropertyUtils { + + /** Prefix for system property placeholders: "${". */ + public static final String PLACEHOLDER_PREFIX = "${"; + + /** Suffix for system property placeholders: "}". */ + public static final String PLACEHOLDER_SUFFIX = "}"; + + /** Value separator for system property placeholders: ":". */ + public static final String VALUE_SEPARATOR = ":"; + + + private static final PropertyPlaceholderHelper strictHelper = + new PropertyPlaceholderHelper(PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX, VALUE_SEPARATOR, false); + + private static final PropertyPlaceholderHelper nonStrictHelper = + new PropertyPlaceholderHelper(PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX, VALUE_SEPARATOR, true); + + + /** + * Resolve {@code ${...}} placeholders in the given text, replacing them with + * corresponding system property values. + * @param text the String to resolve + * @return the resolved String + * @throws IllegalArgumentException if there is an unresolvable placeholder + * @see #PLACEHOLDER_PREFIX + * @see #PLACEHOLDER_SUFFIX + */ + public static String resolvePlaceholders(String text) { + return resolvePlaceholders(text, false); + } + + /** + * Resolve {@code ${...}} placeholders in the given text, replacing them with + * corresponding system property values. Unresolvable placeholders with no default + * value are ignored and passed through unchanged if the flag is set to {@code true}. + * @param text the String to resolve + * @param ignoreUnresolvablePlaceholders whether unresolved placeholders are to be ignored + * @return the resolved String + * @throws IllegalArgumentException if there is an unresolvable placeholder + * @see #PLACEHOLDER_PREFIX + * @see #PLACEHOLDER_SUFFIX + * and the "ignoreUnresolvablePlaceholders" flag is {@code false} + */ + public static String resolvePlaceholders(String text, boolean ignoreUnresolvablePlaceholders) { + if (text.isEmpty()) { + return text; + } + PropertyPlaceholderHelper helper = (ignoreUnresolvablePlaceholders ? nonStrictHelper : strictHelper); + return helper.replacePlaceholders(text, new SystemPropertyPlaceholderResolver(text)); + } + + + /** + * PlaceholderResolver implementation that resolves against system properties + * and system environment variables. + */ + private static class SystemPropertyPlaceholderResolver implements PropertyPlaceholderHelper.PlaceholderResolver { + + private final String text; + + public SystemPropertyPlaceholderResolver(String text) { + this.text = text; + } + + @Override + @Nullable + public String resolvePlaceholder(String placeholderName) { + try { + String propVal = System.getProperty(placeholderName); + if (propVal == null) { + // Fall back to searching the system environment. + propVal = System.getenv(placeholderName); + } + return propVal; + } + catch (Throwable ex) { + System.err.println("Could not resolve placeholder '" + placeholderName + "' in [" + + this.text + "] as system property: " + ex); + return null; + } + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/TypeUtils.java b/spring-core/src/main/java/org/springframework/util/TypeUtils.java new file mode 100644 index 0000000..4a959ae --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/TypeUtils.java @@ -0,0 +1,226 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; + +import org.springframework.lang.Nullable; + +/** + * Utility to work with Java 5 generic type parameters. + * Mainly for internal use within the framework. + * + * @author Ramnivas Laddad + * @author Juergen Hoeller + * @author Chris Beams + * @since 2.0.7 + */ +public abstract class TypeUtils { + + /** + * Check if the right-hand side type may be assigned to the left-hand side + * type following the Java generics rules. + * @param lhsType the target type + * @param rhsType the value type that should be assigned to the target type + * @return true if rhs is assignable to lhs + */ + public static boolean isAssignable(Type lhsType, Type rhsType) { + Assert.notNull(lhsType, "Left-hand side type must not be null"); + Assert.notNull(rhsType, "Right-hand side type must not be null"); + + // all types are assignable to themselves and to class Object + if (lhsType.equals(rhsType) || Object.class == lhsType) { + return true; + } + + if (lhsType instanceof Class) { + Class lhsClass = (Class) lhsType; + + // just comparing two classes + if (rhsType instanceof Class) { + return ClassUtils.isAssignable(lhsClass, (Class) rhsType); + } + + if (rhsType instanceof ParameterizedType) { + Type rhsRaw = ((ParameterizedType) rhsType).getRawType(); + + // a parameterized type is always assignable to its raw class type + if (rhsRaw instanceof Class) { + return ClassUtils.isAssignable(lhsClass, (Class) rhsRaw); + } + } + else if (lhsClass.isArray() && rhsType instanceof GenericArrayType) { + Type rhsComponent = ((GenericArrayType) rhsType).getGenericComponentType(); + + return isAssignable(lhsClass.getComponentType(), rhsComponent); + } + } + + // parameterized types are only assignable to other parameterized types and class types + if (lhsType instanceof ParameterizedType) { + if (rhsType instanceof Class) { + Type lhsRaw = ((ParameterizedType) lhsType).getRawType(); + + if (lhsRaw instanceof Class) { + return ClassUtils.isAssignable((Class) lhsRaw, (Class) rhsType); + } + } + else if (rhsType instanceof ParameterizedType) { + return isAssignable((ParameterizedType) lhsType, (ParameterizedType) rhsType); + } + } + + if (lhsType instanceof GenericArrayType) { + Type lhsComponent = ((GenericArrayType) lhsType).getGenericComponentType(); + + if (rhsType instanceof Class) { + Class rhsClass = (Class) rhsType; + + if (rhsClass.isArray()) { + return isAssignable(lhsComponent, rhsClass.getComponentType()); + } + } + else if (rhsType instanceof GenericArrayType) { + Type rhsComponent = ((GenericArrayType) rhsType).getGenericComponentType(); + + return isAssignable(lhsComponent, rhsComponent); + } + } + + if (lhsType instanceof WildcardType) { + return isAssignable((WildcardType) lhsType, rhsType); + } + + return false; + } + + private static boolean isAssignable(ParameterizedType lhsType, ParameterizedType rhsType) { + if (lhsType.equals(rhsType)) { + return true; + } + + Type[] lhsTypeArguments = lhsType.getActualTypeArguments(); + Type[] rhsTypeArguments = rhsType.getActualTypeArguments(); + + if (lhsTypeArguments.length != rhsTypeArguments.length) { + return false; + } + + for (int size = lhsTypeArguments.length, i = 0; i < size; ++i) { + Type lhsArg = lhsTypeArguments[i]; + Type rhsArg = rhsTypeArguments[i]; + + if (!lhsArg.equals(rhsArg) && + !(lhsArg instanceof WildcardType && isAssignable((WildcardType) lhsArg, rhsArg))) { + return false; + } + } + + return true; + } + + private static boolean isAssignable(WildcardType lhsType, Type rhsType) { + Type[] lUpperBounds = lhsType.getUpperBounds(); + + // supply the implicit upper bound if none are specified + if (lUpperBounds.length == 0) { + lUpperBounds = new Type[] { Object.class }; + } + + Type[] lLowerBounds = lhsType.getLowerBounds(); + + // supply the implicit lower bound if none are specified + if (lLowerBounds.length == 0) { + lLowerBounds = new Type[] { null }; + } + + if (rhsType instanceof WildcardType) { + // both the upper and lower bounds of the right-hand side must be + // completely enclosed in the upper and lower bounds of the left- + // hand side. + WildcardType rhsWcType = (WildcardType) rhsType; + Type[] rUpperBounds = rhsWcType.getUpperBounds(); + + if (rUpperBounds.length == 0) { + rUpperBounds = new Type[] { Object.class }; + } + + Type[] rLowerBounds = rhsWcType.getLowerBounds(); + + if (rLowerBounds.length == 0) { + rLowerBounds = new Type[] { null }; + } + + for (Type lBound : lUpperBounds) { + for (Type rBound : rUpperBounds) { + if (!isAssignableBound(lBound, rBound)) { + return false; + } + } + + for (Type rBound : rLowerBounds) { + if (!isAssignableBound(lBound, rBound)) { + return false; + } + } + } + + for (Type lBound : lLowerBounds) { + for (Type rBound : rUpperBounds) { + if (!isAssignableBound(rBound, lBound)) { + return false; + } + } + + for (Type rBound : rLowerBounds) { + if (!isAssignableBound(rBound, lBound)) { + return false; + } + } + } + } + else { + for (Type lBound : lUpperBounds) { + if (!isAssignableBound(lBound, rhsType)) { + return false; + } + } + + for (Type lBound : lLowerBounds) { + if (!isAssignableBound(rhsType, lBound)) { + return false; + } + } + } + + return true; + } + + public static boolean isAssignableBound(@Nullable Type lhsType, @Nullable Type rhsType) { + if (rhsType == null) { + return true; + } + if (lhsType == null) { + return false; + } + return isAssignable(lhsType, rhsType); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/UpdateMessageDigestInputStream.java b/spring-core/src/main/java/org/springframework/util/UpdateMessageDigestInputStream.java new file mode 100644 index 0000000..5d1d6bf --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/UpdateMessageDigestInputStream.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.io.IOException; +import java.io.InputStream; +import java.security.MessageDigest; + +/** + * Extension of {@link java.io.InputStream} that allows for optimized + * implementations of message digesting. + * + * @author Craig Andrews + * @since 4.2 + */ +abstract class UpdateMessageDigestInputStream extends InputStream { + + /** + * Update the message digest with the rest of the bytes in this stream. + *

    Using this method is more optimized since it avoids creating new + * byte arrays for each call. + * @param messageDigest the message digest to update + * @throws IOException when propagated from {@link #read()} + */ + public void updateMessageDigest(MessageDigest messageDigest) throws IOException { + int data; + while ((data = read()) != -1) { + messageDigest.update((byte) data); + } + } + + /** + * Update the message digest with the next len bytes in this stream. + *

    Using this method is more optimized since it avoids creating new + * byte arrays for each call. + * @param messageDigest the message digest to update + * @param len how many bytes to read from this stream and use to update the message digest + * @throws IOException when propagated from {@link #read()} + */ + public void updateMessageDigest(MessageDigest messageDigest, int len) throws IOException { + int data; + int bytesRead = 0; + while (bytesRead < len && (data = read()) != -1) { + messageDigest.update((byte) data); + bytesRead++; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/backoff/BackOff.java b/spring-core/src/main/java/org/springframework/util/backoff/BackOff.java new file mode 100644 index 0000000..bd13e81 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/backoff/BackOff.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.backoff; + +/** + * Provide a {@link BackOffExecution} that indicates the rate at which + * an operation should be retried. + * + *

    Users of this interface are expected to use it like this: + * + *

    + * BackOffExecution exec = backOff.start();
    + *
    + * // In the operation recovery/retry loop:
    + * long waitInterval = exec.nextBackOff();
    + * if (waitInterval == BackOffExecution.STOP) {
    + *     // do not retry operation
    + * }
    + * else {
    + *     // sleep, e.g. Thread.sleep(waitInterval)
    + *     // retry operation
    + * }
    + * }
    + * + * Once the underlying operation has completed successfully, + * the execution instance can be simply discarded. + * + * @author Stephane Nicoll + * @since 4.1 + * @see BackOffExecution + */ +@FunctionalInterface +public interface BackOff { + + /** + * Start a new back off execution. + * @return a fresh {@link BackOffExecution} ready to be used + */ + BackOffExecution start(); + +} diff --git a/spring-core/src/main/java/org/springframework/util/backoff/BackOffExecution.java b/spring-core/src/main/java/org/springframework/util/backoff/BackOffExecution.java new file mode 100644 index 0000000..822b3f3 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/backoff/BackOffExecution.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.backoff; + +/** + * Represent a particular back-off execution. + * + *

    Implementations do not need to be thread safe. + * + * @author Stephane Nicoll + * @since 4.1 + * @see BackOff + */ +@FunctionalInterface +public interface BackOffExecution { + + /** + * Return value of {@link #nextBackOff()} that indicates that the operation + * should not be retried. + */ + long STOP = -1; + + /** + * Return the number of milliseconds to wait before retrying the operation + * or {@link #STOP} ({@value #STOP}) to indicate that no further attempt + * should be made for the operation. + */ + long nextBackOff(); + +} diff --git a/spring-core/src/main/java/org/springframework/util/backoff/ExponentialBackOff.java b/spring-core/src/main/java/org/springframework/util/backoff/ExponentialBackOff.java new file mode 100644 index 0000000..0331699 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/backoff/ExponentialBackOff.java @@ -0,0 +1,228 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.backoff; + +import org.springframework.util.Assert; + +/** + * Implementation of {@link BackOff} that increases the back off period for each + * retry attempt. When the interval has reached the {@link #setMaxInterval(long) + * max interval}, it is no longer increased. Stops retrying once the + * {@link #setMaxElapsedTime(long) max elapsed time} has been reached. + * + *

    Example: The default interval is {@value #DEFAULT_INITIAL_INTERVAL} ms, + * the default multiplier is {@value #DEFAULT_MULTIPLIER}, and the default max + * interval is {@value #DEFAULT_MAX_INTERVAL}. For 10 attempts the sequence will be + * as follows: + * + *

    + * request#     back off
    + *
    + *  1              2000
    + *  2              3000
    + *  3              4500
    + *  4              6750
    + *  5             10125
    + *  6             15187
    + *  7             22780
    + *  8             30000
    + *  9             30000
    + * 10             30000
    + * 
    + * + *

    Note that the default max elapsed time is {@link Long#MAX_VALUE}. Use + * {@link #setMaxElapsedTime(long)} to limit the maximum length of time + * that an instance should accumulate before returning + * {@link BackOffExecution#STOP}. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public class ExponentialBackOff implements BackOff { + + /** + * The default initial interval. + */ + public static final long DEFAULT_INITIAL_INTERVAL = 2000L; + + /** + * The default multiplier (increases the interval by 50%). + */ + public static final double DEFAULT_MULTIPLIER = 1.5; + + /** + * The default maximum back off time. + */ + public static final long DEFAULT_MAX_INTERVAL = 30000L; + + /** + * The default maximum elapsed time. + */ + public static final long DEFAULT_MAX_ELAPSED_TIME = Long.MAX_VALUE; + + + private long initialInterval = DEFAULT_INITIAL_INTERVAL; + + private double multiplier = DEFAULT_MULTIPLIER; + + private long maxInterval = DEFAULT_MAX_INTERVAL; + + private long maxElapsedTime = DEFAULT_MAX_ELAPSED_TIME; + + + /** + * Create an instance with the default settings. + * @see #DEFAULT_INITIAL_INTERVAL + * @see #DEFAULT_MULTIPLIER + * @see #DEFAULT_MAX_INTERVAL + * @see #DEFAULT_MAX_ELAPSED_TIME + */ + public ExponentialBackOff() { + } + + /** + * Create an instance with the supplied settings. + * @param initialInterval the initial interval in milliseconds + * @param multiplier the multiplier (should be greater than or equal to 1) + */ + public ExponentialBackOff(long initialInterval, double multiplier) { + checkMultiplier(multiplier); + this.initialInterval = initialInterval; + this.multiplier = multiplier; + } + + + /** + * The initial interval in milliseconds. + */ + public void setInitialInterval(long initialInterval) { + this.initialInterval = initialInterval; + } + + /** + * Return the initial interval in milliseconds. + */ + public long getInitialInterval() { + return this.initialInterval; + } + + /** + * The value to multiply the current interval by for each retry attempt. + */ + public void setMultiplier(double multiplier) { + checkMultiplier(multiplier); + this.multiplier = multiplier; + } + + /** + * Return the value to multiply the current interval by for each retry attempt. + */ + public double getMultiplier() { + return this.multiplier; + } + + /** + * The maximum back off time. + */ + public void setMaxInterval(long maxInterval) { + this.maxInterval = maxInterval; + } + + /** + * Return the maximum back off time. + */ + public long getMaxInterval() { + return this.maxInterval; + } + + /** + * The maximum elapsed time in milliseconds after which a call to + * {@link BackOffExecution#nextBackOff()} returns {@link BackOffExecution#STOP}. + */ + public void setMaxElapsedTime(long maxElapsedTime) { + this.maxElapsedTime = maxElapsedTime; + } + + /** + * Return the maximum elapsed time in milliseconds after which a call to + * {@link BackOffExecution#nextBackOff()} returns {@link BackOffExecution#STOP}. + */ + public long getMaxElapsedTime() { + return this.maxElapsedTime; + } + + @Override + public BackOffExecution start() { + return new ExponentialBackOffExecution(); + } + + private void checkMultiplier(double multiplier) { + Assert.isTrue(multiplier >= 1, () -> "Invalid multiplier '" + multiplier + "'. Should be greater than " + + "or equal to 1. A multiplier of 1 is equivalent to a fixed interval."); + } + + + private class ExponentialBackOffExecution implements BackOffExecution { + + private long currentInterval = -1; + + private long currentElapsedTime = 0; + + @Override + public long nextBackOff() { + if (this.currentElapsedTime >= maxElapsedTime) { + return STOP; + } + + long nextInterval = computeNextInterval(); + this.currentElapsedTime += nextInterval; + return nextInterval; + } + + private long computeNextInterval() { + long maxInterval = getMaxInterval(); + if (this.currentInterval >= maxInterval) { + return maxInterval; + } + else if (this.currentInterval < 0) { + long initialInterval = getInitialInterval(); + this.currentInterval = Math.min(initialInterval, maxInterval); + } + else { + this.currentInterval = multiplyInterval(maxInterval); + } + return this.currentInterval; + } + + private long multiplyInterval(long maxInterval) { + long i = this.currentInterval; + i *= getMultiplier(); + return Math.min(i, maxInterval); + } + + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("ExponentialBackOff{"); + sb.append("currentInterval=").append(this.currentInterval < 0 ? "n/a" : this.currentInterval + "ms"); + sb.append(", multiplier=").append(getMultiplier()); + sb.append('}'); + return sb.toString(); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/backoff/FixedBackOff.java b/spring-core/src/main/java/org/springframework/util/backoff/FixedBackOff.java new file mode 100644 index 0000000..b4d80c4 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/backoff/FixedBackOff.java @@ -0,0 +1,121 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.backoff; + +/** + * A simple {@link BackOff} implementation that provides a fixed interval + * between two attempts and a maximum number of retries. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public class FixedBackOff implements BackOff { + + /** + * The default recovery interval: 5000 ms = 5 seconds. + */ + public static final long DEFAULT_INTERVAL = 5000; + + /** + * Constant value indicating an unlimited number of attempts. + */ + public static final long UNLIMITED_ATTEMPTS = Long.MAX_VALUE; + + private long interval = DEFAULT_INTERVAL; + + private long maxAttempts = UNLIMITED_ATTEMPTS; + + + /** + * Create an instance with an interval of {@value #DEFAULT_INTERVAL} + * ms and an unlimited number of attempts. + */ + public FixedBackOff() { + } + + /** + * Create an instance. + * @param interval the interval between two attempts + * @param maxAttempts the maximum number of attempts + */ + public FixedBackOff(long interval, long maxAttempts) { + this.interval = interval; + this.maxAttempts = maxAttempts; + } + + + /** + * Set the interval between two attempts in milliseconds. + */ + public void setInterval(long interval) { + this.interval = interval; + } + + /** + * Return the interval between two attempts in milliseconds. + */ + public long getInterval() { + return this.interval; + } + + /** + * Set the maximum number of attempts in milliseconds. + */ + public void setMaxAttempts(long maxAttempts) { + this.maxAttempts = maxAttempts; + } + + /** + * Return the maximum number of attempts in milliseconds. + */ + public long getMaxAttempts() { + return this.maxAttempts; + } + + @Override + public BackOffExecution start() { + return new FixedBackOffExecution(); + } + + + private class FixedBackOffExecution implements BackOffExecution { + + private long currentAttempts = 0; + + @Override + public long nextBackOff() { + this.currentAttempts++; + if (this.currentAttempts <= getMaxAttempts()) { + return getInterval(); + } + else { + return STOP; + } + } + + @Override + public String toString() { + String attemptValue = (FixedBackOff.this.maxAttempts == Long.MAX_VALUE ? + "unlimited" : String.valueOf(FixedBackOff.this.maxAttempts)); + return "FixedBackOff{interval=" + FixedBackOff.this.interval + + ", currentAttempts=" + this.currentAttempts + + ", maxAttempts=" + attemptValue + + '}'; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/backoff/package-info.java b/spring-core/src/main/java/org/springframework/util/backoff/package-info.java new file mode 100644 index 0000000..071542a --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/backoff/package-info.java @@ -0,0 +1,9 @@ +/** + * A generic back-off abstraction. + */ +@NonNullApi +@NonNullFields +package org.springframework.util.backoff; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/main/java/org/springframework/util/comparator/BooleanComparator.java b/spring-core/src/main/java/org/springframework/util/comparator/BooleanComparator.java new file mode 100644 index 0000000..6f59c2e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/comparator/BooleanComparator.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.comparator; + +import java.io.Serializable; +import java.util.Comparator; + +import org.springframework.lang.Nullable; + +/** + * A {@link Comparator} for {@link Boolean} objects that can sort either + * {@code true} or {@code false} first. + * + * @author Keith Donald + * @since 1.2.2 + */ +@SuppressWarnings("serial") +public class BooleanComparator implements Comparator, Serializable { + + /** + * A shared default instance of this comparator, + * treating {@code true} lower than {@code false}. + */ + public static final BooleanComparator TRUE_LOW = new BooleanComparator(true); + + /** + * A shared default instance of this comparator, + * treating {@code true} higher than {@code false}. + */ + public static final BooleanComparator TRUE_HIGH = new BooleanComparator(false); + + + private final boolean trueLow; + + + /** + * Create a BooleanComparator that sorts boolean values based on + * the provided flag. + *

    Alternatively, you can use the default shared instances: + * {@code BooleanComparator.TRUE_LOW} and + * {@code BooleanComparator.TRUE_HIGH}. + * @param trueLow whether to treat true as lower or higher than false + * @see #TRUE_LOW + * @see #TRUE_HIGH + */ + public BooleanComparator(boolean trueLow) { + this.trueLow = trueLow; + } + + + @Override + public int compare(Boolean v1, Boolean v2) { + return (v1 ^ v2) ? ((v1 ^ this.trueLow) ? 1 : -1) : 0; + } + + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof BooleanComparator && + this.trueLow == ((BooleanComparator) other).trueLow)); + } + + @Override + public int hashCode() { + return getClass().hashCode() * (this.trueLow ? -1 : 1); + } + + @Override + public String toString() { + return "BooleanComparator: " + (this.trueLow ? "true low" : "true high"); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/comparator/ComparableComparator.java b/spring-core/src/main/java/org/springframework/util/comparator/ComparableComparator.java new file mode 100644 index 0000000..07e7a9f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/comparator/ComparableComparator.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.comparator; + +import java.util.Comparator; + +/** + * Comparator that adapts Comparables to the Comparator interface. + * Mainly for internal use in other Comparators, when supposed + * to work on Comparables. + * + * @author Keith Donald + * @since 1.2.2 + * @param the type of comparable objects that may be compared by this comparator + * @see Comparable + */ +public class ComparableComparator> implements Comparator { + + /** + * A shared instance of this default comparator. + * @see Comparators#comparable() + */ + @SuppressWarnings("rawtypes") + public static final ComparableComparator INSTANCE = new ComparableComparator(); + + + @Override + public int compare(T o1, T o2) { + return o1.compareTo(o2); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/comparator/Comparators.java b/spring-core/src/main/java/org/springframework/util/comparator/Comparators.java new file mode 100644 index 0000000..543418f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/comparator/Comparators.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.comparator; + +import java.util.Comparator; + +/** + * Convenient entry point with generically typed factory methods + * for common Spring {@link Comparator} variants. + * + * @author Juergen Hoeller + * @since 5.0 + */ +public abstract class Comparators { + + /** + * Return a {@link Comparable} adapter. + * @see ComparableComparator#INSTANCE + */ + @SuppressWarnings("unchecked") + public static Comparator comparable() { + return ComparableComparator.INSTANCE; + } + + /** + * Return a {@link Comparable} adapter which accepts + * null values and sorts them lower than non-null values. + * @see NullSafeComparator#NULLS_LOW + */ + @SuppressWarnings("unchecked") + public static Comparator nullsLow() { + return NullSafeComparator.NULLS_LOW; + } + + /** + * Return a decorator for the given comparator which accepts + * null values and sorts them lower than non-null values. + * @see NullSafeComparator#NullSafeComparator(boolean) + */ + public static Comparator nullsLow(Comparator comparator) { + return new NullSafeComparator<>(comparator, true); + } + + /** + * Return a {@link Comparable} adapter which accepts + * null values and sorts them higher than non-null values. + * @see NullSafeComparator#NULLS_HIGH + */ + @SuppressWarnings("unchecked") + public static Comparator nullsHigh() { + return NullSafeComparator.NULLS_HIGH; + } + + /** + * Return a decorator for the given comparator which accepts + * null values and sorts them higher than non-null values. + * @see NullSafeComparator#NullSafeComparator(boolean) + */ + public static Comparator nullsHigh(Comparator comparator) { + return new NullSafeComparator<>(comparator, false); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/comparator/CompoundComparator.java b/spring-core/src/main/java/org/springframework/util/comparator/CompoundComparator.java new file mode 100644 index 0000000..a7e3565 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/comparator/CompoundComparator.java @@ -0,0 +1,206 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.comparator; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * A comparator that chains a sequence of one or more Comparators. + * + *

    A compound comparator calls each Comparator in sequence until a single + * Comparator returns a non-zero result, or the comparators are exhausted and + * zero is returned. + * + *

    This facilitates in-memory sorting similar to multi-column sorting in SQL. + * The order of any single Comparator in the list can also be reversed. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 1.2.2 + * @param the type of objects that may be compared by this comparator + * @deprecated as of Spring Framework 5.0, in favor of the standard JDK 8 + * {@link Comparator#thenComparing(Comparator)} + */ +@Deprecated +@SuppressWarnings({"serial", "rawtypes"}) +public class CompoundComparator implements Comparator, Serializable { + + private final List comparators; + + + /** + * Construct a CompoundComparator with initially no Comparators. Clients + * must add at least one Comparator before calling the compare method or an + * IllegalStateException is thrown. + */ + public CompoundComparator() { + this.comparators = new ArrayList<>(); + } + + /** + * Construct a CompoundComparator from the Comparators in the provided array. + *

    All Comparators will default to ascending sort order, + * unless they are InvertibleComparators. + * @param comparators the comparators to build into a compound comparator + * @see InvertibleComparator + */ + @SuppressWarnings("unchecked") + public CompoundComparator(Comparator... comparators) { + Assert.notNull(comparators, "Comparators must not be null"); + this.comparators = new ArrayList<>(comparators.length); + for (Comparator comparator : comparators) { + addComparator(comparator); + } + } + + + /** + * Add a Comparator to the end of the chain. + *

    The Comparator will default to ascending sort order, + * unless it is a InvertibleComparator. + * @param comparator the Comparator to add to the end of the chain + * @see InvertibleComparator + */ + @SuppressWarnings("unchecked") + public void addComparator(Comparator comparator) { + if (comparator instanceof InvertibleComparator) { + this.comparators.add((InvertibleComparator) comparator); + } + else { + this.comparators.add(new InvertibleComparator(comparator)); + } + } + + /** + * Add a Comparator to the end of the chain using the provided sort order. + * @param comparator the Comparator to add to the end of the chain + * @param ascending the sort order: ascending (true) or descending (false) + */ + @SuppressWarnings("unchecked") + public void addComparator(Comparator comparator, boolean ascending) { + this.comparators.add(new InvertibleComparator(comparator, ascending)); + } + + /** + * Replace the Comparator at the given index. + *

    The Comparator will default to ascending sort order, + * unless it is a InvertibleComparator. + * @param index the index of the Comparator to replace + * @param comparator the Comparator to place at the given index + * @see InvertibleComparator + */ + @SuppressWarnings("unchecked") + public void setComparator(int index, Comparator comparator) { + if (comparator instanceof InvertibleComparator) { + this.comparators.set(index, (InvertibleComparator) comparator); + } + else { + this.comparators.set(index, new InvertibleComparator(comparator)); + } + } + + /** + * Replace the Comparator at the given index using the given sort order. + * @param index the index of the Comparator to replace + * @param comparator the Comparator to place at the given index + * @param ascending the sort order: ascending (true) or descending (false) + */ + public void setComparator(int index, Comparator comparator, boolean ascending) { + this.comparators.set(index, new InvertibleComparator<>(comparator, ascending)); + } + + /** + * Invert the sort order of each sort definition contained by this compound + * comparator. + */ + public void invertOrder() { + for (InvertibleComparator comparator : this.comparators) { + comparator.invertOrder(); + } + } + + /** + * Invert the sort order of the sort definition at the specified index. + * @param index the index of the comparator to invert + */ + public void invertOrder(int index) { + this.comparators.get(index).invertOrder(); + } + + /** + * Change the sort order at the given index to ascending. + * @param index the index of the comparator to change + */ + public void setAscendingOrder(int index) { + this.comparators.get(index).setAscending(true); + } + + /** + * Change the sort order at the given index to descending sort. + * @param index the index of the comparator to change + */ + public void setDescendingOrder(int index) { + this.comparators.get(index).setAscending(false); + } + + /** + * Returns the number of aggregated comparators. + */ + public int getComparatorCount() { + return this.comparators.size(); + } + + + @Override + @SuppressWarnings("unchecked") + public int compare(T o1, T o2) { + Assert.state(!this.comparators.isEmpty(), + "No sort definitions have been added to this CompoundComparator to compare"); + for (InvertibleComparator comparator : this.comparators) { + int result = comparator.compare(o1, o2); + if (result != 0) { + return result; + } + } + return 0; + } + + + @Override + @SuppressWarnings("unchecked") + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof CompoundComparator && + this.comparators.equals(((CompoundComparator) other).comparators))); + } + + @Override + public int hashCode() { + return this.comparators.hashCode(); + } + + @Override + public String toString() { + return "CompoundComparator: " + this.comparators; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/comparator/InstanceComparator.java b/spring-core/src/main/java/org/springframework/util/comparator/InstanceComparator.java new file mode 100644 index 0000000..66d4b4f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/comparator/InstanceComparator.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.comparator; + +import java.util.Comparator; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Compares objects based on an arbitrary class order. Allows objects to be sorted based + * on the types of class that they inherit — for example, this comparator can be used + * to sort a list of {@code Number}s such that {@code Long}s occur before {@code Integer}s. + * + *

    Only the specified {@code instanceOrder} classes are considered during comparison. + * If two objects are both instances of the ordered type this comparator will return a + * value of {@code 0}. Consider combining with {@link Comparator#thenComparing(Comparator)} + * if additional sorting is required. + * + * @author Phillip Webb + * @since 3.2 + * @param the type of objects that may be compared by this comparator + * @see Comparator#thenComparing(Comparator) + */ +public class InstanceComparator implements Comparator { + + private final Class[] instanceOrder; + + + /** + * Create a new {@link InstanceComparator} instance. + * @param instanceOrder the ordered list of classes that should be used when comparing + * objects. Classes earlier in the list will be given a higher priority. + */ + public InstanceComparator(Class... instanceOrder) { + Assert.notNull(instanceOrder, "'instanceOrder' array must not be null"); + this.instanceOrder = instanceOrder; + } + + + @Override + public int compare(T o1, T o2) { + int i1 = getOrder(o1); + int i2 = getOrder(o2); + return (Integer.compare(i1, i2)); + } + + private int getOrder(@Nullable T object) { + if (object != null) { + for (int i = 0; i < this.instanceOrder.length; i++) { + if (this.instanceOrder[i].isInstance(object)) { + return i; + } + } + } + return this.instanceOrder.length; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/comparator/InvertibleComparator.java b/spring-core/src/main/java/org/springframework/util/comparator/InvertibleComparator.java new file mode 100644 index 0000000..fbb17f6 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/comparator/InvertibleComparator.java @@ -0,0 +1,133 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.comparator; + +import java.io.Serializable; +import java.util.Comparator; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * A decorator for a comparator, with an "ascending" flag denoting + * whether comparison results should be treated in forward (standard + * ascending) order or flipped for reverse (descending) order. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 1.2.2 + * @param the type of objects that may be compared by this comparator + * @deprecated as of Spring Framework 5.0, in favor of the standard JDK 8 + * {@link Comparator#reversed()} + */ +@Deprecated +@SuppressWarnings("serial") +public class InvertibleComparator implements Comparator, Serializable { + + private final Comparator comparator; + + private boolean ascending = true; + + + /** + * Create an InvertibleComparator that sorts ascending by default. + * For the actual comparison, the specified Comparator will be used. + * @param comparator the comparator to decorate + */ + public InvertibleComparator(Comparator comparator) { + Assert.notNull(comparator, "Comparator must not be null"); + this.comparator = comparator; + } + + /** + * Create an InvertibleComparator that sorts based on the provided order. + * For the actual comparison, the specified Comparator will be used. + * @param comparator the comparator to decorate + * @param ascending the sort order: ascending (true) or descending (false) + */ + public InvertibleComparator(Comparator comparator, boolean ascending) { + Assert.notNull(comparator, "Comparator must not be null"); + this.comparator = comparator; + setAscending(ascending); + } + + + /** + * Specify the sort order: ascending (true) or descending (false). + */ + public void setAscending(boolean ascending) { + this.ascending = ascending; + } + + /** + * Return the sort order: ascending (true) or descending (false). + */ + public boolean isAscending() { + return this.ascending; + } + + /** + * Invert the sort order: ascending -> descending or + * descending -> ascending. + */ + public void invertOrder() { + this.ascending = !this.ascending; + } + + + @Override + public int compare(T o1, T o2) { + int result = this.comparator.compare(o1, o2); + if (result != 0) { + // Invert the order if it is a reverse sort. + if (!this.ascending) { + if (Integer.MIN_VALUE == result) { + result = Integer.MAX_VALUE; + } + else { + result *= -1; + } + } + return result; + } + return 0; + } + + @Override + @SuppressWarnings("unchecked") + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof InvertibleComparator)) { + return false; + } + InvertibleComparator otherComp = (InvertibleComparator) other; + return (this.comparator.equals(otherComp.comparator) && this.ascending == otherComp.ascending); + } + + @Override + public int hashCode() { + return this.comparator.hashCode(); + } + + @Override + public String toString() { + return "InvertibleComparator: [" + this.comparator + "]; ascending=" + this.ascending; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/comparator/NullSafeComparator.java b/spring-core/src/main/java/org/springframework/util/comparator/NullSafeComparator.java new file mode 100644 index 0000000..37933af --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/comparator/NullSafeComparator.java @@ -0,0 +1,133 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.comparator; + +import java.util.Comparator; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * A Comparator that will safely compare nulls to be lower or higher than + * other objects. Can decorate a given Comparator or work on Comparables. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 1.2.2 + * @param the type of objects that may be compared by this comparator + * @see Comparable + */ +public class NullSafeComparator implements Comparator { + + /** + * A shared default instance of this comparator, treating nulls lower + * than non-null objects. + * @see Comparators#nullsLow() + */ + @SuppressWarnings("rawtypes") + public static final NullSafeComparator NULLS_LOW = new NullSafeComparator<>(true); + + /** + * A shared default instance of this comparator, treating nulls higher + * than non-null objects. + * @see Comparators#nullsHigh() + */ + @SuppressWarnings("rawtypes") + public static final NullSafeComparator NULLS_HIGH = new NullSafeComparator<>(false); + + + private final Comparator nonNullComparator; + + private final boolean nullsLow; + + + /** + * Create a NullSafeComparator that sorts {@code null} based on + * the provided flag, working on Comparables. + *

    When comparing two non-null objects, their Comparable implementation + * will be used: this means that non-null elements (that this Comparator + * will be applied to) need to implement Comparable. + *

    As a convenience, you can use the default shared instances: + * {@code NullSafeComparator.NULLS_LOW} and + * {@code NullSafeComparator.NULLS_HIGH}. + * @param nullsLow whether to treat nulls lower or higher than non-null objects + * @see Comparable + * @see #NULLS_LOW + * @see #NULLS_HIGH + */ + @SuppressWarnings("unchecked") + private NullSafeComparator(boolean nullsLow) { + this.nonNullComparator = ComparableComparator.INSTANCE; + this.nullsLow = nullsLow; + } + + /** + * Create a NullSafeComparator that sorts {@code null} based on the + * provided flag, decorating the given Comparator. + *

    When comparing two non-null objects, the specified Comparator will be used. + * The given underlying Comparator must be able to handle the elements that this + * Comparator will be applied to. + * @param comparator the comparator to use when comparing two non-null objects + * @param nullsLow whether to treat nulls lower or higher than non-null objects + */ + public NullSafeComparator(Comparator comparator, boolean nullsLow) { + Assert.notNull(comparator, "Non-null Comparator is required"); + this.nonNullComparator = comparator; + this.nullsLow = nullsLow; + } + + + @Override + public int compare(@Nullable T o1, @Nullable T o2) { + if (o1 == o2) { + return 0; + } + if (o1 == null) { + return (this.nullsLow ? -1 : 1); + } + if (o2 == null) { + return (this.nullsLow ? 1 : -1); + } + return this.nonNullComparator.compare(o1, o2); + } + + + @Override + @SuppressWarnings("unchecked") + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof NullSafeComparator)) { + return false; + } + NullSafeComparator otherComp = (NullSafeComparator) other; + return (this.nonNullComparator.equals(otherComp.nonNullComparator) && this.nullsLow == otherComp.nullsLow); + } + + @Override + public int hashCode() { + return this.nonNullComparator.hashCode() * (this.nullsLow ? -1 : 1); + } + + @Override + public String toString() { + return "NullSafeComparator: non-null comparator [" + this.nonNullComparator + "]; " + + (this.nullsLow ? "nulls low" : "nulls high"); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/comparator/package-info.java b/spring-core/src/main/java/org/springframework/util/comparator/package-info.java new file mode 100644 index 0000000..3d4ebd5 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/comparator/package-info.java @@ -0,0 +1,10 @@ +/** + * Useful generic {@code java.util.Comparator} implementations, + * such as an invertible comparator and a compound comparator. + */ +@NonNullApi +@NonNullFields +package org.springframework.util.comparator; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/CompletableToListenableFutureAdapter.java b/spring-core/src/main/java/org/springframework/util/concurrent/CompletableToListenableFutureAdapter.java new file mode 100644 index 0000000..4f10d36 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/concurrent/CompletableToListenableFutureAdapter.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.concurrent; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * Adapts a {@link CompletableFuture} or {@link CompletionStage} into a + * Spring {@link ListenableFuture}. + * + * @author Sebastien Deleuze + * @author Juergen Hoeller + * @since 4.2 + * @param the result type returned by this Future's {@code get} method + */ +public class CompletableToListenableFutureAdapter implements ListenableFuture { + + private final CompletableFuture completableFuture; + + private final ListenableFutureCallbackRegistry callbacks = new ListenableFutureCallbackRegistry<>(); + + + /** + * Create a new adapter for the given {@link CompletionStage}. + * @since 4.3.7 + */ + public CompletableToListenableFutureAdapter(CompletionStage completionStage) { + this(completionStage.toCompletableFuture()); + } + + /** + * Create a new adapter for the given {@link CompletableFuture}. + */ + public CompletableToListenableFutureAdapter(CompletableFuture completableFuture) { + this.completableFuture = completableFuture; + this.completableFuture.whenComplete((result, ex) -> { + if (ex != null) { + this.callbacks.failure(ex); + } + else { + this.callbacks.success(result); + } + }); + } + + + @Override + public void addCallback(ListenableFutureCallback callback) { + this.callbacks.addCallback(callback); + } + + @Override + public void addCallback(SuccessCallback successCallback, FailureCallback failureCallback) { + this.callbacks.addSuccessCallback(successCallback); + this.callbacks.addFailureCallback(failureCallback); + } + + @Override + public CompletableFuture completable() { + return this.completableFuture; + } + + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return this.completableFuture.cancel(mayInterruptIfRunning); + } + + @Override + public boolean isCancelled() { + return this.completableFuture.isCancelled(); + } + + @Override + public boolean isDone() { + return this.completableFuture.isDone(); + } + + @Override + public T get() throws InterruptedException, ExecutionException { + return this.completableFuture.get(); + } + + @Override + public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return this.completableFuture.get(timeout, unit); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/DelegatingCompletableFuture.java b/spring-core/src/main/java/org/springframework/util/concurrent/DelegatingCompletableFuture.java new file mode 100644 index 0000000..92ff748 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/concurrent/DelegatingCompletableFuture.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.concurrent; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; + +import org.springframework.util.Assert; + +/** + * Extension of {@link CompletableFuture} which allows for cancelling + * a delegate along with the {@link CompletableFuture} itself. + * + * @author Juergen Hoeller + * @since 5.0 + * @param the result type returned by this Future's {@code get} method + */ +class DelegatingCompletableFuture extends CompletableFuture { + + private final Future delegate; + + + public DelegatingCompletableFuture(Future delegate) { + Assert.notNull(delegate, "Delegate must not be null"); + this.delegate = delegate; + } + + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + boolean result = this.delegate.cancel(mayInterruptIfRunning); + super.cancel(mayInterruptIfRunning); + return result; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/FailureCallback.java b/spring-core/src/main/java/org/springframework/util/concurrent/FailureCallback.java new file mode 100644 index 0000000..cefe6db --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/concurrent/FailureCallback.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.concurrent; + +/** + * Failure callback for a {@link ListenableFuture}. + * + * @author Sebastien Deleuze + * @since 4.1 + */ +@FunctionalInterface +public interface FailureCallback { + + /** + * Called when the {@link ListenableFuture} completes with failure. + *

    Note that Exceptions raised by this method are ignored. + * @param ex the failure + */ + void onFailure(Throwable ex); + +} diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/FutureAdapter.java b/spring-core/src/main/java/org/springframework/util/concurrent/FutureAdapter.java new file mode 100644 index 0000000..4de0b46 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/concurrent/FutureAdapter.java @@ -0,0 +1,137 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.concurrent; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Abstract class that adapts a {@link Future} parameterized over S into a {@code Future} + * parameterized over T. All methods are delegated to the adaptee, where {@link #get()} + * and {@link #get(long, TimeUnit)} call {@link #adapt(Object)} on the adaptee's result. + * + * @author Arjen Poutsma + * @since 4.0 + * @param the type of this {@code Future} + * @param the type of the adaptee's {@code Future} + */ +public abstract class FutureAdapter implements Future { + + private final Future adaptee; + + @Nullable + private Object result; + + private State state = State.NEW; + + private final Object mutex = new Object(); + + + /** + * Constructs a new {@code FutureAdapter} with the given adaptee. + * @param adaptee the future to delegate to + */ + protected FutureAdapter(Future adaptee) { + Assert.notNull(adaptee, "Delegate must not be null"); + this.adaptee = adaptee; + } + + + /** + * Returns the adaptee. + */ + protected Future getAdaptee() { + return this.adaptee; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return this.adaptee.cancel(mayInterruptIfRunning); + } + + @Override + public boolean isCancelled() { + return this.adaptee.isCancelled(); + } + + @Override + public boolean isDone() { + return this.adaptee.isDone(); + } + + @Override + @Nullable + public T get() throws InterruptedException, ExecutionException { + return adaptInternal(this.adaptee.get()); + } + + @Override + @Nullable + public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return adaptInternal(this.adaptee.get(timeout, unit)); + } + + @SuppressWarnings("unchecked") + @Nullable + final T adaptInternal(S adapteeResult) throws ExecutionException { + synchronized (this.mutex) { + switch (this.state) { + case SUCCESS: + return (T) this.result; + case FAILURE: + Assert.state(this.result instanceof ExecutionException, "Failure without exception"); + throw (ExecutionException) this.result; + case NEW: + try { + T adapted = adapt(adapteeResult); + this.result = adapted; + this.state = State.SUCCESS; + return adapted; + } + catch (ExecutionException ex) { + this.result = ex; + this.state = State.FAILURE; + throw ex; + } + catch (Throwable ex) { + ExecutionException execEx = new ExecutionException(ex); + this.result = execEx; + this.state = State.FAILURE; + throw execEx; + } + default: + throw new IllegalStateException(); + } + } + } + + /** + * Adapts the given adaptee's result into T. + * @return the adapted result + */ + @Nullable + protected abstract T adapt(S adapteeResult) throws ExecutionException; + + + private enum State {NEW, SUCCESS, FAILURE} + +} diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFuture.java b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFuture.java new file mode 100644 index 0000000..6b3bcb4 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFuture.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.concurrent; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; + +/** + * Extend {@link Future} with the capability to accept completion callbacks. + * If the future has completed when the callback is added, the callback is + * triggered immediately. + * + *

    Inspired by {@code com.google.common.util.concurrent.ListenableFuture}. + * + * @author Arjen Poutsma + * @author Sebastien Deleuze + * @author Juergen Hoeller + * @since 4.0 + * @param the result type returned by this Future's {@code get} method + */ +public interface ListenableFuture extends Future { + + /** + * Register the given {@code ListenableFutureCallback}. + * @param callback the callback to register + */ + void addCallback(ListenableFutureCallback callback); + + /** + * Java 8 lambda-friendly alternative with success and failure callbacks. + * @param successCallback the success callback + * @param failureCallback the failure callback + * @since 4.1 + */ + void addCallback(SuccessCallback successCallback, FailureCallback failureCallback); + + + /** + * Expose this {@link ListenableFuture} as a JDK {@link CompletableFuture}. + * @since 5.0 + */ + default CompletableFuture completable() { + CompletableFuture completable = new DelegatingCompletableFuture<>(this); + addCallback(completable::complete, completable::completeExceptionally); + return completable; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureAdapter.java b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureAdapter.java new file mode 100644 index 0000000..3223646 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureAdapter.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.concurrent; + +import java.util.concurrent.ExecutionException; + +import org.springframework.lang.Nullable; + +/** + * Abstract class that adapts a {@link ListenableFuture} parameterized over S into a + * {@code ListenableFuture} parameterized over T. All methods are delegated to the + * adaptee, where {@link #get()}, {@link #get(long, java.util.concurrent.TimeUnit)}, + * and {@link ListenableFutureCallback#onSuccess(Object)} call {@link #adapt(Object)} + * on the adaptee's result. + * + * @author Arjen Poutsma + * @since 4.0 + * @param the type of this {@code Future} + * @param the type of the adaptee's {@code Future} + */ +public abstract class ListenableFutureAdapter extends FutureAdapter implements ListenableFuture { + + /** + * Construct a new {@code ListenableFutureAdapter} with the given adaptee. + * @param adaptee the future to adapt to + */ + protected ListenableFutureAdapter(ListenableFuture adaptee) { + super(adaptee); + } + + + @Override + public void addCallback(final ListenableFutureCallback callback) { + addCallback(callback, callback); + } + + @Override + public void addCallback(final SuccessCallback successCallback, final FailureCallback failureCallback) { + ListenableFuture listenableAdaptee = (ListenableFuture) getAdaptee(); + listenableAdaptee.addCallback(new ListenableFutureCallback() { + @Override + public void onSuccess(@Nullable S result) { + T adapted = null; + if (result != null) { + try { + adapted = adaptInternal(result); + } + catch (ExecutionException ex) { + Throwable cause = ex.getCause(); + onFailure(cause != null ? cause : ex); + return; + } + catch (Throwable ex) { + onFailure(ex); + return; + } + } + successCallback.onSuccess(adapted); + } + @Override + public void onFailure(Throwable ex) { + failureCallback.onFailure(ex); + } + }); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureCallback.java b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureCallback.java new file mode 100644 index 0000000..215d136 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureCallback.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.concurrent; + +/** + * Callback mechanism for the outcome, success or failure, from a + * {@link ListenableFuture}. + * + * @author Arjen Poutsma + * @author Sebastien Deleuze + * @since 4.0 + * @param the result type + */ +public interface ListenableFutureCallback extends SuccessCallback, FailureCallback { + +} diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureCallbackRegistry.java b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureCallbackRegistry.java new file mode 100644 index 0000000..d3be92b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureCallbackRegistry.java @@ -0,0 +1,166 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.concurrent; + +import java.util.ArrayDeque; +import java.util.Queue; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Helper class for {@link ListenableFuture} implementations that maintains a + * of success and failure callbacks and helps to notify them. + * + *

    Inspired by {@code com.google.common.util.concurrent.ExecutionList}. + * + * @author Arjen Poutsma + * @author Sebastien Deleuze + * @author Rossen Stoyanchev + * @since 4.0 + * @param the callback result type + */ +public class ListenableFutureCallbackRegistry { + + private final Queue> successCallbacks = new ArrayDeque<>(1); + + private final Queue failureCallbacks = new ArrayDeque<>(1); + + private State state = State.NEW; + + @Nullable + private Object result; + + private final Object mutex = new Object(); + + + /** + * Add the given callback to this registry. + * @param callback the callback to add + */ + public void addCallback(ListenableFutureCallback callback) { + Assert.notNull(callback, "'callback' must not be null"); + synchronized (this.mutex) { + switch (this.state) { + case NEW: + this.successCallbacks.add(callback); + this.failureCallbacks.add(callback); + break; + case SUCCESS: + notifySuccess(callback); + break; + case FAILURE: + notifyFailure(callback); + break; + } + } + } + + @SuppressWarnings("unchecked") + private void notifySuccess(SuccessCallback callback) { + try { + callback.onSuccess((T) this.result); + } + catch (Throwable ex) { + // Ignore + } + } + + private void notifyFailure(FailureCallback callback) { + Assert.state(this.result instanceof Throwable, "No Throwable result for failure state"); + try { + callback.onFailure((Throwable) this.result); + } + catch (Throwable ex) { + // Ignore + } + } + + /** + * Add the given success callback to this registry. + * @param callback the success callback to add + * @since 4.1 + */ + public void addSuccessCallback(SuccessCallback callback) { + Assert.notNull(callback, "'callback' must not be null"); + synchronized (this.mutex) { + switch (this.state) { + case NEW: + this.successCallbacks.add(callback); + break; + case SUCCESS: + notifySuccess(callback); + break; + } + } + } + + /** + * Add the given failure callback to this registry. + * @param callback the failure callback to add + * @since 4.1 + */ + public void addFailureCallback(FailureCallback callback) { + Assert.notNull(callback, "'callback' must not be null"); + synchronized (this.mutex) { + switch (this.state) { + case NEW: + this.failureCallbacks.add(callback); + break; + case FAILURE: + notifyFailure(callback); + break; + } + } + } + + /** + * Trigger a {@link ListenableFutureCallback#onSuccess(Object)} call on all + * added callbacks with the given result. + * @param result the result to trigger the callbacks with + */ + public void success(@Nullable T result) { + synchronized (this.mutex) { + this.state = State.SUCCESS; + this.result = result; + SuccessCallback callback; + while ((callback = this.successCallbacks.poll()) != null) { + notifySuccess(callback); + } + } + } + + /** + * Trigger a {@link ListenableFutureCallback#onFailure(Throwable)} call on all + * added callbacks with the given {@code Throwable}. + * @param ex the exception to trigger the callbacks with + */ + public void failure(Throwable ex) { + synchronized (this.mutex) { + this.state = State.FAILURE; + this.result = ex; + FailureCallback callback; + while ((callback = this.failureCallbacks.poll()) != null) { + notifyFailure(callback); + } + } + } + + + private enum State {NEW, SUCCESS, FAILURE} + +} diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureTask.java b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureTask.java new file mode 100644 index 0000000..fd517aa --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureTask.java @@ -0,0 +1,103 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.concurrent; + +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; + +import org.springframework.lang.Nullable; + +/** + * Extension of {@link FutureTask} that implements {@link ListenableFuture}. + * + * @author Arjen Poutsma + * @since 4.0 + * @param the result type returned by this Future's {@code get} method + */ +public class ListenableFutureTask extends FutureTask implements ListenableFuture { + + private final ListenableFutureCallbackRegistry callbacks = new ListenableFutureCallbackRegistry<>(); + + + /** + * Create a new {@code ListenableFutureTask} that will, upon running, + * execute the given {@link Callable}. + * @param callable the callable task + */ + public ListenableFutureTask(Callable callable) { + super(callable); + } + + /** + * Create a {@code ListenableFutureTask} that will, upon running, + * execute the given {@link Runnable}, and arrange that {@link #get()} + * will return the given result on successful completion. + * @param runnable the runnable task + * @param result the result to return on successful completion + */ + public ListenableFutureTask(Runnable runnable, @Nullable T result) { + super(runnable, result); + } + + + @Override + public void addCallback(ListenableFutureCallback callback) { + this.callbacks.addCallback(callback); + } + + @Override + public void addCallback(SuccessCallback successCallback, FailureCallback failureCallback) { + this.callbacks.addSuccessCallback(successCallback); + this.callbacks.addFailureCallback(failureCallback); + } + + @Override + public CompletableFuture completable() { + CompletableFuture completable = new DelegatingCompletableFuture<>(this); + this.callbacks.addSuccessCallback(completable::complete); + this.callbacks.addFailureCallback(completable::completeExceptionally); + return completable; + } + + + @Override + protected void done() { + Throwable cause; + try { + T result = get(); + this.callbacks.success(result); + return; + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + return; + } + catch (ExecutionException ex) { + cause = ex.getCause(); + if (cause == null) { + cause = ex; + } + } + catch (Throwable ex) { + cause = ex; + } + this.callbacks.failure(cause); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/MonoToListenableFutureAdapter.java b/spring-core/src/main/java/org/springframework/util/concurrent/MonoToListenableFutureAdapter.java new file mode 100644 index 0000000..5f58ae1 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/concurrent/MonoToListenableFutureAdapter.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.concurrent; + +import reactor.core.publisher.Mono; + +/** + * Adapts a {@link Mono} into a {@link ListenableFuture} by obtaining a + * {@code CompletableFuture} from the {@code Mono} via {@link Mono#toFuture()} + * and then adapting it with {@link CompletableToListenableFutureAdapter}. + * + * @author Rossen Stoyanchev + * @author Stephane Maldini + * @since 5.1 + * @param the object type + */ +public class MonoToListenableFutureAdapter extends CompletableToListenableFutureAdapter { + + public MonoToListenableFutureAdapter(Mono mono) { + super(mono.toFuture()); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/SettableListenableFuture.java b/spring-core/src/main/java/org/springframework/util/concurrent/SettableListenableFuture.java new file mode 100644 index 0000000..e046fbd --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/concurrent/SettableListenableFuture.java @@ -0,0 +1,187 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.concurrent; + +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * A {@link ListenableFuture} whose value can be set via {@link #set(Object)} + * or {@link #setException(Throwable)}. It may also get cancelled. + * + *

    Inspired by {@code com.google.common.util.concurrent.SettableFuture}. + * + * @author Mattias Severson + * @author Rossen Stoyanchev + * @author Juergen Hoeller + * @since 4.1 + * @param the result type returned by this Future's {@code get} method + */ +public class SettableListenableFuture implements ListenableFuture { + + private static final Callable DUMMY_CALLABLE = () -> { + throw new IllegalStateException("Should never be called"); + }; + + + private final SettableTask settableTask = new SettableTask<>(); + + + /** + * Set the value of this future. This method will return {@code true} if the + * value was set successfully, or {@code false} if the future has already been + * set or cancelled. + * @param value the value that will be set + * @return {@code true} if the value was successfully set, else {@code false} + */ + public boolean set(@Nullable T value) { + return this.settableTask.setResultValue(value); + } + + /** + * Set the exception of this future. This method will return {@code true} if the + * exception was set successfully, or {@code false} if the future has already been + * set or cancelled. + * @param exception the value that will be set + * @return {@code true} if the exception was successfully set, else {@code false} + */ + public boolean setException(Throwable exception) { + Assert.notNull(exception, "Exception must not be null"); + return this.settableTask.setExceptionResult(exception); + } + + + @Override + public void addCallback(ListenableFutureCallback callback) { + this.settableTask.addCallback(callback); + } + + @Override + public void addCallback(SuccessCallback successCallback, FailureCallback failureCallback) { + this.settableTask.addCallback(successCallback, failureCallback); + } + + @Override + public CompletableFuture completable() { + return this.settableTask.completable(); + } + + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + boolean cancelled = this.settableTask.cancel(mayInterruptIfRunning); + if (cancelled && mayInterruptIfRunning) { + interruptTask(); + } + return cancelled; + } + + @Override + public boolean isCancelled() { + return this.settableTask.isCancelled(); + } + + @Override + public boolean isDone() { + return this.settableTask.isDone(); + } + + /** + * Retrieve the value. + *

    This method returns the value if it has been set via {@link #set(Object)}, + * throws an {@link java.util.concurrent.ExecutionException} if an exception has + * been set via {@link #setException(Throwable)}, or throws a + * {@link java.util.concurrent.CancellationException} if the future has been cancelled. + * @return the value associated with this future + */ + @Override + public T get() throws InterruptedException, ExecutionException { + return this.settableTask.get(); + } + + /** + * Retrieve the value. + *

    This method returns the value if it has been set via {@link #set(Object)}, + * throws an {@link java.util.concurrent.ExecutionException} if an exception has + * been set via {@link #setException(Throwable)}, or throws a + * {@link java.util.concurrent.CancellationException} if the future has been cancelled. + * @param timeout the maximum time to wait + * @param unit the unit of the timeout argument + * @return the value associated with this future + */ + @Override + public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return this.settableTask.get(timeout, unit); + } + + /** + * Subclasses can override this method to implement interruption of the future's + * computation. The method is invoked automatically by a successful call to + * {@link #cancel(boolean) cancel(true)}. + *

    The default implementation is empty. + */ + protected void interruptTask() { + } + + + private static class SettableTask extends ListenableFutureTask { + + @Nullable + private volatile Thread completingThread; + + @SuppressWarnings("unchecked") + public SettableTask() { + super((Callable) DUMMY_CALLABLE); + } + + public boolean setResultValue(@Nullable T value) { + set(value); + return checkCompletingThread(); + } + + public boolean setExceptionResult(Throwable exception) { + setException(exception); + return checkCompletingThread(); + } + + @Override + protected void done() { + if (!isCancelled()) { + // Implicitly invoked by set/setException: store current thread for + // determining whether the given result has actually triggered completion + // (since FutureTask.set/setException unfortunately don't expose that) + this.completingThread = Thread.currentThread(); + } + super.done(); + } + + private boolean checkCompletingThread() { + boolean check = (this.completingThread == Thread.currentThread()); + if (check) { + this.completingThread = null; // only first match actually counts + } + return check; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/SuccessCallback.java b/spring-core/src/main/java/org/springframework/util/concurrent/SuccessCallback.java new file mode 100644 index 0000000..fbed76e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/concurrent/SuccessCallback.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.concurrent; + +import org.springframework.lang.Nullable; + +/** + * Success callback for a {@link ListenableFuture}. + * + * @author Sebastien Deleuze + * @since 4.1 + * @param the result type + */ +@FunctionalInterface +public interface SuccessCallback { + + /** + * Called when the {@link ListenableFuture} completes with success. + *

    Note that Exceptions raised by this method are ignored. + * @param result the result + */ + void onSuccess(@Nullable T result); + +} diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/package-info.java b/spring-core/src/main/java/org/springframework/util/concurrent/package-info.java new file mode 100644 index 0000000..fc4777e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/concurrent/package-info.java @@ -0,0 +1,9 @@ +/** + * Useful generic {@code java.util.concurrent.Future} extensions. + */ +@NonNullApi +@NonNullFields +package org.springframework.util.concurrent; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/main/java/org/springframework/util/function/SingletonSupplier.java b/spring-core/src/main/java/org/springframework/util/function/SingletonSupplier.java new file mode 100644 index 0000000..cf63e6c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/function/SingletonSupplier.java @@ -0,0 +1,159 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.function; + +import java.util.function.Supplier; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * A {@link java.util.function.Supplier} decorator that caches a singleton result and + * makes it available from {@link #get()} (nullable) and {@link #obtain()} (null-safe). + * + *

    A {@code SingletonSupplier} can be constructed via {@code of} factory methods + * or via constructors that provide a default supplier as a fallback. This is + * particularly useful for method reference suppliers, falling back to a default + * supplier for a method that returned {@code null} and caching the result. + * + * @author Juergen Hoeller + * @since 5.1 + * @param the type of results supplied by this supplier + */ +public class SingletonSupplier implements Supplier { + + @Nullable + private final Supplier instanceSupplier; + + @Nullable + private final Supplier defaultSupplier; + + @Nullable + private volatile T singletonInstance; + + + /** + * Build a {@code SingletonSupplier} with the given singleton instance + * and a default supplier for the case when the instance is {@code null}. + * @param instance the singleton instance (potentially {@code null}) + * @param defaultSupplier the default supplier as a fallback + */ + public SingletonSupplier(@Nullable T instance, Supplier defaultSupplier) { + this.instanceSupplier = null; + this.defaultSupplier = defaultSupplier; + this.singletonInstance = instance; + } + + /** + * Build a {@code SingletonSupplier} with the given instance supplier + * and a default supplier for the case when the instance is {@code null}. + * @param instanceSupplier the immediate instance supplier + * @param defaultSupplier the default supplier as a fallback + */ + public SingletonSupplier(@Nullable Supplier instanceSupplier, Supplier defaultSupplier) { + this.instanceSupplier = instanceSupplier; + this.defaultSupplier = defaultSupplier; + } + + private SingletonSupplier(Supplier supplier) { + this.instanceSupplier = supplier; + this.defaultSupplier = null; + } + + private SingletonSupplier(T singletonInstance) { + this.instanceSupplier = null; + this.defaultSupplier = null; + this.singletonInstance = singletonInstance; + } + + + /** + * Get the shared singleton instance for this supplier. + * @return the singleton instance (or {@code null} if none) + */ + @Override + @Nullable + public T get() { + T instance = this.singletonInstance; + if (instance == null) { + synchronized (this) { + instance = this.singletonInstance; + if (instance == null) { + if (this.instanceSupplier != null) { + instance = this.instanceSupplier.get(); + } + if (instance == null && this.defaultSupplier != null) { + instance = this.defaultSupplier.get(); + } + this.singletonInstance = instance; + } + } + } + return instance; + } + + /** + * Obtain the shared singleton instance for this supplier. + * @return the singleton instance (never {@code null}) + * @throws IllegalStateException in case of no instance + */ + public T obtain() { + T instance = get(); + Assert.state(instance != null, "No instance from Supplier"); + return instance; + } + + + /** + * Build a {@code SingletonSupplier} with the given singleton instance. + * @param instance the singleton instance (never {@code null}) + * @return the singleton supplier (never {@code null}) + */ + public static SingletonSupplier of(T instance) { + return new SingletonSupplier<>(instance); + } + + /** + * Build a {@code SingletonSupplier} with the given singleton instance. + * @param instance the singleton instance (potentially {@code null}) + * @return the singleton supplier, or {@code null} if the instance was {@code null} + */ + @Nullable + public static SingletonSupplier ofNullable(@Nullable T instance) { + return (instance != null ? new SingletonSupplier<>(instance) : null); + } + + /** + * Build a {@code SingletonSupplier} with the given supplier. + * @param supplier the instance supplier (never {@code null}) + * @return the singleton supplier (never {@code null}) + */ + public static SingletonSupplier of(Supplier supplier) { + return new SingletonSupplier<>(supplier); + } + + /** + * Build a {@code SingletonSupplier} with the given supplier. + * @param supplier the instance supplier (potentially {@code null}) + * @return the singleton supplier, or {@code null} if the instance supplier was {@code null} + */ + @Nullable + public static SingletonSupplier ofNullable(@Nullable Supplier supplier) { + return (supplier != null ? new SingletonSupplier<>(supplier) : null); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/function/SupplierUtils.java b/spring-core/src/main/java/org/springframework/util/function/SupplierUtils.java new file mode 100644 index 0000000..22f0863 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/function/SupplierUtils.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.function; + +import java.util.function.Supplier; + +import org.springframework.lang.Nullable; + +/** + * Convenience utilities for {@link java.util.function.Supplier} handling. + * + * @author Juergen Hoeller + * @since 5.1 + * @see SingletonSupplier + */ +public abstract class SupplierUtils { + + /** + * Resolve the given {@code Supplier}, getting its result or immediately + * returning {@code null} if the supplier itself was {@code null}. + * @param supplier the supplier to resolve + * @return the supplier's result, or {@code null} if none + */ + @Nullable + public static T resolve(@Nullable Supplier supplier) { + return (supplier != null ? supplier.get() : null); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/function/package-info.java b/spring-core/src/main/java/org/springframework/util/function/package-info.java new file mode 100644 index 0000000..7c16649 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/function/package-info.java @@ -0,0 +1,9 @@ +/** + * Useful generic {@code java.util.function} helper classes. + */ +@NonNullApi +@NonNullFields +package org.springframework.util.function; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/main/java/org/springframework/util/package-info.java b/spring-core/src/main/java/org/springframework/util/package-info.java new file mode 100644 index 0000000..93237c0 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/package-info.java @@ -0,0 +1,10 @@ +/** + * Miscellaneous utility classes, such as String manipulation utilities, + * a Log4J configurer, and a state holder for paged lists of objects. + */ +@NonNullApi +@NonNullFields +package org.springframework.util; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/main/java/org/springframework/util/unit/DataSize.java b/spring-core/src/main/java/org/springframework/util/unit/DataSize.java new file mode 100644 index 0000000..976ee1b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/unit/DataSize.java @@ -0,0 +1,274 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.unit; + +import java.io.Serializable; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * A data size, such as '12MB'. + * + *

    This class models data size in terms of bytes and is immutable and thread-safe. + * + *

    The terms and units used in this class are based on + * binary prefixes + * indicating multiplication by powers of 2. Consult the following table and + * the Javadoc for {@link DataUnit} for details. + * + *

    + * + * + * + * + * + * + * + *
    TermData SizeSize in Bytes
    byte1B1
    kilobyte1KB1,024
    megabyte1MB1,048,576
    gigabyte1GB1,073,741,824
    terabyte1TB1,099,511,627,776
    + * + * @author Stephane Nicoll + * @author Sam Brannen + * @since 5.1 + * @see DataUnit + */ +@SuppressWarnings("serial") +public final class DataSize implements Comparable, Serializable { + + /** + * The pattern for parsing. + */ + private static final Pattern PATTERN = Pattern.compile("^([+\\-]?\\d+)([a-zA-Z]{0,2})$"); + + /** + * Bytes per Kilobyte. + */ + private static final long BYTES_PER_KB = 1024; + + /** + * Bytes per Megabyte. + */ + private static final long BYTES_PER_MB = BYTES_PER_KB * 1024; + + /** + * Bytes per Gigabyte. + */ + private static final long BYTES_PER_GB = BYTES_PER_MB * 1024; + + /** + * Bytes per Terabyte. + */ + private static final long BYTES_PER_TB = BYTES_PER_GB * 1024; + + + private final long bytes; + + + private DataSize(long bytes) { + this.bytes = bytes; + } + + + /** + * Obtain a {@link DataSize} representing the specified number of bytes. + * @param bytes the number of bytes, positive or negative + * @return a {@link DataSize} + */ + public static DataSize ofBytes(long bytes) { + return new DataSize(bytes); + } + + /** + * Obtain a {@link DataSize} representing the specified number of kilobytes. + * @param kilobytes the number of kilobytes, positive or negative + * @return a {@link DataSize} + */ + public static DataSize ofKilobytes(long kilobytes) { + return new DataSize(Math.multiplyExact(kilobytes, BYTES_PER_KB)); + } + + /** + * Obtain a {@link DataSize} representing the specified number of megabytes. + * @param megabytes the number of megabytes, positive or negative + * @return a {@link DataSize} + */ + public static DataSize ofMegabytes(long megabytes) { + return new DataSize(Math.multiplyExact(megabytes, BYTES_PER_MB)); + } + + /** + * Obtain a {@link DataSize} representing the specified number of gigabytes. + * @param gigabytes the number of gigabytes, positive or negative + * @return a {@link DataSize} + */ + public static DataSize ofGigabytes(long gigabytes) { + return new DataSize(Math.multiplyExact(gigabytes, BYTES_PER_GB)); + } + + /** + * Obtain a {@link DataSize} representing the specified number of terabytes. + * @param terabytes the number of terabytes, positive or negative + * @return a {@link DataSize} + */ + public static DataSize ofTerabytes(long terabytes) { + return new DataSize(Math.multiplyExact(terabytes, BYTES_PER_TB)); + } + + /** + * Obtain a {@link DataSize} representing an amount in the specified {@link DataUnit}. + * @param amount the amount of the size, measured in terms of the unit, + * positive or negative + * @return a corresponding {@link DataSize} + */ + public static DataSize of(long amount, DataUnit unit) { + Assert.notNull(unit, "Unit must not be null"); + return new DataSize(Math.multiplyExact(amount, unit.size().toBytes())); + } + + /** + * Obtain a {@link DataSize} from a text string such as {@code 12MB} using + * {@link DataUnit#BYTES} if no unit is specified. + *

    + * Examples: + *

    +	 * "12KB" -- parses as "12 kilobytes"
    +	 * "5MB"  -- parses as "5 megabytes"
    +	 * "20"   -- parses as "20 bytes"
    +	 * 
    + * @param text the text to parse + * @return the parsed {@link DataSize} + * @see #parse(CharSequence, DataUnit) + */ + public static DataSize parse(CharSequence text) { + return parse(text, null); + } + + /** + * Obtain a {@link DataSize} from a text string such as {@code 12MB} using + * the specified default {@link DataUnit} if no unit is specified. + *

    + * The string starts with a number followed optionally by a unit matching one of the + * supported {@linkplain DataUnit suffixes}. + *

    + * Examples: + *

    +	 * "12KB" -- parses as "12 kilobytes"
    +	 * "5MB"  -- parses as "5 megabytes"
    +	 * "20"   -- parses as "20 kilobytes" (where the {@code defaultUnit} is {@link DataUnit#KILOBYTES})
    +	 * 
    + * @param text the text to parse + * @return the parsed {@link DataSize} + */ + public static DataSize parse(CharSequence text, @Nullable DataUnit defaultUnit) { + Assert.notNull(text, "Text must not be null"); + try { + Matcher matcher = PATTERN.matcher(text); + Assert.state(matcher.matches(), "Does not match data size pattern"); + DataUnit unit = determineDataUnit(matcher.group(2), defaultUnit); + long amount = Long.parseLong(matcher.group(1)); + return DataSize.of(amount, unit); + } + catch (Exception ex) { + throw new IllegalArgumentException("'" + text + "' is not a valid data size", ex); + } + } + + private static DataUnit determineDataUnit(String suffix, @Nullable DataUnit defaultUnit) { + DataUnit defaultUnitToUse = (defaultUnit != null ? defaultUnit : DataUnit.BYTES); + return (StringUtils.hasLength(suffix) ? DataUnit.fromSuffix(suffix) : defaultUnitToUse); + } + + /** + * Checks if this size is negative, excluding zero. + * @return true if this size has a size less than zero bytes + */ + public boolean isNegative() { + return this.bytes < 0; + } + + /** + * Return the number of bytes in this instance. + * @return the number of bytes + */ + public long toBytes() { + return this.bytes; + } + + /** + * Return the number of kilobytes in this instance. + * @return the number of kilobytes + */ + public long toKilobytes() { + return this.bytes / BYTES_PER_KB; + } + + /** + * Return the number of megabytes in this instance. + * @return the number of megabytes + */ + public long toMegabytes() { + return this.bytes / BYTES_PER_MB; + } + + /** + * Return the number of gigabytes in this instance. + * @return the number of gigabytes + */ + public long toGigabytes() { + return this.bytes / BYTES_PER_GB; + } + + /** + * Return the number of terabytes in this instance. + * @return the number of terabytes + */ + public long toTerabytes() { + return this.bytes / BYTES_PER_TB; + } + + @Override + public int compareTo(DataSize other) { + return Long.compare(this.bytes, other.bytes); + } + + @Override + public String toString() { + return String.format("%dB", this.bytes); + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + DataSize otherSize = (DataSize) other; + return (this.bytes == otherSize.bytes); + } + + @Override + public int hashCode() { + return Long.hashCode(this.bytes); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/unit/DataUnit.java b/spring-core/src/main/java/org/springframework/util/unit/DataUnit.java new file mode 100644 index 0000000..8b5dc43 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/unit/DataUnit.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.unit; + +/** + * A standard set of {@link DataSize} units. + * + *

    The unit prefixes used in this class are + * binary prefixes + * indicating multiplication by powers of 2. The following table displays the + * enum constants defined in this class and corresponding values. + * + *

    + * + * + * + * + * + * + * + *
    ConstantData SizePower of 2Size in Bytes
    {@link #BYTES}1B2^01
    {@link #KILOBYTES}1KB2^101,024
    {@link #MEGABYTES}1MB2^201,048,576
    {@link #GIGABYTES}1GB2^301,073,741,824
    {@link #TERABYTES}1TB2^401,099,511,627,776
    + * + * @author Stephane Nicoll + * @author Sam Brannen + * @since 5.1 + * @see DataSize + */ +public enum DataUnit { + + /** + * Bytes, represented by suffix {@code B}. + */ + BYTES("B", DataSize.ofBytes(1)), + + /** + * Kilobytes, represented by suffix {@code KB}. + */ + KILOBYTES("KB", DataSize.ofKilobytes(1)), + + /** + * Megabytes, represented by suffix {@code MB}. + */ + MEGABYTES("MB", DataSize.ofMegabytes(1)), + + /** + * Gigabytes, represented by suffix {@code GB}. + */ + GIGABYTES("GB", DataSize.ofGigabytes(1)), + + /** + * Terabytes, represented by suffix {@code TB}. + */ + TERABYTES("TB", DataSize.ofTerabytes(1)); + + + private final String suffix; + + private final DataSize size; + + + DataUnit(String suffix, DataSize size) { + this.suffix = suffix; + this.size = size; + } + + DataSize size() { + return this.size; + } + + /** + * Return the {@link DataUnit} matching the specified {@code suffix}. + * @param suffix one of the standard suffixes + * @return the {@link DataUnit} matching the specified {@code suffix} + * @throws IllegalArgumentException if the suffix does not match the suffix + * of any of this enum's constants + */ + public static DataUnit fromSuffix(String suffix) { + for (DataUnit candidate : values()) { + if (candidate.suffix.equals(suffix)) { + return candidate; + } + } + throw new IllegalArgumentException("Unknown data unit suffix '" + suffix + "'"); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/unit/package-info.java b/spring-core/src/main/java/org/springframework/util/unit/package-info.java new file mode 100644 index 0000000..bad1762 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/unit/package-info.java @@ -0,0 +1,9 @@ +/** + * Useful unit data types. + */ +@NonNullApi +@NonNullFields +package org.springframework.util.unit; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/main/java/org/springframework/util/xml/AbstractStaxHandler.java b/spring-core/src/main/java/org/springframework/util/xml/AbstractStaxHandler.java new file mode 100644 index 0000000..4a72e50 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/AbstractStaxHandler.java @@ -0,0 +1,276 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.xml.XMLConstants; +import javax.xml.namespace.QName; +import javax.xml.stream.XMLStreamException; + +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.xml.sax.ext.LexicalHandler; + +import org.springframework.lang.Nullable; + +/** + * Abstract base class for SAX {@code ContentHandler} and {@code LexicalHandler} + * implementations that use StAX as a basis. All methods delegate to internal template + * methods, capable of throwing a {@code XMLStreamException}. Additionally, an namespace + * context stack is used to keep track of declared namespaces. + * + * @author Arjen Poutsma + * @since 4.0.3 + */ +abstract class AbstractStaxHandler implements ContentHandler, LexicalHandler { + + private final List> namespaceMappings = new ArrayList<>(); + + private boolean inCData; + + + @Override + public final void startDocument() throws SAXException { + removeAllNamespaceMappings(); + newNamespaceMapping(); + try { + startDocumentInternal(); + } + catch (XMLStreamException ex) { + throw new SAXException("Could not handle startDocument: " + ex.getMessage(), ex); + } + } + + @Override + public final void endDocument() throws SAXException { + removeAllNamespaceMappings(); + try { + endDocumentInternal(); + } + catch (XMLStreamException ex) { + throw new SAXException("Could not handle endDocument: " + ex.getMessage(), ex); + } + } + + @Override + public final void startPrefixMapping(String prefix, String uri) { + currentNamespaceMapping().put(prefix, uri); + } + + @Override + public final void endPrefixMapping(String prefix) { + } + + @Override + public final void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { + try { + startElementInternal(toQName(uri, qName), atts, currentNamespaceMapping()); + newNamespaceMapping(); + } + catch (XMLStreamException ex) { + throw new SAXException("Could not handle startElement: " + ex.getMessage(), ex); + } + } + + @Override + public final void endElement(String uri, String localName, String qName) throws SAXException { + try { + endElementInternal(toQName(uri, qName), currentNamespaceMapping()); + removeNamespaceMapping(); + } + catch (XMLStreamException ex) { + throw new SAXException("Could not handle endElement: " + ex.getMessage(), ex); + } + } + + @Override + public final void characters(char[] ch, int start, int length) throws SAXException { + try { + String data = new String(ch, start, length); + if (!this.inCData) { + charactersInternal(data); + } + else { + cDataInternal(data); + } + } + catch (XMLStreamException ex) { + throw new SAXException("Could not handle characters: " + ex.getMessage(), ex); + } + } + + @Override + public final void ignorableWhitespace(char[] ch, int start, int length) throws SAXException { + try { + ignorableWhitespaceInternal(new String(ch, start, length)); + } + catch (XMLStreamException ex) { + throw new SAXException( + "Could not handle ignorableWhitespace:" + ex.getMessage(), ex); + } + } + + @Override + public final void processingInstruction(String target, String data) throws SAXException { + try { + processingInstructionInternal(target, data); + } + catch (XMLStreamException ex) { + throw new SAXException("Could not handle processingInstruction: " + ex.getMessage(), ex); + } + } + + @Override + public final void skippedEntity(String name) throws SAXException { + try { + skippedEntityInternal(name); + } + catch (XMLStreamException ex) { + throw new SAXException("Could not handle skippedEntity: " + ex.getMessage(), ex); + } + } + + @Override + public final void startDTD(String name, @Nullable String publicId, String systemId) throws SAXException { + try { + StringBuilder builder = new StringBuilder(""); + + dtdInternal(builder.toString()); + } + catch (XMLStreamException ex) { + throw new SAXException("Could not handle startDTD: " + ex.getMessage(), ex); + } + } + + @Override + public final void endDTD() throws SAXException { + } + + @Override + public final void startCDATA() throws SAXException { + this.inCData = true; + } + + @Override + public final void endCDATA() throws SAXException { + this.inCData = false; + } + + @Override + public final void comment(char[] ch, int start, int length) throws SAXException { + try { + commentInternal(new String(ch, start, length)); + } + catch (XMLStreamException ex) { + throw new SAXException("Could not handle comment: " + ex.getMessage(), ex); + } + } + + @Override + public void startEntity(String name) throws SAXException { + } + + @Override + public void endEntity(String name) throws SAXException { + } + + /** + * Convert a namespace URI and DOM or SAX qualified name to a {@code QName}. The + * qualified name can have the form {@code prefix:localname} or {@code localName}. + * @param namespaceUri the namespace URI + * @param qualifiedName the qualified name + * @return a QName + */ + protected QName toQName(String namespaceUri, String qualifiedName) { + int idx = qualifiedName.indexOf(':'); + if (idx == -1) { + return new QName(namespaceUri, qualifiedName); + } + else { + String prefix = qualifiedName.substring(0, idx); + String localPart = qualifiedName.substring(idx + 1); + return new QName(namespaceUri, localPart, prefix); + } + } + + protected boolean isNamespaceDeclaration(QName qName) { + String prefix = qName.getPrefix(); + String localPart = qName.getLocalPart(); + return (XMLConstants.XMLNS_ATTRIBUTE.equals(localPart) && prefix.isEmpty()) || + (XMLConstants.XMLNS_ATTRIBUTE.equals(prefix) && !localPart.isEmpty()); + } + + + private Map currentNamespaceMapping() { + return this.namespaceMappings.get(this.namespaceMappings.size() - 1); + } + + private void newNamespaceMapping() { + this.namespaceMappings.add(new HashMap<>()); + } + + private void removeNamespaceMapping() { + this.namespaceMappings.remove(this.namespaceMappings.size() - 1); + } + + private void removeAllNamespaceMappings() { + this.namespaceMappings.clear(); + } + + + protected abstract void startDocumentInternal() throws XMLStreamException; + + protected abstract void endDocumentInternal() throws XMLStreamException; + + protected abstract void startElementInternal(QName name, Attributes attributes, + Map namespaceMapping) throws XMLStreamException; + + protected abstract void endElementInternal(QName name, Map namespaceMapping) + throws XMLStreamException; + + protected abstract void charactersInternal(String data) throws XMLStreamException; + + protected abstract void cDataInternal(String data) throws XMLStreamException; + + protected abstract void ignorableWhitespaceInternal(String data) throws XMLStreamException; + + protected abstract void processingInstructionInternal(String target, String data) + throws XMLStreamException; + + protected abstract void skippedEntityInternal(String name) throws XMLStreamException; + + protected abstract void dtdInternal(String dtd) throws XMLStreamException; + + protected abstract void commentInternal(String comment) throws XMLStreamException; + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/AbstractStaxXMLReader.java b/spring-core/src/main/java/org/springframework/util/xml/AbstractStaxXMLReader.java new file mode 100644 index 0000000..aa6e77c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/AbstractStaxXMLReader.java @@ -0,0 +1,243 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import java.util.LinkedHashMap; +import java.util.Map; + +import javax.xml.namespace.QName; +import javax.xml.stream.Location; +import javax.xml.stream.XMLStreamException; + +import org.xml.sax.InputSource; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; +import org.xml.sax.SAXNotRecognizedException; +import org.xml.sax.SAXNotSupportedException; +import org.xml.sax.SAXParseException; + +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Abstract base class for SAX {@code XMLReader} implementations that use StAX as a basis. + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @since 3.0 + * @see #setContentHandler(org.xml.sax.ContentHandler) + * @see #setDTDHandler(org.xml.sax.DTDHandler) + * @see #setEntityResolver(org.xml.sax.EntityResolver) + * @see #setErrorHandler(org.xml.sax.ErrorHandler) + */ +abstract class AbstractStaxXMLReader extends AbstractXMLReader { + + private static final String NAMESPACES_FEATURE_NAME = "http://xml.org/sax/features/namespaces"; + + private static final String NAMESPACE_PREFIXES_FEATURE_NAME = "http://xml.org/sax/features/namespace-prefixes"; + + private static final String IS_STANDALONE_FEATURE_NAME = "http://xml.org/sax/features/is-standalone"; + + + private boolean namespacesFeature = true; + + private boolean namespacePrefixesFeature = false; + + @Nullable + private Boolean isStandalone; + + private final Map namespaces = new LinkedHashMap<>(); + + + @Override + public boolean getFeature(String name) throws SAXNotRecognizedException, SAXNotSupportedException { + switch (name) { + case NAMESPACES_FEATURE_NAME: + return this.namespacesFeature; + case NAMESPACE_PREFIXES_FEATURE_NAME: + return this.namespacePrefixesFeature; + case IS_STANDALONE_FEATURE_NAME: + if (this.isStandalone != null) { + return this.isStandalone; + } + else { + throw new SAXNotSupportedException("startDocument() callback not completed yet"); + } + default: + return super.getFeature(name); + } + } + + @Override + public void setFeature(String name, boolean value) throws SAXNotRecognizedException, SAXNotSupportedException { + if (NAMESPACES_FEATURE_NAME.equals(name)) { + this.namespacesFeature = value; + } + else if (NAMESPACE_PREFIXES_FEATURE_NAME.equals(name)) { + this.namespacePrefixesFeature = value; + } + else { + super.setFeature(name, value); + } + } + + protected void setStandalone(boolean standalone) { + this.isStandalone = standalone; + } + + /** + * Indicates whether the SAX feature {@code http://xml.org/sax/features/namespaces} is turned on. + */ + protected boolean hasNamespacesFeature() { + return this.namespacesFeature; + } + + /** + * Indicates whether the SAX feature {@code http://xml.org/sax/features/namespaces-prefixes} is turned on. + */ + protected boolean hasNamespacePrefixesFeature() { + return this.namespacePrefixesFeature; + } + + /** + * Convert a {@code QName} to a qualified name, as used by DOM and SAX. + * The returned string has a format of {@code prefix:localName} if the + * prefix is set, or just {@code localName} if not. + * @param qName the {@code QName} + * @return the qualified name + */ + protected String toQualifiedName(QName qName) { + String prefix = qName.getPrefix(); + if (!StringUtils.hasLength(prefix)) { + return qName.getLocalPart(); + } + else { + return prefix + ":" + qName.getLocalPart(); + } + } + + + /** + * Parse the StAX XML reader passed at construction-time. + *

    NOTE:: The given {@code InputSource} is not read, but ignored. + * @param ignored is ignored + * @throws SAXException a SAX exception, possibly wrapping a {@code XMLStreamException} + */ + @Override + public final void parse(InputSource ignored) throws SAXException { + parse(); + } + + /** + * Parse the StAX XML reader passed at construction-time. + *

    NOTE:: The given system identifier is not read, but ignored. + * @param ignored is ignored + * @throws SAXException a SAX exception, possibly wrapping a {@code XMLStreamException} + */ + @Override + public final void parse(String ignored) throws SAXException { + parse(); + } + + private void parse() throws SAXException { + try { + parseInternal(); + } + catch (XMLStreamException ex) { + Locator locator = null; + if (ex.getLocation() != null) { + locator = new StaxLocator(ex.getLocation()); + } + SAXParseException saxException = new SAXParseException(ex.getMessage(), locator, ex); + if (getErrorHandler() != null) { + getErrorHandler().fatalError(saxException); + } + else { + throw saxException; + } + } + } + + /** + * Template method that parses the StAX reader passed at construction-time. + */ + protected abstract void parseInternal() throws SAXException, XMLStreamException; + + + /** + * Start the prefix mapping for the given prefix. + * @see org.xml.sax.ContentHandler#startPrefixMapping(String, String) + */ + protected void startPrefixMapping(@Nullable String prefix, String namespace) throws SAXException { + if (getContentHandler() != null && StringUtils.hasLength(namespace)) { + if (prefix == null) { + prefix = ""; + } + if (!namespace.equals(this.namespaces.get(prefix))) { + getContentHandler().startPrefixMapping(prefix, namespace); + this.namespaces.put(prefix, namespace); + } + } + } + + /** + * End the prefix mapping for the given prefix. + * @see org.xml.sax.ContentHandler#endPrefixMapping(String) + */ + protected void endPrefixMapping(String prefix) throws SAXException { + if (getContentHandler() != null && this.namespaces.containsKey(prefix)) { + getContentHandler().endPrefixMapping(prefix); + this.namespaces.remove(prefix); + } + } + + + /** + * Implementation of the {@code Locator} interface based on a given StAX {@code Location}. + * @see Locator + * @see Location + */ + private static class StaxLocator implements Locator { + + private final Location location; + + public StaxLocator(Location location) { + this.location = location; + } + + @Override + public String getPublicId() { + return this.location.getPublicId(); + } + + @Override + public String getSystemId() { + return this.location.getSystemId(); + } + + @Override + public int getLineNumber() { + return this.location.getLineNumber(); + } + + @Override + public int getColumnNumber() { + return this.location.getColumnNumber(); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/AbstractXMLEventReader.java b/spring-core/src/main/java/org/springframework/util/xml/AbstractXMLEventReader.java new file mode 100644 index 0000000..9fb08bb --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/AbstractXMLEventReader.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import java.util.NoSuchElementException; + +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLStreamException; + +import org.springframework.util.ClassUtils; + +/** + * Abstract base class for {@code XMLEventReader}s. + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @since 5.0 + */ +abstract class AbstractXMLEventReader implements XMLEventReader { + + private boolean closed; + + + @Override + public Object next() { + try { + return nextEvent(); + } + catch (XMLStreamException ex) { + throw new NoSuchElementException(ex.getMessage()); + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException( + "remove not supported on " + ClassUtils.getShortName(getClass())); + } + + /** + * This implementation throws an {@code IllegalArgumentException} for any property. + * @throws IllegalArgumentException when called + */ + @Override + public Object getProperty(String name) throws IllegalArgumentException { + throw new IllegalArgumentException("Property not supported: [" + name + "]"); + } + + @Override + public void close() { + this.closed = true; + } + + /** + * Check if the reader is closed, and throws a {@code XMLStreamException} if so. + * @throws XMLStreamException if the reader is closed + * @see #close() + */ + protected void checkIfClosed() throws XMLStreamException { + if (this.closed) { + throw new XMLStreamException("XMLEventReader has been closed"); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/AbstractXMLReader.java b/spring-core/src/main/java/org/springframework/util/xml/AbstractXMLReader.java new file mode 100644 index 0000000..520ab10 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/AbstractXMLReader.java @@ -0,0 +1,171 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import org.xml.sax.ContentHandler; +import org.xml.sax.DTDHandler; +import org.xml.sax.EntityResolver; +import org.xml.sax.ErrorHandler; +import org.xml.sax.SAXNotRecognizedException; +import org.xml.sax.SAXNotSupportedException; +import org.xml.sax.XMLReader; +import org.xml.sax.ext.LexicalHandler; + +import org.springframework.lang.Nullable; + +/** + * Abstract base class for SAX {@code XMLReader} implementations. + * Contains properties as defined in {@link XMLReader}, and does not recognize any features. + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @since 3.0 + * @see #setContentHandler(org.xml.sax.ContentHandler) + * @see #setDTDHandler(org.xml.sax.DTDHandler) + * @see #setEntityResolver(org.xml.sax.EntityResolver) + * @see #setErrorHandler(org.xml.sax.ErrorHandler) + */ +abstract class AbstractXMLReader implements XMLReader { + + @Nullable + private DTDHandler dtdHandler; + + @Nullable + private ContentHandler contentHandler; + + @Nullable + private EntityResolver entityResolver; + + @Nullable + private ErrorHandler errorHandler; + + @Nullable + private LexicalHandler lexicalHandler; + + + @Override + public void setContentHandler(@Nullable ContentHandler contentHandler) { + this.contentHandler = contentHandler; + } + + @Override + @Nullable + public ContentHandler getContentHandler() { + return this.contentHandler; + } + + @Override + public void setDTDHandler(@Nullable DTDHandler dtdHandler) { + this.dtdHandler = dtdHandler; + } + + @Override + @Nullable + public DTDHandler getDTDHandler() { + return this.dtdHandler; + } + + @Override + public void setEntityResolver(@Nullable EntityResolver entityResolver) { + this.entityResolver = entityResolver; + } + + @Override + @Nullable + public EntityResolver getEntityResolver() { + return this.entityResolver; + } + + @Override + public void setErrorHandler(@Nullable ErrorHandler errorHandler) { + this.errorHandler = errorHandler; + } + + @Override + @Nullable + public ErrorHandler getErrorHandler() { + return this.errorHandler; + } + + @Nullable + protected LexicalHandler getLexicalHandler() { + return this.lexicalHandler; + } + + + /** + * This implementation throws a {@code SAXNotRecognizedException} exception + * for any feature outside of the "http://xml.org/sax/features/" namespace + * and returns {@code false} for any feature within. + */ + @Override + public boolean getFeature(String name) throws SAXNotRecognizedException, SAXNotSupportedException { + if (name.startsWith("http://xml.org/sax/features/")) { + return false; + } + else { + throw new SAXNotRecognizedException(name); + } + } + + /** + * This implementation throws a {@code SAXNotRecognizedException} exception + * for any feature outside of the "http://xml.org/sax/features/" namespace + * and accepts a {@code false} value for any feature within. + */ + @Override + public void setFeature(String name, boolean value) throws SAXNotRecognizedException, SAXNotSupportedException { + if (name.startsWith("http://xml.org/sax/features/")) { + if (value) { + throw new SAXNotSupportedException(name); + } + } + else { + throw new SAXNotRecognizedException(name); + } + } + + /** + * Throws a {@code SAXNotRecognizedException} exception when the given property does not signify a lexical + * handler. The property name for a lexical handler is {@code http://xml.org/sax/properties/lexical-handler}. + */ + @Override + @Nullable + public Object getProperty(String name) throws SAXNotRecognizedException, SAXNotSupportedException { + if ("http://xml.org/sax/properties/lexical-handler".equals(name)) { + return this.lexicalHandler; + } + else { + throw new SAXNotRecognizedException(name); + } + } + + /** + * Throws a {@code SAXNotRecognizedException} exception when the given property does not signify a lexical + * handler. The property name for a lexical handler is {@code http://xml.org/sax/properties/lexical-handler}. + */ + @Override + public void setProperty(String name, Object value) throws SAXNotRecognizedException, SAXNotSupportedException { + if ("http://xml.org/sax/properties/lexical-handler".equals(name)) { + this.lexicalHandler = (LexicalHandler) value; + } + else { + throw new SAXNotRecognizedException(name); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/AbstractXMLStreamReader.java b/spring-core/src/main/java/org/springframework/util/xml/AbstractXMLStreamReader.java new file mode 100644 index 0000000..e50a5dd --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/AbstractXMLStreamReader.java @@ -0,0 +1,204 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import javax.xml.namespace.QName; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +import org.springframework.lang.Nullable; + +/** + * Abstract base class for {@code XMLStreamReader}s. + * + * @author Arjen Poutsma + * @since 3.0 + */ +abstract class AbstractXMLStreamReader implements XMLStreamReader { + + @Override + public String getElementText() throws XMLStreamException { + if (getEventType() != XMLStreamConstants.START_ELEMENT) { + throw new XMLStreamException("Parser must be on START_ELEMENT to read next text", getLocation()); + } + int eventType = next(); + StringBuilder builder = new StringBuilder(); + while (eventType != XMLStreamConstants.END_ELEMENT) { + if (eventType == XMLStreamConstants.CHARACTERS || eventType == XMLStreamConstants.CDATA || + eventType == XMLStreamConstants.SPACE || eventType == XMLStreamConstants.ENTITY_REFERENCE) { + builder.append(getText()); + } + else if (eventType == XMLStreamConstants.PROCESSING_INSTRUCTION || + eventType == XMLStreamConstants.COMMENT) { + // skipping + } + else if (eventType == XMLStreamConstants.END_DOCUMENT) { + throw new XMLStreamException("Unexpected end of document when reading element text content", + getLocation()); + } + else if (eventType == XMLStreamConstants.START_ELEMENT) { + throw new XMLStreamException("Element text content may not contain START_ELEMENT", getLocation()); + } + else { + throw new XMLStreamException("Unexpected event type " + eventType, getLocation()); + } + eventType = next(); + } + return builder.toString(); + } + + @Override + public String getAttributeLocalName(int index) { + return getAttributeName(index).getLocalPart(); + } + + @Override + public String getAttributeNamespace(int index) { + return getAttributeName(index).getNamespaceURI(); + } + + @Override + public String getAttributePrefix(int index) { + return getAttributeName(index).getPrefix(); + } + + @Override + public String getNamespaceURI() { + int eventType = getEventType(); + if (eventType == XMLStreamConstants.START_ELEMENT || eventType == XMLStreamConstants.END_ELEMENT) { + return getName().getNamespaceURI(); + } + else { + throw new IllegalStateException("Parser must be on START_ELEMENT or END_ELEMENT state"); + } + } + + @Override + public String getNamespaceURI(String prefix) { + return getNamespaceContext().getNamespaceURI(prefix); + } + + @Override + public boolean hasText() { + int eventType = getEventType(); + return (eventType == XMLStreamConstants.SPACE || eventType == XMLStreamConstants.CHARACTERS || + eventType == XMLStreamConstants.COMMENT || eventType == XMLStreamConstants.CDATA || + eventType == XMLStreamConstants.ENTITY_REFERENCE); + } + + @Override + public String getPrefix() { + int eventType = getEventType(); + if (eventType == XMLStreamConstants.START_ELEMENT || eventType == XMLStreamConstants.END_ELEMENT) { + return getName().getPrefix(); + } + else { + throw new IllegalStateException("Parser must be on START_ELEMENT or END_ELEMENT state"); + } + } + + @Override + public boolean hasName() { + int eventType = getEventType(); + return (eventType == XMLStreamConstants.START_ELEMENT || eventType == XMLStreamConstants.END_ELEMENT); + } + + @Override + public boolean isWhiteSpace() { + return getEventType() == XMLStreamConstants.SPACE; + } + + @Override + public boolean isStartElement() { + return getEventType() == XMLStreamConstants.START_ELEMENT; + } + + @Override + public boolean isEndElement() { + return getEventType() == XMLStreamConstants.END_ELEMENT; + } + + @Override + public boolean isCharacters() { + return getEventType() == XMLStreamConstants.CHARACTERS; + } + + @Override + public int nextTag() throws XMLStreamException { + int eventType = next(); + while (eventType == XMLStreamConstants.CHARACTERS && isWhiteSpace() || + eventType == XMLStreamConstants.CDATA && isWhiteSpace() || eventType == XMLStreamConstants.SPACE || + eventType == XMLStreamConstants.PROCESSING_INSTRUCTION || eventType == XMLStreamConstants.COMMENT) { + eventType = next(); + } + if (eventType != XMLStreamConstants.START_ELEMENT && eventType != XMLStreamConstants.END_ELEMENT) { + throw new XMLStreamException("expected start or end tag", getLocation()); + } + return eventType; + } + + @Override + public void require(int expectedType, String namespaceURI, String localName) throws XMLStreamException { + int eventType = getEventType(); + if (eventType != expectedType) { + throw new XMLStreamException("Expected [" + expectedType + "] but read [" + eventType + "]"); + } + } + + @Override + @Nullable + public String getAttributeValue(@Nullable String namespaceURI, String localName) { + for (int i = 0; i < getAttributeCount(); i++) { + QName name = getAttributeName(i); + if (name.getLocalPart().equals(localName) && + (namespaceURI == null || name.getNamespaceURI().equals(namespaceURI))) { + return getAttributeValue(i); + } + } + return null; + } + + @Override + public boolean hasNext() { + return getEventType() != END_DOCUMENT; + } + + @Override + public String getLocalName() { + return getName().getLocalPart(); + } + + @Override + public char[] getTextCharacters() { + return getText().toCharArray(); + } + + @Override + public int getTextCharacters(int sourceStart, char[] target, int targetStart, int length) { + char[] source = getTextCharacters(); + length = Math.min(length, source.length); + System.arraycopy(source, sourceStart, target, targetStart, length); + return length; + } + + @Override + public int getTextLength() { + return getText().length(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/DomContentHandler.java b/spring-core/src/main/java/org/springframework/util/xml/DomContentHandler.java new file mode 100644 index 0000000..6d12531 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/DomContentHandler.java @@ -0,0 +1,144 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import java.util.ArrayList; +import java.util.List; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.ProcessingInstruction; +import org.w3c.dom.Text; +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.Locator; + +/** + * SAX {@code ContentHandler} that transforms callback calls to DOM {@code Node}s. + * + * @author Arjen Poutsma + * @since 3.0 + * @see org.w3c.dom.Node + */ +class DomContentHandler implements ContentHandler { + + private final Document document; + + private final List elements = new ArrayList<>(); + + private final Node node; + + + /** + * Create a new instance of the {@code DomContentHandler} with the given node. + * @param node the node to publish events to + */ + DomContentHandler(Node node) { + this.node = node; + if (node instanceof Document) { + this.document = (Document) node; + } + else { + this.document = node.getOwnerDocument(); + } + } + + + private Node getParent() { + if (!this.elements.isEmpty()) { + return this.elements.get(this.elements.size() - 1); + } + else { + return this.node; + } + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) { + Node parent = getParent(); + Element element = this.document.createElementNS(uri, qName); + for (int i = 0; i < attributes.getLength(); i++) { + String attrUri = attributes.getURI(i); + String attrQname = attributes.getQName(i); + String value = attributes.getValue(i); + if (!attrQname.startsWith("xmlns")) { + element.setAttributeNS(attrUri, attrQname, value); + } + } + element = (Element) parent.appendChild(element); + this.elements.add(element); + } + + @Override + public void endElement(String uri, String localName, String qName) { + this.elements.remove(this.elements.size() - 1); + } + + @Override + public void characters(char[] ch, int start, int length) { + String data = new String(ch, start, length); + Node parent = getParent(); + Node lastChild = parent.getLastChild(); + if (lastChild != null && lastChild.getNodeType() == Node.TEXT_NODE) { + ((Text) lastChild).appendData(data); + } + else { + Text text = this.document.createTextNode(data); + parent.appendChild(text); + } + } + + @Override + public void processingInstruction(String target, String data) { + Node parent = getParent(); + ProcessingInstruction pi = this.document.createProcessingInstruction(target, data); + parent.appendChild(pi); + } + + + // Unsupported + + @Override + public void setDocumentLocator(Locator locator) { + } + + @Override + public void startDocument() { + } + + @Override + public void endDocument() { + } + + @Override + public void startPrefixMapping(String prefix, String uri) { + } + + @Override + public void endPrefixMapping(String prefix) { + } + + @Override + public void ignorableWhitespace(char[] ch, int start, int length) { + } + + @Override + public void skippedEntity(String name) { + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/DomUtils.java b/spring-core/src/main/java/org/springframework/util/xml/DomUtils.java new file mode 100644 index 0000000..3fd081a --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/DomUtils.java @@ -0,0 +1,195 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.w3c.dom.CharacterData; +import org.w3c.dom.Comment; +import org.w3c.dom.Element; +import org.w3c.dom.EntityReference; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.ContentHandler; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Convenience methods for working with the DOM API, + * in particular for working with DOM Nodes and DOM Elements. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @author Costin Leau + * @author Arjen Poutsma + * @author Luke Taylor + * @since 1.2 + * @see org.w3c.dom.Node + * @see org.w3c.dom.Element + */ +public abstract class DomUtils { + + /** + * Retrieves all child elements of the given DOM element that match any of the given element names. + * Only looks at the direct child level of the given element; do not go into further depth + * (in contrast to the DOM API's {@code getElementsByTagName} method). + * @param ele the DOM element to analyze + * @param childEleNames the child element names to look for + * @return a List of child {@code org.w3c.dom.Element} instances + * @see org.w3c.dom.Element + * @see org.w3c.dom.Element#getElementsByTagName + */ + public static List getChildElementsByTagName(Element ele, String... childEleNames) { + Assert.notNull(ele, "Element must not be null"); + Assert.notNull(childEleNames, "Element names collection must not be null"); + List childEleNameList = Arrays.asList(childEleNames); + NodeList nl = ele.getChildNodes(); + List childEles = new ArrayList<>(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (node instanceof Element && nodeNameMatch(node, childEleNameList)) { + childEles.add((Element) node); + } + } + return childEles; + } + + /** + * Retrieves all child elements of the given DOM element that match the given element name. + * Only look at the direct child level of the given element; do not go into further depth + * (in contrast to the DOM API's {@code getElementsByTagName} method). + * @param ele the DOM element to analyze + * @param childEleName the child element name to look for + * @return a List of child {@code org.w3c.dom.Element} instances + * @see org.w3c.dom.Element + * @see org.w3c.dom.Element#getElementsByTagName + */ + public static List getChildElementsByTagName(Element ele, String childEleName) { + return getChildElementsByTagName(ele, new String[] {childEleName}); + } + + /** + * Utility method that returns the first child element identified by its name. + * @param ele the DOM element to analyze + * @param childEleName the child element name to look for + * @return the {@code org.w3c.dom.Element} instance, or {@code null} if none found + */ + @Nullable + public static Element getChildElementByTagName(Element ele, String childEleName) { + Assert.notNull(ele, "Element must not be null"); + Assert.notNull(childEleName, "Element name must not be null"); + NodeList nl = ele.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (node instanceof Element && nodeNameMatch(node, childEleName)) { + return (Element) node; + } + } + return null; + } + + /** + * Utility method that returns the first child element value identified by its name. + * @param ele the DOM element to analyze + * @param childEleName the child element name to look for + * @return the extracted text value, or {@code null} if no child element found + */ + @Nullable + public static String getChildElementValueByTagName(Element ele, String childEleName) { + Element child = getChildElementByTagName(ele, childEleName); + return (child != null ? getTextValue(child) : null); + } + + /** + * Retrieves all child elements of the given DOM element. + * @param ele the DOM element to analyze + * @return a List of child {@code org.w3c.dom.Element} instances + */ + public static List getChildElements(Element ele) { + Assert.notNull(ele, "Element must not be null"); + NodeList nl = ele.getChildNodes(); + List childEles = new ArrayList<>(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (node instanceof Element) { + childEles.add((Element) node); + } + } + return childEles; + } + + /** + * Extracts the text value from the given DOM element, ignoring XML comments. + *

    Appends all CharacterData nodes and EntityReference nodes into a single + * String value, excluding Comment nodes. Only exposes actual user-specified + * text, no default values of any kind. + * @see CharacterData + * @see EntityReference + * @see Comment + */ + public static String getTextValue(Element valueEle) { + Assert.notNull(valueEle, "Element must not be null"); + StringBuilder sb = new StringBuilder(); + NodeList nl = valueEle.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + Node item = nl.item(i); + if ((item instanceof CharacterData && !(item instanceof Comment)) || item instanceof EntityReference) { + sb.append(item.getNodeValue()); + } + } + return sb.toString(); + } + + /** + * Namespace-aware equals comparison. Returns {@code true} if either + * {@link Node#getLocalName} or {@link Node#getNodeName} equals + * {@code desiredName}, otherwise returns {@code false}. + */ + public static boolean nodeNameEquals(Node node, String desiredName) { + Assert.notNull(node, "Node must not be null"); + Assert.notNull(desiredName, "Desired name must not be null"); + return nodeNameMatch(node, desiredName); + } + + /** + * Returns a SAX {@code ContentHandler} that transforms callback calls to DOM {@code Node}s. + * @param node the node to publish events to + * @return the content handler + */ + public static ContentHandler createContentHandler(Node node) { + return new DomContentHandler(node); + } + + /** + * Matches the given node's name and local name against the given desired name. + */ + private static boolean nodeNameMatch(Node node, String desiredName) { + return (desiredName.equals(node.getNodeName()) || desiredName.equals(node.getLocalName())); + } + + /** + * Matches the given node's name and local name against the given desired names. + */ + private static boolean nodeNameMatch(Node node, Collection desiredNames) { + return (desiredNames.contains(node.getNodeName()) || desiredNames.contains(node.getLocalName())); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/ListBasedXMLEventReader.java b/spring-core/src/main/java/org/springframework/util/xml/ListBasedXMLEventReader.java new file mode 100644 index 0000000..803139c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/ListBasedXMLEventReader.java @@ -0,0 +1,143 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; + +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.events.Characters; +import javax.xml.stream.events.XMLEvent; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Implementation of {@code XMLEventReader} based on a {@link List} + * of {@link XMLEvent} elements. + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @since 5.0 + */ +class ListBasedXMLEventReader extends AbstractXMLEventReader { + + private final List events; + + @Nullable + private XMLEvent currentEvent; + + private int cursor = 0; + + + public ListBasedXMLEventReader(List events) { + Assert.notNull(events, "XMLEvent List must not be null"); + this.events = new ArrayList<>(events); + } + + + @Override + public boolean hasNext() { + return (this.cursor < this.events.size()); + } + + @Override + public XMLEvent nextEvent() { + if (hasNext()) { + this.currentEvent = this.events.get(this.cursor); + this.cursor++; + return this.currentEvent; + } + else { + throw new NoSuchElementException(); + } + } + + @Override + @Nullable + public XMLEvent peek() { + if (hasNext()) { + return this.events.get(this.cursor); + } + else { + return null; + } + } + + @Override + public String getElementText() throws XMLStreamException { + checkIfClosed(); + if (this.currentEvent == null || !this.currentEvent.isStartElement()) { + throw new XMLStreamException("Not at START_ELEMENT: " + this.currentEvent); + } + + StringBuilder builder = new StringBuilder(); + while (true) { + XMLEvent event = nextEvent(); + if (event.isEndElement()) { + break; + } + else if (!event.isCharacters()) { + throw new XMLStreamException("Unexpected non-text event: " + event); + } + Characters characters = event.asCharacters(); + if (!characters.isIgnorableWhiteSpace()) { + builder.append(event.asCharacters().getData()); + } + } + return builder.toString(); + } + + @Override + @Nullable + public XMLEvent nextTag() throws XMLStreamException { + checkIfClosed(); + + while (true) { + XMLEvent event = nextEvent(); + switch (event.getEventType()) { + case XMLStreamConstants.START_ELEMENT: + case XMLStreamConstants.END_ELEMENT: + return event; + case XMLStreamConstants.END_DOCUMENT: + return null; + case XMLStreamConstants.SPACE: + case XMLStreamConstants.COMMENT: + case XMLStreamConstants.PROCESSING_INSTRUCTION: + continue; + case XMLStreamConstants.CDATA: + case XMLStreamConstants.CHARACTERS: + if (!event.asCharacters().isWhiteSpace()) { + throw new XMLStreamException( + "Non-ignorable whitespace CDATA or CHARACTERS event: " + event); + } + break; + default: + throw new XMLStreamException("Expected START_ELEMENT or END_ELEMENT: " + event); + } + } + } + + @Override + public void close() { + super.close(); + this.events.clear(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/SimpleNamespaceContext.java b/spring-core/src/main/java/org/springframework/util/xml/SimpleNamespaceContext.java new file mode 100644 index 0000000..05a0408 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/SimpleNamespaceContext.java @@ -0,0 +1,170 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import javax.xml.XMLConstants; +import javax.xml.namespace.NamespaceContext; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Simple {@code javax.xml.namespace.NamespaceContext} implementation. + * Follows the standard {@code NamespaceContext} contract, and is loadable + * via a {@code java.util.Map} or {@code java.util.Properties} object + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @since 3.0 + */ +public class SimpleNamespaceContext implements NamespaceContext { + + private final Map prefixToNamespaceUri = new HashMap<>(); + + private final Map> namespaceUriToPrefixes = new HashMap<>(); + + private String defaultNamespaceUri = ""; + + + @Override + public String getNamespaceURI(String prefix) { + Assert.notNull(prefix, "No prefix given"); + if (XMLConstants.XML_NS_PREFIX.equals(prefix)) { + return XMLConstants.XML_NS_URI; + } + else if (XMLConstants.XMLNS_ATTRIBUTE.equals(prefix)) { + return XMLConstants.XMLNS_ATTRIBUTE_NS_URI; + } + else if (XMLConstants.DEFAULT_NS_PREFIX.equals(prefix)) { + return this.defaultNamespaceUri; + } + else if (this.prefixToNamespaceUri.containsKey(prefix)) { + return this.prefixToNamespaceUri.get(prefix); + } + return ""; + } + + @Override + @Nullable + public String getPrefix(String namespaceUri) { + Set prefixes = getPrefixesSet(namespaceUri); + return (!prefixes.isEmpty() ? prefixes.iterator().next() : null); + } + + @Override + public Iterator getPrefixes(String namespaceUri) { + return getPrefixesSet(namespaceUri).iterator(); + } + + private Set getPrefixesSet(String namespaceUri) { + Assert.notNull(namespaceUri, "No namespaceUri given"); + if (this.defaultNamespaceUri.equals(namespaceUri)) { + return Collections.singleton(XMLConstants.DEFAULT_NS_PREFIX); + } + else if (XMLConstants.XML_NS_URI.equals(namespaceUri)) { + return Collections.singleton(XMLConstants.XML_NS_PREFIX); + } + else if (XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(namespaceUri)) { + return Collections.singleton(XMLConstants.XMLNS_ATTRIBUTE); + } + else { + Set prefixes = this.namespaceUriToPrefixes.get(namespaceUri); + return (prefixes != null ? Collections.unmodifiableSet(prefixes) : Collections.emptySet()); + } + } + + + /** + * Set the bindings for this namespace context. + * The supplied map must consist of string key value pairs. + */ + public void setBindings(Map bindings) { + bindings.forEach(this::bindNamespaceUri); + } + + /** + * Bind the given namespace as default namespace. + * @param namespaceUri the namespace uri + */ + public void bindDefaultNamespaceUri(String namespaceUri) { + bindNamespaceUri(XMLConstants.DEFAULT_NS_PREFIX, namespaceUri); + } + + /** + * Bind the given prefix to the given namespace. + * @param prefix the namespace prefix + * @param namespaceUri the namespace uri + */ + public void bindNamespaceUri(String prefix, String namespaceUri) { + Assert.notNull(prefix, "No prefix given"); + Assert.notNull(namespaceUri, "No namespaceUri given"); + if (XMLConstants.DEFAULT_NS_PREFIX.equals(prefix)) { + this.defaultNamespaceUri = namespaceUri; + } + else { + this.prefixToNamespaceUri.put(prefix, namespaceUri); + Set prefixes = + this.namespaceUriToPrefixes.computeIfAbsent(namespaceUri, k -> new LinkedHashSet<>()); + prefixes.add(prefix); + } + } + + /** + * Remove the given prefix from this context. + * @param prefix the prefix to be removed + */ + public void removeBinding(@Nullable String prefix) { + if (XMLConstants.DEFAULT_NS_PREFIX.equals(prefix)) { + this.defaultNamespaceUri = ""; + } + else if (prefix != null) { + String namespaceUri = this.prefixToNamespaceUri.remove(prefix); + if (namespaceUri != null) { + Set prefixes = this.namespaceUriToPrefixes.get(namespaceUri); + if (prefixes != null) { + prefixes.remove(prefix); + if (prefixes.isEmpty()) { + this.namespaceUriToPrefixes.remove(namespaceUri); + } + } + } + } + } + + /** + * Remove all declared prefixes. + */ + public void clear() { + this.prefixToNamespaceUri.clear(); + this.namespaceUriToPrefixes.clear(); + } + + /** + * Return all declared prefixes. + */ + public Iterator getBoundPrefixes() { + return this.prefixToNamespaceUri.keySet().iterator(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/SimpleSaxErrorHandler.java b/spring-core/src/main/java/org/springframework/util/xml/SimpleSaxErrorHandler.java new file mode 100644 index 0000000..8b98214 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/SimpleSaxErrorHandler.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import org.apache.commons.logging.Log; +import org.xml.sax.ErrorHandler; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +/** + * Simple {@code org.xml.sax.ErrorHandler} implementation: + * logs warnings using the given Commons Logging logger instance, + * and rethrows errors to discontinue the XML transformation. + * + * @author Juergen Hoeller + * @since 1.2 + */ +public class SimpleSaxErrorHandler implements ErrorHandler { + + private final Log logger; + + + /** + * Create a new SimpleSaxErrorHandler for the given + * Commons Logging logger instance. + */ + public SimpleSaxErrorHandler(Log logger) { + this.logger = logger; + } + + + @Override + public void warning(SAXParseException ex) throws SAXException { + logger.warn("Ignored XML validation warning", ex); + } + + @Override + public void error(SAXParseException ex) throws SAXException { + throw ex; + } + + @Override + public void fatalError(SAXParseException ex) throws SAXException { + throw ex; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/SimpleTransformErrorListener.java b/spring-core/src/main/java/org/springframework/util/xml/SimpleTransformErrorListener.java new file mode 100644 index 0000000..88e3dc6 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/SimpleTransformErrorListener.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import javax.xml.transform.ErrorListener; +import javax.xml.transform.TransformerException; + +import org.apache.commons.logging.Log; + +/** + * Simple {@code javax.xml.transform.ErrorListener} implementation: + * logs warnings using the given Commons Logging logger instance, + * and rethrows errors to discontinue the XML transformation. + * + * @author Juergen Hoeller + * @since 1.2 + */ +public class SimpleTransformErrorListener implements ErrorListener { + + private final Log logger; + + + /** + * Create a new SimpleTransformErrorListener for the given + * Commons Logging logger instance. + */ + public SimpleTransformErrorListener(Log logger) { + this.logger = logger; + } + + + @Override + public void warning(TransformerException ex) throws TransformerException { + logger.warn("XSLT transformation warning", ex); + } + + @Override + public void error(TransformerException ex) throws TransformerException { + logger.error("XSLT transformation error", ex); + } + + @Override + public void fatalError(TransformerException ex) throws TransformerException { + throw ex; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/StaxEventHandler.java b/spring-core/src/main/java/org/springframework/util/xml/StaxEventHandler.java new file mode 100644 index 0000000..e57141a --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/StaxEventHandler.java @@ -0,0 +1,196 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.xml.namespace.QName; +import javax.xml.stream.Location; +import javax.xml.stream.XMLEventFactory; +import javax.xml.stream.XMLEventWriter; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.events.Attribute; +import javax.xml.stream.events.Namespace; + +import org.xml.sax.Attributes; +import org.xml.sax.Locator; +import org.xml.sax.ext.LexicalHandler; + +import org.springframework.lang.Nullable; + +/** + * SAX {@link org.xml.sax.ContentHandler} and {@link LexicalHandler} + * that writes to a {@link javax.xml.stream.util.XMLEventConsumer}. + * + * @author Arjen Poutsma + * @since 4.0.3 + */ +class StaxEventHandler extends AbstractStaxHandler { + + private final XMLEventFactory eventFactory; + + private final XMLEventWriter eventWriter; + + + /** + * Construct a new instance of the {@code StaxEventContentHandler} that writes to the + * given {@code XMLEventWriter}. A default {@code XMLEventFactory} will be created. + * @param eventWriter the writer to write events to + */ + public StaxEventHandler(XMLEventWriter eventWriter) { + this.eventFactory = XMLEventFactory.newInstance(); + this.eventWriter = eventWriter; + } + + /** + * Construct a new instance of the {@code StaxEventContentHandler} that uses the given + * event factory to create events and writes to the given {@code XMLEventConsumer}. + * @param eventWriter the writer to write events to + * @param factory the factory used to create events + */ + public StaxEventHandler(XMLEventWriter eventWriter, XMLEventFactory factory) { + this.eventFactory = factory; + this.eventWriter = eventWriter; + } + + + @Override + public void setDocumentLocator(@Nullable Locator locator) { + if (locator != null) { + this.eventFactory.setLocation(new LocatorLocationAdapter(locator)); + } + } + + @Override + protected void startDocumentInternal() throws XMLStreamException { + this.eventWriter.add(this.eventFactory.createStartDocument()); + } + + @Override + protected void endDocumentInternal() throws XMLStreamException { + this.eventWriter.add(this.eventFactory.createEndDocument()); + } + + @Override + protected void startElementInternal(QName name, Attributes atts, + Map namespaceMapping) throws XMLStreamException { + + List attributes = getAttributes(atts); + List namespaces = getNamespaces(namespaceMapping); + this.eventWriter.add( + this.eventFactory.createStartElement(name, attributes.iterator(), namespaces.iterator())); + + } + + private List getNamespaces(Map namespaceMappings) { + List result = new ArrayList<>(namespaceMappings.size()); + namespaceMappings.forEach((prefix, namespaceUri) -> + result.add(this.eventFactory.createNamespace(prefix, namespaceUri))); + return result; + } + + private List getAttributes(Attributes attributes) { + int attrLength = attributes.getLength(); + List result = new ArrayList<>(attrLength); + for (int i = 0; i < attrLength; i++) { + QName attrName = toQName(attributes.getURI(i), attributes.getQName(i)); + if (!isNamespaceDeclaration(attrName)) { + result.add(this.eventFactory.createAttribute(attrName, attributes.getValue(i))); + } + } + return result; + } + + @Override + protected void endElementInternal(QName name, Map namespaceMapping) throws XMLStreamException { + List namespaces = getNamespaces(namespaceMapping); + this.eventWriter.add(this.eventFactory.createEndElement(name, namespaces.iterator())); + } + + @Override + protected void charactersInternal(String data) throws XMLStreamException { + this.eventWriter.add(this.eventFactory.createCharacters(data)); + } + + @Override + protected void cDataInternal(String data) throws XMLStreamException { + this.eventWriter.add(this.eventFactory.createCData(data)); + } + + @Override + protected void ignorableWhitespaceInternal(String data) throws XMLStreamException { + this.eventWriter.add(this.eventFactory.createIgnorableSpace(data)); + } + + @Override + protected void processingInstructionInternal(String target, String data) throws XMLStreamException { + this.eventWriter.add(this.eventFactory.createProcessingInstruction(target, data)); + } + + @Override + protected void dtdInternal(String dtd) throws XMLStreamException { + this.eventWriter.add(this.eventFactory.createDTD(dtd)); + } + + @Override + protected void commentInternal(String comment) throws XMLStreamException { + this.eventWriter.add(this.eventFactory.createComment(comment)); + } + + // Ignored + @Override + protected void skippedEntityInternal(String name) { + } + + + private static final class LocatorLocationAdapter implements Location { + + private final Locator locator; + + public LocatorLocationAdapter(Locator locator) { + this.locator = locator; + } + + @Override + public int getLineNumber() { + return this.locator.getLineNumber(); + } + + @Override + public int getColumnNumber() { + return this.locator.getColumnNumber(); + } + + @Override + public int getCharacterOffset() { + return -1; + } + + @Override + public String getPublicId() { + return this.locator.getPublicId(); + } + + @Override + public String getSystemId() { + return this.locator.getSystemId(); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/StaxEventXMLReader.java b/spring-core/src/main/java/org/springframework/util/xml/StaxEventXMLReader.java new file mode 100644 index 0000000..3ec0b1b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/StaxEventXMLReader.java @@ -0,0 +1,348 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import java.util.Iterator; + +import javax.xml.namespace.QName; +import javax.xml.stream.Location; +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.events.Attribute; +import javax.xml.stream.events.Characters; +import javax.xml.stream.events.Comment; +import javax.xml.stream.events.DTD; +import javax.xml.stream.events.EndElement; +import javax.xml.stream.events.EntityDeclaration; +import javax.xml.stream.events.EntityReference; +import javax.xml.stream.events.Namespace; +import javax.xml.stream.events.NotationDeclaration; +import javax.xml.stream.events.ProcessingInstruction; +import javax.xml.stream.events.StartDocument; +import javax.xml.stream.events.StartElement; +import javax.xml.stream.events.XMLEvent; + +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.xml.sax.ext.Locator2; +import org.xml.sax.helpers.AttributesImpl; + +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * SAX {@code XMLReader} that reads from a StAX {@code XMLEventReader}. Consumes {@code XMLEvents} from + * an {@code XMLEventReader}, and calls the corresponding methods on the SAX callback interfaces. + * + * @author Arjen Poutsma + * @since 3.0 + * @see XMLEventReader + * @see #setContentHandler(org.xml.sax.ContentHandler) + * @see #setDTDHandler(org.xml.sax.DTDHandler) + * @see #setEntityResolver(org.xml.sax.EntityResolver) + * @see #setErrorHandler(org.xml.sax.ErrorHandler) + */ +@SuppressWarnings("rawtypes") +class StaxEventXMLReader extends AbstractStaxXMLReader { + + private static final String DEFAULT_XML_VERSION = "1.0"; + + private final XMLEventReader reader; + + private String xmlVersion = DEFAULT_XML_VERSION; + + @Nullable + private String encoding; + + + /** + * Constructs a new instance of the {@code StaxEventXmlReader} that reads from + * the given {@code XMLEventReader}. The supplied event reader must be in + * {@code XMLStreamConstants.START_DOCUMENT} or {@code XMLStreamConstants.START_ELEMENT} state. + * @param reader the {@code XMLEventReader} to read from + * @throws IllegalStateException if the reader is not at the start of a document or element + */ + StaxEventXMLReader(XMLEventReader reader) { + try { + XMLEvent event = reader.peek(); + if (event != null && !(event.isStartDocument() || event.isStartElement())) { + throw new IllegalStateException("XMLEventReader not at start of document or element"); + } + } + catch (XMLStreamException ex) { + throw new IllegalStateException("Could not read first element: " + ex.getMessage()); + } + this.reader = reader; + } + + + @Override + protected void parseInternal() throws SAXException, XMLStreamException { + boolean documentStarted = false; + boolean documentEnded = false; + int elementDepth = 0; + while (this.reader.hasNext() && elementDepth >= 0) { + XMLEvent event = this.reader.nextEvent(); + if (!event.isStartDocument() && !event.isEndDocument() && !documentStarted) { + handleStartDocument(event); + documentStarted = true; + } + switch (event.getEventType()) { + case XMLStreamConstants.START_DOCUMENT: + handleStartDocument(event); + documentStarted = true; + break; + case XMLStreamConstants.START_ELEMENT: + elementDepth++; + handleStartElement(event.asStartElement()); + break; + case XMLStreamConstants.END_ELEMENT: + elementDepth--; + if (elementDepth >= 0) { + handleEndElement(event.asEndElement()); + } + break; + case XMLStreamConstants.PROCESSING_INSTRUCTION: + handleProcessingInstruction((ProcessingInstruction) event); + break; + case XMLStreamConstants.CHARACTERS: + case XMLStreamConstants.SPACE: + case XMLStreamConstants.CDATA: + handleCharacters(event.asCharacters()); + break; + case XMLStreamConstants.END_DOCUMENT: + handleEndDocument(); + documentEnded = true; + break; + case XMLStreamConstants.NOTATION_DECLARATION: + handleNotationDeclaration((NotationDeclaration) event); + break; + case XMLStreamConstants.ENTITY_DECLARATION: + handleEntityDeclaration((EntityDeclaration) event); + break; + case XMLStreamConstants.COMMENT: + handleComment((Comment) event); + break; + case XMLStreamConstants.DTD: + handleDtd((DTD) event); + break; + case XMLStreamConstants.ENTITY_REFERENCE: + handleEntityReference((EntityReference) event); + break; + } + } + if (documentStarted && !documentEnded) { + handleEndDocument(); + } + + } + + private void handleStartDocument(final XMLEvent event) throws SAXException { + if (event.isStartDocument()) { + StartDocument startDocument = (StartDocument) event; + String xmlVersion = startDocument.getVersion(); + if (StringUtils.hasLength(xmlVersion)) { + this.xmlVersion = xmlVersion; + } + if (startDocument.encodingSet()) { + this.encoding = startDocument.getCharacterEncodingScheme(); + } + } + + ContentHandler contentHandler = getContentHandler(); + if (contentHandler != null) { + final Location location = event.getLocation(); + contentHandler.setDocumentLocator(new Locator2() { + @Override + public int getColumnNumber() { + return (location != null ? location.getColumnNumber() : -1); + } + @Override + public int getLineNumber() { + return (location != null ? location.getLineNumber() : -1); + } + @Override + @Nullable + public String getPublicId() { + return (location != null ? location.getPublicId() : null); + } + @Override + @Nullable + public String getSystemId() { + return (location != null ? location.getSystemId() : null); + } + @Override + public String getXMLVersion() { + return xmlVersion; + } + @Override + @Nullable + public String getEncoding() { + return encoding; + } + }); + contentHandler.startDocument(); + } + } + + private void handleStartElement(StartElement startElement) throws SAXException { + if (getContentHandler() != null) { + QName qName = startElement.getName(); + if (hasNamespacesFeature()) { + for (Iterator i = startElement.getNamespaces(); i.hasNext();) { + Namespace namespace = (Namespace) i.next(); + startPrefixMapping(namespace.getPrefix(), namespace.getNamespaceURI()); + } + for (Iterator i = startElement.getAttributes(); i.hasNext();){ + Attribute attribute = (Attribute) i.next(); + QName attributeName = attribute.getName(); + startPrefixMapping(attributeName.getPrefix(), attributeName.getNamespaceURI()); + } + + getContentHandler().startElement(qName.getNamespaceURI(), qName.getLocalPart(), toQualifiedName(qName), + getAttributes(startElement)); + } + else { + getContentHandler().startElement("", "", toQualifiedName(qName), getAttributes(startElement)); + } + } + } + + private void handleCharacters(Characters characters) throws SAXException { + char[] data = characters.getData().toCharArray(); + if (getContentHandler() != null && characters.isIgnorableWhiteSpace()) { + getContentHandler().ignorableWhitespace(data, 0, data.length); + return; + } + if (characters.isCData() && getLexicalHandler() != null) { + getLexicalHandler().startCDATA(); + } + if (getContentHandler() != null) { + getContentHandler().characters(data, 0, data.length); + } + if (characters.isCData() && getLexicalHandler() != null) { + getLexicalHandler().endCDATA(); + } + } + + private void handleEndElement(EndElement endElement) throws SAXException { + if (getContentHandler() != null) { + QName qName = endElement.getName(); + if (hasNamespacesFeature()) { + getContentHandler().endElement(qName.getNamespaceURI(), qName.getLocalPart(), toQualifiedName(qName)); + for (Iterator i = endElement.getNamespaces(); i.hasNext();) { + Namespace namespace = (Namespace) i.next(); + endPrefixMapping(namespace.getPrefix()); + } + } + else { + getContentHandler().endElement("", "", toQualifiedName(qName)); + } + + } + } + + private void handleEndDocument() throws SAXException { + if (getContentHandler() != null) { + getContentHandler().endDocument(); + } + } + + private void handleNotationDeclaration(NotationDeclaration declaration) throws SAXException { + if (getDTDHandler() != null) { + getDTDHandler().notationDecl(declaration.getName(), declaration.getPublicId(), declaration.getSystemId()); + } + } + + private void handleEntityDeclaration(EntityDeclaration entityDeclaration) throws SAXException { + if (getDTDHandler() != null) { + getDTDHandler().unparsedEntityDecl(entityDeclaration.getName(), entityDeclaration.getPublicId(), + entityDeclaration.getSystemId(), entityDeclaration.getNotationName()); + } + } + + private void handleProcessingInstruction(ProcessingInstruction pi) throws SAXException { + if (getContentHandler() != null) { + getContentHandler().processingInstruction(pi.getTarget(), pi.getData()); + } + } + + private void handleComment(Comment comment) throws SAXException { + if (getLexicalHandler() != null) { + char[] ch = comment.getText().toCharArray(); + getLexicalHandler().comment(ch, 0, ch.length); + } + } + + private void handleDtd(DTD dtd) throws SAXException { + if (getLexicalHandler() != null) { + javax.xml.stream.Location location = dtd.getLocation(); + getLexicalHandler().startDTD(null, location.getPublicId(), location.getSystemId()); + } + if (getLexicalHandler() != null) { + getLexicalHandler().endDTD(); + } + + } + + private void handleEntityReference(EntityReference reference) throws SAXException { + if (getLexicalHandler() != null) { + getLexicalHandler().startEntity(reference.getName()); + } + if (getLexicalHandler() != null) { + getLexicalHandler().endEntity(reference.getName()); + } + + } + + private Attributes getAttributes(StartElement event) { + AttributesImpl attributes = new AttributesImpl(); + for (Iterator i = event.getAttributes(); i.hasNext();) { + Attribute attribute = (Attribute) i.next(); + QName qName = attribute.getName(); + String namespace = qName.getNamespaceURI(); + if (namespace == null || !hasNamespacesFeature()) { + namespace = ""; + } + String type = attribute.getDTDType(); + if (type == null) { + type = "CDATA"; + } + attributes.addAttribute(namespace, qName.getLocalPart(), toQualifiedName(qName), type, attribute.getValue()); + } + if (hasNamespacePrefixesFeature()) { + for (Iterator i = event.getNamespaces(); i.hasNext();) { + Namespace namespace = (Namespace) i.next(); + String prefix = namespace.getPrefix(); + String namespaceUri = namespace.getNamespaceURI(); + String qName; + if (StringUtils.hasLength(prefix)) { + qName = "xmlns:" + prefix; + } + else { + qName = "xmlns"; + } + attributes.addAttribute("", "", qName, "CDATA", namespaceUri); + } + } + + return attributes; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/StaxResult.java b/spring-core/src/main/java/org/springframework/util/xml/StaxResult.java new file mode 100644 index 0000000..80a86a0 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/StaxResult.java @@ -0,0 +1,124 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import javax.xml.stream.XMLEventWriter; +import javax.xml.stream.XMLStreamWriter; +import javax.xml.transform.sax.SAXResult; + +import org.xml.sax.ContentHandler; +import org.xml.sax.ext.LexicalHandler; + +import org.springframework.lang.Nullable; + +/** + * Implementation of the {@code Result} tagging interface for StAX writers. Can be constructed with + * an {@code XMLEventConsumer} or an {@code XMLStreamWriter}. + * + *

    This class is necessary because there is no implementation of {@code Source} for StaxReaders + * in JAXP 1.3. There is a {@code StAXResult} in JAXP 1.4 (JDK 1.6), but this class is kept around + * for backwards compatibility reasons. + * + *

    Even though {@code StaxResult} extends from {@code SAXResult}, calling the methods of + * {@code SAXResult} is not supported. In general, the only supported operation + * on this class is to use the {@code ContentHandler} obtained via {@link #getHandler()} to parse an + * input source using an {@code XMLReader}. Calling {@link #setHandler(org.xml.sax.ContentHandler)} + * or {@link #setLexicalHandler(org.xml.sax.ext.LexicalHandler)} will result in + * {@code UnsupportedOperationException}s. + * + * @author Arjen Poutsma + * @since 3.0 + * @see XMLEventWriter + * @see XMLStreamWriter + * @see javax.xml.transform.Transformer + */ +class StaxResult extends SAXResult { + + @Nullable + private XMLEventWriter eventWriter; + + @Nullable + private XMLStreamWriter streamWriter; + + + /** + * Construct a new instance of the {@code StaxResult} with the specified {@code XMLEventWriter}. + * @param eventWriter the {@code XMLEventWriter} to write to + */ + public StaxResult(XMLEventWriter eventWriter) { + StaxEventHandler handler = new StaxEventHandler(eventWriter); + super.setHandler(handler); + super.setLexicalHandler(handler); + this.eventWriter = eventWriter; + } + + /** + * Construct a new instance of the {@code StaxResult} with the specified {@code XMLStreamWriter}. + * @param streamWriter the {@code XMLStreamWriter} to write to + */ + public StaxResult(XMLStreamWriter streamWriter) { + StaxStreamHandler handler = new StaxStreamHandler(streamWriter); + super.setHandler(handler); + super.setLexicalHandler(handler); + this.streamWriter = streamWriter; + } + + + /** + * Return the {@code XMLEventWriter} used by this {@code StaxResult}. + *

    If this {@code StaxResult} was created with an {@code XMLStreamWriter}, + * the result will be {@code null}. + * @return the StAX event writer used by this result + * @see #StaxResult(javax.xml.stream.XMLEventWriter) + */ + @Nullable + public XMLEventWriter getXMLEventWriter() { + return this.eventWriter; + } + + /** + * Return the {@code XMLStreamWriter} used by this {@code StaxResult}. + *

    If this {@code StaxResult} was created with an {@code XMLEventConsumer}, + * the result will be {@code null}. + * @return the StAX stream writer used by this result + * @see #StaxResult(javax.xml.stream.XMLStreamWriter) + */ + @Nullable + public XMLStreamWriter getXMLStreamWriter() { + return this.streamWriter; + } + + + /** + * Throws an {@code UnsupportedOperationException}. + * @throws UnsupportedOperationException always + */ + @Override + public void setHandler(ContentHandler handler) { + throw new UnsupportedOperationException("setHandler is not supported"); + } + + /** + * Throws an {@code UnsupportedOperationException}. + * @throws UnsupportedOperationException always + */ + @Override + public void setLexicalHandler(LexicalHandler handler) { + throw new UnsupportedOperationException("setLexicalHandler is not supported"); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/StaxSource.java b/spring-core/src/main/java/org/springframework/util/xml/StaxSource.java new file mode 100644 index 0000000..392cd12 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/StaxSource.java @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLStreamReader; +import javax.xml.transform.sax.SAXSource; + +import org.xml.sax.InputSource; +import org.xml.sax.XMLReader; + +import org.springframework.lang.Nullable; + +/** + * Implementation of the {@code Source} tagging interface for StAX readers. Can be constructed with + * an {@code XMLEventReader} or an {@code XMLStreamReader}. + * + *

    This class is necessary because there is no implementation of {@code Source} for StAX Readers + * in JAXP 1.3. There is a {@code StAXSource} in JAXP 1.4 (JDK 1.6), but this class is kept around + * for backwards compatibility reasons. + * + *

    Even though {@code StaxSource} extends from {@code SAXSource}, calling the methods of + * {@code SAXSource} is not supported. In general, the only supported operation + * on this class is to use the {@code XMLReader} obtained via {@link #getXMLReader()} to parse the + * input source obtained via {@link #getInputSource()}. Calling {@link #setXMLReader(XMLReader)} + * or {@link #setInputSource(InputSource)} will result in {@code UnsupportedOperationException #setInputSource(InputSource)} will result in {@code UnsupportedOperationExceptions}. + * + * @author Arjen Poutsma + * @since 3.0 + * @see XMLEventReader + * @see XMLStreamReader + * @see javax.xml.transform.Transformer + */ +class StaxSource extends SAXSource { + + @Nullable + private XMLEventReader eventReader; + + @Nullable + private XMLStreamReader streamReader; + + + /** + * Construct a new instance of the {@code StaxSource} with the specified {@code XMLEventReader}. + * The supplied event reader must be in {@code XMLStreamConstants.START_DOCUMENT} or + * {@code XMLStreamConstants.START_ELEMENT} state. + * @param eventReader the {@code XMLEventReader} to read from + * @throws IllegalStateException if the reader is not at the start of a document or element + */ + StaxSource(XMLEventReader eventReader) { + super(new StaxEventXMLReader(eventReader), new InputSource()); + this.eventReader = eventReader; + } + + /** + * Construct a new instance of the {@code StaxSource} with the specified {@code XMLStreamReader}. + * The supplied stream reader must be in {@code XMLStreamConstants.START_DOCUMENT} or + * {@code XMLStreamConstants.START_ELEMENT} state. + * @param streamReader the {@code XMLStreamReader} to read from + * @throws IllegalStateException if the reader is not at the start of a document or element + */ + StaxSource(XMLStreamReader streamReader) { + super(new StaxStreamXMLReader(streamReader), new InputSource()); + this.streamReader = streamReader; + } + + + /** + * Return the {@code XMLEventReader} used by this {@code StaxSource}. + *

    If this {@code StaxSource} was created with an {@code XMLStreamReader}, + * the result will be {@code null}. + * @return the StAX event reader used by this source + * @see StaxSource#StaxSource(javax.xml.stream.XMLEventReader) + */ + @Nullable + XMLEventReader getXMLEventReader() { + return this.eventReader; + } + + /** + * Return the {@code XMLStreamReader} used by this {@code StaxSource}. + *

    If this {@code StaxSource} was created with an {@code XMLEventReader}, + * the result will be {@code null}. + * @return the StAX event reader used by this source + * @see StaxSource#StaxSource(javax.xml.stream.XMLEventReader) + */ + @Nullable + XMLStreamReader getXMLStreamReader() { + return this.streamReader; + } + + + /** + * Throws an {@code UnsupportedOperationException}. + * @throws UnsupportedOperationException always + */ + @Override + public void setInputSource(InputSource inputSource) { + throw new UnsupportedOperationException("setInputSource is not supported"); + } + + /** + * Throws an {@code UnsupportedOperationException}. + * @throws UnsupportedOperationException always + */ + @Override + public void setXMLReader(XMLReader reader) { + throw new UnsupportedOperationException("setXMLReader is not supported"); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/StaxStreamHandler.java b/spring-core/src/main/java/org/springframework/util/xml/StaxStreamHandler.java new file mode 100644 index 0000000..552ba48 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/StaxStreamHandler.java @@ -0,0 +1,137 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import java.util.Map; + +import javax.xml.XMLConstants; +import javax.xml.namespace.QName; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +import org.xml.sax.Attributes; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; +import org.xml.sax.ext.LexicalHandler; + +/** + * SAX {@link org.xml.sax.ContentHandler} and {@link LexicalHandler} + * that writes to an {@link XMLStreamWriter}. + * + * @author Arjen Poutsma + * @since 4.0.3 + */ +class StaxStreamHandler extends AbstractStaxHandler { + + private final XMLStreamWriter streamWriter; + + + public StaxStreamHandler(XMLStreamWriter streamWriter) { + this.streamWriter = streamWriter; + } + + + @Override + protected void startDocumentInternal() throws XMLStreamException { + this.streamWriter.writeStartDocument(); + } + + @Override + protected void endDocumentInternal() throws XMLStreamException { + this.streamWriter.writeEndDocument(); + } + + @Override + protected void startElementInternal(QName name, Attributes attributes, + Map namespaceMapping) throws XMLStreamException { + + this.streamWriter.writeStartElement(name.getPrefix(), name.getLocalPart(), name.getNamespaceURI()); + + for (Map.Entry entry : namespaceMapping.entrySet()) { + String prefix = entry.getKey(); + String namespaceUri = entry.getValue(); + this.streamWriter.writeNamespace(prefix, namespaceUri); + if (XMLConstants.DEFAULT_NS_PREFIX.equals(prefix)) { + this.streamWriter.setDefaultNamespace(namespaceUri); + } + else { + this.streamWriter.setPrefix(prefix, namespaceUri); + } + } + for (int i = 0; i < attributes.getLength(); i++) { + QName attrName = toQName(attributes.getURI(i), attributes.getQName(i)); + if (!isNamespaceDeclaration(attrName)) { + this.streamWriter.writeAttribute(attrName.getPrefix(), attrName.getNamespaceURI(), + attrName.getLocalPart(), attributes.getValue(i)); + } + } + } + + @Override + protected void endElementInternal(QName name, Map namespaceMapping) throws XMLStreamException { + this.streamWriter.writeEndElement(); + } + + @Override + protected void charactersInternal(String data) throws XMLStreamException { + this.streamWriter.writeCharacters(data); + } + + @Override + protected void cDataInternal(String data) throws XMLStreamException { + this.streamWriter.writeCData(data); + } + + @Override + protected void ignorableWhitespaceInternal(String data) throws XMLStreamException { + this.streamWriter.writeCharacters(data); + } + + @Override + protected void processingInstructionInternal(String target, String data) throws XMLStreamException { + this.streamWriter.writeProcessingInstruction(target, data); + } + + @Override + protected void dtdInternal(String dtd) throws XMLStreamException { + this.streamWriter.writeDTD(dtd); + } + + @Override + protected void commentInternal(String comment) throws XMLStreamException { + this.streamWriter.writeComment(comment); + } + + // Ignored + + @Override + public void setDocumentLocator(Locator locator) { + } + + @Override + public void startEntity(String name) throws SAXException { + } + + @Override + public void endEntity(String name) throws SAXException { + } + + @Override + protected void skippedEntityInternal(String name) throws XMLStreamException { + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/StaxStreamXMLReader.java b/spring-core/src/main/java/org/springframework/util/xml/StaxStreamXMLReader.java new file mode 100644 index 0000000..5812c43 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/StaxStreamXMLReader.java @@ -0,0 +1,308 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import javax.xml.namespace.QName; +import javax.xml.stream.Location; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.xml.sax.ext.Locator2; +import org.xml.sax.helpers.AttributesImpl; + +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * SAX {@code XMLReader} that reads from a StAX {@code XMLStreamReader}. Reads from an + * {@code XMLStreamReader}, and calls the corresponding methods on the SAX callback interfaces. + * + * @author Arjen Poutsma + * @since 3.0 + * @see XMLStreamReader + * @see #setContentHandler(org.xml.sax.ContentHandler) + * @see #setDTDHandler(org.xml.sax.DTDHandler) + * @see #setEntityResolver(org.xml.sax.EntityResolver) + * @see #setErrorHandler(org.xml.sax.ErrorHandler) + */ +class StaxStreamXMLReader extends AbstractStaxXMLReader { + + private static final String DEFAULT_XML_VERSION = "1.0"; + + private final XMLStreamReader reader; + + private String xmlVersion = DEFAULT_XML_VERSION; + + @Nullable + private String encoding; + + + /** + * Construct a new instance of the {@code StaxStreamXmlReader} that reads from the given + * {@code XMLStreamReader}. The supplied stream reader must be in {@code XMLStreamConstants.START_DOCUMENT} + * or {@code XMLStreamConstants.START_ELEMENT} state. + * @param reader the {@code XMLEventReader} to read from + * @throws IllegalStateException if the reader is not at the start of a document or element + */ + StaxStreamXMLReader(XMLStreamReader reader) { + int event = reader.getEventType(); + if (!(event == XMLStreamConstants.START_DOCUMENT || event == XMLStreamConstants.START_ELEMENT)) { + throw new IllegalStateException("XMLEventReader not at start of document or element"); + } + this.reader = reader; + } + + + @Override + protected void parseInternal() throws SAXException, XMLStreamException { + boolean documentStarted = false; + boolean documentEnded = false; + int elementDepth = 0; + int eventType = this.reader.getEventType(); + while (true) { + if (eventType != XMLStreamConstants.START_DOCUMENT && eventType != XMLStreamConstants.END_DOCUMENT && + !documentStarted) { + handleStartDocument(); + documentStarted = true; + } + switch (eventType) { + case XMLStreamConstants.START_ELEMENT: + elementDepth++; + handleStartElement(); + break; + case XMLStreamConstants.END_ELEMENT: + elementDepth--; + if (elementDepth >= 0) { + handleEndElement(); + } + break; + case XMLStreamConstants.PROCESSING_INSTRUCTION: + handleProcessingInstruction(); + break; + case XMLStreamConstants.CHARACTERS: + case XMLStreamConstants.SPACE: + case XMLStreamConstants.CDATA: + handleCharacters(); + break; + case XMLStreamConstants.START_DOCUMENT: + handleStartDocument(); + documentStarted = true; + break; + case XMLStreamConstants.END_DOCUMENT: + handleEndDocument(); + documentEnded = true; + break; + case XMLStreamConstants.COMMENT: + handleComment(); + break; + case XMLStreamConstants.DTD: + handleDtd(); + break; + case XMLStreamConstants.ENTITY_REFERENCE: + handleEntityReference(); + break; + } + if (this.reader.hasNext() && elementDepth >= 0) { + eventType = this.reader.next(); + } + else { + break; + } + } + if (!documentEnded) { + handleEndDocument(); + } + } + + private void handleStartDocument() throws SAXException { + if (XMLStreamConstants.START_DOCUMENT == this.reader.getEventType()) { + String xmlVersion = this.reader.getVersion(); + if (StringUtils.hasLength(xmlVersion)) { + this.xmlVersion = xmlVersion; + } + this.encoding = this.reader.getCharacterEncodingScheme(); + } + + ContentHandler contentHandler = getContentHandler(); + if (contentHandler != null) { + final Location location = this.reader.getLocation(); + contentHandler.setDocumentLocator(new Locator2() { + @Override + public int getColumnNumber() { + return (location != null ? location.getColumnNumber() : -1); + } + @Override + public int getLineNumber() { + return (location != null ? location.getLineNumber() : -1); + } + @Override + @Nullable + public String getPublicId() { + return (location != null ? location.getPublicId() : null); + } + @Override + @Nullable + public String getSystemId() { + return (location != null ? location.getSystemId() : null); + } + @Override + public String getXMLVersion() { + return xmlVersion; + } + @Override + @Nullable + public String getEncoding() { + return encoding; + } + }); + contentHandler.startDocument(); + if (this.reader.standaloneSet()) { + setStandalone(this.reader.isStandalone()); + } + } + } + + private void handleStartElement() throws SAXException { + if (getContentHandler() != null) { + QName qName = this.reader.getName(); + if (hasNamespacesFeature()) { + for (int i = 0; i < this.reader.getNamespaceCount(); i++) { + startPrefixMapping(this.reader.getNamespacePrefix(i), this.reader.getNamespaceURI(i)); + } + for (int i = 0; i < this.reader.getAttributeCount(); i++) { + String prefix = this.reader.getAttributePrefix(i); + String namespace = this.reader.getAttributeNamespace(i); + if (StringUtils.hasLength(namespace)) { + startPrefixMapping(prefix, namespace); + } + } + getContentHandler().startElement(qName.getNamespaceURI(), qName.getLocalPart(), + toQualifiedName(qName), getAttributes()); + } + else { + getContentHandler().startElement("", "", toQualifiedName(qName), getAttributes()); + } + } + } + + private void handleEndElement() throws SAXException { + if (getContentHandler() != null) { + QName qName = this.reader.getName(); + if (hasNamespacesFeature()) { + getContentHandler().endElement(qName.getNamespaceURI(), qName.getLocalPart(), toQualifiedName(qName)); + for (int i = 0; i < this.reader.getNamespaceCount(); i++) { + String prefix = this.reader.getNamespacePrefix(i); + if (prefix == null) { + prefix = ""; + } + endPrefixMapping(prefix); + } + } + else { + getContentHandler().endElement("", "", toQualifiedName(qName)); + } + } + } + + private void handleCharacters() throws SAXException { + if (XMLStreamConstants.CDATA == this.reader.getEventType() && getLexicalHandler() != null) { + getLexicalHandler().startCDATA(); + } + if (getContentHandler() != null) { + getContentHandler().characters(this.reader.getTextCharacters(), + this.reader.getTextStart(), this.reader.getTextLength()); + } + if (XMLStreamConstants.CDATA == this.reader.getEventType() && getLexicalHandler() != null) { + getLexicalHandler().endCDATA(); + } + } + + private void handleComment() throws SAXException { + if (getLexicalHandler() != null) { + getLexicalHandler().comment(this.reader.getTextCharacters(), + this.reader.getTextStart(), this.reader.getTextLength()); + } + } + + private void handleDtd() throws SAXException { + if (getLexicalHandler() != null) { + Location location = this.reader.getLocation(); + getLexicalHandler().startDTD(null, location.getPublicId(), location.getSystemId()); + } + if (getLexicalHandler() != null) { + getLexicalHandler().endDTD(); + } + } + + private void handleEntityReference() throws SAXException { + if (getLexicalHandler() != null) { + getLexicalHandler().startEntity(this.reader.getLocalName()); + } + if (getLexicalHandler() != null) { + getLexicalHandler().endEntity(this.reader.getLocalName()); + } + } + + private void handleEndDocument() throws SAXException { + if (getContentHandler() != null) { + getContentHandler().endDocument(); + } + } + + private void handleProcessingInstruction() throws SAXException { + if (getContentHandler() != null) { + getContentHandler().processingInstruction(this.reader.getPITarget(), this.reader.getPIData()); + } + } + + private Attributes getAttributes() { + AttributesImpl attributes = new AttributesImpl(); + for (int i = 0; i < this.reader.getAttributeCount(); i++) { + String namespace = this.reader.getAttributeNamespace(i); + if (namespace == null || !hasNamespacesFeature()) { + namespace = ""; + } + String type = this.reader.getAttributeType(i); + if (type == null) { + type = "CDATA"; + } + attributes.addAttribute(namespace, this.reader.getAttributeLocalName(i), + toQualifiedName(this.reader.getAttributeName(i)), type, this.reader.getAttributeValue(i)); + } + if (hasNamespacePrefixesFeature()) { + for (int i = 0; i < this.reader.getNamespaceCount(); i++) { + String prefix = this.reader.getNamespacePrefix(i); + String namespaceUri = this.reader.getNamespaceURI(i); + String qName; + if (StringUtils.hasLength(prefix)) { + qName = "xmlns:" + prefix; + } + else { + qName = "xmlns"; + } + attributes.addAttribute("", "", qName, "CDATA", namespaceUri); + } + } + + return attributes; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/StaxUtils.java b/spring-core/src/main/java/org/springframework/util/xml/StaxUtils.java new file mode 100644 index 0000000..f2aaf69 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/StaxUtils.java @@ -0,0 +1,330 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import java.util.List; +import java.util.function.Supplier; + +import javax.xml.stream.XMLEventFactory; +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLEventWriter; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLResolver; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; +import javax.xml.stream.XMLStreamWriter; +import javax.xml.stream.events.XMLEvent; +import javax.xml.transform.Result; +import javax.xml.transform.Source; +import javax.xml.transform.stax.StAXResult; +import javax.xml.transform.stax.StAXSource; + +import org.xml.sax.ContentHandler; +import org.xml.sax.XMLReader; + +import org.springframework.lang.Nullable; +import org.springframework.util.StreamUtils; + +/** + * Convenience methods for working with the StAX API. Partly historic due to JAXP 1.3 + * compatibility; as of Spring 4.0, relying on JAXP 1.4 as included in JDK 1.6 and higher. + * + *

    In particular, methods for using StAX ({@code javax.xml.stream}) in combination with + * the TrAX API ({@code javax.xml.transform}), and converting StAX readers/writers into SAX + * readers/handlers and vice-versa. + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @since 3.0 + */ +public abstract class StaxUtils { + + private static final XMLResolver NO_OP_XML_RESOLVER = + (publicID, systemID, base, ns) -> StreamUtils.emptyInput(); + + + /** + * Create an {@link XMLInputFactory} with Spring's defensive setup, + * i.e. no support for the resolution of DTDs and external entities. + * @return a new defensively initialized input factory instance to use + * @since 5.0 + */ + public static XMLInputFactory createDefensiveInputFactory() { + return createDefensiveInputFactory(XMLInputFactory::newInstance); + } + + /** + * Variant of {@link #createDefensiveInputFactory()} with a custom instance. + * @param instanceSupplier supplier for the input factory instance + * @return a new defensively initialized input factory instance to use + * @since 5.0.12 + */ + public static T createDefensiveInputFactory(Supplier instanceSupplier) { + T inputFactory = instanceSupplier.get(); + inputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false); + inputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false); + inputFactory.setXMLResolver(NO_OP_XML_RESOLVER); + return inputFactory; + } + + /** + * Create a JAXP 1.4 {@link StAXSource} for the given {@link XMLStreamReader}. + * @param streamReader the StAX stream reader + * @return a source wrapping the {@code streamReader} + */ + public static Source createStaxSource(XMLStreamReader streamReader) { + return new StAXSource(streamReader); + } + + /** + * Create a JAXP 1.4 {@link StAXSource} for the given {@link XMLEventReader}. + * @param eventReader the StAX event reader + * @return a source wrapping the {@code eventReader} + */ + public static Source createStaxSource(XMLEventReader eventReader) throws XMLStreamException { + return new StAXSource(eventReader); + } + + /** + * Create a custom, non-JAXP 1.4 StAX {@link Source} for the given {@link XMLStreamReader}. + * @param streamReader the StAX stream reader + * @return a source wrapping the {@code streamReader} + */ + public static Source createCustomStaxSource(XMLStreamReader streamReader) { + return new StaxSource(streamReader); + } + + /** + * Create a custom, non-JAXP 1.4 StAX {@link Source} for the given {@link XMLEventReader}. + * @param eventReader the StAX event reader + * @return a source wrapping the {@code eventReader} + */ + public static Source createCustomStaxSource(XMLEventReader eventReader) { + return new StaxSource(eventReader); + } + + /** + * Indicate whether the given {@link Source} is a JAXP 1.4 StAX Source or + * custom StAX Source. + * @return {@code true} if {@code source} is a JAXP 1.4 {@link StAXSource} or + * custom StAX Source; {@code false} otherwise + */ + public static boolean isStaxSource(Source source) { + return (source instanceof StAXSource || source instanceof StaxSource); + } + + /** + * Return the {@link XMLStreamReader} for the given StAX Source. + * @param source a JAXP 1.4 {@link StAXSource} + * @return the {@link XMLStreamReader} + * @throws IllegalArgumentException if {@code source} isn't a JAXP 1.4 {@link StAXSource} + * or custom StAX Source + */ + @Nullable + public static XMLStreamReader getXMLStreamReader(Source source) { + if (source instanceof StAXSource) { + return ((StAXSource) source).getXMLStreamReader(); + } + else if (source instanceof StaxSource) { + return ((StaxSource) source).getXMLStreamReader(); + } + else { + throw new IllegalArgumentException("Source '" + source + "' is neither StaxSource nor StAXSource"); + } + } + + /** + * Return the {@link XMLEventReader} for the given StAX Source. + * @param source a JAXP 1.4 {@link StAXSource} + * @return the {@link XMLEventReader} + * @throws IllegalArgumentException if {@code source} isn't a JAXP 1.4 {@link StAXSource} + * or custom StAX Source + */ + @Nullable + public static XMLEventReader getXMLEventReader(Source source) { + if (source instanceof StAXSource) { + return ((StAXSource) source).getXMLEventReader(); + } + else if (source instanceof StaxSource) { + return ((StaxSource) source).getXMLEventReader(); + } + else { + throw new IllegalArgumentException("Source '" + source + "' is neither StaxSource nor StAXSource"); + } + } + + /** + * Create a JAXP 1.4 {@link StAXResult} for the given {@link XMLStreamWriter}. + * @param streamWriter the StAX stream writer + * @return a result wrapping the {@code streamWriter} + */ + public static Result createStaxResult(XMLStreamWriter streamWriter) { + return new StAXResult(streamWriter); + } + + /** + * Create a JAXP 1.4 {@link StAXResult} for the given {@link XMLEventWriter}. + * @param eventWriter the StAX event writer + * @return a result wrapping {@code streamReader} + */ + public static Result createStaxResult(XMLEventWriter eventWriter) { + return new StAXResult(eventWriter); + } + + /** + * Create a custom, non-JAXP 1.4 StAX {@link Result} for the given {@link XMLStreamWriter}. + * @param streamWriter the StAX stream writer + * @return a source wrapping the {@code streamWriter} + */ + public static Result createCustomStaxResult(XMLStreamWriter streamWriter) { + return new StaxResult(streamWriter); + } + + /** + * Create a custom, non-JAXP 1.4 StAX {@link Result} for the given {@link XMLEventWriter}. + * @param eventWriter the StAX event writer + * @return a source wrapping the {@code eventWriter} + */ + public static Result createCustomStaxResult(XMLEventWriter eventWriter) { + return new StaxResult(eventWriter); + } + + /** + * Indicate whether the given {@link Result} is a JAXP 1.4 StAX Result or + * custom StAX Result. + * @return {@code true} if {@code result} is a JAXP 1.4 {@link StAXResult} or + * custom StAX Result; {@code false} otherwise + */ + public static boolean isStaxResult(Result result) { + return (result instanceof StAXResult || result instanceof StaxResult); + } + + /** + * Return the {@link XMLStreamWriter} for the given StAX Result. + * @param result a JAXP 1.4 {@link StAXResult} + * @return the {@link XMLStreamReader} + * @throws IllegalArgumentException if {@code source} isn't a JAXP 1.4 {@link StAXResult} + * or custom StAX Result + */ + @Nullable + public static XMLStreamWriter getXMLStreamWriter(Result result) { + if (result instanceof StAXResult) { + return ((StAXResult) result).getXMLStreamWriter(); + } + else if (result instanceof StaxResult) { + return ((StaxResult) result).getXMLStreamWriter(); + } + else { + throw new IllegalArgumentException("Result '" + result + "' is neither StaxResult nor StAXResult"); + } + } + + /** + * Return the {@link XMLEventWriter} for the given StAX Result. + * @param result a JAXP 1.4 {@link StAXResult} + * @return the {@link XMLStreamReader} + * @throws IllegalArgumentException if {@code source} isn't a JAXP 1.4 {@link StAXResult} + * or custom StAX Result + */ + @Nullable + public static XMLEventWriter getXMLEventWriter(Result result) { + if (result instanceof StAXResult) { + return ((StAXResult) result).getXMLEventWriter(); + } + else if (result instanceof StaxResult) { + return ((StaxResult) result).getXMLEventWriter(); + } + else { + throw new IllegalArgumentException("Result '" + result + "' is neither StaxResult nor StAXResult"); + } + } + + /** + * Create a {@link XMLEventReader} from the given list of {@link XMLEvent}. + * @param events the list of {@link XMLEvent XMLEvents}. + * @return an {@code XMLEventReader} that reads from the given events + * @since 5.0 + */ + public static XMLEventReader createXMLEventReader(List events) { + return new ListBasedXMLEventReader(events); + } + + /** + * Create a SAX {@link ContentHandler} that writes to the given StAX {@link XMLStreamWriter}. + * @param streamWriter the StAX stream writer + * @return a content handler writing to the {@code streamWriter} + */ + public static ContentHandler createContentHandler(XMLStreamWriter streamWriter) { + return new StaxStreamHandler(streamWriter); + } + + /** + * Create a SAX {@link ContentHandler} that writes events to the given StAX {@link XMLEventWriter}. + * @param eventWriter the StAX event writer + * @return a content handler writing to the {@code eventWriter} + */ + public static ContentHandler createContentHandler(XMLEventWriter eventWriter) { + return new StaxEventHandler(eventWriter); + } + + /** + * Create a SAX {@link XMLReader} that reads from the given StAX {@link XMLStreamReader}. + * @param streamReader the StAX stream reader + * @return a XMLReader reading from the {@code streamWriter} + */ + public static XMLReader createXMLReader(XMLStreamReader streamReader) { + return new StaxStreamXMLReader(streamReader); + } + + /** + * Create a SAX {@link XMLReader} that reads from the given StAX {@link XMLEventReader}. + * @param eventReader the StAX event reader + * @return a XMLReader reading from the {@code eventWriter} + */ + public static XMLReader createXMLReader(XMLEventReader eventReader) { + return new StaxEventXMLReader(eventReader); + } + + /** + * Return a {@link XMLStreamReader} that reads from a {@link XMLEventReader}. + * Useful because the StAX {@code XMLInputFactory} allows one to create an + * event reader from a stream reader, but not vice-versa. + * @return a stream reader that reads from an event reader + */ + public static XMLStreamReader createEventStreamReader(XMLEventReader eventReader) throws XMLStreamException { + return new XMLEventStreamReader(eventReader); + } + + /** + * Return a {@link XMLStreamWriter} that writes to a {@link XMLEventWriter}. + * @return a stream writer that writes to an event writer + * @since 3.2 + */ + public static XMLStreamWriter createEventStreamWriter(XMLEventWriter eventWriter) { + return new XMLEventStreamWriter(eventWriter, XMLEventFactory.newFactory()); + } + + /** + * Return a {@link XMLStreamWriter} that writes to a {@link XMLEventWriter}. + * @return a stream writer that writes to an event writer + * @since 3.0.5 + */ + public static XMLStreamWriter createEventStreamWriter(XMLEventWriter eventWriter, XMLEventFactory eventFactory) { + return new XMLEventStreamWriter(eventWriter, eventFactory); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/TransformerUtils.java b/spring-core/src/main/java/org/springframework/util/xml/TransformerUtils.java new file mode 100644 index 0000000..e6f6cd3 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/TransformerUtils.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; + +import org.springframework.util.Assert; + +/** + * Contains common behavior relating to {@link javax.xml.transform.Transformer Transformers} + * and the {@code javax.xml.transform} package in general. + * + * @author Rick Evans + * @author Juergen Hoeller + * @since 2.5.5 + */ +public abstract class TransformerUtils { + + /** + * The indent amount of characters if {@link #enableIndenting indenting is enabled}. + *

    Defaults to "2". + */ + public static final int DEFAULT_INDENT_AMOUNT = 2; + + + /** + * Enable indenting for the supplied {@link javax.xml.transform.Transformer}. + *

    If the underlying XSLT engine is Xalan, then the special output key {@code indent-amount} + * will be also be set to a value of {@link #DEFAULT_INDENT_AMOUNT} characters. + * @param transformer the target transformer + * @see javax.xml.transform.Transformer#setOutputProperty(String, String) + * @see javax.xml.transform.OutputKeys#INDENT + */ + public static void enableIndenting(Transformer transformer) { + enableIndenting(transformer, DEFAULT_INDENT_AMOUNT); + } + + /** + * Enable indenting for the supplied {@link javax.xml.transform.Transformer}. + *

    If the underlying XSLT engine is Xalan, then the special output key {@code indent-amount} + * will be also be set to a value of {@link #DEFAULT_INDENT_AMOUNT} characters. + * @param transformer the target transformer + * @param indentAmount the size of the indent (2 characters, 3 characters, etc) + * @see javax.xml.transform.Transformer#setOutputProperty(String, String) + * @see javax.xml.transform.OutputKeys#INDENT + */ + public static void enableIndenting(Transformer transformer, int indentAmount) { + Assert.notNull(transformer, "Transformer must not be null"); + if (indentAmount < 0) { + throw new IllegalArgumentException("Invalid indent amount (must not be less than zero): " + indentAmount); + } + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + try { + // Xalan-specific, but this is the most common XSLT engine in any case + transformer.setOutputProperty("{http://xml.apache.org/xalan}indent-amount", String.valueOf(indentAmount)); + } + catch (IllegalArgumentException ignored) { + } + } + + /** + * Disable indenting for the supplied {@link javax.xml.transform.Transformer}. + * @param transformer the target transformer + * @see javax.xml.transform.OutputKeys#INDENT + */ + public static void disableIndenting(Transformer transformer) { + Assert.notNull(transformer, "Transformer must not be null"); + transformer.setOutputProperty(OutputKeys.INDENT, "no"); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/XMLEventStreamReader.java b/spring-core/src/main/java/org/springframework/util/xml/XMLEventStreamReader.java new file mode 100644 index 0000000..7a27d68 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/XMLEventStreamReader.java @@ -0,0 +1,298 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import java.util.Iterator; + +import javax.xml.namespace.NamespaceContext; +import javax.xml.namespace.QName; +import javax.xml.stream.Location; +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.events.Attribute; +import javax.xml.stream.events.Comment; +import javax.xml.stream.events.Namespace; +import javax.xml.stream.events.ProcessingInstruction; +import javax.xml.stream.events.StartDocument; +import javax.xml.stream.events.XMLEvent; + +import org.springframework.lang.Nullable; + +/** + * Implementation of the {@link javax.xml.stream.XMLStreamReader} interface that wraps a + * {@link XMLEventReader}. Useful because the StAX {@link javax.xml.stream.XMLInputFactory} + * allows one to create a event reader from a stream reader, but not vice-versa. + * + * @author Arjen Poutsma + * @since 3.0 + * @see StaxUtils#createEventStreamReader(javax.xml.stream.XMLEventReader) + */ +class XMLEventStreamReader extends AbstractXMLStreamReader { + + private XMLEvent event; + + private final XMLEventReader eventReader; + + + public XMLEventStreamReader(XMLEventReader eventReader) throws XMLStreamException { + this.eventReader = eventReader; + this.event = eventReader.nextEvent(); + } + + + @Override + public QName getName() { + if (this.event.isStartElement()) { + return this.event.asStartElement().getName(); + } + else if (this.event.isEndElement()) { + return this.event.asEndElement().getName(); + } + else { + throw new IllegalStateException(); + } + } + + @Override + public Location getLocation() { + return this.event.getLocation(); + } + + @Override + public int getEventType() { + return this.event.getEventType(); + } + + @Override + @Nullable + public String getVersion() { + if (this.event.isStartDocument()) { + return ((StartDocument) this.event).getVersion(); + } + else { + return null; + } + } + + @Override + public Object getProperty(String name) throws IllegalArgumentException { + return this.eventReader.getProperty(name); + } + + @Override + public boolean isStandalone() { + if (this.event.isStartDocument()) { + return ((StartDocument) this.event).isStandalone(); + } + else { + throw new IllegalStateException(); + } + } + + @Override + public boolean standaloneSet() { + if (this.event.isStartDocument()) { + return ((StartDocument) this.event).standaloneSet(); + } + else { + throw new IllegalStateException(); + } + } + + @Override + @Nullable + public String getEncoding() { + return null; + } + + @Override + @Nullable + public String getCharacterEncodingScheme() { + return null; + } + + @Override + public String getPITarget() { + if (this.event.isProcessingInstruction()) { + return ((ProcessingInstruction) this.event).getTarget(); + } + else { + throw new IllegalStateException(); + } + } + + @Override + public String getPIData() { + if (this.event.isProcessingInstruction()) { + return ((ProcessingInstruction) this.event).getData(); + } + else { + throw new IllegalStateException(); + } + } + + @Override + public int getTextStart() { + return 0; + } + + @Override + public String getText() { + if (this.event.isCharacters()) { + return this.event.asCharacters().getData(); + } + else if (this.event.getEventType() == XMLEvent.COMMENT) { + return ((Comment) this.event).getText(); + } + else { + throw new IllegalStateException(); + } + } + + @Override + @SuppressWarnings("rawtypes") + public int getAttributeCount() { + if (!this.event.isStartElement()) { + throw new IllegalStateException(); + } + Iterator attributes = this.event.asStartElement().getAttributes(); + return countIterator(attributes); + } + + @Override + public boolean isAttributeSpecified(int index) { + return getAttribute(index).isSpecified(); + } + + @Override + public QName getAttributeName(int index) { + return getAttribute(index).getName(); + } + + @Override + public String getAttributeType(int index) { + return getAttribute(index).getDTDType(); + } + + @Override + public String getAttributeValue(int index) { + return getAttribute(index).getValue(); + } + + @SuppressWarnings("rawtypes") + private Attribute getAttribute(int index) { + if (!this.event.isStartElement()) { + throw new IllegalStateException(); + } + int count = 0; + Iterator attributes = this.event.asStartElement().getAttributes(); + while (attributes.hasNext()) { + Attribute attribute = (Attribute) attributes.next(); + if (count == index) { + return attribute; + } + else { + count++; + } + } + throw new IllegalArgumentException(); + } + + @Override + public NamespaceContext getNamespaceContext() { + if (this.event.isStartElement()) { + return this.event.asStartElement().getNamespaceContext(); + } + else { + throw new IllegalStateException(); + } + } + + @Override + @SuppressWarnings("rawtypes") + public int getNamespaceCount() { + Iterator namespaces; + if (this.event.isStartElement()) { + namespaces = this.event.asStartElement().getNamespaces(); + } + else if (this.event.isEndElement()) { + namespaces = this.event.asEndElement().getNamespaces(); + } + else { + throw new IllegalStateException(); + } + return countIterator(namespaces); + } + + @Override + public String getNamespacePrefix(int index) { + return getNamespace(index).getPrefix(); + } + + @Override + public String getNamespaceURI(int index) { + return getNamespace(index).getNamespaceURI(); + } + + @SuppressWarnings("rawtypes") + private Namespace getNamespace(int index) { + Iterator namespaces; + if (this.event.isStartElement()) { + namespaces = this.event.asStartElement().getNamespaces(); + } + else if (this.event.isEndElement()) { + namespaces = this.event.asEndElement().getNamespaces(); + } + else { + throw new IllegalStateException(); + } + int count = 0; + while (namespaces.hasNext()) { + Namespace namespace = (Namespace) namespaces.next(); + if (count == index) { + return namespace; + } + else { + count++; + } + } + throw new IllegalArgumentException(); + } + + @Override + public int next() throws XMLStreamException { + this.event = this.eventReader.nextEvent(); + return this.event.getEventType(); + } + + @Override + public void close() throws XMLStreamException { + this.eventReader.close(); + } + + + @SuppressWarnings("rawtypes") + private static int countIterator(Iterator iterator) { + int count = 0; + while (iterator.hasNext()) { + iterator.next(); + count++; + } + return count; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/XMLEventStreamWriter.java b/spring-core/src/main/java/org/springframework/util/xml/XMLEventStreamWriter.java new file mode 100644 index 0000000..6c6d6de --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/XMLEventStreamWriter.java @@ -0,0 +1,276 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import javax.xml.namespace.NamespaceContext; +import javax.xml.namespace.QName; +import javax.xml.stream.XMLEventFactory; +import javax.xml.stream.XMLEventWriter; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import javax.xml.stream.events.EndElement; +import javax.xml.stream.events.Namespace; +import javax.xml.stream.events.StartElement; + +/** + * Implementation of the {@link javax.xml.stream.XMLStreamWriter} interface + * that wraps an {@link XMLEventWriter}. + * + * @author Arjen Poutsma + * @since 3.0.5 + * @see StaxUtils#createEventStreamWriter(javax.xml.stream.XMLEventWriter, javax.xml.stream.XMLEventFactory) + */ +class XMLEventStreamWriter implements XMLStreamWriter { + + private static final String DEFAULT_ENCODING = "UTF-8"; + + private final XMLEventWriter eventWriter; + + private final XMLEventFactory eventFactory; + + private final List endElements = new ArrayList<>(); + + private boolean emptyElement = false; + + + public XMLEventStreamWriter(XMLEventWriter eventWriter, XMLEventFactory eventFactory) { + this.eventWriter = eventWriter; + this.eventFactory = eventFactory; + } + + + @Override + public void setNamespaceContext(NamespaceContext context) throws XMLStreamException { + this.eventWriter.setNamespaceContext(context); + } + + @Override + public NamespaceContext getNamespaceContext() { + return this.eventWriter.getNamespaceContext(); + } + + @Override + public void setPrefix(String prefix, String uri) throws XMLStreamException { + this.eventWriter.setPrefix(prefix, uri); + } + + @Override + public String getPrefix(String uri) throws XMLStreamException { + return this.eventWriter.getPrefix(uri); + } + + @Override + public void setDefaultNamespace(String uri) throws XMLStreamException { + this.eventWriter.setDefaultNamespace(uri); + } + + @Override + public Object getProperty(String name) throws IllegalArgumentException { + throw new IllegalArgumentException(); + } + + + @Override + public void writeStartDocument() throws XMLStreamException { + closeEmptyElementIfNecessary(); + this.eventWriter.add(this.eventFactory.createStartDocument()); + } + + @Override + public void writeStartDocument(String version) throws XMLStreamException { + closeEmptyElementIfNecessary(); + this.eventWriter.add(this.eventFactory.createStartDocument(DEFAULT_ENCODING, version)); + } + + @Override + public void writeStartDocument(String encoding, String version) throws XMLStreamException { + closeEmptyElementIfNecessary(); + this.eventWriter.add(this.eventFactory.createStartDocument(encoding, version)); + } + + @Override + public void writeStartElement(String localName) throws XMLStreamException { + closeEmptyElementIfNecessary(); + doWriteStartElement(this.eventFactory.createStartElement(new QName(localName), null, null)); + } + + @Override + public void writeStartElement(String namespaceURI, String localName) throws XMLStreamException { + closeEmptyElementIfNecessary(); + doWriteStartElement(this.eventFactory.createStartElement(new QName(namespaceURI, localName), null, null)); + } + + @Override + public void writeStartElement(String prefix, String localName, String namespaceURI) throws XMLStreamException { + closeEmptyElementIfNecessary(); + doWriteStartElement(this.eventFactory.createStartElement(new QName(namespaceURI, localName, prefix), null, null)); + } + + private void doWriteStartElement(StartElement startElement) throws XMLStreamException { + this.eventWriter.add(startElement); + this.endElements.add(this.eventFactory.createEndElement(startElement.getName(), startElement.getNamespaces())); + } + + @Override + public void writeEmptyElement(String localName) throws XMLStreamException { + closeEmptyElementIfNecessary(); + writeStartElement(localName); + this.emptyElement = true; + } + + @Override + public void writeEmptyElement(String namespaceURI, String localName) throws XMLStreamException { + closeEmptyElementIfNecessary(); + writeStartElement(namespaceURI, localName); + this.emptyElement = true; + } + + @Override + public void writeEmptyElement(String prefix, String localName, String namespaceURI) throws XMLStreamException { + closeEmptyElementIfNecessary(); + writeStartElement(prefix, localName, namespaceURI); + this.emptyElement = true; + } + + private void closeEmptyElementIfNecessary() throws XMLStreamException { + if (this.emptyElement) { + this.emptyElement = false; + writeEndElement(); + } + } + + @Override + public void writeEndElement() throws XMLStreamException { + closeEmptyElementIfNecessary(); + int last = this.endElements.size() - 1; + EndElement lastEndElement = this.endElements.remove(last); + this.eventWriter.add(lastEndElement); + } + + @Override + public void writeAttribute(String localName, String value) throws XMLStreamException { + this.eventWriter.add(this.eventFactory.createAttribute(localName, value)); + } + + @Override + public void writeAttribute(String namespaceURI, String localName, String value) throws XMLStreamException { + this.eventWriter.add(this.eventFactory.createAttribute(new QName(namespaceURI, localName), value)); + } + + @Override + public void writeAttribute(String prefix, String namespaceURI, String localName, String value) + throws XMLStreamException { + + this.eventWriter.add(this.eventFactory.createAttribute(prefix, namespaceURI, localName, value)); + } + + @Override + public void writeNamespace(String prefix, String namespaceURI) throws XMLStreamException { + doWriteNamespace(this.eventFactory.createNamespace(prefix, namespaceURI)); + } + + @Override + public void writeDefaultNamespace(String namespaceURI) throws XMLStreamException { + doWriteNamespace(this.eventFactory.createNamespace(namespaceURI)); + } + + @SuppressWarnings("rawtypes") + private void doWriteNamespace(Namespace namespace) throws XMLStreamException { + int last = this.endElements.size() - 1; + EndElement oldEndElement = this.endElements.get(last); + Iterator oldNamespaces = oldEndElement.getNamespaces(); + List newNamespaces = new ArrayList<>(); + while (oldNamespaces.hasNext()) { + Namespace oldNamespace = (Namespace) oldNamespaces.next(); + newNamespaces.add(oldNamespace); + } + newNamespaces.add(namespace); + EndElement newEndElement = this.eventFactory.createEndElement(oldEndElement.getName(), newNamespaces.iterator()); + this.eventWriter.add(namespace); + this.endElements.set(last, newEndElement); + } + + @Override + public void writeCharacters(String text) throws XMLStreamException { + closeEmptyElementIfNecessary(); + this.eventWriter.add(this.eventFactory.createCharacters(text)); + } + + @Override + public void writeCharacters(char[] text, int start, int len) throws XMLStreamException { + closeEmptyElementIfNecessary(); + this.eventWriter.add(this.eventFactory.createCharacters(new String(text, start, len))); + } + + @Override + public void writeCData(String data) throws XMLStreamException { + closeEmptyElementIfNecessary(); + this.eventWriter.add(this.eventFactory.createCData(data)); + } + + @Override + public void writeComment(String data) throws XMLStreamException { + closeEmptyElementIfNecessary(); + this.eventWriter.add(this.eventFactory.createComment(data)); + } + + @Override + public void writeProcessingInstruction(String target) throws XMLStreamException { + closeEmptyElementIfNecessary(); + this.eventWriter.add(this.eventFactory.createProcessingInstruction(target, "")); + } + + @Override + public void writeProcessingInstruction(String target, String data) throws XMLStreamException { + closeEmptyElementIfNecessary(); + this.eventWriter.add(this.eventFactory.createProcessingInstruction(target, data)); + } + + @Override + public void writeDTD(String dtd) throws XMLStreamException { + closeEmptyElementIfNecessary(); + this.eventWriter.add(this.eventFactory.createDTD(dtd)); + } + + @Override + public void writeEntityRef(String name) throws XMLStreamException { + closeEmptyElementIfNecessary(); + this.eventWriter.add(this.eventFactory.createEntityReference(name, null)); + } + + @Override + public void writeEndDocument() throws XMLStreamException { + closeEmptyElementIfNecessary(); + this.eventWriter.add(this.eventFactory.createEndDocument()); + } + + @Override + public void flush() throws XMLStreamException { + this.eventWriter.flush(); + } + + @Override + public void close() throws XMLStreamException { + closeEmptyElementIfNecessary(); + this.eventWriter.close(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/XmlValidationModeDetector.java b/spring-core/src/main/java/org/springframework/util/xml/XmlValidationModeDetector.java new file mode 100644 index 0000000..19814c2 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/XmlValidationModeDetector.java @@ -0,0 +1,203 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import java.io.BufferedReader; +import java.io.CharConversionException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Detects whether an XML stream is using DTD- or XSD-based validation. + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Sam Brannen + * @since 2.0 + */ +public class XmlValidationModeDetector { + + /** + * Indicates that the validation should be disabled. + */ + public static final int VALIDATION_NONE = 0; + + /** + * Indicates that the validation mode should be auto-guessed, since we cannot find + * a clear indication (probably choked on some special characters, or the like). + */ + public static final int VALIDATION_AUTO = 1; + + /** + * Indicates that DTD validation should be used (we found a "DOCTYPE" declaration). + */ + public static final int VALIDATION_DTD = 2; + + /** + * Indicates that XSD validation should be used (found no "DOCTYPE" declaration). + */ + public static final int VALIDATION_XSD = 3; + + + /** + * The token in a XML document that declares the DTD to use for validation + * and thus that DTD validation is being used. + */ + private static final String DOCTYPE = "DOCTYPE"; + + /** + * The token that indicates the start of an XML comment. + */ + private static final String START_COMMENT = ""; + + + /** + * Indicates whether or not the current parse position is inside an XML comment. + */ + private boolean inComment; + + + /** + * Detect the validation mode for the XML document in the supplied {@link InputStream}. + * Note that the supplied {@link InputStream} is closed by this method before returning. + * @param inputStream the InputStream to parse + * @throws IOException in case of I/O failure + * @see #VALIDATION_DTD + * @see #VALIDATION_XSD + */ + public int detectValidationMode(InputStream inputStream) throws IOException { + // Peek into the file to look for DOCTYPE. + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + boolean isDtdValidated = false; + String content; + while ((content = reader.readLine()) != null) { + content = consumeCommentTokens(content); + if (this.inComment || !StringUtils.hasText(content)) { + continue; + } + if (hasDoctype(content)) { + isDtdValidated = true; + break; + } + if (hasOpeningTag(content)) { + // End of meaningful data... + break; + } + } + return (isDtdValidated ? VALIDATION_DTD : VALIDATION_XSD); + } + catch (CharConversionException ex) { + // Choked on some character encoding... + // Leave the decision up to the caller. + return VALIDATION_AUTO; + } + } + + + /** + * Does the content contain the DTD DOCTYPE declaration? + */ + private boolean hasDoctype(String content) { + return content.contains(DOCTYPE); + } + + /** + * Does the supplied content contain an XML opening tag. If the parse state is currently + * in an XML comment then this method always returns false. It is expected that all comment + * tokens will have consumed for the supplied content before passing the remainder to this method. + */ + private boolean hasOpeningTag(String content) { + if (this.inComment) { + return false; + } + int openTagIndex = content.indexOf('<'); + return (openTagIndex > -1 && (content.length() > openTagIndex + 1) && + Character.isLetter(content.charAt(openTagIndex + 1))); + } + + /** + * Consume all leading and trailing comments in the given String and return + * the remaining content, which may be empty since the supplied content might + * be all comment data. + */ + @Nullable + private String consumeCommentTokens(String line) { + int indexOfStartComment = line.indexOf(START_COMMENT); + if (indexOfStartComment == -1 && !line.contains(END_COMMENT)) { + return line; + } + + String result = ""; + String currLine = line; + if (indexOfStartComment >= 0) { + result = line.substring(0, indexOfStartComment); + currLine = line.substring(indexOfStartComment); + } + + while ((currLine = consume(currLine)) != null) { + if (!this.inComment && !currLine.trim().startsWith(START_COMMENT)) { + return result + currLine; + } + } + return null; + } + + /** + * Consume the next comment token, update the "inComment" flag + * and return the remaining content. + */ + @Nullable + private String consume(String line) { + int index = (this.inComment ? endComment(line) : startComment(line)); + return (index == -1 ? null : line.substring(index)); + } + + /** + * Try to consume the {@link #START_COMMENT} token. + * @see #commentToken(String, String, boolean) + */ + private int startComment(String line) { + return commentToken(line, START_COMMENT, true); + } + + private int endComment(String line) { + return commentToken(line, END_COMMENT, false); + } + + /** + * Try to consume the supplied token against the supplied content and update the + * in comment parse state to the supplied value. Returns the index into the content + * which is after the token or -1 if the token is not found. + */ + private int commentToken(String line, String token, boolean inCommentIfPresent) { + int index = line.indexOf(token); + if (index > - 1) { + this.inComment = inCommentIfPresent; + } + return (index == -1 ? index : index + token.length()); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/xml/package-info.java b/spring-core/src/main/java/org/springframework/util/xml/package-info.java new file mode 100644 index 0000000..b077891 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/xml/package-info.java @@ -0,0 +1,10 @@ +/** + * Miscellaneous utility classes for XML parsing and transformation, + * such as error handlers that log warnings via Commons Logging. + */ +@NonNullApi +@NonNullFields +package org.springframework.util.xml; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/main/kotlin/org/springframework/core/env/PropertyResolverExtensions.kt b/spring-core/src/main/kotlin/org/springframework/core/env/PropertyResolverExtensions.kt new file mode 100644 index 0000000..c954a27 --- /dev/null +++ b/spring-core/src/main/kotlin/org/springframework/core/env/PropertyResolverExtensions.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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. + */ + +@file:Suppress("EXTENSION_SHADOWED_BY_MEMBER") + +package org.springframework.core.env + +/** + * Extension for [PropertyResolver.getProperty] providing Array like getter returning a + * nullable [String]. + * + * ```kotlin + * val name = env["name"] ?: "Seb" + * ``` + * + * @author Sebastien Deleuze + * @since 5.0 + */ +operator fun PropertyResolver.get(key: String) : String? = getProperty(key) + + +/** + * Extension for [PropertyResolver.getProperty] providing a `getProperty(...)` + * variant returning a nullable [String]. + * + * @author Sebastien Deleuze + * @since 5.1 + */ +inline fun PropertyResolver.getProperty(key: String) : T? = + getProperty(key, T::class.java) + +/** + * Extension for [PropertyResolver.getRequiredProperty] providing a + * `getRequiredProperty(...)` variant. + * + * @author Sebastien Deleuze + * @since 5.1 + */ +inline fun PropertyResolver.getRequiredProperty(key: String) : T = + getRequiredProperty(key, T::class.java) diff --git a/spring-core/src/test/java/example/type/AnnotatedComponent.java b/spring-core/src/test/java/example/type/AnnotatedComponent.java new file mode 100644 index 0000000..33fab84 --- /dev/null +++ b/spring-core/src/test/java/example/type/AnnotatedComponent.java @@ -0,0 +1,21 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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 example.type; + +@EnclosingAnnotation(nested2 = @NestedAnnotation) +public class AnnotatedComponent { +} diff --git a/spring-core/src/test/java/example/type/AnnotationTypeFilterTestsTypes.java b/spring-core/src/test/java/example/type/AnnotationTypeFilterTestsTypes.java new file mode 100644 index 0000000..25f6501 --- /dev/null +++ b/spring-core/src/test/java/example/type/AnnotationTypeFilterTestsTypes.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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 example.type; + +/** + * We must use a standalone set of types to ensure that no one else is loading + * them and interfering with + * {@link org.springframework.core.type.ClassloadingAssertions#assertClassNotLoaded(String)}. + * + * @author Ramnivas Laddad + * @author Juergen Hoeller + * @author Oliver Gierke + * @author Sam Brannen + * @see org.springframework.core.type.AnnotationTypeFilterTests + */ +public class AnnotationTypeFilterTestsTypes { + + @InheritedAnnotation + public static class SomeComponent { + } + + + @InheritedAnnotation + public interface SomeComponentInterface { + } + + + @SuppressWarnings("unused") + public static class SomeClassWithSomeComponentInterface implements Cloneable, SomeComponentInterface { + } + + + @SuppressWarnings("unused") + public static class SomeSubclassOfSomeComponent extends SomeComponent { + } + + @NonInheritedAnnotation + public static class SomeClassMarkedWithNonInheritedAnnotation { + } + + + @SuppressWarnings("unused") + public static class SomeSubclassOfSomeClassMarkedWithNonInheritedAnnotation extends SomeClassMarkedWithNonInheritedAnnotation { + } + + + @SuppressWarnings("unused") + public static class SomeNonCandidateClass { + } + +} diff --git a/spring-core/src/test/java/example/type/AspectJTypeFilterTestsTypes.java b/spring-core/src/test/java/example/type/AspectJTypeFilterTestsTypes.java new file mode 100644 index 0000000..089fe1b --- /dev/null +++ b/spring-core/src/test/java/example/type/AspectJTypeFilterTestsTypes.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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 example.type; + +import org.springframework.core.testfixture.stereotype.Component; + +/** + * We must use a standalone set of types to ensure that no one else is loading + * them and interfering with + * {@link org.springframework.core.type.ClassloadingAssertions#assertClassNotLoaded(String)}. + * + * @author Ramnivas Laddad + * @author Sam Brannen + * @see org.springframework.core.type.AspectJTypeFilterTests + */ +public class AspectJTypeFilterTestsTypes { + + public interface SomeInterface { + } + + public static class SomeClass { + } + + public static class SomeClassExtendingSomeClass extends SomeClass { + } + + public static class SomeClassImplementingSomeInterface implements SomeInterface { + } + + public static class SomeClassExtendingSomeClassExtendingSomeClassAndImplementingSomeInterface + extends SomeClassExtendingSomeClass implements SomeInterface { + } + + @Component + public static class SomeClassAnnotatedWithComponent { + } + +} diff --git a/spring-core/src/test/java/example/type/AssignableTypeFilterTestsTypes.java b/spring-core/src/test/java/example/type/AssignableTypeFilterTestsTypes.java new file mode 100644 index 0000000..ceb4da3 --- /dev/null +++ b/spring-core/src/test/java/example/type/AssignableTypeFilterTestsTypes.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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 example.type; + +/** + * We must use a standalone set of types to ensure that no one else is loading + * them and interfering with + * {@link org.springframework.core.type.ClassloadingAssertions#assertClassNotLoaded(String)}. + * + * @author Ramnivas Laddad + * @author Juergen Hoeller + * @author Sam Brannen + * @see org.springframework.core.type.AssignableTypeFilterTests + */ +public class AssignableTypeFilterTestsTypes { + + public static class TestNonInheritingClass { + } + + public interface TestInterface { + } + + public static class TestInterfaceImpl implements TestInterface { + } + + public interface SomeDaoLikeInterface { + } + + public static class SomeDaoLikeImpl extends SimpleJdbcDaoSupport implements SomeDaoLikeInterface { + } + + public interface JdbcDaoSupport { + } + + public static class SimpleJdbcDaoSupport implements JdbcDaoSupport { + } + +} diff --git a/spring-core/src/test/java/example/type/EnclosingAnnotation.java b/spring-core/src/test/java/example/type/EnclosingAnnotation.java new file mode 100644 index 0000000..8c9bbd2 --- /dev/null +++ b/spring-core/src/test/java/example/type/EnclosingAnnotation.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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 example.type; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.springframework.core.annotation.AliasFor; + +@Retention(RetentionPolicy.RUNTIME) +public @interface EnclosingAnnotation { + + @AliasFor("nested2") + NestedAnnotation nested1() default @NestedAnnotation; + + @AliasFor("nested1") + NestedAnnotation nested2() default @NestedAnnotation; + +} diff --git a/spring-core/src/test/java/example/type/InheritedAnnotation.java b/spring-core/src/test/java/example/type/InheritedAnnotation.java new file mode 100644 index 0000000..961a3f8 --- /dev/null +++ b/spring-core/src/test/java/example/type/InheritedAnnotation.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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 example.type; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +public @interface InheritedAnnotation { +} diff --git a/spring-core/src/test/java/example/type/NestedAnnotation.java b/spring-core/src/test/java/example/type/NestedAnnotation.java new file mode 100644 index 0000000..8d3789f --- /dev/null +++ b/spring-core/src/test/java/example/type/NestedAnnotation.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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 example.type; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.ANNOTATION_TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface NestedAnnotation { + + String name() default ""; +} diff --git a/spring-core/src/test/java/example/type/NonInheritedAnnotation.java b/spring-core/src/test/java/example/type/NonInheritedAnnotation.java new file mode 100644 index 0000000..1e3a118 --- /dev/null +++ b/spring-core/src/test/java/example/type/NonInheritedAnnotation.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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 example.type; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface NonInheritedAnnotation { +} diff --git a/spring-core/src/test/java/org/springframework/core/AttributeAccessorSupportTests.java b/spring-core/src/test/java/org/springframework/core/AttributeAccessorSupportTests.java new file mode 100644 index 0000000..bd56c91 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/AttributeAccessorSupportTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + * @author Sam Brannen + * @since 2.0 + */ +class AttributeAccessorSupportTests { + + private static final String NAME = "foo"; + + private static final String VALUE = "bar"; + + private AttributeAccessor attributeAccessor = new SimpleAttributeAccessorSupport(); + + @Test + void setAndGet() throws Exception { + this.attributeAccessor.setAttribute(NAME, VALUE); + assertThat(this.attributeAccessor.getAttribute(NAME)).isEqualTo(VALUE); + } + + @Test + void setAndHas() throws Exception { + assertThat(this.attributeAccessor.hasAttribute(NAME)).isFalse(); + this.attributeAccessor.setAttribute(NAME, VALUE); + assertThat(this.attributeAccessor.hasAttribute(NAME)).isTrue(); + } + + @Test + void remove() throws Exception { + assertThat(this.attributeAccessor.hasAttribute(NAME)).isFalse(); + this.attributeAccessor.setAttribute(NAME, VALUE); + assertThat(this.attributeAccessor.removeAttribute(NAME)).isEqualTo(VALUE); + assertThat(this.attributeAccessor.hasAttribute(NAME)).isFalse(); + } + + @Test + void attributeNames() throws Exception { + this.attributeAccessor.setAttribute(NAME, VALUE); + this.attributeAccessor.setAttribute("abc", "123"); + String[] attributeNames = this.attributeAccessor.attributeNames(); + Arrays.sort(attributeNames); + assertThat(Arrays.binarySearch(attributeNames, NAME) > -1).isTrue(); + assertThat(Arrays.binarySearch(attributeNames, "abc") > -1).isTrue(); + } + + @SuppressWarnings("serial") + private static class SimpleAttributeAccessorSupport extends AttributeAccessorSupport { + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/BridgeMethodResolverTests.java b/spring-core/src/test/java/org/springframework/core/BridgeMethodResolverTests.java new file mode 100644 index 0000000..00f9653 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/BridgeMethodResolverTests.java @@ -0,0 +1,1346 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.concurrent.DelayQueue; +import java.util.concurrent.Delayed; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + * @author Juergen Hoeller + * @author Chris Beams + */ +@SuppressWarnings("rawtypes") +class BridgeMethodResolverTests { + + private static Method findMethodWithReturnType(String name, Class returnType, Class targetType) { + Method[] methods = targetType.getMethods(); + for (Method m : methods) { + if (m.getName().equals(name) && m.getReturnType().equals(returnType)) { + return m; + } + } + return null; + } + + + @Test + void findBridgedMethod() throws Exception { + Method unbridged = MyFoo.class.getDeclaredMethod("someMethod", String.class, Object.class); + Method bridged = MyFoo.class.getDeclaredMethod("someMethod", Serializable.class, Object.class); + assertThat(unbridged.isBridge()).isFalse(); + assertThat(bridged.isBridge()).isTrue(); + + assertThat(BridgeMethodResolver.findBridgedMethod(unbridged)).as("Unbridged method not returned directly").isEqualTo(unbridged); + assertThat(BridgeMethodResolver.findBridgedMethod(bridged)).as("Incorrect bridged method returned").isEqualTo(unbridged); + } + + @Test + void findBridgedVarargMethod() throws Exception { + Method unbridged = MyFoo.class.getDeclaredMethod("someVarargMethod", String.class, Object[].class); + Method bridged = MyFoo.class.getDeclaredMethod("someVarargMethod", Serializable.class, Object[].class); + assertThat(unbridged.isBridge()).isFalse(); + assertThat(bridged.isBridge()).isTrue(); + + assertThat(BridgeMethodResolver.findBridgedMethod(unbridged)).as("Unbridged method not returned directly").isEqualTo(unbridged); + assertThat(BridgeMethodResolver.findBridgedMethod(bridged)).as("Incorrect bridged method returned").isEqualTo(unbridged); + } + + @Test + void findBridgedMethodInHierarchy() throws Exception { + Method bridgeMethod = DateAdder.class.getMethod("add", Object.class); + assertThat(bridgeMethod.isBridge()).isTrue(); + Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(bridgeMethod); + assertThat(bridgedMethod.isBridge()).isFalse(); + assertThat(bridgedMethod.getName()).isEqualTo("add"); + assertThat(bridgedMethod.getParameterCount()).isEqualTo(1); + assertThat(bridgedMethod.getParameterTypes()[0]).isEqualTo(Date.class); + } + + @Test + void isBridgeMethodFor() throws Exception { + Method bridged = MyBar.class.getDeclaredMethod("someMethod", String.class, Object.class); + Method other = MyBar.class.getDeclaredMethod("someMethod", Integer.class, Object.class); + Method bridge = MyBar.class.getDeclaredMethod("someMethod", Object.class, Object.class); + + assertThat(BridgeMethodResolver.isBridgeMethodFor(bridge, bridged, MyBar.class)).as("Should be bridge method").isTrue(); + assertThat(BridgeMethodResolver.isBridgeMethodFor(bridge, other, MyBar.class)).as("Should not be bridge method").isFalse(); + } + + @Test + void doubleParameterization() throws Exception { + Method objectBridge = MyBoo.class.getDeclaredMethod("foo", Object.class); + Method serializableBridge = MyBoo.class.getDeclaredMethod("foo", Serializable.class); + + Method stringFoo = MyBoo.class.getDeclaredMethod("foo", String.class); + Method integerFoo = MyBoo.class.getDeclaredMethod("foo", Integer.class); + + assertThat(BridgeMethodResolver.findBridgedMethod(objectBridge)).as("foo(String) not resolved.").isEqualTo(stringFoo); + assertThat(BridgeMethodResolver.findBridgedMethod(serializableBridge)).as("foo(Integer) not resolved.").isEqualTo(integerFoo); + } + + @Test + void findBridgedMethodFromMultipleBridges() throws Exception { + Method loadWithObjectReturn = findMethodWithReturnType("load", Object.class, SettingsDaoImpl.class); + assertThat(loadWithObjectReturn).isNotNull(); + + Method loadWithSettingsReturn = findMethodWithReturnType("load", Settings.class, SettingsDaoImpl.class); + assertThat(loadWithSettingsReturn).isNotNull(); + assertThat(loadWithSettingsReturn).isNotSameAs(loadWithObjectReturn); + + Method method = SettingsDaoImpl.class.getMethod("load"); + assertThat(BridgeMethodResolver.findBridgedMethod(loadWithObjectReturn)).isEqualTo(method); + assertThat(BridgeMethodResolver.findBridgedMethod(loadWithSettingsReturn)).isEqualTo(method); + } + + @Test + void findBridgedMethodFromParent() throws Exception { + Method loadFromParentBridge = SettingsDaoImpl.class.getMethod("loadFromParent"); + assertThat(loadFromParentBridge.isBridge()).isTrue(); + + Method loadFromParent = AbstractDaoImpl.class.getMethod("loadFromParent"); + assertThat(loadFromParent.isBridge()).isFalse(); + + assertThat(BridgeMethodResolver.findBridgedMethod(loadFromParentBridge)).isEqualTo(loadFromParent); + } + + @Test + void withSingleBoundParameterizedOnInstantiate() throws Exception { + Method bridgeMethod = DelayQueue.class.getMethod("add", Object.class); + assertThat(bridgeMethod.isBridge()).isTrue(); + Method actualMethod = DelayQueue.class.getMethod("add", Delayed.class); + assertThat(actualMethod.isBridge()).isFalse(); + assertThat(BridgeMethodResolver.findBridgedMethod(bridgeMethod)).isEqualTo(actualMethod); + } + + @Test + void withDoubleBoundParameterizedOnInstantiate() throws Exception { + Method bridgeMethod = SerializableBounded.class.getMethod("boundedOperation", Object.class); + assertThat(bridgeMethod.isBridge()).isTrue(); + Method actualMethod = SerializableBounded.class.getMethod("boundedOperation", HashMap.class); + assertThat(actualMethod.isBridge()).isFalse(); + assertThat(BridgeMethodResolver.findBridgedMethod(bridgeMethod)).isEqualTo(actualMethod); + } + + @Test + void withGenericParameter() throws Exception { + Method[] methods = StringGenericParameter.class.getMethods(); + Method bridgeMethod = null; + Method bridgedMethod = null; + for (Method method : methods) { + if ("getFor".equals(method.getName()) && !method.getParameterTypes()[0].equals(Integer.class)) { + if (method.getReturnType().equals(Object.class)) { + bridgeMethod = method; + } + else { + bridgedMethod = method; + } + } + } + assertThat(bridgeMethod != null && bridgeMethod.isBridge()).isTrue(); + boolean condition = bridgedMethod != null && !bridgedMethod.isBridge(); + assertThat(condition).isTrue(); + assertThat(BridgeMethodResolver.findBridgedMethod(bridgeMethod)).isEqualTo(bridgedMethod); + } + + @Test + void onAllMethods() throws Exception { + Method[] methods = StringList.class.getMethods(); + for (Method method : methods) { + assertThat(BridgeMethodResolver.findBridgedMethod(method)).isNotNull(); + } + } + + @Test + void spr2583() throws Exception { + Method bridgedMethod = MessageBroadcasterImpl.class.getMethod("receive", MessageEvent.class); + assertThat(bridgedMethod.isBridge()).isFalse(); + Method bridgeMethod = MessageBroadcasterImpl.class.getMethod("receive", Event.class); + assertThat(bridgeMethod.isBridge()).isTrue(); + + Method otherMethod = MessageBroadcasterImpl.class.getMethod("receive", NewMessageEvent.class); + assertThat(otherMethod.isBridge()).isFalse(); + + assertThat(BridgeMethodResolver.isBridgeMethodFor(bridgeMethod, otherMethod, MessageBroadcasterImpl.class)).as("Match identified incorrectly").isFalse(); + assertThat(BridgeMethodResolver.isBridgeMethodFor(bridgeMethod, bridgedMethod, MessageBroadcasterImpl.class)).as("Match not found correctly").isTrue(); + + assertThat(BridgeMethodResolver.findBridgedMethod(bridgeMethod)).isEqualTo(bridgedMethod); + } + + @Test + void spr2603() throws Exception { + Method objectBridge = YourHomer.class.getDeclaredMethod("foo", Bounded.class); + Method abstractBoundedFoo = YourHomer.class.getDeclaredMethod("foo", AbstractBounded.class); + + Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(objectBridge); + assertThat(bridgedMethod).as("foo(AbstractBounded) not resolved.").isEqualTo(abstractBoundedFoo); + } + + @Test + void spr2648() throws Exception { + Method bridgeMethod = ReflectionUtils.findMethod(GenericSqlMapIntegerDao.class, "saveOrUpdate", Object.class); + assertThat(bridgeMethod != null && bridgeMethod.isBridge()).isTrue(); + Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(bridgeMethod); + assertThat(bridgedMethod.isBridge()).isFalse(); + assertThat(bridgedMethod.getName()).isEqualTo("saveOrUpdate"); + } + + @Test + void spr2763() throws Exception { + Method bridgedMethod = AbstractDao.class.getDeclaredMethod("save", Object.class); + assertThat(bridgedMethod.isBridge()).isFalse(); + + Method bridgeMethod = UserDaoImpl.class.getDeclaredMethod("save", User.class); + assertThat(bridgeMethod.isBridge()).isTrue(); + + assertThat(BridgeMethodResolver.findBridgedMethod(bridgeMethod)).isEqualTo(bridgedMethod); + } + + @Test + void spr3041() throws Exception { + Method bridgedMethod = BusinessDao.class.getDeclaredMethod("save", Business.class); + assertThat(bridgedMethod.isBridge()).isFalse(); + + Method bridgeMethod = BusinessDao.class.getDeclaredMethod("save", Object.class); + assertThat(bridgeMethod.isBridge()).isTrue(); + + assertThat(BridgeMethodResolver.findBridgedMethod(bridgeMethod)).isEqualTo(bridgedMethod); + } + + @Test + void spr3173() throws Exception { + Method bridgedMethod = UserDaoImpl.class.getDeclaredMethod("saveVararg", User.class, Object[].class); + assertThat(bridgedMethod.isBridge()).isFalse(); + + Method bridgeMethod = UserDaoImpl.class.getDeclaredMethod("saveVararg", Object.class, Object[].class); + assertThat(bridgeMethod.isBridge()).isTrue(); + + assertThat(BridgeMethodResolver.findBridgedMethod(bridgeMethod)).isEqualTo(bridgedMethod); + } + + @Test + void spr3304() throws Exception { + Method bridgedMethod = MegaMessageProducerImpl.class.getDeclaredMethod("receive", MegaMessageEvent.class); + assertThat(bridgedMethod.isBridge()).isFalse(); + + Method bridgeMethod = MegaMessageProducerImpl.class.getDeclaredMethod("receive", MegaEvent.class); + assertThat(bridgeMethod.isBridge()).isTrue(); + + assertThat(BridgeMethodResolver.findBridgedMethod(bridgeMethod)).isEqualTo(bridgedMethod); + } + + @Test + void spr3324() throws Exception { + Method bridgedMethod = BusinessDao.class.getDeclaredMethod("get", Long.class); + assertThat(bridgedMethod.isBridge()).isFalse(); + + Method bridgeMethod = BusinessDao.class.getDeclaredMethod("get", Object.class); + assertThat(bridgeMethod.isBridge()).isTrue(); + + assertThat(BridgeMethodResolver.findBridgedMethod(bridgeMethod)).isEqualTo(bridgedMethod); + } + + @Test + void spr3357() throws Exception { + Method bridgedMethod = ExtendsAbstractImplementsInterface.class.getDeclaredMethod( + "doSomething", DomainObjectExtendsSuper.class, Object.class); + assertThat(bridgedMethod.isBridge()).isFalse(); + + Method bridgeMethod = ExtendsAbstractImplementsInterface.class.getDeclaredMethod( + "doSomething", DomainObjectSuper.class, Object.class); + assertThat(bridgeMethod.isBridge()).isTrue(); + + assertThat(BridgeMethodResolver.findBridgedMethod(bridgeMethod)).isEqualTo(bridgedMethod); + } + + @Test + void spr3485() throws Exception { + Method bridgedMethod = DomainObject.class.getDeclaredMethod( + "method2", ParameterType.class, byte[].class); + assertThat(bridgedMethod.isBridge()).isFalse(); + + Method bridgeMethod = DomainObject.class.getDeclaredMethod( + "method2", Serializable.class, Object.class); + assertThat(bridgeMethod.isBridge()).isTrue(); + + assertThat(BridgeMethodResolver.findBridgedMethod(bridgeMethod)).isEqualTo(bridgedMethod); + } + + @Test + void spr3534() throws Exception { + Method bridgeMethod = ReflectionUtils.findMethod(TestEmailProvider.class, "findBy", Object.class); + assertThat(bridgeMethod != null && bridgeMethod.isBridge()).isTrue(); + Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(bridgeMethod); + assertThat(bridgedMethod.isBridge()).isFalse(); + assertThat(bridgedMethod.getName()).isEqualTo("findBy"); + } + + @Test // SPR-16103 + void testClassHierarchy() throws Exception { + doTestHierarchyResolution(FooClass.class); + } + + @Test // SPR-16103 + void testInterfaceHierarchy() throws Exception { + doTestHierarchyResolution(FooInterface.class); + } + + private void doTestHierarchyResolution(Class clazz) throws Exception { + for (Method method : clazz.getDeclaredMethods()){ + Method bridged = BridgeMethodResolver.findBridgedMethod(method); + Method expected = clazz.getMethod("test", FooEntity.class); + assertThat(bridged).isEqualTo(expected); + } + } + + + public interface Foo { + + void someMethod(T theArg, Object otherArg); + + void someVarargMethod(T theArg, Object... otherArg); + } + + + public static class MyFoo implements Foo { + + public void someMethod(Integer theArg, Object otherArg) { + } + + @Override + public void someMethod(String theArg, Object otherArg) { + } + + @Override + public void someVarargMethod(String theArg, Object... otherArgs) { + } + } + + + public static abstract class Bar { + + void someMethod(Map m, Object otherArg) { + } + + void someMethod(T theArg, Map m) { + } + + abstract void someMethod(T theArg, Object otherArg); + } + + + public static abstract class InterBar extends Bar { + + } + + + public static class MyBar extends InterBar { + + @Override + public void someMethod(String theArg, Object otherArg) { + } + + public void someMethod(Integer theArg, Object otherArg) { + } + } + + + public interface Adder { + + void add(T item); + } + + + public static abstract class AbstractDateAdder implements Adder { + + @Override + public abstract void add(Date date); + } + + + public static class DateAdder extends AbstractDateAdder { + + @Override + public void add(Date date) { + } + } + + + public static class Enclosing { + + public class Enclosed { + + public class ReallyDeepNow { + + void someMethod(S s, T t, R r) { + } + } + } + } + + + public static class ExtendsEnclosing extends Enclosing { + + public class ExtendsEnclosed extends Enclosed { + + public class ExtendsReallyDeepNow extends ReallyDeepNow { + + @Override + void someMethod(Integer s, String t, Long r) { + throw new UnsupportedOperationException(); + } + } + } + } + + + public interface Boo { + + void foo(E e); + + void foo(T t); + } + + + public static class MyBoo implements Boo { + + @Override + public void foo(String e) { + throw new UnsupportedOperationException(); + } + + @Override + public void foo(Integer t) { + throw new UnsupportedOperationException(); + } + } + + + public interface Settings { + } + + + public interface ConcreteSettings extends Settings { + } + + + public interface Dao { + + T load(); + + S loadFromParent(); + } + + + public interface SettingsDao extends Dao { + + @Override + T load(); + } + + + public interface ConcreteSettingsDao extends SettingsDao { + + @Override + String loadFromParent(); + } + + + static abstract class AbstractDaoImpl implements Dao { + + protected T object; + + protected S otherObject; + + protected AbstractDaoImpl(T object, S otherObject) { + this.object = object; + this.otherObject = otherObject; + } + + // @Transactional(readOnly = true) + @Override + public S loadFromParent() { + return otherObject; + } + } + + + static class SettingsDaoImpl extends AbstractDaoImpl + implements ConcreteSettingsDao { + + protected SettingsDaoImpl(ConcreteSettings object) { + super(object, "From Parent"); + } + + // @Transactional(readOnly = true) + @Override + public ConcreteSettings load() { + return super.object; + } + } + + + public interface Bounded { + + boolean boundedOperation(E e); + } + + + private static class AbstractBounded implements Bounded { + + @Override + public boolean boundedOperation(E myE) { + return true; + } + } + + + private static class SerializableBounded extends AbstractBounded { + + @Override + public boolean boundedOperation(E myE) { + return false; + } + } + + + public interface GenericParameter { + + T getFor(Class cls); + } + + + @SuppressWarnings("unused") + private static class StringGenericParameter implements GenericParameter { + + @Override + public String getFor(Class cls) { + return "foo"; + } + + public String getFor(Integer integer) { + return "foo"; + } + } + + + private static class StringList implements List { + + @Override + public int size() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isEmpty() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean contains(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public Iterator iterator() { + throw new UnsupportedOperationException(); + } + + @Override + public Object[] toArray() { + throw new UnsupportedOperationException(); + } + + @Override + public T[] toArray(T[] a) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean add(String o) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(int index, Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @Override + public String get(int index) { + throw new UnsupportedOperationException(); + } + + @Override + public String set(int index, String element) { + throw new UnsupportedOperationException(); + } + + @Override + public void add(int index, String element) { + throw new UnsupportedOperationException(); + } + + @Override + public String remove(int index) { + throw new UnsupportedOperationException(); + } + + @Override + public int indexOf(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public int lastIndexOf(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public ListIterator listIterator() { + throw new UnsupportedOperationException(); + } + + @Override + public ListIterator listIterator(int index) { + throw new UnsupportedOperationException(); + } + + @Override + public List subList(int fromIndex, int toIndex) { + throw new UnsupportedOperationException(); + } + } + + + public interface Event { + + int getPriority(); + } + + + public static class GenericEvent implements Event { + + private int priority; + + @Override + public int getPriority() { + return priority; + } + + /** + * Constructor that takes an event priority + */ + public GenericEvent(int priority) { + this.priority = priority; + } + + /** + * Default Constructor + */ + public GenericEvent() { + } + } + + + public interface UserInitiatedEvent { + } + + + public static abstract class BaseUserInitiatedEvent extends GenericEvent implements UserInitiatedEvent { + } + + + public static class MessageEvent extends BaseUserInitiatedEvent { + } + + + public interface Channel { + + void send(E event); + + void subscribe(final Receiver receiver, Class event); + + void unsubscribe(final Receiver receiver, Class event); + } + + + public interface Broadcaster { + } + + + public interface EventBroadcaster extends Broadcaster { + + void subscribe(); + + void unsubscribe(); + + void setChannel(Channel channel); + } + + + public static class GenericBroadcasterImpl implements Broadcaster { + } + + + @SuppressWarnings({"unused", "unchecked"}) + public static abstract class GenericEventBroadcasterImpl + extends GenericBroadcasterImpl implements EventBroadcaster { + + private Class[] subscribingEvents; + + private Channel channel; + + /** + * Abstract method to retrieve instance of subclass + * + * @return receiver instance + */ + public abstract Receiver getInstance(); + + @Override + public void setChannel(Channel channel) { + this.channel = channel; + } + + private String beanName; + + public void setBeanName(String name) { + this.beanName = name; + } + + @Override + public void subscribe() { + } + + @Override + public void unsubscribe() { + } + + public GenericEventBroadcasterImpl(Class... events) { + } + } + + + public interface Receiver { + + void receive(E event); + } + + + public interface MessageBroadcaster extends Receiver { + + } + + + public static class RemovedMessageEvent extends MessageEvent { + + } + + + public static class NewMessageEvent extends MessageEvent { + + } + + + public static class ModifiedMessageEvent extends MessageEvent { + + } + + + @SuppressWarnings({"serial", "unchecked"}) + public static class MessageBroadcasterImpl extends GenericEventBroadcasterImpl + implements Serializable, // implement an unrelated interface first (SPR-16288) + MessageBroadcaster { + + public MessageBroadcasterImpl() { + super(NewMessageEvent.class); + } + + @Override + public void receive(MessageEvent event) { + throw new UnsupportedOperationException("should not be called, use subclassed events"); + } + + public void receive(NewMessageEvent event) { + } + + @Override + public Receiver getInstance() { + return null; + } + + public void receive(RemovedMessageEvent event) { + } + + public void receive(ModifiedMessageEvent event) { + } + } + + + //----------------------------- + // SPR-2454 Test Classes + //----------------------------- + + public interface SimpleGenericRepository { + + public Class getPersistentClass(); + + List findByQuery(); + + List findAll(); + + T refresh(T entity); + + T saveOrUpdate(T entity); + + void delete(Collection entities); + } + + + public interface RepositoryRegistry { + + SimpleGenericRepository getFor(Class entityType); + } + + + @SuppressWarnings("unchecked") + public static class SettableRepositoryRegistry> + implements RepositoryRegistry { + + protected void injectInto(R rep) { + } + + public void register(R rep) { + } + + public void register(R... reps) { + } + + public void setRepos(R... reps) { + } + + @Override + public SimpleGenericRepository getFor(Class entityType) { + return null; + } + + public void afterPropertiesSet() throws Exception { + } + } + + + public interface ConvenientGenericRepository + extends SimpleGenericRepository { + + T findById(ID id, boolean lock); + + List findByExample(T exampleInstance); + + void delete(ID id); + + void delete(T entity); + } + + + public static class GenericHibernateRepository + implements ConvenientGenericRepository { + + /** + * @param c Mandatory. The domain class this repository is responsible for. + */ + // Since it is impossible to determine the actual type of a type + // parameter (!), we resort to requiring the caller to provide the + // actual type as parameter, too. + // Not set in a constructor to enable easy CGLIB-proxying (passing + // constructor arguments to Spring AOP proxies is quite cumbersome). + public void setPersistentClass(Class c) { + } + + @Override + public Class getPersistentClass() { + return null; + } + + @Override + public T findById(ID id, boolean lock) { + return null; + } + + @Override + public List findAll() { + return null; + } + + @Override + public List findByExample(T exampleInstance) { + return null; + } + + @Override + public List findByQuery() { + return null; + } + + @Override + public T saveOrUpdate(T entity) { + return null; + } + + @Override + public void delete(T entity) { + } + + @Override + public T refresh(T entity) { + return null; + } + + @Override + public void delete(ID id) { + } + + @Override + public void delete(Collection entities) { + } + } + + + public static class HibernateRepositoryRegistry + extends SettableRepositoryRegistry> { + + @Override + public void injectInto(GenericHibernateRepository rep) { + } + + @Override + public GenericHibernateRepository getFor(Class entityType) { + return null; + } + } + + + //------------------- + // SPR-2603 classes + //------------------- + + public interface Homer { + + void foo(E e); + } + + + public static class MyHomer, L extends T> implements Homer { + + @Override + public void foo(L t) { + throw new UnsupportedOperationException(); + } + } + + + public static class YourHomer, L extends T> extends + MyHomer { + + @Override + public void foo(L t) { + throw new UnsupportedOperationException(); + } + } + + + public interface GenericDao { + + void saveOrUpdate(T t); + } + + + public interface ConvenienceGenericDao extends GenericDao { + } + + + public static class GenericSqlMapDao implements ConvenienceGenericDao { + + @Override + public void saveOrUpdate(T t) { + throw new UnsupportedOperationException(); + } + } + + + public static class GenericSqlMapIntegerDao extends GenericSqlMapDao { + + @Override + public void saveOrUpdate(T t) { + } + } + + + public static class Permission { + } + + + public static class User { + } + + + public interface UserDao { + + // @Transactional + void save(User user); + + // @Transactional + void save(Permission perm); + } + + + public static abstract class AbstractDao { + + public void save(T t) { + } + + public void saveVararg(T t, Object... args) { + } + } + + + public static class UserDaoImpl extends AbstractDao implements UserDao { + + @Override + public void save(Permission perm) { + } + + @Override + public void saveVararg(User user, Object... args) { + } + } + + + public interface DaoInterface { + + T get(P id); + } + + + public static abstract class BusinessGenericDao + implements DaoInterface { + + public void save(T object) { + } + } + + + public static class Business { + } + + + public static class BusinessDao extends BusinessGenericDao, Long> { + + @Override + public void save(Business business) { + } + + @Override + public Business get(Long id) { + return null; + } + + public Business get(String code) { + return null; + } + } + + + //------------------- + // SPR-3304 classes + //------------------- + + private static class MegaEvent { + } + + + private static class MegaMessageEvent extends MegaEvent { + } + + + private static class NewMegaMessageEvent extends MegaEvent { + } + + + private static class ModifiedMegaMessageEvent extends MegaEvent { + } + + + public interface MegaReceiver { + + void receive(E event); + } + + + public interface MegaMessageProducer extends MegaReceiver { + } + + + private static class Other { + } + + + @SuppressWarnings("unused") + private static class MegaMessageProducerImpl extends Other implements MegaMessageProducer { + + public void receive(NewMegaMessageEvent event) { + throw new UnsupportedOperationException(); + } + + public void receive(ModifiedMegaMessageEvent event) { + throw new UnsupportedOperationException(); + } + + @Override + public void receive(MegaMessageEvent event) { + throw new UnsupportedOperationException(); + } + } + + + //------------------- + // SPR-3357 classes + //------------------- + + private static class DomainObjectSuper { + } + + + private static class DomainObjectExtendsSuper extends DomainObjectSuper { + } + + + public interface IGenericInterface { + + void doSomething(final D domainObject, final T value); + } + + + @SuppressWarnings("unused") + private static abstract class AbstractImplementsInterface implements IGenericInterface { + + @Override + public void doSomething(D domainObject, T value) { + } + + public void anotherBaseMethod() { + } + } + + + private static class ExtendsAbstractImplementsInterface extends AbstractImplementsInterface { + + @Override + public void doSomething(DomainObjectExtendsSuper domainObject, T value) { + super.doSomething(domainObject, value); + } + } + + + //------------------- + // SPR-3485 classes + //------------------- + + @SuppressWarnings("serial") + private static class ParameterType implements Serializable { + } + + + private static class AbstractDomainObject

    { + + public R method1(P p) { + return null; + } + + public void method2(P p, R r) { + } + } + + + private static class DomainObject extends AbstractDomainObject { + + @Override + public byte[] method1(ParameterType p) { + return super.method1(p); + } + + @Override + public void method2(ParameterType p, byte[] r) { + super.method2(p, r); + } + } + + + //------------------- + // SPR-3534 classes + //------------------- + + public interface SearchProvider { + + Collection findBy(CONDITIONS_TYPE conditions); + } + + + public static class SearchConditions { + } + + + public interface IExternalMessageProvider> + extends SearchProvider { + } + + + public static class ExternalMessage { + } + + + public static class ExternalMessageSearchConditions extends SearchConditions { + } + + + public static class ExternalMessageProvider> + implements IExternalMessageProvider { + + @Override + public Collection findBy(T conditions) { + return null; + } + } + + + public static class EmailMessage extends ExternalMessage { + } + + + public static class EmailSearchConditions extends ExternalMessageSearchConditions { + } + + + public static class EmailMessageProvider extends ExternalMessageProvider { + } + + + public static class TestEmailProvider extends EmailMessageProvider { + + @Override + public Collection findBy(EmailSearchConditions conditions) { + return null; + } + } + + + //------------------- + // SPR-16103 classes + //------------------- + + public static abstract class BaseEntity { + } + + public static class FooEntity extends BaseEntity { + } + + public static class BaseClass { + + public S test(S T) { + return null; + } + } + + public static class EntityClass extends BaseClass { + + @Override + public S test(S T) { + return null; + } + } + + public static class FooClass extends EntityClass { + + @Override + public S test(S T) { + return null; + } + } + + public interface BaseInterface { + + S test(S T); + } + + public interface EntityInterface extends BaseInterface { + + @Override + S test(S T); + } + + public interface FooInterface extends EntityInterface { + + @Override + S test(S T); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/CollectionFactoryTests.java b/spring-core/src/test/java/org/springframework/core/CollectionFactoryTests.java new file mode 100644 index 0000000..56dbd93 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/CollectionFactoryTests.java @@ -0,0 +1,306 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.NavigableSet; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.springframework.core.CollectionFactory.createApproximateCollection; +import static org.springframework.core.CollectionFactory.createApproximateMap; +import static org.springframework.core.CollectionFactory.createCollection; +import static org.springframework.core.CollectionFactory.createMap; + +/** + * Unit tests for {@link CollectionFactory}. + * + * @author Oliver Gierke + * @author Sam Brannen + * @since 4.1.4 + */ +class CollectionFactoryTests { + + /** + * The test demonstrates that the generics-based API for + * {@link CollectionFactory#createApproximateCollection(Object, int)} + * is not type-safe. + *

    Specifically, the parameterized type {@code E} is not bound to + * the type of elements contained in the {@code collection} argument + * passed to {@code createApproximateCollection()}. Thus casting the + * value returned by {@link EnumSet#copyOf(EnumSet)} to + * {@code (Collection)} cannot guarantee that the returned collection + * actually contains elements of type {@code E}. + */ + @Test + void createApproximateCollectionIsNotTypeSafeForEnumSet() { + Collection ints = createApproximateCollection(EnumSet.of(Color.BLUE), 3); + + // Use a try-catch block to ensure that the exception is thrown as a result of the + // next line and not as a result of the previous line. + + // Note that ints is of type Collection, but the collection returned + // by createApproximateCollection() is of type Collection. Thus, 42 + // cannot be cast to a Color. + + assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> + ints.add(42)); + } + + @Test + void createCollectionIsNotTypeSafeForEnumSet() { + Collection ints = createCollection(EnumSet.class, Color.class, 3); + + // Use a try-catch block to ensure that the exception is thrown as a result of the + // next line and not as a result of the previous line. + + // Note that ints is of type Collection, but the collection returned + // by createCollection() is of type Collection. Thus, 42 cannot be cast + // to a Color. + + assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> + ints.add(42)); + } + + /** + * The test demonstrates that the generics-based API for + * {@link CollectionFactory#createApproximateMap(Object, int)} + * is not type-safe. + *

    The reasoning is similar that described in + * {@link #createApproximateCollectionIsNotTypeSafeForEnumSet}. + */ + @Test + void createApproximateMapIsNotTypeSafeForEnumMap() { + EnumMap enumMap = new EnumMap<>(Color.class); + enumMap.put(Color.RED, 1); + enumMap.put(Color.BLUE, 2); + Map map = createApproximateMap(enumMap, 3); + + // Use a try-catch block to ensure that the exception is thrown as a result of the + // next line and not as a result of the previous line. + + // Note that the 'map' key must be of type String, but the keys in the map + // returned by createApproximateMap() are of type Color. Thus "foo" cannot be + // cast to a Color. + + assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> + map.put("foo", 1)); + } + + @Test + void createMapIsNotTypeSafeForEnumMap() { + Map map = createMap(EnumMap.class, Color.class, 3); + + // Use a try-catch block to ensure that the exception is thrown as a result of the + // next line and not as a result of the previous line. + + // Note that the 'map' key must be of type String, but the keys in the map + // returned by createMap() are of type Color. Thus "foo" cannot be cast to a + // Color. + + assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> + map.put("foo", 1)); + } + + @Test + void createMapIsNotTypeSafeForLinkedMultiValueMap() { + Map map = createMap(MultiValueMap.class, null, 3); + + // Use a try-catch block to ensure that the exception is thrown as a result of the + // next line and not as a result of the previous line. + + // Note: 'map' values must be of type Integer, but the values in the map + // returned by createMap() are of type java.util.List. Thus 1 cannot be + // cast to a List. + + assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> + map.put("foo", 1)); + } + + @Test + void createApproximateCollectionFromEmptyHashSet() { + Collection set = createApproximateCollection(new HashSet(), 2); + Assertions.assertThat(set).isEmpty(); + } + + @Test + void createApproximateCollectionFromNonEmptyHashSet() { + HashSet hashSet = new HashSet<>(); + hashSet.add("foo"); + Collection set = createApproximateCollection(hashSet, 2); + assertThat(set).isEmpty(); + } + + @Test + void createApproximateCollectionFromEmptyEnumSet() { + Collection colors = createApproximateCollection(EnumSet.noneOf(Color.class), 2); + assertThat(colors).isEmpty(); + } + + @Test + void createApproximateCollectionFromNonEmptyEnumSet() { + Collection colors = createApproximateCollection(EnumSet.of(Color.BLUE), 2); + assertThat(colors).isEmpty(); + } + + @Test + void createApproximateMapFromEmptyHashMap() { + Map map = createApproximateMap(new HashMap(), 2); + assertThat(map).isEmpty(); + } + + @Test + void createApproximateMapFromNonEmptyHashMap() { + Map hashMap = new HashMap<>(); + hashMap.put("foo", "bar"); + Map map = createApproximateMap(hashMap, 2); + assertThat(map).isEmpty(); + } + + @Test + void createApproximateMapFromEmptyEnumMap() { + Map colors = createApproximateMap(new EnumMap(Color.class), 2); + assertThat(colors).isEmpty(); + } + + @Test + void createApproximateMapFromNonEmptyEnumMap() { + EnumMap enumMap = new EnumMap<>(Color.class); + enumMap.put(Color.BLUE, "blue"); + Map colors = createApproximateMap(enumMap, 2); + assertThat(colors).isEmpty(); + } + + @Test + void createsCollectionsCorrectly() { + // interfaces + assertThat(createCollection(List.class, 0)).isInstanceOf(ArrayList.class); + assertThat(createCollection(Set.class, 0)).isInstanceOf(LinkedHashSet.class); + assertThat(createCollection(Collection.class, 0)).isInstanceOf(LinkedHashSet.class); + assertThat(createCollection(SortedSet.class, 0)).isInstanceOf(TreeSet.class); + assertThat(createCollection(NavigableSet.class, 0)).isInstanceOf(TreeSet.class); + + assertThat(createCollection(List.class, String.class, 0)).isInstanceOf(ArrayList.class); + assertThat(createCollection(Set.class, String.class, 0)).isInstanceOf(LinkedHashSet.class); + assertThat(createCollection(Collection.class, String.class, 0)).isInstanceOf(LinkedHashSet.class); + assertThat(createCollection(SortedSet.class, String.class, 0)).isInstanceOf(TreeSet.class); + assertThat(createCollection(NavigableSet.class, String.class, 0)).isInstanceOf(TreeSet.class); + + // concrete types + assertThat(createCollection(HashSet.class, 0)).isInstanceOf(HashSet.class); + assertThat(createCollection(HashSet.class, String.class, 0)).isInstanceOf(HashSet.class); + } + + @Test + void createsEnumSet() { + assertThat(createCollection(EnumSet.class, Color.class, 0)).isInstanceOf(EnumSet.class); + } + + @Test // SPR-17619 + void createsEnumSetSubclass() { + EnumSet enumSet = EnumSet.noneOf(Color.class); + assertThat(createCollection(enumSet.getClass(), Color.class, 0)).isInstanceOf(enumSet.getClass()); + } + + @Test + void rejectsInvalidElementTypeForEnumSet() { + assertThatIllegalArgumentException().isThrownBy(() -> + createCollection(EnumSet.class, Object.class, 0)); + } + + @Test + void rejectsNullElementTypeForEnumSet() { + assertThatIllegalArgumentException().isThrownBy(() -> + createCollection(EnumSet.class, null, 0)); + } + + @Test + void rejectsNullCollectionType() { + assertThatIllegalArgumentException().isThrownBy(() -> + createCollection(null, Object.class, 0)); + } + + @Test + void createsMapsCorrectly() { + // interfaces + assertThat(createMap(Map.class, 0)).isInstanceOf(LinkedHashMap.class); + assertThat(createMap(SortedMap.class, 0)).isInstanceOf(TreeMap.class); + assertThat(createMap(NavigableMap.class, 0)).isInstanceOf(TreeMap.class); + assertThat(createMap(MultiValueMap.class, 0)).isInstanceOf(LinkedMultiValueMap.class); + + assertThat(createMap(Map.class, String.class, 0)).isInstanceOf(LinkedHashMap.class); + assertThat(createMap(SortedMap.class, String.class, 0)).isInstanceOf(TreeMap.class); + assertThat(createMap(NavigableMap.class, String.class, 0)).isInstanceOf(TreeMap.class); + assertThat(createMap(MultiValueMap.class, String.class, 0)).isInstanceOf(LinkedMultiValueMap.class); + + // concrete types + assertThat(createMap(HashMap.class, 0)).isInstanceOf(HashMap.class); + + assertThat(createMap(HashMap.class, String.class, 0)).isInstanceOf(HashMap.class); + } + + @Test + void createsEnumMap() { + assertThat(createMap(EnumMap.class, Color.class, 0)).isInstanceOf(EnumMap.class); + } + + @Test + void rejectsInvalidKeyTypeForEnumMap() { + assertThatIllegalArgumentException().isThrownBy(() -> + createMap(EnumMap.class, Object.class, 0)); + } + + @Test + void rejectsNullKeyTypeForEnumMap() { + assertThatIllegalArgumentException().isThrownBy(() -> + createMap(EnumMap.class, null, 0)); + } + + @Test + void rejectsNullMapType() { + assertThatIllegalArgumentException().isThrownBy(() -> + createMap(null, Object.class, 0)); + } + + + enum Color { + RED, BLUE; + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/ConstantsTests.java b/spring-core/src/test/java/org/springframework/core/ConstantsTests.java new file mode 100644 index 0000000..377f6f8 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/ConstantsTests.java @@ -0,0 +1,256 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.util.Locale; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Rod Johnson + * @author Juergen Hoeller + * @author Rick Evans + * @since 28.04.2003 + */ +class ConstantsTests { + + @Test + void constants() { + Constants c = new Constants(A.class); + assertThat(c.getClassName()).isEqualTo(A.class.getName()); + assertThat(c.getSize()).isEqualTo(9); + + assertThat(c.asNumber("DOG").intValue()).isEqualTo(A.DOG); + assertThat(c.asNumber("dog").intValue()).isEqualTo(A.DOG); + assertThat(c.asNumber("cat").intValue()).isEqualTo(A.CAT); + + assertThatExceptionOfType(Constants.ConstantException.class).isThrownBy(() -> + c.asNumber("bogus")); + + assertThat(c.asString("S1").equals(A.S1)).isTrue(); + assertThatExceptionOfType(Constants.ConstantException.class).as("wrong type").isThrownBy(() -> + c.asNumber("S1")); + } + + @Test + void getNames() { + Constants c = new Constants(A.class); + + Set names = c.getNames(""); + assertThat(names.size()).isEqualTo(c.getSize()); + assertThat(names.contains("DOG")).isTrue(); + assertThat(names.contains("CAT")).isTrue(); + assertThat(names.contains("S1")).isTrue(); + + names = c.getNames("D"); + assertThat(names.size()).isEqualTo(1); + assertThat(names.contains("DOG")).isTrue(); + + names = c.getNames("d"); + assertThat(names.size()).isEqualTo(1); + assertThat(names.contains("DOG")).isTrue(); + } + + @Test + void getValues() { + Constants c = new Constants(A.class); + + Set values = c.getValues(""); + assertThat(values.size()).isEqualTo(7); + assertThat(values.contains(Integer.valueOf(0))).isTrue(); + assertThat(values.contains(Integer.valueOf(66))).isTrue(); + assertThat(values.contains("")).isTrue(); + + values = c.getValues("D"); + assertThat(values.size()).isEqualTo(1); + assertThat(values.contains(Integer.valueOf(0))).isTrue(); + + values = c.getValues("prefix"); + assertThat(values.size()).isEqualTo(2); + assertThat(values.contains(Integer.valueOf(1))).isTrue(); + assertThat(values.contains(Integer.valueOf(2))).isTrue(); + + values = c.getValuesForProperty("myProperty"); + assertThat(values.size()).isEqualTo(2); + assertThat(values.contains(Integer.valueOf(1))).isTrue(); + assertThat(values.contains(Integer.valueOf(2))).isTrue(); + } + + @Test + void getValuesInTurkey() { + Locale oldLocale = Locale.getDefault(); + Locale.setDefault(new Locale("tr", "")); + try { + Constants c = new Constants(A.class); + + Set values = c.getValues(""); + assertThat(values.size()).isEqualTo(7); + assertThat(values.contains(Integer.valueOf(0))).isTrue(); + assertThat(values.contains(Integer.valueOf(66))).isTrue(); + assertThat(values.contains("")).isTrue(); + + values = c.getValues("D"); + assertThat(values.size()).isEqualTo(1); + assertThat(values.contains(Integer.valueOf(0))).isTrue(); + + values = c.getValues("prefix"); + assertThat(values.size()).isEqualTo(2); + assertThat(values.contains(Integer.valueOf(1))).isTrue(); + assertThat(values.contains(Integer.valueOf(2))).isTrue(); + + values = c.getValuesForProperty("myProperty"); + assertThat(values.size()).isEqualTo(2); + assertThat(values.contains(Integer.valueOf(1))).isTrue(); + assertThat(values.contains(Integer.valueOf(2))).isTrue(); + } + finally { + Locale.setDefault(oldLocale); + } + } + + @Test + void suffixAccess() { + Constants c = new Constants(A.class); + + Set names = c.getNamesForSuffix("_PROPERTY"); + assertThat(names.size()).isEqualTo(2); + assertThat(names.contains("NO_PROPERTY")).isTrue(); + assertThat(names.contains("YES_PROPERTY")).isTrue(); + + Set values = c.getValuesForSuffix("_PROPERTY"); + assertThat(values.size()).isEqualTo(2); + assertThat(values.contains(Integer.valueOf(3))).isTrue(); + assertThat(values.contains(Integer.valueOf(4))).isTrue(); + } + + @Test + void toCode() { + Constants c = new Constants(A.class); + + assertThat(c.toCode(Integer.valueOf(0), "")).isEqualTo("DOG"); + assertThat(c.toCode(Integer.valueOf(0), "D")).isEqualTo("DOG"); + assertThat(c.toCode(Integer.valueOf(0), "DO")).isEqualTo("DOG"); + assertThat(c.toCode(Integer.valueOf(0), "DoG")).isEqualTo("DOG"); + assertThat(c.toCode(Integer.valueOf(0), null)).isEqualTo("DOG"); + assertThat(c.toCode(Integer.valueOf(66), "")).isEqualTo("CAT"); + assertThat(c.toCode(Integer.valueOf(66), "C")).isEqualTo("CAT"); + assertThat(c.toCode(Integer.valueOf(66), "ca")).isEqualTo("CAT"); + assertThat(c.toCode(Integer.valueOf(66), "cAt")).isEqualTo("CAT"); + assertThat(c.toCode(Integer.valueOf(66), null)).isEqualTo("CAT"); + assertThat(c.toCode("", "")).isEqualTo("S1"); + assertThat(c.toCode("", "s")).isEqualTo("S1"); + assertThat(c.toCode("", "s1")).isEqualTo("S1"); + assertThat(c.toCode("", null)).isEqualTo("S1"); + assertThatExceptionOfType(Constants.ConstantException.class).isThrownBy(() -> + c.toCode("bogus", "bogus")); + assertThatExceptionOfType(Constants.ConstantException.class).isThrownBy(() -> + c.toCode("bogus", null)); + + assertThat(c.toCodeForProperty(Integer.valueOf(1), "myProperty")).isEqualTo("MY_PROPERTY_NO"); + assertThat(c.toCodeForProperty(Integer.valueOf(2), "myProperty")).isEqualTo("MY_PROPERTY_YES"); + assertThatExceptionOfType(Constants.ConstantException.class).isThrownBy(() -> + c.toCodeForProperty("bogus", "bogus")); + + assertThat(c.toCodeForSuffix(Integer.valueOf(0), "")).isEqualTo("DOG"); + assertThat(c.toCodeForSuffix(Integer.valueOf(0), "G")).isEqualTo("DOG"); + assertThat(c.toCodeForSuffix(Integer.valueOf(0), "OG")).isEqualTo("DOG"); + assertThat(c.toCodeForSuffix(Integer.valueOf(0), "DoG")).isEqualTo("DOG"); + assertThat(c.toCodeForSuffix(Integer.valueOf(0), null)).isEqualTo("DOG"); + assertThat(c.toCodeForSuffix(Integer.valueOf(66), "")).isEqualTo("CAT"); + assertThat(c.toCodeForSuffix(Integer.valueOf(66), "T")).isEqualTo("CAT"); + assertThat(c.toCodeForSuffix(Integer.valueOf(66), "at")).isEqualTo("CAT"); + assertThat(c.toCodeForSuffix(Integer.valueOf(66), "cAt")).isEqualTo("CAT"); + assertThat(c.toCodeForSuffix(Integer.valueOf(66), null)).isEqualTo("CAT"); + assertThat(c.toCodeForSuffix("", "")).isEqualTo("S1"); + assertThat(c.toCodeForSuffix("", "1")).isEqualTo("S1"); + assertThat(c.toCodeForSuffix("", "s1")).isEqualTo("S1"); + assertThat(c.toCodeForSuffix("", null)).isEqualTo("S1"); + assertThatExceptionOfType(Constants.ConstantException.class).isThrownBy(() -> + c.toCodeForSuffix("bogus", "bogus")); + assertThatExceptionOfType(Constants.ConstantException.class).isThrownBy(() -> + c.toCodeForSuffix("bogus", null)); + } + + @Test + void getValuesWithNullPrefix() throws Exception { + Constants c = new Constants(A.class); + Set values = c.getValues(null); + assertThat(values.size()).as("Must have returned *all* public static final values").isEqualTo(7); + } + + @Test + void getValuesWithEmptyStringPrefix() throws Exception { + Constants c = new Constants(A.class); + Set values = c.getValues(""); + assertThat(values.size()).as("Must have returned *all* public static final values").isEqualTo(7); + } + + @Test + void getValuesWithWhitespacedStringPrefix() throws Exception { + Constants c = new Constants(A.class); + Set values = c.getValues(" "); + assertThat(values.size()).as("Must have returned *all* public static final values").isEqualTo(7); + } + + @Test + void withClassThatExposesNoConstants() throws Exception { + Constants c = new Constants(NoConstants.class); + assertThat(c.getSize()).isEqualTo(0); + final Set values = c.getValues(""); + assertThat(values).isNotNull(); + assertThat(values.size()).isEqualTo(0); + } + + @Test + void ctorWithNullClass() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new Constants(null)); + } + + + private static final class NoConstants { + } + + + @SuppressWarnings("unused") + private static final class A { + + public static final int DOG = 0; + public static final int CAT = 66; + public static final String S1 = ""; + + public static final int PREFIX_NO = 1; + public static final int PREFIX_YES = 2; + + public static final int MY_PROPERTY_NO = 1; + public static final int MY_PROPERTY_YES = 2; + + public static final int NO_PROPERTY = 3; + public static final int YES_PROPERTY = 4; + + /** ignore these */ + protected static final int P = -1; + protected boolean f; + static final Object o = new Object(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/ConventionsTests.java b/spring-core/src/test/java/org/springframework/core/ConventionsTests.java new file mode 100644 index 0000000..ada7e89 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/ConventionsTests.java @@ -0,0 +1,154 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Single; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.tests.sample.objects.TestObject; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Unit tests for {@link Conventions}. + * + * @author Rob Harrop + * @author Sam Brannen + */ +class ConventionsTests { + + @Test + void simpleObject() { + assertThat(Conventions.getVariableName(new TestObject())).as("Incorrect singular variable name").isEqualTo("testObject"); + assertThat(Conventions.getVariableNameForParameter(getMethodParameter(TestObject.class))).as("Incorrect singular variable name").isEqualTo("testObject"); + assertThat(Conventions.getVariableNameForReturnType(getMethodForReturnType(TestObject.class))).as("Incorrect singular variable name").isEqualTo("testObject"); + } + + @Test + void array() { + Object actual = Conventions.getVariableName(new TestObject[0]); + assertThat(actual).as("Incorrect plural array form").isEqualTo("testObjectList"); + } + + @Test + void list() { + assertThat(Conventions.getVariableName(Collections.singletonList(new TestObject()))).as("Incorrect plural List form").isEqualTo("testObjectList"); + assertThat(Conventions.getVariableNameForParameter(getMethodParameter(List.class))).as("Incorrect plural List form").isEqualTo("testObjectList"); + assertThat(Conventions.getVariableNameForReturnType(getMethodForReturnType(List.class))).as("Incorrect plural List form").isEqualTo("testObjectList"); + } + + @Test + void emptyList() { + assertThatIllegalArgumentException().isThrownBy(() -> + Conventions.getVariableName(new ArrayList<>())); + } + + @Test + void set() { + assertThat(Conventions.getVariableName(Collections.singleton(new TestObject()))).as("Incorrect plural Set form").isEqualTo("testObjectList"); + assertThat(Conventions.getVariableNameForParameter(getMethodParameter(Set.class))).as("Incorrect plural Set form").isEqualTo("testObjectList"); + assertThat(Conventions.getVariableNameForReturnType(getMethodForReturnType(Set.class))).as("Incorrect plural Set form").isEqualTo("testObjectList"); + } + + @Test + void reactiveParameters() { + assertThat(Conventions.getVariableNameForParameter(getMethodParameter(Mono.class))).isEqualTo("testObjectMono"); + assertThat(Conventions.getVariableNameForParameter(getMethodParameter(Flux.class))).isEqualTo("testObjectFlux"); + assertThat(Conventions.getVariableNameForParameter(getMethodParameter(Single.class))).isEqualTo("testObjectSingle"); + assertThat(Conventions.getVariableNameForParameter(getMethodParameter(Observable.class))).isEqualTo("testObjectObservable"); + } + + @Test + void reactiveReturnTypes() { + assertThat(Conventions.getVariableNameForReturnType(getMethodForReturnType(Mono.class))).isEqualTo("testObjectMono"); + assertThat(Conventions.getVariableNameForReturnType(getMethodForReturnType(Flux.class))).isEqualTo("testObjectFlux"); + assertThat(Conventions.getVariableNameForReturnType(getMethodForReturnType(Single.class))).isEqualTo("testObjectSingle"); + assertThat(Conventions.getVariableNameForReturnType(getMethodForReturnType(Observable.class))).isEqualTo("testObjectObservable"); + } + + @Test + void attributeNameToPropertyName() { + assertThat(Conventions.attributeNameToPropertyName("transaction-manager")).isEqualTo("transactionManager"); + assertThat(Conventions.attributeNameToPropertyName("pointcut-ref")).isEqualTo("pointcutRef"); + assertThat(Conventions.attributeNameToPropertyName("lookup-on-startup")).isEqualTo("lookupOnStartup"); + } + + @Test + void getQualifiedAttributeName() { + String baseName = "foo"; + Class cls = String.class; + String desiredResult = "java.lang.String.foo"; + assertThat(Conventions.getQualifiedAttributeName(cls, baseName)).isEqualTo(desiredResult); + } + + + private static MethodParameter getMethodParameter(Class parameterType) { + Method method = ClassUtils.getMethod(TestBean.class, "handle", (Class[]) null); + for (int i=0; i < method.getParameterCount(); i++) { + if (parameterType.equals(method.getParameterTypes()[i])) { + return new MethodParameter(method, i); + } + } + throw new IllegalArgumentException("Parameter type not found: " + parameterType); + } + + private static Method getMethodForReturnType(Class returnType) { + return Arrays.stream(TestBean.class.getMethods()) + .filter(method -> method.getReturnType().equals(returnType)) + .findFirst() + .orElseThrow(() -> + new IllegalArgumentException("Unique return type not found: " + returnType)); + } + + + @SuppressWarnings("unused") + private static class TestBean { + + public void handle(TestObject to, + List toList, Set toSet, + Mono toMono, Flux toFlux, + Single toSingle, Observable toObservable) { } + + public TestObject handleTo() { return null; } + + public List handleToList() { return null; } + + public Set handleToSet() { return null; } + + public Mono handleToMono() { return null; } + + public Flux handleToFlux() { return null; } + + public Single handleToSingle() { return null; } + + public Observable handleToObservable() { return null; } + + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/ExceptionDepthComparatorTests.java b/spring-core/src/test/java/org/springframework/core/ExceptionDepthComparatorTests.java new file mode 100644 index 0000000..f30f04b --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/ExceptionDepthComparatorTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @author Chris Shepperd + */ +@SuppressWarnings("unchecked") +class ExceptionDepthComparatorTests { + + @Test + void targetBeforeSameDepth() throws Exception { + Class foundClass = findClosestMatch(TargetException.class, SameDepthException.class); + assertThat(foundClass).isEqualTo(TargetException.class); + } + + @Test + void sameDepthBeforeTarget() throws Exception { + Class foundClass = findClosestMatch(SameDepthException.class, TargetException.class); + assertThat(foundClass).isEqualTo(TargetException.class); + } + + @Test + void lowestDepthBeforeTarget() throws Exception { + Class foundClass = findClosestMatch(LowestDepthException.class, TargetException.class); + assertThat(foundClass).isEqualTo(TargetException.class); + } + + @Test + void targetBeforeLowestDepth() throws Exception { + Class foundClass = findClosestMatch(TargetException.class, LowestDepthException.class); + assertThat(foundClass).isEqualTo(TargetException.class); + } + + @Test + void noDepthBeforeTarget() throws Exception { + Class foundClass = findClosestMatch(NoDepthException.class, TargetException.class); + assertThat(foundClass).isEqualTo(TargetException.class); + } + + @Test + void noDepthBeforeHighestDepth() throws Exception { + Class foundClass = findClosestMatch(NoDepthException.class, HighestDepthException.class); + assertThat(foundClass).isEqualTo(HighestDepthException.class); + } + + @Test + void highestDepthBeforeNoDepth() throws Exception { + Class foundClass = findClosestMatch(HighestDepthException.class, NoDepthException.class); + assertThat(foundClass).isEqualTo(HighestDepthException.class); + } + + @Test + void highestDepthBeforeLowestDepth() throws Exception { + Class foundClass = findClosestMatch(HighestDepthException.class, LowestDepthException.class); + assertThat(foundClass).isEqualTo(LowestDepthException.class); + } + + @Test + void lowestDepthBeforeHighestDepth() throws Exception { + Class foundClass = findClosestMatch(LowestDepthException.class, HighestDepthException.class); + assertThat(foundClass).isEqualTo(LowestDepthException.class); + } + + private Class findClosestMatch( + Class... classes) { + return ExceptionDepthComparator.findClosestMatch(Arrays.asList(classes), new TargetException()); + } + + @SuppressWarnings("serial") + public class HighestDepthException extends Throwable { + } + + @SuppressWarnings("serial") + public class LowestDepthException extends HighestDepthException { + } + + @SuppressWarnings("serial") + public class TargetException extends LowestDepthException { + } + + @SuppressWarnings("serial") + public class SameDepthException extends LowestDepthException { + } + + @SuppressWarnings("serial") + public class NoDepthException extends TargetException { + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java b/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java new file mode 100644 index 0000000..21e1d73 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java @@ -0,0 +1,325 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.core.GenericTypeResolver.getTypeVariableMap; +import static org.springframework.core.GenericTypeResolver.resolveReturnTypeArgument; +import static org.springframework.core.GenericTypeResolver.resolveType; +import static org.springframework.core.GenericTypeResolver.resolveTypeArgument; +import static org.springframework.util.ReflectionUtils.findMethod; + +/** + * @author Juergen Hoeller + * @author Sam Brannen + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +class GenericTypeResolverTests { + + @Test + void simpleInterfaceType() { + assertThat(resolveTypeArgument(MySimpleInterfaceType.class, MyInterfaceType.class)).isEqualTo(String.class); + } + + @Test + void simpleCollectionInterfaceType() { + assertThat(resolveTypeArgument(MyCollectionInterfaceType.class, MyInterfaceType.class)).isEqualTo(Collection.class); + } + + @Test + void simpleSuperclassType() { + assertThat(resolveTypeArgument(MySimpleSuperclassType.class, MySuperclassType.class)).isEqualTo(String.class); + } + + @Test + void simpleCollectionSuperclassType() { + assertThat(resolveTypeArgument(MyCollectionSuperclassType.class, MySuperclassType.class)).isEqualTo(Collection.class); + } + + @Test + void nullIfNotResolvable() { + GenericClass obj = new GenericClass<>(); + assertThat((Object) resolveTypeArgument(obj.getClass(), GenericClass.class)).isNull(); + } + + @Test + void methodReturnTypes() { + assertThat(resolveReturnTypeArgument(findMethod(MyTypeWithMethods.class, "integer"), MyInterfaceType.class)).isEqualTo(Integer.class); + assertThat(resolveReturnTypeArgument(findMethod(MyTypeWithMethods.class, "string"), MyInterfaceType.class)).isEqualTo(String.class); + assertThat(resolveReturnTypeArgument(findMethod(MyTypeWithMethods.class, "raw"), MyInterfaceType.class)).isEqualTo(null); + assertThat(resolveReturnTypeArgument(findMethod(MyTypeWithMethods.class, "object"), MyInterfaceType.class)).isEqualTo(null); + } + + @Test + void testResolveType() { + Method intMessageMethod = findMethod(MyTypeWithMethods.class, "readIntegerInputMessage", MyInterfaceType.class); + MethodParameter intMessageMethodParam = new MethodParameter(intMessageMethod, 0); + assertThat(resolveType(intMessageMethodParam.getGenericParameterType(), new HashMap<>())).isEqualTo(MyInterfaceType.class); + + Method intArrMessageMethod = findMethod(MyTypeWithMethods.class, "readIntegerArrayInputMessage", + MyInterfaceType[].class); + MethodParameter intArrMessageMethodParam = new MethodParameter(intArrMessageMethod, 0); + assertThat(resolveType(intArrMessageMethodParam.getGenericParameterType(), new HashMap<>())).isEqualTo(MyInterfaceType[].class); + + Method genericArrMessageMethod = findMethod(MySimpleTypeWithMethods.class, "readGenericArrayInputMessage", + Object[].class); + MethodParameter genericArrMessageMethodParam = new MethodParameter(genericArrMessageMethod, 0); + Map varMap = getTypeVariableMap(MySimpleTypeWithMethods.class); + assertThat(resolveType(genericArrMessageMethodParam.getGenericParameterType(), varMap)).isEqualTo(Integer[].class); + } + + @Test + void boundParameterizedType() { + assertThat(resolveTypeArgument(TestImpl.class, TestIfc.class)).isEqualTo(B.class); + } + + @Test + void testGetTypeVariableMap() throws Exception { + Map map; + + map = GenericTypeResolver.getTypeVariableMap(MySimpleInterfaceType.class); + assertThat(map.toString()).isEqualTo("{T=class java.lang.String}"); + + map = GenericTypeResolver.getTypeVariableMap(MyCollectionInterfaceType.class); + assertThat(map.toString()).isEqualTo("{T=java.util.Collection}"); + + map = GenericTypeResolver.getTypeVariableMap(MyCollectionSuperclassType.class); + assertThat(map.toString()).isEqualTo("{T=java.util.Collection}"); + + map = GenericTypeResolver.getTypeVariableMap(MySimpleTypeWithMethods.class); + assertThat(map.toString()).isEqualTo("{T=class java.lang.Integer}"); + + map = GenericTypeResolver.getTypeVariableMap(TopLevelClass.class); + assertThat(map.toString()).isEqualTo("{}"); + + map = GenericTypeResolver.getTypeVariableMap(TypedTopLevelClass.class); + assertThat(map.toString()).isEqualTo("{T=class java.lang.Integer}"); + + map = GenericTypeResolver.getTypeVariableMap(TypedTopLevelClass.TypedNested.class); + assertThat(map.size()).isEqualTo(2); + Type t = null; + Type x = null; + for (Map.Entry entry : map.entrySet()) { + if (entry.getKey().toString().equals("T")) { + t = entry.getValue(); + } + else { + x = entry.getValue(); + } + } + assertThat(t).isEqualTo(Integer.class); + assertThat(x).isEqualTo(Long.class); + } + + @Test // SPR-11030 + void getGenericsCannotBeResolved() throws Exception { + Class[] resolved = GenericTypeResolver.resolveTypeArguments(List.class, Iterable.class); + assertThat((Object) resolved).isNull(); + } + + @Test // SPR-11052 + void getRawMapTypeCannotBeResolved() throws Exception { + Class[] resolved = GenericTypeResolver.resolveTypeArguments(Map.class, Map.class); + assertThat((Object) resolved).isNull(); + } + + @Test // SPR-11044 + @SuppressWarnings("deprecation") + void getGenericsOnArrayFromParamCannotBeResolved() throws Exception { + MethodParameter methodParameter = MethodParameter.forExecutable( + WithArrayBase.class.getDeclaredMethod("array", Object[].class), 0); + Class resolved = GenericTypeResolver.resolveParameterType(methodParameter, WithArray.class); + assertThat(resolved).isEqualTo(Object[].class); + } + + @Test // SPR-11044 + void getGenericsOnArrayFromReturnCannotBeResolved() throws Exception { + Class resolved = GenericTypeResolver.resolveReturnType( + WithArrayBase.class.getDeclaredMethod("array", Object[].class), WithArray.class); + assertThat(resolved).isEqualTo(Object[].class); + } + + @Test // SPR-11763 + void resolveIncompleteTypeVariables() { + Class[] resolved = GenericTypeResolver.resolveTypeArguments(IdFixingRepository.class, Repository.class); + assertThat(resolved).isNotNull(); + assertThat(resolved.length).isEqualTo(2); + assertThat(resolved[0]).isEqualTo(Object.class); + assertThat(resolved[1]).isEqualTo(Long.class); + } + + + public interface MyInterfaceType { + } + + public class MySimpleInterfaceType implements MyInterfaceType { + } + + public class MyCollectionInterfaceType implements MyInterfaceType> { + } + + public abstract class MySuperclassType { + } + + public class MySimpleSuperclassType extends MySuperclassType { + } + + public class MyCollectionSuperclassType extends MySuperclassType> { + } + + public static class MyTypeWithMethods { + + public MyInterfaceType integer() { + return null; + } + + public MySimpleInterfaceType string() { + return null; + } + + public Object object() { + return null; + } + + public MyInterfaceType raw() { + return null; + } + + public String notParameterized() { + return null; + } + + public String notParameterizedWithArguments(Integer x, Boolean b) { + return null; + } + + /** + * Simulates a factory method that wraps the supplied object in a proxy of the + * same type. + */ + public static T createProxy(T object) { + return null; + } + + /** + * Similar to {@link #createProxy(Object)} but adds an additional argument before + * the argument of type {@code T}. Note that they may potentially be of the same + * time when invoked! + */ + public static T createNamedProxy(String name, T object) { + return null; + } + + /** + * Simulates factory methods found in libraries such as Mockito and EasyMock. + */ + public static MOCK createMock(Class toMock) { + return null; + } + + /** + * Similar to {@link #createMock(Class)} but adds an additional method argument + * before the parameterized argument. + */ + public static T createNamedMock(String name, Class toMock) { + return null; + } + + /** + * Similar to {@link #createNamedMock(String, Class)} but adds an additional + * parameterized type. + */ + public static T createVMock(V name, Class toMock) { + return null; + } + + /** + * Extract some value of the type supported by the interface (i.e., by a concrete, + * non-generic implementation of the interface). + */ + public static T extractValueFrom(MyInterfaceType myInterfaceType) { + return null; + } + + /** + * Extract some magic value from the supplied map. + */ + public static V extractMagicValue(Map map) { + return null; + } + + public void readIntegerInputMessage(MyInterfaceType message) { + } + + public void readIntegerArrayInputMessage(MyInterfaceType[] message) { + } + + public void readGenericArrayInputMessage(T[] message) { + } + } + + public static class MySimpleTypeWithMethods extends MyTypeWithMethods { + } + + static class GenericClass { + } + + class A{} + + class B{} + + class TestIfc{} + + class TestImpl> extends TestIfc{ + } + + static class TopLevelClass { + class Nested { + } + } + + static class TypedTopLevelClass extends TopLevelClass { + class TypedNested extends Nested { + } + } + + static abstract class WithArrayBase { + + public abstract T[] array(T... args); + } + + static abstract class WithArray extends WithArrayBase { + } + + interface Repository { + } + + interface IdFixingRepository extends Repository { + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/LocalVariableTableParameterNameDiscovererTests.java b/spring-core/src/test/java/org/springframework/core/LocalVariableTableParameterNameDiscovererTests.java new file mode 100644 index 0000000..4ab6fc8 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/LocalVariableTableParameterNameDiscovererTests.java @@ -0,0 +1,325 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.awt.Component; +import java.io.PrintStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.Date; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.tests.sample.objects.TestObject; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Adrian Colyer + */ +class LocalVariableTableParameterNameDiscovererTests { + + private final LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer(); + + + @Test + void methodParameterNameDiscoveryNoArgs() throws NoSuchMethodException { + Method getName = TestObject.class.getMethod("getName"); + String[] names = discoverer.getParameterNames(getName); + assertThat(names).as("should find method info").isNotNull(); + assertThat(names.length).as("no argument names").isEqualTo(0); + } + + @Test + void methodParameterNameDiscoveryWithArgs() throws NoSuchMethodException { + Method setName = TestObject.class.getMethod("setName", String.class); + String[] names = discoverer.getParameterNames(setName); + assertThat(names).as("should find method info").isNotNull(); + assertThat(names.length).as("one argument").isEqualTo(1); + assertThat(names[0]).isEqualTo("name"); + } + + @Test + void consParameterNameDiscoveryNoArgs() throws NoSuchMethodException { + Constructor noArgsCons = TestObject.class.getConstructor(); + String[] names = discoverer.getParameterNames(noArgsCons); + assertThat(names).as("should find cons info").isNotNull(); + assertThat(names.length).as("no argument names").isEqualTo(0); + } + + @Test + void consParameterNameDiscoveryArgs() throws NoSuchMethodException { + Constructor twoArgCons = TestObject.class.getConstructor(String.class, int.class); + String[] names = discoverer.getParameterNames(twoArgCons); + assertThat(names).as("should find cons info").isNotNull(); + assertThat(names.length).as("one argument").isEqualTo(2); + assertThat(names[0]).isEqualTo("name"); + assertThat(names[1]).isEqualTo("age"); + } + + @Test + void staticMethodParameterNameDiscoveryNoArgs() throws NoSuchMethodException { + Method m = getClass().getMethod("staticMethodNoLocalVars"); + String[] names = discoverer.getParameterNames(m); + assertThat(names).as("should find method info").isNotNull(); + assertThat(names.length).as("no argument names").isEqualTo(0); + } + + @Test + void overloadedStaticMethod() throws Exception { + Class clazz = this.getClass(); + + Method m1 = clazz.getMethod("staticMethod", Long.TYPE, Long.TYPE); + String[] names = discoverer.getParameterNames(m1); + assertThat(names).as("should find method info").isNotNull(); + assertThat(names.length).as("two arguments").isEqualTo(2); + assertThat(names[0]).isEqualTo("x"); + assertThat(names[1]).isEqualTo("y"); + + Method m2 = clazz.getMethod("staticMethod", Long.TYPE, Long.TYPE, Long.TYPE); + names = discoverer.getParameterNames(m2); + assertThat(names).as("should find method info").isNotNull(); + assertThat(names.length).as("three arguments").isEqualTo(3); + assertThat(names[0]).isEqualTo("x"); + assertThat(names[1]).isEqualTo("y"); + assertThat(names[2]).isEqualTo("z"); + } + + @Test + void overloadedStaticMethodInInnerClass() throws Exception { + Class clazz = InnerClass.class; + + Method m1 = clazz.getMethod("staticMethod", Long.TYPE); + String[] names = discoverer.getParameterNames(m1); + assertThat(names).as("should find method info").isNotNull(); + assertThat(names.length).as("one argument").isEqualTo(1); + assertThat(names[0]).isEqualTo("x"); + + Method m2 = clazz.getMethod("staticMethod", Long.TYPE, Long.TYPE); + names = discoverer.getParameterNames(m2); + assertThat(names).as("should find method info").isNotNull(); + assertThat(names.length).as("two arguments").isEqualTo(2); + assertThat(names[0]).isEqualTo("x"); + assertThat(names[1]).isEqualTo("y"); + } + + @Test + void overloadedMethod() throws Exception { + Class clazz = this.getClass(); + + Method m1 = clazz.getMethod("instanceMethod", Double.TYPE, Double.TYPE); + String[] names = discoverer.getParameterNames(m1); + assertThat(names).as("should find method info").isNotNull(); + assertThat(names.length).as("two arguments").isEqualTo(2); + assertThat(names[0]).isEqualTo("x"); + assertThat(names[1]).isEqualTo("y"); + + Method m2 = clazz.getMethod("instanceMethod", Double.TYPE, Double.TYPE, Double.TYPE); + names = discoverer.getParameterNames(m2); + assertThat(names).as("should find method info").isNotNull(); + assertThat(names.length).as("three arguments").isEqualTo(3); + assertThat(names[0]).isEqualTo("x"); + assertThat(names[1]).isEqualTo("y"); + assertThat(names[2]).isEqualTo("z"); + } + + @Test + void overloadedMethodInInnerClass() throws Exception { + Class clazz = InnerClass.class; + + Method m1 = clazz.getMethod("instanceMethod", String.class); + String[] names = discoverer.getParameterNames(m1); + assertThat(names).as("should find method info").isNotNull(); + assertThat(names.length).as("one argument").isEqualTo(1); + assertThat(names[0]).isEqualTo("aa"); + + Method m2 = clazz.getMethod("instanceMethod", String.class, String.class); + names = discoverer.getParameterNames(m2); + assertThat(names).as("should find method info").isNotNull(); + assertThat(names.length).as("two arguments").isEqualTo(2); + assertThat(names[0]).isEqualTo("aa"); + assertThat(names[1]).isEqualTo("bb"); + } + + @Test + void generifiedClass() throws Exception { + Class clazz = GenerifiedClass.class; + + Constructor ctor = clazz.getDeclaredConstructor(Object.class); + String[] names = discoverer.getParameterNames(ctor); + assertThat(names.length).isEqualTo(1); + assertThat(names[0]).isEqualTo("key"); + + ctor = clazz.getDeclaredConstructor(Object.class, Object.class); + names = discoverer.getParameterNames(ctor); + assertThat(names.length).isEqualTo(2); + assertThat(names[0]).isEqualTo("key"); + assertThat(names[1]).isEqualTo("value"); + + Method m = clazz.getMethod("generifiedStaticMethod", Object.class); + names = discoverer.getParameterNames(m); + assertThat(names.length).isEqualTo(1); + assertThat(names[0]).isEqualTo("param"); + + m = clazz.getMethod("generifiedMethod", Object.class, long.class, Object.class, Object.class); + names = discoverer.getParameterNames(m); + assertThat(names.length).isEqualTo(4); + assertThat(names[0]).isEqualTo("param"); + assertThat(names[1]).isEqualTo("x"); + assertThat(names[2]).isEqualTo("key"); + assertThat(names[3]).isEqualTo("value"); + + m = clazz.getMethod("voidStaticMethod", Object.class, long.class, int.class); + names = discoverer.getParameterNames(m); + assertThat(names.length).isEqualTo(3); + assertThat(names[0]).isEqualTo("obj"); + assertThat(names[1]).isEqualTo("x"); + assertThat(names[2]).isEqualTo("i"); + + m = clazz.getMethod("nonVoidStaticMethod", Object.class, long.class, int.class); + names = discoverer.getParameterNames(m); + assertThat(names.length).isEqualTo(3); + assertThat(names[0]).isEqualTo("obj"); + assertThat(names[1]).isEqualTo("x"); + assertThat(names[2]).isEqualTo("i"); + + m = clazz.getMethod("getDate"); + names = discoverer.getParameterNames(m); + assertThat(names.length).isEqualTo(0); + } + + @Disabled("Ignored because Ubuntu packages OpenJDK with debug symbols enabled. See SPR-8078.") + @Test + void classesWithoutDebugSymbols() throws Exception { + // JDK classes don't have debug information (usually) + Class clazz = Component.class; + String methodName = "list"; + + Method m = clazz.getMethod(methodName); + String[] names = discoverer.getParameterNames(m); + assertThat(names).isNull(); + + m = clazz.getMethod(methodName, PrintStream.class); + names = discoverer.getParameterNames(m); + assertThat(names).isNull(); + + m = clazz.getMethod(methodName, PrintStream.class, int.class); + names = discoverer.getParameterNames(m); + assertThat(names).isNull(); + } + + + public static void staticMethodNoLocalVars() { + } + + public static long staticMethod(long x, long y) { + long u = x * y; + return u; + } + + public static long staticMethod(long x, long y, long z) { + long u = x * y * z; + return u; + } + + public double instanceMethod(double x, double y) { + double u = x * y; + return u; + } + + public double instanceMethod(double x, double y, double z) { + double u = x * y * z; + return u; + } + + + public static class InnerClass { + + public int waz = 0; + + public InnerClass() { + } + + public InnerClass(String firstArg, long secondArg, Object thirdArg) { + long foo = 0; + short bar = 10; + this.waz = (int) (foo + bar); + } + + public String instanceMethod(String aa) { + return aa; + } + + public String instanceMethod(String aa, String bb) { + return aa + bb; + } + + public static long staticMethod(long x) { + long u = x; + return u; + } + + public static long staticMethod(long x, long y) { + long u = x * y; + return u; + } + } + + + public static class GenerifiedClass { + + private static long date; + + static { + // some custom static bloc or + date = new Date().getTime(); + } + + public GenerifiedClass() { + this(null, null); + } + + public GenerifiedClass(K key) { + this(key, null); + } + + public GenerifiedClass(K key, V value) { + } + + public static

    long generifiedStaticMethod(P param) { + return date; + } + + public

    void generifiedMethod(P param, long x, K key, V value) { + // nothing + } + + public static void voidStaticMethod(Object obj, long x, int i) { + // nothing + } + + public static long nonVoidStaticMethod(Object obj, long x, int i) { + return date; + } + + public static long getDate() { + return date; + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/MethodParameterTests.java b/spring-core/src/test/java/org/springframework/core/MethodParameterTests.java new file mode 100644 index 0000000..6f5220f --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/MethodParameterTests.java @@ -0,0 +1,271 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.concurrent.Callable; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link MethodParameter}. + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @author Sam Brannen + * @author Phillip Webb + */ +class MethodParameterTests { + + private Method method; + + private MethodParameter stringParameter; + + private MethodParameter longParameter; + + private MethodParameter intReturnType; + + + @BeforeEach + void setup() throws NoSuchMethodException { + method = getClass().getMethod("method", String.class, Long.TYPE); + stringParameter = new MethodParameter(method, 0); + longParameter = new MethodParameter(method, 1); + intReturnType = new MethodParameter(method, -1); + } + + + @Test + void equals() throws NoSuchMethodException { + assertThat(stringParameter).isEqualTo(stringParameter); + assertThat(longParameter).isEqualTo(longParameter); + assertThat(intReturnType).isEqualTo(intReturnType); + + assertThat(stringParameter.equals(longParameter)).isFalse(); + assertThat(stringParameter.equals(intReturnType)).isFalse(); + assertThat(longParameter.equals(stringParameter)).isFalse(); + assertThat(longParameter.equals(intReturnType)).isFalse(); + assertThat(intReturnType.equals(stringParameter)).isFalse(); + assertThat(intReturnType.equals(longParameter)).isFalse(); + + Method method = getClass().getMethod("method", String.class, Long.TYPE); + MethodParameter methodParameter = new MethodParameter(method, 0); + assertThat(methodParameter).isEqualTo(stringParameter); + assertThat(stringParameter).isEqualTo(methodParameter); + assertThat(methodParameter).isNotEqualTo(longParameter); + assertThat(longParameter).isNotEqualTo(methodParameter); + } + + @Test + void testHashCode() throws NoSuchMethodException { + assertThat(stringParameter.hashCode()).isEqualTo(stringParameter.hashCode()); + assertThat(longParameter.hashCode()).isEqualTo(longParameter.hashCode()); + assertThat(intReturnType.hashCode()).isEqualTo(intReturnType.hashCode()); + + Method method = getClass().getMethod("method", String.class, Long.TYPE); + MethodParameter methodParameter = new MethodParameter(method, 0); + assertThat(methodParameter.hashCode()).isEqualTo(stringParameter.hashCode()); + assertThat(methodParameter.hashCode()).isNotEqualTo(longParameter.hashCode()); + } + + @Test + @SuppressWarnings("deprecation") + void testFactoryMethods() { + assertThat(MethodParameter.forMethodOrConstructor(method, 0)).isEqualTo(stringParameter); + assertThat(MethodParameter.forMethodOrConstructor(method, 1)).isEqualTo(longParameter); + + assertThat(MethodParameter.forExecutable(method, 0)).isEqualTo(stringParameter); + assertThat(MethodParameter.forExecutable(method, 1)).isEqualTo(longParameter); + + assertThat(MethodParameter.forParameter(method.getParameters()[0])).isEqualTo(stringParameter); + assertThat(MethodParameter.forParameter(method.getParameters()[1])).isEqualTo(longParameter); + } + + @Test + void indexValidation() { + assertThatIllegalArgumentException().isThrownBy(() -> + new MethodParameter(method, 2)); + } + + @Test + void annotatedConstructorParameterInStaticNestedClass() throws Exception { + Constructor constructor = NestedClass.class.getDeclaredConstructor(String.class); + MethodParameter methodParameter = MethodParameter.forExecutable(constructor, 0); + assertThat(methodParameter.getParameterType()).isEqualTo(String.class); + assertThat(methodParameter.getParameterAnnotation(Param.class)).as("Failed to find @Param annotation").isNotNull(); + } + + @Test // SPR-16652 + void annotatedConstructorParameterInInnerClass() throws Exception { + Constructor constructor = InnerClass.class.getConstructor(getClass(), String.class, Callable.class); + + MethodParameter methodParameter = MethodParameter.forExecutable(constructor, 0); + assertThat(methodParameter.getParameterType()).isEqualTo(getClass()); + assertThat(methodParameter.getParameterAnnotation(Param.class)).isNull(); + + methodParameter = MethodParameter.forExecutable(constructor, 1); + assertThat(methodParameter.getParameterType()).isEqualTo(String.class); + assertThat(methodParameter.getParameterAnnotation(Param.class)).as("Failed to find @Param annotation").isNotNull(); + + methodParameter = MethodParameter.forExecutable(constructor, 2); + assertThat(methodParameter.getParameterType()).isEqualTo(Callable.class); + assertThat(methodParameter.getParameterAnnotation(Param.class)).isNull(); + } + + @Test // SPR-16734 + void genericConstructorParameterInInnerClass() throws Exception { + Constructor constructor = InnerClass.class.getConstructor(getClass(), String.class, Callable.class); + + MethodParameter methodParameter = MethodParameter.forExecutable(constructor, 0); + assertThat(methodParameter.getParameterType()).isEqualTo(getClass()); + assertThat(methodParameter.getGenericParameterType()).isEqualTo(getClass()); + + methodParameter = MethodParameter.forExecutable(constructor, 1); + assertThat(methodParameter.getParameterType()).isEqualTo(String.class); + assertThat(methodParameter.getGenericParameterType()).isEqualTo(String.class); + + methodParameter = MethodParameter.forExecutable(constructor, 2); + assertThat(methodParameter.getParameterType()).isEqualTo(Callable.class); + assertThat(methodParameter.getGenericParameterType()).isEqualTo(ResolvableType.forClassWithGenerics(Callable.class, Integer.class).getType()); + } + + @Test + @Deprecated + void multipleResolveParameterTypeCalls() throws Exception { + Method method = ArrayList.class.getMethod("get", int.class); + MethodParameter methodParameter = MethodParameter.forExecutable(method, -1); + assertThat(methodParameter.getParameterType()).isEqualTo(Object.class); + GenericTypeResolver.resolveParameterType(methodParameter, StringList.class); + assertThat(methodParameter.getParameterType()).isEqualTo(String.class); + GenericTypeResolver.resolveParameterType(methodParameter, IntegerList.class); + assertThat(methodParameter.getParameterType()).isEqualTo(Integer.class); + } + + @Test + void equalsAndHashCodeConsidersContainingClass() throws Exception { + Method method = ArrayList.class.getMethod("get", int.class); + MethodParameter m1 = MethodParameter.forExecutable(method, -1); + MethodParameter m2 = MethodParameter.forExecutable(method, -1); + MethodParameter m3 = MethodParameter.forExecutable(method, -1).nested(); + assertThat(m1).isEqualTo(m2).isNotEqualTo(m3); + assertThat(m1.hashCode()).isEqualTo(m2.hashCode()); + } + + @Test + void equalsAndHashCodeConsidersNesting() throws Exception { + Method method = ArrayList.class.getMethod("get", int.class); + MethodParameter m1 = MethodParameter.forExecutable(method, -1) + .withContainingClass(StringList.class); + MethodParameter m2 = MethodParameter.forExecutable(method, -1) + .withContainingClass(StringList.class); + MethodParameter m3 = MethodParameter.forExecutable(method, -1) + .withContainingClass(IntegerList.class); + MethodParameter m4 = MethodParameter.forExecutable(method, -1); + assertThat(m1).isEqualTo(m2).isNotEqualTo(m3).isNotEqualTo(m4); + assertThat(m1.hashCode()).isEqualTo(m2.hashCode()); + } + + @Test + void withContainingClassReturnsNewInstance() throws Exception { + Method method = ArrayList.class.getMethod("get", int.class); + MethodParameter m1 = MethodParameter.forExecutable(method, -1); + MethodParameter m2 = m1.withContainingClass(StringList.class); + MethodParameter m3 = m1.withContainingClass(IntegerList.class); + assertThat(m1).isNotSameAs(m2).isNotSameAs(m3); + assertThat(m1.getParameterType()).isEqualTo(Object.class); + assertThat(m2.getParameterType()).isEqualTo(String.class); + assertThat(m3.getParameterType()).isEqualTo(Integer.class); + } + + @Test + void withTypeIndexReturnsNewInstance() throws Exception { + Method method = ArrayList.class.getMethod("get", int.class); + MethodParameter m1 = MethodParameter.forExecutable(method, -1); + MethodParameter m2 = m1.withTypeIndex(2); + MethodParameter m3 = m1.withTypeIndex(3); + assertThat(m1).isNotSameAs(m2).isNotSameAs(m3); + assertThat(m1.getTypeIndexForCurrentLevel()).isNull(); + assertThat(m2.getTypeIndexForCurrentLevel()).isEqualTo(2); + assertThat(m3.getTypeIndexForCurrentLevel()).isEqualTo(3); + } + + @Test + @SuppressWarnings("deprecation") + void mutatingNestingLevelShouldNotChangeNewInstance() throws Exception { + Method method = ArrayList.class.getMethod("get", int.class); + MethodParameter m1 = MethodParameter.forExecutable(method, -1); + MethodParameter m2 = m1.withTypeIndex(2); + assertThat(m2.getTypeIndexForCurrentLevel()).isEqualTo(2); + m1.setTypeIndexForCurrentLevel(1); + m2.decreaseNestingLevel(); + assertThat(m2.getTypeIndexForCurrentLevel()).isNull(); + } + + @Test + void nestedWithTypeIndexReturnsNewInstance() throws Exception { + Method method = ArrayList.class.getMethod("get", int.class); + MethodParameter m1 = MethodParameter.forExecutable(method, -1); + MethodParameter m2 = m1.nested(2); + MethodParameter m3 = m1.nested(3); + assertThat(m1).isNotSameAs(m2).isNotSameAs(m3); + assertThat(m1.getTypeIndexForCurrentLevel()).isNull(); + assertThat(m2.getTypeIndexForCurrentLevel()).isEqualTo(2); + assertThat(m3.getTypeIndexForCurrentLevel()).isEqualTo(3); + } + + public int method(String p1, long p2) { + return 42; + } + + @SuppressWarnings("unused") + private static class NestedClass { + + NestedClass(@Param String s) { + } + } + + @SuppressWarnings("unused") + private class InnerClass { + + public InnerClass(@Param String s, Callable i) { + } + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.PARAMETER) + private @interface Param { + } + + @SuppressWarnings("serial") + private static class StringList extends ArrayList { + } + + @SuppressWarnings("serial") + private static class IntegerList extends ArrayList { + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/NestedExceptionTests.java b/spring-core/src/test/java/org/springframework/core/NestedExceptionTests.java new file mode 100644 index 0000000..5134ced --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/NestedExceptionTests.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rod Johnson + * @author Juergen Hoeller + */ +@SuppressWarnings("serial") +class NestedExceptionTests { + + @Test + void nestedRuntimeExceptionWithNoRootCause() { + String mesg = "mesg of mine"; + // Making a class abstract doesn't _really_ prevent instantiation :-) + NestedRuntimeException nex = new NestedRuntimeException(mesg) {}; + assertThat(nex.getCause()).isNull(); + assertThat(mesg).isEqualTo(nex.getMessage()); + + // Check printStackTrace + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintWriter pw = new PrintWriter(baos); + nex.printStackTrace(pw); + pw.flush(); + String stackTrace = new String(baos.toByteArray()); + assertThat(stackTrace.contains(mesg)).isTrue(); + } + + @Test + void nestedRuntimeExceptionWithRootCause() { + String myMessage = "mesg for this exception"; + String rootCauseMsg = "this is the obscure message of the root cause"; + Exception rootCause = new Exception(rootCauseMsg); + // Making a class abstract doesn't _really_ prevent instantiation :-) + NestedRuntimeException nex = new NestedRuntimeException(myMessage, rootCause) {}; + assertThat(rootCause).isEqualTo(nex.getCause()); + assertThat(nex.getMessage().contains(myMessage)).isTrue(); + assertThat(nex.getMessage().endsWith(rootCauseMsg)).isTrue(); + + // check PrintStackTrace + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintWriter pw = new PrintWriter(baos); + nex.printStackTrace(pw); + pw.flush(); + String stackTrace = new String(baos.toByteArray()); + assertThat(stackTrace.contains(rootCause.getClass().getName())).isTrue(); + assertThat(stackTrace.contains(rootCauseMsg)).isTrue(); + } + + @Test + void nestedCheckedExceptionWithNoRootCause() { + String mesg = "mesg of mine"; + // Making a class abstract doesn't _really_ prevent instantiation :-) + NestedCheckedException nex = new NestedCheckedException(mesg) {}; + assertThat(nex.getCause()).isNull(); + assertThat(mesg).isEqualTo(nex.getMessage()); + + // Check printStackTrace + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintWriter pw = new PrintWriter(baos); + nex.printStackTrace(pw); + pw.flush(); + String stackTrace = new String(baos.toByteArray()); + assertThat(stackTrace.contains(mesg)).isTrue(); + } + + @Test + void nestedCheckedExceptionWithRootCause() { + String myMessage = "mesg for this exception"; + String rootCauseMsg = "this is the obscure message of the root cause"; + Exception rootCause = new Exception(rootCauseMsg); + // Making a class abstract doesn't _really_ prevent instantiation :-) + NestedCheckedException nex = new NestedCheckedException(myMessage, rootCause) {}; + assertThat(rootCause).isEqualTo(nex.getCause()); + assertThat(nex.getMessage().contains(myMessage)).isTrue(); + assertThat(nex.getMessage().endsWith(rootCauseMsg)).isTrue(); + + // check PrintStackTrace + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintWriter pw = new PrintWriter(baos); + nex.printStackTrace(pw); + pw.flush(); + String stackTrace = new String(baos.toByteArray()); + assertThat(stackTrace.contains(rootCause.getClass().getName())).isTrue(); + assertThat(stackTrace.contains(rootCauseMsg)).isTrue(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/OrderComparatorTests.java b/spring-core/src/test/java/org/springframework/core/OrderComparatorTests.java new file mode 100644 index 0000000..cd67f21 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/OrderComparatorTests.java @@ -0,0 +1,185 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.util.Comparator; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for the {@link OrderComparator} class. + * + * @author Rick Evans + * @author Stephane Nicoll + * @author Juergen Hoeller + * @author Sam Brannen + */ +class OrderComparatorTests { + + private final OrderComparator comparator = new OrderComparator(); + + + @Test + void compareOrderedInstancesBefore() { + assertThat(this.comparator.compare(new StubOrdered(100), new StubOrdered(2000))).isEqualTo(-1); + } + + @Test + void compareOrderedInstancesSame() { + assertThat(this.comparator.compare(new StubOrdered(100), new StubOrdered(100))).isEqualTo(0); + } + + @Test + void compareOrderedInstancesAfter() { + assertThat(this.comparator.compare(new StubOrdered(982300), new StubOrdered(100))).isEqualTo(1); + } + + @Test + void compareOrderedInstancesNullFirst() { + assertThat(this.comparator.compare(null, new StubOrdered(100))).isEqualTo(1); + } + + @Test + void compareOrderedInstancesNullLast() { + assertThat(this.comparator.compare(new StubOrdered(100), null)).isEqualTo(-1); + } + + @Test + void compareOrderedInstancesDoubleNull() { + assertThat(this.comparator.compare(null, null)).isEqualTo(0); + } + + @Test + void compareTwoNonOrderedInstancesEndsUpAsSame() { + assertThat(this.comparator.compare(new Object(), new Object())).isEqualTo(0); + } + + @Test + void comparePriorityOrderedInstancesBefore() { + assertThat(this.comparator.compare(new StubPriorityOrdered(100), new StubPriorityOrdered(2000))).isEqualTo(-1); + } + + @Test + void comparePriorityOrderedInstancesSame() { + assertThat(this.comparator.compare(new StubPriorityOrdered(100), new StubPriorityOrdered(100))).isEqualTo(0); + } + + @Test + void comparePriorityOrderedInstancesAfter() { + assertThat(this.comparator.compare(new StubPriorityOrdered(982300), new StubPriorityOrdered(100))).isEqualTo(1); + } + + @Test + void comparePriorityOrderedInstanceToStandardOrderedInstanceWithHigherPriority() { + assertThatPriorityOrderedAlwaysWins(new StubPriorityOrdered(200), new StubOrdered(100)); + } + + @Test + void comparePriorityOrderedInstanceToStandardOrderedInstanceWithSamePriority() { + assertThatPriorityOrderedAlwaysWins(new StubPriorityOrdered(100), new StubOrdered(100)); + } + + @Test + void comparePriorityOrderedInstanceToStandardOrderedInstanceWithLowerPriority() { + assertThatPriorityOrderedAlwaysWins(new StubPriorityOrdered(100), new StubOrdered(200)); + } + + private void assertThatPriorityOrderedAlwaysWins(StubPriorityOrdered priority, StubOrdered standard) { + assertThat(this.comparator.compare(priority, standard)).isEqualTo(-1); + assertThat(this.comparator.compare(standard, priority)).isEqualTo(1); + } + + @Test + void compareWithSimpleSourceProvider() { + Comparator customComparator = this.comparator.withSourceProvider( + new TestSourceProvider(5L, new StubOrdered(25))); + assertThat(customComparator.compare(new StubOrdered(10), 5L)).isEqualTo(-1); + } + + @Test + void compareWithSourceProviderArray() { + Comparator customComparator = this.comparator.withSourceProvider( + new TestSourceProvider(5L, new Object[] {new StubOrdered(10), new StubOrdered(-25)})); + assertThat(customComparator.compare(5L, new Object())).isEqualTo(-1); + } + + @Test + void compareWithSourceProviderArrayNoMatch() { + Comparator customComparator = this.comparator.withSourceProvider( + new TestSourceProvider(5L, new Object[] {new Object(), new Object()})); + assertThat(customComparator.compare(new Object(), 5L)).isEqualTo(0); + } + + @Test + void compareWithSourceProviderEmpty() { + Comparator customComparator = this.comparator.withSourceProvider( + new TestSourceProvider(50L, new Object())); + assertThat(customComparator.compare(new Object(), 5L)).isEqualTo(0); + } + + + private static class StubOrdered implements Ordered { + + private final int order; + + StubOrdered(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return this.order; + } + } + + private static class StubPriorityOrdered implements PriorityOrdered { + + private final int order; + + StubPriorityOrdered(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return this.order; + } + } + + private static class TestSourceProvider implements OrderComparator.OrderSourceProvider { + + private final Object target; + + private final Object orderSource; + + TestSourceProvider(Object target, Object orderSource) { + this.target = target; + this.orderSource = orderSource; + } + + @Override + public Object getOrderSource(Object obj) { + if (target.equals(obj)) { + return orderSource; + } + return null; + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/ParameterizedTypeReferenceTests.java b/spring-core/src/test/java/org/springframework/core/ParameterizedTypeReferenceTests.java new file mode 100644 index 0000000..cb7e041 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/ParameterizedTypeReferenceTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test fixture for {@link ParameterizedTypeReference}. + * + * @author Arjen Poutsma + * @author Rossen Stoyanchev + */ +class ParameterizedTypeReferenceTests { + + @Test + void stringTypeReference() { + ParameterizedTypeReference typeReference = new ParameterizedTypeReference() {}; + assertThat(typeReference.getType()).isEqualTo(String.class); + } + + @Test + void mapTypeReference() throws Exception { + Type mapType = getClass().getMethod("mapMethod").getGenericReturnType(); + ParameterizedTypeReference> typeReference = new ParameterizedTypeReference>() {}; + assertThat(typeReference.getType()).isEqualTo(mapType); + } + + @Test + void listTypeReference() throws Exception { + Type listType = getClass().getMethod("listMethod").getGenericReturnType(); + ParameterizedTypeReference> typeReference = new ParameterizedTypeReference>() {}; + assertThat(typeReference.getType()).isEqualTo(listType); + } + + @Test + void reflectiveTypeReferenceWithSpecificDeclaration() throws Exception{ + Type listType = getClass().getMethod("listMethod").getGenericReturnType(); + ParameterizedTypeReference> typeReference = ParameterizedTypeReference.forType(listType); + assertThat(typeReference.getType()).isEqualTo(listType); + } + + @Test + void reflectiveTypeReferenceWithGenericDeclaration() throws Exception{ + Type listType = getClass().getMethod("listMethod").getGenericReturnType(); + ParameterizedTypeReference typeReference = ParameterizedTypeReference.forType(listType); + assertThat(typeReference.getType()).isEqualTo(listType); + } + + + public static Map mapMethod() { + return null; + } + + public static List listMethod() { + return null; + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/PrioritizedParameterNameDiscovererTests.java b/spring-core/src/test/java/org/springframework/core/PrioritizedParameterNameDiscovererTests.java new file mode 100644 index 0000000..0a6918f --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/PrioritizedParameterNameDiscovererTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import org.springframework.tests.sample.objects.TestObject; + +import static org.assertj.core.api.Assertions.assertThat; + +class PrioritizedParameterNameDiscovererTests { + + private static final String[] FOO_BAR = new String[] { "foo", "bar" }; + + private static final String[] SOMETHING_ELSE = new String[] { "something", "else" }; + + private final ParameterNameDiscoverer returnsFooBar = new ParameterNameDiscoverer() { + @Override + public String[] getParameterNames(Method m) { + return FOO_BAR; + } + @Override + public String[] getParameterNames(Constructor ctor) { + return FOO_BAR; + } + }; + + private final ParameterNameDiscoverer returnsSomethingElse = new ParameterNameDiscoverer() { + @Override + public String[] getParameterNames(Method m) { + return SOMETHING_ELSE; + } + @Override + public String[] getParameterNames(Constructor ctor) { + return SOMETHING_ELSE; + } + }; + + private final Method anyMethod; + + public PrioritizedParameterNameDiscovererTests() throws SecurityException, NoSuchMethodException { + anyMethod = TestObject.class.getMethod("getAge"); + } + + @Test + void noParametersDiscoverers() { + ParameterNameDiscoverer pnd = new PrioritizedParameterNameDiscoverer(); + assertThat(pnd.getParameterNames(anyMethod)).isNull(); + assertThat(pnd.getParameterNames((Constructor) null)).isNull(); + } + + @Test + void orderedParameterDiscoverers1() { + PrioritizedParameterNameDiscoverer pnd = new PrioritizedParameterNameDiscoverer(); + pnd.addDiscoverer(returnsFooBar); + assertThat(Arrays.equals(FOO_BAR, pnd.getParameterNames(anyMethod))).isTrue(); + assertThat(Arrays.equals(FOO_BAR, pnd.getParameterNames((Constructor) null))).isTrue(); + pnd.addDiscoverer(returnsSomethingElse); + assertThat(Arrays.equals(FOO_BAR, pnd.getParameterNames(anyMethod))).isTrue(); + assertThat(Arrays.equals(FOO_BAR, pnd.getParameterNames((Constructor) null))).isTrue(); + } + + @Test + void orderedParameterDiscoverers2() { + PrioritizedParameterNameDiscoverer pnd = new PrioritizedParameterNameDiscoverer(); + pnd.addDiscoverer(returnsSomethingElse); + assertThat(Arrays.equals(SOMETHING_ELSE, pnd.getParameterNames(anyMethod))).isTrue(); + assertThat(Arrays.equals(SOMETHING_ELSE, pnd.getParameterNames((Constructor) null))).isTrue(); + pnd.addDiscoverer(returnsFooBar); + assertThat(Arrays.equals(SOMETHING_ELSE, pnd.getParameterNames(anyMethod))).isTrue(); + assertThat(Arrays.equals(SOMETHING_ELSE, pnd.getParameterNames((Constructor) null))).isTrue(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/ReactiveAdapterRegistryTests.java b/spring-core/src/test/java/org/springframework/core/ReactiveAdapterRegistryTests.java new file mode 100644 index 0000000..4dc08a7 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/ReactiveAdapterRegistryTests.java @@ -0,0 +1,375 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import kotlinx.coroutines.Deferred; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; +import reactor.core.CoreSubscriber; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link ReactiveAdapterRegistry}. + * @author Rossen Stoyanchev + */ +@SuppressWarnings("unchecked") +class ReactiveAdapterRegistryTests { + + private final ReactiveAdapterRegistry registry = ReactiveAdapterRegistry.getSharedInstance(); + + + @Test + void getAdapterForReactiveSubType() { + + ReactiveAdapter adapter1 = getAdapter(Flux.class); + ReactiveAdapter adapter2 = getAdapter(ExtendedFlux.class); + + assertThat(adapter2).isSameAs(adapter1); + + this.registry.registerReactiveType( + ReactiveTypeDescriptor.multiValue(ExtendedFlux.class, ExtendedFlux::empty), + o -> (ExtendedFlux) o, + ExtendedFlux::from); + + ReactiveAdapter adapter3 = getAdapter(ExtendedFlux.class); + + assertThat(adapter3).isNotNull(); + assertThat(adapter3).isNotSameAs(adapter1); + } + + @Nested + class Reactor { + + @Test + void defaultAdapterRegistrations() { + + // Reactor + assertThat(getAdapter(Mono.class)).isNotNull(); + assertThat(getAdapter(Flux.class)).isNotNull(); + + // Publisher + assertThat(getAdapter(Publisher.class)).isNotNull(); + + // Completable + assertThat(getAdapter(CompletableFuture.class)).isNotNull(); + } + + @Test + void toFlux() { + List sequence = Arrays.asList(1, 2, 3); + Publisher source = io.reactivex.rxjava3.core.Flowable.fromIterable(sequence); + Object target = getAdapter(Flux.class).fromPublisher(source); + assertThat(target instanceof Flux).isTrue(); + assertThat(((Flux) target).collectList().block(Duration.ofMillis(1000))).isEqualTo(sequence); + } + + @Test + void toMono() { + Publisher source = io.reactivex.rxjava3.core.Flowable.fromArray(1, 2, 3); + Object target = getAdapter(Mono.class).fromPublisher(source); + assertThat(target instanceof Mono).isTrue(); + assertThat(((Mono) target).block(Duration.ofMillis(1000))).isEqualTo(Integer.valueOf(1)); + } + + @Test + void toCompletableFuture() throws Exception { + Publisher source = Flux.fromArray(new Integer[] {1, 2, 3}); + Object target = getAdapter(CompletableFuture.class).fromPublisher(source); + assertThat(target instanceof CompletableFuture).isTrue(); + assertThat(((CompletableFuture) target).get()).isEqualTo(Integer.valueOf(1)); + } + + @Test + void fromCompletableFuture() { + CompletableFuture future = new CompletableFuture<>(); + future.complete(1); + Object target = getAdapter(CompletableFuture.class).toPublisher(future); + assertThat(target instanceof Mono).as("Expected Mono Publisher: " + target.getClass().getName()).isTrue(); + assertThat(((Mono) target).block(Duration.ofMillis(1000))).isEqualTo(Integer.valueOf(1)); + } + } + + @Nested + class RxJava1 { + + @Test + void defaultAdapterRegistrations() { + assertThat(getAdapter(rx.Observable.class)).isNotNull(); + assertThat(getAdapter(rx.Single.class)).isNotNull(); + assertThat(getAdapter(rx.Completable.class)).isNotNull(); + } + + @Test + void toObservable() { + List sequence = Arrays.asList(1, 2, 3); + Publisher source = Flux.fromIterable(sequence); + Object target = getAdapter(rx.Observable.class).fromPublisher(source); + assertThat(target instanceof rx.Observable).isTrue(); + assertThat(((rx.Observable) target).toList().toBlocking().first()).isEqualTo(sequence); + } + + @Test + void toSingle() { + Publisher source = Flux.fromArray(new Integer[] {1}); + Object target = getAdapter(rx.Single.class).fromPublisher(source); + assertThat(target instanceof rx.Single).isTrue(); + assertThat(((rx.Single) target).toBlocking().value()).isEqualTo(Integer.valueOf(1)); + } + + @Test + void toCompletable() { + Publisher source = Flux.fromArray(new Integer[] {1, 2, 3}); + Object target = getAdapter(rx.Completable.class).fromPublisher(source); + assertThat(target instanceof rx.Completable).isTrue(); + assertThat(((rx.Completable) target).get()).isNull(); + } + + @Test + void fromObservable() { + List sequence = Arrays.asList(1, 2, 3); + Object source = rx.Observable.from(sequence); + Object target = getAdapter(rx.Observable.class).toPublisher(source); + assertThat(target instanceof Flux).as("Expected Flux Publisher: " + target.getClass().getName()).isTrue(); + assertThat(((Flux) target).collectList().block(Duration.ofMillis(1000))).isEqualTo(sequence); + } + + @Test + void fromSingle() { + Object source = rx.Single.just(1); + Object target = getAdapter(rx.Single.class).toPublisher(source); + assertThat(target instanceof Mono).as("Expected Mono Publisher: " + target.getClass().getName()).isTrue(); + assertThat(((Mono) target).block(Duration.ofMillis(1000))).isEqualTo(Integer.valueOf(1)); + } + + @Test + void fromCompletable() { + Object source = rx.Completable.complete(); + Object target = getAdapter(rx.Completable.class).toPublisher(source); + assertThat(target instanceof Mono).as("Expected Mono Publisher: " + target.getClass().getName()).isTrue(); + ((Mono) target).block(Duration.ofMillis(1000)); + } + } + + @Nested + class RxJava2 { + + @Test + void defaultAdapterRegistrations() { + + // RxJava 2 + assertThat(getAdapter(io.reactivex.Flowable.class)).isNotNull(); + assertThat(getAdapter(io.reactivex.Observable.class)).isNotNull(); + assertThat(getAdapter(io.reactivex.Single.class)).isNotNull(); + assertThat(getAdapter(io.reactivex.Maybe.class)).isNotNull(); + assertThat(getAdapter(io.reactivex.Completable.class)).isNotNull(); + } + + @Test + void toFlowable() { + List sequence = Arrays.asList(1, 2, 3); + Publisher source = Flux.fromIterable(sequence); + Object target = getAdapter(io.reactivex.Flowable.class).fromPublisher(source); + assertThat(target instanceof io.reactivex.Flowable).isTrue(); + assertThat(((io.reactivex.Flowable) target).toList().blockingGet()).isEqualTo(sequence); + } + + @Test + void toObservable() { + List sequence = Arrays.asList(1, 2, 3); + Publisher source = Flux.fromIterable(sequence); + Object target = getAdapter(io.reactivex.Observable.class).fromPublisher(source); + assertThat(target instanceof io.reactivex.Observable).isTrue(); + assertThat(((io.reactivex.Observable) target).toList().blockingGet()).isEqualTo(sequence); + } + + @Test + void toSingle() { + Publisher source = Flux.fromArray(new Integer[] {1}); + Object target = getAdapter(io.reactivex.Single.class).fromPublisher(source); + assertThat(target instanceof io.reactivex.Single).isTrue(); + assertThat(((io.reactivex.Single) target).blockingGet()).isEqualTo(Integer.valueOf(1)); + } + + @Test + void toCompletable() { + Publisher source = Flux.fromArray(new Integer[] {1, 2, 3}); + Object target = getAdapter(io.reactivex.Completable.class).fromPublisher(source); + assertThat(target instanceof io.reactivex.Completable).isTrue(); + ((io.reactivex.Completable) target).blockingAwait(); + } + + @Test + void fromFlowable() { + List sequence = Arrays.asList(1, 2, 3); + Object source = io.reactivex.Flowable.fromIterable(sequence); + Object target = getAdapter(io.reactivex.Flowable.class).toPublisher(source); + assertThat(target instanceof Flux).as("Expected Flux Publisher: " + target.getClass().getName()).isTrue(); + assertThat(((Flux) target).collectList().block(Duration.ofMillis(1000))).isEqualTo(sequence); + } + + @Test + void fromObservable() { + List sequence = Arrays.asList(1, 2, 3); + Object source = io.reactivex.Observable.fromIterable(sequence); + Object target = getAdapter(io.reactivex.Observable.class).toPublisher(source); + assertThat(target instanceof Flux).as("Expected Flux Publisher: " + target.getClass().getName()).isTrue(); + assertThat(((Flux) target).collectList().block(Duration.ofMillis(1000))).isEqualTo(sequence); + } + + @Test + void fromSingle() { + Object source = io.reactivex.Single.just(1); + Object target = getAdapter(io.reactivex.Single.class).toPublisher(source); + assertThat(target instanceof Mono).as("Expected Mono Publisher: " + target.getClass().getName()).isTrue(); + assertThat(((Mono) target).block(Duration.ofMillis(1000))).isEqualTo(Integer.valueOf(1)); + } + + @Test + void fromCompletable() { + Object source = io.reactivex.Completable.complete(); + Object target = getAdapter(io.reactivex.Completable.class).toPublisher(source); + assertThat(target instanceof Mono).as("Expected Mono Publisher: " + target.getClass().getName()).isTrue(); + ((Mono) target).block(Duration.ofMillis(1000)); + } + } + + @Nested + class RxJava3 { + + @Test + void defaultAdapterRegistrations() { + + // RxJava 3 + assertThat(getAdapter(io.reactivex.rxjava3.core.Flowable.class)).isNotNull(); + assertThat(getAdapter(io.reactivex.rxjava3.core.Observable.class)).isNotNull(); + assertThat(getAdapter(io.reactivex.rxjava3.core.Single.class)).isNotNull(); + assertThat(getAdapter(io.reactivex.rxjava3.core.Maybe.class)).isNotNull(); + assertThat(getAdapter(io.reactivex.rxjava3.core.Completable.class)).isNotNull(); + } + + @Test + void toFlowable() { + List sequence = Arrays.asList(1, 2, 3); + Publisher source = Flux.fromIterable(sequence); + Object target = getAdapter(io.reactivex.rxjava3.core.Flowable.class).fromPublisher(source); + assertThat(target instanceof io.reactivex.rxjava3.core.Flowable).isTrue(); + assertThat(((io.reactivex.rxjava3.core.Flowable) target).toList().blockingGet()).isEqualTo(sequence); + } + + @Test + void toObservable() { + List sequence = Arrays.asList(1, 2, 3); + Publisher source = Flux.fromIterable(sequence); + Object target = getAdapter(io.reactivex.rxjava3.core.Observable.class).fromPublisher(source); + assertThat(target instanceof io.reactivex.rxjava3.core.Observable).isTrue(); + assertThat(((io.reactivex.rxjava3.core.Observable) target).toList().blockingGet()).isEqualTo(sequence); + } + + @Test + void toSingle() { + Publisher source = Flux.fromArray(new Integer[] {1}); + Object target = getAdapter(io.reactivex.rxjava3.core.Single.class).fromPublisher(source); + assertThat(target instanceof io.reactivex.rxjava3.core.Single).isTrue(); + assertThat(((io.reactivex.rxjava3.core.Single) target).blockingGet()).isEqualTo(Integer.valueOf(1)); + } + + @Test + void toCompletable() { + Publisher source = Flux.fromArray(new Integer[] {1, 2, 3}); + Object target = getAdapter(io.reactivex.rxjava3.core.Completable.class).fromPublisher(source); + assertThat(target instanceof io.reactivex.rxjava3.core.Completable).isTrue(); + ((io.reactivex.rxjava3.core.Completable) target).blockingAwait(); + } + + @Test + void fromFlowable() { + List sequence = Arrays.asList(1, 2, 3); + Object source = io.reactivex.rxjava3.core.Flowable.fromIterable(sequence); + Object target = getAdapter(io.reactivex.rxjava3.core.Flowable.class).toPublisher(source); + assertThat(target instanceof Flux).as("Expected Flux Publisher: " + target.getClass().getName()).isTrue(); + assertThat(((Flux) target).collectList().block(Duration.ofMillis(1000))).isEqualTo(sequence); + } + + @Test + void fromObservable() { + List sequence = Arrays.asList(1, 2, 3); + Object source = io.reactivex.rxjava3.core.Observable.fromIterable(sequence); + Object target = getAdapter(io.reactivex.rxjava3.core.Observable.class).toPublisher(source); + assertThat(target instanceof Flux).as("Expected Flux Publisher: " + target.getClass().getName()).isTrue(); + assertThat(((Flux) target).collectList().block(Duration.ofMillis(1000))).isEqualTo(sequence); + } + + @Test + void fromSingle() { + Object source = io.reactivex.rxjava3.core.Single.just(1); + Object target = getAdapter(io.reactivex.rxjava3.core.Single.class).toPublisher(source); + assertThat(target instanceof Mono).as("Expected Mono Publisher: " + target.getClass().getName()).isTrue(); + assertThat(((Mono) target).block(Duration.ofMillis(1000))).isEqualTo(Integer.valueOf(1)); + } + + @Test + void fromCompletable() { + Object source = io.reactivex.rxjava3.core.Completable.complete(); + Object target = getAdapter(io.reactivex.rxjava3.core.Completable.class).toPublisher(source); + assertThat(target instanceof Mono).as("Expected Mono Publisher: " + target.getClass().getName()).isTrue(); + ((Mono) target).block(Duration.ofMillis(1000)); + } + } + + @Nested + class Kotlin { + + @Test + void defaultAdapterRegistrations() { + + // Coroutines + assertThat(getAdapter(Deferred.class)).isNotNull(); + } + + @Test + void deferred() { + assertThat(getAdapter(CompletableFuture.class).getDescriptor().isDeferred()).isEqualTo(false); + assertThat(getAdapter(Deferred.class).getDescriptor().isDeferred()).isEqualTo(true); + assertThat(getAdapter(kotlinx.coroutines.flow.Flow.class).getDescriptor().isDeferred()).isEqualTo(true); + } + } + + private ReactiveAdapter getAdapter(Class reactiveType) { + ReactiveAdapter adapter = this.registry.getAdapter(reactiveType); + assertThat(adapter).isNotNull(); + return adapter; + } + + + private static class ExtendedFlux extends Flux { + + @Override + public void subscribe(CoreSubscriber actual) { + throw new UnsupportedOperationException(); + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java new file mode 100644 index 0000000..ba58532 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java @@ -0,0 +1,1649 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.AbstractCollection; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.concurrent.Callable; + +import org.assertj.core.api.AbstractAssert; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.core.ResolvableType.VariableResolver; +import org.springframework.util.MultiValueMap; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link ResolvableType}. + * + * @author Phillip Webb + * @author Juergen Hoeller + * @author Sebastien Deleuze + */ +@SuppressWarnings("rawtypes") +@ExtendWith(MockitoExtension.class) +class ResolvableTypeTests { + + @Captor + private ArgumentCaptor> typeVariableCaptor; + + + @Test + void noneReturnValues() throws Exception { + ResolvableType none = ResolvableType.NONE; + assertThat(none.as(Object.class)).isEqualTo(ResolvableType.NONE); + assertThat(none.asCollection()).isEqualTo(ResolvableType.NONE); + assertThat(none.asMap()).isEqualTo(ResolvableType.NONE); + assertThat(none.getComponentType()).isEqualTo(ResolvableType.NONE); + assertThat(none.getGeneric(0)).isEqualTo(ResolvableType.NONE); + assertThat(none.getGenerics().length).isEqualTo(0); + assertThat(none.getInterfaces().length).isEqualTo(0); + assertThat(none.getSuperType()).isEqualTo(ResolvableType.NONE); + assertThat(none.getType()).isEqualTo(ResolvableType.EmptyType.INSTANCE); + assertThat(none.hasGenerics()).isEqualTo(false); + assertThat(none.isArray()).isEqualTo(false); + assertThat(none.resolve()).isNull(); + assertThat(none.resolve(String.class)).isEqualTo(String.class); + assertThat(none.resolveGeneric(0)).isNull(); + assertThat(none.resolveGenerics().length).isEqualTo(0); + assertThat(none.toString()).isEqualTo("?"); + assertThat(none.hasUnresolvableGenerics()).isEqualTo(false); + assertThat(none.isAssignableFrom(ResolvableType.forClass(Object.class))).isEqualTo(false); + } + + @Test + void forClass() throws Exception { + ResolvableType type = ResolvableType.forClass(ExtendsList.class); + assertThat(type.getType()).isEqualTo(ExtendsList.class); + assertThat(type.getRawClass()).isEqualTo(ExtendsList.class); + assertThat(type.isAssignableFrom(ExtendsList.class)).isTrue(); + assertThat(type.isAssignableFrom(ArrayList.class)).isFalse(); + } + + @Test + void forClassWithNull() throws Exception { + ResolvableType type = ResolvableType.forClass(null); + assertThat(type.getType()).isEqualTo(Object.class); + assertThat(type.getRawClass()).isEqualTo(Object.class); + assertThat(type.isAssignableFrom(Object.class)).isTrue(); + assertThat(type.isAssignableFrom(String.class)).isTrue(); + } + + @Test + void forRawClass() throws Exception { + ResolvableType type = ResolvableType.forRawClass(ExtendsList.class); + assertThat(type.getType()).isEqualTo(ExtendsList.class); + assertThat(type.getRawClass()).isEqualTo(ExtendsList.class); + assertThat(type.isAssignableFrom(ExtendsList.class)).isTrue(); + assertThat(type.isAssignableFrom(ArrayList.class)).isFalse(); + } + + @Test + void forRawClassWithNull() throws Exception { + ResolvableType type = ResolvableType.forRawClass(null); + assertThat(type.getType()).isEqualTo(Object.class); + assertThat(type.getRawClass()).isEqualTo(Object.class); + assertThat(type.isAssignableFrom(Object.class)).isTrue(); + assertThat(type.isAssignableFrom(String.class)).isTrue(); + } + + @Test // gh-23321 + void forRawClassAssignableFromTypeVariable() throws Exception { + ResolvableType typeVariable = ResolvableType.forClass(ExtendsList.class).as(List.class).getGeneric(); + ResolvableType raw = ResolvableType.forRawClass(CharSequence.class); + assertThat(raw.resolve()).isEqualTo(CharSequence.class); + assertThat(typeVariable.resolve()).isEqualTo(CharSequence.class); + assertThat(raw.resolve().isAssignableFrom(typeVariable.resolve())).isTrue(); + assertThat(typeVariable.resolve().isAssignableFrom(raw.resolve())).isTrue(); + assertThat(raw.isAssignableFrom(typeVariable)).isTrue(); + assertThat(typeVariable.isAssignableFrom(raw)).isTrue(); + } + + @Test + void forInstanceMustNotBeNull() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + ResolvableType.forInstance(null)) + .withMessageContaining("Instance must not be null"); + } + + @Test + void forInstanceNoProvider() throws Exception { + ResolvableType type = ResolvableType.forInstance(new Object()); + assertThat(type.getType()).isEqualTo(Object.class); + assertThat(type.resolve()).isEqualTo(Object.class); + } + + @Test + void forInstanceProvider() throws Exception { + ResolvableType type = ResolvableType.forInstance(new MyGenericInterfaceType<>(String.class)); + assertThat(type.getRawClass()).isEqualTo(MyGenericInterfaceType.class); + assertThat(type.getGeneric().resolve()).isEqualTo(String.class); + } + + @Test + void forInstanceProviderNull() throws Exception { + ResolvableType type = ResolvableType.forInstance(new MyGenericInterfaceType(null)); + assertThat(type.getType()).isEqualTo(MyGenericInterfaceType.class); + assertThat(type.resolve()).isEqualTo(MyGenericInterfaceType.class); + } + + @Test + void forField() throws Exception { + Field field = Fields.class.getField("charSequenceList"); + ResolvableType type = ResolvableType.forField(field); + assertThat(type.getType()).isEqualTo(field.getGenericType()); + } + + @Test + void forPrivateField() throws Exception { + Field field = Fields.class.getDeclaredField("privateField"); + ResolvableType type = ResolvableType.forField(field); + assertThat(type.getType()).isEqualTo(field.getGenericType()); + assertThat(type.resolve()).isEqualTo(List.class); + assertThat(type.getSource()).isSameAs(field); + + Field field2 = Fields.class.getDeclaredField("otherPrivateField"); + ResolvableType type2 = ResolvableType.forField(field2); + assertThat(type2.getType()).isEqualTo(field2.getGenericType()); + assertThat(type2.resolve()).isEqualTo(List.class); + assertThat(type2.getSource()).isSameAs(field2); + + assertThat(type2).isEqualTo(type); + assertThat(type2.hashCode()).isEqualTo(type.hashCode()); + } + + @Test + void forFieldMustNotBeNull() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + ResolvableType.forField(null)) + .withMessageContaining("Field must not be null"); + } + + @Test + void forConstructorParameter() throws Exception { + Constructor constructor = Constructors.class.getConstructor(List.class); + ResolvableType type = ResolvableType.forConstructorParameter(constructor, 0); + assertThat(type.getType()).isEqualTo(constructor.getGenericParameterTypes()[0]); + } + + @Test + void forConstructorParameterMustNotBeNull() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + ResolvableType.forConstructorParameter(null, 0)) + .withMessageContaining("Constructor must not be null"); + } + + @Test + void forMethodParameterByIndex() throws Exception { + Method method = Methods.class.getMethod("charSequenceParameter", List.class); + ResolvableType type = ResolvableType.forMethodParameter(method, 0); + assertThat(type.getType()).isEqualTo(method.getGenericParameterTypes()[0]); + } + + @Test + void forMethodParameterByIndexMustNotBeNull() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + ResolvableType.forMethodParameter(null, 0)) + .withMessageContaining("Method must not be null"); + } + + @Test + void forMethodParameter() throws Exception { + Method method = Methods.class.getMethod("charSequenceParameter", List.class); + MethodParameter methodParameter = MethodParameter.forExecutable(method, 0); + ResolvableType type = ResolvableType.forMethodParameter(methodParameter); + assertThat(type.getType()).isEqualTo(method.getGenericParameterTypes()[0]); + } + + @Test + @SuppressWarnings("deprecation") + void forMethodParameterWithNesting() throws Exception { + Method method = Methods.class.getMethod("nested", Map.class); + MethodParameter methodParameter = MethodParameter.forExecutable(method, 0); + methodParameter.increaseNestingLevel(); + ResolvableType type = ResolvableType.forMethodParameter(methodParameter); + assertThat(type.resolve()).isEqualTo(Map.class); + assertThat(type.getGeneric(0).resolve()).isEqualTo(Byte.class); + assertThat(type.getGeneric(1).resolve()).isEqualTo(Long.class); + } + + @Test + @SuppressWarnings("deprecation") + void forMethodParameterWithNestingAndLevels() throws Exception { + Method method = Methods.class.getMethod("nested", Map.class); + MethodParameter methodParameter = MethodParameter.forExecutable(method, 0); + methodParameter.increaseNestingLevel(); + methodParameter.setTypeIndexForCurrentLevel(0); + ResolvableType type = ResolvableType.forMethodParameter(methodParameter); + assertThat(type.resolve()).isEqualTo(Map.class); + assertThat(type.getGeneric(0).resolve()).isEqualTo(String.class); + assertThat(type.getGeneric(1).resolve()).isEqualTo(Integer.class); + } + + @Test + void forMethodParameterMustNotBeNull() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + ResolvableType.forMethodParameter(null)) + .withMessageContaining("MethodParameter must not be null"); + } + + @Test // SPR-16210 + void forMethodParameterWithSameSignatureAndGenerics() throws Exception { + Method method = Methods.class.getMethod("list1"); + MethodParameter methodParameter = MethodParameter.forExecutable(method, -1); + ResolvableType type = ResolvableType.forMethodParameter(methodParameter); + assertThat(((MethodParameter)type.getSource()).getMethod()).isEqualTo(method); + + method = Methods.class.getMethod("list2"); + methodParameter = MethodParameter.forExecutable(method, -1); + type = ResolvableType.forMethodParameter(methodParameter); + assertThat(((MethodParameter)type.getSource()).getMethod()).isEqualTo(method); + } + + @Test + void forMethodReturn() throws Exception { + Method method = Methods.class.getMethod("charSequenceReturn"); + ResolvableType type = ResolvableType.forMethodReturnType(method); + assertThat(type.getType()).isEqualTo(method.getGenericReturnType()); + } + + @Test + void forMethodReturnMustNotBeNull() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + ResolvableType.forMethodReturnType(null)) + .withMessageContaining("Method must not be null"); + } + + @Test + void classType() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("classType")); + assertThat(type.getType().getClass()).isEqualTo(Class.class); + } + + @Test + void parameterizedType() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("parameterizedType")); + assertThat(type.getType()).isInstanceOf(ParameterizedType.class); + } + + @Test + void arrayClassType() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("arrayClassType")); + assertThat(type.getType()).isInstanceOf(Class.class); + assertThat(((Class) type.getType()).isArray()).isEqualTo(true); + } + + @Test + void genericArrayType() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("genericArrayType")); + assertThat(type.getType()).isInstanceOf(GenericArrayType.class); + } + + @Test + void wildcardType() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("wildcardType")); + assertThat(type.getType()).isInstanceOf(ParameterizedType.class); + assertThat(type.getGeneric().getType()).isInstanceOf(WildcardType.class); + } + + @Test + void typeVariableType() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("typeVariableType")); + assertThat(type.getType()).isInstanceOf(TypeVariable.class); + } + + @Test + void getComponentTypeForClassArray() throws Exception { + Field field = Fields.class.getField("arrayClassType"); + ResolvableType type = ResolvableType.forField(field); + assertThat(type.isArray()).isEqualTo(true); + assertThat(type.getComponentType().getType()) + .isEqualTo(((Class) field.getGenericType()).getComponentType()); + } + + @Test + void getComponentTypeForGenericArrayType() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("genericArrayType")); + assertThat(type.isArray()).isEqualTo(true); + assertThat(type.getComponentType().getType()).isEqualTo( + ((GenericArrayType) type.getType()).getGenericComponentType()); + } + + @Test + void getComponentTypeForVariableThatResolvesToGenericArray() throws Exception { + ResolvableType type = ResolvableType.forClass(ListOfGenericArray.class).asCollection().getGeneric(); + assertThat(type.isArray()).isEqualTo(true); + assertThat(type.getType()).isInstanceOf(TypeVariable.class); + assertThat(type.getComponentType().getType().toString()).isEqualTo( + "java.util.List"); + } + + @Test + void getComponentTypeForNonArray() throws Exception { + ResolvableType type = ResolvableType.forClass(String.class); + assertThat(type.isArray()).isEqualTo(false); + assertThat(type.getComponentType()).isEqualTo(ResolvableType.NONE); + } + + @Test + void asCollection() throws Exception { + ResolvableType type = ResolvableType.forClass(ExtendsList.class).asCollection(); + assertThat(type.resolve()).isEqualTo(Collection.class); + assertThat(type.resolveGeneric()).isEqualTo(CharSequence.class); + } + + @Test + void asMap() throws Exception { + ResolvableType type = ResolvableType.forClass(ExtendsMap.class).asMap(); + assertThat(type.resolve()).isEqualTo(Map.class); + assertThat(type.resolveGeneric(0)).isEqualTo(String.class); + assertThat(type.resolveGeneric(1)).isEqualTo(Integer.class); + } + + @Test + void asFromInterface() throws Exception { + ResolvableType type = ResolvableType.forClass(ExtendsList.class).as(List.class); + assertThat(type.getType().toString()).isEqualTo("java.util.List"); + } + + @Test + void asFromInheritedInterface() throws Exception { + ResolvableType type = ResolvableType.forClass(ExtendsList.class).as(Collection.class); + assertThat(type.getType().toString()).isEqualTo("java.util.Collection"); + } + + @Test + void asFromSuperType() throws Exception { + ResolvableType type = ResolvableType.forClass(ExtendsList.class).as(ArrayList.class); + assertThat(type.getType().toString()).isEqualTo("java.util.ArrayList"); + } + + @Test + void asFromInheritedSuperType() throws Exception { + ResolvableType type = ResolvableType.forClass(ExtendsList.class).as(List.class); + assertThat(type.getType().toString()).isEqualTo("java.util.List"); + } + + @Test + void asNotFound() throws Exception { + ResolvableType type = ResolvableType.forClass(ExtendsList.class).as(Map.class); + assertThat(type).isSameAs(ResolvableType.NONE); + } + + @Test + void asSelf() throws Exception { + ResolvableType type = ResolvableType.forClass(ExtendsList.class); + assertThat(type.as(ExtendsList.class)).isEqualTo(type); + } + + @Test + void getSuperType() throws Exception { + ResolvableType type = ResolvableType.forClass(ExtendsList.class).getSuperType(); + assertThat(type.resolve()).isEqualTo(ArrayList.class); + type = type.getSuperType(); + assertThat(type.resolve()).isEqualTo(AbstractList.class); + type = type.getSuperType(); + assertThat(type.resolve()).isEqualTo(AbstractCollection.class); + type = type.getSuperType(); + assertThat(type.resolve()).isEqualTo(Object.class); + } + + @Test + void getInterfaces() throws Exception { + ResolvableType type = ResolvableType.forClass(ExtendsList.class); + assertThat(type.getInterfaces().length).isEqualTo(0); + SortedSet interfaces = new TreeSet<>(); + for (ResolvableType interfaceType : type.getSuperType().getInterfaces()) { + interfaces.add(interfaceType.toString()); + } + assertThat(interfaces.toString()).isEqualTo( + "[java.io.Serializable, java.lang.Cloneable, " + + "java.util.List, java.util.RandomAccess]"); + } + + @Test + void noSuperType() throws Exception { + assertThat(ResolvableType.forClass(Object.class).getSuperType()) + .isEqualTo(ResolvableType.NONE); + } + + @Test + void noInterfaces() throws Exception { + assertThat(ResolvableType.forClass(Object.class).getInterfaces()).isEmpty(); + } + + @Test + void nested() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("nested")); + type = type.getNested(2); + assertThat(type.resolve()).isEqualTo(Map.class); + assertThat(type.getGeneric(0).resolve()).isEqualTo(Byte.class); + assertThat(type.getGeneric(1).resolve()).isEqualTo(Long.class); + } + + @Test + void nestedWithIndexes() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("nested")); + type = type.getNested(2, Collections.singletonMap(2, 0)); + assertThat(type.resolve()).isEqualTo(Map.class); + assertThat(type.getGeneric(0).resolve()).isEqualTo(String.class); + assertThat(type.getGeneric(1).resolve()).isEqualTo(Integer.class); + } + + @Test + void nestedWithArray() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("genericArrayType")); + type = type.getNested(2); + assertThat(type.resolve()).isEqualTo(List.class); + assertThat(type.resolveGeneric()).isEqualTo(String.class); + } + + @Test + void getGeneric() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("stringList")); + assertThat(type.getGeneric().getType()).isEqualTo(String.class); + } + + @Test + void getGenericByIndex() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("stringIntegerMultiValueMap")); + assertThat(type.getGeneric(0).getType()).isEqualTo(String.class); + assertThat(type.getGeneric(1).getType()).isEqualTo(Integer.class); + } + + @Test + void getGenericOfGeneric() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("stringListList")); + assertThat(type.getGeneric().getType().toString()).isEqualTo("java.util.List"); + assertThat(type.getGeneric().getGeneric().getType()).isEqualTo(String.class); + } + + @Test + void genericOfGenericWithAs() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("stringListList")).asCollection(); + assertThat(type.toString()).isEqualTo("java.util.Collection>"); + assertThat(type.getGeneric().asCollection().toString()).isEqualTo("java.util.Collection"); + } + + @Test + void getGenericOfGenericByIndexes() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("stringListList")); + assertThat(type.getGeneric(0, 0).getType()).isEqualTo(String.class); + } + + @Test + void getGenericOutOfBounds() throws Exception { + ResolvableType type = ResolvableType.forClass(List.class, ExtendsList.class); + assertThat(type.getGeneric(0)).isNotEqualTo(ResolvableType.NONE); + assertThat(type.getGeneric(1)).isEqualTo(ResolvableType.NONE); + assertThat(type.getGeneric(0, 1)).isEqualTo(ResolvableType.NONE); + } + + @Test + void hasGenerics() throws Exception { + ResolvableType type = ResolvableType.forClass(ExtendsList.class); + assertThat(type.hasGenerics()).isEqualTo(false); + assertThat(type.asCollection().hasGenerics()).isEqualTo(true); + } + + @Test + void getGenericsFromParameterizedType() throws Exception { + ResolvableType type = ResolvableType.forClass(List.class, ExtendsList.class); + ResolvableType[] generics = type.getGenerics(); + assertThat(generics.length).isEqualTo(1); + assertThat(generics[0].resolve()).isEqualTo(CharSequence.class); + } + + @Test + void getGenericsFromClass() throws Exception { + ResolvableType type = ResolvableType.forClass(List.class); + ResolvableType[] generics = type.getGenerics(); + assertThat(generics.length).isEqualTo(1); + assertThat(generics[0].getType().toString()).isEqualTo("E"); + } + + @Test + void noGetGenerics() throws Exception { + ResolvableType type = ResolvableType.forClass(ExtendsList.class); + ResolvableType[] generics = type.getGenerics(); + assertThat(generics.length).isEqualTo(0); + } + + @Test + void getResolvedGenerics() throws Exception { + ResolvableType type = ResolvableType.forClass(List.class, ExtendsList.class); + Class[] generics = type.resolveGenerics(); + assertThat(generics.length).isEqualTo(1); + assertThat(generics[0]).isEqualTo(CharSequence.class); + } + + @Test + void resolveClassType() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("classType")); + assertThat(type.resolve()).isEqualTo(List.class); + } + + @Test + void resolveParameterizedType() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("parameterizedType")); + assertThat(type.resolve()).isEqualTo(List.class); + } + + @Test + void resolveArrayClassType() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("arrayClassType")); + assertThat(type.resolve()).isEqualTo(List[].class); + } + + @Test + void resolveGenericArrayType() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("genericArrayType")); + assertThat(type.resolve()).isEqualTo(List[].class); + assertThat(type.getComponentType().resolve()).isEqualTo(List.class); + assertThat(type.getComponentType().getGeneric().resolve()).isEqualTo(String.class); + } + + @Test + void resolveGenericMultiArrayType() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("genericMultiArrayType")); + assertThat(type.resolve()).isEqualTo(List[][][].class); + assertThat(type.getComponentType().resolve()).isEqualTo(List[][].class); + } + + @Test + void resolveGenericArrayFromGeneric() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("stringArrayList")); + ResolvableType generic = type.asCollection().getGeneric(); + assertThat(generic.getType().toString()).isEqualTo("E"); + assertThat(generic.isArray()).isEqualTo(true); + assertThat(generic.resolve()).isEqualTo(String[].class); + } + + @Test + void resolveVariableGenericArray() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("variableTypeGenericArray"), TypedFields.class); + assertThat(type.getType().toString()).isEqualTo("T[]"); + assertThat(type.isArray()).isEqualTo(true); + assertThat(type.resolve()).isEqualTo(String[].class); + } + + @Test + void resolveVariableGenericArrayUnknown() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("variableTypeGenericArray")); + assertThat(type.getType().toString()).isEqualTo("T[]"); + assertThat(type.isArray()).isEqualTo(true); + assertThat(type.resolve()).isNull(); + } + + @Test + void resolveVariableGenericArrayUnknownWithFallback() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("variableTypeGenericArray")); + assertThat(type.getType().toString()).isEqualTo("T[]"); + assertThat(type.isArray()).isEqualTo(true); + assertThat(type.toClass()).isEqualTo(Object.class); + } + + @Test + void resolveWildcardTypeUpperBounds() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("wildcardType")); + assertThat(type.getGeneric().resolve()).isEqualTo(Number.class); + } + + @Test + void resolveWildcardLowerBounds() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("wildcardSuperType")); + assertThat(type.getGeneric().resolve()).isEqualTo(Number.class); + } + + @Test + void resolveVariableFromFieldType() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("stringList")); + assertThat(type.resolve()).isEqualTo(List.class); + assertThat(type.getGeneric().resolve()).isEqualTo(String.class); + } + + @Test + void resolveVariableFromFieldTypeUnknown() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("parameterizedType")); + assertThat(type.resolve()).isEqualTo(List.class); + assertThat(type.getGeneric().resolve()).isNull(); + } + + @Test + void resolveVariableFromInheritedField() throws Exception { + ResolvableType type = ResolvableType.forField( + Fields.class.getField("stringIntegerMultiValueMap")).as(Map.class); + assertThat(type.getGeneric(0).resolve()).isEqualTo(String.class); + assertThat(type.getGeneric(1).resolve()).isEqualTo(List.class); + assertThat(type.getGeneric(1, 0).resolve()).isEqualTo(Integer.class); + } + + @Test + void resolveVariableFromInheritedFieldSwitched() throws Exception { + ResolvableType type = ResolvableType.forField( + Fields.class.getField("stringIntegerMultiValueMapSwitched")).as(Map.class); + assertThat(type.getGeneric(0).resolve()).isEqualTo(String.class); + assertThat(type.getGeneric(1).resolve()).isEqualTo(List.class); + assertThat(type.getGeneric(1, 0).resolve()).isEqualTo(Integer.class); + } + + @Test + void doesResolveFromOuterOwner() throws Exception { + ResolvableType type = ResolvableType.forField( + Fields.class.getField("listOfListOfUnknown")).as(Collection.class); + assertThat(type.getGeneric(0).resolve()).isEqualTo(List.class); + assertThat(type.getGeneric(0).as(Collection.class).getGeneric(0).as(Collection.class).resolve()).isNull(); + } + + @Test + void resolveBoundedTypeVariableResult() throws Exception { + ResolvableType type = ResolvableType.forMethodReturnType(Methods.class.getMethod("boundedTypeVariableResult")); + assertThat(type.resolve()).isEqualTo(CharSequence.class); + } + + + @Test + void resolveBoundedTypeVariableWildcardResult() throws Exception { + ResolvableType type = ResolvableType.forMethodReturnType(Methods.class.getMethod("boundedTypeVariableWildcardResult")); + assertThat(type.getGeneric(1).asCollection().resolveGeneric()).isEqualTo(CharSequence.class); + } + + @Test + void resolveVariableNotFound() throws Exception { + ResolvableType type = ResolvableType.forMethodReturnType(Methods.class.getMethod("typedReturn")); + assertThat(type.resolve()).isNull(); + } + + @Test + void resolveTypeVariableFromSimpleInterfaceType() { + ResolvableType type = ResolvableType.forClass( + MySimpleInterfaceType.class).as(MyInterfaceType.class); + assertThat(type.resolveGeneric()).isEqualTo(String.class); + } + + @Test + void resolveTypeVariableFromSimpleCollectionInterfaceType() { + ResolvableType type = ResolvableType.forClass( + MyCollectionInterfaceType.class).as(MyInterfaceType.class); + assertThat(type.resolveGeneric()).isEqualTo(Collection.class); + assertThat(type.resolveGeneric(0, 0)).isEqualTo(String.class); + } + + @Test + void resolveTypeVariableFromSimpleSuperclassType() { + ResolvableType type = ResolvableType.forClass( + MySimpleSuperclassType.class).as(MySuperclassType.class); + assertThat(type.resolveGeneric()).isEqualTo(String.class); + } + + @Test + void resolveTypeVariableFromSimpleCollectionSuperclassType() { + ResolvableType type = ResolvableType.forClass( + MyCollectionSuperclassType.class).as(MySuperclassType.class); + assertThat(type.resolveGeneric()).isEqualTo(Collection.class); + assertThat(type.resolveGeneric(0, 0)).isEqualTo(String.class); + } + + @Test + void resolveTypeVariableFromFieldTypeWithImplementsClass() throws Exception { + ResolvableType type = ResolvableType.forField( + Fields.class.getField("parameterizedType"), TypedFields.class); + assertThat(type.resolve()).isEqualTo(List.class); + assertThat(type.getGeneric().resolve()).isEqualTo(String.class); + } + + @Test + void resolveTypeVariableFromFieldTypeWithImplementsType() throws Exception { + ResolvableType implementationType = ResolvableType.forClassWithGenerics( + Fields.class, Integer.class); + ResolvableType type = ResolvableType.forField( + Fields.class.getField("parameterizedType"), implementationType); + assertThat(type.resolve()).isEqualTo(List.class); + assertThat(type.getGeneric().resolve()).isEqualTo(Integer.class); + } + + @Test + void resolveTypeVariableFromSuperType() throws Exception { + ResolvableType type = ResolvableType.forClass(ExtendsList.class); + assertThat(type.resolve()).isEqualTo(ExtendsList.class); + assertThat(type.asCollection().resolveGeneric()) + .isEqualTo(CharSequence.class); + } + + @Test + void resolveTypeVariableFromClassWithImplementsClass() throws Exception { + ResolvableType type = ResolvableType.forClass( + MySuperclassType.class, MyCollectionSuperclassType.class); + assertThat(type.resolveGeneric()).isEqualTo(Collection.class); + assertThat(type.resolveGeneric(0, 0)).isEqualTo(String.class); + } + + @Test + void resolveTypeVariableFromConstructorParameter() throws Exception { + Constructor constructor = Constructors.class.getConstructor(List.class); + ResolvableType type = ResolvableType.forConstructorParameter(constructor, 0); + assertThat(type.resolve()).isEqualTo(List.class); + assertThat(type.resolveGeneric(0)).isEqualTo(CharSequence.class); + } + + @Test + void resolveUnknownTypeVariableFromConstructorParameter() throws Exception { + Constructor constructor = Constructors.class.getConstructor(Map.class); + ResolvableType type = ResolvableType.forConstructorParameter(constructor, 0); + assertThat(type.resolve()).isEqualTo(Map.class); + assertThat(type.resolveGeneric(0)).isNull(); + } + + @Test + void resolveTypeVariableFromConstructorParameterWithImplementsClass() throws Exception { + Constructor constructor = Constructors.class.getConstructor(Map.class); + ResolvableType type = ResolvableType.forConstructorParameter( + constructor, 0, TypedConstructors.class); + assertThat(type.resolve()).isEqualTo(Map.class); + assertThat(type.resolveGeneric(0)).isEqualTo(String.class); + } + + @Test + void resolveTypeVariableFromMethodParameter() throws Exception { + Method method = Methods.class.getMethod("typedParameter", Object.class); + ResolvableType type = ResolvableType.forMethodParameter(method, 0); + assertThat(type.resolve()).isNull(); + assertThat(type.getType().toString()).isEqualTo("T"); + } + + @Test + void resolveTypeVariableFromMethodParameterWithImplementsClass() throws Exception { + Method method = Methods.class.getMethod("typedParameter", Object.class); + ResolvableType type = ResolvableType.forMethodParameter(method, 0, TypedMethods.class); + assertThat(type.resolve()).isEqualTo(String.class); + assertThat(type.getType().toString()).isEqualTo("T"); + } + + @Test + void resolveTypeVariableFromMethodParameterType() throws Exception { + Method method = Methods.class.getMethod("typedParameter", Object.class); + MethodParameter methodParameter = MethodParameter.forExecutable(method, 0); + ResolvableType type = ResolvableType.forMethodParameter(methodParameter); + assertThat(type.resolve()).isNull(); + assertThat(type.getType().toString()).isEqualTo("T"); + } + + @Test + @SuppressWarnings("deprecation") + void resolveTypeVariableFromMethodParameterTypeWithImplementsClass() throws Exception { + Method method = Methods.class.getMethod("typedParameter", Object.class); + MethodParameter methodParameter = MethodParameter.forExecutable(method, 0); + methodParameter.setContainingClass(TypedMethods.class); + ResolvableType type = ResolvableType.forMethodParameter(methodParameter); + assertThat(type.resolve()).isEqualTo(String.class); + assertThat(type.getType().toString()).isEqualTo("T"); + } + + @Test + void resolveTypeVariableFromMethodParameterTypeWithImplementsType() throws Exception { + Method method = Methods.class.getMethod("typedParameter", Object.class); + MethodParameter methodParameter = MethodParameter.forExecutable(method, 0); + ResolvableType implementationType = ResolvableType.forClassWithGenerics(Methods.class, Integer.class); + ResolvableType type = ResolvableType.forMethodParameter(methodParameter, implementationType); + assertThat(type.resolve()).isEqualTo(Integer.class); + assertThat(type.getType().toString()).isEqualTo("T"); + } + + @Test + void resolveTypeVariableFromMethodReturn() throws Exception { + Method method = Methods.class.getMethod("typedReturn"); + ResolvableType type = ResolvableType.forMethodReturnType(method); + assertThat(type.resolve()).isNull(); + assertThat(type.getType().toString()).isEqualTo("T"); + } + + @Test + void resolveTypeVariableFromMethodReturnWithImplementsClass() throws Exception { + Method method = Methods.class.getMethod("typedReturn"); + ResolvableType type = ResolvableType.forMethodReturnType(method, TypedMethods.class); + assertThat(type.resolve()).isEqualTo(String.class); + assertThat(type.getType().toString()).isEqualTo("T"); + } + + @Test + void resolveTypeVariableFromType() throws Exception { + Type sourceType = Methods.class.getMethod("typedReturn").getGenericReturnType(); + ResolvableType type = ResolvableType.forType(sourceType); + assertThat(type.resolve()).isNull(); + assertThat(type.getType().toString()).isEqualTo("T"); + } + + @Test + void resolveTypeVariableFromTypeWithVariableResolver() throws Exception { + Type sourceType = Methods.class.getMethod("typedReturn").getGenericReturnType(); + ResolvableType type = ResolvableType.forType( + sourceType, ResolvableType.forClass(TypedMethods.class).as(Methods.class).asVariableResolver()); + assertThat(type.resolve()).isEqualTo(String.class); + assertThat(type.getType().toString()).isEqualTo("T"); + } + + @Test + void resolveTypeWithCustomVariableResolver() throws Exception { + VariableResolver variableResolver = mock(VariableResolver.class); + given(variableResolver.getSource()).willReturn(this); + ResolvableType longType = ResolvableType.forClass(Long.class); + given(variableResolver.resolveVariable(any())).willReturn(longType); + + ResolvableType variable = ResolvableType.forType( + Fields.class.getField("typeVariableType").getGenericType(), variableResolver); + ResolvableType parameterized = ResolvableType.forType( + Fields.class.getField("parameterizedType").getGenericType(), variableResolver); + + assertThat(variable.resolve()).isEqualTo(Long.class); + assertThat(parameterized.resolve()).isEqualTo(List.class); + assertThat(parameterized.resolveGeneric()).isEqualTo(Long.class); + verify(variableResolver, atLeastOnce()).resolveVariable(this.typeVariableCaptor.capture()); + assertThat(this.typeVariableCaptor.getValue().getName()).isEqualTo("T"); + } + + @Test + void resolveTypeVariableFromReflectiveParameterizedTypeReference() throws Exception { + Type sourceType = Methods.class.getMethod("typedReturn").getGenericReturnType(); + ResolvableType type = ResolvableType.forType(ParameterizedTypeReference.forType(sourceType)); + assertThat(type.resolve()).isNull(); + assertThat(type.getType().toString()).isEqualTo("T"); + } + + @Test + void resolveTypeVariableFromDeclaredParameterizedTypeReference() throws Exception { + Type sourceType = Methods.class.getMethod("charSequenceReturn").getGenericReturnType(); + ResolvableType reflectiveType = ResolvableType.forType(sourceType); + ResolvableType declaredType = ResolvableType.forType(new ParameterizedTypeReference>() {}); + assertThat(declaredType).isEqualTo(reflectiveType); + } + + @Test + void toStrings() throws Exception { + assertThat(ResolvableType.NONE.toString()).isEqualTo("?"); + + assertThat(forField("classType")).hasToString("java.util.List"); + assertThat(forField("typeVariableType")).hasToString("?"); + assertThat(forField("parameterizedType")).hasToString("java.util.List"); + assertThat(forField("arrayClassType")).hasToString("java.util.List[]"); + assertThat(forField("genericArrayType")).hasToString("java.util.List[]"); + assertThat(forField("genericMultiArrayType")).hasToString("java.util.List[][][]"); + assertThat(forField("wildcardType")).hasToString("java.util.List"); + assertThat(forField("wildcardSuperType")).hasToString("java.util.List"); + assertThat(forField("charSequenceList")).hasToString("java.util.List"); + assertThat(forField("stringList")).hasToString("java.util.List"); + assertThat(forField("stringListList")).hasToString("java.util.List>"); + assertThat(forField("stringArrayList")).hasToString("java.util.List"); + assertThat(forField("stringIntegerMultiValueMap")).hasToString("org.springframework.util.MultiValueMap"); + assertThat(forField("stringIntegerMultiValueMapSwitched")).hasToString(VariableNameSwitch.class.getName() + ""); + assertThat(forField("listOfListOfUnknown")).hasToString("java.util.List>"); + + assertThat(forTypedField("typeVariableType")).hasToString("java.lang.String"); + assertThat(forTypedField("parameterizedType")).hasToString("java.util.List"); + + assertThat(ResolvableType.forClass(ListOfGenericArray.class).toString()).isEqualTo(ListOfGenericArray.class.getName()); + assertThat(ResolvableType.forClass(List.class, ListOfGenericArray.class).toString()).isEqualTo("java.util.List[]>"); + } + + @Test + void getSource() throws Exception { + Class classType = MySimpleInterfaceType.class; + Field basicField = Fields.class.getField("classType"); + Field field = Fields.class.getField("charSequenceList"); + Method method = Methods.class.getMethod("charSequenceParameter", List.class); + MethodParameter methodParameter = MethodParameter.forExecutable(method, 0); + assertThat(ResolvableType.forField(basicField).getSource()).isEqualTo(basicField); + assertThat(ResolvableType.forField(field).getSource()).isEqualTo(field); + assertThat(ResolvableType.forMethodParameter(methodParameter).getSource()).isEqualTo(methodParameter); + assertThat(ResolvableType.forMethodParameter(method, 0).getSource()).isEqualTo(methodParameter); + assertThat(ResolvableType.forClass(classType).getSource()).isEqualTo(classType); + assertThat(ResolvableType.forClass(classType).getSuperType().getSource()).isEqualTo(classType.getGenericSuperclass()); + } + + @Test + void resolveFromOuterClass() throws Exception { + Field field = EnclosedInParameterizedType.InnerTyped.class.getField("field"); + ResolvableType type = ResolvableType.forField(field, TypedEnclosedInParameterizedType.TypedInnerTyped.class); + assertThat(type.resolve()).isEqualTo(Integer.class); + } + + @Test + void resolveFromClassWithGenerics() throws Exception { + ResolvableType type = ResolvableType.forClassWithGenerics(List.class, ResolvableType.forClassWithGenerics(List.class, String.class)); + assertThat(type.asCollection().toString()).isEqualTo("java.util.Collection>"); + assertThat(type.asCollection().getGeneric().toString()).isEqualTo("java.util.List"); + assertThat(type.asCollection().getGeneric().asCollection().toString()).isEqualTo("java.util.Collection"); + assertThat(type.toString()).isEqualTo("java.util.List>"); + assertThat(type.asCollection().getGeneric().getGeneric().resolve()).isEqualTo(String.class); + } + + @Test + void isAssignableFromMustNotBeNull() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + ResolvableType.forClass(Object.class).isAssignableFrom((ResolvableType) null)) + .withMessageContaining("Type must not be null"); + } + + @Test + void isAssignableFromForNone() throws Exception { + ResolvableType objectType = ResolvableType.forClass(Object.class); + assertThat(objectType.isAssignableFrom(ResolvableType.NONE)).isEqualTo(false); + assertThat(ResolvableType.NONE.isAssignableFrom(objectType)).isEqualTo(false); + } + + @Test + void isAssignableFromForClassAndClass() throws Exception { + ResolvableType objectType = ResolvableType.forClass(Object.class); + ResolvableType charSequenceType = ResolvableType.forClass(CharSequence.class); + ResolvableType stringType = ResolvableType.forClass(String.class); + + assertThatResolvableType(objectType).isAssignableFrom(objectType, charSequenceType, stringType); + assertThatResolvableType(charSequenceType).isAssignableFrom(charSequenceType, stringType).isNotAssignableFrom(objectType); + assertThatResolvableType(stringType).isAssignableFrom(stringType).isNotAssignableFrom(objectType, charSequenceType); + + assertThat(objectType.isAssignableFrom(String.class)).isTrue(); + assertThat(objectType.isAssignableFrom(StringBuilder.class)).isTrue(); + assertThat(charSequenceType.isAssignableFrom(String.class)).isTrue(); + assertThat(charSequenceType.isAssignableFrom(StringBuilder.class)).isTrue(); + assertThat(stringType.isAssignableFrom(String.class)).isTrue(); + assertThat(stringType.isAssignableFrom(StringBuilder.class)).isFalse(); + + assertThat(objectType.isInstance("a String")).isTrue(); + assertThat(objectType.isInstance(new StringBuilder("a StringBuilder"))).isTrue(); + assertThat(charSequenceType.isInstance("a String")).isTrue(); + assertThat(charSequenceType.isInstance(new StringBuilder("a StringBuilder"))).isTrue(); + assertThat(stringType.isInstance("a String")).isTrue(); + assertThat(stringType.isInstance(new StringBuilder("a StringBuilder"))).isFalse(); + } + + @Test + void isAssignableFromCannotBeResolved() throws Exception { + ResolvableType objectType = ResolvableType.forClass(Object.class); + ResolvableType unresolvableVariable = ResolvableType.forField(AssignmentBase.class.getField("o")); + assertThat(unresolvableVariable.resolve()).isNull(); + assertThatResolvableType(objectType).isAssignableFrom(unresolvableVariable); + assertThatResolvableType(unresolvableVariable).isAssignableFrom(objectType); + } + + @Test + void isAssignableFromForClassAndSimpleVariable() throws Exception { + ResolvableType objectType = ResolvableType.forClass(Object.class); + ResolvableType charSequenceType = ResolvableType.forClass(CharSequence.class); + ResolvableType stringType = ResolvableType.forClass(String.class); + + ResolvableType objectVariable = ResolvableType.forField(AssignmentBase.class.getField("o"), Assignment.class); + ResolvableType charSequenceVariable = ResolvableType.forField(AssignmentBase.class.getField("c"), Assignment.class); + ResolvableType stringVariable = ResolvableType.forField(AssignmentBase.class.getField("s"), Assignment.class); + + assertThatResolvableType(objectType).isAssignableFrom(objectVariable, charSequenceVariable, stringVariable); + assertThatResolvableType(charSequenceType).isAssignableFrom(charSequenceVariable, stringVariable).isNotAssignableFrom(objectVariable); + assertThatResolvableType(stringType).isAssignableFrom(stringVariable).isNotAssignableFrom(objectVariable, charSequenceVariable); + + assertThatResolvableType(objectVariable).isAssignableFrom(objectType, charSequenceType, stringType); + assertThatResolvableType(charSequenceVariable).isAssignableFrom(charSequenceType, stringType).isNotAssignableFrom(objectType); + assertThatResolvableType(stringVariable).isAssignableFrom(stringType).isNotAssignableFrom(objectType, charSequenceType); + + assertThatResolvableType(objectVariable).isAssignableFrom(objectVariable, charSequenceVariable, stringVariable); + assertThatResolvableType(charSequenceVariable).isAssignableFrom(charSequenceVariable, stringVariable).isNotAssignableFrom(objectVariable); + assertThatResolvableType(stringVariable).isAssignableFrom(stringVariable).isNotAssignableFrom(objectVariable, charSequenceVariable); + } + + @Test + void isAssignableFromForSameClassNonExtendsGenerics() throws Exception { + ResolvableType objectList = ResolvableType.forField(AssignmentBase.class.getField("listo"), Assignment.class); + ResolvableType stringList = ResolvableType.forField(AssignmentBase.class.getField("lists"), Assignment.class); + + assertThatResolvableType(stringList).isNotAssignableFrom(objectList); + assertThatResolvableType(objectList).isNotAssignableFrom(stringList); + assertThatResolvableType(stringList).isAssignableFrom(stringList); + } + + @Test + void isAssignableFromForSameClassExtendsGenerics() throws Exception { + + // Generic assignment can be a little confusing, given: + // + // List c1, List c2, List s; + // + // c2 = s; is allowed and is often used for argument input, for example + // see List.addAll(). You can get items from c2 but you cannot add items without + // getting a generic type 'is not applicable for the arguments' error. This makes + // sense since if you added a StringBuffer to c2 it would break the rules on s. + // + // c1 = s; not allowed. Since there is no '? extends' to cause the generic + // 'is not applicable for the arguments' error when adding (which would pollute + // s). + + ResolvableType objectList = ResolvableType.forField(AssignmentBase.class.getField("listo"), Assignment.class); + ResolvableType charSequenceList = ResolvableType.forField(AssignmentBase.class.getField("listc"), Assignment.class); + ResolvableType stringList = ResolvableType.forField(AssignmentBase.class.getField("lists"), Assignment.class); + ResolvableType extendsObjectList = ResolvableType.forField(AssignmentBase.class.getField("listxo"), Assignment.class); + ResolvableType extendsCharSequenceList = ResolvableType.forField(AssignmentBase.class.getField("listxc"), Assignment.class); + ResolvableType extendsStringList = ResolvableType.forField(AssignmentBase.class.getField("listxs"), Assignment.class); + + assertThatResolvableType(objectList).isNotAssignableFrom(extendsObjectList, extendsCharSequenceList, extendsStringList); + assertThatResolvableType(charSequenceList).isNotAssignableFrom(extendsObjectList, extendsCharSequenceList, extendsStringList); + assertThatResolvableType(stringList).isNotAssignableFrom(extendsObjectList, extendsCharSequenceList, extendsStringList); + assertThatResolvableType(extendsObjectList).isAssignableFrom(objectList, charSequenceList, stringList); + assertThatResolvableType(extendsObjectList).isAssignableFrom(extendsObjectList, extendsCharSequenceList, extendsStringList); + assertThatResolvableType(extendsCharSequenceList).isAssignableFrom(extendsCharSequenceList, extendsStringList).isNotAssignableFrom(extendsObjectList); + assertThatResolvableType(extendsCharSequenceList).isAssignableFrom(charSequenceList, stringList).isNotAssignableFrom(objectList); + assertThatResolvableType(extendsStringList).isAssignableFrom(extendsStringList).isNotAssignableFrom(extendsObjectList, extendsCharSequenceList); + assertThatResolvableType(extendsStringList).isAssignableFrom(stringList).isNotAssignableFrom(objectList, charSequenceList); + } + + @Test + void isAssignableFromForDifferentClassesWithGenerics() throws Exception { + ResolvableType extendsCharSequenceCollection = ResolvableType.forField(AssignmentBase.class.getField("collectionxc"), Assignment.class); + ResolvableType charSequenceCollection = ResolvableType.forField(AssignmentBase.class.getField("collectionc"), Assignment.class); + ResolvableType charSequenceList = ResolvableType.forField(AssignmentBase.class.getField("listc"), Assignment.class); + ResolvableType extendsCharSequenceList = ResolvableType.forField(AssignmentBase.class.getField("listxc"), Assignment.class); + ResolvableType extendsStringList = ResolvableType.forField(AssignmentBase.class.getField("listxs"), Assignment.class); + + assertThatResolvableType(extendsCharSequenceCollection).isAssignableFrom(charSequenceCollection, charSequenceList, extendsCharSequenceList, extendsStringList); + assertThatResolvableType(charSequenceCollection).isAssignableFrom(charSequenceList).isNotAssignableFrom(extendsCharSequenceList, extendsStringList); + assertThatResolvableType(charSequenceList).isNotAssignableFrom(extendsCharSequenceCollection, charSequenceCollection); + assertThatResolvableType(extendsCharSequenceList).isNotAssignableFrom(extendsCharSequenceCollection, charSequenceCollection); + assertThatResolvableType(extendsStringList).isNotAssignableFrom(charSequenceCollection, charSequenceList, extendsCharSequenceList); + } + + @Test + void isAssignableFromForArrays() throws Exception { + ResolvableType object = ResolvableType.forField(AssignmentBase.class.getField("o"), Assignment.class); + ResolvableType objectArray = ResolvableType.forField(AssignmentBase.class.getField("oarray"), Assignment.class); + ResolvableType charSequenceArray = ResolvableType.forField(AssignmentBase.class.getField("carray"), Assignment.class); + ResolvableType stringArray = ResolvableType.forField(AssignmentBase.class.getField("sarray"), Assignment.class); + + assertThatResolvableType(object).isAssignableFrom(objectArray, charSequenceArray, stringArray); + assertThatResolvableType(objectArray).isAssignableFrom(objectArray, charSequenceArray, stringArray).isNotAssignableFrom(object); + assertThatResolvableType(charSequenceArray).isAssignableFrom(charSequenceArray, stringArray).isNotAssignableFrom(object, objectArray); + assertThatResolvableType(stringArray).isAssignableFrom(stringArray).isNotAssignableFrom(object, objectArray, charSequenceArray); + } + + @Test + void isAssignableFromForWildcards() throws Exception { + ResolvableType object = ResolvableType.forClass(Object.class); + ResolvableType charSequence = ResolvableType.forClass(CharSequence.class); + ResolvableType string = ResolvableType.forClass(String.class); + ResolvableType extendsAnon = ResolvableType.forField(AssignmentBase.class.getField("listAnon"), Assignment.class).getGeneric(); + ResolvableType extendsObject = ResolvableType.forField(AssignmentBase.class.getField("listxo"), Assignment.class).getGeneric(); + ResolvableType extendsCharSequence = ResolvableType.forField(AssignmentBase.class.getField("listxc"), Assignment.class).getGeneric(); + ResolvableType extendsString = ResolvableType.forField(AssignmentBase.class.getField("listxs"), Assignment.class).getGeneric(); + ResolvableType superObject = ResolvableType.forField(AssignmentBase.class.getField("listso"), Assignment.class).getGeneric(); + ResolvableType superCharSequence = ResolvableType.forField(AssignmentBase.class.getField("listsc"), Assignment.class).getGeneric(); + ResolvableType superString = ResolvableType.forField(AssignmentBase.class.getField("listss"), Assignment.class).getGeneric(); + + // Language Spec 4.5.1. Type Arguments and Wildcards + + // ? extends T <= ? extends S if T <: S + assertThatResolvableType(extendsCharSequence).isAssignableFrom(extendsCharSequence, extendsString).isNotAssignableFrom(extendsObject); + assertThatResolvableType(extendsCharSequence).isAssignableFrom(charSequence, string).isNotAssignableFrom(object); + + // ? super T <= ? super S if S <: T + assertThatResolvableType(superCharSequence).isAssignableFrom(superObject, superCharSequence).isNotAssignableFrom(superString); + assertThatResolvableType(superCharSequence).isAssignableFrom(object, charSequence).isNotAssignableFrom(string); + + // [Implied] super / extends cannot be mixed + assertThatResolvableType(superCharSequence).isNotAssignableFrom(extendsObject, extendsCharSequence, extendsString); + assertThatResolvableType(extendsCharSequence).isNotAssignableFrom(superObject, superCharSequence, superString); + + // T <= T + assertThatResolvableType(charSequence).isAssignableFrom(charSequence, string).isNotAssignableFrom(object); + + // T <= ? extends T + assertThatResolvableType(extendsCharSequence).isAssignableFrom(charSequence, string).isNotAssignableFrom(object); + assertThatResolvableType(charSequence).isNotAssignableFrom(extendsObject, extendsCharSequence, extendsString); + assertThatResolvableType(extendsAnon).isAssignableFrom(object, charSequence, string); + + // T <= ? super T + assertThatResolvableType(superCharSequence).isAssignableFrom(object, charSequence).isNotAssignableFrom(string); + assertThatResolvableType(charSequence).isNotAssignableFrom(superObject, superCharSequence, superString); + } + + @Test + void isAssignableFromForComplexWildcards() throws Exception { + ResolvableType complex1 = ResolvableType.forField(AssignmentBase.class.getField("complexWildcard1")); + ResolvableType complex2 = ResolvableType.forField(AssignmentBase.class.getField("complexWildcard2")); + ResolvableType complex3 = ResolvableType.forField(AssignmentBase.class.getField("complexWildcard3")); + ResolvableType complex4 = ResolvableType.forField(AssignmentBase.class.getField("complexWildcard4")); + + assertThatResolvableType(complex1).isAssignableFrom(complex2); + assertThatResolvableType(complex2).isNotAssignableFrom(complex1); + assertThatResolvableType(complex3).isAssignableFrom(complex4); + assertThatResolvableType(complex4).isNotAssignableFrom(complex3); + } + + @Test + void hashCodeAndEquals() throws Exception { + ResolvableType forClass = ResolvableType.forClass(List.class); + ResolvableType forFieldDirect = ResolvableType.forField(Fields.class.getDeclaredField("stringList")); + ResolvableType forFieldViaType = ResolvableType.forType(Fields.class.getDeclaredField("stringList").getGenericType(), (VariableResolver) null); + ResolvableType forFieldWithImplementation = ResolvableType.forField(Fields.class.getDeclaredField("stringList"), TypedFields.class); + + assertThat(forClass).isEqualTo(forClass); + assertThat(forClass.hashCode()).isEqualTo(forClass.hashCode()); + assertThat(forClass).isNotEqualTo(forFieldDirect); + assertThat(forClass).isNotEqualTo(forFieldWithImplementation); + + assertThat(forFieldDirect).isEqualTo(forFieldDirect); + assertThat(forFieldDirect).isNotEqualTo(forFieldViaType); + assertThat(forFieldDirect).isNotEqualTo(forFieldWithImplementation); + } + + @Test + void javaDocSample() throws Exception { + ResolvableType t = ResolvableType.forField(getClass().getDeclaredField("myMap")); + assertThat(t.toString()).isEqualTo("java.util.HashMap>"); + assertThat(t.getType().getTypeName()).isEqualTo("java.util.HashMap>"); + assertThat(t.getType().toString()).isEqualTo("java.util.HashMap>"); + assertThat(t.getSuperType().toString()).isEqualTo("java.util.AbstractMap>"); + assertThat(t.asMap().toString()).isEqualTo("java.util.Map>"); + assertThat(t.getGeneric(0).resolve()).isEqualTo(Integer.class); + assertThat(t.getGeneric(1).resolve()).isEqualTo(List.class); + assertThat(t.getGeneric(1).toString()).isEqualTo("java.util.List"); + assertThat(t.resolveGeneric(1, 0)).isEqualTo(String.class); + } + + @Test + void forClassWithGenerics() throws Exception { + ResolvableType elementType = ResolvableType.forClassWithGenerics(Map.class, Integer.class, String.class); + ResolvableType listType = ResolvableType.forClassWithGenerics(List.class, elementType); + assertThat(listType.toString()).isEqualTo("java.util.List>"); + assertThat(listType.getType().getTypeName()).isEqualTo("java.util.List>"); + assertThat(listType.getType().toString()).isEqualTo("java.util.List>"); + } + + @Test + void classWithGenericsAs() throws Exception { + ResolvableType type = ResolvableType.forClassWithGenerics(MultiValueMap.class, Integer.class, String.class); + assertThat(type.asMap().toString()).isEqualTo("java.util.Map>"); + } + + @Test + void forClassWithMismatchedGenerics() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + ResolvableType.forClassWithGenerics(Map.class, Integer.class)) + .withMessageContaining("Mismatched number of generics specified"); + } + + @Test + void forArrayComponent() throws Exception { + ResolvableType elementType = ResolvableType.forField(Fields.class.getField("stringList")); + ResolvableType type = ResolvableType.forArrayComponent(elementType); + assertThat(type.toString()).isEqualTo("java.util.List[]"); + assertThat(type.resolve()).isEqualTo(List[].class); + } + + @Test + void serialize() throws Exception { + testSerialization(ResolvableType.forClass(List.class)); + testSerialization(ResolvableType.forField(Fields.class.getField("charSequenceList"))); + testSerialization(ResolvableType.forMethodParameter(Methods.class.getMethod("charSequenceParameter", List.class), 0)); + testSerialization(ResolvableType.forMethodReturnType(Methods.class.getMethod("charSequenceReturn"))); + testSerialization(ResolvableType.forConstructorParameter(Constructors.class.getConstructor(List.class), 0)); + testSerialization(ResolvableType.forField(Fields.class.getField("charSequenceList")).getGeneric()); + ResolvableType deserializedNone = testSerialization(ResolvableType.NONE); + assertThat(deserializedNone).isSameAs(ResolvableType.NONE); + } + + @Test + void canResolveVoid() throws Exception { + ResolvableType type = ResolvableType.forClass(void.class); + assertThat(type.resolve()).isEqualTo(void.class); + } + + @Test + void narrow() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("stringList")); + ResolvableType narrow = ResolvableType.forType(ArrayList.class, type); + assertThat(narrow.getGeneric().resolve()).isEqualTo(String.class); + } + + @Test + void hasUnresolvableGenerics() throws Exception { + ResolvableType type = ResolvableType.forField(Fields.class.getField("stringList")); + assertThat(type.hasUnresolvableGenerics()).isEqualTo(false); + } + + @Test + void hasUnresolvableGenericsBasedOnOwnGenerics() throws Exception { + ResolvableType type = ResolvableType.forClass(List.class); + assertThat(type.hasUnresolvableGenerics()).isEqualTo(true); + } + + @Test + void hasUnresolvableGenericsWhenSelfNotResolvable() throws Exception { + ResolvableType type = ResolvableType.forClass(List.class).getGeneric(); + assertThat(type.hasUnresolvableGenerics()).isEqualTo(false); + } + + @Test + void hasUnresolvableGenericsWhenImplementesRawInterface() throws Exception { + ResolvableType type = ResolvableType.forClass(MySimpleInterfaceTypeWithImplementsRaw.class); + for (ResolvableType generic : type.getGenerics()) { + assertThat(generic.resolve()).isNotNull(); + } + assertThat(type.hasUnresolvableGenerics()).isEqualTo(true); + } + + @Test + void hasUnresolvableGenericsWhenExtends() throws Exception { + ResolvableType type = ResolvableType.forClass(ExtendsMySimpleInterfaceTypeWithImplementsRaw.class); + for (ResolvableType generic : type.getGenerics()) { + assertThat(generic.resolve()).isNotNull(); + } + assertThat(type.hasUnresolvableGenerics()).isEqualTo(true); + } + + @Test + void spr11219() throws Exception { + ResolvableType type = ResolvableType.forField(BaseProvider.class.getField("stuff"), BaseProvider.class); + assertThat(type.getNested(2).isAssignableFrom(ResolvableType.forClass(BaseImplementation.class))).isTrue(); + assertThat(type.toString()).isEqualTo("java.util.Collection>"); + } + + @Test + void spr12701() throws Exception { + ResolvableType resolvableType = ResolvableType.forClassWithGenerics(Callable.class, String.class); + Type type = resolvableType.getType(); + assertThat(type).isInstanceOf(ParameterizedType.class); + assertThat(((ParameterizedType) type).getRawType()).isEqualTo(Callable.class); + assertThat(((ParameterizedType) type).getActualTypeArguments().length).isEqualTo(1); + assertThat(((ParameterizedType) type).getActualTypeArguments()[0]).isEqualTo(String.class); + } + + @Test + void spr14648() throws Exception { + ResolvableType collectionClass = ResolvableType.forRawClass(Collection.class); + ResolvableType setClass = ResolvableType.forRawClass(Set.class); + ResolvableType fromReturnType = ResolvableType.forMethodReturnType(Methods.class.getMethod("wildcardSet")); + assertThat(collectionClass.isAssignableFrom(fromReturnType)).isTrue(); + assertThat(setClass.isAssignableFrom(fromReturnType)).isTrue(); + } + + @Test + void spr16456() throws Exception { + ResolvableType genericType = ResolvableType.forField( + UnresolvedWithGenerics.class.getDeclaredField("set")).asCollection(); + ResolvableType type = ResolvableType.forClassWithGenerics(ArrayList.class, genericType.getGeneric()); + assertThat(type.resolveGeneric()).isEqualTo(Integer.class); + } + + + private ResolvableType testSerialization(ResolvableType type) throws Exception { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(bos); + oos.writeObject(type); + oos.close(); + ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray())); + ResolvableType read = (ResolvableType) ois.readObject(); + assertThat(read).isEqualTo(type); + assertThat(read.getType()).isEqualTo(type.getType()); + assertThat(read.resolve()).isEqualTo(type.resolve()); + return read; + } + + private ResolvableType forField(String field) throws NoSuchFieldException { + return ResolvableType.forField(Fields.class.getField(field)); + } + + private ResolvableType forTypedField(String field) throws NoSuchFieldException { + return ResolvableType.forField(Fields.class.getField(field), TypedFields.class); + } + + private static ResolvableTypeAssert assertThatResolvableType(ResolvableType type) { + return new ResolvableTypeAssert(type); + } + + + @SuppressWarnings("unused") + private HashMap> myMap; + + + @SuppressWarnings("serial") + static class ExtendsList extends ArrayList { + } + + @SuppressWarnings("serial") + static class ExtendsMap extends HashMap { + } + + + static class Fields { + + public List classType; + + public T typeVariableType; + + public List parameterizedType; + + public List[] arrayClassType; + + public List[] genericArrayType; + + public List[][][] genericMultiArrayType; + + public List wildcardType; + + public List wildcardSuperType = new ArrayList(); + + public List charSequenceList; + + public List stringList; + + public List> stringListList; + + public List stringArrayList; + + public MultiValueMap stringIntegerMultiValueMap; + + public VariableNameSwitch stringIntegerMultiValueMapSwitched; + + public List listOfListOfUnknown; + + @SuppressWarnings("unused") + private List privateField; + + @SuppressWarnings("unused") + private List otherPrivateField; + + public Map, Map> nested; + + public T[] variableTypeGenericArray; + } + + + static class TypedFields extends Fields { + } + + + interface Methods { + + List charSequenceReturn(); + + void charSequenceParameter(List cs); + + R boundedTypeVariableResult(); + + Map> boundedTypeVariableWildcardResult(); + + void nested(Map, Map> p); + + void typedParameter(T p); + + T typedReturn(); + + Set wildcardSet(); + + List list1(); + + List list2(); + } + + + static class AssignmentBase { + + public O o; + + public C c; + + public S s; + + public List listo; + + public List listc; + + public List lists; + + public List listAnon; + + public List listxo; + + public List listxc; + + public List listxs; + + public List listso; + + public List listsc; + + public List listss; + + public O[] oarray; + + public C[] carray; + + public S[] sarray; + + public Collection collectionc; + + public Collection collectionxc; + + public Map> complexWildcard1; + + public MultiValueMap complexWildcard2; + + public Collection> complexWildcard3; + + public List> complexWildcard4; + } + + + static class Assignment extends AssignmentBase { + } + + + interface TypedMethods extends Methods { + } + + + static class Constructors { + + public Constructors(List p) { + } + + public Constructors(Map p) { + } + } + + + static class TypedConstructors extends Constructors { + + public TypedConstructors(List p) { + super(p); + } + + public TypedConstructors(Map p) { + super(p); + } + } + + + public interface MyInterfaceType { + } + + public class MyGenericInterfaceType implements MyInterfaceType, ResolvableTypeProvider { + + private final Class type; + + public MyGenericInterfaceType(Class type) { + this.type = type; + } + + @Override + public ResolvableType getResolvableType() { + if (this.type == null) { + return null; + } + return ResolvableType.forClassWithGenerics(getClass(), this.type); + } + } + + + public class MySimpleInterfaceType implements MyInterfaceType { + } + + public abstract class MySimpleInterfaceTypeWithImplementsRaw implements MyInterfaceType, List { + } + + public abstract class ExtendsMySimpleInterfaceTypeWithImplementsRaw extends MySimpleInterfaceTypeWithImplementsRaw { + } + + + public class MyCollectionInterfaceType implements MyInterfaceType> { + } + + + public abstract class MySuperclassType { + } + + + public class MySimpleSuperclassType extends MySuperclassType { + } + + + public class MyCollectionSuperclassType extends MySuperclassType> { + } + + + interface Wildcard extends List { + } + + + interface RawExtendsWildcard extends Wildcard { + } + + + interface VariableNameSwitch extends MultiValueMap { + } + + + interface ListOfGenericArray extends List[]> { + } + + + static class EnclosedInParameterizedType { + + static class InnerRaw { + } + + class InnerTyped { + + public T field; + } + } + + + static class TypedEnclosedInParameterizedType extends EnclosedInParameterizedType { + + class TypedInnerTyped extends InnerTyped { + } + } + + + public interface IProvider

    { + } + + public interface IBase> { + } + + public abstract class AbstractBase> implements IBase { + } + + public class BaseImplementation extends AbstractBase { + } + + public class BaseProvider> implements IProvider> { + + public Collection> stuff; + } + + + public abstract class UnresolvedWithGenerics { + + Set set; + } + + + private static class ResolvableTypeAssert extends AbstractAssert{ + + public ResolvableTypeAssert(ResolvableType actual) { + super(actual, ResolvableTypeAssert.class); + } + + public ResolvableTypeAssert isAssignableFrom(ResolvableType... types) { + for (ResolvableType type : types) { + if (!actual.isAssignableFrom(type)) { + throw new AssertionError("Expecting " + describe(actual) + " to be assignable from " + describe(type)); + } + } + return this; + } + + public ResolvableTypeAssert isNotAssignableFrom(ResolvableType... types) { + for (ResolvableType type : types) { + if (actual.isAssignableFrom(type)) { + throw new AssertionError("Expecting " + describe(actual) + " to not be assignable from " + describe(type)); + } + } + return this; + } + + private String describe(ResolvableType type) { + if (type == ResolvableType.NONE) { + return "NONE"; + } + if (type.getType().getClass().equals(Class.class)) { + return type.toString(); + } + return type.getType() + ":" + type; + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/SerializableTypeWrapperTests.java b/spring-core/src/test/java/org/springframework/core/SerializableTypeWrapperTests.java new file mode 100644 index 0000000..8cd70d0 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/SerializableTypeWrapperTests.java @@ -0,0 +1,147 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SerializableTypeWrapper}. + * + * @author Phillip Webb + */ +class SerializableTypeWrapperTests { + + @Test + void forField() throws Exception { + Type type = SerializableTypeWrapper.forField(Fields.class.getField("parameterizedType")); + assertThat(type.toString()).isEqualTo("java.util.List"); + assertSerializable(type); + } + + @Test + void forMethodParameter() throws Exception { + Method method = Methods.class.getDeclaredMethod("method", Class.class, Object.class); + Type type = SerializableTypeWrapper.forMethodParameter(MethodParameter.forExecutable(method, 0)); + assertThat(type.toString()).isEqualTo("java.lang.Class"); + assertSerializable(type); + } + + @Test + void forConstructor() throws Exception { + Constructor constructor = Constructors.class.getDeclaredConstructor(List.class); + Type type = SerializableTypeWrapper.forMethodParameter(MethodParameter.forExecutable(constructor, 0)); + assertThat(type.toString()).isEqualTo("java.util.List"); + assertSerializable(type); + } + + @Test + void classType() throws Exception { + Type type = SerializableTypeWrapper.forField(Fields.class.getField("classType")); + assertThat(type.toString()).isEqualTo("class java.lang.String"); + assertSerializable(type); + } + + @Test + void genericArrayType() throws Exception { + GenericArrayType type = (GenericArrayType) SerializableTypeWrapper.forField(Fields.class.getField("genericArrayType")); + assertThat(type.toString()).isEqualTo("java.util.List[]"); + assertSerializable(type); + assertSerializable(type.getGenericComponentType()); + } + + @Test + void parameterizedType() throws Exception { + ParameterizedType type = (ParameterizedType) SerializableTypeWrapper.forField(Fields.class.getField("parameterizedType")); + assertThat(type.toString()).isEqualTo("java.util.List"); + assertSerializable(type); + assertSerializable(type.getOwnerType()); + assertSerializable(type.getRawType()); + assertSerializable(type.getActualTypeArguments()); + assertSerializable(type.getActualTypeArguments()[0]); + } + + @Test + void typeVariableType() throws Exception { + TypeVariable type = (TypeVariable) SerializableTypeWrapper.forField(Fields.class.getField("typeVariableType")); + assertThat(type.toString()).isEqualTo("T"); + assertSerializable(type); + assertSerializable(type.getBounds()); + } + + @Test + void wildcardType() throws Exception { + ParameterizedType typeSource = (ParameterizedType) SerializableTypeWrapper.forField(Fields.class.getField("wildcardType")); + WildcardType type = (WildcardType) typeSource.getActualTypeArguments()[0]; + assertThat(type.toString()).isEqualTo("? extends java.lang.CharSequence"); + assertSerializable(type); + assertSerializable(type.getLowerBounds()); + assertSerializable(type.getUpperBounds()); + } + + + private void assertSerializable(Object source) throws Exception { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(bos); + oos.writeObject(source); + oos.close(); + ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray())); + assertThat(ois.readObject()).isEqualTo(source); + } + + + static class Fields { + + public String classType; + + public List[] genericArrayType; + + public List parameterizedType; + + public T typeVariableType; + + public List wildcardType; + } + + + interface Methods { + + List method(Class p1, T p2); + } + + + static class Constructors { + + public Constructors(List p) { + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/SimpleAliasRegistryTests.java b/spring-core/src/test/java/org/springframework/core/SimpleAliasRegistryTests.java new file mode 100644 index 0000000..5e67912 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/SimpleAliasRegistryTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + */ +class SimpleAliasRegistryTests { + + @Test + void aliasChaining() { + SimpleAliasRegistry registry = new SimpleAliasRegistry(); + registry.registerAlias("test", "testAlias"); + registry.registerAlias("testAlias", "testAlias2"); + registry.registerAlias("testAlias2", "testAlias3"); + + assertThat(registry.hasAlias("test", "testAlias")).isTrue(); + assertThat(registry.hasAlias("test", "testAlias2")).isTrue(); + assertThat(registry.hasAlias("test", "testAlias3")).isTrue(); + assertThat(registry.canonicalName("testAlias")).isEqualTo("test"); + assertThat(registry.canonicalName("testAlias2")).isEqualTo("test"); + assertThat(registry.canonicalName("testAlias3")).isEqualTo("test"); + } + + @Test // SPR-17191 + void aliasChainingWithMultipleAliases() { + SimpleAliasRegistry registry = new SimpleAliasRegistry(); + registry.registerAlias("name", "alias_a"); + registry.registerAlias("name", "alias_b"); + assertThat(registry.hasAlias("name", "alias_a")).isTrue(); + assertThat(registry.hasAlias("name", "alias_b")).isTrue(); + + registry.registerAlias("real_name", "name"); + assertThat(registry.hasAlias("real_name", "name")).isTrue(); + assertThat(registry.hasAlias("real_name", "alias_a")).isTrue(); + assertThat(registry.hasAlias("real_name", "alias_b")).isTrue(); + + registry.registerAlias("name", "alias_c"); + assertThat(registry.hasAlias("real_name", "name")).isTrue(); + assertThat(registry.hasAlias("real_name", "alias_a")).isTrue(); + assertThat(registry.hasAlias("real_name", "alias_b")).isTrue(); + assertThat(registry.hasAlias("real_name", "alias_c")).isTrue(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/SortedPropertiesTests.java b/spring-core/src/test/java/org/springframework/core/SortedPropertiesTests.java new file mode 100644 index 0000000..491be2a --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/SortedPropertiesTests.java @@ -0,0 +1,216 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Properties; + +import org.junit.jupiter.api.Test; + +import static java.util.Arrays.stream; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +/** + * Unit tests for {@link SortedProperties}. + * + * @author Sam Brannen + * @since 5.2 + */ +class SortedPropertiesTests { + + @Test + void keys() { + assertKeys(createSortedProps()); + } + + @Test + void keysFromPrototype() { + assertKeys(createSortedPropsFromPrototype()); + } + + @Test + void keySet() { + assertKeySet(createSortedProps()); + } + + @Test + void keySetFromPrototype() { + assertKeySet(createSortedPropsFromPrototype()); + } + + @Test + void entrySet() { + assertEntrySet(createSortedProps()); + } + + @Test + void entrySetFromPrototype() { + assertEntrySet(createSortedPropsFromPrototype()); + } + + @Test + void sortsPropertiesUsingOutputStream() throws IOException { + SortedProperties sortedProperties = createSortedProps(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + sortedProperties.store(baos, "custom comment"); + + String[] lines = lines(baos); + assertThat(lines).hasSize(7); + assertThat(lines[0]).isEqualTo("#custom comment"); + assertThat(lines[1]).as("timestamp").startsWith("#"); + + assertPropsAreSorted(lines); + } + + @Test + void sortsPropertiesUsingWriter() throws IOException { + SortedProperties sortedProperties = createSortedProps(); + + StringWriter writer = new StringWriter(); + sortedProperties.store(writer, "custom comment"); + + String[] lines = lines(writer); + assertThat(lines).hasSize(7); + assertThat(lines[0]).isEqualTo("#custom comment"); + assertThat(lines[1]).as("timestamp").startsWith("#"); + + assertPropsAreSorted(lines); + } + + @Test + void sortsPropertiesAndOmitsCommentsUsingOutputStream() throws IOException { + SortedProperties sortedProperties = createSortedProps(true); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + sortedProperties.store(baos, "custom comment"); + + String[] lines = lines(baos); + assertThat(lines).hasSize(5); + + assertPropsAreSorted(lines); + } + + @Test + void sortsPropertiesAndOmitsCommentsUsingWriter() throws IOException { + SortedProperties sortedProperties = createSortedProps(true); + + StringWriter writer = new StringWriter(); + sortedProperties.store(writer, "custom comment"); + + String[] lines = lines(writer); + assertThat(lines).hasSize(5); + + assertPropsAreSorted(lines); + } + + @Test + void storingAsXmlSortsPropertiesAndOmitsComments() throws IOException { + SortedProperties sortedProperties = createSortedProps(true); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + sortedProperties.storeToXML(baos, "custom comment"); + + String[] lines = lines(baos); + + assertThat(lines).isNotEmpty(); + // Leniently match first line due to differences between JDK 8 and JDK 9+. + String regex = "<\\?xml .*\\?>"; + assertThat(lines[0]).matches(regex); + assertThat(lines).filteredOn(line -> !line.matches(regex)).containsExactly( // + "", // + "", // + "blue", // + "sweet", // + "apple", // + "medium", // + "car", // + "" // + ); + } + + private SortedProperties createSortedProps() { + return createSortedProps(false); + } + + private SortedProperties createSortedProps(boolean omitComments) { + SortedProperties sortedProperties = new SortedProperties(omitComments); + populateProperties(sortedProperties); + return sortedProperties; + } + + private SortedProperties createSortedPropsFromPrototype() { + Properties properties = new Properties(); + populateProperties(properties); + return new SortedProperties(properties, false); + } + + private void populateProperties(Properties properties) { + properties.setProperty("color", "blue"); + properties.setProperty("fragrance", "sweet"); + properties.setProperty("fruit", "apple"); + properties.setProperty("size", "medium"); + properties.setProperty("vehicle", "car"); + } + + private String[] lines(ByteArrayOutputStream baos) { + return lines(new String(baos.toByteArray(), StandardCharsets.ISO_8859_1)); + } + + private String[] lines(StringWriter writer) { + return lines(writer.toString()); + } + + private String[] lines(String input) { + return input.trim().split(SortedProperties.EOL); + } + + private void assertKeys(Properties properties) { + assertThat(Collections.list(properties.keys())) // + .containsExactly("color", "fragrance", "fruit", "size", "vehicle"); + } + + private void assertKeySet(Properties properties) { + assertThat(properties.keySet()).containsExactly("color", "fragrance", "fruit", "size", "vehicle"); + } + + private void assertEntrySet(Properties properties) { + assertThat(properties.entrySet()).containsExactly( // + entry("color", "blue"), // + entry("fragrance", "sweet"), // + entry("fruit", "apple"), // + entry("size", "medium"), // + entry("vehicle", "car") // + ); + } + + private void assertPropsAreSorted(String[] lines) { + assertThat(stream(lines).filter(s -> !s.startsWith("#"))).containsExactly( // + "color=blue", // + "fragrance=sweet", // + "fruit=apple", // + "size=medium", // + "vehicle=car"// + ); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/SpringCoreBlockHoundIntegrationTests.java b/spring-core/src/test/java/org/springframework/core/SpringCoreBlockHoundIntegrationTests.java new file mode 100644 index 0000000..f2245ef --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/SpringCoreBlockHoundIntegrationTests.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledForJreRange; +import reactor.blockhound.BlockHound; +import reactor.core.scheduler.ReactorBlockHoundIntegration; +import reactor.core.scheduler.Schedulers; + +import org.springframework.tests.sample.objects.TestObject; +import org.springframework.util.ConcurrentReferenceHashMap; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.condition.JRE.JAVA_14; + +/** + * Tests to verify the spring-core BlockHound integration rules. + * + * @author Rossen Stoyanchev + * @since 5.2.4 + */ +@DisabledForJreRange(min = JAVA_14) +public class SpringCoreBlockHoundIntegrationTests { + + + @BeforeAll + static void setUp() { + BlockHound.builder() + .with(new ReactorBlockHoundIntegration()) // Reactor non-blocking thread predicate + .with(new ReactiveAdapterRegistry.SpringCoreBlockHoundIntegration()) + .install(); + } + + + @Test + void blockHoundIsInstalled() { + assertThatThrownBy(() -> testNonBlockingTask(() -> Thread.sleep(10))) + .hasMessageContaining("Blocking call!"); + } + + @Test + void localVariableTableParameterNameDiscoverer() { + testNonBlockingTask(() -> { + Method setName = TestObject.class.getMethod("setName", String.class); + String[] names = new LocalVariableTableParameterNameDiscoverer().getParameterNames(setName); + assertThat(names).isEqualTo(new String[] {"name"}); + }); + } + + @Test + void concurrentReferenceHashMap() { + int size = 10000; + Map map = new ConcurrentReferenceHashMap<>(size); + + CompletableFuture future1 = new CompletableFuture<>(); + testNonBlockingTask(() -> { + for (int i = 0; i < size / 2; i++) { + map.put("a" + i, "bar"); + } + }, future1); + + CompletableFuture future2 = new CompletableFuture<>(); + testNonBlockingTask(() -> { + for (int i = 0; i < size / 2; i++) { + map.put("b" + i, "bar"); + } + }, future2); + + CompletableFuture.allOf(future1, future2).join(); + assertThat(map).hasSize(size); + } + + private void testNonBlockingTask(NonBlockingTask task) { + CompletableFuture future = new CompletableFuture<>(); + testNonBlockingTask(task, future); + future.join(); + } + + private void testNonBlockingTask(NonBlockingTask task, CompletableFuture future) { + Schedulers.parallel().schedule(() -> { + try { + task.run(); + future.complete(null); + } + catch (Throwable ex) { + future.completeExceptionally(ex); + } + }); + } + + + @FunctionalInterface + private interface NonBlockingTask { + + void run() throws Exception; + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/StandardReflectionParameterNameDiscoverTests.java b/spring-core/src/test/java/org/springframework/core/StandardReflectionParameterNameDiscoverTests.java new file mode 100644 index 0000000..8f51f22 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/StandardReflectionParameterNameDiscoverTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for StandardReflectionParameterNameDiscoverer + * + * @author Rob Winch + */ +class StandardReflectionParameterNameDiscoverTests { + + private ParameterNameDiscoverer parameterNameDiscoverer; + + @BeforeEach + void setup() { + parameterNameDiscoverer = new StandardReflectionParameterNameDiscoverer(); + } + + @Test + void getParameterNamesOnInterface() { + Method method = ReflectionUtils.findMethod(MessageService.class,"sendMessage", String.class); + String[] actualParams = parameterNameDiscoverer.getParameterNames(method); + assertThat(actualParams).isEqualTo(new String[]{"message"}); + } + + public interface MessageService { + void sendMessage(String message); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java new file mode 100644 index 0000000..2957f31 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java @@ -0,0 +1,1553 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.Date; +import java.util.List; +import java.util.Set; + +import javax.annotation.Nonnull; +import javax.annotation.ParametersAreNonnullByDefault; +import javax.annotation.Resource; +import javax.annotation.meta.When; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.core.annotation.AnnotationUtilsTests.ExtendsBaseClassWithGenericAnnotatedMethod; +import org.springframework.core.annotation.AnnotationUtilsTests.ImplementsInterfaceWithGenericAnnotatedMethod; +import org.springframework.core.annotation.AnnotationUtilsTests.WebController; +import org.springframework.core.annotation.AnnotationUtilsTests.WebMapping; +import org.springframework.core.testfixture.stereotype.Component; +import org.springframework.core.testfixture.stereotype.Indexed; +import org.springframework.lang.NonNullApi; +import org.springframework.lang.Nullable; +import org.springframework.util.MultiValueMap; + +import static java.util.Arrays.asList; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.core.annotation.AnnotatedElementUtils.findAllMergedAnnotations; +import static org.springframework.core.annotation.AnnotatedElementUtils.findMergedAnnotation; +import static org.springframework.core.annotation.AnnotatedElementUtils.getAllAnnotationAttributes; +import static org.springframework.core.annotation.AnnotatedElementUtils.getAllMergedAnnotations; +import static org.springframework.core.annotation.AnnotatedElementUtils.getMergedAnnotation; +import static org.springframework.core.annotation.AnnotatedElementUtils.getMergedAnnotationAttributes; +import static org.springframework.core.annotation.AnnotatedElementUtils.getMetaAnnotationTypes; +import static org.springframework.core.annotation.AnnotatedElementUtils.hasAnnotation; +import static org.springframework.core.annotation.AnnotatedElementUtils.hasMetaAnnotationTypes; +import static org.springframework.core.annotation.AnnotatedElementUtils.isAnnotated; +import static org.springframework.core.annotation.AnnotationUtilsTests.asArray; + +/** + * Unit tests for {@link AnnotatedElementUtils}. + * + * @author Sam Brannen + * @author Rossen Stoyanchev + * @author Juergen Hoeller + * @since 4.0.3 + * @see AnnotationUtilsTests + * @see MultipleComposedAnnotationsOnSingleAnnotatedElementTests + * @see ComposedRepeatableAnnotationsTests + */ +class AnnotatedElementUtilsTests { + + private static final String TX_NAME = Transactional.class.getName(); + + + @Test + void getMetaAnnotationTypesOnNonAnnotatedClass() { + assertThat(getMetaAnnotationTypes(NonAnnotatedClass.class, TransactionalComponent.class).isEmpty()).isTrue(); + assertThat(getMetaAnnotationTypes(NonAnnotatedClass.class, TransactionalComponent.class.getName()).isEmpty()).isTrue(); + } + + @Test + void getMetaAnnotationTypesOnClassWithMetaDepth1() { + Set names = getMetaAnnotationTypes(TransactionalComponentClass.class, TransactionalComponent.class); + assertThat(names).isEqualTo(names(Transactional.class, Component.class, Indexed.class)); + + names = getMetaAnnotationTypes(TransactionalComponentClass.class, TransactionalComponent.class.getName()); + assertThat(names).isEqualTo(names(Transactional.class, Component.class, Indexed.class)); + } + + @Test + void getMetaAnnotationTypesOnClassWithMetaDepth2() { + Set names = getMetaAnnotationTypes(ComposedTransactionalComponentClass.class, ComposedTransactionalComponent.class); + assertThat(names).isEqualTo(names(TransactionalComponent.class, Transactional.class, Component.class, Indexed.class)); + + names = getMetaAnnotationTypes(ComposedTransactionalComponentClass.class, ComposedTransactionalComponent.class.getName()); + assertThat(names).isEqualTo(names(TransactionalComponent.class, Transactional.class, Component.class, Indexed.class)); + } + + private Set names(Class... classes) { + return stream(classes).map(Class::getName).collect(toSet()); + } + + @Test + void hasMetaAnnotationTypesOnNonAnnotatedClass() { + assertThat(hasMetaAnnotationTypes(NonAnnotatedClass.class, TX_NAME)).isFalse(); + } + + @Test + void hasMetaAnnotationTypesOnClassWithMetaDepth0() { + assertThat(hasMetaAnnotationTypes(TransactionalComponentClass.class, TransactionalComponent.class.getName())).isFalse(); + } + + @Test + void hasMetaAnnotationTypesOnClassWithMetaDepth1() { + assertThat(hasMetaAnnotationTypes(TransactionalComponentClass.class, TX_NAME)).isTrue(); + assertThat(hasMetaAnnotationTypes(TransactionalComponentClass.class, Component.class.getName())).isTrue(); + } + + @Test + void hasMetaAnnotationTypesOnClassWithMetaDepth2() { + assertThat(hasMetaAnnotationTypes(ComposedTransactionalComponentClass.class, TX_NAME)).isTrue(); + assertThat(hasMetaAnnotationTypes(ComposedTransactionalComponentClass.class, Component.class.getName())).isTrue(); + assertThat(hasMetaAnnotationTypes(ComposedTransactionalComponentClass.class, ComposedTransactionalComponent.class.getName())).isFalse(); + } + + @Test + void isAnnotatedOnNonAnnotatedClass() { + assertThat(isAnnotated(NonAnnotatedClass.class, Transactional.class)).isFalse(); + } + + @Test + void isAnnotatedOnClassWithMetaDepth() { + assertThat(isAnnotated(TransactionalComponentClass.class, TransactionalComponent.class)).isTrue(); + assertThat(isAnnotated(SubTransactionalComponentClass.class, TransactionalComponent.class)).as("isAnnotated() does not search the class hierarchy.").isFalse(); + assertThat(isAnnotated(TransactionalComponentClass.class, Transactional.class)).isTrue(); + assertThat(isAnnotated(TransactionalComponentClass.class, Component.class)).isTrue(); + assertThat(isAnnotated(ComposedTransactionalComponentClass.class, Transactional.class)).isTrue(); + assertThat(isAnnotated(ComposedTransactionalComponentClass.class, Component.class)).isTrue(); + assertThat(isAnnotated(ComposedTransactionalComponentClass.class, ComposedTransactionalComponent.class)).isTrue(); + } + + @Test + void isAnnotatedForPlainTypes() { + assertThat(isAnnotated(Order.class, Documented.class)).isTrue(); + assertThat(isAnnotated(NonNullApi.class, Documented.class)).isTrue(); + assertThat(isAnnotated(NonNullApi.class, Nonnull.class)).isTrue(); + assertThat(isAnnotated(ParametersAreNonnullByDefault.class, Nonnull.class)).isTrue(); + } + + @Test + void isAnnotatedWithNameOnNonAnnotatedClass() { + assertThat(isAnnotated(NonAnnotatedClass.class, TX_NAME)).isFalse(); + } + + @Test + void isAnnotatedWithNameOnClassWithMetaDepth() { + assertThat(isAnnotated(TransactionalComponentClass.class, TransactionalComponent.class.getName())).isTrue(); + assertThat(isAnnotated(SubTransactionalComponentClass.class, TransactionalComponent.class.getName())).as("isAnnotated() does not search the class hierarchy.").isFalse(); + assertThat(isAnnotated(TransactionalComponentClass.class, TX_NAME)).isTrue(); + assertThat(isAnnotated(TransactionalComponentClass.class, Component.class.getName())).isTrue(); + assertThat(isAnnotated(ComposedTransactionalComponentClass.class, TX_NAME)).isTrue(); + assertThat(isAnnotated(ComposedTransactionalComponentClass.class, Component.class.getName())).isTrue(); + assertThat(isAnnotated(ComposedTransactionalComponentClass.class, ComposedTransactionalComponent.class.getName())).isTrue(); + } + + @Test + void hasAnnotationOnNonAnnotatedClass() { + assertThat(hasAnnotation(NonAnnotatedClass.class, Transactional.class)).isFalse(); + } + + @Test + void hasAnnotationOnClassWithMetaDepth() { + assertThat(hasAnnotation(TransactionalComponentClass.class, TransactionalComponent.class)).isTrue(); + assertThat(hasAnnotation(SubTransactionalComponentClass.class, TransactionalComponent.class)).isTrue(); + assertThat(hasAnnotation(TransactionalComponentClass.class, Transactional.class)).isTrue(); + assertThat(hasAnnotation(TransactionalComponentClass.class, Component.class)).isTrue(); + assertThat(hasAnnotation(ComposedTransactionalComponentClass.class, Transactional.class)).isTrue(); + assertThat(hasAnnotation(ComposedTransactionalComponentClass.class, Component.class)).isTrue(); + assertThat(hasAnnotation(ComposedTransactionalComponentClass.class, ComposedTransactionalComponent.class)).isTrue(); + } + + @Test + void hasAnnotationForPlainTypes() { + assertThat(hasAnnotation(Order.class, Documented.class)).isTrue(); + assertThat(hasAnnotation(NonNullApi.class, Documented.class)).isTrue(); + assertThat(hasAnnotation(NonNullApi.class, Nonnull.class)).isTrue(); + assertThat(hasAnnotation(ParametersAreNonnullByDefault.class, Nonnull.class)).isTrue(); + } + + @Test + void getAllAnnotationAttributesOnNonAnnotatedClass() { + assertThat(getAllAnnotationAttributes(NonAnnotatedClass.class, TX_NAME)).isNull(); + } + + @Test + void getAllAnnotationAttributesOnClassWithLocalAnnotation() { + MultiValueMap attributes = getAllAnnotationAttributes(TxConfig.class, TX_NAME); + assertThat(attributes).as("Annotation attributes map for @Transactional on TxConfig").isNotNull(); + assertThat(attributes.get("value")).as("value for TxConfig").isEqualTo(asList("TxConfig")); + } + + @Test + void getAllAnnotationAttributesOnClassWithLocalComposedAnnotationAndInheritedAnnotation() { + MultiValueMap attributes = getAllAnnotationAttributes(SubClassWithInheritedAnnotation.class, TX_NAME); + assertThat(attributes).as("Annotation attributes map for @Transactional on SubClassWithInheritedAnnotation").isNotNull(); + assertThat(attributes.get("qualifier")).isEqualTo(asList("composed2", "transactionManager")); + } + + @Test + void getAllAnnotationAttributesFavorsInheritedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() { + MultiValueMap attributes = getAllAnnotationAttributes(SubSubClassWithInheritedAnnotation.class, TX_NAME); + assertThat(attributes).as("Annotation attributes map for @Transactional on SubSubClassWithInheritedAnnotation").isNotNull(); + assertThat(attributes.get("qualifier")).isEqualTo(asList("transactionManager")); + } + + @Test + void getAllAnnotationAttributesFavorsInheritedComposedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() { + MultiValueMap attributes = getAllAnnotationAttributes( SubSubClassWithInheritedComposedAnnotation.class, TX_NAME); + assertThat(attributes).as("Annotation attributes map for @Transactional on SubSubClassWithInheritedComposedAnnotation").isNotNull(); + assertThat(attributes.get("qualifier")).isEqualTo(asList("composed1")); + } + + /** + * If the "value" entry contains both "DerivedTxConfig" AND "TxConfig", then + * the algorithm is accidentally picking up shadowed annotations of the same + * type within the class hierarchy. Such undesirable behavior would cause the + * logic in {@code org.springframework.context.annotation.ProfileCondition} + * to fail. + */ + @Test + void getAllAnnotationAttributesOnClassWithLocalAnnotationThatShadowsAnnotationFromSuperclass() { + // See org.springframework.core.env.EnvironmentSystemIntegrationTests#mostSpecificDerivedClassDrivesEnvironment_withDevEnvAndDerivedDevConfigClass + MultiValueMap attributes = getAllAnnotationAttributes(DerivedTxConfig.class, TX_NAME); + assertThat(attributes).as("Annotation attributes map for @Transactional on DerivedTxConfig").isNotNull(); + assertThat(attributes.get("value")).as("value for DerivedTxConfig").isEqualTo(asList("DerivedTxConfig")); + } + + /** + * Note: this functionality is required by {@code org.springframework.context.annotation.ProfileCondition}. + */ + @Test + void getAllAnnotationAttributesOnClassWithMultipleComposedAnnotations() { + // See org.springframework.core.env.EnvironmentSystemIntegrationTests + MultiValueMap attributes = getAllAnnotationAttributes(TxFromMultipleComposedAnnotations.class, TX_NAME); + assertThat(attributes).as("Annotation attributes map for @Transactional on TxFromMultipleComposedAnnotations").isNotNull(); + assertThat(attributes.get("value")).as("value for TxFromMultipleComposedAnnotations.").isEqualTo(asList("TxInheritedComposed", "TxComposed")); + } + + @Test + void getAllAnnotationAttributesOnLangType() { + MultiValueMap attributes = getAllAnnotationAttributes( + NonNullApi.class, Nonnull.class.getName()); + assertThat(attributes).as("Annotation attributes map for @Nonnull on NonNullApi").isNotNull(); + assertThat(attributes.get("when")).as("value for NonNullApi").isEqualTo(asList(When.ALWAYS)); + } + + @Test + void getAllAnnotationAttributesOnJavaxType() { + MultiValueMap attributes = getAllAnnotationAttributes( + ParametersAreNonnullByDefault.class, Nonnull.class.getName()); + assertThat(attributes).as("Annotation attributes map for @Nonnull on NonNullApi").isNotNull(); + assertThat(attributes.get("when")).as("value for NonNullApi").isEqualTo(asList(When.ALWAYS)); + } + + @Test + void getMergedAnnotationAttributesOnClassWithLocalAnnotation() { + Class element = TxConfig.class; + String name = TX_NAME; + AnnotationAttributes attributes = getMergedAnnotationAttributes(element, name); + assertThat(attributes).as("Annotation attributes for @Transactional on TxConfig").isNotNull(); + assertThat(attributes.getString("value")).as("value for TxConfig").isEqualTo("TxConfig"); + // Verify contracts between utility methods: + assertThat(isAnnotated(element, name)).isTrue(); + } + + @Test + void getMergedAnnotationAttributesOnClassWithLocalAnnotationThatShadowsAnnotationFromSuperclass() { + Class element = DerivedTxConfig.class; + String name = TX_NAME; + AnnotationAttributes attributes = getMergedAnnotationAttributes(element, name); + assertThat(attributes).as("Annotation attributes for @Transactional on DerivedTxConfig").isNotNull(); + assertThat(attributes.getString("value")).as("value for DerivedTxConfig").isEqualTo("DerivedTxConfig"); + // Verify contracts between utility methods: + assertThat(isAnnotated(element, name)).isTrue(); + } + + @Test + void getMergedAnnotationAttributesOnMetaCycleAnnotatedClassWithMissingTargetMetaAnnotation() { + AnnotationAttributes attributes = getMergedAnnotationAttributes(MetaCycleAnnotatedClass.class, TX_NAME); + assertThat(attributes).as("Should not find annotation attributes for @Transactional on MetaCycleAnnotatedClass").isNull(); + } + + @Test + void getMergedAnnotationAttributesFavorsLocalComposedAnnotationOverInheritedAnnotation() { + Class element = SubClassWithInheritedAnnotation.class; + String name = TX_NAME; + AnnotationAttributes attributes = getMergedAnnotationAttributes(element, name); + assertThat(attributes).as("AnnotationAttributes for @Transactional on SubClassWithInheritedAnnotation").isNotNull(); + // Verify contracts between utility methods: + assertThat(isAnnotated(element, name)).isTrue(); + assertThat(attributes.getBoolean("readOnly")).as("readOnly flag for SubClassWithInheritedAnnotation.").isTrue(); + } + + @Test + void getMergedAnnotationAttributesFavorsInheritedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() { + Class element = SubSubClassWithInheritedAnnotation.class; + String name = TX_NAME; + AnnotationAttributes attributes = getMergedAnnotationAttributes(element, name); + assertThat(attributes).as("AnnotationAttributes for @Transactional on SubSubClassWithInheritedAnnotation").isNotNull(); + // Verify contracts between utility methods: + assertThat(isAnnotated(element, name)).isTrue(); + assertThat(attributes.getBoolean("readOnly")).as("readOnly flag for SubSubClassWithInheritedAnnotation.").isFalse(); + } + + @Test + void getMergedAnnotationAttributesFavorsInheritedComposedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() { + Class element = SubSubClassWithInheritedComposedAnnotation.class; + String name = TX_NAME; + AnnotationAttributes attributes = getMergedAnnotationAttributes(element, name); + assertThat(attributes).as("AnnotationAttributes for @Transactional on SubSubClassWithInheritedComposedAnnotation.").isNotNull(); + // Verify contracts between utility methods: + assertThat(isAnnotated(element, name)).isTrue(); + assertThat(attributes.getBoolean("readOnly")).as("readOnly flag for SubSubClassWithInheritedComposedAnnotation.").isFalse(); + } + + @Test + void getMergedAnnotationAttributesFromInterfaceImplementedBySuperclass() { + Class element = ConcreteClassWithInheritedAnnotation.class; + String name = TX_NAME; + AnnotationAttributes attributes = getMergedAnnotationAttributes(element, name); + assertThat(attributes).as("Should not find @Transactional on ConcreteClassWithInheritedAnnotation").isNull(); + // Verify contracts between utility methods: + assertThat(isAnnotated(element, name)).isFalse(); + } + + @Test + void getMergedAnnotationAttributesOnInheritedAnnotationInterface() { + Class element = InheritedAnnotationInterface.class; + String name = TX_NAME; + AnnotationAttributes attributes = getMergedAnnotationAttributes(element, name); + assertThat(attributes).as("Should find @Transactional on InheritedAnnotationInterface").isNotNull(); + // Verify contracts between utility methods: + assertThat(isAnnotated(element, name)).isTrue(); + } + + @Test + void getMergedAnnotationAttributesOnNonInheritedAnnotationInterface() { + Class element = NonInheritedAnnotationInterface.class; + String name = Order.class.getName(); + AnnotationAttributes attributes = getMergedAnnotationAttributes(element, name); + assertThat(attributes).as("Should find @Order on NonInheritedAnnotationInterface").isNotNull(); + // Verify contracts between utility methods: + assertThat(isAnnotated(element, name)).isTrue(); + } + + @Test + void getMergedAnnotationAttributesWithConventionBasedComposedAnnotation() { + Class element = ConventionBasedComposedContextConfigClass.class; + String name = ContextConfig.class.getName(); + AnnotationAttributes attributes = getMergedAnnotationAttributes(element, name); + + assertThat(attributes).as("Should find @ContextConfig on " + element.getSimpleName()).isNotNull(); + assertThat(attributes.getStringArray("locations")).as("locations").isEqualTo(asArray("explicitDeclaration")); + assertThat(attributes.getStringArray("value")).as("value").isEqualTo(asArray("explicitDeclaration")); + + // Verify contracts between utility methods: + assertThat(isAnnotated(element, name)).isTrue(); + } + + /** + * This test should never pass, simply because Spring does not support a hybrid + * approach for annotation attribute overrides with transitive implicit aliases. + * See SPR-13554 for details. + *

    Furthermore, if you choose to execute this test, it can fail for either + * the first test class or the second one (with different exceptions), depending + * on the order in which the JVM returns the attribute methods via reflection. + */ + @Disabled("Permanently disabled but left in place for illustrative purposes") + @Test + void getMergedAnnotationAttributesWithHalfConventionBasedAndHalfAliasedComposedAnnotation() { + for (Class clazz : asList(HalfConventionBasedAndHalfAliasedComposedContextConfigClassV1.class, + HalfConventionBasedAndHalfAliasedComposedContextConfigClassV2.class)) { + getMergedAnnotationAttributesWithHalfConventionBasedAndHalfAliasedComposedAnnotation(clazz); + } + } + + private void getMergedAnnotationAttributesWithHalfConventionBasedAndHalfAliasedComposedAnnotation(Class clazz) { + String[] expected = asArray("explicitDeclaration"); + String name = ContextConfig.class.getName(); + String simpleName = clazz.getSimpleName(); + AnnotationAttributes attributes = getMergedAnnotationAttributes(clazz, name); + + assertThat(attributes).as("Should find @ContextConfig on " + simpleName).isNotNull(); + assertThat(attributes.getStringArray("locations")).as("locations for class [" + clazz.getSimpleName() + "]").isEqualTo(expected); + assertThat(attributes.getStringArray("value")).as("value for class [" + clazz.getSimpleName() + "]").isEqualTo(expected); + + // Verify contracts between utility methods: + assertThat(isAnnotated(clazz, name)).isTrue(); + } + + @Test + void getMergedAnnotationAttributesWithAliasedComposedAnnotation() { + Class element = AliasedComposedContextConfigClass.class; + String name = ContextConfig.class.getName(); + AnnotationAttributes attributes = getMergedAnnotationAttributes(element, name); + + assertThat(attributes).as("Should find @ContextConfig on " + element.getSimpleName()).isNotNull(); + assertThat(attributes.getStringArray("value")).as("value").isEqualTo(asArray("test.xml")); + assertThat(attributes.getStringArray("locations")).as("locations").isEqualTo(asArray("test.xml")); + + // Verify contracts between utility methods: + assertThat(isAnnotated(element, name)).isTrue(); + } + + @Test + void getMergedAnnotationAttributesWithAliasedValueComposedAnnotation() { + Class element = AliasedValueComposedContextConfigClass.class; + String name = ContextConfig.class.getName(); + AnnotationAttributes attributes = getMergedAnnotationAttributes(element, name); + + assertThat(attributes).as("Should find @ContextConfig on " + element.getSimpleName()).isNotNull(); + assertThat(attributes.getStringArray("locations")).as("locations").isEqualTo(asArray("test.xml")); + assertThat(attributes.getStringArray("value")).as("value").isEqualTo(asArray("test.xml")); + + // Verify contracts between utility methods: + assertThat(isAnnotated(element, name)).isTrue(); + } + + @Test + void getMergedAnnotationAttributesWithImplicitAliasesInMetaAnnotationOnComposedAnnotation() { + Class element = ComposedImplicitAliasesContextConfigClass.class; + String name = ImplicitAliasesContextConfig.class.getName(); + AnnotationAttributes attributes = getMergedAnnotationAttributes(element, name); + String[] expected = asArray("A.xml", "B.xml"); + + assertThat(attributes).as("Should find @ImplicitAliasesContextConfig on " + element.getSimpleName()).isNotNull(); + assertThat(attributes.getStringArray("groovyScripts")).as("groovyScripts").isEqualTo(expected); + assertThat(attributes.getStringArray("xmlFiles")).as("xmlFiles").isEqualTo(expected); + assertThat(attributes.getStringArray("locations")).as("locations").isEqualTo(expected); + assertThat(attributes.getStringArray("value")).as("value").isEqualTo(expected); + + // Verify contracts between utility methods: + assertThat(isAnnotated(element, name)).isTrue(); + } + + @Test + void getMergedAnnotationWithAliasedValueComposedAnnotation() { + assertGetMergedAnnotation(AliasedValueComposedContextConfigClass.class, "test.xml"); + } + + @Test + void getMergedAnnotationWithImplicitAliasesForSameAttributeInComposedAnnotation() { + assertGetMergedAnnotation(ImplicitAliasesContextConfigClass1.class, "foo.xml"); + assertGetMergedAnnotation(ImplicitAliasesContextConfigClass2.class, "bar.xml"); + assertGetMergedAnnotation(ImplicitAliasesContextConfigClass3.class, "baz.xml"); + } + + @Test + void getMergedAnnotationWithTransitiveImplicitAliases() { + assertGetMergedAnnotation(TransitiveImplicitAliasesContextConfigClass.class, "test.groovy"); + } + + @Test + void getMergedAnnotationWithTransitiveImplicitAliasesWithSingleElementOverridingAnArrayViaAliasFor() { + assertGetMergedAnnotation(SingleLocationTransitiveImplicitAliasesContextConfigClass.class, "test.groovy"); + } + + @Test + void getMergedAnnotationWithTransitiveImplicitAliasesWithSkippedLevel() { + assertGetMergedAnnotation(TransitiveImplicitAliasesWithSkippedLevelContextConfigClass.class, "test.xml"); + } + + @Test + void getMergedAnnotationWithTransitiveImplicitAliasesWithSkippedLevelWithSingleElementOverridingAnArrayViaAliasFor() { + assertGetMergedAnnotation(SingleLocationTransitiveImplicitAliasesWithSkippedLevelContextConfigClass.class, "test.xml"); + } + + private void assertGetMergedAnnotation(Class element, String... expected) { + String name = ContextConfig.class.getName(); + ContextConfig contextConfig = getMergedAnnotation(element, ContextConfig.class); + + assertThat(contextConfig).as("Should find @ContextConfig on " + element.getSimpleName()).isNotNull(); + assertThat(contextConfig.locations()).as("locations").isEqualTo(expected); + assertThat(contextConfig.value()).as("value").isEqualTo(expected); + Object[] expecteds = new Class[0]; + assertThat(contextConfig.classes()).as("classes").isEqualTo(expecteds); + + // Verify contracts between utility methods: + assertThat(isAnnotated(element, name)).isTrue(); + } + + @Test + void getMergedAnnotationWithImplicitAliasesInMetaAnnotationOnComposedAnnotation() { + Class element = ComposedImplicitAliasesContextConfigClass.class; + String name = ImplicitAliasesContextConfig.class.getName(); + ImplicitAliasesContextConfig config = getMergedAnnotation(element, ImplicitAliasesContextConfig.class); + String[] expected = asArray("A.xml", "B.xml"); + + assertThat(config).as("Should find @ImplicitAliasesContextConfig on " + element.getSimpleName()).isNotNull(); + assertThat(config.groovyScripts()).as("groovyScripts").isEqualTo(expected); + assertThat(config.xmlFiles()).as("xmlFiles").isEqualTo(expected); + assertThat(config.locations()).as("locations").isEqualTo(expected); + assertThat(config.value()).as("value").isEqualTo(expected); + + // Verify contracts between utility methods: + assertThat(isAnnotated(element, name)).isTrue(); + } + + @Test + void getMergedAnnotationWithImplicitAliasesWithDefaultsInMetaAnnotationOnComposedAnnotation() { + Class element = ImplicitAliasesWithDefaultsClass.class; + String name = AliasesWithDefaults.class.getName(); + AliasesWithDefaults annotation = getMergedAnnotation(element, AliasesWithDefaults.class); + + assertThat(annotation).as("Should find @AliasesWithDefaults on " + element.getSimpleName()).isNotNull(); + assertThat(annotation.a1()).as("a1").isEqualTo("ImplicitAliasesWithDefaults"); + assertThat(annotation.a2()).as("a2").isEqualTo("ImplicitAliasesWithDefaults"); + + // Verify contracts between utility methods: + assertThat(isAnnotated(element, name)).isTrue(); + } + + @Test + void getMergedAnnotationAttributesWithInvalidConventionBasedComposedAnnotation() { + Class element = InvalidConventionBasedComposedContextConfigClass.class; + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + getMergedAnnotationAttributes(element, ContextConfig.class)) + .withMessageContaining("Different @AliasFor mirror values for annotation") + .withMessageContaining("attribute 'locations' and its alias 'value'") + .withMessageContaining("values of [{requiredLocationsDeclaration}] and [{duplicateDeclaration}]"); + } + + @Test + void getMergedAnnotationAttributesWithShadowedAliasComposedAnnotation() { + Class element = ShadowedAliasComposedContextConfigClass.class; + AnnotationAttributes attributes = getMergedAnnotationAttributes(element, ContextConfig.class); + + String[] expected = asArray("test.xml"); + + assertThat(attributes).as("Should find @ContextConfig on " + element.getSimpleName()).isNotNull(); + assertThat(attributes.getStringArray("locations")).as("locations").isEqualTo(expected); + assertThat(attributes.getStringArray("value")).as("value").isEqualTo(expected); + } + + @Test + void findMergedAnnotationAttributesOnInheritedAnnotationInterface() { + AnnotationAttributes attributes = findMergedAnnotationAttributes(InheritedAnnotationInterface.class, Transactional.class); + assertThat(attributes).as("Should find @Transactional on InheritedAnnotationInterface").isNotNull(); + } + + @Test + void findMergedAnnotationAttributesOnSubInheritedAnnotationInterface() { + AnnotationAttributes attributes = findMergedAnnotationAttributes(SubInheritedAnnotationInterface.class, Transactional.class); + assertThat(attributes).as("Should find @Transactional on SubInheritedAnnotationInterface").isNotNull(); + } + + @Test + void findMergedAnnotationAttributesOnSubSubInheritedAnnotationInterface() { + AnnotationAttributes attributes = findMergedAnnotationAttributes(SubSubInheritedAnnotationInterface.class, Transactional.class); + assertThat(attributes).as("Should find @Transactional on SubSubInheritedAnnotationInterface").isNotNull(); + } + + @Test + void findMergedAnnotationAttributesOnNonInheritedAnnotationInterface() { + AnnotationAttributes attributes = findMergedAnnotationAttributes(NonInheritedAnnotationInterface.class, Order.class); + assertThat(attributes).as("Should find @Order on NonInheritedAnnotationInterface").isNotNull(); + } + + @Test + void findMergedAnnotationAttributesOnSubNonInheritedAnnotationInterface() { + AnnotationAttributes attributes = findMergedAnnotationAttributes(SubNonInheritedAnnotationInterface.class, Order.class); + assertThat(attributes).as("Should find @Order on SubNonInheritedAnnotationInterface").isNotNull(); + } + + @Test + void findMergedAnnotationAttributesOnSubSubNonInheritedAnnotationInterface() { + AnnotationAttributes attributes = findMergedAnnotationAttributes(SubSubNonInheritedAnnotationInterface.class, Order.class); + assertThat(attributes).as("Should find @Order on SubSubNonInheritedAnnotationInterface").isNotNull(); + } + + @Test + void findMergedAnnotationAttributesInheritedFromInterfaceMethod() throws NoSuchMethodException { + Method method = ConcreteClassWithInheritedAnnotation.class.getMethod("handleFromInterface"); + AnnotationAttributes attributes = findMergedAnnotationAttributes(method, Order.class); + assertThat(attributes).as("Should find @Order on ConcreteClassWithInheritedAnnotation.handleFromInterface() method").isNotNull(); + } + + @Test + void findMergedAnnotationAttributesInheritedFromAbstractMethod() throws NoSuchMethodException { + Method method = ConcreteClassWithInheritedAnnotation.class.getMethod("handle"); + AnnotationAttributes attributes = findMergedAnnotationAttributes(method, Transactional.class); + assertThat(attributes).as("Should find @Transactional on ConcreteClassWithInheritedAnnotation.handle() method").isNotNull(); + } + + @Test + void findMergedAnnotationAttributesInheritedFromBridgedMethod() throws NoSuchMethodException { + Method method = ConcreteClassWithInheritedAnnotation.class.getMethod("handleParameterized", String.class); + AnnotationAttributes attributes = findMergedAnnotationAttributes(method, Transactional.class); + assertThat(attributes).as("Should find @Transactional on bridged ConcreteClassWithInheritedAnnotation.handleParameterized()").isNotNull(); + } + + /** + * Bridge/bridged method setup code copied from + * {@link org.springframework.core.BridgeMethodResolverTests#withGenericParameter()}. + * @since 4.2 + */ + @Test + void findMergedAnnotationAttributesFromBridgeMethod() { + Method[] methods = StringGenericParameter.class.getMethods(); + Method bridgeMethod = null; + Method bridgedMethod = null; + + for (Method method : methods) { + if ("getFor".equals(method.getName()) && !method.getParameterTypes()[0].equals(Integer.class)) { + if (method.getReturnType().equals(Object.class)) { + bridgeMethod = method; + } + else { + bridgedMethod = method; + } + } + } + assertThat(bridgeMethod != null && bridgeMethod.isBridge()).isTrue(); + boolean condition = bridgedMethod != null && !bridgedMethod.isBridge(); + assertThat(condition).isTrue(); + + AnnotationAttributes attributes = findMergedAnnotationAttributes(bridgeMethod, Order.class); + assertThat(attributes).as("Should find @Order on StringGenericParameter.getFor() bridge method").isNotNull(); + } + + @Test + void findMergedAnnotationAttributesOnClassWithMetaAndLocalTxConfig() { + AnnotationAttributes attributes = findMergedAnnotationAttributes(MetaAndLocalTxConfigClass.class, Transactional.class); + assertThat(attributes).as("Should find @Transactional on MetaAndLocalTxConfigClass").isNotNull(); + assertThat(attributes.getString("qualifier")).as("TX qualifier for MetaAndLocalTxConfigClass.").isEqualTo("localTxMgr"); + } + + @Test + void findAndSynthesizeAnnotationAttributesOnClassWithAttributeAliasesInTargetAnnotation() { + String qualifier = "aliasForQualifier"; + + // 1) Find and merge AnnotationAttributes from the annotation hierarchy + AnnotationAttributes attributes = findMergedAnnotationAttributes( + AliasedTransactionalComponentClass.class, AliasedTransactional.class); + assertThat(attributes).as("@AliasedTransactional on AliasedTransactionalComponentClass.").isNotNull(); + + // 2) Synthesize the AnnotationAttributes back into the target annotation + AliasedTransactional annotation = AnnotationUtils.synthesizeAnnotation(attributes, + AliasedTransactional.class, AliasedTransactionalComponentClass.class); + assertThat(annotation).isNotNull(); + + // 3) Verify that the AnnotationAttributes and synthesized annotation are equivalent + assertThat(attributes.getString("value")).as("TX value via attributes.").isEqualTo(qualifier); + assertThat(annotation.value()).as("TX value via synthesized annotation.").isEqualTo(qualifier); + assertThat(attributes.getString("qualifier")).as("TX qualifier via attributes.").isEqualTo(qualifier); + assertThat(annotation.qualifier()).as("TX qualifier via synthesized annotation.").isEqualTo(qualifier); + } + + @Test + void findMergedAnnotationAttributesOnClassWithAttributeAliasInComposedAnnotationAndNestedAnnotationsInTargetAnnotation() { + AnnotationAttributes attributes = assertComponentScanAttributes(TestComponentScanClass.class, "com.example.app.test"); + + Filter[] excludeFilters = attributes.getAnnotationArray("excludeFilters", Filter.class); + assertThat(excludeFilters).isNotNull(); + + List patterns = stream(excludeFilters).map(Filter::pattern).collect(toList()); + assertThat(patterns).isEqualTo(asList("*Test", "*Tests")); + } + + /** + * This test ensures that {@link AnnotationUtils#postProcessAnnotationAttributes} + * uses {@code ObjectUtils.nullSafeEquals()} to check for equality between annotation + * attributes since attributes may be arrays. + */ + @Test + void findMergedAnnotationAttributesOnClassWithBothAttributesOfAnAliasPairDeclared() { + assertComponentScanAttributes(ComponentScanWithBasePackagesAndValueAliasClass.class, "com.example.app.test"); + } + + /** + * @since 5.2.1 + * @see #23767 + */ + @Test + void findMergedAnnotationAttributesOnMethodWithComposedMetaTransactionalAnnotation() throws Exception { + Method method = getClass().getDeclaredMethod("composedTransactionalMethod"); + + AnnotationAttributes attributes = findMergedAnnotationAttributes(method, AliasedTransactional.class); + assertThat(attributes).as("Should find @AliasedTransactional on " + method).isNotNull(); + assertThat(attributes.getString("value")).as("TX qualifier for " + method).isEqualTo("anotherTransactionManager"); + assertThat(attributes.getString("qualifier")).as("TX qualifier for " + method).isEqualTo("anotherTransactionManager"); + } + + /** + * @since 5.2.1 + * @see #23767 + */ + @Test + void findMergedAnnotationOnMethodWithComposedMetaTransactionalAnnotation() throws Exception { + Method method = getClass().getDeclaredMethod("composedTransactionalMethod"); + + AliasedTransactional annotation = findMergedAnnotation(method, AliasedTransactional.class); + assertThat(annotation).as("Should find @AliasedTransactional on " + method).isNotNull(); + assertThat(annotation.value()).as("TX qualifier for " + method).isEqualTo("anotherTransactionManager"); + assertThat(annotation.qualifier()).as("TX qualifier for " + method).isEqualTo("anotherTransactionManager"); + } + + /** + * @since 5.2.1 + * @see #23767 + */ + @Test + void findMergedAnnotationAttributesOnClassWithComposedMetaTransactionalAnnotation() throws Exception { + Class clazz = ComposedTransactionalClass.class; + + AnnotationAttributes attributes = findMergedAnnotationAttributes(clazz, AliasedTransactional.class); + assertThat(attributes).as("Should find @AliasedTransactional on " + clazz).isNotNull(); + assertThat(attributes.getString("value")).as("TX qualifier for " + clazz).isEqualTo("anotherTransactionManager"); + assertThat(attributes.getString("qualifier")).as("TX qualifier for " + clazz).isEqualTo("anotherTransactionManager"); + } + + /** + * @since 5.2.1 + * @see #23767 + */ + @Test + void findMergedAnnotationOnClassWithComposedMetaTransactionalAnnotation() throws Exception { + Class clazz = ComposedTransactionalClass.class; + + AliasedTransactional annotation = findMergedAnnotation(clazz, AliasedTransactional.class); + assertThat(annotation).as("Should find @AliasedTransactional on " + clazz).isNotNull(); + assertThat(annotation.value()).as("TX qualifier for " + clazz).isEqualTo("anotherTransactionManager"); + assertThat(annotation.qualifier()).as("TX qualifier for " + clazz).isEqualTo("anotherTransactionManager"); + } + + @Test + void findMergedAnnotationAttributesWithSingleElementOverridingAnArrayViaConvention() { + assertComponentScanAttributes(ConventionBasedSinglePackageComponentScanClass.class, "com.example.app.test"); + } + + @Test + void findMergedAnnotationAttributesWithSingleElementOverridingAnArrayViaAliasFor() { + assertComponentScanAttributes(AliasForBasedSinglePackageComponentScanClass.class, "com.example.app.test"); + } + + private AnnotationAttributes assertComponentScanAttributes(Class element, String... expected) { + AnnotationAttributes attributes = findMergedAnnotationAttributes(element, ComponentScan.class); + + assertThat(attributes).as("Should find @ComponentScan on " + element).isNotNull(); + assertThat(attributes.getStringArray("value")).as("value: ").isEqualTo(expected); + assertThat(attributes.getStringArray("basePackages")).as("basePackages: ").isEqualTo(expected); + + return attributes; + } + + private AnnotationAttributes findMergedAnnotationAttributes(AnnotatedElement element, Class annotationType) { + return AnnotatedElementUtils.findMergedAnnotationAttributes(element, annotationType.getName(), false, false); + } + + @Test + void findMergedAnnotationWithAttributeAliasesInTargetAnnotation() { + Class element = AliasedTransactionalComponentClass.class; + AliasedTransactional annotation = findMergedAnnotation(element, AliasedTransactional.class); + assertThat(annotation).as("@AliasedTransactional on " + element).isNotNull(); + assertThat(annotation.value()).as("TX value via synthesized annotation.").isEqualTo("aliasForQualifier"); + assertThat(annotation.qualifier()).as("TX qualifier via synthesized annotation.").isEqualTo("aliasForQualifier"); + } + + @Test + void findMergedAnnotationForMultipleMetaAnnotationsWithClashingAttributeNames() { + String[] xmlLocations = asArray("test.xml"); + String[] propFiles = asArray("test.properties"); + + Class element = AliasedComposedContextConfigAndTestPropSourceClass.class; + + ContextConfig contextConfig = findMergedAnnotation(element, ContextConfig.class); + assertThat(contextConfig).as("@ContextConfig on " + element).isNotNull(); + assertThat(contextConfig.locations()).as("locations").isEqualTo(xmlLocations); + assertThat(contextConfig.value()).as("value").isEqualTo(xmlLocations); + + // Synthesized annotation + TestPropSource testPropSource = AnnotationUtils.findAnnotation(element, TestPropSource.class); + assertThat(testPropSource.locations()).as("locations").isEqualTo(propFiles); + assertThat(testPropSource.value()).as("value").isEqualTo(propFiles); + + // Merged annotation + testPropSource = findMergedAnnotation(element, TestPropSource.class); + assertThat(testPropSource).as("@TestPropSource on " + element).isNotNull(); + assertThat(testPropSource.locations()).as("locations").isEqualTo(propFiles); + assertThat(testPropSource.value()).as("value").isEqualTo(propFiles); + } + + @Test + void findMergedAnnotationWithLocalAliasesThatConflictWithAttributesInMetaAnnotationByConvention() { + final String[] EMPTY = new String[0]; + Class element = SpringAppConfigClass.class; + ContextConfig contextConfig = findMergedAnnotation(element, ContextConfig.class); + + assertThat(contextConfig).as("Should find @ContextConfig on " + element).isNotNull(); + assertThat(contextConfig.locations()).as("locations for " + element).isEqualTo(EMPTY); + // 'value' in @SpringAppConfig should not override 'value' in @ContextConfig + assertThat(contextConfig.value()).as("value for " + element).isEqualTo(EMPTY); + assertThat(contextConfig.classes()).as("classes for " + element).isEqualTo(new Class[] {Number.class}); + } + + @Test + void findMergedAnnotationWithSingleElementOverridingAnArrayViaConvention() throws Exception { + assertWebMapping(WebController.class.getMethod("postMappedWithPathAttribute")); + } + + @Test + void findMergedAnnotationWithSingleElementOverridingAnArrayViaAliasFor() throws Exception { + assertWebMapping(WebController.class.getMethod("getMappedWithValueAttribute")); + assertWebMapping(WebController.class.getMethod("getMappedWithPathAttribute")); + } + + private void assertWebMapping(AnnotatedElement element) { + WebMapping webMapping = findMergedAnnotation(element, WebMapping.class); + assertThat(webMapping).isNotNull(); + assertThat(webMapping.value()).as("value attribute: ").isEqualTo(asArray("/test")); + assertThat(webMapping.path()).as("path attribute: ").isEqualTo(asArray("/test")); + } + + @Test + void javaLangAnnotationTypeViaFindMergedAnnotation() throws Exception { + Constructor deprecatedCtor = Date.class.getConstructor(String.class); + assertThat(findMergedAnnotation(deprecatedCtor, Deprecated.class)).isEqualTo(deprecatedCtor.getAnnotation(Deprecated.class)); + assertThat(findMergedAnnotation(Date.class, Deprecated.class)).isEqualTo(Date.class.getAnnotation(Deprecated.class)); + } + + @Test + void javaxAnnotationTypeViaFindMergedAnnotation() throws Exception { + assertThat(findMergedAnnotation(ResourceHolder.class, Resource.class)).isEqualTo(ResourceHolder.class.getAnnotation(Resource.class)); + assertThat(findMergedAnnotation(SpringAppConfigClass.class, Resource.class)).isEqualTo(SpringAppConfigClass.class.getAnnotation(Resource.class)); + } + + @Test + void javaxMetaAnnotationTypeViaFindMergedAnnotation() throws Exception { + assertThat(findMergedAnnotation(ParametersAreNonnullByDefault.class, Nonnull.class)).isEqualTo(ParametersAreNonnullByDefault.class.getAnnotation(Nonnull.class)); + assertThat(findMergedAnnotation(ResourceHolder.class, Nonnull.class)).isEqualTo(ParametersAreNonnullByDefault.class.getAnnotation(Nonnull.class)); + } + + @Test + void nullableAnnotationTypeViaFindMergedAnnotation() throws Exception { + Method method = TransactionalServiceImpl.class.getMethod("doIt"); + assertThat(findMergedAnnotation(method, Resource.class)).isEqualTo(method.getAnnotation(Resource.class)); + } + + @Test + void getAllMergedAnnotationsOnClassWithInterface() throws Exception { + Method method = TransactionalServiceImpl.class.getMethod("doIt"); + Set allMergedAnnotations = getAllMergedAnnotations(method, Transactional.class); + assertThat(allMergedAnnotations.isEmpty()).isTrue(); + } + + @Test + void findAllMergedAnnotationsOnClassWithInterface() throws Exception { + Method method = TransactionalServiceImpl.class.getMethod("doIt"); + Set allMergedAnnotations = findAllMergedAnnotations(method, Transactional.class); + assertThat(allMergedAnnotations.size()).isEqualTo(1); + } + + @Test // SPR-16060 + void findMethodAnnotationFromGenericInterface() throws Exception { + Method method = ImplementsInterfaceWithGenericAnnotatedMethod.class.getMethod("foo", String.class); + Order order = findMergedAnnotation(method, Order.class); + assertThat(order).isNotNull(); + } + + @Test // SPR-17146 + void findMethodAnnotationFromGenericSuperclass() throws Exception { + Method method = ExtendsBaseClassWithGenericAnnotatedMethod.class.getMethod("foo", String.class); + Order order = findMergedAnnotation(method, Order.class); + assertThat(order).isNotNull(); + } + + @Test // gh-22655 + void forAnnotationsCreatesCopyOfArrayOnEachCall() { + AnnotatedElement element = AnnotatedElementUtils.forAnnotations(ForAnnotationsClass.class.getDeclaredAnnotations()); + // Trigger the NPE as originally reported in the bug + AnnotationsScanner.getDeclaredAnnotations(element, false); + AnnotationsScanner.getDeclaredAnnotations(element, false); + // Also specifically test we get different instances + assertThat(element.getDeclaredAnnotations()).isNotSameAs(element.getDeclaredAnnotations()); + } + + @Test // gh-22703 + void getMergedAnnotationOnThreeDeepMetaWithValue() { + ValueAttribute annotation = AnnotatedElementUtils.getMergedAnnotation( + ValueAttributeMetaMetaClass.class, ValueAttribute.class); + assertThat(annotation.value()).containsExactly("FromValueAttributeMeta"); + } + + + // ------------------------------------------------------------------------- + + @MetaCycle3 + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.ANNOTATION_TYPE) + @interface MetaCycle1 { + } + + @MetaCycle1 + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.ANNOTATION_TYPE) + @interface MetaCycle2 { + } + + @MetaCycle2 + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @interface MetaCycle3 { + } + + @MetaCycle3 + static class MetaCycleAnnotatedClass { + } + + // ------------------------------------------------------------------------- + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.TYPE, ElementType.METHOD}) + @Inherited + @interface Transactional { + + String value() default ""; + + String qualifier() default "transactionManager"; + + boolean readOnly() default false; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.TYPE, ElementType.METHOD}) + @Inherited + @interface AliasedTransactional { + + @AliasFor("qualifier") + String value() default ""; + + @AliasFor("value") + String qualifier() default ""; + } + + @AliasedTransactional + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.TYPE, ElementType.METHOD}) + @interface MyAliasedTransactional { + + @AliasFor(annotation = AliasedTransactional.class, attribute = "value") + String value() default "defaultTransactionManager"; + } + + @MyAliasedTransactional("anotherTransactionManager") + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.TYPE, ElementType.METHOD}) + @interface ComposedMyAliasedTransactional { + } + + @Transactional(qualifier = "composed1") + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @Inherited + @interface InheritedComposed { + } + + @Transactional(qualifier = "composed2", readOnly = true) + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @interface Composed { + } + + @Transactional + @Retention(RetentionPolicy.RUNTIME) + @interface TxComposedWithOverride { + + String qualifier() default "txMgr"; + } + + @Transactional("TxInheritedComposed") + @Retention(RetentionPolicy.RUNTIME) + @interface TxInheritedComposed { + } + + @Transactional("TxComposed") + @Retention(RetentionPolicy.RUNTIME) + @interface TxComposed { + } + + @Transactional + @Component + @Retention(RetentionPolicy.RUNTIME) + @interface TransactionalComponent { + } + + @TransactionalComponent + @Retention(RetentionPolicy.RUNTIME) + @interface ComposedTransactionalComponent { + } + + @AliasedTransactional(value = "aliasForQualifier") + @Component + @Retention(RetentionPolicy.RUNTIME) + @interface AliasedTransactionalComponent { + } + + @TxComposedWithOverride + // Override default "txMgr" from @TxComposedWithOverride with "localTxMgr" + @Transactional(qualifier = "localTxMgr") + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @interface MetaAndLocalTxConfig { + } + + /** + * Mock of {@code org.springframework.test.context.TestPropertySource}. + */ + @Retention(RetentionPolicy.RUNTIME) + @interface TestPropSource { + + @AliasFor("locations") + String[] value() default {}; + + @AliasFor("value") + String[] locations() default {}; + } + + /** + * Mock of {@code org.springframework.test.context.ContextConfiguration}. + */ + @Retention(RetentionPolicy.RUNTIME) + @interface ContextConfig { + + @AliasFor("locations") + String[] value() default {}; + + @AliasFor("value") + String[] locations() default {}; + + Class[] classes() default {}; + } + + @ContextConfig + @Retention(RetentionPolicy.RUNTIME) + @interface ConventionBasedComposedContextConfig { + + String[] locations() default {}; + } + + @ContextConfig(value = "duplicateDeclaration") + @Retention(RetentionPolicy.RUNTIME) + @interface InvalidConventionBasedComposedContextConfig { + + String[] locations(); + } + + /** + * This hybrid approach for annotation attribute overrides with transitive implicit + * aliases is unsupported. See SPR-13554 for details. + */ + @ContextConfig + @Retention(RetentionPolicy.RUNTIME) + @interface HalfConventionBasedAndHalfAliasedComposedContextConfig { + + String[] locations() default {}; + + @AliasFor(annotation = ContextConfig.class, attribute = "locations") + String[] xmlConfigFiles() default {}; + } + + @ContextConfig + @Retention(RetentionPolicy.RUNTIME) + @interface AliasedComposedContextConfig { + + @AliasFor(annotation = ContextConfig.class, attribute = "locations") + String[] xmlConfigFiles(); + } + + @ContextConfig + @Retention(RetentionPolicy.RUNTIME) + @interface AliasedValueComposedContextConfig { + + @AliasFor(annotation = ContextConfig.class, attribute = "value") + String[] locations(); + } + + @ContextConfig + @Retention(RetentionPolicy.RUNTIME) + @interface ImplicitAliasesContextConfig { + + @AliasFor(annotation = ContextConfig.class, attribute = "locations") + String[] groovyScripts() default {}; + + @AliasFor(annotation = ContextConfig.class, attribute = "locations") + String[] xmlFiles() default {}; + + // intentionally omitted: attribute = "locations" + @AliasFor(annotation = ContextConfig.class) + String[] locations() default {}; + + // intentionally omitted: attribute = "locations" (SPR-14069) + @AliasFor(annotation = ContextConfig.class) + String[] value() default {}; + } + + @ImplicitAliasesContextConfig(xmlFiles = {"A.xml", "B.xml"}) + @Retention(RetentionPolicy.RUNTIME) + @interface ComposedImplicitAliasesContextConfig { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasesWithDefaults { + + @AliasFor("a2") + String a1() default "AliasesWithDefaults"; + + @AliasFor("a1") + String a2() default "AliasesWithDefaults"; + } + + @Retention(RetentionPolicy.RUNTIME) + @AliasesWithDefaults + @interface ImplicitAliasesWithDefaults { + + @AliasFor(annotation = AliasesWithDefaults.class, attribute = "a1") + String b1() default "ImplicitAliasesWithDefaults"; + + @AliasFor(annotation = AliasesWithDefaults.class, attribute = "a2") + String b2() default "ImplicitAliasesWithDefaults"; + } + + @ImplicitAliasesContextConfig + @Retention(RetentionPolicy.RUNTIME) + @interface TransitiveImplicitAliasesContextConfig { + + @AliasFor(annotation = ImplicitAliasesContextConfig.class, attribute = "xmlFiles") + String[] xml() default {}; + + @AliasFor(annotation = ImplicitAliasesContextConfig.class, attribute = "groovyScripts") + String[] groovy() default {}; + } + + @ImplicitAliasesContextConfig + @Retention(RetentionPolicy.RUNTIME) + @interface SingleLocationTransitiveImplicitAliasesContextConfig { + + @AliasFor(annotation = ImplicitAliasesContextConfig.class, attribute = "xmlFiles") + String xml() default ""; + + @AliasFor(annotation = ImplicitAliasesContextConfig.class, attribute = "groovyScripts") + String groovy() default ""; + } + + @ImplicitAliasesContextConfig + @Retention(RetentionPolicy.RUNTIME) + @interface TransitiveImplicitAliasesWithSkippedLevelContextConfig { + + @AliasFor(annotation = ContextConfig.class, attribute = "locations") + String[] xml() default {}; + + @AliasFor(annotation = ImplicitAliasesContextConfig.class, attribute = "groovyScripts") + String[] groovy() default {}; + } + + @ImplicitAliasesContextConfig + @Retention(RetentionPolicy.RUNTIME) + @interface SingleLocationTransitiveImplicitAliasesWithSkippedLevelContextConfig { + + @AliasFor(annotation = ContextConfig.class, attribute = "locations") + String xml() default ""; + + @AliasFor(annotation = ImplicitAliasesContextConfig.class, attribute = "groovyScripts") + String groovy() default ""; + } + + /** + * Although the configuration declares an explicit value for 'value' and + * requires a value for the aliased 'locations', this does not result in + * an error since 'locations' effectively shadows the 'value' + * attribute (which cannot be set via the composed annotation anyway). + * + * If 'value' were not shadowed, such a declaration would not make sense. + */ + @ContextConfig(value = "duplicateDeclaration") + @Retention(RetentionPolicy.RUNTIME) + @interface ShadowedAliasComposedContextConfig { + + @AliasFor(annotation = ContextConfig.class, attribute = "locations") + String[] xmlConfigFiles(); + } + + @ContextConfig(locations = "shadowed.xml") + @TestPropSource(locations = "test.properties") + @Retention(RetentionPolicy.RUNTIME) + @interface AliasedComposedContextConfigAndTestPropSource { + + @AliasFor(annotation = ContextConfig.class, attribute = "locations") + String[] xmlConfigFiles() default "default.xml"; + } + + /** + * Mock of {@code org.springframework.boot.test.SpringApplicationConfiguration}. + */ + @ContextConfig + @Retention(RetentionPolicy.RUNTIME) + @interface SpringAppConfig { + + @AliasFor(annotation = ContextConfig.class, attribute = "locations") + String[] locations() default {}; + + @AliasFor("value") + Class[] classes() default {}; + + @AliasFor("classes") + Class[] value() default {}; + } + + /** + * Mock of {@code org.springframework.context.annotation.ComponentScan} + */ + @Retention(RetentionPolicy.RUNTIME) + @interface ComponentScan { + + @AliasFor("basePackages") + String[] value() default {}; + + // Intentionally no alias declaration for "value" + String[] basePackages() default {}; + + Filter[] excludeFilters() default {}; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({}) + @interface Filter { + + String pattern(); + } + + @ComponentScan(excludeFilters = {@Filter(pattern = "*Test"), @Filter(pattern = "*Tests")}) + @Retention(RetentionPolicy.RUNTIME) + @interface TestComponentScan { + + @AliasFor(attribute = "basePackages", annotation = ComponentScan.class) + String[] packages(); + } + + @ComponentScan + @Retention(RetentionPolicy.RUNTIME) + @interface ConventionBasedSinglePackageComponentScan { + + String basePackages(); + } + + @ComponentScan + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForBasedSinglePackageComponentScan { + + @AliasFor(attribute = "basePackages", annotation = ComponentScan.class) + String pkg(); + } + + // ------------------------------------------------------------------------- + + static class NonAnnotatedClass { + } + + @TransactionalComponent + static class TransactionalComponentClass { + } + + static class SubTransactionalComponentClass extends TransactionalComponentClass { + } + + @ComposedTransactionalComponent + static class ComposedTransactionalComponentClass { + } + + @AliasedTransactionalComponent + static class AliasedTransactionalComponentClass { + } + + @ComposedMyAliasedTransactional + void composedTransactionalMethod() { + } + + @ComposedMyAliasedTransactional + static class ComposedTransactionalClass { + } + + @Transactional + static class ClassWithInheritedAnnotation { + } + + @Composed + static class SubClassWithInheritedAnnotation extends ClassWithInheritedAnnotation { + } + + static class SubSubClassWithInheritedAnnotation extends SubClassWithInheritedAnnotation { + } + + @InheritedComposed + static class ClassWithInheritedComposedAnnotation { + } + + @Composed + static class SubClassWithInheritedComposedAnnotation extends ClassWithInheritedComposedAnnotation { + } + + static class SubSubClassWithInheritedComposedAnnotation extends SubClassWithInheritedComposedAnnotation { + } + + @MetaAndLocalTxConfig + static class MetaAndLocalTxConfigClass { + } + + @Transactional("TxConfig") + static class TxConfig { + } + + @Transactional("DerivedTxConfig") + static class DerivedTxConfig extends TxConfig { + } + + @TxInheritedComposed + @TxComposed + static class TxFromMultipleComposedAnnotations { + } + + @Transactional + static interface InterfaceWithInheritedAnnotation { + + @Order + void handleFromInterface(); + } + + static abstract class AbstractClassWithInheritedAnnotation implements InterfaceWithInheritedAnnotation { + + @Transactional + public abstract void handle(); + + @Transactional + public void handleParameterized(T t) { + } + } + + static class ConcreteClassWithInheritedAnnotation extends AbstractClassWithInheritedAnnotation { + + @Override + public void handle() { + } + + @Override + public void handleParameterized(String s) { + } + + @Override + public void handleFromInterface() { + } + } + + public interface GenericParameter { + + T getFor(Class cls); + } + + @SuppressWarnings("unused") + private static class StringGenericParameter implements GenericParameter { + + @Order + @Override + public String getFor(Class cls) { + return "foo"; + } + + public String getFor(Integer integer) { + return "foo"; + } + } + + @Transactional + public interface InheritedAnnotationInterface { + } + + public interface SubInheritedAnnotationInterface extends InheritedAnnotationInterface { + } + + public interface SubSubInheritedAnnotationInterface extends SubInheritedAnnotationInterface { + } + + @Order + public interface NonInheritedAnnotationInterface { + } + + public interface SubNonInheritedAnnotationInterface extends NonInheritedAnnotationInterface { + } + + public interface SubSubNonInheritedAnnotationInterface extends SubNonInheritedAnnotationInterface { + } + + @ConventionBasedComposedContextConfig(locations = "explicitDeclaration") + static class ConventionBasedComposedContextConfigClass { + } + + @InvalidConventionBasedComposedContextConfig(locations = "requiredLocationsDeclaration") + static class InvalidConventionBasedComposedContextConfigClass { + } + + @HalfConventionBasedAndHalfAliasedComposedContextConfig(xmlConfigFiles = "explicitDeclaration") + static class HalfConventionBasedAndHalfAliasedComposedContextConfigClassV1 { + } + + @HalfConventionBasedAndHalfAliasedComposedContextConfig(locations = "explicitDeclaration") + static class HalfConventionBasedAndHalfAliasedComposedContextConfigClassV2 { + } + + @AliasedComposedContextConfig(xmlConfigFiles = "test.xml") + static class AliasedComposedContextConfigClass { + } + + @AliasedValueComposedContextConfig(locations = "test.xml") + static class AliasedValueComposedContextConfigClass { + } + + @ImplicitAliasesContextConfig("foo.xml") + static class ImplicitAliasesContextConfigClass1 { + } + + @ImplicitAliasesContextConfig(locations = "bar.xml") + static class ImplicitAliasesContextConfigClass2 { + } + + @ImplicitAliasesContextConfig(xmlFiles = "baz.xml") + static class ImplicitAliasesContextConfigClass3 { + } + + @ImplicitAliasesWithDefaults + static class ImplicitAliasesWithDefaultsClass { + } + + @TransitiveImplicitAliasesContextConfig(groovy = "test.groovy") + static class TransitiveImplicitAliasesContextConfigClass { + } + + @SingleLocationTransitiveImplicitAliasesContextConfig(groovy = "test.groovy") + static class SingleLocationTransitiveImplicitAliasesContextConfigClass { + } + + @TransitiveImplicitAliasesWithSkippedLevelContextConfig(xml = "test.xml") + static class TransitiveImplicitAliasesWithSkippedLevelContextConfigClass { + } + + @SingleLocationTransitiveImplicitAliasesWithSkippedLevelContextConfig(xml = "test.xml") + static class SingleLocationTransitiveImplicitAliasesWithSkippedLevelContextConfigClass { + } + + @ComposedImplicitAliasesContextConfig + static class ComposedImplicitAliasesContextConfigClass { + } + + @ShadowedAliasComposedContextConfig(xmlConfigFiles = "test.xml") + static class ShadowedAliasComposedContextConfigClass { + } + + @AliasedComposedContextConfigAndTestPropSource(xmlConfigFiles = "test.xml") + static class AliasedComposedContextConfigAndTestPropSourceClass { + } + + @ComponentScan(value = "com.example.app.test", basePackages = "com.example.app.test") + static class ComponentScanWithBasePackagesAndValueAliasClass { + } + + @TestComponentScan(packages = "com.example.app.test") + static class TestComponentScanClass { + } + + @ConventionBasedSinglePackageComponentScan(basePackages = "com.example.app.test") + static class ConventionBasedSinglePackageComponentScanClass { + } + + @AliasForBasedSinglePackageComponentScan(pkg = "com.example.app.test") + static class AliasForBasedSinglePackageComponentScanClass { + } + + @SpringAppConfig(Number.class) + static class SpringAppConfigClass { + } + + @Resource(name = "x") + @ParametersAreNonnullByDefault + static class ResourceHolder { + } + + interface TransactionalService { + + @Transactional + @Nullable + Object doIt(); + } + + class TransactionalServiceImpl implements TransactionalService { + + @Override + @Nullable + public Object doIt() { + return null; + } + } + + @Deprecated + @ComponentScan + class ForAnnotationsClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ValueAttribute { + + String[] value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @ValueAttribute("FromValueAttributeMeta") + @interface ValueAttributeMeta { + + @AliasFor("alias") + String[] value() default {}; + + @AliasFor("value") + String[] alias() default {}; + + } + + @Retention(RetentionPolicy.RUNTIME) + @ValueAttributeMeta("FromValueAttributeMetaMeta") + @interface ValueAttributeMetaMeta { + } + + @ValueAttributeMetaMeta + static class ValueAttributeMetaMetaClass { + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationAttributesTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationAttributesTests.java new file mode 100644 index 0000000..4d99c42 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationAttributesTests.java @@ -0,0 +1,253 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.annotation.AnnotationUtilsTests.ImplicitAliasesContextConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Unit tests for {@link AnnotationAttributes}. + * + * @author Chris Beams + * @author Sam Brannen + * @author Juergen Hoeller + * @since 3.1.1 + */ +class AnnotationAttributesTests { + + private AnnotationAttributes attributes = new AnnotationAttributes(); + + + @Test + void typeSafeAttributeAccess() { + AnnotationAttributes nestedAttributes = new AnnotationAttributes(); + nestedAttributes.put("value", 10); + nestedAttributes.put("name", "algernon"); + + attributes.put("name", "dave"); + attributes.put("names", new String[] {"dave", "frank", "hal"}); + attributes.put("bool1", true); + attributes.put("bool2", false); + attributes.put("color", Color.RED); + attributes.put("class", Integer.class); + attributes.put("classes", new Class[] {Number.class, Short.class, Integer.class}); + attributes.put("number", 42); + attributes.put("anno", nestedAttributes); + attributes.put("annoArray", new AnnotationAttributes[] {nestedAttributes}); + + assertThat(attributes.getString("name")).isEqualTo("dave"); + assertThat(attributes.getStringArray("names")).isEqualTo(new String[] {"dave", "frank", "hal"}); + assertThat(attributes.getBoolean("bool1")).isEqualTo(true); + assertThat(attributes.getBoolean("bool2")).isEqualTo(false); + assertThat(attributes.getEnum("color")).isEqualTo(Color.RED); + assertThat(attributes.getClass("class").equals(Integer.class)).isTrue(); + assertThat(attributes.getClassArray("classes")).isEqualTo(new Class[] {Number.class, Short.class, Integer.class}); + assertThat(attributes.getNumber("number")).isEqualTo(42); + assertThat(attributes.getAnnotation("anno").getNumber("value")).isEqualTo(10); + assertThat(attributes.getAnnotationArray("annoArray")[0].getString("name")).isEqualTo("algernon"); + + } + + @Test + void unresolvableClassWithClassNotFoundException() throws Exception { + attributes.put("unresolvableClass", new ClassNotFoundException("myclass")); + assertThatIllegalArgumentException() + .isThrownBy(() -> attributes.getClass("unresolvableClass")) + .withMessageContaining("myclass") + .withCauseInstanceOf(ClassNotFoundException.class); + } + + @Test + void unresolvableClassWithLinkageError() throws Exception { + attributes.put("unresolvableClass", new LinkageError("myclass")); + assertThatIllegalArgumentException() + .isThrownBy(() -> attributes.getClass("unresolvableClass")) + .withMessageContaining("myclass") + .withCauseInstanceOf(LinkageError.class); + } + + @Test + void singleElementToSingleElementArrayConversionSupport() throws Exception { + Filter filter = FilteredClass.class.getAnnotation(Filter.class); + + AnnotationAttributes nestedAttributes = new AnnotationAttributes(); + nestedAttributes.put("name", "Dilbert"); + + // Store single elements + attributes.put("names", "Dogbert"); + attributes.put("classes", Number.class); + attributes.put("nestedAttributes", nestedAttributes); + attributes.put("filters", filter); + + // Get back arrays of single elements + assertThat(attributes.getStringArray("names")).isEqualTo(new String[] {"Dogbert"}); + assertThat(attributes.getClassArray("classes")).isEqualTo(new Class[] {Number.class}); + + AnnotationAttributes[] array = attributes.getAnnotationArray("nestedAttributes"); + assertThat(array).isNotNull(); + assertThat(array.length).isEqualTo(1); + assertThat(array[0].getString("name")).isEqualTo("Dilbert"); + + Filter[] filters = attributes.getAnnotationArray("filters", Filter.class); + assertThat(filters).isNotNull(); + assertThat(filters.length).isEqualTo(1); + assertThat(filters[0].pattern()).isEqualTo("foo"); + } + + @Test + void nestedAnnotations() throws Exception { + Filter filter = FilteredClass.class.getAnnotation(Filter.class); + + attributes.put("filter", filter); + attributes.put("filters", new Filter[] {filter, filter}); + + Filter retrievedFilter = attributes.getAnnotation("filter", Filter.class); + assertThat(retrievedFilter).isEqualTo(filter); + assertThat(retrievedFilter.pattern()).isEqualTo("foo"); + + Filter[] retrievedFilters = attributes.getAnnotationArray("filters", Filter.class); + assertThat(retrievedFilters).isNotNull(); + assertThat(retrievedFilters.length).isEqualTo(2); + assertThat(retrievedFilters[1].pattern()).isEqualTo("foo"); + } + + @Test + void getEnumWithNullAttributeName() { + assertThatIllegalArgumentException() + .isThrownBy(() -> attributes.getEnum(null)) + .withMessageContaining("must not be null or empty"); + } + + @Test + void getEnumWithEmptyAttributeName() { + assertThatIllegalArgumentException() + .isThrownBy(() -> attributes.getEnum("")) + .withMessageContaining("must not be null or empty"); + } + + @Test + void getEnumWithUnknownAttributeName() { + assertThatIllegalArgumentException() + .isThrownBy(() -> attributes.getEnum("bogus")) + .withMessageContaining("Attribute 'bogus' not found"); + } + + @Test + void getEnumWithTypeMismatch() { + attributes.put("color", "RED"); + assertThatIllegalArgumentException() + .isThrownBy(() -> attributes.getEnum("color")) + .withMessageContaining("Attribute 'color' is of type String, but Enum was expected"); + } + + @Test + void getAliasedStringWithImplicitAliases() { + String value = "metaverse"; + List aliases = Arrays.asList("value", "location1", "location2", "location3", "xmlFile", "groovyScript"); + + attributes = new AnnotationAttributes(ImplicitAliasesContextConfig.class); + attributes.put("value", value); + AnnotationUtils.postProcessAnnotationAttributes(null, attributes, false); + aliases.stream().forEach(alias -> assertThat(attributes.getString(alias)).isEqualTo(value)); + + attributes = new AnnotationAttributes(ImplicitAliasesContextConfig.class); + attributes.put("location1", value); + AnnotationUtils.postProcessAnnotationAttributes(null, attributes, false); + aliases.stream().forEach(alias -> assertThat(attributes.getString(alias)).isEqualTo(value)); + + attributes = new AnnotationAttributes(ImplicitAliasesContextConfig.class); + attributes.put("value", value); + attributes.put("location1", value); + attributes.put("xmlFile", value); + attributes.put("groovyScript", value); + AnnotationUtils.postProcessAnnotationAttributes(null, attributes, false); + aliases.stream().forEach(alias -> assertThat(attributes.getString(alias)).isEqualTo(value)); + } + + @Test + void getAliasedStringArrayWithImplicitAliases() { + String[] value = new String[] {"test.xml"}; + List aliases = Arrays.asList("value", "location1", "location2", "location3", "xmlFile", "groovyScript"); + + attributes = new AnnotationAttributes(ImplicitAliasesContextConfig.class); + attributes.put("location1", value); + AnnotationUtils.postProcessAnnotationAttributes(null, attributes, false); + aliases.stream().forEach(alias -> assertThat(attributes.getStringArray(alias)).isEqualTo(value)); + + attributes = new AnnotationAttributes(ImplicitAliasesContextConfig.class); + attributes.put("value", value); + AnnotationUtils.postProcessAnnotationAttributes(null, attributes, false); + aliases.stream().forEach(alias -> assertThat(attributes.getStringArray(alias)).isEqualTo(value)); + + attributes = new AnnotationAttributes(ImplicitAliasesContextConfig.class); + attributes.put("location1", value); + attributes.put("value", value); + AnnotationUtils.postProcessAnnotationAttributes(null, attributes, false); + aliases.stream().forEach(alias -> assertThat(attributes.getStringArray(alias)).isEqualTo(value)); + + attributes = new AnnotationAttributes(ImplicitAliasesContextConfig.class); + attributes.put("location1", value); + AnnotationUtils.registerDefaultValues(attributes); + AnnotationUtils.postProcessAnnotationAttributes(null, attributes, false); + aliases.stream().forEach(alias -> assertThat(attributes.getStringArray(alias)).isEqualTo(value)); + + attributes = new AnnotationAttributes(ImplicitAliasesContextConfig.class); + attributes.put("value", value); + AnnotationUtils.registerDefaultValues(attributes); + AnnotationUtils.postProcessAnnotationAttributes(null, attributes, false); + aliases.stream().forEach(alias -> assertThat(attributes.getStringArray(alias)).isEqualTo(value)); + + attributes = new AnnotationAttributes(ImplicitAliasesContextConfig.class); + AnnotationUtils.registerDefaultValues(attributes); + AnnotationUtils.postProcessAnnotationAttributes(null, attributes, false); + aliases.stream().forEach(alias -> assertThat(attributes.getStringArray(alias)).isEqualTo(new String[] {""})); + } + + + enum Color { + + RED, WHITE, BLUE + } + + + @Retention(RetentionPolicy.RUNTIME) + @interface Filter { + + @AliasFor(attribute = "classes") + Class[] value() default {}; + + @AliasFor(attribute = "value") + Class[] classes() default {}; + + String pattern(); + } + + + @Filter(pattern = "foo") + static class FilteredClass { + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationAwareOrderComparatorTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationAwareOrderComparatorTests.java new file mode 100644 index 0000000..afe0bcc --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationAwareOrderComparatorTests.java @@ -0,0 +1,133 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.util.ArrayList; +import java.util.List; + +import javax.annotation.Priority; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @author Oliver Gierke + */ +class AnnotationAwareOrderComparatorTests { + + @Test + void instanceVariableIsAnAnnotationAwareOrderComparator() { + assertThat(AnnotationAwareOrderComparator.INSTANCE).isInstanceOf(AnnotationAwareOrderComparator.class); + } + + @Test + void sortInstances() { + List list = new ArrayList<>(); + list.add(new B()); + list.add(new A()); + AnnotationAwareOrderComparator.sort(list); + assertThat(list.get(0) instanceof A).isTrue(); + assertThat(list.get(1) instanceof B).isTrue(); + } + + @Test + void sortInstancesWithPriority() { + List list = new ArrayList<>(); + list.add(new B2()); + list.add(new A2()); + AnnotationAwareOrderComparator.sort(list); + assertThat(list.get(0) instanceof A2).isTrue(); + assertThat(list.get(1) instanceof B2).isTrue(); + } + + @Test + void sortInstancesWithOrderAndPriority() { + List list = new ArrayList<>(); + list.add(new B()); + list.add(new A2()); + AnnotationAwareOrderComparator.sort(list); + assertThat(list.get(0) instanceof A2).isTrue(); + assertThat(list.get(1) instanceof B).isTrue(); + } + + @Test + void sortInstancesWithSubclass() { + List list = new ArrayList<>(); + list.add(new B()); + list.add(new C()); + AnnotationAwareOrderComparator.sort(list); + assertThat(list.get(0) instanceof C).isTrue(); + assertThat(list.get(1) instanceof B).isTrue(); + } + + @Test + void sortClasses() { + List list = new ArrayList<>(); + list.add(B.class); + list.add(A.class); + AnnotationAwareOrderComparator.sort(list); + assertThat(list.get(0)).isEqualTo(A.class); + assertThat(list.get(1)).isEqualTo(B.class); + } + + @Test + void sortClassesWithSubclass() { + List list = new ArrayList<>(); + list.add(B.class); + list.add(C.class); + AnnotationAwareOrderComparator.sort(list); + assertThat(list.get(0)).isEqualTo(C.class); + assertThat(list.get(1)).isEqualTo(B.class); + } + + @Test + void sortWithNulls() { + List list = new ArrayList<>(); + list.add(null); + list.add(B.class); + list.add(null); + list.add(A.class); + AnnotationAwareOrderComparator.sort(list); + assertThat(list.get(0)).isEqualTo(A.class); + assertThat(list.get(1)).isEqualTo(B.class); + assertThat(list.get(2)).isNull(); + assertThat(list.get(3)).isNull(); + } + + + @Order(1) + private static class A { + } + + @Order(2) + private static class B { + } + + private static class C extends A { + } + + @Priority(1) + private static class A2 { + } + + @Priority(2) + private static class B2 { + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationBackCompatibiltyTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationBackCompatibiltyTests.java new file mode 100644 index 0000000..d65ffb0 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationBackCompatibiltyTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests to ensure back-compatibility with Spring Framework 5.1. + * + * @author Phillip Webb + * @since 5.2 + */ +class AnnotationBackCompatibiltyTests { + + @Test + void multiplRoutesToMetaAnnotation() { + Class source = WithMetaMetaTestAnnotation1AndMetaTestAnnotation2.class; + // Merged annotation chooses lowest depth + MergedAnnotation mergedAnnotation = MergedAnnotations.from(source).get(TestAnnotation.class); + assertThat(mergedAnnotation.getString("value")).isEqualTo("testAndMetaTest"); + // AnnotatedElementUtils finds first + TestAnnotation previousVersion = AnnotatedElementUtils.getMergedAnnotation(source, TestAnnotation.class); + assertThat(previousVersion.value()).isEqualTo("metaTest"); + } + + @Test + void defaultValue() { + DefaultValueAnnotation synthesized = MergedAnnotations.from(WithDefaultValue.class).get(DefaultValueAnnotation.class).synthesize(); + assertThat(synthesized).isInstanceOf(SynthesizedAnnotation.class); + Object defaultValue = AnnotationUtils.getDefaultValue(synthesized, "enumValue"); + assertThat(defaultValue).isEqualTo(TestEnum.ONE); + } + + @Retention(RetentionPolicy.RUNTIME) + @interface TestAnnotation { + + String value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @TestAnnotation("metaTest") + @interface MetaTestAnnotation { + + } + + @Retention(RetentionPolicy.RUNTIME) + @TestAnnotation("testAndMetaTest") + @MetaTestAnnotation + @interface TestAndMetaTestAnnotation { + + } + + @Retention(RetentionPolicy.RUNTIME) + @MetaTestAnnotation + @interface MetaMetaTestAnnotation { + } + + @MetaMetaTestAnnotation + @TestAndMetaTestAnnotation + static class WithMetaMetaTestAnnotation1AndMetaTestAnnotation2 { + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface DefaultValueAnnotation { + + @AliasFor("enumAlias") + TestEnum enumValue() default TestEnum.ONE; + + @AliasFor("enumValue") + TestEnum enumAlias() default TestEnum.ONE; + + } + + @DefaultValueAnnotation + static class WithDefaultValue { + + } + + static enum TestEnum { + + ONE, + + TWO + + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationEnclosingClassSample.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationEnclosingClassSample.java new file mode 100644 index 0000000..7db91cf --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationEnclosingClassSample.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Example class used to test {@link AnnotationsScanner} with enclosing classes. + * + * @author Phillip Webb + * @since 5.2 + */ +@AnnotationEnclosingClassSample.EnclosedOne +public class AnnotationEnclosingClassSample { + + @EnclosedTwo + public static class EnclosedStatic { + + @EnclosedThree + public static class EnclosedStaticStatic { + + } + + } + + @EnclosedTwo + public class EnclosedInner { + + @EnclosedThree + public class EnclosedInnerInner { + + } + + } + + @Retention(RetentionPolicy.RUNTIME) + public static @interface EnclosedOne { + + } + + @Retention(RetentionPolicy.RUNTIME) + public static @interface EnclosedTwo { + + } + + @Retention(RetentionPolicy.RUNTIME) + public static @interface EnclosedThree { + + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationFilterTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationFilterTests.java new file mode 100644 index 0000000..de5319b --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationFilterTests.java @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.annotation.Nonnull; + +import org.junit.jupiter.api.Test; + +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AnnotationFilter}. + * + * @author Phillip Webb + */ +class AnnotationFilterTests { + + private static final AnnotationFilter FILTER = annotationType -> + ObjectUtils.nullSafeEquals(annotationType, TestAnnotation.class.getName()); + + + @Test + void matchesAnnotationWhenMatchReturnsTrue() { + TestAnnotation annotation = WithTestAnnotation.class.getDeclaredAnnotation(TestAnnotation.class); + assertThat(FILTER.matches(annotation)).isTrue(); + } + + @Test + void matchesAnnotationWhenNoMatchReturnsFalse() { + OtherAnnotation annotation = WithOtherAnnotation.class.getDeclaredAnnotation(OtherAnnotation.class); + assertThat(FILTER.matches(annotation)).isFalse(); + } + + @Test + void matchesAnnotationClassWhenMatchReturnsTrue() { + Class annotationType = TestAnnotation.class; + assertThat(FILTER.matches(annotationType)).isTrue(); + } + + @Test + void matchesAnnotationClassWhenNoMatchReturnsFalse() { + Class annotationType = OtherAnnotation.class; + assertThat(FILTER.matches(annotationType)).isFalse(); + } + + @Test + void plainWhenJavaLangAnnotationReturnsTrue() { + assertThat(AnnotationFilter.PLAIN.matches(Retention.class)).isTrue(); + } + + @Test + void plainWhenSpringLangAnnotationReturnsTrue() { + assertThat(AnnotationFilter.PLAIN.matches(Nullable.class)).isTrue(); + } + + @Test + void plainWhenOtherAnnotationReturnsFalse() { + assertThat(AnnotationFilter.PLAIN.matches(TestAnnotation.class)).isFalse(); + } + + @Test + void javaWhenJavaLangAnnotationReturnsTrue() { + assertThat(AnnotationFilter.JAVA.matches(Retention.class)).isTrue(); + } + + @Test + void javaWhenJavaxAnnotationReturnsTrue() { + assertThat(AnnotationFilter.JAVA.matches(Nonnull.class)).isTrue(); + } + + @Test + void javaWhenSpringLangAnnotationReturnsFalse() { + assertThat(AnnotationFilter.JAVA.matches(Nullable.class)).isFalse(); + } + + @Test + void javaWhenOtherAnnotationReturnsFalse() { + assertThat(AnnotationFilter.JAVA.matches(TestAnnotation.class)).isFalse(); + } + + @Test + @SuppressWarnings("deprecation") + void noneReturnsFalse() { + assertThat(AnnotationFilter.NONE.matches(Retention.class)).isFalse(); + assertThat(AnnotationFilter.NONE.matches(Nullable.class)).isFalse(); + assertThat(AnnotationFilter.NONE.matches(TestAnnotation.class)).isFalse(); + } + + + @Retention(RetentionPolicy.RUNTIME) + @interface TestAnnotation { + } + + @TestAnnotation + static class WithTestAnnotation { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface OtherAnnotation { + } + + @OtherAnnotation + static class WithOtherAnnotation { + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationIntrospectionFailureTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationIntrospectionFailureTests.java new file mode 100644 index 0000000..0fa93c9 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationIntrospectionFailureTests.java @@ -0,0 +1,149 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.OverridingClassLoader; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests that trigger annotation introspection failures and ensure that they are + * dealt with correctly. + * + * @author Phillip Webb + * @since 5.2 + * @see AnnotationUtils + * @see AnnotatedElementUtils + */ +class AnnotationIntrospectionFailureTests { + + @Test + void filteredTypeThrowsTypeNotPresentException() throws Exception { + FilteringClassLoader classLoader = new FilteringClassLoader( + getClass().getClassLoader()); + Class withExampleAnnotation = ClassUtils.forName( + WithExampleAnnotation.class.getName(), classLoader); + Annotation annotation = withExampleAnnotation.getAnnotations()[0]; + Method method = annotation.annotationType().getMethod("value"); + method.setAccessible(true); + assertThatExceptionOfType(TypeNotPresentException.class).isThrownBy(() -> + ReflectionUtils.invokeMethod(method, annotation)) + .withCauseInstanceOf(ClassNotFoundException.class); + } + + @Test + @SuppressWarnings("unchecked") + void filteredTypeInMetaAnnotationWhenUsingAnnotatedElementUtilsHandlesException() throws Exception { + FilteringClassLoader classLoader = new FilteringClassLoader( + getClass().getClassLoader()); + Class withExampleMetaAnnotation = ClassUtils.forName( + WithExampleMetaAnnotation.class.getName(), classLoader); + Class exampleAnnotationClass = (Class) ClassUtils.forName( + ExampleAnnotation.class.getName(), classLoader); + Class exampleMetaAnnotationClass = (Class) ClassUtils.forName( + ExampleMetaAnnotation.class.getName(), classLoader); + assertThat(AnnotatedElementUtils.getMergedAnnotationAttributes( + withExampleMetaAnnotation, exampleAnnotationClass)).isNull(); + assertThat(AnnotatedElementUtils.getMergedAnnotationAttributes( + withExampleMetaAnnotation, exampleMetaAnnotationClass)).isNull(); + assertThat(AnnotatedElementUtils.hasAnnotation(withExampleMetaAnnotation, + exampleAnnotationClass)).isFalse(); + assertThat(AnnotatedElementUtils.hasAnnotation(withExampleMetaAnnotation, + exampleMetaAnnotationClass)).isFalse(); + } + + @Test + @SuppressWarnings("unchecked") + void filteredTypeInMetaAnnotationWhenUsingMergedAnnotationsHandlesException() throws Exception { + FilteringClassLoader classLoader = new FilteringClassLoader( + getClass().getClassLoader()); + Class withExampleMetaAnnotation = ClassUtils.forName( + WithExampleMetaAnnotation.class.getName(), classLoader); + Class exampleAnnotationClass = (Class) ClassUtils.forName( + ExampleAnnotation.class.getName(), classLoader); + Class exampleMetaAnnotationClass = (Class) ClassUtils.forName( + ExampleMetaAnnotation.class.getName(), classLoader); + MergedAnnotations annotations = MergedAnnotations.from(withExampleMetaAnnotation); + assertThat(annotations.get(exampleAnnotationClass).isPresent()).isFalse(); + assertThat(annotations.get(exampleMetaAnnotationClass).isPresent()).isFalse(); + assertThat(annotations.isPresent(exampleMetaAnnotationClass)).isFalse(); + assertThat(annotations.isPresent(exampleAnnotationClass)).isFalse(); + } + + + static class FilteringClassLoader extends OverridingClassLoader { + + FilteringClassLoader(ClassLoader parent) { + super(parent); + } + + @Override + protected boolean isEligibleForOverriding(String className) { + return className.startsWith( + AnnotationIntrospectionFailureTests.class.getName()); + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (name.startsWith(AnnotationIntrospectionFailureTests.class.getName()) && + name.contains("Filtered")) { + throw new ClassNotFoundException(name); + } + return super.loadClass(name, resolve); + } + } + + static class FilteredType { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ExampleAnnotation { + + Class value() default Void.class; + } + + @ExampleAnnotation(FilteredType.class) + static class WithExampleAnnotation { + } + + @Retention(RetentionPolicy.RUNTIME) + @ExampleAnnotation + @interface ExampleMetaAnnotation { + + @AliasFor(annotation = ExampleAnnotation.class, attribute = "value") + Class example1() default Void.class; + + @AliasFor(annotation = ExampleAnnotation.class, attribute = "value") + Class example2() default Void.class; + + } + + @ExampleMetaAnnotation(example1 = FilteredType.class) + static class WithExampleMetaAnnotation { + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationTypeMappingsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationTypeMappingsTests.java new file mode 100644 index 0000000..a6baf3a --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationTypeMappingsTests.java @@ -0,0 +1,950 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.IntStream; + +import javax.annotation.Nullable; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.annotation.AnnotationTypeMapping.MirrorSets; +import org.springframework.core.annotation.AnnotationTypeMapping.MirrorSets.MirrorSet; +import org.springframework.lang.UsesSunMisc; +import org.springframework.util.ReflectionUtils; + +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link AnnotationTypeMappings} and {@link AnnotationTypeMapping}. + * + * @author Phillip Webb + * @author Sam Brannen + */ +class AnnotationTypeMappingsTests { + + @Test + void forAnnotationTypeWhenNoMetaAnnotationsReturnsMappings() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(SimpleAnnotation.class); + assertThat(mappings.size()).isEqualTo(1); + assertThat(mappings.get(0).getAnnotationType()).isEqualTo(SimpleAnnotation.class); + assertThat(getAll(mappings)).flatExtracting( + AnnotationTypeMapping::getAnnotationType).containsExactly(SimpleAnnotation.class); + } + + @Test + void forAnnotationWhenHasSpringAnnotationReturnsFilteredMappings() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(WithSpringLangAnnotation.class); + assertThat(mappings.size()).isEqualTo(1); + } + + @Test + void forAnnotationTypeWhenMetaAnnotationsReturnsMappings() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(MetaAnnotated.class); + assertThat(mappings.size()).isEqualTo(6); + assertThat(getAll(mappings)).flatExtracting( + AnnotationTypeMapping::getAnnotationType).containsExactly( + MetaAnnotated.class, A.class, B.class, AA.class, AB.class, + ABC.class); + } + + @Test + void forAnnotationTypeWhenHasRepeatingMetaAnnotationReturnsMapping() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(WithRepeatedMetaAnnotations.class); + assertThat(mappings.size()).isEqualTo(3); + assertThat(getAll(mappings)).flatExtracting( + AnnotationTypeMapping::getAnnotationType).containsExactly( + WithRepeatedMetaAnnotations.class, Repeating.class, Repeating.class); + } + + @Test + void forAnnotationTypeWhenRepeatableMetaAnnotationIsFiltered() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(WithRepeatedMetaAnnotations.class, + Repeating.class.getName()::equals); + assertThat(getAll(mappings)).flatExtracting(AnnotationTypeMapping::getAnnotationType) + .containsExactly(WithRepeatedMetaAnnotations.class); + } + + @Test + void forAnnotationTypeWhenSelfAnnotatedReturnsMapping() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(SelfAnnotated.class); + assertThat(mappings.size()).isEqualTo(1); + assertThat(getAll(mappings)).flatExtracting( + AnnotationTypeMapping::getAnnotationType).containsExactly(SelfAnnotated.class); + } + + @Test + void forAnnotationTypeWhenFormsLoopReturnsMapping() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(LoopA.class); + assertThat(mappings.size()).isEqualTo(2); + assertThat(getAll(mappings)).flatExtracting( + AnnotationTypeMapping::getAnnotationType).containsExactly(LoopA.class, LoopB.class); + } + + @Test + void forAnnotationTypeWhenHasAliasForWithBothValueAndAttributeThrowsException() { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + AnnotationTypeMappings.forAnnotationType(AliasForWithBothValueAndAttribute.class)) + .withMessage("In @AliasFor declared on attribute 'test' in annotation [" + + AliasForWithBothValueAndAttribute.class.getName() + + "], attribute 'attribute' and its alias 'value' are present with values of 'foo' and 'bar', but only one is permitted."); + } + + @Test + void forAnnotationTypeWhenAliasForToSelfNonExistingAttribute() { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + AnnotationTypeMappings.forAnnotationType(AliasForToSelfNonExistingAttribute.class)) + .withMessage("@AliasFor declaration on attribute 'test' in annotation [" + + AliasForToSelfNonExistingAttribute.class.getName() + + "] declares an alias for 'missing' which is not present."); + } + + @Test + void forAnnotationTypeWhenAliasForToOtherNonExistingAttribute() { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + AnnotationTypeMappings.forAnnotationType(AliasForToOtherNonExistingAttribute.class)) + .withMessage("Attribute 'test' in annotation [" + + AliasForToOtherNonExistingAttribute.class.getName() + + "] is declared as an @AliasFor nonexistent " + + "attribute 'missing' in annotation [" + + AliasForToOtherNonExistingAttributeTarget.class.getName() + + "]."); + } + + @Test + void forAnnotationTypeWhenAliasForToSelf() { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + AnnotationTypeMappings.forAnnotationType(AliasForToSelf.class)) + .withMessage("@AliasFor declaration on attribute 'test' in annotation [" + + AliasForToSelf.class.getName() + + "] points to itself. Specify 'annotation' to point to " + + "a same-named attribute on a meta-annotation."); + } + + @Test + void forAnnotationTypeWhenAliasForWithArrayCompatibleReturnTypes() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( + AliasForWithArrayCompatibleReturnTypes.class); + AnnotationTypeMapping mapping = getMapping(mappings, + AliasForWithArrayCompatibleReturnTypesTarget.class); + assertThat(getAliasMapping(mapping, 0).getName()).isEqualTo("test"); + } + + @Test + void forAnnotationTypeWhenAliasForWithIncompatibleReturnTypes() { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + AnnotationTypeMappings.forAnnotationType(AliasForWithIncompatibleReturnTypes.class)) + .withMessage("Misconfigured aliases: attribute 'test' in annotation [" + + AliasForWithIncompatibleReturnTypes.class.getName() + + "] and attribute 'test' in annotation [" + + AliasForWithIncompatibleReturnTypesTarget.class.getName() + + "] must declare the same return type."); + } + + @Test + void forAnnotationTypeWhenAliasForToSelfAnnotatedToOtherAttribute() { + String annotationType = AliasForToSelfAnnotatedToOtherAttribute.class.getName(); + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForToSelfAnnotatedToOtherAttribute.class)) + .withMessage("Attribute 'b' in annotation [" + annotationType + + "] must be declared as an @AliasFor attribute 'a' in annotation [" + annotationType + + "], not attribute 'c' in annotation [" + annotationType + "]."); + } + + @Test + void forAnnotationTypeWhenAliasForHasMixedImplicitAndExplicitAliases() { + assertMixedImplicitAndExplicitAliases(AliasForWithMixedImplicitAndExplicitAliasesV1.class, "b"); + assertMixedImplicitAndExplicitAliases(AliasForWithMixedImplicitAndExplicitAliasesV2.class, "a"); + } + + private void assertMixedImplicitAndExplicitAliases(Class annotationType, String overriddenAttribute) { + String annotationName = annotationType.getName(); + String metaAnnotationName = AliasPair.class.getName(); + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(annotationType)) + .withMessage("Attribute 'b' in annotation [" + annotationName + + "] must be declared as an @AliasFor attribute 'a' in annotation [" + annotationName + + "], not attribute '" + overriddenAttribute + "' in annotation [" + metaAnnotationName + "]."); + } + + @Test + void forAnnotationTypeWhenAliasForNonMetaAnnotated() { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + AnnotationTypeMappings.forAnnotationType(AliasForNonMetaAnnotated.class)) + .withMessage("@AliasFor declaration on attribute 'test' in annotation [" + + AliasForNonMetaAnnotated.class.getName() + + "] declares an alias for attribute 'test' in annotation [" + + AliasForNonMetaAnnotatedTarget.class.getName() + + "] which is not meta-present."); + } + + @Test + void forAnnotationTypeWhenAliasForSelfWithDifferentDefaults() { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + AnnotationTypeMappings.forAnnotationType(AliasForSelfWithDifferentDefaults.class)) + .withMessage("Misconfigured aliases: attribute 'a' in annotation [" + + AliasForSelfWithDifferentDefaults.class.getName() + + "] and attribute 'b' in annotation [" + + AliasForSelfWithDifferentDefaults.class.getName() + + "] must declare the same default value."); + } + + @Test + void forAnnotationTypeWhenAliasForSelfWithMissingDefault() { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + AnnotationTypeMappings.forAnnotationType(AliasForSelfWithMissingDefault.class)) + .withMessage("Misconfigured aliases: attribute 'a' in annotation [" + + AliasForSelfWithMissingDefault.class.getName() + + "] and attribute 'b' in annotation [" + + AliasForSelfWithMissingDefault.class.getName() + + "] must declare default values."); + } + + @Test + void forAnnotationTypeWhenAliasWithExplicitMirrorAndDifferentDefaults() { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + AnnotationTypeMappings.forAnnotationType(AliasWithExplicitMirrorAndDifferentDefaults.class)) + .withMessage("Misconfigured aliases: attribute 'a' in annotation [" + + AliasWithExplicitMirrorAndDifferentDefaults.class.getName() + + "] and attribute 'c' in annotation [" + + AliasWithExplicitMirrorAndDifferentDefaults.class.getName() + + "] must declare the same default value."); + } + + @Test + void getDistanceReturnsDistance() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(Mapped.class); + assertThat(mappings.get(0).getDistance()).isEqualTo(0); + assertThat(mappings.get(1).getDistance()).isEqualTo(1); + } + + @Test + void getAnnotationTypeReturnsAnnotationType() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(Mapped.class); + assertThat(mappings.get(0).getAnnotationType()).isEqualTo(Mapped.class); + assertThat(mappings.get(1).getAnnotationType()).isEqualTo(MappedTarget.class); + } + + @Test + void getMetaTypeReturnsTypes() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(ThreeDeepA.class); + AnnotationTypeMapping mappingC = mappings.get(2); + assertThat(mappingC.getMetaTypes()).containsExactly(ThreeDeepA.class, ThreeDeepB.class, ThreeDeepC.class); + } + + @Test + void getAnnotationWhenRootReturnsNull() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(Mapped.class); + assertThat(mappings.get(0).getAnnotation()).isNull(); + } + + @Test + void getAnnotationWhenMetaAnnotationReturnsAnnotation() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(Mapped.class); + assertThat(mappings.get(1).getAnnotation()).isEqualTo(Mapped.class.getAnnotation(MappedTarget.class)); + } + + @Test + void getAttributesReturnsAttributes() { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(Mapped.class).get(0); + AttributeMethods attributes = mapping.getAttributes(); + assertThat(attributes.size()).isEqualTo(2); + assertThat(attributes.get(0).getName()).isEqualTo("alias"); + assertThat(attributes.get(1).getName()).isEqualTo("convention"); + } + + @Test + void getAliasMappingReturnsAttributes() throws Exception { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(Mapped.class).get(1); + assertThat(getAliasMapping(mapping, 0)).isEqualTo(Mapped.class.getDeclaredMethod("alias")); + } + + @Test + void getConventionMappingReturnsAttributes() throws Exception { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(Mapped.class).get(1); + assertThat(getConventionMapping(mapping, 1)).isEqualTo(Mapped.class.getDeclaredMethod("convention")); + } + + @Test + void getMirrorSetWhenAliasPairReturnsMirrors() { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(AliasPair.class).get(0); + MirrorSets mirrorSets = mapping.getMirrorSets(); + assertThat(mirrorSets.size()).isEqualTo(1); + assertThat(mirrorSets.get(0).size()).isEqualTo(2); + assertThat(mirrorSets.get(0).get(0).getName()).isEqualTo("a"); + assertThat(mirrorSets.get(0).get(1).getName()).isEqualTo("b"); + } + + @Test + void getMirrorSetWhenImplicitMirrorsReturnsMirrors() { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(ImplicitMirrors.class).get(0); + MirrorSets mirrorSets = mapping.getMirrorSets(); + assertThat(mirrorSets.size()).isEqualTo(1); + assertThat(mirrorSets.get(0).size()).isEqualTo(2); + assertThat(mirrorSets.get(0).get(0).getName()).isEqualTo("a"); + assertThat(mirrorSets.get(0).get(1).getName()).isEqualTo("b"); + } + + @Test + void getMirrorSetWhenThreeDeepReturnsMirrors() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(ThreeDeepA.class); + AnnotationTypeMapping mappingA = mappings.get(0); + MirrorSets mirrorSetsA = mappingA.getMirrorSets(); + assertThat(mirrorSetsA.size()).isEqualTo(2); + assertThat(getNames(mirrorSetsA.get(0))).containsExactly("a1", "a2", "a3"); + AnnotationTypeMapping mappingB = mappings.get(1); + MirrorSets mirrorSetsB = mappingB.getMirrorSets(); + assertThat(mirrorSetsB.size()).isEqualTo(1); + assertThat(getNames(mirrorSetsB.get(0))).containsExactly("b1", "b2"); + AnnotationTypeMapping mappingC = mappings.get(2); + MirrorSets mirrorSetsC = mappingC.getMirrorSets(); + assertThat(mirrorSetsC.size()).isEqualTo(0); + } + + @Test + void getAliasMappingWhenThreeDeepReturnsMappedAttributes() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(ThreeDeepA.class); + AnnotationTypeMapping mappingA = mappings.get(0); + assertThat(getAliasMapping(mappingA, 0)).isNull(); + assertThat(getAliasMapping(mappingA, 1)).isNull(); + assertThat(getAliasMapping(mappingA, 2)).isNull(); + assertThat(getAliasMapping(mappingA, 3)).isNull(); + assertThat(getAliasMapping(mappingA, 4)).isNull(); + AnnotationTypeMapping mappingB = mappings.get(1); + assertThat(getAliasMapping(mappingB, 0).getName()).isEqualTo("a1"); + assertThat(getAliasMapping(mappingB, 1).getName()).isEqualTo("a1"); + AnnotationTypeMapping mappingC = mappings.get(2); + assertThat(getAliasMapping(mappingC, 0).getName()).isEqualTo("a1"); + assertThat(getAliasMapping(mappingC, 1).getName()).isEqualTo("a4"); + } + + @Test + void getAliasMappingsWhenHasDefinedAttributesReturnsMappedAttributes() { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(DefinedAttributes.class).get(1); + assertThat(getAliasMapping(mapping, 0)).isNull(); + assertThat(getAliasMapping(mapping, 1).getName()).isEqualTo("value"); + } + + @Test + void resolveMirrorsWhenAliasPairResolves() { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(AliasPair.class).get(0); + Method[] resolvedA = resolveMirrorSets(mapping, WithAliasPairA.class, AliasPair.class); + assertThat(resolvedA[0].getName()).isEqualTo("a"); + assertThat(resolvedA[1].getName()).isEqualTo("a"); + Method[] resolvedB = resolveMirrorSets(mapping, WithAliasPairB.class, AliasPair.class); + assertThat(resolvedB[0].getName()).isEqualTo("b"); + assertThat(resolvedB[1].getName()).isEqualTo("b"); + } + + @Test + void resolveMirrorsWhenHasSameValuesUsesFirst() { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(AliasPair.class).get(0); + Method[] resolved = resolveMirrorSets(mapping, WithSameValueAliasPair.class, AliasPair.class); + assertThat(resolved[0].getName()).isEqualTo("a"); + assertThat(resolved[1].getName()).isEqualTo("a"); + } + + @Test + void resolveMirrorsWhenOnlyHasDefaultValuesUsesFirst() { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(AliasPair.class).get(0); + Method[] resolved = resolveMirrorSets(mapping, WithDefaultValueAliasPair.class, AliasPair.class); + assertThat(resolved[0].getName()).isEqualTo("a"); + assertThat(resolved[1].getName()).isEqualTo("a"); + } + + @Test + void resolveMirrorsWhenHasDifferentValuesThrowsException() { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(AliasPair.class).get(0); + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + resolveMirrorSets(mapping, WithDifferentValueAliasPair.class, AliasPair.class)) + .withMessage("Different @AliasFor mirror values for annotation [" + + AliasPair.class.getName() + "] declared on " + + WithDifferentValueAliasPair.class.getName() + + "; attribute 'a' and its alias 'b' are declared with values of [test1] and [test2]."); + } + + @Test + void resolveMirrorsWhenHasWithMultipleRoutesToAliasReturnsMirrors() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( + MultipleRoutesToAliasA.class); + AnnotationTypeMapping mappingsA = getMapping(mappings, MultipleRoutesToAliasA.class); + assertThat(mappingsA.getMirrorSets().size()).isZero(); + AnnotationTypeMapping mappingsB = getMapping(mappings, MultipleRoutesToAliasB.class); + assertThat(getNames(mappingsB.getMirrorSets().get(0))).containsExactly("b1", "b2", "b3"); + AnnotationTypeMapping mappingsC = getMapping(mappings, MultipleRoutesToAliasC.class); + assertThat(getNames(mappingsC.getMirrorSets().get(0))).containsExactly("c1", "c2"); + } + + @Test + void getAliasMappingWhenHasWithMultipleRoutesToAliasReturnsMappedAttributes() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( + MultipleRoutesToAliasA.class); + AnnotationTypeMapping mappingsA = getMapping(mappings, MultipleRoutesToAliasA.class); + assertThat(getAliasMapping(mappingsA, 0)).isNull(); + AnnotationTypeMapping mappingsB = getMapping(mappings, MultipleRoutesToAliasB.class); + assertThat(getAliasMapping(mappingsB, 0).getName()).isEqualTo("a1"); + assertThat(getAliasMapping(mappingsB, 1).getName()).isEqualTo("a1"); + assertThat(getAliasMapping(mappingsB, 2).getName()).isEqualTo("a1"); + AnnotationTypeMapping mappingsC = getMapping(mappings, MultipleRoutesToAliasC.class); + assertThat(getAliasMapping(mappingsC, 0).getName()).isEqualTo("a1"); + assertThat(getAliasMapping(mappingsC, 1).getName()).isEqualTo("a1"); + } + + @Test + void getConventionMappingWhenConventionToExplicitAliasesReturnsMappedAttributes() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(ConventionToExplicitAliases.class); + AnnotationTypeMapping mapping = getMapping(mappings, ConventionToExplicitAliasesTarget.class); + assertThat(mapping.getConventionMapping(0)).isEqualTo(0); + assertThat(mapping.getConventionMapping(1)).isEqualTo(0); + } + + @Test + void isEquivalentToDefaultValueWhenValueAndDefaultAreNullReturnsTrue() { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(ClassValue.class).get(0); + assertThat(mapping.isEquivalentToDefaultValue(0, null, ReflectionUtils::invokeMethod)).isTrue(); + } + + @Test + void isEquivalentToDefaultValueWhenValueAndDefaultMatchReturnsTrue() { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(ClassValueWithDefault.class).get(0); + assertThat(mapping.isEquivalentToDefaultValue(0, InputStream.class, ReflectionUtils::invokeMethod)).isTrue(); + } + + @Test + void isEquivalentToDefaultValueWhenClassAndStringNamesMatchReturnsTrue() { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(ClassValueWithDefault.class).get(0); + assertThat(mapping.isEquivalentToDefaultValue(0, "java.io.InputStream", ReflectionUtils::invokeMethod)).isTrue(); + } + + @Test + void isEquivalentToDefaultValueWhenClassArrayAndStringArrayNamesMatchReturnsTrue() { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(ClassArrayValueWithDefault.class).get(0); + assertThat(mapping.isEquivalentToDefaultValue(0, + new String[] { "java.io.InputStream", "java.io.OutputStream" }, + ReflectionUtils::invokeMethod)).isTrue(); + } + + @Test + void isEquivalentToDefaultValueWhenNestedAnnotationAndExtractedValuesMatchReturnsTrueAndValueSuppliedAsMap() { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(NestedValue.class).get(0); + Map value = Collections.singletonMap("value", "java.io.InputStream"); + assertThat(mapping.isEquivalentToDefaultValue(0, value, TypeMappedAnnotation::extractFromMap)).isTrue(); + } + + @Test // gh-24375 + void isEquivalentToDefaultValueWhenNestedAnnotationAndExtractedValuesMatchReturnsTrueAndValueSuppliedAsTypeMappedAnnotation() { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(NestedValue.class).get(0); + Map attributes = Collections.singletonMap("value", "java.io.InputStream"); + MergedAnnotation value = TypeMappedAnnotation.of(getClass().getClassLoader(), null, ClassValue.class, attributes); + assertThat(mapping.isEquivalentToDefaultValue(0, value, TypeMappedAnnotation::extractFromMap)).isTrue(); + } + + @Test + void isEquivalentToDefaultValueWhenNotMatchingReturnsFalse() { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(ClassValueWithDefault.class).get(0); + assertThat(mapping.isEquivalentToDefaultValue(0, OutputStream.class, ReflectionUtils::invokeMethod)).isFalse(); + } + + private Method[] resolveMirrorSets(AnnotationTypeMapping mapping, Class element, + Class annotationClass) { + Annotation annotation = element.getAnnotation(annotationClass); + int[] resolved = mapping.getMirrorSets().resolve(element.getName(), annotation, ReflectionUtils::invokeMethod); + Method[] result = new Method[resolved.length]; + for (int i = 0; i < resolved.length; i++) { + result[i] = resolved[i] != -1 ? mapping.getAttributes().get(resolved[i]) : null; + } + return result; + } + + @Nullable + private Method getAliasMapping(AnnotationTypeMapping mapping, int attributeIndex) { + int mapped = mapping.getAliasMapping(attributeIndex); + return mapped != -1 ? mapping.getRoot().getAttributes().get(mapped) : null; + } + + @Nullable + private Method getConventionMapping(AnnotationTypeMapping mapping, int attributeIndex) { + int mapped = mapping.getConventionMapping(attributeIndex); + return mapped != -1 ? mapping.getRoot().getAttributes().get(mapped) : null; + } + + private AnnotationTypeMapping getMapping(AnnotationTypeMappings mappings, + Class annotationType) { + + for (AnnotationTypeMapping candidate : getAll(mappings)) { + if (candidate.getAnnotationType() == annotationType) { + return candidate; + } + } + return null; + } + + private List getAll(AnnotationTypeMappings mappings) { + // AnnotationTypeMappings does not implement Iterable so we don't create + // too many garbage Iterators + return IntStream.range(0, mappings.size()).mapToObj(mappings::get).collect(toList()); + } + + private List getNames(MirrorSet mirrorSet) { + List names = new ArrayList<>(mirrorSet.size()); + for (int i = 0; i < mirrorSet.size(); i++) { + names.add(mirrorSet.get(i).getName()); + } + return names; + } + + + @Retention(RetentionPolicy.RUNTIME) + @interface SimpleAnnotation { + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @UsesSunMisc + @interface WithSpringLangAnnotation { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AA { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ABC { + } + + @Retention(RetentionPolicy.RUNTIME) + @ABC + @interface AB { + } + + @Retention(RetentionPolicy.RUNTIME) + @AA + @AB + @interface A { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface B { + } + + @Retention(RetentionPolicy.RUNTIME) + @A + @B + @interface MetaAnnotated { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface Repeatings { + + Repeating[] value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Repeatable(Repeatings.class) + @interface Repeating { + } + + @Retention(RetentionPolicy.RUNTIME) + @Repeating + @Repeating + @interface WithRepeatedMetaAnnotations { + } + + @Retention(RetentionPolicy.RUNTIME) + @SelfAnnotated + @interface SelfAnnotated { + } + + @Retention(RetentionPolicy.RUNTIME) + @LoopB + @interface LoopA { + } + + @Retention(RetentionPolicy.RUNTIME) + @LoopA + @interface LoopB { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForWithBothValueAndAttribute { + + @AliasFor(value = "bar", attribute = "foo") + String test(); + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForToSelfNonExistingAttribute { + + @AliasFor("missing") + String test() default ""; + + String other() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForToOtherNonExistingAttributeTarget { + + String other() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @AliasForToOtherNonExistingAttributeTarget + @interface AliasForToOtherNonExistingAttribute { + + @AliasFor(annotation = AliasForToOtherNonExistingAttributeTarget.class, attribute = "missing") + String test() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForToSelf { + + @AliasFor("test") + String test() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForWithArrayCompatibleReturnTypesTarget { + + String[] test() default {}; + } + + @Retention(RetentionPolicy.RUNTIME) + @AliasForWithArrayCompatibleReturnTypesTarget + @interface AliasForWithArrayCompatibleReturnTypes { + + @AliasFor(annotation = AliasForWithArrayCompatibleReturnTypesTarget.class) + String test() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForWithIncompatibleReturnTypesTarget { + + String test() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForWithIncompatibleReturnTypes { + + @AliasFor(annotation = AliasForWithIncompatibleReturnTypesTarget.class) + String[] test() default {}; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForToSelfAnnotatedToOtherAttribute { + + @AliasFor("b") + String a() default ""; + + @AliasFor("c") + String b() default ""; + + @AliasFor("a") + String c() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasPair { + + @AliasFor("b") + String a() default ""; + + @AliasFor("a") + String b() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @AliasPair + @interface AliasForWithMixedImplicitAndExplicitAliasesV1 { + + // attempted implicit alias via attribute override + @AliasFor(annotation = AliasPair.class, attribute = "b") + String b() default ""; + + // explicit local alias + @AliasFor("b") + String a() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @AliasPair + @interface AliasForWithMixedImplicitAndExplicitAliasesV2 { + + // attempted implicit alias via attribute override + @AliasFor(annotation = AliasPair.class, attribute = "a") + String b() default ""; + + // explicit local alias + @AliasFor("b") + String a() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForNonMetaAnnotated { + + @AliasFor(annotation = AliasForNonMetaAnnotatedTarget.class) + String test() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForNonMetaAnnotatedTarget { + + String test() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForSelfWithDifferentDefaults { + + @AliasFor("b") + String a() default "a"; + + @AliasFor("a") + String b() default "b"; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForSelfWithMissingDefault { + + @AliasFor("b") + String a() default "a"; + + @AliasFor("a") + String b(); + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasWithExplicitMirrorAndDifferentDefaultsTarget { + + String a() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @AliasWithExplicitMirrorAndDifferentDefaultsTarget + @interface AliasWithExplicitMirrorAndDifferentDefaults { + + @AliasFor(annotation = AliasWithExplicitMirrorAndDifferentDefaultsTarget.class, attribute = "a") + String a() default "x"; + + @AliasFor(annotation = AliasWithExplicitMirrorAndDifferentDefaultsTarget.class, attribute = "a") + String b() default "x"; + + @AliasFor(annotation = AliasWithExplicitMirrorAndDifferentDefaultsTarget.class, attribute = "a") + String c() default "y"; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface MappedTarget { + + String convention() default ""; + + String aliasTarget() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @MappedTarget + @interface Mapped { + + String convention() default ""; + + @AliasFor(annotation = MappedTarget.class, attribute = "aliasTarget") + String alias() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ImplicitMirrorsTarget { + + @AliasFor("d") + String c() default ""; + + @AliasFor("c") + String d() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @ImplicitMirrorsTarget + @interface ImplicitMirrors { + + @AliasFor(annotation = ImplicitMirrorsTarget.class, attribute = "c") + String a() default ""; + + @AliasFor(annotation = ImplicitMirrorsTarget.class, attribute = "c") + String b() default ""; + } + + + @Retention(RetentionPolicy.RUNTIME) + @interface ThreeDeepC { + + String c1() default ""; + + String c2() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @ThreeDeepC + @interface ThreeDeepB { + + @AliasFor(annotation = ThreeDeepC.class, attribute = "c1") + String b1() default ""; + + @AliasFor(annotation = ThreeDeepC.class, attribute = "c1") + String b2() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @ThreeDeepB + @interface ThreeDeepA { + + @AliasFor(annotation = ThreeDeepB.class, attribute = "b1") + String a1() default ""; + + @AliasFor(annotation = ThreeDeepB.class, attribute = "b2") + String a2() default ""; + + @AliasFor(annotation = ThreeDeepC.class, attribute = "c1") + String a3() default ""; + + @AliasFor(annotation = ThreeDeepC.class, attribute = "c2") + String a4() default ""; + + @AliasFor(annotation = ThreeDeepC.class, attribute = "c2") + String a5() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface DefinedAttributesTarget { + + String a(); + + String b() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @DefinedAttributesTarget(a = "test") + @interface DefinedAttributes { + + @AliasFor(annotation = DefinedAttributesTarget.class, attribute = "b") + String value(); + } + + @AliasPair(a = "test") + static class WithAliasPairA { + } + + @AliasPair(b = "test") + static class WithAliasPairB { + } + + @AliasPair(a = "test", b = "test") + static class WithSameValueAliasPair { + } + + @AliasPair(a = "test1", b = "test2") + static class WithDifferentValueAliasPair { + } + + @AliasPair + static class WithDefaultValueAliasPair { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface MultipleRoutesToAliasC { + + @AliasFor("c2") + String c1() default ""; + + @AliasFor("c1") + String c2() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @MultipleRoutesToAliasC + @interface MultipleRoutesToAliasB { + + @AliasFor(annotation = MultipleRoutesToAliasC.class, attribute = "c2") + String b1() default ""; + + @AliasFor(annotation = MultipleRoutesToAliasC.class, attribute = "c2") + String b2() default ""; + + @AliasFor(annotation = MultipleRoutesToAliasC.class, attribute = "c1") + String b3() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @MultipleRoutesToAliasB + @interface MultipleRoutesToAliasA { + + @AliasFor(annotation = MultipleRoutesToAliasB.class, attribute = "b2") + String a1() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ConventionToExplicitAliasesTarget { + + @AliasFor("test") + String value() default ""; + + @AliasFor("value") + String test() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @ConventionToExplicitAliasesTarget + @interface ConventionToExplicitAliases { + + String test() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ClassValue { + + Class value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ClassValueWithDefault { + + Class value() default InputStream.class; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ClassArrayValueWithDefault { + + Class[] value() default { InputStream.class, OutputStream.class }; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface NestedValue { + + ClassValue value() default @ClassValue(InputStream.class); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java new file mode 100644 index 0000000..556c7c6 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java @@ -0,0 +1,1836 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; + +import javax.annotation.Nonnull; +import javax.annotation.ParametersAreNonnullByDefault; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.core.Ordered; +import org.springframework.core.annotation.subpackage.NonPublicAnnotatedClass; +import org.springframework.core.testfixture.stereotype.Component; +import org.springframework.lang.NonNullApi; + +import static java.util.Arrays.asList; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.springframework.core.annotation.AnnotationUtils.VALUE; +import static org.springframework.core.annotation.AnnotationUtils.findAnnotation; +import static org.springframework.core.annotation.AnnotationUtils.findAnnotationDeclaringClass; +import static org.springframework.core.annotation.AnnotationUtils.findAnnotationDeclaringClassForTypes; +import static org.springframework.core.annotation.AnnotationUtils.getAnnotation; +import static org.springframework.core.annotation.AnnotationUtils.getAnnotationAttributes; +import static org.springframework.core.annotation.AnnotationUtils.getDeclaredRepeatableAnnotations; +import static org.springframework.core.annotation.AnnotationUtils.getDefaultValue; +import static org.springframework.core.annotation.AnnotationUtils.getRepeatableAnnotations; +import static org.springframework.core.annotation.AnnotationUtils.getValue; +import static org.springframework.core.annotation.AnnotationUtils.isAnnotationDeclaredLocally; +import static org.springframework.core.annotation.AnnotationUtils.isAnnotationInherited; +import static org.springframework.core.annotation.AnnotationUtils.isAnnotationMetaPresent; +import static org.springframework.core.annotation.AnnotationUtils.synthesizeAnnotation; + +/** + * Unit tests for {@link AnnotationUtils}. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Sam Brannen + * @author Chris Beams + * @author Phillip Webb + * @author Oleg Zhurakousky + */ +@SuppressWarnings("deprecation") +class AnnotationUtilsTests { + + @BeforeEach + void clearCacheBeforeTests() { + AnnotationUtils.clearCache(); + } + + + @Test + void findMethodAnnotationOnLeaf() throws Exception { + Method m = Leaf.class.getMethod("annotatedOnLeaf"); + assertThat(m.getAnnotation(Order.class)).isNotNull(); + assertThat(getAnnotation(m, Order.class)).isNotNull(); + assertThat(findAnnotation(m, Order.class)).isNotNull(); + } + + // @since 4.2 + @Test + void findMethodAnnotationWithAnnotationOnMethodInInterface() throws Exception { + Method m = Leaf.class.getMethod("fromInterfaceImplementedByRoot"); + // @Order is not @Inherited + assertThat(m.getAnnotation(Order.class)).isNull(); + // getAnnotation() does not search on interfaces + assertThat(getAnnotation(m, Order.class)).isNull(); + // findAnnotation() does search on interfaces + assertThat(findAnnotation(m, Order.class)).isNotNull(); + } + + // @since 4.2 + @Test + void findMethodAnnotationWithMetaAnnotationOnLeaf() throws Exception { + Method m = Leaf.class.getMethod("metaAnnotatedOnLeaf"); + assertThat(m.getAnnotation(Order.class)).isNull(); + assertThat(getAnnotation(m, Order.class)).isNotNull(); + assertThat(findAnnotation(m, Order.class)).isNotNull(); + } + + // @since 4.2 + @Test + void findMethodAnnotationWithMetaMetaAnnotationOnLeaf() throws Exception { + Method m = Leaf.class.getMethod("metaMetaAnnotatedOnLeaf"); + assertThat(m.getAnnotation(Component.class)).isNull(); + assertThat(getAnnotation(m, Component.class)).isNull(); + assertThat(findAnnotation(m, Component.class)).isNotNull(); + } + + @Test + void findMethodAnnotationOnRoot() throws Exception { + Method m = Leaf.class.getMethod("annotatedOnRoot"); + assertThat(m.getAnnotation(Order.class)).isNotNull(); + assertThat(getAnnotation(m, Order.class)).isNotNull(); + assertThat(findAnnotation(m, Order.class)).isNotNull(); + } + + // @since 4.2 + @Test + void findMethodAnnotationWithMetaAnnotationOnRoot() throws Exception { + Method m = Leaf.class.getMethod("metaAnnotatedOnRoot"); + assertThat(m.getAnnotation(Order.class)).isNull(); + assertThat(getAnnotation(m, Order.class)).isNotNull(); + assertThat(findAnnotation(m, Order.class)).isNotNull(); + } + + @Test + void findMethodAnnotationOnRootButOverridden() throws Exception { + Method m = Leaf.class.getMethod("overrideWithoutNewAnnotation"); + assertThat(m.getAnnotation(Order.class)).isNull(); + assertThat(getAnnotation(m, Order.class)).isNull(); + assertThat(findAnnotation(m, Order.class)).isNotNull(); + } + + @Test + void findMethodAnnotationNotAnnotated() throws Exception { + Method m = Leaf.class.getMethod("notAnnotated"); + assertThat(findAnnotation(m, Order.class)).isNull(); + } + + @Test + void findMethodAnnotationOnBridgeMethod() throws Exception { + Method bridgeMethod = SimpleFoo.class.getMethod("something", Object.class); + assertThat(bridgeMethod.isBridge()).isTrue(); + + assertThat(bridgeMethod.getAnnotation(Order.class)).isNull(); + assertThat(getAnnotation(bridgeMethod, Order.class)).isNull(); + assertThat(findAnnotation(bridgeMethod, Order.class)).isNotNull(); + + boolean runningInEclipse = Arrays.stream(new Exception().getStackTrace()) + .anyMatch(element -> element.getClassName().startsWith("org.eclipse.jdt")); + + // As of JDK 8, invoking getAnnotation() on a bridge method actually finds an + // annotation on its 'bridged' method [1]; however, the Eclipse compiler will not + // support this until Eclipse 4.9 [2]. Thus, we effectively ignore the following + // assertion if the test is currently executing within the Eclipse IDE. + // + // [1] https://bugs.openjdk.java.net/browse/JDK-6695379 + // [2] https://bugs.eclipse.org/bugs/show_bug.cgi?id=495396 + // + if (!runningInEclipse) { + assertThat(bridgeMethod.getAnnotation(Transactional.class)).isNotNull(); + } + assertThat(getAnnotation(bridgeMethod, Transactional.class)).isNotNull(); + assertThat(findAnnotation(bridgeMethod, Transactional.class)).isNotNull(); + } + + @Test + void findMethodAnnotationOnBridgedMethod() throws Exception { + Method bridgedMethod = SimpleFoo.class.getMethod("something", String.class); + assertThat(bridgedMethod.isBridge()).isFalse(); + + assertThat(bridgedMethod.getAnnotation(Order.class)).isNull(); + assertThat(getAnnotation(bridgedMethod, Order.class)).isNull(); + assertThat(findAnnotation(bridgedMethod, Order.class)).isNotNull(); + + assertThat(bridgedMethod.getAnnotation(Transactional.class)).isNotNull(); + assertThat(getAnnotation(bridgedMethod, Transactional.class)).isNotNull(); + assertThat(findAnnotation(bridgedMethod, Transactional.class)).isNotNull(); + } + + @Test + void findMethodAnnotationFromInterface() throws Exception { + Method method = ImplementsInterfaceWithAnnotatedMethod.class.getMethod("foo"); + Order order = findAnnotation(method, Order.class); + assertThat(order).isNotNull(); + } + + @Test // SPR-16060 + void findMethodAnnotationFromGenericInterface() throws Exception { + Method method = ImplementsInterfaceWithGenericAnnotatedMethod.class.getMethod("foo", String.class); + Order order = findAnnotation(method, Order.class); + assertThat(order).isNotNull(); + } + + @Test // SPR-17146 + void findMethodAnnotationFromGenericSuperclass() throws Exception { + Method method = ExtendsBaseClassWithGenericAnnotatedMethod.class.getMethod("foo", String.class); + Order order = findAnnotation(method, Order.class); + assertThat(order).isNotNull(); + } + + @Test + void findMethodAnnotationFromInterfaceOnSuper() throws Exception { + Method method = SubOfImplementsInterfaceWithAnnotatedMethod.class.getMethod("foo"); + Order order = findAnnotation(method, Order.class); + assertThat(order).isNotNull(); + } + + @Test + void findMethodAnnotationFromInterfaceWhenSuperDoesNotImplementMethod() throws Exception { + Method method = SubOfAbstractImplementsInterfaceWithAnnotatedMethod.class.getMethod("foo"); + Order order = findAnnotation(method, Order.class); + assertThat(order).isNotNull(); + } + + // @since 4.1.2 + @Test + void findClassAnnotationFavorsMoreLocallyDeclaredComposedAnnotationsOverAnnotationsOnInterfaces() { + Component component = findAnnotation(ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class, Component.class); + assertThat(component).isNotNull(); + assertThat(component.value()).isEqualTo("meta2"); + } + + // @since 4.0.3 + @Test + void findClassAnnotationFavorsMoreLocallyDeclaredComposedAnnotationsOverInheritedAnnotations() { + Transactional transactional = findAnnotation(SubSubClassWithInheritedAnnotation.class, Transactional.class); + assertThat(transactional).isNotNull(); + assertThat(transactional.readOnly()).as("readOnly flag for SubSubClassWithInheritedAnnotation").isTrue(); + } + + // @since 4.0.3 + @Test + void findClassAnnotationFavorsMoreLocallyDeclaredComposedAnnotationsOverInheritedComposedAnnotations() { + Component component = findAnnotation(SubSubClassWithInheritedMetaAnnotation.class, Component.class); + assertThat(component).isNotNull(); + assertThat(component.value()).isEqualTo("meta2"); + } + + @Test + void findClassAnnotationOnMetaMetaAnnotatedClass() { + Component component = findAnnotation(MetaMetaAnnotatedClass.class, Component.class); + assertThat(component).as("Should find meta-annotation on composed annotation on class").isNotNull(); + assertThat(component.value()).isEqualTo("meta2"); + } + + @Test + void findClassAnnotationOnMetaMetaMetaAnnotatedClass() { + Component component = findAnnotation(MetaMetaMetaAnnotatedClass.class, Component.class); + assertThat(component).as("Should find meta-annotation on meta-annotation on composed annotation on class").isNotNull(); + assertThat(component.value()).isEqualTo("meta2"); + } + + @Test + void findClassAnnotationOnAnnotatedClassWithMissingTargetMetaAnnotation() { + // TransactionalClass is NOT annotated or meta-annotated with @Component + Component component = findAnnotation(TransactionalClass.class, Component.class); + assertThat(component).as("Should not find @Component on TransactionalClass").isNull(); + } + + @Test + void findClassAnnotationOnMetaCycleAnnotatedClassWithMissingTargetMetaAnnotation() { + Component component = findAnnotation(MetaCycleAnnotatedClass.class, Component.class); + assertThat(component).as("Should not find @Component on MetaCycleAnnotatedClass").isNull(); + } + + // @since 4.2 + @Test + void findClassAnnotationOnInheritedAnnotationInterface() { + Transactional tx = findAnnotation(InheritedAnnotationInterface.class, Transactional.class); + assertThat(tx).as("Should find @Transactional on InheritedAnnotationInterface").isNotNull(); + } + + // @since 4.2 + @Test + void findClassAnnotationOnSubInheritedAnnotationInterface() { + Transactional tx = findAnnotation(SubInheritedAnnotationInterface.class, Transactional.class); + assertThat(tx).as("Should find @Transactional on SubInheritedAnnotationInterface").isNotNull(); + } + + // @since 4.2 + @Test + void findClassAnnotationOnSubSubInheritedAnnotationInterface() { + Transactional tx = findAnnotation(SubSubInheritedAnnotationInterface.class, Transactional.class); + assertThat(tx).as("Should find @Transactional on SubSubInheritedAnnotationInterface").isNotNull(); + } + + // @since 4.2 + @Test + void findClassAnnotationOnNonInheritedAnnotationInterface() { + Order order = findAnnotation(NonInheritedAnnotationInterface.class, Order.class); + assertThat(order).as("Should find @Order on NonInheritedAnnotationInterface").isNotNull(); + } + + // @since 4.2 + @Test + void findClassAnnotationOnSubNonInheritedAnnotationInterface() { + Order order = findAnnotation(SubNonInheritedAnnotationInterface.class, Order.class); + assertThat(order).as("Should find @Order on SubNonInheritedAnnotationInterface").isNotNull(); + } + + // @since 4.2 + @Test + void findClassAnnotationOnSubSubNonInheritedAnnotationInterface() { + Order order = findAnnotation(SubSubNonInheritedAnnotationInterface.class, Order.class); + assertThat(order).as("Should find @Order on SubSubNonInheritedAnnotationInterface").isNotNull(); + } + + @Test + void findAnnotationDeclaringClassForAllScenarios() { + // no class-level annotation + assertThat((Object) findAnnotationDeclaringClass(Transactional.class, NonAnnotatedInterface.class)).isNull(); + assertThat((Object) findAnnotationDeclaringClass(Transactional.class, NonAnnotatedClass.class)).isNull(); + + // inherited class-level annotation; note: @Transactional is inherited + assertThat(findAnnotationDeclaringClass(Transactional.class, InheritedAnnotationInterface.class)).isEqualTo(InheritedAnnotationInterface.class); + assertThat(findAnnotationDeclaringClass(Transactional.class, SubInheritedAnnotationInterface.class)).isNull(); + assertThat(findAnnotationDeclaringClass(Transactional.class, InheritedAnnotationClass.class)).isEqualTo(InheritedAnnotationClass.class); + assertThat(findAnnotationDeclaringClass(Transactional.class, SubInheritedAnnotationClass.class)).isEqualTo(InheritedAnnotationClass.class); + + // non-inherited class-level annotation; note: @Order is not inherited, + // but findAnnotationDeclaringClass() should still find it on classes. + assertThat(findAnnotationDeclaringClass(Order.class, NonInheritedAnnotationInterface.class)).isEqualTo(NonInheritedAnnotationInterface.class); + assertThat(findAnnotationDeclaringClass(Order.class, SubNonInheritedAnnotationInterface.class)).isNull(); + assertThat(findAnnotationDeclaringClass(Order.class, NonInheritedAnnotationClass.class)).isEqualTo(NonInheritedAnnotationClass.class); + assertThat(findAnnotationDeclaringClass(Order.class, SubNonInheritedAnnotationClass.class)).isEqualTo(NonInheritedAnnotationClass.class); + } + + @Test + void findAnnotationDeclaringClassForTypesWithSingleCandidateType() { + // no class-level annotation + List> transactionalCandidateList = Collections.singletonList(Transactional.class); + assertThat((Object) findAnnotationDeclaringClassForTypes(transactionalCandidateList, NonAnnotatedInterface.class)).isNull(); + assertThat((Object) findAnnotationDeclaringClassForTypes(transactionalCandidateList, NonAnnotatedClass.class)).isNull(); + + // inherited class-level annotation; note: @Transactional is inherited + assertThat(findAnnotationDeclaringClassForTypes(transactionalCandidateList, InheritedAnnotationInterface.class)).isEqualTo(InheritedAnnotationInterface.class); + assertThat(findAnnotationDeclaringClassForTypes(transactionalCandidateList, SubInheritedAnnotationInterface.class)).isNull(); + assertThat(findAnnotationDeclaringClassForTypes(transactionalCandidateList, InheritedAnnotationClass.class)).isEqualTo(InheritedAnnotationClass.class); + assertThat(findAnnotationDeclaringClassForTypes(transactionalCandidateList, SubInheritedAnnotationClass.class)).isEqualTo(InheritedAnnotationClass.class); + + // non-inherited class-level annotation; note: @Order is not inherited, + // but findAnnotationDeclaringClassForTypes() should still find it on classes. + List> orderCandidateList = Collections.singletonList(Order.class); + assertThat(findAnnotationDeclaringClassForTypes(orderCandidateList, NonInheritedAnnotationInterface.class)).isEqualTo(NonInheritedAnnotationInterface.class); + assertThat(findAnnotationDeclaringClassForTypes(orderCandidateList, SubNonInheritedAnnotationInterface.class)).isNull(); + assertThat(findAnnotationDeclaringClassForTypes(orderCandidateList, NonInheritedAnnotationClass.class)).isEqualTo(NonInheritedAnnotationClass.class); + assertThat(findAnnotationDeclaringClassForTypes(orderCandidateList, SubNonInheritedAnnotationClass.class)).isEqualTo(NonInheritedAnnotationClass.class); + } + + @Test + void findAnnotationDeclaringClassForTypesWithMultipleCandidateTypes() { + List> candidates = asList(Transactional.class, Order.class); + + // no class-level annotation + assertThat((Object) findAnnotationDeclaringClassForTypes(candidates, NonAnnotatedInterface.class)).isNull(); + assertThat((Object) findAnnotationDeclaringClassForTypes(candidates, NonAnnotatedClass.class)).isNull(); + + // inherited class-level annotation; note: @Transactional is inherited + assertThat(findAnnotationDeclaringClassForTypes(candidates, InheritedAnnotationInterface.class)).isEqualTo(InheritedAnnotationInterface.class); + assertThat(findAnnotationDeclaringClassForTypes(candidates, SubInheritedAnnotationInterface.class)).isNull(); + assertThat(findAnnotationDeclaringClassForTypes(candidates, InheritedAnnotationClass.class)).isEqualTo(InheritedAnnotationClass.class); + assertThat(findAnnotationDeclaringClassForTypes(candidates, SubInheritedAnnotationClass.class)).isEqualTo(InheritedAnnotationClass.class); + + // non-inherited class-level annotation; note: @Order is not inherited, + // but findAnnotationDeclaringClassForTypes() should still find it on classes. + assertThat(findAnnotationDeclaringClassForTypes(candidates, NonInheritedAnnotationInterface.class)).isEqualTo(NonInheritedAnnotationInterface.class); + assertThat(findAnnotationDeclaringClassForTypes(candidates, SubNonInheritedAnnotationInterface.class)).isNull(); + assertThat(findAnnotationDeclaringClassForTypes(candidates, NonInheritedAnnotationClass.class)).isEqualTo(NonInheritedAnnotationClass.class); + assertThat(findAnnotationDeclaringClassForTypes(candidates, SubNonInheritedAnnotationClass.class)).isEqualTo(NonInheritedAnnotationClass.class); + + // class hierarchy mixed with @Transactional and @Order declarations + assertThat(findAnnotationDeclaringClassForTypes(candidates, TransactionalClass.class)).isEqualTo(TransactionalClass.class); + assertThat(findAnnotationDeclaringClassForTypes(candidates, TransactionalAndOrderedClass.class)).isEqualTo(TransactionalAndOrderedClass.class); + assertThat(findAnnotationDeclaringClassForTypes(candidates, SubTransactionalAndOrderedClass.class)).isEqualTo(TransactionalAndOrderedClass.class); + } + + @Test + void isAnnotationDeclaredLocallyForAllScenarios() { + // no class-level annotation + assertThat(isAnnotationDeclaredLocally(Transactional.class, NonAnnotatedInterface.class)).isFalse(); + assertThat(isAnnotationDeclaredLocally(Transactional.class, NonAnnotatedClass.class)).isFalse(); + + // inherited class-level annotation; note: @Transactional is inherited + assertThat(isAnnotationDeclaredLocally(Transactional.class, InheritedAnnotationInterface.class)).isTrue(); + assertThat(isAnnotationDeclaredLocally(Transactional.class, SubInheritedAnnotationInterface.class)).isFalse(); + assertThat(isAnnotationDeclaredLocally(Transactional.class, InheritedAnnotationClass.class)).isTrue(); + assertThat(isAnnotationDeclaredLocally(Transactional.class, SubInheritedAnnotationClass.class)).isFalse(); + + // non-inherited class-level annotation; note: @Order is not inherited + assertThat(isAnnotationDeclaredLocally(Order.class, NonInheritedAnnotationInterface.class)).isTrue(); + assertThat(isAnnotationDeclaredLocally(Order.class, SubNonInheritedAnnotationInterface.class)).isFalse(); + assertThat(isAnnotationDeclaredLocally(Order.class, NonInheritedAnnotationClass.class)).isTrue(); + assertThat(isAnnotationDeclaredLocally(Order.class, SubNonInheritedAnnotationClass.class)).isFalse(); + } + + @Test + void isAnnotationInheritedForAllScenarios() { + // no class-level annotation + assertThat(isAnnotationInherited(Transactional.class, NonAnnotatedInterface.class)).isFalse(); + assertThat(isAnnotationInherited(Transactional.class, NonAnnotatedClass.class)).isFalse(); + + // inherited class-level annotation; note: @Transactional is inherited + assertThat(isAnnotationInherited(Transactional.class, InheritedAnnotationInterface.class)).isFalse(); + // isAnnotationInherited() does not currently traverse interface hierarchies. + // Thus the following, though perhaps counter intuitive, must be false: + assertThat(isAnnotationInherited(Transactional.class, SubInheritedAnnotationInterface.class)).isFalse(); + assertThat(isAnnotationInherited(Transactional.class, InheritedAnnotationClass.class)).isFalse(); + assertThat(isAnnotationInherited(Transactional.class, SubInheritedAnnotationClass.class)).isTrue(); + + // non-inherited class-level annotation; note: @Order is not inherited + assertThat(isAnnotationInherited(Order.class, NonInheritedAnnotationInterface.class)).isFalse(); + assertThat(isAnnotationInherited(Order.class, SubNonInheritedAnnotationInterface.class)).isFalse(); + assertThat(isAnnotationInherited(Order.class, NonInheritedAnnotationClass.class)).isFalse(); + assertThat(isAnnotationInherited(Order.class, SubNonInheritedAnnotationClass.class)).isFalse(); + } + + @Test + void isAnnotationMetaPresentForPlainType() { + assertThat(isAnnotationMetaPresent(Order.class, Documented.class)).isTrue(); + assertThat(isAnnotationMetaPresent(NonNullApi.class, Documented.class)).isTrue(); + assertThat(isAnnotationMetaPresent(NonNullApi.class, Nonnull.class)).isTrue(); + assertThat(isAnnotationMetaPresent(ParametersAreNonnullByDefault.class, Nonnull.class)).isTrue(); + } + + @Test + void getAnnotationAttributesWithoutAttributeAliases() { + Component component = WebController.class.getAnnotation(Component.class); + assertThat(component).isNotNull(); + + AnnotationAttributes attributes = (AnnotationAttributes) getAnnotationAttributes(component); + assertThat(attributes).isNotNull(); + assertThat(attributes.getString(VALUE)).as("value attribute: ").isEqualTo("webController"); + assertThat(attributes.annotationType()).isEqualTo(Component.class); + } + + @Test + void getAnnotationAttributesWithNestedAnnotations() { + ComponentScan componentScan = ComponentScanClass.class.getAnnotation(ComponentScan.class); + assertThat(componentScan).isNotNull(); + + AnnotationAttributes attributes = getAnnotationAttributes(ComponentScanClass.class, componentScan); + assertThat(attributes).isNotNull(); + assertThat(attributes.annotationType()).isEqualTo(ComponentScan.class); + + Filter[] filters = attributes.getAnnotationArray("excludeFilters", Filter.class); + assertThat(filters).isNotNull(); + + List patterns = stream(filters).map(Filter::pattern).collect(toList()); + assertThat(patterns).isEqualTo(asList("*Foo", "*Bar")); + } + + @Test + void getAnnotationAttributesWithAttributeAliases() throws Exception { + Method method = WebController.class.getMethod("handleMappedWithValueAttribute"); + WebMapping webMapping = method.getAnnotation(WebMapping.class); + AnnotationAttributes attributes = (AnnotationAttributes) getAnnotationAttributes(webMapping); + assertThat(attributes).isNotNull(); + assertThat(attributes.annotationType()).isEqualTo(WebMapping.class); + assertThat(attributes.getString("name")).as("name attribute: ").isEqualTo("foo"); + assertThat(attributes.getStringArray(VALUE)).as("value attribute: ").isEqualTo(asArray("/test")); + assertThat(attributes.getStringArray("path")).as("path attribute: ").isEqualTo(asArray("/test")); + + method = WebController.class.getMethod("handleMappedWithPathAttribute"); + webMapping = method.getAnnotation(WebMapping.class); + attributes = (AnnotationAttributes) getAnnotationAttributes(webMapping); + assertThat(attributes).isNotNull(); + assertThat(attributes.annotationType()).isEqualTo(WebMapping.class); + assertThat(attributes.getString("name")).as("name attribute: ").isEqualTo("bar"); + assertThat(attributes.getStringArray(VALUE)).as("value attribute: ").isEqualTo(asArray("/test")); + assertThat(attributes.getStringArray("path")).as("path attribute: ").isEqualTo(asArray("/test")); + } + + @Test + void getAnnotationAttributesWithAttributeAliasesWithDifferentValues() throws Exception { + Method method = WebController.class.getMethod("handleMappedWithDifferentPathAndValueAttributes"); + WebMapping webMapping = method.getAnnotation(WebMapping.class); + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + getAnnotationAttributes(webMapping)) + .withMessageContaining("attribute 'path' and its alias 'value'") + .withMessageContaining("values of [{/test}] and [{/enigma}]"); + } + + @Test + void getValueFromAnnotation() throws Exception { + Method method = SimpleFoo.class.getMethod("something", Object.class); + Order order = findAnnotation(method, Order.class); + + assertThat(getValue(order, VALUE)).isEqualTo(1); + assertThat(getValue(order)).isEqualTo(1); + } + + @Test + void getValueFromNonPublicAnnotation() throws Exception { + Annotation[] declaredAnnotations = NonPublicAnnotatedClass.class.getDeclaredAnnotations(); + assertThat(declaredAnnotations.length).isEqualTo(1); + Annotation annotation = declaredAnnotations[0]; + assertThat(annotation).isNotNull(); + assertThat(annotation.annotationType().getSimpleName()).isEqualTo("NonPublicAnnotation"); + assertThat(getValue(annotation, VALUE)).isEqualTo(42); + assertThat(getValue(annotation)).isEqualTo(42); + } + + @Test + void getDefaultValueFromAnnotation() throws Exception { + Method method = SimpleFoo.class.getMethod("something", Object.class); + Order order = findAnnotation(method, Order.class); + + assertThat(getDefaultValue(order, VALUE)).isEqualTo(Ordered.LOWEST_PRECEDENCE); + assertThat(getDefaultValue(order)).isEqualTo(Ordered.LOWEST_PRECEDENCE); + } + + @Test + void getDefaultValueFromNonPublicAnnotation() { + Annotation[] declaredAnnotations = NonPublicAnnotatedClass.class.getDeclaredAnnotations(); + assertThat(declaredAnnotations.length).isEqualTo(1); + Annotation annotation = declaredAnnotations[0]; + assertThat(annotation).isNotNull(); + assertThat(annotation.annotationType().getSimpleName()).isEqualTo("NonPublicAnnotation"); + assertThat(getDefaultValue(annotation, VALUE)).isEqualTo(-1); + assertThat(getDefaultValue(annotation)).isEqualTo(-1); + } + + @Test + void getDefaultValueFromAnnotationType() { + assertThat(getDefaultValue(Order.class, VALUE)).isEqualTo(Ordered.LOWEST_PRECEDENCE); + assertThat(getDefaultValue(Order.class)).isEqualTo(Ordered.LOWEST_PRECEDENCE); + } + + @Test + void findRepeatableAnnotation() { + Repeatable repeatable = findAnnotation(MyRepeatable.class, Repeatable.class); + assertThat(repeatable).isNotNull(); + assertThat(repeatable.value()).isEqualTo(MyRepeatableContainer.class); + } + + @Test + void getRepeatableAnnotationsDeclaredOnMethod() throws Exception { + Method method = InterfaceWithRepeated.class.getMethod("foo"); + Set annotations = getRepeatableAnnotations(method, MyRepeatable.class, MyRepeatableContainer.class); + assertThat(annotations).isNotNull(); + List values = annotations.stream().map(MyRepeatable::value).collect(toList()); + assertThat(values).isEqualTo(asList("A", "B", "C", "meta1")); + } + + @Test + void getRepeatableAnnotationsDeclaredOnClassWithAttributeAliases() { + final List expectedLocations = asList("A", "B"); + + Set annotations = getRepeatableAnnotations(ConfigHierarchyTestCase.class, ContextConfig.class, null); + assertThat(annotations).isNotNull(); + assertThat(annotations.size()).as("size if container type is omitted: ").isEqualTo(0); + + annotations = getRepeatableAnnotations(ConfigHierarchyTestCase.class, ContextConfig.class, Hierarchy.class); + assertThat(annotations).isNotNull(); + + List locations = annotations.stream().map(ContextConfig::location).collect(toList()); + assertThat(locations).isEqualTo(expectedLocations); + + List values = annotations.stream().map(ContextConfig::value).collect(toList()); + assertThat(values).isEqualTo(expectedLocations); + } + + @Test + void getRepeatableAnnotationsDeclaredOnClass() { + final List expectedValuesJava = asList("A", "B", "C"); + final List expectedValuesSpring = asList("A", "B", "C", "meta1"); + + // Java 8 + MyRepeatable[] array = MyRepeatableClass.class.getAnnotationsByType(MyRepeatable.class); + assertThat(array).isNotNull(); + List values = stream(array).map(MyRepeatable::value).collect(toList()); + assertThat(values).isEqualTo(expectedValuesJava); + + // Spring + Set set = getRepeatableAnnotations(MyRepeatableClass.class, MyRepeatable.class, MyRepeatableContainer.class); + assertThat(set).isNotNull(); + values = set.stream().map(MyRepeatable::value).collect(toList()); + assertThat(values).isEqualTo(expectedValuesSpring); + + // When container type is omitted and therefore inferred from @Repeatable + set = getRepeatableAnnotations(MyRepeatableClass.class, MyRepeatable.class); + assertThat(set).isNotNull(); + values = set.stream().map(MyRepeatable::value).collect(toList()); + assertThat(values).isEqualTo(expectedValuesSpring); + } + + @Test + void getRepeatableAnnotationsDeclaredOnSuperclass() { + final Class clazz = SubMyRepeatableClass.class; + final List expectedValuesJava = asList("A", "B", "C"); + final List expectedValuesSpring = asList("A", "B", "C", "meta1"); + + // Java 8 + MyRepeatable[] array = clazz.getAnnotationsByType(MyRepeatable.class); + assertThat(array).isNotNull(); + List values = stream(array).map(MyRepeatable::value).collect(toList()); + assertThat(values).isEqualTo(expectedValuesJava); + + // Spring + Set set = getRepeatableAnnotations(clazz, MyRepeatable.class, MyRepeatableContainer.class); + assertThat(set).isNotNull(); + values = set.stream().map(MyRepeatable::value).collect(toList()); + assertThat(values).isEqualTo(expectedValuesSpring); + + // When container type is omitted and therefore inferred from @Repeatable + set = getRepeatableAnnotations(clazz, MyRepeatable.class); + assertThat(set).isNotNull(); + values = set.stream().map(MyRepeatable::value).collect(toList()); + assertThat(values).isEqualTo(expectedValuesSpring); + } + + @Test + void getRepeatableAnnotationsDeclaredOnClassAndSuperclass() { + final Class clazz = SubMyRepeatableWithAdditionalLocalDeclarationsClass.class; + final List expectedValuesJava = asList("X", "Y", "Z"); + final List expectedValuesSpring = asList("X", "Y", "Z", "meta2"); + + // Java 8 + MyRepeatable[] array = clazz.getAnnotationsByType(MyRepeatable.class); + assertThat(array).isNotNull(); + List values = stream(array).map(MyRepeatable::value).collect(toList()); + assertThat(values).isEqualTo(expectedValuesJava); + + // Spring + Set set = getRepeatableAnnotations(clazz, MyRepeatable.class, MyRepeatableContainer.class); + assertThat(set).isNotNull(); + values = set.stream().map(MyRepeatable::value).collect(toList()); + assertThat(values).isEqualTo(expectedValuesSpring); + + // When container type is omitted and therefore inferred from @Repeatable + set = getRepeatableAnnotations(clazz, MyRepeatable.class); + assertThat(set).isNotNull(); + values = set.stream().map(MyRepeatable::value).collect(toList()); + assertThat(values).isEqualTo(expectedValuesSpring); + } + + @Test + void getRepeatableAnnotationsDeclaredOnMultipleSuperclasses() { + final Class clazz = SubSubMyRepeatableWithAdditionalLocalDeclarationsClass.class; + final List expectedValuesJava = asList("X", "Y", "Z"); + final List expectedValuesSpring = asList("X", "Y", "Z", "meta2"); + + // Java 8 + MyRepeatable[] array = clazz.getAnnotationsByType(MyRepeatable.class); + assertThat(array).isNotNull(); + List values = stream(array).map(MyRepeatable::value).collect(toList()); + assertThat(values).isEqualTo(expectedValuesJava); + + // Spring + Set set = getRepeatableAnnotations(clazz, MyRepeatable.class, MyRepeatableContainer.class); + assertThat(set).isNotNull(); + values = set.stream().map(MyRepeatable::value).collect(toList()); + assertThat(values).isEqualTo(expectedValuesSpring); + + // When container type is omitted and therefore inferred from @Repeatable + set = getRepeatableAnnotations(clazz, MyRepeatable.class); + assertThat(set).isNotNull(); + values = set.stream().map(MyRepeatable::value).collect(toList()); + assertThat(values).isEqualTo(expectedValuesSpring); + } + + @Test + void getDeclaredRepeatableAnnotationsDeclaredOnClass() { + final List expectedValuesJava = asList("A", "B", "C"); + final List expectedValuesSpring = asList("A", "B", "C", "meta1"); + + // Java 8 + MyRepeatable[] array = MyRepeatableClass.class.getDeclaredAnnotationsByType(MyRepeatable.class); + assertThat(array).isNotNull(); + List values = stream(array).map(MyRepeatable::value).collect(toList()); + assertThat(values).isEqualTo(expectedValuesJava); + + // Spring + Set set = getDeclaredRepeatableAnnotations( + MyRepeatableClass.class, MyRepeatable.class, MyRepeatableContainer.class); + assertThat(set).isNotNull(); + values = set.stream().map(MyRepeatable::value).collect(toList()); + assertThat(values).isEqualTo(expectedValuesSpring); + + // When container type is omitted and therefore inferred from @Repeatable + set = getDeclaredRepeatableAnnotations(MyRepeatableClass.class, MyRepeatable.class); + assertThat(set).isNotNull(); + values = set.stream().map(MyRepeatable::value).collect(toList()); + assertThat(values).isEqualTo(expectedValuesSpring); + } + + @Test + void getDeclaredRepeatableAnnotationsDeclaredOnSuperclass() { + final Class clazz = SubMyRepeatableClass.class; + + // Java 8 + MyRepeatable[] array = clazz.getDeclaredAnnotationsByType(MyRepeatable.class); + assertThat(array).isNotNull(); + assertThat(array.length).isEqualTo(0); + + // Spring + Set set = getDeclaredRepeatableAnnotations(clazz, MyRepeatable.class, MyRepeatableContainer.class); + assertThat(set).isNotNull(); + assertThat(set).hasSize(0); + + // When container type is omitted and therefore inferred from @Repeatable + set = getDeclaredRepeatableAnnotations(clazz, MyRepeatable.class); + assertThat(set).isNotNull(); + assertThat(set).hasSize(0); + } + + @Test + void synthesizeAnnotationWithImplicitAliasesWithMissingDefaultValues() throws Exception { + Class clazz = ImplicitAliasesWithMissingDefaultValuesContextConfigClass.class; + Class annotationType = + ImplicitAliasesWithMissingDefaultValuesContextConfig.class; + ImplicitAliasesWithMissingDefaultValuesContextConfig config = clazz.getAnnotation(annotationType); + assertThat(config).isNotNull(); + + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + synthesizeAnnotation(config, clazz)) + .withMessageStartingWith("Misconfigured aliases:") + .withMessageContaining("attribute 'location1' in annotation [" + annotationType.getName() + "]") + .withMessageContaining("attribute 'location2' in annotation [" + annotationType.getName() + "]") + .withMessageContaining("default values"); + } + + @Test + void synthesizeAnnotationWithImplicitAliasesWithDifferentDefaultValues() throws Exception { + Class clazz = ImplicitAliasesWithDifferentDefaultValuesContextConfigClass.class; + Class annotationType = + ImplicitAliasesWithDifferentDefaultValuesContextConfig.class; + ImplicitAliasesWithDifferentDefaultValuesContextConfig config = clazz.getAnnotation(annotationType); + assertThat(config).isNotNull(); + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + synthesizeAnnotation(config, clazz)) + .withMessageStartingWith("Misconfigured aliases:") + .withMessageContaining("attribute 'location1' in annotation [" + annotationType.getName() + "]") + .withMessageContaining("attribute 'location2' in annotation [" + annotationType.getName() + "]") + .withMessageContaining("same default value"); + } + + @Test + void synthesizeAnnotationWithImplicitAliasesWithDuplicateValues() throws Exception { + Class clazz = ImplicitAliasesWithDuplicateValuesContextConfigClass.class; + Class annotationType = + ImplicitAliasesWithDuplicateValuesContextConfig.class; + ImplicitAliasesWithDuplicateValuesContextConfig config = clazz.getAnnotation(annotationType); + assertThat(config).isNotNull(); + + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + synthesizeAnnotation(config, clazz).location1()) + .withMessageStartingWith("Different @AliasFor mirror values") + .withMessageContaining(annotationType.getName()) + .withMessageContaining("declared on class") + .withMessageContaining(clazz.getName()) + .withMessageContaining("attribute 'location1' and its alias 'location2'") + .withMessageContaining("with values of [1] and [2]"); + } + + @Test + void synthesizeAnnotationFromMapWithoutAttributeAliases() throws Exception { + Component component = WebController.class.getAnnotation(Component.class); + assertThat(component).isNotNull(); + + Map map = Collections.singletonMap(VALUE, "webController"); + Component synthesizedComponent = synthesizeAnnotation(map, Component.class, WebController.class); + assertThat(synthesizedComponent).isNotNull(); + + assertThat(synthesizedComponent).isNotSameAs(component); + assertThat(component.value()).as("value from component: ").isEqualTo("webController"); + assertThat(synthesizedComponent.value()).as("value from synthesized component: ").isEqualTo("webController"); + } + + @Test + @SuppressWarnings("unchecked") + void synthesizeAnnotationFromMapWithNestedMap() throws Exception { + ComponentScanSingleFilter componentScan = + ComponentScanSingleFilterClass.class.getAnnotation(ComponentScanSingleFilter.class); + assertThat(componentScan).isNotNull(); + assertThat(componentScan.value().pattern()).as("value from ComponentScan: ").isEqualTo("*Foo"); + + AnnotationAttributes attributes = getAnnotationAttributes( + ComponentScanSingleFilterClass.class, componentScan, false, true); + assertThat(attributes).isNotNull(); + assertThat(attributes.annotationType()).isEqualTo(ComponentScanSingleFilter.class); + + Map filterMap = (Map) attributes.get("value"); + assertThat(filterMap).isNotNull(); + assertThat(filterMap.get("pattern")).isEqualTo("*Foo"); + + // Modify nested map + filterMap.put("pattern", "newFoo"); + filterMap.put("enigma", 42); + + ComponentScanSingleFilter synthesizedComponentScan = synthesizeAnnotation( + attributes, ComponentScanSingleFilter.class, ComponentScanSingleFilterClass.class); + assertThat(synthesizedComponentScan).isNotNull(); + + assertThat(synthesizedComponentScan).isNotSameAs(componentScan); + assertThat(synthesizedComponentScan.value().pattern()).as("value from synthesized ComponentScan: ").isEqualTo("newFoo"); + } + + @Test + @SuppressWarnings("unchecked") + void synthesizeAnnotationFromMapWithNestedArrayOfMaps() throws Exception { + ComponentScan componentScan = ComponentScanClass.class.getAnnotation(ComponentScan.class); + assertThat(componentScan).isNotNull(); + + AnnotationAttributes attributes = getAnnotationAttributes(ComponentScanClass.class, componentScan, false, true); + assertThat(attributes).isNotNull(); + assertThat(attributes.annotationType()).isEqualTo(ComponentScan.class); + + Map[] filters = (Map[]) attributes.get("excludeFilters"); + assertThat(filters).isNotNull(); + + List patterns = stream(filters).map(m -> (String) m.get("pattern")).collect(toList()); + assertThat(patterns).isEqualTo(asList("*Foo", "*Bar")); + + // Modify nested maps + filters[0].put("pattern", "newFoo"); + filters[0].put("enigma", 42); + filters[1].put("pattern", "newBar"); + filters[1].put("enigma", 42); + + ComponentScan synthesizedComponentScan = + synthesizeAnnotation(attributes, ComponentScan.class, ComponentScanClass.class); + assertThat(synthesizedComponentScan).isNotNull(); + + assertThat(synthesizedComponentScan).isNotSameAs(componentScan); + patterns = stream(synthesizedComponentScan.excludeFilters()).map(Filter::pattern).collect(toList()); + assertThat(patterns).isEqualTo(asList("newFoo", "newBar")); + } + + @Test + void synthesizeAnnotationFromDefaultsWithoutAttributeAliases() throws Exception { + AnnotationWithDefaults annotationWithDefaults = synthesizeAnnotation(AnnotationWithDefaults.class); + assertThat(annotationWithDefaults).isNotNull(); + assertThat(annotationWithDefaults.text()).as("text: ").isEqualTo("enigma"); + assertThat(annotationWithDefaults.predicate()).as("predicate: ").isTrue(); + assertThat(annotationWithDefaults.characters()).as("characters: ").isEqualTo(new char[] { 'a', 'b', 'c' }); + } + + @Test + void synthesizeAnnotationFromDefaultsWithAttributeAliases() throws Exception { + ContextConfig contextConfig = synthesizeAnnotation(ContextConfig.class); + assertThat(contextConfig).isNotNull(); + assertThat(contextConfig.value()).as("value: ").isEqualTo(""); + assertThat(contextConfig.location()).as("location: ").isEqualTo(""); + } + + @Test + void synthesizeAnnotationFromMapWithMinimalAttributesWithAttributeAliases() throws Exception { + Map map = Collections.singletonMap("location", "test.xml"); + ContextConfig contextConfig = synthesizeAnnotation(map, ContextConfig.class, null); + assertThat(contextConfig).isNotNull(); + assertThat(contextConfig.value()).as("value: ").isEqualTo("test.xml"); + assertThat(contextConfig.location()).as("location: ").isEqualTo("test.xml"); + } + + @Test + void synthesizeAnnotationFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements() throws Exception { + Map map = Collections.singletonMap("value", "/foo"); + Get get = synthesizeAnnotation(map, Get.class, null); + assertThat(get).isNotNull(); + assertThat(get.value()).as("value: ").isEqualTo("/foo"); + assertThat(get.path()).as("path: ").isEqualTo("/foo"); + + map = Collections.singletonMap("path", "/foo"); + get = synthesizeAnnotation(map, Get.class, null); + assertThat(get).isNotNull(); + assertThat(get.value()).as("value: ").isEqualTo("/foo"); + assertThat(get.path()).as("path: ").isEqualTo("/foo"); + } + + @Test + void synthesizeAnnotationFromMapWithImplicitAttributeAliases() throws Exception { + assertAnnotationSynthesisFromMapWithImplicitAliases("value"); + assertAnnotationSynthesisFromMapWithImplicitAliases("location1"); + assertAnnotationSynthesisFromMapWithImplicitAliases("location2"); + assertAnnotationSynthesisFromMapWithImplicitAliases("location3"); + assertAnnotationSynthesisFromMapWithImplicitAliases("xmlFile"); + assertAnnotationSynthesisFromMapWithImplicitAliases("groovyScript"); + } + + private void assertAnnotationSynthesisFromMapWithImplicitAliases(String attributeNameAndValue) throws Exception { + Map map = Collections.singletonMap(attributeNameAndValue, attributeNameAndValue); + ImplicitAliasesContextConfig config = synthesizeAnnotation(map, ImplicitAliasesContextConfig.class, null); + assertThat(config).isNotNull(); + assertThat(config.value()).as("value: ").isEqualTo(attributeNameAndValue); + assertThat(config.location1()).as("location1: ").isEqualTo(attributeNameAndValue); + assertThat(config.location2()).as("location2: ").isEqualTo(attributeNameAndValue); + assertThat(config.location3()).as("location3: ").isEqualTo(attributeNameAndValue); + assertThat(config.xmlFile()).as("xmlFile: ").isEqualTo(attributeNameAndValue); + assertThat(config.groovyScript()).as("groovyScript: ").isEqualTo(attributeNameAndValue); + } + + @Test + void synthesizeAnnotationFromMapWithMissingAttributeValue() throws Exception { + assertMissingTextAttribute(Collections.emptyMap()); + } + + @Test + void synthesizeAnnotationFromMapWithNullAttributeValue() throws Exception { + Map map = Collections.singletonMap("text", null); + assertThat(map.containsKey("text")).isTrue(); + assertMissingTextAttribute(map); + } + + private void assertMissingTextAttribute(Map attributes) { + assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(() -> + synthesizeAnnotation(attributes, AnnotationWithoutDefaults.class, null).text()) + .withMessageContaining("No value found for attribute named 'text' in merged annotation"); + } + + @Test + void synthesizeAnnotationFromMapWithAttributeOfIncorrectType() throws Exception { + Map map = Collections.singletonMap(VALUE, 42L); + assertThatIllegalStateException().isThrownBy(() -> + synthesizeAnnotation(map, Component.class, null).value()) + .withMessageContaining("Attribute 'value' in annotation org.springframework.core.testfixture.stereotype.Component " + + "should be compatible with java.lang.String but a java.lang.Long value was returned"); + } + + @Test + void synthesizeAnnotationFromAnnotationAttributesWithoutAttributeAliases() throws Exception { + // 1) Get an annotation + Component component = WebController.class.getAnnotation(Component.class); + assertThat(component).isNotNull(); + + // 2) Convert the annotation into AnnotationAttributes + AnnotationAttributes attributes = getAnnotationAttributes(WebController.class, component); + assertThat(attributes).isNotNull(); + + // 3) Synthesize the AnnotationAttributes back into an annotation + Component synthesizedComponent = synthesizeAnnotation(attributes, Component.class, WebController.class); + assertThat(synthesizedComponent).isNotNull(); + + // 4) Verify that the original and synthesized annotations are equivalent + assertThat(synthesizedComponent).isNotSameAs(component); + assertThat(synthesizedComponent).isEqualTo(component); + assertThat(component.value()).as("value from component: ").isEqualTo("webController"); + assertThat(synthesizedComponent.value()).as("value from synthesized component: ").isEqualTo("webController"); + } + + @Test // gh-22702 + void findAnnotationWithRepeatablesElements() throws Exception { + assertThat(AnnotationUtils.findAnnotation(TestRepeatablesClass.class, + TestRepeatable.class)).isNull(); + assertThat(AnnotationUtils.findAnnotation(TestRepeatablesClass.class, + TestRepeatableContainer.class)).isNotNull(); + } + + @Test // gh-23856 + void findAnnotationFindsRepeatableContainerOnComposedAnnotationMetaAnnotatedWithRepeatableAnnotations() throws Exception { + MyRepeatableContainer annotation = AnnotationUtils.findAnnotation(MyRepeatableMeta1And2.class, MyRepeatableContainer.class); + + assertThat(annotation).isNotNull(); + assertThat(annotation.value()).extracting(MyRepeatable::value).containsExactly("meta1", "meta2"); + } + + @Test // gh-23856 + void findAnnotationFindsRepeatableContainerOnComposedAnnotationMetaAnnotatedWithRepeatableAnnotationsOnMethod() throws Exception { + Method method = getClass().getDeclaredMethod("methodWithComposedAnnotationMetaAnnotatedWithRepeatableAnnotations"); + MyRepeatableContainer annotation = AnnotationUtils.findAnnotation(method, MyRepeatableContainer.class); + + assertThat(annotation).isNotNull(); + assertThat(annotation.value()).extracting(MyRepeatable::value).containsExactly("meta1", "meta2"); + } + + @Test // gh-23929 + void findDeprecatedAnnotation() throws Exception { + assertThat(getAnnotation(DeprecatedClass.class, Deprecated.class)).isNotNull(); + assertThat(getAnnotation(SubclassOfDeprecatedClass.class, Deprecated.class)).isNull(); + assertThat(findAnnotation(DeprecatedClass.class, Deprecated.class)).isNotNull(); + assertThat(findAnnotation(SubclassOfDeprecatedClass.class, Deprecated.class)).isNotNull(); + } + + + @SafeVarargs + static T[] asArray(T... arr) { + return arr; + } + + + @Component("meta1") + @Order + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface Meta1 { + } + + @Component("meta2") + @Transactional(readOnly = true) + @Retention(RetentionPolicy.RUNTIME) + @interface Meta2 { + } + + @Meta2 + @Retention(RetentionPolicy.RUNTIME) + @interface MetaMeta { + } + + @MetaMeta + @Retention(RetentionPolicy.RUNTIME) + @interface MetaMetaMeta { + } + + @MetaCycle3 + @Retention(RetentionPolicy.RUNTIME) + @interface MetaCycle1 { + } + + @MetaCycle1 + @Retention(RetentionPolicy.RUNTIME) + @interface MetaCycle2 { + } + + @MetaCycle2 + @Retention(RetentionPolicy.RUNTIME) + @interface MetaCycle3 { + } + + @Meta1 + interface InterfaceWithMetaAnnotation { + } + + @Meta2 + static class ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface implements InterfaceWithMetaAnnotation { + } + + @Meta1 + static class ClassWithInheritedMetaAnnotation { + } + + @Meta2 + static class SubClassWithInheritedMetaAnnotation extends ClassWithInheritedMetaAnnotation { + } + + static class SubSubClassWithInheritedMetaAnnotation extends SubClassWithInheritedMetaAnnotation { + } + + @Transactional + static class ClassWithInheritedAnnotation { + } + + @Meta2 + static class SubClassWithInheritedAnnotation extends ClassWithInheritedAnnotation { + } + + static class SubSubClassWithInheritedAnnotation extends SubClassWithInheritedAnnotation { + } + + @MetaMeta + static class MetaMetaAnnotatedClass { + } + + @MetaMetaMeta + static class MetaMetaMetaAnnotatedClass { + } + + @MetaCycle3 + static class MetaCycleAnnotatedClass { + } + + public interface AnnotatedInterface { + + @Order(0) + void fromInterfaceImplementedByRoot(); + } + + public static class Root implements AnnotatedInterface { + + @Order(27) + public void annotatedOnRoot() { + } + + @Meta1 + public void metaAnnotatedOnRoot() { + } + + public void overrideToAnnotate() { + } + + @Order(27) + public void overrideWithoutNewAnnotation() { + } + + public void notAnnotated() { + } + + @Override + public void fromInterfaceImplementedByRoot() { + } + } + + public static class Leaf extends Root { + + @Order(25) + public void annotatedOnLeaf() { + } + + @Meta1 + public void metaAnnotatedOnLeaf() { + } + + @MetaMeta + public void metaMetaAnnotatedOnLeaf() { + } + + @Override + @Order(1) + public void overrideToAnnotate() { + } + + @Override + public void overrideWithoutNewAnnotation() { + } + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface Transactional { + + boolean readOnly() default false; + } + + public static abstract class Foo { + + @Order(1) + public abstract void something(T arg); + } + + public static class SimpleFoo extends Foo { + + @Override + @Transactional + public void something(final String arg) { + } + } + + @Transactional + public interface InheritedAnnotationInterface { + } + + public interface SubInheritedAnnotationInterface extends InheritedAnnotationInterface { + } + + public interface SubSubInheritedAnnotationInterface extends SubInheritedAnnotationInterface { + } + + @Order + public interface NonInheritedAnnotationInterface { + } + + public interface SubNonInheritedAnnotationInterface extends NonInheritedAnnotationInterface { + } + + public interface SubSubNonInheritedAnnotationInterface extends SubNonInheritedAnnotationInterface { + } + + public static class NonAnnotatedClass { + } + + public interface NonAnnotatedInterface { + } + + @Transactional + public static class InheritedAnnotationClass { + } + + public static class SubInheritedAnnotationClass extends InheritedAnnotationClass { + } + + @Order + public static class NonInheritedAnnotationClass { + } + + public static class SubNonInheritedAnnotationClass extends NonInheritedAnnotationClass { + } + + @Transactional + public static class TransactionalClass { + } + + @Order + public static class TransactionalAndOrderedClass extends TransactionalClass { + } + + public static class SubTransactionalAndOrderedClass extends TransactionalAndOrderedClass { + } + + public interface InterfaceWithAnnotatedMethod { + + @Order + void foo(); + } + + public static class ImplementsInterfaceWithAnnotatedMethod implements InterfaceWithAnnotatedMethod { + + @Override + public void foo() { + } + } + + public static class SubOfImplementsInterfaceWithAnnotatedMethod extends ImplementsInterfaceWithAnnotatedMethod { + + @Override + public void foo() { + } + } + + public abstract static class AbstractDoesNotImplementInterfaceWithAnnotatedMethod + implements InterfaceWithAnnotatedMethod { + } + + public static class SubOfAbstractImplementsInterfaceWithAnnotatedMethod + extends AbstractDoesNotImplementInterfaceWithAnnotatedMethod { + + @Override + public void foo() { + } + } + + public interface InterfaceWithGenericAnnotatedMethod { + + @Order + void foo(T t); + } + + public static class ImplementsInterfaceWithGenericAnnotatedMethod implements InterfaceWithGenericAnnotatedMethod { + + @Override + public void foo(String t) { + } + } + + public static abstract class BaseClassWithGenericAnnotatedMethod { + + @Order + abstract void foo(T t); + } + + public static class ExtendsBaseClassWithGenericAnnotatedMethod extends BaseClassWithGenericAnnotatedMethod { + + @Override + public void foo(String t) { + } + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface MyRepeatableContainer { + + MyRepeatable[] value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @Repeatable(MyRepeatableContainer.class) + @interface MyRepeatable { + + String value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @MyRepeatable("meta1") + @interface MyRepeatableMeta1 { + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @MyRepeatable("meta2") + @interface MyRepeatableMeta2 { + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @MyRepeatable("meta1") + @MyRepeatable("meta2") + @interface MyRepeatableMeta1And2 { + } + + @MyRepeatableMeta1And2 + void methodWithComposedAnnotationMetaAnnotatedWithRepeatableAnnotations() { + } + + interface InterfaceWithRepeated { + + @MyRepeatable("A") + @MyRepeatableContainer({@MyRepeatable("B"), @MyRepeatable("C")}) + @MyRepeatableMeta1 + void foo(); + } + + @MyRepeatable("A") + @MyRepeatableContainer({@MyRepeatable("B"), @MyRepeatable("C")}) + @MyRepeatableMeta1 + static class MyRepeatableClass { + } + + static class SubMyRepeatableClass extends MyRepeatableClass { + } + + @MyRepeatable("X") + @MyRepeatableContainer({@MyRepeatable("Y"), @MyRepeatable("Z")}) + @MyRepeatableMeta2 + static class SubMyRepeatableWithAdditionalLocalDeclarationsClass extends MyRepeatableClass { + } + + static class SubSubMyRepeatableWithAdditionalLocalDeclarationsClass extends + SubMyRepeatableWithAdditionalLocalDeclarationsClass { + } + + enum RequestMethod { + GET, POST + } + + /** + * Mock of {@code org.springframework.web.bind.annotation.RequestMapping}. + */ + @Retention(RetentionPolicy.RUNTIME) + @interface WebMapping { + + String name(); + + @AliasFor("path") + String[] value() default ""; + + @AliasFor(attribute = "value") + String[] path() default ""; + + RequestMethod[] method() default {}; + } + + /** + * Mock of {@code org.springframework.web.bind.annotation.GetMapping}, except + * that the String arrays are overridden with single String elements. + */ + @Retention(RetentionPolicy.RUNTIME) + @WebMapping(method = RequestMethod.GET, name = "") + @interface Get { + + @AliasFor(annotation = WebMapping.class) + String value() default ""; + + @AliasFor(annotation = WebMapping.class) + String path() default ""; + } + + /** + * Mock of {@code org.springframework.web.bind.annotation.PostMapping}, except + * that the path is overridden by convention with single String element. + */ + @Retention(RetentionPolicy.RUNTIME) + @WebMapping(method = RequestMethod.POST, name = "") + @interface Post { + + String path() default ""; + } + + @Component("webController") + static class WebController { + + @WebMapping(value = "/test", name = "foo") + public void handleMappedWithValueAttribute() { + } + + @WebMapping(path = "/test", name = "bar", method = { RequestMethod.GET, RequestMethod.POST }) + public void handleMappedWithPathAttribute() { + } + + @Get("/test") + public void getMappedWithValueAttribute() { + } + + @Get(path = "/test") + public void getMappedWithPathAttribute() { + } + + @Post(path = "/test") + public void postMappedWithPathAttribute() { + } + + /** + * mapping is logically "equal" to handleMappedWithPathAttribute(). + */ + @WebMapping(value = "/test", path = "/test", name = "bar", method = { RequestMethod.GET, RequestMethod.POST }) + public void handleMappedWithSamePathAndValueAttributes() { + } + + @WebMapping(value = "/enigma", path = "/test", name = "baz") + public void handleMappedWithDifferentPathAndValueAttributes() { + } + } + + /** + * Mock of {@code org.springframework.test.context.ContextConfiguration}. + */ + @Retention(RetentionPolicy.RUNTIME) + @interface ContextConfig { + + @AliasFor("location") + String value() default ""; + + @AliasFor("value") + String location() default ""; + + Class klass() default Object.class; + } + + /** + * Mock of {@code org.springframework.test.context.ContextHierarchy}. + */ + @Retention(RetentionPolicy.RUNTIME) + @interface Hierarchy { + ContextConfig[] value(); + } + + @Hierarchy({@ContextConfig("A"), @ContextConfig(location = "B")}) + static class ConfigHierarchyTestCase { + } + + @ContextConfig("simple.xml") + static class SimpleConfigTestCase { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface CharsContainer { + + @AliasFor(attribute = "chars") + char[] value() default {}; + + @AliasFor(attribute = "value") + char[] chars() default {}; + } + + @CharsContainer(chars = { 'x', 'y', 'z' }) + static class GroupOfCharsClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForWithMissingAttributeDeclaration { + + @AliasFor + String foo() default ""; + } + + @AliasForWithMissingAttributeDeclaration + static class AliasForWithMissingAttributeDeclarationClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForWithDuplicateAttributeDeclaration { + + @AliasFor(value = "bar", attribute = "baz") + String foo() default ""; + } + + @AliasForWithDuplicateAttributeDeclaration + static class AliasForWithDuplicateAttributeDeclarationClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForNonexistentAttribute { + + @AliasFor("bar") + String foo() default ""; + } + + @AliasForNonexistentAttribute + static class AliasForNonexistentAttributeClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForWithoutMirroredAliasFor { + + @AliasFor("bar") + String foo() default ""; + + String bar() default ""; + } + + @AliasForWithoutMirroredAliasFor + static class AliasForWithoutMirroredAliasForClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForWithMirroredAliasForWrongAttribute { + + @AliasFor(attribute = "bar") + String[] foo() default ""; + + @AliasFor(attribute = "quux") + String[] bar() default ""; + } + + @AliasForWithMirroredAliasForWrongAttribute + static class AliasForWithMirroredAliasForWrongAttributeClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForAttributeOfDifferentType { + + @AliasFor("bar") + String[] foo() default ""; + + @AliasFor("foo") + boolean bar() default true; + } + + @AliasForAttributeOfDifferentType + static class AliasForAttributeOfDifferentTypeClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForWithMissingDefaultValues { + + @AliasFor(attribute = "bar") + String foo(); + + @AliasFor(attribute = "foo") + String bar(); + } + + @AliasForWithMissingDefaultValues(foo = "foo", bar = "bar") + static class AliasForWithMissingDefaultValuesClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForAttributeWithDifferentDefaultValue { + + @AliasFor("bar") + String foo() default "X"; + + @AliasFor("foo") + String bar() default "Z"; + } + + @AliasForAttributeWithDifferentDefaultValue + static class AliasForAttributeWithDifferentDefaultValueClass { + } + + // @ContextConfig --> Intentionally NOT meta-present + @Retention(RetentionPolicy.RUNTIME) + @interface AliasedComposedContextConfigNotMetaPresent { + + @AliasFor(annotation = ContextConfig.class, attribute = "location") + String xmlConfigFile(); + } + + @AliasedComposedContextConfigNotMetaPresent(xmlConfigFile = "test.xml") + static class AliasedComposedContextConfigNotMetaPresentClass { + } + + @ContextConfig + @Retention(RetentionPolicy.RUNTIME) + @interface AliasedComposedContextConfig { + + @AliasFor(annotation = ContextConfig.class, attribute = "location") + String xmlConfigFile(); + } + + @ContextConfig + @Retention(RetentionPolicy.RUNTIME) + public @interface ImplicitAliasesContextConfig { + + @AliasFor(annotation = ContextConfig.class, attribute = "location") + String xmlFile() default ""; + + @AliasFor(annotation = ContextConfig.class, attribute = "location") + String groovyScript() default ""; + + @AliasFor(annotation = ContextConfig.class, attribute = "location") + String value() default ""; + + @AliasFor(annotation = ContextConfig.class, attribute = "location") + String location1() default ""; + + @AliasFor(annotation = ContextConfig.class, attribute = "location") + String location2() default ""; + + @AliasFor(annotation = ContextConfig.class, attribute = "location") + String location3() default ""; + + @AliasFor(annotation = ContextConfig.class, attribute = "klass") + Class configClass() default Object.class; + + String nonAliasedAttribute() default ""; + } + + // Attribute value intentionally matches attribute name: + @ImplicitAliasesContextConfig(groovyScript = "groovyScript") + static class GroovyImplicitAliasesContextConfigClass { + } + + // Attribute value intentionally matches attribute name: + @ImplicitAliasesContextConfig(xmlFile = "xmlFile") + static class XmlImplicitAliasesContextConfigClass { + } + + // Attribute value intentionally matches attribute name: + @ImplicitAliasesContextConfig("value") + static class ValueImplicitAliasesContextConfigClass { + } + + // Attribute value intentionally matches attribute name: + @ImplicitAliasesContextConfig(location1 = "location1") + static class Location1ImplicitAliasesContextConfigClass { + } + + // Attribute value intentionally matches attribute name: + @ImplicitAliasesContextConfig(location2 = "location2") + static class Location2ImplicitAliasesContextConfigClass { + } + + // Attribute value intentionally matches attribute name: + @ImplicitAliasesContextConfig(location3 = "location3") + static class Location3ImplicitAliasesContextConfigClass { + } + + @ContextConfig + @Retention(RetentionPolicy.RUNTIME) + @interface ImplicitAliasesWithImpliedAliasNamesOmittedContextConfig { + + // intentionally omitted: attribute = "value" + @AliasFor(annotation = ContextConfig.class) + String value() default ""; + + // intentionally omitted: attribute = "locations" + @AliasFor(annotation = ContextConfig.class) + String location() default ""; + + @AliasFor(annotation = ContextConfig.class, attribute = "location") + String xmlFile() default ""; + } + + @ImplicitAliasesWithImpliedAliasNamesOmittedContextConfig + @Retention(RetentionPolicy.RUNTIME) + @interface TransitiveImplicitAliasesWithImpliedAliasNamesOmittedContextConfig { + + @AliasFor(annotation = ImplicitAliasesWithImpliedAliasNamesOmittedContextConfig.class, attribute = "xmlFile") + String xml() default ""; + + @AliasFor(annotation = ImplicitAliasesWithImpliedAliasNamesOmittedContextConfig.class, attribute = "location") + String groovy() default ""; + } + + // Attribute value intentionally matches attribute name: + @ImplicitAliasesWithImpliedAliasNamesOmittedContextConfig("value") + static class ValueImplicitAliasesWithImpliedAliasNamesOmittedContextConfigClass { + } + + // Attribute value intentionally matches attribute name: + @ImplicitAliasesWithImpliedAliasNamesOmittedContextConfig(location = "location") + static class LocationsImplicitAliasesWithImpliedAliasNamesOmittedContextConfigClass { + } + + // Attribute value intentionally matches attribute name: + @ImplicitAliasesWithImpliedAliasNamesOmittedContextConfig(xmlFile = "xmlFile") + static class XmlFilesImplicitAliasesWithImpliedAliasNamesOmittedContextConfigClass { + } + + @ContextConfig + @Retention(RetentionPolicy.RUNTIME) + @interface ImplicitAliasesWithMissingDefaultValuesContextConfig { + + @AliasFor(annotation = ContextConfig.class, attribute = "location") + String location1(); + + @AliasFor(annotation = ContextConfig.class, attribute = "location") + String location2(); + } + + @ImplicitAliasesWithMissingDefaultValuesContextConfig(location1 = "1", location2 = "2") + static class ImplicitAliasesWithMissingDefaultValuesContextConfigClass { + } + + @ContextConfig + @Retention(RetentionPolicy.RUNTIME) + @interface ImplicitAliasesWithDifferentDefaultValuesContextConfig { + + @AliasFor(annotation = ContextConfig.class, attribute = "location") + String location1() default "foo"; + + @AliasFor(annotation = ContextConfig.class, attribute = "location") + String location2() default "bar"; + } + + @ImplicitAliasesWithDifferentDefaultValuesContextConfig(location1 = "1", location2 = "2") + static class ImplicitAliasesWithDifferentDefaultValuesContextConfigClass { + } + + @ContextConfig + @Retention(RetentionPolicy.RUNTIME) + @interface ImplicitAliasesWithDuplicateValuesContextConfig { + + @AliasFor(annotation = ContextConfig.class, attribute = "location") + String location1() default ""; + + @AliasFor(annotation = ContextConfig.class, attribute = "location") + String location2() default ""; + } + + @ImplicitAliasesWithDuplicateValuesContextConfig(location1 = "1", location2 = "2") + static class ImplicitAliasesWithDuplicateValuesContextConfigClass { + } + + @ContextConfig + @Retention(RetentionPolicy.RUNTIME) + @interface ImplicitAliasesForAliasPairContextConfig { + + @AliasFor(annotation = ContextConfig.class, attribute = "location") + String xmlFile() default ""; + + @AliasFor(annotation = ContextConfig.class, value = "value") + String groovyScript() default ""; + } + + @ImplicitAliasesForAliasPairContextConfig(xmlFile = "test.xml") + static class ImplicitAliasesForAliasPairContextConfigClass { + } + + @ImplicitAliasesContextConfig + @Retention(RetentionPolicy.RUNTIME) + @interface TransitiveImplicitAliasesContextConfig { + + @AliasFor(annotation = ImplicitAliasesContextConfig.class, attribute = "xmlFile") + String xml() default ""; + + @AliasFor(annotation = ImplicitAliasesContextConfig.class, attribute = "groovyScript") + String groovy() default ""; + } + + @TransitiveImplicitAliasesContextConfig(xml = "test.xml") + static class TransitiveImplicitAliasesContextConfigClass { + } + + @ImplicitAliasesForAliasPairContextConfig + @Retention(RetentionPolicy.RUNTIME) + @interface TransitiveImplicitAliasesForAliasPairContextConfig { + + @AliasFor(annotation = ImplicitAliasesForAliasPairContextConfig.class, attribute = "xmlFile") + String xml() default ""; + + @AliasFor(annotation = ImplicitAliasesForAliasPairContextConfig.class, attribute = "groovyScript") + String groovy() default ""; + } + + @TransitiveImplicitAliasesForAliasPairContextConfig(xml = "test.xml") + static class TransitiveImplicitAliasesForAliasPairContextConfigClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({}) + @interface Filter { + String pattern(); + } + + /** + * Mock of {@code org.springframework.context.annotation.ComponentScan}. + */ + @Retention(RetentionPolicy.RUNTIME) + @interface ComponentScan { + Filter[] excludeFilters() default {}; + } + + @ComponentScan(excludeFilters = {@Filter(pattern = "*Foo"), @Filter(pattern = "*Bar")}) + static class ComponentScanClass { + } + + /** + * Mock of {@code org.springframework.context.annotation.ComponentScan}. + */ + @Retention(RetentionPolicy.RUNTIME) + @interface ComponentScanSingleFilter { + Filter value(); + } + + @ComponentScanSingleFilter(@Filter(pattern = "*Foo")) + static class ComponentScanSingleFilterClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AnnotationWithDefaults { + String text() default "enigma"; + boolean predicate() default true; + char[] characters() default {'a', 'b', 'c'}; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AnnotationWithoutDefaults { + String text(); + } + + @ContextConfig(value = "foo", location = "bar") + interface ContextConfigMismatch { + } + + @Retention(RetentionPolicy.RUNTIME) + @Repeatable(TestRepeatableContainer.class) + @interface TestRepeatable { + + String value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @interface TestRepeatableContainer { + + TestRepeatable[] value(); + } + + @TestRepeatable("a") + @TestRepeatable("b") + static class TestRepeatablesClass { + } + + @Deprecated + static class DeprecatedClass { + } + + static class SubclassOfDeprecatedClass extends DeprecatedClass { + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java new file mode 100644 index 0000000..83518d9 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java @@ -0,0 +1,788 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; +import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AnnotationsScanner}. + * + * @author Phillip Webb + * @author Sam Brannen + */ +class AnnotationsScannerTests { + + @Test + void directStrategyOnClassWhenNotAnnotatedScansNone() { + Class source = WithNoAnnotations.class; + assertThat(scan(source, SearchStrategy.DIRECT)).isEmpty(); + } + + @Test + void directStrategyOnClassScansAnnotations() { + Class source = WithSingleAnnotation.class; + assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly("0:TestAnnotation1"); + } + + @Test + void directStrategyOnClassWhenMultipleAnnotationsScansAnnotations() { + Class source = WithMultipleAnnotations.class; + assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly("0:TestAnnotation1", "0:TestAnnotation2"); + } + + @Test + void directStrategyOnClassWhenHasSuperclassScansOnlyDirect() { + Class source = WithSingleSuperclass.class; + assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly("0:TestAnnotation1"); + } + + @Test + void directStrategyOnClassWhenHasInterfaceScansOnlyDirect() { + Class source = WithSingleInterface.class; + assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly("0:TestAnnotation1"); + } + + @Test + void directStrategyOnClassHierarchyScansInCorrectOrder() { + Class source = WithHierarchy.class; + assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly("0:TestAnnotation1"); + } + + @Test + void inheritedAnnotationsStrategyOnClassWhenNotAnnotatedScansNone() { + Class source = WithNoAnnotations.class; + assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).isEmpty(); + } + + @Test + void inheritedAnnotationsStrategyOnClassScansAnnotations() { + Class source = WithSingleAnnotation.class; + assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly("0:TestAnnotation1"); + } + + @Test + void inheritedAnnotationsStrategyOnClassWhenMultipleAnnotationsScansAnnotations() { + Class source = WithMultipleAnnotations.class; + assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly( + "0:TestAnnotation1", "0:TestAnnotation2"); + } + + @Test + void inheritedAnnotationsStrategyOnClassWhenHasSuperclassScansOnlyInherited() { + Class source = WithSingleSuperclass.class; + assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly( + "0:TestAnnotation1", "1:TestInheritedAnnotation2"); + } + + @Test + void inheritedAnnotationsStrategyOnClassWhenHasInterfaceDoesNotIncludeInterfaces() { + Class source = WithSingleInterface.class; + assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly("0:TestAnnotation1"); + } + + @Test + void inheritedAnnotationsStrategyOnClassHierarchyScansInCorrectOrder() { + Class source = WithHierarchy.class; + assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly( + "0:TestAnnotation1", "1:TestInheritedAnnotation2", "2:TestInheritedAnnotation3"); + } + + @Test + void inheritedAnnotationsStrategyOnClassWhenHasAnnotationOnBothClassesIncudesOnlyOne() { + Class source = WithSingleSuperclassAndDoubleInherited.class; + assertThat(Arrays.stream(source.getAnnotations()).map( + Annotation::annotationType).map(Class::getName)).containsExactly( + TestInheritedAnnotation2.class.getName()); + assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly("0:TestInheritedAnnotation2"); + } + + @Test + void superclassStrategyOnClassWhenNotAnnotatedScansNone() { + Class source = WithNoAnnotations.class; + assertThat(scan(source, SearchStrategy.SUPERCLASS)).isEmpty(); + } + + @Test + void superclassStrategyOnClassScansAnnotations() { + Class source = WithSingleAnnotation.class; + assertThat(scan(source, SearchStrategy.SUPERCLASS)).containsExactly("0:TestAnnotation1"); + } + + @Test + void superclassStrategyOnClassWhenMultipleAnnotationsScansAnnotations() { + Class source = WithMultipleAnnotations.class; + assertThat(scan(source, SearchStrategy.SUPERCLASS)).containsExactly("0:TestAnnotation1", "0:TestAnnotation2"); + } + + @Test + void superclassStrategyOnClassWhenHasSuperclassScansSuperclass() { + Class source = WithSingleSuperclass.class; + assertThat(scan(source, SearchStrategy.SUPERCLASS)).containsExactly( + "0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2"); + } + + @Test + void superclassStrategyOnClassWhenHasInterfaceDoesNotIncludeInterfaces() { + Class source = WithSingleInterface.class; + assertThat(scan(source, SearchStrategy.SUPERCLASS)).containsExactly("0:TestAnnotation1"); + } + + @Test + void superclassStrategyOnClassHierarchyScansInCorrectOrder() { + Class source = WithHierarchy.class; + assertThat(scan(source, SearchStrategy.SUPERCLASS)).containsExactly( + "0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2", + "2:TestAnnotation3", "2:TestInheritedAnnotation3"); + } + + @Test + void typeHierarchyStrategyOnClassWhenNotAnnotatedScansNone() { + Class source = WithNoAnnotations.class; + assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).isEmpty(); + } + + @Test + void typeHierarchyStrategyOnClassScansAnnotations() { + Class source = WithSingleAnnotation.class; + assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).containsExactly("0:TestAnnotation1"); + } + + @Test + void typeHierarchyStrategyOnClassWhenMultipleAnnotationsScansAnnotations() { + Class source = WithMultipleAnnotations.class; + assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).containsExactly( + "0:TestAnnotation1", "0:TestAnnotation2"); + } + + @Test + void typeHierarchyStrategyOnClassWhenHasSuperclassScansSuperclass() { + Class source = WithSingleSuperclass.class; + assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).containsExactly( + "0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2"); + } + + @Test + void typeHierarchyStrategyOnClassWhenHasInterfaceDoesNotIncludeInterfaces() { + Class source = WithSingleInterface.class; + assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).containsExactly( + "0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2"); + } + + @Test + void typeHierarchyStrategyOnClassHierarchyScansInCorrectOrder() { + Class source = WithHierarchy.class; + assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).containsExactly( + "0:TestAnnotation1", "1:TestAnnotation5", "1:TestInheritedAnnotation5", + "2:TestAnnotation6", "3:TestAnnotation2", "3:TestInheritedAnnotation2", + "4:TestAnnotation3", "4:TestInheritedAnnotation3", "5:TestAnnotation4"); + } + + @Test + void directStrategyOnMethodWhenNotAnnotatedScansNone() { + Method source = methodFrom(WithNoAnnotations.class); + assertThat(scan(source, SearchStrategy.DIRECT)).isEmpty(); + } + + @Test + void directStrategyOnMethodScansAnnotations() { + Method source = methodFrom(WithSingleAnnotation.class); + assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly("0:TestAnnotation1"); + } + + @Test + void directStrategyOnMethodWhenMultipleAnnotationsScansAnnotations() { + Method source = methodFrom(WithMultipleAnnotations.class); + assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly( + "0:TestAnnotation1", "0:TestAnnotation2"); + } + + @Test + void directStrategyOnMethodWhenHasSuperclassScansOnlyDirect() { + Method source = methodFrom(WithSingleSuperclass.class); + assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly("0:TestAnnotation1"); + } + + @Test + void directStrategyOnMethodWhenHasInterfaceScansOnlyDirect() { + Method source = methodFrom(WithSingleInterface.class); + assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly("0:TestAnnotation1"); + } + + @Test + void directStrategyOnMethodHierarchyScansInCorrectOrder() { + Method source = methodFrom(WithHierarchy.class); + assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly("0:TestAnnotation1"); + } + + @Test + void inheritedAnnotationsStrategyOnMethodWhenNotAnnotatedScansNone() { + Method source = methodFrom(WithNoAnnotations.class); + assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).isEmpty(); + } + + @Test + void inheritedAnnotationsStrategyOnMethodScansAnnotations() { + Method source = methodFrom(WithSingleAnnotation.class); + assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly("0:TestAnnotation1"); + } + + @Test + void inheritedAnnotationsStrategyOnMethodWhenMultipleAnnotationsScansAnnotations() { + Method source = methodFrom(WithMultipleAnnotations.class); + assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly( + "0:TestAnnotation1", "0:TestAnnotation2"); + } + + @Test + void inheritedAnnotationsMethodOnMethodWhenHasSuperclassIgnoresInherited() { + Method source = methodFrom(WithSingleSuperclass.class); + assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly("0:TestAnnotation1"); + } + + @Test + void inheritedAnnotationsStrategyOnMethodWhenHasInterfaceDoesNotIncludeInterfaces() { + Method source = methodFrom(WithSingleInterface.class); + assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly("0:TestAnnotation1"); + } + + @Test + void inheritedAnnotationsStrategyOnMethodHierarchyScansInCorrectOrder() { + Method source = methodFrom(WithHierarchy.class); + assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly("0:TestAnnotation1"); + } + + @Test + void superclassStrategyOnMethodWhenNotAnnotatedScansNone() { + Method source = methodFrom(WithNoAnnotations.class); + assertThat(scan(source, SearchStrategy.SUPERCLASS)).isEmpty(); + } + + @Test + void superclassStrategyOnMethodScansAnnotations() { + Method source = methodFrom(WithSingleAnnotation.class); + assertThat(scan(source, SearchStrategy.SUPERCLASS)).containsExactly("0:TestAnnotation1"); + } + + @Test + void superclassStrategyOnMethodWhenMultipleAnnotationsScansAnnotations() { + Method source = methodFrom(WithMultipleAnnotations.class); + assertThat(scan(source, SearchStrategy.SUPERCLASS)).containsExactly( + "0:TestAnnotation1", "0:TestAnnotation2"); + } + + @Test + void superclassStrategyOnMethodWhenHasSuperclassScansSuperclass() { + Method source = methodFrom(WithSingleSuperclass.class); + assertThat(scan(source, SearchStrategy.SUPERCLASS)).containsExactly( + "0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2"); + } + + @Test + void superclassStrategyOnMethodWhenHasInterfaceDoesNotIncludeInterfaces() { + Method source = methodFrom(WithSingleInterface.class); + assertThat(scan(source, SearchStrategy.SUPERCLASS)).containsExactly("0:TestAnnotation1"); + } + + @Test + void superclassStrategyOnMethodHierarchyScansInCorrectOrder() { + Method source = methodFrom(WithHierarchy.class); + assertThat(scan(source, SearchStrategy.SUPERCLASS)).containsExactly( + "0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2", + "2:TestAnnotation3"); + } + + @Test + void typeHierarchyStrategyOnMethodWhenNotAnnotatedScansNone() { + Method source = methodFrom(WithNoAnnotations.class); + assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).isEmpty(); + } + + @Test + void typeHierarchyStrategyOnMethodScansAnnotations() { + Method source = methodFrom(WithSingleAnnotation.class); + assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).containsExactly("0:TestAnnotation1"); + } + + @Test + void typeHierarchyStrategyOnMethodWhenMultipleAnnotationsScansAnnotations() { + Method source = methodFrom(WithMultipleAnnotations.class); + assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).containsExactly( + "0:TestAnnotation1", "0:TestAnnotation2"); + } + + @Test + void typeHierarchyStrategyOnMethodWhenHasSuperclassScansSuperclass() { + Method source = methodFrom(WithSingleSuperclass.class); + assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).containsExactly( + "0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2"); + } + + @Test + void typeHierarchyStrategyOnMethodWhenHasInterfaceDoesNotIncludeInterfaces() { + Method source = methodFrom(WithSingleInterface.class); + assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).containsExactly( + "0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2"); + } + + @Test + void typeHierarchyStrategyOnMethodHierarchyScansInCorrectOrder() { + Method source = methodFrom(WithHierarchy.class); + assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).containsExactly( + "0:TestAnnotation1", "1:TestAnnotation5", "1:TestInheritedAnnotation5", + "2:TestAnnotation6", "3:TestAnnotation2", "3:TestInheritedAnnotation2", + "4:TestAnnotation3", "5:TestAnnotation4"); + } + + @Test + void typeHierarchyStrategyOnBridgeMethodScansAnnotations() throws Exception { + Method source = BridgedMethod.class.getDeclaredMethod("method", Object.class); + assertThat(source.isBridge()).isTrue(); + assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).containsExactly( + "0:TestAnnotation1", "1:TestAnnotation2"); + } + + @Test + void typeHierarchyStrategyOnBridgedMethodScansAnnotations() throws Exception { + Method source = BridgedMethod.class.getDeclaredMethod("method", String.class); + assertThat(source.isBridge()).isFalse(); + assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).containsExactly( + "0:TestAnnotation1", "1:TestAnnotation2"); + } + + @Test + void directStrategyOnBridgeMethodScansAnnotations() throws Exception { + Method source = BridgedMethod.class.getDeclaredMethod("method", Object.class); + assertThat(source.isBridge()).isTrue(); + assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly("0:TestAnnotation1"); + } + + @Test + void directStrategyOnBridgedMethodScansAnnotations() throws Exception { + Method source = BridgedMethod.class.getDeclaredMethod("method", String.class); + assertThat(source.isBridge()).isFalse(); + assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly("0:TestAnnotation1"); + } + + @Test + void typeHierarchyStrategyOnMethodWithIgnorablesScansAnnotations() { + Method source = methodFrom(Ignorable.class); + assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).containsExactly("0:TestAnnotation1"); + } + + @Test + void typeHierarchyStrategyOnMethodWithMultipleCandidatesScansAnnotations() { + Method source = methodFrom(MultipleMethods.class); + assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).containsExactly("0:TestAnnotation1"); + } + + @Test + void typeHierarchyStrategyOnMethodWithGenericParameterOverrideScansAnnotations() { + Method source = ReflectionUtils.findMethod(GenericOverride.class, "method", String.class); + assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).containsExactly( + "0:TestAnnotation1", "1:TestAnnotation2"); + } + + @Test + void typeHierarchyStrategyOnMethodWithGenericParameterNonOverrideScansAnnotations() { + Method source = ReflectionUtils.findMethod(GenericNonOverride.class, "method", StringBuilder.class); + assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).containsExactly("0:TestAnnotation1"); + } + + @Test + void typeHierarchyWithEnclosedStrategyOnEnclosedStaticClassScansAnnotations() { + Class source = AnnotationEnclosingClassSample.EnclosedStatic.EnclosedStaticStatic.class; + assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES)) + .containsExactly("0:EnclosedThree", "1:EnclosedTwo", "2:EnclosedOne"); + } + + @Test + void typeHierarchyWithEnclosedStrategyOnEnclosedInnerClassScansAnnotations() { + Class source = AnnotationEnclosingClassSample.EnclosedInner.EnclosedInnerInner.class; + assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES)) + .containsExactly("0:EnclosedThree", "1:EnclosedTwo", "2:EnclosedOne"); + } + + @Test + void typeHierarchyWithEnclosedStrategyOnMethodHierarchyUsesTypeHierarchyScan() { + Method source = methodFrom(WithHierarchy.class); + assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES)).containsExactly( + "0:TestAnnotation1", "1:TestAnnotation5", "1:TestInheritedAnnotation5", + "2:TestAnnotation6", "3:TestAnnotation2", "3:TestInheritedAnnotation2", + "4:TestAnnotation3", "5:TestAnnotation4"); + } + + @Test + void scanWhenProcessorReturnsFromDoWithAggregateExitsEarly() { + String result = AnnotationsScanner.scan(this, WithSingleSuperclass.class, + SearchStrategy.TYPE_HIERARCHY, new AnnotationsProcessor() { + + @Override + @Nullable + public String doWithAggregate(Object context, int aggregateIndex) { + return ""; + } + + @Override + @Nullable + public String doWithAnnotations(Object context, int aggregateIndex, + Object source, Annotation[] annotations) { + throw new IllegalStateException("Should not call"); + } + + }); + assertThat(result).isEmpty(); + } + + @Test + void scanWhenProcessorReturnsFromDoWithAnnotationsExitsEarly() { + List indexes = new ArrayList<>(); + String result = AnnotationsScanner.scan(this, WithSingleSuperclass.class, + SearchStrategy.TYPE_HIERARCHY, + (context, aggregateIndex, source, annotations) -> { + indexes.add(aggregateIndex); + return ""; + }); + assertThat(result).isEmpty(); + assertThat(indexes).containsExactly(0); + } + + @Test + void scanWhenProcessorHasFinishMethodUsesFinishResult() { + String result = AnnotationsScanner.scan(this, WithSingleSuperclass.class, + SearchStrategy.TYPE_HIERARCHY, new AnnotationsProcessor() { + + @Override + @Nullable + public String doWithAnnotations(Object context, int aggregateIndex, + Object source, Annotation[] annotations) { + return "K"; + } + + @Override + @Nullable + public String finish(String result) { + return "O" + result; + } + + }); + assertThat(result).isEqualTo("OK"); + } + + + private Method methodFrom(Class type) { + return ReflectionUtils.findMethod(type, "method"); + } + + private Stream scan(AnnotatedElement element, SearchStrategy searchStrategy) { + List results = new ArrayList<>(); + AnnotationsScanner.scan(this, element, searchStrategy, + (criteria, aggregateIndex, source, annotations) -> { + trackIndexedAnnotations(aggregateIndex, annotations, results); + return null; // continue searching + }); + return results.stream(); + } + + private void trackIndexedAnnotations(int aggregateIndex, Annotation[] annotations, List results) { + Arrays.stream(annotations) + .filter(Objects::nonNull) + .map(annotation -> indexedName(aggregateIndex, annotation)) + .forEach(results::add); + } + + private String indexedName(int aggregateIndex, Annotation annotation) { + return aggregateIndex + ":" + annotation.annotationType().getSimpleName(); + } + + + @Retention(RetentionPolicy.RUNTIME) + @interface TestAnnotation1 { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface TestAnnotation2 { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface TestAnnotation3 { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface TestAnnotation4 { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface TestAnnotation5 { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface TestAnnotation6 { + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface TestInheritedAnnotation1 { + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface TestInheritedAnnotation2 { + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface TestInheritedAnnotation3 { + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface TestInheritedAnnotation4 { + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface TestInheritedAnnotation5 { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface OnSuperClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface OnInterface { + } + + static class WithNoAnnotations { + + public void method() { + } + } + + @TestAnnotation1 + static class WithSingleAnnotation { + + @TestAnnotation1 + public void method() { + } + } + + @TestAnnotation1 + @TestAnnotation2 + static class WithMultipleAnnotations { + + @TestAnnotation1 + @TestAnnotation2 + public void method() { + } + } + + @TestAnnotation2 + @TestInheritedAnnotation2 + static class SingleSuperclass { + + @TestAnnotation2 + @TestInheritedAnnotation2 + public void method() { + } + } + + @TestAnnotation1 + static class WithSingleSuperclass extends SingleSuperclass { + + @Override + @TestAnnotation1 + public void method() { + } + } + + @TestInheritedAnnotation2 + static class WithSingleSuperclassAndDoubleInherited extends SingleSuperclass { + + @Override + @TestAnnotation1 + public void method() { + } + } + + @TestAnnotation1 + static class WithSingleInterface implements SingleInterface { + + @Override + @TestAnnotation1 + public void method() { + } + } + + @TestAnnotation2 + @TestInheritedAnnotation2 + interface SingleInterface { + + @TestAnnotation2 + @TestInheritedAnnotation2 + void method(); + } + + @TestAnnotation1 + static class WithHierarchy extends HierarchySuperclass implements HierarchyInterface { + + @Override + @TestAnnotation1 + public void method() { + } + } + + @TestAnnotation2 + @TestInheritedAnnotation2 + static class HierarchySuperclass extends HierarchySuperSuperclass { + + @Override + @TestAnnotation2 + @TestInheritedAnnotation2 + public void method() { + } + } + + @TestAnnotation3 + @TestInheritedAnnotation3 + static class HierarchySuperSuperclass implements HierarchySuperSuperclassInterface { + + @Override + @TestAnnotation3 + public void method() { + } + } + + @TestAnnotation4 + interface HierarchySuperSuperclassInterface { + + @TestAnnotation4 + void method(); + } + + @TestAnnotation5 + @TestInheritedAnnotation5 + interface HierarchyInterface extends HierarchyInterfaceInterface { + + @Override + @TestAnnotation5 + @TestInheritedAnnotation5 + void method(); + } + + @TestAnnotation6 + interface HierarchyInterfaceInterface { + + @TestAnnotation6 + void method(); + } + + static class BridgedMethod implements BridgeMethod { + + @Override + @TestAnnotation1 + public void method(String arg) { + } + } + + interface BridgeMethod { + + @TestAnnotation2 + void method(T arg); + } + + static class Ignorable implements IgnorableOverrideInterface1, IgnorableOverrideInterface2 { + + @Override + @TestAnnotation1 + public void method() { + } + } + + interface IgnorableOverrideInterface1 { + + @Nullable + void method(); + } + + interface IgnorableOverrideInterface2 { + + @Nullable + void method(); + } + + static abstract class MultipleMethods implements MultipleMethodsInterface { + + @TestAnnotation1 + public void method() { + } + } + + interface MultipleMethodsInterface { + + @TestAnnotation2 + void method(String arg); + + @TestAnnotation2 + void method1(); + } + + static class GenericOverride implements GenericOverrideInterface { + + @Override + @TestAnnotation1 + public void method(String argument) { + } + } + + interface GenericOverrideInterface { + + @TestAnnotation2 + void method(T argument); + } + + static abstract class GenericNonOverride implements GenericNonOverrideInterface { + + @TestAnnotation1 + public void method(StringBuilder argument) { + } + } + + interface GenericNonOverrideInterface { + + @TestAnnotation2 + void method(T argument); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AttributeMethodsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AttributeMethodsTests.java new file mode 100644 index 0000000..6fb0dcd --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/AttributeMethodsTests.java @@ -0,0 +1,229 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link AttributeMethods}. + * + * @author Phillip Webb + */ +class AttributeMethodsTests { + + @Test + void forAnnotationTypeWhenNullReturnsNone() { + AttributeMethods methods = AttributeMethods.forAnnotationType(null); + assertThat(methods).isSameAs(AttributeMethods.NONE); + } + + @Test + void forAnnotationTypeWhenHasNoAttributesReturnsNone() { + AttributeMethods methods = AttributeMethods.forAnnotationType(NoAttributes.class); + assertThat(methods).isSameAs(AttributeMethods.NONE); + } + + @Test + void forAnnotationTypeWhenHasMultipleAttributesReturnsAttributes() { + AttributeMethods methods = AttributeMethods.forAnnotationType(MultipleAttributes.class); + assertThat(methods.get("value").getName()).isEqualTo("value"); + assertThat(methods.get("intValue").getName()).isEqualTo("intValue"); + assertThat(getAll(methods)).flatExtracting(Method::getName).containsExactly("intValue", "value"); + } + + @Test + void hasOnlyValueAttributeWhenHasOnlyValueAttributeReturnsTrue() { + AttributeMethods methods = AttributeMethods.forAnnotationType(ValueOnly.class); + assertThat(methods.hasOnlyValueAttribute()).isTrue(); + } + + @Test + void hasOnlyValueAttributeWhenHasOnlySingleNonValueAttributeReturnsFalse() { + AttributeMethods methods = AttributeMethods.forAnnotationType(NonValueOnly.class); + assertThat(methods.hasOnlyValueAttribute()).isFalse(); + } + + @Test + void hasOnlyValueAttributeWhenHasOnlyMultipleAttributesIncludingValueReturnsFalse() { + AttributeMethods methods = AttributeMethods.forAnnotationType(MultipleAttributes.class); + assertThat(methods.hasOnlyValueAttribute()).isFalse(); + } + + @Test + void indexOfNameReturnsIndex() { + AttributeMethods methods = AttributeMethods.forAnnotationType(MultipleAttributes.class); + assertThat(methods.indexOf("value")).isEqualTo(1); + } + + @Test + void indexOfMethodReturnsIndex() throws Exception { + AttributeMethods methods = AttributeMethods.forAnnotationType(MultipleAttributes.class); + Method method = MultipleAttributes.class.getDeclaredMethod("value"); + assertThat(methods.indexOf(method)).isEqualTo(1); + } + + @Test + void sizeReturnsSize() { + AttributeMethods methods = AttributeMethods.forAnnotationType(MultipleAttributes.class); + assertThat(methods.size()).isEqualTo(2); + } + + @Test + void canThrowTypeNotPresentExceptionWhenHasClassAttributeReturnsTrue() { + AttributeMethods methods = AttributeMethods.forAnnotationType(ClassValue.class); + assertThat(methods.canThrowTypeNotPresentException(0)).isTrue(); + } + + @Test + void canThrowTypeNotPresentExceptionWhenHasClassArrayAttributeReturnsTrue() { + AttributeMethods methods = AttributeMethods.forAnnotationType(ClassArrayValue.class); + assertThat(methods.canThrowTypeNotPresentException(0)).isTrue(); + } + + @Test + void canThrowTypeNotPresentExceptionWhenNotClassOrClassArrayAttributeReturnsFalse() { + AttributeMethods methods = AttributeMethods.forAnnotationType(ValueOnly.class); + assertThat(methods.canThrowTypeNotPresentException(0)).isFalse(); + } + + @Test + void hasDefaultValueMethodWhenHasDefaultValueMethodReturnsTrue() { + AttributeMethods methods = AttributeMethods.forAnnotationType(DefaultValueAttribute.class); + assertThat(methods.hasDefaultValueMethod()).isTrue(); + } + + @Test + void hasDefaultValueMethodWhenHasNoDefaultValueMethodsReturnsFalse() { + AttributeMethods methods = AttributeMethods.forAnnotationType(MultipleAttributes.class); + assertThat(methods.hasDefaultValueMethod()).isFalse(); + } + + @Test + void isValidWhenHasTypeNotPresentExceptionReturnsFalse() { + ClassValue annotation = mockAnnotation(ClassValue.class); + given(annotation.value()).willThrow(TypeNotPresentException.class); + AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType()); + assertThat(attributes.isValid(annotation)).isFalse(); + } + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + void isValidWhenDoesNotHaveTypeNotPresentExceptionReturnsTrue() { + ClassValue annotation = mock(ClassValue.class); + given(annotation.value()).willReturn((Class) InputStream.class); + AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType()); + assertThat(attributes.isValid(annotation)).isTrue(); + } + + @Test + void validateWhenHasTypeNotPresentExceptionThrowsException() { + ClassValue annotation = mockAnnotation(ClassValue.class); + given(annotation.value()).willThrow(TypeNotPresentException.class); + AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType()); + assertThatIllegalStateException().isThrownBy(() -> attributes.validate(annotation)); + } + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + void validateWhenDoesNotHaveTypeNotPresentExceptionThrowsNothing() { + ClassValue annotation = mockAnnotation(ClassValue.class); + given(annotation.value()).willReturn((Class) InputStream.class); + AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType()); + attributes.validate(annotation); + } + + private List getAll(AttributeMethods attributes) { + List result = new ArrayList<>(attributes.size()); + for (int i = 0; i < attributes.size(); i++) { + result.add(attributes.get(i)); + } + return result; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private A mockAnnotation(Class annotationType) { + A annotation = mock(annotationType); + given(annotation.annotationType()).willReturn((Class) annotationType); + return annotation; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface NoAttributes { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface MultipleAttributes { + + int intValue(); + + String value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ValueOnly { + + String value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface NonValueOnly { + + String test(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ClassValue { + + Class value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ClassArrayValue { + + Class[] value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface DefaultValueAttribute { + + String one(); + + String two(); + + String three() default "3"; + + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/ComposedRepeatableAnnotationsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/ComposedRepeatableAnnotationsTests.java new file mode 100644 index 0000000..6550133 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/ComposedRepeatableAnnotationsTests.java @@ -0,0 +1,381 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.AnnotatedElement; +import java.util.Iterator; +import java.util.Set; + +import org.assertj.core.api.ThrowableAssert.ThrowingCallable; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.springframework.core.annotation.AnnotatedElementUtils.findMergedRepeatableAnnotations; +import static org.springframework.core.annotation.AnnotatedElementUtils.getMergedRepeatableAnnotations; + +/** + * Unit tests that verify support for getting and finding all composed, repeatable + * annotations on a single annotated element. + * + *

    See SPR-13973. + * + * @author Sam Brannen + * @since 4.3 + * @see AnnotatedElementUtils#getMergedRepeatableAnnotations + * @see AnnotatedElementUtils#findMergedRepeatableAnnotations + * @see AnnotatedElementUtilsTests + * @see MultipleComposedAnnotationsOnSingleAnnotatedElementTests + */ +class ComposedRepeatableAnnotationsTests { + + + @Test + void getNonRepeatableAnnotation() { + expectNonRepeatableAnnotation(() -> + getMergedRepeatableAnnotations(getClass(), NonRepeatable.class)); + } + + @Test + void getInvalidRepeatableAnnotationContainerMissingValueAttribute() { + expectContainerMissingValueAttribute(() -> + getMergedRepeatableAnnotations(getClass(), InvalidRepeatable.class, ContainerMissingValueAttribute.class)); + } + + @Test + void getInvalidRepeatableAnnotationContainerWithNonArrayValueAttribute() { + expectContainerWithNonArrayValueAttribute(() -> + getMergedRepeatableAnnotations(getClass(), InvalidRepeatable.class, ContainerWithNonArrayValueAttribute.class)); + } + + @Test + void getInvalidRepeatableAnnotationContainerWithArrayValueAttributeButWrongComponentType() { + expectContainerWithArrayValueAttributeButWrongComponentType(() -> + getMergedRepeatableAnnotations(getClass(), InvalidRepeatable.class, ContainerWithArrayValueAttributeButWrongComponentType.class)); + } + + @Test + void getRepeatableAnnotationsOnClass() { + assertGetRepeatableAnnotations(RepeatableClass.class); + } + + @Test + void getRepeatableAnnotationsOnSuperclass() { + assertGetRepeatableAnnotations(SubRepeatableClass.class); + } + + @Test + void getComposedRepeatableAnnotationsOnClass() { + assertGetRepeatableAnnotations(ComposedRepeatableClass.class); + } + + @Test + void getComposedRepeatableAnnotationsMixedWithContainerOnClass() { + assertGetRepeatableAnnotations(ComposedRepeatableMixedWithContainerClass.class); + } + + @Test + void getComposedContainerForRepeatableAnnotationsOnClass() { + assertGetRepeatableAnnotations(ComposedContainerClass.class); + } + + @Test + void getNoninheritedComposedRepeatableAnnotationsOnClass() { + Class element = NoninheritedRepeatableClass.class; + Set annotations = getMergedRepeatableAnnotations(element, Noninherited.class); + assertNoninheritedRepeatableAnnotations(annotations); + } + + @Test + void getNoninheritedComposedRepeatableAnnotationsOnSuperclass() { + Class element = SubNoninheritedRepeatableClass.class; + Set annotations = getMergedRepeatableAnnotations(element, Noninherited.class); + assertThat(annotations).isNotNull(); + assertThat(annotations.size()).isEqualTo(0); + } + + @Test + void findNonRepeatableAnnotation() { + expectNonRepeatableAnnotation(() -> + findMergedRepeatableAnnotations(getClass(), NonRepeatable.class)); + } + + @Test + void findInvalidRepeatableAnnotationContainerMissingValueAttribute() { + expectContainerMissingValueAttribute(() -> + findMergedRepeatableAnnotations(getClass(), InvalidRepeatable.class, ContainerMissingValueAttribute.class)); + } + + @Test + void findInvalidRepeatableAnnotationContainerWithNonArrayValueAttribute() { + expectContainerWithNonArrayValueAttribute(() -> + findMergedRepeatableAnnotations(getClass(), InvalidRepeatable.class, ContainerWithNonArrayValueAttribute.class)); + } + + @Test + void findInvalidRepeatableAnnotationContainerWithArrayValueAttributeButWrongComponentType() { + expectContainerWithArrayValueAttributeButWrongComponentType(() -> + findMergedRepeatableAnnotations(getClass(), InvalidRepeatable.class, + ContainerWithArrayValueAttributeButWrongComponentType.class)); + } + + @Test + void findRepeatableAnnotationsOnClass() { + assertFindRepeatableAnnotations(RepeatableClass.class); + } + + @Test + void findRepeatableAnnotationsOnSuperclass() { + assertFindRepeatableAnnotations(SubRepeatableClass.class); + } + + @Test + void findComposedRepeatableAnnotationsOnClass() { + assertFindRepeatableAnnotations(ComposedRepeatableClass.class); + } + + @Test + void findComposedRepeatableAnnotationsMixedWithContainerOnClass() { + assertFindRepeatableAnnotations(ComposedRepeatableMixedWithContainerClass.class); + } + + @Test + void findNoninheritedComposedRepeatableAnnotationsOnClass() { + Class element = NoninheritedRepeatableClass.class; + Set annotations = findMergedRepeatableAnnotations(element, Noninherited.class); + assertNoninheritedRepeatableAnnotations(annotations); + } + + @Test + void findNoninheritedComposedRepeatableAnnotationsOnSuperclass() { + Class element = SubNoninheritedRepeatableClass.class; + Set annotations = findMergedRepeatableAnnotations(element, Noninherited.class); + assertNoninheritedRepeatableAnnotations(annotations); + } + + @Test + void findComposedContainerForRepeatableAnnotationsOnClass() { + assertFindRepeatableAnnotations(ComposedContainerClass.class); + } + + private void expectNonRepeatableAnnotation(ThrowingCallable throwingCallable) { + assertThatIllegalArgumentException().isThrownBy(throwingCallable) + .withMessageStartingWith("Annotation type must be a repeatable annotation") + .withMessageContaining("failed to resolve container type for") + .withMessageContaining(NonRepeatable.class.getName()); + } + + private void expectContainerMissingValueAttribute(ThrowingCallable throwingCallable) { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(throwingCallable) + .withMessageStartingWith("Invalid declaration of container type") + .withMessageContaining(ContainerMissingValueAttribute.class.getName()) + .withMessageContaining("for repeatable annotation") + .withMessageContaining(InvalidRepeatable.class.getName()) + .withCauseExactlyInstanceOf(NoSuchMethodException.class); + } + + private void expectContainerWithNonArrayValueAttribute(ThrowingCallable throwingCallable) { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(throwingCallable) + .withMessageStartingWith("Container type") + .withMessageContaining(ContainerWithNonArrayValueAttribute.class.getName()) + .withMessageContaining("must declare a 'value' attribute for an array of type") + .withMessageContaining(InvalidRepeatable.class.getName()); + } + + private void expectContainerWithArrayValueAttributeButWrongComponentType(ThrowingCallable throwingCallable) { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(throwingCallable) + .withMessageStartingWith("Container type") + .withMessageContaining(ContainerWithArrayValueAttributeButWrongComponentType.class.getName()) + .withMessageContaining("must declare a 'value' attribute for an array of type") + .withMessageContaining(InvalidRepeatable.class.getName()); + } + + private void assertGetRepeatableAnnotations(AnnotatedElement element) { + assertThat(element).isNotNull(); + + Set peteRepeats = getMergedRepeatableAnnotations(element, PeteRepeat.class); + assertThat(peteRepeats).isNotNull(); + assertThat(peteRepeats.size()).isEqualTo(3); + + Iterator iterator = peteRepeats.iterator(); + assertThat(iterator.next().value()).isEqualTo("A"); + assertThat(iterator.next().value()).isEqualTo("B"); + assertThat(iterator.next().value()).isEqualTo("C"); + } + + private void assertFindRepeatableAnnotations(AnnotatedElement element) { + assertThat(element).isNotNull(); + + Set peteRepeats = findMergedRepeatableAnnotations(element, PeteRepeat.class); + assertThat(peteRepeats).isNotNull(); + assertThat(peteRepeats.size()).isEqualTo(3); + + Iterator iterator = peteRepeats.iterator(); + assertThat(iterator.next().value()).isEqualTo("A"); + assertThat(iterator.next().value()).isEqualTo("B"); + assertThat(iterator.next().value()).isEqualTo("C"); + } + + private void assertNoninheritedRepeatableAnnotations(Set annotations) { + assertThat(annotations).isNotNull(); + assertThat(annotations.size()).isEqualTo(3); + + Iterator iterator = annotations.iterator(); + assertThat(iterator.next().value()).isEqualTo("A"); + assertThat(iterator.next().value()).isEqualTo("B"); + assertThat(iterator.next().value()).isEqualTo("C"); + } + + + // ------------------------------------------------------------------------- + + @Retention(RetentionPolicy.RUNTIME) + @interface NonRepeatable { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ContainerMissingValueAttribute { + // InvalidRepeatable[] value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ContainerWithNonArrayValueAttribute { + + InvalidRepeatable value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ContainerWithArrayValueAttributeButWrongComponentType { + + String[] value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @interface InvalidRepeatable { + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface PeteRepeats { + + PeteRepeat[] value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @Repeatable(PeteRepeats.class) + @interface PeteRepeat { + + String value(); + } + + @PeteRepeat("shadowed") + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface ForPetesSake { + + @AliasFor(annotation = PeteRepeat.class) + String value(); + } + + @PeteRepeat("shadowed") + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface ForTheLoveOfFoo { + + @AliasFor(annotation = PeteRepeat.class) + String value(); + } + + @PeteRepeats({ @PeteRepeat("B"), @PeteRepeat("C") }) + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface ComposedContainer { + } + + @PeteRepeat("A") + @PeteRepeats({ @PeteRepeat("B"), @PeteRepeat("C") }) + static class RepeatableClass { + } + + static class SubRepeatableClass extends RepeatableClass { + } + + @ForPetesSake("B") + @ForTheLoveOfFoo("C") + @PeteRepeat("A") + static class ComposedRepeatableClass { + } + + @ForPetesSake("C") + @PeteRepeats(@PeteRepeat("A")) + @PeteRepeat("B") + static class ComposedRepeatableMixedWithContainerClass { + } + + @PeteRepeat("A") + @ComposedContainer + static class ComposedContainerClass { + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @interface Noninheriteds { + + Noninherited[] value(); + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Repeatable(Noninheriteds.class) + @interface Noninherited { + + @AliasFor("name") + String value() default ""; + + @AliasFor("value") + String name() default ""; + } + + @Noninherited(name = "shadowed") + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @interface ComposedNoninherited { + + @AliasFor(annotation = Noninherited.class) + String name() default ""; + } + + @ComposedNoninherited(name = "C") + @Noninheriteds({ @Noninherited(value = "A"), @Noninherited(name = "B") }) + static class NoninheritedRepeatableClass { + } + + static class SubNoninheritedRepeatableClass extends NoninheritedRepeatableClass { + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationClassLoaderTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationClassLoaderTests.java new file mode 100644 index 0000000..c21ab51 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationClassLoaderTests.java @@ -0,0 +1,171 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.OverridingClassLoader; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MergedAnnotation} to ensure the correct class loader is + * used. + * + * @author Phillip Webb + * @since 5.2 + */ +class MergedAnnotationClassLoaderTests { + + private static final String TEST_ANNOTATION = TestAnnotation.class.getName(); + + private static final String TEST_META_ANNOTATION = TestMetaAnnotation.class.getName(); + + private static final String WITH_TEST_ANNOTATION = WithTestAnnotation.class.getName(); + + private static final String TEST_REFERENCE = TestReference.class.getName(); + + @Test + void synthesizedUsesCorrectClassLoader() throws Exception { + ClassLoader parent = getClass().getClassLoader(); + TestClassLoader child = new TestClassLoader(parent); + Class source = child.loadClass(WITH_TEST_ANNOTATION); + Annotation annotation = getDeclaredAnnotation(source, TEST_ANNOTATION); + Annotation metaAnnotation = getDeclaredAnnotation(annotation.annotationType(), + TEST_META_ANNOTATION); + // We should have loaded the source and initial annotation from child + assertThat(source.getClassLoader()).isEqualTo(child); + assertThat(annotation.getClass().getClassLoader()).isEqualTo(child); + assertThat(annotation.annotationType().getClassLoader()).isEqualTo(child); + // The meta-annotation should have been loaded by the parent + assertThat(metaAnnotation.getClass().getClassLoader()).isEqualTo(parent); + assertThat(metaAnnotation.getClass().getClassLoader()).isEqualTo(parent); + assertThat( + getEnumAttribute(metaAnnotation).getClass().getClassLoader()).isEqualTo( + parent); + assertThat(getClassAttribute(metaAnnotation).getClassLoader()).isEqualTo(child); + // MergedAnnotation should follow the same class loader logic + MergedAnnotations mergedAnnotations = MergedAnnotations.from(source); + Annotation synthesized = mergedAnnotations.get(TEST_ANNOTATION).synthesize(); + Annotation synthesizedMeta = mergedAnnotations.get( + TEST_META_ANNOTATION).synthesize(); + assertThat(synthesized.getClass().getClassLoader()).isEqualTo(child); + assertThat(synthesized.annotationType().getClassLoader()).isEqualTo(child); + assertThat(synthesizedMeta.getClass().getClassLoader()).isEqualTo(parent); + assertThat(synthesizedMeta.getClass().getClassLoader()).isEqualTo(parent); + assertThat(getClassAttribute(synthesizedMeta).getClassLoader()).isEqualTo(child); + assertThat( + getEnumAttribute(synthesizedMeta).getClass().getClassLoader()).isEqualTo( + parent); + assertThat(synthesized).isEqualTo(annotation); + assertThat(synthesizedMeta).isEqualTo(metaAnnotation); + // Also check utils version + Annotation utilsMeta = AnnotatedElementUtils.getMergedAnnotation(source, + TestMetaAnnotation.class); + assertThat(utilsMeta.getClass().getClassLoader()).isEqualTo(parent); + assertThat(getClassAttribute(utilsMeta).getClassLoader()).isEqualTo(child); + assertThat(getEnumAttribute(utilsMeta).getClass().getClassLoader()).isEqualTo( + parent); + assertThat(utilsMeta).isEqualTo(metaAnnotation); + } + + private Class getClassAttribute(Annotation annotation) throws Exception { + return (Class) getAttributeValue(annotation, "classValue"); + } + + private Enum getEnumAttribute(Annotation annotation) throws Exception { + return (Enum) getAttributeValue(annotation, "enumValue"); + } + + private Object getAttributeValue(Annotation annotation, String name) + throws Exception { + Method classValueMethod = annotation.annotationType().getDeclaredMethod(name); + classValueMethod.setAccessible(true); + return classValueMethod.invoke(annotation); + } + + private Annotation getDeclaredAnnotation(Class element, String annotationType) { + for (Annotation annotation : element.getDeclaredAnnotations()) { + if (annotation.annotationType().getName().equals(annotationType)) { + return annotation; + } + } + return null; + } + + private static class TestClassLoader extends OverridingClassLoader { + + public TestClassLoader(ClassLoader parent) { + super(parent); + } + + @Override + protected boolean isEligibleForOverriding(String className) { + return WITH_TEST_ANNOTATION.equals(className) + || TEST_ANNOTATION.equals(className) + || TEST_REFERENCE.equals(className); + } + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface TestMetaAnnotation { + + @AliasFor("d") + String c() default ""; + + @AliasFor("c") + String d() default ""; + + Class classValue(); + + TestEnum enumValue(); + + } + + @TestMetaAnnotation(classValue = TestReference.class, enumValue = TestEnum.TWO) + @Retention(RetentionPolicy.RUNTIME) + static @interface TestAnnotation { + + @AliasFor("b") + String a() default ""; + + @AliasFor("a") + String b() default ""; + + } + + @TestAnnotation + static class WithTestAnnotation { + + } + + static class TestReference { + + } + + static enum TestEnum { + + ONE, TWO, THREE + + } +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationCollectorsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationCollectorsTests.java new file mode 100644 index 0000000..0aaeb57 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationCollectorsTests.java @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.annotation.MergedAnnotation.Adapt; +import org.springframework.util.MultiValueMap; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MergedAnnotationCollectors}. + * + * @author Phillip Webb + */ +class MergedAnnotationCollectorsTests { + + @Test + void toAnnotationSetCollectsLinkedHashSetWithSynthesizedAnnotations() { + Set set = stream().collect( + MergedAnnotationCollectors.toAnnotationSet()); + assertThat(set).isInstanceOf(LinkedHashSet.class).flatExtracting( + TestAnnotation::value).containsExactly("a", "b", "c"); + assertThat(set).allMatch(SynthesizedAnnotation.class::isInstance); + } + + @Test + void toAnnotationArrayCollectsAnnotationArrayWithSynthesizedAnnotations() { + Annotation[] array = stream().collect( + MergedAnnotationCollectors.toAnnotationArray()); + assertThat(Arrays.stream(array).map( + annotation -> ((TestAnnotation) annotation).value())).containsExactly("a", + "b", "c"); + assertThat(array).allMatch(SynthesizedAnnotation.class::isInstance); + } + + @Test + void toSuppliedAnnotationArrayCollectsAnnotationArrayWithSynthesizedAnnotations() { + TestAnnotation[] array = stream().collect( + MergedAnnotationCollectors.toAnnotationArray(TestAnnotation[]::new)); + assertThat(Arrays.stream(array).map(TestAnnotation::value)).containsExactly("a", + "b", "c"); + assertThat(array).allMatch(SynthesizedAnnotation.class::isInstance); + } + + @Test + void toMultiValueMapCollectsMultiValueMap() { + MultiValueMap map = stream().map( + MergedAnnotation::filterDefaultValues).collect( + MergedAnnotationCollectors.toMultiValueMap( + Adapt.CLASS_TO_STRING)); + assertThat(map.get("value")).containsExactly("a", "b", "c"); + assertThat(map.get("extra")).containsExactly("java.lang.String", + "java.lang.Integer"); + } + + @Test + void toFinishedMultiValueMapCollectsMultiValueMap() { + MultiValueMap map = stream().collect( + MergedAnnotationCollectors.toMultiValueMap(result -> { + result.add("finished", true); + return result; + })); + assertThat(map.get("value")).containsExactly("a", "b", "c"); + assertThat(map.get("extra")).containsExactly(void.class, String.class, + Integer.class); + assertThat(map.get("finished")).containsExactly(true); + } + + private Stream> stream() { + return MergedAnnotations.from(WithTestAnnotations.class).stream(TestAnnotation.class); + } + + + @Retention(RetentionPolicy.RUNTIME) + @Repeatable(TestAnnotations.class) + @interface TestAnnotation { + + @AliasFor("name") + String value() default ""; + + @AliasFor("value") + String name() default ""; + + Class extra() default void.class; + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface TestAnnotations { + + TestAnnotation[] value(); + } + + @TestAnnotation("a") + @TestAnnotation(name = "b", extra = String.class) + @TestAnnotation(name = "c", extra = Integer.class) + static class WithTestAnnotations { + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationPredicatesTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationPredicatesTests.java new file mode 100644 index 0000000..96e94ca --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationPredicatesTests.java @@ -0,0 +1,162 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link MergedAnnotationPredicates}. + * + * @author Phillip Webb + */ +class MergedAnnotationPredicatesTests { + + @Test + void typeInStringArrayWhenNameMatchesAccepts() { + MergedAnnotation annotation = MergedAnnotations.from( + WithTestAnnotation.class).get(TestAnnotation.class); + assertThat(MergedAnnotationPredicates.typeIn( + TestAnnotation.class.getName())).accepts(annotation); + } + + @Test + void typeInStringArrayWhenNameDoesNotMatchRejects() { + MergedAnnotation annotation = MergedAnnotations.from( + WithTestAnnotation.class).get(TestAnnotation.class); + assertThat(MergedAnnotationPredicates.typeIn( + MissingAnnotation.class.getName())).rejects(annotation); + } + + @Test + void typeInClassArrayWhenNameMatchesAccepts() { + MergedAnnotation annotation = + MergedAnnotations.from(WithTestAnnotation.class).get(TestAnnotation.class); + assertThat(MergedAnnotationPredicates.typeIn(TestAnnotation.class)).accepts(annotation); + } + + @Test + void typeInClassArrayWhenNameDoesNotMatchRejects() { + MergedAnnotation annotation = + MergedAnnotations.from(WithTestAnnotation.class).get(TestAnnotation.class); + assertThat(MergedAnnotationPredicates.typeIn(MissingAnnotation.class)).rejects(annotation); + } + + @Test + void typeInCollectionWhenMatchesStringInCollectionAccepts() { + MergedAnnotation annotation = MergedAnnotations.from( + WithTestAnnotation.class).get(TestAnnotation.class); + assertThat(MergedAnnotationPredicates.typeIn( + Collections.singleton(TestAnnotation.class.getName()))).accepts(annotation); + } + + @Test + void typeInCollectionWhenMatchesClassInCollectionAccepts() { + MergedAnnotation annotation = MergedAnnotations.from( + WithTestAnnotation.class).get(TestAnnotation.class); + assertThat(MergedAnnotationPredicates.typeIn( + Collections.singleton(TestAnnotation.class))).accepts(annotation); + } + + @Test + void typeInCollectionWhenDoesNotMatchAnyRejects() { + MergedAnnotation annotation = MergedAnnotations.from( + WithTestAnnotation.class).get(TestAnnotation.class); + assertThat(MergedAnnotationPredicates.typeIn(Arrays.asList( + MissingAnnotation.class.getName(), MissingAnnotation.class))).rejects(annotation); + } + + @Test + void firstRunOfAcceptsOnlyFirstRun() { + List> filtered = MergedAnnotations.from( + WithMultipleTestAnnotation.class).stream(TestAnnotation.class).filter( + MergedAnnotationPredicates.firstRunOf( + this::firstCharOfValue)).collect(Collectors.toList()); + assertThat(filtered.stream().map( + annotation -> annotation.getString("value"))).containsExactly("a1", "a2", "a3"); + } + + @Test + void firstRunOfWhenValueExtractorIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> + MergedAnnotationPredicates.firstRunOf(null)); + } + + @Test + void uniqueAcceptsUniquely() { + List> filtered = MergedAnnotations.from( + WithMultipleTestAnnotation.class).stream(TestAnnotation.class).filter( + MergedAnnotationPredicates.unique( + this::firstCharOfValue)).collect(Collectors.toList()); + assertThat(filtered.stream().map( + annotation -> annotation.getString("value"))).containsExactly("a1", "b1", "c1"); + } + + @Test + void uniqueWhenKeyExtractorIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> + MergedAnnotationPredicates.unique(null)); + } + + private char firstCharOfValue(MergedAnnotation annotation) { + return annotation.getString("value").charAt(0); + } + + + @Retention(RetentionPolicy.RUNTIME) + @Repeatable(TestAnnotations.class) + @interface TestAnnotation { + + String value() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface TestAnnotations { + + TestAnnotation[] value(); + } + + @interface MissingAnnotation { + } + + @TestAnnotation("test") + static class WithTestAnnotation { + } + + @TestAnnotation("a1") + @TestAnnotation("a2") + @TestAnnotation("a3") + @TestAnnotation("b1") + @TestAnnotation("b2") + @TestAnnotation("b3") + @TestAnnotation("c1") + @TestAnnotation("c2") + @TestAnnotation("c3") + static class WithMultipleTestAnnotation { + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsCollectionTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsCollectionTests.java new file mode 100644 index 0000000..90fd632 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsCollectionTests.java @@ -0,0 +1,314 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Spliterator; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link MergedAnnotationsCollection}. + * + * @author Phillip Webb + */ +class MergedAnnotationsCollectionTests { + + @Test + void ofWhenDirectAnnotationsIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy( + () -> MergedAnnotationsCollection.of(null)).withMessage( + "Annotations must not be null"); + } + + @Test + void ofWhenEmptyReturnsSharedNoneInstance() { + MergedAnnotations annotations = MergedAnnotationsCollection.of(new ArrayList<>()); + assertThat(annotations).isSameAs(TypeMappedAnnotations.NONE); + } + + @Test + void createWhenAnnotationIsNotDirectlyPresentThrowsException() { + MergedAnnotation annotation = mock(MergedAnnotation.class); + given(annotation.isDirectlyPresent()).willReturn(false); + assertThatIllegalArgumentException().isThrownBy(() -> + MergedAnnotationsCollection.of(Collections.singleton(annotation))) + .withMessage("Annotation must be directly present"); + } + + @Test + void createWhenAnnotationAggregateIndexIsNotZeroThrowsException() { + MergedAnnotation annotation = mock(MergedAnnotation.class); + given(annotation.isDirectlyPresent()).willReturn(true); + given(annotation.getAggregateIndex()).willReturn(1); + assertThatIllegalArgumentException().isThrownBy(() -> + MergedAnnotationsCollection.of(Collections.singleton(annotation))) + .withMessage("Annotation must have aggregate index of zero"); + } + + @Test + void iterateIteratesInCorrectOrder() { + MergedAnnotations annotations = getDirectAndSimple(); + List> types = new ArrayList<>(); + for (MergedAnnotation annotation : annotations) { + types.add(annotation.getType()); + } + assertThat(types).containsExactly(Direct.class, Simple.class, Meta1.class, + Meta2.class, Meta11.class); + } + + @Test + void spliteratorIteratesInCorrectOrder() { + MergedAnnotations annotations = getDirectAndSimple(); + Spliterator> spliterator = annotations.spliterator(); + List> types = new ArrayList<>(); + spliterator.forEachRemaining(annotation -> types.add(annotation.getType())); + assertThat(types).containsExactly(Direct.class, Simple.class, Meta1.class, + Meta2.class, Meta11.class); + } + + @Test + void spliteratorEstimatesSize() { + MergedAnnotations annotations = getDirectAndSimple(); + Spliterator> spliterator = annotations.spliterator(); + assertThat(spliterator.estimateSize()).isEqualTo(5); + spliterator.tryAdvance( + annotation -> assertThat(annotation.getType()).isEqualTo(Direct.class)); + assertThat(spliterator.estimateSize()).isEqualTo(4); + } + + @Test + void isPresentWhenDirectlyPresentReturnsTrue() { + MergedAnnotations annotations = getDirectAndSimple(); + assertThat(annotations.isPresent(Direct.class)).isTrue(); + assertThat(annotations.isPresent(Direct.class.getName())).isTrue(); + } + + @Test + void isPresentWhenMetaPresentReturnsTrue() { + MergedAnnotations annotations = getDirectAndSimple(); + assertThat(annotations.isPresent(Meta11.class)).isTrue(); + assertThat(annotations.isPresent(Meta11.class.getName())).isTrue(); + } + + @Test + void isPresentWhenNotPresentReturnsFalse() { + MergedAnnotations annotations = getDirectAndSimple(); + assertThat(annotations.isPresent(Missing.class)).isFalse(); + assertThat(annotations.isPresent(Missing.class.getName())).isFalse(); + + } + + @Test + void isDirectlyPresentWhenDirectlyPresentReturnsTrue() { + MergedAnnotations annotations = getDirectAndSimple(); + assertThat(annotations.isDirectlyPresent(Direct.class)).isTrue(); + assertThat(annotations.isDirectlyPresent(Direct.class.getName())).isTrue(); + } + + @Test + void isDirectlyPresentWhenMetaPresentReturnsFalse() { + MergedAnnotations annotations = getDirectAndSimple(); + assertThat(annotations.isDirectlyPresent(Meta11.class)).isFalse(); + assertThat(annotations.isDirectlyPresent(Meta11.class.getName())).isFalse(); + } + + @Test + void isDirectlyPresentWhenNotPresentReturnsFalse() { + MergedAnnotations annotations = getDirectAndSimple(); + assertThat(annotations.isDirectlyPresent(Missing.class)).isFalse(); + assertThat(annotations.isDirectlyPresent(Missing.class.getName())).isFalse(); + } + + @Test + void getReturnsAppropriateAnnotation() { + MergedAnnotations annotations = getMultiRoute1(); + assertThat(annotations.get(MultiRouteTarget.class).getString( + MergedAnnotation.VALUE)).isEqualTo("12"); + assertThat(annotations.get(MultiRouteTarget.class.getName()).getString( + MergedAnnotation.VALUE)).isEqualTo("12"); + } + + @Test + void getWhenNotPresentReturnsMissing() { + MergedAnnotations annotations = getDirectAndSimple(); + assertThat(annotations.get(Missing.class)).isEqualTo(MergedAnnotation.missing()); + } + + @Test + void getWithPredicateReturnsOnlyMatching() { + MergedAnnotations annotations = getMultiRoute1(); + assertThat(annotations.get(MultiRouteTarget.class, + annotation -> annotation.getDistance() >= 3).getString( + MergedAnnotation.VALUE)).isEqualTo("111"); + } + + @Test + void getWithSelectorReturnsSelected() { + MergedAnnotations annotations = getMultiRoute1(); + MergedAnnotationSelector deepest = (existing, + candidate) -> candidate.getDistance() > existing.getDistance() ? candidate + : existing; + assertThat(annotations.get(MultiRouteTarget.class, null, deepest).getString( + MergedAnnotation.VALUE)).isEqualTo("111"); + } + + @Test + void streamStreamsInCorrectOrder() { + MergedAnnotations annotations = getDirectAndSimple(); + List> types = new ArrayList<>(); + annotations.stream().forEach(annotation -> types.add(annotation.getType())); + assertThat(types).containsExactly(Direct.class, Simple.class, Meta1.class, + Meta2.class, Meta11.class); + } + + @Test + void streamWithTypeStreamsInCorrectOrder() { + MergedAnnotations annotations = getMultiRoute1(); + List values = new ArrayList<>(); + annotations.stream(MultiRouteTarget.class).forEach( + annotation -> values.add(annotation.getString(MergedAnnotation.VALUE))); + assertThat(values).containsExactly("12", "111"); + } + + @Test + void getMetaWhenRootHasAttributeValuesShouldAliasAttributes() { + MergedAnnotation root = MergedAnnotation.of(null, null, Aliased.class, + Collections.singletonMap("testAlias", "test")); + MergedAnnotations annotations = MergedAnnotationsCollection.of( + Collections.singleton(root)); + MergedAnnotation metaAnnotation = annotations.get(AliasTarget.class); + assertThat(metaAnnotation.getString("test")).isEqualTo("test"); + } + + @Test + void getMetaWhenRootHasNoAttributeValuesShouldAliasAttributes() { + MergedAnnotation root = MergedAnnotation.of(null, null, Aliased.class, + Collections.emptyMap()); + MergedAnnotations annotations = MergedAnnotationsCollection.of( + Collections.singleton(root)); + MergedAnnotation metaAnnotation = annotations.get(AliasTarget.class); + assertThat(root.getString("testAlias")).isEqualTo("newdefault"); + assertThat(metaAnnotation.getString("test")).isEqualTo("newdefault"); + } + + private MergedAnnotations getDirectAndSimple() { + List> list = new ArrayList<>(); + list.add(MergedAnnotation.of(null, null, Direct.class, Collections.emptyMap())); + list.add(MergedAnnotation.of(null, null, Simple.class, Collections.emptyMap())); + return MergedAnnotationsCollection.of(list); + } + + private MergedAnnotations getMultiRoute1() { + List> list = new ArrayList<>(); + list.add(MergedAnnotation.of(null, null, MultiRoute1.class, + Collections.emptyMap())); + return MergedAnnotationsCollection.of(list); + } + + @Meta1 + @Meta2 + @Retention(RetentionPolicy.RUNTIME) + @interface Direct { + + } + + @Meta11 + @Retention(RetentionPolicy.RUNTIME) + @interface Meta1 { + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface Meta2 { + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface Meta11 { + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface Simple { + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface Missing { + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface MultiRouteTarget { + + String value(); + + } + + @MultiRoute11 + @MultiRoute12 + @Retention(RetentionPolicy.RUNTIME) + @interface MultiRoute1 { + + } + + @MultiRoute111 + @Retention(RetentionPolicy.RUNTIME) + @interface MultiRoute11 { + + } + + @MultiRouteTarget("12") + @Retention(RetentionPolicy.RUNTIME) + @interface MultiRoute12 { + + } + + @MultiRouteTarget("111") + @Retention(RetentionPolicy.RUNTIME) + @interface MultiRoute111 { + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasTarget { + + String test() default "default"; + + } + + @Retention(RetentionPolicy.RUNTIME) + @AliasTarget + @interface Aliased { + + @AliasFor(annotation = AliasTarget.class, attribute = "test") + String testAlias() default "newdefault"; + + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsComposedOnSingleAnnotatedElementTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsComposedOnSingleAnnotatedElementTests.java new file mode 100644 index 0000000..5c6aa83 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsComposedOnSingleAnnotatedElementTests.java @@ -0,0 +1,306 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests that verify support for finding multiple composed annotations on a single + * annotated element. + * + * @author Phillip Webb + * @author Sam Brannen + */ +class MergedAnnotationsComposedOnSingleAnnotatedElementTests { + + // See SPR-13486 + + @Test + void inheritedStrategyMultipleComposedAnnotationsOnClass() { + assertInheritedStrategyBehavior(MultipleComposedCachesClass.class); + } + + @Test + void inheritedStrategyMultipleInheritedComposedAnnotationsOnSuperclass() { + assertInheritedStrategyBehavior(SubMultipleComposedCachesClass.class); + } + + @Test + void inheritedStrategyMultipleNoninheritedComposedAnnotationsOnClass() { + MergedAnnotations annotations = MergedAnnotations.from( + MultipleNoninheritedComposedCachesClass.class, + SearchStrategy.INHERITED_ANNOTATIONS); + assertThat(stream(annotations, "value")).containsExactly("noninheritedCache1", + "noninheritedCache2"); + } + + @Test + void inheritedStrategyMultipleNoninheritedComposedAnnotationsOnSuperclass() { + MergedAnnotations annotations = MergedAnnotations.from( + SubMultipleNoninheritedComposedCachesClass.class, + SearchStrategy.INHERITED_ANNOTATIONS); + assertThat(annotations.stream(Cacheable.class)).isEmpty(); + } + + @Test + void inheritedStrategyComposedPlusLocalAnnotationsOnClass() { + assertInheritedStrategyBehavior(ComposedPlusLocalCachesClass.class); + } + + @Test + void inheritedStrategyMultipleComposedAnnotationsOnInterface() { + MergedAnnotations annotations = MergedAnnotations.from( + MultipleComposedCachesOnInterfaceClass.class, + SearchStrategy.INHERITED_ANNOTATIONS); + assertThat(annotations.stream(Cacheable.class)).isEmpty(); + } + + @Test + void inheritedStrategyMultipleComposedAnnotationsOnMethod() throws Exception { + assertInheritedStrategyBehavior( + getClass().getDeclaredMethod("multipleComposedCachesMethod")); + } + + @Test + void inheritedStrategyComposedPlusLocalAnnotationsOnMethod() throws Exception { + assertInheritedStrategyBehavior( + getClass().getDeclaredMethod("composedPlusLocalCachesMethod")); + } + + private void assertInheritedStrategyBehavior(AnnotatedElement element) { + MergedAnnotations annotations = MergedAnnotations.from(element, + SearchStrategy.INHERITED_ANNOTATIONS); + assertThat(stream(annotations, "key")).containsExactly("fooKey", "barKey"); + assertThat(stream(annotations, "value")).containsExactly("fooCache", "barCache"); + } + + @Test + void typeHierarchyStrategyMultipleComposedAnnotationsOnClass() { + assertTypeHierarchyStrategyBehavior(MultipleComposedCachesClass.class); + } + + @Test + void typeHierarchyStrategyMultipleInheritedComposedAnnotationsOnSuperclass() { + assertTypeHierarchyStrategyBehavior(SubMultipleComposedCachesClass.class); + } + + @Test + void typeHierarchyStrategyMultipleNoninheritedComposedAnnotationsOnClass() { + MergedAnnotations annotations = MergedAnnotations.from( + MultipleNoninheritedComposedCachesClass.class, SearchStrategy.TYPE_HIERARCHY); + assertThat(stream(annotations, "value")).containsExactly("noninheritedCache1", + "noninheritedCache2"); + } + + @Test + void typeHierarchyStrategyMultipleNoninheritedComposedAnnotationsOnSuperclass() { + MergedAnnotations annotations = MergedAnnotations.from( + SubMultipleNoninheritedComposedCachesClass.class, + SearchStrategy.TYPE_HIERARCHY); + assertThat(stream(annotations, "value")).containsExactly("noninheritedCache1", + "noninheritedCache2"); + } + + @Test + void typeHierarchyStrategyComposedPlusLocalAnnotationsOnClass() { + assertTypeHierarchyStrategyBehavior(ComposedPlusLocalCachesClass.class); + } + + @Test + void typeHierarchyStrategyMultipleComposedAnnotationsOnInterface() { + assertTypeHierarchyStrategyBehavior(MultipleComposedCachesOnInterfaceClass.class); + } + + @Test + void typeHierarchyStrategyComposedCacheOnInterfaceAndLocalCacheOnClass() { + assertTypeHierarchyStrategyBehavior( + ComposedCacheOnInterfaceAndLocalCacheClass.class); + } + + @Test + void typeHierarchyStrategyMultipleComposedAnnotationsOnMethod() throws Exception { + assertTypeHierarchyStrategyBehavior( + getClass().getDeclaredMethod("multipleComposedCachesMethod")); + } + + @Test + void typeHierarchyStrategyComposedPlusLocalAnnotationsOnMethod() + throws Exception { + assertTypeHierarchyStrategyBehavior( + getClass().getDeclaredMethod("composedPlusLocalCachesMethod")); + } + + @Test + void typeHierarchyStrategyMultipleComposedAnnotationsOnBridgeMethod() + throws Exception { + assertTypeHierarchyStrategyBehavior(getBridgeMethod()); + } + + private void assertTypeHierarchyStrategyBehavior(AnnotatedElement element) { + MergedAnnotations annotations = MergedAnnotations.from(element, + SearchStrategy.TYPE_HIERARCHY); + assertThat(stream(annotations, "key")).containsExactly("fooKey", "barKey"); + assertThat(stream(annotations, "value")).containsExactly("fooCache", "barCache"); + } + + Method getBridgeMethod() throws NoSuchMethodException { + List methods = new ArrayList<>(); + ReflectionUtils.doWithLocalMethods(StringGenericParameter.class, method -> { + if ("getFor".equals(method.getName())) { + methods.add(method); + } + }); + Method bridgeMethod = methods.get(0).getReturnType().equals(Object.class) + ? methods.get(0) + : methods.get(1); + assertThat(bridgeMethod.isBridge()).isTrue(); + return bridgeMethod; + } + + private Stream stream(MergedAnnotations annotations, String attributeName) { + return annotations.stream(Cacheable.class).map( + annotation -> annotation.getString(attributeName)); + } + + // @formatter:off + + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface Cacheable { + @AliasFor("cacheName") + String value() default ""; + @AliasFor("value") + String cacheName() default ""; + String key() default ""; + } + + @Cacheable("fooCache") + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface FooCache { + @AliasFor(annotation = Cacheable.class) + String key() default ""; + } + + @Cacheable("barCache") + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface BarCache { + @AliasFor(annotation = Cacheable.class) + String key(); + } + + @Cacheable("noninheritedCache1") + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @interface NoninheritedCache1 { + @AliasFor(annotation = Cacheable.class) + String key() default ""; + } + + @Cacheable("noninheritedCache2") + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @interface NoninheritedCache2 { + @AliasFor(annotation = Cacheable.class) + String key() default ""; + } + + @FooCache(key = "fooKey") + @BarCache(key = "barKey") + private static class MultipleComposedCachesClass { + } + + private static class SubMultipleComposedCachesClass + extends MultipleComposedCachesClass { + } + + @NoninheritedCache1 + @NoninheritedCache2 + private static class MultipleNoninheritedComposedCachesClass { + } + + private static class SubMultipleNoninheritedComposedCachesClass + extends MultipleNoninheritedComposedCachesClass { + } + + @Cacheable(cacheName = "fooCache", key = "fooKey") + @BarCache(key = "barKey") + private static class ComposedPlusLocalCachesClass { + } + + @FooCache(key = "fooKey") + @BarCache(key = "barKey") + private interface MultipleComposedCachesInterface { + } + + private static class MultipleComposedCachesOnInterfaceClass implements MultipleComposedCachesInterface { + } + + @BarCache(key = "barKey") + private interface ComposedCacheInterface { + } + + @Cacheable(cacheName = "fooCache", key = "fooKey") + private static class ComposedCacheOnInterfaceAndLocalCacheClass implements ComposedCacheInterface { + } + + @FooCache(key = "fooKey") + @BarCache(key = "barKey") + private void multipleComposedCachesMethod() { + } + + @Cacheable(cacheName = "fooCache", key = "fooKey") + @BarCache(key = "barKey") + private void composedPlusLocalCachesMethod() { + } + + public interface GenericParameter { + T getFor(Class cls); + } + + @SuppressWarnings("unused") + private static class StringGenericParameter implements GenericParameter { + @FooCache(key = "fooKey") + @BarCache(key = "barKey") + @Override + public String getFor(Class cls) { return "foo"; } + public String getFor(Integer integer) { return "foo"; } + } + + // @formatter:on + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java new file mode 100644 index 0000000..40b1fdf --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java @@ -0,0 +1,452 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.AnnotatedElement; +import java.util.Set; +import java.util.stream.Stream; + +import org.assertj.core.api.ThrowableTypeAssert; +import org.junit.jupiter.api.Test; + +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link MergedAnnotations} and {@link RepeatableContainers} that + * verify support for repeatable annotations. + * + * @author Phillip Webb + * @author Sam Brannen + */ +class MergedAnnotationsRepeatableAnnotationTests { + + // See SPR-13973 + + @Test + void inheritedAnnotationsWhenNonRepeatableThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> + getAnnotations(null, NonRepeatable.class, SearchStrategy.INHERITED_ANNOTATIONS, getClass())) + .satisfies(this::nonRepeatableRequirements); + } + + @Test + void inheritedAnnotationsWhenContainerMissingValueAttributeThrowsException() { + assertThatAnnotationConfigurationException().isThrownBy(() -> + getAnnotations(ContainerMissingValueAttribute.class, InvalidRepeatable.class, + SearchStrategy.INHERITED_ANNOTATIONS, getClass())) + .satisfies(this::missingValueAttributeRequirements); + } + + @Test + void inheritedAnnotationsWhenWhenNonArrayValueAttributeThrowsException() { + assertThatAnnotationConfigurationException().isThrownBy(() -> + getAnnotations(ContainerWithNonArrayValueAttribute.class, InvalidRepeatable.class, + SearchStrategy.INHERITED_ANNOTATIONS, getClass())) + .satisfies(this::nonArrayValueAttributeRequirements); + } + + @Test + void inheritedAnnotationsWhenWrongComponentTypeThrowsException() { + assertThatAnnotationConfigurationException().isThrownBy(() -> + getAnnotations(ContainerWithArrayValueAttributeButWrongComponentType.class, + InvalidRepeatable.class, SearchStrategy.INHERITED_ANNOTATIONS, getClass())) + .satisfies(this::wrongComponentTypeRequirements); + } + + @Test + void inheritedAnnotationsWhenOnClassReturnsAnnotations() { + Set annotations = getAnnotations(null, PeteRepeat.class, + SearchStrategy.INHERITED_ANNOTATIONS, RepeatableClass.class); + assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", + "C"); + } + + @Test + void inheritedAnnotationsWhenWhenOnSuperclassReturnsAnnotations() { + Set annotations = getAnnotations(null, PeteRepeat.class, + SearchStrategy.INHERITED_ANNOTATIONS, SubRepeatableClass.class); + assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", + "C"); + } + + @Test + void inheritedAnnotationsWhenComposedOnClassReturnsAnnotations() { + Set annotations = getAnnotations(null, PeteRepeat.class, + SearchStrategy.INHERITED_ANNOTATIONS, ComposedRepeatableClass.class); + assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", + "C"); + } + + @Test + void inheritedAnnotationsWhenComposedMixedWithContainerOnClassReturnsAnnotations() { + Set annotations = getAnnotations(null, PeteRepeat.class, + SearchStrategy.INHERITED_ANNOTATIONS, + ComposedRepeatableMixedWithContainerClass.class); + assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", + "C"); + } + + @Test + void inheritedAnnotationsWhenComposedContainerForRepeatableOnClassReturnsAnnotations() { + Set annotations = getAnnotations(null, PeteRepeat.class, + SearchStrategy.INHERITED_ANNOTATIONS, ComposedContainerClass.class); + assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", + "C"); + } + + @Test + void inheritedAnnotationsWhenNoninheritedComposedRepeatableOnClassReturnsAnnotations() { + Set annotations = getAnnotations(null, Noninherited.class, + SearchStrategy.INHERITED_ANNOTATIONS, NoninheritedRepeatableClass.class); + assertThat(annotations.stream().map(Noninherited::value)).containsExactly("A", + "B", "C"); + } + + @Test + void inheritedAnnotationsWhenNoninheritedComposedRepeatableOnSuperclassReturnsAnnotations() { + Set annotations = getAnnotations(null, Noninherited.class, + SearchStrategy.INHERITED_ANNOTATIONS, + SubNoninheritedRepeatableClass.class); + assertThat(annotations).isEmpty(); + } + + @Test + void typeHierarchyWhenNonRepeatableThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> + getAnnotations(null, NonRepeatable.class, SearchStrategy.TYPE_HIERARCHY, getClass())) + .satisfies(this::nonRepeatableRequirements); + } + + @Test + void typeHierarchyWhenContainerMissingValueAttributeThrowsException() { + assertThatAnnotationConfigurationException().isThrownBy(() -> + getAnnotations(ContainerMissingValueAttribute.class, InvalidRepeatable.class, + SearchStrategy.TYPE_HIERARCHY, getClass())) + .satisfies(this::missingValueAttributeRequirements); + } + + @Test + void typeHierarchyWhenWhenNonArrayValueAttributeThrowsException() { + assertThatAnnotationConfigurationException().isThrownBy(() -> + getAnnotations(ContainerWithNonArrayValueAttribute.class, InvalidRepeatable.class, + SearchStrategy.TYPE_HIERARCHY, getClass())) + .satisfies(this::nonArrayValueAttributeRequirements); + } + + @Test + void typeHierarchyWhenWrongComponentTypeThrowsException() { + assertThatAnnotationConfigurationException().isThrownBy(() -> + getAnnotations(ContainerWithArrayValueAttributeButWrongComponentType.class, + InvalidRepeatable.class, SearchStrategy.TYPE_HIERARCHY, getClass())) + .satisfies(this::wrongComponentTypeRequirements); + } + + @Test + void typeHierarchyWhenOnClassReturnsAnnotations() { + Set annotations = getAnnotations(null, PeteRepeat.class, + SearchStrategy.TYPE_HIERARCHY, RepeatableClass.class); + assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", + "C"); + } + + @Test + void typeHierarchyWhenWhenOnSuperclassReturnsAnnotations() { + Set annotations = getAnnotations(null, PeteRepeat.class, + SearchStrategy.TYPE_HIERARCHY, SubRepeatableClass.class); + assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", + "C"); + } + + @Test + void typeHierarchyWhenComposedOnClassReturnsAnnotations() { + Set annotations = getAnnotations(null, PeteRepeat.class, + SearchStrategy.TYPE_HIERARCHY, ComposedRepeatableClass.class); + assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", + "C"); + } + + @Test + void typeHierarchyWhenComposedMixedWithContainerOnClassReturnsAnnotations() { + Set annotations = getAnnotations(null, PeteRepeat.class, + SearchStrategy.TYPE_HIERARCHY, + ComposedRepeatableMixedWithContainerClass.class); + assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", + "C"); + } + + @Test + void typeHierarchyWhenComposedContainerForRepeatableOnClassReturnsAnnotations() { + Set annotations = getAnnotations(null, PeteRepeat.class, + SearchStrategy.TYPE_HIERARCHY, ComposedContainerClass.class); + assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", + "C"); + } + + @Test + void typeHierarchyAnnotationsWhenNoninheritedComposedRepeatableOnClassReturnsAnnotations() { + Set annotations = getAnnotations(null, Noninherited.class, + SearchStrategy.TYPE_HIERARCHY, NoninheritedRepeatableClass.class); + assertThat(annotations.stream().map(Noninherited::value)).containsExactly("A", + "B", "C"); + } + + @Test + void typeHierarchyAnnotationsWhenNoninheritedComposedRepeatableOnSuperclassReturnsAnnotations() { + Set annotations = getAnnotations(null, Noninherited.class, + SearchStrategy.TYPE_HIERARCHY, SubNoninheritedRepeatableClass.class); + assertThat(annotations.stream().map(Noninherited::value)).containsExactly("A", + "B", "C"); + } + + @Test + void typeHierarchyAnnotationsWithLocalComposedAnnotationWhoseRepeatableMetaAnnotationsAreFiltered() { + Class element = WithRepeatedMetaAnnotationsClass.class; + SearchStrategy searchStrategy = SearchStrategy.TYPE_HIERARCHY; + AnnotationFilter annotationFilter = PeteRepeat.class.getName()::equals; + + Set annotations = getAnnotations(null, PeteRepeat.class, searchStrategy, element, annotationFilter); + assertThat(annotations).isEmpty(); + + MergedAnnotations mergedAnnotations = MergedAnnotations.from(element, searchStrategy, + RepeatableContainers.standardRepeatables(), annotationFilter); + Stream> annotationTypes = mergedAnnotations.stream() + .map(MergedAnnotation::synthesize) + .map(Annotation::annotationType); + assertThat(annotationTypes).containsExactly(WithRepeatedMetaAnnotations.class, Noninherited.class, Noninherited.class); + } + + private Set getAnnotations(Class container, + Class repeatable, SearchStrategy searchStrategy, AnnotatedElement element) { + + return getAnnotations(container, repeatable, searchStrategy, element, AnnotationFilter.PLAIN); + } + + private Set getAnnotations(Class container, + Class repeatable, SearchStrategy searchStrategy, AnnotatedElement element, AnnotationFilter annotationFilter) { + + RepeatableContainers containers = RepeatableContainers.of(repeatable, container); + MergedAnnotations annotations = MergedAnnotations.from(element, searchStrategy, containers, annotationFilter); + return annotations.stream(repeatable).collect(MergedAnnotationCollectors.toAnnotationSet()); + } + + private void nonRepeatableRequirements(Exception ex) { + assertThat(ex.getMessage()).startsWith( + "Annotation type must be a repeatable annotation").contains( + "failed to resolve container type for", + NonRepeatable.class.getName()); + } + + private void missingValueAttributeRequirements(Exception ex) { + assertThat(ex.getMessage()).startsWith( + "Invalid declaration of container type").contains( + ContainerMissingValueAttribute.class.getName(), + "for repeatable annotation", InvalidRepeatable.class.getName()); + assertThat(ex).hasCauseInstanceOf(NoSuchMethodException.class); + } + + private void nonArrayValueAttributeRequirements(Exception ex) { + assertThat(ex.getMessage()).startsWith("Container type").contains( + ContainerWithNonArrayValueAttribute.class.getName(), + "must declare a 'value' attribute for an array of type", + InvalidRepeatable.class.getName()); + } + + private void wrongComponentTypeRequirements(Exception ex) { + assertThat(ex.getMessage()).startsWith("Container type").contains( + ContainerWithArrayValueAttributeButWrongComponentType.class.getName(), + "must declare a 'value' attribute for an array of type", + InvalidRepeatable.class.getName()); + } + + private static ThrowableTypeAssert assertThatAnnotationConfigurationException() { + return assertThatExceptionOfType(AnnotationConfigurationException.class); + } + + @Retention(RetentionPolicy.RUNTIME) + @interface NonRepeatable { + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ContainerMissingValueAttribute { + + // InvalidRepeatable[] value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ContainerWithNonArrayValueAttribute { + + InvalidRepeatable value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ContainerWithArrayValueAttributeButWrongComponentType { + + String[] value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface InvalidRepeatable { + + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface PeteRepeats { + + PeteRepeat[] value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @Repeatable(PeteRepeats.class) + @interface PeteRepeat { + + String value(); + + } + + @PeteRepeat("shadowed") + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface ForPetesSake { + + @AliasFor(annotation = PeteRepeat.class) + String value(); + + } + + @PeteRepeat("shadowed") + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface ForTheLoveOfFoo { + + @AliasFor(annotation = PeteRepeat.class) + String value(); + + } + + @PeteRepeats({ @PeteRepeat("B"), @PeteRepeat("C") }) + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface ComposedContainer { + + } + + @PeteRepeat("A") + @PeteRepeats({ @PeteRepeat("B"), @PeteRepeat("C") }) + static class RepeatableClass { + + } + + static class SubRepeatableClass extends RepeatableClass { + + } + + @ForPetesSake("B") + @ForTheLoveOfFoo("C") + @PeteRepeat("A") + static class ComposedRepeatableClass { + + } + + @ForPetesSake("C") + @PeteRepeats(@PeteRepeat("A")) + @PeteRepeat("B") + static class ComposedRepeatableMixedWithContainerClass { + + } + + @PeteRepeat("A") + @ComposedContainer + static class ComposedContainerClass { + + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @interface Noninheriteds { + + Noninherited[] value(); + + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Repeatable(Noninheriteds.class) + @interface Noninherited { + + @AliasFor("name") + String value() default ""; + + @AliasFor("value") + String name() default ""; + + } + + @Noninherited(name = "shadowed") + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @interface ComposedNoninherited { + + @AliasFor(annotation = Noninherited.class) + String name() default ""; + + } + + @ComposedNoninherited(name = "C") + @Noninheriteds({ @Noninherited(value = "A"), @Noninherited(name = "B") }) + static class NoninheritedRepeatableClass { + + } + + static class SubNoninheritedRepeatableClass extends NoninheritedRepeatableClass { + + } + + @Retention(RetentionPolicy.RUNTIME) + @PeteRepeat("A") + @PeteRepeat("B") + @interface WithRepeatedMetaAnnotations { + } + + @WithRepeatedMetaAnnotations + @PeteRepeat("C") + @Noninherited("X") + @Noninherited("Y") + static class WithRepeatedMetaAnnotationsClass { + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java new file mode 100644 index 0000000..e9e3586 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java @@ -0,0 +1,3530 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.annotation.Resource; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.Ordered; +import org.springframework.core.annotation.MergedAnnotation.Adapt; +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; +import org.springframework.core.annotation.subpackage.NonPublicAnnotatedClass; +import org.springframework.core.testfixture.stereotype.Component; +import org.springframework.core.testfixture.stereotype.Indexed; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.MultiValueMap; +import org.springframework.util.ReflectionUtils; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.entry; + +/** + * Tests for {@link MergedAnnotations} and {@link MergedAnnotation}. These tests + * cover common usage scenarios and were mainly ported from the original + * {@code AnnotationUtils} and {@code AnnotatedElementUtils} tests. + * + * @author Phillip Webb + * @author Rod Johnson + * @author Juergen Hoeller + * @author Sam Brannen + * @author Chris Beams + * @author Oleg Zhurakousky + * @author Rossen Stoyanchev + * @see MergedAnnotationsRepeatableAnnotationTests + * @see MergedAnnotationClassLoaderTests + */ +class MergedAnnotationsTests { + + @Test + void fromPreconditions() { + SearchStrategy strategy = SearchStrategy.DIRECT; + RepeatableContainers containers = RepeatableContainers.standardRepeatables(); + + assertThatIllegalArgumentException() + .isThrownBy(() -> MergedAnnotations.from(getClass(), strategy, null, AnnotationFilter.PLAIN)) + .withMessage("RepeatableContainers must not be null"); + assertThatIllegalArgumentException() + .isThrownBy(() -> MergedAnnotations.from(getClass(), strategy, containers, null)) + .withMessage("AnnotationFilter must not be null"); + + assertThatIllegalArgumentException() + .isThrownBy(() -> MergedAnnotations.from(getClass(), new Annotation[0], null, AnnotationFilter.PLAIN)) + .withMessage("RepeatableContainers must not be null"); + assertThatIllegalArgumentException() + .isThrownBy(() -> MergedAnnotations.from(getClass(), new Annotation[0], containers, null)) + .withMessage("AnnotationFilter must not be null"); + } + + @Test + void streamWhenFromNonAnnotatedClass() { + assertThat(MergedAnnotations.from(NonAnnotatedClass.class). + stream(TransactionalComponent.class)).isEmpty(); + } + + @Test + void streamWhenFromClassWithMetaDepth1() { + Stream> classes = MergedAnnotations.from(TransactionalComponent.class) + .stream().map(MergedAnnotation::getType); + assertThat(classes).containsExactly(Transactional.class, Component.class, Indexed.class); + } + + @Test + void streamWhenFromClassWithMetaDepth2() { + Stream> classes = MergedAnnotations.from(ComposedTransactionalComponent.class) + .stream().map(MergedAnnotation::getType); + assertThat(classes).containsExactly(TransactionalComponent.class, + Transactional.class, Component.class, Indexed.class); + } + + @Test + void isPresentWhenFromNonAnnotatedClass() { + assertThat(MergedAnnotations.from(NonAnnotatedClass.class). + isPresent(Transactional.class)).isFalse(); + } + + @Test + void isPresentWhenFromAnnotationClassWithMetaDepth0() { + assertThat(MergedAnnotations.from(TransactionalComponent.class). + isPresent(TransactionalComponent.class)).isFalse(); + } + + @Test + void isPresentWhenFromAnnotationClassWithMetaDepth1() { + MergedAnnotations annotations = MergedAnnotations.from(TransactionalComponent.class); + assertThat(annotations.isPresent(Transactional.class)).isTrue(); + assertThat(annotations.isPresent(Component.class)).isTrue(); + } + + @Test + void isPresentWhenFromAnnotationClassWithMetaDepth2() { + MergedAnnotations annotations = MergedAnnotations.from( + ComposedTransactionalComponent.class); + assertThat(annotations.isPresent(Transactional.class)).isTrue(); + assertThat(annotations.isPresent(Component.class)).isTrue(); + assertThat(annotations.isPresent(ComposedTransactionalComponent.class)).isFalse(); + } + + @Test + void isPresentWhenFromClassWithMetaDepth0() { + assertThat(MergedAnnotations.from(TransactionalComponentClass.class).isPresent( + TransactionalComponent.class)).isTrue(); + } + + @Test + void isPresentWhenFromSubclassWithMetaDepth0() { + assertThat(MergedAnnotations.from(SubTransactionalComponentClass.class).isPresent( + TransactionalComponent.class)).isFalse(); + } + + @Test + void isPresentWhenFromClassWithMetaDepth1() { + MergedAnnotations annotations = MergedAnnotations.from( + TransactionalComponentClass.class); + assertThat(annotations.isPresent(Transactional.class)).isTrue(); + assertThat(annotations.isPresent(Component.class)).isTrue(); + } + + @Test + void isPresentWhenFromClassWithMetaDepth2() { + MergedAnnotations annotations = MergedAnnotations.from( + ComposedTransactionalComponentClass.class); + assertThat(annotations.isPresent(Transactional.class)).isTrue(); + assertThat(annotations.isPresent(Component.class)).isTrue(); + assertThat(annotations.isPresent(ComposedTransactionalComponent.class)).isTrue(); + } + + @Test + void getParent() { + MergedAnnotations annotations = MergedAnnotations.from(ComposedTransactionalComponentClass.class); + assertThat(annotations.get(TransactionalComponent.class).getMetaSource().getType()) + .isEqualTo(ComposedTransactionalComponent.class); + } + + @Test + void getRootWhenNotDirect() { + MergedAnnotations annotations = MergedAnnotations.from(ComposedTransactionalComponentClass.class); + MergedAnnotation annotation = annotations.get(TransactionalComponent.class); + assertThat(annotation.getDistance()).isGreaterThan(0); + assertThat(annotation.getRoot().getType()).isEqualTo(ComposedTransactionalComponent.class); + } + + @Test + void getRootWhenDirect() { + MergedAnnotations annotations = MergedAnnotations.from(ComposedTransactionalComponentClass.class); + MergedAnnotation annotation = annotations.get(ComposedTransactionalComponent.class); + assertThat(annotation.getDistance()).isEqualTo(0); + assertThat(annotation.getRoot()).isSameAs(annotation); + } + + @Test + void getMetaTypes() { + MergedAnnotation annotation = MergedAnnotations.from( + ComposedTransactionalComponentClass.class).get( + TransactionalComponent.class); + assertThat(annotation.getMetaTypes()).containsExactly( + ComposedTransactionalComponent.class, TransactionalComponent.class); + } + + @Test + void collectMultiValueMapFromNonAnnotatedClass() { + MultiValueMap map = MergedAnnotations.from( + NonAnnotatedClass.class).stream(Transactional.class).collect( + MergedAnnotationCollectors.toMultiValueMap()); + assertThat(map).isEmpty(); + } + + @Test + void collectMultiValueMapFromClassWithLocalAnnotation() { + MultiValueMap map = MergedAnnotations.from(TxConfig.class).stream( + Transactional.class).collect( + MergedAnnotationCollectors.toMultiValueMap()); + assertThat(map).contains(entry("value", Arrays.asList("TxConfig"))); + } + + @Test + void collectMultiValueMapFromClassWithLocalComposedAnnotationAndInheritedAnnotation() { + MultiValueMap map = MergedAnnotations.from( + SubClassWithInheritedAnnotation.class, + SearchStrategy.INHERITED_ANNOTATIONS).stream(Transactional.class).collect( + MergedAnnotationCollectors.toMultiValueMap()); + assertThat(map).contains( + entry("qualifier", Arrays.asList("composed2", "transactionManager"))); + } + + @Test + void collectMultiValueMapFavorsInheritedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() { + MultiValueMap map = MergedAnnotations.from( + SubSubClassWithInheritedAnnotation.class, + SearchStrategy.INHERITED_ANNOTATIONS).stream(Transactional.class).collect( + MergedAnnotationCollectors.toMultiValueMap()); + assertThat(map).contains(entry("qualifier", Arrays.asList("transactionManager"))); + } + + @Test + void collectMultiValueMapFavorsInheritedComposedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() { + MultiValueMap map = MergedAnnotations.from( + SubSubClassWithInheritedComposedAnnotation.class, + SearchStrategy.INHERITED_ANNOTATIONS).stream(Transactional.class).collect( + MergedAnnotationCollectors.toMultiValueMap()); + assertThat(map).contains(entry("qualifier", Arrays.asList("composed1"))); + } + + /** + * If the "value" entry contains both "DerivedTxConfig" AND "TxConfig", then + * the algorithm is accidentally picking up shadowed annotations of the same + * type within the class hierarchy. Such undesirable behavior would cause + * the logic in + * {@code org.springframework.context.annotation.ProfileCondition} to fail. + */ + @Test + void collectMultiValueMapFromClassWithLocalAnnotationThatShadowsAnnotationFromSuperclass() { + MultiValueMap map = MergedAnnotations.from(DerivedTxConfig.class, + SearchStrategy.INHERITED_ANNOTATIONS).stream(Transactional.class).collect( + MergedAnnotationCollectors.toMultiValueMap()); + assertThat(map).contains(entry("value", Arrays.asList("DerivedTxConfig"))); + } + + /** + * Note: this functionality is required by + * {@code org.springframework.context.annotation.ProfileCondition}. + */ + @Test + void collectMultiValueMapFromClassWithMultipleComposedAnnotations() { + MultiValueMap map = MergedAnnotations.from( + TxFromMultipleComposedAnnotations.class, + SearchStrategy.INHERITED_ANNOTATIONS).stream(Transactional.class).collect( + MergedAnnotationCollectors.toMultiValueMap()); + assertThat(map).contains( + entry("value", Arrays.asList("TxInheritedComposed", "TxComposed"))); + } + + @Test + void getWithInheritedAnnotationsFromClassWithLocalAnnotation() { + MergedAnnotation annotation = MergedAnnotations.from(TxConfig.class, + SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + assertThat(annotation.getString("value")).isEqualTo("TxConfig"); + } + + @Test + void getWithInheritedAnnotationsFromClassWithLocalAnnotationThatShadowsAnnotationFromSuperclass() { + MergedAnnotation annotation = MergedAnnotations.from(DerivedTxConfig.class, + SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + assertThat(annotation.getString("value")).isEqualTo("DerivedTxConfig"); + } + + @Test + void getWithInheritedAnnotationsFromMetaCycleAnnotatedClassWithMissingTargetMetaAnnotation() { + MergedAnnotation annotation = MergedAnnotations.from( + MetaCycleAnnotatedClass.class, SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + assertThat(annotation.isPresent()).isFalse(); + } + + @Test + void getWithInheritedAnnotationsFavorsLocalComposedAnnotationOverInheritedAnnotation() { + MergedAnnotation annotation = MergedAnnotations.from( + SubClassWithInheritedAnnotation.class, + SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + assertThat(annotation.getBoolean("readOnly")).isTrue(); + } + + @Test + void getWithInheritedAnnotationsFavorsInheritedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() { + MergedAnnotation annotation = MergedAnnotations.from( + SubSubClassWithInheritedAnnotation.class, + SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + assertThat(annotation.getBoolean("readOnly")).isFalse(); + } + + @Test + void getWithInheritedAnnotationsFavorsInheritedComposedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() { + MergedAnnotation annotation = MergedAnnotations.from( + SubSubClassWithInheritedComposedAnnotation.class, + SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + assertThat(annotation.getBoolean("readOnly")).isFalse(); + } + + @Test + void getWithInheritedAnnotationsFromInterfaceImplementedBySuperclass() { + MergedAnnotation annotation = MergedAnnotations.from( + ConcreteClassWithInheritedAnnotation.class, + SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + assertThat(annotation.isPresent()).isFalse(); + } + + @Test + void getWithInheritedAnnotationsFromInheritedAnnotationInterface() { + MergedAnnotation annotation = MergedAnnotations.from( + InheritedAnnotationInterface.class, + SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + assertThat(annotation.isPresent()).isTrue(); + } + + @Test + void getWithInheritedAnnotationsFromNonInheritedAnnotationInterface() { + MergedAnnotation annotation = MergedAnnotations.from( + NonInheritedAnnotationInterface.class, + SearchStrategy.INHERITED_ANNOTATIONS).get(Order.class); + assertThat(annotation.isPresent()).isTrue(); + } + + @Test + void getWithInheritedAnnotationsAttributesWithConventionBasedComposedAnnotation() { + MergedAnnotation annotation = MergedAnnotations.from( + ConventionBasedComposedContextConfigurationClass.class, + SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); + assertThat(annotation.isPresent()).isTrue(); + assertThat(annotation.getStringArray("locations")).containsExactly( + "explicitDeclaration"); + assertThat(annotation.getStringArray("value")).containsExactly( + "explicitDeclaration"); + } + + @Test + void getWithInheritedAnnotationsFromHalfConventionBasedAndHalfAliasedComposedAnnotation1() { + // SPR-13554: convention mapping mixed with AliasFor annotations + // xmlConfigFiles can be used because it has an AliasFor annotation + MergedAnnotation annotation = MergedAnnotations.from( + HalfConventionBasedAndHalfAliasedComposedContextConfigurationClass1.class, + SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); + assertThat(annotation.getStringArray("locations")).containsExactly( + "explicitDeclaration"); + assertThat(annotation.getStringArray("value")).containsExactly( + "explicitDeclaration"); + } + + @Test + void withInheritedAnnotationsFromHalfConventionBasedAndHalfAliasedComposedAnnotation2() { + // SPR-13554: convention mapping mixed with AliasFor annotations + // locations doesn't apply because it has no AliasFor annotation + MergedAnnotation annotation = MergedAnnotations.from( + HalfConventionBasedAndHalfAliasedComposedContextConfigurationClass2.class, + SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); + assertThat(annotation.getStringArray("locations")).isEmpty(); + assertThat(annotation.getStringArray("value")).isEmpty(); + } + + @Test + void withInheritedAnnotationsFromAliasedComposedAnnotation() { + MergedAnnotation annotation = MergedAnnotations.from( + AliasedComposedContextConfigurationClass.class, + SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); + assertThat(annotation.getStringArray("value")).containsExactly("test.xml"); + assertThat(annotation.getStringArray("locations")).containsExactly("test.xml"); + } + + @Test + void withInheritedAnnotationsFromAliasedValueComposedAnnotation() { + MergedAnnotation annotation = MergedAnnotations.from( + AliasedValueComposedContextConfigurationClass.class, + SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); + assertThat(annotation.getStringArray("value")).containsExactly("test.xml"); + assertThat(annotation.getStringArray("locations")).containsExactly("test.xml"); + } + + @Test + void getWithInheritedAnnotationsFromImplicitAliasesInMetaAnnotationOnComposedAnnotation() { + MergedAnnotation annotation = MergedAnnotations.from( + ComposedImplicitAliasesContextConfigurationClass.class, + SearchStrategy.INHERITED_ANNOTATIONS).get( + ImplicitAliasesContextConfiguration.class); + assertThat(annotation.getStringArray("groovyScripts")).containsExactly("A.xml", + "B.xml"); + assertThat(annotation.getStringArray("xmlFiles")).containsExactly("A.xml", + "B.xml"); + assertThat(annotation.getStringArray("locations")).containsExactly("A.xml", + "B.xml"); + assertThat(annotation.getStringArray("value")).containsExactly("A.xml", "B.xml"); + } + + @Test + void getWithInheritedAnnotationsFromAliasedValueComposedAnnotation() { + testGetWithInherited(AliasedValueComposedContextConfigurationClass.class, "test.xml"); + } + + @Test + void getWithInheritedAnnotationsFromImplicitAliasesForSameAttributeInComposedAnnotation() { + testGetWithInherited(ImplicitAliasesContextConfigurationClass1.class, "foo.xml"); + testGetWithInherited(ImplicitAliasesContextConfigurationClass2.class, "bar.xml"); + testGetWithInherited(ImplicitAliasesContextConfigurationClass3.class, "baz.xml"); + } + + @Test + void getWithInheritedAnnotationsFromTransitiveImplicitAliases() { + testGetWithInherited(TransitiveImplicitAliasesContextConfigurationClass.class, "test.groovy"); + } + + @Test + void getWithInheritedAnnotationsFromTransitiveImplicitAliasesWithSingleElementOverridingAnArrayViaAliasFor() { + testGetWithInherited( + SingleLocationTransitiveImplicitAliasesContextConfigurationClass.class, "test.groovy"); + } + + @Test + void getWithInheritedAnnotationsFromTransitiveImplicitAliasesWithSkippedLevel() { + testGetWithInherited( + TransitiveImplicitAliasesWithSkippedLevelContextConfigurationClass.class, "test.xml"); + } + + @Test + void getWithInheritedAnnotationsFromTransitiveImplicitAliasesWithSkippedLevelWithSingleElementOverridingAnArrayViaAliasFor() { + testGetWithInherited( + SingleLocationTransitiveImplicitAliasesWithSkippedLevelContextConfigurationClass.class, "test.xml"); + } + + private void testGetWithInherited(Class element, String... expected) { + MergedAnnotation annotation = MergedAnnotations.from(element, + SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); + assertThat(annotation.getStringArray("locations")).isEqualTo(expected); + assertThat(annotation.getStringArray("value")).isEqualTo(expected); + assertThat(annotation.getClassArray("classes")).isEmpty(); + } + + @Test + void getWithInheritedAnnotationsFromInvalidConventionBasedComposedAnnotation() { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + MergedAnnotations.from(InvalidConventionBasedComposedContextConfigurationClass.class, + SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class)); + } + + @Test + void getWithInheritedAnnotationsFromShadowedAliasComposedAnnotation() { + MergedAnnotation annotation = MergedAnnotations.from( + ShadowedAliasComposedContextConfigurationClass.class, + SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); + assertThat(annotation.getStringArray("locations")).containsExactly("test.xml"); + assertThat(annotation.getStringArray("value")).containsExactly("test.xml"); + } + + @Test + void getWithTypeHierarchyFromInheritedAnnotationInterface() { + MergedAnnotation annotation = MergedAnnotations.from( + InheritedAnnotationInterface.class, SearchStrategy.TYPE_HIERARCHY).get(Transactional.class); + assertThat(annotation.isPresent()).isTrue(); + assertThat(annotation.getAggregateIndex()).isEqualTo(0); + } + + @Test + void getWithTypeHierarchyFromSubInheritedAnnotationInterface() { + MergedAnnotation annotation = MergedAnnotations.from( + SubInheritedAnnotationInterface.class, SearchStrategy.TYPE_HIERARCHY).get(Transactional.class); + assertThat(annotation.isPresent()).isTrue(); + assertThat(annotation.getAggregateIndex()).isEqualTo(1); + } + + @Test + void getWithTypeHierarchyFromSubSubInheritedAnnotationInterface() { + MergedAnnotation annotation = MergedAnnotations.from( + SubSubInheritedAnnotationInterface.class, SearchStrategy.TYPE_HIERARCHY).get(Transactional.class); + assertThat(annotation.isPresent()).isTrue(); + assertThat(annotation.getAggregateIndex()).isEqualTo(2); + } + + @Test + void getWithTypeHierarchyFromNonInheritedAnnotationInterface() { + MergedAnnotation annotation = MergedAnnotations.from( + NonInheritedAnnotationInterface.class, SearchStrategy.TYPE_HIERARCHY).get(Order.class); + assertThat(annotation.isPresent()).isTrue(); + assertThat(annotation.getAggregateIndex()).isEqualTo(0); + } + + @Test + void getWithTypeHierarchyFromSubNonInheritedAnnotationInterface() { + MergedAnnotation annotation = MergedAnnotations.from( + SubNonInheritedAnnotationInterface.class, SearchStrategy.TYPE_HIERARCHY).get(Order.class); + assertThat(annotation.isPresent()).isTrue(); + assertThat(annotation.getAggregateIndex()).isEqualTo(1); + } + + @Test + void getWithTypeHierarchyFromSubSubNonInheritedAnnotationInterface() { + MergedAnnotation annotation = MergedAnnotations.from( + SubSubNonInheritedAnnotationInterface.class, + SearchStrategy.TYPE_HIERARCHY).get(Order.class); + assertThat(annotation.isPresent()).isTrue(); + assertThat(annotation.getAggregateIndex()).isEqualTo(2); + } + + @Test + void getWithTypeHierarchyInheritedFromInterfaceMethod() + throws NoSuchMethodException { + Method method = ConcreteClassWithInheritedAnnotation.class.getMethod( + "handleFromInterface"); + MergedAnnotation annotation = MergedAnnotations.from(method, + SearchStrategy.TYPE_HIERARCHY).get(Order.class); + assertThat(annotation.isPresent()).isTrue(); + assertThat(annotation.getAggregateIndex()).isEqualTo(1); + } + + @Test + void getWithTypeHierarchyInheritedFromAbstractMethod() throws NoSuchMethodException { + Method method = ConcreteClassWithInheritedAnnotation.class.getMethod("handle"); + MergedAnnotation annotation = MergedAnnotations.from(method, + SearchStrategy.TYPE_HIERARCHY).get(Transactional.class); + assertThat(annotation.isPresent()).isTrue(); + assertThat(annotation.getAggregateIndex()).isEqualTo(1); + } + + @Test + void getWithTypeHierarchyInheritedFromBridgedMethod() throws NoSuchMethodException { + Method method = ConcreteClassWithInheritedAnnotation.class.getMethod( + "handleParameterized", String.class); + MergedAnnotation annotation = MergedAnnotations.from(method, + SearchStrategy.TYPE_HIERARCHY).get(Transactional.class); + assertThat(annotation.isPresent()).isTrue(); + assertThat(annotation.getAggregateIndex()).isEqualTo(1); + } + + @Test + void getWithTypeHierarchyFromBridgeMethod() { + List methods = new ArrayList<>(); + ReflectionUtils.doWithLocalMethods(StringGenericParameter.class, method -> { + if ("getFor".equals(method.getName())) { + methods.add(method); + } + }); + Method bridgeMethod = methods.get(0).getReturnType().equals(Object.class) ? + methods.get(0) : methods.get(1); + Method bridgedMethod = methods.get(0).getReturnType().equals(Object.class) ? + methods.get(1) : methods.get(0); + assertThat(bridgeMethod.isBridge()).isTrue(); + assertThat(bridgedMethod.isBridge()).isFalse(); + MergedAnnotation annotation = MergedAnnotations.from(bridgeMethod, + SearchStrategy.TYPE_HIERARCHY).get(Order.class); + assertThat(annotation.isPresent()).isTrue(); + assertThat(annotation.getAggregateIndex()).isEqualTo(0); + } + + @Test + void getWithTypeHierarchyFromClassWithMetaAndLocalTxConfig() { + MergedAnnotation annotation = MergedAnnotations.from( + MetaAndLocalTxConfigClass.class, SearchStrategy.TYPE_HIERARCHY).get( + Transactional.class); + assertThat(annotation.getString("qualifier")).isEqualTo("localTxMgr"); + } + + @Test + void getWithTypeHierarchyFromClassWithAttributeAliasesInTargetAnnotation() { + MergedAnnotation mergedAnnotation = MergedAnnotations.from( + AliasedTransactionalComponentClass.class, SearchStrategy.TYPE_HIERARCHY).get( + AliasedTransactional.class); + AliasedTransactional synthesizedAnnotation = mergedAnnotation.synthesize(); + String qualifier = "aliasForQualifier"; + assertThat(mergedAnnotation.getString("value")).isEqualTo(qualifier); + assertThat(mergedAnnotation.getString("qualifier")).isEqualTo(qualifier); + assertThat(synthesizedAnnotation.value()).isEqualTo(qualifier); + assertThat(synthesizedAnnotation.qualifier()).isEqualTo(qualifier); + } + + @Test // gh-23767 + void getWithTypeHierarchyFromClassWithComposedMetaTransactionalAnnotation() { + MergedAnnotation mergedAnnotation = MergedAnnotations.from( + ComposedTransactionalClass.class, SearchStrategy.TYPE_HIERARCHY).get( + AliasedTransactional.class); + assertThat(mergedAnnotation.getString("value")).isEqualTo("anotherTransactionManager"); + assertThat(mergedAnnotation.getString("qualifier")).isEqualTo("anotherTransactionManager"); + } + + @Test // gh-23767 + void getWithTypeHierarchyFromClassWithMetaMetaAliasedTransactional() { + MergedAnnotation mergedAnnotation = MergedAnnotations.from( + MetaMetaAliasedTransactionalClass.class, SearchStrategy.TYPE_HIERARCHY).get( + AliasedTransactional.class); + assertThat(mergedAnnotation.getString("value")).isEqualTo("meta"); + assertThat(mergedAnnotation.getString("qualifier")).isEqualTo("meta"); + } + + @Test + void getWithTypeHierarchyFromClassWithAttributeAliasInComposedAnnotationAndNestedAnnotationsInTargetAnnotation() { + MergedAnnotation annotation = testGetWithTypeHierarchy( + TestComponentScanClass.class, "com.example.app.test"); + MergedAnnotation[] excludeFilters = annotation.getAnnotationArray( + "excludeFilters", Filter.class); + assertThat(Arrays.stream(excludeFilters).map( + filter -> filter.getString("pattern"))).containsExactly("*Test", "*Tests"); + } + + @Test + void getWithTypeHierarchyFromClassWithBothAttributesOfAnAliasPairDeclared() { + testGetWithTypeHierarchy(ComponentScanWithBasePackagesAndValueAliasClass.class, "com.example.app.test"); + } + + @Test + void getWithTypeHierarchyWithSingleElementOverridingAnArrayViaConvention() { + testGetWithTypeHierarchy(ConventionBasedSinglePackageComponentScanClass.class, "com.example.app.test"); + } + + @Test + void getWithTypeHierarchyWithSingleElementOverridingAnArrayViaAliasFor() { + testGetWithTypeHierarchy(AliasForBasedSinglePackageComponentScanClass.class, "com.example.app.test"); + } + + private MergedAnnotation testGetWithTypeHierarchy(Class element, String... expected) { + MergedAnnotation annotation = MergedAnnotations.from(element, + SearchStrategy.TYPE_HIERARCHY).get(ComponentScan.class); + assertThat(annotation.getStringArray("value")).containsExactly(expected); + assertThat(annotation.getStringArray("basePackages")).containsExactly(expected); + return annotation; + } + + @Test + void getWithTypeHierarchyWhenMultipleMetaAnnotationsHaveClashingAttributeNames() { + MergedAnnotations annotations = MergedAnnotations.from( + AliasedComposedContextConfigurationAndTestPropertySourceClass.class, + SearchStrategy.TYPE_HIERARCHY); + MergedAnnotation contextConfig = annotations.get(ContextConfiguration.class); + assertThat(contextConfig.getStringArray("locations")).containsExactly("test.xml"); + assertThat(contextConfig.getStringArray("value")).containsExactly("test.xml"); + MergedAnnotation testPropSource = annotations.get(TestPropertySource.class); + assertThat(testPropSource.getStringArray("locations")).containsExactly( + "test.properties"); + assertThat(testPropSource.getStringArray("value")).containsExactly( + "test.properties"); + } + + @Test + void getWithTypeHierarchyWithLocalAliasesThatConflictWithAttributesInMetaAnnotationByConvention() { + MergedAnnotation annotation = MergedAnnotations.from( + SpringApplicationConfigurationClass.class, SearchStrategy.TYPE_HIERARCHY).get( + ContextConfiguration.class); + assertThat(annotation.getStringArray("locations")).isEmpty(); + assertThat(annotation.getStringArray("value")).isEmpty(); + assertThat(annotation.getClassArray("classes")).containsExactly(Number.class); + } + + @Test + void getWithTypeHierarchyOnMethodWithSingleElementOverridingAnArrayViaConvention() throws Exception { + testGetWithTypeHierarchyWebMapping( + WebController.class.getMethod("postMappedWithPathAttribute")); + } + + @Test + void getWithTypeHierarchyOnMethodWithSingleElementOverridingAnArrayViaAliasFor() throws Exception { + testGetWithTypeHierarchyWebMapping( + WebController.class.getMethod("getMappedWithValueAttribute")); + testGetWithTypeHierarchyWebMapping( + WebController.class.getMethod("getMappedWithPathAttribute")); + } + + private void testGetWithTypeHierarchyWebMapping(AnnotatedElement element) { + MergedAnnotation annotation = MergedAnnotations.from(element, + SearchStrategy.TYPE_HIERARCHY).get(RequestMapping.class); + assertThat(annotation.getStringArray("value")).containsExactly("/test"); + assertThat(annotation.getStringArray("path")).containsExactly("/test"); + } + + @Test + void getDirectWithJavaxAnnotationType() throws Exception { + assertThat(MergedAnnotations.from(ResourceHolder.class).get( + Resource.class).getString("name")).isEqualTo("x"); + } + + @Test + void streamInheritedFromClassWithInterface() throws Exception { + Method method = TransactionalServiceImpl.class.getMethod("doIt"); + assertThat(MergedAnnotations.from(method, SearchStrategy.INHERITED_ANNOTATIONS).stream( + Transactional.class)).isEmpty(); + } + + @Test + void streamTypeHierarchyFromClassWithInterface() throws Exception { + Method method = TransactionalServiceImpl.class.getMethod("doIt"); + assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).stream( + Transactional.class)).hasSize(1); + } + + @Test + void streamTypeHierarchyAndEnclosingClassesFromNonAnnotatedInnerClassWithAnnotatedEnclosingClass() { + Stream> classes = MergedAnnotations.from(AnnotatedClass.NonAnnotatedInnerClass.class, + SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES).stream().map(MergedAnnotation::getType); + assertThat(classes).containsExactly(Component.class, Indexed.class); + } + + @Test + void streamTypeHierarchyAndEnclosingClassesFromNonAnnotatedStaticNestedClassWithAnnotatedEnclosingClass() { + Stream> classes = MergedAnnotations.from(AnnotatedClass.NonAnnotatedStaticNestedClass.class, + SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES).stream().map(MergedAnnotation::getType); + assertThat(classes).containsExactly(Component.class, Indexed.class); + } + + @Test + void getFromMethodWithMethodAnnotationOnLeaf() throws Exception { + Method method = Leaf.class.getMethod("annotatedOnLeaf"); + assertThat(method.getAnnotation(Order.class)).isNotNull(); + assertThat(MergedAnnotations.from(method).get(Order.class).getDistance()).isEqualTo(0); + assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get( + Order.class).getDistance()).isEqualTo(0); + } + + @Test + void getFromMethodWithAnnotationOnMethodInInterface() throws Exception { + Method method = Leaf.class.getMethod("fromInterfaceImplementedByRoot"); + assertThat(method.getAnnotation(Order.class)).isNull(); + assertThat(MergedAnnotations.from(method).get(Order.class).getDistance()).isEqualTo(-1); + assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get( + Order.class).getDistance()).isEqualTo(0); + } + + @Test + void getFromMethodWithMetaAnnotationOnLeaf() throws Exception { + Method method = Leaf.class.getMethod("metaAnnotatedOnLeaf"); + assertThat(method.getAnnotation(Order.class)).isNull(); + assertThat(MergedAnnotations.from(method).get(Order.class).getDistance()).isEqualTo(1); + assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get( + Order.class).getDistance()).isEqualTo(1); + } + + @Test + void getFromMethodWithMetaMetaAnnotationOnLeaf() throws Exception { + Method method = Leaf.class.getMethod("metaMetaAnnotatedOnLeaf"); + assertThat(method.getAnnotation(Component.class)).isNull(); + assertThat(MergedAnnotations.from(method).get(Component.class).getDistance()).isEqualTo(2); + assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get( + Component.class).getDistance()).isEqualTo(2); + } + + @Test + void getWithAnnotationOnRoot() throws Exception { + Method method = Leaf.class.getMethod("annotatedOnRoot"); + assertThat(method.getAnnotation(Order.class)).isNotNull(); + assertThat(MergedAnnotations.from(method).get(Order.class).getDistance()).isEqualTo(0); + assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get( + Order.class).getDistance()).isEqualTo(0); + } + + @Test + void getFromMethodWithMetaAnnotationOnRoot() throws Exception { + Method method = Leaf.class.getMethod("metaAnnotatedOnRoot"); + assertThat(method.getAnnotation(Order.class)).isNull(); + assertThat(MergedAnnotations.from(method).get(Order.class).getDistance()).isEqualTo(1); + assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get( + Order.class).getDistance()).isEqualTo(1); + } + + @Test + void getFromMethodWithOnRootButOverridden() throws Exception { + Method method = Leaf.class.getMethod("overrideWithoutNewAnnotation"); + assertThat(method.getAnnotation(Order.class)).isNull(); + assertThat(MergedAnnotations.from(method).get(Order.class).getDistance()).isEqualTo(-1); + assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get( + Order.class).getDistance()).isEqualTo(0); + } + + @Test + void getFromMethodWithNotAnnotated() throws Exception { + Method method = Leaf.class.getMethod("notAnnotated"); + assertThat(method.getAnnotation(Order.class)).isNull(); + assertThat(MergedAnnotations.from(method).get(Order.class).getDistance()).isEqualTo(-1); + assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get( + Order.class).getDistance()).isEqualTo(-1); + } + + @Test + void getFromMethodWithBridgeMethod() throws Exception { + Method method = TransactionalStringGeneric.class.getMethod("something", Object.class); + assertThat(method.isBridge()).isTrue(); + assertThat(method.getAnnotation(Order.class)).isNull(); + assertThat(MergedAnnotations.from(method).get(Order.class).getDistance()).isEqualTo(-1); + assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get( + Order.class).getDistance()).isEqualTo(0); + boolean runningInEclipse = Arrays.stream( + new Exception().getStackTrace()).anyMatch( + element -> element.getClassName().startsWith("org.eclipse.jdt")); + // As of JDK 8, invoking getAnnotation() on a bridge method actually finds an + // annotation on its 'bridged' method [1]; however, the Eclipse compiler + // will not support this until Eclipse 4.9 [2]. Thus, we effectively ignore the + // following assertion if the test is currently executing within the Eclipse IDE. + // [1] https://bugs.openjdk.java.net/browse/JDK-6695379 + // [2] https://bugs.eclipse.org/bugs/show_bug.cgi?id=495396 + if (!runningInEclipse) { + assertThat(method.getAnnotation(Transactional.class)).isNotNull(); + } + assertThat(MergedAnnotations.from(method).get( + Transactional.class).getDistance()).isEqualTo(0); + assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get( + Transactional.class).getDistance()).isEqualTo(0); + } + + @Test + void getFromMethodWithBridgedMethod() throws Exception { + Method method = TransactionalStringGeneric.class.getMethod("something", String.class); + assertThat(method.isBridge()).isFalse(); + assertThat(method.getAnnotation(Order.class)).isNull(); + assertThat(MergedAnnotations.from(method).get(Order.class).getDistance()).isEqualTo(-1); + assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get( + Order.class).getDistance()).isEqualTo(0); + assertThat(method.getAnnotation(Transactional.class)).isNotNull(); + assertThat(MergedAnnotations.from(method).get( + Transactional.class).getDistance()).isEqualTo(0); + assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get( + Transactional.class).getDistance()).isEqualTo(0); + } + + @Test + void getFromMethodWithInterface() throws Exception { + Method method = ImplementsInterfaceWithAnnotatedMethod.class.getMethod("foo"); + assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get( + Order.class).getDistance()).isEqualTo(0); + } + + @Test // SPR-16060 + void getFromMethodWithGenericInterface() throws Exception { + Method method = ImplementsInterfaceWithGenericAnnotatedMethod.class.getMethod("foo", String.class); + assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get( + Order.class).getDistance()).isEqualTo(0); + } + + @Test // SPR-17146 + void getFromMethodWithGenericSuperclass() throws Exception { + Method method = ExtendsBaseClassWithGenericAnnotatedMethod.class.getMethod("foo", String.class); + assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get( + Order.class).getDistance()).isEqualTo(0); + } + + @Test + void getFromMethodWithInterfaceOnSuper() throws Exception { + Method method = SubOfImplementsInterfaceWithAnnotatedMethod.class.getMethod("foo"); + assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get( + Order.class).getDistance()).isEqualTo(0); + } + + @Test + void getFromMethodWhenInterfaceWhenSuperDoesNotImplementMethod() throws Exception { + Method method = SubOfAbstractImplementsInterfaceWithAnnotatedMethod.class.getMethod("foo"); + assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get( + Order.class).getDistance()).isEqualTo(0); + } + + @Test + void getDirectFromClassFavorsMoreLocallyDeclaredComposedAnnotationsOverAnnotationsOnInterfaces() { + MergedAnnotation annotation = MergedAnnotations.from( + ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class, + SearchStrategy.TYPE_HIERARCHY).get(Component.class); + assertThat(annotation.getString("value")).isEqualTo("meta2"); + } + + @Test + void getDirectFromClassFavorsMoreLocallyDeclaredComposedAnnotationsOverInheritedAnnotations() { + MergedAnnotation annotation = MergedAnnotations.from( + SubSubClassWithInheritedAnnotation.class, SearchStrategy.TYPE_HIERARCHY).get(Transactional.class); + assertThat(annotation.getBoolean("readOnly")).isTrue(); + } + + @Test + void getDirectFromClassFavorsMoreLocallyDeclaredComposedAnnotationsOverInheritedComposedAnnotations() { + MergedAnnotation annotation = MergedAnnotations.from( + SubSubClassWithInheritedMetaAnnotation.class, + SearchStrategy.TYPE_HIERARCHY).get(Component.class); + assertThat(annotation.getString("value")).isEqualTo("meta2"); + } + + @Test + void getDirectFromClassgetDirectFromClassMetaMetaAnnotatedClass() { + MergedAnnotation annotation = MergedAnnotations.from( + MetaMetaAnnotatedClass.class, SearchStrategy.TYPE_HIERARCHY).get(Component.class); + assertThat(annotation.getString("value")).isEqualTo("meta2"); + } + + @Test + void getDirectFromClassWithMetaMetaMetaAnnotatedClass() { + MergedAnnotation annotation = MergedAnnotations.from( + MetaMetaMetaAnnotatedClass.class, SearchStrategy.TYPE_HIERARCHY).get(Component.class); + assertThat(annotation.getString("value")).isEqualTo("meta2"); + } + + @Test + void getDirectFromClassWithAnnotatedClassWithMissingTargetMetaAnnotation() { + // TransactionalClass is NOT annotated or meta-annotated with @Component + MergedAnnotation annotation = MergedAnnotations.from(TransactionalClass.class, + SearchStrategy.TYPE_HIERARCHY).get(Component.class); + assertThat(annotation.isPresent()).isFalse(); + } + + @Test + void getDirectFromClassWithMetaCycleAnnotatedClassWithMissingTargetMetaAnnotation() { + MergedAnnotation annotation = MergedAnnotations.from( + MetaCycleAnnotatedClass.class, SearchStrategy.TYPE_HIERARCHY).get(Component.class); + assertThat(annotation.isPresent()).isFalse(); + } + + @Test + void getDirectFromClassWithInheritedAnnotationInterface() { + MergedAnnotation annotation = MergedAnnotations.from( + InheritedAnnotationInterface.class, SearchStrategy.TYPE_HIERARCHY).get(Transactional.class); + assertThat(annotation.getAggregateIndex()).isEqualTo(0); + } + + @Test + void getDirectFromClassWithSubInheritedAnnotationInterface() { + MergedAnnotation annotation = MergedAnnotations.from( + SubInheritedAnnotationInterface.class, SearchStrategy.TYPE_HIERARCHY).get(Transactional.class); + assertThat(annotation.getAggregateIndex()).isEqualTo(1); + } + + @Test + void getDirectFromClassWithSubSubInheritedAnnotationInterface() { + MergedAnnotation annotation = MergedAnnotations.from( + SubSubInheritedAnnotationInterface.class, SearchStrategy.TYPE_HIERARCHY).get(Transactional.class); + assertThat(annotation.getAggregateIndex()).isEqualTo(2); + } + + @Test + void getDirectFromClassWithNonInheritedAnnotationInterface() { + MergedAnnotation annotation = MergedAnnotations.from( + NonInheritedAnnotationInterface.class, SearchStrategy.TYPE_HIERARCHY).get(Order.class); + assertThat(annotation.getAggregateIndex()).isEqualTo(0); + } + + @Test + void getDirectFromClassWithSubNonInheritedAnnotationInterface() { + MergedAnnotation annotation = MergedAnnotations.from( + SubNonInheritedAnnotationInterface.class, SearchStrategy.TYPE_HIERARCHY).get(Order.class); + assertThat(annotation.getAggregateIndex()).isEqualTo(1); + } + + @Test + void getDirectFromClassWithSubSubNonInheritedAnnotationInterface() { + MergedAnnotation annotation = MergedAnnotations.from( + SubSubNonInheritedAnnotationInterface.class, + SearchStrategy.TYPE_HIERARCHY).get(Order.class); + assertThat(annotation.getAggregateIndex()).isEqualTo(2); + } + + @Test + void getSuperClassForAllScenarios() { + // no class-level annotation + assertThat(MergedAnnotations.from(NonAnnotatedInterface.class, + SearchStrategy.SUPERCLASS).get( + Transactional.class).getSource()).isNull(); + assertThat(MergedAnnotations.from(NonAnnotatedClass.class, + SearchStrategy.SUPERCLASS).get( + Transactional.class).getSource()).isNull(); + // inherited class-level annotation; note: @Transactional is inherited + assertThat(MergedAnnotations.from(InheritedAnnotationInterface.class, + SearchStrategy.SUPERCLASS).get( + Transactional.class).getSource()).isEqualTo( + InheritedAnnotationInterface.class); + assertThat(MergedAnnotations.from(SubInheritedAnnotationInterface.class, + SearchStrategy.SUPERCLASS).get( + Transactional.class).getSource()).isNull(); + assertThat(MergedAnnotations.from(InheritedAnnotationClass.class, + SearchStrategy.SUPERCLASS).get( + Transactional.class).getSource()).isEqualTo( + InheritedAnnotationClass.class); + assertThat(MergedAnnotations.from(SubInheritedAnnotationClass.class, + SearchStrategy.SUPERCLASS).get( + Transactional.class).getSource()).isEqualTo( + InheritedAnnotationClass.class); + // non-inherited class-level annotation; note: @Order is not inherited, + // but we should still find it on classes. + assertThat(MergedAnnotations.from(NonInheritedAnnotationInterface.class, + SearchStrategy.SUPERCLASS).get(Order.class).getSource()).isEqualTo( + NonInheritedAnnotationInterface.class); + assertThat(MergedAnnotations.from(SubNonInheritedAnnotationInterface.class, + SearchStrategy.SUPERCLASS).get(Order.class).getSource()).isNull(); + assertThat(MergedAnnotations.from(NonInheritedAnnotationClass.class, + SearchStrategy.SUPERCLASS).get(Order.class).getSource()).isEqualTo( + NonInheritedAnnotationClass.class); + assertThat(MergedAnnotations.from(SubNonInheritedAnnotationClass.class, + SearchStrategy.SUPERCLASS).get(Order.class).getSource()).isEqualTo( + NonInheritedAnnotationClass.class); + } + + @Test + void getSuperClassSourceForTypesWithSingleCandidateType() { + // no class-level annotation + List> transactionalCandidateList = Collections.singletonList(Transactional.class); + assertThat(getSuperClassSourceWithTypeIn(NonAnnotatedInterface.class, + transactionalCandidateList)).isNull(); + assertThat(getSuperClassSourceWithTypeIn(NonAnnotatedClass.class, + transactionalCandidateList)).isNull(); + // inherited class-level annotation; note: @Transactional is inherited + assertThat(getSuperClassSourceWithTypeIn(InheritedAnnotationInterface.class, + transactionalCandidateList)).isEqualTo( + InheritedAnnotationInterface.class); + assertThat(getSuperClassSourceWithTypeIn(SubInheritedAnnotationInterface.class, + transactionalCandidateList)).isNull(); + assertThat(getSuperClassSourceWithTypeIn(InheritedAnnotationClass.class, + transactionalCandidateList)).isEqualTo(InheritedAnnotationClass.class); + assertThat(getSuperClassSourceWithTypeIn(SubInheritedAnnotationClass.class, + transactionalCandidateList)).isEqualTo(InheritedAnnotationClass.class); + // non-inherited class-level annotation; note: @Order is not inherited, + // but should still find it on classes. + List> orderCandidateList = Collections.singletonList( + Order.class); + assertThat(getSuperClassSourceWithTypeIn(NonInheritedAnnotationInterface.class, + orderCandidateList)).isEqualTo(NonInheritedAnnotationInterface.class); + assertThat(getSuperClassSourceWithTypeIn(SubNonInheritedAnnotationInterface.class, + orderCandidateList)).isNull(); + assertThat(getSuperClassSourceWithTypeIn(NonInheritedAnnotationClass.class, + orderCandidateList)).isEqualTo(NonInheritedAnnotationClass.class); + assertThat(getSuperClassSourceWithTypeIn(SubNonInheritedAnnotationClass.class, + orderCandidateList)).isEqualTo(NonInheritedAnnotationClass.class); + } + + @Test + void getSuperClassSourceForTypesWithMultipleCandidateTypes() { + List> candidates = Arrays.asList(Transactional.class, Order.class); + // no class-level annotation + assertThat(getSuperClassSourceWithTypeIn(NonAnnotatedInterface.class, + candidates)).isNull(); + assertThat(getSuperClassSourceWithTypeIn(NonAnnotatedClass.class, + candidates)).isNull(); + // inherited class-level annotation; note: @Transactional is inherited + assertThat(getSuperClassSourceWithTypeIn(InheritedAnnotationInterface.class, + candidates)).isEqualTo(InheritedAnnotationInterface.class); + assertThat(getSuperClassSourceWithTypeIn(SubInheritedAnnotationInterface.class, + candidates)).isNull(); + assertThat(getSuperClassSourceWithTypeIn(InheritedAnnotationClass.class, + candidates)).isEqualTo(InheritedAnnotationClass.class); + assertThat(getSuperClassSourceWithTypeIn(SubInheritedAnnotationClass.class, + candidates)).isEqualTo(InheritedAnnotationClass.class); + // non-inherited class-level annotation; note: @Order is not inherited, + // but findAnnotationDeclaringClassForTypes() should still find it on + // classes. + assertThat(getSuperClassSourceWithTypeIn(NonInheritedAnnotationInterface.class, + candidates)).isEqualTo(NonInheritedAnnotationInterface.class); + assertThat(getSuperClassSourceWithTypeIn(SubNonInheritedAnnotationInterface.class, + candidates)).isNull(); + assertThat(getSuperClassSourceWithTypeIn(NonInheritedAnnotationClass.class, + candidates)).isEqualTo(NonInheritedAnnotationClass.class); + assertThat(getSuperClassSourceWithTypeIn(SubNonInheritedAnnotationClass.class, + candidates)).isEqualTo(NonInheritedAnnotationClass.class); + // class hierarchy mixed with @Transactional and @Order declarations + assertThat(getSuperClassSourceWithTypeIn(TransactionalClass.class, + candidates)).isEqualTo(TransactionalClass.class); + assertThat(getSuperClassSourceWithTypeIn(TransactionalAndOrderedClass.class, + candidates)).isEqualTo(TransactionalAndOrderedClass.class); + assertThat(getSuperClassSourceWithTypeIn(SubTransactionalAndOrderedClass.class, + candidates)).isEqualTo(TransactionalAndOrderedClass.class); + } + + private Object getSuperClassSourceWithTypeIn(Class clazz, List> annotationTypes) { + return MergedAnnotations.from(clazz, SearchStrategy.SUPERCLASS).stream().filter( + MergedAnnotationPredicates.typeIn(annotationTypes).and( + MergedAnnotation::isDirectlyPresent)).map( + MergedAnnotation::getSource).findFirst().orElse(null); + } + + @Test + void isDirectlyPresentForAllScenarios() throws Exception { + // no class-level annotation + assertThat(MergedAnnotations.from(NonAnnotatedInterface.class).get( + Transactional.class).isDirectlyPresent()).isFalse(); + assertThat(MergedAnnotations.from(NonAnnotatedInterface.class).isDirectlyPresent( + Transactional.class)).isFalse(); + assertThat(MergedAnnotations.from(NonAnnotatedClass.class).get( + Transactional.class).isDirectlyPresent()).isFalse(); + assertThat(MergedAnnotations.from(NonAnnotatedClass.class).isDirectlyPresent( + Transactional.class)).isFalse(); + // inherited class-level annotation; note: @Transactional is inherited + assertThat(MergedAnnotations.from(InheritedAnnotationInterface.class).get( + Transactional.class).isDirectlyPresent()).isTrue(); + assertThat(MergedAnnotations.from( + InheritedAnnotationInterface.class).isDirectlyPresent( + Transactional.class)).isTrue(); + assertThat(MergedAnnotations.from(SubInheritedAnnotationInterface.class).get( + Transactional.class).isDirectlyPresent()).isFalse(); + assertThat(MergedAnnotations.from( + SubInheritedAnnotationInterface.class).isDirectlyPresent( + Transactional.class)).isFalse(); + assertThat(MergedAnnotations.from(InheritedAnnotationClass.class).get( + Transactional.class).isDirectlyPresent()).isTrue(); + assertThat( + MergedAnnotations.from(InheritedAnnotationClass.class).isDirectlyPresent( + Transactional.class)).isTrue(); + assertThat(MergedAnnotations.from(SubInheritedAnnotationClass.class).get( + Transactional.class).isDirectlyPresent()).isFalse(); + assertThat(MergedAnnotations.from( + SubInheritedAnnotationClass.class).isDirectlyPresent( + Transactional.class)).isFalse(); + // non-inherited class-level annotation; note: @Order is not inherited + assertThat(MergedAnnotations.from(NonInheritedAnnotationInterface.class).get( + Order.class).isDirectlyPresent()).isTrue(); + assertThat(MergedAnnotations.from( + NonInheritedAnnotationInterface.class).isDirectlyPresent( + Order.class)).isTrue(); + assertThat(MergedAnnotations.from(SubNonInheritedAnnotationInterface.class).get( + Order.class).isDirectlyPresent()).isFalse(); + assertThat(MergedAnnotations.from( + SubNonInheritedAnnotationInterface.class).isDirectlyPresent( + Order.class)).isFalse(); + assertThat(MergedAnnotations.from(NonInheritedAnnotationClass.class).get( + Order.class).isDirectlyPresent()).isTrue(); + assertThat(MergedAnnotations.from( + NonInheritedAnnotationClass.class).isDirectlyPresent( + Order.class)).isTrue(); + assertThat(MergedAnnotations.from(SubNonInheritedAnnotationClass.class).get( + Order.class).isDirectlyPresent()).isFalse(); + assertThat(MergedAnnotations.from( + SubNonInheritedAnnotationClass.class).isDirectlyPresent( + Order.class)).isFalse(); + } + + @Test + void getAggregateIndexForAllScenarios() { + // no class-level annotation + assertThat(MergedAnnotations.from(NonAnnotatedInterface.class, + SearchStrategy.INHERITED_ANNOTATIONS).get( + Transactional.class).getAggregateIndex()).isEqualTo(-1); + assertThat(MergedAnnotations.from(NonAnnotatedClass.class, + SearchStrategy.INHERITED_ANNOTATIONS).get( + Transactional.class).getAggregateIndex()).isEqualTo(-1); + // inherited class-level annotation; note: @Transactional is inherited + assertThat(MergedAnnotations.from(InheritedAnnotationInterface.class, + SearchStrategy.INHERITED_ANNOTATIONS).get( + Transactional.class).getAggregateIndex()).isEqualTo(0); + // Since we're not traversing interface hierarchies the following, + // though perhaps counter intuitive, must be false: + assertThat(MergedAnnotations.from(SubInheritedAnnotationInterface.class, + SearchStrategy.INHERITED_ANNOTATIONS).get( + Transactional.class).getAggregateIndex()).isEqualTo(-1); + assertThat(MergedAnnotations.from(InheritedAnnotationClass.class, + SearchStrategy.INHERITED_ANNOTATIONS).get( + Transactional.class).getAggregateIndex()).isEqualTo(0); + assertThat(MergedAnnotations.from(SubInheritedAnnotationClass.class, + SearchStrategy.INHERITED_ANNOTATIONS).get( + Transactional.class).getAggregateIndex()).isEqualTo(1); + // non-inherited class-level annotation; note: @Order is not inherited + assertThat(MergedAnnotations.from(NonInheritedAnnotationInterface.class, + SearchStrategy.INHERITED_ANNOTATIONS).get( + Order.class).getAggregateIndex()).isEqualTo(0); + assertThat(MergedAnnotations.from(SubNonInheritedAnnotationInterface.class, + SearchStrategy.INHERITED_ANNOTATIONS).get( + Order.class).getAggregateIndex()).isEqualTo(-1); + assertThat(MergedAnnotations.from(NonInheritedAnnotationClass.class, + SearchStrategy.INHERITED_ANNOTATIONS).get( + Order.class).getAggregateIndex()).isEqualTo(0); + assertThat(MergedAnnotations.from(SubNonInheritedAnnotationClass.class, + SearchStrategy.INHERITED_ANNOTATIONS).get( + Order.class).getAggregateIndex()).isEqualTo(-1); + } + + @Test + void getDirectWithoutAttributeAliases() { + MergedAnnotation annotation = MergedAnnotations.from(WebController.class).get(Component.class); + assertThat(annotation.getString("value")).isEqualTo("webController"); + } + + @Test + void getDirectWithNestedAnnotations() { + MergedAnnotation annotation = MergedAnnotations.from(ComponentScanClass.class).get(ComponentScan.class); + MergedAnnotation[] filters = annotation.getAnnotationArray("excludeFilters", Filter.class); + assertThat(Arrays.stream(filters).map( + filter -> filter.getString("pattern"))).containsExactly("*Foo", "*Bar"); + } + + @Test + void getDirectWithAttributeAliases1() throws Exception { + Method method = WebController.class.getMethod("handleMappedWithValueAttribute"); + MergedAnnotation annotation = MergedAnnotations.from(method).get(RequestMapping.class); + assertThat(annotation.getString("name")).isEqualTo("foo"); + assertThat(annotation.getStringArray("value")).containsExactly("/test"); + assertThat(annotation.getStringArray("path")).containsExactly("/test"); + } + + @Test + void getDirectWithAttributeAliases2() throws Exception { + Method method = WebController.class.getMethod("handleMappedWithPathAttribute"); + MergedAnnotation annotation = MergedAnnotations.from(method).get(RequestMapping.class); + assertThat(annotation.getString("name")).isEqualTo("bar"); + assertThat(annotation.getStringArray("value")).containsExactly("/test"); + assertThat(annotation.getStringArray("path")).containsExactly("/test"); + } + + @Test + void getDirectWithAttributeAliasesWithDifferentValues() throws Exception { + Method method = WebController.class.getMethod("handleMappedWithDifferentPathAndValueAttributes"); + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + MergedAnnotations.from(method).get(RequestMapping.class)) + .withMessageContaining("attribute 'path' and its alias 'value'") + .withMessageContaining("values of [{/test}] and [{/enigma}]"); + } + + @Test + void getValueFromAnnotation() throws Exception { + Method method = TransactionalStringGeneric.class.getMethod("something", Object.class); + MergedAnnotation annotation = + MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get(Order.class); + assertThat(annotation.getInt("value")).isEqualTo(1); + } + + @Test + void getValueFromNonPublicAnnotation() throws Exception { + Annotation[] declaredAnnotations = NonPublicAnnotatedClass.class.getDeclaredAnnotations(); + assertThat(declaredAnnotations).hasSize(1); + Annotation annotation = declaredAnnotations[0]; + MergedAnnotation mergedAnnotation = MergedAnnotation.from(annotation); + assertThat(mergedAnnotation.getType().getSimpleName()).isEqualTo("NonPublicAnnotation"); + assertThat(mergedAnnotation.synthesize().annotationType().getSimpleName()).isEqualTo("NonPublicAnnotation"); + assertThat(mergedAnnotation.getInt("value")).isEqualTo(42); + } + + @Test + void getDefaultValueFromAnnotation() throws Exception { + Method method = TransactionalStringGeneric.class.getMethod("something", Object.class); + MergedAnnotation annotation = + MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get(Order.class); + assertThat(annotation.getDefaultValue("value")).contains(Ordered.LOWEST_PRECEDENCE); + } + + @Test + void getDefaultValueFromNonPublicAnnotation() { + Annotation[] declaredAnnotations = NonPublicAnnotatedClass.class.getDeclaredAnnotations(); + assertThat(declaredAnnotations).hasSize(1); + Annotation declaredAnnotation = declaredAnnotations[0]; + MergedAnnotation annotation = MergedAnnotation.from(declaredAnnotation); + assertThat(annotation.getType().getName()).isEqualTo( + "org.springframework.core.annotation.subpackage.NonPublicAnnotation"); + assertThat(annotation.getDefaultValue("value")).contains(-1); + } + + @Test + void getDefaultValueFromAnnotationType() { + MergedAnnotation annotation = MergedAnnotation.of(Order.class); + assertThat(annotation.getDefaultValue("value")).contains(Ordered.LOWEST_PRECEDENCE); + } + + @Test + void getRepeatableDeclaredOnMethod() throws Exception { + Method method = InterfaceWithRepeated.class.getMethod("foo"); + Stream> annotations = MergedAnnotations.from( + method, SearchStrategy.TYPE_HIERARCHY).stream(MyRepeatable.class); + Stream values = annotations.map( + annotation -> annotation.getString("value")); + assertThat(values).containsExactly("A", "B", "C", "meta1"); + } + + @Test + @SuppressWarnings("deprecation") + void getRepeatableDeclaredOnClassWithAttributeAliases() { + assertThat(MergedAnnotations.from(HierarchyClass.class).stream( + TestConfiguration.class)).isEmpty(); + RepeatableContainers containers = RepeatableContainers.of(TestConfiguration.class, + Hierarchy.class); + MergedAnnotations annotations = MergedAnnotations.from(HierarchyClass.class, + SearchStrategy.DIRECT, containers, AnnotationFilter.NONE); + assertThat(annotations.stream(TestConfiguration.class).map( + annotation -> annotation.getString("location"))).containsExactly("A", "B"); + assertThat(annotations.stream(TestConfiguration.class).map( + annotation -> annotation.getString("value"))).containsExactly("A", "B"); + } + + @Test + void getRepeatableDeclaredOnClass() { + Class element = MyRepeatableClass.class; + String[] expectedValuesJava = { "A", "B", "C" }; + String[] expectedValuesSpring = { "A", "B", "C", "meta1" }; + testRepeatables(SearchStrategy.SUPERCLASS, element, expectedValuesJava, expectedValuesSpring); + } + + @Test + void getRepeatableDeclaredOnSuperclass() { + Class element = SubMyRepeatableClass.class; + String[] expectedValuesJava = { "A", "B", "C" }; + String[] expectedValuesSpring = { "A", "B", "C", "meta1" }; + testRepeatables(SearchStrategy.SUPERCLASS, element, expectedValuesJava, expectedValuesSpring); + } + + @Test + void getRepeatableDeclaredOnClassAndSuperclass() { + Class element = SubMyRepeatableWithAdditionalLocalDeclarationsClass.class; + String[] expectedValuesJava = { "X", "Y", "Z" }; + String[] expectedValuesSpring = { "X", "Y", "Z", "meta2" }; + testRepeatables(SearchStrategy.SUPERCLASS, element, expectedValuesJava, expectedValuesSpring); + } + + @Test + void getRepeatableDeclaredOnMultipleSuperclasses() { + Class element = SubSubMyRepeatableWithAdditionalLocalDeclarationsClass.class; + String[] expectedValuesJava = { "X", "Y", "Z" }; + String[] expectedValuesSpring = { "X", "Y", "Z", "meta2" }; + testRepeatables(SearchStrategy.SUPERCLASS, element, expectedValuesJava, expectedValuesSpring); + } + + @Test + void getDirectRepeatablesDeclaredOnClass() { + Class element = MyRepeatableClass.class; + String[] expectedValuesJava = { "A", "B", "C" }; + String[] expectedValuesSpring = { "A", "B", "C", "meta1" }; + testRepeatables(SearchStrategy.DIRECT, element, expectedValuesJava, expectedValuesSpring); + } + + @Test + void getDirectRepeatablesDeclaredOnSuperclass() { + Class element = SubMyRepeatableClass.class; + String[] expectedValuesJava = {}; + String[] expectedValuesSpring = {}; + testRepeatables(SearchStrategy.DIRECT, element, expectedValuesJava, expectedValuesSpring); + } + + private void testRepeatables(SearchStrategy searchStrategy, Class element, + String[] expectedValuesJava, String[] expectedValuesSpring) { + + testJavaRepeatables(searchStrategy, element, expectedValuesJava); + testExplicitRepeatables(searchStrategy, element, expectedValuesSpring); + testStandardRepeatables(searchStrategy, element, expectedValuesSpring); + } + + private void testJavaRepeatables(SearchStrategy searchStrategy, Class element, String[] expected) { + MyRepeatable[] annotations = searchStrategy == SearchStrategy.DIRECT ? + element.getDeclaredAnnotationsByType(MyRepeatable.class) : + element.getAnnotationsByType(MyRepeatable.class); + assertThat(Arrays.stream(annotations).map(MyRepeatable::value)).containsExactly( + expected); + } + + private void testExplicitRepeatables(SearchStrategy searchStrategy, Class element, String[] expected) { + MergedAnnotations annotations = MergedAnnotations.from(element, searchStrategy, + RepeatableContainers.of(MyRepeatable.class, MyRepeatableContainer.class), + AnnotationFilter.PLAIN); + assertThat(annotations.stream(MyRepeatable.class).filter( + MergedAnnotationPredicates.firstRunOf( + MergedAnnotation::getAggregateIndex)).map( + annotation -> annotation.getString( + "value"))).containsExactly(expected); + } + + private void testStandardRepeatables(SearchStrategy searchStrategy, Class element, String[] expected) { + MergedAnnotations annotations = MergedAnnotations.from(element, searchStrategy); + assertThat(annotations.stream(MyRepeatable.class).filter( + MergedAnnotationPredicates.firstRunOf( + MergedAnnotation::getAggregateIndex)).map( + annotation -> annotation.getString( + "value"))).containsExactly(expected); + } + + @Test + void synthesizeWithoutAttributeAliases() throws Exception { + Component component = WebController.class.getAnnotation(Component.class); + assertThat(component).isNotNull(); + Component synthesizedComponent = MergedAnnotation.from(component).synthesize(); + assertThat(synthesizedComponent).isNotNull(); + assertThat(synthesizedComponent).isEqualTo(component); + assertThat(synthesizedComponent.value()).isEqualTo("webController"); + } + + @Test + void synthesizeAlreadySynthesized() throws Exception { + Method method = WebController.class.getMethod("handleMappedWithValueAttribute"); + RequestMapping webMapping = method.getAnnotation(RequestMapping.class); + assertThat(webMapping).isNotNull(); + + RequestMapping synthesizedWebMapping = MergedAnnotation.from(webMapping).synthesize(); + RequestMapping synthesizedAgainWebMapping = MergedAnnotation.from(synthesizedWebMapping).synthesize(); + + assertThat(synthesizedWebMapping).isInstanceOf(SynthesizedAnnotation.class); + assertThat(synthesizedAgainWebMapping).isInstanceOf(SynthesizedAnnotation.class); + assertThat(synthesizedWebMapping).isEqualTo(synthesizedAgainWebMapping); + assertThat(synthesizedWebMapping.name()).isEqualTo("foo"); + assertThat(synthesizedWebMapping.path()).containsExactly("/test"); + assertThat(synthesizedWebMapping.value()).containsExactly("/test"); + } + + @Test + void synthesizeShouldNotSynthesizeNonsynthesizableAnnotations() throws Exception { + Method method = getClass().getDeclaredMethod("getId"); + + Id id = method.getAnnotation(Id.class); + assertThat(id).isNotNull(); + Id synthesizedId = MergedAnnotation.from(id).synthesize(); + assertThat(id).isEqualTo(synthesizedId); + // It doesn't make sense to synthesize @Id since it declares zero attributes. + assertThat(synthesizedId).isNotInstanceOf(SynthesizedAnnotation.class); + assertThat(id).isSameAs(synthesizedId); + + GeneratedValue generatedValue = method.getAnnotation(GeneratedValue.class); + assertThat(generatedValue).isNotNull(); + GeneratedValue synthesizedGeneratedValue = MergedAnnotation.from(generatedValue).synthesize(); + assertThat(generatedValue).isEqualTo(synthesizedGeneratedValue); + // It doesn't make sense to synthesize @GeneratedValue since it declares zero attributes with aliases. + assertThat(synthesizedGeneratedValue).isNotInstanceOf(SynthesizedAnnotation.class); + assertThat(generatedValue).isSameAs(synthesizedGeneratedValue); + } + + /** + * If an attempt is made to synthesize an annotation from an annotation instance + * that has already been synthesized, the original synthesized annotation should + * ideally be returned as-is without creating a new proxy instance with the same + * values. + */ + @Test + void synthesizeShouldNotResynthesizeAlreadySynthesizedAnnotations() throws Exception { + Method method = WebController.class.getMethod("handleMappedWithValueAttribute"); + RequestMapping webMapping = method.getAnnotation(RequestMapping.class); + assertThat(webMapping).isNotNull(); + + MergedAnnotation mergedAnnotation1 = MergedAnnotation.from(webMapping); + RequestMapping synthesizedWebMapping1 = mergedAnnotation1.synthesize(); + RequestMapping synthesizedWebMapping2 = MergedAnnotation.from(webMapping).synthesize(); + + assertThat(synthesizedWebMapping1).isInstanceOf(SynthesizedAnnotation.class); + assertThat(synthesizedWebMapping2).isInstanceOf(SynthesizedAnnotation.class); + assertThat(synthesizedWebMapping1).isEqualTo(synthesizedWebMapping2); + + // Synthesizing an annotation from a different MergedAnnotation results in a different synthesized annotation instance. + assertThat(synthesizedWebMapping1).isNotSameAs(synthesizedWebMapping2); + // Synthesizing an annotation from the same MergedAnnotation results in the same synthesized annotation instance. + assertThat(synthesizedWebMapping1).isSameAs(mergedAnnotation1.synthesize()); + + RequestMapping synthesizedAgainWebMapping = MergedAnnotation.from(synthesizedWebMapping1).synthesize(); + assertThat(synthesizedWebMapping1).isEqualTo(synthesizedAgainWebMapping); + // Synthesizing an already synthesized annotation results in the original synthesized annotation instance. + assertThat(synthesizedWebMapping1).isSameAs(synthesizedAgainWebMapping); + } + + @Test + void synthesizeWhenAliasForIsMissingAttributeDeclaration() throws Exception { + AliasForWithMissingAttributeDeclaration annotation = + AliasForWithMissingAttributeDeclarationClass.class.getAnnotation( + AliasForWithMissingAttributeDeclaration.class); + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + MergedAnnotation.from(annotation)) + .withMessageStartingWith("@AliasFor declaration on attribute 'foo' in annotation") + .withMessageContaining(AliasForWithMissingAttributeDeclaration.class.getName()) + .withMessageContaining("points to itself"); + } + + @Test + void synthesizeWhenAliasForHasDuplicateAttributeDeclaration() throws Exception { + AliasForWithDuplicateAttributeDeclaration annotation = AliasForWithDuplicateAttributeDeclarationClass.class.getAnnotation( + AliasForWithDuplicateAttributeDeclaration.class); + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + MergedAnnotation.from(annotation)) + .withMessageStartingWith("In @AliasFor declared on attribute 'foo' in annotation") + .withMessageContaining(AliasForWithDuplicateAttributeDeclaration.class.getName()) + .withMessageContaining("attribute 'attribute' and its alias 'value' are present with values of 'baz' and 'bar'"); + } + + @Test + void synthesizeWhenAttributeAliasForNonexistentAttribute() throws Exception { + AliasForNonexistentAttribute annotation = AliasForNonexistentAttributeClass.class.getAnnotation( + AliasForNonexistentAttribute.class); + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + MergedAnnotation.from(annotation)) + .withMessageStartingWith("@AliasFor declaration on attribute 'foo' in annotation") + .withMessageContaining(AliasForNonexistentAttribute.class.getName()) + .withMessageContaining("declares an alias for 'bar' which is not present"); + } + + @Test + void synthesizeWhenAttributeAliasWithMirroredAliasForWrongAttribute() throws Exception { + AliasForWithMirroredAliasForWrongAttribute annotation = + AliasForWithMirroredAliasForWrongAttributeClass.class.getAnnotation( + AliasForWithMirroredAliasForWrongAttribute.class); + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + MergedAnnotation.from(annotation)) + .withMessage("@AliasFor declaration on attribute 'bar' in annotation [" + + AliasForWithMirroredAliasForWrongAttribute.class.getName() + + "] declares an alias for 'quux' which is not present."); + } + + @Test + void synthesizeWhenAttributeAliasForAttributeOfDifferentType() throws Exception { + AliasForAttributeOfDifferentType annotation = AliasForAttributeOfDifferentTypeClass.class.getAnnotation( + AliasForAttributeOfDifferentType.class); + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + MergedAnnotation.from(annotation)) + .withMessageStartingWith("Misconfigured aliases") + .withMessageContaining(AliasForAttributeOfDifferentType.class.getName()) + .withMessageContaining("attribute 'foo'") + .withMessageContaining("attribute 'bar'") + .withMessageContaining("same return type"); + } + + @Test + void synthesizeWhenAttributeAliasForWithMissingDefaultValues() throws Exception { + AliasForWithMissingDefaultValues annotation = AliasForWithMissingDefaultValuesClass.class.getAnnotation( + AliasForWithMissingDefaultValues.class); + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + MergedAnnotation.from(annotation)) + .withMessageStartingWith("Misconfigured aliases") + .withMessageContaining(AliasForWithMissingDefaultValues.class.getName()) + .withMessageContaining("attribute 'foo' in annotation") + .withMessageContaining("attribute 'bar' in annotation") + .withMessageContaining("default values"); + } + + @Test + void synthesizeWhenAttributeAliasForAttributeWithDifferentDefaultValue() throws Exception { + AliasForAttributeWithDifferentDefaultValue annotation = + AliasForAttributeWithDifferentDefaultValueClass.class.getAnnotation( + AliasForAttributeWithDifferentDefaultValue.class); + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + MergedAnnotation.from(annotation)) + .withMessageStartingWith("Misconfigured aliases") + .withMessageContaining(AliasForAttributeWithDifferentDefaultValue.class.getName()) + .withMessageContaining("attribute 'foo' in annotation") + .withMessageContaining("attribute 'bar' in annotation") + .withMessageContaining("same default value"); + } + + @Test + void synthesizeWhenAttributeAliasForMetaAnnotationThatIsNotMetaPresent() throws Exception { + AliasedComposedTestConfigurationNotMetaPresent annotation = + AliasedComposedTestConfigurationNotMetaPresentClass.class.getAnnotation( + AliasedComposedTestConfigurationNotMetaPresent.class); + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + MergedAnnotation.from(annotation)) + .withMessageStartingWith("@AliasFor declaration on attribute 'xmlConfigFile' in annotation") + .withMessageContaining(AliasedComposedTestConfigurationNotMetaPresent.class.getName()) + .withMessageContaining("declares an alias for attribute 'location' in annotation") + .withMessageContaining(TestConfiguration.class.getName()) + .withMessageContaining("not meta-present"); + } + + @Test + void synthesizeWithImplicitAliases() throws Exception { + testSynthesisWithImplicitAliases(ValueImplicitAliasesTestConfigurationClass.class, "value"); + testSynthesisWithImplicitAliases(Location1ImplicitAliasesTestConfigurationClass.class, "location1"); + testSynthesisWithImplicitAliases(XmlImplicitAliasesTestConfigurationClass.class, "xmlFile"); + testSynthesisWithImplicitAliases(GroovyImplicitAliasesSimpleTestConfigurationClass.class, "groovyScript"); + } + + private void testSynthesisWithImplicitAliases(Class clazz, String expected) + throws Exception { + ImplicitAliasesTestConfiguration config = clazz.getAnnotation( + ImplicitAliasesTestConfiguration.class); + assertThat(config).isNotNull(); + ImplicitAliasesTestConfiguration synthesized = MergedAnnotation.from( + config).synthesize(); + assertThat(synthesized).isInstanceOf(SynthesizedAnnotation.class); + assertThat(synthesized.value()).isEqualTo(expected); + assertThat(synthesized.location1()).isEqualTo(expected); + assertThat(synthesized.xmlFile()).isEqualTo(expected); + assertThat(synthesized.groovyScript()).isEqualTo(expected); + } + + @Test + void synthesizeWithImplicitAliasesWithImpliedAliasNamesOmitted() + throws Exception { + testSynthesisWithImplicitAliasesWithImpliedAliasNamesOmitted( + ValueImplicitAliasesWithImpliedAliasNamesOmittedTestConfigurationClass.class, + "value"); + testSynthesisWithImplicitAliasesWithImpliedAliasNamesOmitted( + LocationsImplicitAliasesWithImpliedAliasNamesOmittedTestConfigurationClass.class, + "location"); + testSynthesisWithImplicitAliasesWithImpliedAliasNamesOmitted( + XmlFilesImplicitAliasesWithImpliedAliasNamesOmittedTestConfigurationClass.class, + "xmlFile"); + } + + private void testSynthesisWithImplicitAliasesWithImpliedAliasNamesOmitted( + Class clazz, String expected) { + ImplicitAliasesWithImpliedAliasNamesOmittedTestConfiguration config = clazz.getAnnotation( + ImplicitAliasesWithImpliedAliasNamesOmittedTestConfiguration.class); + assertThat(config).isNotNull(); + ImplicitAliasesWithImpliedAliasNamesOmittedTestConfiguration synthesized = + MergedAnnotation.from(config).synthesize(); + assertThat(synthesized).isInstanceOf(SynthesizedAnnotation.class); + assertThat(synthesized.value()).isEqualTo(expected); + assertThat(synthesized.location()).isEqualTo(expected); + assertThat(synthesized.xmlFile()).isEqualTo(expected); + } + + @Test + void synthesizeWithImplicitAliasesForAliasPair() throws Exception { + ImplicitAliasesForAliasPairTestConfiguration config = + ImplicitAliasesForAliasPairTestConfigurationClass.class.getAnnotation( + ImplicitAliasesForAliasPairTestConfiguration.class); + ImplicitAliasesForAliasPairTestConfiguration synthesized = MergedAnnotation.from(config).synthesize(); + assertThat(synthesized).isInstanceOf(SynthesizedAnnotation.class); + assertThat(synthesized.xmlFile()).isEqualTo("test.xml"); + assertThat(synthesized.groovyScript()).isEqualTo("test.xml"); + } + + @Test + void synthesizeWithTransitiveImplicitAliases() throws Exception { + TransitiveImplicitAliasesTestConfiguration config = + TransitiveImplicitAliasesTestConfigurationClass.class.getAnnotation( + TransitiveImplicitAliasesTestConfiguration.class); + TransitiveImplicitAliasesTestConfiguration synthesized = MergedAnnotation.from(config).synthesize(); + assertThat(synthesized).isInstanceOf(SynthesizedAnnotation.class); + assertThat(synthesized.xml()).isEqualTo("test.xml"); + assertThat(synthesized.groovy()).isEqualTo("test.xml"); + } + + @Test + void synthesizeWithTransitiveImplicitAliasesForAliasPair() throws Exception { + TransitiveImplicitAliasesForAliasPairTestConfiguration config = + TransitiveImplicitAliasesForAliasPairTestConfigurationClass.class.getAnnotation( + TransitiveImplicitAliasesForAliasPairTestConfiguration.class); + TransitiveImplicitAliasesForAliasPairTestConfiguration synthesized = MergedAnnotation.from( + config).synthesize(); + assertThat(synthesized).isInstanceOf(SynthesizedAnnotation.class); + assertThat(synthesized.xml()).isEqualTo("test.xml"); + assertThat(synthesized.groovy()).isEqualTo("test.xml"); + } + + @Test + void synthesizeWithImplicitAliasesWithMissingDefaultValues() throws Exception { + Class clazz = ImplicitAliasesWithMissingDefaultValuesTestConfigurationClass.class; + Class annotationType = + ImplicitAliasesWithMissingDefaultValuesTestConfiguration.class; + ImplicitAliasesWithMissingDefaultValuesTestConfiguration config = clazz.getAnnotation( + annotationType); + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + MergedAnnotation.from(clazz, config)) + .withMessageStartingWith("Misconfigured aliases:") + .withMessageContaining("attribute 'location1' in annotation [" + annotationType.getName() + "]") + .withMessageContaining("attribute 'location2' in annotation [" + annotationType.getName() + "]") + .withMessageContaining("default values"); + } + + @Test + void synthesizeWithImplicitAliasesWithDifferentDefaultValues() + throws Exception { + Class clazz = ImplicitAliasesWithDifferentDefaultValuesTestConfigurationClass.class; + Class annotationType = + ImplicitAliasesWithDifferentDefaultValuesTestConfiguration.class; + ImplicitAliasesWithDifferentDefaultValuesTestConfiguration config = clazz.getAnnotation( + annotationType); + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + MergedAnnotation.from(clazz, config)) + .withMessageStartingWith("Misconfigured aliases:") + .withMessageContaining("attribute 'location1' in annotation [" + annotationType.getName() + "]") + .withMessageContaining("attribute 'location2' in annotation [" + annotationType.getName() + "]") + .withMessageContaining("same default value"); + } + + @Test + void synthesizeWithImplicitAliasesWithDuplicateValues() throws Exception { + Class clazz = ImplicitAliasesWithDuplicateValuesTestConfigurationClass.class; + Class annotationType = + ImplicitAliasesWithDuplicateValuesTestConfiguration.class; + ImplicitAliasesWithDuplicateValuesTestConfiguration config = clazz.getAnnotation( + annotationType); + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + MergedAnnotation.from(clazz, config)) + .withMessageStartingWith("Different @AliasFor mirror values for annotation") + .withMessageContaining(annotationType.getName()) + .withMessageContaining("declared on class") + .withMessageContaining(clazz.getName()) + .withMessageContaining("are declared with values of"); + } + + @Test + void synthesizeFromMapWithoutAttributeAliases() throws Exception { + Component component = WebController.class.getAnnotation(Component.class); + assertThat(component).isNotNull(); + Map map = Collections.singletonMap("value", "webController"); + MergedAnnotation annotation = MergedAnnotation.of(Component.class, map); + Component synthesizedComponent = annotation.synthesize(); + assertThat(synthesizedComponent).isInstanceOf(SynthesizedAnnotation.class); + assertThat(synthesizedComponent.value()).isEqualTo("webController"); + } + + @Test + @SuppressWarnings("unchecked") + void synthesizeFromMapWithNestedMap() throws Exception { + ComponentScanSingleFilter componentScan = ComponentScanSingleFilterClass.class.getAnnotation( + ComponentScanSingleFilter.class); + assertThat(componentScan).isNotNull(); + assertThat(componentScan.value().pattern()).isEqualTo("*Foo"); + Map map = MergedAnnotation.from(componentScan).asMap( + annotation -> new LinkedHashMap(), + Adapt.ANNOTATION_TO_MAP); + Map filterMap = (Map) map.get("value"); + assertThat(filterMap.get("pattern")).isEqualTo("*Foo"); + filterMap.put("pattern", "newFoo"); + filterMap.put("enigma", 42); + MergedAnnotation annotation = MergedAnnotation.of( + ComponentScanSingleFilter.class, map); + ComponentScanSingleFilter synthesizedComponentScan = annotation.synthesize(); + assertThat(synthesizedComponentScan).isInstanceOf(SynthesizedAnnotation.class); + assertThat(synthesizedComponentScan.value().pattern()).isEqualTo("newFoo"); + } + + @Test + @SuppressWarnings("unchecked") + void synthesizeFromMapWithNestedArrayOfMaps() throws Exception { + ComponentScan componentScan = ComponentScanClass.class.getAnnotation( + ComponentScan.class); + assertThat(componentScan).isNotNull(); + Map map = MergedAnnotation.from(componentScan).asMap( + annotation -> new LinkedHashMap(), + Adapt.ANNOTATION_TO_MAP); + Map[] filters = (Map[]) map.get("excludeFilters"); + List patterns = Arrays.stream(filters).map( + m -> (String) m.get("pattern")).collect(Collectors.toList()); + assertThat(patterns).containsExactly("*Foo", "*Bar"); + filters[0].put("pattern", "newFoo"); + filters[0].put("enigma", 42); + filters[1].put("pattern", "newBar"); + filters[1].put("enigma", 42); + MergedAnnotation annotation = MergedAnnotation.of( + ComponentScan.class, map); + ComponentScan synthesizedComponentScan = annotation.synthesize(); + assertThat(synthesizedComponentScan).isInstanceOf(SynthesizedAnnotation.class); + assertThat(Arrays.stream(synthesizedComponentScan.excludeFilters()).map( + Filter::pattern)).containsExactly("newFoo", "newBar"); + } + + @Test + void synthesizeFromDefaultsWithoutAttributeAliases() throws Exception { + MergedAnnotation annotation = MergedAnnotation.of( + AnnotationWithDefaults.class); + AnnotationWithDefaults synthesized = annotation.synthesize(); + assertThat(synthesized.text()).isEqualTo("enigma"); + assertThat(synthesized.predicate()).isTrue(); + assertThat(synthesized.characters()).containsExactly('a', 'b', 'c'); + } + + @Test + void synthesizeFromDefaultsWithAttributeAliases() throws Exception { + MergedAnnotation annotation = MergedAnnotation.of( + TestConfiguration.class); + TestConfiguration synthesized = annotation.synthesize(); + assertThat(synthesized.value()).isEqualTo(""); + assertThat(synthesized.location()).isEqualTo(""); + } + + @Test + void synthesizeWhenAttributeAliasesWithDifferentValues() throws Exception { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + MergedAnnotation.from(TestConfigurationMismatch.class.getAnnotation(TestConfiguration.class)).synthesize()); + } + + @Test + void synthesizeFromMapWithMinimalAttributesWithAttributeAliases() + throws Exception { + Map map = Collections.singletonMap("location", "test.xml"); + MergedAnnotation annotation = MergedAnnotation.of( + TestConfiguration.class, map); + TestConfiguration synthesized = annotation.synthesize(); + assertThat(synthesized.value()).isEqualTo("test.xml"); + assertThat(synthesized.location()).isEqualTo("test.xml"); + } + + @Test + void synthesizeFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements() + throws Exception { + synthesizeFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements( + Collections.singletonMap("value", "/foo")); + synthesizeFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements( + Collections.singletonMap("path", "/foo")); + } + + private void synthesizeFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements( + Map map) { + MergedAnnotation annotation = MergedAnnotation.of(GetMapping.class, + map); + GetMapping synthesized = annotation.synthesize(); + assertThat(synthesized.value()).isEqualTo("/foo"); + assertThat(synthesized.path()).isEqualTo("/foo"); + } + + @Test + void synthesizeFromMapWithImplicitAttributeAliases() throws Exception { + testSynthesisFromMapWithImplicitAliases("value"); + testSynthesisFromMapWithImplicitAliases("location1"); + testSynthesisFromMapWithImplicitAliases("location2"); + testSynthesisFromMapWithImplicitAliases("location3"); + testSynthesisFromMapWithImplicitAliases("xmlFile"); + testSynthesisFromMapWithImplicitAliases("groovyScript"); + } + + private void testSynthesisFromMapWithImplicitAliases(String attributeNameAndValue) + throws Exception { + Map map = Collections.singletonMap(attributeNameAndValue, + attributeNameAndValue); + MergedAnnotation annotation = MergedAnnotation.of( + ImplicitAliasesTestConfiguration.class, map); + ImplicitAliasesTestConfiguration synthesized = annotation.synthesize(); + assertThat(synthesized.value()).isEqualTo(attributeNameAndValue); + assertThat(synthesized.location1()).isEqualTo(attributeNameAndValue); + assertThat(synthesized.location2()).isEqualTo(attributeNameAndValue); + assertThat(synthesized.location2()).isEqualTo(attributeNameAndValue); + assertThat(synthesized.xmlFile()).isEqualTo(attributeNameAndValue); + assertThat(synthesized.groovyScript()).isEqualTo(attributeNameAndValue); + } + + @Test + void synthesizeFromMapWithMissingAttributeValue() throws Exception { + testMissingTextAttribute(Collections.emptyMap()); + } + + @Test + void synthesizeFromMapWithNullAttributeValue() throws Exception { + Map map = Collections.singletonMap("text", null); + assertThat(map).containsKey("text"); + testMissingTextAttribute(map); + } + + private void testMissingTextAttribute(Map attributes) { + assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(() -> + MergedAnnotation.of(AnnotationWithoutDefaults.class, attributes).synthesize().text()) + .withMessage("No value found for attribute named 'text' in merged annotation " + + AnnotationWithoutDefaults.class.getName()); + } + + @Test + void synthesizeFromMapWithAttributeOfIncorrectType() throws Exception { + Map map = Collections.singletonMap("value", 42L); + MergedAnnotation annotation = MergedAnnotation.of(Component.class, map); + assertThatIllegalStateException().isThrownBy(() -> annotation.synthesize().value()) + .withMessage("Attribute 'value' in annotation " + + "org.springframework.core.testfixture.stereotype.Component should be " + + "compatible with java.lang.String but a java.lang.Long value was returned"); + } + + @Test + void synthesizeFromAnnotationAttributesWithoutAttributeAliases() throws Exception { + Component component = WebController.class.getAnnotation(Component.class); + assertThat(component).isNotNull(); + Map attributes = MergedAnnotation.from(component).asMap(); + Component synthesized = MergedAnnotation.of(Component.class, attributes).synthesize(); + assertThat(synthesized).isInstanceOf(SynthesizedAnnotation.class); + assertThat(synthesized).isEqualTo(component); + } + + @Test + void toStringForSynthesizedAnnotations() throws Exception { + Method methodWithPath = WebController.class.getMethod("handleMappedWithPathAttribute"); + RequestMapping webMappingWithAliases = methodWithPath.getAnnotation(RequestMapping.class); + assertThat(webMappingWithAliases).isNotNull(); + Method methodWithPathAndValue = WebController.class.getMethod("handleMappedWithSamePathAndValueAttributes"); + RequestMapping webMappingWithPathAndValue = methodWithPathAndValue.getAnnotation(RequestMapping.class); + assertThat(methodWithPathAndValue).isNotNull(); + RequestMapping synthesizedWebMapping1 = MergedAnnotation.from(webMappingWithAliases).synthesize(); + RequestMapping synthesizedWebMapping2 = MergedAnnotation.from(webMappingWithPathAndValue).synthesize(); + assertThat(webMappingWithAliases.toString()).isNotEqualTo(synthesizedWebMapping1.toString()); + assertToStringForWebMappingWithPathAndValue(synthesizedWebMapping1); + assertToStringForWebMappingWithPathAndValue(synthesizedWebMapping2); + } + + private void assertToStringForWebMappingWithPathAndValue(RequestMapping webMapping) { + String prefix = "@" + RequestMapping.class.getName() + "("; + assertThat(webMapping.toString()).startsWith(prefix).contains("value=[/test]", + "path=[/test]", "name=bar", "method=", "[GET, POST]").endsWith(")"); + } + + @Test + void equalsForSynthesizedAnnotations() throws Exception { + Method methodWithPath = WebController.class.getMethod( + "handleMappedWithPathAttribute"); + RequestMapping webMappingWithAliases = methodWithPath.getAnnotation( + RequestMapping.class); + assertThat(webMappingWithAliases).isNotNull(); + Method methodWithPathAndValue = WebController.class.getMethod( + "handleMappedWithSamePathAndValueAttributes"); + RequestMapping webMappingWithPathAndValue = methodWithPathAndValue.getAnnotation( + RequestMapping.class); + assertThat(webMappingWithPathAndValue).isNotNull(); + RequestMapping synthesizedWebMapping1 = MergedAnnotation.from( + webMappingWithAliases).synthesize(); + RequestMapping synthesizedWebMapping2 = MergedAnnotation.from( + webMappingWithPathAndValue).synthesize(); + // Equality amongst standard annotations + assertThat(webMappingWithAliases).isEqualTo(webMappingWithAliases); + assertThat(webMappingWithPathAndValue).isEqualTo(webMappingWithPathAndValue); + // Inequality amongst standard annotations + assertThat(webMappingWithAliases).isNotEqualTo(webMappingWithPathAndValue); + assertThat(webMappingWithPathAndValue).isNotEqualTo(webMappingWithAliases); + // Equality amongst synthesized annotations + assertThat(synthesizedWebMapping1).isEqualTo(synthesizedWebMapping1); + assertThat(synthesizedWebMapping2).isEqualTo(synthesizedWebMapping2); + assertThat(synthesizedWebMapping1).isEqualTo(synthesizedWebMapping2); + assertThat(synthesizedWebMapping2).isEqualTo(synthesizedWebMapping1); + // Equality between standard and synthesized annotations + assertThat(synthesizedWebMapping1).isEqualTo(webMappingWithPathAndValue); + assertThat(webMappingWithPathAndValue).isEqualTo(synthesizedWebMapping1); + // Inequality between standard and synthesized annotations + assertThat(synthesizedWebMapping1).isNotEqualTo(webMappingWithAliases); + assertThat(webMappingWithAliases).isNotEqualTo(synthesizedWebMapping1); + } + + @Test + void hashCodeForSynthesizedAnnotations() throws Exception { + Method methodWithPath = WebController.class.getMethod( + "handleMappedWithPathAttribute"); + RequestMapping webMappingWithAliases = methodWithPath.getAnnotation( + RequestMapping.class); + assertThat(webMappingWithAliases).isNotNull(); + Method methodWithPathAndValue = WebController.class.getMethod( + "handleMappedWithSamePathAndValueAttributes"); + RequestMapping webMappingWithPathAndValue = methodWithPathAndValue.getAnnotation( + RequestMapping.class); + assertThat(webMappingWithPathAndValue).isNotNull(); + RequestMapping synthesizedWebMapping1 = MergedAnnotation.from( + webMappingWithAliases).synthesize(); + assertThat(synthesizedWebMapping1).isNotNull(); + RequestMapping synthesizedWebMapping2 = MergedAnnotation.from( + webMappingWithPathAndValue).synthesize(); + assertThat(synthesizedWebMapping2).isNotNull(); + // Equality amongst standard annotations + assertThat(webMappingWithAliases.hashCode()).isEqualTo( + webMappingWithAliases.hashCode()); + assertThat(webMappingWithPathAndValue.hashCode()).isEqualTo( + webMappingWithPathAndValue.hashCode()); + // Inequality amongst standard annotations + assertThat(webMappingWithAliases.hashCode()).isNotEqualTo( + webMappingWithPathAndValue.hashCode()); + assertThat(webMappingWithPathAndValue.hashCode()).isNotEqualTo( + webMappingWithAliases.hashCode()); + // Equality amongst synthesized annotations + assertThat(synthesizedWebMapping1.hashCode()).isEqualTo( + synthesizedWebMapping1.hashCode()); + assertThat(synthesizedWebMapping2.hashCode()).isEqualTo( + synthesizedWebMapping2.hashCode()); + assertThat(synthesizedWebMapping1.hashCode()).isEqualTo( + synthesizedWebMapping2.hashCode()); + assertThat(synthesizedWebMapping2.hashCode()).isEqualTo( + synthesizedWebMapping1.hashCode()); + // Equality between standard and synthesized annotations + assertThat(synthesizedWebMapping1.hashCode()).isEqualTo( + webMappingWithPathAndValue.hashCode()); + assertThat(webMappingWithPathAndValue.hashCode()).isEqualTo( + synthesizedWebMapping1.hashCode()); + // Inequality between standard and synthesized annotations + assertThat(synthesizedWebMapping1.hashCode()).isNotEqualTo( + webMappingWithAliases.hashCode()); + assertThat(webMappingWithAliases.hashCode()).isNotEqualTo( + synthesizedWebMapping1.hashCode()); + } + + /** + * Fully reflection-based test that verifies support for synthesizing + * annotations across packages with non-public visibility of user types + * (e.g., a non-public annotation that uses {@code @AliasFor}). + */ + @Test + @SuppressWarnings("unchecked") + void synthesizeNonPublicWithAttributeAliasesFromDifferentPackage() throws Exception { + Class type = ClassUtils.forName( + "org.springframework.core.annotation.subpackage.NonPublicAliasedAnnotatedClass", + null); + Class annotationType = (Class) ClassUtils.forName( + "org.springframework.core.annotation.subpackage.NonPublicAliasedAnnotation", + null); + Annotation annotation = type.getAnnotation(annotationType); + assertThat(annotation).isNotNull(); + MergedAnnotation mergedAnnotation = MergedAnnotation.from(annotation); + Annotation synthesizedAnnotation = mergedAnnotation.synthesize(); + assertThat(synthesizedAnnotation).isInstanceOf(SynthesizedAnnotation.class); + assertThat(mergedAnnotation.getString("name")).isEqualTo("test"); + assertThat(mergedAnnotation.getString("path")).isEqualTo("/test"); + assertThat(mergedAnnotation.getString("value")).isEqualTo("/test"); + } + + @Test + void synthesizeWithArrayOfAnnotations() throws Exception { + Hierarchy hierarchy = HierarchyClass.class.getAnnotation(Hierarchy.class); + assertThat(hierarchy).isNotNull(); + Hierarchy synthesizedHierarchy = MergedAnnotation.from(hierarchy).synthesize(); + assertThat(synthesizedHierarchy).isInstanceOf(SynthesizedAnnotation.class); + TestConfiguration[] configs = synthesizedHierarchy.value(); + assertThat(configs).isNotNull(); + assertThat(configs).allMatch(SynthesizedAnnotation.class::isInstance); + assertThat(configs).extracting(TestConfiguration::value).containsExactly("A", "B"); + assertThat(configs).extracting(TestConfiguration::location).containsExactly("A", "B"); + + TestConfiguration contextConfig = TestConfigurationClass.class.getAnnotation(TestConfiguration.class); + assertThat(contextConfig).isNotNull(); + // Alter array returned from synthesized annotation + configs[0] = contextConfig; + assertThat(configs).extracting(TestConfiguration::value).containsExactly("simple.xml", "B"); + // Re-retrieve the array from the synthesized annotation + configs = synthesizedHierarchy.value(); + assertThat(configs).extracting(TestConfiguration::value).containsExactly("A", "B"); + } + + @Test + void synthesizeWithArrayOfChars() throws Exception { + CharsContainer charsContainer = GroupOfCharsClass.class.getAnnotation( + CharsContainer.class); + assertThat(charsContainer).isNotNull(); + CharsContainer synthesizedCharsContainer = MergedAnnotation.from( + charsContainer).synthesize(); + assertThat(synthesizedCharsContainer).isInstanceOf(SynthesizedAnnotation.class); + char[] chars = synthesizedCharsContainer.chars(); + assertThat(chars).containsExactly('x', 'y', 'z'); + // Alter array returned from synthesized annotation + chars[0] = '?'; + // Re-retrieve the array from the synthesized annotation + chars = synthesizedCharsContainer.chars(); + assertThat(chars).containsExactly('x', 'y', 'z'); + } + + @Test + void getValueWhenHasDefaultOverride() { + MergedAnnotation annotation = MergedAnnotations.from( + DefaultOverrideClass.class).get(DefaultOverrideRoot.class); + assertThat(annotation.getString("text")).isEqualTo("metameta"); + } + + @Test // gh-22654 + void getValueWhenHasDefaultOverrideWithImplicitAlias() { + MergedAnnotation annotation1 = MergedAnnotations.from( + DefaultOverrideImplicitAliasMetaClass1.class).get(DefaultOverrideRoot.class); + assertThat(annotation1.getString("text")).isEqualTo("alias-meta-1"); + MergedAnnotation annotation2 = MergedAnnotations.from( + DefaultOverrideImplicitAliasMetaClass2.class).get(DefaultOverrideRoot.class); + assertThat(annotation2.getString("text")).isEqualTo("alias-meta-2"); + } + + @Test // gh-22654 + void getValueWhenHasDefaultOverrideWithExplicitAlias() { + MergedAnnotation annotation = MergedAnnotations.from( + DefaultOverrideExplicitAliasRootMetaMetaClass.class).get( + DefaultOverrideExplicitAliasRoot.class); + assertThat(annotation.getString("text")).isEqualTo("meta"); + assertThat(annotation.getString("value")).isEqualTo("meta"); + } + + @Test // gh-22703 + void getValueWhenThreeDeepMetaWithValue() { + MergedAnnotation annotation = MergedAnnotations.from( + ValueAttributeMetaMetaClass.class).get(ValueAttribute.class); + assertThat(annotation.getStringArray(MergedAnnotation.VALUE)).containsExactly( + "FromValueAttributeMeta"); + } + + @Test + void asAnnotationAttributesReturnsPopulatedAnnotationAttributes() { + MergedAnnotation annotation = MergedAnnotations.from( + SpringApplicationConfigurationClass.class).get( + SpringApplicationConfiguration.class); + AnnotationAttributes attributes = annotation.asAnnotationAttributes( + Adapt.CLASS_TO_STRING); + assertThat(attributes).containsEntry("classes", new String[] { Number.class.getName() }); + assertThat(attributes.annotationType()).isEqualTo(SpringApplicationConfiguration.class); + } + + // @formatter:off + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.TYPE, ElementType.METHOD }) + @Inherited + @interface Transactional { + + String value() default ""; + + String qualifier() default "transactionManager"; + + boolean readOnly() default false; + } + + @Transactional + @Component + @Retention(RetentionPolicy.RUNTIME) + @interface TransactionalComponent { + } + + @TransactionalComponent + @Retention(RetentionPolicy.RUNTIME) + @interface ComposedTransactionalComponent { + } + + static class NonAnnotatedClass { + } + + @Component + static class AnnotatedClass { + + class NonAnnotatedInnerClass { + } + + static class NonAnnotatedStaticNestedClass { + } + } + + static interface NonAnnotatedInterface { + } + + @TransactionalComponent + static class TransactionalComponentClass { + } + + static class SubTransactionalComponentClass extends TransactionalComponentClass { + } + + @ComposedTransactionalComponent + static class ComposedTransactionalComponentClass { + } + + @AliasedTransactionalComponent + static class AliasedTransactionalComponentClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.TYPE, ElementType.METHOD }) + @Inherited + @interface AliasedTransactional { + + @AliasFor(attribute = "qualifier") + String value() default ""; + + @AliasFor(attribute = "value") + String qualifier() default ""; + } + + @Transactional(qualifier = "composed1") + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @Inherited + @interface InheritedComposed { + } + + @Transactional(qualifier = "composed2", readOnly = true) + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @interface Composed { + } + + @Transactional + @Retention(RetentionPolicy.RUNTIME) + @interface TxComposedWithOverride { + + String qualifier() default "txMgr"; + } + + @Transactional("TxInheritedComposed") + @Retention(RetentionPolicy.RUNTIME) + @interface TxInheritedComposed { + } + + @Transactional("TxComposed") + @Retention(RetentionPolicy.RUNTIME) + @interface TxComposed { + } + + @AliasedTransactional(value = "aliasForQualifier") + @Component + @Retention(RetentionPolicy.RUNTIME) + @interface AliasedTransactionalComponent { + } + + @AliasedTransactional + @Retention(RetentionPolicy.RUNTIME) + @interface MyAliasedTransactional { + + @AliasFor(annotation = AliasedTransactional.class, attribute = "value") + String value() default "defaultTransactionManager"; + } + + @MyAliasedTransactional("anotherTransactionManager") + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.TYPE, ElementType.METHOD}) + @interface ComposedMyAliasedTransactional { + } + + @ComposedMyAliasedTransactional + static class ComposedTransactionalClass { + } + + @AliasedTransactional("meta") + @Retention(RetentionPolicy.RUNTIME) + @interface MetaAliasedTransactional { + } + + @MetaAliasedTransactional + @Retention(RetentionPolicy.RUNTIME) + @interface MetaMetaAliasedTransactional { + } + + @MetaMetaAliasedTransactional + static class MetaMetaAliasedTransactionalClass { + } + + @TxComposedWithOverride + // Override default "txMgr" from @TxComposedWithOverride with "localTxMgr" + @Transactional(qualifier = "localTxMgr") + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @interface MetaAndLocalTxConfig { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface TestPropertySource { + + @AliasFor("locations") + String[] value() default {}; + + @AliasFor("value") + String[] locations() default {}; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ContextConfiguration { + + @AliasFor(attribute = "locations") + String[] value() default {}; + + @AliasFor(attribute = "value") + String[] locations() default {}; + + Class[] classes() default {}; + } + + @ContextConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface ConventionBasedComposedContextConfiguration { + + String[] locations() default {}; + } + + @ContextConfiguration(value = "duplicateDeclaration") + @Retention(RetentionPolicy.RUNTIME) + @interface InvalidConventionBasedComposedContextConfiguration { + + String[] locations(); + } + + /** + * This hybrid approach for annotation attribute overrides with transitive implicit + * aliases is unsupported. See SPR-13554 for details. + */ + @ContextConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface HalfConventionBasedAndHalfAliasedComposedContextConfiguration { + + String[] locations() default {}; + + @AliasFor(annotation = ContextConfiguration.class, attribute = "locations") + String[] xmlConfigFiles() default {}; + } + + @ContextConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface AliasedComposedContextConfiguration { + + @AliasFor(annotation = ContextConfiguration.class, attribute = "locations") + String[] xmlConfigFiles(); + } + + @ContextConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface AliasedValueComposedContextConfiguration { + + @AliasFor(annotation = ContextConfiguration.class, attribute = "value") + String[] locations(); + } + + @ContextConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface ImplicitAliasesContextConfiguration { + + @AliasFor(annotation = ContextConfiguration.class, attribute = "locations") + String[] groovyScripts() default {}; + + @AliasFor(annotation = ContextConfiguration.class, attribute = "locations") + String[] xmlFiles() default {}; + + // intentionally omitted: attribute = "locations" + @AliasFor(annotation = ContextConfiguration.class) + String[] locations() default {}; + + // intentionally omitted: attribute = "locations" (SPR-14069) + @AliasFor(annotation = ContextConfiguration.class) + String[] value() default {}; + } + + @ImplicitAliasesContextConfiguration(xmlFiles = { "A.xml", "B.xml" }) + @Retention(RetentionPolicy.RUNTIME) + @interface ComposedImplicitAliasesContextConfiguration { + } + + @ImplicitAliasesContextConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface TransitiveImplicitAliasesContextConfiguration { + + @AliasFor(annotation = ImplicitAliasesContextConfiguration.class, attribute = "xmlFiles") + String[] xml() default {}; + + @AliasFor(annotation = ImplicitAliasesContextConfiguration.class, attribute = "groovyScripts") + String[] groovy() default {}; + } + + @ImplicitAliasesContextConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface SingleLocationTransitiveImplicitAliasesContextConfiguration { + + @AliasFor(annotation = ImplicitAliasesContextConfiguration.class, attribute = "xmlFiles") + String xml() default ""; + + @AliasFor(annotation = ImplicitAliasesContextConfiguration.class, attribute = "groovyScripts") + String groovy() default ""; + } + + @ImplicitAliasesContextConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface TransitiveImplicitAliasesWithSkippedLevelContextConfiguration { + + @AliasFor(annotation = ContextConfiguration.class, attribute = "locations") + String[] xml() default {}; + + @AliasFor(annotation = ImplicitAliasesContextConfiguration.class, attribute = "groovyScripts") + String[] groovy() default {}; + } + + @ImplicitAliasesContextConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface SingleLocationTransitiveImplicitAliasesWithSkippedLevelContextConfiguration { + + @AliasFor(annotation = ContextConfiguration.class, attribute = "locations") + String xml() default ""; + + @AliasFor(annotation = ImplicitAliasesContextConfiguration.class, attribute = "groovyScripts") + String groovy() default ""; + } + + /** + * Although the configuration declares an explicit value for 'value' and requires a + * value for the aliased 'locations', this does not result in an error since + * 'locations' effectively shadows the 'value' attribute (which cannot be set via the + * composed annotation anyway). If 'value' were not shadowed, such a declaration would + * not make sense. + */ + @ContextConfiguration(value = "duplicateDeclaration") + @Retention(RetentionPolicy.RUNTIME) + @interface ShadowedAliasComposedContextConfiguration { + + @AliasFor(annotation = ContextConfiguration.class, attribute = "locations") + String[] xmlConfigFiles(); + } + + @ContextConfiguration(locations = "shadowed.xml") + @TestPropertySource(locations = "test.properties") + @Retention(RetentionPolicy.RUNTIME) + @interface AliasedComposedContextConfigurationAndTestPropertySource { + + @AliasFor(annotation = ContextConfiguration.class, attribute = "locations") + String[] xmlConfigFiles() default "default.xml"; + } + + @ContextConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface SpringApplicationConfiguration { + + @AliasFor(annotation = ContextConfiguration.class, attribute = "locations") + String[] locations() default {}; + + @AliasFor("value") + Class[] classes() default {}; + + @AliasFor("classes") + Class[] value() default {}; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ComponentScan { + + @AliasFor("basePackages") + String[] value() default {}; + + @AliasFor("value") + String[] basePackages() default {}; + + Filter[] excludeFilters() default {}; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({}) + @interface Filter { + + String pattern(); + } + + @ComponentScan(excludeFilters = { @Filter(pattern = "*Test"), + @Filter(pattern = "*Tests") }) + @Retention(RetentionPolicy.RUNTIME) + @interface TestComponentScan { + + @AliasFor(attribute = "basePackages", annotation = ComponentScan.class) + String[] packages(); + } + + @ComponentScan + @Retention(RetentionPolicy.RUNTIME) + @interface ConventionBasedSinglePackageComponentScan { + + String basePackages(); + } + + @ComponentScan + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForBasedSinglePackageComponentScan { + + @AliasFor(attribute = "basePackages", annotation = ComponentScan.class) + String pkg(); + } + + @Transactional + static class ClassWithInheritedAnnotation { + } + + @Composed + static class SubClassWithInheritedAnnotation extends ClassWithInheritedAnnotation { + } + + static class SubSubClassWithInheritedAnnotation + extends SubClassWithInheritedAnnotation { + } + + @InheritedComposed + static class ClassWithInheritedComposedAnnotation { + } + + @Composed + static class SubClassWithInheritedComposedAnnotation + extends ClassWithInheritedComposedAnnotation { + } + + static class SubSubClassWithInheritedComposedAnnotation + extends SubClassWithInheritedComposedAnnotation { + } + + @MetaAndLocalTxConfig + static class MetaAndLocalTxConfigClass { + } + + @Transactional("TxConfig") + static class TxConfig { + } + + @Transactional("DerivedTxConfig") + static class DerivedTxConfig extends TxConfig { + } + + @TxInheritedComposed + @TxComposed + static class TxFromMultipleComposedAnnotations { + } + + @Transactional + static interface InterfaceWithInheritedAnnotation { + + @Order + void handleFromInterface(); + } + + static abstract class AbstractClassWithInheritedAnnotation + implements InterfaceWithInheritedAnnotation { + + @Transactional + public abstract void handle(); + + @Transactional + public void handleParameterized(T t) { + } + } + + static class ConcreteClassWithInheritedAnnotation + extends AbstractClassWithInheritedAnnotation { + + @Override + public void handle() { + } + + @Override + public void handleParameterized(String s) { + } + + @Override + public void handleFromInterface() { + } + } + + public interface GenericParameter { + + T getFor(Class cls); + } + + @SuppressWarnings("unused") + private static class StringGenericParameter implements GenericParameter { + + @Order + @Override + public String getFor(Class cls) { + return "foo"; + } + + public String getFor(Integer integer) { + return "foo"; + } + } + + @Transactional + public interface InheritedAnnotationInterface { + } + + public interface SubInheritedAnnotationInterface + extends InheritedAnnotationInterface { + } + + public interface SubSubInheritedAnnotationInterface + extends SubInheritedAnnotationInterface { + } + + @Order + public interface NonInheritedAnnotationInterface { + } + + public interface SubNonInheritedAnnotationInterface + extends NonInheritedAnnotationInterface { + } + + public interface SubSubNonInheritedAnnotationInterface + extends SubNonInheritedAnnotationInterface { + } + + @ConventionBasedComposedContextConfiguration(locations = "explicitDeclaration") + static class ConventionBasedComposedContextConfigurationClass { + } + + @InvalidConventionBasedComposedContextConfiguration(locations = "requiredLocationsDeclaration") + static class InvalidConventionBasedComposedContextConfigurationClass { + } + + @HalfConventionBasedAndHalfAliasedComposedContextConfiguration(xmlConfigFiles = "explicitDeclaration") + static class HalfConventionBasedAndHalfAliasedComposedContextConfigurationClass1 { + } + + @HalfConventionBasedAndHalfAliasedComposedContextConfiguration(locations = "explicitDeclaration") + static class HalfConventionBasedAndHalfAliasedComposedContextConfigurationClass2 { + } + + @AliasedComposedContextConfiguration(xmlConfigFiles = "test.xml") + static class AliasedComposedContextConfigurationClass { + } + + @AliasedValueComposedContextConfiguration(locations = "test.xml") + static class AliasedValueComposedContextConfigurationClass { + } + + @ImplicitAliasesContextConfiguration("foo.xml") + static class ImplicitAliasesContextConfigurationClass1 { + } + + @ImplicitAliasesContextConfiguration(locations = "bar.xml") + static class ImplicitAliasesContextConfigurationClass2 { + } + + @ImplicitAliasesContextConfiguration(xmlFiles = "baz.xml") + static class ImplicitAliasesContextConfigurationClass3 { + } + + @TransitiveImplicitAliasesContextConfiguration(groovy = "test.groovy") + static class TransitiveImplicitAliasesContextConfigurationClass { + } + + @SingleLocationTransitiveImplicitAliasesContextConfiguration(groovy = "test.groovy") + static class SingleLocationTransitiveImplicitAliasesContextConfigurationClass { + } + + @TransitiveImplicitAliasesWithSkippedLevelContextConfiguration(xml = "test.xml") + static class TransitiveImplicitAliasesWithSkippedLevelContextConfigurationClass { + } + + @SingleLocationTransitiveImplicitAliasesWithSkippedLevelContextConfiguration(xml = "test.xml") + static class SingleLocationTransitiveImplicitAliasesWithSkippedLevelContextConfigurationClass { + } + + @ComposedImplicitAliasesContextConfiguration + static class ComposedImplicitAliasesContextConfigurationClass { + } + + @ShadowedAliasComposedContextConfiguration(xmlConfigFiles = "test.xml") + static class ShadowedAliasComposedContextConfigurationClass { + } + + @AliasedComposedContextConfigurationAndTestPropertySource(xmlConfigFiles = "test.xml") + static class AliasedComposedContextConfigurationAndTestPropertySourceClass { + } + + @ComponentScan(value = "com.example.app.test", basePackages = "com.example.app.test") + static class ComponentScanWithBasePackagesAndValueAliasClass { + } + + @TestComponentScan(packages = "com.example.app.test") + static class TestComponentScanClass { + } + + @ConventionBasedSinglePackageComponentScan(basePackages = "com.example.app.test") + static class ConventionBasedSinglePackageComponentScanClass { + } + + @AliasForBasedSinglePackageComponentScan(pkg = "com.example.app.test") + static class AliasForBasedSinglePackageComponentScanClass { + } + + @SpringApplicationConfiguration(Number.class) + static class SpringApplicationConfigurationClass { + } + + @Resource(name = "x") + static class ResourceHolder { + } + + interface TransactionalService { + + @Transactional + void doIt(); + } + + class TransactionalServiceImpl implements TransactionalService { + + @Override + public void doIt() { + } + } + + @Component("meta1") + @Order + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface Meta1 { + } + + @Component("meta2") + @Transactional(readOnly = true) + @Retention(RetentionPolicy.RUNTIME) + @interface Meta2 { + } + + @Meta2 + @Retention(RetentionPolicy.RUNTIME) + @interface MetaMeta { + } + + @MetaMeta + @Retention(RetentionPolicy.RUNTIME) + @interface MetaMetaMeta { + } + + @MetaCycle3 + @Retention(RetentionPolicy.RUNTIME) + @interface MetaCycle1 { + } + + @MetaCycle1 + @Retention(RetentionPolicy.RUNTIME) + @interface MetaCycle2 { + } + + @MetaCycle2 + @Retention(RetentionPolicy.RUNTIME) + @interface MetaCycle3 { + } + + @Meta1 + interface InterfaceWithMetaAnnotation { + } + + @Meta2 + static class ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface + implements InterfaceWithMetaAnnotation { + } + + @Meta1 + static class ClassWithInheritedMetaAnnotation { + } + + @Meta2 + static class SubClassWithInheritedMetaAnnotation + extends ClassWithInheritedMetaAnnotation { + } + + static class SubSubClassWithInheritedMetaAnnotation + extends SubClassWithInheritedMetaAnnotation { + } + + @MetaMeta + static class MetaMetaAnnotatedClass { + } + + @MetaMetaMeta + static class MetaMetaMetaAnnotatedClass { + } + + @MetaCycle3 + static class MetaCycleAnnotatedClass { + } + + interface AnnotatedInterface { + + @Order(0) + void fromInterfaceImplementedByRoot(); + } + + interface NullableAnnotatedInterface { + + @Nullable + void fromInterfaceImplementedByRoot(); + } + + static class Root implements AnnotatedInterface { + + @Order(27) + public void annotatedOnRoot() { + } + + @Meta1 + public void metaAnnotatedOnRoot() { + } + + public void overrideToAnnotate() { + } + + @Order(27) + public void overrideWithoutNewAnnotation() { + } + + public void notAnnotated() { + } + + @Override + public void fromInterfaceImplementedByRoot() { + } + } + + public static class Leaf extends Root { + + @Order(25) + public void annotatedOnLeaf() { + } + + @Meta1 + public void metaAnnotatedOnLeaf() { + } + + @MetaMeta + public void metaMetaAnnotatedOnLeaf() { + } + + @Override + @Order(1) + public void overrideToAnnotate() { + } + + @Override + public void overrideWithoutNewAnnotation() { + } + } + + public static abstract class SimpleGeneric { + + @Order(1) + public abstract void something(T arg); + + } + + public static class TransactionalStringGeneric extends SimpleGeneric { + + @Override + @Transactional + public void something(final String arg) { + } + } + + @Transactional + public static class InheritedAnnotationClass { + } + + public static class SubInheritedAnnotationClass extends InheritedAnnotationClass { + } + + @Order + public static class NonInheritedAnnotationClass { + } + + public static class SubNonInheritedAnnotationClass + extends NonInheritedAnnotationClass { + } + + @Transactional + public static class TransactionalClass { + } + + @Order + public static class TransactionalAndOrderedClass extends TransactionalClass { + } + + public static class SubTransactionalAndOrderedClass + extends TransactionalAndOrderedClass { + } + + public interface InterfaceWithAnnotatedMethod { + + @Order + void foo(); + } + + public static class ImplementsInterfaceWithAnnotatedMethod + implements InterfaceWithAnnotatedMethod { + + @Override + public void foo() { + } + } + + public static class SubOfImplementsInterfaceWithAnnotatedMethod + extends ImplementsInterfaceWithAnnotatedMethod { + + @Override + public void foo() { + } + } + + public abstract static class AbstractDoesNotImplementInterfaceWithAnnotatedMethod + implements InterfaceWithAnnotatedMethod { + } + + public static class SubOfAbstractImplementsInterfaceWithAnnotatedMethod + extends AbstractDoesNotImplementInterfaceWithAnnotatedMethod { + + @Override + public void foo() { + } + } + + public interface InterfaceWithGenericAnnotatedMethod { + + @Order + void foo(T t); + } + + public static class ImplementsInterfaceWithGenericAnnotatedMethod + implements InterfaceWithGenericAnnotatedMethod { + + @Override + public void foo(String t) { + } + } + + public static abstract class BaseClassWithGenericAnnotatedMethod { + + @Order + abstract void foo(T t); + } + + public static class ExtendsBaseClassWithGenericAnnotatedMethod + extends BaseClassWithGenericAnnotatedMethod { + + @Override + public void foo(String t) { + } + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface MyRepeatableContainer { + + MyRepeatable[] value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @Repeatable(MyRepeatableContainer.class) + @interface MyRepeatable { + + String value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @MyRepeatable("meta1") + @interface MyRepeatableMeta1 { + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @MyRepeatable("meta2") + @interface MyRepeatableMeta2 { + } + + interface InterfaceWithRepeated { + + @MyRepeatable("A") + @MyRepeatableContainer({ @MyRepeatable("B"), @MyRepeatable("C") }) + @MyRepeatableMeta1 + void foo(); + } + + @MyRepeatable("A") + @MyRepeatableContainer({ @MyRepeatable("B"), @MyRepeatable("C") }) + @MyRepeatableMeta1 + static class MyRepeatableClass { + } + + static class SubMyRepeatableClass extends MyRepeatableClass { + } + + @MyRepeatable("X") + @MyRepeatableContainer({ @MyRepeatable("Y"), @MyRepeatable("Z") }) + @MyRepeatableMeta2 + static class SubMyRepeatableWithAdditionalLocalDeclarationsClass + extends MyRepeatableClass { + } + + static class SubSubMyRepeatableWithAdditionalLocalDeclarationsClass + extends SubMyRepeatableWithAdditionalLocalDeclarationsClass { + } + + enum RequestMethod { + GET, POST + } + + @Retention(RetentionPolicy.RUNTIME) + @interface RequestMapping { + + String name(); + + @AliasFor("path") + String[] value() default ""; + + @AliasFor(attribute = "value") + String[] path() default ""; + + RequestMethod[] method() default {}; + } + + @Retention(RetentionPolicy.RUNTIME) + @RequestMapping(method = RequestMethod.GET, name = "") + @interface GetMapping { + + @AliasFor(annotation = RequestMapping.class) + String value() default ""; + + @AliasFor(annotation = RequestMapping.class) + String path() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @RequestMapping(method = RequestMethod.POST, name = "") + @interface PostMapping { + + String path() default ""; + } + + @Component("webController") + static class WebController { + + @RequestMapping(value = "/test", name = "foo") + public void handleMappedWithValueAttribute() { + } + + @RequestMapping(path = "/test", name = "bar", method = { RequestMethod.GET, + RequestMethod.POST }) + public void handleMappedWithPathAttribute() { + } + + @GetMapping("/test") + public void getMappedWithValueAttribute() { + } + + @GetMapping(path = "/test") + public void getMappedWithPathAttribute() { + } + + @PostMapping(path = "/test") + public void postMappedWithPathAttribute() { + } + + @RequestMapping(value = "/test", path = "/test", name = "bar", method = { + RequestMethod.GET, RequestMethod.POST }) + public void handleMappedWithSamePathAndValueAttributes() { + } + + @RequestMapping(value = "/enigma", path = "/test", name = "baz") + public void handleMappedWithDifferentPathAndValueAttributes() { + } + } + + /** + * Mimics javax.persistence.Id + */ + @Retention(RUNTIME) + @interface Id { + } + + /** + * Mimics javax.persistence.GeneratedValue + */ + @Retention(RUNTIME) + @interface GeneratedValue { + String strategy(); + } + + @Id + @GeneratedValue(strategy = "AUTO") + private Long getId() { + return 42L; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface TestConfiguration { + + @AliasFor("location") + String value() default ""; + + @AliasFor("value") + String location() default ""; + + Class configClass() default Object.class; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface Hierarchy { + + TestConfiguration[] value(); + } + + @Hierarchy({ @TestConfiguration("A"), @TestConfiguration(location = "B") }) + static class HierarchyClass { + } + + @TestConfiguration("simple.xml") + static class TestConfigurationClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface CharsContainer { + + @AliasFor(attribute = "chars") + char[] value() default {}; + + @AliasFor(attribute = "value") + char[] chars() default {}; + } + + @CharsContainer(chars = { 'x', 'y', 'z' }) + static class GroupOfCharsClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForWithMissingAttributeDeclaration { + + @AliasFor + String foo() default ""; + } + + @AliasForWithMissingAttributeDeclaration + static class AliasForWithMissingAttributeDeclarationClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForWithDuplicateAttributeDeclaration { + + @AliasFor(value = "bar", attribute = "baz") + String foo() default ""; + } + + @AliasForWithDuplicateAttributeDeclaration + static class AliasForWithDuplicateAttributeDeclarationClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForNonexistentAttribute { + + @AliasFor("bar") + String foo() default ""; + } + + @AliasForNonexistentAttribute + static class AliasForNonexistentAttributeClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForWithoutMirroredAliasFor { + + @AliasFor("bar") + String foo() default ""; + + String bar() default ""; + } + + @AliasForWithoutMirroredAliasFor + static class AliasForWithoutMirroredAliasForClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForWithMirroredAliasForWrongAttribute { + + @AliasFor(attribute = "bar") + String[] foo() default ""; + + @AliasFor(attribute = "quux") + String[] bar() default ""; + } + + @AliasForWithMirroredAliasForWrongAttribute + static class AliasForWithMirroredAliasForWrongAttributeClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForAttributeOfDifferentType { + + @AliasFor("bar") + String[] foo() default ""; + + @AliasFor("foo") + boolean bar() default true; + } + + @AliasForAttributeOfDifferentType + static class AliasForAttributeOfDifferentTypeClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForWithMissingDefaultValues { + + @AliasFor(attribute = "bar") + String foo(); + + @AliasFor(attribute = "foo") + String bar(); + } + + @AliasForWithMissingDefaultValues(foo = "foo", bar = "bar") + static class AliasForWithMissingDefaultValuesClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForAttributeWithDifferentDefaultValue { + + @AliasFor("bar") + String foo() default "X"; + + @AliasFor("foo") + String bar() default "Z"; + } + + @AliasForAttributeWithDifferentDefaultValue + static class AliasForAttributeWithDifferentDefaultValueClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasedComposedTestConfigurationNotMetaPresent { + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String xmlConfigFile(); + } + + @AliasedComposedTestConfigurationNotMetaPresent(xmlConfigFile = "test.xml") + static class AliasedComposedTestConfigurationNotMetaPresentClass { + } + + @TestConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface AliasedComposedTestConfiguration { + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String xmlConfigFile(); + } + + @TestConfiguration + @Retention(RetentionPolicy.RUNTIME) + public @interface ImplicitAliasesTestConfiguration { + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String xmlFile() default ""; + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String groovyScript() default ""; + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String value() default ""; + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String location1() default ""; + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String location2() default ""; + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String location3() default ""; + + @AliasFor(annotation = TestConfiguration.class, attribute = "configClass") + Class configClass() default Object.class; + + String nonAliasedAttribute() default ""; + } + + @ImplicitAliasesTestConfiguration(groovyScript = "groovyScript") + static class GroovyImplicitAliasesSimpleTestConfigurationClass { + } + + @ImplicitAliasesTestConfiguration(xmlFile = "xmlFile") + static class XmlImplicitAliasesTestConfigurationClass { + } + + @ImplicitAliasesTestConfiguration("value") + static class ValueImplicitAliasesTestConfigurationClass { + } + + @ImplicitAliasesTestConfiguration(location1 = "location1") + static class Location1ImplicitAliasesTestConfigurationClass { + } + + @ImplicitAliasesTestConfiguration(location2 = "location2") + static class Location2ImplicitAliasesTestConfigurationClass { + } + + @ImplicitAliasesTestConfiguration(location3 = "location3") + static class Location3ImplicitAliasesTestConfigurationClass { + } + + @TestConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface ImplicitAliasesWithImpliedAliasNamesOmittedTestConfiguration { + + @AliasFor(annotation = TestConfiguration.class) + String value() default ""; + + @AliasFor(annotation = TestConfiguration.class) + String location() default ""; + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String xmlFile() default ""; + } + + @ImplicitAliasesWithImpliedAliasNamesOmittedTestConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface TransitiveImplicitAliasesWithImpliedAliasNamesOmittedTestConfiguration { + + @AliasFor(annotation = ImplicitAliasesWithImpliedAliasNamesOmittedTestConfiguration.class, attribute = "xmlFile") + String xml() default ""; + + @AliasFor(annotation = ImplicitAliasesWithImpliedAliasNamesOmittedTestConfiguration.class, attribute = "location") + String groovy() default ""; + } + + @ImplicitAliasesWithImpliedAliasNamesOmittedTestConfiguration("value") + static class ValueImplicitAliasesWithImpliedAliasNamesOmittedTestConfigurationClass { + } + + @ImplicitAliasesWithImpliedAliasNamesOmittedTestConfiguration(location = "location") + static class LocationsImplicitAliasesWithImpliedAliasNamesOmittedTestConfigurationClass { + } + + @ImplicitAliasesWithImpliedAliasNamesOmittedTestConfiguration(xmlFile = "xmlFile") + static class XmlFilesImplicitAliasesWithImpliedAliasNamesOmittedTestConfigurationClass { + } + + @TestConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface ImplicitAliasesWithMissingDefaultValuesTestConfiguration { + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String location1(); + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String location2(); + } + + @ImplicitAliasesWithMissingDefaultValuesTestConfiguration(location1 = "1", location2 = "2") + static class ImplicitAliasesWithMissingDefaultValuesTestConfigurationClass { + } + + @TestConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface ImplicitAliasesWithDifferentDefaultValuesTestConfiguration { + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String location1() default "foo"; + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String location2() default "bar"; + } + + @ImplicitAliasesWithDifferentDefaultValuesTestConfiguration(location1 = "1", location2 = "2") + static class ImplicitAliasesWithDifferentDefaultValuesTestConfigurationClass { + } + + @TestConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface ImplicitAliasesWithDuplicateValuesTestConfiguration { + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String location1() default ""; + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String location2() default ""; + } + + @ImplicitAliasesWithDuplicateValuesTestConfiguration(location1 = "1", location2 = "2") + static class ImplicitAliasesWithDuplicateValuesTestConfigurationClass { + } + + @TestConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface ImplicitAliasesForAliasPairTestConfiguration { + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String xmlFile() default ""; + + @AliasFor(annotation = TestConfiguration.class, value = "value") + String groovyScript() default ""; + } + + @ImplicitAliasesForAliasPairTestConfiguration(xmlFile = "test.xml") + static class ImplicitAliasesForAliasPairTestConfigurationClass { + } + + @ImplicitAliasesTestConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface TransitiveImplicitAliasesTestConfiguration { + + @AliasFor(annotation = ImplicitAliasesTestConfiguration.class, attribute = "xmlFile") + String xml() default ""; + + @AliasFor(annotation = ImplicitAliasesTestConfiguration.class, attribute = "groovyScript") + String groovy() default ""; + } + + @TransitiveImplicitAliasesTestConfiguration(xml = "test.xml") + static class TransitiveImplicitAliasesTestConfigurationClass { + } + + @ImplicitAliasesForAliasPairTestConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface TransitiveImplicitAliasesForAliasPairTestConfiguration { + + @AliasFor(annotation = ImplicitAliasesForAliasPairTestConfiguration.class, attribute = "xmlFile") + String xml() default ""; + + @AliasFor(annotation = ImplicitAliasesForAliasPairTestConfiguration.class, attribute = "groovyScript") + String groovy() default ""; + } + + @TransitiveImplicitAliasesForAliasPairTestConfiguration(xml = "test.xml") + static class TransitiveImplicitAliasesForAliasPairTestConfigurationClass { + } + + @ComponentScan(excludeFilters = { @Filter(pattern = "*Foo"), + @Filter(pattern = "*Bar") }) + static class ComponentScanClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ComponentScanSingleFilter { + + Filter value(); + } + + @ComponentScanSingleFilter(@Filter(pattern = "*Foo")) + static class ComponentScanSingleFilterClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AnnotationWithDefaults { + + String text() default "enigma"; + + boolean predicate() default true; + + char[] characters() default { 'a', 'b', 'c' }; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AnnotationWithoutDefaults { + + String text(); + } + + @TestConfiguration(value = "foo", location = "bar") + interface TestConfigurationMismatch { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface DefaultOverrideRoot { + + String text() default "root"; + + } + + @Retention(RetentionPolicy.RUNTIME) + @DefaultOverrideRoot + @interface DefaultOverrideMeta { + + } + + @Retention(RetentionPolicy.RUNTIME) + @DefaultOverrideMeta + @interface DefaultOverrideMetaMeta { + + String text() default "metameta"; + + } + + @Retention(RetentionPolicy.RUNTIME) + @DefaultOverrideMetaMeta + @interface DefaultOverrideMetaMetaMeta { + + } + + @DefaultOverrideMetaMetaMeta + static class DefaultOverrideClass { + + } + + @Retention(RetentionPolicy.RUNTIME) + @DefaultOverrideRoot + @interface DefaultOverrideImplicitAlias { + + @AliasFor(annotation=DefaultOverrideRoot.class, attribute="text") + String text1() default "alias"; + + @AliasFor(annotation=DefaultOverrideRoot.class, attribute="text") + String text2() default "alias"; + + } + + @Retention(RetentionPolicy.RUNTIME) + @DefaultOverrideImplicitAlias(text1="alias-meta-1") + @interface DefaultOverrideAliasImplicitMeta1 { + + } + + @Retention(RetentionPolicy.RUNTIME) + @DefaultOverrideImplicitAlias(text2="alias-meta-2") + @interface DefaultOverrideImplicitAliasMeta2 { + + } + + @DefaultOverrideAliasImplicitMeta1 + static class DefaultOverrideImplicitAliasMetaClass1 { + + } + + @DefaultOverrideImplicitAliasMeta2 + static class DefaultOverrideImplicitAliasMetaClass2 { + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface DefaultOverrideExplicitAliasRoot { + + @AliasFor("value") + String text() default ""; + + @AliasFor("text") + String value() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + @DefaultOverrideExplicitAliasRoot("meta") + @interface DefaultOverrideExplicitAliasRootMeta { + + } + + @Retention(RetentionPolicy.RUNTIME) + @DefaultOverrideExplicitAliasRootMeta + @interface DefaultOverrideExplicitAliasRootMetaMeta { + + } + + @DefaultOverrideExplicitAliasRootMetaMeta + static class DefaultOverrideExplicitAliasRootMetaMetaClass { + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ValueAttribute { + + String[] value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @ValueAttribute("FromValueAttributeMeta") + @interface ValueAttributeMeta { + + String[] value() default {}; + + } + + @Retention(RetentionPolicy.RUNTIME) + @ValueAttributeMeta("FromValueAttributeMetaMeta") + @interface ValueAttributeMetaMeta { + + } + + @ValueAttributeMetaMeta + static class ValueAttributeMetaMetaClass { + + } + // @formatter:on + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MissingMergedAnnotationTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MissingMergedAnnotationTests.java new file mode 100644 index 0000000..3835369 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/MissingMergedAnnotationTests.java @@ -0,0 +1,313 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Collections; +import java.util.Map; +import java.util.NoSuchElementException; + +import org.assertj.core.api.ThrowableTypeAssert; +import org.junit.jupiter.api.Test; + +import org.springframework.util.ConcurrentReferenceHashMap; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link MissingMergedAnnotation}. + * + * @author Phillip Webb + */ +class MissingMergedAnnotationTests { + + private final MergedAnnotation missing = MissingMergedAnnotation.getInstance(); + + + @Test + void getTypeThrowsNoSuchElementException() { + assertThatNoSuchElementException().isThrownBy(this.missing::getType); + } + + @Test + void metaTypesReturnsEmptyList() { + assertThat(this.missing.getMetaTypes()).isEmpty(); + } + + @Test + void isPresentReturnsFalse() { + assertThat(this.missing.isPresent()).isFalse(); + } + + @Test + void isDirectlyPresentReturnsFalse() { + assertThat(this.missing.isDirectlyPresent()).isFalse(); + } + + @Test + void isMetaPresentReturnsFalse() { + assertThat(this.missing.isMetaPresent()).isFalse(); + } + + @Test + void getDistanceReturnsMinusOne() { + assertThat(this.missing.getDistance()).isEqualTo(-1); + } + + @Test + void getAggregateIndexReturnsMinusOne() { + assertThat(this.missing.getAggregateIndex()).isEqualTo(-1); + } + + @Test + void getSourceReturnsNull() { + assertThat(this.missing.getSource()).isNull(); + } + + @Test + void getMetaSourceReturnsNull() { + assertThat(this.missing.getMetaSource()).isNull(); + } + + @Test + void getRootReturnsEmptyAnnotation() { + assertThat(this.missing.getRoot()).isSameAs(this.missing); + } + + @Test + void hasNonDefaultValueThrowsNoSuchElementException() { + assertThatNoSuchElementException().isThrownBy( + () -> this.missing.hasNonDefaultValue("value")); + } + + @Test + void hasDefaultValueThrowsNoSuchElementException() { + assertThatNoSuchElementException().isThrownBy( + () -> this.missing.hasDefaultValue("value")); + } + + @Test + void getByteThrowsNoSuchElementException() { + assertThatNoSuchElementException().isThrownBy( + () -> this.missing.getByte("value")); + } + + @Test + void getByteArrayThrowsNoSuchElementException() { + assertThatNoSuchElementException().isThrownBy( + () -> this.missing.getByteArray("value")); + } + + @Test + void getBooleanThrowsNoSuchElementException() { + assertThatNoSuchElementException().isThrownBy( + () -> this.missing.getBoolean("value")); + } + + @Test + void getBooleanArrayThrowsNoSuchElementException() { + assertThatNoSuchElementException().isThrownBy( + () -> this.missing.getBooleanArray("value")); + } + + @Test + void getCharThrowsNoSuchElementException() { + assertThatNoSuchElementException().isThrownBy( + () -> this.missing.getChar("value")); + } + + @Test + void getCharArrayThrowsNoSuchElementException() { + assertThatNoSuchElementException().isThrownBy( + () -> this.missing.getCharArray("value")); + } + + @Test + void getShortThrowsNoSuchElementException() { + assertThatNoSuchElementException().isThrownBy( + () -> this.missing.getShort("value")); + } + + @Test + void getShortArrayThrowsNoSuchElementException() { + assertThatNoSuchElementException().isThrownBy( + () -> this.missing.getShortArray("value")); + } + + @Test + void getIntThrowsNoSuchElementException() { + assertThatNoSuchElementException().isThrownBy(() -> this.missing.getInt("value")); + } + + @Test + void getIntArrayThrowsNoSuchElementException() { + assertThatNoSuchElementException().isThrownBy( + () -> this.missing.getIntArray("value")); + } + + @Test + void getLongThrowsNoSuchElementException() { + assertThatNoSuchElementException().isThrownBy( + () -> this.missing.getLong("value")); + } + + @Test + void getLongArrayThrowsNoSuchElementException() { + assertThatNoSuchElementException().isThrownBy( + () -> this.missing.getLongArray("value")); + } + + @Test + void getDoubleThrowsNoSuchElementException() { + assertThatNoSuchElementException().isThrownBy( + () -> this.missing.getDouble("value")); + } + + @Test + void getDoubleArrayThrowsNoSuchElementException() { + assertThatNoSuchElementException().isThrownBy( + () -> this.missing.getDoubleArray("value")); + } + + @Test + void getFloatThrowsNoSuchElementException() { + assertThatNoSuchElementException().isThrownBy( + () -> this.missing.getFloat("value")); + } + + @Test + void getFloatArrayThrowsNoSuchElementException() { + assertThatNoSuchElementException().isThrownBy( + () -> this.missing.getFloatArray("value")); + } + + @Test + void getStringThrowsNoSuchElementException() { + assertThatNoSuchElementException().isThrownBy( + () -> this.missing.getString("value")); + } + + @Test + void getStringArrayThrowsNoSuchElementException() { + assertThatNoSuchElementException().isThrownBy( + () -> this.missing.getStringArray("value")); + } + + @Test + void getClassThrowsNoSuchElementException() { + assertThatNoSuchElementException().isThrownBy( + () -> this.missing.getClass("value")); + } + + @Test + void getClassArrayThrowsNoSuchElementException() { + assertThatNoSuchElementException().isThrownBy( + () -> this.missing.getClassArray("value")); + } + + @Test + void getEnumThrowsNoSuchElementException() { + assertThatNoSuchElementException().isThrownBy( + () -> this.missing.getEnum("value", TestEnum.class)); + } + + @Test + void getEnumArrayThrowsNoSuchElementException() { + assertThatNoSuchElementException().isThrownBy( + () -> this.missing.getEnumArray("value", TestEnum.class)); + } + + @Test + void getAnnotationThrowsNoSuchElementException() { + assertThatNoSuchElementException().isThrownBy( + () -> this.missing.getAnnotation("value", TestAnnotation.class)); + } + + @Test + void getAnnotationArrayThrowsNoSuchElementException() { + assertThatNoSuchElementException().isThrownBy( + () -> this.missing.getAnnotationArray("value", TestAnnotation.class)); + } + + @Test + void getValueReturnsEmpty() { + assertThat(this.missing.getValue("value", Integer.class)).isEmpty(); + } + + @Test + void getDefaultValueReturnsEmpty() { + assertThat(this.missing.getDefaultValue("value", Integer.class)).isEmpty(); + } + + @Test + void synthesizeThrowsNoSuchElementException() { + assertThatNoSuchElementException().isThrownBy(() -> this.missing.synthesize()); + } + + @Test + void synthesizeWithPredicateWhenPredicateMatchesThrowsNoSuchElementException() { + assertThatNoSuchElementException().isThrownBy( + () -> this.missing.synthesize(annotation -> true)); + } + + @Test + void synthesizeWithPredicateWhenPredicateDoesNotMatchReturnsEmpty() { + assertThat(this.missing.synthesize(annotation -> false)).isEmpty(); + } + + @Test + void toStringReturnsString() { + assertThat(this.missing.toString()).isEqualTo("(missing)"); + } + + @Test + void asAnnotationAttributesReturnsNewAnnotationAttributes() { + AnnotationAttributes attributes = this.missing.asAnnotationAttributes(); + assertThat(attributes).isEmpty(); + assertThat(this.missing.asAnnotationAttributes()).isNotSameAs(attributes); + } + + @Test + void asMapReturnsEmptyMap() { + Map map = this.missing.asMap(); + assertThat(map).isSameAs(Collections.EMPTY_MAP); + } + + @Test + void asMapWithFactoryReturnsNewMapFromFactory() { + Map map = this.missing.asMap(annotation->new ConcurrentReferenceHashMap<>()); + assertThat(map).isInstanceOf(ConcurrentReferenceHashMap.class); + } + + + private static ThrowableTypeAssert assertThatNoSuchElementException() { + return assertThatExceptionOfType(NoSuchElementException.class); + } + + + private enum TestEnum { + ONE, TWO, THREE + } + + + @Retention(RetentionPolicy.RUNTIME) + private @interface TestAnnotation { + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MultipleComposedAnnotationsOnSingleAnnotatedElementTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MultipleComposedAnnotationsOnSingleAnnotatedElementTests.java new file mode 100644 index 0000000..b201008 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/MultipleComposedAnnotationsOnSingleAnnotatedElementTests.java @@ -0,0 +1,370 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.Iterator; +import java.util.Set; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.core.annotation.AnnotatedElementUtils.findAllMergedAnnotations; +import static org.springframework.core.annotation.AnnotatedElementUtils.getAllMergedAnnotations; + +/** + * Unit tests that verify support for finding multiple composed annotations on + * a single annotated element. + * + *

    See SPR-13486. + * + * @author Sam Brannen + * @since 4.3 + * @see AnnotatedElementUtils + * @see AnnotatedElementUtilsTests + * @see ComposedRepeatableAnnotationsTests + */ +class MultipleComposedAnnotationsOnSingleAnnotatedElementTests { + + @Test + void getMultipleComposedAnnotationsOnClass() { + assertGetAllMergedAnnotationsBehavior(MultipleComposedCachesClass.class); + } + + @Test + void getMultipleInheritedComposedAnnotationsOnSuperclass() { + assertGetAllMergedAnnotationsBehavior(SubMultipleComposedCachesClass.class); + } + + @Test + void getMultipleNoninheritedComposedAnnotationsOnClass() { + Class element = MultipleNoninheritedComposedCachesClass.class; + Set cacheables = getAllMergedAnnotations(element, Cacheable.class); + assertThat(cacheables).isNotNull(); + assertThat(cacheables.size()).isEqualTo(2); + + Iterator iterator = cacheables.iterator(); + Cacheable cacheable1 = iterator.next(); + Cacheable cacheable2 = iterator.next(); + assertThat(cacheable1.value()).isEqualTo("noninheritedCache1"); + assertThat(cacheable2.value()).isEqualTo("noninheritedCache2"); + } + + @Test + void getMultipleNoninheritedComposedAnnotationsOnSuperclass() { + Class element = SubMultipleNoninheritedComposedCachesClass.class; + Set cacheables = getAllMergedAnnotations(element, Cacheable.class); + assertThat(cacheables).isNotNull(); + assertThat(cacheables.size()).isEqualTo(0); + } + + @Test + void getComposedPlusLocalAnnotationsOnClass() { + assertGetAllMergedAnnotationsBehavior(ComposedPlusLocalCachesClass.class); + } + + @Test + void getMultipleComposedAnnotationsOnInterface() { + Class element = MultipleComposedCachesOnInterfaceClass.class; + Set cacheables = getAllMergedAnnotations(element, Cacheable.class); + assertThat(cacheables).isNotNull(); + assertThat(cacheables.size()).isEqualTo(0); + } + + @Test + void getMultipleComposedAnnotationsOnMethod() throws Exception { + AnnotatedElement element = getClass().getDeclaredMethod("multipleComposedCachesMethod"); + assertGetAllMergedAnnotationsBehavior(element); + } + + @Test + void getComposedPlusLocalAnnotationsOnMethod() throws Exception { + AnnotatedElement element = getClass().getDeclaredMethod("composedPlusLocalCachesMethod"); + assertGetAllMergedAnnotationsBehavior(element); + } + + @Test + @Disabled("Disabled since some Java 8 updates handle the bridge method differently") + void getMultipleComposedAnnotationsOnBridgeMethod() throws Exception { + Set cacheables = getAllMergedAnnotations(getBridgeMethod(), Cacheable.class); + assertThat(cacheables).isNotNull(); + assertThat(cacheables.size()).isEqualTo(0); + } + + @Test + void findMultipleComposedAnnotationsOnClass() { + assertFindAllMergedAnnotationsBehavior(MultipleComposedCachesClass.class); + } + + @Test + void findMultipleInheritedComposedAnnotationsOnSuperclass() { + assertFindAllMergedAnnotationsBehavior(SubMultipleComposedCachesClass.class); + } + + @Test + void findMultipleNoninheritedComposedAnnotationsOnClass() { + Class element = MultipleNoninheritedComposedCachesClass.class; + Set cacheables = findAllMergedAnnotations(element, Cacheable.class); + assertThat(cacheables).isNotNull(); + assertThat(cacheables.size()).isEqualTo(2); + + Iterator iterator = cacheables.iterator(); + Cacheable cacheable1 = iterator.next(); + Cacheable cacheable2 = iterator.next(); + assertThat(cacheable1.value()).isEqualTo("noninheritedCache1"); + assertThat(cacheable2.value()).isEqualTo("noninheritedCache2"); + } + + @Test + void findMultipleNoninheritedComposedAnnotationsOnSuperclass() { + Class element = SubMultipleNoninheritedComposedCachesClass.class; + Set cacheables = findAllMergedAnnotations(element, Cacheable.class); + assertThat(cacheables).isNotNull(); + assertThat(cacheables.size()).isEqualTo(2); + + Iterator iterator = cacheables.iterator(); + Cacheable cacheable1 = iterator.next(); + Cacheable cacheable2 = iterator.next(); + assertThat(cacheable1.value()).isEqualTo("noninheritedCache1"); + assertThat(cacheable2.value()).isEqualTo("noninheritedCache2"); + } + + @Test + void findComposedPlusLocalAnnotationsOnClass() { + assertFindAllMergedAnnotationsBehavior(ComposedPlusLocalCachesClass.class); + } + + @Test + void findMultipleComposedAnnotationsOnInterface() { + assertFindAllMergedAnnotationsBehavior(MultipleComposedCachesOnInterfaceClass.class); + } + + @Test + void findComposedCacheOnInterfaceAndLocalCacheOnClass() { + assertFindAllMergedAnnotationsBehavior(ComposedCacheOnInterfaceAndLocalCacheClass.class); + } + + @Test + void findMultipleComposedAnnotationsOnMethod() throws Exception { + AnnotatedElement element = getClass().getDeclaredMethod("multipleComposedCachesMethod"); + assertFindAllMergedAnnotationsBehavior(element); + } + + @Test + void findComposedPlusLocalAnnotationsOnMethod() throws Exception { + AnnotatedElement element = getClass().getDeclaredMethod("composedPlusLocalCachesMethod"); + assertFindAllMergedAnnotationsBehavior(element); + } + + @Test + void findMultipleComposedAnnotationsOnBridgeMethod() throws Exception { + assertFindAllMergedAnnotationsBehavior(getBridgeMethod()); + } + + /** + * Bridge/bridged method setup code copied from + * {@link org.springframework.core.BridgeMethodResolverTests#withGenericParameter()}. + */ + Method getBridgeMethod() throws NoSuchMethodException { + Method[] methods = StringGenericParameter.class.getMethods(); + Method bridgeMethod = null; + Method bridgedMethod = null; + + for (Method method : methods) { + if ("getFor".equals(method.getName()) && !method.getParameterTypes()[0].equals(Integer.class)) { + if (method.getReturnType().equals(Object.class)) { + bridgeMethod = method; + } + else { + bridgedMethod = method; + } + } + } + assertThat(bridgeMethod != null && bridgeMethod.isBridge()).isTrue(); + boolean condition = bridgedMethod != null && !bridgedMethod.isBridge(); + assertThat(condition).isTrue(); + + return bridgeMethod; + } + + private void assertGetAllMergedAnnotationsBehavior(AnnotatedElement element) { + assertThat(element).isNotNull(); + + Set cacheables = getAllMergedAnnotations(element, Cacheable.class); + assertThat(cacheables).isNotNull(); + assertThat(cacheables.size()).isEqualTo(2); + + Iterator iterator = cacheables.iterator(); + Cacheable fooCacheable = iterator.next(); + Cacheable barCacheable = iterator.next(); + assertThat(fooCacheable.key()).isEqualTo("fooKey"); + assertThat(fooCacheable.value()).isEqualTo("fooCache"); + assertThat(barCacheable.key()).isEqualTo("barKey"); + assertThat(barCacheable.value()).isEqualTo("barCache"); + } + + private void assertFindAllMergedAnnotationsBehavior(AnnotatedElement element) { + assertThat(element).isNotNull(); + + Set cacheables = findAllMergedAnnotations(element, Cacheable.class); + assertThat(cacheables).isNotNull(); + assertThat(cacheables.size()).isEqualTo(2); + + Iterator iterator = cacheables.iterator(); + Cacheable fooCacheable = iterator.next(); + Cacheable barCacheable = iterator.next(); + assertThat(fooCacheable.key()).isEqualTo("fooKey"); + assertThat(fooCacheable.value()).isEqualTo("fooCache"); + assertThat(barCacheable.key()).isEqualTo("barKey"); + assertThat(barCacheable.value()).isEqualTo("barCache"); + } + + + // ------------------------------------------------------------------------- + + /** + * Mock of {@code org.springframework.cache.annotation.Cacheable}. + */ + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface Cacheable { + + @AliasFor("cacheName") + String value() default ""; + + @AliasFor("value") + String cacheName() default ""; + + String key() default ""; + } + + @Cacheable("fooCache") + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface FooCache { + + @AliasFor(annotation = Cacheable.class) + String key() default ""; + } + + @Cacheable("barCache") + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface BarCache { + + @AliasFor(annotation = Cacheable.class) + String key(); + } + + @Cacheable("noninheritedCache1") + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @interface NoninheritedCache1 { + + @AliasFor(annotation = Cacheable.class) + String key() default ""; + } + + @Cacheable("noninheritedCache2") + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @interface NoninheritedCache2 { + + @AliasFor(annotation = Cacheable.class) + String key() default ""; + } + + @FooCache(key = "fooKey") + @BarCache(key = "barKey") + private static class MultipleComposedCachesClass { + } + + private static class SubMultipleComposedCachesClass extends MultipleComposedCachesClass { + } + + @NoninheritedCache1 + @NoninheritedCache2 + private static class MultipleNoninheritedComposedCachesClass { + } + + private static class SubMultipleNoninheritedComposedCachesClass extends MultipleNoninheritedComposedCachesClass { + } + + @Cacheable(cacheName = "fooCache", key = "fooKey") + @BarCache(key = "barKey") + private static class ComposedPlusLocalCachesClass { + } + + @FooCache(key = "fooKey") + @BarCache(key = "barKey") + private interface MultipleComposedCachesInterface { + } + + private static class MultipleComposedCachesOnInterfaceClass implements MultipleComposedCachesInterface { + } + + @Cacheable(cacheName = "fooCache", key = "fooKey") + private interface ComposedCacheInterface { + } + + @BarCache(key = "barKey") + private static class ComposedCacheOnInterfaceAndLocalCacheClass implements ComposedCacheInterface { + } + + + @FooCache(key = "fooKey") + @BarCache(key = "barKey") + private void multipleComposedCachesMethod() { + } + + @Cacheable(cacheName = "fooCache", key = "fooKey") + @BarCache(key = "barKey") + private void composedPlusLocalCachesMethod() { + } + + + public interface GenericParameter { + + T getFor(Class cls); + } + + @SuppressWarnings("unused") + private static class StringGenericParameter implements GenericParameter { + + @FooCache(key = "fooKey") + @BarCache(key = "barKey") + @Override + public String getFor(Class cls) { + return "foo"; + } + + public String getFor(Integer integer) { + return "foo"; + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/OrderSourceProviderTests.java b/spring-core/src/test/java/org/springframework/core/annotation/OrderSourceProviderTests.java new file mode 100644 index 0000000..e4912c8 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/OrderSourceProviderTests.java @@ -0,0 +1,187 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.Ordered; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Stephane Nicoll + * @author Juergen Hoeller + */ +class OrderSourceProviderTests { + + private final AnnotationAwareOrderComparator comparator = AnnotationAwareOrderComparator.INSTANCE; + + + @Test + void plainComparator() { + List items = new ArrayList<>(); + C c = new C(5); + C c2 = new C(-5); + items.add(c); + items.add(c2); + items.sort(comparator); + assertOrder(items, c2, c); + } + + @Test + void listNoFactoryMethod() { + A a = new A(); + C c = new C(-50); + B b = new B(); + + List items = Arrays.asList(a, c, b); + items.sort(comparator.withSourceProvider(obj -> null)); + assertOrder(items, c, a, b); + } + + @Test + void listFactoryMethod() { + A a = new A(); + C c = new C(3); + B b = new B(); + + List items = Arrays.asList(a, c, b); + items.sort(comparator.withSourceProvider(obj -> { + if (obj == a) { + return new C(4); + } + if (obj == b) { + return new C(2); + } + return null; + })); + assertOrder(items, b, c, a); + } + + @Test + void listFactoryMethodOverridesStaticOrder() { + A a = new A(); + C c = new C(5); + C c2 = new C(-5); + + List items = Arrays.asList(a, c, c2); + items.sort(comparator.withSourceProvider(obj -> { + if (obj == a) { + return 4; + } + if (obj == c2) { + return 2; + } + return null; + })); + assertOrder(items, c2, a, c); + } + + @Test + void arrayNoFactoryMethod() { + A a = new A(); + C c = new C(-50); + B b = new B(); + + Object[] items = new Object[] {a, c, b}; + Arrays.sort(items, comparator.withSourceProvider(obj -> null)); + assertOrder(items, c, a, b); + } + + @Test + void arrayFactoryMethod() { + A a = new A(); + C c = new C(3); + B b = new B(); + + Object[] items = new Object[] {a, c, b}; + Arrays.sort(items, comparator.withSourceProvider(obj -> { + if (obj == a) { + return new C(4); + } + if (obj == b) { + return new C(2); + } + return null; + })); + assertOrder(items, b, c, a); + } + + @Test + void arrayFactoryMethodOverridesStaticOrder() { + A a = new A(); + C c = new C(5); + C c2 = new C(-5); + + Object[] items = new Object[] {a, c, c2}; + Arrays.sort(items, comparator.withSourceProvider(obj -> { + if (obj == a) { + return 4; + } + if (obj == c2) { + return 2; + } + return null; + })); + assertOrder(items, c2, a, c); + } + + + private void assertOrder(List actual, Object... expected) { + for (int i = 0; i < actual.size(); i++) { + assertThat(actual.get(i)).as("Wrong instance at index '" + i + "'").isSameAs(expected[i]); + } + assertThat(actual.size()).as("Wrong number of items").isEqualTo(expected.length); + } + + private void assertOrder(Object[] actual, Object... expected) { + for (int i = 0; i < actual.length; i++) { + assertThat(actual[i]).as("Wrong instance at index '" + i + "'").isSameAs(expected[i]); + } + assertThat(expected.length).as("Wrong number of items").isEqualTo(expected.length); + } + + + @Order(1) + private static class A { + } + + + @Order(2) + private static class B { + } + + + private static class C implements Ordered { + + private final int order; + + private C(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return order; + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/OrderUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/OrderUtilsTests.java new file mode 100644 index 0000000..08d980b --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/OrderUtilsTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import javax.annotation.Priority; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Stephane Nicoll + * @author Juergen Hoeller + */ +class OrderUtilsTests { + + @Test + void getSimpleOrder() { + assertThat(OrderUtils.getOrder(SimpleOrder.class, null)).isEqualTo(Integer.valueOf(50)); + assertThat(OrderUtils.getOrder(SimpleOrder.class, null)).isEqualTo(Integer.valueOf(50)); + } + + @Test + void getPriorityOrder() { + assertThat(OrderUtils.getOrder(SimplePriority.class, null)).isEqualTo(Integer.valueOf(55)); + assertThat(OrderUtils.getOrder(SimplePriority.class, null)).isEqualTo(Integer.valueOf(55)); + } + + @Test + void getOrderWithBoth() { + assertThat(OrderUtils.getOrder(OrderAndPriority.class, null)).isEqualTo(Integer.valueOf(50)); + assertThat(OrderUtils.getOrder(OrderAndPriority.class, null)).isEqualTo(Integer.valueOf(50)); + } + + @Test + void getDefaultOrder() { + assertThat(OrderUtils.getOrder(NoOrder.class, 33)).isEqualTo(33); + assertThat(OrderUtils.getOrder(NoOrder.class, 33)).isEqualTo(33); + } + + @Test + void getPriorityValueNoAnnotation() { + assertThat(OrderUtils.getPriority(SimpleOrder.class)).isNull(); + assertThat(OrderUtils.getPriority(SimpleOrder.class)).isNull(); + } + + @Test + void getPriorityValue() { + assertThat(OrderUtils.getPriority(OrderAndPriority.class)).isEqualTo(Integer.valueOf(55)); + assertThat(OrderUtils.getPriority(OrderAndPriority.class)).isEqualTo(Integer.valueOf(55)); + } + + + @Order(50) + private static class SimpleOrder {} + + @Priority(55) + private static class SimplePriority {} + + @Order(50) + @Priority(55) + private static class OrderAndPriority {} + + private static class NoOrder {} + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/PackagesAnnotationFilterTests.java b/spring-core/src/test/java/org/springframework/core/annotation/PackagesAnnotationFilterTests.java new file mode 100644 index 0000000..12c5c87 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/PackagesAnnotationFilterTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link PackagesAnnotationFilter}. + * + * @author Phillip Webb + */ +class PackagesAnnotationFilterTests { + + @Test + void createWhenPackagesIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> + new PackagesAnnotationFilter((String[]) null)) + .withMessage("Packages array must not be null"); + } + + @Test + void createWhenPackagesContainsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> + new PackagesAnnotationFilter((String) null)) + .withMessage("Packages array must not have empty elements"); + } + + @Test + void createWhenPackagesContainsEmptyTextThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> + new PackagesAnnotationFilter("")) + .withMessage("Packages array must not have empty elements"); + } + + @Test + void matchesWhenInPackageReturnsTrue() { + PackagesAnnotationFilter filter = new PackagesAnnotationFilter("com.example"); + assertThat(filter.matches("com.example.Component")).isTrue(); + } + + @Test + void matchesWhenNotInPackageReturnsFalse() { + PackagesAnnotationFilter filter = new PackagesAnnotationFilter("com.example"); + assertThat(filter.matches("org.springframework.sterotype.Component")).isFalse(); + } + + @Test + void matchesWhenInSimilarPackageReturnsFalse() { + PackagesAnnotationFilter filter = new PackagesAnnotationFilter("com.example"); + assertThat(filter.matches("com.examples.Component")).isFalse(); + } + + @Test + void equalsAndHashCode() { + PackagesAnnotationFilter filter1 = new PackagesAnnotationFilter("com.example", + "org.springframework"); + PackagesAnnotationFilter filter2 = new PackagesAnnotationFilter( + "org.springframework", "com.example"); + PackagesAnnotationFilter filter3 = new PackagesAnnotationFilter("com.examples"); + assertThat(filter1.hashCode()).isEqualTo(filter2.hashCode()); + assertThat(filter1).isEqualTo(filter1).isEqualTo(filter2).isNotEqualTo(filter3); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/RepeatableContainersTests.java b/spring-core/src/test/java/org/springframework/core/annotation/RepeatableContainersTests.java new file mode 100644 index 0000000..29aeb70 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/RepeatableContainersTests.java @@ -0,0 +1,284 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link RepeatableContainers}. + * + * @author Phillip Webb + */ +class RepeatableContainersTests { + + @Test + void standardRepeatablesWhenNonRepeatableReturnsNull() { + Object[] values = findRepeatedAnnotationValues( + RepeatableContainers.standardRepeatables(), WithNonRepeatable.class, + NonRepeatable.class); + assertThat(values).isNull(); + } + + @Test + void standardRepeatablesWhenSingleReturnsNull() { + Object[] values = findRepeatedAnnotationValues( + RepeatableContainers.standardRepeatables(), + WithSingleStandardRepeatable.class, StandardRepeatable.class); + assertThat(values).isNull(); + } + + @Test + void standardRepeatablesWhenContainerReturnsRepeats() { + Object[] values = findRepeatedAnnotationValues( + RepeatableContainers.standardRepeatables(), WithStandardRepeatables.class, + StandardContainer.class); + assertThat(values).containsExactly("a", "b"); + } + + @Test + void standardRepeatablesWhenContainerButNotRepeatableReturnsNull() { + Object[] values = findRepeatedAnnotationValues( + RepeatableContainers.standardRepeatables(), WithExplicitRepeatables.class, + ExplicitContainer.class); + assertThat(values).isNull(); + } + + @Test + void ofExplicitWhenNonRepeatableReturnsNull() { + Object[] values = findRepeatedAnnotationValues( + RepeatableContainers.of(ExplicitRepeatable.class, + ExplicitContainer.class), + WithNonRepeatable.class, NonRepeatable.class); + assertThat(values).isNull(); + } + + @Test + void ofExplicitWhenStandardRepeatableContainerReturnsNull() { + Object[] values = findRepeatedAnnotationValues( + RepeatableContainers.of(ExplicitRepeatable.class, + ExplicitContainer.class), + WithStandardRepeatables.class, StandardContainer.class); + assertThat(values).isNull(); + } + + @Test + void ofExplicitWhenContainerReturnsRepeats() { + Object[] values = findRepeatedAnnotationValues( + RepeatableContainers.of(ExplicitRepeatable.class, + ExplicitContainer.class), + WithExplicitRepeatables.class, ExplicitContainer.class); + assertThat(values).containsExactly("a", "b"); + } + + @Test + void ofExplicitWhenHasNoValueThrowsException() { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + RepeatableContainers.of(ExplicitRepeatable.class, InvalidNoValue.class)) + .withMessageContaining("Invalid declaration of container type [" + + InvalidNoValue.class.getName() + + "] for repeatable annotation [" + + ExplicitRepeatable.class.getName() + "]"); + } + + @Test + void ofExplicitWhenValueIsNotArrayThrowsException() { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + RepeatableContainers.of(ExplicitRepeatable.class, InvalidNotArray.class)) + .withMessage("Container type [" + + InvalidNotArray.class.getName() + + "] must declare a 'value' attribute for an array of type [" + + ExplicitRepeatable.class.getName() + "]"); + } + + @Test + void ofExplicitWhenValueIsArrayOfWrongTypeThrowsException() { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> + RepeatableContainers.of(ExplicitRepeatable.class, InvalidWrongArrayType.class)) + .withMessage("Container type [" + + InvalidWrongArrayType.class.getName() + + "] must declare a 'value' attribute for an array of type [" + + ExplicitRepeatable.class.getName() + "]"); + } + + @Test + void ofExplicitWhenAnnotationIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> + RepeatableContainers.of(null, null)) + .withMessage("Repeatable must not be null"); + } + + @Test + void ofExplicitWhenContainerIsNullDeducesContainer() { + Object[] values = findRepeatedAnnotationValues( + RepeatableContainers.of(StandardRepeatable.class, null), + WithStandardRepeatables.class, StandardContainer.class); + assertThat(values).containsExactly("a", "b"); + } + + @Test + void ofExplicitWhenContainerIsNullAndNotRepeatableThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> + RepeatableContainers.of(ExplicitRepeatable.class, null)) + .withMessage("Annotation type must be a repeatable annotation: " + + "failed to resolve container type for " + + ExplicitRepeatable.class.getName()); + } + + @Test + void standardAndExplicitReturnsRepeats() { + RepeatableContainers repeatableContainers = RepeatableContainers.standardRepeatables().and( + ExplicitContainer.class, ExplicitRepeatable.class); + assertThat(findRepeatedAnnotationValues(repeatableContainers, + WithStandardRepeatables.class, StandardContainer.class)).containsExactly( + "a", "b"); + assertThat(findRepeatedAnnotationValues(repeatableContainers, + WithExplicitRepeatables.class, ExplicitContainer.class)).containsExactly( + "a", "b"); + } + + @Test + void noneAlwaysReturnsNull() { + Object[] values = findRepeatedAnnotationValues( + RepeatableContainers.none(), WithStandardRepeatables.class, + StandardContainer.class); + assertThat(values).isNull(); + } + + @Test + void equalsAndHashcode() { + RepeatableContainers c1 = RepeatableContainers.of(ExplicitRepeatable.class, + ExplicitContainer.class); + RepeatableContainers c2 = RepeatableContainers.of(ExplicitRepeatable.class, + ExplicitContainer.class); + RepeatableContainers c3 = RepeatableContainers.standardRepeatables(); + RepeatableContainers c4 = RepeatableContainers.standardRepeatables().and( + ExplicitContainer.class, ExplicitRepeatable.class); + assertThat(c1.hashCode()).isEqualTo(c2.hashCode()); + assertThat(c1).isEqualTo(c1).isEqualTo(c2); + assertThat(c1).isNotEqualTo(c3).isNotEqualTo(c4); + } + + private Object[] findRepeatedAnnotationValues(RepeatableContainers containers, + Class element, Class annotationType) { + Annotation[] annotations = containers.findRepeatedAnnotations( + element.getAnnotation(annotationType)); + return extractValues(annotations); + } + + private Object[] extractValues(Annotation[] annotations) { + try { + if (annotations == null) { + return null; + } + Object[] result = new String[annotations.length]; + for (int i = 0; i < annotations.length; i++) { + result[i] = annotations[i].annotationType().getMethod("value").invoke( + annotations[i]); + } + return result; + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface NonRepeatable { + + String value() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + @Repeatable(StandardContainer.class) + static @interface StandardRepeatable { + + String value() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface StandardContainer { + + StandardRepeatable[] value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface ExplicitRepeatable { + + String value() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface ExplicitContainer { + + ExplicitRepeatable[] value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface InvalidNoValue { + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface InvalidNotArray { + + int value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface InvalidWrongArrayType { + + StandardRepeatable[] value(); + + } + + @NonRepeatable("a") + static class WithNonRepeatable { + + } + + @StandardRepeatable("a") + static class WithSingleStandardRepeatable { + + } + + @StandardRepeatable("a") + @StandardRepeatable("b") + static class WithStandardRepeatables { + + } + + @ExplicitContainer({ @ExplicitRepeatable("a"), @ExplicitRepeatable("b") }) + static class WithExplicitRepeatables { + + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/SynthesizingMethodParameterTests.java b/spring-core/src/test/java/org/springframework/core/annotation/SynthesizingMethodParameterTests.java new file mode 100644 index 0000000..a1eea09 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/SynthesizingMethodParameterTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.core.MethodParameter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Juergen Hoeller + * @since 5.0 + */ +class SynthesizingMethodParameterTests { + + private Method method; + + private SynthesizingMethodParameter stringParameter; + + private SynthesizingMethodParameter longParameter; + + private SynthesizingMethodParameter intReturnType; + + + @BeforeEach + void setUp() throws NoSuchMethodException { + method = getClass().getMethod("method", String.class, Long.TYPE); + stringParameter = new SynthesizingMethodParameter(method, 0); + longParameter = new SynthesizingMethodParameter(method, 1); + intReturnType = new SynthesizingMethodParameter(method, -1); + } + + + @Test + void equals() throws NoSuchMethodException { + assertThat(stringParameter).isEqualTo(stringParameter); + assertThat(longParameter).isEqualTo(longParameter); + assertThat(intReturnType).isEqualTo(intReturnType); + + assertThat(stringParameter.equals(longParameter)).isFalse(); + assertThat(stringParameter.equals(intReturnType)).isFalse(); + assertThat(longParameter.equals(stringParameter)).isFalse(); + assertThat(longParameter.equals(intReturnType)).isFalse(); + assertThat(intReturnType.equals(stringParameter)).isFalse(); + assertThat(intReturnType.equals(longParameter)).isFalse(); + + Method method = getClass().getMethod("method", String.class, Long.TYPE); + MethodParameter methodParameter = new SynthesizingMethodParameter(method, 0); + assertThat(methodParameter).isEqualTo(stringParameter); + assertThat(stringParameter).isEqualTo(methodParameter); + assertThat(methodParameter).isNotEqualTo(longParameter); + assertThat(longParameter).isNotEqualTo(methodParameter); + + methodParameter = new MethodParameter(method, 0); + assertThat(methodParameter).isEqualTo(stringParameter); + assertThat(stringParameter).isEqualTo(methodParameter); + assertThat(methodParameter).isNotEqualTo(longParameter); + assertThat(longParameter).isNotEqualTo(methodParameter); + } + + @Test + void testHashCode() throws NoSuchMethodException { + assertThat(stringParameter.hashCode()).isEqualTo(stringParameter.hashCode()); + assertThat(longParameter.hashCode()).isEqualTo(longParameter.hashCode()); + assertThat(intReturnType.hashCode()).isEqualTo(intReturnType.hashCode()); + + Method method = getClass().getMethod("method", String.class, Long.TYPE); + SynthesizingMethodParameter methodParameter = new SynthesizingMethodParameter(method, 0); + assertThat(methodParameter.hashCode()).isEqualTo(stringParameter.hashCode()); + assertThat(methodParameter.hashCode()).isNotEqualTo(longParameter.hashCode()); + } + + @Test + void factoryMethods() { + assertThat(SynthesizingMethodParameter.forExecutable(method, 0)).isEqualTo(stringParameter); + assertThat(SynthesizingMethodParameter.forExecutable(method, 1)).isEqualTo(longParameter); + + assertThat(SynthesizingMethodParameter.forParameter(method.getParameters()[0])).isEqualTo(stringParameter); + assertThat(SynthesizingMethodParameter.forParameter(method.getParameters()[1])).isEqualTo(longParameter); + } + + @Test + void indexValidation() { + assertThatIllegalArgumentException().isThrownBy(() -> + new SynthesizingMethodParameter(method, 2)); + } + + + public int method(String p1, long p2) { + return 42; + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/TypeMappedAnnotationTests.java b/spring-core/src/test/java/org/springframework/core/annotation/TypeMappedAnnotationTests.java new file mode 100644 index 0000000..d4ec258 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/TypeMappedAnnotationTests.java @@ -0,0 +1,269 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation; + +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link TypeMappedAnnotation}. See also + * {@link MergedAnnotationsTests} for a much more extensive collection of tests. + * + * @author Phillip Webb + */ +class TypeMappedAnnotationTests { + + @Test + void mappingWhenMirroredReturnsMirroredValues() { + testExplicitMirror(WithExplicitMirrorA.class); + testExplicitMirror(WithExplicitMirrorB.class); + } + + private void testExplicitMirror(Class annotatedClass) { + TypeMappedAnnotation annotation = getTypeMappedAnnotation( + annotatedClass, ExplicitMirror.class); + assertThat(annotation.getString("a")).isEqualTo("test"); + assertThat(annotation.getString("b")).isEqualTo("test"); + } + + @Test + void mappingExplicitAliasToMetaAnnotationReturnsMappedValues() { + TypeMappedAnnotation annotation = getTypeMappedAnnotation( + WithExplicitAliasToMetaAnnotation.class, + ExplicitAliasToMetaAnnotation.class, + ExplicitAliasMetaAnnotationTarget.class); + assertThat(annotation.getString("aliased")).isEqualTo("aliased"); + assertThat(annotation.getString("nonAliased")).isEqualTo("nonAliased"); + } + + @Test + void mappingConventionAliasToMetaAnnotationReturnsMappedValues() { + TypeMappedAnnotation annotation = getTypeMappedAnnotation( + WithConventionAliasToMetaAnnotation.class, + ConventionAliasToMetaAnnotation.class, + ConventionAliasMetaAnnotationTarget.class); + assertThat(annotation.getString("value")).isEqualTo(""); + assertThat(annotation.getString("convention")).isEqualTo("convention"); + } + + @Test + void adaptFromEmptyArrayToAnyComponentType() { + AttributeMethods methods = AttributeMethods.forAnnotationType(ArrayTypes.class); + Map attributes = new HashMap<>(); + for (int i = 0; i < methods.size(); i++) { + attributes.put(methods.get(i).getName(), new Object[] {}); + } + MergedAnnotation annotation = TypeMappedAnnotation.of(null, null, + ArrayTypes.class, attributes); + assertThat(annotation.getValue("stringValue")).contains(new String[] {}); + assertThat(annotation.getValue("byteValue")).contains(new byte[] {}); + assertThat(annotation.getValue("shortValue")).contains(new short[] {}); + assertThat(annotation.getValue("intValue")).contains(new int[] {}); + assertThat(annotation.getValue("longValue")).contains(new long[] {}); + assertThat(annotation.getValue("booleanValue")).contains(new boolean[] {}); + assertThat(annotation.getValue("charValue")).contains(new char[] {}); + assertThat(annotation.getValue("doubleValue")).contains(new double[] {}); + assertThat(annotation.getValue("floatValue")).contains(new float[] {}); + assertThat(annotation.getValue("classValue")).contains(new Class[] {}); + assertThat(annotation.getValue("annotationValue")).contains(new MergedAnnotation[] {}); + assertThat(annotation.getValue("enumValue")).contains(new ExampleEnum[] {}); + } + + @Test + void adaptFromNestedMergedAnnotation() { + MergedAnnotation nested = MergedAnnotation.of(Nested.class); + MergedAnnotation annotation = TypeMappedAnnotation.of(null, null, + NestedContainer.class, Collections.singletonMap("value", nested)); + assertThat(annotation.getAnnotation("value", Nested.class)).isSameAs(nested); + } + + @Test + void adaptFromStringToClass() { + MergedAnnotation annotation = TypeMappedAnnotation.of(null, null, + ClassAttributes.class, + Collections.singletonMap("classValue", InputStream.class.getName())); + assertThat(annotation.getString("classValue")).isEqualTo(InputStream.class.getName()); + assertThat(annotation.getClass("classValue")).isEqualTo(InputStream.class); + } + + @Test + void adaptFromStringArrayToClassArray() { + MergedAnnotation annotation = TypeMappedAnnotation.of(null, null, ClassAttributes.class, + Collections.singletonMap("classArrayValue", new String[] { InputStream.class.getName() })); + assertThat(annotation.getStringArray("classArrayValue")).containsExactly(InputStream.class.getName()); + assertThat(annotation.getClassArray("classArrayValue")).containsExactly(InputStream.class); + } + + private TypeMappedAnnotation getTypeMappedAnnotation( + Class source, Class annotationType) { + return getTypeMappedAnnotation(source, annotationType, annotationType); + } + + private TypeMappedAnnotation getTypeMappedAnnotation( + Class source, Class rootAnnotationType, + Class annotationType) { + Annotation rootAnnotation = source.getAnnotation(rootAnnotationType); + AnnotationTypeMapping mapping = getMapping(rootAnnotation, annotationType); + return TypeMappedAnnotation.createIfPossible(mapping, source, rootAnnotation, 0, IntrospectionFailureLogger.INFO); + } + + private AnnotationTypeMapping getMapping(Annotation annotation, + Class mappedAnnotationType) { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( + annotation.annotationType()); + for (int i = 0; i < mappings.size(); i++) { + AnnotationTypeMapping candidate = mappings.get(i); + if (candidate.getAnnotationType().equals(mappedAnnotationType)) { + return candidate; + } + } + throw new IllegalStateException( + "No mapping from " + annotation + " to " + mappedAnnotationType); + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface ExplicitMirror { + + @AliasFor("b") + String a() default ""; + + @AliasFor("a") + String b() default ""; + + } + + @ExplicitMirror(a = "test") + static class WithExplicitMirrorA { + + } + + @ExplicitMirror(b = "test") + static class WithExplicitMirrorB { + + } + + @Retention(RetentionPolicy.RUNTIME) + @ExplicitAliasMetaAnnotationTarget(nonAliased = "nonAliased") + static @interface ExplicitAliasToMetaAnnotation { + + @AliasFor(annotation = ExplicitAliasMetaAnnotationTarget.class) + String aliased() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface ExplicitAliasMetaAnnotationTarget { + + String aliased() default ""; + + String nonAliased() default ""; + + } + + @ExplicitAliasToMetaAnnotation(aliased = "aliased") + private static class WithExplicitAliasToMetaAnnotation { + + } + + @Retention(RetentionPolicy.RUNTIME) + @ConventionAliasMetaAnnotationTarget + static @interface ConventionAliasToMetaAnnotation { + + String value() default ""; + + String convention() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface ConventionAliasMetaAnnotationTarget { + + String value() default ""; + + String convention() default ""; + + } + + @ConventionAliasToMetaAnnotation(value = "value", convention = "convention") + private static class WithConventionAliasToMetaAnnotation { + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface ArrayTypes { + + String[] stringValue(); + + byte[] byteValue(); + + short[] shortValue(); + + int[] intValue(); + + long[] longValue(); + + boolean[] booleanValue(); + + char[] charValue(); + + double[] doubleValue(); + + float[] floatValue(); + + Class[] classValue(); + + ExplicitMirror[] annotationValue(); + + ExampleEnum[] enumValue(); + + } + + enum ExampleEnum {ONE,TWO,THREE} + + @Retention(RetentionPolicy.RUNTIME) + static @interface NestedContainer { + + Nested value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface Nested { + + String value() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface ClassAttributes { + + Class classValue(); + + Class[] classArrayValue(); + + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/subpackage/NonPublicAliasedAnnotatedClass.java b/spring-core/src/test/java/org/springframework/core/annotation/subpackage/NonPublicAliasedAnnotatedClass.java new file mode 100644 index 0000000..f0249cf --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/subpackage/NonPublicAliasedAnnotatedClass.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation.subpackage; + +/** + * Class annotated with a non-public (i.e., package private) custom annotation + * that uses {@code @AliasFor}. + * + * @author Sam Brannen + * @since 4.2 + */ +@NonPublicAliasedAnnotation(name = "test", path = "/test") +class NonPublicAliasedAnnotatedClass { +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/subpackage/NonPublicAliasedAnnotation.java b/spring-core/src/test/java/org/springframework/core/annotation/subpackage/NonPublicAliasedAnnotation.java new file mode 100644 index 0000000..25c9adb --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/subpackage/NonPublicAliasedAnnotation.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation.subpackage; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.springframework.core.annotation.AliasFor; + +/** + * Non-public mock of {@code org.springframework.web.bind.annotation.RequestMapping}. + * + * @author Sam Brannen + * @since 4.2 + */ +@Retention(RetentionPolicy.RUNTIME) +@interface NonPublicAliasedAnnotation { + + String name(); + + @AliasFor("path") + String value() default ""; + + @AliasFor("value") + String path() default ""; +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/subpackage/NonPublicAnnotatedClass.java b/spring-core/src/test/java/org/springframework/core/annotation/subpackage/NonPublicAnnotatedClass.java new file mode 100644 index 0000000..cea28d7 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/subpackage/NonPublicAnnotatedClass.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation.subpackage; + +/** + * Class annotated with a non-public (i.e., package private) custom annotation. + * + * @author Sam Brannen + * @since 4.0 + */ +@NonPublicAnnotation(42) +public class NonPublicAnnotatedClass { + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/subpackage/NonPublicAnnotation.java b/spring-core/src/test/java/org/springframework/core/annotation/subpackage/NonPublicAnnotation.java new file mode 100644 index 0000000..9a484dc --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/subpackage/NonPublicAnnotation.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.annotation.subpackage; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Non-public (i.e., package private) custom annotation. + * + * @author Sam Brannen + * @since 4.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@interface NonPublicAnnotation { + + int value() default -1; +} diff --git a/spring-core/src/test/java/org/springframework/core/codec/ByteArrayDecoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/ByteArrayDecoderTests.java new file mode 100644 index 0000000..10f93f5 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/codec/ByteArrayDecoderTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.nio.charset.StandardCharsets; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.testfixture.codec.AbstractDecoderTests; +import org.springframework.util.MimeTypeUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + */ +class ByteArrayDecoderTests extends AbstractDecoderTests { + + private final byte[] fooBytes = "foo".getBytes(StandardCharsets.UTF_8); + + private final byte[] barBytes = "bar".getBytes(StandardCharsets.UTF_8); + + + ByteArrayDecoderTests() { + super(new ByteArrayDecoder()); + } + + @Override + @Test + public void canDecode() { + assertThat(this.decoder.canDecode(ResolvableType.forClass(byte[].class), + MimeTypeUtils.TEXT_PLAIN)).isTrue(); + assertThat(this.decoder.canDecode(ResolvableType.forClass(Integer.class), + MimeTypeUtils.TEXT_PLAIN)).isFalse(); + assertThat(this.decoder.canDecode(ResolvableType.forClass(byte[].class), + MimeTypeUtils.APPLICATION_JSON)).isTrue(); + } + + @Override + @Test + public void decode() { + Flux input = Flux.concat( + dataBuffer(this.fooBytes), + dataBuffer(this.barBytes)); + + testDecodeAll(input, byte[].class, step -> step + .consumeNextWith(expectBytes(this.fooBytes)) + .consumeNextWith(expectBytes(this.barBytes)) + .verifyComplete()); + + } + + @Override + @Test + public void decodeToMono() { + Flux input = Flux.concat( + dataBuffer(this.fooBytes), + dataBuffer(this.barBytes)); + + byte[] expected = new byte[this.fooBytes.length + this.barBytes.length]; + System.arraycopy(this.fooBytes, 0, expected, 0, this.fooBytes.length); + System.arraycopy(this.barBytes, 0, expected, this.fooBytes.length, this.barBytes.length); + + testDecodeToMonoAll(input, byte[].class, step -> step + .consumeNextWith(expectBytes(expected)) + .verifyComplete()); + } + + private Consumer expectBytes(byte[] expected) { + return bytes -> assertThat(bytes).isEqualTo(expected); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/codec/ByteArrayEncoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/ByteArrayEncoderTests.java new file mode 100644 index 0000000..dcb5043 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/codec/ByteArrayEncoderTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.testfixture.codec.AbstractEncoderTests; +import org.springframework.util.MimeTypeUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + */ +class ByteArrayEncoderTests extends AbstractEncoderTests { + + private final byte[] fooBytes = "foo".getBytes(StandardCharsets.UTF_8); + + private final byte[] barBytes = "bar".getBytes(StandardCharsets.UTF_8); + + ByteArrayEncoderTests() { + super(new ByteArrayEncoder()); + } + + + @Override + @Test + public void canEncode() { + assertThat(this.encoder.canEncode(ResolvableType.forClass(byte[].class), + MimeTypeUtils.TEXT_PLAIN)).isTrue(); + assertThat(this.encoder.canEncode(ResolvableType.forClass(Integer.class), + MimeTypeUtils.TEXT_PLAIN)).isFalse(); + assertThat(this.encoder.canEncode(ResolvableType.forClass(byte[].class), + MimeTypeUtils.APPLICATION_JSON)).isTrue(); + + // SPR-15464 + assertThat(this.encoder.canEncode(ResolvableType.NONE, null)).isFalse(); + } + + @Override + @Test + public void encode() { + Flux input = Flux.just(this.fooBytes, this.barBytes); + + testEncodeAll(input, byte[].class, step -> step + .consumeNextWith(expectBytes(this.fooBytes)) + .consumeNextWith(expectBytes(this.barBytes)) + .verifyComplete()); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/codec/ByteBufferDecoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/ByteBufferDecoderTests.java new file mode 100644 index 0000000..d14a754 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/codec/ByteBufferDecoderTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.testfixture.codec.AbstractDecoderTests; +import org.springframework.util.MimeTypeUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Sebastien Deleuze + */ +class ByteBufferDecoderTests extends AbstractDecoderTests { + + private final byte[] fooBytes = "foo".getBytes(StandardCharsets.UTF_8); + + private final byte[] barBytes = "bar".getBytes(StandardCharsets.UTF_8); + + + ByteBufferDecoderTests() { + super(new ByteBufferDecoder()); + } + + @Override + @Test + public void canDecode() { + assertThat(this.decoder.canDecode(ResolvableType.forClass(ByteBuffer.class), + MimeTypeUtils.TEXT_PLAIN)).isTrue(); + assertThat(this.decoder.canDecode(ResolvableType.forClass(Integer.class), + MimeTypeUtils.TEXT_PLAIN)).isFalse(); + assertThat(this.decoder.canDecode(ResolvableType.forClass(ByteBuffer.class), + MimeTypeUtils.APPLICATION_JSON)).isTrue(); + } + + @Override + @Test + public void decode() { + Flux input = Flux.concat( + dataBuffer(this.fooBytes), + dataBuffer(this.barBytes)); + + testDecodeAll(input, ByteBuffer.class, step -> step + .consumeNextWith(expectByteBuffer(ByteBuffer.wrap(this.fooBytes))) + .consumeNextWith(expectByteBuffer(ByteBuffer.wrap(this.barBytes))) + .verifyComplete()); + + + } + + @Override + @Test + public void decodeToMono() { + Flux input = Flux.concat( + dataBuffer(this.fooBytes), + dataBuffer(this.barBytes)); + ByteBuffer expected = ByteBuffer.allocate(this.fooBytes.length + this.barBytes.length); + expected.put(this.fooBytes).put(this.barBytes).flip(); + + testDecodeToMonoAll(input, ByteBuffer.class, step -> step + .consumeNextWith(expectByteBuffer(expected)) + .verifyComplete()); + + } + + private Consumer expectByteBuffer(ByteBuffer expected) { + return actual -> assertThat(actual).isEqualTo(expected); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/codec/ByteBufferEncoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/ByteBufferEncoderTests.java new file mode 100644 index 0000000..1bc0e3f --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/codec/ByteBufferEncoderTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.testfixture.codec.AbstractEncoderTests; +import org.springframework.util.MimeTypeUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Sebastien Deleuze + */ +class ByteBufferEncoderTests extends AbstractEncoderTests { + + private final byte[] fooBytes = "foo".getBytes(StandardCharsets.UTF_8); + + private final byte[] barBytes = "bar".getBytes(StandardCharsets.UTF_8); + + ByteBufferEncoderTests() { + super(new ByteBufferEncoder()); + } + + @Override + @Test + public void canEncode() { + assertThat(this.encoder.canEncode(ResolvableType.forClass(ByteBuffer.class), + MimeTypeUtils.TEXT_PLAIN)).isTrue(); + assertThat(this.encoder.canEncode(ResolvableType.forClass(Integer.class), + MimeTypeUtils.TEXT_PLAIN)).isFalse(); + assertThat(this.encoder.canEncode(ResolvableType.forClass(ByteBuffer.class), + MimeTypeUtils.APPLICATION_JSON)).isTrue(); + + // SPR-15464 + assertThat(this.encoder.canEncode(ResolvableType.NONE, null)).isFalse(); + } + + @Override + @Test + public void encode() { + Flux input = Flux.just(this.fooBytes, this.barBytes) + .map(ByteBuffer::wrap); + + testEncodeAll(input, ByteBuffer.class, step -> step + .consumeNextWith(expectBytes(this.fooBytes)) + .consumeNextWith(expectBytes(this.barBytes)) + .verifyComplete()); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/codec/CharSequenceEncoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/CharSequenceEncoderTests.java new file mode 100644 index 0000000..e73bb2c --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/codec/CharSequenceEncoderTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.nio.charset.Charset; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.testfixture.codec.AbstractEncoderTests; +import org.springframework.util.MimeTypeUtils; + +import static java.nio.charset.StandardCharsets.ISO_8859_1; +import static java.nio.charset.StandardCharsets.US_ASCII; +import static java.nio.charset.StandardCharsets.UTF_16; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Sebastien Deleuze + */ +class CharSequenceEncoderTests extends AbstractEncoderTests { + + private final String foo = "foo"; + + private final String bar = "bar"; + + CharSequenceEncoderTests() { + super(CharSequenceEncoder.textPlainOnly()); + } + + + @Override + @Test + public void canEncode() throws Exception { + assertThat(this.encoder.canEncode(ResolvableType.forClass(String.class), + MimeTypeUtils.TEXT_PLAIN)).isTrue(); + assertThat(this.encoder.canEncode(ResolvableType.forClass(StringBuilder.class), + MimeTypeUtils.TEXT_PLAIN)).isTrue(); + assertThat(this.encoder.canEncode(ResolvableType.forClass(StringBuffer.class), + MimeTypeUtils.TEXT_PLAIN)).isTrue(); + assertThat(this.encoder.canEncode(ResolvableType.forClass(Integer.class), + MimeTypeUtils.TEXT_PLAIN)).isFalse(); + assertThat(this.encoder.canEncode(ResolvableType.forClass(String.class), + MimeTypeUtils.APPLICATION_JSON)).isFalse(); + + // SPR-15464 + assertThat(this.encoder.canEncode(ResolvableType.NONE, null)).isFalse(); + } + + @Override + @Test + public void encode() { + Flux input = Flux.just(this.foo, this.bar); + + testEncodeAll(input, CharSequence.class, step -> step + .consumeNextWith(expectString(this.foo)) + .consumeNextWith(expectString(this.bar)) + .verifyComplete()); + } + + @Test + void calculateCapacity() { + String sequence = "Hello World!"; + Stream.of(UTF_8, UTF_16, ISO_8859_1, US_ASCII, Charset.forName("BIG5")) + .forEach(charset -> { + int capacity = this.encoder.calculateCapacity(sequence, charset); + int length = sequence.length(); + assertThat(capacity >= length).as(String.format("%s has capacity %d; length %d", charset, capacity, length)).isTrue(); + }); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/codec/DataBufferDecoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/DataBufferDecoderTests.java new file mode 100644 index 0000000..e129653 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/codec/DataBufferDecoderTests.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.nio.charset.StandardCharsets; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.core.testfixture.codec.AbstractDecoderTests; +import org.springframework.util.MimeTypeUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Sebastien Deleuze + */ +class DataBufferDecoderTests extends AbstractDecoderTests { + + private final byte[] fooBytes = "foo".getBytes(StandardCharsets.UTF_8); + + private final byte[] barBytes = "bar".getBytes(StandardCharsets.UTF_8); + + + DataBufferDecoderTests() { + super(new DataBufferDecoder()); + } + + @Override + @Test + public void canDecode() { + assertThat(this.decoder.canDecode(ResolvableType.forClass(DataBuffer.class), + MimeTypeUtils.TEXT_PLAIN)).isTrue(); + assertThat(this.decoder.canDecode(ResolvableType.forClass(Integer.class), + MimeTypeUtils.TEXT_PLAIN)).isFalse(); + assertThat(this.decoder.canDecode(ResolvableType.forClass(DataBuffer.class), + MimeTypeUtils.APPLICATION_JSON)).isTrue(); + } + + @Override + @Test + public void decode() { + Flux input = Flux.just( + this.bufferFactory.wrap(this.fooBytes), + this.bufferFactory.wrap(this.barBytes)); + + testDecodeAll(input, DataBuffer.class, step -> step + .consumeNextWith(expectDataBuffer(this.fooBytes)) + .consumeNextWith(expectDataBuffer(this.barBytes)) + .verifyComplete()); + } + + @Override + @Test + public void decodeToMono() throws Exception { + Flux input = Flux.concat( + dataBuffer(this.fooBytes), + dataBuffer(this.barBytes)); + + byte[] expected = new byte[this.fooBytes.length + this.barBytes.length]; + System.arraycopy(this.fooBytes, 0, expected, 0, this.fooBytes.length); + System.arraycopy(this.barBytes, 0, expected, this.fooBytes.length, this.barBytes.length); + + testDecodeToMonoAll(input, DataBuffer.class, step -> step + .consumeNextWith(expectDataBuffer(expected)) + .verifyComplete()); + } + + private Consumer expectDataBuffer(byte[] expected) { + return actual -> { + byte[] actualBytes = new byte[actual.readableByteCount()]; + actual.read(actualBytes); + assertThat(actualBytes).isEqualTo(expected); + + DataBufferUtils.release(actual); + }; + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/codec/DataBufferEncoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/DataBufferEncoderTests.java new file mode 100644 index 0000000..9a578ed --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/codec/DataBufferEncoderTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.testfixture.codec.AbstractEncoderTests; +import org.springframework.util.MimeTypeUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Sebastien Deleuze + */ +class DataBufferEncoderTests extends AbstractEncoderTests { + + private final byte[] fooBytes = "foo".getBytes(StandardCharsets.UTF_8); + + private final byte[] barBytes = "bar".getBytes(StandardCharsets.UTF_8); + + DataBufferEncoderTests() { + super(new DataBufferEncoder()); + } + + + @Override + @Test + public void canEncode() { + assertThat(this.encoder.canEncode(ResolvableType.forClass(DataBuffer.class), + MimeTypeUtils.TEXT_PLAIN)).isTrue(); + assertThat(this.encoder.canEncode(ResolvableType.forClass(Integer.class), + MimeTypeUtils.TEXT_PLAIN)).isFalse(); + assertThat(this.encoder.canEncode(ResolvableType.forClass(DataBuffer.class), + MimeTypeUtils.APPLICATION_JSON)).isTrue(); + + // SPR-15464 + assertThat(this.encoder.canEncode(ResolvableType.NONE, null)).isFalse(); + } + + @Override + @Test + public void encode() throws Exception { + Flux input = Flux.just(this.fooBytes, this.barBytes) + .flatMap(bytes -> Mono.defer(() -> { + DataBuffer dataBuffer = this.bufferFactory.allocateBuffer(bytes.length); + dataBuffer.write(bytes); + return Mono.just(dataBuffer); + })); + + testEncodeAll(input, DataBuffer.class, step -> step + .consumeNextWith(expectBytes(this.fooBytes)) + .consumeNextWith(expectBytes(this.barBytes)) + .verifyComplete()); + + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/codec/NettyByteBufDecoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/NettyByteBufDecoderTests.java new file mode 100644 index 0000000..7249fdc --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/codec/NettyByteBufDecoderTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.nio.charset.StandardCharsets; +import java.util.function.Consumer; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.testfixture.codec.AbstractDecoderTests; +import org.springframework.util.MimeTypeUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Vladislav Kisel + */ +class NettyByteBufDecoderTests extends AbstractDecoderTests { + + private final byte[] fooBytes = "foo".getBytes(StandardCharsets.UTF_8); + + private final byte[] barBytes = "bar".getBytes(StandardCharsets.UTF_8); + + + NettyByteBufDecoderTests() { + super(new NettyByteBufDecoder()); + } + + @Override + @Test + public void canDecode() { + assertThat(this.decoder.canDecode(ResolvableType.forClass(ByteBuf.class), + MimeTypeUtils.TEXT_PLAIN)).isTrue(); + assertThat(this.decoder.canDecode(ResolvableType.forClass(Integer.class), + MimeTypeUtils.TEXT_PLAIN)).isFalse(); + assertThat(this.decoder.canDecode(ResolvableType.forClass(ByteBuf.class), + MimeTypeUtils.APPLICATION_JSON)).isTrue(); + } + + @Override + @Test + public void decode() { + Flux input = Flux.concat( + dataBuffer(this.fooBytes), + dataBuffer(this.barBytes)); + + testDecodeAll(input, ByteBuf.class, step -> step + .consumeNextWith(expectByteBuffer(Unpooled.copiedBuffer(this.fooBytes))) + .consumeNextWith(expectByteBuffer(Unpooled.copiedBuffer(this.barBytes))) + .verifyComplete()); + } + + @Override + @Test + public void decodeToMono() { + Flux input = Flux.concat( + dataBuffer(this.fooBytes), + dataBuffer(this.barBytes)); + + ByteBuf expected = Unpooled.buffer(this.fooBytes.length + this.barBytes.length) + .writeBytes(this.fooBytes) + .writeBytes(this.barBytes) + .readerIndex(0); + + testDecodeToMonoAll(input, ByteBuf.class, step -> step + .consumeNextWith(expectByteBuffer(expected)) + .verifyComplete()); + } + + private Consumer expectByteBuffer(ByteBuf expected) { + return actual -> assertThat(actual).isEqualTo(expected); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/codec/NettyByteBufEncoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/NettyByteBufEncoderTests.java new file mode 100644 index 0000000..0c6bff3 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/codec/NettyByteBufEncoderTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.nio.charset.StandardCharsets; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.testfixture.codec.AbstractEncoderTests; +import org.springframework.util.MimeTypeUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Vladislav Kisel + */ +class NettyByteBufEncoderTests extends AbstractEncoderTests { + + private final byte[] fooBytes = "foo".getBytes(StandardCharsets.UTF_8); + + private final byte[] barBytes = "bar".getBytes(StandardCharsets.UTF_8); + + NettyByteBufEncoderTests() { + super(new NettyByteBufEncoder()); + } + + @Override + @Test + public void canEncode() { + assertThat(this.encoder.canEncode(ResolvableType.forClass(ByteBuf.class), + MimeTypeUtils.TEXT_PLAIN)).isTrue(); + assertThat(this.encoder.canEncode(ResolvableType.forClass(Integer.class), + MimeTypeUtils.TEXT_PLAIN)).isFalse(); + assertThat(this.encoder.canEncode(ResolvableType.forClass(ByteBuf.class), + MimeTypeUtils.APPLICATION_JSON)).isTrue(); + + // gh-20024 + assertThat(this.encoder.canEncode(ResolvableType.NONE, null)).isFalse(); + } + + @Override + @Test + public void encode() { + Flux input = Flux.just(this.fooBytes, this.barBytes).map(Unpooled::copiedBuffer); + + testEncodeAll(input, ByteBuf.class, step -> step + .consumeNextWith(expectBytes(this.fooBytes)) + .consumeNextWith(expectBytes(this.barBytes)) + .verifyComplete()); + } +} diff --git a/spring-core/src/test/java/org/springframework/core/codec/ResourceDecoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/ResourceDecoderTests.java new file mode 100644 index 0000000..f060ce4 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/codec/ResourceDecoderTests.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.testfixture.codec.AbstractDecoderTests; +import org.springframework.util.MimeTypeUtils; +import org.springframework.util.StreamUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.core.ResolvableType.forClass; + +/** + * @author Arjen Poutsma + */ +class ResourceDecoderTests extends AbstractDecoderTests { + + private final byte[] fooBytes = "foo".getBytes(StandardCharsets.UTF_8); + + private final byte[] barBytes = "bar".getBytes(StandardCharsets.UTF_8); + + + ResourceDecoderTests() { + super(new ResourceDecoder()); + } + + @Override + @Test + public void canDecode() { + assertThat(this.decoder.canDecode(forClass(InputStreamResource.class), MimeTypeUtils.TEXT_PLAIN)).isTrue(); + assertThat(this.decoder.canDecode(forClass(ByteArrayResource.class), MimeTypeUtils.TEXT_PLAIN)).isTrue(); + assertThat(this.decoder.canDecode(forClass(Resource.class), MimeTypeUtils.TEXT_PLAIN)).isTrue(); + assertThat(this.decoder.canDecode(forClass(InputStreamResource.class), MimeTypeUtils.APPLICATION_JSON)).isTrue(); + assertThat(this.decoder.canDecode(forClass(Object.class), MimeTypeUtils.APPLICATION_JSON)).isFalse(); + } + + + @Override + @Test + public void decode() { + Flux input = Flux.concat(dataBuffer(this.fooBytes), dataBuffer(this.barBytes)); + + testDecodeAll(input, Resource.class, step -> step + .consumeNextWith(resource -> { + try { + byte[] bytes = StreamUtils.copyToByteArray(resource.getInputStream()); + assertThat(new String(bytes)).isEqualTo("foobar"); + } + catch (IOException ex) { + throw new AssertionError(ex.getMessage(), ex); + } + }) + .expectComplete() + .verify()); + } + + @Override + @Test + public void decodeToMono() { + Flux input = Flux.concat(dataBuffer(this.fooBytes), dataBuffer(this.barBytes)); + testDecodeToMonoAll(input, ResolvableType.forClass(Resource.class), + step -> step + .consumeNextWith(value -> { + Resource resource = (Resource) value; + try { + byte[] bytes = StreamUtils.copyToByteArray(resource.getInputStream()); + assertThat(new String(bytes)).isEqualTo("foobar"); + assertThat(resource.getFilename()).isEqualTo("testFile"); + } + catch (IOException ex) { + throw new AssertionError(ex.getMessage(), ex); + } + }) + .expectComplete() + .verify(), + null, + Collections.singletonMap(ResourceDecoder.FILENAME_HINT, "testFile")); + } + + @Test + public void decodeInputStreamResource() { + Flux input = Flux.concat(dataBuffer(this.fooBytes), dataBuffer(this.barBytes)); + testDecodeAll(input, InputStreamResource.class, step -> step + .consumeNextWith(resource -> { + try { + byte[] bytes = StreamUtils.copyToByteArray(resource.getInputStream()); + assertThat(new String(bytes)).isEqualTo("foobar"); + assertThat(resource.contentLength()).isEqualTo(fooBytes.length + barBytes.length); + } + catch (IOException ex) { + throw new AssertionError(ex.getMessage(), ex); + } + }) + .expectComplete() + .verify()); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/codec/ResourceEncoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/ResourceEncoderTests.java new file mode 100644 index 0000000..1925415 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/codec/ResourceEncoderTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.testfixture.codec.AbstractEncoderTests; +import org.springframework.lang.Nullable; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + */ +class ResourceEncoderTests extends AbstractEncoderTests { + + private final byte[] bytes = "foo".getBytes(UTF_8); + + + ResourceEncoderTests() { + super(new ResourceEncoder()); + } + + @Override + @Test + public void canEncode() { + assertThat(this.encoder.canEncode(ResolvableType.forClass(InputStreamResource.class), + MimeTypeUtils.TEXT_PLAIN)).isTrue(); + assertThat(this.encoder.canEncode(ResolvableType.forClass(ByteArrayResource.class), + MimeTypeUtils.TEXT_PLAIN)).isTrue(); + assertThat(this.encoder.canEncode(ResolvableType.forClass(Resource.class), + MimeTypeUtils.TEXT_PLAIN)).isTrue(); + assertThat(this.encoder.canEncode(ResolvableType.forClass(InputStreamResource.class), + MimeTypeUtils.APPLICATION_JSON)).isTrue(); + + // SPR-15464 + assertThat(this.encoder.canEncode(ResolvableType.NONE, null)).isFalse(); + } + + @Override + @Test + public void encode() { + Flux input = Flux.just(new ByteArrayResource(this.bytes)); + + testEncodeAll(input, Resource.class, step -> step + .consumeNextWith(expectBytes(this.bytes)) + .verifyComplete()); + } + + @Override + protected void testEncodeError(Publisher input, ResolvableType outputType, + @Nullable MimeType mimeType, @Nullable Map hints) { + + Flux i = Flux.error(new InputException()); + + Flux result = ((Encoder) this.encoder).encode(i, + this.bufferFactory, outputType, + mimeType, hints); + + StepVerifier.create(result) + .expectError(InputException.class) + .verify(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/codec/ResourceRegionEncoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/ResourceRegionEncoderTests.java new file mode 100644 index 0000000..465780e --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/codec/ResourceRegionEncoderTests.java @@ -0,0 +1,199 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.util.Collections; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; +import org.reactivestreams.Subscription; +import reactor.core.publisher.BaseSubscriber; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.core.io.support.ResourceRegion; +import org.springframework.core.testfixture.io.buffer.AbstractLeakCheckingTests; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test cases for {@link ResourceRegionEncoder} class. + * @author Brian Clozel + */ +class ResourceRegionEncoderTests extends AbstractLeakCheckingTests { + + private ResourceRegionEncoder encoder = new ResourceRegionEncoder(); + + @Test + void canEncode() { + ResolvableType resourceRegion = ResolvableType.forClass(ResourceRegion.class); + MimeType allMimeType = MimeType.valueOf("*/*"); + + assertThat(this.encoder.canEncode(ResolvableType.forClass(Resource.class), + MimeTypeUtils.APPLICATION_OCTET_STREAM)).isFalse(); + assertThat(this.encoder.canEncode(ResolvableType.forClass(Resource.class), allMimeType)).isFalse(); + assertThat(this.encoder.canEncode(resourceRegion, MimeTypeUtils.APPLICATION_OCTET_STREAM)).isTrue(); + assertThat(this.encoder.canEncode(resourceRegion, allMimeType)).isTrue(); + + // SPR-15464 + assertThat(this.encoder.canEncode(ResolvableType.NONE, null)).isFalse(); + } + + @Test + void shouldEncodeResourceRegionFileResource() throws Exception { + ResourceRegion region = new ResourceRegion( + new ClassPathResource("ResourceRegionEncoderTests.txt", getClass()), 0, 6); + Flux result = this.encoder.encode(Mono.just(region), this.bufferFactory, + ResolvableType.forClass(ResourceRegion.class), + MimeTypeUtils.APPLICATION_OCTET_STREAM, + Collections.emptyMap()); + + StepVerifier.create(result) + .consumeNextWith(stringConsumer("Spring")) + .expectComplete() + .verify(); + } + + @Test + void shouldEncodeMultipleResourceRegionsFileResource() { + Resource resource = new ClassPathResource("ResourceRegionEncoderTests.txt", getClass()); + Flux regions = Flux.just( + new ResourceRegion(resource, 0, 6), + new ResourceRegion(resource, 7, 9), + new ResourceRegion(resource, 17, 4), + new ResourceRegion(resource, 22, 17) + ); + String boundary = MimeTypeUtils.generateMultipartBoundaryString(); + + Flux result = this.encoder.encode(regions, this.bufferFactory, + ResolvableType.forClass(ResourceRegion.class), + MimeType.valueOf("text/plain"), + Collections.singletonMap(ResourceRegionEncoder.BOUNDARY_STRING_HINT, boundary) + ); + + StepVerifier.create(result) + .consumeNextWith(stringConsumer("\r\n--" + boundary + "\r\n")) + .consumeNextWith(stringConsumer("Content-Type: text/plain\r\n")) + .consumeNextWith(stringConsumer("Content-Range: bytes 0-5/39\r\n\r\n")) + .consumeNextWith(stringConsumer("Spring")) + .consumeNextWith(stringConsumer("\r\n--" + boundary + "\r\n")) + .consumeNextWith(stringConsumer("Content-Type: text/plain\r\n")) + .consumeNextWith(stringConsumer("Content-Range: bytes 7-15/39\r\n\r\n")) + .consumeNextWith(stringConsumer("Framework")) + .consumeNextWith(stringConsumer("\r\n--" + boundary + "\r\n")) + .consumeNextWith(stringConsumer("Content-Type: text/plain\r\n")) + .consumeNextWith(stringConsumer("Content-Range: bytes 17-20/39\r\n\r\n")) + .consumeNextWith(stringConsumer("test")) + .consumeNextWith(stringConsumer("\r\n--" + boundary + "\r\n")) + .consumeNextWith(stringConsumer("Content-Type: text/plain\r\n")) + .consumeNextWith(stringConsumer("Content-Range: bytes 22-38/39\r\n\r\n")) + .consumeNextWith(stringConsumer("resource content.")) + .consumeNextWith(stringConsumer("\r\n--" + boundary + "--")) + .expectComplete() + .verify(); + } + + @Test // gh-22107 + void cancelWithoutDemandForMultipleResourceRegions() { + Resource resource = new ClassPathResource("ResourceRegionEncoderTests.txt", getClass()); + Flux regions = Flux.just( + new ResourceRegion(resource, 0, 6), + new ResourceRegion(resource, 7, 9), + new ResourceRegion(resource, 17, 4), + new ResourceRegion(resource, 22, 17) + ); + String boundary = MimeTypeUtils.generateMultipartBoundaryString(); + + Flux flux = this.encoder.encode(regions, this.bufferFactory, + ResolvableType.forClass(ResourceRegion.class), + MimeType.valueOf("text/plain"), + Collections.singletonMap(ResourceRegionEncoder.BOUNDARY_STRING_HINT, boundary) + ); + + ZeroDemandSubscriber subscriber = new ZeroDemandSubscriber(); + flux.subscribe(subscriber); + subscriber.cancel(); + } + + @Test // gh-22107 + void cancelWithoutDemandForSingleResourceRegion() { + Resource resource = new ClassPathResource("ResourceRegionEncoderTests.txt", getClass()); + Mono regions = Mono.just(new ResourceRegion(resource, 0, 6)); + String boundary = MimeTypeUtils.generateMultipartBoundaryString(); + + Flux flux = this.encoder.encode(regions, this.bufferFactory, + ResolvableType.forClass(ResourceRegion.class), + MimeType.valueOf("text/plain"), + Collections.singletonMap(ResourceRegionEncoder.BOUNDARY_STRING_HINT, boundary) + ); + + ZeroDemandSubscriber subscriber = new ZeroDemandSubscriber(); + flux.subscribe(subscriber); + subscriber.cancel(); + } + + @Test + void nonExisting() { + Resource resource = new ClassPathResource("ResourceRegionEncoderTests.txt", getClass()); + Resource nonExisting = new ClassPathResource("does not exist", getClass()); + Flux regions = Flux.just( + new ResourceRegion(resource, 0, 6), + new ResourceRegion(nonExisting, 0, 6)); + + String boundary = MimeTypeUtils.generateMultipartBoundaryString(); + + Flux result = this.encoder.encode(regions, this.bufferFactory, + ResolvableType.forClass(ResourceRegion.class), + MimeType.valueOf("text/plain"), + Collections.singletonMap(ResourceRegionEncoder.BOUNDARY_STRING_HINT, boundary)); + + StepVerifier.create(result) + .consumeNextWith(stringConsumer("\r\n--" + boundary + "\r\n")) + .consumeNextWith(stringConsumer("Content-Type: text/plain\r\n")) + .consumeNextWith(stringConsumer("Content-Range: bytes 0-5/39\r\n\r\n")) + .consumeNextWith(stringConsumer("Spring")) + .expectError(EncodingException.class) + .verify(); + } + + protected Consumer stringConsumer(String expected) { + return dataBuffer -> { + String value = dataBuffer.toString(UTF_8); + DataBufferUtils.release(dataBuffer); + assertThat(value).isEqualTo(expected); + }; + } + + + private static class ZeroDemandSubscriber extends BaseSubscriber { + + @Override + protected void hookOnSubscribe(Subscription subscription) { + // Just subscribe without requesting + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/codec/StringDecoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/StringDecoderTests.java new file mode 100644 index 0000000..a41e1fd --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/codec/StringDecoderTests.java @@ -0,0 +1,252 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.codec; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferLimitException; +import org.springframework.core.testfixture.codec.AbstractDecoderTests; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +import static java.nio.charset.StandardCharsets.UTF_16BE; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link StringDecoder}. + * + * @author Sebastien Deleuze + * @author Brian Clozel + * @author Mark Paluch + */ +class StringDecoderTests extends AbstractDecoderTests { + + private static final ResolvableType TYPE = ResolvableType.forClass(String.class); + + + StringDecoderTests() { + super(StringDecoder.allMimeTypes()); + } + + + @Override + @Test + public void canDecode() { + assertThat(this.decoder.canDecode(TYPE, MimeTypeUtils.TEXT_PLAIN)).isTrue(); + assertThat(this.decoder.canDecode(TYPE, MimeTypeUtils.TEXT_HTML)).isTrue(); + assertThat(this.decoder.canDecode(TYPE, MimeTypeUtils.APPLICATION_JSON)).isTrue(); + assertThat(this.decoder.canDecode(TYPE, MimeTypeUtils.parseMimeType("text/plain;charset=utf-8"))).isTrue(); + assertThat(this.decoder.canDecode(ResolvableType.forClass(Integer.class), MimeTypeUtils.TEXT_PLAIN)).isFalse(); + assertThat(this.decoder.canDecode(ResolvableType.forClass(Object.class), MimeTypeUtils.APPLICATION_JSON)).isFalse(); + } + + @Override + @Test + public void decode() { + String u = "ü"; + String e = "é"; + String o = "ø"; + String s = String.format("%s\n%s\n%s", u, e, o); + Flux input = toDataBuffers(s, 1, UTF_8); + + // TODO: temporarily replace testDecodeAll with explicit decode/cancel/empty + // see https://github.com/reactor/reactor-core/issues/2041 + +// testDecode(input, TYPE, step -> step.expectNext(u, e, o).verifyComplete(), null, null); +// testDecodeCancel(input, TYPE, null, null); +// testDecodeEmpty(TYPE, null, null); + + testDecodeAll(input, TYPE, step -> step.expectNext(u, e, o).verifyComplete(), null, null); + } + + @Test + void decodeMultibyteCharacterUtf16() { + String u = "ü"; + String e = "é"; + String o = "ø"; + String s = String.format("%s\n%s\n%s", u, e, o); + Flux source = toDataBuffers(s, 2, UTF_16BE); + MimeType mimeType = MimeTypeUtils.parseMimeType("text/plain;charset=utf-16be"); + + testDecode(source, TYPE, step -> step.expectNext(u, e, o).verifyComplete(), mimeType, null); + } + + private Flux toDataBuffers(String s, int length, Charset charset) { + byte[] bytes = s.getBytes(charset); + List chunks = new ArrayList<>(); + for (int i = 0; i < bytes.length; i += length) { + chunks.add(Arrays.copyOfRange(bytes, i, i + length)); + } + return Flux.fromIterable(chunks) + .map(chunk -> { + DataBuffer dataBuffer = this.bufferFactory.allocateBuffer(length); + dataBuffer.write(chunk, 0, chunk.length); + return dataBuffer; + }); + } + + @Test + void decodeNewLine() { + Flux input = Flux.just( + stringBuffer("\r\nabc\n"), + stringBuffer("def"), + stringBuffer("ghi\r\n\n"), + stringBuffer("jkl"), + stringBuffer("mno\npqr\n"), + stringBuffer("stu"), + stringBuffer("vw"), + stringBuffer("xyz") + ); + + testDecode(input, String.class, step -> step + .expectNext("") + .expectNext("abc") + .expectNext("defghi") + .expectNext("") + .expectNext("jklmno") + .expectNext("pqr") + .expectNext("stuvwxyz") + .expectComplete() + .verify()); + } + + @Test + void maxInMemoryLimit() { + Flux input = Flux.just( + stringBuffer("abc\n"), stringBuffer("defg\n"), + stringBuffer("hi"), stringBuffer("jkl"), stringBuffer("mnop")); + + this.decoder.setMaxInMemorySize(5); + testDecode(input, String.class, step -> + step.expectNext("abc", "defg").verifyError(DataBufferLimitException.class)); + } + + @Test + void maxInMemoryLimitDoesNotApplyToParsedItemsThatDontRequireBuffering() { + Flux input = Flux.just( + stringBuffer("TOO MUCH DATA\nanother line\n\nand another\n")); + + this.decoder.setMaxInMemorySize(5); + + testDecode(input, String.class, step -> step + .expectNext("TOO MUCH DATA") + .expectNext("another line") + .expectNext("") + .expectNext("and another") + .expectComplete() + .verify()); + } + + @Test // gh-24339 + void maxInMemoryLimitReleaseUnprocessedLinesWhenUnlimited() { + Flux input = Flux.just(stringBuffer("Line 1\nLine 2\nLine 3\n")); + + this.decoder.setMaxInMemorySize(-1); + testDecodeCancel(input, ResolvableType.forClass(String.class), null, Collections.emptyMap()); + } + + @Test + void decodeNewLineIncludeDelimiters() { + this.decoder = StringDecoder.allMimeTypes(StringDecoder.DEFAULT_DELIMITERS, false); + + Flux input = Flux.just( + stringBuffer("\r\nabc\n"), + stringBuffer("def"), + stringBuffer("ghi\r\n\n"), + stringBuffer("jkl"), + stringBuffer("mno\npqr\n"), + stringBuffer("stu"), + stringBuffer("vw"), + stringBuffer("xyz") + ); + + testDecode(input, String.class, step -> step + .expectNext("\r\n") + .expectNext("abc\n") + .expectNext("defghi\r\n") + .expectNext("\n") + .expectNext("jklmno\n") + .expectNext("pqr\n") + .expectNext("stuvwxyz") + .expectComplete() + .verify()); + } + + @Test + void decodeEmptyFlux() { + Flux input = Flux.empty(); + + testDecode(input, String.class, step -> step + .expectComplete() + .verify()); + } + + @Test + void decodeEmptyDataBuffer() { + Flux input = Flux.just(stringBuffer("")); + Flux output = this.decoder.decode(input, + TYPE, null, Collections.emptyMap()); + + StepVerifier.create(output) + .expectNext("") + .expectComplete().verify(); + + } + + @Override + @Test + public void decodeToMono() { + Flux input = Flux.just( + stringBuffer("foo"), + stringBuffer("bar"), + stringBuffer("baz")); + + testDecodeToMonoAll(input, String.class, step -> step + .expectNext("foobarbaz") + .expectComplete() + .verify()); + } + + @Test + void decodeToMonoWithEmptyFlux() { + Flux input = Flux.empty(); + + testDecodeToMono(input, String.class, step -> step + .expectComplete() + .verify()); + } + + private DataBuffer stringBuffer(String value) { + byte[] bytes = value.getBytes(StandardCharsets.UTF_8); + DataBuffer buffer = this.bufferFactory.allocateBuffer(bytes.length); + buffer.write(bytes); + return buffer; + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/convert/TypeDescriptorTests.java b/spring-core/src/test/java/org/springframework/core/convert/TypeDescriptorTests.java new file mode 100644 index 0000000..e77ffaa --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/convert/TypeDescriptorTests.java @@ -0,0 +1,1024 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.MethodParameter; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link TypeDescriptor}. + * + * @author Keith Donald + * @author Andy Clement + * @author Phillip Webb + * @author Sam Brannen + * @author Nathan Piper + */ +@SuppressWarnings("rawtypes") +class TypeDescriptorTests { + + @Test + void parameterPrimitive() throws Exception { + TypeDescriptor desc = new TypeDescriptor(new MethodParameter(getClass().getMethod("testParameterPrimitive", int.class), 0)); + assertThat(desc.getType()).isEqualTo(int.class); + assertThat(desc.getObjectType()).isEqualTo(Integer.class); + assertThat(desc.getName()).isEqualTo("int"); + assertThat(desc.toString()).isEqualTo("int"); + assertThat(desc.isPrimitive()).isTrue(); + assertThat(desc.getAnnotations().length).isEqualTo(0); + assertThat(desc.isCollection()).isFalse(); + assertThat(desc.isMap()).isFalse(); + } + + @Test + void parameterScalar() throws Exception { + TypeDescriptor desc = new TypeDescriptor(new MethodParameter(getClass().getMethod("testParameterScalar", String.class), 0)); + assertThat(desc.getType()).isEqualTo(String.class); + assertThat(desc.getObjectType()).isEqualTo(String.class); + assertThat(desc.getName()).isEqualTo("java.lang.String"); + assertThat(desc.toString()).isEqualTo("java.lang.String"); + assertThat(!desc.isPrimitive()).isTrue(); + assertThat(desc.getAnnotations().length).isEqualTo(0); + assertThat(desc.isCollection()).isFalse(); + assertThat(desc.isArray()).isFalse(); + assertThat(desc.isMap()).isFalse(); + } + + @Test + void parameterList() throws Exception { + MethodParameter methodParameter = new MethodParameter(getClass().getMethod("testParameterList", List.class), 0); + TypeDescriptor desc = new TypeDescriptor(methodParameter); + assertThat(desc.getType()).isEqualTo(List.class); + assertThat(desc.getObjectType()).isEqualTo(List.class); + assertThat(desc.getName()).isEqualTo("java.util.List"); + assertThat(desc.toString()).isEqualTo("java.util.List>>>"); + assertThat(!desc.isPrimitive()).isTrue(); + assertThat(desc.getAnnotations().length).isEqualTo(0); + assertThat(desc.isCollection()).isTrue(); + assertThat(desc.isArray()).isFalse(); + assertThat(desc.getElementTypeDescriptor().getType()).isEqualTo(List.class); + assertThat(desc.getElementTypeDescriptor()).isEqualTo(TypeDescriptor.nested(methodParameter, 1)); + assertThat(desc.getElementTypeDescriptor().getElementTypeDescriptor()).isEqualTo(TypeDescriptor.nested(methodParameter, 2)); + assertThat(desc.getElementTypeDescriptor().getElementTypeDescriptor().getMapValueTypeDescriptor()).isEqualTo(TypeDescriptor.nested(methodParameter, 3)); + assertThat(desc.getElementTypeDescriptor().getElementTypeDescriptor().getMapKeyTypeDescriptor().getType()).isEqualTo(Integer.class); + assertThat(desc.getElementTypeDescriptor().getElementTypeDescriptor().getMapValueTypeDescriptor().getType()).isEqualTo(Enum.class); + assertThat(desc.isMap()).isFalse(); + } + + @Test + void parameterListNoParamTypes() throws Exception { + MethodParameter methodParameter = new MethodParameter(getClass().getMethod("testParameterListNoParamTypes", List.class), 0); + TypeDescriptor desc = new TypeDescriptor(methodParameter); + assertThat(desc.getType()).isEqualTo(List.class); + assertThat(desc.getObjectType()).isEqualTo(List.class); + assertThat(desc.getName()).isEqualTo("java.util.List"); + assertThat(desc.toString()).isEqualTo("java.util.List"); + assertThat(!desc.isPrimitive()).isTrue(); + assertThat(desc.getAnnotations().length).isEqualTo(0); + assertThat(desc.isCollection()).isTrue(); + assertThat(desc.isArray()).isFalse(); + assertThat((Object) desc.getElementTypeDescriptor()).isNull(); + assertThat(desc.isMap()).isFalse(); + } + + @Test + void parameterArray() throws Exception { + MethodParameter methodParameter = new MethodParameter(getClass().getMethod("testParameterArray", Integer[].class), 0); + TypeDescriptor desc = new TypeDescriptor(methodParameter); + assertThat(desc.getType()).isEqualTo(Integer[].class); + assertThat(desc.getObjectType()).isEqualTo(Integer[].class); + assertThat(desc.getName()).isEqualTo("java.lang.Integer[]"); + assertThat(desc.toString()).isEqualTo("java.lang.Integer[]"); + assertThat(!desc.isPrimitive()).isTrue(); + assertThat(desc.getAnnotations().length).isEqualTo(0); + assertThat(desc.isCollection()).isFalse(); + assertThat(desc.isArray()).isTrue(); + assertThat(desc.getElementTypeDescriptor().getType()).isEqualTo(Integer.class); + assertThat(desc.getElementTypeDescriptor()).isEqualTo(TypeDescriptor.valueOf(Integer.class)); + assertThat(desc.isMap()).isFalse(); + } + + @Test + void parameterMap() throws Exception { + MethodParameter methodParameter = new MethodParameter(getClass().getMethod("testParameterMap", Map.class), 0); + TypeDescriptor desc = new TypeDescriptor(methodParameter); + assertThat(desc.getType()).isEqualTo(Map.class); + assertThat(desc.getObjectType()).isEqualTo(Map.class); + assertThat(desc.getName()).isEqualTo("java.util.Map"); + assertThat(desc.toString()).isEqualTo("java.util.Map>"); + assertThat(!desc.isPrimitive()).isTrue(); + assertThat(desc.getAnnotations().length).isEqualTo(0); + assertThat(desc.isCollection()).isFalse(); + assertThat(desc.isArray()).isFalse(); + assertThat(desc.isMap()).isTrue(); + assertThat(desc.getMapValueTypeDescriptor()).isEqualTo(TypeDescriptor.nested(methodParameter, 1)); + assertThat(desc.getMapValueTypeDescriptor().getElementTypeDescriptor()).isEqualTo(TypeDescriptor.nested(methodParameter, 2)); + assertThat(desc.getMapKeyTypeDescriptor().getType()).isEqualTo(Integer.class); + assertThat(desc.getMapValueTypeDescriptor().getType()).isEqualTo(List.class); + assertThat(desc.getMapValueTypeDescriptor().getElementTypeDescriptor().getType()).isEqualTo(String.class); + } + + @Test + void parameterAnnotated() throws Exception { + TypeDescriptor t1 = new TypeDescriptor(new MethodParameter(getClass().getMethod("testAnnotatedMethod", String.class), 0)); + assertThat(t1.getType()).isEqualTo(String.class); + assertThat(t1.getAnnotations().length).isEqualTo(1); + assertThat(t1.getAnnotation(ParameterAnnotation.class)).isNotNull(); + assertThat(t1.hasAnnotation(ParameterAnnotation.class)).isTrue(); + assertThat(t1.getAnnotation(ParameterAnnotation.class).value()).isEqualTo(123); + } + + @Test + void getAnnotationsReturnsClonedArray() throws Exception { + TypeDescriptor t = new TypeDescriptor(new MethodParameter(getClass().getMethod("testAnnotatedMethod", String.class), 0)); + t.getAnnotations()[0] = null; + assertThat(t.getAnnotations()[0]).isNotNull(); + } + + @Test + void propertyComplex() throws Exception { + Property property = new Property(getClass(), getClass().getMethod("getComplexProperty"), + getClass().getMethod("setComplexProperty", Map.class)); + TypeDescriptor desc = new TypeDescriptor(property); + assertThat(desc.getMapKeyTypeDescriptor().getType()).isEqualTo(String.class); + assertThat(desc.getMapValueTypeDescriptor().getElementTypeDescriptor().getElementTypeDescriptor().getType()).isEqualTo(Integer.class); + } + + @Test + void propertyGenericType() throws Exception { + GenericType genericBean = new IntegerType(); + Property property = new Property(getClass(), genericBean.getClass().getMethod("getProperty"), + genericBean.getClass().getMethod("setProperty", Integer.class)); + TypeDescriptor desc = new TypeDescriptor(property); + assertThat(desc.getType()).isEqualTo(Integer.class); + } + + @Test + void propertyTypeCovariance() throws Exception { + GenericType genericBean = new NumberType(); + Property property = new Property(getClass(), genericBean.getClass().getMethod("getProperty"), + genericBean.getClass().getMethod("setProperty", Number.class)); + TypeDescriptor desc = new TypeDescriptor(property); + assertThat(desc.getType()).isEqualTo(Integer.class); + } + + @Test + void propertyGenericTypeList() throws Exception { + GenericType genericBean = new IntegerType(); + Property property = new Property(getClass(), genericBean.getClass().getMethod("getListProperty"), + genericBean.getClass().getMethod("setListProperty", List.class)); + TypeDescriptor desc = new TypeDescriptor(property); + assertThat(desc.getType()).isEqualTo(List.class); + assertThat(desc.getElementTypeDescriptor().getType()).isEqualTo(Integer.class); + } + + @Test + void propertyGenericClassList() throws Exception { + IntegerClass genericBean = new IntegerClass(); + Property property = new Property(genericBean.getClass(), genericBean.getClass().getMethod("getListProperty"), + genericBean.getClass().getMethod("setListProperty", List.class)); + TypeDescriptor desc = new TypeDescriptor(property); + assertThat(desc.getType()).isEqualTo(List.class); + assertThat(desc.getElementTypeDescriptor().getType()).isEqualTo(Integer.class); + assertThat(desc.getAnnotation(MethodAnnotation1.class)).isNotNull(); + assertThat(desc.hasAnnotation(MethodAnnotation1.class)).isTrue(); + } + + @Test + void property() throws Exception { + Property property = new Property( + getClass(), getClass().getMethod("getProperty"), getClass().getMethod("setProperty", Map.class)); + TypeDescriptor desc = new TypeDescriptor(property); + assertThat(desc.getType()).isEqualTo(Map.class); + assertThat(desc.getMapKeyTypeDescriptor().getElementTypeDescriptor().getType()).isEqualTo(Integer.class); + assertThat(desc.getMapValueTypeDescriptor().getElementTypeDescriptor().getType()).isEqualTo(Long.class); + assertThat(desc.getAnnotation(MethodAnnotation1.class)).isNotNull(); + assertThat(desc.getAnnotation(MethodAnnotation2.class)).isNotNull(); + assertThat(desc.getAnnotation(MethodAnnotation3.class)).isNotNull(); + } + + @Test + void getAnnotationOnMethodThatIsLocallyAnnotated() throws Exception { + assertAnnotationFoundOnMethod(MethodAnnotation1.class, "methodWithLocalAnnotation"); + } + + @Test + void getAnnotationOnMethodThatIsMetaAnnotated() throws Exception { + assertAnnotationFoundOnMethod(MethodAnnotation1.class, "methodWithComposedAnnotation"); + } + + @Test + void getAnnotationOnMethodThatIsMetaMetaAnnotated() throws Exception { + assertAnnotationFoundOnMethod(MethodAnnotation1.class, "methodWithComposedComposedAnnotation"); + } + + private void assertAnnotationFoundOnMethod(Class annotationType, String methodName) throws Exception { + TypeDescriptor typeDescriptor = new TypeDescriptor(new MethodParameter(getClass().getMethod(methodName), -1)); + assertThat(typeDescriptor.getAnnotation(annotationType)).as("Should have found @" + annotationType.getSimpleName() + " on " + methodName + ".").isNotNull(); + } + + @Test + void fieldScalar() throws Exception { + TypeDescriptor typeDescriptor = new TypeDescriptor(getClass().getField("fieldScalar")); + assertThat(typeDescriptor.isPrimitive()).isFalse(); + assertThat(typeDescriptor.isArray()).isFalse(); + assertThat(typeDescriptor.isCollection()).isFalse(); + assertThat(typeDescriptor.isMap()).isFalse(); + assertThat(typeDescriptor.getType()).isEqualTo(Integer.class); + assertThat(typeDescriptor.getObjectType()).isEqualTo(Integer.class); + } + + @Test + void fieldList() throws Exception { + TypeDescriptor typeDescriptor = new TypeDescriptor(TypeDescriptorTests.class.getDeclaredField("listOfString")); + assertThat(typeDescriptor.isArray()).isFalse(); + assertThat(typeDescriptor.getType()).isEqualTo(List.class); + assertThat(typeDescriptor.getElementTypeDescriptor().getType()).isEqualTo(String.class); + assertThat(typeDescriptor.toString()).isEqualTo("java.util.List"); + } + + @Test + void fieldListOfListOfString() throws Exception { + TypeDescriptor typeDescriptor = new TypeDescriptor(TypeDescriptorTests.class.getDeclaredField("listOfListOfString")); + assertThat(typeDescriptor.isArray()).isFalse(); + assertThat(typeDescriptor.getType()).isEqualTo(List.class); + assertThat(typeDescriptor.getElementTypeDescriptor().getType()).isEqualTo(List.class); + assertThat(typeDescriptor.getElementTypeDescriptor().getElementTypeDescriptor().getType()).isEqualTo(String.class); + assertThat(typeDescriptor.toString()).isEqualTo("java.util.List>"); + } + + @Test + void fieldListOfListUnknown() throws Exception { + TypeDescriptor typeDescriptor = new TypeDescriptor(TypeDescriptorTests.class.getDeclaredField("listOfListOfUnknown")); + assertThat(typeDescriptor.isArray()).isFalse(); + assertThat(typeDescriptor.getType()).isEqualTo(List.class); + assertThat(typeDescriptor.getElementTypeDescriptor().getType()).isEqualTo(List.class); + assertThat(typeDescriptor.getElementTypeDescriptor().getElementTypeDescriptor()).isNull(); + assertThat(typeDescriptor.toString()).isEqualTo("java.util.List>"); + } + + @Test + void fieldArray() throws Exception { + TypeDescriptor typeDescriptor = new TypeDescriptor(TypeDescriptorTests.class.getDeclaredField("intArray")); + assertThat(typeDescriptor.isArray()).isTrue(); + assertThat(typeDescriptor.getElementTypeDescriptor().getType()).isEqualTo(Integer.TYPE); + assertThat(typeDescriptor.toString()).isEqualTo("int[]"); + } + + @Test + void fieldComplexTypeDescriptor() throws Exception { + TypeDescriptor typeDescriptor = new TypeDescriptor(TypeDescriptorTests.class.getDeclaredField("arrayOfListOfString")); + assertThat(typeDescriptor.isArray()).isTrue(); + assertThat(typeDescriptor.getElementTypeDescriptor().getType()).isEqualTo(List.class); + assertThat(typeDescriptor.getElementTypeDescriptor().getElementTypeDescriptor().getType()).isEqualTo(String.class); + assertThat(typeDescriptor.toString()).isEqualTo("java.util.List[]"); + } + + @Test + void fieldComplexTypeDescriptor2() throws Exception { + TypeDescriptor typeDescriptor = new TypeDescriptor(TypeDescriptorTests.class.getDeclaredField("nestedMapField")); + assertThat(typeDescriptor.isMap()).isTrue(); + assertThat(typeDescriptor.getMapKeyTypeDescriptor().getType()).isEqualTo(String.class); + assertThat(typeDescriptor.getMapValueTypeDescriptor().getType()).isEqualTo(List.class); + assertThat(typeDescriptor.getMapValueTypeDescriptor().getElementTypeDescriptor().getType()).isEqualTo(Integer.class); + assertThat(typeDescriptor.toString()).isEqualTo("java.util.Map>"); + } + + @Test + void fieldMap() throws Exception { + TypeDescriptor desc = new TypeDescriptor(TypeDescriptorTests.class.getField("fieldMap")); + assertThat(desc.isMap()).isTrue(); + assertThat(desc.getMapKeyTypeDescriptor().getElementTypeDescriptor().getType()).isEqualTo(Integer.class); + assertThat(desc.getMapValueTypeDescriptor().getElementTypeDescriptor().getType()).isEqualTo(Long.class); + } + + @Test + void fieldAnnotated() throws Exception { + TypeDescriptor typeDescriptor = new TypeDescriptor(getClass().getField("fieldAnnotated")); + assertThat(typeDescriptor.getAnnotations().length).isEqualTo(1); + assertThat(typeDescriptor.getAnnotation(FieldAnnotation.class)).isNotNull(); + } + + @Test + void valueOfScalar() { + TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(Integer.class); + assertThat(typeDescriptor.isPrimitive()).isFalse(); + assertThat(typeDescriptor.isArray()).isFalse(); + assertThat(typeDescriptor.isCollection()).isFalse(); + assertThat(typeDescriptor.isMap()).isFalse(); + assertThat(typeDescriptor.getType()).isEqualTo(Integer.class); + assertThat(typeDescriptor.getObjectType()).isEqualTo(Integer.class); + } + + @Test + void valueOfPrimitive() { + TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(int.class); + assertThat(typeDescriptor.isPrimitive()).isTrue(); + assertThat(typeDescriptor.isArray()).isFalse(); + assertThat(typeDescriptor.isCollection()).isFalse(); + assertThat(typeDescriptor.isMap()).isFalse(); + assertThat(typeDescriptor.getType()).isEqualTo(Integer.TYPE); + assertThat(typeDescriptor.getObjectType()).isEqualTo(Integer.class); + } + + @Test + void valueOfArray() throws Exception { + TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(int[].class); + assertThat(typeDescriptor.isArray()).isTrue(); + assertThat(typeDescriptor.isCollection()).isFalse(); + assertThat(typeDescriptor.isMap()).isFalse(); + assertThat(typeDescriptor.getElementTypeDescriptor().getType()).isEqualTo(Integer.TYPE); + } + + @Test + void valueOfCollection() throws Exception { + TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(Collection.class); + assertThat(typeDescriptor.isCollection()).isTrue(); + assertThat(typeDescriptor.isArray()).isFalse(); + assertThat(typeDescriptor.isMap()).isFalse(); + assertThat((Object) typeDescriptor.getElementTypeDescriptor()).isNull(); + } + + @Test + void forObject() { + TypeDescriptor desc = TypeDescriptor.forObject("3"); + assertThat(desc.getType()).isEqualTo(String.class); + } + + @Test + void forObjectNullTypeDescriptor() { + TypeDescriptor desc = TypeDescriptor.forObject(null); + assertThat((Object) desc).isNull(); + } + + @Test + void nestedMethodParameterType2Levels() throws Exception { + TypeDescriptor t1 = TypeDescriptor.nested(new MethodParameter(getClass().getMethod("test2", List.class), 0), 2); + assertThat(t1.getType()).isEqualTo(String.class); + } + + @Test + void nestedMethodParameterTypeMap() throws Exception { + TypeDescriptor t1 = TypeDescriptor.nested(new MethodParameter(getClass().getMethod("test3", Map.class), 0), 1); + assertThat(t1.getType()).isEqualTo(String.class); + } + + @Test + void nestedMethodParameterTypeMapTwoLevels() throws Exception { + TypeDescriptor t1 = TypeDescriptor.nested(new MethodParameter(getClass().getMethod("test4", List.class), 0), 2); + assertThat(t1.getType()).isEqualTo(String.class); + } + + @Test + void nestedMethodParameterNot1NestedLevel() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + TypeDescriptor.nested(new MethodParameter(getClass().getMethod("test4", List.class), 0, 2), 2)); + } + + @Test + void nestedTooManyLevels() throws Exception { + TypeDescriptor t1 = TypeDescriptor.nested(new MethodParameter(getClass().getMethod("test4", List.class), 0), 3); + assertThat((Object) t1).isNull(); + } + + @Test + void nestedMethodParameterTypeNotNestable() throws Exception { + TypeDescriptor t1 = TypeDescriptor.nested(new MethodParameter(getClass().getMethod("test5", String.class), 0), 2); + assertThat((Object) t1).isNull(); + } + + @Test + void nestedMethodParameterTypeInvalidNestingLevel() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + TypeDescriptor.nested(new MethodParameter(getClass().getMethod("test5", String.class), 0, 2), 2)); + } + + @Test + void nestedNotParameterized() throws Exception { + TypeDescriptor t1 = TypeDescriptor.nested(new MethodParameter(getClass().getMethod("test6", List.class), 0), 1); + assertThat(t1.getType()).isEqualTo(List.class); + assertThat(t1.toString()).isEqualTo("java.util.List"); + TypeDescriptor t2 = TypeDescriptor.nested(new MethodParameter(getClass().getMethod("test6", List.class), 0), 2); + assertThat((Object) t2).isNull(); + } + + @Test + void nestedFieldTypeMapTwoLevels() throws Exception { + TypeDescriptor t1 = TypeDescriptor.nested(getClass().getField("test4"), 2); + assertThat(t1.getType()).isEqualTo(String.class); + } + + @Test + void nestedPropertyTypeMapTwoLevels() throws Exception { + Property property = new Property(getClass(), getClass().getMethod("getTest4"), getClass().getMethod("setTest4", List.class)); + TypeDescriptor t1 = TypeDescriptor.nested(property, 2); + assertThat(t1.getType()).isEqualTo(String.class); + } + + @Test + void collection() { + TypeDescriptor desc = TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(Integer.class)); + assertThat(desc.getType()).isEqualTo(List.class); + assertThat(desc.getObjectType()).isEqualTo(List.class); + assertThat(desc.getName()).isEqualTo("java.util.List"); + assertThat(desc.toString()).isEqualTo("java.util.List"); + assertThat(!desc.isPrimitive()).isTrue(); + assertThat(desc.getAnnotations().length).isEqualTo(0); + assertThat(desc.isCollection()).isTrue(); + assertThat(desc.isArray()).isFalse(); + assertThat(desc.getElementTypeDescriptor().getType()).isEqualTo(Integer.class); + assertThat(desc.getElementTypeDescriptor()).isEqualTo(TypeDescriptor.valueOf(Integer.class)); + assertThat(desc.isMap()).isFalse(); + } + + @Test + void collectionNested() { + TypeDescriptor desc = TypeDescriptor.collection(List.class, TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(Integer.class))); + assertThat(desc.getType()).isEqualTo(List.class); + assertThat(desc.getObjectType()).isEqualTo(List.class); + assertThat(desc.getName()).isEqualTo("java.util.List"); + assertThat(desc.toString()).isEqualTo("java.util.List>"); + assertThat(!desc.isPrimitive()).isTrue(); + assertThat(desc.getAnnotations().length).isEqualTo(0); + assertThat(desc.isCollection()).isTrue(); + assertThat(desc.isArray()).isFalse(); + assertThat(desc.getElementTypeDescriptor().getType()).isEqualTo(List.class); + assertThat(desc.getElementTypeDescriptor().getElementTypeDescriptor()).isEqualTo(TypeDescriptor.valueOf(Integer.class)); + assertThat(desc.isMap()).isFalse(); + } + + @Test + void map() { + TypeDescriptor desc = TypeDescriptor.map(Map.class, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)); + assertThat(desc.getType()).isEqualTo(Map.class); + assertThat(desc.getObjectType()).isEqualTo(Map.class); + assertThat(desc.getName()).isEqualTo("java.util.Map"); + assertThat(desc.toString()).isEqualTo("java.util.Map"); + assertThat(!desc.isPrimitive()).isTrue(); + assertThat(desc.getAnnotations().length).isEqualTo(0); + assertThat(desc.isCollection()).isFalse(); + assertThat(desc.isArray()).isFalse(); + assertThat(desc.isMap()).isTrue(); + assertThat(desc.getMapKeyTypeDescriptor().getType()).isEqualTo(String.class); + assertThat(desc.getMapValueTypeDescriptor().getType()).isEqualTo(Integer.class); + } + + @Test + void mapNested() { + TypeDescriptor desc = TypeDescriptor.map(Map.class, TypeDescriptor.valueOf(String.class), + TypeDescriptor.map(Map.class, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class))); + assertThat(desc.getType()).isEqualTo(Map.class); + assertThat(desc.getObjectType()).isEqualTo(Map.class); + assertThat(desc.getName()).isEqualTo("java.util.Map"); + assertThat(desc.toString()).isEqualTo("java.util.Map>"); + assertThat(!desc.isPrimitive()).isTrue(); + assertThat(desc.getAnnotations().length).isEqualTo(0); + assertThat(desc.isCollection()).isFalse(); + assertThat(desc.isArray()).isFalse(); + assertThat(desc.isMap()).isTrue(); + assertThat(desc.getMapKeyTypeDescriptor().getType()).isEqualTo(String.class); + assertThat(desc.getMapValueTypeDescriptor().getMapKeyTypeDescriptor().getType()).isEqualTo(String.class); + assertThat(desc.getMapValueTypeDescriptor().getMapValueTypeDescriptor().getType()).isEqualTo(Integer.class); + } + + @Test + void narrow() { + TypeDescriptor desc = TypeDescriptor.valueOf(Number.class); + Integer value = Integer.valueOf(3); + desc = desc.narrow(value); + assertThat(desc.getType()).isEqualTo(Integer.class); + } + + @Test + void elementType() { + TypeDescriptor desc = TypeDescriptor.valueOf(List.class); + Integer value = Integer.valueOf(3); + desc = desc.elementTypeDescriptor(value); + assertThat(desc.getType()).isEqualTo(Integer.class); + } + + @Test + void elementTypePreserveContext() throws Exception { + TypeDescriptor desc = new TypeDescriptor(getClass().getField("listPreserveContext")); + assertThat(desc.getElementTypeDescriptor().getElementTypeDescriptor().getType()).isEqualTo(Integer.class); + List value = new ArrayList<>(3); + desc = desc.elementTypeDescriptor(value); + assertThat(desc.getElementTypeDescriptor().getType()).isEqualTo(Integer.class); + assertThat(desc.getAnnotation(FieldAnnotation.class)).isNotNull(); + } + + @Test + void mapKeyType() { + TypeDescriptor desc = TypeDescriptor.valueOf(Map.class); + Integer value = Integer.valueOf(3); + desc = desc.getMapKeyTypeDescriptor(value); + assertThat(desc.getType()).isEqualTo(Integer.class); + } + + @Test + void mapKeyTypePreserveContext() throws Exception { + TypeDescriptor desc = new TypeDescriptor(getClass().getField("mapPreserveContext")); + assertThat(desc.getMapKeyTypeDescriptor().getElementTypeDescriptor().getType()).isEqualTo(Integer.class); + List value = new ArrayList<>(3); + desc = desc.getMapKeyTypeDescriptor(value); + assertThat(desc.getElementTypeDescriptor().getType()).isEqualTo(Integer.class); + assertThat(desc.getAnnotation(FieldAnnotation.class)).isNotNull(); + } + + @Test + void mapValueType() { + TypeDescriptor desc = TypeDescriptor.valueOf(Map.class); + Integer value = Integer.valueOf(3); + desc = desc.getMapValueTypeDescriptor(value); + assertThat(desc.getType()).isEqualTo(Integer.class); + } + + @Test + void mapValueTypePreserveContext() throws Exception { + TypeDescriptor desc = new TypeDescriptor(getClass().getField("mapPreserveContext")); + assertThat(desc.getMapValueTypeDescriptor().getElementTypeDescriptor().getType()).isEqualTo(Integer.class); + List value = new ArrayList<>(3); + desc = desc.getMapValueTypeDescriptor(value); + assertThat(desc.getElementTypeDescriptor().getType()).isEqualTo(Integer.class); + assertThat(desc.getAnnotation(FieldAnnotation.class)).isNotNull(); + } + + @Test + void equality() throws Exception { + TypeDescriptor t1 = TypeDescriptor.valueOf(String.class); + TypeDescriptor t2 = TypeDescriptor.valueOf(String.class); + TypeDescriptor t3 = TypeDescriptor.valueOf(Date.class); + TypeDescriptor t4 = TypeDescriptor.valueOf(Date.class); + TypeDescriptor t5 = TypeDescriptor.valueOf(List.class); + TypeDescriptor t6 = TypeDescriptor.valueOf(List.class); + TypeDescriptor t7 = TypeDescriptor.valueOf(Map.class); + TypeDescriptor t8 = TypeDescriptor.valueOf(Map.class); + assertThat(t2).isEqualTo(t1); + assertThat(t4).isEqualTo(t3); + assertThat(t6).isEqualTo(t5); + assertThat(t8).isEqualTo(t7); + + TypeDescriptor t9 = new TypeDescriptor(getClass().getField("listField")); + TypeDescriptor t10 = new TypeDescriptor(getClass().getField("listField")); + assertThat(t10).isEqualTo(t9); + + TypeDescriptor t11 = new TypeDescriptor(getClass().getField("mapField")); + TypeDescriptor t12 = new TypeDescriptor(getClass().getField("mapField")); + assertThat(t12).isEqualTo(t11); + + MethodParameter testAnnotatedMethod = new MethodParameter(getClass().getMethod("testAnnotatedMethod", String.class), 0); + TypeDescriptor t13 = new TypeDescriptor(testAnnotatedMethod); + TypeDescriptor t14 = new TypeDescriptor(testAnnotatedMethod); + assertThat(t14).isEqualTo(t13); + + TypeDescriptor t15 = new TypeDescriptor(testAnnotatedMethod); + TypeDescriptor t16 = new TypeDescriptor(new MethodParameter(getClass().getMethod("testAnnotatedMethodDifferentAnnotationValue", String.class), 0)); + assertThat(t16).isNotEqualTo(t15); + + TypeDescriptor t17 = new TypeDescriptor(testAnnotatedMethod); + TypeDescriptor t18 = new TypeDescriptor(new MethodParameter(getClass().getMethod("test5", String.class), 0)); + assertThat(t18).isNotEqualTo(t17); + } + + @Test + void isAssignableTypes() { + assertThat(TypeDescriptor.valueOf(Integer.class).isAssignableTo(TypeDescriptor.valueOf(Number.class))).isTrue(); + assertThat(TypeDescriptor.valueOf(Number.class).isAssignableTo(TypeDescriptor.valueOf(Integer.class))).isFalse(); + assertThat(TypeDescriptor.valueOf(String.class).isAssignableTo(TypeDescriptor.valueOf(String[].class))).isFalse(); + } + + @Test + void isAssignableElementTypes() throws Exception { + assertThat(new TypeDescriptor(getClass().getField("listField")).isAssignableTo(new TypeDescriptor(getClass().getField("listField")))).isTrue(); + assertThat(new TypeDescriptor(getClass().getField("notGenericList")).isAssignableTo(new TypeDescriptor(getClass().getField("listField")))).isTrue(); + assertThat(new TypeDescriptor(getClass().getField("listField")).isAssignableTo(new TypeDescriptor(getClass().getField("notGenericList")))).isTrue(); + assertThat(new TypeDescriptor(getClass().getField("isAssignableElementTypes")).isAssignableTo(new TypeDescriptor(getClass().getField("listField")))).isFalse(); + assertThat(TypeDescriptor.valueOf(List.class).isAssignableTo(new TypeDescriptor(getClass().getField("listField")))).isTrue(); + } + + @Test + void isAssignableMapKeyValueTypes() throws Exception { + assertThat(new TypeDescriptor(getClass().getField("mapField")).isAssignableTo(new TypeDescriptor(getClass().getField("mapField")))).isTrue(); + assertThat(new TypeDescriptor(getClass().getField("notGenericMap")).isAssignableTo(new TypeDescriptor(getClass().getField("mapField")))).isTrue(); + assertThat(new TypeDescriptor(getClass().getField("mapField")).isAssignableTo(new TypeDescriptor(getClass().getField("notGenericMap")))).isTrue(); + assertThat(new TypeDescriptor(getClass().getField("isAssignableMapKeyValueTypes")).isAssignableTo(new TypeDescriptor(getClass().getField("mapField")))).isFalse(); + assertThat(TypeDescriptor.valueOf(Map.class).isAssignableTo(new TypeDescriptor(getClass().getField("mapField")))).isTrue(); + } + + @Test + void multiValueMap() throws Exception { + TypeDescriptor td = new TypeDescriptor(getClass().getField("multiValueMap")); + assertThat(td.isMap()).isTrue(); + assertThat(td.getMapKeyTypeDescriptor().getType()).isEqualTo(String.class); + assertThat(td.getMapValueTypeDescriptor().getType()).isEqualTo(List.class); + assertThat(td.getMapValueTypeDescriptor().getElementTypeDescriptor().getType()).isEqualTo(Integer.class); + } + + @Test + void passDownGeneric() throws Exception { + TypeDescriptor td = new TypeDescriptor(getClass().getField("passDownGeneric")); + assertThat(td.getElementTypeDescriptor().getType()).isEqualTo(List.class); + assertThat(td.getElementTypeDescriptor().getElementTypeDescriptor().getType()).isEqualTo(Set.class); + assertThat(td.getElementTypeDescriptor().getElementTypeDescriptor().getElementTypeDescriptor().getType()).isEqualTo(Integer.class); + } + + @Test + void upCast() throws Exception { + Property property = new Property(getClass(), getClass().getMethod("getProperty"), + getClass().getMethod("setProperty", Map.class)); + TypeDescriptor typeDescriptor = new TypeDescriptor(property); + TypeDescriptor upCast = typeDescriptor.upcast(Object.class); + assertThat(upCast.getAnnotation(MethodAnnotation1.class) != null).isTrue(); + } + + @Test + void upCastNotSuper() throws Exception { + Property property = new Property(getClass(), getClass().getMethod("getProperty"), + getClass().getMethod("setProperty", Map.class)); + TypeDescriptor typeDescriptor = new TypeDescriptor(property); + assertThatIllegalArgumentException().isThrownBy(() -> + typeDescriptor.upcast(Collection.class)) + .withMessage("interface java.util.Map is not assignable to interface java.util.Collection"); + } + + @Test + void elementTypeForCollectionSubclass() throws Exception { + @SuppressWarnings("serial") + class CustomSet extends HashSet { + } + + assertThat(TypeDescriptor.valueOf(String.class)).isEqualTo(TypeDescriptor.valueOf(CustomSet.class).getElementTypeDescriptor()); + assertThat(TypeDescriptor.valueOf(String.class)).isEqualTo(TypeDescriptor.forObject(new CustomSet()).getElementTypeDescriptor()); + } + + @Test + void elementTypeForMapSubclass() throws Exception { + @SuppressWarnings("serial") + class CustomMap extends HashMap { + } + + assertThat(TypeDescriptor.valueOf(String.class)).isEqualTo(TypeDescriptor.valueOf(CustomMap.class).getMapKeyTypeDescriptor()); + assertThat(TypeDescriptor.valueOf(Integer.class)).isEqualTo(TypeDescriptor.valueOf(CustomMap.class).getMapValueTypeDescriptor()); + assertThat(TypeDescriptor.valueOf(String.class)).isEqualTo(TypeDescriptor.forObject(new CustomMap()).getMapKeyTypeDescriptor()); + assertThat(TypeDescriptor.valueOf(Integer.class)).isEqualTo(TypeDescriptor.forObject(new CustomMap()).getMapValueTypeDescriptor()); + } + + @Test + void createMapArray() throws Exception { + TypeDescriptor mapType = TypeDescriptor.map( + LinkedHashMap.class, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)); + TypeDescriptor arrayType = TypeDescriptor.array(mapType); + assertThat(LinkedHashMap[].class).isEqualTo(arrayType.getType()); + assertThat(mapType).isEqualTo(arrayType.getElementTypeDescriptor()); + } + + @Test + void createStringArray() throws Exception { + TypeDescriptor arrayType = TypeDescriptor.array(TypeDescriptor.valueOf(String.class)); + assertThat(TypeDescriptor.valueOf(String[].class)).isEqualTo(arrayType); + } + + @Test + void createNullArray() throws Exception { + assertThat((Object) TypeDescriptor.array(null)).isNull(); + } + + @Test + void serializable() throws Exception { + TypeDescriptor typeDescriptor = TypeDescriptor.forObject(""); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ObjectOutputStream outputStream = new ObjectOutputStream(out); + outputStream.writeObject(typeDescriptor); + ObjectInputStream inputStream = new ObjectInputStream(new ByteArrayInputStream( + out.toByteArray())); + TypeDescriptor readObject = (TypeDescriptor) inputStream.readObject(); + assertThat(readObject).isEqualTo(typeDescriptor); + } + + @Test + void createCollectionWithNullElement() throws Exception { + TypeDescriptor typeDescriptor = TypeDescriptor.collection(List.class, null); + assertThat(typeDescriptor.getElementTypeDescriptor()).isNull(); + } + + @Test + void createMapWithNullElements() throws Exception { + TypeDescriptor typeDescriptor = TypeDescriptor.map(LinkedHashMap.class, null, null); + assertThat(typeDescriptor.getMapKeyTypeDescriptor()).isNull(); + assertThat(typeDescriptor.getMapValueTypeDescriptor()).isNull(); + } + + @Test + void getSource() throws Exception { + Field field = getClass().getField("fieldScalar"); + MethodParameter methodParameter = new MethodParameter(getClass().getMethod("testParameterPrimitive", int.class), 0); + assertThat(new TypeDescriptor(field).getSource()).isEqualTo(field); + assertThat(new TypeDescriptor(methodParameter).getSource()).isEqualTo(methodParameter); + assertThat(TypeDescriptor.valueOf(Integer.class).getSource()).isEqualTo(Integer.class); + } + + + // Methods designed for test introspection + + public void testParameterPrimitive(int primitive) { + } + + public void testParameterScalar(String value) { + } + + public void testParameterList(List>>> list) { + } + + public void testParameterListNoParamTypes(List list) { + } + + public void testParameterArray(Integer[] array) { + } + + public void testParameterMap(Map> map) { + } + + public void test1(List param1) { + } + + public void test2(List> param1) { + } + + public void test3(Map param1) { + } + + public void test4(List> param1) { + } + + public void test5(String param1) { + } + + public void test6(List param1) { + } + + public List> getTest4() { + return null; + } + + public void setTest4(List> test4) { + } + + public Map>> getComplexProperty() { + return null; + } + + @MethodAnnotation1 + public Map, List> getProperty() { + return property; + } + + @MethodAnnotation2 + public void setProperty(Map, List> property) { + this.property = property; + } + + @MethodAnnotation1 + public void methodWithLocalAnnotation() { + } + + @ComposedMethodAnnotation1 + public void methodWithComposedAnnotation() { + } + + @ComposedComposedMethodAnnotation1 + public void methodWithComposedComposedAnnotation() { + } + + public void setComplexProperty(Map>> complexProperty) { + } + + public void testAnnotatedMethod(@ParameterAnnotation(123) String parameter) { + } + + public void testAnnotatedMethodDifferentAnnotationValue(@ParameterAnnotation(567) String parameter) { + } + + + // Fields designed for test introspection + + public Integer fieldScalar; + + public List listOfString; + + public List> listOfListOfString = new ArrayList<>(); + + public List listOfListOfUnknown = new ArrayList<>(); + + public int[] intArray; + + public List[] arrayOfListOfString; + + public List listField = new ArrayList<>(); + + public Map mapField = new HashMap<>(); + + public Map> nestedMapField = new HashMap<>(); + + public Map, List> fieldMap; + + public List> test4; + + @FieldAnnotation + public List fieldAnnotated; + + @FieldAnnotation + public List> listPreserveContext; + + @FieldAnnotation + public Map, List> mapPreserveContext; + + @MethodAnnotation3 + private Map, List> property; + + public List notGenericList; + + public List isAssignableElementTypes; + + public Map notGenericMap; + + public Map isAssignableMapKeyValueTypes; + + public MultiValueMap multiValueMap = new LinkedMultiValueMap<>(); + + public PassDownGeneric passDownGeneric = new PassDownGeneric<>(); + + + // Classes designed for test introspection + + @SuppressWarnings("serial") + public static class PassDownGeneric extends ArrayList>> { + } + + + public static class GenericClass { + + public T getProperty() { + return null; + } + + public void setProperty(T t) { + } + + @MethodAnnotation1 + public List getListProperty() { + return null; + } + + public void setListProperty(List t) { + } + } + + + public static class IntegerClass extends GenericClass { + } + + + public interface GenericType { + + T getProperty(); + + void setProperty(T t); + + List getListProperty(); + + void setListProperty(List t); + } + + + public class IntegerType implements GenericType { + + @Override + public Integer getProperty() { + return null; + } + + @Override + public void setProperty(Integer t) { + } + + @Override + public List getListProperty() { + return null; + } + + @Override + public void setListProperty(List t) { + } + } + + + public class NumberType implements GenericType { + + @Override + public Integer getProperty() { + return null; + } + + @Override + public void setProperty(Number t) { + } + + @Override + public List getListProperty() { + return null; + } + + @Override + public void setListProperty(List t) { + } + } + + + // Annotations used on tested elements + + @Target({ElementType.PARAMETER}) + @Retention(RetentionPolicy.RUNTIME) + public @interface ParameterAnnotation { + + int value(); + } + + + @Target({ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + public @interface FieldAnnotation { + } + + + @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) + @Retention(RetentionPolicy.RUNTIME) + public @interface MethodAnnotation1 { + } + + + @Target({ElementType.METHOD}) + @Retention(RetentionPolicy.RUNTIME) + public @interface MethodAnnotation2 { + } + + + @Target({ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + public @interface MethodAnnotation3 { + } + + + @MethodAnnotation1 + @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) + @Retention(RetentionPolicy.RUNTIME) + public @interface ComposedMethodAnnotation1 { + } + + + @ComposedMethodAnnotation1 + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface ComposedComposedMethodAnnotation1 { + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/convert/converter/ConverterTests.java b/spring-core/src/test/java/org/springframework/core/convert/converter/ConverterTests.java new file mode 100644 index 0000000..65d519f --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/convert/converter/ConverterTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.converter; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link Converter} + * + * @author Josh Cummings + * @author Sam Brannen + * @since 5.3 + */ +class ConverterTests { + + private final Converter moduloTwo = number -> number % 2; + private final Converter addOne = number -> number + 1; + + + @Test + void andThenWhenGivenANullConverterThenThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.moduloTwo.andThen(null)); + } + + @Test + void andThenWhenGivenConverterThenComposesInOrder() { + assertThat(this.moduloTwo.andThen(this.addOne).convert(13)).isEqualTo(2); + assertThat(this.addOne.andThen(this.moduloTwo).convert(13)).isEqualTo(0); + } + + @Test + void andThenCanConvertfromDifferentSourceType() { + Converter length = String::length; + assertThat(length.andThen(this.moduloTwo).convert("example")).isEqualTo(1); + assertThat(length.andThen(this.addOne).convert("example")).isEqualTo(8); + } + + @Test + void andThenCanConvertToDifferentTargetType() { + Converter length = String::length; + Converter toString = Object::toString; + assertThat(length.andThen(toString).convert("example")).isEqualTo("7"); + assertThat(toString.andThen(length).convert(1_000)).isEqualTo(4); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/convert/converter/ConvertingComparatorTests.java b/spring-core/src/test/java/org/springframework/core/convert/converter/ConvertingComparatorTests.java new file mode 100644 index 0000000..93eb365 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/convert/converter/ConvertingComparatorTests.java @@ -0,0 +1,150 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.converter; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.util.comparator.ComparableComparator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link ConvertingComparator}. + * + * @author Phillip Webb + */ +class ConvertingComparatorTests { + + private final StringToInteger converter = new StringToInteger(); + + private final ConversionService conversionService = new DefaultConversionService(); + + private final TestComparator comparator = new TestComparator(); + + @Test + void shouldThrowOnNullComparator() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new ConvertingComparator<>(null, this.converter)); + } + + @Test + void shouldThrowOnNullConverter() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new ConvertingComparator(this.comparator, null)); + } + + @Test + void shouldThrowOnNullConversionService() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new ConvertingComparator(this.comparator, null, Integer.class)); + } + + @Test + void shouldThrowOnNullType() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new ConvertingComparator(this.comparator, this.conversionService, null)); + } + + @Test + void shouldUseConverterOnCompare() throws Exception { + ConvertingComparator convertingComparator = new ConvertingComparator<>( + this.comparator, this.converter); + testConversion(convertingComparator); + } + + @Test + void shouldUseConversionServiceOnCompare() throws Exception { + ConvertingComparator convertingComparator = new ConvertingComparator<>( + comparator, conversionService, Integer.class); + testConversion(convertingComparator); + } + + @Test + void shouldGetForConverter() throws Exception { + testConversion(new ConvertingComparator<>(comparator, converter)); + } + + private void testConversion(ConvertingComparator convertingComparator) { + assertThat(convertingComparator.compare("0", "0")).isEqualTo(0); + assertThat(convertingComparator.compare("0", "1")).isEqualTo(-1); + assertThat(convertingComparator.compare("1", "0")).isEqualTo(1); + comparator.assertCalled(); + } + + @Test + void shouldGetMapEntryKeys() throws Exception { + ArrayList> list = createReverseOrderMapEntryList(); + Comparator> comparator = ConvertingComparator.mapEntryKeys(new ComparableComparator()); + list.sort(comparator); + assertThat(list.get(0).getKey()).isEqualTo("a"); + } + + @Test + void shouldGetMapEntryValues() throws Exception { + ArrayList> list = createReverseOrderMapEntryList(); + Comparator> comparator = ConvertingComparator.mapEntryValues(new ComparableComparator()); + list.sort(comparator); + assertThat(list.get(0).getValue()).isEqualTo(1); + } + + private ArrayList> createReverseOrderMapEntryList() { + Map map = new LinkedHashMap<>(); + map.put("b", 2); + map.put("a", 1); + ArrayList> list = new ArrayList<>( + map.entrySet()); + assertThat(list.get(0).getKey()).isEqualTo("b"); + return list; + } + + private static class StringToInteger implements Converter { + + @Override + public Integer convert(String source) { + return Integer.valueOf(source); + } + + } + + + private static class TestComparator extends ComparableComparator { + + private boolean called; + + @Override + public int compare(Integer o1, Integer o2) { + assertThat(o1).isInstanceOf(Integer.class); + assertThat(o2).isInstanceOf(Integer.class); + this.called = true; + return super.compare(o1, o2); + }; + + public void assertCalled() { + assertThat(this.called).isTrue(); + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java b/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java new file mode 100644 index 0000000..a538c8d --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java @@ -0,0 +1,1165 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.converter; + +import java.awt.Color; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.ZoneId; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Currency; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.Set; +import java.util.TimeZone; +import java.util.UUID; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.core.convert.ConverterNotFoundException; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Unit tests for {@link DefaultConversionService}. + * + *

    In this package for enforcing accessibility checks to non-public classes outside + * of the {@code org.springframework.core.convert.support} implementation package. + * Only in such a scenario, {@code setAccessible(true)} is actually necessary. + * + * @author Keith Donald + * @author Juergen Hoeller + * @author Stephane Nicoll + * @author Sam Brannen + */ +class DefaultConversionServiceTests { + + private final DefaultConversionService conversionService = new DefaultConversionService(); + + + @Test + void stringToCharacter() { + assertThat(conversionService.convert("1", Character.class)).isEqualTo(Character.valueOf('1')); + } + + @Test + void stringToCharacterEmptyString() { + assertThat(conversionService.convert("", Character.class)).isEqualTo(null); + } + + @Test + void stringToCharacterInvalidString() { + assertThatExceptionOfType(ConversionFailedException.class).isThrownBy(() -> + conversionService.convert("invalid", Character.class)); + } + + @Test + void characterToString() { + assertThat(conversionService.convert('3', String.class)).isEqualTo("3"); + } + + @Test + void stringToBooleanTrue() { + assertThat(conversionService.convert("true", Boolean.class)).isEqualTo(true); + assertThat(conversionService.convert("on", Boolean.class)).isEqualTo(true); + assertThat(conversionService.convert("yes", Boolean.class)).isEqualTo(true); + assertThat(conversionService.convert("1", Boolean.class)).isEqualTo(true); + assertThat(conversionService.convert("TRUE", Boolean.class)).isEqualTo(true); + assertThat(conversionService.convert("ON", Boolean.class)).isEqualTo(true); + assertThat(conversionService.convert("YES", Boolean.class)).isEqualTo(true); + } + + @Test + void stringToBooleanFalse() { + assertThat(conversionService.convert("false", Boolean.class)).isEqualTo(false); + assertThat(conversionService.convert("off", Boolean.class)).isEqualTo(false); + assertThat(conversionService.convert("no", Boolean.class)).isEqualTo(false); + assertThat(conversionService.convert("0", Boolean.class)).isEqualTo(false); + assertThat(conversionService.convert("FALSE", Boolean.class)).isEqualTo(false); + assertThat(conversionService.convert("OFF", Boolean.class)).isEqualTo(false); + assertThat(conversionService.convert("NO", Boolean.class)).isEqualTo(false); + } + + @Test + void stringToBooleanEmptyString() { + assertThat(conversionService.convert("", Boolean.class)).isEqualTo(null); + } + + @Test + void stringToBooleanInvalidString() { + assertThatExceptionOfType(ConversionFailedException.class).isThrownBy(() -> + conversionService.convert("invalid", Boolean.class)); + } + + @Test + void booleanToString() { + assertThat(conversionService.convert(true, String.class)).isEqualTo("true"); + } + + @Test + void stringToByte() { + assertThat(conversionService.convert("1", Byte.class)).isEqualTo((byte) 1); + } + + @Test + void byteToString() { + assertThat(conversionService.convert("A".getBytes()[0], String.class)).isEqualTo("65"); + } + + @Test + void stringToShort() { + assertThat(conversionService.convert("1", Short.class)).isEqualTo((short) 1); + } + + @Test + void shortToString() { + short three = 3; + assertThat(conversionService.convert(three, String.class)).isEqualTo("3"); + } + + @Test + void stringToInteger() { + assertThat(conversionService.convert("1", Integer.class)).isEqualTo((int) Integer.valueOf(1)); + } + + @Test + void integerToString() { + assertThat(conversionService.convert(3, String.class)).isEqualTo("3"); + } + + @Test + void stringToLong() { + assertThat(conversionService.convert("1", Long.class)).isEqualTo(Long.valueOf(1)); + } + + @Test + void longToString() { + assertThat(conversionService.convert(3L, String.class)).isEqualTo("3"); + } + + @Test + void stringToFloat() { + assertThat(conversionService.convert("1.0", Float.class)).isEqualTo(Float.valueOf("1.0")); + } + + @Test + void floatToString() { + assertThat(conversionService.convert(Float.valueOf("1.0"), String.class)).isEqualTo("1.0"); + } + + @Test + void stringToDouble() { + assertThat(conversionService.convert("1.0", Double.class)).isEqualTo(Double.valueOf("1.0")); + } + + @Test + void doubleToString() { + assertThat(conversionService.convert(Double.valueOf("1.0"), String.class)).isEqualTo("1.0"); + } + + @Test + void stringToBigInteger() { + assertThat(conversionService.convert("1", BigInteger.class)).isEqualTo(new BigInteger("1")); + } + + @Test + void bigIntegerToString() { + assertThat(conversionService.convert(new BigInteger("100"), String.class)).isEqualTo("100"); + } + + @Test + void stringToBigDecimal() { + assertThat(conversionService.convert("1.0", BigDecimal.class)).isEqualTo(new BigDecimal("1.0")); + } + + @Test + void bigDecimalToString() { + assertThat(conversionService.convert(new BigDecimal("100.00"), String.class)).isEqualTo("100.00"); + } + + @Test + void stringToNumber() { + assertThat(conversionService.convert("1.0", Number.class)).isEqualTo(new BigDecimal("1.0")); + } + + @Test + void stringToNumberEmptyString() { + assertThat(conversionService.convert("", Number.class)).isEqualTo(null); + } + + @Test + void stringToEnum() { + assertThat(conversionService.convert("BAR", Foo.class)).isEqualTo(Foo.BAR); + } + + @Test + void stringToEnumWithSubclass() { + assertThat(conversionService.convert("BAZ", SubFoo.BAR.getClass())).isEqualTo(SubFoo.BAZ); + } + + @Test + void stringToEnumEmptyString() { + assertThat(conversionService.convert("", Foo.class)).isEqualTo(null); + } + + @Test + void enumToString() { + assertThat(conversionService.convert(Foo.BAR, String.class)).isEqualTo("BAR"); + } + + @Test + void integerToEnum() { + assertThat(conversionService.convert(0, Foo.class)).isEqualTo(Foo.BAR); + } + + @Test + void integerToEnumWithSubclass() { + assertThat(conversionService.convert(1, SubFoo.BAR.getClass())).isEqualTo(SubFoo.BAZ); + } + + @Test + void integerToEnumNull() { + assertThat(conversionService.convert(null, Foo.class)).isEqualTo(null); + } + + @Test + void enumToInteger() { + assertThat(conversionService.convert(Foo.BAR, Integer.class)).isEqualTo((int) Integer.valueOf(0)); + } + + @Test + void stringToEnumSet() throws Exception { + assertThat(conversionService.convert("BAR", TypeDescriptor.valueOf(String.class), + new TypeDescriptor(getClass().getField("enumSet")))).isEqualTo(EnumSet.of(Foo.BAR)); + } + + @Test + void stringToLocale() { + assertThat(conversionService.convert("en", Locale.class)).isEqualTo(Locale.ENGLISH); + } + + @Test + void stringToLocaleWithCountry() { + assertThat(conversionService.convert("en_US", Locale.class)).isEqualTo(Locale.US); + } + + @Test + void stringToLocaleWithLanguageTag() { + assertThat(conversionService.convert("en-US", Locale.class)).isEqualTo(Locale.US); + } + + @Test + void stringToCharset() { + assertThat(conversionService.convert("UTF-8", Charset.class)).isEqualTo(StandardCharsets.UTF_8); + } + + @Test + void charsetToString() { + assertThat(conversionService.convert(StandardCharsets.UTF_8, String.class)).isEqualTo("UTF-8"); + } + + @Test + void stringToCurrency() { + assertThat(conversionService.convert("EUR", Currency.class)).isEqualTo(Currency.getInstance("EUR")); + } + + @Test + void currencyToString() { + assertThat(conversionService.convert(Currency.getInstance("USD"), String.class)).isEqualTo("USD"); + } + + @Test + void stringToString() { + String str = "test"; + assertThat(conversionService.convert(str, String.class)).isSameAs(str); + } + + @Test + void uuidToStringAndStringToUuid() { + UUID uuid = UUID.randomUUID(); + String convertToString = conversionService.convert(uuid, String.class); + UUID convertToUUID = conversionService.convert(convertToString, UUID.class); + assertThat(convertToUUID).isEqualTo(uuid); + } + + @Test + void numberToNumber() { + assertThat(conversionService.convert(1, Long.class)).isEqualTo(Long.valueOf(1)); + } + + @Test + void numberToNumberNotSupportedNumber() { + assertThatExceptionOfType(ConversionFailedException.class).isThrownBy(() -> + conversionService.convert(1, CustomNumber.class)); + } + + @Test + void numberToCharacter() { + assertThat(conversionService.convert(65, Character.class)).isEqualTo(Character.valueOf('A')); + } + + @Test + void characterToNumber() { + assertThat(conversionService.convert('A', Integer.class)).isEqualTo(65); + } + + // collection conversion + + @Test + void convertArrayToCollectionInterface() { + List result = conversionService.convert(new String[] {"1", "2", "3"}, List.class); + assertThat(result.get(0)).isEqualTo("1"); + assertThat(result.get(1)).isEqualTo("2"); + assertThat(result.get(2)).isEqualTo("3"); + } + + @Test + void convertArrayToCollectionGenericTypeConversion() throws Exception { + @SuppressWarnings("unchecked") + List result = (List) conversionService.convert(new String[] {"1", "2", "3"}, TypeDescriptor + .valueOf(String[].class), new TypeDescriptor(getClass().getDeclaredField("genericList"))); + assertThat((int) result.get(0)).isEqualTo((int) Integer.valueOf(1)); + assertThat((int) result.get(1)).isEqualTo((int) Integer.valueOf(2)); + assertThat((int) result.get(2)).isEqualTo((int) Integer.valueOf(3)); + } + + @Test + void convertArrayToStream() throws Exception { + String[] source = {"1", "3", "4"}; + @SuppressWarnings("unchecked") + Stream result = (Stream) this.conversionService.convert(source, + TypeDescriptor.valueOf(String[].class), + new TypeDescriptor(getClass().getDeclaredField("genericStream"))); + assertThat(result.mapToInt(x -> x).sum()).isEqualTo(8); + } + + @Test + void spr7766() throws Exception { + ConverterRegistry registry = (conversionService); + registry.addConverter(new ColorConverter()); + @SuppressWarnings("unchecked") + List colors = (List) conversionService.convert(new String[] {"ffffff", "#000000"}, + TypeDescriptor.valueOf(String[].class), + new TypeDescriptor(new MethodParameter(getClass().getMethod("handlerMethod", List.class), 0))); + assertThat(colors.size()).isEqualTo(2); + assertThat(colors.get(0)).isEqualTo(Color.WHITE); + assertThat(colors.get(1)).isEqualTo(Color.BLACK); + } + + @Test + void convertArrayToCollectionImpl() { + ArrayList result = conversionService.convert(new String[] {"1", "2", "3"}, ArrayList.class); + assertThat(result.get(0)).isEqualTo("1"); + assertThat(result.get(1)).isEqualTo("2"); + assertThat(result.get(2)).isEqualTo("3"); + } + + @Test + void convertArrayToAbstractCollection() { + assertThatExceptionOfType(ConversionFailedException.class).isThrownBy(() -> + conversionService.convert(new String[]{"1", "2", "3"}, AbstractList.class)); + } + + @Test + void convertArrayToString() { + String result = conversionService.convert(new String[] {"1", "2", "3"}, String.class); + assertThat(result).isEqualTo("1,2,3"); + } + + @Test + void convertArrayToStringWithElementConversion() { + String result = conversionService.convert(new Integer[] {1, 2, 3}, String.class); + assertThat(result).isEqualTo("1,2,3"); + } + + @Test + void convertEmptyArrayToString() { + String result = conversionService.convert(new String[0], String.class); + assertThat(result).isEqualTo(""); + } + + @Test + void convertStringToArray() { + String[] result = conversionService.convert("1,2,3", String[].class); + assertThat(result.length).isEqualTo(3); + assertThat(result[0]).isEqualTo("1"); + assertThat(result[1]).isEqualTo("2"); + assertThat(result[2]).isEqualTo("3"); + } + + @Test + void convertStringToArrayWithElementConversion() { + Integer[] result = conversionService.convert("1,2,3", Integer[].class); + assertThat(result.length).isEqualTo(3); + assertThat((int) result[0]).isEqualTo((int) Integer.valueOf(1)); + assertThat((int) result[1]).isEqualTo((int) Integer.valueOf(2)); + assertThat((int) result[2]).isEqualTo((int) Integer.valueOf(3)); + } + + @Test + void convertStringToPrimitiveArrayWithElementConversion() { + int[] result = conversionService.convert("1,2,3", int[].class); + assertThat(result.length).isEqualTo(3); + assertThat(result[0]).isEqualTo(1); + assertThat(result[1]).isEqualTo(2); + assertThat(result[2]).isEqualTo(3); + } + + @Test + void convertEmptyStringToArray() { + String[] result = conversionService.convert("", String[].class); + assertThat(result.length).isEqualTo(0); + } + + @Test + void convertArrayToObject() { + Object[] array = new Object[] {3L}; + Object result = conversionService.convert(array, Long.class); + assertThat(result).isEqualTo(3L); + } + + @Test + void convertArrayToObjectWithElementConversion() { + String[] array = new String[] {"3"}; + Integer result = conversionService.convert(array, Integer.class); + assertThat((int) result).isEqualTo((int) Integer.valueOf(3)); + } + + @Test + void convertArrayToObjectAssignableTargetType() { + Long[] array = new Long[] {3L}; + Long[] result = (Long[]) conversionService.convert(array, Object.class); + assertThat(result).isEqualTo(array); + } + + @Test + void convertObjectToArray() { + Object[] result = conversionService.convert(3L, Object[].class); + assertThat(result.length).isEqualTo(1); + assertThat(result[0]).isEqualTo(3L); + } + + @Test + void convertObjectToArrayWithElementConversion() { + Integer[] result = conversionService.convert(3L, Integer[].class); + assertThat(result.length).isEqualTo(1); + assertThat((int) result[0]).isEqualTo((int) Integer.valueOf(3)); + } + + @Test + void convertCollectionToArray() { + List list = new ArrayList<>(); + list.add("1"); + list.add("2"); + list.add("3"); + String[] result = conversionService.convert(list, String[].class); + assertThat(result[0]).isEqualTo("1"); + assertThat(result[1]).isEqualTo("2"); + assertThat(result[2]).isEqualTo("3"); + } + + @Test + void convertCollectionToArrayWithElementConversion() { + List list = new ArrayList<>(); + list.add("1"); + list.add("2"); + list.add("3"); + Integer[] result = conversionService.convert(list, Integer[].class); + assertThat((int) result[0]).isEqualTo((int) Integer.valueOf(1)); + assertThat((int) result[1]).isEqualTo((int) Integer.valueOf(2)); + assertThat((int) result[2]).isEqualTo((int) Integer.valueOf(3)); + } + + @Test + void convertCollectionToString() { + List list = Arrays.asList("foo", "bar"); + String result = conversionService.convert(list, String.class); + assertThat(result).isEqualTo("foo,bar"); + } + + @Test + void convertCollectionToStringWithElementConversion() throws Exception { + List list = Arrays.asList(3, 5); + String result = (String) conversionService.convert(list, + new TypeDescriptor(getClass().getField("genericList")), TypeDescriptor.valueOf(String.class)); + assertThat(result).isEqualTo("3,5"); + } + + @Test + void convertStringToCollection() { + List result = conversionService.convert("1,2,3", List.class); + assertThat(result.size()).isEqualTo(3); + assertThat(result.get(0)).isEqualTo("1"); + assertThat(result.get(1)).isEqualTo("2"); + assertThat(result.get(2)).isEqualTo("3"); + } + + @Test + void convertStringToCollectionWithElementConversion() throws Exception { + List result = (List) conversionService.convert("1,2,3", TypeDescriptor.valueOf(String.class), + new TypeDescriptor(getClass().getField("genericList"))); + assertThat(result.size()).isEqualTo(3); + assertThat(result.get(0)).isEqualTo(1); + assertThat(result.get(1)).isEqualTo(2); + assertThat(result.get(2)).isEqualTo(3); + } + + @Test + void convertEmptyStringToCollection() { + Collection result = conversionService.convert("", Collection.class); + assertThat(result.size()).isEqualTo(0); + } + + @Test + void convertCollectionToObject() { + List list = Collections.singletonList(3L); + Long result = conversionService.convert(list, Long.class); + assertThat(result).isEqualTo(Long.valueOf(3)); + } + + @Test + void convertCollectionToObjectWithElementConversion() { + List list = Collections.singletonList("3"); + Integer result = conversionService.convert(list, Integer.class); + assertThat((int) result).isEqualTo((int) Integer.valueOf(3)); + } + + @Test + void convertCollectionToObjectAssignableTarget() throws Exception { + Collection source = new ArrayList<>(); + source.add("foo"); + Object result = conversionService.convert(source, new TypeDescriptor(getClass().getField("assignableTarget"))); + assertThat(result).isEqualTo(source); + } + + @Test + void convertCollectionToObjectWithCustomConverter() { + List source = new ArrayList<>(); + source.add("A"); + source.add("B"); + conversionService.addConverter(List.class, ListWrapper.class, ListWrapper::new); + ListWrapper result = conversionService.convert(source, ListWrapper.class); + assertThat(result.getList()).isSameAs(source); + } + + @Test + void convertObjectToCollection() { + List result = conversionService.convert(3L, List.class); + assertThat(result.size()).isEqualTo(1); + assertThat(result.get(0)).isEqualTo(3L); + } + + @Test + void convertObjectToCollectionWithElementConversion() throws Exception { + @SuppressWarnings("unchecked") + List result = (List) conversionService.convert(3L, TypeDescriptor.valueOf(Long.class), + new TypeDescriptor(getClass().getField("genericList"))); + assertThat(result.size()).isEqualTo(1); + assertThat((int) result.get(0)).isEqualTo((int) Integer.valueOf(3)); + } + + @Test + void convertStringArrayToIntegerArray() { + Integer[] result = conversionService.convert(new String[] {"1", "2", "3"}, Integer[].class); + assertThat((int) result[0]).isEqualTo((int) Integer.valueOf(1)); + assertThat((int) result[1]).isEqualTo((int) Integer.valueOf(2)); + assertThat((int) result[2]).isEqualTo((int) Integer.valueOf(3)); + } + + @Test + void convertStringArrayToIntArray() { + int[] result = conversionService.convert(new String[] {"1", "2", "3"}, int[].class); + assertThat(result[0]).isEqualTo(1); + assertThat(result[1]).isEqualTo(2); + assertThat(result[2]).isEqualTo(3); + } + + @Test + void convertIntegerArrayToIntegerArray() { + Integer[] result = conversionService.convert(new Integer[] {1, 2, 3}, Integer[].class); + assertThat((int) result[0]).isEqualTo((int) Integer.valueOf(1)); + assertThat((int) result[1]).isEqualTo((int) Integer.valueOf(2)); + assertThat((int) result[2]).isEqualTo((int) Integer.valueOf(3)); + } + + @Test + void convertIntegerArrayToIntArray() { + int[] result = conversionService.convert(new Integer[] {1, 2, 3}, int[].class); + assertThat(result[0]).isEqualTo(1); + assertThat(result[1]).isEqualTo(2); + assertThat(result[2]).isEqualTo(3); + } + + @Test + void convertObjectArrayToIntegerArray() { + Integer[] result = conversionService.convert(new Object[] {1, 2, 3}, Integer[].class); + assertThat((int) result[0]).isEqualTo((int) Integer.valueOf(1)); + assertThat((int) result[1]).isEqualTo((int) Integer.valueOf(2)); + assertThat((int) result[2]).isEqualTo((int) Integer.valueOf(3)); + } + + @Test + void convertObjectArrayToIntArray() { + int[] result = conversionService.convert(new Object[] {1, 2, 3}, int[].class); + assertThat(result[0]).isEqualTo(1); + assertThat(result[1]).isEqualTo(2); + assertThat(result[2]).isEqualTo(3); + } + + @Test + void convertByteArrayToWrapperArray() { + byte[] byteArray = new byte[] {1, 2, 3}; + Byte[] converted = conversionService.convert(byteArray, Byte[].class); + assertThat(converted).isEqualTo(new Byte[]{1, 2, 3}); + } + + @Test + void convertArrayToArrayAssignable() { + int[] result = conversionService.convert(new int[] {1, 2, 3}, int[].class); + assertThat(result[0]).isEqualTo(1); + assertThat(result[1]).isEqualTo(2); + assertThat(result[2]).isEqualTo(3); + } + + @Test + void convertListOfNonStringifiable() { + List list = Arrays.asList(new TestEntity(1L), new TestEntity(2L)); + assertThat(conversionService.canConvert(list.getClass(), String.class)).isTrue(); + try { + conversionService.convert(list, String.class); + } + catch (ConversionFailedException ex) { + assertThat(ex.getMessage().contains(list.getClass().getName())).isTrue(); + assertThat(ex.getCause() instanceof ConverterNotFoundException).isTrue(); + assertThat(ex.getCause().getMessage().contains(TestEntity.class.getName())).isTrue(); + } + } + + @Test + void convertListOfStringToString() { + List list = Arrays.asList("Foo", "Bar"); + assertThat(conversionService.canConvert(list.getClass(), String.class)).isTrue(); + String result = conversionService.convert(list, String.class); + assertThat(result).isEqualTo("Foo,Bar"); + } + + @Test + void convertListOfListToString() { + List list1 = Arrays.asList("Foo", "Bar"); + List list2 = Arrays.asList("Baz", "Boop"); + List> list = Arrays.asList(list1, list2); + assertThat(conversionService.canConvert(list.getClass(), String.class)).isTrue(); + String result = conversionService.convert(list, String.class); + assertThat(result).isEqualTo("Foo,Bar,Baz,Boop"); + } + + @Test + void convertCollectionToCollection() throws Exception { + Set foo = new LinkedHashSet<>(); + foo.add("1"); + foo.add("2"); + foo.add("3"); + @SuppressWarnings("unchecked") + List bar = (List) conversionService.convert(foo, TypeDescriptor.forObject(foo), + new TypeDescriptor(getClass().getField("genericList"))); + assertThat((int) bar.get(0)).isEqualTo((int) Integer.valueOf(1)); + assertThat((int) bar.get(1)).isEqualTo((int) Integer.valueOf(2)); + assertThat((int) bar.get(2)).isEqualTo((int) Integer.valueOf(3)); + } + + @Test + void convertCollectionToCollectionNull() throws Exception { + @SuppressWarnings("unchecked") + List bar = (List) conversionService.convert(null, + TypeDescriptor.valueOf(LinkedHashSet.class), new TypeDescriptor(getClass().getField("genericList"))); + assertThat((Object) bar).isNull(); + } + + @Test + @SuppressWarnings("rawtypes") + void convertCollectionToCollectionNotGeneric() { + Set foo = new LinkedHashSet<>(); + foo.add("1"); + foo.add("2"); + foo.add("3"); + List bar = (List) conversionService.convert(foo, TypeDescriptor.valueOf(LinkedHashSet.class), TypeDescriptor + .valueOf(List.class)); + assertThat(bar.get(0)).isEqualTo("1"); + assertThat(bar.get(1)).isEqualTo("2"); + assertThat(bar.get(2)).isEqualTo("3"); + } + + @Test + @SuppressWarnings({"unchecked", "rawtypes"}) + void convertCollectionToCollectionSpecialCaseSourceImpl() throws Exception { + Map map = new LinkedHashMap(); + map.put("1", "1"); + map.put("2", "2"); + map.put("3", "3"); + Collection values = map.values(); + List bar = (List) conversionService.convert(values, + TypeDescriptor.forObject(values), new TypeDescriptor(getClass().getField("genericList"))); + assertThat(bar.size()).isEqualTo(3); + assertThat((int) bar.get(0)).isEqualTo((int) Integer.valueOf(1)); + assertThat((int) bar.get(1)).isEqualTo((int) Integer.valueOf(2)); + assertThat((int) bar.get(2)).isEqualTo((int) Integer.valueOf(3)); + } + + @Test + void collection() { + List strings = new ArrayList<>(); + strings.add("3"); + strings.add("9"); + @SuppressWarnings("unchecked") + List integers = (List) conversionService.convert(strings, + TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(Integer.class))); + assertThat((int) integers.get(0)).isEqualTo((int) Integer.valueOf(3)); + assertThat((int) integers.get(1)).isEqualTo((int) Integer.valueOf(9)); + } + + @Test + void convertMapToMap() throws Exception { + Map foo = new HashMap<>(); + foo.put("1", "BAR"); + foo.put("2", "BAZ"); + @SuppressWarnings("unchecked") + Map map = (Map) conversionService.convert(foo, + TypeDescriptor.forObject(foo), new TypeDescriptor(getClass().getField("genericMap"))); + assertThat(map.get(1)).isEqualTo(Foo.BAR); + assertThat(map.get(2)).isEqualTo(Foo.BAZ); + } + + @Test + void convertHashMapValuesToList() { + Map hashMap = new LinkedHashMap<>(); + hashMap.put("1", 1); + hashMap.put("2", 2); + List converted = conversionService.convert(hashMap.values(), List.class); + assertThat(converted).isEqualTo(Arrays.asList(1, 2)); + } + + @Test + void map() { + Map strings = new HashMap<>(); + strings.put("3", "9"); + strings.put("6", "31"); + @SuppressWarnings("unchecked") + Map integers = (Map) conversionService.convert(strings, + TypeDescriptor.map(Map.class, TypeDescriptor.valueOf(Integer.class), TypeDescriptor.valueOf(Integer.class))); + assertThat((int) integers.get(3)).isEqualTo((int) Integer.valueOf(9)); + assertThat((int) integers.get(6)).isEqualTo((int) Integer.valueOf(31)); + } + + @Test + void convertPropertiesToString() { + Properties foo = new Properties(); + foo.setProperty("1", "BAR"); + foo.setProperty("2", "BAZ"); + String result = conversionService.convert(foo, String.class); + assertThat(result.contains("1=BAR")).isTrue(); + assertThat(result.contains("2=BAZ")).isTrue(); + } + + @Test + void convertStringToProperties() { + Properties result = conversionService.convert("a=b\nc=2\nd=", Properties.class); + assertThat(result.size()).isEqualTo(3); + assertThat(result.getProperty("a")).isEqualTo("b"); + assertThat(result.getProperty("c")).isEqualTo("2"); + assertThat(result.getProperty("d")).isEqualTo(""); + } + + @Test + void convertStringToPropertiesWithSpaces() { + Properties result = conversionService.convert(" foo=bar\n bar=baz\n baz=boop", Properties.class); + assertThat(result.get("foo")).isEqualTo("bar"); + assertThat(result.get("bar")).isEqualTo("baz"); + assertThat(result.get("baz")).isEqualTo("boop"); + } + + // generic object conversion + + @Test + void convertObjectToStringWithValueOfMethodPresentUsingToString() { + ISBN.reset(); + assertThat(conversionService.convert(new ISBN("123456789"), String.class)).isEqualTo("123456789"); + + assertThat(ISBN.constructorCount).as("constructor invocations").isEqualTo(1); + assertThat(ISBN.valueOfCount).as("valueOf() invocations").isEqualTo(0); + assertThat(ISBN.toStringCount).as("toString() invocations").isEqualTo(1); + } + + @Test + void convertObjectToObjectUsingValueOfMethod() { + ISBN.reset(); + assertThat(conversionService.convert("123456789", ISBN.class)).isEqualTo(new ISBN("123456789")); + + assertThat(ISBN.valueOfCount).as("valueOf() invocations").isEqualTo(1); + // valueOf() invokes the constructor + assertThat(ISBN.constructorCount).as("constructor invocations").isEqualTo(2); + assertThat(ISBN.toStringCount).as("toString() invocations").isEqualTo(0); + } + + @Test + void convertObjectToStringUsingToString() { + SSN.reset(); + assertThat(conversionService.convert(new SSN("123456789"), String.class)).isEqualTo("123456789"); + + assertThat(SSN.constructorCount).as("constructor invocations").isEqualTo(1); + assertThat(SSN.toStringCount).as("toString() invocations").isEqualTo(1); + } + + @Test + void convertObjectToObjectUsingObjectConstructor() { + SSN.reset(); + assertThat(conversionService.convert("123456789", SSN.class)).isEqualTo(new SSN("123456789")); + + assertThat(SSN.constructorCount).as("constructor invocations").isEqualTo(2); + assertThat(SSN.toStringCount).as("toString() invocations").isEqualTo(0); + } + + @Test + void convertStringToTimezone() { + assertThat(conversionService.convert("GMT+2", TimeZone.class).getID()).isEqualTo("GMT+02:00"); + } + + @Test + void convertObjectToStringWithJavaTimeOfMethodPresent() { + assertThat(conversionService.convert(ZoneId.of("GMT+1"), String.class).startsWith("GMT+")).isTrue(); + } + + @Test + void convertObjectToStringNotSupported() { + assertThat(conversionService.canConvert(TestEntity.class, String.class)).isFalse(); + } + + @Test + void convertObjectToObjectWithJavaTimeOfMethod() { + assertThat(conversionService.convert("GMT+1", ZoneId.class)).isEqualTo(ZoneId.of("GMT+1")); + } + + @Test + void convertObjectToObjectNoValueOfMethodOrConstructor() { + assertThatExceptionOfType(ConverterNotFoundException.class).isThrownBy(() -> + conversionService.convert(Long.valueOf(3), SSN.class)); + } + + @Test + void convertObjectToObjectFinderMethod() { + TestEntity e = conversionService.convert(1L, TestEntity.class); + assertThat(e.getId()).isEqualTo(Long.valueOf(1)); + } + + @Test + void convertObjectToObjectFinderMethodWithNull() { + TestEntity entity = (TestEntity) conversionService.convert(null, + TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(TestEntity.class)); + assertThat((Object) entity).isNull(); + } + + @Test + void convertObjectToObjectFinderMethodWithIdConversion() { + TestEntity entity = conversionService.convert("1", TestEntity.class); + assertThat(entity.getId()).isEqualTo(Long.valueOf(1)); + } + + @Test + void convertCharArrayToString() { + String converted = conversionService.convert(new char[] {'a', 'b', 'c'}, String.class); + assertThat(converted).isEqualTo("a,b,c"); + } + + @Test + void convertStringToCharArray() { + char[] converted = conversionService.convert("a,b,c", char[].class); + assertThat(converted).isEqualTo(new char[]{'a', 'b', 'c'}); + } + + @Test + void convertStringToCustomCharArray() { + conversionService.addConverter(String.class, char[].class, String::toCharArray); + char[] converted = conversionService.convert("abc", char[].class); + assertThat(converted).isEqualTo(new char[] {'a', 'b', 'c'}); + } + + @Test + @SuppressWarnings("unchecked") + void multidimensionalArrayToListConversionShouldConvertEntriesCorrectly() { + String[][] grid = new String[][] {new String[] {"1", "2", "3", "4"}, new String[] {"5", "6", "7", "8"}, + new String[] {"9", "10", "11", "12"}}; + List converted = conversionService.convert(grid, List.class); + String[][] convertedBack = conversionService.convert(converted, String[][].class); + assertThat(convertedBack).isEqualTo(grid); + } + + @Test + void convertCannotOptimizeArray() { + conversionService.addConverter(Byte.class, Byte.class, source -> (byte) (source + 1)); + byte[] byteArray = new byte[] {1, 2, 3}; + byte[] converted = conversionService.convert(byteArray, byte[].class); + assertThat(converted).isNotSameAs(byteArray); + assertThat(converted).isEqualTo(new byte[]{2, 3, 4}); + } + + @Test + @SuppressWarnings("unchecked") + void convertObjectToOptional() { + Method method = ClassUtils.getMethod(TestEntity.class, "handleOptionalValue", Optional.class); + MethodParameter parameter = new MethodParameter(method, 0); + TypeDescriptor descriptor = new TypeDescriptor(parameter); + Object actual = conversionService.convert("1,2,3", TypeDescriptor.valueOf(String.class), descriptor); + assertThat(actual.getClass()).isEqualTo(Optional.class); + assertThat(((Optional>) actual).get()).isEqualTo(Arrays.asList(1, 2, 3)); + } + + @Test + void convertObjectToOptionalNull() { + assertThat(conversionService.convert(null, TypeDescriptor.valueOf(Object.class), + TypeDescriptor.valueOf(Optional.class))).isSameAs(Optional.empty()); + assertThat((Object) conversionService.convert(null, Optional.class)).isSameAs(Optional.empty()); + } + + @Test + void convertExistingOptional() { + assertThat(conversionService.convert(Optional.empty(), TypeDescriptor.valueOf(Object.class), + TypeDescriptor.valueOf(Optional.class))).isSameAs(Optional.empty()); + assertThat((Object) conversionService.convert(Optional.empty(), Optional.class)).isSameAs(Optional.empty()); + } + + + // test fields and helpers + + public List genericList = new ArrayList<>(); + + public Stream genericStream; + + public Map genericMap = new HashMap<>(); + + public EnumSet enumSet; + + public Object assignableTarget; + + + public void handlerMethod(List color) { + } + + + public enum Foo { + + BAR, BAZ + } + + + public enum SubFoo { + + BAR { + @Override + String s() { + return "x"; + } + }, + BAZ { + @Override + String s() { + return "y"; + } + }; + + abstract String s(); + } + + + public class ColorConverter implements Converter { + + @Override + public Color convert(String source) { + if (!source.startsWith("#")) { + source = "#" + source; + } + return Color.decode(source); + } + } + + + @SuppressWarnings("serial") + public static class CustomNumber extends Number { + + @Override + public double doubleValue() { + return 0; + } + + @Override + public float floatValue() { + return 0; + } + + @Override + public int intValue() { + return 0; + } + + @Override + public long longValue() { + return 0; + } + } + + + public static class TestEntity { + + private Long id; + + public TestEntity(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + + public static TestEntity findTestEntity(Long id) { + return new TestEntity(id); + } + + public void handleOptionalValue(Optional> value) { + } + } + + + private static class ListWrapper { + + private List list; + + public ListWrapper(List list) { + this.list = list; + } + + public List getList() { + return list; + } + } + + + private static class SSN { + + static int constructorCount = 0; + + static int toStringCount = 0; + + static void reset() { + constructorCount = 0; + toStringCount = 0; + } + + private final String value; + + public SSN(String value) { + constructorCount++; + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof SSN)) { + return false; + } + SSN ssn = (SSN) o; + return this.value.equals(ssn.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public String toString() { + toStringCount++; + return value; + } + } + + + private static class ISBN { + + static int constructorCount = 0; + static int toStringCount = 0; + static int valueOfCount = 0; + + static void reset() { + constructorCount = 0; + toStringCount = 0; + valueOfCount = 0; + } + + private final String value; + + public ISBN(String value) { + constructorCount++; + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof ISBN)) { + return false; + } + ISBN isbn = (ISBN) o; + return this.value.equals(isbn.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public String toString() { + toStringCount++; + return value; + } + + @SuppressWarnings("unused") + public static ISBN valueOf(String value) { + valueOfCount++; + return new ISBN(value); + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/convert/support/ByteBufferConverterTests.java b/spring-core/src/test/java/org/springframework/core/convert/support/ByteBufferConverterTests.java new file mode 100644 index 0000000..9c0f46e --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/convert/support/ByteBufferConverterTests.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.nio.ByteBuffer; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.core.convert.converter.Converter; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ByteBufferConverter}. + * + * @author Phillip Webb + * @author Juergen Hoeller + */ +class ByteBufferConverterTests { + + private GenericConversionService conversionService; + + + @BeforeEach + void setup() { + this.conversionService = new DefaultConversionService(); + this.conversionService.addConverter(new ByteArrayToOtherTypeConverter()); + this.conversionService.addConverter(new OtherTypeToByteArrayConverter()); + } + + + @Test + void byteArrayToByteBuffer() throws Exception { + byte[] bytes = new byte[] { 1, 2, 3 }; + ByteBuffer convert = this.conversionService.convert(bytes, ByteBuffer.class); + assertThat(convert.array()).isNotSameAs(bytes); + assertThat(convert.array()).isEqualTo(bytes); + } + + @Test + void byteBufferToByteArray() throws Exception { + byte[] bytes = new byte[] { 1, 2, 3 }; + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); + byte[] convert = this.conversionService.convert(byteBuffer, byte[].class); + assertThat(convert).isNotSameAs(bytes); + assertThat(convert).isEqualTo(bytes); + } + + @Test + void byteBufferToOtherType() throws Exception { + byte[] bytes = new byte[] { 1, 2, 3 }; + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); + OtherType convert = this.conversionService.convert(byteBuffer, OtherType.class); + assertThat(convert.bytes).isNotSameAs(bytes); + assertThat(convert.bytes).isEqualTo(bytes); + } + + @Test + void otherTypeToByteBuffer() throws Exception { + byte[] bytes = new byte[] { 1, 2, 3 }; + OtherType otherType = new OtherType(bytes); + ByteBuffer convert = this.conversionService.convert(otherType, ByteBuffer.class); + assertThat(convert.array()).isNotSameAs(bytes); + assertThat(convert.array()).isEqualTo(bytes); + } + + @Test + void byteBufferToByteBuffer() throws Exception { + byte[] bytes = new byte[] { 1, 2, 3 }; + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); + ByteBuffer convert = this.conversionService.convert(byteBuffer, ByteBuffer.class); + assertThat(convert).isNotSameAs(byteBuffer.rewind()); + assertThat(convert).isEqualTo(byteBuffer.rewind()); + assertThat(convert).isEqualTo(ByteBuffer.wrap(bytes)); + assertThat(convert.array()).isEqualTo(bytes); + } + + + private static class OtherType { + + private byte[] bytes; + + public OtherType(byte[] bytes) { + this.bytes = bytes; + } + + } + + private static class ByteArrayToOtherTypeConverter implements Converter { + + @Override + public OtherType convert(byte[] source) { + return new OtherType(source); + } + } + + + private static class OtherTypeToByteArrayConverter implements Converter { + + @Override + public byte[] convert(OtherType source) { + return source.bytes; + } + + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/convert/support/CollectionToCollectionConverterTests.java b/spring-core/src/test/java/org/springframework/core/convert/support/CollectionToCollectionConverterTests.java new file mode 100644 index 0000000..b7eca2c --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/convert/support/CollectionToCollectionConverterTests.java @@ -0,0 +1,362 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Vector; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.core.convert.ConverterNotFoundException; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Keith Donald + * @author Juergen Hoeller + * @author Stephane Nicoll + */ +class CollectionToCollectionConverterTests { + + private GenericConversionService conversionService = new GenericConversionService(); + + + @BeforeEach + void setUp() { + conversionService.addConverter(new CollectionToCollectionConverter(conversionService)); + } + + + @Test + void scalarList() throws Exception { + List list = new ArrayList<>(); + list.add("9"); + list.add("37"); + TypeDescriptor sourceType = TypeDescriptor.forObject(list); + TypeDescriptor targetType = new TypeDescriptor(getClass().getField("scalarListTarget")); + assertThat(conversionService.canConvert(sourceType, targetType)).isTrue(); + try { + conversionService.convert(list, sourceType, targetType); + } + catch (ConversionFailedException ex) { + boolean condition = ex.getCause() instanceof ConverterNotFoundException; + assertThat(condition).isTrue(); + } + conversionService.addConverterFactory(new StringToNumberConverterFactory()); + assertThat(conversionService.canConvert(sourceType, targetType)).isTrue(); + @SuppressWarnings("unchecked") + List result = (List) conversionService.convert(list, sourceType, targetType); + assertThat(list.equals(result)).isFalse(); + assertThat(result.get(0).intValue()).isEqualTo(9); + assertThat(result.get(1).intValue()).isEqualTo(37); + } + + @Test + void emptyListToList() throws Exception { + conversionService.addConverter(new CollectionToCollectionConverter(conversionService)); + conversionService.addConverterFactory(new StringToNumberConverterFactory()); + List list = new ArrayList<>(); + TypeDescriptor sourceType = TypeDescriptor.forObject(list); + TypeDescriptor targetType = new TypeDescriptor(getClass().getField("emptyListTarget")); + assertThat(conversionService.canConvert(sourceType, targetType)).isTrue(); + assertThat(conversionService.convert(list, sourceType, targetType)).isEqualTo(list); + } + + @Test + void emptyListToListDifferentTargetType() throws Exception { + conversionService.addConverter(new CollectionToCollectionConverter(conversionService)); + conversionService.addConverterFactory(new StringToNumberConverterFactory()); + List list = new ArrayList<>(); + TypeDescriptor sourceType = TypeDescriptor.forObject(list); + TypeDescriptor targetType = new TypeDescriptor(getClass().getField("emptyListDifferentTarget")); + assertThat(conversionService.canConvert(sourceType, targetType)).isTrue(); + @SuppressWarnings("unchecked") + ArrayList result = (ArrayList) conversionService.convert(list, sourceType, targetType); + assertThat(result.getClass()).isEqualTo(ArrayList.class); + assertThat(result.isEmpty()).isTrue(); + } + + @Test + void collectionToObjectInteraction() throws Exception { + List> list = new ArrayList<>(); + list.add(Arrays.asList("9", "12")); + list.add(Arrays.asList("37", "23")); + conversionService.addConverter(new CollectionToObjectConverter(conversionService)); + assertThat(conversionService.canConvert(List.class, List.class)).isTrue(); + assertThat((Object) conversionService.convert(list, List.class)).isSameAs(list); + } + + @Test + @SuppressWarnings("unchecked") + void arrayCollectionToObjectInteraction() throws Exception { + List[] array = new List[2]; + array[0] = Arrays.asList("9", "12"); + array[1] = Arrays.asList("37", "23"); + conversionService.addConverter(new ArrayToCollectionConverter(conversionService)); + conversionService.addConverter(new CollectionToObjectConverter(conversionService)); + assertThat(conversionService.canConvert(String[].class, List.class)).isTrue(); + assertThat(conversionService.convert(array, List.class)).isEqualTo(Arrays.asList(array)); + } + + @Test + @SuppressWarnings("unchecked") + void objectToCollection() throws Exception { + List> list = new ArrayList<>(); + list.add(Arrays.asList("9", "12")); + list.add(Arrays.asList("37", "23")); + conversionService.addConverterFactory(new StringToNumberConverterFactory()); + conversionService.addConverter(new ObjectToCollectionConverter(conversionService)); + conversionService.addConverter(new CollectionToObjectConverter(conversionService)); + TypeDescriptor sourceType = TypeDescriptor.forObject(list); + TypeDescriptor targetType = new TypeDescriptor(getClass().getField("objectToCollection")); + assertThat(conversionService.canConvert(sourceType, targetType)).isTrue(); + List>> result = (List>>) conversionService.convert(list, sourceType, targetType); + assertThat(result.get(0).get(0).get(0)).isEqualTo((Integer) 9); + assertThat(result.get(0).get(1).get(0)).isEqualTo((Integer) 12); + assertThat(result.get(1).get(0).get(0)).isEqualTo((Integer) 37); + assertThat(result.get(1).get(1).get(0)).isEqualTo((Integer) 23); + } + + @Test + @SuppressWarnings("unchecked") + void stringToCollection() throws Exception { + List> list = new ArrayList<>(); + list.add(Arrays.asList("9,12")); + list.add(Arrays.asList("37,23")); + conversionService.addConverterFactory(new StringToNumberConverterFactory()); + conversionService.addConverter(new StringToCollectionConverter(conversionService)); + conversionService.addConverter(new ObjectToCollectionConverter(conversionService)); + conversionService.addConverter(new CollectionToObjectConverter(conversionService)); + TypeDescriptor sourceType = TypeDescriptor.forObject(list); + TypeDescriptor targetType = new TypeDescriptor(getClass().getField("objectToCollection")); + assertThat(conversionService.canConvert(sourceType, targetType)).isTrue(); + List>> result = (List>>) conversionService.convert(list, sourceType, targetType); + assertThat(result.get(0).get(0).get(0)).isEqualTo((Integer) 9); + assertThat(result.get(0).get(0).get(1)).isEqualTo((Integer) 12); + assertThat(result.get(1).get(0).get(0)).isEqualTo((Integer) 37); + assertThat(result.get(1).get(0).get(1)).isEqualTo((Integer) 23); + } + + @Test + void convertEmptyVector_shouldReturnEmptyArrayList() { + Vector vector = new Vector<>(); + vector.add("Element"); + testCollectionConversionToArrayList(vector); + } + + @Test + void convertNonEmptyVector_shouldReturnNonEmptyArrayList() { + Vector vector = new Vector<>(); + vector.add("Element"); + testCollectionConversionToArrayList(vector); + } + + @Test + void collectionsEmptyList() throws Exception { + CollectionToCollectionConverter converter = new CollectionToCollectionConverter(new GenericConversionService()); + TypeDescriptor type = new TypeDescriptor(getClass().getField("list")); + converter.convert(list, type, TypeDescriptor.valueOf(Class.forName("java.util.Collections$EmptyList"))); + } + + @SuppressWarnings("rawtypes") + private void testCollectionConversionToArrayList(Collection aSource) { + Object myConverted = (new CollectionToCollectionConverter(new GenericConversionService())).convert( + aSource, TypeDescriptor.forObject(aSource), TypeDescriptor.forObject(new ArrayList())); + boolean condition = myConverted instanceof ArrayList; + assertThat(condition).isTrue(); + assertThat(((ArrayList) myConverted).size()).isEqualTo(aSource.size()); + } + + @Test + void listToCollectionNoCopyRequired() throws NoSuchFieldException { + List input = new ArrayList<>(Arrays.asList("foo", "bar")); + assertThat(conversionService.convert(input, TypeDescriptor.forObject(input), + new TypeDescriptor(getClass().getField("wildcardCollection")))).isSameAs(input); + } + + @Test + void differentImpls() throws Exception { + List resources = new ArrayList<>(); + resources.add(new ClassPathResource("test")); + resources.add(new FileSystemResource("test")); + resources.add(new TestResource()); + TypeDescriptor sourceType = TypeDescriptor.forObject(resources); + assertThat(conversionService.convert(resources, sourceType, new TypeDescriptor(getClass().getField("resources")))).isSameAs(resources); + } + + @Test + void mixedInNulls() throws Exception { + List resources = new ArrayList<>(); + resources.add(new ClassPathResource("test")); + resources.add(null); + resources.add(new FileSystemResource("test")); + resources.add(new TestResource()); + TypeDescriptor sourceType = TypeDescriptor.forObject(resources); + assertThat(conversionService.convert(resources, sourceType, new TypeDescriptor(getClass().getField("resources")))).isSameAs(resources); + } + + @Test + void allNulls() throws Exception { + List resources = new ArrayList<>(); + resources.add(null); + resources.add(null); + TypeDescriptor sourceType = TypeDescriptor.forObject(resources); + assertThat(conversionService.convert(resources, sourceType, new TypeDescriptor(getClass().getField("resources")))).isSameAs(resources); + } + + @Test + void elementTypesNotConvertible() throws Exception { + List resources = new ArrayList<>(); + resources.add(null); + resources.add(null); + TypeDescriptor sourceType = new TypeDescriptor(getClass().getField("strings")); + assertThatExceptionOfType(ConverterNotFoundException.class).isThrownBy(() -> + conversionService.convert(resources, sourceType, new TypeDescriptor(getClass().getField("resources")))); + } + + @Test + void nothingInCommon() throws Exception { + List resources = new ArrayList<>(); + resources.add(new ClassPathResource("test")); + resources.add(3); + TypeDescriptor sourceType = TypeDescriptor.forObject(resources); + assertThatExceptionOfType(ConversionFailedException.class).isThrownBy(() -> + conversionService.convert(resources, sourceType, new TypeDescriptor(getClass().getField("resources")))); + } + + @Test + void stringToEnumSet() throws Exception { + conversionService.addConverterFactory(new StringToEnumConverterFactory()); + List list = new ArrayList<>(); + list.add("A"); + list.add("C"); + assertThat(conversionService.convert(list, TypeDescriptor.forObject(list), new TypeDescriptor(getClass().getField("enumSet")))).isEqualTo(EnumSet.of(MyEnum.A, MyEnum.C)); + } + + + public ArrayList scalarListTarget; + + public List emptyListTarget; + + public ArrayList emptyListDifferentTarget; + + public List>> objectToCollection; + + public List strings; + + public List list = Collections.emptyList(); + + public Collection wildcardCollection = Collections.emptyList(); + + public List resources; + + public EnumSet enumSet; + + + public static abstract class BaseResource implements Resource { + + @Override + public InputStream getInputStream() throws IOException { + return null; + } + + @Override + public boolean exists() { + return false; + } + + @Override + public boolean isReadable() { + return false; + } + + @Override + public boolean isOpen() { + return false; + } + + @Override + public boolean isFile() { + return false; + } + + @Override + public URL getURL() throws IOException { + return null; + } + + @Override + public URI getURI() throws IOException { + return null; + } + + @Override + public File getFile() throws IOException { + return null; + } + + @Override + public long contentLength() throws IOException { + return 0; + } + + @Override + public long lastModified() throws IOException { + return 0; + } + + @Override + public Resource createRelative(String relativePath) throws IOException { + return null; + } + + @Override + public String getFilename() { + return null; + } + + @Override + public String getDescription() { + return null; + } + } + + + public static class TestResource extends BaseResource { + } + + + public enum MyEnum {A, B, C} + +} diff --git a/spring-core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java b/spring-core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java new file mode 100644 index 0000000..01c4027 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java @@ -0,0 +1,937 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.awt.Color; +import java.awt.SystemColor; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.core.convert.ConverterNotFoundException; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalConverter; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.core.convert.converter.GenericConverter; +import org.springframework.core.io.DescriptiveResource; +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +import static java.util.Comparator.naturalOrder; +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Unit tests for {@link GenericConversionService}. + * + *

    In this package for access to package-local converter implementations. + * + * @author Keith Donald + * @author Juergen Hoeller + * @author Phillip Webb + * @author David Haraburda + * @author Sam Brannen + */ +class GenericConversionServiceTests { + + private final GenericConversionService conversionService = new GenericConversionService(); + + + @Test + void canConvert() { + assertThat(conversionService.canConvert(String.class, Integer.class)).isFalse(); + conversionService.addConverterFactory(new StringToNumberConverterFactory()); + assertThat(conversionService.canConvert(String.class, Integer.class)).isTrue(); + } + + @Test + void canConvertAssignable() { + assertThat(conversionService.canConvert(String.class, String.class)).isTrue(); + assertThat(conversionService.canConvert(Integer.class, Number.class)).isTrue(); + assertThat(conversionService.canConvert(boolean.class, boolean.class)).isTrue(); + assertThat(conversionService.canConvert(boolean.class, Boolean.class)).isTrue(); + } + + @Test + void canConvertFromClassSourceTypeToNullTargetType() { + assertThatIllegalArgumentException().isThrownBy(() -> + conversionService.canConvert(String.class, null)); + } + + @Test + void canConvertFromTypeDescriptorSourceTypeToNullTargetType() { + assertThatIllegalArgumentException().isThrownBy(() -> + conversionService.canConvert(TypeDescriptor.valueOf(String.class), null)); + } + + @Test + void canConvertNullSourceType() { + assertThat(conversionService.canConvert(null, Integer.class)).isTrue(); + assertThat(conversionService.canConvert(null, TypeDescriptor.valueOf(Integer.class))).isTrue(); + } + + @Test + void convert() { + conversionService.addConverterFactory(new StringToNumberConverterFactory()); + assertThat(conversionService.convert("3", Integer.class)).isEqualTo((int) Integer.valueOf(3)); + } + + @Test + void convertNullSource() { + assertThat(conversionService.convert(null, Integer.class)).isEqualTo(null); + } + + @Test + void convertNullSourcePrimitiveTarget() { + assertThatExceptionOfType(ConversionFailedException.class).isThrownBy(() -> + conversionService.convert(null, int.class)); + } + + @Test + void convertNullSourcePrimitiveTargetTypeDescriptor() { + assertThatExceptionOfType(ConversionFailedException.class).isThrownBy(() -> + conversionService.convert(null, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(int.class))); + } + + @Test + void convertNotNullSourceNullSourceTypeDescriptor() { + assertThatIllegalArgumentException().isThrownBy(() -> + conversionService.convert("3", null, TypeDescriptor.valueOf(int.class))); + } + + @Test + void convertAssignableSource() { + assertThat(conversionService.convert(false, boolean.class)).isEqualTo(Boolean.FALSE); + assertThat(conversionService.convert(false, Boolean.class)).isEqualTo(Boolean.FALSE); + } + + @Test + void converterNotFound() { + assertThatExceptionOfType(ConverterNotFoundException.class).isThrownBy(() -> + conversionService.convert("3", Integer.class)); + } + + @Test + void addConverterNoSourceTargetClassInfoAvailable() { + assertThatIllegalArgumentException().isThrownBy(() -> + conversionService.addConverter(new UntypedConverter())); + } + + @Test + void sourceTypeIsVoid() { + assertThat(conversionService.canConvert(void.class, String.class)).isFalse(); + } + + @Test + void targetTypeIsVoid() { + assertThat(conversionService.canConvert(String.class, void.class)).isFalse(); + } + + @Test + void convertNull() { + assertThat(conversionService.convert(null, Integer.class)).isNull(); + } + + @Test + void convertToNullTargetClass() { + assertThatIllegalArgumentException().isThrownBy(() -> + conversionService.convert("3", (Class) null)); + } + + @Test + void convertToNullTargetTypeDescriptor() { + assertThatIllegalArgumentException().isThrownBy(() -> + conversionService.convert("3", TypeDescriptor.valueOf(String.class), null)); + } + + @Test + void convertWrongSourceTypeDescriptor() { + assertThatIllegalArgumentException().isThrownBy(() -> + conversionService.convert("3", TypeDescriptor.valueOf(Integer.class), TypeDescriptor.valueOf(Long.class))); + } + + @Test + void convertWrongTypeArgument() { + conversionService.addConverterFactory(new StringToNumberConverterFactory()); + assertThatExceptionOfType(ConversionFailedException.class).isThrownBy(() -> + conversionService.convert("BOGUS", Integer.class)); + } + + @Test + void convertSuperSourceType() { + conversionService.addConverter(new Converter() { + @Override + public Integer convert(CharSequence source) { + return Integer.valueOf(source.toString()); + } + }); + Integer result = conversionService.convert("3", Integer.class); + assertThat((int) result).isEqualTo((int) Integer.valueOf(3)); + } + + // SPR-8718 + @Test + void convertSuperTarget() { + conversionService.addConverter(new ColorConverter()); + assertThatExceptionOfType(ConverterNotFoundException.class).isThrownBy(() -> + conversionService.convert("#000000", SystemColor.class)); + } + + @Test + void convertObjectToPrimitive() { + assertThat(conversionService.canConvert(String.class, boolean.class)).isFalse(); + conversionService.addConverter(new StringToBooleanConverter()); + assertThat(conversionService.canConvert(String.class, boolean.class)).isTrue(); + Boolean b = conversionService.convert("true", boolean.class); + assertThat(b).isTrue(); + assertThat(conversionService.canConvert(TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(boolean.class))).isTrue(); + b = (Boolean) conversionService.convert("true", TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(boolean.class)); + assertThat(b).isTrue(); + } + + @Test + void convertObjectToPrimitiveViaConverterFactory() { + assertThat(conversionService.canConvert(String.class, int.class)).isFalse(); + conversionService.addConverterFactory(new StringToNumberConverterFactory()); + assertThat(conversionService.canConvert(String.class, int.class)).isTrue(); + Integer three = conversionService.convert("3", int.class); + assertThat(three.intValue()).isEqualTo(3); + } + + @Test + void genericConverterDelegatingBackToConversionServiceConverterNotFound() { + conversionService.addConverter(new ObjectToArrayConverter(conversionService)); + assertThat(conversionService.canConvert(String.class, Integer[].class)).isFalse(); + assertThatExceptionOfType(ConverterNotFoundException.class).isThrownBy(() -> + conversionService.convert("3,4,5", Integer[].class)); + } + + @Test + void listToIterableConversion() { + List raw = new ArrayList<>(); + raw.add("one"); + raw.add("two"); + Object converted = conversionService.convert(raw, Iterable.class); + assertThat(converted).isSameAs(raw); + } + + @Test + void listToObjectConversion() { + List raw = new ArrayList<>(); + raw.add("one"); + raw.add("two"); + Object converted = conversionService.convert(raw, Object.class); + assertThat(converted).isSameAs(raw); + } + + @Test + void mapToObjectConversion() { + Map raw = new HashMap<>(); + raw.put("key", "value"); + Object converted = conversionService.convert(raw, Object.class); + assertThat(converted).isSameAs(raw); + } + + @Test + void interfaceToString() { + conversionService.addConverter(new MyBaseInterfaceToStringConverter()); + conversionService.addConverter(new ObjectToStringConverter()); + Object converted = conversionService.convert(new MyInterfaceImplementer(), String.class); + assertThat(converted).isEqualTo("RESULT"); + } + + @Test + void interfaceArrayToStringArray() { + conversionService.addConverter(new MyBaseInterfaceToStringConverter()); + conversionService.addConverter(new ArrayToArrayConverter(conversionService)); + String[] converted = conversionService.convert(new MyInterface[] {new MyInterfaceImplementer()}, String[].class); + assertThat(converted[0]).isEqualTo("RESULT"); + } + + @Test + void objectArrayToStringArray() { + conversionService.addConverter(new MyBaseInterfaceToStringConverter()); + conversionService.addConverter(new ArrayToArrayConverter(conversionService)); + String[] converted = conversionService.convert(new MyInterfaceImplementer[] {new MyInterfaceImplementer()}, String[].class); + assertThat(converted[0]).isEqualTo("RESULT"); + } + + @Test + void stringArrayToResourceArray() { + conversionService.addConverter(new MyStringArrayToResourceArrayConverter()); + Resource[] converted = conversionService.convert(new String[] { "x1", "z3" }, Resource[].class); + List descriptions = Arrays.stream(converted).map(Resource::getDescription).sorted(naturalOrder()).collect(toList()); + assertThat(descriptions).isEqualTo(Arrays.asList("1", "3")); + } + + @Test + void stringArrayToIntegerArray() { + conversionService.addConverter(new MyStringArrayToIntegerArrayConverter()); + Integer[] converted = conversionService.convert(new String[] {"x1", "z3"}, Integer[].class); + assertThat(converted).isEqualTo(new Integer[] { 1, 3 }); + } + + @Test + void stringToIntegerArray() { + conversionService.addConverter(new MyStringToIntegerArrayConverter()); + Integer[] converted = conversionService.convert("x1,z3", Integer[].class); + assertThat(converted).isEqualTo(new Integer[] { 1, 3 }); + } + + @Test + void wildcardMap() throws Exception { + Map input = new LinkedHashMap<>(); + input.put("key", "value"); + Object converted = conversionService.convert(input, TypeDescriptor.forObject(input), new TypeDescriptor(getClass().getField("wildcardMap"))); + assertThat(converted).isEqualTo(input); + } + + @Test + void stringToString() { + String value = "myValue"; + String result = conversionService.convert(value, String.class); + assertThat(result).isSameAs(value); + } + + @Test + void stringToObject() { + String value = "myValue"; + Object result = conversionService.convert(value, Object.class); + assertThat(result).isSameAs(value); + } + + @Test + void ignoreCopyConstructor() { + WithCopyConstructor value = new WithCopyConstructor(); + Object result = conversionService.convert(value, WithCopyConstructor.class); + assertThat(result).isSameAs(value); + } + + @Test + void emptyListToArray() { + conversionService.addConverter(new CollectionToArrayConverter(conversionService)); + conversionService.addConverterFactory(new StringToNumberConverterFactory()); + List list = new ArrayList<>(); + TypeDescriptor sourceType = TypeDescriptor.forObject(list); + TypeDescriptor targetType = TypeDescriptor.valueOf(String[].class); + assertThat(conversionService.canConvert(sourceType, targetType)).isTrue(); + assertThat(((String[]) conversionService.convert(list, sourceType, targetType)).length).isEqualTo(0); + } + + @Test + void emptyListToObject() { + conversionService.addConverter(new CollectionToObjectConverter(conversionService)); + conversionService.addConverterFactory(new StringToNumberConverterFactory()); + List list = new ArrayList<>(); + TypeDescriptor sourceType = TypeDescriptor.forObject(list); + TypeDescriptor targetType = TypeDescriptor.valueOf(Integer.class); + assertThat(conversionService.canConvert(sourceType, targetType)).isTrue(); + assertThat(conversionService.convert(list, sourceType, targetType)).isNull(); + } + + @Test + void stringToArrayCanConvert() { + conversionService.addConverter(new StringToArrayConverter(conversionService)); + assertThat(conversionService.canConvert(String.class, Integer[].class)).isFalse(); + conversionService.addConverterFactory(new StringToNumberConverterFactory()); + assertThat(conversionService.canConvert(String.class, Integer[].class)).isTrue(); + } + + @Test + void stringToCollectionCanConvert() throws Exception { + conversionService.addConverter(new StringToCollectionConverter(conversionService)); + assertThat(conversionService.canConvert(String.class, Collection.class)).isTrue(); + TypeDescriptor targetType = new TypeDescriptor(getClass().getField("integerCollection")); + assertThat(conversionService.canConvert(TypeDescriptor.valueOf(String.class), targetType)).isFalse(); + conversionService.addConverterFactory(new StringToNumberConverterFactory()); + assertThat(conversionService.canConvert(TypeDescriptor.valueOf(String.class), targetType)).isTrue(); + } + + @Test + void convertiblePairsInSet() { + Set set = new HashSet<>(); + set.add(new GenericConverter.ConvertiblePair(Number.class, String.class)); + assert set.contains(new GenericConverter.ConvertiblePair(Number.class, String.class)); + } + + @Test + void convertiblePairEqualsAndHash() { + GenericConverter.ConvertiblePair pair = new GenericConverter.ConvertiblePair(Number.class, String.class); + GenericConverter.ConvertiblePair pairEqual = new GenericConverter.ConvertiblePair(Number.class, String.class); + assertThat(pairEqual).isEqualTo(pair); + assertThat(pairEqual.hashCode()).isEqualTo(pair.hashCode()); + } + + @Test + void convertiblePairDifferentEqualsAndHash() { + GenericConverter.ConvertiblePair pair = new GenericConverter.ConvertiblePair(Number.class, String.class); + GenericConverter.ConvertiblePair pairOpposite = new GenericConverter.ConvertiblePair(String.class, Number.class); + assertThat(pair.equals(pairOpposite)).isFalse(); + assertThat(pair.hashCode() == pairOpposite.hashCode()).isFalse(); + } + + @Test + void canConvertIllegalArgumentNullTargetTypeFromClass() { + assertThatIllegalArgumentException().isThrownBy(() -> + conversionService.canConvert(String.class, null)); + } + + @Test + void canConvertIllegalArgumentNullTargetTypeFromTypeDescriptor() { + assertThatIllegalArgumentException().isThrownBy(() -> + conversionService.canConvert(TypeDescriptor.valueOf(String.class), null)); + } + + @Test + void removeConvertible() { + conversionService.addConverter(new ColorConverter()); + assertThat(conversionService.canConvert(String.class, Color.class)).isTrue(); + conversionService.removeConvertible(String.class, Color.class); + assertThat(conversionService.canConvert(String.class, Color.class)).isFalse(); + } + + @Test + void conditionalConverter() { + MyConditionalConverter converter = new MyConditionalConverter(); + conversionService.addConverter(new ColorConverter()); + conversionService.addConverter(converter); + assertThat(conversionService.convert("#000000", Color.class)).isEqualTo(Color.BLACK); + assertThat(converter.getMatchAttempts() > 0).isTrue(); + } + + @Test + void conditionalConverterFactory() { + MyConditionalConverterFactory converter = new MyConditionalConverterFactory(); + conversionService.addConverter(new ColorConverter()); + conversionService.addConverterFactory(converter); + assertThat(conversionService.convert("#000000", Color.class)).isEqualTo(Color.BLACK); + assertThat(converter.getMatchAttempts() > 0).isTrue(); + assertThat(converter.getNestedMatchAttempts() > 0).isTrue(); + } + + @Test + void conditionalConverterCachingForDifferentAnnotationAttributes() throws Exception { + conversionService.addConverter(new ColorConverter()); + conversionService.addConverter(new MyConditionalColorConverter()); + + assertThat(conversionService.convert("000000xxxx", + new TypeDescriptor(getClass().getField("activeColor")))).isEqualTo(Color.BLACK); + assertThat(conversionService.convert(" #000000 ", + new TypeDescriptor(getClass().getField("inactiveColor")))).isEqualTo(Color.BLACK); + assertThat(conversionService.convert("000000yyyy", + new TypeDescriptor(getClass().getField("activeColor")))).isEqualTo(Color.BLACK); + assertThat(conversionService.convert(" #000000 ", + new TypeDescriptor(getClass().getField("inactiveColor")))).isEqualTo(Color.BLACK); + } + + @Test + void shouldNotSupportNullConvertibleTypesFromNonConditionalGenericConverter() { + GenericConverter converter = new NonConditionalGenericConverter(); + assertThatIllegalStateException().isThrownBy(() -> + conversionService.addConverter(converter)) + .withMessage("Only conditional converters may return null convertible types"); + } + + @Test + void conditionalConversionForAllTypes() { + MyConditionalGenericConverter converter = new MyConditionalGenericConverter(); + conversionService.addConverter(converter); + assertThat(conversionService.convert(3, Integer.class)).isEqualTo(3); + assertThat(converter.getSourceTypes().size()).isGreaterThan(2); + assertThat(converter.getSourceTypes().stream().allMatch(td -> Integer.class.equals(td.getType()))).isTrue(); + } + + @Test + void convertOptimizeArray() { + // SPR-9566 + byte[] byteArray = new byte[] { 1, 2, 3 }; + byte[] converted = conversionService.convert(byteArray, byte[].class); + assertThat(converted).isSameAs(byteArray); + } + + @Test + void enumToStringConversion() { + conversionService.addConverter(new EnumToStringConverter(conversionService)); + assertThat(conversionService.convert(MyEnum.A, String.class)).isEqualTo("A"); + } + + @Test + void subclassOfEnumToString() throws Exception { + conversionService.addConverter(new EnumToStringConverter(conversionService)); + assertThat(conversionService.convert(EnumWithSubclass.FIRST, String.class)).isEqualTo("FIRST"); + } + + @Test + void enumWithInterfaceToStringConversion() { + // SPR-9692 + conversionService.addConverter(new EnumToStringConverter(conversionService)); + conversionService.addConverter(new MyEnumInterfaceToStringConverter()); + assertThat(conversionService.convert(MyEnum.A, String.class)).isEqualTo("1"); + } + + @Test + void stringToEnumWithInterfaceConversion() { + conversionService.addConverterFactory(new StringToEnumConverterFactory()); + conversionService.addConverterFactory(new StringToMyEnumInterfaceConverterFactory()); + assertThat(conversionService.convert("1", MyEnum.class)).isEqualTo(MyEnum.A); + } + + @Test + void stringToEnumWithBaseInterfaceConversion() { + conversionService.addConverterFactory(new StringToEnumConverterFactory()); + conversionService.addConverterFactory(new StringToMyEnumBaseInterfaceConverterFactory()); + assertThat(conversionService.convert("base1", MyEnum.class)).isEqualTo(MyEnum.A); + } + + @Test + void convertNullAnnotatedStringToString() throws Exception { + String source = null; + TypeDescriptor sourceType = new TypeDescriptor(getClass().getField("annotatedString")); + TypeDescriptor targetType = TypeDescriptor.valueOf(String.class); + conversionService.convert(source, sourceType, targetType); + } + + @Test + void multipleCollectionTypesFromSameSourceType() throws Exception { + conversionService.addConverter(new MyStringToRawCollectionConverter()); + conversionService.addConverter(new MyStringToGenericCollectionConverter()); + conversionService.addConverter(new MyStringToStringCollectionConverter()); + conversionService.addConverter(new MyStringToIntegerCollectionConverter()); + + assertThat(conversionService.convert("test", TypeDescriptor.valueOf(String.class), new TypeDescriptor(getClass().getField("stringCollection")))).isEqualTo(Collections.singleton("testX")); + assertThat(conversionService.convert("test", TypeDescriptor.valueOf(String.class), new TypeDescriptor(getClass().getField("integerCollection")))).isEqualTo(Collections.singleton(4)); + assertThat(conversionService.convert("test", TypeDescriptor.valueOf(String.class), new TypeDescriptor(getClass().getField("rawCollection")))).isEqualTo(Collections.singleton(4)); + assertThat(conversionService.convert("test", TypeDescriptor.valueOf(String.class), new TypeDescriptor(getClass().getField("genericCollection")))).isEqualTo(Collections.singleton(4)); + assertThat(conversionService.convert("test", TypeDescriptor.valueOf(String.class), new TypeDescriptor(getClass().getField("rawCollection")))).isEqualTo(Collections.singleton(4)); + assertThat(conversionService.convert("test", TypeDescriptor.valueOf(String.class), new TypeDescriptor(getClass().getField("stringCollection")))).isEqualTo(Collections.singleton("testX")); + } + + @Test + void adaptedCollectionTypesFromSameSourceType() throws Exception { + conversionService.addConverter(new MyStringToStringCollectionConverter()); + + assertThat(conversionService.convert("test", TypeDescriptor.valueOf(String.class), new TypeDescriptor(getClass().getField("stringCollection")))).isEqualTo(Collections.singleton("testX")); + assertThat(conversionService.convert("test", TypeDescriptor.valueOf(String.class), new TypeDescriptor(getClass().getField("genericCollection")))).isEqualTo(Collections.singleton("testX")); + assertThat(conversionService.convert("test", TypeDescriptor.valueOf(String.class), new TypeDescriptor(getClass().getField("rawCollection")))).isEqualTo(Collections.singleton("testX")); + assertThat(conversionService.convert("test", TypeDescriptor.valueOf(String.class), new TypeDescriptor(getClass().getField("genericCollection")))).isEqualTo(Collections.singleton("testX")); + assertThat(conversionService.convert("test", TypeDescriptor.valueOf(String.class), new TypeDescriptor(getClass().getField("stringCollection")))).isEqualTo(Collections.singleton("testX")); + assertThat(conversionService.convert("test", TypeDescriptor.valueOf(String.class), new TypeDescriptor(getClass().getField("rawCollection")))).isEqualTo(Collections.singleton("testX")); + + assertThatExceptionOfType(ConverterNotFoundException.class).isThrownBy(() -> + conversionService.convert("test", TypeDescriptor.valueOf(String.class), new TypeDescriptor(getClass().getField("integerCollection")))); + } + + @Test + void genericCollectionAsSource() throws Exception { + conversionService.addConverter(new MyStringToGenericCollectionConverter()); + + assertThat(conversionService.convert("test", TypeDescriptor.valueOf(String.class), new TypeDescriptor(getClass().getField("stringCollection")))).isEqualTo(Collections.singleton("testX")); + assertThat(conversionService.convert("test", TypeDescriptor.valueOf(String.class), new TypeDescriptor(getClass().getField("genericCollection")))).isEqualTo(Collections.singleton("testX")); + assertThat(conversionService.convert("test", TypeDescriptor.valueOf(String.class), new TypeDescriptor(getClass().getField("rawCollection")))).isEqualTo(Collections.singleton("testX")); + + // The following is unpleasant but a consequence of the generic collection converter above... + assertThat(conversionService.convert("test", TypeDescriptor.valueOf(String.class), new TypeDescriptor(getClass().getField("integerCollection")))).isEqualTo(Collections.singleton("testX")); + } + + @Test + void rawCollectionAsSource() throws Exception { + conversionService.addConverter(new MyStringToRawCollectionConverter()); + + assertThat(conversionService.convert("test", TypeDescriptor.valueOf(String.class), new TypeDescriptor(getClass().getField("stringCollection")))).isEqualTo(Collections.singleton("testX")); + assertThat(conversionService.convert("test", TypeDescriptor.valueOf(String.class), new TypeDescriptor(getClass().getField("genericCollection")))).isEqualTo(Collections.singleton("testX")); + assertThat(conversionService.convert("test", TypeDescriptor.valueOf(String.class), new TypeDescriptor(getClass().getField("rawCollection")))).isEqualTo(Collections.singleton("testX")); + + // The following is unpleasant but a consequence of the raw collection converter above... + assertThat(conversionService.convert("test", TypeDescriptor.valueOf(String.class), new TypeDescriptor(getClass().getField("integerCollection")))).isEqualTo(Collections.singleton("testX")); + } + + + @ExampleAnnotation(active = true) + public String annotatedString; + + @ExampleAnnotation(active = true) + public Color activeColor; + + @ExampleAnnotation(active = false) + public Color inactiveColor; + + public Map wildcardMap; + + @SuppressWarnings("rawtypes") + public Collection rawCollection; + + public Collection genericCollection; + + public Collection stringCollection; + + public Collection integerCollection; + + + @Retention(RetentionPolicy.RUNTIME) + private @interface ExampleAnnotation { + + boolean active(); + } + + + private interface MyBaseInterface { + } + + + private interface MyInterface extends MyBaseInterface { + } + + + private static class MyInterfaceImplementer implements MyInterface { + } + + + private static class MyBaseInterfaceToStringConverter implements Converter { + + @Override + public String convert(MyBaseInterface source) { + return "RESULT"; + } + } + + + private static class MyStringArrayToResourceArrayConverter implements Converter { + + @Override + public Resource[] convert(String[] source) { + return Arrays.stream(source).map(s -> s.substring(1)).map(DescriptiveResource::new).toArray(Resource[]::new); + } + } + + + private static class MyStringArrayToIntegerArrayConverter implements Converter { + + @Override + public Integer[] convert(String[] source) { + return Arrays.stream(source).map(s -> s.substring(1)).map(Integer::valueOf).toArray(Integer[]::new); + } + } + + + private static class MyStringToIntegerArrayConverter implements Converter { + + @Override + public Integer[] convert(String source) { + String[] srcArray = StringUtils.commaDelimitedListToStringArray(source); + return Arrays.stream(srcArray).map(s -> s.substring(1)).map(Integer::valueOf).toArray(Integer[]::new); + } + } + + + private static class WithCopyConstructor { + + WithCopyConstructor() {} + + @SuppressWarnings("unused") + WithCopyConstructor(WithCopyConstructor value) {} + } + + + private static class MyConditionalConverter implements Converter, ConditionalConverter { + + private int matchAttempts = 0; + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + matchAttempts++; + return false; + } + + @Override + public Color convert(String source) { + throw new IllegalStateException(); + } + + public int getMatchAttempts() { + return matchAttempts; + } + } + + + private static class NonConditionalGenericConverter implements GenericConverter { + + @Override + public Set getConvertibleTypes() { + return null; + } + + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + return null; + } + } + + + private static class MyConditionalGenericConverter implements GenericConverter, ConditionalConverter { + + private final List sourceTypes = new ArrayList<>(); + + @Override + public Set getConvertibleTypes() { + return null; + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + sourceTypes.add(sourceType); + return false; + } + + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + return null; + } + + public List getSourceTypes() { + return sourceTypes; + } + } + + + private static class MyConditionalConverterFactory implements ConverterFactory, ConditionalConverter { + + private MyConditionalConverter converter = new MyConditionalConverter(); + + private int matchAttempts = 0; + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + matchAttempts++; + return true; + } + + @Override + @SuppressWarnings("unchecked") + public Converter getConverter(Class targetType) { + return (Converter) converter; + } + + public int getMatchAttempts() { + return matchAttempts; + } + + public int getNestedMatchAttempts() { + return converter.getMatchAttempts(); + } + } + + private static interface MyEnumBaseInterface { + String getBaseCode(); + } + + + private interface MyEnumInterface extends MyEnumBaseInterface { + String getCode(); + } + + + private enum MyEnum implements MyEnumInterface { + + A("1"), + B("2"), + C("3"); + + private final String code; + + MyEnum(String code) { + this.code = code; + } + + @Override + public String getCode() { + return code; + } + + @Override + public String getBaseCode() { + return "base" + code; + } + } + + + private enum EnumWithSubclass { + + FIRST { + @Override + public String toString() { + return "1st"; + } + } + } + + + @SuppressWarnings("rawtypes") + private static class MyStringToRawCollectionConverter implements Converter { + + @Override + public Collection convert(String source) { + return Collections.singleton(source + "X"); + } + } + + + private static class MyStringToGenericCollectionConverter implements Converter> { + + @Override + public Collection convert(String source) { + return Collections.singleton(source + "X"); + } + } + + + private static class MyEnumInterfaceToStringConverter implements Converter { + + @Override + public String convert(T source) { + return source.getCode(); + } + } + + + private static class StringToMyEnumInterfaceConverterFactory implements ConverterFactory { + + @Override + @SuppressWarnings({"unchecked", "rawtypes"}) + public Converter getConverter(Class targetType) { + return new StringToMyEnumInterfaceConverter(targetType); + } + + private static class StringToMyEnumInterfaceConverter & MyEnumInterface> implements Converter { + + private final Class enumType; + + public StringToMyEnumInterfaceConverter(Class enumType) { + this.enumType = enumType; + } + + @Override + public T convert(String source) { + for (T value : enumType.getEnumConstants()) { + if (value.getCode().equals(source)) { + return value; + } + } + return null; + } + } + } + + + private static class StringToMyEnumBaseInterfaceConverterFactory implements ConverterFactory { + + @Override + @SuppressWarnings({"unchecked", "rawtypes"}) + public Converter getConverter(Class targetType) { + return new StringToMyEnumBaseInterfaceConverter(targetType); + } + + private static class StringToMyEnumBaseInterfaceConverter & MyEnumBaseInterface> implements Converter { + + private final Class enumType; + + public StringToMyEnumBaseInterfaceConverter(Class enumType) { + this.enumType = enumType; + } + + @Override + public T convert(String source) { + for (T value : enumType.getEnumConstants()) { + if (value.getBaseCode().equals(source)) { + return value; + } + } + return null; + } + } + } + + + private static class MyStringToStringCollectionConverter implements Converter> { + + @Override + public Collection convert(String source) { + return Collections.singleton(source + "X"); + } + } + + + private static class MyStringToIntegerCollectionConverter implements Converter> { + + @Override + public Collection convert(String source) { + return Collections.singleton(source.length()); + } + } + + + @SuppressWarnings("rawtypes") + private static class UntypedConverter implements Converter { + + @Override + public Object convert(Object source) { + return source; + } + } + + + private static class ColorConverter implements Converter { + + @Override + public Color convert(String source) { + return Color.decode(source.trim()); + } + } + + + private static class MyConditionalColorConverter implements Converter, ConditionalConverter { + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + ExampleAnnotation ann = targetType.getAnnotation(ExampleAnnotation.class); + return (ann != null && ann.active()); + } + + @Override + public Color convert(String source) { + return Color.decode(source.substring(0, 6)); + } + } +} diff --git a/spring-core/src/test/java/org/springframework/core/convert/support/MapToMapConverterTests.java b/spring-core/src/test/java/org/springframework/core/convert/support/MapToMapConverterTests.java new file mode 100644 index 0000000..fd900a7 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/convert/support/MapToMapConverterTests.java @@ -0,0 +1,307 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.core.convert.ConverterNotFoundException; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Keith Donald + * @author Phil Webb + * @author Juergen Hoeller + */ +class MapToMapConverterTests { + + private final GenericConversionService conversionService = new GenericConversionService(); + + + @BeforeEach + void setUp() { + conversionService.addConverter(new MapToMapConverter(conversionService)); + } + + + @Test + void scalarMap() throws Exception { + Map map = new HashMap<>(); + map.put("1", "9"); + map.put("2", "37"); + TypeDescriptor sourceType = TypeDescriptor.forObject(map); + TypeDescriptor targetType = new TypeDescriptor(getClass().getField("scalarMapTarget")); + + assertThat(conversionService.canConvert(sourceType, targetType)).isTrue(); + try { + conversionService.convert(map, sourceType, targetType); + } + catch (ConversionFailedException ex) { + assertThat(ex.getCause() instanceof ConverterNotFoundException).isTrue(); + } + + conversionService.addConverterFactory(new StringToNumberConverterFactory()); + assertThat(conversionService.canConvert(sourceType, targetType)).isTrue(); + @SuppressWarnings("unchecked") + Map result = (Map) conversionService.convert(map, sourceType, targetType); + assertThat(map.equals(result)).isFalse(); + assertThat((int) result.get(1)).isEqualTo(9); + assertThat((int) result.get(2)).isEqualTo(37); + } + + @Test + void scalarMapNotGenericTarget() throws Exception { + Map map = new HashMap<>(); + map.put("1", "9"); + map.put("2", "37"); + + assertThat(conversionService.canConvert(Map.class, Map.class)).isTrue(); + assertThat((Map) conversionService.convert(map, Map.class)).isSameAs(map); + } + + @Test + void scalarMapNotGenericSourceField() throws Exception { + Map map = new HashMap<>(); + map.put("1", "9"); + map.put("2", "37"); + TypeDescriptor sourceType = new TypeDescriptor(getClass().getField("notGenericMapSource")); + TypeDescriptor targetType = new TypeDescriptor(getClass().getField("scalarMapTarget")); + + assertThat(conversionService.canConvert(sourceType, targetType)).isTrue(); + try { + conversionService.convert(map, sourceType, targetType); + } + catch (ConversionFailedException ex) { + assertThat(ex.getCause() instanceof ConverterNotFoundException).isTrue(); + } + + conversionService.addConverterFactory(new StringToNumberConverterFactory()); + assertThat(conversionService.canConvert(sourceType, targetType)).isTrue(); + @SuppressWarnings("unchecked") + Map result = (Map) conversionService.convert(map, sourceType, targetType); + assertThat(map.equals(result)).isFalse(); + assertThat((int) result.get(1)).isEqualTo(9); + assertThat((int) result.get(2)).isEqualTo(37); + } + + @Test + void collectionMap() throws Exception { + Map> map = new HashMap<>(); + map.put("1", Arrays.asList("9", "12")); + map.put("2", Arrays.asList("37", "23")); + TypeDescriptor sourceType = TypeDescriptor.forObject(map); + TypeDescriptor targetType = new TypeDescriptor(getClass().getField("collectionMapTarget")); + + assertThat(conversionService.canConvert(sourceType, targetType)).isTrue(); + try { + conversionService.convert(map, sourceType, targetType); + } + catch (ConversionFailedException ex) { + assertThat(ex.getCause() instanceof ConverterNotFoundException).isTrue(); + } + + conversionService.addConverter(new CollectionToCollectionConverter(conversionService)); + conversionService.addConverterFactory(new StringToNumberConverterFactory()); + assertThat(conversionService.canConvert(sourceType, targetType)).isTrue(); + @SuppressWarnings("unchecked") + Map> result = (Map>) conversionService.convert(map, sourceType, targetType); + assertThat(map.equals(result)).isFalse(); + assertThat(result.get(1)).isEqualTo(Arrays.asList(9, 12)); + assertThat(result.get(2)).isEqualTo(Arrays.asList(37, 23)); + } + + @Test + void collectionMapSourceTarget() throws Exception { + Map> map = new HashMap<>(); + map.put("1", Arrays.asList("9", "12")); + map.put("2", Arrays.asList("37", "23")); + TypeDescriptor sourceType = new TypeDescriptor(getClass().getField("sourceCollectionMapTarget")); + TypeDescriptor targetType = new TypeDescriptor(getClass().getField("collectionMapTarget")); + + assertThat(conversionService.canConvert(sourceType, targetType)).isFalse(); + assertThatExceptionOfType(ConverterNotFoundException.class).isThrownBy(() -> + conversionService.convert(map, sourceType, targetType)); + + conversionService.addConverter(new CollectionToCollectionConverter(conversionService)); + conversionService.addConverterFactory(new StringToNumberConverterFactory()); + assertThat(conversionService.canConvert(sourceType, targetType)).isTrue(); + @SuppressWarnings("unchecked") + Map> result = (Map>) conversionService.convert(map, sourceType, targetType); + assertThat(map.equals(result)).isFalse(); + assertThat(result.get(1)).isEqualTo(Arrays.asList(9, 12)); + assertThat(result.get(2)).isEqualTo(Arrays.asList(37, 23)); + } + + @Test + void collectionMapNotGenericTarget() throws Exception { + Map> map = new HashMap<>(); + map.put("1", Arrays.asList("9", "12")); + map.put("2", Arrays.asList("37", "23")); + + assertThat(conversionService.canConvert(Map.class, Map.class)).isTrue(); + assertThat((Map) conversionService.convert(map, Map.class)).isSameAs(map); + } + + @Test + void collectionMapNotGenericTargetCollectionToObjectInteraction() throws Exception { + Map> map = new HashMap<>(); + map.put("1", Arrays.asList("9", "12")); + map.put("2", Arrays.asList("37", "23")); + conversionService.addConverter(new CollectionToCollectionConverter(conversionService)); + conversionService.addConverter(new CollectionToObjectConverter(conversionService)); + + assertThat(conversionService.canConvert(Map.class, Map.class)).isTrue(); + assertThat((Map) conversionService.convert(map, Map.class)).isSameAs(map); + } + + @Test + void emptyMap() throws Exception { + Map map = new HashMap<>(); + TypeDescriptor sourceType = TypeDescriptor.forObject(map); + TypeDescriptor targetType = new TypeDescriptor(getClass().getField("emptyMapTarget")); + + assertThat(conversionService.canConvert(sourceType, targetType)).isTrue(); + assertThat(conversionService.convert(map, sourceType, targetType)).isSameAs(map); + } + + @Test + void emptyMapNoTargetGenericInfo() throws Exception { + Map map = new HashMap<>(); + + assertThat(conversionService.canConvert(Map.class, Map.class)).isTrue(); + assertThat((Map) conversionService.convert(map, Map.class)).isSameAs(map); + } + + @Test + void emptyMapDifferentTargetImplType() throws Exception { + Map map = new HashMap<>(); + TypeDescriptor sourceType = TypeDescriptor.forObject(map); + TypeDescriptor targetType = new TypeDescriptor(getClass().getField("emptyMapDifferentTarget")); + + assertThat(conversionService.canConvert(sourceType, targetType)).isTrue(); + @SuppressWarnings("unchecked") + LinkedHashMap result = (LinkedHashMap) conversionService.convert(map, sourceType, targetType); + assertThat(result).isEqualTo(map); + assertThat(result.getClass()).isEqualTo(LinkedHashMap.class); + } + + @Test + void noDefaultConstructorCopyNotRequired() throws Exception { + // SPR-9284 + NoDefaultConstructorMap map = new NoDefaultConstructorMap<>( + Collections.singletonMap("1", 1)); + TypeDescriptor sourceType = TypeDescriptor.map(NoDefaultConstructorMap.class, + TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)); + TypeDescriptor targetType = TypeDescriptor.map(NoDefaultConstructorMap.class, + TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)); + + assertThat(conversionService.canConvert(sourceType, targetType)).isTrue(); + @SuppressWarnings("unchecked") + Map result = (Map) conversionService.convert(map, sourceType, targetType); + assertThat(result).isEqualTo(map); + assertThat(result.getClass()).isEqualTo(NoDefaultConstructorMap.class); + } + + @Test + @SuppressWarnings("unchecked") + void multiValueMapToMultiValueMap() throws Exception { + DefaultConversionService.addDefaultConverters(conversionService); + MultiValueMap source = new LinkedMultiValueMap<>(); + source.put("a", Arrays.asList(1, 2, 3)); + source.put("b", Arrays.asList(4, 5, 6)); + TypeDescriptor targetType = new TypeDescriptor(getClass().getField("multiValueMapTarget")); + + MultiValueMap converted = (MultiValueMap) conversionService.convert(source, targetType); + assertThat(converted.size()).isEqualTo(2); + assertThat(converted.get("a")).isEqualTo(Arrays.asList("1", "2", "3")); + assertThat(converted.get("b")).isEqualTo(Arrays.asList("4", "5", "6")); + } + + @Test + @SuppressWarnings("unchecked") + void mapToMultiValueMap() throws Exception { + DefaultConversionService.addDefaultConverters(conversionService); + Map source = new HashMap<>(); + source.put("a", 1); + source.put("b", 2); + TypeDescriptor targetType = new TypeDescriptor(getClass().getField("multiValueMapTarget")); + + MultiValueMap converted = (MultiValueMap) conversionService.convert(source, targetType); + assertThat(converted.size()).isEqualTo(2); + assertThat(converted.get("a")).isEqualTo(Arrays.asList("1")); + assertThat(converted.get("b")).isEqualTo(Arrays.asList("2")); + } + + @Test + void stringToEnumMap() throws Exception { + conversionService.addConverterFactory(new StringToEnumConverterFactory()); + Map source = new HashMap<>(); + source.put("A", 1); + source.put("C", 2); + EnumMap result = new EnumMap<>(MyEnum.class); + result.put(MyEnum.A, 1); + result.put(MyEnum.C, 2); + + assertThat(conversionService.convert(source, + TypeDescriptor.forObject(source), new TypeDescriptor(getClass().getField("enumMap")))).isEqualTo(result); + } + + + public Map scalarMapTarget; + + public Map> collectionMapTarget; + + public Map> sourceCollectionMapTarget; + + public Map emptyMapTarget; + + public LinkedHashMap emptyMapDifferentTarget; + + public MultiValueMap multiValueMapTarget; + + @SuppressWarnings("rawtypes") + public Map notGenericMapSource; + + public EnumMap enumMap; + + + @SuppressWarnings("serial") + public static class NoDefaultConstructorMap extends HashMap { + + public NoDefaultConstructorMap(Map map) { + super(map); + } + } + + + public enum MyEnum {A, B, C} + +} diff --git a/spring-core/src/test/java/org/springframework/core/convert/support/StreamConverterTests.java b/spring-core/src/test/java/org/springframework/core/convert/support/StreamConverterTests.java new file mode 100644 index 0000000..42f4041 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/convert/support/StreamConverterTests.java @@ -0,0 +1,203 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.convert.support; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.core.convert.ConverterNotFoundException; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.Converter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link StreamConverter}. + * + * @author Stephane Nicoll + * @since 4.2 + */ +class StreamConverterTests { + + private final GenericConversionService conversionService = new GenericConversionService(); + + private final StreamConverter streamConverter = new StreamConverter(this.conversionService); + + + @BeforeEach + void setup() { + this.conversionService.addConverter(new CollectionToCollectionConverter(this.conversionService)); + this.conversionService.addConverter(new ArrayToCollectionConverter(this.conversionService)); + this.conversionService.addConverter(new CollectionToArrayConverter(this.conversionService)); + this.conversionService.addConverter(this.streamConverter); + } + + + @Test + void convertFromStreamToList() throws NoSuchFieldException { + this.conversionService.addConverter(Number.class, String.class, new ObjectToStringConverter()); + Stream stream = Arrays.asList(1, 2, 3).stream(); + TypeDescriptor listOfStrings = new TypeDescriptor(Types.class.getField("listOfStrings")); + Object result = this.conversionService.convert(stream, listOfStrings); + + assertThat(result).as("Converted object must not be null").isNotNull(); + boolean condition = result instanceof List; + assertThat(condition).as("Converted object must be a list").isTrue(); + @SuppressWarnings("unchecked") + List content = (List) result; + assertThat(content.get(0)).isEqualTo("1"); + assertThat(content.get(1)).isEqualTo("2"); + assertThat(content.get(2)).isEqualTo("3"); + assertThat(content.size()).as("Wrong number of elements").isEqualTo(3); + } + + @Test + void convertFromStreamToArray() throws NoSuchFieldException { + this.conversionService.addConverterFactory(new NumberToNumberConverterFactory()); + Stream stream = Arrays.asList(1, 2, 3).stream(); + TypeDescriptor arrayOfLongs = new TypeDescriptor(Types.class.getField("arrayOfLongs")); + Object result = this.conversionService.convert(stream, arrayOfLongs); + + assertThat(result).as("Converted object must not be null").isNotNull(); + assertThat(result.getClass().isArray()).as("Converted object must be an array").isTrue(); + Long[] content = (Long[]) result; + assertThat(content[0]).isEqualTo(Long.valueOf(1L)); + assertThat(content[1]).isEqualTo(Long.valueOf(2L)); + assertThat(content[2]).isEqualTo(Long.valueOf(3L)); + assertThat(content.length).as("Wrong number of elements").isEqualTo(3); + } + + @Test + void convertFromStreamToRawList() throws NoSuchFieldException { + Stream stream = Arrays.asList(1, 2, 3).stream(); + TypeDescriptor listOfStrings = new TypeDescriptor(Types.class.getField("rawList")); + Object result = this.conversionService.convert(stream, listOfStrings); + + assertThat(result).as("Converted object must not be null").isNotNull(); + boolean condition = result instanceof List; + assertThat(condition).as("Converted object must be a list").isTrue(); + @SuppressWarnings("unchecked") + List content = (List) result; + assertThat(content.get(0)).isEqualTo(1); + assertThat(content.get(1)).isEqualTo(2); + assertThat(content.get(2)).isEqualTo(3); + assertThat(content.size()).as("Wrong number of elements").isEqualTo(3); + } + + @Test + void convertFromStreamToArrayNoConverter() throws NoSuchFieldException { + Stream stream = Arrays.asList(1, 2, 3).stream(); + TypeDescriptor arrayOfLongs = new TypeDescriptor(Types.class.getField("arrayOfLongs")); + assertThatExceptionOfType(ConversionFailedException.class).isThrownBy(() -> + this.conversionService.convert(stream, arrayOfLongs)) + .withCauseInstanceOf(ConverterNotFoundException.class); + } + + @Test + @SuppressWarnings("resource") + void convertFromListToStream() throws NoSuchFieldException { + this.conversionService.addConverterFactory(new StringToNumberConverterFactory()); + List stream = Arrays.asList("1", "2", "3"); + TypeDescriptor streamOfInteger = new TypeDescriptor(Types.class.getField("streamOfIntegers")); + Object result = this.conversionService.convert(stream, streamOfInteger); + + assertThat(result).as("Converted object must not be null").isNotNull(); + boolean condition = result instanceof Stream; + assertThat(condition).as("Converted object must be a stream").isTrue(); + @SuppressWarnings("unchecked") + Stream content = (Stream) result; + assertThat(content.mapToInt(x -> x).sum()).isEqualTo(6); + } + + @Test + @SuppressWarnings("resource") + void convertFromArrayToStream() throws NoSuchFieldException { + Integer[] stream = new Integer[] {1, 0, 1}; + this.conversionService.addConverter(new Converter() { + @Override + public Boolean convert(Integer source) { + return source == 1; + } + }); + TypeDescriptor streamOfBoolean = new TypeDescriptor(Types.class.getField("streamOfBooleans")); + Object result = this.conversionService.convert(stream, streamOfBoolean); + + assertThat(result).as("Converted object must not be null").isNotNull(); + boolean condition = result instanceof Stream; + assertThat(condition).as("Converted object must be a stream").isTrue(); + @SuppressWarnings("unchecked") + Stream content = (Stream) result; + assertThat(content.filter(x -> x).count()).isEqualTo(2); + } + + @Test + @SuppressWarnings("resource") + void convertFromListToRawStream() throws NoSuchFieldException { + List stream = Arrays.asList("1", "2", "3"); + TypeDescriptor streamOfInteger = new TypeDescriptor(Types.class.getField("rawStream")); + Object result = this.conversionService.convert(stream, streamOfInteger); + + assertThat(result).as("Converted object must not be null").isNotNull(); + boolean condition = result instanceof Stream; + assertThat(condition).as("Converted object must be a stream").isTrue(); + @SuppressWarnings("unchecked") + Stream content = (Stream) result; + StringBuilder sb = new StringBuilder(); + content.forEach(sb::append); + assertThat(sb.toString()).isEqualTo("123"); + } + + @Test + void doesNotMatchIfNoStream() throws NoSuchFieldException { + assertThat(this.streamConverter.matches( + new TypeDescriptor(Types.class.getField("listOfStrings")), + new TypeDescriptor(Types.class.getField("arrayOfLongs")))).as("Should not match non stream type").isFalse(); + } + + @Test + void shouldFailToConvertIfNoStream() throws NoSuchFieldException { + TypeDescriptor sourceType = new TypeDescriptor(Types.class.getField("listOfStrings")); + TypeDescriptor targetType = new TypeDescriptor(Types.class.getField("arrayOfLongs")); + assertThatIllegalStateException().isThrownBy(() -> + this.streamConverter.convert(new Object(), sourceType, targetType)); + } + + + @SuppressWarnings({ "rawtypes" }) + static class Types { + + public List listOfStrings; + + public Long[] arrayOfLongs; + + public Stream streamOfIntegers; + + public Stream streamOfBooleans; + + public Stream rawStream; + + public List rawList; + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/env/CompositePropertySourceTests.java b/spring-core/src/test/java/org/springframework/core/env/CompositePropertySourceTests.java new file mode 100644 index 0000000..d7bafe3 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/env/CompositePropertySourceTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link CompositePropertySource}. + * + * @author Phillip Webb + */ +class CompositePropertySourceTests { + + @Test + void addFirst() { + PropertySource p1 = new MapPropertySource("p1", Collections.emptyMap()); + PropertySource p2 = new MapPropertySource("p2", Collections.emptyMap()); + PropertySource p3 = new MapPropertySource("p3", Collections.emptyMap()); + CompositePropertySource composite = new CompositePropertySource("c"); + composite.addPropertySource(p2); + composite.addPropertySource(p3); + composite.addPropertySource(p1); + composite.addFirstPropertySource(p1); + String s = composite.toString(); + int i1 = s.indexOf("name='p1'"); + int i2 = s.indexOf("name='p2'"); + int i3 = s.indexOf("name='p3'"); + assertThat(((i1 < i2) && (i2 < i3))).as("Bad order: " + s).isTrue(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/env/CustomEnvironmentTests.java b/spring-core/src/test/java/org/springframework/core/env/CustomEnvironmentTests.java new file mode 100644 index 0000000..f564c74 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/env/CustomEnvironmentTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests covering the extensibility of {@link AbstractEnvironment}. + * + * @author Chris Beams + * @since 3.1 + */ +class CustomEnvironmentTests { + + // -- tests relating to customizing reserved default profiles ---------------------- + + @Test + void control() { + Environment env = new AbstractEnvironment() { }; + assertThat(env.acceptsProfiles(defaultProfile())).isTrue(); + } + + @Test + void withNoReservedDefaultProfile() { + class CustomEnvironment extends AbstractEnvironment { + @Override + protected Set getReservedDefaultProfiles() { + return Collections.emptySet(); + } + } + + Environment env = new CustomEnvironment(); + assertThat(env.acceptsProfiles(defaultProfile())).isFalse(); + } + + @Test + void withSingleCustomReservedDefaultProfile() { + class CustomEnvironment extends AbstractEnvironment { + @Override + protected Set getReservedDefaultProfiles() { + return Collections.singleton("rd1"); + } + } + + Environment env = new CustomEnvironment(); + assertThat(env.acceptsProfiles(defaultProfile())).isFalse(); + assertThat(env.acceptsProfiles(Profiles.of("rd1"))).isTrue(); + } + + @Test + void withMultiCustomReservedDefaultProfile() { + class CustomEnvironment extends AbstractEnvironment { + @Override + @SuppressWarnings("serial") + protected Set getReservedDefaultProfiles() { + return new HashSet() {{ + add("rd1"); + add("rd2"); + }}; + } + } + + ConfigurableEnvironment env = new CustomEnvironment(); + assertThat(env.acceptsProfiles(defaultProfile())).isFalse(); + assertThat(env.acceptsProfiles(Profiles.of("rd1 | rd2"))).isTrue(); + + // finally, issue additional assertions to cover all combinations of calling these + // methods, however unlikely. + env.setDefaultProfiles("d1"); + assertThat(env.acceptsProfiles(Profiles.of("rd1 | rd2"))).isFalse(); + assertThat(env.acceptsProfiles(Profiles.of("d1"))).isTrue(); + + env.setActiveProfiles("a1", "a2"); + assertThat(env.acceptsProfiles(Profiles.of("d1"))).isFalse(); + assertThat(env.acceptsProfiles(Profiles.of("a1 | a2"))).isTrue(); + + env.setActiveProfiles(); + assertThat(env.acceptsProfiles(Profiles.of("d1"))).isTrue(); + assertThat(env.acceptsProfiles(Profiles.of("a1 | a2"))).isFalse(); + + env.setDefaultProfiles(); + assertThat(env.acceptsProfiles(defaultProfile())).isFalse(); + assertThat(env.acceptsProfiles(Profiles.of("rd1 | rd2"))).isFalse(); + assertThat(env.acceptsProfiles(Profiles.of("d1"))).isFalse(); + assertThat(env.acceptsProfiles(Profiles.of("a1 | a2"))).isFalse(); + } + + private Profiles defaultProfile() { + return Profiles.of(AbstractEnvironment.RESERVED_DEFAULT_PROFILE_NAME); + } + + + // -- tests relating to customizing property sources ------------------------------- +} diff --git a/spring-core/src/test/java/org/springframework/core/env/JOptCommandLinePropertySourceTests.java b/spring-core/src/test/java/org/springframework/core/env/JOptCommandLinePropertySourceTests.java new file mode 100644 index 0000000..0ebb4e9 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/env/JOptCommandLinePropertySourceTests.java @@ -0,0 +1,187 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import java.util.Arrays; + +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link JOptCommandLinePropertySource}. + * + * @author Chris Beams + * @author Sam Brannen + * @since 3.1 + */ +class JOptCommandLinePropertySourceTests { + + @Test + void withRequiredArg_andArgIsPresent() { + OptionParser parser = new OptionParser(); + parser.accepts("foo").withRequiredArg(); + OptionSet options = parser.parse("--foo=bar"); + + PropertySource ps = new JOptCommandLinePropertySource(options); + assertThat(ps.getProperty("foo")).isEqualTo("bar"); + } + + @Test + void withOptionalArg_andArgIsMissing() { + OptionParser parser = new OptionParser(); + parser.accepts("foo").withOptionalArg(); + OptionSet options = parser.parse("--foo"); + + PropertySource ps = new JOptCommandLinePropertySource(options); + assertThat(ps.containsProperty("foo")).isTrue(); + assertThat(ps.getProperty("foo")).isEqualTo(""); + } + + @Test // gh-24464 + void withOptionalArg_andArgIsEmpty() { + OptionParser parser = new OptionParser(); + parser.accepts("foo").withOptionalArg(); + OptionSet options = parser.parse("--foo="); + + PropertySource ps = new JOptCommandLinePropertySource(options); + assertThat(ps.containsProperty("foo")).isTrue(); + assertThat(ps.getProperty("foo")).isEqualTo(""); + } + + @Test + void withNoArg() { + OptionParser parser = new OptionParser(); + parser.accepts("o1"); + parser.accepts("o2"); + OptionSet options = parser.parse("--o1"); + + PropertySource ps = new JOptCommandLinePropertySource(options); + assertThat(ps.containsProperty("o1")).isTrue(); + assertThat(ps.containsProperty("o2")).isFalse(); + assertThat(ps.getProperty("o1")).isEqualTo(""); + assertThat(ps.getProperty("o2")).isNull(); + } + + @Test + void withRequiredArg_andMultipleArgsPresent_usingDelimiter() { + OptionParser parser = new OptionParser(); + parser.accepts("foo").withRequiredArg().withValuesSeparatedBy(','); + OptionSet options = parser.parse("--foo=bar,baz,biz"); + + CommandLinePropertySource ps = new JOptCommandLinePropertySource(options); + assertThat(ps.getOptionValues("foo")).containsExactly("bar", "baz", "biz"); + assertThat(ps.getProperty("foo")).isEqualTo("bar,baz,biz"); + } + + @Test + void withRequiredArg_andMultipleArgsPresent_usingRepeatedOption() { + OptionParser parser = new OptionParser(); + parser.accepts("foo").withRequiredArg().withValuesSeparatedBy(','); + OptionSet options = parser.parse("--foo=bar", "--foo=baz", "--foo=biz"); + + CommandLinePropertySource ps = new JOptCommandLinePropertySource(options); + assertThat(ps.getOptionValues("foo")).containsExactly("bar", "baz", "biz"); + assertThat(ps.getProperty("foo")).isEqualTo("bar,baz,biz"); + } + + @Test + void withMissingOption() { + OptionParser parser = new OptionParser(); + parser.accepts("foo").withRequiredArg().withValuesSeparatedBy(','); + OptionSet options = parser.parse(); // <-- no options whatsoever + + PropertySource ps = new JOptCommandLinePropertySource(options); + assertThat(ps.getProperty("foo")).isNull(); + } + + @Test + void withDottedOptionName() { + OptionParser parser = new OptionParser(); + parser.accepts("spring.profiles.active").withRequiredArg(); + OptionSet options = parser.parse("--spring.profiles.active=p1"); + + CommandLinePropertySource ps = new JOptCommandLinePropertySource(options); + assertThat(ps.getProperty("spring.profiles.active")).isEqualTo("p1"); + } + + @Test + void withDefaultNonOptionArgsNameAndNoNonOptionArgsPresent() { + OptionParser parser = new OptionParser(); + parser.acceptsAll(Arrays.asList("o1","option1")).withRequiredArg(); + parser.accepts("o2"); + OptionSet optionSet = parser.parse("--o1=v1", "--o2"); + EnumerablePropertySource ps = new JOptCommandLinePropertySource(optionSet); + + assertThat(ps.containsProperty("nonOptionArgs")).isFalse(); + assertThat(ps.containsProperty("o1")).isTrue(); + assertThat(ps.containsProperty("o2")).isTrue(); + + assertThat(ps.containsProperty("nonOptionArgs")).isFalse(); + assertThat(ps.getProperty("nonOptionArgs")).isNull(); + assertThat(ps.getPropertyNames()).hasSize(2); + } + + @Test + void withDefaultNonOptionArgsNameAndNonOptionArgsPresent() { + OptionParser parser = new OptionParser(); + parser.accepts("o1").withRequiredArg(); + parser.accepts("o2"); + OptionSet optionSet = parser.parse("--o1=v1", "noa1", "--o2", "noa2"); + PropertySource ps = new JOptCommandLinePropertySource(optionSet); + + assertThat(ps.containsProperty("nonOptionArgs")).isTrue(); + assertThat(ps.containsProperty("o1")).isTrue(); + assertThat(ps.containsProperty("o2")).isTrue(); + + assertThat(ps.getProperty("nonOptionArgs")).isEqualTo("noa1,noa2"); + } + + @Test + void withCustomNonOptionArgsNameAndNoNonOptionArgsPresent() { + OptionParser parser = new OptionParser(); + parser.accepts("o1").withRequiredArg(); + parser.accepts("o2"); + OptionSet optionSet = parser.parse("--o1=v1", "noa1", "--o2", "noa2"); + CommandLinePropertySource ps = new JOptCommandLinePropertySource(optionSet); + ps.setNonOptionArgsPropertyName("NOA"); + + assertThat(ps.containsProperty("nonOptionArgs")).isFalse(); + assertThat(ps.containsProperty("NOA")).isTrue(); + assertThat(ps.containsProperty("o1")).isTrue(); + assertThat(ps.containsProperty("o2")).isTrue(); + String nonOptionArgs = ps.getProperty("NOA"); + assertThat(nonOptionArgs).isEqualTo("noa1,noa2"); + } + + @Test + void withRequiredArg_ofTypeEnum() { + OptionParser parser = new OptionParser(); + parser.accepts("o1").withRequiredArg().ofType(OptionEnum.class); + OptionSet options = parser.parse("--o1=VAL_1"); + + PropertySource ps = new JOptCommandLinePropertySource(options); + assertThat(ps.getProperty("o1")).isEqualTo("VAL_1"); + } + + public enum OptionEnum { + VAL_1; + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/env/MutablePropertySourcesTests.java b/spring-core/src/test/java/org/springframework/core/env/MutablePropertySourcesTests.java new file mode 100644 index 0000000..222104a --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/env/MutablePropertySourcesTests.java @@ -0,0 +1,182 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import java.util.Iterator; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.testfixture.env.MockPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Chris Beams + * @author Juergen Hoeller + */ +class MutablePropertySourcesTests { + + @Test + void test() { + MutablePropertySources sources = new MutablePropertySources(); + sources.addLast(new MockPropertySource("b").withProperty("p1", "bValue")); + sources.addLast(new MockPropertySource("d").withProperty("p1", "dValue")); + sources.addLast(new MockPropertySource("f").withProperty("p1", "fValue")); + + assertThat(sources.size()).isEqualTo(3); + assertThat(sources.contains("a")).isFalse(); + assertThat(sources.contains("b")).isTrue(); + assertThat(sources.contains("c")).isFalse(); + assertThat(sources.contains("d")).isTrue(); + assertThat(sources.contains("e")).isFalse(); + assertThat(sources.contains("f")).isTrue(); + assertThat(sources.contains("g")).isFalse(); + + assertThat(sources.get("b")).isNotNull(); + assertThat(sources.get("b").getProperty("p1")).isEqualTo("bValue"); + assertThat(sources.get("d")).isNotNull(); + assertThat(sources.get("d").getProperty("p1")).isEqualTo("dValue"); + + sources.addBefore("b", new MockPropertySource("a")); + sources.addAfter("b", new MockPropertySource("c")); + + assertThat(sources.size()).isEqualTo(5); + assertThat(sources.precedenceOf(PropertySource.named("a"))).isEqualTo(0); + assertThat(sources.precedenceOf(PropertySource.named("b"))).isEqualTo(1); + assertThat(sources.precedenceOf(PropertySource.named("c"))).isEqualTo(2); + assertThat(sources.precedenceOf(PropertySource.named("d"))).isEqualTo(3); + assertThat(sources.precedenceOf(PropertySource.named("f"))).isEqualTo(4); + + sources.addBefore("f", new MockPropertySource("e")); + sources.addAfter("f", new MockPropertySource("g")); + + assertThat(sources.size()).isEqualTo(7); + assertThat(sources.precedenceOf(PropertySource.named("a"))).isEqualTo(0); + assertThat(sources.precedenceOf(PropertySource.named("b"))).isEqualTo(1); + assertThat(sources.precedenceOf(PropertySource.named("c"))).isEqualTo(2); + assertThat(sources.precedenceOf(PropertySource.named("d"))).isEqualTo(3); + assertThat(sources.precedenceOf(PropertySource.named("e"))).isEqualTo(4); + assertThat(sources.precedenceOf(PropertySource.named("f"))).isEqualTo(5); + assertThat(sources.precedenceOf(PropertySource.named("g"))).isEqualTo(6); + + sources.addLast(new MockPropertySource("a")); + assertThat(sources.size()).isEqualTo(7); + assertThat(sources.precedenceOf(PropertySource.named("b"))).isEqualTo(0); + assertThat(sources.precedenceOf(PropertySource.named("c"))).isEqualTo(1); + assertThat(sources.precedenceOf(PropertySource.named("d"))).isEqualTo(2); + assertThat(sources.precedenceOf(PropertySource.named("e"))).isEqualTo(3); + assertThat(sources.precedenceOf(PropertySource.named("f"))).isEqualTo(4); + assertThat(sources.precedenceOf(PropertySource.named("g"))).isEqualTo(5); + assertThat(sources.precedenceOf(PropertySource.named("a"))).isEqualTo(6); + + sources.addFirst(new MockPropertySource("a")); + assertThat(sources.size()).isEqualTo(7); + assertThat(sources.precedenceOf(PropertySource.named("a"))).isEqualTo(0); + assertThat(sources.precedenceOf(PropertySource.named("b"))).isEqualTo(1); + assertThat(sources.precedenceOf(PropertySource.named("c"))).isEqualTo(2); + assertThat(sources.precedenceOf(PropertySource.named("d"))).isEqualTo(3); + assertThat(sources.precedenceOf(PropertySource.named("e"))).isEqualTo(4); + assertThat(sources.precedenceOf(PropertySource.named("f"))).isEqualTo(5); + assertThat(sources.precedenceOf(PropertySource.named("g"))).isEqualTo(6); + + assertThat(PropertySource.named("a")).isEqualTo(sources.remove("a")); + assertThat(sources.size()).isEqualTo(6); + assertThat(sources.contains("a")).isFalse(); + + assertThat((Object) sources.remove("a")).isNull(); + assertThat(sources.size()).isEqualTo(6); + + String bogusPS = "bogus"; + assertThatIllegalArgumentException().isThrownBy(() -> + sources.addAfter(bogusPS, new MockPropertySource("h"))) + .withMessageContaining("does not exist"); + + sources.addFirst(new MockPropertySource("a")); + assertThat(sources.size()).isEqualTo(7); + assertThat(sources.precedenceOf(PropertySource.named("a"))).isEqualTo(0); + assertThat(sources.precedenceOf(PropertySource.named("b"))).isEqualTo(1); + assertThat(sources.precedenceOf(PropertySource.named("c"))).isEqualTo(2); + + sources.replace("a", new MockPropertySource("a-replaced")); + assertThat(sources.size()).isEqualTo(7); + assertThat(sources.precedenceOf(PropertySource.named("a-replaced"))).isEqualTo(0); + assertThat(sources.precedenceOf(PropertySource.named("b"))).isEqualTo(1); + assertThat(sources.precedenceOf(PropertySource.named("c"))).isEqualTo(2); + + sources.replace("a-replaced", new MockPropertySource("a")); + + assertThatIllegalArgumentException().isThrownBy(() -> + sources.replace(bogusPS, new MockPropertySource("bogus-replaced"))) + .withMessageContaining("does not exist"); + + assertThatIllegalArgumentException().isThrownBy(() -> + sources.addBefore("b", new MockPropertySource("b"))) + .withMessageContaining("cannot be added relative to itself"); + + assertThatIllegalArgumentException().isThrownBy(() -> + sources.addAfter("b", new MockPropertySource("b"))) + .withMessageContaining("cannot be added relative to itself"); + } + + @Test + void getNonExistentPropertySourceReturnsNull() { + MutablePropertySources sources = new MutablePropertySources(); + assertThat(sources.get("bogus")).isNull(); + } + + @Test + void iteratorContainsPropertySource() { + MutablePropertySources sources = new MutablePropertySources(); + sources.addLast(new MockPropertySource("test")); + + Iterator> it = sources.iterator(); + assertThat(it.hasNext()).isTrue(); + assertThat(it.next().getName()).isEqualTo("test"); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy( + it::remove); + assertThat(it.hasNext()).isFalse(); + } + + @Test + void iteratorIsEmptyForEmptySources() { + MutablePropertySources sources = new MutablePropertySources(); + Iterator> it = sources.iterator(); + assertThat(it.hasNext()).isFalse(); + } + + @Test + void streamContainsPropertySource() { + MutablePropertySources sources = new MutablePropertySources(); + sources.addLast(new MockPropertySource("test")); + + assertThat(sources.stream()).isNotNull(); + assertThat(sources.stream().count()).isEqualTo(1L); + assertThat(sources.stream().anyMatch(source -> "test".equals(source.getName()))).isTrue(); + assertThat(sources.stream().anyMatch(source -> "bogus".equals(source.getName()))).isFalse(); + } + + @Test + void streamIsEmptyForEmptySources() { + MutablePropertySources sources = new MutablePropertySources(); + assertThat(sources.stream()).isNotNull(); + assertThat(sources.stream().count()).isEqualTo(0L); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/env/ProfilesTests.java b/spring-core/src/test/java/org/springframework/core/env/ProfilesTests.java new file mode 100644 index 0000000..3e39969 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/env/ProfilesTests.java @@ -0,0 +1,389 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; +import java.util.function.Supplier; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link Profiles}. + * + * @author Phillip Webb + * @author Stephane Nicoll + * @author Sam Brannen + * @since 5.1 + */ +class ProfilesTests { + + @Test + void ofWhenNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> + Profiles.of((String[]) null)) + .withMessageContaining("Must specify at least one profile"); + } + + @Test + void ofWhenEmptyThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> + Profiles.of()) + .withMessageContaining("Must specify at least one profile"); + } + + @Test + void ofNullElement() { + assertThatIllegalArgumentException().isThrownBy(() -> + Profiles.of((String) null)) + .withMessageContaining("must contain text"); + } + + @Test + void ofEmptyElement() { + assertThatIllegalArgumentException().isThrownBy(() -> + Profiles.of(" ")) + .withMessageContaining("must contain text"); + } + + @Test + void ofSingleElement() { + Profiles profiles = Profiles.of("spring"); + assertThat(profiles.matches(activeProfiles("spring"))).isTrue(); + assertThat(profiles.matches(activeProfiles("framework"))).isFalse(); + } + + @Test + void ofSingleInvertedElement() { + Profiles profiles = Profiles.of("!spring"); + assertThat(profiles.matches(activeProfiles("spring"))).isFalse(); + assertThat(profiles.matches(activeProfiles("framework"))).isTrue(); + } + + @Test + void ofMultipleElements() { + Profiles profiles = Profiles.of("spring", "framework"); + assertThat(profiles.matches(activeProfiles("spring"))).isTrue(); + assertThat(profiles.matches(activeProfiles("framework"))).isTrue(); + assertThat(profiles.matches(activeProfiles("java"))).isFalse(); + } + + @Test + void ofMultipleElementsWithInverted() { + Profiles profiles = Profiles.of("!spring", "framework"); + assertThat(profiles.matches(activeProfiles("spring"))).isFalse(); + assertThat(profiles.matches(activeProfiles("spring", "framework"))).isTrue(); + assertThat(profiles.matches(activeProfiles("framework"))).isTrue(); + assertThat(profiles.matches(activeProfiles("java"))).isTrue(); + } + + @Test + void ofMultipleElementsAllInverted() { + Profiles profiles = Profiles.of("!spring", "!framework"); + assertThat(profiles.matches(activeProfiles("spring"))).isTrue(); + assertThat(profiles.matches(activeProfiles("framework"))).isTrue(); + assertThat(profiles.matches(activeProfiles("java"))).isTrue(); + assertThat(profiles.matches(activeProfiles("spring", "framework"))).isFalse(); + assertThat(profiles.matches(activeProfiles("spring", "framework", "java"))).isFalse(); + } + + @Test + void ofSingleExpression() { + Profiles profiles = Profiles.of("(spring)"); + assertThat(profiles.matches(activeProfiles("spring"))).isTrue(); + assertThat(profiles.matches(activeProfiles("framework"))).isFalse(); + } + + @Test + void ofSingleExpressionInverted() { + Profiles profiles = Profiles.of("!(spring)"); + assertThat(profiles.matches(activeProfiles("spring"))).isFalse(); + assertThat(profiles.matches(activeProfiles("framework"))).isTrue(); + } + + @Test + void ofSingleInvertedExpression() { + Profiles profiles = Profiles.of("(!spring)"); + assertThat(profiles.matches(activeProfiles("spring"))).isFalse(); + assertThat(profiles.matches(activeProfiles("framework"))).isTrue(); + } + + @Test + void ofOrExpression() { + Profiles profiles = Profiles.of("(spring | framework)"); + assertOrExpression(profiles); + } + + @Test + void ofOrExpressionWithoutSpaces() { + Profiles profiles = Profiles.of("(spring|framework)"); + assertOrExpression(profiles); + } + + private void assertOrExpression(Profiles profiles) { + assertThat(profiles.matches(activeProfiles("spring"))).isTrue(); + assertThat(profiles.matches(activeProfiles("framework"))).isTrue(); + assertThat(profiles.matches(activeProfiles("spring", "framework"))).isTrue(); + assertThat(profiles.matches(activeProfiles("java"))).isFalse(); + } + + @Test + void ofAndExpression() { + Profiles profiles = Profiles.of("(spring & framework)"); + assertAndExpression(profiles); + } + + @Test + void ofAndExpressionWithoutSpaces() { + Profiles profiles = Profiles.of("spring&framework)"); + assertAndExpression(profiles); + } + + @Test + void ofAndExpressionWithoutParentheses() { + Profiles profiles = Profiles.of("spring & framework"); + assertAndExpression(profiles); + } + + private void assertAndExpression(Profiles profiles) { + assertThat(profiles.matches(activeProfiles("spring"))).isFalse(); + assertThat(profiles.matches(activeProfiles("framework"))).isFalse(); + assertThat(profiles.matches(activeProfiles("spring", "framework"))).isTrue(); + assertThat(profiles.matches(activeProfiles("java"))).isFalse(); + } + + @Test + void ofNotAndExpression() { + Profiles profiles = Profiles.of("!(spring & framework)"); + assertOfNotAndExpression(profiles); + } + + @Test + void ofNotAndExpressionWithoutSpaces() { + Profiles profiles = Profiles.of("!(spring&framework)"); + assertOfNotAndExpression(profiles); + } + + private void assertOfNotAndExpression(Profiles profiles) { + assertThat(profiles.matches(activeProfiles("spring"))).isTrue(); + assertThat(profiles.matches(activeProfiles("framework"))).isTrue(); + assertThat(profiles.matches(activeProfiles("spring", "framework"))).isFalse(); + assertThat(profiles.matches(activeProfiles("java"))).isTrue(); + } + + @Test + void ofAndExpressionWithInvertedSingleElement() { + Profiles profiles = Profiles.of("!spring & framework"); + assertOfAndExpressionWithInvertedSingleElement(profiles); + } + + @Test + void ofAndExpressionWithInBracketsInvertedSingleElement() { + Profiles profiles = Profiles.of("(!spring) & framework"); + assertOfAndExpressionWithInvertedSingleElement(profiles); + } + + @Test + void ofAndExpressionWithInvertedSingleElementInBrackets() { + Profiles profiles = Profiles.of("! (spring) & framework"); + assertOfAndExpressionWithInvertedSingleElement(profiles); + } + + @Test + void ofAndExpressionWithInvertedSingleElementInBracketsWithoutSpaces() { + Profiles profiles = Profiles.of("!(spring)&framework"); + assertOfAndExpressionWithInvertedSingleElement(profiles); + } + + @Test + void ofAndExpressionWithInvertedSingleElementWithoutSpaces() { + Profiles profiles = Profiles.of("!spring&framework"); + assertOfAndExpressionWithInvertedSingleElement(profiles); + } + + private void assertOfAndExpressionWithInvertedSingleElement(Profiles profiles) { + assertThat(profiles.matches(activeProfiles("framework"))).isTrue(); + assertThat(profiles.matches(activeProfiles("java"))).isFalse(); + assertThat(profiles.matches(activeProfiles("spring", "framework"))).isFalse(); + assertThat(profiles.matches(activeProfiles("spring"))).isFalse(); + } + + @Test + void ofOrExpressionWithInvertedSingleElementWithoutSpaces() { + Profiles profiles = Profiles.of("!spring|framework"); + assertOfOrExpressionWithInvertedSingleElement(profiles); + } + + private void assertOfOrExpressionWithInvertedSingleElement(Profiles profiles) { + assertThat(profiles.matches(activeProfiles("framework"))).isTrue(); + assertThat(profiles.matches(activeProfiles("java"))).isTrue(); + assertThat(profiles.matches(activeProfiles("spring", "framework"))).isTrue(); + assertThat(profiles.matches(activeProfiles("spring"))).isFalse(); + } + + @Test + void ofNotOrExpression() { + Profiles profiles = Profiles.of("!(spring | framework)"); + assertOfNotOrExpression(profiles); + } + + @Test + void ofNotOrExpressionWithoutSpaces() { + Profiles profiles = Profiles.of("!(spring|framework)"); + assertOfNotOrExpression(profiles); + } + + private void assertOfNotOrExpression(Profiles profiles) { + assertThat(profiles.matches(activeProfiles("spring"))).isFalse(); + assertThat(profiles.matches(activeProfiles("framework"))).isFalse(); + assertThat(profiles.matches(activeProfiles("spring", "framework"))).isFalse(); + assertThat(profiles.matches(activeProfiles("java"))).isTrue(); + } + + @Test + void ofComplexExpression() { + Profiles profiles = Profiles.of("(spring & framework) | (spring & java)"); + assertComplexExpression(profiles); + } + + @Test + void ofComplexExpressionWithoutSpaces() { + Profiles profiles = Profiles.of("(spring&framework)|(spring&java)"); + assertComplexExpression(profiles); + } + + private void assertComplexExpression(Profiles profiles) { + assertThat(profiles.matches(activeProfiles("spring"))).isFalse(); + assertThat(profiles.matches(activeProfiles("spring", "framework"))).isTrue(); + assertThat(profiles.matches(activeProfiles("spring", "java"))).isTrue(); + assertThat(profiles.matches(activeProfiles("java", "framework"))).isFalse(); + } + + @Test + void malformedExpressions() { + assertMalformed(() -> Profiles.of("(")); + assertMalformed(() -> Profiles.of(")")); + assertMalformed(() -> Profiles.of("a & b | c")); + } + + @Test + void sensibleToString() { + assertThat(Profiles.of("spring")).hasToString("spring"); + assertThat(Profiles.of("(spring & framework) | (spring & java)")).hasToString("(spring & framework) | (spring & java)"); + assertThat(Profiles.of("(spring&framework)|(spring&java)")).hasToString("(spring&framework)|(spring&java)"); + assertThat(Profiles.of("spring & framework", "java | kotlin")).hasToString("spring & framework or java | kotlin"); + assertThat(Profiles.of("java | kotlin", "spring & framework")).hasToString("java | kotlin or spring & framework"); + } + + @Test + void sensibleEquals() { + assertEqual("(spring & framework) | (spring & java)"); + assertEqual("(spring&framework)|(spring&java)"); + assertEqual("spring & framework", "java | kotlin"); + + // Ensure order of individual expressions does not affect equals(). + String expression1 = "A | B"; + String expression2 = "C & (D | E)"; + Profiles profiles1 = Profiles.of(expression1, expression2); + Profiles profiles2 = Profiles.of(expression2, expression1); + assertThat(profiles1).isEqualTo(profiles2); + assertThat(profiles2).isEqualTo(profiles1); + } + + private void assertEqual(String... expressions) { + Profiles profiles1 = Profiles.of(expressions); + Profiles profiles2 = Profiles.of(expressions); + assertThat(profiles1).isEqualTo(profiles2); + assertThat(profiles2).isEqualTo(profiles1); + } + + @Test + void sensibleHashCode() { + assertHashCode("(spring & framework) | (spring & java)"); + assertHashCode("(spring&framework)|(spring&java)"); + assertHashCode("spring & framework", "java | kotlin"); + + // Ensure order of individual expressions does not affect hashCode(). + String expression1 = "A | B"; + String expression2 = "C & (D | E)"; + Profiles profiles1 = Profiles.of(expression1, expression2); + Profiles profiles2 = Profiles.of(expression2, expression1); + assertThat(profiles1).hasSameHashCodeAs(profiles2); + } + + private void assertHashCode(String... expressions) { + Profiles profiles1 = Profiles.of(expressions); + Profiles profiles2 = Profiles.of(expressions); + assertThat(profiles1).hasSameHashCodeAs(profiles2); + } + + @Test + void equalsAndHashCodeAreNotBasedOnLogicalStructureOfNodesWithinExpressionTree() { + Profiles profiles1 = Profiles.of("A | B"); + Profiles profiles2 = Profiles.of("B | A"); + + assertThat(profiles1.matches(activeProfiles("A"))).isTrue(); + assertThat(profiles1.matches(activeProfiles("B"))).isTrue(); + assertThat(profiles2.matches(activeProfiles("A"))).isTrue(); + assertThat(profiles2.matches(activeProfiles("B"))).isTrue(); + + assertThat(profiles1).isNotEqualTo(profiles2); + assertThat(profiles2).isNotEqualTo(profiles1); + assertThat(profiles1.hashCode()).isNotEqualTo(profiles2.hashCode()); + } + + + private static void assertMalformed(Supplier supplier) { + assertThatIllegalArgumentException().isThrownBy( + supplier::get) + .withMessageContaining("Malformed"); + } + + private static Predicate activeProfiles(String... profiles) { + return new MockActiveProfiles(profiles); + } + + private static class MockActiveProfiles implements Predicate { + + private final List activeProfiles; + + MockActiveProfiles(String[] activeProfiles) { + this.activeProfiles = Arrays.asList(activeProfiles); + } + + @Override + public boolean test(String profile) { + // The following if-condition (which basically mimics + // AbstractEnvironment#validateProfile(String)) is necessary in order + // to ensure that the Profiles implementation returned by Profiles.of() + // never passes an invalid (parsed) profile name to the active profiles + // predicate supplied to Profiles#matches(Predicate). + if (!StringUtils.hasText(profile) || profile.charAt(0) == '!') { + throw new IllegalArgumentException("Invalid profile [" + profile + "]"); + } + return this.activeProfiles.contains(profile); + } + + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/env/PropertySourceTests.java b/spring-core/src/test/java/org/springframework/core/env/PropertySourceTests.java new file mode 100644 index 0000000..1a7bed2 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/env/PropertySourceTests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link PropertySource} implementations. + * + * @author Chris Beams + * @since 3.1 + */ +class PropertySourceTests { + + @Test + @SuppressWarnings("serial") + void equals() { + Map map1 = new HashMap() {{ + put("a", "b"); + }}; + Map map2 = new HashMap() {{ + put("c", "d"); + }}; + Properties props1 = new Properties() {{ + setProperty("a", "b"); + }}; + Properties props2 = new Properties() {{ + setProperty("c", "d"); + }}; + + MapPropertySource mps = new MapPropertySource("mps", map1); + assertThat(mps).isEqualTo(mps); + + assertThat(new MapPropertySource("x", map1).equals(new MapPropertySource("x", map1))).isTrue(); + assertThat(new MapPropertySource("x", map1).equals(new MapPropertySource("x", map2))).isTrue(); + assertThat(new MapPropertySource("x", map1).equals(new PropertiesPropertySource("x", props1))).isTrue(); + assertThat(new MapPropertySource("x", map1).equals(new PropertiesPropertySource("x", props2))).isTrue(); + + assertThat(new MapPropertySource("x", map1).equals(new Object())).isFalse(); + assertThat(new MapPropertySource("x", map1).equals("x")).isFalse(); + assertThat(new MapPropertySource("x", map1).equals(new MapPropertySource("y", map1))).isFalse(); + assertThat(new MapPropertySource("x", map1).equals(new MapPropertySource("y", map2))).isFalse(); + assertThat(new MapPropertySource("x", map1).equals(new PropertiesPropertySource("y", props1))).isFalse(); + assertThat(new MapPropertySource("x", map1).equals(new PropertiesPropertySource("y", props2))).isFalse(); + } + + @Test + @SuppressWarnings("serial") + void collectionsOperations() { + Map map1 = new HashMap() {{ + put("a", "b"); + }}; + Map map2 = new HashMap() {{ + put("c", "d"); + }}; + + PropertySource ps1 = new MapPropertySource("ps1", map1); + ps1.getSource(); + List> propertySources = new ArrayList<>(); + assertThat(propertySources.add(ps1)).isEqualTo(true); + assertThat(propertySources.contains(ps1)).isTrue(); + assertThat(propertySources.contains(PropertySource.named("ps1"))).isTrue(); + + PropertySource ps1replacement = new MapPropertySource("ps1", map2); // notice - different map + assertThat(propertySources.add(ps1replacement)).isTrue(); // true because linkedlist allows duplicates + assertThat(propertySources).hasSize(2); + assertThat(propertySources.remove(PropertySource.named("ps1"))).isTrue(); + assertThat(propertySources).hasSize(1); + assertThat(propertySources.remove(PropertySource.named("ps1"))).isTrue(); + assertThat(propertySources).hasSize(0); + + PropertySource ps2 = new MapPropertySource("ps2", map2); + propertySources.add(ps1); + propertySources.add(ps2); + assertThat(propertySources.indexOf(PropertySource.named("ps1"))).isEqualTo(0); + assertThat(propertySources.indexOf(PropertySource.named("ps2"))).isEqualTo(1); + propertySources.clear(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/env/PropertySourcesPropertyResolverTests.java b/spring-core/src/test/java/org/springframework/core/env/PropertySourcesPropertyResolverTests.java new file mode 100644 index 0000000..f51fc97 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/env/PropertySourcesPropertyResolverTests.java @@ -0,0 +1,335 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.core.convert.ConverterNotFoundException; +import org.springframework.core.testfixture.env.MockPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * @author Chris Beams + * @since 3.1 + */ +class PropertySourcesPropertyResolverTests { + + private Properties testProperties; + + private MutablePropertySources propertySources; + + private ConfigurablePropertyResolver propertyResolver; + + + @BeforeEach + void setUp() { + propertySources = new MutablePropertySources(); + propertyResolver = new PropertySourcesPropertyResolver(propertySources); + testProperties = new Properties(); + propertySources.addFirst(new PropertiesPropertySource("testProperties", testProperties)); + } + + + @Test + void containsProperty() { + assertThat(propertyResolver.containsProperty("foo")).isFalse(); + testProperties.put("foo", "bar"); + assertThat(propertyResolver.containsProperty("foo")).isTrue(); + } + + @Test + void getProperty() { + assertThat(propertyResolver.getProperty("foo")).isNull(); + testProperties.put("foo", "bar"); + assertThat(propertyResolver.getProperty("foo")).isEqualTo("bar"); + } + + @Test + void getProperty_withDefaultValue() { + assertThat(propertyResolver.getProperty("foo", "myDefault")).isEqualTo("myDefault"); + testProperties.put("foo", "bar"); + assertThat(propertyResolver.getProperty("foo")).isEqualTo("bar"); + } + + @Test + void getProperty_propertySourceSearchOrderIsFIFO() { + MutablePropertySources sources = new MutablePropertySources(); + PropertyResolver resolver = new PropertySourcesPropertyResolver(sources); + sources.addFirst(new MockPropertySource("ps1").withProperty("pName", "ps1Value")); + assertThat(resolver.getProperty("pName")).isEqualTo("ps1Value"); + sources.addFirst(new MockPropertySource("ps2").withProperty("pName", "ps2Value")); + assertThat(resolver.getProperty("pName")).isEqualTo("ps2Value"); + sources.addFirst(new MockPropertySource("ps3").withProperty("pName", "ps3Value")); + assertThat(resolver.getProperty("pName")).isEqualTo("ps3Value"); + } + + @Test + void getProperty_withExplicitNullValue() { + // java.util.Properties does not allow null values (because Hashtable does not) + Map nullableProperties = new HashMap<>(); + propertySources.addLast(new MapPropertySource("nullableProperties", nullableProperties)); + nullableProperties.put("foo", null); + assertThat(propertyResolver.getProperty("foo")).isNull(); + } + + @Test + void getProperty_withTargetType_andDefaultValue() { + assertThat(propertyResolver.getProperty("foo", Integer.class, 42)).isEqualTo(42); + testProperties.put("foo", 13); + assertThat(propertyResolver.getProperty("foo", Integer.class, 42)).isEqualTo(13); + } + + @Test + void getProperty_withStringArrayConversion() { + testProperties.put("foo", "bar,baz"); + assertThat(propertyResolver.getProperty("foo", String[].class)).isEqualTo(new String[] { "bar", "baz" }); + } + + @Test + void getProperty_withNonConvertibleTargetType() { + testProperties.put("foo", "bar"); + + class TestType { } + + assertThatExceptionOfType(ConverterNotFoundException.class).isThrownBy(() -> + propertyResolver.getProperty("foo", TestType.class)); + } + + @Test + void getProperty_doesNotCache_replaceExistingKeyPostConstruction() { + String key = "foo"; + String value1 = "bar"; + String value2 = "biz"; + + HashMap map = new HashMap<>(); + map.put(key, value1); // before construction + MutablePropertySources propertySources = new MutablePropertySources(); + propertySources.addFirst(new MapPropertySource("testProperties", map)); + PropertyResolver propertyResolver = new PropertySourcesPropertyResolver(propertySources); + assertThat(propertyResolver.getProperty(key)).isEqualTo(value1); + map.put(key, value2); // after construction and first resolution + assertThat(propertyResolver.getProperty(key)).isEqualTo(value2); + } + + @Test + void getProperty_doesNotCache_addNewKeyPostConstruction() { + HashMap map = new HashMap<>(); + MutablePropertySources propertySources = new MutablePropertySources(); + propertySources.addFirst(new MapPropertySource("testProperties", map)); + PropertyResolver propertyResolver = new PropertySourcesPropertyResolver(propertySources); + assertThat(propertyResolver.getProperty("foo")).isNull(); + map.put("foo", "42"); + assertThat(propertyResolver.getProperty("foo")).isEqualTo("42"); + } + + @Test + void getPropertySources_replacePropertySource() { + propertySources = new MutablePropertySources(); + propertyResolver = new PropertySourcesPropertyResolver(propertySources); + propertySources.addLast(new MockPropertySource("local").withProperty("foo", "localValue")); + propertySources.addLast(new MockPropertySource("system").withProperty("foo", "systemValue")); + + // 'local' was added first so has precedence + assertThat(propertyResolver.getProperty("foo")).isEqualTo("localValue"); + + // replace 'local' with new property source + propertySources.replace("local", new MockPropertySource("new").withProperty("foo", "newValue")); + + // 'system' now has precedence + assertThat(propertyResolver.getProperty("foo")).isEqualTo("newValue"); + + assertThat(propertySources).hasSize(2); + } + + @Test + void getRequiredProperty() { + testProperties.put("exists", "xyz"); + assertThat(propertyResolver.getRequiredProperty("exists")).isEqualTo("xyz"); + + assertThatIllegalStateException().isThrownBy(() -> + propertyResolver.getRequiredProperty("bogus")); + } + + @Test + void getRequiredProperty_withStringArrayConversion() { + testProperties.put("exists", "abc,123"); + assertThat(propertyResolver.getRequiredProperty("exists", String[].class)).isEqualTo(new String[] { "abc", "123" }); + + assertThatIllegalStateException().isThrownBy(() -> + propertyResolver.getRequiredProperty("bogus", String[].class)); + } + + @Test + void resolvePlaceholders() { + MutablePropertySources propertySources = new MutablePropertySources(); + propertySources.addFirst(new MockPropertySource().withProperty("key", "value")); + PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources); + assertThat(resolver.resolvePlaceholders("Replace this ${key}")).isEqualTo("Replace this value"); + } + + @Test + void resolvePlaceholders_withUnresolvable() { + MutablePropertySources propertySources = new MutablePropertySources(); + propertySources.addFirst(new MockPropertySource().withProperty("key", "value")); + PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources); + assertThat(resolver.resolvePlaceholders("Replace this ${key} plus ${unknown}")) + .isEqualTo("Replace this value plus ${unknown}"); + } + + @Test + void resolvePlaceholders_withDefaultValue() { + MutablePropertySources propertySources = new MutablePropertySources(); + propertySources.addFirst(new MockPropertySource().withProperty("key", "value")); + PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources); + assertThat(resolver.resolvePlaceholders("Replace this ${key} plus ${unknown:defaultValue}")) + .isEqualTo("Replace this value plus defaultValue"); + } + + @Test + void resolvePlaceholders_withNullInput() { + assertThatIllegalArgumentException().isThrownBy(() -> + new PropertySourcesPropertyResolver(new MutablePropertySources()).resolvePlaceholders(null)); + } + + @Test + void resolveRequiredPlaceholders() { + MutablePropertySources propertySources = new MutablePropertySources(); + propertySources.addFirst(new MockPropertySource().withProperty("key", "value")); + PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources); + assertThat(resolver.resolveRequiredPlaceholders("Replace this ${key}")).isEqualTo("Replace this value"); + } + + @Test + void resolveRequiredPlaceholders_withUnresolvable() { + MutablePropertySources propertySources = new MutablePropertySources(); + propertySources.addFirst(new MockPropertySource().withProperty("key", "value")); + PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources); + assertThatIllegalArgumentException().isThrownBy(() -> + resolver.resolveRequiredPlaceholders("Replace this ${key} plus ${unknown}")); + } + + @Test + void resolveRequiredPlaceholders_withDefaultValue() { + MutablePropertySources propertySources = new MutablePropertySources(); + propertySources.addFirst(new MockPropertySource().withProperty("key", "value")); + PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources); + assertThat(resolver.resolveRequiredPlaceholders("Replace this ${key} plus ${unknown:defaultValue}")) + .isEqualTo("Replace this value plus defaultValue"); + } + + @Test + void resolveRequiredPlaceholders_withNullInput() { + assertThatIllegalArgumentException().isThrownBy(() -> + new PropertySourcesPropertyResolver(new MutablePropertySources()).resolveRequiredPlaceholders(null)); + } + + @Test + void setRequiredProperties_andValidateRequiredProperties() { + // no properties have been marked as required -> validation should pass + propertyResolver.validateRequiredProperties(); + + // mark which properties are required + propertyResolver.setRequiredProperties("foo", "bar"); + + // neither foo nor bar properties are present -> validating should throw + assertThatExceptionOfType(MissingRequiredPropertiesException.class).isThrownBy( + propertyResolver::validateRequiredProperties) + .withMessage("The following properties were declared as required " + + "but could not be resolved: [foo, bar]"); + + // add foo property -> validation should fail only on missing 'bar' property + testProperties.put("foo", "fooValue"); + assertThatExceptionOfType(MissingRequiredPropertiesException.class).isThrownBy( + propertyResolver::validateRequiredProperties) + .withMessage("The following properties were declared as required " + + "but could not be resolved: [bar]"); + + // add bar property -> validation should pass, even with an empty string value + testProperties.put("bar", ""); + propertyResolver.validateRequiredProperties(); + } + + @Test + void resolveNestedPropertyPlaceholders() { + MutablePropertySources ps = new MutablePropertySources(); + ps.addFirst(new MockPropertySource() + .withProperty("p1", "v1") + .withProperty("p2", "v2") + .withProperty("p3", "${p1}:${p2}") // nested placeholders + .withProperty("p4", "${p3}") // deeply nested placeholders + .withProperty("p5", "${p1}:${p2}:${bogus}") // unresolvable placeholder + .withProperty("p6", "${p1}:${p2}:${bogus:def}") // unresolvable w/ default + .withProperty("pL", "${pR}") // cyclic reference left + .withProperty("pR", "${pL}") // cyclic reference right + ); + ConfigurablePropertyResolver pr = new PropertySourcesPropertyResolver(ps); + assertThat(pr.getProperty("p1")).isEqualTo("v1"); + assertThat(pr.getProperty("p2")).isEqualTo("v2"); + assertThat(pr.getProperty("p3")).isEqualTo("v1:v2"); + assertThat(pr.getProperty("p4")).isEqualTo("v1:v2"); + assertThatIllegalArgumentException().isThrownBy(() -> + pr.getProperty("p5")) + .withMessageContaining("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\""); + assertThat(pr.getProperty("p6")).isEqualTo("v1:v2:def"); + assertThatIllegalArgumentException().isThrownBy(() -> + pr.getProperty("pL")) + .withMessageContaining("Circular"); + } + + @Test + void ignoreUnresolvableNestedPlaceholdersIsConfigurable() { + MutablePropertySources ps = new MutablePropertySources(); + ps.addFirst(new MockPropertySource() + .withProperty("p1", "v1") + .withProperty("p2", "v2") + .withProperty("p3", "${p1}:${p2}:${bogus:def}") // unresolvable w/ default + .withProperty("p4", "${p1}:${p2}:${bogus}") // unresolvable placeholder + ); + ConfigurablePropertyResolver pr = new PropertySourcesPropertyResolver(ps); + assertThat(pr.getProperty("p1")).isEqualTo("v1"); + assertThat(pr.getProperty("p2")).isEqualTo("v2"); + assertThat(pr.getProperty("p3")).isEqualTo("v1:v2:def"); + + // placeholders nested within the value of "p4" are unresolvable and cause an + // exception by default + assertThatIllegalArgumentException().isThrownBy(() -> + pr.getProperty("p4")) + .withMessageContaining("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\""); + + // relax the treatment of unresolvable nested placeholders + pr.setIgnoreUnresolvableNestedPlaceholders(true); + // and observe they now pass through unresolved + assertThat(pr.getProperty("p4")).isEqualTo("v1:v2:${bogus}"); + + // resolve[Nested]Placeholders methods behave as usual regardless the value of + // ignoreUnresolvableNestedPlaceholders + assertThat(pr.resolvePlaceholders("${p1}:${p2}:${bogus}")).isEqualTo("v1:v2:${bogus}"); + assertThatIllegalArgumentException().isThrownBy(() -> + pr.resolveRequiredPlaceholders("${p1}:${p2}:${bogus}")) + .withMessageContaining("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\""); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/env/SimpleCommandLineArgsParserTests.java b/spring-core/src/test/java/org/springframework/core/env/SimpleCommandLineArgsParserTests.java new file mode 100644 index 0000000..4e3f186 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/env/SimpleCommandLineArgsParserTests.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Unit tests for {@link SimpleCommandLineArgsParser}. + * + * @author Chris Beams + * @author Sam Brannen + */ +class SimpleCommandLineArgsParserTests { + + private final SimpleCommandLineArgsParser parser = new SimpleCommandLineArgsParser(); + + + @Test + void withNoOptions() { + assertThat(parser.parse().getOptionValues("foo")).isNull(); + } + + @Test + void withSingleOptionAndNoValue() { + CommandLineArgs args = parser.parse("--o1"); + assertThat(args.containsOption("o1")).isTrue(); + assertThat(args.getOptionValues("o1")).isEqualTo(Collections.EMPTY_LIST); + } + + @Test + void withSingleOptionAndValue() { + CommandLineArgs args = parser.parse("--o1=v1"); + assertThat(args.containsOption("o1")).isTrue(); + assertThat(args.getOptionValues("o1")).containsExactly("v1"); + } + + @Test + void withMixOfOptionsHavingValueAndOptionsHavingNoValue() { + CommandLineArgs args = parser.parse("--o1=v1", "--o2"); + assertThat(args.containsOption("o1")).isTrue(); + assertThat(args.containsOption("o2")).isTrue(); + assertThat(args.containsOption("o3")).isFalse(); + assertThat(args.getOptionValues("o1")).containsExactly("v1"); + assertThat(args.getOptionValues("o2")).isEqualTo(Collections.EMPTY_LIST); + assertThat(args.getOptionValues("o3")).isNull(); + } + + @Test + void withEmptyOptionText() { + assertThatIllegalArgumentException().isThrownBy(() -> parser.parse("--")); + } + + @Test + void withEmptyOptionName() { + assertThatIllegalArgumentException().isThrownBy(() -> parser.parse("--=v1")); + } + + @Test + void withEmptyOptionValue() { + CommandLineArgs args = parser.parse("--o1="); + assertThat(args.containsOption("o1")).isTrue(); + assertThat(args.getOptionValues("o1")).containsExactly(""); + } + + @Test + void withEmptyOptionNameAndEmptyOptionValue() { + assertThatIllegalArgumentException().isThrownBy(() -> parser.parse("--=")); + } + + @Test + void withNonOptionArguments() { + CommandLineArgs args = parser.parse("--o1=v1", "noa1", "--o2=v2", "noa2"); + assertThat(args.getOptionValues("o1")).containsExactly("v1"); + assertThat(args.getOptionValues("o2")).containsExactly("v2"); + + List nonOptions = args.getNonOptionArgs(); + assertThat(nonOptions).containsExactly("noa1", "noa2"); + } + + @Test + void assertOptionNamesIsUnmodifiable() { + CommandLineArgs args = new SimpleCommandLineArgsParser().parse(); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + args.getOptionNames().add("bogus")); + } + + @Test + void assertNonOptionArgsIsUnmodifiable() { + CommandLineArgs args = new SimpleCommandLineArgsParser().parse(); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + args.getNonOptionArgs().add("foo")); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/env/SimpleCommandLinePropertySourceTests.java b/spring-core/src/test/java/org/springframework/core/env/SimpleCommandLinePropertySourceTests.java new file mode 100644 index 0000000..9b67142 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/env/SimpleCommandLinePropertySourceTests.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link SimpleCommandLinePropertySource}. + * + * @author Chris Beams + * @author Sam Brannen + * @since 3.1 + */ +class SimpleCommandLinePropertySourceTests { + + @Test + void withDefaultName() { + PropertySource ps = new SimpleCommandLinePropertySource(); + assertThat(ps.getName()).isEqualTo(CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME); + } + + @Test + void withCustomName() { + PropertySource ps = new SimpleCommandLinePropertySource("ps1", new String[0]); + assertThat(ps.getName()).isEqualTo("ps1"); + } + + @Test + void withNoArgs() { + PropertySource ps = new SimpleCommandLinePropertySource(); + assertThat(ps.containsProperty("foo")).isFalse(); + assertThat(ps.getProperty("foo")).isNull(); + } + + @Test + void withOptionArgsOnly() { + CommandLinePropertySource ps = new SimpleCommandLinePropertySource("--o1=v1", "--o2"); + assertThat(ps.containsProperty("o1")).isTrue(); + assertThat(ps.containsProperty("o2")).isTrue(); + assertThat(ps.containsProperty("o3")).isFalse(); + assertThat(ps.getProperty("o1")).isEqualTo("v1"); + assertThat(ps.getProperty("o2")).isEqualTo(""); + assertThat(ps.getProperty("o3")).isNull(); + } + + @Test // gh-24464 + void withOptionalArg_andArgIsEmpty() { + EnumerablePropertySource ps = new SimpleCommandLinePropertySource("--foo="); + + assertThat(ps.containsProperty("foo")).isTrue(); + assertThat(ps.getProperty("foo")).isEqualTo(""); + } + + @Test + void withDefaultNonOptionArgsNameAndNoNonOptionArgsPresent() { + EnumerablePropertySource ps = new SimpleCommandLinePropertySource("--o1=v1", "--o2"); + + assertThat(ps.containsProperty("nonOptionArgs")).isFalse(); + assertThat(ps.containsProperty("o1")).isTrue(); + assertThat(ps.containsProperty("o2")).isTrue(); + + assertThat(ps.containsProperty("nonOptionArgs")).isFalse(); + assertThat(ps.getProperty("nonOptionArgs")).isNull(); + assertThat(ps.getPropertyNames()).hasSize(2); + } + + @Test + void withDefaultNonOptionArgsNameAndNonOptionArgsPresent() { + CommandLinePropertySource ps = new SimpleCommandLinePropertySource("--o1=v1", "noa1", "--o2", "noa2"); + + assertThat(ps.containsProperty("nonOptionArgs")).isTrue(); + assertThat(ps.containsProperty("o1")).isTrue(); + assertThat(ps.containsProperty("o2")).isTrue(); + + String nonOptionArgs = ps.getProperty("nonOptionArgs"); + assertThat(nonOptionArgs).isEqualTo("noa1,noa2"); + } + + @Test + void withCustomNonOptionArgsNameAndNoNonOptionArgsPresent() { + CommandLinePropertySource ps = new SimpleCommandLinePropertySource("--o1=v1", "noa1", "--o2", "noa2"); + ps.setNonOptionArgsPropertyName("NOA"); + + assertThat(ps.containsProperty("nonOptionArgs")).isFalse(); + assertThat(ps.containsProperty("NOA")).isTrue(); + assertThat(ps.containsProperty("o1")).isTrue(); + assertThat(ps.containsProperty("o2")).isTrue(); + String nonOptionArgs = ps.getProperty("NOA"); + assertThat(nonOptionArgs).isEqualTo("noa1,noa2"); + } + + @Test + void covertNonOptionArgsToStringArrayAndList() { + CommandLinePropertySource ps = new SimpleCommandLinePropertySource("--o1=v1", "noa1", "--o2", "noa2"); + StandardEnvironment env = new StandardEnvironment(); + env.getPropertySources().addFirst(ps); + + String nonOptionArgs = env.getProperty("nonOptionArgs"); + assertThat(nonOptionArgs).isEqualTo("noa1,noa2"); + + String[] nonOptionArgsArray = env.getProperty("nonOptionArgs", String[].class); + assertThat(nonOptionArgsArray[0]).isEqualTo("noa1"); + assertThat(nonOptionArgsArray[1]).isEqualTo("noa2"); + + @SuppressWarnings("unchecked") + List nonOptionArgsList = env.getProperty("nonOptionArgs", List.class); + assertThat(nonOptionArgsList.get(0)).isEqualTo("noa1"); + assertThat(nonOptionArgsList.get(1)).isEqualTo("noa2"); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/env/StandardEnvironmentTests.java b/spring-core/src/test/java/org/springframework/core/env/StandardEnvironmentTests.java new file mode 100644 index 0000000..acd6d84 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/env/StandardEnvironmentTests.java @@ -0,0 +1,502 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import java.security.AccessControlException; +import java.security.Permission; +import java.util.Arrays; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.SpringProperties; +import org.springframework.core.testfixture.env.EnvironmentTestUtils; +import org.springframework.core.testfixture.env.MockPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.springframework.core.env.AbstractEnvironment.ACTIVE_PROFILES_PROPERTY_NAME; +import static org.springframework.core.env.AbstractEnvironment.DEFAULT_PROFILES_PROPERTY_NAME; +import static org.springframework.core.env.AbstractEnvironment.RESERVED_DEFAULT_PROFILE_NAME; + +/** + * Unit tests for {@link StandardEnvironment}. + * + * @author Chris Beams + * @author Juergen Hoeller + * @author Sam Brannen + */ +@SuppressWarnings("deprecation") +public class StandardEnvironmentTests { + + private static final String ALLOWED_PROPERTY_NAME = "theanswer"; + private static final String ALLOWED_PROPERTY_VALUE = "42"; + + private static final String DISALLOWED_PROPERTY_NAME = "verboten"; + private static final String DISALLOWED_PROPERTY_VALUE = "secret"; + + private static final String STRING_PROPERTY_NAME = "stringPropName"; + private static final String STRING_PROPERTY_VALUE = "stringPropValue"; + private static final Object NON_STRING_PROPERTY_NAME = new Object(); + private static final Object NON_STRING_PROPERTY_VALUE = new Object(); + + private final ConfigurableEnvironment environment = new StandardEnvironment(); + + + @Test + void merge() { + ConfigurableEnvironment child = new StandardEnvironment(); + child.setActiveProfiles("c1", "c2"); + child.getPropertySources().addLast( + new MockPropertySource("childMock") + .withProperty("childKey", "childVal") + .withProperty("bothKey", "childBothVal")); + + ConfigurableEnvironment parent = new StandardEnvironment(); + parent.setActiveProfiles("p1", "p2"); + parent.getPropertySources().addLast( + new MockPropertySource("parentMock") + .withProperty("parentKey", "parentVal") + .withProperty("bothKey", "parentBothVal")); + + assertThat(child.getProperty("childKey")).isEqualTo("childVal"); + assertThat(child.getProperty("parentKey")).isNull(); + assertThat(child.getProperty("bothKey")).isEqualTo("childBothVal"); + + assertThat(parent.getProperty("childKey")).isNull(); + assertThat(parent.getProperty("parentKey")).isEqualTo("parentVal"); + assertThat(parent.getProperty("bothKey")).isEqualTo("parentBothVal"); + + assertThat(child.getActiveProfiles()).isEqualTo(new String[]{"c1","c2"}); + assertThat(parent.getActiveProfiles()).isEqualTo(new String[]{"p1","p2"}); + + child.merge(parent); + + assertThat(child.getProperty("childKey")).isEqualTo("childVal"); + assertThat(child.getProperty("parentKey")).isEqualTo("parentVal"); + assertThat(child.getProperty("bothKey")).isEqualTo("childBothVal"); + + assertThat(parent.getProperty("childKey")).isNull(); + assertThat(parent.getProperty("parentKey")).isEqualTo("parentVal"); + assertThat(parent.getProperty("bothKey")).isEqualTo("parentBothVal"); + + assertThat(child.getActiveProfiles()).isEqualTo(new String[]{"c1","c2","p1","p2"}); + assertThat(parent.getActiveProfiles()).isEqualTo(new String[]{"p1","p2"}); + } + + @Test + void propertySourceOrder() { + ConfigurableEnvironment env = new StandardEnvironment(); + MutablePropertySources sources = env.getPropertySources(); + assertThat(sources.precedenceOf(PropertySource.named(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME))).isEqualTo(0); + assertThat(sources.precedenceOf(PropertySource.named(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME))).isEqualTo(1); + assertThat(sources).hasSize(2); + } + + @Test + void propertySourceTypes() { + ConfigurableEnvironment env = new StandardEnvironment(); + MutablePropertySources sources = env.getPropertySources(); + assertThat(sources.get(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME)).isInstanceOf(SystemEnvironmentPropertySource.class); + } + + @Test + void activeProfilesIsEmptyByDefault() { + assertThat(environment.getActiveProfiles().length).isEqualTo(0); + } + + @Test + void defaultProfilesContainsDefaultProfileByDefault() { + assertThat(environment.getDefaultProfiles().length).isEqualTo(1); + assertThat(environment.getDefaultProfiles()[0]).isEqualTo("default"); + } + + @Test + void setActiveProfiles() { + environment.setActiveProfiles("local", "embedded"); + String[] activeProfiles = environment.getActiveProfiles(); + assertThat(activeProfiles).contains("local", "embedded"); + assertThat(activeProfiles.length).isEqualTo(2); + } + + @Test + void setActiveProfiles_withNullProfileArray() { + assertThatIllegalArgumentException().isThrownBy(() -> environment.setActiveProfiles((String[]) null)); + } + + @Test + void setActiveProfiles_withNullProfile() { + assertThatIllegalArgumentException().isThrownBy(() -> environment.setActiveProfiles((String) null)); + } + + @Test + void setActiveProfiles_withEmptyProfile() { + assertThatIllegalArgumentException().isThrownBy(() -> environment.setActiveProfiles("")); + } + + @Test + void setActiveProfiles_withNotOperator() { + assertThatIllegalArgumentException().isThrownBy(() -> environment.setActiveProfiles("p1", "!p2")); + } + + @Test + void setDefaultProfiles_withNullProfileArray() { + assertThatIllegalArgumentException().isThrownBy(() -> environment.setDefaultProfiles((String[]) null)); + } + + @Test + void setDefaultProfiles_withNullProfile() { + assertThatIllegalArgumentException().isThrownBy(() -> environment.setDefaultProfiles((String) null)); + } + + @Test + void setDefaultProfiles_withEmptyProfile() { + assertThatIllegalArgumentException().isThrownBy(() -> environment.setDefaultProfiles("")); + } + + @Test + void setDefaultProfiles_withNotOperator() { + assertThatIllegalArgumentException().isThrownBy(() -> environment.setDefaultProfiles("d1", "!d2")); + } + + @Test + void addActiveProfile() { + assertThat(environment.getActiveProfiles().length).isEqualTo(0); + environment.setActiveProfiles("local", "embedded"); + assertThat(environment.getActiveProfiles()).contains("local", "embedded"); + assertThat(environment.getActiveProfiles().length).isEqualTo(2); + environment.addActiveProfile("p1"); + assertThat(environment.getActiveProfiles()).contains("p1"); + assertThat(environment.getActiveProfiles().length).isEqualTo(3); + environment.addActiveProfile("p2"); + environment.addActiveProfile("p3"); + assertThat(environment.getActiveProfiles()).contains("p2", "p3"); + assertThat(environment.getActiveProfiles().length).isEqualTo(5); + } + + @Test + void addActiveProfile_whenActiveProfilesPropertyIsAlreadySet() { + ConfigurableEnvironment env = new StandardEnvironment(); + assertThat(env.getProperty(ACTIVE_PROFILES_PROPERTY_NAME)).isNull(); + env.getPropertySources().addFirst(new MockPropertySource().withProperty(ACTIVE_PROFILES_PROPERTY_NAME, "p1")); + assertThat(env.getProperty(ACTIVE_PROFILES_PROPERTY_NAME)).isEqualTo("p1"); + env.addActiveProfile("p2"); + assertThat(env.getActiveProfiles()).contains("p1", "p2"); + } + + @Test + void reservedDefaultProfile() { + assertThat(environment.getDefaultProfiles()).isEqualTo(new String[]{RESERVED_DEFAULT_PROFILE_NAME}); + System.setProperty(DEFAULT_PROFILES_PROPERTY_NAME, "d0"); + assertThat(environment.getDefaultProfiles()).isEqualTo(new String[]{"d0"}); + environment.setDefaultProfiles("d1", "d2"); + assertThat(environment.getDefaultProfiles()).isEqualTo(new String[]{"d1","d2"}); + System.clearProperty(DEFAULT_PROFILES_PROPERTY_NAME); + } + + @Test + void defaultProfileWithCircularPlaceholder() { + try { + System.setProperty(DEFAULT_PROFILES_PROPERTY_NAME, "${spring.profiles.default}"); + assertThatIllegalArgumentException().isThrownBy(() -> environment.getDefaultProfiles()); + } + finally { + System.clearProperty(DEFAULT_PROFILES_PROPERTY_NAME); + } + } + + @Test + void getActiveProfiles_systemPropertiesEmpty() { + assertThat(environment.getActiveProfiles().length).isEqualTo(0); + System.setProperty(ACTIVE_PROFILES_PROPERTY_NAME, ""); + assertThat(environment.getActiveProfiles().length).isEqualTo(0); + System.clearProperty(ACTIVE_PROFILES_PROPERTY_NAME); + } + + @Test + void getActiveProfiles_fromSystemProperties() { + System.setProperty(ACTIVE_PROFILES_PROPERTY_NAME, "foo"); + assertThat(Arrays.asList(environment.getActiveProfiles())).contains("foo"); + System.clearProperty(ACTIVE_PROFILES_PROPERTY_NAME); + } + + @Test + void getActiveProfiles_fromSystemProperties_withMultipleProfiles() { + System.setProperty(ACTIVE_PROFILES_PROPERTY_NAME, "foo,bar"); + assertThat(environment.getActiveProfiles()).contains("foo", "bar"); + System.clearProperty(ACTIVE_PROFILES_PROPERTY_NAME); + } + + @Test + void getActiveProfiles_fromSystemProperties_withMulitpleProfiles_withWhitespace() { + System.setProperty(ACTIVE_PROFILES_PROPERTY_NAME, " bar , baz "); // notice whitespace + assertThat(environment.getActiveProfiles()).contains("bar", "baz"); + System.clearProperty(ACTIVE_PROFILES_PROPERTY_NAME); + } + + @Test + void getDefaultProfiles() { + assertThat(environment.getDefaultProfiles()).isEqualTo(new String[] {RESERVED_DEFAULT_PROFILE_NAME}); + environment.getPropertySources().addFirst(new MockPropertySource().withProperty(DEFAULT_PROFILES_PROPERTY_NAME, "pd1")); + assertThat(environment.getDefaultProfiles().length).isEqualTo(1); + assertThat(Arrays.asList(environment.getDefaultProfiles())).contains("pd1"); + } + + @Test + void setDefaultProfiles() { + environment.setDefaultProfiles(); + assertThat(environment.getDefaultProfiles().length).isEqualTo(0); + environment.setDefaultProfiles("pd1"); + assertThat(Arrays.asList(environment.getDefaultProfiles())).contains("pd1"); + environment.setDefaultProfiles("pd2", "pd3"); + assertThat(environment.getDefaultProfiles()).doesNotContain("pd1"); + assertThat(environment.getDefaultProfiles()).contains("pd2", "pd3"); + } + + @Test + void acceptsProfiles_withEmptyArgumentList() { + assertThatIllegalArgumentException().isThrownBy( + environment::acceptsProfiles); + } + + @Test + void acceptsProfiles_withNullArgumentList() { + assertThatIllegalArgumentException().isThrownBy(() -> environment.acceptsProfiles((String[]) null)); + } + + @Test + void acceptsProfiles_withNullArgument() { + assertThatIllegalArgumentException().isThrownBy(() -> environment.acceptsProfiles((String) null)); + } + + @Test + void acceptsProfiles_withEmptyArgument() { + assertThatIllegalArgumentException().isThrownBy(() -> environment.acceptsProfiles("")); + } + + @Test + void acceptsProfiles_activeProfileSetProgrammatically() { + assertThat(environment.acceptsProfiles("p1", "p2")).isFalse(); + environment.setActiveProfiles("p1"); + assertThat(environment.acceptsProfiles("p1", "p2")).isTrue(); + environment.setActiveProfiles("p2"); + assertThat(environment.acceptsProfiles("p1", "p2")).isTrue(); + environment.setActiveProfiles("p1", "p2"); + assertThat(environment.acceptsProfiles("p1", "p2")).isTrue(); + } + + @Test + void acceptsProfiles_activeProfileSetViaProperty() { + assertThat(environment.acceptsProfiles("p1")).isFalse(); + environment.getPropertySources().addFirst(new MockPropertySource().withProperty(ACTIVE_PROFILES_PROPERTY_NAME, "p1")); + assertThat(environment.acceptsProfiles("p1")).isTrue(); + } + + @Test + void acceptsProfiles_defaultProfile() { + assertThat(environment.acceptsProfiles("pd")).isFalse(); + environment.setDefaultProfiles("pd"); + assertThat(environment.acceptsProfiles("pd")).isTrue(); + environment.setActiveProfiles("p1"); + assertThat(environment.acceptsProfiles("pd")).isFalse(); + assertThat(environment.acceptsProfiles("p1")).isTrue(); + } + + @Test + void acceptsProfiles_withNotOperator() { + assertThat(environment.acceptsProfiles("p1")).isFalse(); + assertThat(environment.acceptsProfiles("!p1")).isTrue(); + environment.addActiveProfile("p1"); + assertThat(environment.acceptsProfiles("p1")).isTrue(); + assertThat(environment.acceptsProfiles("!p1")).isFalse(); + } + + @Test + void acceptsProfiles_withInvalidNotOperator() { + assertThatIllegalArgumentException().isThrownBy(() -> environment.acceptsProfiles("p1", "!")); + } + + @Test + void acceptsProfiles_withProfileExpression() { + assertThat(environment.acceptsProfiles(Profiles.of("p1 & p2"))).isFalse(); + environment.addActiveProfile("p1"); + assertThat(environment.acceptsProfiles(Profiles.of("p1 & p2"))).isFalse(); + environment.addActiveProfile("p2"); + assertThat(environment.acceptsProfiles(Profiles.of("p1 & p2"))).isTrue(); + } + + @Test + void environmentSubclass_withCustomProfileValidation() { + ConfigurableEnvironment env = new AbstractEnvironment() { + @Override + protected void validateProfile(String profile) { + super.validateProfile(profile); + if (profile.contains("-")) { + throw new IllegalArgumentException( + "Invalid profile [" + profile + "]: must not contain dash character"); + } + } + }; + + env.addActiveProfile("validProfile"); // succeeds + + assertThatIllegalArgumentException().isThrownBy(() -> + env.addActiveProfile("invalid-profile")) + .withMessage("Invalid profile [invalid-profile]: must not contain dash character"); + } + + @Test + void suppressGetenvAccessThroughSystemProperty() { + System.setProperty("spring.getenv.ignore", "true"); + assertThat(environment.getSystemEnvironment().isEmpty()).isTrue(); + System.clearProperty("spring.getenv.ignore"); + } + + @Test + void suppressGetenvAccessThroughSpringProperty() { + SpringProperties.setProperty("spring.getenv.ignore", "true"); + assertThat(environment.getSystemEnvironment().isEmpty()).isTrue(); + SpringProperties.setProperty("spring.getenv.ignore", null); + } + + @Test + void suppressGetenvAccessThroughSpringFlag() { + SpringProperties.setFlag("spring.getenv.ignore"); + assertThat(environment.getSystemEnvironment().isEmpty()).isTrue(); + SpringProperties.setProperty("spring.getenv.ignore", null); + } + + @Test + void getSystemProperties_withAndWithoutSecurityManager() { + System.setProperty(ALLOWED_PROPERTY_NAME, ALLOWED_PROPERTY_VALUE); + System.setProperty(DISALLOWED_PROPERTY_NAME, DISALLOWED_PROPERTY_VALUE); + System.getProperties().put(STRING_PROPERTY_NAME, NON_STRING_PROPERTY_VALUE); + System.getProperties().put(NON_STRING_PROPERTY_NAME, STRING_PROPERTY_VALUE); + + { + Map systemProperties = environment.getSystemProperties(); + assertThat(systemProperties).isNotNull(); + assertThat(System.getProperties()).isSameAs(systemProperties); + assertThat(systemProperties.get(ALLOWED_PROPERTY_NAME)).isEqualTo(ALLOWED_PROPERTY_VALUE); + assertThat(systemProperties.get(DISALLOWED_PROPERTY_NAME)).isEqualTo(DISALLOWED_PROPERTY_VALUE); + + // non-string keys and values work fine... until the security manager is introduced below + assertThat(systemProperties.get(STRING_PROPERTY_NAME)).isEqualTo(NON_STRING_PROPERTY_VALUE); + assertThat(systemProperties.get(NON_STRING_PROPERTY_NAME)).isEqualTo(STRING_PROPERTY_VALUE); + } + + SecurityManager oldSecurityManager = System.getSecurityManager(); + SecurityManager securityManager = new SecurityManager() { + @Override + public void checkPropertiesAccess() { + // see https://download.oracle.com/javase/1.5.0/docs/api/java/lang/System.html#getProperties() + throw new AccessControlException("Accessing the system properties is disallowed"); + } + @Override + public void checkPropertyAccess(String key) { + // see https://download.oracle.com/javase/1.5.0/docs/api/java/lang/System.html#getProperty(java.lang.String) + if (DISALLOWED_PROPERTY_NAME.equals(key)) { + throw new AccessControlException( + String.format("Accessing the system property [%s] is disallowed", DISALLOWED_PROPERTY_NAME)); + } + } + @Override + public void checkPermission(Permission perm) { + // allow everything else + } + }; + + try { + System.setSecurityManager(securityManager); + + { + Map systemProperties = environment.getSystemProperties(); + assertThat(systemProperties).isNotNull(); + assertThat(systemProperties).isInstanceOf(ReadOnlySystemAttributesMap.class); + assertThat((String)systemProperties.get(ALLOWED_PROPERTY_NAME)).isEqualTo(ALLOWED_PROPERTY_VALUE); + assertThat(systemProperties.get(DISALLOWED_PROPERTY_NAME)).isNull(); + + // nothing we can do here in terms of warning the user that there was + // actually a (non-string) value available. By this point, we only + // have access to calling System.getProperty(), which itself returns null + // if the value is non-string. So we're stuck with returning a potentially + // misleading null. + assertThat(systemProperties.get(STRING_PROPERTY_NAME)).isNull(); + + // in the case of a non-string *key*, however, we can do better. Alert + // the user that under these very special conditions (non-object key + + // SecurityManager that disallows access to system properties), they + // cannot do what they're attempting. + assertThatIllegalArgumentException().as("searching with non-string key against ReadOnlySystemAttributesMap").isThrownBy(() -> + systemProperties.get(NON_STRING_PROPERTY_NAME)); + } + } + finally { + System.setSecurityManager(oldSecurityManager); + System.clearProperty(ALLOWED_PROPERTY_NAME); + System.clearProperty(DISALLOWED_PROPERTY_NAME); + System.getProperties().remove(STRING_PROPERTY_NAME); + System.getProperties().remove(NON_STRING_PROPERTY_NAME); + } + } + + @Test + void getSystemEnvironment_withAndWithoutSecurityManager() { + EnvironmentTestUtils.getModifiableSystemEnvironment().put(ALLOWED_PROPERTY_NAME, ALLOWED_PROPERTY_VALUE); + EnvironmentTestUtils.getModifiableSystemEnvironment().put(DISALLOWED_PROPERTY_NAME, DISALLOWED_PROPERTY_VALUE); + + { + Map systemEnvironment = environment.getSystemEnvironment(); + assertThat(systemEnvironment).isNotNull(); + assertThat(System.getenv()).isSameAs(systemEnvironment); + } + + SecurityManager oldSecurityManager = System.getSecurityManager(); + SecurityManager securityManager = new SecurityManager() { + @Override + public void checkPermission(Permission perm) { + //see https://download.oracle.com/javase/1.5.0/docs/api/java/lang/System.html#getenv() + if ("getenv.*".equals(perm.getName())) { + throw new AccessControlException("Accessing the system environment is disallowed"); + } + //see https://download.oracle.com/javase/1.5.0/docs/api/java/lang/System.html#getenv(java.lang.String) + if (("getenv."+DISALLOWED_PROPERTY_NAME).equals(perm.getName())) { + throw new AccessControlException( + String.format("Accessing the system environment variable [%s] is disallowed", DISALLOWED_PROPERTY_NAME)); + } + } + }; + + try { + System.setSecurityManager(securityManager); + { + Map systemEnvironment = environment.getSystemEnvironment(); + assertThat(systemEnvironment).isNotNull(); + assertThat(systemEnvironment).isInstanceOf(ReadOnlySystemAttributesMap.class); + assertThat(systemEnvironment.get(ALLOWED_PROPERTY_NAME)).isEqualTo(ALLOWED_PROPERTY_VALUE); + assertThat(systemEnvironment.get(DISALLOWED_PROPERTY_NAME)).isNull(); + } + } + finally { + System.setSecurityManager(oldSecurityManager); + } + + EnvironmentTestUtils.getModifiableSystemEnvironment().remove(ALLOWED_PROPERTY_NAME); + EnvironmentTestUtils.getModifiableSystemEnvironment().remove(DISALLOWED_PROPERTY_NAME); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/env/SystemEnvironmentPropertySourceTests.java b/spring-core/src/test/java/org/springframework/core/env/SystemEnvironmentPropertySourceTests.java new file mode 100644 index 0000000..daca3a7 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/env/SystemEnvironmentPropertySourceTests.java @@ -0,0 +1,177 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Unit tests for {@link SystemEnvironmentPropertySource}. + * + * @author Chris Beams + * @author Juergen Hoeller + * @since 3.1 + */ +class SystemEnvironmentPropertySourceTests { + + private Map envMap; + + private PropertySource ps; + + + @BeforeEach + void setUp() { + envMap = new HashMap<>(); + ps = new SystemEnvironmentPropertySource("sysEnv", envMap); + } + + + @Test + void none() { + assertThat(ps.containsProperty("a.key")).isEqualTo(false); + assertThat(ps.getProperty("a.key")).isNull(); + } + + @Test + void normalWithoutPeriod() { + envMap.put("akey", "avalue"); + + assertThat(ps.containsProperty("akey")).isEqualTo(true); + assertThat(ps.getProperty("akey")).isEqualTo("avalue"); + } + + @Test + void normalWithPeriod() { + envMap.put("a.key", "a.value"); + + assertThat(ps.containsProperty("a.key")).isEqualTo(true); + assertThat(ps.getProperty("a.key")).isEqualTo("a.value"); + } + + @Test + void withUnderscore() { + envMap.put("a_key", "a_value"); + + assertThat(ps.containsProperty("a_key")).isEqualTo(true); + assertThat(ps.containsProperty("a.key")).isEqualTo(true); + + assertThat(ps.getProperty("a_key")).isEqualTo("a_value"); + assertThat( ps.getProperty("a.key")).isEqualTo("a_value"); + } + + @Test + void withBothPeriodAndUnderscore() { + envMap.put("a_key", "a_value"); + envMap.put("a.key", "a.value"); + + assertThat(ps.getProperty("a_key")).isEqualTo("a_value"); + assertThat( ps.getProperty("a.key")).isEqualTo("a.value"); + } + + @Test + void withUppercase() { + envMap.put("A_KEY", "a_value"); + envMap.put("A_LONG_KEY", "a_long_value"); + envMap.put("A_DOT.KEY", "a_dot_value"); + envMap.put("A_HYPHEN-KEY", "a_hyphen_value"); + + assertThat(ps.containsProperty("A_KEY")).isEqualTo(true); + assertThat(ps.containsProperty("A.KEY")).isEqualTo(true); + assertThat(ps.containsProperty("A-KEY")).isEqualTo(true); + assertThat(ps.containsProperty("a_key")).isEqualTo(true); + assertThat(ps.containsProperty("a.key")).isEqualTo(true); + assertThat(ps.containsProperty("a-key")).isEqualTo(true); + assertThat(ps.containsProperty("A_LONG_KEY")).isEqualTo(true); + assertThat(ps.containsProperty("A.LONG.KEY")).isEqualTo(true); + assertThat(ps.containsProperty("A-LONG-KEY")).isEqualTo(true); + assertThat(ps.containsProperty("A.LONG-KEY")).isEqualTo(true); + assertThat(ps.containsProperty("A-LONG.KEY")).isEqualTo(true); + assertThat(ps.containsProperty("A_long_KEY")).isEqualTo(true); + assertThat(ps.containsProperty("A.long.KEY")).isEqualTo(true); + assertThat(ps.containsProperty("A-long-KEY")).isEqualTo(true); + assertThat(ps.containsProperty("A.long-KEY")).isEqualTo(true); + assertThat(ps.containsProperty("A-long.KEY")).isEqualTo(true); + assertThat(ps.containsProperty("A_DOT.KEY")).isEqualTo(true); + assertThat(ps.containsProperty("A-DOT.KEY")).isEqualTo(true); + assertThat(ps.containsProperty("A_dot.KEY")).isEqualTo(true); + assertThat(ps.containsProperty("A-dot.KEY")).isEqualTo(true); + assertThat(ps.containsProperty("A_HYPHEN-KEY")).isEqualTo(true); + assertThat(ps.containsProperty("A.HYPHEN-KEY")).isEqualTo(true); + assertThat(ps.containsProperty("A_hyphen-KEY")).isEqualTo(true); + assertThat(ps.containsProperty("A.hyphen-KEY")).isEqualTo(true); + + assertThat(ps.getProperty("A_KEY")).isEqualTo("a_value"); + assertThat(ps.getProperty("A.KEY")).isEqualTo("a_value"); + assertThat(ps.getProperty("A-KEY")).isEqualTo("a_value"); + assertThat(ps.getProperty("a_key")).isEqualTo("a_value"); + assertThat(ps.getProperty("a.key")).isEqualTo("a_value"); + assertThat(ps.getProperty("a-key")).isEqualTo("a_value"); + assertThat(ps.getProperty("A_LONG_KEY")).isEqualTo("a_long_value"); + assertThat(ps.getProperty("A.LONG.KEY")).isEqualTo("a_long_value"); + assertThat(ps.getProperty("A-LONG-KEY")).isEqualTo("a_long_value"); + assertThat(ps.getProperty("A.LONG-KEY")).isEqualTo("a_long_value"); + assertThat(ps.getProperty("A-LONG.KEY")).isEqualTo("a_long_value"); + assertThat(ps.getProperty("A_long_KEY")).isEqualTo("a_long_value"); + assertThat(ps.getProperty("A.long.KEY")).isEqualTo("a_long_value"); + assertThat(ps.getProperty("A-long-KEY")).isEqualTo("a_long_value"); + assertThat(ps.getProperty("A.long-KEY")).isEqualTo("a_long_value"); + assertThat(ps.getProperty("A-long.KEY")).isEqualTo("a_long_value"); + assertThat(ps.getProperty("A_DOT.KEY")).isEqualTo("a_dot_value"); + assertThat(ps.getProperty("A-DOT.KEY")).isEqualTo("a_dot_value"); + assertThat(ps.getProperty("A_dot.KEY")).isEqualTo("a_dot_value"); + assertThat(ps.getProperty("A-dot.KEY")).isEqualTo("a_dot_value"); + assertThat(ps.getProperty("A_HYPHEN-KEY")).isEqualTo("a_hyphen_value"); + assertThat(ps.getProperty("A.HYPHEN-KEY")).isEqualTo("a_hyphen_value"); + assertThat(ps.getProperty("A_hyphen-KEY")).isEqualTo("a_hyphen_value"); + assertThat(ps.getProperty("A.hyphen-KEY")).isEqualTo("a_hyphen_value"); + } + + @Test + @SuppressWarnings("serial") + void withSecurityConstraints() throws Exception { + envMap = new HashMap() { + @Override + public boolean containsKey(Object key) { + throw new UnsupportedOperationException(); + } + @Override + public Set keySet() { + return new HashSet<>(super.keySet()); + } + }; + envMap.put("A_KEY", "a_value"); + + ps = new SystemEnvironmentPropertySource("sysEnv", envMap) { + @Override + protected boolean isSecurityManagerPresent() { + return true; + } + }; + + assertThat(ps.containsProperty("A_KEY")).isEqualTo(true); + assertThat(ps.getProperty("A_KEY")).isEqualTo("a_value"); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/io/ClassPathResourceTests.java b/spring-core/src/test/java/org/springframework/core/io/ClassPathResourceTests.java new file mode 100644 index 0000000..9720f08 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/io/ClassPathResourceTests.java @@ -0,0 +1,138 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io; + +import java.io.FileNotFoundException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Unit tests that serve as regression tests for the bugs described in SPR-6888 + * and SPR-9413. + * + * @author Chris Beams + * @author Sam Brannen + */ +class ClassPathResourceTests { + + private static final String PACKAGE_PATH = "org/springframework/core/io"; + private static final String NONEXISTENT_RESOURCE_NAME = "nonexistent.xml"; + private static final String FQ_RESOURCE_PATH = PACKAGE_PATH + '/' + NONEXISTENT_RESOURCE_NAME; + + /** + * Absolute path version of {@link #FQ_RESOURCE_PATH}. + */ + private static final String FQ_RESOURCE_PATH_WITH_LEADING_SLASH = '/' + FQ_RESOURCE_PATH; + + private static final Pattern DESCRIPTION_PATTERN = Pattern.compile("^class path resource \\[(.+?)]$"); + + + @Test + void stringConstructorRaisesExceptionWithFullyQualifiedPath() { + assertExceptionContainsFullyQualifiedPath(new ClassPathResource(FQ_RESOURCE_PATH)); + } + + @Test + void classLiteralConstructorRaisesExceptionWithFullyQualifiedPath() { + assertExceptionContainsFullyQualifiedPath(new ClassPathResource(NONEXISTENT_RESOURCE_NAME, getClass())); + } + + @Test + void classLoaderConstructorRaisesExceptionWithFullyQualifiedPath() { + assertExceptionContainsFullyQualifiedPath(new ClassPathResource(FQ_RESOURCE_PATH, getClass().getClassLoader())); + } + + @Test + void getDescriptionWithStringConstructor() { + assertDescriptionContainsExpectedPath(new ClassPathResource(FQ_RESOURCE_PATH), FQ_RESOURCE_PATH); + } + + @Test + void getDescriptionWithStringConstructorAndLeadingSlash() { + assertDescriptionContainsExpectedPath(new ClassPathResource(FQ_RESOURCE_PATH_WITH_LEADING_SLASH), + FQ_RESOURCE_PATH); + } + + @Test + void getDescriptionWithClassLiteralConstructor() { + assertDescriptionContainsExpectedPath(new ClassPathResource(NONEXISTENT_RESOURCE_NAME, getClass()), + FQ_RESOURCE_PATH); + } + + @Test + void getDescriptionWithClassLiteralConstructorAndLeadingSlash() { + assertDescriptionContainsExpectedPath( + new ClassPathResource(FQ_RESOURCE_PATH_WITH_LEADING_SLASH, getClass()), FQ_RESOURCE_PATH); + } + + @Test + void getDescriptionWithClassLoaderConstructor() { + assertDescriptionContainsExpectedPath( + new ClassPathResource(FQ_RESOURCE_PATH, getClass().getClassLoader()), FQ_RESOURCE_PATH); + } + + @Test + void getDescriptionWithClassLoaderConstructorAndLeadingSlash() { + assertDescriptionContainsExpectedPath( + new ClassPathResource(FQ_RESOURCE_PATH_WITH_LEADING_SLASH, getClass().getClassLoader()), FQ_RESOURCE_PATH); + } + + @Test + void dropLeadingSlashForClassLoaderAccess() { + assertThat(new ClassPathResource("/test.html").getPath()).isEqualTo("test.html"); + assertThat(((ClassPathResource) new ClassPathResource("").createRelative("/test.html")).getPath()).isEqualTo("test.html"); + } + + @Test + void preserveLeadingSlashForClassRelativeAccess() { + assertThat(new ClassPathResource("/test.html", getClass()).getPath()).isEqualTo("/test.html"); + assertThat(((ClassPathResource) new ClassPathResource("", getClass()).createRelative("/test.html")).getPath()).isEqualTo("/test.html"); + } + + @Test + void directoryNotReadable() { + Resource fileDir = new ClassPathResource("org/springframework/core"); + assertThat(fileDir.exists()).isTrue(); + assertThat(fileDir.isReadable()).isFalse(); + + Resource jarDir = new ClassPathResource("reactor/core"); + assertThat(jarDir.exists()).isTrue(); + assertThat(jarDir.isReadable()).isFalse(); + } + + + private void assertDescriptionContainsExpectedPath(ClassPathResource resource, String expectedPath) { + Matcher matcher = DESCRIPTION_PATTERN.matcher(resource.getDescription()); + assertThat(matcher.matches()).isTrue(); + assertThat(matcher.groupCount()).isEqualTo(1); + String match = matcher.group(1); + + assertThat(match).isEqualTo(expectedPath); + } + + private void assertExceptionContainsFullyQualifiedPath(ClassPathResource resource) { + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy( + resource::getInputStream) + .withMessageContaining(FQ_RESOURCE_PATH); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/io/PathResourceTests.java b/spring-core/src/test/java/org/springframework/core/io/PathResourceTests.java new file mode 100644 index 0000000..59761c1 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/io/PathResourceTests.java @@ -0,0 +1,332 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.AccessDeniedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for the {@link PathResource} class. + * + * @author Philippe Marschall + * @author Phillip Webb + * @author Nicholas Williams + * @author Stephane Nicoll + * @author Juergen Hoeller + * @author Arjen Poutsma + */ +@Deprecated +class PathResourceTests { + + private static final String TEST_DIR = + platformPath("src/test/resources/org/springframework/core/io"); + + private static final String TEST_FILE = + platformPath("src/test/resources/org/springframework/core/io/example.properties"); + + private static final String NON_EXISTING_FILE = + platformPath("src/test/resources/org/springframework/core/io/doesnotexist.properties"); + + + private static String platformPath(String string) { + return string.replace('/', File.separatorChar); + } + + + @Test + void nullPath() { + assertThatIllegalArgumentException().isThrownBy(() -> + new PathResource((Path) null)) + .withMessageContaining("Path must not be null"); + } + + @Test + void nullPathString() { + assertThatIllegalArgumentException().isThrownBy(() -> + new PathResource((String) null)) + .withMessageContaining("Path must not be null"); + } + + @Test + void nullUri() { + assertThatIllegalArgumentException().isThrownBy(() -> + new PathResource((URI) null)) + .withMessageContaining("URI must not be null"); + } + + @Test + void createFromPath() { + Path path = Paths.get(TEST_FILE); + PathResource resource = new PathResource(path); + assertThat(resource.getPath()).isEqualTo(TEST_FILE); + } + + @Test + void createFromString() { + PathResource resource = new PathResource(TEST_FILE); + assertThat(resource.getPath()).isEqualTo(TEST_FILE); + } + + @Test + void createFromUri() { + File file = new File(TEST_FILE); + PathResource resource = new PathResource(file.toURI()); + assertThat(resource.getPath()).isEqualTo(file.getAbsoluteFile().toString()); + } + + @Test + void getPathForFile() { + PathResource resource = new PathResource(TEST_FILE); + assertThat(resource.getPath()).isEqualTo(TEST_FILE); + } + + @Test + void getPathForDir() { + PathResource resource = new PathResource(TEST_DIR); + assertThat(resource.getPath()).isEqualTo(TEST_DIR); + } + + @Test + void fileExists() { + PathResource resource = new PathResource(TEST_FILE); + assertThat(resource.exists()).isEqualTo(true); + } + + @Test + void dirExists() { + PathResource resource = new PathResource(TEST_DIR); + assertThat(resource.exists()).isEqualTo(true); + } + + @Test + void fileDoesNotExist() { + PathResource resource = new PathResource(NON_EXISTING_FILE); + assertThat(resource.exists()).isEqualTo(false); + } + + @Test + void fileIsReadable() { + PathResource resource = new PathResource(TEST_FILE); + assertThat(resource.isReadable()).isEqualTo(true); + } + + @Test + void doesNotExistIsNotReadable() { + PathResource resource = new PathResource(NON_EXISTING_FILE); + assertThat(resource.isReadable()).isEqualTo(false); + } + + @Test + void directoryIsNotReadable() { + PathResource resource = new PathResource(TEST_DIR); + assertThat(resource.isReadable()).isEqualTo(false); + } + + @Test + void getInputStream() throws IOException { + PathResource resource = new PathResource(TEST_FILE); + byte[] bytes = FileCopyUtils.copyToByteArray(resource.getInputStream()); + assertThat(bytes.length).isGreaterThan(0); + } + + @Test + void getInputStreamForDir() throws IOException { + PathResource resource = new PathResource(TEST_DIR); + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy( + resource::getInputStream); + } + + @Test + void getInputStreamDoesNotExist() throws IOException { + PathResource resource = new PathResource(NON_EXISTING_FILE); + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy( + resource::getInputStream); + } + + @Test + void getUrl() throws IOException { + PathResource resource = new PathResource(TEST_FILE); + assertThat(resource.getURL().toString()).endsWith("core/io/example.properties"); + } + + @Test + void getUri() throws IOException { + PathResource resource = new PathResource(TEST_FILE); + assertThat(resource.getURI().toString()).endsWith("core/io/example.properties"); + } + + @Test + void getFile() throws IOException { + PathResource resource = new PathResource(TEST_FILE); + File file = new File(TEST_FILE); + assertThat(resource.getFile().getAbsoluteFile()).isEqualTo(file.getAbsoluteFile()); + } + + @Test + void getFileUnsupported() throws IOException { + Path path = mock(Path.class); + given(path.normalize()).willReturn(path); + given(path.toFile()).willThrow(new UnsupportedOperationException()); + PathResource resource = new PathResource(path); + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy( + resource::getFile); + } + + @Test + void contentLength() throws IOException { + PathResource resource = new PathResource(TEST_FILE); + File file = new File(TEST_FILE); + assertThat(resource.contentLength()).isEqualTo(file.length()); + } + + @Test + void contentLengthForDirectory() throws IOException { + PathResource resource = new PathResource(TEST_DIR); + File file = new File(TEST_DIR); + assertThat(resource.contentLength()).isEqualTo(file.length()); + } + + @Test + void lastModified() throws IOException { + PathResource resource = new PathResource(TEST_FILE); + File file = new File(TEST_FILE); + assertThat(resource.lastModified() / 1000).isEqualTo(file.lastModified() / 1000); + } + + @Test + void createRelativeFromDir() throws IOException { + Resource resource = new PathResource(TEST_DIR).createRelative("example.properties"); + assertThat(resource).isEqualTo(new PathResource(TEST_FILE)); + } + + @Test + void createRelativeFromFile() throws IOException { + Resource resource = new PathResource(TEST_FILE).createRelative("../example.properties"); + assertThat(resource).isEqualTo(new PathResource(TEST_FILE)); + } + + @Test + void filename() { + Resource resource = new PathResource(TEST_FILE); + assertThat(resource.getFilename()).isEqualTo("example.properties"); + } + + @Test + void description() { + Resource resource = new PathResource(TEST_FILE); + assertThat(resource.getDescription()).contains("path ["); + assertThat(resource.getDescription()).contains(TEST_FILE); + } + + @Test + void fileIsWritable() { + PathResource resource = new PathResource(TEST_FILE); + assertThat(resource.isWritable()).isEqualTo(true); + } + + @Test + void directoryIsNotWritable() { + PathResource resource = new PathResource(TEST_DIR); + assertThat(resource.isWritable()).isEqualTo(false); + } + + @Test + void outputStream(@TempDir Path temporaryFolder) throws IOException { + PathResource resource = new PathResource(temporaryFolder.resolve("test")); + FileCopyUtils.copy("test".getBytes(StandardCharsets.UTF_8), resource.getOutputStream()); + assertThat(resource.contentLength()).isEqualTo(4L); + } + + @Test + void doesNotExistOutputStream(@TempDir Path temporaryFolder) throws IOException { + File file = temporaryFolder.resolve("test").toFile(); + file.delete(); + PathResource resource = new PathResource(file.toPath()); + FileCopyUtils.copy("test".getBytes(), resource.getOutputStream()); + assertThat(resource.contentLength()).isEqualTo(4L); + } + + @Test + void directoryOutputStream() throws IOException { + PathResource resource = new PathResource(TEST_DIR); + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy( + resource::getOutputStream); + } + + @Test + void getReadableByteChannel() throws IOException { + PathResource resource = new PathResource(TEST_FILE); + try (ReadableByteChannel channel = resource.readableChannel()) { + ByteBuffer buffer = ByteBuffer.allocate((int) resource.contentLength()); + channel.read(buffer); + buffer.rewind(); + assertThat(buffer.limit()).isGreaterThan(0); + } + } + + @Test + void getReadableByteChannelForDir() throws IOException { + PathResource resource = new PathResource(TEST_DIR); + try { + resource.readableChannel(); + } + catch (AccessDeniedException ex) { + // on Windows + } + } + + @Test + void getReadableByteChannelDoesNotExist() throws IOException { + PathResource resource = new PathResource(NON_EXISTING_FILE); + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy( + resource::readableChannel); + } + + @Test + void getWritableChannel(@TempDir Path temporaryFolder) throws IOException { + Path testPath = temporaryFolder.resolve("test"); + Files.createFile(testPath); + PathResource resource = new PathResource(testPath); + ByteBuffer buffer = ByteBuffer.wrap("test".getBytes(StandardCharsets.UTF_8)); + try (WritableByteChannel channel = resource.writableChannel()) { + channel.write(buffer); + } + assertThat(resource.contentLength()).isEqualTo(4L); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/io/ResourceEditorTests.java b/spring-core/src/test/java/org/springframework/core/io/ResourceEditorTests.java new file mode 100644 index 0000000..92ac760 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/io/ResourceEditorTests.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io; + +import java.beans.PropertyEditor; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.env.StandardEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Unit tests for the {@link ResourceEditor} class. + * + * @author Rick Evans + * @author Arjen Poutsma + * @author Dave Syer + */ +class ResourceEditorTests { + + @Test + void sunnyDay() { + PropertyEditor editor = new ResourceEditor(); + editor.setAsText("classpath:org/springframework/core/io/ResourceEditorTests.class"); + Resource resource = (Resource) editor.getValue(); + assertThat(resource).isNotNull(); + assertThat(resource.exists()).isTrue(); + } + + @Test + void ctorWithNullCtorArgs() { + assertThatIllegalArgumentException().isThrownBy(() -> + new ResourceEditor(null, null)); + } + + @Test + void setAndGetAsTextWithNull() { + PropertyEditor editor = new ResourceEditor(); + editor.setAsText(null); + assertThat(editor.getAsText()).isEqualTo(""); + } + + @Test + void setAndGetAsTextWithWhitespaceResource() { + PropertyEditor editor = new ResourceEditor(); + editor.setAsText(" "); + assertThat(editor.getAsText()).isEqualTo(""); + } + + @Test + void systemPropertyReplacement() { + PropertyEditor editor = new ResourceEditor(); + System.setProperty("test.prop", "foo"); + try { + editor.setAsText("${test.prop}"); + Resource resolved = (Resource) editor.getValue(); + assertThat(resolved.getFilename()).isEqualTo("foo"); + } + finally { + System.getProperties().remove("test.prop"); + } + } + + @Test + void systemPropertyReplacementWithUnresolvablePlaceholder() { + PropertyEditor editor = new ResourceEditor(); + System.setProperty("test.prop", "foo"); + try { + editor.setAsText("${test.prop}-${bar}"); + Resource resolved = (Resource) editor.getValue(); + assertThat(resolved.getFilename()).isEqualTo("foo-${bar}"); + } + finally { + System.getProperties().remove("test.prop"); + } + } + + @Test + void strictSystemPropertyReplacementWithUnresolvablePlaceholder() { + PropertyEditor editor = new ResourceEditor(new DefaultResourceLoader(), new StandardEnvironment(), false); + System.setProperty("test.prop", "foo"); + try { + assertThatIllegalArgumentException().isThrownBy(() -> { + editor.setAsText("${test.prop}-${bar}"); + editor.getValue(); + }); + } + finally { + System.getProperties().remove("test.prop"); + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/io/ResourceTests.java b/spring-core/src/test/java/org/springframework/core/io/ResourceTests.java new file mode 100644 index 0000000..5a9c8e5 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/io/ResourceTests.java @@ -0,0 +1,333 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashSet; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * Unit tests for various {@link Resource} implementations. + * + * @author Juergen Hoeller + * @author Chris Beams + * @author Sam Brannen + * @since 09.09.2004 + */ +class ResourceTests { + + @Test + void byteArrayResource() throws IOException { + Resource resource = new ByteArrayResource("testString".getBytes()); + assertThat(resource.exists()).isTrue(); + assertThat(resource.isOpen()).isFalse(); + String content = FileCopyUtils.copyToString(new InputStreamReader(resource.getInputStream())); + assertThat(content).isEqualTo("testString"); + assertThat(new ByteArrayResource("testString".getBytes())).isEqualTo(resource); + } + + @Test + void byteArrayResourceWithDescription() throws IOException { + Resource resource = new ByteArrayResource("testString".getBytes(), "my description"); + assertThat(resource.exists()).isTrue(); + assertThat(resource.isOpen()).isFalse(); + String content = FileCopyUtils.copyToString(new InputStreamReader(resource.getInputStream())); + assertThat(content).isEqualTo("testString"); + assertThat(resource.getDescription().contains("my description")).isTrue(); + assertThat(new ByteArrayResource("testString".getBytes())).isEqualTo(resource); + } + + @Test + void inputStreamResource() throws IOException { + InputStream is = new ByteArrayInputStream("testString".getBytes()); + Resource resource = new InputStreamResource(is); + assertThat(resource.exists()).isTrue(); + assertThat(resource.isOpen()).isTrue(); + String content = FileCopyUtils.copyToString(new InputStreamReader(resource.getInputStream())); + assertThat(content).isEqualTo("testString"); + assertThat(new InputStreamResource(is)).isEqualTo(resource); + } + + @Test + void inputStreamResourceWithDescription() throws IOException { + InputStream is = new ByteArrayInputStream("testString".getBytes()); + Resource resource = new InputStreamResource(is, "my description"); + assertThat(resource.exists()).isTrue(); + assertThat(resource.isOpen()).isTrue(); + String content = FileCopyUtils.copyToString(new InputStreamReader(resource.getInputStream())); + assertThat(content).isEqualTo("testString"); + assertThat(resource.getDescription().contains("my description")).isTrue(); + assertThat(new InputStreamResource(is)).isEqualTo(resource); + } + + @Test + void classPathResource() throws IOException { + Resource resource = new ClassPathResource("org/springframework/core/io/Resource.class"); + doTestResource(resource); + Resource resource2 = new ClassPathResource("org/springframework/core/../core/io/./Resource.class"); + assertThat(resource2).isEqualTo(resource); + Resource resource3 = new ClassPathResource("org/springframework/core/").createRelative("../core/io/./Resource.class"); + assertThat(resource3).isEqualTo(resource); + + // Check whether equal/hashCode works in a HashSet. + HashSet resources = new HashSet<>(); + resources.add(resource); + resources.add(resource2); + assertThat(resources.size()).isEqualTo(1); + } + + @Test + void classPathResourceWithClassLoader() throws IOException { + Resource resource = + new ClassPathResource("org/springframework/core/io/Resource.class", getClass().getClassLoader()); + doTestResource(resource); + assertThat(new ClassPathResource("org/springframework/core/../core/io/./Resource.class", getClass().getClassLoader())).isEqualTo(resource); + } + + @Test + void classPathResourceWithClass() throws IOException { + Resource resource = new ClassPathResource("Resource.class", getClass()); + doTestResource(resource); + assertThat(new ClassPathResource("Resource.class", getClass())).isEqualTo(resource); + } + + @Test + void fileSystemResource() throws IOException { + String file = getClass().getResource("Resource.class").getFile(); + Resource resource = new FileSystemResource(file); + doTestResource(resource); + assertThat(resource).isEqualTo(new FileSystemResource(file)); + } + + @Test + void fileSystemResourceWithFile() throws IOException { + File file = new File(getClass().getResource("Resource.class").getFile()); + Resource resource = new FileSystemResource(file); + doTestResource(resource); + assertThat(resource).isEqualTo(new FileSystemResource(file)); + } + + @Test + void fileSystemResourceWithFilePath() throws Exception { + Path filePath = Paths.get(getClass().getResource("Resource.class").toURI()); + Resource resource = new FileSystemResource(filePath); + doTestResource(resource); + assertThat(resource).isEqualTo(new FileSystemResource(filePath)); + } + + @Test + void fileSystemResourceWithPlainPath() { + Resource resource = new FileSystemResource("core/io/Resource.class"); + assertThat(new FileSystemResource("core/../core/io/./Resource.class")).isEqualTo(resource); + } + + @Test + void urlResource() throws IOException { + Resource resource = new UrlResource(getClass().getResource("Resource.class")); + doTestResource(resource); + assertThat(resource).isEqualTo(new UrlResource(getClass().getResource("Resource.class"))); + + Resource resource2 = new UrlResource("file:core/io/Resource.class"); + assertThat(new UrlResource("file:core/../core/io/./Resource.class")).isEqualTo(resource2); + + assertThat(new UrlResource("file:/dir/test.txt?argh").getFilename()).isEqualTo("test.txt"); + assertThat(new UrlResource("file:\\dir\\test.txt?argh").getFilename()).isEqualTo("test.txt"); + assertThat(new UrlResource("file:\\dir/test.txt?argh").getFilename()).isEqualTo("test.txt"); + } + + private void doTestResource(Resource resource) throws IOException { + assertThat(resource.getFilename()).isEqualTo("Resource.class"); + assertThat(resource.getURL().getFile().endsWith("Resource.class")).isTrue(); + assertThat(resource.exists()).isTrue(); + assertThat(resource.isReadable()).isTrue(); + assertThat(resource.contentLength() > 0).isTrue(); + assertThat(resource.lastModified() > 0).isTrue(); + + Resource relative1 = resource.createRelative("ClassPathResource.class"); + assertThat(relative1.getFilename()).isEqualTo("ClassPathResource.class"); + assertThat(relative1.getURL().getFile().endsWith("ClassPathResource.class")).isTrue(); + assertThat(relative1.exists()).isTrue(); + assertThat(relative1.isReadable()).isTrue(); + assertThat(relative1.contentLength() > 0).isTrue(); + assertThat(relative1.lastModified() > 0).isTrue(); + + Resource relative2 = resource.createRelative("support/ResourcePatternResolver.class"); + assertThat(relative2.getFilename()).isEqualTo("ResourcePatternResolver.class"); + assertThat(relative2.getURL().getFile().endsWith("ResourcePatternResolver.class")).isTrue(); + assertThat(relative2.exists()).isTrue(); + assertThat(relative2.isReadable()).isTrue(); + assertThat(relative2.contentLength() > 0).isTrue(); + assertThat(relative2.lastModified() > 0).isTrue(); + + Resource relative3 = resource.createRelative("../SpringVersion.class"); + assertThat(relative3.getFilename()).isEqualTo("SpringVersion.class"); + assertThat(relative3.getURL().getFile().endsWith("SpringVersion.class")).isTrue(); + assertThat(relative3.exists()).isTrue(); + assertThat(relative3.isReadable()).isTrue(); + assertThat(relative3.contentLength() > 0).isTrue(); + assertThat(relative3.lastModified() > 0).isTrue(); + + Resource relative4 = resource.createRelative("X.class"); + assertThat(relative4.exists()).isFalse(); + assertThat(relative4.isReadable()).isFalse(); + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy( + relative4::contentLength); + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy( + relative4::lastModified); + } + + @Test + void classPathResourceWithRelativePath() throws IOException { + Resource resource = new ClassPathResource("dir/"); + Resource relative = resource.createRelative("subdir"); + assertThat(relative).isEqualTo(new ClassPathResource("dir/subdir")); + } + + @Test + void fileSystemResourceWithRelativePath() throws IOException { + Resource resource = new FileSystemResource("dir/"); + Resource relative = resource.createRelative("subdir"); + assertThat(relative).isEqualTo(new FileSystemResource("dir/subdir")); + } + + @Test + void urlResourceWithRelativePath() throws IOException { + Resource resource = new UrlResource("file:dir/"); + Resource relative = resource.createRelative("subdir"); + assertThat(relative).isEqualTo(new UrlResource("file:dir/subdir")); + } + + @Test + void nonFileResourceExists() throws Exception { + URL url = new URL("https://spring.io/"); + + // Abort if spring.io is not reachable. + assumeTrue(urlIsReachable(url)); + + Resource resource = new UrlResource(url); + assertThat(resource.exists()).isTrue(); + } + + private boolean urlIsReachable(URL url) { + try { + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("HEAD"); + connection.setReadTimeout(5_000); + return connection.getResponseCode() == HttpURLConnection.HTTP_OK; + } + catch (Exception ex) { + return false; + } + } + + @Test + void abstractResourceExceptions() throws Exception { + final String name = "test-resource"; + + Resource resource = new AbstractResource() { + @Override + public String getDescription() { + return name; + } + @Override + public InputStream getInputStream() throws IOException { + throw new FileNotFoundException(); + } + }; + + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy( + resource::getURL) + .withMessageContaining(name); + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy( + resource::getFile) + .withMessageContaining(name); + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(() -> + resource.createRelative("/testing")) + .withMessageContaining(name); + + assertThat(resource.getFilename()).isNull(); + } + + @Test + void contentLength() throws IOException { + AbstractResource resource = new AbstractResource() { + @Override + public InputStream getInputStream() { + return new ByteArrayInputStream(new byte[] { 'a', 'b', 'c' }); + } + @Override + public String getDescription() { + return ""; + } + }; + assertThat(resource.contentLength()).isEqualTo(3L); + } + + @Test + void readableChannel() throws IOException { + Resource resource = new FileSystemResource(getClass().getResource("Resource.class").getFile()); + try (ReadableByteChannel channel = resource.readableChannel()) { + ByteBuffer buffer = ByteBuffer.allocate((int) resource.contentLength()); + channel.read(buffer); + buffer.rewind(); + assertThat(buffer.limit() > 0).isTrue(); + } + } + + @Test + void inputStreamNotFoundOnFileSystemResource() throws IOException { + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(() -> + new FileSystemResource(getClass().getResource("Resource.class").getFile()).createRelative("X").getInputStream()); + } + + @Test + void readableChannelNotFoundOnFileSystemResource() throws IOException { + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(() -> + new FileSystemResource(getClass().getResource("Resource.class").getFile()).createRelative("X").readableChannel()); + } + + @Test + void inputStreamNotFoundOnClassPathResource() throws IOException { + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(() -> + new ClassPathResource("Resource.class", getClass()).createRelative("X").getInputStream()); + } + + @Test + void readableChannelNotFoundOnClassPathResource() throws IOException { + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(() -> + new ClassPathResource("Resource.class", getClass()).createRelative("X").readableChannel()); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java b/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java new file mode 100644 index 0000000..619b9ce --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java @@ -0,0 +1,746 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.buffer; + +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import org.springframework.core.testfixture.io.buffer.AbstractDataBufferAllocatingTests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Arjen Poutsma + * @author Sam Brannen + */ +class DataBufferTests extends AbstractDataBufferAllocatingTests { + + @ParameterizedDataBufferAllocatingTest + void byteCountsAndPositions(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = createDataBuffer(2); + + assertThat(buffer.readPosition()).isEqualTo(0); + assertThat(buffer.writePosition()).isEqualTo(0); + assertThat(buffer.readableByteCount()).isEqualTo(0); + assertThat(buffer.writableByteCount()).isEqualTo(2); + assertThat(buffer.capacity()).isEqualTo(2); + + buffer.write((byte) 'a'); + assertThat(buffer.readPosition()).isEqualTo(0); + assertThat(buffer.writePosition()).isEqualTo(1); + assertThat(buffer.readableByteCount()).isEqualTo(1); + assertThat(buffer.writableByteCount()).isEqualTo(1); + assertThat(buffer.capacity()).isEqualTo(2); + + buffer.write((byte) 'b'); + assertThat(buffer.readPosition()).isEqualTo(0); + assertThat(buffer.writePosition()).isEqualTo(2); + assertThat(buffer.readableByteCount()).isEqualTo(2); + assertThat(buffer.writableByteCount()).isEqualTo(0); + assertThat(buffer.capacity()).isEqualTo(2); + + buffer.read(); + assertThat(buffer.readPosition()).isEqualTo(1); + assertThat(buffer.writePosition()).isEqualTo(2); + assertThat(buffer.readableByteCount()).isEqualTo(1); + assertThat(buffer.writableByteCount()).isEqualTo(0); + assertThat(buffer.capacity()).isEqualTo(2); + + buffer.read(); + assertThat(buffer.readPosition()).isEqualTo(2); + assertThat(buffer.writePosition()).isEqualTo(2); + assertThat(buffer.readableByteCount()).isEqualTo(0); + assertThat(buffer.writableByteCount()).isEqualTo(0); + assertThat(buffer.capacity()).isEqualTo(2); + + release(buffer); + } + + @ParameterizedDataBufferAllocatingTest + void readPositionSmallerThanZero(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = createDataBuffer(1); + try { + assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> + buffer.readPosition(-1)); + } + finally { + release(buffer); + } + } + + @ParameterizedDataBufferAllocatingTest + void readPositionGreaterThanWritePosition(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = createDataBuffer(1); + try { + assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> + buffer.readPosition(1)); + } + finally { + release(buffer); + } + } + + @ParameterizedDataBufferAllocatingTest + void writePositionSmallerThanReadPosition(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = createDataBuffer(2); + try { + buffer.write((byte) 'a'); + buffer.read(); + assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> + buffer.writePosition(0)); + } + finally { + release(buffer); + } + } + + @ParameterizedDataBufferAllocatingTest + void writePositionGreaterThanCapacity(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = createDataBuffer(1); + try { + assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> + buffer.writePosition(2)); + } + finally { + release(buffer); + } + } + + @ParameterizedDataBufferAllocatingTest + void writeAndRead(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = createDataBuffer(5); + buffer.write(new byte[]{'a', 'b', 'c'}); + + int ch = buffer.read(); + assertThat(ch).isEqualTo((byte) 'a'); + + buffer.write((byte) 'd'); + buffer.write((byte) 'e'); + + byte[] result = new byte[4]; + buffer.read(result); + + assertThat(result).isEqualTo(new byte[]{'b', 'c', 'd', 'e'}); + + release(buffer); + } + + @ParameterizedDataBufferAllocatingTest + void writeNullString(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = createDataBuffer(1); + try { + assertThatIllegalArgumentException().isThrownBy(() -> + buffer.write(null, StandardCharsets.UTF_8)); + } + finally { + release(buffer); + } + } + + @ParameterizedDataBufferAllocatingTest + void writeNullCharset(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = createDataBuffer(1); + try { + assertThatIllegalArgumentException().isThrownBy(() -> + buffer.write("test", null)); + } + finally { + release(buffer); + } + } + + @ParameterizedDataBufferAllocatingTest + void writeEmptyString(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = createDataBuffer(1); + buffer.write("", StandardCharsets.UTF_8); + + assertThat(buffer.readableByteCount()).isEqualTo(0); + + release(buffer); + } + + @ParameterizedDataBufferAllocatingTest + void writeUtf8String(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = createDataBuffer(6); + buffer.write("Spring", StandardCharsets.UTF_8); + + byte[] result = new byte[6]; + buffer.read(result); + + assertThat(result).isEqualTo("Spring".getBytes(StandardCharsets.UTF_8)); + release(buffer); + } + + @ParameterizedDataBufferAllocatingTest + void writeUtf8StringOutGrowsCapacity(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = createDataBuffer(5); + buffer.write("Spring €", StandardCharsets.UTF_8); + + byte[] result = new byte[10]; + buffer.read(result); + + assertThat(result).isEqualTo("Spring €".getBytes(StandardCharsets.UTF_8)); + release(buffer); + } + + @ParameterizedDataBufferAllocatingTest + void writeIsoString(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = createDataBuffer(3); + buffer.write("\u00A3", StandardCharsets.ISO_8859_1); + + byte[] result = new byte[1]; + buffer.read(result); + + assertThat(result).isEqualTo("\u00A3".getBytes(StandardCharsets.ISO_8859_1)); + release(buffer); + } + + @ParameterizedDataBufferAllocatingTest + void writeMultipleUtf8String(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = createDataBuffer(1); + buffer.write("abc", StandardCharsets.UTF_8); + assertThat(buffer.readableByteCount()).isEqualTo(3); + + buffer.write("def", StandardCharsets.UTF_8); + assertThat(buffer.readableByteCount()).isEqualTo(6); + + buffer.write("ghi", StandardCharsets.UTF_8); + assertThat(buffer.readableByteCount()).isEqualTo(9); + + byte[] result = new byte[9]; + buffer.read(result); + + assertThat(result).isEqualTo("abcdefghi".getBytes()); + + release(buffer); + } + + @ParameterizedDataBufferAllocatingTest + void toStringNullCharset(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = createDataBuffer(1); + try { + assertThatIllegalArgumentException().isThrownBy(() -> + buffer.toString(null)); + } + finally { + release(buffer); + } + } + + @ParameterizedDataBufferAllocatingTest + void toStringUtf8(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + String spring = "Spring"; + byte[] bytes = spring.getBytes(StandardCharsets.UTF_8); + DataBuffer buffer = createDataBuffer(bytes.length); + buffer.write(bytes); + + String result = buffer.toString(StandardCharsets.UTF_8); + + assertThat(result).isEqualTo(spring); + release(buffer); + } + + @ParameterizedDataBufferAllocatingTest + void toStringSection(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + String spring = "Spring"; + byte[] bytes = spring.getBytes(StandardCharsets.UTF_8); + DataBuffer buffer = createDataBuffer(bytes.length); + buffer.write(bytes); + + String result = buffer.toString(1, 3, StandardCharsets.UTF_8); + + assertThat(result).isEqualTo("pri"); + release(buffer); + } + + @ParameterizedDataBufferAllocatingTest + void inputStream(String displayName, DataBufferFactory bufferFactory) throws Exception { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = createDataBuffer(4); + buffer.write(new byte[]{'a', 'b', 'c', 'd', 'e'}); + buffer.readPosition(1); + + InputStream inputStream = buffer.asInputStream(); + + assertThat(inputStream.available()).isEqualTo(4); + + int result = inputStream.read(); + assertThat(result).isEqualTo((byte) 'b'); + assertThat(inputStream.available()).isEqualTo(3); + + byte[] bytes = new byte[2]; + int len = inputStream.read(bytes); + assertThat(len).isEqualTo(2); + assertThat(bytes).isEqualTo(new byte[]{'c', 'd'}); + assertThat(inputStream.available()).isEqualTo(1); + + Arrays.fill(bytes, (byte) 0); + len = inputStream.read(bytes); + assertThat(len).isEqualTo(1); + assertThat(bytes).isEqualTo(new byte[]{'e', (byte) 0}); + assertThat(inputStream.available()).isEqualTo(0); + + assertThat(inputStream.read()).isEqualTo(-1); + assertThat(inputStream.read(bytes)).isEqualTo(-1); + + release(buffer); + } + + @ParameterizedDataBufferAllocatingTest + void inputStreamReleaseOnClose(String displayName, DataBufferFactory bufferFactory) throws Exception { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = createDataBuffer(3); + byte[] bytes = {'a', 'b', 'c'}; + buffer.write(bytes); + + try (InputStream inputStream = buffer.asInputStream(true)) { + byte[] result = new byte[3]; + int len = inputStream.read(result); + assertThat(len).isEqualTo(3); + assertThat(result).isEqualTo(bytes); + } + + // AbstractDataBufferAllocatingTests.leakDetector will verify the buffer's release + } + + @ParameterizedDataBufferAllocatingTest + void outputStream(String displayName, DataBufferFactory bufferFactory) throws Exception { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = createDataBuffer(4); + buffer.write((byte) 'a'); + + OutputStream outputStream = buffer.asOutputStream(); + outputStream.write('b'); + outputStream.write(new byte[]{'c', 'd'}); + + buffer.write((byte) 'e'); + + byte[] bytes = new byte[5]; + buffer.read(bytes); + assertThat(bytes).isEqualTo(new byte[]{'a', 'b', 'c', 'd', 'e'}); + + release(buffer); + } + + @ParameterizedDataBufferAllocatingTest + void expand(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = createDataBuffer(1); + buffer.write((byte) 'a'); + assertThat(buffer.capacity()).isEqualTo(1); + buffer.write((byte) 'b'); + + assertThat(buffer.capacity() > 1).isTrue(); + + release(buffer); + } + + @ParameterizedDataBufferAllocatingTest + void increaseCapacity(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = createDataBuffer(1); + assertThat(buffer.capacity()).isEqualTo(1); + + buffer.capacity(2); + assertThat(buffer.capacity()).isEqualTo(2); + + release(buffer); + } + + @ParameterizedDataBufferAllocatingTest + void decreaseCapacityLowReadPosition(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = createDataBuffer(2); + buffer.writePosition(2); + buffer.capacity(1); + assertThat(buffer.capacity()).isEqualTo(1); + + release(buffer); + } + + @ParameterizedDataBufferAllocatingTest + void decreaseCapacityHighReadPosition(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = createDataBuffer(2); + buffer.writePosition(2); + buffer.readPosition(2); + buffer.capacity(1); + assertThat(buffer.capacity()).isEqualTo(1); + + release(buffer); + } + + @ParameterizedDataBufferAllocatingTest + void capacityLessThanZero(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = createDataBuffer(1); + try { + assertThatIllegalArgumentException().isThrownBy(() -> + buffer.capacity(-1)); + } + finally { + release(buffer); + } + } + + @ParameterizedDataBufferAllocatingTest + void writeByteBuffer(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer1 = createDataBuffer(1); + buffer1.write((byte) 'a'); + ByteBuffer buffer2 = createByteBuffer(2); + buffer2.put((byte) 'b'); + buffer2.flip(); + ByteBuffer buffer3 = createByteBuffer(3); + buffer3.put((byte) 'c'); + buffer3.flip(); + + buffer1.write(buffer2, buffer3); + buffer1.write((byte) 'd'); // make sure the write index is correctly set + + assertThat(buffer1.readableByteCount()).isEqualTo(4); + byte[] result = new byte[4]; + buffer1.read(result); + + assertThat(result).isEqualTo(new byte[]{'a', 'b', 'c', 'd'}); + + release(buffer1); + } + + private ByteBuffer createByteBuffer(int capacity) { + return ByteBuffer.allocate(capacity); + } + + @ParameterizedDataBufferAllocatingTest + void writeDataBuffer(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer1 = createDataBuffer(1); + buffer1.write((byte) 'a'); + DataBuffer buffer2 = createDataBuffer(2); + buffer2.write((byte) 'b'); + DataBuffer buffer3 = createDataBuffer(3); + buffer3.write((byte) 'c'); + + buffer1.write(buffer2, buffer3); + buffer1.write((byte) 'd'); // make sure the write index is correctly set + + assertThat(buffer1.readableByteCount()).isEqualTo(4); + byte[] result = new byte[4]; + buffer1.read(result); + + assertThat(result).isEqualTo(new byte[]{'a', 'b', 'c', 'd'}); + + release(buffer1, buffer2, buffer3); + } + + @ParameterizedDataBufferAllocatingTest + void asByteBuffer(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = createDataBuffer(4); + buffer.write(new byte[]{'a', 'b', 'c'}); + buffer.read(); // skip a + + ByteBuffer result = buffer.asByteBuffer(); + assertThat(result.capacity()).isEqualTo(2); + + buffer.write((byte) 'd'); + assertThat(result.remaining()).isEqualTo(2); + + byte[] resultBytes = new byte[2]; + result.get(resultBytes); + assertThat(resultBytes).isEqualTo(new byte[]{'b', 'c'}); + + release(buffer); + } + + @ParameterizedDataBufferAllocatingTest + void asByteBufferIndexLength(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = createDataBuffer(3); + buffer.write(new byte[]{'a', 'b'}); + + ByteBuffer result = buffer.asByteBuffer(1, 2); + assertThat(result.capacity()).isEqualTo(2); + + buffer.write((byte) 'c'); + assertThat(result.remaining()).isEqualTo(2); + + byte[] resultBytes = new byte[2]; + result.get(resultBytes); + assertThat(resultBytes).isEqualTo(new byte[]{'b', 'c'}); + + release(buffer); + } + + @ParameterizedDataBufferAllocatingTest + void byteBufferContainsDataBufferChanges(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer dataBuffer = createDataBuffer(1); + ByteBuffer byteBuffer = dataBuffer.asByteBuffer(0, 1); + + dataBuffer.write((byte) 'a'); + + assertThat(byteBuffer.limit()).isEqualTo(1); + byte b = byteBuffer.get(); + assertThat(b).isEqualTo((byte) 'a'); + + release(dataBuffer); + } + + @ParameterizedDataBufferAllocatingTest + void dataBufferContainsByteBufferChanges(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer dataBuffer = createDataBuffer(1); + ByteBuffer byteBuffer = dataBuffer.asByteBuffer(0, 1); + + byteBuffer.put((byte) 'a'); + dataBuffer.writePosition(1); + + byte b = dataBuffer.read(); + assertThat(b).isEqualTo((byte) 'a'); + + release(dataBuffer); + } + + @ParameterizedDataBufferAllocatingTest + void emptyAsByteBuffer(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = createDataBuffer(1); + + ByteBuffer result = buffer.asByteBuffer(); + assertThat(result.capacity()).isEqualTo(0); + + release(buffer); + } + + @ParameterizedDataBufferAllocatingTest + void indexOf(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = createDataBuffer(3); + buffer.write(new byte[]{'a', 'b', 'c'}); + + int result = buffer.indexOf(b -> b == 'c', 0); + assertThat(result).isEqualTo(2); + + result = buffer.indexOf(b -> b == 'c', Integer.MIN_VALUE); + assertThat(result).isEqualTo(2); + + result = buffer.indexOf(b -> b == 'c', Integer.MAX_VALUE); + assertThat(result).isEqualTo(-1); + + result = buffer.indexOf(b -> b == 'z', 0); + assertThat(result).isEqualTo(-1); + + release(buffer); + } + + @ParameterizedDataBufferAllocatingTest + void lastIndexOf(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = createDataBuffer(3); + buffer.write(new byte[]{'a', 'b', 'c'}); + + int result = buffer.lastIndexOf(b -> b == 'b', 2); + assertThat(result).isEqualTo(1); + + result = buffer.lastIndexOf(b -> b == 'c', 2); + assertThat(result).isEqualTo(2); + + result = buffer.lastIndexOf(b -> b == 'b', Integer.MAX_VALUE); + assertThat(result).isEqualTo(1); + + result = buffer.lastIndexOf(b -> b == 'c', Integer.MAX_VALUE); + assertThat(result).isEqualTo(2); + + result = buffer.lastIndexOf(b -> b == 'b', Integer.MIN_VALUE); + assertThat(result).isEqualTo(-1); + + result = buffer.lastIndexOf(b -> b == 'c', Integer.MIN_VALUE); + assertThat(result).isEqualTo(-1); + + result = buffer.lastIndexOf(b -> b == 'z', 0); + assertThat(result).isEqualTo(-1); + + release(buffer); + } + + @ParameterizedDataBufferAllocatingTest + void slice(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = createDataBuffer(3); + buffer.write(new byte[]{'a', 'b'}); + + DataBuffer slice = buffer.slice(1, 2); + assertThat(slice.readableByteCount()).isEqualTo(2); + assertThatExceptionOfType(Exception.class).isThrownBy(() -> + slice.write((byte) 0)); + buffer.write((byte) 'c'); + + assertThat(buffer.readableByteCount()).isEqualTo(3); + byte[] result = new byte[3]; + buffer.read(result); + + assertThat(result).isEqualTo(new byte[]{'a', 'b', 'c'}); + + assertThat(slice.readableByteCount()).isEqualTo(2); + result = new byte[2]; + slice.read(result); + + assertThat(result).isEqualTo(new byte[]{'b', 'c'}); + + + release(buffer); + } + + @ParameterizedDataBufferAllocatingTest + void retainedSlice(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = createDataBuffer(3); + buffer.write(new byte[]{'a', 'b'}); + + DataBuffer slice = buffer.retainedSlice(1, 2); + assertThat(slice.readableByteCount()).isEqualTo(2); + assertThatExceptionOfType(Exception.class).isThrownBy(() -> + slice.write((byte) 0)); + buffer.write((byte) 'c'); + + assertThat(buffer.readableByteCount()).isEqualTo(3); + byte[] result = new byte[3]; + buffer.read(result); + + assertThat(result).isEqualTo(new byte[]{'a', 'b', 'c'}); + + assertThat(slice.readableByteCount()).isEqualTo(2); + result = new byte[2]; + slice.read(result); + + assertThat(result).isEqualTo(new byte[]{'b', 'c'}); + + + release(buffer, slice); + } + + @ParameterizedDataBufferAllocatingTest + void spr16351(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = createDataBuffer(6); + byte[] bytes = {'a', 'b', 'c', 'd', 'e', 'f'}; + buffer.write(bytes); + DataBuffer slice = buffer.slice(3, 3); + buffer.writePosition(3); + buffer.write(slice); + + assertThat(buffer.readableByteCount()).isEqualTo(6); + byte[] result = new byte[6]; + buffer.read(result); + + assertThat(result).isEqualTo(bytes); + + release(buffer); + } + + @ParameterizedDataBufferAllocatingTest + void join(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer composite = this.bufferFactory.join(Arrays.asList(stringBuffer("a"), + stringBuffer("b"), stringBuffer("c"))); + assertThat(composite.readableByteCount()).isEqualTo(3); + byte[] bytes = new byte[3]; + composite.read(bytes); + + assertThat(bytes).isEqualTo(new byte[] {'a','b','c'}); + + release(composite); + } + + @ParameterizedDataBufferAllocatingTest + void getByte(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = stringBuffer("abc"); + + assertThat(buffer.getByte(0)).isEqualTo((byte) 'a'); + assertThat(buffer.getByte(1)).isEqualTo((byte) 'b'); + assertThat(buffer.getByte(2)).isEqualTo((byte) 'c'); + assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> + buffer.getByte(-1)); + + assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> + buffer.getByte(3)); + + release(buffer); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferUtilsTests.java b/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferUtilsTests.java new file mode 100644 index 0000000..8615551 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferUtilsTests.java @@ -0,0 +1,952 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.buffer; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousFileChannel; +import java.nio.channels.CompletionHandler; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.PooledByteBufAllocator; +import org.junit.jupiter.api.Test; +import org.mockito.stubbing.Answer; +import org.reactivestreams.Subscription; +import reactor.core.publisher.BaseSubscriber; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.testfixture.io.buffer.AbstractDataBufferAllocatingTests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; + +/** + * @author Arjen Poutsma + * @author Sam Brannen + */ +class DataBufferUtilsTests extends AbstractDataBufferAllocatingTests { + + private final Resource resource; + private final Path tempFile; + + + DataBufferUtilsTests() throws Exception { + this.resource = new ClassPathResource("DataBufferUtilsTests.txt", getClass()); + this.tempFile = Files.createTempFile("DataBufferUtilsTests", null); + } + + + @ParameterizedDataBufferAllocatingTest + void readInputStream(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + Flux flux = DataBufferUtils.readInputStream( + () -> this.resource.getInputStream(), super.bufferFactory, 3); + + verifyReadData(flux); + } + + @ParameterizedDataBufferAllocatingTest + void readByteChannel(String displayName, DataBufferFactory bufferFactory) throws Exception { + super.bufferFactory = bufferFactory; + + URI uri = this.resource.getURI(); + Flux result = + DataBufferUtils.readByteChannel(() -> FileChannel.open(Paths.get(uri), StandardOpenOption.READ), + super.bufferFactory, 3); + + verifyReadData(result); + } + + @ParameterizedDataBufferAllocatingTest + void readByteChannelError(String displayName, DataBufferFactory bufferFactory) throws Exception { + super.bufferFactory = bufferFactory; + + ReadableByteChannel channel = mock(ReadableByteChannel.class); + given(channel.read(any())) + .willAnswer(invocation -> { + ByteBuffer buffer = invocation.getArgument(0); + buffer.put("foo".getBytes(StandardCharsets.UTF_8)); + buffer.flip(); + return 3; + }) + .willThrow(new IOException()); + + Flux result = + DataBufferUtils.readByteChannel(() -> channel, super.bufferFactory, 3); + + StepVerifier.create(result) + .consumeNextWith(stringConsumer("foo")) + .expectError(IOException.class) + .verify(Duration.ofSeconds(3)); + } + + @ParameterizedDataBufferAllocatingTest + void readByteChannelCancel(String displayName, DataBufferFactory bufferFactory) throws Exception { + super.bufferFactory = bufferFactory; + + URI uri = this.resource.getURI(); + Flux result = + DataBufferUtils.readByteChannel(() -> FileChannel.open(Paths.get(uri), StandardOpenOption.READ), + super.bufferFactory, 3); + + StepVerifier.create(result) + .consumeNextWith(stringConsumer("foo")) + .thenCancel() + .verify(); + } + + @ParameterizedDataBufferAllocatingTest + void readAsynchronousFileChannel(String displayName, DataBufferFactory bufferFactory) throws Exception { + super.bufferFactory = bufferFactory; + + URI uri = this.resource.getURI(); + Flux flux = DataBufferUtils.readAsynchronousFileChannel( + () -> AsynchronousFileChannel.open(Paths.get(uri), StandardOpenOption.READ), + super.bufferFactory, 3); + + verifyReadData(flux); + } + + @ParameterizedDataBufferAllocatingTest + void readAsynchronousFileChannelPosition(String displayName, DataBufferFactory bufferFactory) throws Exception { + super.bufferFactory = bufferFactory; + + URI uri = this.resource.getURI(); + Flux flux = DataBufferUtils.readAsynchronousFileChannel( + () -> AsynchronousFileChannel.open(Paths.get(uri), StandardOpenOption.READ), + 9, super.bufferFactory, 3); + + StepVerifier.create(flux) + .consumeNextWith(stringConsumer("qux")) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @ParameterizedDataBufferAllocatingTest + void readAsynchronousFileChannelError(String displayName, DataBufferFactory bufferFactory) throws Exception { + super.bufferFactory = bufferFactory; + + AsynchronousFileChannel channel = mock(AsynchronousFileChannel.class); + willAnswer(invocation -> { + ByteBuffer byteBuffer = invocation.getArgument(0); + byteBuffer.put("foo".getBytes(StandardCharsets.UTF_8)); + byteBuffer.flip(); + long pos = invocation.getArgument(1); + assertThat(pos).isEqualTo(0); + DataBuffer dataBuffer = invocation.getArgument(2); + CompletionHandler completionHandler = invocation.getArgument(3); + completionHandler.completed(3, dataBuffer); + return null; + }).willAnswer(invocation -> { + DataBuffer dataBuffer = invocation.getArgument(2); + CompletionHandler completionHandler = invocation.getArgument(3); + completionHandler.failed(new IOException(), dataBuffer); + return null; + }) + .given(channel).read(any(), anyLong(), any(), any()); + + Flux result = + DataBufferUtils.readAsynchronousFileChannel(() -> channel, super.bufferFactory, 3); + + StepVerifier.create(result) + .consumeNextWith(stringConsumer("foo")) + .expectError(IOException.class) + .verify(Duration.ofSeconds(3)); + } + + @ParameterizedDataBufferAllocatingTest + void readAsynchronousFileChannelCancel(String displayName, DataBufferFactory bufferFactory) throws Exception { + super.bufferFactory = bufferFactory; + + URI uri = this.resource.getURI(); + Flux flux = DataBufferUtils.readAsynchronousFileChannel( + () -> AsynchronousFileChannel.open(Paths.get(uri), StandardOpenOption.READ), + super.bufferFactory, 3); + + StepVerifier.create(flux) + .consumeNextWith(stringConsumer("foo")) + .thenCancel() + .verify(); + } + + @ParameterizedDataBufferAllocatingTest // gh-22107 + void readAsynchronousFileChannelCancelWithoutDemand(String displayName, DataBufferFactory bufferFactory) throws Exception { + super.bufferFactory = bufferFactory; + + URI uri = this.resource.getURI(); + Flux flux = DataBufferUtils.readAsynchronousFileChannel( + () -> AsynchronousFileChannel.open(Paths.get(uri), StandardOpenOption.READ), + super.bufferFactory, 3); + + BaseSubscriber subscriber = new ZeroDemandSubscriber(); + flux.subscribe(subscriber); + subscriber.cancel(); + } + + @ParameterizedDataBufferAllocatingTest + void readPath(String displayName, DataBufferFactory bufferFactory) throws Exception { + super.bufferFactory = bufferFactory; + + Flux flux = DataBufferUtils.read(this.resource.getFile().toPath(), super.bufferFactory, 3); + + verifyReadData(flux); + } + + @ParameterizedDataBufferAllocatingTest + void readResource(String displayName, DataBufferFactory bufferFactory) throws Exception { + super.bufferFactory = bufferFactory; + + Flux flux = DataBufferUtils.read(this.resource, super.bufferFactory, 3); + + verifyReadData(flux); + } + + @ParameterizedDataBufferAllocatingTest + void readResourcePosition(String displayName, DataBufferFactory bufferFactory) throws Exception { + super.bufferFactory = bufferFactory; + + Flux flux = DataBufferUtils.read(this.resource, 9, super.bufferFactory, 3); + + StepVerifier.create(flux) + .consumeNextWith(stringConsumer("qux")) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + private void verifyReadData(Flux buffers) { + StepVerifier.create(buffers) + .consumeNextWith(stringConsumer("foo")) + .consumeNextWith(stringConsumer("bar")) + .consumeNextWith(stringConsumer("baz")) + .consumeNextWith(stringConsumer("qux")) + .expectComplete() + .verify(Duration.ofSeconds(3)); + } + + @ParameterizedDataBufferAllocatingTest + void readResourcePositionAndTakeUntil(String displayName, DataBufferFactory bufferFactory) throws Exception { + super.bufferFactory = bufferFactory; + + Resource resource = new ClassPathResource("DataBufferUtilsTests.txt", getClass()); + Flux flux = DataBufferUtils.read(resource, 3, super.bufferFactory, 3); + + flux = DataBufferUtils.takeUntilByteCount(flux, 5); + + + StepVerifier.create(flux) + .consumeNextWith(stringConsumer("bar")) + .consumeNextWith(stringConsumer("ba")) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @ParameterizedDataBufferAllocatingTest + void readByteArrayResourcePositionAndTakeUntil(String displayName, DataBufferFactory bufferFactory) throws Exception { + super.bufferFactory = bufferFactory; + + Resource resource = new ByteArrayResource("foobarbazqux" .getBytes()); + Flux flux = DataBufferUtils.read(resource, 3, super.bufferFactory, 3); + + flux = DataBufferUtils.takeUntilByteCount(flux, 5); + + + StepVerifier.create(flux) + .consumeNextWith(stringConsumer("bar")) + .consumeNextWith(stringConsumer("ba")) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @ParameterizedDataBufferAllocatingTest + void writeOutputStream(String displayName, DataBufferFactory bufferFactory) throws Exception { + super.bufferFactory = bufferFactory; + + DataBuffer foo = stringBuffer("foo"); + DataBuffer bar = stringBuffer("bar"); + DataBuffer baz = stringBuffer("baz"); + DataBuffer qux = stringBuffer("qux"); + Flux flux = Flux.just(foo, bar, baz, qux); + + OutputStream os = Files.newOutputStream(tempFile); + + Flux writeResult = DataBufferUtils.write(flux, os); + verifyWrittenData(writeResult); + os.close(); + } + + @ParameterizedDataBufferAllocatingTest + void writeWritableByteChannel(String displayName, DataBufferFactory bufferFactory) throws Exception { + super.bufferFactory = bufferFactory; + + DataBuffer foo = stringBuffer("foo"); + DataBuffer bar = stringBuffer("bar"); + DataBuffer baz = stringBuffer("baz"); + DataBuffer qux = stringBuffer("qux"); + Flux flux = Flux.just(foo, bar, baz, qux); + + WritableByteChannel channel = Files.newByteChannel(tempFile, StandardOpenOption.WRITE); + + Flux writeResult = DataBufferUtils.write(flux, channel); + verifyWrittenData(writeResult); + channel.close(); + } + + @ParameterizedDataBufferAllocatingTest + void writeWritableByteChannelErrorInFlux(String displayName, DataBufferFactory bufferFactory) throws Exception { + super.bufferFactory = bufferFactory; + + DataBuffer foo = stringBuffer("foo"); + DataBuffer bar = stringBuffer("bar"); + Flux flux = Flux.just(foo, bar).concatWith(Flux.error(new RuntimeException())); + + WritableByteChannel channel = Files.newByteChannel(tempFile, StandardOpenOption.WRITE); + + Flux writeResult = DataBufferUtils.write(flux, channel); + StepVerifier.create(writeResult) + .consumeNextWith(stringConsumer("foo")) + .consumeNextWith(stringConsumer("bar")) + .expectError() + .verify(Duration.ofSeconds(5)); + + String result = String.join("", Files.readAllLines(tempFile)); + + assertThat(result).isEqualTo("foobar"); + channel.close(); + } + + @ParameterizedDataBufferAllocatingTest + void writeWritableByteChannelErrorInWrite(String displayName, DataBufferFactory bufferFactory) throws Exception { + super.bufferFactory = bufferFactory; + + DataBuffer foo = stringBuffer("foo"); + DataBuffer bar = stringBuffer("bar"); + Flux flux = Flux.just(foo, bar); + + WritableByteChannel channel = mock(WritableByteChannel.class); + given(channel.write(any())) + .willAnswer(invocation -> { + ByteBuffer buffer = invocation.getArgument(0); + int written = buffer.remaining(); + buffer.position(buffer.limit()); + return written; + }) + .willThrow(new IOException()); + + Flux writeResult = DataBufferUtils.write(flux, channel); + StepVerifier.create(writeResult) + .consumeNextWith(stringConsumer("foo")) + .consumeNextWith(stringConsumer("bar")) + .expectError(IOException.class) + .verify(Duration.ofSeconds(3)); + + channel.close(); + } + + @ParameterizedDataBufferAllocatingTest + void writeWritableByteChannelCancel(String displayName, DataBufferFactory bufferFactory) throws Exception { + super.bufferFactory = bufferFactory; + + DataBuffer foo = stringBuffer("foo"); + DataBuffer bar = stringBuffer("bar"); + Flux flux = Flux.just(foo, bar); + + WritableByteChannel channel = Files.newByteChannel(tempFile, StandardOpenOption.WRITE); + + Flux writeResult = DataBufferUtils.write(flux, channel); + StepVerifier.create(writeResult, 1) + .consumeNextWith(stringConsumer("foo")) + .thenCancel() + .verify(Duration.ofSeconds(5)); + + String result = String.join("", Files.readAllLines(tempFile)); + + assertThat(result).isEqualTo("foo"); + channel.close(); + + flux.subscribe(DataBufferUtils::release); + } + + @ParameterizedDataBufferAllocatingTest + void writeAsynchronousFileChannel(String displayName, DataBufferFactory bufferFactory) throws Exception { + super.bufferFactory = bufferFactory; + + DataBuffer foo = stringBuffer("foo"); + DataBuffer bar = stringBuffer("bar"); + DataBuffer baz = stringBuffer("baz"); + DataBuffer qux = stringBuffer("qux"); + Flux flux = Flux.just(foo, bar, baz, qux); + + AsynchronousFileChannel channel = + AsynchronousFileChannel.open(tempFile, StandardOpenOption.WRITE); + + Flux writeResult = DataBufferUtils.write(flux, channel); + verifyWrittenData(writeResult); + channel.close(); + } + + private void verifyWrittenData(Flux writeResult) throws IOException { + StepVerifier.create(writeResult) + .consumeNextWith(stringConsumer("foo")) + .consumeNextWith(stringConsumer("bar")) + .consumeNextWith(stringConsumer("baz")) + .consumeNextWith(stringConsumer("qux")) + .expectComplete() + .verify(Duration.ofSeconds(3)); + + String result = String.join("", Files.readAllLines(tempFile)); + + assertThat(result).isEqualTo("foobarbazqux"); + } + + @ParameterizedDataBufferAllocatingTest + void writeAsynchronousFileChannelErrorInFlux(String displayName, DataBufferFactory bufferFactory) throws Exception { + super.bufferFactory = bufferFactory; + + DataBuffer foo = stringBuffer("foo"); + DataBuffer bar = stringBuffer("bar"); + Flux flux = + Flux.just(foo, bar).concatWith(Mono.error(new RuntimeException())); + + AsynchronousFileChannel channel = + AsynchronousFileChannel.open(tempFile, StandardOpenOption.WRITE); + + Flux writeResult = DataBufferUtils.write(flux, channel); + StepVerifier.create(writeResult) + .consumeNextWith(stringConsumer("foo")) + .consumeNextWith(stringConsumer("bar")) + .expectError(RuntimeException.class) + .verify(); + + String result = String.join("", Files.readAllLines(tempFile)); + + assertThat(result).isEqualTo("foobar"); + channel.close(); + } + + @ParameterizedDataBufferAllocatingTest + @SuppressWarnings("unchecked") + void writeAsynchronousFileChannelErrorInWrite(String displayName, DataBufferFactory bufferFactory) throws Exception { + super.bufferFactory = bufferFactory; + + DataBuffer foo = stringBuffer("foo"); + DataBuffer bar = stringBuffer("bar"); + Flux flux = Flux.just(foo, bar); + + AsynchronousFileChannel channel = mock(AsynchronousFileChannel.class); + willAnswer(invocation -> { + ByteBuffer buffer = invocation.getArgument(0); + long pos = invocation.getArgument(1); + CompletionHandler completionHandler = invocation.getArgument(3); + + assertThat(pos).isEqualTo(0); + + int written = buffer.remaining(); + buffer.position(buffer.limit()); + completionHandler.completed(written, buffer); + + return null; + }) + .willAnswer(invocation -> { + ByteBuffer buffer = invocation.getArgument(0); + CompletionHandler completionHandler = + invocation.getArgument(3); + completionHandler.failed(new IOException(), buffer); + return null; + }) + .given(channel).write(isA(ByteBuffer.class), anyLong(), isA(ByteBuffer.class), isA(CompletionHandler.class)); + + Flux writeResult = DataBufferUtils.write(flux, channel); + StepVerifier.create(writeResult) + .consumeNextWith(stringConsumer("foo")) + .consumeNextWith(stringConsumer("bar")) + .expectError(IOException.class) + .verify(); + + channel.close(); + } + + @ParameterizedDataBufferAllocatingTest + void writeAsynchronousFileChannelCanceled(String displayName, DataBufferFactory bufferFactory) throws Exception { + super.bufferFactory = bufferFactory; + + DataBuffer foo = stringBuffer("foo"); + DataBuffer bar = stringBuffer("bar"); + Flux flux = Flux.just(foo, bar); + + AsynchronousFileChannel channel = + AsynchronousFileChannel.open(tempFile, StandardOpenOption.WRITE); + + Flux writeResult = DataBufferUtils.write(flux, channel); + StepVerifier.create(writeResult, 1) + .consumeNextWith(stringConsumer("foo")) + .thenCancel() + .verify(); + + String result = String.join("", Files.readAllLines(tempFile)); + + assertThat(result).isEqualTo("foo"); + channel.close(); + + flux.subscribe(DataBufferUtils::release); + } + + @ParameterizedDataBufferAllocatingTest + void writePath(String displayName, DataBufferFactory bufferFactory) throws Exception { + super.bufferFactory = bufferFactory; + + DataBuffer foo = stringBuffer("foo"); + DataBuffer bar = stringBuffer("bar"); + Flux flux = Flux.just(foo, bar); + + Mono result = DataBufferUtils.write(flux, tempFile); + + StepVerifier.create(result) + .verifyComplete(); + + List written = Files.readAllLines(tempFile); + assertThat(written).contains("foobar"); + } + + @ParameterizedDataBufferAllocatingTest + void readAndWriteByteChannel(String displayName, DataBufferFactory bufferFactory) throws Exception { + super.bufferFactory = bufferFactory; + + Path source = Paths.get( + DataBufferUtilsTests.class.getResource("DataBufferUtilsTests.txt").toURI()); + Flux sourceFlux = + DataBufferUtils + .readByteChannel(() -> FileChannel.open(source, StandardOpenOption.READ), + super.bufferFactory, 3); + + Path destination = Files.createTempFile("DataBufferUtilsTests", null); + WritableByteChannel channel = Files.newByteChannel(destination, StandardOpenOption.WRITE); + + DataBufferUtils.write(sourceFlux, channel) + .subscribe(DataBufferUtils.releaseConsumer(), + throwable -> { + throw new AssertionError(throwable.getMessage(), throwable); + }, + () -> { + try { + String expected = String.join("", Files.readAllLines(source)); + String result = String.join("", Files.readAllLines(destination)); + assertThat(result).isEqualTo(expected); + } + catch (IOException e) { + throw new AssertionError(e.getMessage(), e); + } + finally { + DataBufferUtils.closeChannel(channel); + } + }); + } + + @ParameterizedDataBufferAllocatingTest + void readAndWriteAsynchronousFileChannel(String displayName, DataBufferFactory bufferFactory) throws Exception { + super.bufferFactory = bufferFactory; + + Path source = Paths.get( + DataBufferUtilsTests.class.getResource("DataBufferUtilsTests.txt").toURI()); + Flux sourceFlux = DataBufferUtils.readAsynchronousFileChannel( + () -> AsynchronousFileChannel.open(source, StandardOpenOption.READ), + super.bufferFactory, 3); + + Path destination = Files.createTempFile("DataBufferUtilsTests", null); + AsynchronousFileChannel channel = + AsynchronousFileChannel.open(destination, StandardOpenOption.WRITE); + + CountDownLatch latch = new CountDownLatch(1); + + DataBufferUtils.write(sourceFlux, channel) + .subscribe(DataBufferUtils::release, + throwable -> { + throw new AssertionError(throwable.getMessage(), throwable); + }, + () -> { + try { + String expected = String.join("", Files.readAllLines(source)); + String result = String.join("", Files.readAllLines(destination)); + + assertThat(result).isEqualTo(expected); + latch.countDown(); + + } + catch (IOException e) { + throw new AssertionError(e.getMessage(), e); + } + finally { + DataBufferUtils.closeChannel(channel); + } + }); + + latch.await(); + } + + @ParameterizedDataBufferAllocatingTest + void takeUntilByteCount(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + Flux result = DataBufferUtils.takeUntilByteCount( + Flux.just(stringBuffer("foo"), stringBuffer("bar")), 5L); + + StepVerifier.create(result) + .consumeNextWith(stringConsumer("foo")) + .consumeNextWith(stringConsumer("ba")) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @ParameterizedDataBufferAllocatingTest + void takeUntilByteCountCanceled(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + Flux source = Flux.concat( + deferStringBuffer("foo"), + deferStringBuffer("bar") + ); + Flux result = DataBufferUtils.takeUntilByteCount( + source, 5L); + + StepVerifier.create(result) + .consumeNextWith(stringConsumer("foo")) + .thenCancel() + .verify(Duration.ofSeconds(5)); + } + + @ParameterizedDataBufferAllocatingTest + void takeUntilByteCountError(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + Flux source = Flux.concat( + Mono.defer(() -> Mono.just(stringBuffer("foo"))), + Mono.error(new RuntimeException()) + ); + + Flux result = DataBufferUtils.takeUntilByteCount(source, 5L); + + StepVerifier.create(result) + .consumeNextWith(stringConsumer("foo")) + .expectError(RuntimeException.class) + .verify(Duration.ofSeconds(5)); + } + + @ParameterizedDataBufferAllocatingTest + void takeUntilByteCountExact(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + Flux source = Flux.concat( + deferStringBuffer("foo"), + deferStringBuffer("bar"), + deferStringBuffer("baz") + ); + + Flux result = DataBufferUtils.takeUntilByteCount(source, 6L); + + StepVerifier.create(result) + .consumeNextWith(stringConsumer("foo")) + .consumeNextWith(stringConsumer("bar")) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @ParameterizedDataBufferAllocatingTest + void skipUntilByteCount(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + Flux source = Flux.concat( + deferStringBuffer("foo"), + deferStringBuffer("bar"), + deferStringBuffer("baz") + ); + Flux result = DataBufferUtils.skipUntilByteCount(source, 5L); + + StepVerifier.create(result) + .consumeNextWith(stringConsumer("r")) + .consumeNextWith(stringConsumer("baz")) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @ParameterizedDataBufferAllocatingTest + void skipUntilByteCountCancelled(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + Flux source = Flux.concat( + deferStringBuffer("foo"), + deferStringBuffer("bar") + ); + Flux result = DataBufferUtils.skipUntilByteCount(source, 5L); + + StepVerifier.create(result) + .consumeNextWith(stringConsumer("r")) + .thenCancel() + .verify(Duration.ofSeconds(5)); + } + + @ParameterizedDataBufferAllocatingTest + void skipUntilByteCountErrorInFlux(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer foo = stringBuffer("foo"); + Flux flux = + Flux.just(foo).concatWith(Mono.error(new RuntimeException())); + Flux result = DataBufferUtils.skipUntilByteCount(flux, 3L); + + StepVerifier.create(result) + .expectError(RuntimeException.class) + .verify(Duration.ofSeconds(5)); + } + + @ParameterizedDataBufferAllocatingTest + void skipUntilByteCountShouldSkipAll(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer foo = stringBuffer("foo"); + DataBuffer bar = stringBuffer("bar"); + DataBuffer baz = stringBuffer("baz"); + Flux flux = Flux.just(foo, bar, baz); + Flux result = DataBufferUtils.skipUntilByteCount(flux, 9L); + + StepVerifier.create(result) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @ParameterizedDataBufferAllocatingTest + void releaseConsumer(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer foo = stringBuffer("foo"); + DataBuffer bar = stringBuffer("bar"); + DataBuffer baz = stringBuffer("baz"); + Flux flux = Flux.just(foo, bar, baz); + + flux.subscribe(DataBufferUtils.releaseConsumer()); + + assertReleased(foo); + assertReleased(bar); + assertReleased(baz); + } + + private static void assertReleased(DataBuffer dataBuffer) { + if (dataBuffer instanceof NettyDataBuffer) { + ByteBuf byteBuf = ((NettyDataBuffer) dataBuffer).getNativeBuffer(); + assertThat(byteBuf.refCnt()).isEqualTo(0); + } + } + + @ParameterizedDataBufferAllocatingTest + void SPR16070(String displayName, DataBufferFactory bufferFactory) throws Exception { + super.bufferFactory = bufferFactory; + + ReadableByteChannel channel = mock(ReadableByteChannel.class); + given(channel.read(any())) + .willAnswer(putByte('a')) + .willAnswer(putByte('b')) + .willAnswer(putByte('c')) + .willReturn(-1); + + Flux read = + DataBufferUtils.readByteChannel(() -> channel, super.bufferFactory, 1); + + StepVerifier.create(read) + .consumeNextWith(stringConsumer("a")) + .consumeNextWith(stringConsumer("b")) + .consumeNextWith(stringConsumer("c")) + .expectComplete() + .verify(Duration.ofSeconds(5)); + + } + + private Answer putByte(int b) { + return invocation -> { + ByteBuffer buffer = invocation.getArgument(0); + buffer.put((byte) b); + return 1; + }; + } + + @ParameterizedDataBufferAllocatingTest + void join(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer foo = stringBuffer("foo"); + DataBuffer bar = stringBuffer("bar"); + DataBuffer baz = stringBuffer("baz"); + Flux flux = Flux.just(foo, bar, baz); + Mono result = DataBufferUtils.join(flux); + + StepVerifier.create(result) + .consumeNextWith(buf -> { + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("foobarbaz"); + release(buf); + }) + .verifyComplete(); + } + + @ParameterizedDataBufferAllocatingTest + void joinWithLimit(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer foo = stringBuffer("foo"); + DataBuffer bar = stringBuffer("bar"); + DataBuffer baz = stringBuffer("baz"); + Flux flux = Flux.just(foo, bar, baz); + Mono result = DataBufferUtils.join(flux, 8); + + StepVerifier.create(result) + .verifyError(DataBufferLimitException.class); + } + + @Test // gh-26060 + void joinWithLimitDoesNotOverRelease() { + NettyDataBufferFactory bufferFactory = new NettyDataBufferFactory(PooledByteBufAllocator.DEFAULT); + byte[] bytes = "foo-bar-baz".getBytes(StandardCharsets.UTF_8); + + NettyDataBuffer buffer = bufferFactory.allocateBuffer(bytes.length); + buffer.getNativeBuffer().retain(); // should be at 2 now + buffer.write(bytes); + + Mono result = DataBufferUtils.join(Flux.just(buffer), 8); + + StepVerifier.create(result).verifyError(DataBufferLimitException.class); + assertThat(buffer.getNativeBuffer().refCnt()).isEqualTo(1); + buffer.release(); + } + + @ParameterizedDataBufferAllocatingTest + void joinErrors(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer foo = stringBuffer("foo"); + DataBuffer bar = stringBuffer("bar"); + Flux flux = Flux.just(foo, bar).concatWith(Flux.error(new RuntimeException())); + Mono result = DataBufferUtils.join(flux); + + StepVerifier.create(result) + .expectError(RuntimeException.class) + .verify(); + } + + @ParameterizedDataBufferAllocatingTest + void joinCanceled(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + Flux source = Flux.concat( + deferStringBuffer("foo"), + deferStringBuffer("bar"), + deferStringBuffer("baz") + ); + Mono result = DataBufferUtils.join(source); + + StepVerifier.create(result) + .thenCancel() + .verify(); + } + + @ParameterizedDataBufferAllocatingTest + void matcher(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer foo = stringBuffer("foo"); + DataBuffer bar = stringBuffer("bar"); + + byte[] delims = "ooba".getBytes(StandardCharsets.UTF_8); + DataBufferUtils.Matcher matcher = DataBufferUtils.matcher(delims); + int result = matcher.match(foo); + assertThat(result).isEqualTo(-1); + result = matcher.match(bar); + assertThat(result).isEqualTo(1); + + + release(foo, bar); + } + + @ParameterizedDataBufferAllocatingTest + void matcher2(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer foo = stringBuffer("foooobar"); + + byte[] delims = "oo".getBytes(StandardCharsets.UTF_8); + DataBufferUtils.Matcher matcher = DataBufferUtils.matcher(delims); + int endIndex = matcher.match(foo); + assertThat(endIndex).isEqualTo(2); + foo.readPosition(endIndex + 1); + endIndex = matcher.match(foo); + assertThat(endIndex).isEqualTo(4); + foo.readPosition(endIndex + 1); + endIndex = matcher.match(foo); + assertThat(endIndex).isEqualTo(-1); + + release(foo); + } + + @ParameterizedDataBufferAllocatingTest + void matcher3(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer foo = stringBuffer("foooobar"); + + byte[] delims = "oo".getBytes(StandardCharsets.UTF_8); + DataBufferUtils.Matcher matcher = DataBufferUtils.matcher(delims); + int endIndex = matcher.match(foo); + assertThat(endIndex).isEqualTo(2); + foo.readPosition(endIndex + 1); + endIndex = matcher.match(foo); + assertThat(endIndex).isEqualTo(4); + foo.readPosition(endIndex + 1); + endIndex = matcher.match(foo); + assertThat(endIndex).isEqualTo(-1); + + release(foo); + } + + + private static class ZeroDemandSubscriber extends BaseSubscriber { + + @Override + protected void hookOnSubscribe(Subscription subscription) { + // Just subscribe without requesting + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/io/buffer/LeakAwareDataBufferFactoryTests.java b/spring-core/src/test/java/org/springframework/core/io/buffer/LeakAwareDataBufferFactoryTests.java new file mode 100644 index 0000000..64531c0 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/io/buffer/LeakAwareDataBufferFactoryTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.buffer; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.testfixture.io.buffer.LeakAwareDataBufferFactory; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.core.io.buffer.DataBufferUtils.release; + +/** + * @author Arjen Poutsma + */ +class LeakAwareDataBufferFactoryTests { + + private final LeakAwareDataBufferFactory bufferFactory = new LeakAwareDataBufferFactory(); + + + @Test + void leak() { + DataBuffer dataBuffer = this.bufferFactory.allocateBuffer(); + try { + assertThatExceptionOfType(AssertionError.class).isThrownBy( + this.bufferFactory::checkForLeaks); + } + finally { + release(dataBuffer); + } + } + + @Test + void noLeak() { + DataBuffer dataBuffer = this.bufferFactory.allocateBuffer(); + release(dataBuffer); + this.bufferFactory.checkForLeaks(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/io/buffer/LimitedDataBufferListTests.java b/spring-core/src/test/java/org/springframework/core/io/buffer/LimitedDataBufferListTests.java new file mode 100644 index 0000000..971cd12 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/io/buffer/LimitedDataBufferListTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.buffer; + +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Unit tests for {@link LimitedDataBufferList}. + * @author Rossen Stoyanchev + * @since 5.1.11 + */ +public class LimitedDataBufferListTests { + + @Test + void limitEnforced() { + LimitedDataBufferList list = new LimitedDataBufferList(5); + + assertThatThrownBy(() -> list.add(toDataBuffer("123456"))).isInstanceOf(DataBufferLimitException.class); + assertThat(list).isEmpty(); + } + + @Test + void limitIgnored() { + new LimitedDataBufferList(-1).add(toDataBuffer("123456")); + } + + @Test + void clearResetsCount() { + LimitedDataBufferList list = new LimitedDataBufferList(5); + list.add(toDataBuffer("12345")); + list.clear(); + list.add(toDataBuffer("12345")); + } + + + private static DataBuffer toDataBuffer(String value) { + byte[] bytes = value.getBytes(StandardCharsets.UTF_8); + return DefaultDataBufferFactory.sharedInstance.wrap(bytes); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/io/buffer/PooledDataBufferTests.java b/spring-core/src/test/java/org/springframework/core/io/buffer/PooledDataBufferTests.java new file mode 100644 index 0000000..71a1fed --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/io/buffer/PooledDataBufferTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.buffer; + +import io.netty.buffer.PooledByteBufAllocator; +import io.netty.buffer.UnpooledByteBufAllocator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * @author Arjen Poutsma + * @author Sam Brannen + */ +class PooledDataBufferTests { + + @Nested + class UnpooledByteBufAllocatorWithPreferDirectTrueTests implements PooledDataBufferTestingTrait { + + @Override + public DataBufferFactory createDataBufferFactory() { + return new NettyDataBufferFactory(new UnpooledByteBufAllocator(true)); + } + } + + @Nested + class UnpooledByteBufAllocatorWithPreferDirectFalseTests implements PooledDataBufferTestingTrait { + + @Override + public DataBufferFactory createDataBufferFactory() { + return new NettyDataBufferFactory(new UnpooledByteBufAllocator(true)); + } + } + + @Nested + class PooledByteBufAllocatorWithPreferDirectTrueTests implements PooledDataBufferTestingTrait { + + @Override + public DataBufferFactory createDataBufferFactory() { + return new NettyDataBufferFactory(new PooledByteBufAllocator(true)); + } + } + + @Nested + class PooledByteBufAllocatorWithPreferDirectFalseTests implements PooledDataBufferTestingTrait { + + @Override + public DataBufferFactory createDataBufferFactory() { + return new NettyDataBufferFactory(new PooledByteBufAllocator(true)); + } + } + + interface PooledDataBufferTestingTrait { + + DataBufferFactory createDataBufferFactory(); + + default PooledDataBuffer createDataBuffer(int capacity) { + return (PooledDataBuffer) createDataBufferFactory().allocateBuffer(capacity); + } + + @Test + default void retainAndRelease() { + PooledDataBuffer buffer = createDataBuffer(1); + buffer.write((byte) 'a'); + + buffer.retain(); + assertThat(buffer.release()).isFalse(); + assertThat(buffer.release()).isTrue(); + } + + @Test + default void tooManyReleases() { + PooledDataBuffer buffer = createDataBuffer(1); + buffer.write((byte) 'a'); + + buffer.release(); + assertThatIllegalStateException().isThrownBy(buffer::release); + } + + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/io/buffer/support/DataBufferTestUtilsTests.java b/spring-core/src/test/java/org/springframework/core/io/buffer/support/DataBufferTestUtilsTests.java new file mode 100644 index 0000000..96f720f --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/io/buffer/support/DataBufferTestUtilsTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.buffer.support; + +import java.nio.charset.StandardCharsets; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.testfixture.io.buffer.AbstractDataBufferAllocatingTests; +import org.springframework.core.testfixture.io.buffer.DataBufferTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + * @author Sam Brannen + */ +class DataBufferTestUtilsTests extends AbstractDataBufferAllocatingTests { + + @ParameterizedDataBufferAllocatingTest + void dumpBytes(String displayName, DataBufferFactory bufferFactory) { + this.bufferFactory = bufferFactory; + + DataBuffer buffer = this.bufferFactory.allocateBuffer(4); + byte[] source = {'a', 'b', 'c', 'd'}; + buffer.write(source); + + byte[] result = DataBufferTestUtils.dumpBytes(buffer); + + assertThat(result).isEqualTo(source); + + release(buffer); + } + + @ParameterizedDataBufferAllocatingTest + void dumpString(String displayName, DataBufferFactory bufferFactory) { + this.bufferFactory = bufferFactory; + + DataBuffer buffer = this.bufferFactory.allocateBuffer(4); + String source = "abcd"; + buffer.write(source.getBytes(StandardCharsets.UTF_8)); + String result = buffer.toString(StandardCharsets.UTF_8); + release(buffer); + + assertThat(result).isEqualTo(source); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/io/support/DummyFactory.java b/spring-core/src/test/java/org/springframework/core/io/support/DummyFactory.java new file mode 100644 index 0000000..794e02e --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/io/support/DummyFactory.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.support; + +/** + * Used by {@link SpringFactoriesLoaderTests} + * + * @author Arjen Poutsma + */ +public interface DummyFactory { + + String getString(); + +} diff --git a/spring-core/src/test/java/org/springframework/core/io/support/DummyPackagePrivateFactory.java b/spring-core/src/test/java/org/springframework/core/io/support/DummyPackagePrivateFactory.java new file mode 100644 index 0000000..3c26547 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/io/support/DummyPackagePrivateFactory.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.support; + +/** + * Used by {@link SpringFactoriesLoaderTests} + + * @author Phillip Webb + */ +class DummyPackagePrivateFactory { + +} diff --git a/spring-core/src/test/java/org/springframework/core/io/support/EncodedResourceTests.java b/spring-core/src/test/java/org/springframework/core/io/support/EncodedResourceTests.java new file mode 100644 index 0000000..76eae72 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/io/support/EncodedResourceTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.support; + +import java.nio.charset.Charset; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.DescriptiveResource; +import org.springframework.core.io.Resource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link EncodedResource}. + * + * @author Sam Brannen + * @since 3.2.14 + */ +class EncodedResourceTests { + + private static final String UTF8 = "UTF-8"; + private static final String UTF16 = "UTF-16"; + private static final Charset UTF8_CS = Charset.forName(UTF8); + private static final Charset UTF16_CS = Charset.forName(UTF16); + + private final Resource resource = new DescriptiveResource("test"); + + + @Test + void equalsWithNullOtherObject() { + assertThat(new EncodedResource(resource).equals(null)).isFalse(); + } + + @Test + void equalsWithSameEncoding() { + EncodedResource er1 = new EncodedResource(resource, UTF8); + EncodedResource er2 = new EncodedResource(resource, UTF8); + assertThat(er2).isEqualTo(er1); + } + + @Test + void equalsWithDifferentEncoding() { + EncodedResource er1 = new EncodedResource(resource, UTF8); + EncodedResource er2 = new EncodedResource(resource, UTF16); + assertThat(er2).isNotEqualTo(er1); + } + + @Test + void equalsWithSameCharset() { + EncodedResource er1 = new EncodedResource(resource, UTF8_CS); + EncodedResource er2 = new EncodedResource(resource, UTF8_CS); + assertThat(er2).isEqualTo(er1); + } + + @Test + void equalsWithDifferentCharset() { + EncodedResource er1 = new EncodedResource(resource, UTF8_CS); + EncodedResource er2 = new EncodedResource(resource, UTF16_CS); + assertThat(er2).isNotEqualTo(er1); + } + + @Test + void equalsWithEncodingAndCharset() { + EncodedResource er1 = new EncodedResource(resource, UTF8); + EncodedResource er2 = new EncodedResource(resource, UTF8_CS); + assertThat(er2).isNotEqualTo(er1); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/io/support/MyDummyFactory1.java b/spring-core/src/test/java/org/springframework/core/io/support/MyDummyFactory1.java new file mode 100644 index 0000000..807be5e --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/io/support/MyDummyFactory1.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.support; + +import org.springframework.core.annotation.Order; + +/** + * Used by {@link SpringFactoriesLoaderTests} + * + * @author Arjen Poutsma + */ +@Order(1) +public class MyDummyFactory1 implements DummyFactory { + + @Override + public String getString() { + return "Foo"; + } +} diff --git a/spring-core/src/test/java/org/springframework/core/io/support/MyDummyFactory2.java b/spring-core/src/test/java/org/springframework/core/io/support/MyDummyFactory2.java new file mode 100644 index 0000000..3746271 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/io/support/MyDummyFactory2.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.support; + +import org.springframework.core.annotation.Order; + +/** + * Used by {@link SpringFactoriesLoaderTests} + * + * @author Arjen Poutsma + */ +@Order(2) +public class MyDummyFactory2 implements DummyFactory { + + @Override + public String getString() { + return "Bar"; + } +} diff --git a/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java b/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java new file mode 100644 index 0000000..41db4dc --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java @@ -0,0 +1,167 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.support; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.Resource; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * If this test case fails, uncomment diagnostics in the + * {@link #assertProtocolAndFilenames} method. + * + * @author Oliver Hutchison + * @author Juergen Hoeller + * @author Chris Beams + * @author Sam Brannen + * @since 17.11.2004 + */ +class PathMatchingResourcePatternResolverTests { + + private static final String[] CLASSES_IN_CORE_IO_SUPPORT = + new String[] {"EncodedResource.class", "LocalizedResourceHelper.class", + "PathMatchingResourcePatternResolver.class", "PropertiesLoaderSupport.class", + "PropertiesLoaderUtils.class", "ResourceArrayPropertyEditor.class", + "ResourcePatternResolver.class", "ResourcePatternUtils.class"}; + + private static final String[] TEST_CLASSES_IN_CORE_IO_SUPPORT = + new String[] {"PathMatchingResourcePatternResolverTests.class"}; + + private static final String[] CLASSES_IN_REACTOR_UTIL_ANNOTATIONS = + new String[] {"NonNull.class", "NonNullApi.class", "Nullable.class"}; + + private PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + + + @Test + void invalidPrefixWithPatternElementInIt() throws IOException { + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(() -> + resolver.getResources("xx**:**/*.xy")); + } + + @Test + void singleResourceOnFileSystem() throws IOException { + Resource[] resources = + resolver.getResources("org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.class"); + assertThat(resources.length).isEqualTo(1); + assertProtocolAndFilenames(resources, "file", "PathMatchingResourcePatternResolverTests.class"); + } + + @Test + void singleResourceInJar() throws IOException { + Resource[] resources = resolver.getResources("org/reactivestreams/Publisher.class"); + assertThat(resources.length).isEqualTo(1); + assertProtocolAndFilenames(resources, "jar", "Publisher.class"); + } + + @Disabled + @Test + void classpathStarWithPatternOnFileSystem() throws IOException { + Resource[] resources = resolver.getResources("classpath*:org/springframework/core/io/sup*/*.class"); + // Have to exclude Clover-generated class files here, + // as we might be running as part of a Clover test run. + List noCloverResources = new ArrayList<>(); + for (Resource resource : resources) { + if (!resource.getFilename().contains("$__CLOVER_")) { + noCloverResources.add(resource); + } + } + resources = noCloverResources.toArray(new Resource[0]); + assertProtocolAndFilenames(resources, "file", + StringUtils.concatenateStringArrays(CLASSES_IN_CORE_IO_SUPPORT, TEST_CLASSES_IN_CORE_IO_SUPPORT)); + } + + @Test + void getResourcesOnFileSystemContainingHashtagsInTheirFileNames() throws IOException { + Resource[] resources = resolver.getResources("classpath*:org/springframework/core/io/**/resource#test*.txt"); + assertThat(resources).extracting(Resource::getFile).extracting(File::getName) + .containsExactlyInAnyOrder("resource#test1.txt", "resource#test2.txt"); + } + + @Test + void classpathWithPatternInJar() throws IOException { + Resource[] resources = resolver.getResources("classpath:reactor/util/annotation/*.class"); + assertProtocolAndFilenames(resources, "jar", CLASSES_IN_REACTOR_UTIL_ANNOTATIONS); + } + + @Test + void classpathStarWithPatternInJar() throws IOException { + Resource[] resources = resolver.getResources("classpath*:reactor/util/annotation/*.class"); + assertProtocolAndFilenames(resources, "jar", CLASSES_IN_REACTOR_UTIL_ANNOTATIONS); + } + + @Test + void rootPatternRetrievalInJarFiles() throws IOException { + Resource[] resources = resolver.getResources("classpath*:*.dtd"); + boolean found = false; + for (Resource resource : resources) { + if (resource.getFilename().equals("aspectj_1_5_0.dtd")) { + found = true; + break; + } + } + assertThat(found).as("Could not find aspectj_1_5_0.dtd in the root of the aspectjweaver jar").isTrue(); + } + + + private void assertProtocolAndFilenames(Resource[] resources, String protocol, String... filenames) + throws IOException { + + // Uncomment the following if you encounter problems with matching against the file system + // It shows file locations. +// String[] actualNames = new String[resources.length]; +// for (int i = 0; i < resources.length; i++) { +// actualNames[i] = resources[i].getFilename(); +// } +// List sortedActualNames = new LinkedList(Arrays.asList(actualNames)); +// List expectedNames = new LinkedList(Arrays.asList(fileNames)); +// Collections.sort(sortedActualNames); +// Collections.sort(expectedNames); +// +// System.out.println("-----------"); +// System.out.println("Expected: " + StringUtils.collectionToCommaDelimitedString(expectedNames)); +// System.out.println("Actual: " + StringUtils.collectionToCommaDelimitedString(sortedActualNames)); +// for (int i = 0; i < resources.length; i++) { +// System.out.println(resources[i]); +// } + + assertThat(resources.length).as("Correct number of files found").isEqualTo(filenames.length); + for (Resource resource : resources) { + String actualProtocol = resource.getURL().getProtocol(); + assertThat(actualProtocol).isEqualTo(protocol); + assertFilenameIn(resource, filenames); + } + } + + private void assertFilenameIn(Resource resource, String... filenames) { + String filename = resource.getFilename(); + assertThat(Arrays.stream(filenames).anyMatch(filename::endsWith)).as(resource + " does not have a filename that matches any of the specified names").isTrue(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/io/support/ResourceArrayPropertyEditorTests.java b/spring-core/src/test/java/org/springframework/core/io/support/ResourceArrayPropertyEditorTests.java new file mode 100644 index 0000000..4bb19cd --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/io/support/ResourceArrayPropertyEditorTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.support; + +import java.beans.PropertyEditor; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.Resource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Dave Syer + * @author Juergen Hoeller + */ +class ResourceArrayPropertyEditorTests { + + @Test + void vanillaResource() { + PropertyEditor editor = new ResourceArrayPropertyEditor(); + editor.setAsText("classpath:org/springframework/core/io/support/ResourceArrayPropertyEditor.class"); + Resource[] resources = (Resource[]) editor.getValue(); + assertThat(resources).isNotNull(); + assertThat(resources[0].exists()).isTrue(); + } + + @Test + void patternResource() { + // N.B. this will sometimes fail if you use classpath: instead of classpath*:. + // The result depends on the classpath - if test-classes are segregated from classes + // and they come first on the classpath (like in Maven) then it breaks, if classes + // comes first (like in Spring Build) then it is OK. + PropertyEditor editor = new ResourceArrayPropertyEditor(); + editor.setAsText("classpath*:org/springframework/core/io/support/Resource*Editor.class"); + Resource[] resources = (Resource[]) editor.getValue(); + assertThat(resources).isNotNull(); + assertThat(resources[0].exists()).isTrue(); + } + + @Test + void systemPropertyReplacement() { + PropertyEditor editor = new ResourceArrayPropertyEditor(); + System.setProperty("test.prop", "foo"); + try { + editor.setAsText("${test.prop}"); + Resource[] resources = (Resource[]) editor.getValue(); + assertThat(resources[0].getFilename()).isEqualTo("foo"); + } + finally { + System.getProperties().remove("test.prop"); + } + } + + @Test + void strictSystemPropertyReplacementWithUnresolvablePlaceholder() { + PropertyEditor editor = new ResourceArrayPropertyEditor( + new PathMatchingResourcePatternResolver(), new StandardEnvironment(), + false); + System.setProperty("test.prop", "foo"); + try { + assertThatIllegalArgumentException().isThrownBy(() -> + editor.setAsText("${test.prop}-${bar}")); + } + finally { + System.getProperties().remove("test.prop"); + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/io/support/ResourcePropertySourceTests.java b/spring-core/src/test/java/org/springframework/core/io/support/ResourcePropertySourceTests.java new file mode 100644 index 0000000..df4a13c --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/io/support/ResourcePropertySourceTests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.support; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.env.PropertySource; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link ResourcePropertySource}. + * + * @author Chris Beams + * @author Sam Brannen + * @since 3.1 + */ +class ResourcePropertySourceTests { + + private static final String PROPERTIES_PATH = "org/springframework/core/io/example.properties"; + private static final String PROPERTIES_LOCATION = "classpath:" + PROPERTIES_PATH; + private static final String PROPERTIES_RESOURCE_DESCRIPTION = "class path resource [" + PROPERTIES_PATH + "]"; + + private static final String XML_PROPERTIES_PATH = "org/springframework/core/io/example.xml"; + private static final String XML_PROPERTIES_LOCATION = "classpath:" + XML_PROPERTIES_PATH; + private static final String XML_PROPERTIES_RESOURCE_DESCRIPTION = "class path resource [" + XML_PROPERTIES_PATH + "]"; + + @Test + void withLocationAndGeneratedName() throws IOException { + PropertySource ps = new ResourcePropertySource(PROPERTIES_LOCATION); + assertThat(ps.getProperty("foo")).isEqualTo("bar"); + assertThat(ps.getName()).isEqualTo(PROPERTIES_RESOURCE_DESCRIPTION); + } + + @Test + void xmlWithLocationAndGeneratedName() throws IOException { + PropertySource ps = new ResourcePropertySource(XML_PROPERTIES_LOCATION); + assertThat(ps.getProperty("foo")).isEqualTo("bar"); + assertThat(ps.getName()).isEqualTo(XML_PROPERTIES_RESOURCE_DESCRIPTION); + } + + @Test + void withLocationAndExplicitName() throws IOException { + PropertySource ps = new ResourcePropertySource("ps1", PROPERTIES_LOCATION); + assertThat(ps.getProperty("foo")).isEqualTo("bar"); + assertThat(ps.getName()).isEqualTo("ps1"); + } + + @Test + void withLocationAndExplicitNameAndExplicitClassLoader() throws IOException { + PropertySource ps = new ResourcePropertySource("ps1", PROPERTIES_LOCATION, getClass().getClassLoader()); + assertThat(ps.getProperty("foo")).isEqualTo("bar"); + assertThat(ps.getName()).isEqualTo("ps1"); + } + + @Test + void withLocationAndGeneratedNameAndExplicitClassLoader() throws IOException { + PropertySource ps = new ResourcePropertySource(PROPERTIES_LOCATION, getClass().getClassLoader()); + assertThat(ps.getProperty("foo")).isEqualTo("bar"); + assertThat(ps.getName()).isEqualTo(PROPERTIES_RESOURCE_DESCRIPTION); + } + + @Test + void withResourceAndGeneratedName() throws IOException { + PropertySource ps = new ResourcePropertySource(new ClassPathResource(PROPERTIES_PATH)); + assertThat(ps.getProperty("foo")).isEqualTo("bar"); + assertThat(ps.getName()).isEqualTo(PROPERTIES_RESOURCE_DESCRIPTION); + } + + @Test + void withResourceAndExplicitName() throws IOException { + PropertySource ps = new ResourcePropertySource("ps1", new ClassPathResource(PROPERTIES_PATH)); + assertThat(ps.getProperty("foo")).isEqualTo("bar"); + assertThat(ps.getName()).isEqualTo("ps1"); + } + + @Test + void withResourceHavingNoDescription() throws IOException { + PropertySource ps = new ResourcePropertySource(new ByteArrayResource("foo=bar".getBytes(), "")); + assertThat(ps.getProperty("foo")).isEqualTo("bar"); + assertThat(ps.getName()).isEqualTo("Byte array resource []"); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/io/support/ResourceRegionTests.java b/spring-core/src/test/java/org/springframework/core/io/support/ResourceRegionTests.java new file mode 100644 index 0000000..d7d00be --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/io/support/ResourceRegionTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.support; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.Resource; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for the {@link ResourceRegion} class. + * + * @author Brian Clozel + */ +class ResourceRegionTests { + + @Test + void shouldThrowExceptionWithNullResource() { + assertThatIllegalArgumentException().isThrownBy(() -> + new ResourceRegion(null, 0, 1)); + } + + @Test + void shouldThrowExceptionForNegativePosition() { + assertThatIllegalArgumentException().isThrownBy(() -> + new ResourceRegion(mock(Resource.class), -1, 1)); + } + + @Test + void shouldThrowExceptionForNegativeCount() { + assertThatIllegalArgumentException().isThrownBy(() -> + new ResourceRegion(mock(Resource.class), 0, -1)); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/io/support/SpringFactoriesLoaderTests.java b/spring-core/src/test/java/org/springframework/core/io/support/SpringFactoriesLoaderTests.java new file mode 100644 index 0000000..913943d --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/io/support/SpringFactoriesLoaderTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.io.support; + +import java.lang.reflect.Modifier; +import java.util.List; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link SpringFactoriesLoader}. + * + * @author Arjen Poutsma + * @author Phillip Webb + * @author Sam Brannen + */ +class SpringFactoriesLoaderTests { + + @BeforeAll + static void clearCache() { + SpringFactoriesLoader.cache.clear(); + assertThat(SpringFactoriesLoader.cache).isEmpty(); + } + + @AfterAll + static void checkCache() { + assertThat(SpringFactoriesLoader.cache).hasSize(1); + } + + @Test + void loadFactoryNames() { + List factoryNames = SpringFactoriesLoader.loadFactoryNames(DummyFactory.class, null); + assertThat(factoryNames).containsExactlyInAnyOrder(MyDummyFactory1.class.getName(), MyDummyFactory2.class.getName()); + } + + @Test + void loadFactoriesWithNoRegisteredImplementations() { + List factories = SpringFactoriesLoader.loadFactories(Integer.class, null); + assertThat(factories).isEmpty(); + } + + @Test + void loadFactoriesInCorrectOrderWithDuplicateRegistrationsPresent() { + List factories = SpringFactoriesLoader.loadFactories(DummyFactory.class, null); + assertThat(factories).hasSize(2); + assertThat(factories.get(0)).isInstanceOf(MyDummyFactory1.class); + assertThat(factories.get(1)).isInstanceOf(MyDummyFactory2.class); + } + + @Test + void loadPackagePrivateFactory() { + List factories = + SpringFactoriesLoader.loadFactories(DummyPackagePrivateFactory.class, null); + assertThat(factories).hasSize(1); + assertThat(Modifier.isPublic(factories.get(0).getClass().getModifiers())).isFalse(); + } + + @Test + void attemptToLoadFactoryOfIncompatibleType() { + assertThatIllegalArgumentException() + .isThrownBy(() -> SpringFactoriesLoader.loadFactories(String.class, null)) + .withMessageContaining("Unable to instantiate factory class " + + "[org.springframework.core.io.support.MyDummyFactory1] for factory type [java.lang.String]"); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/log/LogSupportTests.java b/spring-core/src/test/java/org/springframework/core/log/LogSupportTests.java new file mode 100644 index 0000000..f438754 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/log/LogSupportTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.log; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @since 5.2 + */ +class LogSupportTests { + + @Test + void logMessageWithSupplier() { + LogMessage msg = LogMessage.of(() -> new StringBuilder("a").append(" b")); + assertThat(msg.toString()).isEqualTo("a b"); + assertThat(msg.toString()).isSameAs(msg.toString()); + } + + @Test + void logMessageWithFormat1() { + LogMessage msg = LogMessage.format("a %s", "b"); + assertThat(msg.toString()).isEqualTo("a b"); + assertThat(msg.toString()).isSameAs(msg.toString()); + } + + @Test + void logMessageWithFormat2() { + LogMessage msg = LogMessage.format("a %s %s", "b", "c"); + assertThat(msg.toString()).isEqualTo("a b c"); + assertThat(msg.toString()).isSameAs(msg.toString()); + } + + @Test + void logMessageWithFormat3() { + LogMessage msg = LogMessage.format("a %s %s %s", "b", "c", "d"); + assertThat(msg.toString()).isEqualTo("a b c d"); + assertThat(msg.toString()).isSameAs(msg.toString()); + } + + @Test + void logMessageWithFormat4() { + LogMessage msg = LogMessage.format("a %s %s %s %s", "b", "c", "d", "e"); + assertThat(msg.toString()).isEqualTo("a b c d e"); + assertThat(msg.toString()).isSameAs(msg.toString()); + } + + @Test + void logMessageWithFormatX() { + LogMessage msg = LogMessage.format("a %s %s %s %s %s", "b", "c", "d", "e", "f"); + assertThat(msg.toString()).isEqualTo("a b c d e f"); + assertThat(msg.toString()).isSameAs(msg.toString()); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/serializer/SerializationConverterTests.java b/spring-core/src/test/java/org/springframework/core/serializer/SerializationConverterTests.java new file mode 100644 index 0000000..c383732 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/serializer/SerializationConverterTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.serializer; + +import java.io.NotSerializableException; +import java.io.Serializable; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.serializer.support.DeserializingConverter; +import org.springframework.core.serializer.support.SerializationFailedException; +import org.springframework.core.serializer.support.SerializingConverter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Gary Russell + * @author Mark Fisher + * @since 3.0.5 + */ +class SerializationConverterTests { + + @Test + void serializeAndDeserializeString() { + SerializingConverter toBytes = new SerializingConverter(); + byte[] bytes = toBytes.convert("Testing"); + DeserializingConverter fromBytes = new DeserializingConverter(); + assertThat(fromBytes.convert(bytes)).isEqualTo("Testing"); + } + + @Test + void nonSerializableObject() { + SerializingConverter toBytes = new SerializingConverter(); + assertThatExceptionOfType(SerializationFailedException.class).isThrownBy(() -> + toBytes.convert(new Object())) + .withCauseInstanceOf(IllegalArgumentException.class); + } + + @Test + void nonSerializableField() { + SerializingConverter toBytes = new SerializingConverter(); + assertThatExceptionOfType(SerializationFailedException.class).isThrownBy(() -> + toBytes.convert(new UnSerializable())) + .withCauseInstanceOf(NotSerializableException.class); + } + + @Test + void deserializationFailure() { + DeserializingConverter fromBytes = new DeserializingConverter(); + assertThatExceptionOfType(SerializationFailedException.class).isThrownBy(() -> + fromBytes.convert("Junk".getBytes())); + } + + + class UnSerializable implements Serializable { + + private static final long serialVersionUID = 1L; + + @SuppressWarnings("unused") + private Object object; + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/style/DefaultValueStylerTests.java b/spring-core/src/test/java/org/springframework/core/style/DefaultValueStylerTests.java new file mode 100644 index 0000000..c2450c4 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/style/DefaultValueStylerTests.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.style; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link DefaultValueStyler}. + * + * @since 5.2 + */ +class DefaultValueStylerTests { + + private final DefaultValueStyler styler = new DefaultValueStyler(); + + + @Test + void styleBasics() throws NoSuchMethodException { + assertThat(styler.style(null)).isEqualTo("[null]"); + assertThat(styler.style("str")).isEqualTo("'str'"); + assertThat(styler.style(String.class)).isEqualTo("String"); + assertThat(styler.style(String.class.getMethod("toString"))).isEqualTo("toString@String"); + } + + @Test + void stylePlainObject() { + Object obj = new Object(); + + assertThat(styler.style(obj)).isEqualTo(String.valueOf(obj)); + } + + @Test + void styleMaps() { + Map map = Collections.emptyMap(); + assertThat(styler.style(map)).isEqualTo("map[[empty]]"); + + map = Collections.singletonMap("key", 1); + assertThat(styler.style(map)).isEqualTo("map['key' -> 1]"); + + map = new HashMap<>(); + map.put("key1", 1); + map.put("key2", 2); + assertThat(styler.style(map)).isEqualTo("map['key1' -> 1, 'key2' -> 2]"); + } + + @Test + void styleMapEntries() { + Map map = new LinkedHashMap<>(); + map.put("key1", 1); + map.put("key2", 2); + + Iterator> entries = map.entrySet().iterator(); + + assertThat(styler.style(entries.next())).isEqualTo("'key1' -> 1"); + assertThat(styler.style(entries.next())).isEqualTo("'key2' -> 2"); + } + + @Test + void styleCollections() { + List list = Collections.emptyList(); + assertThat(styler.style(list)).isEqualTo("list[[empty]]"); + + list = Collections.singletonList(1); + assertThat(styler.style(list)).isEqualTo("list[1]"); + + list = Arrays.asList(1, 2); + assertThat(styler.style(list)).isEqualTo("list[1, 2]"); + } + + @Test + void stylePrimitiveArrays() { + int[] array = new int[0]; + assertThat(styler.style(array)).isEqualTo("array[[empty]]"); + + array = new int[] { 1 }; + assertThat(styler.style(array)).isEqualTo("array[1]"); + + array = new int[] { 1, 2 }; + assertThat(styler.style(array)).isEqualTo("array[1, 2]"); + } + + @Test + void styleObjectArrays() { + String[] array = new String[0]; + assertThat(styler.style(array)).isEqualTo("array[[empty]]"); + + array = new String[] { "str1" }; + assertThat(styler.style(array)).isEqualTo("array['str1']"); + + array = new String[] { "str1", "str2" }; + assertThat(styler.style(array)).isEqualTo("array['str1', 'str2']"); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/style/ToStringCreatorTests.java b/spring-core/src/test/java/org/springframework/core/style/ToStringCreatorTests.java new file mode 100644 index 0000000..ef7d971 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/style/ToStringCreatorTests.java @@ -0,0 +1,137 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.style; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.util.ObjectUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Keith Donald + */ +class ToStringCreatorTests { + + private SomeObject s1, s2, s3; + + + @BeforeEach + void setUp() throws Exception { + s1 = new SomeObject() { + @Override + public String toString() { + return "A"; + } + }; + s2 = new SomeObject() { + @Override + public String toString() { + return "B"; + } + }; + s3 = new SomeObject() { + @Override + public String toString() { + return "C"; + } + }; + } + + @Test + void defaultStyleMap() { + final Map map = getMap(); + Object stringy = new Object() { + @Override + public String toString() { + return new ToStringCreator(this).append("familyFavoriteSport", map).toString(); + } + }; + assertThat(stringy.toString()).isEqualTo(("[ToStringCreatorTests.4@" + ObjectUtils.getIdentityHexString(stringy) + + " familyFavoriteSport = map['Keri' -> 'Softball', 'Scot' -> 'Fishing', 'Keith' -> 'Flag Football']]")); + } + + private Map getMap() { + Map map = new LinkedHashMap<>(); + map.put("Keri", "Softball"); + map.put("Scot", "Fishing"); + map.put("Keith", "Flag Football"); + return map; + } + + @Test + void defaultStyleArray() { + SomeObject[] array = new SomeObject[] {s1, s2, s3}; + String str = new ToStringCreator(array).toString(); + assertThat(str).isEqualTo(("[@" + ObjectUtils.getIdentityHexString(array) + + " array[A, B, C]]")); + } + + @Test + void primitiveArrays() { + int[] integers = new int[] {0, 1, 2, 3, 4}; + String str = new ToStringCreator(integers).toString(); + assertThat(str).isEqualTo(("[@" + ObjectUtils.getIdentityHexString(integers) + " array[0, 1, 2, 3, 4]]")); + } + + @Test + void appendList() { + List list = new ArrayList<>(); + list.add(s1); + list.add(s2); + list.add(s3); + String str = new ToStringCreator(this).append("myLetters", list).toString(); + assertThat(str).isEqualTo(("[ToStringCreatorTests@" + ObjectUtils.getIdentityHexString(this) + " myLetters = list[A, B, C]]")); + } + + @Test + void appendSet() { + Set set = new LinkedHashSet<>(); + set.add(s1); + set.add(s2); + set.add(s3); + String str = new ToStringCreator(this).append("myLetters", set).toString(); + assertThat(str).isEqualTo(("[ToStringCreatorTests@" + ObjectUtils.getIdentityHexString(this) + " myLetters = set[A, B, C]]")); + } + + @Test + void appendClass() { + String str = new ToStringCreator(this).append("myClass", this.getClass()).toString(); + assertThat(str).isEqualTo(("[ToStringCreatorTests@" + ObjectUtils.getIdentityHexString(this) + + " myClass = ToStringCreatorTests]")); + } + + @Test + void appendMethod() throws Exception { + String str = new ToStringCreator(this).append("myMethod", this.getClass().getDeclaredMethod("appendMethod")).toString(); + assertThat(str).isEqualTo(("[ToStringCreatorTests@" + ObjectUtils.getIdentityHexString(this) + + " myMethod = appendMethod@ToStringCreatorTests]")); + } + + + public static class SomeObject { + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/task/SimpleAsyncTaskExecutorTests.java b/spring-core/src/test/java/org/springframework/core/task/SimpleAsyncTaskExecutorTests.java new file mode 100644 index 0000000..c78f9a9 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/task/SimpleAsyncTaskExecutorTests.java @@ -0,0 +1,144 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.task; + +import java.util.concurrent.ThreadFactory; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.ConcurrencyThrottleSupport; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * @author Rick Evans + * @author Juergen Hoeller + * @author Sam Brannen + */ +class SimpleAsyncTaskExecutorTests { + + @Test + void cannotExecuteWhenConcurrencyIsSwitchedOff() throws Exception { + SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(); + executor.setConcurrencyLimit(ConcurrencyThrottleSupport.NO_CONCURRENCY); + assertThat(executor.isThrottleActive()).isTrue(); + assertThatIllegalStateException().isThrownBy(() -> + executor.execute(new NoOpRunnable())); + } + + @Test + void throttleIsNotActiveByDefault() throws Exception { + SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(); + assertThat(executor.isThrottleActive()).as("Concurrency throttle must not default to being active (on)").isFalse(); + } + + @Test + void threadNameGetsSetCorrectly() throws Exception { + final String customPrefix = "chankPop#"; + final Object monitor = new Object(); + SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(customPrefix); + ThreadNameHarvester task = new ThreadNameHarvester(monitor); + executeAndWait(executor, task, monitor); + assertThat(task.getThreadName()).startsWith(customPrefix); + } + + @Test + void threadFactoryOverridesDefaults() throws Exception { + final Object monitor = new Object(); + SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(r, "test"); + } + }); + ThreadNameHarvester task = new ThreadNameHarvester(monitor); + executeAndWait(executor, task, monitor); + assertThat(task.getThreadName()).isEqualTo("test"); + } + + @Test + void throwsExceptionWhenSuppliedWithNullRunnable() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new SimpleAsyncTaskExecutor().execute(null)); + } + + private void executeAndWait(SimpleAsyncTaskExecutor executor, Runnable task, Object monitor) { + synchronized (monitor) { + executor.execute(task); + try { + monitor.wait(); + } + catch (InterruptedException ignored) { + } + } + } + + + private static final class NoOpRunnable implements Runnable { + + @Override + public void run() { + // no-op + } + } + + + private static abstract class AbstractNotifyingRunnable implements Runnable { + + private final Object monitor; + + protected AbstractNotifyingRunnable(Object monitor) { + this.monitor = monitor; + } + + @Override + public final void run() { + synchronized (this.monitor) { + try { + doRun(); + } + finally { + this.monitor.notifyAll(); + } + } + } + + protected abstract void doRun(); + } + + + private static final class ThreadNameHarvester extends AbstractNotifyingRunnable { + + private String threadName; + + protected ThreadNameHarvester(Object monitor) { + super(monitor); + } + + public String getThreadName() { + return this.threadName; + } + + @Override + protected void doRun() { + this.threadName = Thread.currentThread().getName(); + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/testfixture/TestGroupParsingTests.java b/spring-core/src/test/java/org/springframework/core/testfixture/TestGroupParsingTests.java new file mode 100644 index 0000000..a7b3562 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/testfixture/TestGroupParsingTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.testfixture; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link TestGroup} parsing. + * + * @author Phillip Webb + * @author Sam Brannen + */ +class TestGroupParsingTests { + + @Test + void parseNull() { + assertThat(TestGroup.parse(null)).isEqualTo(Collections.emptySet()); + } + + @Test + void parseEmptyString() { + assertThat(TestGroup.parse("")).isEqualTo(Collections.emptySet()); + } + + @Test + void parseBlankString() { + assertThat(TestGroup.parse(" ")).isEqualTo(Collections.emptySet()); + } + + @Test + void parseWithSpaces() { + assertThat(TestGroup.parse(" LONG_RUNNING, LONG_RUNNING ")).containsOnly(TestGroup.LONG_RUNNING); + } + + @Test + void parseInMixedCase() { + assertThat(TestGroup.parse("long_running, LonG_RunnING")).containsOnly(TestGroup.LONG_RUNNING); + } + + @Test + void parseMissing() { + assertThatIllegalArgumentException() + .isThrownBy(() -> TestGroup.parse("long_running, missing")) + .withMessageContaining("Unable to find test group 'missing' when parsing " + + "testGroups value: 'long_running, missing'. Available groups include: " + + "[LONG_RUNNING]"); + } + + @Test + void parseAll() { + assertThat(TestGroup.parse("all")).isEqualTo(EnumSet.allOf(TestGroup.class)); + } + + @Test + void parseAllExceptLongRunning() { + Set expected = EnumSet.allOf(TestGroup.class); + expected.remove(TestGroup.LONG_RUNNING); + assertThat(TestGroup.parse("all-long_running")).isEqualTo(expected); + } + + @Test + void parseAllExceptMissing() { + assertThatIllegalArgumentException() + .isThrownBy(() -> TestGroup.parse("all-missing")) + .withMessageContaining("Unable to find test group 'missing' when parsing " + + "testGroups value: 'all-missing'. Available groups include: " + + "[LONG_RUNNING]"); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/testfixture/TestGroupTests.java b/spring-core/src/test/java/org/springframework/core/testfixture/TestGroupTests.java new file mode 100644 index 0000000..0a04ce3 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/testfixture/TestGroupTests.java @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.testfixture; + +import java.util.Arrays; +import java.util.Set; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentest4j.TestAbortedException; + +import static java.util.stream.Collectors.joining; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; + +/** + * Tests for {@link TestGroup}. + * + * @author Sam Brannen + * @since 5.0 + */ +class TestGroupTests { + + private static final String TEST_GROUPS_SYSTEM_PROPERTY = "testGroups"; + + + private String originalTestGroups; + + + @BeforeEach + void trackOriginalTestGroups() { + this.originalTestGroups = System.getProperty(TEST_GROUPS_SYSTEM_PROPERTY); + } + + @AfterEach + void restoreOriginalTestGroups() { + if (this.originalTestGroups != null) { + setTestGroups(this.originalTestGroups); + } + else { + setTestGroups(""); + } + } + + @Test + void assumeGroupWithNoActiveTestGroups() { + setTestGroups(""); + + assertThatExceptionOfType(TestAbortedException.class).isThrownBy(() -> assumeGroup(LONG_RUNNING)); + } + + @Test + void assumeGroupWithMatchingActiveTestGroup() { + setTestGroups(LONG_RUNNING); + assertThatCode(() -> assumeGroup(LONG_RUNNING)) + .as("assumption should NOT have failed") + .doesNotThrowAnyException(); + } + + @Test + void assumeGroupWithBogusActiveTestGroup() { + assertBogusActiveTestGroupBehavior("bogus"); + } + + @Test + void assumeGroupWithAllMinusBogusActiveTestGroup() { + assertBogusActiveTestGroupBehavior("all-bogus"); + } + + private void assertBogusActiveTestGroupBehavior(String testGroups) { + // Should result in something similar to the following: + // + // java.lang.IllegalStateException: Failed to parse 'testGroups' system property: + // Unable to find test group 'bogus' when parsing testGroups value: 'all-bogus'. + // Available groups include: [LONG_RUNNING,PERFORMANCE] + + setTestGroups(testGroups); + assertThatIllegalStateException() + .isThrownBy(() -> assumeGroup(LONG_RUNNING)) + .withMessageStartingWith("Failed to parse '" + TEST_GROUPS_SYSTEM_PROPERTY + "' system property: ") + .havingCause() + .isInstanceOf(IllegalArgumentException.class) + .withMessage( + "Unable to find test group 'bogus' when parsing testGroups value: '" + testGroups + + "'. Available groups include: [LONG_RUNNING]"); + } + + private void setTestGroups(TestGroup... testGroups) { + setTestGroups(Arrays.stream(testGroups).map(TestGroup::name).collect(joining(", "))); + } + + private void setTestGroups(String testGroups) { + System.setProperty(TEST_GROUPS_SYSTEM_PROPERTY, testGroups); + } + + /** + * Assume that a particular {@link TestGroup} is active. + * @param group the group that must be active + * @throws org.opentest4j.TestAbortedException if the assumption fails + */ + private static void assumeGroup(TestGroup group) { + Set testGroups = TestGroup.loadTestGroups(); + assumeTrue(testGroups.contains(group), + () -> "Requires inactive test group " + group + "; active test groups: " + testGroups); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/type/AbstractAnnotationMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/AbstractAnnotationMetadataTests.java new file mode 100644 index 0000000..583787b --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/type/AbstractAnnotationMetadataTests.java @@ -0,0 +1,368 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.type.AbstractAnnotationMetadataTests.TestMemberClass.TestMemberClassInnerClass; +import org.springframework.core.type.AbstractAnnotationMetadataTests.TestMemberClass.TestMemberClassInnerInterface; +import org.springframework.util.MultiValueMap; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +/** + * Base class for {@link AnnotationMetadata} tests. + * + * @author Phillip Webb + */ +public abstract class AbstractAnnotationMetadataTests { + + @Test + public void getClassNameReturnsClassName() { + assertThat(get(TestClass.class).getClassName()).isEqualTo(TestClass.class.getName()); + } + + @Test + public void isInterfaceWhenInterfaceReturnsTrue() { + assertThat(get(TestInterface.class).isInterface()).isTrue(); + assertThat(get(TestAnnotation.class).isInterface()).isTrue(); + } + + @Test + public void isInterfaceWhenNotInterfaceReturnsFalse() { + assertThat(get(TestClass.class).isInterface()).isFalse(); + } + + @Test + public void isAnnotationWhenAnnotationReturnsTrue() { + assertThat(get(TestAnnotation.class).isAnnotation()).isTrue(); + } + + @Test + public void isAnnotationWhenNotAnnotationReturnsFalse() { + assertThat(get(TestClass.class).isAnnotation()).isFalse(); + assertThat(get(TestInterface.class).isAnnotation()).isFalse(); + } + + @Test + public void isFinalWhenFinalReturnsTrue() { + assertThat(get(TestFinalClass.class).isFinal()).isTrue(); + } + + @Test + public void isFinalWhenNonFinalReturnsFalse() { + assertThat(get(TestClass.class).isFinal()).isFalse(); + } + + @Test + public void isIndependentWhenIndependentReturnsTrue() { + assertThat(get(AbstractAnnotationMetadataTests.class).isIndependent()).isTrue(); + assertThat(get(TestClass.class).isIndependent()).isTrue(); + } + + @Test + public void isIndependentWhenNotIndependentReturnsFalse() { + assertThat(get(TestNonStaticInnerClass.class).isIndependent()).isFalse(); + } + + @Test + public void getEnclosingClassNameWhenHasEnclosingClassReturnsEnclosingClass() { + assertThat(get(TestClass.class).getEnclosingClassName()).isEqualTo( + AbstractAnnotationMetadataTests.class.getName()); + } + + @Test + public void getEnclosingClassNameWhenHasNoEnclosingClassReturnsNull() { + assertThat(get(AbstractAnnotationMetadataTests.class).getEnclosingClassName()).isNull(); + } + + @Test + public void getSuperClassNameWhenHasSuperClassReturnsName() { + assertThat(get(TestSubclass.class).getSuperClassName()).isEqualTo(TestClass.class.getName()); + assertThat(get(TestClass.class).getSuperClassName()).isEqualTo(Object.class.getName()); + } + + @Test + public void getSuperClassNameWhenHasNoSuperClassReturnsNull() { + assertThat(get(Object.class).getSuperClassName()).isNull(); + assertThat(get(TestInterface.class).getSuperClassName()).isNull(); + assertThat(get(TestSubInterface.class).getSuperClassName()).isNull(); + } + + @Test + public void getInterfaceNamesWhenHasInterfacesReturnsNames() { + assertThat(get(TestSubclass.class).getInterfaceNames()).containsExactlyInAnyOrder(TestInterface.class.getName()); + assertThat(get(TestSubInterface.class).getInterfaceNames()).containsExactlyInAnyOrder(TestInterface.class.getName()); + } + + @Test + public void getInterfaceNamesWhenHasNoInterfacesReturnsEmptyArray() { + assertThat(get(TestClass.class).getInterfaceNames()).isEmpty(); + } + + @Test + public void getMemberClassNamesWhenHasMemberClassesReturnsNames() { + assertThat(get(TestMemberClass.class).getMemberClassNames()).containsExactlyInAnyOrder( + TestMemberClassInnerClass.class.getName(), TestMemberClassInnerInterface.class.getName()); + } + + @Test + public void getMemberClassNamesWhenHasNoMemberClassesReturnsEmptyArray() { + assertThat(get(TestClass.class).getMemberClassNames()).isEmpty(); + } + + @Test + public void getAnnotationsReturnsDirectAnnotations() { + assertThat(get(WithDirectAnnotations.class).getAnnotations().stream()) + .filteredOn(MergedAnnotation::isDirectlyPresent) + .extracting(a -> a.getType().getName()) + .containsExactlyInAnyOrder(DirectAnnotation1.class.getName(), DirectAnnotation2.class.getName()); + } + + @Test + public void isAnnotatedWhenMatchesDirectAnnotationReturnsTrue() { + assertThat(get(WithDirectAnnotations.class).isAnnotated(DirectAnnotation1.class.getName())).isTrue(); + } + + @Test + public void isAnnotatedWhenMatchesMetaAnnotationReturnsTrue() { + assertThat(get(WithMetaAnnotations.class).isAnnotated(MetaAnnotation2.class.getName())).isTrue(); + } + + @Test + public void isAnnotatedWhenDoesNotMatchDirectOrMetaAnnotationReturnsFalse() { + assertThat(get(TestClass.class).isAnnotated(DirectAnnotation1.class.getName())).isFalse(); + } + + @Test + public void getAnnotationAttributesReturnsAttributes() { + assertThat(get(WithAnnotationAttributes.class).getAnnotationAttributes(AnnotationAttributes.class.getName())) + .containsOnly(entry("name", "test"), entry("size", 1)); + } + + @Test + public void getAllAnnotationAttributesReturnsAllAttributes() { + MultiValueMap attributes = + get(WithMetaAnnotationAttributes.class).getAllAnnotationAttributes(AnnotationAttributes.class.getName()); + assertThat(attributes).containsOnlyKeys("name", "size"); + assertThat(attributes.get("name")).containsExactlyInAnyOrder("m1", "m2"); + assertThat(attributes.get("size")).containsExactlyInAnyOrder(1, 2); + } + + @Test + public void getAnnotationTypesReturnsDirectAnnotations() { + AnnotationMetadata metadata = get(WithDirectAnnotations.class); + assertThat(metadata.getAnnotationTypes()).containsExactlyInAnyOrder( + DirectAnnotation1.class.getName(), DirectAnnotation2.class.getName()); + } + + @Test + public void getMetaAnnotationTypesReturnsMetaAnnotations() { + AnnotationMetadata metadata = get(WithMetaAnnotations.class); + assertThat(metadata.getMetaAnnotationTypes(MetaAnnotationRoot.class.getName())) + .containsExactlyInAnyOrder(MetaAnnotation1.class.getName(), MetaAnnotation2.class.getName()); + } + + @Test + public void hasAnnotationWhenMatchesDirectAnnotationReturnsTrue() { + assertThat(get(WithDirectAnnotations.class).hasAnnotation(DirectAnnotation1.class.getName())).isTrue(); + } + + @Test + public void hasAnnotationWhenMatchesMetaAnnotationReturnsFalse() { + assertThat(get(WithMetaAnnotations.class).hasAnnotation(MetaAnnotation1.class.getName())).isFalse(); + assertThat(get(WithMetaAnnotations.class).hasAnnotation(MetaAnnotation2.class.getName())).isFalse(); + } + + @Test + public void hasAnnotationWhenDoesNotMatchDirectOrMetaAnnotationReturnsFalse() { + assertThat(get(TestClass.class).hasAnnotation(DirectAnnotation1.class.getName())).isFalse(); + } + + @Test + public void hasMetaAnnotationWhenMatchesDirectReturnsFalse() { + assertThat(get(WithDirectAnnotations.class).hasMetaAnnotation(DirectAnnotation1.class.getName())).isFalse(); + } + + @Test + public void hasMetaAnnotationWhenMatchesMetaAnnotationReturnsTrue() { + assertThat(get(WithMetaAnnotations.class).hasMetaAnnotation(MetaAnnotation1.class.getName())).isTrue(); + assertThat(get(WithMetaAnnotations.class).hasMetaAnnotation(MetaAnnotation2.class.getName())).isTrue(); + } + + @Test + public void hasMetaAnnotationWhenDoesNotMatchDirectOrMetaAnnotationReturnsFalse() { + assertThat(get(TestClass.class).hasMetaAnnotation(MetaAnnotation1.class.getName())).isFalse(); + } + + @Test + public void hasAnnotatedMethodsWhenMatchesDirectAnnotationReturnsTrue() { + assertThat(get(WithAnnotatedMethod.class).hasAnnotatedMethods(DirectAnnotation1.class.getName())).isTrue(); + } + + @Test + public void hasAnnotatedMethodsWhenMatchesMetaAnnotationReturnsTrue() { + assertThat(get(WithMetaAnnotatedMethod.class).hasAnnotatedMethods(MetaAnnotation2.class.getName())).isTrue(); + } + + @Test + public void hasAnnotatedMethodsWhenDoesNotMatchAnyAnnotationReturnsFalse() { + assertThat(get(WithAnnotatedMethod.class).hasAnnotatedMethods(MetaAnnotation2.class.getName())).isFalse(); + assertThat(get(WithNonAnnotatedMethod.class).hasAnnotatedMethods(DirectAnnotation1.class.getName())).isFalse(); + } + + @Test + public void getAnnotatedMethodsReturnsMatchingAnnotatedAndMetaAnnotatedMethods() { + assertThat(get(WithDirectAndMetaAnnotatedMethods.class).getAnnotatedMethods(MetaAnnotation2.class.getName())) + .extracting(MethodMetadata::getMethodName) + .containsExactlyInAnyOrder("direct", "meta"); + } + + protected abstract AnnotationMetadata get(Class source); + + + @Retention(RetentionPolicy.RUNTIME) + public @interface DirectAnnotation1 { + } + + @Retention(RetentionPolicy.RUNTIME) + public @interface DirectAnnotation2 { + } + + @Retention(RetentionPolicy.RUNTIME) + @MetaAnnotation1 + public @interface MetaAnnotationRoot { + } + + @Retention(RetentionPolicy.RUNTIME) + @MetaAnnotation2 + public @interface MetaAnnotation1 { + } + + @Retention(RetentionPolicy.RUNTIME) + public @interface MetaAnnotation2 { + } + + public static class TestClass { + } + + public static interface TestInterface { + } + + public static interface TestSubInterface extends TestInterface { + } + + public @interface TestAnnotation { + } + + public static final class TestFinalClass { + } + + public class TestNonStaticInnerClass { + } + + public static class TestSubclass extends TestClass implements TestInterface { + } + + @DirectAnnotation1 + @DirectAnnotation2 + public static class WithDirectAnnotations { + } + + @MetaAnnotationRoot + public static class WithMetaAnnotations { + } + + public static class TestMemberClass { + + public static class TestMemberClassInnerClass { + } + + interface TestMemberClassInnerInterface { + } + + } + + public static class WithAnnotatedMethod { + + @DirectAnnotation1 + public void test() { + } + + } + + public static class WithMetaAnnotatedMethod { + + @MetaAnnotationRoot + public void test() { + } + + } + + public static class WithNonAnnotatedMethod { + + public void test() { + } + + } + + public static class WithDirectAndMetaAnnotatedMethods { + + @MetaAnnotation2 + public void direct() { + } + + @MetaAnnotationRoot + public void meta() { + } + + } + + @AnnotationAttributes(name = "test", size = 1) + public static class WithAnnotationAttributes { + } + + @MetaAnnotationAttributes1 + @MetaAnnotationAttributes2 + public static class WithMetaAnnotationAttributes { + } + + @Retention(RetentionPolicy.RUNTIME) + @AnnotationAttributes(name = "m1", size = 1) + public @interface MetaAnnotationAttributes1 { + } + + @Retention(RetentionPolicy.RUNTIME) + @AnnotationAttributes(name = "m2", size = 2) + public @interface MetaAnnotationAttributes2 { + } + + @Retention(RetentionPolicy.RUNTIME) + public @interface AnnotationAttributes { + + String name(); + + int size(); + + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/type/AbstractClassMetadataMemberClassTests.java b/spring-core/src/test/java/org/springframework/core/type/AbstractClassMetadataMemberClassTests.java new file mode 100644 index 0000000..7c520f9 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/type/AbstractClassMetadataMemberClassTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Abstract base class for testing implementations of + * {@link ClassMetadata#getMemberClassNames()}. + * + * @author Chris Beams + * @since 3.1 + */ +public abstract class AbstractClassMetadataMemberClassTests { + + public abstract ClassMetadata getClassMetadataFor(Class clazz); + + @Test + void withNoMemberClasses() { + ClassMetadata metadata = getClassMetadataFor(L0_a.class); + String[] nestedClasses = metadata.getMemberClassNames(); + assertThat(nestedClasses).isEqualTo(new String[]{}); + } + + public static class L0_a { + } + + + @Test + void withPublicMemberClasses() { + ClassMetadata metadata = getClassMetadataFor(L0_b.class); + String[] nestedClasses = metadata.getMemberClassNames(); + assertThat(nestedClasses).isEqualTo(new String[]{L0_b.L1.class.getName()}); + } + + public static class L0_b { + public static class L1 { } + } + + + @Test + void withNonPublicMemberClasses() { + ClassMetadata metadata = getClassMetadataFor(L0_c.class); + String[] nestedClasses = metadata.getMemberClassNames(); + assertThat(nestedClasses).isEqualTo(new String[]{L0_c.L1.class.getName()}); + } + + public static class L0_c { + private static class L1 { } + } + + + @Test + void againstMemberClass() { + ClassMetadata metadata = getClassMetadataFor(L0_b.L1.class); + String[] nestedClasses = metadata.getMemberClassNames(); + assertThat(nestedClasses).isEqualTo(new String[]{}); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/type/AbstractMethodMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/AbstractMethodMetadataTests.java new file mode 100644 index 0000000..07d48e4 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/type/AbstractMethodMetadataTests.java @@ -0,0 +1,273 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import example.type.AnnotatedComponent; +import example.type.EnclosingAnnotation; +import org.junit.jupiter.api.Test; + +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.util.MultiValueMap; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +/** + * Base class for {@link MethodMetadata} tests. + * + * @author Phillip Webb + * @author Sam Brannen + */ +public abstract class AbstractMethodMetadataTests { + + @Test + public void getMethodNameReturnsMethodName() { + assertThat(getTagged(WithMethod.class).getMethodName()).isEqualTo("test"); + } + + @Test + public void getDeclaringClassReturnsDeclaringClass() { + assertThat(getTagged(WithMethod.class).getDeclaringClassName()).isEqualTo( + WithMethod.class.getName()); + } + + @Test + public void getReturnTypeReturnsReturnType() { + assertThat(getTagged(WithMethod.class).getReturnTypeName()).isEqualTo( + String.class.getName()); + } + + @Test + public void isAbstractWhenAbstractReturnsTrue() { + assertThat(getTagged(WithAbstractMethod.class).isAbstract()).isTrue(); + } + + @Test + public void isAbstractWhenNotAbstractReturnsFalse() { + assertThat(getTagged(WithMethod.class).isAbstract()).isFalse(); + } + + @Test + public void isStatusWhenStaticReturnsTrue() { + assertThat(getTagged(WithStaticMethod.class).isStatic()).isTrue(); + } + + @Test + public void isStaticWhenNotStaticReturnsFalse() { + assertThat(getTagged(WithMethod.class).isStatic()).isFalse(); + } + + @Test + public void isFinalWhenFinalReturnsTrue() { + assertThat(getTagged(WithFinalMethod.class).isFinal()).isTrue(); + } + + @Test + public void isFinalWhenNonFinalReturnsFalse() { + assertThat(getTagged(WithMethod.class).isFinal()).isFalse(); + } + + @Test + public void isOverridableWhenOverridableReturnsTrue() { + assertThat(getTagged(WithMethod.class).isOverridable()).isTrue(); + } + + @Test + public void isOverridableWhenNonOverridableReturnsFalse() { + assertThat(getTagged(WithStaticMethod.class).isOverridable()).isFalse(); + assertThat(getTagged(WithFinalMethod.class).isOverridable()).isFalse(); + assertThat(getTagged(WithPrivateMethod.class).isOverridable()).isFalse(); + } + + @Test + public void getAnnotationsReturnsDirectAnnotations() { + MethodMetadata metadata = getTagged(WithDirectAnnotation.class); + assertThat(metadata.getAnnotations().stream().filter( + MergedAnnotation::isDirectlyPresent).map( + a -> a.getType().getName())).containsExactlyInAnyOrder( + Tag.class.getName(), + DirectAnnotation.class.getName()); + } + + @Test + public void isAnnotatedWhenMatchesDirectAnnotationReturnsTrue() { + assertThat(getTagged(WithDirectAnnotation.class).isAnnotated( + DirectAnnotation.class.getName())).isTrue(); + } + + @Test + public void isAnnotatedWhenMatchesMetaAnnotationReturnsTrue() { + assertThat(getTagged(WithMetaAnnotation.class).isAnnotated( + DirectAnnotation.class.getName())).isTrue(); + } + + @Test + public void isAnnotatedWhenDoesNotMatchDirectOrMetaAnnotationReturnsFalse() { + assertThat(getTagged(WithMethod.class).isAnnotated( + DirectAnnotation.class.getName())).isFalse(); + } + + @Test + public void getAnnotationAttributesReturnsAttributes() { + assertThat(getTagged(WithAnnotationAttributes.class).getAnnotationAttributes( + AnnotationAttributes.class.getName())).containsOnly(entry("name", "test"), + entry("size", 1)); + } + + @Test + public void getAllAnnotationAttributesReturnsAllAttributes() { + MultiValueMap attributes = getTagged( + WithMetaAnnotationAttributes.class).getAllAnnotationAttributes( + AnnotationAttributes.class.getName()); + assertThat(attributes).containsOnlyKeys("name", "size"); + assertThat(attributes.get("name")).containsExactlyInAnyOrder("m1", "m2"); + assertThat(attributes.get("size")).containsExactlyInAnyOrder(1, 2); + } + + @Test // gh-24375 + public void metadataLoadsForNestedAnnotations() { + AnnotationMetadata annotationMetadata = get(AnnotatedComponent.class); + assertThat(annotationMetadata.getAnnotationTypes()).containsExactly(EnclosingAnnotation.class.getName()); + } + + protected MethodMetadata getTagged(Class source) { + return get(source, Tag.class.getName()); + } + + protected MethodMetadata get(Class source, String annotationName) { + return get(source).getAnnotatedMethods(annotationName).iterator().next(); + } + + protected abstract AnnotationMetadata get(Class source); + + @Retention(RetentionPolicy.RUNTIME) + public static @interface Tag { + + } + + public static class WithMethod { + + @Tag + public String test() { + return ""; + } + + } + + public abstract static class WithAbstractMethod { + + @Tag + public abstract String test(); + + } + + public static class WithStaticMethod { + + @Tag + public static String test() { + return ""; + } + + } + + public static class WithFinalMethod { + + @Tag + public final String test() { + return ""; + } + + } + + public static class WithPrivateMethod { + + @Tag + private final String test() { + return ""; + } + + } + + public static abstract class WithDirectAnnotation { + + @Tag + @DirectAnnotation + public abstract String test(); + + } + + public static abstract class WithMetaAnnotation { + + @Tag + @MetaAnnotation + public abstract String test(); + + } + + @Retention(RetentionPolicy.RUNTIME) + public static @interface DirectAnnotation { + + } + + @DirectAnnotation + @Retention(RetentionPolicy.RUNTIME) + public static @interface MetaAnnotation { + + } + + public static abstract class WithAnnotationAttributes { + + @Tag + @AnnotationAttributes(name = "test", size = 1) + public abstract String test(); + + } + + public static abstract class WithMetaAnnotationAttributes { + + @Tag + @MetaAnnotationAttributes1 + @MetaAnnotationAttributes2 + public abstract String test(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @AnnotationAttributes(name = "m1", size = 1) + public static @interface MetaAnnotationAttributes1 { + + } + + @Retention(RetentionPolicy.RUNTIME) + @AnnotationAttributes(name = "m2", size = 2) + public static @interface MetaAnnotationAttributes2 { + + } + + @Retention(RetentionPolicy.RUNTIME) + public static @interface AnnotationAttributes { + + String name(); + + int size(); + + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java new file mode 100644 index 0000000..f134288 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java @@ -0,0 +1,595 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type; + +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.annotation.AliasFor; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.testfixture.stereotype.Component; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests demonstrating that the reflection-based {@link StandardAnnotationMetadata} + * and ASM-based {@code SimpleAnnotationMetadata} produce almost identical output. + * + * @author Juergen Hoeller + * @author Chris Beams + * @author Phillip Webb + * @author Sam Brannen + * @see InheritedAnnotationsAnnotationMetadataTests + */ +class AnnotationMetadataTests { + + @Test + void standardAnnotationMetadata() { + AnnotationMetadata metadata = AnnotationMetadata.introspect(AnnotatedComponent.class); + doTestAnnotationInfo(metadata); + doTestMethodAnnotationInfo(metadata); + } + + @Test + void asmAnnotationMetadata() throws Exception { + MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(AnnotatedComponent.class.getName()); + AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); + doTestAnnotationInfo(metadata); + doTestMethodAnnotationInfo(metadata); + } + + @Test + void standardAnnotationMetadataForSubclass() { + AnnotationMetadata metadata = AnnotationMetadata.introspect(AnnotatedComponentSubClass.class); + doTestSubClassAnnotationInfo(metadata, false); + } + + @Test + void asmAnnotationMetadataForSubclass() throws Exception { + MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(AnnotatedComponentSubClass.class.getName()); + AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); + doTestSubClassAnnotationInfo(metadata, true); + } + + private void doTestSubClassAnnotationInfo(AnnotationMetadata metadata, boolean asm) { + assertThat(metadata.getClassName()).isEqualTo(AnnotatedComponentSubClass.class.getName()); + assertThat(metadata.isInterface()).isFalse(); + assertThat(metadata.isAnnotation()).isFalse(); + assertThat(metadata.isAbstract()).isFalse(); + assertThat(metadata.isConcrete()).isTrue(); + assertThat(metadata.hasSuperClass()).isTrue(); + assertThat(metadata.getSuperClassName()).isEqualTo(AnnotatedComponent.class.getName()); + assertThat(metadata.getInterfaceNames().length).isEqualTo(0); + assertThat(metadata.isAnnotated(Component.class.getName())).isFalse(); + assertThat(metadata.isAnnotated(Scope.class.getName())).isFalse(); + assertThat(metadata.isAnnotated(SpecialAttr.class.getName())).isFalse(); + + if (asm) { + assertThat(metadata.isAnnotated(NamedComposedAnnotation.class.getName())).isFalse(); + assertThat(metadata.hasAnnotation(NamedComposedAnnotation.class.getName())).isFalse(); + assertThat(metadata.getAnnotationTypes()).isEmpty(); + } + else { + assertThat(metadata.isAnnotated(NamedComposedAnnotation.class.getName())).isTrue(); + assertThat(metadata.hasAnnotation(NamedComposedAnnotation.class.getName())).isTrue(); + assertThat(metadata.getAnnotationTypes()).containsExactly(NamedComposedAnnotation.class.getName()); + } + + assertThat(metadata.hasAnnotation(Component.class.getName())).isFalse(); + assertThat(metadata.hasAnnotation(Scope.class.getName())).isFalse(); + assertThat(metadata.hasAnnotation(SpecialAttr.class.getName())).isFalse(); + assertThat(metadata.hasMetaAnnotation(Component.class.getName())).isFalse(); + assertThat(metadata.hasMetaAnnotation(MetaAnnotation.class.getName())).isFalse(); + assertThat(metadata.getAnnotationAttributes(Component.class.getName())).isNull(); + assertThat(metadata.getAnnotationAttributes(MetaAnnotation.class.getName(), false)).isNull(); + assertThat(metadata.getAnnotationAttributes(MetaAnnotation.class.getName(), true)).isNull(); + assertThat(metadata.getAnnotatedMethods(DirectAnnotation.class.getName()).size()).isEqualTo(0); + assertThat(metadata.isAnnotated(IsAnnotatedAnnotation.class.getName())).isEqualTo(false); + assertThat(metadata.getAllAnnotationAttributes(DirectAnnotation.class.getName())).isNull(); + } + + @Test + void standardAnnotationMetadataForInterface() { + AnnotationMetadata metadata = AnnotationMetadata.introspect(AnnotationMetadata.class); + doTestMetadataForInterfaceClass(metadata); + } + + @Test + void asmAnnotationMetadataForInterface() throws Exception { + MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(AnnotationMetadata.class.getName()); + AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); + doTestMetadataForInterfaceClass(metadata); + } + + private void doTestMetadataForInterfaceClass(AnnotationMetadata metadata) { + assertThat(metadata.getClassName()).isEqualTo(AnnotationMetadata.class.getName()); + assertThat(metadata.isInterface()).isTrue(); + assertThat(metadata.isAnnotation()).isFalse(); + assertThat(metadata.isAbstract()).isTrue(); + assertThat(metadata.isConcrete()).isFalse(); + assertThat(metadata.hasSuperClass()).isFalse(); + assertThat(metadata.getSuperClassName()).isNull(); + assertThat(metadata.getInterfaceNames().length).isEqualTo(2); + assertThat(metadata.getInterfaceNames()[0]).isEqualTo(ClassMetadata.class.getName()); + assertThat(metadata.getInterfaceNames()[1]).isEqualTo(AnnotatedTypeMetadata.class.getName()); + assertThat(metadata.getAnnotationTypes()).hasSize(0); + } + + @Test + void standardAnnotationMetadataForAnnotation() { + AnnotationMetadata metadata = AnnotationMetadata.introspect(Component.class); + doTestMetadataForAnnotationClass(metadata); + } + + @Test + void asmAnnotationMetadataForAnnotation() throws Exception { + MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(Component.class.getName()); + AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); + doTestMetadataForAnnotationClass(metadata); + } + + private void doTestMetadataForAnnotationClass(AnnotationMetadata metadata) { + assertThat(metadata.getClassName()).isEqualTo(Component.class.getName()); + assertThat(metadata.isInterface()).isTrue(); + assertThat(metadata.isAnnotation()).isTrue(); + assertThat(metadata.isAbstract()).isTrue(); + assertThat(metadata.isConcrete()).isFalse(); + assertThat(metadata.hasSuperClass()).isFalse(); + assertThat(metadata.getSuperClassName()).isNull(); + assertThat(metadata.getInterfaceNames().length).isEqualTo(1); + assertThat(metadata.getInterfaceNames()[0]).isEqualTo(Annotation.class.getName()); + assertThat(metadata.isAnnotated(Documented.class.getName())).isFalse(); + assertThat(metadata.isAnnotated(Scope.class.getName())).isFalse(); + assertThat(metadata.isAnnotated(SpecialAttr.class.getName())).isFalse(); + assertThat(metadata.hasAnnotation(Documented.class.getName())).isFalse(); + assertThat(metadata.hasAnnotation(Scope.class.getName())).isFalse(); + assertThat(metadata.hasAnnotation(SpecialAttr.class.getName())).isFalse(); + assertThat(metadata.getAnnotationTypes()).hasSize(1); + } + + /** + * In order to preserve backward-compatibility, {@link StandardAnnotationMetadata} + * defaults to return nested annotations and annotation arrays as actual + * Annotation instances. It is recommended for compatibility with ASM-based + * AnnotationMetadata implementations to set the 'nestedAnnotationsAsMap' flag to + * 'true' as is done in the main test above. + */ + @Test + @Deprecated + void standardAnnotationMetadata_nestedAnnotationsAsMap_false() { + AnnotationMetadata metadata = new StandardAnnotationMetadata(AnnotatedComponent.class); + AnnotationAttributes specialAttrs = (AnnotationAttributes) metadata.getAnnotationAttributes(SpecialAttr.class.getName()); + Annotation[] nestedAnnoArray = (Annotation[]) specialAttrs.get("nestedAnnoArray"); + assertThat(nestedAnnoArray[0]).isInstanceOf(NestedAnno.class); + } + + @Test + @Deprecated + void metaAnnotationOverridesUsingStandardAnnotationMetadata() { + AnnotationMetadata metadata = new StandardAnnotationMetadata(ComposedConfigurationWithAttributeOverridesClass.class); + assertMetaAnnotationOverrides(metadata); + } + + @Test + void metaAnnotationOverridesUsingAnnotationMetadataReadingVisitor() throws Exception { + MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(ComposedConfigurationWithAttributeOverridesClass.class.getName()); + AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); + assertMetaAnnotationOverrides(metadata); + } + + private void assertMetaAnnotationOverrides(AnnotationMetadata metadata) { + AnnotationAttributes attributes = (AnnotationAttributes) metadata.getAnnotationAttributes( + TestComponentScan.class.getName(), false); + assertThat(attributes.getStringArray("basePackages")).containsExactly("org.example.componentscan"); + assertThat(attributes.getStringArray("value")).isEmpty(); + assertThat(attributes.getClassArray("basePackageClasses")).isEmpty(); + } + + @Test // SPR-11649 + void multipleAnnotationsWithIdenticalAttributeNamesUsingStandardAnnotationMetadata() { + AnnotationMetadata metadata = AnnotationMetadata.introspect(NamedAnnotationsClass.class); + assertMultipleAnnotationsWithIdenticalAttributeNames(metadata); + } + + @Test // SPR-11649 + void multipleAnnotationsWithIdenticalAttributeNamesUsingAnnotationMetadataReadingVisitor() throws Exception { + MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(NamedAnnotationsClass.class.getName()); + AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); + assertMultipleAnnotationsWithIdenticalAttributeNames(metadata); + } + + @Test // SPR-11649 + void composedAnnotationWithMetaAnnotationsWithIdenticalAttributeNamesUsingStandardAnnotationMetadata() { + AnnotationMetadata metadata = AnnotationMetadata.introspect(NamedComposedAnnotationClass.class); + assertMultipleAnnotationsWithIdenticalAttributeNames(metadata); + } + + @Test // SPR-11649 + void composedAnnotationWithMetaAnnotationsWithIdenticalAttributeNamesUsingAnnotationMetadataReadingVisitor() throws Exception { + MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(NamedComposedAnnotationClass.class.getName()); + AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); + assertMultipleAnnotationsWithIdenticalAttributeNames(metadata); + } + + @Test + void inheritedAnnotationWithMetaAnnotationsWithIdenticalAttributeNamesUsingStandardAnnotationMetadata() { + AnnotationMetadata metadata = AnnotationMetadata.introspect(NamedComposedAnnotationExtended.class); + assertThat(metadata.hasAnnotation(NamedComposedAnnotation.class.getName())).isTrue(); + } + + @Test + void inheritedAnnotationWithMetaAnnotationsWithIdenticalAttributeNamesUsingAnnotationMetadataReadingVisitor() throws Exception { + MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(NamedComposedAnnotationExtended.class.getName()); + AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); + assertThat(metadata.hasAnnotation(NamedComposedAnnotation.class.getName())).isFalse(); + } + + + private void assertMultipleAnnotationsWithIdenticalAttributeNames(AnnotationMetadata metadata) { + AnnotationAttributes attributes1 = (AnnotationAttributes) metadata.getAnnotationAttributes( + NamedAnnotation1.class.getName(), false); + String name1 = attributes1.getString("name"); + assertThat(name1).as("name of NamedAnnotation1").isEqualTo("name 1"); + + AnnotationAttributes attributes2 = (AnnotationAttributes) metadata.getAnnotationAttributes( + NamedAnnotation2.class.getName(), false); + String name2 = attributes2.getString("name"); + assertThat(name2).as("name of NamedAnnotation2").isEqualTo("name 2"); + + AnnotationAttributes attributes3 = (AnnotationAttributes) metadata.getAnnotationAttributes( + NamedAnnotation3.class.getName(), false); + String name3 = attributes3.getString("name"); + assertThat(name3).as("name of NamedAnnotation3").isEqualTo("name 3"); + } + + private void doTestAnnotationInfo(AnnotationMetadata metadata) { + assertThat(metadata.getClassName()).isEqualTo(AnnotatedComponent.class.getName()); + assertThat(metadata.isInterface()).isFalse(); + assertThat(metadata.isAnnotation()).isFalse(); + assertThat(metadata.isAbstract()).isFalse(); + assertThat(metadata.isConcrete()).isTrue(); + assertThat(metadata.hasSuperClass()).isTrue(); + assertThat(metadata.getSuperClassName()).isEqualTo(Object.class.getName()); + assertThat(metadata.getInterfaceNames().length).isEqualTo(1); + assertThat(metadata.getInterfaceNames()[0]).isEqualTo(Serializable.class.getName()); + + assertThat(metadata.isAnnotated(Component.class.getName())).isTrue(); + + assertThat(metadata.isAnnotated(NamedComposedAnnotation.class.getName())).isTrue(); + + assertThat(metadata.hasAnnotation(Component.class.getName())).isTrue(); + assertThat(metadata.hasAnnotation(Scope.class.getName())).isTrue(); + assertThat(metadata.hasAnnotation(SpecialAttr.class.getName())).isTrue(); + + assertThat(metadata.hasAnnotation(NamedComposedAnnotation.class.getName())).isTrue(); + assertThat(metadata.getAnnotationTypes()).containsExactlyInAnyOrder( + Component.class.getName(), Scope.class.getName(), + SpecialAttr.class.getName(), DirectAnnotation.class.getName(), + MetaMetaAnnotation.class.getName(), EnumSubclasses.class.getName(), + NamedComposedAnnotation.class.getName()); + + AnnotationAttributes compAttrs = (AnnotationAttributes) metadata.getAnnotationAttributes(Component.class.getName()); + assertThat(compAttrs).hasSize(1); + assertThat(compAttrs.getString("value")).isEqualTo("myName"); + AnnotationAttributes scopeAttrs = (AnnotationAttributes) metadata.getAnnotationAttributes(Scope.class.getName()); + assertThat(scopeAttrs).hasSize(1); + assertThat(scopeAttrs.getString("value")).isEqualTo("myScope"); + + Set methods = metadata.getAnnotatedMethods(DirectAnnotation.class.getName()); + MethodMetadata method = methods.iterator().next(); + assertThat(method.getAnnotationAttributes(DirectAnnotation.class.getName()).get("value")).isEqualTo("direct"); + assertThat(method.getAnnotationAttributes(DirectAnnotation.class.getName()).get("myValue")).isEqualTo("direct"); + List allMeta = method.getAllAnnotationAttributes(DirectAnnotation.class.getName()).get("value"); + assertThat(new HashSet<>(allMeta)).isEqualTo(new HashSet(Arrays.asList("direct", "meta"))); + allMeta = method.getAllAnnotationAttributes(DirectAnnotation.class.getName()).get("additional"); + assertThat(new HashSet<>(allMeta)).isEqualTo(new HashSet(Arrays.asList("direct"))); + + assertThat(metadata.isAnnotated(IsAnnotatedAnnotation.class.getName())).isTrue(); + + { // perform tests with classValuesAsString = false (the default) + AnnotationAttributes specialAttrs = (AnnotationAttributes) metadata.getAnnotationAttributes(SpecialAttr.class.getName()); + assertThat(specialAttrs).hasSize(6); + assertThat(String.class.isAssignableFrom(specialAttrs.getClass("clazz"))).isTrue(); + assertThat(specialAttrs.getEnum("state").equals(Thread.State.NEW)).isTrue(); + + AnnotationAttributes nestedAnno = specialAttrs.getAnnotation("nestedAnno"); + assertThat("na").isEqualTo(nestedAnno.getString("value")); + assertThat(nestedAnno.getEnum("anEnum").equals(SomeEnum.LABEL1)).isTrue(); + assertThat((Class[]) nestedAnno.get("classArray")).isEqualTo(new Class[] {String.class}); + + AnnotationAttributes[] nestedAnnoArray = specialAttrs.getAnnotationArray("nestedAnnoArray"); + assertThat(nestedAnnoArray.length).isEqualTo(2); + assertThat(nestedAnnoArray[0].getString("value")).isEqualTo("default"); + assertThat(nestedAnnoArray[0].getEnum("anEnum").equals(SomeEnum.DEFAULT)).isTrue(); + assertThat((Class[]) nestedAnnoArray[0].get("classArray")).isEqualTo(new Class[] {Void.class}); + assertThat(nestedAnnoArray[1].getString("value")).isEqualTo("na1"); + assertThat(nestedAnnoArray[1].getEnum("anEnum").equals(SomeEnum.LABEL2)).isTrue(); + assertThat((Class[]) nestedAnnoArray[1].get("classArray")).isEqualTo(new Class[] {Number.class}); + assertThat(nestedAnnoArray[1].getClassArray("classArray")).isEqualTo(new Class[] {Number.class}); + + AnnotationAttributes optional = specialAttrs.getAnnotation("optional"); + assertThat(optional.getString("value")).isEqualTo("optional"); + assertThat(optional.getEnum("anEnum").equals(SomeEnum.DEFAULT)).isTrue(); + assertThat((Class[]) optional.get("classArray")).isEqualTo(new Class[] {Void.class}); + assertThat(optional.getClassArray("classArray")).isEqualTo(new Class[] {Void.class}); + + AnnotationAttributes[] optionalArray = specialAttrs.getAnnotationArray("optionalArray"); + assertThat(optionalArray.length).isEqualTo(1); + assertThat(optionalArray[0].getString("value")).isEqualTo("optional"); + assertThat(optionalArray[0].getEnum("anEnum").equals(SomeEnum.DEFAULT)).isTrue(); + assertThat((Class[]) optionalArray[0].get("classArray")).isEqualTo(new Class[] {Void.class}); + assertThat(optionalArray[0].getClassArray("classArray")).isEqualTo(new Class[] {Void.class}); + + assertThat(metadata.getAnnotationAttributes(DirectAnnotation.class.getName()).get("value")).isEqualTo("direct"); + allMeta = metadata.getAllAnnotationAttributes(DirectAnnotation.class.getName()).get("value"); + assertThat(new HashSet<>(allMeta)).isEqualTo(new HashSet(Arrays.asList("direct", "meta"))); + allMeta = metadata.getAllAnnotationAttributes(DirectAnnotation.class.getName()).get("additional"); + assertThat(new HashSet<>(allMeta)).isEqualTo(new HashSet(Arrays.asList("direct", ""))); + assertThat(metadata.getAnnotationAttributes(DirectAnnotation.class.getName()).get("additional")).isEqualTo(""); + assertThat(((String[]) metadata.getAnnotationAttributes(DirectAnnotation.class.getName()).get("additionalArray")).length).isEqualTo(0); + } + { // perform tests with classValuesAsString = true + AnnotationAttributes specialAttrs = (AnnotationAttributes) metadata.getAnnotationAttributes( + SpecialAttr.class.getName(), true); + assertThat(specialAttrs).hasSize(6); + assertThat(specialAttrs.get("clazz")).isEqualTo(String.class.getName()); + assertThat(specialAttrs.getString("clazz")).isEqualTo(String.class.getName()); + + AnnotationAttributes nestedAnno = specialAttrs.getAnnotation("nestedAnno"); + assertThat(nestedAnno.getStringArray("classArray")).isEqualTo(new String[] { String.class.getName() }); + assertThat(nestedAnno.getStringArray("classArray")).isEqualTo(new String[] { String.class.getName() }); + + AnnotationAttributes[] nestedAnnoArray = specialAttrs.getAnnotationArray("nestedAnnoArray"); + assertThat((String[]) nestedAnnoArray[0].get("classArray")).isEqualTo(new String[] { Void.class.getName() }); + assertThat(nestedAnnoArray[0].getStringArray("classArray")).isEqualTo(new String[] { Void.class.getName() }); + assertThat((String[]) nestedAnnoArray[1].get("classArray")).isEqualTo(new String[] { Number.class.getName() }); + assertThat(nestedAnnoArray[1].getStringArray("classArray")).isEqualTo(new String[] { Number.class.getName() }); + + AnnotationAttributes optional = specialAttrs.getAnnotation("optional"); + assertThat((String[]) optional.get("classArray")).isEqualTo(new String[] { Void.class.getName() }); + assertThat(optional.getStringArray("classArray")).isEqualTo(new String[] { Void.class.getName() }); + + AnnotationAttributes[] optionalArray = specialAttrs.getAnnotationArray("optionalArray"); + assertThat((String[]) optionalArray[0].get("classArray")).isEqualTo(new String[] { Void.class.getName() }); + assertThat(optionalArray[0].getStringArray("classArray")).isEqualTo(new String[] { Void.class.getName() }); + + assertThat(metadata.getAnnotationAttributes(DirectAnnotation.class.getName()).get("value")).isEqualTo("direct"); + allMeta = metadata.getAllAnnotationAttributes(DirectAnnotation.class.getName()).get("value"); + assertThat(new HashSet<>(allMeta)).isEqualTo(new HashSet(Arrays.asList("direct", "meta"))); + } + } + + private void doTestMethodAnnotationInfo(AnnotationMetadata classMetadata) { + Set methods = classMetadata.getAnnotatedMethods(TestAutowired.class.getName()); + assertThat(methods).hasSize(1); + for (MethodMetadata methodMetadata : methods) { + assertThat(methodMetadata.isAnnotated(TestAutowired.class.getName())).isTrue(); + } + } + + + // ------------------------------------------------------------------------- + + public static enum SomeEnum { + LABEL1, LABEL2, DEFAULT + } + + @Target({}) + @Retention(RetentionPolicy.RUNTIME) + public @interface NestedAnno { + + String value() default "default"; + + SomeEnum anEnum() default SomeEnum.DEFAULT; + + Class[] classArray() default Void.class; + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + public @interface SpecialAttr { + + Class clazz(); + + Thread.State state(); + + NestedAnno nestedAnno(); + + NestedAnno[] nestedAnnoArray(); + + NestedAnno optional() default @NestedAnno(value = "optional", anEnum = SomeEnum.DEFAULT, classArray = Void.class); + + NestedAnno[] optionalArray() default { @NestedAnno(value = "optional", anEnum = SomeEnum.DEFAULT, classArray = Void.class) }; + } + + @Target({ElementType.TYPE, ElementType.METHOD}) + @Retention(RetentionPolicy.RUNTIME) + public @interface DirectAnnotation { + + @AliasFor("myValue") + String value() default ""; + + @AliasFor("value") + String myValue() default ""; + + String additional() default "direct"; + + String[] additionalArray() default "direct"; + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + public @interface IsAnnotatedAnnotation { + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @DirectAnnotation("meta") + @IsAnnotatedAnnotation + public @interface MetaAnnotation { + + String additional() default "meta"; + } + + @Target({ElementType.TYPE, ElementType.METHOD}) + @Retention(RetentionPolicy.RUNTIME) + @MetaAnnotation + public @interface MetaMetaAnnotation { + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + public @interface EnumSubclasses { + + SubclassEnum[] value(); + } + + // SPR-10914 + public enum SubclassEnum { + FOO { + /* Do not delete! This subclassing is intentional. */ + }, + BAR { + /* Do not delete! This subclassing is intentional. */ + } + } + + @Component("myName") + @Scope("myScope") + @SpecialAttr(clazz = String.class, state = Thread.State.NEW, + nestedAnno = @NestedAnno(value = "na", anEnum = SomeEnum.LABEL1, classArray = {String.class}), + nestedAnnoArray = {@NestedAnno, @NestedAnno(value = "na1", anEnum = SomeEnum.LABEL2, classArray = {Number.class})}) + @SuppressWarnings({"serial", "unused"}) + @DirectAnnotation(value = "direct", additional = "", additionalArray = {}) + @MetaMetaAnnotation + @EnumSubclasses({SubclassEnum.FOO, SubclassEnum.BAR}) + @NamedComposedAnnotation + private static class AnnotatedComponent implements Serializable { + + @TestAutowired + public void doWork(@TestQualifier("myColor") java.awt.Color color) { + } + + public void doSleep() { + } + + @DirectAnnotation("direct") + @MetaMetaAnnotation + public void meta() { + } + } + + @SuppressWarnings("serial") + private static class AnnotatedComponentSubClass extends AnnotatedComponent { + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Component + public @interface TestConfiguration { + + String value() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface TestComponentScan { + + String[] value() default {}; + + String[] basePackages() default {}; + + Class[] basePackageClasses() default {}; + } + + @TestConfiguration + @TestComponentScan(basePackages = "bogus") + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface ComposedConfigurationWithAttributeOverrides { + + String[] basePackages() default {}; + } + + @ComposedConfigurationWithAttributeOverrides(basePackages = "org.example.componentscan") + public static class ComposedConfigurationWithAttributeOverridesClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface NamedAnnotation1 { + String name() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface NamedAnnotation2 { + String name() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface NamedAnnotation3 { + String name() default ""; + } + + @NamedAnnotation1(name = "name 1") + @NamedAnnotation2(name = "name 2") + @NamedAnnotation3(name = "name 3") + public static class NamedAnnotationsClass { + } + + @NamedAnnotation1(name = "name 1") + @NamedAnnotation2(name = "name 2") + @NamedAnnotation3(name = "name 3") + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @Inherited + public @interface NamedComposedAnnotation { + } + + @NamedComposedAnnotation + public static class NamedComposedAnnotationClass { + } + + public static class NamedComposedAnnotationExtended extends NamedComposedAnnotationClass { + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/type/AnnotationTypeFilterTests.java b/spring-core/src/test/java/org/springframework/core/type/AnnotationTypeFilterTests.java new file mode 100644 index 0000000..1550c9d --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/type/AnnotationTypeFilterTests.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type; + +import example.type.AnnotationTypeFilterTestsTypes; +import example.type.InheritedAnnotation; +import example.type.NonInheritedAnnotation; +import org.junit.jupiter.api.Test; + +import org.springframework.core.testfixture.stereotype.Component; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; +import org.springframework.core.type.filter.AnnotationTypeFilter; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Ramnivas Laddad + * @author Juergen Hoeller + * @author Oliver Gierke + * @author Sam Brannen + * @see AnnotationTypeFilterTestsTypes + */ +class AnnotationTypeFilterTests { + + @Test + void directAnnotationMatch() throws Exception { + MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + String classUnderTest = "example.type.AnnotationTypeFilterTestsTypes$SomeComponent"; + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(classUnderTest); + + AnnotationTypeFilter filter = new AnnotationTypeFilter(InheritedAnnotation.class); + assertThat(filter.match(metadataReader, metadataReaderFactory)).isTrue(); + ClassloadingAssertions.assertClassNotLoaded(classUnderTest); + } + + @Test + void inheritedAnnotationFromInterfaceDoesNotMatch() throws Exception { + MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + String classUnderTest = "example.type.AnnotationTypeFilterTestsTypes$SomeClassWithSomeComponentInterface"; + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(classUnderTest); + + AnnotationTypeFilter filter = new AnnotationTypeFilter(InheritedAnnotation.class); + // Must fail as annotation on interfaces should not be considered a match + assertThat(filter.match(metadataReader, metadataReaderFactory)).isFalse(); + ClassloadingAssertions.assertClassNotLoaded(classUnderTest); + } + + @Test + void inheritedAnnotationFromBaseClassDoesMatch() throws Exception { + MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + String classUnderTest = "example.type.AnnotationTypeFilterTestsTypes$SomeSubclassOfSomeComponent"; + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(classUnderTest); + + AnnotationTypeFilter filter = new AnnotationTypeFilter(InheritedAnnotation.class); + assertThat(filter.match(metadataReader, metadataReaderFactory)).isTrue(); + ClassloadingAssertions.assertClassNotLoaded(classUnderTest); + } + + @Test + void nonInheritedAnnotationDoesNotMatch() throws Exception { + MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + String classUnderTest = "example.type.AnnotationTypeFilterTestsTypes$SomeSubclassOfSomeClassMarkedWithNonInheritedAnnotation"; + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(classUnderTest); + + AnnotationTypeFilter filter = new AnnotationTypeFilter(NonInheritedAnnotation.class); + // Must fail as annotation isn't inherited + assertThat(filter.match(metadataReader, metadataReaderFactory)).isFalse(); + ClassloadingAssertions.assertClassNotLoaded(classUnderTest); + } + + @Test + void nonAnnotatedClassDoesntMatch() throws Exception { + MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + String classUnderTest = "example.type.AnnotationTypeFilterTestsTypes$SomeNonCandidateClass"; + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(classUnderTest); + + AnnotationTypeFilter filter = new AnnotationTypeFilter(Component.class); + assertThat(filter.match(metadataReader, metadataReaderFactory)).isFalse(); + ClassloadingAssertions.assertClassNotLoaded(classUnderTest); + } + + @Test + void matchesInterfacesIfConfigured() throws Exception { + MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + String classUnderTest = "example.type.AnnotationTypeFilterTestsTypes$SomeClassWithSomeComponentInterface"; + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(classUnderTest); + + AnnotationTypeFilter filter = new AnnotationTypeFilter(InheritedAnnotation.class, false, true); + assertThat(filter.match(metadataReader, metadataReaderFactory)).isTrue(); + ClassloadingAssertions.assertClassNotLoaded(classUnderTest); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/type/AspectJTypeFilterTests.java b/spring-core/src/test/java/org/springframework/core/type/AspectJTypeFilterTests.java new file mode 100644 index 0000000..a356881 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/type/AspectJTypeFilterTests.java @@ -0,0 +1,148 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type; + +import example.type.AspectJTypeFilterTestsTypes; +import org.junit.jupiter.api.Test; + +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; +import org.springframework.core.type.filter.AspectJTypeFilter; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Ramnivas Laddad + * @author Sam Brannen + * @see AspectJTypeFilterTestsTypes + */ +class AspectJTypeFilterTests { + + @Test + void namePatternMatches() throws Exception { + assertMatch("example.type.AspectJTypeFilterTestsTypes$SomeClass", + "example.type.AspectJTypeFilterTestsTypes.SomeClass"); + assertMatch("example.type.AspectJTypeFilterTestsTypes$SomeClass", + "*"); + assertMatch("example.type.AspectJTypeFilterTestsTypes$SomeClass", + "*..SomeClass"); + assertMatch("example.type.AspectJTypeFilterTestsTypes$SomeClass", + "example..SomeClass"); + } + + @Test + void namePatternNoMatches() throws Exception { + assertNoMatch("example.type.AspectJTypeFilterTestsTypes$SomeClass", + "example.type.AspectJTypeFilterTestsTypes.SomeClassX"); + } + + @Test + void subclassPatternMatches() throws Exception { + assertMatch("example.type.AspectJTypeFilterTestsTypes$SomeClassExtendingSomeClass", + "example.type.AspectJTypeFilterTestsTypes.SomeClass+"); + assertMatch("example.type.AspectJTypeFilterTestsTypes$SomeClassExtendingSomeClass", + "*+"); + assertMatch("example.type.AspectJTypeFilterTestsTypes$SomeClassExtendingSomeClass", + "java.lang.Object+"); + + assertMatch("example.type.AspectJTypeFilterTestsTypes$SomeClassImplementingSomeInterface", + "example.type.AspectJTypeFilterTestsTypes.SomeInterface+"); + assertMatch("example.type.AspectJTypeFilterTestsTypes$SomeClassImplementingSomeInterface", + "*+"); + assertMatch("example.type.AspectJTypeFilterTestsTypes$SomeClassImplementingSomeInterface", + "java.lang.Object+"); + + assertMatch("example.type.AspectJTypeFilterTestsTypes$SomeClassExtendingSomeClassExtendingSomeClassAndImplementingSomeInterface", + "example.type.AspectJTypeFilterTestsTypes.SomeInterface+"); + assertMatch("example.type.AspectJTypeFilterTestsTypes$SomeClassExtendingSomeClassExtendingSomeClassAndImplementingSomeInterface", + "example.type.AspectJTypeFilterTestsTypes.SomeClassExtendingSomeClass+"); + assertMatch("example.type.AspectJTypeFilterTestsTypes$SomeClassExtendingSomeClassExtendingSomeClassAndImplementingSomeInterface", + "example.type.AspectJTypeFilterTestsTypes.SomeClass+"); + assertMatch("example.type.AspectJTypeFilterTestsTypes$SomeClassExtendingSomeClassExtendingSomeClassAndImplementingSomeInterface", + "*+"); + assertMatch("example.type.AspectJTypeFilterTestsTypes$SomeClassExtendingSomeClassExtendingSomeClassAndImplementingSomeInterface", + "java.lang.Object+"); + } + + @Test + void subclassPatternNoMatches() throws Exception { + assertNoMatch("example.type.AspectJTypeFilterTestsTypes$SomeClassExtendingSomeClass", + "java.lang.String+"); + } + + @Test + void annotationPatternMatches() throws Exception { + assertMatch("example.type.AspectJTypeFilterTestsTypes$SomeClassAnnotatedWithComponent", + "@org.springframework.core.testfixture.stereotype.Component *..*"); + assertMatch("example.type.AspectJTypeFilterTestsTypes$SomeClassAnnotatedWithComponent", + "@* *..*"); + assertMatch("example.type.AspectJTypeFilterTestsTypes$SomeClassAnnotatedWithComponent", + "@*..* *..*"); + assertMatch("example.type.AspectJTypeFilterTestsTypes$SomeClassAnnotatedWithComponent", + "@*..*Component *..*"); + assertMatch("example.type.AspectJTypeFilterTestsTypes$SomeClassAnnotatedWithComponent", + "@org.springframework.core.testfixture.stereotype.Component *..*Component"); + assertMatch("example.type.AspectJTypeFilterTestsTypes$SomeClassAnnotatedWithComponent", + "@org.springframework.core.testfixture.stereotype.Component *"); + } + + @Test + void annotationPatternNoMatches() throws Exception { + assertNoMatch("example.type.AspectJTypeFilterTestsTypes$SomeClassAnnotatedWithComponent", + "@org.springframework.stereotype.Repository *..*"); + } + + @Test + void compositionPatternMatches() throws Exception { + assertMatch("example.type.AspectJTypeFilterTestsTypes$SomeClass", + "!*..SomeOtherClass"); + assertMatch("example.type.AspectJTypeFilterTestsTypes$SomeClassExtendingSomeClassExtendingSomeClassAndImplementingSomeInterface", + "example.type.AspectJTypeFilterTestsTypes.SomeInterface+ " + + "&& example.type.AspectJTypeFilterTestsTypes.SomeClass+ " + + "&& example.type.AspectJTypeFilterTestsTypes.SomeClassExtendingSomeClass+"); + assertMatch("example.type.AspectJTypeFilterTestsTypes$SomeClassExtendingSomeClassExtendingSomeClassAndImplementingSomeInterface", + "example.type.AspectJTypeFilterTestsTypes.SomeInterface+ " + + "|| example.type.AspectJTypeFilterTestsTypes.SomeClass+ " + + "|| example.type.AspectJTypeFilterTestsTypes.SomeClassExtendingSomeClass+"); + } + + @Test + void compositionPatternNoMatches() throws Exception { + assertNoMatch("example.type.AspectJTypeFilterTestsTypes$SomeClass", + "*..Bogus && example.type.AspectJTypeFilterTestsTypes.SomeClass"); + } + + private void assertMatch(String type, String typePattern) throws Exception { + MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(type); + + AspectJTypeFilter filter = new AspectJTypeFilter(typePattern, getClass().getClassLoader()); + assertThat(filter.match(metadataReader, metadataReaderFactory)).isTrue(); + ClassloadingAssertions.assertClassNotLoaded(type); + } + + private void assertNoMatch(String type, String typePattern) throws Exception { + MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(type); + + AspectJTypeFilter filter = new AspectJTypeFilter(typePattern, getClass().getClassLoader()); + assertThat(filter.match(metadataReader, metadataReaderFactory)).isFalse(); + ClassloadingAssertions.assertClassNotLoaded(type); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/type/AssignableTypeFilterTests.java b/spring-core/src/test/java/org/springframework/core/type/AssignableTypeFilterTests.java new file mode 100644 index 0000000..757371a --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/type/AssignableTypeFilterTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; +import org.springframework.core.type.filter.AssignableTypeFilter; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Ramnivas Laddad + * @author Juergen Hoeller + */ +class AssignableTypeFilterTests { + + @Test + void directMatch() throws Exception { + MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + String classUnderTest = "example.type.AssignableTypeFilterTestsTypes$TestNonInheritingClass"; + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(classUnderTest); + + AssignableTypeFilter matchingFilter = new AssignableTypeFilter(example.type.AssignableTypeFilterTestsTypes.TestNonInheritingClass.class); + AssignableTypeFilter notMatchingFilter = new AssignableTypeFilter(example.type.AssignableTypeFilterTestsTypes.TestInterface.class); + assertThat(notMatchingFilter.match(metadataReader, metadataReaderFactory)).isFalse(); + assertThat(matchingFilter.match(metadataReader, metadataReaderFactory)).isTrue(); + } + + @Test + void interfaceMatch() throws Exception { + MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + String classUnderTest = "example.type.AssignableTypeFilterTestsTypes$TestInterfaceImpl"; + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(classUnderTest); + + AssignableTypeFilter filter = new AssignableTypeFilter(example.type.AssignableTypeFilterTestsTypes.TestInterface.class); + assertThat(filter.match(metadataReader, metadataReaderFactory)).isTrue(); + ClassloadingAssertions.assertClassNotLoaded(classUnderTest); + } + + @Test + void superClassMatch() throws Exception { + MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + String classUnderTest = "example.type.AssignableTypeFilterTestsTypes$SomeDaoLikeImpl"; + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(classUnderTest); + + AssignableTypeFilter filter = new AssignableTypeFilter(example.type.AssignableTypeFilterTestsTypes.SimpleJdbcDaoSupport.class); + assertThat(filter.match(metadataReader, metadataReaderFactory)).isTrue(); + ClassloadingAssertions.assertClassNotLoaded(classUnderTest); + } + + @Test + void interfaceThroughSuperClassMatch() throws Exception { + MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + String classUnderTest = "example.type.AssignableTypeFilterTestsTypes$SomeDaoLikeImpl"; + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(classUnderTest); + + AssignableTypeFilter filter = new AssignableTypeFilter(example.type.AssignableTypeFilterTestsTypes.JdbcDaoSupport.class); + assertThat(filter.match(metadataReader, metadataReaderFactory)).isTrue(); + ClassloadingAssertions.assertClassNotLoaded(classUnderTest); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/type/CachingMetadataReaderLeakTests.java b/spring-core/src/test/java/org/springframework/core/type/CachingMetadataReaderLeakTests.java new file mode 100644 index 0000000..89421fe --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/type/CachingMetadataReaderLeakTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type; + +import java.net.URL; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; +import org.springframework.core.testfixture.EnabledForTestGroups; +import org.springframework.core.type.classreading.CachingMetadataReaderFactory; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; + +/** + * Unit tests for checking the behaviour of {@link CachingMetadataReaderFactory} under + * load. If the cache is not controlled, this test should fail with an out of memory + * exception around entry 5k. + * + * @author Costin Leau + * @author Sam Brannen + */ +@EnabledForTestGroups(LONG_RUNNING) +class CachingMetadataReaderLeakTests { + + private static final int ITEMS_TO_LOAD = 9999; + + private final MetadataReaderFactory mrf = new CachingMetadataReaderFactory(); + + @Test + void significantLoad() throws Exception { + // the biggest public class in the JDK (>60k) + URL url = getClass().getResource("/java/awt/Component.class"); + assertThat(url).isNotNull(); + + // look at a LOT of items + for (int i = 0; i < ITEMS_TO_LOAD; i++) { + Resource resource = new UrlResource(url) { + + @Override + public boolean equals(Object obj) { + return (obj == this); + } + + @Override + public int hashCode() { + return System.identityHashCode(this); + } + }; + + MetadataReader reader = mrf.getMetadataReader(resource); + assertThat(reader).isNotNull(); + } + + // useful for profiling to take snapshots + // System.in.read(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/type/ClassloadingAssertions.java b/spring-core/src/test/java/org/springframework/core/type/ClassloadingAssertions.java new file mode 100644 index 0000000..8c26fbc --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/type/ClassloadingAssertions.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type; + +import java.lang.reflect.Method; + +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Ramnivas Laddad + * @author Sam Brannen + */ +abstract class ClassloadingAssertions { + + private static boolean isClassLoaded(String className) { + ClassLoader cl = ClassUtils.getDefaultClassLoader(); + Method findLoadedClassMethod = ReflectionUtils.findMethod(cl.getClass(), "findLoadedClass", String.class); + ReflectionUtils.makeAccessible(findLoadedClassMethod); + Class loadedClass = (Class) ReflectionUtils.invokeMethod(findLoadedClassMethod, cl, className); + return loadedClass != null; + } + + public static void assertClassNotLoaded(String className) { + assertThat(isClassLoaded(className)).as("Class [" + className + "] should not have been loaded").isFalse(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/type/InheritedAnnotationsAnnotationMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/InheritedAnnotationsAnnotationMetadataTests.java new file mode 100644 index 0000000..3255676 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/type/InheritedAnnotationsAnnotationMetadataTests.java @@ -0,0 +1,188 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Map; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; +import org.springframework.util.MultiValueMap; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests demonstrating that the reflection-based {@link StandardAnnotationMetadata} + * supports {@link Inherited @Inherited} annotations; whereas, the ASM-based + * {@code SimpleAnnotationMetadata} does not. + * + * @author Sam Brannen + * @since 5.2.3 + * @see AnnotationMetadataTests + */ +class InheritedAnnotationsAnnotationMetadataTests { + + private final AnnotationMetadata standardMetadata = AnnotationMetadata.introspect(AnnotatedSubclass.class); + + private final AnnotationMetadata asmMetadata; + + + InheritedAnnotationsAnnotationMetadataTests() throws Exception { + MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(AnnotatedSubclass.class.getName()); + this.asmMetadata = metadataReader.getAnnotationMetadata(); + } + + @Test + void getAnnotationTypes() { + assertThat(standardMetadata.getAnnotationTypes()).containsExactlyInAnyOrder( + NamedAnnotation3.class.getName(), + InheritedComposedAnnotation.class.getName()); + + assertThat(asmMetadata.getAnnotationTypes()).containsExactly( + NamedAnnotation3.class.getName()); + } + + @Test + void hasAnnotation() { + assertThat(standardMetadata.hasAnnotation(InheritedComposedAnnotation.class.getName())).isTrue(); + assertThat(standardMetadata.hasAnnotation(NamedAnnotation3.class.getName())).isTrue(); + + // true because @NamedAnnotation3 is also directly present + assertThat(asmMetadata.hasAnnotation(NamedAnnotation3.class.getName())).isTrue(); + + assertThat(asmMetadata.hasAnnotation(InheritedComposedAnnotation.class.getName())).isFalse(); + } + + @Test + void getMetaAnnotationTypes() { + Set metaAnnotationTypes; + + metaAnnotationTypes = standardMetadata.getMetaAnnotationTypes(InheritedComposedAnnotation.class.getName()); + assertThat(metaAnnotationTypes).containsExactlyInAnyOrder( + MetaAnnotation.class.getName(), + NamedAnnotation1.class.getName(), + NamedAnnotation2.class.getName(), + NamedAnnotation3.class.getName()); + + metaAnnotationTypes = asmMetadata.getMetaAnnotationTypes(InheritedComposedAnnotation.class.getName()); + assertThat(metaAnnotationTypes).isEmpty(); + } + + @Test + void hasMetaAnnotation() { + assertThat(standardMetadata.hasMetaAnnotation(NamedAnnotation1.class.getName())).isTrue(); + assertThat(standardMetadata.hasMetaAnnotation(NamedAnnotation2.class.getName())).isTrue(); + assertThat(standardMetadata.hasMetaAnnotation(NamedAnnotation3.class.getName())).isTrue(); + assertThat(standardMetadata.hasMetaAnnotation(MetaAnnotation.class.getName())).isTrue(); + + assertThat(asmMetadata.hasMetaAnnotation(NamedAnnotation1.class.getName())).isFalse(); + assertThat(asmMetadata.hasMetaAnnotation(NamedAnnotation2.class.getName())).isFalse(); + assertThat(asmMetadata.hasMetaAnnotation(NamedAnnotation3.class.getName())).isFalse(); + assertThat(asmMetadata.hasMetaAnnotation(MetaAnnotation.class.getName())).isFalse(); + } + + @Test + void isAnnotated() { + assertThat(standardMetadata.isAnnotated(InheritedComposedAnnotation.class.getName())).isTrue(); + assertThat(standardMetadata.isAnnotated(NamedAnnotation1.class.getName())).isTrue(); + assertThat(standardMetadata.isAnnotated(NamedAnnotation2.class.getName())).isTrue(); + assertThat(standardMetadata.isAnnotated(NamedAnnotation3.class.getName())).isTrue(); + assertThat(standardMetadata.isAnnotated(MetaAnnotation.class.getName())).isTrue(); + + // true because @NamedAnnotation3 is also directly present + assertThat(asmMetadata.isAnnotated(NamedAnnotation3.class.getName())).isTrue(); + + assertThat(asmMetadata.isAnnotated(InheritedComposedAnnotation.class.getName())).isFalse(); + assertThat(asmMetadata.isAnnotated(NamedAnnotation1.class.getName())).isFalse(); + assertThat(asmMetadata.isAnnotated(NamedAnnotation2.class.getName())).isFalse(); + assertThat(asmMetadata.isAnnotated(MetaAnnotation.class.getName())).isFalse(); + } + + @Test + void getAnnotationAttributes() { + Map annotationAttributes; + + annotationAttributes = standardMetadata.getAnnotationAttributes(NamedAnnotation1.class.getName()); + assertThat(annotationAttributes.get("name")).isEqualTo("name 1"); + + annotationAttributes = asmMetadata.getAnnotationAttributes(NamedAnnotation1.class.getName()); + assertThat(annotationAttributes).isNull(); + } + + @Test + void getAllAnnotationAttributes() { + MultiValueMap annotationAttributes; + + annotationAttributes = standardMetadata.getAllAnnotationAttributes(NamedAnnotation3.class.getName()); + assertThat(annotationAttributes).containsKey("name"); + assertThat(annotationAttributes.get("name")).containsExactlyInAnyOrder("name 3", "local"); + + annotationAttributes = asmMetadata.getAllAnnotationAttributes(NamedAnnotation1.class.getName()); + assertThat(annotationAttributes).isNull(); + } + + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.ANNOTATION_TYPE) + @interface MetaAnnotation { + } + + @MetaAnnotation + @Retention(RetentionPolicy.RUNTIME) + @interface NamedAnnotation1 { + + String name() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface NamedAnnotation2 { + + String name() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface NamedAnnotation3 { + + String name() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @NamedAnnotation1(name = "name 1") + @NamedAnnotation2(name = "name 2") + @NamedAnnotation3(name = "name 3") + @Inherited + @interface InheritedComposedAnnotation { + } + + @InheritedComposedAnnotation + private static class AnnotatedClass { + } + + @NamedAnnotation3(name = "local") + private static class AnnotatedSubclass extends AnnotatedClass { + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/type/Scope.java b/spring-core/src/test/java/org/springframework/core/type/Scope.java new file mode 100644 index 0000000..eb58192 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/type/Scope.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2009 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Copy of the {@code @Scope} annotation for testing purposes. + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface Scope { + + String value() default "singleton"; + +} diff --git a/spring-core/src/test/java/org/springframework/core/type/StandardAnnotationMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/StandardAnnotationMetadataTests.java new file mode 100644 index 0000000..e545ac7 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/type/StandardAnnotationMetadataTests.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type; + +/** + * Tests for {@link StandardAnnotationMetadata}. + * + * @author Phillip Webb + */ +class StandardAnnotationMetadataTests extends AbstractAnnotationMetadataTests { + + @Override + @SuppressWarnings("deprecation") + protected AnnotationMetadata get(Class source) { + return new StandardAnnotationMetadata(source); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/type/StandardClassMetadataMemberClassTests.java b/spring-core/src/test/java/org/springframework/core/type/StandardClassMetadataMemberClassTests.java new file mode 100644 index 0000000..b8fd64a --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/type/StandardClassMetadataMemberClassTests.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type; + +/** + * @author Chris Beams + * @since 3.1 + * @see AbstractClassMetadataMemberClassTests + */ +class StandardClassMetadataMemberClassTests extends AbstractClassMetadataMemberClassTests { + + @Override + @SuppressWarnings("deprecation") + public ClassMetadata getClassMetadataFor(Class clazz) { + return new StandardClassMetadata(clazz); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/type/StandardMethodMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/StandardMethodMetadataTests.java new file mode 100644 index 0000000..950d6a4 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/type/StandardMethodMetadataTests.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type; + +/** + * Tests for {@link StandardMethodMetadata}. + * + * @author Phillip Webb + */ +class StandardMethodMetadataTests extends AbstractMethodMetadataTests { + + @Override + protected AnnotationMetadata get(Class source) { + return AnnotationMetadata.introspect(source); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/type/TestAutowired.java b/spring-core/src/test/java/org/springframework/core/type/TestAutowired.java new file mode 100644 index 0000000..bf9ec43 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/type/TestAutowired.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD}) +public @interface TestAutowired { + + /** + * Declares whether the annotated dependency is required. + *

    Defaults to {@code true}. + */ + boolean required() default true; + +} diff --git a/spring-core/src/test/java/org/springframework/core/type/TestQualifier.java b/spring-core/src/test/java/org/springframework/core/type/TestQualifier.java new file mode 100644 index 0000000..321ec43 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/type/TestQualifier.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) +@Inherited +@Documented +public @interface TestQualifier { + + String value() default ""; + +} diff --git a/spring-core/src/test/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitorTests.java b/spring-core/src/test/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitorTests.java new file mode 100644 index 0000000..fc34076 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitorTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.classreading; + +import java.io.BufferedInputStream; +import java.io.InputStream; + +import org.junit.jupiter.api.Test; + +import org.springframework.asm.ClassReader; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.AbstractAnnotationMetadataTests; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link AnnotationMetadataReadingVisitor}. + * + * @author Phillip Webb + */ +@SuppressWarnings("deprecation") +class AnnotationMetadataReadingVisitorTests extends AbstractAnnotationMetadataTests { + + @Override + protected AnnotationMetadata get(Class source) { + try { + ClassLoader classLoader = source.getClassLoader(); + String className = source.getName(); + String resourcePath = ResourceLoader.CLASSPATH_URL_PREFIX + + ClassUtils.convertClassNameToResourcePath(className) + + ClassUtils.CLASS_FILE_SUFFIX; + Resource resource = new DefaultResourceLoader().getResource(resourcePath); + try (InputStream inputStream = new BufferedInputStream( + resource.getInputStream())) { + ClassReader classReader = new ClassReader(inputStream); + AnnotationMetadataReadingVisitor metadata = new AnnotationMetadataReadingVisitor( + classLoader); + classReader.accept(metadata, ClassReader.SKIP_DEBUG); + return metadata; + } + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + + @Override + @Test + public void getAnnotationsReturnsDirectAnnotations() { + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy( + super::getAnnotationsReturnsDirectAnnotations); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/type/classreading/ClassMetadataReadingVisitorMemberClassTests.java b/spring-core/src/test/java/org/springframework/core/type/classreading/ClassMetadataReadingVisitorMemberClassTests.java new file mode 100644 index 0000000..cdb594f --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/type/classreading/ClassMetadataReadingVisitorMemberClassTests.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2011 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.classreading; + +import java.io.IOException; + +import org.springframework.core.type.AbstractClassMetadataMemberClassTests; +import org.springframework.core.type.ClassMetadata; + +/** + * @author Chris Beams + * @since 3.1 + * @see AbstractClassMetadataMemberClassTests + */ +class ClassMetadataReadingVisitorMemberClassTests extends AbstractClassMetadataMemberClassTests { + + @Override + public ClassMetadata getClassMetadataFor(Class clazz) { + try { + MetadataReader reader = + new SimpleMetadataReaderFactory().getMetadataReader(clazz.getName()); + return reader.getAnnotationMetadata(); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/type/classreading/MergedAnnotationMetadataVisitorTests.java b/spring-core/src/test/java/org/springframework/core/type/classreading/MergedAnnotationMetadataVisitorTests.java new file mode 100644 index 0000000..9c68a8c --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/type/classreading/MergedAnnotationMetadataVisitorTests.java @@ -0,0 +1,264 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.classreading; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.jupiter.api.Test; + +import org.springframework.asm.AnnotationVisitor; +import org.springframework.asm.ClassReader; +import org.springframework.asm.ClassVisitor; +import org.springframework.asm.SpringAsmInfo; +import org.springframework.core.annotation.MergedAnnotation; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MergedAnnotationReadingVisitor}. + * + * @author Phillip Webb + */ +class MergedAnnotationMetadataVisitorTests { + + private MergedAnnotation annotation; + + @Test + void visitWhenHasSimpleTypesCreatesAnnotation() { + loadFrom(WithSimpleTypesAnnotation.class); + assertThat(this.annotation.getType()).isEqualTo(SimpleTypesAnnotation.class); + assertThat(this.annotation.getValue("stringValue")).contains("string"); + assertThat(this.annotation.getValue("byteValue")).contains((byte) 1); + assertThat(this.annotation.getValue("shortValue")).contains((short) 2); + assertThat(this.annotation.getValue("intValue")).contains(3); + assertThat(this.annotation.getValue("longValue")).contains(4L); + assertThat(this.annotation.getValue("booleanValue")).contains(true); + assertThat(this.annotation.getValue("charValue")).contains('c'); + assertThat(this.annotation.getValue("doubleValue")).contains(5.0); + assertThat(this.annotation.getValue("floatValue")).contains(6.0f); + } + + @Test + void visitWhenHasSimpleArrayTypesCreatesAnnotation() { + loadFrom(WithSimpleArrayTypesAnnotation.class); + assertThat(this.annotation.getType()).isEqualTo(SimpleArrayTypesAnnotation.class); + assertThat(this.annotation.getValue("stringValue")).contains( + new String[] { "string" }); + assertThat(this.annotation.getValue("byteValue")).contains(new byte[] { 1 }); + assertThat(this.annotation.getValue("shortValue")).contains(new short[] { 2 }); + assertThat(this.annotation.getValue("intValue")).contains(new int[] { 3 }); + assertThat(this.annotation.getValue("longValue")).contains(new long[] { 4 }); + assertThat(this.annotation.getValue("booleanValue")).contains( + new boolean[] { true }); + assertThat(this.annotation.getValue("charValue")).contains(new char[] { 'c' }); + assertThat(this.annotation.getValue("doubleValue")).contains( + new double[] { 5.0 }); + assertThat(this.annotation.getValue("floatValue")).contains(new float[] { 6.0f }); + } + + @Test + void visitWhenHasEmptySimpleArrayTypesCreatesAnnotation() { + loadFrom(WithSimpleEmptyArrayTypesAnnotation.class); + assertThat(this.annotation.getType()).isEqualTo(SimpleArrayTypesAnnotation.class); + assertThat(this.annotation.getValue("stringValue")).contains(new String[] {}); + assertThat(this.annotation.getValue("byteValue")).contains(new byte[] {}); + assertThat(this.annotation.getValue("shortValue")).contains(new short[] {}); + assertThat(this.annotation.getValue("intValue")).contains(new int[] {}); + assertThat(this.annotation.getValue("longValue")).contains(new long[] {}); + assertThat(this.annotation.getValue("booleanValue")).contains(new boolean[] {}); + assertThat(this.annotation.getValue("charValue")).contains(new char[] {}); + assertThat(this.annotation.getValue("doubleValue")).contains(new double[] {}); + assertThat(this.annotation.getValue("floatValue")).contains(new float[] {}); + } + + @Test + void visitWhenHasEnumAttributesCreatesAnnotation() { + loadFrom(WithEnumAnnotation.class); + assertThat(this.annotation.getType()).isEqualTo(EnumAnnotation.class); + assertThat(this.annotation.getValue("enumValue")).contains(ExampleEnum.ONE); + assertThat(this.annotation.getValue("enumArrayValue")).contains( + new ExampleEnum[] { ExampleEnum.ONE, ExampleEnum.TWO }); + } + + @Test + void visitWhenHasAnnotationAttributesCreatesAnnotation() { + loadFrom(WithAnnotationAnnotation.class); + assertThat(this.annotation.getType()).isEqualTo(AnnotationAnnotation.class); + MergedAnnotation value = this.annotation.getAnnotation( + "annotationValue", NestedAnnotation.class); + assertThat(value.isPresent()).isTrue(); + assertThat(value.getString(MergedAnnotation.VALUE)).isEqualTo("a"); + MergedAnnotation[] arrayValue = this.annotation.getAnnotationArray( + "annotationArrayValue", NestedAnnotation.class); + assertThat(arrayValue).hasSize(2); + assertThat(arrayValue[0].getString(MergedAnnotation.VALUE)).isEqualTo("b"); + assertThat(arrayValue[1].getString(MergedAnnotation.VALUE)).isEqualTo("c"); + } + + @Test + void visitWhenHasClassAttributesCreatesAnnotation() { + loadFrom(WithClassAnnotation.class); + assertThat(this.annotation.getType()).isEqualTo(ClassAnnotation.class); + assertThat(this.annotation.getString("classValue")).isEqualTo(InputStream.class.getName()); + assertThat(this.annotation.getClass("classValue")).isEqualTo(InputStream.class); + assertThat(this.annotation.getValue("classValue")).contains(InputStream.class); + assertThat(this.annotation.getStringArray("classArrayValue")).containsExactly(OutputStream.class.getName()); + assertThat(this.annotation.getValue("classArrayValue")).contains(new Class[] {OutputStream.class}); + } + + private void loadFrom(Class type) { + ClassVisitor visitor = new ClassVisitor(SpringAsmInfo.ASM_VERSION) { + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + return MergedAnnotationReadingVisitor.get(getClass().getClassLoader(), + null, descriptor, visible, + annotation -> MergedAnnotationMetadataVisitorTests.this.annotation = annotation); + } + + }; + try { + new ClassReader(type.getName()).accept(visitor, ClassReader.SKIP_DEBUG + | ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + @SimpleTypesAnnotation(stringValue = "string", byteValue = 1, shortValue = 2, intValue = 3, longValue = 4, booleanValue = true, charValue = 'c', doubleValue = 5.0, floatValue = 6.0f) + static class WithSimpleTypesAnnotation { + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface SimpleTypesAnnotation { + + String stringValue(); + + byte byteValue(); + + short shortValue(); + + int intValue(); + + long longValue(); + + boolean booleanValue(); + + char charValue(); + + double doubleValue(); + + float floatValue(); + + } + + @SimpleArrayTypesAnnotation(stringValue = "string", byteValue = 1, shortValue = 2, intValue = 3, longValue = 4, booleanValue = true, charValue = 'c', doubleValue = 5.0, floatValue = 6.0f) + static class WithSimpleArrayTypesAnnotation { + + } + + @SimpleArrayTypesAnnotation(stringValue = {}, byteValue = {}, shortValue = {}, intValue = {}, longValue = {}, booleanValue = {}, charValue = {}, doubleValue = {}, floatValue = {}) + static class WithSimpleEmptyArrayTypesAnnotation { + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface SimpleArrayTypesAnnotation { + + String[] stringValue(); + + byte[] byteValue(); + + short[] shortValue(); + + int[] intValue(); + + long[] longValue(); + + boolean[] booleanValue(); + + char[] charValue(); + + double[] doubleValue(); + + float[] floatValue(); + + } + + @EnumAnnotation(enumValue = ExampleEnum.ONE, enumArrayValue = { ExampleEnum.ONE, + ExampleEnum.TWO }) + static class WithEnumAnnotation { + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface EnumAnnotation { + + ExampleEnum enumValue(); + + ExampleEnum[] enumArrayValue(); + + } + + enum ExampleEnum { + ONE, TWO, THREE + } + + @AnnotationAnnotation(annotationValue = @NestedAnnotation("a"), annotationArrayValue = { + @NestedAnnotation("b"), @NestedAnnotation("c") }) + static class WithAnnotationAnnotation { + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AnnotationAnnotation { + + NestedAnnotation annotationValue(); + + NestedAnnotation[] annotationArrayValue(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface NestedAnnotation { + + String value() default ""; + + } + + @ClassAnnotation(classValue = InputStream.class, classArrayValue = OutputStream.class) + static class WithClassAnnotation { + + } + + + @Retention(RetentionPolicy.RUNTIME) + @interface ClassAnnotation { + + Class classValue(); + + Class[] classArrayValue(); + + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitorTests.java b/spring-core/src/test/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitorTests.java new file mode 100644 index 0000000..0ed4ed6 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitorTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.classreading; + +import java.io.BufferedInputStream; +import java.io.InputStream; + +import org.junit.jupiter.api.Test; + +import org.springframework.asm.ClassReader; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.AbstractMethodMetadataTests; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link MethodMetadataReadingVisitor}. + * + * @author Phillip Webb + */ +@SuppressWarnings("deprecation") +class MethodMetadataReadingVisitorTests extends AbstractMethodMetadataTests { + + @Override + protected AnnotationMetadata get(Class source) { + try { + ClassLoader classLoader = source.getClassLoader(); + String className = source.getName(); + String resourcePath = ResourceLoader.CLASSPATH_URL_PREFIX + + ClassUtils.convertClassNameToResourcePath(className) + + ClassUtils.CLASS_FILE_SUFFIX; + Resource resource = new DefaultResourceLoader().getResource(resourcePath); + try (InputStream inputStream = new BufferedInputStream( + resource.getInputStream())) { + ClassReader classReader = new ClassReader(inputStream); + AnnotationMetadataReadingVisitor metadata = new AnnotationMetadataReadingVisitor( + classLoader); + classReader.accept(metadata, ClassReader.SKIP_DEBUG); + return metadata; + } + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + + @Override + @Test + public void getAnnotationsReturnsDirectAnnotations() { + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy( + super::getAnnotationsReturnsDirectAnnotations); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/type/classreading/SimpleAnnotationMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/classreading/SimpleAnnotationMetadataTests.java new file mode 100644 index 0000000..f66c81b --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/type/classreading/SimpleAnnotationMetadataTests.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.classreading; + +import org.springframework.core.type.AbstractAnnotationMetadataTests; +import org.springframework.core.type.AnnotationMetadata; + +/** + * Tests for {@link SimpleAnnotationMetadata} and + * {@link SimpleAnnotationMetadataReadingVisitor}. + * + * @author Phillip Webb + */ +class SimpleAnnotationMetadataTests extends AbstractAnnotationMetadataTests { + + @Override + protected AnnotationMetadata get(Class source) { + try { + return new SimpleMetadataReaderFactory( + source.getClassLoader()).getMetadataReader( + source.getName()).getAnnotationMetadata(); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/type/classreading/SimpleMethodMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/classreading/SimpleMethodMetadataTests.java new file mode 100644 index 0000000..4afd423 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/type/classreading/SimpleMethodMetadataTests.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.type.classreading; + +import org.springframework.core.type.AbstractMethodMetadataTests; +import org.springframework.core.type.AnnotationMetadata; + +/** + * Tests for {@link SimpleMethodMetadata} and + * {@link SimpleMethodMetadataReadingVisitor}. + * + * @author Phillip Webb + */ +class SimpleMethodMetadataTests extends AbstractMethodMetadataTests { + + @Override + protected AnnotationMetadata get(Class source) { + try { + return new SimpleMetadataReaderFactory( + source.getClassLoader()).getMetadataReader( + source.getName()).getAnnotationMetadata(); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/tests/MockitoUtils.java b/spring-core/src/test/java/org/springframework/tests/MockitoUtils.java new file mode 100644 index 0000000..60df908 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/tests/MockitoUtils.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.tests; + +import java.util.List; + +import org.mockito.Mockito; +import org.mockito.internal.stubbing.InvocationContainerImpl; +import org.mockito.internal.util.MockUtil; +import org.mockito.invocation.Invocation; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * General test utilities for use with {@link Mockito}. + * + * @author Phillip Webb + */ +public abstract class MockitoUtils { + + /** + * Verify the same invocations have been applied to two mocks. This is generally not + * the preferred way test with mockito and should be avoided if possible. + * @param expected the mock containing expected invocations + * @param actual the mock containing actual invocations + * @param argumentAdapters adapters that can be used to change argument values before they are compared + */ + public static void verifySameInvocations(T expected, T actual, InvocationArgumentsAdapter... argumentAdapters) { + List expectedInvocations = + ((InvocationContainerImpl) MockUtil.getMockHandler(expected).getInvocationContainer()).getInvocations(); + List actualInvocations = + ((InvocationContainerImpl) MockUtil.getMockHandler(actual).getInvocationContainer()).getInvocations(); + verifySameInvocations(expectedInvocations, actualInvocations, argumentAdapters); + } + + private static void verifySameInvocations(List expectedInvocations, List actualInvocations, + InvocationArgumentsAdapter... argumentAdapters) { + + assertThat(expectedInvocations.size()).isEqualTo(actualInvocations.size()); + for (int i = 0; i < expectedInvocations.size(); i++) { + verifySameInvocation(expectedInvocations.get(i), actualInvocations.get(i), argumentAdapters); + } + } + + private static void verifySameInvocation(Invocation expectedInvocation, Invocation actualInvocation, + InvocationArgumentsAdapter... argumentAdapters) { + + assertThat(expectedInvocation.getMethod()).isEqualTo(actualInvocation.getMethod()); + Object[] expectedArguments = getInvocationArguments(expectedInvocation, argumentAdapters); + Object[] actualArguments = getInvocationArguments(actualInvocation, argumentAdapters); + assertThat(expectedArguments).isEqualTo(actualArguments); + } + + private static Object[] getInvocationArguments(Invocation invocation, InvocationArgumentsAdapter... argumentAdapters) { + Object[] arguments = invocation.getArguments(); + for (InvocationArgumentsAdapter adapter : argumentAdapters) { + arguments = adapter.adaptArguments(arguments); + } + return arguments; + } + + + /** + * Adapter strategy that can be used to change invocation arguments. + */ + public interface InvocationArgumentsAdapter { + + /** + * Change the arguments if required. + * @param arguments the source arguments + * @return updated or original arguments (never {@code null}) + */ + Object[] adaptArguments(Object[] arguments); + } + +} diff --git a/spring-core/src/test/java/org/springframework/tests/package-info.java b/spring-core/src/test/java/org/springframework/tests/package-info.java new file mode 100644 index 0000000..cd59135 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/tests/package-info.java @@ -0,0 +1,6 @@ +/** + * Shared utilities that are used internally throughout the test suite but are not + * published. This package should not be confused with {@code org.springframework.test} + * which contains published code from the 'spring-test' module. + */ +package org.springframework.tests; diff --git a/spring-core/src/test/java/org/springframework/tests/sample/objects/DerivedTestObject.java b/spring-core/src/test/java/org/springframework/tests/sample/objects/DerivedTestObject.java new file mode 100644 index 0000000..6a04f88 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/tests/sample/objects/DerivedTestObject.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.tests.sample.objects; + +import java.io.Serializable; + +@SuppressWarnings("serial") +public class DerivedTestObject extends TestObject implements Serializable { + +} diff --git a/spring-core/src/test/java/org/springframework/tests/sample/objects/GenericObject.java b/spring-core/src/test/java/org/springframework/tests/sample/objects/GenericObject.java new file mode 100644 index 0000000..ef55a7a --- /dev/null +++ b/spring-core/src/test/java/org/springframework/tests/sample/objects/GenericObject.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.tests.sample.objects; + +import java.util.List; + +import org.springframework.core.io.Resource; + +public class GenericObject { + + private List resourceList; + + public List getResourceList() { + return this.resourceList; + } + + public void setResourceList(List resourceList) { + this.resourceList = resourceList; + } + +} diff --git a/spring-core/src/test/java/org/springframework/tests/sample/objects/ITestInterface.java b/spring-core/src/test/java/org/springframework/tests/sample/objects/ITestInterface.java new file mode 100644 index 0000000..a8c55df --- /dev/null +++ b/spring-core/src/test/java/org/springframework/tests/sample/objects/ITestInterface.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.tests.sample.objects; + + +public interface ITestInterface { + + void absquatulate(); + +} diff --git a/spring-core/src/test/java/org/springframework/tests/sample/objects/ITestObject.java b/spring-core/src/test/java/org/springframework/tests/sample/objects/ITestObject.java new file mode 100644 index 0000000..f7f888e --- /dev/null +++ b/spring-core/src/test/java/org/springframework/tests/sample/objects/ITestObject.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.tests.sample.objects; + +public interface ITestObject { + + String getName(); + + void setName(String name); + + int getAge(); + + void setAge(int age); + + TestObject getSpouse(); + + void setSpouse(TestObject spouse); + +} diff --git a/spring-core/src/test/java/org/springframework/tests/sample/objects/TestObject.java b/spring-core/src/test/java/org/springframework/tests/sample/objects/TestObject.java new file mode 100644 index 0000000..e36283a --- /dev/null +++ b/spring-core/src/test/java/org/springframework/tests/sample/objects/TestObject.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.tests.sample.objects; + +public class TestObject implements ITestObject, ITestInterface, Comparable { + + private String name; + + private int age; + + private TestObject spouse; + + public TestObject() { + } + + public TestObject(String name, int age) { + this.name = name; + this.age = age; + } + + @Override + public String getName() { + return name; + } + + @Override + public void setName(String name) { + this.name = name; + } + + @Override + public int getAge() { + return this.age; + } + + @Override + public void setAge(int age) { + this.age = age; + } + + @Override + public TestObject getSpouse() { + return this.spouse; + } + + @Override + public void setSpouse(TestObject spouse) { + this.spouse = spouse; + } + + @Override + public void absquatulate() { + } + + @Override + public int compareTo(Object o) { + if (this.name != null && o instanceof TestObject) { + return this.name.compareTo(((TestObject) o).getName()); + } + else { + return 1; + } + } +} diff --git a/spring-core/src/test/java/org/springframework/tests/sample/objects/package-info.java b/spring-core/src/test/java/org/springframework/tests/sample/objects/package-info.java new file mode 100644 index 0000000..98c15ca --- /dev/null +++ b/spring-core/src/test/java/org/springframework/tests/sample/objects/package-info.java @@ -0,0 +1,4 @@ +/** + * General purpose sample objects that can be used with tests. + */ +package org.springframework.tests.sample.objects; diff --git a/spring-core/src/test/java/org/springframework/util/AntPathMatcherTests.java b/spring-core/src/test/java/org/springframework/util/AntPathMatcherTests.java new file mode 100644 index 0000000..8bebf59 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/AntPathMatcherTests.java @@ -0,0 +1,707 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Unit tests for {@link AntPathMatcher}. + * + * @author Alef Arendsen + * @author Seth Ladd + * @author Juergen Hoeller + * @author Arjen Poutsma + * @author Rossen Stoyanchev + * @author Sam Brannen + */ +class AntPathMatcherTests { + + private final AntPathMatcher pathMatcher = new AntPathMatcher(); + + + @Test + void match() { + // test exact matching + assertThat(pathMatcher.match("test", "test")).isTrue(); + assertThat(pathMatcher.match("/test", "/test")).isTrue(); + // SPR-14141 + assertThat(pathMatcher.match("https://example.org", "https://example.org")).isTrue(); + assertThat(pathMatcher.match("/test.jpg", "test.jpg")).isFalse(); + assertThat(pathMatcher.match("test", "/test")).isFalse(); + assertThat(pathMatcher.match("/test", "test")).isFalse(); + + // test matching with ?'s + assertThat(pathMatcher.match("t?st", "test")).isTrue(); + assertThat(pathMatcher.match("??st", "test")).isTrue(); + assertThat(pathMatcher.match("tes?", "test")).isTrue(); + assertThat(pathMatcher.match("te??", "test")).isTrue(); + assertThat(pathMatcher.match("?es?", "test")).isTrue(); + assertThat(pathMatcher.match("tes?", "tes")).isFalse(); + assertThat(pathMatcher.match("tes?", "testt")).isFalse(); + assertThat(pathMatcher.match("tes?", "tsst")).isFalse(); + + // test matching with *'s + assertThat(pathMatcher.match("*", "test")).isTrue(); + assertThat(pathMatcher.match("test*", "test")).isTrue(); + assertThat(pathMatcher.match("test*", "testTest")).isTrue(); + assertThat(pathMatcher.match("test/*", "test/Test")).isTrue(); + assertThat(pathMatcher.match("test/*", "test/t")).isTrue(); + assertThat(pathMatcher.match("test/*", "test/")).isTrue(); + assertThat(pathMatcher.match("*test*", "AnothertestTest")).isTrue(); + assertThat(pathMatcher.match("*test", "Anothertest")).isTrue(); + assertThat(pathMatcher.match("*.*", "test.")).isTrue(); + assertThat(pathMatcher.match("*.*", "test.test")).isTrue(); + assertThat(pathMatcher.match("*.*", "test.test.test")).isTrue(); + assertThat(pathMatcher.match("test*aaa", "testblaaaa")).isTrue(); + assertThat(pathMatcher.match("test*", "tst")).isFalse(); + assertThat(pathMatcher.match("test*", "tsttest")).isFalse(); + assertThat(pathMatcher.match("test*", "test/")).isFalse(); + assertThat(pathMatcher.match("test*", "test/t")).isFalse(); + assertThat(pathMatcher.match("test/*", "test")).isFalse(); + assertThat(pathMatcher.match("*test*", "tsttst")).isFalse(); + assertThat(pathMatcher.match("*test", "tsttst")).isFalse(); + assertThat(pathMatcher.match("*.*", "tsttst")).isFalse(); + assertThat(pathMatcher.match("test*aaa", "test")).isFalse(); + assertThat(pathMatcher.match("test*aaa", "testblaaab")).isFalse(); + + // test matching with ?'s and /'s + assertThat(pathMatcher.match("/?", "/a")).isTrue(); + assertThat(pathMatcher.match("/?/a", "/a/a")).isTrue(); + assertThat(pathMatcher.match("/a/?", "/a/b")).isTrue(); + assertThat(pathMatcher.match("/??/a", "/aa/a")).isTrue(); + assertThat(pathMatcher.match("/a/??", "/a/bb")).isTrue(); + assertThat(pathMatcher.match("/?", "/a")).isTrue(); + + // test matching with **'s + assertThat(pathMatcher.match("/**", "/testing/testing")).isTrue(); + assertThat(pathMatcher.match("/*/**", "/testing/testing")).isTrue(); + assertThat(pathMatcher.match("/**/*", "/testing/testing")).isTrue(); + assertThat(pathMatcher.match("/bla/**/bla", "/bla/testing/testing/bla")).isTrue(); + assertThat(pathMatcher.match("/bla/**/bla", "/bla/testing/testing/bla/bla")).isTrue(); + assertThat(pathMatcher.match("/**/test", "/bla/bla/test")).isTrue(); + assertThat(pathMatcher.match("/bla/**/**/bla", "/bla/bla/bla/bla/bla/bla")).isTrue(); + assertThat(pathMatcher.match("/bla*bla/test", "/blaXXXbla/test")).isTrue(); + assertThat(pathMatcher.match("/*bla/test", "/XXXbla/test")).isTrue(); + assertThat(pathMatcher.match("/bla*bla/test", "/blaXXXbl/test")).isFalse(); + assertThat(pathMatcher.match("/*bla/test", "XXXblab/test")).isFalse(); + assertThat(pathMatcher.match("/*bla/test", "XXXbl/test")).isFalse(); + + assertThat(pathMatcher.match("/????", "/bala/bla")).isFalse(); + assertThat(pathMatcher.match("/**/*bla", "/bla/bla/bla/bbb")).isFalse(); + + assertThat(pathMatcher.match("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing/")).isTrue(); + assertThat(pathMatcher.match("/*bla*/**/bla/*", "/XXXblaXXXX/testing/testing/bla/testing")).isTrue(); + assertThat(pathMatcher.match("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing")).isTrue(); + assertThat(pathMatcher.match("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing.jpg")).isTrue(); + + assertThat(pathMatcher.match("*bla*/**/bla/**", "XXXblaXXXX/testing/testing/bla/testing/testing/")).isTrue(); + assertThat(pathMatcher.match("*bla*/**/bla/*", "XXXblaXXXX/testing/testing/bla/testing")).isTrue(); + assertThat(pathMatcher.match("*bla*/**/bla/**", "XXXblaXXXX/testing/testing/bla/testing/testing")).isTrue(); + assertThat(pathMatcher.match("*bla*/**/bla/*", "XXXblaXXXX/testing/testing/bla/testing/testing")).isFalse(); + + assertThat(pathMatcher.match("/x/x/**/bla", "/x/x/x/")).isFalse(); + + assertThat(pathMatcher.match("/foo/bar/**", "/foo/bar")).isTrue(); + + assertThat(pathMatcher.match("", "")).isTrue(); + + assertThat(pathMatcher.match("/{bla}.*", "/testing.html")).isTrue(); + assertThat(pathMatcher.match("/{bla}", "//x\ny")).isTrue(); + } + + @Test + void matchWithNullPath() { + assertThat(pathMatcher.match("/test", null)).isFalse(); + assertThat(pathMatcher.match("/", null)).isFalse(); + assertThat(pathMatcher.match(null, null)).isFalse(); + } + + // SPR-14247 + @Test + void matchWithTrimTokensEnabled() throws Exception { + pathMatcher.setTrimTokens(true); + + assertThat(pathMatcher.match("/foo/bar", "/foo /bar")).isTrue(); + } + + @Test + void matchStart() { + // test exact matching + assertThat(pathMatcher.matchStart("test", "test")).isTrue(); + assertThat(pathMatcher.matchStart("/test", "/test")).isTrue(); + assertThat(pathMatcher.matchStart("/test.jpg", "test.jpg")).isFalse(); + assertThat(pathMatcher.matchStart("test", "/test")).isFalse(); + assertThat(pathMatcher.matchStart("/test", "test")).isFalse(); + + // test matching with ?'s + assertThat(pathMatcher.matchStart("t?st", "test")).isTrue(); + assertThat(pathMatcher.matchStart("??st", "test")).isTrue(); + assertThat(pathMatcher.matchStart("tes?", "test")).isTrue(); + assertThat(pathMatcher.matchStart("te??", "test")).isTrue(); + assertThat(pathMatcher.matchStart("?es?", "test")).isTrue(); + assertThat(pathMatcher.matchStart("tes?", "tes")).isFalse(); + assertThat(pathMatcher.matchStart("tes?", "testt")).isFalse(); + assertThat(pathMatcher.matchStart("tes?", "tsst")).isFalse(); + + // test matching with *'s + assertThat(pathMatcher.matchStart("*", "test")).isTrue(); + assertThat(pathMatcher.matchStart("test*", "test")).isTrue(); + assertThat(pathMatcher.matchStart("test*", "testTest")).isTrue(); + assertThat(pathMatcher.matchStart("test/*", "test/Test")).isTrue(); + assertThat(pathMatcher.matchStart("test/*", "test/t")).isTrue(); + assertThat(pathMatcher.matchStart("test/*", "test/")).isTrue(); + assertThat(pathMatcher.matchStart("*test*", "AnothertestTest")).isTrue(); + assertThat(pathMatcher.matchStart("*test", "Anothertest")).isTrue(); + assertThat(pathMatcher.matchStart("*.*", "test.")).isTrue(); + assertThat(pathMatcher.matchStart("*.*", "test.test")).isTrue(); + assertThat(pathMatcher.matchStart("*.*", "test.test.test")).isTrue(); + assertThat(pathMatcher.matchStart("test*aaa", "testblaaaa")).isTrue(); + assertThat(pathMatcher.matchStart("test*", "tst")).isFalse(); + assertThat(pathMatcher.matchStart("test*", "test/")).isFalse(); + assertThat(pathMatcher.matchStart("test*", "tsttest")).isFalse(); + assertThat(pathMatcher.matchStart("test*", "test/")).isFalse(); + assertThat(pathMatcher.matchStart("test*", "test/t")).isFalse(); + assertThat(pathMatcher.matchStart("test/*", "test")).isTrue(); + assertThat(pathMatcher.matchStart("test/t*.txt", "test")).isTrue(); + assertThat(pathMatcher.matchStart("*test*", "tsttst")).isFalse(); + assertThat(pathMatcher.matchStart("*test", "tsttst")).isFalse(); + assertThat(pathMatcher.matchStart("*.*", "tsttst")).isFalse(); + assertThat(pathMatcher.matchStart("test*aaa", "test")).isFalse(); + assertThat(pathMatcher.matchStart("test*aaa", "testblaaab")).isFalse(); + + // test matching with ?'s and /'s + assertThat(pathMatcher.matchStart("/?", "/a")).isTrue(); + assertThat(pathMatcher.matchStart("/?/a", "/a/a")).isTrue(); + assertThat(pathMatcher.matchStart("/a/?", "/a/b")).isTrue(); + assertThat(pathMatcher.matchStart("/??/a", "/aa/a")).isTrue(); + assertThat(pathMatcher.matchStart("/a/??", "/a/bb")).isTrue(); + assertThat(pathMatcher.matchStart("/?", "/a")).isTrue(); + + // test matching with **'s + assertThat(pathMatcher.matchStart("/**", "/testing/testing")).isTrue(); + assertThat(pathMatcher.matchStart("/*/**", "/testing/testing")).isTrue(); + assertThat(pathMatcher.matchStart("/**/*", "/testing/testing")).isTrue(); + assertThat(pathMatcher.matchStart("test*/**", "test/")).isTrue(); + assertThat(pathMatcher.matchStart("test*/**", "test/t")).isTrue(); + assertThat(pathMatcher.matchStart("/bla/**/bla", "/bla/testing/testing/bla")).isTrue(); + assertThat(pathMatcher.matchStart("/bla/**/bla", "/bla/testing/testing/bla/bla")).isTrue(); + assertThat(pathMatcher.matchStart("/**/test", "/bla/bla/test")).isTrue(); + assertThat(pathMatcher.matchStart("/bla/**/**/bla", "/bla/bla/bla/bla/bla/bla")).isTrue(); + assertThat(pathMatcher.matchStart("/bla*bla/test", "/blaXXXbla/test")).isTrue(); + assertThat(pathMatcher.matchStart("/*bla/test", "/XXXbla/test")).isTrue(); + assertThat(pathMatcher.matchStart("/bla*bla/test", "/blaXXXbl/test")).isFalse(); + assertThat(pathMatcher.matchStart("/*bla/test", "XXXblab/test")).isFalse(); + assertThat(pathMatcher.matchStart("/*bla/test", "XXXbl/test")).isFalse(); + + assertThat(pathMatcher.matchStart("/????", "/bala/bla")).isFalse(); + assertThat(pathMatcher.matchStart("/**/*bla", "/bla/bla/bla/bbb")).isTrue(); + + assertThat(pathMatcher.matchStart("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing/")).isTrue(); + assertThat(pathMatcher.matchStart("/*bla*/**/bla/*", "/XXXblaXXXX/testing/testing/bla/testing")).isTrue(); + assertThat(pathMatcher.matchStart("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing")).isTrue(); + assertThat(pathMatcher.matchStart("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing.jpg")).isTrue(); + + assertThat(pathMatcher.matchStart("*bla*/**/bla/**", "XXXblaXXXX/testing/testing/bla/testing/testing/")).isTrue(); + assertThat(pathMatcher.matchStart("*bla*/**/bla/*", "XXXblaXXXX/testing/testing/bla/testing")).isTrue(); + assertThat(pathMatcher.matchStart("*bla*/**/bla/**", "XXXblaXXXX/testing/testing/bla/testing/testing")).isTrue(); + assertThat(pathMatcher.matchStart("*bla*/**/bla/*", "XXXblaXXXX/testing/testing/bla/testing/testing")).isTrue(); + + assertThat(pathMatcher.matchStart("/x/x/**/bla", "/x/x/x/")).isTrue(); + + assertThat(pathMatcher.matchStart("", "")).isTrue(); + } + + @Test + void uniqueDeliminator() { + pathMatcher.setPathSeparator("."); + + // test exact matching + assertThat(pathMatcher.match("test", "test")).isTrue(); + assertThat(pathMatcher.match(".test", ".test")).isTrue(); + assertThat(pathMatcher.match(".test/jpg", "test/jpg")).isFalse(); + assertThat(pathMatcher.match("test", ".test")).isFalse(); + assertThat(pathMatcher.match(".test", "test")).isFalse(); + + // test matching with ?'s + assertThat(pathMatcher.match("t?st", "test")).isTrue(); + assertThat(pathMatcher.match("??st", "test")).isTrue(); + assertThat(pathMatcher.match("tes?", "test")).isTrue(); + assertThat(pathMatcher.match("te??", "test")).isTrue(); + assertThat(pathMatcher.match("?es?", "test")).isTrue(); + assertThat(pathMatcher.match("tes?", "tes")).isFalse(); + assertThat(pathMatcher.match("tes?", "testt")).isFalse(); + assertThat(pathMatcher.match("tes?", "tsst")).isFalse(); + + // test matching with *'s + assertThat(pathMatcher.match("*", "test")).isTrue(); + assertThat(pathMatcher.match("test*", "test")).isTrue(); + assertThat(pathMatcher.match("test*", "testTest")).isTrue(); + assertThat(pathMatcher.match("*test*", "AnothertestTest")).isTrue(); + assertThat(pathMatcher.match("*test", "Anothertest")).isTrue(); + assertThat(pathMatcher.match("*/*", "test/")).isTrue(); + assertThat(pathMatcher.match("*/*", "test/test")).isTrue(); + assertThat(pathMatcher.match("*/*", "test/test/test")).isTrue(); + assertThat(pathMatcher.match("test*aaa", "testblaaaa")).isTrue(); + assertThat(pathMatcher.match("test*", "tst")).isFalse(); + assertThat(pathMatcher.match("test*", "tsttest")).isFalse(); + assertThat(pathMatcher.match("*test*", "tsttst")).isFalse(); + assertThat(pathMatcher.match("*test", "tsttst")).isFalse(); + assertThat(pathMatcher.match("*/*", "tsttst")).isFalse(); + assertThat(pathMatcher.match("test*aaa", "test")).isFalse(); + assertThat(pathMatcher.match("test*aaa", "testblaaab")).isFalse(); + + // test matching with ?'s and .'s + assertThat(pathMatcher.match(".?", ".a")).isTrue(); + assertThat(pathMatcher.match(".?.a", ".a.a")).isTrue(); + assertThat(pathMatcher.match(".a.?", ".a.b")).isTrue(); + assertThat(pathMatcher.match(".??.a", ".aa.a")).isTrue(); + assertThat(pathMatcher.match(".a.??", ".a.bb")).isTrue(); + assertThat(pathMatcher.match(".?", ".a")).isTrue(); + + // test matching with **'s + assertThat(pathMatcher.match(".**", ".testing.testing")).isTrue(); + assertThat(pathMatcher.match(".*.**", ".testing.testing")).isTrue(); + assertThat(pathMatcher.match(".**.*", ".testing.testing")).isTrue(); + assertThat(pathMatcher.match(".bla.**.bla", ".bla.testing.testing.bla")).isTrue(); + assertThat(pathMatcher.match(".bla.**.bla", ".bla.testing.testing.bla.bla")).isTrue(); + assertThat(pathMatcher.match(".**.test", ".bla.bla.test")).isTrue(); + assertThat(pathMatcher.match(".bla.**.**.bla", ".bla.bla.bla.bla.bla.bla")).isTrue(); + assertThat(pathMatcher.match(".bla*bla.test", ".blaXXXbla.test")).isTrue(); + assertThat(pathMatcher.match(".*bla.test", ".XXXbla.test")).isTrue(); + assertThat(pathMatcher.match(".bla*bla.test", ".blaXXXbl.test")).isFalse(); + assertThat(pathMatcher.match(".*bla.test", "XXXblab.test")).isFalse(); + assertThat(pathMatcher.match(".*bla.test", "XXXbl.test")).isFalse(); + } + + @Test + void extractPathWithinPattern() throws Exception { + assertThat(pathMatcher.extractPathWithinPattern("/docs/commit.html", "/docs/commit.html")).isEqualTo(""); + + assertThat(pathMatcher.extractPathWithinPattern("/docs/*", "/docs/cvs/commit")).isEqualTo("cvs/commit"); + assertThat(pathMatcher.extractPathWithinPattern("/docs/cvs/*.html", "/docs/cvs/commit.html")).isEqualTo("commit.html"); + assertThat(pathMatcher.extractPathWithinPattern("/docs/**", "/docs/cvs/commit")).isEqualTo("cvs/commit"); + assertThat(pathMatcher.extractPathWithinPattern("/docs/**/*.html", "/docs/cvs/commit.html")).isEqualTo("cvs/commit.html"); + assertThat(pathMatcher.extractPathWithinPattern("/docs/**/*.html", "/docs/commit.html")).isEqualTo("commit.html"); + assertThat(pathMatcher.extractPathWithinPattern("/*.html", "/commit.html")).isEqualTo("commit.html"); + assertThat(pathMatcher.extractPathWithinPattern("/*.html", "/docs/commit.html")).isEqualTo("docs/commit.html"); + assertThat(pathMatcher.extractPathWithinPattern("*.html", "/commit.html")).isEqualTo("/commit.html"); + assertThat(pathMatcher.extractPathWithinPattern("*.html", "/docs/commit.html")).isEqualTo("/docs/commit.html"); + assertThat(pathMatcher.extractPathWithinPattern("**/*.*", "/docs/commit.html")).isEqualTo("/docs/commit.html"); + assertThat(pathMatcher.extractPathWithinPattern("*", "/docs/commit.html")).isEqualTo("/docs/commit.html"); + // SPR-10515 + assertThat(pathMatcher.extractPathWithinPattern("**/commit.html", "/docs/cvs/other/commit.html")).isEqualTo("/docs/cvs/other/commit.html"); + assertThat(pathMatcher.extractPathWithinPattern("/docs/**/commit.html", "/docs/cvs/other/commit.html")).isEqualTo("cvs/other/commit.html"); + assertThat(pathMatcher.extractPathWithinPattern("/docs/**/**/**/**", "/docs/cvs/other/commit.html")).isEqualTo("cvs/other/commit.html"); + + assertThat(pathMatcher.extractPathWithinPattern("/d?cs/*", "/docs/cvs/commit")).isEqualTo("docs/cvs/commit"); + assertThat(pathMatcher.extractPathWithinPattern("/docs/c?s/*.html", "/docs/cvs/commit.html")).isEqualTo("cvs/commit.html"); + assertThat(pathMatcher.extractPathWithinPattern("/d?cs/**", "/docs/cvs/commit")).isEqualTo("docs/cvs/commit"); + assertThat(pathMatcher.extractPathWithinPattern("/d?cs/**/*.html", "/docs/cvs/commit.html")).isEqualTo("docs/cvs/commit.html"); + } + + @Test + void extractUriTemplateVariables() throws Exception { + Map result = pathMatcher.extractUriTemplateVariables("/hotels/{hotel}", "/hotels/1"); + assertThat(result).isEqualTo(Collections.singletonMap("hotel", "1")); + + result = pathMatcher.extractUriTemplateVariables("/h?tels/{hotel}", "/hotels/1"); + assertThat(result).isEqualTo(Collections.singletonMap("hotel", "1")); + + result = pathMatcher.extractUriTemplateVariables("/hotels/{hotel}/bookings/{booking}", "/hotels/1/bookings/2"); + Map expected = new LinkedHashMap<>(); + expected.put("hotel", "1"); + expected.put("booking", "2"); + assertThat(result).isEqualTo(expected); + + result = pathMatcher.extractUriTemplateVariables("/**/hotels/**/{hotel}", "/foo/hotels/bar/1"); + assertThat(result).isEqualTo(Collections.singletonMap("hotel", "1")); + + result = pathMatcher.extractUriTemplateVariables("/{page}.html", "/42.html"); + assertThat(result).isEqualTo(Collections.singletonMap("page", "42")); + + result = pathMatcher.extractUriTemplateVariables("/{page}.*", "/42.html"); + assertThat(result).isEqualTo(Collections.singletonMap("page", "42")); + + result = pathMatcher.extractUriTemplateVariables("/A-{B}-C", "/A-b-C"); + assertThat(result).isEqualTo(Collections.singletonMap("B", "b")); + + result = pathMatcher.extractUriTemplateVariables("/{name}.{extension}", "/test.html"); + expected = new LinkedHashMap<>(); + expected.put("name", "test"); + expected.put("extension", "html"); + assertThat(result).isEqualTo(expected); + } + + @Test + void extractUriTemplateVariablesRegex() { + Map result = pathMatcher + .extractUriTemplateVariables("{symbolicName:[\\w\\.]+}-{version:[\\w\\.]+}.jar", + "com.example-1.0.0.jar"); + assertThat(result.get("symbolicName")).isEqualTo("com.example"); + assertThat(result.get("version")).isEqualTo("1.0.0"); + + result = pathMatcher.extractUriTemplateVariables("{symbolicName:[\\w\\.]+}-sources-{version:[\\w\\.]+}.jar", + "com.example-sources-1.0.0.jar"); + assertThat(result.get("symbolicName")).isEqualTo("com.example"); + assertThat(result.get("version")).isEqualTo("1.0.0"); + } + + /** + * SPR-7787 + */ + @Test + void extractUriTemplateVarsRegexQualifiers() { + Map result = pathMatcher.extractUriTemplateVariables( + "{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.]+}.jar", + "com.example-sources-1.0.0.jar"); + assertThat(result.get("symbolicName")).isEqualTo("com.example"); + assertThat(result.get("version")).isEqualTo("1.0.0"); + + result = pathMatcher.extractUriTemplateVariables( + "{symbolicName:[\\w\\.]+}-sources-{version:[\\d\\.]+}-{year:\\d{4}}{month:\\d{2}}{day:\\d{2}}.jar", + "com.example-sources-1.0.0-20100220.jar"); + assertThat(result.get("symbolicName")).isEqualTo("com.example"); + assertThat(result.get("version")).isEqualTo("1.0.0"); + assertThat(result.get("year")).isEqualTo("2010"); + assertThat(result.get("month")).isEqualTo("02"); + assertThat(result.get("day")).isEqualTo("20"); + + result = pathMatcher.extractUriTemplateVariables( + "{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.\\{\\}]+}.jar", + "com.example-sources-1.0.0.{12}.jar"); + assertThat(result.get("symbolicName")).isEqualTo("com.example"); + assertThat(result.get("version")).isEqualTo("1.0.0.{12}"); + } + + /** + * SPR-8455 + */ + @Test + void extractUriTemplateVarsRegexCapturingGroups() { + assertThatIllegalArgumentException().isThrownBy(() -> + pathMatcher.extractUriTemplateVariables("/web/{id:foo(bar)?}", "/web/foobar")) + .withMessageContaining("The number of capturing groups in the pattern"); + } + + @Test + void combine() { + assertThat(pathMatcher.combine(null, null)).isEqualTo(""); + assertThat(pathMatcher.combine("/hotels", null)).isEqualTo("/hotels"); + assertThat(pathMatcher.combine(null, "/hotels")).isEqualTo("/hotels"); + assertThat(pathMatcher.combine("/hotels/*", "booking")).isEqualTo("/hotels/booking"); + assertThat(pathMatcher.combine("/hotels/*", "/booking")).isEqualTo("/hotels/booking"); + assertThat(pathMatcher.combine("/hotels/**", "booking")).isEqualTo("/hotels/**/booking"); + assertThat(pathMatcher.combine("/hotels/**", "/booking")).isEqualTo("/hotels/**/booking"); + assertThat(pathMatcher.combine("/hotels", "/booking")).isEqualTo("/hotels/booking"); + assertThat(pathMatcher.combine("/hotels", "booking")).isEqualTo("/hotels/booking"); + assertThat(pathMatcher.combine("/hotels/", "booking")).isEqualTo("/hotels/booking"); + assertThat(pathMatcher.combine("/hotels/*", "{hotel}")).isEqualTo("/hotels/{hotel}"); + assertThat(pathMatcher.combine("/hotels/**", "{hotel}")).isEqualTo("/hotels/**/{hotel}"); + assertThat(pathMatcher.combine("/hotels", "{hotel}")).isEqualTo("/hotels/{hotel}"); + assertThat(pathMatcher.combine("/hotels", "{hotel}.*")).isEqualTo("/hotels/{hotel}.*"); + assertThat(pathMatcher.combine("/hotels/*/booking", "{booking}")).isEqualTo("/hotels/*/booking/{booking}"); + assertThat(pathMatcher.combine("/*.html", "/hotel.html")).isEqualTo("/hotel.html"); + assertThat(pathMatcher.combine("/*.html", "/hotel")).isEqualTo("/hotel.html"); + assertThat(pathMatcher.combine("/*.html", "/hotel.*")).isEqualTo("/hotel.html"); + assertThat(pathMatcher.combine("/**", "/*.html")).isEqualTo("/*.html"); + assertThat(pathMatcher.combine("/*", "/*.html")).isEqualTo("/*.html"); + assertThat(pathMatcher.combine("/*.*", "/*.html")).isEqualTo("/*.html"); + // SPR-8858 + assertThat(pathMatcher.combine("/{foo}", "/bar")).isEqualTo("/{foo}/bar"); + // SPR-7970 + assertThat(pathMatcher.combine("/user", "/user")).isEqualTo("/user/user"); + // SPR-10062 + assertThat(pathMatcher.combine("/{foo:.*[^0-9].*}", "/edit/")).isEqualTo("/{foo:.*[^0-9].*}/edit/"); + // SPR-10554 + assertThat(pathMatcher.combine("/1.0", "/foo/test")).isEqualTo("/1.0/foo/test"); + // SPR-12975 + assertThat(pathMatcher.combine("/", "/hotel")).isEqualTo("/hotel"); + // SPR-12975 + assertThat(pathMatcher.combine("/hotel/", "/booking")).isEqualTo("/hotel/booking"); + } + + @Test + void combineWithTwoFileExtensionPatterns() { + assertThatIllegalArgumentException().isThrownBy(() -> + pathMatcher.combine("/*.html", "/*.txt")); + } + + @Test + void patternComparator() { + Comparator comparator = pathMatcher.getPatternComparator("/hotels/new"); + + assertThat(comparator.compare(null, null)).isEqualTo(0); + assertThat(comparator.compare(null, "/hotels/new")).isEqualTo(1); + assertThat(comparator.compare("/hotels/new", null)).isEqualTo(-1); + + assertThat(comparator.compare("/hotels/new", "/hotels/new")).isEqualTo(0); + + assertThat(comparator.compare("/hotels/new", "/hotels/*")).isEqualTo(-1); + assertThat(comparator.compare("/hotels/*", "/hotels/new")).isEqualTo(1); + assertThat(comparator.compare("/hotels/*", "/hotels/*")).isEqualTo(0); + + assertThat(comparator.compare("/hotels/new", "/hotels/{hotel}")).isEqualTo(-1); + assertThat(comparator.compare("/hotels/{hotel}", "/hotels/new")).isEqualTo(1); + assertThat(comparator.compare("/hotels/{hotel}", "/hotels/{hotel}")).isEqualTo(0); + assertThat(comparator.compare("/hotels/{hotel}/booking", "/hotels/{hotel}/bookings/{booking}")).isEqualTo(-1); + assertThat(comparator.compare("/hotels/{hotel}/bookings/{booking}", "/hotels/{hotel}/booking")).isEqualTo(1); + + // SPR-10550 + assertThat(comparator.compare("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}", "/**")).isEqualTo(-1); + assertThat(comparator.compare("/**", "/hotels/{hotel}/bookings/{booking}/cutomers/{customer}")).isEqualTo(1); + assertThat(comparator.compare("/**", "/**")).isEqualTo(0); + + assertThat(comparator.compare("/hotels/{hotel}", "/hotels/*")).isEqualTo(-1); + assertThat(comparator.compare("/hotels/*", "/hotels/{hotel}")).isEqualTo(1); + + assertThat(comparator.compare("/hotels/*", "/hotels/*/**")).isEqualTo(-1); + assertThat(comparator.compare("/hotels/*/**", "/hotels/*")).isEqualTo(1); + + assertThat(comparator.compare("/hotels/new", "/hotels/new.*")).isEqualTo(-1); + assertThat(comparator.compare("/hotels/{hotel}", "/hotels/{hotel}.*")).isEqualTo(2); + + // SPR-6741 + assertThat(comparator.compare("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}", "/hotels/**")).isEqualTo(-1); + assertThat(comparator.compare("/hotels/**", "/hotels/{hotel}/bookings/{booking}/cutomers/{customer}")).isEqualTo(1); + assertThat(comparator.compare("/hotels/foo/bar/**", "/hotels/{hotel}")).isEqualTo(1); + assertThat(comparator.compare("/hotels/{hotel}", "/hotels/foo/bar/**")).isEqualTo(-1); + + // gh-23125 + assertThat(comparator.compare("/hotels/*/bookings/**", "/hotels/**")).isEqualTo(-11); + + // SPR-8683 + assertThat(comparator.compare("/**", "/hotels/{hotel}")).isEqualTo(1); + + // longer is better + assertThat(comparator.compare("/hotels", "/hotels2")).isEqualTo(1); + + // SPR-13139 + assertThat(comparator.compare("*", "*/**")).isEqualTo(-1); + assertThat(comparator.compare("*/**", "*")).isEqualTo(1); + } + + @Test + void patternComparatorSort() { + Comparator comparator = pathMatcher.getPatternComparator("/hotels/new"); + List paths = new ArrayList<>(3); + + paths.add(null); + paths.add("/hotels/new"); + paths.sort(comparator); + assertThat(paths.get(0)).isEqualTo("/hotels/new"); + assertThat(paths.get(1)).isNull(); + paths.clear(); + + paths.add("/hotels/new"); + paths.add(null); + paths.sort(comparator); + assertThat(paths.get(0)).isEqualTo("/hotels/new"); + assertThat(paths.get(1)).isNull(); + paths.clear(); + + paths.add("/hotels/*"); + paths.add("/hotels/new"); + paths.sort(comparator); + assertThat(paths.get(0)).isEqualTo("/hotels/new"); + assertThat(paths.get(1)).isEqualTo("/hotels/*"); + paths.clear(); + + paths.add("/hotels/new"); + paths.add("/hotels/*"); + paths.sort(comparator); + assertThat(paths.get(0)).isEqualTo("/hotels/new"); + assertThat(paths.get(1)).isEqualTo("/hotels/*"); + paths.clear(); + + paths.add("/hotels/**"); + paths.add("/hotels/*"); + paths.sort(comparator); + assertThat(paths.get(0)).isEqualTo("/hotels/*"); + assertThat(paths.get(1)).isEqualTo("/hotels/**"); + paths.clear(); + + paths.add("/hotels/*"); + paths.add("/hotels/**"); + paths.sort(comparator); + assertThat(paths.get(0)).isEqualTo("/hotels/*"); + assertThat(paths.get(1)).isEqualTo("/hotels/**"); + paths.clear(); + + paths.add("/hotels/{hotel}"); + paths.add("/hotels/new"); + paths.sort(comparator); + assertThat(paths.get(0)).isEqualTo("/hotels/new"); + assertThat(paths.get(1)).isEqualTo("/hotels/{hotel}"); + paths.clear(); + + paths.add("/hotels/new"); + paths.add("/hotels/{hotel}"); + paths.sort(comparator); + assertThat(paths.get(0)).isEqualTo("/hotels/new"); + assertThat(paths.get(1)).isEqualTo("/hotels/{hotel}"); + paths.clear(); + + paths.add("/hotels/*"); + paths.add("/hotels/{hotel}"); + paths.add("/hotels/new"); + paths.sort(comparator); + assertThat(paths.get(0)).isEqualTo("/hotels/new"); + assertThat(paths.get(1)).isEqualTo("/hotels/{hotel}"); + assertThat(paths.get(2)).isEqualTo("/hotels/*"); + paths.clear(); + + paths.add("/hotels/ne*"); + paths.add("/hotels/n*"); + Collections.shuffle(paths); + paths.sort(comparator); + assertThat(paths.get(0)).isEqualTo("/hotels/ne*"); + assertThat(paths.get(1)).isEqualTo("/hotels/n*"); + paths.clear(); + + comparator = pathMatcher.getPatternComparator("/hotels/new.html"); + paths.add("/hotels/new.*"); + paths.add("/hotels/{hotel}"); + Collections.shuffle(paths); + paths.sort(comparator); + assertThat(paths.get(0)).isEqualTo("/hotels/new.*"); + assertThat(paths.get(1)).isEqualTo("/hotels/{hotel}"); + paths.clear(); + + comparator = pathMatcher.getPatternComparator("/web/endUser/action/login.html"); + paths.add("/**/login.*"); + paths.add("/**/endUser/action/login.*"); + paths.sort(comparator); + assertThat(paths.get(0)).isEqualTo("/**/endUser/action/login.*"); + assertThat(paths.get(1)).isEqualTo("/**/login.*"); + paths.clear(); + } + + @Test // SPR-8687 + void trimTokensOff() { + pathMatcher.setTrimTokens(false); + + assertThat(pathMatcher.match("/group/{groupName}/members", "/group/sales/members")).isTrue(); + assertThat(pathMatcher.match("/group/{groupName}/members", "/group/ sales/members")).isTrue(); + assertThat(pathMatcher.match("/group/{groupName}/members", "/Group/ Sales/Members")).isFalse(); + } + + @Test // SPR-13286 + void caseInsensitive() { + pathMatcher.setCaseSensitive(false); + + assertThat(pathMatcher.match("/group/{groupName}/members", "/group/sales/members")).isTrue(); + assertThat(pathMatcher.match("/group/{groupName}/members", "/Group/Sales/Members")).isTrue(); + assertThat(pathMatcher.match("/Group/{groupName}/Members", "/group/Sales/members")).isTrue(); + } + + @Test + void defaultCacheSetting() { + match(); + assertThat(pathMatcher.stringMatcherCache.size() > 20).isTrue(); + + for (int i = 0; i < 65536; i++) { + pathMatcher.match("test" + i, "test"); + } + // Cache turned off because it went beyond the threshold + assertThat(pathMatcher.stringMatcherCache.isEmpty()).isTrue(); + } + + @Test + void cachePatternsSetToTrue() { + pathMatcher.setCachePatterns(true); + match(); + assertThat(pathMatcher.stringMatcherCache.size() > 20).isTrue(); + + for (int i = 0; i < 65536; i++) { + pathMatcher.match("test" + i, "test" + i); + } + // Cache keeps being alive due to the explicit cache setting + assertThat(pathMatcher.stringMatcherCache.size() > 65536).isTrue(); + } + + @Test + void preventCreatingStringMatchersIfPathDoesNotStartsWithPatternPrefix() { + pathMatcher.setCachePatterns(true); + assertThat(pathMatcher.stringMatcherCache.size()).isEqualTo(0); + + pathMatcher.match("test?", "test"); + assertThat(pathMatcher.stringMatcherCache.size()).isEqualTo(1); + + pathMatcher.match("test?", "best"); + pathMatcher.match("test/*", "view/test.jpg"); + pathMatcher.match("test/**/test.jpg", "view/test.jpg"); + pathMatcher.match("test/{name}.jpg", "view/test.jpg"); + assertThat(pathMatcher.stringMatcherCache.size()).isEqualTo(1); + } + + @Test + void creatingStringMatchersIfPatternPrefixCannotDetermineIfPathMatch() { + pathMatcher.setCachePatterns(true); + assertThat(pathMatcher.stringMatcherCache.size()).isEqualTo(0); + + pathMatcher.match("test", "testian"); + pathMatcher.match("test?", "testFf"); + pathMatcher.match("test/*", "test/dir/name.jpg"); + pathMatcher.match("test/{name}.jpg", "test/lorem.jpg"); + pathMatcher.match("bla/**/test.jpg", "bla/test.jpg"); + pathMatcher.match("**/{name}.jpg", "test/lorem.jpg"); + pathMatcher.match("/**/{name}.jpg", "/test/lorem.jpg"); + pathMatcher.match("/*/dir/{name}.jpg", "/*/dir/lorem.jpg"); + + assertThat(pathMatcher.stringMatcherCache.size()).isEqualTo(7); + } + + @Test + void cachePatternsSetToFalse() { + pathMatcher.setCachePatterns(false); + match(); + assertThat(pathMatcher.stringMatcherCache.isEmpty()).isTrue(); + } + + @Test + void extensionMappingWithDotPathSeparator() { + pathMatcher.setPathSeparator("."); + assertThat(pathMatcher.combine("/*.html", "hotel.*")).as("Extension mapping should be disabled with \".\" as path separator").isEqualTo("/*.html.hotel.*"); + } + + @Test // gh-22959 + void isPattern() { + assertThat(pathMatcher.isPattern("/test/*")).isTrue(); + assertThat(pathMatcher.isPattern("/test/**/name")).isTrue(); + assertThat(pathMatcher.isPattern("/test?")).isTrue(); + assertThat(pathMatcher.isPattern("/test/{name}")).isTrue(); + + assertThat(pathMatcher.isPattern("/test/name")).isFalse(); + assertThat(pathMatcher.isPattern("/test/foo{bar")).isFalse(); + } + + @Test // gh-23297 + void isPatternWithNullPath() { + assertThat(pathMatcher.isPattern(null)).isFalse(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/AssertTests.java b/spring-core/src/test/java/org/springframework/util/AssertTests.java new file mode 100644 index 0000000..d29bac8 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/AssertTests.java @@ -0,0 +1,731 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.util.Collection; +import java.util.Map; +import java.util.function.Supplier; + +import org.junit.jupiter.api.Test; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Unit tests for the {@link Assert} class. + * + * @author Keith Donald + * @author Erwin Vervaet + * @author Rick Evans + * @author Arjen Poutsma + * @author Sam Brannen + * @author Juergen Hoeller + */ +class AssertTests { + + @Test + void stateWithMessage() { + Assert.state(true, "enigma"); + } + + @Test + void stateWithFalseExpressionAndMessage() { + assertThatIllegalStateException().isThrownBy(() -> + Assert.state(false, "enigma")).withMessageContaining("enigma"); + } + + @Test + void stateWithMessageSupplier() { + Assert.state(true, () -> "enigma"); + } + + @Test + void stateWithFalseExpressionAndMessageSupplier() { + assertThatIllegalStateException().isThrownBy(() -> + Assert.state(false, () -> "enigma")) + .withMessageContaining("enigma"); + } + + @Test + void stateWithFalseExpressionAndNullMessageSupplier() { + assertThatIllegalStateException().isThrownBy(() -> + Assert.state(false, (Supplier) null)) + .withMessage(null); + } + + @Test + void isTrueWithMessage() { + Assert.isTrue(true, "enigma"); + } + + @Test + void isTrueWithFalse() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.isTrue(false, "enigma")) + .withMessageContaining("enigma"); + } + + @Test + void isTrueWithMessageSupplier() { + Assert.isTrue(true, () -> "enigma"); + } + + @Test + void isTrueWithFalseAndMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.isTrue(false, () -> "enigma")) + .withMessageContaining("enigma"); + } + + @Test + void isTrueWithFalseAndNullMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.isTrue(false, (Supplier) null)) + .withMessage(null); + } + + @Test + void isNullWithMessage() { + Assert.isNull(null, "Bla"); + } + + @Test + void isNullWithMessageSupplier() { + Assert.isNull(null, () -> "enigma"); + } + + @Test + void isNullWithNonNullObjectAndMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.isNull("foo", () -> "enigma")) + .withMessageContaining("enigma"); + } + + @Test + void isNullWithNonNullObjectAndNullMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.isNull("foo", (Supplier) null)) + .withMessage(null); + } + + @Test + void notNullWithMessage() { + Assert.notNull("foo", "enigma"); + } + + @Test + void notNullWithMessageSupplier() { + Assert.notNull("foo", () -> "enigma"); + } + + @Test + void notNullWithNullAndMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.notNull(null, () -> "enigma")) + .withMessageContaining("enigma"); + } + + @Test + void notNullWithNullAndNullMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.notNull(null, (Supplier) null)) + .withMessage(null); + } + + @Test + void hasLength() { + Assert.hasLength("I Heart ...", "enigma"); + } + + @Test + void hasLengthWithWhitespaceOnly() { + Assert.hasLength("\t ", "enigma"); + } + + @Test + void hasLengthWithEmptyString() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.hasLength("", "enigma")) + .withMessageContaining("enigma"); + } + + @Test + void hasLengthWithNull() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.hasLength(null, "enigma")) + .withMessageContaining("enigma"); + } + + @Test + void hasLengthWithMessageSupplier() { + Assert.hasLength("foo", () -> "enigma"); + } + + @Test + void hasLengthWithWhitespaceOnlyAndMessageSupplier() { + Assert.hasLength("\t", () -> "enigma"); + } + + @Test + void hasLengthWithEmptyStringAndMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.hasLength("", () -> "enigma")) + .withMessageContaining("enigma"); + } + + @Test + void hasLengthWithNullAndMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.hasLength(null, () -> "enigma")) + .withMessageContaining("enigma"); + } + + @Test + void hasLengthWithNullAndNullMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.hasLength(null, (Supplier) null)) + .withMessage(null); + } + + @Test + void hasText() { + Assert.hasText("foo", "enigma"); + } + + @Test + void hasTextWithWhitespaceOnly() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.hasText("\t ", "enigma")) + .withMessageContaining("enigma"); + } + + @Test + void hasTextWithEmptyString() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.hasText("", "enigma")) + .withMessageContaining("enigma"); + } + + @Test + void hasTextWithNull() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.hasText(null, "enigma")) + .withMessageContaining("enigma"); + } + + @Test + void hasTextWithMessageSupplier() { + Assert.hasText("foo", () -> "enigma"); + } + + @Test + void hasTextWithWhitespaceOnlyAndMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.hasText("\t ", () -> "enigma")) + .withMessageContaining("enigma"); + } + + @Test + void hasTextWithEmptyStringAndMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.hasText("", () -> "enigma")).withMessageContaining("enigma"); + } + + @Test + void hasTextWithNullAndMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.hasText(null, () -> "enigma")) + .withMessageContaining("enigma"); + } + + @Test + void hasTextWithNullAndNullMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.hasText(null, (Supplier) null)) + .withMessage(null); + } + + @Test + void doesNotContainWithNullSearchString() { + Assert.doesNotContain(null, "rod", "enigma"); + } + + @Test + void doesNotContainWithNullSubstring() { + Assert.doesNotContain("A cool chick's name is Brod.", null, "enigma"); + } + + @Test + void doesNotContainWithEmptySubstring() { + Assert.doesNotContain("A cool chick's name is Brod.", "", "enigma"); + } + + @Test + void doesNotContainWithNullSearchStringAndNullSubstring() { + Assert.doesNotContain(null, null, "enigma"); + } + + @Test + void doesNotContainWithMessageSupplier() { + Assert.doesNotContain("foo", "bar", () -> "enigma"); + } + + @Test + void doesNotContainWithNullSearchStringAndMessageSupplier() { + Assert.doesNotContain(null, "bar", () -> "enigma"); + } + + @Test + void doesNotContainWithNullSubstringAndMessageSupplier() { + Assert.doesNotContain("foo", null, () -> "enigma"); + } + + @Test + void doesNotContainWithNullSearchStringAndNullSubstringAndMessageSupplier() { + Assert.doesNotContain(null, null, () -> "enigma"); + } + + @Test + void doesNotContainWithSubstringPresentInSearchStringAndMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.doesNotContain("1234", "23", () -> "enigma")) + .withMessageContaining("enigma"); + } + + @Test + void doesNotContainWithNullMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.doesNotContain("1234", "23", (Supplier) null)) + .withMessage(null); + } + + @Test + void notEmptyArray() { + Assert.notEmpty(new String[] {"1234"}, "enigma"); + } + + @Test + void notEmptyArrayWithEmptyArray() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.notEmpty(new String[] {}, "enigma")) + .withMessageContaining("enigma"); + } + + @Test + void notEmptyArrayWithNullArray() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.notEmpty((Object[]) null, "enigma")) + .withMessageContaining("enigma"); + } + + @Test + void notEmptyArrayWithMessageSupplier() { + Assert.notEmpty(new String[] {"1234"}, () -> "enigma"); + } + + @Test + void notEmptyArrayWithEmptyArrayAndMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.notEmpty(new String[] {}, () -> "enigma")) + .withMessageContaining("enigma"); + } + + @Test + void notEmptyArrayWithNullArrayAndMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.notEmpty((Object[]) null, () -> "enigma")) + .withMessageContaining("enigma"); + } + + @Test + void notEmptyArrayWithEmptyArrayAndNullMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.notEmpty(new String[] {}, (Supplier) null)) + .withMessage(null); + } + + @Test + void noNullElements() { + Assert.noNullElements(new String[] { "1234" }, "enigma"); + } + + @Test + void noNullElementsWithEmptyArray() { + Assert.noNullElements(new String[] {}, "enigma"); + } + + @Test + void noNullElementsWithMessageSupplier() { + Assert.noNullElements(new String[] { "1234" }, () -> "enigma"); + } + + @Test + void noNullElementsWithEmptyArrayAndMessageSupplier() { + Assert.noNullElements(new String[] {}, () -> "enigma"); + } + + @Test + void noNullElementsWithNullArrayAndMessageSupplier() { + Assert.noNullElements((Object[]) null, () -> "enigma"); + } + + @Test + void noNullElementsWithNullElementsAndMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.noNullElements(new String[] { "foo", null, "bar" }, () -> "enigma")) + .withMessageContaining("enigma"); + } + + @Test + void noNullElementsWithNullElementsAndNullMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.noNullElements(new String[] { "foo", null, "bar" }, (Supplier) null)) + .withMessage(null); + } + + @Test + void noNullElementsWithCollection() { + assertThatCode(() -> + Assert.noNullElements(asList("foo", "bar"), "enigma")) + .doesNotThrowAnyException(); + } + + @Test + void noNullElementsWithEmptyCollection() { + assertThatCode(() -> + Assert.noNullElements(emptyList(), "enigma")) + .doesNotThrowAnyException(); + } + + @Test + void noNullElementsWithNullCollection() { + assertThatCode(() -> + Assert.noNullElements((Collection) null, "enigma")) + .doesNotThrowAnyException(); + } + + @Test + void noNullElementsWithCollectionAndNullElement() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.noNullElements(asList("foo", null, "bar"), "enigma")) + .withMessageContaining("enigma"); + } + + @Test + void noNullElementsWithCollectionAndMessageSupplier() { + assertThatCode(() -> + Assert.noNullElements(asList("foo", "bar"), () -> "enigma")) + .doesNotThrowAnyException(); + } + + @Test + void noNullElementsWithEmptyCollectionAndMessageSupplier() { + assertThatCode(() -> + Assert.noNullElements(emptyList(), "enigma")) + .doesNotThrowAnyException(); + } + + @Test + void noNullElementsWithNullCollectionAndMessageSupplier() { + assertThatCode(() -> + Assert.noNullElements((Collection) null, () -> "enigma")) + .doesNotThrowAnyException(); + } + + @Test + void noNullElementsWithCollectionAndNullElementAndMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.noNullElements(asList("foo", null, "bar"), () -> "enigma")) + .withMessageContaining("enigma"); + } + + @Test + void noNullElementsWithCollectionAndNullElementAndNullMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.noNullElements(asList("foo", null, "bar"), (Supplier) null)) + .withMessage(null); + } + + @Test + void notEmptyCollection() { + Assert.notEmpty(singletonList("foo"), "enigma"); + } + + @Test + void notEmptyCollectionWithEmptyCollection() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.notEmpty(emptyList(), "enigma")) + .withMessageContaining("enigma"); + } + + @Test + void notEmptyCollectionWithNullCollection() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.notEmpty((Collection) null, "enigma")) + .withMessageContaining("enigma"); + } + + @Test + void notEmptyCollectionWithMessageSupplier() { + Assert.notEmpty(singletonList("foo"), () -> "enigma"); + } + + @Test + void notEmptyCollectionWithEmptyCollectionAndMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.notEmpty(emptyList(), () -> "enigma")) + .withMessageContaining("enigma"); + } + + @Test + void notEmptyCollectionWithNullCollectionAndMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.notEmpty((Collection) null, () -> "enigma")) + .withMessageContaining("enigma"); + } + + @Test + void notEmptyCollectionWithEmptyCollectionAndNullMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.notEmpty(emptyList(), (Supplier) null)) + .withMessage(null); + } + + @Test + void notEmptyMap() { + Assert.notEmpty(singletonMap("foo", "bar"), "enigma"); + } + + @Test + void notEmptyMapWithNullMap() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.notEmpty((Map) null, "enigma")) + .withMessageContaining("enigma"); + } + + @Test + void notEmptyMapWithEmptyMap() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.notEmpty(emptyMap(), "enigma")) + .withMessageContaining("enigma"); + } + + @Test + void notEmptyMapWithMessageSupplier() { + Assert.notEmpty(singletonMap("foo", "bar"), () -> "enigma"); + } + + @Test + void notEmptyMapWithEmptyMapAndMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.notEmpty(emptyMap(), () -> "enigma")) + .withMessageContaining("enigma"); + } + + @Test + void notEmptyMapWithNullMapAndMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.notEmpty((Map) null, () -> "enigma")) + .withMessageContaining("enigma"); + } + + @Test + void notEmptyMapWithEmptyMapAndNullMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.notEmpty(emptyMap(), (Supplier) null)) + .withMessage(null); + } + + @Test + void isInstanceOf() { + Assert.isInstanceOf(String.class, "foo", "enigma"); + } + + @Test + void isInstanceOfWithNullType() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.isInstanceOf(null, "foo", "enigma")) + .withMessageContaining("Type to check against must not be null"); + } + + @Test + void isInstanceOfWithNullInstance() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.isInstanceOf(String.class, null, "enigma")) + .withMessageContaining("enigma: null"); + } + + @Test + void isInstanceOfWithTypeMismatchAndNullMessage() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.isInstanceOf(String.class, 42L, (String) null)) + .withMessageContaining("Object of class [java.lang.Long] must be an instance of class java.lang.String"); + } + + @Test + void isInstanceOfWithTypeMismatchAndCustomMessage() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.isInstanceOf(String.class, 42L, "Custom message")) + .withMessageContaining("Custom message: java.lang.Long"); + } + + @Test + void isInstanceOfWithTypeMismatchAndCustomMessageWithSeparator() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.isInstanceOf(String.class, 42L, "Custom message:")) + .withMessageContaining("Custom message: Object of class [java.lang.Long] must be an instance of class java.lang.String"); + } + + @Test + void isInstanceOfWithTypeMismatchAndCustomMessageWithSpace() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.isInstanceOf(String.class, 42L, "Custom message for ")) + .withMessageContaining("Custom message for java.lang.Long"); + } + + @Test + void isInstanceOfWithMessageSupplier() { + Assert.isInstanceOf(String.class, "foo", () -> "enigma"); + } + + @Test + void isInstanceOfWithNullTypeAndMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.isInstanceOf(null, "foo", () -> "enigma")) + .withMessageContaining("Type to check against must not be null"); + } + + @Test + void isInstanceOfWithNullInstanceAndMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.isInstanceOf(String.class, null, () -> "enigma")) + .withMessageContaining("enigma: null"); + } + + @Test + void isInstanceOfWithTypeMismatchAndNullMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.isInstanceOf(String.class, 42L, (Supplier) null)) + .withMessageContaining("Object of class [java.lang.Long] must be an instance of class java.lang.String"); + } + + @Test + void isInstanceOfWithTypeMismatchAndMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.isInstanceOf(String.class, 42L, () -> "enigma")) + .withMessageContaining("enigma: java.lang.Long"); + } + + @Test + void isAssignable() { + Assert.isAssignable(Number.class, Integer.class, "enigma"); + } + + @Test + void isAssignableWithNullSupertype() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.isAssignable(null, Integer.class, "enigma")) + .withMessageContaining("Super type to check against must not be null"); + } + + @Test + void isAssignableWithNullSubtype() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.isAssignable(Integer.class, null, "enigma")) + .withMessageContaining("enigma: null"); + } + + @Test + void isAssignableWithTypeMismatchAndNullMessage() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.isAssignable(String.class, Integer.class, (String) null)) + .withMessageContaining("class java.lang.Integer is not assignable to class java.lang.String"); + } + + @Test + void isAssignableWithTypeMismatchAndCustomMessage() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.isAssignable(String.class, Integer.class, "Custom message")) + .withMessageContaining("Custom message: class java.lang.Integer"); + } + + @Test + void isAssignableWithTypeMismatchAndCustomMessageWithSeparator() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.isAssignable(String.class, Integer.class, "Custom message:")) + .withMessageContaining("Custom message: class java.lang.Integer is not assignable to class java.lang.String"); + } + + @Test + void isAssignableWithTypeMismatchAndCustomMessageWithSpace() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.isAssignable(String.class, Integer.class, "Custom message for ")) + .withMessageContaining("Custom message for class java.lang.Integer"); + } + + @Test + void isAssignableWithMessageSupplier() { + Assert.isAssignable(Number.class, Integer.class, () -> "enigma"); + } + + @Test + void isAssignableWithNullSupertypeAndMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.isAssignable(null, Integer.class, () -> "enigma")) + .withMessageContaining("Super type to check against must not be null"); + } + + @Test + void isAssignableWithNullSubtypeAndMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.isAssignable(Integer.class, null, () -> "enigma")) + .withMessageContaining("enigma: null"); + } + + @Test + void isAssignableWithTypeMismatchAndNullMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.isAssignable(String.class, Integer.class, (Supplier) null)) + .withMessageContaining("class java.lang.Integer is not assignable to class java.lang.String"); + } + + @Test + void isAssignableWithTypeMismatchAndMessageSupplier() { + assertThatIllegalArgumentException().isThrownBy(() -> + Assert.isAssignable(String.class, Integer.class, () -> "enigma")) + .withMessageContaining("enigma: class java.lang.Integer"); + } + + @Test + void state() { + Assert.state(true, "enigma"); + } + + @Test + void stateWithFalseExpression() { + assertThatIllegalStateException().isThrownBy(() -> + Assert.state(false, "enigma")) + .withMessageContaining("enigma"); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/AutoPopulatingListTests.java b/spring-core/src/test/java/org/springframework/util/AutoPopulatingListTests.java new file mode 100644 index 0000000..b3d553f --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/AutoPopulatingListTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.util.ArrayList; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.testfixture.io.SerializationTestUtils; +import org.springframework.tests.sample.objects.TestObject; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + * @author Juergen Hoeller + */ +class AutoPopulatingListTests { + + @Test + void withClass() throws Exception { + doTestWithClass(new AutoPopulatingList<>(TestObject.class)); + } + + @Test + void withClassAndUserSuppliedBackingList() throws Exception { + doTestWithClass(new AutoPopulatingList(new ArrayList<>(), TestObject.class)); + } + + @Test + void withElementFactory() throws Exception { + doTestWithElementFactory(new AutoPopulatingList<>(new MockElementFactory())); + } + + @Test + void withElementFactoryAndUserSuppliedBackingList() throws Exception { + doTestWithElementFactory(new AutoPopulatingList(new ArrayList<>(), new MockElementFactory())); + } + + private void doTestWithClass(AutoPopulatingList list) { + Object lastElement = null; + for (int x = 0; x < 10; x++) { + Object element = list.get(x); + assertThat(list.get(x)).as("Element is null").isNotNull(); + boolean condition = element instanceof TestObject; + assertThat(condition).as("Element is incorrect type").isTrue(); + assertThat(element).isNotSameAs(lastElement); + lastElement = element; + } + + String helloWorld = "Hello World!"; + list.add(10, null); + list.add(11, helloWorld); + assertThat(list.get(11)).isEqualTo(helloWorld); + + boolean condition3 = list.get(10) instanceof TestObject; + assertThat(condition3).isTrue(); + boolean condition2 = list.get(12) instanceof TestObject; + assertThat(condition2).isTrue(); + boolean condition1 = list.get(13) instanceof TestObject; + assertThat(condition1).isTrue(); + boolean condition = list.get(20) instanceof TestObject; + assertThat(condition).isTrue(); + } + + private void doTestWithElementFactory(AutoPopulatingList list) { + doTestWithClass(list); + + for (int x = 0; x < list.size(); x++) { + Object element = list.get(x); + if (element instanceof TestObject) { + assertThat(((TestObject) element).getAge()).isEqualTo(x); + } + } + } + + @Test + void serialization() throws Exception { + AutoPopulatingList list = new AutoPopulatingList(TestObject.class); + assertThat(SerializationTestUtils.serializeAndDeserialize(list)).isEqualTo(list); + } + + + private static class MockElementFactory implements AutoPopulatingList.ElementFactory { + + @Override + public Object createElement(int index) { + TestObject bean = new TestObject(); + bean.setAge(index); + return bean; + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/Base64UtilsTests.java b/spring-core/src/test/java/org/springframework/util/Base64UtilsTests.java new file mode 100644 index 0000000..593d81e --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/Base64UtilsTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.io.UnsupportedEncodingException; + +import javax.xml.bind.DatatypeConverter; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @since 4.2 + */ +class Base64UtilsTests { + + @Test + void encode() throws UnsupportedEncodingException { + byte[] bytes = new byte[] + {-0x4f, 0xa, -0x73, -0x4f, 0x64, -0x20, 0x75, 0x41, 0x5, -0x49, -0x57, -0x65, -0x19, 0x2e, 0x3f, -0x1b}; + assertThat(Base64Utils.decode(Base64Utils.encode(bytes))).isEqualTo(bytes); + + bytes = "Hello World".getBytes("UTF-8"); + assertThat(Base64Utils.decode(Base64Utils.encode(bytes))).isEqualTo(bytes); + + bytes = "Hello World\r\nSecond Line".getBytes("UTF-8"); + assertThat(Base64Utils.decode(Base64Utils.encode(bytes))).isEqualTo(bytes); + + bytes = "Hello World\r\nSecond Line\r\n".getBytes("UTF-8"); + assertThat(Base64Utils.decode(Base64Utils.encode(bytes))).isEqualTo(bytes); + + bytes = new byte[] { (byte) 0xfb, (byte) 0xf0 }; + assertThat(Base64Utils.encode(bytes)).isEqualTo("+/A=".getBytes()); + assertThat(Base64Utils.decode(Base64Utils.encode(bytes))).isEqualTo(bytes); + + assertThat(Base64Utils.encodeUrlSafe(bytes)).isEqualTo("-_A=".getBytes()); + assertThat(Base64Utils.decodeUrlSafe(Base64Utils.encodeUrlSafe(bytes))).isEqualTo(bytes); + } + + @Test + void encodeToStringWithJdk8VsJaxb() throws UnsupportedEncodingException { + byte[] bytes = new byte[] + {-0x4f, 0xa, -0x73, -0x4f, 0x64, -0x20, 0x75, 0x41, 0x5, -0x49, -0x57, -0x65, -0x19, 0x2e, 0x3f, -0x1b}; + assertThat(DatatypeConverter.printBase64Binary(bytes)).isEqualTo(Base64Utils.encodeToString(bytes)); + assertThat(Base64Utils.decodeFromString(Base64Utils.encodeToString(bytes))).isEqualTo(bytes); + assertThat(DatatypeConverter.parseBase64Binary(DatatypeConverter.printBase64Binary(bytes))).isEqualTo(bytes); + + bytes = "Hello World".getBytes("UTF-8"); + assertThat(DatatypeConverter.printBase64Binary(bytes)).isEqualTo(Base64Utils.encodeToString(bytes)); + assertThat(Base64Utils.decodeFromString(Base64Utils.encodeToString(bytes))).isEqualTo(bytes); + assertThat(DatatypeConverter.parseBase64Binary(DatatypeConverter.printBase64Binary(bytes))).isEqualTo(bytes); + + bytes = "Hello World\r\nSecond Line".getBytes("UTF-8"); + assertThat(DatatypeConverter.printBase64Binary(bytes)).isEqualTo(Base64Utils.encodeToString(bytes)); + assertThat(Base64Utils.decodeFromString(Base64Utils.encodeToString(bytes))).isEqualTo(bytes); + assertThat(DatatypeConverter.parseBase64Binary(DatatypeConverter.printBase64Binary(bytes))).isEqualTo(bytes); + + bytes = "Hello World\r\nSecond Line\r\n".getBytes("UTF-8"); + assertThat(DatatypeConverter.printBase64Binary(bytes)).isEqualTo(Base64Utils.encodeToString(bytes)); + assertThat(Base64Utils.decodeFromString(Base64Utils.encodeToString(bytes))).isEqualTo(bytes); + assertThat(DatatypeConverter.parseBase64Binary(DatatypeConverter.printBase64Binary(bytes))).isEqualTo(bytes); + } + + @Test + void encodeDecodeUrlSafe() { + byte[] bytes = new byte[] { (byte) 0xfb, (byte) 0xf0 }; + assertThat(Base64Utils.encodeUrlSafe(bytes)).isEqualTo("-_A=".getBytes()); + assertThat(Base64Utils.decodeUrlSafe(Base64Utils.encodeUrlSafe(bytes))).isEqualTo(bytes); + + assertThat(Base64Utils.encodeToUrlSafeString(bytes)).isEqualTo("-_A="); + assertThat(Base64Utils.decodeFromUrlSafeString(Base64Utils.encodeToUrlSafeString(bytes))).isEqualTo(bytes); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java b/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java new file mode 100644 index 0000000..194887d --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/ClassUtilsTests.java @@ -0,0 +1,491 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.io.Externalizable; +import java.io.Serializable; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import org.springframework.tests.sample.objects.DerivedTestObject; +import org.springframework.tests.sample.objects.ITestInterface; +import org.springframework.tests.sample.objects.ITestObject; +import org.springframework.tests.sample.objects.TestObject; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link ClassUtils}. + * + * @author Colin Sampaleanu + * @author Juergen Hoeller + * @author Rob Harrop + * @author Rick Evans + * @author Sam Brannen + */ +class ClassUtilsTests { + + private final ClassLoader classLoader = getClass().getClassLoader(); + + + @BeforeEach + void clearStatics() { + InnerClass.noArgCalled = false; + InnerClass.argCalled = false; + InnerClass.overloadedCalled = false; + } + + + @Test + void isPresent() { + assertThat(ClassUtils.isPresent("java.lang.String", classLoader)).isTrue(); + assertThat(ClassUtils.isPresent("java.lang.MySpecialString", classLoader)).isFalse(); + } + + @Test + void forName() throws ClassNotFoundException { + assertThat(ClassUtils.forName("java.lang.String", classLoader)).isEqualTo(String.class); + assertThat(ClassUtils.forName("java.lang.String[]", classLoader)).isEqualTo(String[].class); + assertThat(ClassUtils.forName(String[].class.getName(), classLoader)).isEqualTo(String[].class); + assertThat(ClassUtils.forName(String[][].class.getName(), classLoader)).isEqualTo(String[][].class); + assertThat(ClassUtils.forName(String[][][].class.getName(), classLoader)).isEqualTo(String[][][].class); + assertThat(ClassUtils.forName("org.springframework.tests.sample.objects.TestObject", classLoader)).isEqualTo(TestObject.class); + assertThat(ClassUtils.forName("org.springframework.tests.sample.objects.TestObject[]", classLoader)).isEqualTo(TestObject[].class); + assertThat(ClassUtils.forName(TestObject[].class.getName(), classLoader)).isEqualTo(TestObject[].class); + assertThat(ClassUtils.forName("org.springframework.tests.sample.objects.TestObject[][]", classLoader)).isEqualTo(TestObject[][].class); + assertThat(ClassUtils.forName(TestObject[][].class.getName(), classLoader)).isEqualTo(TestObject[][].class); + assertThat(ClassUtils.forName("[[[S", classLoader)).isEqualTo(short[][][].class); + } + + @Test + void forNameWithPrimitiveClasses() throws ClassNotFoundException { + assertThat(ClassUtils.forName("boolean", classLoader)).isEqualTo(boolean.class); + assertThat(ClassUtils.forName("byte", classLoader)).isEqualTo(byte.class); + assertThat(ClassUtils.forName("char", classLoader)).isEqualTo(char.class); + assertThat(ClassUtils.forName("short", classLoader)).isEqualTo(short.class); + assertThat(ClassUtils.forName("int", classLoader)).isEqualTo(int.class); + assertThat(ClassUtils.forName("long", classLoader)).isEqualTo(long.class); + assertThat(ClassUtils.forName("float", classLoader)).isEqualTo(float.class); + assertThat(ClassUtils.forName("double", classLoader)).isEqualTo(double.class); + assertThat(ClassUtils.forName("void", classLoader)).isEqualTo(void.class); + } + + @Test + void forNameWithPrimitiveArrays() throws ClassNotFoundException { + assertThat(ClassUtils.forName("boolean[]", classLoader)).isEqualTo(boolean[].class); + assertThat(ClassUtils.forName("byte[]", classLoader)).isEqualTo(byte[].class); + assertThat(ClassUtils.forName("char[]", classLoader)).isEqualTo(char[].class); + assertThat(ClassUtils.forName("short[]", classLoader)).isEqualTo(short[].class); + assertThat(ClassUtils.forName("int[]", classLoader)).isEqualTo(int[].class); + assertThat(ClassUtils.forName("long[]", classLoader)).isEqualTo(long[].class); + assertThat(ClassUtils.forName("float[]", classLoader)).isEqualTo(float[].class); + assertThat(ClassUtils.forName("double[]", classLoader)).isEqualTo(double[].class); + } + + @Test + void forNameWithPrimitiveArraysInternalName() throws ClassNotFoundException { + assertThat(ClassUtils.forName(boolean[].class.getName(), classLoader)).isEqualTo(boolean[].class); + assertThat(ClassUtils.forName(byte[].class.getName(), classLoader)).isEqualTo(byte[].class); + assertThat(ClassUtils.forName(char[].class.getName(), classLoader)).isEqualTo(char[].class); + assertThat(ClassUtils.forName(short[].class.getName(), classLoader)).isEqualTo(short[].class); + assertThat(ClassUtils.forName(int[].class.getName(), classLoader)).isEqualTo(int[].class); + assertThat(ClassUtils.forName(long[].class.getName(), classLoader)).isEqualTo(long[].class); + assertThat(ClassUtils.forName(float[].class.getName(), classLoader)).isEqualTo(float[].class); + assertThat(ClassUtils.forName(double[].class.getName(), classLoader)).isEqualTo(double[].class); + } + + @Test + void isCacheSafe() { + ClassLoader childLoader1 = new ClassLoader(classLoader) {}; + ClassLoader childLoader2 = new ClassLoader(classLoader) {}; + ClassLoader childLoader3 = new ClassLoader(classLoader) { + @Override + public Class loadClass(String name) throws ClassNotFoundException { + return childLoader1.loadClass(name); + } + }; + Class composite = ClassUtils.createCompositeInterface( + new Class[] {Serializable.class, Externalizable.class}, childLoader1); + + assertThat(ClassUtils.isCacheSafe(String.class, null)).isTrue(); + assertThat(ClassUtils.isCacheSafe(String.class, classLoader)).isTrue(); + assertThat(ClassUtils.isCacheSafe(String.class, childLoader1)).isTrue(); + assertThat(ClassUtils.isCacheSafe(String.class, childLoader2)).isTrue(); + assertThat(ClassUtils.isCacheSafe(String.class, childLoader3)).isTrue(); + assertThat(ClassUtils.isCacheSafe(InnerClass.class, null)).isFalse(); + assertThat(ClassUtils.isCacheSafe(InnerClass.class, classLoader)).isTrue(); + assertThat(ClassUtils.isCacheSafe(InnerClass.class, childLoader1)).isTrue(); + assertThat(ClassUtils.isCacheSafe(InnerClass.class, childLoader2)).isTrue(); + assertThat(ClassUtils.isCacheSafe(InnerClass.class, childLoader3)).isTrue(); + assertThat(ClassUtils.isCacheSafe(composite, null)).isFalse(); + assertThat(ClassUtils.isCacheSafe(composite, classLoader)).isFalse(); + assertThat(ClassUtils.isCacheSafe(composite, childLoader1)).isTrue(); + assertThat(ClassUtils.isCacheSafe(composite, childLoader2)).isFalse(); + assertThat(ClassUtils.isCacheSafe(composite, childLoader3)).isTrue(); + } + + @ParameterizedTest + @CsvSource({ + "boolean, boolean", + "byte, byte", + "char, char", + "short, short", + "int, int", + "long, long", + "float, float", + "double, double", + "[Z, boolean[]", + "[B, byte[]", + "[C, char[]", + "[S, short[]", + "[I, int[]", + "[J, long[]", + "[F, float[]", + "[D, double[]" + }) + void resolvePrimitiveClassName(String input, Class output) { + assertThat(ClassUtils.resolvePrimitiveClassName(input)).isEqualTo(output); + } + + @Test + void getShortName() { + String className = ClassUtils.getShortName(getClass()); + assertThat(className).as("Class name did not match").isEqualTo("ClassUtilsTests"); + } + + @Test + void getShortNameForObjectArrayClass() { + String className = ClassUtils.getShortName(Object[].class); + assertThat(className).as("Class name did not match").isEqualTo("Object[]"); + } + + @Test + void getShortNameForMultiDimensionalObjectArrayClass() { + String className = ClassUtils.getShortName(Object[][].class); + assertThat(className).as("Class name did not match").isEqualTo("Object[][]"); + } + + @Test + void getShortNameForPrimitiveArrayClass() { + String className = ClassUtils.getShortName(byte[].class); + assertThat(className).as("Class name did not match").isEqualTo("byte[]"); + } + + @Test + void getShortNameForMultiDimensionalPrimitiveArrayClass() { + String className = ClassUtils.getShortName(byte[][][].class); + assertThat(className).as("Class name did not match").isEqualTo("byte[][][]"); + } + + @Test + void getShortNameForInnerClass() { + String className = ClassUtils.getShortName(InnerClass.class); + assertThat(className).as("Class name did not match").isEqualTo("ClassUtilsTests.InnerClass"); + } + + @Test + void getShortNameAsProperty() { + String shortName = ClassUtils.getShortNameAsProperty(this.getClass()); + assertThat(shortName).as("Class name did not match").isEqualTo("classUtilsTests"); + } + + @Test + void getClassFileName() { + assertThat(ClassUtils.getClassFileName(String.class)).isEqualTo("String.class"); + assertThat(ClassUtils.getClassFileName(getClass())).isEqualTo("ClassUtilsTests.class"); + } + + @Test + void getPackageName() { + assertThat(ClassUtils.getPackageName(String.class)).isEqualTo("java.lang"); + assertThat(ClassUtils.getPackageName(getClass())).isEqualTo(getClass().getPackage().getName()); + } + + @Test + void getQualifiedName() { + String className = ClassUtils.getQualifiedName(getClass()); + assertThat(className).as("Class name did not match").isEqualTo("org.springframework.util.ClassUtilsTests"); + } + + @Test + void getQualifiedNameForObjectArrayClass() { + String className = ClassUtils.getQualifiedName(Object[].class); + assertThat(className).as("Class name did not match").isEqualTo("java.lang.Object[]"); + } + + @Test + void getQualifiedNameForMultiDimensionalObjectArrayClass() { + String className = ClassUtils.getQualifiedName(Object[][].class); + assertThat(className).as("Class name did not match").isEqualTo("java.lang.Object[][]"); + } + + @Test + void getQualifiedNameForPrimitiveArrayClass() { + String className = ClassUtils.getQualifiedName(byte[].class); + assertThat(className).as("Class name did not match").isEqualTo("byte[]"); + } + + @Test + void getQualifiedNameForMultiDimensionalPrimitiveArrayClass() { + String className = ClassUtils.getQualifiedName(byte[][].class); + assertThat(className).as("Class name did not match").isEqualTo("byte[][]"); + } + + @Test + void hasMethod() { + assertThat(ClassUtils.hasMethod(Collection.class, "size")).isTrue(); + assertThat(ClassUtils.hasMethod(Collection.class, "remove", Object.class)).isTrue(); + assertThat(ClassUtils.hasMethod(Collection.class, "remove")).isFalse(); + assertThat(ClassUtils.hasMethod(Collection.class, "someOtherMethod")).isFalse(); + } + + @Test + void getMethodIfAvailable() { + Method method = ClassUtils.getMethodIfAvailable(Collection.class, "size"); + assertThat(method).isNotNull(); + assertThat(method.getName()).isEqualTo("size"); + + method = ClassUtils.getMethodIfAvailable(Collection.class, "remove", Object.class); + assertThat(method).isNotNull(); + assertThat(method.getName()).isEqualTo("remove"); + + assertThat(ClassUtils.getMethodIfAvailable(Collection.class, "remove")).isNull(); + assertThat(ClassUtils.getMethodIfAvailable(Collection.class, "someOtherMethod")).isNull(); + } + + @Test + void getMethodCountForName() { + assertThat(ClassUtils.getMethodCountForName(OverloadedMethodsClass.class, "print")).as("Verifying number of overloaded 'print' methods for OverloadedMethodsClass.").isEqualTo(2); + assertThat(ClassUtils.getMethodCountForName(SubOverloadedMethodsClass.class, "print")).as("Verifying number of overloaded 'print' methods for SubOverloadedMethodsClass.").isEqualTo(4); + } + + @Test + void countOverloadedMethods() { + assertThat(ClassUtils.hasAtLeastOneMethodWithName(TestObject.class, "foobar")).isFalse(); + // no args + assertThat(ClassUtils.hasAtLeastOneMethodWithName(TestObject.class, "hashCode")).isTrue(); + // matches although it takes an arg + assertThat(ClassUtils.hasAtLeastOneMethodWithName(TestObject.class, "setAge")).isTrue(); + } + + @Test + void noArgsStaticMethod() throws IllegalAccessException, InvocationTargetException { + Method method = ClassUtils.getStaticMethod(InnerClass.class, "staticMethod"); + method.invoke(null, (Object[]) null); + assertThat(InnerClass.noArgCalled).as("no argument method was not invoked.").isTrue(); + } + + @Test + void argsStaticMethod() throws IllegalAccessException, InvocationTargetException { + Method method = ClassUtils.getStaticMethod(InnerClass.class, "argStaticMethod", String.class); + method.invoke(null, "test"); + assertThat(InnerClass.argCalled).as("argument method was not invoked.").isTrue(); + } + + @Test + void overloadedStaticMethod() throws IllegalAccessException, InvocationTargetException { + Method method = ClassUtils.getStaticMethod(InnerClass.class, "staticMethod", String.class); + method.invoke(null, "test"); + assertThat(InnerClass.overloadedCalled).as("argument method was not invoked.").isTrue(); + } + + @Test + void isAssignable() { + assertThat(ClassUtils.isAssignable(Object.class, Object.class)).isTrue(); + assertThat(ClassUtils.isAssignable(String.class, String.class)).isTrue(); + assertThat(ClassUtils.isAssignable(Object.class, String.class)).isTrue(); + assertThat(ClassUtils.isAssignable(Object.class, Integer.class)).isTrue(); + assertThat(ClassUtils.isAssignable(Number.class, Integer.class)).isTrue(); + assertThat(ClassUtils.isAssignable(Number.class, int.class)).isTrue(); + assertThat(ClassUtils.isAssignable(Integer.class, int.class)).isTrue(); + assertThat(ClassUtils.isAssignable(int.class, Integer.class)).isTrue(); + assertThat(ClassUtils.isAssignable(String.class, Object.class)).isFalse(); + assertThat(ClassUtils.isAssignable(Integer.class, Number.class)).isFalse(); + assertThat(ClassUtils.isAssignable(Integer.class, double.class)).isFalse(); + assertThat(ClassUtils.isAssignable(double.class, Integer.class)).isFalse(); + } + + @Test + void classPackageAsResourcePath() { + String result = ClassUtils.classPackageAsResourcePath(Proxy.class); + assertThat(result).isEqualTo("java/lang/reflect"); + } + + @Test + void addResourcePathToPackagePath() { + String result = "java/lang/reflect/xyzabc.xml"; + assertThat(ClassUtils.addResourcePathToPackagePath(Proxy.class, "xyzabc.xml")).isEqualTo(result); + assertThat(ClassUtils.addResourcePathToPackagePath(Proxy.class, "/xyzabc.xml")).isEqualTo(result); + + assertThat(ClassUtils.addResourcePathToPackagePath(Proxy.class, "a/b/c/d.xml")).isEqualTo("java/lang/reflect/a/b/c/d.xml"); + } + + @Test + void getAllInterfaces() { + DerivedTestObject testBean = new DerivedTestObject(); + List> ifcs = Arrays.asList(ClassUtils.getAllInterfaces(testBean)); + assertThat(ifcs.size()).as("Correct number of interfaces").isEqualTo(4); + assertThat(ifcs.contains(Serializable.class)).as("Contains Serializable").isTrue(); + assertThat(ifcs.contains(ITestObject.class)).as("Contains ITestBean").isTrue(); + assertThat(ifcs.contains(ITestInterface.class)).as("Contains IOther").isTrue(); + } + + @Test + void classNamesToString() { + List> ifcs = new ArrayList<>(); + ifcs.add(Serializable.class); + ifcs.add(Runnable.class); + assertThat(ifcs.toString()).isEqualTo("[interface java.io.Serializable, interface java.lang.Runnable]"); + assertThat(ClassUtils.classNamesToString(ifcs)).isEqualTo("[java.io.Serializable, java.lang.Runnable]"); + + List> classes = new ArrayList<>(); + classes.add(ArrayList.class); + classes.add(Integer.class); + assertThat(classes.toString()).isEqualTo("[class java.util.ArrayList, class java.lang.Integer]"); + assertThat(ClassUtils.classNamesToString(classes)).isEqualTo("[java.util.ArrayList, java.lang.Integer]"); + + assertThat(Collections.singletonList(List.class).toString()).isEqualTo("[interface java.util.List]"); + assertThat(ClassUtils.classNamesToString(List.class)).isEqualTo("[java.util.List]"); + + assertThat(Collections.EMPTY_LIST.toString()).isEqualTo("[]"); + assertThat(ClassUtils.classNamesToString(Collections.emptyList())).isEqualTo("[]"); + } + + @Test + void determineCommonAncestor() { + assertThat(ClassUtils.determineCommonAncestor(Integer.class, Number.class)).isEqualTo(Number.class); + assertThat(ClassUtils.determineCommonAncestor(Number.class, Integer.class)).isEqualTo(Number.class); + assertThat(ClassUtils.determineCommonAncestor(Number.class, null)).isEqualTo(Number.class); + assertThat(ClassUtils.determineCommonAncestor(null, Integer.class)).isEqualTo(Integer.class); + assertThat(ClassUtils.determineCommonAncestor(Integer.class, Integer.class)).isEqualTo(Integer.class); + + assertThat(ClassUtils.determineCommonAncestor(Integer.class, Float.class)).isEqualTo(Number.class); + assertThat(ClassUtils.determineCommonAncestor(Float.class, Integer.class)).isEqualTo(Number.class); + assertThat(ClassUtils.determineCommonAncestor(Integer.class, String.class)).isNull(); + assertThat(ClassUtils.determineCommonAncestor(String.class, Integer.class)).isNull(); + + assertThat(ClassUtils.determineCommonAncestor(List.class, Collection.class)).isEqualTo(Collection.class); + assertThat(ClassUtils.determineCommonAncestor(Collection.class, List.class)).isEqualTo(Collection.class); + assertThat(ClassUtils.determineCommonAncestor(Collection.class, null)).isEqualTo(Collection.class); + assertThat(ClassUtils.determineCommonAncestor(null, List.class)).isEqualTo(List.class); + assertThat(ClassUtils.determineCommonAncestor(List.class, List.class)).isEqualTo(List.class); + + assertThat(ClassUtils.determineCommonAncestor(List.class, Set.class)).isNull(); + assertThat(ClassUtils.determineCommonAncestor(Set.class, List.class)).isNull(); + assertThat(ClassUtils.determineCommonAncestor(List.class, Runnable.class)).isNull(); + assertThat(ClassUtils.determineCommonAncestor(Runnable.class, List.class)).isNull(); + + assertThat(ClassUtils.determineCommonAncestor(List.class, ArrayList.class)).isEqualTo(List.class); + assertThat(ClassUtils.determineCommonAncestor(ArrayList.class, List.class)).isEqualTo(List.class); + assertThat(ClassUtils.determineCommonAncestor(List.class, String.class)).isNull(); + assertThat(ClassUtils.determineCommonAncestor(String.class, List.class)).isNull(); + } + + @ParameterizedTest + @WrapperTypes + void isPrimitiveWrapper(Class type) { + assertThat(ClassUtils.isPrimitiveWrapper(type)).isTrue(); + } + + @ParameterizedTest + @PrimitiveTypes + void isPrimitiveOrWrapperWithPrimitive(Class type) { + assertThat(ClassUtils.isPrimitiveOrWrapper(type)).isTrue(); + } + + @ParameterizedTest + @WrapperTypes + void isPrimitiveOrWrapperWithWrapper(Class type) { + assertThat(ClassUtils.isPrimitiveOrWrapper(type)).isTrue(); + } + + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @ValueSource(classes = { Boolean.class, Character.class, Byte.class, Short.class, + Integer.class, Long.class, Float.class, Double.class, Void.class }) + @interface WrapperTypes { + } + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @ValueSource(classes = { boolean.class, char.class, byte.class, short.class, + int.class, long.class, float.class, double.class, void.class }) + @interface PrimitiveTypes { + } + + public static class InnerClass { + + static boolean noArgCalled; + static boolean argCalled; + static boolean overloadedCalled; + + public static void staticMethod() { + noArgCalled = true; + } + + public static void staticMethod(String anArg) { + overloadedCalled = true; + } + + public static void argStaticMethod(String anArg) { + argCalled = true; + } + } + + @SuppressWarnings("unused") + private static class OverloadedMethodsClass { + + public void print(String messages) { + /* no-op */ + } + + public void print(String[] messages) { + /* no-op */ + } + } + + @SuppressWarnings("unused") + private static class SubOverloadedMethodsClass extends OverloadedMethodsClass { + + public void print(String header, String[] messages) { + /* no-op */ + } + + void print(String header, String[] messages, String footer) { + /* no-op */ + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/CollectionUtilsTests.java b/spring-core/src/test/java/org/springframework/util/CollectionUtilsTests.java new file mode 100644 index 0000000..451a689 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/CollectionUtilsTests.java @@ -0,0 +1,239 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + * @author Juergen Hoeller + * @author Rick Evans + */ +class CollectionUtilsTests { + + @Test + void isEmpty() { + assertThat(CollectionUtils.isEmpty((Set) null)).isTrue(); + assertThat(CollectionUtils.isEmpty((Map) null)).isTrue(); + assertThat(CollectionUtils.isEmpty(new HashMap())).isTrue(); + assertThat(CollectionUtils.isEmpty(new HashSet<>())).isTrue(); + + List list = new ArrayList<>(); + list.add(new Object()); + assertThat(CollectionUtils.isEmpty(list)).isFalse(); + + Map map = new HashMap<>(); + map.put("foo", "bar"); + assertThat(CollectionUtils.isEmpty(map)).isFalse(); + } + + @Test + void mergeArrayIntoCollection() { + Object[] arr = new Object[] {"value1", "value2"}; + List> list = new ArrayList<>(); + list.add("value3"); + + CollectionUtils.mergeArrayIntoCollection(arr, list); + assertThat(list.get(0)).isEqualTo("value3"); + assertThat(list.get(1)).isEqualTo("value1"); + assertThat(list.get(2)).isEqualTo("value2"); + } + + @Test + void mergePrimitiveArrayIntoCollection() { + int[] arr = new int[] {1, 2}; + List> list = new ArrayList<>(); + list.add(Integer.valueOf(3)); + + CollectionUtils.mergeArrayIntoCollection(arr, list); + assertThat(list.get(0)).isEqualTo(Integer.valueOf(3)); + assertThat(list.get(1)).isEqualTo(Integer.valueOf(1)); + assertThat(list.get(2)).isEqualTo(Integer.valueOf(2)); + } + + @Test + void mergePropertiesIntoMap() { + Properties defaults = new Properties(); + defaults.setProperty("prop1", "value1"); + Properties props = new Properties(defaults); + props.setProperty("prop2", "value2"); + props.put("prop3", Integer.valueOf(3)); + + Map map = new HashMap<>(); + map.put("prop4", "value4"); + + CollectionUtils.mergePropertiesIntoMap(props, map); + assertThat(map.get("prop1")).isEqualTo("value1"); + assertThat(map.get("prop2")).isEqualTo("value2"); + assertThat(map.get("prop3")).isEqualTo(Integer.valueOf(3)); + assertThat(map.get("prop4")).isEqualTo("value4"); + } + + @Test + void contains() { + assertThat(CollectionUtils.contains((Iterator) null, "myElement")).isFalse(); + assertThat(CollectionUtils.contains((Enumeration) null, "myElement")).isFalse(); + assertThat(CollectionUtils.contains(new ArrayList().iterator(), "myElement")).isFalse(); + assertThat(CollectionUtils.contains(new Hashtable().keys(), "myElement")).isFalse(); + + List list = new ArrayList<>(); + list.add("myElement"); + assertThat(CollectionUtils.contains(list.iterator(), "myElement")).isTrue(); + + Hashtable ht = new Hashtable<>(); + ht.put("myElement", "myValue"); + assertThat(CollectionUtils.contains(ht.keys(), "myElement")).isTrue(); + } + + @Test + void containsAny() throws Exception { + List source = new ArrayList<>(); + source.add("abc"); + source.add("def"); + source.add("ghi"); + + List candidates = new ArrayList<>(); + candidates.add("xyz"); + candidates.add("def"); + candidates.add("abc"); + + assertThat(CollectionUtils.containsAny(source, candidates)).isTrue(); + candidates.remove("def"); + assertThat(CollectionUtils.containsAny(source, candidates)).isTrue(); + candidates.remove("abc"); + assertThat(CollectionUtils.containsAny(source, candidates)).isFalse(); + } + + @Test + void containsInstanceWithNullCollection() throws Exception { + assertThat(CollectionUtils.containsInstance(null, this)).as("Must return false if supplied Collection argument is null").isFalse(); + } + + @Test + void containsInstanceWithInstancesThatAreEqualButDistinct() throws Exception { + List list = new ArrayList<>(); + list.add(new Instance("fiona")); + assertThat(CollectionUtils.containsInstance(list, new Instance("fiona"))).as("Must return false if instance is not in the supplied Collection argument").isFalse(); + } + + @Test + void containsInstanceWithSameInstance() throws Exception { + List list = new ArrayList<>(); + list.add(new Instance("apple")); + Instance instance = new Instance("fiona"); + list.add(instance); + assertThat(CollectionUtils.containsInstance(list, instance)).as("Must return true if instance is in the supplied Collection argument").isTrue(); + } + + @Test + void containsInstanceWithNullInstance() throws Exception { + List list = new ArrayList<>(); + list.add(new Instance("apple")); + list.add(new Instance("fiona")); + assertThat(CollectionUtils.containsInstance(list, null)).as("Must return false if null instance is supplied").isFalse(); + } + + @Test + void findFirstMatch() throws Exception { + List source = new ArrayList<>(); + source.add("abc"); + source.add("def"); + source.add("ghi"); + + List candidates = new ArrayList<>(); + candidates.add("xyz"); + candidates.add("def"); + candidates.add("abc"); + + assertThat(CollectionUtils.findFirstMatch(source, candidates)).isEqualTo("def"); + } + + @Test + void hasUniqueObject() { + List list = new ArrayList<>(); + list.add("myElement"); + list.add("myOtherElement"); + assertThat(CollectionUtils.hasUniqueObject(list)).isFalse(); + + list = new ArrayList<>(); + list.add("myElement"); + assertThat(CollectionUtils.hasUniqueObject(list)).isTrue(); + + list = new ArrayList<>(); + list.add("myElement"); + list.add(null); + assertThat(CollectionUtils.hasUniqueObject(list)).isFalse(); + + list = new ArrayList<>(); + list.add(null); + list.add("myElement"); + assertThat(CollectionUtils.hasUniqueObject(list)).isFalse(); + + list = new ArrayList<>(); + list.add(null); + list.add(null); + assertThat(CollectionUtils.hasUniqueObject(list)).isTrue(); + + list = new ArrayList<>(); + list.add(null); + assertThat(CollectionUtils.hasUniqueObject(list)).isTrue(); + + list = new ArrayList<>(); + assertThat(CollectionUtils.hasUniqueObject(list)).isFalse(); + } + + + private static final class Instance { + + private final String name; + + public Instance(String name) { + this.name = name; + } + + @Override + public boolean equals(Object rhs) { + if (this == rhs) { + return true; + } + if (rhs == null || this.getClass() != rhs.getClass()) { + return false; + } + Instance instance = (Instance) rhs; + return this.name.equals(instance.name); + } + + @Override + public int hashCode() { + return this.name.hashCode(); + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/CompositeIteratorTests.java b/spring-core/src/test/java/org/springframework/util/CompositeIteratorTests.java new file mode 100644 index 0000000..737e062 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/CompositeIteratorTests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Test case for {@link CompositeIterator}. + * + * @author Erwin Vervaet + * @author Juergen Hoeller + */ +class CompositeIteratorTests { + + @Test + void noIterators() { + CompositeIterator it = new CompositeIterator<>(); + assertThat(it.hasNext()).isFalse(); + assertThatExceptionOfType(NoSuchElementException.class).isThrownBy( + it::next); + } + + @Test + void singleIterator() { + CompositeIterator it = new CompositeIterator<>(); + it.add(Arrays.asList("0", "1").iterator()); + for (int i = 0; i < 2; i++) { + assertThat(it.hasNext()).isTrue(); + assertThat(it.next()).isEqualTo(String.valueOf(i)); + } + assertThat(it.hasNext()).isFalse(); + assertThatExceptionOfType(NoSuchElementException.class).isThrownBy( + it::next); + } + + @Test + void multipleIterators() { + CompositeIterator it = new CompositeIterator<>(); + it.add(Arrays.asList("0", "1").iterator()); + it.add(Arrays.asList("2").iterator()); + it.add(Arrays.asList("3", "4").iterator()); + for (int i = 0; i < 5; i++) { + assertThat(it.hasNext()).isTrue(); + assertThat(it.next()).isEqualTo(String.valueOf(i)); + } + assertThat(it.hasNext()).isFalse(); + + assertThatExceptionOfType(NoSuchElementException.class).isThrownBy( + it::next); + } + + @Test + void inUse() { + List list = Arrays.asList("0", "1"); + CompositeIterator it = new CompositeIterator<>(); + it.add(list.iterator()); + it.hasNext(); + assertThatIllegalStateException().isThrownBy(() -> + it.add(list.iterator())); + CompositeIterator it2 = new CompositeIterator<>(); + it2.add(list.iterator()); + it2.next(); + assertThatIllegalStateException().isThrownBy(() -> + it2.add(list.iterator())); + } + + @Test + void duplicateIterators() { + List list = Arrays.asList("0", "1"); + Iterator iterator = list.iterator(); + CompositeIterator it = new CompositeIterator<>(); + it.add(iterator); + it.add(list.iterator()); + assertThatIllegalArgumentException().isThrownBy(() -> + it.add(iterator)); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/ConcurrentLruCacheTests.java b/spring-core/src/test/java/org/springframework/util/ConcurrentLruCacheTests.java new file mode 100644 index 0000000..31ec759 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/ConcurrentLruCacheTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + */ +class ConcurrentLruCacheTests { + + private final ConcurrentLruCache cache = new ConcurrentLruCache<>(2, key -> key + "value"); + + + @Test + void getAndSize() { + assertThat(this.cache.sizeLimit()).isEqualTo(2); + assertThat(this.cache.size()).isEqualTo(0); + assertThat(this.cache.get("k1")).isEqualTo("k1value"); + assertThat(this.cache.size()).isEqualTo(1); + assertThat(this.cache.contains("k1")).isTrue(); + assertThat(this.cache.get("k2")).isEqualTo("k2value"); + assertThat(this.cache.size()).isEqualTo(2); + assertThat(this.cache.contains("k1")).isTrue(); + assertThat(this.cache.contains("k2")).isTrue(); + assertThat(this.cache.get("k3")).isEqualTo("k3value"); + assertThat(this.cache.size()).isEqualTo(2); + assertThat(this.cache.contains("k1")).isFalse(); + assertThat(this.cache.contains("k2")).isTrue(); + assertThat(this.cache.contains("k3")).isTrue(); + } + + @Test + void removeAndSize() { + assertThat(this.cache.get("k1")).isEqualTo("k1value"); + assertThat(this.cache.get("k2")).isEqualTo("k2value"); + assertThat(this.cache.size()).isEqualTo(2); + assertThat(this.cache.contains("k1")).isTrue(); + assertThat(this.cache.contains("k2")).isTrue(); + this.cache.remove("k2"); + assertThat(this.cache.size()).isEqualTo(1); + assertThat(this.cache.contains("k1")).isTrue(); + assertThat(this.cache.contains("k2")).isFalse(); + assertThat(this.cache.get("k3")).isEqualTo("k3value"); + assertThat(this.cache.size()).isEqualTo(2); + assertThat(this.cache.contains("k1")).isTrue(); + assertThat(this.cache.contains("k2")).isFalse(); + assertThat(this.cache.contains("k3")).isTrue(); + } + + @Test + void clearAndSize() { + assertThat(this.cache.get("k1")).isEqualTo("k1value"); + assertThat(this.cache.get("k2")).isEqualTo("k2value"); + assertThat(this.cache.size()).isEqualTo(2); + assertThat(this.cache.contains("k1")).isTrue(); + assertThat(this.cache.contains("k2")).isTrue(); + this.cache.clear(); + assertThat(this.cache.size()).isEqualTo(0); + assertThat(this.cache.contains("k1")).isFalse(); + assertThat(this.cache.contains("k2")).isFalse(); + assertThat(this.cache.get("k3")).isEqualTo("k3value"); + assertThat(this.cache.size()).isEqualTo(1); + assertThat(this.cache.contains("k1")).isFalse(); + assertThat(this.cache.contains("k2")).isFalse(); + assertThat(this.cache.contains("k3")).isTrue(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java b/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java new file mode 100644 index 0000000..b8ddb11 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java @@ -0,0 +1,662 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.lang.Nullable; +import org.springframework.util.ConcurrentReferenceHashMap.Entry; +import org.springframework.util.ConcurrentReferenceHashMap.Reference; +import org.springframework.util.ConcurrentReferenceHashMap.Restructure; +import org.springframework.util.comparator.ComparableComparator; +import org.springframework.util.comparator.NullSafeComparator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link ConcurrentReferenceHashMap}. + * + * @author Phillip Webb + */ +class ConcurrentReferenceHashMapTests { + + private static final Comparator NULL_SAFE_STRING_SORT = new NullSafeComparator( + new ComparableComparator(), true); + + private TestWeakConcurrentCache map = new TestWeakConcurrentCache<>(); + + + @Test + void shouldCreateWithDefaults() { + ConcurrentReferenceHashMap map = new ConcurrentReferenceHashMap<>(); + assertThat(map.getSegmentsSize()).isEqualTo(16); + assertThat(map.getSegment(0).getSize()).isEqualTo(1); + assertThat(map.getLoadFactor()).isEqualTo(0.75f); + } + + @Test + void shouldCreateWithInitialCapacity() { + ConcurrentReferenceHashMap map = new ConcurrentReferenceHashMap<>(32); + assertThat(map.getSegmentsSize()).isEqualTo(16); + assertThat(map.getSegment(0).getSize()).isEqualTo(2); + assertThat(map.getLoadFactor()).isEqualTo(0.75f); + } + + @Test + void shouldCreateWithInitialCapacityAndLoadFactor() { + ConcurrentReferenceHashMap map = new ConcurrentReferenceHashMap<>(32, 0.5f); + assertThat(map.getSegmentsSize()).isEqualTo(16); + assertThat(map.getSegment(0).getSize()).isEqualTo(2); + assertThat(map.getLoadFactor()).isEqualTo(0.5f); + } + + @Test + void shouldCreateWithInitialCapacityAndConcurrentLevel() { + ConcurrentReferenceHashMap map = new ConcurrentReferenceHashMap<>(16, 2); + assertThat(map.getSegmentsSize()).isEqualTo(2); + assertThat(map.getSegment(0).getSize()).isEqualTo(8); + assertThat(map.getLoadFactor()).isEqualTo(0.75f); + } + + @Test + void shouldCreateFullyCustom() { + ConcurrentReferenceHashMap map = new ConcurrentReferenceHashMap<>(5, 0.5f, 3); + // concurrencyLevel of 3 ends up as 4 (nearest power of 2) + assertThat(map.getSegmentsSize()).isEqualTo(4); + // initialCapacity is 5/4 (rounded up, to nearest power of 2) + assertThat(map.getSegment(0).getSize()).isEqualTo(2); + assertThat(map.getLoadFactor()).isEqualTo(0.5f); + } + + @Test + void shouldNeedNonNegativeInitialCapacity() { + new ConcurrentReferenceHashMap(0, 1); + assertThatIllegalArgumentException().isThrownBy(() -> + new TestWeakConcurrentCache(-1, 1)) + .withMessageContaining("Initial capacity must not be negative"); + } + + @Test + void shouldNeedPositiveLoadFactor() { + new ConcurrentReferenceHashMap(0, 0.1f, 1); + assertThatIllegalArgumentException().isThrownBy(() -> + new TestWeakConcurrentCache(0, 0.0f, 1)) + .withMessageContaining("Load factor must be positive"); + } + + @Test + void shouldNeedPositiveConcurrencyLevel() { + new ConcurrentReferenceHashMap(1, 1); + assertThatIllegalArgumentException().isThrownBy(() -> + new TestWeakConcurrentCache(1, 0)) + .withMessageContaining("Concurrency level must be positive"); + } + + @Test + void shouldPutAndGet() { + // NOTE we are using mock references so we don't need to worry about GC + assertThat(this.map).hasSize(0); + this.map.put(123, "123"); + assertThat(this.map.get(123)).isEqualTo("123"); + assertThat(this.map).hasSize(1); + this.map.put(123, "123b"); + assertThat(this.map).hasSize(1); + this.map.put(123, null); + assertThat(this.map).hasSize(1); + } + + @Test + void shouldReplaceOnDoublePut() { + this.map.put(123, "321"); + this.map.put(123, "123"); + assertThat(this.map.get(123)).isEqualTo("123"); + } + + @Test + void shouldPutNullKey() { + assertThat(this.map.get(null)).isNull(); + assertThat(this.map.getOrDefault(null, "456")).isEqualTo("456"); + this.map.put(null, "123"); + assertThat(this.map.get(null)).isEqualTo("123"); + assertThat(this.map.getOrDefault(null, "456")).isEqualTo("123"); + } + + @Test + void shouldPutNullValue() { + assertThat(this.map.get(123)).isNull(); + assertThat(this.map.getOrDefault(123, "456")).isEqualTo("456"); + this.map.put(123, "321"); + assertThat(this.map.get(123)).isEqualTo("321"); + assertThat(this.map.getOrDefault(123, "456")).isEqualTo("321"); + this.map.put(123, null); + assertThat(this.map.get(123)).isNull(); + assertThat(this.map.getOrDefault(123, "456")).isNull(); + } + + @Test + void shouldGetWithNoItems() { + assertThat(this.map.get(123)).isNull(); + } + + @Test + void shouldApplySupplementalHash() { + Integer key = 123; + this.map.put(key, "123"); + assertThat(this.map.getSupplementalHash()).isNotEqualTo(key.hashCode()); + assertThat(this.map.getSupplementalHash() >> 30 & 0xFF).isNotEqualTo(0); + } + + @Test + void shouldGetFollowingNexts() { + // Use loadFactor to disable resize + this.map = new TestWeakConcurrentCache<>(1, 10.0f, 1); + this.map.put(1, "1"); + this.map.put(2, "2"); + this.map.put(3, "3"); + assertThat(this.map.getSegment(0).getSize()).isEqualTo(1); + assertThat(this.map.get(1)).isEqualTo("1"); + assertThat(this.map.get(2)).isEqualTo("2"); + assertThat(this.map.get(3)).isEqualTo("3"); + assertThat(this.map.get(4)).isNull(); + } + + @Test + void shouldResize() { + this.map = new TestWeakConcurrentCache<>(1, 0.75f, 1); + this.map.put(1, "1"); + assertThat(this.map.getSegment(0).getSize()).isEqualTo(1); + assertThat(this.map.get(1)).isEqualTo("1"); + + this.map.put(2, "2"); + assertThat(this.map.getSegment(0).getSize()).isEqualTo(2); + assertThat(this.map.get(1)).isEqualTo("1"); + assertThat(this.map.get(2)).isEqualTo("2"); + + this.map.put(3, "3"); + assertThat(this.map.getSegment(0).getSize()).isEqualTo(4); + assertThat(this.map.get(1)).isEqualTo("1"); + assertThat(this.map.get(2)).isEqualTo("2"); + assertThat(this.map.get(3)).isEqualTo("3"); + + this.map.put(4, "4"); + assertThat(this.map.getSegment(0).getSize()).isEqualTo(8); + assertThat(this.map.get(4)).isEqualTo("4"); + + // Putting again should not increase the count + for (int i = 1; i <= 5; i++) { + this.map.put(i, String.valueOf(i)); + } + assertThat(this.map.getSegment(0).getSize()).isEqualTo(8); + assertThat(this.map.get(5)).isEqualTo("5"); + } + + @Test + void shouldPurgeOnGet() { + this.map = new TestWeakConcurrentCache<>(1, 0.75f, 1); + for (int i = 1; i <= 5; i++) { + this.map.put(i, String.valueOf(i)); + } + this.map.getMockReference(1, Restructure.NEVER).queueForPurge(); + this.map.getMockReference(3, Restructure.NEVER).queueForPurge(); + assertThat(this.map.getReference(1, Restructure.WHEN_NECESSARY)).isNull(); + assertThat(this.map.get(2)).isEqualTo("2"); + assertThat(this.map.getReference(3, Restructure.WHEN_NECESSARY)).isNull(); + assertThat(this.map.get(4)).isEqualTo("4"); + assertThat(this.map.get(5)).isEqualTo("5"); + } + + @Test + void shouldPurgeOnPut() { + this.map = new TestWeakConcurrentCache<>(1, 0.75f, 1); + for (int i = 1; i <= 5; i++) { + this.map.put(i, String.valueOf(i)); + } + this.map.getMockReference(1, Restructure.NEVER).queueForPurge(); + this.map.getMockReference(3, Restructure.NEVER).queueForPurge(); + this.map.put(1, "1"); + assertThat(this.map.get(1)).isEqualTo("1"); + assertThat(this.map.get(2)).isEqualTo("2"); + assertThat(this.map.getReference(3, Restructure.WHEN_NECESSARY)).isNull(); + assertThat(this.map.get(4)).isEqualTo("4"); + assertThat(this.map.get(5)).isEqualTo("5"); + } + + @Test + void shouldPutIfAbsent() { + assertThat(this.map.putIfAbsent(123, "123")).isNull(); + assertThat(this.map.putIfAbsent(123, "123b")).isEqualTo("123"); + assertThat(this.map.get(123)).isEqualTo("123"); + } + + @Test + void shouldPutIfAbsentWithNullValue() { + assertThat(this.map.putIfAbsent(123, null)).isNull(); + assertThat(this.map.putIfAbsent(123, "123")).isNull(); + assertThat(this.map.get(123)).isNull(); + } + + @Test + void shouldPutIfAbsentWithNullKey() { + assertThat(this.map.putIfAbsent(null, "123")).isNull(); + assertThat(this.map.putIfAbsent(null, "123b")).isEqualTo("123"); + assertThat(this.map.get(null)).isEqualTo("123"); + } + + @Test + void shouldRemoveKeyAndValue() { + this.map.put(123, "123"); + assertThat(this.map.remove(123, "456")).isFalse(); + assertThat(this.map.get(123)).isEqualTo("123"); + assertThat(this.map.remove(123, "123")).isTrue(); + assertThat(this.map.containsKey(123)).isFalse(); + assertThat(this.map.isEmpty()).isTrue(); + } + + @Test + void shouldRemoveKeyAndValueWithExistingNull() { + this.map.put(123, null); + assertThat(this.map.remove(123, "456")).isFalse(); + assertThat(this.map.get(123)).isNull(); + assertThat(this.map.remove(123, null)).isTrue(); + assertThat(this.map.containsKey(123)).isFalse(); + assertThat(this.map.isEmpty()).isTrue(); + } + + @Test + void shouldReplaceOldValueWithNewValue() { + this.map.put(123, "123"); + assertThat(this.map.replace(123, "456", "789")).isFalse(); + assertThat(this.map.get(123)).isEqualTo("123"); + assertThat(this.map.replace(123, "123", "789")).isTrue(); + assertThat(this.map.get(123)).isEqualTo("789"); + } + + @Test + void shouldReplaceOldNullValueWithNewValue() { + this.map.put(123, null); + assertThat(this.map.replace(123, "456", "789")).isFalse(); + assertThat(this.map.get(123)).isNull(); + assertThat(this.map.replace(123, null, "789")).isTrue(); + assertThat(this.map.get(123)).isEqualTo("789"); + } + + @Test + void shouldReplaceValue() { + this.map.put(123, "123"); + assertThat(this.map.replace(123, "456")).isEqualTo("123"); + assertThat(this.map.get(123)).isEqualTo("456"); + } + + @Test + void shouldReplaceNullValue() { + this.map.put(123, null); + assertThat(this.map.replace(123, "456")).isNull(); + assertThat(this.map.get(123)).isEqualTo("456"); + } + + @Test + void shouldGetSize() { + assertThat(this.map).hasSize(0); + this.map.put(123, "123"); + this.map.put(123, null); + this.map.put(456, "456"); + assertThat(this.map).hasSize(2); + } + + @Test + void shouldSupportIsEmpty() { + assertThat(this.map.isEmpty()).isTrue(); + this.map.put(123, "123"); + this.map.put(123, null); + this.map.put(456, "456"); + assertThat(this.map.isEmpty()).isFalse(); + } + + @Test + void shouldContainKey() { + assertThat(this.map.containsKey(123)).isFalse(); + assertThat(this.map.containsKey(456)).isFalse(); + this.map.put(123, "123"); + this.map.put(456, null); + assertThat(this.map.containsKey(123)).isTrue(); + assertThat(this.map.containsKey(456)).isTrue(); + } + + @Test + void shouldContainValue() { + assertThat(this.map.containsValue("123")).isFalse(); + assertThat(this.map.containsValue(null)).isFalse(); + this.map.put(123, "123"); + this.map.put(456, null); + assertThat(this.map.containsValue("123")).isTrue(); + assertThat(this.map.containsValue(null)).isTrue(); + } + + @Test + void shouldRemoveWhenKeyIsInMap() { + this.map.put(123, null); + this.map.put(456, "456"); + this.map.put(null, "789"); + assertThat(this.map.remove(123)).isNull(); + assertThat(this.map.remove(456)).isEqualTo("456"); + assertThat(this.map.remove(null)).isEqualTo("789"); + assertThat(this.map.isEmpty()).isTrue(); + } + + @Test + void shouldRemoveWhenKeyIsNotInMap() { + assertThat(this.map.remove(123)).isNull(); + assertThat(this.map.remove(null)).isNull(); + assertThat(this.map.isEmpty()).isTrue(); + } + + @Test + void shouldPutAll() { + Map m = new HashMap<>(); + m.put(123, "123"); + m.put(456, null); + m.put(null, "789"); + this.map.putAll(m); + assertThat(this.map).hasSize(3); + assertThat(this.map.get(123)).isEqualTo("123"); + assertThat(this.map.get(456)).isNull(); + assertThat(this.map.get(null)).isEqualTo("789"); + } + + @Test + void shouldClear() { + this.map.put(123, "123"); + this.map.put(456, null); + this.map.put(null, "789"); + this.map.clear(); + assertThat(this.map).hasSize(0); + assertThat(this.map.containsKey(123)).isFalse(); + assertThat(this.map.containsKey(456)).isFalse(); + assertThat(this.map.containsKey(null)).isFalse(); + } + + @Test + void shouldGetKeySet() { + this.map.put(123, "123"); + this.map.put(456, null); + this.map.put(null, "789"); + Set expected = new HashSet<>(); + expected.add(123); + expected.add(456); + expected.add(null); + assertThat(this.map.keySet()).isEqualTo(expected); + } + + @Test + void shouldGetValues() { + this.map.put(123, "123"); + this.map.put(456, null); + this.map.put(null, "789"); + List actual = new ArrayList<>(this.map.values()); + List expected = new ArrayList<>(); + expected.add("123"); + expected.add(null); + expected.add("789"); + actual.sort(NULL_SAFE_STRING_SORT); + expected.sort(NULL_SAFE_STRING_SORT); + assertThat(actual).isEqualTo(expected); + } + + @Test + void shouldGetEntrySet() { + this.map.put(123, "123"); + this.map.put(456, null); + this.map.put(null, "789"); + HashMap expected = new HashMap<>(); + expected.put(123, "123"); + expected.put(456, null); + expected.put(null, "789"); + assertThat(this.map.entrySet()).isEqualTo(expected.entrySet()); + } + + @Test + void shouldGetEntrySetFollowingNext() { + // Use loadFactor to disable resize + this.map = new TestWeakConcurrentCache<>(1, 10.0f, 1); + this.map.put(1, "1"); + this.map.put(2, "2"); + this.map.put(3, "3"); + HashMap expected = new HashMap<>(); + expected.put(1, "1"); + expected.put(2, "2"); + expected.put(3, "3"); + assertThat(this.map.entrySet()).isEqualTo(expected.entrySet()); + } + + @Test + void shouldRemoveViaEntrySet() { + this.map.put(1, "1"); + this.map.put(2, "2"); + this.map.put(3, "3"); + Iterator> iterator = this.map.entrySet().iterator(); + iterator.next(); + iterator.next(); + iterator.remove(); + iterator.next(); + assertThat(iterator.hasNext()).isFalse(); + assertThat(this.map).hasSize(2); + assertThat(this.map.containsKey(2)).isFalse(); + } + + @Test + void shouldSetViaEntrySet() { + this.map.put(1, "1"); + this.map.put(2, "2"); + this.map.put(3, "3"); + Iterator> iterator = this.map.entrySet().iterator(); + iterator.next(); + iterator.next().setValue("2b"); + iterator.next(); + assertThat(iterator.hasNext()).isFalse(); + assertThat(this.map).hasSize(3); + assertThat(this.map.get(2)).isEqualTo("2b"); + } + + @Test + @Disabled("Intended for use during development only") + void shouldBeFasterThanSynchronizedMap() throws InterruptedException { + Map> synchronizedMap = Collections.synchronizedMap(new WeakHashMap>()); + StopWatch mapTime = timeMultiThreaded("SynchronizedMap", synchronizedMap, v -> new WeakReference<>(String.valueOf(v))); + System.out.println(mapTime.prettyPrint()); + + this.map.setDisableTestHooks(true); + StopWatch cacheTime = timeMultiThreaded("WeakConcurrentCache", this.map, String::valueOf); + System.out.println(cacheTime.prettyPrint()); + + // We should be at least 4 time faster + assertThat(cacheTime.getTotalTimeSeconds()).isLessThan(mapTime.getTotalTimeSeconds() / 4.0); + } + + @Test + void shouldSupportNullReference() { + // GC could happen during restructure so we must be able to create a reference for a null entry + map.createReferenceManager().createReference(null, 1234, null); + } + + /** + * Time a multi-threaded access to a cache. + * @return the timing stopwatch + */ + private StopWatch timeMultiThreaded(String id, final Map map, + ValueFactory factory) throws InterruptedException { + + StopWatch stopWatch = new StopWatch(id); + for (int i = 0; i < 500; i++) { + map.put(i, factory.newValue(i)); + } + Thread[] threads = new Thread[30]; + stopWatch.start("Running threads"); + for (int threadIndex = 0; threadIndex < threads.length; threadIndex++) { + threads[threadIndex] = new Thread("Cache access thread " + threadIndex) { + @Override + public void run() { + for (int j = 0; j < 1000; j++) { + for (int i = 0; i < 1000; i++) { + map.get(i); + } + } + } + }; + } + for (Thread thread : threads) { + thread.start(); + } + + for (Thread thread : threads) { + if (thread.isAlive()) { + thread.join(2000); + } + } + stopWatch.stop(); + return stopWatch; + } + + + private interface ValueFactory { + + V newValue(int k); + } + + + private static class TestWeakConcurrentCache extends ConcurrentReferenceHashMap { + + private int supplementalHash; + + private final LinkedList> queue = new LinkedList<>(); + + private boolean disableTestHooks; + + public TestWeakConcurrentCache() { + super(); + } + + public void setDisableTestHooks(boolean disableTestHooks) { + this.disableTestHooks = disableTestHooks; + } + + public TestWeakConcurrentCache(int initialCapacity, float loadFactor, int concurrencyLevel) { + super(initialCapacity, loadFactor, concurrencyLevel); + } + + public TestWeakConcurrentCache(int initialCapacity, int concurrencyLevel) { + super(initialCapacity, concurrencyLevel); + } + + @Override + protected int getHash(@Nullable Object o) { + if (this.disableTestHooks) { + return super.getHash(o); + } + // For testing we want more control of the hash + this.supplementalHash = super.getHash(o); + return (o != null ? o.hashCode() : 0); + } + + public int getSupplementalHash() { + return this.supplementalHash; + } + + @Override + protected ReferenceManager createReferenceManager() { + return new ReferenceManager() { + @Override + public Reference createReference(Entry entry, int hash, @Nullable Reference next) { + if (TestWeakConcurrentCache.this.disableTestHooks) { + return super.createReference(entry, hash, next); + } + return new MockReference<>(entry, hash, next, TestWeakConcurrentCache.this.queue); + } + @Override + public Reference pollForPurge() { + if (TestWeakConcurrentCache.this.disableTestHooks) { + return super.pollForPurge(); + } + return TestWeakConcurrentCache.this.queue.isEmpty() ? null : TestWeakConcurrentCache.this.queue.removeFirst(); + } + }; + } + + public MockReference getMockReference(K key, Restructure restructure) { + return (MockReference) super.getReference(key, restructure); + } + } + + + private static class MockReference implements Reference { + + private final int hash; + + private Entry entry; + + private final Reference next; + + private final LinkedList> queue; + + public MockReference(Entry entry, int hash, Reference next, LinkedList> queue) { + this.hash = hash; + this.entry = entry; + this.next = next; + this.queue = queue; + } + + @Override + public Entry get() { + return this.entry; + } + + @Override + public int getHash() { + return this.hash; + } + + @Override + public Reference getNext() { + return this.next; + } + + @Override + public void release() { + this.queue.add(this); + this.entry = null; + } + + public void queueForPurge() { + this.queue.add(this); + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/DigestUtilsTests.java b/spring-core/src/test/java/org/springframework/util/DigestUtilsTests.java new file mode 100644 index 0000000..2ba9c67 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/DigestUtilsTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + * @author Juergen Hoeller + */ +class DigestUtilsTests { + + private byte[] bytes; + + + @BeforeEach + void createBytes() throws UnsupportedEncodingException { + bytes = "Hello World".getBytes("UTF-8"); + } + + + @Test + void md5() throws IOException { + byte[] expected = new byte[] + {-0x4f, 0xa, -0x73, -0x4f, 0x64, -0x20, 0x75, 0x41, 0x5, -0x49, -0x57, -0x65, -0x19, 0x2e, 0x3f, -0x1b}; + + byte[] result = DigestUtils.md5Digest(bytes); + assertThat(result).as("Invalid hash").isEqualTo(expected); + + result = DigestUtils.md5Digest(new ByteArrayInputStream(bytes)); + assertThat(result).as("Invalid hash").isEqualTo(expected); + } + + @Test + void md5Hex() throws IOException { + String expected = "b10a8db164e0754105b7a99be72e3fe5"; + + String hash = DigestUtils.md5DigestAsHex(bytes); + assertThat(hash).as("Invalid hash").isEqualTo(expected); + + hash = DigestUtils.md5DigestAsHex(new ByteArrayInputStream(bytes)); + assertThat(hash).as("Invalid hash").isEqualTo(expected); + } + + @Test + void md5StringBuilder() throws IOException { + String expected = "b10a8db164e0754105b7a99be72e3fe5"; + + StringBuilder builder = new StringBuilder(); + DigestUtils.appendMd5DigestAsHex(bytes, builder); + assertThat(builder.toString()).as("Invalid hash").isEqualTo(expected); + + builder = new StringBuilder(); + DigestUtils.appendMd5DigestAsHex(new ByteArrayInputStream(bytes), builder); + assertThat(builder.toString()).as("Invalid hash").isEqualTo(expected); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/ExceptionTypeFilterTests.java b/spring-core/src/test/java/org/springframework/util/ExceptionTypeFilterTests.java new file mode 100644 index 0000000..b4d4d41 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/ExceptionTypeFilterTests.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import org.junit.jupiter.api.Test; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Stephane Nicoll + */ +class ExceptionTypeFilterTests { + + @Test + void subClassMatch() { + ExceptionTypeFilter filter = new ExceptionTypeFilter(asList(RuntimeException.class), null, true); + assertThat(filter.match(RuntimeException.class)).isTrue(); + assertThat(filter.match(IllegalStateException.class)).isTrue(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/ExponentialBackOffTests.java b/spring-core/src/test/java/org/springframework/util/ExponentialBackOffTests.java new file mode 100644 index 0000000..8080f82 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/ExponentialBackOffTests.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.backoff.BackOffExecution; +import org.springframework.util.backoff.ExponentialBackOff; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Stephane Nicoll + */ +class ExponentialBackOffTests { + + @Test + void defaultInstance() { + ExponentialBackOff backOff = new ExponentialBackOff(); + BackOffExecution execution = backOff.start(); + assertThat(execution.nextBackOff()).isEqualTo(2000L); + assertThat(execution.nextBackOff()).isEqualTo(3000L); + assertThat(execution.nextBackOff()).isEqualTo(4500L); + } + + @Test + void simpleIncrease() { + ExponentialBackOff backOff = new ExponentialBackOff(100L, 2.0); + BackOffExecution execution = backOff.start(); + assertThat(execution.nextBackOff()).isEqualTo(100L); + assertThat(execution.nextBackOff()).isEqualTo(200L); + assertThat(execution.nextBackOff()).isEqualTo(400L); + assertThat(execution.nextBackOff()).isEqualTo(800L); + } + + @Test + void fixedIncrease() { + ExponentialBackOff backOff = new ExponentialBackOff(100L, 1.0); + backOff.setMaxElapsedTime(300L); + + BackOffExecution execution = backOff.start(); + assertThat(execution.nextBackOff()).isEqualTo(100L); + assertThat(execution.nextBackOff()).isEqualTo(100L); + assertThat(execution.nextBackOff()).isEqualTo(100L); + assertThat(execution.nextBackOff()).isEqualTo(BackOffExecution.STOP); + } + + @Test + void maxIntervalReached() { + ExponentialBackOff backOff = new ExponentialBackOff(2000L, 2.0); + backOff.setMaxInterval(4000L); + + BackOffExecution execution = backOff.start(); + assertThat(execution.nextBackOff()).isEqualTo(2000L); + assertThat(execution.nextBackOff()).isEqualTo(4000L); + // max reached + assertThat(execution.nextBackOff()).isEqualTo(4000L); + assertThat(execution.nextBackOff()).isEqualTo(4000L); + } + + @Test + void maxAttemptsReached() { + ExponentialBackOff backOff = new ExponentialBackOff(2000L, 2.0); + backOff.setMaxElapsedTime(4000L); + + BackOffExecution execution = backOff.start(); + assertThat(execution.nextBackOff()).isEqualTo(2000L); + assertThat(execution.nextBackOff()).isEqualTo(4000L); + // > 4 sec wait in total + assertThat(execution.nextBackOff()).isEqualTo(BackOffExecution.STOP); + } + + @Test + void startReturnDifferentInstances() { + ExponentialBackOff backOff = new ExponentialBackOff(); + backOff.setInitialInterval(2000L); + backOff.setMultiplier(2.0); + backOff.setMaxElapsedTime(4000L); + + BackOffExecution execution = backOff.start(); + BackOffExecution execution2 = backOff.start(); + + assertThat(execution.nextBackOff()).isEqualTo(2000L); + assertThat(execution2.nextBackOff()).isEqualTo(2000L); + assertThat(execution.nextBackOff()).isEqualTo(4000L); + assertThat(execution2.nextBackOff()).isEqualTo(4000L); + assertThat(execution.nextBackOff()).isEqualTo(BackOffExecution.STOP); + assertThat(execution2.nextBackOff()).isEqualTo(BackOffExecution.STOP); + } + + @Test + void invalidInterval() { + ExponentialBackOff backOff = new ExponentialBackOff(); + assertThatIllegalArgumentException().isThrownBy(() -> + backOff.setMultiplier(0.9)); + } + + @Test + void maxIntervalReachedImmediately() { + ExponentialBackOff backOff = new ExponentialBackOff(1000L, 2.0); + backOff.setMaxInterval(50L); + + BackOffExecution execution = backOff.start(); + assertThat(execution.nextBackOff()).isEqualTo(50L); + assertThat(execution.nextBackOff()).isEqualTo(50L); + } + + @Test + void toStringContent() { + ExponentialBackOff backOff = new ExponentialBackOff(2000L, 2.0); + BackOffExecution execution = backOff.start(); + assertThat(execution.toString()).isEqualTo("ExponentialBackOff{currentInterval=n/a, multiplier=2.0}"); + execution.nextBackOff(); + assertThat(execution.toString()).isEqualTo("ExponentialBackOff{currentInterval=2000ms, multiplier=2.0}"); + execution.nextBackOff(); + assertThat(execution.toString()).isEqualTo("ExponentialBackOff{currentInterval=4000ms, multiplier=2.0}"); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/FastByteArrayOutputStreamTests.java b/spring-core/src/test/java/org/springframework/util/FastByteArrayOutputStreamTests.java new file mode 100644 index 0000000..9831d5c --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/FastByteArrayOutputStreamTests.java @@ -0,0 +1,221 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIOException; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Test suite for {@link FastByteArrayOutputStream}. + * + * @author Craig Andrews + */ +class FastByteArrayOutputStreamTests { + + private static final int INITIAL_CAPACITY = 256; + + private final FastByteArrayOutputStream os = new FastByteArrayOutputStream(INITIAL_CAPACITY); + + private final byte[] helloBytes = "Hello World".getBytes(StandardCharsets.UTF_8); + + + @Test + void size() throws Exception { + this.os.write(this.helloBytes); + assertThat(this.helloBytes.length).isEqualTo(this.os.size()); + } + + @Test + void resize() throws Exception { + this.os.write(this.helloBytes); + int sizeBefore = this.os.size(); + this.os.resize(64); + assertByteArrayEqualsString(this.os); + assertThat(this.os.size()).isEqualTo(sizeBefore); + } + + @Test + void autoGrow() throws IOException { + this.os.resize(1); + for (int i = 0; i < 10; i++) { + this.os.write(1); + } + assertThat(this.os.size()).isEqualTo(10); + assertThat(new byte[] {1, 1, 1, 1, 1, 1, 1, 1, 1, 1}).isEqualTo(this.os.toByteArray()); + } + + @Test + void write() throws Exception { + this.os.write(this.helloBytes); + assertByteArrayEqualsString(this.os); + } + + @Test + void reset() throws Exception { + this.os.write(this.helloBytes); + assertByteArrayEqualsString(this.os); + this.os.reset(); + assertThat(this.os.size()).isEqualTo(0); + this.os.write(this.helloBytes); + assertByteArrayEqualsString(this.os); + } + + @Test + void close() throws Exception { + this.os.close(); + assertThatIOException().isThrownBy(() -> + this.os.write(this.helloBytes)); + } + + @Test + void toByteArrayUnsafe() throws Exception { + this.os.write(this.helloBytes); + assertByteArrayEqualsString(this.os); + assertThat(this.os.toByteArrayUnsafe()).isSameAs(this.os.toByteArrayUnsafe()); + assertThat(this.helloBytes).isEqualTo(this.os.toByteArray()); + } + + @Test + void writeTo() throws Exception { + this.os.write(this.helloBytes); + assertByteArrayEqualsString(this.os); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + this.os.writeTo(baos); + assertThat(this.helloBytes).isEqualTo(baos.toByteArray()); + } + + @Test + void failResize() throws Exception { + this.os.write(this.helloBytes); + assertThatIllegalArgumentException().isThrownBy(() -> + this.os.resize(5)); + } + + @Test + void getInputStream() throws Exception { + this.os.write(this.helloBytes); + assertThat(this.os.getInputStream()).isNotNull(); + } + + @Test + void getInputStreamAvailable() throws Exception { + this.os.write(this.helloBytes); + assertThat(this.helloBytes.length).isEqualTo(this.os.getInputStream().available()); + } + + @Test + void getInputStreamRead() throws Exception { + this.os.write(this.helloBytes); + InputStream inputStream = this.os.getInputStream(); + assertThat(this.helloBytes[0]).isEqualTo((byte) inputStream.read()); + assertThat(this.helloBytes[1]).isEqualTo((byte) inputStream.read()); + assertThat(this.helloBytes[2]).isEqualTo((byte) inputStream.read()); + assertThat(this.helloBytes[3]).isEqualTo((byte) inputStream.read()); + } + + @Test + void getInputStreamReadBytePromotion() throws Exception { + byte[] bytes = new byte[] { -1 }; + this.os.write(bytes); + InputStream inputStream = this.os.getInputStream(); + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + assertThat(inputStream.read()).isEqualTo(bais.read()); + } + + @Test + void getInputStreamReadAll() throws Exception { + this.os.write(this.helloBytes); + InputStream inputStream = this.os.getInputStream(); + byte[] actual = new byte[inputStream.available()]; + int bytesRead = inputStream.read(actual); + assertThat(bytesRead).isEqualTo(this.helloBytes.length); + assertThat(actual).isEqualTo(this.helloBytes); + assertThat(inputStream.available()).isEqualTo(0); + } + + @Test + void getInputStreamReadBeyondEndOfStream() throws Exception { + this.os.write(this.helloBytes); + InputStream inputStream = os.getInputStream(); + byte[] actual = new byte[inputStream.available() + 1]; + int bytesRead = inputStream.read(actual); + assertThat(bytesRead).isEqualTo(this.helloBytes.length); + for (int i = 0; i < bytesRead; i++) { + assertThat(actual[i]).isEqualTo(this.helloBytes[i]); + } + assertThat(actual[this.helloBytes.length]).isEqualTo((byte) 0); + assertThat(inputStream.available()).isEqualTo(0); + } + + @Test + void getInputStreamSkip() throws Exception { + this.os.write(this.helloBytes); + InputStream inputStream = this.os.getInputStream(); + assertThat(this.helloBytes[0]).isEqualTo((byte) inputStream.read()); + assertThat(inputStream.skip(1)).isEqualTo(1); + assertThat(this.helloBytes[2]).isEqualTo((byte) inputStream.read()); + assertThat(inputStream.available()).isEqualTo((this.helloBytes.length - 3)); + } + + @Test + void getInputStreamSkipAll() throws Exception { + this.os.write(this.helloBytes); + InputStream inputStream = this.os.getInputStream(); + assertThat(this.helloBytes.length).isEqualTo(inputStream.skip(1000)); + assertThat(inputStream.available()).isEqualTo(0); + } + + @Test + void updateMessageDigest() throws Exception { + StringBuilder builder = new StringBuilder("\"0"); + this.os.write(this.helloBytes); + InputStream inputStream = this.os.getInputStream(); + DigestUtils.appendMd5DigestAsHex(inputStream, builder); + builder.append("\""); + String actual = builder.toString(); + assertThat(actual).isEqualTo("\"0b10a8db164e0754105b7a99be72e3fe5\""); + } + + @Test + void updateMessageDigestManyBuffers() throws Exception { + StringBuilder builder = new StringBuilder("\"0"); + // filling at least one 256 buffer + for ( int i = 0; i < 30; i++) { + this.os.write(this.helloBytes); + } + InputStream inputStream = this.os.getInputStream(); + DigestUtils.appendMd5DigestAsHex(inputStream, builder); + builder.append("\""); + String actual = builder.toString(); + assertThat(actual).isEqualTo("\"06225ca1e4533354c516e74512065331d\""); + } + + + private void assertByteArrayEqualsString(FastByteArrayOutputStream actual) { + assertThat(actual.toByteArray()).isEqualTo(this.helloBytes); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/FileCopyUtilsTests.java b/spring-core/src/test/java/org/springframework/util/FileCopyUtilsTests.java new file mode 100644 index 0000000..e0958a6 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/FileCopyUtilsTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for the FileCopyUtils class. + * + * @author Juergen Hoeller + * @since 12.03.2005 + */ +class FileCopyUtilsTests { + + @Test + void copyFromInputStream() throws IOException { + byte[] content = "content".getBytes(); + ByteArrayInputStream in = new ByteArrayInputStream(content); + ByteArrayOutputStream out = new ByteArrayOutputStream(content.length); + int count = FileCopyUtils.copy(in, out); + assertThat(count).isEqualTo(content.length); + assertThat(Arrays.equals(content, out.toByteArray())).isTrue(); + } + + @Test + void copyFromByteArray() throws IOException { + byte[] content = "content".getBytes(); + ByteArrayOutputStream out = new ByteArrayOutputStream(content.length); + FileCopyUtils.copy(content, out); + assertThat(Arrays.equals(content, out.toByteArray())).isTrue(); + } + + @Test + void copyToByteArray() throws IOException { + byte[] content = "content".getBytes(); + ByteArrayInputStream in = new ByteArrayInputStream(content); + byte[] result = FileCopyUtils.copyToByteArray(in); + assertThat(Arrays.equals(content, result)).isTrue(); + } + + @Test + void copyFromReader() throws IOException { + String content = "content"; + StringReader in = new StringReader(content); + StringWriter out = new StringWriter(); + int count = FileCopyUtils.copy(in, out); + assertThat(count).isEqualTo(content.length()); + assertThat(out.toString()).isEqualTo(content); + } + + @Test + void copyFromString() throws IOException { + String content = "content"; + StringWriter out = new StringWriter(); + FileCopyUtils.copy(content, out); + assertThat(out.toString()).isEqualTo(content); + } + + @Test + void copyToString() throws IOException { + String content = "content"; + StringReader in = new StringReader(content); + String result = FileCopyUtils.copyToString(in); + assertThat(result).isEqualTo(content); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/FileSystemUtilsTests.java b/spring-core/src/test/java/org/springframework/util/FileSystemUtilsTests.java new file mode 100644 index 0000000..4f49f76 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/FileSystemUtilsTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.io.File; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Harrop + */ +class FileSystemUtilsTests { + + @Test + void deleteRecursively() throws Exception { + File root = new File("./tmp/root"); + File child = new File(root, "child"); + File grandchild = new File(child, "grandchild"); + + grandchild.mkdirs(); + + File bar = new File(child, "bar.txt"); + bar.createNewFile(); + + assertThat(root.exists()).isTrue(); + assertThat(child.exists()).isTrue(); + assertThat(grandchild.exists()).isTrue(); + assertThat(bar.exists()).isTrue(); + + FileSystemUtils.deleteRecursively(root); + + assertThat(root.exists()).isFalse(); + assertThat(child.exists()).isFalse(); + assertThat(grandchild.exists()).isFalse(); + assertThat(bar.exists()).isFalse(); + } + + @Test + void copyRecursively() throws Exception { + File src = new File("./tmp/src"); + File child = new File(src, "child"); + File grandchild = new File(child, "grandchild"); + + grandchild.mkdirs(); + + File bar = new File(child, "bar.txt"); + bar.createNewFile(); + + assertThat(src.exists()).isTrue(); + assertThat(child.exists()).isTrue(); + assertThat(grandchild.exists()).isTrue(); + assertThat(bar.exists()).isTrue(); + + File dest = new File("./dest"); + FileSystemUtils.copyRecursively(src, dest); + + assertThat(dest.exists()).isTrue(); + assertThat(new File(dest, child.getName()).exists()).isTrue(); + + FileSystemUtils.deleteRecursively(src); + assertThat(src.exists()).isFalse(); + } + + + @AfterEach + void tearDown() throws Exception { + File tmp = new File("./tmp"); + if (tmp.exists()) { + FileSystemUtils.deleteRecursively(tmp); + } + File dest = new File("./dest"); + if (dest.exists()) { + FileSystemUtils.deleteRecursively(dest); + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/FixedBackOffTests.java b/spring-core/src/test/java/org/springframework/util/FixedBackOffTests.java new file mode 100644 index 0000000..764bb41 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/FixedBackOffTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.backoff.BackOffExecution; +import org.springframework.util.backoff.FixedBackOff; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Stephane Nicoll + */ +class FixedBackOffTests { + + @Test + void defaultInstance() { + FixedBackOff backOff = new FixedBackOff(); + BackOffExecution execution = backOff.start(); + for (int i = 0; i < 100; i++) { + assertThat(execution.nextBackOff()).isEqualTo(FixedBackOff.DEFAULT_INTERVAL); + } + } + + @Test + void noAttemptAtAll() { + FixedBackOff backOff = new FixedBackOff(100L, 0L); + BackOffExecution execution = backOff.start(); + assertThat(execution.nextBackOff()).isEqualTo(BackOffExecution.STOP); + } + + @Test + void maxAttemptsReached() { + FixedBackOff backOff = new FixedBackOff(200L, 2); + BackOffExecution execution = backOff.start(); + assertThat(execution.nextBackOff()).isEqualTo(200L); + assertThat(execution.nextBackOff()).isEqualTo(200L); + assertThat(execution.nextBackOff()).isEqualTo(BackOffExecution.STOP); + } + + @Test + void startReturnDifferentInstances() { + FixedBackOff backOff = new FixedBackOff(100L, 1); + BackOffExecution execution = backOff.start(); + BackOffExecution execution2 = backOff.start(); + + assertThat(execution.nextBackOff()).isEqualTo(100L); + assertThat(execution2.nextBackOff()).isEqualTo(100L); + assertThat(execution.nextBackOff()).isEqualTo(BackOffExecution.STOP); + assertThat(execution2.nextBackOff()).isEqualTo(BackOffExecution.STOP); + } + + @Test + void liveUpdate() { + FixedBackOff backOff = new FixedBackOff(100L, 1); + BackOffExecution execution = backOff.start(); + assertThat(execution.nextBackOff()).isEqualTo(100L); + + backOff.setInterval(200L); + backOff.setMaxAttempts(2); + + assertThat(execution.nextBackOff()).isEqualTo(200L); + assertThat(execution.nextBackOff()).isEqualTo(BackOffExecution.STOP); + } + + @Test + void toStringContent() { + FixedBackOff backOff = new FixedBackOff(200L, 10); + BackOffExecution execution = backOff.start(); + assertThat(execution.toString()).isEqualTo("FixedBackOff{interval=200, currentAttempts=0, maxAttempts=10}"); + execution.nextBackOff(); + assertThat(execution.toString()).isEqualTo("FixedBackOff{interval=200, currentAttempts=1, maxAttempts=10}"); + execution.nextBackOff(); + assertThat(execution.toString()).isEqualTo("FixedBackOff{interval=200, currentAttempts=2, maxAttempts=10}"); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/InstanceFilterTests.java b/spring-core/src/test/java/org/springframework/util/InstanceFilterTests.java new file mode 100644 index 0000000..8e761b3 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/InstanceFilterTests.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import org.junit.jupiter.api.Test; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Stephane Nicoll + */ +class InstanceFilterTests { + + @Test + void emptyFilterApplyMatchIfEmpty() { + InstanceFilter filter = new InstanceFilter<>(null, null, true); + match(filter, "foo"); + match(filter, "bar"); + } + + @Test + void includesFilter() { + InstanceFilter filter = new InstanceFilter<>( + asList("First", "Second"), null, true); + match(filter, "Second"); + doNotMatch(filter, "foo"); + } + + @Test + void excludesFilter() { + InstanceFilter filter = new InstanceFilter<>( + null, asList("First", "Second"), true); + doNotMatch(filter, "Second"); + match(filter, "foo"); + } + + @Test + void includesAndExcludesFilters() { + InstanceFilter filter = new InstanceFilter<>( + asList("foo", "Bar"), asList("First", "Second"), true); + doNotMatch(filter, "Second"); + match(filter, "foo"); + } + + @Test + void includesAndExcludesFiltersConflict() { + InstanceFilter filter = new InstanceFilter<>( + asList("First"), asList("First"), true); + doNotMatch(filter, "First"); + } + + private void match(InstanceFilter filter, T candidate) { + assertThat(filter.match(candidate)).as("filter '" + filter + "' should match " + candidate).isTrue(); + } + + private void doNotMatch(InstanceFilter filter, T candidate) { + assertThat(filter.match(candidate)).as("filter '" + filter + "' should not match " + candidate).isFalse(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/LinkedCaseInsensitiveMapTests.java b/spring-core/src/test/java/org/springframework/util/LinkedCaseInsensitiveMapTests.java new file mode 100644 index 0000000..0a2f6df --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/LinkedCaseInsensitiveMapTests.java @@ -0,0 +1,220 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.util.Iterator; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link LinkedCaseInsensitiveMap}. + * + * @author Juergen Hoeller + * @author Phillip Webb + */ +class LinkedCaseInsensitiveMapTests { + + private final LinkedCaseInsensitiveMap map = new LinkedCaseInsensitiveMap<>(); + + + @Test + void putAndGet() { + assertThat(map.put("key", "value1")).isNull(); + assertThat(map.put("key", "value2")).isEqualTo("value1"); + assertThat(map.put("key", "value3")).isEqualTo("value2"); + assertThat(map.size()).isEqualTo(1); + assertThat(map.get("key")).isEqualTo("value3"); + assertThat(map.get("KEY")).isEqualTo("value3"); + assertThat(map.get("Key")).isEqualTo("value3"); + assertThat(map.containsKey("key")).isTrue(); + assertThat(map.containsKey("KEY")).isTrue(); + assertThat(map.containsKey("Key")).isTrue(); + assertThat(map.keySet().contains("key")).isTrue(); + assertThat(map.keySet().contains("KEY")).isTrue(); + assertThat(map.keySet().contains("Key")).isTrue(); + } + + @Test + void putWithOverlappingKeys() { + assertThat(map.put("key", "value1")).isNull(); + assertThat(map.put("KEY", "value2")).isEqualTo("value1"); + assertThat(map.put("Key", "value3")).isEqualTo("value2"); + assertThat(map.size()).isEqualTo(1); + assertThat(map.get("key")).isEqualTo("value3"); + assertThat(map.get("KEY")).isEqualTo("value3"); + assertThat(map.get("Key")).isEqualTo("value3"); + assertThat(map.containsKey("key")).isTrue(); + assertThat(map.containsKey("KEY")).isTrue(); + assertThat(map.containsKey("Key")).isTrue(); + assertThat(map.keySet().contains("key")).isTrue(); + assertThat(map.keySet().contains("KEY")).isTrue(); + assertThat(map.keySet().contains("Key")).isTrue(); + } + + @Test + void getOrDefault() { + assertThat(map.put("key", "value1")).isNull(); + assertThat(map.put("KEY", "value2")).isEqualTo("value1"); + assertThat(map.put("Key", "value3")).isEqualTo("value2"); + assertThat(map.getOrDefault("key", "N")).isEqualTo("value3"); + assertThat(map.getOrDefault("KEY", "N")).isEqualTo("value3"); + assertThat(map.getOrDefault("Key", "N")).isEqualTo("value3"); + assertThat(map.getOrDefault("keeeey", "N")).isEqualTo("N"); + assertThat(map.getOrDefault(new Object(), "N")).isEqualTo("N"); + } + + @Test + void getOrDefaultWithNullValue() { + assertThat(map.put("key", null)).isNull(); + assertThat(map.put("KEY", null)).isNull(); + assertThat(map.put("Key", null)).isNull(); + assertThat(map.getOrDefault("key", "N")).isNull(); + assertThat(map.getOrDefault("KEY", "N")).isNull(); + assertThat(map.getOrDefault("Key", "N")).isNull(); + assertThat(map.getOrDefault("keeeey", "N")).isEqualTo("N"); + assertThat(map.getOrDefault(new Object(), "N")).isEqualTo("N"); + } + + @Test + void computeIfAbsentWithExistingValue() { + assertThat(map.putIfAbsent("key", "value1")).isNull(); + assertThat(map.putIfAbsent("KEY", "value2")).isEqualTo("value1"); + assertThat(map.put("Key", "value3")).isEqualTo("value1"); + assertThat(map.computeIfAbsent("key", key2 -> "value1")).isEqualTo("value3"); + assertThat(map.computeIfAbsent("KEY", key1 -> "value2")).isEqualTo("value3"); + assertThat(map.computeIfAbsent("Key", key -> "value3")).isEqualTo("value3"); + } + + @Test + void computeIfAbsentWithComputedValue() { + assertThat(map.computeIfAbsent("key", key2 -> "value1")).isEqualTo("value1"); + assertThat(map.computeIfAbsent("KEY", key1 -> "value2")).isEqualTo("value1"); + assertThat(map.computeIfAbsent("Key", key -> "value3")).isEqualTo("value1"); + } + + @Test + void mapClone() { + assertThat(map.put("key", "value1")).isNull(); + LinkedCaseInsensitiveMap copy = map.clone(); + + assertThat(copy.getLocale()).isEqualTo(map.getLocale()); + assertThat(map.get("key")).isEqualTo("value1"); + assertThat(map.get("KEY")).isEqualTo("value1"); + assertThat(map.get("Key")).isEqualTo("value1"); + assertThat(copy.get("key")).isEqualTo("value1"); + assertThat(copy.get("KEY")).isEqualTo("value1"); + assertThat(copy.get("Key")).isEqualTo("value1"); + + copy.put("Key", "value2"); + assertThat(map.size()).isEqualTo(1); + assertThat(copy.size()).isEqualTo(1); + assertThat(map.get("key")).isEqualTo("value1"); + assertThat(map.get("KEY")).isEqualTo("value1"); + assertThat(map.get("Key")).isEqualTo("value1"); + assertThat(copy.get("key")).isEqualTo("value2"); + assertThat(copy.get("KEY")).isEqualTo("value2"); + assertThat(copy.get("Key")).isEqualTo("value2"); + } + + + @Test + void clearFromKeySet() { + map.put("key", "value"); + map.keySet().clear(); + map.computeIfAbsent("key", k -> "newvalue"); + assertThat(map.get("key")).isEqualTo("newvalue"); + } + + @Test + void removeFromKeySet() { + map.put("key", "value"); + map.keySet().remove("key"); + map.computeIfAbsent("key", k -> "newvalue"); + assertThat(map.get("key")).isEqualTo("newvalue"); + } + + @Test + void removeFromKeySetViaIterator() { + map.put("key", "value"); + nextAndRemove(map.keySet().iterator()); + assertThat(map.size()).isEqualTo(0); + map.computeIfAbsent("key", k -> "newvalue"); + assertThat(map.get("key")).isEqualTo("newvalue"); + } + + @Test + void clearFromValues() { + map.put("key", "value"); + map.values().clear(); + assertThat(map.size()).isEqualTo(0); + map.computeIfAbsent("key", k -> "newvalue"); + assertThat(map.get("key")).isEqualTo("newvalue"); + } + + @Test + void removeFromValues() { + map.put("key", "value"); + map.values().remove("value"); + assertThat(map.size()).isEqualTo(0); + map.computeIfAbsent("key", k -> "newvalue"); + assertThat(map.get("key")).isEqualTo("newvalue"); + } + + @Test + void removeFromValuesViaIterator() { + map.put("key", "value"); + nextAndRemove(map.values().iterator()); + assertThat(map.size()).isEqualTo(0); + map.computeIfAbsent("key", k -> "newvalue"); + assertThat(map.get("key")).isEqualTo("newvalue"); + } + + @Test + void clearFromEntrySet() { + map.put("key", "value"); + map.entrySet().clear(); + assertThat(map.size()).isEqualTo(0); + map.computeIfAbsent("key", k -> "newvalue"); + assertThat(map.get("key")).isEqualTo("newvalue"); + } + + @Test + void removeFromEntrySet() { + map.put("key", "value"); + map.entrySet().remove(map.entrySet().iterator().next()); + assertThat(map.size()).isEqualTo(0); + map.computeIfAbsent("key", k -> "newvalue"); + assertThat(map.get("key")).isEqualTo("newvalue"); + } + + @Test + void removeFromEntrySetViaIterator() { + map.put("key", "value"); + nextAndRemove(map.entrySet().iterator()); + assertThat(map.size()).isEqualTo(0); + map.computeIfAbsent("key", k -> "newvalue"); + assertThat(map.get("key")).isEqualTo("newvalue"); + } + + private void nextAndRemove(Iterator iterator) { + iterator.next(); + iterator.remove(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/LinkedMultiValueMapTests.java b/spring-core/src/test/java/org/springframework/util/LinkedMultiValueMapTests.java new file mode 100644 index 0000000..e8a4bba --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/LinkedMultiValueMapTests.java @@ -0,0 +1,133 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + * @author Juergen Hoeller + */ +class LinkedMultiValueMapTests { + + private final LinkedMultiValueMap map = new LinkedMultiValueMap<>(); + + + @Test + void add() { + map.add("key", "value1"); + map.add("key", "value2"); + assertThat(map).hasSize(1); + assertThat(map.get("key")).containsExactly("value1", "value2"); + } + + @Test + void addIfAbsentWhenAbsent() { + map.addIfAbsent("key", "value1"); + assertThat(map.get("key")).containsExactly("value1"); + } + + @Test + void addIfAbsentWhenPresent() { + map.add("key", "value1"); + map.addIfAbsent("key", "value2"); + assertThat(map.get("key")).containsExactly("value1"); + } + + @Test + void set() { + map.set("key", "value1"); + map.set("key", "value2"); + assertThat(map.get("key")).containsExactly("value2"); + } + + @Test + void addAll() { + map.add("key", "value1"); + map.addAll("key", Arrays.asList("value2", "value3")); + assertThat(map).hasSize(1); + assertThat(map.get("key")).containsExactly("value1","value2","value3"); + } + + @Test + void addAllWithEmptyList() { + map.addAll("key", Collections.emptyList()); + assertThat(map).hasSize(1); + assertThat(map.get("key")).isEmpty(); + assertThat(map.getFirst("key")).isNull(); + } + + @Test + void getFirst() { + List values = new ArrayList<>(2); + values.add("value1"); + values.add("value2"); + map.put("key", values); + assertThat(map.getFirst("key")).isEqualTo("value1"); + assertThat(map.getFirst("other")).isNull(); + } + + @Test + void getFirstWithEmptyList() { + map.put("key", Collections.emptyList()); + assertThat(map.getFirst("key")).isNull(); + assertThat(map.getFirst("other")).isNull(); + } + + @Test + void toSingleValueMap() { + List values = new ArrayList<>(2); + values.add("value1"); + values.add("value2"); + map.put("key", values); + Map singleValueMap = map.toSingleValueMap(); + assertThat(singleValueMap).hasSize(1); + assertThat(singleValueMap.get("key")).isEqualTo("value1"); + } + + @Test + void toSingleValueMapWithEmptyList() { + map.put("key", Collections.emptyList()); + Map singleValueMap = map.toSingleValueMap(); + assertThat(singleValueMap).isEmpty(); + assertThat(singleValueMap.get("key")).isNull(); + } + + @Test + void equals() { + map.set("key1", "value1"); + assertThat(map).isEqualTo(map); + MultiValueMap o1 = new LinkedMultiValueMap<>(); + o1.set("key1", "value1"); + assertThat(o1).isEqualTo(map); + assertThat(map).isEqualTo(o1); + Map> o2 = new HashMap<>(); + o2.put("key1", Collections.singletonList("value1")); + assertThat(o2).isEqualTo(map); + assertThat(map).isEqualTo(o2); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/MethodInvokerTests.java b/spring-core/src/test/java/org/springframework/util/MethodInvokerTests.java new file mode 100644 index 0000000..13fa345 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/MethodInvokerTests.java @@ -0,0 +1,306 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Colin Sampaleanu + * @author Juergen Hoeller + * @author Sam Brannen + * @since 21.11.2003 + */ +class MethodInvokerTests { + + @Test + void plainMethodInvoker() throws Exception { + // sanity check: singleton, non-static should work + TestClass1 tc1 = new TestClass1(); + MethodInvoker mi = new MethodInvoker(); + mi.setTargetObject(tc1); + mi.setTargetMethod("method1"); + mi.prepare(); + Integer i = (Integer) mi.invoke(); + assertThat(i.intValue()).isEqualTo(1); + + // defensive check: singleton, non-static should work with null array + tc1 = new TestClass1(); + mi = new MethodInvoker(); + mi.setTargetObject(tc1); + mi.setTargetMethod("method1"); + mi.setArguments((Object[]) null); + mi.prepare(); + i = (Integer) mi.invoke(); + assertThat(i.intValue()).isEqualTo(1); + + // sanity check: check that argument count matching works + mi = new MethodInvoker(); + mi.setTargetClass(TestClass1.class); + mi.setTargetMethod("supertypes"); + mi.setArguments(new ArrayList<>(), new ArrayList<>(), "hello"); + mi.prepare(); + assertThat(mi.invoke()).isEqualTo("hello"); + + mi = new MethodInvoker(); + mi.setTargetClass(TestClass1.class); + mi.setTargetMethod("supertypes2"); + mi.setArguments(new ArrayList<>(), new ArrayList<>(), "hello", "bogus"); + mi.prepare(); + assertThat(mi.invoke()).isEqualTo("hello"); + + // Sanity check: check that argument conversion doesn't work with plain MethodInvoker + mi = new MethodInvoker(); + mi.setTargetClass(TestClass1.class); + mi.setTargetMethod("supertypes2"); + mi.setArguments(new ArrayList<>(), new ArrayList<>(), "hello", Boolean.TRUE); + + assertThatExceptionOfType(NoSuchMethodException.class).isThrownBy( + mi::prepare); + } + + @Test + void stringWithMethodInvoker() throws Exception { + MethodInvoker methodInvoker = new MethodInvoker(); + methodInvoker.setTargetObject(new Greeter()); + methodInvoker.setTargetMethod("greet"); + methodInvoker.setArguments("no match"); + + assertThatExceptionOfType(NoSuchMethodException.class).isThrownBy( + methodInvoker::prepare); + } + + @Test + void purchaserWithMethodInvoker() throws Exception { + MethodInvoker methodInvoker = new MethodInvoker(); + methodInvoker.setTargetObject(new Greeter()); + methodInvoker.setTargetMethod("greet"); + methodInvoker.setArguments(new Purchaser()); + methodInvoker.prepare(); + String greeting = (String) methodInvoker.invoke(); + assertThat(greeting).isEqualTo("purchaser: hello"); + } + + @Test + void shopperWithMethodInvoker() throws Exception { + MethodInvoker methodInvoker = new MethodInvoker(); + methodInvoker.setTargetObject(new Greeter()); + methodInvoker.setTargetMethod("greet"); + methodInvoker.setArguments(new Shopper()); + methodInvoker.prepare(); + String greeting = (String) methodInvoker.invoke(); + assertThat(greeting).isEqualTo("purchaser: may I help you?"); + } + + @Test + void salesmanWithMethodInvoker() throws Exception { + MethodInvoker methodInvoker = new MethodInvoker(); + methodInvoker.setTargetObject(new Greeter()); + methodInvoker.setTargetMethod("greet"); + methodInvoker.setArguments(new Salesman()); + methodInvoker.prepare(); + String greeting = (String) methodInvoker.invoke(); + assertThat(greeting).isEqualTo("greetable: how are sales?"); + } + + @Test + void customerWithMethodInvoker() throws Exception { + MethodInvoker methodInvoker = new MethodInvoker(); + methodInvoker.setTargetObject(new Greeter()); + methodInvoker.setTargetMethod("greet"); + methodInvoker.setArguments(new Customer()); + methodInvoker.prepare(); + String greeting = (String) methodInvoker.invoke(); + assertThat(greeting).isEqualTo("customer: good day"); + } + + @Test + void regularWithMethodInvoker() throws Exception { + MethodInvoker methodInvoker = new MethodInvoker(); + methodInvoker.setTargetObject(new Greeter()); + methodInvoker.setTargetMethod("greet"); + methodInvoker.setArguments(new Regular("Kotter")); + methodInvoker.prepare(); + String greeting = (String) methodInvoker.invoke(); + assertThat(greeting).isEqualTo("regular: welcome back Kotter"); + } + + @Test + void vipWithMethodInvoker() throws Exception { + MethodInvoker methodInvoker = new MethodInvoker(); + methodInvoker.setTargetObject(new Greeter()); + methodInvoker.setTargetMethod("greet"); + methodInvoker.setArguments(new VIP("Fonzie")); + methodInvoker.prepare(); + String greeting = (String) methodInvoker.invoke(); + assertThat(greeting).isEqualTo("regular: whassup dude?"); + } + + + public static class TestClass1 { + + public static int _staticField1; + + public int _field1 = 0; + + public int method1() { + return ++_field1; + } + + public static int staticMethod1() { + return ++TestClass1._staticField1; + } + + public static void voidRetvalMethod() { + } + + public static void nullArgument(Object arg) { + } + + public static void intArgument(int arg) { + } + + public static void intArguments(int[] arg) { + } + + public static String supertypes(Collection c, Integer i) { + return i.toString(); + } + + public static String supertypes(Collection c, List l, String s) { + return s; + } + + public static String supertypes2(Collection c, List l, Integer i) { + return i.toString(); + } + + public static String supertypes2(Collection c, List l, String s, Integer i) { + return s; + } + + public static String supertypes2(Collection c, List l, String s, String s2) { + return s; + } + } + + + @SuppressWarnings("unused") + public static class Greeter { + + // should handle Salesman (only interface) + public String greet(Greetable greetable) { + return "greetable: " + greetable.getGreeting(); + } + + // should handle Shopper (beats Greetable since it is a class) + protected String greet(Purchaser purchaser) { + return "purchaser: " + purchaser.getGreeting(); + } + + // should handle Customer (exact match) + String greet(Customer customer) { + return "customer: " + customer.getGreeting(); + } + + // should handle Regular (exact) and VIP (closest match) + private String greet(Regular regular) { + return "regular: " + regular.getGreeting(); + } + } + + + private interface Greetable { + + String getGreeting(); + } + + + private interface Person extends Greetable { + } + + + private static class Purchaser implements Greetable { + + @Override + public String getGreeting() { + return "hello"; + } + } + + + private static class Shopper extends Purchaser implements Person { + + @Override + public String getGreeting() { + return "may I help you?"; + } + } + + + private static class Salesman implements Person { + + @Override + public String getGreeting() { + return "how are sales?"; + } + } + + + private static class Customer extends Shopper { + + @Override + public String getGreeting() { + return "good day"; + } + } + + + private static class Regular extends Customer { + + private String name; + + public Regular(String name) { + this.name = name; + } + + @Override + public String getGreeting() { + return "welcome back " + name ; + } + } + + + private static class VIP extends Regular { + + public VIP(String name) { + super(name); + } + + @Override + public String getGreeting() { + return "whassup dude?"; + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java b/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java new file mode 100644 index 0000000..59bfad0 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java @@ -0,0 +1,423 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.testfixture.io.SerializationTestUtils; + +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Unit tests for {@link MimeType}. + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @author Sam Brannen + * @author Dimitrios Liapis + */ +class MimeTypeTests { + + @Test + void slashInSubtype() { + assertThatIllegalArgumentException().isThrownBy(() -> + new MimeType("text", "/")); + } + + @Test + void valueOfNoSubtype() { + assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() -> + MimeType.valueOf("audio")); + } + + @Test + void valueOfNoSubtypeSlash() { + assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() -> + MimeType.valueOf("audio/")); + } + + @Test + void valueOfIllegalType() { + assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() -> + MimeType.valueOf("audio(/basic")); + } + + @Test + void valueOfIllegalSubtype() { + assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() -> + MimeType.valueOf("audio/basic)")); + } + + @Test + void valueOfIllegalCharset() { + assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() -> + MimeType.valueOf("text/html; charset=foo-bar")); + } + + @Test + void parseCharset() { + String s = "text/html; charset=iso-8859-1"; + MimeType mimeType = MimeType.valueOf(s); + assertThat(mimeType.getType()).as("Invalid type").isEqualTo("text"); + assertThat(mimeType.getSubtype()).as("Invalid subtype").isEqualTo("html"); + assertThat(mimeType.getCharset()).as("Invalid charset").isEqualTo(StandardCharsets.ISO_8859_1); + } + + @Test + void parseQuotedCharset() { + String s = "application/xml;charset=\"utf-8\""; + MimeType mimeType = MimeType.valueOf(s); + assertThat(mimeType.getType()).as("Invalid type").isEqualTo("application"); + assertThat(mimeType.getSubtype()).as("Invalid subtype").isEqualTo("xml"); + assertThat(mimeType.getCharset()).as("Invalid charset").isEqualTo(StandardCharsets.UTF_8); + } + + @Test + void parseQuotedSeparator() { + String s = "application/xop+xml;charset=utf-8;type=\"application/soap+xml;action=\\\"https://x.y.z\\\"\""; + MimeType mimeType = MimeType.valueOf(s); + assertThat(mimeType.getType()).as("Invalid type").isEqualTo("application"); + assertThat(mimeType.getSubtype()).as("Invalid subtype").isEqualTo("xop+xml"); + assertThat(mimeType.getCharset()).as("Invalid charset").isEqualTo(StandardCharsets.UTF_8); + assertThat(mimeType.getParameter("type")).isEqualTo("\"application/soap+xml;action=\\\"https://x.y.z\\\"\""); + } + + @Test + void withConversionService() { + ConversionService conversionService = new DefaultConversionService(); + assertThat(conversionService.canConvert(String.class, MimeType.class)).isTrue(); + MimeType mimeType = MimeType.valueOf("application/xml"); + assertThat(conversionService.convert("application/xml", MimeType.class)).isEqualTo(mimeType); + } + + @Test + void includes() { + MimeType textPlain = MimeTypeUtils.TEXT_PLAIN; + assertThat(textPlain.includes(textPlain)).as("Equal types is not inclusive").isTrue(); + MimeType allText = new MimeType("text"); + + assertThat(allText.includes(textPlain)).as("All subtypes is not inclusive").isTrue(); + assertThat(textPlain.includes(allText)).as("All subtypes is inclusive").isFalse(); + + assertThat(MimeTypeUtils.ALL.includes(textPlain)).as("All types is not inclusive").isTrue(); + assertThat(textPlain.includes(MimeTypeUtils.ALL)).as("All types is inclusive").isFalse(); + + assertThat(MimeTypeUtils.ALL.includes(textPlain)).as("All types is not inclusive").isTrue(); + assertThat(textPlain.includes(MimeTypeUtils.ALL)).as("All types is inclusive").isFalse(); + + MimeType applicationSoapXml = new MimeType("application", "soap+xml"); + MimeType applicationWildcardXml = new MimeType("application", "*+xml"); + MimeType suffixXml = new MimeType("application", "x.y+z+xml"); // SPR-15795 + + assertThat(applicationSoapXml.includes(applicationSoapXml)).isTrue(); + assertThat(applicationWildcardXml.includes(applicationWildcardXml)).isTrue(); + assertThat(applicationWildcardXml.includes(suffixXml)).isTrue(); + + assertThat(applicationWildcardXml.includes(applicationSoapXml)).isTrue(); + assertThat(applicationSoapXml.includes(applicationWildcardXml)).isFalse(); + assertThat(suffixXml.includes(applicationWildcardXml)).isFalse(); + + assertThat(applicationWildcardXml.includes(MimeTypeUtils.APPLICATION_JSON)).isFalse(); + } + + @Test + void isCompatible() { + MimeType textPlain = MimeTypeUtils.TEXT_PLAIN; + assertThat(textPlain.isCompatibleWith(textPlain)).as("Equal types is not compatible").isTrue(); + MimeType allText = new MimeType("text"); + + assertThat(allText.isCompatibleWith(textPlain)).as("All subtypes is not compatible").isTrue(); + assertThat(textPlain.isCompatibleWith(allText)).as("All subtypes is not compatible").isTrue(); + + assertThat(MimeTypeUtils.ALL.isCompatibleWith(textPlain)).as("All types is not compatible").isTrue(); + assertThat(textPlain.isCompatibleWith(MimeTypeUtils.ALL)).as("All types is not compatible").isTrue(); + + assertThat(MimeTypeUtils.ALL.isCompatibleWith(textPlain)).as("All types is not compatible").isTrue(); + assertThat(textPlain.isCompatibleWith(MimeTypeUtils.ALL)).as("All types is compatible").isTrue(); + + MimeType applicationSoapXml = new MimeType("application", "soap+xml"); + MimeType applicationWildcardXml = new MimeType("application", "*+xml"); + MimeType suffixXml = new MimeType("application", "x.y+z+xml"); // SPR-15795 + + assertThat(applicationSoapXml.isCompatibleWith(applicationSoapXml)).isTrue(); + assertThat(applicationWildcardXml.isCompatibleWith(applicationWildcardXml)).isTrue(); + assertThat(applicationWildcardXml.isCompatibleWith(suffixXml)).isTrue(); + + assertThat(applicationWildcardXml.isCompatibleWith(applicationSoapXml)).isTrue(); + assertThat(applicationSoapXml.isCompatibleWith(applicationWildcardXml)).isTrue(); + assertThat(suffixXml.isCompatibleWith(applicationWildcardXml)).isTrue(); + + assertThat(applicationWildcardXml.isCompatibleWith(MimeTypeUtils.APPLICATION_JSON)).isFalse(); + } + + @Test + void testToString() { + MimeType mimeType = new MimeType("text", "plain"); + String result = mimeType.toString(); + assertThat(result).as("Invalid toString() returned").isEqualTo("text/plain"); + } + + @Test + void parseMimeType() { + String s = "audio/*"; + MimeType mimeType = MimeTypeUtils.parseMimeType(s); + assertThat(mimeType.getType()).as("Invalid type").isEqualTo("audio"); + assertThat(mimeType.getSubtype()).as("Invalid subtype").isEqualTo("*"); + } + + @Test + void parseMimeTypeNoSubtype() { + assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() -> + MimeTypeUtils.parseMimeType("audio")); + } + + @Test + void parseMimeTypeNoSubtypeSlash() { + assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() -> + MimeTypeUtils.parseMimeType("audio/")); + } + + @Test + void parseMimeTypeTypeRange() { + assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() -> + MimeTypeUtils.parseMimeType("*/json")); + } + + @Test + void parseMimeTypeIllegalType() { + assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() -> + MimeTypeUtils.parseMimeType("audio(/basic")); + } + + @Test + void parseMimeTypeIllegalSubtype() { + assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() -> + MimeTypeUtils.parseMimeType("audio/basic)")); + } + + @Test + void parseMimeTypeMissingTypeAndSubtype() { + assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() -> + MimeTypeUtils.parseMimeType(" ;a=b")); + } + + @Test + void parseMimeTypeEmptyParameterAttribute() { + assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() -> + MimeTypeUtils.parseMimeType("audio/*;=value")); + } + + @Test + void parseMimeTypeEmptyParameterValue() { + assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() -> + MimeTypeUtils.parseMimeType("audio/*;attr=")); + } + + @Test + void parseMimeTypeIllegalParameterAttribute() { + assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() -> + MimeTypeUtils.parseMimeType("audio/*;attr<=value")); + } + + @Test + void parseMimeTypeIllegalParameterValue() { + assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() -> + MimeTypeUtils.parseMimeType("audio/*;attr=v>alue")); + } + + @Test + void parseMimeTypeIllegalCharset() { + assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() -> + MimeTypeUtils.parseMimeType("text/html; charset=foo-bar")); + } + + @Test // SPR-8917 + void parseMimeTypeQuotedParameterValue() { + MimeType mimeType = MimeTypeUtils.parseMimeType("audio/*;attr=\"v>alue\""); + assertThat(mimeType.getParameter("attr")).isEqualTo("\"v>alue\""); + } + + @Test // SPR-8917 + void parseMimeTypeSingleQuotedParameterValue() { + MimeType mimeType = MimeTypeUtils.parseMimeType("audio/*;attr='v>alue'"); + assertThat(mimeType.getParameter("attr")).isEqualTo("'v>alue'"); + } + + @Test // SPR-16630 + void parseMimeTypeWithSpacesAroundEquals() { + MimeType mimeType = MimeTypeUtils.parseMimeType("multipart/x-mixed-replace;boundary = --myboundary"); + assertThat(mimeType.getParameter("boundary")).isEqualTo("--myboundary"); + } + + @Test // SPR-16630 + void parseMimeTypeWithSpacesAroundEqualsAndQuotedValue() { + MimeType mimeType = MimeTypeUtils.parseMimeType("text/plain; foo = \" bar \" "); + assertThat(mimeType.getParameter("foo")).isEqualTo("\" bar \""); + } + + @Test + void parseMimeTypeIllegalQuotedParameterValue() { + assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() -> + MimeTypeUtils.parseMimeType("audio/*;attr=\"")); + } + + @Test + void parseMimeTypeNull() { + assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() -> + MimeTypeUtils.parseMimeType(null)); + } + + @Test + void parseMimeTypes() { + String s = "text/plain, text/html, text/x-dvi, text/x-c"; + List mimeTypes = MimeTypeUtils.parseMimeTypes(s); + assertThat(mimeTypes).as("No mime types returned").isNotNull(); + assertThat(mimeTypes.size()).as("Invalid amount of mime types").isEqualTo(4); + + mimeTypes = MimeTypeUtils.parseMimeTypes(null); + assertThat(mimeTypes).as("No mime types returned").isNotNull(); + assertThat(mimeTypes.size()).as("Invalid amount of mime types").isEqualTo(0); + } + + @Test // gh-23241 + void parseMimeTypesWithTrailingComma() { + List mimeTypes = MimeTypeUtils.parseMimeTypes("text/plain, text/html,"); + assertThat(mimeTypes).as("No mime types returned").isNotNull(); + assertThat(mimeTypes.size()).as("Incorrect number of mime types").isEqualTo(2); + } + + @Test // SPR-17459 + void parseMimeTypesWithQuotedParameters() { + testWithQuotedParameters("foo/bar;param=\",\""); + testWithQuotedParameters("foo/bar;param=\"s,a,\""); + testWithQuotedParameters("foo/bar;param=\"s,\"", "text/x-c"); + testWithQuotedParameters("foo/bar;param=\"a\\\"b,c\""); + testWithQuotedParameters("foo/bar;param=\"\\\\\""); + testWithQuotedParameters("foo/bar;param=\"\\,\\\""); + } + + @Test + void parseSubtypeSuffix() { + MimeType type = new MimeType("application", "vdn.something+json"); + assertThat(type.getSubtypeSuffix()).isEqualTo("json"); + type = new MimeType("application", "vdn.something"); + assertThat(type.getSubtypeSuffix()).isNull(); + type = new MimeType("application", "vdn.something+"); + assertThat(type.getSubtypeSuffix()).isEqualTo(""); + type = new MimeType("application", "vdn.some+thing+json"); + assertThat(type.getSubtypeSuffix()).isEqualTo("json"); + } + + @Test // gh-25350 + void wildcardSubtypeCompatibleWithSuffix() { + MimeType applicationStar = new MimeType("application", "*"); + MimeType applicationVndJson = new MimeType("application", "vnd.something+json"); + assertThat(applicationStar.isCompatibleWith(applicationVndJson)).isTrue(); + } + + private void testWithQuotedParameters(String... mimeTypes) { + String s = String.join(",", mimeTypes); + List actual = MimeTypeUtils.parseMimeTypes(s); + + assertThat(actual.size()).isEqualTo(mimeTypes.length); + for (int i = 0; i < mimeTypes.length; i++) { + assertThat(actual.get(i).toString()).isEqualTo(mimeTypes[i]); + } + } + + @Test + void compareTo() { + MimeType audioBasic = new MimeType("audio", "basic"); + MimeType audio = new MimeType("audio"); + MimeType audioWave = new MimeType("audio", "wave"); + MimeType audioBasicLevel = new MimeType("audio", "basic", singletonMap("level", "1")); + + // equal + assertThat(audioBasic.compareTo(audioBasic)).as("Invalid comparison result").isEqualTo(0); + assertThat(audio.compareTo(audio)).as("Invalid comparison result").isEqualTo(0); + assertThat(audioBasicLevel.compareTo(audioBasicLevel)).as("Invalid comparison result").isEqualTo(0); + + assertThat(audioBasicLevel.compareTo(audio) > 0).as("Invalid comparison result").isTrue(); + + List expected = new ArrayList<>(); + expected.add(audio); + expected.add(audioBasic); + expected.add(audioBasicLevel); + expected.add(audioWave); + + List result = new ArrayList<>(expected); + Random rnd = new Random(); + + // shuffle & sort 10 times + for (int i = 0; i < 10; i++) { + Collections.shuffle(result, rnd); + Collections.sort(result); + + for (int j = 0; j < result.size(); j++) { + assertThat(result.get(j)).as("Invalid media type at " + j + ", run " + i).isSameAs(expected.get(j)); + } + } + } + + @Test + void compareToCaseSensitivity() { + MimeType m1 = new MimeType("audio", "basic"); + MimeType m2 = new MimeType("Audio", "Basic"); + assertThat(m1.compareTo(m2)).as("Invalid comparison result").isEqualTo(0); + assertThat(m2.compareTo(m1)).as("Invalid comparison result").isEqualTo(0); + + m1 = new MimeType("audio", "basic", singletonMap("foo", "bar")); + m2 = new MimeType("audio", "basic", singletonMap("Foo", "bar")); + assertThat(m1.compareTo(m2)).as("Invalid comparison result").isEqualTo(0); + assertThat(m2.compareTo(m1)).as("Invalid comparison result").isEqualTo(0); + + m1 = new MimeType("audio", "basic", singletonMap("foo", "bar")); + m2 = new MimeType("audio", "basic", singletonMap("foo", "Bar")); + assertThat(m1.compareTo(m2) != 0).as("Invalid comparison result").isTrue(); + assertThat(m2.compareTo(m1) != 0).as("Invalid comparison result").isTrue(); + } + + @Test // SPR-13157 + void equalsIsCaseInsensitiveForCharsets() { + MimeType m1 = new MimeType("text", "plain", singletonMap("charset", "UTF-8")); + MimeType m2 = new MimeType("text", "plain", singletonMap("charset", "utf-8")); + assertThat(m2).isEqualTo(m1); + assertThat(m1).isEqualTo(m2); + assertThat(m1.compareTo(m2)).isEqualTo(0); + assertThat(m2.compareTo(m1)).isEqualTo(0); + } + + @Test // gh-26127 + void serialize() throws Exception { + MimeType original = new MimeType("text", "plain", StandardCharsets.UTF_8); + MimeType deserialized = SerializationTestUtils.serializeAndDeserialize(original); + assertThat(deserialized).isEqualTo(original); + assertThat(original).isEqualTo(deserialized); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/NumberUtilsTests.java b/spring-core/src/test/java/org/springframework/util/NumberUtilsTests.java new file mode 100644 index 0000000..5858111 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/NumberUtilsTests.java @@ -0,0 +1,409 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.text.NumberFormat; +import java.util.Locale; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Rob Harrop + * @author Juergen Hoeller + */ +class NumberUtilsTests { + + @Test + void parseNumber() { + String aByte = "" + Byte.MAX_VALUE; + String aShort = "" + Short.MAX_VALUE; + String anInteger = "" + Integer.MAX_VALUE; + String aLong = "" + Long.MAX_VALUE; + String aFloat = "" + Float.MAX_VALUE; + String aDouble = "" + Double.MAX_VALUE; + + assertThat(NumberUtils.parseNumber(aByte, Byte.class)).as("Byte did not parse").isEqualTo(Byte.valueOf(Byte.MAX_VALUE)); + assertThat(NumberUtils.parseNumber(aShort, Short.class)).as("Short did not parse").isEqualTo(Short.valueOf(Short.MAX_VALUE)); + assertThat(NumberUtils.parseNumber(anInteger, Integer.class)).as("Integer did not parse").isEqualTo(Integer.valueOf(Integer.MAX_VALUE)); + assertThat(NumberUtils.parseNumber(aLong, Long.class)).as("Long did not parse").isEqualTo(Long.valueOf(Long.MAX_VALUE)); + assertThat(NumberUtils.parseNumber(aFloat, Float.class)).as("Float did not parse").isEqualTo(Float.valueOf(Float.MAX_VALUE)); + assertThat(NumberUtils.parseNumber(aDouble, Double.class)).as("Double did not parse").isEqualTo(Double.valueOf(Double.MAX_VALUE)); + } + + @Test + void parseNumberUsingNumberFormat() { + NumberFormat nf = NumberFormat.getNumberInstance(Locale.US); + String aByte = "" + Byte.MAX_VALUE; + String aShort = "" + Short.MAX_VALUE; + String anInteger = "" + Integer.MAX_VALUE; + String aLong = "" + Long.MAX_VALUE; + String aFloat = "" + Float.MAX_VALUE; + String aDouble = "" + Double.MAX_VALUE; + + assertThat(NumberUtils.parseNumber(aByte, Byte.class, nf)).as("Byte did not parse").isEqualTo(Byte.valueOf(Byte.MAX_VALUE)); + assertThat(NumberUtils.parseNumber(aShort, Short.class, nf)).as("Short did not parse").isEqualTo(Short.valueOf(Short.MAX_VALUE)); + assertThat(NumberUtils.parseNumber(anInteger, Integer.class, nf)).as("Integer did not parse").isEqualTo(Integer.valueOf(Integer.MAX_VALUE)); + assertThat(NumberUtils.parseNumber(aLong, Long.class, nf)).as("Long did not parse").isEqualTo(Long.valueOf(Long.MAX_VALUE)); + assertThat(NumberUtils.parseNumber(aFloat, Float.class, nf)).as("Float did not parse").isEqualTo(Float.valueOf(Float.MAX_VALUE)); + assertThat(NumberUtils.parseNumber(aDouble, Double.class, nf)).as("Double did not parse").isEqualTo(Double.valueOf(Double.MAX_VALUE)); + } + + @Test + void parseNumberRequiringTrim() { + String aByte = " " + Byte.MAX_VALUE + " "; + String aShort = " " + Short.MAX_VALUE + " "; + String anInteger = " " + Integer.MAX_VALUE + " "; + String aLong = " " + Long.MAX_VALUE + " "; + String aFloat = " " + Float.MAX_VALUE + " "; + String aDouble = " " + Double.MAX_VALUE + " "; + + assertThat(NumberUtils.parseNumber(aByte, Byte.class)).as("Byte did not parse").isEqualTo(Byte.valueOf(Byte.MAX_VALUE)); + assertThat(NumberUtils.parseNumber(aShort, Short.class)).as("Short did not parse").isEqualTo(Short.valueOf(Short.MAX_VALUE)); + assertThat(NumberUtils.parseNumber(anInteger, Integer.class)).as("Integer did not parse").isEqualTo(Integer.valueOf(Integer.MAX_VALUE)); + assertThat(NumberUtils.parseNumber(aLong, Long.class)).as("Long did not parse").isEqualTo(Long.valueOf(Long.MAX_VALUE)); + assertThat(NumberUtils.parseNumber(aFloat, Float.class)).as("Float did not parse").isEqualTo(Float.valueOf(Float.MAX_VALUE)); + assertThat(NumberUtils.parseNumber(aDouble, Double.class)).as("Double did not parse").isEqualTo(Double.valueOf(Double.MAX_VALUE)); + } + + @Test + void parseNumberRequiringTrimUsingNumberFormat() { + NumberFormat nf = NumberFormat.getNumberInstance(Locale.US); + String aByte = " " + Byte.MAX_VALUE + " "; + String aShort = " " + Short.MAX_VALUE + " "; + String anInteger = " " + Integer.MAX_VALUE + " "; + String aLong = " " + Long.MAX_VALUE + " "; + String aFloat = " " + Float.MAX_VALUE + " "; + String aDouble = " " + Double.MAX_VALUE + " "; + + assertThat(NumberUtils.parseNumber(aByte, Byte.class, nf)).as("Byte did not parse").isEqualTo(Byte.valueOf(Byte.MAX_VALUE)); + assertThat(NumberUtils.parseNumber(aShort, Short.class, nf)).as("Short did not parse").isEqualTo(Short.valueOf(Short.MAX_VALUE)); + assertThat(NumberUtils.parseNumber(anInteger, Integer.class, nf)).as("Integer did not parse").isEqualTo(Integer.valueOf(Integer.MAX_VALUE)); + assertThat(NumberUtils.parseNumber(aLong, Long.class, nf)).as("Long did not parse").isEqualTo(Long.valueOf(Long.MAX_VALUE)); + assertThat(NumberUtils.parseNumber(aFloat, Float.class, nf)).as("Float did not parse").isEqualTo(Float.valueOf(Float.MAX_VALUE)); + assertThat(NumberUtils.parseNumber(aDouble, Double.class, nf)).as("Double did not parse").isEqualTo(Double.valueOf(Double.MAX_VALUE)); + } + + @Test + void parseNumberAsHex() { + String aByte = "0x" + Integer.toHexString(Byte.valueOf(Byte.MAX_VALUE).intValue()); + String aShort = "0x" + Integer.toHexString(Short.valueOf(Short.MAX_VALUE).intValue()); + String anInteger = "0x" + Integer.toHexString(Integer.MAX_VALUE); + String aLong = "0x" + Long.toHexString(Long.MAX_VALUE); + String aReallyBigInt = "FEBD4E677898DFEBFFEE44"; + + assertByteEquals(aByte); + assertShortEquals(aShort); + assertIntegerEquals(anInteger); + assertLongEquals(aLong); + assertThat(NumberUtils.parseNumber("0x" + aReallyBigInt, BigInteger.class)).as("BigInteger did not parse").isEqualTo(new BigInteger(aReallyBigInt, 16)); + } + + @Test + void parseNumberAsNegativeHex() { + String aByte = "-0x80"; + String aShort = "-0x8000"; + String anInteger = "-0x80000000"; + String aLong = "-0x8000000000000000"; + String aReallyBigInt = "FEBD4E677898DFEBFFEE44"; + + assertNegativeByteEquals(aByte); + assertNegativeShortEquals(aShort); + assertNegativeIntegerEquals(anInteger); + assertNegativeLongEquals(aLong); + assertThat(NumberUtils.parseNumber("-0x" + aReallyBigInt, BigInteger.class)).as("BigInteger did not parse").isEqualTo(new BigInteger(aReallyBigInt, 16).negate()); + } + + @Test + void convertDoubleToBigInteger() { + Double decimal = Double.valueOf(3.14d); + assertThat(NumberUtils.convertNumberToTargetClass(decimal, BigInteger.class)).isEqualTo(new BigInteger("3")); + } + + @Test + void convertBigDecimalToBigInteger() { + String number = "987459837583750387355346"; + BigDecimal decimal = new BigDecimal(number); + assertThat(NumberUtils.convertNumberToTargetClass(decimal, BigInteger.class)).isEqualTo(new BigInteger(number)); + } + + @Test + void convertNonExactBigDecimalToBigInteger() { + BigDecimal decimal = new BigDecimal("987459837583750387355346.14"); + assertThat(NumberUtils.convertNumberToTargetClass(decimal, BigInteger.class)).isEqualTo(new BigInteger("987459837583750387355346")); + } + + @Test + void parseBigDecimalNumber1() { + String bigDecimalAsString = "0.10"; + Number bigDecimal = NumberUtils.parseNumber(bigDecimalAsString, BigDecimal.class); + assertThat(bigDecimal).isEqualTo(new BigDecimal(bigDecimalAsString)); + } + + @Test + void parseBigDecimalNumber2() { + String bigDecimalAsString = "0.001"; + Number bigDecimal = NumberUtils.parseNumber(bigDecimalAsString, BigDecimal.class); + assertThat(bigDecimal).isEqualTo(new BigDecimal(bigDecimalAsString)); + } + + @Test + void parseBigDecimalNumber3() { + String bigDecimalAsString = "3.14159265358979323846"; + Number bigDecimal = NumberUtils.parseNumber(bigDecimalAsString, BigDecimal.class); + assertThat(bigDecimal).isEqualTo(new BigDecimal(bigDecimalAsString)); + } + + @Test + void parseLocalizedBigDecimalNumber1() { + String bigDecimalAsString = "0.10"; + NumberFormat numberFormat = NumberFormat.getInstance(Locale.ENGLISH); + Number bigDecimal = NumberUtils.parseNumber(bigDecimalAsString, BigDecimal.class, numberFormat); + assertThat(bigDecimal).isEqualTo(new BigDecimal(bigDecimalAsString)); + } + + @Test + void parseLocalizedBigDecimalNumber2() { + String bigDecimalAsString = "0.001"; + NumberFormat numberFormat = NumberFormat.getInstance(Locale.ENGLISH); + Number bigDecimal = NumberUtils.parseNumber(bigDecimalAsString, BigDecimal.class, numberFormat); + assertThat(bigDecimal).isEqualTo(new BigDecimal(bigDecimalAsString)); + } + + @Test + void parseLocalizedBigDecimalNumber3() { + String bigDecimalAsString = "3.14159265358979323846"; + NumberFormat numberFormat = NumberFormat.getInstance(Locale.ENGLISH); + Number bigDecimal = NumberUtils.parseNumber(bigDecimalAsString, BigDecimal.class, numberFormat); + assertThat(bigDecimal).isEqualTo(new BigDecimal(bigDecimalAsString)); + } + + @Test + void parseOverflow() { + String aLong = "" + Long.MAX_VALUE; + String aDouble = "" + Double.MAX_VALUE; + + assertThatIllegalArgumentException().isThrownBy(() -> + NumberUtils.parseNumber(aLong, Byte.class)); + + assertThatIllegalArgumentException().isThrownBy(() -> + NumberUtils.parseNumber(aLong, Short.class)); + + assertThatIllegalArgumentException().isThrownBy(() -> + NumberUtils.parseNumber(aLong, Integer.class)); + + assertThat(NumberUtils.parseNumber(aLong, Long.class)).isEqualTo(Long.valueOf(Long.MAX_VALUE)); + assertThat(NumberUtils.parseNumber(aDouble, Double.class)).isEqualTo(Double.valueOf(Double.MAX_VALUE)); + } + + @Test + void parseNegativeOverflow() { + String aLong = "" + Long.MIN_VALUE; + String aDouble = "" + Double.MIN_VALUE; + + assertThatIllegalArgumentException().isThrownBy(() -> + NumberUtils.parseNumber(aLong, Byte.class)); + + assertThatIllegalArgumentException().isThrownBy(() -> + NumberUtils.parseNumber(aLong, Short.class)); + + assertThatIllegalArgumentException().isThrownBy(() -> + NumberUtils.parseNumber(aLong, Integer.class)); + + assertThat(NumberUtils.parseNumber(aLong, Long.class)).isEqualTo(Long.valueOf(Long.MIN_VALUE)); + assertThat(NumberUtils.parseNumber(aDouble, Double.class)).isEqualTo(Double.valueOf(Double.MIN_VALUE)); + } + + @Test + void parseOverflowUsingNumberFormat() { + NumberFormat nf = NumberFormat.getNumberInstance(Locale.US); + String aLong = "" + Long.MAX_VALUE; + String aDouble = "" + Double.MAX_VALUE; + + assertThatIllegalArgumentException().isThrownBy(() -> + NumberUtils.parseNumber(aLong, Byte.class, nf)); + + assertThatIllegalArgumentException().isThrownBy(() -> + NumberUtils.parseNumber(aLong, Short.class, nf)); + + assertThatIllegalArgumentException().isThrownBy(() -> + NumberUtils.parseNumber(aLong, Integer.class, nf)); + + assertThat(NumberUtils.parseNumber(aLong, Long.class, nf)).isEqualTo(Long.valueOf(Long.MAX_VALUE)); + assertThat(NumberUtils.parseNumber(aDouble, Double.class, nf)).isEqualTo(Double.valueOf(Double.MAX_VALUE)); + } + + @Test + void parseNegativeOverflowUsingNumberFormat() { + NumberFormat nf = NumberFormat.getNumberInstance(Locale.US); + String aLong = "" + Long.MIN_VALUE; + String aDouble = "" + Double.MIN_VALUE; + + assertThatIllegalArgumentException().isThrownBy(() -> + NumberUtils.parseNumber(aLong, Byte.class, nf)); + + assertThatIllegalArgumentException().isThrownBy(() -> + NumberUtils.parseNumber(aLong, Short.class, nf)); + + assertThatIllegalArgumentException().isThrownBy(() -> + NumberUtils.parseNumber(aLong, Integer.class, nf)); + + assertThat(NumberUtils.parseNumber(aLong, Long.class, nf)).isEqualTo(Long.valueOf(Long.MIN_VALUE)); + assertThat(NumberUtils.parseNumber(aDouble, Double.class, nf)).isEqualTo(Double.valueOf(Double.MIN_VALUE)); + } + + @Test + void convertToInteger() { + assertThat(NumberUtils.convertNumberToTargetClass(BigInteger.valueOf(-1), Integer.class)).isEqualTo(Integer.valueOf(Integer.valueOf(-1))); + assertThat(NumberUtils.convertNumberToTargetClass(BigInteger.valueOf(0), Integer.class)).isEqualTo(Integer.valueOf(Integer.valueOf(0))); + assertThat(NumberUtils.convertNumberToTargetClass(BigInteger.valueOf(1), Integer.class)).isEqualTo(Integer.valueOf(Integer.valueOf(1))); + assertThat(NumberUtils.convertNumberToTargetClass(BigInteger.valueOf(Integer.MAX_VALUE), Integer.class)).isEqualTo(Integer.valueOf(Integer.MAX_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass(BigInteger.valueOf(Integer.MAX_VALUE + 1), Integer.class)).isEqualTo(Integer.valueOf(Integer.MIN_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass(BigInteger.valueOf(Integer.MIN_VALUE), Integer.class)).isEqualTo(Integer.valueOf(Integer.MIN_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass(BigInteger.valueOf(Integer.MIN_VALUE - 1), Integer.class)).isEqualTo(Integer.valueOf(Integer.MAX_VALUE)); + + assertThat(NumberUtils.convertNumberToTargetClass(Long.valueOf(-1), Integer.class)).isEqualTo(Integer.valueOf(Integer.valueOf(-1))); + assertThat(NumberUtils.convertNumberToTargetClass(Long.valueOf(0), Integer.class)).isEqualTo(Integer.valueOf(Integer.valueOf(0))); + assertThat(NumberUtils.convertNumberToTargetClass(Long.valueOf(1), Integer.class)).isEqualTo(Integer.valueOf(Integer.valueOf(1))); + assertThat(NumberUtils.convertNumberToTargetClass(Long.valueOf(Integer.MAX_VALUE), Integer.class)).isEqualTo(Integer.valueOf(Integer.MAX_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass(Long.valueOf(Integer.MAX_VALUE + 1), Integer.class)).isEqualTo(Integer.valueOf(Integer.MIN_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass(Long.valueOf(Integer.MIN_VALUE), Integer.class)).isEqualTo(Integer.valueOf(Integer.MIN_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass(Long.valueOf(Integer.MIN_VALUE - 1), Integer.class)).isEqualTo(Integer.valueOf(Integer.MAX_VALUE)); + + assertThat(NumberUtils.convertNumberToTargetClass(Integer.valueOf(-1), Integer.class)).isEqualTo(Integer.valueOf(Integer.valueOf(-1))); + assertThat(NumberUtils.convertNumberToTargetClass(Integer.valueOf(0), Integer.class)).isEqualTo(Integer.valueOf(Integer.valueOf(0))); + assertThat(NumberUtils.convertNumberToTargetClass(Integer.valueOf(1), Integer.class)).isEqualTo(Integer.valueOf(Integer.valueOf(1))); + assertThat(NumberUtils.convertNumberToTargetClass(Integer.valueOf(Integer.MAX_VALUE), Integer.class)).isEqualTo(Integer.valueOf(Integer.MAX_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass(Integer.valueOf(Integer.MAX_VALUE + 1), Integer.class)).isEqualTo(Integer.valueOf(Integer.MIN_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass(Integer.valueOf(Integer.MIN_VALUE), Integer.class)).isEqualTo(Integer.valueOf(Integer.MIN_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass(Integer.valueOf(Integer.MIN_VALUE - 1), Integer.class)).isEqualTo(Integer.valueOf(Integer.MAX_VALUE)); + + assertThat(NumberUtils.convertNumberToTargetClass(Short.valueOf((short) -1), Integer.class)).isEqualTo(Integer.valueOf(Integer.valueOf(-1))); + assertThat(NumberUtils.convertNumberToTargetClass(Short.valueOf((short) 0), Integer.class)).isEqualTo(Integer.valueOf(Integer.valueOf(0))); + assertThat(NumberUtils.convertNumberToTargetClass(Short.valueOf((short) 1), Integer.class)).isEqualTo(Integer.valueOf(Integer.valueOf(1))); + assertThat(NumberUtils.convertNumberToTargetClass(Short.valueOf(Short.MAX_VALUE), Integer.class)).isEqualTo(Integer.valueOf(Short.MAX_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass(Short.valueOf((short) (Short.MAX_VALUE + 1)), Integer.class)).isEqualTo(Integer.valueOf(Short.MIN_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass(Short.valueOf(Short.MIN_VALUE), Integer.class)).isEqualTo(Integer.valueOf(Short.MIN_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass(Short.valueOf((short) (Short.MIN_VALUE - 1)), Integer.class)).isEqualTo(Integer.valueOf(Short.MAX_VALUE)); + + assertThat(NumberUtils.convertNumberToTargetClass(Byte.valueOf((byte) -1), Integer.class)).isEqualTo(Integer.valueOf(Integer.valueOf(-1))); + assertThat(NumberUtils.convertNumberToTargetClass(Byte.valueOf((byte) 0), Integer.class)).isEqualTo(Integer.valueOf(Integer.valueOf(0))); + assertThat(NumberUtils.convertNumberToTargetClass(Byte.valueOf((byte) 1), Integer.class)).isEqualTo(Integer.valueOf(Integer.valueOf(1))); + assertThat(NumberUtils.convertNumberToTargetClass(Byte.valueOf(Byte.MAX_VALUE), Integer.class)).isEqualTo(Integer.valueOf(Byte.MAX_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass(Byte.valueOf((byte) (Byte.MAX_VALUE + 1)), Integer.class)).isEqualTo(Integer.valueOf(Byte.MIN_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass(Byte.valueOf(Byte.MIN_VALUE), Integer.class)).isEqualTo(Integer.valueOf(Byte.MIN_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass(Byte.valueOf((byte) (Byte.MIN_VALUE - 1)), Integer.class)).isEqualTo(Integer.valueOf(Byte.MAX_VALUE)); + + assertToNumberOverflow(Long.valueOf(Long.MAX_VALUE + 1), Integer.class); + assertToNumberOverflow(Long.valueOf(Long.MIN_VALUE - 1), Integer.class); + assertToNumberOverflow(BigInteger.valueOf(Integer.MAX_VALUE).add(BigInteger.ONE), Integer.class); + assertToNumberOverflow(BigInteger.valueOf(Integer.MIN_VALUE).subtract(BigInteger.ONE), Integer.class); + assertToNumberOverflow(new BigDecimal("18446744073709551611"), Integer.class); + } + + @Test + void convertToLong() { + assertThat(NumberUtils.convertNumberToTargetClass(BigInteger.valueOf(-1), Long.class)).isEqualTo(Long.valueOf(Long.valueOf(-1))); + assertThat(NumberUtils.convertNumberToTargetClass(BigInteger.valueOf(0), Long.class)).isEqualTo(Long.valueOf(Long.valueOf(0))); + assertThat(NumberUtils.convertNumberToTargetClass(BigInteger.valueOf(1), Long.class)).isEqualTo(Long.valueOf(Long.valueOf(1))); + assertThat(NumberUtils.convertNumberToTargetClass(BigInteger.valueOf(Long.MAX_VALUE), Long.class)).isEqualTo(Long.valueOf(Long.MAX_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass(BigInteger.valueOf(Long.MAX_VALUE + 1), Long.class)).isEqualTo(Long.valueOf(Long.MIN_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass(BigInteger.valueOf(Long.MIN_VALUE), Long.class)).isEqualTo(Long.valueOf(Long.MIN_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass(BigInteger.valueOf(Long.MIN_VALUE - 1), Long.class)).isEqualTo(Long.valueOf(Long.MAX_VALUE)); + + assertThat(NumberUtils.convertNumberToTargetClass(Long.valueOf(-1), Long.class)).isEqualTo(Long.valueOf(Long.valueOf(-1))); + assertThat(NumberUtils.convertNumberToTargetClass(Long.valueOf(0), Long.class)).isEqualTo(Long.valueOf(Long.valueOf(0))); + assertThat(NumberUtils.convertNumberToTargetClass(Long.valueOf(1), Long.class)).isEqualTo(Long.valueOf(Long.valueOf(1))); + assertThat(NumberUtils.convertNumberToTargetClass(Long.valueOf(Long.MAX_VALUE), Long.class)).isEqualTo(Long.valueOf(Long.MAX_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass(Long.valueOf(Long.MAX_VALUE + 1), Long.class)).isEqualTo(Long.valueOf(Long.MIN_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass(Long.valueOf(Long.MIN_VALUE), Long.class)).isEqualTo(Long.valueOf(Long.MIN_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass(Long.valueOf(Long.MIN_VALUE - 1), Long.class)).isEqualTo(Long.valueOf(Long.MAX_VALUE)); + + assertThat(NumberUtils.convertNumberToTargetClass(Integer.valueOf(-1), Long.class)).isEqualTo(Long.valueOf(Integer.valueOf(-1))); + assertThat(NumberUtils.convertNumberToTargetClass(Integer.valueOf(0), Long.class)).isEqualTo(Long.valueOf(Integer.valueOf(0))); + assertThat(NumberUtils.convertNumberToTargetClass(Integer.valueOf(1), Long.class)).isEqualTo(Long.valueOf(Integer.valueOf(1))); + assertThat(NumberUtils.convertNumberToTargetClass(Integer.valueOf(Integer.MAX_VALUE), Long.class)).isEqualTo(Long.valueOf(Integer.MAX_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass(Integer.valueOf(Integer.MAX_VALUE + 1), Long.class)).isEqualTo(Long.valueOf(Integer.MIN_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass(Integer.valueOf(Integer.MIN_VALUE), Long.class)).isEqualTo(Long.valueOf(Integer.MIN_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass(Integer.valueOf(Integer.MIN_VALUE - 1), Long.class)).isEqualTo(Long.valueOf(Integer.MAX_VALUE)); + + assertThat(NumberUtils.convertNumberToTargetClass(Short.valueOf((short) -1), Long.class)).isEqualTo(Long.valueOf(Integer.valueOf(-1))); + assertThat(NumberUtils.convertNumberToTargetClass(Short.valueOf((short) 0), Long.class)).isEqualTo(Long.valueOf(Integer.valueOf(0))); + assertThat(NumberUtils.convertNumberToTargetClass(Short.valueOf((short) 1), Long.class)).isEqualTo(Long.valueOf(Integer.valueOf(1))); + assertThat(NumberUtils.convertNumberToTargetClass(Short.valueOf(Short.MAX_VALUE), Long.class)).isEqualTo(Long.valueOf(Short.MAX_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass(Short.valueOf((short) (Short.MAX_VALUE + 1)), Long.class)).isEqualTo(Long.valueOf(Short.MIN_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass(Short.valueOf(Short.MIN_VALUE), Long.class)).isEqualTo(Long.valueOf(Short.MIN_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass(Short.valueOf((short) (Short.MIN_VALUE - 1)), Long.class)).isEqualTo(Long.valueOf(Short.MAX_VALUE)); + + assertThat(NumberUtils.convertNumberToTargetClass(Byte.valueOf((byte) -1), Long.class)).isEqualTo(Long.valueOf(Integer.valueOf(-1))); + assertThat(NumberUtils.convertNumberToTargetClass(Byte.valueOf((byte) 0), Long.class)).isEqualTo(Long.valueOf(Integer.valueOf(0))); + assertThat(NumberUtils.convertNumberToTargetClass(Byte.valueOf((byte) 1), Long.class)).isEqualTo(Long.valueOf(Integer.valueOf(1))); + assertThat(NumberUtils.convertNumberToTargetClass(Byte.valueOf(Byte.MAX_VALUE), Long.class)).isEqualTo(Long.valueOf(Byte.MAX_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass(Byte.valueOf((byte) (Byte.MAX_VALUE + 1)), Long.class)).isEqualTo(Long.valueOf(Byte.MIN_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass(Byte.valueOf(Byte.MIN_VALUE), Long.class)).isEqualTo(Long.valueOf(Byte.MIN_VALUE)); + assertThat(NumberUtils.convertNumberToTargetClass(Byte.valueOf((byte) (Byte.MIN_VALUE - 1)), Long.class)).isEqualTo(Long.valueOf(Byte.MAX_VALUE)); + + assertToNumberOverflow(BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE), Long.class); + assertToNumberOverflow(BigInteger.valueOf(Long.MIN_VALUE).subtract(BigInteger.ONE), Long.class); + assertToNumberOverflow(new BigDecimal("18446744073709551611"), Long.class); + } + + + private void assertLongEquals(String aLong) { + assertThat(NumberUtils.parseNumber(aLong, Long.class).longValue()).as("Long did not parse").isEqualTo(Long.MAX_VALUE); + } + + private void assertIntegerEquals(String anInteger) { + assertThat(NumberUtils.parseNumber(anInteger, Integer.class).intValue()).as("Integer did not parse").isEqualTo(Integer.MAX_VALUE); + } + + private void assertShortEquals(String aShort) { + assertThat(NumberUtils.parseNumber(aShort, Short.class).shortValue()).as("Short did not parse").isEqualTo(Short.MAX_VALUE); + } + + private void assertByteEquals(String aByte) { + assertThat(NumberUtils.parseNumber(aByte, Byte.class).byteValue()).as("Byte did not parse").isEqualTo(Byte.MAX_VALUE); + } + + private void assertNegativeLongEquals(String aLong) { + assertThat(NumberUtils.parseNumber(aLong, Long.class).longValue()).as("Long did not parse").isEqualTo(Long.MIN_VALUE); + } + + private void assertNegativeIntegerEquals(String anInteger) { + assertThat(NumberUtils.parseNumber(anInteger, Integer.class).intValue()).as("Integer did not parse").isEqualTo(Integer.MIN_VALUE); + } + + private void assertNegativeShortEquals(String aShort) { + assertThat(NumberUtils.parseNumber(aShort, Short.class).shortValue()).as("Short did not parse").isEqualTo(Short.MIN_VALUE); + } + + private void assertNegativeByteEquals(String aByte) { + assertThat(NumberUtils.parseNumber(aByte, Byte.class).byteValue()).as("Byte did not parse").isEqualTo(Byte.MIN_VALUE); + } + + private void assertToNumberOverflow(Number number, Class targetClass) { + String msg = "overflow: from=" + number + ", toClass=" + targetClass; + assertThatIllegalArgumentException().as(msg).isThrownBy(() -> + NumberUtils.convertNumberToTargetClass(number, targetClass)) + .withMessageEndingWith("overflow"); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/ObjectUtilsTests.java b/spring-core/src/test/java/org/springframework/util/ObjectUtilsTests.java new file mode 100644 index 0000000..aa6cf35 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/ObjectUtilsTests.java @@ -0,0 +1,833 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.springframework.util.ObjectUtils.isEmpty; + +/** + * Unit tests for {@link ObjectUtils}. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Rick Evans + * @author Sam Brannen + * @author Hyunjin Choi + */ +class ObjectUtilsTests { + + @Test + void isCheckedException() { + assertThat(ObjectUtils.isCheckedException(new Exception())).isTrue(); + assertThat(ObjectUtils.isCheckedException(new SQLException())).isTrue(); + + assertThat(ObjectUtils.isCheckedException(new RuntimeException())).isFalse(); + assertThat(ObjectUtils.isCheckedException(new IllegalArgumentException(""))).isFalse(); + + // Any Throwable other than RuntimeException and Error + // has to be considered checked according to the JLS. + assertThat(ObjectUtils.isCheckedException(new Throwable())).isTrue(); + } + + @Test + void isCompatibleWithThrowsClause() { + Class[] empty = new Class[0]; + Class[] exception = new Class[] {Exception.class}; + Class[] sqlAndIO = new Class[] {SQLException.class, IOException.class}; + Class[] throwable = new Class[] {Throwable.class}; + + assertThat(ObjectUtils.isCompatibleWithThrowsClause(new RuntimeException())).isTrue(); + assertThat(ObjectUtils.isCompatibleWithThrowsClause(new RuntimeException(), empty)).isTrue(); + assertThat(ObjectUtils.isCompatibleWithThrowsClause(new RuntimeException(), exception)).isTrue(); + assertThat(ObjectUtils.isCompatibleWithThrowsClause(new RuntimeException(), sqlAndIO)).isTrue(); + assertThat(ObjectUtils.isCompatibleWithThrowsClause(new RuntimeException(), throwable)).isTrue(); + + assertThat(ObjectUtils.isCompatibleWithThrowsClause(new Exception())).isFalse(); + assertThat(ObjectUtils.isCompatibleWithThrowsClause(new Exception(), empty)).isFalse(); + assertThat(ObjectUtils.isCompatibleWithThrowsClause(new Exception(), exception)).isTrue(); + assertThat(ObjectUtils.isCompatibleWithThrowsClause(new Exception(), sqlAndIO)).isFalse(); + assertThat(ObjectUtils.isCompatibleWithThrowsClause(new Exception(), throwable)).isTrue(); + + assertThat(ObjectUtils.isCompatibleWithThrowsClause(new SQLException())).isFalse(); + assertThat(ObjectUtils.isCompatibleWithThrowsClause(new SQLException(), empty)).isFalse(); + assertThat(ObjectUtils.isCompatibleWithThrowsClause(new SQLException(), exception)).isTrue(); + assertThat(ObjectUtils.isCompatibleWithThrowsClause(new SQLException(), sqlAndIO)).isTrue(); + assertThat(ObjectUtils.isCompatibleWithThrowsClause(new SQLException(), throwable)).isTrue(); + + assertThat(ObjectUtils.isCompatibleWithThrowsClause(new Throwable())).isFalse(); + assertThat(ObjectUtils.isCompatibleWithThrowsClause(new Throwable(), empty)).isFalse(); + assertThat(ObjectUtils.isCompatibleWithThrowsClause(new Throwable(), exception)).isFalse(); + assertThat(ObjectUtils.isCompatibleWithThrowsClause(new Throwable(), sqlAndIO)).isFalse(); + assertThat(ObjectUtils.isCompatibleWithThrowsClause(new Throwable(), throwable)).isTrue(); + } + + @Test + void isEmptyNull() { + assertThat(isEmpty(null)).isTrue(); + } + + @Test + void isEmptyArray() { + assertThat(isEmpty(new char[0])).isTrue(); + assertThat(isEmpty(new Object[0])).isTrue(); + assertThat(isEmpty(new Integer[0])).isTrue(); + + assertThat(isEmpty(new int[] {42})).isFalse(); + assertThat(isEmpty(new Integer[] {42})).isFalse(); + } + + @Test + void isEmptyCollection() { + assertThat(isEmpty(Collections.emptyList())).isTrue(); + assertThat(isEmpty(Collections.emptySet())).isTrue(); + + Set set = new HashSet<>(); + set.add("foo"); + assertThat(isEmpty(set)).isFalse(); + assertThat(isEmpty(Arrays.asList("foo"))).isFalse(); + } + + @Test + void isEmptyMap() { + assertThat(isEmpty(Collections.emptyMap())).isTrue(); + + HashMap map = new HashMap<>(); + map.put("foo", 42L); + assertThat(isEmpty(map)).isFalse(); + } + + @Test + void isEmptyCharSequence() { + assertThat(isEmpty(new StringBuilder())).isTrue(); + assertThat(isEmpty("")).isTrue(); + + assertThat(isEmpty(new StringBuilder("foo"))).isFalse(); + assertThat(isEmpty(" ")).isFalse(); + assertThat(isEmpty("\t")).isFalse(); + assertThat(isEmpty("foo")).isFalse(); + } + + @Test + void isEmptyUnsupportedObjectType() { + assertThat(isEmpty(42L)).isFalse(); + assertThat(isEmpty(new Object())).isFalse(); + } + + @Test + void toObjectArray() { + int[] a = new int[] {1, 2, 3, 4, 5}; + Integer[] wrapper = (Integer[]) ObjectUtils.toObjectArray(a); + assertThat(wrapper.length == 5).isTrue(); + for (int i = 0; i < wrapper.length; i++) { + assertThat(wrapper[i].intValue()).isEqualTo(a[i]); + } + } + + @Test + void toObjectArrayWithNull() { + Object[] objects = ObjectUtils.toObjectArray(null); + assertThat(objects).isNotNull(); + assertThat(objects.length).isEqualTo(0); + } + + @Test + void toObjectArrayWithEmptyPrimitiveArray() { + Object[] objects = ObjectUtils.toObjectArray(new byte[] {}); + assertThat(objects).isNotNull(); + assertThat(objects.length).isEqualTo(0); + } + + @Test + void toObjectArrayWithNonArrayType() { + assertThatIllegalArgumentException().isThrownBy(() -> + ObjectUtils.toObjectArray("Not an []")); + } + + @Test + void toObjectArrayWithNonPrimitiveArray() { + String[] source = new String[] {"Bingo"}; + assertThat(ObjectUtils.toObjectArray(source)).isEqualTo(source); + } + + @Test + void addObjectToArraySunnyDay() { + String[] array = new String[] {"foo", "bar"}; + String newElement = "baz"; + Object[] newArray = ObjectUtils.addObjectToArray(array, newElement); + assertThat(newArray.length).isEqualTo(3); + assertThat(newArray[2]).isEqualTo(newElement); + } + + @Test + void addObjectToArrayWhenEmpty() { + String[] array = new String[0]; + String newElement = "foo"; + String[] newArray = ObjectUtils.addObjectToArray(array, newElement); + assertThat(newArray.length).isEqualTo(1); + assertThat(newArray[0]).isEqualTo(newElement); + } + + @Test + void addObjectToSingleNonNullElementArray() { + String existingElement = "foo"; + String[] array = new String[] {existingElement}; + String newElement = "bar"; + String[] newArray = ObjectUtils.addObjectToArray(array, newElement); + assertThat(newArray.length).isEqualTo(2); + assertThat(newArray[0]).isEqualTo(existingElement); + assertThat(newArray[1]).isEqualTo(newElement); + } + + @Test + void addObjectToSingleNullElementArray() { + String[] array = new String[] {null}; + String newElement = "bar"; + String[] newArray = ObjectUtils.addObjectToArray(array, newElement); + assertThat(newArray.length).isEqualTo(2); + assertThat(newArray[0]).isEqualTo(null); + assertThat(newArray[1]).isEqualTo(newElement); + } + + @Test + void addObjectToNullArray() throws Exception { + String newElement = "foo"; + String[] newArray = ObjectUtils.addObjectToArray(null, newElement); + assertThat(newArray.length).isEqualTo(1); + assertThat(newArray[0]).isEqualTo(newElement); + } + + @Test + void addNullObjectToNullArray() throws Exception { + Object[] newArray = ObjectUtils.addObjectToArray(null, null); + assertThat(newArray.length).isEqualTo(1); + assertThat(newArray[0]).isEqualTo(null); + } + + @Test + void nullSafeEqualsWithArrays() throws Exception { + assertThat(ObjectUtils.nullSafeEquals(new String[] {"a", "b", "c"}, new String[] {"a", "b", "c"})).isTrue(); + assertThat(ObjectUtils.nullSafeEquals(new int[] {1, 2, 3}, new int[] {1, 2, 3})).isTrue(); + } + + @Test + @Deprecated + void hashCodeWithBooleanFalse() { + int expected = Boolean.FALSE.hashCode(); + assertThat(ObjectUtils.hashCode(false)).isEqualTo(expected); + } + + @Test + @Deprecated + void hashCodeWithBooleanTrue() { + int expected = Boolean.TRUE.hashCode(); + assertThat(ObjectUtils.hashCode(true)).isEqualTo(expected); + } + + @Test + @Deprecated + void hashCodeWithDouble() { + double dbl = 9830.43; + int expected = (new Double(dbl)).hashCode(); + assertThat(ObjectUtils.hashCode(dbl)).isEqualTo(expected); + } + + @Test + @Deprecated + void hashCodeWithFloat() { + float flt = 34.8f; + int expected = (new Float(flt)).hashCode(); + assertThat(ObjectUtils.hashCode(flt)).isEqualTo(expected); + } + + @Test + @Deprecated + void hashCodeWithLong() { + long lng = 883L; + int expected = (new Long(lng)).hashCode(); + assertThat(ObjectUtils.hashCode(lng)).isEqualTo(expected); + } + + @Test + void identityToString() { + Object obj = new Object(); + String expected = obj.getClass().getName() + "@" + ObjectUtils.getIdentityHexString(obj); + String actual = ObjectUtils.identityToString(obj); + assertThat(actual).isEqualTo(expected); + } + + @Test + void identityToStringWithNullObject() { + assertThat(ObjectUtils.identityToString(null)).isEqualTo(""); + } + + @Test + void isArrayOfPrimitivesWithBooleanArray() { + assertThat(ClassUtils.isPrimitiveArray(boolean[].class)).isTrue(); + } + + @Test + void isArrayOfPrimitivesWithObjectArray() { + assertThat(ClassUtils.isPrimitiveArray(Object[].class)).isFalse(); + } + + @Test + void isArrayOfPrimitivesWithNonArray() { + assertThat(ClassUtils.isPrimitiveArray(String.class)).isFalse(); + } + + @Test + void isPrimitiveOrWrapperWithBooleanPrimitiveClass() { + assertThat(ClassUtils.isPrimitiveOrWrapper(boolean.class)).isTrue(); + } + + @Test + void isPrimitiveOrWrapperWithBooleanWrapperClass() { + assertThat(ClassUtils.isPrimitiveOrWrapper(Boolean.class)).isTrue(); + } + + @Test + void isPrimitiveOrWrapperWithBytePrimitiveClass() { + assertThat(ClassUtils.isPrimitiveOrWrapper(byte.class)).isTrue(); + } + + @Test + void isPrimitiveOrWrapperWithByteWrapperClass() { + assertThat(ClassUtils.isPrimitiveOrWrapper(Byte.class)).isTrue(); + } + + @Test + void isPrimitiveOrWrapperWithCharacterClass() { + assertThat(ClassUtils.isPrimitiveOrWrapper(Character.class)).isTrue(); + } + + @Test + void isPrimitiveOrWrapperWithCharClass() { + assertThat(ClassUtils.isPrimitiveOrWrapper(char.class)).isTrue(); + } + + @Test + void isPrimitiveOrWrapperWithDoublePrimitiveClass() { + assertThat(ClassUtils.isPrimitiveOrWrapper(double.class)).isTrue(); + } + + @Test + void isPrimitiveOrWrapperWithDoubleWrapperClass() { + assertThat(ClassUtils.isPrimitiveOrWrapper(Double.class)).isTrue(); + } + + @Test + void isPrimitiveOrWrapperWithFloatPrimitiveClass() { + assertThat(ClassUtils.isPrimitiveOrWrapper(float.class)).isTrue(); + } + + @Test + void isPrimitiveOrWrapperWithFloatWrapperClass() { + assertThat(ClassUtils.isPrimitiveOrWrapper(Float.class)).isTrue(); + } + + @Test + void isPrimitiveOrWrapperWithIntClass() { + assertThat(ClassUtils.isPrimitiveOrWrapper(int.class)).isTrue(); + } + + @Test + void isPrimitiveOrWrapperWithIntegerClass() { + assertThat(ClassUtils.isPrimitiveOrWrapper(Integer.class)).isTrue(); + } + + @Test + void isPrimitiveOrWrapperWithLongPrimitiveClass() { + assertThat(ClassUtils.isPrimitiveOrWrapper(long.class)).isTrue(); + } + + @Test + void isPrimitiveOrWrapperWithLongWrapperClass() { + assertThat(ClassUtils.isPrimitiveOrWrapper(Long.class)).isTrue(); + } + + @Test + void isPrimitiveOrWrapperWithNonPrimitiveOrWrapperClass() { + assertThat(ClassUtils.isPrimitiveOrWrapper(Object.class)).isFalse(); + } + + @Test + void isPrimitiveOrWrapperWithShortPrimitiveClass() { + assertThat(ClassUtils.isPrimitiveOrWrapper(short.class)).isTrue(); + } + + @Test + void isPrimitiveOrWrapperWithShortWrapperClass() { + assertThat(ClassUtils.isPrimitiveOrWrapper(Short.class)).isTrue(); + } + + @Test + void nullSafeHashCodeWithBooleanArray() { + int expected = 31 * 7 + Boolean.TRUE.hashCode(); + expected = 31 * expected + Boolean.FALSE.hashCode(); + + boolean[] array = {true, false}; + int actual = ObjectUtils.nullSafeHashCode(array); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void nullSafeHashCodeWithBooleanArrayEqualToNull() { + assertThat(ObjectUtils.nullSafeHashCode((boolean[]) null)).isEqualTo(0); + } + + @Test + void nullSafeHashCodeWithByteArray() { + int expected = 31 * 7 + 8; + expected = 31 * expected + 10; + + byte[] array = {8, 10}; + int actual = ObjectUtils.nullSafeHashCode(array); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void nullSafeHashCodeWithByteArrayEqualToNull() { + assertThat(ObjectUtils.nullSafeHashCode((byte[]) null)).isEqualTo(0); + } + + @Test + void nullSafeHashCodeWithCharArray() { + int expected = 31 * 7 + 'a'; + expected = 31 * expected + 'E'; + + char[] array = {'a', 'E'}; + int actual = ObjectUtils.nullSafeHashCode(array); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void nullSafeHashCodeWithCharArrayEqualToNull() { + assertThat(ObjectUtils.nullSafeHashCode((char[]) null)).isEqualTo(0); + } + + @Test + void nullSafeHashCodeWithDoubleArray() { + long bits = Double.doubleToLongBits(8449.65); + int expected = 31 * 7 + (int) (bits ^ (bits >>> 32)); + bits = Double.doubleToLongBits(9944.923); + expected = 31 * expected + (int) (bits ^ (bits >>> 32)); + + double[] array = {8449.65, 9944.923}; + int actual = ObjectUtils.nullSafeHashCode(array); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void nullSafeHashCodeWithDoubleArrayEqualToNull() { + assertThat(ObjectUtils.nullSafeHashCode((double[]) null)).isEqualTo(0); + } + + @Test + void nullSafeHashCodeWithFloatArray() { + int expected = 31 * 7 + Float.floatToIntBits(9.6f); + expected = 31 * expected + Float.floatToIntBits(7.4f); + + float[] array = {9.6f, 7.4f}; + int actual = ObjectUtils.nullSafeHashCode(array); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void nullSafeHashCodeWithFloatArrayEqualToNull() { + assertThat(ObjectUtils.nullSafeHashCode((float[]) null)).isEqualTo(0); + } + + @Test + void nullSafeHashCodeWithIntArray() { + int expected = 31 * 7 + 884; + expected = 31 * expected + 340; + + int[] array = {884, 340}; + int actual = ObjectUtils.nullSafeHashCode(array); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void nullSafeHashCodeWithIntArrayEqualToNull() { + assertThat(ObjectUtils.nullSafeHashCode((int[]) null)).isEqualTo(0); + } + + @Test + void nullSafeHashCodeWithLongArray() { + long lng = 7993L; + int expected = 31 * 7 + (int) (lng ^ (lng >>> 32)); + lng = 84320L; + expected = 31 * expected + (int) (lng ^ (lng >>> 32)); + + long[] array = {7993L, 84320L}; + int actual = ObjectUtils.nullSafeHashCode(array); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void nullSafeHashCodeWithLongArrayEqualToNull() { + assertThat(ObjectUtils.nullSafeHashCode((long[]) null)).isEqualTo(0); + } + + @Test + void nullSafeHashCodeWithObject() { + String str = "Luke"; + assertThat(ObjectUtils.nullSafeHashCode(str)).isEqualTo(str.hashCode()); + } + + @Test + void nullSafeHashCodeWithObjectArray() { + int expected = 31 * 7 + "Leia".hashCode(); + expected = 31 * expected + "Han".hashCode(); + + Object[] array = {"Leia", "Han"}; + int actual = ObjectUtils.nullSafeHashCode(array); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void nullSafeHashCodeWithObjectArrayEqualToNull() { + assertThat(ObjectUtils.nullSafeHashCode((Object[]) null)).isEqualTo(0); + } + + @Test + void nullSafeHashCodeWithObjectBeingBooleanArray() { + Object array = new boolean[] {true, false}; + int expected = ObjectUtils.nullSafeHashCode((boolean[]) array); + assertEqualHashCodes(expected, array); + } + + @Test + void nullSafeHashCodeWithObjectBeingByteArray() { + Object array = new byte[] {6, 39}; + int expected = ObjectUtils.nullSafeHashCode((byte[]) array); + assertEqualHashCodes(expected, array); + } + + @Test + void nullSafeHashCodeWithObjectBeingCharArray() { + Object array = new char[] {'l', 'M'}; + int expected = ObjectUtils.nullSafeHashCode((char[]) array); + assertEqualHashCodes(expected, array); + } + + @Test + void nullSafeHashCodeWithObjectBeingDoubleArray() { + Object array = new double[] {68930.993, 9022.009}; + int expected = ObjectUtils.nullSafeHashCode((double[]) array); + assertEqualHashCodes(expected, array); + } + + @Test + void nullSafeHashCodeWithObjectBeingFloatArray() { + Object array = new float[] {9.9f, 9.54f}; + int expected = ObjectUtils.nullSafeHashCode((float[]) array); + assertEqualHashCodes(expected, array); + } + + @Test + void nullSafeHashCodeWithObjectBeingIntArray() { + Object array = new int[] {89, 32}; + int expected = ObjectUtils.nullSafeHashCode((int[]) array); + assertEqualHashCodes(expected, array); + } + + @Test + void nullSafeHashCodeWithObjectBeingLongArray() { + Object array = new long[] {4389, 320}; + int expected = ObjectUtils.nullSafeHashCode((long[]) array); + assertEqualHashCodes(expected, array); + } + + @Test + void nullSafeHashCodeWithObjectBeingObjectArray() { + Object array = new Object[] {"Luke", "Anakin"}; + int expected = ObjectUtils.nullSafeHashCode((Object[]) array); + assertEqualHashCodes(expected, array); + } + + @Test + void nullSafeHashCodeWithObjectBeingShortArray() { + Object array = new short[] {5, 3}; + int expected = ObjectUtils.nullSafeHashCode((short[]) array); + assertEqualHashCodes(expected, array); + } + + @Test + void nullSafeHashCodeWithObjectEqualToNull() { + assertThat(ObjectUtils.nullSafeHashCode((Object) null)).isEqualTo(0); + } + + @Test + void nullSafeHashCodeWithShortArray() { + int expected = 31 * 7 + 70; + expected = 31 * expected + 8; + + short[] array = {70, 8}; + int actual = ObjectUtils.nullSafeHashCode(array); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void nullSafeHashCodeWithShortArrayEqualToNull() { + assertThat(ObjectUtils.nullSafeHashCode((short[]) null)).isEqualTo(0); + } + + @Test + void nullSafeToStringWithBooleanArray() { + boolean[] array = {true, false}; + assertThat(ObjectUtils.nullSafeToString(array)).isEqualTo("{true, false}"); + } + + @Test + void nullSafeToStringWithBooleanArrayBeingEmpty() { + boolean[] array = {}; + assertThat(ObjectUtils.nullSafeToString(array)).isEqualTo("{}"); + } + + @Test + void nullSafeToStringWithBooleanArrayEqualToNull() { + assertThat(ObjectUtils.nullSafeToString((boolean[]) null)).isEqualTo("null"); + } + + @Test + void nullSafeToStringWithByteArray() { + byte[] array = {5, 8}; + assertThat(ObjectUtils.nullSafeToString(array)).isEqualTo("{5, 8}"); + } + + @Test + void nullSafeToStringWithByteArrayBeingEmpty() { + byte[] array = {}; + assertThat(ObjectUtils.nullSafeToString(array)).isEqualTo("{}"); + } + + @Test + void nullSafeToStringWithByteArrayEqualToNull() { + assertThat(ObjectUtils.nullSafeToString((byte[]) null)).isEqualTo("null"); + } + + @Test + void nullSafeToStringWithCharArray() { + char[] array = {'A', 'B'}; + assertThat(ObjectUtils.nullSafeToString(array)).isEqualTo("{'A', 'B'}"); + } + + @Test + void nullSafeToStringWithCharArrayBeingEmpty() { + char[] array = {}; + assertThat(ObjectUtils.nullSafeToString(array)).isEqualTo("{}"); + } + + @Test + void nullSafeToStringWithCharArrayEqualToNull() { + assertThat(ObjectUtils.nullSafeToString((char[]) null)).isEqualTo("null"); + } + + @Test + void nullSafeToStringWithDoubleArray() { + double[] array = {8594.93, 8594023.95}; + assertThat(ObjectUtils.nullSafeToString(array)).isEqualTo("{8594.93, 8594023.95}"); + } + + @Test + void nullSafeToStringWithDoubleArrayBeingEmpty() { + double[] array = {}; + assertThat(ObjectUtils.nullSafeToString(array)).isEqualTo("{}"); + } + + @Test + void nullSafeToStringWithDoubleArrayEqualToNull() { + assertThat(ObjectUtils.nullSafeToString((double[]) null)).isEqualTo("null"); + } + + @Test + void nullSafeToStringWithFloatArray() { + float[] array = {8.6f, 43.8f}; + assertThat(ObjectUtils.nullSafeToString(array)).isEqualTo("{8.6, 43.8}"); + } + + @Test + void nullSafeToStringWithFloatArrayBeingEmpty() { + float[] array = {}; + assertThat(ObjectUtils.nullSafeToString(array)).isEqualTo("{}"); + } + + @Test + void nullSafeToStringWithFloatArrayEqualToNull() { + assertThat(ObjectUtils.nullSafeToString((float[]) null)).isEqualTo("null"); + } + + @Test + void nullSafeToStringWithIntArray() { + int[] array = {9, 64}; + assertThat(ObjectUtils.nullSafeToString(array)).isEqualTo("{9, 64}"); + } + + @Test + void nullSafeToStringWithIntArrayBeingEmpty() { + int[] array = {}; + assertThat(ObjectUtils.nullSafeToString(array)).isEqualTo("{}"); + } + + @Test + void nullSafeToStringWithIntArrayEqualToNull() { + assertThat(ObjectUtils.nullSafeToString((int[]) null)).isEqualTo("null"); + } + + @Test + void nullSafeToStringWithLongArray() { + long[] array = {434L, 23423L}; + assertThat(ObjectUtils.nullSafeToString(array)).isEqualTo("{434, 23423}"); + } + + @Test + void nullSafeToStringWithLongArrayBeingEmpty() { + long[] array = {}; + assertThat(ObjectUtils.nullSafeToString(array)).isEqualTo("{}"); + } + + @Test + void nullSafeToStringWithLongArrayEqualToNull() { + assertThat(ObjectUtils.nullSafeToString((long[]) null)).isEqualTo("null"); + } + + @Test + void nullSafeToStringWithPlainOldString() { + assertThat(ObjectUtils.nullSafeToString("I shoh love tha taste of mangoes")).isEqualTo("I shoh love tha taste of mangoes"); + } + + @Test + void nullSafeToStringWithObjectArray() { + Object[] array = {"Han", Long.valueOf(43)}; + assertThat(ObjectUtils.nullSafeToString(array)).isEqualTo("{Han, 43}"); + } + + @Test + void nullSafeToStringWithObjectArrayBeingEmpty() { + Object[] array = {}; + assertThat(ObjectUtils.nullSafeToString(array)).isEqualTo("{}"); + } + + @Test + void nullSafeToStringWithObjectArrayEqualToNull() { + assertThat(ObjectUtils.nullSafeToString((Object[]) null)).isEqualTo("null"); + } + + @Test + void nullSafeToStringWithShortArray() { + short[] array = {7, 9}; + assertThat(ObjectUtils.nullSafeToString(array)).isEqualTo("{7, 9}"); + } + + @Test + void nullSafeToStringWithShortArrayBeingEmpty() { + short[] array = {}; + assertThat(ObjectUtils.nullSafeToString(array)).isEqualTo("{}"); + } + + @Test + void nullSafeToStringWithShortArrayEqualToNull() { + assertThat(ObjectUtils.nullSafeToString((short[]) null)).isEqualTo("null"); + } + + @Test + void nullSafeToStringWithStringArray() { + String[] array = {"Luke", "Anakin"}; + assertThat(ObjectUtils.nullSafeToString(array)).isEqualTo("{Luke, Anakin}"); + } + + @Test + void nullSafeToStringWithStringArrayBeingEmpty() { + String[] array = {}; + assertThat(ObjectUtils.nullSafeToString(array)).isEqualTo("{}"); + } + + @Test + void nullSafeToStringWithStringArrayEqualToNull() { + assertThat(ObjectUtils.nullSafeToString((String[]) null)).isEqualTo("null"); + } + + @Test + void containsConstant() { + assertThat(ObjectUtils.containsConstant(Tropes.values(), "FOO")).isTrue(); + assertThat(ObjectUtils.containsConstant(Tropes.values(), "foo")).isTrue(); + assertThat(ObjectUtils.containsConstant(Tropes.values(), "BaR")).isTrue(); + assertThat(ObjectUtils.containsConstant(Tropes.values(), "bar")).isTrue(); + assertThat(ObjectUtils.containsConstant(Tropes.values(), "BAZ")).isTrue(); + assertThat(ObjectUtils.containsConstant(Tropes.values(), "baz")).isTrue(); + + assertThat(ObjectUtils.containsConstant(Tropes.values(), "BOGUS")).isFalse(); + + assertThat(ObjectUtils.containsConstant(Tropes.values(), "FOO", true)).isTrue(); + assertThat(ObjectUtils.containsConstant(Tropes.values(), "foo", true)).isFalse(); + } + + @Test + void containsElement() { + Object[] array = {"foo", "bar", 42, new String[] {"baz", "quux"}}; + + assertThat(ObjectUtils.containsElement(null, "foo")).isFalse(); + assertThat(ObjectUtils.containsElement(array, null)).isFalse(); + assertThat(ObjectUtils.containsElement(array, "bogus")).isFalse(); + + assertThat(ObjectUtils.containsElement(array, "foo")).isTrue(); + assertThat(ObjectUtils.containsElement(array, "bar")).isTrue(); + assertThat(ObjectUtils.containsElement(array, 42)).isTrue(); + assertThat(ObjectUtils.containsElement(array, new String[] {"baz", "quux"})).isTrue(); + } + + @Test + void caseInsensitiveValueOf() { + assertThat(ObjectUtils.caseInsensitiveValueOf(Tropes.values(), "foo")).isEqualTo(Tropes.FOO); + assertThat(ObjectUtils.caseInsensitiveValueOf(Tropes.values(), "BAR")).isEqualTo(Tropes.BAR); + + assertThatIllegalArgumentException().isThrownBy(() -> + ObjectUtils.caseInsensitiveValueOf(Tropes.values(), "bogus")) + .withMessage("Constant [bogus] does not exist in enum type org.springframework.util.ObjectUtilsTests$Tropes"); + } + + private void assertEqualHashCodes(int expected, Object array) { + int actual = ObjectUtils.nullSafeHashCode(array); + assertThat(actual).isEqualTo(expected); + assertThat(array.hashCode() != actual).isTrue(); + } + + + enum Tropes {FOO, BAR, baz} + +} diff --git a/spring-core/src/test/java/org/springframework/util/PatternMatchUtilsTests.java b/spring-core/src/test/java/org/springframework/util/PatternMatchUtilsTests.java new file mode 100644 index 0000000..7a42db5 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/PatternMatchUtilsTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @author Johan Gorter + */ +class PatternMatchUtilsTests { + + @Test + void trivial() { + assertThat(PatternMatchUtils.simpleMatch((String) null, "")).isEqualTo(false); + assertThat(PatternMatchUtils.simpleMatch("1", null)).isEqualTo(false); + doTest("*", "123", true); + doTest("123", "123", true); + } + + @Test + void startsWith() { + doTest("get*", "getMe", true); + doTest("get*", "setMe", false); + } + + @Test + void endsWith() { + doTest("*Test", "getMeTest", true); + doTest("*Test", "setMe", false); + } + + @Test + void between() { + doTest("*stuff*", "getMeTest", false); + doTest("*stuff*", "getstuffTest", true); + doTest("*stuff*", "stuffTest", true); + doTest("*stuff*", "getstuff", true); + doTest("*stuff*", "stuff", true); + } + + @Test + void startsEnds() { + doTest("on*Event", "onMyEvent", true); + doTest("on*Event", "onEvent", true); + doTest("3*3", "3", false); + doTest("3*3", "33", true); + } + + @Test + void startsEndsBetween() { + doTest("12*45*78", "12345678", true); + doTest("12*45*78", "123456789", false); + doTest("12*45*78", "012345678", false); + doTest("12*45*78", "124578", true); + doTest("12*45*78", "1245457878", true); + doTest("3*3*3", "33", false); + doTest("3*3*3", "333", true); + } + + @Test + void ridiculous() { + doTest("*1*2*3*", "0011002001010030020201030", true); + doTest("1*2*3*4", "10300204", false); + doTest("1*2*3*3", "10300203", false); + doTest("*1*2*3*", "123", true); + doTest("*1*2*3*", "132", false); + } + + @Test + void patternVariants() { + doTest("*a", "*", false); + doTest("*a", "a", true); + doTest("*a", "b", false); + doTest("*a", "aa", true); + doTest("*a", "ba", true); + doTest("*a", "ab", false); + doTest("**a", "*", false); + doTest("**a", "a", true); + doTest("**a", "b", false); + doTest("**a", "aa", true); + doTest("**a", "ba", true); + doTest("**a", "ab", false); + } + + private void doTest(String pattern, String str, boolean shouldMatch) { + assertThat(PatternMatchUtils.simpleMatch(pattern, str)).isEqualTo(shouldMatch); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/PropertiesPersisterTests.java b/spring-core/src/test/java/org/springframework/util/PropertiesPersisterTests.java new file mode 100644 index 0000000..f24acf3 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/PropertiesPersisterTests.java @@ -0,0 +1,135 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.Properties; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @since 11.01.2005 + */ +class PropertiesPersisterTests { + + @Test + void propertiesPersister() throws IOException { + String propString = "code1=message1\ncode2:message2"; + Properties props = loadProperties(propString, false); + String propCopy = storeProperties(props, null, false); + loadProperties(propCopy, false); + } + + @Test + void propertiesPersisterWithWhitespace() throws IOException { + String propString = " code1\t= \tmessage1\n code2 \t :\t mess\\\n \t age2"; + Properties props = loadProperties(propString, false); + String propCopy = storeProperties(props, null, false); + loadProperties(propCopy, false); + } + + @Test + void propertiesPersisterWithHeader() throws IOException { + String propString = "code1=message1\ncode2:message2"; + Properties props = loadProperties(propString, false); + String propCopy = storeProperties(props, "myHeader", false); + loadProperties(propCopy, false); + } + + @Test + void propertiesPersisterWithEmptyValue() throws IOException { + String propString = "code1=message1\ncode2:message2\ncode3="; + Properties props = loadProperties(propString, false); + String propCopy = storeProperties(props, null, false); + loadProperties(propCopy, false); + } + + @Test + void propertiesPersisterWithReader() throws IOException { + String propString = "code1=message1\ncode2:message2"; + Properties props = loadProperties(propString, true); + String propCopy = storeProperties(props, null, true); + loadProperties(propCopy, false); + } + + @Test + void propertiesPersisterWithReaderAndWhitespace() throws IOException { + String propString = " code1\t= \tmessage1\n code2 \t :\t mess\\\n \t age2"; + Properties props = loadProperties(propString, true); + String propCopy = storeProperties(props, null, true); + loadProperties(propCopy, false); + } + + @Test + void propertiesPersisterWithReaderAndHeader() throws IOException { + String propString = "code1\t=\tmessage1\n code2 \t : \t message2"; + Properties props = loadProperties(propString, true); + String propCopy = storeProperties(props, "myHeader", true); + loadProperties(propCopy, false); + } + + @Test + void propertiesPersisterWithReaderAndEmptyValue() throws IOException { + String propString = "code1=message1\ncode2:message2\ncode3="; + Properties props = loadProperties(propString, true); + String propCopy = storeProperties(props, null, true); + loadProperties(propCopy, false); + } + + private Properties loadProperties(String propString, boolean useReader) throws IOException { + DefaultPropertiesPersister persister = new DefaultPropertiesPersister(); + Properties props = new Properties(); + if (useReader) { + persister.load(props, new StringReader(propString)); + } + else { + persister.load(props, new ByteArrayInputStream(propString.getBytes())); + } + assertThat(props.getProperty("code1")).isEqualTo("message1"); + assertThat(props.getProperty("code2")).isEqualTo("message2"); + return props; + } + + private String storeProperties(Properties props, String header, boolean useWriter) throws IOException { + DefaultPropertiesPersister persister = new DefaultPropertiesPersister(); + String propCopy = null; + if (useWriter) { + StringWriter propWriter = new StringWriter(); + persister.store(props, propWriter, header); + propCopy = propWriter.toString(); + } + else { + ByteArrayOutputStream propOut = new ByteArrayOutputStream(); + persister.store(props, propOut, header); + propCopy = new String(propOut.toByteArray()); + } + if (header != null) { + assertThat(propCopy.contains(header)).isTrue(); + } + assertThat(propCopy.contains("\ncode1=message1")).isTrue(); + assertThat(propCopy.contains("\ncode2=message2")).isTrue(); + return propCopy; + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/PropertyPlaceholderHelperTests.java b/spring-core/src/test/java/org/springframework/util/PropertyPlaceholderHelperTests.java new file mode 100644 index 0000000..92c1595 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/PropertyPlaceholderHelperTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.util.Properties; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Rob Harrop + */ +class PropertyPlaceholderHelperTests { + + private final PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("${", "}"); + + @Test + void withProperties() { + String text = "foo=${foo}"; + Properties props = new Properties(); + props.setProperty("foo", "bar"); + + assertThat(this.helper.replacePlaceholders(text, props)).isEqualTo("foo=bar"); + } + + @Test + void withMultipleProperties() { + String text = "foo=${foo},bar=${bar}"; + Properties props = new Properties(); + props.setProperty("foo", "bar"); + props.setProperty("bar", "baz"); + + assertThat(this.helper.replacePlaceholders(text, props)).isEqualTo("foo=bar,bar=baz"); + } + + @Test + void recurseInProperty() { + String text = "foo=${bar}"; + Properties props = new Properties(); + props.setProperty("bar", "${baz}"); + props.setProperty("baz", "bar"); + + assertThat(this.helper.replacePlaceholders(text, props)).isEqualTo("foo=bar"); + } + + @Test + void recurseInPlaceholder() { + String text = "foo=${b${inner}}"; + Properties props = new Properties(); + props.setProperty("bar", "bar"); + props.setProperty("inner", "ar"); + + assertThat(this.helper.replacePlaceholders(text, props)).isEqualTo("foo=bar"); + + text = "${top}"; + props = new Properties(); + props.setProperty("top", "${child}+${child}"); + props.setProperty("child", "${${differentiator}.grandchild}"); + props.setProperty("differentiator", "first"); + props.setProperty("first.grandchild", "actualValue"); + + assertThat(this.helper.replacePlaceholders(text, props)).isEqualTo("actualValue+actualValue"); + } + + @Test + void withResolver() { + String text = "foo=${foo}"; + PlaceholderResolver resolver = placeholderName -> "foo".equals(placeholderName) ? "bar" : null; + + assertThat(this.helper.replacePlaceholders(text, resolver)).isEqualTo("foo=bar"); + } + + @Test + void unresolvedPlaceholderIsIgnored() { + String text = "foo=${foo},bar=${bar}"; + Properties props = new Properties(); + props.setProperty("foo", "bar"); + + assertThat(this.helper.replacePlaceholders(text, props)).isEqualTo("foo=bar,bar=${bar}"); + } + + @Test + void unresolvedPlaceholderAsError() { + String text = "foo=${foo},bar=${bar}"; + Properties props = new Properties(); + props.setProperty("foo", "bar"); + + PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("${", "}", null, false); + assertThatIllegalArgumentException().isThrownBy(() -> + helper.replacePlaceholders(text, props)); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/ReflectionUtilsTests.java b/spring-core/src/test/java/org/springframework/util/ReflectionUtilsTests.java new file mode 100644 index 0000000..8ca69dd --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/ReflectionUtilsTests.java @@ -0,0 +1,399 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.rmi.ConnectException; +import java.rmi.RemoteException; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.tests.sample.objects.TestObject; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Rob Harrop + * @author Juergen Hoeller + * @author Sam Brannen + * @author Arjen Poutsma + */ +class ReflectionUtilsTests { + + @Test + void findField() { + Field field = ReflectionUtils.findField(TestObjectSubclassWithPublicField.class, "publicField", String.class); + assertThat(field).isNotNull(); + assertThat(field.getName()).isEqualTo("publicField"); + assertThat(field.getType()).isEqualTo(String.class); + assertThat(Modifier.isPublic(field.getModifiers())).as("Field should be public.").isTrue(); + + field = ReflectionUtils.findField(TestObjectSubclassWithNewField.class, "prot", String.class); + assertThat(field).isNotNull(); + assertThat(field.getName()).isEqualTo("prot"); + assertThat(field.getType()).isEqualTo(String.class); + assertThat(Modifier.isProtected(field.getModifiers())).as("Field should be protected.").isTrue(); + + field = ReflectionUtils.findField(TestObjectSubclassWithNewField.class, "name", String.class); + assertThat(field).isNotNull(); + assertThat(field.getName()).isEqualTo("name"); + assertThat(field.getType()).isEqualTo(String.class); + assertThat(Modifier.isPrivate(field.getModifiers())).as("Field should be private.").isTrue(); + } + + @Test + void setField() { + TestObjectSubclassWithNewField testBean = new TestObjectSubclassWithNewField(); + Field field = ReflectionUtils.findField(TestObjectSubclassWithNewField.class, "name", String.class); + + ReflectionUtils.makeAccessible(field); + + ReflectionUtils.setField(field, testBean, "FooBar"); + assertThat(testBean.getName()).isNotNull(); + assertThat(testBean.getName()).isEqualTo("FooBar"); + + ReflectionUtils.setField(field, testBean, null); + assertThat((Object) testBean.getName()).isNull(); + } + + @Test + void invokeMethod() throws Exception { + String rob = "Rob Harrop"; + + TestObject bean = new TestObject(); + bean.setName(rob); + + Method getName = TestObject.class.getMethod("getName"); + Method setName = TestObject.class.getMethod("setName", String.class); + + Object name = ReflectionUtils.invokeMethod(getName, bean); + assertThat(name).as("Incorrect name returned").isEqualTo(rob); + + String juergen = "Juergen Hoeller"; + ReflectionUtils.invokeMethod(setName, bean, juergen); + assertThat(bean.getName()).as("Incorrect name set").isEqualTo(juergen); + } + + @Test + void declaresException() throws Exception { + Method remoteExMethod = A.class.getDeclaredMethod("foo", Integer.class); + assertThat(ReflectionUtils.declaresException(remoteExMethod, RemoteException.class)).isTrue(); + assertThat(ReflectionUtils.declaresException(remoteExMethod, ConnectException.class)).isTrue(); + assertThat(ReflectionUtils.declaresException(remoteExMethod, NoSuchMethodException.class)).isFalse(); + assertThat(ReflectionUtils.declaresException(remoteExMethod, Exception.class)).isFalse(); + + Method illegalExMethod = B.class.getDeclaredMethod("bar", String.class); + assertThat(ReflectionUtils.declaresException(illegalExMethod, IllegalArgumentException.class)).isTrue(); + assertThat(ReflectionUtils.declaresException(illegalExMethod, NumberFormatException.class)).isTrue(); + assertThat(ReflectionUtils.declaresException(illegalExMethod, IllegalStateException.class)).isFalse(); + assertThat(ReflectionUtils.declaresException(illegalExMethod, Exception.class)).isFalse(); + } + + @Test + void copySrcToDestinationOfIncorrectClass() { + TestObject src = new TestObject(); + String dest = new String(); + assertThatIllegalArgumentException().isThrownBy(() -> + ReflectionUtils.shallowCopyFieldState(src, dest)); + } + + @Test + void rejectsNullSrc() { + TestObject src = null; + String dest = new String(); + assertThatIllegalArgumentException().isThrownBy(() -> + ReflectionUtils.shallowCopyFieldState(src, dest)); + } + + @Test + void rejectsNullDest() { + TestObject src = new TestObject(); + String dest = null; + assertThatIllegalArgumentException().isThrownBy(() -> + ReflectionUtils.shallowCopyFieldState(src, dest)); + } + + @Test + void validCopy() { + TestObject src = new TestObject(); + TestObject dest = new TestObject(); + testValidCopy(src, dest); + } + + @Test + void validCopyOnSubTypeWithNewField() { + TestObjectSubclassWithNewField src = new TestObjectSubclassWithNewField(); + TestObjectSubclassWithNewField dest = new TestObjectSubclassWithNewField(); + src.magic = 11; + + // Will check inherited fields are copied + testValidCopy(src, dest); + + // Check subclass fields were copied + assertThat(dest.magic).isEqualTo(src.magic); + assertThat(dest.prot).isEqualTo(src.prot); + } + + @Test + void validCopyToSubType() { + TestObject src = new TestObject(); + TestObjectSubclassWithNewField dest = new TestObjectSubclassWithNewField(); + dest.magic = 11; + testValidCopy(src, dest); + // Should have left this one alone + assertThat(dest.magic).isEqualTo(11); + } + + @Test + void validCopyToSubTypeWithFinalField() { + TestObjectSubclassWithFinalField src = new TestObjectSubclassWithFinalField(); + TestObjectSubclassWithFinalField dest = new TestObjectSubclassWithFinalField(); + // Check that this doesn't fail due to attempt to assign final + testValidCopy(src, dest); + } + + private void testValidCopy(TestObject src, TestObject dest) { + src.setName("freddie"); + src.setAge(15); + src.setSpouse(new TestObject()); + assertThat(src.getAge() == dest.getAge()).isFalse(); + + ReflectionUtils.shallowCopyFieldState(src, dest); + assertThat(dest.getAge()).isEqualTo(src.getAge()); + assertThat(dest.getSpouse()).isEqualTo(src.getSpouse()); + } + + @Test + void doWithProtectedMethods() { + ListSavingMethodCallback mc = new ListSavingMethodCallback(); + ReflectionUtils.doWithMethods(TestObject.class, mc, method -> Modifier.isProtected(method.getModifiers())); + assertThat(mc.getMethodNames().isEmpty()).isFalse(); + assertThat(mc.getMethodNames().contains("clone")).as("Must find protected method on Object").isTrue(); + assertThat(mc.getMethodNames().contains("finalize")).as("Must find protected method on Object").isTrue(); + assertThat(mc.getMethodNames().contains("hashCode")).as("Public, not protected").isFalse(); + assertThat(mc.getMethodNames().contains("absquatulate")).as("Public, not protected").isFalse(); + } + + @Test + void duplicatesFound() { + ListSavingMethodCallback mc = new ListSavingMethodCallback(); + ReflectionUtils.doWithMethods(TestObjectSubclass.class, mc); + int absquatulateCount = 0; + for (String name : mc.getMethodNames()) { + if (name.equals("absquatulate")) { + ++absquatulateCount; + } + } + assertThat(absquatulateCount).as("Found 2 absquatulates").isEqualTo(2); + } + + @Test + void findMethod() throws Exception { + assertThat(ReflectionUtils.findMethod(B.class, "bar", String.class)).isNotNull(); + assertThat(ReflectionUtils.findMethod(B.class, "foo", Integer.class)).isNotNull(); + assertThat(ReflectionUtils.findMethod(B.class, "getClass")).isNotNull(); + } + + @Disabled("[SPR-8644] findMethod() does not currently support var-args") + @Test + void findMethodWithVarArgs() throws Exception { + assertThat(ReflectionUtils.findMethod(B.class, "add", int.class, int.class, int.class)).isNotNull(); + } + + @Test + void isCglibRenamedMethod() throws SecurityException, NoSuchMethodException { + @SuppressWarnings("unused") + class C { + public void CGLIB$m1$123() { + } + + public void CGLIB$m1$0() { + } + + public void CGLIB$$0() { + } + + public void CGLIB$m1$() { + } + + public void CGLIB$m1() { + } + + public void m1() { + } + + public void m1$() { + } + + public void m1$1() { + } + } + assertThat(ReflectionUtils.isCglibRenamedMethod(C.class.getMethod("CGLIB$m1$123"))).isTrue(); + assertThat(ReflectionUtils.isCglibRenamedMethod(C.class.getMethod("CGLIB$m1$0"))).isTrue(); + assertThat(ReflectionUtils.isCglibRenamedMethod(C.class.getMethod("CGLIB$$0"))).isFalse(); + assertThat(ReflectionUtils.isCglibRenamedMethod(C.class.getMethod("CGLIB$m1$"))).isFalse(); + assertThat(ReflectionUtils.isCglibRenamedMethod(C.class.getMethod("CGLIB$m1"))).isFalse(); + assertThat(ReflectionUtils.isCglibRenamedMethod(C.class.getMethod("m1"))).isFalse(); + assertThat(ReflectionUtils.isCglibRenamedMethod(C.class.getMethod("m1$"))).isFalse(); + assertThat(ReflectionUtils.isCglibRenamedMethod(C.class.getMethod("m1$1"))).isFalse(); + } + + @Test + void getAllDeclaredMethods() throws Exception { + class Foo { + @Override + public String toString() { + return super.toString(); + } + } + int toStringMethodCount = 0; + for (Method method : ReflectionUtils.getAllDeclaredMethods(Foo.class)) { + if (method.getName().equals("toString")) { + toStringMethodCount++; + } + } + assertThat(toStringMethodCount).isEqualTo(2); + } + + @Test + void getUniqueDeclaredMethods() throws Exception { + class Foo { + @Override + public String toString() { + return super.toString(); + } + } + int toStringMethodCount = 0; + for (Method method : ReflectionUtils.getUniqueDeclaredMethods(Foo.class)) { + if (method.getName().equals("toString")) { + toStringMethodCount++; + } + } + assertThat(toStringMethodCount).isEqualTo(1); + } + + @Test + void getUniqueDeclaredMethods_withCovariantReturnType() throws Exception { + class Parent { + @SuppressWarnings("unused") + public Number m1() { + return Integer.valueOf(42); + } + } + class Leaf extends Parent { + @Override + public Integer m1() { + return Integer.valueOf(42); + } + } + int m1MethodCount = 0; + Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(Leaf.class); + for (Method method : methods) { + if (method.getName().equals("m1")) { + m1MethodCount++; + } + } + assertThat(m1MethodCount).isEqualTo(1); + assertThat(ObjectUtils.containsElement(methods, Leaf.class.getMethod("m1"))).isTrue(); + assertThat(ObjectUtils.containsElement(methods, Parent.class.getMethod("m1"))).isFalse(); + } + + @Test + void getDeclaredMethodsReturnsCopy() { + Method[] m1 = ReflectionUtils.getDeclaredMethods(A.class); + Method[] m2 = ReflectionUtils.getDeclaredMethods(A.class); + assertThat(m1). isNotSameAs(m2); + } + + private static class ListSavingMethodCallback implements ReflectionUtils.MethodCallback { + + private List methodNames = new ArrayList<>(); + + private List methods = new ArrayList<>(); + + @Override + public void doWith(Method m) throws IllegalArgumentException, IllegalAccessException { + this.methodNames.add(m.getName()); + this.methods.add(m); + } + + public List getMethodNames() { + return this.methodNames; + } + + @SuppressWarnings("unused") + public List getMethods() { + return this.methods; + } + } + + private static class TestObjectSubclass extends TestObject { + + @Override + public void absquatulate() { + throw new UnsupportedOperationException(); + } + } + + private static class TestObjectSubclassWithPublicField extends TestObject { + + @SuppressWarnings("unused") + public String publicField = "foo"; + } + + private static class TestObjectSubclassWithNewField extends TestObject { + + private int magic; + + protected String prot = "foo"; + } + + private static class TestObjectSubclassWithFinalField extends TestObject { + + @SuppressWarnings("unused") + private final String foo = "will break naive copy that doesn't exclude statics"; + } + + private static class A { + + @SuppressWarnings("unused") + private void foo(Integer i) throws RemoteException { + } + } + + @SuppressWarnings("unused") + private static class B extends A { + + void bar(String s) throws IllegalArgumentException { + } + + int add(int... args) { + int sum = 0; + for (int i = 0; i < args.length; i++) { + sum += args[i]; + } + return sum; + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/ResizableByteArrayOutputStreamTests.java b/spring-core/src/test/java/org/springframework/util/ResizableByteArrayOutputStreamTests.java new file mode 100644 index 0000000..8646aba --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/ResizableByteArrayOutputStreamTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Brian Clozel + * @author Juergen Hoeller + */ +class ResizableByteArrayOutputStreamTests { + + private static final int INITIAL_CAPACITY = 256; + + private ResizableByteArrayOutputStream baos; + + private byte[] helloBytes; + + + @BeforeEach + void setUp() throws Exception { + this.baos = new ResizableByteArrayOutputStream(INITIAL_CAPACITY); + this.helloBytes = "Hello World".getBytes("UTF-8"); + } + + + @Test + void resize() throws Exception { + assertThat(this.baos.capacity()).isEqualTo(INITIAL_CAPACITY); + this.baos.write(helloBytes); + int size = 64; + this.baos.resize(size); + assertThat(this.baos.capacity()).isEqualTo(size); + assertByteArrayEqualsString(this.baos); + } + + @Test + void autoGrow() { + assertThat(this.baos.capacity()).isEqualTo(INITIAL_CAPACITY); + for (int i = 0; i < 129; i++) { + this.baos.write(0); + } + assertThat(this.baos.capacity()).isEqualTo(256); + } + + @Test + void grow() throws Exception { + assertThat(this.baos.capacity()).isEqualTo(INITIAL_CAPACITY); + this.baos.write(helloBytes); + this.baos.grow(1000); + assertThat(this.baos.capacity()).isEqualTo((this.helloBytes.length + 1000)); + assertByteArrayEqualsString(this.baos); + } + + @Test + void write() throws Exception{ + this.baos.write(helloBytes); + assertByteArrayEqualsString(this.baos); + } + + @Test + void failResize() throws Exception{ + this.baos.write(helloBytes); + assertThatIllegalArgumentException().isThrownBy(() -> + this.baos.resize(5)); + } + + + private void assertByteArrayEqualsString(ResizableByteArrayOutputStream actual) { + assertThat(actual.toByteArray()).isEqualTo(helloBytes); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/ResourceUtilsTests.java b/spring-core/src/test/java/org/springframework/util/ResourceUtilsTests.java new file mode 100644 index 0000000..dc7b833 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/ResourceUtilsTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.io.IOException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + */ +class ResourceUtilsTests { + + @Test + void isJarURL() throws Exception { + assertThat(ResourceUtils.isJarURL(new URL("jar:file:myjar.jar!/mypath"))).isTrue(); + assertThat(ResourceUtils.isJarURL(new URL(null, "zip:file:myjar.jar!/mypath", new DummyURLStreamHandler()))).isTrue(); + assertThat(ResourceUtils.isJarURL(new URL(null, "wsjar:file:myjar.jar!/mypath", new DummyURLStreamHandler()))).isTrue(); + assertThat(ResourceUtils.isJarURL(new URL(null, "jar:war:file:mywar.war*/myjar.jar!/mypath", new DummyURLStreamHandler()))).isTrue(); + assertThat(ResourceUtils.isJarURL(new URL("file:myjar.jar"))).isFalse(); + assertThat(ResourceUtils.isJarURL(new URL("http:myserver/myjar.jar"))).isFalse(); + } + + @Test + void extractJarFileURL() throws Exception { + assertThat(ResourceUtils.extractJarFileURL(new URL("jar:file:myjar.jar!/mypath"))).isEqualTo(new URL("file:myjar.jar")); + assertThat(ResourceUtils.extractJarFileURL(new URL(null, "jar:myjar.jar!/mypath", new DummyURLStreamHandler()))).isEqualTo(new URL("file:/myjar.jar")); + assertThat(ResourceUtils.extractJarFileURL(new URL(null, "zip:file:myjar.jar!/mypath", new DummyURLStreamHandler()))).isEqualTo(new URL("file:myjar.jar")); + assertThat(ResourceUtils.extractJarFileURL(new URL(null, "wsjar:file:myjar.jar!/mypath", new DummyURLStreamHandler()))).isEqualTo(new URL("file:myjar.jar")); + + assertThat(ResourceUtils.extractJarFileURL(new URL("file:myjar.jar"))).isEqualTo(new URL("file:myjar.jar")); + assertThat(ResourceUtils.extractJarFileURL(new URL("jar:file:myjar.jar!/"))).isEqualTo(new URL("file:myjar.jar")); + assertThat(ResourceUtils.extractJarFileURL(new URL(null, "zip:file:myjar.jar!/", new DummyURLStreamHandler()))).isEqualTo(new URL("file:myjar.jar")); + assertThat(ResourceUtils.extractJarFileURL(new URL(null, "wsjar:file:myjar.jar!/", new DummyURLStreamHandler()))).isEqualTo(new URL("file:myjar.jar")); + } + + @Test + void extractArchiveURL() throws Exception { + assertThat(ResourceUtils.extractArchiveURL(new URL("jar:file:myjar.jar!/mypath"))).isEqualTo(new URL("file:myjar.jar")); + assertThat(ResourceUtils.extractArchiveURL(new URL(null, "jar:myjar.jar!/mypath", new DummyURLStreamHandler()))).isEqualTo(new URL("file:/myjar.jar")); + assertThat(ResourceUtils.extractArchiveURL(new URL(null, "zip:file:myjar.jar!/mypath", new DummyURLStreamHandler()))).isEqualTo(new URL("file:myjar.jar")); + assertThat(ResourceUtils.extractArchiveURL(new URL(null, "wsjar:file:myjar.jar!/mypath", new DummyURLStreamHandler()))).isEqualTo(new URL("file:myjar.jar")); + assertThat(ResourceUtils.extractArchiveURL(new URL(null, "jar:war:file:mywar.war*/myjar.jar!/mypath", new DummyURLStreamHandler()))).isEqualTo(new URL("file:mywar.war")); + + assertThat(ResourceUtils.extractArchiveURL(new URL("file:myjar.jar"))).isEqualTo(new URL("file:myjar.jar")); + assertThat(ResourceUtils.extractArchiveURL(new URL("jar:file:myjar.jar!/"))).isEqualTo(new URL("file:myjar.jar")); + assertThat(ResourceUtils.extractArchiveURL(new URL(null, "zip:file:myjar.jar!/", new DummyURLStreamHandler()))).isEqualTo(new URL("file:myjar.jar")); + assertThat(ResourceUtils.extractArchiveURL(new URL(null, "wsjar:file:myjar.jar!/", new DummyURLStreamHandler()))).isEqualTo(new URL("file:myjar.jar")); + assertThat(ResourceUtils.extractArchiveURL(new URL(null, "jar:war:file:mywar.war*/myjar.jar!/", new DummyURLStreamHandler()))).isEqualTo(new URL("file:mywar.war")); + } + + + /** + * Dummy URLStreamHandler that's just specified to suppress the standard + * {@code java.net.URL} URLStreamHandler lookup, to be able to + * use the standard URL class for parsing "rmi:..." URLs. + */ + private static class DummyURLStreamHandler extends URLStreamHandler { + + @Override + protected URLConnection openConnection(URL url) throws IOException { + throw new UnsupportedOperationException(); + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/SerializationUtilsTests.java b/spring-core/src/test/java/org/springframework/util/SerializationUtilsTests.java new file mode 100644 index 0000000..d48b00b --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/SerializationUtilsTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.math.BigInteger; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Unit tests for {@link SerializationUtils}. + * + * @author Dave Syer + * @since 3.0.5 + */ +class SerializationUtilsTests { + + private static final BigInteger FOO = new BigInteger( + "-9702942423549012526722364838327831379660941553432801565505143675386108883970811292563757558516603356009681061" + + "5697574744209306031461371833798723505120163874786203211176873686513374052845353833564048"); + + + @Test + void serializeCycleSunnyDay() throws Exception { + assertThat(SerializationUtils.deserialize(SerializationUtils.serialize("foo"))).isEqualTo("foo"); + } + + @Test + void deserializeUndefined() throws Exception { + assertThatIllegalStateException().isThrownBy(() -> SerializationUtils.deserialize(FOO.toByteArray())); + } + + @Test + void serializeNonSerializable() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> SerializationUtils.serialize(new Object())); + } + + @Test + void deserializeNonSerializable() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> SerializationUtils.deserialize("foo".getBytes())); + } + + @Test + void serializeNull() throws Exception { + assertThat(SerializationUtils.serialize(null)).isNull(); + } + + @Test + void deserializeNull() throws Exception { + assertThat(SerializationUtils.deserialize(null)).isNull(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/SocketUtilsTests.java b/spring-core/src/test/java/org/springframework/util/SocketUtilsTests.java new file mode 100644 index 0000000..d13e78e --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/SocketUtilsTests.java @@ -0,0 +1,238 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.util.SortedSet; + +import javax.net.ServerSocketFactory; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.springframework.util.SocketUtils.PORT_RANGE_MAX; +import static org.springframework.util.SocketUtils.PORT_RANGE_MIN; + +/** + * Unit tests for {@link SocketUtils}. + * + * @author Sam Brannen + * @author Gary Russell + */ +class SocketUtilsTests { + + @Test + void canBeInstantiated() { + // Just making sure somebody doesn't try to make SocketUtils abstract, + // since that would be a breaking change due to the intentional public + // constructor. + new SocketUtils(); + } + + // TCP + + @Test + void findAvailableTcpPortWithZeroMinPort() { + assertThatIllegalArgumentException().isThrownBy(() -> + SocketUtils.findAvailableTcpPort(0)); + } + + @Test + void findAvailableTcpPortWithNegativeMinPort() { + assertThatIllegalArgumentException().isThrownBy(() -> + SocketUtils.findAvailableTcpPort(-500)); + } + + @Test + void findAvailableTcpPort() { + int port = SocketUtils.findAvailableTcpPort(); + assertPortInRange(port, PORT_RANGE_MIN, PORT_RANGE_MAX); + } + + @Test + void findAvailableTcpPortWithMinPortEqualToMaxPort() { + int minMaxPort = SocketUtils.findAvailableTcpPort(); + int port = SocketUtils.findAvailableTcpPort(minMaxPort, minMaxPort); + assertThat(port).isEqualTo(minMaxPort); + } + + @Test + void findAvailableTcpPortWhenPortOnLoopbackInterfaceIsNotAvailable() throws Exception { + int port = SocketUtils.findAvailableTcpPort(); + try (ServerSocket socket = ServerSocketFactory.getDefault().createServerSocket(port, 1, InetAddress.getByName("localhost"))) { + assertThat(socket).isNotNull(); + // will only look for the exact port + assertThatIllegalStateException().isThrownBy(() -> + SocketUtils.findAvailableTcpPort(port, port)) + .withMessageStartingWith("Could not find an available TCP port") + .withMessageEndingWith("after 1 attempts"); + } + } + + @Test + void findAvailableTcpPortWithMin() { + int port = SocketUtils.findAvailableTcpPort(50000); + assertPortInRange(port, 50000, PORT_RANGE_MAX); + } + + @Test + void findAvailableTcpPortInRange() { + int minPort = 20000; + int maxPort = minPort + 1000; + int port = SocketUtils.findAvailableTcpPort(minPort, maxPort); + assertPortInRange(port, minPort, maxPort); + } + + @Test + void find4AvailableTcpPorts() { + findAvailableTcpPorts(4); + } + + @Test + void find50AvailableTcpPorts() { + findAvailableTcpPorts(50); + } + + @Test + void find4AvailableTcpPortsInRange() { + findAvailableTcpPorts(4, 30000, 35000); + } + + @Test + void find50AvailableTcpPortsInRange() { + findAvailableTcpPorts(50, 40000, 45000); + } + + @Test + void findAvailableTcpPortsWithRequestedNumberGreaterThanSizeOfRange() { + assertThatIllegalArgumentException().isThrownBy(() -> + findAvailableTcpPorts(50, 45000, 45010)); + } + + + // UDP + + @Test + void findAvailableUdpPortWithZeroMinPort() { + assertThatIllegalArgumentException().isThrownBy(() -> + SocketUtils.findAvailableUdpPort(0)); + } + + @Test + void findAvailableUdpPortWithNegativeMinPort() { + assertThatIllegalArgumentException().isThrownBy(() -> + SocketUtils.findAvailableUdpPort(-500)); + } + + @Test + void findAvailableUdpPort() { + int port = SocketUtils.findAvailableUdpPort(); + assertPortInRange(port, PORT_RANGE_MIN, PORT_RANGE_MAX); + } + + @Test + void findAvailableUdpPortWhenPortOnLoopbackInterfaceIsNotAvailable() throws Exception { + int port = SocketUtils.findAvailableUdpPort(); + try (DatagramSocket socket = new DatagramSocket(port, InetAddress.getByName("localhost"))) { + assertThat(socket).isNotNull(); + // will only look for the exact port + assertThatIllegalStateException().isThrownBy(() -> + SocketUtils.findAvailableUdpPort(port, port)) + .withMessageStartingWith("Could not find an available UDP port") + .withMessageEndingWith("after 1 attempts"); + } + } + + @Test + void findAvailableUdpPortWithMin() { + int port = SocketUtils.findAvailableUdpPort(50000); + assertPortInRange(port, 50000, PORT_RANGE_MAX); + } + + @Test + void findAvailableUdpPortInRange() { + int minPort = 20000; + int maxPort = minPort + 1000; + int port = SocketUtils.findAvailableUdpPort(minPort, maxPort); + assertPortInRange(port, minPort, maxPort); + } + + @Test + void find4AvailableUdpPorts() { + findAvailableUdpPorts(4); + } + + @Test + void find50AvailableUdpPorts() { + findAvailableUdpPorts(50); + } + + @Test + void find4AvailableUdpPortsInRange() { + findAvailableUdpPorts(4, 30000, 35000); + } + + @Test + void find50AvailableUdpPortsInRange() { + findAvailableUdpPorts(50, 40000, 45000); + } + + @Test + void findAvailableUdpPortsWithRequestedNumberGreaterThanSizeOfRange() { + assertThatIllegalArgumentException().isThrownBy(() -> + findAvailableUdpPorts(50, 45000, 45010)); + } + + + // Helpers + + private void findAvailableTcpPorts(int numRequested) { + SortedSet ports = SocketUtils.findAvailableTcpPorts(numRequested); + assertAvailablePorts(ports, numRequested, PORT_RANGE_MIN, PORT_RANGE_MAX); + } + + private void findAvailableTcpPorts(int numRequested, int minPort, int maxPort) { + SortedSet ports = SocketUtils.findAvailableTcpPorts(numRequested, minPort, maxPort); + assertAvailablePorts(ports, numRequested, minPort, maxPort); + } + + private void findAvailableUdpPorts(int numRequested) { + SortedSet ports = SocketUtils.findAvailableUdpPorts(numRequested); + assertAvailablePorts(ports, numRequested, PORT_RANGE_MIN, PORT_RANGE_MAX); + } + + private void findAvailableUdpPorts(int numRequested, int minPort, int maxPort) { + SortedSet ports = SocketUtils.findAvailableUdpPorts(numRequested, minPort, maxPort); + assertAvailablePorts(ports, numRequested, minPort, maxPort); + } + private void assertPortInRange(int port, int minPort, int maxPort) { + assertThat(port >= minPort).as("port [" + port + "] >= " + minPort).isTrue(); + assertThat(port <= maxPort).as("port [" + port + "] <= " + maxPort).isTrue(); + } + + private void assertAvailablePorts(SortedSet ports, int numRequested, int minPort, int maxPort) { + assertThat(ports.size()).as("number of ports requested").isEqualTo(numRequested); + for (int port : ports) { + assertPortInRange(port, minPort, maxPort); + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/StopWatchTests.java b/spring-core/src/test/java/org/springframework/util/StopWatchTests.java new file mode 100644 index 0000000..93af29b --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/StopWatchTests.java @@ -0,0 +1,146 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.StopWatch.TaskInfo; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Unit tests for {@link StopWatch}. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Sam Brannen + */ +class StopWatchTests { + + private static final String ID = "myId"; + + private static final String name1 = "Task 1"; + private static final String name2 = "Task 2"; + + private static final long duration1 = 200; + private static final long duration2 = 100; + private static final long fudgeFactor = 50; + + private final StopWatch stopWatch = new StopWatch(ID); + + + @Test + void failureToStartBeforeGettingTimings() { + assertThatIllegalStateException().isThrownBy(stopWatch::getLastTaskTimeMillis); + } + + @Test + void failureToStartBeforeStop() { + assertThatIllegalStateException().isThrownBy(stopWatch::stop); + } + + @Test + void rejectsStartTwice() { + stopWatch.start(); + assertThat(stopWatch.isRunning()).isTrue(); + stopWatch.stop(); + assertThat(stopWatch.isRunning()).isFalse(); + + stopWatch.start(); + assertThat(stopWatch.isRunning()).isTrue(); + assertThatIllegalStateException().isThrownBy(stopWatch::start); + } + + @Test + void validUsage() throws Exception { + assertThat(stopWatch.isRunning()).isFalse(); + + stopWatch.start(name1); + Thread.sleep(duration1); + assertThat(stopWatch.isRunning()).isTrue(); + assertThat(stopWatch.currentTaskName()).isEqualTo(name1); + stopWatch.stop(); + assertThat(stopWatch.isRunning()).isFalse(); + assertThat(stopWatch.getLastTaskTimeNanos()) + .as("last task time in nanoseconds for task #1") + .isGreaterThanOrEqualTo(millisToNanos(duration1 - fudgeFactor)) + .isLessThanOrEqualTo(millisToNanos(duration1 + fudgeFactor)); + assertThat(stopWatch.getTotalTimeMillis()) + .as("total time in milliseconds for task #1") + .isGreaterThanOrEqualTo(duration1 - fudgeFactor) + .isLessThanOrEqualTo(duration1 + fudgeFactor); + assertThat(stopWatch.getTotalTimeSeconds()) + .as("total time in seconds for task #1") + .isGreaterThanOrEqualTo((duration1 - fudgeFactor) / 1000.0) + .isLessThanOrEqualTo((duration1 + fudgeFactor) / 1000.0); + + stopWatch.start(name2); + Thread.sleep(duration2); + assertThat(stopWatch.isRunning()).isTrue(); + assertThat(stopWatch.currentTaskName()).isEqualTo(name2); + stopWatch.stop(); + assertThat(stopWatch.isRunning()).isFalse(); + assertThat(stopWatch.getLastTaskTimeNanos()) + .as("last task time in nanoseconds for task #2") + .isGreaterThanOrEqualTo(millisToNanos(duration2)) + .isLessThanOrEqualTo(millisToNanos(duration2 + fudgeFactor)); + assertThat(stopWatch.getTotalTimeMillis()) + .as("total time in milliseconds for tasks #1 and #2") + .isGreaterThanOrEqualTo(duration1 + duration2 - fudgeFactor) + .isLessThanOrEqualTo(duration1 + duration2 + fudgeFactor); + assertThat(stopWatch.getTotalTimeSeconds()) + .as("total time in seconds for task #2") + .isGreaterThanOrEqualTo((duration1 + duration2 - fudgeFactor) / 1000.0) + .isLessThanOrEqualTo((duration1 + duration2 + fudgeFactor) / 1000.0); + + assertThat(stopWatch.getTaskCount()).isEqualTo(2); + assertThat(stopWatch.prettyPrint()).contains(name1, name2); + assertThat(stopWatch.getTaskInfo()).extracting(TaskInfo::getTaskName).containsExactly(name1, name2); + assertThat(stopWatch.toString()).contains(ID, name1, name2); + assertThat(stopWatch.getId()).isEqualTo(ID); + } + + @Test + void validUsageDoesNotKeepTaskList() throws Exception { + stopWatch.setKeepTaskList(false); + + stopWatch.start(name1); + Thread.sleep(duration1); + assertThat(stopWatch.currentTaskName()).isEqualTo(name1); + stopWatch.stop(); + + stopWatch.start(name2); + Thread.sleep(duration2); + assertThat(stopWatch.currentTaskName()).isEqualTo(name2); + stopWatch.stop(); + + assertThat(stopWatch.getTaskCount()).isEqualTo(2); + assertThat(stopWatch.prettyPrint()).contains("No task info kept"); + assertThat(stopWatch.toString()).doesNotContain(name1, name2); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(stopWatch::getTaskInfo) + .withMessage("Task info is not being kept!"); + } + + private static long millisToNanos(long duration) { + return MILLISECONDS.toNanos(duration); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/StreamUtilsTests.java b/spring-core/src/test/java/org/springframework/util/StreamUtilsTests.java new file mode 100644 index 0000000..21f0829 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/StreamUtilsTests.java @@ -0,0 +1,138 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Random; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link StreamUtils}. + * + * @author Phillip Webb + */ +class StreamUtilsTests { + + private byte[] bytes = new byte[StreamUtils.BUFFER_SIZE + 10]; + + private String string = ""; + + @BeforeEach + void setup() { + new Random().nextBytes(bytes); + while (string.length() < StreamUtils.BUFFER_SIZE + 10) { + string += UUID.randomUUID().toString(); + } + } + + @Test + void copyToByteArray() throws Exception { + InputStream inputStream = spy(new ByteArrayInputStream(bytes)); + byte[] actual = StreamUtils.copyToByteArray(inputStream); + assertThat(actual).isEqualTo(bytes); + verify(inputStream, never()).close(); + } + + @Test + void copyToString() throws Exception { + Charset charset = Charset.defaultCharset(); + InputStream inputStream = spy(new ByteArrayInputStream(string.getBytes(charset))); + String actual = StreamUtils.copyToString(inputStream, charset); + assertThat(actual).isEqualTo(string); + verify(inputStream, never()).close(); + } + + @Test + void copyBytes() throws Exception { + ByteArrayOutputStream out = spy(new ByteArrayOutputStream()); + StreamUtils.copy(bytes, out); + assertThat(out.toByteArray()).isEqualTo(bytes); + verify(out, never()).close(); + } + + @Test + void copyString() throws Exception { + Charset charset = Charset.defaultCharset(); + ByteArrayOutputStream out = spy(new ByteArrayOutputStream()); + StreamUtils.copy(string, charset, out); + assertThat(out.toByteArray()).isEqualTo(string.getBytes(charset)); + verify(out, never()).close(); + } + + @Test + void copyStream() throws Exception { + ByteArrayOutputStream out = spy(new ByteArrayOutputStream()); + StreamUtils.copy(new ByteArrayInputStream(bytes), out); + assertThat(out.toByteArray()).isEqualTo(bytes); + verify(out, never()).close(); + } + + @Test + void copyRange() throws Exception { + ByteArrayOutputStream out = spy(new ByteArrayOutputStream()); + StreamUtils.copyRange(new ByteArrayInputStream(bytes), out, 0, 100); + byte[] range = Arrays.copyOfRange(bytes, 0, 101); + assertThat(out.toByteArray()).isEqualTo(range); + verify(out, never()).close(); + } + + @Test + void nonClosingInputStream() throws Exception { + InputStream source = mock(InputStream.class); + InputStream nonClosing = StreamUtils.nonClosing(source); + nonClosing.read(); + nonClosing.read(bytes); + nonClosing.read(bytes, 1, 2); + nonClosing.close(); + InOrder ordered = inOrder(source); + ordered.verify(source).read(); + ordered.verify(source).read(bytes, 0, bytes.length); + ordered.verify(source).read(bytes, 1, 2); + ordered.verify(source, never()).close(); + } + + @Test + void nonClosingOutputStream() throws Exception { + OutputStream source = mock(OutputStream.class); + OutputStream nonClosing = StreamUtils.nonClosing(source); + nonClosing.write(1); + nonClosing.write(bytes); + nonClosing.write(bytes, 1, 2); + nonClosing.close(); + InOrder ordered = inOrder(source); + ordered.verify(source).write(1); + ordered.verify(source).write(bytes, 0, bytes.length); + ordered.verify(source).write(bytes, 1, 2); + ordered.verify(source, never()).close(); + } +} diff --git a/spring-core/src/test/java/org/springframework/util/StringUtilsTests.java b/spring-core/src/test/java/org/springframework/util/StringUtilsTests.java new file mode 100644 index 0000000..145a442 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/StringUtilsTests.java @@ -0,0 +1,778 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.util.Arrays; +import java.util.Locale; +import java.util.Properties; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Rod Johnson + * @author Juergen Hoeller + * @author Rick Evans + * @author Sam Brannen + */ +class StringUtilsTests { + + @Test + void hasTextBlank() { + String blank = " "; + assertThat(StringUtils.hasText(blank)).isEqualTo(false); + } + + @Test + void hasTextNullEmpty() { + assertThat(StringUtils.hasText(null)).isEqualTo(false); + assertThat(StringUtils.hasText("")).isEqualTo(false); + } + + @Test + void hasTextValid() { + assertThat(StringUtils.hasText("t")).isEqualTo(true); + } + + @Test + void containsWhitespace() { + assertThat(StringUtils.containsWhitespace(null)).isFalse(); + assertThat(StringUtils.containsWhitespace("")).isFalse(); + assertThat(StringUtils.containsWhitespace("a")).isFalse(); + assertThat(StringUtils.containsWhitespace("abc")).isFalse(); + assertThat(StringUtils.containsWhitespace(" ")).isTrue(); + assertThat(StringUtils.containsWhitespace("\t")).isTrue(); + assertThat(StringUtils.containsWhitespace("\n")).isTrue(); + assertThat(StringUtils.containsWhitespace(" a")).isTrue(); + assertThat(StringUtils.containsWhitespace("abc ")).isTrue(); + assertThat(StringUtils.containsWhitespace("a b")).isTrue(); + assertThat(StringUtils.containsWhitespace("a b")).isTrue(); + } + + @Test + void trimWhitespace() { + assertThat(StringUtils.trimWhitespace(null)).isEqualTo(null); + assertThat(StringUtils.trimWhitespace("")).isEqualTo(""); + assertThat(StringUtils.trimWhitespace(" ")).isEqualTo(""); + assertThat(StringUtils.trimWhitespace("\t")).isEqualTo(""); + assertThat(StringUtils.trimWhitespace("\n")).isEqualTo(""); + assertThat(StringUtils.trimWhitespace(" \t\n")).isEqualTo(""); + assertThat(StringUtils.trimWhitespace(" a")).isEqualTo("a"); + assertThat(StringUtils.trimWhitespace("a ")).isEqualTo("a"); + assertThat(StringUtils.trimWhitespace(" a ")).isEqualTo("a"); + assertThat(StringUtils.trimWhitespace(" a b ")).isEqualTo("a b"); + assertThat(StringUtils.trimWhitespace(" a b c ")).isEqualTo("a b c"); + } + + @Test + void trimAllWhitespace() { + assertThat(StringUtils.trimAllWhitespace(null)).isEqualTo(null); + assertThat(StringUtils.trimAllWhitespace("")).isEqualTo(""); + assertThat(StringUtils.trimAllWhitespace(" ")).isEqualTo(""); + assertThat(StringUtils.trimAllWhitespace("\t")).isEqualTo(""); + assertThat(StringUtils.trimAllWhitespace("\n")).isEqualTo(""); + assertThat(StringUtils.trimAllWhitespace(" \t\n")).isEqualTo(""); + assertThat(StringUtils.trimAllWhitespace(" a")).isEqualTo("a"); + assertThat(StringUtils.trimAllWhitespace("a ")).isEqualTo("a"); + assertThat(StringUtils.trimAllWhitespace(" a ")).isEqualTo("a"); + assertThat(StringUtils.trimAllWhitespace(" a b ")).isEqualTo("ab"); + assertThat(StringUtils.trimAllWhitespace(" a b c ")).isEqualTo("abc"); + } + + @Test + void trimLeadingWhitespace() { + assertThat(StringUtils.trimLeadingWhitespace(null)).isEqualTo(null); + assertThat(StringUtils.trimLeadingWhitespace("")).isEqualTo(""); + assertThat(StringUtils.trimLeadingWhitespace(" ")).isEqualTo(""); + assertThat(StringUtils.trimLeadingWhitespace("\t")).isEqualTo(""); + assertThat(StringUtils.trimLeadingWhitespace("\n")).isEqualTo(""); + assertThat(StringUtils.trimLeadingWhitespace(" \t\n")).isEqualTo(""); + assertThat(StringUtils.trimLeadingWhitespace(" a")).isEqualTo("a"); + assertThat(StringUtils.trimLeadingWhitespace("a ")).isEqualTo("a "); + assertThat(StringUtils.trimLeadingWhitespace(" a ")).isEqualTo("a "); + assertThat(StringUtils.trimLeadingWhitespace(" a b ")).isEqualTo("a b "); + assertThat(StringUtils.trimLeadingWhitespace(" a b c ")).isEqualTo("a b c "); + } + + @Test + void trimTrailingWhitespace() { + assertThat(StringUtils.trimTrailingWhitespace(null)).isEqualTo(null); + assertThat(StringUtils.trimTrailingWhitespace("")).isEqualTo(""); + assertThat(StringUtils.trimTrailingWhitespace(" ")).isEqualTo(""); + assertThat(StringUtils.trimTrailingWhitespace("\t")).isEqualTo(""); + assertThat(StringUtils.trimTrailingWhitespace("\n")).isEqualTo(""); + assertThat(StringUtils.trimTrailingWhitespace(" \t\n")).isEqualTo(""); + assertThat(StringUtils.trimTrailingWhitespace("a ")).isEqualTo("a"); + assertThat(StringUtils.trimTrailingWhitespace(" a")).isEqualTo(" a"); + assertThat(StringUtils.trimTrailingWhitespace(" a ")).isEqualTo(" a"); + assertThat(StringUtils.trimTrailingWhitespace(" a b ")).isEqualTo(" a b"); + assertThat(StringUtils.trimTrailingWhitespace(" a b c ")).isEqualTo(" a b c"); + } + + @Test + void trimLeadingCharacter() { + assertThat(StringUtils.trimLeadingCharacter(null, ' ')).isEqualTo(null); + assertThat(StringUtils.trimLeadingCharacter("", ' ')).isEqualTo(""); + assertThat(StringUtils.trimLeadingCharacter(" ", ' ')).isEqualTo(""); + assertThat(StringUtils.trimLeadingCharacter("\t", ' ')).isEqualTo("\t"); + assertThat(StringUtils.trimLeadingCharacter(" a", ' ')).isEqualTo("a"); + assertThat(StringUtils.trimLeadingCharacter("a ", ' ')).isEqualTo("a "); + assertThat(StringUtils.trimLeadingCharacter(" a ", ' ')).isEqualTo("a "); + assertThat(StringUtils.trimLeadingCharacter(" a b ", ' ')).isEqualTo("a b "); + assertThat(StringUtils.trimLeadingCharacter(" a b c ", ' ')).isEqualTo("a b c "); + } + + @Test + void trimTrailingCharacter() { + assertThat(StringUtils.trimTrailingCharacter(null, ' ')).isEqualTo(null); + assertThat(StringUtils.trimTrailingCharacter("", ' ')).isEqualTo(""); + assertThat(StringUtils.trimTrailingCharacter(" ", ' ')).isEqualTo(""); + assertThat(StringUtils.trimTrailingCharacter("\t", ' ')).isEqualTo("\t"); + assertThat(StringUtils.trimTrailingCharacter("a ", ' ')).isEqualTo("a"); + assertThat(StringUtils.trimTrailingCharacter(" a", ' ')).isEqualTo(" a"); + assertThat(StringUtils.trimTrailingCharacter(" a ", ' ')).isEqualTo(" a"); + assertThat(StringUtils.trimTrailingCharacter(" a b ", ' ')).isEqualTo(" a b"); + assertThat(StringUtils.trimTrailingCharacter(" a b c ", ' ')).isEqualTo(" a b c"); + } + + @Test + void startsWithIgnoreCase() { + String prefix = "fOo"; + assertThat(StringUtils.startsWithIgnoreCase("foo", prefix)).isTrue(); + assertThat(StringUtils.startsWithIgnoreCase("Foo", prefix)).isTrue(); + assertThat(StringUtils.startsWithIgnoreCase("foobar", prefix)).isTrue(); + assertThat(StringUtils.startsWithIgnoreCase("foobarbar", prefix)).isTrue(); + assertThat(StringUtils.startsWithIgnoreCase("Foobar", prefix)).isTrue(); + assertThat(StringUtils.startsWithIgnoreCase("FoobarBar", prefix)).isTrue(); + assertThat(StringUtils.startsWithIgnoreCase("foObar", prefix)).isTrue(); + assertThat(StringUtils.startsWithIgnoreCase("FOObar", prefix)).isTrue(); + assertThat(StringUtils.startsWithIgnoreCase("fOobar", prefix)).isTrue(); + assertThat(StringUtils.startsWithIgnoreCase(null, prefix)).isFalse(); + assertThat(StringUtils.startsWithIgnoreCase("fOobar", null)).isFalse(); + assertThat(StringUtils.startsWithIgnoreCase("b", prefix)).isFalse(); + assertThat(StringUtils.startsWithIgnoreCase("barfoo", prefix)).isFalse(); + assertThat(StringUtils.startsWithIgnoreCase("barfoobar", prefix)).isFalse(); + } + + @Test + void endsWithIgnoreCase() { + String suffix = "fOo"; + assertThat(StringUtils.endsWithIgnoreCase("foo", suffix)).isTrue(); + assertThat(StringUtils.endsWithIgnoreCase("Foo", suffix)).isTrue(); + assertThat(StringUtils.endsWithIgnoreCase("barfoo", suffix)).isTrue(); + assertThat(StringUtils.endsWithIgnoreCase("barbarfoo", suffix)).isTrue(); + assertThat(StringUtils.endsWithIgnoreCase("barFoo", suffix)).isTrue(); + assertThat(StringUtils.endsWithIgnoreCase("barBarFoo", suffix)).isTrue(); + assertThat(StringUtils.endsWithIgnoreCase("barfoO", suffix)).isTrue(); + assertThat(StringUtils.endsWithIgnoreCase("barFOO", suffix)).isTrue(); + assertThat(StringUtils.endsWithIgnoreCase("barfOo", suffix)).isTrue(); + assertThat(StringUtils.endsWithIgnoreCase(null, suffix)).isFalse(); + assertThat(StringUtils.endsWithIgnoreCase("barfOo", null)).isFalse(); + assertThat(StringUtils.endsWithIgnoreCase("b", suffix)).isFalse(); + assertThat(StringUtils.endsWithIgnoreCase("foobar", suffix)).isFalse(); + assertThat(StringUtils.endsWithIgnoreCase("barfoobar", suffix)).isFalse(); + } + + @Test + void substringMatch() { + assertThat(StringUtils.substringMatch("foo", 0, "foo")).isTrue(); + assertThat(StringUtils.substringMatch("foo", 1, "oo")).isTrue(); + assertThat(StringUtils.substringMatch("foo", 2, "o")).isTrue(); + assertThat(StringUtils.substringMatch("foo", 0, "fOo")).isFalse(); + assertThat(StringUtils.substringMatch("foo", 1, "fOo")).isFalse(); + assertThat(StringUtils.substringMatch("foo", 2, "fOo")).isFalse(); + assertThat(StringUtils.substringMatch("foo", 3, "fOo")).isFalse(); + assertThat(StringUtils.substringMatch("foo", 1, "Oo")).isFalse(); + assertThat(StringUtils.substringMatch("foo", 2, "Oo")).isFalse(); + assertThat(StringUtils.substringMatch("foo", 3, "Oo")).isFalse(); + assertThat(StringUtils.substringMatch("foo", 2, "O")).isFalse(); + assertThat(StringUtils.substringMatch("foo", 3, "O")).isFalse(); + } + + @Test + void countOccurrencesOf() { + assertThat(StringUtils.countOccurrencesOf(null, null) == 0).as("nullx2 = 0").isTrue(); + assertThat(StringUtils.countOccurrencesOf("s", null) == 0).as("null string = 0").isTrue(); + assertThat(StringUtils.countOccurrencesOf(null, "s") == 0).as("null substring = 0").isTrue(); + String s = "erowoiueoiur"; + assertThat(StringUtils.countOccurrencesOf(s, "WERWER") == 0).as("not found = 0").isTrue(); + assertThat(StringUtils.countOccurrencesOf(s, "x") == 0).as("not found char = 0").isTrue(); + assertThat(StringUtils.countOccurrencesOf(s, " ") == 0).as("not found ws = 0").isTrue(); + assertThat(StringUtils.countOccurrencesOf(s, "") == 0).as("not found empty string = 0").isTrue(); + assertThat(StringUtils.countOccurrencesOf(s, "e") == 2).as("found char=2").isTrue(); + assertThat(StringUtils.countOccurrencesOf(s, "oi") == 2).as("found substring=2").isTrue(); + assertThat(StringUtils.countOccurrencesOf(s, "oiu") == 2).as("found substring=2").isTrue(); + assertThat(StringUtils.countOccurrencesOf(s, "oiur") == 1).as("found substring=3").isTrue(); + assertThat(StringUtils.countOccurrencesOf(s, "r") == 2).as("test last").isTrue(); + } + + @Test + void replace() { + String inString = "a6AazAaa77abaa"; + String oldPattern = "aa"; + String newPattern = "foo"; + + // Simple replace + String s = StringUtils.replace(inString, oldPattern, newPattern); + assertThat(s.equals("a6AazAfoo77abfoo")).as("Replace 1 worked").isTrue(); + + // Non match: no change + s = StringUtils.replace(inString, "qwoeiruqopwieurpoqwieur", newPattern); + assertThat(s).as("Replace non-matched is returned as-is").isSameAs(inString); + + // Null new pattern: should ignore + s = StringUtils.replace(inString, oldPattern, null); + assertThat(s).as("Replace non-matched is returned as-is").isSameAs(inString); + + // Null old pattern: should ignore + s = StringUtils.replace(inString, null, newPattern); + assertThat(s).as("Replace non-matched is returned as-is").isSameAs(inString); + } + + @Test + void delete() { + String inString = "The quick brown fox jumped over the lazy dog"; + + String noThe = StringUtils.delete(inString, "the"); + assertThat(noThe.equals("The quick brown fox jumped over lazy dog")).as("Result has no the [" + noThe + "]").isTrue(); + + String nohe = StringUtils.delete(inString, "he"); + assertThat(nohe.equals("T quick brown fox jumped over t lazy dog")).as("Result has no he [" + nohe + "]").isTrue(); + + String nosp = StringUtils.delete(inString, " "); + assertThat(nosp.equals("Thequickbrownfoxjumpedoverthelazydog")).as("Result has no spaces").isTrue(); + + String killEnd = StringUtils.delete(inString, "dog"); + assertThat(killEnd.equals("The quick brown fox jumped over the lazy ")).as("Result has no dog").isTrue(); + + String mismatch = StringUtils.delete(inString, "dxxcxcxog"); + assertThat(mismatch.equals(inString)).as("Result is unchanged").isTrue(); + + String nochange = StringUtils.delete(inString, ""); + assertThat(nochange.equals(inString)).as("Result is unchanged").isTrue(); + } + + @Test + void deleteAny() { + String inString = "Able was I ere I saw Elba"; + + String res = StringUtils.deleteAny(inString, "I"); + assertThat(res).as("Result has no 'I'").isEqualTo("Able was ere saw Elba"); + + res = StringUtils.deleteAny(inString, "AeEba!"); + assertThat(res).as("Result has no 'AeEba!'").isEqualTo("l ws I r I sw l"); + + res = StringUtils.deleteAny(inString, "#@$#$^"); + assertThat(res).as("Result is unchanged").isEqualTo(inString); + } + + @Test + void deleteAnyWhitespace() { + String whitespace = "This is\n\n\n \t a messagy string with whitespace\n"; + assertThat(whitespace).as("Has CR").contains("\n"); + assertThat(whitespace).as("Has tab").contains("\t"); + assertThat(whitespace).as("Has space").contains(" "); + + String cleaned = StringUtils.deleteAny(whitespace, "\n\t "); + assertThat(cleaned).as("Has no CR").doesNotContain("\n"); + assertThat(cleaned).as("Has no tab").doesNotContain("\t"); + assertThat(cleaned).as("Has no space").doesNotContain(" "); + assertThat(cleaned.length()).as("Still has chars").isGreaterThan(10); + } + + @Test + void quote() { + assertThat(StringUtils.quote("myString")).isEqualTo("'myString'"); + assertThat(StringUtils.quote("")).isEqualTo("''"); + assertThat(StringUtils.quote(null)).isNull(); + } + + @Test + void quoteIfString() { + assertThat(StringUtils.quoteIfString("myString")).isEqualTo("'myString'"); + assertThat(StringUtils.quoteIfString("")).isEqualTo("''"); + assertThat(StringUtils.quoteIfString(5)).isEqualTo(5); + assertThat(StringUtils.quoteIfString(null)).isNull(); + } + + @Test + void unqualify() { + String qualified = "i.am.not.unqualified"; + assertThat(StringUtils.unqualify(qualified)).isEqualTo("unqualified"); + } + + @Test + void capitalize() { + String capitalized = "i am not capitalized"; + assertThat(StringUtils.capitalize(capitalized)).isEqualTo("I am not capitalized"); + } + + @Test + void uncapitalize() { + String capitalized = "I am capitalized"; + assertThat(StringUtils.uncapitalize(capitalized)).isEqualTo("i am capitalized"); + } + + @Test + void getFilename() { + assertThat(StringUtils.getFilename(null)).isEqualTo(null); + assertThat(StringUtils.getFilename("")).isEqualTo(""); + assertThat(StringUtils.getFilename("myfile")).isEqualTo("myfile"); + assertThat(StringUtils.getFilename("mypath/myfile")).isEqualTo("myfile"); + assertThat(StringUtils.getFilename("myfile.")).isEqualTo("myfile."); + assertThat(StringUtils.getFilename("mypath/myfile.")).isEqualTo("myfile."); + assertThat(StringUtils.getFilename("myfile.txt")).isEqualTo("myfile.txt"); + assertThat(StringUtils.getFilename("mypath/myfile.txt")).isEqualTo("myfile.txt"); + } + + @Test + void getFilenameExtension() { + assertThat(StringUtils.getFilenameExtension(null)).isEqualTo(null); + assertThat(StringUtils.getFilenameExtension("")).isEqualTo(null); + assertThat(StringUtils.getFilenameExtension("myfile")).isEqualTo(null); + assertThat(StringUtils.getFilenameExtension("myPath/myfile")).isEqualTo(null); + assertThat(StringUtils.getFilenameExtension("/home/user/.m2/settings/myfile")).isEqualTo(null); + assertThat(StringUtils.getFilenameExtension("myfile.")).isEqualTo(""); + assertThat(StringUtils.getFilenameExtension("myPath/myfile.")).isEqualTo(""); + assertThat(StringUtils.getFilenameExtension("myfile.txt")).isEqualTo("txt"); + assertThat(StringUtils.getFilenameExtension("mypath/myfile.txt")).isEqualTo("txt"); + assertThat(StringUtils.getFilenameExtension("/home/user/.m2/settings/myfile.txt")).isEqualTo("txt"); + } + + @Test + void stripFilenameExtension() { + assertThat(StringUtils.stripFilenameExtension("")).isEqualTo(""); + assertThat(StringUtils.stripFilenameExtension("myfile")).isEqualTo("myfile"); + assertThat(StringUtils.stripFilenameExtension("myfile.")).isEqualTo("myfile"); + assertThat(StringUtils.stripFilenameExtension("myfile.txt")).isEqualTo("myfile"); + assertThat(StringUtils.stripFilenameExtension("mypath/myfile")).isEqualTo("mypath/myfile"); + assertThat(StringUtils.stripFilenameExtension("mypath/myfile.")).isEqualTo("mypath/myfile"); + assertThat(StringUtils.stripFilenameExtension("mypath/myfile.txt")).isEqualTo("mypath/myfile"); + assertThat(StringUtils.stripFilenameExtension("/home/user/.m2/settings/myfile")).isEqualTo("/home/user/.m2/settings/myfile"); + assertThat(StringUtils.stripFilenameExtension("/home/user/.m2/settings/myfile.")).isEqualTo("/home/user/.m2/settings/myfile"); + assertThat(StringUtils.stripFilenameExtension("/home/user/.m2/settings/myfile.txt")).isEqualTo("/home/user/.m2/settings/myfile"); + } + + @Test + void cleanPath() { + assertThat(StringUtils.cleanPath("mypath/myfile")).isEqualTo("mypath/myfile"); + assertThat(StringUtils.cleanPath("mypath\\myfile")).isEqualTo("mypath/myfile"); + assertThat(StringUtils.cleanPath("mypath/../mypath/myfile")).isEqualTo("mypath/myfile"); + assertThat(StringUtils.cleanPath("mypath/myfile/../../mypath/myfile")).isEqualTo("mypath/myfile"); + assertThat(StringUtils.cleanPath("../mypath/myfile")).isEqualTo("../mypath/myfile"); + assertThat(StringUtils.cleanPath("../mypath/../mypath/myfile")).isEqualTo("../mypath/myfile"); + assertThat(StringUtils.cleanPath("mypath/../../mypath/myfile")).isEqualTo("../mypath/myfile"); + assertThat(StringUtils.cleanPath("/../mypath/myfile")).isEqualTo("/../mypath/myfile"); + assertThat(StringUtils.cleanPath("/a/:b/../../mypath/myfile")).isEqualTo("/mypath/myfile"); + assertThat(StringUtils.cleanPath("/")).isEqualTo("/"); + assertThat(StringUtils.cleanPath("/mypath/../")).isEqualTo("/"); + assertThat(StringUtils.cleanPath("mypath/..")).isEqualTo(""); + assertThat(StringUtils.cleanPath("mypath/../.")).isEqualTo(""); + assertThat(StringUtils.cleanPath("mypath/../")).isEqualTo("./"); + assertThat(StringUtils.cleanPath("././")).isEqualTo("./"); + assertThat(StringUtils.cleanPath("./")).isEqualTo("./"); + assertThat(StringUtils.cleanPath("../")).isEqualTo("../"); + assertThat(StringUtils.cleanPath("./../")).isEqualTo("../"); + assertThat(StringUtils.cleanPath(".././")).isEqualTo("../"); + assertThat(StringUtils.cleanPath("file:/")).isEqualTo("file:/"); + assertThat(StringUtils.cleanPath("file:/mypath/../")).isEqualTo("file:/"); + assertThat(StringUtils.cleanPath("file:mypath/..")).isEqualTo("file:"); + assertThat(StringUtils.cleanPath("file:mypath/../.")).isEqualTo("file:"); + assertThat(StringUtils.cleanPath("file:mypath/../")).isEqualTo("file:./"); + assertThat(StringUtils.cleanPath("file:././")).isEqualTo("file:./"); + assertThat(StringUtils.cleanPath("file:./")).isEqualTo("file:./"); + assertThat(StringUtils.cleanPath("file:../")).isEqualTo("file:../"); + assertThat(StringUtils.cleanPath("file:./../")).isEqualTo("file:../"); + assertThat(StringUtils.cleanPath("file:.././")).isEqualTo("file:../"); + assertThat(StringUtils.cleanPath("file:/mypath/spring.factories")).isEqualTo("file:/mypath/spring.factories"); + assertThat(StringUtils.cleanPath("file:///c:/some/../path/the%20file.txt")).isEqualTo("file:///c:/path/the%20file.txt"); + } + + @Test + void pathEquals() { + assertThat(StringUtils.pathEquals("/dummy1/dummy2/dummy3", "/dummy1/dummy2/dummy3")).as("Must be true for the same strings").isTrue(); + assertThat(StringUtils.pathEquals("C:\\dummy1\\dummy2\\dummy3", "C:\\dummy1\\dummy2\\dummy3")).as("Must be true for the same win strings").isTrue(); + assertThat(StringUtils.pathEquals("/dummy1/bin/../dummy2/dummy3", "/dummy1/dummy2/dummy3")).as("Must be true for one top path on 1").isTrue(); + assertThat(StringUtils.pathEquals("C:\\dummy1\\dummy2\\dummy3", "C:\\dummy1\\bin\\..\\dummy2\\dummy3")).as("Must be true for one win top path on 2").isTrue(); + assertThat(StringUtils.pathEquals("/dummy1/bin/../dummy2/bin/../dummy3", "/dummy1/dummy2/dummy3")).as("Must be true for two top paths on 1").isTrue(); + assertThat(StringUtils.pathEquals("C:\\dummy1\\dummy2\\dummy3", "C:\\dummy1\\bin\\..\\dummy2\\bin\\..\\dummy3")).as("Must be true for two win top paths on 2").isTrue(); + assertThat(StringUtils.pathEquals("/dummy1/bin/tmp/../../dummy2/dummy3", "/dummy1/dummy2/dummy3")).as("Must be true for double top paths on 1").isTrue(); + assertThat(StringUtils.pathEquals("/dummy1/dummy2/dummy3", "/dummy1/dum/dum/../../dummy2/dummy3")).as("Must be true for double top paths on 2 with similarity").isTrue(); + assertThat(StringUtils.pathEquals("./dummy1/dummy2/dummy3", "dummy1/dum/./dum/../../dummy2/dummy3")).as("Must be true for current paths").isTrue(); + assertThat(StringUtils.pathEquals("./dummy1/dummy2/dummy3", "/dummy1/dum/./dum/../../dummy2/dummy3")).as("Must be false for relative/absolute paths").isFalse(); + assertThat(StringUtils.pathEquals("/dummy1/dummy2/dummy3", "/dummy1/dummy4/dummy3")).as("Must be false for different strings").isFalse(); + assertThat(StringUtils.pathEquals("/dummy1/bin/tmp/../dummy2/dummy3", "/dummy1/dummy2/dummy3")).as("Must be false for one false path on 1").isFalse(); + assertThat(StringUtils.pathEquals("C:\\dummy1\\dummy2\\dummy3", "C:\\dummy1\\bin\\tmp\\..\\dummy2\\dummy3")).as("Must be false for one false win top path on 2").isFalse(); + assertThat(StringUtils.pathEquals("/dummy1/bin/../dummy2/dummy3", "/dummy1/dummy2/dummy4")).as("Must be false for top path on 1 + difference").isFalse(); + } + + @Test + void concatenateStringArrays() { + String[] input1 = new String[] {"myString2"}; + String[] input2 = new String[] {"myString1", "myString2"}; + String[] result = StringUtils.concatenateStringArrays(input1, input2); + assertThat(result.length).isEqualTo(3); + assertThat(result[0]).isEqualTo("myString2"); + assertThat(result[1]).isEqualTo("myString1"); + assertThat(result[2]).isEqualTo("myString2"); + + assertThat(StringUtils.concatenateStringArrays(input1, null)).isEqualTo(input1); + assertThat(StringUtils.concatenateStringArrays(null, input2)).isEqualTo(input2); + assertThat(StringUtils.concatenateStringArrays(null, null)).isNull(); + } + + @Test + @Deprecated + void mergeStringArrays() { + String[] input1 = new String[] {"myString2"}; + String[] input2 = new String[] {"myString1", "myString2"}; + String[] result = StringUtils.mergeStringArrays(input1, input2); + assertThat(result.length).isEqualTo(2); + assertThat(result[0]).isEqualTo("myString2"); + assertThat(result[1]).isEqualTo("myString1"); + + assertThat(StringUtils.mergeStringArrays(input1, null)).isEqualTo(input1); + assertThat(StringUtils.mergeStringArrays(null, input2)).isEqualTo(input2); + assertThat(StringUtils.mergeStringArrays(null, null)).isNull(); + } + + @Test + void sortStringArray() { + String[] input = new String[] {"myString2"}; + input = StringUtils.addStringToArray(input, "myString1"); + assertThat(input[0]).isEqualTo("myString2"); + assertThat(input[1]).isEqualTo("myString1"); + + StringUtils.sortStringArray(input); + assertThat(input[0]).isEqualTo("myString1"); + assertThat(input[1]).isEqualTo("myString2"); + } + + @Test + void removeDuplicateStrings() { + String[] input = new String[] {"myString2", "myString1", "myString2"}; + input = StringUtils.removeDuplicateStrings(input); + assertThat(input[0]).isEqualTo("myString2"); + assertThat(input[1]).isEqualTo("myString1"); + } + + @Test + void splitArrayElementsIntoProperties() { + String[] input = new String[] {"key1=value1 ", "key2 =\"value2\""}; + Properties result = StringUtils.splitArrayElementsIntoProperties(input, "="); + assertThat(result.getProperty("key1")).isEqualTo("value1"); + assertThat(result.getProperty("key2")).isEqualTo("\"value2\""); + } + + @Test + void splitArrayElementsIntoPropertiesAndDeletedChars() { + String[] input = new String[] {"key1=value1 ", "key2 =\"value2\""}; + Properties result = StringUtils.splitArrayElementsIntoProperties(input, "=", "\""); + assertThat(result.getProperty("key1")).isEqualTo("value1"); + assertThat(result.getProperty("key2")).isEqualTo("value2"); + } + + @Test + void tokenizeToStringArray() { + String[] sa = StringUtils.tokenizeToStringArray("a,b , ,c", ","); + assertThat(sa.length).isEqualTo(3); + assertThat(sa[0].equals("a") && sa[1].equals("b") && sa[2].equals("c")).as("components are correct").isTrue(); + } + + @Test + void tokenizeToStringArrayWithNotIgnoreEmptyTokens() { + String[] sa = StringUtils.tokenizeToStringArray("a,b , ,c", ",", true, false); + assertThat(sa.length).isEqualTo(4); + assertThat(sa[0].equals("a") && sa[1].equals("b") && sa[2].isEmpty() && sa[3].equals("c")).as("components are correct").isTrue(); + } + + @Test + void tokenizeToStringArrayWithNotTrimTokens() { + String[] sa = StringUtils.tokenizeToStringArray("a,b ,c", ",", false, true); + assertThat(sa.length).isEqualTo(3); + assertThat(sa[0].equals("a") && sa[1].equals("b ") && sa[2].equals("c")).as("components are correct").isTrue(); + } + + @Test + void commaDelimitedListToStringArrayWithNullProducesEmptyArray() { + String[] sa = StringUtils.commaDelimitedListToStringArray(null); + assertThat(sa != null).as("String array isn't null with null input").isTrue(); + assertThat(sa.length == 0).as("String array length == 0 with null input").isTrue(); + } + + @Test + void commaDelimitedListToStringArrayWithEmptyStringProducesEmptyArray() { + String[] sa = StringUtils.commaDelimitedListToStringArray(""); + assertThat(sa != null).as("String array isn't null with null input").isTrue(); + assertThat(sa.length == 0).as("String array length == 0 with null input").isTrue(); + } + + @Test + void delimitedListToStringArrayWithComma() { + String[] sa = StringUtils.delimitedListToStringArray("a,b", ","); + assertThat(sa.length).isEqualTo(2); + assertThat(sa[0]).isEqualTo("a"); + assertThat(sa[1]).isEqualTo("b"); + } + + @Test + void delimitedListToStringArrayWithSemicolon() { + String[] sa = StringUtils.delimitedListToStringArray("a;b", ";"); + assertThat(sa.length).isEqualTo(2); + assertThat(sa[0]).isEqualTo("a"); + assertThat(sa[1]).isEqualTo("b"); + } + + @Test + void delimitedListToStringArrayWithEmptyDelimiter() { + String[] sa = StringUtils.delimitedListToStringArray("a,b", ""); + assertThat(sa.length).isEqualTo(3); + assertThat(sa[0]).isEqualTo("a"); + assertThat(sa[1]).isEqualTo(","); + assertThat(sa[2]).isEqualTo("b"); + } + + @Test + void delimitedListToStringArrayWithNullDelimiter() { + String[] sa = StringUtils.delimitedListToStringArray("a,b", null); + assertThat(sa.length).isEqualTo(1); + assertThat(sa[0]).isEqualTo("a,b"); + } + + @Test + void commaDelimitedListToStringArrayMatchWords() { + // Could read these from files + String[] sa = new String[] {"foo", "bar", "big"}; + doTestCommaDelimitedListToStringArrayLegalMatch(sa); + doTestStringArrayReverseTransformationMatches(sa); + + sa = new String[] {"a", "b", "c"}; + doTestCommaDelimitedListToStringArrayLegalMatch(sa); + doTestStringArrayReverseTransformationMatches(sa); + + // Test same words + sa = new String[] {"AA", "AA", "AA", "AA", "AA"}; + doTestCommaDelimitedListToStringArrayLegalMatch(sa); + doTestStringArrayReverseTransformationMatches(sa); + } + + private void doTestStringArrayReverseTransformationMatches(String[] sa) { + String[] reverse = + StringUtils.commaDelimitedListToStringArray(StringUtils.arrayToCommaDelimitedString(sa)); + assertThat(Arrays.asList(reverse)).as("Reverse transformation is equal").isEqualTo(Arrays.asList(sa)); + } + + @Test + void commaDelimitedListToStringArraySingleString() { + // Could read these from files + String s = "woeirqupoiewuropqiewuorpqiwueopriquwopeiurqopwieur"; + String[] sa = StringUtils.commaDelimitedListToStringArray(s); + assertThat(sa.length == 1).as("Found one String with no delimiters").isTrue(); + assertThat(sa[0].equals(s)).as("Single array entry matches input String with no delimiters").isTrue(); + } + + @Test + void commaDelimitedListToStringArrayWithOtherPunctuation() { + // Could read these from files + String[] sa = new String[] {"xcvwert4456346&*.", "///", ".!", ".", ";"}; + doTestCommaDelimitedListToStringArrayLegalMatch(sa); + } + + /** + * We expect to see the empty Strings in the output. + */ + @Test + void commaDelimitedListToStringArrayEmptyStrings() { + // Could read these from files + String[] sa = StringUtils.commaDelimitedListToStringArray("a,,b"); + assertThat(sa.length).as("a,,b produces array length 3").isEqualTo(3); + assertThat(sa[0].equals("a") && sa[1].isEmpty() && sa[2].equals("b")).as("components are correct").isTrue(); + + sa = new String[] {"", "", "a", ""}; + doTestCommaDelimitedListToStringArrayLegalMatch(sa); + } + + private void doTestCommaDelimitedListToStringArrayLegalMatch(String[] components) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < components.length; i++) { + if (i != 0) { + sb.append(","); + } + sb.append(components[i]); + } + String[] sa = StringUtils.commaDelimitedListToStringArray(sb.toString()); + assertThat(sa != null).as("String array isn't null with legal match").isTrue(); + assertThat(sa.length).as("String array length is correct with legal match").isEqualTo(components.length); + assertThat(Arrays.equals(sa, components)).as("Output equals input").isTrue(); + } + + + @Test + void parseLocaleStringSunnyDay() { + Locale expectedLocale = Locale.UK; + Locale locale = StringUtils.parseLocaleString(expectedLocale.toString()); + assertThat(locale).as("When given a bona-fide Locale string, must not return null.").isNotNull(); + assertThat(locale).isEqualTo(expectedLocale); + } + + @Test + void parseLocaleStringWithMalformedLocaleString() { + Locale locale = StringUtils.parseLocaleString("_banjo_on_my_knee"); + assertThat(locale).as("When given a malformed Locale string, must not return null.").isNotNull(); + } + + @Test + void parseLocaleStringWithEmptyLocaleStringYieldsNullLocale() { + Locale locale = StringUtils.parseLocaleString(""); + assertThat(locale).as("When given an empty Locale string, must return null.").isNull(); + } + + @Test // SPR-8637 + void parseLocaleWithMultiSpecialCharactersInVariant() { + String variant = "proper-northern"; + String localeString = "en_GB_" + variant; + Locale locale = StringUtils.parseLocaleString(localeString); + assertThat(locale.getVariant()).as("Multi-valued variant portion of the Locale not extracted correctly.").isEqualTo(variant); + } + + @Test // SPR-3671 + void parseLocaleWithMultiValuedVariant() { + String variant = "proper_northern"; + String localeString = "en_GB_" + variant; + Locale locale = StringUtils.parseLocaleString(localeString); + assertThat(locale.getVariant()).as("Multi-valued variant portion of the Locale not extracted correctly.").isEqualTo(variant); + } + + @Test // SPR-3671 + void parseLocaleWithMultiValuedVariantUsingSpacesAsSeparators() { + String variant = "proper northern"; + String localeString = "en GB " + variant; + Locale locale = StringUtils.parseLocaleString(localeString); + assertThat(locale.getVariant()).as("Multi-valued variant portion of the Locale not extracted correctly.").isEqualTo(variant); + } + + @Test // SPR-3671 + void parseLocaleWithMultiValuedVariantUsingMixtureOfUnderscoresAndSpacesAsSeparators() { + String variant = "proper northern"; + String localeString = "en_GB_" + variant; + Locale locale = StringUtils.parseLocaleString(localeString); + assertThat(locale.getVariant()).as("Multi-valued variant portion of the Locale not extracted correctly.").isEqualTo(variant); + } + + @Test // SPR-3671 + void parseLocaleWithMultiValuedVariantUsingSpacesAsSeparatorsWithLotsOfLeadingWhitespace() { + String variant = "proper northern"; + String localeString = "en GB " + variant; // lots of whitespace + Locale locale = StringUtils.parseLocaleString(localeString); + assertThat(locale.getVariant()).as("Multi-valued variant portion of the Locale not extracted correctly.").isEqualTo(variant); + } + + @Test // SPR-3671 + void parseLocaleWithMultiValuedVariantUsingUnderscoresAsSeparatorsWithLotsOfLeadingWhitespace() { + String variant = "proper_northern"; + String localeString = "en_GB_____" + variant; // lots of underscores + Locale locale = StringUtils.parseLocaleString(localeString); + assertThat(locale.getVariant()).as("Multi-valued variant portion of the Locale not extracted correctly.").isEqualTo(variant); + } + + @Test // SPR-7779 + void parseLocaleWithInvalidCharacters() { + assertThatIllegalArgumentException().isThrownBy(() -> + StringUtils.parseLocaleString("%0D%0AContent-length:30%0D%0A%0D%0A%3Cscript%3Ealert%28123%29%3C/script%3E")); + } + + @Test // SPR-9420 + void parseLocaleWithSameLowercaseTokenForLanguageAndCountry() { + assertThat(StringUtils.parseLocaleString("tr_tr").toString()).isEqualTo("tr_TR"); + assertThat(StringUtils.parseLocaleString("bg_bg_vnt").toString()).isEqualTo("bg_BG_vnt"); + } + + @Test // SPR-11806 + void parseLocaleWithVariantContainingCountryCode() { + String variant = "GBtest"; + String localeString = "en_GB_" + variant; + Locale locale = StringUtils.parseLocaleString(localeString); + assertThat(locale.getVariant()).as("Variant containing country code not extracted correctly").isEqualTo(variant); + } + + @Test // SPR-14718, SPR-7598 + void parseJava7Variant() { + assertThat(StringUtils.parseLocaleString("sr__#LATN").toString()).isEqualTo("sr__#LATN"); + } + + @Test // SPR-16651 + void availableLocalesWithLocaleString() { + for (Locale locale : Locale.getAvailableLocales()) { + Locale parsedLocale = StringUtils.parseLocaleString(locale.toString()); + if (parsedLocale == null) { + assertThat(locale.getLanguage()).isEqualTo(""); + } + else { + assertThat(locale.toString()).isEqualTo(parsedLocale.toString()); + } + } + } + + @Test // SPR-16651 + void availableLocalesWithLanguageTag() { + for (Locale locale : Locale.getAvailableLocales()) { + Locale parsedLocale = StringUtils.parseLocale(locale.toLanguageTag()); + if (parsedLocale == null) { + assertThat(locale.getLanguage()).isEqualTo(""); + } + else { + assertThat(locale.toLanguageTag()).isEqualTo(parsedLocale.toLanguageTag()); + } + } + } + + @Test + void invalidLocaleWithLocaleString() { + assertThat(StringUtils.parseLocaleString("invalid")).isEqualTo(new Locale("invalid")); + assertThat(StringUtils.parseLocaleString("invalidvalue")).isEqualTo(new Locale("invalidvalue")); + assertThat(StringUtils.parseLocaleString("invalidvalue_foo")).isEqualTo(new Locale("invalidvalue", "foo")); + assertThat(StringUtils.parseLocaleString("")).isNull(); + } + + @Test + void invalidLocaleWithLanguageTag() { + assertThat(StringUtils.parseLocale("invalid")).isEqualTo(new Locale("invalid")); + assertThat(StringUtils.parseLocale("invalidvalue")).isEqualTo(new Locale("invalidvalue")); + assertThat(StringUtils.parseLocale("invalidvalue_foo")).isEqualTo(new Locale("invalidvalue", "foo")); + assertThat(StringUtils.parseLocale("")).isNull(); + } + + @Test + void split() { + assertThat(StringUtils.split("Hello, world", ",")).containsExactly("Hello", " world"); + assertThat(StringUtils.split(",Hello world", ",")).containsExactly("", "Hello world"); + assertThat(StringUtils.split("Hello world,", ",")).containsExactly("Hello world", ""); + assertThat(StringUtils.split("Hello, world,", ",")).containsExactly("Hello", " world,"); + } + + @Test + void splitWithEmptyStringOrNull() { + assertThat(StringUtils.split("Hello, world", "")).isNull(); + assertThat(StringUtils.split("", ",")).isNull(); + assertThat(StringUtils.split(null, ",")).isNull(); + assertThat(StringUtils.split("Hello, world", null)).isNull(); + assertThat(StringUtils.split(null, null)).isNull(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/SystemPropertyUtilsTests.java b/spring-core/src/test/java/org/springframework/util/SystemPropertyUtilsTests.java new file mode 100644 index 0000000..ce4cf42 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/SystemPropertyUtilsTests.java @@ -0,0 +1,139 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Rob Harrop + * @author Juergen Hoeller + */ +class SystemPropertyUtilsTests { + + @Test + void replaceFromSystemProperty() { + System.setProperty("test.prop", "bar"); + try { + String resolved = SystemPropertyUtils.resolvePlaceholders("${test.prop}"); + assertThat(resolved).isEqualTo("bar"); + } + finally { + System.getProperties().remove("test.prop"); + } + } + + @Test + void replaceFromSystemPropertyWithDefault() { + System.setProperty("test.prop", "bar"); + try { + String resolved = SystemPropertyUtils.resolvePlaceholders("${test.prop:foo}"); + assertThat(resolved).isEqualTo("bar"); + } + finally { + System.getProperties().remove("test.prop"); + } + } + + @Test + void replaceFromSystemPropertyWithExpressionDefault() { + System.setProperty("test.prop", "bar"); + try { + String resolved = SystemPropertyUtils.resolvePlaceholders("${test.prop:#{foo.bar}}"); + assertThat(resolved).isEqualTo("bar"); + } + finally { + System.getProperties().remove("test.prop"); + } + } + + @Test + void replaceFromSystemPropertyWithExpressionContainingDefault() { + System.setProperty("test.prop", "bar"); + try { + String resolved = SystemPropertyUtils.resolvePlaceholders("${test.prop:Y#{foo.bar}X}"); + assertThat(resolved).isEqualTo("bar"); + } + finally { + System.getProperties().remove("test.prop"); + } + } + + @Test + void replaceWithDefault() { + String resolved = SystemPropertyUtils.resolvePlaceholders("${test.prop:foo}"); + assertThat(resolved).isEqualTo("foo"); + } + + @Test + void replaceWithExpressionDefault() { + String resolved = SystemPropertyUtils.resolvePlaceholders("${test.prop:#{foo.bar}}"); + assertThat(resolved).isEqualTo("#{foo.bar}"); + } + + @Test + void replaceWithExpressionContainingDefault() { + String resolved = SystemPropertyUtils.resolvePlaceholders("${test.prop:Y#{foo.bar}X}"); + assertThat(resolved).isEqualTo("Y#{foo.bar}X"); + } + + @Test + void replaceWithNoDefault() { + assertThatIllegalArgumentException().isThrownBy(() -> + SystemPropertyUtils.resolvePlaceholders("${test.prop}")); + } + + @Test + void replaceWithNoDefaultIgnored() { + String resolved = SystemPropertyUtils.resolvePlaceholders("${test.prop}", true); + assertThat(resolved).isEqualTo("${test.prop}"); + } + + @Test + void replaceWithEmptyDefault() { + String resolved = SystemPropertyUtils.resolvePlaceholders("${test.prop:}"); + assertThat(resolved).isEqualTo(""); + } + + @Test + void recursiveFromSystemProperty() { + System.setProperty("test.prop", "foo=${bar}"); + System.setProperty("bar", "baz"); + try { + String resolved = SystemPropertyUtils.resolvePlaceholders("${test.prop}"); + assertThat(resolved).isEqualTo("foo=baz"); + } + finally { + System.getProperties().remove("test.prop"); + System.getProperties().remove("bar"); + } + } + + @Test + void replaceFromEnv() { + Map env = System.getenv(); + if (env.containsKey("PATH")) { + String text = "${PATH}"; + assertThat(SystemPropertyUtils.resolvePlaceholders(text)).isEqualTo(env.get("PATH")); + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/TypeUtilsTests.java b/spring-core/src/test/java/org/springframework/util/TypeUtilsTests.java new file mode 100644 index 0000000..221f264 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/TypeUtilsTests.java @@ -0,0 +1,127 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link TypeUtils}. + * + * @author Juergen Hoeller + * @author Chris Beams + */ +class TypeUtilsTests { + + public static Object object; + + public static String string; + + public static Integer number; + + public static List objects; + + public static List strings; + + public static List openObjects; + + public static List openNumbers; + + public static List storableObjectList; + + public static List[] array; + + public static List[] openArray; + + + @Test + void withClasses() { + assertThat(TypeUtils.isAssignable(Object.class, Object.class)).isTrue(); + assertThat(TypeUtils.isAssignable(Object.class, String.class)).isTrue(); + assertThat(TypeUtils.isAssignable(String.class, Object.class)).isFalse(); + assertThat(TypeUtils.isAssignable(List.class, List.class)).isTrue(); + assertThat(TypeUtils.isAssignable(List.class, LinkedList.class)).isTrue(); + assertThat(TypeUtils.isAssignable(List.class, Collection.class)).isFalse(); + assertThat(TypeUtils.isAssignable(List.class, HashSet.class)).isFalse(); + } + + @Test + void withParameterizedTypes() throws Exception { + Type objectsType = getClass().getField("objects").getGenericType(); + Type openObjectsType = getClass().getField("openObjects").getGenericType(); + Type stringsType = getClass().getField("strings").getGenericType(); + assertThat(TypeUtils.isAssignable(Object.class, objectsType)).isTrue(); + assertThat(TypeUtils.isAssignable(Object.class, openObjectsType)).isTrue(); + assertThat(TypeUtils.isAssignable(Object.class, stringsType)).isTrue(); + assertThat(TypeUtils.isAssignable(List.class, objectsType)).isTrue(); + assertThat(TypeUtils.isAssignable(List.class, openObjectsType)).isTrue(); + assertThat(TypeUtils.isAssignable(List.class, stringsType)).isTrue(); + assertThat(TypeUtils.isAssignable(objectsType, List.class)).isTrue(); + assertThat(TypeUtils.isAssignable(openObjectsType, List.class)).isTrue(); + assertThat(TypeUtils.isAssignable(stringsType, List.class)).isTrue(); + assertThat(TypeUtils.isAssignable(objectsType, objectsType)).isTrue(); + assertThat(TypeUtils.isAssignable(openObjectsType, openObjectsType)).isTrue(); + assertThat(TypeUtils.isAssignable(stringsType, stringsType)).isTrue(); + assertThat(TypeUtils.isAssignable(openObjectsType, objectsType)).isTrue(); + assertThat(TypeUtils.isAssignable(openObjectsType, stringsType)).isTrue(); + assertThat(TypeUtils.isAssignable(stringsType, objectsType)).isFalse(); + assertThat(TypeUtils.isAssignable(objectsType, stringsType)).isFalse(); + } + + @Test + void withWildcardTypes() throws Exception { + ParameterizedType openObjectsType = (ParameterizedType) getClass().getField("openObjects").getGenericType(); + ParameterizedType openNumbersType = (ParameterizedType) getClass().getField("openNumbers").getGenericType(); + Type storableObjectListType = getClass().getField("storableObjectList").getGenericType(); + + Type objectType = getClass().getField("object").getGenericType(); + Type numberType = getClass().getField("number").getGenericType(); + Type stringType = getClass().getField("string").getGenericType(); + + Type openWildcard = openObjectsType.getActualTypeArguments()[0]; // '?' + Type openNumbersWildcard = openNumbersType.getActualTypeArguments()[0]; // '? extends number' + + assertThat(TypeUtils.isAssignable(openWildcard, objectType)).isTrue(); + assertThat(TypeUtils.isAssignable(openNumbersWildcard, numberType)).isTrue(); + assertThat(TypeUtils.isAssignable(openNumbersWildcard, stringType)).isFalse(); + assertThat(TypeUtils.isAssignable(storableObjectListType, openObjectsType)).isFalse(); + } + + @Test + void withGenericArrayTypes() throws Exception { + Type arrayType = getClass().getField("array").getGenericType(); + Type openArrayType = getClass().getField("openArray").getGenericType(); + assertThat(TypeUtils.isAssignable(Object.class, arrayType)).isTrue(); + assertThat(TypeUtils.isAssignable(Object.class, openArrayType)).isTrue(); + assertThat(TypeUtils.isAssignable(List[].class, arrayType)).isTrue(); + assertThat(TypeUtils.isAssignable(List[].class, openArrayType)).isTrue(); + assertThat(TypeUtils.isAssignable(arrayType, List[].class)).isTrue(); + assertThat(TypeUtils.isAssignable(openArrayType, List[].class)).isTrue(); + assertThat(TypeUtils.isAssignable(arrayType, arrayType)).isTrue(); + assertThat(TypeUtils.isAssignable(openArrayType, openArrayType)).isTrue(); + assertThat(TypeUtils.isAssignable(openArrayType, arrayType)).isTrue(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/comparator/BooleanComparatorTests.java b/spring-core/src/test/java/org/springframework/util/comparator/BooleanComparatorTests.java new file mode 100644 index 0000000..ae003d5 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/comparator/BooleanComparatorTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.comparator; + +import java.util.Comparator; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Tests for {@link BooleanComparator}. + * + * @author Keith Donald + * @author Chris Beams + * @author Phillip Webb + */ +class BooleanComparatorTests { + + @Test + void shouldCompareWithTrueLow() { + Comparator c = new BooleanComparator(true); + assertThat(c.compare(true, false)).isEqualTo(-1); + assertThat(c.compare(Boolean.TRUE, Boolean.TRUE)).isEqualTo(0); + } + + @Test + void shouldCompareWithTrueHigh() { + Comparator c = new BooleanComparator(false); + assertThat(c.compare(true, false)).isEqualTo(1); + assertThat(c.compare(Boolean.TRUE, Boolean.TRUE)).isEqualTo(0); + } + + @Test + void shouldCompareFromTrueLow() { + Comparator c = BooleanComparator.TRUE_LOW; + assertThat(c.compare(true, false)).isEqualTo(-1); + assertThat(c.compare(Boolean.TRUE, Boolean.TRUE)).isEqualTo(0); + } + + @Test + void shouldCompareFromTrueHigh() { + Comparator c = BooleanComparator.TRUE_HIGH; + assertThat(c.compare(true, false)).isEqualTo(1); + assertThat(c.compare(Boolean.TRUE, Boolean.TRUE)).isEqualTo(0); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/comparator/ComparableComparatorTests.java b/spring-core/src/test/java/org/springframework/util/comparator/ComparableComparatorTests.java new file mode 100644 index 0000000..2a4ef7e --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/comparator/ComparableComparatorTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.comparator; + +import java.util.Comparator; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link ComparableComparator}. + * + * @author Keith Donald + * @author Chris Beams + * @author Phillip Webb + */ +class ComparableComparatorTests { + + @Test + void comparableComparator() { + Comparator c = new ComparableComparator<>(); + String s1 = "abc"; + String s2 = "cde"; + assertThat(c.compare(s1, s2) < 0).isTrue(); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + void shouldNeedComparable() { + Comparator c = new ComparableComparator(); + Object o1 = new Object(); + Object o2 = new Object(); + assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> + c.compare(o1, o2)); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/comparator/CompoundComparatorTests.java b/spring-core/src/test/java/org/springframework/util/comparator/CompoundComparatorTests.java new file mode 100644 index 0000000..bf60e2d --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/comparator/CompoundComparatorTests.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.comparator; + +import java.util.Comparator; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Test for {@link CompoundComparator}. + * + * @author Keith Donald + * @author Chris Beams + * @author Phillip Webb + */ +@Deprecated +class CompoundComparatorTests { + + @Test + void shouldNeedAtLeastOneComparator() { + Comparator c = new CompoundComparator<>(); + assertThatIllegalStateException().isThrownBy(() -> + c.compare("foo", "bar")); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/comparator/InstanceComparatorTests.java b/spring-core/src/test/java/org/springframework/util/comparator/InstanceComparatorTests.java new file mode 100644 index 0000000..fc0f5e2 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/comparator/InstanceComparatorTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.comparator; + +import java.util.Comparator; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Tests for {@link InstanceComparator}. + * + * @author Phillip Webb + */ +class InstanceComparatorTests { + + private C1 c1 = new C1(); + + private C2 c2 = new C2(); + + private C3 c3 = new C3(); + + private C4 c4 = new C4(); + + @Test + void shouldCompareClasses() throws Exception { + Comparator comparator = new InstanceComparator<>(C1.class, C2.class); + assertThat(comparator.compare(c1, c1)).isEqualTo(0); + assertThat(comparator.compare(c1, c2)).isEqualTo(-1); + assertThat(comparator.compare(c2, c1)).isEqualTo(1); + assertThat(comparator.compare(c2, c3)).isEqualTo(-1); + assertThat(comparator.compare(c2, c4)).isEqualTo(-1); + assertThat(comparator.compare(c3, c4)).isEqualTo(0); + } + + @Test + void shouldCompareInterfaces() throws Exception { + Comparator comparator = new InstanceComparator<>(I1.class, I2.class); + assertThat(comparator.compare(c1, c1)).isEqualTo(0); + assertThat(comparator.compare(c1, c2)).isEqualTo(0); + assertThat(comparator.compare(c2, c1)).isEqualTo(0); + assertThat(comparator.compare(c1, c3)).isEqualTo(-1); + assertThat(comparator.compare(c3, c1)).isEqualTo(1); + assertThat(comparator.compare(c3, c4)).isEqualTo(0); + } + + @Test + void shouldCompareMix() throws Exception { + Comparator comparator = new InstanceComparator<>(I1.class, C3.class); + assertThat(comparator.compare(c1, c1)).isEqualTo(0); + assertThat(comparator.compare(c3, c4)).isEqualTo(-1); + assertThat(comparator.compare(c3, null)).isEqualTo(-1); + assertThat(comparator.compare(c4, null)).isEqualTo(0); + } + + private static interface I1 { + + } + + private static interface I2 { + + } + + private static class C1 implements I1 { + } + + private static class C2 implements I1 { + } + + private static class C3 implements I2 { + } + + private static class C4 implements I2 { + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/comparator/InvertibleComparatorTests.java b/spring-core/src/test/java/org/springframework/util/comparator/InvertibleComparatorTests.java new file mode 100644 index 0000000..df3f1a2 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/comparator/InvertibleComparatorTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.comparator; + +import java.util.Comparator; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + + +/** + * Tests for {@link InvertibleComparator}. + * + * @author Keith Donald + * @author Chris Beams + * @author Phillip Webb + */ +@Deprecated +class InvertibleComparatorTests { + + private final Comparator comparator = new ComparableComparator<>(); + + + @Test + void shouldNeedComparator() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new InvertibleComparator<>(null)); + } + + @Test + void shouldNeedComparatorWithAscending() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + new InvertibleComparator<>(null, true)); + } + + @Test + void shouldDefaultToAscending() throws Exception { + InvertibleComparator invertibleComparator = new InvertibleComparator<>(comparator); + assertThat(invertibleComparator.isAscending()).isTrue(); + assertThat(invertibleComparator.compare(1, 2)).isEqualTo(-1); + } + + @Test + void shouldInvert() throws Exception { + InvertibleComparator invertibleComparator = new InvertibleComparator<>(comparator); + assertThat(invertibleComparator.isAscending()).isTrue(); + assertThat(invertibleComparator.compare(1, 2)).isEqualTo(-1); + invertibleComparator.invertOrder(); + assertThat(invertibleComparator.isAscending()).isFalse(); + assertThat(invertibleComparator.compare(1, 2)).isEqualTo(1); + } + + @Test + void shouldCompareAscending() throws Exception { + InvertibleComparator invertibleComparator = new InvertibleComparator<>(comparator, true); + assertThat(invertibleComparator.compare(1, 2)).isEqualTo(-1); + } + + @Test + void shouldCompareDescending() throws Exception { + InvertibleComparator invertibleComparator = new InvertibleComparator<>(comparator, false); + assertThat(invertibleComparator.compare(1, 2)).isEqualTo(1); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/comparator/NullSafeComparatorTests.java b/spring-core/src/test/java/org/springframework/util/comparator/NullSafeComparatorTests.java new file mode 100644 index 0000000..e20f4f5 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/comparator/NullSafeComparatorTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.comparator; + +import java.util.Comparator; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link NullSafeComparator}. + * + * @author Keith Donald + * @author Chris Beams + * @author Phillip Webb + */ +class NullSafeComparatorTests { + + @SuppressWarnings("unchecked") + @Test + void shouldCompareWithNullsLow() { + Comparator c = NullSafeComparator.NULLS_LOW; + assertThat(c.compare(null, "boo") < 0).isTrue(); + } + + @SuppressWarnings("unchecked") + @Test + void shouldCompareWithNullsHigh() { + Comparator c = NullSafeComparator.NULLS_HIGH; + assertThat(c.compare(null, "boo") > 0).isTrue(); + assertThat(c.compare(null, null) == 0).isTrue(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/concurrent/FutureAdapterTests.java b/spring-core/src/test/java/org/springframework/util/concurrent/FutureAdapterTests.java new file mode 100644 index 0000000..c192888 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/concurrent/FutureAdapterTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.concurrent; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * @author Arjen Poutsma + */ +class FutureAdapterTests { + + private FutureAdapter adapter; + + private Future adaptee; + + + @BeforeEach + @SuppressWarnings("unchecked") + void setUp() { + adaptee = mock(Future.class); + adapter = new FutureAdapter(adaptee) { + @Override + protected String adapt(Integer adapteeResult) throws ExecutionException { + return adapteeResult.toString(); + } + }; + } + + @Test + void cancel() throws Exception { + given(adaptee.cancel(true)).willReturn(true); + boolean result = adapter.cancel(true); + assertThat(result).isTrue(); + } + + @Test + void isCancelled() { + given(adaptee.isCancelled()).willReturn(true); + boolean result = adapter.isCancelled(); + assertThat(result).isTrue(); + } + + @Test + void isDone() { + given(adaptee.isDone()).willReturn(true); + boolean result = adapter.isDone(); + assertThat(result).isTrue(); + } + + @Test + void get() throws Exception { + given(adaptee.get()).willReturn(42); + String result = adapter.get(); + assertThat(result).isEqualTo("42"); + } + + @Test + void getTimeOut() throws Exception { + given(adaptee.get(1, TimeUnit.SECONDS)).willReturn(42); + String result = adapter.get(1, TimeUnit.SECONDS); + assertThat(result).isEqualTo("42"); + } + + +} diff --git a/spring-core/src/test/java/org/springframework/util/concurrent/ListenableFutureTaskTests.java b/spring-core/src/test/java/org/springframework/util/concurrent/ListenableFutureTaskTests.java new file mode 100644 index 0000000..469a355 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/concurrent/ListenableFutureTaskTests.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.concurrent; + +import java.io.IOException; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +/** + * @author Arjen Poutsma + * @author Sebastien Deleuze + */ +@SuppressWarnings("unchecked") +class ListenableFutureTaskTests { + + @Test + void success() throws Exception { + final String s = "Hello World"; + Callable callable = () -> s; + + ListenableFutureTask task = new ListenableFutureTask<>(callable); + task.addCallback(new ListenableFutureCallback() { + @Override + public void onSuccess(String result) { + assertThat(result).isEqualTo(s); + } + @Override + public void onFailure(Throwable ex) { + throw new AssertionError(ex.getMessage(), ex); + } + }); + task.run(); + + assertThat(task.get()).isSameAs(s); + assertThat(task.completable().get()).isSameAs(s); + task.completable().thenAccept(v -> assertThat(v).isSameAs(s)); + } + + @Test + void failure() throws Exception { + final String s = "Hello World"; + Callable callable = () -> { + throw new IOException(s); + }; + + ListenableFutureTask task = new ListenableFutureTask<>(callable); + task.addCallback(new ListenableFutureCallback() { + @Override + public void onSuccess(String result) { + fail("onSuccess not expected"); + } + @Override + public void onFailure(Throwable ex) { + assertThat(ex.getMessage()).isEqualTo(s); + } + }); + task.run(); + + assertThatExceptionOfType(ExecutionException.class) + .isThrownBy(task::get) + .havingCause() + .withMessage(s); + assertThatExceptionOfType(ExecutionException.class) + .isThrownBy(task.completable()::get) + .havingCause() + .withMessage(s); + } + + @Test + void successWithLambdas() throws Exception { + final String s = "Hello World"; + Callable callable = () -> s; + + SuccessCallback successCallback = mock(SuccessCallback.class); + FailureCallback failureCallback = mock(FailureCallback.class); + ListenableFutureTask task = new ListenableFutureTask<>(callable); + task.addCallback(successCallback, failureCallback); + task.run(); + verify(successCallback).onSuccess(s); + verifyNoInteractions(failureCallback); + + assertThat(task.get()).isSameAs(s); + assertThat(task.completable().get()).isSameAs(s); + task.completable().thenAccept(v -> assertThat(v).isSameAs(s)); + } + + @Test + void failureWithLambdas() throws Exception { + final String s = "Hello World"; + IOException ex = new IOException(s); + Callable callable = () -> { + throw ex; + }; + + SuccessCallback successCallback = mock(SuccessCallback.class); + FailureCallback failureCallback = mock(FailureCallback.class); + ListenableFutureTask task = new ListenableFutureTask<>(callable); + task.addCallback(successCallback, failureCallback); + task.run(); + verify(failureCallback).onFailure(ex); + verifyNoInteractions(successCallback); + + assertThatExceptionOfType(ExecutionException.class).isThrownBy( + task::get) + .satisfies(e -> assertThat(e.getCause().getMessage()).isEqualTo(s)); + assertThatExceptionOfType(ExecutionException.class).isThrownBy( + task.completable()::get) + .satisfies(e -> assertThat(e.getCause().getMessage()).isEqualTo(s)); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/concurrent/MonoToListenableFutureAdapterTests.java b/spring-core/src/test/java/org/springframework/util/concurrent/MonoToListenableFutureAdapterTests.java new file mode 100644 index 0000000..bea1a0d --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/concurrent/MonoToListenableFutureAdapterTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.concurrent; + +import java.time.Duration; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link MonoToListenableFutureAdapter}. + * @author Rossen Stoyanchev + */ +class MonoToListenableFutureAdapterTests { + + @Test + void success() { + String expected = "one"; + AtomicReference actual = new AtomicReference<>(); + ListenableFuture future = new MonoToListenableFutureAdapter<>(Mono.just(expected)); + future.addCallback(actual::set, actual::set); + + assertThat(actual.get()).isEqualTo(expected); + } + + @Test + void failure() { + Throwable expected = new IllegalStateException("oops"); + AtomicReference actual = new AtomicReference<>(); + ListenableFuture future = new MonoToListenableFutureAdapter<>(Mono.error(expected)); + future.addCallback(actual::set, actual::set); + + assertThat(actual.get()).isEqualTo(expected); + } + + @Test + void cancellation() { + Mono mono = Mono.delay(Duration.ofSeconds(60)); + Future future = new MonoToListenableFutureAdapter<>(mono); + + assertThat(future.cancel(true)).isTrue(); + assertThat(future.isCancelled()).isTrue(); + } + + @Test + void cancellationAfterTerminated() { + Future future = new MonoToListenableFutureAdapter<>(Mono.empty()); + + assertThat(future.cancel(true)).as("Should return false if task already completed").isFalse(); + assertThat(future.isCancelled()).isFalse(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/concurrent/SettableListenableFutureTests.java b/spring-core/src/test/java/org/springframework/util/concurrent/SettableListenableFutureTests.java new file mode 100644 index 0000000..a14ecf5 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/concurrent/SettableListenableFutureTests.java @@ -0,0 +1,410 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.concurrent; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * @author Mattias Severson + * @author Juergen Hoeller + */ +class SettableListenableFutureTests { + + private final SettableListenableFuture settableListenableFuture = new SettableListenableFuture<>(); + + + @Test + void validateInitialValues() { + assertThat(settableListenableFuture.isCancelled()).isFalse(); + assertThat(settableListenableFuture.isDone()).isFalse(); + } + + @Test + void returnsSetValue() throws ExecutionException, InterruptedException { + String string = "hello"; + assertThat(settableListenableFuture.set(string)).isTrue(); + assertThat(settableListenableFuture.get()).isEqualTo(string); + assertThat(settableListenableFuture.isCancelled()).isFalse(); + assertThat(settableListenableFuture.isDone()).isTrue(); + } + + @Test + void returnsSetValueFromCompletable() throws ExecutionException, InterruptedException { + String string = "hello"; + assertThat(settableListenableFuture.set(string)).isTrue(); + Future completable = settableListenableFuture.completable(); + assertThat(completable.get()).isEqualTo(string); + assertThat(completable.isCancelled()).isFalse(); + assertThat(completable.isDone()).isTrue(); + } + + @Test + void setValueUpdatesDoneStatus() { + settableListenableFuture.set("hello"); + assertThat(settableListenableFuture.isCancelled()).isFalse(); + assertThat(settableListenableFuture.isDone()).isTrue(); + } + + @Test + void throwsSetExceptionWrappedInExecutionException() throws Exception { + Throwable exception = new RuntimeException(); + assertThat(settableListenableFuture.setException(exception)).isTrue(); + + assertThatExceptionOfType(ExecutionException.class).isThrownBy( + settableListenableFuture::get) + .withCause(exception); + + assertThat(settableListenableFuture.isCancelled()).isFalse(); + assertThat(settableListenableFuture.isDone()).isTrue(); + } + + @Test + void throwsSetExceptionWrappedInExecutionExceptionFromCompletable() throws Exception { + Throwable exception = new RuntimeException(); + assertThat(settableListenableFuture.setException(exception)).isTrue(); + Future completable = settableListenableFuture.completable(); + + assertThatExceptionOfType(ExecutionException.class).isThrownBy( + completable::get) + .withCause(exception); + + assertThat(completable.isCancelled()).isFalse(); + assertThat(completable.isDone()).isTrue(); + } + + @Test + void throwsSetErrorWrappedInExecutionException() throws Exception { + Throwable exception = new OutOfMemoryError(); + assertThat(settableListenableFuture.setException(exception)).isTrue(); + + assertThatExceptionOfType(ExecutionException.class).isThrownBy( + settableListenableFuture::get) + .withCause(exception); + + assertThat(settableListenableFuture.isCancelled()).isFalse(); + assertThat(settableListenableFuture.isDone()).isTrue(); + } + + @Test + void throwsSetErrorWrappedInExecutionExceptionFromCompletable() throws Exception { + Throwable exception = new OutOfMemoryError(); + assertThat(settableListenableFuture.setException(exception)).isTrue(); + Future completable = settableListenableFuture.completable(); + + assertThatExceptionOfType(ExecutionException.class).isThrownBy( + completable::get) + .withCause(exception); + + assertThat(completable.isCancelled()).isFalse(); + assertThat(completable.isDone()).isTrue(); + } + + @Test + void setValueTriggersCallback() { + String string = "hello"; + final String[] callbackHolder = new String[1]; + + settableListenableFuture.addCallback(new ListenableFutureCallback() { + @Override + public void onSuccess(String result) { + callbackHolder[0] = result; + } + @Override + public void onFailure(Throwable ex) { + throw new AssertionError("Expected onSuccess() to be called", ex); + } + }); + + settableListenableFuture.set(string); + assertThat(callbackHolder[0]).isEqualTo(string); + assertThat(settableListenableFuture.isCancelled()).isFalse(); + assertThat(settableListenableFuture.isDone()).isTrue(); + } + + @Test + void setValueTriggersCallbackOnlyOnce() { + String string = "hello"; + final String[] callbackHolder = new String[1]; + + settableListenableFuture.addCallback(new ListenableFutureCallback() { + @Override + public void onSuccess(String result) { + callbackHolder[0] = result; + } + @Override + public void onFailure(Throwable ex) { + throw new AssertionError("Expected onSuccess() to be called", ex); + } + }); + + settableListenableFuture.set(string); + assertThat(settableListenableFuture.set("good bye")).isFalse(); + assertThat(callbackHolder[0]).isEqualTo(string); + assertThat(settableListenableFuture.isCancelled()).isFalse(); + assertThat(settableListenableFuture.isDone()).isTrue(); + } + + @Test + void setExceptionTriggersCallback() { + Throwable exception = new RuntimeException(); + final Throwable[] callbackHolder = new Throwable[1]; + + settableListenableFuture.addCallback(new ListenableFutureCallback() { + @Override + public void onSuccess(String result) { + fail("Expected onFailure() to be called"); + } + @Override + public void onFailure(Throwable ex) { + callbackHolder[0] = ex; + } + }); + + settableListenableFuture.setException(exception); + assertThat(callbackHolder[0]).isEqualTo(exception); + assertThat(settableListenableFuture.isCancelled()).isFalse(); + assertThat(settableListenableFuture.isDone()).isTrue(); + } + + @Test + void setExceptionTriggersCallbackOnlyOnce() { + Throwable exception = new RuntimeException(); + final Throwable[] callbackHolder = new Throwable[1]; + + settableListenableFuture.addCallback(new ListenableFutureCallback() { + @Override + public void onSuccess(String result) { + fail("Expected onFailure() to be called"); + } + @Override + public void onFailure(Throwable ex) { + callbackHolder[0] = ex; + } + }); + + settableListenableFuture.setException(exception); + assertThat(settableListenableFuture.setException(new IllegalArgumentException())).isFalse(); + assertThat(callbackHolder[0]).isEqualTo(exception); + assertThat(settableListenableFuture.isCancelled()).isFalse(); + assertThat(settableListenableFuture.isDone()).isTrue(); + } + + @Test + void nullIsAcceptedAsValueToSet() throws ExecutionException, InterruptedException { + settableListenableFuture.set(null); + assertThat((Object) settableListenableFuture.get()).isNull(); + assertThat(settableListenableFuture.isCancelled()).isFalse(); + assertThat(settableListenableFuture.isDone()).isTrue(); + } + + @Test + void getWaitsForCompletion() throws ExecutionException, InterruptedException { + final String string = "hello"; + + new Thread(() -> { + try { + Thread.sleep(20L); + settableListenableFuture.set(string); + } + catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + }).start(); + + String value = settableListenableFuture.get(); + assertThat(value).isEqualTo(string); + assertThat(settableListenableFuture.isCancelled()).isFalse(); + assertThat(settableListenableFuture.isDone()).isTrue(); + } + + @Test + void getWithTimeoutThrowsTimeoutException() throws ExecutionException, InterruptedException { + assertThatExceptionOfType(TimeoutException.class).isThrownBy(() -> + settableListenableFuture.get(1L, TimeUnit.MILLISECONDS)); + } + + @Test + void getWithTimeoutWaitsForCompletion() throws ExecutionException, InterruptedException, TimeoutException { + final String string = "hello"; + + new Thread(() -> { + try { + Thread.sleep(20L); + settableListenableFuture.set(string); + } + catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + }).start(); + + String value = settableListenableFuture.get(500L, TimeUnit.MILLISECONDS); + assertThat(value).isEqualTo(string); + assertThat(settableListenableFuture.isCancelled()).isFalse(); + assertThat(settableListenableFuture.isDone()).isTrue(); + } + + @Test + void cancelPreventsValueFromBeingSet() { + assertThat(settableListenableFuture.cancel(true)).isTrue(); + assertThat(settableListenableFuture.set("hello")).isFalse(); + assertThat(settableListenableFuture.isCancelled()).isTrue(); + assertThat(settableListenableFuture.isDone()).isTrue(); + } + + @Test + void cancelSetsFutureToDone() { + settableListenableFuture.cancel(true); + assertThat(settableListenableFuture.isCancelled()).isTrue(); + assertThat(settableListenableFuture.isDone()).isTrue(); + } + + @Test + void cancelWithMayInterruptIfRunningTrueCallsOverriddenMethod() { + InterruptibleSettableListenableFuture interruptibleFuture = new InterruptibleSettableListenableFuture(); + assertThat(interruptibleFuture.cancel(true)).isTrue(); + assertThat(interruptibleFuture.calledInterruptTask()).isTrue(); + assertThat(interruptibleFuture.isCancelled()).isTrue(); + assertThat(interruptibleFuture.isDone()).isTrue(); + } + + @Test + void cancelWithMayInterruptIfRunningFalseDoesNotCallOverriddenMethod() { + InterruptibleSettableListenableFuture interruptibleFuture = new InterruptibleSettableListenableFuture(); + assertThat(interruptibleFuture.cancel(false)).isTrue(); + assertThat(interruptibleFuture.calledInterruptTask()).isFalse(); + assertThat(interruptibleFuture.isCancelled()).isTrue(); + assertThat(interruptibleFuture.isDone()).isTrue(); + } + + @Test + void setPreventsCancel() { + assertThat(settableListenableFuture.set("hello")).isTrue(); + assertThat(settableListenableFuture.cancel(true)).isFalse(); + assertThat(settableListenableFuture.isCancelled()).isFalse(); + assertThat(settableListenableFuture.isDone()).isTrue(); + } + + @Test + void cancelPreventsExceptionFromBeingSet() { + assertThat(settableListenableFuture.cancel(true)).isTrue(); + assertThat(settableListenableFuture.setException(new RuntimeException())).isFalse(); + assertThat(settableListenableFuture.isCancelled()).isTrue(); + assertThat(settableListenableFuture.isDone()).isTrue(); + } + + @Test + void setExceptionPreventsCancel() { + assertThat(settableListenableFuture.setException(new RuntimeException())).isTrue(); + assertThat(settableListenableFuture.cancel(true)).isFalse(); + assertThat(settableListenableFuture.isCancelled()).isFalse(); + assertThat(settableListenableFuture.isDone()).isTrue(); + } + + @Test + void cancelStateThrowsExceptionWhenCallingGet() throws ExecutionException, InterruptedException { + settableListenableFuture.cancel(true); + + assertThatExceptionOfType(CancellationException.class).isThrownBy(() -> + settableListenableFuture.get()); + + assertThat(settableListenableFuture.isCancelled()).isTrue(); + assertThat(settableListenableFuture.isDone()).isTrue(); + } + + @Test + void cancelStateThrowsExceptionWhenCallingGetWithTimeout() throws ExecutionException, TimeoutException, InterruptedException { + new Thread(() -> { + try { + Thread.sleep(20L); + settableListenableFuture.cancel(true); + } + catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + }).start(); + + assertThatExceptionOfType(CancellationException.class).isThrownBy(() -> + settableListenableFuture.get(500L, TimeUnit.MILLISECONDS)); + + assertThat(settableListenableFuture.isCancelled()).isTrue(); + assertThat(settableListenableFuture.isDone()).isTrue(); + } + + @Test + @SuppressWarnings({"rawtypes", "unchecked"}) + public void cancelDoesNotNotifyCallbacksOnSet() { + ListenableFutureCallback callback = mock(ListenableFutureCallback.class); + settableListenableFuture.addCallback(callback); + settableListenableFuture.cancel(true); + + verify(callback).onFailure(any(CancellationException.class)); + verifyNoMoreInteractions(callback); + + settableListenableFuture.set("hello"); + verifyNoMoreInteractions(callback); + + assertThat(settableListenableFuture.isCancelled()).isTrue(); + assertThat(settableListenableFuture.isDone()).isTrue(); + } + + @Test + @SuppressWarnings({"rawtypes", "unchecked"}) + public void cancelDoesNotNotifyCallbacksOnSetException() { + ListenableFutureCallback callback = mock(ListenableFutureCallback.class); + settableListenableFuture.addCallback(callback); + settableListenableFuture.cancel(true); + + verify(callback).onFailure(any(CancellationException.class)); + verifyNoMoreInteractions(callback); + + settableListenableFuture.setException(new RuntimeException()); + verifyNoMoreInteractions(callback); + + assertThat(settableListenableFuture.isCancelled()).isTrue(); + assertThat(settableListenableFuture.isDone()).isTrue(); + } + + + private static class InterruptibleSettableListenableFuture extends SettableListenableFuture { + + private boolean interrupted = false; + + @Override + protected void interruptTask() { + interrupted = true; + } + + boolean calledInterruptTask() { + return interrupted; + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/unit/DataSizeTests.java b/spring-core/src/test/java/org/springframework/util/unit/DataSizeTests.java new file mode 100644 index 0000000..74e830c --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/unit/DataSizeTests.java @@ -0,0 +1,218 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.unit; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link DataSize}. + * + * @author Stephane Nicoll + */ +class DataSizeTests { + + @Test + void ofBytesToBytes() { + assertThat(DataSize.ofBytes(1024).toBytes()).isEqualTo(1024); + } + + @Test + void ofBytesToKilobytes() { + assertThat(DataSize.ofBytes(1024).toKilobytes()).isEqualTo(1); + } + + @Test + void ofKilobytesToKilobytes() { + assertThat(DataSize.ofKilobytes(1024).toKilobytes()).isEqualTo(1024); + } + + @Test + void ofKilobytesToMegabytes() { + assertThat(DataSize.ofKilobytes(1024).toMegabytes()).isEqualTo(1); + } + + @Test + void ofMegabytesToMegabytes() { + assertThat(DataSize.ofMegabytes(1024).toMegabytes()).isEqualTo(1024); + } + + @Test + void ofMegabytesToGigabytes() { + assertThat(DataSize.ofMegabytes(2048).toGigabytes()).isEqualTo(2); + } + + @Test + void ofGigabytesToGigabytes() { + assertThat(DataSize.ofGigabytes(4096).toGigabytes()).isEqualTo(4096); + } + + @Test + void ofGigabytesToTerabytes() { + assertThat(DataSize.ofGigabytes(4096).toTerabytes()).isEqualTo(4); + } + + @Test + void ofTerabytesToGigabytes() { + assertThat(DataSize.ofTerabytes(1).toGigabytes()).isEqualTo(1024); + } + + @Test + void ofWithBytesUnit() { + assertThat(DataSize.of(10, DataUnit.BYTES)).isEqualTo(DataSize.ofBytes(10)); + } + + @Test + void ofWithKilobytesUnit() { + assertThat(DataSize.of(20, DataUnit.KILOBYTES)).isEqualTo(DataSize.ofKilobytes(20)); + } + + @Test + void ofWithMegabytesUnit() { + assertThat(DataSize.of(30, DataUnit.MEGABYTES)).isEqualTo(DataSize.ofMegabytes(30)); + } + + @Test + void ofWithGigabytesUnit() { + assertThat(DataSize.of(40, DataUnit.GIGABYTES)).isEqualTo(DataSize.ofGigabytes(40)); + } + + @Test + void ofWithTerabytesUnit() { + assertThat(DataSize.of(50, DataUnit.TERABYTES)).isEqualTo(DataSize.ofTerabytes(50)); + } + + @Test + void parseWithDefaultUnitUsesBytes() { + assertThat(DataSize.parse("1024")).isEqualTo(DataSize.ofKilobytes(1)); + } + + @Test + void parseNegativeNumberWithDefaultUnitUsesBytes() { + assertThat(DataSize.parse("-1")).isEqualTo(DataSize.ofBytes(-1)); + } + + @Test + void parseWithNullDefaultUnitUsesBytes() { + assertThat(DataSize.parse("1024", null)).isEqualTo(DataSize.ofKilobytes(1)); + } + + @Test + void parseNegativeNumberWithNullDefaultUnitUsesBytes() { + assertThat(DataSize.parse("-1024", null)).isEqualTo(DataSize.ofKilobytes(-1)); + } + + @Test + void parseWithCustomDefaultUnit() { + assertThat(DataSize.parse("1", DataUnit.KILOBYTES)).isEqualTo(DataSize.ofKilobytes(1)); + } + + @Test + void parseNegativeNumberWithCustomDefaultUnit() { + assertThat(DataSize.parse("-1", DataUnit.KILOBYTES)).isEqualTo(DataSize.ofKilobytes(-1)); + } + + @Test + void parseWithBytes() { + assertThat(DataSize.parse("1024B")).isEqualTo(DataSize.ofKilobytes(1)); + } + + @Test + void parseWithNegativeBytes() { + assertThat(DataSize.parse("-1024B")).isEqualTo(DataSize.ofKilobytes(-1)); + } + + @Test + void parseWithPositiveBytes() { + assertThat(DataSize.parse("+1024B")).isEqualTo(DataSize.ofKilobytes(1)); + } + + @Test + void parseWithKilobytes() { + assertThat(DataSize.parse("1KB")).isEqualTo(DataSize.ofBytes(1024)); + } + + @Test + void parseWithNegativeKilobytes() { + assertThat(DataSize.parse("-1KB")).isEqualTo(DataSize.ofBytes(-1024)); + } + + @Test + void parseWithMegabytes() { + assertThat(DataSize.parse("4MB")).isEqualTo(DataSize.ofMegabytes(4)); + } + + @Test + void parseWithNegativeMegabytes() { + assertThat(DataSize.parse("-4MB")).isEqualTo(DataSize.ofMegabytes(-4)); + } + + @Test + void parseWithGigabytes() { + assertThat(DataSize.parse("1GB")).isEqualTo(DataSize.ofMegabytes(1024)); + } + + @Test + void parseWithNegativeGigabytes() { + assertThat(DataSize.parse("-1GB")).isEqualTo(DataSize.ofMegabytes(-1024)); + } + + @Test + void parseWithTerabytes() { + assertThat(DataSize.parse("1TB")).isEqualTo(DataSize.ofTerabytes(1)); + } + + @Test + void parseWithNegativeTerabytes() { + assertThat(DataSize.parse("-1TB")).isEqualTo(DataSize.ofTerabytes(-1)); + } + + @Test + void isNegativeWithPositive() { + assertThat(DataSize.ofBytes(50).isNegative()).isFalse(); + } + + @Test + void isNegativeWithZero() { + assertThat(DataSize.ofBytes(0).isNegative()).isFalse(); + } + + @Test + void isNegativeWithNegative() { + assertThat(DataSize.ofBytes(-1).isNegative()).isTrue(); + } + + @Test + void toStringUsesBytes() { + assertThat(DataSize.ofKilobytes(1).toString()).isEqualTo("1024B"); + } + + @Test + void toStringWithNegativeBytes() { + assertThat(DataSize.ofKilobytes(-1).toString()).isEqualTo("-1024B"); + } + + @Test + void parseWithUnsupportedUnit() { + assertThatIllegalArgumentException().isThrownBy(() -> + DataSize.parse("3WB")) + .withMessage("'3WB' is not a valid data size"); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/xml/AbstractStaxHandlerTests.java b/spring-core/src/test/java/org/springframework/util/xml/AbstractStaxHandlerTests.java new file mode 100644 index 0000000..76d5570 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/xml/AbstractStaxHandlerTests.java @@ -0,0 +1,147 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import java.io.StringReader; +import java.io.StringWriter; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.transform.Result; +import javax.xml.transform.dom.DOMResult; +import javax.xml.transform.stream.StreamResult; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.xml.sax.InputSource; +import org.xml.sax.XMLReader; +import org.xmlunit.util.Predicate; + +import org.springframework.core.testfixture.xml.XmlContent; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + * @author Sam Brannen + */ +abstract class AbstractStaxHandlerTests { + + private static final String COMPLEX_XML = + "" + + "" + + "characters " + + "" + + ""; + + private static final String SIMPLE_XML = "" + + "content" + + ""; + + private static final Predicate nodeFilter = (n -> n.getNodeType() != Node.COMMENT_NODE && + n.getNodeType() != Node.DOCUMENT_TYPE_NODE && n.getNodeType() != Node.PROCESSING_INSTRUCTION_NODE); + + + private XMLReader xmlReader; + + + @BeforeEach + @SuppressWarnings("deprecation") // on JDK 9 + void createXMLReader() throws Exception { + xmlReader = org.xml.sax.helpers.XMLReaderFactory.createXMLReader(); + xmlReader.setEntityResolver((publicId, systemId) -> new InputSource(new StringReader(""))); + } + + + @Test + void noNamespacePrefixes() throws Exception { + StringWriter stringWriter = new StringWriter(); + AbstractStaxHandler handler = createStaxHandler(new StreamResult(stringWriter)); + xmlReader.setContentHandler(handler); + xmlReader.setProperty("http://xml.org/sax/properties/lexical-handler", handler); + + xmlReader.setFeature("http://xml.org/sax/features/namespaces", true); + xmlReader.setFeature("http://xml.org/sax/features/namespace-prefixes", false); + + xmlReader.parse(new InputSource(new StringReader(COMPLEX_XML))); + + assertThat(XmlContent.from(stringWriter)).isSimilarTo(COMPLEX_XML, nodeFilter); + } + + @Test + void namespacePrefixes() throws Exception { + StringWriter stringWriter = new StringWriter(); + AbstractStaxHandler handler = createStaxHandler(new StreamResult(stringWriter)); + xmlReader.setContentHandler(handler); + xmlReader.setProperty("http://xml.org/sax/properties/lexical-handler", handler); + + xmlReader.setFeature("http://xml.org/sax/features/namespaces", true); + xmlReader.setFeature("http://xml.org/sax/features/namespace-prefixes", true); + + xmlReader.parse(new InputSource(new StringReader(COMPLEX_XML))); + + assertThat(XmlContent.from(stringWriter)).isSimilarTo(COMPLEX_XML, nodeFilter); + } + + @Test + void noNamespacePrefixesDom() throws Exception { + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + documentBuilderFactory.setNamespaceAware(true); + DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + + Document expected = documentBuilder.parse(new InputSource(new StringReader(SIMPLE_XML))); + + Document result = documentBuilder.newDocument(); + AbstractStaxHandler handler = createStaxHandler(new DOMResult(result)); + xmlReader.setContentHandler(handler); + xmlReader.setProperty("http://xml.org/sax/properties/lexical-handler", handler); + + xmlReader.setFeature("http://xml.org/sax/features/namespaces", true); + xmlReader.setFeature("http://xml.org/sax/features/namespace-prefixes", false); + + xmlReader.parse(new InputSource(new StringReader(SIMPLE_XML))); + + assertThat(XmlContent.of(result)).isSimilarTo(expected, nodeFilter); + } + + @Test + void namespacePrefixesDom() throws Exception { + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + documentBuilderFactory.setNamespaceAware(true); + DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + + Document expected = documentBuilder.parse(new InputSource(new StringReader(SIMPLE_XML))); + + Document result = documentBuilder.newDocument(); + AbstractStaxHandler handler = createStaxHandler(new DOMResult(result)); + xmlReader.setContentHandler(handler); + xmlReader.setProperty("http://xml.org/sax/properties/lexical-handler", handler); + + xmlReader.setFeature("http://xml.org/sax/features/namespaces", true); + xmlReader.setFeature("http://xml.org/sax/features/namespace-prefixes", true); + + xmlReader.parse(new InputSource(new StringReader(SIMPLE_XML))); + + assertThat(XmlContent.of(result)).isSimilarTo(expected, nodeFilter); + } + + protected abstract AbstractStaxHandler createStaxHandler(Result result) throws XMLStreamException; + +} diff --git a/spring-core/src/test/java/org/springframework/util/xml/AbstractStaxXMLReaderTests.java b/spring-core/src/test/java/org/springframework/util/xml/AbstractStaxXMLReaderTests.java new file mode 100644 index 0000000..317a88b --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/xml/AbstractStaxXMLReaderTests.java @@ -0,0 +1,292 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMResult; +import javax.xml.transform.sax.SAXSource; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.w3c.dom.Node; +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.InputSource; +import org.xml.sax.Locator; +import org.xml.sax.XMLReader; +import org.xml.sax.ext.LexicalHandler; +import org.xml.sax.helpers.AttributesImpl; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.tests.MockitoUtils; +import org.springframework.tests.MockitoUtils.InvocationArgumentsAdapter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; + +/** + * @author Arjen Poutsma + */ +abstract class AbstractStaxXMLReaderTests { + + protected static XMLInputFactory inputFactory; + + private XMLReader standardReader; + + private ContentHandler standardContentHandler; + + + @BeforeEach + @SuppressWarnings("deprecation") // on JDK 9 + void setUp() throws Exception { + inputFactory = XMLInputFactory.newInstance(); + standardReader = org.xml.sax.helpers.XMLReaderFactory.createXMLReader(); + standardContentHandler = mockContentHandler(); + standardReader.setContentHandler(standardContentHandler); + } + + + @Test + void contentHandlerNamespacesNoPrefixes() throws Exception { + standardReader.setFeature("http://xml.org/sax/features/namespaces", true); + standardReader.setFeature("http://xml.org/sax/features/namespace-prefixes", false); + standardReader.parse(new InputSource(createTestInputStream())); + + AbstractStaxXMLReader staxXmlReader = createStaxXmlReader(createTestInputStream()); + ContentHandler contentHandler = mockContentHandler(); + staxXmlReader.setFeature("http://xml.org/sax/features/namespaces", true); + staxXmlReader.setFeature("http://xml.org/sax/features/namespace-prefixes", false); + staxXmlReader.setContentHandler(contentHandler); + staxXmlReader.parse(new InputSource()); + + verifyIdenticalInvocations(standardContentHandler, contentHandler); + } + + @Test + void contentHandlerNamespacesPrefixes() throws Exception { + standardReader.setFeature("http://xml.org/sax/features/namespaces", true); + standardReader.setFeature("http://xml.org/sax/features/namespace-prefixes", true); + standardReader.parse(new InputSource(createTestInputStream())); + + AbstractStaxXMLReader staxXmlReader = createStaxXmlReader(createTestInputStream()); + ContentHandler contentHandler = mockContentHandler(); + staxXmlReader.setFeature("http://xml.org/sax/features/namespaces", true); + staxXmlReader.setFeature("http://xml.org/sax/features/namespace-prefixes", true); + staxXmlReader.setContentHandler(contentHandler); + staxXmlReader.parse(new InputSource()); + + verifyIdenticalInvocations(standardContentHandler, contentHandler); + } + + @Test + void contentHandlerNoNamespacesPrefixes() throws Exception { + standardReader.setFeature("http://xml.org/sax/features/namespaces", false); + standardReader.setFeature("http://xml.org/sax/features/namespace-prefixes", true); + standardReader.parse(new InputSource(createTestInputStream())); + + AbstractStaxXMLReader staxXmlReader = createStaxXmlReader(createTestInputStream()); + ContentHandler contentHandler = mockContentHandler(); + staxXmlReader.setFeature("http://xml.org/sax/features/namespaces", false); + staxXmlReader.setFeature("http://xml.org/sax/features/namespace-prefixes", true); + staxXmlReader.setContentHandler(contentHandler); + staxXmlReader.parse(new InputSource()); + + verifyIdenticalInvocations(standardContentHandler, contentHandler); + } + + @Test + void whitespace() throws Exception { + String xml = " Some text "; + + Transformer transformer = TransformerFactory.newInstance().newTransformer(); + + AbstractStaxXMLReader staxXmlReader = createStaxXmlReader( + new ByteArrayInputStream(xml.getBytes("UTF-8"))); + + SAXSource source = new SAXSource(staxXmlReader, new InputSource()); + DOMResult result = new DOMResult(); + + transformer.transform(source, result); + + Node node1 = result.getNode().getFirstChild().getFirstChild(); + assertThat(node1.getTextContent()).isEqualTo(" "); + assertThat(node1.getNextSibling().getTextContent()).isEqualTo(" Some text "); + } + + @Test + void lexicalHandler() throws Exception { + Resource testLexicalHandlerXml = new ClassPathResource("testLexicalHandler.xml", getClass()); + + LexicalHandler expectedLexicalHandler = mockLexicalHandler(); + standardReader.setContentHandler(null); + standardReader.setProperty("http://xml.org/sax/properties/lexical-handler", expectedLexicalHandler); + standardReader.parse(new InputSource(testLexicalHandlerXml.getInputStream())); + inputFactory.setProperty("javax.xml.stream.isCoalescing", Boolean.FALSE); + inputFactory.setProperty("http://java.sun.com/xml/stream/properties/report-cdata-event", Boolean.TRUE); + inputFactory.setProperty("javax.xml.stream.isReplacingEntityReferences", Boolean.FALSE); + inputFactory.setProperty("javax.xml.stream.isSupportingExternalEntities", Boolean.FALSE); + + LexicalHandler actualLexicalHandler = mockLexicalHandler(); + willAnswer(invocation -> invocation.getArguments()[0] = "element"). + given(actualLexicalHandler).startDTD(anyString(), anyString(), anyString()); + AbstractStaxXMLReader staxXmlReader = createStaxXmlReader(testLexicalHandlerXml.getInputStream()); + staxXmlReader.setProperty("http://xml.org/sax/properties/lexical-handler", actualLexicalHandler); + staxXmlReader.parse(new InputSource()); + + // TODO: broken comparison since Mockito 2.2 upgrade + // verifyIdenticalInvocations(expectedLexicalHandler, actualLexicalHandler); + } + + + private LexicalHandler mockLexicalHandler() throws Exception { + LexicalHandler lexicalHandler = mock(LexicalHandler.class); + willAnswer(new CopyCharsAnswer()).given(lexicalHandler).comment(any(char[].class), anyInt(), anyInt()); + return lexicalHandler; + } + + private InputStream createTestInputStream() { + return getClass().getResourceAsStream("testContentHandler.xml"); + } + + protected final ContentHandler mockContentHandler() throws Exception { + ContentHandler contentHandler = mock(ContentHandler.class); + willAnswer(new CopyCharsAnswer()).given(contentHandler).characters(any(char[].class), anyInt(), anyInt()); + willAnswer(new CopyCharsAnswer()).given(contentHandler).ignorableWhitespace(any(char[].class), anyInt(), anyInt()); + willAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + invocation.getArguments()[3] = new AttributesImpl((Attributes) invocation.getArguments()[3]); + return null; + } + }).given(contentHandler).startElement(anyString(), anyString(), anyString(), any(Attributes.class)); + return contentHandler; + } + + protected void verifyIdenticalInvocations(T expected, T actual) { + MockitoUtils.verifySameInvocations(expected, actual, + new SkipLocatorArgumentsAdapter(), new CharArrayToStringAdapter(), new PartialAttributesAdapter()); + } + + protected abstract AbstractStaxXMLReader createStaxXmlReader(InputStream inputStream) throws XMLStreamException; + + + private static class SkipLocatorArgumentsAdapter implements InvocationArgumentsAdapter { + + @Override + public Object[] adaptArguments(Object[] arguments) { + for (int i = 0; i < arguments.length; i++) { + if (arguments[i] instanceof Locator) { + arguments[i] = null; + } + } + return arguments; + } + } + + + private static class CharArrayToStringAdapter implements InvocationArgumentsAdapter { + + @Override + public Object[] adaptArguments(Object[] arguments) { + if (arguments.length == 3 && arguments[0] instanceof char[] + && arguments[1] instanceof Integer && arguments[2] instanceof Integer) { + return new Object[] {new String((char[]) arguments[0], (Integer) arguments[1], (Integer) arguments[2])}; + } + return arguments; + } + } + + + private static class PartialAttributesAdapter implements InvocationArgumentsAdapter { + + @Override + public Object[] adaptArguments(Object[] arguments) { + for (int i = 0; i < arguments.length; i++) { + if (arguments[i] instanceof Attributes) { + arguments[i] = new PartialAttributes((Attributes) arguments[i]); + } + } + return arguments; + } + } + + + private static class CopyCharsAnswer implements Answer { + + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + char[] chars = (char[]) invocation.getArguments()[0]; + char[] copy = new char[chars.length]; + System.arraycopy(chars, 0, copy, 0, chars.length); + invocation.getArguments()[0] = copy; + return null; + } + } + + + private static class PartialAttributes { + + private final Attributes attributes; + + public PartialAttributes(Attributes attributes) { + this.attributes = attributes; + } + + @Override + public boolean equals(Object obj) { + Attributes other = ((PartialAttributes) obj).attributes; + if (this.attributes.getLength() != other.getLength()) { + return false; + } + for (int i = 0; i < other.getLength(); i++) { + boolean found = false; + for (int j = 0; j < attributes.getLength(); j++) { + if (other.getURI(i).equals(attributes.getURI(j)) + && other.getQName(i).equals(attributes.getQName(j)) + && other.getType(i).equals(attributes.getType(j)) + && other.getValue(i).equals(attributes.getValue(j))) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + return 1; + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/xml/DomContentHandlerTests.java b/spring-core/src/test/java/org/springframework/util/xml/DomContentHandlerTests.java new file mode 100644 index 0000000..40cef21 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/xml/DomContentHandlerTests.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import java.io.StringReader; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.InputSource; +import org.xml.sax.XMLReader; + +import org.springframework.core.testfixture.xml.XmlContent; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link DomContentHandler}. + */ +class DomContentHandlerTests { + + private static final String XML_1 = + "" + "" + "" + + "content" + + ""; + + private static final String XML_2_EXPECTED = + "" + "" + "" + + ""; + + private static final String XML_2_SNIPPET = + "" + ""; + + + private Document expected; + + private DomContentHandler handler; + + private Document result; + + private XMLReader xmlReader; + + private DocumentBuilder documentBuilder; + + + @BeforeEach + @SuppressWarnings("deprecation") // on JDK 9 + void setUp() throws Exception { + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + documentBuilderFactory.setNamespaceAware(true); + documentBuilder = documentBuilderFactory.newDocumentBuilder(); + result = documentBuilder.newDocument(); + xmlReader = org.xml.sax.helpers.XMLReaderFactory.createXMLReader(); + } + + + @Test + void contentHandlerDocumentNamespacePrefixes() throws Exception { + xmlReader.setFeature("http://xml.org/sax/features/namespace-prefixes", true); + handler = new DomContentHandler(result); + expected = documentBuilder.parse(new InputSource(new StringReader(XML_1))); + xmlReader.setContentHandler(handler); + xmlReader.parse(new InputSource(new StringReader(XML_1))); + assertThat(XmlContent.of(result)).as("Invalid result").isSimilarTo(expected); + } + + @Test + void contentHandlerDocumentNoNamespacePrefixes() throws Exception { + handler = new DomContentHandler(result); + expected = documentBuilder.parse(new InputSource(new StringReader(XML_1))); + xmlReader.setContentHandler(handler); + xmlReader.parse(new InputSource(new StringReader(XML_1))); + assertThat(XmlContent.of(result)).as("Invalid result").isSimilarTo(expected); + } + + @Test + void contentHandlerElement() throws Exception { + Element rootElement = result.createElementNS("namespace", "root"); + result.appendChild(rootElement); + handler = new DomContentHandler(rootElement); + expected = documentBuilder.parse(new InputSource(new StringReader(XML_2_EXPECTED))); + xmlReader.setContentHandler(handler); + xmlReader.parse(new InputSource(new StringReader(XML_2_SNIPPET))); + assertThat(XmlContent.of(result)).as("Invalid result").isSimilarTo(expected); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/xml/ListBasedXMLEventReaderTests.java b/spring-core/src/test/java/org/springframework/util/xml/ListBasedXMLEventReaderTests.java new file mode 100644 index 0000000..9ea91f2 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/xml/ListBasedXMLEventReaderTests.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import java.io.StringReader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLEventWriter; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.events.XMLEvent; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.testfixture.xml.XmlContent; + +import static javax.xml.stream.XMLStreamConstants.END_DOCUMENT; +import static javax.xml.stream.XMLStreamConstants.END_ELEMENT; +import static javax.xml.stream.XMLStreamConstants.START_DOCUMENT; +import static javax.xml.stream.XMLStreamConstants.START_ELEMENT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Arjen Poutsma + * @author Andrzej Hołowko + */ +class ListBasedXMLEventReaderTests { + + private final XMLInputFactory inputFactory = XMLInputFactory.newInstance(); + + private final XMLOutputFactory outputFactory = XMLOutputFactory.newFactory(); + + + @Test + void standard() throws Exception { + String xml = "baz"; + List events = readEvents(xml); + + ListBasedXMLEventReader reader = new ListBasedXMLEventReader(events); + + StringWriter resultWriter = new StringWriter(); + XMLEventWriter writer = this.outputFactory.createXMLEventWriter(resultWriter); + writer.add(reader); + + assertThat(XmlContent.from(resultWriter)).isSimilarTo(xml); + } + + @Test + void getElementText() throws Exception { + String xml = "baz"; + List events = readEvents(xml); + + ListBasedXMLEventReader reader = new ListBasedXMLEventReader(events); + + assertThat(reader.nextEvent().getEventType()).isEqualTo(START_DOCUMENT); + assertThat(reader.nextEvent().getEventType()).isEqualTo(START_ELEMENT); + assertThat(reader.nextEvent().getEventType()).isEqualTo(START_ELEMENT); + assertThat(reader.getElementText()).isEqualTo("baz"); + assertThat(reader.nextEvent().getEventType()).isEqualTo(END_ELEMENT); + assertThat(reader.nextEvent().getEventType()).isEqualTo(END_DOCUMENT); + } + + @Test + void getElementTextThrowsExceptionAtWrongPosition() throws Exception { + String xml = "baz"; + List events = readEvents(xml); + + ListBasedXMLEventReader reader = new ListBasedXMLEventReader(events); + + assertThat(reader.nextEvent().getEventType()).isEqualTo(START_DOCUMENT); + + assertThatExceptionOfType(XMLStreamException.class).isThrownBy( + reader::getElementText) + .withMessageStartingWith("Not at START_ELEMENT"); + } + + private List readEvents(String xml) throws XMLStreamException { + XMLEventReader reader = this.inputFactory.createXMLEventReader(new StringReader(xml)); + List events = new ArrayList<>(); + while (reader.hasNext()) { + events.add(reader.nextEvent()); + } + return events; + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/xml/SimpleNamespaceContextTests.java b/spring-core/src/test/java/org/springframework/util/xml/SimpleNamespaceContextTests.java new file mode 100644 index 0000000..0996650 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/xml/SimpleNamespaceContextTests.java @@ -0,0 +1,207 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Set; + +import javax.xml.XMLConstants; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Arjen Poutsma + * @author Leo Arnold + */ +class SimpleNamespaceContextTests { + + private final String unboundPrefix = "unbound"; + private final String prefix = "prefix"; + private final String namespaceUri = "https://Namespace-name-URI"; + private final String additionalNamespaceUri = "https://Additional-namespace-name-URI"; + private final String unboundNamespaceUri = "https://Unbound-namespace-name-URI"; + private final String defaultNamespaceUri = "https://Default-namespace-name-URI"; + + private final SimpleNamespaceContext context = new SimpleNamespaceContext(); + + + @Test + void getNamespaceURI_withNull() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + context.getNamespaceURI(null)); + } + + @Test + void getNamespaceURI() { + context.bindNamespaceUri(XMLConstants.XMLNS_ATTRIBUTE, additionalNamespaceUri); + assertThat(context.getNamespaceURI(XMLConstants.XMLNS_ATTRIBUTE)) + .as("Always returns \"http://www.w3.org/2000/xmlns/\" for \"xmlns\"") + .isEqualTo(XMLConstants.XMLNS_ATTRIBUTE_NS_URI); + context.bindNamespaceUri(XMLConstants.XML_NS_PREFIX, additionalNamespaceUri); + assertThat(context.getNamespaceURI(XMLConstants.XML_NS_PREFIX)) + .as("Always returns \"http://www.w3.org/XML/1998/namespace\" for \"xml\"") + .isEqualTo(XMLConstants.XML_NS_URI); + + assertThat(context.getNamespaceURI(unboundPrefix)) + .as("Returns \"\" for an unbound prefix") + .isEqualTo(XMLConstants.NULL_NS_URI); + context.bindNamespaceUri(prefix, namespaceUri); + assertThat(context.getNamespaceURI(prefix)) + .as("Returns the bound namespace URI for a bound prefix") + .isEqualTo(namespaceUri); + + assertThat(context.getNamespaceURI(XMLConstants.DEFAULT_NS_PREFIX)) + .as("By default returns URI \"\" for the default namespace prefix") + .isEqualTo(XMLConstants.NULL_NS_URI); + context.bindDefaultNamespaceUri(defaultNamespaceUri); + assertThat(context.getNamespaceURI(XMLConstants.DEFAULT_NS_PREFIX)) + .as("Returns the set URI for the default namespace prefix") + .isEqualTo(defaultNamespaceUri); + } + + @Test + void getPrefix_withNull() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + context.getPrefix(null)); + } + + @Test + void getPrefix() { + assertThat(context.getPrefix(XMLConstants.XMLNS_ATTRIBUTE_NS_URI)) + .as("Always returns \"xmlns\" for \"http://www.w3.org/2000/xmlns/\"") + .isEqualTo(XMLConstants.XMLNS_ATTRIBUTE); + assertThat(context.getPrefix(XMLConstants.XML_NS_URI)) + .as("Always returns \"xml\" for \"http://www.w3.org/XML/1998/namespace\"") + .isEqualTo(XMLConstants.XML_NS_PREFIX); + + assertThat(context.getPrefix(unboundNamespaceUri)).as("Returns null for an unbound namespace URI").isNull(); + context.bindNamespaceUri("prefix1", namespaceUri); + context.bindNamespaceUri("prefix2", namespaceUri); + assertThat(context.getPrefix(namespaceUri)) + .as("Returns a prefix for a bound namespace URI") + .matches(prefix -> "prefix1".equals(prefix) || "prefix2".equals(prefix)); + } + + @Test + void getPrefixes_withNull() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + context.getPrefixes(null)); + } + + @Test + void getPrefixes_IteratorIsNotModifiable() throws Exception { + context.bindNamespaceUri(prefix, namespaceUri); + Iterator iterator = context.getPrefixes(namespaceUri); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy( + iterator::remove); + } + + @Test + void getPrefixes() { + assertThat(getItemSet(context.getPrefixes(XMLConstants.XMLNS_ATTRIBUTE_NS_URI))) + .as("Returns only \"xmlns\" for \"http://www.w3.org/2000/xmlns/\"") + .containsExactly(XMLConstants.XMLNS_ATTRIBUTE); + assertThat(getItemSet(context.getPrefixes(XMLConstants.XML_NS_URI))) + .as("Returns only \"xml\" for \"http://www.w3.org/XML/1998/namespace\"") + .containsExactly(XMLConstants.XML_NS_PREFIX); + + assertThat(context.getPrefixes("unbound Namespace URI").hasNext()) + .as("Returns empty iterator for unbound prefix") + .isFalse(); + context.bindNamespaceUri("prefix1", namespaceUri); + context.bindNamespaceUri("prefix2", namespaceUri); + assertThat(getItemSet(context.getPrefixes(namespaceUri))) + .as("Returns all prefixes (and only those) bound to the namespace URI") + .containsExactlyInAnyOrder("prefix1", "prefix2"); + } + + @Test + void bindNamespaceUri_withNullNamespaceUri() { + assertThatIllegalArgumentException().isThrownBy(() -> + context.bindNamespaceUri("prefix", null)); + } + + @Test + void bindNamespaceUri_withNullPrefix() { + assertThatIllegalArgumentException().isThrownBy(() -> + context.bindNamespaceUri(null, namespaceUri)); + } + + @Test + void bindNamespaceUri() { + context.bindNamespaceUri(prefix, namespaceUri); + assertThat(context.getNamespaceURI(prefix)) + .as("The Namespace URI was bound to the prefix") + .isEqualTo(namespaceUri); + assertThat(getItemSet(context.getPrefixes(namespaceUri))) + .as("The prefix was bound to the namespace URI") + .contains(prefix); + } + + @Test + void getBoundPrefixes() { + context.bindNamespaceUri("prefix1", namespaceUri); + context.bindNamespaceUri("prefix2", namespaceUri); + context.bindNamespaceUri("prefix3", additionalNamespaceUri); + assertThat(getItemSet(context.getBoundPrefixes())) + .as("Returns all bound prefixes") + .containsExactlyInAnyOrder("prefix1", "prefix2", "prefix3"); + } + + @Test + void clear() { + context.bindNamespaceUri("prefix1", namespaceUri); + context.bindNamespaceUri("prefix2", namespaceUri); + context.bindNamespaceUri("prefix3", additionalNamespaceUri); + context.clear(); + assertThat(context.getBoundPrefixes().hasNext()).as("All bound prefixes were removed").isFalse(); + assertThat(context.getPrefixes(namespaceUri).hasNext()).as("All bound namespace URIs were removed").isFalse(); + } + + @Test + void removeBinding() { + context.removeBinding(unboundPrefix); + + context.bindNamespaceUri(prefix, namespaceUri); + context.removeBinding(prefix); + assertThat(context.getNamespaceURI(prefix)).as("Returns default namespace URI for removed prefix").isEqualTo(XMLConstants.NULL_NS_URI); + assertThat(context.getPrefix(namespaceUri)).as("#getPrefix returns null when all prefixes for a namespace URI were removed").isNull(); + assertThat(context.getPrefixes(namespaceUri).hasNext()).as("#getPrefixes returns an empty iterator when all prefixes for a namespace URI were removed").isFalse(); + + context.bindNamespaceUri("prefix1", additionalNamespaceUri); + context.bindNamespaceUri("prefix2", additionalNamespaceUri); + context.removeBinding("prefix1"); + assertThat(context.getNamespaceURI("prefix1")).as("Prefix was unbound").isEqualTo(XMLConstants.NULL_NS_URI); + assertThat(context.getPrefix(additionalNamespaceUri)).as("#getPrefix returns a bound prefix after removal of another prefix for the same namespace URI").isEqualTo("prefix2"); + assertThat(getItemSet(context.getPrefixes(additionalNamespaceUri))) + .as("Prefix was removed from namespace URI") + .containsExactly("prefix2"); + } + + + private Set getItemSet(Iterator iterator) { + Set itemSet = new LinkedHashSet<>(); + iterator.forEachRemaining(itemSet::add); + return itemSet; + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/xml/StaxEventHandlerTests.java b/spring-core/src/test/java/org/springframework/util/xml/StaxEventHandlerTests.java new file mode 100644 index 0000000..35e6120 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/xml/StaxEventHandlerTests.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import javax.xml.stream.XMLEventWriter; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.transform.Result; + +/** + * @author Arjen Poutsma + */ +class StaxEventHandlerTests extends AbstractStaxHandlerTests { + + @Override + protected AbstractStaxHandler createStaxHandler(Result result) throws XMLStreamException { + XMLOutputFactory outputFactory = XMLOutputFactory.newFactory(); + XMLEventWriter eventWriter = outputFactory.createXMLEventWriter(result); + return new StaxEventHandler(eventWriter); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/xml/StaxEventXMLReaderTests.java b/spring-core/src/test/java/org/springframework/util/xml/StaxEventXMLReaderTests.java new file mode 100644 index 0000000..6acf48e --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/xml/StaxEventXMLReaderTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import java.io.InputStream; +import java.io.StringReader; + +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; + +import org.junit.jupiter.api.Test; +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.InputSource; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +class StaxEventXMLReaderTests extends AbstractStaxXMLReaderTests { + + public static final String CONTENT = ""; + + @Override + protected AbstractStaxXMLReader createStaxXmlReader(InputStream inputStream) throws XMLStreamException { + return new StaxEventXMLReader(inputFactory.createXMLEventReader(inputStream)); + } + + @Test + void partial() throws Exception { + XMLInputFactory inputFactory = XMLInputFactory.newInstance(); + XMLEventReader eventReader = inputFactory.createXMLEventReader(new StringReader(CONTENT)); + eventReader.nextTag(); // skip to root + StaxEventXMLReader xmlReader = new StaxEventXMLReader(eventReader); + ContentHandler contentHandler = mock(ContentHandler.class); + xmlReader.setContentHandler(contentHandler); + xmlReader.parse(new InputSource()); + verify(contentHandler).startDocument(); + verify(contentHandler).startElement(eq("http://springframework.org/spring-ws"), eq("child"), eq("child"), any(Attributes.class)); + verify(contentHandler).endElement("http://springframework.org/spring-ws", "child", "child"); + verify(contentHandler).endDocument(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/xml/StaxResultTests.java b/spring-core/src/test/java/org/springframework/util/xml/StaxResultTests.java new file mode 100644 index 0000000..e284337 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/xml/StaxResultTests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; + +import javax.xml.stream.XMLEventWriter; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamWriter; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.stream.StreamSource; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.core.testfixture.xml.XmlContent; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + */ +class StaxResultTests { + + private static final String XML = ""; + + private Transformer transformer; + + private XMLOutputFactory inputFactory; + + @BeforeEach + void setUp() throws Exception { + TransformerFactory transformerFactory = TransformerFactory.newInstance(); + transformer = transformerFactory.newTransformer(); + inputFactory = XMLOutputFactory.newInstance(); + } + + @Test + void streamWriterSource() throws Exception { + StringWriter stringWriter = new StringWriter(); + XMLStreamWriter streamWriter = inputFactory.createXMLStreamWriter(stringWriter); + Reader reader = new StringReader(XML); + Source source = new StreamSource(reader); + StaxResult result = new StaxResult(streamWriter); + assertThat(result.getXMLStreamWriter()).as("Invalid streamWriter returned").isEqualTo(streamWriter); + assertThat(result.getXMLEventWriter()).as("EventWriter returned").isNull(); + transformer.transform(source, result); + assertThat(XmlContent.from(stringWriter)).as("Invalid result").isSimilarTo(XML); + } + + @Test + void eventWriterSource() throws Exception { + StringWriter stringWriter = new StringWriter(); + XMLEventWriter eventWriter = inputFactory.createXMLEventWriter(stringWriter); + Reader reader = new StringReader(XML); + Source source = new StreamSource(reader); + StaxResult result = new StaxResult(eventWriter); + assertThat(result.getXMLEventWriter()).as("Invalid eventWriter returned").isEqualTo(eventWriter); + assertThat(result.getXMLStreamWriter()).as("StreamWriter returned").isNull(); + transformer.transform(source, result); + assertThat(XmlContent.from(stringWriter)).as("Invalid result").isSimilarTo(XML); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/xml/StaxSourceTests.java b/spring-core/src/test/java/org/springframework/util/xml/StaxSourceTests.java new file mode 100644 index 0000000..dc82a03 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/xml/StaxSourceTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import java.io.StringReader; +import java.io.StringWriter; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamReader; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMResult; +import javax.xml.transform.stream.StreamResult; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.xml.sax.InputSource; + +import org.springframework.core.testfixture.xml.XmlContent; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + */ +class StaxSourceTests { + + private static final String XML = ""; + + private Transformer transformer; + + private XMLInputFactory inputFactory; + + private DocumentBuilder documentBuilder; + + @BeforeEach + void setUp() throws Exception { + TransformerFactory transformerFactory = TransformerFactory.newInstance(); + transformer = transformerFactory.newTransformer(); + inputFactory = XMLInputFactory.newInstance(); + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + documentBuilderFactory.setNamespaceAware(true); + documentBuilder = documentBuilderFactory.newDocumentBuilder(); + } + + @Test + void streamReaderSourceToStreamResult() throws Exception { + XMLStreamReader streamReader = inputFactory.createXMLStreamReader(new StringReader(XML)); + StaxSource source = new StaxSource(streamReader); + assertThat(source.getXMLStreamReader()).as("Invalid streamReader returned").isEqualTo(streamReader); + assertThat((Object) source.getXMLEventReader()).as("EventReader returned").isNull(); + StringWriter writer = new StringWriter(); + transformer.transform(source, new StreamResult(writer)); + assertThat(XmlContent.from(writer)).as("Invalid result").isSimilarTo(XML); + } + + @Test + void streamReaderSourceToDOMResult() throws Exception { + XMLStreamReader streamReader = inputFactory.createXMLStreamReader(new StringReader(XML)); + StaxSource source = new StaxSource(streamReader); + assertThat(source.getXMLStreamReader()).as("Invalid streamReader returned").isEqualTo(streamReader); + assertThat((Object) source.getXMLEventReader()).as("EventReader returned").isNull(); + + Document expected = documentBuilder.parse(new InputSource(new StringReader(XML))); + Document result = documentBuilder.newDocument(); + transformer.transform(source, new DOMResult(result)); + assertThat(XmlContent.of(result)).as("Invalid result").isSimilarTo(expected); + } + + @Test + void eventReaderSourceToStreamResult() throws Exception { + XMLEventReader eventReader = inputFactory.createXMLEventReader(new StringReader(XML)); + StaxSource source = new StaxSource(eventReader); + assertThat((Object) source.getXMLEventReader()).as("Invalid eventReader returned").isEqualTo(eventReader); + assertThat(source.getXMLStreamReader()).as("StreamReader returned").isNull(); + StringWriter writer = new StringWriter(); + transformer.transform(source, new StreamResult(writer)); + assertThat(XmlContent.from(writer)).as("Invalid result").isSimilarTo(XML); + } + + @Test + void eventReaderSourceToDOMResult() throws Exception { + XMLEventReader eventReader = inputFactory.createXMLEventReader(new StringReader(XML)); + StaxSource source = new StaxSource(eventReader); + assertThat((Object) source.getXMLEventReader()).as("Invalid eventReader returned").isEqualTo(eventReader); + assertThat(source.getXMLStreamReader()).as("StreamReader returned").isNull(); + + Document expected = documentBuilder.parse(new InputSource(new StringReader(XML))); + Document result = documentBuilder.newDocument(); + transformer.transform(source, new DOMResult(result)); + assertThat(XmlContent.of(result)).as("Invalid result").isSimilarTo(expected); + } +} diff --git a/spring-core/src/test/java/org/springframework/util/xml/StaxStreamHandlerTests.java b/spring-core/src/test/java/org/springframework/util/xml/StaxStreamHandlerTests.java new file mode 100644 index 0000000..7517382 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/xml/StaxStreamHandlerTests.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import javax.xml.transform.Result; + +/** + * @author Arjen Poutsma + */ +class StaxStreamHandlerTests extends AbstractStaxHandlerTests { + + @Override + protected AbstractStaxHandler createStaxHandler(Result result) throws XMLStreamException { + XMLOutputFactory outputFactory = XMLOutputFactory.newFactory(); + XMLStreamWriter streamWriter = outputFactory.createXMLStreamWriter(result); + return new StaxStreamHandler(streamWriter); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/xml/StaxStreamXMLReaderTests.java b/spring-core/src/test/java/org/springframework/util/xml/StaxStreamXMLReaderTests.java new file mode 100644 index 0000000..26b1367 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/xml/StaxStreamXMLReaderTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import java.io.InputStream; +import java.io.StringReader; + +import javax.xml.namespace.QName; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +import org.junit.jupiter.api.Test; +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.InputSource; +import org.xml.sax.Locator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +class StaxStreamXMLReaderTests extends AbstractStaxXMLReaderTests { + + public static final String CONTENT = ""; + + @Override + protected AbstractStaxXMLReader createStaxXmlReader(InputStream inputStream) throws XMLStreamException { + return new StaxStreamXMLReader(inputFactory.createXMLStreamReader(inputStream)); + } + + @Test + void partial() throws Exception { + XMLInputFactory inputFactory = XMLInputFactory.newInstance(); + XMLStreamReader streamReader = inputFactory.createXMLStreamReader(new StringReader(CONTENT)); + streamReader.nextTag(); // skip to root + assertThat(streamReader.getName()).as("Invalid element").isEqualTo(new QName("http://springframework.org/spring-ws", "root")); + streamReader.nextTag(); // skip to child + assertThat(streamReader.getName()).as("Invalid element").isEqualTo(new QName("http://springframework.org/spring-ws", "child")); + StaxStreamXMLReader xmlReader = new StaxStreamXMLReader(streamReader); + + ContentHandler contentHandler = mock(ContentHandler.class); + xmlReader.setContentHandler(contentHandler); + xmlReader.parse(new InputSource()); + + verify(contentHandler).setDocumentLocator(any(Locator.class)); + verify(contentHandler).startDocument(); + verify(contentHandler).startElement(eq("http://springframework.org/spring-ws"), eq("child"), eq("child"), any(Attributes.class)); + verify(contentHandler).endElement("http://springframework.org/spring-ws", "child", "child"); + verify(contentHandler).endDocument(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/xml/StaxUtilsTests.java b/spring-core/src/test/java/org/springframework/util/xml/StaxUtilsTests.java new file mode 100644 index 0000000..959bc0b --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/xml/StaxUtilsTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import java.io.StringReader; +import java.io.StringWriter; + +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamReader; +import javax.xml.stream.XMLStreamWriter; +import javax.xml.transform.Result; +import javax.xml.transform.Source; +import javax.xml.transform.dom.DOMResult; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.sax.SAXResult; +import javax.xml.transform.sax.SAXSource; +import javax.xml.transform.stax.StAXResult; +import javax.xml.transform.stax.StAXSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + */ +class StaxUtilsTests { + + @Test + void isStaxSourceInvalid() throws Exception { + assertThat(StaxUtils.isStaxSource(new DOMSource())).as("A StAX Source").isFalse(); + assertThat(StaxUtils.isStaxSource(new SAXSource())).as("A StAX Source").isFalse(); + assertThat(StaxUtils.isStaxSource(new StreamSource())).as("A StAX Source").isFalse(); + } + + @Test + void isStaxSource() throws Exception { + XMLInputFactory inputFactory = XMLInputFactory.newInstance(); + String expected = ""; + XMLStreamReader streamReader = inputFactory.createXMLStreamReader(new StringReader(expected)); + Source source = StaxUtils.createCustomStaxSource(streamReader); + + assertThat(StaxUtils.isStaxSource(source)).as("Not a StAX Source").isTrue(); + } + + @Test + void isStaxSourceJaxp14() throws Exception { + XMLInputFactory inputFactory = XMLInputFactory.newInstance(); + String expected = ""; + XMLStreamReader streamReader = inputFactory.createXMLStreamReader(new StringReader(expected)); + StAXSource source = new StAXSource(streamReader); + + assertThat(StaxUtils.isStaxSource(source)).as("Not a StAX Source").isTrue(); + } + + @Test + void isStaxResultInvalid() throws Exception { + assertThat(StaxUtils.isStaxResult(new DOMResult())).as("A StAX Result").isFalse(); + assertThat(StaxUtils.isStaxResult(new SAXResult())).as("A StAX Result").isFalse(); + assertThat(StaxUtils.isStaxResult(new StreamResult())).as("A StAX Result").isFalse(); + } + + @Test + void isStaxResult() throws Exception { + XMLOutputFactory outputFactory = XMLOutputFactory.newInstance(); + XMLStreamWriter streamWriter = outputFactory.createXMLStreamWriter(new StringWriter()); + Result result = StaxUtils.createCustomStaxResult(streamWriter); + + assertThat(StaxUtils.isStaxResult(result)).as("Not a StAX Result").isTrue(); + } + + @Test + void isStaxResultJaxp14() throws Exception { + XMLOutputFactory outputFactory = XMLOutputFactory.newInstance(); + XMLStreamWriter streamWriter = outputFactory.createXMLStreamWriter(new StringWriter()); + StAXResult result = new StAXResult(streamWriter); + + assertThat(StaxUtils.isStaxResult(result)).as("Not a StAX Result").isTrue(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/xml/TransformerUtilsTests.java b/spring-core/src/test/java/org/springframework/util/xml/TransformerUtilsTests.java new file mode 100644 index 0000000..b4de128 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/xml/TransformerUtilsTests.java @@ -0,0 +1,164 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import java.util.Properties; + +import javax.xml.transform.ErrorListener; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Result; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.URIResolver; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Unit tests for {@link TransformerUtils}. + * + * @author Rick Evans + * @author Arjen Poutsma + */ +class TransformerUtilsTests { + + @Test + void enableIndentingSunnyDay() throws Exception { + Transformer transformer = new StubTransformer(); + TransformerUtils.enableIndenting(transformer); + String indent = transformer.getOutputProperty(OutputKeys.INDENT); + assertThat(indent).isNotNull(); + assertThat(indent).isEqualTo("yes"); + String indentAmount = transformer.getOutputProperty("{http://xml.apache.org/xalan}indent-amount"); + assertThat(indentAmount).isNotNull(); + assertThat(indentAmount).isEqualTo(String.valueOf(TransformerUtils.DEFAULT_INDENT_AMOUNT)); + } + + @Test + void enableIndentingSunnyDayWithCustomKosherIndentAmount() throws Exception { + final String indentAmountProperty = "10"; + Transformer transformer = new StubTransformer(); + TransformerUtils.enableIndenting(transformer, Integer.parseInt(indentAmountProperty)); + String indent = transformer.getOutputProperty(OutputKeys.INDENT); + assertThat(indent).isNotNull(); + assertThat(indent).isEqualTo("yes"); + String indentAmount = transformer.getOutputProperty("{http://xml.apache.org/xalan}indent-amount"); + assertThat(indentAmount).isNotNull(); + assertThat(indentAmount).isEqualTo(indentAmountProperty); + } + + @Test + void disableIndentingSunnyDay() throws Exception { + Transformer transformer = new StubTransformer(); + TransformerUtils.disableIndenting(transformer); + String indent = transformer.getOutputProperty(OutputKeys.INDENT); + assertThat(indent).isNotNull(); + assertThat(indent).isEqualTo("no"); + } + + @Test + void enableIndentingWithNullTransformer() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + TransformerUtils.enableIndenting(null)); + } + + @Test + void disableIndentingWithNullTransformer() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + TransformerUtils.disableIndenting(null)); + } + + @Test + void enableIndentingWithNegativeIndentAmount() throws Exception { + assertThatIllegalArgumentException().isThrownBy(() -> + TransformerUtils.enableIndenting(new StubTransformer(), -21938)); + } + + @Test + void enableIndentingWithZeroIndentAmount() throws Exception { + TransformerUtils.enableIndenting(new StubTransformer(), 0); + } + + private static class StubTransformer extends Transformer { + + private Properties outputProperties = new Properties(); + + @Override + public void transform(Source xmlSource, Result outputTarget) throws TransformerException { + throw new UnsupportedOperationException(); + } + + @Override + public void setParameter(String name, Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public Object getParameter(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public void clearParameters() { + throw new UnsupportedOperationException(); + } + + @Override + public void setURIResolver(URIResolver resolver) { + throw new UnsupportedOperationException(); + } + + @Override + public URIResolver getURIResolver() { + throw new UnsupportedOperationException(); + } + + @Override + public void setOutputProperties(Properties oformat) { + throw new UnsupportedOperationException(); + } + + @Override + public Properties getOutputProperties() { + return this.outputProperties; + } + + @Override + public void setOutputProperty(String name, String value) throws IllegalArgumentException { + this.outputProperties.setProperty(name, value); + } + + @Override + public String getOutputProperty(String name) throws IllegalArgumentException { + return this.outputProperties.getProperty(name); + } + + @Override + public void setErrorListener(ErrorListener listener) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public ErrorListener getErrorListener() { + throw new UnsupportedOperationException(); + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/xml/XMLEventStreamReaderTests.java b/spring-core/src/test/java/org/springframework/util/xml/XMLEventStreamReaderTests.java new file mode 100644 index 0000000..633a538 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/xml/XMLEventStreamReaderTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import java.io.StringReader; +import java.io.StringWriter; + +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLInputFactory; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.stax.StAXSource; +import javax.xml.transform.stream.StreamResult; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Node; +import org.xmlunit.util.Predicate; + +import org.springframework.core.testfixture.xml.XmlContent; + +import static org.assertj.core.api.Assertions.assertThat; + +class XMLEventStreamReaderTests { + + private static final String XML = + "content" + ; + + private XMLEventStreamReader streamReader; + + @BeforeEach + void createStreamReader() throws Exception { + XMLInputFactory inputFactory = XMLInputFactory.newInstance(); + XMLEventReader eventReader = inputFactory.createXMLEventReader(new StringReader(XML)); + streamReader = new XMLEventStreamReader(eventReader); + } + + @Test + void readAll() throws Exception { + while (streamReader.hasNext()) { + streamReader.next(); + } + } + + @Test + void readCorrect() throws Exception { + Transformer transformer = TransformerFactory.newInstance().newTransformer(); + StAXSource source = new StAXSource(streamReader); + StringWriter writer = new StringWriter(); + transformer.transform(source, new StreamResult(writer)); + Predicate nodeFilter = n -> + n.getNodeType() != Node.DOCUMENT_TYPE_NODE && n.getNodeType() != Node.PROCESSING_INSTRUCTION_NODE; + assertThat(XmlContent.from(writer)).isSimilarTo(XML, nodeFilter); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/xml/XMLEventStreamWriterTests.java b/spring-core/src/test/java/org/springframework/util/xml/XMLEventStreamWriterTests.java new file mode 100644 index 0000000..f09526e --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/xml/XMLEventStreamWriterTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import java.io.StringWriter; + +import javax.xml.stream.XMLEventFactory; +import javax.xml.stream.XMLEventWriter; +import javax.xml.stream.XMLOutputFactory; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Node; +import org.xmlunit.util.Predicate; + +import org.springframework.core.testfixture.xml.XmlContent; + +import static org.assertj.core.api.Assertions.assertThat; + +class XMLEventStreamWriterTests { + + private static final String XML = + "content"; + + private XMLEventStreamWriter streamWriter; + + private StringWriter stringWriter; + + @BeforeEach + void createStreamReader() throws Exception { + stringWriter = new StringWriter(); + XMLOutputFactory outputFactory = XMLOutputFactory.newInstance(); + XMLEventWriter eventWriter = outputFactory.createXMLEventWriter(stringWriter); + streamWriter = new XMLEventStreamWriter(eventWriter, XMLEventFactory.newInstance()); + } + + @Test + void write() throws Exception { + streamWriter.writeStartDocument(); + streamWriter.writeProcessingInstruction("pi", "content"); + streamWriter.writeStartElement("namespace", "root"); + streamWriter.writeDefaultNamespace("namespace"); + streamWriter.writeStartElement("prefix", "child", "namespace2"); + streamWriter.writeNamespace("prefix", "namespace2"); + streamWriter.writeComment("comment"); + streamWriter.writeCharacters("content"); + streamWriter.writeEndElement(); + streamWriter.writeEndElement(); + streamWriter.writeEndDocument(); + + Predicate nodeFilter = n -> n.getNodeType() != Node.DOCUMENT_TYPE_NODE && n.getNodeType() != Node.PROCESSING_INSTRUCTION_NODE; + assertThat(XmlContent.from(stringWriter)).isSimilarTo(XML, nodeFilter); + } + + +} diff --git a/spring-core/src/test/java/org/springframework/util/xml/XmlValidationModeDetectorTests.java b/spring-core/src/test/java/org/springframework/util/xml/XmlValidationModeDetectorTests.java new file mode 100644 index 0000000..631a61d --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/xml/XmlValidationModeDetectorTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.util.xml; + +import java.io.InputStream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.util.xml.XmlValidationModeDetector.VALIDATION_DTD; + +/** + * Unit tests for {@link XmlValidationModeDetector}. + * + * @author Sam Brannen + * @since 5.1.10 + */ +class XmlValidationModeDetectorTests { + + private final XmlValidationModeDetector xmlValidationModeDetector = new XmlValidationModeDetector(); + + + @ParameterizedTest + @ValueSource(strings = { "dtdWithTrailingComment.xml", "dtdWithLeadingComment.xml", "dtdWithCommentOnNextLine.xml", + "dtdWithMultipleComments.xml" }) + void dtdDetection(String fileName) throws Exception { + InputStream inputStream = getClass().getResourceAsStream(fileName); + assertThat(xmlValidationModeDetector.detectValidationMode(inputStream)).isEqualTo(VALIDATION_DTD); + } + +} diff --git a/spring-core/src/test/kotlin/org/springframework/core/KotlinDefaultParameterNameDiscovererTests.kt b/spring-core/src/test/kotlin/org/springframework/core/KotlinDefaultParameterNameDiscovererTests.kt new file mode 100644 index 0000000..7fe1992 --- /dev/null +++ b/spring-core/src/test/kotlin/org/springframework/core/KotlinDefaultParameterNameDiscovererTests.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class KotlinDefaultParameterNameDiscovererTests { + + private val parameterNameDiscoverer = DefaultParameterNameDiscoverer() + + enum class MyEnum { + ONE, TWO + } + + @Test // SPR-16931 + fun getParameterNamesOnEnum() { + val constructor = MyEnum::class.java.declaredConstructors[0] + val actualParams = parameterNameDiscoverer.getParameterNames(constructor) + assertThat(actualParams!!.size).isEqualTo(2) + } +} diff --git a/spring-core/src/test/kotlin/org/springframework/core/KotlinGenericTypeResolverTests.kt b/spring-core/src/test/kotlin/org/springframework/core/KotlinGenericTypeResolverTests.kt new file mode 100644 index 0000000..9780a4f --- /dev/null +++ b/spring-core/src/test/kotlin/org/springframework/core/KotlinGenericTypeResolverTests.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.core.GenericTypeResolver.resolveReturnTypeArgument +import java.lang.reflect.Method + +/** + * Tests for Kotlin support in [GenericTypeResolver]. + * + * @author Konrad Kaminski + * @author Sebastien Deleuze + */ +class KotlinGenericTypeResolverTests { + + @Test + fun methodReturnTypes() { + assertThat(resolveReturnTypeArgument(findMethod(MyTypeWithMethods::class.java, "integer")!!, + MyInterfaceType::class.java)).isEqualTo(Integer::class.java) + assertThat(resolveReturnTypeArgument(findMethod(MyTypeWithMethods::class.java, "string")!!, + MyInterfaceType::class.java)).isEqualTo(String::class.java) + assertThat(resolveReturnTypeArgument(findMethod(MyTypeWithMethods::class.java, "raw")!!, + MyInterfaceType::class.java)).isNull() + assertThat(resolveReturnTypeArgument(findMethod(MyTypeWithMethods::class.java, "object")!!, + MyInterfaceType::class.java)).isNull() + } + + private fun findMethod(clazz: Class<*>, name: String): Method? = + clazz.methods.firstOrNull { it.name == name } + + open class MyTypeWithMethods { + suspend fun integer(): MyInterfaceType? = null + + suspend fun string(): MySimpleInterfaceType? = null + + suspend fun `object`(): Any? = null + + suspend fun raw(): MyInterfaceType<*>? = null + } + + interface MyInterfaceType + + interface MySimpleInterfaceType: MyInterfaceType + + open class MySimpleTypeWithMethods: MyTypeWithMethods() +} diff --git a/spring-core/src/test/kotlin/org/springframework/core/KotlinMethodParameterTests.kt b/spring-core/src/test/kotlin/org/springframework/core/KotlinMethodParameterTests.kt new file mode 100644 index 0000000..419698d --- /dev/null +++ b/spring-core/src/test/kotlin/org/springframework/core/KotlinMethodParameterTests.kt @@ -0,0 +1,163 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import java.lang.reflect.Method +import java.lang.reflect.TypeVariable +import kotlin.coroutines.Continuation +import kotlin.reflect.full.declaredFunctions +import kotlin.reflect.jvm.javaMethod + +/** + * Tests for Kotlin support in [MethodParameter]. + * + * @author Raman Gupta + * @author Sebastien Deleuze + * @author Juergen Hoeller + * @author Konrad Kaminski + */ +class KotlinMethodParameterTests { + + private val nullableMethod: Method = javaClass.getMethod("nullable", String::class.java) + + private val nonNullableMethod = javaClass.getMethod("nonNullable", String::class.java) + + private val innerClassConstructor = InnerClass::class.java.getConstructor(KotlinMethodParameterTests::class.java) + + private val innerClassWithParametersConstructor = InnerClassWithParameter::class.java + .getConstructor(KotlinMethodParameterTests::class.java, String::class.java, String::class.java) + + private val regularClassConstructor = RegularClass::class.java.getConstructor(String::class.java, String::class.java) + + + @Test + fun `Method parameter nullability`() { + assertThat(MethodParameter(nullableMethod, 0).isOptional).isTrue() + assertThat(MethodParameter(nonNullableMethod, 0).isOptional).isFalse() + } + + @Test + fun `Method return type nullability`() { + assertThat(MethodParameter(nullableMethod, -1).isOptional).isTrue() + assertThat(MethodParameter(nonNullableMethod, -1).isOptional).isFalse() + } + + @Test // SPR-17222 + fun `Inner class constructor`() { + assertThat(MethodParameter(innerClassConstructor, 0).isOptional).isFalse() + assertThat(MethodParameter(innerClassWithParametersConstructor, 0).isOptional).isFalse() + assertThat(MethodParameter(innerClassWithParametersConstructor, 1).isOptional).isFalse() + assertThat(MethodParameter(innerClassWithParametersConstructor, 2).isOptional).isTrue() + } + + @Test + fun `Regular class constructor`() { + assertThat(MethodParameter(regularClassConstructor, 0).isOptional).isFalse() + assertThat(MethodParameter(regularClassConstructor, 1).isOptional).isTrue() + } + + @Test + fun `Suspending function return type`() { + assertThat(returnParameterType("suspendFun")).isEqualTo(Number::class.java) + assertThat(returnGenericParameterType("suspendFun")).isEqualTo(Number::class.java) + + assertThat(returnParameterType("suspendFun2")).isEqualTo(Producer::class.java) + assertThat(returnGenericParameterTypeName("suspendFun2")).isEqualTo("org.springframework.core.Producer") + + assertThat(returnParameterType("suspendFun3")).isEqualTo(Wrapper::class.java) + assertThat(returnGenericParameterTypeName("suspendFun3")).isEqualTo("org.springframework.core.Wrapper") + + assertThat(returnParameterType("suspendFun4")).isEqualTo(Consumer::class.java) + assertThat(returnGenericParameterTypeName("suspendFun4")).isEqualTo("org.springframework.core.Consumer") + + assertThat(returnParameterType("suspendFun5")).isEqualTo(Producer::class.java) + assertThat(returnGenericParameterType("suspendFun5")).isInstanceOf(TypeVariable::class.java) + assertThat(returnGenericParameterTypeBoundName("suspendFun5")).isEqualTo("org.springframework.core.Producer") + + assertThat(returnParameterType("suspendFun6")).isEqualTo(Wrapper::class.java) + assertThat(returnGenericParameterType("suspendFun6")).isInstanceOf(TypeVariable::class.java) + assertThat(returnGenericParameterTypeBoundName("suspendFun6")).isEqualTo("org.springframework.core.Wrapper") + + assertThat(returnParameterType("suspendFun7")).isEqualTo(Consumer::class.java) + assertThat(returnGenericParameterType("suspendFun7")).isInstanceOf(TypeVariable::class.java) + assertThat(returnGenericParameterTypeBoundName("suspendFun7")).isEqualTo("org.springframework.core.Consumer") + + assertThat(returnParameterType("suspendFun8")).isEqualTo(Object::class.java) + assertThat(returnGenericParameterType("suspendFun8")).isEqualTo(Object::class.java) + } + + @Test + fun `Continuation parameter is optional`() { + val method = this::class.java.getDeclaredMethod("suspendFun", String::class.java, Continuation::class.java) + assertThat(MethodParameter(method, 0).isOptional).isFalse() + assertThat(MethodParameter(method, 1).isOptional).isTrue() + } + + private fun returnParameterType(funName: String) = returnMethodParameter(funName).parameterType + private fun returnGenericParameterType(funName: String) = returnMethodParameter(funName).genericParameterType + private fun returnGenericParameterTypeName(funName: String) = returnGenericParameterType(funName).typeName + private fun returnGenericParameterTypeBoundName(funName: String) = (returnGenericParameterType(funName) as TypeVariable<*>).bounds[0].typeName + + private fun returnMethodParameter(funName: String) = + MethodParameter(this::class.declaredFunctions.first { it.name == funName }.javaMethod!!, -1) + + @Suppress("unused_parameter") + fun nullable(nullable: String?): Int? = 42 + + @Suppress("unused_parameter") + fun nonNullable(nonNullable: String): Int = 42 + + inner class InnerClass + + @Suppress("unused_parameter") + inner class InnerClassWithParameter(nonNullable: String, nullable: String?) + + @Suppress("unused_parameter") + class RegularClass(nonNullable: String, nullable: String?) + + @Suppress("unused", "unused_parameter") + suspend fun suspendFun(p1: String): Number = TODO() + + @Suppress("unused", "unused_parameter") + suspend fun suspendFun2(p1: String): Producer = TODO() + + @Suppress("unused", "unused_parameter") + suspend fun suspendFun3(p1: String): Wrapper = TODO() + + @Suppress("unused", "unused_parameter") + suspend fun suspendFun4(p1: String): Consumer = TODO() + + @Suppress("unused", "unused_parameter") + suspend fun > suspendFun5(p1: String): T = TODO() + + @Suppress("unused", "unused_parameter") + suspend fun > suspendFun6(p1: String): T = TODO() + + @Suppress("unused", "unused_parameter") + suspend fun > suspendFun7(p1: String): T = TODO() + + @Suppress("unused", "unused_parameter") + suspend fun suspendFun8(p1: String): Any? = TODO() +} + +interface Producer + +interface Wrapper + +interface Consumer diff --git a/spring-core/src/test/kotlin/org/springframework/core/KotlinReactiveAdapterRegistryTests.kt b/spring-core/src/test/kotlin/org/springframework/core/KotlinReactiveAdapterRegistryTests.kt new file mode 100644 index 0000000..b58a236 --- /dev/null +++ b/spring-core/src/test/kotlin/org/springframework/core/KotlinReactiveAdapterRegistryTests.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core + +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.reactivestreams.Publisher +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import reactor.test.StepVerifier +import java.time.Duration +import kotlin.reflect.KClass + +class KotlinReactiveAdapterRegistryTests { + + private val registry = ReactiveAdapterRegistry.getSharedInstance() + + @Test + fun deferredToPublisher() { + val source = GlobalScope.async { 1 } + val target: Publisher = getAdapter(Deferred::class).toPublisher(source) + assertThat(target).isInstanceOf(Mono::class.java) + assertThat((target as Mono).block(Duration.ofMillis(1000))).isEqualTo(1) + } + + @Test + fun publisherToDeferred() { + val source = Mono.just(1) + val target = getAdapter(Deferred::class).fromPublisher(source) + assertThat(target).isInstanceOf(Deferred::class.java) + assertThat(runBlocking { (target as Deferred<*>).await() }).isEqualTo(1) + } + + @Test + fun flowToPublisher() { + val source = flow { + emit(1) + emit(2) + emit(3) + } + val target: Publisher = getAdapter(Flow::class).toPublisher(source) + assertThat(target).isInstanceOf(Flux::class.java) + StepVerifier.create(target) + .expectNext(1) + .expectNext(2) + .expectNext(3) + .verifyComplete() + } + + @Test + fun publisherToFlow() { + val source = Flux.just(1, 2, 3) + val target = getAdapter(Flow::class).fromPublisher(source) + assertThat(target).isInstanceOf(Flow::class.java) + assertThat(runBlocking { (target as Flow<*>).toList() }).contains(1, 2, 3) + } + + private fun getAdapter(reactiveType: KClass<*>): ReactiveAdapter { + return this.registry.getAdapter(reactiveType.java)!! + } +} diff --git a/spring-core/src/test/kotlin/org/springframework/core/KotlinReflectionParameterNameDiscovererTests.kt b/spring-core/src/test/kotlin/org/springframework/core/KotlinReflectionParameterNameDiscovererTests.kt new file mode 100644 index 0000000..55b5ed4 --- /dev/null +++ b/spring-core/src/test/kotlin/org/springframework/core/KotlinReflectionParameterNameDiscovererTests.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +import org.springframework.util.ReflectionUtils + +/** + * Tests for KotlinReflectionParameterNameDiscoverer + */ +class KotlinReflectionParameterNameDiscovererTests { + + private val parameterNameDiscoverer = KotlinReflectionParameterNameDiscoverer() + + @Test + fun getParameterNamesOnInterface() { + val method = ReflectionUtils.findMethod(MessageService::class.java,"sendMessage", String::class.java)!! + val actualParams = parameterNameDiscoverer.getParameterNames(method) + assertThat(actualParams).contains("message") + } + + @Test + fun getParameterNamesOnClass() { + val method = ReflectionUtils.findMethod(MessageServiceImpl::class.java,"sendMessage", String::class.java)!! + val actualParams = parameterNameDiscoverer.getParameterNames(method) + assertThat(actualParams).contains("message") + } + + @Test + fun getParameterNamesOnExtensionMethod() { + val method = ReflectionUtils.findMethod(UtilityClass::class.java, "identity", String::class.java)!! + val actualParams = parameterNameDiscoverer.getParameterNames(method)!! + assertThat(actualParams).contains("\$receiver") + } + + interface MessageService { + fun sendMessage(message: String) + } + + class MessageServiceImpl { + fun sendMessage(message: String) = message + } + + class UtilityClass { + fun String.identity() = this + } +} diff --git a/spring-core/src/test/kotlin/org/springframework/core/env/KotlinPropertyResolverExtensionsTests.kt b/spring-core/src/test/kotlin/org/springframework/core/env/KotlinPropertyResolverExtensionsTests.kt new file mode 100644 index 0000000..81534ca --- /dev/null +++ b/spring-core/src/test/kotlin/org/springframework/core/env/KotlinPropertyResolverExtensionsTests.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.env + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test + +/** + * Mock object based tests for PropertyResolver Kotlin extensions. + * + * @author Sebastien Deleuze + */ +@Disabled +class KotlinPropertyResolverExtensionsTests { + + val propertyResolver = mockk() + + @Test + fun `get operator`() { + every { propertyResolver.getProperty("name") } returns "foo" + propertyResolver["name"] + verify { propertyResolver.getProperty("name") } + } + + @Test + fun `getProperty extension`() { + every { propertyResolver.getProperty("name", String::class.java) } returns "foo" + propertyResolver.getProperty("name") + verify { propertyResolver.getProperty("name", String::class.java) } + } + + @Test + fun `getRequiredProperty extension`() { + every { propertyResolver.getRequiredProperty("name", String::class.java) } returns "foo" + propertyResolver.getRequiredProperty("name") + verify { propertyResolver.getRequiredProperty("name", String::class.java) } + } + +} diff --git a/spring-core/src/test/resources/log4j2-test.xml b/spring-core/src/test/resources/log4j2-test.xml new file mode 100644 index 0000000..37bbffb --- /dev/null +++ b/spring-core/src/test/resources/log4j2-test.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/spring-core/src/test/resources/org/springframework/core/codec/ResourceRegionEncoderTests.txt b/spring-core/src/test/resources/org/springframework/core/codec/ResourceRegionEncoderTests.txt new file mode 100644 index 0000000..84bbb9d --- /dev/null +++ b/spring-core/src/test/resources/org/springframework/core/codec/ResourceRegionEncoderTests.txt @@ -0,0 +1 @@ +Spring Framework test resource content. \ No newline at end of file diff --git a/spring-core/src/test/resources/org/springframework/core/io/buffer/DataBufferUtilsTests.txt b/spring-core/src/test/resources/org/springframework/core/io/buffer/DataBufferUtilsTests.txt new file mode 100644 index 0000000..d66e359 --- /dev/null +++ b/spring-core/src/test/resources/org/springframework/core/io/buffer/DataBufferUtilsTests.txt @@ -0,0 +1 @@ +foobarbazqux \ No newline at end of file diff --git a/spring-core/src/test/resources/org/springframework/core/io/example.properties b/spring-core/src/test/resources/org/springframework/core/io/example.properties new file mode 100644 index 0000000..74d0a43 --- /dev/null +++ b/spring-core/src/test/resources/org/springframework/core/io/example.properties @@ -0,0 +1 @@ +foo=bar diff --git a/spring-core/src/test/resources/org/springframework/core/io/example.xml b/spring-core/src/test/resources/org/springframework/core/io/example.xml new file mode 100644 index 0000000..1d63853 --- /dev/null +++ b/spring-core/src/test/resources/org/springframework/core/io/example.xml @@ -0,0 +1,6 @@ + + + + bar + + diff --git a/spring-core/src/test/resources/org/springframework/core/io/support/resource#test1.txt b/spring-core/src/test/resources/org/springframework/core/io/support/resource#test1.txt new file mode 100644 index 0000000..4f67a83 --- /dev/null +++ b/spring-core/src/test/resources/org/springframework/core/io/support/resource#test1.txt @@ -0,0 +1 @@ +test 1 \ No newline at end of file diff --git a/spring-core/src/test/resources/org/springframework/core/io/support/resource#test2.txt b/spring-core/src/test/resources/org/springframework/core/io/support/resource#test2.txt new file mode 100644 index 0000000..81403e4 --- /dev/null +++ b/spring-core/src/test/resources/org/springframework/core/io/support/resource#test2.txt @@ -0,0 +1 @@ +test 2 \ No newline at end of file diff --git a/spring-core/src/test/resources/org/springframework/util/testlog4j.properties b/spring-core/src/test/resources/org/springframework/util/testlog4j.properties new file mode 100644 index 0000000..15d9af5 --- /dev/null +++ b/spring-core/src/test/resources/org/springframework/util/testlog4j.properties @@ -0,0 +1,2 @@ +log4j.rootCategory=DEBUG, mock +log4j.appender.mock=org.springframework.util.MockLog4jAppender \ No newline at end of file diff --git a/spring-core/src/test/resources/org/springframework/util/xml/dtdWithCommentOnNextLine.xml b/spring-core/src/test/resources/org/springframework/util/xml/dtdWithCommentOnNextLine.xml new file mode 100644 index 0000000..cc610ba --- /dev/null +++ b/spring-core/src/test/resources/org/springframework/util/xml/dtdWithCommentOnNextLine.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/spring-core/src/test/resources/org/springframework/util/xml/dtdWithLeadingComment.xml b/spring-core/src/test/resources/org/springframework/util/xml/dtdWithLeadingComment.xml new file mode 100644 index 0000000..710a5a4 --- /dev/null +++ b/spring-core/src/test/resources/org/springframework/util/xml/dtdWithLeadingComment.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/spring-core/src/test/resources/org/springframework/util/xml/dtdWithMultipleComments.xml b/spring-core/src/test/resources/org/springframework/util/xml/dtdWithMultipleComments.xml new file mode 100644 index 0000000..03b9964 --- /dev/null +++ b/spring-core/src/test/resources/org/springframework/util/xml/dtdWithMultipleComments.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/spring-core/src/test/resources/org/springframework/util/xml/dtdWithTrailingComment.xml b/spring-core/src/test/resources/org/springframework/util/xml/dtdWithTrailingComment.xml new file mode 100644 index 0000000..c8fd329 --- /dev/null +++ b/spring-core/src/test/resources/org/springframework/util/xml/dtdWithTrailingComment.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/spring-core/src/test/resources/org/springframework/util/xml/testContentHandler.xml b/spring-core/src/test/resources/org/springframework/util/xml/testContentHandler.xml new file mode 100644 index 0000000..062b7b7 --- /dev/null +++ b/spring-core/src/test/resources/org/springframework/util/xml/testContentHandler.xml @@ -0,0 +1,2 @@ + Some text \ No newline at end of file diff --git a/spring-core/src/test/resources/org/springframework/util/xml/testLexicalHandler.xml b/spring-core/src/test/resources/org/springframework/util/xml/testLexicalHandler.xml new file mode 100644 index 0000000..93bdf41 --- /dev/null +++ b/spring-core/src/test/resources/org/springframework/util/xml/testLexicalHandler.xml @@ -0,0 +1,8 @@ + +]> + + + + &entity; + \ No newline at end of file diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/Assume.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/Assume.java new file mode 100644 index 0000000..83c3dd8 --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/Assume.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.testfixture; + +import org.apache.commons.logging.Log; + +import static org.junit.jupiter.api.Assumptions.assumeFalse; + +/** + * Utility methods that allow JUnit tests to assume certain conditions hold + * {@code true}. If an assumption fails, it means the test should be aborted. + * + * @author Rob Winch + * @author Phillip Webb + * @author Sam Brannen + * @since 3.2 + * @see #notLogging(Log) + * @see EnabledForTestGroups @EnabledForTestGroups + */ +public abstract class Assume { + + /** + * Assume that the specified log is not set to Trace or Debug. + * @param log the log to test + * @throws org.opentest4j.TestAbortedException if the assumption fails + */ + public static void notLogging(Log log) { + assumeFalse(log.isTraceEnabled()); + assumeFalse(log.isDebugEnabled()); + } + +} diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/EnabledForTestGroups.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/EnabledForTestGroups.java new file mode 100644 index 0000000..490f428 --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/EnabledForTestGroups.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.testfixture; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * {@code @EnabledForTestGroups} is used to enable the annotated test class or + * test method for one or more {@link TestGroup} {@linkplain #value values}. + * + * @author Sam Brannen + * @since 5.2 + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ExtendWith(TestGroupsCondition.class) +public @interface EnabledForTestGroups { + + /** + * One or more {@link TestGroup}s that must be active. + */ + TestGroup[] value(); + +} diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/JavaUtilLoggingConfigurer.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/JavaUtilLoggingConfigurer.java new file mode 100644 index 0000000..d544830 --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/JavaUtilLoggingConfigurer.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.testfixture; + +import java.io.InputStream; +import java.util.logging.LogManager; + +import org.junit.platform.launcher.TestExecutionListener; +import org.junit.platform.launcher.TestPlan; + +/** + * JUnit Platform {@link TestExecutionListener} that configures Java Util Logging + * (JUL) from a file named {@code jul-test.properties} in the root of the classpath. + * + *

    This allows for projects to configure JUL for a test suite, analogous to + * log4j's support via {@code log4j2-test.xml}. + * + *

    This listener can be automatically registered on the JUnit Platform by + * adding the fully qualified name of this class to a file named + * {@code /META-INF/services/org.junit.platform.launcher.TestExecutionListener} + * — for example, under {@code src/test/resources}. + * + * @author Sam Brannen + * @since 5.2.2 + */ +public class JavaUtilLoggingConfigurer implements TestExecutionListener { + + public static final String JUL_TEST_PROPERTIES_FILE = "jul-test.properties"; + + + @Override + public void testPlanExecutionStarted(TestPlan testPlan) { + try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(JUL_TEST_PROPERTIES_FILE)) { + LogManager.getLogManager().readConfiguration(inputStream); + } + catch (Exception ex) { + System.err.println("WARNING: failed to configure Java Util Logging from classpath resource " + + JUL_TEST_PROPERTIES_FILE); + System.err.println(ex); + } + } +} diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/TestGroup.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/TestGroup.java new file mode 100644 index 0000000..a5fca97 --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/TestGroup.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.testfixture; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.util.StringUtils; + +import static java.lang.String.format; + +/** + * A test group used to limit when certain tests are run. + * + * @see EnabledForTestGroups @EnabledForTestGroups + * @author Phillip Webb + * @author Chris Beams + * @author Sam Brannen + */ +public enum TestGroup { + + /** + * Tests that take a considerable amount of time to run. Any test lasting longer than + * 500ms should be considered a candidate in order to avoid making the overall test + * suite too slow to run during the normal development cycle. + */ + LONG_RUNNING; + + + /** + * Determine if this {@link TestGroup} is active. + * @since 5.2 + */ + public boolean isActive() { + return loadTestGroups().contains(this); + } + + + private static final String TEST_GROUPS_SYSTEM_PROPERTY = "testGroups"; + + /** + * Load test groups dynamically instead of during static initialization in + * order to avoid a {@link NoClassDefFoundError} being thrown while attempting + * to load collaborator classes. + */ + static Set loadTestGroups() { + try { + return TestGroup.parse(System.getProperty(TEST_GROUPS_SYSTEM_PROPERTY)); + } + catch (Exception ex) { + throw new IllegalStateException("Failed to parse '" + TEST_GROUPS_SYSTEM_PROPERTY + + "' system property: " + ex.getMessage(), ex); + } + } + + /** + * Parse the specified comma separated string of groups. + * @param value the comma separated string of groups + * @return a set of groups + * @throws IllegalArgumentException if any specified group name is not a + * valid {@link TestGroup} + */ + static Set parse(String value) throws IllegalArgumentException { + if (!StringUtils.hasText(value)) { + return Collections.emptySet(); + } + String originalValue = value; + value = value.trim(); + if ("ALL".equalsIgnoreCase(value)) { + return EnumSet.allOf(TestGroup.class); + } + if (value.toUpperCase().startsWith("ALL-")) { + Set groups = EnumSet.allOf(TestGroup.class); + groups.removeAll(parseGroups(originalValue, value.substring(4))); + return groups; + } + return parseGroups(originalValue, value); + } + + private static Set parseGroups(String originalValue, String value) throws IllegalArgumentException { + Set groups = new HashSet<>(); + for (String group : value.split(",")) { + try { + groups.add(valueOf(group.trim().toUpperCase())); + } + catch (IllegalArgumentException ex) { + throw new IllegalArgumentException(format( + "Unable to find test group '%s' when parsing testGroups value: '%s'. " + + "Available groups include: [%s]", group.trim(), originalValue, + StringUtils.arrayToCommaDelimitedString(TestGroup.values()))); + } + } + return groups; + } + +} diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/TestGroupsCondition.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/TestGroupsCondition.java new file mode 100644 index 0000000..010f33b --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/TestGroupsCondition.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.testfixture; + +import java.util.Arrays; +import java.util.Optional; + +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtensionContext; + +import org.springframework.util.Assert; + +import static org.junit.jupiter.api.extension.ConditionEvaluationResult.disabled; +import static org.junit.jupiter.api.extension.ConditionEvaluationResult.enabled; +import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation; + +/** + * {@link ExecutionCondition} for Spring's {@link TestGroup} support. + * + * @author Sam Brannen + * @since 5.2 + * @see EnabledForTestGroups @EnabledForTestGroups + */ +class TestGroupsCondition implements ExecutionCondition { + + private static final ConditionEvaluationResult ENABLED_BY_DEFAULT = enabled("@EnabledForTestGroups is not present"); + + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + Optional optional = findAnnotation(context.getElement(), EnabledForTestGroups.class); + if (!optional.isPresent()) { + return ENABLED_BY_DEFAULT; + } + TestGroup[] testGroups = optional.get().value(); + Assert.state(testGroups.length > 0, "You must declare at least one TestGroup in @EnabledForTestGroups"); + return (Arrays.stream(testGroups).anyMatch(TestGroup::isActive)) ? + enabled("Enabled for TestGroups: " + Arrays.toString(testGroups)) : + disabled("Disabled for TestGroups: " + Arrays.toString(testGroups)); + } + +} diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/TimeStamped.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/TimeStamped.java new file mode 100644 index 0000000..58f45ad --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/TimeStamped.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.testfixture; + +/** + * This interface can be implemented by cacheable objects or cache entries, + * to enable the freshness of objects to be checked. + * + * @author Rod Johnson + */ +public interface TimeStamped { + + /** + * Return the timestamp for this object. + * @return long the timestamp for this object, + * as returned by System.currentTimeMillis() + */ + long getTimeStamp(); + +} diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/codec/AbstractDecoderTests.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/codec/AbstractDecoderTests.java new file mode 100644 index 0000000..5804d93 --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/codec/AbstractDecoderTests.java @@ -0,0 +1,429 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.testfixture.codec; + +import java.time.Duration; +import java.util.Map; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.Decoder; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.testfixture.io.buffer.AbstractLeakCheckingTests; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.MimeType; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Abstract base class for {@link Decoder} unit tests. Subclasses need to implement + * {@link #canDecode()}, {@link #decode()} and {@link #decodeToMono()}, possibly using the wide + * variety of helper methods like {@link #testDecodeAll} or {@link #testDecodeToMonoAll}. + * + * @author Arjen Poutsma + * @since 5.1.3 + */ +public abstract class AbstractDecoderTests> extends AbstractLeakCheckingTests { + + /** + * The decoder to test. + */ + protected D decoder; + + /** + * Construct a new {@code AbstractDecoderTests} instance for the given decoder. + * @param decoder the decoder + */ + protected AbstractDecoderTests(D decoder) { + Assert.notNull(decoder, "Encoder must not be null"); + + this.decoder = decoder; + } + + + /** + * Subclasses should implement this method to test {@link Decoder#canDecode}. + */ + @Test + public abstract void canDecode() throws Exception; + + /** + * Subclasses should implement this method to test {@link Decoder#decode}, possibly using + * {@link #testDecodeAll} or other helper methods. + */ + @Test + public abstract void decode() throws Exception; + + /** + * Subclasses should implement this method to test {@link Decoder#decodeToMono}, possibly using + * {@link #testDecodeToMonoAll}. + */ + @Test + public abstract void decodeToMono() throws Exception; + + // Flux + + /** + * Helper methods that tests for a variety of {@link Flux} decoding scenarios. This methods + * invokes: + *

      + *
    • {@link #testDecode(Publisher, ResolvableType, Consumer, MimeType, Map)}
    • + *
    • {@link #testDecodeError(Publisher, ResolvableType, MimeType, Map)}
    • + *
    • {@link #testDecodeCancel(Publisher, ResolvableType, MimeType, Map)}
    • + *
    • {@link #testDecodeEmpty(ResolvableType, MimeType, Map)}
    • + *
    + * + * @param input the input to be provided to the decoder + * @param outputClass the desired output class + * @param stepConsumer a consumer to {@linkplain StepVerifier verify} the output + * @param the output type + */ + protected void testDecodeAll(Publisher input, Class outputClass, + Consumer> stepConsumer) { + + testDecodeAll(input, ResolvableType.forClass(outputClass), stepConsumer, null, null); + } + + /** + * Helper methods that tests for a variety of {@link Flux} decoding scenarios. This methods + * invokes: + *
      + *
    • {@link #testDecode(Publisher, ResolvableType, Consumer, MimeType, Map)}
    • + *
    • {@link #testDecodeError(Publisher, ResolvableType, MimeType, Map)}
    • + *
    • {@link #testDecodeCancel(Publisher, ResolvableType, MimeType, Map)}
    • + *
    • {@link #testDecodeEmpty(ResolvableType, MimeType, Map)}
    • + *
    + * + * @param input the input to be provided to the decoder + * @param outputType the desired output type + * @param stepConsumer a consumer to {@linkplain StepVerifier verify} the output + * @param mimeType the mime type to use for decoding. May be {@code null}. + * @param hints the hints used for decoding. May be {@code null}. + * @param the output type + */ + protected void testDecodeAll(Publisher input, ResolvableType outputType, + Consumer> stepConsumer, + @Nullable MimeType mimeType, @Nullable Map hints) { + + testDecode(input, outputType, stepConsumer, mimeType, hints); + testDecodeError(input, outputType, mimeType, hints); + testDecodeCancel(input, outputType, mimeType, hints); + testDecodeEmpty(outputType, mimeType, hints); + } + + /** + * Test a standard {@link Decoder#decode decode} scenario. For example: + *
    +	 * byte[] bytes1 = ...
    +	 * byte[] bytes2 = ...
    +	 *
    +	 * Flux<DataBuffer> input = Flux.concat(
    +	 *   dataBuffer(bytes1),
    +	 *   dataBuffer(bytes2));
    +	 *
    +	 * testDecodeAll(input, byte[].class, step -> step
    +	 *   .consumeNextWith(expectBytes(bytes1))
    +	 *   .consumeNextWith(expectBytes(bytes2))
    +	 * 	 .verifyComplete());
    +	 * 
    + * + * @param input the input to be provided to the decoder + * @param outputClass the desired output class + * @param stepConsumer a consumer to {@linkplain StepVerifier verify} the output + * @param the output type + */ + protected void testDecode(Publisher input, Class outputClass, + Consumer> stepConsumer) { + + testDecode(input, ResolvableType.forClass(outputClass), stepConsumer, null, null); + } + + /** + * Test a standard {@link Decoder#decode decode} scenario. For example: + *
    +	 * byte[] bytes1 = ...
    +	 * byte[] bytes2 = ...
    +	 *
    +	 * Flux<DataBuffer> input = Flux.concat(
    +	 *   dataBuffer(bytes1),
    +	 *   dataBuffer(bytes2));
    +	 *
    +	 * testDecodeAll(input, byte[].class, step -> step
    +	 *   .consumeNextWith(expectBytes(bytes1))
    +	 *   .consumeNextWith(expectBytes(bytes2))
    +	 * 	 .verifyComplete());
    +	 * 
    + * + * @param input the input to be provided to the decoder + * @param outputType the desired output type + * @param stepConsumer a consumer to {@linkplain StepVerifier verify} the output + * @param mimeType the mime type to use for decoding. May be {@code null}. + * @param hints the hints used for decoding. May be {@code null}. + * @param the output type + */ + @SuppressWarnings("unchecked") + protected void testDecode(Publisher input, ResolvableType outputType, + Consumer> stepConsumer, + @Nullable MimeType mimeType, @Nullable Map hints) { + + Flux result = (Flux) this.decoder.decode(input, outputType, mimeType, hints); + StepVerifier.FirstStep step = StepVerifier.create(result); + stepConsumer.accept(step); + } + + /** + * Test a {@link Decoder#decode decode} scenario where the input stream contains an error. + * This test method will feed the first element of the {@code input} stream to the decoder, + * followed by an {@link InputException}. + * The result is expected to contain one "normal" element, followed by the error. + * + * @param input the input to be provided to the decoder + * @param outputType the desired output type + * @param mimeType the mime type to use for decoding. May be {@code null}. + * @param hints the hints used for decoding. May be {@code null}. + * @see InputException + */ + protected void testDecodeError(Publisher input, ResolvableType outputType, + @Nullable MimeType mimeType, @Nullable Map hints) { + + Flux buffer = Mono.from(input).concatWith(Flux.error(new InputException())); + assertThatExceptionOfType(InputException.class).isThrownBy(() -> + this.decoder.decode(buffer, outputType, mimeType, hints).blockLast(Duration.ofSeconds(5))); + } + + /** + * Test a {@link Decoder#decode decode} scenario where the input stream is canceled. + * This test method will feed the first element of the {@code input} stream to the decoder, + * followed by a cancel signal. + * The result is expected to contain one "normal" element. + * + * @param input the input to be provided to the decoder + * @param outputType the desired output type + * @param mimeType the mime type to use for decoding. May be {@code null}. + * @param hints the hints used for decoding. May be {@code null}. + */ + protected void testDecodeCancel(Publisher input, ResolvableType outputType, + @Nullable MimeType mimeType, @Nullable Map hints) { + + Flux result = this.decoder.decode(input, outputType, mimeType, hints); + StepVerifier.create(result).expectNextCount(1).thenCancel().verify(); + } + + /** + * Test a {@link Decoder#decode decode} scenario where the input stream is empty. + * The output is expected to be empty as well. + * + * @param outputType the desired output type + * @param mimeType the mime type to use for decoding. May be {@code null}. + * @param hints the hints used for decoding. May be {@code null}. + */ + protected void testDecodeEmpty(ResolvableType outputType, @Nullable MimeType mimeType, + @Nullable Map hints) { + + Flux input = Flux.empty(); + Flux result = this.decoder.decode(input, outputType, mimeType, hints); + StepVerifier.create(result).verifyComplete(); + } + + // Mono + + /** + * Helper methods that tests for a variety of {@link Mono} decoding scenarios. This methods + * invokes: + *
      + *
    • {@link #testDecodeToMono(Publisher, ResolvableType, Consumer, MimeType, Map)}
    • + *
    • {@link #testDecodeToMonoError(Publisher, ResolvableType, MimeType, Map)}
    • + *
    • {@link #testDecodeToMonoCancel(Publisher, ResolvableType, MimeType, Map)}
    • + *
    • {@link #testDecodeToMonoEmpty(ResolvableType, MimeType, Map)}
    • + *
    + * + * @param input the input to be provided to the decoder + * @param outputClass the desired output class + * @param stepConsumer a consumer to {@linkplain StepVerifier verify} the output + * @param the output type + */ + protected void testDecodeToMonoAll(Publisher input, + Class outputClass, Consumer> stepConsumer) { + + testDecodeToMonoAll(input, ResolvableType.forClass(outputClass), stepConsumer, null, null); + } + + /** + * Helper methods that tests for a variety of {@link Mono} decoding scenarios. This methods + * invokes: + *
      + *
    • {@link #testDecodeToMono(Publisher, ResolvableType, Consumer, MimeType, Map)}
    • + *
    • {@link #testDecodeToMonoError(Publisher, ResolvableType, MimeType, Map)}
    • + *
    • {@link #testDecodeToMonoCancel(Publisher, ResolvableType, MimeType, Map)}
    • + *
    • {@link #testDecodeToMonoEmpty(ResolvableType, MimeType, Map)}
    • + *
    + * + * @param input the input to be provided to the decoder + * @param outputType the desired output type + * @param stepConsumer a consumer to {@linkplain StepVerifier verify} the output + * @param mimeType the mime type to use for decoding. May be {@code null}. + * @param hints the hints used for decoding. May be {@code null}. + * @param the output type + */ + protected void testDecodeToMonoAll(Publisher input, ResolvableType outputType, + Consumer> stepConsumer, + @Nullable MimeType mimeType, @Nullable Map hints) { + + testDecodeToMono(input, outputType, stepConsumer, mimeType, hints); + testDecodeToMonoError(input, outputType, mimeType, hints); + testDecodeToMonoCancel(input, outputType, mimeType, hints); + testDecodeToMonoEmpty(outputType, mimeType, hints); + } + + /** + * Test a standard {@link Decoder#decodeToMono decode} scenario. For example: + *
    +	 * byte[] bytes1 = ...
    +	 * byte[] bytes2 = ...
    +	 * byte[] allBytes = ... // bytes1 + bytes2
    +	 *
    +	 * Flux<DataBuffer> input = Flux.concat(
    +	 *   dataBuffer(bytes1),
    +	 *   dataBuffer(bytes2));
    +	 *
    +	 * testDecodeAll(input, byte[].class, step -> step
    +	 *   .consumeNextWith(expectBytes(allBytes))
    +	 * 	 .verifyComplete());
    +	 * 
    + * + * @param input the input to be provided to the decoder + * @param outputClass the desired output class + * @param stepConsumer a consumer to {@linkplain StepVerifier verify} the output + * @param the output type + */ + protected void testDecodeToMono(Publisher input, + Class outputClass, Consumer> stepConsumer) { + + testDecodeToMono(input, ResolvableType.forClass(outputClass), stepConsumer, null, null); + } + + /** + * Test a standard {@link Decoder#decodeToMono decode} scenario. For example: + *
    +	 * byte[] bytes1 = ...
    +	 * byte[] bytes2 = ...
    +	 * byte[] allBytes = ... // bytes1 + bytes2
    +	 *
    +	 * Flux<DataBuffer> input = Flux.concat(
    +	 *   dataBuffer(bytes1),
    +	 *   dataBuffer(bytes2));
    +	 *
    +	 * testDecodeAll(input, byte[].class, step -> step
    +	 *   .consumeNextWith(expectBytes(allBytes))
    +	 * 	 .verifyComplete());
    +	 * 
    + * + * @param input the input to be provided to the decoder + * @param outputType the desired output type + * @param stepConsumer a consumer to {@linkplain StepVerifier verify} the output + * @param mimeType the mime type to use for decoding. May be {@code null}. + * @param hints the hints used for decoding. May be {@code null}. + * @param the output type + */ + @SuppressWarnings("unchecked") + protected void testDecodeToMono(Publisher input, ResolvableType outputType, + Consumer> stepConsumer, + @Nullable MimeType mimeType, @Nullable Map hints) { + + Mono result = (Mono) this.decoder.decodeToMono(input, outputType, mimeType, hints); + StepVerifier.FirstStep step = StepVerifier.create(result); + stepConsumer.accept(step); + } + + /** + * Test a {@link Decoder#decodeToMono decode} scenario where the input stream contains an error. + * This test method will feed the first element of the {@code input} stream to the decoder, + * followed by an {@link InputException}. + * The result is expected to contain the error. + * + * @param input the input to be provided to the decoder + * @param outputType the desired output type + * @param mimeType the mime type to use for decoding. May be {@code null}. + * @param hints the hints used for decoding. May be {@code null}. + * @see InputException + */ + protected void testDecodeToMonoError(Publisher input, ResolvableType outputType, + @Nullable MimeType mimeType, @Nullable Map hints) { + + input = Mono.from(input).concatWith(Flux.error(new InputException())); + Mono result = this.decoder.decodeToMono(input, outputType, mimeType, hints); + StepVerifier.create(result).expectError(InputException.class).verify(); + } + + /** + * Test a {@link Decoder#decodeToMono decode} scenario where the input stream is canceled. + * This test method will immediately cancel the output stream. + * + * @param input the input to be provided to the decoder + * @param outputType the desired output type + * @param mimeType the mime type to use for decoding. May be {@code null}. + * @param hints the hints used for decoding. May be {@code null}. + */ + protected void testDecodeToMonoCancel(Publisher input, ResolvableType outputType, + @Nullable MimeType mimeType, @Nullable Map hints) { + + Mono result = this.decoder.decodeToMono(input, outputType, mimeType, hints); + StepVerifier.create(result).thenCancel().verify(); + } + + /** + * Test a {@link Decoder#decodeToMono decode} scenario where the input stream is empty. + * The output is expected to be empty as well. + * + * @param outputType the desired output type + * @param mimeType the mime type to use for decoding. May be {@code null}. + * @param hints the hints used for decoding. May be {@code null}. + */ + protected void testDecodeToMonoEmpty(ResolvableType outputType, @Nullable MimeType mimeType, + @Nullable Map hints) { + + Mono result = this.decoder.decodeToMono(Flux.empty(), outputType, mimeType, hints); + StepVerifier.create(result).verifyComplete(); + } + + /** + * Creates a deferred {@link DataBuffer} containing the given bytes. + * @param bytes the bytes that are to be stored in the buffer + * @return the deferred buffer + */ + protected Mono dataBuffer(byte[] bytes) { + return Mono.fromCallable(() -> { + DataBuffer dataBuffer = this.bufferFactory.allocateBuffer(bytes.length); + dataBuffer.write(bytes); + return dataBuffer; + }); + } + + /** + * Exception used in {@link #testDecodeError} and {@link #testDecodeToMonoError} + */ + @SuppressWarnings("serial") + public static class InputException extends RuntimeException {} + +} diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/codec/AbstractEncoderTests.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/codec/AbstractEncoderTests.java new file mode 100644 index 0000000..106cc11 --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/codec/AbstractEncoderTests.java @@ -0,0 +1,273 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.testfixture.codec; + +import java.util.Map; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.Encoder; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.core.testfixture.io.buffer.AbstractLeakCheckingTests; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.MimeType; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.core.io.buffer.DataBufferUtils.release; + +/** + * Abstract base class for {@link Encoder} unit tests. Subclasses need to implement + * {@link #canEncode()} and {@link #encode()}, possibly using the wide + * * variety of helper methods like {@link #testEncodeAll}. + * + * @author Arjen Poutsma + * @since 5.1.3 + */ +public abstract class AbstractEncoderTests> extends AbstractLeakCheckingTests { + + /** + * The encoder to test. + */ + protected final E encoder; + + + /** + * Construct a new {@code AbstractEncoderTestCase} for the given parameters. + * @param encoder the encoder + */ + protected AbstractEncoderTests(E encoder) { + + Assert.notNull(encoder, "Encoder must not be null"); + + this.encoder = encoder; + } + + + /** + * Subclasses should implement this method to test {@link Encoder#canEncode}. + */ + @Test + public abstract void canEncode() throws Exception; + + /** + * Subclasses should implement this method to test {@link Encoder#encode}, possibly using + * {@link #testEncodeAll} or other helper methods. + */ + @Test + public abstract void encode() throws Exception; + + + /** + * Helper methods that tests for a variety of encoding scenarios. This methods + * invokes: + *
      + *
    • {@link #testEncode(Publisher, ResolvableType, Consumer, MimeType, Map)}
    • + *
    • {@link #testEncodeError(Publisher, ResolvableType, MimeType, Map)}
    • + *
    • {@link #testEncodeCancel(Publisher, ResolvableType, MimeType, Map)}
    • + *
    • {@link #testEncodeEmpty(ResolvableType, MimeType, Map)}
    • + *
    + * + * @param input the input to be provided to the encoder + * @param inputClass the input class + * @param stepConsumer a consumer to {@linkplain StepVerifier verify} the output + * @param the output type + */ + protected void testEncodeAll(Publisher input, Class inputClass, + Consumer> stepConsumer) { + testEncodeAll(input, ResolvableType.forClass(inputClass), stepConsumer, null, null); + } + + /** + * Helper methods that tests for a variety of decoding scenarios. This methods + * invokes: + *
      + *
    • {@link #testEncode(Publisher, ResolvableType, Consumer, MimeType, Map)}
    • + *
    • {@link #testEncodeError(Publisher, ResolvableType, MimeType, Map)}
    • + *
    • {@link #testEncodeCancel(Publisher, ResolvableType, MimeType, Map)}
    • + *
    • {@link #testEncodeEmpty(ResolvableType, MimeType, Map)}
    • + *
    + * + * @param input the input to be provided to the encoder + * @param inputType the input type + * @param stepConsumer a consumer to {@linkplain StepVerifier verify} the output + * @param mimeType the mime type to use for decoding. May be {@code null}. + * @param hints the hints used for decoding. May be {@code null}. + * @param the output type + */ + protected void testEncodeAll(Publisher input, ResolvableType inputType, + Consumer> stepConsumer, + @Nullable MimeType mimeType, @Nullable Map hints) { + testEncode(input, inputType, stepConsumer, mimeType, hints); + testEncodeError(input, inputType, mimeType, hints); + testEncodeCancel(input, inputType, mimeType, hints); + testEncodeEmpty(inputType, mimeType, hints); + } + + /** + * Test a standard {@link Encoder#encode encode} scenario. + * + * @param input the input to be provided to the encoder + * @param inputClass the input class + * @param stepConsumer a consumer to {@linkplain StepVerifier verify} the output + * @param the output type + */ + protected void testEncode(Publisher input, Class inputClass, + Consumer> stepConsumer) { + testEncode(input, ResolvableType.forClass(inputClass), stepConsumer, null, null); + } + + /** + * Test a standard {@link Encoder#encode encode} scenario. + * + * @param input the input to be provided to the encoder + * @param inputType the input type + * @param stepConsumer a consumer to {@linkplain StepVerifier verify} the output + * @param mimeType the mime type to use for decoding. May be {@code null}. + * @param hints the hints used for decoding. May be {@code null}. + * @param the output type + */ + protected void testEncode(Publisher input, ResolvableType inputType, + Consumer> stepConsumer, + @Nullable MimeType mimeType, @Nullable Map hints) { + + Flux result = encoder().encode(input, this.bufferFactory, inputType, + mimeType, hints); + StepVerifier.FirstStep step = StepVerifier.create(result); + stepConsumer.accept(step); + } + + /** + * Test a {@link Encoder#encode encode} scenario where the input stream contains an error. + * This test method will feed the first element of the {@code input} stream to the encoder, + * followed by an {@link InputException}. + * The result is expected to contain one "normal" element, followed by the error. + * + * @param input the input to be provided to the encoder + * @param inputType the input type + * @param mimeType the mime type to use for decoding. May be {@code null}. + * @param hints the hints used for decoding. May be {@code null}. + * @see InputException + */ + protected void testEncodeError(Publisher input, ResolvableType inputType, + @Nullable MimeType mimeType, @Nullable Map hints) { + + input = Flux.concat( + Flux.from(input).take(1), + Flux.error(new InputException())); + + Flux result = encoder().encode(input, this.bufferFactory, inputType, + mimeType, hints); + + StepVerifier.create(result) + .consumeNextWith(DataBufferUtils::release) + .expectError(InputException.class) + .verify(); + } + + /** + * Test a {@link Encoder#encode encode} scenario where the input stream is canceled. + * This test method will feed the first element of the {@code input} stream to the decoder, + * followed by a cancel signal. + * The result is expected to contain one "normal" element. + * + * @param input the input to be provided to the encoder + * @param inputType the input type + * @param mimeType the mime type to use for decoding. May be {@code null}. + * @param hints the hints used for decoding. May be {@code null}. + */ + protected void testEncodeCancel(Publisher input, ResolvableType inputType, + @Nullable MimeType mimeType, @Nullable Map hints) { + + Flux result = encoder().encode(input, this.bufferFactory, inputType, mimeType, + hints); + + StepVerifier.create(result) + .consumeNextWith(DataBufferUtils::release) + .thenCancel() + .verify(); + } + + /** + * Test a {@link Encoder#encode encode} scenario where the input stream is empty. + * The output is expected to be empty as well. + * + * @param inputType the input type + * @param mimeType the mime type to use for decoding. May be {@code null}. + * @param hints the hints used for decoding. May be {@code null}. + */ + protected void testEncodeEmpty(ResolvableType inputType, @Nullable MimeType mimeType, + @Nullable Map hints) { + + Flux input = Flux.empty(); + Flux result = encoder().encode(input, this.bufferFactory, inputType, + mimeType, hints); + + StepVerifier.create(result) + .verifyComplete(); + } + + /** + * Create a result consumer that expects the given bytes. + * @param expected the expected bytes + * @return a consumer that expects the given data buffer to be equal to {@code expected} + */ + protected final Consumer expectBytes(byte[] expected) { + return dataBuffer -> { + byte[] resultBytes = new byte[dataBuffer.readableByteCount()]; + dataBuffer.read(resultBytes); + release(dataBuffer); + assertThat(resultBytes).isEqualTo(expected); + }; + } + + /** + * Create a result consumer that expects the given string, using the UTF-8 encoding. + * @param expected the expected string + * @return a consumer that expects the given data buffer to be equal to {@code expected} + */ + protected Consumer expectString(String expected) { + return dataBuffer -> { + String actual = dataBuffer.toString(UTF_8); + release(dataBuffer); + assertThat(actual).isEqualTo(expected); + }; + + } + + @SuppressWarnings("unchecked") + private Encoder encoder() { + return (Encoder) this.encoder; + + } + + /** + * Exception used in {@link #testEncodeError}. + */ + @SuppressWarnings("serial") + public static class InputException extends RuntimeException { + + } + +} diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/env/EnvironmentTestUtils.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/env/EnvironmentTestUtils.java new file mode 100644 index 0000000..8606c47 --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/env/EnvironmentTestUtils.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.testfixture.env; + +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.Map; + +import org.springframework.core.env.StandardEnvironment; + +/** + * Test utilities for {@link StandardEnvironment}. + * + * @author Chris Beams + * @author Juergen Hoeller + */ +public class EnvironmentTestUtils { + + @SuppressWarnings("unchecked") + public static Map getModifiableSystemEnvironment() { + // for os x / linux + Class[] classes = Collections.class.getDeclaredClasses(); + Map env = System.getenv(); + for (Class cl : classes) { + if ("java.util.Collections$UnmodifiableMap".equals(cl.getName())) { + try { + Field field = cl.getDeclaredField("m"); + field.setAccessible(true); + Object obj = field.get(env); + if (obj != null && obj.getClass().getName().equals("java.lang.ProcessEnvironment$StringEnvironment")) { + return (Map) obj; + } + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + } + + // for windows + Class processEnvironmentClass; + try { + processEnvironmentClass = Class.forName("java.lang.ProcessEnvironment"); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + + try { + Field theCaseInsensitiveEnvironmentField = processEnvironmentClass.getDeclaredField("theCaseInsensitiveEnvironment"); + theCaseInsensitiveEnvironmentField.setAccessible(true); + Object obj = theCaseInsensitiveEnvironmentField.get(null); + return (Map) obj; + } + catch (NoSuchFieldException ex) { + // do nothing + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + + try { + Field theEnvironmentField = processEnvironmentClass.getDeclaredField("theEnvironment"); + theEnvironmentField.setAccessible(true); + Object obj = theEnvironmentField.get(null); + return (Map) obj; + } + catch (NoSuchFieldException ex) { + // do nothing + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + + throw new IllegalStateException(); + } + +} diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/env/MockPropertySource.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/env/MockPropertySource.java new file mode 100644 index 0000000..ae4944d --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/env/MockPropertySource.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.testfixture.env; + +import java.util.Properties; + +import org.springframework.core.env.PropertiesPropertySource; +import org.springframework.core.env.PropertySource; + +/** + * Simple {@link PropertySource} implementation for use in testing. Accepts + * a user-provided {@link Properties} object, or if omitted during construction, + * the implementation will initialize its own. + * + * The {@link #setProperty} and {@link #withProperty} methods are exposed for + * convenience, for example: + *
    + * {@code
    + *   PropertySource source = new MockPropertySource().withProperty("foo", "bar");
    + * }
    + * 
    + * + * @author Chris Beams + * @since 3.1 + * @see org.springframework.mock.env.MockEnvironment + */ +public class MockPropertySource extends PropertiesPropertySource { + + /** + * {@value} is the default name for {@link MockPropertySource} instances not + * otherwise given an explicit name. + * @see #MockPropertySource() + * @see #MockPropertySource(String) + */ + public static final String MOCK_PROPERTIES_PROPERTY_SOURCE_NAME = "mockProperties"; + + + /** + * Create a new {@code MockPropertySource} named {@value #MOCK_PROPERTIES_PROPERTY_SOURCE_NAME} + * that will maintain its own internal {@link Properties} instance. + */ + public MockPropertySource() { + this(new Properties()); + } + + /** + * Create a new {@code MockPropertySource} with the given name that will + * maintain its own internal {@link Properties} instance. + * @param name the {@linkplain #getName() name} of the property source + */ + public MockPropertySource(String name) { + this(name, new Properties()); + } + + /** + * Create a new {@code MockPropertySource} named {@value #MOCK_PROPERTIES_PROPERTY_SOURCE_NAME} + * and backed by the given {@link Properties} object. + * @param properties the properties to use + */ + public MockPropertySource(Properties properties) { + this(MOCK_PROPERTIES_PROPERTY_SOURCE_NAME, properties); + } + + /** + * Create a new {@code MockPropertySource} with the given name and backed by the given + * {@link Properties} object + * @param name the {@linkplain #getName() name} of the property source + * @param properties the properties to use + */ + public MockPropertySource(String name, Properties properties) { + super(name, properties); + } + + + /** + * Set the given property on the underlying {@link Properties} object. + */ + public void setProperty(String name, Object value) { + this.source.put(name, value); + } + + /** + * Convenient synonym for {@link #setProperty} that returns the current instance. + * Useful for method chaining and fluent-style use. + * @return this {@link MockPropertySource} instance + */ + public MockPropertySource withProperty(String name, Object value) { + this.setProperty(name, value); + return this; + } + +} diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/ResourceTestUtils.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/ResourceTestUtils.java new file mode 100644 index 0000000..3542ab5 --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/ResourceTestUtils.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.testfixture.io; + +import org.springframework.core.io.ClassPathResource; + +/** + * Convenience utilities for common operations with test resources. + * + * @author Chris Beams + */ +public abstract class ResourceTestUtils { + + /** + * Load a {@link ClassPathResource} qualified by the simple name of clazz, + * and relative to the package for clazz. + *

    Example: given a clazz 'com.foo.BarTests' and a resourceSuffix of 'context.xml', + * this method will return a ClassPathResource representing com/foo/BarTests-context.xml + *

    Intended for use loading context configuration XML files within JUnit tests. + */ + public static ClassPathResource qualifiedResource(Class clazz, String resourceSuffix) { + return new ClassPathResource(String.format("%s-%s", clazz.getSimpleName(), resourceSuffix), clazz); + } + +} diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/SerializationTestUtils.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/SerializationTestUtils.java new file mode 100644 index 0000000..4c5b884 --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/SerializationTestUtils.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.testfixture.io; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.NotSerializableException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; + +/** + * Utilities for testing serializability of objects. + * + *

    Exposes static methods for use in other test cases. + * + * @author Rod Johnson + * @author Sam Brannen + */ +public class SerializationTestUtils { + + public static void testSerialization(Object o) throws IOException { + OutputStream baos = new ByteArrayOutputStream(); + try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(o); + } + } + + public static boolean isSerializable(Object o) throws IOException { + try { + testSerialization(o); + return true; + } + catch (NotSerializableException ex) { + return false; + } + } + + @SuppressWarnings("unchecked") + public static T serializeAndDeserialize(T o) throws IOException, ClassNotFoundException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(o); + oos.flush(); + } + byte[] bytes = baos.toByteArray(); + + ByteArrayInputStream is = new ByteArrayInputStream(bytes); + try (ObjectInputStream ois = new ObjectInputStream(is)) { + return (T) ois.readObject(); + } + } + + public static T serializeAndDeserialize(Object o, Class expectedType) throws IOException, ClassNotFoundException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(o); + oos.flush(); + } + byte[] bytes = baos.toByteArray(); + + ByteArrayInputStream is = new ByteArrayInputStream(bytes); + try (ObjectInputStream ois = new ObjectInputStream(is)) { + return expectedType.cast(ois.readObject()); + } + } + +} diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/AbstractDataBufferAllocatingTests.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/AbstractDataBufferAllocatingTests.java new file mode 100644 index 0000000..ffe4c5d --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/AbstractDataBufferAllocatingTests.java @@ -0,0 +1,176 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.testfixture.io.buffer; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.PoolArenaMetric; +import io.netty.buffer.PooledByteBufAllocator; +import io.netty.buffer.PooledByteBufAllocatorMetric; +import io.netty.buffer.UnpooledByteBufAllocator; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import reactor.core.publisher.Mono; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.core.io.buffer.NettyDataBufferFactory; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +/** + * Base class for tests that read or write data buffers with an extension to check + * that allocated buffers have been released. + * + * @author Arjen Poutsma + * @author Rossen Stoyanchev + * @author Sam Brannen + */ +public abstract class AbstractDataBufferAllocatingTests { + + @RegisterExtension + AfterEachCallback leakDetector = context -> waitForDataBufferRelease(Duration.ofSeconds(2)); + + protected DataBufferFactory bufferFactory; + + + protected DataBuffer createDataBuffer(int capacity) { + return this.bufferFactory.allocateBuffer(capacity); + } + + protected DataBuffer stringBuffer(String value) { + return byteBuffer(value.getBytes(StandardCharsets.UTF_8)); + } + + protected Mono deferStringBuffer(String value) { + return Mono.defer(() -> Mono.just(stringBuffer(value))); + } + + protected DataBuffer byteBuffer(byte[] value) { + DataBuffer buffer = this.bufferFactory.allocateBuffer(value.length); + buffer.write(value); + return buffer; + } + + protected void release(DataBuffer... buffers) { + Arrays.stream(buffers).forEach(DataBufferUtils::release); + } + + protected Consumer stringConsumer(String expected) { + return dataBuffer -> { + String value = dataBuffer.toString(UTF_8); + DataBufferUtils.release(dataBuffer); + assertThat(value).isEqualTo(expected); + }; + } + + /** + * Wait until allocations are at 0, or the given duration elapses. + */ + private void waitForDataBufferRelease(Duration duration) throws InterruptedException { + Instant start = Instant.now(); + while (true) { + try { + verifyAllocations(); + break; + } + catch (AssertionError ex) { + if (Instant.now().isAfter(start.plus(duration))) { + throw ex; + } + } + Thread.sleep(50); + } + } + + private void verifyAllocations() { + if (this.bufferFactory instanceof NettyDataBufferFactory) { + ByteBufAllocator allocator = ((NettyDataBufferFactory) this.bufferFactory).getByteBufAllocator(); + if (allocator instanceof PooledByteBufAllocator) { + Instant start = Instant.now(); + while (true) { + PooledByteBufAllocatorMetric metric = ((PooledByteBufAllocator) allocator).metric(); + long total = getAllocations(metric.directArenas()) + getAllocations(metric.heapArenas()); + if (total == 0) { + return; + } + if (Instant.now().isBefore(start.plus(Duration.ofSeconds(5)))) { + try { + Thread.sleep(50); + } + catch (InterruptedException ex) { + // ignore + } + continue; + } + assertThat(total).as("ByteBuf Leak: " + total + " unreleased allocations").isEqualTo(0); + } + } + } + } + + private static long getAllocations(List metrics) { + return metrics.stream().mapToLong(PoolArenaMetric::numActiveAllocations).sum(); + } + + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("org.springframework.core.testfixture.io.buffer.AbstractDataBufferAllocatingTests#dataBufferFactories()") + public @interface ParameterizedDataBufferAllocatingTest { + } + + @SuppressWarnings("deprecation") // PooledByteBufAllocator no longer supports tinyCacheSize. + public static Stream dataBufferFactories() { + return Stream.of( + arguments("NettyDataBufferFactory - UnpooledByteBufAllocator - preferDirect = true", + new NettyDataBufferFactory(new UnpooledByteBufAllocator(true))), + arguments("NettyDataBufferFactory - UnpooledByteBufAllocator - preferDirect = false", + new NettyDataBufferFactory(new UnpooledByteBufAllocator(false))), + // 1) Disable caching for reliable leak detection, see https://github.com/netty/netty/issues/5275 + // 2) maxOrder is 4 (vs default 11) but can be increased if necessary + arguments("NettyDataBufferFactory - PooledByteBufAllocator - preferDirect = true", + new NettyDataBufferFactory(new PooledByteBufAllocator(true, 1, 1, 4096, 4, 0, 0, 0, true))), + arguments("NettyDataBufferFactory - PooledByteBufAllocator - preferDirect = false", + new NettyDataBufferFactory(new PooledByteBufAllocator(false, 1, 1, 4096, 4, 0, 0, 0, true))), + arguments("DefaultDataBufferFactory - preferDirect = true", + new DefaultDataBufferFactory(true)), + arguments("DefaultDataBufferFactory - preferDirect = false", + new DefaultDataBufferFactory(false)) + ); + } + +} diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/AbstractLeakCheckingTests.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/AbstractLeakCheckingTests.java new file mode 100644 index 0000000..061cad5 --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/AbstractLeakCheckingTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.testfixture.io.buffer; + +import org.junit.jupiter.api.AfterEach; + +import org.springframework.core.io.buffer.DataBufferFactory; + +/** + * Abstract base class for unit tests that allocate data buffers via a {@link DataBufferFactory}. + * After each unit test, this base class checks whether all created buffers have been released, + * throwing an {@link AssertionError} if not. + * + * @author Arjen Poutsma + * @since 5.1.3 + * @see LeakAwareDataBufferFactory + */ +public abstract class AbstractLeakCheckingTests { + + /** + * The data buffer factory. + */ + protected final LeakAwareDataBufferFactory bufferFactory = new LeakAwareDataBufferFactory(); + + /** + * Checks whether any of the data buffers created by {@link #bufferFactory} have not been + * released, throwing an assertion error if so. + */ + @AfterEach + final void checkForLeaks() { + this.bufferFactory.checkForLeaks(); + } + +} diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/DataBufferTestUtils.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/DataBufferTestUtils.java new file mode 100644 index 0000000..0277163 --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/DataBufferTestUtils.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.testfixture.io.buffer; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.util.Assert; + +/** + * Utility class for working with {@link DataBuffer}s in tests. + * + *

    Note that this class is in the {@code test} tree of the project: + * the methods contained herein are not suitable for production code bases. + * + * @author Arjen Poutsma + */ +public abstract class DataBufferTestUtils { + + /** + * Dump all the bytes in the given data buffer, and returns them as a byte array. + *

    Note that this method reads the entire buffer into the heap, which might + * consume a lot of memory. + * @param buffer the data buffer to dump the bytes of + * @return the bytes in the given data buffer + */ + public static byte[] dumpBytes(DataBuffer buffer) { + Assert.notNull(buffer, "'buffer' must not be null"); + byte[] bytes = new byte[buffer.readableByteCount()]; + buffer.read(bytes); + return bytes; + } + +} diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/LeakAwareDataBuffer.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/LeakAwareDataBuffer.java new file mode 100644 index 0000000..32fe8c5 --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/LeakAwareDataBuffer.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.testfixture.io.buffer; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.core.io.buffer.DataBufferWrapper; +import org.springframework.core.io.buffer.PooledDataBuffer; +import org.springframework.util.Assert; + +/** + * DataBuffer implementation created by {@link LeakAwareDataBufferFactory}. + * + * @author Arjen Poutsma + */ +class LeakAwareDataBuffer extends DataBufferWrapper implements PooledDataBuffer { + + private final AssertionError leakError; + + private final LeakAwareDataBufferFactory dataBufferFactory; + + + LeakAwareDataBuffer(DataBuffer delegate, LeakAwareDataBufferFactory dataBufferFactory) { + super(delegate); + Assert.notNull(dataBufferFactory, "DataBufferFactory must not be null"); + this.dataBufferFactory = dataBufferFactory; + this.leakError = createLeakError(delegate); + } + + private static AssertionError createLeakError(DataBuffer delegate) { + String message = String.format("DataBuffer leak detected: {%s} has not been released.%n" + + "Stack trace of buffer allocation statement follows:", + delegate); + AssertionError result = new AssertionError(message); + // remove first four irrelevant stack trace elements + StackTraceElement[] oldTrace = result.getStackTrace(); + StackTraceElement[] newTrace = new StackTraceElement[oldTrace.length - 4]; + System.arraycopy(oldTrace, 4, newTrace, 0, oldTrace.length - 4); + result.setStackTrace(newTrace); + return result; + } + + AssertionError leakError() { + return this.leakError; + } + + + @Override + public boolean isAllocated() { + DataBuffer delegate = dataBuffer(); + return delegate instanceof PooledDataBuffer && + ((PooledDataBuffer) delegate).isAllocated(); + } + + @Override + public PooledDataBuffer retain() { + DataBufferUtils.retain(dataBuffer()); + return this; + } + + @Override + public PooledDataBuffer touch(Object hint) { + DataBufferUtils.touch(dataBuffer(), hint); + return this; + } + + @Override + public boolean release() { + DataBufferUtils.release(dataBuffer()); + return isAllocated(); + } + + @Override + public LeakAwareDataBufferFactory factory() { + return this.dataBufferFactory; + } + + @Override + public String toString() { + return String.format("LeakAwareDataBuffer (%s)", dataBuffer()); + } +} diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/LeakAwareDataBufferFactory.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/LeakAwareDataBufferFactory.java new file mode 100644 index 0000000..29ed590 --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/LeakAwareDataBufferFactory.java @@ -0,0 +1,146 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.testfixture.io.buffer; + +import java.nio.ByteBuffer; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +import io.netty.buffer.PooledByteBufAllocator; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.core.io.buffer.NettyDataBufferFactory; +import org.springframework.util.Assert; + +/** + * Implementation of the {@code DataBufferFactory} interface that keeps track of + * memory leaks. + *

    Useful for unit tests that handle data buffers. Simply inherit from + * {@link AbstractLeakCheckingTests} or call {@link #checkForLeaks()} in + * a JUnit after method yourself, and any buffers that have not been + * released will result in an {@link AssertionError}. + * + * @author Arjen Poutsma + * @see LeakAwareDataBufferFactory + */ +public class LeakAwareDataBufferFactory implements DataBufferFactory { + + private static final Log logger = LogFactory.getLog(LeakAwareDataBufferFactory.class); + + + private final DataBufferFactory delegate; + + private final List created = new ArrayList<>(); + + private final AtomicBoolean trackCreated = new AtomicBoolean(true); + + + /** + * Creates a new {@code LeakAwareDataBufferFactory} by wrapping a + * {@link DefaultDataBufferFactory}. + */ + public LeakAwareDataBufferFactory() { + this(new NettyDataBufferFactory(PooledByteBufAllocator.DEFAULT)); + } + + /** + * Creates a new {@code LeakAwareDataBufferFactory} by wrapping the given delegate. + * @param delegate the delegate buffer factory to wrap. + */ + public LeakAwareDataBufferFactory(DataBufferFactory delegate) { + Assert.notNull(delegate, "Delegate must not be null"); + this.delegate = delegate; + } + + + /** + * Checks whether all of the data buffers allocated by this factory have also been released. + * If not, then an {@link AssertionError} is thrown. Typically used from a JUnit after + * method. + */ + public void checkForLeaks() { + this.trackCreated.set(false); + Instant start = Instant.now(); + while (true) { + if (this.created.stream().noneMatch(LeakAwareDataBuffer::isAllocated)) { + return; + } + if (Instant.now().isBefore(start.plus(Duration.ofSeconds(5)))) { + try { + Thread.sleep(50); + } + catch (InterruptedException ex) { + // ignore + } + continue; + } + List errors = this.created.stream() + .filter(LeakAwareDataBuffer::isAllocated) + .map(LeakAwareDataBuffer::leakError) + .collect(Collectors.toList()); + + errors.forEach(it -> logger.error("Leaked error: ", it)); + throw new AssertionError(errors.size() + " buffer leaks detected (see logs above)"); + } + } + + @Override + public DataBuffer allocateBuffer() { + return createLeakAwareDataBuffer(this.delegate.allocateBuffer()); + } + + @Override + public DataBuffer allocateBuffer(int initialCapacity) { + return createLeakAwareDataBuffer(this.delegate.allocateBuffer(initialCapacity)); + } + + private DataBuffer createLeakAwareDataBuffer(DataBuffer delegateBuffer) { + LeakAwareDataBuffer dataBuffer = new LeakAwareDataBuffer(delegateBuffer, this); + if (this.trackCreated.get()) { + this.created.add(dataBuffer); + } + return dataBuffer; + } + + @Override + public DataBuffer wrap(ByteBuffer byteBuffer) { + return this.delegate.wrap(byteBuffer); + } + + @Override + public DataBuffer wrap(byte[] bytes) { + return this.delegate.wrap(bytes); + } + + @Override + public DataBuffer join(List dataBuffers) { + // Remove LeakAwareDataBuffer wrapper so delegate can find native buffers + dataBuffers = dataBuffers.stream() + .map(o -> o instanceof LeakAwareDataBuffer ? ((LeakAwareDataBuffer) o).dataBuffer() : o) + .collect(Collectors.toList()); + return new LeakAwareDataBuffer(this.delegate.join(dataBuffers), this); + } + +} diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/security/TestPrincipal.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/security/TestPrincipal.java new file mode 100644 index 0000000..78c60b0 --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/security/TestPrincipal.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.testfixture.security; + +import java.security.Principal; + +/** + * An implementation of {@link Principal} for testing. + * + * @author Rossen Stoyanchev + */ +public class TestPrincipal implements Principal { + + private final String name; + + public TestPrincipal(String name) { + this.name = name; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof TestPrincipal)) { + return false; + } + TestPrincipal p = (TestPrincipal) obj; + return this.name.equals(p.name); + } + + @Override + public int hashCode() { + return this.name.hashCode(); + } + +} diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/stereotype/Component.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/stereotype/Component.java new file mode 100644 index 0000000..15dc1f1 --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/stereotype/Component.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.testfixture.stereotype; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Copy of the standard {@code Component} annotation for testing purposes. + * + * @author Mark Fisher + * @since 2.5 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Indexed +public @interface Component { + + /** + * The value may indicate a suggestion for a logical component name, + * to be turned into a Spring bean in case of an autodetected component. + * @return the suggested component name, if any (or empty String otherwise) + */ + String value() default ""; + +} diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/stereotype/Indexed.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/stereotype/Indexed.java new file mode 100644 index 0000000..0f91f76 --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/stereotype/Indexed.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.testfixture.stereotype; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Copy of the standard {@code Indexed} annotation for testing purposes. + * + * @author Stephane Nicoll + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Indexed { +} diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/xml/XmlContent.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/xml/XmlContent.java new file mode 100644 index 0000000..94b4491 --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/xml/XmlContent.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.testfixture.xml; + +import java.io.StringWriter; + +import org.assertj.core.api.AssertProvider; +import org.xmlunit.assertj.XmlAssert; + +/** + * {@link AssertProvider} to allow XML content assertions. Ultimately delegates + * to {@link XmlAssert}. + * + * @author Phillip Webb + */ +public class XmlContent implements AssertProvider { + + private final Object source; + + private XmlContent(Object source) { + this.source = source; + } + + @Override + public XmlContentAssert assertThat() { + return new XmlContentAssert(this.source); + } + + public static XmlContent from(Object source) { + return of(source); + } + + public static XmlContent of(Object source) { + if (source instanceof StringWriter) { + return of(source.toString()); + } + return new XmlContent(source); + } + +} diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/xml/XmlContentAssert.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/xml/XmlContentAssert.java new file mode 100644 index 0000000..6e05f82 --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/xml/XmlContentAssert.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.core.testfixture.xml; + +import org.assertj.core.api.AbstractAssert; +import org.w3c.dom.Node; +import org.xmlunit.assertj.XmlAssert; +import org.xmlunit.diff.DifferenceEvaluator; +import org.xmlunit.diff.NodeMatcher; +import org.xmlunit.util.Predicate; + +/** + * Assertions exposed by {@link XmlContent}. + * + * @author Phillip Webb + */ +public class XmlContentAssert extends AbstractAssert { + + XmlContentAssert(Object actual) { + super(actual, XmlContentAssert.class); + } + + public XmlContentAssert isSimilarTo(Object control) { + XmlAssert.assertThat(super.actual).and(control).areSimilar(); + return this; + } + + public XmlContentAssert isSimilarTo(Object control, Predicate nodeFilter) { + XmlAssert.assertThat(super.actual).and(control).withNodeFilter(nodeFilter).areSimilar(); + return this; + } + + public XmlContentAssert isSimilarTo(String control, + DifferenceEvaluator differenceEvaluator) { + XmlAssert.assertThat(super.actual).and(control).withDifferenceEvaluator( + differenceEvaluator).areSimilar(); + return this; + } + + public XmlContentAssert isSimilarToIgnoringWhitespace(Object control) { + XmlAssert.assertThat(super.actual).and(control).ignoreWhitespace().areSimilar(); + return this; + } + + + public XmlContentAssert isSimilarToIgnoringWhitespace(String control, NodeMatcher nodeMatcher) { + XmlAssert.assertThat(super.actual).and(control).ignoreWhitespace().withNodeMatcher(nodeMatcher).areSimilar(); + return this; + } + +} diff --git a/spring-expression/readme.txt b/spring-expression/readme.txt new file mode 100644 index 0000000..54402b9 --- /dev/null +++ b/spring-expression/readme.txt @@ -0,0 +1,40 @@ +List of outstanding things to think about - turn into tickets once distilled to a core set of issues + +High Importance + +- In the resolver/executor model we cache executors. They are currently recorded in the AST and so if the user chooses to evaluate an expression +in a different context then the stored executor may be incorrect. It may harmless 'fail' which would cause us to retrieve a new one, but +can it do anything malicious? In which case we either need to forget them when the context changes or store them elsewhere. Should caching be +something that can be switched on/off by the context? (shouldCacheExecutors() on the interface?) +- Expression serialization needs supporting +- expression basic interface and common package. Should LiteralExpression be settable? should getExpressionString return quoted value? + +Low Importance + +- For the ternary operator, should isWritable() return true/false depending on evaluating the condition and check isWritable() of whichever branch it +would have taken? At the moment ternary expressions are just considered NOT writable. +- Enhance type locator interface with direct support for register/unregister imports and ability to set class loader? +- Should some of the common errors (like SpelMessages.TYPE_NOT_FOUND) be promoted to top level exceptions? +- Expression comparison - is it necessary? + +Syntax + +- should the 'is' operator change to 'instanceof' ? +- in this expression we hit the problem of not being able to write chars, since '' always means string: + evaluate("new java.lang.String('hello').charAt(2).equals('l'.charAt(0))", true, Boolean.class); + So 'l'.charAt(0) was required - wonder if we can build in a converter for a single length string to char? + Can't do that as equals take Object and so we don't know to do a cast in order to pass a char into equals + We certainly cannot do a cast (unless casts are added to the syntax). See MethodInvocationTest.testStringClass() +- MATCHES is now the thing that takes a java regex. What does 'like' do? right now it is the SQL LIKE that supports + wildcards % and _. It has a poor implementation but I need to know whether to keep it in the language before + fixing that. +- Need to agree on a standard date format for 'default' processing of dates. Currently it is: + formatter = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss z", Locale.UK); + // this is something of this format: "Wed, 4 Jul 2001 12:08:56 GMT" + // https://java.sun.com/j2se/1.4.2/docs/api/java/text/SimpleDateFormat.html +- See LiteralTests for Date (4,5,6) - should date take an expression rather than be hardcoded in the grammar + to take 2 strings only? +- when doing arithmetic, eg. 8.4 / 4 and the user asks for an Integer return type - do we silently coerce or + say we cannot as it won't fit into an int? (see OperatorTests.testMathOperatorDivide04) +- Is $index within projection/selection useful or just cute? +- All reals are represented as Doubles (so 1.25f is held internally as a double, can be converted to float when required though) - is that ok? \ No newline at end of file diff --git a/spring-expression/spring-expression.gradle b/spring-expression/spring-expression.gradle new file mode 100644 index 0000000..9f8299d --- /dev/null +++ b/spring-expression/spring-expression.gradle @@ -0,0 +1,10 @@ +description = "Spring Expression Language (SpEL)" + +apply plugin: "kotlin" + +dependencies { + compile(project(":spring-core")) + testCompile(testFixtures(project(":spring-core"))) + testCompile("org.jetbrains.kotlin:kotlin-reflect") + testCompile("org.jetbrains.kotlin:kotlin-stdlib") +} diff --git a/spring-expression/src/jmh/java/org/springframework/expression/spel/SpelBenchmark.java b/spring-expression/src/jmh/java/org/springframework/expression/spel/SpelBenchmark.java new file mode 100644 index 0000000..1f12a17 --- /dev/null +++ b/spring-expression/src/jmh/java/org/springframework/expression/spel/SpelBenchmark.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import java.util.HashMap; +import java.util.Map; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; + +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +/** + * Benchmarks for parsing and executing SpEL expressions. + * @author Brian Clozel + */ +@BenchmarkMode(Mode.Throughput) +public class SpelBenchmark { + + @State(Scope.Benchmark) + public static class BenchmarkData { + + public ExpressionParser parser = new SpelExpressionParser(); + + public EvaluationContext eContext = TestScenarioCreator.getTestEvaluationContext(); + + } + + @Benchmark + public Object propertyAccessParseAndExecution(BenchmarkData data) { + Expression expr = data.parser.parseExpression("placeOfBirth.city"); + return expr.getValue(data.eContext); + } + + @Benchmark + public Object methodAccessParseAndExecution(BenchmarkData data) { + Expression expr = data.parser.parseExpression("getPlaceOfBirth().getCity()"); + return expr.getValue(data.eContext); + } + + @State(Scope.Benchmark) + public static class CachingBenchmarkData extends BenchmarkData { + + public Expression propertyExpression; + + public Expression methodExpression; + + public CachingBenchmarkData() { + this.propertyExpression = this.parser.parseExpression("placeOfBirth.city"); + this.methodExpression = this.parser.parseExpression("getPlaceOfBirth().getCity()"); + } + } + + @Benchmark + public Object cachingPropertyAccessParseAndExecution(CachingBenchmarkData data) { + return data.propertyExpression.getValue(data.eContext); + } + + @Benchmark + public Object cachingMethodAccessParseAndExecution(CachingBenchmarkData data) { + return data.methodExpression.getValue(data.eContext); + } + + @State(Scope.Benchmark) + public static class ValueBenchmarkData { + + public EvaluationContext context; + + public Expression expression; + + public ValueBenchmarkData() { + Map map = new HashMap<>(); + map.put("key", "value"); + this.context = new StandardEvaluationContext(map); + ExpressionParser spelExpressionParser = new SpelExpressionParser(); + this.expression = spelExpressionParser.parseExpression("#root['key']"); + } + } + + @Benchmark + public Object getValueFromMap(ValueBenchmarkData data) { + return data.expression.getValue(data.context); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/AccessException.java b/spring-expression/src/main/java/org/springframework/expression/AccessException.java new file mode 100644 index 0000000..0e92df5 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/AccessException.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression; + +/** + * An AccessException is thrown by an accessor if it has an unexpected problem. + * + * @author Andy Clement + * @since 3.0 + */ +@SuppressWarnings("serial") +public class AccessException extends Exception { + + /** + * Create an AccessException with a specific message. + * @param message the message + */ + public AccessException(String message) { + super(message); + } + + /** + * Create an AccessException with a specific message and cause. + * @param message the message + * @param cause the cause + */ + public AccessException(String message, Exception cause) { + super(message, cause); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/BeanResolver.java b/spring-expression/src/main/java/org/springframework/expression/BeanResolver.java new file mode 100644 index 0000000..eabeb9f --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/BeanResolver.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression; + +/** + * A bean resolver can be registered with the evaluation context and will kick in + * for bean references: {@code @myBeanName} and {@code &myBeanName} expressions. + * The & variant syntax allows access to the factory bean where relevant. + * + * @author Andy Clement + * @since 3.0.3 + */ +public interface BeanResolver { + + /** + * Look up a bean by the given name and return a corresponding instance for it. + * For attempting access to a factory bean, the name needs a & prefix. + * @param context the current evaluation context + * @param beanName the name of the bean to look up + * @return an object representing the bean + * @throws AccessException if there is an unexpected problem resolving the bean + */ + Object resolve(EvaluationContext context, String beanName) throws AccessException; + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/ConstructorExecutor.java b/spring-expression/src/main/java/org/springframework/expression/ConstructorExecutor.java new file mode 100644 index 0000000..c7e0fd3 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/ConstructorExecutor.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression; + + +// TODO Is the resolver/executor model too pervasive in this package? +/** + * Executors are built by resolvers and can be cached by the infrastructure to repeat an + * operation quickly without going back to the resolvers. For example, the particular + * constructor to run on a class may be discovered by the reflection constructor resolver + * - it will then build a ConstructorExecutor that executes that constructor and the + * ConstructorExecutor can be reused without needing to go back to the resolver to + * discover the constructor again. + * + *

    They can become stale, and in that case should throw an AccessException - this will + * cause the infrastructure to go back to the resolvers to ask for a new one. + * + * @author Andy Clement + * @since 3.0 + */ +public interface ConstructorExecutor { + + /** + * Execute a constructor in the specified context using the specified arguments. + * @param context the evaluation context in which the command is being executed + * @param arguments the arguments to the constructor call, should match (in terms + * of number and type) whatever the command will need to run + * @return the new object + * @throws AccessException if there is a problem executing the command or the + * CommandExecutor is no longer valid + */ + TypedValue execute(EvaluationContext context, Object... arguments) throws AccessException; + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/ConstructorResolver.java b/spring-expression/src/main/java/org/springframework/expression/ConstructorResolver.java new file mode 100644 index 0000000..b8e1a20 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/ConstructorResolver.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression; + +import java.util.List; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.lang.Nullable; + +/** + * A constructor resolver attempts locate a constructor and returns a ConstructorExecutor + * that can be used to invoke that constructor. The ConstructorExecutor will be cached but + * if it 'goes stale' the resolvers will be called again. + * + * @author Andy Clement + * @since 3.0 + */ +@FunctionalInterface +public interface ConstructorResolver { + + /** + * Within the supplied context determine a suitable constructor on the supplied type + * that can handle the specified arguments. Return a ConstructorExecutor that can be + * used to invoke that constructor (or {@code null} if no constructor could be found). + * @param context the current evaluation context + * @param typeName the type upon which to look for the constructor + * @param argumentTypes the arguments that the constructor must be able to handle + * @return a ConstructorExecutor that can invoke the constructor, or null if non found + */ + @Nullable + ConstructorExecutor resolve(EvaluationContext context, String typeName, List argumentTypes) + throws AccessException; + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java new file mode 100644 index 0000000..0b4d2ea --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression; + +import java.util.List; + +import org.springframework.lang.Nullable; + +/** + * Expressions are executed in an evaluation context. It is in this context that + * references are resolved when encountered during expression evaluation. + * + *

    There is a default implementation of this EvaluationContext interface: + * {@link org.springframework.expression.spel.support.StandardEvaluationContext} + * which can be extended, rather than having to implement everything manually. + * + * @author Andy Clement + * @author Juergen Hoeller + * @since 3.0 + */ +public interface EvaluationContext { + + /** + * Return the default root context object against which unqualified + * properties/methods/etc should be resolved. This can be overridden + * when evaluating an expression. + */ + TypedValue getRootObject(); + + /** + * Return a list of accessors that will be asked in turn to read/write a property. + */ + List getPropertyAccessors(); + + /** + * Return a list of resolvers that will be asked in turn to locate a constructor. + */ + List getConstructorResolvers(); + + /** + * Return a list of resolvers that will be asked in turn to locate a method. + */ + List getMethodResolvers(); + + /** + * Return a bean resolver that can look up beans by name. + */ + @Nullable + BeanResolver getBeanResolver(); + + /** + * Return a type locator that can be used to find types, either by short or + * fully qualified name. + */ + TypeLocator getTypeLocator(); + + /** + * Return a type converter that can convert (or coerce) a value from one type to another. + */ + TypeConverter getTypeConverter(); + + /** + * Return a type comparator for comparing pairs of objects for equality. + */ + TypeComparator getTypeComparator(); + + /** + * Return an operator overloader that may support mathematical operations + * between more than the standard set of types. + */ + OperatorOverloader getOperatorOverloader(); + + /** + * Set a named variable within this evaluation context to a specified value. + * @param name the name of the variable to set + * @param value the value to be placed in the variable + */ + void setVariable(String name, @Nullable Object value); + + /** + * Look up a named variable within this evaluation context. + * @param name variable to lookup + * @return the value of the variable, or {@code null} if not found + */ + @Nullable + Object lookupVariable(String name); + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/EvaluationException.java b/spring-expression/src/main/java/org/springframework/expression/EvaluationException.java new file mode 100644 index 0000000..400f561 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/EvaluationException.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression; + +/** + * Represent an exception that occurs during expression evaluation. + * + * @author Andy Clement + * @since 3.0 + */ +@SuppressWarnings("serial") +public class EvaluationException extends ExpressionException { + + /** + * Create a new expression evaluation exception. + * @param message description of the problem that occurred + */ + public EvaluationException(String message) { + super(message); + } + + /** + * Create a new expression evaluation exception. + * @param message description of the problem that occurred + * @param cause the underlying cause of this exception + */ + public EvaluationException(String message, Throwable cause) { + super(message,cause); + } + + /** + * Create a new expression evaluation exception. + * @param position the position in the expression where the problem occurred + * @param message description of the problem that occurred + */ + public EvaluationException(int position, String message) { + super(position, message); + } + + /** + * Create a new expression evaluation exception. + * @param expressionString the expression that could not be evaluated + * @param message description of the problem that occurred + */ + public EvaluationException(String expressionString, String message) { + super(expressionString, message); + } + + /** + * Create a new expression evaluation exception. + * @param position the position in the expression where the problem occurred + * @param message description of the problem that occurred + * @param cause the underlying cause of this exception + */ + public EvaluationException(int position, String message, Throwable cause) { + super(position, message, cause); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/Expression.java b/spring-expression/src/main/java/org/springframework/expression/Expression.java new file mode 100644 index 0000000..8babd78 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/Expression.java @@ -0,0 +1,265 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.lang.Nullable; + +/** + * An expression capable of evaluating itself against context objects. + * Encapsulates the details of a previously parsed expression string. + * Provides a common abstraction for expression evaluation. + * + * @author Keith Donald + * @author Andy Clement + * @author Juergen Hoeller + * @since 3.0 + */ +public interface Expression { + + /** + * Return the original string used to create this expression (unmodified). + * @return the original expression string + */ + String getExpressionString(); + + /** + * Evaluate this expression in the default standard context. + * @return the evaluation result + * @throws EvaluationException if there is a problem during evaluation + */ + @Nullable + Object getValue() throws EvaluationException; + + /** + * Evaluate the expression in the default context. If the result + * of the evaluation does not match (and cannot be converted to) + * the expected result type then an exception will be returned. + * @param desiredResultType the class the caller would like the result to be + * @return the evaluation result + * @throws EvaluationException if there is a problem during evaluation + */ + @Nullable + T getValue(@Nullable Class desiredResultType) throws EvaluationException; + + /** + * Evaluate this expression against the specified root object. + * @param rootObject the root object against which to evaluate the expression + * @return the evaluation result + * @throws EvaluationException if there is a problem during evaluation + */ + @Nullable + Object getValue(@Nullable Object rootObject) throws EvaluationException; + + /** + * Evaluate the expression in the default context against the specified root + * object. If the result of the evaluation does not match (and cannot be + * converted to) the expected result type then an exception will be returned. + * @param rootObject the root object against which to evaluate the expression + * @param desiredResultType the class the caller would like the result to be + * @return the evaluation result + * @throws EvaluationException if there is a problem during evaluation + */ + @Nullable + T getValue(@Nullable Object rootObject, @Nullable Class desiredResultType) throws EvaluationException; + + /** + * Evaluate this expression in the provided context and return the result + * of evaluation. + * @param context the context in which to evaluate the expression + * @return the evaluation result + * @throws EvaluationException if there is a problem during evaluation + */ + @Nullable + Object getValue(EvaluationContext context) throws EvaluationException; + + /** + * Evaluate this expression in the provided context and return the result + * of evaluation, but use the supplied root context as an override for any + * default root object specified in the context. + * @param context the context in which to evaluate the expression + * @param rootObject the root object against which to evaluate the expression + * @return the evaluation result + * @throws EvaluationException if there is a problem during evaluation + */ + @Nullable + Object getValue(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException; + + /** + * Evaluate the expression in a specified context which can resolve references + * to properties, methods, types, etc. The type of the evaluation result is + * expected to be of a particular class and an exception will be thrown if it + * is not and cannot be converted to that type. + * @param context the context in which to evaluate the expression + * @param desiredResultType the class the caller would like the result to be + * @return the evaluation result + * @throws EvaluationException if there is a problem during evaluation + */ + @Nullable + T getValue(EvaluationContext context, @Nullable Class desiredResultType) throws EvaluationException; + + /** + * Evaluate the expression in a specified context which can resolve references + * to properties, methods, types, etc. The type of the evaluation result is + * expected to be of a particular class and an exception will be thrown if it + * is not and cannot be converted to that type. The supplied root object + * overrides any default specified on the supplied context. + * @param context the context in which to evaluate the expression + * @param rootObject the root object against which to evaluate the expression + * @param desiredResultType the class the caller would like the result to be + * @return the evaluation result + * @throws EvaluationException if there is a problem during evaluation + */ + @Nullable + T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable Class desiredResultType) + throws EvaluationException; + + /** + * Return the most general type that can be passed to a {@link #setValue} + * method using the default context. + * @return the most general type of value that can be set on this context + * @throws EvaluationException if there is a problem determining the type + */ + @Nullable + Class getValueType() throws EvaluationException; + + /** + * Return the most general type that can be passed to the + * {@link #setValue(Object, Object)} method using the default context. + * @param rootObject the root object against which to evaluate the expression + * @return the most general type of value that can be set on this context + * @throws EvaluationException if there is a problem determining the type + */ + @Nullable + Class getValueType(@Nullable Object rootObject) throws EvaluationException; + + /** + * Return the most general type that can be passed to the + * {@link #setValue(EvaluationContext, Object)} method for the given context. + * @param context the context in which to evaluate the expression + * @return the most general type of value that can be set on this context + * @throws EvaluationException if there is a problem determining the type + */ + @Nullable + Class getValueType(EvaluationContext context) throws EvaluationException; + + /** + * Return the most general type that can be passed to the + * {@link #setValue(EvaluationContext, Object, Object)} method for the given + * context. The supplied root object overrides any specified in the context. + * @param context the context in which to evaluate the expression + * @param rootObject the root object against which to evaluate the expression + * @return the most general type of value that can be set on this context + * @throws EvaluationException if there is a problem determining the type + */ + @Nullable + Class getValueType(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException; + + /** + * Return the most general type that can be passed to a {@link #setValue} + * method using the default context. + * @return a type descriptor for values that can be set on this context + * @throws EvaluationException if there is a problem determining the type + */ + @Nullable + TypeDescriptor getValueTypeDescriptor() throws EvaluationException; + + /** + * Return the most general type that can be passed to the + * {@link #setValue(Object, Object)} method using the default context. + * @param rootObject the root object against which to evaluate the expression + * @return a type descriptor for values that can be set on this context + * @throws EvaluationException if there is a problem determining the type + */ + @Nullable + TypeDescriptor getValueTypeDescriptor(@Nullable Object rootObject) throws EvaluationException; + + /** + * Return the most general type that can be passed to the + * {@link #setValue(EvaluationContext, Object)} method for the given context. + * @param context the context in which to evaluate the expression + * @return a type descriptor for values that can be set on this context + * @throws EvaluationException if there is a problem determining the type + */ + @Nullable + TypeDescriptor getValueTypeDescriptor(EvaluationContext context) throws EvaluationException; + + /** + * Return the most general type that can be passed to the + * {@link #setValue(EvaluationContext, Object, Object)} method for the given + * context. The supplied root object overrides any specified in the context. + * @param context the context in which to evaluate the expression + * @param rootObject the root object against which to evaluate the expression + * @return a type descriptor for values that can be set on this context + * @throws EvaluationException if there is a problem determining the type + */ + @Nullable + TypeDescriptor getValueTypeDescriptor(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException; + + /** + * Determine if an expression can be written to, i.e. setValue() can be called. + * @param rootObject the root object against which to evaluate the expression + * @return {@code true} if the expression is writable; {@code false} otherwise + * @throws EvaluationException if there is a problem determining if it is writable + */ + boolean isWritable(@Nullable Object rootObject) throws EvaluationException; + + /** + * Determine if an expression can be written to, i.e. setValue() can be called. + * @param context the context in which the expression should be checked + * @return {@code true} if the expression is writable; {@code false} otherwise + * @throws EvaluationException if there is a problem determining if it is writable + */ + boolean isWritable(EvaluationContext context) throws EvaluationException; + + /** + * Determine if an expression can be written to, i.e. setValue() can be called. + * The supplied root object overrides any specified in the context. + * @param context the context in which the expression should be checked + * @param rootObject the root object against which to evaluate the expression + * @return {@code true} if the expression is writable; {@code false} otherwise + * @throws EvaluationException if there is a problem determining if it is writable + */ + boolean isWritable(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException; + + /** + * Set this expression in the provided context to the value provided. + * @param rootObject the root object against which to evaluate the expression + * @param value the new value + * @throws EvaluationException if there is a problem during evaluation + */ + void setValue(@Nullable Object rootObject, @Nullable Object value) throws EvaluationException; + + /** + * Set this expression in the provided context to the value provided. + * @param context the context in which to set the value of the expression + * @param value the new value + * @throws EvaluationException if there is a problem during evaluation + */ + void setValue(EvaluationContext context, @Nullable Object value) throws EvaluationException; + + /** + * Set this expression in the provided context to the value provided. + * The supplied root object overrides any specified in the context. + * @param context the context in which to set the value of the expression + * @param rootObject the root object against which to evaluate the expression + * @param value the new value + * @throws EvaluationException if there is a problem during evaluation + */ + void setValue(EvaluationContext context, @Nullable Object rootObject, @Nullable Object value) throws EvaluationException; + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/ExpressionException.java b/spring-expression/src/main/java/org/springframework/expression/ExpressionException.java new file mode 100644 index 0000000..5d6f4ae --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/ExpressionException.java @@ -0,0 +1,163 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression; + +import org.springframework.lang.Nullable; + +/** + * Super class for exceptions that can occur whilst processing expressions. + * + * @author Andy Clement + * @author Phillip Webb + * @since 3.0 + */ +@SuppressWarnings("serial") +public class ExpressionException extends RuntimeException { + + @Nullable + protected final String expressionString; + + protected int position; // -1 if not known; should be known in all reasonable cases + + + /** + * Construct a new expression exception. + * @param message a descriptive message + */ + public ExpressionException(String message) { + super(message); + this.expressionString = null; + this.position = 0; + } + + /** + * Construct a new expression exception. + * @param message a descriptive message + * @param cause the underlying cause of this exception + */ + public ExpressionException(String message, Throwable cause) { + super(message, cause); + this.expressionString = null; + this.position = 0; + } + + /** + * Construct a new expression exception. + * @param expressionString the expression string + * @param message a descriptive message + */ + public ExpressionException(@Nullable String expressionString, String message) { + super(message); + this.expressionString = expressionString; + this.position = -1; + } + + /** + * Construct a new expression exception. + * @param expressionString the expression string + * @param position the position in the expression string where the problem occurred + * @param message a descriptive message + */ + public ExpressionException(@Nullable String expressionString, int position, String message) { + super(message); + this.expressionString = expressionString; + this.position = position; + } + + /** + * Construct a new expression exception. + * @param position the position in the expression string where the problem occurred + * @param message a descriptive message + */ + public ExpressionException(int position, String message) { + super(message); + this.expressionString = null; + this.position = position; + } + + /** + * Construct a new expression exception. + * @param position the position in the expression string where the problem occurred + * @param message a descriptive message + * @param cause the underlying cause of this exception + */ + public ExpressionException(int position, String message, Throwable cause) { + super(message, cause); + this.expressionString = null; + this.position = position; + } + + + /** + * Return the expression string. + */ + @Nullable + public final String getExpressionString() { + return this.expressionString; + } + + /** + * Return the position in the expression string where the problem occurred. + */ + public final int getPosition() { + return this.position; + } + + /** + * Return the exception message. + * As of Spring 4.0, this method returns the same result as {@link #toDetailedString()}. + * @see #getSimpleMessage() + * @see java.lang.Throwable#getMessage() + */ + @Override + public String getMessage() { + return toDetailedString(); + } + + /** + * Return a detailed description of this exception, including the expression + * String and position (if available) as well as the actual exception message. + */ + public String toDetailedString() { + if (this.expressionString != null) { + StringBuilder output = new StringBuilder(); + output.append("Expression ["); + output.append(this.expressionString); + output.append("]"); + if (this.position >= 0) { + output.append(" @"); + output.append(this.position); + } + output.append(": "); + output.append(getSimpleMessage()); + return output.toString(); + } + else { + return getSimpleMessage(); + } + } + + /** + * Return the exception simple message without including the expression + * that caused the failure. + * @since 4.0 + */ + public String getSimpleMessage() { + return super.getMessage(); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/ExpressionInvocationTargetException.java b/spring-expression/src/main/java/org/springframework/expression/ExpressionInvocationTargetException.java new file mode 100644 index 0000000..9753ef8 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/ExpressionInvocationTargetException.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression; + +/** + * This exception wraps (as cause) a checked exception thrown by some method that SpEL + * invokes. It differs from a SpelEvaluationException because this indicates the + * occurrence of a checked exception that the invoked method was defined to throw. + * SpelEvaluationExceptions are for handling (and wrapping) unexpected exceptions. + * + * @author Andy Clement + * @since 3.0.3 + */ +@SuppressWarnings("serial") +public class ExpressionInvocationTargetException extends EvaluationException { + + public ExpressionInvocationTargetException(int position, String message, Throwable cause) { + super(position, message, cause); + } + + public ExpressionInvocationTargetException(int position, String message) { + super(position, message); + } + + public ExpressionInvocationTargetException(String expressionString, String message) { + super(expressionString, message); + } + + public ExpressionInvocationTargetException(String message, Throwable cause) { + super(message, cause); + } + + public ExpressionInvocationTargetException(String message) { + super(message); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/ExpressionParser.java b/spring-expression/src/main/java/org/springframework/expression/ExpressionParser.java new file mode 100644 index 0000000..c90dc07 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/ExpressionParser.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2009 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression; + +/** + * Parses expression strings into compiled expressions that can be evaluated. + * Supports parsing templates as well as standard expression strings. + * + * @author Keith Donald + * @author Andy Clement + * @since 3.0 + */ +public interface ExpressionParser { + + /** + * Parse the expression string and return an Expression object you can use for repeated evaluation. + *

    Some examples: + *

    +	 *     3 + 4
    +	 *     name.firstName
    +	 * 
    + * @param expressionString the raw expression string to parse + * @return an evaluator for the parsed expression + * @throws ParseException an exception occurred during parsing + */ + Expression parseExpression(String expressionString) throws ParseException; + + /** + * Parse the expression string and return an Expression object you can use for repeated evaluation. + *

    Some examples: + *

    +	 *     3 + 4
    +	 *     name.firstName
    +	 * 
    + * @param expressionString the raw expression string to parse + * @param context a context for influencing this expression parsing routine (optional) + * @return an evaluator for the parsed expression + * @throws ParseException an exception occurred during parsing + */ + Expression parseExpression(String expressionString, ParserContext context) throws ParseException; + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/MethodExecutor.java b/spring-expression/src/main/java/org/springframework/expression/MethodExecutor.java new file mode 100644 index 0000000..3c0b44d --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/MethodExecutor.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression; + +/** + * MethodExecutors are built by the resolvers and can be cached by the infrastructure to + * repeat an operation quickly without going back to the resolvers. For example, the + * particular method to run on an object may be discovered by the reflection method + * resolver - it will then build a MethodExecutor that executes that method and the + * MethodExecutor can be reused without needing to go back to the resolver to discover + * the method again. + * + *

    They can become stale, and in that case should throw an AccessException: + * This will cause the infrastructure to go back to the resolvers to ask for a new one. + * + * @author Andy Clement + * @since 3.0 + */ +public interface MethodExecutor { + + /** + * Execute a command using the specified arguments, and using the specified expression state. + * @param context the evaluation context in which the command is being executed + * @param target the target object of the call - null for static methods + * @param arguments the arguments to the executor, should match (in terms of number + * and type) whatever the command will need to run + * @return the value returned from execution + * @throws AccessException if there is a problem executing the command or the + * MethodExecutor is no longer valid + */ + TypedValue execute(EvaluationContext context, Object target, Object... arguments) throws AccessException; + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/MethodFilter.java b/spring-expression/src/main/java/org/springframework/expression/MethodFilter.java new file mode 100644 index 0000000..f5a5a09 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/MethodFilter.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression; + +import java.lang.reflect.Method; +import java.util.List; + +/** + * MethodFilter instances allow SpEL users to fine tune the behaviour of the method + * resolution process. Method resolution (which translates from a method name in an + * expression to a real method to invoke) will normally retrieve candidate methods for + * invocation via a simple call to 'Class.getMethods()' and will choose the first one that + * is suitable for the input parameters. By registering a MethodFilter the user can + * receive a callback and change the methods that will be considered suitable. + * + * @author Andy Clement + * @since 3.0.1 + */ +@FunctionalInterface +public interface MethodFilter { + + /** + * Called by the method resolver to allow the SpEL user to organize the list of + * candidate methods that may be invoked. The filter can remove methods that should + * not be considered candidates and it may sort the results. The resolver will then + * search through the methods as returned from the filter when looking for a suitable + * candidate to invoke. + * @param methods the full list of methods the resolver was going to choose from + * @return a possible subset of input methods that may be sorted by order of relevance + */ + List filter(List methods); + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/MethodResolver.java b/spring-expression/src/main/java/org/springframework/expression/MethodResolver.java new file mode 100644 index 0000000..db555ce --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/MethodResolver.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression; + +import java.util.List; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.lang.Nullable; + +/** + * A method resolver attempts locate a method and returns a command executor that can be + * used to invoke that method. The command executor will be cached but if it 'goes stale' + * the resolvers will be called again. + * + * @author Andy Clement + * @since 3.0 + */ +public interface MethodResolver { + + /** + * Within the supplied context determine a suitable method on the supplied object that + * can handle the specified arguments. Return a {@link MethodExecutor} that can be used + * to invoke that method, or {@code null} if no method could be found. + * @param context the current evaluation context + * @param targetObject the object upon which the method is being called + * @param argumentTypes the arguments that the constructor must be able to handle + * @return a MethodExecutor that can invoke the method, or null if the method cannot be found + */ + @Nullable + MethodExecutor resolve(EvaluationContext context, Object targetObject, String name, + List argumentTypes) throws AccessException; + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/Operation.java b/spring-expression/src/main/java/org/springframework/expression/Operation.java new file mode 100644 index 0000000..bf20fe3 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/Operation.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression; + +/** + * Supported operations that an {@link OperatorOverloader} can implement for any pair of + * operands. + * + * @author Andy Clement + * @since 3.0 + */ +public enum Operation { + + /** + * Add operation. + */ + ADD, + + /** + * Subtract operation. + */ + SUBTRACT, + + /** + * Divide operation. + */ + DIVIDE, + + /** + * Multiply operation. + */ + MULTIPLY, + + /** + * Modulus operation. + */ + MODULUS, + + /** + * Power operation. + */ + POWER + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/OperatorOverloader.java b/spring-expression/src/main/java/org/springframework/expression/OperatorOverloader.java new file mode 100644 index 0000000..2e6d431 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/OperatorOverloader.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression; + +import org.springframework.lang.Nullable; + +/** + * By default the mathematical operators {@link Operation} support simple types + * like numbers. By providing an implementation of OperatorOverloader, a user + * of the expression language can support these operations on other types. + * + * @author Andy Clement + * @since 3.0 + */ +public interface OperatorOverloader { + + /** + * Return true if the operator overloader supports the specified operation + * between the two operands and so should be invoked to handle it. + * @param operation the operation to be performed + * @param leftOperand the left operand + * @param rightOperand the right operand + * @return true if the OperatorOverloader supports the specified operation + * between the two operands + * @throws EvaluationException if there is a problem performing the operation + */ + boolean overridesOperation(Operation operation, @Nullable Object leftOperand, @Nullable Object rightOperand) + throws EvaluationException; + + /** + * Execute the specified operation on two operands, returning a result. + * See {@link Operation} for supported operations. + * @param operation the operation to be performed + * @param leftOperand the left operand + * @param rightOperand the right operand + * @return the result of performing the operation on the two operands + * @throws EvaluationException if there is a problem performing the operation + */ + Object operate(Operation operation, @Nullable Object leftOperand, @Nullable Object rightOperand) + throws EvaluationException; + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/ParseException.java b/spring-expression/src/main/java/org/springframework/expression/ParseException.java new file mode 100644 index 0000000..3c85016 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/ParseException.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression; + +import org.springframework.lang.Nullable; + +/** + * Represent an exception that occurs during expression parsing. + * + * @author Andy Clement + * @since 3.0 + */ +@SuppressWarnings("serial") +public class ParseException extends ExpressionException { + + /** + * Create a new expression parsing exception. + * @param expressionString the expression string that could not be parsed + * @param position the position in the expression string where the problem occurred + * @param message description of the problem that occurred + */ + public ParseException(@Nullable String expressionString, int position, String message) { + super(expressionString, position, message); + } + + /** + * Create a new expression parsing exception. + * @param position the position in the expression string where the problem occurred + * @param message description of the problem that occurred + * @param cause the underlying cause of this exception + */ + public ParseException(int position, String message, Throwable cause) { + super(position, message, cause); + } + + /** + * Create a new expression parsing exception. + * @param position the position in the expression string where the problem occurred + * @param message description of the problem that occurred + */ + public ParseException(int position, String message) { + super(position, message); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/ParserContext.java b/spring-expression/src/main/java/org/springframework/expression/ParserContext.java new file mode 100644 index 0000000..873d428 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/ParserContext.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression; + +/** + * Input provided to an expression parser that can influence an expression + * parsing/compilation routine. + * + * @author Keith Donald + * @author Andy Clement + * @since 3.0 + */ +public interface ParserContext { + + /** + * Whether or not the expression being parsed is a template. A template expression + * consists of literal text that can be mixed with evaluatable blocks. Some examples: + *

    +	 * 	   Some literal text
    +	 *     Hello #{name.firstName}!
    +	 *     #{3 + 4}
    +	 * 
    + * @return true if the expression is a template, false otherwise + */ + boolean isTemplate(); + + /** + * For template expressions, returns the prefix that identifies the start of an + * expression block within a string. For example: "${" + * @return the prefix that identifies the start of an expression + */ + String getExpressionPrefix(); + + /** + * For template expressions, return the prefix that identifies the end of an + * expression block within a string. For example: "}" + * @return the suffix that identifies the end of an expression + */ + String getExpressionSuffix(); + + + /** + * The default ParserContext implementation that enables template expression + * parsing mode. The expression prefix is "#{" and the expression suffix is "}". + * @see #isTemplate() + */ + ParserContext TEMPLATE_EXPRESSION = new ParserContext() { + + @Override + public boolean isTemplate() { + return true; + } + + @Override + public String getExpressionPrefix() { + return "#{"; + } + + @Override + public String getExpressionSuffix() { + return "}"; + } + }; + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/PropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/PropertyAccessor.java new file mode 100644 index 0000000..13056c4 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/PropertyAccessor.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression; + +import org.springframework.lang.Nullable; + +/** + * A property accessor is able to read from (and possibly write to) an object's properties. + * + *

    This interface places no restrictions, and so implementors are free to access properties + * directly as fields or through getters or in any other way they see as appropriate. + * + *

    A resolver can optionally specify an array of target classes for which it should be + * called. However, if it returns {@code null} from {@link #getSpecificTargetClasses()}, + * it will be called for all property references and given a chance to determine if it + * can read or write them. + * + *

    Property resolvers are considered to be ordered, and each will be called in turn. + * The only rule that affects the call order is that any resolver naming the target + * class directly in {@link #getSpecificTargetClasses()} will be called first, before + * the general resolvers. + * + * @author Andy Clement + * @since 3.0 + */ +public interface PropertyAccessor { + + /** + * Return an array of classes for which this resolver should be called. + *

    Returning {@code null} indicates this is a general resolver that + * can be called in an attempt to resolve a property on any type. + * @return an array of classes that this resolver is suitable for + * (or {@code null} if a general resolver) + */ + @Nullable + Class[] getSpecificTargetClasses(); + + /** + * Called to determine if a resolver instance is able to access a specified property + * on a specified target object. + * @param context the evaluation context in which the access is being attempted + * @param target the target object upon which the property is being accessed + * @param name the name of the property being accessed + * @return true if this resolver is able to read the property + * @throws AccessException if there is any problem determining whether the property can be read + */ + boolean canRead(EvaluationContext context, @Nullable Object target, String name) throws AccessException; + + /** + * Called to read a property from a specified target object. + * Should only succeed if {@link #canRead} also returns {@code true}. + * @param context the evaluation context in which the access is being attempted + * @param target the target object upon which the property is being accessed + * @param name the name of the property being accessed + * @return a TypedValue object wrapping the property value read and a type descriptor for it + * @throws AccessException if there is any problem accessing the property value + */ + TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException; + + /** + * Called to determine if a resolver instance is able to write to a specified + * property on a specified target object. + * @param context the evaluation context in which the access is being attempted + * @param target the target object upon which the property is being accessed + * @param name the name of the property being accessed + * @return true if this resolver is able to write to the property + * @throws AccessException if there is any problem determining whether the + * property can be written to + */ + boolean canWrite(EvaluationContext context, @Nullable Object target, String name) throws AccessException; + + /** + * Called to write to a property on a specified target object. + * Should only succeed if {@link #canWrite} also returns {@code true}. + * @param context the evaluation context in which the access is being attempted + * @param target the target object upon which the property is being accessed + * @param name the name of the property being accessed + * @param newValue the new value for the property + * @throws AccessException if there is any problem writing to the property value + */ + void write(EvaluationContext context, @Nullable Object target, String name, @Nullable Object newValue) + throws AccessException; + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/TypeComparator.java b/spring-expression/src/main/java/org/springframework/expression/TypeComparator.java new file mode 100644 index 0000000..7696cd7 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/TypeComparator.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression; + +import org.springframework.lang.Nullable; + +/** + * Instances of a type comparator should be able to compare pairs of objects for equality. + * The specification of the return value is the same as for {@link java.lang.Comparable}. + * + * @author Andy Clement + * @since 3.0 + * @see java.lang.Comparable + */ +public interface TypeComparator { + + /** + * Return {@code true} if the comparator can compare these two objects. + * @param firstObject the first object + * @param secondObject the second object + * @return {@code true} if the comparator can compare these objects + */ + boolean canCompare(@Nullable Object firstObject, @Nullable Object secondObject); + + /** + * Compare two given objects. + * @param firstObject the first object + * @param secondObject the second object + * @return 0 if they are equal, <0 if the first is smaller than the second, + * or >0 if the first is larger than the second + * @throws EvaluationException if a problem occurs during comparison + * (or if they are not comparable in the first place) + */ + int compare(@Nullable Object firstObject, @Nullable Object secondObject) throws EvaluationException; + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/TypeConverter.java b/spring-expression/src/main/java/org/springframework/expression/TypeConverter.java new file mode 100644 index 0000000..f2f67c0 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/TypeConverter.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.lang.Nullable; + +/** + * A type converter can convert values between different types encountered during + * expression evaluation. This is an SPI for the expression parser; see + * {@link org.springframework.core.convert.ConversionService} for the primary + * user API to Spring's conversion facilities. + * + * @author Andy Clement + * @author Juergen Hoeller + * @since 3.0 + */ +public interface TypeConverter { + + /** + * Return {@code true} if the type converter can convert the specified type + * to the desired target type. + * @param sourceType a type descriptor that describes the source type + * @param targetType a type descriptor that describes the requested result type + * @return {@code true} if that conversion can be performed + */ + boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType); + + /** + * Convert (or coerce) a value from one type to another, for example from a + * {@code boolean} to a {@code String}. + *

    The {@link TypeDescriptor} parameters enable support for typed collections: + * A caller may prefer a {@code List<Integer>}, for example, rather than + * simply any {@code List}. + * @param value the value to be converted + * @param sourceType a type descriptor that supplies extra information about the + * source object + * @param targetType a type descriptor that supplies extra information about the + * requested result type + * @return the converted value + * @throws EvaluationException if conversion failed or is not possible to begin with + */ + @Nullable + Object convertValue(@Nullable Object value, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType); + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/TypeLocator.java b/spring-expression/src/main/java/org/springframework/expression/TypeLocator.java new file mode 100644 index 0000000..ec1c531 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/TypeLocator.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression; + +/** + * Implementers of this interface are expected to be able to locate types. + * They may use a custom {@link ClassLoader} and/or deal with common + * package prefixes (e.g. {@code java.lang}) however they wish. + * + *

    See {@link org.springframework.expression.spel.support.StandardTypeLocator} + * for an example implementation. + * + * @author Andy Clement + * @since 3.0 + */ +@FunctionalInterface +public interface TypeLocator { + + /** + * Find a type by name. The name may or may not be fully qualified + * (e.g. {@code String} or {@code java.lang.String}). + * @param typeName the type to be located + * @return the {@code Class} object representing that type + * @throws EvaluationException if there is a problem finding the type + */ + Class findType(String typeName) throws EvaluationException; + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/TypedValue.java b/spring-expression/src/main/java/org/springframework/expression/TypedValue.java new file mode 100644 index 0000000..d2d470b --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/TypedValue.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +/** + * Encapsulates an object and a {@link TypeDescriptor} that describes it. + * The type descriptor can contain generic declarations that would not + * be accessible through a simple {@code getClass()} call on the object. + * + * @author Andy Clement + * @author Juergen Hoeller + * @since 3.0 + */ +public class TypedValue { + + /** + * {@link TypedValue} for {@code null}. + */ + public static final TypedValue NULL = new TypedValue(null); + + + @Nullable + private final Object value; + + @Nullable + private TypeDescriptor typeDescriptor; + + + /** + * Create a {@link TypedValue} for a simple object. The {@link TypeDescriptor} + * is inferred from the object, so no generic declarations are preserved. + * @param value the object value + */ + public TypedValue(@Nullable Object value) { + this.value = value; + this.typeDescriptor = null; // initialized when/if requested + } + + /** + * Create a {@link TypedValue} for a particular value with a particular + * {@link TypeDescriptor} which may contain additional generic declarations. + * @param value the object value + * @param typeDescriptor a type descriptor describing the type of the value + */ + public TypedValue(@Nullable Object value, @Nullable TypeDescriptor typeDescriptor) { + this.value = value; + this.typeDescriptor = typeDescriptor; + } + + + @Nullable + public Object getValue() { + return this.value; + } + + @Nullable + public TypeDescriptor getTypeDescriptor() { + if (this.typeDescriptor == null && this.value != null) { + this.typeDescriptor = TypeDescriptor.forObject(this.value); + } + return this.typeDescriptor; + } + + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof TypedValue)) { + return false; + } + TypedValue otherTv = (TypedValue) other; + // Avoid TypeDescriptor initialization if not necessary + return (ObjectUtils.nullSafeEquals(this.value, otherTv.value) && + ((this.typeDescriptor == null && otherTv.typeDescriptor == null) || + ObjectUtils.nullSafeEquals(getTypeDescriptor(), otherTv.getTypeDescriptor()))); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.value); + } + + @Override + public String toString() { + return "TypedValue: '" + this.value + "' of [" + getTypeDescriptor() + "]"; + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/common/CompositeStringExpression.java b/spring-expression/src/main/java/org/springframework/expression/common/CompositeStringExpression.java new file mode 100644 index 0000000..a37cb21 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/common/CompositeStringExpression.java @@ -0,0 +1,218 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.common; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Expression; +import org.springframework.expression.TypedValue; +import org.springframework.lang.Nullable; + +/** + * Represents a template expression broken into pieces. Each piece will be an Expression + * but pure text parts to the template will be represented as LiteralExpression objects. + * An example of a template expression might be: + * + *

    + * "Hello ${getName()}"
    + * 
    + * + * which will be represented as a CompositeStringExpression of two parts. The first part + * being a LiteralExpression representing 'Hello ' and the second part being a real + * expression that will call {@code getName()} when invoked. + * + * @author Andy Clement + * @author Juergen Hoeller + * @since 3.0 + */ +public class CompositeStringExpression implements Expression { + + private final String expressionString; + + /** The array of expressions that make up the composite expression. */ + private final Expression[] expressions; + + + public CompositeStringExpression(String expressionString, Expression[] expressions) { + this.expressionString = expressionString; + this.expressions = expressions; + } + + + @Override + public final String getExpressionString() { + return this.expressionString; + } + + public final Expression[] getExpressions() { + return this.expressions; + } + + @Override + public String getValue() throws EvaluationException { + StringBuilder sb = new StringBuilder(); + for (Expression expression : this.expressions) { + String value = expression.getValue(String.class); + if (value != null) { + sb.append(value); + } + } + return sb.toString(); + } + + @Override + @Nullable + public T getValue(@Nullable Class expectedResultType) throws EvaluationException { + Object value = getValue(); + return ExpressionUtils.convertTypedValue(null, new TypedValue(value), expectedResultType); + } + + @Override + public String getValue(@Nullable Object rootObject) throws EvaluationException { + StringBuilder sb = new StringBuilder(); + for (Expression expression : this.expressions) { + String value = expression.getValue(rootObject, String.class); + if (value != null) { + sb.append(value); + } + } + return sb.toString(); + } + + @Override + @Nullable + public T getValue(@Nullable Object rootObject, @Nullable Class desiredResultType) throws EvaluationException { + Object value = getValue(rootObject); + return ExpressionUtils.convertTypedValue(null, new TypedValue(value), desiredResultType); + } + + @Override + public String getValue(EvaluationContext context) throws EvaluationException { + StringBuilder sb = new StringBuilder(); + for (Expression expression : this.expressions) { + String value = expression.getValue(context, String.class); + if (value != null) { + sb.append(value); + } + } + return sb.toString(); + } + + @Override + @Nullable + public T getValue(EvaluationContext context, @Nullable Class expectedResultType) + throws EvaluationException { + + Object value = getValue(context); + return ExpressionUtils.convertTypedValue(context, new TypedValue(value), expectedResultType); + } + + @Override + public String getValue(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException { + StringBuilder sb = new StringBuilder(); + for (Expression expression : this.expressions) { + String value = expression.getValue(context, rootObject, String.class); + if (value != null) { + sb.append(value); + } + } + return sb.toString(); + } + + @Override + @Nullable + public T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable Class desiredResultType) + throws EvaluationException { + + Object value = getValue(context,rootObject); + return ExpressionUtils.convertTypedValue(context, new TypedValue(value), desiredResultType); + } + + @Override + public Class getValueType() { + return String.class; + } + + @Override + public Class getValueType(EvaluationContext context) { + return String.class; + } + + @Override + public Class getValueType(@Nullable Object rootObject) throws EvaluationException { + return String.class; + } + + @Override + public Class getValueType(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException { + return String.class; + } + + @Override + public TypeDescriptor getValueTypeDescriptor() { + return TypeDescriptor.valueOf(String.class); + } + + @Override + public TypeDescriptor getValueTypeDescriptor(@Nullable Object rootObject) throws EvaluationException { + return TypeDescriptor.valueOf(String.class); + } + + @Override + public TypeDescriptor getValueTypeDescriptor(EvaluationContext context) { + return TypeDescriptor.valueOf(String.class); + } + + @Override + public TypeDescriptor getValueTypeDescriptor(EvaluationContext context, @Nullable Object rootObject) + throws EvaluationException { + + return TypeDescriptor.valueOf(String.class); + } + + @Override + public boolean isWritable(@Nullable Object rootObject) throws EvaluationException { + return false; + } + + @Override + public boolean isWritable(EvaluationContext context) { + return false; + } + + @Override + public boolean isWritable(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException { + return false; + } + + @Override + public void setValue(@Nullable Object rootObject, @Nullable Object value) throws EvaluationException { + throw new EvaluationException(this.expressionString, "Cannot call setValue on a composite expression"); + } + + @Override + public void setValue(EvaluationContext context, @Nullable Object value) throws EvaluationException { + throw new EvaluationException(this.expressionString, "Cannot call setValue on a composite expression"); + } + + @Override + public void setValue(EvaluationContext context, @Nullable Object rootObject, @Nullable Object value) throws EvaluationException { + throw new EvaluationException(this.expressionString, "Cannot call setValue on a composite expression"); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/common/ExpressionUtils.java b/spring-expression/src/main/java/org/springframework/expression/common/ExpressionUtils.java new file mode 100644 index 0000000..3ea264b --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/common/ExpressionUtils.java @@ -0,0 +1,132 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.common; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.TypeConverter; +import org.springframework.expression.TypedValue; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * Common utility functions that may be used by any Expression Language provider. + * + * @author Andy Clement + * @author Juergen Hoeller + * @since 3.0 + */ +public abstract class ExpressionUtils { + + /** + * Determines if there is a type converter available in the specified context and + * attempts to use it to convert the supplied value to the specified type. Throws an + * exception if conversion is not possible. + * @param context the evaluation context that may define a type converter + * @param typedValue the value to convert and a type descriptor describing it + * @param targetType the type to attempt conversion to + * @return the converted value + * @throws EvaluationException if there is a problem during conversion or conversion + * of the value to the specified type is not supported + */ + @SuppressWarnings("unchecked") + @Nullable + public static T convertTypedValue( + @Nullable EvaluationContext context, TypedValue typedValue, @Nullable Class targetType) { + + Object value = typedValue.getValue(); + if (targetType == null) { + return (T) value; + } + if (context != null) { + return (T) context.getTypeConverter().convertValue( + value, typedValue.getTypeDescriptor(), TypeDescriptor.valueOf(targetType)); + } + if (ClassUtils.isAssignableValue(targetType, value)) { + return (T) value; + } + throw new EvaluationException("Cannot convert value '" + value + "' to type '" + targetType.getName() + "'"); + } + + /** + * Attempt to convert a typed value to an int using the supplied type converter. + */ + public static int toInt(TypeConverter typeConverter, TypedValue typedValue) { + return convertValue(typeConverter, typedValue, Integer.class); + } + + /** + * Attempt to convert a typed value to a boolean using the supplied type converter. + */ + public static boolean toBoolean(TypeConverter typeConverter, TypedValue typedValue) { + return convertValue(typeConverter, typedValue, Boolean.class); + } + + /** + * Attempt to convert a typed value to a double using the supplied type converter. + */ + public static double toDouble(TypeConverter typeConverter, TypedValue typedValue) { + return convertValue(typeConverter, typedValue, Double.class); + } + + /** + * Attempt to convert a typed value to a long using the supplied type converter. + */ + public static long toLong(TypeConverter typeConverter, TypedValue typedValue) { + return convertValue(typeConverter, typedValue, Long.class); + } + + /** + * Attempt to convert a typed value to a char using the supplied type converter. + */ + public static char toChar(TypeConverter typeConverter, TypedValue typedValue) { + return convertValue(typeConverter, typedValue, Character.class); + } + + /** + * Attempt to convert a typed value to a short using the supplied type converter. + */ + public static short toShort(TypeConverter typeConverter, TypedValue typedValue) { + return convertValue(typeConverter, typedValue, Short.class); + } + + /** + * Attempt to convert a typed value to a float using the supplied type converter. + */ + public static float toFloat(TypeConverter typeConverter, TypedValue typedValue) { + return convertValue(typeConverter, typedValue, Float.class); + } + + /** + * Attempt to convert a typed value to a byte using the supplied type converter. + */ + public static byte toByte(TypeConverter typeConverter, TypedValue typedValue) { + return convertValue(typeConverter, typedValue, Byte.class); + } + + @SuppressWarnings("unchecked") + private static T convertValue(TypeConverter typeConverter, TypedValue typedValue, Class targetType) { + Object result = typeConverter.convertValue(typedValue.getValue(), typedValue.getTypeDescriptor(), + TypeDescriptor.valueOf(targetType)); + if (result == null) { + throw new IllegalStateException("Null conversion result for value [" + typedValue.getValue() + "]"); + } + return (T) result; + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/common/LiteralExpression.java b/spring-expression/src/main/java/org/springframework/expression/common/LiteralExpression.java new file mode 100644 index 0000000..07c64bd --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/common/LiteralExpression.java @@ -0,0 +1,174 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.common; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Expression; +import org.springframework.expression.TypedValue; +import org.springframework.lang.Nullable; + +/** + * A very simple hardcoded implementation of the Expression interface that represents a + * string literal. It is used with CompositeStringExpression when representing a template + * expression which is made up of pieces - some being real expressions to be handled by + * an EL implementation like SpEL, and some being just textual elements. + * + * @author Andy Clement + * @author Juergen Hoeller + * @since 3.0 + */ +public class LiteralExpression implements Expression { + + /** Fixed literal value of this expression. */ + private final String literalValue; + + + public LiteralExpression(String literalValue) { + this.literalValue = literalValue; + } + + + @Override + public final String getExpressionString() { + return this.literalValue; + } + + @Override + public Class getValueType(EvaluationContext context) { + return String.class; + } + + @Override + public String getValue() { + return this.literalValue; + } + + @Override + @Nullable + public T getValue(@Nullable Class expectedResultType) throws EvaluationException { + Object value = getValue(); + return ExpressionUtils.convertTypedValue(null, new TypedValue(value), expectedResultType); + } + + @Override + public String getValue(@Nullable Object rootObject) { + return this.literalValue; + } + + @Override + @Nullable + public T getValue(@Nullable Object rootObject, @Nullable Class desiredResultType) throws EvaluationException { + Object value = getValue(rootObject); + return ExpressionUtils.convertTypedValue(null, new TypedValue(value), desiredResultType); + } + + @Override + public String getValue(EvaluationContext context) { + return this.literalValue; + } + + @Override + @Nullable + public T getValue(EvaluationContext context, @Nullable Class expectedResultType) + throws EvaluationException { + + Object value = getValue(context); + return ExpressionUtils.convertTypedValue(context, new TypedValue(value), expectedResultType); + } + + @Override + public String getValue(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException { + return this.literalValue; + } + + @Override + @Nullable + public T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable Class desiredResultType) + throws EvaluationException { + + Object value = getValue(context, rootObject); + return ExpressionUtils.convertTypedValue(context, new TypedValue(value), desiredResultType); + } + + @Override + public Class getValueType() { + return String.class; + } + + @Override + public Class getValueType(@Nullable Object rootObject) throws EvaluationException { + return String.class; + } + + @Override + public Class getValueType(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException { + return String.class; + } + + @Override + public TypeDescriptor getValueTypeDescriptor() { + return TypeDescriptor.valueOf(String.class); + } + + @Override + public TypeDescriptor getValueTypeDescriptor(@Nullable Object rootObject) throws EvaluationException { + return TypeDescriptor.valueOf(String.class); + } + + @Override + public TypeDescriptor getValueTypeDescriptor(EvaluationContext context) { + return TypeDescriptor.valueOf(String.class); + } + + @Override + public TypeDescriptor getValueTypeDescriptor(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException { + return TypeDescriptor.valueOf(String.class); + } + + @Override + public boolean isWritable(@Nullable Object rootObject) throws EvaluationException { + return false; + } + + @Override + public boolean isWritable(EvaluationContext context) { + return false; + } + + @Override + public boolean isWritable(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException { + return false; + } + + @Override + public void setValue(@Nullable Object rootObject, @Nullable Object value) throws EvaluationException { + throw new EvaluationException(this.literalValue, "Cannot call setValue() on a LiteralExpression"); + } + + @Override + public void setValue(EvaluationContext context, @Nullable Object value) throws EvaluationException { + throw new EvaluationException(this.literalValue, "Cannot call setValue() on a LiteralExpression"); + } + + @Override + public void setValue(EvaluationContext context, @Nullable Object rootObject, @Nullable Object value) throws EvaluationException { + throw new EvaluationException(this.literalValue, "Cannot call setValue() on a LiteralExpression"); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/common/TemplateAwareExpressionParser.java b/spring-expression/src/main/java/org/springframework/expression/common/TemplateAwareExpressionParser.java new file mode 100644 index 0000000..58bae54 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/common/TemplateAwareExpressionParser.java @@ -0,0 +1,287 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.common; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; + +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.ParseException; +import org.springframework.expression.ParserContext; +import org.springframework.lang.Nullable; + +/** + * An expression parser that understands templates. It can be subclassed by expression + * parsers that do not offer first class support for templating. + * + * @author Keith Donald + * @author Juergen Hoeller + * @author Andy Clement + * @since 3.0 + */ +public abstract class TemplateAwareExpressionParser implements ExpressionParser { + + @Override + public Expression parseExpression(String expressionString) throws ParseException { + return parseExpression(expressionString, null); + } + + @Override + public Expression parseExpression(String expressionString, @Nullable ParserContext context) throws ParseException { + if (context != null && context.isTemplate()) { + return parseTemplate(expressionString, context); + } + else { + return doParseExpression(expressionString, context); + } + } + + + private Expression parseTemplate(String expressionString, ParserContext context) throws ParseException { + if (expressionString.isEmpty()) { + return new LiteralExpression(""); + } + + Expression[] expressions = parseExpressions(expressionString, context); + if (expressions.length == 1) { + return expressions[0]; + } + else { + return new CompositeStringExpression(expressionString, expressions); + } + } + + /** + * Helper that parses given expression string using the configured parser. The + * expression string can contain any number of expressions all contained in "${...}" + * markers. For instance: "foo${expr0}bar${expr1}". The static pieces of text will + * also be returned as Expressions that just return that static piece of text. As a + * result, evaluating all returned expressions and concatenating the results produces + * the complete evaluated string. Unwrapping is only done of the outermost delimiters + * found, so the string 'hello ${foo${abc}}' would break into the pieces 'hello ' and + * 'foo${abc}'. This means that expression languages that used ${..} as part of their + * functionality are supported without any problem. The parsing is aware of the + * structure of an embedded expression. It assumes that parentheses '(', square + * brackets '[' and curly brackets '}' must be in pairs within the expression unless + * they are within a string literal and a string literal starts and terminates with a + * single quote '. + * @param expressionString the expression string + * @return the parsed expressions + * @throws ParseException when the expressions cannot be parsed + */ + private Expression[] parseExpressions(String expressionString, ParserContext context) throws ParseException { + List expressions = new ArrayList<>(); + String prefix = context.getExpressionPrefix(); + String suffix = context.getExpressionSuffix(); + int startIdx = 0; + + while (startIdx < expressionString.length()) { + int prefixIndex = expressionString.indexOf(prefix, startIdx); + if (prefixIndex >= startIdx) { + // an inner expression was found - this is a composite + if (prefixIndex > startIdx) { + expressions.add(new LiteralExpression(expressionString.substring(startIdx, prefixIndex))); + } + int afterPrefixIndex = prefixIndex + prefix.length(); + int suffixIndex = skipToCorrectEndSuffix(suffix, expressionString, afterPrefixIndex); + if (suffixIndex == -1) { + throw new ParseException(expressionString, prefixIndex, + "No ending suffix '" + suffix + "' for expression starting at character " + + prefixIndex + ": " + expressionString.substring(prefixIndex)); + } + if (suffixIndex == afterPrefixIndex) { + throw new ParseException(expressionString, prefixIndex, + "No expression defined within delimiter '" + prefix + suffix + + "' at character " + prefixIndex); + } + String expr = expressionString.substring(prefixIndex + prefix.length(), suffixIndex); + expr = expr.trim(); + if (expr.isEmpty()) { + throw new ParseException(expressionString, prefixIndex, + "No expression defined within delimiter '" + prefix + suffix + + "' at character " + prefixIndex); + } + expressions.add(doParseExpression(expr, context)); + startIdx = suffixIndex + suffix.length(); + } + else { + // no more ${expressions} found in string, add rest as static text + expressions.add(new LiteralExpression(expressionString.substring(startIdx))); + startIdx = expressionString.length(); + } + } + + return expressions.toArray(new Expression[0]); + } + + /** + * Return true if the specified suffix can be found at the supplied position in the + * supplied expression string. + * @param expressionString the expression string which may contain the suffix + * @param pos the start position at which to check for the suffix + * @param suffix the suffix string + */ + private boolean isSuffixHere(String expressionString, int pos, String suffix) { + int suffixPosition = 0; + for (int i = 0; i < suffix.length() && pos < expressionString.length(); i++) { + if (expressionString.charAt(pos++) != suffix.charAt(suffixPosition++)) { + return false; + } + } + if (suffixPosition != suffix.length()) { + // the expressionString ran out before the suffix could entirely be found + return false; + } + return true; + } + + /** + * Copes with nesting, for example '${...${...}}' where the correct end for the first + * ${ is the final }. + * @param suffix the suffix + * @param expressionString the expression string + * @param afterPrefixIndex the most recently found prefix location for which the + * matching end suffix is being sought + * @return the position of the correct matching nextSuffix or -1 if none can be found + */ + private int skipToCorrectEndSuffix(String suffix, String expressionString, int afterPrefixIndex) + throws ParseException { + + // Chew on the expression text - relying on the rules: + // brackets must be in pairs: () [] {} + // string literals are "..." or '...' and these may contain unmatched brackets + int pos = afterPrefixIndex; + int maxlen = expressionString.length(); + int nextSuffix = expressionString.indexOf(suffix, afterPrefixIndex); + if (nextSuffix == -1) { + return -1; // the suffix is missing + } + Deque stack = new ArrayDeque<>(); + while (pos < maxlen) { + if (isSuffixHere(expressionString, pos, suffix) && stack.isEmpty()) { + break; + } + char ch = expressionString.charAt(pos); + switch (ch) { + case '{': + case '[': + case '(': + stack.push(new Bracket(ch, pos)); + break; + case '}': + case ']': + case ')': + if (stack.isEmpty()) { + throw new ParseException(expressionString, pos, "Found closing '" + ch + + "' at position " + pos + " without an opening '" + + Bracket.theOpenBracketFor(ch) + "'"); + } + Bracket p = stack.pop(); + if (!p.compatibleWithCloseBracket(ch)) { + throw new ParseException(expressionString, pos, "Found closing '" + ch + + "' at position " + pos + " but most recent opening is '" + p.bracket + + "' at position " + p.pos); + } + break; + case '\'': + case '"': + // jump to the end of the literal + int endLiteral = expressionString.indexOf(ch, pos + 1); + if (endLiteral == -1) { + throw new ParseException(expressionString, pos, + "Found non terminating string literal starting at position " + pos); + } + pos = endLiteral; + break; + } + pos++; + } + if (!stack.isEmpty()) { + Bracket p = stack.pop(); + throw new ParseException(expressionString, p.pos, "Missing closing '" + + Bracket.theCloseBracketFor(p.bracket) + "' for '" + p.bracket + "' at position " + p.pos); + } + if (!isSuffixHere(expressionString, pos, suffix)) { + return -1; + } + return pos; + } + + + /** + * Actually parse the expression string and return an Expression object. + * @param expressionString the raw expression string to parse + * @param context a context for influencing this expression parsing routine (optional) + * @return an evaluator for the parsed expression + * @throws ParseException an exception occurred during parsing + */ + protected abstract Expression doParseExpression(String expressionString, @Nullable ParserContext context) + throws ParseException; + + + /** + * This captures a type of bracket and the position in which it occurs in the + * expression. The positional information is used if an error has to be reported + * because the related end bracket cannot be found. Bracket is used to describe: + * square brackets [] round brackets () and curly brackets {} + */ + private static class Bracket { + + char bracket; + + int pos; + + Bracket(char bracket, int pos) { + this.bracket = bracket; + this.pos = pos; + } + + boolean compatibleWithCloseBracket(char closeBracket) { + if (this.bracket == '{') { + return closeBracket == '}'; + } + else if (this.bracket == '[') { + return closeBracket == ']'; + } + return closeBracket == ')'; + } + + static char theOpenBracketFor(char closeBracket) { + if (closeBracket == '}') { + return '{'; + } + else if (closeBracket == ']') { + return '['; + } + return '('; + } + + static char theCloseBracketFor(char openBracket) { + if (openBracket == '{') { + return '}'; + } + else if (openBracket == '[') { + return ']'; + } + return ')'; + } + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/common/TemplateParserContext.java b/spring-expression/src/main/java/org/springframework/expression/common/TemplateParserContext.java new file mode 100644 index 0000000..24a2b82 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/common/TemplateParserContext.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.common; + +import org.springframework.expression.ParserContext; + +/** + * Configurable {@link ParserContext} implementation for template parsing. Expects the + * expression prefix and suffix as constructor arguments. + * + * @author Juergen Hoeller + * @since 3.0 + */ +public class TemplateParserContext implements ParserContext { + + private final String expressionPrefix; + + private final String expressionSuffix; + + + /** + * Create a new TemplateParserContext with the default "#{" prefix and "}" suffix. + */ + public TemplateParserContext() { + this("#{", "}"); + } + + /** + * Create a new TemplateParserContext for the given prefix and suffix. + * @param expressionPrefix the expression prefix to use + * @param expressionSuffix the expression suffix to use + */ + public TemplateParserContext(String expressionPrefix, String expressionSuffix) { + this.expressionPrefix = expressionPrefix; + this.expressionSuffix = expressionSuffix; + } + + + @Override + public final boolean isTemplate() { + return true; + } + + @Override + public final String getExpressionPrefix() { + return this.expressionPrefix; + } + + @Override + public final String getExpressionSuffix() { + return this.expressionSuffix; + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/common/package-info.java b/spring-expression/src/main/java/org/springframework/expression/common/package-info.java new file mode 100644 index 0000000..080af63 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/common/package-info.java @@ -0,0 +1,9 @@ +/** + * Common utility classes behind the Spring Expression Language. + */ +@NonNullApi +@NonNullFields +package org.springframework.expression.common; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-expression/src/main/java/org/springframework/expression/package-info.java b/spring-expression/src/main/java/org/springframework/expression/package-info.java new file mode 100644 index 0000000..48a91d1 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/package-info.java @@ -0,0 +1,9 @@ +/** + * Core abstractions behind the Spring Expression Language. + */ +@NonNullApi +@NonNullFields +package org.springframework.expression; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/CodeFlow.java b/spring-expression/src/main/java/org/springframework/expression/spel/CodeFlow.java new file mode 100644 index 0000000..cce0582 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/CodeFlow.java @@ -0,0 +1,1063 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; + +import org.springframework.asm.ClassWriter; +import org.springframework.asm.MethodVisitor; +import org.springframework.asm.Opcodes; +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; + +/** + * Manages the class being generated by the compilation process. + * + *

    Records intermediate compilation state as the bytecode is generated. + * Also includes various bytecode generation helper functions. + * + * @author Andy Clement + * @author Juergen Hoeller + * @since 4.1 + */ +public class CodeFlow implements Opcodes { + + /** + * Name of the class being generated. Typically used when generating code + * that accesses freshly generated fields on the generated type. + */ + private final String className; + + /** + * The current class being generated. + */ + private final ClassWriter classWriter; + + /** + * Record the type of what is on top of the bytecode stack (i.e. the type of the + * output from the previous expression component). New scopes are used to evaluate + * sub-expressions like the expressions for the argument values in a method invocation + * expression. + */ + private final Deque> compilationScopes; + + /** + * As SpEL ast nodes are called to generate code for the main evaluation method + * they can register to add a field to this class. Any registered FieldAdders + * will be called after the main evaluation function has finished being generated. + */ + @Nullable + private List fieldAdders; + + /** + * As SpEL ast nodes are called to generate code for the main evaluation method + * they can register to add code to a static initializer in the class. Any + * registered ClinitAdders will be called after the main evaluation function + * has finished being generated. + */ + @Nullable + private List clinitAdders; + + /** + * When code generation requires holding a value in a class level field, this + * is used to track the next available field id (used as a name suffix). + */ + private int nextFieldId = 1; + + /** + * When code generation requires an intermediate variable within a method, + * this method records the next available variable (variable 0 is 'this'). + */ + private int nextFreeVariableId = 1; + + + /** + * Construct a new {@code CodeFlow} for the given class. + * @param className the name of the class + * @param classWriter the corresponding ASM {@code ClassWriter} + */ + public CodeFlow(String className, ClassWriter classWriter) { + this.className = className; + this.classWriter = classWriter; + this.compilationScopes = new ArrayDeque<>(); + this.compilationScopes.add(new ArrayList()); + } + + + /** + * Push the byte code to load the target (i.e. what was passed as the first argument + * to CompiledExpression.getValue(target, context)) + * @param mv the visitor into which the load instruction should be inserted + */ + public void loadTarget(MethodVisitor mv) { + mv.visitVarInsn(ALOAD, 1); + } + + /** + * Push the bytecode to load the EvaluationContext (the second parameter passed to + * the compiled expression method). + * @param mv the visitor into which the load instruction should be inserted + * @since 4.3.4 + */ + public void loadEvaluationContext(MethodVisitor mv) { + mv.visitVarInsn(ALOAD, 2); + } + + /** + * Record the descriptor for the most recently evaluated expression element. + * @param descriptor type descriptor for most recently evaluated element + */ + public void pushDescriptor(@Nullable String descriptor) { + if (descriptor != null) { + this.compilationScopes.element().add(descriptor); + } + } + + /** + * Enter a new compilation scope, usually due to nested expression evaluation. For + * example when the arguments for a method invocation expression are being evaluated, + * each argument will be evaluated in a new scope. + */ + public void enterCompilationScope() { + this.compilationScopes.push(new ArrayList<>()); + } + + /** + * Exit a compilation scope, usually after a nested expression has been evaluated. For + * example after an argument for a method invocation has been evaluated this method + * returns us to the previous (outer) scope. + */ + public void exitCompilationScope() { + this.compilationScopes.pop(); + } + + /** + * Return the descriptor for the item currently on top of the stack (in the current scope). + */ + @Nullable + public String lastDescriptor() { + return CollectionUtils.lastElement(this.compilationScopes.peek()); + } + + /** + * If the codeflow shows the last expression evaluated to java.lang.Boolean then + * insert the necessary instructions to unbox that to a boolean primitive. + * @param mv the visitor into which new instructions should be inserted + */ + public void unboxBooleanIfNecessary(MethodVisitor mv) { + if ("Ljava/lang/Boolean".equals(lastDescriptor())) { + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Boolean", "booleanValue", "()Z", false); + } + } + + /** + * Called after the main expression evaluation method has been generated, this + * method will callback any registered FieldAdders or ClinitAdders to add any + * extra information to the class representing the compiled expression. + */ + public void finish() { + if (this.fieldAdders != null) { + for (FieldAdder fieldAdder : this.fieldAdders) { + fieldAdder.generateField(this.classWriter, this); + } + } + if (this.clinitAdders != null) { + MethodVisitor mv = this.classWriter.visitMethod(ACC_PUBLIC | ACC_STATIC, "", "()V", null, null); + mv.visitCode(); + this.nextFreeVariableId = 0; // to 0 because there is no 'this' in a clinit + for (ClinitAdder clinitAdder : this.clinitAdders) { + clinitAdder.generateCode(mv, this); + } + mv.visitInsn(RETURN); + mv.visitMaxs(0,0); // not supplied due to COMPUTE_MAXS + mv.visitEnd(); + } + } + + /** + * Register a FieldAdder which will add a new field to the generated + * class to support the code produced by an ast nodes primary + * generateCode() method. + */ + public void registerNewField(FieldAdder fieldAdder) { + if (this.fieldAdders == null) { + this.fieldAdders = new ArrayList<>(); + } + this.fieldAdders.add(fieldAdder); + } + + /** + * Register a ClinitAdder which will add code to the static + * initializer in the generated class to support the code + * produced by an ast nodes primary generateCode() method. + */ + public void registerNewClinit(ClinitAdder clinitAdder) { + if (this.clinitAdders == null) { + this.clinitAdders = new ArrayList<>(); + } + this.clinitAdders.add(clinitAdder); + } + + public int nextFieldId() { + return this.nextFieldId++; + } + + public int nextFreeVariableId() { + return this.nextFreeVariableId++; + } + + public String getClassName() { + return this.className; + } + + + /** + * Insert any necessary cast and value call to convert from a boxed type to a + * primitive value. + * @param mv the method visitor into which instructions should be inserted + * @param ch the primitive type desired as output + * @param stackDescriptor the descriptor of the type on top of the stack + */ + public static void insertUnboxInsns(MethodVisitor mv, char ch, @Nullable String stackDescriptor) { + if (stackDescriptor == null) { + return; + } + switch (ch) { + case 'Z': + if (!stackDescriptor.equals("Ljava/lang/Boolean")) { + mv.visitTypeInsn(CHECKCAST, "java/lang/Boolean"); + } + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Boolean", "booleanValue", "()Z", false); + break; + case 'B': + if (!stackDescriptor.equals("Ljava/lang/Byte")) { + mv.visitTypeInsn(CHECKCAST, "java/lang/Byte"); + } + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Byte", "byteValue", "()B", false); + break; + case 'C': + if (!stackDescriptor.equals("Ljava/lang/Character")) { + mv.visitTypeInsn(CHECKCAST, "java/lang/Character"); + } + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Character", "charValue", "()C", false); + break; + case 'D': + if (!stackDescriptor.equals("Ljava/lang/Double")) { + mv.visitTypeInsn(CHECKCAST, "java/lang/Double"); + } + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Double", "doubleValue", "()D", false); + break; + case 'F': + if (!stackDescriptor.equals("Ljava/lang/Float")) { + mv.visitTypeInsn(CHECKCAST, "java/lang/Float"); + } + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Float", "floatValue", "()F", false); + break; + case 'I': + if (!stackDescriptor.equals("Ljava/lang/Integer")) { + mv.visitTypeInsn(CHECKCAST, "java/lang/Integer"); + } + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I", false); + break; + case 'J': + if (!stackDescriptor.equals("Ljava/lang/Long")) { + mv.visitTypeInsn(CHECKCAST, "java/lang/Long"); + } + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "longValue", "()J", false); + break; + case 'S': + if (!stackDescriptor.equals("Ljava/lang/Short")) { + mv.visitTypeInsn(CHECKCAST, "java/lang/Short"); + } + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Short", "shortValue", "()S", false); + break; + default: + throw new IllegalArgumentException("Unboxing should not be attempted for descriptor '" + ch + "'"); + } + } + + /** + * For numbers, use the appropriate method on the number to convert it to the primitive type requested. + * @param mv the method visitor into which instructions should be inserted + * @param targetDescriptor the primitive type desired as output + * @param stackDescriptor the descriptor of the type on top of the stack + */ + public static void insertUnboxNumberInsns( + MethodVisitor mv, char targetDescriptor, @Nullable String stackDescriptor) { + + if (stackDescriptor == null) { + return; + } + + switch (targetDescriptor) { + case 'D': + if (stackDescriptor.equals("Ljava/lang/Object")) { + mv.visitTypeInsn(CHECKCAST, "java/lang/Number"); + } + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Number", "doubleValue", "()D", false); + break; + case 'F': + if (stackDescriptor.equals("Ljava/lang/Object")) { + mv.visitTypeInsn(CHECKCAST, "java/lang/Number"); + } + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Number", "floatValue", "()F", false); + break; + case 'J': + if (stackDescriptor.equals("Ljava/lang/Object")) { + mv.visitTypeInsn(CHECKCAST, "java/lang/Number"); + } + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Number", "longValue", "()J", false); + break; + case 'I': + if (stackDescriptor.equals("Ljava/lang/Object")) { + mv.visitTypeInsn(CHECKCAST, "java/lang/Number"); + } + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Number", "intValue", "()I", false); + break; + // does not handle Z, B, C, S + default: + throw new IllegalArgumentException("Unboxing should not be attempted for descriptor '" + targetDescriptor + "'"); + } + } + + /** + * Insert any necessary numeric conversion bytecodes based upon what is on the stack and the desired target type. + * @param mv the method visitor into which instructions should be placed + * @param targetDescriptor the (primitive) descriptor of the target type + * @param stackDescriptor the descriptor of the operand on top of the stack + */ + public static void insertAnyNecessaryTypeConversionBytecodes(MethodVisitor mv, char targetDescriptor, String stackDescriptor) { + if (CodeFlow.isPrimitive(stackDescriptor)) { + char stackTop = stackDescriptor.charAt(0); + if (stackTop == 'I' || stackTop == 'B' || stackTop == 'S' || stackTop == 'C') { + if (targetDescriptor == 'D') { + mv.visitInsn(I2D); + } + else if (targetDescriptor == 'F') { + mv.visitInsn(I2F); + } + else if (targetDescriptor == 'J') { + mv.visitInsn(I2L); + } + else if (targetDescriptor == 'I') { + // nop + } + else { + throw new IllegalStateException("Cannot get from " + stackTop + " to " + targetDescriptor); + } + } + else if (stackTop == 'J') { + if (targetDescriptor == 'D') { + mv.visitInsn(L2D); + } + else if (targetDescriptor == 'F') { + mv.visitInsn(L2F); + } + else if (targetDescriptor == 'J') { + // nop + } + else if (targetDescriptor == 'I') { + mv.visitInsn(L2I); + } + else { + throw new IllegalStateException("Cannot get from " + stackTop + " to " + targetDescriptor); + } + } + else if (stackTop == 'F') { + if (targetDescriptor == 'D') { + mv.visitInsn(F2D); + } + else if (targetDescriptor == 'F') { + // nop + } + else if (targetDescriptor == 'J') { + mv.visitInsn(F2L); + } + else if (targetDescriptor == 'I') { + mv.visitInsn(F2I); + } + else { + throw new IllegalStateException("Cannot get from " + stackTop + " to " + targetDescriptor); + } + } + else if (stackTop == 'D') { + if (targetDescriptor == 'D') { + // nop + } + else if (targetDescriptor == 'F') { + mv.visitInsn(D2F); + } + else if (targetDescriptor == 'J') { + mv.visitInsn(D2L); + } + else if (targetDescriptor == 'I') { + mv.visitInsn(D2I); + } + else { + throw new IllegalStateException("Cannot get from " + stackDescriptor + " to " + targetDescriptor); + } + } + } + } + + + /** + * Create the JVM signature descriptor for a method. This consists of the descriptors + * for the method parameters surrounded with parentheses, followed by the + * descriptor for the return type. Note the descriptors here are JVM descriptors, + * unlike the other descriptor forms the compiler is using which do not include the + * trailing semicolon. + * @param method the method + * @return a String signature descriptor (e.g. "(ILjava/lang/String;)V") + */ + public static String createSignatureDescriptor(Method method) { + Class[] params = method.getParameterTypes(); + StringBuilder sb = new StringBuilder(); + sb.append("("); + for (Class param : params) { + sb.append(toJvmDescriptor(param)); + } + sb.append(")"); + sb.append(toJvmDescriptor(method.getReturnType())); + return sb.toString(); + } + + /** + * Create the JVM signature descriptor for a constructor. This consists of the + * descriptors for the constructor parameters surrounded with parentheses, followed by + * the descriptor for the return type, which is always "V". Note the + * descriptors here are JVM descriptors, unlike the other descriptor forms the + * compiler is using which do not include the trailing semicolon. + * @param ctor the constructor + * @return a String signature descriptor (e.g. "(ILjava/lang/String;)V") + */ + public static String createSignatureDescriptor(Constructor ctor) { + Class[] params = ctor.getParameterTypes(); + StringBuilder sb = new StringBuilder(); + sb.append("("); + for (Class param : params) { + sb.append(toJvmDescriptor(param)); + } + sb.append(")V"); + return sb.toString(); + } + + /** + * Determine the JVM descriptor for a specified class. Unlike the other descriptors + * used in the compilation process, this is the one the JVM wants, so this one + * includes any necessary trailing semicolon (e.g. Ljava/lang/String; rather than + * Ljava/lang/String) + * @param clazz a class + * @return the JVM descriptor for the class + */ + public static String toJvmDescriptor(Class clazz) { + StringBuilder sb = new StringBuilder(); + if (clazz.isArray()) { + while (clazz.isArray()) { + sb.append("["); + clazz = clazz.getComponentType(); + } + } + if (clazz.isPrimitive()) { + if (clazz == Boolean.TYPE) { + sb.append('Z'); + } + else if (clazz == Byte.TYPE) { + sb.append('B'); + } + else if (clazz == Character.TYPE) { + sb.append('C'); + } + else if (clazz == Double.TYPE) { + sb.append('D'); + } + else if (clazz == Float.TYPE) { + sb.append('F'); + } + else if (clazz == Integer.TYPE) { + sb.append('I'); + } + else if (clazz == Long.TYPE) { + sb.append('J'); + } + else if (clazz == Short.TYPE) { + sb.append('S'); + } + else if (clazz == Void.TYPE) { + sb.append('V'); + } + } + else { + sb.append("L"); + sb.append(clazz.getName().replace('.', '/')); + sb.append(";"); + } + return sb.toString(); + } + + /** + * Determine the descriptor for an object instance (or {@code null}). + * @param value an object (possibly {@code null}) + * @return the type descriptor for the object + * (descriptor is "Ljava/lang/Object" for {@code null} value) + */ + public static String toDescriptorFromObject(@Nullable Object value) { + if (value == null) { + return "Ljava/lang/Object"; + } + else { + return toDescriptor(value.getClass()); + } + } + + /** + * Determine whether the descriptor is for a boolean primitive or boolean reference type. + * @param descriptor type descriptor + * @return {@code true} if the descriptor is boolean compatible + */ + public static boolean isBooleanCompatible(@Nullable String descriptor) { + return (descriptor != null && (descriptor.equals("Z") || descriptor.equals("Ljava/lang/Boolean"))); + } + + /** + * Determine whether the descriptor is for a primitive type. + * @param descriptor type descriptor + * @return {@code true} if a primitive type + */ + public static boolean isPrimitive(@Nullable String descriptor) { + return (descriptor != null && descriptor.length() == 1); + } + + /** + * Determine whether the descriptor is for a primitive array (e.g. "[[I"). + * @param descriptor the descriptor for a possible primitive array + * @return {@code true} if the descriptor a primitive array + */ + public static boolean isPrimitiveArray(@Nullable String descriptor) { + if (descriptor == null) { + return false; + } + boolean primitive = true; + for (int i = 0, max = descriptor.length(); i < max; i++) { + char ch = descriptor.charAt(i); + if (ch == '[') { + continue; + } + primitive = (ch != 'L'); + break; + } + return primitive; + } + + /** + * Determine whether boxing/unboxing can get from one type to the other. + * Assumes at least one of the types is in boxed form (i.e. single char descriptor). + * @return {@code true} if it is possible to get (via boxing) from one descriptor to the other + */ + public static boolean areBoxingCompatible(String desc1, String desc2) { + if (desc1.equals(desc2)) { + return true; + } + if (desc1.length() == 1) { + if (desc1.equals("Z")) { + return desc2.equals("Ljava/lang/Boolean"); + } + else if (desc1.equals("D")) { + return desc2.equals("Ljava/lang/Double"); + } + else if (desc1.equals("F")) { + return desc2.equals("Ljava/lang/Float"); + } + else if (desc1.equals("I")) { + return desc2.equals("Ljava/lang/Integer"); + } + else if (desc1.equals("J")) { + return desc2.equals("Ljava/lang/Long"); + } + } + else if (desc2.length() == 1) { + if (desc2.equals("Z")) { + return desc1.equals("Ljava/lang/Boolean"); + } + else if (desc2.equals("D")) { + return desc1.equals("Ljava/lang/Double"); + } + else if (desc2.equals("F")) { + return desc1.equals("Ljava/lang/Float"); + } + else if (desc2.equals("I")) { + return desc1.equals("Ljava/lang/Integer"); + } + else if (desc2.equals("J")) { + return desc1.equals("Ljava/lang/Long"); + } + } + return false; + } + + /** + * Determine if the supplied descriptor is for a supported number type or boolean. The + * compilation process only (currently) supports certain number types. These are + * double, float, long and int. + * @param descriptor the descriptor for a type + * @return {@code true} if the descriptor is for a supported numeric type or boolean + */ + public static boolean isPrimitiveOrUnboxableSupportedNumberOrBoolean(@Nullable String descriptor) { + if (descriptor == null) { + return false; + } + if (isPrimitiveOrUnboxableSupportedNumber(descriptor)) { + return true; + } + return ("Z".equals(descriptor) || descriptor.equals("Ljava/lang/Boolean")); + } + + /** + * Determine if the supplied descriptor is for a supported number. The compilation + * process only (currently) supports certain number types. These are double, float, + * long and int. + * @param descriptor the descriptor for a type + * @return {@code true} if the descriptor is for a supported numeric type + */ + public static boolean isPrimitiveOrUnboxableSupportedNumber(@Nullable String descriptor) { + if (descriptor == null) { + return false; + } + if (descriptor.length() == 1) { + return "DFIJ".contains(descriptor); + } + if (descriptor.startsWith("Ljava/lang/")) { + String name = descriptor.substring("Ljava/lang/".length()); + if (name.equals("Double") || name.equals("Float") || name.equals("Integer") || name.equals("Long")) { + return true; + } + } + return false; + } + + /** + * Determine whether the given number is to be considered as an integer + * for the purposes of a numeric operation at the bytecode level. + * @param number the number to check + * @return {@code true} if it is an {@link Integer}, {@link Short} or {@link Byte} + */ + public static boolean isIntegerForNumericOp(Number number) { + return (number instanceof Integer || number instanceof Short || number instanceof Byte); + } + + /** + * Convert a type descriptor to the single character primitive descriptor. + * @param descriptor a descriptor for a type that should have a primitive representation + * @return the single character descriptor for a primitive input descriptor + */ + public static char toPrimitiveTargetDesc(String descriptor) { + if (descriptor.length() == 1) { + return descriptor.charAt(0); + } + else if (descriptor.equals("Ljava/lang/Boolean")) { + return 'Z'; + } + else if (descriptor.equals("Ljava/lang/Byte")) { + return 'B'; + } + else if (descriptor.equals("Ljava/lang/Character")) { + return 'C'; + } + else if (descriptor.equals("Ljava/lang/Double")) { + return 'D'; + } + else if (descriptor.equals("Ljava/lang/Float")) { + return 'F'; + } + else if (descriptor.equals("Ljava/lang/Integer")) { + return 'I'; + } + else if (descriptor.equals("Ljava/lang/Long")) { + return 'J'; + } + else if (descriptor.equals("Ljava/lang/Short")) { + return 'S'; + } + else { + throw new IllegalStateException("No primitive for '" + descriptor + "'"); + } + } + + /** + * Insert the appropriate CHECKCAST instruction for the supplied descriptor. + * @param mv the target visitor into which the instruction should be inserted + * @param descriptor the descriptor of the type to cast to + */ + public static void insertCheckCast(MethodVisitor mv, @Nullable String descriptor) { + if (descriptor != null && descriptor.length() != 1) { + if (descriptor.charAt(0) == '[') { + if (isPrimitiveArray(descriptor)) { + mv.visitTypeInsn(CHECKCAST, descriptor); + } + else { + mv.visitTypeInsn(CHECKCAST, descriptor + ";"); + } + } + else { + if (!descriptor.equals("Ljava/lang/Object")) { + // This is chopping off the 'L' to leave us with "java/lang/String" + mv.visitTypeInsn(CHECKCAST, descriptor.substring(1)); + } + } + } + } + + /** + * Determine the appropriate boxing instruction for a specific type (if it needs + * boxing) and insert the instruction into the supplied visitor. + * @param mv the target visitor for the new instructions + * @param descriptor the descriptor of a type that may or may not need boxing + */ + public static void insertBoxIfNecessary(MethodVisitor mv, @Nullable String descriptor) { + if (descriptor != null && descriptor.length() == 1) { + insertBoxIfNecessary(mv, descriptor.charAt(0)); + } + } + + /** + * Determine the appropriate boxing instruction for a specific type (if it needs + * boxing) and insert the instruction into the supplied visitor. + * @param mv the target visitor for the new instructions + * @param ch the descriptor of the type that might need boxing + */ + public static void insertBoxIfNecessary(MethodVisitor mv, char ch) { + switch (ch) { + case 'Z': + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;", false); + break; + case 'B': + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Byte", "valueOf", "(B)Ljava/lang/Byte;", false); + break; + case 'C': + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Character", "valueOf", "(C)Ljava/lang/Character;", false); + break; + case 'D': + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Double", "valueOf", "(D)Ljava/lang/Double;", false); + break; + case 'F': + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Float", "valueOf", "(F)Ljava/lang/Float;", false); + break; + case 'I': + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false); + break; + case 'J': + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false); + break; + case 'S': + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Short", "valueOf", "(S)Ljava/lang/Short;", false); + break; + case 'L': + case 'V': + case '[': + // no box needed + break; + default: + throw new IllegalArgumentException("Boxing should not be attempted for descriptor '" + ch + "'"); + } + } + + /** + * Deduce the descriptor for a type. Descriptors are like JVM type names but missing the + * trailing ';' so for Object the descriptor is "Ljava/lang/Object" for int it is "I". + * @param type the type (may be primitive) for which to determine the descriptor + * @return the descriptor + */ + public static String toDescriptor(Class type) { + String name = type.getName(); + if (type.isPrimitive()) { + switch (name.length()) { + case 3: + return "I"; + case 4: + if (name.equals("byte")) { + return "B"; + } + else if (name.equals("char")) { + return "C"; + } + else if (name.equals("long")) { + return "J"; + } + else if (name.equals("void")) { + return "V"; + } + break; + case 5: + if (name.equals("float")) { + return "F"; + } + else if (name.equals("short")) { + return "S"; + } + break; + case 6: + if (name.equals("double")) { + return "D"; + } + break; + case 7: + if (name.equals("boolean")) { + return "Z"; + } + break; + } + } + else { + if (name.charAt(0) != '[') { + return "L" + type.getName().replace('.', '/'); + } + else { + if (name.endsWith(";")) { + return name.substring(0, name.length() - 1).replace('.', '/'); + } + else { + return name; // array has primitive component type + } + } + } + return ""; + } + + /** + * Create an array of descriptors representing the parameter types for the supplied + * method. Returns a zero sized array if there are no parameters. + * @param method a Method + * @return a String array of descriptors, one entry for each method parameter + */ + public static String[] toParamDescriptors(Method method) { + return toDescriptors(method.getParameterTypes()); + } + + /** + * Create an array of descriptors representing the parameter types for the supplied + * constructor. Returns a zero sized array if there are no parameters. + * @param ctor a Constructor + * @return a String array of descriptors, one entry for each constructor parameter + */ + public static String[] toParamDescriptors(Constructor ctor) { + return toDescriptors(ctor.getParameterTypes()); + } + + /** + * Create an array of descriptors from an array of classes. + * @param types the input array of classes + * @return an array of descriptors + */ + public static String[] toDescriptors(Class[] types) { + int typesCount = types.length; + String[] descriptors = new String[typesCount]; + for (int p = 0; p < typesCount; p++) { + descriptors[p] = toDescriptor(types[p]); + } + return descriptors; + } + + /** + * Create the optimal instruction for loading a number on the stack. + * @param mv where to insert the bytecode + * @param value the value to be loaded + */ + public static void insertOptimalLoad(MethodVisitor mv, int value) { + if (value < 6) { + mv.visitInsn(ICONST_0+value); + } + else if (value < Byte.MAX_VALUE) { + mv.visitIntInsn(BIPUSH, value); + } + else if (value < Short.MAX_VALUE) { + mv.visitIntInsn(SIPUSH, value); + } + else { + mv.visitLdcInsn(value); + } + } + + /** + * Produce appropriate bytecode to store a stack item in an array. The + * instruction to use varies depending on whether the type + * is a primitive or reference type. + * @param mv where to insert the bytecode + * @param arrayElementType the type of the array elements + */ + public static void insertArrayStore(MethodVisitor mv, String arrayElementType) { + if (arrayElementType.length()==1) { + switch (arrayElementType.charAt(0)) { + case 'I': + mv.visitInsn(IASTORE); + break; + case 'J': + mv.visitInsn(LASTORE); + break; + case 'F': + mv.visitInsn(FASTORE); + break; + case 'D': + mv.visitInsn(DASTORE); + break; + case 'B': + mv.visitInsn(BASTORE); + break; + case 'C': + mv.visitInsn(CASTORE); + break; + case 'S': + mv.visitInsn(SASTORE); + break; + case 'Z': + mv.visitInsn(BASTORE); + break; + default: + throw new IllegalArgumentException( + "Unexpected arraytype " + arrayElementType.charAt(0)); + } + } + else { + mv.visitInsn(AASTORE); + } + } + + /** + * Determine the appropriate T tag to use for the NEWARRAY bytecode. + * @param arraytype the array primitive component type + * @return the T tag to use for NEWARRAY + */ + public static int arrayCodeFor(String arraytype) { + switch (arraytype.charAt(0)) { + case 'I': return T_INT; + case 'J': return T_LONG; + case 'F': return T_FLOAT; + case 'D': return T_DOUBLE; + case 'B': return T_BYTE; + case 'C': return T_CHAR; + case 'S': return T_SHORT; + case 'Z': return T_BOOLEAN; + default: + throw new IllegalArgumentException("Unexpected arraytype " + arraytype.charAt(0)); + } + } + + /** + * Return if the supplied array type has a core component reference type. + */ + public static boolean isReferenceTypeArray(String arraytype) { + int length = arraytype.length(); + for (int i = 0; i < length; i++) { + char ch = arraytype.charAt(i); + if (ch == '[') { + continue; + } + return (ch == 'L'); + } + return false; + } + + /** + * Produce the correct bytecode to build an array. The opcode to use and the + * signature to pass along with the opcode can vary depending on the signature + * of the array type. + * @param mv the methodvisitor into which code should be inserted + * @param size the size of the array + * @param arraytype the type of the array + */ + public static void insertNewArrayCode(MethodVisitor mv, int size, String arraytype) { + insertOptimalLoad(mv, size); + if (arraytype.length() == 1) { + mv.visitIntInsn(NEWARRAY, CodeFlow.arrayCodeFor(arraytype)); + } + else { + if (arraytype.charAt(0) == '[') { + // Handling the nested array case here. + // If vararg is [[I then we want [I and not [I; + if (CodeFlow.isReferenceTypeArray(arraytype)) { + mv.visitTypeInsn(ANEWARRAY, arraytype + ";"); + } + else { + mv.visitTypeInsn(ANEWARRAY, arraytype); + } + } + else { + mv.visitTypeInsn(ANEWARRAY, arraytype.substring(1)); + } + } + } + + /** + * For use in mathematical operators, handles converting from a (possibly boxed) + * number on the stack to a primitive numeric type. + *

    For example, from a Integer to a double, just need to call 'Number.doubleValue()' + * but from an int to a double, need to use the bytecode 'i2d'. + * @param mv the method visitor when instructions should be appended + * @param stackDescriptor a descriptor of the operand on the stack + * @param targetDescriptor a primitive type descriptor + */ + public static void insertNumericUnboxOrPrimitiveTypeCoercion( + MethodVisitor mv, @Nullable String stackDescriptor, char targetDescriptor) { + + if (!CodeFlow.isPrimitive(stackDescriptor)) { + CodeFlow.insertUnboxNumberInsns(mv, targetDescriptor, stackDescriptor); + } + else { + CodeFlow.insertAnyNecessaryTypeConversionBytecodes(mv, targetDescriptor, stackDescriptor); + } + } + + public static String toBoxedDescriptor(String primitiveDescriptor) { + switch (primitiveDescriptor.charAt(0)) { + case 'I': return "Ljava/lang/Integer"; + case 'J': return "Ljava/lang/Long"; + case 'F': return "Ljava/lang/Float"; + case 'D': return "Ljava/lang/Double"; + case 'B': return "Ljava/lang/Byte"; + case 'C': return "Ljava/lang/Character"; + case 'S': return "Ljava/lang/Short"; + case 'Z': return "Ljava/lang/Boolean"; + default: + throw new IllegalArgumentException("Unexpected non primitive descriptor " + primitiveDescriptor); + } + } + + + /** + * Interface used to generate fields. + */ + @FunctionalInterface + public interface FieldAdder { + + void generateField(ClassWriter cw, CodeFlow codeflow); + } + + + /** + * Interface used to generate {@code clinit} static initializer blocks. + */ + @FunctionalInterface + public interface ClinitAdder { + + void generateCode(MethodVisitor mv, CodeFlow codeflow); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/CompilablePropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/spel/CompilablePropertyAccessor.java new file mode 100644 index 0000000..634267d --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/CompilablePropertyAccessor.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import org.springframework.asm.MethodVisitor; +import org.springframework.asm.Opcodes; +import org.springframework.expression.PropertyAccessor; + +/** + * A compilable property accessor is able to generate bytecode that represents + * the access operation, facilitating compilation to bytecode of expressions + * that use the accessor. + * + * @author Andy Clement + * @since 4.1 + */ +public interface CompilablePropertyAccessor extends PropertyAccessor, Opcodes { + + /** + * Return {@code true} if this property accessor is currently suitable for compilation. + */ + boolean isCompilable(); + + /** + * Return the type of the accessed property - may only be known once an access has occurred. + */ + Class getPropertyType(); + + /** + * Generate the bytecode the performs the access operation into the specified MethodVisitor + * using context information from the codeflow where necessary. + * @param propertyName the name of the property + * @param mv the Asm method visitor into which code should be generated + * @param cf the current state of the expression compiler + */ + void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf); + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/CompiledExpression.java b/spring-expression/src/main/java/org/springframework/expression/spel/CompiledExpression.java new file mode 100644 index 0000000..acab389 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/CompiledExpression.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.lang.Nullable; + +/** + * Base superclass for compiled expressions. Each generated compiled expression class + * will extend this class and implement the {@link #getValue} method. It is not intended + * to be subclassed by user code. + * + * @author Andy Clement + * @since 4.1 + */ +public abstract class CompiledExpression { + + /** + * Subclasses of CompiledExpression generated by SpelCompiler will provide an + * implementation of this method. + */ + public abstract Object getValue(@Nullable Object target, @Nullable EvaluationContext context) + throws EvaluationException; + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java b/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java new file mode 100644 index 0000000..252af44 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java @@ -0,0 +1,294 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import java.util.ArrayDeque; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Operation; +import org.springframework.expression.OperatorOverloader; +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.TypeComparator; +import org.springframework.expression.TypeConverter; +import org.springframework.expression.TypedValue; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * An ExpressionState is for maintaining per-expression-evaluation state, any changes to + * it are not seen by other expressions but it gives a place to hold local variables and + * for component expressions in a compound expression to communicate state. This is in + * contrast to the EvaluationContext, which is shared amongst expression evaluations, and + * any changes to it will be seen by other expressions or any code that chooses to ask + * questions of the context. + * + *

    It also acts as a place for to define common utility routines that the various AST + * nodes might need. + * + * @author Andy Clement + * @author Juergen Hoeller + * @since 3.0 + */ +public class ExpressionState { + + private final EvaluationContext relatedContext; + + private final TypedValue rootObject; + + private final SpelParserConfiguration configuration; + + @Nullable + private Deque contextObjects; + + @Nullable + private Deque variableScopes; + + // When entering a new scope there is a new base object which should be used + // for '#this' references (or to act as a target for unqualified references). + // This ArrayDeque captures those objects at each nested scope level. + // For example: + // #list1.?[#list2.contains(#this)] + // On entering the selection we enter a new scope, and #this is now the + // element from list1 + @Nullable + private ArrayDeque scopeRootObjects; + + + public ExpressionState(EvaluationContext context) { + this(context, context.getRootObject(), new SpelParserConfiguration(false, false)); + } + + public ExpressionState(EvaluationContext context, SpelParserConfiguration configuration) { + this(context, context.getRootObject(), configuration); + } + + public ExpressionState(EvaluationContext context, TypedValue rootObject) { + this(context, rootObject, new SpelParserConfiguration(false, false)); + } + + public ExpressionState(EvaluationContext context, TypedValue rootObject, SpelParserConfiguration configuration) { + Assert.notNull(context, "EvaluationContext must not be null"); + Assert.notNull(configuration, "SpelParserConfiguration must not be null"); + this.relatedContext = context; + this.rootObject = rootObject; + this.configuration = configuration; + } + + + /** + * The active context object is what unqualified references to properties/etc are resolved against. + */ + public TypedValue getActiveContextObject() { + if (CollectionUtils.isEmpty(this.contextObjects)) { + return this.rootObject; + } + return this.contextObjects.element(); + } + + public void pushActiveContextObject(TypedValue obj) { + if (this.contextObjects == null) { + this.contextObjects = new ArrayDeque<>(); + } + this.contextObjects.push(obj); + } + + public void popActiveContextObject() { + if (this.contextObjects == null) { + this.contextObjects = new ArrayDeque<>(); + } + try { + this.contextObjects.pop(); + } + catch (NoSuchElementException ex) { + throw new IllegalStateException("Cannot pop active context object: stack is empty"); + } + } + + public TypedValue getRootContextObject() { + return this.rootObject; + } + + public TypedValue getScopeRootContextObject() { + if (CollectionUtils.isEmpty(this.scopeRootObjects)) { + return this.rootObject; + } + return this.scopeRootObjects.element(); + } + + public void setVariable(String name, @Nullable Object value) { + this.relatedContext.setVariable(name, value); + } + + public TypedValue lookupVariable(String name) { + Object value = this.relatedContext.lookupVariable(name); + return (value != null ? new TypedValue(value) : TypedValue.NULL); + } + + public TypeComparator getTypeComparator() { + return this.relatedContext.getTypeComparator(); + } + + public Class findType(String type) throws EvaluationException { + return this.relatedContext.getTypeLocator().findType(type); + } + + public Object convertValue(Object value, TypeDescriptor targetTypeDescriptor) throws EvaluationException { + Object result = this.relatedContext.getTypeConverter().convertValue( + value, TypeDescriptor.forObject(value), targetTypeDescriptor); + if (result == null) { + throw new IllegalStateException("Null conversion result for value [" + value + "]"); + } + return result; + } + + public TypeConverter getTypeConverter() { + return this.relatedContext.getTypeConverter(); + } + + @Nullable + public Object convertValue(TypedValue value, TypeDescriptor targetTypeDescriptor) throws EvaluationException { + Object val = value.getValue(); + return this.relatedContext.getTypeConverter().convertValue( + val, TypeDescriptor.forObject(val), targetTypeDescriptor); + } + + /* + * A new scope is entered when a function is invoked. + */ + public void enterScope(Map argMap) { + initVariableScopes().push(new VariableScope(argMap)); + initScopeRootObjects().push(getActiveContextObject()); + } + + public void enterScope() { + initVariableScopes().push(new VariableScope(Collections.emptyMap())); + initScopeRootObjects().push(getActiveContextObject()); + } + + public void enterScope(String name, Object value) { + initVariableScopes().push(new VariableScope(name, value)); + initScopeRootObjects().push(getActiveContextObject()); + } + + public void exitScope() { + initVariableScopes().pop(); + initScopeRootObjects().pop(); + } + + public void setLocalVariable(String name, Object value) { + initVariableScopes().element().setVariable(name, value); + } + + @Nullable + public Object lookupLocalVariable(String name) { + for (VariableScope scope : initVariableScopes()) { + if (scope.definesVariable(name)) { + return scope.lookupVariable(name); + } + } + return null; + } + + private Deque initVariableScopes() { + if (this.variableScopes == null) { + this.variableScopes = new ArrayDeque<>(); + // top-level empty variable scope + this.variableScopes.add(new VariableScope()); + } + return this.variableScopes; + } + + private Deque initScopeRootObjects() { + if (this.scopeRootObjects == null) { + this.scopeRootObjects = new ArrayDeque<>(); + } + return this.scopeRootObjects; + } + + public TypedValue operate(Operation op, @Nullable Object left, @Nullable Object right) throws EvaluationException { + OperatorOverloader overloader = this.relatedContext.getOperatorOverloader(); + if (overloader.overridesOperation(op, left, right)) { + Object returnValue = overloader.operate(op, left, right); + return new TypedValue(returnValue); + } + else { + String leftType = (left == null ? "null" : left.getClass().getName()); + String rightType = (right == null? "null" : right.getClass().getName()); + throw new SpelEvaluationException(SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES, op, leftType, rightType); + } + } + + public List getPropertyAccessors() { + return this.relatedContext.getPropertyAccessors(); + } + + public EvaluationContext getEvaluationContext() { + return this.relatedContext; + } + + public SpelParserConfiguration getConfiguration() { + return this.configuration; + } + + + /** + * A new scope is entered when a function is called and it is used to hold the + * parameters to the function call. If the names of the parameters clash with + * those in a higher level scope, those in the higher level scope will not be + * accessible whilst the function is executing. When the function returns, + * the scope is exited. + */ + private static class VariableScope { + + private final Map vars = new HashMap<>(); + + public VariableScope() { + } + + public VariableScope(@Nullable Map arguments) { + if (arguments != null) { + this.vars.putAll(arguments); + } + } + + public VariableScope(String name, Object value) { + this.vars.put(name,value); + } + + public Object lookupVariable(String name) { + return this.vars.get(name); + } + + public void setVariable(String name, Object value) { + this.vars.put(name,value); + } + + public boolean definesVariable(String name) { + return this.vars.containsKey(name); + } + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/InternalParseException.java b/spring-expression/src/main/java/org/springframework/expression/spel/InternalParseException.java new file mode 100644 index 0000000..3debd82 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/InternalParseException.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +/** + * Wraps a real parse exception. This exception flows to the top parse method and then + * the wrapped exception is thrown as the real problem. + * + * @author Andy Clement + * @since 3.0 + */ +@SuppressWarnings("serial") +public class InternalParseException extends RuntimeException { + + public InternalParseException(SpelParseException cause) { + super(cause); + } + + @Override + public SpelParseException getCause() { + return (SpelParseException) super.getCause(); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/SpelCompilerMode.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelCompilerMode.java new file mode 100644 index 0000000..cf6e024 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelCompilerMode.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +/** + * Captures the possible configuration settings for a compiler that can be + * used when evaluating expressions. + * + * @author Andy Clement + * @since 4.1 + */ +public enum SpelCompilerMode { + + /** + * The compiler is switched off; this is the default. + */ + OFF, + + /** + * In immediate mode, expressions are compiled as soon as possible (usually after 1 interpreted run). + * If a compiled expression fails it will throw an exception to the caller. + */ + IMMEDIATE, + + /** + * In mixed mode, expression evaluation silently switches between interpreted and compiled over time. + * After a number of runs the expression gets compiled. If it later fails (possibly due to inferred + * type information changing) then that will be caught internally and the system switches back to + * interpreted mode. It may subsequently compile it again later. + */ + MIXED + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/SpelEvaluationException.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelEvaluationException.java new file mode 100644 index 0000000..c55fe0b --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelEvaluationException.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import org.springframework.expression.EvaluationException; + +/** + * Root exception for Spring EL related exceptions. Rather than holding a hard coded + * string indicating the problem, it records a message key and the inserts for the + * message. See {@link SpelMessage} for the list of all possible messages that can occur. + * + * @author Andy Clement + * @author Juergen Hoeller + * @since 3.0 + */ +@SuppressWarnings("serial") +public class SpelEvaluationException extends EvaluationException { + + private final SpelMessage message; + + private final Object[] inserts; + + + public SpelEvaluationException(SpelMessage message, Object... inserts) { + super(message.formatMessage(inserts)); + this.message = message; + this.inserts = inserts; + } + + public SpelEvaluationException(int position, SpelMessage message, Object... inserts) { + super(position, message.formatMessage(inserts)); + this.message = message; + this.inserts = inserts; + } + + public SpelEvaluationException(int position, Throwable cause, SpelMessage message, Object... inserts) { + super(position, message.formatMessage(inserts), cause); + this.message = message; + this.inserts = inserts; + } + + public SpelEvaluationException(Throwable cause, SpelMessage message, Object... inserts) { + super(message.formatMessage(inserts), cause); + this.message = message; + this.inserts = inserts; + } + + + /** + * Set the position in the related expression which gave rise to this exception. + */ + public void setPosition(int position) { + this.position = position; + } + + /** + * Return the message code. + */ + public SpelMessage getMessageCode() { + return this.message; + } + + /** + * Return the message inserts. + */ + public Object[] getInserts() { + return this.inserts; + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java new file mode 100644 index 0000000..8998cb2 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java @@ -0,0 +1,301 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import java.text.MessageFormat; + +/** + * Contains all the messages that can be produced by the Spring Expression Language. + * Each message has a kind (info, warn, error) and a code number. Tests can be written to + * expect particular code numbers rather than particular text, enabling the message text + * to more easily be modified and the tests to run successfully in different locales. + * + *

    When a message is formatted, it will have this kind of form, capturing the prefix + * and the error kind: + * + *

    EL1004E: Type cannot be found 'String'
    + * + * @author Andy Clement + * @author Juergen Hoeller + * @since 3.0 + */ +public enum SpelMessage { + + TYPE_CONVERSION_ERROR(Kind.ERROR, 1001, + "Type conversion problem, cannot convert from {0} to {1}"), + + CONSTRUCTOR_NOT_FOUND(Kind.ERROR, 1002, + "Constructor call: No suitable constructor found on type {0} for arguments {1}"), + + CONSTRUCTOR_INVOCATION_PROBLEM(Kind.ERROR, 1003, + "A problem occurred whilst attempting to construct an object of type ''{0}'' using arguments ''{1}''"), + + METHOD_NOT_FOUND(Kind.ERROR, 1004, + "Method call: Method {0} cannot be found on type {1}"), + + TYPE_NOT_FOUND(Kind.ERROR, 1005, + "Type cannot be found ''{0}''"), + + FUNCTION_NOT_DEFINED(Kind.ERROR, 1006, + "Function ''{0}'' could not be found"), + + PROPERTY_OR_FIELD_NOT_READABLE_ON_NULL(Kind.ERROR, 1007, + "Property or field ''{0}'' cannot be found on null"), + + PROPERTY_OR_FIELD_NOT_READABLE(Kind.ERROR, 1008, + "Property or field ''{0}'' cannot be found on object of type ''{1}'' - maybe not public or not valid?"), + + PROPERTY_OR_FIELD_NOT_WRITABLE_ON_NULL(Kind.ERROR, 1009, + "Property or field ''{0}'' cannot be set on null"), + + PROPERTY_OR_FIELD_NOT_WRITABLE(Kind.ERROR, 1010, + "Property or field ''{0}'' cannot be set on object of type ''{1}'' - maybe not public or not writable?"), + + METHOD_CALL_ON_NULL_OBJECT_NOT_ALLOWED(Kind.ERROR, 1011, + "Method call: Attempted to call method {0} on null context object"), + + CANNOT_INDEX_INTO_NULL_VALUE(Kind.ERROR, 1012, + "Cannot index into a null value"), + + NOT_COMPARABLE(Kind.ERROR, 1013, + "Cannot compare instances of {0} and {1}"), + + INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION(Kind.ERROR, 1014, + "Incorrect number of arguments for function, {0} supplied but function takes {1}"), + + INVALID_TYPE_FOR_SELECTION(Kind.ERROR, 1015, + "Cannot perform selection on input data of type ''{0}''"), + + RESULT_OF_SELECTION_CRITERIA_IS_NOT_BOOLEAN(Kind.ERROR, 1016, + "Result of selection criteria is not boolean"), + + BETWEEN_RIGHT_OPERAND_MUST_BE_TWO_ELEMENT_LIST(Kind.ERROR, 1017, + "Right operand for the 'between' operator has to be a two-element list"), + + INVALID_PATTERN(Kind.ERROR, 1018, + "Pattern is not valid ''{0}''"), + + PROJECTION_NOT_SUPPORTED_ON_TYPE(Kind.ERROR, 1019, + "Projection is not supported on the type ''{0}''"), + + ARGLIST_SHOULD_NOT_BE_EVALUATED(Kind.ERROR, 1020, + "The argument list of a lambda expression should never have getValue() called upon it"), + + EXCEPTION_DURING_PROPERTY_READ(Kind.ERROR, 1021, + "A problem occurred whilst attempting to access the property ''{0}'': ''{1}''"), + + FUNCTION_REFERENCE_CANNOT_BE_INVOKED(Kind.ERROR, 1022, + "The function ''{0}'' mapped to an object of type ''{1}'' which cannot be invoked"), + + EXCEPTION_DURING_FUNCTION_CALL(Kind.ERROR, 1023, + "A problem occurred whilst attempting to invoke the function ''{0}'': ''{1}''"), + + ARRAY_INDEX_OUT_OF_BOUNDS(Kind.ERROR, 1024, + "The array has ''{0}'' elements, index ''{1}'' is invalid"), + + COLLECTION_INDEX_OUT_OF_BOUNDS(Kind.ERROR, 1025, + "The collection has ''{0}'' elements, index ''{1}'' is invalid"), + + STRING_INDEX_OUT_OF_BOUNDS(Kind.ERROR, 1026, + "The string has ''{0}'' characters, index ''{1}'' is invalid"), + + INDEXING_NOT_SUPPORTED_FOR_TYPE(Kind.ERROR, 1027, + "Indexing into type ''{0}'' is not supported"), + + INSTANCEOF_OPERATOR_NEEDS_CLASS_OPERAND(Kind.ERROR, 1028, + "The operator 'instanceof' needs the right operand to be a class, not a ''{0}''"), + + EXCEPTION_DURING_METHOD_INVOCATION(Kind.ERROR, 1029, + "A problem occurred when trying to execute method ''{0}'' on object of type ''{1}'': ''{2}''"), + + OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES(Kind.ERROR, 1030, + "The operator ''{0}'' is not supported between objects of type ''{1}'' and ''{2}''"), + + PROBLEM_LOCATING_METHOD(Kind.ERROR, 1031, + "Problem locating method {0} on type {1}"), + + SETVALUE_NOT_SUPPORTED( Kind.ERROR, 1032, + "setValue(ExpressionState, Object) not supported for ''{0}''"), + + MULTIPLE_POSSIBLE_METHODS(Kind.ERROR, 1033, + "Method call of ''{0}'' is ambiguous, supported type conversions allow multiple variants to match"), + + EXCEPTION_DURING_PROPERTY_WRITE(Kind.ERROR, 1034, + "A problem occurred whilst attempting to set the property ''{0}'': {1}"), + + NOT_AN_INTEGER(Kind.ERROR, 1035, + "The value ''{0}'' cannot be parsed as an int"), + + NOT_A_LONG(Kind.ERROR, 1036, + "The value ''{0}'' cannot be parsed as a long"), + + INVALID_FIRST_OPERAND_FOR_MATCHES_OPERATOR(Kind.ERROR, 1037, + "First operand to matches operator must be a string. ''{0}'' is not"), + + INVALID_SECOND_OPERAND_FOR_MATCHES_OPERATOR(Kind.ERROR, 1038, + "Second operand to matches operator must be a string. ''{0}'' is not"), + + FUNCTION_MUST_BE_STATIC(Kind.ERROR, 1039, + "Only static methods can be called via function references. " + + "The method ''{0}'' referred to by name ''{1}'' is not static."), + + NOT_A_REAL(Kind.ERROR, 1040, + "The value ''{0}'' cannot be parsed as a double"), + + MORE_INPUT(Kind.ERROR, 1041, + "After parsing a valid expression, there is still more data in the expression: ''{0}''"), + + RIGHT_OPERAND_PROBLEM(Kind.ERROR, 1042, + "Problem parsing right operand"), + + NOT_EXPECTED_TOKEN(Kind.ERROR, 1043, + "Unexpected token. Expected ''{0}'' but was ''{1}''"), + + OOD(Kind.ERROR, 1044, + "Unexpectedly ran out of input"), + + NON_TERMINATING_DOUBLE_QUOTED_STRING(Kind.ERROR, 1045, + "Cannot find terminating \" for string"), + + NON_TERMINATING_QUOTED_STRING(Kind.ERROR, 1046, + "Cannot find terminating '' for string"), + + MISSING_LEADING_ZERO_FOR_NUMBER(Kind.ERROR, 1047, + "A real number must be prefixed by zero, it cannot start with just ''.''"), + + REAL_CANNOT_BE_LONG(Kind.ERROR, 1048, + "Real number cannot be suffixed with a long (L or l) suffix"), + + UNEXPECTED_DATA_AFTER_DOT(Kind.ERROR, 1049, + "Unexpected data after ''.'': ''{0}''"), + + MISSING_CONSTRUCTOR_ARGS(Kind.ERROR, 1050, + "The arguments '(...)' for the constructor call are missing"), + + RUN_OUT_OF_ARGUMENTS(Kind.ERROR, 1051, + "Unexpectedly ran out of arguments"), + + UNABLE_TO_GROW_COLLECTION(Kind.ERROR, 1052, + "Unable to grow collection"), + + UNABLE_TO_GROW_COLLECTION_UNKNOWN_ELEMENT_TYPE(Kind.ERROR, 1053, + "Unable to grow collection: unable to determine list element type"), + + UNABLE_TO_CREATE_LIST_FOR_INDEXING(Kind.ERROR, 1054, + "Unable to dynamically create a List to replace a null value"), + + UNABLE_TO_CREATE_MAP_FOR_INDEXING(Kind.ERROR, 1055, + "Unable to dynamically create a Map to replace a null value"), + + UNABLE_TO_DYNAMICALLY_CREATE_OBJECT(Kind.ERROR, 1056, + "Unable to dynamically create instance of ''{0}'' to replace a null value"), + + NO_BEAN_RESOLVER_REGISTERED(Kind.ERROR, 1057, + "No bean resolver registered in the context to resolve access to bean ''{0}''"), + + EXCEPTION_DURING_BEAN_RESOLUTION(Kind.ERROR, 1058, + "A problem occurred when trying to resolve bean ''{0}'':''{1}''"), + + INVALID_BEAN_REFERENCE(Kind.ERROR, 1059, + "@ or & can only be followed by an identifier or a quoted name"), + + TYPE_NAME_EXPECTED_FOR_ARRAY_CONSTRUCTION(Kind.ERROR, 1060, + "Expected the type of the new array to be specified as a String but found ''{0}''"), + + INCORRECT_ELEMENT_TYPE_FOR_ARRAY(Kind.ERROR, 1061, + "The array of type ''{0}'' cannot have an element of type ''{1}'' inserted"), + + MULTIDIM_ARRAY_INITIALIZER_NOT_SUPPORTED(Kind.ERROR, 1062, + "Using an initializer to build a multi-dimensional array is not currently supported"), + + MISSING_ARRAY_DIMENSION(Kind.ERROR, 1063, + "A required array dimension has not been specified"), + + INITIALIZER_LENGTH_INCORRECT(Kind.ERROR, 1064, + "Array initializer size does not match array dimensions"), + + UNEXPECTED_ESCAPE_CHAR(Kind.ERROR, 1065, + "Unexpected escape character"), + + OPERAND_NOT_INCREMENTABLE(Kind.ERROR, 1066, + "The expression component ''{0}'' does not support increment"), + + OPERAND_NOT_DECREMENTABLE(Kind.ERROR, 1067, + "The expression component ''{0}'' does not support decrement"), + + NOT_ASSIGNABLE(Kind.ERROR, 1068, + "The expression component ''{0}'' is not assignable"), + + MISSING_CHARACTER(Kind.ERROR, 1069, + "Missing expected character ''{0}''"), + + LEFT_OPERAND_PROBLEM(Kind.ERROR, 1070, + "Problem parsing left operand"), + + MISSING_SELECTION_EXPRESSION(Kind.ERROR, 1071, + "A required selection expression has not been specified"), + + /** @since 4.1 */ + EXCEPTION_RUNNING_COMPILED_EXPRESSION(Kind.ERROR, 1072, + "An exception occurred whilst evaluating a compiled expression"), + + /** @since 4.3.17 */ + FLAWED_PATTERN(Kind.ERROR, 1073, + "Failed to efficiently evaluate pattern ''{0}'': consider redesigning it"); + + + private final Kind kind; + + private final int code; + + private final String message; + + + SpelMessage(Kind kind, int code, String message) { + this.kind = kind; + this.code = code; + this.message = message; + } + + + /** + * Produce a complete message including the prefix and with the inserts + * applied to the message. + * @param inserts the inserts to put into the formatted message + * @return a formatted message + * @since 4.3.5 + */ + public String formatMessage(Object... inserts) { + StringBuilder formattedMessage = new StringBuilder(); + formattedMessage.append("EL").append(this.code); + switch (this.kind) { + case ERROR: + formattedMessage.append("E"); + break; + } + formattedMessage.append(": "); + formattedMessage.append(MessageFormat.format(this.message, inserts)); + return formattedMessage.toString(); + } + + + /** + * Message kinds. + */ + public enum Kind { INFO, WARNING, ERROR } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/SpelNode.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelNode.java new file mode 100644 index 0000000..1889f2e --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelNode.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import org.springframework.expression.EvaluationException; +import org.springframework.expression.TypedValue; +import org.springframework.lang.Nullable; + +/** + * Represents a node in the AST for a parsed expression. + * + * @author Andy Clement + * @since 3.0 + */ +public interface SpelNode { + + /** + * Evaluate the expression node in the context of the supplied expression state + * and return the value. + * @param expressionState the current expression state (includes the context) + * @return the value of this node evaluated against the specified state + */ + @Nullable + Object getValue(ExpressionState expressionState) throws EvaluationException; + + /** + * Evaluate the expression node in the context of the supplied expression state + * and return the typed value. + * @param expressionState the current expression state (includes the context) + * @return the type value of this node evaluated against the specified state + */ + TypedValue getTypedValue(ExpressionState expressionState) throws EvaluationException; + + /** + * Determine if this expression node will support a setValue() call. + * @param expressionState the current expression state (includes the context) + * @return true if the expression node will allow setValue() + * @throws EvaluationException if something went wrong trying to determine + * if the node supports writing + */ + boolean isWritable(ExpressionState expressionState) throws EvaluationException; + + /** + * Evaluate the expression to a node and then set the new value on that node. + * For example, if the expression evaluates to a property reference, then the + * property will be set to the new value. + * @param expressionState the current expression state (includes the context) + * @param newValue the new value + * @throws EvaluationException if any problem occurs evaluating the expression or + * setting the new value + */ + void setValue(ExpressionState expressionState, @Nullable Object newValue) throws EvaluationException; + + /** + * Return the string form the this AST node. + * @return the string form + */ + String toStringAST(); + + /** + * Return the number of children under this node. + * @return the child count + */ + int getChildCount(); + + /** + * Helper method that returns a SpelNode rather than an Antlr Tree node. + * @return the child node cast to a SpelNode + */ + SpelNode getChild(int index); + + /** + * Determine the class of the object passed in, unless it is already a class object. + * @param obj the object that the caller wants the class of + * @return the class of the object if it is not already a class object, + * or {@code null} if the object is {@code null} + */ + @Nullable + Class getObjectClass(@Nullable Object obj); + + /** + * Return the start position of this AST node in the expression string. + * @return the start position + */ + int getStartPosition(); + + /** + * Return the end position of this AST node in the expression string. + * @return the end position + */ + int getEndPosition(); + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/SpelParseException.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelParseException.java new file mode 100644 index 0000000..4b5e4c5 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelParseException.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import org.springframework.expression.ParseException; +import org.springframework.lang.Nullable; + +/** + * Root exception for Spring EL related exceptions. Rather than holding a hard coded + * string indicating the problem, it records a message key and the inserts for the + * message. See {@link SpelMessage} for the list of all possible messages that can occur. + * + * @author Andy Clement + * @author Juergen Hoeller + * @since 3.0 + */ +@SuppressWarnings("serial") +public class SpelParseException extends ParseException { + + private final SpelMessage message; + + private final Object[] inserts; + + + public SpelParseException(@Nullable String expressionString, int position, SpelMessage message, Object... inserts) { + super(expressionString, position, message.formatMessage(inserts)); + this.message = message; + this.inserts = inserts; + } + + public SpelParseException(int position, SpelMessage message, Object... inserts) { + super(position, message.formatMessage(inserts)); + this.message = message; + this.inserts = inserts; + } + + public SpelParseException(int position, Throwable cause, SpelMessage message, Object... inserts) { + super(position, message.formatMessage(inserts), cause); + this.message = message; + this.inserts = inserts; + } + + + /** + * Return the message code. + */ + public SpelMessage getMessageCode() { + return this.message; + } + + /** + * Return the message inserts. + */ + public Object[] getInserts() { + return this.inserts; + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java new file mode 100644 index 0000000..fb12605 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java @@ -0,0 +1,145 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import org.springframework.core.SpringProperties; +import org.springframework.lang.Nullable; + +/** + * Configuration object for the SpEL expression parser. + * + * @author Juergen Hoeller + * @author Phillip Webb + * @author Andy Clement + * @since 3.0 + * @see org.springframework.expression.spel.standard.SpelExpressionParser#SpelExpressionParser(SpelParserConfiguration) + */ +public class SpelParserConfiguration { + + private static final SpelCompilerMode defaultCompilerMode; + + static { + String compilerMode = SpringProperties.getProperty("spring.expression.compiler.mode"); + defaultCompilerMode = (compilerMode != null ? + SpelCompilerMode.valueOf(compilerMode.toUpperCase()) : SpelCompilerMode.OFF); + } + + + private final SpelCompilerMode compilerMode; + + @Nullable + private final ClassLoader compilerClassLoader; + + private final boolean autoGrowNullReferences; + + private final boolean autoGrowCollections; + + private final int maximumAutoGrowSize; + + + /** + * Create a new {@code SpelParserConfiguration} instance with default settings. + */ + public SpelParserConfiguration() { + this(null, null, false, false, Integer.MAX_VALUE); + } + + /** + * Create a new {@code SpelParserConfiguration} instance. + * @param compilerMode the compiler mode for the parser + * @param compilerClassLoader the ClassLoader to use as the basis for expression compilation + */ + public SpelParserConfiguration(@Nullable SpelCompilerMode compilerMode, @Nullable ClassLoader compilerClassLoader) { + this(compilerMode, compilerClassLoader, false, false, Integer.MAX_VALUE); + } + + /** + * Create a new {@code SpelParserConfiguration} instance. + * @param autoGrowNullReferences if null references should automatically grow + * @param autoGrowCollections if collections should automatically grow + * @see #SpelParserConfiguration(boolean, boolean, int) + */ + public SpelParserConfiguration(boolean autoGrowNullReferences, boolean autoGrowCollections) { + this(null, null, autoGrowNullReferences, autoGrowCollections, Integer.MAX_VALUE); + } + + /** + * Create a new {@code SpelParserConfiguration} instance. + * @param autoGrowNullReferences if null references should automatically grow + * @param autoGrowCollections if collections should automatically grow + * @param maximumAutoGrowSize the maximum size that the collection can auto grow + */ + public SpelParserConfiguration(boolean autoGrowNullReferences, boolean autoGrowCollections, int maximumAutoGrowSize) { + this(null, null, autoGrowNullReferences, autoGrowCollections, maximumAutoGrowSize); + } + + /** + * Create a new {@code SpelParserConfiguration} instance. + * @param compilerMode the compiler mode that parsers using this configuration object should use + * @param compilerClassLoader the ClassLoader to use as the basis for expression compilation + * @param autoGrowNullReferences if null references should automatically grow + * @param autoGrowCollections if collections should automatically grow + * @param maximumAutoGrowSize the maximum size that the collection can auto grow + */ + public SpelParserConfiguration(@Nullable SpelCompilerMode compilerMode, @Nullable ClassLoader compilerClassLoader, + boolean autoGrowNullReferences, boolean autoGrowCollections, int maximumAutoGrowSize) { + + this.compilerMode = (compilerMode != null ? compilerMode : defaultCompilerMode); + this.compilerClassLoader = compilerClassLoader; + this.autoGrowNullReferences = autoGrowNullReferences; + this.autoGrowCollections = autoGrowCollections; + this.maximumAutoGrowSize = maximumAutoGrowSize; + } + + + /** + * Return the configuration mode for parsers using this configuration object. + */ + public SpelCompilerMode getCompilerMode() { + return this.compilerMode; + } + + /** + * Return the ClassLoader to use as the basis for expression compilation. + */ + @Nullable + public ClassLoader getCompilerClassLoader() { + return this.compilerClassLoader; + } + + /** + * Return {@code true} if {@code null} references should be automatically grown. + */ + public boolean isAutoGrowNullReferences() { + return this.autoGrowNullReferences; + } + + /** + * Return {@code true} if collections should be automatically grown. + */ + public boolean isAutoGrowCollections() { + return this.autoGrowCollections; + } + + /** + * Return the maximum size that a collection can auto grow. + */ + public int getMaximumAutoGrowSize() { + return this.maximumAutoGrowSize; + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Assign.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Assign.java new file mode 100644 index 0000000..a009a07 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Assign.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import org.springframework.expression.EvaluationException; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.ExpressionState; + +/** + * Represents assignment. An alternative to calling {@code setValue} + * for an expression which indicates an assign statement. + * + *

    Example: 'someNumberProperty=42' + * + * @author Andy Clement + * @since 3.0 + */ +public class Assign extends SpelNodeImpl { + + public Assign(int startPos, int endPos, SpelNodeImpl... operands) { + super(startPos, endPos, operands); + } + + + @Override + public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + TypedValue newValue = this.children[1].getValueInternal(state); + getChild(0).setValue(state, newValue.getValue()); + return newValue; + } + + @Override + public String toStringAST() { + return getChild(0).toStringAST() + "=" + getChild(1).toStringAST(); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/AstUtils.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/AstUtils.java new file mode 100644 index 0000000..ac7a9f0 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/AstUtils.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.expression.PropertyAccessor; +import org.springframework.lang.Nullable; + +/** + * Utilities methods for use in the Ast classes. + * + * @author Andy Clement + * @since 3.0.2 + */ +public abstract class AstUtils { + + /** + * Determines the set of property resolvers that should be used to try and access a + * property on the specified target type. The resolvers are considered to be in an + * ordered list, however in the returned list any that are exact matches for the input + * target type (as opposed to 'general' resolvers that could work for any type) are + * placed at the start of the list. In addition, there are specific resolvers that + * exactly name the class in question and resolvers that name a specific class but it + * is a supertype of the class we have. These are put at the end of the specific resolvers + * set and will be tried after exactly matching accessors but before generic accessors. + * @param targetType the type upon which property access is being attempted + * @return a list of resolvers that should be tried in order to access the property + */ + public static List getPropertyAccessorsToTry( + @Nullable Class targetType, List propertyAccessors) { + + List specificAccessors = new ArrayList<>(); + List generalAccessors = new ArrayList<>(); + for (PropertyAccessor resolver : propertyAccessors) { + Class[] targets = resolver.getSpecificTargetClasses(); + if (targets == null) { // generic resolver that says it can be used for any type + generalAccessors.add(resolver); + } + else { + if (targetType != null) { + for (Class clazz : targets) { + if (clazz == targetType) { // put exact matches on the front to be tried first? + specificAccessors.add(resolver); + } + else if (clazz.isAssignableFrom(targetType)) { // put supertype matches at the end of the + // specificAccessor list + generalAccessors.add(resolver); + } + } + } + } + } + List resolvers = new ArrayList<>(specificAccessors.size() + generalAccessors.size()); + resolvers.addAll(specificAccessors); + resolvers.addAll(generalAccessors); + return resolvers; + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/BeanReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/BeanReference.java new file mode 100644 index 0000000..a59d982 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/BeanReference.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import org.springframework.expression.AccessException; +import org.springframework.expression.BeanResolver; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; + +/** + * Represents a bean reference to a type, for example @foo or @'foo.bar'. + * For a FactoryBean the syntax &foo can be used to access the factory itself. + * + * @author Andy Clement + */ +public class BeanReference extends SpelNodeImpl { + + private static final String FACTORY_BEAN_PREFIX = "&"; + + private final String beanName; + + + public BeanReference(int startPos, int endPos, String beanName) { + super(startPos, endPos); + this.beanName = beanName; + } + + + @Override + public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + BeanResolver beanResolver = state.getEvaluationContext().getBeanResolver(); + if (beanResolver == null) { + throw new SpelEvaluationException( + getStartPosition(), SpelMessage.NO_BEAN_RESOLVER_REGISTERED, this.beanName); + } + + try { + return new TypedValue(beanResolver.resolve(state.getEvaluationContext(), this.beanName)); + } + catch (AccessException ex) { + throw new SpelEvaluationException(getStartPosition(), ex, SpelMessage.EXCEPTION_DURING_BEAN_RESOLUTION, + this.beanName, ex.getMessage()); + } + } + + @Override + public String toStringAST() { + StringBuilder sb = new StringBuilder(); + if (!this.beanName.startsWith(FACTORY_BEAN_PREFIX)) { + sb.append("@"); + } + if (!this.beanName.contains(".")) { + sb.append(this.beanName); + } + else { + sb.append("'").append(this.beanName).append("'"); + } + return sb.toString(); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/BooleanLiteral.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/BooleanLiteral.java new file mode 100644 index 0000000..55dfcee --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/BooleanLiteral.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.support.BooleanTypedValue; + +/** + * Represents the literal values {@code TRUE} and {@code FALSE}. + * + * @author Andy Clement + * @since 3.0 + */ +public class BooleanLiteral extends Literal { + + private final BooleanTypedValue value; + + + public BooleanLiteral(String payload, int startPos, int endPos, boolean value) { + super(payload, startPos, endPos); + this.value = BooleanTypedValue.forValue(value); + this.exitTypeDescriptor = "Z"; + } + + + @Override + public BooleanTypedValue getLiteralValue() { + return this.value; + } + + @Override + public boolean isCompilable() { + return true; + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + if (this.value == BooleanTypedValue.TRUE) { + mv.visitLdcInsn(1); + } + else { + mv.visitLdcInsn(0); + } + cf.pushDescriptor(this.exitTypeDescriptor); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/CompoundExpression.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/CompoundExpression.java new file mode 100644 index 0000000..0e47fac --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/CompoundExpression.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.util.StringJoiner; + +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.lang.Nullable; + +/** + * Represents a DOT separated expression sequence, such as + * {@code 'property1.property2.methodOne()'}. + * + * @author Andy Clement + * @since 3.0 + */ +public class CompoundExpression extends SpelNodeImpl { + + public CompoundExpression(int startPos, int endPos, SpelNodeImpl... expressionComponents) { + super(startPos, endPos, expressionComponents); + if (expressionComponents.length < 2) { + throw new IllegalStateException("Do not build compound expressions with less than two entries: " + + expressionComponents.length); + } + } + + + @Override + protected ValueRef getValueRef(ExpressionState state) throws EvaluationException { + if (getChildCount() == 1) { + return this.children[0].getValueRef(state); + } + + SpelNodeImpl nextNode = this.children[0]; + try { + TypedValue result = nextNode.getValueInternal(state); + int cc = getChildCount(); + for (int i = 1; i < cc - 1; i++) { + try { + state.pushActiveContextObject(result); + nextNode = this.children[i]; + result = nextNode.getValueInternal(state); + } + finally { + state.popActiveContextObject(); + } + } + try { + state.pushActiveContextObject(result); + nextNode = this.children[cc - 1]; + return nextNode.getValueRef(state); + } + finally { + state.popActiveContextObject(); + } + } + catch (SpelEvaluationException ex) { + // Correct the position for the error before re-throwing + ex.setPosition(nextNode.getStartPosition()); + throw ex; + } + } + + /** + * Evaluates a compound expression. This involves evaluating each piece in turn and the + * return value from each piece is the active context object for the subsequent piece. + * @param state the state in which the expression is being evaluated + * @return the final value from the last piece of the compound expression + */ + @Override + public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + ValueRef ref = getValueRef(state); + TypedValue result = ref.getValue(); + this.exitTypeDescriptor = this.children[this.children.length - 1].exitTypeDescriptor; + return result; + } + + @Override + public void setValue(ExpressionState state, @Nullable Object value) throws EvaluationException { + getValueRef(state).setValue(value); + } + + @Override + public boolean isWritable(ExpressionState state) throws EvaluationException { + return getValueRef(state).isWritable(); + } + + @Override + public String toStringAST() { + StringJoiner sj = new StringJoiner("."); + for (int i = 0; i < getChildCount(); i++) { + sj.add(getChild(i).toStringAST()); + } + return sj.toString(); + } + + @Override + public boolean isCompilable() { + for (SpelNodeImpl child: this.children) { + if (!child.isCompilable()) { + return false; + } + } + return true; + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + for (SpelNodeImpl child : this.children) { + child.generateCode(mv, cf); + } + cf.pushDescriptor(this.exitTypeDescriptor); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java new file mode 100644 index 0000000..c70fcfb --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java @@ -0,0 +1,468 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.asm.MethodVisitor; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.AccessException; +import org.springframework.expression.ConstructorExecutor; +import org.springframework.expression.ConstructorResolver; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.TypeConverter; +import org.springframework.expression.TypedValue; +import org.springframework.expression.common.ExpressionUtils; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.SpelNode; +import org.springframework.expression.spel.support.ReflectiveConstructorExecutor; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Represents the invocation of a constructor. Either a constructor on a regular type or + * construction of an array. When an array is constructed, an initializer can be specified. + * + *

    Examples:
    + * new String('hello world')
    + * new int[]{1,2,3,4}
    + * new int[3] new int[3]{1,2,3} + * + * @author Andy Clement + * @author Juergen Hoeller + * @since 3.0 + */ +public class ConstructorReference extends SpelNodeImpl { + + private boolean isArrayConstructor = false; + + @Nullable + private SpelNodeImpl[] dimensions; + + // TODO is this caching safe - passing the expression around will mean this executor is also being passed around + /** The cached executor that may be reused on subsequent evaluations. */ + @Nullable + private volatile ConstructorExecutor cachedExecutor; + + + /** + * Create a constructor reference. The first argument is the type, the rest are the parameters to the constructor + * call + */ + public ConstructorReference(int startPos, int endPos, SpelNodeImpl... arguments) { + super(startPos, endPos, arguments); + this.isArrayConstructor = false; + } + + /** + * Create a constructor reference. The first argument is the type, the rest are the parameters to the constructor + * call + */ + public ConstructorReference(int startPos, int endPos, SpelNodeImpl[] dimensions, SpelNodeImpl... arguments) { + super(startPos, endPos, arguments); + this.isArrayConstructor = true; + this.dimensions = dimensions; + } + + + /** + * Implements getValue() - delegating to the code for building an array or a simple type. + */ + @Override + public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + if (this.isArrayConstructor) { + return createArray(state); + } + else { + return createNewInstance(state); + } + } + + /** + * Create a new ordinary object and return it. + * @param state the expression state within which this expression is being evaluated + * @return the new object + * @throws EvaluationException if there is a problem creating the object + */ + private TypedValue createNewInstance(ExpressionState state) throws EvaluationException { + Object[] arguments = new Object[getChildCount() - 1]; + List argumentTypes = new ArrayList<>(getChildCount() - 1); + for (int i = 0; i < arguments.length; i++) { + TypedValue childValue = this.children[i + 1].getValueInternal(state); + Object value = childValue.getValue(); + arguments[i] = value; + argumentTypes.add(TypeDescriptor.forObject(value)); + } + + ConstructorExecutor executorToUse = this.cachedExecutor; + if (executorToUse != null) { + try { + return executorToUse.execute(state.getEvaluationContext(), arguments); + } + catch (AccessException ex) { + // Two reasons this can occur: + // 1. the method invoked actually threw a real exception + // 2. the method invoked was not passed the arguments it expected and has become 'stale' + + // In the first case we should not retry, in the second case we should see if there is a + // better suited method. + + // To determine which situation it is, the AccessException will contain a cause. + // If the cause is an InvocationTargetException, a user exception was thrown inside the constructor. + // Otherwise the constructor could not be invoked. + if (ex.getCause() instanceof InvocationTargetException) { + // User exception was the root cause - exit now + Throwable rootCause = ex.getCause().getCause(); + if (rootCause instanceof RuntimeException) { + throw (RuntimeException) rootCause; + } + else { + String typeName = (String) this.children[0].getValueInternal(state).getValue(); + throw new SpelEvaluationException(getStartPosition(), rootCause, + SpelMessage.CONSTRUCTOR_INVOCATION_PROBLEM, typeName, + FormatHelper.formatMethodForMessage("", argumentTypes)); + } + } + + // At this point we know it wasn't a user problem so worth a retry if a better candidate can be found + this.cachedExecutor = null; + } + } + + // Either there was no accessor or it no longer exists + String typeName = (String) this.children[0].getValueInternal(state).getValue(); + Assert.state(typeName != null, "No type name"); + executorToUse = findExecutorForConstructor(typeName, argumentTypes, state); + try { + this.cachedExecutor = executorToUse; + if (executorToUse instanceof ReflectiveConstructorExecutor) { + this.exitTypeDescriptor = CodeFlow.toDescriptor( + ((ReflectiveConstructorExecutor) executorToUse).getConstructor().getDeclaringClass()); + + } + return executorToUse.execute(state.getEvaluationContext(), arguments); + } + catch (AccessException ex) { + throw new SpelEvaluationException(getStartPosition(), ex, + SpelMessage.CONSTRUCTOR_INVOCATION_PROBLEM, typeName, + FormatHelper.formatMethodForMessage("", argumentTypes)); + } + } + + /** + * Go through the list of registered constructor resolvers and see if any can find a + * constructor that takes the specified set of arguments. + * @param typeName the type trying to be constructed + * @param argumentTypes the types of the arguments supplied that the constructor must take + * @param state the current state of the expression + * @return a reusable ConstructorExecutor that can be invoked to run the constructor or null + * @throws SpelEvaluationException if there is a problem locating the constructor + */ + private ConstructorExecutor findExecutorForConstructor(String typeName, + List argumentTypes, ExpressionState state) throws SpelEvaluationException { + + EvaluationContext evalContext = state.getEvaluationContext(); + List ctorResolvers = evalContext.getConstructorResolvers(); + for (ConstructorResolver ctorResolver : ctorResolvers) { + try { + ConstructorExecutor ce = ctorResolver.resolve(state.getEvaluationContext(), typeName, argumentTypes); + if (ce != null) { + return ce; + } + } + catch (AccessException ex) { + throw new SpelEvaluationException(getStartPosition(), ex, + SpelMessage.CONSTRUCTOR_INVOCATION_PROBLEM, typeName, + FormatHelper.formatMethodForMessage("", argumentTypes)); + } + } + throw new SpelEvaluationException(getStartPosition(), SpelMessage.CONSTRUCTOR_NOT_FOUND, typeName, + FormatHelper.formatMethodForMessage("", argumentTypes)); + } + + @Override + public String toStringAST() { + StringBuilder sb = new StringBuilder("new "); + int index = 0; + sb.append(getChild(index++).toStringAST()); + sb.append("("); + for (int i = index; i < getChildCount(); i++) { + if (i > index) { + sb.append(","); + } + sb.append(getChild(i).toStringAST()); + } + sb.append(")"); + return sb.toString(); + } + + /** + * Create an array and return it. + * @param state the expression state within which this expression is being evaluated + * @return the new array + * @throws EvaluationException if there is a problem creating the array + */ + private TypedValue createArray(ExpressionState state) throws EvaluationException { + // First child gives us the array type which will either be a primitive or reference type + Object intendedArrayType = getChild(0).getValue(state); + if (!(intendedArrayType instanceof String)) { + throw new SpelEvaluationException(getChild(0).getStartPosition(), + SpelMessage.TYPE_NAME_EXPECTED_FOR_ARRAY_CONSTRUCTION, + FormatHelper.formatClassNameForMessage( + intendedArrayType != null ? intendedArrayType.getClass() : null)); + } + String type = (String) intendedArrayType; + Class componentType; + TypeCode arrayTypeCode = TypeCode.forName(type); + if (arrayTypeCode == TypeCode.OBJECT) { + componentType = state.findType(type); + } + else { + componentType = arrayTypeCode.getType(); + } + Object newArray; + if (!hasInitializer()) { + // Confirm all dimensions were specified (for example [3][][5] is missing the 2nd dimension) + if (this.dimensions != null) { + for (SpelNodeImpl dimension : this.dimensions) { + if (dimension == null) { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.MISSING_ARRAY_DIMENSION); + } + } + } + TypeConverter typeConverter = state.getEvaluationContext().getTypeConverter(); + + // Shortcut for 1 dimensional + if (this.dimensions.length == 1) { + TypedValue o = this.dimensions[0].getTypedValue(state); + int arraySize = ExpressionUtils.toInt(typeConverter, o); + newArray = Array.newInstance(componentType, arraySize); + } + else { + // Multi-dimensional - hold onto your hat! + int[] dims = new int[this.dimensions.length]; + for (int d = 0; d < this.dimensions.length; d++) { + TypedValue o = this.dimensions[d].getTypedValue(state); + dims[d] = ExpressionUtils.toInt(typeConverter, o); + } + newArray = Array.newInstance(componentType, dims); + } + } + else { + // There is an initializer + if (this.dimensions == null || this.dimensions.length > 1) { + // There is an initializer but this is a multi-dimensional array (e.g. new int[][]{{1,2},{3,4}}) - this + // is not currently supported + throw new SpelEvaluationException(getStartPosition(), + SpelMessage.MULTIDIM_ARRAY_INITIALIZER_NOT_SUPPORTED); + } + TypeConverter typeConverter = state.getEvaluationContext().getTypeConverter(); + InlineList initializer = (InlineList) getChild(1); + // If a dimension was specified, check it matches the initializer length + if (this.dimensions[0] != null) { + TypedValue dValue = this.dimensions[0].getTypedValue(state); + int i = ExpressionUtils.toInt(typeConverter, dValue); + if (i != initializer.getChildCount()) { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.INITIALIZER_LENGTH_INCORRECT); + } + } + // Build the array and populate it + int arraySize = initializer.getChildCount(); + newArray = Array.newInstance(componentType, arraySize); + if (arrayTypeCode == TypeCode.OBJECT) { + populateReferenceTypeArray(state, newArray, typeConverter, initializer, componentType); + } + else if (arrayTypeCode == TypeCode.BOOLEAN) { + populateBooleanArray(state, newArray, typeConverter, initializer); + } + else if (arrayTypeCode == TypeCode.BYTE) { + populateByteArray(state, newArray, typeConverter, initializer); + } + else if (arrayTypeCode == TypeCode.CHAR) { + populateCharArray(state, newArray, typeConverter, initializer); + } + else if (arrayTypeCode == TypeCode.DOUBLE) { + populateDoubleArray(state, newArray, typeConverter, initializer); + } + else if (arrayTypeCode == TypeCode.FLOAT) { + populateFloatArray(state, newArray, typeConverter, initializer); + } + else if (arrayTypeCode == TypeCode.INT) { + populateIntArray(state, newArray, typeConverter, initializer); + } + else if (arrayTypeCode == TypeCode.LONG) { + populateLongArray(state, newArray, typeConverter, initializer); + } + else if (arrayTypeCode == TypeCode.SHORT) { + populateShortArray(state, newArray, typeConverter, initializer); + } + else { + throw new IllegalStateException(arrayTypeCode.name()); + } + } + return new TypedValue(newArray); + } + + private void populateReferenceTypeArray(ExpressionState state, Object newArray, TypeConverter typeConverter, + InlineList initializer, Class componentType) { + + TypeDescriptor toTypeDescriptor = TypeDescriptor.valueOf(componentType); + Object[] newObjectArray = (Object[]) newArray; + for (int i = 0; i < newObjectArray.length; i++) { + SpelNode elementNode = initializer.getChild(i); + Object arrayEntry = elementNode.getValue(state); + newObjectArray[i] = typeConverter.convertValue(arrayEntry, + TypeDescriptor.forObject(arrayEntry), toTypeDescriptor); + } + } + + private void populateByteArray(ExpressionState state, Object newArray, TypeConverter typeConverter, + InlineList initializer) { + + byte[] newByteArray = (byte[]) newArray; + for (int i = 0; i < newByteArray.length; i++) { + TypedValue typedValue = initializer.getChild(i).getTypedValue(state); + newByteArray[i] = ExpressionUtils.toByte(typeConverter, typedValue); + } + } + + private void populateFloatArray(ExpressionState state, Object newArray, TypeConverter typeConverter, + InlineList initializer) { + + float[] newFloatArray = (float[]) newArray; + for (int i = 0; i < newFloatArray.length; i++) { + TypedValue typedValue = initializer.getChild(i).getTypedValue(state); + newFloatArray[i] = ExpressionUtils.toFloat(typeConverter, typedValue); + } + } + + private void populateDoubleArray(ExpressionState state, Object newArray, TypeConverter typeConverter, + InlineList initializer) { + + double[] newDoubleArray = (double[]) newArray; + for (int i = 0; i < newDoubleArray.length; i++) { + TypedValue typedValue = initializer.getChild(i).getTypedValue(state); + newDoubleArray[i] = ExpressionUtils.toDouble(typeConverter, typedValue); + } + } + + private void populateShortArray(ExpressionState state, Object newArray, TypeConverter typeConverter, + InlineList initializer) { + + short[] newShortArray = (short[]) newArray; + for (int i = 0; i < newShortArray.length; i++) { + TypedValue typedValue = initializer.getChild(i).getTypedValue(state); + newShortArray[i] = ExpressionUtils.toShort(typeConverter, typedValue); + } + } + + private void populateLongArray(ExpressionState state, Object newArray, TypeConverter typeConverter, + InlineList initializer) { + + long[] newLongArray = (long[]) newArray; + for (int i = 0; i < newLongArray.length; i++) { + TypedValue typedValue = initializer.getChild(i).getTypedValue(state); + newLongArray[i] = ExpressionUtils.toLong(typeConverter, typedValue); + } + } + + private void populateCharArray(ExpressionState state, Object newArray, TypeConverter typeConverter, + InlineList initializer) { + + char[] newCharArray = (char[]) newArray; + for (int i = 0; i < newCharArray.length; i++) { + TypedValue typedValue = initializer.getChild(i).getTypedValue(state); + newCharArray[i] = ExpressionUtils.toChar(typeConverter, typedValue); + } + } + + private void populateBooleanArray(ExpressionState state, Object newArray, TypeConverter typeConverter, + InlineList initializer) { + + boolean[] newBooleanArray = (boolean[]) newArray; + for (int i = 0; i < newBooleanArray.length; i++) { + TypedValue typedValue = initializer.getChild(i).getTypedValue(state); + newBooleanArray[i] = ExpressionUtils.toBoolean(typeConverter, typedValue); + } + } + + private void populateIntArray(ExpressionState state, Object newArray, TypeConverter typeConverter, + InlineList initializer) { + + int[] newIntArray = (int[]) newArray; + for (int i = 0; i < newIntArray.length; i++) { + TypedValue typedValue = initializer.getChild(i).getTypedValue(state); + newIntArray[i] = ExpressionUtils.toInt(typeConverter, typedValue); + } + } + + private boolean hasInitializer() { + return (getChildCount() > 1); + } + + @Override + public boolean isCompilable() { + if (!(this.cachedExecutor instanceof ReflectiveConstructorExecutor) || + this.exitTypeDescriptor == null) { + return false; + } + + if (getChildCount() > 1) { + for (int c = 1, max = getChildCount();c < max; c++) { + if (!this.children[c].isCompilable()) { + return false; + } + } + } + + ReflectiveConstructorExecutor executor = (ReflectiveConstructorExecutor) this.cachedExecutor; + if (executor == null) { + return false; + } + Constructor constructor = executor.getConstructor(); + return (Modifier.isPublic(constructor.getModifiers()) && + Modifier.isPublic(constructor.getDeclaringClass().getModifiers())); + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + ReflectiveConstructorExecutor executor = ((ReflectiveConstructorExecutor) this.cachedExecutor); + Assert.state(executor != null, "No cached executor"); + + Constructor constructor = executor.getConstructor(); + String classDesc = constructor.getDeclaringClass().getName().replace('.', '/'); + mv.visitTypeInsn(NEW, classDesc); + mv.visitInsn(DUP); + + // children[0] is the type of the constructor, don't want to include that in argument processing + SpelNodeImpl[] arguments = new SpelNodeImpl[this.children.length - 1]; + System.arraycopy(this.children, 1, arguments, 0, this.children.length - 1); + generateCodeForArguments(mv, cf, constructor, arguments); + mv.visitMethodInsn(INVOKESPECIAL, classDesc, "", CodeFlow.createSignatureDescriptor(constructor), false); + cf.pushDescriptor(this.exitTypeDescriptor); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Elvis.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Elvis.java new file mode 100644 index 0000000..60b313e --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Elvis.java @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import org.springframework.asm.Label; +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Represents the elvis operator ?:. For an expression "a?:b" if a is not null, the value + * of the expression is "a", if a is null then the value of the expression is "b". + * + * @author Andy Clement + * @author Juergen Hoeller + * @since 3.0 + */ +public class Elvis extends SpelNodeImpl { + + public Elvis(int startPos, int endPos, SpelNodeImpl... args) { + super(startPos, endPos, args); + } + + + /** + * Evaluate the condition and if not null, return it. + * If it is null, return the other value. + * @param state the expression state + * @throws EvaluationException if the condition does not evaluate correctly + * to a boolean or there is a problem executing the chosen alternative + */ + @Override + public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + TypedValue value = this.children[0].getValueInternal(state); + // If this check is changed, the generateCode method will need changing too + if (value.getValue() != null && !"".equals(value.getValue())) { + return value; + } + else { + TypedValue result = this.children[1].getValueInternal(state); + computeExitTypeDescriptor(); + return result; + } + } + + @Override + public String toStringAST() { + return getChild(0).toStringAST() + " ?: " + getChild(1).toStringAST(); + } + + @Override + public boolean isCompilable() { + SpelNodeImpl condition = this.children[0]; + SpelNodeImpl ifNullValue = this.children[1]; + return (condition.isCompilable() && ifNullValue.isCompilable() && + condition.exitTypeDescriptor != null && ifNullValue.exitTypeDescriptor != null); + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + // exit type descriptor can be null if both components are literal expressions + computeExitTypeDescriptor(); + cf.enterCompilationScope(); + this.children[0].generateCode(mv, cf); + String lastDesc = cf.lastDescriptor(); + Assert.state(lastDesc != null, "No last descriptor"); + CodeFlow.insertBoxIfNecessary(mv, lastDesc.charAt(0)); + cf.exitCompilationScope(); + Label elseTarget = new Label(); + Label endOfIf = new Label(); + mv.visitInsn(DUP); + mv.visitJumpInsn(IFNULL, elseTarget); + // Also check if empty string, as per the code in the interpreted version + mv.visitInsn(DUP); + mv.visitLdcInsn(""); + mv.visitInsn(SWAP); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z",false); + mv.visitJumpInsn(IFEQ, endOfIf); // if not empty, drop through to elseTarget + mv.visitLabel(elseTarget); + mv.visitInsn(POP); + cf.enterCompilationScope(); + this.children[1].generateCode(mv, cf); + if (!CodeFlow.isPrimitive(this.exitTypeDescriptor)) { + lastDesc = cf.lastDescriptor(); + Assert.state(lastDesc != null, "No last descriptor"); + CodeFlow.insertBoxIfNecessary(mv, lastDesc.charAt(0)); + } + cf.exitCompilationScope(); + mv.visitLabel(endOfIf); + cf.pushDescriptor(this.exitTypeDescriptor); + } + + private void computeExitTypeDescriptor() { + if (this.exitTypeDescriptor == null && this.children[0].exitTypeDescriptor != null && + this.children[1].exitTypeDescriptor != null) { + String conditionDescriptor = this.children[0].exitTypeDescriptor; + String ifNullValueDescriptor = this.children[1].exitTypeDescriptor; + if (ObjectUtils.nullSafeEquals(conditionDescriptor, ifNullValueDescriptor)) { + this.exitTypeDescriptor = conditionDescriptor; + } + else { + // Use the easiest to compute common super type + this.exitTypeDescriptor = "Ljava/lang/Object"; + } + } + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/FloatLiteral.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/FloatLiteral.java new file mode 100644 index 0000000..ae9169b --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/FloatLiteral.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.CodeFlow; + +/** + * Expression language AST node that represents a float literal. + * + * @author Satyapal Reddy + * @author Andy Clement + * @since 3.2 + */ +public class FloatLiteral extends Literal { + + private final TypedValue value; + + + public FloatLiteral(String payload, int startPos, int endPos, float value) { + super(payload, startPos, endPos); + this.value = new TypedValue(value); + this.exitTypeDescriptor = "F"; + } + + + @Override + public TypedValue getLiteralValue() { + return this.value; + } + + @Override + public boolean isCompilable() { + return true; + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + mv.visitLdcInsn(this.value.getValue()); + cf.pushDescriptor(this.exitTypeDescriptor); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/FormatHelper.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/FormatHelper.java new file mode 100644 index 0000000..b28441d --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/FormatHelper.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.util.List; +import java.util.StringJoiner; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * Utility methods (formatters etc) used during parsing and evaluation. + * + * @author Andy Clement + */ +abstract class FormatHelper { + + /** + * Produce a readable representation for a given method name with specified arguments. + * @param name the name of the method + * @param argumentTypes the types of the arguments to the method + * @return a nicely formatted representation, e.g. {@code foo(String,int)} + */ + public static String formatMethodForMessage(String name, List argumentTypes) { + StringJoiner sj = new StringJoiner(",", "(", ")"); + for (TypeDescriptor typeDescriptor : argumentTypes) { + if (typeDescriptor != null) { + sj.add(formatClassNameForMessage(typeDescriptor.getType())); + } + else { + sj.add(formatClassNameForMessage(null)); + } + } + return name + sj.toString(); + } + + /** + * Determine a readable name for a given Class object. + *

    A String array will have the formatted name "java.lang.String[]". + * @param clazz the Class whose name is to be formatted + * @return a formatted String suitable for message inclusion + * @see ClassUtils#getQualifiedName(Class) + */ + public static String formatClassNameForMessage(@Nullable Class clazz) { + return (clazz != null ? ClassUtils.getQualifiedName(clazz) : "null"); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java new file mode 100644 index 0000000..0417735 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java @@ -0,0 +1,193 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.StringJoiner; + +import org.springframework.asm.MethodVisitor; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.TypeConverter; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.support.ReflectionHelper; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * A function reference is of the form "#someFunction(a,b,c)". Functions may be defined + * in the context prior to the expression being evaluated. Functions may also be static + * Java methods, registered in the context prior to invocation of the expression. + * + *

    Functions are very simplistic. The arguments are not part of the definition + * (right now), so the names must be unique. + * + * @author Andy Clement + * @author Juergen Hoeller + * @since 3.0 + */ +public class FunctionReference extends SpelNodeImpl { + + private final String name; + + // Captures the most recently used method for the function invocation *if* the method + // can safely be used for compilation (i.e. no argument conversion is going on) + @Nullable + private volatile Method method; + + + public FunctionReference(String functionName, int startPos, int endPos, SpelNodeImpl... arguments) { + super(startPos, endPos, arguments); + this.name = functionName; + } + + + @Override + public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + TypedValue value = state.lookupVariable(this.name); + if (value == TypedValue.NULL) { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.FUNCTION_NOT_DEFINED, this.name); + } + if (!(value.getValue() instanceof Method)) { + // Possibly a static Java method registered as a function + throw new SpelEvaluationException( + SpelMessage.FUNCTION_REFERENCE_CANNOT_BE_INVOKED, this.name, value.getClass()); + } + + try { + return executeFunctionJLRMethod(state, (Method) value.getValue()); + } + catch (SpelEvaluationException ex) { + ex.setPosition(getStartPosition()); + throw ex; + } + } + + /** + * Execute a function represented as a {@code java.lang.reflect.Method}. + * @param state the expression evaluation state + * @param method the method to invoke + * @return the return value of the invoked Java method + * @throws EvaluationException if there is any problem invoking the method + */ + private TypedValue executeFunctionJLRMethod(ExpressionState state, Method method) throws EvaluationException { + Object[] functionArgs = getArguments(state); + + if (!method.isVarArgs()) { + int declaredParamCount = method.getParameterCount(); + if (declaredParamCount != functionArgs.length) { + throw new SpelEvaluationException(SpelMessage.INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION, + functionArgs.length, declaredParamCount); + } + } + if (!Modifier.isStatic(method.getModifiers())) { + throw new SpelEvaluationException(getStartPosition(), + SpelMessage.FUNCTION_MUST_BE_STATIC, ClassUtils.getQualifiedMethodName(method), this.name); + } + + // Convert arguments if necessary and remap them for varargs if required + TypeConverter converter = state.getEvaluationContext().getTypeConverter(); + boolean argumentConversionOccurred = ReflectionHelper.convertAllArguments(converter, functionArgs, method); + if (method.isVarArgs()) { + functionArgs = ReflectionHelper.setupArgumentsForVarargsInvocation( + method.getParameterTypes(), functionArgs); + } + boolean compilable = false; + + try { + ReflectionUtils.makeAccessible(method); + Object result = method.invoke(method.getClass(), functionArgs); + compilable = !argumentConversionOccurred; + return new TypedValue(result, new TypeDescriptor(new MethodParameter(method, -1)).narrow(result)); + } + catch (Exception ex) { + throw new SpelEvaluationException(getStartPosition(), ex, SpelMessage.EXCEPTION_DURING_FUNCTION_CALL, + this.name, ex.getMessage()); + } + finally { + if (compilable) { + this.exitTypeDescriptor = CodeFlow.toDescriptor(method.getReturnType()); + this.method = method; + } + else { + this.exitTypeDescriptor = null; + this.method = null; + } + } + } + + @Override + public String toStringAST() { + StringJoiner sj = new StringJoiner(",", "(", ")"); + for (int i = 0; i < getChildCount(); i++) { + sj.add(getChild(i).toStringAST()); + } + return '#' + this.name + sj.toString(); + } + + /** + * Compute the arguments to the function, they are the children of this expression node. + * @return an array of argument values for the function call + */ + private Object[] getArguments(ExpressionState state) throws EvaluationException { + // Compute arguments to the function + Object[] arguments = new Object[getChildCount()]; + for (int i = 0; i < arguments.length; i++) { + arguments[i] = this.children[i].getValueInternal(state).getValue(); + } + return arguments; + } + + @Override + public boolean isCompilable() { + Method method = this.method; + if (method == null) { + return false; + } + int methodModifiers = method.getModifiers(); + if (!Modifier.isStatic(methodModifiers) || !Modifier.isPublic(methodModifiers) || + !Modifier.isPublic(method.getDeclaringClass().getModifiers())) { + return false; + } + for (SpelNodeImpl child : this.children) { + if (!child.isCompilable()) { + return false; + } + } + return true; + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + Method method = this.method; + Assert.state(method != null, "No method handle"); + String classDesc = method.getDeclaringClass().getName().replace('.', '/'); + generateCodeForArguments(mv, cf, method, this.children); + mv.visitMethodInsn(INVOKESTATIC, classDesc, method.getName(), + CodeFlow.createSignatureDescriptor(method), false); + cf.pushDescriptor(this.exitTypeDescriptor); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Identifier.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Identifier.java new file mode 100644 index 0000000..51a35b2 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Identifier.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelNode; + +/** + * An 'identifier' {@link SpelNode}. + * + * @author Andy Clement + * @since 3.0 + */ +public class Identifier extends SpelNodeImpl { + + private final TypedValue id; + + + public Identifier(String payload, int startPos, int endPos) { + super(startPos, endPos); + this.id = new TypedValue(payload); + } + + + @Override + public String toStringAST() { + return String.valueOf(this.id.getValue()); + } + + @Override + public TypedValue getValueInternal(ExpressionState state) { + return this.id; + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java new file mode 100644 index 0000000..04b5028 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java @@ -0,0 +1,781 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +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.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; + +import org.springframework.asm.MethodVisitor; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.TypeConverter; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.support.ReflectivePropertyAccessor; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +/** + * An Indexer can index into some proceeding structure to access a particular piece of it. + * Supported structures are: strings / collections (lists/sets) / arrays. + * + * @author Andy Clement + * @author Phillip Webb + * @author Stephane Nicoll + * @since 3.0 + */ +// TODO support multidimensional arrays +// TODO support correct syntax for multidimensional [][][] and not [,,,] +public class Indexer extends SpelNodeImpl { + + private enum IndexedType {ARRAY, LIST, MAP, STRING, OBJECT} + + + // These fields are used when the indexer is being used as a property read accessor. + // If the name and target type match these cached values then the cachedReadAccessor + // is used to read the property. If they do not match, the correct accessor is + // discovered and then cached for later use. + + @Nullable + private String cachedReadName; + + @Nullable + private Class cachedReadTargetType; + + @Nullable + private PropertyAccessor cachedReadAccessor; + + // These fields are used when the indexer is being used as a property write accessor. + // If the name and target type match these cached values then the cachedWriteAccessor + // is used to write the property. If they do not match, the correct accessor is + // discovered and then cached for later use. + + @Nullable + private String cachedWriteName; + + @Nullable + private Class cachedWriteTargetType; + + @Nullable + private PropertyAccessor cachedWriteAccessor; + + @Nullable + private IndexedType indexedType; + + + public Indexer(int startPos, int endPos, SpelNodeImpl expr) { + super(startPos, endPos, expr); + } + + + @Override + public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + return getValueRef(state).getValue(); + } + + @Override + public void setValue(ExpressionState state, @Nullable Object newValue) throws EvaluationException { + getValueRef(state).setValue(newValue); + } + + @Override + public boolean isWritable(ExpressionState expressionState) throws SpelEvaluationException { + return true; + } + + + @Override + protected ValueRef getValueRef(ExpressionState state) throws EvaluationException { + TypedValue context = state.getActiveContextObject(); + Object target = context.getValue(); + TypeDescriptor targetDescriptor = context.getTypeDescriptor(); + TypedValue indexValue; + Object index; + + // This first part of the if clause prevents a 'double dereference' of the property (SPR-5847) + if (target instanceof Map && (this.children[0] instanceof PropertyOrFieldReference)) { + PropertyOrFieldReference reference = (PropertyOrFieldReference) this.children[0]; + index = reference.getName(); + indexValue = new TypedValue(index); + } + else { + // In case the map key is unqualified, we want it evaluated against the root object + // so temporarily push that on whilst evaluating the key + try { + state.pushActiveContextObject(state.getRootContextObject()); + indexValue = this.children[0].getValueInternal(state); + index = indexValue.getValue(); + Assert.state(index != null, "No index"); + } + finally { + state.popActiveContextObject(); + } + } + + // Raise a proper exception in case of a null target + if (target == null) { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.CANNOT_INDEX_INTO_NULL_VALUE); + } + // At this point, we need a TypeDescriptor for a non-null target object + Assert.state(targetDescriptor != null, "No type descriptor"); + + // Indexing into a Map + if (target instanceof Map) { + Object key = index; + if (targetDescriptor.getMapKeyTypeDescriptor() != null) { + key = state.convertValue(key, targetDescriptor.getMapKeyTypeDescriptor()); + } + this.indexedType = IndexedType.MAP; + return new MapIndexingValueRef(state.getTypeConverter(), (Map) target, key, targetDescriptor); + } + + // If the object is something that looks indexable by an integer, + // attempt to treat the index value as a number + if (target.getClass().isArray() || target instanceof Collection || target instanceof String) { + int idx = (Integer) state.convertValue(index, TypeDescriptor.valueOf(Integer.class)); + if (target.getClass().isArray()) { + this.indexedType = IndexedType.ARRAY; + return new ArrayIndexingValueRef(state.getTypeConverter(), target, idx, targetDescriptor); + } + else if (target instanceof Collection) { + if (target instanceof List) { + this.indexedType = IndexedType.LIST; + } + return new CollectionIndexingValueRef((Collection) target, idx, targetDescriptor, + state.getTypeConverter(), state.getConfiguration().isAutoGrowCollections(), + state.getConfiguration().getMaximumAutoGrowSize()); + } + else { + this.indexedType = IndexedType.STRING; + return new StringIndexingLValue((String) target, idx, targetDescriptor); + } + } + + // Try and treat the index value as a property of the context object + // TODO: could call the conversion service to convert the value to a String + TypeDescriptor valueType = indexValue.getTypeDescriptor(); + if (valueType != null && String.class == valueType.getType()) { + this.indexedType = IndexedType.OBJECT; + return new PropertyIndexingValueRef( + target, (String) index, state.getEvaluationContext(), targetDescriptor); + } + + throw new SpelEvaluationException( + getStartPosition(), SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE, targetDescriptor); + } + + @Override + public boolean isCompilable() { + if (this.indexedType == IndexedType.ARRAY) { + return (this.exitTypeDescriptor != null); + } + else if (this.indexedType == IndexedType.LIST) { + return this.children[0].isCompilable(); + } + else if (this.indexedType == IndexedType.MAP) { + return (this.children[0] instanceof PropertyOrFieldReference || this.children[0].isCompilable()); + } + else if (this.indexedType == IndexedType.OBJECT) { + // If the string name is changing the accessor is clearly going to change (so no compilation possible) + return (this.cachedReadAccessor != null && + this.cachedReadAccessor instanceof ReflectivePropertyAccessor.OptimalPropertyAccessor && + getChild(0) instanceof StringLiteral); + } + return false; + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + String descriptor = cf.lastDescriptor(); + if (descriptor == null) { + // Stack is empty, should use context object + cf.loadTarget(mv); + } + + if (this.indexedType == IndexedType.ARRAY) { + int insn; + if ("D".equals(this.exitTypeDescriptor)) { + mv.visitTypeInsn(CHECKCAST, "[D"); + insn = DALOAD; + } + else if ("F".equals(this.exitTypeDescriptor)) { + mv.visitTypeInsn(CHECKCAST, "[F"); + insn = FALOAD; + } + else if ("J".equals(this.exitTypeDescriptor)) { + mv.visitTypeInsn(CHECKCAST, "[J"); + insn = LALOAD; + } + else if ("I".equals(this.exitTypeDescriptor)) { + mv.visitTypeInsn(CHECKCAST, "[I"); + insn = IALOAD; + } + else if ("S".equals(this.exitTypeDescriptor)) { + mv.visitTypeInsn(CHECKCAST, "[S"); + insn = SALOAD; + } + else if ("B".equals(this.exitTypeDescriptor)) { + mv.visitTypeInsn(CHECKCAST, "[B"); + insn = BALOAD; + } + else if ("C".equals(this.exitTypeDescriptor)) { + mv.visitTypeInsn(CHECKCAST, "[C"); + insn = CALOAD; + } + else { + mv.visitTypeInsn(CHECKCAST, "["+ this.exitTypeDescriptor + + (CodeFlow.isPrimitiveArray(this.exitTypeDescriptor) ? "" : ";")); + //depthPlusOne(exitTypeDescriptor)+"Ljava/lang/Object;"); + insn = AALOAD; + } + SpelNodeImpl index = this.children[0]; + cf.enterCompilationScope(); + index.generateCode(mv, cf); + cf.exitCompilationScope(); + 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(); + mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "get", "(I)Ljava/lang/Object;", true); + } + + else if (this.indexedType == IndexedType.MAP) { + 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)) { + PropertyOrFieldReference reference = (PropertyOrFieldReference) this.children[0]; + String mapKeyName = reference.getName(); + mv.visitLdcInsn(mapKeyName); + } + else { + cf.enterCompilationScope(); + this.children[0].generateCode(mv, cf); + cf.exitCompilationScope(); + } + mv.visitMethodInsn( + INVOKEINTERFACE, "java/util/Map", "get", "(Ljava/lang/Object;)Ljava/lang/Object;", true); + } + + else if (this.indexedType == IndexedType.OBJECT) { + ReflectivePropertyAccessor.OptimalPropertyAccessor accessor = + (ReflectivePropertyAccessor.OptimalPropertyAccessor) this.cachedReadAccessor; + Assert.state(accessor != null, "No cached read accessor"); + Member member = accessor.member; + boolean isStatic = Modifier.isStatic(member.getModifiers()); + String classDesc = member.getDeclaringClass().getName().replace('.', '/'); + + if (!isStatic) { + if (descriptor == null) { + cf.loadTarget(mv); + } + if (descriptor == null || !classDesc.equals(descriptor.substring(1))) { + mv.visitTypeInsn(CHECKCAST, classDesc); + } + } + + if (member instanceof Method) { + mv.visitMethodInsn((isStatic? INVOKESTATIC : INVOKEVIRTUAL), classDesc, member.getName(), + CodeFlow.createSignatureDescriptor((Method) member), false); + } + else { + mv.visitFieldInsn((isStatic ? GETSTATIC : GETFIELD), classDesc, member.getName(), + CodeFlow.toJvmDescriptor(((Field) member).getType())); + } + } + + cf.pushDescriptor(this.exitTypeDescriptor); + } + + @Override + public String toStringAST() { + StringJoiner sj = new StringJoiner(",", "[", "]"); + for (int i = 0; i < getChildCount(); i++) { + sj.add(getChild(i).toStringAST()); + } + return sj.toString(); + } + + + private void setArrayElement(TypeConverter converter, Object ctx, int idx, @Nullable Object newValue, + Class arrayComponentType) throws EvaluationException { + + if (arrayComponentType == Boolean.TYPE) { + boolean[] array = (boolean[]) ctx; + checkAccess(array.length, idx); + array[idx] = convertValue(converter, newValue, Boolean.class); + } + else if (arrayComponentType == Byte.TYPE) { + byte[] array = (byte[]) ctx; + checkAccess(array.length, idx); + array[idx] = convertValue(converter, newValue, Byte.class); + } + else if (arrayComponentType == Character.TYPE) { + char[] array = (char[]) ctx; + checkAccess(array.length, idx); + array[idx] = convertValue(converter, newValue, Character.class); + } + else if (arrayComponentType == Double.TYPE) { + double[] array = (double[]) ctx; + checkAccess(array.length, idx); + array[idx] = convertValue(converter, newValue, Double.class); + } + else if (arrayComponentType == Float.TYPE) { + float[] array = (float[]) ctx; + checkAccess(array.length, idx); + array[idx] = convertValue(converter, newValue, Float.class); + } + else if (arrayComponentType == Integer.TYPE) { + int[] array = (int[]) ctx; + checkAccess(array.length, idx); + array[idx] = convertValue(converter, newValue, Integer.class); + } + else if (arrayComponentType == Long.TYPE) { + long[] array = (long[]) ctx; + checkAccess(array.length, idx); + array[idx] = convertValue(converter, newValue, Long.class); + } + else if (arrayComponentType == Short.TYPE) { + short[] array = (short[]) ctx; + checkAccess(array.length, idx); + array[idx] = convertValue(converter, newValue, Short.class); + } + else { + Object[] array = (Object[]) ctx; + checkAccess(array.length, idx); + array[idx] = convertValue(converter, newValue, arrayComponentType); + } + } + + private Object accessArrayElement(Object ctx, int idx) throws SpelEvaluationException { + Class arrayComponentType = ctx.getClass().getComponentType(); + if (arrayComponentType == Boolean.TYPE) { + boolean[] array = (boolean[]) ctx; + checkAccess(array.length, idx); + this.exitTypeDescriptor = "Z"; + return array[idx]; + } + else if (arrayComponentType == Byte.TYPE) { + byte[] array = (byte[]) ctx; + checkAccess(array.length, idx); + this.exitTypeDescriptor = "B"; + return array[idx]; + } + else if (arrayComponentType == Character.TYPE) { + char[] array = (char[]) ctx; + checkAccess(array.length, idx); + this.exitTypeDescriptor = "C"; + return array[idx]; + } + else if (arrayComponentType == Double.TYPE) { + double[] array = (double[]) ctx; + checkAccess(array.length, idx); + this.exitTypeDescriptor = "D"; + return array[idx]; + } + else if (arrayComponentType == Float.TYPE) { + float[] array = (float[]) ctx; + checkAccess(array.length, idx); + this.exitTypeDescriptor = "F"; + return array[idx]; + } + else if (arrayComponentType == Integer.TYPE) { + int[] array = (int[]) ctx; + checkAccess(array.length, idx); + this.exitTypeDescriptor = "I"; + return array[idx]; + } + else if (arrayComponentType == Long.TYPE) { + long[] array = (long[]) ctx; + checkAccess(array.length, idx); + this.exitTypeDescriptor = "J"; + return array[idx]; + } + else if (arrayComponentType == Short.TYPE) { + short[] array = (short[]) ctx; + checkAccess(array.length, idx); + this.exitTypeDescriptor = "S"; + return array[idx]; + } + else { + Object[] array = (Object[]) ctx; + checkAccess(array.length, idx); + Object retValue = array[idx]; + this.exitTypeDescriptor = CodeFlow.toDescriptor(arrayComponentType); + return retValue; + } + } + + private void checkAccess(int arrayLength, int index) throws SpelEvaluationException { + if (index >= arrayLength) { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.ARRAY_INDEX_OUT_OF_BOUNDS, + arrayLength, index); + } + } + + @SuppressWarnings("unchecked") + private T convertValue(TypeConverter converter, @Nullable Object value, Class targetType) { + T result = (T) converter.convertValue( + value, TypeDescriptor.forObject(value), TypeDescriptor.valueOf(targetType)); + if (result == null) { + throw new IllegalStateException("Null conversion result for index [" + value + "]"); + } + return result; + } + + + private class ArrayIndexingValueRef implements ValueRef { + + private final TypeConverter typeConverter; + + private final Object array; + + private final int index; + + private final TypeDescriptor typeDescriptor; + + ArrayIndexingValueRef(TypeConverter typeConverter, Object array, int index, TypeDescriptor typeDescriptor) { + this.typeConverter = typeConverter; + this.array = array; + this.index = index; + this.typeDescriptor = typeDescriptor; + } + + @Override + public TypedValue getValue() { + Object arrayElement = accessArrayElement(this.array, this.index); + return new TypedValue(arrayElement, this.typeDescriptor.elementTypeDescriptor(arrayElement)); + } + + @Override + public void setValue(@Nullable Object newValue) { + TypeDescriptor elementType = this.typeDescriptor.getElementTypeDescriptor(); + Assert.state(elementType != null, "No element type"); + setArrayElement(this.typeConverter, this.array, this.index, newValue, elementType.getType()); + } + + @Override + public boolean isWritable() { + return true; + } + } + + + @SuppressWarnings({"rawtypes", "unchecked"}) + private class MapIndexingValueRef implements ValueRef { + + private final TypeConverter typeConverter; + + private final Map map; + + @Nullable + private final Object key; + + private final TypeDescriptor mapEntryDescriptor; + + public MapIndexingValueRef( + TypeConverter typeConverter, Map map, @Nullable Object key, TypeDescriptor mapEntryDescriptor) { + + this.typeConverter = typeConverter; + this.map = map; + this.key = key; + this.mapEntryDescriptor = mapEntryDescriptor; + } + + @Override + public TypedValue getValue() { + Object value = this.map.get(this.key); + exitTypeDescriptor = CodeFlow.toDescriptor(Object.class); + return new TypedValue(value, this.mapEntryDescriptor.getMapValueTypeDescriptor(value)); + } + + @Override + public void setValue(@Nullable Object newValue) { + if (this.mapEntryDescriptor.getMapValueTypeDescriptor() != null) { + newValue = this.typeConverter.convertValue(newValue, TypeDescriptor.forObject(newValue), + this.mapEntryDescriptor.getMapValueTypeDescriptor()); + } + this.map.put(this.key, newValue); + } + + @Override + public boolean isWritable() { + return true; + } + } + + + private class PropertyIndexingValueRef implements ValueRef { + + private final Object targetObject; + + private final String name; + + private final EvaluationContext evaluationContext; + + private final TypeDescriptor targetObjectTypeDescriptor; + + public PropertyIndexingValueRef(Object targetObject, String value, + EvaluationContext evaluationContext, TypeDescriptor targetObjectTypeDescriptor) { + + this.targetObject = targetObject; + this.name = value; + this.evaluationContext = evaluationContext; + this.targetObjectTypeDescriptor = targetObjectTypeDescriptor; + } + + @Override + public TypedValue getValue() { + Class targetObjectRuntimeClass = getObjectClass(this.targetObject); + try { + if (Indexer.this.cachedReadName != null && Indexer.this.cachedReadName.equals(this.name) && + Indexer.this.cachedReadTargetType != null && + Indexer.this.cachedReadTargetType.equals(targetObjectRuntimeClass)) { + // It is OK to use the cached accessor + PropertyAccessor accessor = Indexer.this.cachedReadAccessor; + Assert.state(accessor != null, "No cached read accessor"); + return accessor.read(this.evaluationContext, this.targetObject, this.name); + } + List accessorsToTry = AstUtils.getPropertyAccessorsToTry( + targetObjectRuntimeClass, this.evaluationContext.getPropertyAccessors()); + for (PropertyAccessor accessor : accessorsToTry) { + if (accessor.canRead(this.evaluationContext, this.targetObject, this.name)) { + if (accessor instanceof ReflectivePropertyAccessor) { + accessor = ((ReflectivePropertyAccessor) accessor).createOptimalAccessor( + this.evaluationContext, this.targetObject, this.name); + } + Indexer.this.cachedReadAccessor = accessor; + Indexer.this.cachedReadName = this.name; + Indexer.this.cachedReadTargetType = targetObjectRuntimeClass; + if (accessor instanceof ReflectivePropertyAccessor.OptimalPropertyAccessor) { + ReflectivePropertyAccessor.OptimalPropertyAccessor optimalAccessor = + (ReflectivePropertyAccessor.OptimalPropertyAccessor) accessor; + Member member = optimalAccessor.member; + Indexer.this.exitTypeDescriptor = CodeFlow.toDescriptor(member instanceof Method ? + ((Method) member).getReturnType() : ((Field) member).getType()); + } + return accessor.read(this.evaluationContext, this.targetObject, this.name); + } + } + } + catch (AccessException ex) { + throw new SpelEvaluationException(getStartPosition(), ex, + SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE, this.targetObjectTypeDescriptor.toString()); + } + throw new SpelEvaluationException(getStartPosition(), + SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE, this.targetObjectTypeDescriptor.toString()); + } + + @Override + public void setValue(@Nullable Object newValue) { + Class contextObjectClass = getObjectClass(this.targetObject); + try { + if (Indexer.this.cachedWriteName != null && Indexer.this.cachedWriteName.equals(this.name) && + Indexer.this.cachedWriteTargetType != null && + Indexer.this.cachedWriteTargetType.equals(contextObjectClass)) { + // It is OK to use the cached accessor + PropertyAccessor accessor = Indexer.this.cachedWriteAccessor; + Assert.state(accessor != null, "No cached write accessor"); + accessor.write(this.evaluationContext, this.targetObject, this.name, newValue); + return; + } + List accessorsToTry = AstUtils.getPropertyAccessorsToTry( + contextObjectClass, this.evaluationContext.getPropertyAccessors()); + for (PropertyAccessor accessor : accessorsToTry) { + if (accessor.canWrite(this.evaluationContext, this.targetObject, this.name)) { + Indexer.this.cachedWriteName = this.name; + Indexer.this.cachedWriteTargetType = contextObjectClass; + Indexer.this.cachedWriteAccessor = accessor; + accessor.write(this.evaluationContext, this.targetObject, this.name, newValue); + return; + } + } + } + catch (AccessException ex) { + throw new SpelEvaluationException(getStartPosition(), ex, + SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE, this.name, ex.getMessage()); + } + } + + @Override + public boolean isWritable() { + return true; + } + } + + + @SuppressWarnings({"rawtypes", "unchecked"}) + private class CollectionIndexingValueRef implements ValueRef { + + private final Collection collection; + + private final int index; + + private final TypeDescriptor collectionEntryDescriptor; + + private final TypeConverter typeConverter; + + private final boolean growCollection; + + private final int maximumSize; + + public CollectionIndexingValueRef(Collection collection, int index, TypeDescriptor collectionEntryDescriptor, + TypeConverter typeConverter, boolean growCollection, int maximumSize) { + + this.collection = collection; + this.index = index; + this.collectionEntryDescriptor = collectionEntryDescriptor; + this.typeConverter = typeConverter; + this.growCollection = growCollection; + this.maximumSize = maximumSize; + } + + @Override + public TypedValue getValue() { + growCollectionIfNecessary(); + if (this.collection instanceof List) { + Object o = ((List) this.collection).get(this.index); + exitTypeDescriptor = CodeFlow.toDescriptor(Object.class); + return new TypedValue(o, this.collectionEntryDescriptor.elementTypeDescriptor(o)); + } + int pos = 0; + for (Object o : this.collection) { + if (pos == this.index) { + return new TypedValue(o, this.collectionEntryDescriptor.elementTypeDescriptor(o)); + } + pos++; + } + throw new IllegalStateException("Failed to find indexed element " + this.index + ": " + this.collection); + } + + @Override + public void setValue(@Nullable Object newValue) { + growCollectionIfNecessary(); + if (this.collection instanceof List) { + List list = (List) this.collection; + if (this.collectionEntryDescriptor.getElementTypeDescriptor() != null) { + newValue = this.typeConverter.convertValue(newValue, TypeDescriptor.forObject(newValue), + this.collectionEntryDescriptor.getElementTypeDescriptor()); + } + list.set(this.index, newValue); + } + else { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE, + this.collectionEntryDescriptor.toString()); + } + } + + private void growCollectionIfNecessary() { + if (this.index >= this.collection.size()) { + if (!this.growCollection) { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.COLLECTION_INDEX_OUT_OF_BOUNDS, + this.collection.size(), this.index); + } + if (this.index >= this.maximumSize) { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.UNABLE_TO_GROW_COLLECTION); + } + if (this.collectionEntryDescriptor.getElementTypeDescriptor() == null) { + throw new SpelEvaluationException( + getStartPosition(), SpelMessage.UNABLE_TO_GROW_COLLECTION_UNKNOWN_ELEMENT_TYPE); + } + TypeDescriptor elementType = this.collectionEntryDescriptor.getElementTypeDescriptor(); + try { + Constructor ctor = getDefaultConstructor(elementType.getType()); + int newElements = this.index - this.collection.size(); + while (newElements >= 0) { + // Insert a null value if the element type does not have a default constructor. + this.collection.add(ctor != null ? ctor.newInstance() : null); + newElements--; + } + } + catch (Throwable ex) { + throw new SpelEvaluationException(getStartPosition(), ex, SpelMessage.UNABLE_TO_GROW_COLLECTION); + } + } + } + + @Nullable + private Constructor getDefaultConstructor(Class type) { + try { + return ReflectionUtils.accessibleConstructor(type); + } + catch (Throwable ex) { + return null; + } + } + + @Override + public boolean isWritable() { + return true; + } + } + + + private class StringIndexingLValue implements ValueRef { + + private final String target; + + private final int index; + + private final TypeDescriptor typeDescriptor; + + public StringIndexingLValue(String target, int index, TypeDescriptor typeDescriptor) { + this.target = target; + this.index = index; + this.typeDescriptor = typeDescriptor; + } + + @Override + public TypedValue getValue() { + if (this.index >= this.target.length()) { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.STRING_INDEX_OUT_OF_BOUNDS, + this.target.length(), this.index); + } + return new TypedValue(String.valueOf(this.target.charAt(this.index))); + } + + @Override + public void setValue(@Nullable Object newValue) { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE, + this.typeDescriptor.toString()); + } + + @Override + public boolean isWritable() { + return true; + } + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineList.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineList.java new file mode 100644 index 0000000..4fa298e --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineList.java @@ -0,0 +1,182 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.StringJoiner; + +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelNode; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Represent a list in an expression, e.g. '{1,2,3}' + * + * @author Andy Clement + * @since 3.0.4 + */ +public class InlineList extends SpelNodeImpl { + + // If the list is purely literals, it is a constant value and can be computed and cached + @Nullable + private TypedValue constant; // TODO must be immutable list + + + public InlineList(int startPos, int endPos, SpelNodeImpl... args) { + super(startPos, endPos, args); + checkIfConstant(); + } + + + /** + * If all the components of the list are constants, or lists that themselves contain constants, then a constant list + * can be built to represent this node. This will speed up later getValue calls and reduce the amount of garbage + * created. + */ + private void checkIfConstant() { + boolean isConstant = true; + for (int c = 0, max = getChildCount(); c < max; c++) { + SpelNode child = getChild(c); + if (!(child instanceof Literal)) { + if (child instanceof InlineList) { + InlineList inlineList = (InlineList) child; + if (!inlineList.isConstant()) { + isConstant = false; + } + } + else { + isConstant = false; + } + } + } + if (isConstant) { + List constantList = new ArrayList<>(); + int childcount = getChildCount(); + for (int c = 0; c < childcount; c++) { + SpelNode child = getChild(c); + if ((child instanceof Literal)) { + constantList.add(((Literal) child).getLiteralValue().getValue()); + } + else if (child instanceof InlineList) { + constantList.add(((InlineList) child).getConstantValue()); + } + } + this.constant = new TypedValue(Collections.unmodifiableList(constantList)); + } + } + + @Override + public TypedValue getValueInternal(ExpressionState expressionState) throws EvaluationException { + if (this.constant != null) { + return this.constant; + } + else { + int childCount = getChildCount(); + List returnValue = new ArrayList<>(childCount); + for (int c = 0; c < childCount; c++) { + returnValue.add(getChild(c).getValue(expressionState)); + } + return new TypedValue(returnValue); + } + } + + @Override + public String toStringAST() { + StringJoiner sj = new StringJoiner(",", "{", "}"); + // String ast matches input string, not the 'toString()' of the resultant collection, which would use [] + int count = getChildCount(); + for (int c = 0; c < count; c++) { + sj.add(getChild(c).toStringAST()); + } + return sj.toString(); + } + + /** + * Return whether this list is a constant value. + */ + public boolean isConstant() { + return (this.constant != null); + } + + @SuppressWarnings("unchecked") + @Nullable + public List getConstantValue() { + Assert.state(this.constant != null, "No constant"); + return (List) this.constant.getValue(); + } + + @Override + public boolean isCompilable() { + return isConstant(); + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow codeflow) { + final String constantFieldName = "inlineList$" + codeflow.nextFieldId(); + final String className = codeflow.getClassName(); + + codeflow.registerNewField((cw, cflow) -> + cw.visitField(ACC_PRIVATE | ACC_STATIC | ACC_FINAL, constantFieldName, "Ljava/util/List;", null, null)); + + codeflow.registerNewClinit((mVisitor, cflow) -> + generateClinitCode(className, constantFieldName, mVisitor, cflow, false)); + + mv.visitFieldInsn(GETSTATIC, className, constantFieldName, "Ljava/util/List;"); + codeflow.pushDescriptor("Ljava/util/List"); + } + + void generateClinitCode(String clazzname, String constantFieldName, MethodVisitor mv, CodeFlow codeflow, boolean nested) { + mv.visitTypeInsn(NEW, "java/util/ArrayList"); + mv.visitInsn(DUP); + mv.visitMethodInsn(INVOKESPECIAL, "java/util/ArrayList", "", "()V", false); + if (!nested) { + mv.visitFieldInsn(PUTSTATIC, clazzname, constantFieldName, "Ljava/util/List;"); + } + int childCount = getChildCount(); + for (int c = 0; c < childCount; c++) { + if (!nested) { + mv.visitFieldInsn(GETSTATIC, clazzname, constantFieldName, "Ljava/util/List;"); + } + else { + mv.visitInsn(DUP); + } + // The children might be further lists if they are not constants. In this + // situation do not call back into generateCode() because it will register another clinit adder. + // Instead, directly build the list here: + if (this.children[c] instanceof InlineList) { + ((InlineList)this.children[c]).generateClinitCode(clazzname, constantFieldName, mv, codeflow, true); + } + else { + this.children[c].generateCode(mv, codeflow); + String lastDesc = codeflow.lastDescriptor(); + if (CodeFlow.isPrimitive(lastDesc)) { + CodeFlow.insertBoxIfNecessary(mv, lastDesc.charAt(0)); + } + } + mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "add", "(Ljava/lang/Object;)Z", true); + mv.visitInsn(POP); + } + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineMap.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineMap.java new file mode 100644 index 0000000..b9aa0d5 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineMap.java @@ -0,0 +1,167 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.expression.EvaluationException; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelNode; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Represent a map in an expression, e.g. '{name:'foo',age:12}' + * + * @author Andy Clement + * @since 4.1 + */ +public class InlineMap extends SpelNodeImpl { + + // If the map is purely literals, it is a constant value and can be computed and cached + @Nullable + private TypedValue constant; + + + public InlineMap(int startPos, int endPos, SpelNodeImpl... args) { + super(startPos, endPos, args); + checkIfConstant(); + } + + + /** + * If all the components of the map are constants, or lists/maps that themselves + * contain constants, then a constant list can be built to represent this node. + * This will speed up later getValue calls and reduce the amount of garbage created. + */ + private void checkIfConstant() { + boolean isConstant = true; + for (int c = 0, max = getChildCount(); c < max; c++) { + SpelNode child = getChild(c); + if (!(child instanceof Literal)) { + if (child instanceof InlineList) { + InlineList inlineList = (InlineList) child; + if (!inlineList.isConstant()) { + isConstant = false; + break; + } + } + else if (child instanceof InlineMap) { + InlineMap inlineMap = (InlineMap) child; + if (!inlineMap.isConstant()) { + isConstant = false; + break; + } + } + else if (!(c % 2 == 0 && child instanceof PropertyOrFieldReference)) { + isConstant = false; + break; + } + } + } + if (isConstant) { + Map constantMap = new LinkedHashMap<>(); + int childCount = getChildCount(); + for (int c = 0; c < childCount; c++) { + SpelNode keyChild = getChild(c++); + SpelNode valueChild = getChild(c); + Object key = null; + Object value = null; + if (keyChild instanceof Literal) { + key = ((Literal) keyChild).getLiteralValue().getValue(); + } + else if (keyChild instanceof PropertyOrFieldReference) { + key = ((PropertyOrFieldReference) keyChild).getName(); + } + else { + return; + } + if (valueChild instanceof Literal) { + value = ((Literal) valueChild).getLiteralValue().getValue(); + } + else if (valueChild instanceof InlineList) { + value = ((InlineList) valueChild).getConstantValue(); + } + else if (valueChild instanceof InlineMap) { + value = ((InlineMap) valueChild).getConstantValue(); + } + constantMap.put(key, value); + } + this.constant = new TypedValue(Collections.unmodifiableMap(constantMap)); + } + } + + @Override + public TypedValue getValueInternal(ExpressionState expressionState) throws EvaluationException { + if (this.constant != null) { + return this.constant; + } + else { + Map returnValue = new LinkedHashMap<>(); + int childcount = getChildCount(); + for (int c = 0; c < childcount; c++) { + // TODO allow for key being PropertyOrFieldReference like Indexer on maps + SpelNode keyChild = getChild(c++); + Object key = null; + if (keyChild instanceof PropertyOrFieldReference) { + PropertyOrFieldReference reference = (PropertyOrFieldReference) keyChild; + key = reference.getName(); + } + else { + key = keyChild.getValue(expressionState); + } + Object value = getChild(c).getValue(expressionState); + returnValue.put(key, value); + } + return new TypedValue(returnValue); + } + } + + @Override + public String toStringAST() { + StringBuilder sb = new StringBuilder("{"); + int count = getChildCount(); + for (int c = 0; c < count; c++) { + if (c > 0) { + sb.append(","); + } + sb.append(getChild(c++).toStringAST()); + sb.append(":"); + sb.append(getChild(c).toStringAST()); + } + sb.append("}"); + return sb.toString(); + } + + /** + * Return whether this list is a constant value. + */ + public boolean isConstant() { + return this.constant != null; + } + + @SuppressWarnings("unchecked") + @Nullable + public Map getConstantValue() { + Assert.state(this.constant != null, "No constant"); + return (Map) this.constant.getValue(); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/IntLiteral.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/IntLiteral.java new file mode 100644 index 0000000..1e63d0c --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/IntLiteral.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.util.Assert; + +/** + * Expression language AST node that represents an integer literal. + * + * @author Andy Clement + * @since 3.0 + */ +public class IntLiteral extends Literal { + + private final TypedValue value; + + + public IntLiteral(String payload, int startPos, int endPos, int value) { + super(payload, startPos, endPos); + this.value = new TypedValue(value); + this.exitTypeDescriptor = "I"; + } + + + @Override + public TypedValue getLiteralValue() { + return this.value; + } + + @Override + public boolean isCompilable() { + return true; + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + Integer intValue = (Integer) this.value.getValue(); + Assert.state(intValue != null, "No int value"); + if (intValue == -1) { + // Not sure we can get here because -1 is OpMinus + mv.visitInsn(ICONST_M1); + } + else if (intValue >= 0 && intValue < 6) { + mv.visitInsn(ICONST_0 + intValue); + } + else { + mv.visitLdcInsn(intValue); + } + cf.pushDescriptor(this.exitTypeDescriptor); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Literal.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Literal.java new file mode 100644 index 0000000..6455f3a --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Literal.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.InternalParseException; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.SpelParseException; +import org.springframework.lang.Nullable; + +/** + * Common superclass for nodes representing literals (boolean, string, number, etc). + * + * @author Andy Clement + * @author Juergen Hoeller + */ +public abstract class Literal extends SpelNodeImpl { + + @Nullable + private final String originalValue; + + + public Literal(@Nullable String originalValue, int startPos, int endPos) { + super(startPos, endPos); + this.originalValue = originalValue; + } + + + @Nullable + public final String getOriginalValue() { + return this.originalValue; + } + + @Override + public final TypedValue getValueInternal(ExpressionState state) throws SpelEvaluationException { + return getLiteralValue(); + } + + @Override + public String toString() { + return String.valueOf(getLiteralValue().getValue()); + } + + @Override + public String toStringAST() { + return toString(); + } + + + public abstract TypedValue getLiteralValue(); + + + /** + * Process the string form of a number, using the specified base if supplied + * and return an appropriate literal to hold it. Any suffix to indicate a + * long will be taken into account (either 'l' or 'L' is supported). + * @param numberToken the token holding the number as its payload (eg. 1234 or 0xCAFE) + * @param radix the base of number + * @return a subtype of Literal that can represent it + */ + public static Literal getIntLiteral(String numberToken, int startPos, int endPos, int radix) { + try { + int value = Integer.parseInt(numberToken, radix); + return new IntLiteral(numberToken, startPos, endPos, value); + } + catch (NumberFormatException ex) { + throw new InternalParseException(new SpelParseException(startPos, ex, SpelMessage.NOT_AN_INTEGER, numberToken)); + } + } + + public static Literal getLongLiteral(String numberToken, int startPos, int endPos, int radix) { + try { + long value = Long.parseLong(numberToken, radix); + return new LongLiteral(numberToken, startPos, endPos, value); + } + catch (NumberFormatException ex) { + throw new InternalParseException(new SpelParseException(startPos, ex, SpelMessage.NOT_A_LONG, numberToken)); + } + } + + public static Literal getRealLiteral(String numberToken, int startPos, int endPos, boolean isFloat) { + try { + if (isFloat) { + float value = Float.parseFloat(numberToken); + return new FloatLiteral(numberToken, startPos, endPos, value); + } + else { + double value = Double.parseDouble(numberToken); + return new RealLiteral(numberToken, startPos, endPos, value); + } + } + catch (NumberFormatException ex) { + throw new InternalParseException(new SpelParseException(startPos, ex, SpelMessage.NOT_A_REAL, numberToken)); + } + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/LongLiteral.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/LongLiteral.java new file mode 100644 index 0000000..bbaf12c --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/LongLiteral.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.CodeFlow; + +/** + * Expression language AST node that represents a long integer literal. + * + * @author Andy Clement + * @since 3.0 + */ +public class LongLiteral extends Literal { + + private final TypedValue value; + + + public LongLiteral(String payload, int startPos, int endPos, long value) { + super(payload, startPos, endPos); + this.value = new TypedValue(value); + this.exitTypeDescriptor = "J"; + } + + + @Override + public TypedValue getLiteralValue() { + return this.value; + } + + @Override + public boolean isCompilable() { + return true; + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + mv.visitLdcInsn(this.value.getValue()); + cf.pushDescriptor(this.exitTypeDescriptor); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java new file mode 100644 index 0000000..cbaf1c8 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java @@ -0,0 +1,440 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.StringJoiner; + +import org.springframework.asm.Label; +import org.springframework.asm.MethodVisitor; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.ExpressionInvocationTargetException; +import org.springframework.expression.MethodExecutor; +import org.springframework.expression.MethodResolver; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.support.ReflectiveMethodExecutor; +import org.springframework.expression.spel.support.ReflectiveMethodResolver; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Expression language AST node that represents a method reference. + * + * @author Andy Clement + * @author Juergen Hoeller + * @since 3.0 + */ +public class MethodReference extends SpelNodeImpl { + + private final String name; + + private final boolean nullSafe; + + @Nullable + private String originalPrimitiveExitTypeDescriptor; + + @Nullable + private volatile CachedMethodExecutor cachedExecutor; + + + public MethodReference(boolean nullSafe, String methodName, int startPos, int endPos, SpelNodeImpl... arguments) { + super(startPos, endPos, arguments); + this.name = methodName; + this.nullSafe = nullSafe; + } + + + public final String getName() { + return this.name; + } + + @Override + protected ValueRef getValueRef(ExpressionState state) throws EvaluationException { + Object[] arguments = getArguments(state); + if (state.getActiveContextObject().getValue() == null) { + throwIfNotNullSafe(getArgumentTypes(arguments)); + return ValueRef.NullValueRef.INSTANCE; + } + return new MethodValueRef(state, arguments); + } + + @Override + public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + EvaluationContext evaluationContext = state.getEvaluationContext(); + Object value = state.getActiveContextObject().getValue(); + TypeDescriptor targetType = state.getActiveContextObject().getTypeDescriptor(); + Object[] arguments = getArguments(state); + TypedValue result = getValueInternal(evaluationContext, value, targetType, arguments); + updateExitTypeDescriptor(); + return result; + } + + private TypedValue getValueInternal(EvaluationContext evaluationContext, + @Nullable Object value, @Nullable TypeDescriptor targetType, Object[] arguments) { + + List argumentTypes = getArgumentTypes(arguments); + if (value == null) { + throwIfNotNullSafe(argumentTypes); + return TypedValue.NULL; + } + + MethodExecutor executorToUse = getCachedExecutor(evaluationContext, value, targetType, argumentTypes); + if (executorToUse != null) { + try { + return executorToUse.execute(evaluationContext, value, arguments); + } + catch (AccessException ex) { + // Two reasons this can occur: + // 1. the method invoked actually threw a real exception + // 2. the method invoked was not passed the arguments it expected and + // has become 'stale' + + // In the first case we should not retry, in the second case we should see + // if there is a better suited method. + + // To determine the situation, the AccessException will contain a cause. + // If the cause is an InvocationTargetException, a user exception was + // thrown inside the method. Otherwise the method could not be invoked. + throwSimpleExceptionIfPossible(value, ex); + + // At this point we know it wasn't a user problem so worth a retry if a + // better candidate can be found. + this.cachedExecutor = null; + } + } + + // either there was no accessor or it no longer existed + executorToUse = findAccessorForMethod(argumentTypes, value, evaluationContext); + this.cachedExecutor = new CachedMethodExecutor( + executorToUse, (value instanceof Class ? (Class) value : null), targetType, argumentTypes); + try { + return executorToUse.execute(evaluationContext, value, arguments); + } + catch (AccessException ex) { + // Same unwrapping exception handling as above in above catch block + throwSimpleExceptionIfPossible(value, ex); + throw new SpelEvaluationException(getStartPosition(), ex, + SpelMessage.EXCEPTION_DURING_METHOD_INVOCATION, this.name, + value.getClass().getName(), ex.getMessage()); + } + } + + private void throwIfNotNullSafe(List argumentTypes) { + if (!this.nullSafe) { + throw new SpelEvaluationException(getStartPosition(), + SpelMessage.METHOD_CALL_ON_NULL_OBJECT_NOT_ALLOWED, + FormatHelper.formatMethodForMessage(this.name, argumentTypes)); + } + } + + private Object[] getArguments(ExpressionState state) { + Object[] arguments = new Object[getChildCount()]; + for (int i = 0; i < arguments.length; i++) { + // Make the root object the active context again for evaluating the parameter expressions + try { + state.pushActiveContextObject(state.getScopeRootContextObject()); + arguments[i] = this.children[i].getValueInternal(state).getValue(); + } + finally { + state.popActiveContextObject(); + } + } + return arguments; + } + + private List getArgumentTypes(Object... arguments) { + List descriptors = new ArrayList<>(arguments.length); + for (Object argument : arguments) { + descriptors.add(TypeDescriptor.forObject(argument)); + } + return Collections.unmodifiableList(descriptors); + } + + @Nullable + private MethodExecutor getCachedExecutor(EvaluationContext evaluationContext, Object value, + @Nullable TypeDescriptor target, List argumentTypes) { + + List methodResolvers = evaluationContext.getMethodResolvers(); + if (methodResolvers.size() != 1 || !(methodResolvers.get(0) instanceof ReflectiveMethodResolver)) { + // Not a default ReflectiveMethodResolver - don't know whether caching is valid + return null; + } + + CachedMethodExecutor executorToCheck = this.cachedExecutor; + if (executorToCheck != null && executorToCheck.isSuitable(value, target, argumentTypes)) { + return executorToCheck.get(); + } + this.cachedExecutor = null; + return null; + } + + private MethodExecutor findAccessorForMethod(List argumentTypes, Object targetObject, + EvaluationContext evaluationContext) throws SpelEvaluationException { + + AccessException accessException = null; + List methodResolvers = evaluationContext.getMethodResolvers(); + for (MethodResolver methodResolver : methodResolvers) { + try { + MethodExecutor methodExecutor = methodResolver.resolve( + evaluationContext, targetObject, this.name, argumentTypes); + if (methodExecutor != null) { + return methodExecutor; + } + } + catch (AccessException ex) { + accessException = ex; + break; + } + } + + String method = FormatHelper.formatMethodForMessage(this.name, argumentTypes); + String className = FormatHelper.formatClassNameForMessage( + targetObject instanceof Class ? ((Class) targetObject) : targetObject.getClass()); + if (accessException != null) { + throw new SpelEvaluationException( + getStartPosition(), accessException, SpelMessage.PROBLEM_LOCATING_METHOD, method, className); + } + else { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.METHOD_NOT_FOUND, method, className); + } + } + + /** + * Decode the AccessException, throwing a lightweight evaluation exception or, + * if the cause was a RuntimeException, throw the RuntimeException directly. + */ + private void throwSimpleExceptionIfPossible(Object value, AccessException ex) { + if (ex.getCause() instanceof InvocationTargetException) { + Throwable rootCause = ex.getCause().getCause(); + if (rootCause instanceof RuntimeException) { + throw (RuntimeException) rootCause; + } + throw new ExpressionInvocationTargetException(getStartPosition(), + "A problem occurred when trying to execute method '" + this.name + + "' on object of type [" + value.getClass().getName() + "]", rootCause); + } + } + + private void updateExitTypeDescriptor() { + CachedMethodExecutor executorToCheck = this.cachedExecutor; + if (executorToCheck != null && executorToCheck.get() instanceof ReflectiveMethodExecutor) { + Method method = ((ReflectiveMethodExecutor) executorToCheck.get()).getMethod(); + String descriptor = CodeFlow.toDescriptor(method.getReturnType()); + if (this.nullSafe && CodeFlow.isPrimitive(descriptor)) { + this.originalPrimitiveExitTypeDescriptor = descriptor; + this.exitTypeDescriptor = CodeFlow.toBoxedDescriptor(descriptor); + } + else { + this.exitTypeDescriptor = descriptor; + } + } + } + + @Override + public String toStringAST() { + StringJoiner sj = new StringJoiner(",", "(", ")"); + for (int i = 0; i < getChildCount(); i++) { + sj.add(getChild(i).toStringAST()); + } + return this.name + sj.toString(); + } + + /** + * A method reference is compilable if it has been resolved to a reflectively accessible method + * and the child nodes (arguments to the method) are also compilable. + */ + @Override + public boolean isCompilable() { + CachedMethodExecutor executorToCheck = this.cachedExecutor; + if (executorToCheck == null || executorToCheck.hasProxyTarget() || + !(executorToCheck.get() instanceof ReflectiveMethodExecutor)) { + return false; + } + + for (SpelNodeImpl child : this.children) { + if (!child.isCompilable()) { + return false; + } + } + + ReflectiveMethodExecutor executor = (ReflectiveMethodExecutor) executorToCheck.get(); + if (executor.didArgumentConversionOccur()) { + return false; + } + Class clazz = executor.getMethod().getDeclaringClass(); + if (!Modifier.isPublic(clazz.getModifiers()) && executor.getPublicDeclaringClass() == null) { + return false; + } + + return true; + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + CachedMethodExecutor executorToCheck = this.cachedExecutor; + if (executorToCheck == null || !(executorToCheck.get() instanceof ReflectiveMethodExecutor)) { + throw new IllegalStateException("No applicable cached executor found: " + executorToCheck); + } + + ReflectiveMethodExecutor methodExecutor = (ReflectiveMethodExecutor) executorToCheck.get(); + Method method = methodExecutor.getMethod(); + boolean isStaticMethod = Modifier.isStatic(method.getModifiers()); + String descriptor = cf.lastDescriptor(); + + Label skipIfNull = null; + if (descriptor == null && !isStaticMethod) { + // Nothing on the stack but something is needed + cf.loadTarget(mv); + } + if ((descriptor != null || !isStaticMethod) && this.nullSafe) { + mv.visitInsn(DUP); + skipIfNull = new Label(); + Label continueLabel = new Label(); + mv.visitJumpInsn(IFNONNULL, continueLabel); + CodeFlow.insertCheckCast(mv, this.exitTypeDescriptor); + mv.visitJumpInsn(GOTO, skipIfNull); + mv.visitLabel(continueLabel); + } + if (descriptor != null && isStaticMethod) { + // Something on the stack when nothing is needed + mv.visitInsn(POP); + } + + if (CodeFlow.isPrimitive(descriptor)) { + CodeFlow.insertBoxIfNecessary(mv, descriptor.charAt(0)); + } + + String classDesc; + if (Modifier.isPublic(method.getDeclaringClass().getModifiers())) { + classDesc = method.getDeclaringClass().getName().replace('.', '/'); + } + else { + Class publicDeclaringClass = methodExecutor.getPublicDeclaringClass(); + Assert.state(publicDeclaringClass != null, "No public declaring class"); + classDesc = publicDeclaringClass.getName().replace('.', '/'); + } + + if (!isStaticMethod && (descriptor == null || !descriptor.substring(1).equals(classDesc))) { + CodeFlow.insertCheckCast(mv, "L" + classDesc); + } + + generateCodeForArguments(mv, cf, method, this.children); + mv.visitMethodInsn((isStaticMethod ? INVOKESTATIC : (method.isDefault() ? INVOKEINTERFACE : INVOKEVIRTUAL)), + classDesc, method.getName(), CodeFlow.createSignatureDescriptor(method), + method.getDeclaringClass().isInterface()); + cf.pushDescriptor(this.exitTypeDescriptor); + + if (this.originalPrimitiveExitTypeDescriptor != null) { + // The output of the accessor will be a primitive but from the block above it might be null, + // so to have a 'common stack' element at skipIfNull target we need to box the primitive + CodeFlow.insertBoxIfNecessary(mv, this.originalPrimitiveExitTypeDescriptor); + } + if (skipIfNull != null) { + mv.visitLabel(skipIfNull); + } + } + + + private class MethodValueRef implements ValueRef { + + private final EvaluationContext evaluationContext; + + @Nullable + private final Object value; + + @Nullable + private final TypeDescriptor targetType; + + private final Object[] arguments; + + public MethodValueRef(ExpressionState state, Object[] arguments) { + this.evaluationContext = state.getEvaluationContext(); + this.value = state.getActiveContextObject().getValue(); + this.targetType = state.getActiveContextObject().getTypeDescriptor(); + this.arguments = arguments; + } + + @Override + public TypedValue getValue() { + TypedValue result = MethodReference.this.getValueInternal( + this.evaluationContext, this.value, this.targetType, this.arguments); + updateExitTypeDescriptor(); + return result; + } + + @Override + public void setValue(@Nullable Object newValue) { + throw new IllegalAccessError(); + } + + @Override + public boolean isWritable() { + return false; + } + } + + + private static class CachedMethodExecutor { + + private final MethodExecutor methodExecutor; + + @Nullable + private final Class staticClass; + + @Nullable + private final TypeDescriptor target; + + private final List argumentTypes; + + public CachedMethodExecutor(MethodExecutor methodExecutor, @Nullable Class staticClass, + @Nullable TypeDescriptor target, List argumentTypes) { + + this.methodExecutor = methodExecutor; + this.staticClass = staticClass; + this.target = target; + this.argumentTypes = argumentTypes; + } + + public boolean isSuitable(Object value, @Nullable TypeDescriptor target, List argumentTypes) { + return ((this.staticClass == null || this.staticClass == value) && + ObjectUtils.nullSafeEquals(this.target, target) && this.argumentTypes.equals(argumentTypes)); + } + + public boolean hasProxyTarget() { + return (this.target != null && Proxy.isProxyClass(this.target.getType())); + } + + public MethodExecutor get() { + return this.methodExecutor; + } + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/NullLiteral.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/NullLiteral.java new file mode 100644 index 0000000..2001931 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/NullLiteral.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.CodeFlow; + +/** + * Expression language AST node that represents null. + * + * @author Andy Clement + * @since 3.0 + */ +public class NullLiteral extends Literal { + + public NullLiteral(int startPos, int endPos) { + super(null, startPos, endPos); + this.exitTypeDescriptor = "Ljava/lang/Object"; + } + + + @Override + public TypedValue getLiteralValue() { + return TypedValue.NULL; + } + + @Override + public String toString() { + return "null"; + } + + @Override + public boolean isCompilable() { + return true; + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + mv.visitInsn(ACONST_NULL); + cf.pushDescriptor(this.exitTypeDescriptor); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpAnd.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpAnd.java new file mode 100644 index 0000000..baccf24 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpAnd.java @@ -0,0 +1,103 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import org.springframework.asm.Label; +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.support.BooleanTypedValue; +import org.springframework.lang.Nullable; + +/** + * Represents the boolean AND operation. + * + * @author Andy Clement + * @author Mark Fisher + * @author Oliver Becker + * @since 3.0 + */ +public class OpAnd extends Operator { + + public OpAnd(int startPos, int endPos, SpelNodeImpl... operands) { + super("and", startPos, endPos, operands); + this.exitTypeDescriptor = "Z"; + } + + + @Override + public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + if (!getBooleanValue(state, getLeftOperand())) { + // no need to evaluate right operand + return BooleanTypedValue.FALSE; + } + return BooleanTypedValue.forValue(getBooleanValue(state, getRightOperand())); + } + + private boolean getBooleanValue(ExpressionState state, SpelNodeImpl operand) { + try { + Boolean value = operand.getValue(state, Boolean.class); + assertValueNotNull(value); + return value; + } + catch (SpelEvaluationException ex) { + ex.setPosition(operand.getStartPosition()); + throw ex; + } + } + + private void assertValueNotNull(@Nullable Boolean value) { + if (value == null) { + throw new SpelEvaluationException(SpelMessage.TYPE_CONVERSION_ERROR, "null", "boolean"); + } + } + + @Override + public boolean isCompilable() { + SpelNodeImpl left = getLeftOperand(); + SpelNodeImpl right = getRightOperand(); + return (left.isCompilable() && right.isCompilable() && + CodeFlow.isBooleanCompatible(left.exitTypeDescriptor) && + CodeFlow.isBooleanCompatible(right.exitTypeDescriptor)); + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + // Pseudo: if (!leftOperandValue) { result=false; } else { result=rightOperandValue; } + Label elseTarget = new Label(); + Label endOfIf = new Label(); + cf.enterCompilationScope(); + getLeftOperand().generateCode(mv, cf); + cf.unboxBooleanIfNecessary(mv); + cf.exitCompilationScope(); + mv.visitJumpInsn(IFNE, elseTarget); + mv.visitLdcInsn(0); // FALSE + mv.visitJumpInsn(GOTO,endOfIf); + mv.visitLabel(elseTarget); + cf.enterCompilationScope(); + getRightOperand().generateCode(mv, cf); + cf.unboxBooleanIfNecessary(mv); + cf.exitCompilationScope(); + mv.visitLabel(endOfIf); + cf.pushDescriptor(this.exitTypeDescriptor); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpDec.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpDec.java new file mode 100644 index 0000000..9bab5a1 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpDec.java @@ -0,0 +1,144 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Operation; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.util.Assert; + +/** + * Decrement operator. Can be used in a prefix or postfix form. This will throw + * appropriate exceptions if the operand in question does not support decrement. + * + * @author Andy Clement + * @author Juergen Hoeller + * @author Giovanni Dall'Oglio Risso + * @since 3.2 + */ +public class OpDec extends Operator { + + private final boolean postfix; // false means prefix + + + public OpDec(int startPos, int endPos, boolean postfix, SpelNodeImpl... operands) { + super("--", startPos, endPos, operands); + this.postfix = postfix; + Assert.notEmpty(operands, "Operands must not be empty"); + } + + + @Override + public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + SpelNodeImpl operand = getLeftOperand(); + + // The operand is going to be read and then assigned to, we don't want to evaluate it twice. + ValueRef lvalue = operand.getValueRef(state); + + TypedValue operandTypedValue = lvalue.getValue(); //operand.getValueInternal(state); + Object operandValue = operandTypedValue.getValue(); + TypedValue returnValue = operandTypedValue; + TypedValue newValue = null; + + if (operandValue instanceof Number) { + Number op1 = (Number) operandValue; + if (op1 instanceof BigDecimal) { + newValue = new TypedValue(((BigDecimal) op1).subtract(BigDecimal.ONE), operandTypedValue.getTypeDescriptor()); + } + else if (op1 instanceof Double) { + newValue = new TypedValue(op1.doubleValue() - 1.0d, operandTypedValue.getTypeDescriptor()); + } + else if (op1 instanceof Float) { + newValue = new TypedValue(op1.floatValue() - 1.0f, operandTypedValue.getTypeDescriptor()); + } + else if (op1 instanceof BigInteger) { + newValue = new TypedValue(((BigInteger) op1).subtract(BigInteger.ONE), operandTypedValue.getTypeDescriptor()); + } + else if (op1 instanceof Long) { + newValue = new TypedValue(op1.longValue() - 1L, operandTypedValue.getTypeDescriptor()); + } + else if (op1 instanceof Integer) { + newValue = new TypedValue(op1.intValue() - 1, operandTypedValue.getTypeDescriptor()); + } + else if (op1 instanceof Short) { + newValue = new TypedValue(op1.shortValue() - (short) 1, operandTypedValue.getTypeDescriptor()); + } + else if (op1 instanceof Byte) { + newValue = new TypedValue(op1.byteValue() - (byte) 1, operandTypedValue.getTypeDescriptor()); + } + else { + // Unknown Number subtype -> best guess is double decrement + newValue = new TypedValue(op1.doubleValue() - 1.0d, operandTypedValue.getTypeDescriptor()); + } + } + + if (newValue == null) { + try { + newValue = state.operate(Operation.SUBTRACT, returnValue.getValue(), 1); + } + catch (SpelEvaluationException ex) { + if (ex.getMessageCode() == SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES) { + // This means the operand is not decrementable + throw new SpelEvaluationException(operand.getStartPosition(), + SpelMessage.OPERAND_NOT_DECREMENTABLE, operand.toStringAST()); + } + else { + throw ex; + } + } + } + + // set the new value + try { + lvalue.setValue(newValue.getValue()); + } + catch (SpelEvaluationException see) { + // if unable to set the value the operand is not writable (e.g. 1-- ) + if (see.getMessageCode() == SpelMessage.SETVALUE_NOT_SUPPORTED) { + throw new SpelEvaluationException(operand.getStartPosition(), + SpelMessage.OPERAND_NOT_DECREMENTABLE); + } + else { + throw see; + } + } + + if (!this.postfix) { + // the return value is the new value, not the original value + returnValue = newValue; + } + + return returnValue; + } + + @Override + public String toStringAST() { + return getLeftOperand().toStringAST() + "--"; + } + + @Override + public SpelNodeImpl getRightOperand() { + throw new IllegalStateException("No right operand"); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpDivide.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpDivide.java new file mode 100644 index 0000000..02eae2f --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpDivide.java @@ -0,0 +1,140 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; + +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Operation; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.util.Assert; +import org.springframework.util.NumberUtils; + +/** + * Implements division operator. + * + * @author Andy Clement + * @author Juergen Hoeller + * @author Giovanni Dall'Oglio Risso + * @since 3.0 + */ +public class OpDivide extends Operator { + + public OpDivide(int startPos, int endPos, SpelNodeImpl... operands) { + super("/", startPos, endPos, operands); + } + + + @Override + public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + Object leftOperand = getLeftOperand().getValueInternal(state).getValue(); + Object rightOperand = getRightOperand().getValueInternal(state).getValue(); + + if (leftOperand instanceof Number && rightOperand instanceof Number) { + Number leftNumber = (Number) leftOperand; + Number rightNumber = (Number) rightOperand; + + if (leftNumber instanceof BigDecimal || rightNumber instanceof BigDecimal) { + BigDecimal leftBigDecimal = NumberUtils.convertNumberToTargetClass(leftNumber, BigDecimal.class); + BigDecimal rightBigDecimal = NumberUtils.convertNumberToTargetClass(rightNumber, BigDecimal.class); + int scale = Math.max(leftBigDecimal.scale(), rightBigDecimal.scale()); + return new TypedValue(leftBigDecimal.divide(rightBigDecimal, scale, RoundingMode.HALF_EVEN)); + } + else if (leftNumber instanceof Double || rightNumber instanceof Double) { + this.exitTypeDescriptor = "D"; + return new TypedValue(leftNumber.doubleValue() / rightNumber.doubleValue()); + } + else if (leftNumber instanceof Float || rightNumber instanceof Float) { + this.exitTypeDescriptor = "F"; + return new TypedValue(leftNumber.floatValue() / rightNumber.floatValue()); + } + else if (leftNumber instanceof BigInteger || rightNumber instanceof BigInteger) { + BigInteger leftBigInteger = NumberUtils.convertNumberToTargetClass(leftNumber, BigInteger.class); + BigInteger rightBigInteger = NumberUtils.convertNumberToTargetClass(rightNumber, BigInteger.class); + return new TypedValue(leftBigInteger.divide(rightBigInteger)); + } + else if (leftNumber instanceof Long || rightNumber instanceof Long) { + this.exitTypeDescriptor = "J"; + return new TypedValue(leftNumber.longValue() / rightNumber.longValue()); + } + else if (CodeFlow.isIntegerForNumericOp(leftNumber) || CodeFlow.isIntegerForNumericOp(rightNumber)) { + this.exitTypeDescriptor = "I"; + return new TypedValue(leftNumber.intValue() / rightNumber.intValue()); + } + else { + // Unknown Number subtypes -> best guess is double division + return new TypedValue(leftNumber.doubleValue() / rightNumber.doubleValue()); + } + } + + return state.operate(Operation.DIVIDE, leftOperand, rightOperand); + } + + @Override + public boolean isCompilable() { + if (!getLeftOperand().isCompilable()) { + return false; + } + if (this.children.length > 1) { + if (!getRightOperand().isCompilable()) { + return false; + } + } + return (this.exitTypeDescriptor != null); + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + getLeftOperand().generateCode(mv, cf); + String leftDesc = getLeftOperand().exitTypeDescriptor; + String exitDesc = this.exitTypeDescriptor; + Assert.state(exitDesc != null, "No exit type descriptor"); + char targetDesc = exitDesc.charAt(0); + CodeFlow.insertNumericUnboxOrPrimitiveTypeCoercion(mv, leftDesc, targetDesc); + if (this.children.length > 1) { + cf.enterCompilationScope(); + getRightOperand().generateCode(mv, cf); + String rightDesc = getRightOperand().exitTypeDescriptor; + cf.exitCompilationScope(); + CodeFlow.insertNumericUnboxOrPrimitiveTypeCoercion(mv, rightDesc, targetDesc); + switch (targetDesc) { + case 'I': + mv.visitInsn(IDIV); + break; + case 'J': + mv.visitInsn(LDIV); + break; + case 'F': + mv.visitInsn(FDIV); + break; + case 'D': + mv.visitInsn(DDIV); + break; + default: + throw new IllegalStateException( + "Unrecognized exit type descriptor: '" + this.exitTypeDescriptor + "'"); + } + } + cf.pushDescriptor(this.exitTypeDescriptor); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpEQ.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpEQ.java new file mode 100644 index 0000000..7916093 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpEQ.java @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.support.BooleanTypedValue; + +/** + * Implements the equality operator. + * + * @author Andy Clement + * @since 3.0 + */ +public class OpEQ extends Operator { + + public OpEQ(int startPos, int endPos, SpelNodeImpl... operands) { + super("==", startPos, endPos, operands); + this.exitTypeDescriptor = "Z"; + } + + + @Override + public BooleanTypedValue getValueInternal(ExpressionState state) throws EvaluationException { + Object left = getLeftOperand().getValueInternal(state).getValue(); + Object right = getRightOperand().getValueInternal(state).getValue(); + this.leftActualDescriptor = CodeFlow.toDescriptorFromObject(left); + this.rightActualDescriptor = CodeFlow.toDescriptorFromObject(right); + return BooleanTypedValue.forValue(equalityCheck(state.getEvaluationContext(), left, right)); + } + + // This check is different to the one in the other numeric operators (OpLt/etc) + // because it allows for simple object comparison + @Override + public boolean isCompilable() { + SpelNodeImpl left = getLeftOperand(); + SpelNodeImpl right = getRightOperand(); + if (!left.isCompilable() || !right.isCompilable()) { + return false; + } + + String leftDesc = left.exitTypeDescriptor; + String rightDesc = right.exitTypeDescriptor; + DescriptorComparison dc = DescriptorComparison.checkNumericCompatibility(leftDesc, + rightDesc, this.leftActualDescriptor, this.rightActualDescriptor); + return (!dc.areNumbers || dc.areCompatible); + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + cf.loadEvaluationContext(mv); + String leftDesc = getLeftOperand().exitTypeDescriptor; + String rightDesc = getRightOperand().exitTypeDescriptor; + boolean leftPrim = CodeFlow.isPrimitive(leftDesc); + boolean rightPrim = CodeFlow.isPrimitive(rightDesc); + + cf.enterCompilationScope(); + getLeftOperand().generateCode(mv, cf); + cf.exitCompilationScope(); + if (leftPrim) { + CodeFlow.insertBoxIfNecessary(mv, leftDesc.charAt(0)); + } + cf.enterCompilationScope(); + getRightOperand().generateCode(mv, cf); + cf.exitCompilationScope(); + if (rightPrim) { + CodeFlow.insertBoxIfNecessary(mv, rightDesc.charAt(0)); + } + + String operatorClassName = Operator.class.getName().replace('.', '/'); + String evaluationContextClassName = EvaluationContext.class.getName().replace('.', '/'); + mv.visitMethodInsn(INVOKESTATIC, operatorClassName, "equalityCheck", + "(L" + evaluationContextClassName + ";Ljava/lang/Object;Ljava/lang/Object;)Z", false); + cf.pushDescriptor("Z"); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpGE.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpGE.java new file mode 100644 index 0000000..708e1b8 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpGE.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.support.BooleanTypedValue; +import org.springframework.util.NumberUtils; + +/** + * Implements greater-than-or-equal operator. + * + * @author Andy Clement + * @author Juergen Hoeller + * @author Giovanni Dall'Oglio Risso + * @since 3.0 + */ +public class OpGE extends Operator { + + public OpGE(int startPos, int endPos, SpelNodeImpl... operands) { + super(">=", startPos, endPos, operands); + this.exitTypeDescriptor = "Z"; + } + + + @Override + public BooleanTypedValue getValueInternal(ExpressionState state) throws EvaluationException { + Object left = getLeftOperand().getValueInternal(state).getValue(); + Object right = getRightOperand().getValueInternal(state).getValue(); + + this.leftActualDescriptor = CodeFlow.toDescriptorFromObject(left); + this.rightActualDescriptor = CodeFlow.toDescriptorFromObject(right); + + if (left instanceof Number && right instanceof Number) { + Number leftNumber = (Number) left; + Number rightNumber = (Number) right; + + if (leftNumber instanceof BigDecimal || rightNumber instanceof BigDecimal) { + BigDecimal leftBigDecimal = NumberUtils.convertNumberToTargetClass(leftNumber, BigDecimal.class); + BigDecimal rightBigDecimal = NumberUtils.convertNumberToTargetClass(rightNumber, BigDecimal.class); + return BooleanTypedValue.forValue(leftBigDecimal.compareTo(rightBigDecimal) >= 0); + } + else if (leftNumber instanceof Double || rightNumber instanceof Double) { + return BooleanTypedValue.forValue(leftNumber.doubleValue() >= rightNumber.doubleValue()); + } + else if (leftNumber instanceof Float || rightNumber instanceof Float) { + return BooleanTypedValue.forValue(leftNumber.floatValue() >= rightNumber.floatValue()); + } + else if (leftNumber instanceof BigInteger || rightNumber instanceof BigInteger) { + BigInteger leftBigInteger = NumberUtils.convertNumberToTargetClass(leftNumber, BigInteger.class); + BigInteger rightBigInteger = NumberUtils.convertNumberToTargetClass(rightNumber, BigInteger.class); + return BooleanTypedValue.forValue(leftBigInteger.compareTo(rightBigInteger) >= 0); + } + else if (leftNumber instanceof Long || rightNumber instanceof Long) { + return BooleanTypedValue.forValue(leftNumber.longValue() >= rightNumber.longValue()); + } + else if (leftNumber instanceof Integer || rightNumber instanceof Integer) { + return BooleanTypedValue.forValue(leftNumber.intValue() >= rightNumber.intValue()); + } + else if (leftNumber instanceof Short || rightNumber instanceof Short) { + return BooleanTypedValue.forValue(leftNumber.shortValue() >= rightNumber.shortValue()); + } + else if (leftNumber instanceof Byte || rightNumber instanceof Byte) { + return BooleanTypedValue.forValue(leftNumber.byteValue() >= rightNumber.byteValue()); + } + else { + // Unknown Number subtypes -> best guess is double comparison + return BooleanTypedValue.forValue(leftNumber.doubleValue() >= rightNumber.doubleValue()); + } + } + + return BooleanTypedValue.forValue(state.getTypeComparator().compare(left, right) >= 0); + } + + @Override + public boolean isCompilable() { + return isCompilableOperatorUsingNumerics(); + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + generateComparisonCode(mv, cf, IFLT, IF_ICMPLT); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpGT.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpGT.java new file mode 100644 index 0000000..3a4b2fd --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpGT.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.support.BooleanTypedValue; +import org.springframework.util.NumberUtils; + +/** + * Implements the greater-than operator. + * + * @author Andy Clement + * @author Juergen Hoeller + * @author Giovanni Dall'Oglio Risso + * @since 3.0 + */ +public class OpGT extends Operator { + + public OpGT(int startPos, int endPos, SpelNodeImpl... operands) { + super(">", startPos, endPos, operands); + this.exitTypeDescriptor = "Z"; + } + + + @Override + public BooleanTypedValue getValueInternal(ExpressionState state) throws EvaluationException { + Object left = getLeftOperand().getValueInternal(state).getValue(); + Object right = getRightOperand().getValueInternal(state).getValue(); + + this.leftActualDescriptor = CodeFlow.toDescriptorFromObject(left); + this.rightActualDescriptor = CodeFlow.toDescriptorFromObject(right); + + if (left instanceof Number && right instanceof Number) { + Number leftNumber = (Number) left; + Number rightNumber = (Number) right; + + if (leftNumber instanceof BigDecimal || rightNumber instanceof BigDecimal) { + BigDecimal leftBigDecimal = NumberUtils.convertNumberToTargetClass(leftNumber, BigDecimal.class); + BigDecimal rightBigDecimal = NumberUtils.convertNumberToTargetClass(rightNumber, BigDecimal.class); + return BooleanTypedValue.forValue(leftBigDecimal.compareTo(rightBigDecimal) > 0); + } + else if (leftNumber instanceof Double || rightNumber instanceof Double) { + return BooleanTypedValue.forValue(leftNumber.doubleValue() > rightNumber.doubleValue()); + } + else if (leftNumber instanceof Float || rightNumber instanceof Float) { + return BooleanTypedValue.forValue(leftNumber.floatValue() > rightNumber.floatValue()); + } + else if (leftNumber instanceof BigInteger || rightNumber instanceof BigInteger) { + BigInteger leftBigInteger = NumberUtils.convertNumberToTargetClass(leftNumber, BigInteger.class); + BigInteger rightBigInteger = NumberUtils.convertNumberToTargetClass(rightNumber, BigInteger.class); + return BooleanTypedValue.forValue(leftBigInteger.compareTo(rightBigInteger) > 0); + } + else if (leftNumber instanceof Long || rightNumber instanceof Long) { + return BooleanTypedValue.forValue(leftNumber.longValue() > rightNumber.longValue()); + } + else if (leftNumber instanceof Integer || rightNumber instanceof Integer) { + return BooleanTypedValue.forValue(leftNumber.intValue() > rightNumber.intValue()); + } + else if (leftNumber instanceof Short || rightNumber instanceof Short) { + return BooleanTypedValue.forValue(leftNumber.shortValue() > rightNumber.shortValue()); + } + else if (leftNumber instanceof Byte || rightNumber instanceof Byte) { + return BooleanTypedValue.forValue(leftNumber.byteValue() > rightNumber.byteValue()); + } + else { + // Unknown Number subtypes -> best guess is double comparison + return BooleanTypedValue.forValue(leftNumber.doubleValue() > rightNumber.doubleValue()); + } + } + + if (left instanceof CharSequence && right instanceof CharSequence) { + left = left.toString(); + right = right.toString(); + } + + return BooleanTypedValue.forValue(state.getTypeComparator().compare(left, right) > 0); + } + + @Override + public boolean isCompilable() { + return isCompilableOperatorUsingNumerics(); + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + generateComparisonCode(mv, cf, IFLE, IF_ICMPLE); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpInc.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpInc.java new file mode 100644 index 0000000..ee19b59 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpInc.java @@ -0,0 +1,139 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Operation; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.util.Assert; + +/** + * Increment operator. Can be used in a prefix or postfix form. This will throw + * appropriate exceptions if the operand in question does not support increment. + * + * @author Andy Clement + * @author Juergen Hoeller + * @author Giovanni Dall'Oglio Risso + * @since 3.2 + */ +public class OpInc extends Operator { + + private final boolean postfix; // false means prefix + + + public OpInc(int startPos, int endPos, boolean postfix, SpelNodeImpl... operands) { + super("++", startPos, endPos, operands); + this.postfix = postfix; + Assert.notEmpty(operands, "Operands must not be empty"); + } + + + @Override + public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + SpelNodeImpl operand = getLeftOperand(); + ValueRef valueRef = operand.getValueRef(state); + + TypedValue typedValue = valueRef.getValue(); + Object value = typedValue.getValue(); + TypedValue returnValue = typedValue; + TypedValue newValue = null; + + if (value instanceof Number) { + Number op1 = (Number) value; + if (op1 instanceof BigDecimal) { + newValue = new TypedValue(((BigDecimal) op1).add(BigDecimal.ONE), typedValue.getTypeDescriptor()); + } + else if (op1 instanceof Double) { + newValue = new TypedValue(op1.doubleValue() + 1.0d, typedValue.getTypeDescriptor()); + } + else if (op1 instanceof Float) { + newValue = new TypedValue(op1.floatValue() + 1.0f, typedValue.getTypeDescriptor()); + } + else if (op1 instanceof BigInteger) { + newValue = new TypedValue(((BigInteger) op1).add(BigInteger.ONE), typedValue.getTypeDescriptor()); + } + else if (op1 instanceof Long) { + newValue = new TypedValue(op1.longValue() + 1L, typedValue.getTypeDescriptor()); + } + else if (op1 instanceof Integer) { + newValue = new TypedValue(op1.intValue() + 1, typedValue.getTypeDescriptor()); + } + else if (op1 instanceof Short) { + newValue = new TypedValue(op1.shortValue() + (short) 1, typedValue.getTypeDescriptor()); + } + else if (op1 instanceof Byte) { + newValue = new TypedValue(op1.byteValue() + (byte) 1, typedValue.getTypeDescriptor()); + } + else { + // Unknown Number subtype -> best guess is double increment + newValue = new TypedValue(op1.doubleValue() + 1.0d, typedValue.getTypeDescriptor()); + } + } + + if (newValue == null) { + try { + newValue = state.operate(Operation.ADD, returnValue.getValue(), 1); + } + catch (SpelEvaluationException ex) { + if (ex.getMessageCode() == SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES) { + // This means the operand is not incrementable + throw new SpelEvaluationException(operand.getStartPosition(), + SpelMessage.OPERAND_NOT_INCREMENTABLE, operand.toStringAST()); + } + throw ex; + } + } + + // set the name value + try { + valueRef.setValue(newValue.getValue()); + } + catch (SpelEvaluationException see) { + // If unable to set the value the operand is not writable (e.g. 1++ ) + if (see.getMessageCode() == SpelMessage.SETVALUE_NOT_SUPPORTED) { + throw new SpelEvaluationException(operand.getStartPosition(), SpelMessage.OPERAND_NOT_INCREMENTABLE); + } + else { + throw see; + } + } + + if (!this.postfix) { + // The return value is the new value, not the original value + returnValue = newValue; + } + + return returnValue; + } + + @Override + public String toStringAST() { + return getLeftOperand().toStringAST() + "++"; + } + + @Override + public SpelNodeImpl getRightOperand() { + throw new IllegalStateException("No right operand"); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpLE.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpLE.java new file mode 100644 index 0000000..adbc299 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpLE.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.support.BooleanTypedValue; +import org.springframework.util.NumberUtils; + +/** + * Implements the less-than-or-equal operator. + * + * @author Andy Clement + * @author Juergen Hoeller + * @author Giovanni Dall'Oglio Risso + * @since 3.0 + */ +public class OpLE extends Operator { + + public OpLE(int startPos, int endPos, SpelNodeImpl... operands) { + super("<=", startPos, endPos, operands); + this.exitTypeDescriptor = "Z"; + } + + + @Override + public BooleanTypedValue getValueInternal(ExpressionState state) throws EvaluationException { + Object left = getLeftOperand().getValueInternal(state).getValue(); + Object right = getRightOperand().getValueInternal(state).getValue(); + + this.leftActualDescriptor = CodeFlow.toDescriptorFromObject(left); + this.rightActualDescriptor = CodeFlow.toDescriptorFromObject(right); + + if (left instanceof Number && right instanceof Number) { + Number leftNumber = (Number) left; + Number rightNumber = (Number) right; + + if (leftNumber instanceof BigDecimal || rightNumber instanceof BigDecimal) { + BigDecimal leftBigDecimal = NumberUtils.convertNumberToTargetClass(leftNumber, BigDecimal.class); + BigDecimal rightBigDecimal = NumberUtils.convertNumberToTargetClass(rightNumber, BigDecimal.class); + return BooleanTypedValue.forValue(leftBigDecimal.compareTo(rightBigDecimal) <= 0); + } + else if (leftNumber instanceof Double || rightNumber instanceof Double) { + return BooleanTypedValue.forValue(leftNumber.doubleValue() <= rightNumber.doubleValue()); + } + else if (leftNumber instanceof Float || rightNumber instanceof Float) { + return BooleanTypedValue.forValue(leftNumber.floatValue() <= rightNumber.floatValue()); + } + else if (leftNumber instanceof BigInteger || rightNumber instanceof BigInteger) { + BigInteger leftBigInteger = NumberUtils.convertNumberToTargetClass(leftNumber, BigInteger.class); + BigInteger rightBigInteger = NumberUtils.convertNumberToTargetClass(rightNumber, BigInteger.class); + return BooleanTypedValue.forValue(leftBigInteger.compareTo(rightBigInteger) <= 0); + } + else if (leftNumber instanceof Long || rightNumber instanceof Long) { + return BooleanTypedValue.forValue(leftNumber.longValue() <= rightNumber.longValue()); + } + else if (leftNumber instanceof Integer || rightNumber instanceof Integer) { + return BooleanTypedValue.forValue(leftNumber.intValue() <= rightNumber.intValue()); + } + else if (leftNumber instanceof Short || rightNumber instanceof Short) { + return BooleanTypedValue.forValue(leftNumber.shortValue() <= rightNumber.shortValue()); + } + else if (leftNumber instanceof Byte || rightNumber instanceof Byte) { + return BooleanTypedValue.forValue(leftNumber.byteValue() <= rightNumber.byteValue()); + } + else { + // Unknown Number subtypes -> best guess is double comparison + return BooleanTypedValue.forValue(leftNumber.doubleValue() <= rightNumber.doubleValue()); + } + } + + return BooleanTypedValue.forValue(state.getTypeComparator().compare(left, right) <= 0); + } + + @Override + public boolean isCompilable() { + return isCompilableOperatorUsingNumerics(); + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + generateComparisonCode(mv, cf, IFGT, IF_ICMPGT); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpLT.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpLT.java new file mode 100644 index 0000000..8101dee --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpLT.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.support.BooleanTypedValue; +import org.springframework.util.NumberUtils; + +/** + * Implements the less-than operator. + * + * @author Andy Clement + * @author Juergen Hoeller + * @author Giovanni Dall'Oglio Risso + * @since 3.0 + */ +public class OpLT extends Operator { + + public OpLT(int startPos, int endPos, SpelNodeImpl... operands) { + super("<", startPos, endPos, operands); + this.exitTypeDescriptor = "Z"; + } + + + @Override + public BooleanTypedValue getValueInternal(ExpressionState state) throws EvaluationException { + Object left = getLeftOperand().getValueInternal(state).getValue(); + Object right = getRightOperand().getValueInternal(state).getValue(); + + this.leftActualDescriptor = CodeFlow.toDescriptorFromObject(left); + this.rightActualDescriptor = CodeFlow.toDescriptorFromObject(right); + + if (left instanceof Number && right instanceof Number) { + Number leftNumber = (Number) left; + Number rightNumber = (Number) right; + + if (leftNumber instanceof BigDecimal || rightNumber instanceof BigDecimal) { + BigDecimal leftBigDecimal = NumberUtils.convertNumberToTargetClass(leftNumber, BigDecimal.class); + BigDecimal rightBigDecimal = NumberUtils.convertNumberToTargetClass(rightNumber, BigDecimal.class); + return BooleanTypedValue.forValue(leftBigDecimal.compareTo(rightBigDecimal) < 0); + } + else if (leftNumber instanceof Double || rightNumber instanceof Double) { + return BooleanTypedValue.forValue(leftNumber.doubleValue() < rightNumber.doubleValue()); + } + else if (leftNumber instanceof Float || rightNumber instanceof Float) { + return BooleanTypedValue.forValue(leftNumber.floatValue() < rightNumber.floatValue()); + } + else if (leftNumber instanceof BigInteger || rightNumber instanceof BigInteger) { + BigInteger leftBigInteger = NumberUtils.convertNumberToTargetClass(leftNumber, BigInteger.class); + BigInteger rightBigInteger = NumberUtils.convertNumberToTargetClass(rightNumber, BigInteger.class); + return BooleanTypedValue.forValue(leftBigInteger.compareTo(rightBigInteger) < 0); + } + else if (leftNumber instanceof Long || rightNumber instanceof Long) { + return BooleanTypedValue.forValue(leftNumber.longValue() < rightNumber.longValue()); + } + else if (leftNumber instanceof Integer || rightNumber instanceof Integer) { + return BooleanTypedValue.forValue(leftNumber.intValue() < rightNumber.intValue()); + } + else if (leftNumber instanceof Short || rightNumber instanceof Short) { + return BooleanTypedValue.forValue(leftNumber.shortValue() < rightNumber.shortValue()); + } + else if (leftNumber instanceof Byte || rightNumber instanceof Byte) { + return BooleanTypedValue.forValue(leftNumber.byteValue() < rightNumber.byteValue()); + } + else { + // Unknown Number subtypes -> best guess is double comparison + return BooleanTypedValue.forValue(leftNumber.doubleValue() < rightNumber.doubleValue()); + } + } + + if (left instanceof CharSequence && right instanceof CharSequence) { + left = left.toString(); + right = right.toString(); + } + + return BooleanTypedValue.forValue(state.getTypeComparator().compare(left, right) < 0); + } + + @Override + public boolean isCompilable() { + return isCompilableOperatorUsingNumerics(); + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + generateComparisonCode(mv, cf, IFGE, IF_ICMPGE); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpMinus.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpMinus.java new file mode 100644 index 0000000..54d6b68 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpMinus.java @@ -0,0 +1,230 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Operation; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.util.Assert; +import org.springframework.util.NumberUtils; + +/** + * The minus operator supports: + *
      + *
    • subtraction of numbers + *
    • subtraction of an int from a string of one character + * (effectively decreasing that character), so 'd'-3='a' + *
    + * + *

    It can be used as a unary operator for numbers. + * The standard promotions are performed when the operand types vary (double-int=double). + * For other options it defers to the registered overloader. + * + * @author Andy Clement + * @author Juergen Hoeller + * @author Giovanni Dall'Oglio Risso + * @since 3.0 + */ +public class OpMinus extends Operator { + + public OpMinus(int startPos, int endPos, SpelNodeImpl... operands) { + super("-", startPos, endPos, operands); + } + + + @Override + public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + SpelNodeImpl leftOp = getLeftOperand(); + + if (this.children.length < 2) { // if only one operand, then this is unary minus + Object operand = leftOp.getValueInternal(state).getValue(); + if (operand instanceof Number) { + if (operand instanceof BigDecimal) { + return new TypedValue(((BigDecimal) operand).negate()); + } + else if (operand instanceof Double) { + this.exitTypeDescriptor = "D"; + return new TypedValue(0 - ((Number) operand).doubleValue()); + } + else if (operand instanceof Float) { + this.exitTypeDescriptor = "F"; + return new TypedValue(0 - ((Number) operand).floatValue()); + } + else if (operand instanceof BigInteger) { + return new TypedValue(((BigInteger) operand).negate()); + } + else if (operand instanceof Long) { + this.exitTypeDescriptor = "J"; + return new TypedValue(0 - ((Number) operand).longValue()); + } + else if (operand instanceof Integer) { + this.exitTypeDescriptor = "I"; + return new TypedValue(0 - ((Number) operand).intValue()); + } + else if (operand instanceof Short) { + return new TypedValue(0 - ((Number) operand).shortValue()); + } + else if (operand instanceof Byte) { + return new TypedValue(0 - ((Number) operand).byteValue()); + } + else { + // Unknown Number subtypes -> best guess is double subtraction + return new TypedValue(0 - ((Number) operand).doubleValue()); + } + } + return state.operate(Operation.SUBTRACT, operand, null); + } + + Object left = leftOp.getValueInternal(state).getValue(); + Object right = getRightOperand().getValueInternal(state).getValue(); + + if (left instanceof Number && right instanceof Number) { + Number leftNumber = (Number) left; + Number rightNumber = (Number) right; + + if (leftNumber instanceof BigDecimal || rightNumber instanceof BigDecimal) { + BigDecimal leftBigDecimal = NumberUtils.convertNumberToTargetClass(leftNumber, BigDecimal.class); + BigDecimal rightBigDecimal = NumberUtils.convertNumberToTargetClass(rightNumber, BigDecimal.class); + return new TypedValue(leftBigDecimal.subtract(rightBigDecimal)); + } + else if (leftNumber instanceof Double || rightNumber instanceof Double) { + this.exitTypeDescriptor = "D"; + return new TypedValue(leftNumber.doubleValue() - rightNumber.doubleValue()); + } + else if (leftNumber instanceof Float || rightNumber instanceof Float) { + this.exitTypeDescriptor = "F"; + return new TypedValue(leftNumber.floatValue() - rightNumber.floatValue()); + } + else if (leftNumber instanceof BigInteger || rightNumber instanceof BigInteger) { + BigInteger leftBigInteger = NumberUtils.convertNumberToTargetClass(leftNumber, BigInteger.class); + BigInteger rightBigInteger = NumberUtils.convertNumberToTargetClass(rightNumber, BigInteger.class); + return new TypedValue(leftBigInteger.subtract(rightBigInteger)); + } + else if (leftNumber instanceof Long || rightNumber instanceof Long) { + this.exitTypeDescriptor = "J"; + return new TypedValue(leftNumber.longValue() - rightNumber.longValue()); + } + else if (CodeFlow.isIntegerForNumericOp(leftNumber) || CodeFlow.isIntegerForNumericOp(rightNumber)) { + this.exitTypeDescriptor = "I"; + return new TypedValue(leftNumber.intValue() - rightNumber.intValue()); + } + else { + // Unknown Number subtypes -> best guess is double subtraction + return new TypedValue(leftNumber.doubleValue() - rightNumber.doubleValue()); + } + } + + if (left instanceof String && right instanceof Integer && ((String) left).length() == 1) { + String theString = (String) left; + Integer theInteger = (Integer) right; + // Implements character - int (ie. b - 1 = a) + return new TypedValue(Character.toString((char) (theString.charAt(0) - theInteger))); + } + + return state.operate(Operation.SUBTRACT, left, right); + } + + @Override + public String toStringAST() { + if (this.children.length < 2) { // unary minus + return "-" + getLeftOperand().toStringAST(); + } + return super.toStringAST(); + } + + @Override + public SpelNodeImpl getRightOperand() { + if (this.children.length < 2) { + throw new IllegalStateException("No right operand"); + } + return this.children[1]; + } + + @Override + public boolean isCompilable() { + if (!getLeftOperand().isCompilable()) { + return false; + } + if (this.children.length > 1) { + if (!getRightOperand().isCompilable()) { + return false; + } + } + return (this.exitTypeDescriptor != null); + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + getLeftOperand().generateCode(mv, cf); + String leftDesc = getLeftOperand().exitTypeDescriptor; + String exitDesc = this.exitTypeDescriptor; + Assert.state(exitDesc != null, "No exit type descriptor"); + char targetDesc = exitDesc.charAt(0); + CodeFlow.insertNumericUnboxOrPrimitiveTypeCoercion(mv, leftDesc, targetDesc); + if (this.children.length > 1) { + cf.enterCompilationScope(); + getRightOperand().generateCode(mv, cf); + String rightDesc = getRightOperand().exitTypeDescriptor; + cf.exitCompilationScope(); + CodeFlow.insertNumericUnboxOrPrimitiveTypeCoercion(mv, rightDesc, targetDesc); + switch (targetDesc) { + case 'I': + mv.visitInsn(ISUB); + break; + case 'J': + mv.visitInsn(LSUB); + break; + case 'F': + mv.visitInsn(FSUB); + break; + case 'D': + mv.visitInsn(DSUB); + break; + default: + throw new IllegalStateException( + "Unrecognized exit type descriptor: '" + this.exitTypeDescriptor + "'"); + } + } + else { + switch (targetDesc) { + case 'I': + mv.visitInsn(INEG); + break; + case 'J': + mv.visitInsn(LNEG); + break; + case 'F': + mv.visitInsn(FNEG); + break; + case 'D': + mv.visitInsn(DNEG); + break; + default: + throw new IllegalStateException( + "Unrecognized exit type descriptor: '" + this.exitTypeDescriptor + "'"); + } + } + cf.pushDescriptor(this.exitTypeDescriptor); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpModulus.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpModulus.java new file mode 100644 index 0000000..eadd9a9 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpModulus.java @@ -0,0 +1,138 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Operation; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.util.Assert; +import org.springframework.util.NumberUtils; + +/** + * Implements the modulus operator. + * + * @author Andy Clement + * @author Juergen Hoeller + * @author Giovanni Dall'Oglio Risso + * @since 3.0 + */ +public class OpModulus extends Operator { + + public OpModulus(int startPos, int endPos, SpelNodeImpl... operands) { + super("%", startPos, endPos, operands); + } + + + @Override + public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + Object leftOperand = getLeftOperand().getValueInternal(state).getValue(); + Object rightOperand = getRightOperand().getValueInternal(state).getValue(); + + if (leftOperand instanceof Number && rightOperand instanceof Number) { + Number leftNumber = (Number) leftOperand; + Number rightNumber = (Number) rightOperand; + + if (leftNumber instanceof BigDecimal || rightNumber instanceof BigDecimal) { + BigDecimal leftBigDecimal = NumberUtils.convertNumberToTargetClass(leftNumber, BigDecimal.class); + BigDecimal rightBigDecimal = NumberUtils.convertNumberToTargetClass(rightNumber, BigDecimal.class); + return new TypedValue(leftBigDecimal.remainder(rightBigDecimal)); + } + else if (leftNumber instanceof Double || rightNumber instanceof Double) { + this.exitTypeDescriptor = "D"; + return new TypedValue(leftNumber.doubleValue() % rightNumber.doubleValue()); + } + else if (leftNumber instanceof Float || rightNumber instanceof Float) { + this.exitTypeDescriptor = "F"; + return new TypedValue(leftNumber.floatValue() % rightNumber.floatValue()); + } + else if (leftNumber instanceof BigInteger || rightNumber instanceof BigInteger) { + BigInteger leftBigInteger = NumberUtils.convertNumberToTargetClass(leftNumber, BigInteger.class); + BigInteger rightBigInteger = NumberUtils.convertNumberToTargetClass(rightNumber, BigInteger.class); + return new TypedValue(leftBigInteger.remainder(rightBigInteger)); + } + else if (leftNumber instanceof Long || rightNumber instanceof Long) { + this.exitTypeDescriptor = "J"; + return new TypedValue(leftNumber.longValue() % rightNumber.longValue()); + } + else if (CodeFlow.isIntegerForNumericOp(leftNumber) || CodeFlow.isIntegerForNumericOp(rightNumber)) { + this.exitTypeDescriptor = "I"; + return new TypedValue(leftNumber.intValue() % rightNumber.intValue()); + } + else { + // Unknown Number subtypes -> best guess is double division + return new TypedValue(leftNumber.doubleValue() % rightNumber.doubleValue()); + } + } + + return state.operate(Operation.MODULUS, leftOperand, rightOperand); + } + + @Override + public boolean isCompilable() { + if (!getLeftOperand().isCompilable()) { + return false; + } + if (this.children.length > 1) { + if (!getRightOperand().isCompilable()) { + return false; + } + } + return (this.exitTypeDescriptor != null); + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + getLeftOperand().generateCode(mv, cf); + String leftDesc = getLeftOperand().exitTypeDescriptor; + String exitDesc = this.exitTypeDescriptor; + Assert.state(exitDesc != null, "No exit type descriptor"); + char targetDesc = exitDesc.charAt(0); + CodeFlow.insertNumericUnboxOrPrimitiveTypeCoercion(mv, leftDesc, targetDesc); + if (this.children.length > 1) { + cf.enterCompilationScope(); + getRightOperand().generateCode(mv, cf); + String rightDesc = getRightOperand().exitTypeDescriptor; + cf.exitCompilationScope(); + CodeFlow.insertNumericUnboxOrPrimitiveTypeCoercion(mv, rightDesc, targetDesc); + switch (targetDesc) { + case 'I': + mv.visitInsn(IREM); + break; + case 'J': + mv.visitInsn(LREM); + break; + case 'F': + mv.visitInsn(FREM); + break; + case 'D': + mv.visitInsn(DREM); + break; + default: + throw new IllegalStateException( + "Unrecognized exit type descriptor: '" + this.exitTypeDescriptor + "'"); + } + } + cf.pushDescriptor(this.exitTypeDescriptor); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpMultiply.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpMultiply.java new file mode 100644 index 0000000..fbf2831 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpMultiply.java @@ -0,0 +1,171 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Operation; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.util.Assert; +import org.springframework.util.NumberUtils; + +/** + * Implements the {@code multiply} operator. + * + *

    Conversions and promotions are handled as defined in + * Section 5.6.2 of the + * Java Language Specification, with the addiction of {@code BigDecimal}/{@code BigInteger} management: + * + *

    If any of the operands is of a reference type, unboxing conversion (Section 5.1.8) + * is performed. Then:
    + * If either operand is of type {@code BigDecimal}, the other is converted to {@code BigDecimal}.
    + * If either operand is of type double, the other is converted to double.
    + * Otherwise, if either operand is of type float, the other is converted to float.
    + * If either operand is of type {@code BigInteger}, the other is converted to {@code BigInteger}.
    + * Otherwise, if either operand is of type long, the other is converted to long.
    + * Otherwise, both operands are converted to type int. + * + * @author Andy Clement + * @author Juergen Hoeller + * @author Sam Brannen + * @author Giovanni Dall'Oglio Risso + * @since 3.0 + */ +public class OpMultiply extends Operator { + + public OpMultiply(int startPos, int endPos, SpelNodeImpl... operands) { + super("*", startPos, endPos, operands); + } + + + /** + * Implements the {@code multiply} operator directly here for certain types + * of supported operands and otherwise delegates to any registered overloader + * for types not supported here. + *

    Supported operand types: + *

      + *
    • numbers + *
    • String and int ('abc' * 2 == 'abcabc') + *
    + */ + @Override + public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + Object leftOperand = getLeftOperand().getValueInternal(state).getValue(); + Object rightOperand = getRightOperand().getValueInternal(state).getValue(); + + if (leftOperand instanceof Number && rightOperand instanceof Number) { + Number leftNumber = (Number) leftOperand; + Number rightNumber = (Number) rightOperand; + + if (leftNumber instanceof BigDecimal || rightNumber instanceof BigDecimal) { + BigDecimal leftBigDecimal = NumberUtils.convertNumberToTargetClass(leftNumber, BigDecimal.class); + BigDecimal rightBigDecimal = NumberUtils.convertNumberToTargetClass(rightNumber, BigDecimal.class); + return new TypedValue(leftBigDecimal.multiply(rightBigDecimal)); + } + else if (leftNumber instanceof Double || rightNumber instanceof Double) { + this.exitTypeDescriptor = "D"; + return new TypedValue(leftNumber.doubleValue() * rightNumber.doubleValue()); + } + else if (leftNumber instanceof Float || rightNumber instanceof Float) { + this.exitTypeDescriptor = "F"; + return new TypedValue(leftNumber.floatValue() * rightNumber.floatValue()); + } + else if (leftNumber instanceof BigInteger || rightNumber instanceof BigInteger) { + BigInteger leftBigInteger = NumberUtils.convertNumberToTargetClass(leftNumber, BigInteger.class); + BigInteger rightBigInteger = NumberUtils.convertNumberToTargetClass(rightNumber, BigInteger.class); + return new TypedValue(leftBigInteger.multiply(rightBigInteger)); + } + else if (leftNumber instanceof Long || rightNumber instanceof Long) { + this.exitTypeDescriptor = "J"; + return new TypedValue(leftNumber.longValue() * rightNumber.longValue()); + } + else if (CodeFlow.isIntegerForNumericOp(leftNumber) || CodeFlow.isIntegerForNumericOp(rightNumber)) { + this.exitTypeDescriptor = "I"; + return new TypedValue(leftNumber.intValue() * rightNumber.intValue()); + } + else { + // Unknown Number subtypes -> best guess is double multiplication + return new TypedValue(leftNumber.doubleValue() * rightNumber.doubleValue()); + } + } + + if (leftOperand instanceof String && rightOperand instanceof Integer) { + int repeats = (Integer) rightOperand; + StringBuilder result = new StringBuilder(); + for (int i = 0; i < repeats; i++) { + result.append(leftOperand); + } + return new TypedValue(result.toString()); + } + + return state.operate(Operation.MULTIPLY, leftOperand, rightOperand); + } + + @Override + public boolean isCompilable() { + if (!getLeftOperand().isCompilable()) { + return false; + } + if (this.children.length > 1) { + if (!getRightOperand().isCompilable()) { + return false; + } + } + return (this.exitTypeDescriptor != null); + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + getLeftOperand().generateCode(mv, cf); + String leftDesc = getLeftOperand().exitTypeDescriptor; + String exitDesc = this.exitTypeDescriptor; + Assert.state(exitDesc != null, "No exit type descriptor"); + char targetDesc = exitDesc.charAt(0); + CodeFlow.insertNumericUnboxOrPrimitiveTypeCoercion(mv, leftDesc, targetDesc); + if (this.children.length > 1) { + cf.enterCompilationScope(); + getRightOperand().generateCode(mv, cf); + String rightDesc = getRightOperand().exitTypeDescriptor; + cf.exitCompilationScope(); + CodeFlow.insertNumericUnboxOrPrimitiveTypeCoercion(mv, rightDesc, targetDesc); + switch (targetDesc) { + case 'I': + mv.visitInsn(IMUL); + break; + case 'J': + mv.visitInsn(LMUL); + break; + case 'F': + mv.visitInsn(FMUL); + break; + case 'D': + mv.visitInsn(DMUL); + break; + default: + throw new IllegalStateException( + "Unrecognized exit type descriptor: '" + this.exitTypeDescriptor + "'"); + } + } + cf.pushDescriptor(this.exitTypeDescriptor); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpNE.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpNE.java new file mode 100644 index 0000000..504dfa6 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpNE.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import org.springframework.asm.Label; +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.support.BooleanTypedValue; + +/** + * Implements the not-equal operator. + * + * @author Andy Clement + * @since 3.0 + */ +public class OpNE extends Operator { + + public OpNE(int startPos, int endPos, SpelNodeImpl... operands) { + super("!=", startPos, endPos, operands); + this.exitTypeDescriptor = "Z"; + } + + + @Override + public BooleanTypedValue getValueInternal(ExpressionState state) throws EvaluationException { + Object leftValue = getLeftOperand().getValueInternal(state).getValue(); + Object rightValue = getRightOperand().getValueInternal(state).getValue(); + this.leftActualDescriptor = CodeFlow.toDescriptorFromObject(leftValue); + this.rightActualDescriptor = CodeFlow.toDescriptorFromObject(rightValue); + return BooleanTypedValue.forValue(!equalityCheck(state.getEvaluationContext(), leftValue, rightValue)); + } + + // This check is different to the one in the other numeric operators (OpLt/etc) + // because we allow simple object comparison + @Override + public boolean isCompilable() { + SpelNodeImpl left = getLeftOperand(); + SpelNodeImpl right = getRightOperand(); + if (!left.isCompilable() || !right.isCompilable()) { + return false; + } + + String leftDesc = left.exitTypeDescriptor; + String rightDesc = right.exitTypeDescriptor; + DescriptorComparison dc = DescriptorComparison.checkNumericCompatibility(leftDesc, + rightDesc, this.leftActualDescriptor, this.rightActualDescriptor); + return (!dc.areNumbers || dc.areCompatible); + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + cf.loadEvaluationContext(mv); + String leftDesc = getLeftOperand().exitTypeDescriptor; + String rightDesc = getRightOperand().exitTypeDescriptor; + boolean leftPrim = CodeFlow.isPrimitive(leftDesc); + boolean rightPrim = CodeFlow.isPrimitive(rightDesc); + + cf.enterCompilationScope(); + getLeftOperand().generateCode(mv, cf); + cf.exitCompilationScope(); + if (leftPrim) { + CodeFlow.insertBoxIfNecessary(mv, leftDesc.charAt(0)); + } + cf.enterCompilationScope(); + getRightOperand().generateCode(mv, cf); + cf.exitCompilationScope(); + if (rightPrim) { + CodeFlow.insertBoxIfNecessary(mv, rightDesc.charAt(0)); + } + + String operatorClassName = Operator.class.getName().replace('.', '/'); + String evaluationContextClassName = EvaluationContext.class.getName().replace('.', '/'); + mv.visitMethodInsn(INVOKESTATIC, operatorClassName, "equalityCheck", + "(L" + evaluationContextClassName + ";Ljava/lang/Object;Ljava/lang/Object;)Z", false); + + // Invert the boolean + Label notZero = new Label(); + Label end = new Label(); + mv.visitJumpInsn(IFNE, notZero); + mv.visitInsn(ICONST_1); + mv.visitJumpInsn(GOTO, end); + mv.visitLabel(notZero); + mv.visitInsn(ICONST_0); + mv.visitLabel(end); + + cf.pushDescriptor("Z"); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpOr.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpOr.java new file mode 100644 index 0000000..3afee61 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpOr.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import org.springframework.asm.Label; +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.support.BooleanTypedValue; +import org.springframework.lang.Nullable; + +/** + * Represents the boolean OR operation. + * + * @author Andy Clement + * @author Mark Fisher + * @author Oliver Becker + * @since 3.0 + */ +public class OpOr extends Operator { + + public OpOr(int startPos, int endPos, SpelNodeImpl... operands) { + super("or", startPos, endPos, operands); + this.exitTypeDescriptor = "Z"; + } + + + @Override + public BooleanTypedValue getValueInternal(ExpressionState state) throws EvaluationException { + if (getBooleanValue(state, getLeftOperand())) { + // no need to evaluate right operand + return BooleanTypedValue.TRUE; + } + return BooleanTypedValue.forValue(getBooleanValue(state, getRightOperand())); + } + + private boolean getBooleanValue(ExpressionState state, SpelNodeImpl operand) { + try { + Boolean value = operand.getValue(state, Boolean.class); + assertValueNotNull(value); + return value; + } + catch (SpelEvaluationException ee) { + ee.setPosition(operand.getStartPosition()); + throw ee; + } + } + + private void assertValueNotNull(@Nullable Boolean value) { + if (value == null) { + throw new SpelEvaluationException(SpelMessage.TYPE_CONVERSION_ERROR, "null", "boolean"); + } + } + + @Override + public boolean isCompilable() { + SpelNodeImpl left = getLeftOperand(); + SpelNodeImpl right = getRightOperand(); + return (left.isCompilable() && right.isCompilable() && + CodeFlow.isBooleanCompatible(left.exitTypeDescriptor) && + CodeFlow.isBooleanCompatible(right.exitTypeDescriptor)); + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + // pseudo: if (leftOperandValue) { result=true; } else { result=rightOperandValue; } + Label elseTarget = new Label(); + Label endOfIf = new Label(); + cf.enterCompilationScope(); + getLeftOperand().generateCode(mv, cf); + cf.unboxBooleanIfNecessary(mv); + cf.exitCompilationScope(); + mv.visitJumpInsn(IFEQ, elseTarget); + mv.visitLdcInsn(1); // TRUE + mv.visitJumpInsn(GOTO,endOfIf); + mv.visitLabel(elseTarget); + cf.enterCompilationScope(); + getRightOperand().generateCode(mv, cf); + cf.unboxBooleanIfNecessary(mv); + cf.exitCompilationScope(); + mv.visitLabel(endOfIf); + cf.pushDescriptor(this.exitTypeDescriptor); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpPlus.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpPlus.java new file mode 100644 index 0000000..d128f56 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpPlus.java @@ -0,0 +1,254 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import org.springframework.asm.MethodVisitor; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Operation; +import org.springframework.expression.TypeConverter; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.NumberUtils; + +/** + * The plus operator will: + *
      + *
    • add numbers + *
    • concatenate strings + *
    + * + *

    It can be used as a unary operator for numbers. + * The standard promotions are performed when the operand types vary (double+int=double). + * For other options it defers to the registered overloader. + * + * @author Andy Clement + * @author Juergen Hoeller + * @author Ivo Smid + * @author Giovanni Dall'Oglio Risso + * @since 3.0 + */ +public class OpPlus extends Operator { + + public OpPlus(int startPos, int endPos, SpelNodeImpl... operands) { + super("+", startPos, endPos, operands); + Assert.notEmpty(operands, "Operands must not be empty"); + } + + + @Override + public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + SpelNodeImpl leftOp = getLeftOperand(); + + if (this.children.length < 2) { // if only one operand, then this is unary plus + Object operandOne = leftOp.getValueInternal(state).getValue(); + if (operandOne instanceof Number) { + if (operandOne instanceof Double) { + this.exitTypeDescriptor = "D"; + } + else if (operandOne instanceof Float) { + this.exitTypeDescriptor = "F"; + } + else if (operandOne instanceof Long) { + this.exitTypeDescriptor = "J"; + } + else if (operandOne instanceof Integer) { + this.exitTypeDescriptor = "I"; + } + return new TypedValue(operandOne); + } + return state.operate(Operation.ADD, operandOne, null); + } + + TypedValue operandOneValue = leftOp.getValueInternal(state); + Object leftOperand = operandOneValue.getValue(); + TypedValue operandTwoValue = getRightOperand().getValueInternal(state); + Object rightOperand = operandTwoValue.getValue(); + + if (leftOperand instanceof Number && rightOperand instanceof Number) { + Number leftNumber = (Number) leftOperand; + Number rightNumber = (Number) rightOperand; + + if (leftNumber instanceof BigDecimal || rightNumber instanceof BigDecimal) { + BigDecimal leftBigDecimal = NumberUtils.convertNumberToTargetClass(leftNumber, BigDecimal.class); + BigDecimal rightBigDecimal = NumberUtils.convertNumberToTargetClass(rightNumber, BigDecimal.class); + return new TypedValue(leftBigDecimal.add(rightBigDecimal)); + } + else if (leftNumber instanceof Double || rightNumber instanceof Double) { + this.exitTypeDescriptor = "D"; + return new TypedValue(leftNumber.doubleValue() + rightNumber.doubleValue()); + } + else if (leftNumber instanceof Float || rightNumber instanceof Float) { + this.exitTypeDescriptor = "F"; + return new TypedValue(leftNumber.floatValue() + rightNumber.floatValue()); + } + else if (leftNumber instanceof BigInteger || rightNumber instanceof BigInteger) { + BigInteger leftBigInteger = NumberUtils.convertNumberToTargetClass(leftNumber, BigInteger.class); + BigInteger rightBigInteger = NumberUtils.convertNumberToTargetClass(rightNumber, BigInteger.class); + return new TypedValue(leftBigInteger.add(rightBigInteger)); + } + else if (leftNumber instanceof Long || rightNumber instanceof Long) { + this.exitTypeDescriptor = "J"; + return new TypedValue(leftNumber.longValue() + rightNumber.longValue()); + } + else if (CodeFlow.isIntegerForNumericOp(leftNumber) || CodeFlow.isIntegerForNumericOp(rightNumber)) { + this.exitTypeDescriptor = "I"; + return new TypedValue(leftNumber.intValue() + rightNumber.intValue()); + } + else { + // Unknown Number subtypes -> best guess is double addition + return new TypedValue(leftNumber.doubleValue() + rightNumber.doubleValue()); + } + } + + if (leftOperand instanceof String && rightOperand instanceof String) { + this.exitTypeDescriptor = "Ljava/lang/String"; + return new TypedValue((String) leftOperand + rightOperand); + } + + if (leftOperand instanceof String) { + return new TypedValue( + leftOperand + (rightOperand == null ? "null" : convertTypedValueToString(operandTwoValue, state))); + } + + if (rightOperand instanceof String) { + return new TypedValue( + (leftOperand == null ? "null" : convertTypedValueToString(operandOneValue, state)) + rightOperand); + } + + return state.operate(Operation.ADD, leftOperand, rightOperand); + } + + @Override + public String toStringAST() { + if (this.children.length < 2) { // unary plus + return "+" + getLeftOperand().toStringAST(); + } + return super.toStringAST(); + } + + @Override + public SpelNodeImpl getRightOperand() { + if (this.children.length < 2) { + throw new IllegalStateException("No right operand"); + } + return this.children[1]; + } + + /** + * Convert operand value to string using registered converter or using + * {@code toString} method. + * @param value typed value to be converted + * @param state expression state + * @return {@code TypedValue} instance converted to {@code String} + */ + private static String convertTypedValueToString(TypedValue value, ExpressionState state) { + TypeConverter typeConverter = state.getEvaluationContext().getTypeConverter(); + TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(String.class); + if (typeConverter.canConvert(value.getTypeDescriptor(), typeDescriptor)) { + return String.valueOf(typeConverter.convertValue(value.getValue(), + value.getTypeDescriptor(), typeDescriptor)); + } + return String.valueOf(value.getValue()); + } + + @Override + public boolean isCompilable() { + if (!getLeftOperand().isCompilable()) { + return false; + } + if (this.children.length > 1) { + if (!getRightOperand().isCompilable()) { + return false; + } + } + return (this.exitTypeDescriptor != null); + } + + /** + * Walk through a possible tree of nodes that combine strings and append + * them all to the same (on stack) StringBuilder. + */ + private void walk(MethodVisitor mv, CodeFlow cf, @Nullable SpelNodeImpl operand) { + if (operand instanceof OpPlus) { + OpPlus plus = (OpPlus)operand; + walk(mv, cf, plus.getLeftOperand()); + walk(mv, cf, plus.getRightOperand()); + } + else if (operand != null) { + cf.enterCompilationScope(); + operand.generateCode(mv,cf); + if (!"Ljava/lang/String".equals(cf.lastDescriptor())) { + mv.visitTypeInsn(CHECKCAST, "java/lang/String"); + } + cf.exitCompilationScope(); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false); + } + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + if ("Ljava/lang/String".equals(this.exitTypeDescriptor)) { + mv.visitTypeInsn(NEW, "java/lang/StringBuilder"); + mv.visitInsn(DUP); + mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "", "()V", false); + walk(mv, cf, getLeftOperand()); + walk(mv, cf, getRightOperand()); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false); + } + else { + this.children[0].generateCode(mv, cf); + String leftDesc = this.children[0].exitTypeDescriptor; + String exitDesc = this.exitTypeDescriptor; + Assert.state(exitDesc != null, "No exit type descriptor"); + char targetDesc = exitDesc.charAt(0); + CodeFlow.insertNumericUnboxOrPrimitiveTypeCoercion(mv, leftDesc, targetDesc); + if (this.children.length > 1) { + cf.enterCompilationScope(); + this.children[1].generateCode(mv, cf); + String rightDesc = this.children[1].exitTypeDescriptor; + cf.exitCompilationScope(); + CodeFlow.insertNumericUnboxOrPrimitiveTypeCoercion(mv, rightDesc, targetDesc); + switch (targetDesc) { + case 'I': + mv.visitInsn(IADD); + break; + case 'J': + mv.visitInsn(LADD); + break; + case 'F': + mv.visitInsn(FADD); + break; + case 'D': + mv.visitInsn(DADD); + break; + default: + throw new IllegalStateException( + "Unrecognized exit type descriptor: '" + this.exitTypeDescriptor + "'"); + } + } + } + cf.pushDescriptor(this.exitTypeDescriptor); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Operator.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Operator.java new file mode 100644 index 0000000..12cd283 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Operator.java @@ -0,0 +1,395 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import org.springframework.asm.Label; +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.NumberUtils; +import org.springframework.util.ObjectUtils; + +/** + * Common supertype for operators that operate on either one or two operands. + * In the case of multiply or divide there would be two operands, but for + * unary plus or minus, there is only one. + * + * @author Andy Clement + * @author Juergen Hoeller + * @author Giovanni Dall'Oglio Risso + * @since 3.0 + */ +public abstract class Operator extends SpelNodeImpl { + + private final String operatorName; + + // The descriptors of the runtime operand values are used if the discovered declared + // descriptors are not providing enough information (for example a generic type + // whose accessors seem to only be returning 'Object' - the actual descriptors may + // indicate 'int') + + @Nullable + protected String leftActualDescriptor; + + @Nullable + protected String rightActualDescriptor; + + + public Operator(String payload, int startPos, int endPos, SpelNodeImpl... operands) { + super(startPos, endPos, operands); + this.operatorName = payload; + } + + + public SpelNodeImpl getLeftOperand() { + return this.children[0]; + } + + public SpelNodeImpl getRightOperand() { + return this.children[1]; + } + + public final String getOperatorName() { + return this.operatorName; + } + + /** + * String format for all operators is the same + * {@code '(' [operand] [operator] [operand] ')'}. + */ + @Override + public String toStringAST() { + StringBuilder sb = new StringBuilder("("); + sb.append(getChild(0).toStringAST()); + for (int i = 1; i < getChildCount(); i++) { + sb.append(" ").append(getOperatorName()).append(" "); + sb.append(getChild(i).toStringAST()); + } + sb.append(")"); + return sb.toString(); + } + + + protected boolean isCompilableOperatorUsingNumerics() { + SpelNodeImpl left = getLeftOperand(); + SpelNodeImpl right = getRightOperand(); + if (!left.isCompilable() || !right.isCompilable()) { + return false; + } + + // Supported operand types for equals (at the moment) + String leftDesc = left.exitTypeDescriptor; + String rightDesc = right.exitTypeDescriptor; + DescriptorComparison dc = DescriptorComparison.checkNumericCompatibility( + leftDesc, rightDesc, this.leftActualDescriptor, this.rightActualDescriptor); + return (dc.areNumbers && dc.areCompatible); + } + + /** + * Numeric comparison operators share very similar generated code, only differing in + * two comparison instructions. + */ + protected void generateComparisonCode(MethodVisitor mv, CodeFlow cf, int compInstruction1, int compInstruction2) { + SpelNodeImpl left = getLeftOperand(); + SpelNodeImpl right = getRightOperand(); + String leftDesc = left.exitTypeDescriptor; + String rightDesc = right.exitTypeDescriptor; + Label elseTarget = new Label(); + Label endOfIf = new Label(); + boolean unboxLeft = !CodeFlow.isPrimitive(leftDesc); + boolean unboxRight = !CodeFlow.isPrimitive(rightDesc); + DescriptorComparison dc = DescriptorComparison.checkNumericCompatibility( + leftDesc, rightDesc, this.leftActualDescriptor, this.rightActualDescriptor); + char targetType = dc.compatibleType; // CodeFlow.toPrimitiveTargetDesc(leftDesc); + + cf.enterCompilationScope(); + left.generateCode(mv, cf); + cf.exitCompilationScope(); + if (CodeFlow.isPrimitive(leftDesc)) { + CodeFlow.insertBoxIfNecessary(mv, leftDesc); + unboxLeft = true; + } + + cf.enterCompilationScope(); + right.generateCode(mv, cf); + cf.exitCompilationScope(); + if (CodeFlow.isPrimitive(rightDesc)) { + CodeFlow.insertBoxIfNecessary(mv, rightDesc); + unboxRight = true; + } + + // This code block checks whether the left or right operand is null and handles + // those cases before letting the original code (that only handled actual numbers) run + Label rightIsNonNull = new Label(); + mv.visitInsn(DUP); // stack: left/right/right + mv.visitJumpInsn(IFNONNULL, rightIsNonNull); // stack: left/right + // here: RIGHT==null LEFT==unknown + mv.visitInsn(SWAP); // right/left + Label leftNotNullRightIsNull = new Label(); + mv.visitJumpInsn(IFNONNULL, leftNotNullRightIsNull); // stack: right + // here: RIGHT==null LEFT==null + mv.visitInsn(POP); // stack: + // load 0 or 1 depending on comparison instruction + switch (compInstruction1) { + case IFGE: // OpLT + case IFLE: // OpGT + mv.visitInsn(ICONST_0); // false - null is not < or > null + break; + case IFGT: // OpLE + case IFLT: // OpGE + mv.visitInsn(ICONST_1); // true - null is <= or >= null + break; + default: + throw new IllegalStateException("Unsupported: " + compInstruction1); + } + mv.visitJumpInsn(GOTO, endOfIf); + mv.visitLabel(leftNotNullRightIsNull); // stack: right + // RIGHT==null LEFT!=null + mv.visitInsn(POP); // stack: + // load 0 or 1 depending on comparison instruction + switch (compInstruction1) { + case IFGE: // OpLT + case IFGT: // OpLE + mv.visitInsn(ICONST_0); // false - something is not < or <= null + break; + case IFLE: // OpGT + case IFLT: // OpGE + mv.visitInsn(ICONST_1); // true - something is > or >= null + break; + default: + throw new IllegalStateException("Unsupported: " + compInstruction1); + } + mv.visitJumpInsn(GOTO, endOfIf); + + mv.visitLabel(rightIsNonNull); // stack: left/right + // here: RIGHT!=null LEFT==unknown + mv.visitInsn(SWAP); // stack: right/left + mv.visitInsn(DUP); // stack: right/left/left + Label neitherRightNorLeftAreNull = new Label(); + mv.visitJumpInsn(IFNONNULL, neitherRightNorLeftAreNull); // stack: right/left + // here: RIGHT!=null LEFT==null + mv.visitInsn(POP2); // stack: + switch (compInstruction1) { + case IFGE: // OpLT + case IFGT: // OpLE + mv.visitInsn(ICONST_1); // true - null is < or <= something + break; + case IFLE: // OpGT + case IFLT: // OpGE + mv.visitInsn(ICONST_0); // false - null is not > or >= something + break; + default: + throw new IllegalStateException("Unsupported: " + compInstruction1); + } + mv.visitJumpInsn(GOTO, endOfIf); + mv.visitLabel(neitherRightNorLeftAreNull); // stack: right/left + // neither were null so unbox and proceed with numeric comparison + if (unboxLeft) { + CodeFlow.insertUnboxInsns(mv, targetType, leftDesc); + } + // What we just unboxed might be a double slot item (long/double) + // so can't just use SWAP + // stack: right/left(1or2slots) + if (targetType == 'D' || targetType == 'J') { + mv.visitInsn(DUP2_X1); + mv.visitInsn(POP2); + } + else { + mv.visitInsn(SWAP); + } + // stack: left(1or2)/right + if (unboxRight) { + CodeFlow.insertUnboxInsns(mv, targetType, rightDesc); + } + + // assert: SpelCompiler.boxingCompatible(leftDesc, rightDesc) + if (targetType == 'D') { + mv.visitInsn(DCMPG); + mv.visitJumpInsn(compInstruction1, elseTarget); + } + else if (targetType == 'F') { + mv.visitInsn(FCMPG); + mv.visitJumpInsn(compInstruction1, elseTarget); + } + else if (targetType == 'J') { + mv.visitInsn(LCMP); + mv.visitJumpInsn(compInstruction1, elseTarget); + } + else if (targetType == 'I') { + mv.visitJumpInsn(compInstruction2, elseTarget); + } + else { + throw new IllegalStateException("Unexpected descriptor " + leftDesc); + } + + // Other numbers are not yet supported (isCompilable will not have returned true) + mv.visitInsn(ICONST_1); + mv.visitJumpInsn(GOTO,endOfIf); + mv.visitLabel(elseTarget); + mv.visitInsn(ICONST_0); + mv.visitLabel(endOfIf); + cf.pushDescriptor("Z"); + } + + + /** + * Perform an equality check for the given operand values. + *

    This method is not just used for reflective comparisons in subclasses + * but also from compiled expression code, which is why it needs to be + * declared as {@code public static} here. + * @param context the current evaluation context + * @param left the left-hand operand value + * @param right the right-hand operand value + */ + public static boolean equalityCheck(EvaluationContext context, @Nullable Object left, @Nullable Object right) { + if (left instanceof Number && right instanceof Number) { + Number leftNumber = (Number) left; + Number rightNumber = (Number) right; + + if (leftNumber instanceof BigDecimal || rightNumber instanceof BigDecimal) { + BigDecimal leftBigDecimal = NumberUtils.convertNumberToTargetClass(leftNumber, BigDecimal.class); + BigDecimal rightBigDecimal = NumberUtils.convertNumberToTargetClass(rightNumber, BigDecimal.class); + return (leftBigDecimal.compareTo(rightBigDecimal) == 0); + } + else if (leftNumber instanceof Double || rightNumber instanceof Double) { + return (leftNumber.doubleValue() == rightNumber.doubleValue()); + } + else if (leftNumber instanceof Float || rightNumber instanceof Float) { + return (leftNumber.floatValue() == rightNumber.floatValue()); + } + else if (leftNumber instanceof BigInteger || rightNumber instanceof BigInteger) { + BigInteger leftBigInteger = NumberUtils.convertNumberToTargetClass(leftNumber, BigInteger.class); + BigInteger rightBigInteger = NumberUtils.convertNumberToTargetClass(rightNumber, BigInteger.class); + return (leftBigInteger.compareTo(rightBigInteger) == 0); + } + else if (leftNumber instanceof Long || rightNumber instanceof Long) { + return (leftNumber.longValue() == rightNumber.longValue()); + } + else if (leftNumber instanceof Integer || rightNumber instanceof Integer) { + return (leftNumber.intValue() == rightNumber.intValue()); + } + else if (leftNumber instanceof Short || rightNumber instanceof Short) { + return (leftNumber.shortValue() == rightNumber.shortValue()); + } + else if (leftNumber instanceof Byte || rightNumber instanceof Byte) { + return (leftNumber.byteValue() == rightNumber.byteValue()); + } + else { + // Unknown Number subtypes -> best guess is double comparison + return (leftNumber.doubleValue() == rightNumber.doubleValue()); + } + } + + if (left instanceof CharSequence && right instanceof CharSequence) { + return left.toString().equals(right.toString()); + } + + if (left instanceof Boolean && right instanceof Boolean) { + return left.equals(right); + } + + if (ObjectUtils.nullSafeEquals(left, right)) { + return true; + } + + if (left instanceof Comparable && right instanceof Comparable) { + Class ancestor = ClassUtils.determineCommonAncestor(left.getClass(), right.getClass()); + if (ancestor != null && Comparable.class.isAssignableFrom(ancestor)) { + return (context.getTypeComparator().compare(left, right) == 0); + } + } + + return false; + } + + + /** + * A descriptor comparison encapsulates the result of comparing descriptor + * for two operands and describes at what level they are compatible. + */ + protected static final class DescriptorComparison { + + static final DescriptorComparison NOT_NUMBERS = new DescriptorComparison(false, false, ' '); + + static final DescriptorComparison INCOMPATIBLE_NUMBERS = new DescriptorComparison(true, false, ' '); + + final boolean areNumbers; // Were the two compared descriptor both for numbers? + + final boolean areCompatible; // If they were numbers, were they compatible? + + final char compatibleType; // When compatible, what is the descriptor of the common type + + private DescriptorComparison(boolean areNumbers, boolean areCompatible, char compatibleType) { + this.areNumbers = areNumbers; + this.areCompatible = areCompatible; + this.compatibleType = compatibleType; + } + + /** + * Return an object that indicates whether the input descriptors are compatible. + *

    A declared descriptor is what could statically be determined (e.g. from looking + * at the return value of a property accessor method) whilst an actual descriptor + * is the type of an actual object that was returned, which may differ. + *

    For generic types with unbound type variables, the declared descriptor + * discovered may be 'Object' but from the actual descriptor it is possible to + * observe that the objects are really numeric values (e.g. ints). + * @param leftDeclaredDescriptor the statically determinable left descriptor + * @param rightDeclaredDescriptor the statically determinable right descriptor + * @param leftActualDescriptor the dynamic/runtime left object descriptor + * @param rightActualDescriptor the dynamic/runtime right object descriptor + * @return a DescriptorComparison object indicating the type of compatibility, if any + */ + public static DescriptorComparison checkNumericCompatibility( + @Nullable String leftDeclaredDescriptor, @Nullable String rightDeclaredDescriptor, + @Nullable String leftActualDescriptor, @Nullable String rightActualDescriptor) { + + String ld = leftDeclaredDescriptor; + String rd = rightDeclaredDescriptor; + + boolean leftNumeric = CodeFlow.isPrimitiveOrUnboxableSupportedNumberOrBoolean(ld); + boolean rightNumeric = CodeFlow.isPrimitiveOrUnboxableSupportedNumberOrBoolean(rd); + + // If the declared descriptors aren't providing the information, try the actual descriptors + if (!leftNumeric && !ObjectUtils.nullSafeEquals(ld, leftActualDescriptor)) { + ld = leftActualDescriptor; + leftNumeric = CodeFlow.isPrimitiveOrUnboxableSupportedNumberOrBoolean(ld); + } + if (!rightNumeric && !ObjectUtils.nullSafeEquals(rd, rightActualDescriptor)) { + rd = rightActualDescriptor; + rightNumeric = CodeFlow.isPrimitiveOrUnboxableSupportedNumberOrBoolean(rd); + } + + if (leftNumeric && rightNumeric) { + if (CodeFlow.areBoxingCompatible(ld, rd)) { + return new DescriptorComparison(true, true, CodeFlow.toPrimitiveTargetDesc(ld)); + } + else { + return DescriptorComparison.INCOMPATIBLE_NUMBERS; + } + } + else { + return DescriptorComparison.NOT_NUMBERS; + } + } + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorBetween.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorBetween.java new file mode 100644 index 0000000..e5f94ad --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorBetween.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.util.List; + +import org.springframework.expression.EvaluationException; +import org.springframework.expression.TypeComparator; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.support.BooleanTypedValue; + +/** + * Represents the between operator. The left operand to between must be a single value and + * the right operand must be a list - this operator returns true if the left operand is + * between (using the registered comparator) the two elements in the list. The definition + * of between being inclusive follows the SQL BETWEEN definition. + * + * @author Andy Clement + * @since 3.0 + */ +public class OperatorBetween extends Operator { + + public OperatorBetween(int startPos, int endPos, SpelNodeImpl... operands) { + super("between", startPos, endPos, operands); + } + + + /** + * Returns a boolean based on whether a value is in the range expressed. The first + * operand is any value whilst the second is a list of two values - those two values + * being the bounds allowed for the first operand (inclusive). + * @param state the expression state + * @return true if the left operand is in the range specified, false otherwise + * @throws EvaluationException if there is a problem evaluating the expression + */ + @Override + public BooleanTypedValue getValueInternal(ExpressionState state) throws EvaluationException { + Object left = getLeftOperand().getValueInternal(state).getValue(); + Object right = getRightOperand().getValueInternal(state).getValue(); + if (!(right instanceof List) || ((List) right).size() != 2) { + throw new SpelEvaluationException(getRightOperand().getStartPosition(), + SpelMessage.BETWEEN_RIGHT_OPERAND_MUST_BE_TWO_ELEMENT_LIST); + } + + List list = (List) right; + Object low = list.get(0); + Object high = list.get(1); + TypeComparator comp = state.getTypeComparator(); + try { + return BooleanTypedValue.forValue(comp.compare(left, low) >= 0 && comp.compare(left, high) <= 0); + } + catch (SpelEvaluationException ex) { + ex.setPosition(getStartPosition()); + throw ex; + } + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorInstanceof.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorInstanceof.java new file mode 100644 index 0000000..0deb8b7 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorInstanceof.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import org.springframework.asm.MethodVisitor; +import org.springframework.asm.Type; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.support.BooleanTypedValue; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * The operator 'instanceof' checks if an object is of the class specified in the right + * hand operand, in the same way that {@code instanceof} does in Java. + * + * @author Andy Clement + * @since 3.0 + */ +public class OperatorInstanceof extends Operator { + + @Nullable + private Class type; + + + public OperatorInstanceof(int startPos, int endPos, SpelNodeImpl... operands) { + super("instanceof", startPos, endPos, operands); + } + + + /** + * Compare the left operand to see it is an instance of the type specified as the + * right operand. The right operand must be a class. + * @param state the expression state + * @return {@code true} if the left operand is an instanceof of the right operand, + * otherwise {@code false} + * @throws EvaluationException if there is a problem evaluating the expression + */ + @Override + public BooleanTypedValue getValueInternal(ExpressionState state) throws EvaluationException { + SpelNodeImpl rightOperand = getRightOperand(); + TypedValue left = getLeftOperand().getValueInternal(state); + TypedValue right = rightOperand.getValueInternal(state); + Object leftValue = left.getValue(); + Object rightValue = right.getValue(); + BooleanTypedValue result; + if (!(rightValue instanceof Class)) { + throw new SpelEvaluationException(getRightOperand().getStartPosition(), + SpelMessage.INSTANCEOF_OPERATOR_NEEDS_CLASS_OPERAND, + (rightValue == null ? "null" : rightValue.getClass().getName())); + } + Class rightClass = (Class) rightValue; + if (leftValue == null) { + result = BooleanTypedValue.FALSE; // null is not an instanceof anything + } + else { + result = BooleanTypedValue.forValue(rightClass.isAssignableFrom(leftValue.getClass())); + } + this.type = rightClass; + if (rightOperand instanceof TypeReference) { + // Can only generate bytecode where the right operand is a direct type reference, + // not if it is indirect (for example when right operand is a variable reference) + this.exitTypeDescriptor = "Z"; + } + return result; + } + + @Override + public boolean isCompilable() { + return (this.exitTypeDescriptor != null && getLeftOperand().isCompilable()); + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + getLeftOperand().generateCode(mv, cf); + CodeFlow.insertBoxIfNecessary(mv, cf.lastDescriptor()); + Assert.state(this.type != null, "No type available"); + if (this.type.isPrimitive()) { + // always false - but left operand code always driven + // in case it had side effects + mv.visitInsn(POP); + mv.visitInsn(ICONST_0); // value of false + } + else { + mv.visitTypeInsn(INSTANCEOF, Type.getInternalName(this.type)); + } + cf.pushDescriptor(this.exitTypeDescriptor); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorMatches.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorMatches.java new file mode 100644 index 0000000..22795a9 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorMatches.java @@ -0,0 +1,143 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import org.springframework.expression.EvaluationException; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.support.BooleanTypedValue; + +/** + * Implements the matches operator. Matches takes two operands: + * The first is a String and the second is a Java regex. + * It will return {@code true} when {@link #getValue} is called + * if the first operand matches the regex. + * + * @author Andy Clement + * @author Juergen Hoeller + * @since 3.0 + */ +public class OperatorMatches extends Operator { + + private static final int PATTERN_ACCESS_THRESHOLD = 1000000; + + private final ConcurrentMap patternCache = new ConcurrentHashMap<>(); + + + public OperatorMatches(int startPos, int endPos, SpelNodeImpl... operands) { + super("matches", startPos, endPos, operands); + } + + + /** + * Check the first operand matches the regex specified as the second operand. + * @param state the expression state + * @return {@code true} if the first operand matches the regex specified as the + * second operand, otherwise {@code false} + * @throws EvaluationException if there is a problem evaluating the expression + * (e.g. the regex is invalid) + */ + @Override + public BooleanTypedValue getValueInternal(ExpressionState state) throws EvaluationException { + SpelNodeImpl leftOp = getLeftOperand(); + SpelNodeImpl rightOp = getRightOperand(); + String left = leftOp.getValue(state, String.class); + Object right = getRightOperand().getValue(state); + + if (left == null) { + throw new SpelEvaluationException(leftOp.getStartPosition(), + SpelMessage.INVALID_FIRST_OPERAND_FOR_MATCHES_OPERATOR, (Object) null); + } + if (!(right instanceof String)) { + throw new SpelEvaluationException(rightOp.getStartPosition(), + SpelMessage.INVALID_SECOND_OPERAND_FOR_MATCHES_OPERATOR, right); + } + + try { + String rightString = (String) right; + Pattern pattern = this.patternCache.get(rightString); + if (pattern == null) { + pattern = Pattern.compile(rightString); + this.patternCache.putIfAbsent(rightString, pattern); + } + Matcher matcher = pattern.matcher(new MatcherInput(left, new AccessCount())); + return BooleanTypedValue.forValue(matcher.matches()); + } + catch (PatternSyntaxException ex) { + throw new SpelEvaluationException( + rightOp.getStartPosition(), ex, SpelMessage.INVALID_PATTERN, right); + } + catch (IllegalStateException ex) { + throw new SpelEvaluationException( + rightOp.getStartPosition(), ex, SpelMessage.FLAWED_PATTERN, right); + } + } + + + private static class AccessCount { + + private int count; + + public void check() throws IllegalStateException { + if (this.count++ > PATTERN_ACCESS_THRESHOLD) { + throw new IllegalStateException("Pattern access threshold exceeded"); + } + } + } + + + private static class MatcherInput implements CharSequence { + + private final CharSequence value; + + private AccessCount access; + + public MatcherInput(CharSequence value, AccessCount access) { + this.value = value; + this.access = access; + } + + @Override + public char charAt(int index) { + this.access.check(); + return this.value.charAt(index); + } + + @Override + public CharSequence subSequence(int start, int end) { + return new MatcherInput(this.value.subSequence(start, end), this.access); + } + + @Override + public int length() { + return this.value.length(); + } + + @Override + public String toString() { + return this.value.toString(); + } + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorNot.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorNot.java new file mode 100644 index 0000000..4dd9502 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorNot.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import org.springframework.asm.Label; +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.support.BooleanTypedValue; + +/** + * Represents a NOT operation. + * + * @author Andy Clement + * @author Mark Fisher + * @author Oliver Becker + * @since 3.0 + */ +public class OperatorNot extends SpelNodeImpl { // Not is a unary operator so does not extend BinaryOperator + + public OperatorNot(int startPos, int endPos, SpelNodeImpl operand) { + super(startPos, endPos, operand); + this.exitTypeDescriptor = "Z"; + } + + + @Override + public BooleanTypedValue getValueInternal(ExpressionState state) throws EvaluationException { + try { + Boolean value = this.children[0].getValue(state, Boolean.class); + if (value == null) { + throw new SpelEvaluationException(SpelMessage.TYPE_CONVERSION_ERROR, "null", "boolean"); + } + return BooleanTypedValue.forValue(!value); + } + catch (SpelEvaluationException ex) { + ex.setPosition(getChild(0).getStartPosition()); + throw ex; + } + } + + @Override + public String toStringAST() { + return "!" + getChild(0).toStringAST(); + } + + @Override + public boolean isCompilable() { + SpelNodeImpl child = this.children[0]; + return (child.isCompilable() && CodeFlow.isBooleanCompatible(child.exitTypeDescriptor)); + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + this.children[0].generateCode(mv, cf); + cf.unboxBooleanIfNecessary(mv); + Label elseTarget = new Label(); + Label endOfIf = new Label(); + mv.visitJumpInsn(IFNE,elseTarget); + mv.visitInsn(ICONST_1); // TRUE + mv.visitJumpInsn(GOTO,endOfIf); + mv.visitLabel(elseTarget); + mv.visitInsn(ICONST_0); // FALSE + mv.visitLabel(endOfIf); + cf.pushDescriptor(this.exitTypeDescriptor); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorPower.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorPower.java new file mode 100644 index 0000000..2a256ea --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorPower.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Operation; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.util.NumberUtils; + +/** + * The power operator. + * + * @author Andy Clement + * @author Giovanni Dall'Oglio Risso + * @since 3.0 + */ +public class OperatorPower extends Operator { + + public OperatorPower(int startPos, int endPos, SpelNodeImpl... operands) { + super("^", startPos, endPos, operands); + } + + + @Override + public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + SpelNodeImpl leftOp = getLeftOperand(); + SpelNodeImpl rightOp = getRightOperand(); + + Object leftOperand = leftOp.getValueInternal(state).getValue(); + Object rightOperand = rightOp.getValueInternal(state).getValue(); + + if (leftOperand instanceof Number && rightOperand instanceof Number) { + Number leftNumber = (Number) leftOperand; + Number rightNumber = (Number) rightOperand; + + if (leftNumber instanceof BigDecimal) { + BigDecimal leftBigDecimal = NumberUtils.convertNumberToTargetClass(leftNumber, BigDecimal.class); + return new TypedValue(leftBigDecimal.pow(rightNumber.intValue())); + } + else if (leftNumber instanceof BigInteger) { + BigInteger leftBigInteger = NumberUtils.convertNumberToTargetClass(leftNumber, BigInteger.class); + return new TypedValue(leftBigInteger.pow(rightNumber.intValue())); + } + else if (leftNumber instanceof Double || rightNumber instanceof Double) { + return new TypedValue(Math.pow(leftNumber.doubleValue(), rightNumber.doubleValue())); + } + else if (leftNumber instanceof Float || rightNumber instanceof Float) { + return new TypedValue(Math.pow(leftNumber.floatValue(), rightNumber.floatValue())); + } + + double d = Math.pow(leftNumber.doubleValue(), rightNumber.doubleValue()); + if (d > Integer.MAX_VALUE || leftNumber instanceof Long || rightNumber instanceof Long) { + return new TypedValue((long) d); + } + else { + return new TypedValue((int) d); + } + } + + return state.operate(Operation.POWER, leftOperand, rightOperand); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java new file mode 100644 index 0000000..1639a9e --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java @@ -0,0 +1,162 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.springframework.expression.EvaluationException; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; + +/** + * Represents projection, where a given operation is performed on all elements in some + * input sequence, returning a new sequence of the same size. For example: + * "{1,2,3,4,5,6,7,8,9,10}.!{#isEven(#this)}" returns "[n, y, n, y, n, y, n, y, n, y]" + * + * @author Andy Clement + * @author Mark Fisher + * @author Juergen Hoeller + * @since 3.0 + */ +public class Projection extends SpelNodeImpl { + + private final boolean nullSafe; + + + public Projection(boolean nullSafe, int startPos, int endPos, SpelNodeImpl expression) { + super(startPos, endPos, expression); + this.nullSafe = nullSafe; + } + + + @Override + public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + return getValueRef(state).getValue(); + } + + @Override + protected ValueRef getValueRef(ExpressionState state) throws EvaluationException { + TypedValue op = state.getActiveContextObject(); + + Object operand = op.getValue(); + boolean operandIsArray = ObjectUtils.isArray(operand); + // TypeDescriptor operandTypeDescriptor = op.getTypeDescriptor(); + + // When the input is a map, we push a special context object on the stack + // before calling the specified operation. This special context object + // has two fields 'key' and 'value' that refer to the map entries key + // and value, and they can be referenced in the operation + // eg. {'a':'y','b':'n'}.![value=='y'?key:null]" == ['a', null] + if (operand instanceof Map) { + Map mapData = (Map) operand; + List result = new ArrayList<>(); + for (Map.Entry entry : mapData.entrySet()) { + try { + state.pushActiveContextObject(new TypedValue(entry)); + state.enterScope(); + result.add(this.children[0].getValueInternal(state).getValue()); + } + finally { + state.popActiveContextObject(); + state.exitScope(); + } + } + return new ValueRef.TypedValueHolderValueRef(new TypedValue(result), this); // TODO unable to build correct type descriptor + } + + if (operand instanceof Iterable || operandIsArray) { + Iterable data = (operand instanceof Iterable ? + (Iterable) operand : Arrays.asList(ObjectUtils.toObjectArray(operand))); + + List result = new ArrayList<>(); + Class arrayElementType = null; + for (Object element : data) { + try { + state.pushActiveContextObject(new TypedValue(element)); + state.enterScope("index", result.size()); + Object value = this.children[0].getValueInternal(state).getValue(); + if (value != null && operandIsArray) { + arrayElementType = determineCommonType(arrayElementType, value.getClass()); + } + result.add(value); + } + finally { + state.exitScope(); + state.popActiveContextObject(); + } + } + + if (operandIsArray) { + if (arrayElementType == null) { + arrayElementType = Object.class; + } + Object resultArray = Array.newInstance(arrayElementType, result.size()); + System.arraycopy(result.toArray(), 0, resultArray, 0, result.size()); + return new ValueRef.TypedValueHolderValueRef(new TypedValue(resultArray),this); + } + + return new ValueRef.TypedValueHolderValueRef(new TypedValue(result),this); + } + + if (operand == null) { + if (this.nullSafe) { + return ValueRef.NullValueRef.INSTANCE; + } + throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROJECTION_NOT_SUPPORTED_ON_TYPE, "null"); + } + + throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROJECTION_NOT_SUPPORTED_ON_TYPE, + operand.getClass().getName()); + } + + @Override + public String toStringAST() { + return "![" + getChild(0).toStringAST() + "]"; + } + + private Class determineCommonType(@Nullable Class oldType, Class newType) { + if (oldType == null) { + return newType; + } + if (oldType.isAssignableFrom(newType)) { + return oldType; + } + Class nextType = newType; + while (nextType != Object.class) { + if (nextType.isAssignableFrom(oldType)) { + return nextType; + } + nextType = nextType.getSuperclass(); + } + for (Class nextInterface : ClassUtils.getAllInterfacesForClassAsSet(newType)) { + if (nextInterface.isAssignableFrom(oldType)) { + return nextInterface; + } + } + return Object.class; + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java new file mode 100644 index 0000000..476fb4c --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java @@ -0,0 +1,425 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.asm.Label; +import org.springframework.asm.MethodVisitor; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.CompilablePropertyAccessor; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.support.ReflectivePropertyAccessor; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +/** + * Represents a simple property or field reference. + * + * @author Andy Clement + * @author Juergen Hoeller + * @author Clark Duplichien + * @since 3.0 + */ +public class PropertyOrFieldReference extends SpelNodeImpl { + + private final boolean nullSafe; + + private final String name; + + @Nullable + private String originalPrimitiveExitTypeDescriptor; + + @Nullable + private volatile PropertyAccessor cachedReadAccessor; + + @Nullable + private volatile PropertyAccessor cachedWriteAccessor; + + + public PropertyOrFieldReference(boolean nullSafe, String propertyOrFieldName, int startPos, int endPos) { + super(startPos, endPos); + this.nullSafe = nullSafe; + this.name = propertyOrFieldName; + } + + + public boolean isNullSafe() { + return this.nullSafe; + } + + public String getName() { + return this.name; + } + + + @Override + public ValueRef getValueRef(ExpressionState state) throws EvaluationException { + return new AccessorLValue(this, state.getActiveContextObject(), state.getEvaluationContext(), + state.getConfiguration().isAutoGrowNullReferences()); + } + + @Override + public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + TypedValue tv = getValueInternal(state.getActiveContextObject(), state.getEvaluationContext(), + state.getConfiguration().isAutoGrowNullReferences()); + PropertyAccessor accessorToUse = this.cachedReadAccessor; + if (accessorToUse instanceof CompilablePropertyAccessor) { + CompilablePropertyAccessor accessor = (CompilablePropertyAccessor) accessorToUse; + setExitTypeDescriptor(CodeFlow.toDescriptor(accessor.getPropertyType())); + } + return tv; + } + + private TypedValue getValueInternal(TypedValue contextObject, EvaluationContext evalContext, + boolean isAutoGrowNullReferences) throws EvaluationException { + + TypedValue result = readProperty(contextObject, evalContext, this.name); + + // Dynamically create the objects if the user has requested that optional behavior + if (result.getValue() == null && isAutoGrowNullReferences && + nextChildIs(Indexer.class, PropertyOrFieldReference.class)) { + TypeDescriptor resultDescriptor = result.getTypeDescriptor(); + Assert.state(resultDescriptor != null, "No result type"); + // Create a new collection or map ready for the indexer + if (List.class == resultDescriptor.getType()) { + if (isWritableProperty(this.name, contextObject, evalContext)) { + List newList = new ArrayList<>(); + writeProperty(contextObject, evalContext, this.name, newList); + result = readProperty(contextObject, evalContext, this.name); + } + } + else if (Map.class == resultDescriptor.getType()) { + if (isWritableProperty(this.name,contextObject, evalContext)) { + Map newMap = new HashMap<>(); + writeProperty(contextObject, evalContext, this.name, newMap); + result = readProperty(contextObject, evalContext, this.name); + } + } + else { + // 'simple' object + try { + if (isWritableProperty(this.name,contextObject, evalContext)) { + Class clazz = result.getTypeDescriptor().getType(); + Object newObject = ReflectionUtils.accessibleConstructor(clazz).newInstance(); + writeProperty(contextObject, evalContext, this.name, newObject); + result = readProperty(contextObject, evalContext, this.name); + } + } + catch (InvocationTargetException ex) { + throw new SpelEvaluationException(getStartPosition(), ex.getTargetException(), + SpelMessage.UNABLE_TO_DYNAMICALLY_CREATE_OBJECT, result.getTypeDescriptor().getType()); + } + catch (Throwable ex) { + throw new SpelEvaluationException(getStartPosition(), ex, + SpelMessage.UNABLE_TO_DYNAMICALLY_CREATE_OBJECT, result.getTypeDescriptor().getType()); + } + } + } + return result; + } + + @Override + public void setValue(ExpressionState state, @Nullable Object newValue) throws EvaluationException { + writeProperty(state.getActiveContextObject(), state.getEvaluationContext(), this.name, newValue); + } + + @Override + public boolean isWritable(ExpressionState state) throws EvaluationException { + return isWritableProperty(this.name, state.getActiveContextObject(), state.getEvaluationContext()); + } + + @Override + public String toStringAST() { + return this.name; + } + + /** + * Attempt to read the named property from the current context object. + * @return the value of the property + * @throws EvaluationException if any problem accessing the property or it cannot be found + */ + private TypedValue readProperty(TypedValue contextObject, EvaluationContext evalContext, String name) + throws EvaluationException { + + Object targetObject = contextObject.getValue(); + if (targetObject == null && this.nullSafe) { + return TypedValue.NULL; + } + + PropertyAccessor accessorToUse = this.cachedReadAccessor; + if (accessorToUse != null) { + if (evalContext.getPropertyAccessors().contains(accessorToUse)) { + try { + return accessorToUse.read(evalContext, contextObject.getValue(), name); + } + catch (Exception ex) { + // This is OK - it may have gone stale due to a class change, + // let's try to get a new one and call it before giving up... + } + } + this.cachedReadAccessor = null; + } + + List accessorsToTry = + getPropertyAccessorsToTry(contextObject.getValue(), evalContext.getPropertyAccessors()); + // Go through the accessors that may be able to resolve it. If they are a cacheable accessor then + // get the accessor and use it. If they are not cacheable but report they can read the property + // then ask them to read it + try { + for (PropertyAccessor accessor : accessorsToTry) { + if (accessor.canRead(evalContext, contextObject.getValue(), name)) { + if (accessor instanceof ReflectivePropertyAccessor) { + accessor = ((ReflectivePropertyAccessor) accessor).createOptimalAccessor( + evalContext, contextObject.getValue(), name); + } + this.cachedReadAccessor = accessor; + return accessor.read(evalContext, contextObject.getValue(), name); + } + } + } + catch (Exception ex) { + throw new SpelEvaluationException(ex, SpelMessage.EXCEPTION_DURING_PROPERTY_READ, name, ex.getMessage()); + } + + if (contextObject.getValue() == null) { + throw new SpelEvaluationException(SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE_ON_NULL, name); + } + else { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE, name, + FormatHelper.formatClassNameForMessage(getObjectClass(contextObject.getValue()))); + } + } + + private void writeProperty( + TypedValue contextObject, EvaluationContext evalContext, String name, @Nullable Object newValue) + throws EvaluationException { + + if (contextObject.getValue() == null && this.nullSafe) { + return; + } + if (contextObject.getValue() == null) { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE_ON_NULL, name); + } + + PropertyAccessor accessorToUse = this.cachedWriteAccessor; + if (accessorToUse != null) { + if (evalContext.getPropertyAccessors().contains(accessorToUse)) { + try { + accessorToUse.write(evalContext, contextObject.getValue(), name, newValue); + return; + } + catch (Exception ex) { + // This is OK - it may have gone stale due to a class change, + // let's try to get a new one and call it before giving up... + } + } + this.cachedWriteAccessor = null; + } + + List accessorsToTry = + getPropertyAccessorsToTry(contextObject.getValue(), evalContext.getPropertyAccessors()); + try { + for (PropertyAccessor accessor : accessorsToTry) { + if (accessor.canWrite(evalContext, contextObject.getValue(), name)) { + this.cachedWriteAccessor = accessor; + accessor.write(evalContext, contextObject.getValue(), name, newValue); + return; + } + } + } + catch (AccessException ex) { + throw new SpelEvaluationException(getStartPosition(), ex, SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE, + name, ex.getMessage()); + } + + throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE, name, + FormatHelper.formatClassNameForMessage(getObjectClass(contextObject.getValue()))); + } + + public boolean isWritableProperty(String name, TypedValue contextObject, EvaluationContext evalContext) + throws EvaluationException { + + Object value = contextObject.getValue(); + if (value != null) { + List accessorsToTry = + getPropertyAccessorsToTry(contextObject.getValue(), evalContext.getPropertyAccessors()); + for (PropertyAccessor accessor : accessorsToTry) { + try { + if (accessor.canWrite(evalContext, value, name)) { + return true; + } + } + catch (AccessException ex) { + // let others try + } + } + } + return false; + } + + /** + * Determines the set of property resolvers that should be used to try and access a property + * on the specified target type. The resolvers are considered to be in an ordered list, + * however in the returned list any that are exact matches for the input target type (as + * opposed to 'general' resolvers that could work for any type) are placed at the start of the + * list. In addition, there are specific resolvers that exactly name the class in question + * and resolvers that name a specific class but it is a supertype of the class we have. + * These are put at the end of the specific resolvers set and will be tried after exactly + * matching accessors but before generic accessors. + * @param contextObject the object upon which property access is being attempted + * @return a list of resolvers that should be tried in order to access the property + */ + private List getPropertyAccessorsToTry( + @Nullable Object contextObject, List propertyAccessors) { + + Class targetType = (contextObject != null ? contextObject.getClass() : null); + + List specificAccessors = new ArrayList<>(); + List generalAccessors = new ArrayList<>(); + for (PropertyAccessor resolver : propertyAccessors) { + Class[] targets = resolver.getSpecificTargetClasses(); + if (targets == null) { + // generic resolver that says it can be used for any type + generalAccessors.add(resolver); + } + else if (targetType != null) { + for (Class clazz : targets) { + if (clazz == targetType) { + specificAccessors.add(resolver); + break; + } + else if (clazz.isAssignableFrom(targetType)) { + generalAccessors.add(resolver); + } + } + } + } + List resolvers = new ArrayList<>(specificAccessors); + generalAccessors.removeAll(specificAccessors); + resolvers.addAll(generalAccessors); + return resolvers; + } + + @Override + public boolean isCompilable() { + PropertyAccessor accessorToUse = this.cachedReadAccessor; + return (accessorToUse instanceof CompilablePropertyAccessor && + ((CompilablePropertyAccessor) accessorToUse).isCompilable()); + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + PropertyAccessor accessorToUse = this.cachedReadAccessor; + if (!(accessorToUse instanceof CompilablePropertyAccessor)) { + throw new IllegalStateException("Property accessor is not compilable: " + accessorToUse); + } + + Label skipIfNull = null; + if (this.nullSafe) { + mv.visitInsn(DUP); + skipIfNull = new Label(); + Label continueLabel = new Label(); + mv.visitJumpInsn(IFNONNULL, continueLabel); + CodeFlow.insertCheckCast(mv, this.exitTypeDescriptor); + mv.visitJumpInsn(GOTO, skipIfNull); + mv.visitLabel(continueLabel); + } + + ((CompilablePropertyAccessor) accessorToUse).generateCode(this.name, mv, cf); + cf.pushDescriptor(this.exitTypeDescriptor); + + if (this.originalPrimitiveExitTypeDescriptor != null) { + // The output of the accessor is a primitive but from the block above it might be null, + // so to have a common stack element type at skipIfNull target it is necessary + // to box the primitive + CodeFlow.insertBoxIfNecessary(mv, this.originalPrimitiveExitTypeDescriptor); + } + if (skipIfNull != null) { + mv.visitLabel(skipIfNull); + } + } + + void setExitTypeDescriptor(String descriptor) { + // If this property or field access would return a primitive - and yet + // it is also marked null safe - then the exit type descriptor must be + // promoted to the box type to allow a null value to be passed on + if (this.nullSafe && CodeFlow.isPrimitive(descriptor)) { + this.originalPrimitiveExitTypeDescriptor = descriptor; + this.exitTypeDescriptor = CodeFlow.toBoxedDescriptor(descriptor); + } + else { + this.exitTypeDescriptor = descriptor; + } + } + + + private static class AccessorLValue implements ValueRef { + + private final PropertyOrFieldReference ref; + + private final TypedValue contextObject; + + private final EvaluationContext evalContext; + + private final boolean autoGrowNullReferences; + + public AccessorLValue(PropertyOrFieldReference propertyOrFieldReference, TypedValue activeContextObject, + EvaluationContext evalContext, boolean autoGrowNullReferences) { + + this.ref = propertyOrFieldReference; + this.contextObject = activeContextObject; + this.evalContext = evalContext; + this.autoGrowNullReferences = autoGrowNullReferences; + } + + @Override + public TypedValue getValue() { + TypedValue value = + this.ref.getValueInternal(this.contextObject, this.evalContext, this.autoGrowNullReferences); + PropertyAccessor accessorToUse = this.ref.cachedReadAccessor; + if (accessorToUse instanceof CompilablePropertyAccessor) { + this.ref.setExitTypeDescriptor(CodeFlow.toDescriptor(((CompilablePropertyAccessor) accessorToUse).getPropertyType())); + } + return value; + } + + @Override + public void setValue(@Nullable Object newValue) { + this.ref.writeProperty(this.contextObject, this.evalContext, this.ref.name, newValue); + } + + @Override + public boolean isWritable() { + return this.ref.isWritableProperty(this.ref.name, this.contextObject, this.evalContext); + } + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/QualifiedIdentifier.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/QualifiedIdentifier.java new file mode 100644 index 0000000..bbf1d2d --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/QualifiedIdentifier.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import org.springframework.expression.EvaluationException; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.lang.Nullable; + +/** + * Represents a dot separated sequence of strings that indicate a package qualified type + * reference. + * + *

    Example: "java.lang.String" as in the expression "new java.lang.String('hello')" + * + * @author Andy Clement + * @since 3.0 + */ +public class QualifiedIdentifier extends SpelNodeImpl { + + @Nullable + private TypedValue value; + + + public QualifiedIdentifier(int startPos, int endPos, SpelNodeImpl... operands) { + super(startPos, endPos, operands); + } + + + @Override + public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + // Cache the concatenation of child identifiers + if (this.value == null) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < getChildCount(); i++) { + Object value = this.children[i].getValueInternal(state).getValue(); + if (i > 0 && (value == null || !value.toString().startsWith("$"))) { + sb.append("."); + } + sb.append(value); + } + this.value = new TypedValue(sb.toString()); + } + return this.value; + } + + @Override + public String toStringAST() { + StringBuilder sb = new StringBuilder(); + if (this.value != null) { + sb.append(this.value.getValue()); + } + else { + for (int i = 0; i < getChildCount(); i++) { + if (i > 0) { + sb.append("."); + } + sb.append(getChild(i).toStringAST()); + } + } + return sb.toString(); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/RealLiteral.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/RealLiteral.java new file mode 100644 index 0000000..1a6e970 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/RealLiteral.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.CodeFlow; + +/** + * Expression language AST node that represents a real literal. + * + * @author Andy Clement + * @since 3.0 + */ +public class RealLiteral extends Literal { + + private final TypedValue value; + + + public RealLiteral(String payload, int startPos, int endPos, double value) { + super(payload, startPos, endPos); + this.value = new TypedValue(value); + this.exitTypeDescriptor = "D"; + } + + + @Override + public TypedValue getLiteralValue() { + return this.value; + } + + @Override + public boolean isCompilable() { + return true; + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + mv.visitLdcInsn(this.value.getValue()); + cf.pushDescriptor(this.exitTypeDescriptor); + } + + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java new file mode 100644 index 0000000..d2034ef --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java @@ -0,0 +1,220 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; + +/** + * Represents selection over a map or collection. + * For example: {1,2,3,4,5,6,7,8,9,10}.?{#isEven(#this) == 'y'} returns [2, 4, 6, 8, 10] + * + *

    Basically a subset of the input data is returned based on the + * evaluation of the expression supplied as selection criteria. + * + * @author Andy Clement + * @author Mark Fisher + * @author Sam Brannen + * @author Juergen Hoeller + * @since 3.0 + */ +public class Selection extends SpelNodeImpl { + + /** + * All items ({@code ?[]}). + */ + public static final int ALL = 0; + + /** + * The first item ({@code ^[]}). + */ + public static final int FIRST = 1; + + /** + * The last item ({@code $[]}). + */ + public static final int LAST = 2; + + private final int variant; + + private final boolean nullSafe; + + + public Selection(boolean nullSafe, int variant, int startPos, int endPos, SpelNodeImpl expression) { + super(startPos, endPos, expression); + this.nullSafe = nullSafe; + this.variant = variant; + } + + + @Override + public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + return getValueRef(state).getValue(); + } + + @Override + protected ValueRef getValueRef(ExpressionState state) throws EvaluationException { + TypedValue op = state.getActiveContextObject(); + Object operand = op.getValue(); + SpelNodeImpl selectionCriteria = this.children[0]; + + if (operand instanceof Map) { + Map mapdata = (Map) operand; + // TODO don't lose generic info for the new map + Map result = new HashMap<>(); + Object lastKey = null; + + for (Map.Entry entry : mapdata.entrySet()) { + try { + TypedValue kvPair = new TypedValue(entry); + state.pushActiveContextObject(kvPair); + state.enterScope(); + Object val = selectionCriteria.getValueInternal(state).getValue(); + if (val instanceof Boolean) { + if ((Boolean) val) { + if (this.variant == FIRST) { + result.put(entry.getKey(), entry.getValue()); + return new ValueRef.TypedValueHolderValueRef(new TypedValue(result), this); + } + result.put(entry.getKey(), entry.getValue()); + lastKey = entry.getKey(); + } + } + else { + throw new SpelEvaluationException(selectionCriteria.getStartPosition(), + SpelMessage.RESULT_OF_SELECTION_CRITERIA_IS_NOT_BOOLEAN); + } + } + finally { + state.popActiveContextObject(); + state.exitScope(); + } + } + + if ((this.variant == FIRST || this.variant == LAST) && result.isEmpty()) { + return new ValueRef.TypedValueHolderValueRef(new TypedValue(null), this); + } + + if (this.variant == LAST) { + Map resultMap = new HashMap<>(); + Object lastValue = result.get(lastKey); + resultMap.put(lastKey,lastValue); + return new ValueRef.TypedValueHolderValueRef(new TypedValue(resultMap),this); + } + + return new ValueRef.TypedValueHolderValueRef(new TypedValue(result),this); + } + + if (operand instanceof Iterable || ObjectUtils.isArray(operand)) { + Iterable data = (operand instanceof Iterable ? + (Iterable) operand : Arrays.asList(ObjectUtils.toObjectArray(operand))); + + List result = new ArrayList<>(); + int index = 0; + for (Object element : data) { + try { + state.pushActiveContextObject(new TypedValue(element)); + state.enterScope("index", index); + Object val = selectionCriteria.getValueInternal(state).getValue(); + if (val instanceof Boolean) { + if ((Boolean) val) { + if (this.variant == FIRST) { + return new ValueRef.TypedValueHolderValueRef(new TypedValue(element), this); + } + result.add(element); + } + } + else { + throw new SpelEvaluationException(selectionCriteria.getStartPosition(), + SpelMessage.RESULT_OF_SELECTION_CRITERIA_IS_NOT_BOOLEAN); + } + index++; + } + finally { + state.exitScope(); + state.popActiveContextObject(); + } + } + + if ((this.variant == FIRST || this.variant == LAST) && result.isEmpty()) { + return ValueRef.NullValueRef.INSTANCE; + } + + if (this.variant == LAST) { + return new ValueRef.TypedValueHolderValueRef(new TypedValue(CollectionUtils.lastElement(result)), this); + } + + if (operand instanceof Iterable) { + return new ValueRef.TypedValueHolderValueRef(new TypedValue(result), this); + } + + Class elementType = null; + TypeDescriptor typeDesc = op.getTypeDescriptor(); + if (typeDesc != null) { + TypeDescriptor elementTypeDesc = typeDesc.getElementTypeDescriptor(); + if (elementTypeDesc != null) { + elementType = ClassUtils.resolvePrimitiveIfNecessary(elementTypeDesc.getType()); + } + } + Assert.state(elementType != null, "Unresolvable element type"); + + Object resultArray = Array.newInstance(elementType, result.size()); + System.arraycopy(result.toArray(), 0, resultArray, 0, result.size()); + return new ValueRef.TypedValueHolderValueRef(new TypedValue(resultArray), this); + } + + if (operand == null) { + if (this.nullSafe) { + return ValueRef.NullValueRef.INSTANCE; + } + throw new SpelEvaluationException(getStartPosition(), SpelMessage.INVALID_TYPE_FOR_SELECTION, "null"); + } + + throw new SpelEvaluationException(getStartPosition(), SpelMessage.INVALID_TYPE_FOR_SELECTION, + operand.getClass().getName()); + } + + @Override + public String toStringAST() { + return prefix() + getChild(0).toStringAST() + "]"; + } + + private String prefix() { + switch (this.variant) { + case ALL: return "?["; + case FIRST: return "^["; + case LAST: return "$["; + } + return ""; + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java new file mode 100644 index 0000000..3976fbf --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java @@ -0,0 +1,285 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Member; +import java.lang.reflect.Method; + +import org.springframework.asm.MethodVisitor; +import org.springframework.asm.Opcodes; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.TypedValue; +import org.springframework.expression.common.ExpressionUtils; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.SpelNode; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * The common supertype of all AST nodes in a parsed Spring Expression Language + * format expression. + * + * @author Andy Clement + * @author Juergen Hoeller + * @since 3.0 + */ +public abstract class SpelNodeImpl implements SpelNode, Opcodes { + + private static final SpelNodeImpl[] NO_CHILDREN = new SpelNodeImpl[0]; + + + private final int startPos; + + private final int endPos; + + protected SpelNodeImpl[] children = SpelNodeImpl.NO_CHILDREN; + + @Nullable + private SpelNodeImpl parent; + + /** + * Indicates the type descriptor for the result of this expression node. + * This is set as soon as it is known. For a literal node it is known immediately. + * For a property access or method invocation it is known after one evaluation of + * that node. + *

    The descriptor is like the bytecode form but is slightly easier to work with. + * It does not include the trailing semicolon (for non array reference types). + * Some examples: Ljava/lang/String, I, [I + */ + @Nullable + protected volatile String exitTypeDescriptor; + + + public SpelNodeImpl(int startPos, int endPos, SpelNodeImpl... operands) { + this.startPos = startPos; + this.endPos = endPos; + if (!ObjectUtils.isEmpty(operands)) { + this.children = operands; + for (SpelNodeImpl operand : operands) { + Assert.notNull(operand, "Operand must not be null"); + operand.parent = this; + } + } + } + + + /** + * Return {@code true} if the next child is one of the specified classes. + */ + protected boolean nextChildIs(Class... classes) { + if (this.parent != null) { + SpelNodeImpl[] peers = this.parent.children; + for (int i = 0, max = peers.length; i < max; i++) { + if (this == peers[i]) { + if (i + 1 >= max) { + return false; + } + Class peerClass = peers[i + 1].getClass(); + for (Class desiredClass : classes) { + if (peerClass == desiredClass) { + return true; + } + } + return false; + } + } + } + return false; + } + + @Override + @Nullable + public final Object getValue(ExpressionState expressionState) throws EvaluationException { + return getValueInternal(expressionState).getValue(); + } + + @Override + public final TypedValue getTypedValue(ExpressionState expressionState) throws EvaluationException { + return getValueInternal(expressionState); + } + + // by default Ast nodes are not writable + @Override + public boolean isWritable(ExpressionState expressionState) throws EvaluationException { + return false; + } + + @Override + public void setValue(ExpressionState expressionState, @Nullable Object newValue) throws EvaluationException { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.SETVALUE_NOT_SUPPORTED, getClass()); + } + + @Override + public SpelNode getChild(int index) { + return this.children[index]; + } + + @Override + public int getChildCount() { + return this.children.length; + } + + @Override + @Nullable + public Class getObjectClass(@Nullable Object obj) { + if (obj == null) { + return null; + } + return (obj instanceof Class ? ((Class) obj) : obj.getClass()); + } + + @Override + public int getStartPosition() { + return this.startPos; + } + + @Override + public int getEndPosition() { + return this.endPos; + } + + /** + * Check whether a node can be compiled to bytecode. The reasoning in each node may + * be different but will typically involve checking whether the exit type descriptor + * of the node is known and any relevant child nodes are compilable. + * @return {@code true} if this node can be compiled to bytecode + */ + public boolean isCompilable() { + return false; + } + + /** + * Generate the bytecode for this node into the supplied visitor. Context info about + * the current expression being compiled is available in the codeflow object, e.g. + * including information about the type of the object currently on the stack. + * @param mv the ASM MethodVisitor into which code should be generated + * @param cf a context object with info about what is on the stack + */ + public void generateCode(MethodVisitor mv, CodeFlow cf) { + throw new IllegalStateException(getClass().getName() +" has no generateCode(..) method"); + } + + @Nullable + public String getExitDescriptor() { + return this.exitTypeDescriptor; + } + + @Nullable + protected final T getValue(ExpressionState state, Class desiredReturnType) throws EvaluationException { + return ExpressionUtils.convertTypedValue(state.getEvaluationContext(), getValueInternal(state), desiredReturnType); + } + + protected ValueRef getValueRef(ExpressionState state) throws EvaluationException { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.NOT_ASSIGNABLE, toStringAST()); + } + + public abstract TypedValue getValueInternal(ExpressionState expressionState) throws EvaluationException; + + + /** + * Generate code that handles building the argument values for the specified method. + * This method will take account of whether the invoked method is a varargs method + * and if it is then the argument values will be appropriately packaged into an array. + * @param mv the method visitor where code should be generated + * @param cf the current codeflow + * @param member the method or constructor for which arguments are being setup + * @param arguments the expression nodes for the expression supplied argument values + */ + protected static void generateCodeForArguments(MethodVisitor mv, CodeFlow cf, Member member, SpelNodeImpl[] arguments) { + String[] paramDescriptors = null; + boolean isVarargs = false; + if (member instanceof Constructor) { + Constructor ctor = (Constructor) member; + paramDescriptors = CodeFlow.toDescriptors(ctor.getParameterTypes()); + isVarargs = ctor.isVarArgs(); + } + else { // Method + Method method = (Method)member; + paramDescriptors = CodeFlow.toDescriptors(method.getParameterTypes()); + isVarargs = method.isVarArgs(); + } + if (isVarargs) { + // The final parameter may or may not need packaging into an array, or nothing may + // have been passed to satisfy the varargs and so something needs to be built. + int p = 0; // Current supplied argument being processed + int childCount = arguments.length; + + // Fulfill all the parameter requirements except the last one + for (p = 0; p < paramDescriptors.length - 1; p++) { + generateCodeForArgument(mv, cf, arguments[p], paramDescriptors[p]); + } + + SpelNodeImpl lastChild = (childCount == 0 ? null : arguments[childCount - 1]); + String arrayType = paramDescriptors[paramDescriptors.length - 1]; + // Determine if the final passed argument is already suitably packaged in array + // form to be passed to the method + if (lastChild != null && arrayType.equals(lastChild.getExitDescriptor())) { + generateCodeForArgument(mv, cf, lastChild, paramDescriptors[p]); + } + else { + arrayType = arrayType.substring(1); // trim the leading '[', may leave other '[' + // build array big enough to hold remaining arguments + CodeFlow.insertNewArrayCode(mv, childCount - p, arrayType); + // Package up the remaining arguments into the array + int arrayindex = 0; + while (p < childCount) { + SpelNodeImpl child = arguments[p]; + mv.visitInsn(DUP); + CodeFlow.insertOptimalLoad(mv, arrayindex++); + generateCodeForArgument(mv, cf, child, arrayType); + CodeFlow.insertArrayStore(mv, arrayType); + p++; + } + } + } + else { + for (int i = 0; i < paramDescriptors.length;i++) { + generateCodeForArgument(mv, cf, arguments[i], paramDescriptors[i]); + } + } + } + + /** + * Ask an argument to generate its bytecode and then follow it up + * with any boxing/unboxing/checkcasting to ensure it matches the expected parameter descriptor. + */ + protected static void generateCodeForArgument(MethodVisitor mv, CodeFlow cf, SpelNodeImpl argument, String paramDesc) { + cf.enterCompilationScope(); + argument.generateCode(mv, cf); + String lastDesc = cf.lastDescriptor(); + Assert.state(lastDesc != null, "No last descriptor"); + boolean primitiveOnStack = CodeFlow.isPrimitive(lastDesc); + // Check if need to box it for the method reference? + if (primitiveOnStack && paramDesc.charAt(0) == 'L') { + CodeFlow.insertBoxIfNecessary(mv, lastDesc.charAt(0)); + } + else if (paramDesc.length() == 1 && !primitiveOnStack) { + CodeFlow.insertUnboxInsns(mv, paramDesc.charAt(0), lastDesc); + } + else if (!paramDesc.equals(lastDesc)) { + // This would be unnecessary in the case of subtyping (e.g. method takes Number but Integer passed in) + CodeFlow.insertCheckCast(mv, paramDesc); + } + cf.exitCompilationScope(); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/StringLiteral.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/StringLiteral.java new file mode 100644 index 0000000..3fc939c --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/StringLiteral.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.util.StringUtils; + +/** + * Expression language AST node that represents a string literal. + * + * @author Andy Clement + * @author Juergen Hoeller + * @since 3.0 + */ +public class StringLiteral extends Literal { + + private final TypedValue value; + + + public StringLiteral(String payload, int startPos, int endPos, String value) { + super(payload, startPos, endPos); + + String valueWithinQuotes = value.substring(1, value.length() - 1); + valueWithinQuotes = StringUtils.replace(valueWithinQuotes, "''", "'"); + valueWithinQuotes = StringUtils.replace(valueWithinQuotes, "\"\"", "\""); + + this.value = new TypedValue(valueWithinQuotes); + this.exitTypeDescriptor = "Ljava/lang/String"; + } + + + @Override + public TypedValue getLiteralValue() { + return this.value; + } + + @Override + public String toString() { + return "'" + getLiteralValue().getValue() + "'"; + } + + @Override + public boolean isCompilable() { + return true; + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + mv.visitLdcInsn(this.value.getValue()); + cf.pushDescriptor(this.exitTypeDescriptor); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Ternary.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Ternary.java new file mode 100644 index 0000000..efb9b36 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Ternary.java @@ -0,0 +1,130 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import org.springframework.asm.Label; +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Represents a ternary expression, for example: "someCheck()?true:false". + * + * @author Andy Clement + * @author Juergen Hoeller + * @since 3.0 + */ +public class Ternary extends SpelNodeImpl { + + public Ternary(int startPos, int endPos, SpelNodeImpl... args) { + super(startPos, endPos, args); + } + + + /** + * Evaluate the condition and if true evaluate the first alternative, otherwise + * evaluate the second alternative. + * @param state the expression state + * @throws EvaluationException if the condition does not evaluate correctly to + * a boolean or there is a problem executing the chosen alternative + */ + @Override + public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + Boolean value = this.children[0].getValue(state, Boolean.class); + if (value == null) { + throw new SpelEvaluationException(getChild(0).getStartPosition(), + SpelMessage.TYPE_CONVERSION_ERROR, "null", "boolean"); + } + TypedValue result = this.children[value ? 1 : 2].getValueInternal(state); + computeExitTypeDescriptor(); + return result; + } + + @Override + public String toStringAST() { + return getChild(0).toStringAST() + " ? " + getChild(1).toStringAST() + " : " + getChild(2).toStringAST(); + } + + private void computeExitTypeDescriptor() { + if (this.exitTypeDescriptor == null && this.children[1].exitTypeDescriptor != null && + this.children[2].exitTypeDescriptor != null) { + String leftDescriptor = this.children[1].exitTypeDescriptor; + String rightDescriptor = this.children[2].exitTypeDescriptor; + if (ObjectUtils.nullSafeEquals(leftDescriptor, rightDescriptor)) { + this.exitTypeDescriptor = leftDescriptor; + } + else { + // Use the easiest to compute common super type + this.exitTypeDescriptor = "Ljava/lang/Object"; + } + } + } + + @Override + public boolean isCompilable() { + SpelNodeImpl condition = this.children[0]; + SpelNodeImpl left = this.children[1]; + SpelNodeImpl right = this.children[2]; + return (condition.isCompilable() && left.isCompilable() && right.isCompilable() && + CodeFlow.isBooleanCompatible(condition.exitTypeDescriptor) && + left.exitTypeDescriptor != null && right.exitTypeDescriptor != null); + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + // May reach here without it computed if all elements are literals + computeExitTypeDescriptor(); + cf.enterCompilationScope(); + this.children[0].generateCode(mv, cf); + String lastDesc = cf.lastDescriptor(); + Assert.state(lastDesc != null, "No last descriptor"); + if (!CodeFlow.isPrimitive(lastDesc)) { + CodeFlow.insertUnboxInsns(mv, 'Z', lastDesc); + } + cf.exitCompilationScope(); + Label elseTarget = new Label(); + Label endOfIf = new Label(); + mv.visitJumpInsn(IFEQ, elseTarget); + cf.enterCompilationScope(); + this.children[1].generateCode(mv, cf); + if (!CodeFlow.isPrimitive(this.exitTypeDescriptor)) { + lastDesc = cf.lastDescriptor(); + Assert.state(lastDesc != null, "No last descriptor"); + CodeFlow.insertBoxIfNecessary(mv, lastDesc.charAt(0)); + } + cf.exitCompilationScope(); + mv.visitJumpInsn(GOTO, endOfIf); + mv.visitLabel(elseTarget); + cf.enterCompilationScope(); + this.children[2].generateCode(mv, cf); + if (!CodeFlow.isPrimitive(this.exitTypeDescriptor)) { + lastDesc = cf.lastDescriptor(); + Assert.state(lastDesc != null, "No last descriptor"); + CodeFlow.insertBoxIfNecessary(mv, lastDesc.charAt(0)); + } + cf.exitCompilationScope(); + mv.visitLabel(endOfIf); + cf.pushDescriptor(this.exitTypeDescriptor); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/TypeCode.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/TypeCode.java new file mode 100644 index 0000000..aabc43d --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/TypeCode.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +/** + * Captures primitive types and their corresponding class objects, plus one special entry + * that represents all reference (non-primitive) types. + * + * @author Andy Clement + */ +public enum TypeCode { + + /** + * An {@link Object}. + */ + OBJECT(Object.class), + + /** + * A {@code boolean}. + */ + BOOLEAN(Boolean.TYPE), + + /** + * A {@code byte}. + */ + BYTE(Byte.TYPE), + + /** + * A {@code char}. + */ + CHAR(Character.TYPE), + + /** + * A {@code double}. + */ + DOUBLE(Double.TYPE), + + /** + * A {@code float}. + */ + FLOAT(Float.TYPE), + + /** + * An {@code int}. + */ + INT(Integer.TYPE), + + /** + * A {@code long}. + */ + LONG(Long.TYPE), + + /** + * An {@link Object}. + */ + SHORT(Short.TYPE); + + + private Class type; + + + TypeCode(Class type) { + this.type = type; + } + + + public Class getType() { + return this.type; + } + + + public static TypeCode forName(String name) { + TypeCode[] tcs = values(); + for (int i = 1; i < tcs.length; i++) { + if (tcs[i].name().equalsIgnoreCase(name)) { + return tcs[i]; + } + } + return OBJECT; + } + + public static TypeCode forClass(Class clazz) { + TypeCode[] allValues = TypeCode.values(); + for (TypeCode typeCode : allValues) { + if (clazz == typeCode.getType()) { + return typeCode; + } + } + return OBJECT; + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/TypeReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/TypeReference.java new file mode 100644 index 0000000..8eac5d6 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/TypeReference.java @@ -0,0 +1,138 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.lang.reflect.Array; + +import org.springframework.asm.MethodVisitor; +import org.springframework.asm.Type; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Represents a reference to a type, for example + * {@code "T(String)" or "T(com.somewhere.Foo)"}. + * + * @author Andy Clement + */ +public class TypeReference extends SpelNodeImpl { + + private final int dimensions; + + @Nullable + private transient Class type; + + + public TypeReference(int startPos, int endPos, SpelNodeImpl qualifiedId) { + this(startPos, endPos, qualifiedId, 0); + } + + public TypeReference(int startPos, int endPos, SpelNodeImpl qualifiedId, int dims) { + super(startPos, endPos, qualifiedId); + this.dimensions = dims; + } + + + @Override + public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + // TODO possible optimization here if we cache the discovered type reference, but can we do that? + String typeName = (String) this.children[0].getValueInternal(state).getValue(); + Assert.state(typeName != null, "No type name"); + if (!typeName.contains(".") && Character.isLowerCase(typeName.charAt(0))) { + TypeCode tc = TypeCode.valueOf(typeName.toUpperCase()); + if (tc != TypeCode.OBJECT) { + // It is a primitive type + Class clazz = makeArrayIfNecessary(tc.getType()); + this.exitTypeDescriptor = "Ljava/lang/Class"; + this.type = clazz; + return new TypedValue(clazz); + } + } + Class clazz = state.findType(typeName); + clazz = makeArrayIfNecessary(clazz); + this.exitTypeDescriptor = "Ljava/lang/Class"; + this.type = clazz; + return new TypedValue(clazz); + } + + private Class makeArrayIfNecessary(Class clazz) { + if (this.dimensions != 0) { + for (int i = 0; i < this.dimensions; i++) { + Object array = Array.newInstance(clazz, 0); + clazz = array.getClass(); + } + } + return clazz; + } + + @Override + public String toStringAST() { + StringBuilder sb = new StringBuilder("T("); + sb.append(getChild(0).toStringAST()); + for (int d = 0; d < this.dimensions; d++) { + sb.append("[]"); + } + sb.append(")"); + return sb.toString(); + } + + @Override + public boolean isCompilable() { + return (this.exitTypeDescriptor != null); + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + // TODO Future optimization - if followed by a static method call, skip generating code here + Assert.state(this.type != null, "No type available"); + if (this.type.isPrimitive()) { + if (this.type == Boolean.TYPE) { + mv.visitFieldInsn(GETSTATIC, "java/lang/Boolean", "TYPE", "Ljava/lang/Class;"); + } + else if (this.type == Byte.TYPE) { + mv.visitFieldInsn(GETSTATIC, "java/lang/Byte", "TYPE", "Ljava/lang/Class;"); + } + else if (this.type == Character.TYPE) { + mv.visitFieldInsn(GETSTATIC, "java/lang/Character", "TYPE", "Ljava/lang/Class;"); + } + else if (this.type == Double.TYPE) { + mv.visitFieldInsn(GETSTATIC, "java/lang/Double", "TYPE", "Ljava/lang/Class;"); + } + else if (this.type == Float.TYPE) { + mv.visitFieldInsn(GETSTATIC, "java/lang/Float", "TYPE", "Ljava/lang/Class;"); + } + else if (this.type == Integer.TYPE) { + mv.visitFieldInsn(GETSTATIC, "java/lang/Integer", "TYPE", "Ljava/lang/Class;"); + } + else if (this.type == Long.TYPE) { + mv.visitFieldInsn(GETSTATIC, "java/lang/Long", "TYPE", "Ljava/lang/Class;"); + } + else if (this.type == Short.TYPE) { + mv.visitFieldInsn(GETSTATIC, "java/lang/Short", "TYPE", "Ljava/lang/Class;"); + } + } + else { + mv.visitLdcInsn(Type.getType(this.type)); + } + cf.pushDescriptor(this.exitTypeDescriptor); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/ValueRef.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/ValueRef.java new file mode 100644 index 0000000..dd199cc --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/ValueRef.java @@ -0,0 +1,116 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.lang.Nullable; + +/** + * Represents a reference to a value. With a reference it is possible to get or set the + * value. Passing around value references rather than the values themselves can avoid + * incorrect duplication of operand evaluation. For example in 'list[index++]++' without + * a value reference for 'list[index++]' it would be necessary to evaluate list[index++] + * twice (once to get the value, once to determine where the value goes) and that would + * double increment index. + * + * @author Andy Clement + * @since 3.2 + */ +public interface ValueRef { + + /** + * Returns the value this ValueRef points to, it should not require expression + * component re-evaluation. + * @return the value + */ + TypedValue getValue(); + + /** + * Sets the value this ValueRef points to, it should not require expression component + * re-evaluation. + * @param newValue the new value + */ + void setValue(@Nullable Object newValue); + + /** + * Indicates whether calling setValue(Object) is supported. + * @return true if setValue() is supported for this value reference. + */ + boolean isWritable(); + + + /** + * A ValueRef for the null value. + */ + class NullValueRef implements ValueRef { + + static final NullValueRef INSTANCE = new NullValueRef(); + + @Override + public TypedValue getValue() { + return TypedValue.NULL; + } + + @Override + public void setValue(@Nullable Object newValue) { + // The exception position '0' isn't right but the overhead of creating + // instances of this per node (where the node is solely for error reporting) + // would be unfortunate. + throw new SpelEvaluationException(0, SpelMessage.NOT_ASSIGNABLE, "null"); + } + + @Override + public boolean isWritable() { + return false; + } + } + + + /** + * A ValueRef holder for a single value, which cannot be set. + */ + class TypedValueHolderValueRef implements ValueRef { + + private final TypedValue typedValue; + + private final SpelNodeImpl node; // used only for error reporting + + public TypedValueHolderValueRef(TypedValue typedValue, SpelNodeImpl node) { + this.typedValue = typedValue; + this.node = node; + } + + @Override + public TypedValue getValue() { + return this.typedValue; + } + + @Override + public void setValue(@Nullable Object newValue) { + throw new SpelEvaluationException( + this.node.getStartPosition(), SpelMessage.NOT_ASSIGNABLE, this.node.toStringAST()); + } + + @Override + public boolean isWritable() { + return false; + } + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/VariableReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/VariableReference.java new file mode 100644 index 0000000..769e4ef --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/VariableReference.java @@ -0,0 +1,157 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.lang.reflect.Modifier; + +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.lang.Nullable; + +/** + * Represents a variable reference, eg. #someVar. Note this is different to a *local* + * variable like $someVar + * + * @author Andy Clement + * @since 3.0 + */ +public class VariableReference extends SpelNodeImpl { + + // Well known variables: + private static final String THIS = "this"; // currently active context object + + private static final String ROOT = "root"; // root context object + + + private final String name; + + + public VariableReference(String variableName, int startPos, int endPos) { + super(startPos, endPos); + this.name = variableName; + } + + + @Override + public ValueRef getValueRef(ExpressionState state) throws SpelEvaluationException { + if (this.name.equals(THIS)) { + return new ValueRef.TypedValueHolderValueRef(state.getActiveContextObject(),this); + } + if (this.name.equals(ROOT)) { + return new ValueRef.TypedValueHolderValueRef(state.getRootContextObject(),this); + } + TypedValue result = state.lookupVariable(this.name); + // a null value will mean either the value was null or the variable was not found + return new VariableRef(this.name,result,state.getEvaluationContext()); + } + + @Override + public TypedValue getValueInternal(ExpressionState state) throws SpelEvaluationException { + if (this.name.equals(THIS)) { + return state.getActiveContextObject(); + } + if (this.name.equals(ROOT)) { + TypedValue result = state.getRootContextObject(); + this.exitTypeDescriptor = CodeFlow.toDescriptorFromObject(result.getValue()); + return result; + } + TypedValue result = state.lookupVariable(this.name); + Object value = result.getValue(); + if (value == null || !Modifier.isPublic(value.getClass().getModifiers())) { + // If the type is not public then when generateCode produces a checkcast to it + // then an IllegalAccessError will occur. + // If resorting to Object isn't sufficient, the hierarchy could be traversed for + // the first public type. + this.exitTypeDescriptor = "Ljava/lang/Object"; + } + else { + this.exitTypeDescriptor = CodeFlow.toDescriptorFromObject(value); + } + // a null value will mean either the value was null or the variable was not found + return result; + } + + @Override + public void setValue(ExpressionState state, @Nullable Object value) throws SpelEvaluationException { + state.setVariable(this.name, value); + } + + @Override + public String toStringAST() { + return "#" + this.name; + } + + @Override + public boolean isWritable(ExpressionState expressionState) throws SpelEvaluationException { + return !(this.name.equals(THIS) || this.name.equals(ROOT)); + } + + @Override + public boolean isCompilable() { + return (this.exitTypeDescriptor != null); + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow cf) { + if (this.name.equals(ROOT)) { + mv.visitVarInsn(ALOAD,1); + } + else { + mv.visitVarInsn(ALOAD, 2); + mv.visitLdcInsn(this.name); + mv.visitMethodInsn(INVOKEINTERFACE, "org/springframework/expression/EvaluationContext", "lookupVariable", "(Ljava/lang/String;)Ljava/lang/Object;",true); + } + CodeFlow.insertCheckCast(mv, this.exitTypeDescriptor); + cf.pushDescriptor(this.exitTypeDescriptor); + } + + + private static class VariableRef implements ValueRef { + + private final String name; + + private final TypedValue value; + + private final EvaluationContext evaluationContext; + + public VariableRef(String name, TypedValue value, EvaluationContext evaluationContext) { + this.name = name; + this.value = value; + this.evaluationContext = evaluationContext; + } + + @Override + public TypedValue getValue() { + return this.value; + } + + @Override + public void setValue(@Nullable Object newValue) { + this.evaluationContext.setVariable(this.name, newValue); + } + + @Override + public boolean isWritable() { + return true; + } + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/package-info.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/package-info.java new file mode 100644 index 0000000..d6c0b5e --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/package-info.java @@ -0,0 +1,9 @@ +/** + * SpEL's abstract syntax tree. + */ +@NonNullApi +@NonNullFields +package org.springframework.expression.spel.ast; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/package-info.java b/spring-expression/src/main/java/org/springframework/expression/spel/package-info.java new file mode 100644 index 0000000..81dc5f5 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/package-info.java @@ -0,0 +1,9 @@ +/** + * SpEL's central implementation package. + */ +@NonNullApi +@NonNullFields +package org.springframework.expression.spel; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java new file mode 100644 index 0000000..5b3f69f --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java @@ -0,0 +1,1044 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.standard; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.List; +import java.util.regex.Pattern; + +import org.springframework.expression.ParseException; +import org.springframework.expression.ParserContext; +import org.springframework.expression.common.TemplateAwareExpressionParser; +import org.springframework.expression.spel.InternalParseException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.SpelParseException; +import org.springframework.expression.spel.SpelParserConfiguration; +import org.springframework.expression.spel.ast.Assign; +import org.springframework.expression.spel.ast.BeanReference; +import org.springframework.expression.spel.ast.BooleanLiteral; +import org.springframework.expression.spel.ast.CompoundExpression; +import org.springframework.expression.spel.ast.ConstructorReference; +import org.springframework.expression.spel.ast.Elvis; +import org.springframework.expression.spel.ast.FunctionReference; +import org.springframework.expression.spel.ast.Identifier; +import org.springframework.expression.spel.ast.Indexer; +import org.springframework.expression.spel.ast.InlineList; +import org.springframework.expression.spel.ast.InlineMap; +import org.springframework.expression.spel.ast.Literal; +import org.springframework.expression.spel.ast.MethodReference; +import org.springframework.expression.spel.ast.NullLiteral; +import org.springframework.expression.spel.ast.OpAnd; +import org.springframework.expression.spel.ast.OpDec; +import org.springframework.expression.spel.ast.OpDivide; +import org.springframework.expression.spel.ast.OpEQ; +import org.springframework.expression.spel.ast.OpGE; +import org.springframework.expression.spel.ast.OpGT; +import org.springframework.expression.spel.ast.OpInc; +import org.springframework.expression.spel.ast.OpLE; +import org.springframework.expression.spel.ast.OpLT; +import org.springframework.expression.spel.ast.OpMinus; +import org.springframework.expression.spel.ast.OpModulus; +import org.springframework.expression.spel.ast.OpMultiply; +import org.springframework.expression.spel.ast.OpNE; +import org.springframework.expression.spel.ast.OpOr; +import org.springframework.expression.spel.ast.OpPlus; +import org.springframework.expression.spel.ast.OperatorBetween; +import org.springframework.expression.spel.ast.OperatorInstanceof; +import org.springframework.expression.spel.ast.OperatorMatches; +import org.springframework.expression.spel.ast.OperatorNot; +import org.springframework.expression.spel.ast.OperatorPower; +import org.springframework.expression.spel.ast.Projection; +import org.springframework.expression.spel.ast.PropertyOrFieldReference; +import org.springframework.expression.spel.ast.QualifiedIdentifier; +import org.springframework.expression.spel.ast.Selection; +import org.springframework.expression.spel.ast.SpelNodeImpl; +import org.springframework.expression.spel.ast.StringLiteral; +import org.springframework.expression.spel.ast.Ternary; +import org.springframework.expression.spel.ast.TypeReference; +import org.springframework.expression.spel.ast.VariableReference; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Hand-written SpEL parser. Instances are reusable but are not thread-safe. + * + * @author Andy Clement + * @author Juergen Hoeller + * @author Phillip Webb + * @since 3.0 + */ +class InternalSpelExpressionParser extends TemplateAwareExpressionParser { + + private static final Pattern VALID_QUALIFIED_ID_PATTERN = Pattern.compile("[\\p{L}\\p{N}_$]+"); + + + private final SpelParserConfiguration configuration; + + // For rules that build nodes, they are stacked here for return + private final Deque constructedNodes = new ArrayDeque<>(); + + // The expression being parsed + private String expressionString = ""; + + // The token stream constructed from that expression string + private List tokenStream = Collections.emptyList(); + + // length of a populated token stream + private int tokenStreamLength; + + // Current location in the token stream when processing tokens + private int tokenStreamPointer; + + + /** + * Create a parser with some configured behavior. + * @param configuration custom configuration options + */ + public InternalSpelExpressionParser(SpelParserConfiguration configuration) { + this.configuration = configuration; + } + + + @Override + protected SpelExpression doParseExpression(String expressionString, @Nullable ParserContext context) + throws ParseException { + + try { + this.expressionString = expressionString; + Tokenizer tokenizer = new Tokenizer(expressionString); + this.tokenStream = tokenizer.process(); + this.tokenStreamLength = this.tokenStream.size(); + this.tokenStreamPointer = 0; + this.constructedNodes.clear(); + SpelNodeImpl ast = eatExpression(); + Assert.state(ast != null, "No node"); + Token t = peekToken(); + if (t != null) { + throw new SpelParseException(t.startPos, SpelMessage.MORE_INPUT, toString(nextToken())); + } + Assert.isTrue(this.constructedNodes.isEmpty(), "At least one node expected"); + return new SpelExpression(expressionString, ast, this.configuration); + } + catch (InternalParseException ex) { + throw ex.getCause(); + } + } + + // expression + // : logicalOrExpression + // ( (ASSIGN^ logicalOrExpression) + // | (DEFAULT^ logicalOrExpression) + // | (QMARK^ expression COLON! expression) + // | (ELVIS^ expression))?; + @Nullable + private SpelNodeImpl eatExpression() { + SpelNodeImpl expr = eatLogicalOrExpression(); + Token t = peekToken(); + if (t != null) { + if (t.kind == TokenKind.ASSIGN) { // a=b + if (expr == null) { + expr = new NullLiteral(t.startPos - 1, t.endPos - 1); + } + nextToken(); + SpelNodeImpl assignedValue = eatLogicalOrExpression(); + return new Assign(t.startPos, t.endPos, expr, assignedValue); + } + if (t.kind == TokenKind.ELVIS) { // a?:b (a if it isn't null, otherwise b) + if (expr == null) { + expr = new NullLiteral(t.startPos - 1, t.endPos - 2); + } + nextToken(); // elvis has left the building + SpelNodeImpl valueIfNull = eatExpression(); + if (valueIfNull == null) { + valueIfNull = new NullLiteral(t.startPos + 1, t.endPos + 1); + } + return new Elvis(t.startPos, t.endPos, expr, valueIfNull); + } + if (t.kind == TokenKind.QMARK) { // a?b:c + if (expr == null) { + expr = new NullLiteral(t.startPos - 1, t.endPos - 1); + } + nextToken(); + SpelNodeImpl ifTrueExprValue = eatExpression(); + eatToken(TokenKind.COLON); + SpelNodeImpl ifFalseExprValue = eatExpression(); + return new Ternary(t.startPos, t.endPos, expr, ifTrueExprValue, ifFalseExprValue); + } + } + return expr; + } + + //logicalOrExpression : logicalAndExpression (OR^ logicalAndExpression)*; + @Nullable + private SpelNodeImpl eatLogicalOrExpression() { + SpelNodeImpl expr = eatLogicalAndExpression(); + while (peekIdentifierToken("or") || peekToken(TokenKind.SYMBOLIC_OR)) { + Token t = takeToken(); //consume OR + SpelNodeImpl rhExpr = eatLogicalAndExpression(); + checkOperands(t, expr, rhExpr); + expr = new OpOr(t.startPos, t.endPos, expr, rhExpr); + } + return expr; + } + + // logicalAndExpression : relationalExpression (AND^ relationalExpression)*; + @Nullable + private SpelNodeImpl eatLogicalAndExpression() { + SpelNodeImpl expr = eatRelationalExpression(); + while (peekIdentifierToken("and") || peekToken(TokenKind.SYMBOLIC_AND)) { + Token t = takeToken(); // consume 'AND' + SpelNodeImpl rhExpr = eatRelationalExpression(); + checkOperands(t, expr, rhExpr); + expr = new OpAnd(t.startPos, t.endPos, expr, rhExpr); + } + return expr; + } + + // relationalExpression : sumExpression (relationalOperator^ sumExpression)?; + @Nullable + private SpelNodeImpl eatRelationalExpression() { + SpelNodeImpl expr = eatSumExpression(); + Token relationalOperatorToken = maybeEatRelationalOperator(); + if (relationalOperatorToken != null) { + Token t = takeToken(); // consume relational operator token + SpelNodeImpl rhExpr = eatSumExpression(); + checkOperands(t, expr, rhExpr); + TokenKind tk = relationalOperatorToken.kind; + + if (relationalOperatorToken.isNumericRelationalOperator()) { + if (tk == TokenKind.GT) { + return new OpGT(t.startPos, t.endPos, expr, rhExpr); + } + if (tk == TokenKind.LT) { + return new OpLT(t.startPos, t.endPos, expr, rhExpr); + } + if (tk == TokenKind.LE) { + return new OpLE(t.startPos, t.endPos, expr, rhExpr); + } + if (tk == TokenKind.GE) { + return new OpGE(t.startPos, t.endPos, expr, rhExpr); + } + if (tk == TokenKind.EQ) { + return new OpEQ(t.startPos, t.endPos, expr, rhExpr); + } + Assert.isTrue(tk == TokenKind.NE, "Not-equals token expected"); + return new OpNE(t.startPos, t.endPos, expr, rhExpr); + } + + if (tk == TokenKind.INSTANCEOF) { + return new OperatorInstanceof(t.startPos, t.endPos, expr, rhExpr); + } + + if (tk == TokenKind.MATCHES) { + return new OperatorMatches(t.startPos, t.endPos, expr, rhExpr); + } + + Assert.isTrue(tk == TokenKind.BETWEEN, "Between token expected"); + return new OperatorBetween(t.startPos, t.endPos, expr, rhExpr); + } + return expr; + } + + //sumExpression: productExpression ( (PLUS^ | MINUS^) productExpression)*; + @Nullable + private SpelNodeImpl eatSumExpression() { + SpelNodeImpl expr = eatProductExpression(); + while (peekToken(TokenKind.PLUS, TokenKind.MINUS, TokenKind.INC)) { + Token t = takeToken(); //consume PLUS or MINUS or INC + SpelNodeImpl rhExpr = eatProductExpression(); + checkRightOperand(t, rhExpr); + if (t.kind == TokenKind.PLUS) { + expr = new OpPlus(t.startPos, t.endPos, expr, rhExpr); + } + else if (t.kind == TokenKind.MINUS) { + expr = new OpMinus(t.startPos, t.endPos, expr, rhExpr); + } + } + return expr; + } + + // productExpression: powerExpr ((STAR^ | DIV^| MOD^) powerExpr)* ; + @Nullable + private SpelNodeImpl eatProductExpression() { + SpelNodeImpl expr = eatPowerIncDecExpression(); + while (peekToken(TokenKind.STAR, TokenKind.DIV, TokenKind.MOD)) { + Token t = takeToken(); // consume STAR/DIV/MOD + SpelNodeImpl rhExpr = eatPowerIncDecExpression(); + checkOperands(t, expr, rhExpr); + if (t.kind == TokenKind.STAR) { + expr = new OpMultiply(t.startPos, t.endPos, expr, rhExpr); + } + else if (t.kind == TokenKind.DIV) { + expr = new OpDivide(t.startPos, t.endPos, expr, rhExpr); + } + else { + Assert.isTrue(t.kind == TokenKind.MOD, "Mod token expected"); + expr = new OpModulus(t.startPos, t.endPos, expr, rhExpr); + } + } + return expr; + } + + // powerExpr : unaryExpression (POWER^ unaryExpression)? (INC || DEC) ; + @Nullable + private SpelNodeImpl eatPowerIncDecExpression() { + SpelNodeImpl expr = eatUnaryExpression(); + if (peekToken(TokenKind.POWER)) { + Token t = takeToken(); //consume POWER + SpelNodeImpl rhExpr = eatUnaryExpression(); + checkRightOperand(t, rhExpr); + return new OperatorPower(t.startPos, t.endPos, expr, rhExpr); + } + if (expr != null && peekToken(TokenKind.INC, TokenKind.DEC)) { + Token t = takeToken(); //consume INC/DEC + if (t.getKind() == TokenKind.INC) { + return new OpInc(t.startPos, t.endPos, true, expr); + } + return new OpDec(t.startPos, t.endPos, true, expr); + } + return expr; + } + + // unaryExpression: (PLUS^ | MINUS^ | BANG^ | INC^ | DEC^) unaryExpression | primaryExpression ; + @Nullable + private SpelNodeImpl eatUnaryExpression() { + if (peekToken(TokenKind.PLUS, TokenKind.MINUS, TokenKind.NOT)) { + Token t = takeToken(); + SpelNodeImpl expr = eatUnaryExpression(); + Assert.state(expr != null, "No node"); + if (t.kind == TokenKind.NOT) { + return new OperatorNot(t.startPos, t.endPos, expr); + } + if (t.kind == TokenKind.PLUS) { + return new OpPlus(t.startPos, t.endPos, expr); + } + Assert.isTrue(t.kind == TokenKind.MINUS, "Minus token expected"); + return new OpMinus(t.startPos, t.endPos, expr); + } + if (peekToken(TokenKind.INC, TokenKind.DEC)) { + Token t = takeToken(); + SpelNodeImpl expr = eatUnaryExpression(); + if (t.getKind() == TokenKind.INC) { + return new OpInc(t.startPos, t.endPos, false, expr); + } + return new OpDec(t.startPos, t.endPos, false, expr); + } + return eatPrimaryExpression(); + } + + // primaryExpression : startNode (node)? -> ^(EXPRESSION startNode (node)?); + @Nullable + private SpelNodeImpl eatPrimaryExpression() { + SpelNodeImpl start = eatStartNode(); // always a start node + List nodes = null; + SpelNodeImpl node = eatNode(); + while (node != null) { + if (nodes == null) { + nodes = new ArrayList<>(4); + nodes.add(start); + } + nodes.add(node); + node = eatNode(); + } + if (start == null || nodes == null) { + return start; + } + return new CompoundExpression(start.getStartPosition(), nodes.get(nodes.size() - 1).getEndPosition(), + nodes.toArray(new SpelNodeImpl[0])); + } + + // node : ((DOT dottedNode) | (SAFE_NAVI dottedNode) | nonDottedNode)+; + @Nullable + private SpelNodeImpl eatNode() { + return (peekToken(TokenKind.DOT, TokenKind.SAFE_NAVI) ? eatDottedNode() : eatNonDottedNode()); + } + + // nonDottedNode: indexer; + @Nullable + private SpelNodeImpl eatNonDottedNode() { + if (peekToken(TokenKind.LSQUARE)) { + if (maybeEatIndexer()) { + return pop(); + } + } + return null; + } + + //dottedNode + // : ((methodOrProperty + // | functionOrVar + // | projection + // | selection + // | firstSelection + // | lastSelection + // )) + // ; + private SpelNodeImpl eatDottedNode() { + Token t = takeToken(); // it was a '.' or a '?.' + boolean nullSafeNavigation = (t.kind == TokenKind.SAFE_NAVI); + if (maybeEatMethodOrProperty(nullSafeNavigation) || maybeEatFunctionOrVar() || + maybeEatProjection(nullSafeNavigation) || maybeEatSelection(nullSafeNavigation)) { + return pop(); + } + if (peekToken() == null) { + // unexpectedly ran out of data + throw internalException(t.startPos, SpelMessage.OOD); + } + else { + throw internalException(t.startPos, SpelMessage.UNEXPECTED_DATA_AFTER_DOT, toString(peekToken())); + } + } + + // functionOrVar + // : (POUND ID LPAREN) => function + // | var + // + // function : POUND id=ID methodArgs -> ^(FUNCTIONREF[$id] methodArgs); + // var : POUND id=ID -> ^(VARIABLEREF[$id]); + private boolean maybeEatFunctionOrVar() { + if (!peekToken(TokenKind.HASH)) { + return false; + } + Token t = takeToken(); + Token functionOrVariableName = eatToken(TokenKind.IDENTIFIER); + SpelNodeImpl[] args = maybeEatMethodArgs(); + if (args == null) { + push(new VariableReference(functionOrVariableName.stringValue(), + t.startPos, functionOrVariableName.endPos)); + return true; + } + + push(new FunctionReference(functionOrVariableName.stringValue(), + t.startPos, functionOrVariableName.endPos, args)); + return true; + } + + // methodArgs : LPAREN! (argument (COMMA! argument)* (COMMA!)?)? RPAREN!; + @Nullable + private SpelNodeImpl[] maybeEatMethodArgs() { + if (!peekToken(TokenKind.LPAREN)) { + return null; + } + List args = new ArrayList<>(); + consumeArguments(args); + eatToken(TokenKind.RPAREN); + return args.toArray(new SpelNodeImpl[0]); + } + + private void eatConstructorArgs(List accumulatedArguments) { + if (!peekToken(TokenKind.LPAREN)) { + throw new InternalParseException(new SpelParseException(this.expressionString, + positionOf(peekToken()), SpelMessage.MISSING_CONSTRUCTOR_ARGS)); + } + consumeArguments(accumulatedArguments); + eatToken(TokenKind.RPAREN); + } + + /** + * Used for consuming arguments for either a method or a constructor call. + */ + private void consumeArguments(List accumulatedArguments) { + Token t = peekToken(); + Assert.state(t != null, "Expected token"); + int pos = t.startPos; + Token next; + do { + nextToken(); // consume (first time through) or comma (subsequent times) + t = peekToken(); + if (t == null) { + throw internalException(pos, SpelMessage.RUN_OUT_OF_ARGUMENTS); + } + if (t.kind != TokenKind.RPAREN) { + accumulatedArguments.add(eatExpression()); + } + next = peekToken(); + } + while (next != null && next.kind == TokenKind.COMMA); + + if (next == null) { + throw internalException(pos, SpelMessage.RUN_OUT_OF_ARGUMENTS); + } + } + + private int positionOf(@Nullable Token t) { + if (t == null) { + // if null assume the problem is because the right token was + // not found at the end of the expression + return this.expressionString.length(); + } + return t.startPos; + } + + //startNode + // : parenExpr | literal + // | type + // | methodOrProperty + // | functionOrVar + // | projection + // | selection + // | firstSelection + // | lastSelection + // | indexer + // | constructor + @Nullable + private SpelNodeImpl eatStartNode() { + if (maybeEatLiteral()) { + return pop(); + } + else if (maybeEatParenExpression()) { + return pop(); + } + else if (maybeEatTypeReference() || maybeEatNullReference() || maybeEatConstructorReference() || + maybeEatMethodOrProperty(false) || maybeEatFunctionOrVar()) { + return pop(); + } + else if (maybeEatBeanReference()) { + return pop(); + } + else if (maybeEatProjection(false) || maybeEatSelection(false) || maybeEatIndexer()) { + return pop(); + } + else if (maybeEatInlineListOrMap()) { + return pop(); + } + else { + return null; + } + } + + // parse: @beanname @'bean.name' + // quoted if dotted + private boolean maybeEatBeanReference() { + if (peekToken(TokenKind.BEAN_REF) || peekToken(TokenKind.FACTORY_BEAN_REF)) { + Token beanRefToken = takeToken(); + Token beanNameToken = null; + String beanName = null; + if (peekToken(TokenKind.IDENTIFIER)) { + beanNameToken = eatToken(TokenKind.IDENTIFIER); + beanName = beanNameToken.stringValue(); + } + else if (peekToken(TokenKind.LITERAL_STRING)) { + beanNameToken = eatToken(TokenKind.LITERAL_STRING); + beanName = beanNameToken.stringValue(); + beanName = beanName.substring(1, beanName.length() - 1); + } + else { + throw internalException(beanRefToken.startPos, SpelMessage.INVALID_BEAN_REFERENCE); + } + BeanReference beanReference; + if (beanRefToken.getKind() == TokenKind.FACTORY_BEAN_REF) { + String beanNameString = String.valueOf(TokenKind.FACTORY_BEAN_REF.tokenChars) + beanName; + beanReference = new BeanReference(beanRefToken.startPos, beanNameToken.endPos, beanNameString); + } + else { + beanReference = new BeanReference(beanNameToken.startPos, beanNameToken.endPos, beanName); + } + this.constructedNodes.push(beanReference); + return true; + } + return false; + } + + private boolean maybeEatTypeReference() { + if (peekToken(TokenKind.IDENTIFIER)) { + Token typeName = peekToken(); + Assert.state(typeName != null, "Expected token"); + if (!"T".equals(typeName.stringValue())) { + return false; + } + // It looks like a type reference but is T being used as a map key? + Token t = takeToken(); + if (peekToken(TokenKind.RSQUARE)) { + // looks like 'T]' (T is map key) + push(new PropertyOrFieldReference(false, t.stringValue(), t.startPos, t.endPos)); + return true; + } + eatToken(TokenKind.LPAREN); + SpelNodeImpl node = eatPossiblyQualifiedId(); + // dotted qualified id + // Are there array dimensions? + int dims = 0; + while (peekToken(TokenKind.LSQUARE, true)) { + eatToken(TokenKind.RSQUARE); + dims++; + } + eatToken(TokenKind.RPAREN); + this.constructedNodes.push(new TypeReference(typeName.startPos, typeName.endPos, node, dims)); + return true; + } + return false; + } + + private boolean maybeEatNullReference() { + if (peekToken(TokenKind.IDENTIFIER)) { + Token nullToken = peekToken(); + Assert.state(nullToken != null, "Expected token"); + if (!"null".equalsIgnoreCase(nullToken.stringValue())) { + return false; + } + nextToken(); + this.constructedNodes.push(new NullLiteral(nullToken.startPos, nullToken.endPos)); + return true; + } + return false; + } + + //projection: PROJECT^ expression RCURLY!; + private boolean maybeEatProjection(boolean nullSafeNavigation) { + Token t = peekToken(); + if (!peekToken(TokenKind.PROJECT, true)) { + return false; + } + Assert.state(t != null, "No token"); + SpelNodeImpl expr = eatExpression(); + Assert.state(expr != null, "No node"); + eatToken(TokenKind.RSQUARE); + this.constructedNodes.push(new Projection(nullSafeNavigation, t.startPos, t.endPos, expr)); + return true; + } + + // list = LCURLY (element (COMMA element)*) RCURLY + // map = LCURLY (key ':' value (COMMA key ':' value)*) RCURLY + private boolean maybeEatInlineListOrMap() { + Token t = peekToken(); + if (!peekToken(TokenKind.LCURLY, true)) { + return false; + } + Assert.state(t != null, "No token"); + SpelNodeImpl expr = null; + Token closingCurly = peekToken(); + if (peekToken(TokenKind.RCURLY, true)) { + // empty list '{}' + Assert.state(closingCurly != null, "No token"); + expr = new InlineList(t.startPos, closingCurly.endPos); + } + else if (peekToken(TokenKind.COLON, true)) { + closingCurly = eatToken(TokenKind.RCURLY); + // empty map '{:}' + expr = new InlineMap(t.startPos, closingCurly.endPos); + } + else { + SpelNodeImpl firstExpression = eatExpression(); + // Next is either: + // '}' - end of list + // ',' - more expressions in this list + // ':' - this is a map! + if (peekToken(TokenKind.RCURLY)) { // list with one item in it + List elements = new ArrayList<>(); + elements.add(firstExpression); + closingCurly = eatToken(TokenKind.RCURLY); + expr = new InlineList(t.startPos, closingCurly.endPos, elements.toArray(new SpelNodeImpl[0])); + } + else if (peekToken(TokenKind.COMMA, true)) { // multi-item list + List elements = new ArrayList<>(); + elements.add(firstExpression); + do { + elements.add(eatExpression()); + } + while (peekToken(TokenKind.COMMA, true)); + closingCurly = eatToken(TokenKind.RCURLY); + expr = new InlineList(t.startPos, closingCurly.endPos, elements.toArray(new SpelNodeImpl[0])); + + } + else if (peekToken(TokenKind.COLON, true)) { // map! + List elements = new ArrayList<>(); + elements.add(firstExpression); + elements.add(eatExpression()); + while (peekToken(TokenKind.COMMA, true)) { + elements.add(eatExpression()); + eatToken(TokenKind.COLON); + elements.add(eatExpression()); + } + closingCurly = eatToken(TokenKind.RCURLY); + expr = new InlineMap(t.startPos, closingCurly.endPos, elements.toArray(new SpelNodeImpl[0])); + } + else { + throw internalException(t.startPos, SpelMessage.OOD); + } + } + this.constructedNodes.push(expr); + return true; + } + + private boolean maybeEatIndexer() { + Token t = peekToken(); + if (!peekToken(TokenKind.LSQUARE, true)) { + return false; + } + Assert.state(t != null, "No token"); + SpelNodeImpl expr = eatExpression(); + Assert.state(expr != null, "No node"); + eatToken(TokenKind.RSQUARE); + this.constructedNodes.push(new Indexer(t.startPos, t.endPos, expr)); + return true; + } + + private boolean maybeEatSelection(boolean nullSafeNavigation) { + Token t = peekToken(); + if (!peekSelectToken()) { + return false; + } + Assert.state(t != null, "No token"); + nextToken(); + SpelNodeImpl expr = eatExpression(); + if (expr == null) { + throw internalException(t.startPos, SpelMessage.MISSING_SELECTION_EXPRESSION); + } + eatToken(TokenKind.RSQUARE); + if (t.kind == TokenKind.SELECT_FIRST) { + this.constructedNodes.push(new Selection(nullSafeNavigation, Selection.FIRST, t.startPos, t.endPos, expr)); + } + else if (t.kind == TokenKind.SELECT_LAST) { + this.constructedNodes.push(new Selection(nullSafeNavigation, Selection.LAST, t.startPos, t.endPos, expr)); + } + else { + this.constructedNodes.push(new Selection(nullSafeNavigation, Selection.ALL, t.startPos, t.endPos, expr)); + } + return true; + } + + /** + * Eat an identifier, possibly qualified (meaning that it is dotted). + * TODO AndyC Could create complete identifiers (a.b.c) here rather than a sequence of them? (a, b, c) + */ + private SpelNodeImpl eatPossiblyQualifiedId() { + Deque qualifiedIdPieces = new ArrayDeque<>(); + Token node = peekToken(); + while (isValidQualifiedId(node)) { + nextToken(); + if (node.kind != TokenKind.DOT) { + qualifiedIdPieces.add(new Identifier(node.stringValue(), node.startPos, node.endPos)); + } + node = peekToken(); + } + if (qualifiedIdPieces.isEmpty()) { + if (node == null) { + throw internalException( this.expressionString.length(), SpelMessage.OOD); + } + throw internalException(node.startPos, SpelMessage.NOT_EXPECTED_TOKEN, + "qualified ID", node.getKind().toString().toLowerCase()); + } + return new QualifiedIdentifier(qualifiedIdPieces.getFirst().getStartPosition(), + qualifiedIdPieces.getLast().getEndPosition(), qualifiedIdPieces.toArray(new SpelNodeImpl[0])); + } + + private boolean isValidQualifiedId(@Nullable Token node) { + if (node == null || node.kind == TokenKind.LITERAL_STRING) { + return false; + } + if (node.kind == TokenKind.DOT || node.kind == TokenKind.IDENTIFIER) { + return true; + } + String value = node.stringValue(); + return (StringUtils.hasLength(value) && VALID_QUALIFIED_ID_PATTERN.matcher(value).matches()); + } + + // This is complicated due to the support for dollars in identifiers. + // Dollars are normally separate tokens but there we want to combine + // a series of identifiers and dollars into a single identifier. + private boolean maybeEatMethodOrProperty(boolean nullSafeNavigation) { + if (peekToken(TokenKind.IDENTIFIER)) { + Token methodOrPropertyName = takeToken(); + SpelNodeImpl[] args = maybeEatMethodArgs(); + if (args == null) { + // property + push(new PropertyOrFieldReference(nullSafeNavigation, methodOrPropertyName.stringValue(), + methodOrPropertyName.startPos, methodOrPropertyName.endPos)); + return true; + } + // method reference + push(new MethodReference(nullSafeNavigation, methodOrPropertyName.stringValue(), + methodOrPropertyName.startPos, methodOrPropertyName.endPos, args)); + // TODO what is the end position for a method reference? the name or the last arg? + return true; + } + return false; + } + + //constructor + //: ('new' qualifiedId LPAREN) => 'new' qualifiedId ctorArgs -> ^(CONSTRUCTOR qualifiedId ctorArgs) + private boolean maybeEatConstructorReference() { + if (peekIdentifierToken("new")) { + Token newToken = takeToken(); + // It looks like a constructor reference but is NEW being used as a map key? + if (peekToken(TokenKind.RSQUARE)) { + // looks like 'NEW]' (so NEW used as map key) + push(new PropertyOrFieldReference(false, newToken.stringValue(), newToken.startPos, newToken.endPos)); + return true; + } + SpelNodeImpl possiblyQualifiedConstructorName = eatPossiblyQualifiedId(); + List nodes = new ArrayList<>(); + nodes.add(possiblyQualifiedConstructorName); + if (peekToken(TokenKind.LSQUARE)) { + // array initializer + List dimensions = new ArrayList<>(); + while (peekToken(TokenKind.LSQUARE, true)) { + if (!peekToken(TokenKind.RSQUARE)) { + dimensions.add(eatExpression()); + } + else { + dimensions.add(null); + } + eatToken(TokenKind.RSQUARE); + } + if (maybeEatInlineListOrMap()) { + nodes.add(pop()); + } + push(new ConstructorReference(newToken.startPos, newToken.endPos, + dimensions.toArray(new SpelNodeImpl[0]), nodes.toArray(new SpelNodeImpl[0]))); + } + else { + // regular constructor invocation + eatConstructorArgs(nodes); + // TODO correct end position? + push(new ConstructorReference(newToken.startPos, newToken.endPos, nodes.toArray(new SpelNodeImpl[0]))); + } + return true; + } + return false; + } + + private void push(SpelNodeImpl newNode) { + this.constructedNodes.push(newNode); + } + + private SpelNodeImpl pop() { + return this.constructedNodes.pop(); + } + + // literal + // : INTEGER_LITERAL + // | boolLiteral + // | STRING_LITERAL + // | HEXADECIMAL_INTEGER_LITERAL + // | REAL_LITERAL + // | DQ_STRING_LITERAL + // | NULL_LITERAL + private boolean maybeEatLiteral() { + Token t = peekToken(); + if (t == null) { + return false; + } + if (t.kind == TokenKind.LITERAL_INT) { + push(Literal.getIntLiteral(t.stringValue(), t.startPos, t.endPos, 10)); + } + else if (t.kind == TokenKind.LITERAL_LONG) { + push(Literal.getLongLiteral(t.stringValue(), t.startPos, t.endPos, 10)); + } + else if (t.kind == TokenKind.LITERAL_HEXINT) { + push(Literal.getIntLiteral(t.stringValue(), t.startPos, t.endPos, 16)); + } + else if (t.kind == TokenKind.LITERAL_HEXLONG) { + push(Literal.getLongLiteral(t.stringValue(), t.startPos, t.endPos, 16)); + } + else if (t.kind == TokenKind.LITERAL_REAL) { + push(Literal.getRealLiteral(t.stringValue(), t.startPos, t.endPos, false)); + } + else if (t.kind == TokenKind.LITERAL_REAL_FLOAT) { + push(Literal.getRealLiteral(t.stringValue(), t.startPos, t.endPos, true)); + } + else if (peekIdentifierToken("true")) { + push(new BooleanLiteral(t.stringValue(), t.startPos, t.endPos, true)); + } + else if (peekIdentifierToken("false")) { + push(new BooleanLiteral(t.stringValue(), t.startPos, t.endPos, false)); + } + else if (t.kind == TokenKind.LITERAL_STRING) { + push(new StringLiteral(t.stringValue(), t.startPos, t.endPos, t.stringValue())); + } + else { + return false; + } + nextToken(); + return true; + } + + //parenExpr : LPAREN! expression RPAREN!; + private boolean maybeEatParenExpression() { + if (peekToken(TokenKind.LPAREN)) { + nextToken(); + SpelNodeImpl expr = eatExpression(); + Assert.state(expr != null, "No node"); + eatToken(TokenKind.RPAREN); + push(expr); + return true; + } + else { + return false; + } + } + + // relationalOperator + // : EQUAL | NOT_EQUAL | LESS_THAN | LESS_THAN_OR_EQUAL | GREATER_THAN + // | GREATER_THAN_OR_EQUAL | INSTANCEOF | BETWEEN | MATCHES + @Nullable + private Token maybeEatRelationalOperator() { + Token t = peekToken(); + if (t == null) { + return null; + } + if (t.isNumericRelationalOperator()) { + return t; + } + if (t.isIdentifier()) { + String idString = t.stringValue(); + if (idString.equalsIgnoreCase("instanceof")) { + return t.asInstanceOfToken(); + } + if (idString.equalsIgnoreCase("matches")) { + return t.asMatchesToken(); + } + if (idString.equalsIgnoreCase("between")) { + return t.asBetweenToken(); + } + } + return null; + } + + private Token eatToken(TokenKind expectedKind) { + Token t = nextToken(); + if (t == null) { + int pos = this.expressionString.length(); + throw internalException(pos, SpelMessage.OOD); + } + if (t.kind != expectedKind) { + throw internalException(t.startPos, SpelMessage.NOT_EXPECTED_TOKEN, + expectedKind.toString().toLowerCase(), t.getKind().toString().toLowerCase()); + } + return t; + } + + private boolean peekToken(TokenKind desiredTokenKind) { + return peekToken(desiredTokenKind, false); + } + + private boolean peekToken(TokenKind desiredTokenKind, boolean consumeIfMatched) { + Token t = peekToken(); + if (t == null) { + return false; + } + if (t.kind == desiredTokenKind) { + if (consumeIfMatched) { + this.tokenStreamPointer++; + } + return true; + } + + if (desiredTokenKind == TokenKind.IDENTIFIER) { + // Might be one of the textual forms of the operators (e.g. NE for != ) - + // in which case we can treat it as an identifier. The list is represented here: + // Tokenizer.alternativeOperatorNames and those ones are in order in the TokenKind enum. + if (t.kind.ordinal() >= TokenKind.DIV.ordinal() && t.kind.ordinal() <= TokenKind.NOT.ordinal() && + t.data != null) { + // if t.data were null, we'd know it wasn't the textual form, it was the symbol form + return true; + } + } + return false; + } + + private boolean peekToken(TokenKind possible1, TokenKind possible2) { + Token t = peekToken(); + if (t == null) { + return false; + } + return (t.kind == possible1 || t.kind == possible2); + } + + private boolean peekToken(TokenKind possible1, TokenKind possible2, TokenKind possible3) { + Token t = peekToken(); + if (t == null) { + return false; + } + return (t.kind == possible1 || t.kind == possible2 || t.kind == possible3); + } + + private boolean peekIdentifierToken(String identifierString) { + Token t = peekToken(); + if (t == null) { + return false; + } + return (t.kind == TokenKind.IDENTIFIER && identifierString.equalsIgnoreCase(t.stringValue())); + } + + private boolean peekSelectToken() { + Token t = peekToken(); + if (t == null) { + return false; + } + return (t.kind == TokenKind.SELECT || t.kind == TokenKind.SELECT_FIRST || t.kind == TokenKind.SELECT_LAST); + } + + private Token takeToken() { + if (this.tokenStreamPointer >= this.tokenStreamLength) { + throw new IllegalStateException("No token"); + } + return this.tokenStream.get(this.tokenStreamPointer++); + } + + @Nullable + private Token nextToken() { + if (this.tokenStreamPointer >= this.tokenStreamLength) { + return null; + } + return this.tokenStream.get(this.tokenStreamPointer++); + } + + @Nullable + private Token peekToken() { + if (this.tokenStreamPointer >= this.tokenStreamLength) { + return null; + } + return this.tokenStream.get(this.tokenStreamPointer); + } + + public String toString(@Nullable Token t) { + if (t == null) { + return ""; + } + if (t.getKind().hasPayload()) { + return t.stringValue(); + } + return t.kind.toString().toLowerCase(); + } + + private void checkOperands(Token token, @Nullable SpelNodeImpl left, @Nullable SpelNodeImpl right) { + checkLeftOperand(token, left); + checkRightOperand(token, right); + } + + private void checkLeftOperand(Token token, @Nullable SpelNodeImpl operandExpression) { + if (operandExpression == null) { + throw internalException(token.startPos, SpelMessage.LEFT_OPERAND_PROBLEM); + } + } + + private void checkRightOperand(Token token, @Nullable SpelNodeImpl operandExpression) { + if (operandExpression == null) { + throw internalException(token.startPos, SpelMessage.RIGHT_OPERAND_PROBLEM); + } + } + + private InternalParseException internalException(int startPos, SpelMessage message, Object... inserts) { + return new InternalParseException(new SpelParseException(this.expressionString, startPos, message, inserts)); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.java new file mode 100644 index 0000000..82d17e7 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.java @@ -0,0 +1,308 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.standard; + +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.asm.ClassWriter; +import org.springframework.asm.MethodVisitor; +import org.springframework.asm.Opcodes; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.CompiledExpression; +import org.springframework.expression.spel.SpelParserConfiguration; +import org.springframework.expression.spel.ast.SpelNodeImpl; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * A SpelCompiler will take a regular parsed expression and create (and load) a class + * containing byte code that does the same thing as that expression. The compiled form of + * an expression will evaluate far faster than the interpreted form. + * + *

    The SpelCompiler is not currently handling all expression types but covers many of + * the common cases. The framework is extensible to cover more cases in the future. For + * absolute maximum speed there is *no checking* in the compiled code. The compiled + * version of the expression uses information learned during interpreted runs of the + * expression when it generates the byte code. For example if it knows that a particular + * property dereference always seems to return a Map then it will generate byte code that + * expects the result of the property dereference to be a Map. This ensures maximal + * performance but should the dereference result in something other than a map, the + * compiled expression will fail - like a ClassCastException would occur if passing data + * of an unexpected type in a regular Java program. + * + *

    Due to the lack of checking there are likely some expressions that should never be + * compiled, for example if an expression is continuously dealing with different types of + * data. Due to these cases the compiler is something that must be selectively turned on + * for an associated SpelExpressionParser (through the {@link SpelParserConfiguration} + * object), it is not on by default. + * + *

    Individual expressions can be compiled by calling {@code SpelCompiler.compile(expression)}. + * + * @author Andy Clement + * @author Juergen Hoeller + * @since 4.1 + */ +public final class SpelCompiler implements Opcodes { + + private static final int CLASSES_DEFINED_LIMIT = 100; + + private static final Log logger = LogFactory.getLog(SpelCompiler.class); + + // A compiler is created for each classloader, it manages a child class loader of that + // classloader and the child is used to load the compiled expressions. + private static final Map compilers = new ConcurrentReferenceHashMap<>(); + + + // The child ClassLoader used to load the compiled expression classes + private volatile ChildClassLoader childClassLoader; + + // Counter suffix for generated classes within this SpelCompiler instance + private final AtomicInteger suffixId = new AtomicInteger(1); + + + private SpelCompiler(@Nullable ClassLoader classloader) { + this.childClassLoader = new ChildClassLoader(classloader); + } + + + /** + * Attempt compilation of the supplied expression. A check is made to see + * if it is compilable before compilation proceeds. The check involves + * visiting all the nodes in the expression AST and ensuring enough state + * is known about them that bytecode can be generated for them. + * @param expression the expression to compile + * @return an instance of the class implementing the compiled expression, + * or {@code null} if compilation is not possible + */ + @Nullable + public CompiledExpression compile(SpelNodeImpl expression) { + if (expression.isCompilable()) { + if (logger.isDebugEnabled()) { + logger.debug("SpEL: compiling " + expression.toStringAST()); + } + Class clazz = createExpressionClass(expression); + if (clazz != null) { + try { + return ReflectionUtils.accessibleConstructor(clazz).newInstance(); + } + catch (Throwable ex) { + throw new IllegalStateException("Failed to instantiate CompiledExpression", ex); + } + } + } + + if (logger.isDebugEnabled()) { + logger.debug("SpEL: unable to compile " + expression.toStringAST()); + } + return null; + } + + private int getNextSuffix() { + return this.suffixId.incrementAndGet(); + } + + /** + * Generate the class that encapsulates the compiled expression and define it. + * The generated class will be a subtype of CompiledExpression. + * @param expressionToCompile the expression to be compiled + * @return the expression call, or {@code null} if the decision was to opt out of + * compilation during code generation + */ + @Nullable + private Class createExpressionClass(SpelNodeImpl expressionToCompile) { + // Create class outline 'spel/ExNNN extends org.springframework.expression.spel.CompiledExpression' + String className = "spel/Ex" + getNextSuffix(); + ClassWriter cw = new ExpressionClassWriter(); + cw.visit(V1_8, ACC_PUBLIC, className, null, "org/springframework/expression/spel/CompiledExpression", null); + + // Create default constructor + MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "", "()V", null, null); + mv.visitCode(); + mv.visitVarInsn(ALOAD, 0); + mv.visitMethodInsn(INVOKESPECIAL, "org/springframework/expression/spel/CompiledExpression", + "", "()V", false); + mv.visitInsn(RETURN); + mv.visitMaxs(1, 1); + mv.visitEnd(); + + // Create getValue() method + mv = cw.visitMethod(ACC_PUBLIC, "getValue", + "(Ljava/lang/Object;Lorg/springframework/expression/EvaluationContext;)Ljava/lang/Object;", null, + new String[] {"org/springframework/expression/EvaluationException"}); + mv.visitCode(); + + CodeFlow cf = new CodeFlow(className, cw); + + // Ask the expression AST to generate the body of the method + try { + expressionToCompile.generateCode(mv, cf); + } + catch (IllegalStateException ex) { + if (logger.isDebugEnabled()) { + logger.debug(expressionToCompile.getClass().getSimpleName() + + ".generateCode opted out of compilation: " + ex.getMessage()); + } + return null; + } + + CodeFlow.insertBoxIfNecessary(mv, cf.lastDescriptor()); + if ("V".equals(cf.lastDescriptor())) { + mv.visitInsn(ACONST_NULL); + } + mv.visitInsn(ARETURN); + + mv.visitMaxs(0, 0); // not supplied due to COMPUTE_MAXS + mv.visitEnd(); + cw.visitEnd(); + + cf.finish(); + + byte[] data = cw.toByteArray(); + // TODO need to make this conditionally occur based on a debug flag + // dump(expressionToCompile.toStringAST(), clazzName, data); + return loadClass(StringUtils.replace(className, "/", "."), data); + } + + /** + * Load a compiled expression class. Makes sure the classloaders aren't used too much + * because they anchor compiled classes in memory and prevent GC. If you have expressions + * continually recompiling over time then by replacing the classloader periodically + * at least some of the older variants can be garbage collected. + * @param name the name of the class + * @param bytes the bytecode for the class + * @return the Class object for the compiled expression + */ + @SuppressWarnings("unchecked") + private Class loadClass(String name, byte[] bytes) { + ChildClassLoader ccl = this.childClassLoader; + if (ccl.getClassesDefinedCount() >= CLASSES_DEFINED_LIMIT) { + synchronized (this) { + ChildClassLoader currentCcl = this.childClassLoader; + if (ccl == currentCcl) { + // Still the same ClassLoader that needs to be replaced... + ccl = new ChildClassLoader(ccl.getParent()); + this.childClassLoader = ccl; + } + else { + // Already replaced by some other thread, let's pick it up. + ccl = currentCcl; + } + } + } + return (Class) ccl.defineClass(name, bytes); + } + + + /** + * Factory method for compiler instances. The returned SpelCompiler will + * attach a class loader as the child of the given class loader and this + * child will be used to load compiled expressions. + * @param classLoader the ClassLoader to use as the basis for compilation + * @return a corresponding SpelCompiler instance + */ + public static SpelCompiler getCompiler(@Nullable ClassLoader classLoader) { + ClassLoader clToUse = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader()); + // Quick check for existing compiler without lock contention + SpelCompiler compiler = compilers.get(clToUse); + if (compiler == null) { + // Full lock now since we're creating a child ClassLoader + synchronized (compilers) { + compiler = compilers.get(clToUse); + if (compiler == null) { + compiler = new SpelCompiler(clToUse); + compilers.put(clToUse, compiler); + } + } + } + return compiler; + } + + /** + * Request that an attempt is made to compile the specified expression. + * It may fail if components of the expression are not suitable for compilation + * or the data types involved are not suitable for compilation. Used for testing. + * @param expression the expression to compile + * @return {@code true} if the expression was successfully compiled, + * {@code false} otherwise + */ + public static boolean compile(Expression expression) { + return (expression instanceof SpelExpression && ((SpelExpression) expression).compileExpression()); + } + + /** + * Request to revert to the interpreter for expression evaluation. + * Any compiled form is discarded but can be recreated by later recompiling again. + * @param expression the expression + */ + public static void revertToInterpreted(Expression expression) { + if (expression instanceof SpelExpression) { + ((SpelExpression) expression).revertToInterpreted(); + } + } + + + /** + * A ChildClassLoader will load the generated compiled expression classes. + */ + private static class ChildClassLoader extends URLClassLoader { + + private static final URL[] NO_URLS = new URL[0]; + + private final AtomicInteger classesDefinedCount = new AtomicInteger(0); + + public ChildClassLoader(@Nullable ClassLoader classLoader) { + super(NO_URLS, classLoader); + } + + public Class defineClass(String name, byte[] bytes) { + Class clazz = super.defineClass(name, bytes, 0, bytes.length); + this.classesDefinedCount.incrementAndGet(); + return clazz; + } + + public int getClassesDefinedCount() { + return this.classesDefinedCount.get(); + } + } + + + /** + * An ASM ClassWriter extension bound to the SpelCompiler's ClassLoader. + */ + private class ExpressionClassWriter extends ClassWriter { + + public ExpressionClassWriter() { + super(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); + } + + @Override + protected ClassLoader getClassLoader() { + return childClassLoader; + } + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java new file mode 100644 index 0000000..86ed383 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java @@ -0,0 +1,572 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.standard; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Expression; +import org.springframework.expression.TypedValue; +import org.springframework.expression.common.ExpressionUtils; +import org.springframework.expression.spel.CompiledExpression; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelCompilerMode; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.SpelNode; +import org.springframework.expression.spel.SpelParserConfiguration; +import org.springframework.expression.spel.ast.SpelNodeImpl; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * A {@code SpelExpression} represents a parsed (valid) expression that is ready to be + * evaluated in a specified context. An expression can be evaluated standalone or in a + * specified context. During expression evaluation the context may be asked to resolve + * references to types, beans, properties, and methods. + * + * @author Andy Clement + * @author Juergen Hoeller + * @since 3.0 + */ +public class SpelExpression implements Expression { + + // Number of times to interpret an expression before compiling it + private static final int INTERPRETED_COUNT_THRESHOLD = 100; + + // Number of times to try compiling an expression before giving up + private static final int FAILED_ATTEMPTS_THRESHOLD = 100; + + + private final String expression; + + private final SpelNodeImpl ast; + + private final SpelParserConfiguration configuration; + + // The default context is used if no override is supplied by the user + @Nullable + private EvaluationContext evaluationContext; + + // Holds the compiled form of the expression (if it has been compiled) + @Nullable + private volatile CompiledExpression compiledAst; + + // Count of many times as the expression been interpreted - can trigger compilation + // when certain limit reached + private final AtomicInteger interpretedCount = new AtomicInteger(); + + // The number of times compilation was attempted and failed - enables us to eventually + // give up trying to compile it when it just doesn't seem to be possible. + private final AtomicInteger failedAttempts = new AtomicInteger(); + + + /** + * Construct an expression, only used by the parser. + */ + public SpelExpression(String expression, SpelNodeImpl ast, SpelParserConfiguration configuration) { + this.expression = expression; + this.ast = ast; + this.configuration = configuration; + } + + + /** + * Set the evaluation context that will be used if none is specified on an evaluation call. + * @param evaluationContext the evaluation context to use + */ + public void setEvaluationContext(EvaluationContext evaluationContext) { + this.evaluationContext = evaluationContext; + } + + /** + * Return the default evaluation context that will be used if none is supplied on an evaluation call. + * @return the default evaluation context + */ + public EvaluationContext getEvaluationContext() { + if (this.evaluationContext == null) { + this.evaluationContext = new StandardEvaluationContext(); + } + return this.evaluationContext; + } + + + // implementing Expression + + @Override + public String getExpressionString() { + return this.expression; + } + + @Override + @Nullable + public Object getValue() throws EvaluationException { + CompiledExpression compiledAst = this.compiledAst; + if (compiledAst != null) { + try { + EvaluationContext context = getEvaluationContext(); + return compiledAst.getValue(context.getRootObject().getValue(), context); + } + catch (Throwable ex) { + // If running in mixed mode, revert to interpreted + if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) { + this.compiledAst = null; + this.interpretedCount.set(0); + } + else { + // Running in SpelCompilerMode.immediate mode - propagate exception to caller + throw new SpelEvaluationException(ex, SpelMessage.EXCEPTION_RUNNING_COMPILED_EXPRESSION); + } + } + } + + ExpressionState expressionState = new ExpressionState(getEvaluationContext(), this.configuration); + Object result = this.ast.getValue(expressionState); + checkCompile(expressionState); + return result; + } + + @SuppressWarnings("unchecked") + @Override + @Nullable + public T getValue(@Nullable Class expectedResultType) throws EvaluationException { + CompiledExpression compiledAst = this.compiledAst; + if (compiledAst != null) { + try { + EvaluationContext context = getEvaluationContext(); + Object result = compiledAst.getValue(context.getRootObject().getValue(), context); + if (expectedResultType == null) { + return (T) result; + } + else { + return ExpressionUtils.convertTypedValue( + getEvaluationContext(), new TypedValue(result), expectedResultType); + } + } + catch (Throwable ex) { + // If running in mixed mode, revert to interpreted + if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) { + this.compiledAst = null; + this.interpretedCount.set(0); + } + else { + // Running in SpelCompilerMode.immediate mode - propagate exception to caller + throw new SpelEvaluationException(ex, SpelMessage.EXCEPTION_RUNNING_COMPILED_EXPRESSION); + } + } + } + + ExpressionState expressionState = new ExpressionState(getEvaluationContext(), this.configuration); + TypedValue typedResultValue = this.ast.getTypedValue(expressionState); + checkCompile(expressionState); + return ExpressionUtils.convertTypedValue( + expressionState.getEvaluationContext(), typedResultValue, expectedResultType); + } + + @Override + @Nullable + public Object getValue(@Nullable Object rootObject) throws EvaluationException { + CompiledExpression compiledAst = this.compiledAst; + if (compiledAst != null) { + try { + return compiledAst.getValue(rootObject, getEvaluationContext()); + } + catch (Throwable ex) { + // If running in mixed mode, revert to interpreted + if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) { + this.compiledAst = null; + this.interpretedCount.set(0); + } + else { + // Running in SpelCompilerMode.immediate mode - propagate exception to caller + throw new SpelEvaluationException(ex, SpelMessage.EXCEPTION_RUNNING_COMPILED_EXPRESSION); + } + } + } + + ExpressionState expressionState = + new ExpressionState(getEvaluationContext(), toTypedValue(rootObject), this.configuration); + Object result = this.ast.getValue(expressionState); + checkCompile(expressionState); + return result; + } + + @SuppressWarnings("unchecked") + @Override + @Nullable + public T getValue(@Nullable Object rootObject, @Nullable Class expectedResultType) throws EvaluationException { + CompiledExpression compiledAst = this.compiledAst; + if (compiledAst != null) { + try { + Object result = compiledAst.getValue(rootObject, getEvaluationContext()); + if (expectedResultType == null) { + return (T)result; + } + else { + return ExpressionUtils.convertTypedValue( + getEvaluationContext(), new TypedValue(result), expectedResultType); + } + } + catch (Throwable ex) { + // If running in mixed mode, revert to interpreted + if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) { + this.compiledAst = null; + this.interpretedCount.set(0); + } + else { + // Running in SpelCompilerMode.immediate mode - propagate exception to caller + throw new SpelEvaluationException(ex, SpelMessage.EXCEPTION_RUNNING_COMPILED_EXPRESSION); + } + } + } + + ExpressionState expressionState = + new ExpressionState(getEvaluationContext(), toTypedValue(rootObject), this.configuration); + TypedValue typedResultValue = this.ast.getTypedValue(expressionState); + checkCompile(expressionState); + return ExpressionUtils.convertTypedValue( + expressionState.getEvaluationContext(), typedResultValue, expectedResultType); + } + + @Override + @Nullable + public Object getValue(EvaluationContext context) throws EvaluationException { + Assert.notNull(context, "EvaluationContext is required"); + + CompiledExpression compiledAst = this.compiledAst; + if (compiledAst != null) { + try { + return compiledAst.getValue(context.getRootObject().getValue(), context); + } + catch (Throwable ex) { + // If running in mixed mode, revert to interpreted + if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) { + this.compiledAst = null; + this.interpretedCount.set(0); + } + else { + // Running in SpelCompilerMode.immediate mode - propagate exception to caller + throw new SpelEvaluationException(ex, SpelMessage.EXCEPTION_RUNNING_COMPILED_EXPRESSION); + } + } + } + + ExpressionState expressionState = new ExpressionState(context, this.configuration); + Object result = this.ast.getValue(expressionState); + checkCompile(expressionState); + return result; + } + + @SuppressWarnings("unchecked") + @Override + @Nullable + public T getValue(EvaluationContext context, @Nullable Class expectedResultType) throws EvaluationException { + Assert.notNull(context, "EvaluationContext is required"); + + CompiledExpression compiledAst = this.compiledAst; + if (compiledAst != null) { + try { + Object result = compiledAst.getValue(context.getRootObject().getValue(), context); + if (expectedResultType != null) { + return ExpressionUtils.convertTypedValue(context, new TypedValue(result), expectedResultType); + } + else { + return (T) result; + } + } + catch (Throwable ex) { + // If running in mixed mode, revert to interpreted + if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) { + this.compiledAst = null; + this.interpretedCount.set(0); + } + else { + // Running in SpelCompilerMode.immediate mode - propagate exception to caller + throw new SpelEvaluationException(ex, SpelMessage.EXCEPTION_RUNNING_COMPILED_EXPRESSION); + } + } + } + + ExpressionState expressionState = new ExpressionState(context, this.configuration); + TypedValue typedResultValue = this.ast.getTypedValue(expressionState); + checkCompile(expressionState); + return ExpressionUtils.convertTypedValue(context, typedResultValue, expectedResultType); + } + + @Override + @Nullable + public Object getValue(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException { + Assert.notNull(context, "EvaluationContext is required"); + + CompiledExpression compiledAst = this.compiledAst; + if (compiledAst != null) { + try { + return compiledAst.getValue(rootObject, context); + } + catch (Throwable ex) { + // If running in mixed mode, revert to interpreted + if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) { + this.compiledAst = null; + this.interpretedCount.set(0); + } + else { + // Running in SpelCompilerMode.immediate mode - propagate exception to caller + throw new SpelEvaluationException(ex, SpelMessage.EXCEPTION_RUNNING_COMPILED_EXPRESSION); + } + } + } + + ExpressionState expressionState = new ExpressionState(context, toTypedValue(rootObject), this.configuration); + Object result = this.ast.getValue(expressionState); + checkCompile(expressionState); + return result; + } + + @SuppressWarnings("unchecked") + @Override + @Nullable + public T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable Class expectedResultType) + throws EvaluationException { + + Assert.notNull(context, "EvaluationContext is required"); + + CompiledExpression compiledAst = this.compiledAst; + if (compiledAst != null) { + try { + Object result = compiledAst.getValue(rootObject, context); + if (expectedResultType != null) { + return ExpressionUtils.convertTypedValue(context, new TypedValue(result), expectedResultType); + } + else { + return (T) result; + } + } + catch (Throwable ex) { + // If running in mixed mode, revert to interpreted + if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) { + this.compiledAst = null; + this.interpretedCount.set(0); + } + else { + // Running in SpelCompilerMode.immediate mode - propagate exception to caller + throw new SpelEvaluationException(ex, SpelMessage.EXCEPTION_RUNNING_COMPILED_EXPRESSION); + } + } + } + + ExpressionState expressionState = new ExpressionState(context, toTypedValue(rootObject), this.configuration); + TypedValue typedResultValue = this.ast.getTypedValue(expressionState); + checkCompile(expressionState); + return ExpressionUtils.convertTypedValue(context, typedResultValue, expectedResultType); + } + + @Override + @Nullable + public Class getValueType() throws EvaluationException { + return getValueType(getEvaluationContext()); + } + + @Override + @Nullable + public Class getValueType(@Nullable Object rootObject) throws EvaluationException { + return getValueType(getEvaluationContext(), rootObject); + } + + @Override + @Nullable + public Class getValueType(EvaluationContext context) throws EvaluationException { + Assert.notNull(context, "EvaluationContext is required"); + ExpressionState expressionState = new ExpressionState(context, this.configuration); + TypeDescriptor typeDescriptor = this.ast.getValueInternal(expressionState).getTypeDescriptor(); + return (typeDescriptor != null ? typeDescriptor.getType() : null); + } + + @Override + @Nullable + public Class getValueType(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException { + ExpressionState expressionState = new ExpressionState(context, toTypedValue(rootObject), this.configuration); + TypeDescriptor typeDescriptor = this.ast.getValueInternal(expressionState).getTypeDescriptor(); + return (typeDescriptor != null ? typeDescriptor.getType() : null); + } + + @Override + @Nullable + public TypeDescriptor getValueTypeDescriptor() throws EvaluationException { + return getValueTypeDescriptor(getEvaluationContext()); + } + + @Override + @Nullable + public TypeDescriptor getValueTypeDescriptor(@Nullable Object rootObject) throws EvaluationException { + ExpressionState expressionState = + new ExpressionState(getEvaluationContext(), toTypedValue(rootObject), this.configuration); + return this.ast.getValueInternal(expressionState).getTypeDescriptor(); + } + + @Override + @Nullable + public TypeDescriptor getValueTypeDescriptor(EvaluationContext context) throws EvaluationException { + Assert.notNull(context, "EvaluationContext is required"); + ExpressionState expressionState = new ExpressionState(context, this.configuration); + return this.ast.getValueInternal(expressionState).getTypeDescriptor(); + } + + @Override + @Nullable + public TypeDescriptor getValueTypeDescriptor(EvaluationContext context, @Nullable Object rootObject) + throws EvaluationException { + + Assert.notNull(context, "EvaluationContext is required"); + ExpressionState expressionState = new ExpressionState(context, toTypedValue(rootObject), this.configuration); + return this.ast.getValueInternal(expressionState).getTypeDescriptor(); + } + + @Override + public boolean isWritable(@Nullable Object rootObject) throws EvaluationException { + return this.ast.isWritable( + new ExpressionState(getEvaluationContext(), toTypedValue(rootObject), this.configuration)); + } + + @Override + public boolean isWritable(EvaluationContext context) throws EvaluationException { + Assert.notNull(context, "EvaluationContext is required"); + return this.ast.isWritable(new ExpressionState(context, this.configuration)); + } + + @Override + public boolean isWritable(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException { + Assert.notNull(context, "EvaluationContext is required"); + return this.ast.isWritable(new ExpressionState(context, toTypedValue(rootObject), this.configuration)); + } + + @Override + public void setValue(@Nullable Object rootObject, @Nullable Object value) throws EvaluationException { + this.ast.setValue( + new ExpressionState(getEvaluationContext(), toTypedValue(rootObject), this.configuration), value); + } + + @Override + public void setValue(EvaluationContext context, @Nullable Object value) throws EvaluationException { + Assert.notNull(context, "EvaluationContext is required"); + this.ast.setValue(new ExpressionState(context, this.configuration), value); + } + + @Override + public void setValue(EvaluationContext context, @Nullable Object rootObject, @Nullable Object value) + throws EvaluationException { + + Assert.notNull(context, "EvaluationContext is required"); + this.ast.setValue(new ExpressionState(context, toTypedValue(rootObject), this.configuration), value); + } + + + /** + * Compile the expression if it has been evaluated more than the threshold number + * of times to trigger compilation. + * @param expressionState the expression state used to determine compilation mode + */ + private void checkCompile(ExpressionState expressionState) { + this.interpretedCount.incrementAndGet(); + SpelCompilerMode compilerMode = expressionState.getConfiguration().getCompilerMode(); + if (compilerMode != SpelCompilerMode.OFF) { + if (compilerMode == SpelCompilerMode.IMMEDIATE) { + if (this.interpretedCount.get() > 1) { + compileExpression(); + } + } + else { + // compilerMode = SpelCompilerMode.MIXED + if (this.interpretedCount.get() > INTERPRETED_COUNT_THRESHOLD) { + compileExpression(); + } + } + } + } + + /** + * Perform expression compilation. This will only succeed once exit descriptors for + * all nodes have been determined. If the compilation fails and has failed more than + * 100 times the expression is no longer considered suitable for compilation. + * @return whether this expression has been successfully compiled + */ + public boolean compileExpression() { + CompiledExpression compiledAst = this.compiledAst; + if (compiledAst != null) { + // Previously compiled + return true; + } + if (this.failedAttempts.get() > FAILED_ATTEMPTS_THRESHOLD) { + // Don't try again + return false; + } + + synchronized (this) { + if (this.compiledAst != null) { + // Compiled by another thread before this thread got into the sync block + return true; + } + SpelCompiler compiler = SpelCompiler.getCompiler(this.configuration.getCompilerClassLoader()); + compiledAst = compiler.compile(this.ast); + if (compiledAst != null) { + // Successfully compiled + this.compiledAst = compiledAst; + return true; + } + else { + // Failed to compile + this.failedAttempts.incrementAndGet(); + return false; + } + } + } + + /** + * Cause an expression to revert to being interpreted if it has been using a compiled + * form. It also resets the compilation attempt failure count (an expression is normally no + * longer considered compilable if it cannot be compiled after 100 attempts). + */ + public void revertToInterpreted() { + this.compiledAst = null; + this.interpretedCount.set(0); + this.failedAttempts.set(0); + } + + /** + * Return the Abstract Syntax Tree for the expression. + */ + public SpelNode getAST() { + return this.ast; + } + + /** + * Produce a string representation of the Abstract Syntax Tree for the expression. + * This should ideally look like the input expression, but properly formatted since any + * unnecessary whitespace will have been discarded during the parse of the expression. + * @return the string representation of the AST + */ + public String toStringAST() { + return this.ast.toStringAST(); + } + + private TypedValue toTypedValue(@Nullable Object object) { + return (object != null ? new TypedValue(object) : TypedValue.NULL); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpressionParser.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpressionParser.java new file mode 100644 index 0000000..a8702b4 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpressionParser.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.standard; + +import org.springframework.expression.ParseException; +import org.springframework.expression.ParserContext; +import org.springframework.expression.common.TemplateAwareExpressionParser; +import org.springframework.expression.spel.SpelParserConfiguration; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * SpEL parser. Instances are reusable and thread-safe. + * + * @author Andy Clement + * @author Juergen Hoeller + * @since 3.0 + */ +public class SpelExpressionParser extends TemplateAwareExpressionParser { + + private final SpelParserConfiguration configuration; + + + /** + * Create a parser with default settings. + */ + public SpelExpressionParser() { + this.configuration = new SpelParserConfiguration(); + } + + /** + * Create a parser with the specified configuration. + * @param configuration custom configuration options + */ + public SpelExpressionParser(SpelParserConfiguration configuration) { + Assert.notNull(configuration, "SpelParserConfiguration must not be null"); + this.configuration = configuration; + } + + + public SpelExpression parseRaw(String expressionString) throws ParseException { + return doParseExpression(expressionString, null); + } + + @Override + protected SpelExpression doParseExpression(String expressionString, @Nullable ParserContext context) throws ParseException { + return new InternalSpelExpressionParser(this.configuration).doParseExpression(expressionString, context); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/Token.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/Token.java new file mode 100644 index 0000000..21ab3e5 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/Token.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.standard; + +import org.springframework.lang.Nullable; + +/** + * Holder for a kind of token, the associated data and its position in the input data + * stream (start/end). + * + * @author Andy Clement + * @since 3.0 + */ +class Token { + + TokenKind kind; + + @Nullable + String data; + + int startPos; // index of first character + + int endPos; // index of char after the last character + + + /** + * Constructor for use when there is no particular data for the token + * (e.g. TRUE or '+') + * @param startPos the exact start + * @param endPos the index to the last character + */ + Token(TokenKind tokenKind, int startPos, int endPos) { + this.kind = tokenKind; + this.startPos = startPos; + this.endPos = endPos; + } + + Token(TokenKind tokenKind, char[] tokenData, int startPos, int endPos) { + this(tokenKind, startPos, endPos); + this.data = new String(tokenData); + } + + + public TokenKind getKind() { + return this.kind; + } + + public boolean isIdentifier() { + return (this.kind == TokenKind.IDENTIFIER); + } + + public boolean isNumericRelationalOperator() { + return (this.kind == TokenKind.GT || this.kind == TokenKind.GE || this.kind == TokenKind.LT || + this.kind == TokenKind.LE || this.kind==TokenKind.EQ || this.kind==TokenKind.NE); + } + + public String stringValue() { + return (this.data != null ? this.data : ""); + } + + public Token asInstanceOfToken() { + return new Token(TokenKind.INSTANCEOF, this.startPos, this.endPos); + } + + public Token asMatchesToken() { + return new Token(TokenKind.MATCHES, this.startPos, this.endPos); + } + + public Token asBetweenToken() { + return new Token(TokenKind.BETWEEN, this.startPos, this.endPos); + } + + + @Override + public String toString() { + StringBuilder s = new StringBuilder(); + s.append("[").append(this.kind.toString()); + if (this.kind.hasPayload()) { + s.append(":").append(this.data); + } + s.append("]"); + s.append("(").append(this.startPos).append(",").append(this.endPos).append(")"); + return s.toString(); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/TokenKind.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/TokenKind.java new file mode 100644 index 0000000..82723cf --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/TokenKind.java @@ -0,0 +1,154 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.standard; + +/** + * Token Kinds. + * + * @author Andy Clement + * @since 3.0 + */ +enum TokenKind { + + // ordered by priority - operands first + + LITERAL_INT, + + LITERAL_LONG, + + LITERAL_HEXINT, + + LITERAL_HEXLONG, + + LITERAL_STRING, + + LITERAL_REAL, + + LITERAL_REAL_FLOAT, + + LPAREN("("), + + RPAREN(")"), + + COMMA(","), + + IDENTIFIER, + + COLON(":"), + + HASH("#"), + + RSQUARE("]"), + + LSQUARE("["), + + LCURLY("{"), + + RCURLY("}"), + + DOT("."), + + PLUS("+"), + + STAR("*"), + + MINUS("-"), + + SELECT_FIRST("^["), + + SELECT_LAST("$["), + + QMARK("?"), + + PROJECT("!["), + + DIV("/"), + + GE(">="), + + GT(">"), + + LE("<="), + + LT("<"), + + EQ("=="), + + NE("!="), + + MOD("%"), + + NOT("!"), + + ASSIGN("="), + + INSTANCEOF("instanceof"), + + MATCHES("matches"), + + BETWEEN("between"), + + SELECT("?["), + + POWER("^"), + + ELVIS("?:"), + + SAFE_NAVI("?."), + + BEAN_REF("@"), + + FACTORY_BEAN_REF("&"), + + SYMBOLIC_OR("||"), + + SYMBOLIC_AND("&&"), + + INC("++"), + + DEC("--"); + + + final char[] tokenChars; + + private final boolean hasPayload; // is there more to this token than simply the kind + + + private TokenKind(String tokenString) { + this.tokenChars = tokenString.toCharArray(); + this.hasPayload = (this.tokenChars.length == 0); + } + + private TokenKind() { + this(""); + } + + + @Override + public String toString() { + return (name() + (this.tokenChars.length !=0 ? "(" + new String(this.tokenChars) +")" : "")); + } + + public boolean hasPayload() { + return this.hasPayload; + } + + public int getLength() { + return this.tokenChars.length; + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/Tokenizer.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/Tokenizer.java new file mode 100644 index 0000000..5034c96 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/Tokenizer.java @@ -0,0 +1,591 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.standard; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.expression.spel.InternalParseException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.SpelParseException; + +/** + * Lex some input data into a stream of tokens that can then be parsed. + * + * @author Andy Clement + * @author Juergen Hoeller + * @author Phillip Webb + * @since 3.0 + */ +class Tokenizer { + + // If this gets changed, it must remain sorted... + private static final String[] ALTERNATIVE_OPERATOR_NAMES = + {"DIV", "EQ", "GE", "GT", "LE", "LT", "MOD", "NE", "NOT"}; + + private static final byte[] FLAGS = new byte[256]; + + private static final byte IS_DIGIT = 0x01; + + private static final byte IS_HEXDIGIT = 0x02; + + private static final byte IS_ALPHA = 0x04; + + static { + for (int ch = '0'; ch <= '9'; ch++) { + FLAGS[ch] |= IS_DIGIT | IS_HEXDIGIT; + } + for (int ch = 'A'; ch <= 'F'; ch++) { + FLAGS[ch] |= IS_HEXDIGIT; + } + for (int ch = 'a'; ch <= 'f'; ch++) { + FLAGS[ch] |= IS_HEXDIGIT; + } + for (int ch = 'A'; ch <= 'Z'; ch++) { + FLAGS[ch] |= IS_ALPHA; + } + for (int ch = 'a'; ch <= 'z'; ch++) { + FLAGS[ch] |= IS_ALPHA; + } + } + + + private String expressionString; + + private char[] charsToProcess; + + private int pos; + + private int max; + + private List tokens = new ArrayList<>(); + + + public Tokenizer(String inputData) { + this.expressionString = inputData; + this.charsToProcess = (inputData + "\0").toCharArray(); + this.max = this.charsToProcess.length; + this.pos = 0; + } + + + public List process() { + while (this.pos < this.max) { + char ch = this.charsToProcess[this.pos]; + if (isAlphabetic(ch)) { + lexIdentifier(); + } + else { + switch (ch) { + case '+': + if (isTwoCharToken(TokenKind.INC)) { + pushPairToken(TokenKind.INC); + } + else { + pushCharToken(TokenKind.PLUS); + } + break; + case '_': // the other way to start an identifier + lexIdentifier(); + break; + case '-': + if (isTwoCharToken(TokenKind.DEC)) { + pushPairToken(TokenKind.DEC); + } + else { + pushCharToken(TokenKind.MINUS); + } + break; + case ':': + pushCharToken(TokenKind.COLON); + break; + case '.': + pushCharToken(TokenKind.DOT); + break; + case ',': + pushCharToken(TokenKind.COMMA); + break; + case '*': + pushCharToken(TokenKind.STAR); + break; + case '/': + pushCharToken(TokenKind.DIV); + break; + case '%': + pushCharToken(TokenKind.MOD); + break; + case '(': + pushCharToken(TokenKind.LPAREN); + break; + case ')': + pushCharToken(TokenKind.RPAREN); + break; + case '[': + pushCharToken(TokenKind.LSQUARE); + break; + case '#': + pushCharToken(TokenKind.HASH); + break; + case ']': + pushCharToken(TokenKind.RSQUARE); + break; + case '{': + pushCharToken(TokenKind.LCURLY); + break; + case '}': + pushCharToken(TokenKind.RCURLY); + break; + case '@': + pushCharToken(TokenKind.BEAN_REF); + break; + case '^': + if (isTwoCharToken(TokenKind.SELECT_FIRST)) { + pushPairToken(TokenKind.SELECT_FIRST); + } + else { + pushCharToken(TokenKind.POWER); + } + break; + case '!': + if (isTwoCharToken(TokenKind.NE)) { + pushPairToken(TokenKind.NE); + } + else if (isTwoCharToken(TokenKind.PROJECT)) { + pushPairToken(TokenKind.PROJECT); + } + else { + pushCharToken(TokenKind.NOT); + } + break; + case '=': + if (isTwoCharToken(TokenKind.EQ)) { + pushPairToken(TokenKind.EQ); + } + else { + pushCharToken(TokenKind.ASSIGN); + } + break; + case '&': + if (isTwoCharToken(TokenKind.SYMBOLIC_AND)) { + pushPairToken(TokenKind.SYMBOLIC_AND); + } + else { + pushCharToken(TokenKind.FACTORY_BEAN_REF); + } + break; + case '|': + if (!isTwoCharToken(TokenKind.SYMBOLIC_OR)) { + raiseParseException(this.pos, SpelMessage.MISSING_CHARACTER, "|"); + } + pushPairToken(TokenKind.SYMBOLIC_OR); + break; + case '?': + if (isTwoCharToken(TokenKind.SELECT)) { + pushPairToken(TokenKind.SELECT); + } + else if (isTwoCharToken(TokenKind.ELVIS)) { + pushPairToken(TokenKind.ELVIS); + } + else if (isTwoCharToken(TokenKind.SAFE_NAVI)) { + pushPairToken(TokenKind.SAFE_NAVI); + } + else { + pushCharToken(TokenKind.QMARK); + } + break; + case '$': + if (isTwoCharToken(TokenKind.SELECT_LAST)) { + pushPairToken(TokenKind.SELECT_LAST); + } + else { + lexIdentifier(); + } + break; + case '>': + if (isTwoCharToken(TokenKind.GE)) { + pushPairToken(TokenKind.GE); + } + else { + pushCharToken(TokenKind.GT); + } + break; + case '<': + if (isTwoCharToken(TokenKind.LE)) { + pushPairToken(TokenKind.LE); + } + else { + pushCharToken(TokenKind.LT); + } + break; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + lexNumericLiteral(ch == '0'); + break; + case ' ': + case '\t': + case '\r': + case '\n': + // drift over white space + this.pos++; + break; + case '\'': + lexQuotedStringLiteral(); + break; + case '"': + lexDoubleQuotedStringLiteral(); + break; + case 0: + // hit sentinel at end of value + this.pos++; // will take us to the end + break; + case '\\': + raiseParseException(this.pos, SpelMessage.UNEXPECTED_ESCAPE_CHAR); + break; + default: + throw new IllegalStateException("Cannot handle (" + (int) ch + ") '" + ch + "'"); + } + } + } + return this.tokens; + } + + + // STRING_LITERAL: '\''! (APOS|~'\'')* '\''!; + private void lexQuotedStringLiteral() { + int start = this.pos; + boolean terminated = false; + while (!terminated) { + this.pos++; + char ch = this.charsToProcess[this.pos]; + if (ch == '\'') { + // may not be the end if the char after is also a ' + if (this.charsToProcess[this.pos + 1] == '\'') { + this.pos++; // skip over that too, and continue + } + else { + terminated = true; + } + } + if (isExhausted()) { + raiseParseException(start, SpelMessage.NON_TERMINATING_QUOTED_STRING); + } + } + this.pos++; + this.tokens.add(new Token(TokenKind.LITERAL_STRING, subarray(start, this.pos), start, this.pos)); + } + + // DQ_STRING_LITERAL: '"'! (~'"')* '"'!; + private void lexDoubleQuotedStringLiteral() { + int start = this.pos; + boolean terminated = false; + while (!terminated) { + this.pos++; + char ch = this.charsToProcess[this.pos]; + if (ch == '"') { + // may not be the end if the char after is also a " + if (this.charsToProcess[this.pos + 1] == '"') { + this.pos++; // skip over that too, and continue + } + else { + terminated = true; + } + } + if (isExhausted()) { + raiseParseException(start, SpelMessage.NON_TERMINATING_DOUBLE_QUOTED_STRING); + } + } + this.pos++; + this.tokens.add(new Token(TokenKind.LITERAL_STRING, subarray(start, this.pos), start, this.pos)); + } + + // REAL_LITERAL : + // ('.' (DECIMAL_DIGIT)+ (EXPONENT_PART)? (REAL_TYPE_SUFFIX)?) | + // ((DECIMAL_DIGIT)+ '.' (DECIMAL_DIGIT)+ (EXPONENT_PART)? (REAL_TYPE_SUFFIX)?) | + // ((DECIMAL_DIGIT)+ (EXPONENT_PART) (REAL_TYPE_SUFFIX)?) | + // ((DECIMAL_DIGIT)+ (REAL_TYPE_SUFFIX)); + // fragment INTEGER_TYPE_SUFFIX : ( 'L' | 'l' ); + // fragment HEX_DIGIT : + // '0'|'1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'|'A'|'B'|'C'|'D'|'E'|'F'|'a'|'b'|'c'|'d'|'e'|'f'; + // + // fragment EXPONENT_PART : 'e' (SIGN)* (DECIMAL_DIGIT)+ | 'E' (SIGN)* + // (DECIMAL_DIGIT)+ ; + // fragment SIGN : '+' | '-' ; + // fragment REAL_TYPE_SUFFIX : 'F' | 'f' | 'D' | 'd'; + // INTEGER_LITERAL + // : (DECIMAL_DIGIT)+ (INTEGER_TYPE_SUFFIX)?; + + private void lexNumericLiteral(boolean firstCharIsZero) { + boolean isReal = false; + int start = this.pos; + char ch = this.charsToProcess[this.pos + 1]; + boolean isHex = ch == 'x' || ch == 'X'; + + // deal with hexadecimal + if (firstCharIsZero && isHex) { + this.pos = this.pos + 1; + do { + this.pos++; + } + while (isHexadecimalDigit(this.charsToProcess[this.pos])); + if (isChar('L', 'l')) { + pushHexIntToken(subarray(start + 2, this.pos), true, start, this.pos); + this.pos++; + } + else { + pushHexIntToken(subarray(start + 2, this.pos), false, start, this.pos); + } + return; + } + + // real numbers must have leading digits + + // Consume first part of number + do { + this.pos++; + } + while (isDigit(this.charsToProcess[this.pos])); + + // a '.' indicates this number is a real + ch = this.charsToProcess[this.pos]; + if (ch == '.') { + isReal = true; + int dotpos = this.pos; + // carry on consuming digits + do { + this.pos++; + } + while (isDigit(this.charsToProcess[this.pos])); + if (this.pos == dotpos + 1) { + // the number is something like '3.'. It is really an int but may be + // part of something like '3.toString()'. In this case process it as + // an int and leave the dot as a separate token. + this.pos = dotpos; + pushIntToken(subarray(start, this.pos), false, start, this.pos); + return; + } + } + + int endOfNumber = this.pos; + + // Now there may or may not be an exponent + + // Is it a long ? + if (isChar('L', 'l')) { + if (isReal) { // 3.4L - not allowed + raiseParseException(start, SpelMessage.REAL_CANNOT_BE_LONG); + } + pushIntToken(subarray(start, endOfNumber), true, start, endOfNumber); + this.pos++; + } + else if (isExponentChar(this.charsToProcess[this.pos])) { + isReal = true; // if it wasn't before, it is now + this.pos++; + char possibleSign = this.charsToProcess[this.pos]; + if (isSign(possibleSign)) { + this.pos++; + } + + // exponent digits + do { + this.pos++; + } + while (isDigit(this.charsToProcess[this.pos])); + boolean isFloat = false; + if (isFloatSuffix(this.charsToProcess[this.pos])) { + isFloat = true; + endOfNumber = ++this.pos; + } + else if (isDoubleSuffix(this.charsToProcess[this.pos])) { + endOfNumber = ++this.pos; + } + pushRealToken(subarray(start, this.pos), isFloat, start, this.pos); + } + else { + ch = this.charsToProcess[this.pos]; + boolean isFloat = false; + if (isFloatSuffix(ch)) { + isReal = true; + isFloat = true; + endOfNumber = ++this.pos; + } + else if (isDoubleSuffix(ch)) { + isReal = true; + endOfNumber = ++this.pos; + } + if (isReal) { + pushRealToken(subarray(start, endOfNumber), isFloat, start, endOfNumber); + } + else { + pushIntToken(subarray(start, endOfNumber), false, start, endOfNumber); + } + } + } + + private void lexIdentifier() { + int start = this.pos; + do { + this.pos++; + } + while (isIdentifier(this.charsToProcess[this.pos])); + char[] subarray = subarray(start, this.pos); + + // Check if this is the alternative (textual) representation of an operator (see + // alternativeOperatorNames) + if ((this.pos - start) == 2 || (this.pos - start) == 3) { + String asString = new String(subarray).toUpperCase(); + int idx = Arrays.binarySearch(ALTERNATIVE_OPERATOR_NAMES, asString); + if (idx >= 0) { + pushOneCharOrTwoCharToken(TokenKind.valueOf(asString), start, subarray); + return; + } + } + this.tokens.add(new Token(TokenKind.IDENTIFIER, subarray, start, this.pos)); + } + + private void pushIntToken(char[] data, boolean isLong, int start, int end) { + if (isLong) { + this.tokens.add(new Token(TokenKind.LITERAL_LONG, data, start, end)); + } + else { + this.tokens.add(new Token(TokenKind.LITERAL_INT, data, start, end)); + } + } + + private void pushHexIntToken(char[] data, boolean isLong, int start, int end) { + if (data.length == 0) { + if (isLong) { + raiseParseException(start, SpelMessage.NOT_A_LONG, this.expressionString.substring(start, end + 1)); + } + else { + raiseParseException(start, SpelMessage.NOT_AN_INTEGER, this.expressionString.substring(start, end)); + } + } + if (isLong) { + this.tokens.add(new Token(TokenKind.LITERAL_HEXLONG, data, start, end)); + } + else { + this.tokens.add(new Token(TokenKind.LITERAL_HEXINT, data, start, end)); + } + } + + private void pushRealToken(char[] data, boolean isFloat, int start, int end) { + if (isFloat) { + this.tokens.add(new Token(TokenKind.LITERAL_REAL_FLOAT, data, start, end)); + } + else { + this.tokens.add(new Token(TokenKind.LITERAL_REAL, data, start, end)); + } + } + + private char[] subarray(int start, int end) { + return Arrays.copyOfRange(this.charsToProcess, start, end); + } + + /** + * Check if this might be a two character token. + */ + private boolean isTwoCharToken(TokenKind kind) { + return (kind.tokenChars.length == 2 && + this.charsToProcess[this.pos] == kind.tokenChars[0] && + this.charsToProcess[this.pos + 1] == kind.tokenChars[1]); + } + + /** + * Push a token of just one character in length. + */ + private void pushCharToken(TokenKind kind) { + this.tokens.add(new Token(kind, this.pos, this.pos + 1)); + this.pos++; + } + + /** + * Push a token of two characters in length. + */ + private void pushPairToken(TokenKind kind) { + this.tokens.add(new Token(kind, this.pos, this.pos + 2)); + this.pos += 2; + } + + private void pushOneCharOrTwoCharToken(TokenKind kind, int pos, char[] data) { + this.tokens.add(new Token(kind, data, pos, pos + kind.getLength())); + } + + // ID: ('a'..'z'|'A'..'Z'|'_'|'$') ('a'..'z'|'A'..'Z'|'_'|'$'|'0'..'9'|DOT_ESCAPED)*; + private boolean isIdentifier(char ch) { + return isAlphabetic(ch) || isDigit(ch) || ch == '_' || ch == '$'; + } + + private boolean isChar(char a, char b) { + char ch = this.charsToProcess[this.pos]; + return ch == a || ch == b; + } + + private boolean isExponentChar(char ch) { + return ch == 'e' || ch == 'E'; + } + + private boolean isFloatSuffix(char ch) { + return ch == 'f' || ch == 'F'; + } + + private boolean isDoubleSuffix(char ch) { + return ch == 'd' || ch == 'D'; + } + + private boolean isSign(char ch) { + return ch == '+' || ch == '-'; + } + + private boolean isDigit(char ch) { + if (ch > 255) { + return false; + } + return (FLAGS[ch] & IS_DIGIT) != 0; + } + + private boolean isAlphabetic(char ch) { + if (ch > 255) { + return false; + } + return (FLAGS[ch] & IS_ALPHA) != 0; + } + + private boolean isHexadecimalDigit(char ch) { + if (ch > 255) { + return false; + } + return (FLAGS[ch] & IS_HEXDIGIT) != 0; + } + + private boolean isExhausted() { + return (this.pos == this.max - 1); + } + + private void raiseParseException(int start, SpelMessage msg, Object... inserts) { + throw new InternalParseException(new SpelParseException(this.expressionString, start, msg, inserts)); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/package-info.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/package-info.java new file mode 100644 index 0000000..e26fe15 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/package-info.java @@ -0,0 +1,9 @@ +/** + * SpEL's standard parser implementation. + */ +@NonNullApi +@NonNullFields +package org.springframework.expression.spel.standard; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/BooleanTypedValue.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/BooleanTypedValue.java new file mode 100644 index 0000000..a6f476e --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/BooleanTypedValue.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.support; + +import org.springframework.expression.TypedValue; + +/** + * A {@link TypedValue} for booleans. + * + * @author Andy Clement + * @since 3.0 + */ +public final class BooleanTypedValue extends TypedValue { + + /** + * True. + */ + public static final BooleanTypedValue TRUE = new BooleanTypedValue(true); + + /** + * False. + */ + public static final BooleanTypedValue FALSE = new BooleanTypedValue(false); + + + private BooleanTypedValue(boolean b) { + super(b); + } + + + public static BooleanTypedValue forValue(boolean b) { + return (b ? TRUE : FALSE); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/DataBindingMethodResolver.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/DataBindingMethodResolver.java new file mode 100644 index 0000000..36dbeb1 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/DataBindingMethodResolver.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.support; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.List; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.MethodExecutor; +import org.springframework.lang.Nullable; + +/** + * A {@link org.springframework.expression.MethodResolver} variant for data binding + * purposes, using reflection to access instance methods on a given target object. + * + *

    This accessor does not resolve static methods and also no technical methods + * on {@code java.lang.Object} or {@code java.lang.Class}. + * For unrestricted resolution, choose {@link ReflectiveMethodResolver} instead. + * + * @author Juergen Hoeller + * @since 4.3.15 + * @see #forInstanceMethodInvocation() + * @see DataBindingPropertyAccessor + */ +public final class DataBindingMethodResolver extends ReflectiveMethodResolver { + + private DataBindingMethodResolver() { + super(); + } + + @Override + @Nullable + public MethodExecutor resolve(EvaluationContext context, Object targetObject, String name, + List argumentTypes) throws AccessException { + + if (targetObject instanceof Class) { + throw new IllegalArgumentException("DataBindingMethodResolver does not support Class targets"); + } + return super.resolve(context, targetObject, name, argumentTypes); + } + + @Override + protected boolean isCandidateForInvocation(Method method, Class targetClass) { + if (Modifier.isStatic(method.getModifiers())) { + return false; + } + Class clazz = method.getDeclaringClass(); + return (clazz != Object.class && clazz != Class.class && !ClassLoader.class.isAssignableFrom(targetClass)); + } + + + /** + * Create a new data-binding method resolver for instance method resolution. + */ + public static DataBindingMethodResolver forInstanceMethodInvocation() { + return new DataBindingMethodResolver(); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/DataBindingPropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/DataBindingPropertyAccessor.java new file mode 100644 index 0000000..6dbe01b --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/DataBindingPropertyAccessor.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.support; + +import java.lang.reflect.Method; + +/** + * A {@link org.springframework.expression.PropertyAccessor} variant for data binding + * purposes, using reflection to access properties for reading and possibly writing. + * + *

    A property can be referenced through a public getter method (when being read) + * or a public setter method (when being written), and also as a public field. + * + *

    This accessor is explicitly designed for user-declared properties and does not + * resolve technical properties on {@code java.lang.Object} or {@code java.lang.Class}. + * For unrestricted resolution, choose {@link ReflectivePropertyAccessor} instead. + * + * @author Juergen Hoeller + * @since 4.3.15 + * @see #forReadOnlyAccess() + * @see #forReadWriteAccess() + * @see SimpleEvaluationContext + * @see StandardEvaluationContext + * @see ReflectivePropertyAccessor + */ +public final class DataBindingPropertyAccessor extends ReflectivePropertyAccessor { + + /** + * Create a new property accessor for reading and possibly also writing. + * @param allowWrite whether to also allow for write operations + * @see #canWrite + */ + private DataBindingPropertyAccessor(boolean allowWrite) { + super(allowWrite); + } + + @Override + protected boolean isCandidateForProperty(Method method, Class targetClass) { + Class clazz = method.getDeclaringClass(); + return (clazz != Object.class && clazz != Class.class && !ClassLoader.class.isAssignableFrom(targetClass)); + } + + + /** + * Create a new data-binding property accessor for read-only operations. + */ + public static DataBindingPropertyAccessor forReadOnlyAccess() { + return new DataBindingPropertyAccessor(false); + } + + /** + * Create a new data-binding property accessor for read-write operations. + */ + public static DataBindingPropertyAccessor forReadWriteAccess() { + return new DataBindingPropertyAccessor(true); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java new file mode 100644 index 0000000..d8af411 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java @@ -0,0 +1,425 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.support; + +import java.lang.reflect.Array; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.util.List; + +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.TypeConverter; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.util.MethodInvoker; + +/** + * Utility methods used by the reflection resolver code to discover the appropriate + * methods/constructors and fields that should be used in expressions. + * + * @author Andy Clement + * @author Juergen Hoeller + * @since 3.0 + */ +public abstract class ReflectionHelper { + + /** + * Compare argument arrays and return information about whether they match. + * A supplied type converter and conversionAllowed flag allow for matches to take + * into account that a type may be transformed into a different type by the converter. + * @param expectedArgTypes the types the method/constructor is expecting + * @param suppliedArgTypes the types that are being supplied at the point of invocation + * @param typeConverter a registered type converter + * @return a MatchInfo object indicating what kind of match it was, + * or {@code null} if it was not a match + */ + @Nullable + static ArgumentsMatchInfo compareArguments( + List expectedArgTypes, List suppliedArgTypes, TypeConverter typeConverter) { + + Assert.isTrue(expectedArgTypes.size() == suppliedArgTypes.size(), + "Expected argument types and supplied argument types should be arrays of same length"); + + ArgumentsMatchKind match = ArgumentsMatchKind.EXACT; + for (int i = 0; i < expectedArgTypes.size() && match != null; i++) { + TypeDescriptor suppliedArg = suppliedArgTypes.get(i); + TypeDescriptor expectedArg = expectedArgTypes.get(i); + // The user may supply null - and that will be ok unless a primitive is expected + if (suppliedArg == null) { + if (expectedArg.isPrimitive()) { + match = null; + } + } + else if (!expectedArg.equals(suppliedArg)) { + if (suppliedArg.isAssignableTo(expectedArg)) { + if (match != ArgumentsMatchKind.REQUIRES_CONVERSION) { + match = ArgumentsMatchKind.CLOSE; + } + } + else if (typeConverter.canConvert(suppliedArg, expectedArg)) { + match = ArgumentsMatchKind.REQUIRES_CONVERSION; + } + else { + match = null; + } + } + } + return (match != null ? new ArgumentsMatchInfo(match) : null); + } + + /** + * Based on {@link MethodInvoker#getTypeDifferenceWeight(Class[], Object[])} but operates on TypeDescriptors. + */ + public static int getTypeDifferenceWeight(List paramTypes, List argTypes) { + int result = 0; + for (int i = 0; i < paramTypes.size(); i++) { + TypeDescriptor paramType = paramTypes.get(i); + TypeDescriptor argType = (i < argTypes.size() ? argTypes.get(i) : null); + if (argType == null) { + if (paramType.isPrimitive()) { + return Integer.MAX_VALUE; + } + } + else { + Class paramTypeClazz = paramType.getType(); + if (!ClassUtils.isAssignable(paramTypeClazz, argType.getType())) { + return Integer.MAX_VALUE; + } + if (paramTypeClazz.isPrimitive()) { + paramTypeClazz = Object.class; + } + Class superClass = argType.getType().getSuperclass(); + while (superClass != null) { + if (paramTypeClazz.equals(superClass)) { + result = result + 2; + superClass = null; + } + else if (ClassUtils.isAssignable(paramTypeClazz, superClass)) { + result = result + 2; + superClass = superClass.getSuperclass(); + } + else { + superClass = null; + } + } + if (paramTypeClazz.isInterface()) { + result = result + 1; + } + } + } + return result; + } + + /** + * Compare argument arrays and return information about whether they match. + * A supplied type converter and conversionAllowed flag allow for matches to + * take into account that a type may be transformed into a different type by the + * converter. This variant of compareArguments also allows for a varargs match. + * @param expectedArgTypes the types the method/constructor is expecting + * @param suppliedArgTypes the types that are being supplied at the point of invocation + * @param typeConverter a registered type converter + * @return a MatchInfo object indicating what kind of match it was, + * or {@code null} if it was not a match + */ + @Nullable + static ArgumentsMatchInfo compareArgumentsVarargs( + List expectedArgTypes, List suppliedArgTypes, TypeConverter typeConverter) { + + Assert.isTrue(!CollectionUtils.isEmpty(expectedArgTypes), + "Expected arguments must at least include one array (the varargs parameter)"); + Assert.isTrue(expectedArgTypes.get(expectedArgTypes.size() - 1).isArray(), + "Final expected argument should be array type (the varargs parameter)"); + + ArgumentsMatchKind match = ArgumentsMatchKind.EXACT; + + // Check up until the varargs argument: + + // Deal with the arguments up to 'expected number' - 1 (that is everything but the varargs argument) + int argCountUpToVarargs = expectedArgTypes.size() - 1; + for (int i = 0; i < argCountUpToVarargs && match != null; i++) { + TypeDescriptor suppliedArg = suppliedArgTypes.get(i); + TypeDescriptor expectedArg = expectedArgTypes.get(i); + if (suppliedArg == null) { + if (expectedArg.isPrimitive()) { + match = null; + } + } + else { + if (!expectedArg.equals(suppliedArg)) { + if (suppliedArg.isAssignableTo(expectedArg)) { + if (match != ArgumentsMatchKind.REQUIRES_CONVERSION) { + match = ArgumentsMatchKind.CLOSE; + } + } + else if (typeConverter.canConvert(suppliedArg, expectedArg)) { + match = ArgumentsMatchKind.REQUIRES_CONVERSION; + } + else { + match = null; + } + } + } + } + + // If already confirmed it cannot be a match, then return + if (match == null) { + return null; + } + + if (suppliedArgTypes.size() == expectedArgTypes.size() && + expectedArgTypes.get(expectedArgTypes.size() - 1).equals( + suppliedArgTypes.get(suppliedArgTypes.size() - 1))) { + // Special case: there is one parameter left and it is an array and it matches the varargs + // expected argument - that is a match, the caller has already built the array. Proceed with it. + } + else { + // Now... we have the final argument in the method we are checking as a match and we have 0 + // or more other arguments left to pass to it. + TypeDescriptor varargsDesc = expectedArgTypes.get(expectedArgTypes.size() - 1); + TypeDescriptor elementDesc = varargsDesc.getElementTypeDescriptor(); + Assert.state(elementDesc != null, "No element type"); + Class varargsParamType = elementDesc.getType(); + + // All remaining parameters must be of this type or convertible to this type + for (int i = expectedArgTypes.size() - 1; i < suppliedArgTypes.size(); i++) { + TypeDescriptor suppliedArg = suppliedArgTypes.get(i); + if (suppliedArg == null) { + if (varargsParamType.isPrimitive()) { + match = null; + } + } + else { + if (varargsParamType != suppliedArg.getType()) { + if (ClassUtils.isAssignable(varargsParamType, suppliedArg.getType())) { + if (match != ArgumentsMatchKind.REQUIRES_CONVERSION) { + match = ArgumentsMatchKind.CLOSE; + } + } + else if (typeConverter.canConvert(suppliedArg, TypeDescriptor.valueOf(varargsParamType))) { + match = ArgumentsMatchKind.REQUIRES_CONVERSION; + } + else { + match = null; + } + } + } + } + } + + return (match != null ? new ArgumentsMatchInfo(match) : null); + } + + + // TODO could do with more refactoring around argument handling and varargs + /** + * Convert a supplied set of arguments into the requested types. If the parameterTypes are related to + * a varargs method then the final entry in the parameterTypes array is going to be an array itself whose + * component type should be used as the conversion target for extraneous arguments. (For example, if the + * parameterTypes are {Integer, String[]} and the input arguments are {Integer, boolean, float} then both + * the boolean and float must be converted to strings). This method does *not* repackage the arguments + * into a form suitable for the varargs invocation - a subsequent call to setupArgumentsForVarargsInvocation handles that. + * @param converter the converter to use for type conversions + * @param arguments the arguments to convert to the requested parameter types + * @param method the target Method + * @return true if some kind of conversion occurred on the argument + * @throws SpelEvaluationException if there is a problem with conversion + */ + public static boolean convertAllArguments(TypeConverter converter, Object[] arguments, Method method) + throws SpelEvaluationException { + + Integer varargsPosition = (method.isVarArgs() ? method.getParameterCount() - 1 : null); + return convertArguments(converter, arguments, method, varargsPosition); + } + + /** + * Takes an input set of argument values and converts them to the types specified as the + * required parameter types. The arguments are converted 'in-place' in the input array. + * @param converter the type converter to use for attempting conversions + * @param arguments the actual arguments that need conversion + * @param executable the target Method or Constructor + * @param varargsPosition the known position of the varargs argument, if any + * ({@code null} if not varargs) + * @return {@code true} if some kind of conversion occurred on an argument + * @throws EvaluationException if a problem occurs during conversion + */ + static boolean convertArguments(TypeConverter converter, Object[] arguments, Executable executable, + @Nullable Integer varargsPosition) throws EvaluationException { + + boolean conversionOccurred = false; + if (varargsPosition == null) { + for (int i = 0; i < arguments.length; i++) { + TypeDescriptor targetType = new TypeDescriptor(MethodParameter.forExecutable(executable, i)); + Object argument = arguments[i]; + arguments[i] = converter.convertValue(argument, TypeDescriptor.forObject(argument), targetType); + conversionOccurred |= (argument != arguments[i]); + } + } + else { + // Convert everything up to the varargs position + for (int i = 0; i < varargsPosition; i++) { + TypeDescriptor targetType = new TypeDescriptor(MethodParameter.forExecutable(executable, i)); + Object argument = arguments[i]; + arguments[i] = converter.convertValue(argument, TypeDescriptor.forObject(argument), targetType); + conversionOccurred |= (argument != arguments[i]); + } + MethodParameter methodParam = MethodParameter.forExecutable(executable, varargsPosition); + if (varargsPosition == arguments.length - 1) { + // If the target is varargs and there is just one more argument + // then convert it here + TypeDescriptor targetType = new TypeDescriptor(methodParam); + Object argument = arguments[varargsPosition]; + TypeDescriptor sourceType = TypeDescriptor.forObject(argument); + arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetType); + // Three outcomes of that previous line: + // 1) the input argument was already compatible (ie. array of valid type) and nothing was done + // 2) the input argument was correct type but not in an array so it was made into an array + // 3) the input argument was the wrong type and got converted and put into an array + if (argument != arguments[varargsPosition] && + !isFirstEntryInArray(argument, arguments[varargsPosition])) { + conversionOccurred = true; // case 3 + } + } + else { + // Convert remaining arguments to the varargs element type + TypeDescriptor targetType = new TypeDescriptor(methodParam).getElementTypeDescriptor(); + Assert.state(targetType != null, "No element type"); + for (int i = varargsPosition; i < arguments.length; i++) { + Object argument = arguments[i]; + arguments[i] = converter.convertValue(argument, TypeDescriptor.forObject(argument), targetType); + conversionOccurred |= (argument != arguments[i]); + } + } + } + return conversionOccurred; + } + + /** + * Check if the supplied value is the first entry in the array represented by the possibleArray value. + * @param value the value to check for in the array + * @param possibleArray an array object that may have the supplied value as the first element + * @return true if the supplied value is the first entry in the array + */ + private static boolean isFirstEntryInArray(Object value, @Nullable Object possibleArray) { + if (possibleArray == null) { + return false; + } + Class type = possibleArray.getClass(); + if (!type.isArray() || Array.getLength(possibleArray) == 0 || + !ClassUtils.isAssignableValue(type.getComponentType(), value)) { + return false; + } + Object arrayValue = Array.get(possibleArray, 0); + return (type.getComponentType().isPrimitive() ? arrayValue.equals(value) : arrayValue == value); + } + + /** + * Package up the arguments so that they correctly match what is expected in parameterTypes. + * For example, if parameterTypes is {@code (int, String[])} because the second parameter + * was declared {@code String...}, then if arguments is {@code [1,"a","b"]} then it must be + * repackaged as {@code [1,new String[]{"a","b"}]} in order to match the expected types. + * @param requiredParameterTypes the types of the parameters for the invocation + * @param args the arguments to be setup ready for the invocation + * @return a repackaged array of arguments where any varargs setup has been done + */ + public static Object[] setupArgumentsForVarargsInvocation(Class[] requiredParameterTypes, Object... args) { + // Check if array already built for final argument + int parameterCount = requiredParameterTypes.length; + int argumentCount = args.length; + + // Check if repackaging is needed... + if (parameterCount != args.length || + requiredParameterTypes[parameterCount - 1] != + (args[argumentCount - 1] != null ? args[argumentCount - 1].getClass() : null)) { + + int arraySize = 0; // zero size array if nothing to pass as the varargs parameter + if (argumentCount >= parameterCount) { + arraySize = argumentCount - (parameterCount - 1); + } + + // Create an array for the varargs arguments + Object[] newArgs = new Object[parameterCount]; + System.arraycopy(args, 0, newArgs, 0, newArgs.length - 1); + + // Now sort out the final argument, which is the varargs one. Before entering this method, + // the arguments should have been converted to the box form of the required type. + Class componentType = requiredParameterTypes[parameterCount - 1].getComponentType(); + Object repackagedArgs = Array.newInstance(componentType, arraySize); + for (int i = 0; i < arraySize; i++) { + Array.set(repackagedArgs, i, args[parameterCount - 1 + i]); + } + newArgs[newArgs.length - 1] = repackagedArgs; + return newArgs; + } + return args; + } + + + /** + * Arguments match kinds. + */ + enum ArgumentsMatchKind { + + /** An exact match is where the parameter types exactly match what the method/constructor is expecting. */ + EXACT, + + /** A close match is where the parameter types either exactly match or are assignment-compatible. */ + CLOSE, + + /** A conversion match is where the type converter must be used to transform some of the parameter types. */ + REQUIRES_CONVERSION + } + + + /** + * An instance of ArgumentsMatchInfo describes what kind of match was achieved + * between two sets of arguments - the set that a method/constructor is expecting + * and the set that are being supplied at the point of invocation. If the kind + * indicates that conversion is required for some of the arguments then the arguments + * that require conversion are listed in the argsRequiringConversion array. + */ + static class ArgumentsMatchInfo { + + private final ArgumentsMatchKind kind; + + ArgumentsMatchInfo(ArgumentsMatchKind kind) { + this.kind = kind; + } + + public boolean isExactMatch() { + return (this.kind == ArgumentsMatchKind.EXACT); + } + + public boolean isCloseMatch() { + return (this.kind == ArgumentsMatchKind.CLOSE); + } + + public boolean isMatchRequiringConversion() { + return (this.kind == ArgumentsMatchKind.REQUIRES_CONVERSION); + } + + @Override + public String toString() { + return "ArgumentMatchInfo: " + this.kind; + } + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveConstructorExecutor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveConstructorExecutor.java new file mode 100644 index 0000000..e77eaa3 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveConstructorExecutor.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.support; + +import java.lang.reflect.Constructor; + +import org.springframework.expression.AccessException; +import org.springframework.expression.ConstructorExecutor; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.TypedValue; +import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; + +/** + * A simple ConstructorExecutor implementation that runs a constructor using reflective + * invocation. + * + * @author Andy Clement + * @author Juergen Hoeller + * @since 3.0 + */ +public class ReflectiveConstructorExecutor implements ConstructorExecutor { + + private final Constructor ctor; + + @Nullable + private final Integer varargsPosition; + + + public ReflectiveConstructorExecutor(Constructor ctor) { + this.ctor = ctor; + if (ctor.isVarArgs()) { + this.varargsPosition = ctor.getParameterCount() - 1; + } + else { + this.varargsPosition = null; + } + } + + @Override + public TypedValue execute(EvaluationContext context, Object... arguments) throws AccessException { + try { + ReflectionHelper.convertArguments( + context.getTypeConverter(), arguments, this.ctor, this.varargsPosition); + if (this.ctor.isVarArgs()) { + arguments = ReflectionHelper.setupArgumentsForVarargsInvocation( + this.ctor.getParameterTypes(), arguments); + } + ReflectionUtils.makeAccessible(this.ctor); + return new TypedValue(this.ctor.newInstance(arguments)); + } + catch (Exception ex) { + throw new AccessException("Problem invoking constructor: " + this.ctor, ex); + } + } + + public Constructor getConstructor() { + return this.ctor; + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveConstructorResolver.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveConstructorResolver.java new file mode 100644 index 0000000..c2de81e --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveConstructorResolver.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.support; + +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.AccessException; +import org.springframework.expression.ConstructorExecutor; +import org.springframework.expression.ConstructorResolver; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.TypeConverter; +import org.springframework.lang.Nullable; + +/** + * A constructor resolver that uses reflection to locate the constructor that should be invoked. + * + * @author Andy Clement + * @author Juergen Hoeller + * @since 3.0 + */ +public class ReflectiveConstructorResolver implements ConstructorResolver { + + /** + * Locate a constructor on the type. There are three kinds of match that might occur: + *

      + *
    1. An exact match where the types of the arguments match the types of the constructor + *
    2. An in-exact match where the types we are looking for are subtypes of those defined on the constructor + *
    3. A match where we are able to convert the arguments into those expected by the constructor, according to the + * registered type converter. + *
    + */ + @Override + @Nullable + public ConstructorExecutor resolve(EvaluationContext context, String typeName, List argumentTypes) + throws AccessException { + + try { + TypeConverter typeConverter = context.getTypeConverter(); + Class type = context.getTypeLocator().findType(typeName); + Constructor[] ctors = type.getConstructors(); + + Arrays.sort(ctors, (c1, c2) -> { + int c1pl = c1.getParameterCount(); + int c2pl = c2.getParameterCount(); + return Integer.compare(c1pl, c2pl); + }); + + Constructor closeMatch = null; + Constructor matchRequiringConversion = null; + + for (Constructor ctor : ctors) { + int paramCount = ctor.getParameterCount(); + List paramDescriptors = new ArrayList<>(paramCount); + for (int i = 0; i < paramCount; i++) { + paramDescriptors.add(new TypeDescriptor(new MethodParameter(ctor, i))); + } + ReflectionHelper.ArgumentsMatchInfo matchInfo = null; + if (ctor.isVarArgs() && argumentTypes.size() >= paramCount - 1) { + // *sigh* complicated + // Basically.. we have to have all parameters match up until the varargs one, then the rest of what is + // being provided should be + // the same type whilst the final argument to the method must be an array of that (oh, how easy...not) - + // or the final parameter + // we are supplied does match exactly (it is an array already). + matchInfo = ReflectionHelper.compareArgumentsVarargs(paramDescriptors, argumentTypes, typeConverter); + } + else if (paramCount == argumentTypes.size()) { + // worth a closer look + matchInfo = ReflectionHelper.compareArguments(paramDescriptors, argumentTypes, typeConverter); + } + if (matchInfo != null) { + if (matchInfo.isExactMatch()) { + return new ReflectiveConstructorExecutor(ctor); + } + else if (matchInfo.isCloseMatch()) { + closeMatch = ctor; + } + else if (matchInfo.isMatchRequiringConversion()) { + matchRequiringConversion = ctor; + } + } + } + + if (closeMatch != null) { + return new ReflectiveConstructorExecutor(closeMatch); + } + else if (matchRequiringConversion != null) { + return new ReflectiveConstructorExecutor(matchRequiringConversion); + } + else { + return null; + } + } + catch (EvaluationException ex) { + throw new AccessException("Failed to resolve constructor", ex); + } + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java new file mode 100644 index 0000000..2de2544 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java @@ -0,0 +1,137 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.support; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.MethodExecutor; +import org.springframework.expression.TypedValue; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * {@link MethodExecutor} that works via reflection. + * + * @author Andy Clement + * @author Juergen Hoeller + * @since 3.0 + */ +public class ReflectiveMethodExecutor implements MethodExecutor { + + private final Method originalMethod; + + private final Method methodToInvoke; + + @Nullable + private final Integer varargsPosition; + + private boolean computedPublicDeclaringClass = false; + + @Nullable + private Class publicDeclaringClass; + + private boolean argumentConversionOccurred = false; + + + /** + * Create a new executor for the given method. + * @param method the method to invoke + */ + public ReflectiveMethodExecutor(Method method) { + this.originalMethod = method; + this.methodToInvoke = ClassUtils.getInterfaceMethodIfPossible(method); + if (method.isVarArgs()) { + this.varargsPosition = method.getParameterCount() - 1; + } + else { + this.varargsPosition = null; + } + } + + + /** + * Return the original method that this executor has been configured for. + */ + public final Method getMethod() { + return this.originalMethod; + } + + /** + * Find the first public class in the methods declaring class hierarchy that declares this method. + * Sometimes the reflective method discovery logic finds a suitable method that can easily be + * called via reflection but cannot be called from generated code when compiling the expression + * because of visibility restrictions. For example if a non-public class overrides toString(), + * this helper method will walk up the type hierarchy to find the first public type that declares + * the method (if there is one!). For toString() it may walk as far as Object. + */ + @Nullable + public Class getPublicDeclaringClass() { + if (!this.computedPublicDeclaringClass) { + this.publicDeclaringClass = + discoverPublicDeclaringClass(this.originalMethod, this.originalMethod.getDeclaringClass()); + this.computedPublicDeclaringClass = true; + } + return this.publicDeclaringClass; + } + + @Nullable + private Class discoverPublicDeclaringClass(Method method, Class clazz) { + if (Modifier.isPublic(clazz.getModifiers())) { + try { + clazz.getDeclaredMethod(method.getName(), method.getParameterTypes()); + return clazz; + } + catch (NoSuchMethodException ex) { + // Continue below... + } + } + if (clazz.getSuperclass() != null) { + return discoverPublicDeclaringClass(method, clazz.getSuperclass()); + } + return null; + } + + public boolean didArgumentConversionOccur() { + return this.argumentConversionOccurred; + } + + + @Override + public TypedValue execute(EvaluationContext context, Object target, Object... arguments) throws AccessException { + try { + this.argumentConversionOccurred = ReflectionHelper.convertArguments( + context.getTypeConverter(), arguments, this.originalMethod, this.varargsPosition); + if (this.originalMethod.isVarArgs()) { + arguments = ReflectionHelper.setupArgumentsForVarargsInvocation( + this.originalMethod.getParameterTypes(), arguments); + } + ReflectionUtils.makeAccessible(this.methodToInvoke); + Object value = this.methodToInvoke.invoke(target, arguments); + return new TypedValue(value, new TypeDescriptor(new MethodParameter(this.originalMethod, -1)).narrow(value)); + } + catch (Exception ex) { + throw new AccessException("Problem invoking method: " + this.methodToInvoke, ex); + } + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java new file mode 100644 index 0000000..451aee2 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java @@ -0,0 +1,288 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.support; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.MethodExecutor; +import org.springframework.expression.MethodFilter; +import org.springframework.expression.MethodResolver; +import org.springframework.expression.TypeConverter; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.lang.Nullable; + +/** + * Reflection-based {@link MethodResolver} used by default in {@link StandardEvaluationContext} + * unless explicit method resolvers have been specified. + * + * @author Andy Clement + * @author Juergen Hoeller + * @author Chris Beams + * @since 3.0 + * @see StandardEvaluationContext#addMethodResolver(MethodResolver) + */ +public class ReflectiveMethodResolver implements MethodResolver { + + // Using distance will ensure a more accurate match is discovered, + // more closely following the Java rules. + private final boolean useDistance; + + @Nullable + private Map, MethodFilter> filters; + + + public ReflectiveMethodResolver() { + this.useDistance = true; + } + + /** + * This constructor allows the ReflectiveMethodResolver to be configured such that it + * will use a distance computation to check which is the better of two close matches + * (when there are multiple matches). Using the distance computation is intended to + * ensure matches are more closely representative of what a Java compiler would do + * when taking into account boxing/unboxing and whether the method candidates are + * declared to handle a supertype of the type (of the argument) being passed in. + * @param useDistance {@code true} if distance computation should be used when + * calculating matches; {@code false} otherwise + */ + public ReflectiveMethodResolver(boolean useDistance) { + this.useDistance = useDistance; + } + + + /** + * Register a filter for methods on the given type. + * @param type the type to filter on + * @param filter the corresponding method filter, + * or {@code null} to clear any filter for the given type + */ + public void registerMethodFilter(Class type, @Nullable MethodFilter filter) { + if (this.filters == null) { + this.filters = new HashMap<>(); + } + if (filter != null) { + this.filters.put(type, filter); + } + else { + this.filters.remove(type); + } + } + + /** + * Locate a method on a type. There are three kinds of match that might occur: + *
      + *
    1. an exact match where the types of the arguments match the types of the constructor + *
    2. an in-exact match where the types we are looking for are subtypes of those defined on the constructor + *
    3. a match where we are able to convert the arguments into those expected by the constructor, + * according to the registered type converter + *
    + */ + @Override + @Nullable + public MethodExecutor resolve(EvaluationContext context, Object targetObject, String name, + List argumentTypes) throws AccessException { + + try { + TypeConverter typeConverter = context.getTypeConverter(); + Class type = (targetObject instanceof Class ? (Class) targetObject : targetObject.getClass()); + ArrayList methods = new ArrayList<>(getMethods(type, targetObject)); + + // If a filter is registered for this type, call it + MethodFilter filter = (this.filters != null ? this.filters.get(type) : null); + if (filter != null) { + List filtered = filter.filter(methods); + methods = (filtered instanceof ArrayList ? (ArrayList) filtered : new ArrayList<>(filtered)); + } + + // Sort methods into a sensible order + if (methods.size() > 1) { + methods.sort((m1, m2) -> { + int m1pl = m1.getParameterCount(); + int m2pl = m2.getParameterCount(); + // vararg methods go last + if (m1pl == m2pl) { + if (!m1.isVarArgs() && m2.isVarArgs()) { + return -1; + } + else if (m1.isVarArgs() && !m2.isVarArgs()) { + return 1; + } + else { + return 0; + } + } + return Integer.compare(m1pl, m2pl); + }); + } + + // Resolve any bridge methods + for (int i = 0; i < methods.size(); i++) { + methods.set(i, BridgeMethodResolver.findBridgedMethod(methods.get(i))); + } + + // Remove duplicate methods (possible due to resolved bridge methods) + Set methodsToIterate = new LinkedHashSet<>(methods); + + Method closeMatch = null; + int closeMatchDistance = Integer.MAX_VALUE; + Method matchRequiringConversion = null; + boolean multipleOptions = false; + + for (Method method : methodsToIterate) { + if (method.getName().equals(name)) { + int paramCount = method.getParameterCount(); + List paramDescriptors = new ArrayList<>(paramCount); + for (int i = 0; i < paramCount; i++) { + paramDescriptors.add(new TypeDescriptor(new MethodParameter(method, i))); + } + ReflectionHelper.ArgumentsMatchInfo matchInfo = null; + if (method.isVarArgs() && argumentTypes.size() >= (paramCount - 1)) { + // *sigh* complicated + matchInfo = ReflectionHelper.compareArgumentsVarargs(paramDescriptors, argumentTypes, typeConverter); + } + else if (paramCount == argumentTypes.size()) { + // Name and parameter number match, check the arguments + matchInfo = ReflectionHelper.compareArguments(paramDescriptors, argumentTypes, typeConverter); + } + if (matchInfo != null) { + if (matchInfo.isExactMatch()) { + return new ReflectiveMethodExecutor(method); + } + else if (matchInfo.isCloseMatch()) { + if (this.useDistance) { + int matchDistance = ReflectionHelper.getTypeDifferenceWeight(paramDescriptors, argumentTypes); + if (closeMatch == null || matchDistance < closeMatchDistance) { + // This is a better match... + closeMatch = method; + closeMatchDistance = matchDistance; + } + } + else { + // Take this as a close match if there isn't one already + if (closeMatch == null) { + closeMatch = method; + } + } + } + else if (matchInfo.isMatchRequiringConversion()) { + if (matchRequiringConversion != null) { + multipleOptions = true; + } + matchRequiringConversion = method; + } + } + } + } + if (closeMatch != null) { + return new ReflectiveMethodExecutor(closeMatch); + } + else if (matchRequiringConversion != null) { + if (multipleOptions) { + throw new SpelEvaluationException(SpelMessage.MULTIPLE_POSSIBLE_METHODS, name); + } + return new ReflectiveMethodExecutor(matchRequiringConversion); + } + else { + return null; + } + } + catch (EvaluationException ex) { + throw new AccessException("Failed to resolve method", ex); + } + } + + private Set getMethods(Class type, Object targetObject) { + if (targetObject instanceof Class) { + Set result = new LinkedHashSet<>(); + // Add these so that static methods are invocable on the type: e.g. Float.valueOf(..) + Method[] methods = getMethods(type); + for (Method method : methods) { + if (Modifier.isStatic(method.getModifiers())) { + result.add(method); + } + } + // Also expose methods from java.lang.Class itself + Collections.addAll(result, getMethods(Class.class)); + return result; + } + else if (Proxy.isProxyClass(type)) { + Set result = new LinkedHashSet<>(); + // Expose interface methods (not proxy-declared overrides) for proper vararg introspection + for (Class ifc : type.getInterfaces()) { + Method[] methods = getMethods(ifc); + for (Method method : methods) { + if (isCandidateForInvocation(method, type)) { + result.add(method); + } + } + } + return result; + } + else { + Set result = new LinkedHashSet<>(); + Method[] methods = getMethods(type); + for (Method method : methods) { + if (isCandidateForInvocation(method, type)) { + result.add(method); + } + } + return result; + } + } + + /** + * Return the set of methods for this type. The default implementation returns the + * result of {@link Class#getMethods()} for the given {@code type}, but subclasses + * may override in order to alter the results, e.g. specifying static methods + * declared elsewhere. + * @param type the class for which to return the methods + * @since 3.1.1 + */ + protected Method[] getMethods(Class type) { + return type.getMethods(); + } + + /** + * Determine whether the given {@code Method} is a candidate for method resolution + * on an instance of the given target class. + *

    The default implementation considers any method as a candidate, even for + * static methods sand non-user-declared methods on the {@link Object} base class. + * @param method the Method to evaluate + * @param targetClass the concrete target class that is being introspected + * @since 4.3.15 + */ + protected boolean isCandidateForInvocation(Method method, Class targetClass) { + return true; + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java new file mode 100644 index 0000000..5fd48cd --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java @@ -0,0 +1,786 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.support; + +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.asm.MethodVisitor; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.Property; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.CodeFlow; +import org.springframework.expression.spel.CompilablePropertyAccessor; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * A powerful {@link PropertyAccessor} that uses reflection to access properties + * for reading and possibly also for writing on a target instance. + * + *

    A property can be referenced through a public getter method (when being read) + * or a public setter method (when being written), and also as a public field. + * + * @author Andy Clement + * @author Juergen Hoeller + * @author Phillip Webb + * @author Sam Brannen + * @since 3.0 + * @see StandardEvaluationContext + * @see SimpleEvaluationContext + * @see DataBindingPropertyAccessor + */ +public class ReflectivePropertyAccessor implements PropertyAccessor { + + private static final Set> ANY_TYPES = Collections.emptySet(); + + private static final Set> BOOLEAN_TYPES; + + static { + Set> booleanTypes = new HashSet<>(4); + booleanTypes.add(Boolean.class); + booleanTypes.add(Boolean.TYPE); + BOOLEAN_TYPES = Collections.unmodifiableSet(booleanTypes); + } + + + private final boolean allowWrite; + + private final Map readerCache = new ConcurrentHashMap<>(64); + + private final Map writerCache = new ConcurrentHashMap<>(64); + + private final Map typeDescriptorCache = new ConcurrentHashMap<>(64); + + private final Map, Method[]> sortedMethodsCache = new ConcurrentHashMap<>(64); + + @Nullable + private volatile InvokerPair lastReadInvokerPair; + + + /** + * Create a new property accessor for reading as well writing. + * @see #ReflectivePropertyAccessor(boolean) + */ + public ReflectivePropertyAccessor() { + this.allowWrite = true; + } + + /** + * Create a new property accessor for reading and possibly also writing. + * @param allowWrite whether to allow write operations on a target instance + * @since 4.3.15 + * @see #canWrite + */ + public ReflectivePropertyAccessor(boolean allowWrite) { + this.allowWrite = allowWrite; + } + + + /** + * Returns {@code null} which means this is a general purpose accessor. + */ + @Override + @Nullable + public Class[] getSpecificTargetClasses() { + return null; + } + + @Override + public boolean canRead(EvaluationContext context, @Nullable Object target, String name) throws AccessException { + if (target == null) { + return false; + } + + Class type = (target instanceof Class ? (Class) target : target.getClass()); + if (type.isArray() && name.equals("length")) { + return true; + } + + PropertyCacheKey cacheKey = new PropertyCacheKey(type, name, target instanceof Class); + if (this.readerCache.containsKey(cacheKey)) { + return true; + } + + Method method = findGetterForProperty(name, type, target); + if (method != null) { + // Treat it like a property... + // The readerCache will only contain gettable properties (let's not worry about setters for now). + Property property = new Property(type, method, null); + TypeDescriptor typeDescriptor = new TypeDescriptor(property); + method = ClassUtils.getInterfaceMethodIfPossible(method); + this.readerCache.put(cacheKey, new InvokerPair(method, typeDescriptor)); + this.typeDescriptorCache.put(cacheKey, typeDescriptor); + return true; + } + else { + Field field = findField(name, type, target); + if (field != null) { + TypeDescriptor typeDescriptor = new TypeDescriptor(field); + this.readerCache.put(cacheKey, new InvokerPair(field, typeDescriptor)); + this.typeDescriptorCache.put(cacheKey, typeDescriptor); + return true; + } + } + + return false; + } + + @Override + public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException { + Assert.state(target != null, "Target must not be null"); + Class type = (target instanceof Class ? (Class) target : target.getClass()); + + if (type.isArray() && name.equals("length")) { + if (target instanceof Class) { + throw new AccessException("Cannot access length on array class itself"); + } + return new TypedValue(Array.getLength(target)); + } + + PropertyCacheKey cacheKey = new PropertyCacheKey(type, name, target instanceof Class); + InvokerPair invoker = this.readerCache.get(cacheKey); + this.lastReadInvokerPair = invoker; + + if (invoker == null || invoker.member instanceof Method) { + Method method = (Method) (invoker != null ? invoker.member : null); + if (method == null) { + method = findGetterForProperty(name, type, target); + if (method != null) { + // Treat it like a property... + // The readerCache will only contain gettable properties (let's not worry about setters for now). + Property property = new Property(type, method, null); + TypeDescriptor typeDescriptor = new TypeDescriptor(property); + method = ClassUtils.getInterfaceMethodIfPossible(method); + invoker = new InvokerPair(method, typeDescriptor); + this.lastReadInvokerPair = invoker; + this.readerCache.put(cacheKey, invoker); + } + } + if (method != null) { + try { + ReflectionUtils.makeAccessible(method); + Object value = method.invoke(target); + return new TypedValue(value, invoker.typeDescriptor.narrow(value)); + } + catch (Exception ex) { + throw new AccessException("Unable to access property '" + name + "' through getter method", ex); + } + } + } + + if (invoker == null || invoker.member instanceof Field) { + Field field = (Field) (invoker == null ? null : invoker.member); + if (field == null) { + field = findField(name, type, target); + if (field != null) { + invoker = new InvokerPair(field, new TypeDescriptor(field)); + this.lastReadInvokerPair = invoker; + this.readerCache.put(cacheKey, invoker); + } + } + if (field != null) { + try { + ReflectionUtils.makeAccessible(field); + Object value = field.get(target); + return new TypedValue(value, invoker.typeDescriptor.narrow(value)); + } + catch (Exception ex) { + throw new AccessException("Unable to access field '" + name + "'", ex); + } + } + } + + throw new AccessException("Neither getter method nor field found for property '" + name + "'"); + } + + @Override + public boolean canWrite(EvaluationContext context, @Nullable Object target, String name) throws AccessException { + if (!this.allowWrite || target == null) { + return false; + } + + Class type = (target instanceof Class ? (Class) target : target.getClass()); + PropertyCacheKey cacheKey = new PropertyCacheKey(type, name, target instanceof Class); + if (this.writerCache.containsKey(cacheKey)) { + return true; + } + + Method method = findSetterForProperty(name, type, target); + if (method != null) { + // Treat it like a property + Property property = new Property(type, null, method); + TypeDescriptor typeDescriptor = new TypeDescriptor(property); + method = ClassUtils.getInterfaceMethodIfPossible(method); + this.writerCache.put(cacheKey, method); + this.typeDescriptorCache.put(cacheKey, typeDescriptor); + return true; + } + else { + Field field = findField(name, type, target); + if (field != null) { + this.writerCache.put(cacheKey, field); + this.typeDescriptorCache.put(cacheKey, new TypeDescriptor(field)); + return true; + } + } + + return false; + } + + @Override + public void write(EvaluationContext context, @Nullable Object target, String name, @Nullable Object newValue) + throws AccessException { + + if (!this.allowWrite) { + throw new AccessException("PropertyAccessor for property '" + name + + "' on target [" + target + "] does not allow write operations"); + } + + Assert.state(target != null, "Target must not be null"); + Class type = (target instanceof Class ? (Class) target : target.getClass()); + + Object possiblyConvertedNewValue = newValue; + TypeDescriptor typeDescriptor = getTypeDescriptor(context, target, name); + if (typeDescriptor != null) { + try { + possiblyConvertedNewValue = context.getTypeConverter().convertValue( + newValue, TypeDescriptor.forObject(newValue), typeDescriptor); + } + catch (EvaluationException evaluationException) { + throw new AccessException("Type conversion failure", evaluationException); + } + } + + PropertyCacheKey cacheKey = new PropertyCacheKey(type, name, target instanceof Class); + Member cachedMember = this.writerCache.get(cacheKey); + + if (cachedMember == null || cachedMember instanceof Method) { + Method method = (Method) cachedMember; + if (method == null) { + method = findSetterForProperty(name, type, target); + if (method != null) { + method = ClassUtils.getInterfaceMethodIfPossible(method); + cachedMember = method; + this.writerCache.put(cacheKey, cachedMember); + } + } + if (method != null) { + try { + ReflectionUtils.makeAccessible(method); + method.invoke(target, possiblyConvertedNewValue); + return; + } + catch (Exception ex) { + throw new AccessException("Unable to access property '" + name + "' through setter method", ex); + } + } + } + + if (cachedMember == null || cachedMember instanceof Field) { + Field field = (Field) cachedMember; + if (field == null) { + field = findField(name, type, target); + if (field != null) { + cachedMember = field; + this.writerCache.put(cacheKey, cachedMember); + } + } + if (field != null) { + try { + ReflectionUtils.makeAccessible(field); + field.set(target, possiblyConvertedNewValue); + return; + } + catch (Exception ex) { + throw new AccessException("Unable to access field '" + name + "'", ex); + } + } + } + + throw new AccessException("Neither setter method nor field found for property '" + name + "'"); + } + + /** + * Get the last read invoker pair. + * @deprecated as of 4.3.15 since it is not used within the framework anymore + */ + @Deprecated + @Nullable + public Member getLastReadInvokerPair() { + InvokerPair lastReadInvoker = this.lastReadInvokerPair; + return (lastReadInvoker != null ? lastReadInvoker.member : null); + } + + + @Nullable + private TypeDescriptor getTypeDescriptor(EvaluationContext context, Object target, String name) { + Class type = (target instanceof Class ? (Class) target : target.getClass()); + + if (type.isArray() && name.equals("length")) { + return TypeDescriptor.valueOf(Integer.TYPE); + } + PropertyCacheKey cacheKey = new PropertyCacheKey(type, name, target instanceof Class); + TypeDescriptor typeDescriptor = this.typeDescriptorCache.get(cacheKey); + if (typeDescriptor == null) { + // Attempt to populate the cache entry + try { + if (canRead(context, target, name) || canWrite(context, target, name)) { + typeDescriptor = this.typeDescriptorCache.get(cacheKey); + } + } + catch (AccessException ex) { + // Continue with null type descriptor + } + } + return typeDescriptor; + } + + @Nullable + private Method findGetterForProperty(String propertyName, Class clazz, Object target) { + Method method = findGetterForProperty(propertyName, clazz, target instanceof Class); + if (method == null && target instanceof Class) { + method = findGetterForProperty(propertyName, target.getClass(), false); + } + return method; + } + + @Nullable + private Method findSetterForProperty(String propertyName, Class clazz, Object target) { + Method method = findSetterForProperty(propertyName, clazz, target instanceof Class); + if (method == null && target instanceof Class) { + method = findSetterForProperty(propertyName, target.getClass(), false); + } + return method; + } + + /** + * Find a getter method for the specified property. + */ + @Nullable + protected Method findGetterForProperty(String propertyName, Class clazz, boolean mustBeStatic) { + Method method = findMethodForProperty(getPropertyMethodSuffixes(propertyName), + "get", clazz, mustBeStatic, 0, ANY_TYPES); + if (method == null) { + method = findMethodForProperty(getPropertyMethodSuffixes(propertyName), + "is", clazz, mustBeStatic, 0, BOOLEAN_TYPES); + if (method == null) { + // Record-style plain accessor method, e.g. name() + method = findMethodForProperty(new String[] {propertyName}, + "", clazz, mustBeStatic, 0, ANY_TYPES); + } + } + return method; + } + + /** + * Find a setter method for the specified property. + */ + @Nullable + protected Method findSetterForProperty(String propertyName, Class clazz, boolean mustBeStatic) { + return findMethodForProperty(getPropertyMethodSuffixes(propertyName), + "set", clazz, mustBeStatic, 1, ANY_TYPES); + } + + @Nullable + private Method findMethodForProperty(String[] methodSuffixes, String prefix, Class clazz, + boolean mustBeStatic, int numberOfParams, Set> requiredReturnTypes) { + + Method[] methods = getSortedMethods(clazz); + for (String methodSuffix : methodSuffixes) { + for (Method method : methods) { + if (isCandidateForProperty(method, clazz) && method.getName().equals(prefix + methodSuffix) && + method.getParameterCount() == numberOfParams && + (!mustBeStatic || Modifier.isStatic(method.getModifiers())) && + (requiredReturnTypes.isEmpty() || requiredReturnTypes.contains(method.getReturnType()))) { + return method; + } + } + } + return null; + } + + /** + * Return class methods ordered with non-bridge methods appearing higher. + */ + private Method[] getSortedMethods(Class clazz) { + return this.sortedMethodsCache.computeIfAbsent(clazz, key -> { + Method[] methods = key.getMethods(); + Arrays.sort(methods, (o1, o2) -> (o1.isBridge() == o2.isBridge() ? 0 : (o1.isBridge() ? 1 : -1))); + return methods; + }); + } + + /** + * Determine whether the given {@code Method} is a candidate for property access + * on an instance of the given target class. + *

    The default implementation considers any method as a candidate, even for + * non-user-declared properties on the {@link Object} base class. + * @param method the Method to evaluate + * @param targetClass the concrete target class that is being introspected + * @since 4.3.15 + */ + protected boolean isCandidateForProperty(Method method, Class targetClass) { + return true; + } + + /** + * Return the method suffixes for a given property name. The default implementation + * uses JavaBean conventions with additional support for properties of the form 'xY' + * where the method 'getXY()' is used in preference to the JavaBean convention of + * 'getxY()'. + */ + protected String[] getPropertyMethodSuffixes(String propertyName) { + String suffix = getPropertyMethodSuffix(propertyName); + if (suffix.length() > 0 && Character.isUpperCase(suffix.charAt(0))) { + return new String[] {suffix}; + } + return new String[] {suffix, StringUtils.capitalize(suffix)}; + } + + /** + * Return the method suffix for a given property name. The default implementation + * uses JavaBean conventions. + */ + protected String getPropertyMethodSuffix(String propertyName) { + if (propertyName.length() > 1 && Character.isUpperCase(propertyName.charAt(1))) { + return propertyName; + } + return StringUtils.capitalize(propertyName); + } + + @Nullable + private Field findField(String name, Class clazz, Object target) { + Field field = findField(name, clazz, target instanceof Class); + if (field == null && target instanceof Class) { + field = findField(name, target.getClass(), false); + } + return field; + } + + /** + * Find a field of a certain name on a specified class. + */ + @Nullable + protected Field findField(String name, Class clazz, boolean mustBeStatic) { + Field[] fields = clazz.getFields(); + for (Field field : fields) { + if (field.getName().equals(name) && (!mustBeStatic || Modifier.isStatic(field.getModifiers()))) { + return field; + } + } + // We'll search superclasses and implemented interfaces explicitly, + // although it shouldn't be necessary - however, see SPR-10125. + if (clazz.getSuperclass() != null) { + Field field = findField(name, clazz.getSuperclass(), mustBeStatic); + if (field != null) { + return field; + } + } + for (Class implementedInterface : clazz.getInterfaces()) { + Field field = findField(name, implementedInterface, mustBeStatic); + if (field != null) { + return field; + } + } + return null; + } + + /** + * Attempt to create an optimized property accessor tailored for a property of a + * particular name on a particular class. The general ReflectivePropertyAccessor + * will always work but is not optimal due to the need to lookup which reflective + * member (method/field) to use each time read() is called. This method will just + * return the ReflectivePropertyAccessor instance if it is unable to build a more + * optimal accessor. + *

    Note: An optimal accessor is currently only usable for read attempts. + * Do not call this method if you need a read-write accessor. + * @see OptimalPropertyAccessor + */ + public PropertyAccessor createOptimalAccessor(EvaluationContext context, @Nullable Object target, String name) { + // Don't be clever for arrays or a null target... + if (target == null) { + return this; + } + Class clazz = (target instanceof Class ? (Class) target : target.getClass()); + if (clazz.isArray()) { + return this; + } + + PropertyCacheKey cacheKey = new PropertyCacheKey(clazz, name, target instanceof Class); + InvokerPair invocationTarget = this.readerCache.get(cacheKey); + + if (invocationTarget == null || invocationTarget.member instanceof Method) { + Method method = (Method) (invocationTarget != null ? invocationTarget.member : null); + if (method == null) { + method = findGetterForProperty(name, clazz, target); + if (method != null) { + TypeDescriptor typeDescriptor = new TypeDescriptor(new MethodParameter(method, -1)); + method = ClassUtils.getInterfaceMethodIfPossible(method); + invocationTarget = new InvokerPair(method, typeDescriptor); + ReflectionUtils.makeAccessible(method); + this.readerCache.put(cacheKey, invocationTarget); + } + } + if (method != null) { + return new OptimalPropertyAccessor(invocationTarget); + } + } + + if (invocationTarget == null || invocationTarget.member instanceof Field) { + Field field = (invocationTarget != null ? (Field) invocationTarget.member : null); + if (field == null) { + field = findField(name, clazz, target instanceof Class); + if (field != null) { + invocationTarget = new InvokerPair(field, new TypeDescriptor(field)); + ReflectionUtils.makeAccessible(field); + this.readerCache.put(cacheKey, invocationTarget); + } + } + if (field != null) { + return new OptimalPropertyAccessor(invocationTarget); + } + } + + return this; + } + + + /** + * Captures the member (method/field) to call reflectively to access a property value + * and the type descriptor for the value returned by the reflective call. + */ + private static class InvokerPair { + + final Member member; + + final TypeDescriptor typeDescriptor; + + public InvokerPair(Member member, TypeDescriptor typeDescriptor) { + this.member = member; + this.typeDescriptor = typeDescriptor; + } + } + + + private static final class PropertyCacheKey implements Comparable { + + private final Class clazz; + + private final String property; + + private boolean targetIsClass; + + public PropertyCacheKey(Class clazz, String name, boolean targetIsClass) { + this.clazz = clazz; + this.property = name; + this.targetIsClass = targetIsClass; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof PropertyCacheKey)) { + return false; + } + PropertyCacheKey otherKey = (PropertyCacheKey) other; + return (this.clazz == otherKey.clazz && this.property.equals(otherKey.property) && + this.targetIsClass == otherKey.targetIsClass); + } + + @Override + public int hashCode() { + return (this.clazz.hashCode() * 29 + this.property.hashCode()); + } + + @Override + public String toString() { + return "PropertyCacheKey [clazz=" + this.clazz.getName() + ", property=" + this.property + + ", targetIsClass=" + this.targetIsClass + "]"; + } + + @Override + public int compareTo(PropertyCacheKey other) { + int result = this.clazz.getName().compareTo(other.clazz.getName()); + if (result == 0) { + result = this.property.compareTo(other.property); + } + return result; + } + } + + + /** + * An optimized form of a PropertyAccessor that will use reflection but only knows + * how to access a particular property on a particular class. This is unlike the + * general ReflectivePropertyResolver which manages a cache of methods/fields that + * may be invoked to access different properties on different classes. This optimal + * accessor exists because looking up the appropriate reflective object by class/name + * on each read is not cheap. + */ + public static class OptimalPropertyAccessor implements CompilablePropertyAccessor { + + /** + * The member being accessed. + */ + public final Member member; + + private final TypeDescriptor typeDescriptor; + + OptimalPropertyAccessor(InvokerPair target) { + this.member = target.member; + this.typeDescriptor = target.typeDescriptor; + } + + @Override + @Nullable + public Class[] getSpecificTargetClasses() { + throw new UnsupportedOperationException("Should not be called on an OptimalPropertyAccessor"); + } + + @Override + public boolean canRead(EvaluationContext context, @Nullable Object target, String name) throws AccessException { + if (target == null) { + return false; + } + Class type = (target instanceof Class ? (Class) target : target.getClass()); + if (type.isArray()) { + return false; + } + + if (this.member instanceof Method) { + Method method = (Method) this.member; + String getterName = "get" + StringUtils.capitalize(name); + if (getterName.equals(method.getName())) { + return true; + } + getterName = "is" + StringUtils.capitalize(name); + if (getterName.equals(method.getName())) { + return true; + } + } + return this.member.getName().equals(name); + } + + @Override + public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException { + if (this.member instanceof Method) { + Method method = (Method) this.member; + try { + ReflectionUtils.makeAccessible(method); + Object value = method.invoke(target); + return new TypedValue(value, this.typeDescriptor.narrow(value)); + } + catch (Exception ex) { + throw new AccessException("Unable to access property '" + name + "' through getter method", ex); + } + } + else { + Field field = (Field) this.member; + try { + ReflectionUtils.makeAccessible(field); + Object value = field.get(target); + return new TypedValue(value, this.typeDescriptor.narrow(value)); + } + catch (Exception ex) { + throw new AccessException("Unable to access field '" + name + "'", ex); + } + } + } + + @Override + public boolean canWrite(EvaluationContext context, @Nullable Object target, String name) { + throw new UnsupportedOperationException("Should not be called on an OptimalPropertyAccessor"); + } + + @Override + public void write(EvaluationContext context, @Nullable Object target, String name, @Nullable Object newValue) { + throw new UnsupportedOperationException("Should not be called on an OptimalPropertyAccessor"); + } + + @Override + public boolean isCompilable() { + return (Modifier.isPublic(this.member.getModifiers()) && + Modifier.isPublic(this.member.getDeclaringClass().getModifiers())); + } + + @Override + public Class getPropertyType() { + if (this.member instanceof Method) { + return ((Method) this.member).getReturnType(); + } + else { + return ((Field) this.member).getType(); + } + } + + @Override + public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) { + boolean isStatic = Modifier.isStatic(this.member.getModifiers()); + String descriptor = cf.lastDescriptor(); + String classDesc = this.member.getDeclaringClass().getName().replace('.', '/'); + + if (!isStatic) { + if (descriptor == null) { + cf.loadTarget(mv); + } + if (descriptor == null || !classDesc.equals(descriptor.substring(1))) { + mv.visitTypeInsn(CHECKCAST, classDesc); + } + } + else { + if (descriptor != null) { + // A static field/method call will not consume what is on the stack, + // it needs to be popped off. + mv.visitInsn(POP); + } + } + + if (this.member instanceof Method) { + Method method = (Method) this.member; + boolean isInterface = method.getDeclaringClass().isInterface(); + int opcode = (isStatic ? INVOKESTATIC : isInterface ? INVOKEINTERFACE : INVOKEVIRTUAL); + mv.visitMethodInsn(opcode, classDesc, method.getName(), + CodeFlow.createSignatureDescriptor(method), isInterface); + } + else { + mv.visitFieldInsn((isStatic ? GETSTATIC : GETFIELD), classDesc, this.member.getName(), + CodeFlow.toJvmDescriptor(((Field) this.member).getType())); + } + } + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java new file mode 100644 index 0000000..d8826e3 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java @@ -0,0 +1,356 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.support; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.BeanResolver; +import org.springframework.expression.ConstructorResolver; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.MethodResolver; +import org.springframework.expression.OperatorOverloader; +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.TypeComparator; +import org.springframework.expression.TypeConverter; +import org.springframework.expression.TypeLocator; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.lang.Nullable; + +/** + * A basic implementation of {@link EvaluationContext} that focuses on a subset + * of essential SpEL features and customization options, targeting simple + * condition evaluation and in particular data binding scenarios. + * + *

    In many cases, the full extent of the SpEL language is not required and + * should be meaningfully restricted. Examples include but are not limited to + * data binding expressions, property-based filters, and others. To that effect, + * {@code SimpleEvaluationContext} is tailored to support only a subset of the + * SpEL language syntax, e.g. excluding references to Java types, constructors, + * and bean references. + * + *

    When creating a {@code SimpleEvaluationContext} you need to choose the + * level of support that you need for property access in SpEL expressions: + *

      + *
    • A custom {@code PropertyAccessor} (typically not reflection-based), + * potentially combined with a {@link DataBindingPropertyAccessor}
    • + *
    • Data binding properties for read-only access
    • + *
    • Data binding properties for read and write
    • + *
    + * + *

    Conveniently, {@link SimpleEvaluationContext#forReadOnlyDataBinding()} + * enables read access to properties via {@link DataBindingPropertyAccessor}; + * same for {@link SimpleEvaluationContext#forReadWriteDataBinding()} when + * write access is needed as well. Alternatively, configure custom accessors + * via {@link SimpleEvaluationContext#forPropertyAccessors}, and potentially + * activate method resolution and/or a type converter through the builder. + * + *

    Note that {@code SimpleEvaluationContext} is typically not configured + * with a default root object. Instead it is meant to be created once and + * used repeatedly through {@code getValue} calls on a pre-compiled + * {@link org.springframework.expression.Expression} with both an + * {@code EvaluationContext} and a root object as arguments: + * {@link org.springframework.expression.Expression#getValue(EvaluationContext, Object)}. + * + *

    For more power and flexibility, in particular for internal configuration + * scenarios, consider using {@link StandardEvaluationContext} instead. + * + * @author Rossen Stoyanchev + * @author Juergen Hoeller + * @since 4.3.15 + * @see #forPropertyAccessors + * @see #forReadOnlyDataBinding() + * @see #forReadWriteDataBinding() + * @see StandardEvaluationContext + * @see StandardTypeConverter + * @see DataBindingPropertyAccessor + */ +public final class SimpleEvaluationContext implements EvaluationContext { + + private static final TypeLocator typeNotFoundTypeLocator = typeName -> { + throw new SpelEvaluationException(SpelMessage.TYPE_NOT_FOUND, typeName); + }; + + + private final TypedValue rootObject; + + private final List propertyAccessors; + + private final List methodResolvers; + + private final TypeConverter typeConverter; + + private final TypeComparator typeComparator = new StandardTypeComparator(); + + private final OperatorOverloader operatorOverloader = new StandardOperatorOverloader(); + + private final Map variables = new HashMap<>(); + + + private SimpleEvaluationContext(List accessors, List resolvers, + @Nullable TypeConverter converter, @Nullable TypedValue rootObject) { + + this.propertyAccessors = accessors; + this.methodResolvers = resolvers; + this.typeConverter = (converter != null ? converter : new StandardTypeConverter()); + this.rootObject = (rootObject != null ? rootObject : TypedValue.NULL); + } + + + /** + * Return the specified root object, if any. + */ + @Override + public TypedValue getRootObject() { + return this.rootObject; + } + + /** + * Return the specified {@link PropertyAccessor} delegates, if any. + * @see #forPropertyAccessors + */ + @Override + public List getPropertyAccessors() { + return this.propertyAccessors; + } + + /** + * Return an empty list, always, since this context does not support the + * use of type references. + */ + @Override + public List getConstructorResolvers() { + return Collections.emptyList(); + } + + /** + * Return the specified {@link MethodResolver} delegates, if any. + * @see Builder#withMethodResolvers + */ + @Override + public List getMethodResolvers() { + return this.methodResolvers; + } + + /** + * {@code SimpleEvaluationContext} does not support the use of bean references. + * @return always {@code null} + */ + @Override + @Nullable + public BeanResolver getBeanResolver() { + return null; + } + + /** + * {@code SimpleEvaluationContext} does not support use of type references. + * @return {@code TypeLocator} implementation that raises a + * {@link SpelEvaluationException} with {@link SpelMessage#TYPE_NOT_FOUND}. + */ + @Override + public TypeLocator getTypeLocator() { + return typeNotFoundTypeLocator; + } + + /** + * The configured {@link TypeConverter}. + *

    By default this is {@link StandardTypeConverter}. + * @see Builder#withTypeConverter + * @see Builder#withConversionService + */ + @Override + public TypeConverter getTypeConverter() { + return this.typeConverter; + } + + /** + * Return an instance of {@link StandardTypeComparator}. + */ + @Override + public TypeComparator getTypeComparator() { + return this.typeComparator; + } + + /** + * Return an instance of {@link StandardOperatorOverloader}. + */ + @Override + public OperatorOverloader getOperatorOverloader() { + return this.operatorOverloader; + } + + @Override + public void setVariable(String name, @Nullable Object value) { + this.variables.put(name, value); + } + + @Override + @Nullable + public Object lookupVariable(String name) { + return this.variables.get(name); + } + + + /** + * Create a {@code SimpleEvaluationContext} for the specified {@link PropertyAccessor} + * delegates: typically a custom {@code PropertyAccessor} specific to a use case + * (e.g. attribute resolution in a custom data structure), potentially combined with + * a {@link DataBindingPropertyAccessor} if property dereferences are needed as well. + * @param accessors the accessor delegates to use + * @see DataBindingPropertyAccessor#forReadOnlyAccess() + * @see DataBindingPropertyAccessor#forReadWriteAccess() + */ + public static Builder forPropertyAccessors(PropertyAccessor... accessors) { + for (PropertyAccessor accessor : accessors) { + if (accessor.getClass() == ReflectivePropertyAccessor.class) { + throw new IllegalArgumentException("SimpleEvaluationContext is not designed for use with a plain " + + "ReflectivePropertyAccessor. Consider using DataBindingPropertyAccessor or a custom subclass."); + } + } + return new Builder(accessors); + } + + /** + * Create a {@code SimpleEvaluationContext} for read-only access to + * public properties via {@link DataBindingPropertyAccessor}. + * @see DataBindingPropertyAccessor#forReadOnlyAccess() + * @see #forPropertyAccessors + */ + public static Builder forReadOnlyDataBinding() { + return new Builder(DataBindingPropertyAccessor.forReadOnlyAccess()); + } + + /** + * Create a {@code SimpleEvaluationContext} for read-write access to + * public properties via {@link DataBindingPropertyAccessor}. + * @see DataBindingPropertyAccessor#forReadWriteAccess() + * @see #forPropertyAccessors + */ + public static Builder forReadWriteDataBinding() { + return new Builder(DataBindingPropertyAccessor.forReadWriteAccess()); + } + + + /** + * Builder for {@code SimpleEvaluationContext}. + */ + public static class Builder { + + private final List accessors; + + private List resolvers = Collections.emptyList(); + + @Nullable + private TypeConverter typeConverter; + + @Nullable + private TypedValue rootObject; + + public Builder(PropertyAccessor... accessors) { + this.accessors = Arrays.asList(accessors); + } + + /** + * Register the specified {@link MethodResolver} delegates for + * a combination of property access and method resolution. + * @param resolvers the resolver delegates to use + * @see #withInstanceMethods() + * @see SimpleEvaluationContext#forPropertyAccessors + */ + public Builder withMethodResolvers(MethodResolver... resolvers) { + for (MethodResolver resolver : resolvers) { + if (resolver.getClass() == ReflectiveMethodResolver.class) { + throw new IllegalArgumentException("SimpleEvaluationContext is not designed for use with a plain " + + "ReflectiveMethodResolver. Consider using DataBindingMethodResolver or a custom subclass."); + } + } + this.resolvers = Arrays.asList(resolvers); + return this; + } + + /** + * Register a {@link DataBindingMethodResolver} for instance method invocation purposes + * (i.e. not supporting static methods) in addition to the specified property accessors, + * typically in combination with a {@link DataBindingPropertyAccessor}. + * @see #withMethodResolvers + * @see SimpleEvaluationContext#forReadOnlyDataBinding() + * @see SimpleEvaluationContext#forReadWriteDataBinding() + */ + public Builder withInstanceMethods() { + this.resolvers = Collections.singletonList(DataBindingMethodResolver.forInstanceMethodInvocation()); + return this; + } + + + /** + * Register a custom {@link ConversionService}. + *

    By default a {@link StandardTypeConverter} backed by a + * {@link org.springframework.core.convert.support.DefaultConversionService} is used. + * @see #withTypeConverter + * @see StandardTypeConverter#StandardTypeConverter(ConversionService) + */ + public Builder withConversionService(ConversionService conversionService) { + this.typeConverter = new StandardTypeConverter(conversionService); + return this; + } + /** + * Register a custom {@link TypeConverter}. + *

    By default a {@link StandardTypeConverter} backed by a + * {@link org.springframework.core.convert.support.DefaultConversionService} is used. + * @see #withConversionService + * @see StandardTypeConverter#StandardTypeConverter() + */ + public Builder withTypeConverter(TypeConverter converter) { + this.typeConverter = converter; + return this; + } + + /** + * Specify a default root object to resolve against. + *

    Default is none, expecting an object argument at evaluation time. + * @see org.springframework.expression.Expression#getValue(EvaluationContext) + * @see org.springframework.expression.Expression#getValue(EvaluationContext, Object) + */ + public Builder withRootObject(Object rootObject) { + this.rootObject = new TypedValue(rootObject); + return this; + } + + /** + * Specify a typed root object to resolve against. + *

    Default is none, expecting an object argument at evaluation time. + * @see org.springframework.expression.Expression#getValue(EvaluationContext) + * @see org.springframework.expression.Expression#getValue(EvaluationContext, Object) + */ + public Builder withTypedRootObject(Object rootObject, TypeDescriptor typeDescriptor) { + this.rootObject = new TypedValue(rootObject, typeDescriptor); + return this; + } + + public SimpleEvaluationContext build() { + return new SimpleEvaluationContext(this.accessors, this.resolvers, this.typeConverter, this.rootObject); + } + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java new file mode 100644 index 0000000..7cde546 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java @@ -0,0 +1,315 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.support; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.BeanResolver; +import org.springframework.expression.ConstructorResolver; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.MethodFilter; +import org.springframework.expression.MethodResolver; +import org.springframework.expression.OperatorOverloader; +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.TypeComparator; +import org.springframework.expression.TypeConverter; +import org.springframework.expression.TypeLocator; +import org.springframework.expression.TypedValue; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * A powerful and highly configurable {@link EvaluationContext} implementation. + * This context uses standard implementations of all applicable strategies, + * based on reflection to resolve properties, methods and fields. + * + *

    For a simpler builder-style context variant for data-binding purposes, + * consider using {@link SimpleEvaluationContext} instead which allows for + * opting into several SpEL features as needed by specific evaluation cases. + * + * @author Andy Clement + * @author Juergen Hoeller + * @author Sam Brannen + * @since 3.0 + * @see SimpleEvaluationContext + * @see ReflectivePropertyAccessor + * @see ReflectiveConstructorResolver + * @see ReflectiveMethodResolver + * @see StandardTypeLocator + * @see StandardTypeConverter + * @see StandardTypeComparator + * @see StandardOperatorOverloader + */ +public class StandardEvaluationContext implements EvaluationContext { + + private TypedValue rootObject; + + @Nullable + private volatile List propertyAccessors; + + @Nullable + private volatile List constructorResolvers; + + @Nullable + private volatile List methodResolvers; + + @Nullable + private volatile ReflectiveMethodResolver reflectiveMethodResolver; + + @Nullable + private BeanResolver beanResolver; + + @Nullable + private TypeLocator typeLocator; + + @Nullable + private TypeConverter typeConverter; + + private TypeComparator typeComparator = new StandardTypeComparator(); + + private OperatorOverloader operatorOverloader = new StandardOperatorOverloader(); + + private final Map variables = new ConcurrentHashMap<>(); + + + /** + * Create a {@code StandardEvaluationContext} with a null root object. + */ + public StandardEvaluationContext() { + this.rootObject = TypedValue.NULL; + } + + /** + * Create a {@code StandardEvaluationContext} with the given root object. + * @param rootObject the root object to use + * @see #setRootObject + */ + public StandardEvaluationContext(@Nullable Object rootObject) { + this.rootObject = new TypedValue(rootObject); + } + + + public void setRootObject(@Nullable Object rootObject, TypeDescriptor typeDescriptor) { + this.rootObject = new TypedValue(rootObject, typeDescriptor); + } + + public void setRootObject(@Nullable Object rootObject) { + this.rootObject = (rootObject != null ? new TypedValue(rootObject) : TypedValue.NULL); + } + + @Override + public TypedValue getRootObject() { + return this.rootObject; + } + + public void setPropertyAccessors(List propertyAccessors) { + this.propertyAccessors = propertyAccessors; + } + + @Override + public List getPropertyAccessors() { + return initPropertyAccessors(); + } + + public void addPropertyAccessor(PropertyAccessor accessor) { + addBeforeDefault(initPropertyAccessors(), accessor); + } + + public boolean removePropertyAccessor(PropertyAccessor accessor) { + return initPropertyAccessors().remove(accessor); + } + + public void setConstructorResolvers(List constructorResolvers) { + this.constructorResolvers = constructorResolvers; + } + + @Override + public List getConstructorResolvers() { + return initConstructorResolvers(); + } + + public void addConstructorResolver(ConstructorResolver resolver) { + addBeforeDefault(initConstructorResolvers(), resolver); + } + + public boolean removeConstructorResolver(ConstructorResolver resolver) { + return initConstructorResolvers().remove(resolver); + } + + public void setMethodResolvers(List methodResolvers) { + this.methodResolvers = methodResolvers; + } + + @Override + public List getMethodResolvers() { + return initMethodResolvers(); + } + + public void addMethodResolver(MethodResolver resolver) { + addBeforeDefault(initMethodResolvers(), resolver); + } + + public boolean removeMethodResolver(MethodResolver methodResolver) { + return initMethodResolvers().remove(methodResolver); + } + + public void setBeanResolver(BeanResolver beanResolver) { + this.beanResolver = beanResolver; + } + + @Override + @Nullable + public BeanResolver getBeanResolver() { + return this.beanResolver; + } + + public void setTypeLocator(TypeLocator typeLocator) { + Assert.notNull(typeLocator, "TypeLocator must not be null"); + this.typeLocator = typeLocator; + } + + @Override + public TypeLocator getTypeLocator() { + if (this.typeLocator == null) { + this.typeLocator = new StandardTypeLocator(); + } + return this.typeLocator; + } + + public void setTypeConverter(TypeConverter typeConverter) { + Assert.notNull(typeConverter, "TypeConverter must not be null"); + this.typeConverter = typeConverter; + } + + @Override + public TypeConverter getTypeConverter() { + if (this.typeConverter == null) { + this.typeConverter = new StandardTypeConverter(); + } + return this.typeConverter; + } + + public void setTypeComparator(TypeComparator typeComparator) { + Assert.notNull(typeComparator, "TypeComparator must not be null"); + this.typeComparator = typeComparator; + } + + @Override + public TypeComparator getTypeComparator() { + return this.typeComparator; + } + + public void setOperatorOverloader(OperatorOverloader operatorOverloader) { + Assert.notNull(operatorOverloader, "OperatorOverloader must not be null"); + this.operatorOverloader = operatorOverloader; + } + + @Override + public OperatorOverloader getOperatorOverloader() { + return this.operatorOverloader; + } + + @Override + public void setVariable(@Nullable String name, @Nullable Object value) { + // For backwards compatibility, we ignore null names here... + // And since ConcurrentHashMap cannot store null values, we simply take null + // as a remove from the Map (with the same result from lookupVariable below). + if (name != null) { + if (value != null) { + this.variables.put(name, value); + } + else { + this.variables.remove(name); + } + } + } + + public void setVariables(Map variables) { + variables.forEach(this::setVariable); + } + + public void registerFunction(String name, Method method) { + this.variables.put(name, method); + } + + @Override + @Nullable + public Object lookupVariable(String name) { + return this.variables.get(name); + } + + /** + * Register a {@code MethodFilter} which will be called during method resolution + * for the specified type. + *

    The {@code MethodFilter} may remove methods and/or sort the methods which + * will then be used by SpEL as the candidates to look through for a match. + * @param type the type for which the filter should be called + * @param filter a {@code MethodFilter}, or {@code null} to unregister a filter for the type + * @throws IllegalStateException if the {@link ReflectiveMethodResolver} is not in use + */ + public void registerMethodFilter(Class type, MethodFilter filter) throws IllegalStateException { + initMethodResolvers(); + ReflectiveMethodResolver resolver = this.reflectiveMethodResolver; + if (resolver == null) { + throw new IllegalStateException( + "Method filter cannot be set as the reflective method resolver is not in use"); + } + resolver.registerMethodFilter(type, filter); + } + + + private List initPropertyAccessors() { + List accessors = this.propertyAccessors; + if (accessors == null) { + accessors = new ArrayList<>(5); + accessors.add(new ReflectivePropertyAccessor()); + this.propertyAccessors = accessors; + } + return accessors; + } + + private List initConstructorResolvers() { + List resolvers = this.constructorResolvers; + if (resolvers == null) { + resolvers = new ArrayList<>(1); + resolvers.add(new ReflectiveConstructorResolver()); + this.constructorResolvers = resolvers; + } + return resolvers; + } + + private List initMethodResolvers() { + List resolvers = this.methodResolvers; + if (resolvers == null) { + resolvers = new ArrayList<>(1); + this.reflectiveMethodResolver = new ReflectiveMethodResolver(); + resolvers.add(this.reflectiveMethodResolver); + this.methodResolvers = resolvers; + } + return resolvers; + } + + private static void addBeforeDefault(List resolvers, T resolver) { + resolvers.add(resolvers.size() - 1, resolver); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardOperatorOverloader.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardOperatorOverloader.java new file mode 100644 index 0000000..a2a1ea7 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardOperatorOverloader.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.support; + +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Operation; +import org.springframework.expression.OperatorOverloader; +import org.springframework.lang.Nullable; + +/** + * Standard implementation of {@link OperatorOverloader}. + * + * @author Juergen Hoeller + * @since 3.0 + */ +public class StandardOperatorOverloader implements OperatorOverloader { + + @Override + public boolean overridesOperation(Operation operation, @Nullable Object leftOperand, @Nullable Object rightOperand) + throws EvaluationException { + + return false; + } + + @Override + public Object operate(Operation operation, @Nullable Object leftOperand, @Nullable Object rightOperand) + throws EvaluationException { + + throw new EvaluationException("No operation overloaded by default"); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeComparator.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeComparator.java new file mode 100644 index 0000000..8e48aa8 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeComparator.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.support; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import org.springframework.expression.TypeComparator; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.lang.Nullable; +import org.springframework.util.NumberUtils; + +/** + * A basic {@link TypeComparator} implementation: supports comparison of + * {@link Number} types as well as types implementing {@link Comparable}. + * + * @author Andy Clement + * @author Juergen Hoeller + * @author Giovanni Dall'Oglio Risso + * @since 3.0 + */ +public class StandardTypeComparator implements TypeComparator { + + @Override + public boolean canCompare(@Nullable Object left, @Nullable Object right) { + if (left == null || right == null) { + return true; + } + if (left instanceof Number && right instanceof Number) { + return true; + } + if (left instanceof Comparable) { + return true; + } + return false; + } + + @Override + @SuppressWarnings("unchecked") + public int compare(@Nullable Object left, @Nullable Object right) throws SpelEvaluationException { + // If one is null, check if the other is + if (left == null) { + return (right == null ? 0 : -1); + } + else if (right == null) { + return 1; // left cannot be null at this point + } + + // Basic number comparisons + if (left instanceof Number && right instanceof Number) { + Number leftNumber = (Number) left; + Number rightNumber = (Number) right; + + if (leftNumber instanceof BigDecimal || rightNumber instanceof BigDecimal) { + BigDecimal leftBigDecimal = NumberUtils.convertNumberToTargetClass(leftNumber, BigDecimal.class); + BigDecimal rightBigDecimal = NumberUtils.convertNumberToTargetClass(rightNumber, BigDecimal.class); + return leftBigDecimal.compareTo(rightBigDecimal); + } + else if (leftNumber instanceof Double || rightNumber instanceof Double) { + return Double.compare(leftNumber.doubleValue(), rightNumber.doubleValue()); + } + else if (leftNumber instanceof Float || rightNumber instanceof Float) { + return Float.compare(leftNumber.floatValue(), rightNumber.floatValue()); + } + else if (leftNumber instanceof BigInteger || rightNumber instanceof BigInteger) { + BigInteger leftBigInteger = NumberUtils.convertNumberToTargetClass(leftNumber, BigInteger.class); + BigInteger rightBigInteger = NumberUtils.convertNumberToTargetClass(rightNumber, BigInteger.class); + return leftBigInteger.compareTo(rightBigInteger); + } + else if (leftNumber instanceof Long || rightNumber instanceof Long) { + return Long.compare(leftNumber.longValue(), rightNumber.longValue()); + } + else if (leftNumber instanceof Integer || rightNumber instanceof Integer) { + return Integer.compare(leftNumber.intValue(), rightNumber.intValue()); + } + else if (leftNumber instanceof Short || rightNumber instanceof Short) { + return Short.compare(leftNumber.shortValue(), rightNumber.shortValue()); + } + else if (leftNumber instanceof Byte || rightNumber instanceof Byte) { + return Byte.compare(leftNumber.byteValue(), rightNumber.byteValue()); + } + else { + // Unknown Number subtypes -> best guess is double multiplication + return Double.compare(leftNumber.doubleValue(), rightNumber.doubleValue()); + } + } + + try { + if (left instanceof Comparable) { + return ((Comparable) left).compareTo(right); + } + } + catch (ClassCastException ex) { + throw new SpelEvaluationException(ex, SpelMessage.NOT_COMPARABLE, left.getClass(), right.getClass()); + } + + throw new SpelEvaluationException(SpelMessage.NOT_COMPARABLE, left.getClass(), right.getClass()); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeConverter.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeConverter.java new file mode 100644 index 0000000..8d1a250 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeConverter.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.support; + +import org.springframework.core.convert.ConversionException; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.expression.TypeConverter; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Default implementation of the {@link TypeConverter} interface, + * delegating to a core Spring {@link ConversionService}. + * + * @author Juergen Hoeller + * @author Andy Clement + * @since 3.0 + * @see org.springframework.core.convert.ConversionService + */ +public class StandardTypeConverter implements TypeConverter { + + private final ConversionService conversionService; + + + /** + * Create a StandardTypeConverter for the default ConversionService. + * @see DefaultConversionService#getSharedInstance() + */ + public StandardTypeConverter() { + this.conversionService = DefaultConversionService.getSharedInstance(); + } + + /** + * Create a StandardTypeConverter for the given ConversionService. + * @param conversionService the ConversionService to delegate to + */ + public StandardTypeConverter(ConversionService conversionService) { + Assert.notNull(conversionService, "ConversionService must not be null"); + this.conversionService = conversionService; + } + + + @Override + public boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType) { + return this.conversionService.canConvert(sourceType, targetType); + } + + @Override + @Nullable + public Object convertValue(@Nullable Object value, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) { + try { + return this.conversionService.convert(value, sourceType, targetType); + } + catch (ConversionException ex) { + throw new SpelEvaluationException(ex, SpelMessage.TYPE_CONVERSION_ERROR, + (sourceType != null ? sourceType.toString() : (value != null ? value.getClass().getName() : "null")), + targetType.toString()); + } + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeLocator.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeLocator.java new file mode 100644 index 0000000..dafb615 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeLocator.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.support; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.expression.EvaluationException; +import org.springframework.expression.TypeLocator; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * A simple implementation of {@link TypeLocator} that uses the context ClassLoader + * (or any ClassLoader set upon it). It supports 'well-known' packages: So if a + * type cannot be found, it will try the registered imports to locate it. + * + * @author Andy Clement + * @author Juergen Hoeller + * @since 3.0 + */ +public class StandardTypeLocator implements TypeLocator { + + @Nullable + private final ClassLoader classLoader; + + private final List knownPackagePrefixes = new ArrayList<>(1); + + + /** + * Create a StandardTypeLocator for the default ClassLoader + * (typically, the thread context ClassLoader). + */ + public StandardTypeLocator() { + this(ClassUtils.getDefaultClassLoader()); + } + + /** + * Create a StandardTypeLocator for the given ClassLoader. + * @param classLoader the ClassLoader to delegate to + */ + public StandardTypeLocator(@Nullable ClassLoader classLoader) { + this.classLoader = classLoader; + // Similar to when writing regular Java code, it only knows about java.lang by default + registerImport("java.lang"); + } + + + /** + * Register a new import prefix that will be used when searching for unqualified types. + * Expected format is something like "java.lang". + * @param prefix the prefix to register + */ + public void registerImport(String prefix) { + this.knownPackagePrefixes.add(prefix); + } + + /** + * Remove that specified prefix from this locator's list of imports. + * @param prefix the prefix to remove + */ + public void removeImport(String prefix) { + this.knownPackagePrefixes.remove(prefix); + } + + /** + * Return a list of all the import prefixes registered with this StandardTypeLocator. + * @return a list of registered import prefixes + */ + public List getImportPrefixes() { + return Collections.unmodifiableList(this.knownPackagePrefixes); + } + + + /** + * Find a (possibly unqualified) type reference - first using the type name as-is, + * then trying any registered prefixes if the type name cannot be found. + * @param typeName the type to locate + * @return the class object for the type + * @throws EvaluationException if the type cannot be found + */ + @Override + public Class findType(String typeName) throws EvaluationException { + String nameToLookup = typeName; + try { + return ClassUtils.forName(nameToLookup, this.classLoader); + } + catch (ClassNotFoundException ey) { + // try any registered prefixes before giving up + } + for (String prefix : this.knownPackagePrefixes) { + try { + nameToLookup = prefix + '.' + typeName; + return ClassUtils.forName(nameToLookup, this.classLoader); + } + catch (ClassNotFoundException ex) { + // might be a different prefix + } + } + throw new SpelEvaluationException(SpelMessage.TYPE_NOT_FOUND, typeName); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/package-info.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/package-info.java new file mode 100644 index 0000000..d77cb74 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/package-info.java @@ -0,0 +1,9 @@ +/** + * SpEL's default implementations for various core abstractions. + */ +@NonNullApi +@NonNullFields +package org.springframework.expression.spel.support; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-expression/src/main/resources/org/springframework/expression/spel/generated/SpringExpressions.g b/spring-expression/src/main/resources/org/springframework/expression/spel/generated/SpringExpressions.g new file mode 100644 index 0000000..82a5f4a --- /dev/null +++ b/spring-expression/src/main/resources/org/springframework/expression/spel/generated/SpringExpressions.g @@ -0,0 +1,268 @@ +grammar SpringExpressions; + +options { + language = Java; + output=AST; + k=2; +} + +tokens { + INTEGER_LITERAL; + EXPRESSION; + QUALIFIED_IDENTIFIER; + PROPERTY_OR_FIELD; + INDEXER; + CONSTRUCTOR; + HOLDER; + NAMED_ARGUMENT; + FUNCTIONREF; + TYPEREF; + VARIABLEREF; + METHOD; + ADD; + SUBTRACT; + NUMBER; +} + +// applies only to the parser: +@header {package org.springframework.expression.spel.generated;} + +// applies only to the lexer: +@lexer::header {package org.springframework.expression.spel.generated;} + +@members { + // For collecting info whilst processing rules that can be used in messages + protected Stack paraphrase = new Stack(); +} + +@rulecatch { + catch(RecognitionException e) { + reportError(e); + throw e; + } +} + +expr: expression EOF!; + +expression : + logicalOrExpression + ( (ASSIGN^ logicalOrExpression) + | (DEFAULT^ logicalOrExpression) + | (QMARK^ expression COLON! expression))?; + +parenExpr : LPAREN! expression RPAREN!; + +logicalOrExpression +: logicalAndExpression (OR^ logicalAndExpression)*; + +logicalAndExpression +: relationalExpression (AND^ relationalExpression)*; + +relationalExpression : sumExpression (relationalOperator^ sumExpression)?; + +sumExpression + : productExpression ( (PLUS^ | MINUS^) productExpression)*; + +productExpression + : powerExpr ((STAR^ | DIV^| MOD^) powerExpr)* ; + +powerExpr : unaryExpression (POWER^ unaryExpression)? ; + +unaryExpression + : (PLUS^ | MINUS^ | BANG^) unaryExpression + | primaryExpression ; + +primaryExpression + : startNode (node)? -> ^(EXPRESSION startNode (node)?); + +startNode + : + parenExpr + | methodOrProperty + | functionOrVar + | indexer + | literal + | type + | constructor + | projection + | selection + | firstSelection + | lastSelection + ; + +node + : ((DOT dottedNode) | nonDottedNode)+; + +nonDottedNode + : indexer; + +dottedNode + : + ((methodOrProperty + | functionOrVar + | projection + | selection + | firstSelection + | lastSelection + )) + ; + +functionOrVar + : (POUND ID LPAREN) => function + | var + ; + +function : POUND id=ID methodArgs -> ^(FUNCTIONREF[$id] methodArgs); + +var : POUND id=ID -> ^(VARIABLEREF[$id]); + + +methodOrProperty + : (ID LPAREN) => id=ID methodArgs -> ^(METHOD[$id] methodArgs) + | property + ; + +// may have to preserve these commas to make it easier to offer suggestions in the right place +// mod at 9th feb 19:13 - added the second 'COMMA?' to allow for code completion "foo(A," +// TODO need to preserve commas and then check for badly formed call later (optimizing tree walk) to disallow "foo(a,b,c,)" +methodArgs : LPAREN! (argument (COMMA! argument)* (COMMA!)?)? RPAREN!; + +// If we match ID then create a node called PROPERTY_OR_FIELD and copy the id info into it. +// this means the propertyOrField.text is what id.text would have been, rather than having to +// access id as a child of the new node. +property: id=ID -> ^(PROPERTY_OR_FIELD[$id]); + + +indexer: LBRACKET r1=argument (COMMA r2=argument)* RBRACKET -> ^(INDEXER $r1 ($r2)*); + +// argument; + // TODO make expression conditional with ? if want completion for when the RCURLY is missing +projection: PROJECT^ expression RBRACKET!; + +selection: SELECT^ expression RBRACKET!; + +firstSelection: SELECT_FIRST^ expression RBRACKET!; + +lastSelection: SELECT_LAST^ expression RBRACKET!; + +// TODO cope with array types +type: TYPE qualifiedId RPAREN -> ^(TYPEREF qualifiedId); +//type: TYPE tn=qualifiedId (LBRACKET RBRACKET)? (COMMA qid=qualifiedId)? RPAREN + + +constructor + : ('new' qualifiedId LPAREN) => 'new' qualifiedId ctorArgs -> ^(CONSTRUCTOR qualifiedId ctorArgs) + ; + +ctorArgs + : LPAREN! (namedArgument (COMMA! namedArgument)*)? RPAREN!; + +argument : expression; + +namedArgument + : (ID ASSIGN) => id=ID ASSIGN expression + -> ^(NAMED_ARGUMENT[$id] expression) + | argument ; + +qualifiedId : ID (DOT ID)* -> ^(QUALIFIED_IDENTIFIER ID*); + +contextName : ID (DIV ID)* -> ^(QUALIFIED_IDENTIFIER ID*); + +literal + : INTEGER_LITERAL + | STRING_LITERAL + | DQ_STRING_LITERAL + | boolLiteral + | NULL_LITERAL + | HEXADECIMAL_INTEGER_LITERAL + | REAL_LITERAL + ; + +boolLiteral: TRUE | FALSE; + +INTEGER_LITERAL + : (DECIMAL_DIGIT)+ (INTEGER_TYPE_SUFFIX)?; + +HEXADECIMAL_INTEGER_LITERAL : ('0x' | '0X') (HEX_DIGIT)+ (INTEGER_TYPE_SUFFIX)?; + +relationalOperator + : EQUAL + | NOT_EQUAL + | LESS_THAN + | LESS_THAN_OR_EQUAL + | GREATER_THAN + | GREATER_THAN_OR_EQUAL + | INSTANCEOF + | BETWEEN + | MATCHES + ; + +ASSIGN: '='; +EQUAL: '=='; +NOT_EQUAL: '!='; +LESS_THAN: '<'; +LESS_THAN_OR_EQUAL: '<='; +GREATER_THAN: '>'; +GREATER_THAN_OR_EQUAL: '>='; +INSTANCEOF: 'instanceof'; +BETWEEN:'between'; +MATCHES:'matches'; +NULL_LITERAL: 'null'; + +SEMI: ';'; +DOT: '.'; +COMMA: ','; +LPAREN: '('; +RPAREN: ')'; +LCURLY: '{'; +RCURLY: '}'; +LBRACKET: '['; +RBRACKET: ']'; +PIPE: '|'; + +AND: 'and'; +OR: 'or'; +FALSE: 'false'; +TRUE: 'true'; + +PLUS: '+'; +MINUS: '-'; +DIV: '/'; +STAR: '*'; +MOD: '%'; +POWER: '^'; +BANG: '!'; +POUND: '#'; +QMARK: '?'; +DEFAULT: '??'; +PROJECT: '!['; +SELECT: '?['; +SELECT_FIRST: '^['; +SELECT_LAST: '$['; +TYPE: 'T('; + +STRING_LITERAL: '\''! (APOS|~'\'')* '\''!; +DQ_STRING_LITERAL: '"'! (~'"')* '"'!; +ID: ('a'..'z'|'A'..'Z'|'_') ('a'..'z'|'A'..'Z'|'_'|'0'..'9'|DOT_ESCAPED)*; +DOT_ESCAPED: '\\.'; +WS: ( ' ' | '\t' | '\n' |'\r')+ { $channel=HIDDEN; } ; +DOLLAR: '$'; +AT: '@'; +UPTO: '..'; +COLON: ':'; + + +REAL_LITERAL : + ('.' (DECIMAL_DIGIT)+ (EXPONENT_PART)? (REAL_TYPE_SUFFIX)?) | + ((DECIMAL_DIGIT)+ '.' (DECIMAL_DIGIT)+ (EXPONENT_PART)? (REAL_TYPE_SUFFIX)?) | + ((DECIMAL_DIGIT)+ (EXPONENT_PART) (REAL_TYPE_SUFFIX)?) | + ((DECIMAL_DIGIT)+ (REAL_TYPE_SUFFIX)); + +fragment APOS : '\''! '\''; +fragment DECIMAL_DIGIT : '0'..'9' ; +fragment INTEGER_TYPE_SUFFIX : ( 'L' | 'l' ); +fragment HEX_DIGIT : '0'|'1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'|'A'|'B'|'C'|'D'|'E'|'F'|'a'|'b'|'c'|'d'|'e'|'f'; + +fragment EXPONENT_PART : 'e' (SIGN)* (DECIMAL_DIGIT)+ | 'E' (SIGN)* (DECIMAL_DIGIT)+ ; +fragment SIGN : '+' | '-' ; +fragment REAL_TYPE_SUFFIX : 'F' | 'f' | 'D' | 'd'; diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/AbstractExpressionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/AbstractExpressionTests.java new file mode 100644 index 0000000..7a682db --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/AbstractExpressionTests.java @@ -0,0 +1,309 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import java.util.Arrays; +import java.util.List; + +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.util.ObjectUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Common superclass for expression tests. + * + * @author Andy Clement + */ +public abstract class AbstractExpressionTests { + + private static final boolean DEBUG = false; + + protected static final boolean SHOULD_BE_WRITABLE = true; + + protected static final boolean SHOULD_NOT_BE_WRITABLE = false; + + + protected final ExpressionParser parser = new SpelExpressionParser(); + + protected final StandardEvaluationContext context = TestScenarioCreator.getTestEvaluationContext(); + + + /** + * Evaluate an expression and check that the actual result matches the + * expectedValue and the class of the result matches the expectedClassOfResult. + * @param expression the expression to evaluate + * @param expectedValue the expected result for evaluating the expression + * @param expectedResultType the expected class of the evaluation result + */ + public void evaluate(String expression, Object expectedValue, Class expectedResultType) { + Expression expr = parser.parseExpression(expression); + assertThat(expr).as("expression").isNotNull(); + if (DEBUG) { + SpelUtilities.printAbstractSyntaxTree(System.out, expr); + } + + Object value = expr.getValue(context); + + // Check the return value + if (value == null) { + if (expectedValue == null) { + return; // no point doing other checks + } + assertThat(expectedValue).as("Expression returned null value, but expected '" + expectedValue + "'").isNull(); + } + + Class resultType = value.getClass(); + assertThat(resultType).as("Type of the actual result was not as expected. Expected '" + expectedResultType + + "' but result was of type '" + resultType + "'").isEqualTo(expectedResultType); + + if (expectedValue instanceof String) { + assertThat(AbstractExpressionTests.stringValueOf(value)).as("Did not get expected value for expression '" + expression + "'.").isEqualTo(expectedValue); + } + else { + assertThat(value).as("Did not get expected value for expression '" + expression + "'.").isEqualTo(expectedValue); + } + } + + public void evaluateAndAskForReturnType(String expression, Object expectedValue, Class expectedResultType) { + Expression expr = parser.parseExpression(expression); + assertThat(expr).as("expression").isNotNull(); + if (DEBUG) { + SpelUtilities.printAbstractSyntaxTree(System.out, expr); + } + + Object value = expr.getValue(context, expectedResultType); + if (value == null) { + if (expectedValue == null) { + return; // no point doing other checks + } + assertThat(expectedValue).as("Expression returned null value, but expected '" + expectedValue + "'").isNull(); + } + + Class resultType = value.getClass(); + assertThat(resultType).as("Type of the actual result was not as expected. Expected '" + expectedResultType + + "' but result was of type '" + resultType + "'").isEqualTo(expectedResultType); + assertThat(value).as("Did not get expected value for expression '" + expression + "'.").isEqualTo(expectedValue); + } + + /** + * Evaluate an expression and check that the actual result matches the + * expectedValue and the class of the result matches the expectedClassOfResult. + * This method can also check if the expression is writable (for example, + * it is a variable or property reference). + * @param expression the expression to evaluate + * @param expectedValue the expected result for evaluating the expression + * @param expectedClassOfResult the expected class of the evaluation result + * @param shouldBeWritable should the parsed expression be writable? + */ + public void evaluate(String expression, Object expectedValue, Class expectedClassOfResult, boolean shouldBeWritable) { + Expression expr = parser.parseExpression(expression); + assertThat(expr).as("expression").isNotNull(); + if (DEBUG) { + SpelUtilities.printAbstractSyntaxTree(System.out, expr); + } + Object value = expr.getValue(context); + if (value == null) { + if (expectedValue == null) { + return; // no point doing other checks + } + assertThat(expectedValue).as("Expression returned null value, but expected '" + expectedValue + "'").isNull(); + } + Class resultType = value.getClass(); + if (expectedValue instanceof String) { + assertThat(AbstractExpressionTests.stringValueOf(value)).as("Did not get expected value for expression '" + expression + "'.").isEqualTo(expectedValue); + } + else { + assertThat(value).as("Did not get expected value for expression '" + expression + "'.").isEqualTo(expectedValue); + } + assertThat(expectedClassOfResult.equals(resultType)).as("Type of the result was not as expected. Expected '" + expectedClassOfResult + + "' but result was of type '" + resultType + "'").isTrue(); + + assertThat(expr.isWritable(context)).as("isWritable").isEqualTo(shouldBeWritable); + } + + /** + * Evaluate the specified expression and ensure the expected message comes out. + * The message may have inserts and they will be checked if otherProperties is specified. + * The first entry in otherProperties should always be the position. + * @param expression the expression to evaluate + * @param expectedMessage the expected message + * @param otherProperties the expected inserts within the message + */ + protected void evaluateAndCheckError(String expression, SpelMessage expectedMessage, Object... otherProperties) { + evaluateAndCheckError(expression, null, expectedMessage, otherProperties); + } + + /** + * Evaluate the specified expression and ensure the expected message comes out. + * The message may have inserts and they will be checked if otherProperties is specified. + * The first entry in otherProperties should always be the position. + * @param expression the expression to evaluate + * @param expectedReturnType ask the expression return value to be of this type if possible + * ({@code null} indicates don't ask for conversion) + * @param expectedMessage the expected message + * @param otherProperties the expected inserts within the message + */ + protected void evaluateAndCheckError(String expression, Class expectedReturnType, SpelMessage expectedMessage, + Object... otherProperties) { + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> { + Expression expr = parser.parseExpression(expression); + assertThat(expr).as("expression").isNotNull(); + if (expectedReturnType != null) { + expr.getValue(context, expectedReturnType); + } + else { + expr.getValue(context); + } + }).satisfies(ex -> { + assertThat(ex.getMessageCode()).isEqualTo(expectedMessage); + if (!ObjectUtils.isEmpty(otherProperties)) { + // first one is expected position of the error within the string + int pos = ((Integer) otherProperties[0]).intValue(); + assertThat(ex.getPosition()).as("position").isEqualTo(pos); + if (otherProperties.length > 1) { + // Check inserts match + Object[] inserts = ex.getInserts(); + assertThat(inserts).as("inserts").hasSizeGreaterThanOrEqualTo(otherProperties.length - 1); + Object[] expectedInserts = new Object[inserts.length]; + System.arraycopy(otherProperties, 1, expectedInserts, 0, expectedInserts.length); + assertThat(inserts).as("inserts").containsExactly(expectedInserts); + } + } + }); + } + + /** + * Parse the specified expression and ensure the expected message comes out. + * The message may have inserts and they will be checked if otherProperties is specified. + * The first entry in otherProperties should always be the position. + * @param expression the expression to evaluate + * @param expectedMessage the expected message + * @param otherProperties the expected inserts within the message + */ + protected void parseAndCheckError(String expression, SpelMessage expectedMessage, Object... otherProperties) { + assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> { + Expression expr = parser.parseExpression(expression); + SpelUtilities.printAbstractSyntaxTree(System.out, expr); + }).satisfies(ex -> { + assertThat(ex.getMessageCode()).isEqualTo(expectedMessage); + if (otherProperties != null && otherProperties.length != 0) { + // first one is expected position of the error within the string + int pos = ((Integer) otherProperties[0]).intValue(); + assertThat(pos).as("reported position").isEqualTo(pos); + if (otherProperties.length > 1) { + // Check inserts match + Object[] inserts = ex.getInserts(); + assertThat(inserts).as("inserts").hasSizeGreaterThanOrEqualTo(otherProperties.length - 1); + Object[] expectedInserts = new Object[inserts.length]; + System.arraycopy(otherProperties, 1, expectedInserts, 0, expectedInserts.length); + assertThat(inserts).as("inserts").containsExactly(expectedInserts); + } + } + }); + } + + + protected static String stringValueOf(Object value) { + return stringValueOf(value, false); + } + + /** + * Produce a nice string representation of the input object. + * @param value object to be formatted + * @return a nice string + */ + protected static String stringValueOf(Object value, boolean isNested) { + // do something nice for arrays + if (value == null) { + return "null"; + } + if (value.getClass().isArray()) { + StringBuilder sb = new StringBuilder(); + if (value.getClass().getComponentType().isPrimitive()) { + Class primitiveType = value.getClass().getComponentType(); + if (primitiveType == Integer.TYPE) { + int[] l = (int[]) value; + sb.append("int[").append(l.length).append("]{"); + for (int j = 0; j < l.length; j++) { + if (j > 0) { + sb.append(","); + } + sb.append(stringValueOf(l[j])); + } + sb.append("}"); + } + else if (primitiveType == Long.TYPE) { + long[] l = (long[]) value; + sb.append("long[").append(l.length).append("]{"); + for (int j = 0; j < l.length; j++) { + if (j > 0) { + sb.append(","); + } + sb.append(stringValueOf(l[j])); + } + sb.append("}"); + } + else { + throw new RuntimeException("Please implement support for type " + primitiveType.getName() + + " in ExpressionTestCase.stringValueOf()"); + } + } + else if (value.getClass().getComponentType().isArray()) { + List l = Arrays.asList((Object[]) value); + if (!isNested) { + sb.append(value.getClass().getComponentType().getName()); + } + sb.append("[").append(l.size()).append("]{"); + int i = 0; + for (Object object : l) { + if (i > 0) { + sb.append(","); + } + i++; + sb.append(stringValueOf(object, true)); + } + sb.append("}"); + } + else { + List l = Arrays.asList((Object[]) value); + if (!isNested) { + sb.append(value.getClass().getComponentType().getName()); + } + sb.append("[").append(l.size()).append("]{"); + int i = 0; + for (Object object : l) { + if (i > 0) { + sb.append(","); + } + i++; + sb.append(stringValueOf(object)); + } + sb.append("}"); + } + return sb.toString(); + } + else { + return value.toString(); + } + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ArrayConstructorTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ArrayConstructorTests.java new file mode 100644 index 0000000..267b45d --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ArrayConstructorTests.java @@ -0,0 +1,206 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test construction of arrays. + * + * @author Andy Clement + */ +public class ArrayConstructorTests extends AbstractExpressionTests { + + @Test + public void simpleArrayWithInitializer() { + evaluateArrayBuildingExpression("new int[]{1,2,3}", "[1,2,3]"); + evaluateArrayBuildingExpression("new int[]{}", "[]"); + evaluate("new int[]{}.length", "0", Integer.class); + } + + @Test + public void conversion() { + evaluate("new String[]{1,2,3}[0]", "1", String.class); + evaluate("new int[]{'123'}[0]", 123, Integer.class); + } + + @Test + public void multidimensionalArrays() { + evaluateAndCheckError("new int[][]{{1,2},{3,4}}", SpelMessage.MULTIDIM_ARRAY_INITIALIZER_NOT_SUPPORTED); + evaluateAndCheckError("new int[3][]", SpelMessage.MISSING_ARRAY_DIMENSION); + evaluateAndCheckError("new int[]", SpelMessage.MISSING_ARRAY_DIMENSION); + evaluateAndCheckError("new String[]", SpelMessage.MISSING_ARRAY_DIMENSION); + evaluateAndCheckError("new int[][1]", SpelMessage.MISSING_ARRAY_DIMENSION); + } + + @Test + public void primitiveTypeArrayConstructors() { + evaluateArrayBuildingExpression("new int[]{1,2,3,4}", "[1,2,3,4]"); + evaluateArrayBuildingExpression("new boolean[]{true,false,true}", "[true,false,true]"); + evaluateArrayBuildingExpression("new char[]{'a','b','c'}", "[a,b,c]"); + evaluateArrayBuildingExpression("new long[]{1,2,3,4,5}", "[1,2,3,4,5]"); + evaluateArrayBuildingExpression("new short[]{2,3,4,5,6}", "[2,3,4,5,6]"); + evaluateArrayBuildingExpression("new double[]{1d,2d,3d,4d}", "[1.0,2.0,3.0,4.0]"); + evaluateArrayBuildingExpression("new float[]{1f,2f,3f,4f}", "[1.0,2.0,3.0,4.0]"); + evaluateArrayBuildingExpression("new byte[]{1,2,3,4}", "[1,2,3,4]"); + } + + @Test + public void primitiveTypeArrayConstructorsElements() { + evaluate("new int[]{1,2,3,4}[0]", 1, Integer.class); + evaluate("new boolean[]{true,false,true}[0]", true, Boolean.class); + evaluate("new char[]{'a','b','c'}[0]", 'a', Character.class); + evaluate("new long[]{1,2,3,4,5}[0]", 1L, Long.class); + evaluate("new short[]{2,3,4,5,6}[0]", (short) 2, Short.class); + evaluate("new double[]{1d,2d,3d,4d}[0]", (double) 1, Double.class); + evaluate("new float[]{1f,2f,3f,4f}[0]", (float) 1, Float.class); + evaluate("new byte[]{1,2,3,4}[0]", (byte) 1, Byte.class); + evaluate("new String(new char[]{'h','e','l','l','o'})", "hello", String.class); + } + + @Test + public void errorCases() { + evaluateAndCheckError("new char[7]{'a','c','d','e'}", SpelMessage.INITIALIZER_LENGTH_INCORRECT); + evaluateAndCheckError("new char[3]{'a','c','d','e'}", SpelMessage.INITIALIZER_LENGTH_INCORRECT); + evaluateAndCheckError("new char[2]{'hello','world'}", SpelMessage.TYPE_CONVERSION_ERROR); + evaluateAndCheckError("new String('a','c','d')", SpelMessage.CONSTRUCTOR_INVOCATION_PROBLEM); + } + + @Test + public void typeArrayConstructors() { + evaluate("new String[]{'a','b','c','d'}[1]", "b", String.class); + evaluateAndCheckError("new String[]{'a','b','c','d'}.size()", SpelMessage.METHOD_NOT_FOUND, 30, "size()", + "java.lang.String[]"); + evaluate("new String[]{'a','b','c','d'}.length", 4, Integer.class); + } + + @Test + public void basicArray() { + evaluate("new String[3]", "java.lang.String[3]{null,null,null}", String[].class); + } + + @Test + public void multiDimensionalArray() { + evaluate("new String[2][2]", "[Ljava.lang.String;[2]{[2]{null,null},[2]{null,null}}", String[][].class); + evaluate("new String[3][2][1]", + "[[Ljava.lang.String;[3]{[2]{[1]{null},[1]{null}},[2]{[1]{null},[1]{null}},[2]{[1]{null},[1]{null}}}", + String[][][].class); + } + + @Test + public void constructorInvocation03() { + evaluateAndCheckError("new String[]", SpelMessage.MISSING_ARRAY_DIMENSION); + } + + public void constructorInvocation04() { + evaluateAndCheckError("new Integer[3]{'3','ghi','5'}", SpelMessage.INCORRECT_ELEMENT_TYPE_FOR_ARRAY, 4); + } + + private String evaluateArrayBuildingExpression(String expression, String expectedToString) { + SpelExpressionParser parser = new SpelExpressionParser(); + Expression e = parser.parseExpression(expression); + Object o = e.getValue(); + assertThat(o).isNotNull(); + assertThat(o.getClass().isArray()).isTrue(); + StringBuilder s = new StringBuilder(); + s.append('['); + if (o instanceof int[]) { + int[] array = (int[]) o; + for (int i = 0; i < array.length; i++) { + if (i > 0) { + s.append(','); + } + s.append(array[i]); + } + } + else if (o instanceof boolean[]) { + boolean[] array = (boolean[]) o; + for (int i = 0; i < array.length; i++) { + if (i > 0) { + s.append(','); + } + s.append(array[i]); + } + } + else if (o instanceof char[]) { + char[] array = (char[]) o; + for (int i = 0; i < array.length; i++) { + if (i > 0) { + s.append(','); + } + s.append(array[i]); + } + } + else if (o instanceof long[]) { + long[] array = (long[]) o; + for (int i = 0; i < array.length; i++) { + if (i > 0) { + s.append(','); + } + s.append(array[i]); + } + } + else if (o instanceof short[]) { + short[] array = (short[]) o; + for (int i = 0; i < array.length; i++) { + if (i > 0) { + s.append(','); + } + s.append(array[i]); + } + } + else if (o instanceof double[]) { + double[] array = (double[]) o; + for (int i = 0; i < array.length; i++) { + if (i > 0) { + s.append(','); + } + s.append(array[i]); + } + } + else if (o instanceof float[]) { + float[] array = (float[]) o; + for (int i = 0; i < array.length; i++) { + if (i > 0) { + s.append(','); + } + s.append(array[i]); + } + } + else if (o instanceof byte[]) { + byte[] array = (byte[]) o; + for (int i = 0; i < array.length; i++) { + if (i > 0) { + s.append(','); + } + s.append(array[i]); + } + } + else { + throw new IllegalStateException("Not supported " + o.getClass()); + } + s.append(']'); + assertThat(s.toString()).isEqualTo(expectedToString); + return s.toString(); + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/BooleanExpressionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/BooleanExpressionTests.java new file mode 100644 index 0000000..9761734 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/BooleanExpressionTests.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.expression.spel.support.StandardTypeConverter; + +/** + * Tests the evaluation of real boolean expressions, these use AND, OR, NOT, TRUE, FALSE + * + * @author Andy Clement + * @author Oliver Becker + */ +public class BooleanExpressionTests extends AbstractExpressionTests { + + @Test + public void testBooleanTrue() { + evaluate("true", Boolean.TRUE, Boolean.class); + } + + @Test + public void testBooleanFalse() { + evaluate("false", Boolean.FALSE, Boolean.class); + } + + @Test + public void testOr() { + evaluate("false or false", Boolean.FALSE, Boolean.class); + evaluate("false or true", Boolean.TRUE, Boolean.class); + evaluate("true or false", Boolean.TRUE, Boolean.class); + evaluate("true or true", Boolean.TRUE, Boolean.class); + } + + @Test + public void testAnd() { + evaluate("false and false", Boolean.FALSE, Boolean.class); + evaluate("false and true", Boolean.FALSE, Boolean.class); + evaluate("true and false", Boolean.FALSE, Boolean.class); + evaluate("true and true", Boolean.TRUE, Boolean.class); + } + + @Test + public void testNot() { + evaluate("!false", Boolean.TRUE, Boolean.class); + evaluate("!true", Boolean.FALSE, Boolean.class); + + evaluate("not false", Boolean.TRUE, Boolean.class); + evaluate("NoT true", Boolean.FALSE, Boolean.class); + } + + @Test + public void testCombinations01() { + evaluate("false and false or true", Boolean.TRUE, Boolean.class); + evaluate("true and false or true", Boolean.TRUE, Boolean.class); + evaluate("true and false or false", Boolean.FALSE, Boolean.class); + } + + @Test + public void testWritability() { + evaluate("true and true", Boolean.TRUE, Boolean.class, false); + evaluate("true or true", Boolean.TRUE, Boolean.class, false); + evaluate("!false", Boolean.TRUE, Boolean.class, false); + } + + @Test + public void testBooleanErrors01() { + evaluateAndCheckError("1.0 or false", SpelMessage.TYPE_CONVERSION_ERROR, 0); + evaluateAndCheckError("false or 39.4", SpelMessage.TYPE_CONVERSION_ERROR, 9); + evaluateAndCheckError("true and 'hello'", SpelMessage.TYPE_CONVERSION_ERROR, 9); + evaluateAndCheckError(" 'hello' and 'goodbye'", SpelMessage.TYPE_CONVERSION_ERROR, 1); + evaluateAndCheckError("!35.2", SpelMessage.TYPE_CONVERSION_ERROR, 1); + evaluateAndCheckError("! 'foob'", SpelMessage.TYPE_CONVERSION_ERROR, 2); + } + + @Test + public void testConvertAndHandleNull() { // SPR-9445 + // without null conversion + evaluateAndCheckError("null or true", SpelMessage.TYPE_CONVERSION_ERROR, 0, "null", "boolean"); + evaluateAndCheckError("null and true", SpelMessage.TYPE_CONVERSION_ERROR, 0, "null", "boolean"); + evaluateAndCheckError("!null", SpelMessage.TYPE_CONVERSION_ERROR, 1, "null", "boolean"); + evaluateAndCheckError("null ? 'foo' : 'bar'", SpelMessage.TYPE_CONVERSION_ERROR, 0, "null", "boolean"); + + // with null conversion (null -> false) + GenericConversionService conversionService = new GenericConversionService() { + @Override + protected Object convertNullSource(TypeDescriptor sourceType, TypeDescriptor targetType) { + return targetType.getType() == Boolean.class ? false : null; + } + }; + context.setTypeConverter(new StandardTypeConverter(conversionService)); + + evaluate("null or true", Boolean.TRUE, Boolean.class, false); + evaluate("null and true", Boolean.FALSE, Boolean.class, false); + evaluate("!null", Boolean.TRUE, Boolean.class, false); + evaluate("null ? 'foo' : 'bar'", "bar", String.class, false); + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/CachedMethodExecutorTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/CachedMethodExecutorTests.java new file mode 100644 index 0000000..147faf8 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/CachedMethodExecutorTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.ast.MethodReference; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for caching in {@link MethodReference} (SPR-10657). + * + * @author Oliver Becker + */ +public class CachedMethodExecutorTests { + + private final ExpressionParser parser = new SpelExpressionParser(); + + private final StandardEvaluationContext context = new StandardEvaluationContext(new RootObject()); + + + @Test + public void testCachedExecutionForParameters() { + Expression expression = this.parser.parseExpression("echo(#var)"); + + assertMethodExecution(expression, 42, "int: 42"); + assertMethodExecution(expression, 42, "int: 42"); + assertMethodExecution(expression, "Deep Thought", "String: Deep Thought"); + assertMethodExecution(expression, 42, "int: 42"); + } + + @Test + public void testCachedExecutionForTarget() { + Expression expression = this.parser.parseExpression("#var.echo(42)"); + + assertMethodExecution(expression, new RootObject(), "int: 42"); + assertMethodExecution(expression, new RootObject(), "int: 42"); + assertMethodExecution(expression, new BaseObject(), "String: 42"); + assertMethodExecution(expression, new RootObject(), "int: 42"); + } + + private void assertMethodExecution(Expression expression, Object var, String expected) { + this.context.setVariable("var", var); + assertThat(expression.getValue(this.context)).isEqualTo(expected); + } + + + public static class BaseObject { + + public String echo(String value) { + return "String: " + value; + } + } + + public static class RootObject extends BaseObject { + + public String echo(int value) { + return "int: " + value; + } + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ConstructorInvocationTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ConstructorInvocationTests.java new file mode 100644 index 0000000..89ea09b --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ConstructorInvocationTests.java @@ -0,0 +1,239 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.AccessException; +import org.springframework.expression.ConstructorExecutor; +import org.springframework.expression.ConstructorResolver; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.expression.spel.testresources.PlaceOfBirth; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests invocation of constructors. + * + * @author Andy Clement + */ +public class ConstructorInvocationTests extends AbstractExpressionTests { + + @Test + public void testTypeConstructors() { + evaluate("new String('hello world')", "hello world", String.class); + } + + @Test + public void testNonExistentType() { + evaluateAndCheckError("new FooBar()", SpelMessage.CONSTRUCTOR_INVOCATION_PROBLEM); + } + + + @SuppressWarnings("serial") + static class TestException extends Exception { + + } + + static class Tester { + + public static int counter; + public int i; + + + public Tester() { + } + + public Tester(int i) throws Exception { + counter++; + if (i == 1) { + throw new IllegalArgumentException("IllegalArgumentException for 1"); + } + if (i == 2) { + throw new RuntimeException("RuntimeException for 2"); + } + if (i == 4) { + throw new TestException(); + } + this.i = i; + } + + public Tester(PlaceOfBirth pob) { + + } + + } + + + @Test + public void testConstructorThrowingException_SPR6760() { + // Test ctor on inventor: + // On 1 it will throw an IllegalArgumentException + // On 2 it will throw a RuntimeException + // On 3 it will exit normally + // In each case it increments the Tester field 'counter' when invoked + + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expr = parser.parseExpression("new org.springframework.expression.spel.ConstructorInvocationTests$Tester(#bar).i"); + + // Normal exit + StandardEvaluationContext eContext = TestScenarioCreator.getTestEvaluationContext(); + eContext.setRootObject(new Tester()); + eContext.setVariable("bar", 3); + Object o = expr.getValue(eContext); + assertThat(o).isEqualTo(3); + assertThat(parser.parseExpression("counter").getValue(eContext)).isEqualTo(1); + + // Now the expression has cached that throwException(int) is the right thing to + // call. Let's change 'bar' to be a PlaceOfBirth which indicates the cached + // reference is out of date. + eContext.setVariable("bar", new PlaceOfBirth("London")); + o = expr.getValue(eContext); + assertThat(o).isEqualTo(0); + // That confirms the logic to mark the cached reference stale and retry is working + + // Now let's cause the method to exit via exception and ensure it doesn't cause + // a retry. + + // First, switch back to throwException(int) + eContext.setVariable("bar", 3); + o = expr.getValue(eContext); + assertThat(o).isEqualTo(3); + assertThat(parser.parseExpression("counter").getValue(eContext)).isEqualTo(2); + + // 4 will make it throw a checked exception - this will be wrapped by spel on the + // way out + eContext.setVariable("bar", 4); + assertThatExceptionOfType(Exception.class).isThrownBy(() -> + expr.getValue(eContext)) + .withMessageContaining("Tester"); + // A problem occurred whilst attempting to construct an object of type + // 'org.springframework.expression.spel.ConstructorInvocationTests$Tester' + // using arguments '(java.lang.Integer)' + + // If counter is 4 then the method got called twice! + assertThat(parser.parseExpression("counter").getValue(eContext)).isEqualTo(3); + + // 1 will make it throw a RuntimeException - SpEL will let this through + eContext.setVariable("bar", 1); + assertThatExceptionOfType(Exception.class) + .isThrownBy(() -> expr.getValue(eContext)) + .isNotInstanceOf(SpelEvaluationException.class); + // A problem occurred whilst attempting to construct an object of type + // 'org.springframework.expression.spel.ConstructorInvocationTests$Tester' + // using arguments '(java.lang.Integer)' + + // If counter is 5 then the method got called twice! + assertThat(parser.parseExpression("counter").getValue(eContext)).isEqualTo(4); + } + + @Test + public void testAddingConstructorResolvers() { + StandardEvaluationContext ctx = new StandardEvaluationContext(); + + // reflective constructor accessor is the only one by default + List constructorResolvers = ctx.getConstructorResolvers(); + assertThat(constructorResolvers.size()).isEqualTo(1); + + ConstructorResolver dummy = new DummyConstructorResolver(); + ctx.addConstructorResolver(dummy); + assertThat(ctx.getConstructorResolvers().size()).isEqualTo(2); + + List copy = new ArrayList<>(ctx.getConstructorResolvers()); + assertThat(ctx.removeConstructorResolver(dummy)).isTrue(); + assertThat(ctx.removeConstructorResolver(dummy)).isFalse(); + assertThat(ctx.getConstructorResolvers().size()).isEqualTo(1); + + ctx.setConstructorResolvers(copy); + assertThat(ctx.getConstructorResolvers().size()).isEqualTo(2); + } + + + static class DummyConstructorResolver implements ConstructorResolver { + + @Override + public ConstructorExecutor resolve(EvaluationContext context, String typeName, + List argumentTypes) throws AccessException { + throw new UnsupportedOperationException("Auto-generated method stub"); + } + + } + + + @Test + public void testVarargsInvocation01() { + // Calling 'Fruit(String... strings)' + evaluate("new org.springframework.expression.spel.testresources.Fruit('a','b','c').stringscount()", 3, + Integer.class); + evaluate("new org.springframework.expression.spel.testresources.Fruit('a').stringscount()", 1, Integer.class); + evaluate("new org.springframework.expression.spel.testresources.Fruit().stringscount()", 0, Integer.class); + // all need converting to strings + evaluate("new org.springframework.expression.spel.testresources.Fruit(1,2,3).stringscount()", 3, Integer.class); + // needs string conversion + evaluate("new org.springframework.expression.spel.testresources.Fruit(1).stringscount()", 1, Integer.class); + // first and last need conversion + evaluate("new org.springframework.expression.spel.testresources.Fruit(1,'a',3.0d).stringscount()", 3, + Integer.class); + } + + @Test + public void testVarargsInvocation02() { + // Calling 'Fruit(int i, String... strings)' - returns int+length_of_strings + evaluate("new org.springframework.expression.spel.testresources.Fruit(5,'a','b','c').stringscount()", 8, + Integer.class); + evaluate("new org.springframework.expression.spel.testresources.Fruit(2,'a').stringscount()", 3, Integer.class); + evaluate("new org.springframework.expression.spel.testresources.Fruit(4).stringscount()", 4, Integer.class); + evaluate("new org.springframework.expression.spel.testresources.Fruit(8,2,3).stringscount()", 10, Integer.class); + evaluate("new org.springframework.expression.spel.testresources.Fruit(9).stringscount()", 9, Integer.class); + evaluate("new org.springframework.expression.spel.testresources.Fruit(2,'a',3.0d).stringscount()", 4, + Integer.class); + evaluate( + "new org.springframework.expression.spel.testresources.Fruit(8,stringArrayOfThreeItems).stringscount()", + 11, Integer.class); + } + + /* + * These tests are attempting to call constructors where we need to widen or convert + * the argument in order to satisfy a suitable constructor. + */ + @Test + public void testWidening01() { + // widening of int 3 to double 3 is OK + evaluate("new Double(3)", 3.0d, Double.class); + // widening of int 3 to long 3 is OK + evaluate("new Long(3)", 3L, Long.class); + } + + @Test + @Disabled + public void testArgumentConversion01() { + // Closest ctor will be new String(String) and converter supports Double>String + // TODO currently failing as with new ObjectToArray converter closest constructor + // matched becomes String(byte[]) which fails... + evaluate("new String(3.0d)", "3.0", String.class); + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/DefaultComparatorUnitTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/DefaultComparatorUnitTests.java new file mode 100644 index 0000000..edc88bd --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/DefaultComparatorUnitTests.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import java.math.BigDecimal; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.EvaluationException; +import org.springframework.expression.TypeComparator; +import org.springframework.expression.spel.support.StandardTypeComparator; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for type comparison + * + * @author Andy Clement + * @author Giovanni Dall'Oglio Risso + */ +public class DefaultComparatorUnitTests { + + @Test + public void testPrimitives() throws EvaluationException { + TypeComparator comparator = new StandardTypeComparator(); + // primitive int + assertThat(comparator.compare(1, 2) < 0).isTrue(); + assertThat(comparator.compare(1, 1) == 0).isTrue(); + assertThat(comparator.compare(2, 1) > 0).isTrue(); + + assertThat(comparator.compare(1.0d, 2) < 0).isTrue(); + assertThat(comparator.compare(1.0d, 1) == 0).isTrue(); + assertThat(comparator.compare(2.0d, 1) > 0).isTrue(); + + assertThat(comparator.compare(1.0f, 2) < 0).isTrue(); + assertThat(comparator.compare(1.0f, 1) == 0).isTrue(); + assertThat(comparator.compare(2.0f, 1) > 0).isTrue(); + + assertThat(comparator.compare(1L, 2) < 0).isTrue(); + assertThat(comparator.compare(1L, 1) == 0).isTrue(); + assertThat(comparator.compare(2L, 1) > 0).isTrue(); + + assertThat(comparator.compare(1, 2L) < 0).isTrue(); + assertThat(comparator.compare(1, 1L) == 0).isTrue(); + assertThat(comparator.compare(2, 1L) > 0).isTrue(); + + assertThat(comparator.compare(1L, 2L) < 0).isTrue(); + assertThat(comparator.compare(1L, 1L) == 0).isTrue(); + assertThat(comparator.compare(2L, 1L) > 0).isTrue(); + } + + @Test + public void testNonPrimitiveNumbers() throws EvaluationException { + TypeComparator comparator = new StandardTypeComparator(); + + BigDecimal bdOne = new BigDecimal("1"); + BigDecimal bdTwo = new BigDecimal("2"); + + assertThat(comparator.compare(bdOne, bdTwo) < 0).isTrue(); + assertThat(comparator.compare(bdOne, new BigDecimal("1")) == 0).isTrue(); + assertThat(comparator.compare(bdTwo, bdOne) > 0).isTrue(); + + assertThat(comparator.compare(1, bdTwo) < 0).isTrue(); + assertThat(comparator.compare(1, bdOne) == 0).isTrue(); + assertThat(comparator.compare(2, bdOne) > 0).isTrue(); + + assertThat(comparator.compare(1.0d, bdTwo) < 0).isTrue(); + assertThat(comparator.compare(1.0d, bdOne) == 0).isTrue(); + assertThat(comparator.compare(2.0d, bdOne) > 0).isTrue(); + + assertThat(comparator.compare(1.0f, bdTwo) < 0).isTrue(); + assertThat(comparator.compare(1.0f, bdOne) == 0).isTrue(); + assertThat(comparator.compare(2.0f, bdOne) > 0).isTrue(); + + assertThat(comparator.compare(1L, bdTwo) < 0).isTrue(); + assertThat(comparator.compare(1L, bdOne) == 0).isTrue(); + assertThat(comparator.compare(2L, bdOne) > 0).isTrue(); + + } + + @Test + public void testNulls() throws EvaluationException { + TypeComparator comparator = new StandardTypeComparator(); + assertThat(comparator.compare(null,"abc")<0).isTrue(); + assertThat(comparator.compare(null,null)==0).isTrue(); + assertThat(comparator.compare("abc",null)>0).isTrue(); + } + + @Test + public void testObjects() throws EvaluationException { + TypeComparator comparator = new StandardTypeComparator(); + assertThat(comparator.compare("a","a")==0).isTrue(); + assertThat(comparator.compare("a","b")<0).isTrue(); + assertThat(comparator.compare("b","a")>0).isTrue(); + } + + @Test + public void testCanCompare() throws EvaluationException { + TypeComparator comparator = new StandardTypeComparator(); + assertThat(comparator.canCompare(null,1)).isTrue(); + assertThat(comparator.canCompare(1,null)).isTrue(); + + assertThat(comparator.canCompare(2,1)).isTrue(); + assertThat(comparator.canCompare("abc","def")).isTrue(); + assertThat(comparator.canCompare("abc",3)).isTrue(); + assertThat(comparator.canCompare(String.class,3)).isFalse(); + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java new file mode 100644 index 0000000..98b9704 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java @@ -0,0 +1,1444 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.AccessException; +import org.springframework.expression.BeanResolver; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.MethodExecutor; +import org.springframework.expression.MethodFilter; +import org.springframework.expression.MethodResolver; +import org.springframework.expression.ParseException; +import org.springframework.expression.spel.standard.SpelExpression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.expression.spel.support.StandardTypeLocator; +import org.springframework.expression.spel.testresources.TestPerson; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.within; + +/** + * Tests the evaluation of real expressions in a real context. + * + * @author Andy Clement + * @author Mark Fisher + * @author Sam Brannen + * @author Phillip Webb + * @author Giovanni Dall'Oglio Risso + * @since 3.0 + */ +public class EvaluationTests extends AbstractExpressionTests { + + @Test + public void testCreateListsOnAttemptToIndexNull01() throws EvaluationException, ParseException { + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + Expression e = parser.parseExpression("list[0]"); + TestClass testClass = new TestClass(); + + Object o = e.getValue(new StandardEvaluationContext(testClass)); + assertThat(o).isEqualTo(""); + o = parser.parseExpression("list[3]").getValue(new StandardEvaluationContext(testClass)); + assertThat(o).isEqualTo(""); + assertThat(testClass.list.size()).isEqualTo(4); + + assertThatExceptionOfType(EvaluationException.class).isThrownBy(() -> + parser.parseExpression("list2[3]").getValue(new StandardEvaluationContext(testClass))); + + o = parser.parseExpression("foo[3]").getValue(new StandardEvaluationContext(testClass)); + assertThat(o).isEqualTo(""); + assertThat(testClass.getFoo().size()).isEqualTo(4); + } + + @Test + public void testCreateMapsOnAttemptToIndexNull01() { + TestClass testClass = new TestClass(); + StandardEvaluationContext ctx = new StandardEvaluationContext(testClass); + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + + Object o = parser.parseExpression("map['a']").getValue(ctx); + assertThat(o).isNull(); + o = parser.parseExpression("map").getValue(ctx); + assertThat(o).isNotNull(); + + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + parser.parseExpression("map2['a']").getValue(ctx)); + // map2 should be null, there is no setter + } + + // wibble2 should be null (cannot be initialized dynamically), there is no setter + @Test + public void testCreateObjectsOnAttemptToReferenceNull() { + TestClass testClass = new TestClass(); + StandardEvaluationContext ctx = new StandardEvaluationContext(testClass); + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + + Object o = parser.parseExpression("wibble.bar").getValue(ctx); + assertThat(o).isEqualTo("hello"); + o = parser.parseExpression("wibble").getValue(ctx); + assertThat(o).isNotNull(); + + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + parser.parseExpression("wibble2.bar").getValue(ctx)); + } + + @Test + public void testElvis01() { + evaluate("'Andy'?:'Dave'", "Andy", String.class); + evaluate("null?:'Dave'", "Dave", String.class); + } + + @Test + public void testSafeNavigation() { + evaluate("null?.null?.null", null, null); + } + + @Test + public void testRelOperatorGT01() { + evaluate("3 > 6", "false", Boolean.class); + } + + @Test + public void testRelOperatorLT01() { + evaluate("3 < 6", "true", Boolean.class); + } + + @Test + public void testRelOperatorLE01() { + evaluate("3 <= 6", "true", Boolean.class); + } + + @Test + public void testRelOperatorGE01() { + evaluate("3 >= 6", "false", Boolean.class); + } + + @Test + public void testRelOperatorGE02() { + evaluate("3 >= 3", "true", Boolean.class); + } + + @Test + public void testRelOperatorsInstanceof01() { + evaluate("'xyz' instanceof T(int)", "false", Boolean.class); + } + + @Test + public void testRelOperatorsInstanceof04() { + evaluate("null instanceof T(String)", "false", Boolean.class); + } + + @Test + public void testRelOperatorsInstanceof05() { + evaluate("null instanceof T(Integer)", "false", Boolean.class); + } + + @Test + public void testRelOperatorsInstanceof06() { + evaluateAndCheckError("'A' instanceof null", SpelMessage.INSTANCEOF_OPERATOR_NEEDS_CLASS_OPERAND, 15, "null"); + } + + @Test + public void testRelOperatorsMatches01() { + evaluate("'5.0067' matches '^-?\\d+(\\.\\d{2})?$'", "false", Boolean.class); + } + + @Test + public void testRelOperatorsMatches02() { + evaluate("'5.00' matches '^-?\\d+(\\.\\d{2})?$'", "true", Boolean.class); + } + + @Test + public void testRelOperatorsMatches03() { + evaluateAndCheckError("null matches '^.*$'", SpelMessage.INVALID_FIRST_OPERAND_FOR_MATCHES_OPERATOR, 0, null); + } + + @Test + public void testRelOperatorsMatches04() { + evaluateAndCheckError("'abc' matches null", SpelMessage.INVALID_SECOND_OPERAND_FOR_MATCHES_OPERATOR, 14, null); + } + + @Test + public void testRelOperatorsMatches05() { + evaluate("27 matches '^.*2.*$'", true, Boolean.class); // conversion int>string + } + + @Test // SPR-16731 + public void testMatchesWithPatternAccessThreshold() { + String pattern = "^(?=[a-z0-9-]{1,47})([a-z0-9]+[-]{0,1}){1,47}[a-z0-9]{1}$"; + String expression = "'abcde-fghijklmn-o42pasdfasdfasdf.qrstuvwxyz10x.xx.yyy.zasdfasfd' matches \'" + pattern + "\'"; + Expression expr = parser.parseExpression(expression); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy( + expr::getValue) + .withCauseInstanceOf(IllegalStateException.class) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.FLAWED_PATTERN)); + } + + // mixing operators + @Test + public void testMixingOperators01() { + evaluate("true and 5>3", "true", Boolean.class); + } + + // property access + @Test + public void testPropertyField01() { + evaluate("name", "Nikola Tesla", String.class, false); + // not writable because (1) name is private (2) there is no setter, only a getter + evaluateAndCheckError("madeup", SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE, 0, "madeup", + "org.springframework.expression.spel.testresources.Inventor"); + } + + @Test + public void testPropertyField02_SPR7100() { + evaluate("_name", "Nikola Tesla", String.class); + evaluate("_name_", "Nikola Tesla", String.class); + } + + @Test + public void testRogueTrailingDotCausesNPE_SPR6866() { + assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> + new SpelExpressionParser().parseExpression("placeOfBirth.foo.")) + .satisfies(ex -> { + assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.OOD); + assertThat(ex.getPosition()).isEqualTo(16); + }); + } + + // nested properties + @Test + public void testPropertiesNested01() { + evaluate("placeOfBirth.city", "SmilJan", String.class, true); + } + + @Test + public void testPropertiesNested02() { + evaluate("placeOfBirth.doubleIt(12)", "24", Integer.class); + } + + @Test + public void testPropertiesNested03() throws ParseException { + assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> + new SpelExpressionParser().parseRaw("placeOfBirth.23")) + .satisfies(ex -> { + assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.UNEXPECTED_DATA_AFTER_DOT); + assertThat(ex.getInserts()[0]).isEqualTo("23"); + }); + } + + // methods + @Test + public void testMethods01() { + evaluate("echo(12)", "12", String.class); + } + + @Test + public void testMethods02() { + evaluate("echo(name)", "Nikola Tesla", String.class); + } + + // constructors + @Test + public void testConstructorInvocation01() { + evaluate("new String('hello')", "hello", String.class); + } + + @Test + public void testConstructorInvocation05() { + evaluate("new java.lang.String('foobar')", "foobar", String.class); + } + + @Test + public void testConstructorInvocation06() { + // repeated evaluation to drive use of cached executor + SpelExpression e = (SpelExpression) parser.parseExpression("new String('wibble')"); + String newString = e.getValue(String.class); + assertThat(newString).isEqualTo("wibble"); + newString = e.getValue(String.class); + assertThat(newString).isEqualTo("wibble"); + + // not writable + assertThat(e.isWritable(new StandardEvaluationContext())).isFalse(); + + // ast + assertThat(e.toStringAST()).isEqualTo("new String('wibble')"); + } + + // unary expressions + @Test + public void testUnaryMinus01() { + evaluate("-5", "-5", Integer.class); + } + + @Test + public void testUnaryPlus01() { + evaluate("+5", "5", Integer.class); + } + + @Test + public void testUnaryNot01() { + evaluate("!true", "false", Boolean.class); + } + + @Test + public void testUnaryNot02() { + evaluate("!false", "true", Boolean.class); + } + + @Test + public void testUnaryNotWithNullValue() { + assertThatExceptionOfType(EvaluationException.class).isThrownBy( + parser.parseExpression("!null")::getValue); + } + + @Test + public void testAndWithNullValueOnLeft() { + assertThatExceptionOfType(EvaluationException.class).isThrownBy( + parser.parseExpression("null and true")::getValue); + } + + @Test + public void testAndWithNullValueOnRight() { + assertThatExceptionOfType(EvaluationException.class).isThrownBy( + parser.parseExpression("true and null")::getValue); + } + + @Test + public void testOrWithNullValueOnLeft() { + assertThatExceptionOfType(EvaluationException.class).isThrownBy( + parser.parseExpression("null or false")::getValue); + } + + @Test + public void testOrWithNullValueOnRight() { + assertThatExceptionOfType(EvaluationException.class).isThrownBy( + parser.parseExpression("false or null")::getValue); + } + + // assignment + @Test + public void testAssignmentToVariables01() { + evaluate("#var1='value1'", "value1", String.class); + } + + @Test + public void testTernaryOperator01() { + evaluate("2>4?1:2", 2, Integer.class); + } + + @Test + public void testTernaryOperator02() { + evaluate("'abc'=='abc'?1:2", 1, Integer.class); + } + + @Test + public void testTernaryOperator03() { + // cannot convert String to boolean + evaluateAndCheckError("'hello'?1:2", SpelMessage.TYPE_CONVERSION_ERROR); + } + + @Test + public void testTernaryOperator04() { + Expression e = parser.parseExpression("1>2?3:4"); + assertThat(e.isWritable(context)).isFalse(); + } + + @Test + public void testTernaryOperator05() { + evaluate("1>2?#var=4:#var=5", 5, Integer.class); + evaluate("3?:#var=5", 3, Integer.class); + evaluate("null?:#var=5", 5, Integer.class); + evaluate("2>4?(3>2?true:false):(5<3?true:false)", false, Boolean.class); + } + + @Test + public void testTernaryOperatorWithNullValue() { + assertThatExceptionOfType(EvaluationException.class).isThrownBy( + parser.parseExpression("null ? 0 : 1")::getValue); + } + + @Test + public void methodCallWithRootReferenceThroughParameter() { + evaluate("placeOfBirth.doubleIt(inventions.length)", 18, Integer.class); + } + + @Test + public void ctorCallWithRootReferenceThroughParameter() { + evaluate("new org.springframework.expression.spel.testresources.PlaceOfBirth(inventions[0].toString()).city", + "Telephone repeater", String.class); + } + + @Test + public void fnCallWithRootReferenceThroughParameter() { + evaluate("#reverseInt(inventions.length, inventions.length, inventions.length)", "int[3]{9,9,9}", int[].class); + } + + @Test + public void methodCallWithRootReferenceThroughParameterThatIsAFunctionCall() { + evaluate("placeOfBirth.doubleIt(#reverseInt(inventions.length,2,3)[2])", 18, Integer.class); + } + + @Test + public void testIndexer03() { + evaluate("'christian'[8]", "n", String.class); + } + + @Test + public void testIndexerError() { + evaluateAndCheckError("new org.springframework.expression.spel.testresources.Inventor().inventions[1]", + SpelMessage.CANNOT_INDEX_INTO_NULL_VALUE); + } + + @Test + public void testStaticRef02() { + evaluate("T(java.awt.Color).green.getRGB()!=0", "true", Boolean.class); + } + + // variables and functions + @Test + public void testVariableAccess01() { + evaluate("#answer", "42", Integer.class, true); + } + + @Test + public void testFunctionAccess01() { + evaluate("#reverseInt(1,2,3)", "int[3]{3,2,1}", int[].class); + } + + @Test + public void testFunctionAccess02() { + evaluate("#reverseString('hello')", "olleh", String.class); + } + + // type references + @Test + public void testTypeReferences01() { + evaluate("T(java.lang.String)", "class java.lang.String", Class.class); + } + + @Test + public void testTypeReferencesAndQualifiedIdentifierCaching() { + SpelExpression e = (SpelExpression) parser.parseExpression("T(java.lang.String)"); + assertThat(e.isWritable(new StandardEvaluationContext())).isFalse(); + assertThat(e.toStringAST()).isEqualTo("T(java.lang.String)"); + assertThat(e.getValue(Class.class)).isEqualTo(String.class); + // use cached QualifiedIdentifier: + assertThat(e.toStringAST()).isEqualTo("T(java.lang.String)"); + assertThat(e.getValue(Class.class)).isEqualTo(String.class); + } + + @Test + public void operatorVariants() { + SpelExpression e = (SpelExpression)parser.parseExpression("#a < #b"); + EvaluationContext ctx = new StandardEvaluationContext(); + ctx.setVariable("a", (short) 3); + ctx.setVariable("b", (short) 6); + assertThat(e.getValue(ctx, Boolean.class)).isTrue(); + ctx.setVariable("b", (byte) 6); + assertThat(e.getValue(ctx, Boolean.class)).isTrue(); + ctx.setVariable("a", (byte) 9); + ctx.setVariable("b", (byte) 6); + assertThat(e.getValue(ctx, Boolean.class)).isFalse(); + ctx.setVariable("a", 10L); + ctx.setVariable("b", (short) 30); + assertThat(e.getValue(ctx, Boolean.class)).isTrue(); + ctx.setVariable("a", (byte) 3); + ctx.setVariable("b", (short) 30); + assertThat(e.getValue(ctx, Boolean.class)).isTrue(); + ctx.setVariable("a", (byte) 3); + ctx.setVariable("b", 30L); + assertThat(e.getValue(ctx, Boolean.class)).isTrue(); + ctx.setVariable("a", (byte) 3); + ctx.setVariable("b", 30f); + assertThat(e.getValue(ctx, Boolean.class)).isTrue(); + ctx.setVariable("a", new BigInteger("10")); + ctx.setVariable("b", new BigInteger("20")); + assertThat(e.getValue(ctx, Boolean.class)).isTrue(); + } + + @Test + public void testTypeReferencesPrimitive() { + evaluate("T(int)", "int", Class.class); + evaluate("T(byte)", "byte", Class.class); + evaluate("T(char)", "char", Class.class); + evaluate("T(boolean)", "boolean", Class.class); + evaluate("T(long)", "long", Class.class); + evaluate("T(short)", "short", Class.class); + evaluate("T(double)", "double", Class.class); + evaluate("T(float)", "float", Class.class); + } + + @Test + public void testTypeReferences02() { + evaluate("T(String)", "class java.lang.String", Class.class); + } + + @Test + public void testStringType() { + evaluateAndAskForReturnType("getPlaceOfBirth().getCity()", "SmilJan", String.class); + } + + @Test + public void testNumbers01() { + evaluateAndAskForReturnType("3*4+5", 17, Integer.class); + evaluateAndAskForReturnType("3*4+5", 17L, Long.class); + evaluateAndAskForReturnType("65", 'A', Character.class); + evaluateAndAskForReturnType("3*4+5", (short) 17, Short.class); + evaluateAndAskForReturnType("3*4+5", "17", String.class); + } + + @Test + public void testAdvancedNumerics() { + int twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Integer.class); + assertThat(twentyFour).isEqualTo(24); + double one = parser.parseExpression("8.0 / 5e0 % 2").getValue(Double.class); + assertThat((float) one).isCloseTo((float) 1.6d, within((float) 0d)); + int o = parser.parseExpression("8.0 / 5e0 % 2").getValue(Integer.class); + assertThat(o).isEqualTo(1); + int sixteen = parser.parseExpression("-2 ^ 4").getValue(Integer.class); + assertThat(sixteen).isEqualTo(16); + int minusFortyFive = parser.parseExpression("1+2-3*8^2/2/2").getValue(Integer.class); + assertThat(minusFortyFive).isEqualTo(-45); + } + + @Test + public void testComparison() { + EvaluationContext context = TestScenarioCreator.getTestEvaluationContext(); + boolean trueValue = parser.parseExpression("T(java.util.Date) == Birthdate.Class").getValue( + context, Boolean.class); + assertThat(trueValue).isTrue(); + } + + @Test + public void testResolvingList() { + StandardEvaluationContext context = TestScenarioCreator.getTestEvaluationContext(); + assertThatExceptionOfType(EvaluationException.class).isThrownBy(() -> + parser.parseExpression("T(List)!=null").getValue(context, Boolean.class)); + ((StandardTypeLocator) context.getTypeLocator()).registerImport("java.util"); + assertThat(parser.parseExpression("T(List)!=null").getValue(context, Boolean.class)).isTrue(); + } + + @Test + public void testResolvingString() { + Class stringClass = parser.parseExpression("T(String)").getValue(Class.class); + assertThat(stringClass).isEqualTo(String.class); + } + + /** + * SPR-6984: attempting to index a collection on write using an index that + * doesn't currently exist in the collection (address.crossStreets[0] below) + */ + @Test + public void initializingCollectionElementsOnWrite() { + TestPerson person = new TestPerson(); + EvaluationContext context = new StandardEvaluationContext(person); + SpelParserConfiguration config = new SpelParserConfiguration(true, true); + ExpressionParser parser = new SpelExpressionParser(config); + Expression e = parser.parseExpression("name"); + e.setValue(context, "Oleg"); + assertThat(person.getName()).isEqualTo("Oleg"); + + e = parser.parseExpression("address.street"); + e.setValue(context, "123 High St"); + assertThat(person.getAddress().getStreet()).isEqualTo("123 High St"); + + e = parser.parseExpression("address.crossStreets[0]"); + e.setValue(context, "Blah"); + assertThat(person.getAddress().getCrossStreets().get(0)).isEqualTo("Blah"); + + e = parser.parseExpression("address.crossStreets[3]"); + e.setValue(context, "Wibble"); + assertThat(person.getAddress().getCrossStreets().get(0)).isEqualTo("Blah"); + assertThat(person.getAddress().getCrossStreets().get(3)).isEqualTo("Wibble"); + } + + /** + * Verifies behavior requested in SPR-9613. + */ + @Test + public void caseInsensitiveNullLiterals() { + ExpressionParser parser = new SpelExpressionParser(); + + Expression e = parser.parseExpression("null"); + assertThat(e.getValue()).isNull(); + + e = parser.parseExpression("NULL"); + assertThat(e.getValue()).isNull(); + + e = parser.parseExpression("NuLl"); + assertThat(e.getValue()).isNull(); + } + + /** + * Verifies behavior requested in SPR-9621. + */ + @Test + public void customMethodFilter() { + StandardEvaluationContext context = new StandardEvaluationContext(); + + // Register a custom MethodResolver... + List customResolvers = new ArrayList<>(); + customResolvers.add(new CustomMethodResolver()); + context.setMethodResolvers(customResolvers); + + // or simply... + // context.setMethodResolvers(new ArrayList()); + + // Register a custom MethodFilter... + MethodFilter filter = new CustomMethodFilter(); + assertThatIllegalStateException().isThrownBy(() -> + context.registerMethodFilter(String.class, filter)) + .withMessage("Method filter cannot be set as the reflective method resolver is not in use"); + } + + /** + * This test is checking that with the changes for 9751 that the refactoring in Indexer is + * coping correctly for references beyond collection boundaries. + */ + @Test + public void collectionGrowingViaIndexer() { + Spr9751 instance = new Spr9751(); + + // Add a new element to the list + StandardEvaluationContext ctx = new StandardEvaluationContext(instance); + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + Expression e = parser.parseExpression("listOfStrings[++index3]='def'"); + e.getValue(ctx); + assertThat(instance.listOfStrings.size()).isEqualTo(2); + assertThat(instance.listOfStrings.get(1)).isEqualTo("def"); + + // Check reference beyond end of collection + ctx = new StandardEvaluationContext(instance); + parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + e = parser.parseExpression("listOfStrings[0]"); + String value = e.getValue(ctx, String.class); + assertThat(value).isEqualTo("abc"); + e = parser.parseExpression("listOfStrings[1]"); + value = e.getValue(ctx, String.class); + assertThat(value).isEqualTo("def"); + e = parser.parseExpression("listOfStrings[2]"); + value = e.getValue(ctx, String.class); + assertThat(value).isEqualTo(""); + + // Now turn off growing and reference off the end + StandardEvaluationContext failCtx = new StandardEvaluationContext(instance); + parser = new SpelExpressionParser(new SpelParserConfiguration(false, false)); + Expression failExp = parser.parseExpression("listOfStrings[3]"); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + failExp.getValue(failCtx, String.class)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.COLLECTION_INDEX_OUT_OF_BOUNDS)); + } + + @Test + public void limitCollectionGrowing() { + TestClass instance = new TestClass(); + StandardEvaluationContext ctx = new StandardEvaluationContext(instance); + SpelExpressionParser parser = new SpelExpressionParser( new SpelParserConfiguration(true, true, 3)); + Expression e = parser.parseExpression("foo[2]"); + e.setValue(ctx, "2"); + assertThat(instance.getFoo().size()).isEqualTo(3); + e = parser.parseExpression("foo[3]"); + try { + e.setValue(ctx, "3"); + } + catch (SpelEvaluationException see) { + assertThat(see.getMessageCode()).isEqualTo(SpelMessage.UNABLE_TO_GROW_COLLECTION); + assertThat(instance.getFoo().size()).isEqualTo(3); + } + } + + // For now I am making #this not assignable + @Test + public void increment01root() { + Integer i = 42; + StandardEvaluationContext ctx = new StandardEvaluationContext(i); + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + Expression e = parser.parseExpression("#this++"); + assertThat(i.intValue()).isEqualTo(42); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + e.getValue(ctx, Integer.class)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.NOT_ASSIGNABLE)); + } + + @Test + public void increment02postfix() { + Spr9751 helper = new Spr9751(); + StandardEvaluationContext ctx = new StandardEvaluationContext(helper); + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + Expression e; + + // BigDecimal + e = parser.parseExpression("bd++"); + assertThat(new BigDecimal("2").equals(helper.bd)).isTrue(); + BigDecimal return_bd = e.getValue(ctx, BigDecimal.class); + assertThat(new BigDecimal("2").equals(return_bd)).isTrue(); + assertThat(new BigDecimal("3").equals(helper.bd)).isTrue(); + + // double + e = parser.parseExpression("ddd++"); + assertThat((float) helper.ddd).isCloseTo((float) 2.0d, within((float) 0d)); + double return_ddd = e.getValue(ctx, Double.TYPE); + assertThat((float) return_ddd).isCloseTo((float) 2.0d, within((float) 0d)); + assertThat((float) helper.ddd).isCloseTo((float) 3.0d, within((float) 0d)); + + // float + e = parser.parseExpression("fff++"); + assertThat(helper.fff).isCloseTo(3.0f, within((float) 0d)); + float return_fff = e.getValue(ctx, Float.TYPE); + assertThat(return_fff).isCloseTo(3.0f, within((float) 0d)); + assertThat(helper.fff).isCloseTo(4.0f, within((float) 0d)); + + // long + e = parser.parseExpression("lll++"); + assertThat(helper.lll).isEqualTo(66666L); + long return_lll = e.getValue(ctx, Long.TYPE); + assertThat(return_lll).isEqualTo(66666L); + assertThat(helper.lll).isEqualTo(66667L); + + // int + e = parser.parseExpression("iii++"); + assertThat(helper.iii).isEqualTo(42); + int return_iii = e.getValue(ctx, Integer.TYPE); + assertThat(return_iii).isEqualTo(42); + assertThat(helper.iii).isEqualTo(43); + return_iii = e.getValue(ctx, Integer.TYPE); + assertThat(return_iii).isEqualTo(43); + assertThat(helper.iii).isEqualTo(44); + + // short + e = parser.parseExpression("sss++"); + assertThat(helper.sss).isEqualTo((short) 15); + short return_sss = e.getValue(ctx, Short.TYPE); + assertThat(return_sss).isEqualTo((short) 15); + assertThat(helper.sss).isEqualTo((short) 16); + } + + @Test + public void increment02prefix() { + Spr9751 helper = new Spr9751(); + StandardEvaluationContext ctx = new StandardEvaluationContext(helper); + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + Expression e; + + + // BigDecimal + e = parser.parseExpression("++bd"); + assertThat(new BigDecimal("2").equals(helper.bd)).isTrue(); + BigDecimal return_bd = e.getValue(ctx, BigDecimal.class); + assertThat(new BigDecimal("3").equals(return_bd)).isTrue(); + assertThat(new BigDecimal("3").equals(helper.bd)).isTrue(); + + // double + e = parser.parseExpression("++ddd"); + assertThat((float) helper.ddd).isCloseTo((float) 2.0d, within((float) 0d)); + double return_ddd = e.getValue(ctx, Double.TYPE); + assertThat((float) return_ddd).isCloseTo((float) 3.0d, within((float) 0d)); + assertThat((float) helper.ddd).isCloseTo((float) 3.0d, within((float) 0d)); + + // float + e = parser.parseExpression("++fff"); + assertThat(helper.fff).isCloseTo(3.0f, within((float) 0d)); + float return_fff = e.getValue(ctx, Float.TYPE); + assertThat(return_fff).isCloseTo(4.0f, within((float) 0d)); + assertThat(helper.fff).isCloseTo(4.0f, within((float) 0d)); + + // long + e = parser.parseExpression("++lll"); + assertThat(helper.lll).isEqualTo(66666L); + long return_lll = e.getValue(ctx, Long.TYPE); + assertThat(return_lll).isEqualTo(66667L); + assertThat(helper.lll).isEqualTo(66667L); + + // int + e = parser.parseExpression("++iii"); + assertThat(helper.iii).isEqualTo(42); + int return_iii = e.getValue(ctx, Integer.TYPE); + assertThat(return_iii).isEqualTo(43); + assertThat(helper.iii).isEqualTo(43); + return_iii = e.getValue(ctx, Integer.TYPE); + assertThat(return_iii).isEqualTo(44); + assertThat(helper.iii).isEqualTo(44); + + // short + e = parser.parseExpression("++sss"); + assertThat(helper.sss).isEqualTo((short) 15); + int return_sss = (Integer) e.getValue(ctx); + assertThat(return_sss).isEqualTo((short) 16); + assertThat(helper.sss).isEqualTo((short) 16); + } + + @Test + public void increment03() { + Spr9751 helper = new Spr9751(); + StandardEvaluationContext ctx = new StandardEvaluationContext(helper); + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + + Expression e1 = parser.parseExpression("m()++"); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + e1.getValue(ctx, Double.TYPE)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.OPERAND_NOT_INCREMENTABLE)); + + Expression e2 = parser.parseExpression("++m()"); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + e2.getValue(ctx, Double.TYPE)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.OPERAND_NOT_INCREMENTABLE)); + } + + @Test + public void increment04() { + Integer i = 42; + StandardEvaluationContext ctx = new StandardEvaluationContext(i); + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + Expression e1 = parser.parseExpression("++1"); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + e1.getValue(ctx, Double.TYPE)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.NOT_ASSIGNABLE)); + Expression e2 = parser.parseExpression("1++"); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + e2.getValue(ctx, Double.TYPE)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.NOT_ASSIGNABLE)); + } + + @Test + public void decrement01root() { + Integer i = 42; + StandardEvaluationContext ctx = new StandardEvaluationContext(i); + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + Expression e = parser.parseExpression("#this--"); + assertThat(i.intValue()).isEqualTo(42); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + e.getValue(ctx, Integer.class)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.NOT_ASSIGNABLE)); + } + + @Test + public void decrement02postfix() { + Spr9751 helper = new Spr9751(); + StandardEvaluationContext ctx = new StandardEvaluationContext(helper); + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + Expression e; + + // BigDecimal + e = parser.parseExpression("bd--"); + assertThat(new BigDecimal("2").equals(helper.bd)).isTrue(); + BigDecimal return_bd = e.getValue(ctx,BigDecimal.class); + assertThat(new BigDecimal("2").equals(return_bd)).isTrue(); + assertThat(new BigDecimal("1").equals(helper.bd)).isTrue(); + + // double + e = parser.parseExpression("ddd--"); + assertThat((float) helper.ddd).isCloseTo((float) 2.0d, within((float) 0d)); + double return_ddd = e.getValue(ctx, Double.TYPE); + assertThat((float) return_ddd).isCloseTo((float) 2.0d, within((float) 0d)); + assertThat((float) helper.ddd).isCloseTo((float) 1.0d, within((float) 0d)); + + // float + e = parser.parseExpression("fff--"); + assertThat(helper.fff).isCloseTo(3.0f, within((float) 0d)); + float return_fff = e.getValue(ctx, Float.TYPE); + assertThat(return_fff).isCloseTo(3.0f, within((float) 0d)); + assertThat(helper.fff).isCloseTo(2.0f, within((float) 0d)); + + // long + e = parser.parseExpression("lll--"); + assertThat(helper.lll).isEqualTo(66666L); + long return_lll = e.getValue(ctx, Long.TYPE); + assertThat(return_lll).isEqualTo(66666L); + assertThat(helper.lll).isEqualTo(66665L); + + // int + e = parser.parseExpression("iii--"); + assertThat(helper.iii).isEqualTo(42); + int return_iii = e.getValue(ctx, Integer.TYPE); + assertThat(return_iii).isEqualTo(42); + assertThat(helper.iii).isEqualTo(41); + return_iii = e.getValue(ctx, Integer.TYPE); + assertThat(return_iii).isEqualTo(41); + assertThat(helper.iii).isEqualTo(40); + + // short + e = parser.parseExpression("sss--"); + assertThat(helper.sss).isEqualTo((short) 15); + short return_sss = e.getValue(ctx, Short.TYPE); + assertThat(return_sss).isEqualTo((short) 15); + assertThat(helper.sss).isEqualTo((short) 14); + } + + @Test + public void decrement02prefix() { + Spr9751 helper = new Spr9751(); + StandardEvaluationContext ctx = new StandardEvaluationContext(helper); + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + Expression e; + + // BigDecimal + e = parser.parseExpression("--bd"); + assertThat(new BigDecimal("2").equals(helper.bd)).isTrue(); + BigDecimal return_bd = e.getValue(ctx,BigDecimal.class); + assertThat(new BigDecimal("1").equals(return_bd)).isTrue(); + assertThat(new BigDecimal("1").equals(helper.bd)).isTrue(); + + // double + e = parser.parseExpression("--ddd"); + assertThat((float) helper.ddd).isCloseTo((float) 2.0d, within((float) 0d)); + double return_ddd = e.getValue(ctx, Double.TYPE); + assertThat((float) return_ddd).isCloseTo((float) 1.0d, within((float) 0d)); + assertThat((float) helper.ddd).isCloseTo((float) 1.0d, within((float) 0d)); + + // float + e = parser.parseExpression("--fff"); + assertThat(helper.fff).isCloseTo(3.0f, within((float) 0d)); + float return_fff = e.getValue(ctx, Float.TYPE); + assertThat(return_fff).isCloseTo(2.0f, within((float) 0d)); + assertThat(helper.fff).isCloseTo(2.0f, within((float) 0d)); + + // long + e = parser.parseExpression("--lll"); + assertThat(helper.lll).isEqualTo(66666L); + long return_lll = e.getValue(ctx, Long.TYPE); + assertThat(return_lll).isEqualTo(66665L); + assertThat(helper.lll).isEqualTo(66665L); + + // int + e = parser.parseExpression("--iii"); + assertThat(helper.iii).isEqualTo(42); + int return_iii = e.getValue(ctx, Integer.TYPE); + assertThat(return_iii).isEqualTo(41); + assertThat(helper.iii).isEqualTo(41); + return_iii = e.getValue(ctx, Integer.TYPE); + assertThat(return_iii).isEqualTo(40); + assertThat(helper.iii).isEqualTo(40); + + // short + e = parser.parseExpression("--sss"); + assertThat(helper.sss).isEqualTo((short) 15); + int return_sss = (Integer)e.getValue(ctx); + assertThat(return_sss).isEqualTo(14); + assertThat(helper.sss).isEqualTo((short) 14); + } + + @Test + public void decrement03() { + Spr9751 helper = new Spr9751(); + StandardEvaluationContext ctx = new StandardEvaluationContext(helper); + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + + Expression e1 = parser.parseExpression("m()--"); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + e1.getValue(ctx, Double.TYPE)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.OPERAND_NOT_DECREMENTABLE)); + + Expression e2 = parser.parseExpression("--m()"); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + e2.getValue(ctx, Double.TYPE)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.OPERAND_NOT_DECREMENTABLE)); + } + + + @Test + public void decrement04() { + Integer i = 42; + StandardEvaluationContext ctx = new StandardEvaluationContext(i); + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + Expression e1 = parser.parseExpression("--1"); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + e1.getValue(ctx, Integer.class)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.NOT_ASSIGNABLE)); + + Expression e2 = parser.parseExpression("1--"); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + e2.getValue(ctx, Integer.class)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.NOT_ASSIGNABLE)); + } + + @Test + public void incdecTogether() { + Spr9751 helper = new Spr9751(); + StandardEvaluationContext ctx = new StandardEvaluationContext(helper); + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + Expression e; + + // index1 is 2 at the start - the 'intArray[#root.index1++]' should not be evaluated twice! + // intArray[2] is 3 + e = parser.parseExpression("intArray[#root.index1++]++"); + e.getValue(ctx, Integer.class); + assertThat(helper.index1).isEqualTo(3); + assertThat(helper.intArray[2]).isEqualTo(4); + + // index1 is 3 intArray[3] is 4 + e = parser.parseExpression("intArray[#root.index1++]--"); + assertThat(e.getValue(ctx, Integer.class).intValue()).isEqualTo(4); + assertThat(helper.index1).isEqualTo(4); + assertThat(helper.intArray[3]).isEqualTo(3); + + // index1 is 4, intArray[3] is 3 + e = parser.parseExpression("intArray[--#root.index1]++"); + assertThat(e.getValue(ctx, Integer.class).intValue()).isEqualTo(3); + assertThat(helper.index1).isEqualTo(3); + assertThat(helper.intArray[3]).isEqualTo(4); + } + + + // Verify how all the nodes behave with assignment (++, --, =) + @Test + public void incrementAllNodeTypes() throws SecurityException, NoSuchMethodException { + Spr9751 helper = new Spr9751(); + StandardEvaluationContext ctx = new StandardEvaluationContext(helper); + ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + Expression e; + + // BooleanLiteral + expectFailNotAssignable(parser, ctx, "true++"); + expectFailNotAssignable(parser, ctx, "--false"); + expectFailSetValueNotSupported(parser, ctx, "true=false"); + + // IntLiteral + expectFailNotAssignable(parser, ctx, "12++"); + expectFailNotAssignable(parser, ctx, "--1222"); + expectFailSetValueNotSupported(parser, ctx, "12=16"); + + // LongLiteral + expectFailNotAssignable(parser, ctx, "1.0d++"); + expectFailNotAssignable(parser, ctx, "--3.4d"); + expectFailSetValueNotSupported(parser, ctx, "1.0d=3.2d"); + + // NullLiteral + expectFailNotAssignable(parser, ctx, "null++"); + expectFailNotAssignable(parser, ctx, "--null"); + expectFailSetValueNotSupported(parser, ctx, "null=null"); + expectFailSetValueNotSupported(parser, ctx, "null=123"); + + // OpAnd + expectFailNotAssignable(parser, ctx, "(true && false)++"); + expectFailNotAssignable(parser, ctx, "--(false AND true)"); + expectFailSetValueNotSupported(parser, ctx, "(true && false)=(false && true)"); + + // OpDivide + expectFailNotAssignable(parser, ctx, "(3/4)++"); + expectFailNotAssignable(parser, ctx, "--(2/5)"); + expectFailSetValueNotSupported(parser, ctx, "(1/2)=(3/4)"); + + // OpEq + expectFailNotAssignable(parser, ctx, "(3==4)++"); + expectFailNotAssignable(parser, ctx, "--(2==5)"); + expectFailSetValueNotSupported(parser, ctx, "(1==2)=(3==4)"); + + // OpGE + expectFailNotAssignable(parser, ctx, "(3>=4)++"); + expectFailNotAssignable(parser, ctx, "--(2>=5)"); + expectFailSetValueNotSupported(parser, ctx, "(1>=2)=(3>=4)"); + + // OpGT + expectFailNotAssignable(parser, ctx, "(3>4)++"); + expectFailNotAssignable(parser, ctx, "--(2>5)"); + expectFailSetValueNotSupported(parser, ctx, "(1>2)=(3>4)"); + + // OpLE + expectFailNotAssignable(parser, ctx, "(3<=4)++"); + expectFailNotAssignable(parser, ctx, "--(2<=5)"); + expectFailSetValueNotSupported(parser, ctx, "(1<=2)=(3<=4)"); + + // OpLT + expectFailNotAssignable(parser, ctx, "(3<4)++"); + expectFailNotAssignable(parser, ctx, "--(2<5)"); + expectFailSetValueNotSupported(parser, ctx, "(1<2)=(3<4)"); + + // OpMinus + expectFailNotAssignable(parser, ctx, "(3-4)++"); + expectFailNotAssignable(parser, ctx, "--(2-5)"); + expectFailSetValueNotSupported(parser, ctx, "(1-2)=(3-4)"); + + // OpModulus + expectFailNotAssignable(parser, ctx, "(3%4)++"); + expectFailNotAssignable(parser, ctx, "--(2%5)"); + expectFailSetValueNotSupported(parser, ctx, "(1%2)=(3%4)"); + + // OpMultiply + expectFailNotAssignable(parser, ctx, "(3*4)++"); + expectFailNotAssignable(parser, ctx, "--(2*5)"); + expectFailSetValueNotSupported(parser, ctx, "(1*2)=(3*4)"); + + // OpNE + expectFailNotAssignable(parser, ctx, "(3!=4)++"); + expectFailNotAssignable(parser, ctx, "--(2!=5)"); + expectFailSetValueNotSupported(parser, ctx, "(1!=2)=(3!=4)"); + + // OpOr + expectFailNotAssignable(parser, ctx, "(true || false)++"); + expectFailNotAssignable(parser, ctx, "--(false OR true)"); + expectFailSetValueNotSupported(parser, ctx, "(true || false)=(false OR true)"); + + // OpPlus + expectFailNotAssignable(parser, ctx, "(3+4)++"); + expectFailNotAssignable(parser, ctx, "--(2+5)"); + expectFailSetValueNotSupported(parser, ctx, "(1+2)=(3+4)"); + + // RealLiteral + expectFailNotAssignable(parser, ctx, "1.0d++"); + expectFailNotAssignable(parser, ctx, "--2.0d"); + expectFailSetValueNotSupported(parser, ctx, "(1.0d)=(3.0d)"); + expectFailNotAssignable(parser, ctx, "1.0f++"); + expectFailNotAssignable(parser, ctx, "--2.0f"); + expectFailSetValueNotSupported(parser, ctx, "(1.0f)=(3.0f)"); + + // StringLiteral + expectFailNotAssignable(parser, ctx, "'abc'++"); + expectFailNotAssignable(parser, ctx, "--'def'"); + expectFailSetValueNotSupported(parser, ctx, "'abc'='def'"); + + // Ternary + expectFailNotAssignable(parser, ctx, "(true?true:false)++"); + expectFailNotAssignable(parser, ctx, "--(true?true:false)"); + expectFailSetValueNotSupported(parser, ctx, "(true?true:false)=(true?true:false)"); + + // TypeReference + expectFailNotAssignable(parser, ctx, "T(String)++"); + expectFailNotAssignable(parser, ctx, "--T(Integer)"); + expectFailSetValueNotSupported(parser, ctx, "T(String)=T(Integer)"); + + // OperatorBetween + expectFailNotAssignable(parser, ctx, "(3 between {1,5})++"); + expectFailNotAssignable(parser, ctx, "--(3 between {1,5})"); + expectFailSetValueNotSupported(parser, ctx, "(3 between {1,5})=(3 between {1,5})"); + + // OperatorInstanceOf + expectFailNotAssignable(parser, ctx, "(type instanceof T(String))++"); + expectFailNotAssignable(parser, ctx, "--(type instanceof T(String))"); + expectFailSetValueNotSupported(parser, ctx, "(type instanceof T(String))=(type instanceof T(String))"); + + // Elvis + expectFailNotAssignable(parser, ctx, "(true?:false)++"); + expectFailNotAssignable(parser, ctx, "--(true?:false)"); + expectFailSetValueNotSupported(parser, ctx, "(true?:false)=(true?:false)"); + + // OpInc + expectFailNotAssignable(parser, ctx, "(iii++)++"); + expectFailNotAssignable(parser, ctx, "--(++iii)"); + expectFailSetValueNotSupported(parser, ctx, "(iii++)=(++iii)"); + + // OpDec + expectFailNotAssignable(parser, ctx, "(iii--)++"); + expectFailNotAssignable(parser, ctx, "--(--iii)"); + expectFailSetValueNotSupported(parser, ctx, "(iii--)=(--iii)"); + + // OperatorNot + expectFailNotAssignable(parser, ctx, "(!true)++"); + expectFailNotAssignable(parser, ctx, "--(!false)"); + expectFailSetValueNotSupported(parser, ctx, "(!true)=(!false)"); + + // OperatorPower + expectFailNotAssignable(parser, ctx, "(iii^2)++"); + expectFailNotAssignable(parser, ctx, "--(iii^2)"); + expectFailSetValueNotSupported(parser, ctx, "(iii^2)=(iii^3)"); + + // Assign + // iii=42 + e = parser.parseExpression("iii=iii++"); + assertThat(helper.iii).isEqualTo(42); + int return_iii = e.getValue(ctx, Integer.TYPE); + assertThat(helper.iii).isEqualTo(42); + assertThat(return_iii).isEqualTo(42); + + // Identifier + e = parser.parseExpression("iii++"); + assertThat(helper.iii).isEqualTo(42); + return_iii = e.getValue(ctx, Integer.TYPE); + assertThat(return_iii).isEqualTo(42); + assertThat(helper.iii).isEqualTo(43); + + e = parser.parseExpression("--iii"); + assertThat(helper.iii).isEqualTo(43); + return_iii = e.getValue(ctx, Integer.TYPE); + assertThat(return_iii).isEqualTo(42); + assertThat(helper.iii).isEqualTo(42); + + e = parser.parseExpression("iii=99"); + assertThat(helper.iii).isEqualTo(42); + return_iii = e.getValue(ctx, Integer.TYPE); + assertThat(return_iii).isEqualTo(99); + assertThat(helper.iii).isEqualTo(99); + + // CompoundExpression + // foo.iii == 99 + e = parser.parseExpression("foo.iii++"); + assertThat(helper.foo.iii).isEqualTo(99); + int return_foo_iii = e.getValue(ctx, Integer.TYPE); + assertThat(return_foo_iii).isEqualTo(99); + assertThat(helper.foo.iii).isEqualTo(100); + + e = parser.parseExpression("--foo.iii"); + assertThat(helper.foo.iii).isEqualTo(100); + return_foo_iii = e.getValue(ctx, Integer.TYPE); + assertThat(return_foo_iii).isEqualTo(99); + assertThat(helper.foo.iii).isEqualTo(99); + + e = parser.parseExpression("foo.iii=999"); + assertThat(helper.foo.iii).isEqualTo(99); + return_foo_iii = e.getValue(ctx, Integer.TYPE); + assertThat(return_foo_iii).isEqualTo(999); + assertThat(helper.foo.iii).isEqualTo(999); + + // ConstructorReference + expectFailNotAssignable(parser, ctx, "(new String('abc'))++"); + expectFailNotAssignable(parser, ctx, "--(new String('abc'))"); + expectFailSetValueNotSupported(parser, ctx, "(new String('abc'))=(new String('abc'))"); + + // MethodReference + expectFailNotIncrementable(parser, ctx, "m()++"); + expectFailNotDecrementable(parser, ctx, "--m()"); + expectFailSetValueNotSupported(parser, ctx, "m()=m()"); + + // OperatorMatches + expectFailNotAssignable(parser, ctx, "('abc' matches '^a..')++"); + expectFailNotAssignable(parser, ctx, "--('abc' matches '^a..')"); + expectFailSetValueNotSupported(parser, ctx, "('abc' matches '^a..')=('abc' matches '^a..')"); + + // Selection + ctx.registerFunction("isEven", Spr9751.class.getDeclaredMethod("isEven", Integer.TYPE)); + + expectFailNotIncrementable(parser, ctx, "({1,2,3}.?[#isEven(#this)])++"); + expectFailNotDecrementable(parser, ctx, "--({1,2,3}.?[#isEven(#this)])"); + expectFailNotAssignable(parser, ctx, "({1,2,3}.?[#isEven(#this)])=({1,2,3}.?[#isEven(#this)])"); + + // slightly diff here because return value isn't a list, it is a single entity + expectFailNotAssignable(parser, ctx, "({1,2,3}.^[#isEven(#this)])++"); + expectFailNotAssignable(parser, ctx, "--({1,2,3}.^[#isEven(#this)])"); + expectFailNotAssignable(parser, ctx, "({1,2,3}.^[#isEven(#this)])=({1,2,3}.^[#isEven(#this)])"); + + expectFailNotAssignable(parser, ctx, "({1,2,3}.$[#isEven(#this)])++"); + expectFailNotAssignable(parser, ctx, "--({1,2,3}.$[#isEven(#this)])"); + expectFailNotAssignable(parser, ctx, "({1,2,3}.$[#isEven(#this)])=({1,2,3}.$[#isEven(#this)])"); + + // FunctionReference + expectFailNotAssignable(parser, ctx, "#isEven(3)++"); + expectFailNotAssignable(parser, ctx, "--#isEven(4)"); + expectFailSetValueNotSupported(parser, ctx, "#isEven(3)=#isEven(5)"); + + // VariableReference + ctx.setVariable("wibble", "hello world"); + expectFailNotIncrementable(parser, ctx, "#wibble++"); + expectFailNotDecrementable(parser, ctx, "--#wibble"); + e = parser.parseExpression("#wibble=#wibble+#wibble"); + String s = e.getValue(ctx, String.class); + assertThat(s).isEqualTo("hello worldhello world"); + assertThat(ctx.lookupVariable("wibble")).isEqualTo("hello worldhello world"); + + ctx.setVariable("wobble", 3); + e = parser.parseExpression("#wobble++"); + assertThat(((Integer) ctx.lookupVariable("wobble")).intValue()).isEqualTo(3); + int r = e.getValue(ctx, Integer.TYPE); + assertThat(r).isEqualTo(3); + assertThat(((Integer) ctx.lookupVariable("wobble")).intValue()).isEqualTo(4); + + e = parser.parseExpression("--#wobble"); + assertThat(((Integer) ctx.lookupVariable("wobble")).intValue()).isEqualTo(4); + r = e.getValue(ctx, Integer.TYPE); + assertThat(r).isEqualTo(3); + assertThat(((Integer) ctx.lookupVariable("wobble")).intValue()).isEqualTo(3); + + e = parser.parseExpression("#wobble=34"); + assertThat(((Integer) ctx.lookupVariable("wobble")).intValue()).isEqualTo(3); + r = e.getValue(ctx, Integer.TYPE); + assertThat(r).isEqualTo(34); + assertThat(((Integer) ctx.lookupVariable("wobble")).intValue()).isEqualTo(34); + + // Projection + expectFailNotIncrementable(parser, ctx, "({1,2,3}.![#isEven(#this)])++"); // projection would be {false,true,false} + expectFailNotDecrementable(parser, ctx, "--({1,2,3}.![#isEven(#this)])"); // projection would be {false,true,false} + expectFailNotAssignable(parser, ctx, "({1,2,3}.![#isEven(#this)])=({1,2,3}.![#isEven(#this)])"); + + // InlineList + expectFailNotAssignable(parser, ctx, "({1,2,3})++"); + expectFailNotAssignable(parser, ctx, "--({1,2,3})"); + expectFailSetValueNotSupported(parser, ctx, "({1,2,3})=({1,2,3})"); + + // InlineMap + expectFailNotAssignable(parser, ctx, "({'a':1,'b':2,'c':3})++"); + expectFailNotAssignable(parser, ctx, "--({'a':1,'b':2,'c':3})"); + expectFailSetValueNotSupported(parser, ctx, "({'a':1,'b':2,'c':3})=({'a':1,'b':2,'c':3})"); + + // BeanReference + ctx.setBeanResolver(new MyBeanResolver()); + expectFailNotAssignable(parser, ctx, "@foo++"); + expectFailNotAssignable(parser, ctx, "--@foo"); + expectFailSetValueNotSupported(parser, ctx, "@foo=@bar"); + + // PropertyOrFieldReference + helper.iii = 42; + e = parser.parseExpression("iii++"); + assertThat(helper.iii).isEqualTo(42); + r = e.getValue(ctx, Integer.TYPE); + assertThat(r).isEqualTo(42); + assertThat(helper.iii).isEqualTo(43); + + e = parser.parseExpression("--iii"); + assertThat(helper.iii).isEqualTo(43); + r = e.getValue(ctx, Integer.TYPE); + assertThat(r).isEqualTo(42); + assertThat(helper.iii).isEqualTo(42); + + e = parser.parseExpression("iii=100"); + assertThat(helper.iii).isEqualTo(42); + r = e.getValue(ctx, Integer.TYPE); + assertThat(r).isEqualTo(100); + assertThat(helper.iii).isEqualTo(100); + } + + private void expectFailNotAssignable(ExpressionParser parser, EvaluationContext eContext, String expressionString) { + expectFail(parser, eContext, expressionString, SpelMessage.NOT_ASSIGNABLE); + } + + private void expectFailSetValueNotSupported(ExpressionParser parser, EvaluationContext eContext, String expressionString) { + expectFail(parser, eContext, expressionString, SpelMessage.SETVALUE_NOT_SUPPORTED); + } + + private void expectFailNotIncrementable(ExpressionParser parser, EvaluationContext eContext, String expressionString) { + expectFail(parser, eContext, expressionString, SpelMessage.OPERAND_NOT_INCREMENTABLE); + } + + private void expectFailNotDecrementable(ExpressionParser parser, EvaluationContext eContext, String expressionString) { + expectFail(parser, eContext, expressionString, SpelMessage.OPERAND_NOT_DECREMENTABLE); + } + + private void expectFail(ExpressionParser parser, EvaluationContext eContext, String expressionString, SpelMessage messageCode) { + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> { + Expression e = parser.parseExpression(expressionString); + SpelUtilities.printAbstractSyntaxTree(System.out, e); + e.getValue(eContext); + }).satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(messageCode)); + } + + static class CustomMethodResolver implements MethodResolver { + + @Override + public MethodExecutor resolve(EvaluationContext context, Object targetObject, String name, + List argumentTypes) throws AccessException { + return null; + } + } + + + static class CustomMethodFilter implements MethodFilter { + + @Override + public List filter(List methods) { + return null; + } + + } + + + @SuppressWarnings("rawtypes") + static class TestClass { + + public Foo wibble; + private Foo wibble2; + public Map map; + public Map mapStringToInteger; + public List list; + public List list2; + private Map map2; + private List foo; + + public Map getMap2() { return this.map2; } + public Foo getWibble2() { return this.wibble2; } + public List getFoo() { return this.foo; } + public void setFoo(List newfoo) { this.foo = newfoo; } + } + + + public static class Foo { + + public String bar = "hello"; + + public Foo() {} + } + + + // increment/decrement operators - SPR-9751 + static class Spr9751 { + + public String type = "hello"; + public BigDecimal bd = new BigDecimal("2"); + public double ddd = 2.0d; + public float fff = 3.0f; + public long lll = 66666L; + public int iii = 42; + public short sss = (short)15; + public Spr9751_2 foo = new Spr9751_2(); + + public int[] intArray = new int[]{1,2,3,4,5}; + public int index1 = 2; + + public Integer[] integerArray; + public int index2 = 2; + + public List listOfStrings; + public int index3 = 0; + + public Spr9751() { + integerArray = new Integer[5]; + integerArray[0] = 1; + integerArray[1] = 2; + integerArray[2] = 3; + integerArray[3] = 4; + integerArray[4] = 5; + listOfStrings = new ArrayList<>(); + listOfStrings.add("abc"); + } + + public void m() {} + + public static boolean isEven(int i) { + return (i%2)==0; + } + } + + + static class Spr9751_2 { + + public int iii = 99; + } + + + static class MyBeanResolver implements BeanResolver { + + @Override + public Object resolve(EvaluationContext context, String beanName) throws AccessException { + if (beanName.equals("foo") || beanName.equals("bar")) { + return new Spr9751_2(); + } + throw new AccessException("not heard of " + beanName); + } + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionLanguageScenarioTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionLanguageScenarioTests.java new file mode 100644 index 0000000..aa6bf03 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionLanguageScenarioTests.java @@ -0,0 +1,315 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import java.awt.Color; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Expression; +import org.springframework.expression.ParseException; +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +///CLOVER:OFF + +/** + * Testcases showing the common scenarios/use-cases for picking up the expression language support. + * The first test shows very basic usage, just drop it in and go. By 'standard infrastructure', it means:
    + *
      + *
    • The context classloader is used (so, the default classpath) + *
    • Some basic type converters are included + *
    • properties/methods/constructors are discovered and invoked using reflection + *
    + * The scenarios after that then how to plug in extensions:
    + *
      + *
    • Adding entries to the classpath that will be used to load types and define well known 'imports' + *
    • Defining variables that are then accessible in the expression + *
    • Changing the root context object against which non-qualified references are resolved + *
    • Registering java methods as functions callable from the expression + *
    • Adding a basic property resolver + *
    • Adding an advanced (better performing) property resolver + *
    • Adding your own type converter to support conversion between any types you like + *
    + * + * @author Andy Clement + */ +public class ExpressionLanguageScenarioTests extends AbstractExpressionTests { + + /** + * Scenario: using the standard infrastructure and running simple expression evaluation. + */ + @Test + public void testScenario_UsingStandardInfrastructure() { + try { + // Create a parser + SpelExpressionParser parser = new SpelExpressionParser(); + // Parse an expression + Expression expr = parser.parseRaw("new String('hello world')"); + // Evaluate it using a 'standard' context + Object value = expr.getValue(); + // They are reusable + value = expr.getValue(); + + assertThat(value).isEqualTo("hello world"); + assertThat(value.getClass()).isEqualTo(String.class); + } + catch (EvaluationException | ParseException ex) { + throw new AssertionError(ex.getMessage(), ex); + } + } + + /** + * Scenario: using the standard context but adding your own variables + */ + @Test + public void testScenario_DefiningVariablesThatWillBeAccessibleInExpressions() throws Exception { + // Create a parser + SpelExpressionParser parser = new SpelExpressionParser(); + // Use the standard evaluation context + StandardEvaluationContext ctx = new StandardEvaluationContext(); + ctx.setVariable("favouriteColour","blue"); + List primes = Arrays.asList(2, 3, 5, 7, 11, 13, 17); + ctx.setVariable("primes",primes); + + Expression expr = parser.parseRaw("#favouriteColour"); + Object value = expr.getValue(ctx); + assertThat(value).isEqualTo("blue"); + + expr = parser.parseRaw("#primes.get(1)"); + value = expr.getValue(ctx); + assertThat(value).isEqualTo(3); + + // all prime numbers > 10 from the list (using selection ?{...}) + expr = parser.parseRaw("#primes.?[#this>10]"); + value = expr.getValue(ctx); + assertThat(value.toString()).isEqualTo("[11, 13, 17]"); + } + + + static class TestClass { + public String str; + private int property; + public int getProperty() { return property; } + public void setProperty(int i) { property = i; } + } + + /** + * Scenario: using your own root context object + */ + @Test + public void testScenario_UsingADifferentRootContextObject() throws Exception { + // Create a parser + SpelExpressionParser parser = new SpelExpressionParser(); + // Use the standard evaluation context + StandardEvaluationContext ctx = new StandardEvaluationContext(); + + TestClass tc = new TestClass(); + tc.setProperty(42); + tc.str = "wibble"; + ctx.setRootObject(tc); + + // read it, set it, read it again + Expression expr = parser.parseRaw("str"); + Object value = expr.getValue(ctx); + assertThat(value).isEqualTo("wibble"); + expr = parser.parseRaw("str"); + expr.setValue(ctx, "wobble"); + expr = parser.parseRaw("str"); + value = expr.getValue(ctx); + assertThat(value).isEqualTo("wobble"); + // or using assignment within the expression + expr = parser.parseRaw("str='wabble'"); + value = expr.getValue(ctx); + expr = parser.parseRaw("str"); + value = expr.getValue(ctx); + assertThat(value).isEqualTo("wabble"); + + // private property will be accessed through getter() + expr = parser.parseRaw("property"); + value = expr.getValue(ctx); + assertThat(value).isEqualTo(42); + + // ... and set through setter + expr = parser.parseRaw("property=4"); + value = expr.getValue(ctx); + expr = parser.parseRaw("property"); + value = expr.getValue(ctx); + assertThat(value).isEqualTo(4); + } + + public static String repeat(String s) { return s+s; } + + /** + * Scenario: using your own java methods and calling them from the expression + */ + @Test + public void testScenario_RegisteringJavaMethodsAsFunctionsAndCallingThem() throws SecurityException, NoSuchMethodException { + try { + // Create a parser + SpelExpressionParser parser = new SpelExpressionParser(); + // Use the standard evaluation context + StandardEvaluationContext ctx = new StandardEvaluationContext(); + ctx.registerFunction("repeat",ExpressionLanguageScenarioTests.class.getDeclaredMethod("repeat",String.class)); + + Expression expr = parser.parseRaw("#repeat('hello')"); + Object value = expr.getValue(ctx); + assertThat(value).isEqualTo("hellohello"); + + } + catch (EvaluationException | ParseException ex) { + throw new AssertionError(ex.getMessage(), ex); + } + } + + /** + * Scenario: add a property resolver that will get called in the resolver chain, this one only supports reading. + */ + @Test + public void testScenario_AddingYourOwnPropertyResolvers_1() throws Exception { + // Create a parser + SpelExpressionParser parser = new SpelExpressionParser(); + // Use the standard evaluation context + StandardEvaluationContext ctx = new StandardEvaluationContext(); + + ctx.addPropertyAccessor(new FruitColourAccessor()); + Expression expr = parser.parseRaw("orange"); + Object value = expr.getValue(ctx); + assertThat(value).isEqualTo(Color.orange); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + expr.setValue(ctx, Color.blue)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE_ON_NULL)); + } + + @Test + public void testScenario_AddingYourOwnPropertyResolvers_2() throws Exception { + // Create a parser + SpelExpressionParser parser = new SpelExpressionParser(); + // Use the standard evaluation context + StandardEvaluationContext ctx = new StandardEvaluationContext(); + + ctx.addPropertyAccessor(new VegetableColourAccessor()); + Expression expr = parser.parseRaw("pea"); + Object value = expr.getValue(ctx); + assertThat(value).isEqualTo(Color.green); + + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + expr.setValue(ctx, Color.blue)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE_ON_NULL)); + } + + + /** + * Regardless of the current context object, or root context object, this resolver can tell you what colour a fruit is ! + * It only supports property reading, not writing. To support writing it would need to override canWrite() and write() + */ + private static class FruitColourAccessor implements PropertyAccessor { + + private static Map propertyMap = new HashMap<>(); + + static { + propertyMap.put("banana",Color.yellow); + propertyMap.put("apple",Color.red); + propertyMap.put("orange",Color.orange); + } + + /** + * Null means you might be able to read any property, if an earlier property resolver hasn't beaten you to it + */ + @Override + public Class[] getSpecificTargetClasses() { + return null; + } + + @Override + public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException { + return propertyMap.containsKey(name); + } + + @Override + public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException { + return new TypedValue(propertyMap.get(name)); + } + + @Override + public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException { + return false; + } + + @Override + public void write(EvaluationContext context, Object target, String name, Object newValue) + throws AccessException { + } + + } + + + /** + * Regardless of the current context object, or root context object, this resolver can tell you what colour a vegetable is ! + * It only supports property reading, not writing. + */ + private static class VegetableColourAccessor implements PropertyAccessor { + + private static Map propertyMap = new HashMap<>(); + + static { + propertyMap.put("carrot",Color.orange); + propertyMap.put("pea",Color.green); + } + + /** + * Null means you might be able to read any property, if an earlier property resolver hasn't beaten you to it + */ + @Override + public Class[] getSpecificTargetClasses() { + return null; + } + + @Override + public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException { + return propertyMap.containsKey(name); + } + + @Override + public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException { + return new TypedValue(propertyMap.get(name)); + } + + @Override + public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException { + return false; + } + + @Override + public void write(EvaluationContext context, Object target, String name, Object newValue) throws AccessException { + } + + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionStateTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionStateTests.java new file mode 100644 index 0000000..a789b20 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionStateTests.java @@ -0,0 +1,277 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Operation; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.expression.spel.testresources.Inventor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for the expression state object - some features are not yet exploited in the language (eg nested scopes) + * + * @author Andy Clement + * @author Juergen Hoeller + */ +public class ExpressionStateTests extends AbstractExpressionTests { + + @Test + public void testConstruction() { + EvaluationContext context = TestScenarioCreator.getTestEvaluationContext(); + ExpressionState state = new ExpressionState(context); + assertThat(state.getEvaluationContext()).isEqualTo(context); + } + + // Local variables are in variable scopes which come and go during evaluation. Normal variables are + // accessible through the evaluation context + + @Test + public void testLocalVariables() { + ExpressionState state = getState(); + + Object value = state.lookupLocalVariable("foo"); + assertThat(value).isNull(); + + state.setLocalVariable("foo",34); + value = state.lookupLocalVariable("foo"); + assertThat(value).isEqualTo(34); + + state.setLocalVariable("foo", null); + value = state.lookupLocalVariable("foo"); + assertThat(value).isEqualTo(null); + } + + @Test + public void testVariables() { + ExpressionState state = getState(); + TypedValue typedValue = state.lookupVariable("foo"); + assertThat(typedValue).isEqualTo(TypedValue.NULL); + + state.setVariable("foo",34); + typedValue = state.lookupVariable("foo"); + assertThat(typedValue.getValue()).isEqualTo(34); + assertThat(typedValue.getTypeDescriptor().getType()).isEqualTo(Integer.class); + + state.setVariable("foo","abc"); + typedValue = state.lookupVariable("foo"); + assertThat(typedValue.getValue()).isEqualTo("abc"); + assertThat(typedValue.getTypeDescriptor().getType()).isEqualTo(String.class); + } + + @Test + public void testNoVariableInterference() { + ExpressionState state = getState(); + TypedValue typedValue = state.lookupVariable("foo"); + assertThat(typedValue).isEqualTo(TypedValue.NULL); + + state.setLocalVariable("foo",34); + typedValue = state.lookupVariable("foo"); + assertThat(typedValue).isEqualTo(TypedValue.NULL); + + state.setVariable("goo", "hello"); + assertThat(state.lookupLocalVariable("goo")).isNull(); + } + + @Test + public void testLocalVariableNestedScopes() { + ExpressionState state = getState(); + assertThat(state.lookupLocalVariable("foo")).isEqualTo(null); + + state.setLocalVariable("foo",12); + assertThat(state.lookupLocalVariable("foo")).isEqualTo(12); + + state.enterScope(null); + // found in upper scope + assertThat(state.lookupLocalVariable("foo")).isEqualTo(12); + + state.setLocalVariable("foo","abc"); + // found in nested scope + assertThat(state.lookupLocalVariable("foo")).isEqualTo("abc"); + + state.exitScope(); + // found in nested scope + assertThat(state.lookupLocalVariable("foo")).isEqualTo(12); + } + + @Test + public void testRootContextObject() { + ExpressionState state = getState(); + assertThat(state.getRootContextObject().getValue().getClass()).isEqualTo(Inventor.class); + + // although the root object is being set on the evaluation context, the value in the 'state' remains what it was when constructed + ((StandardEvaluationContext) state.getEvaluationContext()).setRootObject(null); + assertThat(state.getRootContextObject().getValue().getClass()).isEqualTo(Inventor.class); + // assertEquals(null, state.getRootContextObject().getValue()); + + state = new ExpressionState(new StandardEvaluationContext()); + assertThat(state.getRootContextObject()).isEqualTo(TypedValue.NULL); + + + ((StandardEvaluationContext) state.getEvaluationContext()).setRootObject(null); + assertThat(state.getRootContextObject().getValue()).isEqualTo(null); + } + + @Test + public void testActiveContextObject() { + ExpressionState state = getState(); + assertThat(state.getActiveContextObject().getValue()).isEqualTo(state.getRootContextObject().getValue()); + + assertThatIllegalStateException().isThrownBy( + state::popActiveContextObject); + + state.pushActiveContextObject(new TypedValue(34)); + assertThat(state.getActiveContextObject().getValue()).isEqualTo(34); + + state.pushActiveContextObject(new TypedValue("hello")); + assertThat(state.getActiveContextObject().getValue()).isEqualTo("hello"); + + state.popActiveContextObject(); + assertThat(state.getActiveContextObject().getValue()).isEqualTo(34); + + state.popActiveContextObject(); + assertThat(state.getActiveContextObject().getValue()).isEqualTo(state.getRootContextObject().getValue()); + + state = new ExpressionState(new StandardEvaluationContext()); + assertThat(state.getActiveContextObject()).isEqualTo(TypedValue.NULL); + } + + @Test + public void testPopulatedNestedScopes() { + ExpressionState state = getState(); + assertThat(state.lookupLocalVariable("foo")).isNull(); + + state.enterScope("foo",34); + assertThat(state.lookupLocalVariable("foo")).isEqualTo(34); + + state.enterScope(null); + state.setLocalVariable("foo", 12); + assertThat(state.lookupLocalVariable("foo")).isEqualTo(12); + + state.exitScope(); + assertThat(state.lookupLocalVariable("foo")).isEqualTo(34); + + state.exitScope(); + assertThat(state.lookupLocalVariable("goo")).isNull(); + } + + @Test + public void testRootObjectConstructor() { + EvaluationContext ctx = getContext(); + // TypedValue root = ctx.getRootObject(); + // supplied should override root on context + ExpressionState state = new ExpressionState(ctx,new TypedValue("i am a string")); + TypedValue stateRoot = state.getRootContextObject(); + assertThat(stateRoot.getTypeDescriptor().getType()).isEqualTo(String.class); + assertThat(stateRoot.getValue()).isEqualTo("i am a string"); + } + + @Test + public void testPopulatedNestedScopesMap() { + ExpressionState state = getState(); + assertThat(state.lookupLocalVariable("foo")).isNull(); + assertThat(state.lookupLocalVariable("goo")).isNull(); + + Map m = new HashMap<>(); + m.put("foo", 34); + m.put("goo", "abc"); + + state.enterScope(m); + assertThat(state.lookupLocalVariable("foo")).isEqualTo(34); + assertThat(state.lookupLocalVariable("goo")).isEqualTo("abc"); + + state.enterScope(null); + state.setLocalVariable("foo",12); + assertThat(state.lookupLocalVariable("foo")).isEqualTo(12); + assertThat(state.lookupLocalVariable("goo")).isEqualTo("abc"); + + state.exitScope(); + state.exitScope(); + assertThat(state.lookupLocalVariable("foo")).isNull(); + assertThat(state.lookupLocalVariable("goo")).isNull(); + } + + @Test + public void testOperators() { + ExpressionState state = getState(); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + state.operate(Operation.ADD,1,2)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES)); + + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + state.operate(Operation.ADD,null,null)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES)); + } + + @Test + public void testComparator() { + ExpressionState state = getState(); + assertThat(state.getTypeComparator()).isEqualTo(state.getEvaluationContext().getTypeComparator()); + } + + @Test + public void testTypeLocator() throws EvaluationException { + ExpressionState state = getState(); + assertThat(state.getEvaluationContext().getTypeLocator()).isNotNull(); + assertThat(state.findType("java.lang.Integer")).isEqualTo(Integer.class); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + state.findType("someMadeUpName")) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.TYPE_NOT_FOUND)); + + } + + @Test + public void testTypeConversion() throws EvaluationException { + ExpressionState state = getState(); + String s = (String) state.convertValue(34, TypeDescriptor.valueOf(String.class)); + assertThat(s).isEqualTo("34"); + + s = (String)state.convertValue(new TypedValue(34), TypeDescriptor.valueOf(String.class)); + assertThat(s).isEqualTo("34"); + } + + @Test + public void testPropertyAccessors() { + ExpressionState state = getState(); + assertThat(state.getPropertyAccessors()).isEqualTo(state.getEvaluationContext().getPropertyAccessors()); + } + + /** + * @return a new ExpressionState + */ + private ExpressionState getState() { + EvaluationContext context = TestScenarioCreator.getTestEvaluationContext(); + ExpressionState state = new ExpressionState(context); + return state; + } + + private EvaluationContext getContext() { + return TestScenarioCreator.getTestEvaluationContext(); + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionWithConversionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionWithConversionTests.java new file mode 100644 index 0000000..6ad4fe6 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionWithConversionTests.java @@ -0,0 +1,216 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Expression; +import org.springframework.expression.TypeConverter; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Expression evaluation where the TypeConverter plugged in is the + * {@link org.springframework.core.convert.support.GenericConversionService}. + * + * @author Andy Clement + * @author Dave Syer + */ +public class ExpressionWithConversionTests extends AbstractExpressionTests { + + private static List listOfString = new ArrayList<>(); + private static TypeDescriptor typeDescriptorForListOfString = null; + private static List listOfInteger = new ArrayList<>(); + private static TypeDescriptor typeDescriptorForListOfInteger = null; + + static { + listOfString.add("1"); + listOfString.add("2"); + listOfString.add("3"); + listOfInteger.add(4); + listOfInteger.add(5); + listOfInteger.add(6); + } + + @BeforeEach + public void setUp() throws Exception { + ExpressionWithConversionTests.typeDescriptorForListOfString = new TypeDescriptor(ExpressionWithConversionTests.class.getDeclaredField("listOfString")); + ExpressionWithConversionTests.typeDescriptorForListOfInteger = new TypeDescriptor(ExpressionWithConversionTests.class.getDeclaredField("listOfInteger")); + } + + + /** + * Test the service can convert what we are about to use in the expression evaluation tests. + */ + @Test + public void testConversionsAvailable() throws Exception { + TypeConvertorUsingConversionService tcs = new TypeConvertorUsingConversionService(); + + // ArrayList containing List to List + Class clazz = typeDescriptorForListOfString.getElementTypeDescriptor().getType(); + assertThat(clazz).isEqualTo(String.class); + List l = (List) tcs.convertValue(listOfInteger, TypeDescriptor.forObject(listOfInteger), typeDescriptorForListOfString); + assertThat(l).isNotNull(); + + // ArrayList containing List to List + clazz = typeDescriptorForListOfInteger.getElementTypeDescriptor().getType(); + assertThat(clazz).isEqualTo(Integer.class); + + l = (List) tcs.convertValue(listOfString, TypeDescriptor.forObject(listOfString), typeDescriptorForListOfString); + assertThat(l).isNotNull(); + } + + @Test + public void testSetParameterizedList() throws Exception { + StandardEvaluationContext context = TestScenarioCreator.getTestEvaluationContext(); + Expression e = parser.parseExpression("listOfInteger.size()"); + assertThat(e.getValue(context, Integer.class).intValue()).isEqualTo(0); + context.setTypeConverter(new TypeConvertorUsingConversionService()); + // Assign a List to the List field - the component elements should be converted + parser.parseExpression("listOfInteger").setValue(context,listOfString); + // size now 3 + assertThat(e.getValue(context, Integer.class).intValue()).isEqualTo(3); + Class clazz = parser.parseExpression("listOfInteger[1].getClass()").getValue(context, Class.class); // element type correctly Integer + assertThat(clazz).isEqualTo(Integer.class); + } + + @Test + public void testCoercionToCollectionOfPrimitive() throws Exception { + + class TestTarget { + @SuppressWarnings("unused") + public int sum(Collection numbers) { + int total = 0; + for (int i : numbers) { + total += i; + } + return total; + } + } + + StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); + + TypeDescriptor collectionType = new TypeDescriptor(new MethodParameter(TestTarget.class.getDeclaredMethod( + "sum", Collection.class), 0)); + // The type conversion is possible + assertThat(evaluationContext.getTypeConverter() + .canConvert(TypeDescriptor.valueOf(String.class), collectionType)).isTrue(); + // ... and it can be done successfully + assertThat(evaluationContext.getTypeConverter().convertValue("1,2,3,4", TypeDescriptor.valueOf(String.class), collectionType).toString()).isEqualTo("[1, 2, 3, 4]"); + + evaluationContext.setVariable("target", new TestTarget()); + + // OK up to here, so the evaluation should be fine... + // ... but this fails + int result = (Integer) parser.parseExpression("#target.sum(#root)").getValue(evaluationContext, "1,2,3,4"); + assertThat(result).as("Wrong result: " + result).isEqualTo(10); + + } + + @Test + public void testConvert() { + Foo root = new Foo("bar"); + Collection foos = Collections.singletonList("baz"); + + StandardEvaluationContext context = new StandardEvaluationContext(root); + + // property access + Expression expression = parser.parseExpression("foos"); + expression.setValue(context, foos); + Foo baz = root.getFoos().iterator().next(); + assertThat(baz.value).isEqualTo("baz"); + + // method call + expression = parser.parseExpression("setFoos(#foos)"); + context.setVariable("foos", foos); + expression.getValue(context); + baz = root.getFoos().iterator().next(); + assertThat(baz.value).isEqualTo("baz"); + + // method call with result from method call + expression = parser.parseExpression("setFoos(getFoosAsStrings())"); + expression.getValue(context); + baz = root.getFoos().iterator().next(); + assertThat(baz.value).isEqualTo("baz"); + + // method call with result from method call + expression = parser.parseExpression("setFoos(getFoosAsObjects())"); + expression.getValue(context); + baz = root.getFoos().iterator().next(); + assertThat(baz.value).isEqualTo("baz"); + } + + + /** + * Type converter that uses the core conversion service. + */ + private static class TypeConvertorUsingConversionService implements TypeConverter { + + private final ConversionService service = new DefaultConversionService(); + + @Override + public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) { + return this.service.canConvert(sourceType, targetType); + } + + @Override + public Object convertValue(Object value, TypeDescriptor sourceType, TypeDescriptor targetType) throws EvaluationException { + return this.service.convert(value, sourceType, targetType); + } + } + + + public static class Foo { + + public final String value; + + private Collection foos; + + public Foo(String value) { + this.value = value; + } + + public void setFoos(Collection foos) { + this.foos = foos; + } + + public Collection getFoos() { + return this.foos; + } + + public Collection getFoosAsStrings() { + return Collections.singletonList("baz"); + } + + public Collection getFoosAsObjects() { + return Collections.singletonList("baz"); + } + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/InProgressTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/InProgressTests.java new file mode 100644 index 0000000..a1c7d77 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/InProgressTests.java @@ -0,0 +1,158 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import java.util.ArrayList; +import java.util.HashMap; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.spel.standard.SpelExpression; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * These are tests for language features that are not yet considered 'live'. Either missing implementation or + * documentation. + * + * Where implementation is missing the tests are commented out. + * + * @author Andy Clement + */ +public class InProgressTests extends AbstractExpressionTests { + + @Test + public void testRelOperatorsBetween01() { + evaluate("1 between listOneFive", "true", Boolean.class); + // no inline list building at the moment + // evaluate("1 between {1, 5}", "true", Boolean.class); + } + + @Test + public void testRelOperatorsBetweenErrors01() { + evaluateAndCheckError("1 between T(String)", SpelMessage.BETWEEN_RIGHT_OPERAND_MUST_BE_TWO_ELEMENT_LIST, 10); + } + + @Test + public void testRelOperatorsBetweenErrors03() { + evaluateAndCheckError("1 between listOfNumbersUpToTen", + SpelMessage.BETWEEN_RIGHT_OPERAND_MUST_BE_TWO_ELEMENT_LIST, 10); + } + + // PROJECTION + @Test + public void testProjection01() { + evaluate("listOfNumbersUpToTen.![#this<5?'y':'n']", "[y, y, y, y, n, n, n, n, n, n]", ArrayList.class); + // inline list creation not supported at the moment + // evaluate("{1,2,3,4,5,6,7,8,9,10}.!{#isEven(#this)}", "[n, y, n, y, n, y, n, y, n, y]", ArrayList.class); + } + + @Test + public void testProjection02() { + // inline map creation not supported at the moment + // evaluate("#{'a':'y','b':'n','c':'y'}.![value=='y'?key:null].nonnull().sort()", "[a, c]", ArrayList.class); + evaluate("mapOfNumbersUpToTen.![key>5?value:null]", + "[null, null, null, null, null, six, seven, eight, nine, ten]", ArrayList.class); + } + + @Test + public void testProjection05() { + evaluateAndCheckError("'abc'.![true]", SpelMessage.PROJECTION_NOT_SUPPORTED_ON_TYPE); + evaluateAndCheckError("null.![true]", SpelMessage.PROJECTION_NOT_SUPPORTED_ON_TYPE); + evaluate("null?.![true]", null, null); + } + + @Test + public void testProjection06() throws Exception { + SpelExpression expr = (SpelExpression) parser.parseExpression("'abc'.![true]"); + assertThat(expr.toStringAST()).isEqualTo("'abc'.![true]"); + } + + // SELECTION + + @Test + public void testSelection02() { + evaluate("testMap.keySet().?[#this matches '.*o.*']", "[monday]", ArrayList.class); + evaluate("testMap.keySet().?[#this matches '.*r.*'].contains('saturday')", "true", Boolean.class); + evaluate("testMap.keySet().?[#this matches '.*r.*'].size()", "3", Integer.class); + } + + @Test + public void testSelectionError_NonBooleanSelectionCriteria() { + evaluateAndCheckError("listOfNumbersUpToTen.?['nonboolean']", + SpelMessage.RESULT_OF_SELECTION_CRITERIA_IS_NOT_BOOLEAN); + } + + @Test + public void testSelection03() { + evaluate("mapOfNumbersUpToTen.?[key>5].size()", "5", Integer.class); + } + + @Test + public void testSelection04() { + evaluateAndCheckError("mapOfNumbersUpToTen.?['hello'].size()", + SpelMessage.RESULT_OF_SELECTION_CRITERIA_IS_NOT_BOOLEAN); + } + + @Test + public void testSelection05() { + evaluate("mapOfNumbersUpToTen.?[key>11].size()", "0", Integer.class); + evaluate("mapOfNumbersUpToTen.^[key>11]", null, null); + evaluate("mapOfNumbersUpToTen.$[key>11]", null, null); + evaluate("null?.$[key>11]", null, null); + evaluateAndCheckError("null.?[key>11]", SpelMessage.INVALID_TYPE_FOR_SELECTION); + evaluateAndCheckError("'abc'.?[key>11]", SpelMessage.INVALID_TYPE_FOR_SELECTION); + } + + @Test + public void testSelectionFirst01() { + evaluate("listOfNumbersUpToTen.^[#isEven(#this) == 'y']", "2", Integer.class); + } + + @Test + public void testSelectionFirst02() { + evaluate("mapOfNumbersUpToTen.^[key>5].size()", "1", Integer.class); + } + + @Test + public void testSelectionLast01() { + evaluate("listOfNumbersUpToTen.$[#isEven(#this) == 'y']", "10", Integer.class); + } + + @Test + public void testSelectionLast02() { + evaluate("mapOfNumbersUpToTen.$[key>5]", "{10=ten}", HashMap.class); + evaluate("mapOfNumbersUpToTen.$[key>5].size()", "1", Integer.class); + } + + @Test + public void testSelectionAST() throws Exception { + SpelExpression expr = (SpelExpression) parser.parseExpression("'abc'.^[true]"); + assertThat(expr.toStringAST()).isEqualTo("'abc'.^[true]"); + expr = (SpelExpression) parser.parseExpression("'abc'.?[true]"); + assertThat(expr.toStringAST()).isEqualTo("'abc'.?[true]"); + expr = (SpelExpression) parser.parseExpression("'abc'.$[true]"); + assertThat(expr.toStringAST()).isEqualTo("'abc'.$[true]"); + } + + // Constructor invocation + @Test + public void testSetConstruction01() { + evaluate("new java.util.HashSet().addAll({'a','b','c'})", "true", Boolean.class); + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.java new file mode 100644 index 0000000..922ec4a --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.java @@ -0,0 +1,412 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Expression; +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("rawtypes") +public class IndexingTests { + + @Test + @SuppressWarnings("unchecked") + public void indexIntoGenericPropertyContainingMap() { + Map property = new HashMap<>(); + property.put("foo", "bar"); + this.property = property; + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expression = parser.parseExpression("property"); + assertThat(expression.getValueTypeDescriptor(this).toString()).isEqualTo("@org.springframework.expression.spel.IndexingTests$FieldAnnotation java.util.HashMap"); + assertThat(expression.getValue(this)).isEqualTo(property); + assertThat(expression.getValue(this, Map.class)).isEqualTo(property); + expression = parser.parseExpression("property['foo']"); + assertThat(expression.getValue(this)).isEqualTo("bar"); + } + + @FieldAnnotation + public Object property; + + @Test + @SuppressWarnings("unchecked") + public void indexIntoGenericPropertyContainingMapObject() { + Map> property = new HashMap<>(); + Map map = new HashMap<>(); + map.put("foo", "bar"); + property.put("property", map); + SpelExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + context.addPropertyAccessor(new MapAccessor()); + context.setRootObject(property); + Expression expression = parser.parseExpression("property"); + assertThat(expression.getValueTypeDescriptor(context).toString()).isEqualTo("java.util.HashMap"); + assertThat(expression.getValue(context)).isEqualTo(map); + assertThat(expression.getValue(context, Map.class)).isEqualTo(map); + expression = parser.parseExpression("property['foo']"); + assertThat(expression.getValue(context)).isEqualTo("bar"); + } + + public static class MapAccessor implements PropertyAccessor { + + @Override + public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException { + return (((Map) target).containsKey(name)); + } + + @Override + public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException { + return new TypedValue(((Map) target).get(name)); + } + + @Override + public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException { + return true; + } + + @Override + @SuppressWarnings("unchecked") + public void write(EvaluationContext context, Object target, String name, Object newValue) + throws AccessException { + ((Map) target).put(name, newValue); + } + + @Override + public Class[] getSpecificTargetClasses() { + return new Class[] {Map.class}; + } + + } + + @Test + public void setGenericPropertyContainingMap() { + Map property = new HashMap<>(); + property.put("foo", "bar"); + this.property = property; + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expression = parser.parseExpression("property"); + assertThat(expression.getValueTypeDescriptor(this).toString()).isEqualTo("@org.springframework.expression.spel.IndexingTests$FieldAnnotation java.util.HashMap"); + assertThat(expression.getValue(this)).isEqualTo(property); + expression = parser.parseExpression("property['foo']"); + assertThat(expression.getValue(this)).isEqualTo("bar"); + expression.setValue(this, "baz"); + assertThat(expression.getValue(this)).isEqualTo("baz"); + } + + @Test + public void setPropertyContainingMap() { + Map property = new HashMap<>(); + property.put(9, 3); + this.parameterizedMap = property; + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expression = parser.parseExpression("parameterizedMap"); + assertThat(expression.getValueTypeDescriptor(this).toString()).isEqualTo("java.util.HashMap"); + assertThat(expression.getValue(this)).isEqualTo(property); + expression = parser.parseExpression("parameterizedMap['9']"); + assertThat(expression.getValue(this)).isEqualTo(3); + expression.setValue(this, "37"); + assertThat(expression.getValue(this)).isEqualTo(37); + } + + public Map parameterizedMap; + + @Test + public void setPropertyContainingMapAutoGrow() { + SpelExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, false)); + Expression expression = parser.parseExpression("parameterizedMap"); + assertThat(expression.getValueTypeDescriptor(this).toString()).isEqualTo("java.util.Map"); + assertThat(expression.getValue(this)).isEqualTo(property); + expression = parser.parseExpression("parameterizedMap['9']"); + assertThat(expression.getValue(this)).isEqualTo(null); + expression.setValue(this, "37"); + assertThat(expression.getValue(this)).isEqualTo(37); + } + + @Test + public void indexIntoGenericPropertyContainingList() { + List property = new ArrayList<>(); + property.add("bar"); + this.property = property; + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expression = parser.parseExpression("property"); + assertThat(expression.getValueTypeDescriptor(this).toString()).isEqualTo("@org.springframework.expression.spel.IndexingTests$FieldAnnotation java.util.ArrayList"); + assertThat(expression.getValue(this)).isEqualTo(property); + expression = parser.parseExpression("property[0]"); + assertThat(expression.getValue(this)).isEqualTo("bar"); + } + + @Test + public void setGenericPropertyContainingList() { + List property = new ArrayList<>(); + property.add(3); + this.property = property; + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expression = parser.parseExpression("property"); + assertThat(expression.getValueTypeDescriptor(this).toString()).isEqualTo("@org.springframework.expression.spel.IndexingTests$FieldAnnotation java.util.ArrayList"); + assertThat(expression.getValue(this)).isEqualTo(property); + expression = parser.parseExpression("property[0]"); + assertThat(expression.getValue(this)).isEqualTo(3); + expression.setValue(this, "4"); + assertThat(expression.getValue(this)).isEqualTo("4"); + } + + @Test + public void setGenericPropertyContainingListAutogrow() { + List property = new ArrayList<>(); + this.property = property; + SpelExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + Expression expression = parser.parseExpression("property"); + assertThat(expression.getValueTypeDescriptor(this).toString()).isEqualTo("@org.springframework.expression.spel.IndexingTests$FieldAnnotation java.util.ArrayList"); + assertThat(expression.getValue(this)).isEqualTo(property); + expression = parser.parseExpression("property[0]"); + try { + expression.setValue(this, "4"); + } + catch (EvaluationException ex) { + assertThat(ex.getMessage().startsWith("EL1053E")).isTrue(); + } + } + + public List decimals; + + @Test + public void autoGrowListOfElementsWithoutDefaultConstructor() { + this.decimals = new ArrayList<>(); + SpelExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + parser.parseExpression("decimals[0]").setValue(this, "123.4"); + assertThat(decimals).containsExactly(BigDecimal.valueOf(123.4)); + } + + @Test + public void indexIntoPropertyContainingListContainingNullElement() { + this.decimals = new ArrayList<>(); + this.decimals.add(null); + this.decimals.add(BigDecimal.ONE); + SpelExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); + parser.parseExpression("decimals[0]").setValue(this, "9876.5"); + assertThat(decimals).containsExactly(BigDecimal.valueOf(9876.5), BigDecimal.ONE); + } + + @Test + public void indexIntoPropertyContainingList() { + List property = new ArrayList<>(); + property.add(3); + this.parameterizedList = property; + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expression = parser.parseExpression("parameterizedList"); + assertThat(expression.getValueTypeDescriptor(this).toString()).isEqualTo("java.util.ArrayList"); + assertThat(expression.getValue(this)).isEqualTo(property); + expression = parser.parseExpression("parameterizedList[0]"); + assertThat(expression.getValue(this)).isEqualTo(3); + } + + public List parameterizedList; + + @Test + public void indexIntoPropertyContainingListOfList() { + List> property = new ArrayList<>(); + property.add(Arrays.asList(3)); + this.parameterizedListOfList = property; + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expression = parser.parseExpression("parameterizedListOfList[0]"); + assertThat(expression.getValueTypeDescriptor(this).toString()).isEqualTo("java.util.Arrays$ArrayList"); + assertThat(expression.getValue(this)).isEqualTo(property.get(0)); + expression = parser.parseExpression("parameterizedListOfList[0][0]"); + assertThat(expression.getValue(this)).isEqualTo(3); + } + + public List> parameterizedListOfList; + + @Test + public void setPropertyContainingList() { + List property = new ArrayList<>(); + property.add(3); + this.parameterizedList = property; + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expression = parser.parseExpression("parameterizedList"); + assertThat(expression.getValueTypeDescriptor(this).toString()).isEqualTo("java.util.ArrayList"); + assertThat(expression.getValue(this)).isEqualTo(property); + expression = parser.parseExpression("parameterizedList[0]"); + assertThat(expression.getValue(this)).isEqualTo(3); + expression.setValue(this, "4"); + assertThat(expression.getValue(this)).isEqualTo(4); + } + + @Test + public void indexIntoGenericPropertyContainingNullList() { + SpelParserConfiguration configuration = new SpelParserConfiguration(true, true); + SpelExpressionParser parser = new SpelExpressionParser(configuration); + Expression expression = parser.parseExpression("property"); + assertThat(expression.getValueTypeDescriptor(this).toString()).isEqualTo("@org.springframework.expression.spel.IndexingTests$FieldAnnotation java.lang.Object"); + assertThat(expression.getValue(this)).isEqualTo(property); + expression = parser.parseExpression("property[0]"); + try { + assertThat(expression.getValue(this)).isEqualTo("bar"); + } + catch (EvaluationException ex) { + assertThat(ex.getMessage().startsWith("EL1027E")).isTrue(); + } + } + + @Test + public void indexIntoGenericPropertyContainingGrowingList() { + List property = new ArrayList<>(); + this.property = property; + SpelParserConfiguration configuration = new SpelParserConfiguration(true, true); + SpelExpressionParser parser = new SpelExpressionParser(configuration); + Expression expression = parser.parseExpression("property"); + assertThat(expression.getValueTypeDescriptor(this).toString()).isEqualTo("@org.springframework.expression.spel.IndexingTests$FieldAnnotation java.util.ArrayList"); + assertThat(expression.getValue(this)).isEqualTo(property); + expression = parser.parseExpression("property[0]"); + try { + assertThat(expression.getValue(this)).isEqualTo("bar"); + } + catch (EvaluationException ex) { + assertThat(ex.getMessage().startsWith("EL1053E")).isTrue(); + } + } + + @Test + public void indexIntoGenericPropertyContainingGrowingList2() { + List property2 = new ArrayList<>(); + this.property2 = property2; + SpelParserConfiguration configuration = new SpelParserConfiguration(true, true); + SpelExpressionParser parser = new SpelExpressionParser(configuration); + Expression expression = parser.parseExpression("property2"); + assertThat(expression.getValueTypeDescriptor(this).toString()).isEqualTo("java.util.ArrayList"); + assertThat(expression.getValue(this)).isEqualTo(property2); + expression = parser.parseExpression("property2[0]"); + try { + assertThat(expression.getValue(this)).isEqualTo("bar"); + } + catch (EvaluationException ex) { + assertThat(ex.getMessage().startsWith("EL1053E")).isTrue(); + } + } + + public List property2; + + @Test + public void indexIntoGenericPropertyContainingArray() { + String[] property = new String[] { "bar" }; + this.property = property; + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expression = parser.parseExpression("property"); + assertThat(expression.getValueTypeDescriptor(this).toString()).isEqualTo("@org.springframework.expression.spel.IndexingTests$FieldAnnotation java.lang.String[]"); + assertThat(expression.getValue(this)).isEqualTo(property); + expression = parser.parseExpression("property[0]"); + assertThat(expression.getValue(this)).isEqualTo("bar"); + } + + @Test + public void emptyList() { + listOfScalarNotGeneric = new ArrayList(); + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expression = parser.parseExpression("listOfScalarNotGeneric"); + assertThat(expression.getValueTypeDescriptor(this).toString()).isEqualTo("java.util.ArrayList"); + assertThat(expression.getValue(this, String.class)).isEqualTo(""); + } + + @SuppressWarnings("unchecked") + @Test + public void resolveCollectionElementType() { + listNotGeneric = new ArrayList(2); + listNotGeneric.add(5); + listNotGeneric.add(6); + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expression = parser.parseExpression("listNotGeneric"); + assertThat(expression.getValueTypeDescriptor(this).toString()).isEqualTo("@org.springframework.expression.spel.IndexingTests$FieldAnnotation java.util.ArrayList"); + assertThat(expression.getValue(this, String.class)).isEqualTo("5,6"); + } + + @Test + public void resolveCollectionElementTypeNull() { + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expression = parser.parseExpression("listNotGeneric"); + assertThat(expression.getValueTypeDescriptor(this).toString()).isEqualTo("@org.springframework.expression.spel.IndexingTests$FieldAnnotation java.util.List"); + } + + @FieldAnnotation + public List listNotGeneric; + + @Target({ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + public @interface FieldAnnotation { + + } + + @SuppressWarnings("unchecked") + @Test + public void resolveMapKeyValueTypes() { + mapNotGeneric = new HashMap(); + mapNotGeneric.put("baseAmount", 3.11); + mapNotGeneric.put("bonusAmount", 7.17); + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expression = parser.parseExpression("mapNotGeneric"); + assertThat(expression.getValueTypeDescriptor(this).toString()).isEqualTo("@org.springframework.expression.spel.IndexingTests$FieldAnnotation java.util.HashMap"); + } + + @FieldAnnotation + public Map mapNotGeneric; + + @SuppressWarnings("unchecked") + @Test + public void testListOfScalar() { + listOfScalarNotGeneric = new ArrayList(1); + listOfScalarNotGeneric.add("5"); + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expression = parser.parseExpression("listOfScalarNotGeneric[0]"); + assertThat(expression.getValue(this, Integer.class)).isEqualTo(new Integer(5)); + } + + public List listOfScalarNotGeneric; + + + @SuppressWarnings("unchecked") + @Test + public void testListsOfMap() { + listOfMapsNotGeneric = new ArrayList(); + Map map = new HashMap(); + map.put("fruit", "apple"); + listOfMapsNotGeneric.add(map); + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expression = parser.parseExpression("listOfMapsNotGeneric[0]['fruit']"); + assertThat(expression.getValue(this, String.class)).isEqualTo("apple"); + } + + public List listOfMapsNotGeneric; + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ListTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ListTests.java new file mode 100644 index 0000000..9250c1b --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ListTests.java @@ -0,0 +1,166 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import java.util.ArrayList; +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.spel.ast.InlineList; +import org.springframework.expression.spel.standard.SpelExpression; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Test usage of inline lists. + * + * @author Andy Clement + * @author Giovanni Dall'Oglio Risso + * @since 3.0.4 + */ +public class ListTests extends AbstractExpressionTests { + + // if the list is full of literals then it will be of the type unmodifiableClass + // rather than ArrayList + Class unmodifiableClass = Collections.unmodifiableList(new ArrayList<>()).getClass(); + + + @Test + public void testInlineListCreation01() { + evaluate("{1, 2, 3, 4, 5}", "[1, 2, 3, 4, 5]", unmodifiableClass); + } + + @Test + public void testInlineListCreation02() { + evaluate("{'abc', 'xyz'}", "[abc, xyz]", unmodifiableClass); + } + + @Test + public void testInlineListCreation03() { + evaluate("{}", "[]", unmodifiableClass); + } + + @Test + public void testInlineListCreation04() { + evaluate("{'abc'=='xyz'}", "[false]", ArrayList.class); + } + + @Test + public void testInlineListAndNesting() { + evaluate("{{1,2,3},{4,5,6}}", "[[1, 2, 3], [4, 5, 6]]", unmodifiableClass); + evaluate("{{1,'2',3},{4,{'a','b'},5,6}}", "[[1, 2, 3], [4, [a, b], 5, 6]]", unmodifiableClass); + } + + @Test + public void testInlineListError() { + parseAndCheckError("{'abc'", SpelMessage.OOD); + } + + @Test + public void testRelOperatorsIs02() { + evaluate("{1, 2, 3, 4, 5} instanceof T(java.util.List)", "true", Boolean.class); + } + + @Test + public void testInlineListCreation05() { + evaluate("3 between {1,5}", "true", Boolean.class); + } + + @Test + public void testInlineListCreation06() { + evaluate("8 between {1,5}", "false", Boolean.class); + } + + @Test + public void testInlineListAndProjectionSelection() { + evaluate("{1,2,3,4,5,6}.![#this>3]", "[false, false, false, true, true, true]", ArrayList.class); + evaluate("{1,2,3,4,5,6}.?[#this>3]", "[4, 5, 6]", ArrayList.class); + evaluate("{1,2,3,4,5,6,7,8,9,10}.?[#isEven(#this) == 'y']", "[2, 4, 6, 8, 10]", ArrayList.class); + } + + @Test + public void testSetConstruction01() { + evaluate("new java.util.HashSet().addAll({'a','b','c'})", "true", Boolean.class); + } + + @Test + public void testRelOperatorsBetween01() { + evaluate("32 between {32, 42}", "true", Boolean.class); + } + + @Test + public void testRelOperatorsBetween02() { + evaluate("'efg' between {'abc', 'xyz'}", "true", Boolean.class); + } + + @Test + public void testRelOperatorsBetween03() { + evaluate("42 between {32, 42}", "true", Boolean.class); + } + + @Test + public void testRelOperatorsBetween04() { + evaluate("new java.math.BigDecimal('1') between {new java.math.BigDecimal('1'),new java.math.BigDecimal('5')}", + "true", Boolean.class); + evaluate("new java.math.BigDecimal('3') between {new java.math.BigDecimal('1'),new java.math.BigDecimal('5')}", + "true", Boolean.class); + evaluate("new java.math.BigDecimal('5') between {new java.math.BigDecimal('1'),new java.math.BigDecimal('5')}", + "true", Boolean.class); + evaluate("new java.math.BigDecimal('8') between {new java.math.BigDecimal('1'),new java.math.BigDecimal('5')}", + "false", Boolean.class); + } + + @Test + public void testRelOperatorsBetweenErrors02() { + evaluateAndCheckError("'abc' between {5,7}", SpelMessage.NOT_COMPARABLE, 6); + } + + @Test + public void testConstantRepresentation1() { + checkConstantList("{1,2,3,4,5}", true); + checkConstantList("{'abc'}", true); + checkConstantList("{}", true); + checkConstantList("{#a,2,3}", false); + checkConstantList("{1,2,Integer.valueOf(4)}", false); + checkConstantList("{1,2,{#a}}", false); + } + + private void checkConstantList(String expressionText, boolean expectedToBeConstant) { + SpelExpressionParser parser = new SpelExpressionParser(); + SpelExpression expression = (SpelExpression) parser.parseExpression(expressionText); + SpelNode node = expression.getAST(); + boolean condition = node instanceof InlineList; + assertThat(condition).isTrue(); + InlineList inlineList = (InlineList) node; + if (expectedToBeConstant) { + assertThat(inlineList.isConstant()).isTrue(); + } + else { + assertThat(inlineList.isConstant()).isFalse(); + } + } + + @Test + public void testInlineListWriting() { + // list should be unmodifiable + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + evaluate("{1, 2, 3, 4, 5}[0]=6", "[1, 2, 3, 4, 5]", unmodifiableClass)); + } +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/LiteralExpressionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/LiteralExpressionTests.java new file mode 100644 index 0000000..4c59902 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/LiteralExpressionTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.common.LiteralExpression; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Andy Clement + */ +public class LiteralExpressionTests { + + @Test + public void testGetValue() throws Exception { + LiteralExpression lEx = new LiteralExpression("somevalue"); + assertThat(lEx.getValue()).isInstanceOf(String.class).isEqualTo("somevalue"); + assertThat(lEx.getValue(String.class)).isInstanceOf(String.class).isEqualTo("somevalue"); + EvaluationContext ctx = new StandardEvaluationContext(); + assertThat(lEx.getValue(ctx)).isInstanceOf(String.class).isEqualTo("somevalue"); + assertThat(lEx.getValue(ctx, String.class)).isInstanceOf(String.class).isEqualTo("somevalue"); + assertThat(lEx.getValue(new Rooty())).isInstanceOf(String.class).isEqualTo("somevalue"); + assertThat(lEx.getValue(new Rooty(), String.class)).isInstanceOf(String.class).isEqualTo("somevalue"); + assertThat(lEx.getValue(ctx, new Rooty())).isInstanceOf(String.class).isEqualTo("somevalue"); + assertThat(lEx.getValue(ctx, new Rooty(),String.class)).isInstanceOf(String.class).isEqualTo("somevalue"); + assertThat(lEx.getExpressionString()).isEqualTo("somevalue"); + assertThat(lEx.isWritable(new StandardEvaluationContext())).isFalse(); + assertThat(lEx.isWritable(new Rooty())).isFalse(); + assertThat(lEx.isWritable(new StandardEvaluationContext(), new Rooty())).isFalse(); + } + + static class Rooty {} + + @Test + public void testSetValue() { + assertThatExceptionOfType(EvaluationException.class).isThrownBy(() -> + new LiteralExpression("somevalue").setValue(new StandardEvaluationContext(), "flibble")) + .satisfies(ex -> assertThat(ex.getExpressionString()).isEqualTo("somevalue")); + assertThatExceptionOfType(EvaluationException.class).isThrownBy(() -> + new LiteralExpression("somevalue").setValue(new Rooty(), "flibble")) + .satisfies(ex -> assertThat(ex.getExpressionString()).isEqualTo("somevalue")); + assertThatExceptionOfType(EvaluationException.class).isThrownBy(() -> + new LiteralExpression("somevalue").setValue(new StandardEvaluationContext(), new Rooty(), "flibble")) + .satisfies(ex -> assertThat(ex.getExpressionString()).isEqualTo("somevalue")); + } + + @Test + public void testGetValueType() throws Exception { + LiteralExpression lEx = new LiteralExpression("somevalue"); + assertThat(lEx.getValueType()).isEqualTo(String.class); + assertThat(lEx.getValueType(new StandardEvaluationContext())).isEqualTo(String.class); + assertThat(lEx.getValueType(new Rooty())).isEqualTo(String.class); + assertThat(lEx.getValueType(new StandardEvaluationContext(), new Rooty())).isEqualTo(String.class); + assertThat(lEx.getValueTypeDescriptor().getType()).isEqualTo(String.class); + assertThat(lEx.getValueTypeDescriptor(new StandardEvaluationContext()).getType()).isEqualTo(String.class); + assertThat(lEx.getValueTypeDescriptor(new Rooty()).getType()).isEqualTo(String.class); + assertThat(lEx.getValueTypeDescriptor(new StandardEvaluationContext(), new Rooty()).getType()).isEqualTo(String.class); + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/LiteralTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/LiteralTests.java new file mode 100644 index 0000000..fec61a4 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/LiteralTests.java @@ -0,0 +1,174 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.spel.standard.SpelExpression; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests the evaluation of basic literals: boolean, integer, hex integer, long, real, null, date + * + * @author Andy Clement + */ +public class LiteralTests extends AbstractExpressionTests { + + @Test + public void testLiteralBoolean01() { + evaluate("false", "false", Boolean.class); + } + + @Test + public void testLiteralBoolean02() { + evaluate("true", "true", Boolean.class); + } + + @Test + public void testLiteralInteger01() { + evaluate("1", "1", Integer.class); + } + + @Test + public void testLiteralInteger02() { + evaluate("1415", "1415", Integer.class); + } + + @Test + public void testLiteralString01() { + evaluate("'Hello World'", "Hello World", String.class); + } + + @Test + public void testLiteralString02() { + evaluate("'joe bloggs'", "joe bloggs", String.class); + } + + @Test + public void testLiteralString03() { + evaluate("'hello'", "hello", String.class); + } + + @Test + public void testLiteralString04() { + evaluate("'Tony''s Pizza'", "Tony's Pizza", String.class); + evaluate("'Tony\\r''s Pizza'", "Tony\\r's Pizza", String.class); + } + + @Test + public void testLiteralString05() { + evaluate("\"Hello World\"", "Hello World", String.class); + } + + @Test + public void testLiteralString06() { + evaluate("\"Hello ' World\"", "Hello ' World", String.class); + } + + @Test + public void testHexIntLiteral01() { + evaluate("0x7FFFF", "524287", Integer.class); + evaluate("0x7FFFFL", 524287L, Long.class); + evaluate("0X7FFFF", "524287", Integer.class); + evaluate("0X7FFFFl", 524287L, Long.class); + } + + @Test + public void testLongIntLiteral01() { + evaluate("0xCAFEBABEL", 3405691582L, Long.class); + } + + @Test + public void testLongIntInteractions01() { + evaluate("0x20 * 2L", 64L, Long.class); + // ask for the result to be made into an Integer + evaluateAndAskForReturnType("0x20 * 2L", 64, Integer.class); + // ask for the result to be made into an Integer knowing that it will not fit + evaluateAndCheckError("0x1220 * 0xffffffffL", Integer.class, SpelMessage.TYPE_CONVERSION_ERROR, 0); + } + + @Test + public void testSignedIntLiterals() { + evaluate("-1", -1, Integer.class); + evaluate("-0xa", -10, Integer.class); + evaluate("-1L", -1L, Long.class); + evaluate("-0x20l", -32L, Long.class); + } + + @Test + public void testLiteralReal01_CreatingDoubles() { + evaluate("1.25", 1.25d, Double.class); + evaluate("2.99", 2.99d, Double.class); + evaluate("-3.141", -3.141d, Double.class); + evaluate("1.25d", 1.25d, Double.class); + evaluate("2.99d", 2.99d, Double.class); + evaluate("-3.141d", -3.141d, Double.class); + evaluate("1.25D", 1.25d, Double.class); + evaluate("2.99D", 2.99d, Double.class); + evaluate("-3.141D", -3.141d, Double.class); + } + + @Test + public void testLiteralReal02_CreatingFloats() { + // For now, everything becomes a double... + evaluate("1.25f", 1.25f, Float.class); + evaluate("2.5f", 2.5f, Float.class); + evaluate("-3.5f", -3.5f, Float.class); + evaluate("1.25F", 1.25f, Float.class); + evaluate("2.5F", 2.5f, Float.class); + evaluate("-3.5F", -3.5f, Float.class); + } + + @Test + public void testLiteralReal03_UsingExponents() { + evaluate("6.0221415E+23", "6.0221415E23", Double.class); + evaluate("6.0221415e+23", "6.0221415E23", Double.class); + evaluate("6.0221415E+23d", "6.0221415E23", Double.class); + evaluate("6.0221415e+23D", "6.0221415E23", Double.class); + evaluate("6E2f", 6E2f, Float.class); + } + + @Test + public void testLiteralReal04_BadExpressions() { + parseAndCheckError("6.1e23e22", SpelMessage.MORE_INPUT, 6, "e22"); + parseAndCheckError("6.1f23e22", SpelMessage.MORE_INPUT, 4, "23e22"); + } + + @Test + public void testLiteralNull01() { + evaluate("null", null, null); + } + + @Test + public void testConversions() { + // getting the expression type to be what we want - either: + evaluate("new Integer(37).byteValue()", (byte) 37, Byte.class); // calling byteValue() on Integer.class + evaluateAndAskForReturnType("new Integer(37)", (byte) 37, Byte.class); // relying on registered type converters + } + + @Test + public void testNotWritable() throws Exception { + SpelExpression expr = (SpelExpression)parser.parseExpression("37"); + assertThat(expr.isWritable(new StandardEvaluationContext())).isFalse(); + expr = (SpelExpression)parser.parseExpression("37L"); + assertThat(expr.isWritable(new StandardEvaluationContext())).isFalse(); + expr = (SpelExpression)parser.parseExpression("true"); + assertThat(expr.isWritable(new StandardEvaluationContext())).isFalse(); + } +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/MapAccessTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/MapAccessTests.java new file mode 100644 index 0000000..832a636 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/MapAccessTests.java @@ -0,0 +1,186 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Testing variations on map access. + * + * @author Andy Clement + */ +public class MapAccessTests extends AbstractExpressionTests { + + @Test + public void testSimpleMapAccess01() { + evaluate("testMap.get('monday')", "montag", String.class); + } + + @Test + public void testMapAccessThroughIndexer() { + evaluate("testMap['monday']", "montag", String.class); + } + + @Test + public void testCustomMapAccessor() throws Exception { + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext ctx = TestScenarioCreator.getTestEvaluationContext(); + ctx.addPropertyAccessor(new MapAccessor()); + + Expression expr = parser.parseExpression("testMap.monday"); + Object value = expr.getValue(ctx, String.class); + assertThat(value).isEqualTo("montag"); + } + + @Test + public void testVariableMapAccess() throws Exception { + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext ctx = TestScenarioCreator.getTestEvaluationContext(); + ctx.setVariable("day", "saturday"); + + Expression expr = parser.parseExpression("testMap[#day]"); + Object value = expr.getValue(ctx, String.class); + assertThat(value).isEqualTo("samstag"); + } + + @Test + public void testGetValue() { + Map props1 = new HashMap<>(); + props1.put("key1", "value1"); + props1.put("key2", "value2"); + props1.put("key3", "value3"); + + Object bean = new TestBean("name1", new TestBean("name2", null, "Description 2", 15, props1), "description 1", 6, props1); + + ExpressionParser parser = new SpelExpressionParser(); + Expression expr = parser.parseExpression("testBean.properties['key2']"); + assertThat(expr.getValue(bean)).isEqualTo("value2"); + } + + @Test + public void testGetValueFromRootMap() { + Map map = new HashMap<>(); + map.put("key", "value"); + + ExpressionParser spelExpressionParser = new SpelExpressionParser(); + Expression expr = spelExpressionParser.parseExpression("#root['key']"); + assertThat(expr.getValue(map)).isEqualTo("value"); + } + + + public static class TestBean { + + private String name; + private TestBean testBean; + private String description; + private Integer priority; + private Map properties; + + public TestBean(String name, TestBean testBean, String description, Integer priority, Map props) { + this.name = name; + this.testBean = testBean; + this.description = description; + this.priority = priority; + this.properties = props; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public TestBean getTestBean() { + return testBean; + } + + public void setTestBean(TestBean testBean) { + this.testBean = testBean; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Integer getPriority() { + return priority; + } + + public void setPriority(Integer priority) { + this.priority = priority; + } + + public Map getProperties() { + return properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } + } + + + public static class MapAccessor implements PropertyAccessor { + + @Override + public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException { + return (((Map) target).containsKey(name)); + } + + @Override + public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException { + return new TypedValue(((Map) target).get(name)); + } + + @Override + public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException { + return true; + } + + @Override + @SuppressWarnings("unchecked") + public void write(EvaluationContext context, Object target, String name, Object newValue) throws AccessException { + ((Map) target).put(name, newValue); + } + + @Override + public Class[] getSpecificTargetClasses() { + return new Class[] {Map.class}; + } + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/MapTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/MapTests.java new file mode 100644 index 0000000..759a15f --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/MapTests.java @@ -0,0 +1,205 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.spel.ast.InlineMap; +import org.springframework.expression.spel.standard.SpelExpression; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Test usage of inline maps. + * + * @author Andy Clement + * @since 4.1 + */ +public class MapTests extends AbstractExpressionTests { + + // if the list is full of literals then it will be of the type unmodifiableClass + // rather than HashMap (or similar) + Class unmodifiableClass = Collections.unmodifiableMap(new LinkedHashMap<>()).getClass(); + + + @Test + public void testInlineMapCreation01() { + evaluate("{'a':1, 'b':2, 'c':3, 'd':4, 'e':5}", "{a=1, b=2, c=3, d=4, e=5}", unmodifiableClass); + evaluate("{'a':1}", "{a=1}", unmodifiableClass); + } + + @Test + public void testInlineMapCreation02() { + evaluate("{'abc':'def', 'uvw':'xyz'}", "{abc=def, uvw=xyz}", unmodifiableClass); + } + + @Test + public void testInlineMapCreation03() { + evaluate("{:}", "{}", unmodifiableClass); + } + + @Test + public void testInlineMapCreation04() { + evaluate("{'key':'abc'=='xyz'}", "{key=false}", LinkedHashMap.class); + evaluate("{key:'abc'=='xyz'}", "{key=false}", LinkedHashMap.class); + evaluate("{key:'abc'=='xyz',key2:true}[key]", "false", Boolean.class); + evaluate("{key:'abc'=='xyz',key2:true}.get('key2')", "true", Boolean.class); + evaluate("{key:'abc'=='xyz',key2:true}['key2']", "true", Boolean.class); + } + + @Test + public void testInlineMapAndNesting() { + evaluate("{a:{a:1,b:2,c:3},b:{d:4,e:5,f:6}}", "{a={a=1, b=2, c=3}, b={d=4, e=5, f=6}}", unmodifiableClass); + evaluate("{a:{x:1,y:'2',z:3},b:{u:4,v:{'a','b'},w:5,x:6}}", "{a={x=1, y=2, z=3}, b={u=4, v=[a, b], w=5, x=6}}", unmodifiableClass); + evaluate("{a:{1,2,3},b:{4,5,6}}", "{a=[1, 2, 3], b=[4, 5, 6]}", unmodifiableClass); + } + + @Test + public void testInlineMapWithFunkyKeys() { + evaluate("{#root.name:true}","{Nikola Tesla=true}",LinkedHashMap.class); + } + + @Test + public void testInlineMapError() { + parseAndCheckError("{key:'abc'", SpelMessage.OOD); + } + + @Test + public void testRelOperatorsIs02() { + evaluate("{a:1, b:2, c:3, d:4, e:5} instanceof T(java.util.Map)", "true", Boolean.class); + } + + @Test + public void testInlineMapAndProjectionSelection() { + evaluate("{a:1,b:2,c:3,d:4,e:5,f:6}.![value>3]", "[false, false, false, true, true, true]", ArrayList.class); + evaluate("{a:1,b:2,c:3,d:4,e:5,f:6}.?[value>3]", "{d=4, e=5, f=6}", HashMap.class); + evaluate("{a:1,b:2,c:3,d:4,e:5,f:6,g:7,h:8,i:9,j:10}.?[value%2==0]", "{b=2, d=4, f=6, h=8, j=10}", HashMap.class); + // TODO this looks like a serious issue (but not a new one): the context object against which arguments are evaluated seems wrong: +// evaluate("{a:1,b:2,c:3,d:4,e:5,f:6,g:7,h:8,i:9,j:10}.?[isEven(value) == 'y']", "[2, 4, 6, 8, 10]", ArrayList.class); + } + + @Test + public void testSetConstruction01() { + evaluate("new java.util.HashMap().putAll({a:'a',b:'b',c:'c'})", null, Object.class); + } + + @Test + public void testConstantRepresentation1() { + checkConstantMap("{f:{'a','b','c'}}", true); + checkConstantMap("{'a':1,'b':2,'c':3,'d':4,'e':5}", true); + checkConstantMap("{aaa:'abc'}", true); + checkConstantMap("{:}", true); + checkConstantMap("{a:#a,b:2,c:3}", false); + checkConstantMap("{a:1,b:2,c:Integer.valueOf(4)}", false); + checkConstantMap("{a:1,b:2,c:{#a}}", false); + checkConstantMap("{#root.name:true}",false); + checkConstantMap("{a:1,b:2,c:{d:true,e:false}}", true); + checkConstantMap("{a:1,b:2,c:{d:{1,2,3},e:{4,5,6},f:{'a','b','c'}}}", true); + } + + private void checkConstantMap(String expressionText, boolean expectedToBeConstant) { + SpelExpressionParser parser = new SpelExpressionParser(); + SpelExpression expression = (SpelExpression) parser.parseExpression(expressionText); + SpelNode node = expression.getAST(); + boolean condition = node instanceof InlineMap; + assertThat(condition).isTrue(); + InlineMap inlineMap = (InlineMap) node; + if (expectedToBeConstant) { + assertThat(inlineMap.isConstant()).isTrue(); + } + else { + assertThat(inlineMap.isConstant()).isFalse(); + } + } + + @Test + public void testInlineMapWriting() { + // list should be unmodifiable + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + evaluate("{a:1, b:2, c:3, d:4, e:5}[a]=6", "[a:1,b: 2,c: 3,d: 4,e: 5]", unmodifiableClass)); + } + + @Test + public void testMapKeysThatAreAlsoSpELKeywords() { + SpelExpressionParser parser = new SpelExpressionParser(); + SpelExpression expression = null; + Object o = null; + + // expression = (SpelExpression) parser.parseExpression("foo['NEW']"); + // o = expression.getValue(new MapHolder()); + // assertEquals("VALUE",o); + + expression = (SpelExpression) parser.parseExpression("foo[T]"); + o = expression.getValue(new MapHolder()); + assertThat(o).isEqualTo("TV"); + + expression = (SpelExpression) parser.parseExpression("foo[t]"); + o = expression.getValue(new MapHolder()); + assertThat(o).isEqualTo("tv"); + + expression = (SpelExpression) parser.parseExpression("foo[NEW]"); + o = expression.getValue(new MapHolder()); + assertThat(o).isEqualTo("VALUE"); + + expression = (SpelExpression) parser.parseExpression("foo[new]"); + o = expression.getValue(new MapHolder()); + assertThat(o).isEqualTo("value"); + + expression = (SpelExpression) parser.parseExpression("foo['abc.def']"); + o = expression.getValue(new MapHolder()); + assertThat(o).isEqualTo("value"); + + expression = (SpelExpression)parser.parseExpression("foo[foo[NEW]]"); + o = expression.getValue(new MapHolder()); + assertThat(o).isEqualTo("37"); + + expression = (SpelExpression)parser.parseExpression("foo[foo[new]]"); + o = expression.getValue(new MapHolder()); + assertThat(o).isEqualTo("38"); + + expression = (SpelExpression)parser.parseExpression("foo[foo[foo[T]]]"); + o = expression.getValue(new MapHolder()); + assertThat(o).isEqualTo("value"); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static class MapHolder { + + public Map foo; + + public MapHolder() { + foo = new HashMap(); + foo.put("NEW", "VALUE"); + foo.put("new", "value"); + foo.put("T", "TV"); + foo.put("t", "tv"); + foo.put("abc.def", "value"); + foo.put("VALUE","37"); + foo.put("value","38"); + foo.put("TV","new"); + } + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/MethodInvocationTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/MethodInvocationTests.java new file mode 100644 index 0000000..0a025ac --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/MethodInvocationTests.java @@ -0,0 +1,355 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionInvocationTargetException; +import org.springframework.expression.MethodExecutor; +import org.springframework.expression.MethodFilter; +import org.springframework.expression.MethodResolver; +import org.springframework.expression.spel.standard.SpelExpression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.expression.spel.testresources.PlaceOfBirth; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests invocation of methods. + * + * @author Andy Clement + * @author Phillip Webb + */ +public class MethodInvocationTests extends AbstractExpressionTests { + + @Test + public void testSimpleAccess01() { + evaluate("getPlaceOfBirth().getCity()", "SmilJan", String.class); + } + + @Test + public void testStringClass() { + evaluate("new java.lang.String('hello').charAt(2)", 'l', Character.class); + evaluate("new java.lang.String('hello').charAt(2).equals('l'.charAt(0))", true, Boolean.class); + evaluate("'HELLO'.toLowerCase()", "hello", String.class); + evaluate("' abcba '.trim()", "abcba", String.class); + } + + @Test + public void testNonExistentMethods() { + // name is ok but madeup() does not exist + evaluateAndCheckError("name.madeup()", SpelMessage.METHOD_NOT_FOUND, 5); + } + + @Test + public void testWidening01() { + // widening of int 3 to double 3 is OK + evaluate("new Double(3.0d).compareTo(8)", -1, Integer.class); + evaluate("new Double(3.0d).compareTo(3)", 0, Integer.class); + evaluate("new Double(3.0d).compareTo(2)", 1, Integer.class); + } + + @Test + public void testArgumentConversion01() { + // Rely on Double>String conversion for calling startsWith() + evaluate("new String('hello 2.0 to you').startsWith(7.0d)", false, Boolean.class); + evaluate("new String('7.0 foobar').startsWith(7.0d)", true, Boolean.class); + } + + @Test + public void testMethodThrowingException_SPR6760() { + // Test method on inventor: throwException() + // On 1 it will throw an IllegalArgumentException + // On 2 it will throw a RuntimeException + // On 3 it will exit normally + // In each case it increments the Inventor field 'counter' when invoked + + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expr = parser.parseExpression("throwException(#bar)"); + + // Normal exit + StandardEvaluationContext eContext = TestScenarioCreator.getTestEvaluationContext(); + eContext.setVariable("bar", 3); + Object o = expr.getValue(eContext); + assertThat(o).isEqualTo(3); + assertThat(parser.parseExpression("counter").getValue(eContext)).isEqualTo(1); + + // Now the expression has cached that throwException(int) is the right thing to call + // Let's change 'bar' to be a PlaceOfBirth which indicates the cached reference is + // out of date. + eContext.setVariable("bar", new PlaceOfBirth("London")); + o = expr.getValue(eContext); + assertThat(o).isEqualTo("London"); + // That confirms the logic to mark the cached reference stale and retry is working + + // Now let's cause the method to exit via exception and ensure it doesn't cause a retry. + + // First, switch back to throwException(int) + eContext.setVariable("bar", 3); + o = expr.getValue(eContext); + assertThat(o).isEqualTo(3); + assertThat(parser.parseExpression("counter").getValue(eContext)).isEqualTo(2); + + + // Now cause it to throw an exception: + eContext.setVariable("bar", 1); + assertThatExceptionOfType(Exception.class).isThrownBy(() -> expr.getValue(eContext)) + .isNotInstanceOf(SpelEvaluationException.class); + + // If counter is 4 then the method got called twice! + assertThat(parser.parseExpression("counter").getValue(eContext)).isEqualTo(3); + + eContext.setVariable("bar", 4); + assertThatExceptionOfType(ExpressionInvocationTargetException.class).isThrownBy(() -> expr.getValue(eContext)); + + // If counter is 5 then the method got called twice! + assertThat(parser.parseExpression("counter").getValue(eContext)).isEqualTo(4); + } + + /** + * Check on first usage (when the cachedExecutor in MethodReference is null) that the exception is not wrapped. + */ + @Test + public void testMethodThrowingException_SPR6941() { + // Test method on inventor: throwException() + // On 1 it will throw an IllegalArgumentException + // On 2 it will throw a RuntimeException + // On 3 it will exit normally + // In each case it increments the Inventor field 'counter' when invoked + + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expr = parser.parseExpression("throwException(#bar)"); + + context.setVariable("bar", 2); + assertThatExceptionOfType(Exception.class) + .isThrownBy(() -> expr.getValue(context)) + .isNotInstanceOf(SpelEvaluationException.class); + } + + @Test + public void testMethodThrowingException_SPR6941_2() { + // Test method on inventor: throwException() + // On 1 it will throw an IllegalArgumentException + // On 2 it will throw a RuntimeException + // On 3 it will exit normally + // In each case it increments the Inventor field 'counter' when invoked + + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expr = parser.parseExpression("throwException(#bar)"); + + context.setVariable("bar", 4); + assertThatExceptionOfType(ExpressionInvocationTargetException.class).isThrownBy(() -> expr.getValue(context)) + .satisfies(ex -> assertThat(ex.getCause().getClass().getName()).isEqualTo( + "org.springframework.expression.spel.testresources.Inventor$TestException")); + } + + @Test + public void testMethodFiltering_SPR6764() { + SpelExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setRootObject(new TestObject()); + LocalFilter filter = new LocalFilter(); + context.registerMethodFilter(TestObject.class,filter); + + // Filter will be called but not do anything, so first doit() will be invoked + SpelExpression expr = (SpelExpression) parser.parseExpression("doit(1)"); + String result = expr.getValue(context, String.class); + assertThat(result).isEqualTo("1"); + assertThat(filter.filterCalled).isTrue(); + + // Filter will now remove non @Anno annotated methods + filter.removeIfNotAnnotated = true; + filter.filterCalled = false; + expr = (SpelExpression) parser.parseExpression("doit(1)"); + result = expr.getValue(context, String.class); + assertThat(result).isEqualTo("double 1.0"); + assertThat(filter.filterCalled).isTrue(); + + // check not called for other types + filter.filterCalled = false; + context.setRootObject(new String("abc")); + expr = (SpelExpression) parser.parseExpression("charAt(0)"); + result = expr.getValue(context, String.class); + assertThat(result).isEqualTo("a"); + assertThat(filter.filterCalled).isFalse(); + + // check de-registration works + filter.filterCalled = false; + context.registerMethodFilter(TestObject.class,null);//clear filter + context.setRootObject(new TestObject()); + expr = (SpelExpression) parser.parseExpression("doit(1)"); + result = expr.getValue(context, String.class); + assertThat(result).isEqualTo("1"); + assertThat(filter.filterCalled).isFalse(); + } + + @Test + public void testAddingMethodResolvers() { + StandardEvaluationContext ctx = new StandardEvaluationContext(); + + // reflective method accessor is the only one by default + List methodResolvers = ctx.getMethodResolvers(); + assertThat(methodResolvers.size()).isEqualTo(1); + + MethodResolver dummy = new DummyMethodResolver(); + ctx.addMethodResolver(dummy); + assertThat(ctx.getMethodResolvers().size()).isEqualTo(2); + + List copy = new ArrayList<>(ctx.getMethodResolvers()); + assertThat(ctx.removeMethodResolver(dummy)).isTrue(); + assertThat(ctx.removeMethodResolver(dummy)).isFalse(); + assertThat(ctx.getMethodResolvers().size()).isEqualTo(1); + + ctx.setMethodResolvers(copy); + assertThat(ctx.getMethodResolvers().size()).isEqualTo(2); + } + + @Test + public void testVarargsInvocation01() { + // Calling 'public int aVarargsMethod(String... strings)' + //evaluate("aVarargsMethod('a','b','c')", 3, Integer.class); + //evaluate("aVarargsMethod('a')", 1, Integer.class); + evaluate("aVarargsMethod()", 0, Integer.class); + evaluate("aVarargsMethod(1,2,3)", 3, Integer.class); // all need converting to strings + evaluate("aVarargsMethod(1)", 1, Integer.class); // needs string conversion + evaluate("aVarargsMethod(1,'a',3.0d)", 3, Integer.class); // first and last need conversion + // evaluate("aVarargsMethod(new String[]{'a','b','c'})", 3, Integer.class); + } + + @Test + public void testVarargsInvocation02() { + // Calling 'public int aVarargsMethod2(int i, String... strings)' - returns int+length_of_strings + evaluate("aVarargsMethod2(5,'a','b','c')", 8, Integer.class); + evaluate("aVarargsMethod2(2,'a')", 3, Integer.class); + evaluate("aVarargsMethod2(4)", 4, Integer.class); + evaluate("aVarargsMethod2(8,2,3)", 10, Integer.class); + evaluate("aVarargsMethod2(9)", 9, Integer.class); + evaluate("aVarargsMethod2(2,'a',3.0d)", 4, Integer.class); + // evaluate("aVarargsMethod2(8,new String[]{'a','b','c'})", 11, Integer.class); + } + + @Test + public void testInvocationOnNullContextObject() { + evaluateAndCheckError("null.toString()",SpelMessage.METHOD_CALL_ON_NULL_OBJECT_NOT_ALLOWED); + } + + @Test + public void testMethodOfClass() throws Exception { + Expression expression = parser.parseExpression("getName()"); + Object value = expression.getValue(new StandardEvaluationContext(String.class)); + assertThat(value).isEqualTo("java.lang.String"); + } + + @Test + public void invokeMethodWithoutConversion() throws Exception { + final BytesService service = new BytesService(); + byte[] bytes = new byte[100]; + StandardEvaluationContext context = new StandardEvaluationContext(bytes); + context.setBeanResolver((context1, beanName) -> ("service".equals(beanName) ? service : null)); + Expression expression = parser.parseExpression("@service.handleBytes(#root)"); + byte[] outBytes = expression.getValue(context, byte[].class); + assertThat(outBytes).isSameAs(bytes); + } + + + // Simple filter + static class LocalFilter implements MethodFilter { + + public boolean removeIfNotAnnotated = false; + + public boolean filterCalled = false; + + private boolean isAnnotated(Method method) { + Annotation[] anns = method.getAnnotations(); + if (anns == null) { + return false; + } + for (Annotation ann : anns) { + String name = ann.annotationType().getName(); + if (name.endsWith("Anno")) { + return true; + } + } + return false; + } + + @Override + public List filter(List methods) { + filterCalled = true; + List forRemoval = new ArrayList<>(); + for (Method method: methods) { + if (removeIfNotAnnotated && !isAnnotated(method)) { + forRemoval.add(method); + } + } + for (Method method: forRemoval) { + methods.remove(method); + } + return methods; + } + } + + + @Retention(RetentionPolicy.RUNTIME) + @interface Anno { + } + + + class TestObject { + + public int doit(int i) { + return i; + } + + @Anno + public String doit(double d) { + return "double "+d; + } + } + + + static class DummyMethodResolver implements MethodResolver { + + @Override + public MethodExecutor resolve(EvaluationContext context, Object targetObject, String name, + List argumentTypes) throws AccessException { + throw new UnsupportedOperationException(); + } + } + + + public static class BytesService { + + public byte[] handleBytes(byte[] bytes) { + return bytes; + } + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/OperatorOverloaderTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/OperatorOverloaderTests.java new file mode 100644 index 0000000..d949e07 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/OperatorOverloaderTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Operation; +import org.springframework.expression.OperatorOverloader; +import org.springframework.expression.spel.standard.SpelExpression; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test providing operator support + * + * @author Andy Clement + */ +public class OperatorOverloaderTests extends AbstractExpressionTests { + + @Test + public void testSimpleOperations() throws Exception { + // no built in support for this: + evaluateAndCheckError("'abc'-true",SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES); + + StandardEvaluationContext eContext = TestScenarioCreator.getTestEvaluationContext(); + eContext.setOperatorOverloader(new StringAndBooleanAddition()); + + SpelExpression expr = (SpelExpression)parser.parseExpression("'abc'+true"); + assertThat(expr.getValue(eContext)).isEqualTo("abctrue"); + + expr = (SpelExpression)parser.parseExpression("'abc'-true"); + assertThat(expr.getValue(eContext)).isEqualTo("abc"); + + expr = (SpelExpression)parser.parseExpression("'abc'+null"); + assertThat(expr.getValue(eContext)).isEqualTo("abcnull"); + } + + + static class StringAndBooleanAddition implements OperatorOverloader { + + @Override + public Object operate(Operation operation, Object leftOperand, Object rightOperand) throws EvaluationException { + if (operation==Operation.ADD) { + return ((String)leftOperand)+((Boolean)rightOperand).toString(); + } + else { + return leftOperand; + } + } + + @Override + public boolean overridesOperation(Operation operation, Object leftOperand, Object rightOperand) throws EvaluationException { + if (leftOperand instanceof String && rightOperand instanceof Boolean) { + return true; + } + return false; + + } + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/OperatorTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/OperatorTests.java new file mode 100644 index 0000000..92fef3b --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/OperatorTests.java @@ -0,0 +1,638 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.spel.ast.Operator; +import org.springframework.expression.spel.standard.SpelExpression; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests the evaluation of expressions using relational operators. + * + * @author Andy Clement + * @author Juergen Hoeller + * @author Giovanni Dall'Oglio Risso + */ +public class OperatorTests extends AbstractExpressionTests { + + @Test + public void testEqual() { + evaluate("3 == 5", false, Boolean.class); + evaluate("5 == 3", false, Boolean.class); + evaluate("6 == 6", true, Boolean.class); + evaluate("3.0f == 5.0f", false, Boolean.class); + evaluate("3.0f == 3.0f", true, Boolean.class); + evaluate("new java.math.BigDecimal('5') == new java.math.BigDecimal('5')", true, Boolean.class); + evaluate("new java.math.BigDecimal('3') == new java.math.BigDecimal('5')", false, Boolean.class); + evaluate("new java.math.BigDecimal('5') == new java.math.BigDecimal('3')", false, Boolean.class); + evaluate("3 == new java.math.BigDecimal('5')", false, Boolean.class); + evaluate("new java.math.BigDecimal('3') == 5", false, Boolean.class); + evaluate("3L == new java.math.BigDecimal('5')", false, Boolean.class); + evaluate("3.0d == new java.math.BigDecimal('5')", false, Boolean.class); + evaluate("3L == new java.math.BigDecimal('3.1')", false, Boolean.class); + evaluate("3.0d == new java.math.BigDecimal('3.1')", false, Boolean.class); + evaluate("3.0d == new java.math.BigDecimal('3.0')", true, Boolean.class); + evaluate("3.0f == 3.0d", true, Boolean.class); + evaluate("10 == '10'", false, Boolean.class); + evaluate("'abc' == 'abc'", true, Boolean.class); + evaluate("'abc' == new java.lang.StringBuilder('abc')", true, Boolean.class); + evaluate("'abc' == 'def'", false, Boolean.class); + evaluate("'abc' == null", false, Boolean.class); + evaluate("new org.springframework.expression.spel.OperatorTests$SubComparable() == new org.springframework.expression.spel.OperatorTests$OtherSubComparable()", true, Boolean.class); + + evaluate("3 eq 5", false, Boolean.class); + evaluate("5 eQ 3", false, Boolean.class); + evaluate("6 Eq 6", true, Boolean.class); + evaluate("3.0f eq 5.0f", false, Boolean.class); + evaluate("3.0f EQ 3.0f", true, Boolean.class); + evaluate("new java.math.BigDecimal('5') eq new java.math.BigDecimal('5')", true, Boolean.class); + evaluate("new java.math.BigDecimal('3') eq new java.math.BigDecimal('5')", false, Boolean.class); + evaluate("new java.math.BigDecimal('5') eq new java.math.BigDecimal('3')", false, Boolean.class); + evaluate("3 eq new java.math.BigDecimal('5')", false, Boolean.class); + evaluate("new java.math.BigDecimal('3') eq 5", false, Boolean.class); + evaluate("3L eq new java.math.BigDecimal('5')", false, Boolean.class); + evaluate("3.0d eq new java.math.BigDecimal('5')", false, Boolean.class); + evaluate("3L eq new java.math.BigDecimal('3.1')", false, Boolean.class); + evaluate("3.0d eq new java.math.BigDecimal('3.1')", false, Boolean.class); + evaluate("3.0d eq new java.math.BigDecimal('3.0')", true, Boolean.class); + evaluate("3.0f eq 3.0d", true, Boolean.class); + evaluate("10 eq '10'", false, Boolean.class); + evaluate("'abc' eq 'abc'", true, Boolean.class); + evaluate("'abc' eq new java.lang.StringBuilder('abc')", true, Boolean.class); + evaluate("'abc' eq 'def'", false, Boolean.class); + evaluate("'abc' eq null", false, Boolean.class); + evaluate("new org.springframework.expression.spel.OperatorTests$SubComparable() eq new org.springframework.expression.spel.OperatorTests$OtherSubComparable()", true, Boolean.class); + } + + @Test + public void testNotEqual() { + evaluate("3 != 5", true, Boolean.class); + evaluate("5 != 3", true, Boolean.class); + evaluate("6 != 6", false, Boolean.class); + evaluate("3.0f != 5.0f", true, Boolean.class); + evaluate("3.0f != 3.0f", false, Boolean.class); + evaluate("new java.math.BigDecimal('5') != new java.math.BigDecimal('5')", false, Boolean.class); + evaluate("new java.math.BigDecimal('3') != new java.math.BigDecimal('5')", true, Boolean.class); + evaluate("new java.math.BigDecimal('5') != new java.math.BigDecimal('3')", true, Boolean.class); + evaluate("3 != new java.math.BigDecimal('5')", true, Boolean.class); + evaluate("new java.math.BigDecimal('3') != 5", true, Boolean.class); + evaluate("3L != new java.math.BigDecimal('5')", true, Boolean.class); + evaluate("3.0d != new java.math.BigDecimal('5')", true, Boolean.class); + evaluate("3L != new java.math.BigDecimal('3.1')", true, Boolean.class); + evaluate("3.0d != new java.math.BigDecimal('3.1')", true, Boolean.class); + evaluate("3.0d != new java.math.BigDecimal('3.0')", false, Boolean.class); + evaluate("3.0f != 3.0d", false, Boolean.class); + evaluate("10 != '10'", true, Boolean.class); + evaluate("'abc' != 'abc'", false, Boolean.class); + evaluate("'abc' != new java.lang.StringBuilder('abc')", false, Boolean.class); + evaluate("'abc' != 'def'", true, Boolean.class); + evaluate("'abc' != null", true, Boolean.class); + evaluate("new org.springframework.expression.spel.OperatorTests$SubComparable() != new org.springframework.expression.spel.OperatorTests$OtherSubComparable()", false, Boolean.class); + + evaluate("3 ne 5", true, Boolean.class); + evaluate("5 nE 3", true, Boolean.class); + evaluate("6 Ne 6", false, Boolean.class); + evaluate("3.0f NE 5.0f", true, Boolean.class); + evaluate("3.0f ne 3.0f", false, Boolean.class); + evaluate("new java.math.BigDecimal('5') ne new java.math.BigDecimal('5')", false, Boolean.class); + evaluate("new java.math.BigDecimal('3') ne new java.math.BigDecimal('5')", true, Boolean.class); + evaluate("new java.math.BigDecimal('5') ne new java.math.BigDecimal('3')", true, Boolean.class); + evaluate("3 ne new java.math.BigDecimal('5')", true, Boolean.class); + evaluate("new java.math.BigDecimal('3') ne 5", true, Boolean.class); + evaluate("3L ne new java.math.BigDecimal('5')", true, Boolean.class); + evaluate("3.0d ne new java.math.BigDecimal('5')", true, Boolean.class); + evaluate("3L ne new java.math.BigDecimal('3.1')", true, Boolean.class); + evaluate("3.0d ne new java.math.BigDecimal('3.1')", true, Boolean.class); + evaluate("3.0d ne new java.math.BigDecimal('3.0')", false, Boolean.class); + evaluate("3.0f ne 3.0d", false, Boolean.class); + evaluate("10 ne '10'", true, Boolean.class); + evaluate("'abc' ne 'abc'", false, Boolean.class); + evaluate("'abc' ne new java.lang.StringBuilder('abc')", false, Boolean.class); + evaluate("'abc' ne 'def'", true, Boolean.class); + evaluate("'abc' ne null", true, Boolean.class); + evaluate("new org.springframework.expression.spel.OperatorTests$SubComparable() ne new org.springframework.expression.spel.OperatorTests$OtherSubComparable()", false, Boolean.class); + } + + @Test + public void testLessThan() { + evaluate("5 < 5", false, Boolean.class); + evaluate("3 < 5", true, Boolean.class); + evaluate("5 < 3", false, Boolean.class); + evaluate("3L < 5L", true, Boolean.class); + evaluate("5L < 3L", false, Boolean.class); + evaluate("3.0d < 5.0d", true, Boolean.class); + evaluate("5.0d < 3.0d", false, Boolean.class); + evaluate("new java.math.BigDecimal('3') < new java.math.BigDecimal('5')", true, Boolean.class); + evaluate("new java.math.BigDecimal('5') < new java.math.BigDecimal('3')", false, Boolean.class); + evaluate("3 < new java.math.BigDecimal('5')", true, Boolean.class); + evaluate("new java.math.BigDecimal('3') < 5", true, Boolean.class); + evaluate("3L < new java.math.BigDecimal('5')", true, Boolean.class); + evaluate("3.0d < new java.math.BigDecimal('5')", true, Boolean.class); + evaluate("3L < new java.math.BigDecimal('3.1')", true, Boolean.class); + evaluate("3.0d < new java.math.BigDecimal('3.1')", true, Boolean.class); + evaluate("3.0d < new java.math.BigDecimal('3.0')", false, Boolean.class); + evaluate("'abc' < 'def'", true, Boolean.class); + evaluate("'abc' < new java.lang.StringBuilder('def')", true, Boolean.class); + evaluate("'def' < 'abc'", false, Boolean.class); + + evaluate("3 lt 5", true, Boolean.class); + evaluate("5 lt 3", false, Boolean.class); + evaluate("3L lt 5L", true, Boolean.class); + evaluate("5L lt 3L", false, Boolean.class); + evaluate("3.0d lT 5.0d", true, Boolean.class); + evaluate("5.0d Lt 3.0d", false, Boolean.class); + evaluate("new java.math.BigDecimal('3') lt new java.math.BigDecimal('5')", true, Boolean.class); + evaluate("new java.math.BigDecimal('5') lt new java.math.BigDecimal('3')", false, Boolean.class); + evaluate("3 lt new java.math.BigDecimal('5')", true, Boolean.class); + evaluate("new java.math.BigDecimal('3') lt 5", true, Boolean.class); + evaluate("3L lt new java.math.BigDecimal('5')", true, Boolean.class); + evaluate("3.0d lt new java.math.BigDecimal('5')", true, Boolean.class); + evaluate("3L lt new java.math.BigDecimal('3.1')", true, Boolean.class); + evaluate("3.0d lt new java.math.BigDecimal('3.1')", true, Boolean.class); + evaluate("3.0d lt new java.math.BigDecimal('3.0')", false, Boolean.class); + evaluate("'abc' LT 'def'", true, Boolean.class); + evaluate("'abc' lt new java.lang.StringBuilder('def')", true, Boolean.class); + evaluate("'def' lt 'abc'", false, Boolean.class); + } + + @Test + public void testLessThanOrEqual() { + evaluate("3 <= 5", true, Boolean.class); + evaluate("5 <= 3", false, Boolean.class); + evaluate("6 <= 6", true, Boolean.class); + evaluate("3L <= 5L", true, Boolean.class); + evaluate("5L <= 3L", false, Boolean.class); + evaluate("5L <= 5L", true, Boolean.class); + evaluate("3.0d <= 5.0d", true, Boolean.class); + evaluate("5.0d <= 3.0d", false, Boolean.class); + evaluate("5.0d <= 5.0d", true, Boolean.class); + evaluate("new java.math.BigDecimal('5') <= new java.math.BigDecimal('5')", true, Boolean.class); + evaluate("new java.math.BigDecimal('3') <= new java.math.BigDecimal('5')", true, Boolean.class); + evaluate("new java.math.BigDecimal('5') <= new java.math.BigDecimal('3')", false, Boolean.class); + evaluate("3 <= new java.math.BigDecimal('5')", true, Boolean.class); + evaluate("new java.math.BigDecimal('3') <= 5", true, Boolean.class); + evaluate("3L <= new java.math.BigDecimal('5')", true, Boolean.class); + evaluate("3.0d <= new java.math.BigDecimal('5')", true, Boolean.class); + evaluate("3L <= new java.math.BigDecimal('3.1')", true, Boolean.class); + evaluate("3.0d <= new java.math.BigDecimal('3.1')", true, Boolean.class); + evaluate("3.0d <= new java.math.BigDecimal('3.0')", true, Boolean.class); + evaluate("'abc' <= 'def'", true, Boolean.class); + evaluate("'def' <= 'abc'", false, Boolean.class); + evaluate("'abc' <= 'abc'", true, Boolean.class); + + evaluate("3 le 5", true, Boolean.class); + evaluate("5 le 3", false, Boolean.class); + evaluate("6 Le 6", true, Boolean.class); + evaluate("3L lE 5L", true, Boolean.class); + evaluate("5L LE 3L", false, Boolean.class); + evaluate("5L le 5L", true, Boolean.class); + evaluate("3.0d LE 5.0d", true, Boolean.class); + evaluate("5.0d lE 3.0d", false, Boolean.class); + evaluate("5.0d Le 5.0d", true, Boolean.class); + evaluate("new java.math.BigDecimal('5') le new java.math.BigDecimal('5')", true, Boolean.class); + evaluate("new java.math.BigDecimal('3') le new java.math.BigDecimal('5')", true, Boolean.class); + evaluate("new java.math.BigDecimal('5') le new java.math.BigDecimal('3')", false, Boolean.class); + evaluate("3 le new java.math.BigDecimal('5')", true, Boolean.class); + evaluate("new java.math.BigDecimal('3') le 5", true, Boolean.class); + evaluate("3L le new java.math.BigDecimal('5')", true, Boolean.class); + evaluate("3.0d le new java.math.BigDecimal('5')", true, Boolean.class); + evaluate("3L le new java.math.BigDecimal('3.1')", true, Boolean.class); + evaluate("3.0d le new java.math.BigDecimal('3.1')", true, Boolean.class); + evaluate("3.0d le new java.math.BigDecimal('3.0')", true, Boolean.class); + evaluate("'abc' Le 'def'", true, Boolean.class); + evaluate("'def' LE 'abc'", false, Boolean.class); + evaluate("'abc' le 'abc'", true, Boolean.class); + } + + @Test + public void testGreaterThan() { + evaluate("3 > 5", false, Boolean.class); + evaluate("5 > 3", true, Boolean.class); + evaluate("3L > 5L", false, Boolean.class); + evaluate("5L > 3L", true, Boolean.class); + evaluate("3.0d > 5.0d", false, Boolean.class); + evaluate("5.0d > 3.0d", true, Boolean.class); + evaluate("new java.math.BigDecimal('3') > new java.math.BigDecimal('5')", false, Boolean.class); + evaluate("new java.math.BigDecimal('5') > new java.math.BigDecimal('3')", true, Boolean.class); + evaluate("3 > new java.math.BigDecimal('5')", false, Boolean.class); + evaluate("new java.math.BigDecimal('3') > 5", false, Boolean.class); + evaluate("3L > new java.math.BigDecimal('5')", false, Boolean.class); + evaluate("3.0d > new java.math.BigDecimal('5')", false, Boolean.class); + evaluate("3L > new java.math.BigDecimal('3.1')", false, Boolean.class); + evaluate("3.0d > new java.math.BigDecimal('3.1')", false, Boolean.class); + evaluate("3.0d > new java.math.BigDecimal('3.0')", false, Boolean.class); + evaluate("'abc' > 'def'", false, Boolean.class); + evaluate("'abc' > new java.lang.StringBuilder('def')", false, Boolean.class); + evaluate("'def' > 'abc'", true, Boolean.class); + + evaluate("3 gt 5", false, Boolean.class); + evaluate("5 gt 3", true, Boolean.class); + evaluate("3L gt 5L", false, Boolean.class); + evaluate("5L gt 3L", true, Boolean.class); + evaluate("3.0d gt 5.0d", false, Boolean.class); + evaluate("5.0d gT 3.0d", true, Boolean.class); + evaluate("new java.math.BigDecimal('3') gt new java.math.BigDecimal('5')", false, Boolean.class); + evaluate("new java.math.BigDecimal('5') gt new java.math.BigDecimal('3')", true, Boolean.class); + evaluate("3 gt new java.math.BigDecimal('5')", false, Boolean.class); + evaluate("new java.math.BigDecimal('3') gt 5", false, Boolean.class); + evaluate("3L gt new java.math.BigDecimal('5')", false, Boolean.class); + evaluate("3.0d gt new java.math.BigDecimal('5')", false, Boolean.class); + evaluate("3L gt new java.math.BigDecimal('3.1')", false, Boolean.class); + evaluate("3.0d gt new java.math.BigDecimal('3.1')", false, Boolean.class); + evaluate("3.0d gt new java.math.BigDecimal('3.0')", false, Boolean.class); + evaluate("'abc' Gt 'def'", false, Boolean.class); + evaluate("'abc' gt new java.lang.StringBuilder('def')", false, Boolean.class); + evaluate("'def' GT 'abc'", true, Boolean.class); + } + + @Test + public void testGreaterThanOrEqual() { + evaluate("3 >= 5", false, Boolean.class); + evaluate("5 >= 3", true, Boolean.class); + evaluate("6 >= 6", true, Boolean.class); + evaluate("3L >= 5L", false, Boolean.class); + evaluate("5L >= 3L", true, Boolean.class); + evaluate("5L >= 5L", true, Boolean.class); + evaluate("3.0d >= 5.0d", false, Boolean.class); + evaluate("5.0d >= 3.0d", true, Boolean.class); + evaluate("5.0d >= 5.0d", true, Boolean.class); + evaluate("new java.math.BigDecimal('5') >= new java.math.BigDecimal('5')", true, Boolean.class); + evaluate("new java.math.BigDecimal('3') >= new java.math.BigDecimal('5')", false, Boolean.class); + evaluate("new java.math.BigDecimal('5') >= new java.math.BigDecimal('3')", true, Boolean.class); + evaluate("3 >= new java.math.BigDecimal('5')", false, Boolean.class); + evaluate("new java.math.BigDecimal('3') >= 5", false, Boolean.class); + evaluate("3L >= new java.math.BigDecimal('5')", false, Boolean.class); + evaluate("3.0d >= new java.math.BigDecimal('5')", false, Boolean.class); + evaluate("3L >= new java.math.BigDecimal('3.1')", false, Boolean.class); + evaluate("3.0d >= new java.math.BigDecimal('3.1')", false, Boolean.class); + evaluate("3.0d >= new java.math.BigDecimal('3.0')", true, Boolean.class); + evaluate("'abc' >= 'def'", false, Boolean.class); + evaluate("'def' >= 'abc'", true, Boolean.class); + evaluate("'abc' >= 'abc'", true, Boolean.class); + + evaluate("3 GE 5", false, Boolean.class); + evaluate("5 gE 3", true, Boolean.class); + evaluate("6 Ge 6", true, Boolean.class); + evaluate("3L ge 5L", false, Boolean.class); + evaluate("5L ge 3L", true, Boolean.class); + evaluate("5L ge 5L", true, Boolean.class); + evaluate("3.0d ge 5.0d", false, Boolean.class); + evaluate("5.0d ge 3.0d", true, Boolean.class); + evaluate("5.0d ge 5.0d", true, Boolean.class); + evaluate("new java.math.BigDecimal('5') ge new java.math.BigDecimal('5')", true, Boolean.class); + evaluate("new java.math.BigDecimal('3') ge new java.math.BigDecimal('5')", false, Boolean.class); + evaluate("new java.math.BigDecimal('5') ge new java.math.BigDecimal('3')", true, Boolean.class); + evaluate("3 ge new java.math.BigDecimal('5')", false, Boolean.class); + evaluate("new java.math.BigDecimal('3') ge 5", false, Boolean.class); + evaluate("3L ge new java.math.BigDecimal('5')", false, Boolean.class); + evaluate("3.0d ge new java.math.BigDecimal('5')", false, Boolean.class); + evaluate("3L ge new java.math.BigDecimal('3.1')", false, Boolean.class); + evaluate("3.0d ge new java.math.BigDecimal('3.1')", false, Boolean.class); + evaluate("3.0d ge new java.math.BigDecimal('3.0')", true, Boolean.class); + evaluate("'abc' ge 'def'", false, Boolean.class); + evaluate("'def' ge 'abc'", true, Boolean.class); + evaluate("'abc' ge 'abc'", true, Boolean.class); + } + + @Test + public void testIntegerLiteral() { + evaluate("3", 3, Integer.class); + } + + @Test + public void testRealLiteral() { + evaluate("3.5", 3.5d, Double.class); + } + + @Test + public void testMultiplyStringInt() { + evaluate("'a' * 5", "aaaaa", String.class); + } + + @Test + public void testMultiplyDoubleDoubleGivesDouble() { + evaluate("3.0d * 5.0d", 15.0d, Double.class); + } + + @Test + public void testMixedOperandsBigDecimal() { + evaluate("3 * new java.math.BigDecimal('5')", new BigDecimal("15"), BigDecimal.class); + evaluate("3L * new java.math.BigDecimal('5')", new BigDecimal("15"), BigDecimal.class); + evaluate("3.0d * new java.math.BigDecimal('5')", new BigDecimal("15.0"), BigDecimal.class); + + evaluate("3 + new java.math.BigDecimal('5')", new BigDecimal("8"), BigDecimal.class); + evaluate("3L + new java.math.BigDecimal('5')", new BigDecimal("8"), BigDecimal.class); + evaluate("3.0d + new java.math.BigDecimal('5')", new BigDecimal("8.0"), BigDecimal.class); + + evaluate("3 - new java.math.BigDecimal('5')", new BigDecimal("-2"), BigDecimal.class); + evaluate("3L - new java.math.BigDecimal('5')", new BigDecimal("-2"), BigDecimal.class); + evaluate("3.0d - new java.math.BigDecimal('5')", new BigDecimal("-2.0"), BigDecimal.class); + + evaluate("3 / new java.math.BigDecimal('5')", new BigDecimal("1"), BigDecimal.class); + evaluate("3 / new java.math.BigDecimal('5.0')", new BigDecimal("0.6"), BigDecimal.class); + evaluate("3 / new java.math.BigDecimal('5.00')", new BigDecimal("0.60"), BigDecimal.class); + evaluate("3L / new java.math.BigDecimal('5.0')", new BigDecimal("0.6"), BigDecimal.class); + evaluate("3.0d / new java.math.BigDecimal('5.0')", new BigDecimal("0.6"), BigDecimal.class); + + evaluate("5 % new java.math.BigDecimal('3')", new BigDecimal("2"), BigDecimal.class); + evaluate("3 % new java.math.BigDecimal('5')", new BigDecimal("3"), BigDecimal.class); + evaluate("3L % new java.math.BigDecimal('5')", new BigDecimal("3"), BigDecimal.class); + evaluate("3.0d % new java.math.BigDecimal('5')", new BigDecimal("3.0"), BigDecimal.class); + } + + @Test + public void testMathOperatorAdd02() { + evaluate("'hello' + ' ' + 'world'", "hello world", String.class); + } + + @Test + public void testMathOperatorsInChains() { + evaluate("1+2+3",6,Integer.class); + evaluate("2*3*4",24,Integer.class); + evaluate("12-1-2",9,Integer.class); + } + + @Test + public void testIntegerArithmetic() { + evaluate("2 + 4", "6", Integer.class); + evaluate("5 - 4", "1", Integer.class); + evaluate("3 * 5", 15, Integer.class); + evaluate("3.2d * 5", 16.0d, Double.class); + evaluate("3 * 5f", 15f, Float.class); + evaluate("3 / 1", 3, Integer.class); + evaluate("3 % 2", 1, Integer.class); + evaluate("3 mod 2", 1, Integer.class); + evaluate("3 mOd 2", 1, Integer.class); + evaluate("3 Mod 2", 1, Integer.class); + evaluate("3 MOD 2", 1, Integer.class); + } + + @Test + public void testPlus() throws Exception { + evaluate("7 + 2", "9", Integer.class); + evaluate("3.0f + 5.0f", 8.0f, Float.class); + evaluate("3.0d + 5.0d", 8.0d, Double.class); + evaluate("3 + new java.math.BigDecimal('5')", new BigDecimal("8"), BigDecimal.class); + + evaluate("'ab' + 2", "ab2", String.class); + evaluate("2 + 'a'", "2a", String.class); + evaluate("'ab' + null", "abnull", String.class); + evaluate("null + 'ab'", "nullab", String.class); + + // AST: + SpelExpression expr = (SpelExpression)parser.parseExpression("+3"); + assertThat(expr.toStringAST()).isEqualTo("+3"); + expr = (SpelExpression)parser.parseExpression("2+3"); + assertThat(expr.toStringAST()).isEqualTo("(2 + 3)"); + + // use as a unary operator + evaluate("+5d",5d,Double.class); + evaluate("+5L",5L,Long.class); + evaluate("+5",5,Integer.class); + evaluate("+new java.math.BigDecimal('5')", new BigDecimal("5"),BigDecimal.class); + evaluateAndCheckError("+'abc'",SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES); + + // string concatenation + evaluate("'abc'+'def'","abcdef",String.class); + + evaluate("5 + new Integer('37')",42,Integer.class); + } + + @Test + public void testMinus() throws Exception { + evaluate("'c' - 2", "a", String.class); + evaluate("3.0f - 5.0f", -2.0f, Float.class); + evaluateAndCheckError("'ab' - 2", SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES); + evaluateAndCheckError("2-'ab'", SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES); + SpelExpression expr = (SpelExpression)parser.parseExpression("-3"); + assertThat(expr.toStringAST()).isEqualTo("-3"); + expr = (SpelExpression)parser.parseExpression("2-3"); + assertThat(expr.toStringAST()).isEqualTo("(2 - 3)"); + + evaluate("-5d",-5d,Double.class); + evaluate("-5L",-5L,Long.class); + evaluate("-5", -5, Integer.class); + evaluate("-new java.math.BigDecimal('5')", new BigDecimal("-5"),BigDecimal.class); + evaluateAndCheckError("-'abc'", SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES); + } + + @Test + public void testModulus() { + evaluate("3%2",1,Integer.class); + evaluate("3L%2L",1L,Long.class); + evaluate("3.0f%2.0f",1f,Float.class); + evaluate("5.0d % 3.1d", 1.9d, Double.class); + evaluate("new java.math.BigDecimal('5') % new java.math.BigDecimal('3')", new BigDecimal("2"), BigDecimal.class); + evaluate("new java.math.BigDecimal('5') % 3", new BigDecimal("2"), BigDecimal.class); + evaluateAndCheckError("'abc'%'def'",SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES); + } + + @Test + public void testDivide() { + evaluate("3.0f / 5.0f", 0.6f, Float.class); + evaluate("4L/2L",2L,Long.class); + evaluate("3.0f div 5.0f", 0.6f, Float.class); + evaluate("4L DIV 2L",2L,Long.class); + evaluate("new java.math.BigDecimal('3') / 5", new BigDecimal("1"), BigDecimal.class); + evaluate("new java.math.BigDecimal('3.0') / 5", new BigDecimal("0.6"), BigDecimal.class); + evaluate("new java.math.BigDecimal('3.00') / 5", new BigDecimal("0.60"), BigDecimal.class); + evaluate("new java.math.BigDecimal('3.00') / new java.math.BigDecimal('5.0000')", new BigDecimal("0.6000"), BigDecimal.class); + evaluateAndCheckError("'abc'/'def'",SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES); + } + + @Test + public void testMathOperatorDivide_ConvertToDouble() { + evaluateAndAskForReturnType("8/4", new Double(2.0), Double.class); + } + + @Test + public void testMathOperatorDivide04_ConvertToFloat() { + evaluateAndAskForReturnType("8/4", new Float(2.0), Float.class); + } + + @Test + public void testDoubles() { + evaluate("3.0d == 5.0d", false, Boolean.class); + evaluate("3.0d == 3.0d", true, Boolean.class); + evaluate("3.0d != 5.0d", true, Boolean.class); + evaluate("3.0d != 3.0d", false, Boolean.class); + evaluate("3.0d + 5.0d", 8.0d, Double.class); + evaluate("3.0d - 5.0d", -2.0d, Double.class); + evaluate("3.0d * 5.0d", 15.0d, Double.class); + evaluate("3.0d / 5.0d", 0.6d, Double.class); + evaluate("6.0d % 3.5d", 2.5d, Double.class); + } + + @Test + public void testBigDecimals() { + evaluate("3 + new java.math.BigDecimal('5')", new BigDecimal("8"), BigDecimal.class); + evaluate("3 - new java.math.BigDecimal('5')", new BigDecimal("-2"), BigDecimal.class); + evaluate("3 * new java.math.BigDecimal('5')", new BigDecimal("15"), BigDecimal.class); + evaluate("3 / new java.math.BigDecimal('5')", new BigDecimal("1"), BigDecimal.class); + evaluate("5 % new java.math.BigDecimal('3')", new BigDecimal("2"), BigDecimal.class); + evaluate("new java.math.BigDecimal('5') % 3", new BigDecimal("2"), BigDecimal.class); + evaluate("new java.math.BigDecimal('5') ^ 3", new BigDecimal("125"), BigDecimal.class); + } + + @Test + public void testOperatorNames() throws Exception { + Operator node = getOperatorNode((SpelExpression)parser.parseExpression("1==3")); + assertThat(node.getOperatorName()).isEqualTo("=="); + + node = getOperatorNode((SpelExpression)parser.parseExpression("1!=3")); + assertThat(node.getOperatorName()).isEqualTo("!="); + + node = getOperatorNode((SpelExpression)parser.parseExpression("3/3")); + assertThat(node.getOperatorName()).isEqualTo("/"); + + node = getOperatorNode((SpelExpression)parser.parseExpression("3+3")); + assertThat(node.getOperatorName()).isEqualTo("+"); + + node = getOperatorNode((SpelExpression)parser.parseExpression("3-3")); + assertThat(node.getOperatorName()).isEqualTo("-"); + + node = getOperatorNode((SpelExpression)parser.parseExpression("3<4")); + assertThat(node.getOperatorName()).isEqualTo("<"); + + node = getOperatorNode((SpelExpression)parser.parseExpression("3<=4")); + assertThat(node.getOperatorName()).isEqualTo("<="); + + node = getOperatorNode((SpelExpression)parser.parseExpression("3*4")); + assertThat(node.getOperatorName()).isEqualTo("*"); + + node = getOperatorNode((SpelExpression)parser.parseExpression("3%4")); + assertThat(node.getOperatorName()).isEqualTo("%"); + + node = getOperatorNode((SpelExpression)parser.parseExpression("3>=4")); + assertThat(node.getOperatorName()).isEqualTo(">="); + + node = getOperatorNode((SpelExpression)parser.parseExpression("3 between 4")); + assertThat(node.getOperatorName()).isEqualTo("between"); + + node = getOperatorNode((SpelExpression)parser.parseExpression("3 ^ 4")); + assertThat(node.getOperatorName()).isEqualTo("^"); + } + + @Test + public void testOperatorOverloading() { + evaluateAndCheckError("'a' * '2'", SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES); + evaluateAndCheckError("'a' ^ '2'", SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES); + } + + @Test + public void testPower() { + evaluate("3^2",9,Integer.class); + evaluate("3.0d^2.0d",9.0d,Double.class); + evaluate("3L^2L",9L,Long.class); + evaluate("(2^32)^2", 9223372036854775807L, Long.class); + evaluate("new java.math.BigDecimal('5') ^ 3", new BigDecimal("125"), BigDecimal.class); + } + + @Test + public void testMixedOperands_FloatsAndDoubles() { + evaluate("3.0d + 5.0f", 8.0d, Double.class); + evaluate("3.0D - 5.0f", -2.0d, Double.class); + evaluate("3.0f * 5.0d", 15.0d, Double.class); + evaluate("3.0f / 5.0D", 0.6d, Double.class); + evaluate("5.0D % 3f", 2.0d, Double.class); + } + + @Test + public void testMixedOperands_DoublesAndInts() { + evaluate("3.0d + 5", 8.0d, Double.class); + evaluate("3.0D - 5", -2.0d, Double.class); + evaluate("3.0f * 5", 15.0f, Float.class); + evaluate("6.0f / 2", 3.0f, Float.class); + evaluate("6.0f / 4", 1.5f, Float.class); + evaluate("5.0D % 3", 2.0d, Double.class); + evaluate("5.5D % 3", 2.5, Double.class); + } + + @Test + public void testStrings() { + evaluate("'abc' == 'abc'", true, Boolean.class); + evaluate("'abc' == 'def'", false, Boolean.class); + evaluate("'abc' != 'abc'", false, Boolean.class); + evaluate("'abc' != 'def'", true, Boolean.class); + } + + @Test + public void testLongs() { + evaluate("3L == 4L", false, Boolean.class); + evaluate("3L == 3L", true, Boolean.class); + evaluate("3L != 4L", true, Boolean.class); + evaluate("3L != 3L", false, Boolean.class); + evaluate("3L * 50L", 150L, Long.class); + evaluate("3L + 50L", 53L, Long.class); + evaluate("3L - 50L", -47L, Long.class); + } + + @Test + public void testBigIntegers() { + evaluate("3 + new java.math.BigInteger('5')", new BigInteger("8"), BigInteger.class); + evaluate("3 - new java.math.BigInteger('5')", new BigInteger("-2"), BigInteger.class); + evaluate("3 * new java.math.BigInteger('5')", new BigInteger("15"), BigInteger.class); + evaluate("3 / new java.math.BigInteger('5')", new BigInteger("0"), BigInteger.class); + evaluate("5 % new java.math.BigInteger('3')", new BigInteger("2"), BigInteger.class); + evaluate("new java.math.BigInteger('5') % 3", new BigInteger("2"), BigInteger.class); + evaluate("new java.math.BigInteger('5') ^ 3", new BigInteger("125"), BigInteger.class); + } + + + private Operator getOperatorNode(SpelExpression expr) { + SpelNode node = expr.getAST(); + return findOperator(node); + } + + private Operator findOperator(SpelNode node) { + if (node instanceof Operator) { + return (Operator) node; + } + int childCount = node.getChildCount(); + for (int i = 0; i < childCount; i++) { + Operator possible = findOperator(node.getChild(i)); + if (possible != null) { + return possible; + } + } + return null; + } + + + public static class BaseComparable implements Comparable { + + @Override + public int compareTo(BaseComparable other) { + return 0; + } + } + + + public static class SubComparable extends BaseComparable { + } + + + public static class OtherSubComparable extends BaseComparable { + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ParserErrorMessagesTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ParserErrorMessagesTests.java new file mode 100644 index 0000000..359cfc1 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ParserErrorMessagesTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import org.junit.jupiter.api.Test; + +/** + * Tests the messages and exceptions that come out for badly formed expressions + * + * @author Andy Clement + */ +public class ParserErrorMessagesTests extends AbstractExpressionTests { + + @Test + public void testBrokenExpression01() { + // will not fit into an int, needs L suffix + parseAndCheckError("0xCAFEBABE", SpelMessage.NOT_AN_INTEGER); + evaluate("0xCAFEBABEL", 0xCAFEBABEL, Long.class); + parseAndCheckError("0xCAFEBABECAFEBABEL", SpelMessage.NOT_A_LONG); + } + + @Test + public void testBrokenExpression02() { + // rogue 'G' on the end + parseAndCheckError("0xB0BG", SpelMessage.MORE_INPUT, 5, "G"); + } + + @Test + public void testBrokenExpression04() { + // missing right operand + parseAndCheckError("true or ", SpelMessage.RIGHT_OPERAND_PROBLEM, 5); + } + + @Test + public void testBrokenExpression05() { + // missing right operand + parseAndCheckError("1 + ", SpelMessage.RIGHT_OPERAND_PROBLEM, 2); + } + + @Test + public void testBrokenExpression07() { + // T() can only take an identifier (possibly qualified), not a literal + // message ought to say identifier rather than ID + parseAndCheckError("null instanceof T('a')", SpelMessage.NOT_EXPECTED_TOKEN, 18, + "qualified ID","literal_string"); + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ParsingTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ParsingTests.java new file mode 100644 index 0000000..f065fd8 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ParsingTests.java @@ -0,0 +1,460 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.spel.standard.SpelExpression; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Parse some expressions and check we get the AST we expect. Rather than inspecting each node in the AST, we ask it to + * write itself to a string form and check that is as expected. + * + * @author Andy Clement + */ +public class ParsingTests { + + private SpelExpressionParser parser = new SpelExpressionParser(); + + // literals + @Test + public void testLiteralBoolean01() { + parseCheck("false"); + } + + @Test + public void testLiteralLong01() { + parseCheck("37L", "37"); + } + + @Test + public void testLiteralBoolean02() { + parseCheck("true"); + } + + @Test + public void testLiteralBoolean03() { + parseCheck("!true"); + } + + @Test + public void testLiteralInteger01() { + parseCheck("1"); + } + + @Test + public void testLiteralInteger02() { + parseCheck("1415"); + } + + @Test + public void testLiteralString01() { + parseCheck("'hello'"); + } + + @Test + public void testLiteralString02() { + parseCheck("'joe bloggs'"); + } + + @Test + public void testLiteralString03() { + parseCheck("'Tony''s Pizza'", "'Tony's Pizza'"); + } + + @Test + public void testLiteralReal01() { + parseCheck("6.0221415E+23", "6.0221415E23"); + } + + @Test + public void testLiteralHex01() { + parseCheck("0x7FFFFFFF", "2147483647"); + } + + @Test + public void testLiteralDate01() { + parseCheck("date('1974/08/24')"); + } + + @Test + public void testLiteralDate02() { + parseCheck("date('19740824T131030','yyyyMMddTHHmmss')"); + } + + @Test + public void testLiteralNull01() { + parseCheck("null"); + } + + // boolean operators + @Test + public void testBooleanOperatorsOr01() { + parseCheck("false or false", "(false or false)"); + } + + @Test + public void testBooleanOperatorsOr02() { + parseCheck("false or true", "(false or true)"); + } + + @Test + public void testBooleanOperatorsOr03() { + parseCheck("true or false", "(true or false)"); + } + + @Test + public void testBooleanOperatorsOr04() { + parseCheck("true or false", "(true or false)"); + } + + @Test + public void testBooleanOperatorsMix01() { + parseCheck("false or true and false", "(false or (true and false))"); + } + + // relational operators + @Test + public void testRelOperatorsGT01() { + parseCheck("3>6", "(3 > 6)"); + } + + @Test + public void testRelOperatorsLT01() { + parseCheck("3<6", "(3 < 6)"); + } + + @Test + public void testRelOperatorsLE01() { + parseCheck("3<=6", "(3 <= 6)"); + } + + @Test + public void testRelOperatorsGE01() { + parseCheck("3>=6", "(3 >= 6)"); + } + + @Test + public void testRelOperatorsGE02() { + parseCheck("3>=3", "(3 >= 3)"); + } + + @Test + public void testElvis() { + parseCheck("3?:1", "3 ?: 1"); + } + + // public void testRelOperatorsIn01() { + // parseCheck("3 in {1,2,3,4,5}", "(3 in {1,2,3,4,5})"); + // } + // + // public void testRelOperatorsBetween01() { + // parseCheck("1 between {1, 5}", "(1 between {1,5})"); + // } + + // public void testRelOperatorsBetween02() { + // parseCheck("'efg' between {'abc', 'xyz'}", "('efg' between {'abc','xyz'})"); + // }// true + + @Test + public void testRelOperatorsIs01() { + parseCheck("'xyz' instanceof int", "('xyz' instanceof int)"); + }// false + + // public void testRelOperatorsIs02() { + // parseCheck("{1, 2, 3, 4, 5} instanceof List", "({1,2,3,4,5} instanceof List)"); + // }// true + + @Test + public void testRelOperatorsMatches01() { + parseCheck("'5.0067' matches '^-?\\d+(\\.\\d{2})?$'", "('5.0067' matches '^-?\\d+(\\.\\d{2})?$')"); + }// false + + @Test + public void testRelOperatorsMatches02() { + parseCheck("'5.00' matches '^-?\\d+(\\.\\d{2})?$'", "('5.00' matches '^-?\\d+(\\.\\d{2})?$')"); + }// true + + // mathematical operators + @Test + public void testMathOperatorsAdd01() { + parseCheck("2+4", "(2 + 4)"); + } + + @Test + public void testMathOperatorsAdd02() { + parseCheck("'a'+'b'", "('a' + 'b')"); + } + + @Test + public void testMathOperatorsAdd03() { + parseCheck("'hello'+' '+'world'", "(('hello' + ' ') + 'world')"); + } + + @Test + public void testMathOperatorsSubtract01() { + parseCheck("5-4", "(5 - 4)"); + } + + @Test + public void testMathOperatorsMultiply01() { + parseCheck("7*4", "(7 * 4)"); + } + + @Test + public void testMathOperatorsDivide01() { + parseCheck("8/4", "(8 / 4)"); + } + + @Test + public void testMathOperatorModulus01() { + parseCheck("7 % 4", "(7 % 4)"); + } + + // mixed operators + @Test + public void testMixedOperators01() { + parseCheck("true and 5>3", "(true and (5 > 3))"); + } + + // collection processors + // public void testCollectionProcessorsCount01() { + // parseCheck("new String[] {'abc','def','xyz'}.count()"); + // } + + // public void testCollectionProcessorsCount02() { + // parseCheck("new int[] {1,2,3}.count()"); + // } + // + // public void testCollectionProcessorsMax01() { + // parseCheck("new int[] {1,2,3}.max()"); + // } + // + // public void testCollectionProcessorsMin01() { + // parseCheck("new int[] {1,2,3}.min()"); + // } + // + // public void testCollectionProcessorsAverage01() { + // parseCheck("new int[] {1,2,3}.average()"); + // } + // + // public void testCollectionProcessorsSort01() { + // parseCheck("new int[] {3,2,1}.sort()"); + // } + // + // public void testCollectionProcessorsNonNull01() { + // parseCheck("{'a','b',null,'d',null}.nonNull()"); + // } + // + // public void testCollectionProcessorsDistinct01() { + // parseCheck("{'a','b','a','d','e'}.distinct()"); + // } + + // references + @Test + public void testReferences01() { + parseCheck("@foo"); + parseCheck("@'foo.bar'"); + parseCheck("@\"foo.bar.goo\"","@'foo.bar.goo'"); + } + + @Test + public void testReferences03() { + parseCheck("@$$foo"); + } + + // properties + @Test + public void testProperties01() { + parseCheck("name"); + } + + @Test + public void testProperties02() { + parseCheck("placeofbirth.CitY"); + } + + @Test + public void testProperties03() { + parseCheck("a.b.c.d.e"); + } + + // inline list creation + @Test + public void testInlineListCreation01() { + parseCheck("{1, 2, 3, 4, 5}", "{1,2,3,4,5}"); + } + + @Test + public void testInlineListCreation02() { + parseCheck("{'abc','xyz'}", "{'abc','xyz'}"); + } + + // inline map creation + @Test + public void testInlineMapCreation01() { + parseCheck("{'key1':'Value 1','today':DateTime.Today}"); + } + + @Test + public void testInlineMapCreation02() { + parseCheck("{1:'January',2:'February',3:'March'}"); + } + + // methods + @Test + public void testMethods01() { + parseCheck("echo(12)"); + } + + @Test + public void testMethods02() { + parseCheck("echo(name)"); + } + + @Test + public void testMethods03() { + parseCheck("age.doubleItAndAdd(12)"); + } + + // constructors + @Test + public void testConstructors01() { + parseCheck("new String('hello')"); + } + + // public void testConstructors02() { + // parseCheck("new String[3]"); + // } + + // array construction + // public void testArrayConstruction01() { + // parseCheck("new int[] {1, 2, 3, 4, 5}", "new int[] {1,2,3,4,5}"); + // } + // + // public void testArrayConstruction02() { + // parseCheck("new String[] {'abc','xyz'}", "new String[] {'abc','xyz'}"); + // } + + // variables and functions + @Test + public void testVariables01() { + parseCheck("#foo"); + } + + @Test + public void testFunctions01() { + parseCheck("#fn(1,2,3)"); + } + + @Test + public void testFunctions02() { + parseCheck("#fn('hello')"); + } + + // projections and selections + // public void testProjections01() { + // parseCheck("{1,2,3,4,5,6,7,8,9,10}.!{#isEven()}"); + // } + + // public void testSelections01() { + // parseCheck("{1,2,3,4,5,6,7,8,9,10}.?{#isEven(#this) == 'y'}", + // "{1,2,3,4,5,6,7,8,9,10}.?{(#isEven(#this) == 'y')}"); + // } + + // public void testSelectionsFirst01() { + // parseCheck("{1,2,3,4,5,6,7,8,9,10}.^{#isEven(#this) == 'y'}", + // "{1,2,3,4,5,6,7,8,9,10}.^{(#isEven(#this) == 'y')}"); + // } + + // public void testSelectionsLast01() { + // parseCheck("{1,2,3,4,5,6,7,8,9,10}.${#isEven(#this) == 'y'}", + // "{1,2,3,4,5,6,7,8,9,10}.${(#isEven(#this) == 'y')}"); + // } + + // assignment + @Test + public void testAssignmentToVariables01() { + parseCheck("#var1='value1'"); + } + + + // ternary operator + + @Test + public void testTernaryOperator01() { + parseCheck("1>2?3:4","(1 > 2) ? 3 : 4"); + } + + // public void testTernaryOperator01() { + // parseCheck("{1}.#isEven(#this) == 'y'?'it is even':'it is odd'", + // "({1}.#isEven(#this) == 'y') ? 'it is even' : 'it is odd'"); + // } + + // + // public void testLambdaMax() { + // parseCheck("(#max = {|x,y| $x > $y ? $x : $y }; #max(5,25))", "(#max={|x,y| ($x > $y) ? $x : $y };#max(5,25))"); + // } + // + // public void testLambdaFactorial() { + // parseCheck("(#fact = {|n| $n <= 1 ? 1 : $n * #fact($n-1) }; #fact(5))", + // "(#fact={|n| ($n <= 1) ? 1 : ($n * #fact(($n - 1))) };#fact(5))"); + // } // 120 + + // Type references + @Test + public void testTypeReferences01() { + parseCheck("T(java.lang.String)"); + } + + @Test + public void testTypeReferences02() { + parseCheck("T(String)"); + } + + @Test + public void testInlineList1() { + parseCheck("{1,2,3,4}"); + } + + /** + * Parse the supplied expression and then create a string representation of the resultant AST, it should be the same + * as the original expression. + * + * @param expression the expression to parse *and* the expected value of the string form of the resultant AST + */ + public void parseCheck(String expression) { + parseCheck(expression, expression); + } + + /** + * Parse the supplied expression and then create a string representation of the resultant AST, it should be the + * expected value. + * + * @param expression the expression to parse + * @param expectedStringFormOfAST the expected string form of the AST + */ + public void parseCheck(String expression, String expectedStringFormOfAST) { + SpelExpression e = parser.parseRaw(expression); + assertThat(e).isNotNull(); + assertThat(e.toStringAST()).isEqualTo(expectedStringFormOfAST); + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java new file mode 100644 index 0000000..b524752 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java @@ -0,0 +1,367 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Expression; +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.standard.SpelExpression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.SimpleEvaluationContext; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.expression.spel.testresources.Inventor; +import org.springframework.expression.spel.testresources.Person; +import org.springframework.expression.spel.testresources.RecordPerson; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Unit tests for property access. + * + * @author Andy Clement + * @author Juergen Hoeller + * @author Joyce Zhan + * @author Sam Brannen + */ +public class PropertyAccessTests extends AbstractExpressionTests { + + @Test + public void testSimpleAccess01() { + evaluate("name", "Nikola Tesla", String.class); + } + + @Test + public void testSimpleAccess02() { + evaluate("placeOfBirth.city", "SmilJan", String.class); + } + + @Test + public void testSimpleAccess03() { + evaluate("stringArrayOfThreeItems.length", "3", Integer.class); + } + + @Test + public void testNonExistentPropertiesAndMethods() { + // madeup does not exist as a property + evaluateAndCheckError("madeup", SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE, 0); + + // name is ok but foobar does not exist: + evaluateAndCheckError("name.foobar", SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE, 5); + } + + /** + * The standard reflection resolver cannot find properties on null objects but some + * supplied resolver might be able to - so null shouldn't crash the reflection resolver. + */ + @Test + public void testAccessingOnNullObject() { + SpelExpression expr = (SpelExpression)parser.parseExpression("madeup"); + EvaluationContext context = new StandardEvaluationContext(null); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + expr.getValue(context)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE_ON_NULL)); + assertThat(expr.isWritable(context)).isFalse(); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + expr.setValue(context, "abc")) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE_ON_NULL)); + } + + @Test + // Adding a new property accessor just for a particular type + public void testAddingSpecificPropertyAccessor() { + SpelExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext ctx = new StandardEvaluationContext(); + + // Even though this property accessor is added after the reflection one, it specifically + // names the String class as the type it is interested in so is chosen in preference to + // any 'default' ones + ctx.addPropertyAccessor(new StringyPropertyAccessor()); + Expression expr = parser.parseRaw("new String('hello').flibbles"); + Integer i = expr.getValue(ctx, Integer.class); + assertThat((int) i).isEqualTo(7); + + // The reflection one will be used for other properties... + expr = parser.parseRaw("new String('hello').CASE_INSENSITIVE_ORDER"); + Object o = expr.getValue(ctx); + assertThat(o).isNotNull(); + + SpelExpression flibbleexpr = parser.parseRaw("new String('hello').flibbles"); + flibbleexpr.setValue(ctx, 99); + i = flibbleexpr.getValue(ctx, Integer.class); + assertThat((int) i).isEqualTo(99); + + // Cannot set it to a string value + assertThatExceptionOfType(EvaluationException.class).isThrownBy(() -> + flibbleexpr.setValue(ctx, "not allowed")); + // message will be: EL1063E:(pos 20): A problem occurred whilst attempting to set the property + // 'flibbles': 'Cannot set flibbles to an object of type 'class java.lang.String'' + // System.out.println(e.getMessage()); + } + + @Test + public void testAddingRemovingAccessors() { + StandardEvaluationContext ctx = new StandardEvaluationContext(); + + // reflective property accessor is the only one by default + List propertyAccessors = ctx.getPropertyAccessors(); + assertThat(propertyAccessors.size()).isEqualTo(1); + + StringyPropertyAccessor spa = new StringyPropertyAccessor(); + ctx.addPropertyAccessor(spa); + assertThat(ctx.getPropertyAccessors().size()).isEqualTo(2); + + List copy = new ArrayList<>(ctx.getPropertyAccessors()); + assertThat(ctx.removePropertyAccessor(spa)).isTrue(); + assertThat(ctx.removePropertyAccessor(spa)).isFalse(); + assertThat(ctx.getPropertyAccessors().size()).isEqualTo(1); + + ctx.setPropertyAccessors(copy); + assertThat(ctx.getPropertyAccessors().size()).isEqualTo(2); + } + + @Test + public void testAccessingPropertyOfClass() { + Expression expression = parser.parseExpression("name"); + Object value = expression.getValue(new StandardEvaluationContext(String.class)); + assertThat(value).isEqualTo("java.lang.String"); + } + + @Test + public void shouldAlwaysUsePropertyAccessorFromEvaluationContext() { + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expression = parser.parseExpression("name"); + + StandardEvaluationContext context = new StandardEvaluationContext(); + context.addPropertyAccessor(new ConfigurablePropertyAccessor(Collections.singletonMap("name", "Ollie"))); + assertThat(expression.getValue(context)).isEqualTo("Ollie"); + + context = new StandardEvaluationContext(); + context.addPropertyAccessor(new ConfigurablePropertyAccessor(Collections.singletonMap("name", "Jens"))); + assertThat(expression.getValue(context)).isEqualTo("Jens"); + } + + @Test + public void standardGetClassAccess() { + assertThat(parser.parseExpression("'a'.class.name").getValue()).isEqualTo(String.class.getName()); + } + + @Test + public void noGetClassAccess() { + EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + parser.parseExpression("'a'.class.name").getValue(context)); + } + + @Test + public void propertyReadOnly() { + EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); + + Expression expr = parser.parseExpression("name"); + Person target = new Person("p1"); + assertThat(expr.getValue(context, target)).isEqualTo("p1"); + target.setName("p2"); + assertThat(expr.getValue(context, target)).isEqualTo("p2"); + + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + parser.parseExpression("name='p3'").getValue(context, target)); + } + + @Test + public void propertyReadOnlyWithRecordStyle() { + EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); + + Expression expr = parser.parseExpression("name"); + RecordPerson target1 = new RecordPerson("p1"); + assertThat(expr.getValue(context, target1)).isEqualTo("p1"); + RecordPerson target2 = new RecordPerson("p2"); + assertThat(expr.getValue(context, target2)).isEqualTo("p2"); + + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + parser.parseExpression("name='p3'").getValue(context, target2)); + } + + @Test + public void propertyReadWrite() { + EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build(); + + Expression expr = parser.parseExpression("name"); + Person target = new Person("p1"); + assertThat(expr.getValue(context, target)).isEqualTo("p1"); + target.setName("p2"); + assertThat(expr.getValue(context, target)).isEqualTo("p2"); + + parser.parseExpression("name='p3'").getValue(context, target); + assertThat(target.getName()).isEqualTo("p3"); + assertThat(expr.getValue(context, target)).isEqualTo("p3"); + + expr.setValue(context, target, "p4"); + assertThat(target.getName()).isEqualTo("p4"); + assertThat(expr.getValue(context, target)).isEqualTo("p4"); + } + + @Test + public void propertyReadWriteWithRootObject() { + Person target = new Person("p1"); + EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().withRootObject(target).build(); + assertThat(context.getRootObject().getValue()).isSameAs(target); + + Expression expr = parser.parseExpression("name"); + assertThat(expr.getValue(context, target)).isEqualTo("p1"); + target.setName("p2"); + assertThat(expr.getValue(context, target)).isEqualTo("p2"); + + parser.parseExpression("name='p3'").getValue(context, target); + assertThat(target.getName()).isEqualTo("p3"); + assertThat(expr.getValue(context, target)).isEqualTo("p3"); + + expr.setValue(context, target, "p4"); + assertThat(target.getName()).isEqualTo("p4"); + assertThat(expr.getValue(context, target)).isEqualTo("p4"); + } + + @Test + public void propertyAccessWithoutMethodResolver() { + EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); + Person target = new Person("p1"); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + parser.parseExpression("name.substring(1)").getValue(context, target)); + } + + @Test + public void propertyAccessWithInstanceMethodResolver() { + EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().withInstanceMethods().build(); + Person target = new Person("p1"); + assertThat(parser.parseExpression("name.substring(1)").getValue(context, target)).isEqualTo("1"); + } + + @Test + public void propertyAccessWithInstanceMethodResolverAndTypedRootObject() { + Person target = new Person("p1"); + EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding(). + withInstanceMethods().withTypedRootObject(target, TypeDescriptor.valueOf(Object.class)).build(); + + assertThat(parser.parseExpression("name.substring(1)").getValue(context, target)).isEqualTo("1"); + assertThat(context.getRootObject().getValue()).isSameAs(target); + assertThat(context.getRootObject().getTypeDescriptor().getType()).isSameAs(Object.class); + } + + @Test + void propertyAccessWithArrayIndexOutOfBounds() { + EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); + Expression expression = parser.parseExpression("stringArrayOfThreeItems[3]"); + assertThatExceptionOfType(SpelEvaluationException.class) + .isThrownBy(() -> expression.getValue(context, new Inventor())) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.ARRAY_INDEX_OUT_OF_BOUNDS)); + } + + + // This can resolve the property 'flibbles' on any String (very useful...) + private static class StringyPropertyAccessor implements PropertyAccessor { + + int flibbles = 7; + + @Override + public Class[] getSpecificTargetClasses() { + return new Class[] {String.class}; + } + + @Override + public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException { + if (!(target instanceof String)) { + throw new RuntimeException("Assertion Failed! target should be String"); + } + return (name.equals("flibbles")); + } + + @Override + public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException { + if (!(target instanceof String)) { + throw new RuntimeException("Assertion Failed! target should be String"); + } + return (name.equals("flibbles")); + } + + @Override + public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException { + if (!name.equals("flibbles")) { + throw new RuntimeException("Assertion Failed! name should be flibbles"); + } + return new TypedValue(flibbles); + } + + @Override + public void write(EvaluationContext context, Object target, String name, Object newValue) throws AccessException { + if (!name.equals("flibbles")) { + throw new RuntimeException("Assertion Failed! name should be flibbles"); + } + try { + flibbles = (Integer) context.getTypeConverter().convertValue(newValue, + TypeDescriptor.forObject(newValue), TypeDescriptor.valueOf(Integer.class)); + } + catch (EvaluationException ex) { + throw new AccessException("Cannot set flibbles to an object of type '" + newValue.getClass() + "'"); + } + } + } + + + private static class ConfigurablePropertyAccessor implements PropertyAccessor { + + private final Map values; + + public ConfigurablePropertyAccessor(Map values) { + this.values = values; + } + + @Override + public Class[] getSpecificTargetClasses() { + return null; + } + + @Override + public boolean canRead(EvaluationContext context, Object target, String name) { + return true; + } + + @Override + public TypedValue read(EvaluationContext context, Object target, String name) { + return new TypedValue(this.values.get(name)); + } + + @Override + public boolean canWrite(EvaluationContext context, Object target, String name) { + return false; + } + + @Override + public void write(EvaluationContext context, Object target, String name, Object newValue) { + } + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ScenariosForSpringSecurityExpressionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ScenariosForSpringSecurityExpressionTests.java new file mode 100644 index 0000000..5d9798f --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ScenariosForSpringSecurityExpressionTests.java @@ -0,0 +1,324 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import java.lang.reflect.Method; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.MethodExecutor; +import org.springframework.expression.MethodResolver; +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.TypeConverter; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.ReflectionHelper; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +///CLOVER:OFF +/** + * Spring Security scenarios from https://wiki.springsource.com/display/SECURITY/Spring+Security+Expression-based+Authorization + * + * @author Andy Clement + */ +public class ScenariosForSpringSecurityExpressionTests extends AbstractExpressionTests { + + @Test + public void testScenario01_Roles() throws Exception { + SpelExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext ctx = new StandardEvaluationContext(); + Expression expr = parser.parseRaw("hasAnyRole('MANAGER','TELLER')"); + + ctx.setRootObject(new Person("Ben")); + Boolean value = expr.getValue(ctx,Boolean.class); + assertThat((boolean) value).isFalse(); + + ctx.setRootObject(new Manager("Luke")); + value = expr.getValue(ctx,Boolean.class); + assertThat((boolean) value).isTrue(); + } + + @Test + public void testScenario02_ComparingNames() throws Exception { + SpelExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext ctx = new StandardEvaluationContext(); + + ctx.addPropertyAccessor(new SecurityPrincipalAccessor()); + + // Multiple options for supporting this expression: "p.name == principal.name" + // (1) If the right person is the root context object then "name==principal.name" is good enough + Expression expr = parser.parseRaw("name == principal.name"); + + ctx.setRootObject(new Person("Andy")); + Boolean value = expr.getValue(ctx,Boolean.class); + assertThat((boolean) value).isTrue(); + + ctx.setRootObject(new Person("Christian")); + value = expr.getValue(ctx,Boolean.class); + assertThat((boolean) value).isFalse(); + + // (2) Or register an accessor that can understand 'p' and return the right person + expr = parser.parseRaw("p.name == principal.name"); + + PersonAccessor pAccessor = new PersonAccessor(); + ctx.addPropertyAccessor(pAccessor); + ctx.setRootObject(null); + + pAccessor.setPerson(new Person("Andy")); + value = expr.getValue(ctx,Boolean.class); + assertThat((boolean) value).isTrue(); + + pAccessor.setPerson(new Person("Christian")); + value = expr.getValue(ctx,Boolean.class); + assertThat((boolean) value).isFalse(); + } + + @Test + public void testScenario03_Arithmetic() throws Exception { + SpelExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext ctx = new StandardEvaluationContext(); + + // Might be better with a as a variable although it would work as a property too... + // Variable references using a '#' + Expression expr = parser.parseRaw("(hasRole('SUPERVISOR') or (#a < 1.042)) and hasIpAddress('10.10.0.0/16')"); + + Boolean value = null; + + ctx.setVariable("a",1.0d); // referenced as #a in the expression + ctx.setRootObject(new Supervisor("Ben")); // so non-qualified references 'hasRole()' 'hasIpAddress()' are invoked against it + value = expr.getValue(ctx,Boolean.class); + assertThat((boolean) value).isTrue(); + + ctx.setRootObject(new Manager("Luke")); + ctx.setVariable("a",1.043d); + value = expr.getValue(ctx,Boolean.class); + assertThat((boolean) value).isFalse(); + } + + // Here i'm going to change which hasRole() executes and make it one of my own Java methods + @Test + public void testScenario04_ControllingWhichMethodsRun() throws Exception { + SpelExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext ctx = new StandardEvaluationContext(); + + ctx.setRootObject(new Supervisor("Ben")); // so non-qualified references 'hasRole()' 'hasIpAddress()' are invoked against it); + + ctx.addMethodResolver(new MyMethodResolver()); // NEEDS TO OVERRIDE THE REFLECTION ONE - SHOW REORDERING MECHANISM + // Might be better with a as a variable although it would work as a property too... + // Variable references using a '#' +// SpelExpression expr = parser.parseExpression("(hasRole('SUPERVISOR') or (#a < 1.042)) and hasIpAddress('10.10.0.0/16')"); + Expression expr = parser.parseRaw("(hasRole(3) or (#a < 1.042)) and hasIpAddress('10.10.0.0/16')"); + + Boolean value = null; + + ctx.setVariable("a",1.0d); // referenced as #a in the expression + value = expr.getValue(ctx,Boolean.class); + assertThat((boolean) value).isTrue(); + +// ctx.setRootObject(new Manager("Luke")); +// ctx.setVariable("a",1.043d); +// value = (Boolean)expr.getValue(ctx,Boolean.class); +// assertFalse(value); + } + + + static class Person { + + private String n; + + Person(String n) { this.n = n; } + + public String[] getRoles() { return new String[]{"NONE"}; } + + public boolean hasAnyRole(String... roles) { + if (roles == null) return true; + String[] myRoles = getRoles(); + for (int i = 0; i < myRoles.length; i++) { + for (int j = 0; j < roles.length; j++) { + if (myRoles[i].equals(roles[j])) return true; + } + } + return false; + } + + public boolean hasRole(String role) { + return hasAnyRole(role); + } + + public boolean hasIpAddress(String ipaddr) { + return true; + } + + public String getName() { return n; } + } + + + static class Manager extends Person { + + Manager(String n) { + super(n); + } + + @Override + public String[] getRoles() { return new String[]{"MANAGER"};} + } + + + static class Teller extends Person { + + Teller(String n) { + super(n); + } + + @Override + public String[] getRoles() { return new String[]{"TELLER"};} + } + + + static class Supervisor extends Person { + + Supervisor(String n) { + super(n); + } + + @Override + public String[] getRoles() { return new String[]{"SUPERVISOR"};} + } + + + static class SecurityPrincipalAccessor implements PropertyAccessor { + + static class Principal { + public String name = "Andy"; + } + + @Override + public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException { + return name.equals("principal"); + } + + @Override + public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException { + return new TypedValue(new Principal()); + } + + @Override + public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException { + return false; + } + + @Override + public void write(EvaluationContext context, Object target, String name, Object newValue) + throws AccessException { + } + + @Override + public Class[] getSpecificTargetClasses() { + return null; + } + + + } + + + static class PersonAccessor implements PropertyAccessor { + + Person activePerson; + + void setPerson(Person p) { this.activePerson = p; } + + @Override + public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException { + return name.equals("p"); + } + + @Override + public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException { + return new TypedValue(activePerson); + } + + @Override + public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException { + return false; + } + + @Override + public void write(EvaluationContext context, Object target, String name, Object newValue) + throws AccessException { + } + + @Override + public Class[] getSpecificTargetClasses() { + return null; + } + + } + + + static class MyMethodResolver implements MethodResolver { + + static class HasRoleExecutor implements MethodExecutor { + + TypeConverter tc; + + public HasRoleExecutor(TypeConverter typeConverter) { + this.tc = typeConverter; + } + + @Override + public TypedValue execute(EvaluationContext context, Object target, Object... arguments) + throws AccessException { + try { + Method m = HasRoleExecutor.class.getMethod("hasRole", String[].class); + Object[] args = arguments; + if (args != null) { + ReflectionHelper.convertAllArguments(tc, args, m); + } + if (m.isVarArgs()) { + args = ReflectionHelper.setupArgumentsForVarargsInvocation(m.getParameterTypes(), args); + } + return new TypedValue(m.invoke(null, args), new TypeDescriptor(new MethodParameter(m,-1))); + } + catch (Exception ex) { + throw new AccessException("Problem invoking hasRole", ex); + } + } + + public static boolean hasRole(String... strings) { + return true; + } + } + + @Override + public MethodExecutor resolve(EvaluationContext context, Object targetObject, String name, List arguments) + throws AccessException { + if (name.equals("hasRole")) { + return new HasRoleExecutor(context.getTypeConverter()); + } + return null; + } + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SelectionAndProjectionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SelectionAndProjectionTests.java new file mode 100644 index 0000000..148f318 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SelectionAndProjectionTests.java @@ -0,0 +1,468 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Mark Fisher + * @author Sam Brannen + * @author Juergen Hoeller + */ +public class SelectionAndProjectionTests { + + @Test + public void selectionWithList() throws Exception { + Expression expression = new SpelExpressionParser().parseRaw("integers.?[#this<5]"); + EvaluationContext context = new StandardEvaluationContext(new ListTestBean()); + Object value = expression.getValue(context); + boolean condition = value instanceof List; + assertThat(condition).isTrue(); + List list = (List) value; + assertThat(list.size()).isEqualTo(5); + assertThat(list.get(0)).isEqualTo(0); + assertThat(list.get(1)).isEqualTo(1); + assertThat(list.get(2)).isEqualTo(2); + assertThat(list.get(3)).isEqualTo(3); + assertThat(list.get(4)).isEqualTo(4); + } + + @Test + public void selectFirstItemInList() throws Exception { + Expression expression = new SpelExpressionParser().parseRaw("integers.^[#this<5]"); + EvaluationContext context = new StandardEvaluationContext(new ListTestBean()); + Object value = expression.getValue(context); + boolean condition = value instanceof Integer; + assertThat(condition).isTrue(); + assertThat(value).isEqualTo(0); + } + + @Test + public void selectLastItemInList() throws Exception { + Expression expression = new SpelExpressionParser().parseRaw("integers.$[#this<5]"); + EvaluationContext context = new StandardEvaluationContext(new ListTestBean()); + Object value = expression.getValue(context); + boolean condition = value instanceof Integer; + assertThat(condition).isTrue(); + assertThat(value).isEqualTo(4); + } + + @Test + public void selectionWithSet() throws Exception { + Expression expression = new SpelExpressionParser().parseRaw("integers.?[#this<5]"); + EvaluationContext context = new StandardEvaluationContext(new SetTestBean()); + Object value = expression.getValue(context); + boolean condition = value instanceof List; + assertThat(condition).isTrue(); + List list = (List) value; + assertThat(list.size()).isEqualTo(5); + assertThat(list.get(0)).isEqualTo(0); + assertThat(list.get(1)).isEqualTo(1); + assertThat(list.get(2)).isEqualTo(2); + assertThat(list.get(3)).isEqualTo(3); + assertThat(list.get(4)).isEqualTo(4); + } + + @Test + public void selectFirstItemInSet() throws Exception { + Expression expression = new SpelExpressionParser().parseRaw("integers.^[#this<5]"); + EvaluationContext context = new StandardEvaluationContext(new SetTestBean()); + Object value = expression.getValue(context); + boolean condition = value instanceof Integer; + assertThat(condition).isTrue(); + assertThat(value).isEqualTo(0); + } + + @Test + public void selectLastItemInSet() throws Exception { + Expression expression = new SpelExpressionParser().parseRaw("integers.$[#this<5]"); + EvaluationContext context = new StandardEvaluationContext(new SetTestBean()); + Object value = expression.getValue(context); + boolean condition = value instanceof Integer; + assertThat(condition).isTrue(); + assertThat(value).isEqualTo(4); + } + + @Test + public void selectionWithIterable() throws Exception { + Expression expression = new SpelExpressionParser().parseRaw("integers.?[#this<5]"); + EvaluationContext context = new StandardEvaluationContext(new IterableTestBean()); + Object value = expression.getValue(context); + boolean condition = value instanceof List; + assertThat(condition).isTrue(); + List list = (List) value; + assertThat(list.size()).isEqualTo(5); + assertThat(list.get(0)).isEqualTo(0); + assertThat(list.get(1)).isEqualTo(1); + assertThat(list.get(2)).isEqualTo(2); + assertThat(list.get(3)).isEqualTo(3); + assertThat(list.get(4)).isEqualTo(4); + } + + @Test + public void selectionWithArray() throws Exception { + Expression expression = new SpelExpressionParser().parseRaw("integers.?[#this<5]"); + EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); + Object value = expression.getValue(context); + assertThat(value.getClass().isArray()).isTrue(); + TypedValue typedValue = new TypedValue(value); + assertThat(typedValue.getTypeDescriptor().getElementTypeDescriptor().getType()).isEqualTo(Integer.class); + Integer[] array = (Integer[]) value; + assertThat(array.length).isEqualTo(5); + assertThat(array[0]).isEqualTo(0); + assertThat(array[1]).isEqualTo(1); + assertThat(array[2]).isEqualTo(2); + assertThat(array[3]).isEqualTo(3); + assertThat(array[4]).isEqualTo(4); + } + + @Test + public void selectFirstItemInArray() throws Exception { + Expression expression = new SpelExpressionParser().parseRaw("integers.^[#this<5]"); + EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); + Object value = expression.getValue(context); + boolean condition = value instanceof Integer; + assertThat(condition).isTrue(); + assertThat(value).isEqualTo(0); + } + + @Test + public void selectLastItemInArray() throws Exception { + Expression expression = new SpelExpressionParser().parseRaw("integers.$[#this<5]"); + EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); + Object value = expression.getValue(context); + boolean condition = value instanceof Integer; + assertThat(condition).isTrue(); + assertThat(value).isEqualTo(4); + } + + @Test + public void selectionWithPrimitiveArray() throws Exception { + Expression expression = new SpelExpressionParser().parseRaw("ints.?[#this<5]"); + EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); + Object value = expression.getValue(context); + assertThat(value.getClass().isArray()).isTrue(); + TypedValue typedValue = new TypedValue(value); + assertThat(typedValue.getTypeDescriptor().getElementTypeDescriptor().getType()).isEqualTo(Integer.class); + Integer[] array = (Integer[]) value; + assertThat(array.length).isEqualTo(5); + assertThat(array[0]).isEqualTo(0); + assertThat(array[1]).isEqualTo(1); + assertThat(array[2]).isEqualTo(2); + assertThat(array[3]).isEqualTo(3); + assertThat(array[4]).isEqualTo(4); + } + + @Test + public void selectFirstItemInPrimitiveArray() throws Exception { + Expression expression = new SpelExpressionParser().parseRaw("ints.^[#this<5]"); + EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); + Object value = expression.getValue(context); + boolean condition = value instanceof Integer; + assertThat(condition).isTrue(); + assertThat(value).isEqualTo(0); + } + + @Test + public void selectLastItemInPrimitiveArray() throws Exception { + Expression expression = new SpelExpressionParser().parseRaw("ints.$[#this<5]"); + EvaluationContext context = new StandardEvaluationContext(new ArrayTestBean()); + Object value = expression.getValue(context); + boolean condition = value instanceof Integer; + assertThat(condition).isTrue(); + assertThat(value).isEqualTo(4); + } + + @Test + @SuppressWarnings("unchecked") + public void selectionWithMap() { + EvaluationContext context = new StandardEvaluationContext(new MapTestBean()); + ExpressionParser parser = new SpelExpressionParser(); + Expression exp = parser.parseExpression("colors.?[key.startsWith('b')]"); + + Map colorsMap = (Map) exp.getValue(context); + assertThat(colorsMap.size()).isEqualTo(3); + assertThat(colorsMap.containsKey("beige")).isTrue(); + assertThat(colorsMap.containsKey("blue")).isTrue(); + assertThat(colorsMap.containsKey("brown")).isTrue(); + } + + @Test + @SuppressWarnings("unchecked") + public void selectFirstItemInMap() { + EvaluationContext context = new StandardEvaluationContext(new MapTestBean()); + ExpressionParser parser = new SpelExpressionParser(); + + Expression exp = parser.parseExpression("colors.^[key.startsWith('b')]"); + Map colorsMap = (Map) exp.getValue(context); + assertThat(colorsMap.size()).isEqualTo(1); + assertThat(colorsMap.keySet().iterator().next()).isEqualTo("beige"); + } + + @Test + @SuppressWarnings("unchecked") + public void selectLastItemInMap() { + EvaluationContext context = new StandardEvaluationContext(new MapTestBean()); + ExpressionParser parser = new SpelExpressionParser(); + + Expression exp = parser.parseExpression("colors.$[key.startsWith('b')]"); + Map colorsMap = (Map) exp.getValue(context); + assertThat(colorsMap.size()).isEqualTo(1); + assertThat(colorsMap.keySet().iterator().next()).isEqualTo("brown"); + } + + @Test + public void projectionWithList() throws Exception { + Expression expression = new SpelExpressionParser().parseRaw("#testList.![wrapper.value]"); + EvaluationContext context = new StandardEvaluationContext(); + context.setVariable("testList", IntegerTestBean.createList()); + Object value = expression.getValue(context); + boolean condition = value instanceof List; + assertThat(condition).isTrue(); + List list = (List) value; + assertThat(list.size()).isEqualTo(3); + assertThat(list.get(0)).isEqualTo(5); + assertThat(list.get(1)).isEqualTo(6); + assertThat(list.get(2)).isEqualTo(7); + } + + @Test + public void projectionWithSet() throws Exception { + Expression expression = new SpelExpressionParser().parseRaw("#testList.![wrapper.value]"); + EvaluationContext context = new StandardEvaluationContext(); + context.setVariable("testList", IntegerTestBean.createSet()); + Object value = expression.getValue(context); + boolean condition = value instanceof List; + assertThat(condition).isTrue(); + List list = (List) value; + assertThat(list.size()).isEqualTo(3); + assertThat(list.get(0)).isEqualTo(5); + assertThat(list.get(1)).isEqualTo(6); + assertThat(list.get(2)).isEqualTo(7); + } + + @Test + public void projectionWithIterable() throws Exception { + Expression expression = new SpelExpressionParser().parseRaw("#testList.![wrapper.value]"); + EvaluationContext context = new StandardEvaluationContext(); + context.setVariable("testList", IntegerTestBean.createIterable()); + Object value = expression.getValue(context); + boolean condition = value instanceof List; + assertThat(condition).isTrue(); + List list = (List) value; + assertThat(list.size()).isEqualTo(3); + assertThat(list.get(0)).isEqualTo(5); + assertThat(list.get(1)).isEqualTo(6); + assertThat(list.get(2)).isEqualTo(7); + } + + @Test + public void projectionWithArray() throws Exception { + Expression expression = new SpelExpressionParser().parseRaw("#testArray.![wrapper.value]"); + EvaluationContext context = new StandardEvaluationContext(); + context.setVariable("testArray", IntegerTestBean.createArray()); + Object value = expression.getValue(context); + assertThat(value.getClass().isArray()).isTrue(); + TypedValue typedValue = new TypedValue(value); + assertThat(typedValue.getTypeDescriptor().getElementTypeDescriptor().getType()).isEqualTo(Number.class); + Number[] array = (Number[]) value; + assertThat(array.length).isEqualTo(3); + assertThat(array[0]).isEqualTo(5); + assertThat(array[1]).isEqualTo(5.9f); + assertThat(array[2]).isEqualTo(7); + } + + + static class ListTestBean { + + private final List integers = new ArrayList<>(); + + ListTestBean() { + for (int i = 0; i < 10; i++) { + integers.add(i); + } + } + + public List getIntegers() { + return integers; + } + } + + + static class SetTestBean { + + private final Set integers = new LinkedHashSet<>(); + + SetTestBean() { + for (int i = 0; i < 10; i++) { + integers.add(i); + } + } + + public Set getIntegers() { + return integers; + } + } + + + static class IterableTestBean { + + private final Set integers = new LinkedHashSet<>(); + + IterableTestBean() { + for (int i = 0; i < 10; i++) { + integers.add(i); + } + } + + public Iterable getIntegers() { + return new Iterable() { + @Override + public Iterator iterator() { + return integers.iterator(); + } + }; + } + } + + + static class ArrayTestBean { + + private final int[] ints = new int[10]; + + private final Integer[] integers = new Integer[10]; + + ArrayTestBean() { + for (int i = 0; i < 10; i++) { + ints[i] = i; + integers[i] = i; + } + } + + public int[] getInts() { + return ints; + } + + public Integer[] getIntegers() { + return integers; + } + } + + + static class MapTestBean { + + private final Map colors = new TreeMap<>(); + + MapTestBean() { + // colors.put("black", "schwarz"); + colors.put("red", "rot"); + colors.put("brown", "braun"); + colors.put("blue", "blau"); + colors.put("yellow", "gelb"); + colors.put("beige", "beige"); + } + + public Map getColors() { + return colors; + } + } + + + static class IntegerTestBean { + + private final IntegerWrapper wrapper; + + IntegerTestBean(Number value) { + this.wrapper = new IntegerWrapper(value); + } + + public IntegerWrapper getWrapper() { + return this.wrapper; + } + + static List createList() { + List list = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + list.add(new IntegerTestBean(i + 5)); + } + return list; + } + + static Set createSet() { + Set set = new LinkedHashSet<>(); + for (int i = 0; i < 3; i++) { + set.add(new IntegerTestBean(i + 5)); + } + return set; + } + + static Iterable createIterable() { + final Set set = createSet(); + return new Iterable() { + @Override + public Iterator iterator() { + return set.iterator(); + } + }; + } + + static IntegerTestBean[] createArray() { + IntegerTestBean[] array = new IntegerTestBean[3]; + for (int i = 0; i < 3; i++) { + if (i == 1) { + array[i] = new IntegerTestBean(5.9f); + } + else { + array[i] = new IntegerTestBean(i + 5); + } + } + return array; + } + } + + + static class IntegerWrapper { + + private final Number value; + + IntegerWrapper(Number value) { + this.value = value; + } + + public Number getValue() { + return this.value; + } + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SetValueTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SetValueTests.java new file mode 100644 index 0000000..6073d73 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SetValueTests.java @@ -0,0 +1,279 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import java.util.Collection; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Expression; +import org.springframework.expression.ParseException; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.expression.spel.testresources.PlaceOfBirth; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests set value expressions. + * + * @author Keith Donald + * @author Andy Clement + */ +public class SetValueTests extends AbstractExpressionTests { + + private final static boolean DEBUG = false; + + + @Test + public void testSetProperty() { + setValue("wonNobelPrize", true); + } + + @Test + public void testSetNestedProperty() { + setValue("placeOfBirth.city", "Wien"); + } + + @Test + public void testSetArrayElementValue() { + setValue("inventions[0]", "Just the telephone"); + } + + @Test + public void testErrorCase() { + setValueExpectError("3=4", null); + } + + @Test + public void testSetElementOfNull() { + setValueExpectError("new org.springframework.expression.spel.testresources.Inventor().inventions[1]", + SpelMessage.CANNOT_INDEX_INTO_NULL_VALUE); + } + + @Test + public void testSetArrayElementValueAllPrimitiveTypes() { + setValue("arrayContainer.ints[1]", 3); + setValue("arrayContainer.floats[1]", 3.0f); + setValue("arrayContainer.booleans[1]", false); + setValue("arrayContainer.doubles[1]", 3.4d); + setValue("arrayContainer.shorts[1]", (short)3); + setValue("arrayContainer.longs[1]", 3L); + setValue("arrayContainer.bytes[1]", (byte) 3); + setValue("arrayContainer.chars[1]", (char) 3); + } + + @Test + public void testIsWritableForInvalidExpressions_SPR10610() { + StandardEvaluationContext lContext = TestScenarioCreator.getTestEvaluationContext(); + + // PROPERTYORFIELDREFERENCE + // Non existent field (or property): + Expression e1 = parser.parseExpression("arrayContainer.wibble"); + assertThat(e1.isWritable(lContext)).as("Should not be writable!").isFalse(); + + Expression e2 = parser.parseExpression("arrayContainer.wibble.foo"); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + e2.isWritable(lContext)); +// org.springframework.expression.spel.SpelEvaluationException: EL1008E:(pos 15): Property or field 'wibble' cannot be found on object of type 'org.springframework.expression.spel.testresources.ArrayContainer' - maybe not public? +// at org.springframework.expression.spel.ast.PropertyOrFieldReference.readProperty(PropertyOrFieldReference.java:225) + + // VARIABLE + // the variable does not exist (but that is OK, we should be writable) + Expression e3 = parser.parseExpression("#madeup1"); + assertThat(e3.isWritable(lContext)).as("Should be writable!").isTrue(); + + Expression e4 = parser.parseExpression("#madeup2.bar"); // compound expression + assertThat(e4.isWritable(lContext)).as("Should not be writable!").isFalse(); + + // INDEXER + // non existent indexer (wibble made up) + Expression e5 = parser.parseExpression("arrayContainer.wibble[99]"); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + e5.isWritable(lContext)); + + // non existent indexer (index via a string) + Expression e6 = parser.parseExpression("arrayContainer.ints['abc']"); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + e6.isWritable(lContext)); + } + + @Test + public void testSetArrayElementValueAllPrimitiveTypesErrors() { + // none of these sets are possible due to (expected) conversion problems + setValueExpectError("arrayContainer.ints[1]", "wibble"); + setValueExpectError("arrayContainer.floats[1]", "dribble"); + setValueExpectError("arrayContainer.booleans[1]", "nein"); + // TODO -- this fails with NPE due to ArrayToObject converter - discuss with Andy + //setValueExpectError("arrayContainer.doubles[1]", new ArrayList()); + //setValueExpectError("arrayContainer.shorts[1]", new ArrayList()); + //setValueExpectError("arrayContainer.longs[1]", new ArrayList()); + setValueExpectError("arrayContainer.bytes[1]", "NaB"); + setValueExpectError("arrayContainer.chars[1]", "NaC"); + } + + @Test + public void testSetArrayElementNestedValue() { + setValue("placesLived[0].city", "Wien"); + } + + @Test + public void testSetListElementValue() { + setValue("placesLivedList[0]", new PlaceOfBirth("Wien")); + } + + @Test + public void testSetGenericListElementValueTypeCoersion() { + // TODO currently failing since setValue does a getValue and "Wien" string != PlaceOfBirth - check with andy + setValue("placesLivedList[0]", "Wien"); + } + + @Test + public void testSetGenericListElementValueTypeCoersionOK() { + setValue("booleanList[0]", "true", Boolean.TRUE); + } + + @Test + public void testSetListElementNestedValue() { + setValue("placesLived[0].city", "Wien"); + } + + @Test + public void testSetArrayElementInvalidIndex() { + setValueExpectError("placesLived[23]", "Wien"); + setValueExpectError("placesLivedList[23]", "Wien"); + } + + @Test + public void testSetMapElements() { + setValue("testMap['montag']","lundi"); + } + + @Test + public void testIndexingIntoUnsupportedType() { + setValueExpectError("'hello'[3]", 'p'); + } + + @Test + public void testSetPropertyTypeCoersion() { + setValue("publicBoolean", "true", Boolean.TRUE); + } + + @Test + public void testSetPropertyTypeCoersionThroughSetter() { + setValue("SomeProperty", "true", Boolean.TRUE); + } + + @Test + public void testAssign() throws Exception { + StandardEvaluationContext eContext = TestScenarioCreator.getTestEvaluationContext(); + Expression e = parse("publicName='Andy'"); + assertThat(e.isWritable(eContext)).isFalse(); + assertThat(e.getValue(eContext)).isEqualTo("Andy"); + } + + /* + * Testing the coercion of both the keys and the values to the correct type + */ + @Test + public void testSetGenericMapElementRequiresCoercion() throws Exception { + StandardEvaluationContext eContext = TestScenarioCreator.getTestEvaluationContext(); + Expression e = parse("mapOfStringToBoolean[42]"); + assertThat(e.getValue(eContext)).isNull(); + + // Key should be coerced to string representation of 42 + e.setValue(eContext, "true"); + + // All keys should be strings + Set ks = parse("mapOfStringToBoolean.keySet()").getValue(eContext, Set.class); + for (Object o: ks) { + assertThat(o.getClass()).isEqualTo(String.class); + } + + // All values should be booleans + Collection vs = parse("mapOfStringToBoolean.values()").getValue(eContext, Collection.class); + for (Object o: vs) { + assertThat(o.getClass()).isEqualTo(Boolean.class); + } + + // One final test check coercion on the key for a map lookup + Object o = e.getValue(eContext); + assertThat(o).isEqualTo(Boolean.TRUE); + } + + + private Expression parse(String expressionString) throws Exception { + return parser.parseExpression(expressionString); + } + + /** + * Call setValue() but expect it to fail. + */ + protected void setValueExpectError(String expression, Object value) { + Expression e = parser.parseExpression(expression); + assertThat(e).isNotNull(); + if (DEBUG) { + SpelUtilities.printAbstractSyntaxTree(System.out, e); + } + StandardEvaluationContext lContext = TestScenarioCreator.getTestEvaluationContext(); + assertThatExceptionOfType(EvaluationException.class).isThrownBy(() -> + e.setValue(lContext, value)); + } + + protected void setValue(String expression, Object value) { + try { + Expression e = parser.parseExpression(expression); + assertThat(e).isNotNull(); + if (DEBUG) { + SpelUtilities.printAbstractSyntaxTree(System.out, e); + } + StandardEvaluationContext lContext = TestScenarioCreator.getTestEvaluationContext(); + assertThat(e.isWritable(lContext)).as("Expression is not writeable but should be").isTrue(); + e.setValue(lContext, value); + assertThat(e.getValue(lContext,value.getClass())).as("Retrieved value was not equal to set value").isEqualTo(value); + } + catch (EvaluationException | ParseException ex) { + throw new AssertionError("Unexpected Exception: " + ex.getMessage(), ex); + } + } + + /** + * For use when coercion is happening during a setValue(). The expectedValue should be + * the coerced form of the value. + */ + protected void setValue(String expression, Object value, Object expectedValue) { + try { + Expression e = parser.parseExpression(expression); + assertThat(e).isNotNull(); + if (DEBUG) { + SpelUtilities.printAbstractSyntaxTree(System.out, e); + } + StandardEvaluationContext lContext = TestScenarioCreator.getTestEvaluationContext(); + assertThat(e.isWritable(lContext)).as("Expression is not writeable but should be").isTrue(); + e.setValue(lContext, value); + Object a = expectedValue; + Object b = e.getValue(lContext); + assertThat(a).isEqualTo(b); + } + catch (EvaluationException | ParseException ex) { + throw new AssertionError("Unexpected Exception: " + ex.getMessage(), ex); + } + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java new file mode 100644 index 0000000..993a645 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java @@ -0,0 +1,6259 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; + +import org.junit.jupiter.api.Test; + +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.ast.CompoundExpression; +import org.springframework.expression.spel.ast.OpLT; +import org.springframework.expression.spel.ast.SpelNodeImpl; +import org.springframework.expression.spel.ast.Ternary; +import org.springframework.expression.spel.standard.SpelCompiler; +import org.springframework.expression.spel.standard.SpelExpression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.expression.spel.testdata.PersonInOtherPackage; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.within; + +/** + * Checks SpelCompiler behavior. This should cover compilation all compiled node types. + * + * @author Andy Clement + * @since 4.1 + */ +public class SpelCompilationCoverageTests extends AbstractExpressionTests { + + /* + * Further TODOs for compilation: + * + * - OpMinus with a single literal operand could be treated as a negative literal. Will save a + * pointless loading of 0 and then a subtract instruction in code gen. + * - allow other accessors/resolvers to participate in compilation and create their own code + * - A TypeReference followed by (what ends up as) a static method invocation can really skip + * code gen for the TypeReference since once that is used to locate the method it is not + * used again. + * - The opEq implementation is quite basic. It will compare numbers of the same type (allowing + * them to be their boxed or unboxed variants) or compare object references. It does not + * compile expressions where numbers are of different types or when objects implement + * Comparable. + * + * Compiled nodes: + * + * TypeReference + * OperatorInstanceOf + * StringLiteral + * NullLiteral + * RealLiteral + * IntLiteral + * LongLiteral + * BooleanLiteral + * FloatLiteral + * OpOr + * OpAnd + * OperatorNot + * Ternary + * Elvis + * VariableReference + * OpLt + * OpLe + * OpGt + * OpGe + * OpEq + * OpNe + * OpPlus + * OpMinus + * OpMultiply + * OpDivide + * MethodReference + * PropertyOrFieldReference + * Indexer + * CompoundExpression + * ConstructorReference + * FunctionReference + * InlineList + * OpModulus + * + * Not yet compiled (some may never need to be): + * Assign + * BeanReference + * Identifier + * OpDec + * OpBetween + * OpMatches + * OpPower + * OpInc + * Projection + * QualifiedId + * Selection + */ + + + private Expression expression; + + private SpelNodeImpl ast; + + + @Test + public void typeReference() throws Exception { + expression = parse("T(String)"); + assertThat(expression.getValue()).isEqualTo(String.class); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(String.class); + + expression = parse("T(java.io.IOException)"); + assertThat(expression.getValue()).isEqualTo(IOException.class); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(IOException.class); + + expression = parse("T(java.io.IOException[])"); + assertThat(expression.getValue()).isEqualTo(IOException[].class); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(IOException[].class); + + expression = parse("T(int[][])"); + assertThat(expression.getValue()).isEqualTo(int[][].class); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(int[][].class); + + expression = parse("T(int)"); + assertThat(expression.getValue()).isEqualTo(Integer.TYPE); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(Integer.TYPE); + + expression = parse("T(byte)"); + assertThat(expression.getValue()).isEqualTo(Byte.TYPE); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(Byte.TYPE); + + expression = parse("T(char)"); + assertThat(expression.getValue()).isEqualTo(Character.TYPE); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(Character.TYPE); + + expression = parse("T(short)"); + assertThat(expression.getValue()).isEqualTo(Short.TYPE); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(Short.TYPE); + + expression = parse("T(long)"); + assertThat(expression.getValue()).isEqualTo(Long.TYPE); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(Long.TYPE); + + expression = parse("T(float)"); + assertThat(expression.getValue()).isEqualTo(Float.TYPE); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(Float.TYPE); + + expression = parse("T(double)"); + assertThat(expression.getValue()).isEqualTo(Double.TYPE); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(Double.TYPE); + + expression = parse("T(boolean)"); + assertThat(expression.getValue()).isEqualTo(Boolean.TYPE); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(Boolean.TYPE); + + expression = parse("T(Missing)"); + assertGetValueFail(expression); + assertCantCompile(expression); + } + + @SuppressWarnings("unchecked") + @Test + public void operatorInstanceOf() throws Exception { + expression = parse("'xyz' instanceof T(String)"); + assertThat(expression.getValue()).isEqualTo(true); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(true); + + expression = parse("'xyz' instanceof T(Integer)"); + assertThat(expression.getValue()).isEqualTo(false); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(false); + + List list = new ArrayList<>(); + expression = parse("#root instanceof T(java.util.List)"); + assertThat(expression.getValue(list)).isEqualTo(true); + assertCanCompile(expression); + assertThat(expression.getValue(list)).isEqualTo(true); + + List[] arrayOfLists = new List[] {new ArrayList()}; + expression = parse("#root instanceof T(java.util.List[])"); + assertThat(expression.getValue(arrayOfLists)).isEqualTo(true); + assertCanCompile(expression); + assertThat(expression.getValue(arrayOfLists)).isEqualTo(true); + + int[] intArray = new int[] {1,2,3}; + expression = parse("#root instanceof T(int[])"); + assertThat(expression.getValue(intArray)).isEqualTo(true); + assertCanCompile(expression); + assertThat(expression.getValue(intArray)).isEqualTo(true); + + String root = null; + expression = parse("#root instanceof T(Integer)"); + assertThat(expression.getValue(root)).isEqualTo(false); + assertCanCompile(expression); + assertThat(expression.getValue(root)).isEqualTo(false); + + // root still null + expression = parse("#root instanceof T(java.lang.Object)"); + assertThat(expression.getValue(root)).isEqualTo(false); + assertCanCompile(expression); + assertThat(expression.getValue(root)).isEqualTo(false); + + root = "howdy!"; + expression = parse("#root instanceof T(java.lang.Object)"); + assertThat(expression.getValue(root)).isEqualTo(true); + assertCanCompile(expression); + assertThat(expression.getValue(root)).isEqualTo(true); + } + + @Test + public void operatorInstanceOf_SPR14250() throws Exception { + // primitive left operand - should get boxed, return true + expression = parse("3 instanceof T(Integer)"); + assertThat(expression.getValue()).isEqualTo(true); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(true); + + // primitive left operand - should get boxed, return false + expression = parse("3 instanceof T(String)"); + assertThat(expression.getValue()).isEqualTo(false); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(false); + + // double slot left operand - should get boxed, return false + expression = parse("3.0d instanceof T(Integer)"); + assertThat(expression.getValue()).isEqualTo(false); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(false); + + // double slot left operand - should get boxed, return true + expression = parse("3.0d instanceof T(Double)"); + assertThat(expression.getValue()).isEqualTo(true); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(true); + + // Only when the right hand operand is a direct type reference + // will it be compilable. + StandardEvaluationContext ctx = new StandardEvaluationContext(); + ctx.setVariable("foo", String.class); + expression = parse("3 instanceof #foo"); + assertThat(expression.getValue(ctx)).isEqualTo(false); + assertCantCompile(expression); + + // use of primitive as type for instanceof check - compilable + // but always false + expression = parse("3 instanceof T(int)"); + assertThat(expression.getValue()).isEqualTo(false); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(false); + + expression = parse("3 instanceof T(long)"); + assertThat(expression.getValue()).isEqualTo(false); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(false); + } + + @Test + public void stringLiteral() throws Exception { + expression = parser.parseExpression("'abcde'"); + assertThat(expression.getValue(new TestClass1(), String.class)).isEqualTo("abcde"); + assertCanCompile(expression); + String resultC = expression.getValue(new TestClass1(), String.class); + assertThat(resultC).isEqualTo("abcde"); + assertThat(expression.getValue(String.class)).isEqualTo("abcde"); + assertThat(expression.getValue()).isEqualTo("abcde"); + assertThat(expression.getValue(new StandardEvaluationContext())).isEqualTo("abcde"); + expression = parser.parseExpression("\"abcde\""); + assertCanCompile(expression); + assertThat(expression.getValue(String.class)).isEqualTo("abcde"); + } + + @Test + public void nullLiteral() throws Exception { + expression = parser.parseExpression("null"); + Object resultI = expression.getValue(new TestClass1(), Object.class); + assertCanCompile(expression); + Object resultC = expression.getValue(new TestClass1(), Object.class); + assertThat(resultI).isEqualTo(null); + assertThat(resultC).isEqualTo(null); + assertThat(resultC).isEqualTo(null); + } + + @Test + public void realLiteral() throws Exception { + expression = parser.parseExpression("3.4d"); + double resultI = expression.getValue(new TestClass1(), Double.TYPE); + assertCanCompile(expression); + double resultC = expression.getValue(new TestClass1(), Double.TYPE); + assertThat(resultI).isCloseTo(3.4d, within(0.1d)); + + assertThat(resultC).isCloseTo(3.4d, within(0.1d)); + + assertThat(expression.getValue()).isEqualTo(3.4d); + } + + @SuppressWarnings("rawtypes") + @Test + public void inlineList() throws Exception { + expression = parser.parseExpression("'abcde'.substring({1,3,4}[0])"); + Object o = expression.getValue(); + assertThat(o).isEqualTo("bcde"); + assertCanCompile(expression); + o = expression.getValue(); + assertThat(o).isEqualTo("bcde"); + + expression = parser.parseExpression("{'abc','def'}"); + List l = (List) expression.getValue(); + assertThat(l.toString()).isEqualTo("[abc, def]"); + assertCanCompile(expression); + l = (List) expression.getValue(); + assertThat(l.toString()).isEqualTo("[abc, def]"); + + expression = parser.parseExpression("{'abc','def'}[0]"); + o = expression.getValue(); + assertThat(o).isEqualTo("abc"); + assertCanCompile(expression); + o = expression.getValue(); + assertThat(o).isEqualTo("abc"); + + expression = parser.parseExpression("{'abcde','ijklm'}[0].substring({1,3,4}[0])"); + o = expression.getValue(); + assertThat(o).isEqualTo("bcde"); + assertCanCompile(expression); + o = expression.getValue(); + assertThat(o).isEqualTo("bcde"); + + expression = parser.parseExpression("{'abcde','ijklm'}[0].substring({1,3,4}[0],{1,3,4}[1])"); + o = expression.getValue(); + assertThat(o).isEqualTo("bc"); + assertCanCompile(expression); + o = expression.getValue(); + assertThat(o).isEqualTo("bc"); + } + + @SuppressWarnings("rawtypes") + @Test + public void nestedInlineLists() throws Exception { + Object o = null; + + expression = parser.parseExpression("{{1,2,3},{4,5,6},{7,8,9}}"); + o = expression.getValue(); + assertThat(o.toString()).isEqualTo("[[1, 2, 3], [4, 5, 6], [7, 8, 9]]"); + assertCanCompile(expression); + o = expression.getValue(); + assertThat(o.toString()).isEqualTo("[[1, 2, 3], [4, 5, 6], [7, 8, 9]]"); + + expression = parser.parseExpression("{{1,2,3},{4,5,6},{7,8,9}}.toString()"); + o = expression.getValue(); + assertThat(o).isEqualTo("[[1, 2, 3], [4, 5, 6], [7, 8, 9]]"); + assertCanCompile(expression); + o = expression.getValue(); + assertThat(o).isEqualTo("[[1, 2, 3], [4, 5, 6], [7, 8, 9]]"); + + expression = parser.parseExpression("{{1,2,3},{4,5,6},{7,8,9}}[1][0]"); + o = expression.getValue(); + assertThat(o).isEqualTo(4); + assertCanCompile(expression); + o = expression.getValue(); + assertThat(o).isEqualTo(4); + + expression = parser.parseExpression("{{1,2,3},'abc',{7,8,9}}[1]"); + o = expression.getValue(); + assertThat(o).isEqualTo("abc"); + assertCanCompile(expression); + o = expression.getValue(); + assertThat(o).isEqualTo("abc"); + + expression = parser.parseExpression("'abcde'.substring({{1,3},1,3,4}[0][1])"); + o = expression.getValue(); + assertThat(o).isEqualTo("de"); + assertCanCompile(expression); + o = expression.getValue(); + assertThat(o).isEqualTo("de"); + + expression = parser.parseExpression("'abcde'.substring({{1,3},1,3,4}[1])"); + o = expression.getValue(); + assertThat(o).isEqualTo("bcde"); + assertCanCompile(expression); + o = expression.getValue(); + assertThat(o).isEqualTo("bcde"); + + expression = parser.parseExpression("{'abc',{'def','ghi'}}"); + List l = (List) expression.getValue(); + assertThat(l.toString()).isEqualTo("[abc, [def, ghi]]"); + assertCanCompile(expression); + l = (List) expression.getValue(); + assertThat(l.toString()).isEqualTo("[abc, [def, ghi]]"); + + expression = parser.parseExpression("{'abcde',{'ijklm','nopqr'}}[0].substring({1,3,4}[0])"); + o = expression.getValue(); + assertThat(o).isEqualTo("bcde"); + assertCanCompile(expression); + o = expression.getValue(); + assertThat(o).isEqualTo("bcde"); + + expression = parser.parseExpression("{'abcde',{'ijklm','nopqr'}}[1][0].substring({1,3,4}[0])"); + o = expression.getValue(); + assertThat(o).isEqualTo("jklm"); + assertCanCompile(expression); + o = expression.getValue(); + assertThat(o).isEqualTo("jklm"); + + expression = parser.parseExpression("{'abcde',{'ijklm','nopqr'}}[1][1].substring({1,3,4}[0],{1,3,4}[1])"); + o = expression.getValue(); + assertThat(o).isEqualTo("op"); + assertCanCompile(expression); + o = expression.getValue(); + assertThat(o).isEqualTo("op"); + } + + @Test + public void intLiteral() throws Exception { + expression = parser.parseExpression("42"); + int resultI = expression.getValue(new TestClass1(), Integer.TYPE); + assertCanCompile(expression); + int resultC = expression.getValue(new TestClass1(), Integer.TYPE); + assertThat(resultI).isEqualTo(42); + assertThat(resultC).isEqualTo(42); + + expression = parser.parseExpression("T(Integer).valueOf(42)"); + expression.getValue(Integer.class); + assertCanCompile(expression); + assertThat(expression.getValue(Integer.class)).isEqualTo(42); + + // Code gen is different for -1 .. 6 because there are bytecode instructions specifically for those values + + // Not an int literal but an opminus with one operand: + // expression = parser.parseExpression("-1"); + // assertCanCompile(expression); + // assertEquals(-1, expression.getValue()); + expression = parser.parseExpression("0"); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(0); + expression = parser.parseExpression("2"); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(2); + expression = parser.parseExpression("7"); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(7); + } + + @Test + public void longLiteral() throws Exception { + expression = parser.parseExpression("99L"); + long resultI = expression.getValue(new TestClass1(), Long.TYPE); + assertCanCompile(expression); + long resultC = expression.getValue(new TestClass1(), Long.TYPE); + assertThat(resultI).isEqualTo(99L); + assertThat(resultC).isEqualTo(99L); + } + + @Test + public void booleanLiteral() throws Exception { + expression = parser.parseExpression("true"); + boolean resultI = expression.getValue(1, Boolean.TYPE); + assertThat(resultI).isEqualTo(true); + assertThat(SpelCompiler.compile(expression)).isTrue(); + boolean resultC = expression.getValue(1, Boolean.TYPE); + assertThat(resultC).isEqualTo(true); + + expression = parser.parseExpression("false"); + resultI = expression.getValue(1, Boolean.TYPE); + assertThat(resultI).isEqualTo(false); + assertThat(SpelCompiler.compile(expression)).isTrue(); + resultC = expression.getValue(1, Boolean.TYPE); + assertThat(resultC).isEqualTo(false); + } + + @Test + public void floatLiteral() throws Exception { + expression = parser.parseExpression("3.4f"); + float resultI = expression.getValue(new TestClass1(), Float.TYPE); + assertCanCompile(expression); + float resultC = expression.getValue(new TestClass1(), Float.TYPE); + assertThat(resultI).isCloseTo(3.4f, within(0.1f)); + + assertThat(resultC).isCloseTo(3.4f, within(0.1f)); + + assertThat(expression.getValue()).isEqualTo(3.4f); + } + + @Test + public void opOr() throws Exception { + Expression expression = parser.parseExpression("false or false"); + boolean resultI = expression.getValue(1, Boolean.TYPE); + SpelCompiler.compile(expression); + boolean resultC = expression.getValue(1, Boolean.TYPE); + assertThat(resultI).isEqualTo(false); + assertThat(resultC).isEqualTo(false); + + expression = parser.parseExpression("false or true"); + resultI = expression.getValue(1, Boolean.TYPE); + assertCanCompile(expression); + resultC = expression.getValue(1, Boolean.TYPE); + assertThat(resultI).isEqualTo(true); + assertThat(resultC).isEqualTo(true); + + expression = parser.parseExpression("true or false"); + resultI = expression.getValue(1, Boolean.TYPE); + assertCanCompile(expression); + resultC = expression.getValue(1, Boolean.TYPE); + assertThat(resultI).isEqualTo(true); + assertThat(resultC).isEqualTo(true); + + expression = parser.parseExpression("true or true"); + resultI = expression.getValue(1, Boolean.TYPE); + assertCanCompile(expression); + resultC = expression.getValue(1, Boolean.TYPE); + assertThat(resultI).isEqualTo(true); + assertThat(resultC).isEqualTo(true); + + TestClass4 tc = new TestClass4(); + expression = parser.parseExpression("getfalse() or gettrue()"); + resultI = expression.getValue(tc, Boolean.TYPE); + assertCanCompile(expression); + resultC = expression.getValue(tc, Boolean.TYPE); + assertThat(resultI).isEqualTo(true); + assertThat(resultC).isEqualTo(true); + + // Can't compile this as we aren't going down the getfalse() branch in our evaluation + expression = parser.parseExpression("gettrue() or getfalse()"); + resultI = expression.getValue(tc, Boolean.TYPE); + assertCantCompile(expression); + + expression = parser.parseExpression("getA() or getB()"); + tc.a = true; + tc.b = true; + resultI = expression.getValue(tc, Boolean.TYPE); + assertCantCompile(expression); // Haven't yet been into second branch + tc.a = false; + tc.b = true; + resultI = expression.getValue(tc, Boolean.TYPE); + assertCanCompile(expression); // Now been down both + assertThat(resultI).isTrue(); + + boolean b = false; + expression = parse("#root or #root"); + Object resultI2 = expression.getValue(b); + assertCanCompile(expression); + assertThat((boolean) (Boolean) resultI2).isFalse(); + assertThat((boolean) (Boolean) expression.getValue(b)).isFalse(); + } + + @Test + public void opAnd() throws Exception { + Expression expression = parser.parseExpression("false and false"); + boolean resultI = expression.getValue(1, Boolean.TYPE); + SpelCompiler.compile(expression); + boolean resultC = expression.getValue(1, Boolean.TYPE); + assertThat(resultI).isEqualTo(false); + assertThat(resultC).isEqualTo(false); + + expression = parser.parseExpression("false and true"); + resultI = expression.getValue(1, Boolean.TYPE); + SpelCompiler.compile(expression); + resultC = expression.getValue(1, Boolean.TYPE); + assertThat(resultI).isEqualTo(false); + assertThat(resultC).isEqualTo(false); + + expression = parser.parseExpression("true and false"); + resultI = expression.getValue(1, Boolean.TYPE); + SpelCompiler.compile(expression); + resultC = expression.getValue(1, Boolean.TYPE); + assertThat(resultI).isEqualTo(false); + assertThat(resultC).isEqualTo(false); + + expression = parser.parseExpression("true and true"); + resultI = expression.getValue(1, Boolean.TYPE); + SpelCompiler.compile(expression); + resultC = expression.getValue(1, Boolean.TYPE); + assertThat(resultI).isEqualTo(true); + assertThat(resultC).isEqualTo(true); + + TestClass4 tc = new TestClass4(); + + // Can't compile this as we aren't going down the gettrue() branch in our evaluation + expression = parser.parseExpression("getfalse() and gettrue()"); + resultI = expression.getValue(tc, Boolean.TYPE); + assertCantCompile(expression); + + expression = parser.parseExpression("getA() and getB()"); + tc.a = false; + tc.b = false; + resultI = expression.getValue(tc, Boolean.TYPE); + assertCantCompile(expression); // Haven't yet been into second branch + tc.a = true; + tc.b = false; + resultI = expression.getValue(tc, Boolean.TYPE); + assertCanCompile(expression); // Now been down both + assertThat(resultI).isFalse(); + tc.a = true; + tc.b = true; + resultI = expression.getValue(tc, Boolean.TYPE); + assertThat(resultI).isTrue(); + + boolean b = true; + expression = parse("#root and #root"); + Object resultI2 = expression.getValue(b); + assertCanCompile(expression); + assertThat((boolean) (Boolean) resultI2).isTrue(); + assertThat((boolean) (Boolean) expression.getValue(b)).isTrue(); + } + + @Test + public void operatorNot() throws Exception { + expression = parse("!true"); + assertThat(expression.getValue()).isEqualTo(false); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(false); + + expression = parse("!false"); + assertThat(expression.getValue()).isEqualTo(true); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(true); + + boolean b = true; + expression = parse("!#root"); + assertThat(expression.getValue(b)).isEqualTo(false); + assertCanCompile(expression); + assertThat(expression.getValue(b)).isEqualTo(false); + + b = false; + expression = parse("!#root"); + assertThat(expression.getValue(b)).isEqualTo(true); + assertCanCompile(expression); + assertThat(expression.getValue(b)).isEqualTo(true); + } + + @Test + public void ternary() throws Exception { + Expression expression = parser.parseExpression("true?'a':'b'"); + String resultI = expression.getValue(String.class); + assertCanCompile(expression); + String resultC = expression.getValue(String.class); + assertThat(resultI).isEqualTo("a"); + assertThat(resultC).isEqualTo("a"); + + expression = parser.parseExpression("false?'a':'b'"); + resultI = expression.getValue(String.class); + assertCanCompile(expression); + resultC = expression.getValue(String.class); + assertThat(resultI).isEqualTo("b"); + assertThat(resultC).isEqualTo("b"); + + expression = parser.parseExpression("false?1:'b'"); + // All literals so we can do this straight away + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo("b"); + + boolean root = true; + expression = parser.parseExpression("(#root and true)?T(Integer).valueOf(1):T(Long).valueOf(3L)"); + assertThat(expression.getValue(root)).isEqualTo(1); + assertCantCompile(expression); // Have not gone down false branch + root = false; + assertThat(expression.getValue(root)).isEqualTo(3L); + assertCanCompile(expression); + assertThat(expression.getValue(root)).isEqualTo(3L); + root = true; + assertThat(expression.getValue(root)).isEqualTo(1); + } + + @Test + public void ternaryWithBooleanReturn_SPR12271() { + expression = parser.parseExpression("T(Boolean).TRUE?'abc':'def'"); + assertThat(expression.getValue()).isEqualTo("abc"); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo("abc"); + + expression = parser.parseExpression("T(Boolean).FALSE?'abc':'def'"); + assertThat(expression.getValue()).isEqualTo("def"); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo("def"); + } + + @Test + public void nullsafeFieldPropertyDereferencing_SPR16489() throws Exception { + FooObjectHolder foh = new FooObjectHolder(); + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setRootObject(foh); + + // First non compiled: + SpelExpression expression = (SpelExpression) parser.parseExpression("foo?.object"); + assertThat(expression.getValue(context)).isEqualTo("hello"); + foh.foo = null; + assertThat(expression.getValue(context)).isNull(); + + // Now revert state of foh and try compiling it: + foh.foo = new FooObject(); + assertThat(expression.getValue(context)).isEqualTo("hello"); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo("hello"); + foh.foo = null; + assertThat(expression.getValue(context)).isNull(); + + // Static references + expression = (SpelExpression) parser.parseExpression("#var?.propertya"); + context.setVariable("var", StaticsHelper.class); + assertThat(expression.getValue(context).toString()).isEqualTo("sh"); + context.setVariable("var", null); + assertThat(expression.getValue(context)).isNull(); + assertCanCompile(expression); + context.setVariable("var", StaticsHelper.class); + assertThat(expression.getValue(context).toString()).isEqualTo("sh"); + context.setVariable("var", null); + assertThat(expression.getValue(context)).isNull(); + + // Single size primitive (boolean) + expression = (SpelExpression) parser.parseExpression("#var?.a"); + context.setVariable("var", new TestClass4()); + assertThat((boolean) (Boolean) expression.getValue(context)).isFalse(); + context.setVariable("var", null); + assertThat(expression.getValue(context)).isNull(); + assertCanCompile(expression); + context.setVariable("var", new TestClass4()); + assertThat((boolean) (Boolean) expression.getValue(context)).isFalse(); + context.setVariable("var", null); + assertThat(expression.getValue(context)).isNull(); + + // Double slot primitives + expression = (SpelExpression) parser.parseExpression("#var?.four"); + context.setVariable("var", new Three()); + assertThat(expression.getValue(context).toString()).isEqualTo("0.04"); + context.setVariable("var", null); + assertThat(expression.getValue(context)).isNull(); + assertCanCompile(expression); + context.setVariable("var", new Three()); + assertThat(expression.getValue(context).toString()).isEqualTo("0.04"); + context.setVariable("var", null); + assertThat(expression.getValue(context)).isNull(); + } + + @Test + public void nullsafeMethodChaining_SPR16489() throws Exception { + FooObjectHolder foh = new FooObjectHolder(); + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setRootObject(foh); + + // First non compiled: + SpelExpression expression = (SpelExpression) parser.parseExpression("getFoo()?.getObject()"); + assertThat(expression.getValue(context)).isEqualTo("hello"); + foh.foo = null; + assertThat(expression.getValue(context)).isNull(); + assertCanCompile(expression); + foh.foo = new FooObject(); + assertThat(expression.getValue(context)).isEqualTo("hello"); + foh.foo = null; + assertThat(expression.getValue(context)).isNull(); + + // Static method references + expression = (SpelExpression) parser.parseExpression("#var?.methoda()"); + context.setVariable("var", StaticsHelper.class); + assertThat(expression.getValue(context).toString()).isEqualTo("sh"); + context.setVariable("var", null); + assertThat(expression.getValue(context)).isNull(); + assertCanCompile(expression); + context.setVariable("var", StaticsHelper.class); + assertThat(expression.getValue(context).toString()).isEqualTo("sh"); + context.setVariable("var", null); + assertThat(expression.getValue(context)).isNull(); + + // Nullsafe guard on expression element evaluating to primitive/null + expression = (SpelExpression) parser.parseExpression("#var?.intValue()"); + context.setVariable("var", 4); + assertThat(expression.getValue(context).toString()).isEqualTo("4"); + context.setVariable("var", null); + assertThat(expression.getValue(context)).isNull(); + assertCanCompile(expression); + context.setVariable("var", 4); + assertThat(expression.getValue(context).toString()).isEqualTo("4"); + context.setVariable("var", null); + assertThat(expression.getValue(context)).isNull(); + + // Nullsafe guard on expression element evaluating to primitive/null + expression = (SpelExpression) parser.parseExpression("#var?.booleanValue()"); + context.setVariable("var", false); + assertThat(expression.getValue(context).toString()).isEqualTo("false"); + context.setVariable("var", null); + assertThat(expression.getValue(context)).isNull(); + assertCanCompile(expression); + context.setVariable("var", false); + assertThat(expression.getValue(context).toString()).isEqualTo("false"); + context.setVariable("var", null); + assertThat(expression.getValue(context)).isNull(); + + // Nullsafe guard on expression element evaluating to primitive/null + expression = (SpelExpression) parser.parseExpression("#var?.booleanValue()"); + context.setVariable("var", true); + assertThat(expression.getValue(context).toString()).isEqualTo("true"); + context.setVariable("var", null); + assertThat(expression.getValue(context)).isNull(); + assertCanCompile(expression); + context.setVariable("var", true); + assertThat(expression.getValue(context).toString()).isEqualTo("true"); + context.setVariable("var", null); + assertThat(expression.getValue(context)).isNull(); + + // Nullsafe guard on expression element evaluating to primitive/null + expression = (SpelExpression) parser.parseExpression("#var?.longValue()"); + context.setVariable("var", 5L); + assertThat(expression.getValue(context).toString()).isEqualTo("5"); + context.setVariable("var", null); + assertThat(expression.getValue(context)).isNull(); + assertCanCompile(expression); + context.setVariable("var", 5L); + assertThat(expression.getValue(context).toString()).isEqualTo("5"); + context.setVariable("var", null); + assertThat(expression.getValue(context)).isNull(); + + // Nullsafe guard on expression element evaluating to primitive/null + expression = (SpelExpression) parser.parseExpression("#var?.floatValue()"); + context.setVariable("var", 3f); + assertThat(expression.getValue(context).toString()).isEqualTo("3.0"); + context.setVariable("var", null); + assertThat(expression.getValue(context)).isNull(); + assertCanCompile(expression); + context.setVariable("var", 3f); + assertThat(expression.getValue(context).toString()).isEqualTo("3.0"); + context.setVariable("var", null); + assertThat(expression.getValue(context)).isNull(); + + // Nullsafe guard on expression element evaluating to primitive/null + expression = (SpelExpression) parser.parseExpression("#var?.shortValue()"); + context.setVariable("var", (short)8); + assertThat(expression.getValue(context).toString()).isEqualTo("8"); + context.setVariable("var", null); + assertThat(expression.getValue(context)).isNull(); + assertCanCompile(expression); + context.setVariable("var", (short)8); + assertThat(expression.getValue(context).toString()).isEqualTo("8"); + context.setVariable("var", null); + assertThat(expression.getValue(context)).isNull(); + } + + @Test + public void elvis() throws Exception { + Expression expression = parser.parseExpression("'a'?:'b'"); + String resultI = expression.getValue(String.class); + assertCanCompile(expression); + String resultC = expression.getValue(String.class); + assertThat(resultI).isEqualTo("a"); + assertThat(resultC).isEqualTo("a"); + + expression = parser.parseExpression("null?:'a'"); + resultI = expression.getValue(String.class); + assertCanCompile(expression); + resultC = expression.getValue(String.class); + assertThat(resultI).isEqualTo("a"); + assertThat(resultC).isEqualTo("a"); + + String s = "abc"; + expression = parser.parseExpression("#root?:'b'"); + assertCantCompile(expression); + resultI = expression.getValue(s, String.class); + assertThat(resultI).isEqualTo("abc"); + assertCanCompile(expression); + } + + @Test + public void variableReference_root() throws Exception { + String s = "hello"; + Expression expression = parser.parseExpression("#root"); + String resultI = expression.getValue(s, String.class); + assertCanCompile(expression); + String resultC = expression.getValue(s, String.class); + assertThat(resultI).isEqualTo(s); + assertThat(resultC).isEqualTo(s); + + expression = parser.parseExpression("#root"); + int i = (Integer) expression.getValue(42); + assertThat(i).isEqualTo(42); + assertCanCompile(expression); + i = (Integer) expression.getValue(42); + assertThat(i).isEqualTo(42); + } + + public static String concat(String a, String b) { + return a+b; + } + + public static String join(String...strings) { + StringBuilder buf = new StringBuilder(); + for (String string: strings) { + buf.append(string); + } + return buf.toString(); + } + + @Test + public void compiledExpressionShouldWorkWhenUsingCustomFunctionWithVarargs() throws Exception { + StandardEvaluationContext context = null; + + // Here the target method takes Object... and we are passing a string + expression = parser.parseExpression("#doFormat('hey %s', 'there')"); + context = new StandardEvaluationContext(); + context.registerFunction("doFormat", + DelegatingStringFormat.class.getDeclaredMethod("format", String.class, Object[].class)); + ((SpelExpression) expression).setEvaluationContext(context); + + assertThat(expression.getValue(String.class)).isEqualTo("hey there"); + assertThat(((SpelNodeImpl) ((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(String.class)).isEqualTo("hey there"); + + expression = parser.parseExpression("#doFormat([0], 'there')"); + context = new StandardEvaluationContext(new Object[] {"hey %s"}); + context.registerFunction("doFormat", + DelegatingStringFormat.class.getDeclaredMethod("format", String.class, Object[].class)); + ((SpelExpression) expression).setEvaluationContext(context); + + assertThat(expression.getValue(String.class)).isEqualTo("hey there"); + assertThat(((SpelNodeImpl) ((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(String.class)).isEqualTo("hey there"); + + expression = parser.parseExpression("#doFormat([0], #arg)"); + context = new StandardEvaluationContext(new Object[] {"hey %s"}); + context.registerFunction("doFormat", + DelegatingStringFormat.class.getDeclaredMethod("format", String.class, Object[].class)); + context.setVariable("arg", "there"); + ((SpelExpression) expression).setEvaluationContext(context); + + assertThat(expression.getValue(String.class)).isEqualTo("hey there"); + assertThat(((SpelNodeImpl) ((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(String.class)).isEqualTo("hey there"); + } + + @Test + public void functionReference() throws Exception { + EvaluationContext ctx = new StandardEvaluationContext(); + Method m = getClass().getDeclaredMethod("concat", String.class, String.class); + ctx.setVariable("concat",m); + + expression = parser.parseExpression("#concat('a','b')"); + assertThat(expression.getValue(ctx)).isEqualTo("ab"); + assertCanCompile(expression); + assertThat(expression.getValue(ctx)).isEqualTo("ab"); + + expression = parser.parseExpression("#concat(#concat('a','b'),'c').charAt(1)"); + assertThat(expression.getValue(ctx)).isEqualTo('b'); + assertCanCompile(expression); + assertThat(expression.getValue(ctx)).isEqualTo('b'); + + expression = parser.parseExpression("#concat(#a,#b)"); + ctx.setVariable("a", "foo"); + ctx.setVariable("b", "bar"); + assertThat(expression.getValue(ctx)).isEqualTo("foobar"); + assertCanCompile(expression); + assertThat(expression.getValue(ctx)).isEqualTo("foobar"); + ctx.setVariable("b", "boo"); + assertThat(expression.getValue(ctx)).isEqualTo("fooboo"); + + m = Math.class.getDeclaredMethod("pow", Double.TYPE, Double.TYPE); + ctx.setVariable("kapow",m); + expression = parser.parseExpression("#kapow(2.0d,2.0d)"); + assertThat(expression.getValue(ctx).toString()).isEqualTo("4.0"); + assertCanCompile(expression); + assertThat(expression.getValue(ctx).toString()).isEqualTo("4.0"); + } + + @Test + public void functionReferenceVisibility_SPR12359() throws Exception { + // Confirms visibility of what is being called. + StandardEvaluationContext context = new StandardEvaluationContext(new Object[] {"1"}); + context.registerFunction("doCompare", SomeCompareMethod.class.getDeclaredMethod( + "compare", Object.class, Object.class)); + context.setVariable("arg", "2"); + // type nor method are public + expression = parser.parseExpression("#doCompare([0],#arg)"); + assertThat(expression.getValue(context, Integer.class).toString()).isEqualTo("-1"); + assertCantCompile(expression); + + // type not public but method is + context = new StandardEvaluationContext(new Object[] {"1"}); + context.registerFunction("doCompare", SomeCompareMethod.class.getDeclaredMethod( + "compare2", Object.class, Object.class)); + context.setVariable("arg", "2"); + expression = parser.parseExpression("#doCompare([0],#arg)"); + assertThat(expression.getValue(context, Integer.class).toString()).isEqualTo("-1"); + assertCantCompile(expression); + } + + @Test + public void functionReferenceNonCompilableArguments_SPR12359() throws Exception { + StandardEvaluationContext context = new StandardEvaluationContext(new Object[] {"1"}); + context.registerFunction("negate", SomeCompareMethod2.class.getDeclaredMethod( + "negate", Integer.TYPE)); + context.setVariable("arg", "2"); + int[] ints = new int[] {1,2,3}; + context.setVariable("ints",ints); + + expression = parser.parseExpression("#negate(#ints.?[#this<2][0])"); + assertThat(expression.getValue(context, Integer.class).toString()).isEqualTo("-1"); + // Selection isn't compilable. + assertThat(((SpelNodeImpl)((SpelExpression) expression).getAST()).isCompilable()).isFalse(); + } + + @Test + public void functionReferenceVarargs_SPR12359() throws Exception { + StandardEvaluationContext context = new StandardEvaluationContext(); + context.registerFunction("append", + SomeCompareMethod2.class.getDeclaredMethod("append", String[].class)); + context.registerFunction("append2", + SomeCompareMethod2.class.getDeclaredMethod("append2", Object[].class)); + context.registerFunction("append3", + SomeCompareMethod2.class.getDeclaredMethod("append3", String[].class)); + context.registerFunction("append4", + SomeCompareMethod2.class.getDeclaredMethod("append4", String.class, String[].class)); + context.registerFunction("appendChar", + SomeCompareMethod2.class.getDeclaredMethod("appendChar", char[].class)); + context.registerFunction("sum", + SomeCompareMethod2.class.getDeclaredMethod("sum", int[].class)); + context.registerFunction("sumDouble", + SomeCompareMethod2.class.getDeclaredMethod("sumDouble", double[].class)); + context.registerFunction("sumFloat", + SomeCompareMethod2.class.getDeclaredMethod("sumFloat", float[].class)); + context.setVariable("stringArray", new String[] {"x","y","z"}); + context.setVariable("intArray", new int[] {5,6,9}); + context.setVariable("doubleArray", new double[] {5.0d,6.0d,9.0d}); + context.setVariable("floatArray", new float[] {5.0f,6.0f,9.0f}); + + expression = parser.parseExpression("#append('a','b','c')"); + assertThat(expression.getValue(context).toString()).isEqualTo("abc"); + assertThat(((SpelNodeImpl)((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(context).toString()).isEqualTo("abc"); + + expression = parser.parseExpression("#append('a')"); + assertThat(expression.getValue(context).toString()).isEqualTo("a"); + assertThat(((SpelNodeImpl)((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(context).toString()).isEqualTo("a"); + + expression = parser.parseExpression("#append()"); + assertThat(expression.getValue(context).toString()).isEqualTo(""); + assertThat(((SpelNodeImpl)((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(context).toString()).isEqualTo(""); + + expression = parser.parseExpression("#append(#stringArray)"); + assertThat(expression.getValue(context).toString()).isEqualTo("xyz"); + assertThat(((SpelNodeImpl)((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(context).toString()).isEqualTo("xyz"); + + // This is a methodreference invocation, to compare with functionreference + expression = parser.parseExpression("append(#stringArray)"); + assertThat(expression.getValue(context, new SomeCompareMethod2()).toString()).isEqualTo("xyz"); + assertThat(((SpelNodeImpl)((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(context, new SomeCompareMethod2()).toString()).isEqualTo("xyz"); + + expression = parser.parseExpression("#append2('a','b','c')"); + assertThat(expression.getValue(context).toString()).isEqualTo("abc"); + assertThat(((SpelNodeImpl)((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(context).toString()).isEqualTo("abc"); + + expression = parser.parseExpression("append2('a','b')"); + assertThat(expression.getValue(context, new SomeCompareMethod2()).toString()).isEqualTo("ab"); + assertThat(((SpelNodeImpl)((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(context, new SomeCompareMethod2()).toString()).isEqualTo("ab"); + + expression = parser.parseExpression("#append2('a','b')"); + assertThat(expression.getValue(context).toString()).isEqualTo("ab"); + assertThat(((SpelNodeImpl)((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(context).toString()).isEqualTo("ab"); + + expression = parser.parseExpression("#append2()"); + assertThat(expression.getValue(context).toString()).isEqualTo(""); + assertThat(((SpelNodeImpl)((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(context).toString()).isEqualTo(""); + + expression = parser.parseExpression("#append3(#stringArray)"); + assertThat(expression.getValue(context, new SomeCompareMethod2()).toString()).isEqualTo("xyz"); + assertThat(((SpelNodeImpl)((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(context, new SomeCompareMethod2()).toString()).isEqualTo("xyz"); + + // TODO fails due to conversionservice handling of String[] to Object... + // expression = parser.parseExpression("#append2(#stringArray)"); + // assertEquals("xyz", expression.getValue(context).toString()); + // assertTrue(((SpelNodeImpl)((SpelExpression) expression).getAST()).isCompilable()); + // assertCanCompile(expression); + // assertEquals("xyz", expression.getValue(context).toString()); + + expression = parser.parseExpression("#sum(1,2,3)"); + assertThat(expression.getValue(context)).isEqualTo(6); + assertThat(((SpelNodeImpl)((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo(6); + + expression = parser.parseExpression("#sum(2)"); + assertThat(expression.getValue(context)).isEqualTo(2); + assertThat(((SpelNodeImpl)((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo(2); + + expression = parser.parseExpression("#sum()"); + assertThat(expression.getValue(context)).isEqualTo(0); + assertThat(((SpelNodeImpl)((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo(0); + + expression = parser.parseExpression("#sum(#intArray)"); + assertThat(expression.getValue(context)).isEqualTo(20); + assertThat(((SpelNodeImpl)((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo(20); + + expression = parser.parseExpression("#sumDouble(1.0d,2.0d,3.0d)"); + assertThat(expression.getValue(context)).isEqualTo(6); + assertThat(((SpelNodeImpl)((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo(6); + + expression = parser.parseExpression("#sumDouble(2.0d)"); + assertThat(expression.getValue(context)).isEqualTo(2); + assertThat(((SpelNodeImpl)((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo(2); + + expression = parser.parseExpression("#sumDouble()"); + assertThat(expression.getValue(context)).isEqualTo(0); + assertThat(((SpelNodeImpl)((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo(0); + + expression = parser.parseExpression("#sumDouble(#doubleArray)"); + assertThat(expression.getValue(context)).isEqualTo(20); + assertThat(((SpelNodeImpl)((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo(20); + + expression = parser.parseExpression("#sumFloat(1.0f,2.0f,3.0f)"); + assertThat(expression.getValue(context)).isEqualTo(6); + assertThat(((SpelNodeImpl)((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo(6); + + expression = parser.parseExpression("#sumFloat(2.0f)"); + assertThat(expression.getValue(context)).isEqualTo(2); + assertThat(((SpelNodeImpl)((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo(2); + + expression = parser.parseExpression("#sumFloat()"); + assertThat(expression.getValue(context)).isEqualTo(0); + assertThat(((SpelNodeImpl)((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo(0); + + expression = parser.parseExpression("#sumFloat(#floatArray)"); + assertThat(expression.getValue(context)).isEqualTo(20); + assertThat(((SpelNodeImpl)((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo(20); + + + expression = parser.parseExpression("#appendChar('abc'.charAt(0),'abc'.charAt(1))"); + assertThat(expression.getValue(context)).isEqualTo("ab"); + assertThat(((SpelNodeImpl)((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo("ab"); + + + expression = parser.parseExpression("#append4('a','b','c')"); + assertThat(expression.getValue(context).toString()).isEqualTo("a::bc"); + assertThat(((SpelNodeImpl)((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(context).toString()).isEqualTo("a::bc"); + + expression = parser.parseExpression("#append4('a','b')"); + assertThat(expression.getValue(context).toString()).isEqualTo("a::b"); + assertThat(((SpelNodeImpl)((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(context).toString()).isEqualTo("a::b"); + + expression = parser.parseExpression("#append4('a')"); + assertThat(expression.getValue(context).toString()).isEqualTo("a::"); + assertThat(((SpelNodeImpl)((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(context).toString()).isEqualTo("a::"); + + expression = parser.parseExpression("#append4('a',#stringArray)"); + assertThat(expression.getValue(context).toString()).isEqualTo("a::xyz"); + assertThat(((SpelNodeImpl)((SpelExpression) expression).getAST()).isCompilable()).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(context).toString()).isEqualTo("a::xyz"); + } + + @Test + public void functionReferenceVarargs() throws Exception { + EvaluationContext ctx = new StandardEvaluationContext(); + Method m = getClass().getDeclaredMethod("join", String[].class); + ctx.setVariable("join", m); + expression = parser.parseExpression("#join('a','b','c')"); + assertThat(expression.getValue(ctx)).isEqualTo("abc"); + assertCanCompile(expression); + assertThat(expression.getValue(ctx)).isEqualTo("abc"); + } + + @Test + public void variableReference_userDefined() throws Exception { + 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"); + 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'); + ctx.setVariable("target", 42); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + expression.getValue(ctx)) + .withCauseInstanceOf(ClassCastException.class); + } + + @Test + public void opLt() throws Exception { + expression = parse("3.0d < 4.0d"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + expression = parse("3446.0d < 1123.0d"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + + expression = parse("3 < 1"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + expression = parse("2 < 4"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("3.0f < 1.0f"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + expression = parse("1.0f < 5.0f"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("30L < 30L"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + expression = parse("15L < 20L"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + // Differing types of number, not yet supported + expression = parse("1 < 3.0d"); + assertCantCompile(expression); + + expression = parse("T(Integer).valueOf(3) < 4"); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("T(Integer).valueOf(3) < T(Integer).valueOf(3)"); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + + expression = parse("5 < T(Integer).valueOf(3)"); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + } + + @Test + public void opLe() throws Exception { + expression = parse("3.0d <= 4.0d"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + expression = parse("3446.0d <= 1123.0d"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + expression = parse("3446.0d <= 3446.0d"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("3 <= 1"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + expression = parse("2 <= 4"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + expression = parse("3 <= 3"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("3.0f <= 1.0f"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + expression = parse("1.0f <= 5.0f"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + expression = parse("2.0f <= 2.0f"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("30L <= 30L"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + expression = parse("15L <= 20L"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + // Differing types of number, not yet supported + expression = parse("1 <= 3.0d"); + assertCantCompile(expression); + + expression = parse("T(Integer).valueOf(3) <= 4"); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("T(Integer).valueOf(3) <= T(Integer).valueOf(3)"); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("5 <= T(Integer).valueOf(3)"); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + } + + @Test + public void opGt() throws Exception { + expression = parse("3.0d > 4.0d"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + expression = parse("3446.0d > 1123.0d"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("3 > 1"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + expression = parse("2 > 4"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + + expression = parse("3.0f > 1.0f"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + expression = parse("1.0f > 5.0f"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + + expression = parse("30L > 30L"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + expression = parse("15L > 20L"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + + // Differing types of number, not yet supported + expression = parse("1 > 3.0d"); + assertCantCompile(expression); + + expression = parse("T(Integer).valueOf(3) > 4"); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + + expression = parse("T(Integer).valueOf(3) > T(Integer).valueOf(3)"); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + + expression = parse("5 > T(Integer).valueOf(3)"); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + } + + @Test + public void opGe() throws Exception { + expression = parse("3.0d >= 4.0d"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + expression = parse("3446.0d >= 1123.0d"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + expression = parse("3446.0d >= 3446.0d"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("3 >= 1"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + expression = parse("2 >= 4"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + expression = parse("3 >= 3"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("3.0f >= 1.0f"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + expression = parse("1.0f >= 5.0f"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + expression = parse("3.0f >= 3.0f"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("40L >= 30L"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + expression = parse("15L >= 20L"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + expression = parse("30L >= 30L"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + // Differing types of number, not yet supported + expression = parse("1 >= 3.0d"); + assertCantCompile(expression); + + expression = parse("T(Integer).valueOf(3) >= 4"); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + + expression = parse("T(Integer).valueOf(3) >= T(Integer).valueOf(3)"); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("5 >= T(Integer).valueOf(3)"); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + } + + @Test + public void opEq() throws Exception { + String tvar = "35"; + expression = parse("#root == 35"); + assertThat((boolean) (Boolean) expression.getValue(tvar)).isFalse(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue(tvar)).isFalse(); + + expression = parse("35 == #root"); + expression.getValue(tvar); + assertThat((boolean) (Boolean) expression.getValue(tvar)).isFalse(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue(tvar)).isFalse(); + + TestClass7 tc7 = new TestClass7(); + expression = parse("property == 'UK'"); + assertThat((boolean) (Boolean) expression.getValue(tc7)).isTrue(); + TestClass7.property = null; + assertThat((boolean) (Boolean) expression.getValue(tc7)).isFalse(); + assertCanCompile(expression); + TestClass7.reset(); + assertThat((boolean) (Boolean) expression.getValue(tc7)).isTrue(); + TestClass7.property = "UK"; + assertThat((boolean) (Boolean) expression.getValue(tc7)).isTrue(); + TestClass7.reset(); + TestClass7.property = null; + assertThat((boolean) (Boolean) expression.getValue(tc7)).isFalse(); + expression = parse("property == null"); + assertThat((boolean) (Boolean) expression.getValue(tc7)).isTrue(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue(tc7)).isTrue(); + + expression = parse("3.0d == 4.0d"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + expression = parse("3446.0d == 3446.0d"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("3 == 1"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + expression = parse("3 == 3"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("3.0f == 1.0f"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + expression = parse("2.0f == 2.0f"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("30L == 30L"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + expression = parse("15L == 20L"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + + // number types are not the same + expression = parse("1 == 3.0d"); + assertCantCompile(expression); + + Double d = 3.0d; + expression = parse("#root==3.0d"); + assertThat((boolean) (Boolean) expression.getValue(d)).isTrue(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue(d)).isTrue(); + + Integer i = 3; + expression = parse("#root==3"); + assertThat((boolean) (Boolean) expression.getValue(i)).isTrue(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue(i)).isTrue(); + + Float f = 3.0f; + expression = parse("#root==3.0f"); + assertThat((boolean) (Boolean) expression.getValue(f)).isTrue(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue(f)).isTrue(); + + long l = 300L; + expression = parse("#root==300l"); + assertThat((boolean) (Boolean) expression.getValue(l)).isTrue(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue(l)).isTrue(); + + boolean b = true; + expression = parse("#root==true"); + assertThat((boolean) (Boolean) expression.getValue(b)).isTrue(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue(b)).isTrue(); + + expression = parse("T(Integer).valueOf(3) == 4"); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + + expression = parse("T(Integer).valueOf(3) == T(Integer).valueOf(3)"); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("5 == T(Integer).valueOf(3)"); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + + expression = parse("T(Float).valueOf(3.0f) == 4.0f"); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + + expression = parse("T(Float).valueOf(3.0f) == T(Float).valueOf(3.0f)"); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("5.0f == T(Float).valueOf(3.0f)"); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + + expression = parse("T(Long).valueOf(3L) == 4L"); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + + expression = parse("T(Long).valueOf(3L) == T(Long).valueOf(3L)"); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("5L == T(Long).valueOf(3L)"); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + + expression = parse("T(Double).valueOf(3.0d) == 4.0d"); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + + expression = parse("T(Double).valueOf(3.0d) == T(Double).valueOf(3.0d)"); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("5.0d == T(Double).valueOf(3.0d)"); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + + expression = parse("false == true"); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + + expression = parse("T(Boolean).valueOf('true') == T(Boolean).valueOf('true')"); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("T(Boolean).valueOf('true') == true"); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("false == T(Boolean).valueOf('false')"); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + } + + @Test + public void opNe() throws Exception { + expression = parse("3.0d != 4.0d"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + expression = parse("3446.0d != 3446.0d"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + + expression = parse("3 != 1"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + expression = parse("3 != 3"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + + expression = parse("3.0f != 1.0f"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + expression = parse("2.0f != 2.0f"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + + expression = parse("30L != 30L"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + expression = parse("15L != 20L"); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + // not compatible number types + expression = parse("1 != 3.0d"); + assertCantCompile(expression); + + expression = parse("T(Integer).valueOf(3) != 4"); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("T(Integer).valueOf(3) != T(Integer).valueOf(3)"); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + + expression = parse("5 != T(Integer).valueOf(3)"); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("T(Float).valueOf(3.0f) != 4.0f"); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("T(Float).valueOf(3.0f) != T(Float).valueOf(3.0f)"); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + + expression = parse("5.0f != T(Float).valueOf(3.0f)"); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("T(Long).valueOf(3L) != 4L"); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("T(Long).valueOf(3L) != T(Long).valueOf(3L)"); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + + expression = parse("5L != T(Long).valueOf(3L)"); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("T(Double).valueOf(3.0d) == 4.0d"); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + + expression = parse("T(Double).valueOf(3.0d) == T(Double).valueOf(3.0d)"); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("5.0d == T(Double).valueOf(3.0d)"); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + + expression = parse("false == true"); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isFalse(); + + expression = parse("T(Boolean).valueOf('true') == T(Boolean).valueOf('true')"); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("T(Boolean).valueOf('true') == true"); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + + expression = parse("false == T(Boolean).valueOf('false')"); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + assertCanCompile(expression); + assertThat((boolean) (Boolean) expression.getValue()).isTrue(); + } + + @Test + public void opNe_SPR14863() throws Exception { + SpelParserConfiguration configuration = + new SpelParserConfiguration(SpelCompilerMode.MIXED, ClassLoader.getSystemClassLoader()); + SpelExpressionParser parser = new SpelExpressionParser(configuration); + Expression expression = parser.parseExpression("data['my-key'] != 'my-value'"); + + Map data = new HashMap<>(); + data.put("my-key", new String("my-value")); + StandardEvaluationContext context = new StandardEvaluationContext(new MyContext(data)); + assertThat(expression.getValue(context, Boolean.class)).isFalse(); + assertCanCompile(expression); + ((SpelExpression) expression).compileExpression(); + assertThat(expression.getValue(context, Boolean.class)).isFalse(); + + List ls = new ArrayList(); + ls.add(new String("foo")); + context = new StandardEvaluationContext(ls); + expression = parse("get(0) != 'foo'"); + assertThat(expression.getValue(context, Boolean.class)).isFalse(); + assertCanCompile(expression); + assertThat(expression.getValue(context, Boolean.class)).isFalse(); + + ls.remove(0); + ls.add("goo"); + assertThat(expression.getValue(context, Boolean.class)).isTrue(); + } + + @Test + public void opEq_SPR14863() throws Exception { + // Exercise the comparator invocation code that runs in + // equalityCheck() (called from interpreted and compiled code) + expression = parser.parseExpression("#aa==#bb"); + StandardEvaluationContext sec = new StandardEvaluationContext(); + Apple aa = new Apple(1); + Apple bb = new Apple(2); + sec.setVariable("aa",aa); + sec.setVariable("bb",bb); + boolean b = expression.getValue(sec, Boolean.class); + // Verify what the expression caused aa to be compared to + assertThat(aa.gotComparedTo).isEqualTo(bb); + assertThat(b).isFalse(); + bb.setValue(1); + b = expression.getValue(sec, Boolean.class); + assertThat(aa.gotComparedTo).isEqualTo(bb); + assertThat(b).isTrue(); + + assertCanCompile(expression); + + // Similar test with compiled expression + aa = new Apple(99); + bb = new Apple(100); + sec.setVariable("aa",aa); + sec.setVariable("bb",bb); + b = expression.getValue(sec, Boolean.class); + assertThat(b).isFalse(); + assertThat(aa.gotComparedTo).isEqualTo(bb); + bb.setValue(99); + b = expression.getValue(sec, Boolean.class); + assertThat(b).isTrue(); + assertThat(aa.gotComparedTo).isEqualTo(bb); + + + List ls = new ArrayList(); + ls.add(new String("foo")); + StandardEvaluationContext context = new StandardEvaluationContext(ls); + expression = parse("get(0) == 'foo'"); + assertThat(expression.getValue(context, Boolean.class)).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(context, Boolean.class)).isTrue(); + + ls.remove(0); + ls.add("goo"); + assertThat(expression.getValue(context, Boolean.class)).isFalse(); + } + + @Test + public void opPlus() throws Exception { + expression = parse("2+2"); + expression.getValue(); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(4); + + expression = parse("2L+2L"); + expression.getValue(); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(4L); + + expression = parse("2.0f+2.0f"); + expression.getValue(); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(4.0f); + + expression = parse("3.0d+4.0d"); + expression.getValue(); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(7.0d); + + expression = parse("+1"); + expression.getValue(); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(1); + + expression = parse("+1L"); + expression.getValue(); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(1L); + + expression = parse("+1.5f"); + expression.getValue(); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(1.5f); + + expression = parse("+2.5d"); + expression.getValue(); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(2.5d); + + expression = parse("+T(Double).valueOf(2.5d)"); + expression.getValue(); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(2.5d); + + expression = parse("T(Integer).valueOf(2)+6"); + assertThat(expression.getValue()).isEqualTo(8); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(8); + + expression = parse("T(Integer).valueOf(1)+T(Integer).valueOf(3)"); + assertThat(expression.getValue()).isEqualTo(4); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(4); + + expression = parse("1+T(Integer).valueOf(3)"); + assertThat(expression.getValue()).isEqualTo(4); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(4); + + expression = parse("T(Float).valueOf(2.0f)+6"); + assertThat(expression.getValue()).isEqualTo(8.0f); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(8.0f); + + expression = parse("T(Float).valueOf(2.0f)+T(Float).valueOf(3.0f)"); + assertThat(expression.getValue()).isEqualTo(5.0f); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(5.0f); + + expression = parse("3L+T(Long).valueOf(4L)"); + assertThat(expression.getValue()).isEqualTo(7L); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(7L); + + expression = parse("T(Long).valueOf(2L)+6"); + assertThat(expression.getValue()).isEqualTo(8L); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(8L); + + expression = parse("T(Long).valueOf(2L)+T(Long).valueOf(3L)"); + assertThat(expression.getValue()).isEqualTo(5L); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(5L); + + expression = parse("1L+T(Long).valueOf(2L)"); + assertThat(expression.getValue()).isEqualTo(3L); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(3L); + } + + @Test + public void opDivide_mixedNumberTypes() throws Exception { + PayloadX p = new PayloadX(); + + // This is what you had to do before the changes in order for it to compile: + // expression = parse("(T(java.lang.Double).parseDouble(payload.valueI.toString()))/60D"); + + // right is a double + checkCalc(p,"payload.valueSB/60D",2d); + checkCalc(p,"payload.valueBB/60D",2d); + checkCalc(p,"payload.valueFB/60D",2d); + checkCalc(p,"payload.valueDB/60D",2d); + checkCalc(p,"payload.valueJB/60D",2d); + checkCalc(p,"payload.valueIB/60D",2d); + + checkCalc(p,"payload.valueS/60D",2d); + checkCalc(p,"payload.valueB/60D",2d); + checkCalc(p,"payload.valueF/60D",2d); + checkCalc(p,"payload.valueD/60D",2d); + checkCalc(p,"payload.valueJ/60D",2d); + checkCalc(p,"payload.valueI/60D",2d); + + checkCalc(p,"payload.valueSB/payload.valueDB60",2d); + checkCalc(p,"payload.valueBB/payload.valueDB60",2d); + checkCalc(p,"payload.valueFB/payload.valueDB60",2d); + checkCalc(p,"payload.valueDB/payload.valueDB60",2d); + checkCalc(p,"payload.valueJB/payload.valueDB60",2d); + checkCalc(p,"payload.valueIB/payload.valueDB60",2d); + + checkCalc(p,"payload.valueS/payload.valueDB60",2d); + checkCalc(p,"payload.valueB/payload.valueDB60",2d); + checkCalc(p,"payload.valueF/payload.valueDB60",2d); + checkCalc(p,"payload.valueD/payload.valueDB60",2d); + checkCalc(p,"payload.valueJ/payload.valueDB60",2d); + checkCalc(p,"payload.valueI/payload.valueDB60",2d); + + // right is a float + checkCalc(p,"payload.valueSB/60F",2F); + checkCalc(p,"payload.valueBB/60F",2F); + checkCalc(p,"payload.valueFB/60F",2f); + checkCalc(p,"payload.valueDB/60F",2d); + checkCalc(p,"payload.valueJB/60F",2F); + checkCalc(p,"payload.valueIB/60F",2F); + + checkCalc(p,"payload.valueS/60F",2F); + checkCalc(p,"payload.valueB/60F",2F); + checkCalc(p,"payload.valueF/60F",2f); + checkCalc(p,"payload.valueD/60F",2d); + checkCalc(p,"payload.valueJ/60F",2F); + checkCalc(p,"payload.valueI/60F",2F); + + checkCalc(p,"payload.valueSB/payload.valueFB60",2F); + checkCalc(p,"payload.valueBB/payload.valueFB60",2F); + checkCalc(p,"payload.valueFB/payload.valueFB60",2f); + checkCalc(p,"payload.valueDB/payload.valueFB60",2d); + checkCalc(p,"payload.valueJB/payload.valueFB60",2F); + checkCalc(p,"payload.valueIB/payload.valueFB60",2F); + + checkCalc(p,"payload.valueS/payload.valueFB60",2F); + checkCalc(p,"payload.valueB/payload.valueFB60",2F); + checkCalc(p,"payload.valueF/payload.valueFB60",2f); + checkCalc(p,"payload.valueD/payload.valueFB60",2d); + checkCalc(p,"payload.valueJ/payload.valueFB60",2F); + checkCalc(p,"payload.valueI/payload.valueFB60",2F); + + // right is a long + checkCalc(p,"payload.valueSB/60L",2L); + checkCalc(p,"payload.valueBB/60L",2L); + checkCalc(p,"payload.valueFB/60L",2f); + checkCalc(p,"payload.valueDB/60L",2d); + checkCalc(p,"payload.valueJB/60L",2L); + checkCalc(p,"payload.valueIB/60L",2L); + + checkCalc(p,"payload.valueS/60L",2L); + checkCalc(p,"payload.valueB/60L",2L); + checkCalc(p,"payload.valueF/60L",2f); + checkCalc(p,"payload.valueD/60L",2d); + checkCalc(p,"payload.valueJ/60L",2L); + checkCalc(p,"payload.valueI/60L",2L); + + checkCalc(p,"payload.valueSB/payload.valueJB60",2L); + checkCalc(p,"payload.valueBB/payload.valueJB60",2L); + checkCalc(p,"payload.valueFB/payload.valueJB60",2f); + checkCalc(p,"payload.valueDB/payload.valueJB60",2d); + checkCalc(p,"payload.valueJB/payload.valueJB60",2L); + checkCalc(p,"payload.valueIB/payload.valueJB60",2L); + + checkCalc(p,"payload.valueS/payload.valueJB60",2L); + checkCalc(p,"payload.valueB/payload.valueJB60",2L); + checkCalc(p,"payload.valueF/payload.valueJB60",2f); + checkCalc(p,"payload.valueD/payload.valueJB60",2d); + checkCalc(p,"payload.valueJ/payload.valueJB60",2L); + checkCalc(p,"payload.valueI/payload.valueJB60",2L); + + // right is an int + checkCalc(p,"payload.valueSB/60",2); + checkCalc(p,"payload.valueBB/60",2); + checkCalc(p,"payload.valueFB/60",2f); + checkCalc(p,"payload.valueDB/60",2d); + checkCalc(p,"payload.valueJB/60",2L); + checkCalc(p,"payload.valueIB/60",2); + + checkCalc(p,"payload.valueS/60",2); + checkCalc(p,"payload.valueB/60",2); + checkCalc(p,"payload.valueF/60",2f); + checkCalc(p,"payload.valueD/60",2d); + checkCalc(p,"payload.valueJ/60",2L); + checkCalc(p,"payload.valueI/60",2); + + checkCalc(p,"payload.valueSB/payload.valueIB60",2); + checkCalc(p,"payload.valueBB/payload.valueIB60",2); + checkCalc(p,"payload.valueFB/payload.valueIB60",2f); + checkCalc(p,"payload.valueDB/payload.valueIB60",2d); + checkCalc(p,"payload.valueJB/payload.valueIB60",2L); + checkCalc(p,"payload.valueIB/payload.valueIB60",2); + + checkCalc(p,"payload.valueS/payload.valueIB60",2); + checkCalc(p,"payload.valueB/payload.valueIB60",2); + checkCalc(p,"payload.valueF/payload.valueIB60",2f); + checkCalc(p,"payload.valueD/payload.valueIB60",2d); + checkCalc(p,"payload.valueJ/payload.valueIB60",2L); + checkCalc(p,"payload.valueI/payload.valueIB60",2); + + // right is a short + checkCalc(p,"payload.valueSB/payload.valueS",1); + checkCalc(p,"payload.valueBB/payload.valueS",1); + checkCalc(p,"payload.valueFB/payload.valueS",1f); + checkCalc(p,"payload.valueDB/payload.valueS",1d); + checkCalc(p,"payload.valueJB/payload.valueS",1L); + checkCalc(p,"payload.valueIB/payload.valueS",1); + + checkCalc(p,"payload.valueS/payload.valueS",1); + checkCalc(p,"payload.valueB/payload.valueS",1); + checkCalc(p,"payload.valueF/payload.valueS",1f); + checkCalc(p,"payload.valueD/payload.valueS",1d); + checkCalc(p,"payload.valueJ/payload.valueS",1L); + checkCalc(p,"payload.valueI/payload.valueS",1); + + checkCalc(p,"payload.valueSB/payload.valueSB",1); + checkCalc(p,"payload.valueBB/payload.valueSB",1); + checkCalc(p,"payload.valueFB/payload.valueSB",1f); + checkCalc(p,"payload.valueDB/payload.valueSB",1d); + checkCalc(p,"payload.valueJB/payload.valueSB",1L); + checkCalc(p,"payload.valueIB/payload.valueSB",1); + + checkCalc(p,"payload.valueS/payload.valueSB",1); + checkCalc(p,"payload.valueB/payload.valueSB",1); + checkCalc(p,"payload.valueF/payload.valueSB",1f); + checkCalc(p,"payload.valueD/payload.valueSB",1d); + checkCalc(p,"payload.valueJ/payload.valueSB",1L); + checkCalc(p,"payload.valueI/payload.valueSB",1); + + // right is a byte + checkCalc(p,"payload.valueSB/payload.valueB",1); + checkCalc(p,"payload.valueBB/payload.valueB",1); + checkCalc(p,"payload.valueFB/payload.valueB",1f); + checkCalc(p,"payload.valueDB/payload.valueB",1d); + checkCalc(p,"payload.valueJB/payload.valueB",1L); + checkCalc(p,"payload.valueIB/payload.valueB",1); + + checkCalc(p,"payload.valueS/payload.valueB",1); + checkCalc(p,"payload.valueB/payload.valueB",1); + checkCalc(p,"payload.valueF/payload.valueB",1f); + checkCalc(p,"payload.valueD/payload.valueB",1d); + checkCalc(p,"payload.valueJ/payload.valueB",1L); + checkCalc(p,"payload.valueI/payload.valueB",1); + + checkCalc(p,"payload.valueSB/payload.valueBB",1); + checkCalc(p,"payload.valueBB/payload.valueBB",1); + checkCalc(p,"payload.valueFB/payload.valueBB",1f); + checkCalc(p,"payload.valueDB/payload.valueBB",1d); + checkCalc(p,"payload.valueJB/payload.valueBB",1L); + checkCalc(p,"payload.valueIB/payload.valueBB",1); + + checkCalc(p,"payload.valueS/payload.valueBB",1); + checkCalc(p,"payload.valueB/payload.valueBB",1); + checkCalc(p,"payload.valueF/payload.valueBB",1f); + checkCalc(p,"payload.valueD/payload.valueBB",1d); + checkCalc(p,"payload.valueJ/payload.valueBB",1L); + checkCalc(p,"payload.valueI/payload.valueBB",1); + } + + @Test + public void opPlus_mixedNumberTypes() throws Exception { + PayloadX p = new PayloadX(); + + // This is what you had to do before the changes in order for it to compile: + // expression = parse("(T(java.lang.Double).parseDouble(payload.valueI.toString()))/60D"); + + // right is a double + checkCalc(p,"payload.valueSB+60D",180d); + checkCalc(p,"payload.valueBB+60D",180d); + checkCalc(p,"payload.valueFB+60D",180d); + checkCalc(p,"payload.valueDB+60D",180d); + checkCalc(p,"payload.valueJB+60D",180d); + checkCalc(p,"payload.valueIB+60D",180d); + + checkCalc(p,"payload.valueS+60D",180d); + checkCalc(p,"payload.valueB+60D",180d); + checkCalc(p,"payload.valueF+60D",180d); + checkCalc(p,"payload.valueD+60D",180d); + checkCalc(p,"payload.valueJ+60D",180d); + checkCalc(p,"payload.valueI+60D",180d); + + checkCalc(p,"payload.valueSB+payload.valueDB60",180d); + checkCalc(p,"payload.valueBB+payload.valueDB60",180d); + checkCalc(p,"payload.valueFB+payload.valueDB60",180d); + checkCalc(p,"payload.valueDB+payload.valueDB60",180d); + checkCalc(p,"payload.valueJB+payload.valueDB60",180d); + checkCalc(p,"payload.valueIB+payload.valueDB60",180d); + + checkCalc(p,"payload.valueS+payload.valueDB60",180d); + checkCalc(p,"payload.valueB+payload.valueDB60",180d); + checkCalc(p,"payload.valueF+payload.valueDB60",180d); + checkCalc(p,"payload.valueD+payload.valueDB60",180d); + checkCalc(p,"payload.valueJ+payload.valueDB60",180d); + checkCalc(p,"payload.valueI+payload.valueDB60",180d); + + // right is a float + checkCalc(p,"payload.valueSB+60F",180F); + checkCalc(p,"payload.valueBB+60F",180F); + checkCalc(p,"payload.valueFB+60F",180f); + checkCalc(p,"payload.valueDB+60F",180d); + checkCalc(p,"payload.valueJB+60F",180F); + checkCalc(p,"payload.valueIB+60F",180F); + + checkCalc(p,"payload.valueS+60F",180F); + checkCalc(p,"payload.valueB+60F",180F); + checkCalc(p,"payload.valueF+60F",180f); + checkCalc(p,"payload.valueD+60F",180d); + checkCalc(p,"payload.valueJ+60F",180F); + checkCalc(p,"payload.valueI+60F",180F); + + checkCalc(p,"payload.valueSB+payload.valueFB60",180F); + checkCalc(p,"payload.valueBB+payload.valueFB60",180F); + checkCalc(p,"payload.valueFB+payload.valueFB60",180f); + checkCalc(p,"payload.valueDB+payload.valueFB60",180d); + checkCalc(p,"payload.valueJB+payload.valueFB60",180F); + checkCalc(p,"payload.valueIB+payload.valueFB60",180F); + + checkCalc(p,"payload.valueS+payload.valueFB60",180F); + checkCalc(p,"payload.valueB+payload.valueFB60",180F); + checkCalc(p,"payload.valueF+payload.valueFB60",180f); + checkCalc(p,"payload.valueD+payload.valueFB60",180d); + checkCalc(p,"payload.valueJ+payload.valueFB60",180F); + checkCalc(p,"payload.valueI+payload.valueFB60",180F); + + // right is a long + checkCalc(p,"payload.valueSB+60L",180L); + checkCalc(p,"payload.valueBB+60L",180L); + checkCalc(p,"payload.valueFB+60L",180f); + checkCalc(p,"payload.valueDB+60L",180d); + checkCalc(p,"payload.valueJB+60L",180L); + checkCalc(p,"payload.valueIB+60L",180L); + + checkCalc(p,"payload.valueS+60L",180L); + checkCalc(p,"payload.valueB+60L",180L); + checkCalc(p,"payload.valueF+60L",180f); + checkCalc(p,"payload.valueD+60L",180d); + checkCalc(p,"payload.valueJ+60L",180L); + checkCalc(p,"payload.valueI+60L",180L); + + checkCalc(p,"payload.valueSB+payload.valueJB60",180L); + checkCalc(p,"payload.valueBB+payload.valueJB60",180L); + checkCalc(p,"payload.valueFB+payload.valueJB60",180f); + checkCalc(p,"payload.valueDB+payload.valueJB60",180d); + checkCalc(p,"payload.valueJB+payload.valueJB60",180L); + checkCalc(p,"payload.valueIB+payload.valueJB60",180L); + + checkCalc(p,"payload.valueS+payload.valueJB60",180L); + checkCalc(p,"payload.valueB+payload.valueJB60",180L); + checkCalc(p,"payload.valueF+payload.valueJB60",180f); + checkCalc(p,"payload.valueD+payload.valueJB60",180d); + checkCalc(p,"payload.valueJ+payload.valueJB60",180L); + checkCalc(p,"payload.valueI+payload.valueJB60",180L); + + // right is an int + checkCalc(p,"payload.valueSB+60",180); + checkCalc(p,"payload.valueBB+60",180); + checkCalc(p,"payload.valueFB+60",180f); + checkCalc(p,"payload.valueDB+60",180d); + checkCalc(p,"payload.valueJB+60",180L); + checkCalc(p,"payload.valueIB+60",180); + + checkCalc(p,"payload.valueS+60",180); + checkCalc(p,"payload.valueB+60",180); + checkCalc(p,"payload.valueF+60",180f); + checkCalc(p,"payload.valueD+60",180d); + checkCalc(p,"payload.valueJ+60",180L); + checkCalc(p,"payload.valueI+60",180); + + checkCalc(p,"payload.valueSB+payload.valueIB60",180); + checkCalc(p,"payload.valueBB+payload.valueIB60",180); + checkCalc(p,"payload.valueFB+payload.valueIB60",180f); + checkCalc(p,"payload.valueDB+payload.valueIB60",180d); + checkCalc(p,"payload.valueJB+payload.valueIB60",180L); + checkCalc(p,"payload.valueIB+payload.valueIB60",180); + + checkCalc(p,"payload.valueS+payload.valueIB60",180); + checkCalc(p,"payload.valueB+payload.valueIB60",180); + checkCalc(p,"payload.valueF+payload.valueIB60",180f); + checkCalc(p,"payload.valueD+payload.valueIB60",180d); + checkCalc(p,"payload.valueJ+payload.valueIB60",180L); + checkCalc(p,"payload.valueI+payload.valueIB60",180); + + // right is a short + checkCalc(p,"payload.valueSB+payload.valueS",240); + checkCalc(p,"payload.valueBB+payload.valueS",240); + checkCalc(p,"payload.valueFB+payload.valueS",240f); + checkCalc(p,"payload.valueDB+payload.valueS",240d); + checkCalc(p,"payload.valueJB+payload.valueS",240L); + checkCalc(p,"payload.valueIB+payload.valueS",240); + + checkCalc(p,"payload.valueS+payload.valueS",240); + checkCalc(p,"payload.valueB+payload.valueS",240); + checkCalc(p,"payload.valueF+payload.valueS",240f); + checkCalc(p,"payload.valueD+payload.valueS",240d); + checkCalc(p,"payload.valueJ+payload.valueS",240L); + checkCalc(p,"payload.valueI+payload.valueS",240); + + checkCalc(p,"payload.valueSB+payload.valueSB",240); + checkCalc(p,"payload.valueBB+payload.valueSB",240); + checkCalc(p,"payload.valueFB+payload.valueSB",240f); + checkCalc(p,"payload.valueDB+payload.valueSB",240d); + checkCalc(p,"payload.valueJB+payload.valueSB",240L); + checkCalc(p,"payload.valueIB+payload.valueSB",240); + + checkCalc(p,"payload.valueS+payload.valueSB",240); + checkCalc(p,"payload.valueB+payload.valueSB",240); + checkCalc(p,"payload.valueF+payload.valueSB",240f); + checkCalc(p,"payload.valueD+payload.valueSB",240d); + checkCalc(p,"payload.valueJ+payload.valueSB",240L); + checkCalc(p,"payload.valueI+payload.valueSB",240); + + // right is a byte + checkCalc(p,"payload.valueSB+payload.valueB",240); + checkCalc(p,"payload.valueBB+payload.valueB",240); + checkCalc(p,"payload.valueFB+payload.valueB",240f); + checkCalc(p,"payload.valueDB+payload.valueB",240d); + checkCalc(p,"payload.valueJB+payload.valueB",240L); + checkCalc(p,"payload.valueIB+payload.valueB",240); + + checkCalc(p,"payload.valueS+payload.valueB",240); + checkCalc(p,"payload.valueB+payload.valueB",240); + checkCalc(p,"payload.valueF+payload.valueB",240f); + checkCalc(p,"payload.valueD+payload.valueB",240d); + checkCalc(p,"payload.valueJ+payload.valueB",240L); + checkCalc(p,"payload.valueI+payload.valueB",240); + + checkCalc(p,"payload.valueSB+payload.valueBB",240); + checkCalc(p,"payload.valueBB+payload.valueBB",240); + checkCalc(p,"payload.valueFB+payload.valueBB",240f); + checkCalc(p,"payload.valueDB+payload.valueBB",240d); + checkCalc(p,"payload.valueJB+payload.valueBB",240L); + checkCalc(p,"payload.valueIB+payload.valueBB",240); + + checkCalc(p,"payload.valueS+payload.valueBB",240); + checkCalc(p,"payload.valueB+payload.valueBB",240); + checkCalc(p,"payload.valueF+payload.valueBB",240f); + checkCalc(p,"payload.valueD+payload.valueBB",240d); + checkCalc(p,"payload.valueJ+payload.valueBB",240L); + checkCalc(p,"payload.valueI+payload.valueBB",240); + } + + private void checkCalc(PayloadX p, String expression, int expectedResult) { + Expression expr = parse(expression); + assertThat(expr.getValue(p)).isEqualTo(expectedResult); + assertCanCompile(expr); + assertThat(expr.getValue(p)).isEqualTo(expectedResult); + } + + private void checkCalc(PayloadX p, String expression, float expectedResult) { + Expression expr = parse(expression); + assertThat(expr.getValue(p)).isEqualTo(expectedResult); + assertCanCompile(expr); + assertThat(expr.getValue(p)).isEqualTo(expectedResult); + } + + private void checkCalc(PayloadX p, String expression, long expectedResult) { + Expression expr = parse(expression); + assertThat(expr.getValue(p)).isEqualTo(expectedResult); + assertCanCompile(expr); + assertThat(expr.getValue(p)).isEqualTo(expectedResult); + } + + private void checkCalc(PayloadX p, String expression, double expectedResult) { + Expression expr = parse(expression); + assertThat(expr.getValue(p)).isEqualTo(expectedResult); + assertCanCompile(expr); + assertThat(expr.getValue(p)).isEqualTo(expectedResult); + } + + @Test + public void opPlusString() throws Exception { + expression = parse("'hello' + 'world'"); + assertThat(expression.getValue()).isEqualTo("helloworld"); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo("helloworld"); + + // Method with string return + expression = parse("'hello' + getWorld()"); + assertThat(expression.getValue(new Greeter())).isEqualTo("helloworld"); + assertCanCompile(expression); + assertThat(expression.getValue(new Greeter())).isEqualTo("helloworld"); + + // Method with string return + expression = parse("getWorld() + 'hello'"); + assertThat(expression.getValue(new Greeter())).isEqualTo("worldhello"); + assertCanCompile(expression); + assertThat(expression.getValue(new Greeter())).isEqualTo("worldhello"); + + // Three strings, optimal bytecode would only use one StringBuilder + expression = parse("'hello' + getWorld() + ' spring'"); + assertThat(expression.getValue(new Greeter())).isEqualTo("helloworld spring"); + assertCanCompile(expression); + assertThat(expression.getValue(new Greeter())).isEqualTo("helloworld spring"); + + // Three strings, optimal bytecode would only use one StringBuilder + expression = parse("'hello' + 3 + ' spring'"); + assertThat(expression.getValue(new Greeter())).isEqualTo("hello3 spring"); + assertCantCompile(expression); + + expression = parse("object + 'a'"); + assertThat(expression.getValue(new Greeter())).isEqualTo("objecta"); + assertCanCompile(expression); + assertThat(expression.getValue(new Greeter())).isEqualTo("objecta"); + + expression = parse("'a'+object"); + assertThat(expression.getValue(new Greeter())).isEqualTo("aobject"); + assertCanCompile(expression); + assertThat(expression.getValue(new Greeter())).isEqualTo("aobject"); + + expression = parse("'a'+object+'a'"); + assertThat(expression.getValue(new Greeter())).isEqualTo("aobjecta"); + assertCanCompile(expression); + assertThat(expression.getValue(new Greeter())).isEqualTo("aobjecta"); + + expression = parse("object+'a'+object"); + assertThat(expression.getValue(new Greeter())).isEqualTo("objectaobject"); + assertCanCompile(expression); + assertThat(expression.getValue(new Greeter())).isEqualTo("objectaobject"); + + expression = parse("object+object"); + assertThat(expression.getValue(new Greeter())).isEqualTo("objectobject"); + assertCanCompile(expression); + assertThat(expression.getValue(new Greeter())).isEqualTo("objectobject"); + } + + @Test + public void opMinus() throws Exception { + expression = parse("2-2"); + expression.getValue(); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(0); + + expression = parse("4L-2L"); + expression.getValue(); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(2L); + + expression = parse("4.0f-2.0f"); + expression.getValue(); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(2.0f); + + expression = parse("3.0d-4.0d"); + expression.getValue(); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(-1.0d); + + expression = parse("-1"); + expression.getValue(); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(-1); + + expression = parse("-1L"); + expression.getValue(); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(-1L); + + expression = parse("-1.5f"); + expression.getValue(); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(-1.5f); + + expression = parse("-2.5d"); + expression.getValue(); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(-2.5d); + + expression = parse("T(Integer).valueOf(2)-6"); + assertThat(expression.getValue()).isEqualTo(-4); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(-4); + + expression = parse("T(Integer).valueOf(1)-T(Integer).valueOf(3)"); + assertThat(expression.getValue()).isEqualTo(-2); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(-2); + + expression = parse("4-T(Integer).valueOf(3)"); + assertThat(expression.getValue()).isEqualTo(1); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(1); + + expression = parse("T(Float).valueOf(2.0f)-6"); + assertThat(expression.getValue()).isEqualTo(-4.0f); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(-4.0f); + + expression = parse("T(Float).valueOf(8.0f)-T(Float).valueOf(3.0f)"); + assertThat(expression.getValue()).isEqualTo(5.0f); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(5.0f); + + expression = parse("11L-T(Long).valueOf(4L)"); + assertThat(expression.getValue()).isEqualTo(7L); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(7L); + + expression = parse("T(Long).valueOf(9L)-6"); + assertThat(expression.getValue()).isEqualTo(3L); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(3L); + + expression = parse("T(Long).valueOf(4L)-T(Long).valueOf(3L)"); + assertThat(expression.getValue()).isEqualTo(1L); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(1L); + + expression = parse("8L-T(Long).valueOf(2L)"); + assertThat(expression.getValue()).isEqualTo(6L); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(6L); + } + + @Test + public void opMinus_mixedNumberTypes() throws Exception { + PayloadX p = new PayloadX(); + + // This is what you had to do before the changes in order for it to compile: + // expression = parse("(T(java.lang.Double).parseDouble(payload.valueI.toString()))/60D"); + + // right is a double + checkCalc(p,"payload.valueSB-60D",60d); + checkCalc(p,"payload.valueBB-60D",60d); + checkCalc(p,"payload.valueFB-60D",60d); + checkCalc(p,"payload.valueDB-60D",60d); + checkCalc(p,"payload.valueJB-60D",60d); + checkCalc(p,"payload.valueIB-60D",60d); + + checkCalc(p,"payload.valueS-60D",60d); + checkCalc(p,"payload.valueB-60D",60d); + checkCalc(p,"payload.valueF-60D",60d); + checkCalc(p,"payload.valueD-60D",60d); + checkCalc(p,"payload.valueJ-60D",60d); + checkCalc(p,"payload.valueI-60D",60d); + + checkCalc(p,"payload.valueSB-payload.valueDB60",60d); + checkCalc(p,"payload.valueBB-payload.valueDB60",60d); + checkCalc(p,"payload.valueFB-payload.valueDB60",60d); + checkCalc(p,"payload.valueDB-payload.valueDB60",60d); + checkCalc(p,"payload.valueJB-payload.valueDB60",60d); + checkCalc(p,"payload.valueIB-payload.valueDB60",60d); + + checkCalc(p,"payload.valueS-payload.valueDB60",60d); + checkCalc(p,"payload.valueB-payload.valueDB60",60d); + checkCalc(p,"payload.valueF-payload.valueDB60",60d); + checkCalc(p,"payload.valueD-payload.valueDB60",60d); + checkCalc(p,"payload.valueJ-payload.valueDB60",60d); + checkCalc(p,"payload.valueI-payload.valueDB60",60d); + + // right is a float + checkCalc(p,"payload.valueSB-60F",60F); + checkCalc(p,"payload.valueBB-60F",60F); + checkCalc(p,"payload.valueFB-60F",60f); + checkCalc(p,"payload.valueDB-60F",60d); + checkCalc(p,"payload.valueJB-60F",60F); + checkCalc(p,"payload.valueIB-60F",60F); + + checkCalc(p,"payload.valueS-60F",60F); + checkCalc(p,"payload.valueB-60F",60F); + checkCalc(p,"payload.valueF-60F",60f); + checkCalc(p,"payload.valueD-60F",60d); + checkCalc(p,"payload.valueJ-60F",60F); + checkCalc(p,"payload.valueI-60F",60F); + + checkCalc(p,"payload.valueSB-payload.valueFB60",60F); + checkCalc(p,"payload.valueBB-payload.valueFB60",60F); + checkCalc(p,"payload.valueFB-payload.valueFB60",60f); + checkCalc(p,"payload.valueDB-payload.valueFB60",60d); + checkCalc(p,"payload.valueJB-payload.valueFB60",60F); + checkCalc(p,"payload.valueIB-payload.valueFB60",60F); + + checkCalc(p,"payload.valueS-payload.valueFB60",60F); + checkCalc(p,"payload.valueB-payload.valueFB60",60F); + checkCalc(p,"payload.valueF-payload.valueFB60",60f); + checkCalc(p,"payload.valueD-payload.valueFB60",60d); + checkCalc(p,"payload.valueJ-payload.valueFB60",60F); + checkCalc(p,"payload.valueI-payload.valueFB60",60F); + + // right is a long + checkCalc(p,"payload.valueSB-60L",60L); + checkCalc(p,"payload.valueBB-60L",60L); + checkCalc(p,"payload.valueFB-60L",60f); + checkCalc(p,"payload.valueDB-60L",60d); + checkCalc(p,"payload.valueJB-60L",60L); + checkCalc(p,"payload.valueIB-60L",60L); + + checkCalc(p,"payload.valueS-60L",60L); + checkCalc(p,"payload.valueB-60L",60L); + checkCalc(p,"payload.valueF-60L",60f); + checkCalc(p,"payload.valueD-60L",60d); + checkCalc(p,"payload.valueJ-60L",60L); + checkCalc(p,"payload.valueI-60L",60L); + + checkCalc(p,"payload.valueSB-payload.valueJB60",60L); + checkCalc(p,"payload.valueBB-payload.valueJB60",60L); + checkCalc(p,"payload.valueFB-payload.valueJB60",60f); + checkCalc(p,"payload.valueDB-payload.valueJB60",60d); + checkCalc(p,"payload.valueJB-payload.valueJB60",60L); + checkCalc(p,"payload.valueIB-payload.valueJB60",60L); + + checkCalc(p,"payload.valueS-payload.valueJB60",60L); + checkCalc(p,"payload.valueB-payload.valueJB60",60L); + checkCalc(p,"payload.valueF-payload.valueJB60",60f); + checkCalc(p,"payload.valueD-payload.valueJB60",60d); + checkCalc(p,"payload.valueJ-payload.valueJB60",60L); + checkCalc(p,"payload.valueI-payload.valueJB60",60L); + + // right is an int + checkCalc(p,"payload.valueSB-60",60); + checkCalc(p,"payload.valueBB-60",60); + checkCalc(p,"payload.valueFB-60",60f); + checkCalc(p,"payload.valueDB-60",60d); + checkCalc(p,"payload.valueJB-60",60L); + checkCalc(p,"payload.valueIB-60",60); + + checkCalc(p,"payload.valueS-60",60); + checkCalc(p,"payload.valueB-60",60); + checkCalc(p,"payload.valueF-60",60f); + checkCalc(p,"payload.valueD-60",60d); + checkCalc(p,"payload.valueJ-60",60L); + checkCalc(p,"payload.valueI-60",60); + + checkCalc(p,"payload.valueSB-payload.valueIB60",60); + checkCalc(p,"payload.valueBB-payload.valueIB60",60); + checkCalc(p,"payload.valueFB-payload.valueIB60",60f); + checkCalc(p,"payload.valueDB-payload.valueIB60",60d); + checkCalc(p,"payload.valueJB-payload.valueIB60",60L); + checkCalc(p,"payload.valueIB-payload.valueIB60",60); + + checkCalc(p,"payload.valueS-payload.valueIB60",60); + checkCalc(p,"payload.valueB-payload.valueIB60",60); + checkCalc(p,"payload.valueF-payload.valueIB60",60f); + checkCalc(p,"payload.valueD-payload.valueIB60",60d); + checkCalc(p,"payload.valueJ-payload.valueIB60",60L); + checkCalc(p,"payload.valueI-payload.valueIB60",60); + + // right is a short + checkCalc(p,"payload.valueSB-payload.valueS20",100); + checkCalc(p,"payload.valueBB-payload.valueS20",100); + checkCalc(p,"payload.valueFB-payload.valueS20",100f); + checkCalc(p,"payload.valueDB-payload.valueS20",100d); + checkCalc(p,"payload.valueJB-payload.valueS20",100L); + checkCalc(p,"payload.valueIB-payload.valueS20",100); + + checkCalc(p,"payload.valueS-payload.valueS20",100); + checkCalc(p,"payload.valueB-payload.valueS20",100); + checkCalc(p,"payload.valueF-payload.valueS20",100f); + checkCalc(p,"payload.valueD-payload.valueS20",100d); + checkCalc(p,"payload.valueJ-payload.valueS20",100L); + checkCalc(p,"payload.valueI-payload.valueS20",100); + + checkCalc(p,"payload.valueSB-payload.valueSB20",100); + checkCalc(p,"payload.valueBB-payload.valueSB20",100); + checkCalc(p,"payload.valueFB-payload.valueSB20",100f); + checkCalc(p,"payload.valueDB-payload.valueSB20",100d); + checkCalc(p,"payload.valueJB-payload.valueSB20",100L); + checkCalc(p,"payload.valueIB-payload.valueSB20",100); + + checkCalc(p,"payload.valueS-payload.valueSB20",100); + checkCalc(p,"payload.valueB-payload.valueSB20",100); + checkCalc(p,"payload.valueF-payload.valueSB20",100f); + checkCalc(p,"payload.valueD-payload.valueSB20",100d); + checkCalc(p,"payload.valueJ-payload.valueSB20",100L); + checkCalc(p,"payload.valueI-payload.valueSB20",100); + + // right is a byte + checkCalc(p,"payload.valueSB-payload.valueB20",100); + checkCalc(p,"payload.valueBB-payload.valueB20",100); + checkCalc(p,"payload.valueFB-payload.valueB20",100f); + checkCalc(p,"payload.valueDB-payload.valueB20",100d); + checkCalc(p,"payload.valueJB-payload.valueB20",100L); + checkCalc(p,"payload.valueIB-payload.valueB20",100); + + checkCalc(p,"payload.valueS-payload.valueB20",100); + checkCalc(p,"payload.valueB-payload.valueB20",100); + checkCalc(p,"payload.valueF-payload.valueB20",100f); + checkCalc(p,"payload.valueD-payload.valueB20",100d); + checkCalc(p,"payload.valueJ-payload.valueB20",100L); + checkCalc(p,"payload.valueI-payload.valueB20",100); + + checkCalc(p,"payload.valueSB-payload.valueBB20",100); + checkCalc(p,"payload.valueBB-payload.valueBB20",100); + checkCalc(p,"payload.valueFB-payload.valueBB20",100f); + checkCalc(p,"payload.valueDB-payload.valueBB20",100d); + checkCalc(p,"payload.valueJB-payload.valueBB20",100L); + checkCalc(p,"payload.valueIB-payload.valueBB20",100); + + checkCalc(p,"payload.valueS-payload.valueBB20",100); + checkCalc(p,"payload.valueB-payload.valueBB20",100); + checkCalc(p,"payload.valueF-payload.valueBB20",100f); + checkCalc(p,"payload.valueD-payload.valueBB20",100d); + checkCalc(p,"payload.valueJ-payload.valueBB20",100L); + checkCalc(p,"payload.valueI-payload.valueBB20",100); + } + + @Test + public void opMultiply_mixedNumberTypes() throws Exception { + PayloadX p = new PayloadX(); + + // This is what you had to do before the changes in order for it to compile: + // expression = parse("(T(java.lang.Double).parseDouble(payload.valueI.toString()))/60D"); + + // right is a double + checkCalc(p,"payload.valueSB*60D",7200d); + checkCalc(p,"payload.valueBB*60D",7200d); + checkCalc(p,"payload.valueFB*60D",7200d); + checkCalc(p,"payload.valueDB*60D",7200d); + checkCalc(p,"payload.valueJB*60D",7200d); + checkCalc(p,"payload.valueIB*60D",7200d); + + checkCalc(p,"payload.valueS*60D",7200d); + checkCalc(p,"payload.valueB*60D",7200d); + checkCalc(p,"payload.valueF*60D",7200d); + checkCalc(p,"payload.valueD*60D",7200d); + checkCalc(p,"payload.valueJ*60D",7200d); + checkCalc(p,"payload.valueI*60D",7200d); + + checkCalc(p,"payload.valueSB*payload.valueDB60",7200d); + checkCalc(p,"payload.valueBB*payload.valueDB60",7200d); + checkCalc(p,"payload.valueFB*payload.valueDB60",7200d); + checkCalc(p,"payload.valueDB*payload.valueDB60",7200d); + checkCalc(p,"payload.valueJB*payload.valueDB60",7200d); + checkCalc(p,"payload.valueIB*payload.valueDB60",7200d); + + checkCalc(p,"payload.valueS*payload.valueDB60",7200d); + checkCalc(p,"payload.valueB*payload.valueDB60",7200d); + checkCalc(p,"payload.valueF*payload.valueDB60",7200d); + checkCalc(p,"payload.valueD*payload.valueDB60",7200d); + checkCalc(p,"payload.valueJ*payload.valueDB60",7200d); + checkCalc(p,"payload.valueI*payload.valueDB60",7200d); + + // right is a float + checkCalc(p,"payload.valueSB*60F",7200F); + checkCalc(p,"payload.valueBB*60F",7200F); + checkCalc(p,"payload.valueFB*60F",7200f); + checkCalc(p,"payload.valueDB*60F",7200d); + checkCalc(p,"payload.valueJB*60F",7200F); + checkCalc(p,"payload.valueIB*60F",7200F); + + checkCalc(p,"payload.valueS*60F",7200F); + checkCalc(p,"payload.valueB*60F",7200F); + checkCalc(p,"payload.valueF*60F",7200f); + checkCalc(p,"payload.valueD*60F",7200d); + checkCalc(p,"payload.valueJ*60F",7200F); + checkCalc(p,"payload.valueI*60F",7200F); + + checkCalc(p,"payload.valueSB*payload.valueFB60",7200F); + checkCalc(p,"payload.valueBB*payload.valueFB60",7200F); + checkCalc(p,"payload.valueFB*payload.valueFB60",7200f); + checkCalc(p,"payload.valueDB*payload.valueFB60",7200d); + checkCalc(p,"payload.valueJB*payload.valueFB60",7200F); + checkCalc(p,"payload.valueIB*payload.valueFB60",7200F); + + checkCalc(p,"payload.valueS*payload.valueFB60",7200F); + checkCalc(p,"payload.valueB*payload.valueFB60",7200F); + checkCalc(p,"payload.valueF*payload.valueFB60",7200f); + checkCalc(p,"payload.valueD*payload.valueFB60",7200d); + checkCalc(p,"payload.valueJ*payload.valueFB60",7200F); + checkCalc(p,"payload.valueI*payload.valueFB60",7200F); + + // right is a long + checkCalc(p,"payload.valueSB*60L",7200L); + checkCalc(p,"payload.valueBB*60L",7200L); + checkCalc(p,"payload.valueFB*60L",7200f); + checkCalc(p,"payload.valueDB*60L",7200d); + checkCalc(p,"payload.valueJB*60L",7200L); + checkCalc(p,"payload.valueIB*60L",7200L); + + checkCalc(p,"payload.valueS*60L",7200L); + checkCalc(p,"payload.valueB*60L",7200L); + checkCalc(p,"payload.valueF*60L",7200f); + checkCalc(p,"payload.valueD*60L",7200d); + checkCalc(p,"payload.valueJ*60L",7200L); + checkCalc(p,"payload.valueI*60L",7200L); + + checkCalc(p,"payload.valueSB*payload.valueJB60",7200L); + checkCalc(p,"payload.valueBB*payload.valueJB60",7200L); + checkCalc(p,"payload.valueFB*payload.valueJB60",7200f); + checkCalc(p,"payload.valueDB*payload.valueJB60",7200d); + checkCalc(p,"payload.valueJB*payload.valueJB60",7200L); + checkCalc(p,"payload.valueIB*payload.valueJB60",7200L); + + checkCalc(p,"payload.valueS*payload.valueJB60",7200L); + checkCalc(p,"payload.valueB*payload.valueJB60",7200L); + checkCalc(p,"payload.valueF*payload.valueJB60",7200f); + checkCalc(p,"payload.valueD*payload.valueJB60",7200d); + checkCalc(p,"payload.valueJ*payload.valueJB60",7200L); + checkCalc(p,"payload.valueI*payload.valueJB60",7200L); + + // right is an int + checkCalc(p,"payload.valueSB*60",7200); + checkCalc(p,"payload.valueBB*60",7200); + checkCalc(p,"payload.valueFB*60",7200f); + checkCalc(p,"payload.valueDB*60",7200d); + checkCalc(p,"payload.valueJB*60",7200L); + checkCalc(p,"payload.valueIB*60",7200); + + checkCalc(p,"payload.valueS*60",7200); + checkCalc(p,"payload.valueB*60",7200); + checkCalc(p,"payload.valueF*60",7200f); + checkCalc(p,"payload.valueD*60",7200d); + checkCalc(p,"payload.valueJ*60",7200L); + checkCalc(p,"payload.valueI*60",7200); + + checkCalc(p,"payload.valueSB*payload.valueIB60",7200); + checkCalc(p,"payload.valueBB*payload.valueIB60",7200); + checkCalc(p,"payload.valueFB*payload.valueIB60",7200f); + checkCalc(p,"payload.valueDB*payload.valueIB60",7200d); + checkCalc(p,"payload.valueJB*payload.valueIB60",7200L); + checkCalc(p,"payload.valueIB*payload.valueIB60",7200); + + checkCalc(p,"payload.valueS*payload.valueIB60",7200); + checkCalc(p,"payload.valueB*payload.valueIB60",7200); + checkCalc(p,"payload.valueF*payload.valueIB60",7200f); + checkCalc(p,"payload.valueD*payload.valueIB60",7200d); + checkCalc(p,"payload.valueJ*payload.valueIB60",7200L); + checkCalc(p,"payload.valueI*payload.valueIB60",7200); + + // right is a short + checkCalc(p,"payload.valueSB*payload.valueS20",2400); + checkCalc(p,"payload.valueBB*payload.valueS20",2400); + checkCalc(p,"payload.valueFB*payload.valueS20",2400f); + checkCalc(p,"payload.valueDB*payload.valueS20",2400d); + checkCalc(p,"payload.valueJB*payload.valueS20",2400L); + checkCalc(p,"payload.valueIB*payload.valueS20",2400); + + checkCalc(p,"payload.valueS*payload.valueS20",2400); + checkCalc(p,"payload.valueB*payload.valueS20",2400); + checkCalc(p,"payload.valueF*payload.valueS20",2400f); + checkCalc(p,"payload.valueD*payload.valueS20",2400d); + checkCalc(p,"payload.valueJ*payload.valueS20",2400L); + checkCalc(p,"payload.valueI*payload.valueS20",2400); + + checkCalc(p,"payload.valueSB*payload.valueSB20",2400); + checkCalc(p,"payload.valueBB*payload.valueSB20",2400); + checkCalc(p,"payload.valueFB*payload.valueSB20",2400f); + checkCalc(p,"payload.valueDB*payload.valueSB20",2400d); + checkCalc(p,"payload.valueJB*payload.valueSB20",2400L); + checkCalc(p,"payload.valueIB*payload.valueSB20",2400); + + checkCalc(p,"payload.valueS*payload.valueSB20",2400); + checkCalc(p,"payload.valueB*payload.valueSB20",2400); + checkCalc(p,"payload.valueF*payload.valueSB20",2400f); + checkCalc(p,"payload.valueD*payload.valueSB20",2400d); + checkCalc(p,"payload.valueJ*payload.valueSB20",2400L); + checkCalc(p,"payload.valueI*payload.valueSB20",2400); + + // right is a byte + checkCalc(p,"payload.valueSB*payload.valueB20",2400); + checkCalc(p,"payload.valueBB*payload.valueB20",2400); + checkCalc(p,"payload.valueFB*payload.valueB20",2400f); + checkCalc(p,"payload.valueDB*payload.valueB20",2400d); + checkCalc(p,"payload.valueJB*payload.valueB20",2400L); + checkCalc(p,"payload.valueIB*payload.valueB20",2400); + + checkCalc(p,"payload.valueS*payload.valueB20",2400); + checkCalc(p,"payload.valueB*payload.valueB20",2400); + checkCalc(p,"payload.valueF*payload.valueB20",2400f); + checkCalc(p,"payload.valueD*payload.valueB20",2400d); + checkCalc(p,"payload.valueJ*payload.valueB20",2400L); + checkCalc(p,"payload.valueI*payload.valueB20",2400); + + checkCalc(p,"payload.valueSB*payload.valueBB20",2400); + checkCalc(p,"payload.valueBB*payload.valueBB20",2400); + checkCalc(p,"payload.valueFB*payload.valueBB20",2400f); + checkCalc(p,"payload.valueDB*payload.valueBB20",2400d); + checkCalc(p,"payload.valueJB*payload.valueBB20",2400L); + checkCalc(p,"payload.valueIB*payload.valueBB20",2400); + + checkCalc(p,"payload.valueS*payload.valueBB20",2400); + checkCalc(p,"payload.valueB*payload.valueBB20",2400); + checkCalc(p,"payload.valueF*payload.valueBB20",2400f); + checkCalc(p,"payload.valueD*payload.valueBB20",2400d); + checkCalc(p,"payload.valueJ*payload.valueBB20",2400L); + checkCalc(p,"payload.valueI*payload.valueBB20",2400); + } + + @Test + public void opModulus_mixedNumberTypes() throws Exception { + PayloadX p = new PayloadX(); + + // This is what you had to do before the changes in order for it to compile: + // expression = parse("(T(java.lang.Double).parseDouble(payload.valueI.toString()))/60D"); + + // right is a double + checkCalc(p,"payload.valueSB%58D",4d); + checkCalc(p,"payload.valueBB%58D",4d); + checkCalc(p,"payload.valueFB%58D",4d); + checkCalc(p,"payload.valueDB%58D",4d); + checkCalc(p,"payload.valueJB%58D",4d); + checkCalc(p,"payload.valueIB%58D",4d); + + checkCalc(p,"payload.valueS%58D",4d); + checkCalc(p,"payload.valueB%58D",4d); + checkCalc(p,"payload.valueF%58D",4d); + checkCalc(p,"payload.valueD%58D",4d); + checkCalc(p,"payload.valueJ%58D",4d); + checkCalc(p,"payload.valueI%58D",4d); + + checkCalc(p,"payload.valueSB%payload.valueDB58",4d); + checkCalc(p,"payload.valueBB%payload.valueDB58",4d); + checkCalc(p,"payload.valueFB%payload.valueDB58",4d); + checkCalc(p,"payload.valueDB%payload.valueDB58",4d); + checkCalc(p,"payload.valueJB%payload.valueDB58",4d); + checkCalc(p,"payload.valueIB%payload.valueDB58",4d); + + checkCalc(p,"payload.valueS%payload.valueDB58",4d); + checkCalc(p,"payload.valueB%payload.valueDB58",4d); + checkCalc(p,"payload.valueF%payload.valueDB58",4d); + checkCalc(p,"payload.valueD%payload.valueDB58",4d); + checkCalc(p,"payload.valueJ%payload.valueDB58",4d); + checkCalc(p,"payload.valueI%payload.valueDB58",4d); + + // right is a float + checkCalc(p,"payload.valueSB%58F",4F); + checkCalc(p,"payload.valueBB%58F",4F); + checkCalc(p,"payload.valueFB%58F",4f); + checkCalc(p,"payload.valueDB%58F",4d); + checkCalc(p,"payload.valueJB%58F",4F); + checkCalc(p,"payload.valueIB%58F",4F); + + checkCalc(p,"payload.valueS%58F",4F); + checkCalc(p,"payload.valueB%58F",4F); + checkCalc(p,"payload.valueF%58F",4f); + checkCalc(p,"payload.valueD%58F",4d); + checkCalc(p,"payload.valueJ%58F",4F); + checkCalc(p,"payload.valueI%58F",4F); + + checkCalc(p,"payload.valueSB%payload.valueFB58",4F); + checkCalc(p,"payload.valueBB%payload.valueFB58",4F); + checkCalc(p,"payload.valueFB%payload.valueFB58",4f); + checkCalc(p,"payload.valueDB%payload.valueFB58",4d); + checkCalc(p,"payload.valueJB%payload.valueFB58",4F); + checkCalc(p,"payload.valueIB%payload.valueFB58",4F); + + checkCalc(p,"payload.valueS%payload.valueFB58",4F); + checkCalc(p,"payload.valueB%payload.valueFB58",4F); + checkCalc(p,"payload.valueF%payload.valueFB58",4f); + checkCalc(p,"payload.valueD%payload.valueFB58",4d); + checkCalc(p,"payload.valueJ%payload.valueFB58",4F); + checkCalc(p,"payload.valueI%payload.valueFB58",4F); + + // right is a long + checkCalc(p,"payload.valueSB%58L",4L); + checkCalc(p,"payload.valueBB%58L",4L); + checkCalc(p,"payload.valueFB%58L",4f); + checkCalc(p,"payload.valueDB%58L",4d); + checkCalc(p,"payload.valueJB%58L",4L); + checkCalc(p,"payload.valueIB%58L",4L); + + checkCalc(p,"payload.valueS%58L",4L); + checkCalc(p,"payload.valueB%58L",4L); + checkCalc(p,"payload.valueF%58L",4f); + checkCalc(p,"payload.valueD%58L",4d); + checkCalc(p,"payload.valueJ%58L",4L); + checkCalc(p,"payload.valueI%58L",4L); + + checkCalc(p,"payload.valueSB%payload.valueJB58",4L); + checkCalc(p,"payload.valueBB%payload.valueJB58",4L); + checkCalc(p,"payload.valueFB%payload.valueJB58",4f); + checkCalc(p,"payload.valueDB%payload.valueJB58",4d); + checkCalc(p,"payload.valueJB%payload.valueJB58",4L); + checkCalc(p,"payload.valueIB%payload.valueJB58",4L); + + checkCalc(p,"payload.valueS%payload.valueJB58",4L); + checkCalc(p,"payload.valueB%payload.valueJB58",4L); + checkCalc(p,"payload.valueF%payload.valueJB58",4f); + checkCalc(p,"payload.valueD%payload.valueJB58",4d); + checkCalc(p,"payload.valueJ%payload.valueJB58",4L); + checkCalc(p,"payload.valueI%payload.valueJB58",4L); + + // right is an int + checkCalc(p,"payload.valueSB%58",4); + checkCalc(p,"payload.valueBB%58",4); + checkCalc(p,"payload.valueFB%58",4f); + checkCalc(p,"payload.valueDB%58",4d); + checkCalc(p,"payload.valueJB%58",4L); + checkCalc(p,"payload.valueIB%58",4); + + checkCalc(p,"payload.valueS%58",4); + checkCalc(p,"payload.valueB%58",4); + checkCalc(p,"payload.valueF%58",4f); + checkCalc(p,"payload.valueD%58",4d); + checkCalc(p,"payload.valueJ%58",4L); + checkCalc(p,"payload.valueI%58",4); + + checkCalc(p,"payload.valueSB%payload.valueIB58",4); + checkCalc(p,"payload.valueBB%payload.valueIB58",4); + checkCalc(p,"payload.valueFB%payload.valueIB58",4f); + checkCalc(p,"payload.valueDB%payload.valueIB58",4d); + checkCalc(p,"payload.valueJB%payload.valueIB58",4L); + checkCalc(p,"payload.valueIB%payload.valueIB58",4); + + checkCalc(p,"payload.valueS%payload.valueIB58",4); + checkCalc(p,"payload.valueB%payload.valueIB58",4); + checkCalc(p,"payload.valueF%payload.valueIB58",4f); + checkCalc(p,"payload.valueD%payload.valueIB58",4d); + checkCalc(p,"payload.valueJ%payload.valueIB58",4L); + checkCalc(p,"payload.valueI%payload.valueIB58",4); + + // right is a short + checkCalc(p,"payload.valueSB%payload.valueS18",12); + checkCalc(p,"payload.valueBB%payload.valueS18",12); + checkCalc(p,"payload.valueFB%payload.valueS18",12f); + checkCalc(p,"payload.valueDB%payload.valueS18",12d); + checkCalc(p,"payload.valueJB%payload.valueS18",12L); + checkCalc(p,"payload.valueIB%payload.valueS18",12); + + checkCalc(p,"payload.valueS%payload.valueS18",12); + checkCalc(p,"payload.valueB%payload.valueS18",12); + checkCalc(p,"payload.valueF%payload.valueS18",12f); + checkCalc(p,"payload.valueD%payload.valueS18",12d); + checkCalc(p,"payload.valueJ%payload.valueS18",12L); + checkCalc(p,"payload.valueI%payload.valueS18",12); + + checkCalc(p,"payload.valueSB%payload.valueSB18",12); + checkCalc(p,"payload.valueBB%payload.valueSB18",12); + checkCalc(p,"payload.valueFB%payload.valueSB18",12f); + checkCalc(p,"payload.valueDB%payload.valueSB18",12d); + checkCalc(p,"payload.valueJB%payload.valueSB18",12L); + checkCalc(p,"payload.valueIB%payload.valueSB18",12); + + checkCalc(p,"payload.valueS%payload.valueSB18",12); + checkCalc(p,"payload.valueB%payload.valueSB18",12); + checkCalc(p,"payload.valueF%payload.valueSB18",12f); + checkCalc(p,"payload.valueD%payload.valueSB18",12d); + checkCalc(p,"payload.valueJ%payload.valueSB18",12L); + checkCalc(p,"payload.valueI%payload.valueSB18",12); + + // right is a byte + checkCalc(p,"payload.valueSB%payload.valueB18",12); + checkCalc(p,"payload.valueBB%payload.valueB18",12); + checkCalc(p,"payload.valueFB%payload.valueB18",12f); + checkCalc(p,"payload.valueDB%payload.valueB18",12d); + checkCalc(p,"payload.valueJB%payload.valueB18",12L); + checkCalc(p,"payload.valueIB%payload.valueB18",12); + + checkCalc(p,"payload.valueS%payload.valueB18",12); + checkCalc(p,"payload.valueB%payload.valueB18",12); + checkCalc(p,"payload.valueF%payload.valueB18",12f); + checkCalc(p,"payload.valueD%payload.valueB18",12d); + checkCalc(p,"payload.valueJ%payload.valueB18",12L); + checkCalc(p,"payload.valueI%payload.valueB18",12); + + checkCalc(p,"payload.valueSB%payload.valueBB18",12); + checkCalc(p,"payload.valueBB%payload.valueBB18",12); + checkCalc(p,"payload.valueFB%payload.valueBB18",12f); + checkCalc(p,"payload.valueDB%payload.valueBB18",12d); + checkCalc(p,"payload.valueJB%payload.valueBB18",12L); + checkCalc(p,"payload.valueIB%payload.valueBB18",12); + + checkCalc(p,"payload.valueS%payload.valueBB18",12); + checkCalc(p,"payload.valueB%payload.valueBB18",12); + checkCalc(p,"payload.valueF%payload.valueBB18",12f); + checkCalc(p,"payload.valueD%payload.valueBB18",12d); + checkCalc(p,"payload.valueJ%payload.valueBB18",12L); + checkCalc(p,"payload.valueI%payload.valueBB18",12); + } + + @Test + public void opMultiply() throws Exception { + expression = parse("2*2"); + expression.getValue(); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(4); + + expression = parse("2L*2L"); + expression.getValue(); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(4L); + + expression = parse("2.0f*2.0f"); + expression.getValue(); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(4.0f); + + expression = parse("3.0d*4.0d"); + expression.getValue(); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(12.0d); + + expression = parse("T(Float).valueOf(2.0f)*6"); + assertThat(expression.getValue()).isEqualTo(12.0f); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(12.0f); + + expression = parse("T(Float).valueOf(8.0f)*T(Float).valueOf(3.0f)"); + assertThat(expression.getValue()).isEqualTo(24.0f); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(24.0f); + + expression = parse("11L*T(Long).valueOf(4L)"); + assertThat(expression.getValue()).isEqualTo(44L); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(44L); + + expression = parse("T(Long).valueOf(9L)*6"); + assertThat(expression.getValue()).isEqualTo(54L); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(54L); + + expression = parse("T(Long).valueOf(4L)*T(Long).valueOf(3L)"); + assertThat(expression.getValue()).isEqualTo(12L); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(12L); + + expression = parse("8L*T(Long).valueOf(2L)"); + assertThat(expression.getValue()).isEqualTo(16L); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(16L); + + expression = parse("T(Float).valueOf(8.0f)*-T(Float).valueOf(3.0f)"); + assertThat(expression.getValue()).isEqualTo(-24.0f); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(-24.0f); + } + + @Test + public void opDivide() throws Exception { + expression = parse("2/2"); + expression.getValue(); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(1); + + expression = parse("2L/2L"); + expression.getValue(); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(1L); + + expression = parse("2.0f/2.0f"); + expression.getValue(); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(1.0f); + + expression = parse("3.0d/4.0d"); + expression.getValue(); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(0.75d); + + expression = parse("T(Float).valueOf(6.0f)/2"); + assertThat(expression.getValue()).isEqualTo(3.0f); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(3.0f); + + expression = parse("T(Float).valueOf(8.0f)/T(Float).valueOf(2.0f)"); + assertThat(expression.getValue()).isEqualTo(4.0f); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(4.0f); + + expression = parse("12L/T(Long).valueOf(4L)"); + assertThat(expression.getValue()).isEqualTo(3L); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(3L); + + expression = parse("T(Long).valueOf(44L)/11"); + assertThat(expression.getValue()).isEqualTo(4L); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(4L); + + expression = parse("T(Long).valueOf(4L)/T(Long).valueOf(2L)"); + assertThat(expression.getValue()).isEqualTo(2L); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(2L); + + expression = parse("8L/T(Long).valueOf(2L)"); + assertThat(expression.getValue()).isEqualTo(4L); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(4L); + + expression = parse("T(Float).valueOf(8.0f)/-T(Float).valueOf(4.0f)"); + assertThat(expression.getValue()).isEqualTo(-2.0f); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(-2.0f); + } + + @Test + public void opModulus_12041() throws Exception { + expression = parse("2%2"); + assertThat(expression.getValue()).isEqualTo(0); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(0); + + expression = parse("payload%2==0"); + assertThat(expression.getValue(new GenericMessageTestHelper<>(4), Boolean.TYPE)).isTrue(); + assertThat(expression.getValue(new GenericMessageTestHelper<>(5), Boolean.TYPE)).isFalse(); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper<>(4), Boolean.TYPE)).isTrue(); + assertThat(expression.getValue(new GenericMessageTestHelper<>(5), Boolean.TYPE)).isFalse(); + + expression = parse("8%3"); + assertThat(expression.getValue()).isEqualTo(2); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(2); + + expression = parse("17L%5L"); + assertThat(expression.getValue()).isEqualTo(2L); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(2L); + + expression = parse("3.0f%2.0f"); + assertThat(expression.getValue()).isEqualTo(1.0f); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(1.0f); + + expression = parse("3.0d%4.0d"); + assertThat(expression.getValue()).isEqualTo(3.0d); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(3.0d); + + expression = parse("T(Float).valueOf(6.0f)%2"); + assertThat(expression.getValue()).isEqualTo(0.0f); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(0.0f); + + expression = parse("T(Float).valueOf(6.0f)%4"); + assertThat(expression.getValue()).isEqualTo(2.0f); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(2.0f); + + expression = parse("T(Float).valueOf(8.0f)%T(Float).valueOf(3.0f)"); + assertThat(expression.getValue()).isEqualTo(2.0f); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(2.0f); + + expression = parse("13L%T(Long).valueOf(4L)"); + assertThat(expression.getValue()).isEqualTo(1L); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(1L); + + expression = parse("T(Long).valueOf(44L)%12"); + assertThat(expression.getValue()).isEqualTo(8L); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(8L); + + expression = parse("T(Long).valueOf(9L)%T(Long).valueOf(2L)"); + assertThat(expression.getValue()).isEqualTo(1L); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(1L); + + expression = parse("7L%T(Long).valueOf(2L)"); + assertThat(expression.getValue()).isEqualTo(1L); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(1L); + + expression = parse("T(Float).valueOf(9.0f)%-T(Float).valueOf(4.0f)"); + assertThat(expression.getValue()).isEqualTo(1.0f); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo(1.0f); + } + + @Test + public void compilationOfBasicNullSafeMethodReference() { + SpelExpressionParser parser = new SpelExpressionParser( + new SpelParserConfiguration(SpelCompilerMode.OFF, getClass().getClassLoader())); + SpelExpression expression = parser.parseRaw("#it?.equals(3)"); + StandardEvaluationContext context = new StandardEvaluationContext(new Object[] {1}); + context.setVariable("it", 3); + expression.setEvaluationContext(context); + assertThat(expression.getValue(Boolean.class)).isTrue(); + context.setVariable("it", null); + assertThat(expression.getValue(Boolean.class)).isNull(); + + assertCanCompile(expression); + + context.setVariable("it", 3); + assertThat(expression.getValue(Boolean.class)).isTrue(); + context.setVariable("it", null); + assertThat(expression.getValue(Boolean.class)).isNull(); + } + + @Test + public void failsWhenSettingContextForExpression_SPR12326() { + SpelExpressionParser parser = new SpelExpressionParser( + new SpelParserConfiguration(SpelCompilerMode.OFF, getClass().getClassLoader())); + Person3 person = new Person3("foo", 1); + SpelExpression expression = parser.parseRaw("#it?.age?.equals([0])"); + StandardEvaluationContext context = new StandardEvaluationContext(new Object[] {1}); + context.setVariable("it", person); + expression.setEvaluationContext(context); + assertThat(expression.getValue(Boolean.class)).isTrue(); + // This will trigger compilation (second usage) + assertThat(expression.getValue(Boolean.class)).isTrue(); + context.setVariable("it", null); + assertThat(expression.getValue(Boolean.class)).isNull(); + + assertCanCompile(expression); + + context.setVariable("it", person); + assertThat(expression.getValue(Boolean.class)).isTrue(); + context.setVariable("it", null); + assertThat(expression.getValue(Boolean.class)).isNull(); + } + + + /** + * Test variants of using T(...) and static/non-static method/property/field references. + */ + @Test + public void constructorReference_SPR13781() { + // Static field access on a T() referenced type + expression = parser.parseExpression("T(java.util.Locale).ENGLISH"); + assertThat(expression.getValue().toString()).isEqualTo("en"); + assertCanCompile(expression); + assertThat(expression.getValue().toString()).isEqualTo("en"); + + // The actual expression from the bug report. It fails if the ENGLISH reference fails + // to pop the type reference for Locale off the stack (if it isn't popped then + // toLowerCase() will be called with a Locale parameter). In this situation the + // code generation for ENGLISH should notice there is something on the stack that + // is not required and pop it off. + expression = parser.parseExpression("#userId.toString().toLowerCase(T(java.util.Locale).ENGLISH)"); + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setVariable("userId", "RoDnEy"); + assertThat(expression.getValue(context)).isEqualTo("rodney"); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo("rodney"); + + // Property access on a class object + expression = parser.parseExpression("T(String).name"); + assertThat(expression.getValue()).isEqualTo("java.lang.String"); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo("java.lang.String"); + + // Now the type reference isn't on the stack, and needs loading + context = new StandardEvaluationContext(String.class); + expression = parser.parseExpression("name"); + assertThat(expression.getValue(context)).isEqualTo("java.lang.String"); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo("java.lang.String"); + + expression = parser.parseExpression("T(String).getName()"); + assertThat(expression.getValue()).isEqualTo("java.lang.String"); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo("java.lang.String"); + + // These tests below verify that the chain of static accesses (either method/property or field) + // leave the right thing on top of the stack for processing by any outer consuming code. + // Here the consuming code is the String.valueOf() function. If the wrong thing were on + // the stack (for example if the compiled code for static methods wasn't popping the + // previous thing off the stack) the valueOf() would operate on the wrong value. + + String shclass = StaticsHelper.class.getName(); + // Basic chain: property access then method access + expression = parser.parseExpression("T(String).valueOf(T(String).name.valueOf(1))"); + assertThat(expression.getValue()).isEqualTo("1"); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo("1"); + + // chain of statics ending with static method + expression = parser.parseExpression("T(String).valueOf(T(" + shclass + ").methoda().methoda().methodb())"); + assertThat(expression.getValue()).isEqualTo("mb"); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo("mb"); + + // chain of statics ending with static field + expression = parser.parseExpression("T(String).valueOf(T(" + shclass + ").fielda.fielda.fieldb)"); + assertThat(expression.getValue()).isEqualTo("fb"); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo("fb"); + + // chain of statics ending with static property access + expression = parser.parseExpression("T(String).valueOf(T(" + shclass + ").propertya.propertya.propertyb)"); + assertThat(expression.getValue()).isEqualTo("pb"); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo("pb"); + + // variety chain + expression = parser.parseExpression("T(String).valueOf(T(" + shclass + ").fielda.methoda().propertya.fieldb)"); + assertThat(expression.getValue()).isEqualTo("fb"); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo("fb"); + + expression = parser.parseExpression("T(String).valueOf(fielda.fieldb)"); + assertThat(expression.getValue(StaticsHelper.sh)).isEqualTo("fb"); + assertCanCompile(expression); + assertThat(expression.getValue(StaticsHelper.sh)).isEqualTo("fb"); + + expression = parser.parseExpression("T(String).valueOf(propertya.propertyb)"); + assertThat(expression.getValue(StaticsHelper.sh)).isEqualTo("pb"); + assertCanCompile(expression); + assertThat(expression.getValue(StaticsHelper.sh)).isEqualTo("pb"); + + expression = parser.parseExpression("T(String).valueOf(methoda().methodb())"); + assertThat(expression.getValue(StaticsHelper.sh)).isEqualTo("mb"); + assertCanCompile(expression); + assertThat(expression.getValue(StaticsHelper.sh)).isEqualTo("mb"); + + } + + @Test + public void constructorReference_SPR12326() { + String type = getClass().getName(); + String prefix = "new " + type + ".Obj"; + + expression = parser.parseExpression(prefix + "([0])"); + assertThat(((Obj) expression.getValue(new Object[]{"test"})).param1).isEqualTo("test"); + assertCanCompile(expression); + assertThat(((Obj) expression.getValue(new Object[]{"test"})).param1).isEqualTo("test"); + + expression = parser.parseExpression(prefix + "2('foo','bar').output"); + assertThat(expression.getValue(String.class)).isEqualTo("foobar"); + assertCanCompile(expression); + assertThat(expression.getValue(String.class)).isEqualTo("foobar"); + + expression = parser.parseExpression(prefix + "2('foo').output"); + assertThat(expression.getValue(String.class)).isEqualTo("foo"); + assertCanCompile(expression); + assertThat(expression.getValue(String.class)).isEqualTo("foo"); + + expression = parser.parseExpression(prefix + "2().output"); + assertThat(expression.getValue(String.class)).isEqualTo(""); + assertCanCompile(expression); + assertThat(expression.getValue(String.class)).isEqualTo(""); + + expression = parser.parseExpression(prefix + "3(1,2,3).output"); + assertThat(expression.getValue(String.class)).isEqualTo("123"); + assertCanCompile(expression); + assertThat(expression.getValue(String.class)).isEqualTo("123"); + + expression = parser.parseExpression(prefix + "3(1).output"); + assertThat(expression.getValue(String.class)).isEqualTo("1"); + assertCanCompile(expression); + assertThat(expression.getValue(String.class)).isEqualTo("1"); + + expression = parser.parseExpression(prefix + "3().output"); + assertThat(expression.getValue(String.class)).isEqualTo(""); + assertCanCompile(expression); + assertThat(expression.getValue(String.class)).isEqualTo(""); + + expression = parser.parseExpression(prefix + "3('abc',5.0f,1,2,3).output"); + assertThat(expression.getValue(String.class)).isEqualTo("abc:5.0:123"); + assertCanCompile(expression); + assertThat(expression.getValue(String.class)).isEqualTo("abc:5.0:123"); + + expression = parser.parseExpression(prefix + "3('abc',5.0f,1).output"); + assertThat(expression.getValue(String.class)).isEqualTo("abc:5.0:1"); + assertCanCompile(expression); + assertThat(expression.getValue(String.class)).isEqualTo("abc:5.0:1"); + + expression = parser.parseExpression(prefix + "3('abc',5.0f).output"); + assertThat(expression.getValue(String.class)).isEqualTo("abc:5.0:"); + assertCanCompile(expression); + assertThat(expression.getValue(String.class)).isEqualTo("abc:5.0:"); + + expression = parser.parseExpression(prefix + "4(#root).output"); + assertThat(expression.getValue(new int[] {1,2,3}, String.class)).isEqualTo("123"); + assertCanCompile(expression); + assertThat(expression.getValue(new int[] {1,2,3}, String.class)).isEqualTo("123"); + } + + @Test + public void methodReferenceMissingCastAndRootObjectAccessing_SPR12326() { + // Need boxing code on the 1 so that toString() can be called + expression = parser.parseExpression("1.toString()"); + assertThat(expression.getValue()).isEqualTo("1"); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo("1"); + + expression = parser.parseExpression("#it?.age.equals([0])"); + Person person = new Person(1); + StandardEvaluationContext context = new StandardEvaluationContext(new Object[] {person.getAge()}); + context.setVariable("it", person); + assertThat(expression.getValue(context, Boolean.class)).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(context, Boolean.class)).isTrue(); + + // Variant of above more like what was in the bug report: + SpelExpressionParser parser = new SpelExpressionParser( + new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, getClass().getClassLoader())); + + SpelExpression ex = parser.parseRaw("#it?.age.equals([0])"); + context = new StandardEvaluationContext(new Object[] {person.getAge()}); + context.setVariable("it", person); + assertThat(ex.getValue(context, Boolean.class)).isTrue(); + assertThat(ex.getValue(context, Boolean.class)).isTrue(); + + PersonInOtherPackage person2 = new PersonInOtherPackage(1); + ex = parser.parseRaw("#it?.age.equals([0])"); + context = new StandardEvaluationContext(new Object[] {person2.getAge()}); + context.setVariable("it", person2); + assertThat(ex.getValue(context, Boolean.class)).isTrue(); + assertThat(ex.getValue(context, Boolean.class)).isTrue(); + + ex = parser.parseRaw("#it?.age.equals([0])"); + context = new StandardEvaluationContext(new Object[] {person2.getAge()}); + context.setVariable("it", person2); + assertThat((boolean) (Boolean) ex.getValue(context)).isTrue(); + assertThat((boolean) (Boolean) ex.getValue(context)).isTrue(); + } + + @Test + public void constructorReference() throws Exception { + // simple ctor + expression = parser.parseExpression("new String('123')"); + assertThat(expression.getValue()).isEqualTo("123"); + assertCanCompile(expression); + assertThat(expression.getValue()).isEqualTo("123"); + + String testclass8 = "org.springframework.expression.spel.SpelCompilationCoverageTests$TestClass8"; + // multi arg ctor that includes primitives + expression = parser.parseExpression("new " + testclass8 + "(42,'123',4.0d,true)"); + assertThat(expression.getValue().getClass().getName()).isEqualTo(testclass8); + assertCanCompile(expression); + Object o = expression.getValue(); + assertThat(o.getClass().getName()).isEqualTo(testclass8); + TestClass8 tc8 = (TestClass8) o; + assertThat(tc8.i).isEqualTo(42); + assertThat(tc8.s).isEqualTo("123"); + assertThat(tc8.d).isCloseTo(4.0d, within(0.5d)); + + assertThat(tc8.z).isEqualTo(true); + + // no-arg ctor + expression = parser.parseExpression("new " + testclass8 + "()"); + assertThat(expression.getValue().getClass().getName()).isEqualTo(testclass8); + assertCanCompile(expression); + o = expression.getValue(); + assertThat(o.getClass().getName()).isEqualTo(testclass8); + + // pass primitive to reference type ctor + expression = parser.parseExpression("new " + testclass8 + "(42)"); + assertThat(expression.getValue().getClass().getName()).isEqualTo(testclass8); + assertCanCompile(expression); + o = expression.getValue(); + assertThat(o.getClass().getName()).isEqualTo(testclass8); + tc8 = (TestClass8) o; + assertThat(tc8.i).isEqualTo(42); + + // private class, can't compile it + String testclass9 = "org.springframework.expression.spel.SpelCompilationCoverageTests$TestClass9"; + expression = parser.parseExpression("new " + testclass9 + "(42)"); + assertThat(expression.getValue().getClass().getName()).isEqualTo(testclass9); + assertCantCompile(expression); + } + + @Test + public void methodReferenceReflectiveMethodSelectionWithVarargs() throws Exception { + TestClass10 tc = new TestClass10(); + + // Should call the non varargs version of concat + // (which causes the '::' prefix in test output) + expression = parser.parseExpression("concat('test')"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("::test"); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("::test"); + tc.reset(); + + // This will call the varargs concat with an empty array + expression = parser.parseExpression("concat()"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo(""); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.s).isEqualTo(""); + tc.reset(); + + // Should call the non varargs version of concat + // (which causes the '::' prefix in test output) + expression = parser.parseExpression("concat2('test')"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("::test"); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("::test"); + tc.reset(); + + // This will call the varargs concat with an empty array + expression = parser.parseExpression("concat2()"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo(""); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.s).isEqualTo(""); + tc.reset(); + } + + @Test + public void methodReferenceVarargs() throws Exception { + TestClass5 tc = new TestClass5(); + + // varargs string + expression = parser.parseExpression("eleven()"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo(""); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.s).isEqualTo(""); + tc.reset(); + + // varargs string + expression = parser.parseExpression("eleven('aaa')"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("aaa"); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("aaa"); + tc.reset(); + + // varargs string + expression = parser.parseExpression("eleven(stringArray)"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("aaabbbccc"); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("aaabbbccc"); + tc.reset(); + + // varargs string + expression = parser.parseExpression("eleven('aaa','bbb','ccc')"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("aaabbbccc"); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("aaabbbccc"); + tc.reset(); + + expression = parser.parseExpression("sixteen('aaa','bbb','ccc')"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("aaabbbccc"); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("aaabbbccc"); + tc.reset(); + + // TODO Fails related to conversion service converting a String[] to satisfy Object... +// expression = parser.parseExpression("sixteen(stringArray)"); +// assertCantCompile(expression); +// expression.getValue(tc); +// assertEquals("aaabbbccc", tc.s); +// assertCanCompile(expression); +// tc.reset(); +// expression.getValue(tc); +// assertEquals("aaabbbccc", tc.s); +// tc.reset(); + + // varargs int + expression = parser.parseExpression("twelve(1,2,3)"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.i).isEqualTo(6); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.i).isEqualTo(6); + tc.reset(); + + expression = parser.parseExpression("twelve(1)"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.i).isEqualTo(1); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.i).isEqualTo(1); + tc.reset(); + + // one string then varargs string + expression = parser.parseExpression("thirteen('aaa','bbb','ccc')"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("aaa::bbbccc"); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("aaa::bbbccc"); + tc.reset(); + + // nothing passed to varargs parameter + expression = parser.parseExpression("thirteen('aaa')"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("aaa::"); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("aaa::"); + tc.reset(); + + // nested arrays + expression = parser.parseExpression("fourteen('aaa',stringArray,stringArray)"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("aaa::{aaabbbccc}{aaabbbccc}"); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("aaa::{aaabbbccc}{aaabbbccc}"); + tc.reset(); + + // nested primitive array + expression = parser.parseExpression("fifteen('aaa',intArray,intArray)"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("aaa::{112233}{112233}"); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("aaa::{112233}{112233}"); + tc.reset(); + + // varargs boolean + expression = parser.parseExpression("arrayz(true,true,false)"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("truetruefalse"); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("truetruefalse"); + tc.reset(); + + expression = parser.parseExpression("arrayz(true)"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("true"); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("true"); + tc.reset(); + + // varargs short + expression = parser.parseExpression("arrays(s1,s2,s3)"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("123"); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("123"); + tc.reset(); + + expression = parser.parseExpression("arrays(s1)"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("1"); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("1"); + tc.reset(); + + // varargs double + expression = parser.parseExpression("arrayd(1.0d,2.0d,3.0d)"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("1.02.03.0"); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("1.02.03.0"); + tc.reset(); + + expression = parser.parseExpression("arrayd(1.0d)"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("1.0"); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("1.0"); + tc.reset(); + + // varargs long + expression = parser.parseExpression("arrayj(l1,l2,l3)"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("123"); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("123"); + tc.reset(); + + expression = parser.parseExpression("arrayj(l1)"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("1"); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("1"); + tc.reset(); + + // varargs char + expression = parser.parseExpression("arrayc(c1,c2,c3)"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("abc"); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("abc"); + tc.reset(); + + expression = parser.parseExpression("arrayc(c1)"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("a"); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("a"); + tc.reset(); + + // varargs byte + expression = parser.parseExpression("arrayb(b1,b2,b3)"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("656667"); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("656667"); + tc.reset(); + + expression = parser.parseExpression("arrayb(b1)"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("65"); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("65"); + tc.reset(); + + // varargs float + expression = parser.parseExpression("arrayf(f1,f2,f3)"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("1.02.03.0"); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("1.02.03.0"); + tc.reset(); + + expression = parser.parseExpression("arrayf(f1)"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("1.0"); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("1.0"); + tc.reset(); + } + + @Test + public void methodReference() throws Exception { + TestClass5 tc = new TestClass5(); + + // non-static method, no args, void return + expression = parser.parseExpression("one()"); + assertCantCompile(expression); + expression.getValue(tc); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.i).isEqualTo(1); + tc.reset(); + + // static method, no args, void return + expression = parser.parseExpression("two()"); + assertCantCompile(expression); + expression.getValue(tc); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(TestClass5._i).isEqualTo(1); + tc.reset(); + + // non-static method, reference type return + expression = parser.parseExpression("three()"); + assertCantCompile(expression); + expression.getValue(tc); + assertCanCompile(expression); + tc.reset(); + assertThat(expression.getValue(tc)).isEqualTo("hello"); + tc.reset(); + + // non-static method, primitive type return + expression = parser.parseExpression("four()"); + assertCantCompile(expression); + expression.getValue(tc); + assertCanCompile(expression); + tc.reset(); + assertThat(expression.getValue(tc)).isEqualTo(3277700L); + tc.reset(); + + // static method, reference type return + expression = parser.parseExpression("five()"); + assertCantCompile(expression); + expression.getValue(tc); + assertCanCompile(expression); + tc.reset(); + assertThat(expression.getValue(tc)).isEqualTo("hello"); + tc.reset(); + + // static method, primitive type return + expression = parser.parseExpression("six()"); + assertCantCompile(expression); + expression.getValue(tc); + assertCanCompile(expression); + tc.reset(); + assertThat(expression.getValue(tc)).isEqualTo(3277700L); + tc.reset(); + + // non-static method, one parameter of reference type + expression = parser.parseExpression("seven(\"foo\")"); + assertCantCompile(expression); + expression.getValue(tc); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("foo"); + tc.reset(); + + // static method, one parameter of reference type + expression = parser.parseExpression("eight(\"bar\")"); + assertCantCompile(expression); + expression.getValue(tc); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(TestClass5._s).isEqualTo("bar"); + tc.reset(); + + // non-static method, one parameter of primitive type + expression = parser.parseExpression("nine(231)"); + assertCantCompile(expression); + expression.getValue(tc); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(tc.i).isEqualTo(231); + tc.reset(); + + // static method, one parameter of primitive type + expression = parser.parseExpression("ten(111)"); + assertCantCompile(expression); + expression.getValue(tc); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertThat(TestClass5._i).isEqualTo(111); + tc.reset(); + + // method that gets type converted parameters + + // Converting from an int to a string + expression = parser.parseExpression("seven(123)"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("123"); + assertCantCompile(expression); // Uncompilable as argument conversion is occurring + + Expression expression = parser.parseExpression("'abcd'.substring(index1,index2)"); + String resultI = expression.getValue(new TestClass1(), String.class); + assertCanCompile(expression); + String resultC = expression.getValue(new TestClass1(), String.class); + assertThat(resultI).isEqualTo("bc"); + assertThat(resultC).isEqualTo("bc"); + + // Converting from an int to a Number + expression = parser.parseExpression("takeNumber(123)"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("123"); + tc.reset(); + assertCanCompile(expression); // The generated code should include boxing of the int to a Number + expression.getValue(tc); + assertThat(tc.s).isEqualTo("123"); + + // Passing a subtype + expression = parser.parseExpression("takeNumber(T(Integer).valueOf(42))"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("42"); + tc.reset(); + assertCanCompile(expression); // The generated code should include boxing of the int to a Number + expression.getValue(tc); + assertThat(tc.s).isEqualTo("42"); + + // Passing a subtype + expression = parser.parseExpression("takeString(T(Integer).valueOf(42))"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("42"); + tc.reset(); + assertCantCompile(expression); // method takes a string and we are passing an Integer + } + + @Test + public void errorHandling() throws Exception { + TestClass5 tc = new TestClass5(); + + // changing target + + // from primitive array to reference type array + int[] is = new int[] {1,2,3}; + String[] strings = new String[] {"a","b","c"}; + expression = parser.parseExpression("[1]"); + assertThat(expression.getValue(is)).isEqualTo(2); + assertCanCompile(expression); + assertThat(expression.getValue(is)).isEqualTo(2); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + expression.getValue(strings)) + .withCauseInstanceOf(ClassCastException.class); + SpelCompiler.revertToInterpreted(expression); + assertThat(expression.getValue(strings)).isEqualTo("b"); + assertCanCompile(expression); + assertThat(expression.getValue(strings)).isEqualTo("b"); + + + tc.field = "foo"; + expression = parser.parseExpression("seven(field)"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("foo"); + assertCanCompile(expression); + tc.reset(); + tc.field="bar"; + expression.getValue(tc); + + // method with changing parameter types (change reference type) + tc.obj = "foo"; + expression = parser.parseExpression("seven(obj)"); + assertCantCompile(expression); + expression.getValue(tc); + assertThat(tc.s).isEqualTo("foo"); + assertCanCompile(expression); + tc.reset(); + tc.obj=42; + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + expression.getValue(tc)) + .withCauseInstanceOf(ClassCastException.class); + + + // method with changing target + expression = parser.parseExpression("#root.charAt(0)"); + assertThat(expression.getValue("abc")).isEqualTo('a'); + assertCanCompile(expression); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + expression.getValue(42)) + .withCauseInstanceOf(ClassCastException.class); + } + + @Test + public void methodReference_staticMethod() throws Exception { + Expression expression = parser.parseExpression("T(Integer).valueOf(42)"); + int resultI = expression.getValue(new TestClass1(), Integer.TYPE); + assertCanCompile(expression); + int resultC = expression.getValue(new TestClass1(), Integer.TYPE); + assertThat(resultI).isEqualTo(42); + assertThat(resultC).isEqualTo(42); + } + + @Test + public void methodReference_literalArguments_int() throws Exception { + Expression expression = parser.parseExpression("'abcd'.substring(1,3)"); + String resultI = expression.getValue(new TestClass1(), String.class); + assertCanCompile(expression); + String resultC = expression.getValue(new TestClass1(), String.class); + assertThat(resultI).isEqualTo("bc"); + assertThat(resultC).isEqualTo("bc"); + } + + @Test + public void methodReference_simpleInstanceMethodNoArg() throws Exception { + Expression expression = parser.parseExpression("toString()"); + String resultI = expression.getValue(42, String.class); + assertCanCompile(expression); + String resultC = expression.getValue(42, String.class); + assertThat(resultI).isEqualTo("42"); + assertThat(resultC).isEqualTo("42"); + } + + @Test + public void methodReference_simpleInstanceMethodNoArgReturnPrimitive() throws Exception { + expression = parser.parseExpression("intValue()"); + int resultI = expression.getValue(42, Integer.TYPE); + assertThat(resultI).isEqualTo(42); + assertCanCompile(expression); + int resultC = expression.getValue(42, Integer.TYPE); + assertThat(resultC).isEqualTo(42); + } + + @Test + public void methodReference_simpleInstanceMethodOneArgReturnPrimitive1() throws Exception { + Expression expression = parser.parseExpression("indexOf('b')"); + int resultI = expression.getValue("abc", Integer.TYPE); + assertCanCompile(expression); + int resultC = expression.getValue("abc", Integer.TYPE); + assertThat(resultI).isEqualTo(1); + assertThat(resultC).isEqualTo(1); + } + + @Test + public void methodReference_simpleInstanceMethodOneArgReturnPrimitive2() throws Exception { + expression = parser.parseExpression("charAt(2)"); + char resultI = expression.getValue("abc", Character.TYPE); + assertThat(resultI).isEqualTo('c'); + assertCanCompile(expression); + char resultC = expression.getValue("abc", Character.TYPE); + assertThat(resultC).isEqualTo('c'); + } + + @Test + public void compoundExpression() throws Exception { + Payload payload = new Payload(); + expression = parser.parseExpression("DR[0]"); + assertThat(expression.getValue(payload).toString()).isEqualTo("instanceof Two"); + assertCanCompile(expression); + assertThat(expression.getValue(payload).toString()).isEqualTo("instanceof Two"); + ast = getAst(); + assertThat(ast.getExitDescriptor()).isEqualTo("Lorg/springframework/expression/spel/SpelCompilationCoverageTests$Two"); + + expression = parser.parseExpression("holder.three"); + assertThat(expression.getValue(payload).getClass().getName()).isEqualTo("org.springframework.expression.spel.SpelCompilationCoverageTests$Three"); + assertCanCompile(expression); + assertThat(expression.getValue(payload).getClass().getName()).isEqualTo("org.springframework.expression.spel.SpelCompilationCoverageTests$Three"); + ast = getAst(); + assertThat(ast.getExitDescriptor()).isEqualTo("Lorg/springframework/expression/spel/SpelCompilationCoverageTests$Three"); + + expression = parser.parseExpression("DR[0]"); + assertThat(expression.getValue(payload).getClass().getName()).isEqualTo("org.springframework.expression.spel.SpelCompilationCoverageTests$Two"); + assertCanCompile(expression); + assertThat(expression.getValue(payload).getClass().getName()).isEqualTo("org.springframework.expression.spel.SpelCompilationCoverageTests$Two"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Lorg/springframework/expression/spel/SpelCompilationCoverageTests$Two"); + + expression = parser.parseExpression("DR[0].three"); + assertThat(expression.getValue(payload).getClass().getName()).isEqualTo("org.springframework.expression.spel.SpelCompilationCoverageTests$Three"); + assertCanCompile(expression); + assertThat(expression.getValue(payload).getClass().getName()).isEqualTo("org.springframework.expression.spel.SpelCompilationCoverageTests$Three"); + ast = getAst(); + assertThat(ast.getExitDescriptor()).isEqualTo("Lorg/springframework/expression/spel/SpelCompilationCoverageTests$Three"); + + expression = parser.parseExpression("DR[0].three.four"); + assertThat(expression.getValue(payload)).isEqualTo(0.04d); + assertCanCompile(expression); + assertThat(expression.getValue(payload)).isEqualTo(0.04d); + assertThat(getAst().getExitDescriptor()).isEqualTo("D"); + } + + @Test + public void mixingItUp_indexerOpEqTernary() throws Exception { + Map m = new HashMap<>(); + m.put("andy","778"); + + expression = parse("['andy']==null?1:2"); + assertThat(expression.getValue(m)).isEqualTo(2); + assertCanCompile(expression); + assertThat(expression.getValue(m)).isEqualTo(2); + m.remove("andy"); + assertThat(expression.getValue(m)).isEqualTo(1); + } + + @Test + public void propertyReference() throws Exception { + TestClass6 tc = new TestClass6(); + + // non static field + expression = parser.parseExpression("orange"); + assertCantCompile(expression); + assertThat(expression.getValue(tc)).isEqualTo("value1"); + assertCanCompile(expression); + assertThat(expression.getValue(tc)).isEqualTo("value1"); + + // static field + expression = parser.parseExpression("apple"); + assertCantCompile(expression); + assertThat(expression.getValue(tc)).isEqualTo("value2"); + assertCanCompile(expression); + assertThat(expression.getValue(tc)).isEqualTo("value2"); + + // non static getter + expression = parser.parseExpression("banana"); + assertCantCompile(expression); + assertThat(expression.getValue(tc)).isEqualTo("value3"); + assertCanCompile(expression); + assertThat(expression.getValue(tc)).isEqualTo("value3"); + + // static getter + expression = parser.parseExpression("plum"); + assertCantCompile(expression); + assertThat(expression.getValue(tc)).isEqualTo("value4"); + assertCanCompile(expression); + assertThat(expression.getValue(tc)).isEqualTo("value4"); + + // record-style accessor + expression = parser.parseExpression("strawberry"); + assertCantCompile(expression); + assertThat(expression.getValue(tc)).isEqualTo("value5"); + assertCanCompile(expression); + assertThat(expression.getValue(tc)).isEqualTo("value5"); + } + + @Test + public void propertyReferenceVisibility_SPR12771() { + StandardEvaluationContext ctx = new StandardEvaluationContext(); + ctx.setVariable("httpServletRequest", HttpServlet3RequestFactory.getOne()); + // Without a fix compilation was inserting a checkcast to a private type + expression = parser.parseExpression("#httpServletRequest.servletPath"); + assertThat(expression.getValue(ctx)).isEqualTo("wibble"); + assertCanCompile(expression); + assertThat(expression.getValue(ctx)).isEqualTo("wibble"); + } + + @SuppressWarnings("unchecked") + @Test + public void indexer() throws Exception { + String[] sss = new String[] {"a","b","c"}; + Number[] ns = new Number[] {2,8,9}; + int[] is = new int[] {8,9,10}; + double[] ds = new double[] {3.0d,4.0d,5.0d}; + long[] ls = new long[] {2L,3L,4L}; + short[] ss = new short[] {(short)33,(short)44,(short)55}; + float[] fs = new float[] {6.0f,7.0f,8.0f}; + byte[] bs = new byte[] {(byte)2,(byte)3,(byte)4}; + char[] cs = new char[] {'a','b','c'}; + + // Access String (reference type) array + expression = parser.parseExpression("[0]"); + assertThat(expression.getValue(sss)).isEqualTo("a"); + assertCanCompile(expression); + assertThat(expression.getValue(sss)).isEqualTo("a"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String"); + + expression = parser.parseExpression("[1]"); + assertThat(expression.getValue(ns)).isEqualTo(8); + assertCanCompile(expression); + assertThat(expression.getValue(ns)).isEqualTo(8); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Number"); + + // Access int array + expression = parser.parseExpression("[2]"); + assertThat(expression.getValue(is)).isEqualTo(10); + assertCanCompile(expression); + assertThat(expression.getValue(is)).isEqualTo(10); + assertThat(getAst().getExitDescriptor()).isEqualTo("I"); + + // Access double array + expression = parser.parseExpression("[1]"); + assertThat(expression.getValue(ds)).isEqualTo(4.0d); + assertCanCompile(expression); + assertThat(expression.getValue(ds)).isEqualTo(4.0d); + assertThat(getAst().getExitDescriptor()).isEqualTo("D"); + + // Access long array + expression = parser.parseExpression("[0]"); + assertThat(expression.getValue(ls)).isEqualTo(2L); + assertCanCompile(expression); + assertThat(expression.getValue(ls)).isEqualTo(2L); + assertThat(getAst().getExitDescriptor()).isEqualTo("J"); + + // Access short array + expression = parser.parseExpression("[2]"); + assertThat(expression.getValue(ss)).isEqualTo((short)55); + assertCanCompile(expression); + assertThat(expression.getValue(ss)).isEqualTo((short)55); + assertThat(getAst().getExitDescriptor()).isEqualTo("S"); + + // Access float array + expression = parser.parseExpression("[0]"); + assertThat(expression.getValue(fs)).isEqualTo(6.0f); + assertCanCompile(expression); + assertThat(expression.getValue(fs)).isEqualTo(6.0f); + assertThat(getAst().getExitDescriptor()).isEqualTo("F"); + + // Access byte array + expression = parser.parseExpression("[2]"); + assertThat(expression.getValue(bs)).isEqualTo((byte)4); + assertCanCompile(expression); + assertThat(expression.getValue(bs)).isEqualTo((byte)4); + assertThat(getAst().getExitDescriptor()).isEqualTo("B"); + + // Access char array + expression = parser.parseExpression("[1]"); + assertThat(expression.getValue(cs)).isEqualTo('b'); + assertCanCompile(expression); + assertThat(expression.getValue(cs)).isEqualTo('b'); + assertThat(getAst().getExitDescriptor()).isEqualTo("C"); + + // Collections + List strings = new ArrayList<>(); + strings.add("aaa"); + strings.add("bbb"); + strings.add("ccc"); + expression = parser.parseExpression("[1]"); + assertThat(expression.getValue(strings)).isEqualTo("bbb"); + assertCanCompile(expression); + assertThat(expression.getValue(strings)).isEqualTo("bbb"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + List ints = new ArrayList<>(); + ints.add(123); + ints.add(456); + ints.add(789); + expression = parser.parseExpression("[2]"); + assertThat(expression.getValue(ints)).isEqualTo(789); + assertCanCompile(expression); + assertThat(expression.getValue(ints)).isEqualTo(789); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + // Maps + Map map1 = new HashMap<>(); + map1.put("aaa", 111); + map1.put("bbb", 222); + map1.put("ccc", 333); + expression = parser.parseExpression("['aaa']"); + assertThat(expression.getValue(map1)).isEqualTo(111); + assertCanCompile(expression); + assertThat(expression.getValue(map1)).isEqualTo(111); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + // Object + TestClass6 tc = new TestClass6(); + 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"); + + expression = parser.parseExpression("['peach']"); + assertThat(expression.getValue(tc)).isEqualTo(34L); + assertCanCompile(expression); + assertThat(expression.getValue(tc)).isEqualTo(34L); + assertThat(getAst().getExitDescriptor()).isEqualTo("J"); + + // 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"); + + // list of arrays + + List listOfStringArrays = new ArrayList<>(); + listOfStringArrays.add(new String[] {"a","b","c"}); + listOfStringArrays.add(new String[] {"d","e","f"}); + expression = parser.parseExpression("[1]"); + assertThat(stringify(expression.getValue(listOfStringArrays))).isEqualTo("d e f"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(listOfStringArrays))).isEqualTo("d e f"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + expression = parser.parseExpression("[1][0]"); + assertThat(stringify(expression.getValue(listOfStringArrays))).isEqualTo("d"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(listOfStringArrays))).isEqualTo("d"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String"); + + List listOfIntegerArrays = new ArrayList<>(); + listOfIntegerArrays.add(new Integer[] {1,2,3}); + listOfIntegerArrays.add(new Integer[] {4,5,6}); + expression = parser.parseExpression("[0]"); + assertThat(stringify(expression.getValue(listOfIntegerArrays))).isEqualTo("1 2 3"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(listOfIntegerArrays))).isEqualTo("1 2 3"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + expression = parser.parseExpression("[0][1]"); + assertThat(expression.getValue(listOfIntegerArrays)).isEqualTo(2); + assertCanCompile(expression); + assertThat(expression.getValue(listOfIntegerArrays)).isEqualTo(2); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Integer"); + + // array of lists + List[] stringArrayOfLists = new ArrayList[2]; + stringArrayOfLists[0] = new ArrayList<>(); + stringArrayOfLists[0].add("a"); + stringArrayOfLists[0].add("b"); + stringArrayOfLists[0].add("c"); + stringArrayOfLists[1] = new ArrayList<>(); + stringArrayOfLists[1].add("d"); + stringArrayOfLists[1].add("e"); + stringArrayOfLists[1].add("f"); + expression = parser.parseExpression("[1]"); + assertThat(stringify(expression.getValue(stringArrayOfLists))).isEqualTo("d e f"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(stringArrayOfLists))).isEqualTo("d e f"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/util/ArrayList"); + + expression = parser.parseExpression("[1][2]"); + assertThat(stringify(expression.getValue(stringArrayOfLists))).isEqualTo("f"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(stringArrayOfLists))).isEqualTo("f"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + // array of arrays + String[][] referenceTypeArrayOfArrays = new String[][] {new String[] {"a","b","c"},new String[] {"d","e","f"}}; + expression = parser.parseExpression("[1]"); + assertThat(stringify(expression.getValue(referenceTypeArrayOfArrays))).isEqualTo("d e f"); + assertCanCompile(expression); + assertThat(getAst().getExitDescriptor()).isEqualTo("[Ljava/lang/String"); + assertThat(stringify(expression.getValue(referenceTypeArrayOfArrays))).isEqualTo("d e f"); + assertThat(getAst().getExitDescriptor()).isEqualTo("[Ljava/lang/String"); + + expression = parser.parseExpression("[1][2]"); + assertThat(stringify(expression.getValue(referenceTypeArrayOfArrays))).isEqualTo("f"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(referenceTypeArrayOfArrays))).isEqualTo("f"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String"); + + int[][] primitiveTypeArrayOfArrays = new int[][] {new int[] {1,2,3},new int[] {4,5,6}}; + expression = parser.parseExpression("[1]"); + assertThat(stringify(expression.getValue(primitiveTypeArrayOfArrays))).isEqualTo("4 5 6"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(primitiveTypeArrayOfArrays))).isEqualTo("4 5 6"); + assertThat(getAst().getExitDescriptor()).isEqualTo("[I"); + + expression = parser.parseExpression("[1][2]"); + assertThat(stringify(expression.getValue(primitiveTypeArrayOfArrays))).isEqualTo("6"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(primitiveTypeArrayOfArrays))).isEqualTo("6"); + assertThat(getAst().getExitDescriptor()).isEqualTo("I"); + + // list of lists of reference types + List> listOfListOfStrings = new ArrayList<>(); + List list = new ArrayList<>(); + list.add("a"); + list.add("b"); + list.add("c"); + listOfListOfStrings.add(list); + list = new ArrayList<>(); + list.add("d"); + list.add("e"); + list.add("f"); + listOfListOfStrings.add(list); + + expression = parser.parseExpression("[1]"); + assertThat(stringify(expression.getValue(listOfListOfStrings))).isEqualTo("d e f"); + assertCanCompile(expression); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + assertThat(stringify(expression.getValue(listOfListOfStrings))).isEqualTo("d e f"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + expression = parser.parseExpression("[1][2]"); + assertThat(stringify(expression.getValue(listOfListOfStrings))).isEqualTo("f"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(listOfListOfStrings))).isEqualTo("f"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + // Map of lists + Map> mapToLists = new HashMap<>(); + list = new ArrayList<>(); + list.add("a"); + list.add("b"); + list.add("c"); + mapToLists.put("foo", list); + expression = parser.parseExpression("['foo']"); + assertThat(stringify(expression.getValue(mapToLists))).isEqualTo("a b c"); + assertCanCompile(expression); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + assertThat(stringify(expression.getValue(mapToLists))).isEqualTo("a b c"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + expression = parser.parseExpression("['foo'][2]"); + assertThat(stringify(expression.getValue(mapToLists))).isEqualTo("c"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(mapToLists))).isEqualTo("c"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + // Map to array + Map mapToIntArray = new HashMap<>(); + StandardEvaluationContext ctx = new StandardEvaluationContext(); + ctx.addPropertyAccessor(new CompilableMapAccessor()); + mapToIntArray.put("foo",new int[] {1,2,3}); + expression = parser.parseExpression("['foo']"); + assertThat(stringify(expression.getValue(mapToIntArray))).isEqualTo("1 2 3"); + assertCanCompile(expression); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + assertThat(stringify(expression.getValue(mapToIntArray))).isEqualTo("1 2 3"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + expression = parser.parseExpression("['foo'][1]"); + assertThat(expression.getValue(mapToIntArray)).isEqualTo(2); + assertCanCompile(expression); + assertThat(expression.getValue(mapToIntArray)).isEqualTo(2); + + expression = parser.parseExpression("foo"); + assertThat(stringify(expression.getValue(ctx, mapToIntArray))).isEqualTo("1 2 3"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(ctx, mapToIntArray))).isEqualTo("1 2 3"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + expression = parser.parseExpression("foo[1]"); + assertThat(expression.getValue(ctx, mapToIntArray)).isEqualTo(2); + assertCanCompile(expression); + assertThat(expression.getValue(ctx, mapToIntArray)).isEqualTo(2); + + expression = parser.parseExpression("['foo'][2]"); + assertThat(stringify(expression.getValue(ctx, mapToIntArray))).isEqualTo("3"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(ctx, mapToIntArray))).isEqualTo("3"); + assertThat(getAst().getExitDescriptor()).isEqualTo("I"); + + // Map array + Map[] mapArray = new Map[1]; + mapArray[0] = new HashMap<>(); + mapArray[0].put("key", "value1"); + expression = parser.parseExpression("[0]"); + assertThat(stringify(expression.getValue(mapArray))).isEqualTo("{key=value1}"); + assertCanCompile(expression); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/util/Map"); + assertThat(stringify(expression.getValue(mapArray))).isEqualTo("{key=value1}"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/util/Map"); + + expression = parser.parseExpression("[0]['key']"); + assertThat(stringify(expression.getValue(mapArray))).isEqualTo("value1"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(mapArray))).isEqualTo("value1"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + } + + @Test + public void plusNeedingCheckcast_SPR12426() { + expression = parser.parseExpression("object + ' world'"); + Object v = expression.getValue(new FooObject()); + assertThat(v).isEqualTo("hello world"); + assertCanCompile(expression); + assertThat(v).isEqualTo("hello world"); + + expression = parser.parseExpression("object + ' world'"); + v = expression.getValue(new FooString()); + assertThat(v).isEqualTo("hello world"); + assertCanCompile(expression); + assertThat(v).isEqualTo("hello world"); + } + + @Test + public void mixingItUp_propertyAccessIndexerOpLtTernaryRootNull() throws Exception { + Payload payload = new Payload(); + + expression = parser.parseExpression("DR[0].three"); + Object v = expression.getValue(payload); + assertThat(getAst().getExitDescriptor()).isEqualTo("Lorg/springframework/expression/spel/SpelCompilationCoverageTests$Three"); + + Expression expression = parser.parseExpression("DR[0].three.four lt 0.1d?#root:null"); + v = expression.getValue(payload); + + SpelExpression sExpr = (SpelExpression) expression; + Ternary ternary = (Ternary) sExpr.getAST(); + OpLT oplt = (OpLT) ternary.getChild(0); + CompoundExpression cExpr = (CompoundExpression) oplt.getLeftOperand(); + String cExprExitDescriptor = cExpr.getExitDescriptor(); + assertThat(cExprExitDescriptor).isEqualTo("D"); + assertThat(oplt.getExitDescriptor()).isEqualTo("Z"); + + assertCanCompile(expression); + Object vc = expression.getValue(payload); + assertThat(v).isEqualTo(payload); + assertThat(vc).isEqualTo(payload); + payload.DR[0].three.four = 0.13d; + vc = expression.getValue(payload); + assertThat(vc).isNull(); + } + + @Test + public void variantGetter() throws Exception { + Payload2Holder holder = new Payload2Holder(); + StandardEvaluationContext ctx = new StandardEvaluationContext(); + ctx.addPropertyAccessor(new MyAccessor()); + expression = parser.parseExpression("payload2.var1"); + Object v = expression.getValue(ctx,holder); + assertThat(v).isEqualTo("abc"); + + assertCanCompile(expression); + v = expression.getValue(ctx,holder); + assertThat(v).isEqualTo("abc"); + } + + @Test + public void compilerWithGenerics_12040() { + expression = parser.parseExpression("payload!=2"); + assertThat(expression.getValue(new GenericMessageTestHelper<>(4), Boolean.class)).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper<>(2), Boolean.class)).isFalse(); + + expression = parser.parseExpression("2!=payload"); + assertThat(expression.getValue(new GenericMessageTestHelper<>(4), Boolean.class)).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper<>(2), Boolean.class)).isFalse(); + + expression = parser.parseExpression("payload!=6L"); + assertThat(expression.getValue(new GenericMessageTestHelper<>(4L), Boolean.class)).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper<>(6L), Boolean.class)).isFalse(); + + expression = parser.parseExpression("payload==2"); + assertThat(expression.getValue(new GenericMessageTestHelper<>(4), Boolean.class)).isFalse(); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper<>(2), Boolean.class)).isTrue(); + + expression = parser.parseExpression("2==payload"); + assertThat(expression.getValue(new GenericMessageTestHelper<>(4), Boolean.class)).isFalse(); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper<>(2), Boolean.class)).isTrue(); + + expression = parser.parseExpression("payload==6L"); + assertThat(expression.getValue(new GenericMessageTestHelper<>(4L), Boolean.class)).isFalse(); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper<>(6L), Boolean.class)).isTrue(); + + expression = parser.parseExpression("2==payload"); + assertThat(expression.getValue(new GenericMessageTestHelper<>(4), Boolean.class)).isFalse(); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper<>(2), Boolean.class)).isTrue(); + + expression = parser.parseExpression("payload/2"); + assertThat(expression.getValue(new GenericMessageTestHelper<>(4))).isEqualTo(2); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper<>(6))).isEqualTo(3); + + expression = parser.parseExpression("100/payload"); + assertThat(expression.getValue(new GenericMessageTestHelper<>(4))).isEqualTo(25); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper<>(10))).isEqualTo(10); + + expression = parser.parseExpression("payload+2"); + assertThat(expression.getValue(new GenericMessageTestHelper<>(4))).isEqualTo(6); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper<>(6))).isEqualTo(8); + + expression = parser.parseExpression("100+payload"); + assertThat(expression.getValue(new GenericMessageTestHelper<>(4))).isEqualTo(104); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper<>(10))).isEqualTo(110); + + expression = parser.parseExpression("payload-2"); + assertThat(expression.getValue(new GenericMessageTestHelper<>(4))).isEqualTo(2); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper<>(6))).isEqualTo(4); + + expression = parser.parseExpression("100-payload"); + assertThat(expression.getValue(new GenericMessageTestHelper<>(4))).isEqualTo(96); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper<>(10))).isEqualTo(90); + + expression = parser.parseExpression("payload*2"); + assertThat(expression.getValue(new GenericMessageTestHelper<>(4))).isEqualTo(8); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper<>(6))).isEqualTo(12); + + expression = parser.parseExpression("100*payload"); + assertThat(expression.getValue(new GenericMessageTestHelper<>(4))).isEqualTo(400); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper<>(10))).isEqualTo(1000); + + expression = parser.parseExpression("payload/2L"); + assertThat(expression.getValue(new GenericMessageTestHelper<>(4L))).isEqualTo(2L); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper<>(6L))).isEqualTo(3L); + + expression = parser.parseExpression("100L/payload"); + assertThat(expression.getValue(new GenericMessageTestHelper<>(4L))).isEqualTo(25L); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper<>(10L))).isEqualTo(10L); + + expression = parser.parseExpression("payload/2f"); + assertThat(expression.getValue(new GenericMessageTestHelper<>(4f))).isEqualTo(2f); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper<>(6f))).isEqualTo(3f); + + expression = parser.parseExpression("100f/payload"); + assertThat(expression.getValue(new GenericMessageTestHelper<>(4f))).isEqualTo(25f); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper<>(10f))).isEqualTo(10f); + + expression = parser.parseExpression("payload/2d"); + assertThat(expression.getValue(new GenericMessageTestHelper<>(4d))).isEqualTo(2d); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper<>(6d))).isEqualTo(3d); + + expression = parser.parseExpression("100d/payload"); + assertThat(expression.getValue(new GenericMessageTestHelper<>(4d))).isEqualTo(25d); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper<>(10d))).isEqualTo(10d); + } + + // The new helper class here uses an upper bound on the generic + @Test + public void compilerWithGenerics_12040_2() { + expression = parser.parseExpression("payload/2"); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(4))).isEqualTo(2); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(6))).isEqualTo(3); + + expression = parser.parseExpression("9/payload"); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(9))).isEqualTo(1); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(3))).isEqualTo(3); + + expression = parser.parseExpression("payload+2"); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(4))).isEqualTo(6); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(6))).isEqualTo(8); + + expression = parser.parseExpression("100+payload"); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(4))).isEqualTo(104); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(10))).isEqualTo(110); + + expression = parser.parseExpression("payload-2"); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(4))).isEqualTo(2); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(6))).isEqualTo(4); + + expression = parser.parseExpression("100-payload"); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(4))).isEqualTo(96); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(10))).isEqualTo(90); + + expression = parser.parseExpression("payload*2"); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(4))).isEqualTo(8); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(6))).isEqualTo(12); + + expression = parser.parseExpression("100*payload"); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(4))).isEqualTo(400); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(10))).isEqualTo(1000); + } + + // The other numeric operators + @Test + public void compilerWithGenerics_12040_3() { + expression = parser.parseExpression("payload >= 2"); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(4), Boolean.TYPE)).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(1), Boolean.TYPE)).isFalse(); + + expression = parser.parseExpression("2 >= payload"); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(5), Boolean.TYPE)).isFalse(); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(1), Boolean.TYPE)).isTrue(); + + expression = parser.parseExpression("payload > 2"); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(4), Boolean.TYPE)).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(1), Boolean.TYPE)).isFalse(); + + expression = parser.parseExpression("2 > payload"); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(5), Boolean.TYPE)).isFalse(); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(1), Boolean.TYPE)).isTrue(); + + expression = parser.parseExpression("payload <=2"); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(1), Boolean.TYPE)).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(6), Boolean.TYPE)).isFalse(); + + expression = parser.parseExpression("2 <= payload"); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(1), Boolean.TYPE)).isFalse(); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(6), Boolean.TYPE)).isTrue(); + + expression = parser.parseExpression("payload < 2"); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(1), Boolean.TYPE)).isTrue(); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(6), Boolean.TYPE)).isFalse(); + + expression = parser.parseExpression("2 < payload"); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(1), Boolean.TYPE)).isFalse(); + assertCanCompile(expression); + assertThat(expression.getValue(new GenericMessageTestHelper2<>(6), Boolean.TYPE)).isTrue(); + } + + @Test + public void indexerMapAccessor_12045() throws Exception { + SpelParserConfiguration spc = new SpelParserConfiguration( + SpelCompilerMode.IMMEDIATE,getClass().getClassLoader()); + SpelExpressionParser sep = new SpelExpressionParser(spc); + expression=sep.parseExpression("headers[command]"); + MyMessage root = new MyMessage(); + assertThat(expression.getValue(root)).isEqualTo("wibble"); + // This next call was failing because the isCompilable check in Indexer + // did not check on the key being compilable (and also generateCode in the + // Indexer was missing the optimization that it didn't need necessarily + // need to call generateCode for that accessor) + assertThat(expression.getValue(root)).isEqualTo("wibble"); + assertCanCompile(expression); + + // What about a map key that is an expression - ensure the getKey() is evaluated in the right scope + expression=sep.parseExpression("headers[getKey()]"); + assertThat(expression.getValue(root)).isEqualTo("wobble"); + assertThat(expression.getValue(root)).isEqualTo("wobble"); + + expression=sep.parseExpression("list[getKey2()]"); + assertThat(expression.getValue(root)).isEqualTo("wobble"); + assertThat(expression.getValue(root)).isEqualTo("wobble"); + + expression = sep.parseExpression("ia[getKey2()]"); + assertThat(expression.getValue(root)).isEqualTo(3); + assertThat(expression.getValue(root)).isEqualTo(3); + } + + @Test + public void elvisOperator_SPR15192() { + SpelParserConfiguration configuration = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, null); + Expression exp; + + exp = new SpelExpressionParser(configuration).parseExpression("bar()"); + assertThat(exp.getValue(new Foo(), String.class)).isEqualTo("BAR"); + assertCanCompile(exp); + assertThat(exp.getValue(new Foo(), String.class)).isEqualTo("BAR"); + assertIsCompiled(exp); + + exp = new SpelExpressionParser(configuration).parseExpression("bar('baz')"); + assertThat(exp.getValue(new Foo(), String.class)).isEqualTo("BAZ"); + assertCanCompile(exp); + assertThat(exp.getValue(new Foo(), String.class)).isEqualTo("BAZ"); + assertIsCompiled(exp); + + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setVariable("map", Collections.singletonMap("foo", "qux")); + + exp = new SpelExpressionParser(configuration).parseExpression("bar(#map['foo'])"); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("QUX"); + assertCanCompile(exp); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("QUX"); + assertIsCompiled(exp); + + exp = new SpelExpressionParser(configuration).parseExpression("bar(#map['foo'] ?: 'qux')"); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("QUX"); + assertCanCompile(exp); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("QUX"); + assertIsCompiled(exp); + + // When the condition is a primitive + exp = new SpelExpressionParser(configuration).parseExpression("3?:'foo'"); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("3"); + assertCanCompile(exp); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("3"); + assertIsCompiled(exp); + + // When the condition is a double slot primitive + exp = new SpelExpressionParser(configuration).parseExpression("3L?:'foo'"); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("3"); + assertCanCompile(exp); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("3"); + assertIsCompiled(exp); + + // When the condition is an empty string + exp = new SpelExpressionParser(configuration).parseExpression("''?:4L"); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("4"); + assertCanCompile(exp); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("4"); + assertIsCompiled(exp); + + // null condition + exp = new SpelExpressionParser(configuration).parseExpression("null?:4L"); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("4"); + assertCanCompile(exp); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("4"); + assertIsCompiled(exp); + + // variable access returning primitive + exp = new SpelExpressionParser(configuration).parseExpression("#x?:'foo'"); + context.setVariable("x",50); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("50"); + assertCanCompile(exp); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("50"); + assertIsCompiled(exp); + + exp = new SpelExpressionParser(configuration).parseExpression("#x?:'foo'"); + context.setVariable("x",null); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("foo"); + assertCanCompile(exp); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("foo"); + assertIsCompiled(exp); + + // variable access returning array + exp = new SpelExpressionParser(configuration).parseExpression("#x?:'foo'"); + context.setVariable("x",new int[]{1,2,3}); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("1,2,3"); + assertCanCompile(exp); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("1,2,3"); + assertIsCompiled(exp); + } + + @Test + public void elvisOperator_SPR17214() throws Exception { + SpelParserConfiguration spc = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, null); + SpelExpressionParser sep = new SpelExpressionParser(spc); + + RecordHolder rh = null; + + expression = sep.parseExpression("record.get('abc')?:record.put('abc',expression.someLong?.longValue())"); + rh = new RecordHolder(); + assertThat(expression.getValue(rh)).isNull(); + assertThat(expression.getValue(rh)).isEqualTo(3L); + assertCanCompile(expression); + rh = new RecordHolder(); + assertThat(expression.getValue(rh)).isNull(); + assertThat(expression.getValue(rh)).isEqualTo(3L); + + expression = sep.parseExpression("record.get('abc')?:record.put('abc',3L.longValue())"); + rh = new RecordHolder(); + assertThat(expression.getValue(rh)).isNull(); + assertThat(expression.getValue(rh)).isEqualTo(3L); + assertCanCompile(expression); + rh = new RecordHolder(); + assertThat(expression.getValue(rh)).isNull(); + assertThat(expression.getValue(rh)).isEqualTo(3L); + + expression = sep.parseExpression("record.get('abc')?:record.put('abc',3L.longValue())"); + rh = new RecordHolder(); + assertThat(expression.getValue(rh)).isNull(); + assertThat(expression.getValue(rh)).isEqualTo(3L); + assertCanCompile(expression); + rh = new RecordHolder(); + assertThat(expression.getValue(rh)).isNull(); + assertThat(expression.getValue(rh)).isEqualTo(3L); + + expression = sep.parseExpression("record.get('abc')==null?record.put('abc',expression.someLong?.longValue()):null"); + rh = new RecordHolder(); + rh.expression.someLong=6L; + assertThat(expression.getValue(rh)).isNull(); + assertThat(rh.get("abc")).isEqualTo(6L); + assertThat(expression.getValue(rh)).isNull(); + assertCanCompile(expression); + rh = new RecordHolder(); + rh.expression.someLong=6L; + assertThat(expression.getValue(rh)).isNull(); + assertThat(rh.get("abc")).isEqualTo(6L); + assertThat(expression.getValue(rh)).isNull(); + } + + @Test + public void testNullComparison_SPR22358() { + SpelParserConfiguration configuration = new SpelParserConfiguration(SpelCompilerMode.OFF, null); + SpelExpressionParser parser = new SpelExpressionParser(configuration); + StandardEvaluationContext ctx = new StandardEvaluationContext(); + ctx.setRootObject(new Reg(1)); + verifyCompilationAndBehaviourWithNull("value>1", parser, ctx ); + verifyCompilationAndBehaviourWithNull("value<1", parser, ctx ); + verifyCompilationAndBehaviourWithNull("value>=1", parser, ctx ); + verifyCompilationAndBehaviourWithNull("value<=1", parser, ctx ); + + verifyCompilationAndBehaviourWithNull2("value>value2", parser, ctx ); + verifyCompilationAndBehaviourWithNull2("value=value2", parser, ctx ); + verifyCompilationAndBehaviourWithNull2("value<=value2", parser, ctx ); + + verifyCompilationAndBehaviourWithNull("valueD>1.0d", parser, ctx ); + verifyCompilationAndBehaviourWithNull("valueD<1.0d", parser, ctx ); + verifyCompilationAndBehaviourWithNull("valueD>=1.0d", parser, ctx ); + verifyCompilationAndBehaviourWithNull("valueD<=1.0d", parser, ctx ); + + verifyCompilationAndBehaviourWithNull2("valueD>valueD2", parser, ctx ); + verifyCompilationAndBehaviourWithNull2("valueD=valueD2", parser, ctx ); + verifyCompilationAndBehaviourWithNull2("valueD<=valueD2", parser, ctx ); + + verifyCompilationAndBehaviourWithNull("valueL>1L", parser, ctx ); + verifyCompilationAndBehaviourWithNull("valueL<1L", parser, ctx ); + verifyCompilationAndBehaviourWithNull("valueL>=1L", parser, ctx ); + verifyCompilationAndBehaviourWithNull("valueL<=1L", parser, ctx ); + + verifyCompilationAndBehaviourWithNull2("valueL>valueL2", parser, ctx ); + verifyCompilationAndBehaviourWithNull2("valueL=valueL2", parser, ctx ); + verifyCompilationAndBehaviourWithNull2("valueL<=valueL2", parser, ctx ); + + verifyCompilationAndBehaviourWithNull("valueF>1.0f", parser, ctx ); + verifyCompilationAndBehaviourWithNull("valueF<1.0f", parser, ctx ); + verifyCompilationAndBehaviourWithNull("valueF>=1.0f", parser, ctx ); + verifyCompilationAndBehaviourWithNull("valueF<=1.0f", parser, ctx ); + + verifyCompilationAndBehaviourWithNull("valueF>valueF2", parser, ctx ); + verifyCompilationAndBehaviourWithNull("valueF=valueF2", parser, ctx ); + verifyCompilationAndBehaviourWithNull("valueF<=valueF2", parser, ctx ); + } + + private void verifyCompilationAndBehaviourWithNull(String expressionText, SpelExpressionParser parser, StandardEvaluationContext ctx) { + Reg r = (Reg)ctx.getRootObject().getValue(); + r.setValue2(1); // having a value in value2 fields will enable compilation to succeed, then can switch it to null + SpelExpression fast = (SpelExpression) parser.parseExpression(expressionText); + SpelExpression slow = (SpelExpression) parser.parseExpression(expressionText); + fast.getValue(ctx); + assertThat(fast.compileExpression()).isTrue(); + r.setValue2(null); + // try the numbers 0,1,2,null + for (int i = 0; i < 4; i++) { + r.setValue(i < 3 ? i : null); + boolean slowResult = (Boolean)slow.getValue(ctx); + boolean fastResult = (Boolean)fast.getValue(ctx); + assertThat(fastResult).as("Differing results: expression=" + expressionText + + " value=" + r.getValue() + " slow=" + slowResult + " fast="+fastResult).isEqualTo(slowResult); + } + } + + private void verifyCompilationAndBehaviourWithNull2(String expressionText, SpelExpressionParser parser, StandardEvaluationContext ctx) { + SpelExpression fast = (SpelExpression) parser.parseExpression(expressionText); + SpelExpression slow = (SpelExpression) parser.parseExpression(expressionText); + fast.getValue(ctx); + assertThat(fast.compileExpression()).isTrue(); + Reg r = (Reg)ctx.getRootObject().getValue(); + // try the numbers 0,1,2,null + for (int i = 0; i < 4; i++) { + r.setValue(i < 3 ? i : null); + boolean slowResult = (Boolean)slow.getValue(ctx); + boolean fastResult = (Boolean)fast.getValue(ctx); + assertThat(fastResult).as("Differing results: expression=" + expressionText + + " value=" + r.getValue() + " slow=" + slowResult + " fast="+fastResult).isEqualTo(slowResult); + } + } + + @Test + public void ternaryOperator_SPR15192() { + SpelParserConfiguration configuration = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, null); + Expression exp; + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setVariable("map", Collections.singletonMap("foo", "qux")); + + exp = new SpelExpressionParser(configuration).parseExpression("bar(#map['foo'] != null ? #map['foo'] : 'qux')"); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("QUX"); + assertCanCompile(exp); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("QUX"); + assertIsCompiled(exp); + + exp = new SpelExpressionParser(configuration).parseExpression("3==3?3:'foo'"); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("3"); + assertCanCompile(exp); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("3"); + assertIsCompiled(exp); + exp = new SpelExpressionParser(configuration).parseExpression("3!=3?3:'foo'"); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("foo"); + assertCanCompile(exp); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("foo"); + assertIsCompiled(exp); + + // When the condition is a double slot primitive + exp = new SpelExpressionParser(configuration).parseExpression("3==3?3L:'foo'"); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("3"); + assertCanCompile(exp); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("3"); + assertIsCompiled(exp); + exp = new SpelExpressionParser(configuration).parseExpression("3!=3?3L:'foo'"); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("foo"); + assertCanCompile(exp); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("foo"); + assertIsCompiled(exp); + + // When the condition is an empty string + exp = new SpelExpressionParser(configuration).parseExpression("''==''?'abc':4L"); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("abc"); + assertCanCompile(exp); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("abc"); + assertIsCompiled(exp); + + // null condition + exp = new SpelExpressionParser(configuration).parseExpression("3==3?null:4L"); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo(null); + assertCanCompile(exp); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo(null); + assertIsCompiled(exp); + + // variable access returning primitive + exp = new SpelExpressionParser(configuration).parseExpression("#x==#x?50:'foo'"); + context.setVariable("x",50); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("50"); + assertCanCompile(exp); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("50"); + assertIsCompiled(exp); + + exp = new SpelExpressionParser(configuration).parseExpression("#x!=#x?50:'foo'"); + context.setVariable("x",null); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("foo"); + assertCanCompile(exp); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("foo"); + assertIsCompiled(exp); + + // variable access returning array + exp = new SpelExpressionParser(configuration).parseExpression("#x==#x?'1,2,3':'foo'"); + context.setVariable("x",new int[]{1,2,3}); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("1,2,3"); + assertCanCompile(exp); + assertThat(exp.getValue(context, new Foo(), String.class)).isEqualTo("1,2,3"); + assertIsCompiled(exp); + } + + @Test + public void repeatedCompilation() throws Exception { + // Verifying that after a number of compilations, the classloaders + // used to load the compiled expressions are discarded/replaced. + // See SpelCompiler.loadClass() + Field f = SpelExpression.class.getDeclaredField("compiledAst"); + Set classloadersUsed = new HashSet<>(); + for (int i = 0; i < 1500; i++) { // 1500 is greater than SpelCompiler.CLASSES_DEFINED_LIMIT + expression = parser.parseExpression("4 + 5"); + assertThat((int) expression.getValue(Integer.class)).isEqualTo(9); + assertCanCompile(expression); + f.setAccessible(true); + CompiledExpression cEx = (CompiledExpression) f.get(expression); + classloadersUsed.add(cEx.getClass().getClassLoader()); + assertThat((int) expression.getValue(Integer.class)).isEqualTo(9); + } + assertThat(classloadersUsed.size() > 1).isTrue(); + } + + + // Helper methods + + private SpelNodeImpl getAst() { + SpelExpression spelExpression = (SpelExpression) expression; + SpelNode ast = spelExpression.getAST(); + return (SpelNodeImpl)ast; + } + + private String stringify(Object object) { + StringBuilder s = new StringBuilder(); + if (object instanceof List) { + List ls = (List) object; + for (Object l: ls) { + s.append(l); + s.append(" "); + } + } + else if (object instanceof Object[]) { + Object[] os = (Object[]) object; + for (Object o: os) { + s.append(o); + s.append(" "); + } + } + else if (object instanceof int[]) { + int[] is = (int[]) object; + for (int i: is) { + s.append(i); + s.append(" "); + } + } + else { + s.append(object.toString()); + } + return s.toString().trim(); + } + + private void assertCanCompile(Expression expression) { + assertThat(SpelCompiler.compile(expression)).isTrue(); + } + + private void assertCantCompile(Expression expression) { + assertThat(SpelCompiler.compile(expression)).isFalse(); + } + + private Expression parse(String expression) { + return parser.parseExpression(expression); + } + + private void assertGetValueFail(Expression expression) { + assertThatExceptionOfType(Exception.class).isThrownBy(expression::getValue); + } + + public static void assertIsCompiled(Expression expression) { + try { + Field field = SpelExpression.class.getDeclaredField("compiledAst"); + field.setAccessible(true); + Object object = field.get(expression); + assertThat(object).isNotNull(); + } + catch (Exception ex) { + throw new AssertionError(ex.getMessage(), ex); + } + } + + + // Nested types + + public interface Message { + + MessageHeaders getHeaders(); + + @SuppressWarnings("rawtypes") + List getList(); + + int[] getIa(); + } + + + public static class MyMessage implements Message { + + @Override + public MessageHeaders getHeaders() { + MessageHeaders mh = new MessageHeaders(); + mh.put("command", "wibble"); + mh.put("command2", "wobble"); + return mh; + } + + @Override + public int[] getIa() { return new int[] {5,3}; } + + @Override + @SuppressWarnings({"rawtypes", "unchecked"}) + public List getList() { + List l = new ArrayList(); + l.add("wibble"); + l.add("wobble"); + return l; + } + + public String getKey() { + return "command2"; + } + + public int getKey2() { + return 1; + } + } + + + @SuppressWarnings("serial") + public static class MessageHeaders extends HashMap { + } + + + public static class GenericMessageTestHelper { + + private T payload; + + GenericMessageTestHelper(T value) { + this.payload = value; + } + + public T getPayload() { + return payload; + } + } + + + // This test helper has a bound on the type variable + public static class GenericMessageTestHelper2 { + + private T payload; + + GenericMessageTestHelper2(T value) { + this.payload = value; + } + + public T getPayload() { + return payload; + } + } + + + static class MyAccessor implements CompilablePropertyAccessor { + + private Method method; + + @Override + public Class[] getSpecificTargetClasses() { + return new Class[] {Payload2.class}; + } + + @Override + public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException { + // target is a Payload2 instance + return true; + } + + @Override + public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException { + Payload2 payload2 = (Payload2)target; + return new TypedValue(payload2.getField(name)); + } + + @Override + public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException { + return false; + } + + @Override + public void write(EvaluationContext context, Object target, String name, Object newValue) throws AccessException { + } + + @Override + public boolean isCompilable() { + return true; + } + + @Override + public Class getPropertyType() { + return Object.class; + } + + @Override + public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) { + if (method == null) { + try { + method = Payload2.class.getDeclaredMethod("getField", String.class); + } + catch (Exception ex) { + } + } + String descriptor = cf.lastDescriptor(); + String memberDeclaringClassSlashedDescriptor = method.getDeclaringClass().getName().replace('.','/'); + if (descriptor == null) { + cf.loadTarget(mv); + } + if (descriptor == null || !memberDeclaringClassSlashedDescriptor.equals(descriptor.substring(1))) { + mv.visitTypeInsn(CHECKCAST, memberDeclaringClassSlashedDescriptor); + } + mv.visitLdcInsn(propertyName); + mv.visitMethodInsn(INVOKEVIRTUAL, memberDeclaringClassSlashedDescriptor, method.getName(), + CodeFlow.createSignatureDescriptor(method), false); + } + } + + + static class CompilableMapAccessor implements CompilablePropertyAccessor { + + @Override + public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException { + Map map = (Map) target; + return map.containsKey(name); + } + + @Override + public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException { + Map map = (Map) target; + Object value = map.get(name); + if (value == null && !map.containsKey(name)) { + throw new MapAccessException(name); + } + return new TypedValue(value); + } + + @Override + public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException { + return true; + } + + @Override + @SuppressWarnings("unchecked") + public void write(EvaluationContext context, Object target, String name, Object newValue) throws AccessException { + Map map = (Map) target; + map.put(name, newValue); + } + + @Override + public Class[] getSpecificTargetClasses() { + return new Class[] {Map.class}; + } + + @Override + public boolean isCompilable() { + return true; + } + + @Override + public Class getPropertyType() { + return Object.class; + } + + @Override + public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) { + String descriptor = cf.lastDescriptor(); + if (descriptor == null) { + cf.loadTarget(mv); + } + mv.visitLdcInsn(propertyName); + mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "get","(Ljava/lang/Object;)Ljava/lang/Object;",true); + } + } + + + /** + * Exception thrown from {@code read} in order to reset a cached + * PropertyAccessor, allowing other accessors to have a try. + */ + @SuppressWarnings("serial") + private static class MapAccessException extends AccessException { + + private final String key; + + public MapAccessException(String key) { + super(null); + this.key = key; + } + + @Override + public String getMessage() { + return "Map does not contain a value for key '" + this.key + "'"; + } + } + + + public static class Greeter { + + public String getWorld() { + return "world"; + } + + public Object getObject() { + return "object"; + } + } + + public static class FooObjectHolder { + + private FooObject foo = new FooObject(); + + public FooObject getFoo() { + return foo; + } + } + + public static class FooObject { + + public Object getObject() { return "hello"; } + } + + + public static class FooString { + + public String getObject() { return "hello"; } + } + + + public static class Payload { + + Two[] DR = new Two[] {new Two()}; + + public Two holder = new Two(); + + public Two[] getDR() { + return DR; + } + } + + + public static class Payload2 { + + String var1 = "abc"; + String var2 = "def"; + + public Object getField(String name) { + if (name.equals("var1")) { + return var1; + } + else if (name.equals("var2")) { + return var2; + } + return null; + } + } + + + public static class Payload2Holder { + + public Payload2 payload2 = new Payload2(); + } + + + public class Person { + + private int age; + + public Person(int age) { + this.age = age; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + } + + + public class Person3 { + + private int age; + + public Person3(String name, int age) { + this.age = age; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + } + + + public static class Two { + + Three three = new Three(); + + public Three getThree() { + return three; + } + @Override + public String toString() { + return "instanceof Two"; + } + } + + + public static class Three { + + double four = 0.04d; + + public double getFour() { + return four; + } + } + + + public class PayloadX { + + public int valueI = 120; + public Integer valueIB = 120; + public Integer valueIB58 = 58; + public Integer valueIB60 = 60; + public long valueJ = 120L; + public Long valueJB = 120L; + public Long valueJB58 = 58L; + public Long valueJB60 = 60L; + public double valueD = 120D; + public Double valueDB = 120D; + public Double valueDB58 = 58D; + public Double valueDB60 = 60D; + public float valueF = 120F; + public Float valueFB = 120F; + public Float valueFB58 = 58F; + public Float valueFB60 = 60F; + public byte valueB = (byte)120; + public byte valueB18 = (byte)18; + public byte valueB20 = (byte)20; + public Byte valueBB = (byte)120; + public Byte valueBB18 = (byte)18; + public Byte valueBB20 = (byte)20; + public char valueC = (char)120; + public Character valueCB = (char)120; + public short valueS = (short)120; + public short valueS18 = (short)18; + public short valueS20 = (short)20; + public Short valueSB = (short)120; + public Short valueSB18 = (short)18; + public Short valueSB20 = (short)20; + + public PayloadX payload; + + public PayloadX() { + payload = this; + } + } + + + public static class TestClass1 { + + public int index1 = 1; + public int index2 = 3; + public String word = "abcd"; + } + + + public static class TestClass4 { + + public boolean a,b; + public boolean gettrue() { return true; } + public boolean getfalse() { return false; } + public boolean getA() { return a; } + public boolean getB() { return b; } + } + + + public static class TestClass10 { + + public String s = null; + + public void reset() { + s = null; + } + + public void concat(String arg) { + s = "::"+arg; + } + + public void concat(String... vargs) { + if (vargs == null) { + s = ""; + } + else { + s = ""; + for (String varg : vargs) { + s += varg; + } + } + } + + public void concat2(Object arg) { + s = "::"+arg; + } + + public void concat2(Object... vargs) { + if (vargs == null) { + s = ""; + } + else { + s = ""; + for (Object varg : vargs) { + s += varg; + } + } + } + } + + + public static class TestClass5 { + + public int i = 0; + public String s = null; + public static int _i = 0; + public static String _s = null; + + public static short s1 = (short)1; + public static short s2 = (short)2; + public static short s3 = (short)3; + + public static long l1 = 1L; + public static long l2 = 2L; + public static long l3 = 3L; + + public static float f1 = 1f; + public static float f2 = 2f; + public static float f3 = 3f; + + public static char c1 = 'a'; + public static char c2 = 'b'; + public static char c3 = 'c'; + + public static byte b1 = (byte)65; + public static byte b2 = (byte)66; + public static byte b3 = (byte)67; + + public static String[] stringArray = new String[] {"aaa","bbb","ccc"}; + public static int[] intArray = new int[] {11,22,33}; + + public Object obj = null; + + public String field = null; + + public void reset() { + i = 0; + _i = 0; + s = null; + _s = null; + field = null; + } + + public void one() { i = 1; } + + public static void two() { _i = 1; } + + public String three() { return "hello"; } + public long four() { return 3277700L; } + + public static String five() { return "hello"; } + public static long six() { return 3277700L; } + + public void seven(String toset) { s = toset; } + // public void seven(Number n) { s = n.toString(); } + + public void takeNumber(Number n) { s = n.toString(); } + public void takeString(String s) { this.s = s; } + public static void eight(String toset) { _s = toset; } + + public void nine(int toset) { i = toset; } + public static void ten(int toset) { _i = toset; } + + public void eleven(String... vargs) { + if (vargs == null) { + s = ""; + } + else { + s = ""; + for (String varg: vargs) { + s += varg; + } + } + } + + public void twelve(int... vargs) { + if (vargs == null) { + i = 0; + } + else { + i = 0; + for (int varg: vargs) { + i += varg; + } + } + } + + public void thirteen(String a, String... vargs) { + if (vargs == null) { + s = a + "::"; + } + else { + s = a+"::"; + for (String varg: vargs) { + s += varg; + } + } + } + + public void arrayz(boolean... bs) { + s = ""; + if (bs != null) { + s = ""; + for (boolean b: bs) { + s += Boolean.toString(b); + } + } + } + + public void arrays(short... ss) { + s = ""; + if (ss != null) { + s = ""; + for (short s: ss) { + this.s += Short.toString(s); + } + } + } + + public void arrayd(double... vargs) { + s = ""; + if (vargs != null) { + s = ""; + for (double v: vargs) { + this.s += Double.toString(v); + } + } + } + + public void arrayf(float... vargs) { + s = ""; + if (vargs != null) { + s = ""; + for (float v: vargs) { + this.s += Float.toString(v); + } + } + } + + public void arrayj(long... vargs) { + s = ""; + if (vargs != null) { + s = ""; + for (long v: vargs) { + this.s += Long.toString(v); + } + } + } + + public void arrayb(byte... vargs) { + s = ""; + if (vargs != null) { + s = ""; + for (Byte v: vargs) { + this.s += Byte.toString(v); + } + } + } + + public void arrayc(char... vargs) { + s = ""; + if (vargs != null) { + s = ""; + for (char v: vargs) { + this.s += Character.toString(v); + } + } + } + + public void fourteen(String a, String[]... vargs) { + if (vargs == null) { + s = a+"::"; + } + else { + s = a+"::"; + for (String[] varg: vargs) { + s += "{"; + for (String v: varg) { + s += v; + } + s += "}"; + } + } + } + + public void fifteen(String a, int[]... vargs) { + if (vargs == null) { + s = a+"::"; + } + else { + s = a+"::"; + for (int[] varg: vargs) { + s += "{"; + for (int v: varg) { + s += Integer.toString(v); + } + s += "}"; + } + } + } + + public void sixteen(Object... vargs) { + if (vargs == null) { + s = ""; + } + else { + s = ""; + for (Object varg: vargs) { + s += varg; + } + } + } + } + + + public static class TestClass6 { + + public String orange = "value1"; + public static String apple = "value2"; + public long peach = 34L; + + public String getBanana() { + return "value3"; + } + + public static String getPlum() { + return "value4"; + } + + public String strawberry() { + return "value5"; + } + } + + + public static class TestClass7 { + + public static String property; + + static { + String s = "UK 123"; + StringTokenizer st = new StringTokenizer(s); + property = st.nextToken(); + } + + public static void reset() { + String s = "UK 123"; + StringTokenizer st = new StringTokenizer(s); + property = st.nextToken(); + } + } + + + public static class TestClass8 { + + public int i; + public String s; + public double d; + public boolean z; + + public TestClass8(int i, String s, double d, boolean z) { + this.i = i; + this.s = s; + this.d = d; + this.z = z; + } + + public TestClass8() { + } + + public TestClass8(Integer i) { + this.i = i; + } + + @SuppressWarnings("unused") + private TestClass8(String a, String b) { + this.s = a+b; + } + } + + + public static class Obj { + + private final String param1; + + public Obj(String param1){ + this.param1 = param1; + } + } + + + public static class Obj2 { + + public final String output; + + public Obj2(String... params){ + StringBuilder b = new StringBuilder(); + for (String param: params) { + b.append(param); + } + output = b.toString(); + } + } + + + public static class Obj3 { + + public final String output; + + public Obj3(int... params) { + StringBuilder b = new StringBuilder(); + for (int param: params) { + b.append(Integer.toString(param)); + } + output = b.toString(); + } + + public Obj3(String s, Float f, int... ints) { + StringBuilder b = new StringBuilder(); + b.append(s); + b.append(":"); + b.append(Float.toString(f)); + b.append(":"); + for (int param: ints) { + b.append(Integer.toString(param)); + } + output = b.toString(); + } + } + + + public static class Obj4 { + + public final String output; + + public Obj4(int[] params) { + StringBuilder b = new StringBuilder(); + for (int param: params) { + b.append(Integer.toString(param)); + } + output = b.toString(); + } + } + + + @SuppressWarnings("unused") + private static class TestClass9 { + + public TestClass9(int i) { + } + } + + + // These test classes simulate a pattern of public/private classes seen in Spring Security + + // final class HttpServlet3RequestFactory implements HttpServletRequestFactory + static class HttpServlet3RequestFactory { + + static Servlet3SecurityContextHolderAwareRequestWrapper getOne() { + HttpServlet3RequestFactory outer = new HttpServlet3RequestFactory(); + return outer.new Servlet3SecurityContextHolderAwareRequestWrapper(); + } + + // private class Servlet3SecurityContextHolderAwareRequestWrapper extends SecurityContextHolderAwareRequestWrapper + private class Servlet3SecurityContextHolderAwareRequestWrapper extends SecurityContextHolderAwareRequestWrapper { + } + } + + + // public class SecurityContextHolderAwareRequestWrapper extends HttpServletRequestWrapper + static class SecurityContextHolderAwareRequestWrapper extends HttpServletRequestWrapper { + } + + + public static class HttpServletRequestWrapper { + + public String getServletPath() { + return "wibble"; + } + } + + + // Here the declaring class is not public + static class SomeCompareMethod { + + // method not public + static int compare(Object o1, Object o2) { + return -1; + } + + // public + public static int compare2(Object o1, Object o2) { + return -1; + } + } + + + public static class SomeCompareMethod2 { + + public static int negate(int i1) { + return -i1; + } + + public static String append(String... strings) { + StringBuilder b = new StringBuilder(); + for (String string : strings) { + b.append(string); + } + return b.toString(); + } + + public static String append2(Object... objects) { + StringBuilder b = new StringBuilder(); + for (Object object : objects) { + b.append(object.toString()); + } + return b.toString(); + } + + public static String append3(String[] strings) { + StringBuilder b = new StringBuilder(); + for (String string : strings) { + b.append(string); + } + return b.toString(); + } + + public static String append4(String s, String... strings) { + StringBuilder b = new StringBuilder(); + b.append(s).append("::"); + for (String string : strings) { + b.append(string); + } + return b.toString(); + } + + public static String appendChar(char... values) { + StringBuilder b = new StringBuilder(); + for (char ch : values) { + b.append(ch); + } + return b.toString(); + } + + public static int sum(int... ints) { + int total = 0; + for (int i : ints) { + total += i; + } + return total; + } + + public static int sumDouble(double... values) { + int total = 0; + for (double i : values) { + total += i; + } + return total; + } + + public static int sumFloat(float... values) { + int total = 0; + for (float i : values) { + total += i; + } + return total; + } + } + + + public static class DelegatingStringFormat { + + public static String format(String s, Object... args) { + return String.format(s, args); + } + } + + + public static class StaticsHelper { + + static StaticsHelper sh = new StaticsHelper(); + public static StaticsHelper fielda = sh; + public static String fieldb = "fb"; + + public static StaticsHelper methoda() { + return sh; + } + public static String methodb() { + return "mb"; + } + + public static StaticsHelper getPropertya() { + return sh; + } + + public static String getPropertyb() { + return "pb"; + } + + @Override + public String toString() { + return "sh"; + } + } + + + public static class Apple implements Comparable { + + public Object gotComparedTo = null; + public int i; + + public Apple(int i) { + this.i = i; + } + + public void setValue(int i) { + this.i = i; + } + + @Override + public int compareTo(Apple that) { + this.gotComparedTo = that; + if (this.i < that.i) { + return -1; + } + else if (this.i > that.i) { + return +1; + } + else { + return 0; + } + } + } + + + // For opNe_SPR14863 + public static class MyContext { + + private final Map data; + + public MyContext(Map data) { + this.data = data; + } + + public Map getData() { + return data; + } + } + + + public static class Foo { + + public String bar() { + return "BAR"; + } + + public String bar(String arg) { + return arg.toUpperCase(); + } + } + + + public static class RecordHolder { + + public Map record = new HashMap<>(); + + public LongHolder expression = new LongHolder(); + + public void add(String key, Long value) { + record.put(key, value); + } + + public long get(String key) { + return record.get(key); + } + } + + + public static class LongHolder { + + public Long someLong = 3L; + } + + + public class Reg { + + private Integer _value,_value2; + private Long _valueL,_valueL2; + private Double _valueD,_valueD2; + private Float _valueF,_valueF2; + + public Reg(int v) { + this._value = v; + this._valueL = new Long(v); + this._valueD = new Double(v); + this._valueF = new Float(v); + } + + public Integer getValue() { + return _value; + } + + public Long getValueL() { + return _valueL; + } + + public Double getValueD() { + return _valueD; + } + + public Float getValueF() { + return _valueF; + } + + public Integer getValue2() { + return _value2; + } + + public Long getValueL2() { + return _valueL2; + } + + public Double getValueD2() { + return _valueD2; + } + + public Float getValueF2() { + return _valueF2; + } + + public void setValue(Integer value) { + _value = value; + _valueL = value==null?null:new Long(value); + _valueD = value==null?null:new Double(value); + _valueF = value==null?null:new Float(value); + } + + public void setValue2(Integer value) { + _value2 = value; + _valueL2 = value==null?null:new Long(value); + _valueD2 = value==null?null:new Double(value); + _valueF2 = value==null?null:new Float(value); + } + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationPerformanceTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationPerformanceTests.java new file mode 100644 index 0000000..a4609f4 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationPerformanceTests.java @@ -0,0 +1,733 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelCompiler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +/** + * Checks the speed of compiled SpEL expressions. + * + *

    By default these tests are marked @Disabled since they can fail on a busy machine + * because they compare relative performance of interpreted vs compiled. + * + * @author Andy Clement + * @since 4.1 + */ +@Disabled +public class SpelCompilationPerformanceTests extends AbstractExpressionTests { + + int count = 50000; // number of evaluations that are timed in one run + + int iterations = 10; // number of times to repeat 'count' evaluations (for averaging) + + private final static boolean noisyTests = true; + + Expression expression; + + + /** + * This test verifies the new support for compiling mathematical expressions with + * different operand types. + */ + @Test + public void compilingMathematicalExpressionsWithDifferentOperandTypes() throws Exception { + NumberHolder nh = new NumberHolder(); + expression = parser.parseExpression("(T(Integer).valueOf(payload).doubleValue())/18D"); + Object o = expression.getValue(nh); + assertThat(o).isEqualTo(2d); + System.out.println("Performance check for SpEL expression: '(T(Integer).valueOf(payload).doubleValue())/18D'"); + long stime = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + o = expression.getValue(nh); + } + System.out.println("One million iterations: " + (System.currentTimeMillis()-stime) + "ms"); + stime = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + o = expression.getValue(nh); + } + System.out.println("One million iterations: " + (System.currentTimeMillis()-stime) + "ms"); + stime = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + o = expression.getValue(nh); + } + System.out.println("One million iterations: " + (System.currentTimeMillis()-stime) + "ms"); + compile(expression); + System.out.println("Now compiled:"); + o = expression.getValue(nh); + assertThat(o).isEqualTo(2d); + + stime = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + o = expression.getValue(nh); + } + System.out.println("One million iterations: " + (System.currentTimeMillis()-stime) + "ms"); + stime = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + o = expression.getValue(nh); + } + System.out.println("One million iterations: " + (System.currentTimeMillis()-stime) + "ms"); + stime = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + o = expression.getValue(nh); + } + System.out.println("One million iterations: " + (System.currentTimeMillis()-stime) + "ms"); + + expression = parser.parseExpression("payload/18D"); + o = expression.getValue(nh); + assertThat(o).isEqualTo(2d); + System.out.println("Performance check for SpEL expression: 'payload/18D'"); + stime = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + o = expression.getValue(nh); + } + System.out.println("One million iterations: " + (System.currentTimeMillis()-stime) + "ms"); + stime = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + o = expression.getValue(nh); + } + System.out.println("One million iterations: " + (System.currentTimeMillis()-stime) + "ms"); + stime = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + o = expression.getValue(nh); + } + System.out.println("One million iterations: " + (System.currentTimeMillis()-stime) + "ms"); + compile(expression); + System.out.println("Now compiled:"); + o = expression.getValue(nh); + assertThat(o).isEqualTo(2d); + + stime = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + o = expression.getValue(nh); + } + System.out.println("One million iterations: " + (System.currentTimeMillis()-stime) + "ms"); + stime = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + o = expression.getValue(nh); + } + System.out.println("One million iterations: " + (System.currentTimeMillis()-stime) + "ms"); + stime = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + o = expression.getValue(nh); + } + System.out.println("One million iterations: " + (System.currentTimeMillis()-stime) + "ms"); + } + + @Test + public void inlineLists() throws Exception { + expression = parser.parseExpression("{'abcde','ijklm'}[0].substring({1,3,4}[0],{1,3,4}[1])"); + Object o = expression.getValue(); + assertThat(o).isEqualTo("bc"); + System.out.println("Performance check for SpEL expression: '{'abcde','ijklm'}[0].substring({1,3,4}[0],{1,3,4}[1])'"); + long stime = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + o = expression.getValue(); + } + System.out.println("One million iterations: " + (System.currentTimeMillis()-stime) + "ms"); + stime = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + o = expression.getValue(); + } + System.out.println("One million iterations: " + (System.currentTimeMillis()-stime) + "ms"); + stime = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + o = expression.getValue(); + } + System.out.println("One million iterations: " + (System.currentTimeMillis()-stime) + "ms"); + compile(expression); + System.out.println("Now compiled:"); + o = expression.getValue(); + assertThat(o).isEqualTo("bc"); + + stime = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + o = expression.getValue(); + } + System.out.println("One million iterations: " + (System.currentTimeMillis()-stime) + "ms"); + stime = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + o = expression.getValue(); + } + System.out.println("One million iterations: " + (System.currentTimeMillis()-stime) + "ms"); + stime = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + o = expression.getValue(); + } + System.out.println("One million iterations: " + (System.currentTimeMillis()-stime) + "ms"); + } + + @Test + public void inlineNestedLists() throws Exception { + expression = parser.parseExpression("{'abcde',{'ijklm','nopqr'}}[1][0].substring({1,3,4}[0],{1,3,4}[1])"); + Object o = expression.getValue(); + assertThat(o).isEqualTo("jk"); + System.out.println("Performance check for SpEL expression: '{'abcde','ijklm'}[0].substring({1,3,4}[0],{1,3,4}[1])'"); + long stime = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + o = expression.getValue(); + } + System.out.println("One million iterations: " + (System.currentTimeMillis()-stime) + "ms"); + stime = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + o = expression.getValue(); + } + System.out.println("One million iterations: " + (System.currentTimeMillis()-stime) + "ms"); + stime = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + o = expression.getValue(); + } + System.out.println("One million iterations: " + (System.currentTimeMillis()-stime) + "ms"); + compile(expression); + System.out.println("Now compiled:"); + o = expression.getValue(); + assertThat(o).isEqualTo("jk"); + + stime = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + o = expression.getValue(); + } + System.out.println("One million iterations: " + (System.currentTimeMillis()-stime) + "ms"); + stime = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + o = expression.getValue(); + } + System.out.println("One million iterations: " + (System.currentTimeMillis()-stime) + "ms"); + stime = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + o = expression.getValue(); + } + System.out.println("One million iterations: " + (System.currentTimeMillis()-stime) + "ms"); + } + + @Test + public void stringConcatenation() throws Exception { + expression = parser.parseExpression("'hello' + getWorld() + ' spring'"); + Greeter g = new Greeter(); + Object o = expression.getValue(g); + assertThat(o).isEqualTo("helloworld spring"); + + System.out.println("Performance check for SpEL expression: 'hello' + getWorld() + ' spring'"); + long stime = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + o = expression.getValue(g); + } + System.out.println("One million iterations: " + (System.currentTimeMillis()-stime) + "ms"); + stime = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + o = expression.getValue(g); + } + System.out.println("One million iterations: " + (System.currentTimeMillis()-stime) + "ms"); + stime = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + o = expression.getValue(g); + } + System.out.println("One million iterations: " + (System.currentTimeMillis()-stime) + "ms"); + compile(expression); + System.out.println("Now compiled:"); + o = expression.getValue(g); + assertThat(o).isEqualTo("helloworld spring"); + + stime = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + o = expression.getValue(g); + } + System.out.println("One million iterations: " + (System.currentTimeMillis()-stime) + "ms"); + stime = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + o = expression.getValue(g); + } + System.out.println("One million iterations: " + (System.currentTimeMillis()-stime) + "ms"); + stime = System.currentTimeMillis(); + for (int i = 0; i < 1000000; i++) { + o = expression.getValue(g); + } + System.out.println("One million iterations: " + (System.currentTimeMillis()-stime) + "ms"); + } + + @Test + public void complexExpressionPerformance() throws Exception { + Payload payload = new Payload(); + Expression expression = parser.parseExpression("DR[0].DRFixedSection.duration lt 0.1"); + boolean b = false; + long iTotal = 0,cTotal = 0; + + // warmup + for (int i = 0; i < count; i++) { + b = expression.getValue(payload, Boolean.TYPE); + } + + log("timing interpreted: "); + for (int i = 0; i < iterations; i++) { + long stime = System.currentTimeMillis(); + for (int j = 0; j < count; j++) { + b = expression.getValue(payload, Boolean.TYPE); + } + long etime = System.currentTimeMillis(); + long interpretedSpeed = (etime - stime); + iTotal += interpretedSpeed; + log(interpretedSpeed + "ms "); + } + logln(); + + compile(expression); + boolean bc = false; + expression.getValue(payload, Boolean.TYPE); + log("timing compiled: "); + for (int i = 0; i < iterations; i++) { + long stime = System.currentTimeMillis(); + for (int j = 0; j < count; j++) { + bc = expression.getValue(payload, Boolean.TYPE); + } + long etime = System.currentTimeMillis(); + long compiledSpeed = (etime - stime); + cTotal += compiledSpeed; + log(compiledSpeed + "ms "); + } + logln(); + + reportPerformance("complex expression",iTotal, cTotal); + + // Verify the result + assertThat(b).isFalse(); + + // Verify the same result for compiled vs interpreted + assertThat(bc).isEqualTo(b); + + // Verify if the input changes, the result changes + payload.DR[0].DRFixedSection.duration = 0.04d; + bc = expression.getValue(payload, Boolean.TYPE); + assertThat(bc).isTrue(); + } + + public static class HW { + public String hello() { + return "foobar"; + } + } + + @Test + public void compilingMethodReference() throws Exception { + long interpretedTotal = 0, compiledTotal = 0; + long stime,etime; + String interpretedResult = null,compiledResult = null; + + HW testdata = new HW(); + Expression expression = parser.parseExpression("hello()"); + + // warmup + for (int i = 0; i < count; i++) { + interpretedResult = expression.getValue(testdata, String.class); + } + + log("timing interpreted: "); + for (int i = 0; i < iterations; i++) { + stime = System.currentTimeMillis(); + for (int j = 0; j < count; j++) { + interpretedResult = expression.getValue(testdata, String.class); + } + etime = System.currentTimeMillis(); + long interpretedSpeed = (etime - stime); + interpretedTotal += interpretedSpeed; + log(interpretedSpeed + "ms "); + } + logln(); + + compile(expression); + + log("timing compiled: "); + expression.getValue(testdata, String.class); + for (int i = 0; i < iterations; i++) { + stime = System.currentTimeMillis(); + for (int j = 0; j < count; j++) { + compiledResult = expression.getValue(testdata, String.class); + } + etime = System.currentTimeMillis(); + long compiledSpeed = (etime - stime); + compiledTotal += compiledSpeed; + log(compiledSpeed + "ms "); + } + logln(); + + assertThat(compiledResult).isEqualTo(interpretedResult); + reportPerformance("method reference", interpretedTotal, compiledTotal); + if (compiledTotal >= interpretedTotal) { + fail("Compiled version is slower than interpreted!"); + } + } + + + + + @Test + public void compilingPropertyReferenceField() throws Exception { + long interpretedTotal = 0, compiledTotal = 0, stime, etime; + String interpretedResult = null, compiledResult = null; + + TestClass2 testdata = new TestClass2(); + Expression expression = parser.parseExpression("name"); + + // warmup + for (int i = 0; i < count; i++) { + expression.getValue(testdata, String.class); + } + + log("timing interpreted: "); + for (int i = 0; i < iterations; i++) { + stime = System.currentTimeMillis(); + for (int j = 0; j < count; j++) { + interpretedResult = expression.getValue(testdata, String.class); + } + etime = System.currentTimeMillis(); + long interpretedSpeed = (etime - stime); + interpretedTotal += interpretedSpeed; + log(interpretedSpeed + "ms "); + } + logln(); + + compile(expression); + + log("timing compiled: "); + expression.getValue(testdata, String.class); + for (int i = 0; i < iterations; i++) { + stime = System.currentTimeMillis(); + for (int j = 0; j < count; j++) { + compiledResult = expression.getValue(testdata, String.class); + } + etime = System.currentTimeMillis(); + long compiledSpeed = (etime - stime); + compiledTotal += compiledSpeed; + log(compiledSpeed + "ms "); + } + logln(); + + assertThat(compiledResult).isEqualTo(interpretedResult); + reportPerformance("property reference (field)",interpretedTotal, compiledTotal); + } + + @Test + public void compilingPropertyReferenceNestedField() throws Exception { + long interpretedTotal = 0, compiledTotal = 0, stime, etime; + String interpretedResult = null, compiledResult = null; + + TestClass2 testdata = new TestClass2(); + Expression expression = parser.parseExpression("foo.bar.boo"); + + // warmup + for (int i = 0; i < count; i++) { + expression.getValue(testdata, String.class); + } + + log("timing interpreted: "); + for (int i = 0; i < iterations; i++) { + stime = System.currentTimeMillis(); + for (int j = 0; j < count; j++) { + interpretedResult = expression.getValue(testdata, String.class); + } + etime = System.currentTimeMillis(); + long interpretedSpeed = (etime - stime); + interpretedTotal += interpretedSpeed; + log(interpretedSpeed + "ms "); + } + logln(); + + compile(expression); + + log("timing compiled: "); + expression.getValue(testdata, String.class); + for (int i = 0; i < iterations; i++) { + stime = System.currentTimeMillis(); + for (int j = 0; j < count; j++) { + compiledResult = expression.getValue(testdata, String.class); + } + etime = System.currentTimeMillis(); + long compiledSpeed = (etime - stime); + compiledTotal += compiledSpeed; + log(compiledSpeed + "ms "); + } + logln(); + + assertThat(compiledResult).isEqualTo(interpretedResult); + reportPerformance("property reference (nested field)",interpretedTotal, compiledTotal); + } + + @Test + public void compilingPropertyReferenceNestedMixedFieldGetter() throws Exception { + long interpretedTotal = 0, compiledTotal = 0, stime, etime; + String interpretedResult = null, compiledResult = null; + + TestClass2 testdata = new TestClass2(); + Expression expression = parser.parseExpression("foo.baz.boo"); + + // warmup + for (int i = 0; i < count; i++) { + expression.getValue(testdata, String.class); + } + log("timing interpreted: "); + for (int i = 0; i < iterations; i++) { + stime = System.currentTimeMillis(); + for (int j = 0; j < count; j++) { + interpretedResult = expression.getValue(testdata, String.class); + } + etime = System.currentTimeMillis(); + long interpretedSpeed = (etime - stime); + interpretedTotal += interpretedSpeed; + log(interpretedSpeed + "ms "); + } + logln(); + + compile(expression); + + log("timing compiled: "); + expression.getValue(testdata, String.class); + for (int i = 0; i < iterations; i++) { + stime = System.currentTimeMillis(); + for (int j = 0; j < count; j++) { + compiledResult = expression.getValue(testdata, String.class); + } + etime = System.currentTimeMillis(); + long compiledSpeed = (etime - stime); + compiledTotal += compiledSpeed; + log(compiledSpeed + "ms "); + } + logln(); + + assertThat(compiledResult).isEqualTo(interpretedResult); + reportPerformance("nested property reference (mixed field/getter)",interpretedTotal, compiledTotal); + } + + @Test + public void compilingNestedMixedFieldPropertyReferenceMethodReference() throws Exception { + long interpretedTotal = 0, compiledTotal = 0, stime, etime; + String interpretedResult = null, compiledResult = null; + + TestClass2 testdata = new TestClass2(); + Expression expression = parser.parseExpression("foo.bay().boo"); + + // warmup + for (int i = 0; i < count; i++) { + expression.getValue(testdata, String.class); + } + + log("timing interpreted: "); + for (int i = 0; i < iterations; i++) { + stime = System.currentTimeMillis(); + for (int j = 0; j < count; j++) { + interpretedResult = expression.getValue(testdata, String.class); + } + etime = System.currentTimeMillis(); + long interpretedSpeed = (etime - stime); + interpretedTotal += interpretedSpeed; + log(interpretedSpeed + "ms "); + } + logln(); + + compile(expression); + + log("timing compiled: "); + expression.getValue(testdata, String.class); + for (int i = 0; i < iterations; i++) { + stime = System.currentTimeMillis(); + for (int j = 0; j < count; j++) { + compiledResult = expression.getValue(testdata, String.class); + } + etime = System.currentTimeMillis(); + long compiledSpeed = (etime - stime); + compiledTotal += compiledSpeed; + log(compiledSpeed + "ms "); + + } + logln(); + + assertThat(compiledResult).isEqualTo(interpretedResult); + reportPerformance("nested reference (mixed field/method)", interpretedTotal, compiledTotal); + } + + @Test + public void compilingPropertyReferenceGetter() throws Exception { + long interpretedTotal = 0, compiledTotal = 0, stime, etime; + String interpretedResult = null, compiledResult = null; + + TestClass2 testdata = new TestClass2(); + Expression expression = parser.parseExpression("name2"); + + // warmup + for (int i = 0;i < count; i++) { + expression.getValue(testdata, String.class); + } + + log("timing interpreted: "); + for (int i = 0; i < iterations; i++) { + stime = System.currentTimeMillis(); + for (int j = 0; j < count; j++) { + interpretedResult = expression.getValue(testdata, String.class); + } + etime = System.currentTimeMillis(); + long interpretedSpeed = (etime - stime); + interpretedTotal += interpretedSpeed; + log(interpretedSpeed + "ms "); + } + logln(); + + + compile(expression); + + log("timing compiled: "); + expression.getValue(testdata, String.class); + for (int i = 0; i < iterations; i++) { + stime = System.currentTimeMillis(); + for (int j = 0; j < count; j++) { + compiledResult = expression.getValue(testdata, String.class); + } + etime = System.currentTimeMillis(); + long compiledSpeed = (etime - stime); + compiledTotal += compiledSpeed; + log(compiledSpeed + "ms "); + + } + logln(); + + assertThat(compiledResult).isEqualTo(interpretedResult); + + reportPerformance("property reference (getter)", interpretedTotal, compiledTotal); + if (compiledTotal >= interpretedTotal) { + fail("Compiled version is slower than interpreted!"); + } + } + + + private void reportPerformance(String title, long interpretedTotal, long compiledTotal) { + double averageInterpreted = interpretedTotal / iterations; + double averageCompiled = compiledTotal / iterations; + double ratio = (averageCompiled / averageInterpreted) * 100.0d; + logln(">>" + title + ": average for " + count + ": compiled=" + averageCompiled + + "ms interpreted=" + averageInterpreted + "ms: compiled takes " + + ((int) ratio) + "% of the interpreted time"); + if (averageCompiled > averageInterpreted) { + fail("Compiled version took longer than interpreted! CompiledSpeed=~" + averageCompiled + + "ms InterpretedSpeed=" + averageInterpreted + "ms"); + } + logln(); + } + + private void log(String message) { + if (noisyTests) { + System.out.print(message); + } + } + + private void logln(String... message) { + if (noisyTests) { + if (message.length > 0) { + System.out.println(message[0]); + } + else { + System.out.println(); + } + } + } + + private void compile(Expression expression) { + assertThat(SpelCompiler.compile(expression)).isTrue(); + } + + + public static class Payload { + + Two[] DR = new Two[]{new Two()}; + + public Two[] getDR() { + return DR; + } + } + + + public static class Two { + + Three DRFixedSection = new Three(); + + public Three getDRFixedSection() { + return DRFixedSection; + } + } + + + public static class Three { + + double duration = 0.4d; + + public double getDuration() { + return duration; + } + } + + + public static class NumberHolder { + + public int payload = 36; + } + + + public static class Greeter { + + public String getWorld() { + return "world"; + } + } + + public static class TestClass2 { + + public String name = "Santa"; + + private String name2 = "foobar"; + + public String getName2() { + return name2; + } + + public Foo foo = new Foo(); + } + + + public static class Foo { + + public Bar bar = new Bar(); + + Bar b = new Bar(); + + public Bar getBaz() { + return b; + } + + public Bar bay() { + return b; + } + } + + + public static class Bar { + + public String boo = "oranges"; + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java new file mode 100644 index 0000000..dcc4511 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java @@ -0,0 +1,519 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.ParserContext; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.expression.spel.testresources.Inventor; +import org.springframework.expression.spel.testresources.PlaceOfBirth; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.within; + +/** + * Test the examples specified in the documentation. + * + * NOTE: any outgoing changes from this file upon synchronizing with the repo may indicate that + * you need to update the documentation too ! + * + * @author Andy Clement + */ +@SuppressWarnings("rawtypes") +public class SpelDocumentationTests extends AbstractExpressionTests { + + static Inventor tesla ; + static Inventor pupin ; + + static { + GregorianCalendar c = new GregorianCalendar(); + c.set(1856, 7, 9); + tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian"); + tesla.setPlaceOfBirth(new PlaceOfBirth("SmilJan")); + tesla.setInventions(new String[] { "Telephone repeater", "Rotating magnetic field principle", + "Polyphase alternating-current system", "Induction motor", "Alternating-current power transmission", + "Tesla coil transformer", "Wireless communication", "Radio", "Fluorescent lights" }); + + pupin = new Inventor("Pupin", c.getTime(), "Idvor"); + pupin.setPlaceOfBirth(new PlaceOfBirth("Idvor")); + + } + static class IEEE { + private String name; + + + public Inventor[] Members = new Inventor[1]; + public List Members2 = new ArrayList(); + public Map officers = new HashMap<>(); + + public List> reverse = new ArrayList<>(); + + @SuppressWarnings("unchecked") + IEEE() { + officers.put("president",pupin); + List linv = new ArrayList(); + linv.add(tesla); + officers.put("advisors",linv); + Members2.add(tesla); + Members2.add(pupin); + + reverse.add(officers); + } + + public boolean isMember(String name) { + return true; + } + + public String getName() { return name; } + public void setName(String n) { this.name = n; } + } + + @Test + public void testMethodInvocation() { + evaluate("'Hello World'.concat('!')","Hello World!",String.class); + } + + @Test + public void testBeanPropertyAccess() { + evaluate("new String('Hello World'.bytes)","Hello World",String.class); + } + + @Test + public void testArrayLengthAccess() { + evaluate("'Hello World'.bytes.length",11,Integer.class); + } + + @Test + public void testRootObject() throws Exception { + GregorianCalendar c = new GregorianCalendar(); + c.set(1856, 7, 9); + + // The constructor arguments are name, birthday, and nationaltiy. + Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian"); + + ExpressionParser parser = new SpelExpressionParser(); + Expression exp = parser.parseExpression("name"); + + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setRootObject(tesla); + + String name = (String) exp.getValue(context); + assertThat(name).isEqualTo("Nikola Tesla"); + } + + @Test + public void testEqualityCheck() throws Exception { + ExpressionParser parser = new SpelExpressionParser(); + + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setRootObject(tesla); + + Expression exp = parser.parseExpression("name == 'Nikola Tesla'"); + boolean isEqual = exp.getValue(context, Boolean.class); // evaluates to true + assertThat(isEqual).isTrue(); + } + + // Section 7.4.1 + + @Test + public void testXMLBasedConfig() { + evaluate("(T(java.lang.Math).random() * 100.0 )>0",true,Boolean.class); + } + + // Section 7.5 + @Test + public void testLiterals() throws Exception { + ExpressionParser parser = new SpelExpressionParser(); + + String helloWorld = (String) parser.parseExpression("'Hello World'").getValue(); // evals to "Hello World" + assertThat(helloWorld).isEqualTo("Hello World"); + + double avogadrosNumber = (Double) parser.parseExpression("6.0221415E+23").getValue(); + assertThat(avogadrosNumber).isCloseTo(6.0221415E+23, within((double) 0)); + + int maxValue = (Integer) parser.parseExpression("0x7FFFFFFF").getValue(); // evals to 2147483647 + assertThat(maxValue).isEqualTo(Integer.MAX_VALUE); + + boolean trueValue = (Boolean) parser.parseExpression("true").getValue(); + assertThat(trueValue).isTrue(); + + Object nullValue = parser.parseExpression("null").getValue(); + assertThat(nullValue).isNull(); + } + + @Test + public void testPropertyAccess() throws Exception { + EvaluationContext context = TestScenarioCreator.getTestEvaluationContext(); + int year = (Integer) parser.parseExpression("Birthdate.Year + 1900").getValue(context); // 1856 + assertThat(year).isEqualTo(1856); + + String city = (String) parser.parseExpression("placeOfBirth.City").getValue(context); + assertThat(city).isEqualTo("SmilJan"); + } + + @Test + public void testPropertyNavigation() throws Exception { + ExpressionParser parser = new SpelExpressionParser(); + + // Inventions Array + StandardEvaluationContext teslaContext = TestScenarioCreator.getTestEvaluationContext(); +// teslaContext.setRootObject(tesla); + + // evaluates to "Induction motor" + String invention = parser.parseExpression("inventions[3]").getValue(teslaContext, String.class); + assertThat(invention).isEqualTo("Induction motor"); + + // Members List + StandardEvaluationContext societyContext = new StandardEvaluationContext(); + IEEE ieee = new IEEE(); + ieee.Members[0]= tesla; + societyContext.setRootObject(ieee); + + // evaluates to "Nikola Tesla" + String name = parser.parseExpression("Members[0].Name").getValue(societyContext, String.class); + assertThat(name).isEqualTo("Nikola Tesla"); + + // List and Array navigation + // evaluates to "Wireless communication" + invention = parser.parseExpression("Members[0].Inventions[6]").getValue(societyContext, String.class); + assertThat(invention).isEqualTo("Wireless communication"); + } + + + @Test + public void testDictionaryAccess() throws Exception { + StandardEvaluationContext societyContext = new StandardEvaluationContext(); + societyContext.setRootObject(new IEEE()); + // Officer's Dictionary + Inventor pupin = parser.parseExpression("officers['president']").getValue(societyContext, Inventor.class); + assertThat(pupin).isNotNull(); + + // evaluates to "Idvor" + String city = parser.parseExpression("officers['president'].PlaceOfBirth.city").getValue(societyContext, String.class); + assertThat(city).isNotNull(); + + // setting values + Inventor i = parser.parseExpression("officers['advisors'][0]").getValue(societyContext,Inventor.class); + assertThat(i.getName()).isEqualTo("Nikola Tesla"); + + parser.parseExpression("officers['advisors'][0].PlaceOfBirth.Country").setValue(societyContext, "Croatia"); + + Inventor i2 = parser.parseExpression("reverse[0]['advisors'][0]").getValue(societyContext,Inventor.class); + assertThat(i2.getName()).isEqualTo("Nikola Tesla"); + + } + + // 7.5.3 + + @Test + public void testMethodInvocation2() throws Exception { + // string literal, evaluates to "bc" + String c = parser.parseExpression("'abc'.substring(1, 3)").getValue(String.class); + assertThat(c).isEqualTo("bc"); + + StandardEvaluationContext societyContext = new StandardEvaluationContext(); + societyContext.setRootObject(new IEEE()); + // evaluates to true + boolean isMember = parser.parseExpression("isMember('Mihajlo Pupin')").getValue(societyContext, Boolean.class); + assertThat(isMember).isTrue(); + } + + // 7.5.4.1 + + @Test + public void testRelationalOperators() throws Exception { + boolean result = parser.parseExpression("2 == 2").getValue(Boolean.class); + assertThat(result).isTrue(); + // evaluates to false + result = parser.parseExpression("2 < -5.0").getValue(Boolean.class); + assertThat(result).isFalse(); + + // evaluates to true + result = parser.parseExpression("'black' < 'block'").getValue(Boolean.class); + assertThat(result).isTrue(); + } + + @Test + public void testOtherOperators() throws Exception { + // evaluates to false + boolean falseValue = parser.parseExpression("'xyz' instanceof T(int)").getValue(Boolean.class); + assertThat(falseValue).isFalse(); + + // evaluates to true + boolean trueValue = parser.parseExpression("'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); + assertThat(trueValue).isTrue(); + + //evaluates to false + falseValue = parser.parseExpression("'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); + assertThat(falseValue).isFalse(); + } + + // 7.5.4.2 + + @Test + public void testLogicalOperators() throws Exception { + + StandardEvaluationContext societyContext = new StandardEvaluationContext(); + societyContext.setRootObject(new IEEE()); + + // -- AND -- + + // evaluates to false + boolean falseValue = parser.parseExpression("true and false").getValue(Boolean.class); + assertThat(falseValue).isFalse(); + // evaluates to true + String expression = "isMember('Nikola Tesla') and isMember('Mihajlo Pupin')"; + boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class); + + // -- OR -- + + // evaluates to true + trueValue = parser.parseExpression("true or false").getValue(Boolean.class); + assertThat(trueValue).isTrue(); + + // evaluates to true + expression = "isMember('Nikola Tesla') or isMember('Albert Einstien')"; + trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class); + assertThat(trueValue).isTrue(); + + // -- NOT -- + + // evaluates to false + falseValue = parser.parseExpression("!true").getValue(Boolean.class); + assertThat(falseValue).isFalse(); + + + // -- AND and NOT -- + expression = "isMember('Nikola Tesla') and !isMember('Mihajlo Pupin')"; + falseValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class); + assertThat(falseValue).isFalse(); + } + + // 7.5.4.3 + + @Test + public void testNumericalOperators() throws Exception { + // Addition + int two = parser.parseExpression("1 + 1").getValue(Integer.class); // 2 + assertThat(two).isEqualTo(2); + + String testString = parser.parseExpression("'test' + ' ' + 'string'").getValue(String.class); // 'test string' + assertThat(testString).isEqualTo("test string"); + + // Subtraction + int four = parser.parseExpression("1 - -3").getValue(Integer.class); // 4 + assertThat(four).isEqualTo(4); + + double d = parser.parseExpression("1000.00 - 1e4").getValue(Double.class); // -9000 + assertThat(d).isCloseTo(-9000.0d, within((double) 0)); + + // Multiplication + int six = parser.parseExpression("-2 * -3").getValue(Integer.class); // 6 + assertThat(six).isEqualTo(6); + + double twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Double.class); // 24.0 + assertThat(twentyFour).isCloseTo(24.0d, within((double) 0)); + + // Division + int minusTwo = parser.parseExpression("6 / -3").getValue(Integer.class); // -2 + assertThat(minusTwo).isEqualTo(-2); + + double one = parser.parseExpression("8.0 / 4e0 / 2").getValue(Double.class); // 1.0 + assertThat(one).isCloseTo(1.0d, within((double) 0)); + + // Modulus + int three = parser.parseExpression("7 % 4").getValue(Integer.class); // 3 + assertThat(three).isEqualTo(3); + + int oneInt = parser.parseExpression("8 / 5 % 2").getValue(Integer.class); // 1 + assertThat(oneInt).isEqualTo(1); + + // Operator precedence + int minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Integer.class); // -21 + assertThat(minusTwentyOne).isEqualTo(-21); + } + + // 7.5.5 + + @Test + public void testAssignment() throws Exception { + Inventor inventor = new Inventor(); + StandardEvaluationContext inventorContext = new StandardEvaluationContext(); + inventorContext.setRootObject(inventor); + + parser.parseExpression("foo").setValue(inventorContext, "Alexander Seovic2"); + + assertThat(parser.parseExpression("foo").getValue(inventorContext,String.class)).isEqualTo("Alexander Seovic2"); + // alternatively + + String aleks = parser.parseExpression("foo = 'Alexandar Seovic'").getValue(inventorContext, String.class); + assertThat(parser.parseExpression("foo").getValue(inventorContext,String.class)).isEqualTo("Alexandar Seovic"); + assertThat(aleks).isEqualTo("Alexandar Seovic"); + } + + // 7.5.6 + + @Test + public void testTypes() throws Exception { + Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class); + assertThat(dateClass).isEqualTo(Date.class); + boolean trueValue = parser.parseExpression("T(java.math.RoundingMode).CEILING < T(java.math.RoundingMode).FLOOR").getValue(Boolean.class); + assertThat(trueValue).isTrue(); + } + + // 7.5.7 + + @Test + public void testConstructors() throws Exception { + StandardEvaluationContext societyContext = new StandardEvaluationContext(); + societyContext.setRootObject(new IEEE()); + Inventor einstein = + parser.parseExpression("new org.springframework.expression.spel.testresources.Inventor('Albert Einstein',new java.util.Date(), 'German')").getValue(Inventor.class); + assertThat(einstein.getName()).isEqualTo("Albert Einstein"); + //create new inventor instance within add method of List + parser.parseExpression("Members2.add(new org.springframework.expression.spel.testresources.Inventor('Albert Einstein', 'German'))").getValue(societyContext); + } + + // 7.5.8 + + @Test + public void testVariables() throws Exception { + Inventor tesla = new Inventor("Nikola Tesla", "Serbian"); + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setVariable("newName", "Mike Tesla"); + + context.setRootObject(tesla); + + parser.parseExpression("foo = #newName").getValue(context); + + assertThat(tesla.getFoo()).isEqualTo("Mike Tesla"); + } + + @SuppressWarnings("unchecked") + @Test + public void testSpecialVariables() throws Exception { + // create an array of integers + List primes = Arrays.asList(2, 3, 5, 7, 11, 13, 17); + + // create parser and set variable 'primes' as the array of integers + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setVariable("primes",primes); + + // all prime numbers > 10 from the list (using selection ?{...}) + List primesGreaterThanTen = (List) parser.parseExpression("#primes.?[#this>10]").getValue(context); + assertThat(primesGreaterThanTen.toString()).isEqualTo("[11, 13, 17]"); + } + + // 7.5.9 + + @Test + public void testFunctions() throws Exception { + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + context.registerFunction("reverseString", StringUtils.class.getDeclaredMethod("reverseString", String.class)); + + String helloWorldReversed = parser.parseExpression("#reverseString('hello world')").getValue(context, String.class); + assertThat(helloWorldReversed).isEqualTo("dlrow olleh"); + } + + // 7.5.10 + + @Test + public void testTernary() throws Exception { + String falseString = parser.parseExpression("false ? 'trueExp' : 'falseExp'").getValue(String.class); + assertThat(falseString).isEqualTo("falseExp"); + + StandardEvaluationContext societyContext = new StandardEvaluationContext(); + societyContext.setRootObject(new IEEE()); + + + parser.parseExpression("Name").setValue(societyContext, "IEEE"); + societyContext.setVariable("queryName", "Nikola Tesla"); + + String expression = "isMember(#queryName)? #queryName + ' is a member of the ' " + + "+ Name + ' Society' : #queryName + ' is not a member of the ' + Name + ' Society'"; + + String queryResultString = parser.parseExpression(expression).getValue(societyContext, String.class); + assertThat(queryResultString).isEqualTo("Nikola Tesla is a member of the IEEE Society"); + // queryResultString = "Nikola Tesla is a member of the IEEE Society" + } + + // 7.5.11 + + @SuppressWarnings("unchecked") + @Test + public void testSelection() throws Exception { + StandardEvaluationContext societyContext = new StandardEvaluationContext(); + societyContext.setRootObject(new IEEE()); + List list = (List) parser.parseExpression("Members2.?[nationality == 'Serbian']").getValue(societyContext); + assertThat(list.size()).isEqualTo(1); + assertThat(list.get(0).getName()).isEqualTo("Nikola Tesla"); + } + + // 7.5.12 + + @Test + public void testTemplating() throws Exception { + String randomPhrase = + parser.parseExpression("random number is ${T(java.lang.Math).random()}", new TemplatedParserContext()).getValue(String.class); + assertThat(randomPhrase.startsWith("random number")).isTrue(); + } + + static class TemplatedParserContext implements ParserContext { + + @Override + public String getExpressionPrefix() { + return "${"; + } + + @Override + public String getExpressionSuffix() { + return "}"; + } + + @Override + public boolean isTemplate() { + return true; + } + } + + static class StringUtils { + + public static String reverseString(String input) { + StringBuilder backwards = new StringBuilder(); + for (int i = 0; i < input.length(); i++) { + backwards.append(input.charAt(input.length() - 1 - i)); + } + return backwards.toString(); + } + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelExceptionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelExceptionTests.java new file mode 100644 index 0000000..f72ac2f --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelExceptionTests.java @@ -0,0 +1,162 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import java.util.ArrayList; +import java.util.HashMap; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * SpelEvaluationException tests (SPR-16544). + * + * @author Juergen Hoeller + * @author DJ Kulkarni + */ +public class SpelExceptionTests { + + @Test + public void spelExpressionMapNullVariables() { + ExpressionParser parser = new SpelExpressionParser(); + Expression spelExpression = parser.parseExpression("#aMap.containsKey('one')"); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy( + spelExpression::getValue); + } + + @Test + public void spelExpressionMapIndexAccessNullVariables() { + ExpressionParser parser = new SpelExpressionParser(); + Expression spelExpression = parser.parseExpression("#aMap['one'] eq 1"); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy( + spelExpression::getValue); + } + + @Test + @SuppressWarnings("serial") + public void spelExpressionMapWithVariables() { + ExpressionParser parser = new SpelExpressionParser(); + Expression spelExpression = parser.parseExpression("#aMap['one'] eq 1"); + StandardEvaluationContext ctx = new StandardEvaluationContext(); + ctx.setVariables(new HashMap() { + { + put("aMap", new HashMap() { + { + put("one", 1); + put("two", 2); + put("three", 3); + } + }); + + } + }); + boolean result = spelExpression.getValue(ctx, Boolean.class); + assertThat(result).isTrue(); + + } + + @Test + public void spelExpressionListNullVariables() { + ExpressionParser parser = new SpelExpressionParser(); + Expression spelExpression = parser.parseExpression("#aList.contains('one')"); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy( + spelExpression::getValue); + } + + @Test + public void spelExpressionListIndexAccessNullVariables() { + ExpressionParser parser = new SpelExpressionParser(); + Expression spelExpression = parser.parseExpression("#aList[0] eq 'one'"); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy( + spelExpression::getValue); + } + + @Test + @SuppressWarnings("serial") + public void spelExpressionListWithVariables() { + ExpressionParser parser = new SpelExpressionParser(); + Expression spelExpression = parser.parseExpression("#aList.contains('one')"); + StandardEvaluationContext ctx = new StandardEvaluationContext(); + ctx.setVariables(new HashMap() { + { + put("aList", new ArrayList() { + { + add("one"); + add("two"); + add("three"); + } + }); + + } + }); + boolean result = spelExpression.getValue(ctx, Boolean.class); + assertThat(result).isTrue(); + } + + @Test + @SuppressWarnings("serial") + public void spelExpressionListIndexAccessWithVariables() { + ExpressionParser parser = new SpelExpressionParser(); + Expression spelExpression = parser.parseExpression("#aList[0] eq 'one'"); + StandardEvaluationContext ctx = new StandardEvaluationContext(); + ctx.setVariables(new HashMap() { + { + put("aList", new ArrayList() { + { + add("one"); + add("two"); + add("three"); + } + }); + + } + }); + boolean result = spelExpression.getValue(ctx, Boolean.class); + assertThat(result).isTrue(); + } + + @Test + public void spelExpressionArrayIndexAccessNullVariables() { + ExpressionParser parser = new SpelExpressionParser(); + Expression spelExpression = parser.parseExpression("#anArray[0] eq 1"); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy( + spelExpression::getValue); + } + + @Test + @SuppressWarnings("serial") + public void spelExpressionArrayWithVariables() { + ExpressionParser parser = new SpelExpressionParser(); + Expression spelExpression = parser.parseExpression("#anArray[0] eq 1"); + StandardEvaluationContext ctx = new StandardEvaluationContext(); + ctx.setVariables(new HashMap() { + { + put("anArray", new int[] {1,2,3}); + } + }); + boolean result = spelExpression.getValue(ctx, Boolean.class); + assertThat(result).isTrue(); + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java new file mode 100644 index 0000000..b114826 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java @@ -0,0 +1,2468 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.AccessException; +import org.springframework.expression.BeanResolver; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionException; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.MethodExecutor; +import org.springframework.expression.MethodResolver; +import org.springframework.expression.ParserContext; +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.standard.SpelExpression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.ReflectiveMethodResolver; +import org.springframework.expression.spel.support.ReflectivePropertyAccessor; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.expression.spel.support.StandardTypeLocator; +import org.springframework.expression.spel.testresources.le.div.mod.reserved.Reserver; +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Reproduction tests cornering various reported SpEL issues. + * + * @author Andy Clement + * @author Juergen Hoeller + * @author Clark Duplichien + * @author Phillip Webb + * @author Sam Brannen + */ +public class SpelReproTests extends AbstractExpressionTests { + + @Test + public void NPE_SPR5661() { + evaluate("joinThreeStrings('a',null,'c')", "anullc", String.class); + } + + @Test + public void SWF1086() { + evaluate("printDouble(T(java.math.BigDecimal).valueOf(14.35))", "14.35", String.class); + } + + @Test + public void doubleCoercion() { + evaluate("printDouble(14.35)", "14.35", String.class); + } + + @Test + public void doubleArrayCoercion() { + evaluate("printDoubles(getDoublesAsStringList())", "{14.35, 15.45}", String.class); + } + + @Test + public void SPR5899() { + StandardEvaluationContext context = new StandardEvaluationContext(new Spr5899Class()); + Expression expr = new SpelExpressionParser().parseRaw("tryToInvokeWithNull(12)"); + assertThat(expr.getValue(context)).isEqualTo(12); + expr = new SpelExpressionParser().parseRaw("tryToInvokeWithNull(null)"); + assertThat(expr.getValue(context)).isEqualTo(null); + expr = new SpelExpressionParser().parseRaw("tryToInvokeWithNull2(null)"); + assertThatExceptionOfType(EvaluationException.class).isThrownBy( + expr::getValue); + context.setTypeLocator(new MyTypeLocator()); + + // varargs + expr = new SpelExpressionParser().parseRaw("tryToInvokeWithNull3(null,'a','b')"); + assertThat(expr.getValue(context)).isEqualTo("ab"); + + // varargs 2 - null is packed into the varargs + expr = new SpelExpressionParser().parseRaw("tryToInvokeWithNull3(12,'a',null,'c')"); + assertThat(expr.getValue(context)).isEqualTo("anullc"); + + // check we can find the ctor ok + expr = new SpelExpressionParser().parseRaw("new Spr5899Class().toString()"); + assertThat(expr.getValue(context)).isEqualTo("instance"); + + expr = new SpelExpressionParser().parseRaw("new Spr5899Class(null).toString()"); + assertThat(expr.getValue(context)).isEqualTo("instance"); + + // ctor varargs + expr = new SpelExpressionParser().parseRaw("new Spr5899Class(null,'a','b').toString()"); + assertThat(expr.getValue(context)).isEqualTo("instance"); + + // ctor varargs 2 + expr = new SpelExpressionParser().parseRaw("new Spr5899Class(null,'a', null, 'b').toString()"); + assertThat(expr.getValue(context)).isEqualTo("instance"); + } + + @Test + public void SPR5905_InnerTypeReferences() { + StandardEvaluationContext context = new StandardEvaluationContext(new Spr5899Class()); + Expression expr = new SpelExpressionParser().parseRaw("T(java.util.Map$Entry)"); + assertThat(expr.getValue(context)).isEqualTo(Map.Entry.class); + + expr = new SpelExpressionParser().parseRaw("T(org.springframework.expression.spel.SpelReproTests$Outer$Inner).run()"); + assertThat(expr.getValue(context)).isEqualTo(12); + + expr = new SpelExpressionParser().parseRaw("new org.springframework.expression.spel.SpelReproTests$Outer$Inner().run2()"); + assertThat(expr.getValue(context)).isEqualTo(13); + } + + @Test + public void SPR5804() { + Map m = new HashMap<>(); + m.put("foo", "bar"); + StandardEvaluationContext context = new StandardEvaluationContext(m); // root is a map instance + context.addPropertyAccessor(new MapAccessor()); + Expression expr = new SpelExpressionParser().parseRaw("['foo']"); + assertThat(expr.getValue(context)).isEqualTo("bar"); + } + + @Test + public void SPR5847() { + StandardEvaluationContext context = new StandardEvaluationContext(new TestProperties()); + String name = null; + Expression expr = null; + + expr = new SpelExpressionParser().parseRaw("jdbcProperties['username']"); + name = expr.getValue(context, String.class); + assertThat(name).isEqualTo("Dave"); + + expr = new SpelExpressionParser().parseRaw("jdbcProperties[username]"); + name = expr.getValue(context, String.class); + assertThat(name).isEqualTo("Dave"); + + // MapAccessor required for this to work + expr = new SpelExpressionParser().parseRaw("jdbcProperties.username"); + context.addPropertyAccessor(new MapAccessor()); + name = expr.getValue(context, String.class); + assertThat(name).isEqualTo("Dave"); + + // --- dotted property names + + // lookup foo on the root, then bar on that, then use that as the key into + // jdbcProperties + expr = new SpelExpressionParser().parseRaw("jdbcProperties[foo.bar]"); + context.addPropertyAccessor(new MapAccessor()); + name = expr.getValue(context, String.class); + assertThat(name).isEqualTo("Dave2"); + + // key is foo.bar + expr = new SpelExpressionParser().parseRaw("jdbcProperties['foo.bar']"); + context.addPropertyAccessor(new MapAccessor()); + name = expr.getValue(context, String.class); + assertThat(name).isEqualTo("Elephant"); + } + + @Test + public void NPE_SPR5673() { + ParserContext hashes = TemplateExpressionParsingTests.HASH_DELIMITED_PARSER_CONTEXT; + ParserContext dollars = TemplateExpressionParsingTests.DEFAULT_TEMPLATE_PARSER_CONTEXT; + + checkTemplateParsing("abc${'def'} ghi", "abcdef ghi"); + + checkTemplateParsingError("abc${ {}( 'abc'", "Missing closing ')' for '(' at position 8"); + checkTemplateParsingError("abc${ {}[ 'abc'", "Missing closing ']' for '[' at position 8"); + checkTemplateParsingError("abc${ {}{ 'abc'", "Missing closing '}' for '{' at position 8"); + checkTemplateParsingError("abc${ ( 'abc' }", "Found closing '}' at position 14 but most recent opening is '(' at position 6"); + checkTemplateParsingError("abc${ '... }", "Found non terminating string literal starting at position 6"); + checkTemplateParsingError("abc${ \"... }", "Found non terminating string literal starting at position 6"); + checkTemplateParsingError("abc${ ) }", "Found closing ')' at position 6 without an opening '('"); + checkTemplateParsingError("abc${ ] }", "Found closing ']' at position 6 without an opening '['"); + checkTemplateParsingError("abc${ } }", "No expression defined within delimiter '${}' at character 3"); + checkTemplateParsingError("abc$[ } ]", DOLLARSQUARE_TEMPLATE_PARSER_CONTEXT, "Found closing '}' at position 6 without an opening '{'"); + + checkTemplateParsing("abc ${\"def''g}hi\"} jkl", "abc def'g}hi jkl"); + checkTemplateParsing("abc ${'def''g}hi'} jkl", "abc def'g}hi jkl"); + checkTemplateParsing("}", "}"); + checkTemplateParsing("${'hello'} world", "hello world"); + checkTemplateParsing("Hello ${'}'}]", "Hello }]"); + checkTemplateParsing("Hello ${'}'}", "Hello }"); + checkTemplateParsingError("Hello ${ ( ", "No ending suffix '}' for expression starting at character 6: ${ ( "); + checkTemplateParsingError("Hello ${ ( }", "Found closing '}' at position 11 but most recent opening is '(' at position 9"); + checkTemplateParsing("#{'Unable to render embedded object: File ({#this == 2}'}", hashes, "Unable to render embedded object: File ({#this == 2}"); + checkTemplateParsing("This is the last odd number in the list: ${listOfNumbersUpToTen.$[#this%2==1]}", dollars, "This is the last odd number in the list: 9"); + checkTemplateParsing("Hello ${'here is a curly bracket }'}", dollars, "Hello here is a curly bracket }"); + checkTemplateParsing("He${'${'}llo ${'here is a curly bracket }'}}", dollars, "He${llo here is a curly bracket }}"); + checkTemplateParsing("Hello ${'()()()}{}{}{][]{}{][}[][][}{()()'} World", dollars, "Hello ()()()}{}{}{][]{}{][}[][][}{()() World"); + checkTemplateParsing("Hello ${'inner literal that''s got {[(])]}an escaped quote in it'} World", "Hello inner literal that's got {[(])]}an escaped quote in it World"); + checkTemplateParsingError("Hello ${", "No ending suffix '}' for expression starting at character 6: ${"); + } + + @Test + public void propertyAccessOnNullTarget_SPR5663() throws AccessException { + PropertyAccessor accessor = new ReflectivePropertyAccessor(); + EvaluationContext context = TestScenarioCreator.getTestEvaluationContext(); + assertThat(accessor.canRead(context, null, "abc")).isFalse(); + assertThat(accessor.canWrite(context, null, "abc")).isFalse(); + assertThatIllegalStateException().isThrownBy(() -> + accessor.read(context, null, "abc")); + assertThatIllegalStateException().isThrownBy(() -> + accessor.write(context, null, "abc", "foo")); + } + + @Test + public void nestedProperties_SPR6923() { + StandardEvaluationContext context = new StandardEvaluationContext(new Foo()); + Expression expr = new SpelExpressionParser().parseRaw("resource.resource.server"); + String name = expr.getValue(context, String.class); + assertThat(name).isEqualTo("abc"); + } + + /** Should be accessing Goo.getKey because 'bar' field evaluates to "key" */ + @Test + public void indexingAsAPropertyAccess_SPR6968_1() { + StandardEvaluationContext context = new StandardEvaluationContext(new Goo()); + String name = null; + Expression expr = null; + expr = new SpelExpressionParser().parseRaw("instance[bar]"); + name = expr.getValue(context, String.class); + assertThat(name).isEqualTo("hello"); + name = expr.getValue(context, String.class); // will be using the cached accessor this time + assertThat(name).isEqualTo("hello"); + } + + /** Should be accessing Goo.getKey because 'bar' variable evaluates to "key" */ + @Test + public void indexingAsAPropertyAccess_SPR6968_2() { + StandardEvaluationContext context = new StandardEvaluationContext(new Goo()); + context.setVariable("bar", "key"); + String name = null; + Expression expr = null; + expr = new SpelExpressionParser().parseRaw("instance[#bar]"); + name = expr.getValue(context, String.class); + assertThat(name).isEqualTo("hello"); + name = expr.getValue(context, String.class); // will be using the cached accessor this time + assertThat(name).isEqualTo("hello"); + } + + /** $ related identifiers */ + @Test + public void dollarPrefixedIdentifier_SPR7100() { + Holder h = new Holder(); + StandardEvaluationContext context = new StandardEvaluationContext(h); + context.addPropertyAccessor(new MapAccessor()); + h.map.put("$foo", "wibble"); + h.map.put("foo$bar", "wobble"); + h.map.put("foobar$$", "wabble"); + h.map.put("$", "wubble"); + h.map.put("$$", "webble"); + h.map.put("$_$", "tribble"); + String name = null; + Expression expr = null; + + expr = new SpelExpressionParser().parseRaw("map.$foo"); + name = expr.getValue(context, String.class); + assertThat(name).isEqualTo("wibble"); + + expr = new SpelExpressionParser().parseRaw("map.foo$bar"); + name = expr.getValue(context, String.class); + assertThat(name).isEqualTo("wobble"); + + expr = new SpelExpressionParser().parseRaw("map.foobar$$"); + name = expr.getValue(context, String.class); + assertThat(name).isEqualTo("wabble"); + + expr = new SpelExpressionParser().parseRaw("map.$"); + name = expr.getValue(context, String.class); + assertThat(name).isEqualTo("wubble"); + + expr = new SpelExpressionParser().parseRaw("map.$$"); + name = expr.getValue(context, String.class); + assertThat(name).isEqualTo("webble"); + + expr = new SpelExpressionParser().parseRaw("map.$_$"); + name = expr.getValue(context, String.class); + assertThat(name).isEqualTo("tribble"); + } + + /** Should be accessing Goo.wibble field because 'bar' variable evaluates to "wibble" */ + @Test + public void indexingAsAPropertyAccess_SPR6968_3() { + StandardEvaluationContext context = new StandardEvaluationContext(new Goo()); + context.setVariable("bar", "wibble"); + String name = null; + Expression expr = null; + expr = new SpelExpressionParser().parseRaw("instance[#bar]"); + // will access the field 'wibble' and not use a getter + name = expr.getValue(context, String.class); + assertThat(name).isEqualTo("wobble"); + name = expr.getValue(context, String.class); // will be using the cached accessor this time + assertThat(name).isEqualTo("wobble"); + } + + /** + * Should be accessing (setting) Goo.wibble field because 'bar' variable evaluates to + * "wibble" + */ + @Test + public void indexingAsAPropertyAccess_SPR6968_4() { + Goo g = Goo.instance; + StandardEvaluationContext context = new StandardEvaluationContext(g); + context.setVariable("bar", "wibble"); + Expression expr = null; + expr = new SpelExpressionParser().parseRaw("instance[#bar]='world'"); + // will access the field 'wibble' and not use a getter + expr.getValue(context, String.class); + assertThat(g.wibble).isEqualTo("world"); + expr.getValue(context, String.class); // will be using the cached accessor this time + assertThat(g.wibble).isEqualTo("world"); + } + + /** Should be accessing Goo.setKey field because 'bar' variable evaluates to "key" */ + @Test + public void indexingAsAPropertyAccess_SPR6968_5() { + Goo g = Goo.instance; + StandardEvaluationContext context = new StandardEvaluationContext(g); + Expression expr = null; + expr = new SpelExpressionParser().parseRaw("instance[bar]='world'"); + expr.getValue(context, String.class); + assertThat(g.value).isEqualTo("world"); + expr.getValue(context, String.class); // will be using the cached accessor this time + assertThat(g.value).isEqualTo("world"); + } + + @Test + public void dollars() { + StandardEvaluationContext context = new StandardEvaluationContext(new XX()); + Expression expr = null; + expr = new SpelExpressionParser().parseRaw("m['$foo']"); + context.setVariable("file_name", "$foo"); + assertThat(expr.getValue(context, String.class)).isEqualTo("wibble"); + } + + @Test + public void dollars2() { + StandardEvaluationContext context = new StandardEvaluationContext(new XX()); + Expression expr = null; + expr = new SpelExpressionParser().parseRaw("m[$foo]"); + context.setVariable("file_name", "$foo"); + assertThat(expr.getValue(context, String.class)).isEqualTo("wibble"); + } + + private void checkTemplateParsing(String expression, String expectedValue) { + checkTemplateParsing(expression, TemplateExpressionParsingTests.DEFAULT_TEMPLATE_PARSER_CONTEXT, expectedValue); + } + + private void checkTemplateParsing(String expression, ParserContext context, String expectedValue) { + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expr = parser.parseExpression(expression, context); + assertThat(expr.getValue(TestScenarioCreator.getTestEvaluationContext())).isEqualTo(expectedValue); + } + + private void checkTemplateParsingError(String expression, String expectedMessage) { + checkTemplateParsingError(expression, TemplateExpressionParsingTests.DEFAULT_TEMPLATE_PARSER_CONTEXT, expectedMessage); + } + + private void checkTemplateParsingError(String expression, ParserContext context, String expectedMessage) { + SpelExpressionParser parser = new SpelExpressionParser(); + assertThatExceptionOfType(Exception.class).isThrownBy(() -> + parser.parseExpression(expression, context)) + .satisfies(ex -> { + String message = ex.getMessage(); + if (ex instanceof ExpressionException) { + message = ((ExpressionException) ex).getSimpleMessage(); + } + assertThat(message).isEqualTo(expectedMessage); + }); + } + + + private static final ParserContext DOLLARSQUARE_TEMPLATE_PARSER_CONTEXT = new ParserContext() { + @Override + public String getExpressionPrefix() { + return "$["; + } + @Override + public String getExpressionSuffix() { + return "]"; + } + @Override + public boolean isTemplate() { + return true; + } + }; + + @Test + public void beanResolution() { + StandardEvaluationContext context = new StandardEvaluationContext(new XX()); + Expression expr = null; + + // no resolver registered == exception + try { + expr = new SpelExpressionParser().parseRaw("@foo"); + assertThat(expr.getValue(context, String.class)).isEqualTo("custard"); + } + catch (SpelEvaluationException see) { + assertThat(see.getMessageCode()).isEqualTo(SpelMessage.NO_BEAN_RESOLVER_REGISTERED); + assertThat(see.getInserts()[0]).isEqualTo("foo"); + } + + context.setBeanResolver(new MyBeanResolver()); + + // bean exists + expr = new SpelExpressionParser().parseRaw("@foo"); + assertThat(expr.getValue(context, String.class)).isEqualTo("custard"); + + // bean does not exist + expr = new SpelExpressionParser().parseRaw("@bar"); + assertThat(expr.getValue(context, String.class)).isEqualTo(null); + + // bean name will cause AccessException + expr = new SpelExpressionParser().parseRaw("@goo"); + try { + assertThat(expr.getValue(context, String.class)).isEqualTo(null); + } + catch (SpelEvaluationException see) { + assertThat(see.getMessageCode()).isEqualTo(SpelMessage.EXCEPTION_DURING_BEAN_RESOLUTION); + assertThat(see.getInserts()[0]).isEqualTo("goo"); + assertThat(see.getCause() instanceof AccessException).isTrue(); + assertThat(see.getCause().getMessage().startsWith("DONT")).isTrue(); + } + + // bean exists + expr = new SpelExpressionParser().parseRaw("@'foo.bar'"); + assertThat(expr.getValue(context, String.class)).isEqualTo("trouble"); + + // bean exists + try { + expr = new SpelExpressionParser().parseRaw("@378"); + assertThat(expr.getValue(context, String.class)).isEqualTo("trouble"); + } + catch (SpelParseException spe) { + assertThat(spe.getMessageCode()).isEqualTo(SpelMessage.INVALID_BEAN_REFERENCE); + } + } + + @Test + public void elvis_SPR7209_1() { + StandardEvaluationContext context = new StandardEvaluationContext(new XX()); + Expression expr = null; + + // Different parts of elvis expression are null + expr = new SpelExpressionParser().parseRaw("(?:'default')"); + assertThat(expr.getValue()).isEqualTo("default"); + expr = new SpelExpressionParser().parseRaw("?:'default'"); + assertThat(expr.getValue()).isEqualTo("default"); + expr = new SpelExpressionParser().parseRaw("?:"); + assertThat(expr.getValue()).isEqualTo(null); + + // Different parts of ternary expression are null + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + new SpelExpressionParser().parseRaw("(?'abc':'default')").getValue(context)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.TYPE_CONVERSION_ERROR)); + expr = new SpelExpressionParser().parseRaw("(false?'abc':null)"); + assertThat(expr.getValue()).isEqualTo(null); + + // Assignment + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + new SpelExpressionParser().parseRaw("(='default')").getValue(context)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.SETVALUE_NOT_SUPPORTED)); + } + + @Test + public void elvis_SPR7209_2() { + Expression expr = null; + // Have empty string treated as null for elvis + expr = new SpelExpressionParser().parseRaw("?:'default'"); + assertThat(expr.getValue()).isEqualTo("default"); + expr = new SpelExpressionParser().parseRaw("\"\"?:'default'"); + assertThat(expr.getValue()).isEqualTo("default"); + expr = new SpelExpressionParser().parseRaw("''?:'default'"); + assertThat(expr.getValue()).isEqualTo("default"); + } + + @Test + public void mapOfMap_SPR7244() { + Map map = new LinkedHashMap<>(); + map.put("uri", "http:"); + Map nameMap = new LinkedHashMap<>(); + nameMap.put("givenName", "Arthur"); + map.put("value", nameMap); + + StandardEvaluationContext context = new StandardEvaluationContext(map); + ExpressionParser parser = new SpelExpressionParser(); + String el1 = "#root['value'].get('givenName')"; + Expression exp = parser.parseExpression(el1); + Object evaluated = exp.getValue(context); + assertThat(evaluated).isEqualTo("Arthur"); + + String el2 = "#root['value']['givenName']"; + exp = parser.parseExpression(el2); + evaluated = exp.getValue(context); + assertThat(evaluated).isEqualTo("Arthur"); + } + + @Test + public void projectionTypeDescriptors_1() { + StandardEvaluationContext context = new StandardEvaluationContext(new C()); + SpelExpressionParser parser = new SpelExpressionParser(); + String el1 = "ls.![#this.equals('abc')]"; + SpelExpression exp = parser.parseRaw(el1); + List value = (List) exp.getValue(context); + // value is list containing [true,false] + assertThat(value.get(0).getClass()).isEqualTo(Boolean.class); + TypeDescriptor evaluated = exp.getValueTypeDescriptor(context); + assertThat(evaluated.getElementTypeDescriptor()).isEqualTo(null); + } + + @Test + public void projectionTypeDescriptors_2() { + StandardEvaluationContext context = new StandardEvaluationContext(new C()); + SpelExpressionParser parser = new SpelExpressionParser(); + String el1 = "as.![#this.equals('abc')]"; + SpelExpression exp = parser.parseRaw(el1); + Object[] value = (Object[]) exp.getValue(context); + // value is array containing [true,false] + assertThat(value[0].getClass()).isEqualTo(Boolean.class); + TypeDescriptor evaluated = exp.getValueTypeDescriptor(context); + assertThat(evaluated.getElementTypeDescriptor().getType()).isEqualTo(Boolean.class); + } + + @Test + public void projectionTypeDescriptors_3() { + StandardEvaluationContext context = new StandardEvaluationContext(new C()); + SpelExpressionParser parser = new SpelExpressionParser(); + String el1 = "ms.![key.equals('abc')]"; + SpelExpression exp = parser.parseRaw(el1); + List value = (List) exp.getValue(context); + // value is list containing [true,false] + assertThat(value.get(0).getClass()).isEqualTo(Boolean.class); + TypeDescriptor evaluated = exp.getValueTypeDescriptor(context); + assertThat(evaluated.getElementTypeDescriptor()).isEqualTo(null); + } + + @Test + public void greaterThanWithNulls_SPR7840() { + List list = new ArrayList<>(); + list.add(new D("aaa")); + list.add(new D("bbb")); + list.add(new D(null)); + list.add(new D("ccc")); + list.add(new D(null)); + list.add(new D("zzz")); + + StandardEvaluationContext context = new StandardEvaluationContext(list); + SpelExpressionParser parser = new SpelExpressionParser(); + + String el1 = "#root.?[a < 'hhh']"; + SpelExpression exp = parser.parseRaw(el1); + Object value = exp.getValue(context); + assertThat(value.toString()).isEqualTo("[D(aaa), D(bbb), D(null), D(ccc), D(null)]"); + + String el2 = "#root.?[a > 'hhh']"; + SpelExpression exp2 = parser.parseRaw(el2); + Object value2 = exp2.getValue(context); + assertThat(value2.toString()).isEqualTo("[D(zzz)]"); + + // trim out the nulls first + String el3 = "#root.?[a!=null].?[a < 'hhh']"; + SpelExpression exp3 = parser.parseRaw(el3); + Object value3 = exp3.getValue(context); + assertThat(value3.toString()).isEqualTo("[D(aaa), D(bbb), D(ccc)]"); + } + + /** + * Test whether {@link ReflectiveMethodResolver} follows Java Method Invocation + * Conversion order. And more precisely that widening reference conversion is 'higher' + * than a unboxing conversion. + */ + @Test + public void conversionPriority_SPR8224() throws Exception { + + @SuppressWarnings("unused") + class ConversionPriority1 { + public int getX(Number i) { + return 20; + } + public int getX(int i) { + return 10; + } + } + + @SuppressWarnings("unused") + class ConversionPriority2 { + public int getX(int i) { + return 10; + } + public int getX(Number i) { + return 20; + } + } + + final Integer INTEGER = 7; + + EvaluationContext emptyEvalContext = new StandardEvaluationContext(); + + List args = new ArrayList<>(); + args.add(TypeDescriptor.forObject(42)); + + ConversionPriority1 target = new ConversionPriority1(); + MethodExecutor me = new ReflectiveMethodResolver(true).resolve(emptyEvalContext, target, "getX", args); + // MethodInvoker chooses getX(int i) when passing Integer + final int actual = (Integer) me.execute(emptyEvalContext, target, new Integer(42)).getValue(); + // Compiler chooses getX(Number i) when passing Integer + final int compiler = target.getX(INTEGER); + // Fails! + assertThat(actual).isEqualTo(compiler); + + ConversionPriority2 target2 = new ConversionPriority2(); + MethodExecutor me2 = new ReflectiveMethodResolver(true).resolve(emptyEvalContext, target2, "getX", args); + // MethodInvoker chooses getX(int i) when passing Integer + int actual2 = (Integer) me2.execute(emptyEvalContext, target2, new Integer(42)).getValue(); + // Compiler chooses getX(Number i) when passing Integer + int compiler2 = target2.getX(INTEGER); + // Fails! + assertThat(actual2).isEqualTo(compiler2); + + } + + /** + * Test whether {@link ReflectiveMethodResolver} handles Widening Primitive Conversion. That's passing an 'int' to a + * method accepting 'long' is ok. + */ + @Test + public void wideningPrimitiveConversion_SPR8224() throws Exception { + + class WideningPrimitiveConversion { + public int getX(long i) { + return 10; + } + } + + final Integer INTEGER_VALUE = Integer.valueOf(7); + WideningPrimitiveConversion target = new WideningPrimitiveConversion(); + EvaluationContext emptyEvalContext = new StandardEvaluationContext(); + + List args = new ArrayList<>(); + args.add(TypeDescriptor.forObject(INTEGER_VALUE)); + + MethodExecutor me = new ReflectiveMethodResolver(true).resolve(emptyEvalContext, target, "getX", args); + final int actual = (Integer) me.execute(emptyEvalContext, target, INTEGER_VALUE).getValue(); + + final int compiler = target.getX(INTEGER_VALUE); + assertThat(actual).isEqualTo(compiler); + } + + @Test + public void varargsAgainstProxy_SPR16122() { + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expr = parser.parseExpression("process('a', 'b')"); + + VarargsReceiver receiver = new VarargsReceiver(); + VarargsInterface proxy = (VarargsInterface) Proxy.newProxyInstance( + getClass().getClassLoader(), new Class[] {VarargsInterface.class}, + (proxy1, method, args) -> method.invoke(receiver, args)); + + assertThat(expr.getValue(new StandardEvaluationContext(receiver))).isEqualTo("OK"); + assertThat(expr.getValue(new StandardEvaluationContext(proxy))).isEqualTo("OK"); + } + + @Test + public void testCompiledExpressionForProxy_SPR16191() { + SpelExpressionParser expressionParser = + new SpelExpressionParser(new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, null)); + Expression expression = expressionParser.parseExpression("#target.process(#root)"); + + VarargsReceiver receiver = new VarargsReceiver(); + VarargsInterface proxy = (VarargsInterface) Proxy.newProxyInstance( + getClass().getClassLoader(), new Class[] {VarargsInterface.class}, + (proxy1, method, args) -> method.invoke(receiver, args)); + + StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); + evaluationContext.setVariable("target", proxy); + + String result = expression.getValue(evaluationContext, "foo", String.class); + result = expression.getValue(evaluationContext, "foo", String.class); + assertThat(result).isEqualTo("OK"); + } + + @Test + public void varargsAndPrimitives_SPR8174() throws Exception { + EvaluationContext emptyEvalContext = new StandardEvaluationContext(); + List args = new ArrayList<>(); + + args.add(TypeDescriptor.forObject(34L)); + ReflectionUtil ru = new ReflectionUtil<>(); + MethodExecutor me = new ReflectiveMethodResolver().resolve(emptyEvalContext, ru, "methodToCall", args); + + args.set(0, TypeDescriptor.forObject(23)); + me = new ReflectiveMethodResolver().resolve(emptyEvalContext, ru, "foo", args); + me.execute(emptyEvalContext, ru, 45); + + args.set(0, TypeDescriptor.forObject(23f)); + me = new ReflectiveMethodResolver().resolve(emptyEvalContext, ru, "foo", args); + me.execute(emptyEvalContext, ru, 45f); + + args.set(0, TypeDescriptor.forObject(23d)); + me = new ReflectiveMethodResolver().resolve(emptyEvalContext, ru, "foo", args); + me.execute(emptyEvalContext, ru, 23d); + + args.set(0, TypeDescriptor.forObject((short) 23)); + me = new ReflectiveMethodResolver().resolve(emptyEvalContext, ru, "foo", args); + me.execute(emptyEvalContext, ru, (short) 23); + + args.set(0, TypeDescriptor.forObject(23L)); + me = new ReflectiveMethodResolver().resolve(emptyEvalContext, ru, "foo", args); + me.execute(emptyEvalContext, ru, 23L); + + args.set(0, TypeDescriptor.forObject((char) 65)); + me = new ReflectiveMethodResolver().resolve(emptyEvalContext, ru, "foo", args); + me.execute(emptyEvalContext, ru, (char) 65); + + args.set(0, TypeDescriptor.forObject((byte) 23)); + me = new ReflectiveMethodResolver().resolve(emptyEvalContext, ru, "foo", args); + me.execute(emptyEvalContext, ru, (byte) 23); + + args.set(0, TypeDescriptor.forObject(true)); + me = new ReflectiveMethodResolver().resolve(emptyEvalContext, ru, "foo", args); + me.execute(emptyEvalContext, ru, true); + + // trickier: + args.set(0, TypeDescriptor.forObject(12)); + args.add(TypeDescriptor.forObject(23f)); + me = new ReflectiveMethodResolver().resolve(emptyEvalContext, ru, "bar", args); + me.execute(emptyEvalContext, ru, 12, 23f); + } + + @Test + public void reservedWords_SPR8228() { + + // "DIV","EQ","GE","GT","LE","LT","MOD","NE","NOT" + @SuppressWarnings("unused") + class Reserver { + public Reserver getReserver() { + return this; + } + public String NE = "abc"; + public String ne = "def"; + + public int DIV = 1; + public int div = 3; + + public Map m = new HashMap<>(); + + Reserver() { + m.put("NE", "xyz"); + } + } + + StandardEvaluationContext context = new StandardEvaluationContext(new Reserver()); + SpelExpressionParser parser = new SpelExpressionParser(); + String ex = "getReserver().NE"; + SpelExpression exp = parser.parseRaw(ex); + String value = (String) exp.getValue(context); + assertThat(value).isEqualTo("abc"); + + ex = "getReserver().ne"; + exp = parser.parseRaw(ex); + value = (String) exp.getValue(context); + assertThat(value).isEqualTo("def"); + + ex = "getReserver().m[NE]"; + exp = parser.parseRaw(ex); + value = (String) exp.getValue(context); + assertThat(value).isEqualTo("xyz"); + + ex = "getReserver().DIV"; + exp = parser.parseRaw(ex); + assertThat(exp.getValue(context)).isEqualTo(1); + + ex = "getReserver().div"; + exp = parser.parseRaw(ex); + assertThat(exp.getValue(context)).isEqualTo(3); + + exp = parser.parseRaw("NE"); + assertThat(exp.getValue(context)).isEqualTo("abc"); + } + + @Test + public void reservedWordProperties_SPR9862() { + StandardEvaluationContext context = new StandardEvaluationContext(); + SpelExpressionParser parser = new SpelExpressionParser(); + SpelExpression expression = parser.parseRaw("T(org.springframework.expression.spel.testresources.le.div.mod.reserved.Reserver).CONST"); + Object value = expression.getValue(context); + assertThat(Reserver.CONST).isEqualTo(value); + } + + /** + * We add property accessors in the order: + * First, Second, Third, Fourth. + * They are not utilized in this order; preventing a priority or order of operations + * in evaluation of SPEL expressions for a given context. + */ + @Test + public void propertyAccessorOrder_SPR8211() { + ExpressionParser expressionParser = new SpelExpressionParser(); + StandardEvaluationContext evaluationContext = new StandardEvaluationContext(new ContextObject()); + + evaluationContext.addPropertyAccessor(new TestPropertyAccessor("firstContext")); + evaluationContext.addPropertyAccessor(new TestPropertyAccessor("secondContext")); + evaluationContext.addPropertyAccessor(new TestPropertyAccessor("thirdContext")); + evaluationContext.addPropertyAccessor(new TestPropertyAccessor("fourthContext")); + + assertThat(expressionParser.parseExpression("shouldBeFirst").getValue(evaluationContext)).isEqualTo("first"); + assertThat(expressionParser.parseExpression("shouldBeSecond").getValue(evaluationContext)).isEqualTo("second"); + assertThat(expressionParser.parseExpression("shouldBeThird").getValue(evaluationContext)).isEqualTo("third"); + assertThat(expressionParser.parseExpression("shouldBeFourth").getValue(evaluationContext)).isEqualTo("fourth"); + } + + /** + * Test the ability to subclass the ReflectiveMethodResolver and change how it + * determines the set of methods for a type. + */ + @Test + public void customStaticFunctions_SPR9038() { + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + List methodResolvers = new ArrayList<>(); + methodResolvers.add(new ReflectiveMethodResolver() { + @Override + protected Method[] getMethods(Class type) { + try { + return new Method[] {Integer.class.getDeclaredMethod("parseInt", String.class, Integer.TYPE)}; + } + catch (NoSuchMethodException ex) { + return new Method[0]; + } + } + }); + + context.setMethodResolvers(methodResolvers); + Expression expression = parser.parseExpression("parseInt('-FF', 16)"); + + Integer result = expression.getValue(context, "", Integer.class); + assertThat(result.intValue()).isEqualTo(-255); + } + + @Test + public void array() { + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + Expression expression = null; + Object result = null; + + expression = parser.parseExpression("new java.lang.Long[0].class"); + result = expression.getValue(context, ""); + assertThat(result.toString()).as("Equal assertion failed: ").isEqualTo("class [Ljava.lang.Long;"); + + expression = parser.parseExpression("T(java.lang.Long[])"); + result = expression.getValue(context, ""); + assertThat(result.toString()).as("Equal assertion failed: ").isEqualTo("class [Ljava.lang.Long;"); + + expression = parser.parseExpression("T(java.lang.String[][][])"); + result = expression.getValue(context, ""); + assertThat(result.toString()).as("Equal assertion failed: ").isEqualTo("class [[[Ljava.lang.String;"); + assertThat(((SpelExpression) expression).toStringAST()).isEqualTo("T(java.lang.String[][][])"); + + expression = parser.parseExpression("new int[0].class"); + result = expression.getValue(context, ""); + assertThat(result.toString()).as("Equal assertion failed: ").isEqualTo("class [I"); + + expression = parser.parseExpression("T(int[][])"); + result = expression.getValue(context, ""); + assertThat(result.toString()).isEqualTo("class [[I"); + } + + @Test + public void SPR9486_floatFunctionResolver() { + Number expectedResult = Math.abs(-10.2f); + ExpressionParser parser = new SpelExpressionParser(); + SPR9486_FunctionsClass testObject = new SPR9486_FunctionsClass(); + + StandardEvaluationContext context = new StandardEvaluationContext(); + Expression expression = parser.parseExpression("abs(-10.2f)"); + Number result = expression.getValue(context, testObject, Number.class); + assertThat(result).isEqualTo(expectedResult); + } + + @Test + public void SPR9486_addFloatWithDouble() { + Number expectedNumber = 10.21f + 10.2; + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + Expression expression = parser.parseExpression("10.21f + 10.2"); + Number result = expression.getValue(context, null, Number.class); + assertThat(result).isEqualTo(expectedNumber); + } + + @Test + public void SPR9486_addFloatWithFloat() { + Number expectedNumber = 10.21f + 10.2f; + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + Expression expression = parser.parseExpression("10.21f + 10.2f"); + Number result = expression.getValue(context, null, Number.class); + assertThat(result).isEqualTo(expectedNumber); + } + + @Test + public void SPR9486_subtractFloatWithDouble() { + Number expectedNumber = 10.21f - 10.2; + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + Expression expression = parser.parseExpression("10.21f - 10.2"); + Number result = expression.getValue(context, null, Number.class); + assertThat(result).isEqualTo(expectedNumber); + } + + @Test + public void SPR9486_subtractFloatWithFloat() { + Number expectedNumber = 10.21f - 10.2f; + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + Expression expression = parser.parseExpression("10.21f - 10.2f"); + Number result = expression.getValue(context, null, Number.class); + assertThat(result).isEqualTo(expectedNumber); + } + + @Test + public void SPR9486_multiplyFloatWithDouble() { + Number expectedNumber = 10.21f * 10.2; + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + Expression expression = parser.parseExpression("10.21f * 10.2"); + Number result = expression.getValue(context, null, Number.class); + assertThat(result).isEqualTo(expectedNumber); + } + + @Test + public void SPR9486_multiplyFloatWithFloat() { + Number expectedNumber = 10.21f * 10.2f; + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + Expression expression = parser.parseExpression("10.21f * 10.2f"); + Number result = expression.getValue(context, null, Number.class); + assertThat(result).isEqualTo(expectedNumber); + } + + @Test + public void SPR9486_floatDivideByFloat() { + Number expectedNumber = -10.21f / -10.2f; + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + Expression expression = parser.parseExpression("-10.21f / -10.2f"); + Number result = expression.getValue(context, null, Number.class); + assertThat(result).isEqualTo(expectedNumber); + } + + @Test + public void SPR9486_floatDivideByDouble() { + Number expectedNumber = -10.21f / -10.2; + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + Expression expression = parser.parseExpression("-10.21f / -10.2"); + Number result = expression.getValue(context, null, Number.class); + assertThat(result).isEqualTo(expectedNumber); + } + + @Test + public void SPR9486_floatEqFloatUnaryMinus() { + Boolean expectedResult = -10.21f == -10.2f; + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + Expression expression = parser.parseExpression("-10.21f == -10.2f"); + Boolean result = expression.getValue(context, null, Boolean.class); + assertThat(result).isEqualTo(expectedResult); + } + + @Test + public void SPR9486_floatEqDoubleUnaryMinus() { + Boolean expectedResult = -10.21f == -10.2; + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + Expression expression = parser.parseExpression("-10.21f == -10.2"); + Boolean result = expression.getValue(context, null, Boolean.class); + assertThat(result).isEqualTo(expectedResult); + } + + @Test + public void SPR9486_floatEqFloat() { + Boolean expectedResult = 10.215f == 10.2109f; + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + Expression expression = parser.parseExpression("10.215f == 10.2109f"); + Boolean result = expression.getValue(context, null, Boolean.class); + assertThat(result).isEqualTo(expectedResult); + } + + @Test + public void SPR9486_floatEqDouble() { + Boolean expectedResult = 10.215f == 10.2109; + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + Expression expression = parser.parseExpression("10.215f == 10.2109"); + Boolean result = expression.getValue(context, null, Boolean.class); + assertThat(result).isEqualTo(expectedResult); + } + + @Test + public void SPR9486_floatNotEqFloat() { + Boolean expectedResult = 10.215f != 10.2109f; + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + Expression expression = parser.parseExpression("10.215f != 10.2109f"); + Boolean result = expression.getValue(context, null, Boolean.class); + assertThat(result).isEqualTo(expectedResult); + } + + @Test + public void SPR9486_floatNotEqDouble() { + Boolean expectedResult = 10.215f != 10.2109; + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + Expression expression = parser.parseExpression("10.215f != 10.2109"); + Boolean result = expression.getValue(context, null, Boolean.class); + assertThat(result).isEqualTo(expectedResult); + } + + @Test + public void SPR9486_floatLessThanFloat() { + Boolean expectedNumber = -10.21f < -10.2f; + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + Expression expression = parser.parseExpression("-10.21f < -10.2f"); + Boolean result = expression.getValue(context, null, Boolean.class); + assertThat(result).isEqualTo(expectedNumber); + } + + @Test + public void SPR9486_floatLessThanDouble() { + Boolean expectedNumber = -10.21f < -10.2; + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + Expression expression = parser.parseExpression("-10.21f < -10.2"); + Boolean result = expression.getValue(context, null, Boolean.class); + assertThat(result).isEqualTo(expectedNumber); + } + + @Test + public void SPR9486_floatLessThanOrEqualFloat() { + Boolean expectedNumber = -10.21f <= -10.22f; + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + Expression expression = parser.parseExpression("-10.21f <= -10.22f"); + Boolean result = expression.getValue(context, null, Boolean.class); + assertThat(result).isEqualTo(expectedNumber); + } + + @Test + public void SPR9486_floatLessThanOrEqualDouble() { + Boolean expectedNumber = -10.21f <= -10.2; + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + Expression expression = parser.parseExpression("-10.21f <= -10.2"); + Boolean result = expression.getValue(context, null, Boolean.class); + assertThat(result).isEqualTo(expectedNumber); + } + + @Test + public void SPR9486_floatGreaterThanFloat() { + Boolean expectedNumber = -10.21f > -10.2f; + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + Expression expression = parser.parseExpression("-10.21f > -10.2f"); + Boolean result = expression.getValue(context, null, Boolean.class); + assertThat(result).isEqualTo(expectedNumber); + } + + @Test + public void SPR9486_floatGreaterThanDouble() { + Boolean expectedResult = -10.21f > -10.2; + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + Expression expression = parser.parseExpression("-10.21f > -10.2"); + Boolean result = expression.getValue(context, null, Boolean.class); + assertThat(result).isEqualTo(expectedResult); + } + + @Test + public void SPR9486_floatGreaterThanOrEqualFloat() { + Boolean expectedNumber = -10.21f >= -10.2f; + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + Expression expression = parser.parseExpression("-10.21f >= -10.2f"); + Boolean result = expression.getValue(context, null, Boolean.class); + assertThat(result).isEqualTo(expectedNumber); + } + + @Test + public void SPR9486_floatGreaterThanEqualDouble() { + Boolean expectedResult = -10.21f >= -10.2; + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + Expression expression = parser.parseExpression("-10.21f >= -10.2"); + Boolean result = expression.getValue(context, null, Boolean.class); + assertThat(result).isEqualTo(expectedResult); + } + + @Test + public void SPR9486_floatModulusFloat() { + Number expectedResult = 10.21f % 10.2f; + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + Expression expression = parser.parseExpression("10.21f % 10.2f"); + Number result = expression.getValue(context, null, Number.class); + assertThat(result).isEqualTo(expectedResult); + } + + @Test + public void SPR9486_floatModulusDouble() { + Number expectedResult = 10.21f % 10.2; + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + Expression expression = parser.parseExpression("10.21f % 10.2"); + Number result = expression.getValue(context, null, Number.class); + assertThat(result).isEqualTo(expectedResult); + } + + @Test + public void SPR9486_floatPowerFloat() { + Number expectedResult = Math.pow(10.21f, -10.2f); + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + Expression expression = parser.parseExpression("10.21f ^ -10.2f"); + Number result = expression.getValue(context, null, Number.class); + assertThat(result).isEqualTo(expectedResult); + } + + @Test + public void SPR9486_floatPowerDouble() { + Number expectedResult = Math.pow(10.21f, 10.2); + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + Expression expression = parser.parseExpression("10.21f ^ 10.2"); + Number result = expression.getValue(context, null, Number.class); + assertThat(result).isEqualTo(expectedResult); + } + + @Test + public void SPR9994_bridgeMethods() throws Exception { + ReflectivePropertyAccessor accessor = new ReflectivePropertyAccessor(); + StandardEvaluationContext context = new StandardEvaluationContext(); + GenericImplementation target = new GenericImplementation(); + accessor.write(context, target, "property", "1"); + assertThat(target.value).isEqualTo(1); + TypedValue value = accessor.read(context, target, "property"); + assertThat(value.getValue()).isEqualTo(1); + assertThat(value.getTypeDescriptor().getType()).isEqualTo(Integer.class); + assertThat(value.getTypeDescriptor().getAnnotations()).isNotEmpty(); + } + + @Test + public void SPR10162_onlyBridgeMethod() throws Exception { + ReflectivePropertyAccessor accessor = new ReflectivePropertyAccessor(); + StandardEvaluationContext context = new StandardEvaluationContext(); + Object target = new OnlyBridgeMethod(); + TypedValue value = accessor.read(context, target, "property"); + assertThat(value.getValue()).isNull(); + assertThat(value.getTypeDescriptor().getType()).isEqualTo(Integer.class); + } + + @Test + public void SPR10091_simpleTestValueType() { + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext evaluationContext = new StandardEvaluationContext(new BooleanHolder()); + Class valueType = parser.parseExpression("simpleProperty").getValueType(evaluationContext); + assertThat(valueType).isEqualTo(Boolean.class); + } + + @Test + public void SPR10091_simpleTestValue() { + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext evaluationContext = new StandardEvaluationContext(new BooleanHolder()); + Object value = parser.parseExpression("simpleProperty").getValue(evaluationContext); + assertThat(value).isInstanceOf(Boolean.class); + } + + @Test + public void SPR10091_primitiveTestValueType() { + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext evaluationContext = new StandardEvaluationContext(new BooleanHolder()); + Class valueType = parser.parseExpression("primitiveProperty").getValueType(evaluationContext); + assertThat(valueType).isEqualTo(Boolean.class); + } + + @Test + public void SPR10091_primitiveTestValue() { + ExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext evaluationContext = new StandardEvaluationContext(new BooleanHolder()); + Object value = parser.parseExpression("primitiveProperty").getValue(evaluationContext); + assertThat(value).isInstanceOf(Boolean.class); + } + + @Test + public void SPR16123() { + ExpressionParser parser = new SpelExpressionParser(); + parser.parseExpression("simpleProperty").setValue(new BooleanHolder(), null); + assertThatExceptionOfType(EvaluationException.class).isThrownBy(() -> + parser.parseExpression("primitiveProperty").setValue(new BooleanHolder(), null)); + } + + @Test + public void SPR10146_malformedExpressions() { + doTestSpr10146("/foo", "EL1070E: Problem parsing left operand"); + doTestSpr10146("*foo", "EL1070E: Problem parsing left operand"); + doTestSpr10146("%foo", "EL1070E: Problem parsing left operand"); + doTestSpr10146("foo", "EL1070E: Problem parsing left operand"); + doTestSpr10146("&&foo", "EL1070E: Problem parsing left operand"); + doTestSpr10146("||foo", "EL1070E: Problem parsing left operand"); + doTestSpr10146("|foo", "EL1069E: Missing expected character '|'"); + } + + private void doTestSpr10146(String expression, String expectedMessage) { + assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> + new SpelExpressionParser().parseExpression(expression)) + .withMessageContaining(expectedMessage); + } + + @Test + public void SPR10125() { + StandardEvaluationContext context = new StandardEvaluationContext(); + String fromInterface = parser.parseExpression("T(" + StaticFinalImpl1.class.getName() + ").VALUE").getValue( + context, String.class); + assertThat(fromInterface).isEqualTo("interfaceValue"); + String fromClass = parser.parseExpression("T(" + StaticFinalImpl2.class.getName() + ").VALUE").getValue( + context, String.class); + assertThat(fromClass).isEqualTo("interfaceValue"); + } + + @Test + public void SPR10210() { + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setVariable("bridgeExample", new org.springframework.expression.spel.spr10210.D()); + Expression parseExpression = parser.parseExpression("#bridgeExample.bridgeMethod()"); + parseExpression.getValue(context); + } + + @Test + public void SPR10328() { + assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> + parser.parseExpression("$[]")) + .withMessageContaining("EL1071E: A required selection expression has not been specified"); + } + + @Test + public void SPR10452() { + SpelParserConfiguration configuration = new SpelParserConfiguration(false, false); + ExpressionParser parser = new SpelExpressionParser(configuration); + + StandardEvaluationContext context = new StandardEvaluationContext(); + Expression spel = parser.parseExpression("#enumType.values()"); + + context.setVariable("enumType", ABC.class); + Object result = spel.getValue(context); + assertThat(result).isNotNull(); + assertThat(result.getClass().isArray()).isTrue(); + assertThat(Array.get(result, 0)).isEqualTo(ABC.A); + assertThat(Array.get(result, 1)).isEqualTo(ABC.B); + assertThat(Array.get(result, 2)).isEqualTo(ABC.C); + + context.setVariable("enumType", XYZ.class); + result = spel.getValue(context); + assertThat(result).isNotNull(); + assertThat(result.getClass().isArray()).isTrue(); + assertThat(Array.get(result, 0)).isEqualTo(XYZ.X); + assertThat(Array.get(result, 1)).isEqualTo(XYZ.Y); + assertThat(Array.get(result, 2)).isEqualTo(XYZ.Z); + } + + @Test + public void SPR9495() { + SpelParserConfiguration configuration = new SpelParserConfiguration(false, false); + ExpressionParser parser = new SpelExpressionParser(configuration); + + StandardEvaluationContext context = new StandardEvaluationContext(); + Expression spel = parser.parseExpression("#enumType.values()"); + + context.setVariable("enumType", ABC.class); + Object result = spel.getValue(context); + assertThat(result).isNotNull(); + assertThat(result.getClass().isArray()).isTrue(); + assertThat(Array.get(result, 0)).isEqualTo(ABC.A); + assertThat(Array.get(result, 1)).isEqualTo(ABC.B); + assertThat(Array.get(result, 2)).isEqualTo(ABC.C); + + context.addMethodResolver(new MethodResolver() { + @Override + public MethodExecutor resolve(EvaluationContext context, Object targetObject, String name, + List argumentTypes) throws AccessException { + return (context1, target, arguments) -> { + try { + Method method = XYZ.class.getMethod("values"); + Object value = method.invoke(target, arguments); + return new TypedValue(value, new TypeDescriptor(new MethodParameter(method, -1)).narrow(value)); + } + catch (Exception ex) { + throw new AccessException(ex.getMessage(), ex); + } + }; + } + }); + + result = spel.getValue(context); + assertThat(result).isNotNull(); + assertThat(result.getClass().isArray()).isTrue(); + assertThat(Array.get(result, 0)).isEqualTo(XYZ.X); + assertThat(Array.get(result, 1)).isEqualTo(XYZ.Y); + assertThat(Array.get(result, 2)).isEqualTo(XYZ.Z); + } + + @Test + public void SPR10486() { + SpelExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + Spr10486 rootObject = new Spr10486(); + Expression classNameExpression = parser.parseExpression("class.name"); + Expression nameExpression = parser.parseExpression("name"); + assertThat(classNameExpression.getValue(context, rootObject)).isEqualTo(Spr10486.class.getName()); + assertThat(nameExpression.getValue(context, rootObject)).isEqualTo("name"); + } + + @Test + public void SPR11142() { + SpelExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext context = new StandardEvaluationContext(); + Spr11142 rootObject = new Spr11142(); + Expression expression = parser.parseExpression("something"); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + expression.getValue(context, rootObject)) + .withMessageContaining("'something' cannot be found"); + } + + @Test + public void SPR9194() { + TestClass2 one = new TestClass2("abc"); + TestClass2 two = new TestClass2("abc"); + Map map = new HashMap<>(); + map.put("one", one); + map.put("two", two); + + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expr = parser.parseExpression("['one'] == ['two']"); + assertThat(expr.getValue(map, Boolean.class)).isTrue(); + } + + @Test + public void SPR11348() { + Collection coll = new LinkedHashSet<>(); + coll.add("one"); + coll.add("two"); + coll = Collections.unmodifiableCollection(coll); + + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expr = parser.parseExpression("new java.util.ArrayList(#root)"); + Object value = expr.getValue(coll); + assertThat(value instanceof ArrayList).isTrue(); + @SuppressWarnings("rawtypes") + ArrayList list = (ArrayList) value; + assertThat(list.get(0)).isEqualTo("one"); + assertThat(list.get(1)).isEqualTo("two"); + } + + @Test + public void SPR11445_simple() { + StandardEvaluationContext context = new StandardEvaluationContext(new Spr11445Class()); + Expression expr = new SpelExpressionParser().parseRaw("echo(parameter())"); + assertThat(expr.getValue(context)).isEqualTo(1); + } + + @Test + public void SPR11445_beanReference() { + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setBeanResolver(new Spr11445Class()); + Expression expr = new SpelExpressionParser().parseRaw("@bean.echo(@bean.parameter())"); + assertThat(expr.getValue(context)).isEqualTo(1); + } + + @Test + @SuppressWarnings("unchecked") + public void SPR11494() { + Expression exp = new SpelExpressionParser().parseExpression("T(java.util.Arrays).asList('a','b')"); + List list = (List) exp.getValue(); + assertThat(list).hasSize(2); + } + + @Test + public void SPR11609() { + StandardEvaluationContext sec = new StandardEvaluationContext(); + sec.addPropertyAccessor(new MapAccessor()); + Expression exp = new SpelExpressionParser().parseExpression( + "T(org.springframework.expression.spel.SpelReproTests$MapWithConstant).X"); + assertThat(exp.getValue(sec)).isEqualTo(1); + } + + @Test + public void SPR9735() { + Item item = new Item(); + item.setName("parent"); + + Item item1 = new Item(); + item1.setName("child1"); + + Item item2 = new Item(); + item2.setName("child2"); + + item.add(item1); + item.add(item2); + + ExpressionParser parser = new SpelExpressionParser(); + EvaluationContext context = new StandardEvaluationContext(); + Expression exp = parser.parseExpression("#item[0].name"); + context.setVariable("item", item); + + assertThat(exp.getValue(context)).isEqualTo("child1"); + } + + @Test + public void SPR12502() { + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expression = parser.parseExpression("#root.getClass().getName()"); + assertThat(expression.getValue(new UnnamedUser())).isEqualTo(UnnamedUser.class.getName()); + assertThat(expression.getValue(new NamedUser())).isEqualTo(NamedUser.class.getName()); + } + + @Test + @SuppressWarnings("rawtypes") + public void SPR12522() { + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expression = parser.parseExpression("T(java.util.Arrays).asList('')"); + Object value = expression.getValue(); + assertThat(value instanceof List).isTrue(); + assertThat(((List) value).isEmpty()).isTrue(); + } + + @Test + public void SPR12803() { + StandardEvaluationContext sec = new StandardEvaluationContext(); + sec.setVariable("iterable", Collections.emptyList()); + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expression = parser.parseExpression("T(org.springframework.expression.spel.SpelReproTests.FooLists).newArrayList(#iterable)"); + assertThat(expression.getValue(sec) instanceof ArrayList).isTrue(); + } + + @Test + public void SPR12808() { + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expression = parser.parseExpression("T(org.springframework.expression.spel.SpelReproTests.DistanceEnforcer).from(#no)"); + StandardEvaluationContext sec = new StandardEvaluationContext(); + sec.setVariable("no", 1); + assertThat(expression.getValue(sec).toString().startsWith("Integer")).isTrue(); + sec = new StandardEvaluationContext(); + sec.setVariable("no", 1.0F); + assertThat(expression.getValue(sec).toString().startsWith("Number")).isTrue(); + sec = new StandardEvaluationContext(); + sec.setVariable("no", "1.0"); + assertThat(expression.getValue(sec).toString().startsWith("Object")).isTrue(); + } + + @Test + @SuppressWarnings("rawtypes") + public void SPR13055() { + List> myPayload = new ArrayList<>(); + + Map v1 = new HashMap<>(); + Map v2 = new HashMap<>(); + + v1.put("test11", "test11"); + v1.put("test12", "test12"); + v2.put("test21", "test21"); + v2.put("test22", "test22"); + + myPayload.add(v1); + myPayload.add(v2); + + EvaluationContext context = new StandardEvaluationContext(myPayload); + + ExpressionParser parser = new SpelExpressionParser(); + + String ex = "#root.![T(org.springframework.util.StringUtils).collectionToCommaDelimitedString(#this.values())]"; + List res = parser.parseExpression(ex).getValue(context, List.class); + assertThat(res.toString()).isEqualTo("[test12,test11, test22,test21]"); + + res = parser.parseExpression("#root.![#this.values()]").getValue(context, + List.class); + assertThat(res.toString()).isEqualTo("[[test12, test11], [test22, test21]]"); + + res = parser.parseExpression("#root.![values()]").getValue(context, List.class); + assertThat(res.toString()).isEqualTo("[[test12, test11], [test22, test21]]"); + } + + @Test + public void AccessingFactoryBean_spr9511() { + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setBeanResolver(new MyBeanResolver()); + Expression expr = new SpelExpressionParser().parseRaw("@foo"); + assertThat(expr.getValue(context)).isEqualTo("custard"); + expr = new SpelExpressionParser().parseRaw("&foo"); + assertThat(expr.getValue(context)).isEqualTo("foo factory"); + + assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> + new SpelExpressionParser().parseRaw("&@foo")) + .satisfies(ex -> { + assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.INVALID_BEAN_REFERENCE); + assertThat(ex.getPosition()).isEqualTo(0); + }); + + assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> + new SpelExpressionParser().parseRaw("@&foo")) + .satisfies(ex -> { + assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.INVALID_BEAN_REFERENCE); + assertThat(ex.getPosition()).isEqualTo(0); + }); + } + + @Test + public void SPR12035() { + ExpressionParser parser = new SpelExpressionParser(); + + Expression expression1 = parser.parseExpression("list.?[ value>2 ].size()!=0"); + assertThat(expression1.getValue(new BeanClass(new ListOf(1.1), new ListOf(2.2)), Boolean.class)).isTrue(); + + Expression expression2 = parser.parseExpression("list.?[ T(java.lang.Math).abs(value) > 2 ].size()!=0"); + assertThat(expression2.getValue(new BeanClass(new ListOf(1.1), new ListOf(-2.2)), Boolean.class)).isTrue(); + } + + @Test + public void SPR13055_maps() { + EvaluationContext context = new StandardEvaluationContext(); + ExpressionParser parser = new SpelExpressionParser(); + + Expression ex = parser.parseExpression("{'a':'y','b':'n'}.![value=='y'?key:null]"); + assertThat(ex.getValue(context).toString()).isEqualTo("[a, null]"); + + ex = parser.parseExpression("{2:4,3:6}.![T(java.lang.Math).abs(#this.key) + 5]"); + assertThat(ex.getValue(context).toString()).isEqualTo("[7, 8]"); + + ex = parser.parseExpression("{2:4,3:6}.![T(java.lang.Math).abs(#this.value) + 5]"); + assertThat(ex.getValue(context).toString()).isEqualTo("[9, 11]"); + } + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void SPR10417() { + List list1 = new ArrayList(); + list1.add("a"); + list1.add("b"); + list1.add("x"); + List list2 = new ArrayList(); + list2.add("c"); + list2.add("x"); + EvaluationContext context = new StandardEvaluationContext(); + context.setVariable("list1", list1); + context.setVariable("list2", list2); + + // #this should be the element from list1 + Expression ex = parser.parseExpression("#list1.?[#list2.contains(#this)]"); + Object result = ex.getValue(context); + assertThat(result.toString()).isEqualTo("[x]"); + + // toString() should be called on the element from list1 + ex = parser.parseExpression("#list1.?[#list2.contains(toString())]"); + result = ex.getValue(context); + assertThat(result.toString()).isEqualTo("[x]"); + + List list3 = new ArrayList(); + list3.add(1); + list3.add(2); + list3.add(3); + list3.add(4); + + context = new StandardEvaluationContext(); + context.setVariable("list3", list3); + ex = parser.parseExpression("#list3.?[#this > 2]"); + result = ex.getValue(context); + assertThat(result.toString()).isEqualTo("[3, 4]"); + + ex = parser.parseExpression("#list3.?[#this >= T(java.lang.Math).abs(T(java.lang.Math).abs(#this))]"); + result = ex.getValue(context); + assertThat(result.toString()).isEqualTo("[1, 2, 3, 4]"); + } + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void SPR10417_maps() { + Map map1 = new HashMap(); + map1.put("A", 65); + map1.put("B", 66); + map1.put("X", 66); + Map map2 = new HashMap(); + map2.put("X", 66); + + EvaluationContext context = new StandardEvaluationContext(); + context.setVariable("map1", map1); + context.setVariable("map2", map2); + + // #this should be the element from list1 + Expression ex = parser.parseExpression("#map1.?[#map2.containsKey(#this.getKey())]"); + Object result = ex.getValue(context); + assertThat(result.toString()).isEqualTo("{X=66}"); + + ex = parser.parseExpression("#map1.?[#map2.containsKey(key)]"); + result = ex.getValue(context); + assertThat(result.toString()).isEqualTo("{X=66}"); + } + + @Test + public void SPR13918() { + EvaluationContext context = new StandardEvaluationContext(); + context.setVariable("encoding", "UTF-8"); + + Expression ex = parser.parseExpression("T(java.nio.charset.Charset).forName(#encoding)"); + Object result = ex.getValue(context); + assertThat(result).isEqualTo(StandardCharsets.UTF_8); + } + + @Test + public void SPR16032() { + EvaluationContext context = new StandardEvaluationContext(); + context.setVariable("str", "a\0b"); + + Expression ex = parser.parseExpression("#str?.split('\0')"); + Object result = ex.getValue(context); + assertThat(ObjectUtils.nullSafeEquals(result, new String[] {"a", "b"})).isTrue(); + } + + + static class MyTypeLocator extends StandardTypeLocator { + + @Override + public Class findType(String typeName) throws EvaluationException { + if (typeName.equals("Spr5899Class")) { + return Spr5899Class.class; + } + if (typeName.equals("Outer")) { + return Outer.class; + } + return super.findType(typeName); + } + } + + + static class Spr5899Class { + + public Spr5899Class() { + } + + public Spr5899Class(Integer i) { + } + + public Spr5899Class(Integer i, String... s) { + } + + public Integer tryToInvokeWithNull(Integer value) { + return value; + } + + public Integer tryToInvokeWithNull2(int i) { + return i; + } + + public String tryToInvokeWithNull3(Integer value, String... strings) { + StringBuilder sb = new StringBuilder(); + for (String string : strings) { + if (string == null) { + sb.append("null"); + } + else { + sb.append(string); + } + } + return sb.toString(); + } + + @Override + public String toString() { + return "instance"; + } + } + + + static class TestProperties { + + public Properties jdbcProperties = new Properties(); + + public Properties foo = new Properties(); + + TestProperties() { + jdbcProperties.put("username", "Dave"); + jdbcProperties.put("alias", "Dave2"); + jdbcProperties.put("foo.bar", "Elephant"); + foo.put("bar", "alias"); + } + } + + + static class MapAccessor implements PropertyAccessor { + + @Override + public Class[] getSpecificTargetClasses() { + return new Class[] {Map.class}; + } + + @Override + public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException { + return (((Map) target).containsKey(name)); + } + + @Override + public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException { + return new TypedValue(((Map) target).get(name)); + } + + @Override + public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException { + return true; + } + + @Override + @SuppressWarnings("unchecked") + public void write(EvaluationContext context, Object target, String name, Object newValue) throws AccessException { + ((Map) target).put(name, newValue); + } + } + + + static class Outer { + + static class Inner { + + public Inner() { + } + + public static int run() { + return 12; + } + + public int run2() { + return 13; + } + } + } + + + static class XX { + + public Map m; + + public String floo = "bar"; + + public XX() { + m = new HashMap<>(); + m.put("$foo", "wibble"); + m.put("bar", "siddle"); + } + } + + + static class MyBeanResolver implements BeanResolver { + + @Override + public Object resolve(EvaluationContext context, String beanName) throws AccessException { + if (beanName.equals("foo")) { + return "custard"; + } + else if (beanName.equals("foo.bar")) { + return "trouble"; + } + else if (beanName.equals("&foo")) { + return "foo factory"; + } + else if (beanName.equals("goo")) { + throw new AccessException("DONT ASK ME ABOUT GOO"); + } + return null; + } + } + + + static class CCC { + + public boolean method(Object o) { + System.out.println(o); + return false; + } + } + + + static class C { + + public List ls; + + public String[] as; + + public Map ms; + + C() { + ls = new ArrayList<>(); + ls.add("abc"); + ls.add("def"); + as = new String[] { "abc", "def" }; + ms = new HashMap<>(); + ms.put("abc", "xyz"); + ms.put("def", "pqr"); + } + } + + + static class D { + + public String a; + + private D(String s) { + a = s; + } + + @Override + public String toString() { + return "D(" + a + ")"; + } + } + + + static class Resource { + + public String getServer() { + return "abc"; + } + } + + + static class ResourceSummary { + + private final Resource resource; + + ResourceSummary() { + this.resource = new Resource(); + } + + public Resource getResource() { + return resource; + } + } + + + static class Foo { + + public ResourceSummary resource = new ResourceSummary(); + } + + + static class Foo2 { + + public void execute(String str) { + System.out.println("Value: " + str); + } + } + + + static class Message { + + private String payload; + + public String getPayload() { + return payload; + } + + public void setPayload(String payload) { + this.payload = payload; + } + } + + + static class Goo { + + public static Goo instance = new Goo(); + + public String bar = "key"; + + public String value = null; + + public String wibble = "wobble"; + + public String getKey() { + return "hello"; + } + + public void setKey(String s) { + value = s; + } + } + + + static class Holder { + + public Map map = new HashMap<>(); + } + + + static class SPR9486_FunctionsClass { + + public int abs(int value) { + return Math.abs(value); + } + + public float abs(float value) { + return Math.abs(value); + } + } + + + public interface VarargsInterface { + + String process(String... args); + } + + + public static class VarargsReceiver implements VarargsInterface { + + @Override + public String process(String... args) { + return "OK"; + } + } + + + public static class ReflectionUtil { + + public Object methodToCall(T param) { + System.out.println(param + " " + param.getClass()); + return "Object methodToCall(T param)"; + } + + public void foo(int... array) { + if (array.length == 0) { + throw new RuntimeException(); + } + } + + public void foo(float... array) { + if (array.length == 0) { + throw new RuntimeException(); + } + } + + public void foo(double... array) { + if (array.length == 0) { + throw new RuntimeException(); + } + } + + public void foo(short... array) { + if (array.length == 0) { + throw new RuntimeException(); + } + } + + public void foo(long... array) { + if (array.length == 0) { + throw new RuntimeException(); + } + } + + public void foo(boolean... array) { + if (array.length == 0) { + throw new RuntimeException(); + } + } + + public void foo(char... array) { + if (array.length == 0) { + throw new RuntimeException(); + } + } + + public void foo(byte... array) { + if (array.length == 0) { + throw new RuntimeException(); + } + } + + public void bar(int... array) { + if (array.length == 0) { + throw new RuntimeException(); + } + } + } + + + class TestPropertyAccessor implements PropertyAccessor { + + private String mapName; + + public TestPropertyAccessor(String mapName) { + this.mapName = mapName; + } + + @SuppressWarnings("unchecked") + public Map getMap(Object target) { + try { + Field f = target.getClass().getDeclaredField(mapName); + return (Map) f.get(target); + } + catch (Exception ex) { + } + return null; + } + + @Override + public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException { + return getMap(target).containsKey(name); + } + + @Override + public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException { + return getMap(target).containsKey(name); + } + + @Override + public Class[] getSpecificTargetClasses() { + return new Class[] {ContextObject.class}; + } + + @Override + public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException { + return new TypedValue(getMap(target).get(name)); + } + + @Override + public void write(EvaluationContext context, Object target, String name, Object newValue) throws AccessException { + getMap(target).put(name, (String) newValue); + } + } + + + static class ContextObject { + + public Map firstContext = new HashMap<>(); + + public Map secondContext = new HashMap<>(); + + public Map thirdContext = new HashMap<>(); + + public Map fourthContext = new HashMap<>(); + + public ContextObject() { + firstContext.put("shouldBeFirst", "first"); + secondContext.put("shouldBeFirst", "second"); + thirdContext.put("shouldBeFirst", "third"); + fourthContext.put("shouldBeFirst", "fourth"); + + secondContext.put("shouldBeSecond", "second"); + thirdContext.put("shouldBeSecond", "third"); + fourthContext.put("shouldBeSecond", "fourth"); + + thirdContext.put("shouldBeThird", "third"); + fourthContext.put("shouldBeThird", "fourth"); + + fourthContext.put("shouldBeFourth", "fourth"); + } + + public Map getFirstContext() { + return firstContext; + } + + public Map getSecondContext() { + return secondContext; + } + + public Map getThirdContext() { + return thirdContext; + } + + public Map getFourthContext() { + return fourthContext; + } + } + + + public static class ListOf { + + private final double value; + + public ListOf(double v) { + this.value = v; + } + + public double getValue() { + return value; + } + } + + + public static class BeanClass { + + private final List list; + + public BeanClass(ListOf... list) { + this.list = Arrays.asList(list); + } + + public List getList() { + return list; + } + } + + + private enum ABC { A, B, C } + + private enum XYZ { X, Y, Z } + + + public static class BooleanHolder { + + private Boolean simpleProperty = true; + + private boolean primitiveProperty = true; + + public void setSimpleProperty(Boolean simpleProperty) { + this.simpleProperty = simpleProperty; + } + + public Boolean isSimpleProperty() { + return this.simpleProperty; + } + + public void setPrimitiveProperty(boolean primitiveProperty) { + this.primitiveProperty = primitiveProperty; + } + + public boolean isPrimitiveProperty() { + return this.primitiveProperty; + } + } + + + private interface GenericInterface { + + void setProperty(T value); + + T getProperty(); + } + + + private static class GenericImplementation implements GenericInterface { + + int value; + + @Override + public void setProperty(Integer value) { + this.value = value; + } + + @Override + @Nullable + public Integer getProperty() { + return this.value; + } + } + + + static class PackagePrivateClassWithGetter { + + public Integer getProperty() { + return null; + } + } + + + public static class OnlyBridgeMethod extends PackagePrivateClassWithGetter { + } + + + public interface StaticFinal { + + String VALUE = "interfaceValue"; + } + + + public abstract static class AbstractStaticFinal implements StaticFinal { + } + + + public static class StaticFinalImpl1 extends AbstractStaticFinal implements StaticFinal { + } + + + public static class StaticFinalImpl2 extends AbstractStaticFinal { + } + + + public static class Spr10486 { + + private String name = "name"; + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + } + + + static class Spr11142 { + + public String isSomething() { + return ""; + } + } + + + static class TestClass2 { // SPR-9194 + + String string; + + public TestClass2(String string) { + this.string = string; + } + + @Override + public boolean equals(Object other) { + return (this == other || (other instanceof TestClass2 && + this.string.equals(((TestClass2) other).string))); + } + + @Override + public int hashCode() { + return this.string.hashCode(); + } + } + + + static class Spr11445Class implements BeanResolver { + + private final AtomicInteger counter = new AtomicInteger(); + + public int echo(int invocation) { + return invocation; + } + + public int parameter() { + return this.counter.incrementAndGet(); + } + + @Override + public Object resolve(EvaluationContext context, String beanName) throws AccessException { + return (beanName.equals("bean") ? this : null); + } + } + + + @SuppressWarnings({"rawtypes", "serial"}) + public static class MapWithConstant extends HashMap { + + public static final int X = 1; + } + + + public static class Item implements List { + + private String name; + + private List children = new ArrayList<>(); + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + + @Override + public int size() { + return this.children.size(); + } + + @Override + public boolean isEmpty() { + return this.children.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return this.children.contains(o); + } + + @Override + public Iterator iterator() { + return this.children.iterator(); + } + + @Override + public Object[] toArray() { + return this.children.toArray(); + } + + @Override + public T[] toArray(T[] a) { + return this.children.toArray(a); + } + + @Override + public boolean add(Item e) { + return this.children.add(e); + } + + @Override + public boolean remove(Object o) { + return this.children.remove(o); + } + + @Override + public boolean containsAll(Collection c) { + return this.children.containsAll(c); + } + + @Override + public boolean addAll(Collection c) { + return this.children.addAll(c); + } + + @Override + public boolean addAll(int index, Collection c) { + return this.children.addAll(index, c); + } + + @Override + public boolean removeAll(Collection c) { + return this.children.removeAll(c); + } + + @Override + public boolean retainAll(Collection c) { + return this.children.retainAll(c); + } + + @Override + public void clear() { + this.children.clear(); + } + + @Override + public Item get(int index) { + return this.children.get(index); + } + + @Override + public Item set(int index, Item element) { + return this.children.set(index, element); + } + + @Override + public void add(int index, Item element) { + this.children.add(index, element); + } + + @Override + public Item remove(int index) { + return this.children.remove(index); + } + + @Override + public int indexOf(Object o) { + return this.children.indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + return this.children.lastIndexOf(o); + } + + @Override + public ListIterator listIterator() { + return this.children.listIterator(); + } + + @Override + public ListIterator listIterator(int index) { + return this.children.listIterator(index); + } + + @Override + public List subList(int fromIndex, int toIndex) { + return this.children.subList(fromIndex, toIndex); + } + } + + + public static class UnnamedUser { + } + + + public static class NamedUser { + + public String getName() { + return "foo"; + } + } + + + public static class FooLists { + + public static List newArrayList(Iterable iterable) { + return new ArrayList<>(); + } + + public static List newArrayList(Object... elements) { + throw new UnsupportedOperationException(); + } + } + + + public static class DistanceEnforcer { + + public static String from(Number no) { + return "Number:" + no.toString(); + } + + public static String from(Integer no) { + return "Integer:" + no.toString(); + } + + public static String from(Object no) { + return "Object:" + no.toString(); + } + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelUtilities.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelUtilities.java new file mode 100644 index 0000000..c8cfc67 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelUtilities.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import java.io.PrintStream; + +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpression; + +/** + * Utilities for working with Spring Expressions. + * + * @author Andy Clement + */ +public class SpelUtilities { + + /** + * Output an indented representation of the expression syntax tree to the specified output stream. + * @param printStream the output stream to print into + * @param expression the expression to be displayed + */ + public static void printAbstractSyntaxTree(PrintStream printStream, Expression expression) { + printStream.println("===> Expression '" + expression.getExpressionString() + "' - AST start"); + printAST(printStream, ((SpelExpression) expression).getAST(), ""); + printStream.println("===> Expression '" + expression.getExpressionString() + "' - AST end"); + } + + /* + * Helper method for printing the AST with indentation + */ + private static void printAST(PrintStream out, SpelNode t, String indent) { + if (t != null) { + StringBuilder sb = new StringBuilder(); + sb.append(indent).append(t.getClass().getSimpleName()); + sb.append(" value:").append(t.toStringAST()); + sb.append(t.getChildCount() < 2 ? "" : " #children:" + t.getChildCount()); + out.println(sb.toString()); + for (int i = 0; i < t.getChildCount(); i++) { + printAST(out, t.getChild(i), indent + " "); + } + } + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/StandardTypeLocatorTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/StandardTypeLocatorTests.java new file mode 100644 index 0000000..a41ac56 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/StandardTypeLocatorTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.EvaluationException; +import org.springframework.expression.spel.support.StandardTypeLocator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Unit tests for type comparison + * + * @author Andy Clement + */ +public class StandardTypeLocatorTests { + + @Test + public void testImports() throws EvaluationException { + StandardTypeLocator locator = new StandardTypeLocator(); + assertThat(locator.findType("java.lang.Integer")).isEqualTo(Integer.class); + assertThat(locator.findType("java.lang.String")).isEqualTo(String.class); + + List prefixes = locator.getImportPrefixes(); + assertThat(prefixes.size()).isEqualTo(1); + assertThat(prefixes.contains("java.lang")).isTrue(); + assertThat(prefixes.contains("java.util")).isFalse(); + + assertThat(locator.findType("Boolean")).isEqualTo(Boolean.class); + // currently does not know about java.util by default +// assertEquals(java.util.List.class,locator.findType("List")); + + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + locator.findType("URL")) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.TYPE_NOT_FOUND)); + locator.registerImport("java.net"); + assertThat(locator.findType("URL")).isEqualTo(java.net.URL.class); + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/TemplateExpressionParsingTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/TemplateExpressionParsingTests.java new file mode 100644 index 0000000..49face8 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/TemplateExpressionParsingTests.java @@ -0,0 +1,244 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Expression; +import org.springframework.expression.ParseException; +import org.springframework.expression.ParserContext; +import org.springframework.expression.common.CompositeStringExpression; +import org.springframework.expression.common.TemplateParserContext; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Andy Clement + * @author Juergen Hoeller + */ +public class TemplateExpressionParsingTests extends AbstractExpressionTests { + + public static final ParserContext DEFAULT_TEMPLATE_PARSER_CONTEXT = new ParserContext() { + @Override + public String getExpressionPrefix() { + return "${"; + } + @Override + public String getExpressionSuffix() { + return "}"; + } + @Override + public boolean isTemplate() { + return true; + } + }; + + public static final ParserContext HASH_DELIMITED_PARSER_CONTEXT = new ParserContext() { + @Override + public String getExpressionPrefix() { + return "#{"; + } + @Override + public String getExpressionSuffix() { + return "}"; + } + @Override + public boolean isTemplate() { + return true; + } + }; + + + @Test + public void testParsingSimpleTemplateExpression01() throws Exception { + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expr = parser.parseExpression("hello ${'world'}", DEFAULT_TEMPLATE_PARSER_CONTEXT); + Object o = expr.getValue(); + assertThat(o.toString()).isEqualTo("hello world"); + } + + @Test + public void testParsingSimpleTemplateExpression02() throws Exception { + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expr = parser.parseExpression("hello ${'to'} you", DEFAULT_TEMPLATE_PARSER_CONTEXT); + Object o = expr.getValue(); + assertThat(o.toString()).isEqualTo("hello to you"); + } + + @Test + public void testParsingSimpleTemplateExpression03() throws Exception { + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expr = parser.parseExpression("The quick ${'brown'} fox jumped over the ${'lazy'} dog", + DEFAULT_TEMPLATE_PARSER_CONTEXT); + Object o = expr.getValue(); + assertThat(o.toString()).isEqualTo("The quick brown fox jumped over the lazy dog"); + } + + @Test + public void testParsingSimpleTemplateExpression04() throws Exception { + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expr = parser.parseExpression("${'hello'} world", DEFAULT_TEMPLATE_PARSER_CONTEXT); + Object o = expr.getValue(); + assertThat(o.toString()).isEqualTo("hello world"); + + expr = parser.parseExpression("", DEFAULT_TEMPLATE_PARSER_CONTEXT); + o = expr.getValue(); + assertThat(o.toString()).isEqualTo(""); + + expr = parser.parseExpression("abc", DEFAULT_TEMPLATE_PARSER_CONTEXT); + o = expr.getValue(); + assertThat(o.toString()).isEqualTo("abc"); + + expr = parser.parseExpression("abc", DEFAULT_TEMPLATE_PARSER_CONTEXT); + o = expr.getValue((Object)null); + assertThat(o.toString()).isEqualTo("abc"); + } + + @Test + public void testCompositeStringExpression() throws Exception { + SpelExpressionParser parser = new SpelExpressionParser(); + Expression ex = parser.parseExpression("hello ${'world'}", DEFAULT_TEMPLATE_PARSER_CONTEXT); + assertThat(ex.getValue()).isInstanceOf(String.class).isEqualTo("hello world"); + assertThat(ex.getValue(String.class)).isInstanceOf(String.class).isEqualTo("hello world"); + assertThat(ex.getValue((Object)null, String.class)).isInstanceOf(String.class).isEqualTo("hello world"); + assertThat(ex.getValue(new Rooty())).isInstanceOf(String.class).isEqualTo("hello world"); + assertThat(ex.getValue(new Rooty(), String.class)).isInstanceOf(String.class).isEqualTo("hello world"); + + EvaluationContext ctx = new StandardEvaluationContext(); + assertThat(ex.getValue(ctx)).isInstanceOf(String.class).isEqualTo("hello world"); + assertThat(ex.getValue(ctx, String.class)).isInstanceOf(String.class).isEqualTo("hello world"); + assertThat(ex.getValue(ctx, null, String.class)).isInstanceOf(String.class).isEqualTo("hello world"); + assertThat(ex.getValue(ctx, new Rooty())).isInstanceOf(String.class).isEqualTo("hello world"); + assertThat(ex.getValue(ctx, new Rooty(), String.class)).isInstanceOf(String.class).isEqualTo("hello world"); + assertThat(ex.getValue(ctx, new Rooty(), String.class)).isInstanceOf(String.class).isEqualTo("hello world"); + assertThat(ex.getExpressionString()).isEqualTo("hello ${'world'}"); + assertThat(ex.isWritable(new StandardEvaluationContext())).isFalse(); + assertThat(ex.isWritable(new Rooty())).isFalse(); + assertThat(ex.isWritable(new StandardEvaluationContext(), new Rooty())).isFalse(); + + assertThat(ex.getValueType()).isEqualTo(String.class); + assertThat(ex.getValueType(ctx)).isEqualTo(String.class); + assertThat(ex.getValueTypeDescriptor().getType()).isEqualTo(String.class); + assertThat(ex.getValueTypeDescriptor(ctx).getType()).isEqualTo(String.class); + assertThat(ex.getValueType(new Rooty())).isEqualTo(String.class); + assertThat(ex.getValueType(ctx, new Rooty())).isEqualTo(String.class); + assertThat(ex.getValueTypeDescriptor(new Rooty()).getType()).isEqualTo(String.class); + assertThat(ex.getValueTypeDescriptor(ctx, new Rooty()).getType()).isEqualTo(String.class); + assertThatExceptionOfType(EvaluationException.class).isThrownBy(() -> + ex.setValue(ctx, null)); + assertThatExceptionOfType(EvaluationException.class).isThrownBy(() -> + ex.setValue((Object)null, null)); + assertThatExceptionOfType(EvaluationException.class).isThrownBy(() -> + ex.setValue(ctx, null, null)); + } + + static class Rooty {} + + @Test + public void testNestedExpressions() throws Exception { + SpelExpressionParser parser = new SpelExpressionParser(); + // treat the nested ${..} as a part of the expression + Expression ex = parser.parseExpression("hello ${listOfNumbersUpToTen.$[#this<5]} world",DEFAULT_TEMPLATE_PARSER_CONTEXT); + String s = ex.getValue(TestScenarioCreator.getTestEvaluationContext(),String.class); + assertThat(s).isEqualTo("hello 4 world"); + + // not a useful expression but tests nested expression syntax that clashes with template prefix/suffix + ex = parser.parseExpression("hello ${listOfNumbersUpToTen.$[#root.listOfNumbersUpToTen.$[#this%2==1]==3]} world",DEFAULT_TEMPLATE_PARSER_CONTEXT); + assertThat(ex.getClass()).isEqualTo(CompositeStringExpression.class); + CompositeStringExpression cse = (CompositeStringExpression)ex; + Expression[] exprs = cse.getExpressions(); + assertThat(exprs.length).isEqualTo(3); + assertThat(exprs[1].getExpressionString()).isEqualTo("listOfNumbersUpToTen.$[#root.listOfNumbersUpToTen.$[#this%2==1]==3]"); + s = ex.getValue(TestScenarioCreator.getTestEvaluationContext(),String.class); + assertThat(s).isEqualTo("hello world"); + + ex = parser.parseExpression("hello ${listOfNumbersUpToTen.$[#this<5]} ${listOfNumbersUpToTen.$[#this>5]} world",DEFAULT_TEMPLATE_PARSER_CONTEXT); + s = ex.getValue(TestScenarioCreator.getTestEvaluationContext(),String.class); + assertThat(s).isEqualTo("hello 4 10 world"); + + assertThatExceptionOfType(ParseException.class).isThrownBy(() -> + parser.parseExpression("hello ${listOfNumbersUpToTen.$[#this<5]} ${listOfNumbersUpToTen.$[#this>5] world",DEFAULT_TEMPLATE_PARSER_CONTEXT)) + .satisfies(pex -> assertThat(pex.getSimpleMessage()).isEqualTo("No ending suffix '}' for expression starting at character 41: ${listOfNumbersUpToTen.$[#this>5] world")); + + assertThatExceptionOfType(ParseException.class).isThrownBy(() -> + parser.parseExpression("hello ${listOfNumbersUpToTen.$[#root.listOfNumbersUpToTen.$[#this%2==1==3]} world",DEFAULT_TEMPLATE_PARSER_CONTEXT)) + .satisfies(pex -> assertThat(pex.getSimpleMessage()).isEqualTo("Found closing '}' at position 74 but most recent opening is '[' at position 30")); + } + + @Test + + public void testClashingWithSuffixes() throws Exception { + // Just wanting to use the prefix or suffix within the template: + Expression ex = parser.parseExpression("hello ${3+4} world",DEFAULT_TEMPLATE_PARSER_CONTEXT); + String s = ex.getValue(TestScenarioCreator.getTestEvaluationContext(),String.class); + assertThat(s).isEqualTo("hello 7 world"); + + ex = parser.parseExpression("hello ${3+4} wo${'${'}rld",DEFAULT_TEMPLATE_PARSER_CONTEXT); + s = ex.getValue(TestScenarioCreator.getTestEvaluationContext(),String.class); + assertThat(s).isEqualTo("hello 7 wo${rld"); + + ex = parser.parseExpression("hello ${3+4} wo}rld",DEFAULT_TEMPLATE_PARSER_CONTEXT); + s = ex.getValue(TestScenarioCreator.getTestEvaluationContext(),String.class); + assertThat(s).isEqualTo("hello 7 wo}rld"); + } + + @Test + public void testParsingNormalExpressionThroughTemplateParser() throws Exception { + Expression expr = parser.parseExpression("1+2+3"); + assertThat(expr.getValue()).isEqualTo(6); + } + + @Test + public void testErrorCases() throws Exception { + assertThatExceptionOfType(ParseException.class).isThrownBy(() -> + parser.parseExpression("hello ${'world'", DEFAULT_TEMPLATE_PARSER_CONTEXT)) + .satisfies(pex -> { + assertThat(pex.getSimpleMessage()).isEqualTo("No ending suffix '}' for expression starting at character 6: ${'world'"); + assertThat(pex.getExpressionString()).isEqualTo("hello ${'world'"); + }); + assertThatExceptionOfType(ParseException.class).isThrownBy(() -> + parser.parseExpression("hello ${'wibble'${'world'}", DEFAULT_TEMPLATE_PARSER_CONTEXT)) + .satisfies(pex -> assertThat(pex.getSimpleMessage()).isEqualTo("No ending suffix '}' for expression starting at character 6: ${'wibble'${'world'}")); + assertThatExceptionOfType(ParseException.class).isThrownBy(() -> + parser.parseExpression("hello ${} world", DEFAULT_TEMPLATE_PARSER_CONTEXT)) + .satisfies(pex -> assertThat(pex.getSimpleMessage()).isEqualTo("No expression defined within delimiter '${}' at character 6")); + } + + @Test + public void testTemplateParserContext() { + TemplateParserContext tpc = new TemplateParserContext("abc","def"); + assertThat(tpc.getExpressionPrefix()).isEqualTo("abc"); + assertThat(tpc.getExpressionSuffix()).isEqualTo("def"); + assertThat(tpc.isTemplate()).isTrue(); + + tpc = new TemplateParserContext(); + assertThat(tpc.getExpressionPrefix()).isEqualTo("#{"); + assertThat(tpc.getExpressionSuffix()).isEqualTo("}"); + assertThat(tpc.isTemplate()).isTrue(); + + ParserContext pc = ParserContext.TEMPLATE_EXPRESSION; + assertThat(pc.getExpressionPrefix()).isEqualTo("#{"); + assertThat(pc.getExpressionSuffix()).isEqualTo("}"); + assertThat(pc.isTemplate()).isTrue(); + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/TestScenarioCreator.java b/spring-expression/src/test/java/org/springframework/expression/spel/TestScenarioCreator.java new file mode 100644 index 0000000..eb60acd --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/TestScenarioCreator.java @@ -0,0 +1,132 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import java.util.GregorianCalendar; + +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.expression.spel.testresources.Inventor; +import org.springframework.expression.spel.testresources.PlaceOfBirth; + +/** + * Builds an evaluation context for test expressions. + * Features of the test evaluation context are: + *

      + *
    • The root context object is an Inventor instance {@link Inventor} + *
    + */ +public class TestScenarioCreator { + + public static StandardEvaluationContext getTestEvaluationContext() { + StandardEvaluationContext testContext = new StandardEvaluationContext(); + setupRootContextObject(testContext); + populateVariables(testContext); + populateFunctions(testContext); + return testContext; + } + + /** + * Register some Java reflect methods as well known functions that can be called from an expression. + * @param testContext the test evaluation context + */ + private static void populateFunctions(StandardEvaluationContext testContext) { + try { + testContext.registerFunction("isEven", + TestScenarioCreator.class.getDeclaredMethod("isEven", Integer.TYPE)); + testContext.registerFunction("reverseInt", + TestScenarioCreator.class.getDeclaredMethod("reverseInt", Integer.TYPE, Integer.TYPE, Integer.TYPE)); + testContext.registerFunction("reverseString", + TestScenarioCreator.class.getDeclaredMethod("reverseString", String.class)); + testContext.registerFunction("varargsFunctionReverseStringsAndMerge", + TestScenarioCreator.class.getDeclaredMethod("varargsFunctionReverseStringsAndMerge", String[].class)); + testContext.registerFunction("varargsFunctionReverseStringsAndMerge2", + TestScenarioCreator.class.getDeclaredMethod("varargsFunctionReverseStringsAndMerge2", Integer.TYPE, String[].class)); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + + /** + * Register some variables that can be referenced from the tests + * @param testContext the test evaluation context + */ + private static void populateVariables(StandardEvaluationContext testContext) { + testContext.setVariable("answer", 42); + } + + /** + * Create the root context object, an Inventor instance. Non-qualified property + * and method references will be resolved against this context object. + * @param testContext the evaluation context in which to set the root object + */ + private static void setupRootContextObject(StandardEvaluationContext testContext) { + GregorianCalendar c = new GregorianCalendar(); + c.set(1856, 7, 9); + Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian"); + tesla.setPlaceOfBirth(new PlaceOfBirth("SmilJan")); + tesla.setInventions(new String[] { "Telephone repeater", "Rotating magnetic field principle", + "Polyphase alternating-current system", "Induction motor", "Alternating-current power transmission", + "Tesla coil transformer", "Wireless communication", "Radio", "Fluorescent lights" }); + testContext.setRootObject(tesla); + } + + + // These methods are registered in the test context and therefore accessible through function calls + // in test expressions + + public static String isEven(int i) { + if ((i % 2) == 0) { + return "y"; + } + return "n"; + } + + public static int[] reverseInt(int i, int j, int k) { + return new int[] { k, j, i }; + } + + public static String reverseString(String input) { + StringBuilder backwards = new StringBuilder(); + for (int i = 0; i < input.length(); i++) { + backwards.append(input.charAt(input.length() - 1 - i)); + } + return backwards.toString(); + } + + public static String varargsFunctionReverseStringsAndMerge(String... strings) { + StringBuilder sb = new StringBuilder(); + if (strings != null) { + for (int i = strings.length - 1; i >= 0; i--) { + sb.append(strings[i]); + } + } + return sb.toString(); + } + + public static String varargsFunctionReverseStringsAndMerge2(int j, String... strings) { + StringBuilder sb = new StringBuilder(); + sb.append(j); + if (strings != null) { + for (int i = strings.length - 1; i >= 0; i--) { + sb.append(strings[i]); + } + } + return sb.toString(); + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/VariableAndFunctionTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/VariableAndFunctionTests.java new file mode 100644 index 0000000..0333e20 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/VariableAndFunctionTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests the evaluation of expressions that access variables and functions (lambda/java). + * + * @author Andy Clement + */ +public class VariableAndFunctionTests extends AbstractExpressionTests { + + @Test + public void testVariableAccess01() { + evaluate("#answer", "42", Integer.class, SHOULD_BE_WRITABLE); + evaluate("#answer / 2", 21, Integer.class, SHOULD_NOT_BE_WRITABLE); + } + + @Test + public void testVariableAccess_WellKnownVariables() { + evaluate("#this.getName()","Nikola Tesla",String.class); + evaluate("#root.getName()","Nikola Tesla",String.class); + } + + @Test + public void testFunctionAccess01() { + evaluate("#reverseInt(1,2,3)", "int[3]{3,2,1}", int[].class); + evaluate("#reverseInt('1',2,3)", "int[3]{3,2,1}", int[].class); // requires type conversion of '1' to 1 + evaluateAndCheckError("#reverseInt(1)", SpelMessage.INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION, 0, 1, 3); + } + + @Test + public void testFunctionAccess02() { + evaluate("#reverseString('hello')", "olleh", String.class); + evaluate("#reverseString(37)", "73", String.class); // requires type conversion of 37 to '37' + } + + @Test + public void testCallVarargsFunction() { + evaluate("#varargsFunctionReverseStringsAndMerge('a','b','c')", "cba", String.class); + evaluate("#varargsFunctionReverseStringsAndMerge('a')", "a", String.class); + evaluate("#varargsFunctionReverseStringsAndMerge()", "", String.class); + evaluate("#varargsFunctionReverseStringsAndMerge('b',25)", "25b", String.class); + evaluate("#varargsFunctionReverseStringsAndMerge(25)", "25", String.class); + evaluate("#varargsFunctionReverseStringsAndMerge2(1,'a','b','c')", "1cba", String.class); + evaluate("#varargsFunctionReverseStringsAndMerge2(2,'a')", "2a", String.class); + evaluate("#varargsFunctionReverseStringsAndMerge2(3)", "3", String.class); + evaluate("#varargsFunctionReverseStringsAndMerge2(4,'b',25)", "425b", String.class); + evaluate("#varargsFunctionReverseStringsAndMerge2(5,25)", "525", String.class); + } + + @Test + public void testCallingIllegalFunctions() throws Exception { + SpelExpressionParser parser = new SpelExpressionParser(); + StandardEvaluationContext ctx = new StandardEvaluationContext(); + ctx.setVariable("notStatic", this.getClass().getMethod("nonStatic")); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + parser.parseRaw("#notStatic()").getValue(ctx)). + satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.FUNCTION_MUST_BE_STATIC)); + } + + + // this method is used by the test above + public void nonStatic() { + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ast/FormatHelperTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ast/FormatHelperTests.java new file mode 100644 index 0000000..2ecdc9e --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ast/FormatHelperTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.convert.TypeDescriptor; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Andy Wilkinson + */ +public class FormatHelperTests { + + @Test + public void formatMethodWithSingleArgumentForMessage() { + String message = FormatHelper.formatMethodForMessage("foo", Arrays.asList(TypeDescriptor.forObject("a string"))); + assertThat(message).isEqualTo("foo(java.lang.String)"); + } + + @Test + public void formatMethodWithMultipleArgumentsForMessage() { + String message = FormatHelper.formatMethodForMessage("foo", Arrays.asList(TypeDescriptor.forObject("a string"), TypeDescriptor.forObject(Integer.valueOf(5)))); + assertThat(message).isEqualTo("foo(java.lang.String,java.lang.Integer)"); + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ast/OpPlusTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ast/OpPlusTests.java new file mode 100644 index 0000000..ef6cb02 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ast/OpPlusTests.java @@ -0,0 +1,221 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.ast; + +import java.sql.Time; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.expression.spel.support.StandardTypeConverter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Unit tests for SpEL's plus operator. + * + * @author Ivo Smid + * @author Chris Beams + * @since 3.2 + * @see OpPlus + */ +public class OpPlusTests { + + @Test + public void test_emptyOperands() { + assertThatIllegalArgumentException().isThrownBy(() -> + new OpPlus(-1, -1)); + } + + @Test + public void test_unaryPlusWithStringLiteral() { + ExpressionState expressionState = new ExpressionState(new StandardEvaluationContext()); + + StringLiteral str = new StringLiteral("word", -1, -1, "word"); + + OpPlus o = new OpPlus(-1, -1, str); + assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + o.getValueInternal(expressionState)); + } + + @Test + public void test_unaryPlusWithNumberOperand() { + ExpressionState expressionState = new ExpressionState(new StandardEvaluationContext()); + + { + RealLiteral realLiteral = new RealLiteral("123.00", -1, -1, 123.0); + OpPlus o = new OpPlus(-1, -1, realLiteral); + TypedValue value = o.getValueInternal(expressionState); + + assertThat(value.getTypeDescriptor().getObjectType()).isEqualTo(Double.class); + assertThat(value.getTypeDescriptor().getType()).isEqualTo(Double.class); + assertThat(value.getValue()).isEqualTo(realLiteral.getLiteralValue().getValue()); + } + + { + IntLiteral intLiteral = new IntLiteral("123", -1, -1, 123); + OpPlus o = new OpPlus(-1, -1, intLiteral); + TypedValue value = o.getValueInternal(expressionState); + + assertThat(value.getTypeDescriptor().getObjectType()).isEqualTo(Integer.class); + assertThat(value.getTypeDescriptor().getType()).isEqualTo(Integer.class); + assertThat(value.getValue()).isEqualTo(intLiteral.getLiteralValue().getValue()); + } + + { + LongLiteral longLiteral = new LongLiteral("123", -1, -1, 123L); + OpPlus o = new OpPlus(-1, -1, longLiteral); + TypedValue value = o.getValueInternal(expressionState); + + assertThat(value.getTypeDescriptor().getObjectType()).isEqualTo(Long.class); + assertThat(value.getTypeDescriptor().getType()).isEqualTo(Long.class); + assertThat(value.getValue()).isEqualTo(longLiteral.getLiteralValue().getValue()); + } + } + + @Test + public void test_binaryPlusWithNumberOperands() { + ExpressionState expressionState = new ExpressionState(new StandardEvaluationContext()); + + { + RealLiteral n1 = new RealLiteral("123.00", -1, -1, 123.0); + RealLiteral n2 = new RealLiteral("456.00", -1, -1, 456.0); + OpPlus o = new OpPlus(-1, -1, n1, n2); + TypedValue value = o.getValueInternal(expressionState); + + assertThat(value.getTypeDescriptor().getObjectType()).isEqualTo(Double.class); + assertThat(value.getTypeDescriptor().getType()).isEqualTo(Double.class); + assertThat(value.getValue()).isEqualTo(Double.valueOf(123.0 + 456.0)); + } + + { + LongLiteral n1 = new LongLiteral("123", -1, -1, 123L); + LongLiteral n2 = new LongLiteral("456", -1, -1, 456L); + OpPlus o = new OpPlus(-1, -1, n1, n2); + TypedValue value = o.getValueInternal(expressionState); + + assertThat(value.getTypeDescriptor().getObjectType()).isEqualTo(Long.class); + assertThat(value.getTypeDescriptor().getType()).isEqualTo(Long.class); + assertThat(value.getValue()).isEqualTo(Long.valueOf(123L + 456L)); + } + + { + IntLiteral n1 = new IntLiteral("123", -1, -1, 123); + IntLiteral n2 = new IntLiteral("456", -1, -1, 456); + OpPlus o = new OpPlus(-1, -1, n1, n2); + TypedValue value = o.getValueInternal(expressionState); + + assertThat(value.getTypeDescriptor().getObjectType()).isEqualTo(Integer.class); + assertThat(value.getTypeDescriptor().getType()).isEqualTo(Integer.class); + assertThat(value.getValue()).isEqualTo(Integer.valueOf(123 + 456)); + } + } + + @Test + public void test_binaryPlusWithStringOperands() { + ExpressionState expressionState = new ExpressionState(new StandardEvaluationContext()); + + StringLiteral n1 = new StringLiteral("\"foo\"", -1, -1, "\"foo\""); + StringLiteral n2 = new StringLiteral("\"bar\"", -1, -1, "\"bar\""); + OpPlus o = new OpPlus(-1, -1, n1, n2); + TypedValue value = o.getValueInternal(expressionState); + + assertThat(value.getTypeDescriptor().getObjectType()).isEqualTo(String.class); + assertThat(value.getTypeDescriptor().getType()).isEqualTo(String.class); + assertThat(value.getValue()).isEqualTo("foobar"); + } + + @Test + public void test_binaryPlusWithLeftStringOperand() { + ExpressionState expressionState = new ExpressionState(new StandardEvaluationContext()); + + StringLiteral n1 = new StringLiteral("\"number is \"", -1, -1, "\"number is \""); + LongLiteral n2 = new LongLiteral("123", -1, -1, 123); + OpPlus o = new OpPlus(-1, -1, n1, n2); + TypedValue value = o.getValueInternal(expressionState); + + assertThat(value.getTypeDescriptor().getObjectType()).isEqualTo(String.class); + assertThat(value.getTypeDescriptor().getType()).isEqualTo(String.class); + assertThat(value.getValue()).isEqualTo("number is 123"); + } + + @Test + public void test_binaryPlusWithRightStringOperand() { + ExpressionState expressionState = new ExpressionState(new StandardEvaluationContext()); + + LongLiteral n1 = new LongLiteral("123", -1, -1, 123); + StringLiteral n2 = new StringLiteral("\" is a number\"", -1, -1, "\" is a number\""); + OpPlus o = new OpPlus(-1, -1, n1, n2); + TypedValue value = o.getValueInternal(expressionState); + + assertThat(value.getTypeDescriptor().getObjectType()).isEqualTo(String.class); + assertThat(value.getTypeDescriptor().getType()).isEqualTo(String.class); + assertThat(value.getValue()).isEqualTo("123 is a number"); + } + + @Test + public void test_binaryPlusWithTime_ToString() { + ExpressionState expressionState = new ExpressionState(new StandardEvaluationContext()); + Time time = new Time(new Date().getTime()); + + VariableReference var = new VariableReference("timeVar", -1, -1); + var.setValue(expressionState, time); + + StringLiteral n2 = new StringLiteral("\" is now\"", -1, -1, "\" is now\""); + OpPlus o = new OpPlus(-1, -1, var, n2); + TypedValue value = o.getValueInternal(expressionState); + + assertThat(value.getTypeDescriptor().getObjectType()).isEqualTo(String.class); + assertThat(value.getTypeDescriptor().getType()).isEqualTo(String.class); + assertThat(value.getValue()).isEqualTo((time + " is now")); + } + + @Test + public void test_binaryPlusWithTimeConverted() { + SimpleDateFormat format = new SimpleDateFormat("hh :--: mm :--: ss", Locale.ENGLISH); + + GenericConversionService conversionService = new GenericConversionService(); + conversionService.addConverter(Time.class, String.class, format::format); + + StandardEvaluationContext evaluationContextConverter = new StandardEvaluationContext(); + evaluationContextConverter.setTypeConverter(new StandardTypeConverter(conversionService)); + + ExpressionState expressionState = new ExpressionState(evaluationContextConverter); + Time time = new Time(new Date().getTime()); + + VariableReference var = new VariableReference("timeVar", -1, -1); + var.setValue(expressionState, time); + + StringLiteral n2 = new StringLiteral("\" is now\"", -1, -1, "\" is now\""); + OpPlus o = new OpPlus(-1, -1, var, n2); + TypedValue value = o.getValueInternal(expressionState); + + assertThat(value.getTypeDescriptor().getObjectType()).isEqualTo(String.class); + assertThat(value.getTypeDescriptor().getType()).isEqualTo(String.class); + assertThat(value.getValue()).isEqualTo((format.format(time) + " is now")); + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/spr10210/A.java b/spring-expression/src/test/java/org/springframework/expression/spel/spr10210/A.java new file mode 100644 index 0000000..9039777 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/spr10210/A.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.spr10210; + +import org.springframework.expression.spel.spr10210.comp.B; +import org.springframework.expression.spel.spr10210.infra.C; + +@SuppressWarnings("serial") +abstract class A extends B { + + public void bridgeMethod() { + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/spr10210/D.java b/spring-expression/src/test/java/org/springframework/expression/spel/spr10210/D.java new file mode 100644 index 0000000..7683344 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/spr10210/D.java @@ -0,0 +1,21 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.spr10210; + +@SuppressWarnings("serial") +public class D extends A { +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/spr10210/comp/B.java b/spring-expression/src/test/java/org/springframework/expression/spel/spr10210/comp/B.java new file mode 100644 index 0000000..0c7342f --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/spr10210/comp/B.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.spr10210.comp; + +import java.io.Serializable; + +import org.springframework.expression.spel.spr10210.infra.C; + +@SuppressWarnings("serial") +public class B implements Serializable { +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/spr10210/infra/C.java b/spring-expression/src/test/java/org/springframework/expression/spel/spr10210/infra/C.java new file mode 100644 index 0000000..d615c23 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/spr10210/infra/C.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.spr10210.infra; + +public interface C { +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/standard/PropertiesConversionSpelTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/standard/PropertiesConversionSpelTests.java new file mode 100644 index 0000000..887bf22 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/standard/PropertiesConversionSpelTests.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.standard; + +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.Expression; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Mark Fisher + */ +public class PropertiesConversionSpelTests { + + private static final SpelExpressionParser parser = new SpelExpressionParser(); + + @Test + public void props() { + Properties props = new Properties(); + props.setProperty("x", "1"); + props.setProperty("y", "2"); + props.setProperty("z", "3"); + Expression expression = parser.parseExpression("foo(#props)"); + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setVariable("props", props); + String result = expression.getValue(context, new TestBean(), String.class); + assertThat(result).isEqualTo("123"); + } + + @Test + public void mapWithAllStringValues() { + Map map = new HashMap<>(); + map.put("x", "1"); + map.put("y", "2"); + map.put("z", "3"); + Expression expression = parser.parseExpression("foo(#props)"); + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setVariable("props", map); + String result = expression.getValue(context, new TestBean(), String.class); + assertThat(result).isEqualTo("123"); + } + + @Test + public void mapWithNonStringValue() { + Map map = new HashMap<>(); + map.put("x", "1"); + map.put("y", 2); + map.put("z", "3"); + map.put("a", new UUID(1, 1)); + Expression expression = parser.parseExpression("foo(#props)"); + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setVariable("props", map); + String result = expression.getValue(context, new TestBean(), String.class); + assertThat(result).isEqualTo("1null3"); + } + + @Test + public void customMapWithNonStringValue() { + CustomMap map = new CustomMap(); + map.put("x", "1"); + map.put("y", 2); + map.put("z", "3"); + Expression expression = parser.parseExpression("foo(#props)"); + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setVariable("props", map); + String result = expression.getValue(context, new TestBean(), String.class); + assertThat(result).isEqualTo("1null3"); + } + + + private static class TestBean { + + @SuppressWarnings("unused") + public String foo(Properties props) { + return props.getProperty("x") + props.getProperty("y") + props.getProperty("z"); + } + } + + + @SuppressWarnings("serial") + private static class CustomMap extends HashMap { + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelCompilerTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelCompilerTests.java new file mode 100644 index 0000000..c230151 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelCompilerTests.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.standard; + +import java.util.stream.IntStream; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.Ordered; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.SpelCompilationCoverageTests; +import org.springframework.expression.spel.SpelCompilerMode; +import org.springframework.expression.spel.SpelParserConfiguration; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for the {@link SpelCompiler}. + * + * @author Sam Brannen + * @author Andy Clement + * @since 5.1.14 + */ +class SpelCompilerTests { + + @Test // gh-24357 + void expressionCompilesWhenMethodComesFromPublicInterface() { + SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, null); + SpelExpressionParser parser = new SpelExpressionParser(config); + + OrderedComponent component = new OrderedComponent(); + Expression expression = parser.parseExpression("order"); + + // Evaluate the expression multiple times to ensure that it gets compiled. + IntStream.rangeClosed(1, 5).forEach(i -> assertThat(expression.getValue(component)).isEqualTo(42)); + } + + @Test // gh-25706 + void defaultMethodInvocation() { + SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, null); + SpelExpressionParser parser = new SpelExpressionParser(config); + + StandardEvaluationContext context = new StandardEvaluationContext(); + Item item = new Item(); + context.setRootObject(item); + + Expression expression = parser.parseExpression("#root.isEditable2()"); + assertThat(SpelCompiler.compile(expression)).isFalse(); + assertThat(expression.getValue(context)).isEqualTo(false); + assertThat(SpelCompiler.compile(expression)).isTrue(); + SpelCompilationCoverageTests.assertIsCompiled(expression); + assertThat(expression.getValue(context)).isEqualTo(false); + + context.setVariable("user", new User()); + expression = parser.parseExpression("#root.isEditable(#user)"); + assertThat(SpelCompiler.compile(expression)).isFalse(); + assertThat(expression.getValue(context)).isEqualTo(true); + assertThat(SpelCompiler.compile(expression)).isTrue(); + SpelCompilationCoverageTests.assertIsCompiled(expression); + assertThat(expression.getValue(context)).isEqualTo(true); + } + + + static class OrderedComponent implements Ordered { + + @Override + public int getOrder() { + return 42; + } + } + + + public static class User { + + boolean isAdmin() { + return true; + } + } + + + public static class Item implements Editable { + + // some fields + private String someField = ""; + + // some getters and setters + + @Override + public boolean hasSomeProperty() { + return someField != null; + } + } + + + public interface Editable { + + default boolean isEditable(User user) { + return user.isAdmin() && hasSomeProperty(); + } + + default boolean isEditable2() { + return false; + } + + boolean hasSomeProperty(); + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelParserTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelParserTests.java new file mode 100644 index 0000000..9133b7b --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelParserTests.java @@ -0,0 +1,391 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.standard; + +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.ExpressionException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.SpelNode; +import org.springframework.expression.spel.SpelParseException; +import org.springframework.expression.spel.ast.OpAnd; +import org.springframework.expression.spel.ast.OpOr; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Andy Clement + * @author Juergen Hoeller + */ +class SpelParserTests { + + @Test + void theMostBasic() { + SpelExpressionParser parser = new SpelExpressionParser(); + SpelExpression expr = parser.parseRaw("2"); + assertThat(expr).isNotNull(); + assertThat(expr.getAST()).isNotNull(); + assertThat(expr.getValue()).isEqualTo(2); + assertThat(expr.getValueType()).isEqualTo(Integer.class); + assertThat(expr.getAST().getValue(null)).isEqualTo(2); + } + + @Test + void valueType() { + SpelExpressionParser parser = new SpelExpressionParser(); + EvaluationContext ctx = new StandardEvaluationContext(); + Class c = parser.parseRaw("2").getValueType(); + assertThat(c).isEqualTo(Integer.class); + c = parser.parseRaw("12").getValueType(ctx); + assertThat(c).isEqualTo(Integer.class); + c = parser.parseRaw("null").getValueType(); + assertThat(c).isNull(); + c = parser.parseRaw("null").getValueType(ctx); + assertThat(c).isNull(); + Object o = parser.parseRaw("null").getValue(ctx, Integer.class); + assertThat(o).isNull(); + } + + @Test + void whitespace() { + SpelExpressionParser parser = new SpelExpressionParser(); + SpelExpression expr = parser.parseRaw("2 + 3"); + assertThat(expr.getValue()).isEqualTo(5); + expr = parser.parseRaw("2 + 3"); + assertThat(expr.getValue()).isEqualTo(5); + expr = parser.parseRaw("2\n+\t3"); + assertThat(expr.getValue()).isEqualTo(5); + expr = parser.parseRaw("2\r\n+\t3"); + assertThat(expr.getValue()).isEqualTo(5); + } + + @Test + void arithmeticPlus1() { + SpelExpressionParser parser = new SpelExpressionParser(); + SpelExpression expr = parser.parseRaw("2+2"); + assertThat(expr).isNotNull(); + assertThat(expr.getAST()).isNotNull(); + assertThat(expr.getValue()).isEqualTo(4); + } + + @Test + void arithmeticPlus2() { + SpelExpressionParser parser = new SpelExpressionParser(); + SpelExpression expr = parser.parseRaw("37+41"); + assertThat(expr.getValue()).isEqualTo(78); + } + + @Test + void arithmeticMultiply1() { + SpelExpressionParser parser = new SpelExpressionParser(); + SpelExpression expr = parser.parseRaw("2*3"); + assertThat(expr).isNotNull(); + assertThat(expr.getAST()).isNotNull(); + assertThat(expr.getValue()).isEqualTo(6); + } + + @Test + void arithmeticPrecedence1() { + SpelExpressionParser parser = new SpelExpressionParser(); + SpelExpression expr = parser.parseRaw("2*3+5"); + assertThat(expr.getValue()).isEqualTo(11); + } + + @Test + void generalExpressions() { + assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> { + SpelExpressionParser parser = new SpelExpressionParser(); + parser.parseRaw("new String"); + }) + .satisfies(parseExceptionRequirements(SpelMessage.MISSING_CONSTRUCTOR_ARGS, 10)); + + assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> { + SpelExpressionParser parser = new SpelExpressionParser(); + parser.parseRaw("new String(3,"); + }) + .satisfies(parseExceptionRequirements(SpelMessage.RUN_OUT_OF_ARGUMENTS, 10)); + + assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> { + SpelExpressionParser parser = new SpelExpressionParser(); + parser.parseRaw("new String(3"); + }) + .satisfies(parseExceptionRequirements(SpelMessage.RUN_OUT_OF_ARGUMENTS, 10)); + + assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> { + SpelExpressionParser parser = new SpelExpressionParser(); + parser.parseRaw("new String("); + }) + .satisfies(parseExceptionRequirements(SpelMessage.RUN_OUT_OF_ARGUMENTS, 10)); + + assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> { + SpelExpressionParser parser = new SpelExpressionParser(); + parser.parseRaw("\"abc"); + }) + .satisfies(parseExceptionRequirements(SpelMessage.NON_TERMINATING_DOUBLE_QUOTED_STRING, 0)); + + assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> { + SpelExpressionParser parser = new SpelExpressionParser(); + parser.parseRaw("'abc"); + }) + .satisfies(parseExceptionRequirements(SpelMessage.NON_TERMINATING_QUOTED_STRING, 0)); + + } + + private Consumer parseExceptionRequirements( + SpelMessage expectedMessage, int expectedPosition) { + return ex -> { + assertThat(ex.getMessageCode()).isEqualTo(expectedMessage); + assertThat(ex.getPosition()).isEqualTo(expectedPosition); + assertThat(ex.getMessage()).contains(ex.getExpressionString()); + }; + } + + @Test + void arithmeticPrecedence2() { + SpelExpressionParser parser = new SpelExpressionParser(); + SpelExpression expr = parser.parseRaw("2+3*5"); + assertThat(expr.getValue()).isEqualTo(17); + } + + @Test + void arithmeticPrecedence3() { + SpelExpression expr = new SpelExpressionParser().parseRaw("3+10/2"); + assertThat(expr.getValue()).isEqualTo(8); + } + + @Test + void arithmeticPrecedence4() { + SpelExpression expr = new SpelExpressionParser().parseRaw("10/2+3"); + assertThat(expr.getValue()).isEqualTo(8); + } + + @Test + void arithmeticPrecedence5() { + SpelExpression expr = new SpelExpressionParser().parseRaw("(4+10)/2"); + assertThat(expr.getValue()).isEqualTo(7); + } + + @Test + void arithmeticPrecedence6() { + SpelExpression expr = new SpelExpressionParser().parseRaw("(3+2)*2"); + assertThat(expr.getValue()).isEqualTo(10); + } + + @Test + void booleanOperators() { + SpelExpression expr = new SpelExpressionParser().parseRaw("true"); + assertThat(expr.getValue(Boolean.class)).isEqualTo(Boolean.TRUE); + expr = new SpelExpressionParser().parseRaw("false"); + assertThat(expr.getValue(Boolean.class)).isEqualTo(Boolean.FALSE); + expr = new SpelExpressionParser().parseRaw("false and false"); + assertThat(expr.getValue(Boolean.class)).isEqualTo(Boolean.FALSE); + expr = new SpelExpressionParser().parseRaw("true and (true or false)"); + assertThat(expr.getValue(Boolean.class)).isEqualTo(Boolean.TRUE); + expr = new SpelExpressionParser().parseRaw("true and true or false"); + assertThat(expr.getValue(Boolean.class)).isEqualTo(Boolean.TRUE); + expr = new SpelExpressionParser().parseRaw("!true"); + assertThat(expr.getValue(Boolean.class)).isEqualTo(Boolean.FALSE); + expr = new SpelExpressionParser().parseRaw("!(false or true)"); + assertThat(expr.getValue(Boolean.class)).isEqualTo(Boolean.FALSE); + } + + @Test + void booleanOperators_symbolic_spr9614() { + SpelExpression expr = new SpelExpressionParser().parseRaw("true"); + assertThat(expr.getValue(Boolean.class)).isEqualTo(Boolean.TRUE); + expr = new SpelExpressionParser().parseRaw("false"); + assertThat(expr.getValue(Boolean.class)).isEqualTo(Boolean.FALSE); + expr = new SpelExpressionParser().parseRaw("false && false"); + assertThat(expr.getValue(Boolean.class)).isEqualTo(Boolean.FALSE); + expr = new SpelExpressionParser().parseRaw("true && (true || false)"); + assertThat(expr.getValue(Boolean.class)).isEqualTo(Boolean.TRUE); + expr = new SpelExpressionParser().parseRaw("true && true || false"); + assertThat(expr.getValue(Boolean.class)).isEqualTo(Boolean.TRUE); + expr = new SpelExpressionParser().parseRaw("!true"); + assertThat(expr.getValue(Boolean.class)).isEqualTo(Boolean.FALSE); + expr = new SpelExpressionParser().parseRaw("!(false || true)"); + assertThat(expr.getValue(Boolean.class)).isEqualTo(Boolean.FALSE); + } + + @Test + void stringLiterals() { + SpelExpression expr = new SpelExpressionParser().parseRaw("'howdy'"); + assertThat(expr.getValue()).isEqualTo("howdy"); + expr = new SpelExpressionParser().parseRaw("'hello '' world'"); + assertThat(expr.getValue()).isEqualTo("hello ' world"); + } + + @Test + void stringLiterals2() { + SpelExpression expr = new SpelExpressionParser().parseRaw("'howdy'.substring(0,2)"); + assertThat(expr.getValue()).isEqualTo("ho"); + } + + @Test + void testStringLiterals_DoubleQuotes_spr9620() { + SpelExpression expr = new SpelExpressionParser().parseRaw("\"double quote: \"\".\""); + assertThat(expr.getValue()).isEqualTo("double quote: \"."); + expr = new SpelExpressionParser().parseRaw("\"hello \"\" world\""); + assertThat(expr.getValue()).isEqualTo("hello \" world"); + } + + @Test + void testStringLiterals_DoubleQuotes_spr9620_2() { + assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> + new SpelExpressionParser().parseRaw("\"double quote: \\\"\\\".\"")) + .satisfies(ex -> { + assertThat(ex.getPosition()).isEqualTo(17); + assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.UNEXPECTED_ESCAPE_CHAR); + }); + } + + @Test + void positionalInformation() { + SpelExpression expr = new SpelExpressionParser().parseRaw("true and true or false"); + SpelNode rootAst = expr.getAST(); + OpOr operatorOr = (OpOr) rootAst; + OpAnd operatorAnd = (OpAnd) operatorOr.getLeftOperand(); + SpelNode rightOrOperand = operatorOr.getRightOperand(); + + // check position for final 'false' + assertThat(rightOrOperand.getStartPosition()).isEqualTo(17); + assertThat(rightOrOperand.getEndPosition()).isEqualTo(22); + + // check position for first 'true' + assertThat(operatorAnd.getLeftOperand().getStartPosition()).isEqualTo(0); + assertThat(operatorAnd.getLeftOperand().getEndPosition()).isEqualTo(4); + + // check position for second 'true' + assertThat(operatorAnd.getRightOperand().getStartPosition()).isEqualTo(9); + assertThat(operatorAnd.getRightOperand().getEndPosition()).isEqualTo(13); + + // check position for OperatorAnd + assertThat(operatorAnd.getStartPosition()).isEqualTo(5); + assertThat(operatorAnd.getEndPosition()).isEqualTo(8); + + // check position for OperatorOr + assertThat(operatorOr.getStartPosition()).isEqualTo(14); + assertThat(operatorOr.getEndPosition()).isEqualTo(16); + } + + @Test + void tokenKind() { + TokenKind tk = TokenKind.NOT; + assertThat(tk.hasPayload()).isFalse(); + assertThat(tk.toString()).isEqualTo("NOT(!)"); + + tk = TokenKind.MINUS; + assertThat(tk.hasPayload()).isFalse(); + assertThat(tk.toString()).isEqualTo("MINUS(-)"); + + tk = TokenKind.LITERAL_STRING; + assertThat(tk.toString()).isEqualTo("LITERAL_STRING"); + assertThat(tk.hasPayload()).isTrue(); + } + + @Test + void token() { + Token token = new Token(TokenKind.NOT, 0, 3); + assertThat(token.kind).isEqualTo(TokenKind.NOT); + assertThat(token.startPos).isEqualTo(0); + assertThat(token.endPos).isEqualTo(3); + assertThat(token.toString()).isEqualTo("[NOT(!)](0,3)"); + + token = new Token(TokenKind.LITERAL_STRING, "abc".toCharArray(), 0, 3); + assertThat(token.kind).isEqualTo(TokenKind.LITERAL_STRING); + assertThat(token.startPos).isEqualTo(0); + assertThat(token.endPos).isEqualTo(3); + assertThat(token.toString()).isEqualTo("[LITERAL_STRING:abc](0,3)"); + } + + @Test + void exceptions() { + ExpressionException exprEx = new ExpressionException("test"); + assertThat(exprEx.getSimpleMessage()).isEqualTo("test"); + assertThat(exprEx.toDetailedString()).isEqualTo("test"); + assertThat(exprEx.getMessage()).isEqualTo("test"); + + exprEx = new ExpressionException("wibble", "test"); + assertThat(exprEx.getSimpleMessage()).isEqualTo("test"); + assertThat(exprEx.toDetailedString()).isEqualTo("Expression [wibble]: test"); + assertThat(exprEx.getMessage()).isEqualTo("Expression [wibble]: test"); + + exprEx = new ExpressionException("wibble", 3, "test"); + assertThat(exprEx.getSimpleMessage()).isEqualTo("test"); + assertThat(exprEx.toDetailedString()).isEqualTo("Expression [wibble] @3: test"); + assertThat(exprEx.getMessage()).isEqualTo("Expression [wibble] @3: test"); + } + + @Test + void parseMethodsOnNumbers() { + checkNumber("3.14.toString()", "3.14", String.class); + checkNumber("3.toString()", "3", String.class); + } + + @Test + void numerics() { + checkNumber("2", 2, Integer.class); + checkNumber("22", 22, Integer.class); + checkNumber("+22", 22, Integer.class); + checkNumber("-22", -22, Integer.class); + checkNumber("2L", 2L, Long.class); + checkNumber("22l", 22L, Long.class); + + checkNumber("0x1", 1, Integer.class); + checkNumber("0x1L", 1L, Long.class); + checkNumber("0xa", 10, Integer.class); + checkNumber("0xAL", 10L, Long.class); + + checkNumberError("0x", SpelMessage.NOT_AN_INTEGER); + checkNumberError("0xL", SpelMessage.NOT_A_LONG); + checkNumberError(".324", SpelMessage.UNEXPECTED_DATA_AFTER_DOT); + checkNumberError("3.4L", SpelMessage.REAL_CANNOT_BE_LONG); + + checkNumber("3.5f", 3.5f, Float.class); + checkNumber("1.2e3", 1.2e3d, Double.class); + checkNumber("1.2e+3", 1.2e3d, Double.class); + checkNumber("1.2e-3", 1.2e-3d, Double.class); + checkNumber("1.2e3", 1.2e3d, Double.class); + checkNumber("1e+3", 1e3d, Double.class); + } + + + private void checkNumber(String expression, Object value, Class type) { + try { + SpelExpressionParser parser = new SpelExpressionParser(); + SpelExpression expr = parser.parseRaw(expression); + Object exprVal = expr.getValue(); + assertThat(exprVal).isEqualTo(value); + assertThat(exprVal.getClass()).isEqualTo(type); + } + catch (Exception ex) { + throw new AssertionError(ex.getMessage(), ex); + } + } + + private void checkNumberError(String expression, SpelMessage expectedMessage) { + SpelExpressionParser parser = new SpelExpressionParser(); + assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> parser.parseRaw(expression)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(expectedMessage)); + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/support/ReflectionHelperTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/support/ReflectionHelperTests.java new file mode 100644 index 0000000..c651d41 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/support/ReflectionHelperTests.java @@ -0,0 +1,495 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.support; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.ParseException; +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.AbstractExpressionTests; +import org.springframework.expression.spel.SpelUtilities; +import org.springframework.expression.spel.standard.SpelExpression; +import org.springframework.expression.spel.support.ReflectionHelper.ArgumentsMatchKind; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for reflection helper code. + * + * @author Andy Clement + */ +public class ReflectionHelperTests extends AbstractExpressionTests { + + @Test + public void testUtilities() throws ParseException { + SpelExpression expr = (SpelExpression)parser.parseExpression("3+4+5+6+7-2"); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(baos); + SpelUtilities.printAbstractSyntaxTree(ps, expr); + ps.flush(); + String s = baos.toString(); +// ===> Expression '3+4+5+6+7-2' - AST start +// OperatorMinus value:(((((3 + 4) + 5) + 6) + 7) - 2) #children:2 +// OperatorPlus value:((((3 + 4) + 5) + 6) + 7) #children:2 +// OperatorPlus value:(((3 + 4) + 5) + 6) #children:2 +// OperatorPlus value:((3 + 4) + 5) #children:2 +// OperatorPlus value:(3 + 4) #children:2 +// CompoundExpression value:3 +// IntLiteral value:3 +// CompoundExpression value:4 +// IntLiteral value:4 +// CompoundExpression value:5 +// IntLiteral value:5 +// CompoundExpression value:6 +// IntLiteral value:6 +// CompoundExpression value:7 +// IntLiteral value:7 +// CompoundExpression value:2 +// IntLiteral value:2 +// ===> Expression '3+4+5+6+7-2' - AST end + assertThat(s.contains("===> Expression '3+4+5+6+7-2' - AST start")).isTrue(); + assertThat(s.contains(" OpPlus value:((((3 + 4) + 5) + 6) + 7) #children:2")).isTrue(); + } + + @Test + public void testTypedValue() { + TypedValue tv1 = new TypedValue("hello"); + TypedValue tv2 = new TypedValue("hello"); + TypedValue tv3 = new TypedValue("bye"); + assertThat(tv1.getTypeDescriptor().getType()).isEqualTo(String.class); + assertThat(tv1.toString()).isEqualTo("TypedValue: 'hello' of [java.lang.String]"); + assertThat(tv2).isEqualTo(tv1); + assertThat(tv1).isEqualTo(tv2); + assertThat(tv3).isNotEqualTo(tv1); + assertThat(tv3).isNotEqualTo(tv2); + assertThat(tv1).isNotEqualTo(tv3); + assertThat(tv2).isNotEqualTo(tv3); + assertThat(tv2.hashCode()).isEqualTo(tv1.hashCode()); + assertThat(tv3.hashCode()).isNotEqualTo(tv1.hashCode()); + assertThat(tv3.hashCode()).isNotEqualTo(tv2.hashCode()); + } + + @Test + public void testReflectionHelperCompareArguments_ExactMatching() { + StandardTypeConverter tc = new StandardTypeConverter(); + + // Calling foo(String) with (String) is exact match + checkMatch(new Class[] {String.class}, new Class[] {String.class}, tc, ReflectionHelper.ArgumentsMatchKind.EXACT); + + // Calling foo(String,Integer) with (String,Integer) is exact match + checkMatch(new Class[] {String.class, Integer.class}, new Class[] {String.class, Integer.class}, tc, ArgumentsMatchKind.EXACT); + } + + @Test + public void testReflectionHelperCompareArguments_CloseMatching() { + StandardTypeConverter tc = new StandardTypeConverter(); + + // Calling foo(List) with (ArrayList) is close match (no conversion required) + checkMatch(new Class[] {ArrayList.class}, new Class[] {List.class}, tc, ArgumentsMatchKind.CLOSE); + + // Passing (Sub,String) on call to foo(Super,String) is close match + checkMatch(new Class[] {Sub.class, String.class}, new Class[] {Super.class, String.class}, tc, ArgumentsMatchKind.CLOSE); + + // Passing (String,Sub) on call to foo(String,Super) is close match + checkMatch(new Class[] {String.class, Sub.class}, new Class[] {String.class, Super.class}, tc, ArgumentsMatchKind.CLOSE); + } + + @Test + public void testReflectionHelperCompareArguments_RequiresConversionMatching() { + StandardTypeConverter tc = new StandardTypeConverter(); + + // Calling foo(String,int) with (String,Integer) requires boxing conversion of argument one + checkMatch(new Class[] {String.class, Integer.TYPE}, new Class[] {String.class,Integer.class},tc, ArgumentsMatchKind.CLOSE); + + // Passing (int,String) on call to foo(Integer,String) requires boxing conversion of argument zero + checkMatch(new Class[] {Integer.TYPE, String.class}, new Class[] {Integer.class, String.class},tc, ArgumentsMatchKind.CLOSE); + + // Passing (int,Sub) on call to foo(Integer,Super) requires boxing conversion of argument zero + checkMatch(new Class[] {Integer.TYPE, Sub.class}, new Class[] {Integer.class, Super.class}, tc, ArgumentsMatchKind.CLOSE); + + // Passing (int,Sub,boolean) on call to foo(Integer,Super,Boolean) requires boxing conversion of arguments zero and two + // TODO checkMatch(new Class[] {Integer.TYPE, Sub.class, Boolean.TYPE}, new Class[] {Integer.class, Super.class, Boolean.class}, tc, ArgsMatchKind.REQUIRES_CONVERSION); + } + + @Test + public void testReflectionHelperCompareArguments_NotAMatch() { + StandardTypeConverter typeConverter = new StandardTypeConverter(); + + // Passing (Super,String) on call to foo(Sub,String) is not a match + checkMatch(new Class[] {Super.class,String.class}, new Class[] {Sub.class,String.class}, typeConverter, null); + } + + @Test + public void testReflectionHelperCompareArguments_Varargs_ExactMatching() { + StandardTypeConverter tc = new StandardTypeConverter(); + + // Passing (String[]) on call to (String[]) is exact match + checkMatch2(new Class[] {String[].class}, new Class[] {String[].class}, tc, ArgumentsMatchKind.EXACT); + + // Passing (Integer, String[]) on call to (Integer, String[]) is exact match + checkMatch2(new Class[] {Integer.class, String[].class}, new Class[] {Integer.class, String[].class}, tc, ArgumentsMatchKind.EXACT); + + // Passing (String, Integer, String[]) on call to (String, String, String[]) is exact match + checkMatch2(new Class[] {String.class, Integer.class, String[].class}, new Class[] {String.class,Integer.class, String[].class}, tc, ArgumentsMatchKind.EXACT); + + // Passing (Sub, String[]) on call to (Super, String[]) is exact match + checkMatch2(new Class[] {Sub.class, String[].class}, new Class[] {Super.class,String[].class}, tc, ArgumentsMatchKind.CLOSE); + + // Passing (Integer, String[]) on call to (String, String[]) is exact match + checkMatch2(new Class[] {Integer.class, String[].class}, new Class[] {String.class, String[].class}, tc, ArgumentsMatchKind.REQUIRES_CONVERSION); + + // Passing (Integer, Sub, String[]) on call to (String, Super, String[]) is exact match + checkMatch2(new Class[] {Integer.class, Sub.class, String[].class}, new Class[] {String.class,Super .class, String[].class}, tc, ArgumentsMatchKind.REQUIRES_CONVERSION); + + // Passing (String) on call to (String[]) is exact match + checkMatch2(new Class[] {String.class}, new Class[] {String[].class}, tc, ArgumentsMatchKind.EXACT); + + // Passing (Integer,String) on call to (Integer,String[]) is exact match + checkMatch2(new Class[] {Integer.class, String.class}, new Class[] {Integer.class, String[].class}, tc, ArgumentsMatchKind.EXACT); + + // Passing (String) on call to (Integer[]) is conversion match (String to Integer) + checkMatch2(new Class[] {String.class}, new Class[] {Integer[].class}, tc, ArgumentsMatchKind.REQUIRES_CONVERSION); + + // Passing (Sub) on call to (Super[]) is close match + checkMatch2(new Class[] {Sub.class}, new Class[] {Super[].class}, tc, ArgumentsMatchKind.CLOSE); + + // Passing (Super) on call to (Sub[]) is not a match + checkMatch2(new Class[] {Super.class}, new Class[] {Sub[].class}, tc, null); + + checkMatch2(new Class[] {Unconvertable.class, String.class}, new Class[] {Sub.class, Super[].class}, tc, null); + + checkMatch2(new Class[] {Integer.class, Integer.class, String.class}, new Class[] {String.class, String.class, Super[].class}, tc, null); + + checkMatch2(new Class[] {Unconvertable.class, String.class}, new Class[] {Sub.class, Super[].class}, tc, null); + + checkMatch2(new Class[] {Integer.class, Integer.class, String.class}, new Class[] {String.class, String.class, Super[].class}, tc, null); + + checkMatch2(new Class[] {Integer.class, Integer.class, Sub.class}, new Class[] {String.class, String.class, Super[].class}, tc, ArgumentsMatchKind.REQUIRES_CONVERSION); + + checkMatch2(new Class[] {Integer.class, Integer.class, Integer.class}, new Class[] {Integer.class, String[].class}, tc, ArgumentsMatchKind.REQUIRES_CONVERSION); + // what happens on (Integer,String) passed to (Integer[]) ? + } + + @Test + public void testConvertArguments() throws Exception { + StandardTypeConverter tc = new StandardTypeConverter(); + Method oneArg = TestInterface.class.getMethod("oneArg", String.class); + Method twoArg = TestInterface.class.getMethod("twoArg", String.class, String[].class); + + // basic conversion int>String + Object[] args = new Object[] {3}; + ReflectionHelper.convertArguments(tc, args, oneArg, null); + checkArguments(args, "3"); + + // varargs but nothing to convert + args = new Object[] {3}; + ReflectionHelper.convertArguments(tc, args, twoArg, 1); + checkArguments(args, "3"); + + // varargs with nothing needing conversion + args = new Object[] {3, "abc", "abc"}; + ReflectionHelper.convertArguments(tc, args, twoArg, 1); + checkArguments(args, "3", "abc", "abc"); + + // varargs with conversion required + args = new Object[] {3, false ,3.0d}; + ReflectionHelper.convertArguments(tc, args, twoArg, 1); + checkArguments(args, "3", "false", "3.0"); + } + + @Test + public void testConvertArguments2() throws Exception { + StandardTypeConverter tc = new StandardTypeConverter(); + Method oneArg = TestInterface.class.getMethod("oneArg", String.class); + Method twoArg = TestInterface.class.getMethod("twoArg", String.class, String[].class); + + // Simple conversion: int to string + Object[] args = new Object[] {3}; + ReflectionHelper.convertAllArguments(tc, args, oneArg); + checkArguments(args, "3"); + + // varargs conversion + args = new Object[] {3, false, 3.0f}; + ReflectionHelper.convertAllArguments(tc, args, twoArg); + checkArguments(args, "3", "false", "3.0"); + + // varargs conversion but no varargs + args = new Object[] {3}; + ReflectionHelper.convertAllArguments(tc, args, twoArg); + checkArguments(args, "3"); + + // null value + args = new Object[] {3, null, 3.0f}; + ReflectionHelper.convertAllArguments(tc, args, twoArg); + checkArguments(args, "3", null, "3.0"); + } + + @Test + public void testSetupArguments() { + Object[] newArray = ReflectionHelper.setupArgumentsForVarargsInvocation( + new Class[] {String[].class}, "a", "b", "c"); + + assertThat(newArray.length).isEqualTo(1); + Object firstParam = newArray[0]; + assertThat(firstParam.getClass().getComponentType()).isEqualTo(String.class); + Object[] firstParamArray = (Object[]) firstParam; + assertThat(firstParamArray.length).isEqualTo(3); + assertThat(firstParamArray[0]).isEqualTo("a"); + assertThat(firstParamArray[1]).isEqualTo("b"); + assertThat(firstParamArray[2]).isEqualTo("c"); + } + + @Test + public void testReflectivePropertyAccessor() throws Exception { + ReflectivePropertyAccessor rpa = new ReflectivePropertyAccessor(); + Tester t = new Tester(); + t.setProperty("hello"); + EvaluationContext ctx = new StandardEvaluationContext(t); + assertThat(rpa.canRead(ctx, t, "property")).isTrue(); + assertThat(rpa.read(ctx, t, "property").getValue()).isEqualTo("hello"); + // cached accessor used + assertThat(rpa.read(ctx, t, "property").getValue()).isEqualTo("hello"); + + assertThat(rpa.canRead(ctx, t, "field")).isTrue(); + assertThat(rpa.read(ctx, t, "field").getValue()).isEqualTo(3); + // cached accessor used + assertThat(rpa.read(ctx, t, "field").getValue()).isEqualTo(3); + + assertThat(rpa.canWrite(ctx, t, "property")).isTrue(); + rpa.write(ctx, t, "property", "goodbye"); + rpa.write(ctx, t, "property", "goodbye"); // cached accessor used + + assertThat(rpa.canWrite(ctx, t, "field")).isTrue(); + rpa.write(ctx, t, "field", 12); + rpa.write(ctx, t, "field", 12); + + // Attempted write as first activity on this field and property to drive testing + // of populating type descriptor cache + rpa.write(ctx, t, "field2", 3); + rpa.write(ctx, t, "property2", "doodoo"); + assertThat(rpa.read(ctx, t, "field2").getValue()).isEqualTo(3); + + // Attempted read as first activity on this field and property (no canRead before them) + assertThat(rpa.read(ctx, t, "field3").getValue()).isEqualTo(0); + assertThat(rpa.read(ctx, t, "property3").getValue()).isEqualTo("doodoo"); + + // Access through is method + assertThat(rpa .read(ctx, t, "field3").getValue()).isEqualTo(0); + assertThat(rpa.read(ctx, t, "property4").getValue()).isEqualTo(false); + assertThat(rpa.canRead(ctx, t, "property4")).isTrue(); + + // repro SPR-9123, ReflectivePropertyAccessor JavaBean property names compliance tests + assertThat(rpa.read(ctx, t, "iD").getValue()).isEqualTo("iD"); + assertThat(rpa.canRead(ctx, t, "iD")).isTrue(); + assertThat(rpa.read(ctx, t, "id").getValue()).isEqualTo("id"); + assertThat(rpa.canRead(ctx, t, "id")).isTrue(); + assertThat(rpa.read(ctx, t, "ID").getValue()).isEqualTo("ID"); + assertThat(rpa.canRead(ctx, t, "ID")).isTrue(); + // note: "Id" is not a valid JavaBean name, nevertheless it is treated as "id" + assertThat(rpa.read(ctx, t, "Id").getValue()).isEqualTo("id"); + assertThat(rpa.canRead(ctx, t, "Id")).isTrue(); + + // repro SPR-10994 + assertThat(rpa.read(ctx, t, "xyZ").getValue()).isEqualTo("xyZ"); + assertThat(rpa.canRead(ctx, t, "xyZ")).isTrue(); + assertThat(rpa.read(ctx, t, "xY").getValue()).isEqualTo("xY"); + assertThat(rpa.canRead(ctx, t, "xY")).isTrue(); + + // SPR-10122, ReflectivePropertyAccessor JavaBean property names compliance tests - setters + rpa.write(ctx, t, "pEBS", "Test String"); + assertThat(rpa.read(ctx, t, "pEBS").getValue()).isEqualTo("Test String"); + } + + @Test + public void testOptimalReflectivePropertyAccessor() throws Exception { + ReflectivePropertyAccessor reflective = new ReflectivePropertyAccessor(); + Tester tester = new Tester(); + tester.setProperty("hello"); + EvaluationContext ctx = new StandardEvaluationContext(tester); + assertThat(reflective.canRead(ctx, tester, "property")).isTrue(); + assertThat(reflective.read(ctx, tester, "property").getValue()).isEqualTo("hello"); + // cached accessor used + assertThat(reflective.read(ctx, tester, "property").getValue()).isEqualTo("hello"); + + PropertyAccessor property = reflective.createOptimalAccessor(ctx, tester, "property"); + assertThat(property.canRead(ctx, tester, "property")).isTrue(); + assertThat(property.canRead(ctx, tester, "property2")).isFalse(); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + property.canWrite(ctx, tester, "property")); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + property.canWrite(ctx, tester, "property2")); + assertThat(property.read(ctx, tester, "property").getValue()).isEqualTo("hello"); + // cached accessor used + assertThat(property.read(ctx, tester, "property").getValue()).isEqualTo("hello"); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(property::getSpecificTargetClasses); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + property.write(ctx, tester, "property", null)); + + PropertyAccessor field = reflective.createOptimalAccessor(ctx, tester, "field"); + assertThat(field.canRead(ctx, tester, "field")).isTrue(); + assertThat(field.canRead(ctx, tester, "field2")).isFalse(); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + field.canWrite(ctx, tester, "field")); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + field.canWrite(ctx, tester, "field2")); + assertThat(field.read(ctx, tester, "field").getValue()).isEqualTo(3); + // cached accessor used + assertThat(field.read(ctx, tester, "field").getValue()).isEqualTo(3); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(field::getSpecificTargetClasses); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + field.write(ctx, tester, "field", null)); + } + + + /** + * Used to validate the match returned from a compareArguments call. + */ + private void checkMatch(Class[] inputTypes, Class[] expectedTypes, StandardTypeConverter typeConverter, ArgumentsMatchKind expectedMatchKind) { + ReflectionHelper.ArgumentsMatchInfo matchInfo = ReflectionHelper.compareArguments(getTypeDescriptors(expectedTypes), getTypeDescriptors(inputTypes), typeConverter); + if (expectedMatchKind == null) { + assertThat(matchInfo).as("Did not expect them to match in any way").isNull(); + } + else { + assertThat(matchInfo).as("Should not be a null match").isNotNull(); + } + + if (expectedMatchKind == ArgumentsMatchKind.EXACT) { + assertThat(matchInfo.isExactMatch()).isTrue(); + } + else if (expectedMatchKind == ArgumentsMatchKind.CLOSE) { + assertThat(matchInfo.isCloseMatch()).isTrue(); + } + else if (expectedMatchKind == ArgumentsMatchKind.REQUIRES_CONVERSION) { + assertThat(matchInfo.isMatchRequiringConversion()).as("expected to be a match requiring conversion, but was " + matchInfo).isTrue(); + } + } + + /** + * Used to validate the match returned from a compareArguments call. + */ + private void checkMatch2(Class[] inputTypes, Class[] expectedTypes, StandardTypeConverter typeConverter, ArgumentsMatchKind expectedMatchKind) { + ReflectionHelper.ArgumentsMatchInfo matchInfo = ReflectionHelper.compareArgumentsVarargs(getTypeDescriptors(expectedTypes), getTypeDescriptors(inputTypes), typeConverter); + if (expectedMatchKind == null) { + assertThat(matchInfo).as("Did not expect them to match in any way: " + matchInfo).isNull(); + } + else { + assertThat(matchInfo).as("Should not be a null match").isNotNull(); + } + + if (expectedMatchKind == ArgumentsMatchKind.EXACT) { + assertThat(matchInfo.isExactMatch()).isTrue(); + } + else if (expectedMatchKind == ArgumentsMatchKind.CLOSE) { + assertThat(matchInfo.isCloseMatch()).isTrue(); + } + else if (expectedMatchKind == ArgumentsMatchKind.REQUIRES_CONVERSION) { + assertThat(matchInfo.isMatchRequiringConversion()).as("expected to be a match requiring conversion, but was " + matchInfo).isTrue(); + } + } + + private void checkArguments(Object[] args, Object... expected) { + assertThat(args.length).isEqualTo(expected.length); + for (int i = 0; i < expected.length; i++) { + checkArgument(expected[i],args[i]); + } + } + + private void checkArgument(Object expected, Object actual) { + assertThat(actual).isEqualTo(expected); + } + + private List getTypeDescriptors(Class... types) { + List typeDescriptors = new ArrayList<>(types.length); + for (Class type : types) { + typeDescriptors.add(TypeDescriptor.valueOf(type)); + } + return typeDescriptors; + } + + + public interface TestInterface { + + void oneArg(String arg1); + + void twoArg(String arg1, String... arg2); + } + + + static class Super { + } + + + static class Sub extends Super { + } + + + static class Unconvertable { + } + + + static class Tester { + + String property; + public int field = 3; + public int field2; + public int field3 = 0; + String property2; + String property3 = "doodoo"; + boolean property4 = false; + String iD = "iD"; + String id = "id"; + String ID = "ID"; + String pEBS = "pEBS"; + String xY = "xY"; + String xyZ = "xyZ"; + + public String getProperty() { return property; } + + public void setProperty(String value) { property = value; } + + public void setProperty2(String value) { property2 = value; } + + public String getProperty3() { return property3; } + + public boolean isProperty4() { return property4; } + + public String getiD() { return iD; } + + public String getId() { return id; } + + public String getID() { return ID; } + + public String getXY() { return xY; } + + public String getXyZ() { return xyZ; } + + public String getpEBS() { return pEBS; } + + public void setpEBS(String pEBS) { this.pEBS = pEBS; } + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/support/StandardComponentsTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/support/StandardComponentsTests.java new file mode 100644 index 0000000..62bd171 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/support/StandardComponentsTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.support; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Operation; +import org.springframework.expression.OperatorOverloader; +import org.springframework.expression.TypeComparator; +import org.springframework.expression.TypeConverter; +import org.springframework.expression.TypeLocator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +public class StandardComponentsTests { + + @Test + public void testStandardEvaluationContext() { + StandardEvaluationContext context = new StandardEvaluationContext(); + assertThat(context.getTypeComparator()).isNotNull(); + + TypeComparator tc = new StandardTypeComparator(); + context.setTypeComparator(tc); + assertThat(context.getTypeComparator()).isEqualTo(tc); + + TypeLocator tl = new StandardTypeLocator(); + context.setTypeLocator(tl); + assertThat(context.getTypeLocator()).isEqualTo(tl); + } + + @Test + public void testStandardOperatorOverloader() throws EvaluationException { + OperatorOverloader oo = new StandardOperatorOverloader(); + assertThat(oo.overridesOperation(Operation.ADD, null, null)).isFalse(); + assertThatExceptionOfType(EvaluationException.class).isThrownBy(() -> + oo.operate(Operation.ADD, 2, 3)); + } + + @Test + public void testStandardTypeLocator() { + StandardTypeLocator tl = new StandardTypeLocator(); + List prefixes = tl.getImportPrefixes(); + assertThat(prefixes.size()).isEqualTo(1); + tl.registerImport("java.util"); + prefixes = tl.getImportPrefixes(); + assertThat(prefixes.size()).isEqualTo(2); + tl.removeImport("java.util"); + prefixes = tl.getImportPrefixes(); + assertThat(prefixes.size()).isEqualTo(1); + } + + @Test + public void testStandardTypeConverter() throws EvaluationException { + TypeConverter tc = new StandardTypeConverter(); + tc.convertValue(3, TypeDescriptor.forObject(3), TypeDescriptor.valueOf(Double.class)); + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/testdata/PersonInOtherPackage.java b/spring-expression/src/test/java/org/springframework/expression/spel/testdata/PersonInOtherPackage.java new file mode 100644 index 0000000..4c1371d --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/testdata/PersonInOtherPackage.java @@ -0,0 +1,38 @@ +/* + * Copyright 2014-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.testdata; + +/** + * + * @author Andy Clement + * @since 4.1.2 + */ +public class PersonInOtherPackage { + + private int age; + + public PersonInOtherPackage(int age) { + this.age = age; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/testresources/ArrayContainer.java b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/ArrayContainer.java new file mode 100644 index 0000000..1e180f8 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/ArrayContainer.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.testresources; + +/** + * Hold the various kinds of primitive array for access through the test evaluation context. + * + * @author Andy Clement + */ +public class ArrayContainer { + + public int[] ints = new int[3]; + public long[] longs = new long[3]; + public double[] doubles = new double[3]; + public byte[] bytes = new byte[3]; + public char[] chars = new char[3]; + public short[] shorts = new short[3]; + public boolean[] booleans = new boolean[3]; + public float[] floats = new float[3]; + + public ArrayContainer() { + // setup some values + ints[0] = 42; + longs[0] = 42L; + doubles[0] = 42.0d; + bytes[0] = 42; + chars[0] = 42; + shorts[0] = 42; + booleans[0] = true; + floats[0] = 42.0f; + } +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Company.java b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Company.java new file mode 100644 index 0000000..fc11f45 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Company.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.testresources; + +///CLOVER:OFF +public class Company { + String address; + + public Company(String string) { + this.address = string; + } + + public String getAddress() { + return address; + } +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Fruit.java b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Fruit.java new file mode 100644 index 0000000..7fef936 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Fruit.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.testresources; + +import java.awt.Color; + +///CLOVER:OFF +public class Fruit { + public String name; // accessible as property field + public Color color; // accessible as property through getter/setter + public String colorName; // accessible as property through getter/setter + public int stringscount = -1; + + public Fruit(String name, Color color, String colorName) { + this.name = name; + this.color = color; + this.colorName = colorName; + } + + public Color getColor() { + return color; + } + + public Fruit(String... strings) { + stringscount = strings.length; + } + + public Fruit(int i, String... strings) { + stringscount = i + strings.length; + } + + public int stringscount() { + return stringscount; + } + + @Override + public String toString() { + return "A" + (colorName != null && colorName.startsWith("o") ? "n " : " ") + colorName + " " + name; + } +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Inventor.java b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Inventor.java new file mode 100644 index 0000000..bdf6d79 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Inventor.java @@ -0,0 +1,223 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.testresources; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.util.ObjectUtils; + +///CLOVER:OFF +@SuppressWarnings("unused") +public class Inventor { + private String name; + public String _name; + public String _name_; + public String publicName; + private PlaceOfBirth placeOfBirth; + private Date birthdate; + private int sinNumber; + private String nationality; + private String[] inventions; + public String randomField; + public Map testMap; + private boolean wonNobelPrize; + private PlaceOfBirth[] placesLived; + private List placesLivedList = new ArrayList<>(); + public ArrayContainer arrayContainer; + public boolean publicBoolean; + private boolean accessedThroughGetSet; + public List listOfInteger = new ArrayList<>(); + public List booleanList = new ArrayList<>(); + public Map mapOfStringToBoolean = new LinkedHashMap<>(); + public Map mapOfNumbersUpToTen = new LinkedHashMap<>(); + public List listOfNumbersUpToTen = new ArrayList<>(); + public List listOneFive = new ArrayList<>(); + public String[] stringArrayOfThreeItems = new String[]{"1","2","3"}; + private String foo; + public int counter; + + public Inventor(String name, Date birthdate, String nationality) { + this.name = name; + this._name = name; + this._name_ = name; + this.birthdate = birthdate; + this.nationality = nationality; + this.arrayContainer = new ArrayContainer(); + testMap = new HashMap<>(); + testMap.put("monday", "montag"); + testMap.put("tuesday", "dienstag"); + testMap.put("wednesday", "mittwoch"); + testMap.put("thursday", "donnerstag"); + testMap.put("friday", "freitag"); + testMap.put("saturday", "samstag"); + testMap.put("sunday", "sonntag"); + listOneFive.add(1); + listOneFive.add(5); + booleanList.add(false); + booleanList.add(false); + listOfNumbersUpToTen.add(1); + listOfNumbersUpToTen.add(2); + listOfNumbersUpToTen.add(3); + listOfNumbersUpToTen.add(4); + listOfNumbersUpToTen.add(5); + listOfNumbersUpToTen.add(6); + listOfNumbersUpToTen.add(7); + listOfNumbersUpToTen.add(8); + listOfNumbersUpToTen.add(9); + listOfNumbersUpToTen.add(10); + mapOfNumbersUpToTen.put(1,"one"); + mapOfNumbersUpToTen.put(2,"two"); + mapOfNumbersUpToTen.put(3,"three"); + mapOfNumbersUpToTen.put(4,"four"); + mapOfNumbersUpToTen.put(5,"five"); + mapOfNumbersUpToTen.put(6,"six"); + mapOfNumbersUpToTen.put(7,"seven"); + mapOfNumbersUpToTen.put(8,"eight"); + mapOfNumbersUpToTen.put(9,"nine"); + mapOfNumbersUpToTen.put(10,"ten"); + } + + public void setPlaceOfBirth(PlaceOfBirth placeOfBirth2) { + placeOfBirth = placeOfBirth2; + this.placesLived = new PlaceOfBirth[] { placeOfBirth2 }; + this.placesLivedList.add(placeOfBirth2); + } + + public String[] getInventions() { + return inventions; + } + + public void setInventions(String[] inventions) { + this.inventions = inventions; + } + + public PlaceOfBirth getPlaceOfBirth() { + return placeOfBirth; + } + + public int throwException(int valueIn) throws Exception { + counter++; + if (valueIn==1) { + throw new IllegalArgumentException("IllegalArgumentException for 1"); + } + if (valueIn==2) { + throw new RuntimeException("RuntimeException for 2"); + } + if (valueIn==4) { + throw new TestException(); + } + return valueIn; + } + + @SuppressWarnings("serial") + static class TestException extends Exception {} + + public String throwException(PlaceOfBirth pob) { + return pob.getCity(); + } + + public String getName() { + return name; + } + + public boolean getWonNobelPrize() { + return wonNobelPrize; + } + + public void setWonNobelPrize(boolean wonNobelPrize) { + this.wonNobelPrize = wonNobelPrize; + } + + public PlaceOfBirth[] getPlacesLived() { + return placesLived; + } + + public void setPlacesLived(PlaceOfBirth[] placesLived) { + this.placesLived = placesLived; + } + + public List getPlacesLivedList() { + return placesLivedList; + } + + public void setPlacesLivedList(List placesLivedList) { + this.placesLivedList = placesLivedList; + } + + public String echo(Object o) { + return o.toString(); + } + + public String sayHelloTo(String person) { + return "hello " + person; + } + + public String printDouble(Double d) { + return d.toString(); + } + + public String printDoubles(double[] d) { + return ObjectUtils.nullSafeToString(d); + } + + public List getDoublesAsStringList() { + List result = new ArrayList<>(); + result.add("14.35"); + result.add("15.45"); + return result; + } + + public String joinThreeStrings(String a, String b, String c) { + return a + b + c; + } + + public int aVarargsMethod(String... strings) { + if (strings == null) + return 0; + return strings.length; + } + + public int aVarargsMethod2(int i, String... strings) { + if (strings == null) + return i; + return strings.length + i; + } + + public Inventor(String... strings) { + + } + + public boolean getSomeProperty() { + return accessedThroughGetSet; + } + + public void setSomeProperty(boolean b) { + this.accessedThroughGetSet = b; + } + + public Date getBirthdate() { return birthdate;} + + public String getFoo() { return foo; } + public void setFoo(String s) { foo = s; } + + public String getNationality() { return nationality; } +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Person.java b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Person.java new file mode 100644 index 0000000..17939f7 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Person.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.testresources; + +public class Person { + + private String privateName; + + Company company; + + public Person(String name) { + this.privateName = name; + } + + public Person(String name, Company company) { + this.privateName = name; + this.company = company; + } + + public String getName() { + return privateName; + } + + public void setName(String n) { + this.privateName = n; + } + + public Company getCompany() { + return company; + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/testresources/PlaceOfBirth.java b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/PlaceOfBirth.java new file mode 100644 index 0000000..10c4acd --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/PlaceOfBirth.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.testresources; + +///CLOVER:OFF +public class PlaceOfBirth { + private String city; + + public String Country; + + /** + * Keith now has a converter that supports String to X, if X has a ctor that takes a String. + * In order for round tripping to work we need toString() for X to return what it was + * constructed with. This is a bit of a hack because a PlaceOfBirth also encapsulates a + * country - but as it is just a test object, it is ok. + */ + @Override + public String toString() {return city;} + + public String getCity() { + return city; + } + public void setCity(String s) { + this.city = s; + } + + public PlaceOfBirth(String string) { + this.city=string; + } + + public int doubleIt(int i) { + return i*2; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof PlaceOfBirth)) { + return false; + } + PlaceOfBirth oPOB = (PlaceOfBirth)o; + return (city.equals(oPOB.city)); + } + + @Override + public int hashCode() { + return city.hashCode(); + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/testresources/RecordPerson.java b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/RecordPerson.java new file mode 100644 index 0000000..f4f3518 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/RecordPerson.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.testresources; + +public class RecordPerson { + + private String name; + + private Company company; + + public RecordPerson(String name) { + this.name = name; + } + + public RecordPerson(String name, Company company) { + this.name = name; + this.company = company; + } + + public String name() { + return name; + } + + public Company company() { + return company; + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/testresources/TestAddress.java b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/TestAddress.java new file mode 100644 index 0000000..7beb414 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/TestAddress.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.testresources; + +import java.util.List; + +public class TestAddress{ + + private String street; + + private List crossStreets; + + public String getStreet() { + return street; + } + + public void setStreet(String street) { + this.street = street; + } + + public List getCrossStreets() { + return crossStreets; + } + + public void setCrossStreets(List crossStreets) { + this.crossStreets = crossStreets; + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/testresources/TestPerson.java b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/TestPerson.java new file mode 100644 index 0000000..e9470f2 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/TestPerson.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.testresources; + +public class TestPerson { + + private String name; + + private TestAddress address; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public TestAddress getAddress() { + return address; + } + + public void setAddress(TestAddress address) { + this.address = address; + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/testresources/le/div/mod/reserved/Reserver.java b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/le/div/mod/reserved/Reserver.java new file mode 100644 index 0000000..e88f9f2 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/le/div/mod/reserved/Reserver.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.expression.spel.testresources.le.div.mod.reserved; + +/** + * For use when testing that the SpEL expression parser can accommodate SpEL's own + * reserved words being used in package names. + * + * @author Phillip Webb + */ +public class Reserver { + + public static final String CONST = "Const"; + +} diff --git a/spring-expression/src/test/kotlin/org/springframework/expression/spel/KotlinSpelReproTests.kt b/spring-expression/src/test/kotlin/org/springframework/expression/spel/KotlinSpelReproTests.kt new file mode 100644 index 0000000..12a0197 --- /dev/null +++ b/spring-expression/src/test/kotlin/org/springframework/expression/spel/KotlinSpelReproTests.kt @@ -0,0 +1,28 @@ +package org.springframework.expression.spel + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.expression.ExpressionParser +import org.springframework.expression.spel.standard.SpelExpressionParser + +class KotlinSpelReproTests { + + private val parser: ExpressionParser = SpelExpressionParser() + + private val context = TestScenarioCreator.getTestEvaluationContext() + + + @Test + fun `gh-23812 SpEL cannot invoke Kotlin synthetic classes`() { + val expr = parser.parseExpression("new org.springframework.expression.spel.KotlinSpelReproTests\$Config().kotlinSupplier().invoke()") + assertThat(expr.getValue(context)).isEqualTo("test") + } + + class Config { + + fun kotlinSupplier(): () -> String { + return { "test" } + } + + } +} diff --git a/spring-expression/src/test/resources/log4j2-test.xml b/spring-expression/src/test/resources/log4j2-test.xml new file mode 100644 index 0000000..5c4984f --- /dev/null +++ b/spring-expression/src/test/resources/log4j2-test.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/spring-instrument/spring-instrument.gradle b/spring-instrument/spring-instrument.gradle new file mode 100644 index 0000000..f919051 --- /dev/null +++ b/spring-instrument/spring-instrument.gradle @@ -0,0 +1,11 @@ +description = "Spring Instrument" + +jar { + manifest.attributes["Premain-Class"] = + "org.springframework.instrument.InstrumentationSavingAgent" + manifest.attributes["Agent-Class"] = + "org.springframework.instrument.InstrumentationSavingAgent" + manifest.attributes["Can-Redefine-Classes"] = "true" + manifest.attributes["Can-Retransform-Classes"] = "true" + manifest.attributes["Can-Set-Native-Method-Prefix"] = "false" +} diff --git a/spring-instrument/src/main/java/org/springframework/instrument/InstrumentationSavingAgent.java b/spring-instrument/src/main/java/org/springframework/instrument/InstrumentationSavingAgent.java new file mode 100644 index 0000000..b693790 --- /dev/null +++ b/spring-instrument/src/main/java/org/springframework/instrument/InstrumentationSavingAgent.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.instrument; + +import java.lang.instrument.Instrumentation; + +/** + * Java agent that saves the {@link Instrumentation} interface from the JVM + * for later use. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver + */ +public final class InstrumentationSavingAgent { + + private static volatile Instrumentation instrumentation; + + + private InstrumentationSavingAgent() { + } + + + /** + * Save the {@link Instrumentation} interface exposed by the JVM. + */ + public static void premain(String agentArgs, Instrumentation inst) { + instrumentation = inst; + } + + /** + * Save the {@link Instrumentation} interface exposed by the JVM. + * This method is required to dynamically load this Agent with the Attach API. + */ + public static void agentmain(String agentArgs, Instrumentation inst) { + instrumentation = inst; + } + + /** + * Return the {@link Instrumentation} interface exposed by the JVM. + *

    Note that this agent class will typically not be available in the classpath + * unless the agent is actually specified on JVM startup. If you intend to do + * conditional checking with respect to agent availability, consider using + * {@link org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver#getInstrumentation()} + * instead - which will work without the agent class in the classpath as well. + * @return the {@code Instrumentation} instance previously saved when + * the {@link #premain} or {@link #agentmain} methods was called by the JVM; + * will be {@code null} if this class was not used as Java agent when this + * JVM was started or it wasn't installed as agent using the Attach API. + * @see org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver#getInstrumentation() + */ + public static Instrumentation getInstrumentation() { + return instrumentation; + } + +} diff --git a/spring-jcl/spring-jcl.gradle b/spring-jcl/spring-jcl.gradle new file mode 100644 index 0000000..d609737 --- /dev/null +++ b/spring-jcl/spring-jcl.gradle @@ -0,0 +1,6 @@ +description = "Spring Commons Logging Bridge" + +dependencies { + optional("org.apache.logging.log4j:log4j-api") + optional("org.slf4j:slf4j-api") +} diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/Log.java b/spring-jcl/src/main/java/org/apache/commons/logging/Log.java new file mode 100644 index 0000000..b69914a --- /dev/null +++ b/spring-jcl/src/main/java/org/apache/commons/logging/Log.java @@ -0,0 +1,196 @@ +/* + * 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 + * + * https://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.commons.logging; + +/** + * A simple logging interface abstracting logging APIs. In order to be + * instantiated successfully by {@link LogFactory}, classes that implement + * this interface must have a constructor that takes a single String + * parameter representing the "name" of this Log. + * + *

    The six logging levels used by Log are (in order): + *

      + *
    1. trace (the least serious)
    2. + *
    3. debug
    4. + *
    5. info
    6. + *
    7. warn
    8. + *
    9. error
    10. + *
    11. fatal (the most serious)
    12. + *
    + * + * The mapping of these log levels to the concepts used by the underlying + * logging system is implementation dependent. + * The implementation should ensure, though, that this ordering behaves + * as expected. + * + *

    Performance is often a logging concern. + * By examining the appropriate property, + * a component can avoid expensive operations (producing information + * to be logged). + * + *

    For example, + *

    + *    if (log.isDebugEnabled()) {
    + *        ... do something expensive ...
    + *        log.debug(theResult);
    + *    }
    + * 
    + * + *

    Configuration of the underlying logging system will generally be done + * external to the Logging APIs, through whatever mechanism is supported by + * that system. + * + * @author Juergen Hoeller (for the {@code spring-jcl} variant) + * @since 5.0 + */ +public interface Log { + + /** + * Is fatal logging currently enabled? + *

    Call this method to prevent having to perform expensive operations + * (for example, String concatenation) + * when the log level is more than fatal. + * @return true if fatal is enabled in the underlying logger. + */ + boolean isFatalEnabled(); + + /** + * Is error logging currently enabled? + *

    Call this method to prevent having to perform expensive operations + * (for example, String concatenation) + * when the log level is more than error. + * @return true if error is enabled in the underlying logger. + */ + boolean isErrorEnabled(); + + /** + * Is warn logging currently enabled? + *

    Call this method to prevent having to perform expensive operations + * (for example, String concatenation) + * when the log level is more than warn. + * @return true if warn is enabled in the underlying logger. + */ + boolean isWarnEnabled(); + + /** + * Is info logging currently enabled? + *

    Call this method to prevent having to perform expensive operations + * (for example, String concatenation) + * when the log level is more than info. + * @return true if info is enabled in the underlying logger. + */ + boolean isInfoEnabled(); + + /** + * Is debug logging currently enabled? + *

    Call this method to prevent having to perform expensive operations + * (for example, String concatenation) + * when the log level is more than debug. + * @return true if debug is enabled in the underlying logger. + */ + boolean isDebugEnabled(); + + /** + * Is trace logging currently enabled? + *

    Call this method to prevent having to perform expensive operations + * (for example, String concatenation) + * when the log level is more than trace. + * @return true if trace is enabled in the underlying logger. + */ + boolean isTraceEnabled(); + + + /** + * Logs a message with fatal log level. + * @param message log this message + */ + void fatal(Object message); + + /** + * Logs an error with fatal log level. + * @param message log this message + * @param t log this cause + */ + void fatal(Object message, Throwable t); + + /** + * Logs a message with error log level. + * @param message log this message + */ + void error(Object message); + + /** + * Logs an error with error log level. + * @param message log this message + * @param t log this cause + */ + void error(Object message, Throwable t); + + /** + * Logs a message with warn log level. + * @param message log this message + */ + void warn(Object message); + + /** + * Logs an error with warn log level. + * @param message log this message + * @param t log this cause + */ + void warn(Object message, Throwable t); + + /** + * Logs a message with info log level. + * @param message log this message + */ + void info(Object message); + + /** + * Logs an error with info log level. + * @param message log this message + * @param t log this cause + */ + void info(Object message, Throwable t); + + /** + * Logs a message with debug log level. + * @param message log this message + */ + void debug(Object message); + + /** + * Logs an error with debug log level. + * @param message log this message + * @param t log this cause + */ + void debug(Object message, Throwable t); + + /** + * Logs a message with trace log level. + * @param message log this message + */ + void trace(Object message); + + /** + * Logs an error with trace log level. + * @param message log this message + * @param t log this cause + */ + void trace(Object message, Throwable t); + +} diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/LogAdapter.java b/spring-jcl/src/main/java/org/apache/commons/logging/LogAdapter.java new file mode 100644 index 0000000..c918ee7 --- /dev/null +++ b/spring-jcl/src/main/java/org/apache/commons/logging/LogAdapter.java @@ -0,0 +1,701 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.commons.logging; + +import java.io.Serializable; +import java.util.logging.LogRecord; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.spi.ExtendedLogger; +import org.apache.logging.log4j.spi.LoggerContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.spi.LocationAwareLogger; + +/** + * Spring's common JCL adapter behind {@link LogFactory} and {@link LogFactoryService}. + * Detects the presence of Log4j 2.x / SLF4J, falling back to {@code java.util.logging}. + * + * @author Juergen Hoeller + * @since 5.1 + */ +final class LogAdapter { + + private static final String LOG4J_SPI = "org.apache.logging.log4j.spi.ExtendedLogger"; + + private static final String LOG4J_SLF4J_PROVIDER = "org.apache.logging.slf4j.SLF4JProvider"; + + private static final String SLF4J_SPI = "org.slf4j.spi.LocationAwareLogger"; + + private static final String SLF4J_API = "org.slf4j.Logger"; + + + private static final LogApi logApi; + + static { + if (isPresent(LOG4J_SPI)) { + if (isPresent(LOG4J_SLF4J_PROVIDER) && isPresent(SLF4J_SPI)) { + // log4j-to-slf4j bridge -> we'll rather go with the SLF4J SPI; + // however, we still prefer Log4j over the plain SLF4J API since + // the latter does not have location awareness support. + logApi = LogApi.SLF4J_LAL; + } + else { + // Use Log4j 2.x directly, including location awareness support + logApi = LogApi.LOG4J; + } + } + else if (isPresent(SLF4J_SPI)) { + // Full SLF4J SPI including location awareness support + logApi = LogApi.SLF4J_LAL; + } + else if (isPresent(SLF4J_API)) { + // Minimal SLF4J API without location awareness support + logApi = LogApi.SLF4J; + } + else { + // java.util.logging as default + logApi = LogApi.JUL; + } + } + + + private LogAdapter() { + } + + + /** + * Create an actual {@link Log} instance for the selected API. + * @param name the logger name + */ + public static Log createLog(String name) { + switch (logApi) { + case LOG4J: + return Log4jAdapter.createLog(name); + case SLF4J_LAL: + return Slf4jAdapter.createLocationAwareLog(name); + case SLF4J: + return Slf4jAdapter.createLog(name); + default: + // Defensively use lazy-initializing adapter class here as well since the + // java.logging module is not present by default on JDK 9. We are requiring + // its presence if neither Log4j nor SLF4J is available; however, in the + // case of Log4j or SLF4J, we are trying to prevent early initialization + // of the JavaUtilLog adapter - e.g. by a JVM in debug mode - when eagerly + // trying to parse the bytecode for all the cases of this switch clause. + return JavaUtilAdapter.createLog(name); + } + } + + private static boolean isPresent(String className) { + try { + Class.forName(className, false, LogAdapter.class.getClassLoader()); + return true; + } + catch (ClassNotFoundException ex) { + return false; + } + } + + + private enum LogApi {LOG4J, SLF4J_LAL, SLF4J, JUL} + + + private static class Log4jAdapter { + + public static Log createLog(String name) { + return new Log4jLog(name); + } + } + + + private static class Slf4jAdapter { + + public static Log createLocationAwareLog(String name) { + Logger logger = LoggerFactory.getLogger(name); + return (logger instanceof LocationAwareLogger ? + new Slf4jLocationAwareLog((LocationAwareLogger) logger) : new Slf4jLog<>(logger)); + } + + public static Log createLog(String name) { + return new Slf4jLog<>(LoggerFactory.getLogger(name)); + } + } + + + private static class JavaUtilAdapter { + + public static Log createLog(String name) { + return new JavaUtilLog(name); + } + } + + + @SuppressWarnings("serial") + private static class Log4jLog implements Log, Serializable { + + private static final String FQCN = Log4jLog.class.getName(); + + private static final LoggerContext loggerContext = + LogManager.getContext(Log4jLog.class.getClassLoader(), false); + + private final ExtendedLogger logger; + + public Log4jLog(String name) { + LoggerContext context = loggerContext; + if (context == null) { + // Circular call in early-init scenario -> static field not initialized yet + context = LogManager.getContext(Log4jLog.class.getClassLoader(), false); + } + this.logger = context.getLogger(name); + } + + @Override + public boolean isFatalEnabled() { + return this.logger.isEnabled(Level.FATAL); + } + + @Override + public boolean isErrorEnabled() { + return this.logger.isEnabled(Level.ERROR); + } + + @Override + public boolean isWarnEnabled() { + return this.logger.isEnabled(Level.WARN); + } + + @Override + public boolean isInfoEnabled() { + return this.logger.isEnabled(Level.INFO); + } + + @Override + public boolean isDebugEnabled() { + return this.logger.isEnabled(Level.DEBUG); + } + + @Override + public boolean isTraceEnabled() { + return this.logger.isEnabled(Level.TRACE); + } + + @Override + public void fatal(Object message) { + log(Level.FATAL, message, null); + } + + @Override + public void fatal(Object message, Throwable exception) { + log(Level.FATAL, message, exception); + } + + @Override + public void error(Object message) { + log(Level.ERROR, message, null); + } + + @Override + public void error(Object message, Throwable exception) { + log(Level.ERROR, message, exception); + } + + @Override + public void warn(Object message) { + log(Level.WARN, message, null); + } + + @Override + public void warn(Object message, Throwable exception) { + log(Level.WARN, message, exception); + } + + @Override + public void info(Object message) { + log(Level.INFO, message, null); + } + + @Override + public void info(Object message, Throwable exception) { + log(Level.INFO, message, exception); + } + + @Override + public void debug(Object message) { + log(Level.DEBUG, message, null); + } + + @Override + public void debug(Object message, Throwable exception) { + log(Level.DEBUG, message, exception); + } + + @Override + public void trace(Object message) { + log(Level.TRACE, message, null); + } + + @Override + public void trace(Object message, Throwable exception) { + log(Level.TRACE, message, exception); + } + + private void log(Level level, Object message, Throwable exception) { + if (message instanceof String) { + // Explicitly pass a String argument, avoiding Log4j's argument expansion + // for message objects in case of "{}" sequences (SPR-16226) + if (exception != null) { + this.logger.logIfEnabled(FQCN, level, null, (String) message, exception); + } + else { + this.logger.logIfEnabled(FQCN, level, null, (String) message); + } + } + else { + this.logger.logIfEnabled(FQCN, level, null, message, exception); + } + } + } + + + @SuppressWarnings("serial") + private static class Slf4jLog implements Log, Serializable { + + protected final String name; + + protected transient T logger; + + public Slf4jLog(T logger) { + this.name = logger.getName(); + this.logger = logger; + } + + @Override + public boolean isFatalEnabled() { + return isErrorEnabled(); + } + + @Override + public boolean isErrorEnabled() { + return this.logger.isErrorEnabled(); + } + + @Override + public boolean isWarnEnabled() { + return this.logger.isWarnEnabled(); + } + + @Override + public boolean isInfoEnabled() { + return this.logger.isInfoEnabled(); + } + + @Override + public boolean isDebugEnabled() { + return this.logger.isDebugEnabled(); + } + + @Override + public boolean isTraceEnabled() { + return this.logger.isTraceEnabled(); + } + + @Override + public void fatal(Object message) { + error(message); + } + + @Override + public void fatal(Object message, Throwable exception) { + error(message, exception); + } + + @Override + public void error(Object message) { + if (message instanceof String || this.logger.isErrorEnabled()) { + this.logger.error(String.valueOf(message)); + } + } + + @Override + public void error(Object message, Throwable exception) { + if (message instanceof String || this.logger.isErrorEnabled()) { + this.logger.error(String.valueOf(message), exception); + } + } + + @Override + public void warn(Object message) { + if (message instanceof String || this.logger.isWarnEnabled()) { + this.logger.warn(String.valueOf(message)); + } + } + + @Override + public void warn(Object message, Throwable exception) { + if (message instanceof String || this.logger.isWarnEnabled()) { + this.logger.warn(String.valueOf(message), exception); + } + } + + @Override + public void info(Object message) { + if (message instanceof String || this.logger.isInfoEnabled()) { + this.logger.info(String.valueOf(message)); + } + } + + @Override + public void info(Object message, Throwable exception) { + if (message instanceof String || this.logger.isInfoEnabled()) { + this.logger.info(String.valueOf(message), exception); + } + } + + @Override + public void debug(Object message) { + if (message instanceof String || this.logger.isDebugEnabled()) { + this.logger.debug(String.valueOf(message)); + } + } + + @Override + public void debug(Object message, Throwable exception) { + if (message instanceof String || this.logger.isDebugEnabled()) { + this.logger.debug(String.valueOf(message), exception); + } + } + + @Override + public void trace(Object message) { + if (message instanceof String || this.logger.isTraceEnabled()) { + this.logger.trace(String.valueOf(message)); + } + } + + @Override + public void trace(Object message, Throwable exception) { + if (message instanceof String || this.logger.isTraceEnabled()) { + this.logger.trace(String.valueOf(message), exception); + } + } + + protected Object readResolve() { + return Slf4jAdapter.createLog(this.name); + } + } + + + @SuppressWarnings("serial") + private static class Slf4jLocationAwareLog extends Slf4jLog implements Serializable { + + private static final String FQCN = Slf4jLocationAwareLog.class.getName(); + + public Slf4jLocationAwareLog(LocationAwareLogger logger) { + super(logger); + } + + @Override + public void fatal(Object message) { + error(message); + } + + @Override + public void fatal(Object message, Throwable exception) { + error(message, exception); + } + + @Override + public void error(Object message) { + if (message instanceof String || this.logger.isErrorEnabled()) { + this.logger.log(null, FQCN, LocationAwareLogger.ERROR_INT, String.valueOf(message), null, null); + } + } + + @Override + public void error(Object message, Throwable exception) { + if (message instanceof String || this.logger.isErrorEnabled()) { + this.logger.log(null, FQCN, LocationAwareLogger.ERROR_INT, String.valueOf(message), null, exception); + } + } + + @Override + public void warn(Object message) { + if (message instanceof String || this.logger.isWarnEnabled()) { + this.logger.log(null, FQCN, LocationAwareLogger.WARN_INT, String.valueOf(message), null, null); + } + } + + @Override + public void warn(Object message, Throwable exception) { + if (message instanceof String || this.logger.isWarnEnabled()) { + this.logger.log(null, FQCN, LocationAwareLogger.WARN_INT, String.valueOf(message), null, exception); + } + } + + @Override + public void info(Object message) { + if (message instanceof String || this.logger.isInfoEnabled()) { + this.logger.log(null, FQCN, LocationAwareLogger.INFO_INT, String.valueOf(message), null, null); + } + } + + @Override + public void info(Object message, Throwable exception) { + if (message instanceof String || this.logger.isInfoEnabled()) { + this.logger.log(null, FQCN, LocationAwareLogger.INFO_INT, String.valueOf(message), null, exception); + } + } + + @Override + public void debug(Object message) { + if (message instanceof String || this.logger.isDebugEnabled()) { + this.logger.log(null, FQCN, LocationAwareLogger.DEBUG_INT, String.valueOf(message), null, null); + } + } + + @Override + public void debug(Object message, Throwable exception) { + if (message instanceof String || this.logger.isDebugEnabled()) { + this.logger.log(null, FQCN, LocationAwareLogger.DEBUG_INT, String.valueOf(message), null, exception); + } + } + + @Override + public void trace(Object message) { + if (message instanceof String || this.logger.isTraceEnabled()) { + this.logger.log(null, FQCN, LocationAwareLogger.TRACE_INT, String.valueOf(message), null, null); + } + } + + @Override + public void trace(Object message, Throwable exception) { + if (message instanceof String || this.logger.isTraceEnabled()) { + this.logger.log(null, FQCN, LocationAwareLogger.TRACE_INT, String.valueOf(message), null, exception); + } + } + + @Override + protected Object readResolve() { + return Slf4jAdapter.createLocationAwareLog(this.name); + } + } + + + @SuppressWarnings("serial") + private static class JavaUtilLog implements Log, Serializable { + + private String name; + + private transient java.util.logging.Logger logger; + + public JavaUtilLog(String name) { + this.name = name; + this.logger = java.util.logging.Logger.getLogger(name); + } + + @Override + public boolean isFatalEnabled() { + return isErrorEnabled(); + } + + @Override + public boolean isErrorEnabled() { + return this.logger.isLoggable(java.util.logging.Level.SEVERE); + } + + @Override + public boolean isWarnEnabled() { + return this.logger.isLoggable(java.util.logging.Level.WARNING); + } + + @Override + public boolean isInfoEnabled() { + return this.logger.isLoggable(java.util.logging.Level.INFO); + } + + @Override + public boolean isDebugEnabled() { + return this.logger.isLoggable(java.util.logging.Level.FINE); + } + + @Override + public boolean isTraceEnabled() { + return this.logger.isLoggable(java.util.logging.Level.FINEST); + } + + @Override + public void fatal(Object message) { + error(message); + } + + @Override + public void fatal(Object message, Throwable exception) { + error(message, exception); + } + + @Override + public void error(Object message) { + log(java.util.logging.Level.SEVERE, message, null); + } + + @Override + public void error(Object message, Throwable exception) { + log(java.util.logging.Level.SEVERE, message, exception); + } + + @Override + public void warn(Object message) { + log(java.util.logging.Level.WARNING, message, null); + } + + @Override + public void warn(Object message, Throwable exception) { + log(java.util.logging.Level.WARNING, message, exception); + } + + @Override + public void info(Object message) { + log(java.util.logging.Level.INFO, message, null); + } + + @Override + public void info(Object message, Throwable exception) { + log(java.util.logging.Level.INFO, message, exception); + } + + @Override + public void debug(Object message) { + log(java.util.logging.Level.FINE, message, null); + } + + @Override + public void debug(Object message, Throwable exception) { + log(java.util.logging.Level.FINE, message, exception); + } + + @Override + public void trace(Object message) { + log(java.util.logging.Level.FINEST, message, null); + } + + @Override + public void trace(Object message, Throwable exception) { + log(java.util.logging.Level.FINEST, message, exception); + } + + private void log(java.util.logging.Level level, Object message, Throwable exception) { + if (this.logger.isLoggable(level)) { + LogRecord rec; + if (message instanceof LogRecord) { + rec = (LogRecord) message; + } + else { + rec = new LocationResolvingLogRecord(level, String.valueOf(message)); + rec.setLoggerName(this.name); + rec.setResourceBundleName(this.logger.getResourceBundleName()); + rec.setResourceBundle(this.logger.getResourceBundle()); + rec.setThrown(exception); + } + logger.log(rec); + } + } + + protected Object readResolve() { + return new JavaUtilLog(this.name); + } + } + + + @SuppressWarnings("serial") + private static class LocationResolvingLogRecord extends LogRecord { + + private static final String FQCN = JavaUtilLog.class.getName(); + + private volatile boolean resolved; + + public LocationResolvingLogRecord(java.util.logging.Level level, String msg) { + super(level, msg); + } + + @Override + public String getSourceClassName() { + if (!this.resolved) { + resolve(); + } + return super.getSourceClassName(); + } + + @Override + public void setSourceClassName(String sourceClassName) { + super.setSourceClassName(sourceClassName); + this.resolved = true; + } + + @Override + public String getSourceMethodName() { + if (!this.resolved) { + resolve(); + } + return super.getSourceMethodName(); + } + + @Override + public void setSourceMethodName(String sourceMethodName) { + super.setSourceMethodName(sourceMethodName); + this.resolved = true; + } + + private void resolve() { + StackTraceElement[] stack = new Throwable().getStackTrace(); + String sourceClassName = null; + String sourceMethodName = null; + boolean found = false; + for (StackTraceElement element : stack) { + String className = element.getClassName(); + if (FQCN.equals(className)) { + found = true; + } + else if (found) { + sourceClassName = className; + sourceMethodName = element.getMethodName(); + break; + } + } + setSourceClassName(sourceClassName); + setSourceMethodName(sourceMethodName); + } + + @SuppressWarnings("deprecation") // setMillis is deprecated in JDK 9 + protected Object writeReplace() { + LogRecord serialized = new LogRecord(getLevel(), getMessage()); + serialized.setLoggerName(getLoggerName()); + serialized.setResourceBundle(getResourceBundle()); + serialized.setResourceBundleName(getResourceBundleName()); + serialized.setSourceClassName(getSourceClassName()); + serialized.setSourceMethodName(getSourceMethodName()); + serialized.setSequenceNumber(getSequenceNumber()); + serialized.setParameters(getParameters()); + serialized.setThreadID(getThreadID()); + serialized.setMillis(getMillis()); + serialized.setThrown(getThrown()); + return serialized; + } + } + +} diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/LogFactory.java b/spring-jcl/src/main/java/org/apache/commons/logging/LogFactory.java new file mode 100644 index 0000000..0600afc --- /dev/null +++ b/spring-jcl/src/main/java/org/apache/commons/logging/LogFactory.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.commons.logging; + +/** + * A minimal incarnation of Apache Commons Logging's {@code LogFactory} API, + * providing just the common {@link Log} lookup methods. This is inspired + * by the JCL-over-SLF4J bridge and should be source as well as binary + * compatible with all common use of the Commons Logging API (in particular: + * with {@code LogFactory.getLog(Class/String)} field initializers). + * + *

    This implementation does not support Commons Logging's original provider + * detection. It rather only checks for the presence of the Log4j 2.x API + * and the SLF4J 1.7 API in the Spring Framework classpath, falling back to + * {@code java.util.logging} if none of the two is available. In that sense, + * it works as a replacement for the Log4j 2 Commons Logging bridge as well as + * the JCL-over-SLF4J bridge, both of which become irrelevant for Spring-based + * setups as a consequence (with no need for manual excludes of the standard + * Commons Logging API jar anymore either). Furthermore, for simple setups + * without an external logging provider, Spring does not require any extra jar + * on the classpath anymore since this embedded log factory automatically + * delegates to {@code java.util.logging} in such a scenario. + * + *

    Note that this Commons Logging variant is only meant to be used for + * infrastructure logging purposes in the core framework and in extensions. + * It also serves as a common bridge for third-party libraries using the + * Commons Logging API, e.g. Apache HttpClient, and HtmlUnit, bringing + * them into the same consistent arrangement without any extra bridge jars. + * + *

    For logging need in application code, prefer direct use of Log4j 2.x + * or SLF4J or {@code java.util.logging}. Simply put Log4j 2.x or Logback + * (or another SLF4J provider) onto your classpath, without any extra bridges, + * and let the framework auto-adapt to your choice. + * + * @author Juergen Hoeller (for the {@code spring-jcl} variant) + * @since 5.0 + */ +public abstract class LogFactory { + + /** + * Convenience method to return a named logger. + * @param clazz containing Class from which a log name will be derived + */ + public static Log getLog(Class clazz) { + return getLog(clazz.getName()); + } + + /** + * Convenience method to return a named logger. + * @param name logical name of the Log instance to be returned + */ + public static Log getLog(String name) { + return LogAdapter.createLog(name); + } + + + /** + * This method only exists for compatibility with unusual Commons Logging API + * usage like e.g. {@code LogFactory.getFactory().getInstance(Class/String)}. + * @see #getInstance(Class) + * @see #getInstance(String) + * @deprecated in favor of {@link #getLog(Class)}/{@link #getLog(String)} + */ + @Deprecated + public static LogFactory getFactory() { + return new LogFactory() {}; + } + + /** + * Convenience method to return a named logger. + *

    This variant just dispatches straight to {@link #getLog(Class)}. + * @param clazz containing Class from which a log name will be derived + * @deprecated in favor of {@link #getLog(Class)} + */ + @Deprecated + public Log getInstance(Class clazz) { + return getLog(clazz); + } + + /** + * Convenience method to return a named logger. + *

    This variant just dispatches straight to {@link #getLog(String)}. + * @param name logical name of the Log instance to be returned + * @deprecated in favor of {@link #getLog(String)} + */ + @Deprecated + public Log getInstance(String name) { + return getLog(name); + } + +} diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/LogFactoryService.java b/spring-jcl/src/main/java/org/apache/commons/logging/LogFactoryService.java new file mode 100644 index 0000000..ef82eb9 --- /dev/null +++ b/spring-jcl/src/main/java/org/apache/commons/logging/LogFactoryService.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.commons.logging; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A minimal subclass of the standard Apache Commons Logging's {@code LogFactory} class, + * overriding the abstract {@code getInstance} lookup methods. This is just applied in + * case of the standard {@code commons-logging} jar accidentally ending up on the classpath, + * with the standard {@code LogFactory} class performing its META-INF service discovery. + * This implementation simply delegates to Spring's common {@link Log} factory methods. + * + * @author Juergen Hoeller + * @since 5.1 + * @deprecated since it is only meant to be used in the above-mentioned fallback scenario + */ +@Deprecated +public class LogFactoryService extends LogFactory { + + private final Map attributes = new ConcurrentHashMap<>(); + + + @Override + public Log getInstance(Class clazz) { + return getInstance(clazz.getName()); + } + + @Override + public Log getInstance(String name) { + return LogAdapter.createLog(name); + } + + + // Just in case some code happens to call uncommon Commons Logging methods... + + public void setAttribute(String name, Object value) { + if (value != null) { + this.attributes.put(name, value); + } + else { + this.attributes.remove(name); + } + } + + public void removeAttribute(String name) { + this.attributes.remove(name); + } + + public Object getAttribute(String name) { + return this.attributes.get(name); + } + + public String[] getAttributeNames() { + return this.attributes.keySet().toArray(new String[0]); + } + + public void release() { + } + +} diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/impl/NoOpLog.java b/spring-jcl/src/main/java/org/apache/commons/logging/impl/NoOpLog.java new file mode 100644 index 0000000..5c0361d --- /dev/null +++ b/spring-jcl/src/main/java/org/apache/commons/logging/impl/NoOpLog.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.commons.logging.impl; + +import java.io.Serializable; + +import org.apache.commons.logging.Log; + +/** + * Trivial implementation of {@link Log} that throws away all messages. + * + * @author Juergen Hoeller (for the {@code spring-jcl} variant) + * @since 5.0 + */ +@SuppressWarnings("serial") +public class NoOpLog implements Log, Serializable { + + public NoOpLog() { + } + + public NoOpLog(String name) { + } + + + @Override + public boolean isFatalEnabled() { + return false; + } + + @Override + public boolean isErrorEnabled() { + return false; + } + + @Override + public boolean isWarnEnabled() { + return false; + } + + @Override + public boolean isInfoEnabled() { + return false; + } + + @Override + public boolean isDebugEnabled() { + return false; + } + + @Override + public boolean isTraceEnabled() { + return false; + } + + @Override + public void fatal(Object message) { + } + + @Override + public void fatal(Object message, Throwable t) { + } + + @Override + public void error(Object message) { + } + + @Override + public void error(Object message, Throwable t) { + } + + @Override + public void warn(Object message) { + } + + @Override + public void warn(Object message, Throwable t) { + } + + @Override + public void info(Object message) { + } + + @Override + public void info(Object message, Throwable t) { + } + + @Override + public void debug(Object message) { + } + + @Override + public void debug(Object message, Throwable t) { + } + + @Override + public void trace(Object message) { + } + + @Override + public void trace(Object message, Throwable t) { + } + +} diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/impl/SimpleLog.java b/spring-jcl/src/main/java/org/apache/commons/logging/impl/SimpleLog.java new file mode 100644 index 0000000..3dbcbaf --- /dev/null +++ b/spring-jcl/src/main/java/org/apache/commons/logging/impl/SimpleLog.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.commons.logging.impl; + +/** + * Originally a simple Commons Logging provider configured by system properties. + * Deprecated in {@code spring-jcl}, effectively equivalent to {@link NoOpLog}. + * + *

    Instead of instantiating this directly, call {@code LogFactory#getLog(Class/String)} + * which will fall back to {@code java.util.logging} if neither Log4j nor SLF4J are present. + * + * @author Juergen Hoeller (for the {@code spring-jcl} variant) + * @since 5.0 + * @deprecated in {@code spring-jcl} (effectively equivalent to {@link NoOpLog}) + */ +@Deprecated +@SuppressWarnings("serial") +public class SimpleLog extends NoOpLog { + + public SimpleLog(String name) { + super(name); + System.out.println(SimpleLog.class.getName() + " is deprecated and equivalent to NoOpLog in spring-jcl. " + + "Use a standard LogFactory.getLog(Class/String) call instead."); + } + +} diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/impl/package-info.java b/spring-jcl/src/main/java/org/apache/commons/logging/impl/package-info.java new file mode 100644 index 0000000..b06abd6 --- /dev/null +++ b/spring-jcl/src/main/java/org/apache/commons/logging/impl/package-info.java @@ -0,0 +1,11 @@ +/** + * Spring's variant of the + * Commons Logging API: + * with special support for Log4J 2, SLF4J and {@code java.util.logging}. + * + *

    This {@code impl} package is only present for binary compatibility + * with existing Commons Logging usage, e.g. in Commons Configuration. + * {@code NoOpLog} can be used as a {@code Log} fallback instance, and + * {@code SimpleLog} is not meant to work (issuing a warning when used). + */ +package org.apache.commons.logging.impl; diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/package-info.java b/spring-jcl/src/main/java/org/apache/commons/logging/package-info.java new file mode 100644 index 0000000..cbf63ed --- /dev/null +++ b/spring-jcl/src/main/java/org/apache/commons/logging/package-info.java @@ -0,0 +1,25 @@ +/** + * Spring's variant of the + * Commons Logging API: + * with special support for Log4J 2, SLF4J and {@code java.util.logging}. + * + *

    This is a custom bridge along the lines of {@code jcl-over-slf4j}. + * You may exclude {@code spring-jcl} and switch to {@code jcl-over-slf4j} + * instead if you prefer the hard-bound SLF4J bridge. However, Spring's own + * bridge provides a better out-of-the-box experience when using Log4J 2 + * or {@code java.util.logging}, with no extra bridge jars necessary, and + * also easier setup of SLF4J with Logback (no JCL exclude, no JCL bridge). + * + *

    {@link org.apache.commons.logging.Log} is equivalent to the original. + * However, {@link org.apache.commons.logging.LogFactory} is a very different + * implementation which is minimized and optimized for Spring's purposes, + * detecting Log4J 2.x and SLF4J 1.7 in the framework classpath and falling + * back to {@code java.util.logging}. If you run into any issues with this + * implementation, consider excluding {@code spring-jcl} and switching to the + * standard {@code commons-logging} artifact or to {@code jcl-over-slf4j}. + * + *

    Note that this Commons Logging bridge is only meant to be used for + * framework logging purposes, both in the core framework and in extensions. + * For applications, prefer direct use of Log4J/SLF4J or {@code java.util.logging}. + */ +package org.apache.commons.logging; diff --git a/spring-jdbc/spring-jdbc.gradle b/spring-jdbc/spring-jdbc.gradle new file mode 100644 index 0000000..e0737be --- /dev/null +++ b/spring-jdbc/spring-jdbc.gradle @@ -0,0 +1,19 @@ +description = "Spring JDBC" + +apply plugin: "kotlin" + +dependencies { + compile(project(":spring-beans")) + compile(project(":spring-core")) + compile(project(":spring-tx")) + optional(project(":spring-context")) // for JndiDataSourceLookup + optional("javax.transaction:javax.transaction-api") + optional("org.hsqldb:hsqldb") + optional("com.h2database:h2") + optional("org.apache.derby:derby") + optional("org.apache.derby:derbyclient") + optional("org.jetbrains.kotlin:kotlin-reflect") + optional("org.jetbrains.kotlin:kotlin-stdlib") + testCompile(testFixtures(project(":spring-beans"))) + testCompile(testFixtures(project(":spring-core"))) +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/BadSqlGrammarException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/BadSqlGrammarException.java new file mode 100644 index 0000000..665cfa8 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/BadSqlGrammarException.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc; + +import java.sql.SQLException; + +import org.springframework.dao.InvalidDataAccessResourceUsageException; + +/** + * Exception thrown when SQL specified is invalid. Such exceptions always have + * a {@code java.sql.SQLException} root cause. + * + *

    It would be possible to have subclasses for no such table, no such column etc. + * A custom SQLExceptionTranslator could create such more specific exceptions, + * without affecting code using this class. + * + * @author Rod Johnson + * @see InvalidResultSetAccessException + */ +@SuppressWarnings("serial") +public class BadSqlGrammarException extends InvalidDataAccessResourceUsageException { + + private final String sql; + + + /** + * Constructor for BadSqlGrammarException. + * @param task name of current task + * @param sql the offending SQL statement + * @param ex the root cause + */ + public BadSqlGrammarException(String task, String sql, SQLException ex) { + super(task + "; bad SQL grammar [" + sql + "]", ex); + this.sql = sql; + } + + + /** + * Return the wrapped SQLException. + */ + public SQLException getSQLException() { + return (SQLException) getCause(); + } + + /** + * Return the SQL that caused the problem. + */ + public String getSql() { + return this.sql; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/CannotGetJdbcConnectionException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/CannotGetJdbcConnectionException.java new file mode 100644 index 0000000..bbac56f --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/CannotGetJdbcConnectionException.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc; + +import java.sql.SQLException; + +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.lang.Nullable; + +/** + * Fatal exception thrown when we can't connect to an RDBMS using JDBC. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +@SuppressWarnings("serial") +public class CannotGetJdbcConnectionException extends DataAccessResourceFailureException { + + /** + * Constructor for CannotGetJdbcConnectionException. + * @param msg the detail message + * @since 5.0 + */ + public CannotGetJdbcConnectionException(String msg) { + super(msg); + } + + /** + * Constructor for CannotGetJdbcConnectionException. + * @param msg the detail message + * @param ex the root cause SQLException + */ + public CannotGetJdbcConnectionException(String msg, @Nullable SQLException ex) { + super(msg, ex); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/IncorrectResultSetColumnCountException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/IncorrectResultSetColumnCountException.java new file mode 100644 index 0000000..3cc779b --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/IncorrectResultSetColumnCountException.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc; + +import org.springframework.dao.DataRetrievalFailureException; + +/** + * Data access exception thrown when a result set did not have the correct column count, + * for example when expecting a single column but getting 0 or more than 1 columns. + * + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.dao.IncorrectResultSizeDataAccessException + */ +@SuppressWarnings("serial") +public class IncorrectResultSetColumnCountException extends DataRetrievalFailureException { + + private final int expectedCount; + + private final int actualCount; + + + /** + * Constructor for IncorrectResultSetColumnCountException. + * @param expectedCount the expected column count + * @param actualCount the actual column count + */ + public IncorrectResultSetColumnCountException(int expectedCount, int actualCount) { + super("Incorrect column count: expected " + expectedCount + ", actual " + actualCount); + this.expectedCount = expectedCount; + this.actualCount = actualCount; + } + + /** + * Constructor for IncorrectResultCountDataAccessException. + * @param msg the detail message + * @param expectedCount the expected column count + * @param actualCount the actual column count + */ + public IncorrectResultSetColumnCountException(String msg, int expectedCount, int actualCount) { + super(msg); + this.expectedCount = expectedCount; + this.actualCount = actualCount; + } + + + /** + * Return the expected column count. + */ + public int getExpectedCount() { + return this.expectedCount; + } + + /** + * Return the actual column count. + */ + public int getActualCount() { + return this.actualCount; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/InvalidResultSetAccessException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/InvalidResultSetAccessException.java new file mode 100644 index 0000000..122d7b2 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/InvalidResultSetAccessException.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc; + +import java.sql.SQLException; + +import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.lang.Nullable; + +/** + * Exception thrown when a ResultSet has been accessed in an invalid fashion. + * Such exceptions always have a {@code java.sql.SQLException} root cause. + * + *

    This typically happens when an invalid ResultSet column index or name + * has been specified. Also thrown by disconnected SqlRowSets. + * + * @author Juergen Hoeller + * @since 1.2 + * @see BadSqlGrammarException + * @see org.springframework.jdbc.support.rowset.SqlRowSet + */ +@SuppressWarnings("serial") +public class InvalidResultSetAccessException extends InvalidDataAccessResourceUsageException { + + @Nullable + private final String sql; + + + /** + * Constructor for InvalidResultSetAccessException. + * @param task name of current task + * @param sql the offending SQL statement + * @param ex the root cause + */ + public InvalidResultSetAccessException(String task, String sql, SQLException ex) { + super(task + "; invalid ResultSet access for SQL [" + sql + "]", ex); + this.sql = sql; + } + + /** + * Constructor for InvalidResultSetAccessException. + * @param ex the root cause + */ + public InvalidResultSetAccessException(SQLException ex) { + super(ex.getMessage(), ex); + this.sql = null; + } + + + /** + * Return the wrapped SQLException. + */ + public SQLException getSQLException() { + return (SQLException) getCause(); + } + + /** + * Return the SQL that caused the problem. + * @return the offending SQL, if known + */ + @Nullable + public String getSql() { + return this.sql; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/JdbcUpdateAffectedIncorrectNumberOfRowsException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/JdbcUpdateAffectedIncorrectNumberOfRowsException.java new file mode 100644 index 0000000..4b88b41 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/JdbcUpdateAffectedIncorrectNumberOfRowsException.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc; + +import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException; + +/** + * Exception thrown when a JDBC update affects an unexpected number of rows. + * Typically we expect an update to affect a single row, meaning it's an + * error if it affects multiple rows. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +@SuppressWarnings("serial") +public class JdbcUpdateAffectedIncorrectNumberOfRowsException extends IncorrectUpdateSemanticsDataAccessException { + + /** Number of rows that should have been affected. */ + private final int expected; + + /** Number of rows that actually were affected. */ + private final int actual; + + + /** + * Constructor for JdbcUpdateAffectedIncorrectNumberOfRowsException. + * @param sql the SQL we were trying to execute + * @param expected the expected number of rows affected + * @param actual the actual number of rows affected + */ + public JdbcUpdateAffectedIncorrectNumberOfRowsException(String sql, int expected, int actual) { + super("SQL update '" + sql + "' affected " + actual + " rows, not " + expected + " as expected"); + this.expected = expected; + this.actual = actual; + } + + + /** + * Return the number of rows that should have been affected. + */ + public int getExpectedRowsAffected() { + return this.expected; + } + + /** + * Return the number of rows that have actually been affected. + */ + public int getActualRowsAffected() { + return this.actual; + } + + @Override + public boolean wasDataUpdated() { + return (getActualRowsAffected() > 0); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/LobRetrievalFailureException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/LobRetrievalFailureException.java new file mode 100644 index 0000000..c53924a --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/LobRetrievalFailureException.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc; + +import java.io.IOException; + +import org.springframework.dao.DataRetrievalFailureException; + +/** + * Exception to be thrown when a LOB could not be retrieved. + * + * @author Juergen Hoeller + * @since 1.0.2 + */ +@SuppressWarnings("serial") +public class LobRetrievalFailureException extends DataRetrievalFailureException { + + /** + * Constructor for LobRetrievalFailureException. + * @param msg the detail message + */ + public LobRetrievalFailureException(String msg) { + super(msg); + } + + /** + * Constructor for LobRetrievalFailureException. + * @param msg the detail message + * @param ex the root cause IOException + */ + public LobRetrievalFailureException(String msg, IOException ex) { + super(msg, ex); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/SQLWarningException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/SQLWarningException.java new file mode 100644 index 0000000..1256e71 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/SQLWarningException.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc; + +import java.sql.SQLWarning; + +import org.springframework.dao.UncategorizedDataAccessException; + +/** + * Exception thrown when we're not ignoring {@link java.sql.SQLWarning SQLWarnings}. + * + *

    If an SQLWarning is reported, the operation completed, so we will need + * to explicitly roll it back if we're not happy when looking at the warning. + * We might choose to ignore (and log) the warning, or to wrap and throw it + * in the shape of this SQLWarningException instead. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see org.springframework.jdbc.core.JdbcTemplate#setIgnoreWarnings + */ +@SuppressWarnings("serial") +public class SQLWarningException extends UncategorizedDataAccessException { + + /** + * Constructor for SQLWarningException. + * @param msg the detail message + * @param ex the JDBC warning + */ + public SQLWarningException(String msg, SQLWarning ex) { + super(msg, ex); + } + + /** + * Return the underlying SQLWarning. + */ + public SQLWarning SQLWarning() { + return (SQLWarning) getCause(); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/UncategorizedSQLException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/UncategorizedSQLException.java new file mode 100644 index 0000000..315a6f9 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/UncategorizedSQLException.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc; + +import java.sql.SQLException; + +import org.springframework.dao.UncategorizedDataAccessException; +import org.springframework.lang.Nullable; + +/** + * Exception thrown when we can't classify an SQLException into + * one of our generic data access exceptions. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +@SuppressWarnings("serial") +public class UncategorizedSQLException extends UncategorizedDataAccessException { + + /** SQL that led to the problem. */ + @Nullable + private final String sql; + + + /** + * Constructor for UncategorizedSQLException. + * @param task name of current task + * @param sql the offending SQL statement + * @param ex the root cause + */ + public UncategorizedSQLException(String task, @Nullable String sql, SQLException ex) { + super(task + "; uncategorized SQLException" + (sql != null ? " for SQL [" + sql + "]" : "") + + "; SQL state [" + ex.getSQLState() + "]; error code [" + ex.getErrorCode() + "]; " + + ex.getMessage(), ex); + this.sql = sql; + } + + + /** + * Return the underlying SQLException. + */ + public SQLException getSQLException() { + return (SQLException) getCause(); + } + + /** + * Return the SQL that led to the problem (if known). + */ + @Nullable + public String getSql() { + return this.sql; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/config/DatabasePopulatorConfigUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/config/DatabasePopulatorConfigUtils.java new file mode 100644 index 0000000..f172934 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/config/DatabasePopulatorConfigUtils.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.config; + +import java.util.List; + +import org.w3c.dom.Element; + +import org.springframework.beans.BeanMetadataElement; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.TypedStringValue; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.jdbc.datasource.init.CompositeDatabasePopulator; +import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; + +/** + * Internal utility methods used with JDBC configuration. + * + * @author Juergen Hoeller + * @author Stephane Nicoll + * @since 3.1 + */ +abstract class DatabasePopulatorConfigUtils { + + public static void setDatabasePopulator(Element element, BeanDefinitionBuilder builder) { + List scripts = DomUtils.getChildElementsByTagName(element, "script"); + if (!scripts.isEmpty()) { + builder.addPropertyValue("databasePopulator", createDatabasePopulator(element, scripts, "INIT")); + builder.addPropertyValue("databaseCleaner", createDatabasePopulator(element, scripts, "DESTROY")); + } + } + + private static BeanDefinition createDatabasePopulator(Element element, List scripts, String execution) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(CompositeDatabasePopulator.class); + + boolean ignoreFailedDrops = element.getAttribute("ignore-failures").equals("DROPS"); + boolean continueOnError = element.getAttribute("ignore-failures").equals("ALL"); + + ManagedList delegates = new ManagedList<>(); + for (Element scriptElement : scripts) { + String executionAttr = scriptElement.getAttribute("execution"); + if (!StringUtils.hasText(executionAttr)) { + executionAttr = "INIT"; + } + if (!execution.equals(executionAttr)) { + continue; + } + BeanDefinitionBuilder delegate = BeanDefinitionBuilder.genericBeanDefinition(ResourceDatabasePopulator.class); + delegate.addPropertyValue("ignoreFailedDrops", ignoreFailedDrops); + delegate.addPropertyValue("continueOnError", continueOnError); + + // Use a factory bean for the resources so they can be given an order if a pattern is used + BeanDefinitionBuilder resourcesFactory = BeanDefinitionBuilder.genericBeanDefinition(SortedResourcesFactoryBean.class); + resourcesFactory.addConstructorArgValue(new TypedStringValue(scriptElement.getAttribute("location"))); + delegate.addPropertyValue("scripts", resourcesFactory.getBeanDefinition()); + if (StringUtils.hasLength(scriptElement.getAttribute("encoding"))) { + delegate.addPropertyValue("sqlScriptEncoding", new TypedStringValue(scriptElement.getAttribute("encoding"))); + } + String separator = getSeparator(element, scriptElement); + if (separator != null) { + delegate.addPropertyValue("separator", new TypedStringValue(separator)); + } + delegates.add(delegate.getBeanDefinition()); + } + builder.addPropertyValue("populators", delegates); + + return builder.getBeanDefinition(); + } + + @Nullable + private static String getSeparator(Element element, Element scriptElement) { + String scriptSeparator = scriptElement.getAttribute("separator"); + if (StringUtils.hasLength(scriptSeparator)) { + return scriptSeparator; + } + String elementSeparator = element.getAttribute("separator"); + if (StringUtils.hasLength(elementSeparator)) { + return elementSeparator; + } + return null; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/config/EmbeddedDatabaseBeanDefinitionParser.java b/spring-jdbc/src/main/java/org/springframework/jdbc/config/EmbeddedDatabaseBeanDefinitionParser.java new file mode 100644 index 0000000..a568e84 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/config/EmbeddedDatabaseBeanDefinitionParser.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseFactoryBean; +import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; +import org.springframework.util.StringUtils; + +/** + * {@link org.springframework.beans.factory.xml.BeanDefinitionParser} that + * parses an {@code embedded-database} element and creates a {@link BeanDefinition} + * for an {@link EmbeddedDatabaseFactoryBean}. + * + *

    Picks up nested {@code script} elements and configures a + * {@link ResourceDatabasePopulator} for each of them. + * + * @author Oliver Gierke + * @author Juergen Hoeller + * @author Sam Brannen + * @since 3.0 + * @see DatabasePopulatorConfigUtils + */ +class EmbeddedDatabaseBeanDefinitionParser extends AbstractBeanDefinitionParser { + + /** + * Constant for the "database-name" attribute. + */ + static final String DB_NAME_ATTRIBUTE = "database-name"; + + /** + * Constant for the "generate-name" attribute. + */ + static final String GENERATE_NAME_ATTRIBUTE = "generate-name"; + + + @Override + protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(EmbeddedDatabaseFactoryBean.class); + setGenerateUniqueDatabaseNameFlag(element, builder); + setDatabaseName(element, builder); + setDatabaseType(element, builder); + DatabasePopulatorConfigUtils.setDatabasePopulator(element, builder); + builder.getRawBeanDefinition().setSource(parserContext.extractSource(element)); + return builder.getBeanDefinition(); + } + + @Override + protected boolean shouldGenerateIdAsFallback() { + return true; + } + + private void setGenerateUniqueDatabaseNameFlag(Element element, BeanDefinitionBuilder builder) { + String generateName = element.getAttribute(GENERATE_NAME_ATTRIBUTE); + if (StringUtils.hasText(generateName)) { + builder.addPropertyValue("generateUniqueDatabaseName", generateName); + } + } + + private void setDatabaseName(Element element, BeanDefinitionBuilder builder) { + // 1) Check for an explicit database name + String name = element.getAttribute(DB_NAME_ATTRIBUTE); + + // 2) Fall back to an implicit database name based on the ID + if (!StringUtils.hasText(name)) { + name = element.getAttribute(ID_ATTRIBUTE); + } + + if (StringUtils.hasText(name)) { + builder.addPropertyValue("databaseName", name); + } + // else, let EmbeddedDatabaseFactory use the default "testdb" name + } + + private void setDatabaseType(Element element, BeanDefinitionBuilder builder) { + String type = element.getAttribute("type"); + if (StringUtils.hasText(type)) { + builder.addPropertyValue("databaseType", type); + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/config/InitializeDatabaseBeanDefinitionParser.java b/spring-jdbc/src/main/java/org/springframework/jdbc/config/InitializeDatabaseBeanDefinitionParser.java new file mode 100644 index 0000000..49e35a1 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/config/InitializeDatabaseBeanDefinitionParser.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.jdbc.datasource.init.DataSourceInitializer; +import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; + +/** + * {@link org.springframework.beans.factory.xml.BeanDefinitionParser} that parses an {@code initialize-database} + * element and creates a {@link BeanDefinition} of type {@link DataSourceInitializer}. Picks up nested + * {@code script} elements and configures a {@link ResourceDatabasePopulator} for them. + * + * @author Dave Syer + * @author Juergen Hoeller + * @since 3.0 + */ +class InitializeDatabaseBeanDefinitionParser extends AbstractBeanDefinitionParser { + + @Override + protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(DataSourceInitializer.class); + builder.addPropertyReference("dataSource", element.getAttribute("data-source")); + builder.addPropertyValue("enabled", element.getAttribute("enabled")); + DatabasePopulatorConfigUtils.setDatabasePopulator(element, builder); + builder.getRawBeanDefinition().setSource(parserContext.extractSource(element)); + return builder.getBeanDefinition(); + } + + @Override + protected boolean shouldGenerateId() { + return true; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/config/JdbcNamespaceHandler.java b/spring-jdbc/src/main/java/org/springframework/jdbc/config/JdbcNamespaceHandler.java new file mode 100644 index 0000000..60adbdd --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/config/JdbcNamespaceHandler.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.config; + +import org.springframework.beans.factory.xml.NamespaceHandler; +import org.springframework.beans.factory.xml.NamespaceHandlerSupport; + +/** + * {@link NamespaceHandler} for JDBC configuration namespace. + * @author Oliver Gierke + * @author Dave Syer + */ +public class JdbcNamespaceHandler extends NamespaceHandlerSupport { + + @Override + public void init() { + registerBeanDefinitionParser("embedded-database", new EmbeddedDatabaseBeanDefinitionParser()); + registerBeanDefinitionParser("initialize-database", new InitializeDatabaseBeanDefinitionParser()); + } +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/config/SortedResourcesFactoryBean.java b/spring-jdbc/src/main/java/org/springframework/jdbc/config/SortedResourcesFactoryBean.java new file mode 100644 index 0000000..40fa1b0 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/config/SortedResourcesFactoryBean.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.config; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.AbstractFactoryBean; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternUtils; + +/** + * {@link FactoryBean} implementation that takes a list of location Strings + * and creates a sorted array of {@link Resource} instances. + * + * @author Dave Syer + * @author Juergen Hoeller + * @author Christian Dupuis + * @since 3.0 + */ +public class SortedResourcesFactoryBean extends AbstractFactoryBean implements ResourceLoaderAware { + + private final List locations; + + private ResourcePatternResolver resourcePatternResolver; + + + public SortedResourcesFactoryBean(List locations) { + this.locations = locations; + this.resourcePatternResolver = new PathMatchingResourcePatternResolver(); + } + + public SortedResourcesFactoryBean(ResourceLoader resourceLoader, List locations) { + this.locations = locations; + this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader); + } + + + @Override + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader); + } + + + @Override + public Class getObjectType() { + return Resource[].class; + } + + @Override + protected Resource[] createInstance() throws Exception { + List scripts = new ArrayList<>(); + for (String location : this.locations) { + List resources = new ArrayList<>( + Arrays.asList(this.resourcePatternResolver.getResources(location))); + resources.sort((r1, r2) -> { + try { + return r1.getURL().toString().compareTo(r2.getURL().toString()); + } + catch (IOException ex) { + return 0; + } + }); + scripts.addAll(resources); + } + return scripts.toArray(new Resource[0]); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/config/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/config/package-info.java new file mode 100644 index 0000000..83cdb23 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/config/package-info.java @@ -0,0 +1,9 @@ +/** + * Defines the Spring JDBC configuration namespace. + */ +@NonNullApi +@NonNullFields +package org.springframework.jdbc.config; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/ArgumentPreparedStatementSetter.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ArgumentPreparedStatementSetter.java new file mode 100644 index 0000000..4f92385 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ArgumentPreparedStatementSetter.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.springframework.lang.Nullable; + +/** + * Simple adapter for {@link PreparedStatementSetter} that applies a given array of arguments. + * + * @author Juergen Hoeller + * @since 3.2.3 + */ +public class ArgumentPreparedStatementSetter implements PreparedStatementSetter, ParameterDisposer { + + @Nullable + private final Object[] args; + + + /** + * Create a new ArgPreparedStatementSetter for the given arguments. + * @param args the arguments to set + */ + public ArgumentPreparedStatementSetter(@Nullable Object[] args) { + this.args = args; + } + + + @Override + public void setValues(PreparedStatement ps) throws SQLException { + if (this.args != null) { + for (int i = 0; i < this.args.length; i++) { + Object arg = this.args[i]; + doSetValue(ps, i + 1, arg); + } + } + } + + /** + * Set the value for prepared statements specified parameter index using the passed in value. + * This method can be overridden by sub-classes if needed. + * @param ps the PreparedStatement + * @param parameterPosition index of the parameter position + * @param argValue the value to set + * @throws SQLException if thrown by PreparedStatement methods + */ + protected void doSetValue(PreparedStatement ps, int parameterPosition, Object argValue) throws SQLException { + if (argValue instanceof SqlParameterValue) { + SqlParameterValue paramValue = (SqlParameterValue) argValue; + StatementCreatorUtils.setParameterValue(ps, parameterPosition, paramValue, paramValue.getValue()); + } + else { + StatementCreatorUtils.setParameterValue(ps, parameterPosition, SqlTypeValue.TYPE_UNKNOWN, argValue); + } + } + + @Override + public void cleanupParameters() { + StatementCreatorUtils.cleanupParameters(this.args); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/ArgumentTypePreparedStatementSetter.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ArgumentTypePreparedStatementSetter.java new file mode 100644 index 0000000..7e102ef --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ArgumentTypePreparedStatementSetter.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Collection; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.lang.Nullable; + +/** + * Simple adapter for {@link PreparedStatementSetter} that applies + * given arrays of arguments and JDBC argument types. + * + * @author Juergen Hoeller + * @since 3.2.3 + */ +public class ArgumentTypePreparedStatementSetter implements PreparedStatementSetter, ParameterDisposer { + + @Nullable + private final Object[] args; + + @Nullable + private final int[] argTypes; + + + /** + * Create a new ArgTypePreparedStatementSetter for the given arguments. + * @param args the arguments to set + * @param argTypes the corresponding SQL types of the arguments + */ + public ArgumentTypePreparedStatementSetter(@Nullable Object[] args, @Nullable int[] argTypes) { + if ((args != null && argTypes == null) || (args == null && argTypes != null) || + (args != null && args.length != argTypes.length)) { + throw new InvalidDataAccessApiUsageException("args and argTypes parameters must match"); + } + this.args = args; + this.argTypes = argTypes; + } + + + @Override + public void setValues(PreparedStatement ps) throws SQLException { + int parameterPosition = 1; + if (this.args != null && this.argTypes != null) { + for (int i = 0; i < this.args.length; i++) { + Object arg = this.args[i]; + if (arg instanceof Collection && this.argTypes[i] != Types.ARRAY) { + Collection entries = (Collection) arg; + for (Object entry : entries) { + if (entry instanceof Object[]) { + Object[] valueArray = ((Object[]) entry); + for (Object argValue : valueArray) { + doSetValue(ps, parameterPosition, this.argTypes[i], argValue); + parameterPosition++; + } + } + else { + doSetValue(ps, parameterPosition, this.argTypes[i], entry); + parameterPosition++; + } + } + } + else { + doSetValue(ps, parameterPosition, this.argTypes[i], arg); + parameterPosition++; + } + } + } + } + + /** + * Set the value for the prepared statement's specified parameter position using the passed in + * value and type. This method can be overridden by sub-classes if needed. + * @param ps the PreparedStatement + * @param parameterPosition index of the parameter position + * @param argType the argument type + * @param argValue the argument value + * @throws SQLException if thrown by PreparedStatement methods + */ + protected void doSetValue(PreparedStatement ps, int parameterPosition, int argType, Object argValue) + throws SQLException { + + StatementCreatorUtils.setParameterValue(ps, parameterPosition, argType, argValue); + } + + @Override + public void cleanupParameters() { + StatementCreatorUtils.cleanupParameters(this.args); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/BatchPreparedStatementSetter.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/BatchPreparedStatementSetter.java new file mode 100644 index 0000000..19c6216 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/BatchPreparedStatementSetter.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * Batch update callback interface used by the {@link JdbcTemplate} class. + * + *

    This interface sets values on a {@link java.sql.PreparedStatement} provided + * by the JdbcTemplate class, for each of a number of updates in a batch using the + * same SQL. Implementations are responsible for setting any necessary parameters. + * SQL with placeholders will already have been supplied. + * + *

    Implementations do not need to concern themselves with SQLExceptions + * that may be thrown from operations they attempt. The JdbcTemplate class will + * catch and handle SQLExceptions appropriately. + * + * @author Rod Johnson + * @since March 2, 2003 + * @see JdbcTemplate#batchUpdate(String, BatchPreparedStatementSetter) + * @see InterruptibleBatchPreparedStatementSetter + */ +public interface BatchPreparedStatementSetter { + + /** + * Set parameter values on the given PreparedStatement. + * @param ps the PreparedStatement to invoke setter methods on + * @param i index of the statement we're issuing in the batch, starting from 0 + * @throws SQLException if an SQLException is encountered + * (i.e. there is no need to catch SQLException) + */ + void setValues(PreparedStatement ps, int i) throws SQLException; + + /** + * Return the size of the batch. + * @return the number of statements in the batch + */ + int getBatchSize(); + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/BatchUpdateUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/BatchUpdateUtils.java new file mode 100644 index 0000000..8f1fcef --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/BatchUpdateUtils.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; + +import org.springframework.lang.Nullable; + +/** + * Generic utility methods for working with JDBC batch statements. + * Mainly for internal use within the framework. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 3.0 + * @deprecated as of 5.1.3, not used by {@link JdbcTemplate} anymore + */ +@Deprecated +public abstract class BatchUpdateUtils { + + public static int[] executeBatchUpdate( + String sql, final List batchArgs, final int[] columnTypes, JdbcOperations jdbcOperations) { + + if (batchArgs.isEmpty()) { + return new int[0]; + } + + return jdbcOperations.batchUpdate( + sql, + new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + Object[] values = batchArgs.get(i); + setStatementParameters(values, ps, columnTypes); + } + @Override + public int getBatchSize() { + return batchArgs.size(); + } + }); + } + + protected static void setStatementParameters(Object[] values, PreparedStatement ps, @Nullable int[] columnTypes) + throws SQLException { + + int colIndex = 0; + for (Object value : values) { + colIndex++; + if (value instanceof SqlParameterValue) { + SqlParameterValue paramValue = (SqlParameterValue) value; + StatementCreatorUtils.setParameterValue(ps, colIndex, paramValue, paramValue.getValue()); + } + else { + int colType; + if (columnTypes == null || columnTypes.length < colIndex) { + colType = SqlTypeValue.TYPE_UNKNOWN; + } + else { + colType = columnTypes[colIndex - 1]; + } + StatementCreatorUtils.setParameterValue(ps, colIndex, colType, value); + } + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java new file mode 100644 index 0000000..4beec4a --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java @@ -0,0 +1,436 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.beans.PropertyDescriptor; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.BeanWrapperImpl; +import org.springframework.beans.NotWritablePropertyException; +import org.springframework.beans.TypeConverter; +import org.springframework.beans.TypeMismatchException; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * {@link RowMapper} implementation that converts a row into a new instance + * of the specified mapped target class. The mapped target class must be a + * top-level class and it must have a default or no-arg constructor. + * + *

    Column values are mapped based on matching the column name as obtained from result set + * meta-data to public setters for the corresponding properties. The names are matched either + * directly or by transforming a name separating the parts with underscores to the same name + * using "camel" case. + * + *

    Mapping is provided for fields in the target class for many common types, e.g.: + * String, boolean, Boolean, byte, Byte, short, Short, int, Integer, long, Long, + * float, Float, double, Double, BigDecimal, {@code java.util.Date}, etc. + * + *

    To facilitate mapping between columns and fields that don't have matching names, + * try using column aliases in the SQL statement like "select fname as first_name from customer". + * + *

    For 'null' values read from the database, we will attempt to call the setter, but in the case of + * Java primitives, this causes a TypeMismatchException. This class can be configured (using the + * primitivesDefaultedForNullValue property) to trap this exception and use the primitives default value. + * Be aware that if you use the values from the generated bean to update the database the primitive value + * will have been set to the primitive's default value instead of null. + * + *

    Please note that this class is designed to provide convenience rather than high performance. + * For best performance, consider using a custom {@link RowMapper} implementation. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.5 + * @param the result type + */ +public class BeanPropertyRowMapper implements RowMapper { + + /** Logger available to subclasses. */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** The class we are mapping to. */ + @Nullable + private Class mappedClass; + + /** Whether we're strictly validating. */ + private boolean checkFullyPopulated = false; + + /** Whether we're defaulting primitives when mapping a null value. */ + private boolean primitivesDefaultedForNullValue = false; + + /** ConversionService for binding JDBC values to bean properties. */ + @Nullable + private ConversionService conversionService = DefaultConversionService.getSharedInstance(); + + /** Map of the fields we provide mapping for. */ + @Nullable + private Map mappedFields; + + /** Set of bean properties we provide mapping for. */ + @Nullable + private Set mappedProperties; + + + /** + * Create a new {@code BeanPropertyRowMapper} for bean-style configuration. + * @see #setMappedClass + * @see #setCheckFullyPopulated + */ + public BeanPropertyRowMapper() { + } + + /** + * Create a new {@code BeanPropertyRowMapper}, accepting unpopulated + * properties in the target bean. + * @param mappedClass the class that each row should be mapped to + */ + public BeanPropertyRowMapper(Class mappedClass) { + initialize(mappedClass); + } + + /** + * Create a new {@code BeanPropertyRowMapper}. + * @param mappedClass the class that each row should be mapped to + * @param checkFullyPopulated whether we're strictly validating that + * all bean properties have been mapped from corresponding database fields + */ + public BeanPropertyRowMapper(Class mappedClass, boolean checkFullyPopulated) { + initialize(mappedClass); + this.checkFullyPopulated = checkFullyPopulated; + } + + + /** + * Set the class that each row should be mapped to. + */ + public void setMappedClass(Class mappedClass) { + if (this.mappedClass == null) { + initialize(mappedClass); + } + else { + if (this.mappedClass != mappedClass) { + throw new InvalidDataAccessApiUsageException("The mapped class can not be reassigned to map to " + + mappedClass + " since it is already providing mapping for " + this.mappedClass); + } + } + } + + /** + * Get the class that we are mapping to. + */ + @Nullable + public final Class getMappedClass() { + return this.mappedClass; + } + + /** + * Set whether we're strictly validating that all bean properties have been mapped + * from corresponding database fields. + *

    Default is {@code false}, accepting unpopulated properties in the target bean. + */ + public void setCheckFullyPopulated(boolean checkFullyPopulated) { + this.checkFullyPopulated = checkFullyPopulated; + } + + /** + * Return whether we're strictly validating that all bean properties have been + * mapped from corresponding database fields. + */ + public boolean isCheckFullyPopulated() { + return this.checkFullyPopulated; + } + + /** + * Set whether we're defaulting Java primitives in the case of mapping a null value + * from corresponding database fields. + *

    Default is {@code false}, throwing an exception when nulls are mapped to Java primitives. + */ + public void setPrimitivesDefaultedForNullValue(boolean primitivesDefaultedForNullValue) { + this.primitivesDefaultedForNullValue = primitivesDefaultedForNullValue; + } + + /** + * Return whether we're defaulting Java primitives in the case of mapping a null value + * from corresponding database fields. + */ + public boolean isPrimitivesDefaultedForNullValue() { + return this.primitivesDefaultedForNullValue; + } + + /** + * Set a {@link ConversionService} for binding JDBC values to bean properties, + * or {@code null} for none. + *

    Default is a {@link DefaultConversionService}, as of Spring 4.3. This + * provides support for {@code java.time} conversion and other special types. + * @since 4.3 + * @see #initBeanWrapper(BeanWrapper) + */ + public void setConversionService(@Nullable ConversionService conversionService) { + this.conversionService = conversionService; + } + + /** + * Return a {@link ConversionService} for binding JDBC values to bean properties, + * or {@code null} if none. + * @since 4.3 + */ + @Nullable + public ConversionService getConversionService() { + return this.conversionService; + } + + + /** + * Initialize the mapping meta-data for the given class. + * @param mappedClass the mapped class + */ + protected void initialize(Class mappedClass) { + this.mappedClass = mappedClass; + this.mappedFields = new HashMap<>(); + this.mappedProperties = new HashSet<>(); + + for (PropertyDescriptor pd : BeanUtils.getPropertyDescriptors(mappedClass)) { + if (pd.getWriteMethod() != null) { + this.mappedFields.put(lowerCaseName(pd.getName()), pd); + String underscoredName = underscoreName(pd.getName()); + if (!lowerCaseName(pd.getName()).equals(underscoredName)) { + this.mappedFields.put(underscoredName, pd); + } + this.mappedProperties.add(pd.getName()); + } + } + } + + /** + * Convert a name in camelCase to an underscored name in lower case. + * Any upper case letters are converted to lower case with a preceding underscore. + * @param name the original name + * @return the converted name + * @since 4.2 + * @see #lowerCaseName + */ + protected String underscoreName(String name) { + if (!StringUtils.hasLength(name)) { + return ""; + } + + StringBuilder result = new StringBuilder(); + for (int i = 0; i < name.length(); i++) { + char c = name.charAt(i); + if (Character.isUpperCase(c)) { + result.append('_').append(Character.toLowerCase(c)); + } + else { + result.append(c); + } + } + return result.toString(); + } + + /** + * Convert the given name to lower case. + * By default, conversions will happen within the US locale. + * @param name the original name + * @return the converted name + * @since 4.2 + */ + protected String lowerCaseName(String name) { + return name.toLowerCase(Locale.US); + } + + + /** + * Extract the values for all columns in the current row. + *

    Utilizes public setters and result set meta-data. + * @see java.sql.ResultSetMetaData + */ + @Override + public T mapRow(ResultSet rs, int rowNumber) throws SQLException { + BeanWrapperImpl bw = new BeanWrapperImpl(); + initBeanWrapper(bw); + + T mappedObject = constructMappedInstance(rs, bw); + bw.setBeanInstance(mappedObject); + + ResultSetMetaData rsmd = rs.getMetaData(); + int columnCount = rsmd.getColumnCount(); + Set populatedProperties = (isCheckFullyPopulated() ? new HashSet<>() : null); + + for (int index = 1; index <= columnCount; index++) { + String column = JdbcUtils.lookupColumnName(rsmd, index); + String field = lowerCaseName(StringUtils.delete(column, " ")); + PropertyDescriptor pd = (this.mappedFields != null ? this.mappedFields.get(field) : null); + if (pd != null) { + try { + Object value = getColumnValue(rs, index, pd); + if (rowNumber == 0 && logger.isDebugEnabled()) { + logger.debug("Mapping column '" + column + "' to property '" + pd.getName() + + "' of type '" + ClassUtils.getQualifiedName(pd.getPropertyType()) + "'"); + } + try { + bw.setPropertyValue(pd.getName(), value); + } + catch (TypeMismatchException ex) { + if (value == null && this.primitivesDefaultedForNullValue) { + if (logger.isDebugEnabled()) { + logger.debug("Intercepted TypeMismatchException for row " + rowNumber + + " and column '" + column + "' with null value when setting property '" + + pd.getName() + "' of type '" + + ClassUtils.getQualifiedName(pd.getPropertyType()) + + "' on object: " + mappedObject, ex); + } + } + else { + throw ex; + } + } + if (populatedProperties != null) { + populatedProperties.add(pd.getName()); + } + } + catch (NotWritablePropertyException ex) { + throw new DataRetrievalFailureException( + "Unable to map column '" + column + "' to property '" + pd.getName() + "'", ex); + } + } + else { + // No PropertyDescriptor found + if (rowNumber == 0 && logger.isDebugEnabled()) { + logger.debug("No property found for column '" + column + "' mapped to field '" + field + "'"); + } + } + } + + if (populatedProperties != null && !populatedProperties.equals(this.mappedProperties)) { + throw new InvalidDataAccessApiUsageException("Given ResultSet does not contain all fields " + + "necessary to populate object of " + this.mappedClass + ": " + this.mappedProperties); + } + + return mappedObject; + } + + /** + * Construct an instance of the mapped class for the current row. + * @param rs the ResultSet to map (pre-initialized for the current row) + * @param tc a TypeConverter with this RowMapper's conversion service + * @return a corresponding instance of the mapped class + * @throws SQLException if an SQLException is encountered + * @since 5.3 + */ + protected T constructMappedInstance(ResultSet rs, TypeConverter tc) throws SQLException { + Assert.state(this.mappedClass != null, "Mapped class was not specified"); + return BeanUtils.instantiateClass(this.mappedClass); + } + + /** + * Initialize the given BeanWrapper to be used for row mapping. + * To be called for each row. + *

    The default implementation applies the configured {@link ConversionService}, + * if any. Can be overridden in subclasses. + * @param bw the BeanWrapper to initialize + * @see #getConversionService() + * @see BeanWrapper#setConversionService + */ + protected void initBeanWrapper(BeanWrapper bw) { + ConversionService cs = getConversionService(); + if (cs != null) { + bw.setConversionService(cs); + } + } + + /** + * Retrieve a JDBC object value for the specified column. + *

    The default implementation delegates to + * {@link #getColumnValue(ResultSet, int, Class)}. + * @param rs is the ResultSet holding the data + * @param index is the column index + * @param pd the bean property that each result object is expected to match + * @return the Object value + * @throws SQLException in case of extraction failure + * @see #getColumnValue(ResultSet, int, Class) + */ + @Nullable + protected Object getColumnValue(ResultSet rs, int index, PropertyDescriptor pd) throws SQLException { + return JdbcUtils.getResultSetValue(rs, index, pd.getPropertyType()); + } + + /** + * Retrieve a JDBC object value for the specified column. + *

    The default implementation calls + * {@link JdbcUtils#getResultSetValue(java.sql.ResultSet, int, Class)}. + * Subclasses may override this to check specific value types upfront, + * or to post-process values return from {@code getResultSetValue}. + * @param rs is the ResultSet holding the data + * @param index is the column index + * @param paramType the target parameter type + * @return the Object value + * @throws SQLException in case of extraction failure + * @since 5.3 + * @see org.springframework.jdbc.support.JdbcUtils#getResultSetValue(java.sql.ResultSet, int, Class) + */ + @Nullable + protected Object getColumnValue(ResultSet rs, int index, Class paramType) throws SQLException { + return JdbcUtils.getResultSetValue(rs, index, paramType); + } + + + /** + * Static factory method to create a new {@code BeanPropertyRowMapper}. + * @param mappedClass the class that each row should be mapped to + * @see #newInstance(Class, ConversionService) + */ + public static BeanPropertyRowMapper newInstance(Class mappedClass) { + return new BeanPropertyRowMapper<>(mappedClass); + } + + /** + * Static factory method to create a new {@code BeanPropertyRowMapper}. + * @param mappedClass the class that each row should be mapped to + * @param conversionService the {@link ConversionService} for binding + * JDBC values to bean properties, or {@code null} for none + * @since 5.2.3 + * @see #newInstance(Class) + * @see #setConversionService + */ + public static BeanPropertyRowMapper newInstance( + Class mappedClass, @Nullable ConversionService conversionService) { + + BeanPropertyRowMapper rowMapper = newInstance(mappedClass); + rowMapper.setConversionService(conversionService); + return rowMapper; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/CallableStatementCallback.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/CallableStatementCallback.java new file mode 100644 index 0000000..0ec86ab --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/CallableStatementCallback.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.CallableStatement; +import java.sql.SQLException; + +import org.springframework.dao.DataAccessException; +import org.springframework.lang.Nullable; + +/** + * Generic callback interface for code that operates on a CallableStatement. + * Allows to execute any number of operations on a single CallableStatement, + * for example a single execute call or repeated execute calls with varying + * parameters. + * + *

    Used internally by JdbcTemplate, but also useful for application code. + * Note that the passed-in CallableStatement can have been created by the + * framework or by a custom CallableStatementCreator. However, the latter is + * hardly ever necessary, as most custom callback actions will perform updates + * in which case a standard CallableStatement is fine. Custom actions will + * always set parameter values themselves, so that CallableStatementCreator + * capability is not needed either. + * + * @author Juergen Hoeller + * @since 16.03.2004 + * @param the result type + * @see JdbcTemplate#execute(String, CallableStatementCallback) + * @see JdbcTemplate#execute(CallableStatementCreator, CallableStatementCallback) + */ +@FunctionalInterface +public interface CallableStatementCallback { + + /** + * Gets called by {@code JdbcTemplate.execute} with an active JDBC + * CallableStatement. Does not need to care about closing the Statement + * or the Connection, or about handling transactions: this will all be + * handled by Spring's JdbcTemplate. + * + *

    NOTE: Any ResultSets opened should be closed in finally blocks + * within the callback implementation. Spring will close the Statement + * object after the callback returned, but this does not necessarily imply + * that the ResultSet resources will be closed: the Statement objects might + * get pooled by the connection pool, with {@code close} calls only + * returning the object to the pool but not physically closing the resources. + * + *

    If called without a thread-bound JDBC transaction (initiated by + * DataSourceTransactionManager), the code will simply get executed on the + * JDBC connection with its transactional semantics. If JdbcTemplate is + * configured to use a JTA-aware DataSource, the JDBC connection and thus + * the callback code will be transactional if a JTA transaction is active. + * + *

    Allows for returning a result object created within the callback, i.e. + * a domain object or a collection of domain objects. A thrown RuntimeException + * is treated as application exception: it gets propagated to the caller of + * the template. + * + * @param cs active JDBC CallableStatement + * @return a result object, or {@code null} if none + * @throws SQLException if thrown by a JDBC method, to be auto-converted + * into a DataAccessException by an SQLExceptionTranslator + * @throws DataAccessException in case of custom exceptions + */ + @Nullable + T doInCallableStatement(CallableStatement cs) throws SQLException, DataAccessException; + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/CallableStatementCreator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/CallableStatementCreator.java new file mode 100644 index 0000000..4ace8c3 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/CallableStatementCreator.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.SQLException; + +/** + * One of the three central callback interfaces used by the JdbcTemplate class. + * This interface creates a CallableStatement given a connection, provided + * by the JdbcTemplate class. Implementations are responsible for providing + * SQL and any necessary parameters. + * + *

    Implementations do not need to concern themselves with + * SQLExceptions that may be thrown from operations they attempt. + * The JdbcTemplate class will catch and handle SQLExceptions appropriately. + * + *

    A PreparedStatementCreator should also implement the SqlProvider interface + * if it is able to provide the SQL it uses for PreparedStatement creation. + * This allows for better contextual information in case of exceptions. + * + * @author Rod Johnson + * @author Thomas Risberg + * @see JdbcTemplate#execute(CallableStatementCreator, CallableStatementCallback) + * @see JdbcTemplate#call + * @see SqlProvider + */ +@FunctionalInterface +public interface CallableStatementCreator { + + /** + * Create a callable statement in this connection. Allows implementations to use + * CallableStatements. + * @param con the Connection to use to create statement + * @return a callable statement + * @throws SQLException there is no need to catch SQLExceptions + * that may be thrown in the implementation of this method. + * The JdbcTemplate class will handle them. + */ + CallableStatement createCallableStatement(Connection con) throws SQLException; + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/CallableStatementCreatorFactory.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/CallableStatementCreatorFactory.java new file mode 100644 index 0000000..69e88ae --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/CallableStatementCreatorFactory.java @@ -0,0 +1,236 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.lang.Nullable; + +/** + * Helper class that efficiently creates multiple {@link CallableStatementCreator} + * objects with different parameters based on an SQL statement and a single + * set of parameter declarations. + * + * @author Rod Johnson + * @author Thomas Risberg + * @author Juergen Hoeller + */ +public class CallableStatementCreatorFactory { + + /** The SQL call string, which won't change when the parameters change. */ + private final String callString; + + /** List of SqlParameter objects. May not be {@code null}. */ + private final List declaredParameters; + + private int resultSetType = ResultSet.TYPE_FORWARD_ONLY; + + private boolean updatableResults = false; + + + /** + * Create a new factory. Will need to add parameters via the + * {@link #addParameter} method or have no parameters. + * @param callString the SQL call string + */ + public CallableStatementCreatorFactory(String callString) { + this.callString = callString; + this.declaredParameters = new ArrayList<>(); + } + + /** + * Create a new factory with the given SQL and the given parameters. + * @param callString the SQL call string + * @param declaredParameters list of {@link SqlParameter} objects + */ + public CallableStatementCreatorFactory(String callString, List declaredParameters) { + this.callString = callString; + this.declaredParameters = declaredParameters; + } + + + /** + * Return the SQL call string. + * @since 5.1.3 + */ + public final String getCallString() { + return this.callString; + } + + /** + * Add a new declared parameter. + *

    Order of parameter addition is significant. + * @param param the parameter to add to the list of declared parameters + */ + public void addParameter(SqlParameter param) { + this.declaredParameters.add(param); + } + + /** + * Set whether to use prepared statements that return a specific type of ResultSet. + * specific type of ResultSet. + * @param resultSetType the ResultSet type + * @see java.sql.ResultSet#TYPE_FORWARD_ONLY + * @see java.sql.ResultSet#TYPE_SCROLL_INSENSITIVE + * @see java.sql.ResultSet#TYPE_SCROLL_SENSITIVE + */ + public void setResultSetType(int resultSetType) { + this.resultSetType = resultSetType; + } + + /** + * Set whether to use prepared statements capable of returning updatable ResultSets. + */ + public void setUpdatableResults(boolean updatableResults) { + this.updatableResults = updatableResults; + } + + + /** + * Return a new CallableStatementCreator instance given this parameters. + * @param params list of parameters (may be {@code null}) + */ + public CallableStatementCreator newCallableStatementCreator(@Nullable Map params) { + return new CallableStatementCreatorImpl(params != null ? params : new HashMap<>()); + } + + /** + * Return a new CallableStatementCreator instance given this parameter mapper. + * @param inParamMapper the ParameterMapper implementation that will return a Map of parameters + */ + public CallableStatementCreator newCallableStatementCreator(ParameterMapper inParamMapper) { + return new CallableStatementCreatorImpl(inParamMapper); + } + + + /** + * CallableStatementCreator implementation returned by this class. + */ + private class CallableStatementCreatorImpl implements CallableStatementCreator, SqlProvider, ParameterDisposer { + + @Nullable + private ParameterMapper inParameterMapper; + + @Nullable + private Map inParameters; + + /** + * Create a new CallableStatementCreatorImpl. + * @param inParamMapper the ParameterMapper implementation for mapping input parameters + */ + public CallableStatementCreatorImpl(ParameterMapper inParamMapper) { + this.inParameterMapper = inParamMapper; + } + + /** + * Create a new CallableStatementCreatorImpl. + * @param inParams list of SqlParameter objects + */ + public CallableStatementCreatorImpl(Map inParams) { + this.inParameters = inParams; + } + + @Override + public CallableStatement createCallableStatement(Connection con) throws SQLException { + // If we were given a ParameterMapper, we must let the mapper do its thing to create the Map. + if (this.inParameterMapper != null) { + this.inParameters = this.inParameterMapper.createMap(con); + } + else { + if (this.inParameters == null) { + throw new InvalidDataAccessApiUsageException( + "A ParameterMapper or a Map of parameters must be provided"); + } + } + + CallableStatement cs = null; + if (resultSetType == ResultSet.TYPE_FORWARD_ONLY && !updatableResults) { + cs = con.prepareCall(callString); + } + else { + cs = con.prepareCall(callString, resultSetType, + updatableResults ? ResultSet.CONCUR_UPDATABLE : ResultSet.CONCUR_READ_ONLY); + } + + int sqlColIndx = 1; + for (SqlParameter declaredParam : declaredParameters) { + if (!declaredParam.isResultsParameter()) { + // So, it's a call parameter - part of the call string. + // Get the value - it may still be null. + Object inValue = this.inParameters.get(declaredParam.getName()); + if (declaredParam instanceof ResultSetSupportingSqlParameter) { + // It's an output parameter: SqlReturnResultSet parameters already excluded. + // It need not (but may be) supplied by the caller. + if (declaredParam instanceof SqlOutParameter) { + if (declaredParam.getTypeName() != null) { + cs.registerOutParameter(sqlColIndx, declaredParam.getSqlType(), declaredParam.getTypeName()); + } + else { + if (declaredParam.getScale() != null) { + cs.registerOutParameter(sqlColIndx, declaredParam.getSqlType(), declaredParam.getScale()); + } + else { + cs.registerOutParameter(sqlColIndx, declaredParam.getSqlType()); + } + } + if (declaredParam.isInputValueProvided()) { + StatementCreatorUtils.setParameterValue(cs, sqlColIndx, declaredParam, inValue); + } + } + } + else { + // It's an input parameter; must be supplied by the caller. + if (!this.inParameters.containsKey(declaredParam.getName())) { + throw new InvalidDataAccessApiUsageException( + "Required input parameter '" + declaredParam.getName() + "' is missing"); + } + StatementCreatorUtils.setParameterValue(cs, sqlColIndx, declaredParam, inValue); + } + sqlColIndx++; + } + } + + return cs; + } + + @Override + public String getSql() { + return callString; + } + + @Override + public void cleanupParameters() { + if (this.inParameters != null) { + StatementCreatorUtils.cleanupParameters(this.inParameters.values()); + } + } + + @Override + public String toString() { + return "CallableStatementCreator: sql=[" + callString + "]; parameters=" + this.inParameters; + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/ColumnMapRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ColumnMapRowMapper.java new file mode 100644 index 0000000..fed0064 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ColumnMapRowMapper.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.Map; + +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.LinkedCaseInsensitiveMap; + +/** + * {@link RowMapper} implementation that creates a {@code java.util.Map} + * for each row, representing all columns as key-value pairs: one + * entry for each column, with the column name as key. + * + *

    The Map implementation to use and the key to use for each column + * in the column Map can be customized through overriding + * {@link #createColumnMap} and {@link #getColumnKey}, respectively. + * + *

    Note: By default, ColumnMapRowMapper will try to build a linked Map + * with case-insensitive keys, to preserve column order as well as allow any + * casing to be used for column names. This requires Commons Collections on the + * classpath (which will be autodetected). Else, the fallback is a standard linked + * HashMap, which will still preserve column order but requires the application + * to specify the column names in the same casing as exposed by the driver. + * + * @author Juergen Hoeller + * @since 1.2 + * @see JdbcTemplate#queryForList(String) + * @see JdbcTemplate#queryForMap(String) + */ +public class ColumnMapRowMapper implements RowMapper> { + + @Override + public Map mapRow(ResultSet rs, int rowNum) throws SQLException { + ResultSetMetaData rsmd = rs.getMetaData(); + int columnCount = rsmd.getColumnCount(); + Map mapOfColumnValues = createColumnMap(columnCount); + for (int i = 1; i <= columnCount; i++) { + String column = JdbcUtils.lookupColumnName(rsmd, i); + mapOfColumnValues.putIfAbsent(getColumnKey(column), getColumnValue(rs, i)); + } + return mapOfColumnValues; + } + + /** + * Create a Map instance to be used as column map. + *

    By default, a linked case-insensitive Map will be created. + * @param columnCount the column count, to be used as initial + * capacity for the Map + * @return the new Map instance + * @see org.springframework.util.LinkedCaseInsensitiveMap + */ + protected Map createColumnMap(int columnCount) { + return new LinkedCaseInsensitiveMap<>(columnCount); + } + + /** + * Determine the key to use for the given column in the column Map. + * @param columnName the column name as returned by the ResultSet + * @return the column key to use + * @see java.sql.ResultSetMetaData#getColumnName + */ + protected String getColumnKey(String columnName) { + return columnName; + } + + /** + * Retrieve a JDBC object value for the specified column. + *

    The default implementation uses the {@code getObject} method. + * Additionally, this implementation includes a "hack" to get around Oracle + * returning a non standard object for their TIMESTAMP datatype. + * @param rs is the ResultSet holding the data + * @param index is the column index + * @return the Object returned + * @see org.springframework.jdbc.support.JdbcUtils#getResultSetValue + */ + @Nullable + protected Object getColumnValue(ResultSet rs, int index) throws SQLException { + return JdbcUtils.getResultSetValue(rs, index); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/ConnectionCallback.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ConnectionCallback.java new file mode 100644 index 0000000..25bbfd4 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ConnectionCallback.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.Connection; +import java.sql.SQLException; + +import org.springframework.dao.DataAccessException; +import org.springframework.lang.Nullable; + +/** + * Generic callback interface for code that operates on a JDBC Connection. + * Allows to execute any number of operations on a single Connection, + * using any type and number of Statements. + * + *

    This is particularly useful for delegating to existing data access code + * that expects a Connection to work on and throws SQLException. For newly + * written code, it is strongly recommended to use JdbcTemplate's more specific + * operations, for example a {@code query} or {@code update} variant. + * + * @author Juergen Hoeller + * @since 1.1.3 + * @param the result type + * @see JdbcTemplate#execute(ConnectionCallback) + * @see JdbcTemplate#query + * @see JdbcTemplate#update + */ +@FunctionalInterface +public interface ConnectionCallback { + + /** + * Gets called by {@code JdbcTemplate.execute} with an active JDBC + * Connection. Does not need to care about activating or closing the + * Connection, or handling transactions. + *

    If called without a thread-bound JDBC transaction (initiated by + * DataSourceTransactionManager), the code will simply get executed on the + * JDBC connection with its transactional semantics. If JdbcTemplate is + * configured to use a JTA-aware DataSource, the JDBC Connection and thus + * the callback code will be transactional if a JTA transaction is active. + *

    Allows for returning a result object created within the callback, i.e. + * a domain object or a collection of domain objects. Note that there's special + * support for single step actions: see {@code JdbcTemplate.queryForObject} + * etc. A thrown RuntimeException is treated as application exception: + * it gets propagated to the caller of the template. + * @param con active JDBC Connection + * @return a result object, or {@code null} if none + * @throws SQLException if thrown by a JDBC method, to be auto-converted + * to a DataAccessException by an SQLExceptionTranslator + * @throws DataAccessException in case of custom exceptions + * @see JdbcTemplate#queryForObject(String, Class) + * @see JdbcTemplate#queryForRowSet(String) + */ + @Nullable + T doInConnection(Connection con) throws SQLException, DataAccessException; + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java new file mode 100644 index 0000000..0cecdc5 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java @@ -0,0 +1,130 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.lang.reflect.Constructor; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.TypeConverter; +import org.springframework.core.convert.ConversionService; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * {@link RowMapper} implementation that converts a row into a new instance + * of the specified mapped target class. The mapped target class must be a + * top-level class and may either expose a data class constructor with named + * parameters corresponding to column names or classic bean property setters + * (or even a combination of both). + * + *

    Note that this class extends {@link BeanPropertyRowMapper} and can + * therefore serve as a common choice for any mapped target class, flexibly + * adapting to constructor style versus setter methods in the mapped class. + * + * @author Juergen Hoeller + * @since 5.3 + * @param the result type + */ +public class DataClassRowMapper extends BeanPropertyRowMapper { + + @Nullable + private Constructor mappedConstructor; + + @Nullable + private String[] constructorParameterNames; + + @Nullable + private Class[] constructorParameterTypes; + + + /** + * Create a new {@code DataClassRowMapper} for bean-style configuration. + * @see #setMappedClass + * @see #setConversionService + */ + public DataClassRowMapper() { + } + + /** + * Create a new {@code DataClassRowMapper}. + * @param mappedClass the class that each row should be mapped to + */ + public DataClassRowMapper(Class mappedClass) { + super(mappedClass); + } + + + @Override + protected void initialize(Class mappedClass) { + super.initialize(mappedClass); + + this.mappedConstructor = BeanUtils.getResolvableConstructor(mappedClass); + if (this.mappedConstructor.getParameterCount() > 0) { + this.constructorParameterNames = BeanUtils.getParameterNames(this.mappedConstructor); + this.constructorParameterTypes = this.mappedConstructor.getParameterTypes(); + } + } + + @Override + protected T constructMappedInstance(ResultSet rs, TypeConverter tc) throws SQLException { + Assert.state(this.mappedConstructor != null, "Mapped constructor was not initialized"); + + Object[] args; + if (this.constructorParameterNames != null && this.constructorParameterTypes != null) { + args = new Object[this.constructorParameterNames.length]; + for (int i = 0; i < args.length; i++) { + String name = underscoreName(this.constructorParameterNames[i]); + Class type = this.constructorParameterTypes[i]; + args[i] = tc.convertIfNecessary(getColumnValue(rs, rs.findColumn(name), type), type); + } + } + else { + args = new Object[0]; + } + + return BeanUtils.instantiateClass(this.mappedConstructor, args); + } + + + /** + * Static factory method to create a new {@code DataClassRowMapper}. + * @param mappedClass the class that each row should be mapped to + * @see #newInstance(Class, ConversionService) + */ + public static DataClassRowMapper newInstance(Class mappedClass) { + return new DataClassRowMapper<>(mappedClass); + } + + /** + * Static factory method to create a new {@code DataClassRowMapper}. + * @param mappedClass the class that each row should be mapped to + * @param conversionService the {@link ConversionService} for binding + * JDBC values to bean properties, or {@code null} for none + * @see #newInstance(Class) + * @see #setConversionService + */ + public static DataClassRowMapper newInstance( + Class mappedClass, @Nullable ConversionService conversionService) { + + DataClassRowMapper rowMapper = newInstance(mappedClass); + rowMapper.setConversionService(conversionService); + return rowMapper; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/DisposableSqlTypeValue.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/DisposableSqlTypeValue.java new file mode 100644 index 0000000..fb94d83 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/DisposableSqlTypeValue.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2008 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +/** + * Subinterface of {@link SqlTypeValue} that adds a cleanup callback, + * to be invoked after the value has been set and the corresponding + * statement has been executed. + * + * @author Juergen Hoeller + * @since 1.1 + * @see org.springframework.jdbc.core.support.SqlLobValue + */ +public interface DisposableSqlTypeValue extends SqlTypeValue { + + /** + * Clean up resources held by this type value, + * for example the LobCreator in case of an SqlLobValue. + * @see org.springframework.jdbc.core.support.SqlLobValue#cleanup() + * @see org.springframework.jdbc.support.SqlValue#cleanup() + */ + void cleanup(); + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/InterruptibleBatchPreparedStatementSetter.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/InterruptibleBatchPreparedStatementSetter.java new file mode 100644 index 0000000..9de0357 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/InterruptibleBatchPreparedStatementSetter.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +/** + * Extension of the {@link BatchPreparedStatementSetter} interface, + * adding a batch exhaustion check. + * + *

    This interface allows you to signal the end of a batch rather than + * having to determine the exact batch size upfront. Batch size is still + * being honored but it is now the maximum size of the batch. + * + *

    The {@link #isBatchExhausted} method is called after each call to + * {@link #setValues} to determine whether there were some values added, + * or if the batch was determined to be complete and no additional values + * were provided during the last call to {@code setValues}. + * + *

    Consider extending the + * {@link org.springframework.jdbc.core.support.AbstractInterruptibleBatchPreparedStatementSetter} + * base class instead of implementing this interface directly, using a single + * {@code setValuesIfAvailable} callback method that checks for available + * values and sets them, returning whether values have actually been provided. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.0 + * @see JdbcTemplate#batchUpdate(String, BatchPreparedStatementSetter) + * @see org.springframework.jdbc.core.support.AbstractInterruptibleBatchPreparedStatementSetter + */ +public interface InterruptibleBatchPreparedStatementSetter extends BatchPreparedStatementSetter { + + /** + * Return whether the batch is complete, that is, whether there were no + * additional values added during the last {@code setValues} call. + *

    NOTE: If this method returns {@code true}, any parameters + * that might have been set during the last {@code setValues} call will + * be ignored! Make sure that you set a corresponding internal flag if you + * detect exhaustion at the beginning of your {@code setValues} + * implementation, letting this method return {@code true} based on the flag. + * @param i index of the statement we're issuing in the batch, starting from 0 + * @return whether the batch is already exhausted + * @see #setValues + * @see org.springframework.jdbc.core.support.AbstractInterruptibleBatchPreparedStatementSetter#setValuesIfAvailable + */ + boolean isBatchExhausted(int i); + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java new file mode 100644 index 0000000..c285ddc --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java @@ -0,0 +1,1076 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.jdbc.support.rowset.SqlRowSet; +import org.springframework.lang.Nullable; + +/** + * Interface specifying a basic set of JDBC operations. + * Implemented by {@link JdbcTemplate}. Not often used directly, but a useful + * option to enhance testability, as it can easily be mocked or stubbed. + * + *

    Alternatively, the standard JDBC infrastructure can be mocked. + * However, mocking this interface constitutes significantly less work. + * As an alternative to a mock objects approach to testing data access code, + * consider the powerful integration testing support provided in the + * {@code org.springframework.test} package, shipped in + * {@code spring-test.jar}. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see JdbcTemplate + */ +public interface JdbcOperations { + + //------------------------------------------------------------------------- + // Methods dealing with a plain java.sql.Connection + //------------------------------------------------------------------------- + + /** + * Execute a JDBC data access operation, implemented as callback action + * working on a JDBC Connection. This allows for implementing arbitrary + * data access operations, within Spring's managed JDBC environment: + * that is, participating in Spring-managed transactions and converting + * JDBC SQLExceptions into Spring's DataAccessException hierarchy. + *

    The callback action can return a result object, for example a domain + * object or a collection of domain objects. + * @param action a callback object that specifies the action + * @return a result object returned by the action, or {@code null} if none + * @throws DataAccessException if there is any problem + */ + @Nullable + T execute(ConnectionCallback action) throws DataAccessException; + + + //------------------------------------------------------------------------- + // Methods dealing with static SQL (java.sql.Statement) + //------------------------------------------------------------------------- + + /** + * Execute a JDBC data access operation, implemented as callback action + * working on a JDBC Statement. This allows for implementing arbitrary data + * access operations on a single Statement, within Spring's managed JDBC + * environment: that is, participating in Spring-managed transactions and + * converting JDBC SQLExceptions into Spring's DataAccessException hierarchy. + *

    The callback action can return a result object, for example a domain + * object or a collection of domain objects. + * @param action a callback that specifies the action + * @return a result object returned by the action, or {@code null} if none + * @throws DataAccessException if there is any problem + */ + @Nullable + T execute(StatementCallback action) throws DataAccessException; + + /** + * Issue a single SQL execute, typically a DDL statement. + * @param sql static SQL to execute + * @throws DataAccessException if there is any problem + */ + void execute(String sql) throws DataAccessException; + + /** + * Execute a query given static SQL, reading the ResultSet with a + * ResultSetExtractor. + *

    Uses a JDBC Statement, not a PreparedStatement. If you want to + * execute a static query with a PreparedStatement, use the overloaded + * {@code query} method with {@code null} as argument array. + * @param sql the SQL query to execute + * @param rse a callback that will extract all rows of results + * @return an arbitrary result object, as returned by the ResultSetExtractor + * @throws DataAccessException if there is any problem executing the query + * @see #query(String, ResultSetExtractor, Object...) + */ + @Nullable + T query(String sql, ResultSetExtractor rse) throws DataAccessException; + + /** + * Execute a query given static SQL, reading the ResultSet on a per-row + * basis with a RowCallbackHandler. + *

    Uses a JDBC Statement, not a PreparedStatement. If you want to + * execute a static query with a PreparedStatement, use the overloaded + * {@code query} method with {@code null} as argument array. + * @param sql the SQL query to execute + * @param rch a callback that will extract results, one row at a time + * @throws DataAccessException if there is any problem executing the query + * @see #query(String, RowCallbackHandler, Object...) + */ + void query(String sql, RowCallbackHandler rch) throws DataAccessException; + + /** + * Execute a query given static SQL, mapping each row to a result object + * via a RowMapper. + *

    Uses a JDBC Statement, not a PreparedStatement. If you want to + * execute a static query with a PreparedStatement, use the overloaded + * {@code query} method with {@code null} as argument array. + * @param sql the SQL query to execute + * @param rowMapper a callback that will map one object per row + * @return the result List, containing mapped objects + * @throws DataAccessException if there is any problem executing the query + * @see #query(String, RowMapper, Object...) + */ + List query(String sql, RowMapper rowMapper) throws DataAccessException; + + /** + * Execute a query given static SQL, mapping each row to a result object + * via a RowMapper, and turning it into an iterable and closeable Stream. + *

    Uses a JDBC Statement, not a PreparedStatement. If you want to + * execute a static query with a PreparedStatement, use the overloaded + * {@code query} method with {@code null} as argument array. + * @param sql the SQL query to execute + * @param rowMapper a callback that will map one object per row + * @return the result Stream, containing mapped objects, needing to be + * closed once fully processed (e.g. through a try-with-resources clause) + * @throws DataAccessException if there is any problem executing the query + * @since 5.3 + * @see #queryForStream(String, RowMapper, Object...) + */ + Stream queryForStream(String sql, RowMapper rowMapper) throws DataAccessException; + + /** + * Execute a query given static SQL, mapping a single result row to a + * result object via a RowMapper. + *

    Uses a JDBC Statement, not a PreparedStatement. If you want to + * execute a static query with a PreparedStatement, use the overloaded + * {@link #queryForObject(String, RowMapper, Object...)} method with + * {@code null} as argument array. + * @param sql the SQL query to execute + * @param rowMapper a callback that will map one object per row + * @return the single mapped object (may be {@code null} if the given + * {@link RowMapper} returned {@code} null) + * @throws IncorrectResultSizeDataAccessException if the query does not + * return exactly one row + * @throws DataAccessException if there is any problem executing the query + * @see #queryForObject(String, RowMapper, Object...) + */ + @Nullable + T queryForObject(String sql, RowMapper rowMapper) throws DataAccessException; + + /** + * Execute a query for a result object, given static SQL. + *

    Uses a JDBC Statement, not a PreparedStatement. If you want to + * execute a static query with a PreparedStatement, use the overloaded + * {@link #queryForObject(String, Class, Object...)} method with + * {@code null} as argument array. + *

    This method is useful for running static SQL with a known outcome. + * The query is expected to be a single row/single column query; the returned + * result will be directly mapped to the corresponding object type. + * @param sql the SQL query to execute + * @param requiredType the type that the result object is expected to match + * @return the result object of the required type, or {@code null} in case of SQL NULL + * @throws IncorrectResultSizeDataAccessException if the query does not return + * exactly one row, or does not return exactly one column in that row + * @throws DataAccessException if there is any problem executing the query + * @see #queryForObject(String, Class, Object...) + */ + @Nullable + T queryForObject(String sql, Class requiredType) throws DataAccessException; + + /** + * Execute a query for a result map, given static SQL. + *

    Uses a JDBC Statement, not a PreparedStatement. If you want to + * execute a static query with a PreparedStatement, use the overloaded + * {@link #queryForMap(String, Object...)} method with {@code null} + * as argument array. + *

    The query is expected to be a single row query; the result row will be + * mapped to a Map (one entry for each column, using the column name as the key). + * @param sql the SQL query to execute + * @return the result Map (one entry per column, with column name as key) + * @throws IncorrectResultSizeDataAccessException if the query does not + * return exactly one row + * @throws DataAccessException if there is any problem executing the query + * @see #queryForMap(String, Object...) + * @see ColumnMapRowMapper + */ + Map queryForMap(String sql) throws DataAccessException; + + /** + * Execute a query for a result list, given static SQL. + *

    Uses a JDBC Statement, not a PreparedStatement. If you want to + * execute a static query with a PreparedStatement, use the overloaded + * {@code queryForList} method with {@code null} as argument array. + *

    The results will be mapped to a List (one entry for each row) of + * result objects, each of them matching the specified element type. + * @param sql the SQL query to execute + * @param elementType the required type of element in the result list + * (for example, {@code Integer.class}) + * @return a List of objects that match the specified element type + * @throws DataAccessException if there is any problem executing the query + * @see #queryForList(String, Class, Object...) + * @see SingleColumnRowMapper + */ + List queryForList(String sql, Class elementType) throws DataAccessException; + + /** + * Execute a query for a result list, given static SQL. + *

    Uses a JDBC Statement, not a PreparedStatement. If you want to + * execute a static query with a PreparedStatement, use the overloaded + * {@code queryForList} method with {@code null} as argument array. + *

    The results will be mapped to a List (one entry for each row) of + * Maps (one entry for each column using the column name as the key). + * Each element in the list will be of the form returned by this interface's + * {@code queryForMap} methods. + * @param sql the SQL query to execute + * @return an List that contains a Map per row + * @throws DataAccessException if there is any problem executing the query + * @see #queryForList(String, Object...) + */ + List> queryForList(String sql) throws DataAccessException; + + /** + * Execute a query for an SqlRowSet, given static SQL. + *

    Uses a JDBC Statement, not a PreparedStatement. If you want to + * execute a static query with a PreparedStatement, use the overloaded + * {@code queryForRowSet} method with {@code null} as argument array. + *

    The results will be mapped to an SqlRowSet which holds the data in a + * disconnected fashion. This wrapper will translate any SQLExceptions thrown. + *

    Note that, for the default implementation, JDBC RowSet support needs to + * be available at runtime: by default, Sun's {@code com.sun.rowset.CachedRowSetImpl} + * class is used, which is part of JDK 1.5+ and also available separately as part of + * Sun's JDBC RowSet Implementations download (rowset.jar). + * @param sql the SQL query to execute + * @return an SqlRowSet representation (possibly a wrapper around a + * {@code javax.sql.rowset.CachedRowSet}) + * @throws DataAccessException if there is any problem executing the query + * @see #queryForRowSet(String, Object...) + * @see SqlRowSetResultSetExtractor + * @see javax.sql.rowset.CachedRowSet + */ + SqlRowSet queryForRowSet(String sql) throws DataAccessException; + + /** + * Issue a single SQL update operation (such as an insert, update or delete statement). + * @param sql static SQL to execute + * @return the number of rows affected + * @throws DataAccessException if there is any problem. + */ + int update(String sql) throws DataAccessException; + + /** + * Issue multiple SQL updates on a single JDBC Statement using batching. + *

    Will fall back to separate updates on a single Statement if the JDBC + * driver does not support batch updates. + * @param sql defining an array of SQL statements that will be executed. + * @return an array of the number of rows affected by each statement + * @throws DataAccessException if there is any problem executing the batch + */ + int[] batchUpdate(String... sql) throws DataAccessException; + + + //------------------------------------------------------------------------- + // Methods dealing with prepared statements + //------------------------------------------------------------------------- + + /** + * Execute a JDBC data access operation, implemented as callback action + * working on a JDBC PreparedStatement. This allows for implementing arbitrary + * data access operations on a single Statement, within Spring's managed JDBC + * environment: that is, participating in Spring-managed transactions and + * converting JDBC SQLExceptions into Spring's DataAccessException hierarchy. + *

    The callback action can return a result object, for example a domain + * object or a collection of domain objects. + * @param psc a callback that creates a PreparedStatement given a Connection + * @param action a callback that specifies the action + * @return a result object returned by the action, or {@code null} if none + * @throws DataAccessException if there is any problem + */ + @Nullable + T execute(PreparedStatementCreator psc, PreparedStatementCallback action) throws DataAccessException; + + /** + * Execute a JDBC data access operation, implemented as callback action + * working on a JDBC PreparedStatement. This allows for implementing arbitrary + * data access operations on a single Statement, within Spring's managed JDBC + * environment: that is, participating in Spring-managed transactions and + * converting JDBC SQLExceptions into Spring's DataAccessException hierarchy. + *

    The callback action can return a result object, for example a domain + * object or a collection of domain objects. + * @param sql the SQL to execute + * @param action a callback that specifies the action + * @return a result object returned by the action, or {@code null} if none + * @throws DataAccessException if there is any problem + */ + @Nullable + T execute(String sql, PreparedStatementCallback action) throws DataAccessException; + + /** + * Query using a prepared statement, reading the ResultSet with a ResultSetExtractor. + *

    A PreparedStatementCreator can either be implemented directly or + * configured through a PreparedStatementCreatorFactory. + * @param psc a callback that creates a PreparedStatement given a Connection + * @param rse a callback that will extract results + * @return an arbitrary result object, as returned by the ResultSetExtractor + * @throws DataAccessException if there is any problem + * @see PreparedStatementCreatorFactory + */ + @Nullable + T query(PreparedStatementCreator psc, ResultSetExtractor rse) throws DataAccessException; + + /** + * Query using a prepared statement, reading the ResultSet with a ResultSetExtractor. + * @param sql the SQL query to execute + * @param pss a callback that knows how to set values on the prepared statement. + * If this is {@code null}, the SQL will be assumed to contain no bind parameters. + * Even if there are no bind parameters, this callback may be used to set the + * fetch size and other performance options. + * @param rse a callback that will extract results + * @return an arbitrary result object, as returned by the ResultSetExtractor + * @throws DataAccessException if there is any problem + */ + @Nullable + T query(String sql, @Nullable PreparedStatementSetter pss, ResultSetExtractor rse) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list of arguments + * to bind to the query, reading the ResultSet with a ResultSetExtractor. + * @param sql the SQL query to execute + * @param args arguments to bind to the query + * @param argTypes the SQL types of the arguments + * (constants from {@code java.sql.Types}) + * @param rse a callback that will extract results + * @return an arbitrary result object, as returned by the ResultSetExtractor + * @throws DataAccessException if the query fails + * @see java.sql.Types + */ + @Nullable + T query(String sql, Object[] args, int[] argTypes, ResultSetExtractor rse) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list of arguments + * to bind to the query, reading the ResultSet with a ResultSetExtractor. + * @param sql the SQL query to execute + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type); + * may also contain {@link SqlParameterValue} objects which indicate not + * only the argument value but also the SQL type and optionally the scale + * @param rse a callback that will extract results + * @return an arbitrary result object, as returned by the ResultSetExtractor + * @throws DataAccessException if the query fails + * @deprecated as of 5.3, in favor of {@link #query(String, ResultSetExtractor, Object...)} + */ + @Deprecated + @Nullable + T query(String sql, @Nullable Object[] args, ResultSetExtractor rse) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list of arguments + * to bind to the query, reading the ResultSet with a ResultSetExtractor. + * @param sql the SQL query to execute + * @param rse a callback that will extract results + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type); + * may also contain {@link SqlParameterValue} objects which indicate not + * only the argument value but also the SQL type and optionally the scale + * @return an arbitrary result object, as returned by the ResultSetExtractor + * @throws DataAccessException if the query fails + * @since 3.0.1 + */ + @Nullable + T query(String sql, ResultSetExtractor rse, @Nullable Object... args) throws DataAccessException; + + /** + * Query using a prepared statement, reading the ResultSet on a per-row basis + * with a RowCallbackHandler. + *

    A PreparedStatementCreator can either be implemented directly or + * configured through a PreparedStatementCreatorFactory. + * @param psc a callback that creates a PreparedStatement given a Connection + * @param rch a callback that will extract results, one row at a time + * @throws DataAccessException if there is any problem + * @see PreparedStatementCreatorFactory + */ + void query(PreparedStatementCreator psc, RowCallbackHandler rch) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * PreparedStatementSetter implementation that knows how to bind values to the + * query, reading the ResultSet on a per-row basis with a RowCallbackHandler. + * @param sql the SQL query to execute + * @param pss a callback that knows how to set values on the prepared statement. + * If this is {@code null}, the SQL will be assumed to contain no bind parameters. + * Even if there are no bind parameters, this callback may be used to set the + * fetch size and other performance options. + * @param rch a callback that will extract results, one row at a time + * @throws DataAccessException if the query fails + */ + void query(String sql, @Nullable PreparedStatementSetter pss, RowCallbackHandler rch) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list of + * arguments to bind to the query, reading the ResultSet on a per-row basis + * with a RowCallbackHandler. + * @param sql the SQL query to execute + * @param args arguments to bind to the query + * @param argTypes the SQL types of the arguments + * (constants from {@code java.sql.Types}) + * @param rch a callback that will extract results, one row at a time + * @throws DataAccessException if the query fails + * @see java.sql.Types + */ + void query(String sql, Object[] args, int[] argTypes, RowCallbackHandler rch) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list of + * arguments to bind to the query, reading the ResultSet on a per-row basis + * with a RowCallbackHandler. + * @param sql the SQL query to execute + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type); + * may also contain {@link SqlParameterValue} objects which indicate not + * only the argument value but also the SQL type and optionally the scale + * @param rch a callback that will extract results, one row at a time + * @throws DataAccessException if the query fails + * @deprecated as of 5.3, in favor of {@link #query(String, RowCallbackHandler, Object...)} + */ + @Deprecated + void query(String sql, @Nullable Object[] args, RowCallbackHandler rch) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list of + * arguments to bind to the query, reading the ResultSet on a per-row basis + * with a RowCallbackHandler. + * @param sql the SQL query to execute + * @param rch a callback that will extract results, one row at a time + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type); + * may also contain {@link SqlParameterValue} objects which indicate not + * only the argument value but also the SQL type and optionally the scale + * @throws DataAccessException if the query fails + * @since 3.0.1 + */ + void query(String sql, RowCallbackHandler rch, @Nullable Object... args) throws DataAccessException; + + /** + * Query using a prepared statement, mapping each row to a result object + * via a RowMapper. + *

    A PreparedStatementCreator can either be implemented directly or + * configured through a PreparedStatementCreatorFactory. + * @param psc a callback that creates a PreparedStatement given a Connection + * @param rowMapper a callback that will map one object per row + * @return the result List, containing mapped objects + * @throws DataAccessException if there is any problem + * @see PreparedStatementCreatorFactory + */ + List query(PreparedStatementCreator psc, RowMapper rowMapper) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * PreparedStatementSetter implementation that knows how to bind values + * to the query, mapping each row to a result object via a RowMapper. + * @param sql the SQL query to execute + * @param pss a callback that knows how to set values on the prepared statement. + * If this is {@code null}, the SQL will be assumed to contain no bind parameters. + * Even if there are no bind parameters, this callback may be used to set the + * fetch size and other performance options. + * @param rowMapper a callback that will map one object per row + * @return the result List, containing mapped objects + * @throws DataAccessException if the query fails + */ + List query(String sql, @Nullable PreparedStatementSetter pss, RowMapper rowMapper) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list of + * arguments to bind to the query, mapping each row to a result object + * via a RowMapper. + * @param sql the SQL query to execute + * @param args arguments to bind to the query + * @param argTypes the SQL types of the arguments + * (constants from {@code java.sql.Types}) + * @param rowMapper a callback that will map one object per row + * @return the result List, containing mapped objects + * @throws DataAccessException if the query fails + * @see java.sql.Types + */ + List query(String sql, Object[] args, int[] argTypes, RowMapper rowMapper) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list of + * arguments to bind to the query, mapping each row to a result object + * via a RowMapper. + * @param sql the SQL query to execute + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type); + * may also contain {@link SqlParameterValue} objects which indicate not + * only the argument value but also the SQL type and optionally the scale + * @param rowMapper a callback that will map one object per row + * @return the result List, containing mapped objects + * @throws DataAccessException if the query fails + * @deprecated as of 5.3, in favor of {@link #query(String, RowMapper, Object...)} + */ + @Deprecated + List query(String sql, @Nullable Object[] args, RowMapper rowMapper) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list of + * arguments to bind to the query, mapping each row to a result object + * via a RowMapper. + * @param sql the SQL query to execute + * @param rowMapper a callback that will map one object per row + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type); + * may also contain {@link SqlParameterValue} objects which indicate not + * only the argument value but also the SQL type and optionally the scale + * @return the result List, containing mapped objects + * @throws DataAccessException if the query fails + * @since 3.0.1 + */ + List query(String sql, RowMapper rowMapper, @Nullable Object... args) throws DataAccessException; + + /** + * Query using a prepared statement, mapping each row to a result object + * via a RowMapper, and turning it into an iterable and closeable Stream. + *

    A PreparedStatementCreator can either be implemented directly or + * configured through a PreparedStatementCreatorFactory. + * @param psc a callback that creates a PreparedStatement given a Connection + * @param rowMapper a callback that will map one object per row + * @return the result Stream, containing mapped objects, needing to be + * closed once fully processed (e.g. through a try-with-resources clause) + * @throws DataAccessException if there is any problem + * @see PreparedStatementCreatorFactory + * @since 5.3 + */ + Stream queryForStream(PreparedStatementCreator psc, RowMapper rowMapper) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * PreparedStatementSetter implementation that knows how to bind values + * to the query, mapping each row to a result object via a RowMapper, + * and turning it into an iterable and closeable Stream. + * @param sql the SQL query to execute + * @param pss a callback that knows how to set values on the prepared statement. + * If this is {@code null}, the SQL will be assumed to contain no bind parameters. + * Even if there are no bind parameters, this callback may be used to set the + * fetch size and other performance options. + * @param rowMapper a callback that will map one object per row + * @return the result Stream, containing mapped objects, needing to be + * closed once fully processed (e.g. through a try-with-resources clause) + * @throws DataAccessException if the query fails + * @since 5.3 + */ + Stream queryForStream(String sql, @Nullable PreparedStatementSetter pss, RowMapper rowMapper) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list of + * arguments to bind to the query, mapping each row to a result object + * via a RowMapper, and turning it into an iterable and closeable Stream. + * @param sql the SQL query to execute + * @param rowMapper a callback that will map one object per row + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type); + * may also contain {@link SqlParameterValue} objects which indicate not + * only the argument value but also the SQL type and optionally the scale + * @return the result Stream, containing mapped objects, needing to be + * closed once fully processed (e.g. through a try-with-resources clause) + * @throws DataAccessException if the query fails + * @since 5.3 + */ + Stream queryForStream(String sql, RowMapper rowMapper, @Nullable Object... args) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list + * of arguments to bind to the query, mapping a single result row to a + * result object via a RowMapper. + * @param sql the SQL query to execute + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type) + * @param argTypes the SQL types of the arguments + * (constants from {@code java.sql.Types}) + * @param rowMapper a callback that will map one object per row + * @return the single mapped object (may be {@code null} if the given + * {@link RowMapper} returned {@code} null) + * @throws IncorrectResultSizeDataAccessException if the query does not + * return exactly one row + * @throws DataAccessException if the query fails + */ + @Nullable + T queryForObject(String sql, Object[] args, int[] argTypes, RowMapper rowMapper) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list + * of arguments to bind to the query, mapping a single result row to a + * result object via a RowMapper. + * @param sql the SQL query to execute + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type); + * may also contain {@link SqlParameterValue} objects which indicate not + * only the argument value but also the SQL type and optionally the scale + * @param rowMapper a callback that will map one object per row + * @return the single mapped object (may be {@code null} if the given + * {@link RowMapper} returned {@code} null) + * @throws IncorrectResultSizeDataAccessException if the query does not + * return exactly one row + * @throws DataAccessException if the query fails + * @deprecated as of 5.3, in favor of {@link #queryForObject(String, RowMapper, Object...)} + */ + @Deprecated + @Nullable + T queryForObject(String sql, @Nullable Object[] args, RowMapper rowMapper) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list + * of arguments to bind to the query, mapping a single result row to a + * result object via a RowMapper. + * @param sql the SQL query to execute + * @param rowMapper a callback that will map one object per row + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type); + * may also contain {@link SqlParameterValue} objects which indicate not + * only the argument value but also the SQL type and optionally the scale + * @return the single mapped object (may be {@code null} if the given + * {@link RowMapper} returned {@code} null) + * @throws IncorrectResultSizeDataAccessException if the query does not + * return exactly one row + * @throws DataAccessException if the query fails + * @since 3.0.1 + */ + @Nullable + T queryForObject(String sql, RowMapper rowMapper, @Nullable Object... args) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list of + * arguments to bind to the query, expecting a result object. + *

    The query is expected to be a single row/single column query; the returned + * result will be directly mapped to the corresponding object type. + * @param sql the SQL query to execute + * @param args arguments to bind to the query + * @param argTypes the SQL types of the arguments + * (constants from {@code java.sql.Types}) + * @param requiredType the type that the result object is expected to match + * @return the result object of the required type, or {@code null} in case of SQL NULL + * @throws IncorrectResultSizeDataAccessException if the query does not return + * exactly one row, or does not return exactly one column in that row + * @throws DataAccessException if the query fails + * @see #queryForObject(String, Class) + * @see java.sql.Types + */ + @Nullable + T queryForObject(String sql, Object[] args, int[] argTypes, Class requiredType) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list of + * arguments to bind to the query, expecting a result object. + *

    The query is expected to be a single row/single column query; the returned + * result will be directly mapped to the corresponding object type. + * @param sql the SQL query to execute + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type); + * may also contain {@link SqlParameterValue} objects which indicate not + * only the argument value but also the SQL type and optionally the scale + * @param requiredType the type that the result object is expected to match + * @return the result object of the required type, or {@code null} in case of SQL NULL + * @throws IncorrectResultSizeDataAccessException if the query does not return + * exactly one row, or does not return exactly one column in that row + * @throws DataAccessException if the query fails + * @see #queryForObject(String, Class) + * @deprecated as of 5.3, in favor of {@link #queryForObject(String, Class, Object...)} + */ + @Deprecated + @Nullable + T queryForObject(String sql, @Nullable Object[] args, Class requiredType) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list of + * arguments to bind to the query, expecting a result object. + *

    The query is expected to be a single row/single column query; the returned + * result will be directly mapped to the corresponding object type. + * @param sql the SQL query to execute + * @param requiredType the type that the result object is expected to match + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type); + * may also contain {@link SqlParameterValue} objects which indicate not + * only the argument value but also the SQL type and optionally the scale + * @return the result object of the required type, or {@code null} in case of SQL NULL + * @throws IncorrectResultSizeDataAccessException if the query does not return + * exactly one row, or does not return exactly one column in that row + * @throws DataAccessException if the query fails + * @since 3.0.1 + * @see #queryForObject(String, Class) + */ + @Nullable + T queryForObject(String sql, Class requiredType, @Nullable Object... args) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list of + * arguments to bind to the query, expecting a result map. + *

    The query is expected to be a single row query; the result row will be + * mapped to a Map (one entry for each column, using the column name as the key). + * @param sql the SQL query to execute + * @param args arguments to bind to the query + * @param argTypes the SQL types of the arguments + * (constants from {@code java.sql.Types}) + * @return the result Map (one entry per column, with column name as key) + * @throws IncorrectResultSizeDataAccessException if the query does not + * return exactly one row + * @throws DataAccessException if the query fails + * @see #queryForMap(String) + * @see ColumnMapRowMapper + * @see java.sql.Types + */ + Map queryForMap(String sql, Object[] args, int[] argTypes) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list of + * arguments to bind to the query, expecting a result map. + *

    The {@code queryForMap} methods defined by this interface are appropriate + * when you don't have a domain model. Otherwise, consider using one of the + * {@code queryForObject} methods. + *

    The query is expected to be a single row query; the result row will be + * mapped to a Map (one entry for each column, using the column name as the key). + * @param sql the SQL query to execute + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type); + * may also contain {@link SqlParameterValue} objects which indicate not + * only the argument value but also the SQL type and optionally the scale + * @return the result Map (one entry for each column, using the + * column name as the key) + * @throws IncorrectResultSizeDataAccessException if the query does not + * return exactly one row + * @throws DataAccessException if the query fails + * @see #queryForMap(String) + * @see ColumnMapRowMapper + */ + Map queryForMap(String sql, @Nullable Object... args) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list of + * arguments to bind to the query, expecting a result list. + *

    The results will be mapped to a List (one entry for each row) of + * result objects, each of them matching the specified element type. + * @param sql the SQL query to execute + * @param args arguments to bind to the query + * @param argTypes the SQL types of the arguments + * (constants from {@code java.sql.Types}) + * @param elementType the required type of element in the result list + * (for example, {@code Integer.class}) + * @return a List of objects that match the specified element type + * @throws DataAccessException if the query fails + * @see #queryForList(String, Class) + * @see SingleColumnRowMapper + */ + List queryForList(String sql, Object[] args, int[] argTypes, Class elementType) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list of + * arguments to bind to the query, expecting a result list. + *

    The results will be mapped to a List (one entry for each row) of + * result objects, each of them matching the specified element type. + * @param sql the SQL query to execute + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type); + * may also contain {@link SqlParameterValue} objects which indicate not + * only the argument value but also the SQL type and optionally the scale + * @param elementType the required type of element in the result list + * (for example, {@code Integer.class}) + * @return a List of objects that match the specified element type + * @throws DataAccessException if the query fails + * @see #queryForList(String, Class) + * @see SingleColumnRowMapper + * @deprecated as of 5.3, in favor of {@link #queryForList(String, Class, Object...)} + */ + @Deprecated + List queryForList(String sql, @Nullable Object[] args, Class elementType) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list of + * arguments to bind to the query, expecting a result list. + *

    The results will be mapped to a List (one entry for each row) of + * result objects, each of them matching the specified element type. + * @param sql the SQL query to execute + * @param elementType the required type of element in the result list + * (for example, {@code Integer.class}) + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type); + * may also contain {@link SqlParameterValue} objects which indicate not + * only the argument value but also the SQL type and optionally the scale + * @return a List of objects that match the specified element type + * @throws DataAccessException if the query fails + * @since 3.0.1 + * @see #queryForList(String, Class) + * @see SingleColumnRowMapper + */ + List queryForList(String sql, Class elementType, @Nullable Object... args) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list of + * arguments to bind to the query, expecting a result list. + *

    The results will be mapped to a List (one entry for each row) of + * Maps (one entry for each column, using the column name as the key). + * Each element in the list will be of the form returned by this interface's + * {@code queryForMap} methods. + * @param sql the SQL query to execute + * @param args arguments to bind to the query + * @param argTypes the SQL types of the arguments + * (constants from {@code java.sql.Types}) + * @return a List that contains a Map per row + * @throws DataAccessException if the query fails + * @see #queryForList(String) + * @see java.sql.Types + */ + List> queryForList(String sql, Object[] args, int[] argTypes) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list of + * arguments to bind to the query, expecting a result list. + *

    The results will be mapped to a List (one entry for each row) of + * Maps (one entry for each column, using the column name as the key). + * Each element in the list will be of the form returned by this interface's + * {@code queryForMap} methods. + * @param sql the SQL query to execute + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type); + * may also contain {@link SqlParameterValue} objects which indicate not + * only the argument value but also the SQL type and optionally the scale + * @return a List that contains a Map per row + * @throws DataAccessException if the query fails + * @see #queryForList(String) + */ + List> queryForList(String sql, @Nullable Object... args) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list of + * arguments to bind to the query, expecting an SqlRowSet. + *

    The results will be mapped to an SqlRowSet which holds the data in a + * disconnected fashion. This wrapper will translate any SQLExceptions thrown. + *

    Note that, for the default implementation, JDBC RowSet support needs to + * be available at runtime: by default, Sun's {@code com.sun.rowset.CachedRowSetImpl} + * class is used, which is part of JDK 1.5+ and also available separately as part of + * Sun's JDBC RowSet Implementations download (rowset.jar). + * @param sql the SQL query to execute + * @param args arguments to bind to the query + * @param argTypes the SQL types of the arguments + * (constants from {@code java.sql.Types}) + * @return an SqlRowSet representation (possibly a wrapper around a + * {@code javax.sql.rowset.CachedRowSet}) + * @throws DataAccessException if there is any problem executing the query + * @see #queryForRowSet(String) + * @see SqlRowSetResultSetExtractor + * @see javax.sql.rowset.CachedRowSet + * @see java.sql.Types + */ + SqlRowSet queryForRowSet(String sql, Object[] args, int[] argTypes) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list of + * arguments to bind to the query, expecting an SqlRowSet. + *

    The results will be mapped to an SqlRowSet which holds the data in a + * disconnected fashion. This wrapper will translate any SQLExceptions thrown. + *

    Note that, for the default implementation, JDBC RowSet support needs to + * be available at runtime: by default, Sun's {@code com.sun.rowset.CachedRowSetImpl} + * class is used, which is part of JDK 1.5+ and also available separately as part of + * Sun's JDBC RowSet Implementations download (rowset.jar). + * @param sql the SQL query to execute + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type); + * may also contain {@link SqlParameterValue} objects which indicate not + * only the argument value but also the SQL type and optionally the scale + * @return an SqlRowSet representation (possibly a wrapper around a + * {@code javax.sql.rowset.CachedRowSet}) + * @throws DataAccessException if there is any problem executing the query + * @see #queryForRowSet(String) + * @see SqlRowSetResultSetExtractor + * @see javax.sql.rowset.CachedRowSet + */ + SqlRowSet queryForRowSet(String sql, @Nullable Object... args) throws DataAccessException; + + /** + * Issue a single SQL update operation (such as an insert, update or delete + * statement) using a PreparedStatementCreator to provide SQL and any + * required parameters. + *

    A PreparedStatementCreator can either be implemented directly or + * configured through a PreparedStatementCreatorFactory. + * @param psc a callback that provides SQL and any necessary parameters + * @return the number of rows affected + * @throws DataAccessException if there is any problem issuing the update + * @see PreparedStatementCreatorFactory + */ + int update(PreparedStatementCreator psc) throws DataAccessException; + + /** + * Issue an update statement using a PreparedStatementCreator to provide SQL and + * any required parameters. Generated keys will be put into the given KeyHolder. + *

    Note that the given PreparedStatementCreator has to create a statement + * with activated extraction of generated keys (a JDBC 3.0 feature). This can + * either be done directly or through using a PreparedStatementCreatorFactory. + * @param psc a callback that provides SQL and any necessary parameters + * @param generatedKeyHolder a KeyHolder that will hold the generated keys + * @return the number of rows affected + * @throws DataAccessException if there is any problem issuing the update + * @see PreparedStatementCreatorFactory + * @see org.springframework.jdbc.support.GeneratedKeyHolder + */ + int update(PreparedStatementCreator psc, KeyHolder generatedKeyHolder) throws DataAccessException; + + /** + * Issue an update statement using a PreparedStatementSetter to set bind parameters, + * with given SQL. Simpler than using a PreparedStatementCreator as this method + * will create the PreparedStatement: The PreparedStatementSetter just needs to + * set parameters. + * @param sql the SQL containing bind parameters + * @param pss helper that sets bind parameters. If this is {@code null} + * we run an update with static SQL. + * @return the number of rows affected + * @throws DataAccessException if there is any problem issuing the update + */ + int update(String sql, @Nullable PreparedStatementSetter pss) throws DataAccessException; + + /** + * Issue a single SQL update operation (such as an insert, update or delete statement) + * via a prepared statement, binding the given arguments. + * @param sql the SQL containing bind parameters + * @param args arguments to bind to the query + * @param argTypes the SQL types of the arguments + * (constants from {@code java.sql.Types}) + * @return the number of rows affected + * @throws DataAccessException if there is any problem issuing the update + * @see java.sql.Types + */ + int update(String sql, Object[] args, int[] argTypes) throws DataAccessException; + + /** + * Issue a single SQL update operation (such as an insert, update or delete statement) + * via a prepared statement, binding the given arguments. + * @param sql the SQL containing bind parameters + * @param args arguments to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type); + * may also contain {@link SqlParameterValue} objects which indicate not + * only the argument value but also the SQL type and optionally the scale + * @return the number of rows affected + * @throws DataAccessException if there is any problem issuing the update + */ + int update(String sql, @Nullable Object... args) throws DataAccessException; + + /** + * Issue multiple update statements on a single PreparedStatement, + * using batch updates and a BatchPreparedStatementSetter to set values. + *

    Will fall back to separate updates on a single PreparedStatement + * if the JDBC driver does not support batch updates. + * @param sql defining PreparedStatement that will be reused. + * All statements in the batch will use the same SQL. + * @param pss object to set parameters on the PreparedStatement + * created by this method + * @return an array of the number of rows affected by each statement + * (may also contain special JDBC-defined negative values for affected rows such as + * {@link java.sql.Statement#SUCCESS_NO_INFO}/{@link java.sql.Statement#EXECUTE_FAILED}) + * @throws DataAccessException if there is any problem issuing the update + */ + int[] batchUpdate(String sql, BatchPreparedStatementSetter pss) throws DataAccessException; + + /** + * Execute a batch using the supplied SQL statement with the batch of supplied arguments. + * @param sql the SQL statement to execute + * @param batchArgs the List of Object arrays containing the batch of arguments for the query + * @return an array containing the numbers of rows affected by each update in the batch + * (may also contain special JDBC-defined negative values for affected rows such as + * {@link java.sql.Statement#SUCCESS_NO_INFO}/{@link java.sql.Statement#EXECUTE_FAILED}) + * @throws DataAccessException if there is any problem issuing the update + */ + int[] batchUpdate(String sql, List batchArgs) throws DataAccessException; + + /** + * Execute a batch using the supplied SQL statement with the batch of supplied arguments. + * @param sql the SQL statement to execute. + * @param batchArgs the List of Object arrays containing the batch of arguments for the query + * @param argTypes the SQL types of the arguments + * (constants from {@code java.sql.Types}) + * @return an array containing the numbers of rows affected by each update in the batch + * (may also contain special JDBC-defined negative values for affected rows such as + * {@link java.sql.Statement#SUCCESS_NO_INFO}/{@link java.sql.Statement#EXECUTE_FAILED}) + * @throws DataAccessException if there is any problem issuing the update + */ + int[] batchUpdate(String sql, List batchArgs, int[] argTypes) throws DataAccessException; + + /** + * Execute multiple batches using the supplied SQL statement with the collect of supplied + * arguments. The arguments' values will be set using the ParameterizedPreparedStatementSetter. + * Each batch should be of size indicated in 'batchSize'. + * @param sql the SQL statement to execute. + * @param batchArgs the List of Object arrays containing the batch of arguments for the query + * @param batchSize batch size + * @param pss the ParameterizedPreparedStatementSetter to use + * @return an array containing for each batch another array containing the numbers of + * rows affected by each update in the batch + * (may also contain special JDBC-defined negative values for affected rows such as + * {@link java.sql.Statement#SUCCESS_NO_INFO}/{@link java.sql.Statement#EXECUTE_FAILED}) + * @throws DataAccessException if there is any problem issuing the update + * @since 3.1 + */ + int[][] batchUpdate(String sql, Collection batchArgs, int batchSize, + ParameterizedPreparedStatementSetter pss) throws DataAccessException; + + + //------------------------------------------------------------------------- + // Methods dealing with callable statements + //------------------------------------------------------------------------- + + /** + * Execute a JDBC data access operation, implemented as callback action + * working on a JDBC CallableStatement. This allows for implementing arbitrary + * data access operations on a single Statement, within Spring's managed JDBC + * environment: that is, participating in Spring-managed transactions and + * converting JDBC SQLExceptions into Spring's DataAccessException hierarchy. + *

    The callback action can return a result object, for example a domain + * object or a collection of domain objects. + * @param csc a callback that creates a CallableStatement given a Connection + * @param action a callback that specifies the action + * @return a result object returned by the action, or {@code null} if none + * @throws DataAccessException if there is any problem + */ + @Nullable + T execute(CallableStatementCreator csc, CallableStatementCallback action) throws DataAccessException; + + /** + * Execute a JDBC data access operation, implemented as callback action + * working on a JDBC CallableStatement. This allows for implementing arbitrary + * data access operations on a single Statement, within Spring's managed JDBC + * environment: that is, participating in Spring-managed transactions and + * converting JDBC SQLExceptions into Spring's DataAccessException hierarchy. + *

    The callback action can return a result object, for example a domain + * object or a collection of domain objects. + * @param callString the SQL call string to execute + * @param action a callback that specifies the action + * @return a result object returned by the action, or {@code null} if none + * @throws DataAccessException if there is any problem + */ + @Nullable + T execute(String callString, CallableStatementCallback action) throws DataAccessException; + + /** + * Execute an SQL call using a CallableStatementCreator to provide SQL and + * any required parameters. + * @param csc a callback that provides SQL and any necessary parameters + * @param declaredParameters list of declared SqlParameter objects + * @return a Map of extracted out parameters + * @throws DataAccessException if there is any problem issuing the update + */ + Map call(CallableStatementCreator csc, List declaredParameters) + throws DataAccessException; + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java new file mode 100644 index 0000000..f41390c --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -0,0 +1,1755 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.sql.BatchUpdateException; +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Spliterator; +import java.util.function.Consumer; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import javax.sql.DataSource; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.dao.support.DataAccessUtils; +import org.springframework.jdbc.InvalidResultSetAccessException; +import org.springframework.jdbc.SQLWarningException; +import org.springframework.jdbc.UncategorizedSQLException; +import org.springframework.jdbc.datasource.ConnectionProxy; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.jdbc.support.JdbcAccessor; +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.jdbc.support.rowset.SqlRowSet; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedCaseInsensitiveMap; +import org.springframework.util.StringUtils; + +/** + * This is the central class in the JDBC core package. + * It simplifies the use of JDBC and helps to avoid common errors. + * It executes core JDBC workflow, leaving application code to provide SQL + * and extract results. This class executes SQL queries or updates, initiating + * iteration over ResultSets and catching JDBC exceptions and translating + * them to the generic, more informative exception hierarchy defined in the + * {@code org.springframework.dao} package. + * + *

    Code using this class need only implement callback interfaces, giving + * them a clearly defined contract. The {@link PreparedStatementCreator} callback + * interface creates a prepared statement given a Connection, providing SQL and + * any necessary parameters. The {@link ResultSetExtractor} interface extracts + * values from a ResultSet. See also {@link PreparedStatementSetter} and + * {@link RowMapper} for two popular alternative callback interfaces. + * + *

    Can be used within a service implementation via direct instantiation + * with a DataSource reference, or get prepared in an application context + * and given to services as bean reference. Note: The DataSource should + * always be configured as a bean in the application context, in the first case + * given to the service directly, in the second case to the prepared template. + * + *

    Because this class is parameterizable by the callback interfaces and + * the {@link org.springframework.jdbc.support.SQLExceptionTranslator} + * interface, there should be no need to subclass it. + * + *

    All SQL operations performed by this class are logged at debug level, + * using "org.springframework.jdbc.core.JdbcTemplate" as log category. + * + *

    NOTE: An instance of this class is thread-safe once configured. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Thomas Risberg + * @since May 3, 2001 + * @see PreparedStatementCreator + * @see PreparedStatementSetter + * @see CallableStatementCreator + * @see PreparedStatementCallback + * @see CallableStatementCallback + * @see ResultSetExtractor + * @see RowCallbackHandler + * @see RowMapper + * @see org.springframework.jdbc.support.SQLExceptionTranslator + */ +public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { + + private static final String RETURN_RESULT_SET_PREFIX = "#result-set-"; + + private static final String RETURN_UPDATE_COUNT_PREFIX = "#update-count-"; + + + /** If this variable is false, we will throw exceptions on SQL warnings. */ + private boolean ignoreWarnings = true; + + /** + * If this variable is set to a non-negative value, it will be used for setting the + * fetchSize property on statements used for query processing. + */ + private int fetchSize = -1; + + /** + * If this variable is set to a non-negative value, it will be used for setting the + * maxRows property on statements used for query processing. + */ + private int maxRows = -1; + + /** + * If this variable is set to a non-negative value, it will be used for setting the + * queryTimeout property on statements used for query processing. + */ + private int queryTimeout = -1; + + /** + * If this variable is set to true, then all results checking will be bypassed for any + * callable statement processing. This can be used to avoid a bug in some older Oracle + * JDBC drivers like 10.1.0.2. + */ + private boolean skipResultsProcessing = false; + + /** + * If this variable is set to true then all results from a stored procedure call + * that don't have a corresponding SqlOutParameter declaration will be bypassed. + * All other results processing will be take place unless the variable + * {@code skipResultsProcessing} is set to {@code true}. + */ + private boolean skipUndeclaredResults = false; + + /** + * If this variable is set to true then execution of a CallableStatement will return + * the results in a Map that uses case insensitive names for the parameters. + */ + private boolean resultsMapCaseInsensitive = false; + + + /** + * Construct a new JdbcTemplate for bean usage. + *

    Note: The DataSource has to be set before using the instance. + * @see #setDataSource + */ + public JdbcTemplate() { + } + + /** + * Construct a new JdbcTemplate, given a DataSource to obtain connections from. + *

    Note: This will not trigger initialization of the exception translator. + * @param dataSource the JDBC DataSource to obtain connections from + */ + public JdbcTemplate(DataSource dataSource) { + setDataSource(dataSource); + afterPropertiesSet(); + } + + /** + * Construct a new JdbcTemplate, given a DataSource to obtain connections from. + *

    Note: Depending on the "lazyInit" flag, initialization of the exception translator + * will be triggered. + * @param dataSource the JDBC DataSource to obtain connections from + * @param lazyInit whether to lazily initialize the SQLExceptionTranslator + */ + public JdbcTemplate(DataSource dataSource, boolean lazyInit) { + setDataSource(dataSource); + setLazyInit(lazyInit); + afterPropertiesSet(); + } + + + /** + * Set whether or not we want to ignore SQLWarnings. + *

    Default is "true", swallowing and logging all warnings. Switch this flag + * to "false" to make the JdbcTemplate throw an SQLWarningException instead. + * @see java.sql.SQLWarning + * @see org.springframework.jdbc.SQLWarningException + * @see #handleWarnings + */ + public void setIgnoreWarnings(boolean ignoreWarnings) { + this.ignoreWarnings = ignoreWarnings; + } + + /** + * Return whether or not we ignore SQLWarnings. + */ + public boolean isIgnoreWarnings() { + return this.ignoreWarnings; + } + + /** + * Set the fetch size for this JdbcTemplate. This is important for processing large + * result sets: Setting this higher than the default value will increase processing + * speed at the cost of memory consumption; setting this lower can avoid transferring + * row data that will never be read by the application. + *

    Default is -1, indicating to use the JDBC driver's default configuration + * (i.e. to not pass a specific fetch size setting on to the driver). + *

    Note: As of 4.3, negative values other than -1 will get passed on to the + * driver, since e.g. MySQL supports special behavior for {@code Integer.MIN_VALUE}. + * @see java.sql.Statement#setFetchSize + */ + public void setFetchSize(int fetchSize) { + this.fetchSize = fetchSize; + } + + /** + * Return the fetch size specified for this JdbcTemplate. + */ + public int getFetchSize() { + return this.fetchSize; + } + + /** + * Set the maximum number of rows for this JdbcTemplate. This is important for + * processing subsets of large result sets, avoiding to read and hold the entire + * result set in the database or in the JDBC driver if we're never interested in + * the entire result in the first place (for example, when performing searches + * that might return a large number of matches). + *

    Default is -1, indicating to use the JDBC driver's default configuration + * (i.e. to not pass a specific max rows setting on to the driver). + *

    Note: As of 4.3, negative values other than -1 will get passed on to the + * driver, in sync with {@link #setFetchSize}'s support for special MySQL values. + * @see java.sql.Statement#setMaxRows + */ + public void setMaxRows(int maxRows) { + this.maxRows = maxRows; + } + + /** + * Return the maximum number of rows specified for this JdbcTemplate. + */ + public int getMaxRows() { + return this.maxRows; + } + + /** + * Set the query timeout for statements that this JdbcTemplate executes. + *

    Default is -1, indicating to use the JDBC driver's default + * (i.e. to not pass a specific query timeout setting on the driver). + *

    Note: Any timeout specified here will be overridden by the remaining + * transaction timeout when executing within a transaction that has a + * timeout specified at the transaction level. + * @see java.sql.Statement#setQueryTimeout + */ + public void setQueryTimeout(int queryTimeout) { + this.queryTimeout = queryTimeout; + } + + /** + * Return the query timeout for statements that this JdbcTemplate executes. + */ + public int getQueryTimeout() { + return this.queryTimeout; + } + + /** + * Set whether results processing should be skipped. Can be used to optimize callable + * statement processing when we know that no results are being passed back - the processing + * of out parameter will still take place. This can be used to avoid a bug in some older + * Oracle JDBC drivers like 10.1.0.2. + */ + public void setSkipResultsProcessing(boolean skipResultsProcessing) { + this.skipResultsProcessing = skipResultsProcessing; + } + + /** + * Return whether results processing should be skipped. + */ + public boolean isSkipResultsProcessing() { + return this.skipResultsProcessing; + } + + /** + * Set whether undeclared results should be skipped. + */ + public void setSkipUndeclaredResults(boolean skipUndeclaredResults) { + this.skipUndeclaredResults = skipUndeclaredResults; + } + + /** + * Return whether undeclared results should be skipped. + */ + public boolean isSkipUndeclaredResults() { + return this.skipUndeclaredResults; + } + + /** + * Set whether execution of a CallableStatement will return the results in a Map + * that uses case insensitive names for the parameters. + */ + public void setResultsMapCaseInsensitive(boolean resultsMapCaseInsensitive) { + this.resultsMapCaseInsensitive = resultsMapCaseInsensitive; + } + + /** + * Return whether execution of a CallableStatement will return the results in a Map + * that uses case insensitive names for the parameters. + */ + public boolean isResultsMapCaseInsensitive() { + return this.resultsMapCaseInsensitive; + } + + + //------------------------------------------------------------------------- + // Methods dealing with a plain java.sql.Connection + //------------------------------------------------------------------------- + + @Override + @Nullable + public T execute(ConnectionCallback action) throws DataAccessException { + Assert.notNull(action, "Callback object must not be null"); + + Connection con = DataSourceUtils.getConnection(obtainDataSource()); + try { + // Create close-suppressing Connection proxy, also preparing returned Statements. + Connection conToUse = createConnectionProxy(con); + return action.doInConnection(conToUse); + } + catch (SQLException ex) { + // Release Connection early, to avoid potential connection pool deadlock + // in the case when the exception translator hasn't been initialized yet. + String sql = getSql(action); + DataSourceUtils.releaseConnection(con, getDataSource()); + con = null; + throw translateException("ConnectionCallback", sql, ex); + } + finally { + DataSourceUtils.releaseConnection(con, getDataSource()); + } + } + + /** + * Create a close-suppressing proxy for the given JDBC Connection. + * Called by the {@code execute} method. + *

    The proxy also prepares returned JDBC Statements, applying + * statement settings such as fetch size, max rows, and query timeout. + * @param con the JDBC Connection to create a proxy for + * @return the Connection proxy + * @see java.sql.Connection#close() + * @see #execute(ConnectionCallback) + * @see #applyStatementSettings + */ + protected Connection createConnectionProxy(Connection con) { + return (Connection) Proxy.newProxyInstance( + ConnectionProxy.class.getClassLoader(), + new Class[] {ConnectionProxy.class}, + new CloseSuppressingInvocationHandler(con)); + } + + + //------------------------------------------------------------------------- + // Methods dealing with static SQL (java.sql.Statement) + //------------------------------------------------------------------------- + + @Nullable + private T execute(StatementCallback action, boolean closeResources) throws DataAccessException { + Assert.notNull(action, "Callback object must not be null"); + + Connection con = DataSourceUtils.getConnection(obtainDataSource()); + Statement stmt = null; + try { + stmt = con.createStatement(); + applyStatementSettings(stmt); + T result = action.doInStatement(stmt); + handleWarnings(stmt); + return result; + } + catch (SQLException ex) { + // Release Connection early, to avoid potential connection pool deadlock + // in the case when the exception translator hasn't been initialized yet. + String sql = getSql(action); + JdbcUtils.closeStatement(stmt); + stmt = null; + DataSourceUtils.releaseConnection(con, getDataSource()); + con = null; + throw translateException("StatementCallback", sql, ex); + } + finally { + if (closeResources) { + JdbcUtils.closeStatement(stmt); + DataSourceUtils.releaseConnection(con, getDataSource()); + } + } + } + + @Override + @Nullable + public T execute(StatementCallback action) throws DataAccessException { + return execute(action, true); + } + + @Override + public void execute(final String sql) throws DataAccessException { + if (logger.isDebugEnabled()) { + logger.debug("Executing SQL statement [" + sql + "]"); + } + + /** + * Callback to execute the statement. + */ + class ExecuteStatementCallback implements StatementCallback, SqlProvider { + @Override + @Nullable + public Object doInStatement(Statement stmt) throws SQLException { + stmt.execute(sql); + return null; + } + @Override + public String getSql() { + return sql; + } + } + + execute(new ExecuteStatementCallback(), true); + } + + @Override + @Nullable + public T query(final String sql, final ResultSetExtractor rse) throws DataAccessException { + Assert.notNull(sql, "SQL must not be null"); + Assert.notNull(rse, "ResultSetExtractor must not be null"); + if (logger.isDebugEnabled()) { + logger.debug("Executing SQL query [" + sql + "]"); + } + + /** + * Callback to execute the query. + */ + class QueryStatementCallback implements StatementCallback, SqlProvider { + @Override + @Nullable + public T doInStatement(Statement stmt) throws SQLException { + ResultSet rs = null; + try { + rs = stmt.executeQuery(sql); + return rse.extractData(rs); + } + finally { + JdbcUtils.closeResultSet(rs); + } + } + @Override + public String getSql() { + return sql; + } + } + + return execute(new QueryStatementCallback(), true); + } + + @Override + public void query(String sql, RowCallbackHandler rch) throws DataAccessException { + query(sql, new RowCallbackHandlerResultSetExtractor(rch)); + } + + @Override + public List query(String sql, RowMapper rowMapper) throws DataAccessException { + return result(query(sql, new RowMapperResultSetExtractor<>(rowMapper))); + } + + @Override + public Stream queryForStream(String sql, RowMapper rowMapper) throws DataAccessException { + class StreamStatementCallback implements StatementCallback>, SqlProvider { + @Override + public Stream doInStatement(Statement stmt) throws SQLException { + ResultSet rs = stmt.executeQuery(sql); + Connection con = stmt.getConnection(); + return new ResultSetSpliterator<>(rs, rowMapper).stream().onClose(() -> { + JdbcUtils.closeResultSet(rs); + JdbcUtils.closeStatement(stmt); + DataSourceUtils.releaseConnection(con, getDataSource()); + }); + } + @Override + public String getSql() { + return sql; + } + } + + return result(execute(new StreamStatementCallback(), false)); + } + + @Override + public Map queryForMap(String sql) throws DataAccessException { + return result(queryForObject(sql, getColumnMapRowMapper())); + } + + @Override + @Nullable + public T queryForObject(String sql, RowMapper rowMapper) throws DataAccessException { + List results = query(sql, rowMapper); + return DataAccessUtils.nullableSingleResult(results); + } + + @Override + @Nullable + public T queryForObject(String sql, Class requiredType) throws DataAccessException { + return queryForObject(sql, getSingleColumnRowMapper(requiredType)); + } + + @Override + public List queryForList(String sql, Class elementType) throws DataAccessException { + return query(sql, getSingleColumnRowMapper(elementType)); + } + + @Override + public List> queryForList(String sql) throws DataAccessException { + return query(sql, getColumnMapRowMapper()); + } + + @Override + public SqlRowSet queryForRowSet(String sql) throws DataAccessException { + return result(query(sql, new SqlRowSetResultSetExtractor())); + } + + @Override + public int update(final String sql) throws DataAccessException { + Assert.notNull(sql, "SQL must not be null"); + if (logger.isDebugEnabled()) { + logger.debug("Executing SQL update [" + sql + "]"); + } + + /** + * Callback to execute the update statement. + */ + class UpdateStatementCallback implements StatementCallback, SqlProvider { + @Override + public Integer doInStatement(Statement stmt) throws SQLException { + int rows = stmt.executeUpdate(sql); + if (logger.isTraceEnabled()) { + logger.trace("SQL update affected " + rows + " rows"); + } + return rows; + } + @Override + public String getSql() { + return sql; + } + } + + return updateCount(execute(new UpdateStatementCallback(), true)); + } + + @Override + public int[] batchUpdate(final String... sql) throws DataAccessException { + Assert.notEmpty(sql, "SQL array must not be empty"); + if (logger.isDebugEnabled()) { + logger.debug("Executing SQL batch update of " + sql.length + " statements"); + } + + /** + * Callback to execute the batch update. + */ + class BatchUpdateStatementCallback implements StatementCallback, SqlProvider { + + @Nullable + private String currSql; + + @Override + public int[] doInStatement(Statement stmt) throws SQLException, DataAccessException { + int[] rowsAffected = new int[sql.length]; + if (JdbcUtils.supportsBatchUpdates(stmt.getConnection())) { + for (String sqlStmt : sql) { + this.currSql = appendSql(this.currSql, sqlStmt); + stmt.addBatch(sqlStmt); + } + try { + rowsAffected = stmt.executeBatch(); + } + catch (BatchUpdateException ex) { + String batchExceptionSql = null; + for (int i = 0; i < ex.getUpdateCounts().length; i++) { + if (ex.getUpdateCounts()[i] == Statement.EXECUTE_FAILED) { + batchExceptionSql = appendSql(batchExceptionSql, sql[i]); + } + } + if (StringUtils.hasLength(batchExceptionSql)) { + this.currSql = batchExceptionSql; + } + throw ex; + } + } + else { + for (int i = 0; i < sql.length; i++) { + this.currSql = sql[i]; + if (!stmt.execute(sql[i])) { + rowsAffected[i] = stmt.getUpdateCount(); + } + else { + throw new InvalidDataAccessApiUsageException("Invalid batch SQL statement: " + sql[i]); + } + } + } + return rowsAffected; + } + + private String appendSql(@Nullable String sql, String statement) { + return (StringUtils.hasLength(sql) ? sql + "; " + statement : statement); + } + + @Override + @Nullable + public String getSql() { + return this.currSql; + } + } + + int[] result = execute(new BatchUpdateStatementCallback(), true); + Assert.state(result != null, "No update counts"); + return result; + } + + + //------------------------------------------------------------------------- + // Methods dealing with prepared statements + //------------------------------------------------------------------------- + + @Nullable + private T execute(PreparedStatementCreator psc, PreparedStatementCallback action, boolean closeResources) + throws DataAccessException { + + Assert.notNull(psc, "PreparedStatementCreator must not be null"); + Assert.notNull(action, "Callback object must not be null"); + if (logger.isDebugEnabled()) { + String sql = getSql(psc); + logger.debug("Executing prepared SQL statement" + (sql != null ? " [" + sql + "]" : "")); + } + + Connection con = DataSourceUtils.getConnection(obtainDataSource()); + PreparedStatement ps = null; + try { + ps = psc.createPreparedStatement(con); + applyStatementSettings(ps); + T result = action.doInPreparedStatement(ps); + handleWarnings(ps); + return result; + } + catch (SQLException ex) { + // Release Connection early, to avoid potential connection pool deadlock + // in the case when the exception translator hasn't been initialized yet. + if (psc instanceof ParameterDisposer) { + ((ParameterDisposer) psc).cleanupParameters(); + } + String sql = getSql(psc); + psc = null; + JdbcUtils.closeStatement(ps); + ps = null; + DataSourceUtils.releaseConnection(con, getDataSource()); + con = null; + throw translateException("PreparedStatementCallback", sql, ex); + } + finally { + if (closeResources) { + if (psc instanceof ParameterDisposer) { + ((ParameterDisposer) psc).cleanupParameters(); + } + JdbcUtils.closeStatement(ps); + DataSourceUtils.releaseConnection(con, getDataSource()); + } + } + } + + @Override + @Nullable + public T execute(PreparedStatementCreator psc, PreparedStatementCallback action) + throws DataAccessException { + + return execute(psc, action, true); + } + + @Override + @Nullable + public T execute(String sql, PreparedStatementCallback action) throws DataAccessException { + return execute(new SimplePreparedStatementCreator(sql), action, true); + } + + /** + * Query using a prepared statement, allowing for a PreparedStatementCreator + * and a PreparedStatementSetter. Most other query methods use this method, + * but application code will always work with either a creator or a setter. + * @param psc a callback that creates a PreparedStatement given a Connection + * @param pss a callback that knows how to set values on the prepared statement. + * If this is {@code null}, the SQL will be assumed to contain no bind parameters. + * @param rse a callback that will extract results + * @return an arbitrary result object, as returned by the ResultSetExtractor + * @throws DataAccessException if there is any problem + */ + @Nullable + public T query( + PreparedStatementCreator psc, @Nullable final PreparedStatementSetter pss, final ResultSetExtractor rse) + throws DataAccessException { + + Assert.notNull(rse, "ResultSetExtractor must not be null"); + logger.debug("Executing prepared SQL query"); + + return execute(psc, new PreparedStatementCallback() { + @Override + @Nullable + public T doInPreparedStatement(PreparedStatement ps) throws SQLException { + ResultSet rs = null; + try { + if (pss != null) { + pss.setValues(ps); + } + rs = ps.executeQuery(); + return rse.extractData(rs); + } + finally { + JdbcUtils.closeResultSet(rs); + if (pss instanceof ParameterDisposer) { + ((ParameterDisposer) pss).cleanupParameters(); + } + } + } + }, true); + } + + @Override + @Nullable + public T query(PreparedStatementCreator psc, ResultSetExtractor rse) throws DataAccessException { + return query(psc, null, rse); + } + + @Override + @Nullable + public T query(String sql, @Nullable PreparedStatementSetter pss, ResultSetExtractor rse) throws DataAccessException { + return query(new SimplePreparedStatementCreator(sql), pss, rse); + } + + @Override + @Nullable + public T query(String sql, Object[] args, int[] argTypes, ResultSetExtractor rse) throws DataAccessException { + return query(sql, newArgTypePreparedStatementSetter(args, argTypes), rse); + } + + @Deprecated + @Override + @Nullable + public T query(String sql, @Nullable Object[] args, ResultSetExtractor rse) throws DataAccessException { + return query(sql, newArgPreparedStatementSetter(args), rse); + } + + @Override + @Nullable + public T query(String sql, ResultSetExtractor rse, @Nullable Object... args) throws DataAccessException { + return query(sql, newArgPreparedStatementSetter(args), rse); + } + + @Override + public void query(PreparedStatementCreator psc, RowCallbackHandler rch) throws DataAccessException { + query(psc, new RowCallbackHandlerResultSetExtractor(rch)); + } + + @Override + public void query(String sql, @Nullable PreparedStatementSetter pss, RowCallbackHandler rch) throws DataAccessException { + query(sql, pss, new RowCallbackHandlerResultSetExtractor(rch)); + } + + @Override + public void query(String sql, Object[] args, int[] argTypes, RowCallbackHandler rch) throws DataAccessException { + query(sql, newArgTypePreparedStatementSetter(args, argTypes), rch); + } + + @Deprecated + @Override + public void query(String sql, @Nullable Object[] args, RowCallbackHandler rch) throws DataAccessException { + query(sql, newArgPreparedStatementSetter(args), rch); + } + + @Override + public void query(String sql, RowCallbackHandler rch, @Nullable Object... args) throws DataAccessException { + query(sql, newArgPreparedStatementSetter(args), rch); + } + + @Override + public List query(PreparedStatementCreator psc, RowMapper rowMapper) throws DataAccessException { + return result(query(psc, new RowMapperResultSetExtractor<>(rowMapper))); + } + + @Override + public List query(String sql, @Nullable PreparedStatementSetter pss, RowMapper rowMapper) throws DataAccessException { + return result(query(sql, pss, new RowMapperResultSetExtractor<>(rowMapper))); + } + + @Override + public List query(String sql, Object[] args, int[] argTypes, RowMapper rowMapper) throws DataAccessException { + return result(query(sql, args, argTypes, new RowMapperResultSetExtractor<>(rowMapper))); + } + + @Deprecated + @Override + public List query(String sql, @Nullable Object[] args, RowMapper rowMapper) throws DataAccessException { + return result(query(sql, args, new RowMapperResultSetExtractor<>(rowMapper))); + } + + @Override + public List query(String sql, RowMapper rowMapper, @Nullable Object... args) throws DataAccessException { + return result(query(sql, args, new RowMapperResultSetExtractor<>(rowMapper))); + } + + /** + * Query using a prepared statement, allowing for a PreparedStatementCreator + * and a PreparedStatementSetter. Most other query methods use this method, + * but application code will always work with either a creator or a setter. + * @param psc a callback that creates a PreparedStatement given a Connection + * @param pss a callback that knows how to set values on the prepared statement. + * If this is {@code null}, the SQL will be assumed to contain no bind parameters. + * @param rowMapper a callback that will map one object per row + * @return the result Stream, containing mapped objects, needing to be + * closed once fully processed (e.g. through a try-with-resources clause) + * @throws DataAccessException if the query fails + * @since 5.3 + */ + public Stream queryForStream(PreparedStatementCreator psc, @Nullable PreparedStatementSetter pss, + RowMapper rowMapper) throws DataAccessException { + + return result(execute(psc, ps -> { + if (pss != null) { + pss.setValues(ps); + } + ResultSet rs = ps.executeQuery(); + Connection con = ps.getConnection(); + return new ResultSetSpliterator<>(rs, rowMapper).stream().onClose(() -> { + JdbcUtils.closeResultSet(rs); + if (pss instanceof ParameterDisposer) { + ((ParameterDisposer) pss).cleanupParameters(); + } + JdbcUtils.closeStatement(ps); + DataSourceUtils.releaseConnection(con, getDataSource()); + }); + }, false)); + } + + @Override + public Stream queryForStream(PreparedStatementCreator psc, RowMapper rowMapper) throws DataAccessException { + return queryForStream(psc, null, rowMapper); + } + + @Override + public Stream queryForStream(String sql, @Nullable PreparedStatementSetter pss, RowMapper rowMapper) throws DataAccessException { + return queryForStream(new SimplePreparedStatementCreator(sql), pss, rowMapper); + } + + @Override + public Stream queryForStream(String sql, RowMapper rowMapper, @Nullable Object... args) throws DataAccessException { + return queryForStream(new SimplePreparedStatementCreator(sql), newArgPreparedStatementSetter(args), rowMapper); + } + + @Override + @Nullable + public T queryForObject(String sql, Object[] args, int[] argTypes, RowMapper rowMapper) + throws DataAccessException { + + List results = query(sql, args, argTypes, new RowMapperResultSetExtractor<>(rowMapper, 1)); + return DataAccessUtils.nullableSingleResult(results); + } + + @Deprecated + @Override + @Nullable + public T queryForObject(String sql, @Nullable Object[] args, RowMapper rowMapper) throws DataAccessException { + List results = query(sql, args, new RowMapperResultSetExtractor<>(rowMapper, 1)); + return DataAccessUtils.nullableSingleResult(results); + } + + @Override + @Nullable + public T queryForObject(String sql, RowMapper rowMapper, @Nullable Object... args) throws DataAccessException { + List results = query(sql, args, new RowMapperResultSetExtractor<>(rowMapper, 1)); + return DataAccessUtils.nullableSingleResult(results); + } + + @Override + @Nullable + public T queryForObject(String sql, Object[] args, int[] argTypes, Class requiredType) + throws DataAccessException { + + return queryForObject(sql, args, argTypes, getSingleColumnRowMapper(requiredType)); + } + + @Deprecated + @Override + public T queryForObject(String sql, @Nullable Object[] args, Class requiredType) throws DataAccessException { + return queryForObject(sql, args, getSingleColumnRowMapper(requiredType)); + } + + @Override + public T queryForObject(String sql, Class requiredType, @Nullable Object... args) throws DataAccessException { + return queryForObject(sql, args, getSingleColumnRowMapper(requiredType)); + } + + @Override + public Map queryForMap(String sql, Object[] args, int[] argTypes) throws DataAccessException { + return result(queryForObject(sql, args, argTypes, getColumnMapRowMapper())); + } + + @Override + public Map queryForMap(String sql, @Nullable Object... args) throws DataAccessException { + return result(queryForObject(sql, args, getColumnMapRowMapper())); + } + + @Override + public List queryForList(String sql, Object[] args, int[] argTypes, Class elementType) throws DataAccessException { + return query(sql, args, argTypes, getSingleColumnRowMapper(elementType)); + } + + @Deprecated + @Override + public List queryForList(String sql, @Nullable Object[] args, Class elementType) throws DataAccessException { + return query(sql, args, getSingleColumnRowMapper(elementType)); + } + + @Override + public List queryForList(String sql, Class elementType, @Nullable Object... args) throws DataAccessException { + return query(sql, args, getSingleColumnRowMapper(elementType)); + } + + @Override + public List> queryForList(String sql, Object[] args, int[] argTypes) throws DataAccessException { + return query(sql, args, argTypes, getColumnMapRowMapper()); + } + + @Override + public List> queryForList(String sql, @Nullable Object... args) throws DataAccessException { + return query(sql, args, getColumnMapRowMapper()); + } + + @Override + public SqlRowSet queryForRowSet(String sql, Object[] args, int[] argTypes) throws DataAccessException { + return result(query(sql, args, argTypes, new SqlRowSetResultSetExtractor())); + } + + @Override + public SqlRowSet queryForRowSet(String sql, @Nullable Object... args) throws DataAccessException { + return result(query(sql, args, new SqlRowSetResultSetExtractor())); + } + + protected int update(final PreparedStatementCreator psc, @Nullable final PreparedStatementSetter pss) + throws DataAccessException { + + logger.debug("Executing prepared SQL update"); + + return updateCount(execute(psc, ps -> { + try { + if (pss != null) { + pss.setValues(ps); + } + int rows = ps.executeUpdate(); + if (logger.isTraceEnabled()) { + logger.trace("SQL update affected " + rows + " rows"); + } + return rows; + } + finally { + if (pss instanceof ParameterDisposer) { + ((ParameterDisposer) pss).cleanupParameters(); + } + } + }, true)); + } + + @Override + public int update(PreparedStatementCreator psc) throws DataAccessException { + return update(psc, (PreparedStatementSetter) null); + } + + @Override + public int update(final PreparedStatementCreator psc, final KeyHolder generatedKeyHolder) + throws DataAccessException { + + Assert.notNull(generatedKeyHolder, "KeyHolder must not be null"); + logger.debug("Executing SQL update and returning generated keys"); + + return updateCount(execute(psc, ps -> { + int rows = ps.executeUpdate(); + List> generatedKeys = generatedKeyHolder.getKeyList(); + generatedKeys.clear(); + ResultSet keys = ps.getGeneratedKeys(); + if (keys != null) { + try { + RowMapperResultSetExtractor> rse = + new RowMapperResultSetExtractor<>(getColumnMapRowMapper(), 1); + generatedKeys.addAll(result(rse.extractData(keys))); + } + finally { + JdbcUtils.closeResultSet(keys); + } + } + if (logger.isTraceEnabled()) { + logger.trace("SQL update affected " + rows + " rows and returned " + generatedKeys.size() + " keys"); + } + return rows; + }, true)); + } + + @Override + public int update(String sql, @Nullable PreparedStatementSetter pss) throws DataAccessException { + return update(new SimplePreparedStatementCreator(sql), pss); + } + + @Override + public int update(String sql, Object[] args, int[] argTypes) throws DataAccessException { + return update(sql, newArgTypePreparedStatementSetter(args, argTypes)); + } + + @Override + public int update(String sql, @Nullable Object... args) throws DataAccessException { + return update(sql, newArgPreparedStatementSetter(args)); + } + + @Override + public int[] batchUpdate(String sql, final BatchPreparedStatementSetter pss) throws DataAccessException { + if (logger.isDebugEnabled()) { + logger.debug("Executing SQL batch update [" + sql + "]"); + } + + int[] result = execute(sql, (PreparedStatementCallback) ps -> { + try { + int batchSize = pss.getBatchSize(); + InterruptibleBatchPreparedStatementSetter ipss = + (pss instanceof InterruptibleBatchPreparedStatementSetter ? + (InterruptibleBatchPreparedStatementSetter) pss : null); + if (JdbcUtils.supportsBatchUpdates(ps.getConnection())) { + for (int i = 0; i < batchSize; i++) { + pss.setValues(ps, i); + if (ipss != null && ipss.isBatchExhausted(i)) { + break; + } + ps.addBatch(); + } + return ps.executeBatch(); + } + else { + List rowsAffected = new ArrayList<>(); + for (int i = 0; i < batchSize; i++) { + pss.setValues(ps, i); + if (ipss != null && ipss.isBatchExhausted(i)) { + break; + } + rowsAffected.add(ps.executeUpdate()); + } + int[] rowsAffectedArray = new int[rowsAffected.size()]; + for (int i = 0; i < rowsAffectedArray.length; i++) { + rowsAffectedArray[i] = rowsAffected.get(i); + } + return rowsAffectedArray; + } + } + finally { + if (pss instanceof ParameterDisposer) { + ((ParameterDisposer) pss).cleanupParameters(); + } + } + }); + + Assert.state(result != null, "No result array"); + return result; + } + + @Override + public int[] batchUpdate(String sql, List batchArgs) throws DataAccessException { + return batchUpdate(sql, batchArgs, new int[0]); + } + + @Override + public int[] batchUpdate(String sql, List batchArgs, final int[] argTypes) throws DataAccessException { + if (batchArgs.isEmpty()) { + return new int[0]; + } + + return batchUpdate( + sql, + new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + Object[] values = batchArgs.get(i); + int colIndex = 0; + for (Object value : values) { + colIndex++; + if (value instanceof SqlParameterValue) { + SqlParameterValue paramValue = (SqlParameterValue) value; + StatementCreatorUtils.setParameterValue(ps, colIndex, paramValue, paramValue.getValue()); + } + else { + int colType; + if (argTypes.length < colIndex) { + colType = SqlTypeValue.TYPE_UNKNOWN; + } + else { + colType = argTypes[colIndex - 1]; + } + StatementCreatorUtils.setParameterValue(ps, colIndex, colType, value); + } + } + } + @Override + public int getBatchSize() { + return batchArgs.size(); + } + }); + } + + @Override + public int[][] batchUpdate(String sql, final Collection batchArgs, final int batchSize, + final ParameterizedPreparedStatementSetter pss) throws DataAccessException { + + if (logger.isDebugEnabled()) { + logger.debug("Executing SQL batch update [" + sql + "] with a batch size of " + batchSize); + } + int[][] result = execute(sql, (PreparedStatementCallback) ps -> { + List rowsAffected = new ArrayList<>(); + try { + boolean batchSupported = JdbcUtils.supportsBatchUpdates(ps.getConnection()); + int n = 0; + for (T obj : batchArgs) { + pss.setValues(ps, obj); + n++; + if (batchSupported) { + ps.addBatch(); + if (n % batchSize == 0 || n == batchArgs.size()) { + if (logger.isTraceEnabled()) { + int batchIdx = (n % batchSize == 0) ? n / batchSize : (n / batchSize) + 1; + int items = n - ((n % batchSize == 0) ? n / batchSize - 1 : (n / batchSize)) * batchSize; + logger.trace("Sending SQL batch update #" + batchIdx + " with " + items + " items"); + } + rowsAffected.add(ps.executeBatch()); + } + } + else { + int i = ps.executeUpdate(); + rowsAffected.add(new int[] {i}); + } + } + int[][] result1 = new int[rowsAffected.size()][]; + for (int i = 0; i < result1.length; i++) { + result1[i] = rowsAffected.get(i); + } + return result1; + } + finally { + if (pss instanceof ParameterDisposer) { + ((ParameterDisposer) pss).cleanupParameters(); + } + } + }); + + Assert.state(result != null, "No result array"); + return result; + } + + + //------------------------------------------------------------------------- + // Methods dealing with callable statements + //------------------------------------------------------------------------- + + @Override + @Nullable + public T execute(CallableStatementCreator csc, CallableStatementCallback action) + throws DataAccessException { + + Assert.notNull(csc, "CallableStatementCreator must not be null"); + Assert.notNull(action, "Callback object must not be null"); + if (logger.isDebugEnabled()) { + String sql = getSql(csc); + logger.debug("Calling stored procedure" + (sql != null ? " [" + sql + "]" : "")); + } + + Connection con = DataSourceUtils.getConnection(obtainDataSource()); + CallableStatement cs = null; + try { + cs = csc.createCallableStatement(con); + applyStatementSettings(cs); + T result = action.doInCallableStatement(cs); + handleWarnings(cs); + return result; + } + catch (SQLException ex) { + // Release Connection early, to avoid potential connection pool deadlock + // in the case when the exception translator hasn't been initialized yet. + if (csc instanceof ParameterDisposer) { + ((ParameterDisposer) csc).cleanupParameters(); + } + String sql = getSql(csc); + csc = null; + JdbcUtils.closeStatement(cs); + cs = null; + DataSourceUtils.releaseConnection(con, getDataSource()); + con = null; + throw translateException("CallableStatementCallback", sql, ex); + } + finally { + if (csc instanceof ParameterDisposer) { + ((ParameterDisposer) csc).cleanupParameters(); + } + JdbcUtils.closeStatement(cs); + DataSourceUtils.releaseConnection(con, getDataSource()); + } + } + + @Override + @Nullable + public T execute(String callString, CallableStatementCallback action) throws DataAccessException { + return execute(new SimpleCallableStatementCreator(callString), action); + } + + @Override + public Map call(CallableStatementCreator csc, List declaredParameters) + throws DataAccessException { + + final List updateCountParameters = new ArrayList<>(); + final List resultSetParameters = new ArrayList<>(); + final List callParameters = new ArrayList<>(); + + for (SqlParameter parameter : declaredParameters) { + if (parameter.isResultsParameter()) { + if (parameter instanceof SqlReturnResultSet) { + resultSetParameters.add(parameter); + } + else { + updateCountParameters.add(parameter); + } + } + else { + callParameters.add(parameter); + } + } + + Map result = execute(csc, cs -> { + boolean retVal = cs.execute(); + int updateCount = cs.getUpdateCount(); + if (logger.isTraceEnabled()) { + logger.trace("CallableStatement.execute() returned '" + retVal + "'"); + logger.trace("CallableStatement.getUpdateCount() returned " + updateCount); + } + Map resultsMap = createResultsMap(); + if (retVal || updateCount != -1) { + resultsMap.putAll(extractReturnedResults(cs, updateCountParameters, resultSetParameters, updateCount)); + } + resultsMap.putAll(extractOutputParameters(cs, callParameters)); + return resultsMap; + }); + + Assert.state(result != null, "No result map"); + return result; + } + + /** + * Extract returned ResultSets from the completed stored procedure. + * @param cs a JDBC wrapper for the stored procedure + * @param updateCountParameters the parameter list of declared update count parameters for the stored procedure + * @param resultSetParameters the parameter list of declared resultSet parameters for the stored procedure + * @return a Map that contains returned results + */ + protected Map extractReturnedResults(CallableStatement cs, + @Nullable List updateCountParameters, @Nullable List resultSetParameters, + int updateCount) throws SQLException { + + Map results = new LinkedHashMap<>(4); + int rsIndex = 0; + int updateIndex = 0; + boolean moreResults; + if (!this.skipResultsProcessing) { + do { + if (updateCount == -1) { + if (resultSetParameters != null && resultSetParameters.size() > rsIndex) { + SqlReturnResultSet declaredRsParam = (SqlReturnResultSet) resultSetParameters.get(rsIndex); + results.putAll(processResultSet(cs.getResultSet(), declaredRsParam)); + rsIndex++; + } + else { + if (!this.skipUndeclaredResults) { + String rsName = RETURN_RESULT_SET_PREFIX + (rsIndex + 1); + SqlReturnResultSet undeclaredRsParam = new SqlReturnResultSet(rsName, getColumnMapRowMapper()); + if (logger.isTraceEnabled()) { + logger.trace("Added default SqlReturnResultSet parameter named '" + rsName + "'"); + } + results.putAll(processResultSet(cs.getResultSet(), undeclaredRsParam)); + rsIndex++; + } + } + } + else { + if (updateCountParameters != null && updateCountParameters.size() > updateIndex) { + SqlReturnUpdateCount ucParam = (SqlReturnUpdateCount) updateCountParameters.get(updateIndex); + String declaredUcName = ucParam.getName(); + results.put(declaredUcName, updateCount); + updateIndex++; + } + else { + if (!this.skipUndeclaredResults) { + String undeclaredName = RETURN_UPDATE_COUNT_PREFIX + (updateIndex + 1); + if (logger.isTraceEnabled()) { + logger.trace("Added default SqlReturnUpdateCount parameter named '" + undeclaredName + "'"); + } + results.put(undeclaredName, updateCount); + updateIndex++; + } + } + } + moreResults = cs.getMoreResults(); + updateCount = cs.getUpdateCount(); + if (logger.isTraceEnabled()) { + logger.trace("CallableStatement.getUpdateCount() returned " + updateCount); + } + } + while (moreResults || updateCount != -1); + } + return results; + } + + /** + * Extract output parameters from the completed stored procedure. + * @param cs the JDBC wrapper for the stored procedure + * @param parameters parameter list for the stored procedure + * @return a Map that contains returned results + */ + protected Map extractOutputParameters(CallableStatement cs, List parameters) + throws SQLException { + + Map results = CollectionUtils.newLinkedHashMap(parameters.size()); + int sqlColIndex = 1; + for (SqlParameter param : parameters) { + if (param instanceof SqlOutParameter) { + SqlOutParameter outParam = (SqlOutParameter) param; + Assert.state(outParam.getName() != null, "Anonymous parameters not allowed"); + SqlReturnType returnType = outParam.getSqlReturnType(); + if (returnType != null) { + Object out = returnType.getTypeValue(cs, sqlColIndex, outParam.getSqlType(), outParam.getTypeName()); + results.put(outParam.getName(), out); + } + else { + Object out = cs.getObject(sqlColIndex); + if (out instanceof ResultSet) { + if (outParam.isResultSetSupported()) { + results.putAll(processResultSet((ResultSet) out, outParam)); + } + else { + String rsName = outParam.getName(); + SqlReturnResultSet rsParam = new SqlReturnResultSet(rsName, getColumnMapRowMapper()); + results.putAll(processResultSet((ResultSet) out, rsParam)); + if (logger.isTraceEnabled()) { + logger.trace("Added default SqlReturnResultSet parameter named '" + rsName + "'"); + } + } + } + else { + results.put(outParam.getName(), out); + } + } + } + if (!(param.isResultsParameter())) { + sqlColIndex++; + } + } + return results; + } + + /** + * Process the given ResultSet from a stored procedure. + * @param rs the ResultSet to process + * @param param the corresponding stored procedure parameter + * @return a Map that contains returned results + */ + protected Map processResultSet( + @Nullable ResultSet rs, ResultSetSupportingSqlParameter param) throws SQLException { + + if (rs != null) { + try { + if (param.getRowMapper() != null) { + RowMapper rowMapper = param.getRowMapper(); + Object data = (new RowMapperResultSetExtractor<>(rowMapper)).extractData(rs); + return Collections.singletonMap(param.getName(), data); + } + else if (param.getRowCallbackHandler() != null) { + RowCallbackHandler rch = param.getRowCallbackHandler(); + (new RowCallbackHandlerResultSetExtractor(rch)).extractData(rs); + return Collections.singletonMap(param.getName(), + "ResultSet returned from stored procedure was processed"); + } + else if (param.getResultSetExtractor() != null) { + Object data = param.getResultSetExtractor().extractData(rs); + return Collections.singletonMap(param.getName(), data); + } + } + finally { + JdbcUtils.closeResultSet(rs); + } + } + return Collections.emptyMap(); + } + + + //------------------------------------------------------------------------- + // Implementation hooks and helper methods + //------------------------------------------------------------------------- + + /** + * Create a new RowMapper for reading columns as key-value pairs. + * @return the RowMapper to use + * @see ColumnMapRowMapper + */ + protected RowMapper> getColumnMapRowMapper() { + return new ColumnMapRowMapper(); + } + + /** + * Create a new RowMapper for reading result objects from a single column. + * @param requiredType the type that each result object is expected to match + * @return the RowMapper to use + * @see SingleColumnRowMapper + */ + protected RowMapper getSingleColumnRowMapper(Class requiredType) { + return new SingleColumnRowMapper<>(requiredType); + } + + /** + * Create a Map instance to be used as the results map. + *

    If {@link #resultsMapCaseInsensitive} has been set to true, + * a {@link LinkedCaseInsensitiveMap} will be created; otherwise, a + * {@link LinkedHashMap} will be created. + * @return the results Map instance + * @see #setResultsMapCaseInsensitive + * @see #isResultsMapCaseInsensitive + */ + protected Map createResultsMap() { + if (isResultsMapCaseInsensitive()) { + return new LinkedCaseInsensitiveMap<>(); + } + else { + return new LinkedHashMap<>(); + } + } + + /** + * Prepare the given JDBC Statement (or PreparedStatement or CallableStatement), + * applying statement settings such as fetch size, max rows, and query timeout. + * @param stmt the JDBC Statement to prepare + * @throws SQLException if thrown by JDBC API + * @see #setFetchSize + * @see #setMaxRows + * @see #setQueryTimeout + * @see org.springframework.jdbc.datasource.DataSourceUtils#applyTransactionTimeout + */ + protected void applyStatementSettings(Statement stmt) throws SQLException { + int fetchSize = getFetchSize(); + if (fetchSize != -1) { + stmt.setFetchSize(fetchSize); + } + int maxRows = getMaxRows(); + if (maxRows != -1) { + stmt.setMaxRows(maxRows); + } + DataSourceUtils.applyTimeout(stmt, getDataSource(), getQueryTimeout()); + } + + /** + * Create a new arg-based PreparedStatementSetter using the args passed in. + *

    By default, we'll create an {@link ArgumentPreparedStatementSetter}. + * This method allows for the creation to be overridden by subclasses. + * @param args object array with arguments + * @return the new PreparedStatementSetter to use + */ + protected PreparedStatementSetter newArgPreparedStatementSetter(@Nullable Object[] args) { + return new ArgumentPreparedStatementSetter(args); + } + + /** + * Create a new arg-type-based PreparedStatementSetter using the args and types passed in. + *

    By default, we'll create an {@link ArgumentTypePreparedStatementSetter}. + * This method allows for the creation to be overridden by subclasses. + * @param args object array with arguments + * @param argTypes int array of SQLTypes for the associated arguments + * @return the new PreparedStatementSetter to use + */ + protected PreparedStatementSetter newArgTypePreparedStatementSetter(Object[] args, int[] argTypes) { + return new ArgumentTypePreparedStatementSetter(args, argTypes); + } + + /** + * Throw an SQLWarningException if we're not ignoring warnings, + * otherwise log the warnings at debug level. + * @param stmt the current JDBC statement + * @throws SQLWarningException if not ignoring warnings + * @see org.springframework.jdbc.SQLWarningException + */ + protected void handleWarnings(Statement stmt) throws SQLException { + if (isIgnoreWarnings()) { + if (logger.isDebugEnabled()) { + SQLWarning warningToLog = stmt.getWarnings(); + while (warningToLog != null) { + logger.debug("SQLWarning ignored: SQL state '" + warningToLog.getSQLState() + "', error code '" + + warningToLog.getErrorCode() + "', message [" + warningToLog.getMessage() + "]"); + warningToLog = warningToLog.getNextWarning(); + } + } + } + else { + handleWarnings(stmt.getWarnings()); + } + } + + /** + * Throw an SQLWarningException if encountering an actual warning. + * @param warning the warnings object from the current statement. + * May be {@code null}, in which case this method does nothing. + * @throws SQLWarningException in case of an actual warning to be raised + */ + protected void handleWarnings(@Nullable SQLWarning warning) throws SQLWarningException { + if (warning != null) { + throw new SQLWarningException("Warning not ignored", warning); + } + } + + /** + * Translate the given {@link SQLException} into a generic {@link DataAccessException}. + * @param task readable text describing the task being attempted + * @param sql the SQL query or update that caused the problem (may be {@code null}) + * @param ex the offending {@code SQLException} + * @return a DataAccessException wrapping the {@code SQLException} (never {@code null}) + * @since 5.0 + * @see #getExceptionTranslator() + */ + protected DataAccessException translateException(String task, @Nullable String sql, SQLException ex) { + DataAccessException dae = getExceptionTranslator().translate(task, sql, ex); + return (dae != null ? dae : new UncategorizedSQLException(task, sql, ex)); + } + + + /** + * Determine SQL from potential provider object. + * @param sqlProvider object which is potentially an SqlProvider + * @return the SQL string, or {@code null} if not known + * @see SqlProvider + */ + @Nullable + private static String getSql(Object sqlProvider) { + if (sqlProvider instanceof SqlProvider) { + return ((SqlProvider) sqlProvider).getSql(); + } + else { + return null; + } + } + + private static T result(@Nullable T result) { + Assert.state(result != null, "No result"); + return result; + } + + private static int updateCount(@Nullable Integer result) { + Assert.state(result != null, "No update count"); + return result; + } + + + /** + * Invocation handler that suppresses close calls on JDBC Connections. + * Also prepares returned Statement (Prepared/CallbackStatement) objects. + * @see java.sql.Connection#close() + */ + private class CloseSuppressingInvocationHandler implements InvocationHandler { + + private final Connection target; + + public CloseSuppressingInvocationHandler(Connection target) { + this.target = target; + } + + @Override + @Nullable + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // Invocation on ConnectionProxy interface coming in... + + switch (method.getName()) { + case "equals": + // Only consider equal when proxies are identical. + return (proxy == args[0]); + case "hashCode": + // Use hashCode of PersistenceManager proxy. + return System.identityHashCode(proxy); + case "close": + // Handle close method: suppress, not valid. + return null; + case "isClosed": + return false; + case "getTargetConnection": + // Handle getTargetConnection method: return underlying Connection. + return this.target; + case "unwrap": + return (((Class) args[0]).isInstance(proxy) ? proxy : this.target.unwrap((Class) args[0])); + case "isWrapperFor": + return (((Class) args[0]).isInstance(proxy) || this.target.isWrapperFor((Class) args[0])); + } + + // Invoke method on target Connection. + try { + Object retVal = method.invoke(this.target, args); + + // If return value is a JDBC Statement, apply statement settings + // (fetch size, max rows, transaction timeout). + if (retVal instanceof Statement) { + applyStatementSettings(((Statement) retVal)); + } + + return retVal; + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + } + + + /** + * Simple adapter for PreparedStatementCreator, allowing to use a plain SQL statement. + */ + private static class SimplePreparedStatementCreator implements PreparedStatementCreator, SqlProvider { + + private final String sql; + + public SimplePreparedStatementCreator(String sql) { + Assert.notNull(sql, "SQL must not be null"); + this.sql = sql; + } + + @Override + public PreparedStatement createPreparedStatement(Connection con) throws SQLException { + return con.prepareStatement(this.sql); + } + + @Override + public String getSql() { + return this.sql; + } + } + + + /** + * Simple adapter for CallableStatementCreator, allowing to use a plain SQL statement. + */ + private static class SimpleCallableStatementCreator implements CallableStatementCreator, SqlProvider { + + private final String callString; + + public SimpleCallableStatementCreator(String callString) { + Assert.notNull(callString, "Call string must not be null"); + this.callString = callString; + } + + @Override + public CallableStatement createCallableStatement(Connection con) throws SQLException { + return con.prepareCall(this.callString); + } + + @Override + public String getSql() { + return this.callString; + } + } + + + /** + * Adapter to enable use of a RowCallbackHandler inside a ResultSetExtractor. + *

    Uses a regular ResultSet, so we have to be careful when using it: + * We don't use it for navigating since this could lead to unpredictable consequences. + */ + private static class RowCallbackHandlerResultSetExtractor implements ResultSetExtractor { + + private final RowCallbackHandler rch; + + public RowCallbackHandlerResultSetExtractor(RowCallbackHandler rch) { + this.rch = rch; + } + + @Override + @Nullable + public Object extractData(ResultSet rs) throws SQLException { + while (rs.next()) { + this.rch.processRow(rs); + } + return null; + } + } + + + /** + * Spliterator for queryForStream adaptation of a ResultSet to a Stream. + * @since 5.3 + */ + private static class ResultSetSpliterator implements Spliterator { + + private final ResultSet rs; + + private final RowMapper rowMapper; + + private int rowNum = 0; + + public ResultSetSpliterator(ResultSet rs, RowMapper rowMapper) { + this.rs = rs; + this.rowMapper = rowMapper; + } + + @Override + public boolean tryAdvance(Consumer action) { + try { + if (this.rs.next()) { + action.accept(this.rowMapper.mapRow(this.rs, this.rowNum++)); + return true; + } + return false; + } + catch (SQLException ex) { + throw new InvalidResultSetAccessException(ex); + } + } + + @Override + @Nullable + public Spliterator trySplit() { + return null; + } + + @Override + public long estimateSize() { + return Long.MAX_VALUE; + } + + @Override + public int characteristics() { + return Spliterator.ORDERED; + } + + public Stream stream() { + return StreamSupport.stream(this, false); + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/ParameterDisposer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ParameterDisposer.java new file mode 100644 index 0000000..56b669a --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ParameterDisposer.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +/** + * Interface to be implemented by objects that can close resources + * allocated by parameters like {@code SqlLobValue} objects. + * + *

    Typically implemented by {@code PreparedStatementCreators} and + * {@code PreparedStatementSetters} that support {@link DisposableSqlTypeValue} + * objects (e.g. {@code SqlLobValue}) as parameters. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 1.1 + * @see PreparedStatementCreator + * @see PreparedStatementSetter + * @see DisposableSqlTypeValue + * @see org.springframework.jdbc.core.support.SqlLobValue + */ +public interface ParameterDisposer { + + /** + * Close the resources allocated by parameters that the implementing + * object holds, for example in case of a DisposableSqlTypeValue + * (like an SqlLobValue). + * @see DisposableSqlTypeValue#cleanup() + * @see org.springframework.jdbc.core.support.SqlLobValue#cleanup() + */ + void cleanupParameters(); + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/ParameterMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ParameterMapper.java new file mode 100644 index 0000000..e84f6b6 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ParameterMapper.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Map; + +/** + * Implement this interface when parameters need to be customized based + * on the connection. We might need to do this to make use of proprietary + * features, available only with a specific Connection type. + * + * @author Rod Johnson + * @author Thomas Risberg + * @see CallableStatementCreatorFactory#newCallableStatementCreator(ParameterMapper) + * @see org.springframework.jdbc.object.StoredProcedure#execute(ParameterMapper) + */ +@FunctionalInterface +public interface ParameterMapper { + + /** + * Create a Map of input parameters, keyed by name. + * @param con a JDBC connection. This is useful (and the purpose of this interface) + * if we need to do something RDBMS-specific with a proprietary Connection + * implementation class. This class conceals such proprietary details. However, + * it is best to avoid using such proprietary RDBMS features if possible. + * @return a Map of input parameters, keyed by name (never {@code null}) + * @throws SQLException if an SQLException is encountered setting + * parameter values (that is, there's no need to catch SQLException) + */ + Map createMap(Connection con) throws SQLException; + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/ParameterizedPreparedStatementSetter.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ParameterizedPreparedStatementSetter.java new file mode 100644 index 0000000..f029701 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ParameterizedPreparedStatementSetter.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * Parameterized callback interface used by the {@link JdbcTemplate} class for + * batch updates. + * + *

    This interface sets values on a {@link java.sql.PreparedStatement} provided + * by the JdbcTemplate class, for each of a number of updates in a batch using the + * same SQL. Implementations are responsible for setting any necessary parameters. + * SQL with placeholders will already have been supplied. + * + *

    Implementations do not need to concern themselves with SQLExceptions + * that may be thrown from operations they attempt. The JdbcTemplate class will + * catch and handle SQLExceptions appropriately. + * + * @author Nicolas Fabre + * @author Thomas Risberg + * @since 3.1 + * @param the argument type + * @see JdbcTemplate#batchUpdate(String, java.util.Collection, int, ParameterizedPreparedStatementSetter) + */ +@FunctionalInterface +public interface ParameterizedPreparedStatementSetter { + + /** + * Set parameter values on the given PreparedStatement. + * @param ps the PreparedStatement to invoke setter methods on + * @param argument the object containing the values to be set + * @throws SQLException if an SQLException is encountered (i.e. there is no need to catch SQLException) + */ + void setValues(PreparedStatement ps, T argument) throws SQLException; + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCallback.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCallback.java new file mode 100644 index 0000000..97c9f32 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCallback.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.springframework.dao.DataAccessException; +import org.springframework.lang.Nullable; + +/** + * Generic callback interface for code that operates on a PreparedStatement. + * Allows to execute any number of operations on a single PreparedStatement, + * for example a single {@code executeUpdate} call or repeated + * {@code executeUpdate} calls with varying parameters. + * + *

    Used internally by JdbcTemplate, but also useful for application code. + * Note that the passed-in PreparedStatement can have been created by the + * framework or by a custom PreparedStatementCreator. However, the latter is + * hardly ever necessary, as most custom callback actions will perform updates + * in which case a standard PreparedStatement is fine. Custom actions will + * always set parameter values themselves, so that PreparedStatementCreator + * capability is not needed either. + * + * @author Juergen Hoeller + * @since 16.03.2004 + * @param the result type + * @see JdbcTemplate#execute(String, PreparedStatementCallback) + * @see JdbcTemplate#execute(PreparedStatementCreator, PreparedStatementCallback) + */ +@FunctionalInterface +public interface PreparedStatementCallback { + + /** + * Gets called by {@code JdbcTemplate.execute} with an active JDBC + * PreparedStatement. Does not need to care about closing the Statement + * or the Connection, or about handling transactions: this will all be + * handled by Spring's JdbcTemplate. + *

    NOTE: Any ResultSets opened should be closed in finally blocks + * within the callback implementation. Spring will close the Statement + * object after the callback returned, but this does not necessarily imply + * that the ResultSet resources will be closed: the Statement objects might + * get pooled by the connection pool, with {@code close} calls only + * returning the object to the pool but not physically closing the resources. + *

    If called without a thread-bound JDBC transaction (initiated by + * DataSourceTransactionManager), the code will simply get executed on the + * JDBC connection with its transactional semantics. If JdbcTemplate is + * configured to use a JTA-aware DataSource, the JDBC connection and thus + * the callback code will be transactional if a JTA transaction is active. + *

    Allows for returning a result object created within the callback, i.e. + * a domain object or a collection of domain objects. Note that there's + * special support for single step actions: see JdbcTemplate.queryForObject etc. + * A thrown RuntimeException is treated as application exception, it gets + * propagated to the caller of the template. + * @param ps active JDBC PreparedStatement + * @return a result object, or {@code null} if none + * @throws SQLException if thrown by a JDBC method, to be auto-converted + * to a DataAccessException by an SQLExceptionTranslator + * @throws DataAccessException in case of custom exceptions + * @see JdbcTemplate#queryForObject(String, Object[], Class) + * @see JdbcTemplate#queryForList(String, Object[]) + */ + @Nullable + T doInPreparedStatement(PreparedStatement ps) throws SQLException, DataAccessException; + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCreator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCreator.java new file mode 100644 index 0000000..4e0b0e0 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCreator.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * One of the two central callback interfaces used by the JdbcTemplate class. + * This interface creates a PreparedStatement given a connection, provided + * by the JdbcTemplate class. Implementations are responsible for providing + * SQL and any necessary parameters. + * + *

    Implementations do not need to concern themselves with + * SQLExceptions that may be thrown from operations they attempt. + * The JdbcTemplate class will catch and handle SQLExceptions appropriately. + * + *

    A PreparedStatementCreator should also implement the SqlProvider interface + * if it is able to provide the SQL it uses for PreparedStatement creation. + * This allows for better contextual information in case of exceptions. + * + * @author Rod Johnson + * @see JdbcTemplate#execute(PreparedStatementCreator, PreparedStatementCallback) + * @see JdbcTemplate#query(PreparedStatementCreator, RowCallbackHandler) + * @see JdbcTemplate#update(PreparedStatementCreator) + * @see SqlProvider + */ +@FunctionalInterface +public interface PreparedStatementCreator { + + /** + * Create a statement in this connection. Allows implementations to use + * PreparedStatements. The JdbcTemplate will close the created statement. + * @param con the connection used to create statement + * @return a prepared statement + * @throws SQLException there is no need to catch SQLExceptions + * that may be thrown in the implementation of this method. + * The JdbcTemplate class will handle them. + */ + PreparedStatement createPreparedStatement(Connection con) throws SQLException; + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCreatorFactory.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCreatorFactory.java new file mode 100644 index 0000000..e6083f8 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCreatorFactory.java @@ -0,0 +1,304 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.lang.Nullable; + +/** + * Helper class that efficiently creates multiple {@link PreparedStatementCreator} + * objects with different parameters based on an SQL statement and a single + * set of parameter declarations. + * + * @author Rod Johnson + * @author Thomas Risberg + * @author Juergen Hoeller + */ +public class PreparedStatementCreatorFactory { + + /** The SQL, which won't change when the parameters change. */ + private final String sql; + + /** List of SqlParameter objects (may not be {@code null}). */ + private final List declaredParameters; + + private int resultSetType = ResultSet.TYPE_FORWARD_ONLY; + + private boolean updatableResults = false; + + private boolean returnGeneratedKeys = false; + + @Nullable + private String[] generatedKeysColumnNames; + + + /** + * Create a new factory. Will need to add parameters via the + * {@link #addParameter} method or have no parameters. + * @param sql the SQL statement to execute + */ + public PreparedStatementCreatorFactory(String sql) { + this.sql = sql; + this.declaredParameters = new ArrayList<>(); + } + + /** + * Create a new factory with the given SQL and JDBC types. + * @param sql the SQL statement to execute + * @param types int array of JDBC types + */ + public PreparedStatementCreatorFactory(String sql, int... types) { + this.sql = sql; + this.declaredParameters = SqlParameter.sqlTypesToAnonymousParameterList(types); + } + + /** + * Create a new factory with the given SQL and parameters. + * @param sql the SQL statement to execute + * @param declaredParameters list of {@link SqlParameter} objects + */ + public PreparedStatementCreatorFactory(String sql, List declaredParameters) { + this.sql = sql; + this.declaredParameters = declaredParameters; + } + + + /** + * Return the SQL statement to execute. + * @since 5.1.3 + */ + public final String getSql() { + return this.sql; + } + + /** + * Add a new declared parameter. + *

    Order of parameter addition is significant. + * @param param the parameter to add to the list of declared parameters + */ + public void addParameter(SqlParameter param) { + this.declaredParameters.add(param); + } + + /** + * Set whether to use prepared statements that return a specific type of ResultSet. + * @param resultSetType the ResultSet type + * @see java.sql.ResultSet#TYPE_FORWARD_ONLY + * @see java.sql.ResultSet#TYPE_SCROLL_INSENSITIVE + * @see java.sql.ResultSet#TYPE_SCROLL_SENSITIVE + */ + public void setResultSetType(int resultSetType) { + this.resultSetType = resultSetType; + } + + /** + * Set whether to use prepared statements capable of returning updatable ResultSets. + */ + public void setUpdatableResults(boolean updatableResults) { + this.updatableResults = updatableResults; + } + + /** + * Set whether prepared statements should be capable of returning auto-generated keys. + */ + public void setReturnGeneratedKeys(boolean returnGeneratedKeys) { + this.returnGeneratedKeys = returnGeneratedKeys; + } + + /** + * Set the column names of the auto-generated keys. + */ + public void setGeneratedKeysColumnNames(String... names) { + this.generatedKeysColumnNames = names; + } + + + /** + * Return a new PreparedStatementSetter for the given parameters. + * @param params list of parameters (may be {@code null}) + */ + public PreparedStatementSetter newPreparedStatementSetter(@Nullable List params) { + return new PreparedStatementCreatorImpl(params != null ? params : Collections.emptyList()); + } + + /** + * Return a new PreparedStatementSetter for the given parameters. + * @param params the parameter array (may be {@code null}) + */ + public PreparedStatementSetter newPreparedStatementSetter(@Nullable Object[] params) { + return new PreparedStatementCreatorImpl(params != null ? Arrays.asList(params) : Collections.emptyList()); + } + + /** + * Return a new PreparedStatementCreator for the given parameters. + * @param params list of parameters (may be {@code null}) + */ + public PreparedStatementCreator newPreparedStatementCreator(@Nullable List params) { + return new PreparedStatementCreatorImpl(params != null ? params : Collections.emptyList()); + } + + /** + * Return a new PreparedStatementCreator for the given parameters. + * @param params the parameter array (may be {@code null}) + */ + public PreparedStatementCreator newPreparedStatementCreator(@Nullable Object[] params) { + return new PreparedStatementCreatorImpl(params != null ? Arrays.asList(params) : Collections.emptyList()); + } + + /** + * Return a new PreparedStatementCreator for the given parameters. + * @param sqlToUse the actual SQL statement to use (if different from + * the factory's, for example because of named parameter expanding) + * @param params the parameter array (may be {@code null}) + */ + public PreparedStatementCreator newPreparedStatementCreator(String sqlToUse, @Nullable Object[] params) { + return new PreparedStatementCreatorImpl( + sqlToUse, params != null ? Arrays.asList(params) : Collections.emptyList()); + } + + + /** + * PreparedStatementCreator implementation returned by this class. + */ + private class PreparedStatementCreatorImpl + implements PreparedStatementCreator, PreparedStatementSetter, SqlProvider, ParameterDisposer { + + private final String actualSql; + + private final List parameters; + + public PreparedStatementCreatorImpl(List parameters) { + this(sql, parameters); + } + + public PreparedStatementCreatorImpl(String actualSql, List parameters) { + this.actualSql = actualSql; + this.parameters = parameters; + if (parameters.size() != declaredParameters.size()) { + // Account for named parameters being used multiple times + Set names = new HashSet<>(); + for (int i = 0; i < parameters.size(); i++) { + Object param = parameters.get(i); + if (param instanceof SqlParameterValue) { + names.add(((SqlParameterValue) param).getName()); + } + else { + names.add("Parameter #" + i); + } + } + if (names.size() != declaredParameters.size()) { + throw new InvalidDataAccessApiUsageException( + "SQL [" + sql + "]: given " + names.size() + + " parameters but expected " + declaredParameters.size()); + } + } + } + + @Override + public PreparedStatement createPreparedStatement(Connection con) throws SQLException { + PreparedStatement ps; + if (generatedKeysColumnNames != null || returnGeneratedKeys) { + if (generatedKeysColumnNames != null) { + ps = con.prepareStatement(this.actualSql, generatedKeysColumnNames); + } + else { + ps = con.prepareStatement(this.actualSql, PreparedStatement.RETURN_GENERATED_KEYS); + } + } + else if (resultSetType == ResultSet.TYPE_FORWARD_ONLY && !updatableResults) { + ps = con.prepareStatement(this.actualSql); + } + else { + ps = con.prepareStatement(this.actualSql, resultSetType, + updatableResults ? ResultSet.CONCUR_UPDATABLE : ResultSet.CONCUR_READ_ONLY); + } + setValues(ps); + return ps; + } + + @Override + public void setValues(PreparedStatement ps) throws SQLException { + // Set arguments: Does nothing if there are no parameters. + int sqlColIndx = 1; + for (int i = 0; i < this.parameters.size(); i++) { + Object in = this.parameters.get(i); + SqlParameter declaredParameter; + // SqlParameterValue overrides declared parameter meta-data, in particular for + // independence from the declared parameter position in case of named parameters. + if (in instanceof SqlParameterValue) { + SqlParameterValue paramValue = (SqlParameterValue) in; + in = paramValue.getValue(); + declaredParameter = paramValue; + } + else { + if (declaredParameters.size() <= i) { + throw new InvalidDataAccessApiUsageException( + "SQL [" + sql + "]: unable to access parameter number " + (i + 1) + + " given only " + declaredParameters.size() + " parameters"); + + } + declaredParameter = declaredParameters.get(i); + } + if (in instanceof Iterable && declaredParameter.getSqlType() != Types.ARRAY) { + Iterable entries = (Iterable) in; + for (Object entry : entries) { + if (entry instanceof Object[]) { + Object[] valueArray = (Object[]) entry; + for (Object argValue : valueArray) { + StatementCreatorUtils.setParameterValue(ps, sqlColIndx++, declaredParameter, argValue); + } + } + else { + StatementCreatorUtils.setParameterValue(ps, sqlColIndx++, declaredParameter, entry); + } + } + } + else { + StatementCreatorUtils.setParameterValue(ps, sqlColIndx++, declaredParameter, in); + } + } + } + + @Override + public String getSql() { + return sql; + } + + @Override + public void cleanupParameters() { + StatementCreatorUtils.cleanupParameters(this.parameters); + } + + @Override + public String toString() { + return "PreparedStatementCreator: sql=[" + sql + "]; parameters=" + this.parameters; + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementSetter.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementSetter.java new file mode 100644 index 0000000..916d1ef --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementSetter.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * General callback interface used by the {@link JdbcTemplate} class. + * + *

    This interface sets values on a {@link java.sql.PreparedStatement} provided + * by the JdbcTemplate class, for each of a number of updates in a batch using the + * same SQL. Implementations are responsible for setting any necessary parameters. + * SQL with placeholders will already have been supplied. + * + *

    It's easier to use this interface than {@link PreparedStatementCreator}: + * The JdbcTemplate will create the PreparedStatement, with the callback + * only being responsible for setting parameter values. + * + *

    Implementations do not need to concern themselves with + * SQLExceptions that may be thrown from operations they attempt. + * The JdbcTemplate class will catch and handle SQLExceptions appropriately. + * + * @author Rod Johnson + * @since March 2, 2003 + * @see JdbcTemplate#update(String, PreparedStatementSetter) + * @see JdbcTemplate#query(String, PreparedStatementSetter, ResultSetExtractor) + */ +@FunctionalInterface +public interface PreparedStatementSetter { + + /** + * Set parameter values on the given PreparedStatement. + * @param ps the PreparedStatement to invoke setter methods on + * @throws SQLException if an SQLException is encountered + * (i.e. there is no need to catch SQLException) + */ + void setValues(PreparedStatement ps) throws SQLException; + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/ResultSetExtractor.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ResultSetExtractor.java new file mode 100644 index 0000000..0acaf06 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ResultSetExtractor.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.springframework.dao.DataAccessException; +import org.springframework.lang.Nullable; + +/** + * Callback interface used by {@link JdbcTemplate}'s query methods. + * Implementations of this interface perform the actual work of extracting + * results from a {@link java.sql.ResultSet}, but don't need to worry + * about exception handling. {@link java.sql.SQLException SQLExceptions} + * will be caught and handled by the calling JdbcTemplate. + * + *

    This interface is mainly used within the JDBC framework itself. + * A {@link RowMapper} is usually a simpler choice for ResultSet processing, + * mapping one result object per row instead of one result object for + * the entire ResultSet. + * + *

    Note: In contrast to a {@link RowCallbackHandler}, a ResultSetExtractor + * object is typically stateless and thus reusable, as long as it doesn't + * access stateful resources (such as output streams when streaming LOB + * contents) or keep result state within the object. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since April 24, 2003 + * @param the result type + * @see JdbcTemplate + * @see RowCallbackHandler + * @see RowMapper + * @see org.springframework.jdbc.core.support.AbstractLobStreamingResultSetExtractor + */ +@FunctionalInterface +public interface ResultSetExtractor { + + /** + * Implementations must implement this method to process the entire ResultSet. + * @param rs the ResultSet to extract data from. Implementations should + * not close this: it will be closed by the calling JdbcTemplate. + * @return an arbitrary result object, or {@code null} if none + * (the extractor will typically be stateful in the latter case). + * @throws SQLException if an SQLException is encountered getting column + * values or navigating (that is, there's no need to catch SQLException) + * @throws DataAccessException in case of custom exceptions + */ + @Nullable + T extractData(ResultSet rs) throws SQLException, DataAccessException; + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/ResultSetSupportingSqlParameter.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ResultSetSupportingSqlParameter.java new file mode 100644 index 0000000..0c8b9b4 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ResultSetSupportingSqlParameter.java @@ -0,0 +1,147 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.ResultSet; + +import org.springframework.lang.Nullable; + +/** + * Common base class for ResultSet-supporting SqlParameters like + * {@link SqlOutParameter} and {@link SqlReturnResultSet}. + * + * @author Juergen Hoeller + * @since 1.0.2 + */ +public class ResultSetSupportingSqlParameter extends SqlParameter { + + @Nullable + private ResultSetExtractor resultSetExtractor; + + @Nullable + private RowCallbackHandler rowCallbackHandler; + + @Nullable + private RowMapper rowMapper; + + + /** + * Create a new ResultSetSupportingSqlParameter. + * @param name the name of the parameter, as used in input and output maps + * @param sqlType the parameter SQL type according to {@code java.sql.Types} + */ + public ResultSetSupportingSqlParameter(String name, int sqlType) { + super(name, sqlType); + } + + /** + * Create a new ResultSetSupportingSqlParameter. + * @param name the name of the parameter, as used in input and output maps + * @param sqlType the parameter SQL type according to {@code java.sql.Types} + * @param scale the number of digits after the decimal point + * (for DECIMAL and NUMERIC types) + */ + public ResultSetSupportingSqlParameter(String name, int sqlType, int scale) { + super(name, sqlType, scale); + } + + /** + * Create a new ResultSetSupportingSqlParameter. + * @param name the name of the parameter, as used in input and output maps + * @param sqlType the parameter SQL type according to {@code java.sql.Types} + * @param typeName the type name of the parameter (optional) + */ + public ResultSetSupportingSqlParameter(String name, int sqlType, @Nullable String typeName) { + super(name, sqlType, typeName); + } + + /** + * Create a new ResultSetSupportingSqlParameter. + * @param name the name of the parameter, as used in input and output maps + * @param sqlType the parameter SQL type according to {@code java.sql.Types} + * @param rse the {@link ResultSetExtractor} to use for parsing the {@link ResultSet} + */ + public ResultSetSupportingSqlParameter(String name, int sqlType, ResultSetExtractor rse) { + super(name, sqlType); + this.resultSetExtractor = rse; + } + + /** + * Create a new ResultSetSupportingSqlParameter. + * @param name the name of the parameter, as used in input and output maps + * @param sqlType the parameter SQL type according to {@code java.sql.Types} + * @param rch the {@link RowCallbackHandler} to use for parsing the {@link ResultSet} + */ + public ResultSetSupportingSqlParameter(String name, int sqlType, RowCallbackHandler rch) { + super(name, sqlType); + this.rowCallbackHandler = rch; + } + + /** + * Create a new ResultSetSupportingSqlParameter. + * @param name the name of the parameter, as used in input and output maps + * @param sqlType the parameter SQL type according to {@code java.sql.Types} + * @param rm the {@link RowMapper} to use for parsing the {@link ResultSet} + */ + public ResultSetSupportingSqlParameter(String name, int sqlType, RowMapper rm) { + super(name, sqlType); + this.rowMapper = rm; + } + + + /** + * Does this parameter support a ResultSet, i.e. does it hold a + * ResultSetExtractor, RowCallbackHandler or RowMapper? + */ + public boolean isResultSetSupported() { + return (this.resultSetExtractor != null || this.rowCallbackHandler != null || this.rowMapper != null); + } + + /** + * Return the ResultSetExtractor held by this parameter, if any. + */ + @Nullable + public ResultSetExtractor getResultSetExtractor() { + return this.resultSetExtractor; + } + + /** + * Return the RowCallbackHandler held by this parameter, if any. + */ + @Nullable + public RowCallbackHandler getRowCallbackHandler() { + return this.rowCallbackHandler; + } + + /** + * Return the RowMapper held by this parameter, if any. + */ + @Nullable + public RowMapper getRowMapper() { + return this.rowMapper; + } + + + /** + * This implementation always returns {@code false}. + */ + @Override + public boolean isInputValueProvided() { + return false; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/RowCallbackHandler.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/RowCallbackHandler.java new file mode 100644 index 0000000..5daf3de --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/RowCallbackHandler.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * An interface used by {@link JdbcTemplate} for processing rows of a + * {@link java.sql.ResultSet} on a per-row basis. Implementations of + * this interface perform the actual work of processing each row + * but don't need to worry about exception handling. + * {@link java.sql.SQLException SQLExceptions} will be caught and handled + * by the calling JdbcTemplate. + * + *

    In contrast to a {@link ResultSetExtractor}, a RowCallbackHandler + * object is typically stateful: It keeps the result state within the + * object, to be available for later inspection. See + * {@link RowCountCallbackHandler} for a usage example. + * + *

    Consider using a {@link RowMapper} instead if you need to map + * exactly one result object per row, assembling them into a List. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see JdbcTemplate + * @see RowMapper + * @see ResultSetExtractor + * @see RowCountCallbackHandler + */ +@FunctionalInterface +public interface RowCallbackHandler { + + /** + * Implementations must implement this method to process each row of data + * in the ResultSet. This method should not call {@code next()} on + * the ResultSet; it is only supposed to extract values of the current row. + *

    Exactly what the implementation chooses to do is up to it: + * A trivial implementation might simply count rows, while another + * implementation might build an XML document. + * @param rs the ResultSet to process (pre-initialized for the current row) + * @throws SQLException if an SQLException is encountered getting + * column values (that is, there's no need to catch SQLException) + */ + void processRow(ResultSet rs) throws SQLException; + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/RowCountCallbackHandler.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/RowCountCallbackHandler.java new file mode 100644 index 0000000..2d2b09a --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/RowCountCallbackHandler.java @@ -0,0 +1,143 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; + +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.lang.Nullable; + +/** + * Implementation of RowCallbackHandler. Convenient superclass for callback handlers. + * An instance can only be used once. + * + *

    We can either use this on its own (for example, in a test case, to ensure + * that our result sets have valid dimensions), or use it as a superclass + * for callback handlers that actually do something, and will benefit + * from the dimension information it provides. + * + *

    A usage example with JdbcTemplate: + * + *

    JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);  // reusable object
    + *
    + * RowCountCallbackHandler countCallback = new RowCountCallbackHandler();  // not reusable
    + * jdbcTemplate.query("select * from user", countCallback);
    + * int rowCount = countCallback.getRowCount();
    + * + * @author Rod Johnson + * @since May 3, 2001 + */ +public class RowCountCallbackHandler implements RowCallbackHandler { + + /** Rows we've seen so far. */ + private int rowCount; + + /** Columns we've seen so far. */ + private int columnCount; + + /** + * Indexed from 0. Type (as in java.sql.Types) for the columns + * as returned by ResultSetMetaData object. + */ + @Nullable + private int[] columnTypes; + + /** + * Indexed from 0. Column name as returned by ResultSetMetaData object. + */ + @Nullable + private String[] columnNames; + + + /** + * Implementation of ResultSetCallbackHandler. + * Work out column size if this is the first row, otherwise just count rows. + *

    Subclasses can perform custom extraction or processing + * by overriding the {@code processRow(ResultSet, int)} method. + * @see #processRow(java.sql.ResultSet, int) + */ + @Override + public final void processRow(ResultSet rs) throws SQLException { + if (this.rowCount == 0) { + ResultSetMetaData rsmd = rs.getMetaData(); + this.columnCount = rsmd.getColumnCount(); + this.columnTypes = new int[this.columnCount]; + this.columnNames = new String[this.columnCount]; + for (int i = 0; i < this.columnCount; i++) { + this.columnTypes[i] = rsmd.getColumnType(i + 1); + this.columnNames[i] = JdbcUtils.lookupColumnName(rsmd, i + 1); + } + // could also get column names + } + processRow(rs, this.rowCount++); + } + + /** + * Subclasses may override this to perform custom extraction + * or processing. This class's implementation does nothing. + * @param rs the ResultSet to extract data from. This method is + * invoked for each row + * @param rowNum number of the current row (starting from 0) + */ + protected void processRow(ResultSet rs, int rowNum) throws SQLException { + } + + + /** + * Return the types of the columns as java.sql.Types constants + * Valid after processRow is invoked the first time. + * @return the types of the columns as java.sql.Types constants. + * Indexed from 0 to n-1. + */ + @Nullable + public final int[] getColumnTypes() { + return this.columnTypes; + } + + /** + * Return the names of the columns. + * Valid after processRow is invoked the first time. + * @return the names of the columns. + * Indexed from 0 to n-1. + */ + @Nullable + public final String[] getColumnNames() { + return this.columnNames; + } + + /** + * Return the row count of this ResultSet. + * Only valid after processing is complete + * @return the number of rows in this ResultSet + */ + public final int getRowCount() { + return this.rowCount; + } + + /** + * Return the number of columns in this result set. + * Valid once we've seen the first row, + * so subclasses can use it during processing + * @return the number of columns in this result set + */ + public final int getColumnCount() { + return this.columnCount; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/RowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/RowMapper.java new file mode 100644 index 0000000..a520b67 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/RowMapper.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.springframework.lang.Nullable; + +/** + * An interface used by {@link JdbcTemplate} for mapping rows of a + * {@link java.sql.ResultSet} on a per-row basis. Implementations of this + * interface perform the actual work of mapping each row to a result object, + * but don't need to worry about exception handling. + * {@link java.sql.SQLException SQLExceptions} will be caught and handled + * by the calling JdbcTemplate. + * + *

    Typically used either for {@link JdbcTemplate}'s query methods + * or for out parameters of stored procedures. RowMapper objects are + * typically stateless and thus reusable; they are an ideal choice for + * implementing row-mapping logic in a single place. + * + *

    Alternatively, consider subclassing + * {@link org.springframework.jdbc.object.MappingSqlQuery} from the + * {@code jdbc.object} package: Instead of working with separate + * JdbcTemplate and RowMapper objects, you can build executable query + * objects (containing row-mapping logic) in that style. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @param the result type + * @see JdbcTemplate + * @see RowCallbackHandler + * @see ResultSetExtractor + * @see org.springframework.jdbc.object.MappingSqlQuery + */ +@FunctionalInterface +public interface RowMapper { + + /** + * Implementations must implement this method to map each row of data + * in the ResultSet. This method should not call {@code next()} on + * the ResultSet; it is only supposed to map values of the current row. + * @param rs the ResultSet to map (pre-initialized for the current row) + * @param rowNum the number of the current row + * @return the result object for the current row (may be {@code null}) + * @throws SQLException if an SQLException is encountered getting + * column values (that is, there's no need to catch SQLException) + */ + @Nullable + T mapRow(ResultSet rs, int rowNum) throws SQLException; + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/RowMapperResultSetExtractor.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/RowMapperResultSetExtractor.java new file mode 100644 index 0000000..25cbddc --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/RowMapperResultSetExtractor.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.util.Assert; + +/** + * Adapter implementation of the ResultSetExtractor interface that delegates + * to a RowMapper which is supposed to create an object for each row. + * Each object is added to the results List of this ResultSetExtractor. + * + *

    Useful for the typical case of one object per row in the database table. + * The number of entries in the results list will match the number of rows. + * + *

    Note that a RowMapper object is typically stateless and thus reusable; + * just the RowMapperResultSetExtractor adapter is stateful. + * + *

    A usage example with JdbcTemplate: + * + *

    JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);  // reusable object
    + * RowMapper rowMapper = new UserRowMapper();  // reusable object
    + *
    + * List allUsers = (List) jdbcTemplate.query(
    + *     "select * from user",
    + *     new RowMapperResultSetExtractor(rowMapper, 10));
    + *
    + * User user = (User) jdbcTemplate.queryForObject(
    + *     "select * from user where id=?", new Object[] {id},
    + *     new RowMapperResultSetExtractor(rowMapper, 1));
    + * + *

    Alternatively, consider subclassing MappingSqlQuery from the {@code jdbc.object} + * package: Instead of working with separate JdbcTemplate and RowMapper objects, + * you can have executable query objects (containing row-mapping logic) there. + * + * @author Juergen Hoeller + * @since 1.0.2 + * @param the result element type + * @see RowMapper + * @see JdbcTemplate + * @see org.springframework.jdbc.object.MappingSqlQuery + */ +public class RowMapperResultSetExtractor implements ResultSetExtractor> { + + private final RowMapper rowMapper; + + private final int rowsExpected; + + + /** + * Create a new RowMapperResultSetExtractor. + * @param rowMapper the RowMapper which creates an object for each row + */ + public RowMapperResultSetExtractor(RowMapper rowMapper) { + this(rowMapper, 0); + } + + /** + * Create a new RowMapperResultSetExtractor. + * @param rowMapper the RowMapper which creates an object for each row + * @param rowsExpected the number of expected rows + * (just used for optimized collection handling) + */ + public RowMapperResultSetExtractor(RowMapper rowMapper, int rowsExpected) { + Assert.notNull(rowMapper, "RowMapper is required"); + this.rowMapper = rowMapper; + this.rowsExpected = rowsExpected; + } + + + @Override + public List extractData(ResultSet rs) throws SQLException { + List results = (this.rowsExpected > 0 ? new ArrayList<>(this.rowsExpected) : new ArrayList<>()); + int rowNum = 0; + while (rs.next()) { + results.add(this.rowMapper.mapRow(rs, rowNum++)); + } + return results; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java new file mode 100644 index 0000000..3b76290 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java @@ -0,0 +1,243 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.dao.TypeMismatchDataAccessException; +import org.springframework.jdbc.IncorrectResultSetColumnCountException; +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.NumberUtils; + +/** + * {@link RowMapper} implementation that converts a single column into a single + * result value per row. Expects to operate on a {@code java.sql.ResultSet} + * that just contains a single column. + * + *

    The type of the result value for each row can be specified. The value + * for the single column will be extracted from the {@code ResultSet} + * and converted into the specified target type. + * + * @author Juergen Hoeller + * @author Kazuki Shimizu + * @since 1.2 + * @param the result type + * @see JdbcTemplate#queryForList(String, Class) + * @see JdbcTemplate#queryForObject(String, Class) + */ +public class SingleColumnRowMapper implements RowMapper { + + @Nullable + private Class requiredType; + + @Nullable + private ConversionService conversionService = DefaultConversionService.getSharedInstance(); + + /** + * Create a new {@code SingleColumnRowMapper} for bean-style configuration. + * @see #setRequiredType + */ + public SingleColumnRowMapper() { + } + + /** + * Create a new {@code SingleColumnRowMapper}. + * @param requiredType the type that each result object is expected to match + */ + public SingleColumnRowMapper(Class requiredType) { + setRequiredType(requiredType); + } + + + /** + * Set the type that each result object is expected to match. + *

    If not specified, the column value will be exposed as + * returned by the JDBC driver. + */ + public void setRequiredType(Class requiredType) { + this.requiredType = ClassUtils.resolvePrimitiveIfNecessary(requiredType); + } + + /** + * Set a {@link ConversionService} for converting a fetched value. + *

    Default is the {@link DefaultConversionService}. + * @since 5.0.4 + * @see DefaultConversionService#getSharedInstance + */ + public void setConversionService(@Nullable ConversionService conversionService) { + this.conversionService = conversionService; + } + + /** + * Extract a value for the single column in the current row. + *

    Validates that there is only one column selected, + * then delegates to {@code getColumnValue()} and also + * {@code convertValueToRequiredType}, if necessary. + * @see java.sql.ResultSetMetaData#getColumnCount() + * @see #getColumnValue(java.sql.ResultSet, int, Class) + * @see #convertValueToRequiredType(Object, Class) + */ + @Override + @SuppressWarnings("unchecked") + @Nullable + public T mapRow(ResultSet rs, int rowNum) throws SQLException { + // Validate column count. + ResultSetMetaData rsmd = rs.getMetaData(); + int nrOfColumns = rsmd.getColumnCount(); + if (nrOfColumns != 1) { + throw new IncorrectResultSetColumnCountException(1, nrOfColumns); + } + + // Extract column value from JDBC ResultSet. + Object result = getColumnValue(rs, 1, this.requiredType); + if (result != null && this.requiredType != null && !this.requiredType.isInstance(result)) { + // Extracted value does not match already: try to convert it. + try { + return (T) convertValueToRequiredType(result, this.requiredType); + } + catch (IllegalArgumentException ex) { + throw new TypeMismatchDataAccessException( + "Type mismatch affecting row number " + rowNum + " and column type '" + + rsmd.getColumnTypeName(1) + "': " + ex.getMessage()); + } + } + return (T) result; + } + + /** + * Retrieve a JDBC object value for the specified column. + *

    The default implementation calls + * {@link JdbcUtils#getResultSetValue(java.sql.ResultSet, int, Class)}. + * If no required type has been specified, this method delegates to + * {@code getColumnValue(rs, index)}, which basically calls + * {@code ResultSet.getObject(index)} but applies some additional + * default conversion to appropriate value types. + * @param rs is the ResultSet holding the data + * @param index is the column index + * @param requiredType the type that each result object is expected to match + * (or {@code null} if none specified) + * @return the Object value + * @throws SQLException in case of extraction failure + * @see org.springframework.jdbc.support.JdbcUtils#getResultSetValue(java.sql.ResultSet, int, Class) + * @see #getColumnValue(java.sql.ResultSet, int) + */ + @Nullable + protected Object getColumnValue(ResultSet rs, int index, @Nullable Class requiredType) throws SQLException { + if (requiredType != null) { + return JdbcUtils.getResultSetValue(rs, index, requiredType); + } + else { + // No required type specified -> perform default extraction. + return getColumnValue(rs, index); + } + } + + /** + * Retrieve a JDBC object value for the specified column, using the most + * appropriate value type. Called if no required type has been specified. + *

    The default implementation delegates to {@code JdbcUtils.getResultSetValue()}, + * which uses the {@code ResultSet.getObject(index)} method. Additionally, + * it includes a "hack" to get around Oracle returning a non-standard object for + * their TIMESTAMP datatype. See the {@code JdbcUtils#getResultSetValue()} + * javadoc for details. + * @param rs is the ResultSet holding the data + * @param index is the column index + * @return the Object value + * @throws SQLException in case of extraction failure + * @see org.springframework.jdbc.support.JdbcUtils#getResultSetValue(java.sql.ResultSet, int) + */ + @Nullable + protected Object getColumnValue(ResultSet rs, int index) throws SQLException { + return JdbcUtils.getResultSetValue(rs, index); + } + + /** + * Convert the given column value to the specified required type. + * Only called if the extracted column value does not match already. + *

    If the required type is String, the value will simply get stringified + * via {@code toString()}. In case of a Number, the value will be + * converted into a Number, either through number conversion or through + * String parsing (depending on the value type). Otherwise, the value will + * be converted to a required type using the {@link ConversionService}. + * @param value the column value as extracted from {@code getColumnValue()} + * (never {@code null}) + * @param requiredType the type that each result object is expected to match + * (never {@code null}) + * @return the converted value + * @see #getColumnValue(java.sql.ResultSet, int, Class) + */ + @SuppressWarnings("unchecked") + @Nullable + protected Object convertValueToRequiredType(Object value, Class requiredType) { + if (String.class == requiredType) { + return value.toString(); + } + else if (Number.class.isAssignableFrom(requiredType)) { + if (value instanceof Number) { + // Convert original Number to target Number class. + return NumberUtils.convertNumberToTargetClass(((Number) value), (Class) requiredType); + } + else { + // Convert stringified value to target Number class. + return NumberUtils.parseNumber(value.toString(),(Class) requiredType); + } + } + else if (this.conversionService != null && this.conversionService.canConvert(value.getClass(), requiredType)) { + return this.conversionService.convert(value, requiredType); + } + else { + throw new IllegalArgumentException( + "Value [" + value + "] is of type [" + value.getClass().getName() + + "] and cannot be converted to required type [" + requiredType.getName() + "]"); + } + } + + + /** + * Static factory method to create a new {@code SingleColumnRowMapper}. + * @param requiredType the type that each result object is expected to match + * @since 4.1 + * @see #newInstance(Class, ConversionService) + */ + public static SingleColumnRowMapper newInstance(Class requiredType) { + return new SingleColumnRowMapper<>(requiredType); + } + + /** + * Static factory method to create a new {@code SingleColumnRowMapper}. + * @param requiredType the type that each result object is expected to match + * @param conversionService the {@link ConversionService} for converting a + * fetched value, or {@code null} for none + * @since 5.0.4 + * @see #newInstance(Class) + * @see #setConversionService + */ + public static SingleColumnRowMapper newInstance( + Class requiredType, @Nullable ConversionService conversionService) { + + SingleColumnRowMapper rowMapper = newInstance(requiredType); + rowMapper.setConversionService(conversionService); + return rowMapper; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlInOutParameter.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlInOutParameter.java new file mode 100644 index 0000000..578fb99 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlInOutParameter.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.ResultSet; + +/** + * Subclass of {@link SqlOutParameter} to represent an INOUT parameter. + * Will return {@code true} for SqlParameter's {@link #isInputValueProvided} + * test, in contrast to a standard SqlOutParameter. + * + *

    Output parameters - like all stored procedure parameters - must have names. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.0 + */ +public class SqlInOutParameter extends SqlOutParameter { + + /** + * Create a new SqlInOutParameter. + * @param name the name of the parameter, as used in input and output maps + * @param sqlType the parameter SQL type according to {@code java.sql.Types} + */ + public SqlInOutParameter(String name, int sqlType) { + super(name, sqlType); + } + + /** + * Create a new SqlInOutParameter. + * @param name the name of the parameter, as used in input and output maps + * @param sqlType the parameter SQL type according to {@code java.sql.Types} + * @param scale the number of digits after the decimal point + * (for DECIMAL and NUMERIC types) + */ + public SqlInOutParameter(String name, int sqlType, int scale) { + super(name, sqlType, scale); + } + + /** + * Create a new SqlInOutParameter. + * @param name the name of the parameter, as used in input and output maps + * @param sqlType the parameter SQL type according to {@code java.sql.Types} + * @param typeName the type name of the parameter (optional) + */ + public SqlInOutParameter(String name, int sqlType, String typeName) { + super(name, sqlType, typeName); + } + + /** + * Create a new SqlInOutParameter. + * @param name the name of the parameter, as used in input and output maps + * @param sqlType the parameter SQL type according to {@code java.sql.Types} + * @param typeName the type name of the parameter (optional) + * @param sqlReturnType custom value handler for complex type (optional) + */ + public SqlInOutParameter(String name, int sqlType, String typeName, SqlReturnType sqlReturnType) { + super(name, sqlType, typeName, sqlReturnType); + } + + /** + * Create a new SqlInOutParameter. + * @param name the name of the parameter, as used in input and output maps + * @param sqlType the parameter SQL type according to {@code java.sql.Types} + * @param rse the {@link ResultSetExtractor} to use for parsing the {@link ResultSet} + */ + public SqlInOutParameter(String name, int sqlType, ResultSetExtractor rse) { + super(name, sqlType, rse); + } + + /** + * Create a new SqlInOutParameter. + * @param name the name of the parameter, as used in input and output maps + * @param sqlType the parameter SQL type according to {@code java.sql.Types} + * @param rch the {@link RowCallbackHandler} to use for parsing the {@link ResultSet} + */ + public SqlInOutParameter(String name, int sqlType, RowCallbackHandler rch) { + super(name, sqlType, rch); + } + + /** + * Create a new SqlInOutParameter. + * @param name the name of the parameter, as used in input and output maps + * @param sqlType the parameter SQL type according to {@code java.sql.Types} + * @param rm the {@link RowMapper} to use for parsing the {@link ResultSet} + */ + public SqlInOutParameter(String name, int sqlType, RowMapper rm) { + super(name, sqlType, rm); + } + + + /** + * This implementation always returns {@code true}. + */ + @Override + public boolean isInputValueProvided() { + return true; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlOutParameter.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlOutParameter.java new file mode 100644 index 0000000..a43ec1a --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlOutParameter.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.ResultSet; + +import org.springframework.lang.Nullable; + +/** + * Subclass of {@link SqlParameter} to represent an output parameter. + * No additional properties: instanceof will be used to check for such types. + * + *

    Output parameters - like all stored procedure parameters - must have names. + * + * @author Rod Johnson + * @author Thomas Risberg + * @author Juergen Hoeller + * @see SqlReturnResultSet + * @see SqlInOutParameter + */ +public class SqlOutParameter extends ResultSetSupportingSqlParameter { + + @Nullable + private SqlReturnType sqlReturnType; + + + /** + * Create a new SqlOutParameter. + * @param name the name of the parameter, as used in input and output maps + * @param sqlType the parameter SQL type according to {@code java.sql.Types} + */ + public SqlOutParameter(String name, int sqlType) { + super(name, sqlType); + } + + /** + * Create a new SqlOutParameter. + * @param name the name of the parameter, as used in input and output maps + * @param sqlType the parameter SQL type according to {@code java.sql.Types} + * @param scale the number of digits after the decimal point + * (for DECIMAL and NUMERIC types) + */ + public SqlOutParameter(String name, int sqlType, int scale) { + super(name, sqlType, scale); + } + + /** + * Create a new SqlOutParameter. + * @param name the name of the parameter, as used in input and output maps + * @param sqlType the parameter SQL type according to {@code java.sql.Types} + * @param typeName the type name of the parameter (optional) + */ + public SqlOutParameter(String name, int sqlType, @Nullable String typeName) { + super(name, sqlType, typeName); + } + + /** + * Create a new SqlOutParameter. + * @param name the name of the parameter, as used in input and output maps + * @param sqlType the parameter SQL type according to {@code java.sql.Types} + * @param typeName the type name of the parameter (optional) + * @param sqlReturnType custom value handler for complex type (optional) + */ + public SqlOutParameter(String name, int sqlType, @Nullable String typeName, @Nullable SqlReturnType sqlReturnType) { + super(name, sqlType, typeName); + this.sqlReturnType = sqlReturnType; + } + + /** + * Create a new SqlOutParameter. + * @param name the name of the parameter, as used in input and output maps + * @param sqlType the parameter SQL type according to {@code java.sql.Types} + * @param rse the {@link ResultSetExtractor} to use for parsing the {@link ResultSet} + */ + public SqlOutParameter(String name, int sqlType, ResultSetExtractor rse) { + super(name, sqlType, rse); + } + + /** + * Create a new SqlOutParameter. + * @param name the name of the parameter, as used in input and output maps + * @param sqlType the parameter SQL type according to {@code java.sql.Types} + * @param rch the {@link RowCallbackHandler} to use for parsing the {@link ResultSet} + */ + public SqlOutParameter(String name, int sqlType, RowCallbackHandler rch) { + super(name, sqlType, rch); + } + + /** + * Create a new SqlOutParameter. + * @param name the name of the parameter, as used in input and output maps + * @param sqlType the parameter SQL type according to {@code java.sql.Types} + * @param rm the {@link RowMapper} to use for parsing the {@link ResultSet} + */ + public SqlOutParameter(String name, int sqlType, RowMapper rm) { + super(name, sqlType, rm); + } + + + /** + * Return the custom return type, if any. + */ + @Nullable + public SqlReturnType getSqlReturnType() { + return this.sqlReturnType; + } + + /** + * Return whether this parameter holds a custom return type. + */ + public boolean isReturnTypeSupported() { + return (this.sqlReturnType != null); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlParameter.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlParameter.java new file mode 100644 index 0000000..339611d --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlParameter.java @@ -0,0 +1,197 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Object to represent an SQL parameter definition. + * + *

    Parameters may be anonymous, in which case "name" is {@code null}. + * However, all parameters must define an SQL type according to {@link java.sql.Types}. + * + * @author Rod Johnson + * @author Thomas Risberg + * @author Juergen Hoeller + * @see java.sql.Types + */ +public class SqlParameter { + + // The name of the parameter, if any + @Nullable + private String name; + + // SQL type constant from {@code java.sql.Types} + private final int sqlType; + + // Used for types that are user-named like: STRUCT, DISTINCT, JAVA_OBJECT, named array types + @Nullable + private String typeName; + + // The scale to apply in case of a NUMERIC or DECIMAL type, if any + @Nullable + private Integer scale; + + + /** + * Create a new anonymous SqlParameter, supplying the SQL type. + * @param sqlType the SQL type of the parameter according to {@code java.sql.Types} + */ + public SqlParameter(int sqlType) { + this.sqlType = sqlType; + } + + /** + * Create a new anonymous SqlParameter, supplying the SQL type. + * @param sqlType the SQL type of the parameter according to {@code java.sql.Types} + * @param typeName the type name of the parameter (optional) + */ + public SqlParameter(int sqlType, @Nullable String typeName) { + this.sqlType = sqlType; + this.typeName = typeName; + } + + /** + * Create a new anonymous SqlParameter, supplying the SQL type. + * @param sqlType the SQL type of the parameter according to {@code java.sql.Types} + * @param scale the number of digits after the decimal point + * (for DECIMAL and NUMERIC types) + */ + public SqlParameter(int sqlType, int scale) { + this.sqlType = sqlType; + this.scale = scale; + } + + /** + * Create a new SqlParameter, supplying name and SQL type. + * @param name the name of the parameter, as used in input and output maps + * @param sqlType the SQL type of the parameter according to {@code java.sql.Types} + */ + public SqlParameter(String name, int sqlType) { + this.name = name; + this.sqlType = sqlType; + } + + /** + * Create a new SqlParameter, supplying name and SQL type. + * @param name the name of the parameter, as used in input and output maps + * @param sqlType the SQL type of the parameter according to {@code java.sql.Types} + * @param typeName the type name of the parameter (optional) + */ + public SqlParameter(String name, int sqlType, @Nullable String typeName) { + this.name = name; + this.sqlType = sqlType; + this.typeName = typeName; + } + + /** + * Create a new SqlParameter, supplying name and SQL type. + * @param name the name of the parameter, as used in input and output maps + * @param sqlType the SQL type of the parameter according to {@code java.sql.Types} + * @param scale the number of digits after the decimal point + * (for DECIMAL and NUMERIC types) + */ + public SqlParameter(String name, int sqlType, int scale) { + this.name = name; + this.sqlType = sqlType; + this.scale = scale; + } + + /** + * Copy constructor. + * @param otherParam the SqlParameter object to copy from + */ + public SqlParameter(SqlParameter otherParam) { + Assert.notNull(otherParam, "SqlParameter object must not be null"); + this.name = otherParam.name; + this.sqlType = otherParam.sqlType; + this.typeName = otherParam.typeName; + this.scale = otherParam.scale; + } + + + /** + * Return the name of the parameter, or {@code null} if anonymous. + */ + @Nullable + public String getName() { + return this.name; + } + + /** + * Return the SQL type of the parameter. + */ + public int getSqlType() { + return this.sqlType; + } + + /** + * Return the type name of the parameter, if any. + */ + @Nullable + public String getTypeName() { + return this.typeName; + } + + /** + * Return the scale of the parameter, if any. + */ + @Nullable + public Integer getScale() { + return this.scale; + } + + + /** + * Return whether this parameter holds input values that should be set + * before execution even if they are {@code null}. + *

    This implementation always returns {@code true}. + */ + public boolean isInputValueProvided() { + return true; + } + + /** + * Return whether this parameter is an implicit return parameter used during the + * results processing of {@code CallableStatement.getMoreResults/getUpdateCount}. + *

    This implementation always returns {@code false}. + */ + public boolean isResultsParameter() { + return false; + } + + + /** + * Convert a list of JDBC types, as defined in {@code java.sql.Types}, + * to a List of SqlParameter objects as used in this package. + */ + public static List sqlTypesToAnonymousParameterList(@Nullable int... types) { + if (types == null) { + return new ArrayList<>(); + } + List result = new ArrayList<>(types.length); + for (int type : types) { + result.add(new SqlParameter(type)); + } + return result; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlParameterValue.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlParameterValue.java new file mode 100644 index 0000000..30159fe --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlParameterValue.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import org.springframework.lang.Nullable; + +/** + * Object to represent an SQL parameter value, including parameter meta-data + * such as the SQL type and the scale for numeric values. + * + *

    Designed for use with {@link JdbcTemplate}'s operations that take an array of + * argument values: Each such argument value may be a {@code SqlParameterValue}, + * indicating the SQL type (and optionally the scale) instead of letting the + * template guess a default type. Note that this only applies to the operations with + * a 'plain' argument array, not to the overloaded variants with an explicit type array. + * + * @author Juergen Hoeller + * @since 2.0.5 + * @see java.sql.Types + * @see JdbcTemplate#query(String, ResultSetExtractor, Object[]) + * @see JdbcTemplate#query(String, RowCallbackHandler, Object[]) + * @see JdbcTemplate#query(String, RowMapper, Object[]) + * @see JdbcTemplate#update(String, Object[]) + */ +public class SqlParameterValue extends SqlParameter { + + @Nullable + private final Object value; + + + /** + * Create a new SqlParameterValue, supplying the SQL type. + * @param sqlType the SQL type of the parameter according to {@code java.sql.Types} + * @param value the value object + */ + public SqlParameterValue(int sqlType, @Nullable Object value) { + super(sqlType); + this.value = value; + } + + /** + * Create a new SqlParameterValue, supplying the SQL type. + * @param sqlType the SQL type of the parameter according to {@code java.sql.Types} + * @param typeName the type name of the parameter (optional) + * @param value the value object + */ + public SqlParameterValue(int sqlType, @Nullable String typeName, @Nullable Object value) { + super(sqlType, typeName); + this.value = value; + } + + /** + * Create a new SqlParameterValue, supplying the SQL type. + * @param sqlType the SQL type of the parameter according to {@code java.sql.Types} + * @param scale the number of digits after the decimal point + * (for DECIMAL and NUMERIC types) + * @param value the value object + */ + public SqlParameterValue(int sqlType, int scale, @Nullable Object value) { + super(sqlType, scale); + this.value = value; + } + + /** + * Create a new SqlParameterValue based on the given SqlParameter declaration. + * @param declaredParam the declared SqlParameter to define a value for + * @param value the value object + */ + public SqlParameterValue(SqlParameter declaredParam, @Nullable Object value) { + super(declaredParam); + this.value = value; + } + + + /** + * Return the value object that this parameter value holds. + */ + @Nullable + public Object getValue() { + return this.value; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlProvider.java new file mode 100644 index 0000000..e060bc1 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlProvider.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import org.springframework.lang.Nullable; + +/** + * Interface to be implemented by objects that can provide SQL strings. + * + *

    Typically implemented by PreparedStatementCreators, CallableStatementCreators + * and StatementCallbacks that want to expose the SQL they use to create their + * statements, to allow for better contextual information in case of exceptions. + * + * @author Juergen Hoeller + * @since 16.03.2004 + * @see PreparedStatementCreator + * @see CallableStatementCreator + * @see StatementCallback + */ +public interface SqlProvider { + + /** + * Return the SQL string for this object, i.e. + * typically the SQL used for creating statements. + * @return the SQL string, or {@code null} if not available + */ + @Nullable + String getSql(); + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlReturnResultSet.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlReturnResultSet.java new file mode 100644 index 0000000..6ce09fc --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlReturnResultSet.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +/** + * Represents a returned {@link java.sql.ResultSet} from a stored procedure call. + * + *

    A {@link ResultSetExtractor}, {@link RowCallbackHandler} or {@link RowMapper} + * must be provided to handle any returned rows. + * + *

    Returned {@link java.sql.ResultSet ResultSets} - like all stored procedure + * parameters - must have names. + * + * @author Thomas Risberg + * @author Juergen Hoeller + */ +public class SqlReturnResultSet extends ResultSetSupportingSqlParameter { + + /** + * Create a new instance of the {@link SqlReturnResultSet} class. + * @param name the name of the parameter, as used in input and output maps + * @param extractor the {@link ResultSetExtractor} to use for parsing the {@link java.sql.ResultSet} + */ + public SqlReturnResultSet(String name, ResultSetExtractor extractor) { + super(name, 0, extractor); + } + + /** + * Create a new instance of the {@link SqlReturnResultSet} class. + * @param name the name of the parameter, as used in input and output maps + * @param handler the {@link RowCallbackHandler} to use for parsing the {@link java.sql.ResultSet} + */ + public SqlReturnResultSet(String name, RowCallbackHandler handler) { + super(name, 0, handler); + } + + /** + * Create a new instance of the {@link SqlReturnResultSet} class. + * @param name the name of the parameter, as used in input and output maps + * @param mapper the {@link RowMapper} to use for parsing the {@link java.sql.ResultSet} + */ + public SqlReturnResultSet(String name, RowMapper mapper) { + super(name, 0, mapper); + } + + + /** + * This implementation always returns {@code true}. + */ + @Override + public boolean isResultsParameter() { + return true; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlReturnType.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlReturnType.java new file mode 100644 index 0000000..f2558c6 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlReturnType.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.CallableStatement; +import java.sql.SQLException; + +import org.springframework.lang.Nullable; + +/** + * Interface to be implemented for retrieving values for more complex database-specific + * types not supported by the standard {@code CallableStatement.getObject} method. + * + *

    Implementations perform the actual work of getting the actual values. They must + * implement the callback method {@code getTypeValue} which can throw SQLExceptions + * that will be caught and translated by the calling code. This callback method has + * access to the underlying Connection via the given CallableStatement object, if that + * should be needed to create any database-specific objects. + * + * @author Thomas Risberg + * @since 1.1 + * @see java.sql.Types + * @see java.sql.CallableStatement#getObject + * @see org.springframework.jdbc.object.StoredProcedure#execute(java.util.Map) + */ +public interface SqlReturnType { + + /** + * Constant that indicates an unknown (or unspecified) SQL type. + * Passed into setTypeValue if the original operation method does + * not specify an SQL type. + * @see java.sql.Types + * @see JdbcOperations#update(String, Object[]) + */ + int TYPE_UNKNOWN = Integer.MIN_VALUE; + + + /** + * Get the type value from the specific object. + * @param cs the CallableStatement to operate on + * @param paramIndex the index of the parameter for which we need to set the value + * @param sqlType the SQL type of the parameter we are setting + * @param typeName the type name of the parameter (optional) + * @return the target value + * @throws SQLException if an SQLException is encountered setting parameter values + * (that is, there's no need to catch SQLException) + * @see java.sql.Types + * @see java.sql.CallableStatement#getObject + */ + Object getTypeValue(CallableStatement cs, int paramIndex, int sqlType, @Nullable String typeName) + throws SQLException; + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlReturnUpdateCount.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlReturnUpdateCount.java new file mode 100644 index 0000000..f0a1cd1 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlReturnUpdateCount.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.Types; + +/** + * Represents a returned update count from a stored procedure call. + * + *

    Returned update counts - like all stored procedure + * parameters - must have names. + * + * @author Thomas Risberg + */ +public class SqlReturnUpdateCount extends SqlParameter { + + /** + * Create a new SqlReturnUpdateCount. + * @param name the name of the parameter, as used in input and output maps + */ + public SqlReturnUpdateCount(String name) { + super(name, Types.INTEGER); + } + + + /** + * This implementation always returns {@code false}. + */ + @Override + public boolean isInputValueProvided() { + return false; + } + + /** + * This implementation always returns {@code true}. + */ + @Override + public boolean isResultsParameter() { + return true; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlRowSetResultSetExtractor.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlRowSetResultSetExtractor.java new file mode 100644 index 0000000..b4c36b6 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlRowSetResultSetExtractor.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import javax.sql.rowset.CachedRowSet; +import javax.sql.rowset.RowSetFactory; +import javax.sql.rowset.RowSetProvider; + +import org.springframework.jdbc.support.rowset.ResultSetWrappingSqlRowSet; +import org.springframework.jdbc.support.rowset.SqlRowSet; + +/** + * {@link ResultSetExtractor} implementation that returns a Spring {@link SqlRowSet} + * representation for each given {@link ResultSet}. + * + *

    The default implementation uses a standard JDBC CachedRowSet underneath. + * + * @author Juergen Hoeller + * @since 1.2 + * @see #newCachedRowSet + * @see org.springframework.jdbc.support.rowset.SqlRowSet + * @see JdbcTemplate#queryForRowSet(String) + * @see javax.sql.rowset.CachedRowSet + */ +public class SqlRowSetResultSetExtractor implements ResultSetExtractor { + + private static final RowSetFactory rowSetFactory; + + static { + try { + rowSetFactory = RowSetProvider.newFactory(); + } + catch (SQLException ex) { + throw new IllegalStateException("Cannot create RowSetFactory through RowSetProvider", ex); + } + } + + + @Override + public SqlRowSet extractData(ResultSet rs) throws SQLException { + return createSqlRowSet(rs); + } + + /** + * Create a {@link SqlRowSet} that wraps the given {@link ResultSet}, + * representing its data in a disconnected fashion. + *

    This implementation creates a Spring {@link ResultSetWrappingSqlRowSet} + * instance that wraps a standard JDBC {@link CachedRowSet} instance. + * Can be overridden to use a different implementation. + * @param rs the original ResultSet (connected) + * @return the disconnected SqlRowSet + * @throws SQLException if thrown by JDBC methods + * @see #newCachedRowSet() + * @see org.springframework.jdbc.support.rowset.ResultSetWrappingSqlRowSet + */ + protected SqlRowSet createSqlRowSet(ResultSet rs) throws SQLException { + CachedRowSet rowSet = newCachedRowSet(); + rowSet.populate(rs); + return new ResultSetWrappingSqlRowSet(rowSet); + } + + /** + * Create a new {@link CachedRowSet} instance, to be populated by + * the {@code createSqlRowSet} implementation. + *

    The default implementation uses JDBC 4.1's {@link RowSetFactory}. + * @return a new CachedRowSet instance + * @throws SQLException if thrown by JDBC methods + * @see #createSqlRowSet + * @see RowSetProvider#newFactory() + * @see RowSetFactory#createCachedRowSet() + */ + protected CachedRowSet newCachedRowSet() throws SQLException { + return rowSetFactory.createCachedRowSet(); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlTypeValue.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlTypeValue.java new file mode 100644 index 0000000..2349ac1 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlTypeValue.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.lang.Nullable; + +/** + * Interface to be implemented for setting values for more complex database-specific + * types not supported by the standard {@code setObject} method. This is + * effectively an extended variant of {@link org.springframework.jdbc.support.SqlValue}. + * + *

    Implementations perform the actual work of setting the actual values. They must + * implement the callback method {@code setTypeValue} which can throw SQLExceptions + * that will be caught and translated by the calling code. This callback method has + * access to the underlying Connection via the given PreparedStatement object, if that + * should be needed to create any database-specific objects. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 1.1 + * @see java.sql.Types + * @see java.sql.PreparedStatement#setObject + * @see JdbcOperations#update(String, Object[], int[]) + * @see org.springframework.jdbc.support.SqlValue + */ +public interface SqlTypeValue { + + /** + * Constant that indicates an unknown (or unspecified) SQL type. + * Passed into {@code setTypeValue} if the original operation method + * does not specify an SQL type. + * @see java.sql.Types + * @see JdbcOperations#update(String, Object[]) + */ + int TYPE_UNKNOWN = JdbcUtils.TYPE_UNKNOWN; + + + /** + * Set the type value on the given PreparedStatement. + * @param ps the PreparedStatement to work on + * @param paramIndex the index of the parameter for which we need to set the value + * @param sqlType the SQL type of the parameter we are setting + * @param typeName the type name of the parameter (optional) + * @throws SQLException if an SQLException is encountered while setting parameter values + * @see java.sql.Types + * @see java.sql.PreparedStatement#setObject + */ + void setTypeValue(PreparedStatement ps, int paramIndex, int sqlType, @Nullable String typeName) + throws SQLException; + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/StatementCallback.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/StatementCallback.java new file mode 100644 index 0000000..1297b94 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/StatementCallback.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.SQLException; +import java.sql.Statement; + +import org.springframework.dao.DataAccessException; +import org.springframework.lang.Nullable; + +/** + * Generic callback interface for code that operates on a JDBC Statement. + * Allows to execute any number of operations on a single Statement, + * for example a single {@code executeUpdate} call or repeated + * {@code executeUpdate} calls with varying SQL. + * + *

    Used internally by JdbcTemplate, but also useful for application code. + * + * @author Juergen Hoeller + * @since 16.03.2004 + * @param the result type + * @see JdbcTemplate#execute(StatementCallback) + */ +@FunctionalInterface +public interface StatementCallback { + + /** + * Gets called by {@code JdbcTemplate.execute} with an active JDBC + * Statement. Does not need to care about closing the Statement or the + * Connection, or about handling transactions: this will all be handled + * by Spring's JdbcTemplate. + *

    NOTE: Any ResultSets opened should be closed in finally blocks + * within the callback implementation. Spring will close the Statement + * object after the callback returned, but this does not necessarily imply + * that the ResultSet resources will be closed: the Statement objects might + * get pooled by the connection pool, with {@code close} calls only + * returning the object to the pool but not physically closing the resources. + *

    If called without a thread-bound JDBC transaction (initiated by + * DataSourceTransactionManager), the code will simply get executed on the + * JDBC connection with its transactional semantics. If JdbcTemplate is + * configured to use a JTA-aware DataSource, the JDBC connection and thus + * the callback code will be transactional if a JTA transaction is active. + *

    Allows for returning a result object created within the callback, i.e. + * a domain object or a collection of domain objects. Note that there's + * special support for single step actions: see JdbcTemplate.queryForObject etc. + * A thrown RuntimeException is treated as application exception, it gets + * propagated to the caller of the template. + * @param stmt active JDBC Statement + * @return a result object, or {@code null} if none + * @throws SQLException if thrown by a JDBC method, to be auto-converted + * to a DataAccessException by an SQLExceptionTranslator + * @throws DataAccessException in case of custom exceptions + * @see JdbcTemplate#queryForObject(String, Class) + * @see JdbcTemplate#queryForRowSet(String) + */ + @Nullable + T doInStatement(Statement stmt) throws SQLException, DataAccessException; + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/StatementCreatorUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/StatementCreatorUtils.java new file mode 100644 index 0000000..5c6c1d8 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/StatementCreatorUtils.java @@ -0,0 +1,476 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.io.StringReader; +import java.io.StringWriter; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.SpringProperties; +import org.springframework.jdbc.support.SqlValue; +import org.springframework.lang.Nullable; + +/** + * Utility methods for PreparedStatementSetter/Creator and CallableStatementCreator + * implementations, providing sophisticated parameter management (including support + * for LOB values). + * + *

    Used by PreparedStatementCreatorFactory and CallableStatementCreatorFactory, + * but also available for direct use in custom setter/creator implementations. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 1.1 + * @see PreparedStatementSetter + * @see PreparedStatementCreator + * @see CallableStatementCreator + * @see PreparedStatementCreatorFactory + * @see CallableStatementCreatorFactory + * @see SqlParameter + * @see SqlTypeValue + * @see org.springframework.jdbc.core.support.SqlLobValue + */ +public abstract class StatementCreatorUtils { + + /** + * System property that instructs Spring to ignore {@link java.sql.ParameterMetaData#getParameterType} + * completely, i.e. to never even attempt to retrieve {@link PreparedStatement#getParameterMetaData()} + * for {@link StatementCreatorUtils#setNull} calls. + *

    The default is "false", trying {@code getParameterType} calls first and falling back to + * {@link PreparedStatement#setNull} / {@link PreparedStatement#setObject} calls based on + * well-known behavior of common databases. + *

    Consider switching this flag to "true" if you experience misbehavior at runtime, + * e.g. with connection pool issues in case of an exception thrown from {@code getParameterType} + * (as reported on JBoss AS 7) or in case of performance problems (as reported on PostgreSQL). + */ + public static final String IGNORE_GETPARAMETERTYPE_PROPERTY_NAME = "spring.jdbc.getParameterType.ignore"; + + + static boolean shouldIgnoreGetParameterType = SpringProperties.getFlag(IGNORE_GETPARAMETERTYPE_PROPERTY_NAME); + + private static final Log logger = LogFactory.getLog(StatementCreatorUtils.class); + + private static final Map, Integer> javaTypeToSqlTypeMap = new HashMap<>(32); + + static { + javaTypeToSqlTypeMap.put(boolean.class, Types.BOOLEAN); + javaTypeToSqlTypeMap.put(Boolean.class, Types.BOOLEAN); + javaTypeToSqlTypeMap.put(byte.class, Types.TINYINT); + javaTypeToSqlTypeMap.put(Byte.class, Types.TINYINT); + javaTypeToSqlTypeMap.put(short.class, Types.SMALLINT); + javaTypeToSqlTypeMap.put(Short.class, Types.SMALLINT); + javaTypeToSqlTypeMap.put(int.class, Types.INTEGER); + javaTypeToSqlTypeMap.put(Integer.class, Types.INTEGER); + javaTypeToSqlTypeMap.put(long.class, Types.BIGINT); + javaTypeToSqlTypeMap.put(Long.class, Types.BIGINT); + javaTypeToSqlTypeMap.put(BigInteger.class, Types.BIGINT); + javaTypeToSqlTypeMap.put(float.class, Types.FLOAT); + javaTypeToSqlTypeMap.put(Float.class, Types.FLOAT); + javaTypeToSqlTypeMap.put(double.class, Types.DOUBLE); + javaTypeToSqlTypeMap.put(Double.class, Types.DOUBLE); + javaTypeToSqlTypeMap.put(BigDecimal.class, Types.DECIMAL); + javaTypeToSqlTypeMap.put(java.sql.Date.class, Types.DATE); + javaTypeToSqlTypeMap.put(java.sql.Time.class, Types.TIME); + javaTypeToSqlTypeMap.put(java.sql.Timestamp.class, Types.TIMESTAMP); + javaTypeToSqlTypeMap.put(Blob.class, Types.BLOB); + javaTypeToSqlTypeMap.put(Clob.class, Types.CLOB); + } + + + /** + * Derive a default SQL type from the given Java type. + * @param javaType the Java type to translate + * @return the corresponding SQL type, or {@link SqlTypeValue#TYPE_UNKNOWN} if none found + */ + public static int javaTypeToSqlParameterType(@Nullable Class javaType) { + if (javaType == null) { + return SqlTypeValue.TYPE_UNKNOWN; + } + Integer sqlType = javaTypeToSqlTypeMap.get(javaType); + if (sqlType != null) { + return sqlType; + } + if (Number.class.isAssignableFrom(javaType)) { + return Types.NUMERIC; + } + if (isStringValue(javaType)) { + return Types.VARCHAR; + } + if (isDateValue(javaType) || Calendar.class.isAssignableFrom(javaType)) { + return Types.TIMESTAMP; + } + return SqlTypeValue.TYPE_UNKNOWN; + } + + /** + * Set the value for a parameter. The method used is based on the SQL type + * of the parameter and we can handle complex types like arrays and LOBs. + * @param ps the prepared statement or callable statement + * @param paramIndex index of the parameter we are setting + * @param param the parameter as it is declared including type + * @param inValue the value to set + * @throws SQLException if thrown by PreparedStatement methods + */ + public static void setParameterValue(PreparedStatement ps, int paramIndex, SqlParameter param, + @Nullable Object inValue) throws SQLException { + + setParameterValueInternal(ps, paramIndex, param.getSqlType(), param.getTypeName(), param.getScale(), inValue); + } + + /** + * Set the value for a parameter. The method used is based on the SQL type + * of the parameter and we can handle complex types like arrays and LOBs. + * @param ps the prepared statement or callable statement + * @param paramIndex index of the parameter we are setting + * @param sqlType the SQL type of the parameter + * @param inValue the value to set (plain value or an SqlTypeValue) + * @throws SQLException if thrown by PreparedStatement methods + * @see SqlTypeValue + */ + public static void setParameterValue(PreparedStatement ps, int paramIndex, int sqlType, + @Nullable Object inValue) throws SQLException { + + setParameterValueInternal(ps, paramIndex, sqlType, null, null, inValue); + } + + /** + * Set the value for a parameter. The method used is based on the SQL type + * of the parameter and we can handle complex types like arrays and LOBs. + * @param ps the prepared statement or callable statement + * @param paramIndex index of the parameter we are setting + * @param sqlType the SQL type of the parameter + * @param typeName the type name of the parameter + * (optional, only used for SQL NULL and SqlTypeValue) + * @param inValue the value to set (plain value or an SqlTypeValue) + * @throws SQLException if thrown by PreparedStatement methods + * @see SqlTypeValue + */ + public static void setParameterValue(PreparedStatement ps, int paramIndex, int sqlType, String typeName, + @Nullable Object inValue) throws SQLException { + + setParameterValueInternal(ps, paramIndex, sqlType, typeName, null, inValue); + } + + /** + * Set the value for a parameter. The method used is based on the SQL type + * of the parameter and we can handle complex types like arrays and LOBs. + * @param ps the prepared statement or callable statement + * @param paramIndex index of the parameter we are setting + * @param sqlType the SQL type of the parameter + * @param typeName the type name of the parameter + * (optional, only used for SQL NULL and SqlTypeValue) + * @param scale the number of digits after the decimal point + * (for DECIMAL and NUMERIC types) + * @param inValue the value to set (plain value or an SqlTypeValue) + * @throws SQLException if thrown by PreparedStatement methods + * @see SqlTypeValue + */ + private static void setParameterValueInternal(PreparedStatement ps, int paramIndex, int sqlType, + @Nullable String typeName, @Nullable Integer scale, @Nullable Object inValue) throws SQLException { + + String typeNameToUse = typeName; + int sqlTypeToUse = sqlType; + Object inValueToUse = inValue; + + // override type info? + if (inValue instanceof SqlParameterValue) { + SqlParameterValue parameterValue = (SqlParameterValue) inValue; + if (logger.isDebugEnabled()) { + logger.debug("Overriding type info with runtime info from SqlParameterValue: column index " + paramIndex + + ", SQL type " + parameterValue.getSqlType() + ", type name " + parameterValue.getTypeName()); + } + if (parameterValue.getSqlType() != SqlTypeValue.TYPE_UNKNOWN) { + sqlTypeToUse = parameterValue.getSqlType(); + } + if (parameterValue.getTypeName() != null) { + typeNameToUse = parameterValue.getTypeName(); + } + inValueToUse = parameterValue.getValue(); + } + + if (logger.isTraceEnabled()) { + logger.trace("Setting SQL statement parameter value: column index " + paramIndex + + ", parameter value [" + inValueToUse + + "], value class [" + (inValueToUse != null ? inValueToUse.getClass().getName() : "null") + + "], SQL type " + (sqlTypeToUse == SqlTypeValue.TYPE_UNKNOWN ? "unknown" : Integer.toString(sqlTypeToUse))); + } + + if (inValueToUse == null) { + setNull(ps, paramIndex, sqlTypeToUse, typeNameToUse); + } + else { + setValue(ps, paramIndex, sqlTypeToUse, typeNameToUse, scale, inValueToUse); + } + } + + /** + * Set the specified PreparedStatement parameter to null, + * respecting database-specific peculiarities. + */ + private static void setNull(PreparedStatement ps, int paramIndex, int sqlType, @Nullable String typeName) + throws SQLException { + + if (sqlType == SqlTypeValue.TYPE_UNKNOWN || (sqlType == Types.OTHER && typeName == null)) { + boolean useSetObject = false; + Integer sqlTypeToUse = null; + if (!shouldIgnoreGetParameterType) { + try { + sqlTypeToUse = ps.getParameterMetaData().getParameterType(paramIndex); + } + catch (SQLException ex) { + if (logger.isDebugEnabled()) { + logger.debug("JDBC getParameterType call failed - using fallback method instead: " + ex); + } + } + } + if (sqlTypeToUse == null) { + // Proceed with database-specific checks + sqlTypeToUse = Types.NULL; + DatabaseMetaData dbmd = ps.getConnection().getMetaData(); + String jdbcDriverName = dbmd.getDriverName(); + String databaseProductName = dbmd.getDatabaseProductName(); + if (databaseProductName.startsWith("Informix") || + (jdbcDriverName.startsWith("Microsoft") && jdbcDriverName.contains("SQL Server"))) { + // "Microsoft SQL Server JDBC Driver 3.0" versus "Microsoft JDBC Driver 4.0 for SQL Server" + useSetObject = true; + } + else if (databaseProductName.startsWith("DB2") || + jdbcDriverName.startsWith("jConnect") || + jdbcDriverName.startsWith("SQLServer") || + jdbcDriverName.startsWith("Apache Derby")) { + sqlTypeToUse = Types.VARCHAR; + } + } + if (useSetObject) { + ps.setObject(paramIndex, null); + } + else { + ps.setNull(paramIndex, sqlTypeToUse); + } + } + else if (typeName != null) { + ps.setNull(paramIndex, sqlType, typeName); + } + else { + ps.setNull(paramIndex, sqlType); + } + } + + private static void setValue(PreparedStatement ps, int paramIndex, int sqlType, + @Nullable String typeName, @Nullable Integer scale, Object inValue) throws SQLException { + + if (inValue instanceof SqlTypeValue) { + ((SqlTypeValue) inValue).setTypeValue(ps, paramIndex, sqlType, typeName); + } + else if (inValue instanceof SqlValue) { + ((SqlValue) inValue).setValue(ps, paramIndex); + } + else if (sqlType == Types.VARCHAR || sqlType == Types.LONGVARCHAR ) { + ps.setString(paramIndex, inValue.toString()); + } + else if (sqlType == Types.NVARCHAR || sqlType == Types.LONGNVARCHAR) { + ps.setNString(paramIndex, inValue.toString()); + } + else if ((sqlType == Types.CLOB || sqlType == Types.NCLOB) && isStringValue(inValue.getClass())) { + String strVal = inValue.toString(); + if (strVal.length() > 4000) { + // Necessary for older Oracle drivers, in particular when running against an Oracle 10 database. + // Should also work fine against other drivers/databases since it uses standard JDBC 4.0 API. + if (sqlType == Types.NCLOB) { + ps.setNClob(paramIndex, new StringReader(strVal), strVal.length()); + } + else { + ps.setClob(paramIndex, new StringReader(strVal), strVal.length()); + } + } + else { + // Fallback: setString or setNString binding + if (sqlType == Types.NCLOB) { + ps.setNString(paramIndex, strVal); + } + else { + ps.setString(paramIndex, strVal); + } + } + } + else if (sqlType == Types.DECIMAL || sqlType == Types.NUMERIC) { + if (inValue instanceof BigDecimal) { + ps.setBigDecimal(paramIndex, (BigDecimal) inValue); + } + else if (scale != null) { + ps.setObject(paramIndex, inValue, sqlType, scale); + } + else { + ps.setObject(paramIndex, inValue, sqlType); + } + } + else if (sqlType == Types.BOOLEAN) { + if (inValue instanceof Boolean) { + ps.setBoolean(paramIndex, (Boolean) inValue); + } + else { + ps.setObject(paramIndex, inValue, Types.BOOLEAN); + } + } + else if (sqlType == Types.DATE) { + if (inValue instanceof java.util.Date) { + if (inValue instanceof java.sql.Date) { + ps.setDate(paramIndex, (java.sql.Date) inValue); + } + else { + ps.setDate(paramIndex, new java.sql.Date(((java.util.Date) inValue).getTime())); + } + } + else if (inValue instanceof Calendar) { + Calendar cal = (Calendar) inValue; + ps.setDate(paramIndex, new java.sql.Date(cal.getTime().getTime()), cal); + } + else { + ps.setObject(paramIndex, inValue, Types.DATE); + } + } + else if (sqlType == Types.TIME) { + if (inValue instanceof java.util.Date) { + if (inValue instanceof java.sql.Time) { + ps.setTime(paramIndex, (java.sql.Time) inValue); + } + else { + ps.setTime(paramIndex, new java.sql.Time(((java.util.Date) inValue).getTime())); + } + } + else if (inValue instanceof Calendar) { + Calendar cal = (Calendar) inValue; + ps.setTime(paramIndex, new java.sql.Time(cal.getTime().getTime()), cal); + } + else { + ps.setObject(paramIndex, inValue, Types.TIME); + } + } + else if (sqlType == Types.TIMESTAMP) { + if (inValue instanceof java.util.Date) { + if (inValue instanceof java.sql.Timestamp) { + ps.setTimestamp(paramIndex, (java.sql.Timestamp) inValue); + } + else { + ps.setTimestamp(paramIndex, new java.sql.Timestamp(((java.util.Date) inValue).getTime())); + } + } + else if (inValue instanceof Calendar) { + Calendar cal = (Calendar) inValue; + ps.setTimestamp(paramIndex, new java.sql.Timestamp(cal.getTime().getTime()), cal); + } + else { + ps.setObject(paramIndex, inValue, Types.TIMESTAMP); + } + } + else if (sqlType == SqlTypeValue.TYPE_UNKNOWN || (sqlType == Types.OTHER && + "Oracle".equals(ps.getConnection().getMetaData().getDatabaseProductName()))) { + if (isStringValue(inValue.getClass())) { + ps.setString(paramIndex, inValue.toString()); + } + else if (isDateValue(inValue.getClass())) { + ps.setTimestamp(paramIndex, new java.sql.Timestamp(((java.util.Date) inValue).getTime())); + } + else if (inValue instanceof Calendar) { + Calendar cal = (Calendar) inValue; + ps.setTimestamp(paramIndex, new java.sql.Timestamp(cal.getTime().getTime()), cal); + } + else { + // Fall back to generic setObject call without SQL type specified. + ps.setObject(paramIndex, inValue); + } + } + else { + // Fall back to generic setObject call with SQL type specified. + ps.setObject(paramIndex, inValue, sqlType); + } + } + + /** + * Check whether the given value can be treated as a String value. + */ + private static boolean isStringValue(Class inValueType) { + // Consider any CharSequence (including StringBuffer and StringBuilder) as a String. + return (CharSequence.class.isAssignableFrom(inValueType) || + StringWriter.class.isAssignableFrom(inValueType)); + } + + /** + * Check whether the given value is a {@code java.util.Date} + * (but not one of the JDBC-specific subclasses). + */ + private static boolean isDateValue(Class inValueType) { + return (java.util.Date.class.isAssignableFrom(inValueType) && + !(java.sql.Date.class.isAssignableFrom(inValueType) || + java.sql.Time.class.isAssignableFrom(inValueType) || + java.sql.Timestamp.class.isAssignableFrom(inValueType))); + } + + /** + * Clean up all resources held by parameter values which were passed to an + * execute method. This is for example important for closing LOB values. + * @param paramValues parameter values supplied. May be {@code null}. + * @see DisposableSqlTypeValue#cleanup() + * @see org.springframework.jdbc.core.support.SqlLobValue#cleanup() + */ + public static void cleanupParameters(@Nullable Object... paramValues) { + if (paramValues != null) { + cleanupParameters(Arrays.asList(paramValues)); + } + } + + /** + * Clean up all resources held by parameter values which were passed to an + * execute method. This is for example important for closing LOB values. + * @param paramValues parameter values supplied. May be {@code null}. + * @see DisposableSqlTypeValue#cleanup() + * @see org.springframework.jdbc.core.support.SqlLobValue#cleanup() + */ + public static void cleanupParameters(@Nullable Collection paramValues) { + if (paramValues != null) { + for (Object inValue : paramValues) { + // Unwrap SqlParameterValue first... + if (inValue instanceof SqlParameterValue) { + inValue = ((SqlParameterValue) inValue).getValue(); + } + // Check for disposable value types + if (inValue instanceof SqlValue) { + ((SqlValue) inValue).cleanup(); + } + else if (inValue instanceof DisposableSqlTypeValue) { + ((DisposableSqlTypeValue) inValue).cleanup(); + } + } + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataContext.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataContext.java new file mode 100644 index 0000000..2997f87 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataContext.java @@ -0,0 +1,687 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.SqlOutParameter; +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.jdbc.core.SqlParameterValue; +import org.springframework.jdbc.core.SqlReturnResultSet; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils; +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +/** + * Class to manage context meta-data used for the configuration + * and execution of a stored procedure call. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @author Kiril Nugmanov + * @since 2.5 + */ +public class CallMetaDataContext { + + // Logger available to subclasses + protected final Log logger = LogFactory.getLog(getClass()); + + // Name of procedure to call + @Nullable + private String procedureName; + + // Name of catalog for call + @Nullable + private String catalogName; + + // Name of schema for call + @Nullable + private String schemaName; + + // List of SqlParameter objects to be used in call execution + private List callParameters = new ArrayList<>(); + + // Actual name to use for the return value in the output map + @Nullable + private String actualFunctionReturnName; + + // Set of in parameter names to exclude use for any not listed + private Set limitedInParameterNames = new HashSet<>(); + + // List of SqlParameter names for out parameters + private List outParameterNames = new ArrayList<>(); + + // Indicates whether this is a procedure or a function + private boolean function = false; + + // Indicates whether this procedure's return value should be included + private boolean returnValueRequired = false; + + // Should we access call parameter meta-data info or not + private boolean accessCallParameterMetaData = true; + + // Should we bind parameter by name + private boolean namedBinding; + + // The provider of call meta-data + @Nullable + private CallMetaDataProvider metaDataProvider; + + + /** + * Specify the name used for the return value of the function. + */ + public void setFunctionReturnName(String functionReturnName) { + this.actualFunctionReturnName = functionReturnName; + } + + /** + * Get the name used for the return value of the function. + */ + public String getFunctionReturnName() { + return (this.actualFunctionReturnName != null ? this.actualFunctionReturnName : "return"); + } + + /** + * Specify a limited set of in parameters to be used. + */ + public void setLimitedInParameterNames(Set limitedInParameterNames) { + this.limitedInParameterNames = limitedInParameterNames; + } + + /** + * Get a limited set of in parameters to be used. + */ + public Set getLimitedInParameterNames() { + return this.limitedInParameterNames; + } + + /** + * Specify the names of the out parameters. + */ + public void setOutParameterNames(List outParameterNames) { + this.outParameterNames = outParameterNames; + } + + /** + * Get a list of the out parameter names. + */ + public List getOutParameterNames() { + return this.outParameterNames; + } + + /** + * Specify the name of the procedure. + */ + public void setProcedureName(@Nullable String procedureName) { + this.procedureName = procedureName; + } + + /** + * Get the name of the procedure. + */ + @Nullable + public String getProcedureName() { + return this.procedureName; + } + + /** + * Specify the name of the catalog. + */ + public void setCatalogName(@Nullable String catalogName) { + this.catalogName = catalogName; + } + + /** + * Get the name of the catalog. + */ + @Nullable + public String getCatalogName() { + return this.catalogName; + } + + /** + * Specify the name of the schema. + */ + public void setSchemaName(@Nullable String schemaName) { + this.schemaName = schemaName; + } + + /** + * Get the name of the schema. + */ + @Nullable + public String getSchemaName() { + return this.schemaName; + } + + /** + * Specify whether this call is a function call. + */ + public void setFunction(boolean function) { + this.function = function; + } + + /** + * Check whether this call is a function call. + */ + public boolean isFunction() { + return this.function; + } + + /** + * Specify whether a return value is required. + */ + public void setReturnValueRequired(boolean returnValueRequired) { + this.returnValueRequired = returnValueRequired; + } + + /** + * Check whether a return value is required. + */ + public boolean isReturnValueRequired() { + return this.returnValueRequired; + } + + /** + * Specify whether call parameter meta-data should be accessed. + */ + public void setAccessCallParameterMetaData(boolean accessCallParameterMetaData) { + this.accessCallParameterMetaData = accessCallParameterMetaData; + } + + /** + * Check whether call parameter meta-data should be accessed. + */ + public boolean isAccessCallParameterMetaData() { + return this.accessCallParameterMetaData; + } + + /** + * Specify whether parameters should be bound by name. + * @since 4.2 + */ + public void setNamedBinding(boolean namedBinding) { + this.namedBinding = namedBinding; + } + + /** + * Check whether parameters should be bound by name. + * @since 4.2 + */ + public boolean isNamedBinding() { + return this.namedBinding; + } + + + /** + * Initialize this class with meta-data from the database. + * @param dataSource the DataSource used to retrieve meta-data + */ + public void initializeMetaData(DataSource dataSource) { + this.metaDataProvider = CallMetaDataProviderFactory.createMetaDataProvider(dataSource, this); + } + + private CallMetaDataProvider obtainMetaDataProvider() { + Assert.state(this.metaDataProvider != null, "No CallMetaDataProvider - call initializeMetaData first"); + return this.metaDataProvider; + } + + /** + * Create a ReturnResultSetParameter/SqlOutParameter depending on the support provided + * by the JDBC driver used for the database in use. + * @param parameterName the name of the parameter (also used as the name of the List returned in the output) + * @param rowMapper a RowMapper implementation used to map the data returned in the result set + * @return the appropriate SqlParameter + */ + public SqlParameter createReturnResultSetParameter(String parameterName, RowMapper rowMapper) { + CallMetaDataProvider provider = obtainMetaDataProvider(); + if (provider.isReturnResultSetSupported()) { + return new SqlReturnResultSet(parameterName, rowMapper); + } + else { + if (provider.isRefCursorSupported()) { + return new SqlOutParameter(parameterName, provider.getRefCursorSqlType(), rowMapper); + } + else { + throw new InvalidDataAccessApiUsageException( + "Return of a ResultSet from a stored procedure is not supported"); + } + } + } + + /** + * Get the name of the single out parameter for this call. + * If there are multiple parameters, the name of the first one will be returned. + */ + @Nullable + public String getScalarOutParameterName() { + if (isFunction()) { + return getFunctionReturnName(); + } + else { + if (this.outParameterNames.size() > 1) { + logger.info("Accessing single output value when procedure has more than one output parameter"); + } + return (!this.outParameterNames.isEmpty() ? this.outParameterNames.get(0) : null); + } + } + + /** + * Get the List of SqlParameter objects to be used in call execution. + */ + public List getCallParameters() { + return this.callParameters; + } + + /** + * Process the list of parameters provided, and if procedure column meta-data is used, + * the parameters will be matched against the meta-data information and any missing + * ones will be automatically included. + * @param parameters the list of parameters to use as a base + */ + public void processParameters(List parameters) { + this.callParameters = reconcileParameters(parameters); + } + + /** + * Reconcile the provided parameters with available meta-data and add new ones where appropriate. + */ + protected List reconcileParameters(List parameters) { + CallMetaDataProvider provider = obtainMetaDataProvider(); + + final List declaredReturnParams = new ArrayList<>(); + final Map declaredParams = new LinkedHashMap<>(); + boolean returnDeclared = false; + List outParamNames = new ArrayList<>(); + List metaDataParamNames = new ArrayList<>(); + + // Get the names of the meta-data parameters + for (CallParameterMetaData meta : provider.getCallParameterMetaData()) { + if (!meta.isReturnParameter()) { + metaDataParamNames.add(lowerCase(meta.getParameterName())); + } + } + + // Separate implicit return parameters from explicit parameters... + for (SqlParameter param : parameters) { + if (param.isResultsParameter()) { + declaredReturnParams.add(param); + } + else { + String paramName = param.getName(); + if (paramName == null) { + throw new IllegalArgumentException("Anonymous parameters not supported for calls - " + + "please specify a name for the parameter of SQL type " + param.getSqlType()); + } + String paramNameToMatch = lowerCase(provider.parameterNameToUse(paramName)); + declaredParams.put(paramNameToMatch, param); + if (param instanceof SqlOutParameter) { + outParamNames.add(paramName); + if (isFunction() && !metaDataParamNames.contains(paramNameToMatch) && !returnDeclared) { + if (logger.isDebugEnabled()) { + logger.debug("Using declared out parameter '" + paramName + + "' for function return value"); + } + this.actualFunctionReturnName = paramName; + returnDeclared = true; + } + } + } + } + setOutParameterNames(outParamNames); + + List workParams = new ArrayList<>(declaredReturnParams); + if (!provider.isProcedureColumnMetaDataUsed()) { + workParams.addAll(declaredParams.values()); + return workParams; + } + + Map limitedInParamNamesMap = CollectionUtils.newHashMap(this.limitedInParameterNames.size()); + for (String limitedParamName : this.limitedInParameterNames) { + limitedInParamNamesMap.put(lowerCase(provider.parameterNameToUse(limitedParamName)), limitedParamName); + } + + for (CallParameterMetaData meta : provider.getCallParameterMetaData()) { + String paramName = meta.getParameterName(); + String paramNameToCheck = null; + if (paramName != null) { + paramNameToCheck = lowerCase(provider.parameterNameToUse(paramName)); + } + String paramNameToUse = provider.parameterNameToUse(paramName); + if (declaredParams.containsKey(paramNameToCheck) || (meta.isReturnParameter() && returnDeclared)) { + SqlParameter param; + if (meta.isReturnParameter()) { + param = declaredParams.get(getFunctionReturnName()); + if (param == null && !getOutParameterNames().isEmpty()) { + param = declaredParams.get(getOutParameterNames().get(0).toLowerCase()); + } + if (param == null) { + throw new InvalidDataAccessApiUsageException( + "Unable to locate declared parameter for function return value - " + + " add an SqlOutParameter with name '" + getFunctionReturnName() + "'"); + } + else { + this.actualFunctionReturnName = param.getName(); + } + } + else { + param = declaredParams.get(paramNameToCheck); + } + if (param != null) { + workParams.add(param); + if (logger.isDebugEnabled()) { + logger.debug("Using declared parameter for '" + + (paramNameToUse != null ? paramNameToUse : getFunctionReturnName()) + "'"); + } + } + } + else { + if (meta.isReturnParameter()) { + // DatabaseMetaData.procedureColumnReturn or possibly procedureColumnResult + if (!isFunction() && !isReturnValueRequired() && paramName != null && + provider.byPassReturnParameter(paramName)) { + if (logger.isDebugEnabled()) { + logger.debug("Bypassing meta-data return parameter for '" + paramName + "'"); + } + } + else { + String returnNameToUse = + (StringUtils.hasLength(paramNameToUse) ? paramNameToUse : getFunctionReturnName()); + workParams.add(provider.createDefaultOutParameter(returnNameToUse, meta)); + if (isFunction()) { + this.actualFunctionReturnName = returnNameToUse; + outParamNames.add(returnNameToUse); + } + if (logger.isDebugEnabled()) { + logger.debug("Added meta-data return parameter for '" + returnNameToUse + "'"); + } + } + } + else { + if (paramNameToUse == null) { + paramNameToUse = ""; + } + if (meta.getParameterType() == DatabaseMetaData.procedureColumnOut) { + workParams.add(provider.createDefaultOutParameter(paramNameToUse, meta)); + outParamNames.add(paramNameToUse); + if (logger.isDebugEnabled()) { + logger.debug("Added meta-data out parameter for '" + paramNameToUse + "'"); + } + } + else if (meta.getParameterType() == DatabaseMetaData.procedureColumnInOut) { + workParams.add(provider.createDefaultInOutParameter(paramNameToUse, meta)); + outParamNames.add(paramNameToUse); + if (logger.isDebugEnabled()) { + logger.debug("Added meta-data in-out parameter for '" + paramNameToUse + "'"); + } + } + else { + // DatabaseMetaData.procedureColumnIn or possibly procedureColumnUnknown + if (this.limitedInParameterNames.isEmpty() || + limitedInParamNamesMap.containsKey(lowerCase(paramNameToUse))) { + workParams.add(provider.createDefaultInParameter(paramNameToUse, meta)); + if (logger.isDebugEnabled()) { + logger.debug("Added meta-data in parameter for '" + paramNameToUse + "'"); + } + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Limited set of parameters " + limitedInParamNamesMap.keySet() + + " skipped parameter for '" + paramNameToUse + "'"); + } + } + } + } + } + } + + return workParams; + } + + /** + * Match input parameter values with the parameters declared to be used in the call. + * @param parameterSource the input values + * @return a Map containing the matched parameter names with the value taken from the input + */ + public Map matchInParameterValuesWithCallParameters(SqlParameterSource parameterSource) { + // For parameter source lookups we need to provide case-insensitive lookup support + // since the database meta-data is not necessarily providing case sensitive parameter names. + Map caseInsensitiveParameterNames = + SqlParameterSourceUtils.extractCaseInsensitiveParameterNames(parameterSource); + + Map callParameterNames = CollectionUtils.newHashMap(this.callParameters.size()); + Map matchedParameters = CollectionUtils.newHashMap(this.callParameters.size()); + for (SqlParameter parameter : this.callParameters) { + if (parameter.isInputValueProvided()) { + String parameterName = parameter.getName(); + String parameterNameToMatch = obtainMetaDataProvider().parameterNameToUse(parameterName); + if (parameterNameToMatch != null) { + callParameterNames.put(parameterNameToMatch.toLowerCase(), parameterName); + } + if (parameterName != null) { + if (parameterSource.hasValue(parameterName)) { + matchedParameters.put(parameterName, + SqlParameterSourceUtils.getTypedValue(parameterSource, parameterName)); + } + else { + String lowerCaseName = parameterName.toLowerCase(); + if (parameterSource.hasValue(lowerCaseName)) { + matchedParameters.put(parameterName, + SqlParameterSourceUtils.getTypedValue(parameterSource, lowerCaseName)); + } + else { + String englishLowerCaseName = parameterName.toLowerCase(Locale.ENGLISH); + if (parameterSource.hasValue(englishLowerCaseName)) { + matchedParameters.put(parameterName, + SqlParameterSourceUtils.getTypedValue(parameterSource, englishLowerCaseName)); + } + else { + String propertyName = JdbcUtils.convertUnderscoreNameToPropertyName(parameterName); + if (parameterSource.hasValue(propertyName)) { + matchedParameters.put(parameterName, + SqlParameterSourceUtils.getTypedValue(parameterSource, propertyName)); + } + else { + if (caseInsensitiveParameterNames.containsKey(lowerCaseName)) { + String sourceName = caseInsensitiveParameterNames.get(lowerCaseName); + matchedParameters.put(parameterName, + SqlParameterSourceUtils.getTypedValue(parameterSource, sourceName)); + } + else if (logger.isInfoEnabled()) { + logger.info("Unable to locate the corresponding parameter value for '" + + parameterName + "' within the parameter values provided: " + + caseInsensitiveParameterNames.values()); + } + } + } + } + } + } + } + } + + if (logger.isDebugEnabled()) { + logger.debug("Matching " + caseInsensitiveParameterNames.values() + " with " + callParameterNames.values()); + logger.debug("Found match for " + matchedParameters.keySet()); + } + return matchedParameters; + } + + /** + * Match input parameter values with the parameters declared to be used in the call. + * @param inParameters the input values + * @return a Map containing the matched parameter names with the value taken from the input + */ + public Map matchInParameterValuesWithCallParameters(Map inParameters) { + CallMetaDataProvider provider = obtainMetaDataProvider(); + if (!provider.isProcedureColumnMetaDataUsed()) { + return inParameters; + } + + Map callParameterNames = CollectionUtils.newHashMap(this.callParameters.size()); + for (SqlParameter parameter : this.callParameters) { + if (parameter.isInputValueProvided()) { + String parameterName = parameter.getName(); + String parameterNameToMatch = provider.parameterNameToUse(parameterName); + if (parameterNameToMatch != null) { + callParameterNames.put(parameterNameToMatch.toLowerCase(), parameterName); + } + } + } + + Map matchedParameters = CollectionUtils.newHashMap(inParameters.size()); + inParameters.forEach((parameterName, parameterValue) -> { + String parameterNameToMatch = provider.parameterNameToUse(parameterName); + String callParameterName = callParameterNames.get(lowerCase(parameterNameToMatch)); + if (callParameterName == null) { + if (logger.isDebugEnabled()) { + Object value = parameterValue; + if (value instanceof SqlParameterValue) { + value = ((SqlParameterValue) value).getValue(); + } + if (value != null) { + logger.debug("Unable to locate the corresponding IN or IN-OUT parameter for \"" + + parameterName + "\" in the parameters used: " + callParameterNames.keySet()); + } + } + } + else { + matchedParameters.put(callParameterName, parameterValue); + } + }); + + if (matchedParameters.size() < callParameterNames.size()) { + for (String parameterName : callParameterNames.keySet()) { + String parameterNameToMatch = provider.parameterNameToUse(parameterName); + String callParameterName = callParameterNames.get(lowerCase(parameterNameToMatch)); + if (!matchedParameters.containsKey(callParameterName) && logger.isInfoEnabled()) { + logger.info("Unable to locate the corresponding parameter value for '" + parameterName + + "' within the parameter values provided: " + inParameters.keySet()); + } + } + } + + if (logger.isDebugEnabled()) { + logger.debug("Matching " + inParameters.keySet() + " with " + callParameterNames.values()); + logger.debug("Found match for " + matchedParameters.keySet()); + } + return matchedParameters; + } + + public Map matchInParameterValuesWithCallParameters(Object[] parameterValues) { + Map matchedParameters = CollectionUtils.newHashMap(parameterValues.length); + int i = 0; + for (SqlParameter parameter : this.callParameters) { + if (parameter.isInputValueProvided()) { + String parameterName = parameter.getName(); + matchedParameters.put(parameterName, parameterValues[i++]); + } + } + return matchedParameters; + } + + /** + * Build the call string based on configuration and meta-data information. + * @return the call string to be used + */ + public String createCallString() { + Assert.state(this.metaDataProvider != null, "No CallMetaDataProvider available"); + + StringBuilder callString; + int parameterCount = 0; + String catalogNameToUse; + String schemaNameToUse; + + // For Oracle where catalogs are not supported we need to reverse the schema name + // and the catalog name since the catalog is used for the package name + if (this.metaDataProvider.isSupportsSchemasInProcedureCalls() && + !this.metaDataProvider.isSupportsCatalogsInProcedureCalls()) { + schemaNameToUse = this.metaDataProvider.catalogNameToUse(getCatalogName()); + catalogNameToUse = this.metaDataProvider.schemaNameToUse(getSchemaName()); + } + else { + catalogNameToUse = this.metaDataProvider.catalogNameToUse(getCatalogName()); + schemaNameToUse = this.metaDataProvider.schemaNameToUse(getSchemaName()); + } + + if (isFunction() || isReturnValueRequired()) { + callString = new StringBuilder("{? = call "); + parameterCount = -1; + } + else { + callString = new StringBuilder("{call "); + } + + if (StringUtils.hasLength(catalogNameToUse)) { + callString.append(catalogNameToUse).append("."); + } + if (StringUtils.hasLength(schemaNameToUse)) { + callString.append(schemaNameToUse).append("."); + } + callString.append(this.metaDataProvider.procedureNameToUse(getProcedureName())); + callString.append("("); + + for (SqlParameter parameter : this.callParameters) { + if (!parameter.isResultsParameter()) { + if (parameterCount > 0) { + callString.append(", "); + } + if (parameterCount >= 0) { + callString.append(createParameterBinding(parameter)); + } + parameterCount++; + } + } + callString.append(")}"); + + return callString.toString(); + } + + /** + * Build the parameter binding fragment. + * @param parameter call parameter + * @return parameter binding fragment + * @since 4.2 + */ + protected String createParameterBinding(SqlParameter parameter) { + return (isNamedBinding() ? parameter.getName() + " => ?" : "?"); + } + + private static String lowerCase(@Nullable String paramName) { + return (paramName != null ? paramName.toLowerCase() : ""); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProvider.java new file mode 100644 index 0000000..068a1c4 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProvider.java @@ -0,0 +1,183 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.util.List; + +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.lang.Nullable; + +/** + * Interface specifying the API to be implemented by a class providing call meta-data. + * + *

    This is intended for internal use by Spring's + * {@link org.springframework.jdbc.core.simple.SimpleJdbcCall}. + * + * @author Thomas Risberg + * @since 2.5 + */ +public interface CallMetaDataProvider { + + /** + * Initialize using the provided DatabaseMetData. + * @param databaseMetaData used to retrieve database specific information + * @throws SQLException in case of initialization failure + */ + void initializeWithMetaData(DatabaseMetaData databaseMetaData) throws SQLException; + + /** + * Initialize the database specific management of procedure column meta-data. + * This is only called for databases that are supported. This initialization + * can be turned off by specifying that column meta-data should not be used. + * @param databaseMetaData used to retrieve database specific information + * @param catalogName name of catalog to use (or {@code null} if none) + * @param schemaName name of schema name to use (or {@code null} if none) + * @param procedureName name of the stored procedure + * @throws SQLException in case of initialization failure + * @see org.springframework.jdbc.core.simple.SimpleJdbcCall#withoutProcedureColumnMetaDataAccess() + */ + void initializeWithProcedureColumnMetaData(DatabaseMetaData databaseMetaData, @Nullable String catalogName, + @Nullable String schemaName, @Nullable String procedureName) throws SQLException; + + /** + * Provide any modification of the procedure name passed in to match the meta-data currently used. + * This could include altering the case. + */ + @Nullable + String procedureNameToUse(@Nullable String procedureName); + + /** + * Provide any modification of the catalog name passed in to match the meta-data currently used. + * This could include altering the case. + */ + @Nullable + String catalogNameToUse(@Nullable String catalogName); + + /** + * Provide any modification of the schema name passed in to match the meta-data currently used. + * This could include altering the case. + */ + @Nullable + String schemaNameToUse(@Nullable String schemaName); + + /** + * Provide any modification of the catalog name passed in to match the meta-data currently used. + * The returned value will be used for meta-data lookups. This could include altering the case + * used or providing a base catalog if none is provided. + */ + @Nullable + String metaDataCatalogNameToUse(@Nullable String catalogName) ; + + /** + * Provide any modification of the schema name passed in to match the meta-data currently used. + * The returned value will be used for meta-data lookups. This could include altering the case + * used or providing a base schema if none is provided. + */ + @Nullable + String metaDataSchemaNameToUse(@Nullable String schemaName); + + /** + * Provide any modification of the column name passed in to match the meta-data currently used. + * This could include altering the case. + * @param parameterName name of the parameter of column + */ + @Nullable + String parameterNameToUse(@Nullable String parameterName); + + /** + * Create a default out parameter based on the provided meta-data. + * This is used when no explicit parameter declaration has been made. + * @param parameterName the name of the parameter + * @param meta meta-data used for this call + * @return the configured SqlOutParameter + */ + SqlParameter createDefaultOutParameter(String parameterName, CallParameterMetaData meta); + + /** + * Create a default in/out parameter based on the provided meta-data. + * This is used when no explicit parameter declaration has been made. + * @param parameterName the name of the parameter + * @param meta meta-data used for this call + * @return the configured SqlInOutParameter + */ + SqlParameter createDefaultInOutParameter(String parameterName, CallParameterMetaData meta); + + /** + * Create a default in parameter based on the provided meta-data. + * This is used when no explicit parameter declaration has been made. + * @param parameterName the name of the parameter + * @param meta meta-data used for this call + * @return the configured SqlParameter + */ + SqlParameter createDefaultInParameter(String parameterName, CallParameterMetaData meta); + + /** + * Get the name of the current user. Useful for meta-data lookups etc. + * @return current user name from database connection + */ + @Nullable + String getUserName(); + + /** + * Does this database support returning ResultSets that should be retrieved with the JDBC call: + * {@link java.sql.Statement#getResultSet()}? + */ + boolean isReturnResultSetSupported(); + + /** + * Does this database support returning ResultSets as ref cursors to be retrieved with + * {@link java.sql.CallableStatement#getObject(int)} for the specified column. + */ + boolean isRefCursorSupported(); + + /** + * Get the {@link java.sql.Types} type for columns that return ResultSets as ref cursors + * if this feature is supported. + */ + int getRefCursorSqlType(); + + /** + * Are we using the meta-data for the procedure columns? + */ + boolean isProcedureColumnMetaDataUsed(); + + /** + * Should we bypass the return parameter with the specified name. + * This allows the database specific implementation to skip the processing + * for specific results returned by the database call. + */ + boolean byPassReturnParameter(String parameterName); + + /** + * Get the call parameter meta-data that is currently used. + * @return a List of {@link CallParameterMetaData} + */ + List getCallParameterMetaData(); + + /** + * Does the database support the use of catalog name in procedure calls? + */ + boolean isSupportsCatalogsInProcedureCalls(); + + /** + * Does the database support the use of schema name in procedure calls? + */ + boolean isSupportsSchemasInProcedureCalls(); + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProviderFactory.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProviderFactory.java new file mode 100644 index 0000000..088340c --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProviderFactory.java @@ -0,0 +1,148 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.metadata; + +import java.util.Arrays; +import java.util.List; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.jdbc.support.MetaDataAccessException; + +/** + * Factory used to create a {@link CallMetaDataProvider} implementation + * based on the type of database being used. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.5 + */ +public final class CallMetaDataProviderFactory { + + /** List of supported database products for procedure calls. */ + public static final List supportedDatabaseProductsForProcedures = Arrays.asList( + "Apache Derby", + "DB2", + "Informix Dynamic Server", + "MariaDB", + "Microsoft SQL Server", + "MySQL", + "Oracle", + "PostgreSQL", + "Sybase" + ); + + /** List of supported database products for function calls. */ + public static final List supportedDatabaseProductsForFunctions = Arrays.asList( + "MariaDB", + "Microsoft SQL Server", + "MySQL", + "Oracle", + "PostgreSQL" + ); + + private static final Log logger = LogFactory.getLog(CallMetaDataProviderFactory.class); + + + private CallMetaDataProviderFactory() { + } + + + /** + * Create a {@link CallMetaDataProvider} based on the database meta-data. + * @param dataSource the JDBC DataSource to use for retrieving meta-data + * @param context the class that holds configuration and meta-data + * @return instance of the CallMetaDataProvider implementation to be used + */ + public static CallMetaDataProvider createMetaDataProvider(DataSource dataSource, final CallMetaDataContext context) { + try { + return JdbcUtils.extractDatabaseMetaData(dataSource, databaseMetaData -> { + String databaseProductName = JdbcUtils.commonDatabaseName(databaseMetaData.getDatabaseProductName()); + boolean accessProcedureColumnMetaData = context.isAccessCallParameterMetaData(); + if (context.isFunction()) { + if (!supportedDatabaseProductsForFunctions.contains(databaseProductName)) { + if (logger.isInfoEnabled()) { + logger.info(databaseProductName + " is not one of the databases fully supported for function calls " + + "-- supported are: " + supportedDatabaseProductsForFunctions); + } + if (accessProcedureColumnMetaData) { + logger.info("Metadata processing disabled - you must specify all parameters explicitly"); + accessProcedureColumnMetaData = false; + } + } + } + else { + if (!supportedDatabaseProductsForProcedures.contains(databaseProductName)) { + if (logger.isInfoEnabled()) { + logger.info(databaseProductName + " is not one of the databases fully supported for procedure calls " + + "-- supported are: " + supportedDatabaseProductsForProcedures); + } + if (accessProcedureColumnMetaData) { + logger.info("Metadata processing disabled - you must specify all parameters explicitly"); + accessProcedureColumnMetaData = false; + } + } + } + + CallMetaDataProvider provider; + if ("Oracle".equals(databaseProductName)) { + provider = new OracleCallMetaDataProvider(databaseMetaData); + } + else if ("PostgreSQL".equals(databaseProductName)) { + provider = new PostgresCallMetaDataProvider((databaseMetaData)); + } + else if ("Apache Derby".equals(databaseProductName)) { + provider = new DerbyCallMetaDataProvider((databaseMetaData)); + } + else if ("DB2".equals(databaseProductName)) { + provider = new Db2CallMetaDataProvider((databaseMetaData)); + } + else if ("HDB".equals(databaseProductName)) { + provider = new HanaCallMetaDataProvider((databaseMetaData)); + } + else if ("Microsoft SQL Server".equals(databaseProductName)) { + provider = new SqlServerCallMetaDataProvider((databaseMetaData)); + } + else if ("Sybase".equals(databaseProductName)) { + provider = new SybaseCallMetaDataProvider((databaseMetaData)); + } + else { + provider = new GenericCallMetaDataProvider(databaseMetaData); + } + + if (logger.isDebugEnabled()) { + logger.debug("Using " + provider.getClass().getName()); + } + provider.initializeWithMetaData(databaseMetaData); + if (accessProcedureColumnMetaData) { + provider.initializeWithProcedureColumnMetaData(databaseMetaData, + context.getCatalogName(), context.getSchemaName(), context.getProcedureName()); + } + return provider; + }); + } + catch (MetaDataAccessException ex) { + throw new DataAccessResourceFailureException("Error retrieving database meta-data", ex); + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallParameterMetaData.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallParameterMetaData.java new file mode 100644 index 0000000..76d9cec --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallParameterMetaData.java @@ -0,0 +1,132 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; + +import org.springframework.lang.Nullable; + +/** + * Holder of meta-data for a specific parameter that is used for call processing. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.5 + * @see GenericCallMetaDataProvider + */ +public class CallParameterMetaData { + + private final boolean function; + + @Nullable + private final String parameterName; + + private final int parameterType; + + private final int sqlType; + + @Nullable + private final String typeName; + + private final boolean nullable; + + + /** + * Constructor taking all the properties except the function marker. + */ + @Deprecated + public CallParameterMetaData( + @Nullable String columnName, int columnType, int sqlType, @Nullable String typeName, boolean nullable) { + + this(false, columnName, columnType, sqlType, typeName, nullable); + } + + /** + * Constructor taking all the properties including the function marker. + * @since 5.2.9 + */ + public CallParameterMetaData(boolean function, @Nullable String columnName, int columnType, + int sqlType, @Nullable String typeName, boolean nullable) { + + this.function = function; + this.parameterName = columnName; + this.parameterType = columnType; + this.sqlType = sqlType; + this.typeName = typeName; + this.nullable = nullable; + } + + + /** + * Return whether this parameter is declared in a function. + * @since 5.2.9 + */ + public boolean isFunction() { + return this.function; + } + + /** + * Return the parameter name. + */ + @Nullable + public String getParameterName() { + return this.parameterName; + } + + /** + * Return the parameter type. + */ + public int getParameterType() { + return this.parameterType; + } + + /** + * Determine whether the declared parameter qualifies as a 'return' parameter + * for our purposes: type {@link DatabaseMetaData#procedureColumnReturn} or + * {@link DatabaseMetaData#procedureColumnResult}, or in case of a function, + * {@link DatabaseMetaData#functionReturn}. + * @since 4.3.15 + */ + public boolean isReturnParameter() { + return (this.function ? this.parameterType == DatabaseMetaData.functionReturn : + (this.parameterType == DatabaseMetaData.procedureColumnReturn || + this.parameterType == DatabaseMetaData.procedureColumnResult)); + } + + /** + * Return the parameter SQL type. + */ + public int getSqlType() { + return this.sqlType; + } + + /** + * Return the parameter type name. + */ + @Nullable + public String getTypeName() { + return this.typeName; + } + + /** + * Return whether the parameter is nullable. + */ + public boolean isNullable() { + return this.nullable; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/Db2CallMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/Db2CallMetaDataProvider.java new file mode 100644 index 0000000..13904c9 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/Db2CallMetaDataProvider.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +import org.springframework.lang.Nullable; + +/** + * DB2 specific implementation for the {@link CallMetaDataProvider} interface. + * This class is intended for internal use by the Simple JDBC classes. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.5 + */ +public class Db2CallMetaDataProvider extends GenericCallMetaDataProvider { + + public Db2CallMetaDataProvider(DatabaseMetaData databaseMetaData) throws SQLException { + super(databaseMetaData); + } + + + @Override + public void initializeWithMetaData(DatabaseMetaData databaseMetaData) throws SQLException { + try { + setSupportsCatalogsInProcedureCalls(databaseMetaData.supportsCatalogsInProcedureCalls()); + } + catch (SQLException ex) { + logger.debug("Error retrieving 'DatabaseMetaData.supportsCatalogsInProcedureCalls' - " + ex.getMessage()); + } + try { + setSupportsSchemasInProcedureCalls(databaseMetaData.supportsSchemasInProcedureCalls()); + } + catch (SQLException ex) { + logger.debug("Error retrieving 'DatabaseMetaData.supportsSchemasInProcedureCalls' - " + ex.getMessage()); + } + try { + setStoresUpperCaseIdentifiers(databaseMetaData.storesUpperCaseIdentifiers()); + } + catch (SQLException ex) { + logger.debug("Error retrieving 'DatabaseMetaData.storesUpperCaseIdentifiers' - " + ex.getMessage()); + } + try { + setStoresLowerCaseIdentifiers(databaseMetaData.storesLowerCaseIdentifiers()); + } + catch (SQLException ex) { + logger.debug("Error retrieving 'DatabaseMetaData.storesLowerCaseIdentifiers' - " + ex.getMessage()); + } + } + + @Override + @Nullable + public String metaDataSchemaNameToUse(@Nullable String schemaName) { + if (schemaName != null) { + return super.metaDataSchemaNameToUse(schemaName); + } + + // Use current user schema if no schema specified... + String userName = getUserName(); + return (userName != null ? userName.toUpperCase() : null); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/DerbyCallMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/DerbyCallMetaDataProvider.java new file mode 100644 index 0000000..02bbaa6 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/DerbyCallMetaDataProvider.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +import org.springframework.lang.Nullable; + +/** + * Derby specific implementation for the {@link CallMetaDataProvider} interface. + * This class is intended for internal use by the Simple JDBC classes. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.5 + */ +public class DerbyCallMetaDataProvider extends GenericCallMetaDataProvider { + + public DerbyCallMetaDataProvider(DatabaseMetaData databaseMetaData) throws SQLException { + super(databaseMetaData); + } + + + @Override + @Nullable + public String metaDataSchemaNameToUse(@Nullable String schemaName) { + if (schemaName != null) { + return super.metaDataSchemaNameToUse(schemaName); + } + + // Use current user schema if no schema specified... + String userName = getUserName(); + return (userName != null ? userName.toUpperCase() : null); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/DerbyTableMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/DerbyTableMetaDataProvider.java new file mode 100644 index 0000000..5c537c7 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/DerbyTableMetaDataProvider.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +/** + * The Derby specific implementation of {@link TableMetaDataProvider}. + * Overrides the Derby meta-data info regarding retrieving generated keys. + * + * @author Thomas Risberg + * @since 3.0 + */ +public class DerbyTableMetaDataProvider extends GenericTableMetaDataProvider { + + private boolean supportsGeneratedKeysOverride = false; + + + public DerbyTableMetaDataProvider(DatabaseMetaData databaseMetaData) throws SQLException { + super(databaseMetaData); + } + + + @Override + public void initializeWithMetaData(DatabaseMetaData databaseMetaData) throws SQLException { + super.initializeWithMetaData(databaseMetaData); + if (!databaseMetaData.supportsGetGeneratedKeys()) { + if (logger.isInfoEnabled()) { + logger.info("Overriding supportsGetGeneratedKeys from DatabaseMetaData to 'true'; it was reported as " + + "'false' by " + databaseMetaData.getDriverName() + " " + databaseMetaData.getDriverVersion()); + } + this.supportsGeneratedKeysOverride = true; + } + } + + @Override + public boolean isGetGeneratedKeysSupported() { + return (super.isGetGeneratedKeysSupported() || this.supportsGeneratedKeysOverride); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java new file mode 100644 index 0000000..f0b6991 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java @@ -0,0 +1,435 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.jdbc.core.SqlInOutParameter; +import org.springframework.jdbc.core.SqlOutParameter; +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * A generic implementation of the {@link CallMetaDataProvider} interface. + * This class can be extended to provide database specific behavior. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.5 + */ +public class GenericCallMetaDataProvider implements CallMetaDataProvider { + + /** Logger available to subclasses. */ + protected static final Log logger = LogFactory.getLog(CallMetaDataProvider.class); + + + private final String userName; + + private boolean supportsCatalogsInProcedureCalls = true; + + private boolean supportsSchemasInProcedureCalls = true; + + private boolean storesUpperCaseIdentifiers = true; + + private boolean storesLowerCaseIdentifiers = false; + + private boolean procedureColumnMetaDataUsed = false; + + private final List callParameterMetaData = new ArrayList<>(); + + + /** + * Constructor used to initialize with provided database meta-data. + * @param databaseMetaData meta-data to be used + */ + protected GenericCallMetaDataProvider(DatabaseMetaData databaseMetaData) throws SQLException { + this.userName = databaseMetaData.getUserName(); + } + + + @Override + public void initializeWithMetaData(DatabaseMetaData databaseMetaData) throws SQLException { + try { + setSupportsCatalogsInProcedureCalls(databaseMetaData.supportsCatalogsInProcedureCalls()); + } + catch (SQLException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Error retrieving 'DatabaseMetaData.supportsCatalogsInProcedureCalls': " + ex.getMessage()); + } + } + try { + setSupportsSchemasInProcedureCalls(databaseMetaData.supportsSchemasInProcedureCalls()); + } + catch (SQLException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Error retrieving 'DatabaseMetaData.supportsSchemasInProcedureCalls': " + ex.getMessage()); + } + } + try { + setStoresUpperCaseIdentifiers(databaseMetaData.storesUpperCaseIdentifiers()); + } + catch (SQLException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Error retrieving 'DatabaseMetaData.storesUpperCaseIdentifiers': " + ex.getMessage()); + } + } + try { + setStoresLowerCaseIdentifiers(databaseMetaData.storesLowerCaseIdentifiers()); + } + catch (SQLException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Error retrieving 'DatabaseMetaData.storesLowerCaseIdentifiers': " + ex.getMessage()); + } + } + } + + @Override + public void initializeWithProcedureColumnMetaData(DatabaseMetaData databaseMetaData, @Nullable String catalogName, + @Nullable String schemaName, @Nullable String procedureName) throws SQLException { + + this.procedureColumnMetaDataUsed = true; + processProcedureColumns(databaseMetaData, catalogName, schemaName, procedureName); + } + + @Override + public List getCallParameterMetaData() { + return this.callParameterMetaData; + } + + @Override + @Nullable + public String procedureNameToUse(@Nullable String procedureName) { + if (procedureName == null) { + return null; + } + else if (isStoresUpperCaseIdentifiers()) { + return procedureName.toUpperCase(); + } + else if (isStoresLowerCaseIdentifiers()) { + return procedureName.toLowerCase(); + } + else { + return procedureName; + } + } + + @Override + @Nullable + public String catalogNameToUse(@Nullable String catalogName) { + if (catalogName == null) { + return null; + } + else if (isStoresUpperCaseIdentifiers()) { + return catalogName.toUpperCase(); + } + else if (isStoresLowerCaseIdentifiers()) { + return catalogName.toLowerCase(); + } + else { + return catalogName; + } + } + + @Override + @Nullable + public String schemaNameToUse(@Nullable String schemaName) { + if (schemaName == null) { + return null; + } + else if (isStoresUpperCaseIdentifiers()) { + return schemaName.toUpperCase(); + } + else if (isStoresLowerCaseIdentifiers()) { + return schemaName.toLowerCase(); + } + else { + return schemaName; + } + } + + @Override + @Nullable + public String metaDataCatalogNameToUse(@Nullable String catalogName) { + if (isSupportsCatalogsInProcedureCalls()) { + return catalogNameToUse(catalogName); + } + else { + return null; + } + } + + @Override + @Nullable + public String metaDataSchemaNameToUse(@Nullable String schemaName) { + if (isSupportsSchemasInProcedureCalls()) { + return schemaNameToUse(schemaName); + } + else { + return null; + } + } + + @Override + @Nullable + public String parameterNameToUse(@Nullable String parameterName) { + if (parameterName == null) { + return null; + } + else if (isStoresUpperCaseIdentifiers()) { + return parameterName.toUpperCase(); + } + else if (isStoresLowerCaseIdentifiers()) { + return parameterName.toLowerCase(); + } + else { + return parameterName; + } + } + + @Override + public boolean byPassReturnParameter(String parameterName) { + return false; + } + + @Override + public SqlParameter createDefaultOutParameter(String parameterName, CallParameterMetaData meta) { + return new SqlOutParameter(parameterName, meta.getSqlType()); + } + + @Override + public SqlParameter createDefaultInOutParameter(String parameterName, CallParameterMetaData meta) { + return new SqlInOutParameter(parameterName, meta.getSqlType()); + } + + @Override + public SqlParameter createDefaultInParameter(String parameterName, CallParameterMetaData meta) { + return new SqlParameter(parameterName, meta.getSqlType()); + } + + @Override + public String getUserName() { + return this.userName; + } + + @Override + public boolean isReturnResultSetSupported() { + return true; + } + + @Override + public boolean isRefCursorSupported() { + return false; + } + + @Override + public int getRefCursorSqlType() { + return Types.OTHER; + } + + @Override + public boolean isProcedureColumnMetaDataUsed() { + return this.procedureColumnMetaDataUsed; + } + + + /** + * Specify whether the database supports the use of catalog name in procedure calls. + */ + protected void setSupportsCatalogsInProcedureCalls(boolean supportsCatalogsInProcedureCalls) { + this.supportsCatalogsInProcedureCalls = supportsCatalogsInProcedureCalls; + } + + /** + * Does the database support the use of catalog name in procedure calls? + */ + @Override + public boolean isSupportsCatalogsInProcedureCalls() { + return this.supportsCatalogsInProcedureCalls; + } + + /** + * Specify whether the database supports the use of schema name in procedure calls. + */ + protected void setSupportsSchemasInProcedureCalls(boolean supportsSchemasInProcedureCalls) { + this.supportsSchemasInProcedureCalls = supportsSchemasInProcedureCalls; + } + + /** + * Does the database support the use of schema name in procedure calls? + */ + @Override + public boolean isSupportsSchemasInProcedureCalls() { + return this.supportsSchemasInProcedureCalls; + } + + /** + * Specify whether the database uses upper case for identifiers. + */ + protected void setStoresUpperCaseIdentifiers(boolean storesUpperCaseIdentifiers) { + this.storesUpperCaseIdentifiers = storesUpperCaseIdentifiers; + } + + /** + * Does the database use upper case for identifiers? + */ + protected boolean isStoresUpperCaseIdentifiers() { + return this.storesUpperCaseIdentifiers; + } + + /** + * Specify whether the database uses lower case for identifiers. + */ + protected void setStoresLowerCaseIdentifiers(boolean storesLowerCaseIdentifiers) { + this.storesLowerCaseIdentifiers = storesLowerCaseIdentifiers; + } + + /** + * Does the database use lower case for identifiers? + */ + protected boolean isStoresLowerCaseIdentifiers() { + return this.storesLowerCaseIdentifiers; + } + + + /** + * Process the procedure column meta-data. + */ + private void processProcedureColumns(DatabaseMetaData databaseMetaData, + @Nullable String catalogName, @Nullable String schemaName, @Nullable String procedureName) { + + String metaDataCatalogName = metaDataCatalogNameToUse(catalogName); + String metaDataSchemaName = metaDataSchemaNameToUse(schemaName); + String metaDataProcedureName = procedureNameToUse(procedureName); + if (logger.isDebugEnabled()) { + logger.debug("Retrieving meta-data for " + metaDataCatalogName + '/' + + metaDataSchemaName + '/' + metaDataProcedureName); + } + + try { + List found = new ArrayList<>(); + boolean function = false; + + try (ResultSet procedures = databaseMetaData.getProcedures( + metaDataCatalogName, metaDataSchemaName, metaDataProcedureName)) { + while (procedures.next()) { + found.add(procedures.getString("PROCEDURE_CAT") + '.' + procedures.getString("PROCEDURE_SCHEM") + + '.' + procedures.getString("PROCEDURE_NAME")); + } + } + + if (found.isEmpty()) { + // Functions not exposed as procedures anymore on PostgreSQL driver 42.2.11 + try (ResultSet functions = databaseMetaData.getFunctions( + metaDataCatalogName, metaDataSchemaName, metaDataProcedureName)) { + while (functions.next()) { + found.add(functions.getString("FUNCTION_CAT") + '.' + functions.getString("FUNCTION_SCHEM") + + '.' + functions.getString("FUNCTION_NAME")); + function = true; + } + } + } + + if (found.size() > 1) { + throw new InvalidDataAccessApiUsageException( + "Unable to determine the correct call signature - multiple signatures for '" + + metaDataProcedureName + "': found " + found + " " + (function ? "functions" : "procedures")); + } + else if (found.isEmpty()) { + if (metaDataProcedureName != null && metaDataProcedureName.contains(".") && + !StringUtils.hasText(metaDataCatalogName)) { + String packageName = metaDataProcedureName.substring(0, metaDataProcedureName.indexOf('.')); + throw new InvalidDataAccessApiUsageException( + "Unable to determine the correct call signature for '" + metaDataProcedureName + + "' - package name should be specified separately using '.withCatalogName(\"" + + packageName + "\")'"); + } + else if ("Oracle".equals(databaseMetaData.getDatabaseProductName())) { + if (logger.isDebugEnabled()) { + logger.debug("Oracle JDBC driver did not return procedure/function/signature for '" + + metaDataProcedureName + "' - assuming a non-exposed synonym"); + } + } + else { + throw new InvalidDataAccessApiUsageException( + "Unable to determine the correct call signature - no " + + "procedure/function/signature for '" + metaDataProcedureName + "'"); + } + } + + if (logger.isDebugEnabled()) { + logger.debug("Retrieving column meta-data for " + (function ? "function" : "procedure") + ' ' + + metaDataCatalogName + '/' + metaDataSchemaName + '/' + metaDataProcedureName); + } + try (ResultSet columns = function ? + databaseMetaData.getFunctionColumns(metaDataCatalogName, metaDataSchemaName, metaDataProcedureName, null) : + databaseMetaData.getProcedureColumns(metaDataCatalogName, metaDataSchemaName, metaDataProcedureName, null)) { + while (columns.next()) { + String columnName = columns.getString("COLUMN_NAME"); + int columnType = columns.getInt("COLUMN_TYPE"); + if (columnName == null && isInOrOutColumn(columnType, function)) { + if (logger.isDebugEnabled()) { + logger.debug("Skipping meta-data for: " + columnType + " " + columns.getInt("DATA_TYPE") + + " " + columns.getString("TYPE_NAME") + " " + columns.getInt("NULLABLE") + + " (probably a member of a collection)"); + } + } + else { + int nullable = (function ? DatabaseMetaData.functionNullable : DatabaseMetaData.procedureNullable); + CallParameterMetaData meta = new CallParameterMetaData(function, columnName, columnType, + columns.getInt("DATA_TYPE"), columns.getString("TYPE_NAME"), + columns.getInt("NULLABLE") == nullable); + this.callParameterMetaData.add(meta); + if (logger.isDebugEnabled()) { + logger.debug("Retrieved meta-data: " + meta.getParameterName() + " " + + meta.getParameterType() + " " + meta.getSqlType() + " " + + meta.getTypeName() + " " + meta.isNullable()); + } + } + } + } + } + catch (SQLException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Error while retrieving meta-data for procedure columns: " + ex); + } + } + } + + private static boolean isInOrOutColumn(int columnType, boolean function) { + if (function) { + return (columnType == DatabaseMetaData.functionColumnIn || + columnType == DatabaseMetaData.functionColumnInOut || + columnType == DatabaseMetaData.functionColumnOut); + } + else { + return (columnType == DatabaseMetaData.procedureColumnIn || + columnType == DatabaseMetaData.procedureColumnInOut || + columnType == DatabaseMetaData.procedureColumnOut); + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericTableMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericTableMetaDataProvider.java new file mode 100644 index 0000000..b69f9be --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericTableMetaDataProvider.java @@ -0,0 +1,476 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.lang.Nullable; + +/** + * A generic implementation of the {@link TableMetaDataProvider} interface + * which should provide enough features for all supported databases. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.5 + */ +public class GenericTableMetaDataProvider implements TableMetaDataProvider { + + /** Logger available to subclasses. */ + protected static final Log logger = LogFactory.getLog(TableMetaDataProvider.class); + + /** indicator whether column meta-data should be used. */ + private boolean tableColumnMetaDataUsed = false; + + /** the version of the database. */ + @Nullable + private String databaseVersion; + + /** the name of the user currently connected. */ + @Nullable + private String userName; + + /** indicates whether the identifiers are uppercased. */ + private boolean storesUpperCaseIdentifiers = true; + + /** indicates whether the identifiers are lowercased. */ + private boolean storesLowerCaseIdentifiers = false; + + /** indicates whether generated keys retrieval is supported. */ + private boolean getGeneratedKeysSupported = true; + + /** indicates whether the use of a String[] for generated keys is supported. */ + private boolean generatedKeysColumnNameArraySupported = true; + + /** database products we know not supporting the use of a String[] for generated keys. */ + private List productsNotSupportingGeneratedKeysColumnNameArray = + Arrays.asList("Apache Derby", "HSQL Database Engine"); + + /** Collection of TableParameterMetaData objects. */ + private List tableParameterMetaData = new ArrayList<>(); + + + /** + * Constructor used to initialize with provided database meta-data. + * @param databaseMetaData meta-data to be used + */ + protected GenericTableMetaDataProvider(DatabaseMetaData databaseMetaData) throws SQLException { + this.userName = databaseMetaData.getUserName(); + } + + + public void setStoresUpperCaseIdentifiers(boolean storesUpperCaseIdentifiers) { + this.storesUpperCaseIdentifiers = storesUpperCaseIdentifiers; + } + + public boolean isStoresUpperCaseIdentifiers() { + return this.storesUpperCaseIdentifiers; + } + + public void setStoresLowerCaseIdentifiers(boolean storesLowerCaseIdentifiers) { + this.storesLowerCaseIdentifiers = storesLowerCaseIdentifiers; + } + + public boolean isStoresLowerCaseIdentifiers() { + return this.storesLowerCaseIdentifiers; + } + + + @Override + public boolean isTableColumnMetaDataUsed() { + return this.tableColumnMetaDataUsed; + } + + @Override + public List getTableParameterMetaData() { + return this.tableParameterMetaData; + } + + @Override + public boolean isGetGeneratedKeysSupported() { + return this.getGeneratedKeysSupported; + } + + @Override + public boolean isGetGeneratedKeysSimulated(){ + return false; + } + + @Override + @Nullable + public String getSimpleQueryForGetGeneratedKey(String tableName, String keyColumnName) { + return null; + } + + public void setGetGeneratedKeysSupported(boolean getGeneratedKeysSupported) { + this.getGeneratedKeysSupported = getGeneratedKeysSupported; + } + + public void setGeneratedKeysColumnNameArraySupported(boolean generatedKeysColumnNameArraySupported) { + this.generatedKeysColumnNameArraySupported = generatedKeysColumnNameArraySupported; + } + + @Override + public boolean isGeneratedKeysColumnNameArraySupported() { + return this.generatedKeysColumnNameArraySupported; + } + + + @Override + public void initializeWithMetaData(DatabaseMetaData databaseMetaData) throws SQLException { + try { + if (databaseMetaData.supportsGetGeneratedKeys()) { + logger.debug("GetGeneratedKeys is supported"); + setGetGeneratedKeysSupported(true); + } + else { + logger.debug("GetGeneratedKeys is not supported"); + setGetGeneratedKeysSupported(false); + } + } + catch (SQLException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Error retrieving 'DatabaseMetaData.getGeneratedKeys': " + ex.getMessage()); + } + } + try { + String databaseProductName = databaseMetaData.getDatabaseProductName(); + if (this.productsNotSupportingGeneratedKeysColumnNameArray.contains(databaseProductName)) { + if (logger.isDebugEnabled()) { + logger.debug("GeneratedKeysColumnNameArray is not supported for " + databaseProductName); + } + setGeneratedKeysColumnNameArraySupported(false); + } + else { + if (isGetGeneratedKeysSupported()) { + if (logger.isDebugEnabled()) { + logger.debug("GeneratedKeysColumnNameArray is supported for " + databaseProductName); + } + setGeneratedKeysColumnNameArraySupported(true); + } + else { + setGeneratedKeysColumnNameArraySupported(false); + } + } + } + catch (SQLException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Error retrieving 'DatabaseMetaData.getDatabaseProductName': " + ex.getMessage()); + } + } + + try { + this.databaseVersion = databaseMetaData.getDatabaseProductVersion(); + } + catch (SQLException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Error retrieving 'DatabaseMetaData.getDatabaseProductVersion': " + ex.getMessage()); + } + } + + try { + setStoresUpperCaseIdentifiers(databaseMetaData.storesUpperCaseIdentifiers()); + } + catch (SQLException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Error retrieving 'DatabaseMetaData.storesUpperCaseIdentifiers': " + ex.getMessage()); + } + } + + try { + setStoresLowerCaseIdentifiers(databaseMetaData.storesLowerCaseIdentifiers()); + } + catch (SQLException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Error retrieving 'DatabaseMetaData.storesLowerCaseIdentifiers': " + ex.getMessage()); + } + } + } + + @Override + public void initializeWithTableColumnMetaData(DatabaseMetaData databaseMetaData, @Nullable String catalogName, + @Nullable String schemaName, @Nullable String tableName) throws SQLException { + + this.tableColumnMetaDataUsed = true; + locateTableAndProcessMetaData(databaseMetaData, catalogName, schemaName, tableName); + } + + @Override + @Nullable + public String tableNameToUse(@Nullable String tableName) { + if (tableName == null) { + return null; + } + else if (isStoresUpperCaseIdentifiers()) { + return tableName.toUpperCase(); + } + else if (isStoresLowerCaseIdentifiers()) { + return tableName.toLowerCase(); + } + else { + return tableName; + } + } + + @Override + @Nullable + public String catalogNameToUse(@Nullable String catalogName) { + if (catalogName == null) { + return null; + } + else if (isStoresUpperCaseIdentifiers()) { + return catalogName.toUpperCase(); + } + else if (isStoresLowerCaseIdentifiers()) { + return catalogName.toLowerCase(); + } + else { + return catalogName; + } + } + + @Override + @Nullable + public String schemaNameToUse(@Nullable String schemaName) { + if (schemaName == null) { + return null; + } + else if (isStoresUpperCaseIdentifiers()) { + return schemaName.toUpperCase(); + } + else if (isStoresLowerCaseIdentifiers()) { + return schemaName.toLowerCase(); + } + else { + return schemaName; + } + } + + @Override + @Nullable + public String metaDataCatalogNameToUse(@Nullable String catalogName) { + return catalogNameToUse(catalogName); + } + + @Override + @Nullable + public String metaDataSchemaNameToUse(@Nullable String schemaName) { + if (schemaName == null) { + return schemaNameToUse(getDefaultSchema()); + } + return schemaNameToUse(schemaName); + } + + /** + * Provide access to default schema for subclasses. + */ + @Nullable + protected String getDefaultSchema() { + return this.userName; + } + + /** + * Provide access to version info for subclasses. + */ + @Nullable + protected String getDatabaseVersion() { + return this.databaseVersion; + } + + /** + * Method supporting the meta-data processing for a table. + */ + private void locateTableAndProcessMetaData(DatabaseMetaData databaseMetaData, + @Nullable String catalogName, @Nullable String schemaName, @Nullable String tableName) { + + Map tableMeta = new HashMap<>(); + ResultSet tables = null; + try { + tables = databaseMetaData.getTables( + catalogNameToUse(catalogName), schemaNameToUse(schemaName), tableNameToUse(tableName), null); + while (tables != null && tables.next()) { + TableMetaData tmd = new TableMetaData(); + tmd.setCatalogName(tables.getString("TABLE_CAT")); + tmd.setSchemaName(tables.getString("TABLE_SCHEM")); + tmd.setTableName(tables.getString("TABLE_NAME")); + if (tmd.getSchemaName() == null) { + tableMeta.put(this.userName != null ? this.userName.toUpperCase() : "", tmd); + } + else { + tableMeta.put(tmd.getSchemaName().toUpperCase(), tmd); + } + } + } + catch (SQLException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Error while accessing table meta-data results: " + ex.getMessage()); + } + } + finally { + JdbcUtils.closeResultSet(tables); + } + + if (tableMeta.isEmpty()) { + if (logger.isInfoEnabled()) { + logger.info("Unable to locate table meta-data for '" + tableName + "': column names must be provided"); + } + } + else { + processTableColumns(databaseMetaData, findTableMetaData(schemaName, tableName, tableMeta)); + } + } + + private TableMetaData findTableMetaData(@Nullable String schemaName, @Nullable String tableName, + Map tableMeta) { + + if (schemaName != null) { + TableMetaData tmd = tableMeta.get(schemaName.toUpperCase()); + if (tmd == null) { + throw new DataAccessResourceFailureException("Unable to locate table meta-data for '" + + tableName + "' in the '" + schemaName + "' schema"); + } + return tmd; + } + else if (tableMeta.size() == 1) { + return tableMeta.values().iterator().next(); + } + else { + TableMetaData tmd = tableMeta.get(getDefaultSchema()); + if (tmd == null) { + tmd = tableMeta.get(this.userName != null ? this.userName.toUpperCase() : ""); + } + if (tmd == null) { + tmd = tableMeta.get("PUBLIC"); + } + if (tmd == null) { + tmd = tableMeta.get("DBO"); + } + if (tmd == null) { + throw new DataAccessResourceFailureException( + "Unable to locate table meta-data for '" + tableName + "' in the default schema"); + } + return tmd; + } + } + + /** + * Method supporting the meta-data processing for a table's columns. + */ + private void processTableColumns(DatabaseMetaData databaseMetaData, TableMetaData tmd) { + ResultSet tableColumns = null; + String metaDataCatalogName = metaDataCatalogNameToUse(tmd.getCatalogName()); + String metaDataSchemaName = metaDataSchemaNameToUse(tmd.getSchemaName()); + String metaDataTableName = tableNameToUse(tmd.getTableName()); + if (logger.isDebugEnabled()) { + logger.debug("Retrieving meta-data for " + metaDataCatalogName + '/' + + metaDataSchemaName + '/' + metaDataTableName); + } + try { + tableColumns = databaseMetaData.getColumns( + metaDataCatalogName, metaDataSchemaName, metaDataTableName, null); + while (tableColumns.next()) { + String columnName = tableColumns.getString("COLUMN_NAME"); + int dataType = tableColumns.getInt("DATA_TYPE"); + if (dataType == Types.DECIMAL) { + String typeName = tableColumns.getString("TYPE_NAME"); + int decimalDigits = tableColumns.getInt("DECIMAL_DIGITS"); + // Override a DECIMAL data type for no-decimal numerics + // (this is for better Oracle support where there have been issues + // using DECIMAL for certain inserts (see SPR-6912)) + if ("NUMBER".equals(typeName) && decimalDigits == 0) { + dataType = Types.NUMERIC; + if (logger.isDebugEnabled()) { + logger.debug("Overriding meta-data: " + columnName + " now NUMERIC instead of DECIMAL"); + } + } + } + boolean nullable = tableColumns.getBoolean("NULLABLE"); + TableParameterMetaData meta = new TableParameterMetaData(columnName, dataType, nullable); + this.tableParameterMetaData.add(meta); + if (logger.isDebugEnabled()) { + logger.debug("Retrieved meta-data: '" + meta.getParameterName() + "', sqlType=" + + meta.getSqlType() + ", nullable=" + meta.isNullable()); + } + } + } + catch (SQLException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Error while retrieving meta-data for table columns: " + ex.getMessage()); + } + } + finally { + JdbcUtils.closeResultSet(tableColumns); + } + } + + + /** + * Inner class representing table meta-data. + */ + private static class TableMetaData { + + @Nullable + private String catalogName; + + @Nullable + private String schemaName; + + @Nullable + private String tableName; + + public void setCatalogName(String catalogName) { + this.catalogName = catalogName; + } + + @Nullable + public String getCatalogName() { + return this.catalogName; + } + + public void setSchemaName(String schemaName) { + this.schemaName = schemaName; + } + + @Nullable + public String getSchemaName() { + return this.schemaName; + } + + public void setTableName(String tableName) { + this.tableName = tableName; + } + + @Nullable + public String getTableName() { + return this.tableName; + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/HanaCallMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/HanaCallMetaDataProvider.java new file mode 100644 index 0000000..01b2fdb --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/HanaCallMetaDataProvider.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +/** + * SAP HANA specific implementation for the {@link CallMetaDataProvider} interface. + * This class is intended for internal use by the Simple JDBC classes. + * + * @author Subhobrata Dey + * @author Juergen Hoeller + * @since 4.2.1 + */ +public class HanaCallMetaDataProvider extends GenericCallMetaDataProvider { + + public HanaCallMetaDataProvider(DatabaseMetaData databaseMetaData) throws SQLException { + super(databaseMetaData); + } + + + @Override + public void initializeWithMetaData(DatabaseMetaData databaseMetaData) throws SQLException { + super.initializeWithMetaData(databaseMetaData); + setStoresUpperCaseIdentifiers(false); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/HsqlTableMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/HsqlTableMetaDataProvider.java new file mode 100644 index 0000000..6a49e9f --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/HsqlTableMetaDataProvider.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +/** + * The HSQL specific implementation of {@link TableMetaDataProvider}. + * Supports a feature for retrieving generated keys without the JDBC 3.0 + * {@code getGeneratedKeys} support. + * + * @author Thomas Risberg + * @since 2.5 + */ +public class HsqlTableMetaDataProvider extends GenericTableMetaDataProvider { + + public HsqlTableMetaDataProvider(DatabaseMetaData databaseMetaData) throws SQLException { + super(databaseMetaData); + } + + + @Override + public boolean isGetGeneratedKeysSimulated() { + return true; + } + + @Override + public String getSimpleQueryForGetGeneratedKey(String tableName, String keyColumnName) { + return "select max(identity()) from " + tableName; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/OracleCallMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/OracleCallMetaDataProvider.java new file mode 100644 index 0000000..f655eb0 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/OracleCallMetaDataProvider.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.sql.Types; + +import org.springframework.jdbc.core.ColumnMapRowMapper; +import org.springframework.jdbc.core.SqlOutParameter; +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.lang.Nullable; + +/** + * Oracle-specific implementation for the {@link CallMetaDataProvider} interface. + * This class is intended for internal use by the Simple JDBC classes. + * + * @author Thomas Risberg + * @since 2.5 + */ +public class OracleCallMetaDataProvider extends GenericCallMetaDataProvider { + + private static final String REF_CURSOR_NAME = "REF CURSOR"; + + + public OracleCallMetaDataProvider(DatabaseMetaData databaseMetaData) throws SQLException { + super(databaseMetaData); + } + + + @Override + public boolean isReturnResultSetSupported() { + return false; + } + + @Override + public boolean isRefCursorSupported() { + return true; + } + + @Override + public int getRefCursorSqlType() { + return -10; + } + + @Override + @Nullable + public String metaDataCatalogNameToUse(@Nullable String catalogName) { + // Oracle uses catalog name for package name or an empty string if no package + return (catalogName == null ? "" : catalogNameToUse(catalogName)); + } + + @Override + @Nullable + public String metaDataSchemaNameToUse(@Nullable String schemaName) { + // Use current user schema if no schema specified + return (schemaName == null ? getUserName() : super.metaDataSchemaNameToUse(schemaName)); + } + + @Override + public SqlParameter createDefaultOutParameter(String parameterName, CallParameterMetaData meta) { + if (meta.getSqlType() == Types.OTHER && REF_CURSOR_NAME.equals(meta.getTypeName())) { + return new SqlOutParameter(parameterName, getRefCursorSqlType(), new ColumnMapRowMapper()); + } + else { + return super.createDefaultOutParameter(parameterName, meta); + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/OracleTableMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/OracleTableMetaDataProvider.java new file mode 100644 index 0000000..5374099 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/OracleTableMetaDataProvider.java @@ -0,0 +1,169 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.metadata; + +import java.lang.reflect.Method; +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.sql.Types; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; + +/** + * Oracle-specific implementation of the {@link org.springframework.jdbc.core.metadata.TableMetaDataProvider}. + * Supports a feature for including synonyms in the meta-data lookup. Also supports lookup of current schema + * using the {@code sys_context}. + * + *

    Thanks to Mike Youngstrom and Bruce Campbell for submitting the original suggestion for the Oracle + * current schema lookup implementation. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 3.0 + */ +public class OracleTableMetaDataProvider extends GenericTableMetaDataProvider { + + private final boolean includeSynonyms; + + @Nullable + private final String defaultSchema; + + + /** + * Constructor used to initialize with provided database meta-data. + * @param databaseMetaData meta-data to be used + */ + public OracleTableMetaDataProvider(DatabaseMetaData databaseMetaData) throws SQLException { + this(databaseMetaData, false); + } + + /** + * Constructor used to initialize with provided database meta-data. + * @param databaseMetaData meta-data to be used + * @param includeSynonyms whether to include synonyms + */ + public OracleTableMetaDataProvider(DatabaseMetaData databaseMetaData, boolean includeSynonyms) + throws SQLException { + + super(databaseMetaData); + this.includeSynonyms = includeSynonyms; + this.defaultSchema = lookupDefaultSchema(databaseMetaData); + } + + + /* + * Oracle-based implementation for detecting the current schema. + */ + @Nullable + private static String lookupDefaultSchema(DatabaseMetaData databaseMetaData) { + try { + CallableStatement cstmt = null; + try { + Connection con = databaseMetaData.getConnection(); + if (con == null) { + logger.debug("Cannot check default schema - no Connection from DatabaseMetaData"); + return null; + } + cstmt = con.prepareCall("{? = call sys_context('USERENV', 'CURRENT_SCHEMA')}"); + cstmt.registerOutParameter(1, Types.VARCHAR); + cstmt.execute(); + return cstmt.getString(1); + } + finally { + if (cstmt != null) { + cstmt.close(); + } + } + } + catch (SQLException ex) { + logger.debug("Exception encountered during default schema lookup", ex); + return null; + } + } + + @Override + @Nullable + protected String getDefaultSchema() { + if (this.defaultSchema != null) { + return this.defaultSchema; + } + return super.getDefaultSchema(); + } + + + @Override + public void initializeWithTableColumnMetaData(DatabaseMetaData databaseMetaData, + @Nullable String catalogName, @Nullable String schemaName, @Nullable String tableName) + throws SQLException { + + if (!this.includeSynonyms) { + logger.debug("Defaulting to no synonyms in table meta-data lookup"); + super.initializeWithTableColumnMetaData(databaseMetaData, catalogName, schemaName, tableName); + return; + } + + Connection con = databaseMetaData.getConnection(); + if (con == null) { + logger.info("Unable to include synonyms in table meta-data lookup - no Connection from DatabaseMetaData"); + super.initializeWithTableColumnMetaData(databaseMetaData, catalogName, schemaName, tableName); + return; + } + + try { + Class oracleConClass = con.getClass().getClassLoader().loadClass("oracle.jdbc.OracleConnection"); + con = (Connection) con.unwrap(oracleConClass); + } + catch (ClassNotFoundException | SQLException ex) { + if (logger.isInfoEnabled()) { + logger.info("Unable to include synonyms in table meta-data lookup - no Oracle Connection: " + ex); + } + super.initializeWithTableColumnMetaData(databaseMetaData, catalogName, schemaName, tableName); + return; + } + + logger.debug("Including synonyms in table meta-data lookup"); + Method setIncludeSynonyms; + Boolean originalValueForIncludeSynonyms; + + try { + Method getIncludeSynonyms = con.getClass().getMethod("getIncludeSynonyms"); + ReflectionUtils.makeAccessible(getIncludeSynonyms); + originalValueForIncludeSynonyms = (Boolean) getIncludeSynonyms.invoke(con); + + setIncludeSynonyms = con.getClass().getMethod("setIncludeSynonyms", boolean.class); + ReflectionUtils.makeAccessible(setIncludeSynonyms); + setIncludeSynonyms.invoke(con, Boolean.TRUE); + } + catch (Throwable ex) { + throw new InvalidDataAccessApiUsageException("Could not prepare Oracle Connection", ex); + } + + super.initializeWithTableColumnMetaData(databaseMetaData, catalogName, schemaName, tableName); + + try { + setIncludeSynonyms.invoke(con, originalValueForIncludeSynonyms); + } + catch (Throwable ex) { + throw new InvalidDataAccessApiUsageException("Could not reset Oracle Connection", ex); + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/PostgresCallMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/PostgresCallMetaDataProvider.java new file mode 100644 index 0000000..2881eb2 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/PostgresCallMetaDataProvider.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.sql.Types; + +import org.springframework.jdbc.core.ColumnMapRowMapper; +import org.springframework.jdbc.core.SqlOutParameter; +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.lang.Nullable; + +/** + * Postgres-specific implementation for the {@link CallMetaDataProvider} interface. + * This class is intended for internal use by the Simple JDBC classes. + * + * @author Thomas Risberg + * @since 2.5 + */ +public class PostgresCallMetaDataProvider extends GenericCallMetaDataProvider { + + private static final String RETURN_VALUE_NAME = "returnValue"; + + + public PostgresCallMetaDataProvider(DatabaseMetaData databaseMetaData) throws SQLException { + super(databaseMetaData); + } + + + @Override + public boolean isReturnResultSetSupported() { + return false; + } + + @Override + public boolean isRefCursorSupported() { + return true; + } + + @Override + public int getRefCursorSqlType() { + return Types.OTHER; + } + + @Override + @Nullable + public String metaDataSchemaNameToUse(@Nullable String schemaName) { + // Use public schema if no schema specified + return (schemaName == null ? "public" : super.metaDataSchemaNameToUse(schemaName)); + } + + @Override + public SqlParameter createDefaultOutParameter(String parameterName, CallParameterMetaData meta) { + if (meta.getSqlType() == Types.OTHER && "refcursor".equals(meta.getTypeName())) { + return new SqlOutParameter(parameterName, getRefCursorSqlType(), new ColumnMapRowMapper()); + } + else { + return super.createDefaultOutParameter(parameterName, meta); + } + } + + @Override + public boolean byPassReturnParameter(String parameterName) { + return RETURN_VALUE_NAME.equals(parameterName); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/PostgresTableMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/PostgresTableMetaDataProvider.java new file mode 100644 index 0000000..d7d6512 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/PostgresTableMetaDataProvider.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +/** + * The PostgreSQL specific implementation of {@link TableMetaDataProvider}. + * Supports a feature for retrieving generated keys without the JDBC 3.0 + * {@code getGeneratedKeys} support. + * + * @author Thomas Risberg + * @since 2.5 + */ +public class PostgresTableMetaDataProvider extends GenericTableMetaDataProvider { + + public PostgresTableMetaDataProvider(DatabaseMetaData databaseMetaData) throws SQLException { + super(databaseMetaData); + } + + + @Override + public boolean isGetGeneratedKeysSimulated() { + return true; + } + + @Override + public String getSimpleQueryForGetGeneratedKey(String tableName, String keyColumnName) { + return "RETURNING " + keyColumnName; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/SqlServerCallMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/SqlServerCallMetaDataProvider.java new file mode 100644 index 0000000..ce408a1 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/SqlServerCallMetaDataProvider.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +import org.springframework.lang.Nullable; + +/** + * SQL Server specific implementation for the {@link CallMetaDataProvider} interface. + * This class is intended for internal use by the Simple JDBC classes. + * + * @author Thomas Risberg + * @since 2.5 + */ +public class SqlServerCallMetaDataProvider extends GenericCallMetaDataProvider { + + private static final String REMOVABLE_COLUMN_PREFIX = "@"; + + private static final String RETURN_VALUE_NAME = "@RETURN_VALUE"; + + + public SqlServerCallMetaDataProvider(DatabaseMetaData databaseMetaData) throws SQLException { + super(databaseMetaData); + } + + + @Override + @Nullable + public String parameterNameToUse(@Nullable String parameterName) { + if (parameterName == null) { + return null; + } + else if (parameterName.length() > 1 && parameterName.startsWith(REMOVABLE_COLUMN_PREFIX)) { + return super.parameterNameToUse(parameterName.substring(1)); + } + else { + return super.parameterNameToUse(parameterName); + } + } + + @Override + public boolean byPassReturnParameter(String parameterName) { + return RETURN_VALUE_NAME.equals(parameterName); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/SybaseCallMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/SybaseCallMetaDataProvider.java new file mode 100644 index 0000000..5d590a9 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/SybaseCallMetaDataProvider.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +import org.springframework.lang.Nullable; + +/** + * Sybase specific implementation for the {@link CallMetaDataProvider} interface. + * This class is intended for internal use by the Simple JDBC classes. + * + * @author Thomas Risberg + * @since 2.5 + */ +public class SybaseCallMetaDataProvider extends GenericCallMetaDataProvider { + + private static final String REMOVABLE_COLUMN_PREFIX = "@"; + + private static final String RETURN_VALUE_NAME = "RETURN_VALUE"; + + + public SybaseCallMetaDataProvider(DatabaseMetaData databaseMetaData) throws SQLException { + super(databaseMetaData); + } + + + @Override + @Nullable + public String parameterNameToUse(@Nullable String parameterName) { + if (parameterName == null) { + return null; + } + else if (parameterName.length() > 1 && parameterName.startsWith(REMOVABLE_COLUMN_PREFIX)) { + return super.parameterNameToUse(parameterName.substring(1)); + } + else { + return super.parameterNameToUse(parameterName); + } + } + + @Override + public boolean byPassReturnParameter(String parameterName) { + return (RETURN_VALUE_NAME.equals(parameterName) || + RETURN_VALUE_NAME.equals(parameterNameToUse(parameterName))); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java new file mode 100644 index 0000000..908b165 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java @@ -0,0 +1,393 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.metadata; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.jdbc.core.SqlTypeValue; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils; +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * Class to manage context meta-data used for the configuration + * and execution of operations on a database table. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.5 + */ +public class TableMetaDataContext { + + // Logger available to subclasses + protected final Log logger = LogFactory.getLog(getClass()); + + // Name of table for this context + @Nullable + private String tableName; + + // Name of catalog for this context + @Nullable + private String catalogName; + + // Name of schema for this context + @Nullable + private String schemaName; + + // List of columns objects to be used in this context + private List tableColumns = new ArrayList<>(); + + // Should we access insert parameter meta-data info or not + private boolean accessTableColumnMetaData = true; + + // Should we override default for including synonyms for meta-data lookups + private boolean overrideIncludeSynonymsDefault = false; + + // The provider of table meta-data + @Nullable + private TableMetaDataProvider metaDataProvider; + + // Are we using generated key columns + private boolean generatedKeyColumnsUsed = false; + + + /** + * Set the name of the table for this context. + */ + public void setTableName(@Nullable String tableName) { + this.tableName = tableName; + } + + /** + * Get the name of the table for this context. + */ + @Nullable + public String getTableName() { + return this.tableName; + } + + /** + * Set the name of the catalog for this context. + */ + public void setCatalogName(@Nullable String catalogName) { + this.catalogName = catalogName; + } + + /** + * Get the name of the catalog for this context. + */ + @Nullable + public String getCatalogName() { + return this.catalogName; + } + + /** + * Set the name of the schema for this context. + */ + public void setSchemaName(@Nullable String schemaName) { + this.schemaName = schemaName; + } + + /** + * Get the name of the schema for this context. + */ + @Nullable + public String getSchemaName() { + return this.schemaName; + } + + /** + * Specify whether we should access table column meta-data. + */ + public void setAccessTableColumnMetaData(boolean accessTableColumnMetaData) { + this.accessTableColumnMetaData = accessTableColumnMetaData; + } + + /** + * Are we accessing table meta-data? + */ + public boolean isAccessTableColumnMetaData() { + return this.accessTableColumnMetaData; + } + + + /** + * Specify whether we should override default for accessing synonyms. + */ + public void setOverrideIncludeSynonymsDefault(boolean override) { + this.overrideIncludeSynonymsDefault = override; + } + + /** + * Are we overriding include synonyms default? + */ + public boolean isOverrideIncludeSynonymsDefault() { + return this.overrideIncludeSynonymsDefault; + } + + /** + * Get a List of the table column names. + */ + public List getTableColumns() { + return this.tableColumns; + } + + + /** + * Process the current meta-data with the provided configuration options. + * @param dataSource the DataSource being used + * @param declaredColumns any columns that are declared + * @param generatedKeyNames name of generated keys + */ + public void processMetaData(DataSource dataSource, List declaredColumns, String[] generatedKeyNames) { + this.metaDataProvider = TableMetaDataProviderFactory.createMetaDataProvider(dataSource, this); + this.tableColumns = reconcileColumnsToUse(declaredColumns, generatedKeyNames); + } + + private TableMetaDataProvider obtainMetaDataProvider() { + Assert.state(this.metaDataProvider != null, "No TableMetaDataProvider - call processMetaData first"); + return this.metaDataProvider; + } + + /** + * Compare columns created from meta-data with declared columns and return a reconciled list. + * @param declaredColumns declared column names + * @param generatedKeyNames names of generated key columns + */ + protected List reconcileColumnsToUse(List declaredColumns, String[] generatedKeyNames) { + if (generatedKeyNames.length > 0) { + this.generatedKeyColumnsUsed = true; + } + if (!declaredColumns.isEmpty()) { + return new ArrayList<>(declaredColumns); + } + Set keys = new LinkedHashSet<>(generatedKeyNames.length); + for (String key : generatedKeyNames) { + keys.add(key.toUpperCase()); + } + List columns = new ArrayList<>(); + for (TableParameterMetaData meta : obtainMetaDataProvider().getTableParameterMetaData()) { + if (!keys.contains(meta.getParameterName().toUpperCase())) { + columns.add(meta.getParameterName()); + } + } + return columns; + } + + /** + * Match the provided column names and values with the list of columns used. + * @param parameterSource the parameter names and values + */ + public List matchInParameterValuesWithInsertColumns(SqlParameterSource parameterSource) { + List values = new ArrayList<>(); + // For parameter source lookups we need to provide case-insensitive lookup support since the + // database meta-data is not necessarily providing case-sensitive column names + Map caseInsensitiveParameterNames = + SqlParameterSourceUtils.extractCaseInsensitiveParameterNames(parameterSource); + for (String column : this.tableColumns) { + if (parameterSource.hasValue(column)) { + values.add(SqlParameterSourceUtils.getTypedValue(parameterSource, column)); + } + else { + String lowerCaseName = column.toLowerCase(); + if (parameterSource.hasValue(lowerCaseName)) { + values.add(SqlParameterSourceUtils.getTypedValue(parameterSource, lowerCaseName)); + } + else { + String propertyName = JdbcUtils.convertUnderscoreNameToPropertyName(column); + if (parameterSource.hasValue(propertyName)) { + values.add(SqlParameterSourceUtils.getTypedValue(parameterSource, propertyName)); + } + else { + if (caseInsensitiveParameterNames.containsKey(lowerCaseName)) { + values.add(SqlParameterSourceUtils.getTypedValue( + parameterSource, caseInsensitiveParameterNames.get(lowerCaseName))); + } + else { + values.add(null); + } + } + } + } + } + return values; + } + + /** + * Match the provided column names and values with the list of columns used. + * @param inParameters the parameter names and values + */ + public List matchInParameterValuesWithInsertColumns(Map inParameters) { + List values = new ArrayList<>(inParameters.size()); + for (String column : this.tableColumns) { + Object value = inParameters.get(column); + if (value == null) { + value = inParameters.get(column.toLowerCase()); + if (value == null) { + for (Map.Entry entry : inParameters.entrySet()) { + if (column.equalsIgnoreCase(entry.getKey())) { + value = entry.getValue(); + break; + } + } + } + } + values.add(value); + } + return values; + } + + + /** + * Build the insert string based on configuration and meta-data information. + * @return the insert string to be used + */ + public String createInsertString(String... generatedKeyNames) { + Set keys = new LinkedHashSet<>(generatedKeyNames.length); + for (String key : generatedKeyNames) { + keys.add(key.toUpperCase()); + } + StringBuilder insertStatement = new StringBuilder(); + insertStatement.append("INSERT INTO "); + if (getSchemaName() != null) { + insertStatement.append(getSchemaName()); + insertStatement.append("."); + } + insertStatement.append(getTableName()); + insertStatement.append(" ("); + int columnCount = 0; + for (String columnName : getTableColumns()) { + if (!keys.contains(columnName.toUpperCase())) { + columnCount++; + if (columnCount > 1) { + insertStatement.append(", "); + } + insertStatement.append(columnName); + } + } + insertStatement.append(") VALUES("); + if (columnCount < 1) { + if (this.generatedKeyColumnsUsed) { + if (logger.isDebugEnabled()) { + logger.debug("Unable to locate non-key columns for table '" + + getTableName() + "' so an empty insert statement is generated"); + } + } + else { + throw new InvalidDataAccessApiUsageException("Unable to locate columns for table '" + + getTableName() + "' so an insert statement can't be generated"); + } + } + String params = String.join(", ", Collections.nCopies(columnCount, "?")); + insertStatement.append(params); + insertStatement.append(")"); + return insertStatement.toString(); + } + + /** + * Build the array of {@link java.sql.Types} based on configuration and meta-data information. + * @return the array of types to be used + */ + public int[] createInsertTypes() { + int[] types = new int[getTableColumns().size()]; + List parameters = obtainMetaDataProvider().getTableParameterMetaData(); + Map parameterMap = CollectionUtils.newLinkedHashMap(parameters.size()); + for (TableParameterMetaData tpmd : parameters) { + parameterMap.put(tpmd.getParameterName().toUpperCase(), tpmd); + } + int typeIndx = 0; + for (String column : getTableColumns()) { + if (column == null) { + types[typeIndx] = SqlTypeValue.TYPE_UNKNOWN; + } + else { + TableParameterMetaData tpmd = parameterMap.get(column.toUpperCase()); + if (tpmd != null) { + types[typeIndx] = tpmd.getSqlType(); + } + else { + types[typeIndx] = SqlTypeValue.TYPE_UNKNOWN; + } + } + typeIndx++; + } + return types; + } + + + /** + * Does this database support the JDBC 3.0 feature of retrieving generated keys: + * {@link java.sql.DatabaseMetaData#supportsGetGeneratedKeys()}? + */ + public boolean isGetGeneratedKeysSupported() { + return obtainMetaDataProvider().isGetGeneratedKeysSupported(); + } + + /** + * Does this database support simple query to retrieve generated keys + * when the JDBC 3.0 feature is not supported: + * {@link java.sql.DatabaseMetaData#supportsGetGeneratedKeys()}? + */ + public boolean isGetGeneratedKeysSimulated() { + return obtainMetaDataProvider().isGetGeneratedKeysSimulated(); + } + + /** + * Does this database support a simple query to retrieve generated keys + * when the JDBC 3.0 feature is not supported: + * {@link java.sql.DatabaseMetaData#supportsGetGeneratedKeys()}? + * @deprecated as of 4.3.15, in favor of {@link #getSimpleQueryForGetGeneratedKey} + */ + @Deprecated + @Nullable + public String getSimulationQueryForGetGeneratedKey(String tableName, String keyColumnName) { + return getSimpleQueryForGetGeneratedKey(tableName, keyColumnName); + } + + /** + * Does this database support a simple query to retrieve generated keys + * when the JDBC 3.0 feature is not supported: + * {@link java.sql.DatabaseMetaData#supportsGetGeneratedKeys()}? + */ + @Nullable + public String getSimpleQueryForGetGeneratedKey(String tableName, String keyColumnName) { + return obtainMetaDataProvider().getSimpleQueryForGetGeneratedKey(tableName, keyColumnName); + } + + /** + * Is a column name String array for retrieving generated keys supported: + * {@link java.sql.Connection#createStruct(String, Object[])}? + */ + public boolean isGeneratedKeysColumnNameArraySupported() { + return obtainMetaDataProvider().isGeneratedKeysColumnNameArraySupported(); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataProvider.java new file mode 100644 index 0000000..f5af3e9 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataProvider.java @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.util.List; + +import org.springframework.lang.Nullable; + +/** + * Interface specifying the API to be implemented by a class providing table meta-data. + * This is intended for internal use by the Simple JDBC classes. + * + * @author Thomas Risberg + * @since 2.5 + */ +public interface TableMetaDataProvider { + + /** + * Initialize using the database meta-data provided. + * @param databaseMetaData used to retrieve database specific information + * @throws SQLException in case of initialization failure + */ + void initializeWithMetaData(DatabaseMetaData databaseMetaData) throws SQLException; + + /** + * Initialize using provided database meta-data, table and column information. + * This initialization can be turned off by specifying that column meta-data should not be used. + * @param databaseMetaData used to retrieve database specific information + * @param catalogName name of catalog to use (or {@code null} if none) + * @param schemaName name of schema name to use (or {@code null} if none) + * @param tableName name of the table + * @throws SQLException in case of initialization failure + */ + void initializeWithTableColumnMetaData(DatabaseMetaData databaseMetaData, @Nullable String catalogName, + @Nullable String schemaName, @Nullable String tableName) throws SQLException; + + /** + * Get the table name formatted based on meta-data information. + * This could include altering the case. + */ + @Nullable + String tableNameToUse(@Nullable String tableName); + + /** + * Get the catalog name formatted based on meta-data information. + * This could include altering the case. + */ + @Nullable + String catalogNameToUse(@Nullable String catalogName); + + /** + * Get the schema name formatted based on meta-data information. + * This could include altering the case. + */ + @Nullable + String schemaNameToUse(@Nullable String schemaName); + + /** + * Provide any modification of the catalog name passed in to match the meta-data currently used. + * The returned value will be used for meta-data lookups. + * This could include altering the case used or providing a base catalog if none is provided. + */ + @Nullable + String metaDataCatalogNameToUse(@Nullable String catalogName) ; + + /** + * Provide any modification of the schema name passed in to match the meta-data currently used. + * The returned value will be used for meta-data lookups. + * This could include altering the case used or providing a base schema if none is provided. + */ + @Nullable + String metaDataSchemaNameToUse(@Nullable String schemaName) ; + + /** + * Are we using the meta-data for the table columns? + */ + boolean isTableColumnMetaDataUsed(); + + /** + * Does this database support the JDBC 3.0 feature of retrieving generated keys: + * {@link java.sql.DatabaseMetaData#supportsGetGeneratedKeys()}? + */ + boolean isGetGeneratedKeysSupported(); + + /** + * Does this database support a simple query to retrieve the generated key when + * the JDBC 3.0 feature of retrieving generated keys is not supported? + * @see #isGetGeneratedKeysSupported() + */ + boolean isGetGeneratedKeysSimulated(); + + /** + * Get the simple query to retrieve a generated key. + */ + @Nullable + String getSimpleQueryForGetGeneratedKey(String tableName, String keyColumnName); + + /** + * Does this database support a column name String array for retrieving generated keys: + * {@link java.sql.Connection#createStruct(String, Object[])}? + */ + boolean isGeneratedKeysColumnNameArraySupported(); + + /** + * Get the table parameter meta-data that is currently used. + * @return a List of {@link TableParameterMetaData} + */ + List getTableParameterMetaData(); + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataProviderFactory.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataProviderFactory.java new file mode 100644 index 0000000..854d980 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataProviderFactory.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.metadata; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.jdbc.support.MetaDataAccessException; + +/** + * Factory used to create a {@link TableMetaDataProvider} implementation + * based on the type of database being used. + * + * @author Thomas Risberg + * @since 2.5 + */ +public final class TableMetaDataProviderFactory { + + private static final Log logger = LogFactory.getLog(TableMetaDataProviderFactory.class); + + + private TableMetaDataProviderFactory() { + } + + + /** + * Create a {@link TableMetaDataProvider} based on the database meta-data. + * @param dataSource used to retrieve meta-data + * @param context the class that holds configuration and meta-data + * @return instance of the TableMetaDataProvider implementation to be used + */ + public static TableMetaDataProvider createMetaDataProvider(DataSource dataSource, TableMetaDataContext context) { + try { + return JdbcUtils.extractDatabaseMetaData(dataSource, databaseMetaData -> { + String databaseProductName = JdbcUtils.commonDatabaseName(databaseMetaData.getDatabaseProductName()); + boolean accessTableColumnMetaData = context.isAccessTableColumnMetaData(); + TableMetaDataProvider provider; + + if ("Oracle".equals(databaseProductName)) { + provider = new OracleTableMetaDataProvider( + databaseMetaData, context.isOverrideIncludeSynonymsDefault()); + } + else if ("PostgreSQL".equals(databaseProductName)) { + provider = new PostgresTableMetaDataProvider(databaseMetaData); + } + else if ("Apache Derby".equals(databaseProductName)) { + provider = new DerbyTableMetaDataProvider(databaseMetaData); + } + else if ("HSQL Database Engine".equals(databaseProductName)) { + provider = new HsqlTableMetaDataProvider(databaseMetaData); + } + else { + provider = new GenericTableMetaDataProvider(databaseMetaData); + } + + if (logger.isDebugEnabled()) { + logger.debug("Using " + provider.getClass().getSimpleName()); + } + provider.initializeWithMetaData(databaseMetaData); + if (accessTableColumnMetaData) { + provider.initializeWithTableColumnMetaData(databaseMetaData, + context.getCatalogName(), context.getSchemaName(), context.getTableName()); + } + return provider; + }); + } + catch (MetaDataAccessException ex) { + throw new DataAccessResourceFailureException("Error retrieving database meta-data", ex); + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableParameterMetaData.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableParameterMetaData.java new file mode 100644 index 0000000..70a9a91 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableParameterMetaData.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.metadata; + +/** + * Holder of meta-data for a specific parameter that is used for table processing. + * + * @author Thomas Risberg + * @since 2.5 + * @see GenericTableMetaDataProvider + */ +public class TableParameterMetaData { + + private final String parameterName; + + private final int sqlType; + + private final boolean nullable; + + + /** + * Constructor taking all the properties. + */ + public TableParameterMetaData(String columnName, int sqlType, boolean nullable) { + this.parameterName = columnName; + this.sqlType = sqlType; + this.nullable = nullable; + } + + + /** + * Get the parameter name. + */ + public String getParameterName() { + return this.parameterName; + } + + /** + * Get the parameter SQL type. + */ + public int getSqlType() { + return this.sqlType; + } + + /** + * Get whether the parameter/column is nullable. + */ + public boolean isNullable() { + return this.nullable; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/package-info.java new file mode 100644 index 0000000..3627e64 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/package-info.java @@ -0,0 +1,10 @@ +/** + * Context metadata abstraction for the configuration and execution + * of table inserts and stored procedure calls. + */ +@NonNullApi +@NonNullFields +package org.springframework.jdbc.core.metadata; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/AbstractSqlParameterSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/AbstractSqlParameterSource.java new file mode 100644 index 0000000..5d044e4 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/AbstractSqlParameterSource.java @@ -0,0 +1,135 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.namedparam; + +import java.util.HashMap; +import java.util.Map; +import java.util.StringJoiner; + +import org.springframework.jdbc.core.SqlParameterValue; +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Abstract base class for {@link SqlParameterSource} implementations. + * Provides registration of SQL types per parameter and a friendly + * {@link #toString() toString} representation enumerating all parameters for + * a {@code SqlParameterSource} implementing {@link #getParameterNames()}. + * Concrete subclasses must implement {@link #hasValue} and {@link #getValue}. + * + * @author Juergen Hoeller + * @author Jens Schauder + * @since 2.0 + * @see #hasValue(String) + * @see #getValue(String) + * @see #getParameterNames() + */ +public abstract class AbstractSqlParameterSource implements SqlParameterSource { + + private final Map sqlTypes = new HashMap<>(); + + private final Map typeNames = new HashMap<>(); + + + /** + * Register an SQL type for the given parameter. + * @param paramName the name of the parameter + * @param sqlType the SQL type of the parameter + */ + public void registerSqlType(String paramName, int sqlType) { + Assert.notNull(paramName, "Parameter name must not be null"); + this.sqlTypes.put(paramName, sqlType); + } + + /** + * Register an SQL type for the given parameter. + * @param paramName the name of the parameter + * @param typeName the type name of the parameter + */ + public void registerTypeName(String paramName, String typeName) { + Assert.notNull(paramName, "Parameter name must not be null"); + this.typeNames.put(paramName, typeName); + } + + /** + * Return the SQL type for the given parameter, if registered. + * @param paramName the name of the parameter + * @return the SQL type of the parameter, + * or {@code TYPE_UNKNOWN} if not registered + */ + @Override + public int getSqlType(String paramName) { + Assert.notNull(paramName, "Parameter name must not be null"); + return this.sqlTypes.getOrDefault(paramName, TYPE_UNKNOWN); + } + + /** + * Return the type name for the given parameter, if registered. + * @param paramName the name of the parameter + * @return the type name of the parameter, + * or {@code null} if not registered + */ + @Override + @Nullable + public String getTypeName(String paramName) { + Assert.notNull(paramName, "Parameter name must not be null"); + return this.typeNames.get(paramName); + } + + + /** + * Enumerate the parameter names and values with their corresponding SQL type if available, + * or just return the simple {@code SqlParameterSource} implementation class name otherwise. + * @since 5.2 + * @see #getParameterNames() + */ + @Override + public String toString() { + String[] parameterNames = getParameterNames(); + if (parameterNames != null) { + StringJoiner result = new StringJoiner(", ", getClass().getSimpleName() + " {", "}"); + for (String parameterName : parameterNames) { + Object value = getValue(parameterName); + if (value instanceof SqlParameterValue) { + value = ((SqlParameterValue) value).getValue(); + } + String typeName = getTypeName(parameterName); + if (typeName == null) { + int sqlType = getSqlType(parameterName); + if (sqlType != TYPE_UNKNOWN) { + typeName = JdbcUtils.resolveTypeName(sqlType); + if (typeName == null) { + typeName = String.valueOf(sqlType); + } + } + } + StringBuilder entry = new StringBuilder(); + entry.append(parameterName).append('=').append(value); + if (typeName != null) { + entry.append(" (type:").append(typeName).append(')'); + } + result.add(entry); + } + return result.toString(); + } + else { + return getClass().getSimpleName(); + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/BeanPropertySqlParameterSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/BeanPropertySqlParameterSource.java new file mode 100644 index 0000000..83b8edb --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/BeanPropertySqlParameterSource.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.namedparam; + +import java.beans.PropertyDescriptor; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.NotReadablePropertyException; +import org.springframework.beans.PropertyAccessor; +import org.springframework.beans.PropertyAccessorFactory; +import org.springframework.jdbc.core.StatementCreatorUtils; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * {@link SqlParameterSource} implementation that obtains parameter values + * from bean properties of a given JavaBean object. The names of the bean + * properties have to match the parameter names. + * + *

    Uses a Spring BeanWrapper for bean property access underneath. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.0 + * @see NamedParameterJdbcTemplate + * @see org.springframework.beans.BeanWrapper + */ +public class BeanPropertySqlParameterSource extends AbstractSqlParameterSource { + + private final BeanWrapper beanWrapper; + + @Nullable + private String[] propertyNames; + + + /** + * Create a new BeanPropertySqlParameterSource for the given bean. + * @param object the bean instance to wrap + */ + public BeanPropertySqlParameterSource(Object object) { + this.beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(object); + } + + + @Override + public boolean hasValue(String paramName) { + return this.beanWrapper.isReadableProperty(paramName); + } + + @Override + @Nullable + public Object getValue(String paramName) throws IllegalArgumentException { + try { + return this.beanWrapper.getPropertyValue(paramName); + } + catch (NotReadablePropertyException ex) { + throw new IllegalArgumentException(ex.getMessage()); + } + } + + /** + * Derives a default SQL type from the corresponding property type. + * @see org.springframework.jdbc.core.StatementCreatorUtils#javaTypeToSqlParameterType + */ + @Override + public int getSqlType(String paramName) { + int sqlType = super.getSqlType(paramName); + if (sqlType != TYPE_UNKNOWN) { + return sqlType; + } + Class propType = this.beanWrapper.getPropertyType(paramName); + return StatementCreatorUtils.javaTypeToSqlParameterType(propType); + } + + @Override + @NonNull + public String[] getParameterNames() { + return getReadablePropertyNames(); + } + + /** + * Provide access to the property names of the wrapped bean. + * Uses support provided in the {@link PropertyAccessor} interface. + * @return an array containing all the known property names + */ + public String[] getReadablePropertyNames() { + if (this.propertyNames == null) { + List names = new ArrayList<>(); + PropertyDescriptor[] props = this.beanWrapper.getPropertyDescriptors(); + for (PropertyDescriptor pd : props) { + if (this.beanWrapper.isReadableProperty(pd.getName())) { + names.add(pd.getName()); + } + } + this.propertyNames = StringUtils.toStringArray(names); + } + return this.propertyNames; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/EmptySqlParameterSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/EmptySqlParameterSource.java new file mode 100644 index 0000000..f3034c9 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/EmptySqlParameterSource.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.namedparam; + +import org.springframework.lang.Nullable; + +/** + * A simple empty implementation of the {@link SqlParameterSource} interface. + * + * @author Juergen Hoeller + * @since 3.2.2 + */ +public class EmptySqlParameterSource implements SqlParameterSource { + + /** + * A shared instance of {@link EmptySqlParameterSource}. + */ + public static final EmptySqlParameterSource INSTANCE = new EmptySqlParameterSource(); + + + @Override + public boolean hasValue(String paramName) { + return false; + } + + @Override + @Nullable + public Object getValue(String paramName) throws IllegalArgumentException { + throw new IllegalArgumentException("This SqlParameterSource is empty"); + } + + @Override + public int getSqlType(String paramName) { + return TYPE_UNKNOWN; + } + + @Override + @Nullable + public String getTypeName(String paramName) { + return null; + } + + @Override + @Nullable + public String[] getParameterNames() { + return null; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/MapSqlParameterSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/MapSqlParameterSource.java new file mode 100644 index 0000000..7d37148 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/MapSqlParameterSource.java @@ -0,0 +1,174 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.namedparam; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.jdbc.core.SqlParameterValue; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * {@link SqlParameterSource} implementation that holds a given Map of parameters. + * + *

    This class is intended for passing in a simple Map of parameter values + * to the methods of the {@link NamedParameterJdbcTemplate} class. + * + *

    The {@code addValue} methods on this class will make adding several values + * easier. The methods return a reference to the {@link MapSqlParameterSource} + * itself, so you can chain several method calls together within a single statement. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.0 + * @see #addValue(String, Object) + * @see #addValue(String, Object, int) + * @see #registerSqlType + * @see NamedParameterJdbcTemplate + */ +public class MapSqlParameterSource extends AbstractSqlParameterSource { + + private final Map values = new LinkedHashMap<>(); + + + /** + * Create an empty MapSqlParameterSource, + * with values to be added via {@code addValue}. + * @see #addValue(String, Object) + */ + public MapSqlParameterSource() { + } + + /** + * Create a new MapSqlParameterSource, with one value + * comprised of the supplied arguments. + * @param paramName the name of the parameter + * @param value the value of the parameter + * @see #addValue(String, Object) + */ + public MapSqlParameterSource(String paramName, @Nullable Object value) { + addValue(paramName, value); + } + + /** + * Create a new MapSqlParameterSource based on a Map. + * @param values a Map holding existing parameter values (can be {@code null}) + */ + public MapSqlParameterSource(@Nullable Map values) { + addValues(values); + } + + + /** + * Add a parameter to this parameter source. + * @param paramName the name of the parameter + * @param value the value of the parameter + * @return a reference to this parameter source, + * so it's possible to chain several calls together + */ + public MapSqlParameterSource addValue(String paramName, @Nullable Object value) { + Assert.notNull(paramName, "Parameter name must not be null"); + this.values.put(paramName, value); + if (value instanceof SqlParameterValue) { + registerSqlType(paramName, ((SqlParameterValue) value).getSqlType()); + } + return this; + } + + /** + * Add a parameter to this parameter source. + * @param paramName the name of the parameter + * @param value the value of the parameter + * @param sqlType the SQL type of the parameter + * @return a reference to this parameter source, + * so it's possible to chain several calls together + */ + public MapSqlParameterSource addValue(String paramName, @Nullable Object value, int sqlType) { + Assert.notNull(paramName, "Parameter name must not be null"); + this.values.put(paramName, value); + registerSqlType(paramName, sqlType); + return this; + } + + /** + * Add a parameter to this parameter source. + * @param paramName the name of the parameter + * @param value the value of the parameter + * @param sqlType the SQL type of the parameter + * @param typeName the type name of the parameter + * @return a reference to this parameter source, + * so it's possible to chain several calls together + */ + public MapSqlParameterSource addValue(String paramName, @Nullable Object value, int sqlType, String typeName) { + Assert.notNull(paramName, "Parameter name must not be null"); + this.values.put(paramName, value); + registerSqlType(paramName, sqlType); + registerTypeName(paramName, typeName); + return this; + } + + /** + * Add a Map of parameters to this parameter source. + * @param values a Map holding existing parameter values (can be {@code null}) + * @return a reference to this parameter source, + * so it's possible to chain several calls together + */ + public MapSqlParameterSource addValues(@Nullable Map values) { + if (values != null) { + values.forEach((key, value) -> { + this.values.put(key, value); + if (value instanceof SqlParameterValue) { + registerSqlType(key, ((SqlParameterValue) value).getSqlType()); + } + }); + } + return this; + } + + /** + * Expose the current parameter values as read-only Map. + */ + public Map getValues() { + return Collections.unmodifiableMap(this.values); + } + + + @Override + public boolean hasValue(String paramName) { + return this.values.containsKey(paramName); + } + + @Override + @Nullable + public Object getValue(String paramName) { + if (!hasValue(paramName)) { + throw new IllegalArgumentException("No value registered for key '" + paramName + "'"); + } + return this.values.get(paramName); + } + + @Override + @NonNull + public String[] getParameterNames() { + return StringUtils.toStringArray(this.values.keySet()); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterBatchUpdateUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterBatchUpdateUtils.java new file mode 100644 index 0000000..8678bfb --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterBatchUpdateUtils.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.namedparam; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcOperations; + +/** + * Generic utility methods for working with JDBC batch statements using named parameters. + * Mainly for internal use within the framework. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 3.0 + * @deprecated as of 5.1.3, not used by {@link NamedParameterJdbcTemplate} anymore + */ +@Deprecated +public abstract class NamedParameterBatchUpdateUtils extends org.springframework.jdbc.core.BatchUpdateUtils { + + public static int[] executeBatchUpdateWithNamedParameters( + final ParsedSql parsedSql, final SqlParameterSource[] batchArgs, JdbcOperations jdbcOperations) { + + if (batchArgs.length == 0) { + return new int[0]; + } + + String sqlToUse = NamedParameterUtils.substituteNamedParameters(parsedSql, batchArgs[0]); + return jdbcOperations.batchUpdate( + sqlToUse, + new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + Object[] values = NamedParameterUtils.buildValueArray(parsedSql, batchArgs[i], null); + int[] columnTypes = NamedParameterUtils.buildSqlTypeArray(parsedSql, batchArgs[i]); + setStatementParameters(values, ps, columnTypes); + } + @Override + public int getBatchSize() { + return batchArgs.length; + } + }); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcDaoSupport.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcDaoSupport.java new file mode 100644 index 0000000..5de5917 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcDaoSupport.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.namedparam; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.support.JdbcDaoSupport; +import org.springframework.lang.Nullable; + +/** + * Extension of JdbcDaoSupport that exposes a NamedParameterJdbcTemplate as well. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.0 + * @see NamedParameterJdbcTemplate + */ +public class NamedParameterJdbcDaoSupport extends JdbcDaoSupport { + + @Nullable + private NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + + /** + * Create a NamedParameterJdbcTemplate based on the configured JdbcTemplate. + */ + @Override + protected void initTemplateConfig() { + JdbcTemplate jdbcTemplate = getJdbcTemplate(); + if (jdbcTemplate != null) { + this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate); + } + } + + /** + * Return a NamedParameterJdbcTemplate wrapping the configured JdbcTemplate. + */ + @Nullable + public NamedParameterJdbcTemplate getNamedParameterJdbcTemplate() { + return this.namedParameterJdbcTemplate; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcOperations.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcOperations.java new file mode 100644 index 0000000..5d22b53 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcOperations.java @@ -0,0 +1,551 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.namedparam; + +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.PreparedStatementCallback; +import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.jdbc.core.RowCallbackHandler; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.jdbc.support.rowset.SqlRowSet; +import org.springframework.lang.Nullable; + +/** + * Interface specifying a basic set of JDBC operations allowing the use + * of named parameters rather than the traditional '?' placeholders. + * + *

    This is an alternative to the classic + * {@link org.springframework.jdbc.core.JdbcOperations} interface, + * implemented by {@link NamedParameterJdbcTemplate}. This interface is not + * often used directly, but provides a useful option to enhance testability, + * as it can easily be mocked or stubbed. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.0 + * @see NamedParameterJdbcTemplate + * @see org.springframework.jdbc.core.JdbcOperations + */ +public interface NamedParameterJdbcOperations { + + /** + * Expose the classic Spring JdbcTemplate to allow invocation of + * classic JDBC operations. + */ + JdbcOperations getJdbcOperations(); + + + /** + * Execute a JDBC data access operation, implemented as callback action + * working on a JDBC PreparedStatement. This allows for implementing arbitrary + * data access operations on a single Statement, within Spring's managed + * JDBC environment: that is, participating in Spring-managed transactions + * and converting JDBC SQLExceptions into Spring's DataAccessException hierarchy. + *

    The callback action can return a result object, for example a + * domain object or a collection of domain objects. + * @param sql the SQL to execute + * @param paramSource container of arguments to bind to the query + * @param action callback object that specifies the action + * @return a result object returned by the action, or {@code null} + * @throws DataAccessException if there is any problem + */ + @Nullable + T execute(String sql, SqlParameterSource paramSource, PreparedStatementCallback action) + throws DataAccessException; + + /** + * Execute a JDBC data access operation, implemented as callback action + * working on a JDBC PreparedStatement. This allows for implementing arbitrary + * data access operations on a single Statement, within Spring's managed + * JDBC environment: that is, participating in Spring-managed transactions + * and converting JDBC SQLExceptions into Spring's DataAccessException hierarchy. + *

    The callback action can return a result object, for example a + * domain object or a collection of domain objects. + * @param sql the SQL to execute + * @param paramMap map of parameters to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type) + * @param action callback object that specifies the action + * @return a result object returned by the action, or {@code null} + * @throws DataAccessException if there is any problem + */ + @Nullable + T execute(String sql, Map paramMap, PreparedStatementCallback action) + throws DataAccessException; + + /** + * Execute a JDBC data access operation, implemented as callback action + * working on a JDBC PreparedStatement. This allows for implementing arbitrary + * data access operations on a single Statement, within Spring's managed + * JDBC environment: that is, participating in Spring-managed transactions + * and converting JDBC SQLExceptions into Spring's DataAccessException hierarchy. + *

    The callback action can return a result object, for example a + * domain object or a collection of domain objects. + * @param sql the SQL to execute + * @param action callback object that specifies the action + * @return a result object returned by the action, or {@code null} + * @throws DataAccessException if there is any problem + */ + @Nullable + T execute(String sql, PreparedStatementCallback action) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list + * of arguments to bind to the query, reading the ResultSet with a + * ResultSetExtractor. + * @param sql the SQL query to execute + * @param paramSource container of arguments to bind to the query + * @param rse object that will extract results + * @return an arbitrary result object, as returned by the ResultSetExtractor + * @throws DataAccessException if the query fails + */ + @Nullable + T query(String sql, SqlParameterSource paramSource, ResultSetExtractor rse) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list + * of arguments to bind to the query, reading the ResultSet with a + * ResultSetExtractor. + * @param sql the SQL query to execute + * @param paramMap map of parameters to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type) + * @param rse object that will extract results + * @return an arbitrary result object, as returned by the ResultSetExtractor + * @throws DataAccessException if the query fails + */ + @Nullable + T query(String sql, Map paramMap, ResultSetExtractor rse) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL, + * reading the ResultSet with a ResultSetExtractor. + *

    Note: In contrast to the JdbcOperations method with the same signature, + * this query variant always uses a PreparedStatement. It is effectively + * equivalent to a query call with an empty parameter Map. + * @param sql the SQL query to execute + * @param rse object that will extract results + * @return an arbitrary result object, as returned by the ResultSetExtractor + * @throws DataAccessException if the query fails + */ + @Nullable + T query(String sql, ResultSetExtractor rse) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list of + * arguments to bind to the query, reading the ResultSet on a per-row basis + * with a RowCallbackHandler. + * @param sql the SQL query to execute + * @param paramSource container of arguments to bind to the query + * @param rch object that will extract results, one row at a time + * @throws DataAccessException if the query fails + */ + void query(String sql, SqlParameterSource paramSource, RowCallbackHandler rch) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list of + * arguments to bind to the query, reading the ResultSet on a per-row basis + * with a RowCallbackHandler. + * @param sql the SQL query to execute + * @param paramMap map of parameters to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type) + * @param rch object that will extract results, one row at a time + * @throws DataAccessException if the query fails + */ + void query(String sql, Map paramMap, RowCallbackHandler rch) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL, + * reading the ResultSet on a per-row basis with a RowCallbackHandler. + *

    Note: In contrast to the JdbcOperations method with the same signature, + * this query variant always uses a PreparedStatement. It is effectively + * equivalent to a query call with an empty parameter Map. + * @param sql the SQL query to execute + * @param rch object that will extract results, one row at a time + * @throws DataAccessException if the query fails + */ + void query(String sql, RowCallbackHandler rch) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list + * of arguments to bind to the query, mapping each row to a Java object + * via a RowMapper. + * @param sql the SQL query to execute + * @param paramSource container of arguments to bind to the query + * @param rowMapper object that will map one object per row + * @return the result List, containing mapped objects + * @throws DataAccessException if the query fails + */ + List query(String sql, SqlParameterSource paramSource, RowMapper rowMapper) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list + * of arguments to bind to the query, mapping each row to a Java object + * via a RowMapper. + * @param sql the SQL query to execute + * @param paramMap map of parameters to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type) + * @param rowMapper object that will map one object per row + * @return the result List, containing mapped objects + * @throws DataAccessException if the query fails + */ + List query(String sql, Map paramMap, RowMapper rowMapper) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL, + * mapping each row to a Java object via a RowMapper. + *

    Note: In contrast to the JdbcOperations method with the same signature, + * this query variant always uses a PreparedStatement. It is effectively + * equivalent to a query call with an empty parameter Map. + * @param sql the SQL query to execute + * @param rowMapper object that will map one object per row + * @return the result List, containing mapped objects + * @throws DataAccessException if the query fails + */ + List query(String sql, RowMapper rowMapper) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list + * of arguments to bind to the query, mapping each row to a Java object + * via a RowMapper, and turning it into an iterable and closeable Stream. + * @param sql the SQL query to execute + * @param paramSource container of arguments to bind to the query + * @param rowMapper object that will map one object per row + * @return the result Stream, containing mapped objects, needing to be + * closed once fully processed (e.g. through a try-with-resources clause) + * @throws DataAccessException if the query fails + * @since 5.3 + */ + Stream queryForStream(String sql, SqlParameterSource paramSource, RowMapper rowMapper) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list + * of arguments to bind to the query, mapping each row to a Java object + * via a RowMapper, and turning it into an iterable and closeable Stream. + * @param sql the SQL query to execute + * @param paramMap map of parameters to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type) + * @param rowMapper object that will map one object per row + * @return the result Stream, containing mapped objects, needing to be + * closed once fully processed (e.g. through a try-with-resources clause) + * @throws DataAccessException if the query fails + * @since 5.3 + */ + Stream queryForStream(String sql, Map paramMap, RowMapper rowMapper) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list + * of arguments to bind to the query, mapping a single result row to a + * Java object via a RowMapper. + * @param sql the SQL query to execute + * @param paramSource container of arguments to bind to the query + * @param rowMapper object that will map one object per row + * @return the single mapped object (may be {@code null} if the given + * {@link RowMapper} returned {@code} null) + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException + * if the query does not return exactly one row, or does not return exactly + * one column in that row + * @throws DataAccessException if the query fails + */ + @Nullable + T queryForObject(String sql, SqlParameterSource paramSource, RowMapper rowMapper) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a list + * of arguments to bind to the query, mapping a single result row to a + * Java object via a RowMapper. + * @param sql the SQL query to execute + * @param paramMap map of parameters to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type) + * @param rowMapper object that will map one object per row + * @return the single mapped object (may be {@code null} if the given + * {@link RowMapper} returned {@code} null) + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException + * if the query does not return exactly one row, or does not return exactly + * one column in that row + * @throws DataAccessException if the query fails + */ + @Nullable + T queryForObject(String sql, Map paramMap, RowMapper rowMapper) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, expecting a result object. + *

    The query is expected to be a single row/single column query; the returned + * result will be directly mapped to the corresponding object type. + * @param sql the SQL query to execute + * @param paramSource container of arguments to bind to the query + * @param requiredType the type that the result object is expected to match + * @return the result object of the required type, or {@code null} in case of SQL NULL + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException + * if the query does not return exactly one row, or does not return exactly + * one column in that row + * @throws DataAccessException if the query fails + * @see org.springframework.jdbc.core.JdbcTemplate#queryForObject(String, Class) + */ + @Nullable + T queryForObject(String sql, SqlParameterSource paramSource, Class requiredType) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, expecting a result object. + *

    The query is expected to be a single row/single column query; the returned + * result will be directly mapped to the corresponding object type. + * @param sql the SQL query to execute + * @param paramMap map of parameters to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type) + * @param requiredType the type that the result object is expected to match + * @return the result object of the required type, or {@code null} in case of SQL NULL + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException + * if the query does not return exactly one row, or does not return exactly + * one column in that row + * @throws DataAccessException if the query fails + * @see org.springframework.jdbc.core.JdbcTemplate#queryForObject(String, Class) + */ + @Nullable + T queryForObject(String sql, Map paramMap, Class requiredType) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, expecting a result Map. + *

    The query is expected to be a single row query; the result row will be + * mapped to a Map (one entry for each column, using the column name as the key). + * @param sql the SQL query to execute + * @param paramSource container of arguments to bind to the query + * @return the result Map (one entry for each column, using the column name as the key) + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException + * if the query does not return exactly one row + * @throws DataAccessException if the query fails + * @see org.springframework.jdbc.core.JdbcTemplate#queryForMap(String) + * @see org.springframework.jdbc.core.ColumnMapRowMapper + */ + Map queryForMap(String sql, SqlParameterSource paramSource) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, expecting a result Map. + * The queryForMap() methods defined by this interface are appropriate + * when you don't have a domain model. Otherwise, consider using + * one of the queryForObject() methods. + *

    The query is expected to be a single row query; the result row will be + * mapped to a Map (one entry for each column, using the column name as the key). + * @param sql the SQL query to execute + * @param paramMap map of parameters to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type) + * @return the result Map (one entry for each column, using the column name as the key) + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException + * if the query does not return exactly one row + * @throws DataAccessException if the query fails + * @see org.springframework.jdbc.core.JdbcTemplate#queryForMap(String) + * @see org.springframework.jdbc.core.ColumnMapRowMapper + */ + Map queryForMap(String sql, Map paramMap) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, expecting a result list. + *

    The results will be mapped to a List (one entry for each row) of + * result objects, each of them matching the specified element type. + * @param sql the SQL query to execute + * @param paramSource container of arguments to bind to the query + * @param elementType the required type of element in the result list + * (for example, {@code Integer.class}) + * @return a List of objects that match the specified element type + * @throws DataAccessException if the query fails + * @see org.springframework.jdbc.core.JdbcTemplate#queryForList(String, Class) + * @see org.springframework.jdbc.core.SingleColumnRowMapper + */ + List queryForList(String sql, SqlParameterSource paramSource, Class elementType) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, expecting a result list. + *

    The results will be mapped to a List (one entry for each row) of + * result objects, each of them matching the specified element type. + * @param sql the SQL query to execute + * @param paramMap map of parameters to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type) + * @param elementType the required type of element in the result list + * (for example, {@code Integer.class}) + * @return a List of objects that match the specified element type + * @throws DataAccessException if the query fails + * @see org.springframework.jdbc.core.JdbcTemplate#queryForList(String, Class) + * @see org.springframework.jdbc.core.SingleColumnRowMapper + */ + List queryForList(String sql, Map paramMap, Class elementType) + throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, expecting a result list. + *

    The results will be mapped to a List (one entry for each row) of + * Maps (one entry for each column, using the column name as the key). + * Each element in the list will be of the form returned by this interface's + * {@code queryForMap} methods. + * @param sql the SQL query to execute + * @param paramSource container of arguments to bind to the query + * @return a List that contains a Map per row + * @throws DataAccessException if the query fails + * @see org.springframework.jdbc.core.JdbcTemplate#queryForList(String) + */ + List> queryForList(String sql, SqlParameterSource paramSource) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, expecting a result list. + *

    The results will be mapped to a List (one entry for each row) of + * Maps (one entry for each column, using the column name as the key). + * Each element in the list will be of the form returned by this interface's + * {@code queryForMap} methods. + * @param sql the SQL query to execute + * @param paramMap map of parameters to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type) + * @return a List that contains a Map per row + * @throws DataAccessException if the query fails + * @see org.springframework.jdbc.core.JdbcTemplate#queryForList(String) + */ + List> queryForList(String sql, Map paramMap) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, expecting an SqlRowSet. + *

    The results will be mapped to an SqlRowSet which holds the data in a + * disconnected fashion. This wrapper will translate any SQLExceptions thrown. + *

    Note that, for the default implementation, JDBC RowSet support needs to + * be available at runtime: by default, Sun's {@code com.sun.rowset.CachedRowSetImpl} + * class is used, which is part of JDK 1.5+ and also available separately as part of + * Sun's JDBC RowSet Implementations download (rowset.jar). + * @param sql the SQL query to execute + * @param paramSource container of arguments to bind to the query + * @return an SqlRowSet representation (possibly a wrapper around a + * {@code javax.sql.rowset.CachedRowSet}) + * @throws DataAccessException if there is any problem executing the query + * @see org.springframework.jdbc.core.JdbcTemplate#queryForRowSet(String) + * @see org.springframework.jdbc.core.SqlRowSetResultSetExtractor + * @see javax.sql.rowset.CachedRowSet + */ + SqlRowSet queryForRowSet(String sql, SqlParameterSource paramSource) throws DataAccessException; + + /** + * Query given SQL to create a prepared statement from SQL and a + * list of arguments to bind to the query, expecting an SqlRowSet. + *

    The results will be mapped to an SqlRowSet which holds the data in a + * disconnected fashion. This wrapper will translate any SQLExceptions thrown. + *

    Note that, for the default implementation, JDBC RowSet support needs to + * be available at runtime: by default, Sun's {@code com.sun.rowset.CachedRowSetImpl} + * class is used, which is part of JDK 1.5+ and also available separately as part of + * Sun's JDBC RowSet Implementations download (rowset.jar). + * @param sql the SQL query to execute + * @param paramMap map of parameters to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type) + * @return an SqlRowSet representation (possibly a wrapper around a + * {@code javax.sql.rowset.CachedRowSet}) + * @throws DataAccessException if there is any problem executing the query + * @see org.springframework.jdbc.core.JdbcTemplate#queryForRowSet(String) + * @see org.springframework.jdbc.core.SqlRowSetResultSetExtractor + * @see javax.sql.rowset.CachedRowSet + */ + SqlRowSet queryForRowSet(String sql, Map paramMap) throws DataAccessException; + + /** + * Issue an update via a prepared statement, binding the given arguments. + * @param sql the SQL containing named parameters + * @param paramSource container of arguments and SQL types to bind to the query + * @return the number of rows affected + * @throws DataAccessException if there is any problem issuing the update + */ + int update(String sql, SqlParameterSource paramSource) throws DataAccessException; + + /** + * Issue an update via a prepared statement, binding the given arguments. + * @param sql the SQL containing named parameters + * @param paramMap map of parameters to bind to the query + * (leaving it to the PreparedStatement to guess the corresponding SQL type) + * @return the number of rows affected + * @throws DataAccessException if there is any problem issuing the update + */ + int update(String sql, Map paramMap) throws DataAccessException; + + /** + * Issue an update via a prepared statement, binding the given arguments, + * returning generated keys. + * @param sql the SQL containing named parameters + * @param paramSource container of arguments and SQL types to bind to the query + * @param generatedKeyHolder a {@link KeyHolder} that will hold the generated keys + * @return the number of rows affected + * @throws DataAccessException if there is any problem issuing the update + * @see MapSqlParameterSource + * @see org.springframework.jdbc.support.GeneratedKeyHolder + */ + int update(String sql, SqlParameterSource paramSource, KeyHolder generatedKeyHolder) + throws DataAccessException; + + /** + * Issue an update via a prepared statement, binding the given arguments, + * returning generated keys. + * @param sql the SQL containing named parameters + * @param paramSource container of arguments and SQL types to bind to the query + * @param generatedKeyHolder a {@link KeyHolder} that will hold the generated keys + * @param keyColumnNames names of the columns that will have keys generated for them + * @return the number of rows affected + * @throws DataAccessException if there is any problem issuing the update + * @see MapSqlParameterSource + * @see org.springframework.jdbc.support.GeneratedKeyHolder + */ + int update(String sql, SqlParameterSource paramSource, KeyHolder generatedKeyHolder, String[] keyColumnNames) + throws DataAccessException; + + /** + * Executes a batch using the supplied SQL statement with the batch of supplied arguments. + * @param sql the SQL statement to execute + * @param batchValues the array of Maps containing the batch of arguments for the query + * @return an array containing the numbers of rows affected by each update in the batch + * (may also contain special JDBC-defined negative values for affected rows such as + * {@link java.sql.Statement#SUCCESS_NO_INFO}/{@link java.sql.Statement#EXECUTE_FAILED}) + * @throws DataAccessException if there is any problem issuing the update + */ + int[] batchUpdate(String sql, Map[] batchValues); + + /** + * Execute a batch using the supplied SQL statement with the batch of supplied arguments. + * @param sql the SQL statement to execute + * @param batchArgs the array of {@link SqlParameterSource} containing the batch of + * arguments for the query + * @return an array containing the numbers of rows affected by each update in the batch + * (may also contain special JDBC-defined negative values for affected rows such as + * {@link java.sql.Statement#SUCCESS_NO_INFO}/{@link java.sql.Statement#EXECUTE_FAILED}) + * @throws DataAccessException if there is any problem issuing the update + */ + int[] batchUpdate(String sql, SqlParameterSource[] batchArgs); + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java new file mode 100644 index 0000000..ef7b656 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java @@ -0,0 +1,456 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.namedparam; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import javax.sql.DataSource; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.support.DataAccessUtils; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.ColumnMapRowMapper; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.PreparedStatementCallback; +import org.springframework.jdbc.core.PreparedStatementCreator; +import org.springframework.jdbc.core.PreparedStatementCreatorFactory; +import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.jdbc.core.RowCallbackHandler; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.SingleColumnRowMapper; +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.jdbc.core.SqlRowSetResultSetExtractor; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.jdbc.support.rowset.SqlRowSet; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ConcurrentLruCache; + +/** + * Template class with a basic set of JDBC operations, allowing the use + * of named parameters rather than traditional '?' placeholders. + * + *

    This class delegates to a wrapped {@link #getJdbcOperations() JdbcTemplate} + * once the substitution from named parameters to JDBC style '?' placeholders is + * done at execution time. It also allows for expanding a {@link java.util.List} + * of values to the appropriate number of placeholders. + * + *

    The underlying {@link org.springframework.jdbc.core.JdbcTemplate} is + * exposed to allow for convenient access to the traditional + * {@link org.springframework.jdbc.core.JdbcTemplate} methods. + * + *

    NOTE: An instance of this class is thread-safe once configured. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.0 + * @see NamedParameterJdbcOperations + * @see org.springframework.jdbc.core.JdbcTemplate + */ +public class NamedParameterJdbcTemplate implements NamedParameterJdbcOperations { + + /** Default maximum number of entries for this template's SQL cache: 256. */ + public static final int DEFAULT_CACHE_LIMIT = 256; + + + /** The JdbcTemplate we are wrapping. */ + private final JdbcOperations classicJdbcTemplate; + + /** Cache of original SQL String to ParsedSql representation. */ + private volatile ConcurrentLruCache parsedSqlCache = + new ConcurrentLruCache<>(DEFAULT_CACHE_LIMIT, NamedParameterUtils::parseSqlStatement); + + + /** + * Create a new NamedParameterJdbcTemplate for the given {@link DataSource}. + *

    Creates a classic Spring {@link org.springframework.jdbc.core.JdbcTemplate} and wraps it. + * @param dataSource the JDBC DataSource to access + */ + public NamedParameterJdbcTemplate(DataSource dataSource) { + Assert.notNull(dataSource, "DataSource must not be null"); + this.classicJdbcTemplate = new JdbcTemplate(dataSource); + } + + /** + * Create a new NamedParameterJdbcTemplate for the given classic + * Spring {@link org.springframework.jdbc.core.JdbcTemplate}. + * @param classicJdbcTemplate the classic Spring JdbcTemplate to wrap + */ + public NamedParameterJdbcTemplate(JdbcOperations classicJdbcTemplate) { + Assert.notNull(classicJdbcTemplate, "JdbcTemplate must not be null"); + this.classicJdbcTemplate = classicJdbcTemplate; + } + + + /** + * Expose the classic Spring JdbcTemplate operations to allow invocation + * of less commonly used methods. + */ + @Override + public JdbcOperations getJdbcOperations() { + return this.classicJdbcTemplate; + } + + /** + * Expose the classic Spring {@link JdbcTemplate} itself, if available, + * in particular for passing it on to other {@code JdbcTemplate} consumers. + *

    If sufficient for the purposes at hand, {@link #getJdbcOperations()} + * is recommended over this variant. + * @since 5.0.3 + */ + public JdbcTemplate getJdbcTemplate() { + Assert.state(this.classicJdbcTemplate instanceof JdbcTemplate, "No JdbcTemplate available"); + return (JdbcTemplate) this.classicJdbcTemplate; + } + + /** + * Specify the maximum number of entries for this template's SQL cache. + * Default is 256. 0 indicates no caching, always parsing each statement. + */ + public void setCacheLimit(int cacheLimit) { + this.parsedSqlCache = new ConcurrentLruCache<>(cacheLimit, NamedParameterUtils::parseSqlStatement); + } + + /** + * Return the maximum number of entries for this template's SQL cache. + */ + public int getCacheLimit() { + return this.parsedSqlCache.sizeLimit(); + } + + + @Override + @Nullable + public T execute(String sql, SqlParameterSource paramSource, PreparedStatementCallback action) + throws DataAccessException { + + return getJdbcOperations().execute(getPreparedStatementCreator(sql, paramSource), action); + } + + @Override + @Nullable + public T execute(String sql, Map paramMap, PreparedStatementCallback action) + throws DataAccessException { + + return execute(sql, new MapSqlParameterSource(paramMap), action); + } + + @Override + @Nullable + public T execute(String sql, PreparedStatementCallback action) throws DataAccessException { + return execute(sql, EmptySqlParameterSource.INSTANCE, action); + } + + @Override + @Nullable + public T query(String sql, SqlParameterSource paramSource, ResultSetExtractor rse) + throws DataAccessException { + + return getJdbcOperations().query(getPreparedStatementCreator(sql, paramSource), rse); + } + + @Override + @Nullable + public T query(String sql, Map paramMap, ResultSetExtractor rse) + throws DataAccessException { + + return query(sql, new MapSqlParameterSource(paramMap), rse); + } + + @Override + @Nullable + public T query(String sql, ResultSetExtractor rse) throws DataAccessException { + return query(sql, EmptySqlParameterSource.INSTANCE, rse); + } + + @Override + public void query(String sql, SqlParameterSource paramSource, RowCallbackHandler rch) + throws DataAccessException { + + getJdbcOperations().query(getPreparedStatementCreator(sql, paramSource), rch); + } + + @Override + public void query(String sql, Map paramMap, RowCallbackHandler rch) + throws DataAccessException { + + query(sql, new MapSqlParameterSource(paramMap), rch); + } + + @Override + public void query(String sql, RowCallbackHandler rch) throws DataAccessException { + query(sql, EmptySqlParameterSource.INSTANCE, rch); + } + + @Override + public List query(String sql, SqlParameterSource paramSource, RowMapper rowMapper) + throws DataAccessException { + + return getJdbcOperations().query(getPreparedStatementCreator(sql, paramSource), rowMapper); + } + + @Override + public List query(String sql, Map paramMap, RowMapper rowMapper) + throws DataAccessException { + + return query(sql, new MapSqlParameterSource(paramMap), rowMapper); + } + + @Override + public List query(String sql, RowMapper rowMapper) throws DataAccessException { + return query(sql, EmptySqlParameterSource.INSTANCE, rowMapper); + } + + @Override + public Stream queryForStream(String sql, SqlParameterSource paramSource, RowMapper rowMapper) + throws DataAccessException { + + return getJdbcOperations().queryForStream(getPreparedStatementCreator(sql, paramSource), rowMapper); + } + + @Override + public Stream queryForStream(String sql, Map paramMap, RowMapper rowMapper) + throws DataAccessException { + + return queryForStream(sql, new MapSqlParameterSource(paramMap), rowMapper); + } + + @Override + @Nullable + public T queryForObject(String sql, SqlParameterSource paramSource, RowMapper rowMapper) + throws DataAccessException { + + List results = getJdbcOperations().query(getPreparedStatementCreator(sql, paramSource), rowMapper); + return DataAccessUtils.nullableSingleResult(results); + } + + @Override + @Nullable + public T queryForObject(String sql, Map paramMap, RowMapperrowMapper) + throws DataAccessException { + + return queryForObject(sql, new MapSqlParameterSource(paramMap), rowMapper); + } + + @Override + @Nullable + public T queryForObject(String sql, SqlParameterSource paramSource, Class requiredType) + throws DataAccessException { + + return queryForObject(sql, paramSource, new SingleColumnRowMapper<>(requiredType)); + } + + @Override + @Nullable + public T queryForObject(String sql, Map paramMap, Class requiredType) + throws DataAccessException { + + return queryForObject(sql, paramMap, new SingleColumnRowMapper<>(requiredType)); + } + + @Override + public Map queryForMap(String sql, SqlParameterSource paramSource) throws DataAccessException { + Map result = queryForObject(sql, paramSource, new ColumnMapRowMapper()); + Assert.state(result != null, "No result map"); + return result; + } + + @Override + public Map queryForMap(String sql, Map paramMap) throws DataAccessException { + Map result = queryForObject(sql, paramMap, new ColumnMapRowMapper()); + Assert.state(result != null, "No result map"); + return result; + } + + @Override + public List queryForList(String sql, SqlParameterSource paramSource, Class elementType) + throws DataAccessException { + + return query(sql, paramSource, new SingleColumnRowMapper<>(elementType)); + } + + @Override + public List queryForList(String sql, Map paramMap, Class elementType) + throws DataAccessException { + + return queryForList(sql, new MapSqlParameterSource(paramMap), elementType); + } + + @Override + public List> queryForList(String sql, SqlParameterSource paramSource) + throws DataAccessException { + + return query(sql, paramSource, new ColumnMapRowMapper()); + } + + @Override + public List> queryForList(String sql, Map paramMap) + throws DataAccessException { + + return queryForList(sql, new MapSqlParameterSource(paramMap)); + } + + @Override + public SqlRowSet queryForRowSet(String sql, SqlParameterSource paramSource) throws DataAccessException { + SqlRowSet result = getJdbcOperations().query( + getPreparedStatementCreator(sql, paramSource), new SqlRowSetResultSetExtractor()); + Assert.state(result != null, "No result"); + return result; + } + + @Override + public SqlRowSet queryForRowSet(String sql, Map paramMap) throws DataAccessException { + return queryForRowSet(sql, new MapSqlParameterSource(paramMap)); + } + + @Override + public int update(String sql, SqlParameterSource paramSource) throws DataAccessException { + return getJdbcOperations().update(getPreparedStatementCreator(sql, paramSource)); + } + + @Override + public int update(String sql, Map paramMap) throws DataAccessException { + return update(sql, new MapSqlParameterSource(paramMap)); + } + + @Override + public int update(String sql, SqlParameterSource paramSource, KeyHolder generatedKeyHolder) + throws DataAccessException { + + return update(sql, paramSource, generatedKeyHolder, null); + } + + @Override + public int update( + String sql, SqlParameterSource paramSource, KeyHolder generatedKeyHolder, @Nullable String[] keyColumnNames) + throws DataAccessException { + + PreparedStatementCreator psc = getPreparedStatementCreator(sql, paramSource, pscf -> { + if (keyColumnNames != null) { + pscf.setGeneratedKeysColumnNames(keyColumnNames); + } + else { + pscf.setReturnGeneratedKeys(true); + } + }); + return getJdbcOperations().update(psc, generatedKeyHolder); + } + + @Override + public int[] batchUpdate(String sql, Map[] batchValues) { + return batchUpdate(sql, SqlParameterSourceUtils.createBatch(batchValues)); + } + + @Override + public int[] batchUpdate(String sql, SqlParameterSource[] batchArgs) { + if (batchArgs.length == 0) { + return new int[0]; + } + + ParsedSql parsedSql = getParsedSql(sql); + PreparedStatementCreatorFactory pscf = getPreparedStatementCreatorFactory(parsedSql, batchArgs[0]); + + return getJdbcOperations().batchUpdate( + pscf.getSql(), + new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + Object[] values = NamedParameterUtils.buildValueArray(parsedSql, batchArgs[i], null); + pscf.newPreparedStatementSetter(values).setValues(ps); + } + @Override + public int getBatchSize() { + return batchArgs.length; + } + }); + } + + + /** + * Build a {@link PreparedStatementCreator} based on the given SQL and named parameters. + *

    Note: Directly called from all {@code query} variants. Delegates to the common + * {@link #getPreparedStatementCreator(String, SqlParameterSource, Consumer)} method. + * @param sql the SQL statement to execute + * @param paramSource container of arguments to bind + * @return the corresponding {@link PreparedStatementCreator} + * @see #getPreparedStatementCreator(String, SqlParameterSource, Consumer) + */ + protected PreparedStatementCreator getPreparedStatementCreator(String sql, SqlParameterSource paramSource) { + return getPreparedStatementCreator(sql, paramSource, null); + } + + /** + * Build a {@link PreparedStatementCreator} based on the given SQL and named parameters. + *

    Note: Used for the {@code update} variant with generated key handling, and also + * delegated from {@link #getPreparedStatementCreator(String, SqlParameterSource)}. + * @param sql the SQL statement to execute + * @param paramSource container of arguments to bind + * @param customizer callback for setting further properties on the + * {@link PreparedStatementCreatorFactory} in use), applied before the + * actual {@code newPreparedStatementCreator} call + * @return the corresponding {@link PreparedStatementCreator} + * @since 5.0.5 + * @see #getParsedSql(String) + * @see PreparedStatementCreatorFactory#PreparedStatementCreatorFactory(String, List) + * @see PreparedStatementCreatorFactory#newPreparedStatementCreator(Object[]) + */ + protected PreparedStatementCreator getPreparedStatementCreator(String sql, SqlParameterSource paramSource, + @Nullable Consumer customizer) { + + ParsedSql parsedSql = getParsedSql(sql); + PreparedStatementCreatorFactory pscf = getPreparedStatementCreatorFactory(parsedSql, paramSource); + if (customizer != null) { + customizer.accept(pscf); + } + Object[] params = NamedParameterUtils.buildValueArray(parsedSql, paramSource, null); + return pscf.newPreparedStatementCreator(params); + } + + /** + * Obtain a parsed representation of the given SQL statement. + *

    The default implementation uses an LRU cache with an upper limit of 256 entries. + * @param sql the original SQL statement + * @return a representation of the parsed SQL statement + */ + protected ParsedSql getParsedSql(String sql) { + return this.parsedSqlCache.get(sql); + } + + /** + * Build a {@link PreparedStatementCreatorFactory} based on the given SQL and named parameters. + * @param parsedSql parsed representation of the given SQL statement + * @param paramSource container of arguments to bind + * @return the corresponding {@link PreparedStatementCreatorFactory} + * @since 5.1.3 + * @see #getPreparedStatementCreator(String, SqlParameterSource, Consumer) + * @see #getParsedSql(String) + */ + protected PreparedStatementCreatorFactory getPreparedStatementCreatorFactory( + ParsedSql parsedSql, SqlParameterSource paramSource) { + + String sqlToUse = NamedParameterUtils.substituteNamedParameters(parsedSql, paramSource); + List declaredParameters = NamedParameterUtils.buildSqlParameterList(parsedSql, paramSource); + return new PreparedStatementCreatorFactory(sqlToUse, declaredParameters); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java new file mode 100644 index 0000000..4d4c414 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java @@ -0,0 +1,509 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.namedparam; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.jdbc.core.SqlParameterValue; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Helper methods for named parameter parsing. + * + *

    Only intended for internal use within Spring's JDBC framework. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.0 + */ +public abstract class NamedParameterUtils { + + /** + * Set of characters that qualify as comment or quotes starting characters. + */ + private static final String[] START_SKIP = new String[] {"'", "\"", "--", "/*"}; + + /** + * Set of characters that at are the corresponding comment or quotes ending characters. + */ + private static final String[] STOP_SKIP = new String[] {"'", "\"", "\n", "*/"}; + + /** + * Set of characters that qualify as parameter separators, + * indicating that a parameter name in an SQL String has ended. + */ + private static final String PARAMETER_SEPARATORS = "\"':&,;()|=+-*%/\\<>^"; + + /** + * An index with separator flags per character code. + * Technically only needed between 34 and 124 at this point. + */ + private static final boolean[] separatorIndex = new boolean[128]; + + static { + for (char c : PARAMETER_SEPARATORS.toCharArray()) { + separatorIndex[c] = true; + } + } + + + //------------------------------------------------------------------------- + // Core methods used by NamedParameterJdbcTemplate and SqlQuery/SqlUpdate + //------------------------------------------------------------------------- + + /** + * Parse the SQL statement and locate any placeholders or named parameters. + * Named parameters are substituted for a JDBC placeholder. + * @param sql the SQL statement + * @return the parsed statement, represented as ParsedSql instance + */ + public static ParsedSql parseSqlStatement(final String sql) { + Assert.notNull(sql, "SQL must not be null"); + + Set namedParameters = new HashSet<>(); + StringBuilder sqlToUse = new StringBuilder(sql); + List parameterList = new ArrayList<>(); + + char[] statement = sql.toCharArray(); + int namedParameterCount = 0; + int unnamedParameterCount = 0; + int totalParameterCount = 0; + + int escapes = 0; + int i = 0; + while (i < statement.length) { + int skipToPosition = i; + while (i < statement.length) { + skipToPosition = skipCommentsAndQuotes(statement, i); + if (i == skipToPosition) { + break; + } + else { + i = skipToPosition; + } + } + if (i >= statement.length) { + break; + } + char c = statement[i]; + if (c == ':' || c == '&') { + int j = i + 1; + if (c == ':' && j < statement.length && statement[j] == ':') { + // Postgres-style "::" casting operator should be skipped + i = i + 2; + continue; + } + String parameter = null; + if (c == ':' && j < statement.length && statement[j] == '{') { + // :{x} style parameter + while (statement[j] != '}') { + j++; + if (j >= statement.length) { + throw new InvalidDataAccessApiUsageException("Non-terminated named parameter declaration " + + "at position " + i + " in statement: " + sql); + } + if (statement[j] == ':' || statement[j] == '{') { + throw new InvalidDataAccessApiUsageException("Parameter name contains invalid character '" + + statement[j] + "' at position " + i + " in statement: " + sql); + } + } + if (j - i > 2) { + parameter = sql.substring(i + 2, j); + namedParameterCount = addNewNamedParameter(namedParameters, namedParameterCount, parameter); + totalParameterCount = addNamedParameter( + parameterList, totalParameterCount, escapes, i, j + 1, parameter); + } + j++; + } + else { + while (j < statement.length && !isParameterSeparator(statement[j])) { + j++; + } + if (j - i > 1) { + parameter = sql.substring(i + 1, j); + namedParameterCount = addNewNamedParameter(namedParameters, namedParameterCount, parameter); + totalParameterCount = addNamedParameter( + parameterList, totalParameterCount, escapes, i, j, parameter); + } + } + i = j - 1; + } + else { + if (c == '\\') { + int j = i + 1; + if (j < statement.length && statement[j] == ':') { + // escaped ":" should be skipped + sqlToUse.deleteCharAt(i - escapes); + escapes++; + i = i + 2; + continue; + } + } + if (c == '?') { + int j = i + 1; + if (j < statement.length && (statement[j] == '?' || statement[j] == '|' || statement[j] == '&')) { + // Postgres-style "??", "?|", "?&" operator should be skipped + i = i + 2; + continue; + } + unnamedParameterCount++; + totalParameterCount++; + } + } + i++; + } + ParsedSql parsedSql = new ParsedSql(sqlToUse.toString()); + for (ParameterHolder ph : parameterList) { + parsedSql.addNamedParameter(ph.getParameterName(), ph.getStartIndex(), ph.getEndIndex()); + } + parsedSql.setNamedParameterCount(namedParameterCount); + parsedSql.setUnnamedParameterCount(unnamedParameterCount); + parsedSql.setTotalParameterCount(totalParameterCount); + return parsedSql; + } + + private static int addNamedParameter( + List parameterList, int totalParameterCount, int escapes, int i, int j, String parameter) { + + parameterList.add(new ParameterHolder(parameter, i - escapes, j - escapes)); + totalParameterCount++; + return totalParameterCount; + } + + private static int addNewNamedParameter(Set namedParameters, int namedParameterCount, String parameter) { + if (!namedParameters.contains(parameter)) { + namedParameters.add(parameter); + namedParameterCount++; + } + return namedParameterCount; + } + + /** + * Skip over comments and quoted names present in an SQL statement. + * @param statement character array containing SQL statement + * @param position current position of statement + * @return next position to process after any comments or quotes are skipped + */ + private static int skipCommentsAndQuotes(char[] statement, int position) { + for (int i = 0; i < START_SKIP.length; i++) { + if (statement[position] == START_SKIP[i].charAt(0)) { + boolean match = true; + for (int j = 1; j < START_SKIP[i].length(); j++) { + if (statement[position + j] != START_SKIP[i].charAt(j)) { + match = false; + break; + } + } + if (match) { + int offset = START_SKIP[i].length(); + for (int m = position + offset; m < statement.length; m++) { + if (statement[m] == STOP_SKIP[i].charAt(0)) { + boolean endMatch = true; + int endPos = m; + for (int n = 1; n < STOP_SKIP[i].length(); n++) { + if (m + n >= statement.length) { + // last comment not closed properly + return statement.length; + } + if (statement[m + n] != STOP_SKIP[i].charAt(n)) { + endMatch = false; + break; + } + endPos = m + n; + } + if (endMatch) { + // found character sequence ending comment or quote + return endPos + 1; + } + } + } + // character sequence ending comment or quote not found + return statement.length; + } + } + } + return position; + } + + /** + * Parse the SQL statement and locate any placeholders or named parameters. Named + * parameters are substituted for a JDBC placeholder, and any select list is expanded + * to the required number of placeholders. Select lists may contain an array of + * objects, and in that case the placeholders will be grouped and enclosed with + * parentheses. This allows for the use of "expression lists" in the SQL statement + * like:

    + * {@code select id, name, state from table where (name, age) in (('John', 35), ('Ann', 50))} + *

    The parameter values passed in are used to determine the number of placeholders to + * be used for a select list. Select lists should be limited to 100 or fewer elements. + * A larger number of elements is not guaranteed to be supported by the database and + * is strictly vendor-dependent. + * @param parsedSql the parsed representation of the SQL statement + * @param paramSource the source for named parameters + * @return the SQL statement with substituted parameters + * @see #parseSqlStatement + */ + public static String substituteNamedParameters(ParsedSql parsedSql, @Nullable SqlParameterSource paramSource) { + String originalSql = parsedSql.getOriginalSql(); + List paramNames = parsedSql.getParameterNames(); + if (paramNames.isEmpty()) { + return originalSql; + } + StringBuilder actualSql = new StringBuilder(originalSql.length()); + int lastIndex = 0; + for (int i = 0; i < paramNames.size(); i++) { + String paramName = paramNames.get(i); + int[] indexes = parsedSql.getParameterIndexes(i); + int startIndex = indexes[0]; + int endIndex = indexes[1]; + actualSql.append(originalSql, lastIndex, startIndex); + if (paramSource != null && paramSource.hasValue(paramName)) { + Object value = paramSource.getValue(paramName); + if (value instanceof SqlParameterValue) { + value = ((SqlParameterValue) value).getValue(); + } + if (value instanceof Iterable) { + Iterator entryIter = ((Iterable) value).iterator(); + int k = 0; + while (entryIter.hasNext()) { + if (k > 0) { + actualSql.append(", "); + } + k++; + Object entryItem = entryIter.next(); + if (entryItem instanceof Object[]) { + Object[] expressionList = (Object[]) entryItem; + actualSql.append('('); + for (int m = 0; m < expressionList.length; m++) { + if (m > 0) { + actualSql.append(", "); + } + actualSql.append('?'); + } + actualSql.append(')'); + } + else { + actualSql.append('?'); + } + } + } + else { + actualSql.append('?'); + } + } + else { + actualSql.append('?'); + } + lastIndex = endIndex; + } + actualSql.append(originalSql, lastIndex, originalSql.length()); + return actualSql.toString(); + } + + /** + * Convert a Map of named parameter values to a corresponding array. + * @param parsedSql the parsed SQL statement + * @param paramSource the source for named parameters + * @param declaredParams the List of declared SqlParameter objects + * (may be {@code null}). If specified, the parameter metadata will + * be built into the value array in the form of SqlParameterValue objects. + * @return the array of values + */ + public static Object[] buildValueArray( + ParsedSql parsedSql, SqlParameterSource paramSource, @Nullable List declaredParams) { + + Object[] paramArray = new Object[parsedSql.getTotalParameterCount()]; + if (parsedSql.getNamedParameterCount() > 0 && parsedSql.getUnnamedParameterCount() > 0) { + throw new InvalidDataAccessApiUsageException( + "Not allowed to mix named and traditional ? placeholders. You have " + + parsedSql.getNamedParameterCount() + " named parameter(s) and " + + parsedSql.getUnnamedParameterCount() + " traditional placeholder(s) in statement: " + + parsedSql.getOriginalSql()); + } + List paramNames = parsedSql.getParameterNames(); + for (int i = 0; i < paramNames.size(); i++) { + String paramName = paramNames.get(i); + try { + SqlParameter param = findParameter(declaredParams, paramName, i); + paramArray[i] = (param != null ? new SqlParameterValue(param, paramSource.getValue(paramName)) : + SqlParameterSourceUtils.getTypedValue(paramSource, paramName)); + } + catch (IllegalArgumentException ex) { + throw new InvalidDataAccessApiUsageException( + "No value supplied for the SQL parameter '" + paramName + "': " + ex.getMessage()); + } + } + return paramArray; + } + + /** + * Find a matching parameter in the given list of declared parameters. + * @param declaredParams the declared SqlParameter objects + * @param paramName the name of the desired parameter + * @param paramIndex the index of the desired parameter + * @return the declared SqlParameter, or {@code null} if none found + */ + @Nullable + private static SqlParameter findParameter( + @Nullable List declaredParams, String paramName, int paramIndex) { + + if (declaredParams != null) { + // First pass: Look for named parameter match. + for (SqlParameter declaredParam : declaredParams) { + if (paramName.equals(declaredParam.getName())) { + return declaredParam; + } + } + // Second pass: Look for parameter index match. + if (paramIndex < declaredParams.size()) { + SqlParameter declaredParam = declaredParams.get(paramIndex); + // Only accept unnamed parameters for index matches. + if (declaredParam.getName() == null) { + return declaredParam; + } + } + } + return null; + } + + /** + * Determine whether a parameter name ends at the current position, + * that is, whether the given character qualifies as a separator. + */ + private static boolean isParameterSeparator(char c) { + return (c < 128 && separatorIndex[c]) || Character.isWhitespace(c); + } + + /** + * Convert parameter types from an SqlParameterSource into a corresponding int array. + * This is necessary in order to reuse existing methods on JdbcTemplate. + * Any named parameter types are placed in the correct position in the + * Object array based on the parsed SQL statement info. + * @param parsedSql the parsed SQL statement + * @param paramSource the source for named parameters + */ + public static int[] buildSqlTypeArray(ParsedSql parsedSql, SqlParameterSource paramSource) { + int[] sqlTypes = new int[parsedSql.getTotalParameterCount()]; + List paramNames = parsedSql.getParameterNames(); + for (int i = 0; i < paramNames.size(); i++) { + String paramName = paramNames.get(i); + sqlTypes[i] = paramSource.getSqlType(paramName); + } + return sqlTypes; + } + + /** + * Convert parameter declarations from an SqlParameterSource to a corresponding List of SqlParameters. + * This is necessary in order to reuse existing methods on JdbcTemplate. + * The SqlParameter for a named parameter is placed in the correct position in the + * resulting list based on the parsed SQL statement info. + * @param parsedSql the parsed SQL statement + * @param paramSource the source for named parameters + */ + public static List buildSqlParameterList(ParsedSql parsedSql, SqlParameterSource paramSource) { + List paramNames = parsedSql.getParameterNames(); + List params = new ArrayList<>(paramNames.size()); + for (String paramName : paramNames) { + params.add(new SqlParameter( + paramName, paramSource.getSqlType(paramName), paramSource.getTypeName(paramName))); + } + return params; + } + + + //------------------------------------------------------------------------- + // Convenience methods operating on a plain SQL String + //------------------------------------------------------------------------- + + /** + * Parse the SQL statement and locate any placeholders or named parameters. + * Named parameters are substituted for a JDBC placeholder. + *

    This is a shortcut version of + * {@link #parseSqlStatement(String)} in combination with + * {@link #substituteNamedParameters(ParsedSql, SqlParameterSource)}. + * @param sql the SQL statement + * @return the actual (parsed) SQL statement + */ + public static String parseSqlStatementIntoString(String sql) { + ParsedSql parsedSql = parseSqlStatement(sql); + return substituteNamedParameters(parsedSql, null); + } + + /** + * Parse the SQL statement and locate any placeholders or named parameters. + * Named parameters are substituted for a JDBC placeholder and any select list + * is expanded to the required number of placeholders. + *

    This is a shortcut version of + * {@link #substituteNamedParameters(ParsedSql, SqlParameterSource)}. + * @param sql the SQL statement + * @param paramSource the source for named parameters + * @return the SQL statement with substituted parameters + */ + public static String substituteNamedParameters(String sql, SqlParameterSource paramSource) { + ParsedSql parsedSql = parseSqlStatement(sql); + return substituteNamedParameters(parsedSql, paramSource); + } + + /** + * Convert a Map of named parameter values to a corresponding array. + *

    This is a shortcut version of + * {@link #buildValueArray(ParsedSql, SqlParameterSource, java.util.List)}. + * @param sql the SQL statement + * @param paramMap the Map of parameters + * @return the array of values + */ + public static Object[] buildValueArray(String sql, Map paramMap) { + ParsedSql parsedSql = parseSqlStatement(sql); + return buildValueArray(parsedSql, new MapSqlParameterSource(paramMap), null); + } + + + private static class ParameterHolder { + + private final String parameterName; + + private final int startIndex; + + private final int endIndex; + + public ParameterHolder(String parameterName, int startIndex, int endIndex) { + this.parameterName = parameterName; + this.startIndex = startIndex; + this.endIndex = endIndex; + } + + public String getParameterName() { + return this.parameterName; + } + + public int getStartIndex() { + return this.startIndex; + } + + public int getEndIndex() { + return this.endIndex; + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/ParsedSql.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/ParsedSql.java new file mode 100644 index 0000000..d862838 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/ParsedSql.java @@ -0,0 +1,145 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.namedparam; + +import java.util.ArrayList; +import java.util.List; + +/** + * Holds information about a parsed SQL statement. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.0 + */ +public class ParsedSql { + + private final String originalSql; + + private final List parameterNames = new ArrayList<>(); + + private final List parameterIndexes = new ArrayList<>(); + + private int namedParameterCount; + + private int unnamedParameterCount; + + private int totalParameterCount; + + + /** + * Create a new instance of the {@link ParsedSql} class. + * @param originalSql the SQL statement that is being (or is to be) parsed + */ + ParsedSql(String originalSql) { + this.originalSql = originalSql; + } + + /** + * Return the SQL statement that is being parsed. + */ + String getOriginalSql() { + return this.originalSql; + } + + + /** + * Add a named parameter parsed from this SQL statement. + * @param parameterName the name of the parameter + * @param startIndex the start index in the original SQL String + * @param endIndex the end index in the original SQL String + */ + void addNamedParameter(String parameterName, int startIndex, int endIndex) { + this.parameterNames.add(parameterName); + this.parameterIndexes.add(new int[] {startIndex, endIndex}); + } + + /** + * Return all of the parameters (bind variables) in the parsed SQL statement. + * Repeated occurrences of the same parameter name are included here. + */ + List getParameterNames() { + return this.parameterNames; + } + + /** + * Return the parameter indexes for the specified parameter. + * @param parameterPosition the position of the parameter + * (as index in the parameter names List) + * @return the start index and end index, combined into + * a int array of length 2 + */ + int[] getParameterIndexes(int parameterPosition) { + return this.parameterIndexes.get(parameterPosition); + } + + /** + * Set the count of named parameters in the SQL statement. + * Each parameter name counts once; repeated occurrences do not count here. + */ + void setNamedParameterCount(int namedParameterCount) { + this.namedParameterCount = namedParameterCount; + } + + /** + * Return the count of named parameters in the SQL statement. + * Each parameter name counts once; repeated occurrences do not count here. + */ + int getNamedParameterCount() { + return this.namedParameterCount; + } + + /** + * Set the count of all of the unnamed parameters in the SQL statement. + */ + void setUnnamedParameterCount(int unnamedParameterCount) { + this.unnamedParameterCount = unnamedParameterCount; + } + + /** + * Return the count of all of the unnamed parameters in the SQL statement. + */ + int getUnnamedParameterCount() { + return this.unnamedParameterCount; + } + + /** + * Set the total count of all of the parameters in the SQL statement. + * Repeated occurrences of the same parameter name do count here. + */ + void setTotalParameterCount(int totalParameterCount) { + this.totalParameterCount = totalParameterCount; + } + + /** + * Return the total count of all of the parameters in the SQL statement. + * Repeated occurrences of the same parameter name do count here. + */ + int getTotalParameterCount() { + return this.totalParameterCount; + } + + + /** + * Exposes the original SQL String. + */ + @Override + public String toString() { + return this.originalSql; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SqlParameterSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SqlParameterSource.java new file mode 100644 index 0000000..63747b2 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SqlParameterSource.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.namedparam; + +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.lang.Nullable; + +/** + * Interface that defines common functionality for objects that can + * offer parameter values for named SQL parameters, serving as argument + * for {@link NamedParameterJdbcTemplate} operations. + * + *

    This interface allows for the specification of SQL type in addition + * to parameter values. All parameter values and types are identified by + * specifying the name of the parameter. + * + *

    Intended to wrap various implementations like a Map or a JavaBean + * with a consistent interface. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.0 + * @see NamedParameterJdbcOperations + * @see NamedParameterJdbcTemplate + * @see MapSqlParameterSource + * @see BeanPropertySqlParameterSource + */ +public interface SqlParameterSource { + + /** + * Constant that indicates an unknown (or unspecified) SQL type. + * To be returned from {@code getType} when no specific SQL type known. + * @see #getSqlType + * @see java.sql.Types + */ + int TYPE_UNKNOWN = JdbcUtils.TYPE_UNKNOWN; + + + /** + * Determine whether there is a value for the specified named parameter. + * @param paramName the name of the parameter + * @return whether there is a value defined + */ + boolean hasValue(String paramName); + + /** + * Return the parameter value for the requested named parameter. + * @param paramName the name of the parameter + * @return the value of the specified parameter + * @throws IllegalArgumentException if there is no value for the requested parameter + */ + @Nullable + Object getValue(String paramName) throws IllegalArgumentException; + + /** + * Determine the SQL type for the specified named parameter. + * @param paramName the name of the parameter + * @return the SQL type of the specified parameter, + * or {@code TYPE_UNKNOWN} if not known + * @see #TYPE_UNKNOWN + */ + default int getSqlType(String paramName) { + return TYPE_UNKNOWN; + } + + /** + * Determine the type name for the specified named parameter. + * @param paramName the name of the parameter + * @return the type name of the specified parameter, + * or {@code null} if not known + */ + @Nullable + default String getTypeName(String paramName) { + return null; + } + + /** + * Enumerate all available parameter names if possible. + *

    This is an optional operation, primarily for use with + * {@link org.springframework.jdbc.core.simple.SimpleJdbcInsert} + * and {@link org.springframework.jdbc.core.simple.SimpleJdbcCall}. + * @return the array of parameter names, or {@code null} if not determinable + * @since 5.0.3 + * @see SqlParameterSourceUtils#extractCaseInsensitiveParameterNames + */ + @Nullable + default String[] getParameterNames() { + return null; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SqlParameterSourceUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SqlParameterSourceUtils.java new file mode 100644 index 0000000..e2bd60e --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SqlParameterSourceUtils.java @@ -0,0 +1,124 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.namedparam; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.jdbc.core.SqlParameterValue; +import org.springframework.lang.Nullable; + +/** + * Class that provides helper methods for the use of {@link SqlParameterSource}, + * in particular with {@link NamedParameterJdbcTemplate}. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.5 + */ +public abstract class SqlParameterSourceUtils { + + /** + * Create an array of {@link SqlParameterSource} objects populated with data + * from the values passed in (either a {@link Map} or a bean object). + * This will define what is included in a batch operation. + * @param candidates object array of objects containing the values to be used + * @return an array of {@link SqlParameterSource} + * @see MapSqlParameterSource + * @see BeanPropertySqlParameterSource + * @see NamedParameterJdbcTemplate#batchUpdate(String, SqlParameterSource[]) + */ + public static SqlParameterSource[] createBatch(Object... candidates) { + return createBatch(Arrays.asList(candidates)); + } + + /** + * Create an array of {@link SqlParameterSource} objects populated with data + * from the values passed in (either a {@link Map} or a bean object). + * This will define what is included in a batch operation. + * @param candidates collection of objects containing the values to be used + * @return an array of {@link SqlParameterSource} + * @since 5.0.2 + * @see MapSqlParameterSource + * @see BeanPropertySqlParameterSource + * @see NamedParameterJdbcTemplate#batchUpdate(String, SqlParameterSource[]) + */ + @SuppressWarnings("unchecked") + public static SqlParameterSource[] createBatch(Collection candidates) { + SqlParameterSource[] batch = new SqlParameterSource[candidates.size()]; + int i = 0; + for (Object candidate : candidates) { + batch[i] = (candidate instanceof Map ? new MapSqlParameterSource((Map) candidate) : + new BeanPropertySqlParameterSource(candidate)); + i++; + } + return batch; + } + + /** + * Create an array of {@link MapSqlParameterSource} objects populated with data from + * the values passed in. This will define what is included in a batch operation. + * @param valueMaps array of {@link Map} instances containing the values to be used + * @return an array of {@link SqlParameterSource} + * @see MapSqlParameterSource + * @see NamedParameterJdbcTemplate#batchUpdate(String, Map[]) + */ + public static SqlParameterSource[] createBatch(Map[] valueMaps) { + SqlParameterSource[] batch = new SqlParameterSource[valueMaps.length]; + for (int i = 0; i < valueMaps.length; i++) { + batch[i] = new MapSqlParameterSource(valueMaps[i]); + } + return batch; + } + + /** + * Create a wrapped value if parameter has type information, plain object if not. + * @param source the source of parameter values and type information + * @param parameterName the name of the parameter + * @return the value object + * @see SqlParameterValue + */ + @Nullable + public static Object getTypedValue(SqlParameterSource source, String parameterName) { + int sqlType = source.getSqlType(parameterName); + if (sqlType != SqlParameterSource.TYPE_UNKNOWN) { + return new SqlParameterValue(sqlType, source.getTypeName(parameterName), source.getValue(parameterName)); + } + else { + return source.getValue(parameterName); + } + } + + /** + * Create a Map of case insensitive parameter names together with the original name. + * @param parameterSource the source of parameter names + * @return the Map that can be used for case insensitive matching of parameter names + */ + public static Map extractCaseInsensitiveParameterNames(SqlParameterSource parameterSource) { + Map caseInsensitiveParameterNames = new HashMap<>(); + String[] paramNames = parameterSource.getParameterNames(); + if (paramNames != null) { + for (String name : paramNames) { + caseInsensitiveParameterNames.put(name.toLowerCase(), name); + } + } + return caseInsensitiveParameterNames; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/package-info.java new file mode 100644 index 0000000..d3fc292 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/package-info.java @@ -0,0 +1,18 @@ +/** + * JdbcTemplate variant with named parameter support. + * + *

    NamedParameterJdbcTemplate is a wrapper around JdbcTemplate that adds + * support for named parameter parsing. It does not implement the JdbcOperations + * interface or extend JdbcTemplate, but implements the dedicated + * NamedParameterJdbcOperations interface. + * + *

    If you need the full power of Spring JDBC for less common operations, use + * the {@code getJdbcOperations()} method of NamedParameterJdbcTemplate and + * work with the returned classic template, or use a JdbcTemplate instance directly. + */ +@NonNullApi +@NonNullFields +package org.springframework.jdbc.core.namedparam; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/package-info.java new file mode 100644 index 0000000..2d79acd --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/package-info.java @@ -0,0 +1,10 @@ +/** + * Provides the core JDBC framework, based on JdbcTemplate + * and its associated callback interfaces and helper objects. + */ +@NonNullApi +@NonNullFields +package org.springframework.jdbc.core; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcCall.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcCall.java new file mode 100644 index 0000000..ddfdd58 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcCall.java @@ -0,0 +1,463 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.simple; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.jdbc.core.CallableStatementCreator; +import org.springframework.jdbc.core.CallableStatementCreatorFactory; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.jdbc.core.metadata.CallMetaDataContext; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Abstract class to provide base functionality for easy stored procedure calls + * based on configuration options and database meta-data. + * + *

    This class provides the base SPI for {@link SimpleJdbcCall}. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.5 + */ +public abstract class AbstractJdbcCall { + + /** Logger available to subclasses. */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** Lower-level class used to execute SQL. */ + private final JdbcTemplate jdbcTemplate; + + /** Context used to retrieve and manage database meta-data. */ + private final CallMetaDataContext callMetaDataContext = new CallMetaDataContext(); + + /** List of SqlParameter objects. */ + private final List declaredParameters = new ArrayList<>(); + + /** List of RefCursor/ResultSet RowMapper objects. */ + private final Map> declaredRowMappers = new LinkedHashMap<>(); + + /** + * Has this operation been compiled? Compilation means at least checking + * that a DataSource or JdbcTemplate has been provided. + */ + private volatile boolean compiled; + + /** The generated string used for call statement. */ + @Nullable + private String callString; + + /** + * A delegate enabling us to create CallableStatementCreators + * efficiently, based on this class's declared parameters. + */ + @Nullable + private CallableStatementCreatorFactory callableStatementFactory; + + + /** + * Constructor to be used when initializing using a {@link DataSource}. + * @param dataSource the DataSource to be used + */ + protected AbstractJdbcCall(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + /** + * Constructor to be used when initializing using a {@link JdbcTemplate}. + * @param jdbcTemplate the JdbcTemplate to use + */ + protected AbstractJdbcCall(JdbcTemplate jdbcTemplate) { + Assert.notNull(jdbcTemplate, "JdbcTemplate must not be null"); + this.jdbcTemplate = jdbcTemplate; + } + + + /** + * Get the configured {@link JdbcTemplate}. + */ + public JdbcTemplate getJdbcTemplate() { + return this.jdbcTemplate; + } + + /** + * Set the name of the stored procedure. + */ + public void setProcedureName(@Nullable String procedureName) { + this.callMetaDataContext.setProcedureName(procedureName); + } + + /** + * Get the name of the stored procedure. + */ + @Nullable + public String getProcedureName() { + return this.callMetaDataContext.getProcedureName(); + } + + /** + * Set the names of in parameters to be used. + */ + public void setInParameterNames(Set inParameterNames) { + this.callMetaDataContext.setLimitedInParameterNames(inParameterNames); + } + + /** + * Get the names of in parameters to be used. + */ + public Set getInParameterNames() { + return this.callMetaDataContext.getLimitedInParameterNames(); + } + + /** + * Set the catalog name to use. + */ + public void setCatalogName(@Nullable String catalogName) { + this.callMetaDataContext.setCatalogName(catalogName); + } + + /** + * Get the catalog name used. + */ + @Nullable + public String getCatalogName() { + return this.callMetaDataContext.getCatalogName(); + } + + /** + * Set the schema name to use. + */ + public void setSchemaName(@Nullable String schemaName) { + this.callMetaDataContext.setSchemaName(schemaName); + } + + /** + * Get the schema name used. + */ + @Nullable + public String getSchemaName() { + return this.callMetaDataContext.getSchemaName(); + } + + /** + * Specify whether this call is a function call. + * The default is {@code false}. + */ + public void setFunction(boolean function) { + this.callMetaDataContext.setFunction(function); + } + + /** + * Is this call a function call? + */ + public boolean isFunction() { + return this.callMetaDataContext.isFunction(); + } + + /** + * Specify whether the call requires a return value. + * The default is {@code false}. + */ + public void setReturnValueRequired(boolean returnValueRequired) { + this.callMetaDataContext.setReturnValueRequired(returnValueRequired); + } + + /** + * Does the call require a return value? + */ + public boolean isReturnValueRequired() { + return this.callMetaDataContext.isReturnValueRequired(); + } + + /** + * Specify whether parameters should be bound by name. + * The default is {@code false}. + * @since 4.2 + */ + public void setNamedBinding(boolean namedBinding) { + this.callMetaDataContext.setNamedBinding(namedBinding); + } + + /** + * Should parameters be bound by name? + * @since 4.2 + */ + public boolean isNamedBinding() { + return this.callMetaDataContext.isNamedBinding(); + } + + /** + * Specify whether the parameter meta-data for the call should be used. + * The default is {@code true}. + */ + public void setAccessCallParameterMetaData(boolean accessCallParameterMetaData) { + this.callMetaDataContext.setAccessCallParameterMetaData(accessCallParameterMetaData); + } + + /** + * Get the call string that should be used based on parameters and meta-data. + */ + @Nullable + public String getCallString() { + return this.callString; + } + + /** + * Get the {@link CallableStatementCreatorFactory} being used. + */ + protected CallableStatementCreatorFactory getCallableStatementFactory() { + Assert.state(this.callableStatementFactory != null, "No CallableStatementCreatorFactory available"); + return this.callableStatementFactory; + } + + + /** + * Add a declared parameter to the list of parameters for the call. + *

    Only parameters declared as {@code SqlParameter} and {@code SqlInOutParameter} will + * be used to provide input values. This is different from the {@code StoredProcedure} + * class which - for backwards compatibility reasons - allows input values to be provided + * for parameters declared as {@code SqlOutParameter}. + * @param parameter the {@link SqlParameter} to add + */ + public void addDeclaredParameter(SqlParameter parameter) { + Assert.notNull(parameter, "The supplied parameter must not be null"); + if (!StringUtils.hasText(parameter.getName())) { + throw new InvalidDataAccessApiUsageException( + "You must specify a parameter name when declaring parameters for \"" + getProcedureName() + "\""); + } + this.declaredParameters.add(parameter); + if (logger.isDebugEnabled()) { + logger.debug("Added declared parameter for [" + getProcedureName() + "]: " + parameter.getName()); + } + } + + /** + * Add a {@link org.springframework.jdbc.core.RowMapper} for the specified parameter or column. + * @param parameterName name of parameter or column + * @param rowMapper the RowMapper implementation to use + */ + public void addDeclaredRowMapper(String parameterName, RowMapper rowMapper) { + this.declaredRowMappers.put(parameterName, rowMapper); + if (logger.isDebugEnabled()) { + logger.debug("Added row mapper for [" + getProcedureName() + "]: " + parameterName); + } + } + + + //------------------------------------------------------------------------- + // Methods handling compilation issues + //------------------------------------------------------------------------- + + /** + * Compile this JdbcCall using provided parameters and meta-data plus other settings. + *

    This finalizes the configuration for this object and subsequent attempts to compile are + * ignored. This will be implicitly called the first time an un-compiled call is executed. + * @throws org.springframework.dao.InvalidDataAccessApiUsageException if the object hasn't + * been correctly initialized, for example if no DataSource has been provided + */ + public final synchronized void compile() throws InvalidDataAccessApiUsageException { + if (!isCompiled()) { + if (getProcedureName() == null) { + throw new InvalidDataAccessApiUsageException("Procedure or Function name is required"); + } + try { + this.jdbcTemplate.afterPropertiesSet(); + } + catch (IllegalArgumentException ex) { + throw new InvalidDataAccessApiUsageException(ex.getMessage()); + } + compileInternal(); + this.compiled = true; + if (logger.isDebugEnabled()) { + logger.debug("SqlCall for " + (isFunction() ? "function" : "procedure") + + " [" + getProcedureName() + "] compiled"); + } + } + } + + /** + * Delegate method to perform the actual compilation. + *

    Subclasses can override this template method to perform their own compilation. + * Invoked after this base class's compilation is complete. + */ + protected void compileInternal() { + DataSource dataSource = getJdbcTemplate().getDataSource(); + Assert.state(dataSource != null, "No DataSource set"); + this.callMetaDataContext.initializeMetaData(dataSource); + + // Iterate over the declared RowMappers and register the corresponding SqlParameter + this.declaredRowMappers.forEach((key, value) -> this.declaredParameters.add(this.callMetaDataContext.createReturnResultSetParameter(key, value))); + this.callMetaDataContext.processParameters(this.declaredParameters); + + this.callString = this.callMetaDataContext.createCallString(); + if (logger.isDebugEnabled()) { + logger.debug("Compiled stored procedure. Call string is [" + this.callString + "]"); + } + + this.callableStatementFactory = new CallableStatementCreatorFactory( + this.callString, this.callMetaDataContext.getCallParameters()); + + onCompileInternal(); + } + + /** + * Hook method that subclasses may override to react to compilation. + * This implementation does nothing. + */ + protected void onCompileInternal() { + } + + /** + * Is this operation "compiled"? + * @return whether this operation is compiled and ready to use + */ + public boolean isCompiled() { + return this.compiled; + } + + /** + * Check whether this operation has been compiled already; + * lazily compile it if not already compiled. + *

    Automatically called by {@code doExecute}. + */ + protected void checkCompiled() { + if (!isCompiled()) { + logger.debug("JdbcCall call not compiled before execution - invoking compile"); + compile(); + } + } + + + //------------------------------------------------------------------------- + // Methods handling execution + //------------------------------------------------------------------------- + + /** + * Delegate method that executes the call using the passed-in {@link SqlParameterSource}. + * @param parameterSource parameter names and values to be used in call + * @return a Map of out parameters + */ + protected Map doExecute(SqlParameterSource parameterSource) { + checkCompiled(); + Map params = matchInParameterValuesWithCallParameters(parameterSource); + return executeCallInternal(params); + } + + /** + * Delegate method that executes the call using the passed-in array of parameters. + * @param args array of parameter values. The order of values must match the order + * declared for the stored procedure. + * @return a Map of out parameters + */ + protected Map doExecute(Object... args) { + checkCompiled(); + Map params = matchInParameterValuesWithCallParameters(args); + return executeCallInternal(params); + } + + /** + * Delegate method that executes the call using the passed-in Map of parameters. + * @param args a Map of parameter name and values + * @return a Map of out parameters + */ + protected Map doExecute(Map args) { + checkCompiled(); + Map params = matchInParameterValuesWithCallParameters(args); + return executeCallInternal(params); + } + + /** + * Delegate method to perform the actual call processing. + */ + private Map executeCallInternal(Map args) { + CallableStatementCreator csc = getCallableStatementFactory().newCallableStatementCreator(args); + if (logger.isDebugEnabled()) { + logger.debug("The following parameters are used for call " + getCallString() + " with " + args); + int i = 1; + for (SqlParameter param : getCallParameters()) { + logger.debug(i + ": " + param.getName() + ", SQL type "+ param.getSqlType() + ", type name " + + param.getTypeName() + ", parameter class [" + param.getClass().getName() + "]"); + i++; + } + } + return getJdbcTemplate().call(csc, getCallParameters()); + } + + + /** + * Get the name of a single out parameter or return value. + * Used for functions or procedures with one out parameter. + */ + @Nullable + protected String getScalarOutParameterName() { + return this.callMetaDataContext.getScalarOutParameterName(); + } + + /** + * Get a List of all the call parameters to be used for call. + * This includes any parameters added based on meta-data processing. + */ + protected List getCallParameters() { + return this.callMetaDataContext.getCallParameters(); + } + + /** + * Match the provided in parameter values with registered parameters and + * parameters defined via meta-data processing. + * @param parameterSource the parameter vakues provided as a {@link SqlParameterSource} + * @return a Map with parameter names and values + */ + protected Map matchInParameterValuesWithCallParameters(SqlParameterSource parameterSource) { + return this.callMetaDataContext.matchInParameterValuesWithCallParameters(parameterSource); + } + + /** + * Match the provided in parameter values with registered parameters and + * parameters defined via meta-data processing. + * @param args the parameter values provided as an array + * @return a Map with parameter names and values + */ + private Map matchInParameterValuesWithCallParameters(Object[] args) { + return this.callMetaDataContext.matchInParameterValuesWithCallParameters(args); + } + + /** + * Match the provided in parameter values with registered parameters and + * parameters defined via meta-data processing. + * @param args the parameter values provided in a Map + * @return a Map with parameter names and values + */ + protected Map matchInParameterValuesWithCallParameters(Map args) { + return this.callMetaDataContext.matchInParameterValuesWithCallParameters(args); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcInsert.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcInsert.java new file mode 100644 index 0000000..eed67c1 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcInsert.java @@ -0,0 +1,625 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.simple; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.ConnectionCallback; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.SqlTypeValue; +import org.springframework.jdbc.core.StatementCreatorUtils; +import org.springframework.jdbc.core.metadata.TableMetaDataContext; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Abstract class to provide base functionality for easy inserts + * based on configuration options and database meta-data. + * + *

    This class provides the base SPI for {@link SimpleJdbcInsert}. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.5 + */ +public abstract class AbstractJdbcInsert { + + /** Logger available to subclasses. */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** Lower-level class used to execute SQL. */ + private final JdbcTemplate jdbcTemplate; + + /** Context used to retrieve and manage database meta-data. */ + private final TableMetaDataContext tableMetaDataContext = new TableMetaDataContext(); + + /** List of columns objects to be used in insert statement. */ + private final List declaredColumns = new ArrayList<>(); + + /** The names of the columns holding the generated key. */ + private String[] generatedKeyNames = new String[0]; + + /** + * Has this operation been compiled? Compilation means at least checking + * that a DataSource or JdbcTemplate has been provided. + */ + private volatile boolean compiled; + + /** The generated string used for insert statement. */ + private String insertString = ""; + + /** The SQL type information for the insert columns. */ + private int[] insertTypes = new int[0]; + + + /** + * Constructor to be used when initializing using a {@link DataSource}. + * @param dataSource the DataSource to be used + */ + protected AbstractJdbcInsert(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + /** + * Constructor to be used when initializing using a {@link JdbcTemplate}. + * @param jdbcTemplate the JdbcTemplate to use + */ + protected AbstractJdbcInsert(JdbcTemplate jdbcTemplate) { + Assert.notNull(jdbcTemplate, "JdbcTemplate must not be null"); + this.jdbcTemplate = jdbcTemplate; + } + + + //------------------------------------------------------------------------- + // Methods dealing with configuration properties + //------------------------------------------------------------------------- + + /** + * Get the configured {@link JdbcTemplate}. + */ + public JdbcTemplate getJdbcTemplate() { + return this.jdbcTemplate; + } + + /** + * Set the name of the table for this insert. + */ + public void setTableName(@Nullable String tableName) { + checkIfConfigurationModificationIsAllowed(); + this.tableMetaDataContext.setTableName(tableName); + } + + /** + * Get the name of the table for this insert. + */ + @Nullable + public String getTableName() { + return this.tableMetaDataContext.getTableName(); + } + + /** + * Set the name of the schema for this insert. + */ + public void setSchemaName(@Nullable String schemaName) { + checkIfConfigurationModificationIsAllowed(); + this.tableMetaDataContext.setSchemaName(schemaName); + } + + /** + * Get the name of the schema for this insert. + */ + @Nullable + public String getSchemaName() { + return this.tableMetaDataContext.getSchemaName(); + } + + /** + * Set the name of the catalog for this insert. + */ + public void setCatalogName(@Nullable String catalogName) { + checkIfConfigurationModificationIsAllowed(); + this.tableMetaDataContext.setCatalogName(catalogName); + } + + /** + * Get the name of the catalog for this insert. + */ + @Nullable + public String getCatalogName() { + return this.tableMetaDataContext.getCatalogName(); + } + + /** + * Set the names of the columns to be used. + */ + public void setColumnNames(List columnNames) { + checkIfConfigurationModificationIsAllowed(); + this.declaredColumns.clear(); + this.declaredColumns.addAll(columnNames); + } + + /** + * Get the names of the columns used. + */ + public List getColumnNames() { + return Collections.unmodifiableList(this.declaredColumns); + } + + /** + * Specify the name of a single generated key column. + */ + public void setGeneratedKeyName(String generatedKeyName) { + checkIfConfigurationModificationIsAllowed(); + this.generatedKeyNames = new String[] {generatedKeyName}; + } + + /** + * Set the names of any generated keys. + */ + public void setGeneratedKeyNames(String... generatedKeyNames) { + checkIfConfigurationModificationIsAllowed(); + this.generatedKeyNames = generatedKeyNames; + } + + /** + * Get the names of any generated keys. + */ + public String[] getGeneratedKeyNames() { + return this.generatedKeyNames; + } + + /** + * Specify whether the parameter meta-data for the call should be used. + * The default is {@code true}. + */ + public void setAccessTableColumnMetaData(boolean accessTableColumnMetaData) { + this.tableMetaDataContext.setAccessTableColumnMetaData(accessTableColumnMetaData); + } + + /** + * Specify whether the default for including synonyms should be changed. + * The default is {@code false}. + */ + public void setOverrideIncludeSynonymsDefault(boolean override) { + this.tableMetaDataContext.setOverrideIncludeSynonymsDefault(override); + } + + /** + * Get the insert string to be used. + */ + public String getInsertString() { + return this.insertString; + } + + /** + * Get the array of {@link java.sql.Types} to be used for insert. + */ + public int[] getInsertTypes() { + return this.insertTypes; + } + + + //------------------------------------------------------------------------- + // Methods handling compilation issues + //------------------------------------------------------------------------- + + /** + * Compile this JdbcInsert using provided parameters and meta-data plus other settings. + * This finalizes the configuration for this object and subsequent attempts to compile are + * ignored. This will be implicitly called the first time an un-compiled insert is executed. + * @throws InvalidDataAccessApiUsageException if the object hasn't been correctly initialized, + * for example if no DataSource has been provided + */ + public final synchronized void compile() throws InvalidDataAccessApiUsageException { + if (!isCompiled()) { + if (getTableName() == null) { + throw new InvalidDataAccessApiUsageException("Table name is required"); + } + try { + this.jdbcTemplate.afterPropertiesSet(); + } + catch (IllegalArgumentException ex) { + throw new InvalidDataAccessApiUsageException(ex.getMessage()); + } + compileInternal(); + this.compiled = true; + if (logger.isDebugEnabled()) { + logger.debug("JdbcInsert for table [" + getTableName() + "] compiled"); + } + } + } + + /** + * Delegate method to perform the actual compilation. + *

    Subclasses can override this template method to perform their own compilation. + * Invoked after this base class's compilation is complete. + */ + protected void compileInternal() { + DataSource dataSource = getJdbcTemplate().getDataSource(); + Assert.state(dataSource != null, "No DataSource set"); + this.tableMetaDataContext.processMetaData(dataSource, getColumnNames(), getGeneratedKeyNames()); + this.insertString = this.tableMetaDataContext.createInsertString(getGeneratedKeyNames()); + this.insertTypes = this.tableMetaDataContext.createInsertTypes(); + if (logger.isDebugEnabled()) { + logger.debug("Compiled insert object: insert string is [" + this.insertString + "]"); + } + onCompileInternal(); + } + + /** + * Hook method that subclasses may override to react to compilation. + *

    This implementation is empty. + */ + protected void onCompileInternal() { + } + + /** + * Is this operation "compiled"? + * @return whether this operation is compiled and ready to use + */ + public boolean isCompiled() { + return this.compiled; + } + + /** + * Check whether this operation has been compiled already; + * lazily compile it if not already compiled. + *

    Automatically called by {@code validateParameters}. + */ + protected void checkCompiled() { + if (!isCompiled()) { + logger.debug("JdbcInsert not compiled before execution - invoking compile"); + compile(); + } + } + + /** + * Method to check whether we are allowed to make any configuration changes at this time. + * If the class has been compiled, then no further changes to the configuration are allowed. + */ + protected void checkIfConfigurationModificationIsAllowed() { + if (isCompiled()) { + throw new InvalidDataAccessApiUsageException( + "Configuration cannot be altered once the class has been compiled or used"); + } + } + + + //------------------------------------------------------------------------- + // Methods handling execution + //------------------------------------------------------------------------- + + /** + * Delegate method that executes the insert using the passed-in Map of parameters. + * @param args a Map with parameter names and values to be used in insert + * @return the number of rows affected + */ + protected int doExecute(Map args) { + checkCompiled(); + List values = matchInParameterValuesWithInsertColumns(args); + return executeInsertInternal(values); + } + + /** + * Delegate method that executes the insert using the passed-in {@link SqlParameterSource}. + * @param parameterSource parameter names and values to be used in insert + * @return the number of rows affected + */ + protected int doExecute(SqlParameterSource parameterSource) { + checkCompiled(); + List values = matchInParameterValuesWithInsertColumns(parameterSource); + return executeInsertInternal(values); + } + + /** + * Delegate method to execute the insert. + */ + private int executeInsertInternal(List values) { + if (logger.isDebugEnabled()) { + logger.debug("The following parameters are used for insert " + getInsertString() + " with: " + values); + } + return getJdbcTemplate().update(getInsertString(), values.toArray(), getInsertTypes()); + } + + /** + * Method that provides execution of the insert using the passed-in + * Map of parameters and returning a generated key. + * @param args a Map with parameter names and values to be used in insert + * @return the key generated by the insert + */ + protected Number doExecuteAndReturnKey(Map args) { + checkCompiled(); + List values = matchInParameterValuesWithInsertColumns(args); + return executeInsertAndReturnKeyInternal(values); + } + + /** + * Method that provides execution of the insert using the passed-in + * {@link SqlParameterSource} and returning a generated key. + * @param parameterSource parameter names and values to be used in insert + * @return the key generated by the insert + */ + protected Number doExecuteAndReturnKey(SqlParameterSource parameterSource) { + checkCompiled(); + List values = matchInParameterValuesWithInsertColumns(parameterSource); + return executeInsertAndReturnKeyInternal(values); + } + + /** + * Method that provides execution of the insert using the passed-in + * Map of parameters and returning all generated keys. + * @param args a Map with parameter names and values to be used in insert + * @return the KeyHolder containing keys generated by the insert + */ + protected KeyHolder doExecuteAndReturnKeyHolder(Map args) { + checkCompiled(); + List values = matchInParameterValuesWithInsertColumns(args); + return executeInsertAndReturnKeyHolderInternal(values); + } + + /** + * Method that provides execution of the insert using the passed-in + * {@link SqlParameterSource} and returning all generated keys. + * @param parameterSource parameter names and values to be used in insert + * @return the KeyHolder containing keys generated by the insert + */ + protected KeyHolder doExecuteAndReturnKeyHolder(SqlParameterSource parameterSource) { + checkCompiled(); + List values = matchInParameterValuesWithInsertColumns(parameterSource); + return executeInsertAndReturnKeyHolderInternal(values); + } + + /** + * Delegate method to execute the insert, generating a single key. + */ + private Number executeInsertAndReturnKeyInternal(final List values) { + KeyHolder kh = executeInsertAndReturnKeyHolderInternal(values); + if (kh.getKey() != null) { + return kh.getKey(); + } + else { + throw new DataIntegrityViolationException( + "Unable to retrieve the generated key for the insert: " + getInsertString()); + } + } + + /** + * Delegate method to execute the insert, generating any number of keys. + */ + private KeyHolder executeInsertAndReturnKeyHolderInternal(final List values) { + if (logger.isDebugEnabled()) { + logger.debug("The following parameters are used for call " + getInsertString() + " with: " + values); + } + final KeyHolder keyHolder = new GeneratedKeyHolder(); + + if (this.tableMetaDataContext.isGetGeneratedKeysSupported()) { + getJdbcTemplate().update( + con -> { + PreparedStatement ps = prepareStatementForGeneratedKeys(con); + setParameterValues(ps, values, getInsertTypes()); + return ps; + }, + keyHolder); + } + + else { + if (!this.tableMetaDataContext.isGetGeneratedKeysSimulated()) { + throw new InvalidDataAccessResourceUsageException( + "The getGeneratedKeys feature is not supported by this database"); + } + if (getGeneratedKeyNames().length < 1) { + throw new InvalidDataAccessApiUsageException("Generated Key Name(s) not specified. " + + "Using the generated keys features requires specifying the name(s) of the generated column(s)"); + } + if (getGeneratedKeyNames().length > 1) { + throw new InvalidDataAccessApiUsageException( + "Current database only supports retrieving the key for a single column. There are " + + getGeneratedKeyNames().length + " columns specified: " + Arrays.asList(getGeneratedKeyNames())); + } + + Assert.state(getTableName() != null, "No table name set"); + final String keyQuery = this.tableMetaDataContext.getSimpleQueryForGetGeneratedKey( + getTableName(), getGeneratedKeyNames()[0]); + Assert.state(keyQuery != null, "Query for simulating get generated keys must not be null"); + + // This is a hack to be able to get the generated key from a database that doesn't support + // get generated keys feature. HSQL is one, PostgreSQL is another. Postgres uses a RETURNING + // clause while HSQL uses a second query that has to be executed with the same connection. + + if (keyQuery.toUpperCase().startsWith("RETURNING")) { + Long key = getJdbcTemplate().queryForObject( + getInsertString() + " " + keyQuery, Long.class, values.toArray()); + Map keys = new HashMap<>(2); + keys.put(getGeneratedKeyNames()[0], key); + keyHolder.getKeyList().add(keys); + } + else { + getJdbcTemplate().execute((ConnectionCallback) con -> { + // Do the insert + PreparedStatement ps = null; + try { + ps = con.prepareStatement(getInsertString()); + setParameterValues(ps, values, getInsertTypes()); + ps.executeUpdate(); + } + finally { + JdbcUtils.closeStatement(ps); + } + //Get the key + Statement keyStmt = null; + ResultSet rs = null; + try { + keyStmt = con.createStatement(); + rs = keyStmt.executeQuery(keyQuery); + if (rs.next()) { + long key = rs.getLong(1); + Map keys = new HashMap<>(2); + keys.put(getGeneratedKeyNames()[0], key); + keyHolder.getKeyList().add(keys); + } + } + finally { + JdbcUtils.closeResultSet(rs); + JdbcUtils.closeStatement(keyStmt); + } + return null; + }); + } + } + + return keyHolder; + } + + /** + * Create a PreparedStatement to be used for an insert operation with generated keys. + * @param con the Connection to use + * @return the PreparedStatement + */ + private PreparedStatement prepareStatementForGeneratedKeys(Connection con) throws SQLException { + if (getGeneratedKeyNames().length < 1) { + throw new InvalidDataAccessApiUsageException("Generated Key Name(s) not specified. " + + "Using the generated keys features requires specifying the name(s) of the generated column(s)."); + } + PreparedStatement ps; + if (this.tableMetaDataContext.isGeneratedKeysColumnNameArraySupported()) { + if (logger.isDebugEnabled()) { + logger.debug("Using generated keys support with array of column names."); + } + ps = con.prepareStatement(getInsertString(), getGeneratedKeyNames()); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Using generated keys support with Statement.RETURN_GENERATED_KEYS."); + } + ps = con.prepareStatement(getInsertString(), Statement.RETURN_GENERATED_KEYS); + } + return ps; + } + + /** + * Delegate method that executes a batch insert using the passed-in Maps of parameters. + * @param batch array of Maps with parameter names and values to be used in batch insert + * @return array of number of rows affected + */ + @SuppressWarnings("unchecked") + protected int[] doExecuteBatch(Map... batch) { + checkCompiled(); + List> batchValues = new ArrayList<>(batch.length); + for (Map args : batch) { + batchValues.add(matchInParameterValuesWithInsertColumns(args)); + } + return executeBatchInternal(batchValues); + } + + /** + * Delegate method that executes a batch insert using the passed-in {@link SqlParameterSource SqlParameterSources}. + * @param batch array of SqlParameterSource with parameter names and values to be used in insert + * @return array of number of rows affected + */ + protected int[] doExecuteBatch(SqlParameterSource... batch) { + checkCompiled(); + List> batchValues = new ArrayList<>(batch.length); + for (SqlParameterSource parameterSource : batch) { + batchValues.add(matchInParameterValuesWithInsertColumns(parameterSource)); + } + return executeBatchInternal(batchValues); + } + + /** + * Delegate method to execute the batch insert. + */ + private int[] executeBatchInternal(final List> batchValues) { + if (logger.isDebugEnabled()) { + logger.debug("Executing statement " + getInsertString() + " with batch of size: " + batchValues.size()); + } + return getJdbcTemplate().batchUpdate(getInsertString(), + new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + setParameterValues(ps, batchValues.get(i), getInsertTypes()); + } + @Override + public int getBatchSize() { + return batchValues.size(); + } + }); + } + + /** + * Internal implementation for setting parameter values. + * @param preparedStatement the PreparedStatement + * @param values the values to be set + */ + private void setParameterValues(PreparedStatement preparedStatement, List values, @Nullable int... columnTypes) + throws SQLException { + + int colIndex = 0; + for (Object value : values) { + colIndex++; + if (columnTypes == null || colIndex > columnTypes.length) { + StatementCreatorUtils.setParameterValue(preparedStatement, colIndex, SqlTypeValue.TYPE_UNKNOWN, value); + } + else { + StatementCreatorUtils.setParameterValue(preparedStatement, colIndex, columnTypes[colIndex - 1], value); + } + } + } + + /** + * Match the provided in parameter values with registered parameters and parameters + * defined via meta-data processing. + * @param parameterSource the parameter values provided as a {@link SqlParameterSource} + * @return a Map with parameter names and values + */ + protected List matchInParameterValuesWithInsertColumns(SqlParameterSource parameterSource) { + return this.tableMetaDataContext.matchInParameterValuesWithInsertColumns(parameterSource); + } + + /** + * Match the provided in parameter values with registered parameters and parameters + * defined via meta-data processing. + * @param args the parameter values provided in a Map + * @return a Map with parameter names and values + */ + protected List matchInParameterValuesWithInsertColumns(Map args) { + return this.tableMetaDataContext.matchInParameterValuesWithInsertColumns(args); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCall.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCall.java new file mode 100644 index 0000000..dad5287 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCall.java @@ -0,0 +1,201 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.simple; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Map; + +import javax.sql.DataSource; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; + +/** + * A SimpleJdbcCall is a multi-threaded, reusable object representing a call + * to a stored procedure or a stored function. It provides meta-data processing + * to simplify the code needed to access basic stored procedures/functions. + * All you need to provide is the name of the procedure/function and a Map + * containing the parameters when you execute the call. The names of the + * supplied parameters will be matched up with in and out parameters declared + * when the stored procedure was created. + * + *

    The meta-data processing is based on the DatabaseMetaData provided by + * the JDBC driver. Since we rely on the JDBC driver, this "auto-detection" + * can only be used for databases that are known to provide accurate meta-data. + * These currently include Derby, MySQL, Microsoft SQL Server, Oracle, DB2, + * Sybase and PostgreSQL. For any other databases you are required to declare + * all parameters explicitly. You can of course declare all parameters + * explicitly even if the database provides the necessary meta-data. In that + * case your declared parameters will take precedence. You can also turn off + * any meta-data processing if you want to use parameter names that do not + * match what is declared during the stored procedure compilation. + * + *

    The actual insert is being handled using Spring's {@link JdbcTemplate}. + * + *

    Many of the configuration methods return the current instance of the + * SimpleJdbcCall in order to provide the ability to chain multiple ones + * together in a "fluent" interface style. + * + * @author Thomas Risberg + * @author Stephane Nicoll + * @since 2.5 + * @see java.sql.DatabaseMetaData + * @see org.springframework.jdbc.core.JdbcTemplate + */ +public class SimpleJdbcCall extends AbstractJdbcCall implements SimpleJdbcCallOperations { + + /** + * Constructor that takes one parameter with the JDBC DataSource to use when + * creating the underlying JdbcTemplate. + * @param dataSource the {@code DataSource} to use + * @see org.springframework.jdbc.core.JdbcTemplate#setDataSource + */ + public SimpleJdbcCall(DataSource dataSource) { + super(dataSource); + } + + /** + * Alternative Constructor that takes one parameter with the JdbcTemplate to be used. + * @param jdbcTemplate the {@code JdbcTemplate} to use + * @see org.springframework.jdbc.core.JdbcTemplate#setDataSource + */ + public SimpleJdbcCall(JdbcTemplate jdbcTemplate) { + super(jdbcTemplate); + } + + + @Override + public SimpleJdbcCall withProcedureName(String procedureName) { + setProcedureName(procedureName); + setFunction(false); + return this; + } + + @Override + public SimpleJdbcCall withFunctionName(String functionName) { + setProcedureName(functionName); + setFunction(true); + return this; + } + + @Override + public SimpleJdbcCall withSchemaName(String schemaName) { + setSchemaName(schemaName); + return this; + } + + @Override + public SimpleJdbcCall withCatalogName(String catalogName) { + setCatalogName(catalogName); + return this; + } + + @Override + public SimpleJdbcCall withReturnValue() { + setReturnValueRequired(true); + return this; + } + + @Override + public SimpleJdbcCall declareParameters(SqlParameter... sqlParameters) { + for (SqlParameter sqlParameter : sqlParameters) { + if (sqlParameter != null) { + addDeclaredParameter(sqlParameter); + } + } + return this; + } + + @Override + public SimpleJdbcCall useInParameterNames(String... inParameterNames) { + setInParameterNames(new LinkedHashSet<>(Arrays.asList(inParameterNames))); + return this; + } + + @Override + public SimpleJdbcCall returningResultSet(String parameterName, RowMapper rowMapper) { + addDeclaredRowMapper(parameterName, rowMapper); + return this; + } + + @Override + public SimpleJdbcCall withoutProcedureColumnMetaDataAccess() { + setAccessCallParameterMetaData(false); + return this; + } + + @Override + public SimpleJdbcCall withNamedBinding() { + setNamedBinding(true); + return this; + } + + @Override + @SuppressWarnings("unchecked") + public T executeFunction(Class returnType, Object... args) { + return (T) doExecute(args).get(getScalarOutParameterName()); + } + + @Override + @SuppressWarnings("unchecked") + public T executeFunction(Class returnType, Map args) { + return (T) doExecute(args).get(getScalarOutParameterName()); + } + + @Override + @SuppressWarnings("unchecked") + public T executeFunction(Class returnType, SqlParameterSource args) { + return (T) doExecute(args).get(getScalarOutParameterName()); + } + + @Override + @SuppressWarnings("unchecked") + public T executeObject(Class returnType, Object... args) { + return (T) doExecute(args).get(getScalarOutParameterName()); + } + + @Override + @SuppressWarnings("unchecked") + public T executeObject(Class returnType, Map args) { + return (T) doExecute(args).get(getScalarOutParameterName()); + } + + @Override + @SuppressWarnings("unchecked") + public T executeObject(Class returnType, SqlParameterSource args) { + return (T) doExecute(args).get(getScalarOutParameterName()); + } + + @Override + public Map execute(Object... args) { + return doExecute(args); + } + + @Override + public Map execute(Map args) { + return doExecute(args); + } + + @Override + public Map execute(SqlParameterSource parameterSource) { + return doExecute(parameterSource); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCallOperations.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCallOperations.java new file mode 100644 index 0000000..4c81f1b --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCallOperations.java @@ -0,0 +1,193 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.simple; + +import java.util.Map; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; + +/** + * Interface specifying the API for a Simple JDBC Call implemented by {@link SimpleJdbcCall}. + * This interface is not often used directly, but provides the option to enhance testability, + * as it can easily be mocked or stubbed. + * + * @author Thomas Risberg + * @author Stephane Nicoll + * @since 2.5 + */ +public interface SimpleJdbcCallOperations { + + /** + * Specify the procedure name to be used - this implies that we will be calling a stored procedure. + * @param procedureName the name of the stored procedure + * @return the instance of this SimpleJdbcCall + */ + SimpleJdbcCallOperations withProcedureName(String procedureName); + + /** + * Specify the procedure name to be used - this implies that we will be calling a stored function. + * @param functionName the name of the stored function + * @return the instance of this SimpleJdbcCall + */ + SimpleJdbcCallOperations withFunctionName(String functionName); + + /** + * Optionally, specify the name of the schema that contins the stored procedure. + * @param schemaName the name of the schema + * @return the instance of this SimpleJdbcCall + */ + SimpleJdbcCallOperations withSchemaName(String schemaName); + + /** + * Optionally, specify the name of the catalog that contins the stored procedure. + *

    To provide consistency with the Oracle DatabaseMetaData, this is used to specify the + * package name if the procedure is declared as part of a package. + * @param catalogName the catalog or package name + * @return the instance of this SimpleJdbcCall + */ + SimpleJdbcCallOperations withCatalogName(String catalogName); + + /** + * Indicates the procedure's return value should be included in the results returned. + * @return the instance of this SimpleJdbcCall + */ + SimpleJdbcCallOperations withReturnValue(); + + /** + * Specify one or more parameters if desired. These parameters will be supplemented with + * any parameter information retrieved from the database meta-data. + *

    Note that only parameters declared as {@code SqlParameter} and {@code SqlInOutParameter} + * will be used to provide input values. This is different from the {@code StoredProcedure} + * class which - for backwards compatibility reasons - allows input values to be provided + * for parameters declared as {@code SqlOutParameter}. + * @param sqlParameters the parameters to use + * @return the instance of this SimpleJdbcCall + */ + SimpleJdbcCallOperations declareParameters(SqlParameter... sqlParameters); + + /** Not used yet. */ + SimpleJdbcCallOperations useInParameterNames(String... inParameterNames); + + /** + * Used to specify when a ResultSet is returned by the stored procedure and you want it + * mapped by a {@link RowMapper}. The results will be returned using the parameter name + * specified. Multiple ResultSets must be declared in the correct order. + *

    If the database you are using uses ref cursors then the name specified must match + * the name of the parameter declared for the procedure in the database. + * @param parameterName the name of the returned results and/or the name of the ref cursor parameter + * @param rowMapper the RowMapper implementation that will map the data returned for each row + * */ + SimpleJdbcCallOperations returningResultSet(String parameterName, RowMapper rowMapper); + + /** + * Turn off any processing of parameter meta-data information obtained via JDBC. + * @return the instance of this SimpleJdbcCall + */ + SimpleJdbcCallOperations withoutProcedureColumnMetaDataAccess(); + + /** + * Indicates that parameters should be bound by name. + * @return the instance of this SimpleJdbcCall + * @since 4.2 + */ + SimpleJdbcCallOperations withNamedBinding(); + + + /** + * Execute the stored function and return the results obtained as an Object of the + * specified return type. + * @param returnType the type of the value to return + * @param args optional array containing the in parameter values to be used in the call. + * Parameter values must be provided in the same order as the parameters are defined + * for the stored procedure. + */ + T executeFunction(Class returnType, Object... args); + + /** + * Execute the stored function and return the results obtained as an Object of the + * specified return type. + * @param returnType the type of the value to return + * @param args a Map containing the parameter values to be used in the call + */ + T executeFunction(Class returnType, Map args); + + /** + * Execute the stored function and return the results obtained as an Object of the + * specified return type. + * @param returnType the type of the value to return + * @param args the MapSqlParameterSource containing the parameter values to be used in the call + */ + T executeFunction(Class returnType, SqlParameterSource args); + + /** + * Execute the stored procedure and return the single out parameter as an Object + * of the specified return type. In the case where there are multiple out parameters, + * the first one is returned and additional out parameters are ignored. + * @param returnType the type of the value to return + * @param args optional array containing the in parameter values to be used in the call. + * Parameter values must be provided in the same order as the parameters are defined for + * the stored procedure. + */ + T executeObject(Class returnType, Object... args); + + /** + * Execute the stored procedure and return the single out parameter as an Object + * of the specified return type. In the case where there are multiple out parameters, + * the first one is returned and additional out parameters are ignored. + * @param returnType the type of the value to return + * @param args a Map containing the parameter values to be used in the call + */ + T executeObject(Class returnType, Map args); + + /** + * Execute the stored procedure and return the single out parameter as an Object + * of the specified return type. In the case where there are multiple out parameters, + * the first one is returned and additional out parameters are ignored. + * @param returnType the type of the value to return + * @param args the MapSqlParameterSource containing the parameter values to be used in the call + */ + T executeObject(Class returnType, SqlParameterSource args); + + /** + * Execute the stored procedure and return a map of output params, keyed by name + * as in parameter declarations. + * @param args optional array containing the in parameter values to be used in the call. + * Parameter values must be provided in the same order as the parameters are defined for + * the stored procedure. + * @return a Map of output params + */ + Map execute(Object... args); + + /** + * Execute the stored procedure and return a map of output params, keyed by name + * as in parameter declarations. + * @param args a Map containing the parameter values to be used in the call + * @return a Map of output params + */ + Map execute(Map args); + + /** + * Execute the stored procedure and return a map of output params, keyed by name + * as in parameter declarations. + * @param args the SqlParameterSource containing the parameter values to be used in the call + * @return a Map of output params + */ + Map execute(SqlParameterSource args); + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcInsert.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcInsert.java new file mode 100644 index 0000000..bf5995a --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcInsert.java @@ -0,0 +1,156 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.simple; + +import java.util.Arrays; +import java.util.Map; + +import javax.sql.DataSource; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.jdbc.support.KeyHolder; + +/** + * A SimpleJdbcInsert is a multi-threaded, reusable object providing easy insert + * capabilities for a table. It provides meta-data processing to simplify the code + * needed to construct a basic insert statement. All you need to provide is the + * name of the table and a Map containing the column names and the column values. + * + *

    The meta-data processing is based on the DatabaseMetaData provided by the + * JDBC driver. As long as the JDBC driver can provide the names of the columns + * for a specified table than we can rely on this auto-detection feature. If that + * is not the case, then the column names must be specified explicitly. + * + *

    The actual insert is being handled using Spring's {@link JdbcTemplate}. + * + *

    Many of the configuration methods return the current instance of the + * SimpleJdbcInsert to provide the ability to chain multiple ones together + * in a "fluent" interface style. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.5 + * @see java.sql.DatabaseMetaData + * @see org.springframework.jdbc.core.JdbcTemplate + */ +public class SimpleJdbcInsert extends AbstractJdbcInsert implements SimpleJdbcInsertOperations { + + /** + * Constructor that takes one parameter with the JDBC DataSource to use when creating the + * JdbcTemplate. + * @param dataSource the {@code DataSource} to use + * @see org.springframework.jdbc.core.JdbcTemplate#setDataSource + */ + public SimpleJdbcInsert(DataSource dataSource) { + super(dataSource); + } + + /** + * Alternative Constructor that takes one parameter with the JdbcTemplate to be used. + * @param jdbcTemplate the {@code JdbcTemplate} to use + * @see org.springframework.jdbc.core.JdbcTemplate#setDataSource + */ + public SimpleJdbcInsert(JdbcTemplate jdbcTemplate) { + super(jdbcTemplate); + } + + + @Override + public SimpleJdbcInsert withTableName(String tableName) { + setTableName(tableName); + return this; + } + + @Override + public SimpleJdbcInsert withSchemaName(String schemaName) { + setSchemaName(schemaName); + return this; + } + + @Override + public SimpleJdbcInsert withCatalogName(String catalogName) { + setCatalogName(catalogName); + return this; + } + + @Override + public SimpleJdbcInsert usingColumns(String... columnNames) { + setColumnNames(Arrays.asList(columnNames)); + return this; + } + + @Override + public SimpleJdbcInsert usingGeneratedKeyColumns(String... columnNames) { + setGeneratedKeyNames(columnNames); + return this; + } + + @Override + public SimpleJdbcInsertOperations withoutTableColumnMetaDataAccess() { + setAccessTableColumnMetaData(false); + return this; + } + + @Override + public SimpleJdbcInsertOperations includeSynonymsForTableColumnMetaData() { + setOverrideIncludeSynonymsDefault(true); + return this; + } + + @Override + public int execute(Map args) { + return doExecute(args); + } + + @Override + public int execute(SqlParameterSource parameterSource) { + return doExecute(parameterSource); + } + + @Override + public Number executeAndReturnKey(Map args) { + return doExecuteAndReturnKey(args); + } + + @Override + public Number executeAndReturnKey(SqlParameterSource parameterSource) { + return doExecuteAndReturnKey(parameterSource); + } + + @Override + public KeyHolder executeAndReturnKeyHolder(Map args) { + return doExecuteAndReturnKeyHolder(args); + } + + @Override + public KeyHolder executeAndReturnKeyHolder(SqlParameterSource parameterSource) { + return doExecuteAndReturnKeyHolder(parameterSource); + } + + @Override + @SuppressWarnings("unchecked") + public int[] executeBatch(Map... batch) { + return doExecuteBatch(batch); + } + + @Override + public int[] executeBatch(SqlParameterSource... batch) { + return doExecuteBatch(batch); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcInsertOperations.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcInsertOperations.java new file mode 100644 index 0000000..8261a29 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcInsertOperations.java @@ -0,0 +1,153 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.simple; + +import java.util.Map; + +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.jdbc.support.KeyHolder; + +/** + * Interface specifying the API for a Simple JDBC Insert implemented by {@link SimpleJdbcInsert}. + * This interface is not often used directly, but provides the option to enhance testability, + * as it can easily be mocked or stubbed. + * + * @author Thomas Risberg + * @since 2.5 + */ +public interface SimpleJdbcInsertOperations { + + /** + * Specify the table name to be used for the insert. + * @param tableName the name of the stored table + * @return the instance of this SimpleJdbcInsert + */ + SimpleJdbcInsertOperations withTableName(String tableName); + + /** + * Specify the schema name, if any, to be used for the insert. + * @param schemaName the name of the schema + * @return the instance of this SimpleJdbcInsert + */ + SimpleJdbcInsertOperations withSchemaName(String schemaName); + + /** + * Specify the catalog name, if any, to be used for the insert. + * @param catalogName the name of the catalog + * @return the instance of this SimpleJdbcInsert + */ + SimpleJdbcInsertOperations withCatalogName(String catalogName); + + /** + * Specify the column names that the insert statement should be limited to use. + * @param columnNames one or more column names + * @return the instance of this SimpleJdbcInsert + */ + SimpleJdbcInsertOperations usingColumns(String... columnNames); + + /** + * Specify the names of any columns that have auto generated keys. + * @param columnNames one or more column names + * @return the instance of this SimpleJdbcInsert + */ + SimpleJdbcInsertOperations usingGeneratedKeyColumns(String... columnNames); + + /** + * Turn off any processing of column meta-data information obtained via JDBC. + * @return the instance of this SimpleJdbcInsert + */ + SimpleJdbcInsertOperations withoutTableColumnMetaDataAccess(); + + /** + * Include synonyms for the column meta-data lookups via JDBC. + *

    Note: This is only necessary to include for Oracle since other databases + * supporting synonyms seems to include the synonyms automatically. + * @return the instance of this SimpleJdbcInsert + */ + SimpleJdbcInsertOperations includeSynonymsForTableColumnMetaData(); + + + /** + * Execute the insert using the values passed in. + * @param args a Map containing column names and corresponding value + * @return the number of rows affected as returned by the JDBC driver + */ + int execute(Map args); + + /** + * Execute the insert using the values passed in. + * @param parameterSource the SqlParameterSource containing values to use for insert + * @return the number of rows affected as returned by the JDBC driver + */ + int execute(SqlParameterSource parameterSource); + + /** + * Execute the insert using the values passed in and return the generated key. + *

    This requires that the name of the columns with auto generated keys have been specified. + * This method will always return a KeyHolder but the caller must verify that it actually + * contains the generated keys. + * @param args a Map containing column names and corresponding value + * @return the generated key value + */ + Number executeAndReturnKey(Map args); + + /** + * Execute the insert using the values passed in and return the generated key. + *

    This requires that the name of the columns with auto generated keys have been specified. + * This method will always return a KeyHolder but the caller must verify that it actually + * contains the generated keys. + * @param parameterSource the SqlParameterSource containing values to use for insert + * @return the generated key value. + */ + Number executeAndReturnKey(SqlParameterSource parameterSource); + + /** + * Execute the insert using the values passed in and return the generated keys. + *

    This requires that the name of the columns with auto generated keys have been specified. + * This method will always return a KeyHolder but the caller must verify that it actually + * contains the generated keys. + * @param args a Map containing column names and corresponding value + * @return the KeyHolder containing all generated keys + */ + KeyHolder executeAndReturnKeyHolder(Map args); + + /** + * Execute the insert using the values passed in and return the generated keys. + *

    This requires that the name of the columns with auto generated keys have been specified. + * This method will always return a KeyHolder but the caller must verify that it actually + * contains the generated keys. + * @param parameterSource the SqlParameterSource containing values to use for insert + * @return the KeyHolder containing all generated keys + */ + KeyHolder executeAndReturnKeyHolder(SqlParameterSource parameterSource); + + /** + * Execute a batch insert using the batch of values passed in. + * @param batch an array of Maps containing a batch of column names and corresponding value + * @return the array of number of rows affected as returned by the JDBC driver + */ + @SuppressWarnings("unchecked") + int[] executeBatch(Map... batch); + + /** + * Execute a batch insert using the batch of values passed in. + * @param batch an array of SqlParameterSource containing values for the batch + * @return the array of number of rows affected as returned by the JDBC driver + */ + int[] executeBatch(SqlParameterSource... batch); + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/package-info.java new file mode 100644 index 0000000..cc76422 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/package-info.java @@ -0,0 +1,13 @@ +/** + * Simplification layer for table inserts and stored procedure calls. + * + *

    {@code SimpleJdbcInsert} and {@code SimpleJdbcCall} take advantage of database + * meta-data provided by the JDBC driver to simplify the application code. Much of the + * parameter specification becomes unnecessary since it can be looked up in the meta-data. + */ +@NonNullApi +@NonNullFields +package org.springframework.jdbc.core.simple; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractInterruptibleBatchPreparedStatementSetter.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractInterruptibleBatchPreparedStatementSetter.java new file mode 100644 index 0000000..4d82c1e --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractInterruptibleBatchPreparedStatementSetter.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.support; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.springframework.jdbc.core.InterruptibleBatchPreparedStatementSetter; + +/** + * Abstract implementation of the {@link InterruptibleBatchPreparedStatementSetter} + * interface, combining the check for available values and setting of those + * into a single callback method {@link #setValuesIfAvailable}. + * + * @author Juergen Hoeller + * @since 2.0 + * @see #setValuesIfAvailable + */ +public abstract class AbstractInterruptibleBatchPreparedStatementSetter + implements InterruptibleBatchPreparedStatementSetter { + + private boolean exhausted; + + + /** + * This implementation calls {@link #setValuesIfAvailable} + * and sets this instance's exhaustion flag accordingly. + */ + @Override + public final void setValues(PreparedStatement ps, int i) throws SQLException { + this.exhausted = !setValuesIfAvailable(ps, i); + } + + /** + * This implementation return this instance's current exhaustion flag. + */ + @Override + public final boolean isBatchExhausted(int i) { + return this.exhausted; + } + + /** + * This implementation returns {@code Integer.MAX_VALUE}. + * Can be overridden in subclasses to lower the maximum batch size. + */ + @Override + public int getBatchSize() { + return Integer.MAX_VALUE; + } + + + /** + * Check for available values and set them on the given PreparedStatement. + * If no values are available anymore, return {@code false}. + * @param ps the PreparedStatement we'll invoke setter methods on + * @param i index of the statement we're issuing in the batch, starting from 0 + * @return whether there were values to apply (that is, whether the applied + * parameters should be added to the batch and this method should be called + * for a further iteration) + * @throws SQLException if an SQLException is encountered + * (i.e. there is no need to catch SQLException) + */ + protected abstract boolean setValuesIfAvailable(PreparedStatement ps, int i) throws SQLException; + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobCreatingPreparedStatementCallback.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobCreatingPreparedStatementCallback.java new file mode 100644 index 0000000..42f415c --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobCreatingPreparedStatementCallback.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.support; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.PreparedStatementCallback; +import org.springframework.jdbc.support.lob.LobCreator; +import org.springframework.jdbc.support.lob.LobHandler; +import org.springframework.util.Assert; + +/** + * Abstract {@link PreparedStatementCallback} implementation that manages a {@link LobCreator}. + * Typically used as inner class, with access to surrounding method arguments. + * + *

    Delegates to the {@code setValues} template method for setting values + * on the PreparedStatement, using a given LobCreator for BLOB/CLOB arguments. + * + *

    A usage example with {@link org.springframework.jdbc.core.JdbcTemplate}: + * + *

    JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);  // reusable object
    + * LobHandler lobHandler = new DefaultLobHandler();  // reusable object
    + *
    + * jdbcTemplate.execute(
    + *     "INSERT INTO imagedb (image_name, content, description) VALUES (?, ?, ?)",
    + *     new AbstractLobCreatingPreparedStatementCallback(lobHandler) {
    + *       protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException {
    + *         ps.setString(1, name);
    + *         lobCreator.setBlobAsBinaryStream(ps, 2, contentStream, contentLength);
    + *         lobCreator.setClobAsString(ps, 3, description);
    + *       }
    + *     }
    + * );
    + * + * @author Juergen Hoeller + * @since 1.0.2 + * @see org.springframework.jdbc.support.lob.LobCreator + */ +public abstract class AbstractLobCreatingPreparedStatementCallback implements PreparedStatementCallback { + + private final LobHandler lobHandler; + + + /** + * Create a new AbstractLobCreatingPreparedStatementCallback for the + * given LobHandler. + * @param lobHandler the LobHandler to create LobCreators with + */ + public AbstractLobCreatingPreparedStatementCallback(LobHandler lobHandler) { + Assert.notNull(lobHandler, "LobHandler must not be null"); + this.lobHandler = lobHandler; + } + + + @Override + public final Integer doInPreparedStatement(PreparedStatement ps) throws SQLException, DataAccessException { + try (LobCreator lobCreator = this.lobHandler.getLobCreator()) { + setValues(ps, lobCreator); + return ps.executeUpdate(); + } + } + + /** + * Set values on the given PreparedStatement, using the given + * LobCreator for BLOB/CLOB arguments. + * @param ps the PreparedStatement to use + * @param lobCreator the LobCreator to use + * @throws SQLException if thrown by JDBC methods + * @throws DataAccessException in case of custom exceptions + */ + protected abstract void setValues(PreparedStatement ps, LobCreator lobCreator) + throws SQLException, DataAccessException; + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobStreamingResultSetExtractor.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobStreamingResultSetExtractor.java new file mode 100644 index 0000000..5427df2 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobStreamingResultSetExtractor.java @@ -0,0 +1,124 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.support; + +import java.io.IOException; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.jdbc.LobRetrievalFailureException; +import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.lang.Nullable; + +/** + * Abstract ResultSetExtractor implementation that assumes streaming of LOB data. + * Typically used as inner class, with access to surrounding method arguments. + * + *

    Delegates to the {@code streamData} template method for streaming LOB + * content to some OutputStream, typically using a LobHandler. Converts an + * IOException thrown during streaming to a LobRetrievalFailureException. + * + *

    A usage example with JdbcTemplate: + * + *

    JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);  // reusable object
    + * final LobHandler lobHandler = new DefaultLobHandler();  // reusable object
    + *
    + * jdbcTemplate.query(
    + *		 "SELECT content FROM imagedb WHERE image_name=?", new Object[] {name},
    + *		 new AbstractLobStreamingResultSetExtractor() {
    + *			 public void streamData(ResultSet rs) throws SQLException, IOException {
    + *				 FileCopyUtils.copy(lobHandler.getBlobAsBinaryStream(rs, 1), contentStream);
    + *             }
    + *         }
    + * );
    + * + * @author Juergen Hoeller + * @since 1.0.2 + * @param the result type + * @see org.springframework.jdbc.support.lob.LobHandler + * @see org.springframework.jdbc.LobRetrievalFailureException + */ +public abstract class AbstractLobStreamingResultSetExtractor implements ResultSetExtractor { + + /** + * Delegates to handleNoRowFound, handleMultipleRowsFound and streamData, + * according to the ResultSet state. Converts an IOException thrown by + * streamData to a LobRetrievalFailureException. + * @see #handleNoRowFound + * @see #handleMultipleRowsFound + * @see #streamData + * @see org.springframework.jdbc.LobRetrievalFailureException + */ + @Override + @Nullable + public final T extractData(ResultSet rs) throws SQLException, DataAccessException { + if (!rs.next()) { + handleNoRowFound(); + } + else { + try { + streamData(rs); + if (rs.next()) { + handleMultipleRowsFound(); + } + } + catch (IOException ex) { + throw new LobRetrievalFailureException("Could not stream LOB content", ex); + } + } + return null; + } + + /** + * Handle the case where the ResultSet does not contain a row. + * @throws DataAccessException a corresponding exception, + * by default an EmptyResultDataAccessException + * @see org.springframework.dao.EmptyResultDataAccessException + */ + protected void handleNoRowFound() throws DataAccessException { + throw new EmptyResultDataAccessException( + "LobStreamingResultSetExtractor did not find row in database", 1); + } + + /** + * Handle the case where the ResultSet contains multiple rows. + * @throws DataAccessException a corresponding exception, + * by default an IncorrectResultSizeDataAccessException + * @see org.springframework.dao.IncorrectResultSizeDataAccessException + */ + protected void handleMultipleRowsFound() throws DataAccessException { + throw new IncorrectResultSizeDataAccessException( + "LobStreamingResultSetExtractor found multiple rows in database", 1); + } + + /** + * Stream LOB content from the given ResultSet to some OutputStream. + *

    Typically used as inner class, with access to surrounding method arguments + * and to a LobHandler instance variable of the surrounding class. + * @param rs the ResultSet to take the LOB content from + * @throws SQLException if thrown by JDBC methods + * @throws IOException if thrown by stream access methods + * @throws DataAccessException in case of custom exceptions + * @see org.springframework.jdbc.support.lob.LobHandler#getBlobAsBinaryStream + * @see org.springframework.util.FileCopyUtils + */ + protected abstract void streamData(ResultSet rs) throws SQLException, IOException, DataAccessException; + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractSqlTypeValue.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractSqlTypeValue.java new file mode 100644 index 0000000..28e10a9 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractSqlTypeValue.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.support; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.springframework.jdbc.core.SqlTypeValue; +import org.springframework.lang.Nullable; + +/** + * Abstract implementation of the SqlTypeValue interface, for convenient + * creation of type values that are supposed to be passed into the + * {@code PreparedStatement.setObject} method. The {@code createTypeValue} + * callback method has access to the underlying Connection, if that should + * be needed to create any database-specific objects. + * + *

    A usage example from a StoredProcedure (compare this to the plain + * SqlTypeValue version in the superclass javadoc): + * + *

    proc.declareParameter(new SqlParameter("myarray", Types.ARRAY, "NUMBERS"));
    + * ...
    + *
    + * Map<String, Object> in = new HashMap<String, Object>();
    + * in.put("myarray", new AbstractSqlTypeValue() {
    + *   public Object createTypeValue(Connection con, int sqlType, String typeName) throws SQLException {
    + *	   oracle.sql.ArrayDescriptor desc = new oracle.sql.ArrayDescriptor(typeName, con);
    + *	   return new oracle.sql.ARRAY(desc, con, seats);
    + *   }
    + * });
    + * Map out = execute(in);
    + * 
    + * + * @author Juergen Hoeller + * @since 1.1 + * @see java.sql.PreparedStatement#setObject(int, Object, int) + * @see org.springframework.jdbc.object.StoredProcedure + */ +public abstract class AbstractSqlTypeValue implements SqlTypeValue { + + @Override + public final void setTypeValue(PreparedStatement ps, int paramIndex, int sqlType, @Nullable String typeName) + throws SQLException { + + Object value = createTypeValue(ps.getConnection(), sqlType, typeName); + if (sqlType == TYPE_UNKNOWN) { + ps.setObject(paramIndex, value); + } + else { + ps.setObject(paramIndex, value, sqlType); + } + } + + /** + * Create the type value to be passed into {@code PreparedStatement.setObject}. + * @param con the JDBC Connection, if needed to create any database-specific objects + * @param sqlType the SQL type of the parameter we are setting + * @param typeName the type name of the parameter + * @return the type value + * @throws SQLException if an SQLException is encountered setting + * parameter values (that is, there's no need to catch SQLException) + * @see java.sql.PreparedStatement#setObject(int, Object, int) + */ + protected abstract Object createTypeValue(Connection con, int sqlType, @Nullable String typeName) + throws SQLException; + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcBeanDefinitionReader.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcBeanDefinitionReader.java new file mode 100644 index 0000000..d266c35 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcBeanDefinitionReader.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.support; + +import java.util.Properties; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Bean definition reader that reads values from a database table, + * based on a given SQL statement. + * + *

    Expects columns for bean name, property name and value as String. + * Formats for each are identical to the properties format recognized + * by PropertiesBeanDefinitionReader. + * + *

    NOTE: This is mainly intended as an example for a custom + * JDBC-based bean definition reader. It does not aim to offer + * comprehensive functionality. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #loadBeanDefinitions + * @see org.springframework.beans.factory.support.PropertiesBeanDefinitionReader + * @deprecated as of 5.3, in favor of Spring's common bean definition formats + * and/or custom reader implementations + */ +@Deprecated +public class JdbcBeanDefinitionReader { + + private final org.springframework.beans.factory.support.PropertiesBeanDefinitionReader propReader; + + @Nullable + private JdbcTemplate jdbcTemplate; + + + /** + * Create a new JdbcBeanDefinitionReader for the given bean factory, + * using a default PropertiesBeanDefinitionReader underneath. + *

    DataSource or JdbcTemplate still need to be set. + * @see #setDataSource + * @see #setJdbcTemplate + */ + public JdbcBeanDefinitionReader(BeanDefinitionRegistry beanFactory) { + this.propReader = new org.springframework.beans.factory.support.PropertiesBeanDefinitionReader(beanFactory); + } + + /** + * Create a new JdbcBeanDefinitionReader that delegates to the + * given PropertiesBeanDefinitionReader underneath. + *

    DataSource or JdbcTemplate still need to be set. + * @see #setDataSource + * @see #setJdbcTemplate + */ + public JdbcBeanDefinitionReader(org.springframework.beans.factory.support.PropertiesBeanDefinitionReader reader) { + Assert.notNull(reader, "Bean definition reader must not be null"); + this.propReader = reader; + } + + + /** + * Set the DataSource to use to obtain database connections. + * Will implicitly create a new JdbcTemplate with the given DataSource. + */ + public void setDataSource(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + /** + * Set the JdbcTemplate to be used by this bean factory. + * Contains settings for DataSource, SQLExceptionTranslator, etc. + */ + public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { + Assert.notNull(jdbcTemplate, "JdbcTemplate must not be null"); + this.jdbcTemplate = jdbcTemplate; + } + + + /** + * Load bean definitions from the database via the given SQL string. + * @param sql the SQL query to use for loading bean definitions. + * The first three columns must be bean name, property name and value. + * Any join and any other columns are permitted: e.g. + * {@code SELECT BEAN_NAME, PROPERTY, VALUE FROM CONFIG WHERE CONFIG.APP_ID = 1} + * It's also possible to perform a join. Column names are not significant -- + * only the ordering of these first three columns. + */ + public void loadBeanDefinitions(String sql) { + Assert.notNull(this.jdbcTemplate, "Not fully configured - specify DataSource or JdbcTemplate"); + final Properties props = new Properties(); + this.jdbcTemplate.query(sql, rs -> { + String beanName = rs.getString(1); + String property = rs.getString(2); + String value = rs.getString(3); + // Make a properties entry by combining bean name and property. + props.setProperty(beanName + '.' + property, value); + }); + this.propReader.registerBeanDefinitions(props); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcDaoSupport.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcDaoSupport.java new file mode 100644 index 0000000..0184d93 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcDaoSupport.java @@ -0,0 +1,155 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.support; + +import java.sql.Connection; + +import javax.sql.DataSource; + +import org.springframework.dao.support.DaoSupport; +import org.springframework.jdbc.CannotGetJdbcConnectionException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.jdbc.support.SQLExceptionTranslator; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Convenient super class for JDBC-based data access objects. + * + *

    Requires a {@link javax.sql.DataSource} to be set, providing a + * {@link org.springframework.jdbc.core.JdbcTemplate} based on it to + * subclasses through the {@link #getJdbcTemplate()} method. + * + *

    This base class is mainly intended for JdbcTemplate usage but can + * also be used when working with a Connection directly or when using + * {@code org.springframework.jdbc.object} operation objects. + * + * @author Juergen Hoeller + * @since 28.07.2003 + * @see #setDataSource + * @see #getJdbcTemplate + * @see org.springframework.jdbc.core.JdbcTemplate + */ +public abstract class JdbcDaoSupport extends DaoSupport { + + @Nullable + private JdbcTemplate jdbcTemplate; + + + /** + * Set the JDBC DataSource to be used by this DAO. + */ + public final void setDataSource(DataSource dataSource) { + if (this.jdbcTemplate == null || dataSource != this.jdbcTemplate.getDataSource()) { + this.jdbcTemplate = createJdbcTemplate(dataSource); + initTemplateConfig(); + } + } + + /** + * Create a JdbcTemplate for the given DataSource. + * Only invoked if populating the DAO with a DataSource reference! + *

    Can be overridden in subclasses to provide a JdbcTemplate instance + * with different configuration, or a custom JdbcTemplate subclass. + * @param dataSource the JDBC DataSource to create a JdbcTemplate for + * @return the new JdbcTemplate instance + * @see #setDataSource + */ + protected JdbcTemplate createJdbcTemplate(DataSource dataSource) { + return new JdbcTemplate(dataSource); + } + + /** + * Return the JDBC DataSource used by this DAO. + */ + @Nullable + public final DataSource getDataSource() { + return (this.jdbcTemplate != null ? this.jdbcTemplate.getDataSource() : null); + } + + /** + * Set the JdbcTemplate for this DAO explicitly, + * as an alternative to specifying a DataSource. + */ + public final void setJdbcTemplate(@Nullable JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + initTemplateConfig(); + } + + /** + * Return the JdbcTemplate for this DAO, + * pre-initialized with the DataSource or set explicitly. + */ + @Nullable + public final JdbcTemplate getJdbcTemplate() { + return this.jdbcTemplate; + } + + /** + * Initialize the template-based configuration of this DAO. + * Called after a new JdbcTemplate has been set, either directly + * or through a DataSource. + *

    This implementation is empty. Subclasses may override this + * to configure further objects based on the JdbcTemplate. + * @see #getJdbcTemplate() + */ + protected void initTemplateConfig() { + } + + @Override + protected void checkDaoConfig() { + if (this.jdbcTemplate == null) { + throw new IllegalArgumentException("'dataSource' or 'jdbcTemplate' is required"); + } + } + + + /** + * Return the SQLExceptionTranslator of this DAO's JdbcTemplate, + * for translating SQLExceptions in custom JDBC access code. + * @see org.springframework.jdbc.core.JdbcTemplate#getExceptionTranslator() + */ + protected final SQLExceptionTranslator getExceptionTranslator() { + JdbcTemplate jdbcTemplate = getJdbcTemplate(); + Assert.state(jdbcTemplate != null, "No JdbcTemplate set"); + return jdbcTemplate.getExceptionTranslator(); + } + + /** + * Get a JDBC Connection, either from the current transaction or a new one. + * @return the JDBC Connection + * @throws CannotGetJdbcConnectionException if the attempt to get a Connection failed + * @see org.springframework.jdbc.datasource.DataSourceUtils#getConnection(javax.sql.DataSource) + */ + protected final Connection getConnection() throws CannotGetJdbcConnectionException { + DataSource dataSource = getDataSource(); + Assert.state(dataSource != null, "No DataSource set"); + return DataSourceUtils.getConnection(dataSource); + } + + /** + * Close the given JDBC Connection, created via this DAO's DataSource, + * if it isn't bound to the thread. + * @param con the Connection to close + * @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection + */ + protected final void releaseConnection(Connection con) { + DataSourceUtils.releaseConnection(con, getDataSource()); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlLobValue.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlLobValue.java new file mode 100644 index 0000000..44f027d --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlLobValue.java @@ -0,0 +1,220 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.support; + +import java.io.InputStream; +import java.io.Reader; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; + +import org.springframework.jdbc.core.DisposableSqlTypeValue; +import org.springframework.jdbc.support.lob.DefaultLobHandler; +import org.springframework.jdbc.support.lob.LobCreator; +import org.springframework.jdbc.support.lob.LobHandler; +import org.springframework.lang.Nullable; + +/** + * Object to represent an SQL BLOB/CLOB value parameter. BLOBs can either be an + * InputStream or a byte array. CLOBs can be in the form of a Reader, InputStream + * or String. Each CLOB/BLOB value will be stored together with its length. + * The type is based on which constructor is used. Objects of this class are + * immutable except for the LobCreator reference. Use them and discard them. + * + *

    This class holds a reference to a LocCreator that must be closed after the + * update has completed. This is done via a call to the closeLobCreator method. + * All handling of the LobCreator is done by the framework classes that use it - + * no need to set or close the LobCreator for end users of this class. + * + *

    A usage example: + * + *

    JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);  // reusable object
    + * LobHandler lobHandler = new DefaultLobHandler();  // reusable object
    + *
    + * jdbcTemplate.update(
    + *     "INSERT INTO imagedb (image_name, content, description) VALUES (?, ?, ?)",
    + *     new Object[] {
    + *       name,
    + *       new SqlLobValue(contentStream, contentLength, lobHandler),
    + *       new SqlLobValue(description, lobHandler)
    + *     },
    + *     new int[] {Types.VARCHAR, Types.BLOB, Types.CLOB});
    + * 
    + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 1.1 + * @see org.springframework.jdbc.support.lob.LobHandler + * @see org.springframework.jdbc.support.lob.LobCreator + * @see org.springframework.jdbc.core.JdbcTemplate#update(String, Object[], int[]) + * @see org.springframework.jdbc.object.SqlUpdate#update(Object[]) + * @see org.springframework.jdbc.object.StoredProcedure#execute(java.util.Map) + */ +public class SqlLobValue implements DisposableSqlTypeValue { + + @Nullable + private final Object content; + + private final int length; + + /** + * This contains a reference to the LobCreator - so we can close it + * once the update is done. + */ + private final LobCreator lobCreator; + + + /** + * Create a new BLOB value with the given byte array, + * using a DefaultLobHandler. + * @param bytes the byte array containing the BLOB value + * @see org.springframework.jdbc.support.lob.DefaultLobHandler + */ + public SqlLobValue(@Nullable byte[] bytes) { + this(bytes, new DefaultLobHandler()); + } + + /** + * Create a new BLOB value with the given byte array. + * @param bytes the byte array containing the BLOB value + * @param lobHandler the LobHandler to be used + */ + public SqlLobValue(@Nullable byte[] bytes, LobHandler lobHandler) { + this.content = bytes; + this.length = (bytes != null ? bytes.length : 0); + this.lobCreator = lobHandler.getLobCreator(); + } + + /** + * Create a new CLOB value with the given content string, + * using a DefaultLobHandler. + * @param content the String containing the CLOB value + * @see org.springframework.jdbc.support.lob.DefaultLobHandler + */ + public SqlLobValue(@Nullable String content) { + this(content, new DefaultLobHandler()); + } + + /** + * Create a new CLOB value with the given content string. + * @param content the String containing the CLOB value + * @param lobHandler the LobHandler to be used + */ + public SqlLobValue(@Nullable String content, LobHandler lobHandler) { + this.content = content; + this.length = (content != null ? content.length() : 0); + this.lobCreator = lobHandler.getLobCreator(); + } + + /** + * Create a new BLOB/CLOB value with the given stream, + * using a DefaultLobHandler. + * @param stream the stream containing the LOB value + * @param length the length of the LOB value + * @see org.springframework.jdbc.support.lob.DefaultLobHandler + */ + public SqlLobValue(InputStream stream, int length) { + this(stream, length, new DefaultLobHandler()); + } + + /** + * Create a new BLOB/CLOB value with the given stream. + * @param stream the stream containing the LOB value + * @param length the length of the LOB value + * @param lobHandler the LobHandler to be used + */ + public SqlLobValue(InputStream stream, int length, LobHandler lobHandler) { + this.content = stream; + this.length = length; + this.lobCreator = lobHandler.getLobCreator(); + } + + /** + * Create a new CLOB value with the given character stream, + * using a DefaultLobHandler. + * @param reader the character stream containing the CLOB value + * @param length the length of the CLOB value + * @see org.springframework.jdbc.support.lob.DefaultLobHandler + */ + public SqlLobValue(Reader reader, int length) { + this(reader, length, new DefaultLobHandler()); + } + + /** + * Create a new CLOB value with the given character stream. + * @param reader the character stream containing the CLOB value + * @param length the length of the CLOB value + * @param lobHandler the LobHandler to be used + */ + public SqlLobValue(Reader reader, int length, LobHandler lobHandler) { + this.content = reader; + this.length = length; + this.lobCreator = lobHandler.getLobCreator(); + } + + + /** + * Set the specified content via the LobCreator. + */ + @Override + public void setTypeValue(PreparedStatement ps, int paramIndex, int sqlType, @Nullable String typeName) + throws SQLException { + + if (sqlType == Types.BLOB) { + if (this.content instanceof byte[] || this.content == null) { + this.lobCreator.setBlobAsBytes(ps, paramIndex, (byte[]) this.content); + } + else if (this.content instanceof String) { + this.lobCreator.setBlobAsBytes(ps, paramIndex, ((String) this.content).getBytes()); + } + else if (this.content instanceof InputStream) { + this.lobCreator.setBlobAsBinaryStream(ps, paramIndex, (InputStream) this.content, this.length); + } + else { + throw new IllegalArgumentException( + "Content type [" + this.content.getClass().getName() + "] not supported for BLOB columns"); + } + } + else if (sqlType == Types.CLOB) { + if (this.content instanceof String || this.content == null) { + this.lobCreator.setClobAsString(ps, paramIndex, (String) this.content); + } + else if (this.content instanceof InputStream) { + this.lobCreator.setClobAsAsciiStream(ps, paramIndex, (InputStream) this.content, this.length); + } + else if (this.content instanceof Reader) { + this.lobCreator.setClobAsCharacterStream(ps, paramIndex, (Reader) this.content, this.length); + } + else { + throw new IllegalArgumentException( + "Content type [" + this.content.getClass().getName() + "] not supported for CLOB columns"); + } + } + else { + throw new IllegalArgumentException("SqlLobValue only supports SQL types BLOB and CLOB"); + } + } + + /** + * Close the LobCreator, if any. + */ + @Override + public void cleanup() { + this.lobCreator.close(); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/package-info.java new file mode 100644 index 0000000..7e0621e --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/package-info.java @@ -0,0 +1,10 @@ +/** + * Classes supporting the {@code org.springframework.jdbc.core} package. + * Contains a DAO base class for JdbcTemplate usage. + */ +@NonNullApi +@NonNullFields +package org.springframework.jdbc.core.support; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/AbstractDataSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/AbstractDataSource.java new file mode 100644 index 0000000..2bb62d1 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/AbstractDataSource.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource; + +import java.io.PrintWriter; +import java.sql.SQLException; +import java.util.logging.Logger; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Abstract base class for Spring's {@link javax.sql.DataSource} + * implementations, taking care of the padding. + * + *

    'Padding' in the context of this class means default implementations + * for certain methods from the {@code DataSource} interface, such as + * {@link #getLoginTimeout()}, {@link #setLoginTimeout(int)}, and so forth. + * + * @author Juergen Hoeller + * @since 07.05.2003 + * @see DriverManagerDataSource + */ +public abstract class AbstractDataSource implements DataSource { + + /** Logger available to subclasses. */ + protected final Log logger = LogFactory.getLog(getClass()); + + + /** + * Returns 0, indicating the default system timeout is to be used. + */ + @Override + public int getLoginTimeout() throws SQLException { + return 0; + } + + /** + * Setting a login timeout is not supported. + */ + @Override + public void setLoginTimeout(int timeout) throws SQLException { + throw new UnsupportedOperationException("setLoginTimeout"); + } + + /** + * LogWriter methods are not supported. + */ + @Override + public PrintWriter getLogWriter() { + throw new UnsupportedOperationException("getLogWriter"); + } + + /** + * LogWriter methods are not supported. + */ + @Override + public void setLogWriter(PrintWriter pw) throws SQLException { + throw new UnsupportedOperationException("setLogWriter"); + } + + + //--------------------------------------------------------------------- + // Implementation of JDBC 4.0's Wrapper interface + //--------------------------------------------------------------------- + + @Override + @SuppressWarnings("unchecked") + public T unwrap(Class iface) throws SQLException { + if (iface.isInstance(this)) { + return (T) this; + } + throw new SQLException("DataSource of type [" + getClass().getName() + + "] cannot be unwrapped as [" + iface.getName() + "]"); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return iface.isInstance(this); + } + + + //--------------------------------------------------------------------- + // Implementation of JDBC 4.1's getParentLogger method + //--------------------------------------------------------------------- + + @Override + public Logger getParentLogger() { + return Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/AbstractDriverBasedDataSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/AbstractDriverBasedDataSource.java new file mode 100644 index 0000000..df6df3e --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/AbstractDriverBasedDataSource.java @@ -0,0 +1,224 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Properties; + +import org.springframework.lang.Nullable; + +/** + * Abstract base class for JDBC {@link javax.sql.DataSource} implementations + * that operate on a JDBC {@link java.sql.Driver}. + * + * @author Juergen Hoeller + * @since 2.5.5 + * @see SimpleDriverDataSource + * @see DriverManagerDataSource + */ +public abstract class AbstractDriverBasedDataSource extends AbstractDataSource { + + @Nullable + private String url; + + @Nullable + private String username; + + @Nullable + private String password; + + @Nullable + private String catalog; + + @Nullable + private String schema; + + @Nullable + private Properties connectionProperties; + + + /** + * Set the JDBC URL to use for connecting through the Driver. + * @see java.sql.Driver#connect(String, java.util.Properties) + */ + public void setUrl(@Nullable String url) { + this.url = (url != null ? url.trim() : null); + } + + /** + * Return the JDBC URL to use for connecting through the Driver. + */ + @Nullable + public String getUrl() { + return this.url; + } + + /** + * Set the JDBC username to use for connecting through the Driver. + * @see java.sql.Driver#connect(String, java.util.Properties) + */ + public void setUsername(@Nullable String username) { + this.username = username; + } + + /** + * Return the JDBC username to use for connecting through the Driver. + */ + @Nullable + public String getUsername() { + return this.username; + } + + /** + * Set the JDBC password to use for connecting through the Driver. + * @see java.sql.Driver#connect(String, java.util.Properties) + */ + public void setPassword(@Nullable String password) { + this.password = password; + } + + /** + * Return the JDBC password to use for connecting through the Driver. + */ + @Nullable + public String getPassword() { + return this.password; + } + + /** + * Specify a database catalog to be applied to each Connection. + * @since 4.3.2 + * @see Connection#setCatalog + */ + public void setCatalog(@Nullable String catalog) { + this.catalog = catalog; + } + + /** + * Return the database catalog to be applied to each Connection, if any. + * @since 4.3.2 + */ + @Nullable + public String getCatalog() { + return this.catalog; + } + + /** + * Specify a database schema to be applied to each Connection. + * @since 4.3.2 + * @see Connection#setSchema + */ + public void setSchema(@Nullable String schema) { + this.schema = schema; + } + + /** + * Return the database schema to be applied to each Connection, if any. + * @since 4.3.2 + */ + @Nullable + public String getSchema() { + return this.schema; + } + + /** + * Specify arbitrary connection properties as key/value pairs, + * to be passed to the Driver. + *

    Can also contain "user" and "password" properties. However, + * any "username" and "password" bean properties specified on this + * DataSource will override the corresponding connection properties. + * @see java.sql.Driver#connect(String, java.util.Properties) + */ + public void setConnectionProperties(@Nullable Properties connectionProperties) { + this.connectionProperties = connectionProperties; + } + + /** + * Return the connection properties to be passed to the Driver, if any. + */ + @Nullable + public Properties getConnectionProperties() { + return this.connectionProperties; + } + + + /** + * This implementation delegates to {@code getConnectionFromDriver}, + * using the default username and password of this DataSource. + * @see #getConnectionFromDriver(String, String) + * @see #setUsername + * @see #setPassword + */ + @Override + public Connection getConnection() throws SQLException { + return getConnectionFromDriver(getUsername(), getPassword()); + } + + /** + * This implementation delegates to {@code getConnectionFromDriver}, + * using the given username and password. + * @see #getConnectionFromDriver(String, String) + */ + @Override + public Connection getConnection(String username, String password) throws SQLException { + return getConnectionFromDriver(username, password); + } + + + /** + * Build properties for the Driver, including the given username and password (if any), + * and obtain a corresponding Connection. + * @param username the name of the user + * @param password the password to use + * @return the obtained Connection + * @throws SQLException in case of failure + * @see java.sql.Driver#connect(String, java.util.Properties) + */ + protected Connection getConnectionFromDriver(@Nullable String username, @Nullable String password) throws SQLException { + Properties mergedProps = new Properties(); + Properties connProps = getConnectionProperties(); + if (connProps != null) { + mergedProps.putAll(connProps); + } + if (username != null) { + mergedProps.setProperty("user", username); + } + if (password != null) { + mergedProps.setProperty("password", password); + } + + Connection con = getConnectionFromDriver(mergedProps); + if (this.catalog != null) { + con.setCatalog(this.catalog); + } + if (this.schema != null) { + con.setSchema(this.schema); + } + return con; + } + + /** + * Obtain a Connection using the given properties. + *

    Template method to be implemented by subclasses. + * @param props the merged connection properties + * @return the obtained Connection + * @throws SQLException in case of failure + */ + protected abstract Connection getConnectionFromDriver(Properties props) throws SQLException; + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/ConnectionHandle.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/ConnectionHandle.java new file mode 100644 index 0000000..dd0cd1e --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/ConnectionHandle.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource; + +import java.sql.Connection; + +/** + * Simple interface to be implemented by handles for a JDBC Connection. + * Used by JpaDialect, for example. + * + * @author Juergen Hoeller + * @since 1.1 + * @see SimpleConnectionHandle + * @see ConnectionHolder + */ +@FunctionalInterface +public interface ConnectionHandle { + + /** + * Fetch the JDBC Connection that this handle refers to. + */ + Connection getConnection(); + + /** + * Release the JDBC Connection that this handle refers to. + *

    The default implementation is empty, assuming that the lifecycle + * of the connection is managed externally. + * @param con the JDBC Connection to release + */ + default void releaseConnection(Connection con) { + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/ConnectionHolder.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/ConnectionHolder.java new file mode 100644 index 0000000..4f3d1a7 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/ConnectionHolder.java @@ -0,0 +1,217 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Savepoint; + +import org.springframework.lang.Nullable; +import org.springframework.transaction.support.ResourceHolderSupport; +import org.springframework.util.Assert; + +/** + * Resource holder wrapping a JDBC {@link Connection}. + * {@link DataSourceTransactionManager} binds instances of this class + * to the thread, for a specific {@link javax.sql.DataSource}. + * + *

    Inherits rollback-only support for nested JDBC transactions + * and reference count functionality from the base class. + * + *

    Note: This is an SPI class, not intended to be used by applications. + * + * @author Juergen Hoeller + * @since 06.05.2003 + * @see DataSourceTransactionManager + * @see DataSourceUtils + */ +public class ConnectionHolder extends ResourceHolderSupport { + + /** + * Prefix for savepoint names. + */ + public static final String SAVEPOINT_NAME_PREFIX = "SAVEPOINT_"; + + + @Nullable + private ConnectionHandle connectionHandle; + + @Nullable + private Connection currentConnection; + + private boolean transactionActive = false; + + @Nullable + private Boolean savepointsSupported; + + private int savepointCounter = 0; + + + /** + * Create a new ConnectionHolder for the given ConnectionHandle. + * @param connectionHandle the ConnectionHandle to hold + */ + public ConnectionHolder(ConnectionHandle connectionHandle) { + Assert.notNull(connectionHandle, "ConnectionHandle must not be null"); + this.connectionHandle = connectionHandle; + } + + /** + * Create a new ConnectionHolder for the given JDBC Connection, + * wrapping it with a {@link SimpleConnectionHandle}, + * assuming that there is no ongoing transaction. + * @param connection the JDBC Connection to hold + * @see SimpleConnectionHandle + * @see #ConnectionHolder(java.sql.Connection, boolean) + */ + public ConnectionHolder(Connection connection) { + this.connectionHandle = new SimpleConnectionHandle(connection); + } + + /** + * Create a new ConnectionHolder for the given JDBC Connection, + * wrapping it with a {@link SimpleConnectionHandle}. + * @param connection the JDBC Connection to hold + * @param transactionActive whether the given Connection is involved + * in an ongoing transaction + * @see SimpleConnectionHandle + */ + public ConnectionHolder(Connection connection, boolean transactionActive) { + this(connection); + this.transactionActive = transactionActive; + } + + + /** + * Return the ConnectionHandle held by this ConnectionHolder. + */ + @Nullable + public ConnectionHandle getConnectionHandle() { + return this.connectionHandle; + } + + /** + * Return whether this holder currently has a Connection. + */ + protected boolean hasConnection() { + return (this.connectionHandle != null); + } + + /** + * Set whether this holder represents an active, JDBC-managed transaction. + * @see DataSourceTransactionManager + */ + protected void setTransactionActive(boolean transactionActive) { + this.transactionActive = transactionActive; + } + + /** + * Return whether this holder represents an active, JDBC-managed transaction. + */ + protected boolean isTransactionActive() { + return this.transactionActive; + } + + + /** + * Override the existing Connection handle with the given Connection. + * Reset the handle if given {@code null}. + *

    Used for releasing the Connection on suspend (with a {@code null} + * argument) and setting a fresh Connection on resume. + */ + protected void setConnection(@Nullable Connection connection) { + if (this.currentConnection != null) { + if (this.connectionHandle != null) { + this.connectionHandle.releaseConnection(this.currentConnection); + } + this.currentConnection = null; + } + if (connection != null) { + this.connectionHandle = new SimpleConnectionHandle(connection); + } + else { + this.connectionHandle = null; + } + } + + /** + * Return the current Connection held by this ConnectionHolder. + *

    This will be the same Connection until {@code released} + * gets called on the ConnectionHolder, which will reset the + * held Connection, fetching a new Connection on demand. + * @see ConnectionHandle#getConnection() + * @see #released() + */ + public Connection getConnection() { + Assert.notNull(this.connectionHandle, "Active Connection is required"); + if (this.currentConnection == null) { + this.currentConnection = this.connectionHandle.getConnection(); + } + return this.currentConnection; + } + + /** + * Return whether JDBC 3.0 Savepoints are supported. + * Caches the flag for the lifetime of this ConnectionHolder. + * @throws SQLException if thrown by the JDBC driver + */ + public boolean supportsSavepoints() throws SQLException { + if (this.savepointsSupported == null) { + this.savepointsSupported = getConnection().getMetaData().supportsSavepoints(); + } + return this.savepointsSupported; + } + + /** + * Create a new JDBC 3.0 Savepoint for the current Connection, + * using generated savepoint names that are unique for the Connection. + * @return the new Savepoint + * @throws SQLException if thrown by the JDBC driver + */ + public Savepoint createSavepoint() throws SQLException { + this.savepointCounter++; + return getConnection().setSavepoint(SAVEPOINT_NAME_PREFIX + this.savepointCounter); + } + + /** + * Releases the current Connection held by this ConnectionHolder. + *

    This is necessary for ConnectionHandles that expect "Connection borrowing", + * where each returned Connection is only temporarily leased and needs to be + * returned once the data operation is done, to make the Connection available + * for other operations within the same transaction. + */ + @Override + public void released() { + super.released(); + if (!isOpen() && this.currentConnection != null) { + if (this.connectionHandle != null) { + this.connectionHandle.releaseConnection(this.currentConnection); + } + this.currentConnection = null; + } + } + + + @Override + public void clear() { + super.clear(); + this.transactionActive = false; + this.savepointsSupported = null; + this.savepointCounter = 0; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/ConnectionProxy.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/ConnectionProxy.java new file mode 100644 index 0000000..05db7b0 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/ConnectionProxy.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource; + +import java.sql.Connection; + +/** + * Subinterface of {@link java.sql.Connection} to be implemented by + * Connection proxies. Allows access to the underlying target Connection. + * + *

    This interface can be checked when there is a need to cast to a + * native JDBC Connection such as Oracle's OracleConnection. Alternatively, + * all such connections also support JDBC 4.0's {@link Connection#unwrap}. + * + * @author Juergen Hoeller + * @since 1.1 + * @see TransactionAwareDataSourceProxy + * @see LazyConnectionDataSourceProxy + * @see DataSourceUtils#getTargetConnection(java.sql.Connection) + */ +public interface ConnectionProxy extends Connection { + + /** + * Return the target Connection of this proxy. + *

    This will typically be the native driver Connection + * or a wrapper from a connection pool. + * @return the underlying Connection (never {@code null}) + */ + Connection getTargetConnection(); + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java new file mode 100644 index 0000000..d2f619a --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java @@ -0,0 +1,483 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.lang.Nullable; +import org.springframework.transaction.CannotCreateTransactionException; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionSystemException; +import org.springframework.transaction.support.AbstractPlatformTransactionManager; +import org.springframework.transaction.support.DefaultTransactionStatus; +import org.springframework.transaction.support.ResourceTransactionManager; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.transaction.support.TransactionSynchronizationUtils; +import org.springframework.util.Assert; + +/** + * {@link org.springframework.transaction.PlatformTransactionManager} + * implementation for a single JDBC {@link javax.sql.DataSource}. This class is + * capable of working in any environment with any JDBC driver, as long as the setup + * uses a {@code javax.sql.DataSource} as its {@code Connection} factory mechanism. + * Binds a JDBC Connection from the specified DataSource to the current thread, + * potentially allowing for one thread-bound Connection per DataSource. + * + *

    Note: The DataSource that this transaction manager operates on needs + * to return independent Connections. The Connections may come from a pool + * (the typical case), but the DataSource must not return thread-scoped / + * request-scoped Connections or the like. This transaction manager will + * associate Connections with thread-bound transactions itself, according + * to the specified propagation behavior. It assumes that a separate, + * independent Connection can be obtained even during an ongoing transaction. + * + *

    Application code is required to retrieve the JDBC Connection via + * {@link DataSourceUtils#getConnection(DataSource)} instead of a standard + * Java EE-style {@link DataSource#getConnection()} call. Spring classes such as + * {@link org.springframework.jdbc.core.JdbcTemplate} use this strategy implicitly. + * If not used in combination with this transaction manager, the + * {@link DataSourceUtils} lookup strategy behaves exactly like the native + * DataSource lookup; it can thus be used in a portable fashion. + * + *

    Alternatively, you can allow application code to work with the standard + * Java EE-style lookup pattern {@link DataSource#getConnection()}, for example for + * legacy code that is not aware of Spring at all. In that case, define a + * {@link TransactionAwareDataSourceProxy} for your target DataSource, and pass + * that proxy DataSource to your DAOs, which will automatically participate in + * Spring-managed transactions when accessing it. + * + *

    Supports custom isolation levels, and timeouts which get applied as + * appropriate JDBC statement timeouts. To support the latter, application code + * must either use {@link org.springframework.jdbc.core.JdbcTemplate}, call + * {@link DataSourceUtils#applyTransactionTimeout} for each created JDBC Statement, + * or go through a {@link TransactionAwareDataSourceProxy} which will create + * timeout-aware JDBC Connections and Statements automatically. + * + *

    Consider defining a {@link LazyConnectionDataSourceProxy} for your target + * DataSource, pointing both this transaction manager and your DAOs to it. + * This will lead to optimized handling of "empty" transactions, i.e. of transactions + * without any JDBC statements executed. A LazyConnectionDataSourceProxy will not fetch + * an actual JDBC Connection from the target DataSource until a Statement gets executed, + * lazily applying the specified transaction settings to the target Connection. + * + *

    This transaction manager supports nested transactions via the JDBC 3.0 + * {@link java.sql.Savepoint} mechanism. The + * {@link #setNestedTransactionAllowed "nestedTransactionAllowed"} flag defaults + * to "true", since nested transactions will work without restrictions on JDBC + * drivers that support savepoints (such as the Oracle JDBC driver). + * + *

    This transaction manager can be used as a replacement for the + * {@link org.springframework.transaction.jta.JtaTransactionManager} in the single + * resource case, as it does not require a container that supports JTA, typically + * in combination with a locally defined JDBC DataSource (e.g. an Apache Commons + * DBCP connection pool). Switching between this local strategy and a JTA + * environment is just a matter of configuration! + * + *

    As of 4.3.4, this transaction manager triggers flush callbacks on registered + * transaction synchronizations (if synchronization is generally active), assuming + * resources operating on the underlying JDBC {@code Connection}. This allows for + * setup analogous to {@code JtaTransactionManager}, in particular with respect to + * lazily registered ORM resources (e.g. a Hibernate {@code Session}). + * + *

    NOTE: As of 5.3, {@link org.springframework.jdbc.support.JdbcTransactionManager} + * is available as an extended subclass which includes commit/rollback exception + * translation, aligned with {@link org.springframework.jdbc.core.JdbcTemplate}. + * + * @author Juergen Hoeller + * @since 02.05.2003 + * @see #setNestedTransactionAllowed + * @see java.sql.Savepoint + * @see DataSourceUtils#getConnection(javax.sql.DataSource) + * @see DataSourceUtils#applyTransactionTimeout + * @see DataSourceUtils#releaseConnection + * @see TransactionAwareDataSourceProxy + * @see LazyConnectionDataSourceProxy + * @see org.springframework.jdbc.core.JdbcTemplate + */ +@SuppressWarnings("serial") +public class DataSourceTransactionManager extends AbstractPlatformTransactionManager + implements ResourceTransactionManager, InitializingBean { + + @Nullable + private DataSource dataSource; + + private boolean enforceReadOnly = false; + + + /** + * Create a new DataSourceTransactionManager instance. + * A DataSource has to be set to be able to use it. + * @see #setDataSource + */ + public DataSourceTransactionManager() { + setNestedTransactionAllowed(true); + } + + /** + * Create a new DataSourceTransactionManager instance. + * @param dataSource the JDBC DataSource to manage transactions for + */ + public DataSourceTransactionManager(DataSource dataSource) { + this(); + setDataSource(dataSource); + afterPropertiesSet(); + } + + + /** + * Set the JDBC DataSource that this instance should manage transactions for. + *

    This will typically be a locally defined DataSource, for example an + * Apache Commons DBCP connection pool. Alternatively, you can also drive + * transactions for a non-XA J2EE DataSource fetched from JNDI. For an XA + * DataSource, use JtaTransactionManager. + *

    The DataSource specified here should be the target DataSource to manage + * transactions for, not a TransactionAwareDataSourceProxy. Only data access + * code may work with TransactionAwareDataSourceProxy, while the transaction + * manager needs to work on the underlying target DataSource. If there's + * nevertheless a TransactionAwareDataSourceProxy passed in, it will be + * unwrapped to extract its target DataSource. + *

    The DataSource passed in here needs to return independent Connections. + * The Connections may come from a pool (the typical case), but the DataSource + * must not return thread-scoped / request-scoped Connections or the like. + * @see TransactionAwareDataSourceProxy + * @see org.springframework.transaction.jta.JtaTransactionManager + */ + public void setDataSource(@Nullable DataSource dataSource) { + if (dataSource instanceof TransactionAwareDataSourceProxy) { + // If we got a TransactionAwareDataSourceProxy, we need to perform transactions + // for its underlying target DataSource, else data access code won't see + // properly exposed transactions (i.e. transactions for the target DataSource). + this.dataSource = ((TransactionAwareDataSourceProxy) dataSource).getTargetDataSource(); + } + else { + this.dataSource = dataSource; + } + } + + /** + * Return the JDBC DataSource that this instance manages transactions for. + */ + @Nullable + public DataSource getDataSource() { + return this.dataSource; + } + + /** + * Obtain the DataSource for actual use. + * @return the DataSource (never {@code null}) + * @throws IllegalStateException in case of no DataSource set + * @since 5.0 + */ + protected DataSource obtainDataSource() { + DataSource dataSource = getDataSource(); + Assert.state(dataSource != null, "No DataSource set"); + return dataSource; + } + + /** + * Specify whether to enforce the read-only nature of a transaction + * (as indicated by {@link TransactionDefinition#isReadOnly()} + * through an explicit statement on the transactional connection: + * "SET TRANSACTION READ ONLY" as understood by Oracle, MySQL and Postgres. + *

    The exact treatment, including any SQL statement executed on the connection, + * can be customized through {@link #prepareTransactionalConnection}. + *

    This mode of read-only handling goes beyond the {@link Connection#setReadOnly} + * hint that Spring applies by default. In contrast to that standard JDBC hint, + * "SET TRANSACTION READ ONLY" enforces an isolation-level-like connection mode + * where data manipulation statements are strictly disallowed. Also, on Oracle, + * this read-only mode provides read consistency for the entire transaction. + *

    Note that older Oracle JDBC drivers (9i, 10g) used to enforce this read-only + * mode even for {@code Connection.setReadOnly(true}. However, with recent drivers, + * this strong enforcement needs to be applied explicitly, e.g. through this flag. + * @since 4.3.7 + * @see #prepareTransactionalConnection + */ + public void setEnforceReadOnly(boolean enforceReadOnly) { + this.enforceReadOnly = enforceReadOnly; + } + + /** + * Return whether to enforce the read-only nature of a transaction + * through an explicit statement on the transactional connection. + * @since 4.3.7 + * @see #setEnforceReadOnly + */ + public boolean isEnforceReadOnly() { + return this.enforceReadOnly; + } + + @Override + public void afterPropertiesSet() { + if (getDataSource() == null) { + throw new IllegalArgumentException("Property 'dataSource' is required"); + } + } + + + @Override + public Object getResourceFactory() { + return obtainDataSource(); + } + + @Override + protected Object doGetTransaction() { + DataSourceTransactionObject txObject = new DataSourceTransactionObject(); + txObject.setSavepointAllowed(isNestedTransactionAllowed()); + ConnectionHolder conHolder = + (ConnectionHolder) TransactionSynchronizationManager.getResource(obtainDataSource()); + txObject.setConnectionHolder(conHolder, false); + return txObject; + } + + @Override + protected boolean isExistingTransaction(Object transaction) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + return (txObject.hasConnectionHolder() && txObject.getConnectionHolder().isTransactionActive()); + } + + @Override + protected void doBegin(Object transaction, TransactionDefinition definition) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + Connection con = null; + + try { + if (!txObject.hasConnectionHolder() || + txObject.getConnectionHolder().isSynchronizedWithTransaction()) { + Connection newCon = obtainDataSource().getConnection(); + if (logger.isDebugEnabled()) { + logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction"); + } + txObject.setConnectionHolder(new ConnectionHolder(newCon), true); + } + + txObject.getConnectionHolder().setSynchronizedWithTransaction(true); + con = txObject.getConnectionHolder().getConnection(); + + Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition); + txObject.setPreviousIsolationLevel(previousIsolationLevel); + txObject.setReadOnly(definition.isReadOnly()); + + // Switch to manual commit if necessary. This is very expensive in some JDBC drivers, + // so we don't want to do it unnecessarily (for example if we've explicitly + // configured the connection pool to set it already). + if (con.getAutoCommit()) { + txObject.setMustRestoreAutoCommit(true); + if (logger.isDebugEnabled()) { + logger.debug("Switching JDBC Connection [" + con + "] to manual commit"); + } + con.setAutoCommit(false); + } + + prepareTransactionalConnection(con, definition); + txObject.getConnectionHolder().setTransactionActive(true); + + int timeout = determineTimeout(definition); + if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) { + txObject.getConnectionHolder().setTimeoutInSeconds(timeout); + } + + // Bind the connection holder to the thread. + if (txObject.isNewConnectionHolder()) { + TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder()); + } + } + + catch (Throwable ex) { + if (txObject.isNewConnectionHolder()) { + DataSourceUtils.releaseConnection(con, obtainDataSource()); + txObject.setConnectionHolder(null, false); + } + throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex); + } + } + + @Override + protected Object doSuspend(Object transaction) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + txObject.setConnectionHolder(null); + return TransactionSynchronizationManager.unbindResource(obtainDataSource()); + } + + @Override + protected void doResume(@Nullable Object transaction, Object suspendedResources) { + TransactionSynchronizationManager.bindResource(obtainDataSource(), suspendedResources); + } + + @Override + protected void doCommit(DefaultTransactionStatus status) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); + Connection con = txObject.getConnectionHolder().getConnection(); + if (status.isDebug()) { + logger.debug("Committing JDBC transaction on Connection [" + con + "]"); + } + try { + con.commit(); + } + catch (SQLException ex) { + throw translateException("JDBC commit", ex); + } + } + + @Override + protected void doRollback(DefaultTransactionStatus status) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); + Connection con = txObject.getConnectionHolder().getConnection(); + if (status.isDebug()) { + logger.debug("Rolling back JDBC transaction on Connection [" + con + "]"); + } + try { + con.rollback(); + } + catch (SQLException ex) { + throw translateException("JDBC rollback", ex); + } + } + + @Override + protected void doSetRollbackOnly(DefaultTransactionStatus status) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); + if (status.isDebug()) { + logger.debug("Setting JDBC transaction [" + txObject.getConnectionHolder().getConnection() + + "] rollback-only"); + } + txObject.setRollbackOnly(); + } + + @Override + protected void doCleanupAfterCompletion(Object transaction) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + + // Remove the connection holder from the thread, if exposed. + if (txObject.isNewConnectionHolder()) { + TransactionSynchronizationManager.unbindResource(obtainDataSource()); + } + + // Reset connection. + Connection con = txObject.getConnectionHolder().getConnection(); + try { + if (txObject.isMustRestoreAutoCommit()) { + con.setAutoCommit(true); + } + DataSourceUtils.resetConnectionAfterTransaction( + con, txObject.getPreviousIsolationLevel(), txObject.isReadOnly()); + } + catch (Throwable ex) { + logger.debug("Could not reset JDBC Connection after transaction", ex); + } + + if (txObject.isNewConnectionHolder()) { + if (logger.isDebugEnabled()) { + logger.debug("Releasing JDBC Connection [" + con + "] after transaction"); + } + DataSourceUtils.releaseConnection(con, this.dataSource); + } + + txObject.getConnectionHolder().clear(); + } + + + /** + * Prepare the transactional {@code Connection} right after transaction begin. + *

    The default implementation executes a "SET TRANSACTION READ ONLY" statement + * if the {@link #setEnforceReadOnly "enforceReadOnly"} flag is set to {@code true} + * and the transaction definition indicates a read-only transaction. + *

    The "SET TRANSACTION READ ONLY" is understood by Oracle, MySQL and Postgres + * and may work with other databases as well. If you'd like to adapt this treatment, + * override this method accordingly. + * @param con the transactional JDBC Connection + * @param definition the current transaction definition + * @throws SQLException if thrown by JDBC API + * @since 4.3.7 + * @see #setEnforceReadOnly + */ + protected void prepareTransactionalConnection(Connection con, TransactionDefinition definition) + throws SQLException { + + if (isEnforceReadOnly() && definition.isReadOnly()) { + try (Statement stmt = con.createStatement()) { + stmt.executeUpdate("SET TRANSACTION READ ONLY"); + } + } + } + + /** + * Translate the given JDBC commit/rollback exception to a common Spring + * exception to propagate from the {@link #commit}/{@link #rollback} call. + *

    The default implementation throws a {@link TransactionSystemException}. + * Subclasses may specifically identify concurrency failures etc. + * @param task the task description (commit or rollback) + * @param ex the SQLException thrown from commit/rollback + * @return the translated exception to throw, either a + * {@link org.springframework.dao.DataAccessException} or a + * {@link org.springframework.transaction.TransactionException} + * @since 5.3 + */ + protected RuntimeException translateException(String task, SQLException ex) { + return new TransactionSystemException(task + " failed", ex); + } + + + /** + * DataSource transaction object, representing a ConnectionHolder. + * Used as transaction object by DataSourceTransactionManager. + */ + private static class DataSourceTransactionObject extends JdbcTransactionObjectSupport { + + private boolean newConnectionHolder; + + private boolean mustRestoreAutoCommit; + + public void setConnectionHolder(@Nullable ConnectionHolder connectionHolder, boolean newConnectionHolder) { + super.setConnectionHolder(connectionHolder); + this.newConnectionHolder = newConnectionHolder; + } + + public boolean isNewConnectionHolder() { + return this.newConnectionHolder; + } + + public void setMustRestoreAutoCommit(boolean mustRestoreAutoCommit) { + this.mustRestoreAutoCommit = mustRestoreAutoCommit; + } + + public boolean isMustRestoreAutoCommit() { + return this.mustRestoreAutoCommit; + } + + public void setRollbackOnly() { + getConnectionHolder().setRollbackOnly(); + } + + @Override + public boolean isRollbackOnly() { + return getConnectionHolder().isRollbackOnly(); + } + + @Override + public void flush() { + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationUtils.triggerFlush(); + } + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java new file mode 100644 index 0000000..67c8338 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java @@ -0,0 +1,548 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.jdbc.CannotGetJdbcConnectionException; +import org.springframework.lang.Nullable; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.Assert; + +/** + * Helper class that provides static methods for obtaining JDBC Connections from + * a {@link javax.sql.DataSource}. Includes special support for Spring-managed + * transactional Connections, e.g. managed by {@link DataSourceTransactionManager} + * or {@link org.springframework.transaction.jta.JtaTransactionManager}. + * + *

    Used internally by Spring's {@link org.springframework.jdbc.core.JdbcTemplate}, + * Spring's JDBC operation objects and the JDBC {@link DataSourceTransactionManager}. + * Can also be used directly in application code. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #getConnection + * @see #releaseConnection + * @see DataSourceTransactionManager + * @see org.springframework.transaction.jta.JtaTransactionManager + * @see org.springframework.transaction.support.TransactionSynchronizationManager + */ +public abstract class DataSourceUtils { + + /** + * Order value for TransactionSynchronization objects that clean up JDBC Connections. + */ + public static final int CONNECTION_SYNCHRONIZATION_ORDER = 1000; + + private static final Log logger = LogFactory.getLog(DataSourceUtils.class); + + + /** + * Obtain a Connection from the given DataSource. Translates SQLExceptions into + * the Spring hierarchy of unchecked generic data access exceptions, simplifying + * calling code and making any exception that is thrown more meaningful. + *

    Is aware of a corresponding Connection bound to the current thread, for example + * when using {@link DataSourceTransactionManager}. Will bind a Connection to the + * thread if transaction synchronization is active, e.g. when running within a + * {@link org.springframework.transaction.jta.JtaTransactionManager JTA} transaction). + * @param dataSource the DataSource to obtain Connections from + * @return a JDBC Connection from the given DataSource + * @throws org.springframework.jdbc.CannotGetJdbcConnectionException + * if the attempt to get a Connection failed + * @see #releaseConnection + */ + public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException { + try { + return doGetConnection(dataSource); + } + catch (SQLException ex) { + throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection", ex); + } + catch (IllegalStateException ex) { + throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection: " + ex.getMessage()); + } + } + + /** + * Actually obtain a JDBC Connection from the given DataSource. + * Same as {@link #getConnection}, but throwing the original SQLException. + *

    Is aware of a corresponding Connection bound to the current thread, for example + * when using {@link DataSourceTransactionManager}. Will bind a Connection to the thread + * if transaction synchronization is active (e.g. if in a JTA transaction). + *

    Directly accessed by {@link TransactionAwareDataSourceProxy}. + * @param dataSource the DataSource to obtain Connections from + * @return a JDBC Connection from the given DataSource + * @throws SQLException if thrown by JDBC methods + * @see #doReleaseConnection + */ + public static Connection doGetConnection(DataSource dataSource) throws SQLException { + Assert.notNull(dataSource, "No DataSource specified"); + + ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource); + if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) { + conHolder.requested(); + if (!conHolder.hasConnection()) { + logger.debug("Fetching resumed JDBC Connection from DataSource"); + conHolder.setConnection(fetchConnection(dataSource)); + } + return conHolder.getConnection(); + } + // Else we either got no holder or an empty thread-bound holder here. + + logger.debug("Fetching JDBC Connection from DataSource"); + Connection con = fetchConnection(dataSource); + + if (TransactionSynchronizationManager.isSynchronizationActive()) { + try { + // Use same Connection for further JDBC actions within the transaction. + // Thread-bound object will get removed by synchronization at transaction completion. + ConnectionHolder holderToUse = conHolder; + if (holderToUse == null) { + holderToUse = new ConnectionHolder(con); + } + else { + holderToUse.setConnection(con); + } + holderToUse.requested(); + TransactionSynchronizationManager.registerSynchronization( + new ConnectionSynchronization(holderToUse, dataSource)); + holderToUse.setSynchronizedWithTransaction(true); + if (holderToUse != conHolder) { + TransactionSynchronizationManager.bindResource(dataSource, holderToUse); + } + } + catch (RuntimeException ex) { + // Unexpected exception from external delegation call -> close Connection and rethrow. + releaseConnection(con, dataSource); + throw ex; + } + } + + return con; + } + + /** + * Actually fetch a {@link Connection} from the given {@link DataSource}, + * defensively turning an unexpected {@code null} return value from + * {@link DataSource#getConnection()} into an {@link IllegalStateException}. + * @param dataSource the DataSource to obtain Connections from + * @return a JDBC Connection from the given DataSource (never {@code null}) + * @throws SQLException if thrown by JDBC methods + * @throws IllegalStateException if the DataSource returned a null value + * @see DataSource#getConnection() + */ + private static Connection fetchConnection(DataSource dataSource) throws SQLException { + Connection con = dataSource.getConnection(); + if (con == null) { + throw new IllegalStateException("DataSource returned null from getConnection(): " + dataSource); + } + return con; + } + + /** + * Prepare the given Connection with the given transaction semantics. + * @param con the Connection to prepare + * @param definition the transaction definition to apply + * @return the previous isolation level, if any + * @throws SQLException if thrown by JDBC methods + * @see #resetConnectionAfterTransaction + * @see Connection#setTransactionIsolation + * @see Connection#setReadOnly + */ + @Nullable + public static Integer prepareConnectionForTransaction(Connection con, @Nullable TransactionDefinition definition) + throws SQLException { + + Assert.notNull(con, "No Connection specified"); + + boolean debugEnabled = logger.isDebugEnabled(); + // Set read-only flag. + if (definition != null && definition.isReadOnly()) { + try { + if (debugEnabled) { + logger.debug("Setting JDBC Connection [" + con + "] read-only"); + } + con.setReadOnly(true); + } + catch (SQLException | RuntimeException ex) { + Throwable exToCheck = ex; + while (exToCheck != null) { + if (exToCheck.getClass().getSimpleName().contains("Timeout")) { + // Assume it's a connection timeout that would otherwise get lost: e.g. from JDBC 4.0 + throw ex; + } + exToCheck = exToCheck.getCause(); + } + // "read-only not supported" SQLException -> ignore, it's just a hint anyway + logger.debug("Could not set JDBC Connection read-only", ex); + } + } + + // Apply specific isolation level, if any. + Integer previousIsolationLevel = null; + if (definition != null && definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) { + if (debugEnabled) { + logger.debug("Changing isolation level of JDBC Connection [" + con + "] to " + + definition.getIsolationLevel()); + } + int currentIsolation = con.getTransactionIsolation(); + if (currentIsolation != definition.getIsolationLevel()) { + previousIsolationLevel = currentIsolation; + con.setTransactionIsolation(definition.getIsolationLevel()); + } + } + + return previousIsolationLevel; + } + + /** + * Reset the given Connection after a transaction, + * regarding read-only flag and isolation level. + * @param con the Connection to reset + * @param previousIsolationLevel the isolation level to restore, if any + * @param resetReadOnly whether to reset the connection's read-only flag + * @since 5.2.1 + * @see #prepareConnectionForTransaction + * @see Connection#setTransactionIsolation + * @see Connection#setReadOnly + */ + public static void resetConnectionAfterTransaction( + Connection con, @Nullable Integer previousIsolationLevel, boolean resetReadOnly) { + + Assert.notNull(con, "No Connection specified"); + boolean debugEnabled = logger.isDebugEnabled(); + try { + // Reset transaction isolation to previous value, if changed for the transaction. + if (previousIsolationLevel != null) { + if (debugEnabled) { + logger.debug("Resetting isolation level of JDBC Connection [" + + con + "] to " + previousIsolationLevel); + } + con.setTransactionIsolation(previousIsolationLevel); + } + + // Reset read-only flag if we originally switched it to true on transaction begin. + if (resetReadOnly) { + if (debugEnabled) { + logger.debug("Resetting read-only flag of JDBC Connection [" + con + "]"); + } + con.setReadOnly(false); + } + } + catch (Throwable ex) { + logger.debug("Could not reset JDBC Connection after transaction", ex); + } + } + + /** + * Reset the given Connection after a transaction, + * regarding read-only flag and isolation level. + * @param con the Connection to reset + * @param previousIsolationLevel the isolation level to restore, if any + * @deprecated as of 5.1.11, in favor of + * {@link #resetConnectionAfterTransaction(Connection, Integer, boolean)} + */ + @Deprecated + public static void resetConnectionAfterTransaction(Connection con, @Nullable Integer previousIsolationLevel) { + Assert.notNull(con, "No Connection specified"); + try { + // Reset transaction isolation to previous value, if changed for the transaction. + if (previousIsolationLevel != null) { + if (logger.isDebugEnabled()) { + logger.debug("Resetting isolation level of JDBC Connection [" + + con + "] to " + previousIsolationLevel); + } + con.setTransactionIsolation(previousIsolationLevel); + } + + // Reset read-only flag. + if (con.isReadOnly()) { + if (logger.isDebugEnabled()) { + logger.debug("Resetting read-only flag of JDBC Connection [" + con + "]"); + } + con.setReadOnly(false); + } + } + catch (Throwable ex) { + logger.debug("Could not reset JDBC Connection after transaction", ex); + } + } + + /** + * Determine whether the given JDBC Connection is transactional, that is, + * bound to the current thread by Spring's transaction facilities. + * @param con the Connection to check + * @param dataSource the DataSource that the Connection was obtained from + * (may be {@code null}) + * @return whether the Connection is transactional + */ + public static boolean isConnectionTransactional(Connection con, @Nullable DataSource dataSource) { + if (dataSource == null) { + return false; + } + ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource); + return (conHolder != null && connectionEquals(conHolder, con)); + } + + /** + * Apply the current transaction timeout, if any, + * to the given JDBC Statement object. + * @param stmt the JDBC Statement object + * @param dataSource the DataSource that the Connection was obtained from + * @throws SQLException if thrown by JDBC methods + * @see java.sql.Statement#setQueryTimeout + */ + public static void applyTransactionTimeout(Statement stmt, @Nullable DataSource dataSource) throws SQLException { + applyTimeout(stmt, dataSource, -1); + } + + /** + * Apply the specified timeout - overridden by the current transaction timeout, + * if any - to the given JDBC Statement object. + * @param stmt the JDBC Statement object + * @param dataSource the DataSource that the Connection was obtained from + * @param timeout the timeout to apply (or 0 for no timeout outside of a transaction) + * @throws SQLException if thrown by JDBC methods + * @see java.sql.Statement#setQueryTimeout + */ + public static void applyTimeout(Statement stmt, @Nullable DataSource dataSource, int timeout) throws SQLException { + Assert.notNull(stmt, "No Statement specified"); + ConnectionHolder holder = null; + if (dataSource != null) { + holder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource); + } + if (holder != null && holder.hasTimeout()) { + // Remaining transaction timeout overrides specified value. + stmt.setQueryTimeout(holder.getTimeToLiveInSeconds()); + } + else if (timeout >= 0) { + // No current transaction timeout -> apply specified value. + stmt.setQueryTimeout(timeout); + } + } + + /** + * Close the given Connection, obtained from the given DataSource, + * if it is not managed externally (that is, not bound to the thread). + * @param con the Connection to close if necessary + * (if this is {@code null}, the call will be ignored) + * @param dataSource the DataSource that the Connection was obtained from + * (may be {@code null}) + * @see #getConnection + */ + public static void releaseConnection(@Nullable Connection con, @Nullable DataSource dataSource) { + try { + doReleaseConnection(con, dataSource); + } + catch (SQLException ex) { + logger.debug("Could not close JDBC Connection", ex); + } + catch (Throwable ex) { + logger.debug("Unexpected exception on closing JDBC Connection", ex); + } + } + + /** + * Actually close the given Connection, obtained from the given DataSource. + * Same as {@link #releaseConnection}, but throwing the original SQLException. + *

    Directly accessed by {@link TransactionAwareDataSourceProxy}. + * @param con the Connection to close if necessary + * (if this is {@code null}, the call will be ignored) + * @param dataSource the DataSource that the Connection was obtained from + * (may be {@code null}) + * @throws SQLException if thrown by JDBC methods + * @see #doGetConnection + */ + public static void doReleaseConnection(@Nullable Connection con, @Nullable DataSource dataSource) throws SQLException { + if (con == null) { + return; + } + if (dataSource != null) { + ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource); + if (conHolder != null && connectionEquals(conHolder, con)) { + // It's the transactional Connection: Don't close it. + conHolder.released(); + return; + } + } + doCloseConnection(con, dataSource); + } + + /** + * Close the Connection, unless a {@link SmartDataSource} doesn't want us to. + * @param con the Connection to close if necessary + * @param dataSource the DataSource that the Connection was obtained from + * @throws SQLException if thrown by JDBC methods + * @see Connection#close() + * @see SmartDataSource#shouldClose(Connection) + */ + public static void doCloseConnection(Connection con, @Nullable DataSource dataSource) throws SQLException { + if (!(dataSource instanceof SmartDataSource) || ((SmartDataSource) dataSource).shouldClose(con)) { + con.close(); + } + } + + /** + * Determine whether the given two Connections are equal, asking the target + * Connection in case of a proxy. Used to detect equality even if the + * user passed in a raw target Connection while the held one is a proxy. + * @param conHolder the ConnectionHolder for the held Connection (potentially a proxy) + * @param passedInCon the Connection passed-in by the user + * (potentially a target Connection without proxy) + * @return whether the given Connections are equal + * @see #getTargetConnection + */ + private static boolean connectionEquals(ConnectionHolder conHolder, Connection passedInCon) { + if (!conHolder.hasConnection()) { + return false; + } + Connection heldCon = conHolder.getConnection(); + // Explicitly check for identity too: for Connection handles that do not implement + // "equals" properly, such as the ones Commons DBCP exposes). + return (heldCon == passedInCon || heldCon.equals(passedInCon) || + getTargetConnection(heldCon).equals(passedInCon)); + } + + /** + * Return the innermost target Connection of the given Connection. If the given + * Connection is a proxy, it will be unwrapped until a non-proxy Connection is + * found. Otherwise, the passed-in Connection will be returned as-is. + * @param con the Connection proxy to unwrap + * @return the innermost target Connection, or the passed-in one if no proxy + * @see ConnectionProxy#getTargetConnection() + */ + public static Connection getTargetConnection(Connection con) { + Connection conToUse = con; + while (conToUse instanceof ConnectionProxy) { + conToUse = ((ConnectionProxy) conToUse).getTargetConnection(); + } + return conToUse; + } + + /** + * Determine the connection synchronization order to use for the given + * DataSource. Decreased for every level of nesting that a DataSource + * has, checked through the level of DelegatingDataSource nesting. + * @param dataSource the DataSource to check + * @return the connection synchronization order to use + * @see #CONNECTION_SYNCHRONIZATION_ORDER + */ + private static int getConnectionSynchronizationOrder(DataSource dataSource) { + int order = CONNECTION_SYNCHRONIZATION_ORDER; + DataSource currDs = dataSource; + while (currDs instanceof DelegatingDataSource) { + order--; + currDs = ((DelegatingDataSource) currDs).getTargetDataSource(); + } + return order; + } + + + /** + * Callback for resource cleanup at the end of a non-native JDBC transaction + * (e.g. when participating in a JtaTransactionManager transaction). + * @see org.springframework.transaction.jta.JtaTransactionManager + */ + private static class ConnectionSynchronization implements TransactionSynchronization { + + private final ConnectionHolder connectionHolder; + + private final DataSource dataSource; + + private int order; + + private boolean holderActive = true; + + public ConnectionSynchronization(ConnectionHolder connectionHolder, DataSource dataSource) { + this.connectionHolder = connectionHolder; + this.dataSource = dataSource; + this.order = getConnectionSynchronizationOrder(dataSource); + } + + @Override + public int getOrder() { + return this.order; + } + + @Override + public void suspend() { + if (this.holderActive) { + TransactionSynchronizationManager.unbindResource(this.dataSource); + if (this.connectionHolder.hasConnection() && !this.connectionHolder.isOpen()) { + // Release Connection on suspend if the application doesn't keep + // a handle to it anymore. We will fetch a fresh Connection if the + // application accesses the ConnectionHolder again after resume, + // assuming that it will participate in the same transaction. + releaseConnection(this.connectionHolder.getConnection(), this.dataSource); + this.connectionHolder.setConnection(null); + } + } + } + + @Override + public void resume() { + if (this.holderActive) { + TransactionSynchronizationManager.bindResource(this.dataSource, this.connectionHolder); + } + } + + @Override + public void beforeCompletion() { + // Release Connection early if the holder is not open anymore + // (that is, not used by another resource like a Hibernate Session + // that has its own cleanup via transaction synchronization), + // to avoid issues with strict JTA implementations that expect + // the close call before transaction completion. + if (!this.connectionHolder.isOpen()) { + TransactionSynchronizationManager.unbindResource(this.dataSource); + this.holderActive = false; + if (this.connectionHolder.hasConnection()) { + releaseConnection(this.connectionHolder.getConnection(), this.dataSource); + } + } + } + + @Override + public void afterCompletion(int status) { + // If we haven't closed the Connection in beforeCompletion, + // close it now. The holder might have been used for other + // cleanup in the meantime, for example by a Hibernate Session. + if (this.holderActive) { + // The thread-bound ConnectionHolder might not be available anymore, + // since afterCompletion might get called from a different thread. + TransactionSynchronizationManager.unbindResourceIfPossible(this.dataSource); + this.holderActive = false; + if (this.connectionHolder.hasConnection()) { + releaseConnection(this.connectionHolder.getConnection(), this.dataSource); + // Reset the ConnectionHolder: It might remain bound to the thread. + this.connectionHolder.setConnection(null); + } + } + this.connectionHolder.reset(); + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DelegatingDataSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DelegatingDataSource.java new file mode 100644 index 0000000..7204397 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DelegatingDataSource.java @@ -0,0 +1,156 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource; + +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.logging.Logger; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * JDBC {@link javax.sql.DataSource} implementation that delegates all calls + * to a given target {@link javax.sql.DataSource}. + * + *

    This class is meant to be subclassed, with subclasses overriding only + * those methods (such as {@link #getConnection()}) that should not simply + * delegate to the target DataSource. + * + * @author Juergen Hoeller + * @since 1.1 + * @see #getConnection + */ +public class DelegatingDataSource implements DataSource, InitializingBean { + + @Nullable + private DataSource targetDataSource; + + + /** + * Create a new DelegatingDataSource. + * @see #setTargetDataSource + */ + public DelegatingDataSource() { + } + + /** + * Create a new DelegatingDataSource. + * @param targetDataSource the target DataSource + */ + public DelegatingDataSource(DataSource targetDataSource) { + setTargetDataSource(targetDataSource); + } + + + /** + * Set the target DataSource that this DataSource should delegate to. + */ + public void setTargetDataSource(@Nullable DataSource targetDataSource) { + this.targetDataSource = targetDataSource; + } + + /** + * Return the target DataSource that this DataSource should delegate to. + */ + @Nullable + public DataSource getTargetDataSource() { + return this.targetDataSource; + } + + /** + * Obtain the target {@code DataSource} for actual use (never {@code null}). + * @since 5.0 + */ + protected DataSource obtainTargetDataSource() { + DataSource dataSource = getTargetDataSource(); + Assert.state(dataSource != null, "No 'targetDataSource' set"); + return dataSource; + } + + @Override + public void afterPropertiesSet() { + if (getTargetDataSource() == null) { + throw new IllegalArgumentException("Property 'targetDataSource' is required"); + } + } + + + @Override + public Connection getConnection() throws SQLException { + return obtainTargetDataSource().getConnection(); + } + + @Override + public Connection getConnection(String username, String password) throws SQLException { + return obtainTargetDataSource().getConnection(username, password); + } + + @Override + public PrintWriter getLogWriter() throws SQLException { + return obtainTargetDataSource().getLogWriter(); + } + + @Override + public void setLogWriter(PrintWriter out) throws SQLException { + obtainTargetDataSource().setLogWriter(out); + } + + @Override + public int getLoginTimeout() throws SQLException { + return obtainTargetDataSource().getLoginTimeout(); + } + + @Override + public void setLoginTimeout(int seconds) throws SQLException { + obtainTargetDataSource().setLoginTimeout(seconds); + } + + + //--------------------------------------------------------------------- + // Implementation of JDBC 4.0's Wrapper interface + //--------------------------------------------------------------------- + + @Override + @SuppressWarnings("unchecked") + public T unwrap(Class iface) throws SQLException { + if (iface.isInstance(this)) { + return (T) this; + } + return obtainTargetDataSource().unwrap(iface); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return (iface.isInstance(this) || obtainTargetDataSource().isWrapperFor(iface)); + } + + + //--------------------------------------------------------------------- + // Implementation of JDBC 4.1's getParentLogger method + //--------------------------------------------------------------------- + + @Override + public Logger getParentLogger() { + return Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DriverManagerDataSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DriverManagerDataSource.java new file mode 100644 index 0000000..f803cd4 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DriverManagerDataSource.java @@ -0,0 +1,158 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Properties; + +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Simple implementation of the standard JDBC {@link javax.sql.DataSource} interface, + * configuring the plain old JDBC {@link java.sql.DriverManager} via bean properties, and + * returning a new {@link java.sql.Connection} from every {@code getConnection} call. + * + *

    NOTE: This class is not an actual connection pool; it does not actually + * pool Connections. It just serves as simple replacement for a full-blown + * connection pool, implementing the same standard interface, but creating new + * Connections on every call. + * + *

    Useful for test or standalone environments outside of a Java EE container, either + * as a DataSource bean in a corresponding ApplicationContext or in conjunction with + * a simple JNDI environment. Pool-assuming {@code Connection.close()} calls will + * simply close the Connection, so any DataSource-aware persistence code should work. + * + *

    NOTE: Within special class loading environments such as OSGi, this class + * is effectively superseded by {@link SimpleDriverDataSource} due to general class + * loading issues with the JDBC DriverManager that be resolved through direct Driver + * usage (which is exactly what SimpleDriverDataSource does). + * + *

    In a Java EE container, it is recommended to use a JNDI DataSource provided by + * the container. Such a DataSource can be exposed as a DataSource bean in a Spring + * ApplicationContext via {@link org.springframework.jndi.JndiObjectFactoryBean}, + * for seamless switching to and from a local DataSource bean like this class. + * For tests, you can then either set up a mock JNDI environment through Spring's + * {@link org.springframework.mock.jndi.SimpleNamingContextBuilder}, or switch the + * bean definition to a local DataSource (which is simpler and thus recommended). + * + *

    This {@code DriverManagerDataSource} class was originally designed alongside + * Apache Commons DBCP + * and C3P0, featuring bean-style + * {@code BasicDataSource}/{@code ComboPooledDataSource} classes with configuration + * properties for local resource setups. For a modern JDBC connection pool, consider + * HikariCP instead, + * exposing a corresponding {@code HikariDataSource} instance to the application. + * + * @author Juergen Hoeller + * @since 14.03.2003 + * @see SimpleDriverDataSource + */ +public class DriverManagerDataSource extends AbstractDriverBasedDataSource { + + /** + * Constructor for bean-style configuration. + */ + public DriverManagerDataSource() { + } + + /** + * Create a new DriverManagerDataSource with the given JDBC URL, + * not specifying a username or password for JDBC access. + * @param url the JDBC URL to use for accessing the DriverManager + * @see java.sql.DriverManager#getConnection(String) + */ + public DriverManagerDataSource(String url) { + setUrl(url); + } + + /** + * Create a new DriverManagerDataSource with the given standard + * DriverManager parameters. + * @param url the JDBC URL to use for accessing the DriverManager + * @param username the JDBC username to use for accessing the DriverManager + * @param password the JDBC password to use for accessing the DriverManager + * @see java.sql.DriverManager#getConnection(String, String, String) + */ + public DriverManagerDataSource(String url, String username, String password) { + setUrl(url); + setUsername(username); + setPassword(password); + } + + /** + * Create a new DriverManagerDataSource with the given JDBC URL, + * not specifying a username or password for JDBC access. + * @param url the JDBC URL to use for accessing the DriverManager + * @param conProps the JDBC connection properties + * @see java.sql.DriverManager#getConnection(String) + */ + public DriverManagerDataSource(String url, Properties conProps) { + setUrl(url); + setConnectionProperties(conProps); + } + + + /** + * Set the JDBC driver class name. This driver will get initialized + * on startup, registering itself with the JDK's DriverManager. + *

    NOTE: DriverManagerDataSource is primarily intended for accessing + * pre-registered JDBC drivers. If you need to register a new driver, + * consider using {@link SimpleDriverDataSource} instead. Alternatively, consider + * initializing the JDBC driver yourself before instantiating this DataSource. + * The "driverClassName" property is mainly preserved for backwards compatibility, + * as well as for migrating between Commons DBCP and this DataSource. + * @see java.sql.DriverManager#registerDriver(java.sql.Driver) + * @see SimpleDriverDataSource + */ + public void setDriverClassName(String driverClassName) { + Assert.hasText(driverClassName, "Property 'driverClassName' must not be empty"); + String driverClassNameToUse = driverClassName.trim(); + try { + Class.forName(driverClassNameToUse, true, ClassUtils.getDefaultClassLoader()); + } + catch (ClassNotFoundException ex) { + throw new IllegalStateException("Could not load JDBC driver class [" + driverClassNameToUse + "]", ex); + } + if (logger.isDebugEnabled()) { + logger.debug("Loaded JDBC driver: " + driverClassNameToUse); + } + } + + + @Override + protected Connection getConnectionFromDriver(Properties props) throws SQLException { + String url = getUrl(); + Assert.state(url != null, "'url' not set"); + if (logger.isDebugEnabled()) { + logger.debug("Creating new JDBC DriverManager Connection to [" + url + "]"); + } + return getConnectionFromDriverManager(url, props); + } + + /** + * Getting a Connection using the nasty static from DriverManager is extracted + * into a protected method to allow for easy unit testing. + * @see java.sql.DriverManager#getConnection(String, java.util.Properties) + */ + protected Connection getConnectionFromDriverManager(String url, Properties props) throws SQLException { + return DriverManager.getConnection(url, props); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/IsolationLevelDataSourceAdapter.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/IsolationLevelDataSourceAdapter.java new file mode 100644 index 0000000..48c9d8e --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/IsolationLevelDataSourceAdapter.java @@ -0,0 +1,170 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource; + +import java.sql.Connection; +import java.sql.SQLException; + +import org.springframework.core.Constants; +import org.springframework.lang.Nullable; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * An adapter for a target {@link javax.sql.DataSource}, applying the current + * Spring transaction's isolation level (and potentially specified user credentials) + * to every {@code getConnection} call. Also applies the read-only flag, + * if specified. + * + *

    Can be used to proxy a target JNDI DataSource that does not have the + * desired isolation level (and user credentials) configured. Client code + * can work with this DataSource as usual, not worrying about such settings. + * + *

    Inherits the capability to apply specific user credentials from its superclass + * {@link UserCredentialsDataSourceAdapter}; see the latter's javadoc for details + * on that functionality (e.g. {@link #setCredentialsForCurrentThread}). + * + *

    WARNING: This adapter simply calls + * {@link java.sql.Connection#setTransactionIsolation} and/or + * {@link java.sql.Connection#setReadOnly} for every Connection obtained from it. + * It does, however, not reset those settings; it rather expects the target + * DataSource to perform such resetting as part of its connection pool handling. + * Make sure that the target DataSource properly cleans up such transaction state. + * + * @author Juergen Hoeller + * @since 2.0.3 + * @see #setIsolationLevel + * @see #setIsolationLevelName + * @see #setUsername + * @see #setPassword + */ +public class IsolationLevelDataSourceAdapter extends UserCredentialsDataSourceAdapter { + + /** Constants instance for TransactionDefinition. */ + private static final Constants constants = new Constants(TransactionDefinition.class); + + @Nullable + private Integer isolationLevel; + + + /** + * Set the default isolation level by the name of the corresponding constant + * in {@link org.springframework.transaction.TransactionDefinition}, e.g. + * "ISOLATION_SERIALIZABLE". + *

    If not specified, the target DataSource's default will be used. + * Note that a transaction-specific isolation value will always override + * any isolation setting specified at the DataSource level. + * @param constantName name of the constant + * @see org.springframework.transaction.TransactionDefinition#ISOLATION_READ_UNCOMMITTED + * @see org.springframework.transaction.TransactionDefinition#ISOLATION_READ_COMMITTED + * @see org.springframework.transaction.TransactionDefinition#ISOLATION_REPEATABLE_READ + * @see org.springframework.transaction.TransactionDefinition#ISOLATION_SERIALIZABLE + * @see #setIsolationLevel + */ + public final void setIsolationLevelName(String constantName) throws IllegalArgumentException { + if (!constantName.startsWith(DefaultTransactionDefinition.PREFIX_ISOLATION)) { + throw new IllegalArgumentException("Only isolation constants allowed"); + } + setIsolationLevel(constants.asNumber(constantName).intValue()); + } + + /** + * Specify the default isolation level to use for Connection retrieval, + * according to the JDBC {@link java.sql.Connection} constants + * (equivalent to the corresponding Spring + * {@link org.springframework.transaction.TransactionDefinition} constants). + *

    If not specified, the target DataSource's default will be used. + * Note that a transaction-specific isolation value will always override + * any isolation setting specified at the DataSource level. + * @see java.sql.Connection#TRANSACTION_READ_UNCOMMITTED + * @see java.sql.Connection#TRANSACTION_READ_COMMITTED + * @see java.sql.Connection#TRANSACTION_REPEATABLE_READ + * @see java.sql.Connection#TRANSACTION_SERIALIZABLE + * @see org.springframework.transaction.TransactionDefinition#ISOLATION_READ_UNCOMMITTED + * @see org.springframework.transaction.TransactionDefinition#ISOLATION_READ_COMMITTED + * @see org.springframework.transaction.TransactionDefinition#ISOLATION_REPEATABLE_READ + * @see org.springframework.transaction.TransactionDefinition#ISOLATION_SERIALIZABLE + * @see org.springframework.transaction.TransactionDefinition#getIsolationLevel() + * @see org.springframework.transaction.support.TransactionSynchronizationManager#getCurrentTransactionIsolationLevel() + */ + public void setIsolationLevel(int isolationLevel) { + if (!constants.getValues(DefaultTransactionDefinition.PREFIX_ISOLATION).contains(isolationLevel)) { + throw new IllegalArgumentException("Only values of isolation constants allowed"); + } + this.isolationLevel = (isolationLevel != TransactionDefinition.ISOLATION_DEFAULT ? isolationLevel : null); + } + + /** + * Return the statically specified isolation level, + * or {@code null} if none. + */ + @Nullable + protected Integer getIsolationLevel() { + return this.isolationLevel; + } + + + /** + * Applies the current isolation level value and read-only flag + * to the returned Connection. + * @see #getCurrentIsolationLevel() + * @see #getCurrentReadOnlyFlag() + */ + @Override + protected Connection doGetConnection(@Nullable String username, @Nullable String password) throws SQLException { + Connection con = super.doGetConnection(username, password); + Boolean readOnlyToUse = getCurrentReadOnlyFlag(); + if (readOnlyToUse != null) { + con.setReadOnly(readOnlyToUse); + } + Integer isolationLevelToUse = getCurrentIsolationLevel(); + if (isolationLevelToUse != null) { + con.setTransactionIsolation(isolationLevelToUse); + } + return con; + } + + /** + * Determine the current isolation level: either the transaction's + * isolation level or a statically defined isolation level. + * @return the current isolation level, or {@code null} if none + * @see org.springframework.transaction.support.TransactionSynchronizationManager#getCurrentTransactionIsolationLevel() + * @see #setIsolationLevel + */ + @Nullable + protected Integer getCurrentIsolationLevel() { + Integer isolationLevelToUse = TransactionSynchronizationManager.getCurrentTransactionIsolationLevel(); + if (isolationLevelToUse == null) { + isolationLevelToUse = getIsolationLevel(); + } + return isolationLevelToUse; + } + + /** + * Determine the current read-only flag: by default, + * the transaction's read-only hint. + * @return whether there is a read-only hint for the current scope + * @see org.springframework.transaction.support.TransactionSynchronizationManager#isCurrentTransactionReadOnly() + */ + @Nullable + protected Boolean getCurrentReadOnlyFlag() { + boolean txReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly(); + return (txReadOnly ? Boolean.TRUE : null); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/JdbcTransactionObjectSupport.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/JdbcTransactionObjectSupport.java new file mode 100644 index 0000000..412a67a --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/JdbcTransactionObjectSupport.java @@ -0,0 +1,210 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource; + +import java.sql.SQLException; +import java.sql.Savepoint; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.lang.Nullable; +import org.springframework.transaction.CannotCreateTransactionException; +import org.springframework.transaction.NestedTransactionNotSupportedException; +import org.springframework.transaction.SavepointManager; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionSystemException; +import org.springframework.transaction.TransactionUsageException; +import org.springframework.transaction.support.SmartTransactionObject; +import org.springframework.util.Assert; + +/** + * Convenient base class for JDBC-aware transaction objects. Can contain a + * {@link ConnectionHolder} with a JDBC {@code Connection}, and implements the + * {@link SavepointManager} interface based on that {@code ConnectionHolder}. + * + *

    Allows for programmatic management of JDBC {@link java.sql.Savepoint Savepoints}. + * Spring's {@link org.springframework.transaction.support.DefaultTransactionStatus} + * automatically delegates to this, as it autodetects transaction objects which + * implement the {@link SavepointManager} interface. + * + * @author Juergen Hoeller + * @since 1.1 + * @see DataSourceTransactionManager + */ +public abstract class JdbcTransactionObjectSupport implements SavepointManager, SmartTransactionObject { + + private static final Log logger = LogFactory.getLog(JdbcTransactionObjectSupport.class); + + + @Nullable + private ConnectionHolder connectionHolder; + + @Nullable + private Integer previousIsolationLevel; + + private boolean readOnly = false; + + private boolean savepointAllowed = false; + + + /** + * Set the ConnectionHolder for this transaction object. + */ + public void setConnectionHolder(@Nullable ConnectionHolder connectionHolder) { + this.connectionHolder = connectionHolder; + } + + /** + * Return the ConnectionHolder for this transaction object. + */ + public ConnectionHolder getConnectionHolder() { + Assert.state(this.connectionHolder != null, "No ConnectionHolder available"); + return this.connectionHolder; + } + + /** + * Check whether this transaction object has a ConnectionHolder. + */ + public boolean hasConnectionHolder() { + return (this.connectionHolder != null); + } + + /** + * Set the previous isolation level to retain, if any. + */ + public void setPreviousIsolationLevel(@Nullable Integer previousIsolationLevel) { + this.previousIsolationLevel = previousIsolationLevel; + } + + /** + * Return the retained previous isolation level, if any. + */ + @Nullable + public Integer getPreviousIsolationLevel() { + return this.previousIsolationLevel; + } + + /** + * Set the read-only status of this transaction. + * The default is {@code false}. + * @since 5.2.1 + */ + public void setReadOnly(boolean readOnly) { + this.readOnly = readOnly; + } + + /** + * Return the read-only status of this transaction. + * @since 5.2.1 + */ + public boolean isReadOnly() { + return this.readOnly; + } + + /** + * Set whether savepoints are allowed within this transaction. + * The default is {@code false}. + */ + public void setSavepointAllowed(boolean savepointAllowed) { + this.savepointAllowed = savepointAllowed; + } + + /** + * Return whether savepoints are allowed within this transaction. + */ + public boolean isSavepointAllowed() { + return this.savepointAllowed; + } + + @Override + public void flush() { + // no-op + } + + + //--------------------------------------------------------------------- + // Implementation of SavepointManager + //--------------------------------------------------------------------- + + /** + * This implementation creates a JDBC 3.0 Savepoint and returns it. + * @see java.sql.Connection#setSavepoint + */ + @Override + public Object createSavepoint() throws TransactionException { + ConnectionHolder conHolder = getConnectionHolderForSavepoint(); + try { + if (!conHolder.supportsSavepoints()) { + throw new NestedTransactionNotSupportedException( + "Cannot create a nested transaction because savepoints are not supported by your JDBC driver"); + } + if (conHolder.isRollbackOnly()) { + throw new CannotCreateTransactionException( + "Cannot create savepoint for transaction which is already marked as rollback-only"); + } + return conHolder.createSavepoint(); + } + catch (SQLException ex) { + throw new CannotCreateTransactionException("Could not create JDBC savepoint", ex); + } + } + + /** + * This implementation rolls back to the given JDBC 3.0 Savepoint. + * @see java.sql.Connection#rollback(java.sql.Savepoint) + */ + @Override + public void rollbackToSavepoint(Object savepoint) throws TransactionException { + ConnectionHolder conHolder = getConnectionHolderForSavepoint(); + try { + conHolder.getConnection().rollback((Savepoint) savepoint); + conHolder.resetRollbackOnly(); + } + catch (Throwable ex) { + throw new TransactionSystemException("Could not roll back to JDBC savepoint", ex); + } + } + + /** + * This implementation releases the given JDBC 3.0 Savepoint. + * @see java.sql.Connection#releaseSavepoint + */ + @Override + public void releaseSavepoint(Object savepoint) throws TransactionException { + ConnectionHolder conHolder = getConnectionHolderForSavepoint(); + try { + conHolder.getConnection().releaseSavepoint((Savepoint) savepoint); + } + catch (Throwable ex) { + logger.debug("Could not explicitly release JDBC savepoint", ex); + } + } + + protected ConnectionHolder getConnectionHolderForSavepoint() throws TransactionException { + if (!isSavepointAllowed()) { + throw new NestedTransactionNotSupportedException( + "Transaction manager does not allow nested transactions"); + } + if (!hasConnectionHolder()) { + throw new TransactionUsageException( + "Cannot create nested transaction when not exposing a JDBC transaction"); + } + return getConnectionHolder(); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/LazyConnectionDataSourceProxy.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/LazyConnectionDataSourceProxy.java new file mode 100644 index 0000000..e22d47d --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/LazyConnectionDataSourceProxy.java @@ -0,0 +1,440 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.Constants; +import org.springframework.lang.Nullable; + +/** + * Proxy for a target DataSource, fetching actual JDBC Connections lazily, + * i.e. not until first creation of a Statement. Connection initialization + * properties like auto-commit mode, transaction isolation and read-only mode + * will be kept and applied to the actual JDBC Connection as soon as an + * actual Connection is fetched (if ever). Consequently, commit and rollback + * calls will be ignored if no Statements have been created. + * + *

    This DataSource proxy allows to avoid fetching JDBC Connections from + * a pool unless actually necessary. JDBC transaction control can happen + * without fetching a Connection from the pool or communicating with the + * database; this will be done lazily on first creation of a JDBC Statement. + * + *

    If you configure both a LazyConnectionDataSourceProxy and a + * TransactionAwareDataSourceProxy, make sure that the latter is the outermost + * DataSource. In such a scenario, data access code will talk to the + * transaction-aware DataSource, which will in turn work with the + * LazyConnectionDataSourceProxy. + * + *

    Lazy fetching of physical JDBC Connections is particularly beneficial + * in a generic transaction demarcation environment. It allows you to demarcate + * transactions on all methods that could potentially perform data access, + * without paying a performance penalty if no actual data access happens. + * + *

    This DataSource proxy gives you behavior analogous to JTA and a + * transactional JNDI DataSource (as provided by the Java EE server), even + * with a local transaction strategy like DataSourceTransactionManager or + * HibernateTransactionManager. It does not add value with Spring's + * JtaTransactionManager as transaction strategy. + * + *

    Lazy fetching of JDBC Connections is also recommended for read-only + * operations with Hibernate, in particular if the chances of resolving the + * result in the second-level cache are high. This avoids the need to + * communicate with the database at all for such read-only operations. + * You will get the same effect with non-transactional reads, but lazy fetching + * of JDBC Connections allows you to still perform reads in transactions. + * + *

    NOTE: This DataSource proxy needs to return wrapped Connections + * (which implement the {@link ConnectionProxy} interface) in order to handle + * lazy fetching of an actual JDBC Connection. Use {@link Connection#unwrap} + * to retrieve the native JDBC Connection. + * + * @author Juergen Hoeller + * @since 1.1.4 + * @see DataSourceTransactionManager + */ +public class LazyConnectionDataSourceProxy extends DelegatingDataSource { + + /** Constants instance for TransactionDefinition. */ + private static final Constants constants = new Constants(Connection.class); + + private static final Log logger = LogFactory.getLog(LazyConnectionDataSourceProxy.class); + + @Nullable + private Boolean defaultAutoCommit; + + @Nullable + private Integer defaultTransactionIsolation; + + + /** + * Create a new LazyConnectionDataSourceProxy. + * @see #setTargetDataSource + */ + public LazyConnectionDataSourceProxy() { + } + + /** + * Create a new LazyConnectionDataSourceProxy. + * @param targetDataSource the target DataSource + */ + public LazyConnectionDataSourceProxy(DataSource targetDataSource) { + setTargetDataSource(targetDataSource); + afterPropertiesSet(); + } + + + /** + * Set the default auto-commit mode to expose when no target Connection + * has been fetched yet (-> actual JDBC Connection default not known yet). + *

    If not specified, the default gets determined by checking a target + * Connection on startup. If that check fails, the default will be determined + * lazily on first access of a Connection. + * @see java.sql.Connection#setAutoCommit + */ + public void setDefaultAutoCommit(boolean defaultAutoCommit) { + this.defaultAutoCommit = defaultAutoCommit; + } + + /** + * Set the default transaction isolation level to expose when no target Connection + * has been fetched yet (-> actual JDBC Connection default not known yet). + *

    This property accepts the int constant value (e.g. 8) as defined in the + * {@link java.sql.Connection} interface; it is mainly intended for programmatic + * use. Consider using the "defaultTransactionIsolationName" property for setting + * the value by name (e.g. "TRANSACTION_SERIALIZABLE"). + *

    If not specified, the default gets determined by checking a target + * Connection on startup. If that check fails, the default will be determined + * lazily on first access of a Connection. + * @see #setDefaultTransactionIsolationName + * @see java.sql.Connection#setTransactionIsolation + */ + public void setDefaultTransactionIsolation(int defaultTransactionIsolation) { + this.defaultTransactionIsolation = defaultTransactionIsolation; + } + + /** + * Set the default transaction isolation level by the name of the corresponding + * constant in {@link java.sql.Connection}, e.g. "TRANSACTION_SERIALIZABLE". + * @param constantName name of the constant + * @see #setDefaultTransactionIsolation + * @see java.sql.Connection#TRANSACTION_READ_UNCOMMITTED + * @see java.sql.Connection#TRANSACTION_READ_COMMITTED + * @see java.sql.Connection#TRANSACTION_REPEATABLE_READ + * @see java.sql.Connection#TRANSACTION_SERIALIZABLE + */ + public void setDefaultTransactionIsolationName(String constantName) { + setDefaultTransactionIsolation(constants.asNumber(constantName).intValue()); + } + + + @Override + public void afterPropertiesSet() { + super.afterPropertiesSet(); + + // Determine default auto-commit and transaction isolation + // via a Connection from the target DataSource, if possible. + if (this.defaultAutoCommit == null || this.defaultTransactionIsolation == null) { + try { + try (Connection con = obtainTargetDataSource().getConnection()) { + checkDefaultConnectionProperties(con); + } + } + catch (SQLException ex) { + logger.debug("Could not retrieve default auto-commit and transaction isolation settings", ex); + } + } + } + + /** + * Check the default connection properties (auto-commit, transaction isolation), + * keeping them to be able to expose them correctly without fetching an actual + * JDBC Connection from the target DataSource. + *

    This will be invoked once on startup, but also for each retrieval of a + * target Connection. If the check failed on startup (because the database was + * down), we'll lazily retrieve those settings. + * @param con the Connection to use for checking + * @throws SQLException if thrown by Connection methods + */ + protected synchronized void checkDefaultConnectionProperties(Connection con) throws SQLException { + if (this.defaultAutoCommit == null) { + this.defaultAutoCommit = con.getAutoCommit(); + } + if (this.defaultTransactionIsolation == null) { + this.defaultTransactionIsolation = con.getTransactionIsolation(); + } + } + + /** + * Expose the default auto-commit value. + */ + @Nullable + protected Boolean defaultAutoCommit() { + return this.defaultAutoCommit; + } + + /** + * Expose the default transaction isolation value. + */ + @Nullable + protected Integer defaultTransactionIsolation() { + return this.defaultTransactionIsolation; + } + + + /** + * Return a Connection handle that lazily fetches an actual JDBC Connection + * when asked for a Statement (or PreparedStatement or CallableStatement). + *

    The returned Connection handle implements the ConnectionProxy interface, + * allowing to retrieve the underlying target Connection. + * @return a lazy Connection handle + * @see ConnectionProxy#getTargetConnection() + */ + @Override + public Connection getConnection() throws SQLException { + return (Connection) Proxy.newProxyInstance( + ConnectionProxy.class.getClassLoader(), + new Class[] {ConnectionProxy.class}, + new LazyConnectionInvocationHandler()); + } + + /** + * Return a Connection handle that lazily fetches an actual JDBC Connection + * when asked for a Statement (or PreparedStatement or CallableStatement). + *

    The returned Connection handle implements the ConnectionProxy interface, + * allowing to retrieve the underlying target Connection. + * @param username the per-Connection username + * @param password the per-Connection password + * @return a lazy Connection handle + * @see ConnectionProxy#getTargetConnection() + */ + @Override + public Connection getConnection(String username, String password) throws SQLException { + return (Connection) Proxy.newProxyInstance( + ConnectionProxy.class.getClassLoader(), + new Class[] {ConnectionProxy.class}, + new LazyConnectionInvocationHandler(username, password)); + } + + + /** + * Invocation handler that defers fetching an actual JDBC Connection + * until first creation of a Statement. + */ + private class LazyConnectionInvocationHandler implements InvocationHandler { + + @Nullable + private String username; + + @Nullable + private String password; + + @Nullable + private Boolean autoCommit; + + @Nullable + private Integer transactionIsolation; + + private boolean readOnly = false; + + private int holdability = ResultSet.CLOSE_CURSORS_AT_COMMIT; + + private boolean closed = false; + + @Nullable + private Connection target; + + public LazyConnectionInvocationHandler() { + this.autoCommit = defaultAutoCommit(); + this.transactionIsolation = defaultTransactionIsolation(); + } + + public LazyConnectionInvocationHandler(String username, String password) { + this(); + this.username = username; + this.password = password; + } + + @Override + @Nullable + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // Invocation on ConnectionProxy interface coming in... + + switch (method.getName()) { + case "equals": + // We must avoid fetching a target Connection for "equals". + // Only consider equal when proxies are identical. + return (proxy == args[0]); + case "hashCode": + // We must avoid fetching a target Connection for "hashCode", + // and we must return the same hash code even when the target + // Connection has been fetched: use hashCode of Connection proxy. + return System.identityHashCode(proxy); + case "getTargetConnection": + // Handle getTargetConnection method: return underlying connection. + return getTargetConnection(method); + case "unwrap": + if (((Class) args[0]).isInstance(proxy)) { + return proxy; + } + break; + case "isWrapperFor": + if (((Class) args[0]).isInstance(proxy)) { + return true; + } + break; + } + + if (!hasTargetConnection()) { + // No physical target Connection kept yet -> + // resolve transaction demarcation methods without fetching + // a physical JDBC Connection until absolutely necessary. + + switch (method.getName()) { + case "toString": + return "Lazy Connection proxy for target DataSource [" + getTargetDataSource() + "]"; + case "getAutoCommit": + if (this.autoCommit != null) { + return this.autoCommit; + } + // Else fetch actual Connection and check there, + // because we didn't have a default specified. + break; + case "setAutoCommit": + this.autoCommit = (Boolean) args[0]; + return null; + case "getTransactionIsolation": + if (this.transactionIsolation != null) { + return this.transactionIsolation; + } + // Else fetch actual Connection and check there, + // because we didn't have a default specified. + break; + case "setTransactionIsolation": + this.transactionIsolation = (Integer) args[0]; + return null; + case "isReadOnly": + return this.readOnly; + case "setReadOnly": + this.readOnly = (Boolean) args[0]; + return null; + case "getHoldability": + return this.holdability; + case "setHoldability": + this.holdability = (Integer) args[0]; + return null; + case "commit": + case "rollback": + // Ignore: no statements created yet. + return null; + case "getWarnings": + case "clearWarnings": + // Ignore: no warnings to expose yet. + return null; + case "close": + // Ignore: no target connection yet. + this.closed = true; + return null; + case "isClosed": + return this.closed; + default: + if (this.closed) { + // Connection proxy closed, without ever having fetched a + // physical JDBC Connection: throw corresponding SQLException. + throw new SQLException("Illegal operation: connection is closed"); + } + } + } + + // Target Connection already fetched, + // or target Connection necessary for current operation -> + // invoke method on target connection. + try { + return method.invoke(getTargetConnection(method), args); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + + /** + * Return whether the proxy currently holds a target Connection. + */ + private boolean hasTargetConnection() { + return (this.target != null); + } + + /** + * Return the target Connection, fetching it and initializing it if necessary. + */ + private Connection getTargetConnection(Method operation) throws SQLException { + if (this.target == null) { + // No target Connection held -> fetch one. + if (logger.isTraceEnabled()) { + logger.trace("Connecting to database for operation '" + operation.getName() + "'"); + } + + // Fetch physical Connection from DataSource. + this.target = (this.username != null) ? + obtainTargetDataSource().getConnection(this.username, this.password) : + obtainTargetDataSource().getConnection(); + + // If we still lack default connection properties, check them now. + checkDefaultConnectionProperties(this.target); + + // Apply kept transaction settings, if any. + if (this.readOnly) { + try { + this.target.setReadOnly(true); + } + catch (Exception ex) { + // "read-only not supported" -> ignore, it's just a hint anyway + logger.debug("Could not set JDBC Connection read-only", ex); + } + } + if (this.transactionIsolation != null && + !this.transactionIsolation.equals(defaultTransactionIsolation())) { + this.target.setTransactionIsolation(this.transactionIsolation); + } + if (this.autoCommit != null && this.autoCommit != this.target.getAutoCommit()) { + this.target.setAutoCommit(this.autoCommit); + } + } + + else { + // Target Connection already held -> return it. + if (logger.isTraceEnabled()) { + logger.trace("Using existing database connection for operation '" + operation.getName() + "'"); + } + } + + return this.target; + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SimpleConnectionHandle.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SimpleConnectionHandle.java new file mode 100644 index 0000000..20d716b --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SimpleConnectionHandle.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource; + +import java.sql.Connection; + +import org.springframework.util.Assert; + +/** + * Simple implementation of the {@link ConnectionHandle} interface, + * containing a given JDBC Connection. + * + * @author Juergen Hoeller + * @since 1.1 + */ +public class SimpleConnectionHandle implements ConnectionHandle { + + private final Connection connection; + + + /** + * Create a new SimpleConnectionHandle for the given Connection. + * @param connection the JDBC Connection + */ + public SimpleConnectionHandle(Connection connection) { + Assert.notNull(connection, "Connection must not be null"); + this.connection = connection; + } + + /** + * Return the specified Connection as-is. + */ + @Override + public Connection getConnection() { + return this.connection; + } + + + @Override + public String toString() { + return "SimpleConnectionHandle: " + this.connection; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SimpleDriverDataSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SimpleDriverDataSource.java new file mode 100644 index 0000000..09e813d --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SimpleDriverDataSource.java @@ -0,0 +1,147 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource; + +import java.sql.Connection; +import java.sql.Driver; +import java.sql.SQLException; +import java.util.Properties; + +import org.springframework.beans.BeanUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Simple implementation of the standard JDBC {@link javax.sql.DataSource} interface, + * configuring a plain old JDBC {@link java.sql.Driver} via bean properties, and + * returning a new {@link java.sql.Connection} from every {@code getConnection} call. + * + *

    NOTE: This class is not an actual connection pool; it does not actually + * pool Connections. It just serves as simple replacement for a full-blown + * connection pool, implementing the same standard interface, but creating new + * Connections on every call. + * + *

    In a Java EE container, it is recommended to use a JNDI DataSource provided by + * the container. Such a DataSource can be exposed as a DataSource bean in a Spring + * ApplicationContext via {@link org.springframework.jndi.JndiObjectFactoryBean}, + * for seamless switching to and from a local DataSource bean like this class. + * + *

    This {@code SimpleDriverDataSource} class was originally designed alongside + * Apache Commons DBCP + * and C3P0, featuring bean-style + * {@code BasicDataSource}/{@code ComboPooledDataSource} classes with configuration + * properties for local resource setups. For a modern JDBC connection pool, consider + * HikariCP instead, + * exposing a corresponding {@code HikariDataSource} instance to the application. + * + * @author Juergen Hoeller + * @since 2.5.5 + * @see DriverManagerDataSource + */ +public class SimpleDriverDataSource extends AbstractDriverBasedDataSource { + + @Nullable + private Driver driver; + + + /** + * Constructor for bean-style configuration. + */ + public SimpleDriverDataSource() { + } + + /** + * Create a new DriverManagerDataSource with the given standard Driver parameters. + * @param driver the JDBC Driver object + * @param url the JDBC URL to use for accessing the DriverManager + * @see java.sql.Driver#connect(String, java.util.Properties) + */ + public SimpleDriverDataSource(Driver driver, String url) { + setDriver(driver); + setUrl(url); + } + + /** + * Create a new DriverManagerDataSource with the given standard Driver parameters. + * @param driver the JDBC Driver object + * @param url the JDBC URL to use for accessing the DriverManager + * @param username the JDBC username to use for accessing the DriverManager + * @param password the JDBC password to use for accessing the DriverManager + * @see java.sql.Driver#connect(String, java.util.Properties) + */ + public SimpleDriverDataSource(Driver driver, String url, String username, String password) { + setDriver(driver); + setUrl(url); + setUsername(username); + setPassword(password); + } + + /** + * Create a new DriverManagerDataSource with the given standard Driver parameters. + * @param driver the JDBC Driver object + * @param url the JDBC URL to use for accessing the DriverManager + * @param conProps the JDBC connection properties + * @see java.sql.Driver#connect(String, java.util.Properties) + */ + public SimpleDriverDataSource(Driver driver, String url, Properties conProps) { + setDriver(driver); + setUrl(url); + setConnectionProperties(conProps); + } + + + /** + * Specify the JDBC Driver implementation class to use. + *

    An instance of this Driver class will be created and held + * within the SimpleDriverDataSource. + * @see #setDriver + */ + public void setDriverClass(Class driverClass) { + this.driver = BeanUtils.instantiateClass(driverClass); + } + + /** + * Specify the JDBC Driver instance to use. + *

    This allows for passing in a shared, possibly pre-configured + * Driver instance. + * @see #setDriverClass + */ + public void setDriver(@Nullable Driver driver) { + this.driver = driver; + } + + /** + * Return the JDBC Driver instance to use. + */ + @Nullable + public Driver getDriver() { + return this.driver; + } + + + @Override + protected Connection getConnectionFromDriver(Properties props) throws SQLException { + Driver driver = getDriver(); + String url = getUrl(); + Assert.notNull(driver, "Driver must not be null"); + if (logger.isDebugEnabled()) { + logger.debug("Creating new JDBC Driver Connection to [" + url + "]"); + } + return driver.connect(url, props); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SingleConnectionDataSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SingleConnectionDataSource.java new file mode 100644 index 0000000..f61b3e7 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SingleConnectionDataSource.java @@ -0,0 +1,333 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.sql.Connection; +import java.sql.SQLException; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Implementation of {@link SmartDataSource} that wraps a single JDBC Connection + * which is not closed after use. Obviously, this is not multi-threading capable. + * + *

    Note that at shutdown, someone should close the underlying Connection + * via the {@code close()} method. Client code will never call close + * on the Connection handle if it is SmartDataSource-aware (e.g. uses + * {@code DataSourceUtils.releaseConnection}). + * + *

    If client code will call {@code close()} in the assumption of a pooled + * Connection, like when using persistence tools, set "suppressClose" to "true". + * This will return a close-suppressing proxy instead of the physical Connection. + * + *

    This is primarily intended for testing. For example, it enables easy testing + * outside an application server, for code that expects to work on a DataSource. + * In contrast to {@link DriverManagerDataSource}, it reuses the same Connection + * all the time, avoiding excessive creation of physical Connections. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see #getConnection() + * @see java.sql.Connection#close() + * @see DataSourceUtils#releaseConnection + */ +public class SingleConnectionDataSource extends DriverManagerDataSource implements SmartDataSource, DisposableBean { + + /** Create a close-suppressing proxy?. */ + private boolean suppressClose; + + /** Override auto-commit state?. */ + @Nullable + private Boolean autoCommit; + + /** Wrapped Connection. */ + @Nullable + private Connection target; + + /** Proxy Connection. */ + @Nullable + private Connection connection; + + /** Synchronization monitor for the shared Connection. */ + private final Object connectionMonitor = new Object(); + + + /** + * Constructor for bean-style configuration. + */ + public SingleConnectionDataSource() { + } + + /** + * Create a new SingleConnectionDataSource with the given standard + * DriverManager parameters. + * @param url the JDBC URL to use for accessing the DriverManager + * @param username the JDBC username to use for accessing the DriverManager + * @param password the JDBC password to use for accessing the DriverManager + * @param suppressClose if the returned Connection should be a + * close-suppressing proxy or the physical Connection + * @see java.sql.DriverManager#getConnection(String, String, String) + */ + public SingleConnectionDataSource(String url, String username, String password, boolean suppressClose) { + super(url, username, password); + this.suppressClose = suppressClose; + } + + /** + * Create a new SingleConnectionDataSource with the given standard + * DriverManager parameters. + * @param url the JDBC URL to use for accessing the DriverManager + * @param suppressClose if the returned Connection should be a + * close-suppressing proxy or the physical Connection + * @see java.sql.DriverManager#getConnection(String, String, String) + */ + public SingleConnectionDataSource(String url, boolean suppressClose) { + super(url); + this.suppressClose = suppressClose; + } + + /** + * Create a new SingleConnectionDataSource with a given Connection. + * @param target underlying target Connection + * @param suppressClose if the Connection should be wrapped with a Connection that + * suppresses {@code close()} calls (to allow for normal {@code close()} + * usage in applications that expect a pooled Connection but do not know our + * SmartDataSource interface) + */ + public SingleConnectionDataSource(Connection target, boolean suppressClose) { + Assert.notNull(target, "Connection must not be null"); + this.target = target; + this.suppressClose = suppressClose; + this.connection = (suppressClose ? getCloseSuppressingConnectionProxy(target) : target); + } + + + /** + * Set whether the returned Connection should be a close-suppressing proxy + * or the physical Connection. + */ + public void setSuppressClose(boolean suppressClose) { + this.suppressClose = suppressClose; + } + + /** + * Return whether the returned Connection will be a close-suppressing proxy + * or the physical Connection. + */ + protected boolean isSuppressClose() { + return this.suppressClose; + } + + /** + * Set whether the returned Connection's "autoCommit" setting should be overridden. + */ + public void setAutoCommit(boolean autoCommit) { + this.autoCommit = (autoCommit); + } + + /** + * Return whether the returned Connection's "autoCommit" setting should be overridden. + * @return the "autoCommit" value, or {@code null} if none to be applied + */ + @Nullable + protected Boolean getAutoCommitValue() { + return this.autoCommit; + } + + + @Override + public Connection getConnection() throws SQLException { + synchronized (this.connectionMonitor) { + if (this.connection == null) { + // No underlying Connection -> lazy init via DriverManager. + initConnection(); + } + if (this.connection.isClosed()) { + throw new SQLException( + "Connection was closed in SingleConnectionDataSource. Check that user code checks " + + "shouldClose() before closing Connections, or set 'suppressClose' to 'true'"); + } + return this.connection; + } + } + + /** + * Specifying a custom username and password doesn't make sense + * with a single Connection. Returns the single Connection if given + * the same username and password; throws an SQLException else. + */ + @Override + public Connection getConnection(String username, String password) throws SQLException { + if (ObjectUtils.nullSafeEquals(username, getUsername()) && + ObjectUtils.nullSafeEquals(password, getPassword())) { + return getConnection(); + } + else { + throw new SQLException("SingleConnectionDataSource does not support custom username and password"); + } + } + + /** + * This is a single Connection: Do not close it when returning to the "pool". + */ + @Override + public boolean shouldClose(Connection con) { + synchronized (this.connectionMonitor) { + return (con != this.connection && con != this.target); + } + } + + /** + * Close the underlying Connection. + * The provider of this DataSource needs to care for proper shutdown. + *

    As this bean implements DisposableBean, a bean factory will + * automatically invoke this on destruction of its cached singletons. + */ + @Override + public void destroy() { + synchronized (this.connectionMonitor) { + closeConnection(); + } + } + + + /** + * Initialize the underlying Connection via the DriverManager. + */ + public void initConnection() throws SQLException { + if (getUrl() == null) { + throw new IllegalStateException("'url' property is required for lazily initializing a Connection"); + } + synchronized (this.connectionMonitor) { + closeConnection(); + this.target = getConnectionFromDriver(getUsername(), getPassword()); + prepareConnection(this.target); + if (logger.isDebugEnabled()) { + logger.debug("Established shared JDBC Connection: " + this.target); + } + this.connection = (isSuppressClose() ? getCloseSuppressingConnectionProxy(this.target) : this.target); + } + } + + /** + * Reset the underlying shared Connection, to be reinitialized on next access. + */ + public void resetConnection() { + synchronized (this.connectionMonitor) { + closeConnection(); + this.target = null; + this.connection = null; + } + } + + /** + * Prepare the given Connection before it is exposed. + *

    The default implementation applies the auto-commit flag, if necessary. + * Can be overridden in subclasses. + * @param con the Connection to prepare + * @see #setAutoCommit + */ + protected void prepareConnection(Connection con) throws SQLException { + Boolean autoCommit = getAutoCommitValue(); + if (autoCommit != null && con.getAutoCommit() != autoCommit) { + con.setAutoCommit(autoCommit); + } + } + + /** + * Close the underlying shared Connection. + */ + private void closeConnection() { + if (this.target != null) { + try { + this.target.close(); + } + catch (Throwable ex) { + logger.info("Could not close shared JDBC Connection", ex); + } + } + } + + /** + * Wrap the given Connection with a proxy that delegates every method call to it + * but suppresses close calls. + * @param target the original Connection to wrap + * @return the wrapped Connection + */ + protected Connection getCloseSuppressingConnectionProxy(Connection target) { + return (Connection) Proxy.newProxyInstance( + ConnectionProxy.class.getClassLoader(), + new Class[] {ConnectionProxy.class}, + new CloseSuppressingInvocationHandler(target)); + } + + + /** + * Invocation handler that suppresses close calls on JDBC Connections. + */ + private static class CloseSuppressingInvocationHandler implements InvocationHandler { + + private final Connection target; + + public CloseSuppressingInvocationHandler(Connection target) { + this.target = target; + } + + @Override + @Nullable + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // Invocation on ConnectionProxy interface coming in... + + switch (method.getName()) { + case "equals": + // Only consider equal when proxies are identical. + return (proxy == args[0]); + case "hashCode": + // Use hashCode of Connection proxy. + return System.identityHashCode(proxy); + case "close": + // Handle close method: don't pass the call on. + return null; + case "isClosed": + return this.target.isClosed(); + case "getTargetConnection": + // Handle getTargetConnection method: return underlying Connection. + return this.target; + case "unwrap": + return (((Class) args[0]).isInstance(proxy) ? proxy : this.target.unwrap((Class) args[0])); + case "isWrapperFor": + return (((Class) args[0]).isInstance(proxy) || this.target.isWrapperFor((Class) args[0])); + } + + // Invoke method on target Connection. + try { + return method.invoke(this.target, args); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SmartDataSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SmartDataSource.java new file mode 100644 index 0000000..c56133a --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SmartDataSource.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource; + +import java.sql.Connection; + +import javax.sql.DataSource; + +/** + * Extension of the {@code javax.sql.DataSource} interface, to be + * implemented by special DataSources that return JDBC Connections + * in an unwrapped fashion. + * + *

    Classes using this interface can query whether or not the Connection + * should be closed after an operation. Spring's DataSourceUtils and + * JdbcTemplate classes automatically perform such a check. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see SingleConnectionDataSource#shouldClose + * @see DataSourceUtils#releaseConnection + * @see org.springframework.jdbc.core.JdbcTemplate + */ +public interface SmartDataSource extends DataSource { + + /** + * Should we close this Connection, obtained from this DataSource? + *

    Code that uses Connections from a SmartDataSource should always + * perform a check via this method before invoking {@code close()}. + *

    Note that the JdbcTemplate class in the 'jdbc.core' package takes care of + * releasing JDBC Connections, freeing application code of this responsibility. + * @param con the Connection to check + * @return whether the given Connection should be closed + * @see java.sql.Connection#close() + */ + boolean shouldClose(Connection con); + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy.java new file mode 100644 index 0000000..f1afd56 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy.java @@ -0,0 +1,259 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import org.springframework.lang.Nullable; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * Proxy for a target JDBC {@link javax.sql.DataSource}, adding awareness of + * Spring-managed transactions. Similar to a transactional JNDI DataSource + * as provided by a Java EE server. + * + *

    Data access code that should remain unaware of Spring's data access support + * can work with this proxy to seamlessly participate in Spring-managed transactions. + * Note that the transaction manager, for example {@link DataSourceTransactionManager}, + * still needs to work with the underlying DataSource, not with this proxy. + * + *

    Make sure that TransactionAwareDataSourceProxy is the outermost DataSource + * of a chain of DataSource proxies/adapters. TransactionAwareDataSourceProxy + * can delegate either directly to the target connection pool or to some + * intermediary proxy/adapter like {@link LazyConnectionDataSourceProxy} or + * {@link UserCredentialsDataSourceAdapter}. + * + *

    Delegates to {@link DataSourceUtils} for automatically participating in + * thread-bound transactions, for example managed by {@link DataSourceTransactionManager}. + * {@code getConnection} calls and {@code close} calls on returned Connections + * will behave properly within a transaction, i.e. always operate on the transactional + * Connection. If not within a transaction, normal DataSource behavior applies. + * + *

    This proxy allows data access code to work with the plain JDBC API and still + * participate in Spring-managed transactions, similar to JDBC code in a Java EE/JTA + * environment. However, if possible, use Spring's DataSourceUtils, JdbcTemplate or + * JDBC operation objects to get transaction participation even without a proxy for + * the target DataSource, avoiding the need to define such a proxy in the first place. + * + *

    As a further effect, using a transaction-aware DataSource will apply remaining + * transaction timeouts to all created JDBC (Prepared/Callable)Statement. This means + * that all operations performed through standard JDBC will automatically participate + * in Spring-managed transaction timeouts. + * + *

    NOTE: This DataSource proxy needs to return wrapped Connections (which + * implement the {@link ConnectionProxy} interface) in order to handle close calls + * properly. Use {@link Connection#unwrap} to retrieve the native JDBC Connection. + * + * @author Juergen Hoeller + * @since 1.1 + * @see javax.sql.DataSource#getConnection() + * @see java.sql.Connection#close() + * @see DataSourceUtils#doGetConnection + * @see DataSourceUtils#applyTransactionTimeout + * @see DataSourceUtils#doReleaseConnection + */ +public class TransactionAwareDataSourceProxy extends DelegatingDataSource { + + private boolean reobtainTransactionalConnections = false; + + + /** + * Create a new TransactionAwareDataSourceProxy. + * @see #setTargetDataSource + */ + public TransactionAwareDataSourceProxy() { + } + + /** + * Create a new TransactionAwareDataSourceProxy. + * @param targetDataSource the target DataSource + */ + public TransactionAwareDataSourceProxy(DataSource targetDataSource) { + super(targetDataSource); + } + + /** + * Specify whether to reobtain the target Connection for each operation + * performed within a transaction. + *

    The default is "false". Specify "true" to reobtain transactional + * Connections for every call on the Connection proxy; this is advisable + * on JBoss if you hold on to a Connection handle across transaction boundaries. + *

    The effect of this setting is similar to the + * "hibernate.connection.release_mode" value "after_statement". + */ + public void setReobtainTransactionalConnections(boolean reobtainTransactionalConnections) { + this.reobtainTransactionalConnections = reobtainTransactionalConnections; + } + + + /** + * Delegates to DataSourceUtils for automatically participating in Spring-managed + * transactions. Throws the original SQLException, if any. + *

    The returned Connection handle implements the ConnectionProxy interface, + * allowing to retrieve the underlying target Connection. + * @return a transactional Connection if any, a new one else + * @see DataSourceUtils#doGetConnection + * @see ConnectionProxy#getTargetConnection + */ + @Override + public Connection getConnection() throws SQLException { + return getTransactionAwareConnectionProxy(obtainTargetDataSource()); + } + + /** + * Wraps the given Connection with a proxy that delegates every method call to it + * but delegates {@code close()} calls to DataSourceUtils. + * @param targetDataSource the DataSource that the Connection came from + * @return the wrapped Connection + * @see java.sql.Connection#close() + * @see DataSourceUtils#doReleaseConnection + */ + protected Connection getTransactionAwareConnectionProxy(DataSource targetDataSource) { + return (Connection) Proxy.newProxyInstance( + ConnectionProxy.class.getClassLoader(), + new Class[] {ConnectionProxy.class}, + new TransactionAwareInvocationHandler(targetDataSource)); + } + + /** + * Determine whether to obtain a fixed target Connection for the proxy + * or to reobtain the target Connection for each operation. + *

    The default implementation returns {@code true} for all + * standard cases. This can be overridden through the + * {@link #setReobtainTransactionalConnections "reobtainTransactionalConnections"} + * flag, which enforces a non-fixed target Connection within an active transaction. + * Note that non-transactional access will always use a fixed Connection. + * @param targetDataSource the target DataSource + */ + protected boolean shouldObtainFixedConnection(DataSource targetDataSource) { + return (!TransactionSynchronizationManager.isSynchronizationActive() || + !this.reobtainTransactionalConnections); + } + + + /** + * Invocation handler that delegates close calls on JDBC Connections + * to DataSourceUtils for being aware of thread-bound transactions. + */ + private class TransactionAwareInvocationHandler implements InvocationHandler { + + private final DataSource targetDataSource; + + @Nullable + private Connection target; + + private boolean closed = false; + + public TransactionAwareInvocationHandler(DataSource targetDataSource) { + this.targetDataSource = targetDataSource; + } + + @Override + @Nullable + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // Invocation on ConnectionProxy interface coming in... + + switch (method.getName()) { + case "equals": + // Only considered as equal when proxies are identical. + return (proxy == args[0]); + case "hashCode": + // Use hashCode of Connection proxy. + return System.identityHashCode(proxy); + case "toString": + // Allow for differentiating between the proxy and the raw Connection. + StringBuilder sb = new StringBuilder("Transaction-aware proxy for target Connection "); + if (this.target != null) { + sb.append("[").append(this.target.toString()).append("]"); + } + else { + sb.append(" from DataSource [").append(this.targetDataSource).append("]"); + } + return sb.toString(); + case "close": + // Handle close method: only close if not within a transaction. + DataSourceUtils.doReleaseConnection(this.target, this.targetDataSource); + this.closed = true; + return null; + case "isClosed": + return this.closed; + case "unwrap": + if (((Class) args[0]).isInstance(proxy)) { + return proxy; + } + break; + case "isWrapperFor": + if (((Class) args[0]).isInstance(proxy)) { + return true; + } + break; + } + + if (this.target == null) { + if (method.getName().equals("getWarnings") || method.getName().equals("clearWarnings")) { + // Avoid creation of target Connection on pre-close cleanup (e.g. Hibernate Session) + return null; + } + if (this.closed) { + throw new SQLException("Connection handle already closed"); + } + if (shouldObtainFixedConnection(this.targetDataSource)) { + this.target = DataSourceUtils.doGetConnection(this.targetDataSource); + } + } + Connection actualTarget = this.target; + if (actualTarget == null) { + actualTarget = DataSourceUtils.doGetConnection(this.targetDataSource); + } + + if (method.getName().equals("getTargetConnection")) { + // Handle getTargetConnection method: return underlying Connection. + return actualTarget; + } + + // Invoke method on target Connection. + try { + Object retVal = method.invoke(actualTarget, args); + + // If return value is a Statement, apply transaction timeout. + // Applies to createStatement, prepareStatement, prepareCall. + if (retVal instanceof Statement) { + DataSourceUtils.applyTransactionTimeout((Statement) retVal, this.targetDataSource); + } + + return retVal; + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + finally { + if (actualTarget != this.target) { + DataSourceUtils.doReleaseConnection(actualTarget, this.targetDataSource); + } + } + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/UserCredentialsDataSourceAdapter.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/UserCredentialsDataSourceAdapter.java new file mode 100644 index 0000000..8d09575 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/UserCredentialsDataSourceAdapter.java @@ -0,0 +1,223 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource; + +import java.sql.Connection; +import java.sql.SQLException; + +import org.springframework.core.NamedThreadLocal; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * An adapter for a target JDBC {@link javax.sql.DataSource}, applying the specified + * user credentials to every standard {@code getConnection()} call, implicitly + * invoking {@code getConnection(username, password)} on the target. + * All other methods simply delegate to the corresponding methods of the + * target DataSource. + * + *

    Can be used to proxy a target JNDI DataSource that does not have user + * credentials configured. Client code can work with this DataSource as usual, + * using the standard {@code getConnection()} call. + * + *

    In the following example, client code can simply transparently work with + * the preconfigured "myDataSource", implicitly accessing "myTargetDataSource" + * with the specified user credentials. + * + *

    + * <bean id="myTargetDataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
    + *   <property name="jndiName" value="java:comp/env/jdbc/myds"/>
    + * </bean>
    + *
    + * <bean id="myDataSource" class="org.springframework.jdbc.datasource.UserCredentialsDataSourceAdapter">
    + *   <property name="targetDataSource" ref="myTargetDataSource"/>
    + *   <property name="username" value="myusername"/>
    + *   <property name="password" value="mypassword"/>
    + * </bean>
    + * + *

    If the "username" is empty, this proxy will simply delegate to the + * standard {@code getConnection()} method of the target DataSource. + * This can be used to keep a UserCredentialsDataSourceAdapter bean definition + * just for the option of implicitly passing in user credentials if + * the particular target DataSource requires it. + * + * @author Juergen Hoeller + * @since 1.0.2 + * @see #getConnection + */ +public class UserCredentialsDataSourceAdapter extends DelegatingDataSource { + + @Nullable + private String username; + + @Nullable + private String password; + + @Nullable + private String catalog; + + @Nullable + private String schema; + + private final ThreadLocal threadBoundCredentials = + new NamedThreadLocal<>("Current JDBC user credentials"); + + + /** + * Set the default username that this adapter should use for retrieving Connections. + *

    Default is no specific user. Note that an explicitly specified username + * will always override any username/password specified at the DataSource level. + * @see #setPassword + * @see #setCredentialsForCurrentThread(String, String) + * @see #getConnection(String, String) + */ + public void setUsername(String username) { + this.username = username; + } + + /** + * Set the default user's password that this adapter should use for retrieving Connections. + *

    Default is no specific password. Note that an explicitly specified username + * will always override any username/password specified at the DataSource level. + * @see #setUsername + * @see #setCredentialsForCurrentThread(String, String) + * @see #getConnection(String, String) + */ + public void setPassword(String password) { + this.password = password; + } + + /** + * Specify a database catalog to be applied to each retrieved Connection. + * @since 4.3.2 + * @see Connection#setCatalog + */ + public void setCatalog(String catalog) { + this.catalog = catalog; + } + + /** + * Specify a database schema to be applied to each retrieved Connection. + * @since 4.3.2 + * @see Connection#setSchema + */ + public void setSchema(String schema) { + this.schema = schema; + } + + + /** + * Set user credententials for this proxy and the current thread. + * The given username and password will be applied to all subsequent + * {@code getConnection()} calls on this DataSource proxy. + *

    This will override any statically specified user credentials, + * that is, values of the "username" and "password" bean properties. + * @param username the username to apply + * @param password the password to apply + * @see #removeCredentialsFromCurrentThread + */ + public void setCredentialsForCurrentThread(String username, String password) { + this.threadBoundCredentials.set(new JdbcUserCredentials(username, password)); + } + + /** + * Remove any user credentials for this proxy from the current thread. + * Statically specified user credentials apply again afterwards. + * @see #setCredentialsForCurrentThread + */ + public void removeCredentialsFromCurrentThread() { + this.threadBoundCredentials.remove(); + } + + + /** + * Determine whether there are currently thread-bound credentials, + * using them if available, falling back to the statically specified + * username and password (i.e. values of the bean properties) otherwise. + *

    Delegates to {@link #doGetConnection(String, String)} with the + * determined credentials as parameters. + * @see #doGetConnection + */ + @Override + public Connection getConnection() throws SQLException { + JdbcUserCredentials threadCredentials = this.threadBoundCredentials.get(); + Connection con = (threadCredentials != null ? + doGetConnection(threadCredentials.username, threadCredentials.password) : + doGetConnection(this.username, this.password)); + + if (this.catalog != null) { + con.setCatalog(this.catalog); + } + if (this.schema != null) { + con.setSchema(this.schema); + } + return con; + } + + /** + * Simply delegates to {@link #doGetConnection(String, String)}, + * keeping the given user credentials as-is. + */ + @Override + public Connection getConnection(String username, String password) throws SQLException { + return doGetConnection(username, password); + } + + /** + * This implementation delegates to the {@code getConnection(username, password)} + * method of the target DataSource, passing in the specified user credentials. + * If the specified username is empty, it will simply delegate to the standard + * {@code getConnection()} method of the target DataSource. + * @param username the username to use + * @param password the password to use + * @return the Connection + * @see javax.sql.DataSource#getConnection(String, String) + * @see javax.sql.DataSource#getConnection() + */ + protected Connection doGetConnection(@Nullable String username, @Nullable String password) throws SQLException { + Assert.state(getTargetDataSource() != null, "'targetDataSource' is required"); + if (StringUtils.hasLength(username)) { + return getTargetDataSource().getConnection(username, password); + } + else { + return getTargetDataSource().getConnection(); + } + } + + + /** + * Inner class used as ThreadLocal value. + */ + private static final class JdbcUserCredentials { + + public final String username; + + public final String password; + + public JdbcUserCredentials(String username, String password) { + this.username = username; + this.password = password; + } + + @Override + public String toString() { + return "JdbcUserCredentials[username='" + this.username + "',password='" + this.password + "']"; + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/WebSphereDataSourceAdapter.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/WebSphereDataSourceAdapter.java new file mode 100644 index 0000000..95e7a2d --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/WebSphereDataSourceAdapter.java @@ -0,0 +1,204 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.sql.Connection; +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * {@link DataSource} implementation that delegates all calls to a WebSphere + * target {@link DataSource}, typically obtained from JNDI, applying a current + * isolation level and/or current user credentials to every Connection obtained + * from it. + * + *

    Uses IBM-specific API to get a JDBC Connection with a specific isolation + * level (and read-only flag) from a WebSphere DataSource + * (IBM code example). + * Supports the transaction-specific isolation level exposed by + * {@link org.springframework.transaction.support.TransactionSynchronizationManager#getCurrentTransactionIsolationLevel()}. + * It's also possible to specify a default isolation level, to be applied when the + * current Spring-managed transaction does not define a specific isolation level. + * + *

    Usage example, defining the target DataSource as an inner-bean JNDI lookup + * (of course, you can link to any WebSphere DataSource through a bean reference): + * + *

    + * <bean id="myDataSource" class="org.springframework.jdbc.datasource.WebSphereDataSourceAdapter">
    + *   <property name="targetDataSource">
    + *     <bean class="org.springframework.jndi.JndiObjectFactoryBean">
    + *       <property name="jndiName" value="jdbc/myds"/>
    + *     </bean>
    + *   </property>
    + * </bean>
    + * + * Thanks to Ricardo Olivieri for submitting the original implementation + * of this approach! + * + * @author Juergen Hoeller + * @author Lari Hotari + * @author Ricardo N. Olivieri + * @since 2.0.3 + * @see com.ibm.websphere.rsadapter.JDBCConnectionSpec + * @see com.ibm.websphere.rsadapter.WSDataSource#getConnection(com.ibm.websphere.rsadapter.JDBCConnectionSpec) + * @see org.springframework.transaction.support.TransactionSynchronizationManager#getCurrentTransactionIsolationLevel() + * @see org.springframework.transaction.support.TransactionSynchronizationManager#isCurrentTransactionReadOnly() + */ +public class WebSphereDataSourceAdapter extends IsolationLevelDataSourceAdapter { + + protected final Log logger = LogFactory.getLog(getClass()); + + private Class wsDataSourceClass; + + private Method newJdbcConnSpecMethod; + + private Method wsDataSourceGetConnectionMethod; + + private Method setTransactionIsolationMethod; + + private Method setReadOnlyMethod; + + private Method setUserNameMethod; + + private Method setPasswordMethod; + + + /** + * This constructor retrieves the WebSphere JDBC connection spec API, + * so we can get obtain specific WebSphere Connections using reflection. + */ + public WebSphereDataSourceAdapter() { + try { + this.wsDataSourceClass = getClass().getClassLoader().loadClass("com.ibm.websphere.rsadapter.WSDataSource"); + Class jdbcConnSpecClass = getClass().getClassLoader().loadClass("com.ibm.websphere.rsadapter.JDBCConnectionSpec"); + Class wsrraFactoryClass = getClass().getClassLoader().loadClass("com.ibm.websphere.rsadapter.WSRRAFactory"); + this.newJdbcConnSpecMethod = wsrraFactoryClass.getMethod("createJDBCConnectionSpec"); + this.wsDataSourceGetConnectionMethod = + this.wsDataSourceClass.getMethod("getConnection", jdbcConnSpecClass); + this.setTransactionIsolationMethod = + jdbcConnSpecClass.getMethod("setTransactionIsolation", int.class); + this.setReadOnlyMethod = jdbcConnSpecClass.getMethod("setReadOnly", Boolean.class); + this.setUserNameMethod = jdbcConnSpecClass.getMethod("setUserName", String.class); + this.setPasswordMethod = jdbcConnSpecClass.getMethod("setPassword", String.class); + } + catch (Exception ex) { + throw new IllegalStateException( + "Could not initialize WebSphereDataSourceAdapter because WebSphere API classes are not available: " + ex); + } + } + + /** + * Checks that the specified 'targetDataSource' actually is + * a WebSphere WSDataSource. + */ + @Override + public void afterPropertiesSet() { + super.afterPropertiesSet(); + + if (!this.wsDataSourceClass.isInstance(getTargetDataSource())) { + throw new IllegalStateException( + "Specified 'targetDataSource' is not a WebSphere WSDataSource: " + getTargetDataSource()); + } + } + + + /** + * Builds a WebSphere JDBCConnectionSpec object for the current settings + * and calls {@code WSDataSource.getConnection(JDBCConnectionSpec)}. + * @see #createConnectionSpec + * @see com.ibm.websphere.rsadapter.WSDataSource#getConnection(com.ibm.websphere.rsadapter.JDBCConnectionSpec) + */ + @Override + protected Connection doGetConnection(@Nullable String username, @Nullable String password) throws SQLException { + // Create JDBCConnectionSpec using current isolation level value and read-only flag. + Object connSpec = createConnectionSpec( + getCurrentIsolationLevel(), getCurrentReadOnlyFlag(), username, password); + if (logger.isDebugEnabled()) { + logger.debug("Obtaining JDBC Connection from WebSphere DataSource [" + + getTargetDataSource() + "], using ConnectionSpec [" + connSpec + "]"); + } + // Create Connection through invoking WSDataSource.getConnection(JDBCConnectionSpec) + Connection con = (Connection) invokeJdbcMethod( + this.wsDataSourceGetConnectionMethod, obtainTargetDataSource(), connSpec); + Assert.state(con != null, "No Connection"); + return con; + } + + /** + * Create a WebSphere {@code JDBCConnectionSpec} object for the given characteristics. + *

    The default implementation uses reflection to apply the given settings. + * Can be overridden in subclasses to customize the JDBCConnectionSpec object + * (JDBCConnectionSpec javadoc; + * IBM developerWorks article). + * @param isolationLevel the isolation level to apply (or {@code null} if none) + * @param readOnlyFlag the read-only flag to apply (or {@code null} if none) + * @param username the username to apply ({@code null} or empty indicates the default) + * @param password the password to apply (may be {@code null} or empty) + * @throws SQLException if thrown by JDBCConnectionSpec API methods + * @see com.ibm.websphere.rsadapter.JDBCConnectionSpec + */ + protected Object createConnectionSpec(@Nullable Integer isolationLevel, @Nullable Boolean readOnlyFlag, + @Nullable String username, @Nullable String password) throws SQLException { + + Object connSpec = invokeJdbcMethod(this.newJdbcConnSpecMethod, null); + Assert.state(connSpec != null, "No JDBCConnectionSpec"); + if (isolationLevel != null) { + invokeJdbcMethod(this.setTransactionIsolationMethod, connSpec, isolationLevel); + } + if (readOnlyFlag != null) { + invokeJdbcMethod(this.setReadOnlyMethod, connSpec, readOnlyFlag); + } + // If the username is empty, we'll simply let the target DataSource + // use its default credentials. + if (StringUtils.hasLength(username)) { + invokeJdbcMethod(this.setUserNameMethod, connSpec, username); + invokeJdbcMethod(this.setPasswordMethod, connSpec, password); + } + return connSpec; + } + + + @Nullable + private static Object invokeJdbcMethod(Method method, @Nullable Object target, @Nullable Object... args) + throws SQLException { + try { + return method.invoke(target, args); + } + catch (IllegalAccessException ex) { + ReflectionUtils.handleReflectionException(ex); + } + catch (InvocationTargetException ex) { + if (ex.getTargetException() instanceof SQLException) { + throw (SQLException) ex.getTargetException(); + } + ReflectionUtils.handleInvocationTargetException(ex); + } + throw new IllegalStateException("Should never get here"); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/AbstractEmbeddedDatabaseConfigurer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/AbstractEmbeddedDatabaseConfigurer.java new file mode 100644 index 0000000..62e89c4 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/AbstractEmbeddedDatabaseConfigurer.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.embedded; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Base class for {@link EmbeddedDatabaseConfigurer} implementations + * providing common shutdown behavior through a "SHUTDOWN" statement. + * + * @author Oliver Gierke + * @author Juergen Hoeller + * @since 3.0 + */ +abstract class AbstractEmbeddedDatabaseConfigurer implements EmbeddedDatabaseConfigurer { + + protected final Log logger = LogFactory.getLog(getClass()); + + + @Override + public void shutdown(DataSource dataSource, String databaseName) { + Connection con = null; + try { + con = dataSource.getConnection(); + if (con != null) { + try (Statement stmt = con.createStatement()) { + stmt.execute("SHUTDOWN"); + } + } + } + catch (SQLException ex) { + logger.info("Could not shut down embedded database", ex); + } + finally { + if (con != null) { + try { + con.close(); + } + catch (SQLException ex) { + logger.debug("Could not close JDBC Connection on shutdown", ex); + } + catch (Throwable ex) { + logger.debug("Unexpected exception on closing JDBC Connection", ex); + } + } + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/ConnectionProperties.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/ConnectionProperties.java new file mode 100644 index 0000000..ec82d37 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/ConnectionProperties.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.embedded; + +import java.sql.Driver; + +/** + * {@code ConnectionProperties} serves as a simple data container that allows + * essential JDBC connection properties to be configured consistently, + * independent of the actual {@link javax.sql.DataSource DataSource} + * implementation. + * + * @author Keith Donald + * @author Sam Brannen + * @since 3.0 + * @see DataSourceFactory + */ +public interface ConnectionProperties { + + /** + * Set the JDBC driver class to use to connect to the database. + * @param driverClass the jdbc driver class + */ + void setDriverClass(Class driverClass); + + /** + * Set the JDBC connection URL for the database. + * @param url the connection url + */ + void setUrl(String url); + + /** + * Set the username to use to connect to the database. + * @param username the username + */ + void setUsername(String username); + + /** + * Set the password to use to connect to the database. + * @param password the password + */ + void setPassword(String password); + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/DataSourceFactory.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/DataSourceFactory.java new file mode 100644 index 0000000..5ace3dd --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/DataSourceFactory.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.embedded; + +import javax.sql.DataSource; + +/** + * {@code DataSourceFactory} encapsulates the creation of a particular + * {@link DataSource} implementation such as a non-pooling + * {@link org.springframework.jdbc.datasource.SimpleDriverDataSource} + * or a HikariCP pool setup in the shape of a {@code HikariDataSource}. + * + *

    Call {@link #getConnectionProperties()} to configure normalized + * {@code DataSource} properties before calling {@link #getDataSource()} + * to actually get the configured {@code DataSource} instance. + * + * @author Keith Donald + * @author Sam Brannen + * @since 3.0 + */ +public interface DataSourceFactory { + + /** + * Get the {@linkplain ConnectionProperties connection properties} + * of the {@link #getDataSource DataSource} to be configured. + */ + ConnectionProperties getConnectionProperties(); + + /** + * Get the {@link DataSource} with the + * {@linkplain #getConnectionProperties connection properties} applied. + */ + DataSource getDataSource(); + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/DerbyEmbeddedDatabaseConfigurer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/DerbyEmbeddedDatabaseConfigurer.java new file mode 100644 index 0000000..7375d70 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/DerbyEmbeddedDatabaseConfigurer.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.embedded; + +import java.sql.SQLException; +import java.util.Properties; + +import javax.sql.DataSource; + +import org.apache.commons.logging.LogFactory; +import org.apache.derby.jdbc.EmbeddedDriver; + +import org.springframework.lang.Nullable; + +/** + * {@link EmbeddedDatabaseConfigurer} for the Apache Derby database. + * + *

    Call {@link #getInstance()} to get the singleton instance of this class. + * + * @author Oliver Gierke + * @author Juergen Hoeller + * @since 3.0 + */ +final class DerbyEmbeddedDatabaseConfigurer implements EmbeddedDatabaseConfigurer { + + private static final String URL_TEMPLATE = "jdbc:derby:memory:%s;%s"; + + @Nullable + private static DerbyEmbeddedDatabaseConfigurer instance; + + + /** + * Get the singleton {@link DerbyEmbeddedDatabaseConfigurer} instance. + * @return the configurer instance + */ + public static synchronized DerbyEmbeddedDatabaseConfigurer getInstance() { + if (instance == null) { + // disable log file + System.setProperty("derby.stream.error.method", + OutputStreamFactory.class.getName() + ".getNoopOutputStream"); + instance = new DerbyEmbeddedDatabaseConfigurer(); + } + return instance; + } + + + private DerbyEmbeddedDatabaseConfigurer() { + } + + @Override + public void configureConnectionProperties(ConnectionProperties properties, String databaseName) { + properties.setDriverClass(EmbeddedDriver.class); + properties.setUrl(String.format(URL_TEMPLATE, databaseName, "create=true")); + properties.setUsername("sa"); + properties.setPassword(""); + } + + @Override + public void shutdown(DataSource dataSource, String databaseName) { + try { + new EmbeddedDriver().connect( + String.format(URL_TEMPLATE, databaseName, "drop=true"), new Properties()); + } + catch (SQLException ex) { + // Error code that indicates successful shutdown + if (!"08006".equals(ex.getSQLState())) { + LogFactory.getLog(getClass()).warn("Could not shut down embedded Derby database", ex); + } + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabase.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabase.java new file mode 100644 index 0000000..090bbb8 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabase.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.embedded; + +import javax.sql.DataSource; + +/** + * {@code EmbeddedDatabase} serves as a handle to an embedded database instance. + * + *

    An {@code EmbeddedDatabase} is also a {@link DataSource} and adds a + * {@link #shutdown} operation so that the embedded database instance can be + * shut down gracefully. + * + * @author Keith Donald + * @author Sam Brannen + * @since 3.0 + */ +public interface EmbeddedDatabase extends DataSource { + + /** + * Shut down this embedded database. + */ + void shutdown(); + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.java new file mode 100644 index 0000000..9e021a6 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.java @@ -0,0 +1,286 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.embedded; + +import javax.sql.DataSource; + +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.ResourceLoader; +import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; +import org.springframework.jdbc.datasource.init.ScriptUtils; +import org.springframework.util.Assert; + +/** + * A builder that provides a convenient API for constructing an embedded database. + * + *

    Usage Example

    + *
    + * EmbeddedDatabase db = new EmbeddedDatabaseBuilder()
    + *     .generateUniqueName(true)
    + *     .setType(H2)
    + *     .setScriptEncoding("UTF-8")
    + *     .ignoreFailedDrops(true)
    + *     .addScript("schema.sql")
    + *     .addScripts("user_data.sql", "country_data.sql")
    + *     .build();
    + *
    + * // perform actions against the db (EmbeddedDatabase extends javax.sql.DataSource)
    + *
    + * db.shutdown();
    + * 
    + * + * @author Keith Donald + * @author Juergen Hoeller + * @author Dave Syer + * @author Sam Brannen + * @since 3.0 + * @see org.springframework.jdbc.datasource.init.ScriptUtils + * @see org.springframework.jdbc.datasource.init.ResourceDatabasePopulator + * @see org.springframework.jdbc.datasource.init.DatabasePopulatorUtils + */ +public class EmbeddedDatabaseBuilder { + + private final EmbeddedDatabaseFactory databaseFactory; + + private final ResourceDatabasePopulator databasePopulator; + + private final ResourceLoader resourceLoader; + + + /** + * Create a new embedded database builder with a {@link DefaultResourceLoader}. + */ + public EmbeddedDatabaseBuilder() { + this(new DefaultResourceLoader()); + } + + /** + * Create a new embedded database builder with the given {@link ResourceLoader}. + * @param resourceLoader the {@code ResourceLoader} to delegate to + */ + public EmbeddedDatabaseBuilder(ResourceLoader resourceLoader) { + this.databaseFactory = new EmbeddedDatabaseFactory(); + this.databasePopulator = new ResourceDatabasePopulator(); + this.databaseFactory.setDatabasePopulator(this.databasePopulator); + this.resourceLoader = resourceLoader; + } + + /** + * Specify whether a unique ID should be generated and used as the database name. + *

    If the configuration for this builder is reused across multiple + * application contexts within a single JVM, this flag should be enabled + * (i.e., set to {@code true}) in order to ensure that each application context + * gets its own embedded database. + *

    Enabling this flag overrides any explicit name set via {@link #setName}. + * @param flag {@code true} if a unique database name should be generated + * @return {@code this}, to facilitate method chaining + * @since 4.2 + * @see #setName + */ + public EmbeddedDatabaseBuilder generateUniqueName(boolean flag) { + this.databaseFactory.setGenerateUniqueDatabaseName(flag); + return this; + } + + /** + * Set the name of the embedded database. + *

    Defaults to {@link EmbeddedDatabaseFactory#DEFAULT_DATABASE_NAME} if + * not called. + *

    Will be overridden if the {@code generateUniqueName} flag has been + * set to {@code true}. + * @param databaseName the name of the embedded database to build + * @return {@code this}, to facilitate method chaining + * @see #generateUniqueName + */ + public EmbeddedDatabaseBuilder setName(String databaseName) { + this.databaseFactory.setDatabaseName(databaseName); + return this; + } + + /** + * Set the type of embedded database. + *

    Defaults to HSQL if not called. + * @param databaseType the type of embedded database to build + * @return {@code this}, to facilitate method chaining + */ + public EmbeddedDatabaseBuilder setType(EmbeddedDatabaseType databaseType) { + this.databaseFactory.setDatabaseType(databaseType); + return this; + } + + /** + * Set the factory to use to create the {@link DataSource} instance that + * connects to the embedded database. + *

    Defaults to {@link SimpleDriverDataSourceFactory} but can be overridden, + * for example to introduce connection pooling. + * @return {@code this}, to facilitate method chaining + * @since 4.0.3 + */ + public EmbeddedDatabaseBuilder setDataSourceFactory(DataSourceFactory dataSourceFactory) { + Assert.notNull(dataSourceFactory, "DataSourceFactory is required"); + this.databaseFactory.setDataSourceFactory(dataSourceFactory); + return this; + } + + /** + * Add default SQL scripts to execute to populate the database. + *

    The default scripts are {@code "schema.sql"} to create the database + * schema and {@code "data.sql"} to populate the database with data. + * @return {@code this}, to facilitate method chaining + */ + public EmbeddedDatabaseBuilder addDefaultScripts() { + return addScripts("schema.sql", "data.sql"); + } + + /** + * Add an SQL script to execute to initialize or populate the database. + * @param script the script to execute + * @return {@code this}, to facilitate method chaining + */ + public EmbeddedDatabaseBuilder addScript(String script) { + this.databasePopulator.addScript(this.resourceLoader.getResource(script)); + return this; + } + + /** + * Add multiple SQL scripts to execute to initialize or populate the database. + * @param scripts the scripts to execute + * @return {@code this}, to facilitate method chaining + * @since 4.0.3 + */ + public EmbeddedDatabaseBuilder addScripts(String... scripts) { + for (String script : scripts) { + addScript(script); + } + return this; + } + + /** + * Specify the character encoding used in all SQL scripts, if different from + * the platform encoding. + * @param scriptEncoding the encoding used in scripts + * @return {@code this}, to facilitate method chaining + * @since 4.0.3 + */ + public EmbeddedDatabaseBuilder setScriptEncoding(String scriptEncoding) { + this.databasePopulator.setSqlScriptEncoding(scriptEncoding); + return this; + } + + /** + * Specify the statement separator used in all SQL scripts, if a custom one. + *

    Defaults to {@code ";"} if not specified and falls back to {@code "\n"} + * as a last resort; may be set to {@link ScriptUtils#EOF_STATEMENT_SEPARATOR} + * to signal that each script contains a single statement without a separator. + * @param separator the statement separator + * @return {@code this}, to facilitate method chaining + * @since 4.0.3 + */ + public EmbeddedDatabaseBuilder setSeparator(String separator) { + this.databasePopulator.setSeparator(separator); + return this; + } + + /** + * Specify the single-line comment prefix used in all SQL scripts. + *

    Defaults to {@code "--"}. + * @param commentPrefix the prefix for single-line comments + * @return {@code this}, to facilitate method chaining + * @since 4.0.3 + * @see #setCommentPrefixes(String...) + */ + public EmbeddedDatabaseBuilder setCommentPrefix(String commentPrefix) { + this.databasePopulator.setCommentPrefix(commentPrefix); + return this; + } + + /** + * Specify the prefixes that identify single-line comments within all SQL scripts. + *

    Defaults to {@code ["--"]}. + * @param commentPrefixes the prefixes for single-line comments + * @return {@code this}, to facilitate method chaining + * @since 5.2 + */ + public EmbeddedDatabaseBuilder setCommentPrefixes(String... commentPrefixes) { + this.databasePopulator.setCommentPrefixes(commentPrefixes); + return this; + } + + /** + * Specify the start delimiter for block comments in all SQL scripts. + *

    Defaults to {@code "/*"}. + * @param blockCommentStartDelimiter the start delimiter for block comments + * @return {@code this}, to facilitate method chaining + * @since 4.0.3 + * @see #setBlockCommentEndDelimiter + */ + public EmbeddedDatabaseBuilder setBlockCommentStartDelimiter(String blockCommentStartDelimiter) { + this.databasePopulator.setBlockCommentStartDelimiter(blockCommentStartDelimiter); + return this; + } + + /** + * Specify the end delimiter for block comments in all SQL scripts. + *

    Defaults to "*/". + * @param blockCommentEndDelimiter the end delimiter for block comments + * @return {@code this}, to facilitate method chaining + * @since 4.0.3 + * @see #setBlockCommentStartDelimiter + */ + public EmbeddedDatabaseBuilder setBlockCommentEndDelimiter(String blockCommentEndDelimiter) { + this.databasePopulator.setBlockCommentEndDelimiter(blockCommentEndDelimiter); + return this; + } + + /** + * Specify that all failures which occur while executing SQL scripts should + * be logged but should not cause a failure. + *

    Defaults to {@code false}. + * @param flag {@code true} if script execution should continue on error + * @return {@code this}, to facilitate method chaining + * @since 4.0.3 + */ + public EmbeddedDatabaseBuilder continueOnError(boolean flag) { + this.databasePopulator.setContinueOnError(flag); + return this; + } + + /** + * Specify that a failed SQL {@code DROP} statement within an executed + * script can be ignored. + *

    This is useful for a database whose SQL dialect does not support an + * {@code IF EXISTS} clause in a {@code DROP} statement. + *

    The default is {@code false} so that {@link #build building} will fail + * fast if a script starts with a {@code DROP} statement. + * @param flag {@code true} if failed drop statements should be ignored + * @return {@code this}, to facilitate method chaining + * @since 4.0.3 + */ + public EmbeddedDatabaseBuilder ignoreFailedDrops(boolean flag) { + this.databasePopulator.setIgnoreFailedDrops(flag); + return this; + } + + /** + * Build the embedded database. + * @return the embedded database + */ + public EmbeddedDatabase build() { + return this.databaseFactory.getDatabase(); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurer.java new file mode 100644 index 0000000..7b60004 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurer.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.embedded; + +import javax.sql.DataSource; + +/** + * {@code EmbeddedDatabaseConfigurer} encapsulates the configuration required to + * create, connect to, and shut down a specific type of embedded database such as + * HSQL, H2, or Derby. + * + * @author Keith Donald + * @author Sam Brannen + * @since 3.0 + */ +public interface EmbeddedDatabaseConfigurer { + + /** + * Configure the properties required to create and connect to the embedded database. + * @param properties connection properties to configure + * @param databaseName the name of the embedded database + */ + void configureConnectionProperties(ConnectionProperties properties, String databaseName); + + /** + * Shut down the embedded database instance that backs the supplied {@link DataSource}. + * @param dataSource the corresponding {@link DataSource} + * @param databaseName the name of the database being shut down + */ + void shutdown(DataSource dataSource, String databaseName); + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurerFactory.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurerFactory.java new file mode 100644 index 0000000..0964797 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurerFactory.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.embedded; + +import org.springframework.util.Assert; + +/** + * Maps well-known {@linkplain EmbeddedDatabaseType embedded database types} + * to {@link EmbeddedDatabaseConfigurer} strategies. + * + * @author Keith Donald + * @author Oliver Gierke + * @author Sam Brannen + * @since 3.0 + */ +final class EmbeddedDatabaseConfigurerFactory { + + private EmbeddedDatabaseConfigurerFactory() { + } + + + /** + * Return a configurer instance for the given embedded database type. + * @param type the embedded database type (HSQL, H2 or Derby) + * @return the configurer instance + * @throws IllegalStateException if the driver for the specified database type is not available + */ + public static EmbeddedDatabaseConfigurer getConfigurer(EmbeddedDatabaseType type) throws IllegalStateException { + Assert.notNull(type, "EmbeddedDatabaseType is required"); + try { + switch (type) { + case HSQL: + return HsqlEmbeddedDatabaseConfigurer.getInstance(); + case H2: + return H2EmbeddedDatabaseConfigurer.getInstance(); + case DERBY: + return DerbyEmbeddedDatabaseConfigurer.getInstance(); + default: + throw new UnsupportedOperationException("Embedded database type [" + type + "] is not supported"); + } + } + catch (ClassNotFoundException | NoClassDefFoundError ex) { + throw new IllegalStateException("Driver for test database type [" + type + "] is not available", ex); + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java new file mode 100644 index 0000000..52b6688 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java @@ -0,0 +1,308 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.embedded; + +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.UUID; +import java.util.logging.Logger; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.jdbc.datasource.SimpleDriverDataSource; +import org.springframework.jdbc.datasource.init.DatabasePopulator; +import org.springframework.jdbc.datasource.init.DatabasePopulatorUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Factory for creating an {@link EmbeddedDatabase} instance. + * + *

    Callers are guaranteed that the returned database has been fully + * initialized and populated. + * + *

    The factory can be configured as follows: + *

      + *
    • Call {@link #generateUniqueDatabaseName} to set a unique, random name + * for the database. + *
    • Call {@link #setDatabaseName} to set an explicit name for the database. + *
    • Call {@link #setDatabaseType} to set the database type if you wish to + * use one of the supported types. + *
    • Call {@link #setDatabaseConfigurer} to configure support for a custom + * embedded database type. + *
    • Call {@link #setDatabasePopulator} to change the algorithm used to + * populate the database. + *
    • Call {@link #setDataSourceFactory} to change the type of + * {@link DataSource} used to connect to the database. + *
    + * + *

    After configuring the factory, call {@link #getDatabase()} to obtain + * a reference to the {@link EmbeddedDatabase} instance. + * + * @author Keith Donald + * @author Juergen Hoeller + * @author Sam Brannen + * @since 3.0 + */ +public class EmbeddedDatabaseFactory { + + /** + * Default name for an embedded database: {@value}. + */ + public static final String DEFAULT_DATABASE_NAME = "testdb"; + + private static final Log logger = LogFactory.getLog(EmbeddedDatabaseFactory.class); + + private boolean generateUniqueDatabaseName = false; + + private String databaseName = DEFAULT_DATABASE_NAME; + + private DataSourceFactory dataSourceFactory = new SimpleDriverDataSourceFactory(); + + @Nullable + private EmbeddedDatabaseConfigurer databaseConfigurer; + + @Nullable + private DatabasePopulator databasePopulator; + + @Nullable + private DataSource dataSource; + + + /** + * Set the {@code generateUniqueDatabaseName} flag to enable or disable + * generation of a pseudo-random unique ID to be used as the database name. + *

    Setting this flag to {@code true} overrides any explicit name set + * via {@link #setDatabaseName}. + * @since 4.2 + * @see #setDatabaseName + */ + public void setGenerateUniqueDatabaseName(boolean generateUniqueDatabaseName) { + this.generateUniqueDatabaseName = generateUniqueDatabaseName; + } + + /** + * Set the name of the database. + *

    Defaults to {@value #DEFAULT_DATABASE_NAME}. + *

    Will be overridden if the {@code generateUniqueDatabaseName} flag + * has been set to {@code true}. + * @param databaseName name of the embedded database + * @see #setGenerateUniqueDatabaseName + */ + public void setDatabaseName(String databaseName) { + Assert.hasText(databaseName, "Database name is required"); + this.databaseName = databaseName; + } + + /** + * Set the factory to use to create the {@link DataSource} instance that + * connects to the embedded database. + *

    Defaults to {@link SimpleDriverDataSourceFactory}. + */ + public void setDataSourceFactory(DataSourceFactory dataSourceFactory) { + Assert.notNull(dataSourceFactory, "DataSourceFactory is required"); + this.dataSourceFactory = dataSourceFactory; + } + + /** + * Set the type of embedded database to use. + *

    Call this when you wish to configure one of the pre-supported types. + *

    Defaults to HSQL. + * @param type the database type + */ + public void setDatabaseType(EmbeddedDatabaseType type) { + this.databaseConfigurer = EmbeddedDatabaseConfigurerFactory.getConfigurer(type); + } + + /** + * Set the strategy that will be used to configure the embedded database instance. + *

    Call this when you wish to use an embedded database type not already supported. + */ + public void setDatabaseConfigurer(EmbeddedDatabaseConfigurer configurer) { + this.databaseConfigurer = configurer; + } + + /** + * Set the strategy that will be used to initialize or populate the embedded + * database. + *

    Defaults to {@code null}. + */ + public void setDatabasePopulator(DatabasePopulator populator) { + this.databasePopulator = populator; + } + + /** + * Factory method that returns the {@linkplain EmbeddedDatabase embedded database} + * instance, which is also a {@link DataSource}. + */ + public EmbeddedDatabase getDatabase() { + if (this.dataSource == null) { + initDatabase(); + } + return new EmbeddedDataSourceProxy(this.dataSource); + } + + + /** + * Hook to initialize the embedded database. + *

    If the {@code generateUniqueDatabaseName} flag has been set to {@code true}, + * the current value of the {@linkplain #setDatabaseName database name} will + * be overridden with an auto-generated name. + *

    Subclasses may call this method to force initialization; however, + * this method should only be invoked once. + *

    After calling this method, {@link #getDataSource()} returns the + * {@link DataSource} providing connectivity to the database. + */ + protected void initDatabase() { + if (this.generateUniqueDatabaseName) { + setDatabaseName(UUID.randomUUID().toString()); + } + + // Create the embedded database first + if (this.databaseConfigurer == null) { + this.databaseConfigurer = EmbeddedDatabaseConfigurerFactory.getConfigurer(EmbeddedDatabaseType.HSQL); + } + this.databaseConfigurer.configureConnectionProperties( + this.dataSourceFactory.getConnectionProperties(), this.databaseName); + this.dataSource = this.dataSourceFactory.getDataSource(); + + if (logger.isInfoEnabled()) { + if (this.dataSource instanceof SimpleDriverDataSource) { + SimpleDriverDataSource simpleDriverDataSource = (SimpleDriverDataSource) this.dataSource; + logger.info(String.format("Starting embedded database: url='%s', username='%s'", + simpleDriverDataSource.getUrl(), simpleDriverDataSource.getUsername())); + } + else { + logger.info(String.format("Starting embedded database '%s'", this.databaseName)); + } + } + + // Now populate the database + if (this.databasePopulator != null) { + try { + DatabasePopulatorUtils.execute(this.databasePopulator, this.dataSource); + } + catch (RuntimeException ex) { + // failed to populate, so leave it as not initialized + shutdownDatabase(); + throw ex; + } + } + } + + /** + * Hook to shutdown the embedded database. Subclasses may call this method + * to force shutdown. + *

    After calling, {@link #getDataSource()} returns {@code null}. + *

    Does nothing if no embedded database has been initialized. + */ + protected void shutdownDatabase() { + if (this.dataSource != null) { + if (logger.isInfoEnabled()) { + if (this.dataSource instanceof SimpleDriverDataSource) { + logger.info(String.format("Shutting down embedded database: url='%s'", + ((SimpleDriverDataSource) this.dataSource).getUrl())); + } + else { + logger.info(String.format("Shutting down embedded database '%s'", this.databaseName)); + } + } + if (this.databaseConfigurer != null) { + this.databaseConfigurer.shutdown(this.dataSource, this.databaseName); + } + this.dataSource = null; + } + } + + /** + * Hook that gets the {@link DataSource} that provides the connectivity to the + * embedded database. + *

    Returns {@code null} if the {@code DataSource} has not been initialized + * or if the database has been shut down. Subclasses may call this method to + * access the {@code DataSource} instance directly. + */ + @Nullable + protected final DataSource getDataSource() { + return this.dataSource; + } + + + private class EmbeddedDataSourceProxy implements EmbeddedDatabase { + + private final DataSource dataSource; + + public EmbeddedDataSourceProxy(DataSource dataSource) { + this.dataSource = dataSource; + } + + @Override + public Connection getConnection() throws SQLException { + return this.dataSource.getConnection(); + } + + @Override + public Connection getConnection(String username, String password) throws SQLException { + return this.dataSource.getConnection(username, password); + } + + @Override + public PrintWriter getLogWriter() throws SQLException { + return this.dataSource.getLogWriter(); + } + + @Override + public void setLogWriter(PrintWriter out) throws SQLException { + this.dataSource.setLogWriter(out); + } + + @Override + public int getLoginTimeout() throws SQLException { + return this.dataSource.getLoginTimeout(); + } + + @Override + public void setLoginTimeout(int seconds) throws SQLException { + this.dataSource.setLoginTimeout(seconds); + } + + @Override + public T unwrap(Class iface) throws SQLException { + return this.dataSource.unwrap(iface); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return this.dataSource.isWrapperFor(iface); + } + + // getParentLogger() is required for JDBC 4.1 compatibility + @Override + public Logger getParentLogger() { + return Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + } + + @Override + public void shutdown() { + shutdownDatabase(); + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryBean.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryBean.java new file mode 100644 index 0000000..0f05006 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryBean.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.embedded; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jdbc.datasource.init.DatabasePopulator; +import org.springframework.jdbc.datasource.init.DatabasePopulatorUtils; +import org.springframework.lang.Nullable; + +/** + * A subclass of {@link EmbeddedDatabaseFactory} that implements {@link FactoryBean} + * for registration as a Spring bean. Returns the actual {@link DataSource} that + * provides connectivity to the embedded database to Spring. + * + *

    The target {@link DataSource} is returned instead of an {@link EmbeddedDatabase} + * proxy since the {@link FactoryBean} will manage the initialization and destruction + * lifecycle of the embedded database instance. + * + *

    Implements {@link DisposableBean} to shutdown the embedded database when the + * managing Spring container is being closed. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + */ +public class EmbeddedDatabaseFactoryBean extends EmbeddedDatabaseFactory + implements FactoryBean, InitializingBean, DisposableBean { + + @Nullable + private DatabasePopulator databaseCleaner; + + + /** + * Set a script execution to be run in the bean destruction callback, + * cleaning up the database and leaving it in a known state for others. + * @param databaseCleaner the database script executor to run on destroy + * @see #setDatabasePopulator + * @see org.springframework.jdbc.datasource.init.DataSourceInitializer#setDatabaseCleaner + */ + public void setDatabaseCleaner(DatabasePopulator databaseCleaner) { + this.databaseCleaner = databaseCleaner; + } + + @Override + public void afterPropertiesSet() { + initDatabase(); + } + + + @Override + @Nullable + public DataSource getObject() { + return getDataSource(); + } + + @Override + public Class getObjectType() { + return DataSource.class; + } + + @Override + public boolean isSingleton() { + return true; + } + + + @Override + public void destroy() { + if (this.databaseCleaner != null && getDataSource() != null) { + DatabasePopulatorUtils.execute(this.databaseCleaner, getDataSource()); + } + shutdownDatabase(); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseType.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseType.java new file mode 100644 index 0000000..8ae88db --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseType.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.embedded; + +/** + * A supported embedded database type. + * + * @author Keith Donald + * @author Oliver Gierke + * @since 3.0 + */ +public enum EmbeddedDatabaseType { + + /** The Hypersonic Embedded Java SQL Database. */ + HSQL, + + /** The H2 Embedded Java SQL Database Engine. */ + H2, + + /** The Apache Derby Embedded SQL Database. */ + DERBY + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/H2EmbeddedDatabaseConfigurer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/H2EmbeddedDatabaseConfigurer.java new file mode 100644 index 0000000..c3a816e --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/H2EmbeddedDatabaseConfigurer.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.embedded; + +import java.sql.Driver; + +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * {@link EmbeddedDatabaseConfigurer} for an H2 embedded database instance. + * + *

    Call {@link #getInstance()} to get the singleton instance of this class. + * + * @author Oliver Gierke + * @author Juergen Hoeller + * @author Sam Brannen + * @since 3.0 + */ +final class H2EmbeddedDatabaseConfigurer extends AbstractEmbeddedDatabaseConfigurer { + + @Nullable + private static H2EmbeddedDatabaseConfigurer instance; + + private final Class driverClass; + + + /** + * Get the singleton {@code H2EmbeddedDatabaseConfigurer} instance. + * @return the configurer instance + * @throws ClassNotFoundException if H2 is not on the classpath + */ + @SuppressWarnings("unchecked") + public static synchronized H2EmbeddedDatabaseConfigurer getInstance() throws ClassNotFoundException { + if (instance == null) { + instance = new H2EmbeddedDatabaseConfigurer( (Class) + ClassUtils.forName("org.h2.Driver", H2EmbeddedDatabaseConfigurer.class.getClassLoader())); + } + return instance; + } + + + private H2EmbeddedDatabaseConfigurer(Class driverClass) { + this.driverClass = driverClass; + } + + @Override + public void configureConnectionProperties(ConnectionProperties properties, String databaseName) { + properties.setDriverClass(this.driverClass); + properties.setUrl(String.format("jdbc:h2:mem:%s;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false", databaseName)); + properties.setUsername("sa"); + properties.setPassword(""); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/HsqlEmbeddedDatabaseConfigurer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/HsqlEmbeddedDatabaseConfigurer.java new file mode 100644 index 0000000..8f3c69f --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/HsqlEmbeddedDatabaseConfigurer.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.embedded; + +import java.sql.Driver; + +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * {@link EmbeddedDatabaseConfigurer} for an HSQL embedded database instance. + * + *

    Call {@link #getInstance()} to get the singleton instance of this class. + * + * @author Keith Donald + * @author Oliver Gierke + * @since 3.0 + */ +final class HsqlEmbeddedDatabaseConfigurer extends AbstractEmbeddedDatabaseConfigurer { + + @Nullable + private static HsqlEmbeddedDatabaseConfigurer instance; + + private final Class driverClass; + + + /** + * Get the singleton {@link HsqlEmbeddedDatabaseConfigurer} instance. + * @return the configurer instance + * @throws ClassNotFoundException if HSQL is not on the classpath + */ + @SuppressWarnings("unchecked") + public static synchronized HsqlEmbeddedDatabaseConfigurer getInstance() throws ClassNotFoundException { + if (instance == null) { + instance = new HsqlEmbeddedDatabaseConfigurer( (Class) + ClassUtils.forName("org.hsqldb.jdbcDriver", HsqlEmbeddedDatabaseConfigurer.class.getClassLoader())); + } + return instance; + } + + + private HsqlEmbeddedDatabaseConfigurer(Class driverClass) { + this.driverClass = driverClass; + } + + @Override + public void configureConnectionProperties(ConnectionProperties properties, String databaseName) { + properties.setDriverClass(this.driverClass); + properties.setUrl("jdbc:hsqldb:mem:" + databaseName); + properties.setUsername("sa"); + properties.setPassword(""); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/OutputStreamFactory.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/OutputStreamFactory.java new file mode 100644 index 0000000..1c913af --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/OutputStreamFactory.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.embedded; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Internal helper for exposing dummy OutputStreams to embedded databases + * such as Derby, preventing the creation of a log file. + * + * @author Juergen Hoeller + * @since 3.0 + */ +public final class OutputStreamFactory { + + private OutputStreamFactory() { + } + + + /** + * Returns an {@link java.io.OutputStream} that ignores all data given to it. + */ + public static OutputStream getNoopOutputStream() { + return new OutputStream() { + @Override + public void write(int b) throws IOException { + // ignore the output + } + }; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/SimpleDriverDataSourceFactory.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/SimpleDriverDataSourceFactory.java new file mode 100644 index 0000000..b1a5850 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/SimpleDriverDataSourceFactory.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.embedded; + +import java.sql.Driver; + +import javax.sql.DataSource; + +import org.springframework.jdbc.datasource.SimpleDriverDataSource; + +/** + * Creates a {@link SimpleDriverDataSource}. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 3.0 + */ +final class SimpleDriverDataSourceFactory implements DataSourceFactory { + + private final SimpleDriverDataSource dataSource = new SimpleDriverDataSource(); + + @Override + public ConnectionProperties getConnectionProperties() { + return new ConnectionProperties() { + @Override + public void setDriverClass(Class driverClass) { + dataSource.setDriverClass(driverClass); + } + + @Override + public void setUrl(String url) { + dataSource.setUrl(url); + } + + @Override + public void setUsername(String username) { + dataSource.setUsername(username); + } + + @Override + public void setPassword(String password) { + dataSource.setPassword(password); + } + }; + } + + @Override + public DataSource getDataSource() { + return this.dataSource; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/package-info.java new file mode 100644 index 0000000..2f9576f --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/package-info.java @@ -0,0 +1,9 @@ +/** + * Provides extensible support for creating embedded database instances. + */ +@NonNullApi +@NonNullFields +package org.springframework.jdbc.datasource.embedded; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/CannotReadScriptException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/CannotReadScriptException.java new file mode 100644 index 0000000..05d389a --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/CannotReadScriptException.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.init; + +import org.springframework.core.io.support.EncodedResource; + +/** + * Thrown by {@link ScriptUtils} if an SQL script cannot be read. + * + * @author Keith Donald + * @author Sam Brannen + * @since 3.0 + */ +@SuppressWarnings("serial") +public class CannotReadScriptException extends ScriptException { + + /** + * Construct a new {@code CannotReadScriptException}. + * @param resource the resource that cannot be read from + * @param cause the underlying cause of the resource access failure + */ + public CannotReadScriptException(EncodedResource resource, Throwable cause) { + super("Cannot read SQL script from " + resource, cause); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/CompositeDatabasePopulator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/CompositeDatabasePopulator.java new file mode 100644 index 0000000..1896082 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/CompositeDatabasePopulator.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.init; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +/** + * Composite {@link DatabasePopulator} that delegates to a list of given + * {@code DatabasePopulator} implementations, executing all scripts. + * + * @author Dave Syer + * @author Juergen Hoeller + * @author Sam Brannen + * @author Kazuki Shimizu + * @since 3.1 + */ +public class CompositeDatabasePopulator implements DatabasePopulator { + + private final List populators = new ArrayList<>(4); + + + /** + * Create an empty {@code CompositeDatabasePopulator}. + * @see #setPopulators + * @see #addPopulators + */ + public CompositeDatabasePopulator() { + } + + /** + * Create a {@code CompositeDatabasePopulator} with the given populators. + * @param populators one or more populators to delegate to + * @since 4.3 + */ + public CompositeDatabasePopulator(Collection populators) { + this.populators.addAll(populators); + } + + /** + * Create a {@code CompositeDatabasePopulator} with the given populators. + * @param populators one or more populators to delegate to + * @since 4.3 + */ + public CompositeDatabasePopulator(DatabasePopulator... populators) { + this.populators.addAll(Arrays.asList(populators)); + } + + + /** + * Specify one or more populators to delegate to. + */ + public void setPopulators(DatabasePopulator... populators) { + this.populators.clear(); + this.populators.addAll(Arrays.asList(populators)); + } + + /** + * Add one or more populators to the list of delegates. + */ + public void addPopulators(DatabasePopulator... populators) { + this.populators.addAll(Arrays.asList(populators)); + } + + + @Override + public void populate(Connection connection) throws SQLException, ScriptException { + for (DatabasePopulator populator : this.populators) { + populator.populate(connection); + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/DataSourceInitializer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/DataSourceInitializer.java new file mode 100644 index 0000000..d2bb975 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/DataSourceInitializer.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.init; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Used to {@linkplain #setDatabasePopulator set up} a database during + * initialization and {@link #setDatabaseCleaner clean up} a database during + * destruction. + * + * @author Dave Syer + * @author Sam Brannen + * @since 3.0 + * @see DatabasePopulator + */ +public class DataSourceInitializer implements InitializingBean, DisposableBean { + + @Nullable + private DataSource dataSource; + + @Nullable + private DatabasePopulator databasePopulator; + + @Nullable + private DatabasePopulator databaseCleaner; + + private boolean enabled = true; + + + /** + * The {@link DataSource} for the database to populate when this component + * is initialized and to clean up when this component is shut down. + *

    This property is mandatory with no default provided. + * @param dataSource the DataSource + */ + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + } + + /** + * Set the {@link DatabasePopulator} to execute during the bean initialization phase. + * @param databasePopulator the {@code DatabasePopulator} to use during initialization + * @see #setDatabaseCleaner + */ + public void setDatabasePopulator(DatabasePopulator databasePopulator) { + this.databasePopulator = databasePopulator; + } + + /** + * Set the {@link DatabasePopulator} to execute during the bean destruction + * phase, cleaning up the database and leaving it in a known state for others. + * @param databaseCleaner the {@code DatabasePopulator} to use during destruction + * @see #setDatabasePopulator + */ + public void setDatabaseCleaner(DatabasePopulator databaseCleaner) { + this.databaseCleaner = databaseCleaner; + } + + /** + * Flag to explicitly enable or disable the {@linkplain #setDatabasePopulator + * database populator} and {@linkplain #setDatabaseCleaner database cleaner}. + * @param enabled {@code true} if the database populator and database cleaner + * should be called on startup and shutdown, respectively + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + + /** + * Use the {@linkplain #setDatabasePopulator database populator} to set up + * the database. + */ + @Override + public void afterPropertiesSet() { + execute(this.databasePopulator); + } + + /** + * Use the {@linkplain #setDatabaseCleaner database cleaner} to clean up the + * database. + */ + @Override + public void destroy() { + execute(this.databaseCleaner); + } + + private void execute(@Nullable DatabasePopulator populator) { + Assert.state(this.dataSource != null, "DataSource must be set"); + if (this.enabled && populator != null) { + DatabasePopulatorUtils.execute(populator, this.dataSource); + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/DatabasePopulator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/DatabasePopulator.java new file mode 100644 index 0000000..45262b8 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/DatabasePopulator.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.init; + +import java.sql.Connection; +import java.sql.SQLException; + +/** + * Strategy used to populate, initialize, or clean up a database. + * + * @author Keith Donald + * @author Sam Brannen + * @since 3.0 + * @see ResourceDatabasePopulator + * @see DatabasePopulatorUtils + * @see DataSourceInitializer + */ +@FunctionalInterface +public interface DatabasePopulator { + + /** + * Populate, initialize, or clean up the database using the provided JDBC + * connection. + *

    Concrete implementations may throw an {@link SQLException} if + * an error is encountered but are strongly encouraged to throw a + * specific {@link ScriptException} instead. For example, Spring's + * {@link ResourceDatabasePopulator} and {@link DatabasePopulatorUtils} wrap + * all {@code SQLExceptions} in {@code ScriptExceptions}. + * @param connection the JDBC connection to use to populate the db; already + * configured and ready to use; never {@code null} + * @throws SQLException if an unrecoverable data access exception occurs + * during database population + * @throws ScriptException in all other error cases + * @see DatabasePopulatorUtils#execute + */ + void populate(Connection connection) throws SQLException, ScriptException; + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/DatabasePopulatorUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/DatabasePopulatorUtils.java new file mode 100644 index 0000000..3861cb5 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/DatabasePopulatorUtils.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.init; + +import java.sql.Connection; + +import javax.sql.DataSource; + +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.util.Assert; + +/** + * Utility methods for executing a {@link DatabasePopulator}. + * + * @author Juergen Hoeller + * @author Oliver Gierke + * @author Sam Brannen + * @since 3.1 + */ +public abstract class DatabasePopulatorUtils { + + /** + * Execute the given {@link DatabasePopulator} against the given {@link DataSource}. + * @param populator the {@code DatabasePopulator} to execute + * @param dataSource the {@code DataSource} to execute against + * @throws DataAccessException if an error occurs, specifically a {@link ScriptException} + */ + public static void execute(DatabasePopulator populator, DataSource dataSource) throws DataAccessException { + Assert.notNull(populator, "DatabasePopulator must not be null"); + Assert.notNull(dataSource, "DataSource must not be null"); + try { + Connection connection = DataSourceUtils.getConnection(dataSource); + try { + populator.populate(connection); + } + finally { + DataSourceUtils.releaseConnection(connection, dataSource); + } + } + catch (ScriptException ex) { + throw ex; + } + catch (Throwable ex) { + throw new UncategorizedScriptException("Failed to execute database script", ex); + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ResourceDatabasePopulator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ResourceDatabasePopulator.java new file mode 100644 index 0000000..214a857 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ResourceDatabasePopulator.java @@ -0,0 +1,272 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.init; + +import java.sql.Connection; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.sql.DataSource; + +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Populates, initializes, or cleans up a database using SQL scripts defined in + * external resources. + * + *

      + *
    • Call {@link #addScript} to add a single SQL script location. + *
    • Call {@link #addScripts} to add multiple SQL script locations. + *
    • Consult the setter methods in this class for further configuration options. + *
    • Call {@link #populate} or {@link #execute} to initialize or clean up the + * database using the configured scripts. + *
    + * + * @author Keith Donald + * @author Dave Syer + * @author Juergen Hoeller + * @author Chris Beams + * @author Oliver Gierke + * @author Sam Brannen + * @author Chris Baldwin + * @author Phillip Webb + * @since 3.0 + * @see DatabasePopulatorUtils + * @see ScriptUtils + */ +public class ResourceDatabasePopulator implements DatabasePopulator { + + List scripts = new ArrayList<>(); + + @Nullable + private String sqlScriptEncoding; + + private String separator = ScriptUtils.DEFAULT_STATEMENT_SEPARATOR; + + private String[] commentPrefixes = ScriptUtils.DEFAULT_COMMENT_PREFIXES; + + private String blockCommentStartDelimiter = ScriptUtils.DEFAULT_BLOCK_COMMENT_START_DELIMITER; + + private String blockCommentEndDelimiter = ScriptUtils.DEFAULT_BLOCK_COMMENT_END_DELIMITER; + + private boolean continueOnError = false; + + private boolean ignoreFailedDrops = false; + + + /** + * Construct a new {@code ResourceDatabasePopulator} with default settings. + * @since 4.0.3 + */ + public ResourceDatabasePopulator() { + } + + /** + * Construct a new {@code ResourceDatabasePopulator} with default settings + * for the supplied scripts. + * @param scripts the scripts to execute to initialize or clean up the database + * (never {@code null}) + * @since 4.0.3 + */ + public ResourceDatabasePopulator(Resource... scripts) { + setScripts(scripts); + } + + /** + * Construct a new {@code ResourceDatabasePopulator} with the supplied values. + * @param continueOnError flag to indicate that all failures in SQL should be + * logged but not cause a failure + * @param ignoreFailedDrops flag to indicate that a failed SQL {@code DROP} + * statement can be ignored + * @param sqlScriptEncoding the encoding for the supplied SQL scripts + * (may be {@code null} or empty to indicate platform encoding) + * @param scripts the scripts to execute to initialize or clean up the database + * (never {@code null}) + * @since 4.0.3 + */ + public ResourceDatabasePopulator(boolean continueOnError, boolean ignoreFailedDrops, + @Nullable String sqlScriptEncoding, Resource... scripts) { + + this.continueOnError = continueOnError; + this.ignoreFailedDrops = ignoreFailedDrops; + setSqlScriptEncoding(sqlScriptEncoding); + setScripts(scripts); + } + + + /** + * Add a script to execute to initialize or clean up the database. + * @param script the path to an SQL script (never {@code null}) + */ + public void addScript(Resource script) { + Assert.notNull(script, "'script' must not be null"); + this.scripts.add(script); + } + + /** + * Add multiple scripts to execute to initialize or clean up the database. + * @param scripts the scripts to execute (never {@code null}) + */ + public void addScripts(Resource... scripts) { + assertContentsOfScriptArray(scripts); + this.scripts.addAll(Arrays.asList(scripts)); + } + + /** + * Set the scripts to execute to initialize or clean up the database, + * replacing any previously added scripts. + * @param scripts the scripts to execute (never {@code null}) + */ + public void setScripts(Resource... scripts) { + assertContentsOfScriptArray(scripts); + // Ensure that the list is modifiable + this.scripts = new ArrayList<>(Arrays.asList(scripts)); + } + + private void assertContentsOfScriptArray(Resource... scripts) { + Assert.notNull(scripts, "'scripts' must not be null"); + Assert.noNullElements(scripts, "'scripts' must not contain null elements"); + } + + /** + * Specify the encoding for the configured SQL scripts, + * if different from the platform encoding. + * @param sqlScriptEncoding the encoding used in scripts + * (may be {@code null} or empty to indicate platform encoding) + * @see #addScript(Resource) + */ + public void setSqlScriptEncoding(@Nullable String sqlScriptEncoding) { + this.sqlScriptEncoding = (StringUtils.hasText(sqlScriptEncoding) ? sqlScriptEncoding : null); + } + + /** + * Specify the statement separator, if a custom one. + *

    Defaults to {@code ";"} if not specified and falls back to {@code "\n"} + * as a last resort; may be set to {@link ScriptUtils#EOF_STATEMENT_SEPARATOR} + * to signal that each script contains a single statement without a separator. + * @param separator the script statement separator + */ + public void setSeparator(String separator) { + this.separator = separator; + } + + /** + * Set the prefix that identifies single-line comments within the SQL scripts. + *

    Defaults to {@code "--"}. + * @param commentPrefix the prefix for single-line comments + * @see #setCommentPrefixes(String...) + */ + public void setCommentPrefix(String commentPrefix) { + Assert.hasText(commentPrefix, "'commentPrefix' must not be null or empty"); + this.commentPrefixes = new String[] { commentPrefix }; + } + + /** + * Set the prefixes that identify single-line comments within the SQL scripts. + *

    Defaults to {@code ["--"]}. + * @param commentPrefixes the prefixes for single-line comments + * @since 5.2 + */ + public void setCommentPrefixes(String... commentPrefixes) { + Assert.notEmpty(commentPrefixes, "'commentPrefixes' must not be null or empty"); + Assert.noNullElements(commentPrefixes, "'commentPrefixes' must not contain null elements"); + this.commentPrefixes = commentPrefixes; + } + + /** + * Set the start delimiter that identifies block comments within the SQL + * scripts. + *

    Defaults to {@code "/*"}. + * @param blockCommentStartDelimiter the start delimiter for block comments + * (never {@code null} or empty) + * @since 4.0.3 + * @see #setBlockCommentEndDelimiter + */ + public void setBlockCommentStartDelimiter(String blockCommentStartDelimiter) { + Assert.hasText(blockCommentStartDelimiter, "'blockCommentStartDelimiter' must not be null or empty"); + this.blockCommentStartDelimiter = blockCommentStartDelimiter; + } + + /** + * Set the end delimiter that identifies block comments within the SQL + * scripts. + *

    Defaults to "*/". + * @param blockCommentEndDelimiter the end delimiter for block comments + * (never {@code null} or empty) + * @since 4.0.3 + * @see #setBlockCommentStartDelimiter + */ + public void setBlockCommentEndDelimiter(String blockCommentEndDelimiter) { + Assert.hasText(blockCommentEndDelimiter, "'blockCommentEndDelimiter' must not be null or empty"); + this.blockCommentEndDelimiter = blockCommentEndDelimiter; + } + + /** + * Flag to indicate that all failures in SQL should be logged but not cause a failure. + *

    Defaults to {@code false}. + * @param continueOnError {@code true} if script execution should continue on error + */ + public void setContinueOnError(boolean continueOnError) { + this.continueOnError = continueOnError; + } + + /** + * Flag to indicate that a failed SQL {@code DROP} statement can be ignored. + *

    This is useful for a non-embedded database whose SQL dialect does not + * support an {@code IF EXISTS} clause in a {@code DROP} statement. + *

    The default is {@code false} so that if the populator runs accidentally, it will + * fail fast if a script starts with a {@code DROP} statement. + * @param ignoreFailedDrops {@code true} if failed drop statements should be ignored + */ + public void setIgnoreFailedDrops(boolean ignoreFailedDrops) { + this.ignoreFailedDrops = ignoreFailedDrops; + } + + + /** + * {@inheritDoc} + * @see #execute(DataSource) + */ + @Override + public void populate(Connection connection) throws ScriptException { + Assert.notNull(connection, "'connection' must not be null"); + for (Resource script : this.scripts) { + EncodedResource encodedScript = new EncodedResource(script, this.sqlScriptEncoding); + ScriptUtils.executeSqlScript(connection, encodedScript, this.continueOnError, this.ignoreFailedDrops, + this.commentPrefixes, this.separator, this.blockCommentStartDelimiter, this.blockCommentEndDelimiter); + } + } + + /** + * Execute this {@code ResourceDatabasePopulator} against the given + * {@link DataSource}. + *

    Delegates to {@link DatabasePopulatorUtils#execute}. + * @param dataSource the {@code DataSource} to execute against (never {@code null}) + * @throws ScriptException if an error occurs + * @since 4.1 + * @see #populate(Connection) + */ + public void execute(DataSource dataSource) throws ScriptException { + DatabasePopulatorUtils.execute(this, dataSource); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptException.java new file mode 100644 index 0000000..4699eff --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptException.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.init; + +import org.springframework.dao.DataAccessException; +import org.springframework.lang.Nullable; + +/** + * Root of the hierarchy of data access exceptions that are related to processing + * of SQL scripts. + * + * @author Sam Brannen + * @since 4.0.3 + */ +@SuppressWarnings("serial") +public abstract class ScriptException extends DataAccessException { + + /** + * Constructor for {@code ScriptException}. + * @param message the detail message + */ + public ScriptException(String message) { + super(message); + } + + /** + * Constructor for {@code ScriptException}. + * @param message the detail message + * @param cause the root cause + */ + public ScriptException(String message, @Nullable Throwable cause) { + super(message, cause); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptParseException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptParseException.java new file mode 100644 index 0000000..29cd879 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptParseException.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.init; + +import org.springframework.core.io.support.EncodedResource; +import org.springframework.lang.Nullable; + +/** + * Thrown by {@link ScriptUtils} if an SQL script cannot be properly parsed. + * + * @author Sam Brannen + * @since 4.0.3 + */ +@SuppressWarnings("serial") +public class ScriptParseException extends ScriptException { + + /** + * Construct a new {@code ScriptParseException}. + * @param message detailed message + * @param resource the resource from which the SQL script was read + */ + public ScriptParseException(String message, @Nullable EncodedResource resource) { + super(buildMessage(message, resource)); + } + + /** + * Construct a new {@code ScriptParseException}. + * @param message detailed message + * @param resource the resource from which the SQL script was read + * @param cause the underlying cause of the failure + */ + public ScriptParseException(String message, @Nullable EncodedResource resource, @Nullable Throwable cause) { + super(buildMessage(message, resource), cause); + } + + + private static String buildMessage(String message, @Nullable EncodedResource resource) { + return String.format("Failed to parse SQL script from resource [%s]: %s", + (resource == null ? "" : resource), message); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptStatementFailedException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptStatementFailedException.java new file mode 100644 index 0000000..13feee6 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptStatementFailedException.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.init; + +import org.springframework.core.io.support.EncodedResource; + +/** + * Thrown by {@link ScriptUtils} if a statement in an SQL script failed when + * executing it against the target database. + * + * @author Juergen Hoeller + * @author Sam Brannen + * @since 3.0.5 + */ +@SuppressWarnings("serial") +public class ScriptStatementFailedException extends ScriptException { + + /** + * Construct a new {@code ScriptStatementFailedException}. + * @param stmt the actual SQL statement that failed + * @param stmtNumber the statement number in the SQL script (i.e., + * the nth statement present in the resource) + * @param encodedResource the resource from which the SQL statement was read + * @param cause the underlying cause of the failure + */ + public ScriptStatementFailedException(String stmt, int stmtNumber, EncodedResource encodedResource, Throwable cause) { + super(buildErrorMessage(stmt, stmtNumber, encodedResource), cause); + } + + + /** + * Build an error message for an SQL script execution failure, + * based on the supplied arguments. + * @param stmt the actual SQL statement that failed + * @param stmtNumber the statement number in the SQL script (i.e., + * the nth statement present in the resource) + * @param encodedResource the resource from which the SQL statement was read + * @return an error message suitable for an exception's detail message + * or logging + * @since 4.2 + */ + public static String buildErrorMessage(String stmt, int stmtNumber, EncodedResource encodedResource) { + return String.format("Failed to execute SQL script statement #%s of %s: %s", stmtNumber, encodedResource, stmt); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptUtils.java new file mode 100644 index 0000000..fa87e08 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptUtils.java @@ -0,0 +1,650 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.init; + +import java.io.IOException; +import java.io.LineNumberReader; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Generic utility methods for working with SQL scripts. + * + *

    Mainly for internal use within the framework. + * + * @author Thomas Risberg + * @author Sam Brannen + * @author Juergen Hoeller + * @author Keith Donald + * @author Dave Syer + * @author Chris Beams + * @author Oliver Gierke + * @author Chris Baldwin + * @author Nicolas Debeissat + * @author Phillip Webb + * @since 4.0.3 + */ +public abstract class ScriptUtils { + + /** + * Default statement separator within SQL scripts: {@code ";"}. + */ + public static final String DEFAULT_STATEMENT_SEPARATOR = ";"; + + /** + * Fallback statement separator within SQL scripts: {@code "\n"}. + *

    Used if neither a custom separator nor the + * {@link #DEFAULT_STATEMENT_SEPARATOR} is present in a given script. + */ + public static final String FALLBACK_STATEMENT_SEPARATOR = "\n"; + + /** + * End of file (EOF) SQL statement separator: {@code "^^^ END OF SCRIPT ^^^"}. + *

    This value may be supplied as the {@code separator} to {@link + * #executeSqlScript(Connection, EncodedResource, boolean, boolean, String, String, String, String)} + * to denote that an SQL script contains a single statement (potentially + * spanning multiple lines) with no explicit statement separator. Note that + * such a script should not actually contain this value; it is merely a + * virtual statement separator. + */ + public static final String EOF_STATEMENT_SEPARATOR = "^^^ END OF SCRIPT ^^^"; + + /** + * Default prefix for single-line comments within SQL scripts: {@code "--"}. + */ + public static final String DEFAULT_COMMENT_PREFIX = "--"; + + /** + * Default prefixes for single-line comments within SQL scripts: {@code ["--"]}. + * @since 5.2 + */ + public static final String[] DEFAULT_COMMENT_PREFIXES = {DEFAULT_COMMENT_PREFIX}; + + /** + * Default start delimiter for block comments within SQL scripts: {@code "/*"}. + */ + public static final String DEFAULT_BLOCK_COMMENT_START_DELIMITER = "/*"; + + /** + * Default end delimiter for block comments within SQL scripts: "*/". + */ + public static final String DEFAULT_BLOCK_COMMENT_END_DELIMITER = "*/"; + + + private static final Log logger = LogFactory.getLog(ScriptUtils.class); + + + /** + * Split an SQL script into separate statements delimited by the provided + * separator character. Each individual statement will be added to the + * provided {@code List}. + *

    Within the script, {@value #DEFAULT_COMMENT_PREFIX} will be used as the + * comment prefix; any text beginning with the comment prefix and extending to + * the end of the line will be omitted from the output. Similarly, + * {@value #DEFAULT_BLOCK_COMMENT_START_DELIMITER} and + * {@value #DEFAULT_BLOCK_COMMENT_END_DELIMITER} will be used as the + * start and end block comment delimiters: any text enclosed + * in a block comment will be omitted from the output. In addition, multiple + * adjacent whitespace characters will be collapsed into a single space. + * @param script the SQL script + * @param separator character separating each statement (typically a ';') + * @param statements the list that will contain the individual statements + * @throws ScriptException if an error occurred while splitting the SQL script + * @see #splitSqlScript(String, String, List) + * @see #splitSqlScript(EncodedResource, String, String, String, String, String, List) + */ + public static void splitSqlScript(String script, char separator, List statements) throws ScriptException { + splitSqlScript(script, String.valueOf(separator), statements); + } + + /** + * Split an SQL script into separate statements delimited by the provided + * separator string. Each individual statement will be added to the + * provided {@code List}. + *

    Within the script, {@value #DEFAULT_COMMENT_PREFIX} will be used as the + * comment prefix; any text beginning with the comment prefix and extending to + * the end of the line will be omitted from the output. Similarly, + * {@value #DEFAULT_BLOCK_COMMENT_START_DELIMITER} and + * {@value #DEFAULT_BLOCK_COMMENT_END_DELIMITER} will be used as the + * start and end block comment delimiters: any text enclosed + * in a block comment will be omitted from the output. In addition, multiple + * adjacent whitespace characters will be collapsed into a single space. + * @param script the SQL script + * @param separator text separating each statement + * (typically a ';' or newline character) + * @param statements the list that will contain the individual statements + * @throws ScriptException if an error occurred while splitting the SQL script + * @see #splitSqlScript(String, char, List) + * @see #splitSqlScript(EncodedResource, String, String, String, String, String, List) + */ + public static void splitSqlScript(String script, String separator, List statements) throws ScriptException { + splitSqlScript(null, script, separator, DEFAULT_COMMENT_PREFIX, DEFAULT_BLOCK_COMMENT_START_DELIMITER, + DEFAULT_BLOCK_COMMENT_END_DELIMITER, statements); + } + + /** + * Split an SQL script into separate statements delimited by the provided + * separator string. Each individual statement will be added to the provided + * {@code List}. + *

    Within the script, the provided {@code commentPrefix} will be honored: + * any text beginning with the comment prefix and extending to the end of the + * line will be omitted from the output. Similarly, the provided + * {@code blockCommentStartDelimiter} and {@code blockCommentEndDelimiter} + * delimiters will be honored: any text enclosed in a block comment will be + * omitted from the output. In addition, multiple adjacent whitespace characters + * will be collapsed into a single space. + * @param resource the resource from which the script was read + * @param script the SQL script + * @param separator text separating each statement + * (typically a ';' or newline character) + * @param commentPrefix the prefix that identifies SQL line comments + * (typically "--") + * @param blockCommentStartDelimiter the start block comment delimiter; + * never {@code null} or empty + * @param blockCommentEndDelimiter the end block comment delimiter; + * never {@code null} or empty + * @param statements the list that will contain the individual statements + * @throws ScriptException if an error occurred while splitting the SQL script + */ + public static void splitSqlScript(@Nullable EncodedResource resource, String script, + String separator, String commentPrefix, String blockCommentStartDelimiter, + String blockCommentEndDelimiter, List statements) throws ScriptException { + + Assert.hasText(commentPrefix, "'commentPrefix' must not be null or empty"); + splitSqlScript(resource, script, separator, new String[] { commentPrefix }, + blockCommentStartDelimiter, blockCommentEndDelimiter, statements); + } + + /** + * Split an SQL script into separate statements delimited by the provided + * separator string. Each individual statement will be added to the provided + * {@code List}. + *

    Within the script, the provided {@code commentPrefixes} will be honored: + * any text beginning with one of the comment prefixes and extending to the + * end of the line will be omitted from the output. Similarly, the provided + * {@code blockCommentStartDelimiter} and {@code blockCommentEndDelimiter} + * delimiters will be honored: any text enclosed in a block comment will be + * omitted from the output. In addition, multiple adjacent whitespace characters + * will be collapsed into a single space. + * @param resource the resource from which the script was read + * @param script the SQL script + * @param separator text separating each statement + * (typically a ';' or newline character) + * @param commentPrefixes the prefixes that identify SQL line comments + * (typically "--") + * @param blockCommentStartDelimiter the start block comment delimiter; + * never {@code null} or empty + * @param blockCommentEndDelimiter the end block comment delimiter; + * never {@code null} or empty + * @param statements the list that will contain the individual statements + * @throws ScriptException if an error occurred while splitting the SQL script + * @since 5.2 + */ + public static void splitSqlScript(@Nullable EncodedResource resource, String script, + String separator, String[] commentPrefixes, String blockCommentStartDelimiter, + String blockCommentEndDelimiter, List statements) throws ScriptException { + + Assert.hasText(script, "'script' must not be null or empty"); + Assert.notNull(separator, "'separator' must not be null"); + Assert.notEmpty(commentPrefixes, "'commentPrefixes' must not be null or empty"); + for (String commentPrefix : commentPrefixes) { + Assert.hasText(commentPrefix, "'commentPrefixes' must not contain null or empty elements"); + } + Assert.hasText(blockCommentStartDelimiter, "'blockCommentStartDelimiter' must not be null or empty"); + Assert.hasText(blockCommentEndDelimiter, "'blockCommentEndDelimiter' must not be null or empty"); + + StringBuilder sb = new StringBuilder(); + boolean inSingleQuote = false; + boolean inDoubleQuote = false; + boolean inEscape = false; + + for (int i = 0; i < script.length(); i++) { + char c = script.charAt(i); + if (inEscape) { + inEscape = false; + sb.append(c); + continue; + } + // MySQL style escapes + if (c == '\\') { + inEscape = true; + sb.append(c); + continue; + } + if (!inDoubleQuote && (c == '\'')) { + inSingleQuote = !inSingleQuote; + } + else if (!inSingleQuote && (c == '"')) { + inDoubleQuote = !inDoubleQuote; + } + if (!inSingleQuote && !inDoubleQuote) { + if (script.startsWith(separator, i)) { + // We've reached the end of the current statement + if (sb.length() > 0) { + statements.add(sb.toString()); + sb = new StringBuilder(); + } + i += separator.length() - 1; + continue; + } + else if (startsWithAny(script, commentPrefixes, i)) { + // Skip over any content from the start of the comment to the EOL + int indexOfNextNewline = script.indexOf('\n', i); + if (indexOfNextNewline > i) { + i = indexOfNextNewline; + continue; + } + else { + // If there's no EOL, we must be at the end of the script, so stop here. + break; + } + } + else if (script.startsWith(blockCommentStartDelimiter, i)) { + // Skip over any block comments + int indexOfCommentEnd = script.indexOf(blockCommentEndDelimiter, i); + if (indexOfCommentEnd > i) { + i = indexOfCommentEnd + blockCommentEndDelimiter.length() - 1; + continue; + } + else { + throw new ScriptParseException( + "Missing block comment end delimiter: " + blockCommentEndDelimiter, resource); + } + } + else if (c == ' ' || c == '\r' || c == '\n' || c == '\t') { + // Avoid multiple adjacent whitespace characters + if (sb.length() > 0 && sb.charAt(sb.length() - 1) != ' ') { + c = ' '; + } + else { + continue; + } + } + } + sb.append(c); + } + + if (StringUtils.hasText(sb)) { + statements.add(sb.toString()); + } + } + + /** + * Read a script from the given resource, using "{@code --}" as the comment prefix + * and "{@code ;}" as the statement separator, and build a String containing the lines. + * @param resource the {@code EncodedResource} to be read + * @return {@code String} containing the script lines + * @throws IOException in case of I/O errors + */ + static String readScript(EncodedResource resource) throws IOException { + return readScript(resource, DEFAULT_COMMENT_PREFIXES, DEFAULT_STATEMENT_SEPARATOR, DEFAULT_BLOCK_COMMENT_END_DELIMITER); + } + + /** + * Read a script from the provided resource, using the supplied comment prefixes + * and statement separator, and build a {@code String} containing the lines. + *

    Lines beginning with one of the comment prefixes are excluded + * from the results; however, line comments anywhere else — for example, + * within a statement — will be included in the results. + * @param resource the {@code EncodedResource} containing the script + * to be processed + * @param commentPrefixes the prefixes that identify comments in the SQL script + * (typically "--") + * @param separator the statement separator in the SQL script (typically ";") + * @param blockCommentEndDelimiter the end block comment delimiter + * @return a {@code String} containing the script lines + * @throws IOException in case of I/O errors + */ + private static String readScript(EncodedResource resource, @Nullable String[] commentPrefixes, + @Nullable String separator, @Nullable String blockCommentEndDelimiter) throws IOException { + + try (LineNumberReader lnr = new LineNumberReader(resource.getReader())) { + return readScript(lnr, commentPrefixes, separator, blockCommentEndDelimiter); + } + } + + /** + * Read a script from the provided {@code LineNumberReader}, using the supplied + * comment prefix and statement separator, and build a {@code String} containing + * the lines. + *

    Lines beginning with the comment prefix are excluded from the + * results; however, line comments anywhere else — for example, within + * a statement — will be included in the results. + * @param lineNumberReader the {@code LineNumberReader} containing the script + * to be processed + * @param lineCommentPrefix the prefix that identifies comments in the SQL script + * (typically "--") + * @param separator the statement separator in the SQL script (typically ";") + * @param blockCommentEndDelimiter the end block comment delimiter + * @return a {@code String} containing the script lines + * @throws IOException in case of I/O errors + */ + public static String readScript(LineNumberReader lineNumberReader, @Nullable String lineCommentPrefix, + @Nullable String separator, @Nullable String blockCommentEndDelimiter) throws IOException { + + String[] lineCommentPrefixes = (lineCommentPrefix != null) ? new String[] { lineCommentPrefix } : null; + return readScript(lineNumberReader, lineCommentPrefixes, separator, blockCommentEndDelimiter); + } + + /** + * Read a script from the provided {@code LineNumberReader}, using the supplied + * comment prefixes and statement separator, and build a {@code String} containing + * the lines. + *

    Lines beginning with one of the comment prefixes are excluded + * from the results; however, line comments anywhere else — for example, + * within a statement — will be included in the results. + * @param lineNumberReader the {@code LineNumberReader} containing the script + * to be processed + * @param lineCommentPrefixes the prefixes that identify comments in the SQL script + * (typically "--") + * @param separator the statement separator in the SQL script (typically ";") + * @param blockCommentEndDelimiter the end block comment delimiter + * @return a {@code String} containing the script lines + * @throws IOException in case of I/O errors + * @since 5.2 + */ + public static String readScript(LineNumberReader lineNumberReader, @Nullable String[] lineCommentPrefixes, + @Nullable String separator, @Nullable String blockCommentEndDelimiter) throws IOException { + + String currentStatement = lineNumberReader.readLine(); + StringBuilder scriptBuilder = new StringBuilder(); + while (currentStatement != null) { + if ((blockCommentEndDelimiter != null && currentStatement.contains(blockCommentEndDelimiter)) || + (lineCommentPrefixes != null && !startsWithAny(currentStatement, lineCommentPrefixes, 0))) { + if (scriptBuilder.length() > 0) { + scriptBuilder.append('\n'); + } + scriptBuilder.append(currentStatement); + } + currentStatement = lineNumberReader.readLine(); + } + appendSeparatorToScriptIfNecessary(scriptBuilder, separator); + return scriptBuilder.toString(); + } + + private static void appendSeparatorToScriptIfNecessary(StringBuilder scriptBuilder, @Nullable String separator) { + if (separator == null) { + return; + } + String trimmed = separator.trim(); + if (trimmed.length() == separator.length()) { + return; + } + // separator ends in whitespace, so we might want to see if the script is trying + // to end the same way + if (scriptBuilder.lastIndexOf(trimmed) == scriptBuilder.length() - trimmed.length()) { + scriptBuilder.append(separator.substring(trimmed.length())); + } + } + + private static boolean startsWithAny(String script, String[] prefixes, int offset) { + for (String prefix : prefixes) { + if (script.startsWith(prefix, offset)) { + return true; + } + } + return false; + } + + /** + * Does the provided SQL script contain the specified delimiter? + * @param script the SQL script + * @param delim the string delimiting each statement - typically a ';' character + */ + public static boolean containsSqlScriptDelimiters(String script, String delim) { + boolean inLiteral = false; + boolean inEscape = false; + + for (int i = 0; i < script.length(); i++) { + char c = script.charAt(i); + if (inEscape) { + inEscape = false; + continue; + } + // MySQL style escapes + if (c == '\\') { + inEscape = true; + continue; + } + if (c == '\'') { + inLiteral = !inLiteral; + } + if (!inLiteral && script.startsWith(delim, i)) { + return true; + } + } + + return false; + } + + /** + * Execute the given SQL script using default settings for statement + * separators, comment delimiters, and exception handling flags. + *

    Statement separators and comments will be removed before executing + * individual statements within the supplied script. + *

    Warning: this method does not release the + * provided {@link Connection}. + * @param connection the JDBC connection to use to execute the script; already + * configured and ready to use + * @param resource the resource to load the SQL script from; encoded with the + * current platform's default encoding + * @throws ScriptException if an error occurred while executing the SQL script + * @see #executeSqlScript(Connection, EncodedResource, boolean, boolean, String, String, String, String) + * @see #DEFAULT_STATEMENT_SEPARATOR + * @see #DEFAULT_COMMENT_PREFIX + * @see #DEFAULT_BLOCK_COMMENT_START_DELIMITER + * @see #DEFAULT_BLOCK_COMMENT_END_DELIMITER + * @see org.springframework.jdbc.datasource.DataSourceUtils#getConnection + * @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection + */ + public static void executeSqlScript(Connection connection, Resource resource) throws ScriptException { + executeSqlScript(connection, new EncodedResource(resource)); + } + + /** + * Execute the given SQL script using default settings for statement + * separators, comment delimiters, and exception handling flags. + *

    Statement separators and comments will be removed before executing + * individual statements within the supplied script. + *

    Warning: this method does not release the + * provided {@link Connection}. + * @param connection the JDBC connection to use to execute the script; already + * configured and ready to use + * @param resource the resource (potentially associated with a specific encoding) + * to load the SQL script from + * @throws ScriptException if an error occurred while executing the SQL script + * @see #executeSqlScript(Connection, EncodedResource, boolean, boolean, String, String, String, String) + * @see #DEFAULT_STATEMENT_SEPARATOR + * @see #DEFAULT_COMMENT_PREFIX + * @see #DEFAULT_BLOCK_COMMENT_START_DELIMITER + * @see #DEFAULT_BLOCK_COMMENT_END_DELIMITER + * @see org.springframework.jdbc.datasource.DataSourceUtils#getConnection + * @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection + */ + public static void executeSqlScript(Connection connection, EncodedResource resource) throws ScriptException { + executeSqlScript(connection, resource, false, false, DEFAULT_COMMENT_PREFIX, DEFAULT_STATEMENT_SEPARATOR, + DEFAULT_BLOCK_COMMENT_START_DELIMITER, DEFAULT_BLOCK_COMMENT_END_DELIMITER); + } + + /** + * Execute the given SQL script. + *

    Statement separators and comments will be removed before executing + * individual statements within the supplied script. + *

    Warning: this method does not release the + * provided {@link Connection}. + * @param connection the JDBC connection to use to execute the script; already + * configured and ready to use + * @param resource the resource (potentially associated with a specific encoding) + * to load the SQL script from + * @param continueOnError whether or not to continue without throwing an exception + * in the event of an error + * @param ignoreFailedDrops whether or not to continue in the event of specifically + * an error on a {@code DROP} statement + * @param commentPrefix the prefix that identifies single-line comments in the + * SQL script (typically "--") + * @param separator the script statement separator; defaults to + * {@value #DEFAULT_STATEMENT_SEPARATOR} if not specified and falls back to + * {@value #FALLBACK_STATEMENT_SEPARATOR} as a last resort; may be set to + * {@value #EOF_STATEMENT_SEPARATOR} to signal that the script contains a + * single statement without a separator + * @param blockCommentStartDelimiter the start block comment delimiter + * @param blockCommentEndDelimiter the end block comment delimiter + * @throws ScriptException if an error occurred while executing the SQL script + * @see #DEFAULT_STATEMENT_SEPARATOR + * @see #FALLBACK_STATEMENT_SEPARATOR + * @see #EOF_STATEMENT_SEPARATOR + * @see org.springframework.jdbc.datasource.DataSourceUtils#getConnection + * @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection + */ + public static void executeSqlScript(Connection connection, EncodedResource resource, boolean continueOnError, + boolean ignoreFailedDrops, String commentPrefix, @Nullable String separator, + String blockCommentStartDelimiter, String blockCommentEndDelimiter) throws ScriptException { + + executeSqlScript(connection, resource, continueOnError, ignoreFailedDrops, + new String[] { commentPrefix }, separator, blockCommentStartDelimiter, + blockCommentEndDelimiter); + } + + /** + * Execute the given SQL script. + *

    Statement separators and comments will be removed before executing + * individual statements within the supplied script. + *

    Warning: this method does not release the + * provided {@link Connection}. + * @param connection the JDBC connection to use to execute the script; already + * configured and ready to use + * @param resource the resource (potentially associated with a specific encoding) + * to load the SQL script from + * @param continueOnError whether or not to continue without throwing an exception + * in the event of an error + * @param ignoreFailedDrops whether or not to continue in the event of specifically + * an error on a {@code DROP} statement + * @param commentPrefixes the prefixes that identify single-line comments in the + * SQL script (typically "--") + * @param separator the script statement separator; defaults to + * {@value #DEFAULT_STATEMENT_SEPARATOR} if not specified and falls back to + * {@value #FALLBACK_STATEMENT_SEPARATOR} as a last resort; may be set to + * {@value #EOF_STATEMENT_SEPARATOR} to signal that the script contains a + * single statement without a separator + * @param blockCommentStartDelimiter the start block comment delimiter + * @param blockCommentEndDelimiter the end block comment delimiter + * @throws ScriptException if an error occurred while executing the SQL script + * @since 5.2 + * @see #DEFAULT_STATEMENT_SEPARATOR + * @see #FALLBACK_STATEMENT_SEPARATOR + * @see #EOF_STATEMENT_SEPARATOR + * @see org.springframework.jdbc.datasource.DataSourceUtils#getConnection + * @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection + */ + public static void executeSqlScript(Connection connection, EncodedResource resource, boolean continueOnError, + boolean ignoreFailedDrops, String[] commentPrefixes, @Nullable String separator, + String blockCommentStartDelimiter, String blockCommentEndDelimiter) throws ScriptException { + + try { + if (logger.isDebugEnabled()) { + logger.debug("Executing SQL script from " + resource); + } + long startTime = System.currentTimeMillis(); + + String script; + try { + script = readScript(resource, commentPrefixes, separator, blockCommentEndDelimiter); + } + catch (IOException ex) { + throw new CannotReadScriptException(resource, ex); + } + + if (separator == null) { + separator = DEFAULT_STATEMENT_SEPARATOR; + } + if (!EOF_STATEMENT_SEPARATOR.equals(separator) && !containsSqlScriptDelimiters(script, separator)) { + separator = FALLBACK_STATEMENT_SEPARATOR; + } + + List statements = new ArrayList<>(); + splitSqlScript(resource, script, separator, commentPrefixes, blockCommentStartDelimiter, + blockCommentEndDelimiter, statements); + + int stmtNumber = 0; + Statement stmt = connection.createStatement(); + try { + for (String statement : statements) { + stmtNumber++; + try { + stmt.execute(statement); + int rowsAffected = stmt.getUpdateCount(); + if (logger.isDebugEnabled()) { + logger.debug(rowsAffected + " returned as update count for SQL: " + statement); + SQLWarning warningToLog = stmt.getWarnings(); + while (warningToLog != null) { + logger.debug("SQLWarning ignored: SQL state '" + warningToLog.getSQLState() + + "', error code '" + warningToLog.getErrorCode() + + "', message [" + warningToLog.getMessage() + "]"); + warningToLog = warningToLog.getNextWarning(); + } + } + } + catch (SQLException ex) { + boolean dropStatement = StringUtils.startsWithIgnoreCase(statement.trim(), "drop"); + if (continueOnError || (dropStatement && ignoreFailedDrops)) { + if (logger.isDebugEnabled()) { + logger.debug(ScriptStatementFailedException.buildErrorMessage(statement, stmtNumber, resource), ex); + } + } + else { + throw new ScriptStatementFailedException(statement, stmtNumber, resource, ex); + } + } + } + } + finally { + try { + stmt.close(); + } + catch (Throwable ex) { + logger.trace("Could not close JDBC Statement", ex); + } + } + + long elapsedTime = System.currentTimeMillis() - startTime; + if (logger.isDebugEnabled()) { + logger.debug("Executed SQL script from " + resource + " in " + elapsedTime + " ms."); + } + } + catch (Exception ex) { + if (ex instanceof ScriptException) { + throw (ScriptException) ex; + } + throw new UncategorizedScriptException( + "Failed to execute database script from resource [" + resource + "]", ex); + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/UncategorizedScriptException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/UncategorizedScriptException.java new file mode 100644 index 0000000..ad9d6b6 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/UncategorizedScriptException.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.init; + +/** + * Thrown when we cannot determine anything more specific than "something went + * wrong while processing an SQL script": for example, a {@link java.sql.SQLException} + * from JDBC that we cannot pinpoint more precisely. + * + * @author Sam Brannen + * @since 4.0.3 + */ +@SuppressWarnings("serial") +public class UncategorizedScriptException extends ScriptException { + + /** + * Construct a new {@code UncategorizedScriptException}. + * @param message detailed message + */ + public UncategorizedScriptException(String message) { + super(message); + } + + /** + * Construct a new {@code UncategorizedScriptException}. + * @param message detailed message + * @param cause the root cause + */ + public UncategorizedScriptException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/package-info.java new file mode 100644 index 0000000..1e1d53e --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/package-info.java @@ -0,0 +1,9 @@ +/** + * Provides extensible support for initializing databases through scripts. + */ +@NonNullApi +@NonNullFields +package org.springframework.jdbc.datasource.init; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/AbstractRoutingDataSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/AbstractRoutingDataSource.java new file mode 100644 index 0000000..ec33aba --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/AbstractRoutingDataSource.java @@ -0,0 +1,247 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.lookup; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Collections; +import java.util.Map; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jdbc.datasource.AbstractDataSource; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * Abstract {@link javax.sql.DataSource} implementation that routes {@link #getConnection()} + * calls to one of various target DataSources based on a lookup key. The latter is usually + * (but not necessarily) determined through some thread-bound transaction context. + * + * @author Juergen Hoeller + * @since 2.0.1 + * @see #setTargetDataSources + * @see #setDefaultTargetDataSource + * @see #determineCurrentLookupKey() + */ +public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean { + + @Nullable + private Map targetDataSources; + + @Nullable + private Object defaultTargetDataSource; + + private boolean lenientFallback = true; + + private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup(); + + @Nullable + private Map resolvedDataSources; + + @Nullable + private DataSource resolvedDefaultDataSource; + + + /** + * Specify the map of target DataSources, with the lookup key as key. + * The mapped value can either be a corresponding {@link javax.sql.DataSource} + * instance or a data source name String (to be resolved via a + * {@link #setDataSourceLookup DataSourceLookup}). + *

    The key can be of arbitrary type; this class implements the + * generic lookup process only. The concrete key representation will + * be handled by {@link #resolveSpecifiedLookupKey(Object)} and + * {@link #determineCurrentLookupKey()}. + */ + public void setTargetDataSources(Map targetDataSources) { + this.targetDataSources = targetDataSources; + } + + /** + * Specify the default target DataSource, if any. + *

    The mapped value can either be a corresponding {@link javax.sql.DataSource} + * instance or a data source name String (to be resolved via a + * {@link #setDataSourceLookup DataSourceLookup}). + *

    This DataSource will be used as target if none of the keyed + * {@link #setTargetDataSources targetDataSources} match the + * {@link #determineCurrentLookupKey()} current lookup key. + */ + public void setDefaultTargetDataSource(Object defaultTargetDataSource) { + this.defaultTargetDataSource = defaultTargetDataSource; + } + + /** + * Specify whether to apply a lenient fallback to the default DataSource + * if no specific DataSource could be found for the current lookup key. + *

    Default is "true", accepting lookup keys without a corresponding entry + * in the target DataSource map - simply falling back to the default DataSource + * in that case. + *

    Switch this flag to "false" if you would prefer the fallback to only apply + * if the lookup key was {@code null}. Lookup keys without a DataSource + * entry will then lead to an IllegalStateException. + * @see #setTargetDataSources + * @see #setDefaultTargetDataSource + * @see #determineCurrentLookupKey() + */ + public void setLenientFallback(boolean lenientFallback) { + this.lenientFallback = lenientFallback; + } + + /** + * Set the DataSourceLookup implementation to use for resolving data source + * name Strings in the {@link #setTargetDataSources targetDataSources} map. + *

    Default is a {@link JndiDataSourceLookup}, allowing the JNDI names + * of application server DataSources to be specified directly. + */ + public void setDataSourceLookup(@Nullable DataSourceLookup dataSourceLookup) { + this.dataSourceLookup = (dataSourceLookup != null ? dataSourceLookup : new JndiDataSourceLookup()); + } + + + @Override + public void afterPropertiesSet() { + if (this.targetDataSources == null) { + throw new IllegalArgumentException("Property 'targetDataSources' is required"); + } + this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size()); + this.targetDataSources.forEach((key, value) -> { + Object lookupKey = resolveSpecifiedLookupKey(key); + DataSource dataSource = resolveSpecifiedDataSource(value); + this.resolvedDataSources.put(lookupKey, dataSource); + }); + if (this.defaultTargetDataSource != null) { + this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource); + } + } + + /** + * Resolve the given lookup key object, as specified in the + * {@link #setTargetDataSources targetDataSources} map, into + * the actual lookup key to be used for matching with the + * {@link #determineCurrentLookupKey() current lookup key}. + *

    The default implementation simply returns the given key as-is. + * @param lookupKey the lookup key object as specified by the user + * @return the lookup key as needed for matching + */ + protected Object resolveSpecifiedLookupKey(Object lookupKey) { + return lookupKey; + } + + /** + * Resolve the specified data source object into a DataSource instance. + *

    The default implementation handles DataSource instances and data source + * names (to be resolved via a {@link #setDataSourceLookup DataSourceLookup}). + * @param dataSource the data source value object as specified in the + * {@link #setTargetDataSources targetDataSources} map + * @return the resolved DataSource (never {@code null}) + * @throws IllegalArgumentException in case of an unsupported value type + */ + protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException { + if (dataSource instanceof DataSource) { + return (DataSource) dataSource; + } + else if (dataSource instanceof String) { + return this.dataSourceLookup.getDataSource((String) dataSource); + } + else { + throw new IllegalArgumentException( + "Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource); + } + } + + /** + * Return the resolved target DataSources that this router manages. + * @return an unmodifiable map of resolved lookup keys and DataSources + * @throws IllegalStateException if the target DataSources are not resolved yet + * @since 5.2.9 + * @see #setTargetDataSources + */ + public Map getResolvedDataSources() { + Assert.state(this.resolvedDataSources != null, "DataSources not resolved yet - call afterPropertiesSet"); + return Collections.unmodifiableMap(this.resolvedDataSources); + } + + /** + * Return the resolved default target DataSource, if any. + * @return the default DataSource, or {@code null} if none or not resolved yet + * @since 5.2.9 + * @see #setDefaultTargetDataSource + */ + @Nullable + public DataSource getResolvedDefaultDataSource() { + return this.resolvedDefaultDataSource; + } + + + @Override + public Connection getConnection() throws SQLException { + return determineTargetDataSource().getConnection(); + } + + @Override + public Connection getConnection(String username, String password) throws SQLException { + return determineTargetDataSource().getConnection(username, password); + } + + @Override + @SuppressWarnings("unchecked") + public T unwrap(Class iface) throws SQLException { + if (iface.isInstance(this)) { + return (T) this; + } + return determineTargetDataSource().unwrap(iface); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return (iface.isInstance(this) || determineTargetDataSource().isWrapperFor(iface)); + } + + /** + * Retrieve the current target DataSource. Determines the + * {@link #determineCurrentLookupKey() current lookup key}, performs + * a lookup in the {@link #setTargetDataSources targetDataSources} map, + * falls back to the specified + * {@link #setDefaultTargetDataSource default target DataSource} if necessary. + * @see #determineCurrentLookupKey() + */ + protected DataSource determineTargetDataSource() { + Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); + Object lookupKey = determineCurrentLookupKey(); + DataSource dataSource = this.resolvedDataSources.get(lookupKey); + if (dataSource == null && (this.lenientFallback || lookupKey == null)) { + dataSource = this.resolvedDefaultDataSource; + } + if (dataSource == null) { + throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); + } + return dataSource; + } + + /** + * Determine the current lookup key. This will typically be + * implemented to check a thread-bound transaction context. + *

    Allows for arbitrary keys. The returned key needs + * to match the stored lookup key type, as resolved by the + * {@link #resolveSpecifiedLookupKey} method. + */ + @Nullable + protected abstract Object determineCurrentLookupKey(); + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/BeanFactoryDataSourceLookup.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/BeanFactoryDataSourceLookup.java new file mode 100644 index 0000000..ff7e882 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/BeanFactoryDataSourceLookup.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.lookup; + +import javax.sql.DataSource; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * {@link DataSourceLookup} implementation based on a Spring {@link BeanFactory}. + * + *

    Will lookup Spring managed beans identified by bean name, + * expecting them to be of type {@code javax.sql.DataSource}. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.beans.factory.BeanFactory + */ +public class BeanFactoryDataSourceLookup implements DataSourceLookup, BeanFactoryAware { + + @Nullable + private BeanFactory beanFactory; + + + /** + * Create a new instance of the {@link BeanFactoryDataSourceLookup} class. + *

    The BeanFactory to access must be set via {@code setBeanFactory}. + * @see #setBeanFactory + */ + public BeanFactoryDataSourceLookup() { + } + + /** + * Create a new instance of the {@link BeanFactoryDataSourceLookup} class. + *

    Use of this constructor is redundant if this object is being created + * by a Spring IoC container, as the supplied {@link BeanFactory} will be + * replaced by the {@link BeanFactory} that creates it (c.f. the + * {@link BeanFactoryAware} contract). So only use this constructor if you + * are using this class outside the context of a Spring IoC container. + * @param beanFactory the bean factory to be used to lookup {@link DataSource DataSources} + */ + public BeanFactoryDataSourceLookup(BeanFactory beanFactory) { + Assert.notNull(beanFactory, "BeanFactory is required"); + this.beanFactory = beanFactory; + } + + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + + @Override + public DataSource getDataSource(String dataSourceName) throws DataSourceLookupFailureException { + Assert.state(this.beanFactory != null, "BeanFactory is required"); + try { + return this.beanFactory.getBean(dataSourceName, DataSource.class); + } + catch (BeansException ex) { + throw new DataSourceLookupFailureException( + "Failed to look up DataSource bean with name '" + dataSourceName + "'", ex); + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/DataSourceLookup.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/DataSourceLookup.java new file mode 100644 index 0000000..6881287 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/DataSourceLookup.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.lookup; + +import javax.sql.DataSource; + +/** + * Strategy interface for looking up DataSources by name. + * + *

    Used, for example, to resolve data source names in JPA + * {@code persistence.xml} files. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager#setDataSourceLookup + */ +@FunctionalInterface +public interface DataSourceLookup { + + /** + * Retrieve the DataSource identified by the given name. + * @param dataSourceName the name of the DataSource + * @return the DataSource (never {@code null}) + * @throws DataSourceLookupFailureException if the lookup failed + */ + DataSource getDataSource(String dataSourceName) throws DataSourceLookupFailureException; + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/DataSourceLookupFailureException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/DataSourceLookupFailureException.java new file mode 100644 index 0000000..eef1af3 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/DataSourceLookupFailureException.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.lookup; + +import org.springframework.dao.NonTransientDataAccessException; + +/** + * Exception to be thrown by a DataSourceLookup implementation, + * indicating that the specified DataSource could not be obtained. + * + * @author Juergen Hoeller + * @since 2.0 + */ +@SuppressWarnings("serial") +public class DataSourceLookupFailureException extends NonTransientDataAccessException { + + /** + * Constructor for DataSourceLookupFailureException. + * @param msg the detail message + */ + public DataSourceLookupFailureException(String msg) { + super(msg); + } + + /** + * Constructor for DataSourceLookupFailureException. + * @param msg the detail message + * @param cause the root cause (usually from using a underlying + * lookup API such as JNDI) + */ + public DataSourceLookupFailureException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/IsolationLevelDataSourceRouter.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/IsolationLevelDataSourceRouter.java new file mode 100644 index 0000000..b008aab --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/IsolationLevelDataSourceRouter.java @@ -0,0 +1,128 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.lookup; + +import org.springframework.core.Constants; +import org.springframework.lang.Nullable; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * DataSource that routes to one of various target DataSources based on the + * current transaction isolation level. The target DataSources need to be + * configured with the isolation level name as key, as defined on the + * {@link org.springframework.transaction.TransactionDefinition TransactionDefinition interface}. + * + *

    This is particularly useful in combination with JTA transaction management + * (typically through Spring's {@link org.springframework.transaction.jta.JtaTransactionManager}). + * Standard JTA does not support transaction-specific isolation levels. Some JTA + * providers support isolation levels as a vendor-specific extension (e.g. WebLogic), + * which is the preferred way of addressing this. As alternative (e.g. on WebSphere), + * the target database can be represented through multiple JNDI DataSources, each + * configured with a different isolation level (for the entire DataSource). + * The present DataSource router allows to transparently switch to the + * appropriate DataSource based on the current transaction's isolation level. + * + *

    The configuration can for example look like this, assuming that the target + * DataSources are defined as individual Spring beans with names + * "myRepeatableReadDataSource", "mySerializableDataSource" and "myDefaultDataSource": + * + *

    + * <bean id="dataSourceRouter" class="org.springframework.jdbc.datasource.lookup.IsolationLevelDataSourceRouter">
    + *   <property name="targetDataSources">
    + *     <map>
    + *       <entry key="ISOLATION_REPEATABLE_READ" value-ref="myRepeatableReadDataSource"/>
    + *       <entry key="ISOLATION_SERIALIZABLE" value-ref="mySerializableDataSource"/>
    + *     </map>
    + *   </property>
    + *   <property name="defaultTargetDataSource" ref="myDefaultDataSource"/>
    + * </bean>
    + * + * Alternatively, the keyed values can also be data source names, to be resolved + * through a {@link #setDataSourceLookup DataSourceLookup}: by default, JNDI + * names for a standard JNDI lookup. This allows for a single concise definition + * without the need for separate DataSource bean definitions. + * + *
    + * <bean id="dataSourceRouter" class="org.springframework.jdbc.datasource.lookup.IsolationLevelDataSourceRouter">
    + *   <property name="targetDataSources">
    + *     <map>
    + *       <entry key="ISOLATION_REPEATABLE_READ" value="java:comp/env/jdbc/myrrds"/>
    + *       <entry key="ISOLATION_SERIALIZABLE" value="java:comp/env/jdbc/myserds"/>
    + *     </map>
    + *   </property>
    + *   <property name="defaultTargetDataSource" value="java:comp/env/jdbc/mydefds"/>
    + * </bean>
    + * + * Note: If you are using this router in combination with Spring's + * {@link org.springframework.transaction.jta.JtaTransactionManager}, + * don't forget to switch the "allowCustomIsolationLevels" flag to "true". + * (By default, JtaTransactionManager will only accept a default isolation level + * because of the lack of isolation level support in standard JTA itself.) + * + *
    + * <bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager">
    + *   <property name="allowCustomIsolationLevels" value="true"/>
    + * </bean>
    + * + * @author Juergen Hoeller + * @since 2.0.1 + * @see #setTargetDataSources + * @see #setDefaultTargetDataSource + * @see org.springframework.transaction.TransactionDefinition#ISOLATION_READ_UNCOMMITTED + * @see org.springframework.transaction.TransactionDefinition#ISOLATION_READ_COMMITTED + * @see org.springframework.transaction.TransactionDefinition#ISOLATION_REPEATABLE_READ + * @see org.springframework.transaction.TransactionDefinition#ISOLATION_SERIALIZABLE + * @see org.springframework.transaction.jta.JtaTransactionManager + */ +public class IsolationLevelDataSourceRouter extends AbstractRoutingDataSource { + + /** Constants instance for TransactionDefinition. */ + private static final Constants constants = new Constants(TransactionDefinition.class); + + + /** + * Supports Integer values for the isolation level constants + * as well as isolation level names as defined on the + * {@link org.springframework.transaction.TransactionDefinition TransactionDefinition interface}. + */ + @Override + protected Object resolveSpecifiedLookupKey(Object lookupKey) { + if (lookupKey instanceof Integer) { + return lookupKey; + } + else if (lookupKey instanceof String) { + String constantName = (String) lookupKey; + if (!constantName.startsWith(DefaultTransactionDefinition.PREFIX_ISOLATION)) { + throw new IllegalArgumentException("Only isolation constants allowed"); + } + return constants.asNumber(constantName); + } + else { + throw new IllegalArgumentException( + "Invalid lookup key - needs to be isolation level Integer or isolation level name String: " + lookupKey); + } + } + + @Override + @Nullable + protected Object determineCurrentLookupKey() { + return TransactionSynchronizationManager.getCurrentTransactionIsolationLevel(); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/JndiDataSourceLookup.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/JndiDataSourceLookup.java new file mode 100644 index 0000000..01d8f85 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/JndiDataSourceLookup.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.lookup; + +import javax.naming.NamingException; +import javax.sql.DataSource; + +import org.springframework.jndi.JndiLocatorSupport; + +/** + * JNDI-based {@link DataSourceLookup} implementation. + * + *

    For specific JNDI configuration, it is recommended to configure + * the "jndiEnvironment"/"jndiTemplate" properties. + * + * @author Costin Leau + * @author Juergen Hoeller + * @since 2.0 + * @see #setJndiEnvironment + * @see #setJndiTemplate + */ +public class JndiDataSourceLookup extends JndiLocatorSupport implements DataSourceLookup { + + public JndiDataSourceLookup() { + setResourceRef(true); + } + + @Override + public DataSource getDataSource(String dataSourceName) throws DataSourceLookupFailureException { + try { + return lookup(dataSourceName, DataSource.class); + } + catch (NamingException ex) { + throw new DataSourceLookupFailureException( + "Failed to look up JNDI DataSource with name '" + dataSourceName + "'", ex); + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/MapDataSourceLookup.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/MapDataSourceLookup.java new file mode 100644 index 0000000..297a805 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/MapDataSourceLookup.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.lookup; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.sql.DataSource; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Simple {@link DataSourceLookup} implementation that relies on a map for doing lookups. + * + *

    Useful for testing environments or applications that need to match arbitrary + * {@link String} names to target {@link DataSource} objects. + * + * @author Costin Leau + * @author Juergen Hoeller + * @author Rick Evans + * @since 2.0 + */ +public class MapDataSourceLookup implements DataSourceLookup { + + private final Map dataSources = new HashMap<>(4); + + + /** + * Create a new instance of the {@link MapDataSourceLookup} class. + */ + public MapDataSourceLookup() { + } + + /** + * Create a new instance of the {@link MapDataSourceLookup} class. + * @param dataSources the {@link Map} of {@link DataSource DataSources}; the keys + * are {@link String Strings}, the values are actual {@link DataSource} instances. + */ + public MapDataSourceLookup(Map dataSources) { + setDataSources(dataSources); + } + + /** + * Create a new instance of the {@link MapDataSourceLookup} class. + * @param dataSourceName the name under which the supplied {@link DataSource} is to be added + * @param dataSource the {@link DataSource} to be added + */ + public MapDataSourceLookup(String dataSourceName, DataSource dataSource) { + addDataSource(dataSourceName, dataSource); + } + + + /** + * Set the {@link Map} of {@link DataSource DataSources}; the keys + * are {@link String Strings}, the values are actual {@link DataSource} instances. + *

    If the supplied {@link Map} is {@code null}, then this method + * call effectively has no effect. + * @param dataSources said {@link Map} of {@link DataSource DataSources} + */ + public void setDataSources(@Nullable Map dataSources) { + if (dataSources != null) { + this.dataSources.putAll(dataSources); + } + } + + /** + * Get the {@link Map} of {@link DataSource DataSources} maintained by this object. + *

    The returned {@link Map} is {@link Collections#unmodifiableMap(java.util.Map) unmodifiable}. + * @return said {@link Map} of {@link DataSource DataSources} (never {@code null}) + */ + public Map getDataSources() { + return Collections.unmodifiableMap(this.dataSources); + } + + /** + * Add the supplied {@link DataSource} to the map of {@link DataSource DataSources} + * maintained by this object. + * @param dataSourceName the name under which the supplied {@link DataSource} is to be added + * @param dataSource the {@link DataSource} to be so added + */ + public void addDataSource(String dataSourceName, DataSource dataSource) { + Assert.notNull(dataSourceName, "DataSource name must not be null"); + Assert.notNull(dataSource, "DataSource must not be null"); + this.dataSources.put(dataSourceName, dataSource); + } + + @Override + public DataSource getDataSource(String dataSourceName) throws DataSourceLookupFailureException { + Assert.notNull(dataSourceName, "DataSource name must not be null"); + DataSource dataSource = this.dataSources.get(dataSourceName); + if (dataSource == null) { + throw new DataSourceLookupFailureException( + "No DataSource with name '" + dataSourceName + "' registered"); + } + return dataSource; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/SingleDataSourceLookup.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/SingleDataSourceLookup.java new file mode 100644 index 0000000..192ce64 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/SingleDataSourceLookup.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.lookup; + +import javax.sql.DataSource; + +import org.springframework.util.Assert; + +/** + * An implementation of the DataSourceLookup that simply wraps a + * single given DataSource, returned for any data source name. + * + * @author Juergen Hoeller + * @since 2.0 + */ +public class SingleDataSourceLookup implements DataSourceLookup { + + private final DataSource dataSource; + + + /** + * Create a new instance of the {@link SingleDataSourceLookup} class. + * @param dataSource the single {@link DataSource} to wrap + */ + public SingleDataSourceLookup(DataSource dataSource) { + Assert.notNull(dataSource, "DataSource must not be null"); + this.dataSource = dataSource; + } + + + @Override + public DataSource getDataSource(String dataSourceName) { + return this.dataSource; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/package-info.java new file mode 100644 index 0000000..e5dd2ec --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/package-info.java @@ -0,0 +1,9 @@ +/** + * Provides a strategy for looking up JDBC DataSources by name. + */ +@NonNullApi +@NonNullFields +package org.springframework.jdbc.datasource.lookup; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/package-info.java new file mode 100644 index 0000000..baa79a5 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/package-info.java @@ -0,0 +1,11 @@ +/** + * Provides a utility class for easy DataSource access, + * a PlatformTransactionManager for a single DataSource, + * and various simple DataSource implementations. + */ +@NonNullApi +@NonNullFields +package org.springframework.jdbc.datasource; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/BatchSqlUpdate.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/BatchSqlUpdate.java new file mode 100644 index 0000000..243a8d6 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/BatchSqlUpdate.java @@ -0,0 +1,248 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.object; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; + +import javax.sql.DataSource; + +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; + +/** + * SqlUpdate subclass that performs batch update operations. Encapsulates + * queuing up records to be updated, and adds them as a single batch once + * {@code flush} is called or the given batch size has been met. + * + *

    Note that this class is a non-thread-safe object, in contrast + * to all other JDBC operations objects in this package. You need to create + * a new instance of it for each use, or call {@code reset} before + * reuse within the same thread. + * + * @author Keith Donald + * @author Juergen Hoeller + * @since 1.1 + * @see #flush + * @see #reset + */ +public class BatchSqlUpdate extends SqlUpdate { + + /** + * Default number of inserts to accumulate before committing a batch (5000). + */ + public static final int DEFAULT_BATCH_SIZE = 5000; + + + private int batchSize = DEFAULT_BATCH_SIZE; + + private boolean trackRowsAffected = true; + + private final Deque parameterQueue = new ArrayDeque<>(); + + private final List rowsAffected = new ArrayList<>(); + + + /** + * Constructor to allow use as a JavaBean. DataSource and SQL + * must be supplied before compilation and use. + * @see #setDataSource + * @see #setSql + */ + public BatchSqlUpdate() { + super(); + } + + /** + * Construct an update object with a given DataSource and SQL. + * @param ds the DataSource to use to obtain connections + * @param sql the SQL statement to execute + */ + public BatchSqlUpdate(DataSource ds, String sql) { + super(ds, sql); + } + + /** + * Construct an update object with a given DataSource, SQL + * and anonymous parameters. + * @param ds the DataSource to use to obtain connections + * @param sql the SQL statement to execute + * @param types the SQL types of the parameters, as defined in the + * {@code java.sql.Types} class + * @see java.sql.Types + */ + public BatchSqlUpdate(DataSource ds, String sql, int[] types) { + super(ds, sql, types); + } + + /** + * Construct an update object with a given DataSource, SQL, + * anonymous parameters and specifying the maximum number of rows + * that may be affected. + * @param ds the DataSource to use to obtain connections + * @param sql the SQL statement to execute + * @param types the SQL types of the parameters, as defined in the + * {@code java.sql.Types} class + * @param batchSize the number of statements that will trigger + * an automatic intermediate flush + * @see java.sql.Types + */ + public BatchSqlUpdate(DataSource ds, String sql, int[] types, int batchSize) { + super(ds, sql, types); + setBatchSize(batchSize); + } + + + /** + * Set the number of statements that will trigger an automatic intermediate + * flush. {@code update} calls or the given statement parameters will + * be queued until the batch size is met, at which point it will empty the + * queue and execute the batch. + *

    You can also flush already queued statements with an explicit + * {@code flush} call. Note that you need to this after queueing + * all parameters to guarantee that all statements have been flushed. + */ + public void setBatchSize(int batchSize) { + this.batchSize = batchSize; + } + + /** + * Set whether to track the rows affected by batch updates performed + * by this operation object. + *

    Default is "true". Turn this off to save the memory needed for + * the list of row counts. + * @see #getRowsAffected() + */ + public void setTrackRowsAffected(boolean trackRowsAffected) { + this.trackRowsAffected = trackRowsAffected; + } + + /** + * BatchSqlUpdate does not support BLOB or CLOB parameters. + */ + @Override + protected boolean supportsLobParameters() { + return false; + } + + + /** + * Overridden version of {@code update} that adds the given statement + * parameters to the queue rather than executing them immediately. + * All other {@code update} methods of the SqlUpdate base class go + * through this method and will thus behave similarly. + *

    You need to call {@code flush} to actually execute the batch. + * If the specified batch size is reached, an implicit flush will happen; + * you still need to finally call {@code flush} to flush all statements. + * @param params array of parameter objects + * @return the number of rows affected by the update (always -1, + * meaning "not applicable", as the statement is not actually + * executed by this method) + * @see #flush + */ + @Override + public int update(Object... params) throws DataAccessException { + validateParameters(params); + this.parameterQueue.add(params.clone()); + + if (this.parameterQueue.size() == this.batchSize) { + if (logger.isDebugEnabled()) { + logger.debug("Triggering auto-flush because queue reached batch size of " + this.batchSize); + } + flush(); + } + + return -1; + } + + /** + * Trigger any queued update operations to be added as a final batch. + * @return an array of the number of rows affected by each statement + */ + public int[] flush() { + if (this.parameterQueue.isEmpty()) { + return new int[0]; + } + + int[] rowsAffected = getJdbcTemplate().batchUpdate( + resolveSql(), + new BatchPreparedStatementSetter() { + @Override + public int getBatchSize() { + return parameterQueue.size(); + } + @Override + public void setValues(PreparedStatement ps, int index) throws SQLException { + Object[] params = parameterQueue.removeFirst(); + newPreparedStatementSetter(params).setValues(ps); + } + }); + + for (int rowCount : rowsAffected) { + checkRowsAffected(rowCount); + if (this.trackRowsAffected) { + this.rowsAffected.add(rowCount); + } + } + + return rowsAffected; + } + + /** + * Return the current number of statements or statement parameters + * in the queue. + */ + public int getQueueCount() { + return this.parameterQueue.size(); + } + + /** + * Return the number of already executed statements. + */ + public int getExecutionCount() { + return this.rowsAffected.size(); + } + + /** + * Return the number of affected rows for all already executed statements. + * Accumulates all of {@code flush}'s return values until + * {@code reset} is invoked. + * @return an array of the number of rows affected by each statement + * @see #reset + */ + public int[] getRowsAffected() { + int[] result = new int[this.rowsAffected.size()]; + for (int i = 0; i < this.rowsAffected.size(); i++) { + result[i] = this.rowsAffected.get(i); + } + return result; + } + + /** + * Reset the statement parameter queue, the rows affected cache, + * and the execution count. + */ + public void reset() { + this.parameterQueue.clear(); + this.rowsAffected.clear(); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/GenericSqlQuery.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/GenericSqlQuery.java new file mode 100644 index 0000000..c270892 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/GenericSqlQuery.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.object; + +import java.util.Map; + +import org.springframework.beans.BeanUtils; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * A concrete variant of {@link SqlQuery} which can be configured + * with a {@link RowMapper}. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 3.0 + * @param the result type + * @see #setRowMapper + * @see #setRowMapperClass + */ +public class GenericSqlQuery extends SqlQuery { + + @Nullable + private RowMapper rowMapper; + + @SuppressWarnings("rawtypes") + @Nullable + private Class rowMapperClass; + + + /** + * Set a specific {@link RowMapper} instance to use for this query. + * @since 4.3.2 + */ + public void setRowMapper(RowMapper rowMapper) { + this.rowMapper = rowMapper; + } + + /** + * Set a {@link RowMapper} class for this query, creating a fresh + * {@link RowMapper} instance per execution. + */ + @SuppressWarnings("rawtypes") + public void setRowMapperClass(Class rowMapperClass) { + this.rowMapperClass = rowMapperClass; + } + + @Override + public void afterPropertiesSet() { + super.afterPropertiesSet(); + Assert.isTrue(this.rowMapper != null || this.rowMapperClass != null, + "'rowMapper' or 'rowMapperClass' is required"); + } + + + @Override + @SuppressWarnings("unchecked") + protected RowMapper newRowMapper(@Nullable Object[] parameters, @Nullable Map context) { + if (this.rowMapper != null) { + return this.rowMapper; + } + else { + Assert.state(this.rowMapperClass != null, "No RowMapper set"); + return BeanUtils.instantiateClass(this.rowMapperClass); + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/GenericStoredProcedure.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/GenericStoredProcedure.java new file mode 100644 index 0000000..6c55806 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/GenericStoredProcedure.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.object; + +/** + * Concrete implementation making it possible to define the RDBMS stored procedures + * in an application context without writing a custom Java implementation class. + *

    + * This implementation does not provide a typed method for invocation so executions + * must use one of the generic {@link StoredProcedure#execute(java.util.Map)} or + * {@link StoredProcedure#execute(org.springframework.jdbc.core.ParameterMapper)} methods. + * + * @author Thomas Risberg + * @see org.springframework.jdbc.object.StoredProcedure + */ +public class GenericStoredProcedure extends StoredProcedure { + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/MappingSqlQuery.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/MappingSqlQuery.java new file mode 100644 index 0000000..86e38b0 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/MappingSqlQuery.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.object; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Map; + +import javax.sql.DataSource; + +import org.springframework.lang.Nullable; + +/** + * Reusable query in which concrete subclasses must implement the abstract + * mapRow(ResultSet, int) method to convert each row of the JDBC ResultSet + * into an object. + * + *

    Simplifies MappingSqlQueryWithParameters API by dropping parameters and + * context. Most subclasses won't care about parameters. If you don't use + * contextual information, subclass this instead of MappingSqlQueryWithParameters. + * + * @author Rod Johnson + * @author Thomas Risberg + * @author Jean-Pierre Pawlak + * @param the result type + * @see MappingSqlQueryWithParameters + */ +public abstract class MappingSqlQuery extends MappingSqlQueryWithParameters { + + /** + * Constructor that allows use as a JavaBean. + */ + public MappingSqlQuery() { + } + + /** + * Convenient constructor with DataSource and SQL string. + * @param ds the DataSource to use to obtain connections + * @param sql the SQL to run + */ + public MappingSqlQuery(DataSource ds, String sql) { + super(ds, sql); + } + + + /** + * This method is implemented to invoke the simpler mapRow + * template method, ignoring parameters. + * @see #mapRow(ResultSet, int) + */ + @Override + @Nullable + protected final T mapRow(ResultSet rs, int rowNum, @Nullable Object[] parameters, @Nullable Map context) + throws SQLException { + + return mapRow(rs, rowNum); + } + + /** + * Subclasses must implement this method to convert each row of the + * ResultSet into an object of the result type. + *

    Subclasses of this class, as opposed to direct subclasses of + * MappingSqlQueryWithParameters, don't need to concern themselves + * with the parameters to the execute method of the query object. + * @param rs the ResultSet we're working through + * @param rowNum row number (from 0) we're up to + * @return an object of the result type + * @throws SQLException if there's an error extracting data. + * Subclasses can simply not catch SQLExceptions, relying on the + * framework to clean up. + */ + @Nullable + protected abstract T mapRow(ResultSet rs, int rowNum) throws SQLException; + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/MappingSqlQueryWithParameters.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/MappingSqlQueryWithParameters.java new file mode 100644 index 0000000..5743e3c --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/MappingSqlQueryWithParameters.java @@ -0,0 +1,127 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.object; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Map; + +import javax.sql.DataSource; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.lang.Nullable; + +/** + * Reusable RDBMS query in which concrete subclasses must implement + * the abstract mapRow(ResultSet, int) method to map each row of + * the JDBC ResultSet into an object. + * + *

    Such manual mapping is usually preferable to "automatic" + * mapping using reflection, which can become complex in non-trivial + * cases. For example, the present class allows different objects + * to be used for different rows (for example, if a subclass is indicated). + * It allows computed fields to be set. And there's no need for + * ResultSet columns to have the same names as bean properties. + * The Pareto Principle in action: going the extra mile to automate + * the extraction process makes the framework much more complex + * and delivers little real benefit. + * + *

    Subclasses can be constructed providing SQL, parameter types + * and a DataSource. SQL will often vary between subclasses. + * + * @author Rod Johnson + * @author Thomas Risberg + * @author Jean-Pierre Pawlak + * @param the result type + * @see org.springframework.jdbc.object.MappingSqlQuery + * @see org.springframework.jdbc.object.SqlQuery + */ +public abstract class MappingSqlQueryWithParameters extends SqlQuery { + + /** + * Constructor to allow use as a JavaBean. + */ + public MappingSqlQueryWithParameters() { + } + + /** + * Convenient constructor with DataSource and SQL string. + * @param ds the DataSource to use to get connections + * @param sql the SQL to run + */ + public MappingSqlQueryWithParameters(DataSource ds, String sql) { + super(ds, sql); + } + + + /** + * Implementation of protected abstract method. This invokes the subclass's + * implementation of the mapRow() method. + */ + @Override + protected RowMapper newRowMapper(@Nullable Object[] parameters, @Nullable Map context) { + return new RowMapperImpl(parameters, context); + } + + /** + * Subclasses must implement this method to convert each row + * of the ResultSet into an object of the result type. + * @param rs the ResultSet we're working through + * @param rowNum row number (from 0) we're up to + * @param parameters to the query (passed to the execute() method). + * Subclasses are rarely interested in these. + * It can be {@code null} if there are no parameters. + * @param context passed to the execute() method. + * It can be {@code null} if no contextual information is need. + * @return an object of the result type + * @throws SQLException if there's an error extracting data. + * Subclasses can simply not catch SQLExceptions, relying on the + * framework to clean up. + */ + @Nullable + protected abstract T mapRow(ResultSet rs, int rowNum, @Nullable Object[] parameters, @Nullable Map context) + throws SQLException; + + + /** + * Implementation of RowMapper that calls the enclosing + * class's {@code mapRow} method for each row. + */ + protected class RowMapperImpl implements RowMapper { + + @Nullable + private final Object[] params; + + @Nullable + private final Map context; + + /** + * Use an array results. More efficient if we know how many results to expect. + */ + public RowMapperImpl(@Nullable Object[] parameters, @Nullable Map context) { + this.params = parameters; + this.context = context; + } + + @Override + @Nullable + public T mapRow(ResultSet rs, int rowNum) throws SQLException { + return MappingSqlQueryWithParameters.this.mapRow(rs, rowNum, this.params, this.context); + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/RdbmsOperation.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/RdbmsOperation.java new file mode 100644 index 0000000..c3db797 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/RdbmsOperation.java @@ -0,0 +1,481 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.object; + +import java.sql.ResultSet; +import java.sql.Types; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * An "RDBMS operation" is a multi-threaded, reusable object representing a query, + * update, or stored procedure call. An RDBMS operation is not a command, + * as a command is not reusable. However, execute methods may take commands as + * arguments. Subclasses should be JavaBeans, allowing easy configuration. + * + *

    This class and subclasses throw runtime exceptions, defined in the + * {@code org.springframework.dao} package (and as thrown by the + * {@code org.springframework.jdbc.core} package, which the classes + * in this package use under the hood to perform raw JDBC operations). + * + *

    Subclasses should set SQL and add parameters before invoking the + * {@link #compile()} method. The order in which parameters are added is + * significant. The appropriate {@code execute} or {@code update} + * method can then be invoked. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see SqlQuery + * @see SqlUpdate + * @see StoredProcedure + * @see org.springframework.jdbc.core.JdbcTemplate + */ +public abstract class RdbmsOperation implements InitializingBean { + + /** Logger available to subclasses. */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** Lower-level class used to execute SQL. */ + private JdbcTemplate jdbcTemplate = new JdbcTemplate(); + + private int resultSetType = ResultSet.TYPE_FORWARD_ONLY; + + private boolean updatableResults = false; + + private boolean returnGeneratedKeys = false; + + @Nullable + private String[] generatedKeysColumnNames; + + @Nullable + private String sql; + + private final List declaredParameters = new ArrayList<>(); + + /** + * Has this operation been compiled? Compilation means at + * least checking that a DataSource and sql have been provided, + * but subclasses may also implement their own custom validation. + */ + private volatile boolean compiled; + + + /** + * An alternative to the more commonly used {@link #setDataSource} when you want to + * use the same {@link JdbcTemplate} in multiple {@code RdbmsOperations}. This is + * appropriate if the {@code JdbcTemplate} has special configuration such as a + * {@link org.springframework.jdbc.support.SQLExceptionTranslator} to be reused. + */ + public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + /** + * Return the {@link JdbcTemplate} used by this operation object. + */ + public JdbcTemplate getJdbcTemplate() { + return this.jdbcTemplate; + } + + /** + * Set the JDBC {@link DataSource} to obtain connections from. + * @see org.springframework.jdbc.core.JdbcTemplate#setDataSource + */ + public void setDataSource(DataSource dataSource) { + this.jdbcTemplate.setDataSource(dataSource); + } + + /** + * Set the fetch size for this RDBMS operation. This is important for processing + * large result sets: Setting this higher than the default value will increase + * processing speed at the cost of memory consumption; setting this lower can + * avoid transferring row data that will never be read by the application. + *

    Default is -1, indicating to use the driver's default. + * @see org.springframework.jdbc.core.JdbcTemplate#setFetchSize + */ + public void setFetchSize(int fetchSize) { + this.jdbcTemplate.setFetchSize(fetchSize); + } + + /** + * Set the maximum number of rows for this RDBMS operation. This is important + * for processing subsets of large result sets, avoiding to read and hold + * the entire result set in the database or in the JDBC driver. + *

    Default is -1, indicating to use the driver's default. + * @see org.springframework.jdbc.core.JdbcTemplate#setMaxRows + */ + public void setMaxRows(int maxRows) { + this.jdbcTemplate.setMaxRows(maxRows); + } + + /** + * Set the query timeout for statements that this RDBMS operation executes. + *

    Default is -1, indicating to use the JDBC driver's default. + *

    Note: Any timeout specified here will be overridden by the remaining + * transaction timeout when executing within a transaction that has a + * timeout specified at the transaction level. + */ + public void setQueryTimeout(int queryTimeout) { + this.jdbcTemplate.setQueryTimeout(queryTimeout); + } + + /** + * Set whether to use statements that return a specific type of ResultSet. + * @param resultSetType the ResultSet type + * @see java.sql.ResultSet#TYPE_FORWARD_ONLY + * @see java.sql.ResultSet#TYPE_SCROLL_INSENSITIVE + * @see java.sql.ResultSet#TYPE_SCROLL_SENSITIVE + * @see java.sql.Connection#prepareStatement(String, int, int) + */ + public void setResultSetType(int resultSetType) { + this.resultSetType = resultSetType; + } + + /** + * Return whether statements will return a specific type of ResultSet. + */ + public int getResultSetType() { + return this.resultSetType; + } + + /** + * Set whether to use statements that are capable of returning + * updatable ResultSets. + * @see java.sql.Connection#prepareStatement(String, int, int) + */ + public void setUpdatableResults(boolean updatableResults) { + if (isCompiled()) { + throw new InvalidDataAccessApiUsageException( + "The updateableResults flag must be set before the operation is compiled"); + } + this.updatableResults = updatableResults; + } + + /** + * Return whether statements will return updatable ResultSets. + */ + public boolean isUpdatableResults() { + return this.updatableResults; + } + + /** + * Set whether prepared statements should be capable of returning + * auto-generated keys. + * @see java.sql.Connection#prepareStatement(String, int) + */ + public void setReturnGeneratedKeys(boolean returnGeneratedKeys) { + if (isCompiled()) { + throw new InvalidDataAccessApiUsageException( + "The returnGeneratedKeys flag must be set before the operation is compiled"); + } + this.returnGeneratedKeys = returnGeneratedKeys; + } + + /** + * Return whether statements should be capable of returning + * auto-generated keys. + */ + public boolean isReturnGeneratedKeys() { + return this.returnGeneratedKeys; + } + + /** + * Set the column names of the auto-generated keys. + * @see java.sql.Connection#prepareStatement(String, String[]) + */ + public void setGeneratedKeysColumnNames(@Nullable String... names) { + if (isCompiled()) { + throw new InvalidDataAccessApiUsageException( + "The column names for the generated keys must be set before the operation is compiled"); + } + this.generatedKeysColumnNames = names; + } + + /** + * Return the column names of the auto generated keys. + */ + @Nullable + public String[] getGeneratedKeysColumnNames() { + return this.generatedKeysColumnNames; + } + + /** + * Set the SQL executed by this operation. + */ + public void setSql(@Nullable String sql) { + this.sql = sql; + } + + /** + * Subclasses can override this to supply dynamic SQL if they wish, but SQL is + * normally set by calling the {@link #setSql} method or in a subclass constructor. + */ + @Nullable + public String getSql() { + return this.sql; + } + + /** + * Resolve the configured SQL for actual use. + * @return the SQL (never {@code null}) + * @since 5.0 + */ + protected String resolveSql() { + String sql = getSql(); + Assert.state(sql != null, "No SQL set"); + return sql; + } + + /** + * Add anonymous parameters, specifying only their SQL types + * as defined in the {@code java.sql.Types} class. + *

    Parameter ordering is significant. This method is an alternative + * to the {@link #declareParameter} method, which should normally be preferred. + * @param types array of SQL types as defined in the + * {@code java.sql.Types} class + * @throws InvalidDataAccessApiUsageException if the operation is already compiled + */ + public void setTypes(@Nullable int[] types) throws InvalidDataAccessApiUsageException { + if (isCompiled()) { + throw new InvalidDataAccessApiUsageException("Cannot add parameters once query is compiled"); + } + if (types != null) { + for (int type : types) { + declareParameter(new SqlParameter(type)); + } + } + } + + /** + * Declare a parameter for this operation. + *

    The order in which this method is called is significant when using + * positional parameters. It is not significant when using named parameters + * with named SqlParameter objects here; it remains significant when using + * named parameters in combination with unnamed SqlParameter objects here. + * @param param the SqlParameter to add. This will specify SQL type and (optionally) + * the parameter's name. Note that you typically use the {@link SqlParameter} class + * itself here, not any of its subclasses. + * @throws InvalidDataAccessApiUsageException if the operation is already compiled, + * and hence cannot be configured further + */ + public void declareParameter(SqlParameter param) throws InvalidDataAccessApiUsageException { + if (isCompiled()) { + throw new InvalidDataAccessApiUsageException("Cannot add parameters once the query is compiled"); + } + this.declaredParameters.add(param); + } + + /** + * Add one or more declared parameters. Used for configuring this operation + * when used in a bean factory. Each parameter will specify SQL type and (optionally) + * the parameter's name. + * @param parameters an array containing the declared {@link SqlParameter} objects + * @see #declaredParameters + */ + public void setParameters(SqlParameter... parameters) { + if (isCompiled()) { + throw new InvalidDataAccessApiUsageException("Cannot add parameters once the query is compiled"); + } + for (int i = 0; i < parameters.length; i++) { + if (parameters[i] != null) { + this.declaredParameters.add(parameters[i]); + } + else { + throw new InvalidDataAccessApiUsageException("Cannot add parameter at index " + i + " from " + + Arrays.asList(parameters) + " since it is 'null'"); + } + } + } + + /** + * Return a list of the declared {@link SqlParameter} objects. + */ + protected List getDeclaredParameters() { + return this.declaredParameters; + } + + + /** + * Ensures compilation if used in a bean factory. + */ + @Override + public void afterPropertiesSet() { + compile(); + } + + /** + * Compile this query. + * Ignores subsequent attempts to compile. + * @throws InvalidDataAccessApiUsageException if the object hasn't + * been correctly initialized, for example if no DataSource has been provided + */ + public final void compile() throws InvalidDataAccessApiUsageException { + if (!isCompiled()) { + if (getSql() == null) { + throw new InvalidDataAccessApiUsageException("Property 'sql' is required"); + } + + try { + this.jdbcTemplate.afterPropertiesSet(); + } + catch (IllegalArgumentException ex) { + throw new InvalidDataAccessApiUsageException(ex.getMessage()); + } + + compileInternal(); + this.compiled = true; + + if (logger.isDebugEnabled()) { + logger.debug("RdbmsOperation with SQL [" + getSql() + "] compiled"); + } + } + } + + /** + * Is this operation "compiled"? Compilation, as in JDO, + * means that the operation is fully configured, and ready to use. + * The exact meaning of compilation will vary between subclasses. + * @return whether this operation is compiled and ready to use + */ + public boolean isCompiled() { + return this.compiled; + } + + /** + * Check whether this operation has been compiled already; + * lazily compile it if not already compiled. + *

    Automatically called by {@code validateParameters}. + * @see #validateParameters + */ + protected void checkCompiled() { + if (!isCompiled()) { + logger.debug("SQL operation not compiled before execution - invoking compile"); + compile(); + } + } + + /** + * Validate the parameters passed to an execute method based on declared parameters. + * Subclasses should invoke this method before every {@code executeQuery()} + * or {@code update()} method. + * @param parameters the parameters supplied (may be {@code null}) + * @throws InvalidDataAccessApiUsageException if the parameters are invalid + */ + protected void validateParameters(@Nullable Object[] parameters) throws InvalidDataAccessApiUsageException { + checkCompiled(); + int declaredInParameters = 0; + for (SqlParameter param : this.declaredParameters) { + if (param.isInputValueProvided()) { + if (!supportsLobParameters() && + (param.getSqlType() == Types.BLOB || param.getSqlType() == Types.CLOB)) { + throw new InvalidDataAccessApiUsageException( + "BLOB or CLOB parameters are not allowed for this kind of operation"); + } + declaredInParameters++; + } + } + validateParameterCount((parameters != null ? parameters.length : 0), declaredInParameters); + } + + /** + * Validate the named parameters passed to an execute method based on declared parameters. + * Subclasses should invoke this method before every {@code executeQuery()} or + * {@code update()} method. + * @param parameters parameter Map supplied (may be {@code null}) + * @throws InvalidDataAccessApiUsageException if the parameters are invalid + */ + protected void validateNamedParameters(@Nullable Map parameters) throws InvalidDataAccessApiUsageException { + checkCompiled(); + Map paramsToUse = (parameters != null ? parameters : Collections. emptyMap()); + int declaredInParameters = 0; + for (SqlParameter param : this.declaredParameters) { + if (param.isInputValueProvided()) { + if (!supportsLobParameters() && + (param.getSqlType() == Types.BLOB || param.getSqlType() == Types.CLOB)) { + throw new InvalidDataAccessApiUsageException( + "BLOB or CLOB parameters are not allowed for this kind of operation"); + } + if (param.getName() != null && !paramsToUse.containsKey(param.getName())) { + throw new InvalidDataAccessApiUsageException("The parameter named '" + param.getName() + + "' was not among the parameters supplied: " + paramsToUse.keySet()); + } + declaredInParameters++; + } + } + validateParameterCount(paramsToUse.size(), declaredInParameters); + } + + /** + * Validate the given parameter count against the given declared parameters. + * @param suppliedParamCount the number of actual parameters given + * @param declaredInParamCount the number of input parameters declared + */ + private void validateParameterCount(int suppliedParamCount, int declaredInParamCount) { + if (suppliedParamCount < declaredInParamCount) { + throw new InvalidDataAccessApiUsageException(suppliedParamCount + " parameters were supplied, but " + + declaredInParamCount + " in parameters were declared in class [" + getClass().getName() + "]"); + } + if (suppliedParamCount > this.declaredParameters.size() && !allowsUnusedParameters()) { + throw new InvalidDataAccessApiUsageException(suppliedParamCount + " parameters were supplied, but " + + declaredInParamCount + " parameters were declared in class [" + getClass().getName() + "]"); + } + } + + + /** + * Subclasses must implement this template method to perform their own compilation. + * Invoked after this base class's compilation is complete. + *

    Subclasses can assume that SQL and a DataSource have been supplied. + * @throws InvalidDataAccessApiUsageException if the subclass hasn't been + * properly configured + */ + protected abstract void compileInternal() throws InvalidDataAccessApiUsageException; + + /** + * Return whether BLOB/CLOB parameters are supported for this kind of operation. + *

    The default is {@code true}. + */ + protected boolean supportsLobParameters() { + return true; + } + + /** + * Return whether this operation accepts additional parameters that are + * given but not actually used. Applies in particular to parameter Maps. + *

    The default is {@code false}. + * @see StoredProcedure + */ + protected boolean allowsUnusedParameters() { + return false; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlCall.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlCall.java new file mode 100644 index 0000000..23f1018 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlCall.java @@ -0,0 +1,205 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.object; + +import java.util.List; +import java.util.Map; + +import javax.sql.DataSource; + +import org.springframework.jdbc.core.CallableStatementCreator; +import org.springframework.jdbc.core.CallableStatementCreatorFactory; +import org.springframework.jdbc.core.ParameterMapper; +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * RdbmsOperation using a JdbcTemplate and representing an SQL-based + * call such as a stored procedure or a stored function. + * + *

    Configures a CallableStatementCreatorFactory based on the declared + * parameters. + * + * @author Rod Johnson + * @author Thomas Risberg + * @see CallableStatementCreatorFactory + */ +public abstract class SqlCall extends RdbmsOperation { + + /** + * Flag used to indicate that this call is for a function and to + * use the {? = call get_invoice_count(?)} syntax. + */ + private boolean function = false; + + /** + * Flag used to indicate that the sql for this call should be used exactly as + * it is defined. No need to add the escape syntax and parameter place holders. + */ + private boolean sqlReadyForUse = false; + + /** + * Call string as defined in java.sql.CallableStatement. + * String of form {call add_invoice(?, ?, ?)} or {? = call get_invoice_count(?)} + * if isFunction is set to true. Updated after each parameter is added. + */ + @Nullable + private String callString; + + /** + * Object enabling us to create CallableStatementCreators + * efficiently, based on this class's declared parameters. + */ + @Nullable + private CallableStatementCreatorFactory callableStatementFactory; + + + /** + * Constructor to allow use as a JavaBean. + * A DataSource, SQL and any parameters must be supplied before + * invoking the {@code compile} method and using this object. + * @see #setDataSource + * @see #setSql + * @see #compile + */ + public SqlCall() { + } + + /** + * Create a new SqlCall object with SQL, but without parameters. + * Must add parameters or settle with none. + * @param ds the DataSource to obtain connections from + * @param sql the SQL to execute + */ + public SqlCall(DataSource ds, String sql) { + setDataSource(ds); + setSql(sql); + } + + + /** + * Set whether this call is for a function. + */ + public void setFunction(boolean function) { + this.function = function; + } + + /** + * Return whether this call is for a function. + */ + public boolean isFunction() { + return this.function; + } + + /** + * Set whether the SQL can be used as is. + */ + public void setSqlReadyForUse(boolean sqlReadyForUse) { + this.sqlReadyForUse = sqlReadyForUse; + } + + /** + * Return whether the SQL can be used as is. + */ + public boolean isSqlReadyForUse() { + return this.sqlReadyForUse; + } + + + /** + * Overridden method to configure the CallableStatementCreatorFactory + * based on our declared parameters. + * @see RdbmsOperation#compileInternal() + */ + @Override + protected final void compileInternal() { + if (isSqlReadyForUse()) { + this.callString = resolveSql(); + } + else { + StringBuilder callString = new StringBuilder(32); + List parameters = getDeclaredParameters(); + int parameterCount = 0; + if (isFunction()) { + callString.append("{? = call ").append(resolveSql()).append('('); + parameterCount = -1; + } + else { + callString.append("{call ").append(resolveSql()).append('('); + } + for (SqlParameter parameter : parameters) { + if (!parameter.isResultsParameter()) { + if (parameterCount > 0) { + callString.append(", "); + } + if (parameterCount >= 0) { + callString.append('?'); + } + parameterCount++; + } + } + callString.append(")}"); + this.callString = callString.toString(); + } + if (logger.isDebugEnabled()) { + logger.debug("Compiled stored procedure. Call string is [" + this.callString + "]"); + } + + this.callableStatementFactory = new CallableStatementCreatorFactory(this.callString, getDeclaredParameters()); + this.callableStatementFactory.setResultSetType(getResultSetType()); + this.callableStatementFactory.setUpdatableResults(isUpdatableResults()); + + onCompileInternal(); + } + + /** + * Hook method that subclasses may override to react to compilation. + * This implementation does nothing. + */ + protected void onCompileInternal() { + } + + /** + * Get the call string. + */ + @Nullable + public String getCallString() { + return this.callString; + } + + /** + * Return a CallableStatementCreator to perform an operation + * with this parameters. + * @param inParams parameters. May be {@code null}. + */ + protected CallableStatementCreator newCallableStatementCreator(@Nullable Map inParams) { + Assert.state(this.callableStatementFactory != null, "No CallableStatementFactory available"); + return this.callableStatementFactory.newCallableStatementCreator(inParams); + } + + /** + * Return a CallableStatementCreator to perform an operation + * with the parameters returned from this ParameterMapper. + * @param inParamMapper parametermapper. May not be {@code null}. + */ + protected CallableStatementCreator newCallableStatementCreator(ParameterMapper inParamMapper) { + Assert.state(this.callableStatementFactory != null, "No CallableStatementFactory available"); + return this.callableStatementFactory.newCallableStatementCreator(inParamMapper); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlFunction.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlFunction.java new file mode 100644 index 0000000..4a3cb1d --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlFunction.java @@ -0,0 +1,203 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.object; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.springframework.dao.TypeMismatchDataAccessException; +import org.springframework.jdbc.core.SingleColumnRowMapper; +import org.springframework.lang.Nullable; + +/** + * SQL "function" wrapper for a query that returns a single row of results. + * The default behavior is to return an int, but that can be overridden by + * using the constructor with an extra return type parameter. + * + *

    Intended to use to call SQL functions that return a single result using a + * query like "select user()" or "select sysdate from dual". It is not intended + * for calling more complex stored functions or for using a CallableStatement to + * invoke a stored procedure or stored function. Use StoredProcedure or SqlCall + * for this type of processing. + * + *

    This is a concrete class, which there is often no need to subclass. + * Code using this package can create an object of this type, declaring SQL + * and parameters, and then invoke the appropriate {@code run} method + * repeatedly to execute the function. Subclasses are only supposed to add + * specialized {@code run} methods for specific parameter and return types. + * + *

    Like all RdbmsOperation objects, SqlFunction objects are thread-safe. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Jean-Pierre Pawlak + * @param the result type + * @see StoredProcedure + */ +public class SqlFunction extends MappingSqlQuery { + + private final SingleColumnRowMapper rowMapper = new SingleColumnRowMapper<>(); + + + /** + * Constructor to allow use as a JavaBean. + * A DataSource, SQL and any parameters must be supplied before + * invoking the {@code compile} method and using this object. + * @see #setDataSource + * @see #setSql + * @see #compile + */ + public SqlFunction() { + setRowsExpected(1); + } + + /** + * Create a new SqlFunction object with SQL, but without parameters. + * Must add parameters or settle with none. + * @param ds the DataSource to obtain connections from + * @param sql the SQL to execute + */ + public SqlFunction(DataSource ds, String sql) { + setRowsExpected(1); + setDataSource(ds); + setSql(sql); + } + + /** + * Create a new SqlFunction object with SQL and parameters. + * @param ds the DataSource to obtain connections from + * @param sql the SQL to execute + * @param types the SQL types of the parameters, as defined in the + * {@code java.sql.Types} class + * @see java.sql.Types + */ + public SqlFunction(DataSource ds, String sql, int[] types) { + setRowsExpected(1); + setDataSource(ds); + setSql(sql); + setTypes(types); + } + + /** + * Create a new SqlFunction object with SQL, parameters and a result type. + * @param ds the DataSource to obtain connections from + * @param sql the SQL to execute + * @param types the SQL types of the parameters, as defined in the + * {@code java.sql.Types} class + * @param resultType the type that the result object is required to match + * @see #setResultType(Class) + * @see java.sql.Types + */ + public SqlFunction(DataSource ds, String sql, int[] types, Class resultType) { + setRowsExpected(1); + setDataSource(ds); + setSql(sql); + setTypes(types); + setResultType(resultType); + } + + + /** + * Specify the type that the result object is required to match. + *

    If not specified, the result value will be exposed as + * returned by the JDBC driver. + */ + public void setResultType(Class resultType) { + this.rowMapper.setRequiredType(resultType); + } + + + /** + * This implementation of this method extracts a single value from the + * single row returned by the function. If there are a different number + * of rows returned, this is treated as an error. + */ + @Override + @Nullable + protected T mapRow(ResultSet rs, int rowNum) throws SQLException { + return this.rowMapper.mapRow(rs, rowNum); + } + + + /** + * Convenient method to run the function without arguments. + * @return the value of the function + */ + public int run() { + return run(new Object[0]); + } + + /** + * Convenient method to run the function with a single int argument. + * @param parameter single int parameter + * @return the value of the function + */ + public int run(int parameter) { + return run(new Object[] {parameter}); + } + + /** + * Analogous to the SqlQuery.execute([]) method. This is a + * generic method to execute a query, taken a number of arguments. + * @param parameters array of parameters. These will be objects or + * object wrapper types for primitives. + * @return the value of the function + */ + public int run(Object... parameters) { + Object obj = super.findObject(parameters); + if (!(obj instanceof Number)) { + throw new TypeMismatchDataAccessException("Could not convert result object [" + obj + "] to int"); + } + return ((Number) obj).intValue(); + } + + /** + * Convenient method to run the function without arguments, + * returning the value as an object. + * @return the value of the function + */ + @Nullable + public Object runGeneric() { + return findObject((Object[]) null, null); + } + + /** + * Convenient method to run the function with a single int argument. + * @param parameter single int parameter + * @return the value of the function as an Object + */ + @Nullable + public Object runGeneric(int parameter) { + return findObject(parameter); + } + + /** + * Analogous to the {@code SqlQuery.findObject(Object[])} method. + * This is a generic method to execute a query, taken a number of arguments. + * @param parameters array of parameters. These will be objects or + * object wrapper types for primitives. + * @return the value of the function, as an Object + * @see #execute(Object[]) + */ + @Nullable + public Object runGeneric(Object[] parameters) { + return findObject(parameters); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlOperation.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlOperation.java new file mode 100644 index 0000000..94c4af9 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlOperation.java @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.object; + +import org.springframework.jdbc.core.PreparedStatementCreator; +import org.springframework.jdbc.core.PreparedStatementCreatorFactory; +import org.springframework.jdbc.core.PreparedStatementSetter; +import org.springframework.jdbc.core.namedparam.NamedParameterUtils; +import org.springframework.jdbc.core.namedparam.ParsedSql; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Operation object representing an SQL-based operation such as a query or update, + * as opposed to a stored procedure. + * + *

    Configures a {@link org.springframework.jdbc.core.PreparedStatementCreatorFactory} + * based on the declared parameters. + * + * @author Rod Johnson + * @author Juergen Hoeller + */ +public abstract class SqlOperation extends RdbmsOperation { + + /** + * Object enabling us to create PreparedStatementCreators efficiently, + * based on this class's declared parameters. + */ + @Nullable + private PreparedStatementCreatorFactory preparedStatementFactory; + + /** Parsed representation of the SQL statement. */ + @Nullable + private ParsedSql cachedSql; + + /** Monitor for locking the cached representation of the parsed SQL statement. */ + private final Object parsedSqlMonitor = new Object(); + + + /** + * Overridden method to configure the PreparedStatementCreatorFactory + * based on our declared parameters. + */ + @Override + protected final void compileInternal() { + this.preparedStatementFactory = new PreparedStatementCreatorFactory(resolveSql(), getDeclaredParameters()); + this.preparedStatementFactory.setResultSetType(getResultSetType()); + this.preparedStatementFactory.setUpdatableResults(isUpdatableResults()); + this.preparedStatementFactory.setReturnGeneratedKeys(isReturnGeneratedKeys()); + if (getGeneratedKeysColumnNames() != null) { + this.preparedStatementFactory.setGeneratedKeysColumnNames(getGeneratedKeysColumnNames()); + } + + onCompileInternal(); + } + + /** + * Hook method that subclasses may override to post-process compilation. + * This implementation does nothing. + * @see #compileInternal + */ + protected void onCompileInternal() { + } + + /** + * Obtain a parsed representation of this operation's SQL statement. + *

    Typically used for named parameter parsing. + */ + protected ParsedSql getParsedSql() { + synchronized (this.parsedSqlMonitor) { + if (this.cachedSql == null) { + this.cachedSql = NamedParameterUtils.parseSqlStatement(resolveSql()); + } + return this.cachedSql; + } + } + + + /** + * Return a PreparedStatementSetter to perform an operation + * with the given parameters. + * @param params the parameter array (may be {@code null}) + */ + protected final PreparedStatementSetter newPreparedStatementSetter(@Nullable Object[] params) { + Assert.state(this.preparedStatementFactory != null, "No PreparedStatementFactory available"); + return this.preparedStatementFactory.newPreparedStatementSetter(params); + } + + /** + * Return a PreparedStatementCreator to perform an operation + * with the given parameters. + * @param params the parameter array (may be {@code null}) + */ + protected final PreparedStatementCreator newPreparedStatementCreator(@Nullable Object[] params) { + Assert.state(this.preparedStatementFactory != null, "No PreparedStatementFactory available"); + return this.preparedStatementFactory.newPreparedStatementCreator(params); + } + + /** + * Return a PreparedStatementCreator to perform an operation + * with the given parameters. + * @param sqlToUse the actual SQL statement to use (if different from + * the factory's, for example because of named parameter expanding) + * @param params the parameter array (may be {@code null}) + */ + protected final PreparedStatementCreator newPreparedStatementCreator(String sqlToUse, @Nullable Object[] params) { + Assert.state(this.preparedStatementFactory != null, "No PreparedStatementFactory available"); + return this.preparedStatementFactory.newPreparedStatementCreator(sqlToUse, params); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlQuery.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlQuery.java new file mode 100644 index 0000000..1707719 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlQuery.java @@ -0,0 +1,377 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.object; + +import java.util.List; +import java.util.Map; + +import javax.sql.DataSource; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.support.DataAccessUtils; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterUtils; +import org.springframework.jdbc.core.namedparam.ParsedSql; +import org.springframework.lang.Nullable; + +/** + * Reusable operation object representing an SQL query. + * + *

    Subclasses must implement the {@link #newRowMapper} method to provide + * an object that can extract the results of iterating over the + * {@code ResultSet} created during the execution of the query. + * + *

    This class provides a number of public {@code execute} methods that are + * analogous to the different convenient JDO query execute methods. Subclasses + * can either rely on one of these inherited methods, or can add their own + * custom execution methods, with meaningful names and typed parameters + * (definitely a best practice). Each custom query method will invoke one of + * this class's untyped query methods. + * + *

    Like all {@code RdbmsOperation} classes that ship with the Spring + * Framework, {@code SqlQuery} instances are thread-safe after their + * initialization is complete. That is, after they are constructed and configured + * via their setter methods, they can be used safely from multiple threads. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Thomas Risberg + * @param the result type + * @see SqlUpdate + */ +public abstract class SqlQuery extends SqlOperation { + + /** The number of rows to expect; if 0, unknown. */ + private int rowsExpected = 0; + + + /** + * Constructor to allow use as a JavaBean. + *

    The {@code DataSource} and SQL must be supplied before + * compilation and use. + */ + public SqlQuery() { + } + + /** + * Convenient constructor with a {@code DataSource} and SQL string. + * @param ds the {@code DataSource} to use to get connections + * @param sql the SQL to execute; SQL can also be supplied at runtime + * by overriding the {@link #getSql()} method. + */ + public SqlQuery(DataSource ds, String sql) { + setDataSource(ds); + setSql(sql); + } + + + /** + * Set the number of rows expected. + *

    This can be used to ensure efficient storage of results. The + * default behavior is not to expect any specific number of rows. + */ + public void setRowsExpected(int rowsExpected) { + this.rowsExpected = rowsExpected; + } + + /** + * Get the number of rows expected. + */ + public int getRowsExpected() { + return this.rowsExpected; + } + + + /** + * Central execution method. All un-named parameter execution goes through this method. + * @param params parameters, similar to JDO query parameters. + * Primitive parameters must be represented by their Object wrapper type. + * The ordering of parameters is significant. + * @param context the contextual information passed to the {@code mapRow} + * callback method. The JDBC operation itself doesn't rely on this parameter, + * but it can be useful for creating the objects of the result list. + * @return a List of objects, one per row of the ResultSet. Normally all these + * will be of the same class, although it is possible to use different types. + */ + public List execute(@Nullable Object[] params, @Nullable Map context) throws DataAccessException { + validateParameters(params); + RowMapper rowMapper = newRowMapper(params, context); + return getJdbcTemplate().query(newPreparedStatementCreator(params), rowMapper); + } + + /** + * Convenient method to execute without context. + * @param params parameters for the query. Primitive parameters must + * be represented by their Object wrapper type. The ordering of parameters is + * significant. + */ + public List execute(Object... params) throws DataAccessException { + return execute(params, null); + } + + /** + * Convenient method to execute without parameters. + * @param context the contextual information for object creation + */ + public List execute(Map context) throws DataAccessException { + return execute((Object[]) null, context); + } + + /** + * Convenient method to execute without parameters nor context. + */ + public List execute() throws DataAccessException { + return execute((Object[]) null, null); + } + + /** + * Convenient method to execute with a single int parameter and context. + * @param p1 single int parameter + * @param context the contextual information for object creation + */ + public List execute(int p1, @Nullable Map context) throws DataAccessException { + return execute(new Object[] {p1}, context); + } + + /** + * Convenient method to execute with a single int parameter. + * @param p1 single int parameter + */ + public List execute(int p1) throws DataAccessException { + return execute(p1, null); + } + + /** + * Convenient method to execute with two int parameters and context. + * @param p1 first int parameter + * @param p2 second int parameter + * @param context the contextual information for object creation + */ + public List execute(int p1, int p2, @Nullable Map context) throws DataAccessException { + return execute(new Object[] {p1, p2}, context); + } + + /** + * Convenient method to execute with two int parameters. + * @param p1 first int parameter + * @param p2 second int parameter + */ + public List execute(int p1, int p2) throws DataAccessException { + return execute(p1, p2, null); + } + + /** + * Convenient method to execute with a single long parameter and context. + * @param p1 single long parameter + * @param context the contextual information for object creation + */ + public List execute(long p1, @Nullable Map context) throws DataAccessException { + return execute(new Object[] {p1}, context); + } + + /** + * Convenient method to execute with a single long parameter. + * @param p1 single long parameter + */ + public List execute(long p1) throws DataAccessException { + return execute(p1, null); + } + + /** + * Convenient method to execute with a single String parameter and context. + * @param p1 single String parameter + * @param context the contextual information for object creation + */ + public List execute(String p1, @Nullable Map context) throws DataAccessException { + return execute(new Object[] {p1}, context); + } + + /** + * Convenient method to execute with a single String parameter. + * @param p1 single String parameter + */ + public List execute(String p1) throws DataAccessException { + return execute(p1, null); + } + + /** + * Central execution method. All named parameter execution goes through this method. + * @param paramMap parameters associated with the name specified while declaring + * the SqlParameters. Primitive parameters must be represented by their Object wrapper + * type. The ordering of parameters is not significant since they are supplied in a + * SqlParameterMap which is an implementation of the Map interface. + * @param context the contextual information passed to the {@code mapRow} + * callback method. The JDBC operation itself doesn't rely on this parameter, + * but it can be useful for creating the objects of the result list. + * @return a List of objects, one per row of the ResultSet. Normally all these + * will be of the same class, although it is possible to use different types. + */ + public List executeByNamedParam(Map paramMap, @Nullable Map context) throws DataAccessException { + validateNamedParameters(paramMap); + ParsedSql parsedSql = getParsedSql(); + MapSqlParameterSource paramSource = new MapSqlParameterSource(paramMap); + String sqlToUse = NamedParameterUtils.substituteNamedParameters(parsedSql, paramSource); + Object[] params = NamedParameterUtils.buildValueArray(parsedSql, paramSource, getDeclaredParameters()); + RowMapper rowMapper = newRowMapper(params, context); + return getJdbcTemplate().query(newPreparedStatementCreator(sqlToUse, params), rowMapper); + } + + /** + * Convenient method to execute without context. + * @param paramMap parameters associated with the name specified while declaring + * the SqlParameters. Primitive parameters must be represented by their Object wrapper + * type. The ordering of parameters is not significant. + */ + public List executeByNamedParam(Map paramMap) throws DataAccessException { + return executeByNamedParam(paramMap, null); + } + + + /** + * Generic object finder method, used by all other {@code findObject} methods. + * Object finder methods are like EJB entity bean finders, in that it is + * considered an error if they return more than one result. + * @return the result object, or {@code null} if not found. Subclasses may + * choose to treat this as an error and throw an exception. + * @see org.springframework.dao.support.DataAccessUtils#singleResult + */ + @Nullable + public T findObject(@Nullable Object[] params, @Nullable Map context) throws DataAccessException { + List results = execute(params, context); + return DataAccessUtils.singleResult(results); + } + + /** + * Convenient method to find a single object without context. + */ + @Nullable + public T findObject(Object... params) throws DataAccessException { + return findObject(params, null); + } + + /** + * Convenient method to find a single object given a single int parameter + * and a context. + */ + @Nullable + public T findObject(int p1, @Nullable Map context) throws DataAccessException { + return findObject(new Object[] {p1}, context); + } + + /** + * Convenient method to find a single object given a single int parameter. + */ + @Nullable + public T findObject(int p1) throws DataAccessException { + return findObject(p1, null); + } + + /** + * Convenient method to find a single object given two int parameters + * and a context. + */ + @Nullable + public T findObject(int p1, int p2, @Nullable Map context) throws DataAccessException { + return findObject(new Object[] {p1, p2}, context); + } + + /** + * Convenient method to find a single object given two int parameters. + */ + @Nullable + public T findObject(int p1, int p2) throws DataAccessException { + return findObject(p1, p2, null); + } + + /** + * Convenient method to find a single object given a single long parameter + * and a context. + */ + @Nullable + public T findObject(long p1, @Nullable Map context) throws DataAccessException { + return findObject(new Object[] {p1}, context); + } + + /** + * Convenient method to find a single object given a single long parameter. + */ + @Nullable + public T findObject(long p1) throws DataAccessException { + return findObject(p1, null); + } + + /** + * Convenient method to find a single object given a single String parameter + * and a context. + */ + @Nullable + public T findObject(String p1, @Nullable Map context) throws DataAccessException { + return findObject(new Object[] {p1}, context); + } + + /** + * Convenient method to find a single object given a single String parameter. + */ + @Nullable + public T findObject(String p1) throws DataAccessException { + return findObject(p1, null); + } + + /** + * Generic object finder method for named parameters. + * @param paramMap a Map of parameter name to parameter object, + * matching named parameters specified in the SQL statement. + * Ordering is not significant. + * @param context the contextual information passed to the {@code mapRow} + * callback method. The JDBC operation itself doesn't rely on this parameter, + * but it can be useful for creating the objects of the result list. + * @return a List of objects, one per row of the ResultSet. Normally all these + * will be of the same class, although it is possible to use different types. + */ + @Nullable + public T findObjectByNamedParam(Map paramMap, @Nullable Map context) throws DataAccessException { + List results = executeByNamedParam(paramMap, context); + return DataAccessUtils.singleResult(results); + } + + /** + * Convenient method to execute without context. + * @param paramMap a Map of parameter name to parameter object, + * matching named parameters specified in the SQL statement. + * Ordering is not significant. + */ + @Nullable + public T findObjectByNamedParam(Map paramMap) throws DataAccessException { + return findObjectByNamedParam(paramMap, null); + } + + + /** + * Subclasses must implement this method to extract an object per row, to be + * returned by the {@code execute} method as an aggregated {@link List}. + * @param parameters the parameters to the {@code execute()} method, + * in case subclass is interested; may be {@code null} if there + * were no parameters. + * @param context the contextual information passed to the {@code mapRow} + * callback method. The JDBC operation itself doesn't rely on this parameter, + * but it can be useful for creating the objects of the result list. + * @see #execute + */ + protected abstract RowMapper newRowMapper(@Nullable Object[] parameters, @Nullable Map context); + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlUpdate.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlUpdate.java new file mode 100644 index 0000000..a68e87c --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlUpdate.java @@ -0,0 +1,280 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.object; + +import java.util.Map; + +import javax.sql.DataSource; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.jdbc.JdbcUpdateAffectedIncorrectNumberOfRowsException; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterUtils; +import org.springframework.jdbc.core.namedparam.ParsedSql; +import org.springframework.jdbc.support.KeyHolder; + +/** + * Reusable operation object representing an SQL update. + * + *

    This class provides a number of {@code update} methods, + * analogous to the {@code execute} methods of query objects. + * + *

    This class is concrete. Although it can be subclassed (for example + * to add a custom update method) it can easily be parameterized by setting + * SQL and declaring parameters. + * + *

    Like all {@code RdbmsOperation} classes that ship with the Spring + * Framework, {@code SqlQuery} instances are thread-safe after their + * initialization is complete. That is, after they are constructed and configured + * via their setter methods, they can be used safely from multiple threads. + * + * @author Rod Johnson + * @author Thomas Risberg + * @author Juergen Hoeller + * @see SqlQuery + */ +public class SqlUpdate extends SqlOperation { + + /** + * Maximum number of rows the update may affect. If more are + * affected, an exception will be thrown. Ignored if 0. + */ + private int maxRowsAffected = 0; + + /** + * An exact number of rows that must be affected. + * Ignored if 0. + */ + private int requiredRowsAffected = 0; + + + /** + * Constructor to allow use as a JavaBean. DataSource and SQL + * must be supplied before compilation and use. + * @see #setDataSource + * @see #setSql + */ + public SqlUpdate() { + } + + /** + * Constructs an update object with a given DataSource and SQL. + * @param ds the DataSource to use to obtain connections + * @param sql the SQL statement to execute + */ + public SqlUpdate(DataSource ds, String sql) { + setDataSource(ds); + setSql(sql); + } + + /** + * Construct an update object with a given DataSource, SQL + * and anonymous parameters. + * @param ds the DataSource to use to obtain connections + * @param sql the SQL statement to execute + * @param types the SQL types of the parameters, as defined in the + * {@code java.sql.Types} class + * @see java.sql.Types + */ + public SqlUpdate(DataSource ds, String sql, int[] types) { + setDataSource(ds); + setSql(sql); + setTypes(types); + } + + /** + * Construct an update object with a given DataSource, SQL, + * anonymous parameters and specifying the maximum number of rows + * that may be affected. + * @param ds the DataSource to use to obtain connections + * @param sql the SQL statement to execute + * @param types the SQL types of the parameters, as defined in the + * {@code java.sql.Types} class + * @param maxRowsAffected the maximum number of rows that may + * be affected by the update + * @see java.sql.Types + */ + public SqlUpdate(DataSource ds, String sql, int[] types, int maxRowsAffected) { + setDataSource(ds); + setSql(sql); + setTypes(types); + this.maxRowsAffected = maxRowsAffected; + } + + + /** + * Set the maximum number of rows that may be affected by this update. + * The default value is 0, which does not limit the number of rows affected. + * @param maxRowsAffected the maximum number of rows that can be affected by + * this update without this class's update method considering it an error + */ + public void setMaxRowsAffected(int maxRowsAffected) { + this.maxRowsAffected = maxRowsAffected; + } + + /** + * Set the exact number of rows that must be affected by this update. + * The default value is 0, which allows any number of rows to be affected. + *

    This is an alternative to setting the maximum number of rows + * that may be affected. + * @param requiredRowsAffected the exact number of rows that must be affected + * by this update without this class's update method considering it an error + */ + public void setRequiredRowsAffected(int requiredRowsAffected) { + this.requiredRowsAffected = requiredRowsAffected; + } + + /** + * Check the given number of affected rows against the + * specified maximum number or required number. + * @param rowsAffected the number of affected rows + * @throws JdbcUpdateAffectedIncorrectNumberOfRowsException + * if the actually affected rows are out of bounds + * @see #setMaxRowsAffected + * @see #setRequiredRowsAffected + */ + protected void checkRowsAffected(int rowsAffected) throws JdbcUpdateAffectedIncorrectNumberOfRowsException { + if (this.maxRowsAffected > 0 && rowsAffected > this.maxRowsAffected) { + throw new JdbcUpdateAffectedIncorrectNumberOfRowsException(resolveSql(), this.maxRowsAffected, rowsAffected); + } + if (this.requiredRowsAffected > 0 && rowsAffected != this.requiredRowsAffected) { + throw new JdbcUpdateAffectedIncorrectNumberOfRowsException(resolveSql(), this.requiredRowsAffected, rowsAffected); + } + } + + + /** + * Generic method to execute the update given parameters. + * All other update methods invoke this method. + * @param params array of parameters objects + * @return the number of rows affected by the update + */ + public int update(Object... params) throws DataAccessException { + validateParameters(params); + int rowsAffected = getJdbcTemplate().update(newPreparedStatementCreator(params)); + checkRowsAffected(rowsAffected); + return rowsAffected; + } + + /** + * Method to execute the update given arguments and + * retrieve the generated keys using a KeyHolder. + * @param params array of parameter objects + * @param generatedKeyHolder the KeyHolder that will hold the generated keys + * @return the number of rows affected by the update + */ + public int update(Object[] params, KeyHolder generatedKeyHolder) throws DataAccessException { + if (!isReturnGeneratedKeys() && getGeneratedKeysColumnNames() == null) { + throw new InvalidDataAccessApiUsageException( + "The update method taking a KeyHolder should only be used when generated keys have " + + "been configured by calling either 'setReturnGeneratedKeys' or " + + "'setGeneratedKeysColumnNames'."); + } + validateParameters(params); + int rowsAffected = getJdbcTemplate().update(newPreparedStatementCreator(params), generatedKeyHolder); + checkRowsAffected(rowsAffected); + return rowsAffected; + } + + /** + * Convenience method to execute an update with no parameters. + */ + public int update() throws DataAccessException { + return update(new Object[0]); + } + + /** + * Convenient method to execute an update given one int arg. + */ + public int update(int p1) throws DataAccessException { + return update(new Object[] {p1}); + } + + /** + * Convenient method to execute an update given two int args. + */ + public int update(int p1, int p2) throws DataAccessException { + return update(new Object[] {p1, p2}); + } + + /** + * Convenient method to execute an update given one long arg. + */ + public int update(long p1) throws DataAccessException { + return update(new Object[] {p1}); + } + + /** + * Convenient method to execute an update given two long args. + */ + public int update(long p1, long p2) throws DataAccessException { + return update(new Object[] {p1, p2}); + } + + /** + * Convenient method to execute an update given one String arg. + */ + public int update(String p) throws DataAccessException { + return update(new Object[] {p}); + } + + /** + * Convenient method to execute an update given two String args. + */ + public int update(String p1, String p2) throws DataAccessException { + return update(new Object[] {p1, p2}); + } + + /** + * Generic method to execute the update given named parameters. + * All other update methods invoke this method. + * @param paramMap a Map of parameter name to parameter object, + * matching named parameters specified in the SQL statement + * @return the number of rows affected by the update + */ + public int updateByNamedParam(Map paramMap) throws DataAccessException { + validateNamedParameters(paramMap); + ParsedSql parsedSql = getParsedSql(); + MapSqlParameterSource paramSource = new MapSqlParameterSource(paramMap); + String sqlToUse = NamedParameterUtils.substituteNamedParameters(parsedSql, paramSource); + Object[] params = NamedParameterUtils.buildValueArray(parsedSql, paramSource, getDeclaredParameters()); + int rowsAffected = getJdbcTemplate().update(newPreparedStatementCreator(sqlToUse, params)); + checkRowsAffected(rowsAffected); + return rowsAffected; + } + + /** + * Method to execute the update given arguments and + * retrieve the generated keys using a KeyHolder. + * @param paramMap a Map of parameter name to parameter object, + * matching named parameters specified in the SQL statement + * @param generatedKeyHolder the KeyHolder that will hold the generated keys + * @return the number of rows affected by the update + */ + public int updateByNamedParam(Map paramMap, KeyHolder generatedKeyHolder) throws DataAccessException { + validateNamedParameters(paramMap); + ParsedSql parsedSql = getParsedSql(); + MapSqlParameterSource paramSource = new MapSqlParameterSource(paramMap); + String sqlToUse = NamedParameterUtils.substituteNamedParameters(parsedSql, paramSource); + Object[] params = NamedParameterUtils.buildValueArray(parsedSql, paramSource, getDeclaredParameters()); + int rowsAffected = getJdbcTemplate().update(newPreparedStatementCreator(sqlToUse, params), generatedKeyHolder); + checkRowsAffected(rowsAffected); + return rowsAffected; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/StoredProcedure.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/StoredProcedure.java new file mode 100644 index 0000000..d6a24e5 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/StoredProcedure.java @@ -0,0 +1,162 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.object; + +import java.util.HashMap; +import java.util.Map; + +import javax.sql.DataSource; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.ParameterMapper; +import org.springframework.jdbc.core.SqlParameter; + +/** + * Superclass for object abstractions of RDBMS stored procedures. + * This class is abstract and it is intended that subclasses will provide a typed + * method for invocation that delegates to the supplied {@link #execute} method. + * + *

    The inherited {@link #setSql sql} property is the name of the stored procedure + * in the RDBMS. + * + * @author Rod Johnson + * @author Thomas Risberg + */ +public abstract class StoredProcedure extends SqlCall { + + /** + * Allow use as a bean. + */ + protected StoredProcedure() { + } + + /** + * Create a new object wrapper for a stored procedure. + * @param ds the DataSource to use throughout the lifetime + * of this object to obtain connections + * @param name the name of the stored procedure in the database + */ + protected StoredProcedure(DataSource ds, String name) { + setDataSource(ds); + setSql(name); + } + + /** + * Create a new object wrapper for a stored procedure. + * @param jdbcTemplate the JdbcTemplate which wraps DataSource + * @param name the name of the stored procedure in the database + */ + protected StoredProcedure(JdbcTemplate jdbcTemplate, String name) { + setJdbcTemplate(jdbcTemplate); + setSql(name); + } + + + /** + * StoredProcedure parameter Maps are by default allowed to contain + * additional entries that are not actually used as parameters. + */ + @Override + protected boolean allowsUnusedParameters() { + return true; + } + + /** + * Declare a parameter. + *

    Parameters declared as {@code SqlParameter} and {@code SqlInOutParameter} + * will always be used to provide input values. In addition to this, any parameter declared + * as {@code SqlOutParameter} where a non-null input value is provided will also be used + * as an input parameter. + * Note: Calls to declareParameter must be made in the same order as + * they appear in the database's stored procedure parameter list. + *

    Names are purely used to help mapping. + * @param param the parameter object + */ + @Override + public void declareParameter(SqlParameter param) throws InvalidDataAccessApiUsageException { + if (param.getName() == null) { + throw new InvalidDataAccessApiUsageException("Parameters to stored procedures must have names as well as types"); + } + super.declareParameter(param); + } + + /** + * Execute the stored procedure with the provided parameter values. This is + * a convenience method where the order of the passed in parameter values + * must match the order that the parameters where declared in. + * @param inParams variable number of input parameters. Output parameters should + * not be included in this map. It is legal for values to be {@code null}, and this + * will produce the correct behavior using a NULL argument to the stored procedure. + * @return map of output params, keyed by name as in parameter declarations. + * Output parameters will appear here, with their values after the stored procedure + * has been called. + */ + public Map execute(Object... inParams) { + Map paramsToUse = new HashMap<>(); + validateParameters(inParams); + int i = 0; + for (SqlParameter sqlParameter : getDeclaredParameters()) { + if (sqlParameter.isInputValueProvided() && i < inParams.length) { + paramsToUse.put(sqlParameter.getName(), inParams[i++]); + } + } + return getJdbcTemplate().call(newCallableStatementCreator(paramsToUse), getDeclaredParameters()); + } + + /** + * Execute the stored procedure. Subclasses should define a strongly typed + * execute method (with a meaningful name) that invokes this method, populating + * the input map and extracting typed values from the output map. Subclass + * execute methods will often take domain objects as arguments and return values. + * Alternatively, they can return void. + * @param inParams map of input parameters, keyed by name as in parameter + * declarations. Output parameters need not (but can) be included in this map. + * It is legal for map entries to be {@code null}, and this will produce the + * correct behavior using a NULL argument to the stored procedure. + * @return map of output params, keyed by name as in parameter declarations. + * Output parameters will appear here, with their values after the + * stored procedure has been called. + */ + public Map execute(Map inParams) throws DataAccessException { + validateParameters(inParams.values().toArray()); + return getJdbcTemplate().call(newCallableStatementCreator(inParams), getDeclaredParameters()); + } + + /** + * Execute the stored procedure. Subclasses should define a strongly typed + * execute method (with a meaningful name) that invokes this method, passing in + * a ParameterMapper that will populate the input map. This allows mapping database + * specific features since the ParameterMapper has access to the Connection object. + * The execute method is also responsible for extracting typed values from the output map. + * Subclass execute methods will often take domain objects as arguments and return values. + * Alternatively, they can return void. + * @param inParamMapper map of input parameters, keyed by name as in parameter + * declarations. Output parameters need not (but can) be included in this map. + * It is legal for map entries to be {@code null}, and this will produce the correct + * behavior using a NULL argument to the stored procedure. + * @return map of output params, keyed by name as in parameter declarations. + * Output parameters will appear here, with their values after the + * stored procedure has been called. + */ + public Map execute(ParameterMapper inParamMapper) throws DataAccessException { + checkCompiled(); + return getJdbcTemplate().call(newCallableStatementCreator(inParamMapper), getDeclaredParameters()); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/UpdatableSqlQuery.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/UpdatableSqlQuery.java new file mode 100644 index 0000000..3c731b0 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/UpdatableSqlQuery.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.object; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Map; + +import javax.sql.DataSource; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.lang.Nullable; + +/** + * Reusable RDBMS query in which concrete subclasses must implement + * the abstract updateRow(ResultSet, int, context) method to update each + * row of the JDBC ResultSet and optionally map contents into an object. + * + *

    Subclasses can be constructed providing SQL, parameter types + * and a DataSource. SQL will often vary between subclasses. + * + * @author Thomas Risberg + * @param the result type + * @see org.springframework.jdbc.object.SqlQuery + */ +public abstract class UpdatableSqlQuery extends SqlQuery { + + /** + * Constructor to allow use as a JavaBean. + */ + public UpdatableSqlQuery() { + setUpdatableResults(true); + } + + /** + * Convenient constructor with DataSource and SQL string. + * @param ds the DataSource to use to get connections + * @param sql the SQL to run + */ + public UpdatableSqlQuery(DataSource ds, String sql) { + super(ds, sql); + setUpdatableResults(true); + } + + + /** + * Implementation of the superclass template method. This invokes the subclass's + * implementation of the {@code updateRow()} method. + */ + @Override + protected RowMapper newRowMapper(@Nullable Object[] parameters, @Nullable Map context) { + return new RowMapperImpl(context); + } + + /** + * Subclasses must implement this method to update each row of the + * ResultSet and optionally create object of the result type. + * @param rs the ResultSet we're working through + * @param rowNum row number (from 0) we're up to + * @param context passed to the execute() method. + * It can be {@code null} if no contextual information is need. If you + * need to pass in data for each row, you can pass in a HashMap with + * the primary key of the row being the key for the HashMap. That way + * it is easy to locate the updates for each row + * @return an object of the result type + * @throws SQLException if there's an error updateing data. + * Subclasses can simply not catch SQLExceptions, relying on the + * framework to clean up. + */ + protected abstract T updateRow(ResultSet rs, int rowNum, @Nullable Map context) throws SQLException; + + + /** + * Implementation of RowMapper that calls the enclosing + * class's {@code updateRow()} method for each row. + */ + protected class RowMapperImpl implements RowMapper { + + @Nullable + private final Map context; + + public RowMapperImpl(@Nullable Map context) { + this.context = context; + } + + @Override + public T mapRow(ResultSet rs, int rowNum) throws SQLException { + T result = updateRow(rs, rowNum, this.context); + rs.updateRow(); + return result; + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/package-info.java new file mode 100644 index 0000000..b13c09b --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/package-info.java @@ -0,0 +1,22 @@ +/** + * The classes in this package represent RDBMS queries, updates, + * and stored procedures as threadsafe, reusable objects. This approach + * is modelled by JDO, although of course objects returned by queries + * are "disconnected" from the database. + * + *

    This higher level of JDBC abstraction depends on the lower-level + * abstraction in the {@code org.springframework.jdbc.core} package. + * Exceptions thrown are as in the {@code org.springframework.dao} package, + * meaning that code using this package does not need to implement JDBC or + * RDBMS-specific error handling. + * + *

    This package and related packages are discussed in Chapter 9 of + * Expert One-On-One J2EE Design and Development + * by Rod Johnson (Wrox, 2002). + */ +@NonNullApi +@NonNullFields +package org.springframework.jdbc.object; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/package-info.java new file mode 100644 index 0000000..86f4c59 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/package-info.java @@ -0,0 +1,25 @@ +/** + * The classes in this package make JDBC easier to use and + * reduce the likelihood of common errors. In particular, they: + *

      + *
    • Simplify error handling, avoiding the need for try/catch/finally + * blocks in application code. + *
    • Present exceptions to application code in a generic hierarchy of + * unchecked exceptions, enabling applications to catch data access + * exceptions without being dependent on JDBC, and to ignore fatal + * exceptions there is no value in catching. + *
    • Allow the implementation of error handling to be modified + * to target different RDBMSes without introducing proprietary + * dependencies into application code. + *
    + * + *

    This package and related packages are discussed in Chapter 9 of + * Expert One-On-One J2EE Design and Development + * by Rod Johnson (Wrox, 2002). + */ +@NonNullApi +@NonNullFields +package org.springframework.jdbc; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/AbstractFallbackSQLExceptionTranslator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/AbstractFallbackSQLExceptionTranslator.java new file mode 100644 index 0000000..44b8838 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/AbstractFallbackSQLExceptionTranslator.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import java.sql.SQLException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.dao.DataAccessException; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Base class for {@link SQLExceptionTranslator} implementations that allow for + * fallback to some other {@link SQLExceptionTranslator}. + * + * @author Juergen Hoeller + * @since 2.5.6 + */ +public abstract class AbstractFallbackSQLExceptionTranslator implements SQLExceptionTranslator { + + /** Logger available to subclasses. */ + protected final Log logger = LogFactory.getLog(getClass()); + + @Nullable + private SQLExceptionTranslator fallbackTranslator; + + + /** + * Override the default SQL state fallback translator + * (typically a {@link SQLStateSQLExceptionTranslator}). + */ + public void setFallbackTranslator(@Nullable SQLExceptionTranslator fallback) { + this.fallbackTranslator = fallback; + } + + /** + * Return the fallback exception translator, if any. + */ + @Nullable + public SQLExceptionTranslator getFallbackTranslator() { + return this.fallbackTranslator; + } + + + /** + * Pre-checks the arguments, calls {@link #doTranslate}, and invokes the + * {@link #getFallbackTranslator() fallback translator} if necessary. + */ + @Override + @Nullable + public DataAccessException translate(String task, @Nullable String sql, SQLException ex) { + Assert.notNull(ex, "Cannot translate a null SQLException"); + + DataAccessException dae = doTranslate(task, sql, ex); + if (dae != null) { + // Specific exception match found. + return dae; + } + + // Looking for a fallback... + SQLExceptionTranslator fallback = getFallbackTranslator(); + if (fallback != null) { + return fallback.translate(task, sql, ex); + } + + return null; + } + + /** + * Template method for actually translating the given exception. + *

    The passed-in arguments will have been pre-checked. Furthermore, this method + * is allowed to return {@code null} to indicate that no exception match has + * been found and that fallback translation should kick in. + * @param task readable text describing the task being attempted + * @param sql the SQL query or update that caused the problem (if known) + * @param ex the offending {@code SQLException} + * @return the DataAccessException, wrapping the {@code SQLException}; + * or {@code null} if no exception match found + */ + @Nullable + protected abstract DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex); + + + /** + * Build a message {@code String} for the given {@link java.sql.SQLException}. + *

    To be called by translator subclasses when creating an instance of a generic + * {@link org.springframework.dao.DataAccessException} class. + * @param task readable text describing the task being attempted + * @param sql the SQL statement that caused the problem + * @param ex the offending {@code SQLException} + * @return the message {@code String} to use + */ + protected String buildMessage(String task, @Nullable String sql, SQLException ex) { + return task + "; " + (sql != null ? ("SQL [" + sql + "]; ") : "") + ex.getMessage(); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLErrorCodesTranslation.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLErrorCodesTranslation.java new file mode 100644 index 0000000..8c84cb3 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLErrorCodesTranslation.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import org.springframework.dao.DataAccessException; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * JavaBean for holding custom JDBC error codes translation for a particular + * database. The "exceptionClass" property defines which exception will be + * thrown for the list of error codes specified in the errorCodes property. + * + * @author Thomas Risberg + * @since 1.1 + * @see SQLErrorCodeSQLExceptionTranslator + */ +public class CustomSQLErrorCodesTranslation { + + private String[] errorCodes = new String[0]; + + @Nullable + private Class exceptionClass; + + + /** + * Set the SQL error codes to match. + */ + public void setErrorCodes(String... errorCodes) { + this.errorCodes = StringUtils.sortStringArray(errorCodes); + } + + /** + * Return the SQL error codes to match. + */ + public String[] getErrorCodes() { + return this.errorCodes; + } + + /** + * Set the exception class for the specified error codes. + */ + public void setExceptionClass(@Nullable Class exceptionClass) { + if (exceptionClass != null && !DataAccessException.class.isAssignableFrom(exceptionClass)) { + throw new IllegalArgumentException("Invalid exception class [" + exceptionClass + + "]: needs to be a subclass of [org.springframework.dao.DataAccessException]"); + } + this.exceptionClass = exceptionClass; + } + + /** + * Return the exception class for the specified error codes. + */ + @Nullable + public Class getExceptionClass() { + return this.exceptionClass; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistrar.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistrar.java new file mode 100644 index 0000000..0443d03 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistrar.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.factory.InitializingBean; + +/** + * Registry for custom {@link SQLExceptionTranslator} instances for specific databases. + * + * @author Thomas Risberg + * @since 3.1.1 + */ +public class CustomSQLExceptionTranslatorRegistrar implements InitializingBean { + + /** + * Map registry to hold custom translators specific databases. + * Key is the database product name as defined in the + * {@link org.springframework.jdbc.support.SQLErrorCodesFactory}. + */ + private final Map translators = new HashMap<>(); + + + /** + * Setter for a Map of {@link SQLExceptionTranslator} references where the key must + * be the database name as defined in the {@code sql-error-codes.xml} file. + *

    Note that any existing translators will remain unless there is a match in the + * database name, at which point the new translator will replace the existing one. + */ + public void setTranslators(Map translators) { + this.translators.putAll(translators); + } + + @Override + public void afterPropertiesSet() { + this.translators.forEach((dbName, translator) -> + CustomSQLExceptionTranslatorRegistry.getInstance().registerTranslator(dbName, translator)); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistry.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistry.java new file mode 100644 index 0000000..2908e65 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistry.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.lang.Nullable; + +/** + * Registry for custom {@link org.springframework.jdbc.support.SQLExceptionTranslator} instances associated with + * specific databases allowing for overriding translation based on values contained in the configuration file + * named "sql-error-codes.xml". + * + * @author Thomas Risberg + * @since 3.1.1 + * @see SQLErrorCodesFactory + */ +public final class CustomSQLExceptionTranslatorRegistry { + + private static final Log logger = LogFactory.getLog(CustomSQLExceptionTranslatorRegistry.class); + + /** + * Keep track of a single instance so we can return it to classes that request it. + */ + private static final CustomSQLExceptionTranslatorRegistry instance = new CustomSQLExceptionTranslatorRegistry(); + + + /** + * Return the singleton instance. + */ + public static CustomSQLExceptionTranslatorRegistry getInstance() { + return instance; + } + + + /** + * Map registry to hold custom translators specific databases. + * Key is the database product name as defined in the + * {@link org.springframework.jdbc.support.SQLErrorCodesFactory}. + */ + private final Map translatorMap = new HashMap<>(); + + + /** + * Create a new instance of the {@link CustomSQLExceptionTranslatorRegistry} class. + *

    Not public to enforce Singleton design pattern. + */ + private CustomSQLExceptionTranslatorRegistry() { + } + + + /** + * Register a new custom translator for the specified database name. + * @param dbName the database name + * @param translator the custom translator + */ + public void registerTranslator(String dbName, SQLExceptionTranslator translator) { + SQLExceptionTranslator replaced = this.translatorMap.put(dbName, translator); + if (logger.isDebugEnabled()) { + if (replaced != null) { + logger.debug("Replacing custom translator [" + replaced + "] for database '" + dbName + + "' with [" + translator + "]"); + } + else { + logger.debug("Adding custom translator of type [" + translator.getClass().getName() + + "] for database '" + dbName + "'"); + } + } + } + + /** + * Find a custom translator for the specified database. + * @param dbName the database name + * @return the custom translator, or {@code null} if none found + */ + @Nullable + public SQLExceptionTranslator findTranslatorForDatabase(String dbName) { + return this.translatorMap.get(dbName); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/DatabaseMetaDataCallback.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/DatabaseMetaDataCallback.java new file mode 100644 index 0000000..0f4197d --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/DatabaseMetaDataCallback.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +/** + * A callback interface used by the JdbcUtils class. Implementations of this + * interface perform the actual work of extracting database meta-data, but + * don't need to worry about exception handling. SQLExceptions will be caught + * and handled correctly by the JdbcUtils class. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @param the result type + * @see JdbcUtils#extractDatabaseMetaData(javax.sql.DataSource, DatabaseMetaDataCallback) + */ +@FunctionalInterface +public interface DatabaseMetaDataCallback { + + /** + * Implementations must implement this method to process the meta-data + * passed in. Exactly what the implementation chooses to do is up to it. + * @param dbmd the DatabaseMetaData to process + * @return a result object extracted from the meta-data + * (can be an arbitrary object, as needed by the implementation) + * @throws SQLException if an SQLException is encountered getting + * column values (that is, there's no need to catch SQLException) + * @throws MetaDataAccessException in case of other failures while + * extracting meta-data (for example, reflection failure) + */ + T processMetaData(DatabaseMetaData dbmd) throws SQLException, MetaDataAccessException; + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/DatabaseStartupValidator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/DatabaseStartupValidator.java new file mode 100644 index 0000000..f518103 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/DatabaseStartupValidator.java @@ -0,0 +1,183 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.concurrent.TimeUnit; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jdbc.CannotGetJdbcConnectionException; +import org.springframework.lang.Nullable; + +/** + * Bean that checks if a database has already started up. To be referenced + * via "depends-on" from beans that depend on database startup, like a Hibernate + * SessionFactory or custom data access objects that access a DataSource directly. + * + *

    Useful to defer application initialization until a database has started up. + * Particularly appropriate for waiting on a slowly starting Oracle database. + * + * @author Juergen Hoeller + * @author Marten Deinum + * @since 18.12.2003 + */ +public class DatabaseStartupValidator implements InitializingBean { + + /** + * The default interval. + */ + public static final int DEFAULT_INTERVAL = 1; + + /** + * The default timeout. + */ + public static final int DEFAULT_TIMEOUT = 60; + + + protected final Log logger = LogFactory.getLog(getClass()); + + @Nullable + private DataSource dataSource; + + @Nullable + private String validationQuery; + + private int interval = DEFAULT_INTERVAL; + + private int timeout = DEFAULT_TIMEOUT; + + + /** + * Set the DataSource to validate. + */ + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + } + + /** + * Set the SQL query string to use for validation. + * @deprecated as of 5.3, in favor of the JDBC 4.0 connection validation + */ + @Deprecated + public void setValidationQuery(String validationQuery) { + this.validationQuery = validationQuery; + } + + /** + * Set the interval between validation runs (in seconds). + * Default is {@value #DEFAULT_INTERVAL}. + */ + public void setInterval(int interval) { + this.interval = interval; + } + + /** + * Set the timeout (in seconds) after which a fatal exception + * will be thrown. Default is {@value #DEFAULT_TIMEOUT}. + */ + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + + /** + * Check whether the validation query can be executed on a Connection + * from the specified DataSource, with the specified interval between + * checks, until the specified timeout. + */ + @Override + public void afterPropertiesSet() { + if (this.dataSource == null) { + throw new IllegalArgumentException("Property 'dataSource' is required"); + } + + try { + boolean validated = false; + long beginTime = System.currentTimeMillis(); + long deadLine = beginTime + TimeUnit.SECONDS.toMillis(this.timeout); + SQLException latestEx = null; + + while (!validated && System.currentTimeMillis() < deadLine) { + Connection con = null; + Statement stmt = null; + try { + con = this.dataSource.getConnection(); + if (con == null) { + throw new CannotGetJdbcConnectionException("Failed to execute validation: " + + "DataSource returned null from getConnection(): " + this.dataSource); + } + if (this.validationQuery == null) { + validated = con.isValid(this.interval); + } + else { + stmt = con.createStatement(); + stmt.execute(this.validationQuery); + validated = true; + } + } + catch (SQLException ex) { + latestEx = ex; + if (logger.isDebugEnabled()) { + if (this.validationQuery != null) { + logger.debug("Validation query [" + this.validationQuery + "] threw exception", ex); + } + else { + logger.debug("Validation check threw exception", ex); + } + } + if (logger.isInfoEnabled()) { + float rest = ((float) (deadLine - System.currentTimeMillis())) / 1000; + if (rest > this.interval) { + logger.info("Database has not started up yet - retrying in " + this.interval + + " seconds (timeout in " + rest + " seconds)"); + } + } + } + finally { + JdbcUtils.closeStatement(stmt); + JdbcUtils.closeConnection(con); + } + + if (!validated) { + TimeUnit.SECONDS.sleep(this.interval); + } + } + + if (!validated) { + throw new CannotGetJdbcConnectionException( + "Database has not started up within " + this.timeout + " seconds", latestEx); + } + + if (logger.isInfoEnabled()) { + float duration = ((float) (System.currentTimeMillis() - beginTime)) / 1000; + logger.info("Database startup detected after " + duration + " seconds"); + } + } + catch (InterruptedException ex) { + // Re-interrupt current thread, to allow other threads to react. + Thread.currentThread().interrupt(); + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/GeneratedKeyHolder.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/GeneratedKeyHolder.java new file mode 100644 index 0000000..d1c2619 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/GeneratedKeyHolder.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.lang.Nullable; + +/** + * The standard implementation of the {@link KeyHolder} interface, to be used for + * holding auto-generated keys (as potentially returned by JDBC insert statements). + * + *

    Create an instance of this class for each insert operation, and pass it + * to the corresponding {@link org.springframework.jdbc.core.JdbcTemplate} or + * {@link org.springframework.jdbc.object.SqlUpdate} methods. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @author Slawomir Dymitrow + * @since 1.1 + */ +public class GeneratedKeyHolder implements KeyHolder { + + private final List> keyList; + + + /** + * Create a new GeneratedKeyHolder with a default list. + */ + public GeneratedKeyHolder() { + this.keyList = new ArrayList<>(1); + } + + /** + * Create a new GeneratedKeyHolder with a given list. + * @param keyList a list to hold maps of keys + */ + public GeneratedKeyHolder(List> keyList) { + this.keyList = keyList; + } + + + @Override + @Nullable + public Number getKey() throws InvalidDataAccessApiUsageException, DataRetrievalFailureException { + return getKeyAs(Number.class); + } + + @Override + @Nullable + public T getKeyAs(Class keyType) throws InvalidDataAccessApiUsageException, DataRetrievalFailureException { + if (this.keyList.isEmpty()) { + return null; + } + if (this.keyList.size() > 1 || this.keyList.get(0).size() > 1) { + throw new InvalidDataAccessApiUsageException( + "The getKey method should only be used when a single key is returned. " + + "The current key entry contains multiple keys: " + this.keyList); + } + Iterator keyIter = this.keyList.get(0).values().iterator(); + if (keyIter.hasNext()) { + Object key = keyIter.next(); + if (key == null || !(keyType.isAssignableFrom(key.getClass()))) { + throw new DataRetrievalFailureException( + "The generated key type is not supported. " + + "Unable to cast [" + (key != null ? key.getClass().getName() : null) + + "] to [" + keyType.getName() + "]."); + } + return keyType.cast(key); + } + else { + throw new DataRetrievalFailureException("Unable to retrieve the generated key. " + + "Check that the table has an identity column enabled."); + } + } + + @Override + @Nullable + public Map getKeys() throws InvalidDataAccessApiUsageException { + if (this.keyList.isEmpty()) { + return null; + } + if (this.keyList.size() > 1) { + throw new InvalidDataAccessApiUsageException( + "The getKeys method should only be used when keys for a single row are returned. " + + "The current key list contains keys for multiple rows: " + this.keyList); + } + return this.keyList.get(0); + } + + @Override + public List> getKeyList() { + return this.keyList; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcAccessor.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcAccessor.java new file mode 100644 index 0000000..070ada3 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcAccessor.java @@ -0,0 +1,181 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.SpringProperties; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Base class for {@link org.springframework.jdbc.core.JdbcTemplate} and + * other JDBC-accessing DAO helpers, defining common properties such as + * DataSource and exception translator. + * + *

    Not intended to be used directly. + * See {@link org.springframework.jdbc.core.JdbcTemplate}. + * + * @author Juergen Hoeller + * @author Sebastien Deleuze + * @since 28.11.2003 + * @see org.springframework.jdbc.core.JdbcTemplate + */ +public abstract class JdbcAccessor implements InitializingBean { + + /** + * Boolean flag controlled by a {@code spring.xml.ignore} system property that instructs Spring to + * ignore XML, i.e. to not initialize the XML-related infrastructure. + *

    The default is "false". + */ + private static final boolean shouldIgnoreXml = SpringProperties.getFlag("spring.xml.ignore"); + + /** Logger available to subclasses. */ + protected final Log logger = LogFactory.getLog(getClass()); + + @Nullable + private DataSource dataSource; + + @Nullable + private volatile SQLExceptionTranslator exceptionTranslator; + + private boolean lazyInit = true; + + + /** + * Set the JDBC DataSource to obtain connections from. + */ + public void setDataSource(@Nullable DataSource dataSource) { + this.dataSource = dataSource; + } + + /** + * Return the DataSource used by this template. + */ + @Nullable + public DataSource getDataSource() { + return this.dataSource; + } + + /** + * Obtain the DataSource for actual use. + * @return the DataSource (never {@code null}) + * @throws IllegalStateException in case of no DataSource set + * @since 5.0 + */ + protected DataSource obtainDataSource() { + DataSource dataSource = getDataSource(); + Assert.state(dataSource != null, "No DataSource set"); + return dataSource; + } + + /** + * Specify the database product name for the DataSource that this accessor uses. + * This allows to initialize an SQLErrorCodeSQLExceptionTranslator without + * obtaining a Connection from the DataSource to get the meta-data. + * @param dbName the database product name that identifies the error codes entry + * @see SQLErrorCodeSQLExceptionTranslator#setDatabaseProductName + * @see java.sql.DatabaseMetaData#getDatabaseProductName() + */ + public void setDatabaseProductName(String dbName) { + if (!shouldIgnoreXml) { + this.exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dbName); + } + } + + /** + * Set the exception translator for this instance. + *

    If no custom translator is provided, a default + * {@link SQLErrorCodeSQLExceptionTranslator} is used + * which examines the SQLException's vendor-specific error code. + * @see org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator + * @see org.springframework.jdbc.support.SQLStateSQLExceptionTranslator + */ + public void setExceptionTranslator(SQLExceptionTranslator exceptionTranslator) { + this.exceptionTranslator = exceptionTranslator; + } + + /** + * Return the exception translator for this instance. + *

    Creates a default {@link SQLErrorCodeSQLExceptionTranslator} + * for the specified DataSource if none set, or a + * {@link SQLStateSQLExceptionTranslator} in case of no DataSource. + * @see #getDataSource() + */ + public SQLExceptionTranslator getExceptionTranslator() { + SQLExceptionTranslator exceptionTranslator = this.exceptionTranslator; + if (exceptionTranslator != null) { + return exceptionTranslator; + } + synchronized (this) { + exceptionTranslator = this.exceptionTranslator; + if (exceptionTranslator == null) { + DataSource dataSource = getDataSource(); + if (shouldIgnoreXml) { + exceptionTranslator = new SQLExceptionSubclassTranslator(); + } + else if (dataSource != null) { + exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource); + } + else { + exceptionTranslator = new SQLStateSQLExceptionTranslator(); + } + this.exceptionTranslator = exceptionTranslator; + } + return exceptionTranslator; + } + } + + /** + * Set whether to lazily initialize the SQLExceptionTranslator for this accessor, + * on first encounter of an SQLException. Default is "true"; can be switched to + * "false" for initialization on startup. + *

    Early initialization just applies if {@code afterPropertiesSet()} is called. + * @see #getExceptionTranslator() + * @see #afterPropertiesSet() + */ + public void setLazyInit(boolean lazyInit) { + this.lazyInit = lazyInit; + } + + /** + * Return whether to lazily initialize the SQLExceptionTranslator for this accessor. + * @see #getExceptionTranslator() + */ + public boolean isLazyInit() { + return this.lazyInit; + } + + /** + * Eagerly initialize the exception translator, if demanded, + * creating a default one for the specified DataSource if none set. + */ + @Override + public void afterPropertiesSet() { + if (getDataSource() == null) { + throw new IllegalArgumentException("Property 'dataSource' is required"); + } + if (!isLazyInit()) { + getExceptionTranslator(); + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcTransactionManager.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcTransactionManager.java new file mode 100644 index 0000000..b1cab0b --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcTransactionManager.java @@ -0,0 +1,191 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.springframework.core.SpringProperties; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.lang.Nullable; + +/** + * {@link JdbcAccessor}-aligned subclass of the plain {@link DataSourceTransactionManager}, + * adding common JDBC exception translation for the commit and rollback step. + * Typically used in combination with {@link org.springframework.jdbc.core.JdbcTemplate} + * which applies the same {@link SQLExceptionTranslator} infrastructure by default. + * + *

    Exception translation is specifically relevant for commit steps in serializable + * transactions (e.g. on Postgres) where concurrency failures may occur late on commit. + * This allows for throwing {@link org.springframework.dao.ConcurrencyFailureException} to + * callers instead of {@link org.springframework.transaction.TransactionSystemException}. + * + *

    Analogous to {@code HibernateTransactionManager} and {@code JpaTransactionManager}, + * this transaction manager may throw {@link DataAccessException} from {@link #commit} + * and possibly also from {@link #rollback}. Calling code should be prepared for handling + * such exceptions next to {@link org.springframework.transaction.TransactionException}, + * which is generally sensible since {@code TransactionSynchronization} implementations + * may also throw such exceptions in their {@code flush} and {@code beforeCommit} phases. + * + * @author Juergen Hoeller + * @author Sebastien Deleuze + * @since 5.3 + * @see DataSourceTransactionManager + * @see #setDataSource + * @see #setExceptionTranslator + */ +@SuppressWarnings("serial") +public class JdbcTransactionManager extends DataSourceTransactionManager { + + /** + * Boolean flag controlled by a {@code spring.xml.ignore} system property that instructs Spring to + * ignore XML, i.e. to not initialize the XML-related infrastructure. + *

    The default is "false". + */ + private static final boolean shouldIgnoreXml = SpringProperties.getFlag("spring.xml.ignore"); + + + @Nullable + private volatile SQLExceptionTranslator exceptionTranslator; + + private boolean lazyInit = true; + + + /** + * Create a new JdbcTransactionManager instance. + * A DataSource has to be set to be able to use it. + * @see #setDataSource + */ + public JdbcTransactionManager() { + super(); + } + + /** + * Create a new JdbcTransactionManager instance. + * @param dataSource the JDBC DataSource to manage transactions for + */ + public JdbcTransactionManager(DataSource dataSource) { + this(); + setDataSource(dataSource); + afterPropertiesSet(); + } + + + /** + * Specify the database product name for the DataSource that this transaction manager + * uses. This allows to initialize an SQLErrorCodeSQLExceptionTranslator without + * obtaining a Connection from the DataSource to get the meta-data. + * @param dbName the database product name that identifies the error codes entry + * @see JdbcAccessor#setDatabaseProductName + * @see SQLErrorCodeSQLExceptionTranslator#setDatabaseProductName + * @see java.sql.DatabaseMetaData#getDatabaseProductName() + */ + public void setDatabaseProductName(String dbName) { + if (!shouldIgnoreXml) { + this.exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dbName); + } + } + + /** + * Set the exception translator for this instance. + *

    If no custom translator is provided, a default + * {@link SQLErrorCodeSQLExceptionTranslator} is used + * which examines the SQLException's vendor-specific error code. + * @see JdbcAccessor#setExceptionTranslator + * @see org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator + */ + public void setExceptionTranslator(SQLExceptionTranslator exceptionTranslator) { + this.exceptionTranslator = exceptionTranslator; + } + + /** + * Return the exception translator for this instance. + *

    Creates a default {@link SQLErrorCodeSQLExceptionTranslator} + * for the specified DataSource if none set. + * @see #getDataSource() + */ + public SQLExceptionTranslator getExceptionTranslator() { + SQLExceptionTranslator exceptionTranslator = this.exceptionTranslator; + if (exceptionTranslator != null) { + return exceptionTranslator; + } + synchronized (this) { + exceptionTranslator = this.exceptionTranslator; + if (exceptionTranslator == null) { + if (shouldIgnoreXml) { + exceptionTranslator = new SQLExceptionSubclassTranslator(); + } + else { + exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(obtainDataSource()); + } + this.exceptionTranslator = exceptionTranslator; + } + return exceptionTranslator; + } + } + + /** + * Set whether to lazily initialize the SQLExceptionTranslator for this transaction manager, + * on first encounter of an SQLException. Default is "true"; can be switched to + * "false" for initialization on startup. + *

    Early initialization just applies if {@code afterPropertiesSet()} is called. + * @see #getExceptionTranslator() + * @see #afterPropertiesSet() + */ + public void setLazyInit(boolean lazyInit) { + this.lazyInit = lazyInit; + } + + /** + * Return whether to lazily initialize the SQLExceptionTranslator for this transaction manager. + * @see #getExceptionTranslator() + */ + public boolean isLazyInit() { + return this.lazyInit; + } + + /** + * Eagerly initialize the exception translator, if demanded, + * creating a default one for the specified DataSource if none set. + */ + @Override + public void afterPropertiesSet() { + super.afterPropertiesSet(); + if (!isLazyInit()) { + getExceptionTranslator(); + } + } + + + /** + * This implementation attempts to use the {@link SQLExceptionTranslator}, + * falling back to a {@link org.springframework.transaction.TransactionSystemException}. + * @see #getExceptionTranslator() + * @see DataSourceTransactionManager#translateException + */ + @Override + protected RuntimeException translateException(String task, SQLException ex) { + DataAccessException dae = getExceptionTranslator().translate(task, null, ex); + if (dae != null) { + return dae; + } + return super.translateException(task, ex); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java new file mode 100644 index 0000000..6fe7e9e --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java @@ -0,0 +1,551 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.math.BigDecimal; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.Statement; +import java.sql.Types; +import java.util.HashMap; +import java.util.Map; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.jdbc.CannotGetJdbcConnectionException; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.NumberUtils; +import org.springframework.util.StringUtils; + +/** + * Generic utility methods for working with JDBC. Mainly for internal use + * within the framework, but also useful for custom JDBC access code. + * + * @author Thomas Risberg + * @author Juergen Hoeller + */ +public abstract class JdbcUtils { + + /** + * Constant that indicates an unknown (or unspecified) SQL type. + * @see java.sql.Types + */ + public static final int TYPE_UNKNOWN = Integer.MIN_VALUE; + + private static final Log logger = LogFactory.getLog(JdbcUtils.class); + + private static final Map typeNames = new HashMap<>(); + + static { + try { + for (Field field : Types.class.getFields()) { + typeNames.put((Integer) field.get(null), field.getName()); + } + } + catch (Exception ex) { + throw new IllegalStateException("Failed to resolve JDBC Types constants", ex); + } + } + + + /** + * Close the given JDBC Connection and ignore any thrown exception. + * This is useful for typical finally blocks in manual JDBC code. + * @param con the JDBC Connection to close (may be {@code null}) + */ + public static void closeConnection(@Nullable Connection con) { + if (con != null) { + try { + con.close(); + } + catch (SQLException ex) { + logger.debug("Could not close JDBC Connection", ex); + } + catch (Throwable ex) { + // We don't trust the JDBC driver: It might throw RuntimeException or Error. + logger.debug("Unexpected exception on closing JDBC Connection", ex); + } + } + } + + /** + * Close the given JDBC Statement and ignore any thrown exception. + * This is useful for typical finally blocks in manual JDBC code. + * @param stmt the JDBC Statement to close (may be {@code null}) + */ + public static void closeStatement(@Nullable Statement stmt) { + if (stmt != null) { + try { + stmt.close(); + } + catch (SQLException ex) { + logger.trace("Could not close JDBC Statement", ex); + } + catch (Throwable ex) { + // We don't trust the JDBC driver: It might throw RuntimeException or Error. + logger.trace("Unexpected exception on closing JDBC Statement", ex); + } + } + } + + /** + * Close the given JDBC ResultSet and ignore any thrown exception. + * This is useful for typical finally blocks in manual JDBC code. + * @param rs the JDBC ResultSet to close (may be {@code null}) + */ + public static void closeResultSet(@Nullable ResultSet rs) { + if (rs != null) { + try { + rs.close(); + } + catch (SQLException ex) { + logger.trace("Could not close JDBC ResultSet", ex); + } + catch (Throwable ex) { + // We don't trust the JDBC driver: It might throw RuntimeException or Error. + logger.trace("Unexpected exception on closing JDBC ResultSet", ex); + } + } + } + + /** + * Retrieve a JDBC column value from a ResultSet, using the specified value type. + *

    Uses the specifically typed ResultSet accessor methods, falling back to + * {@link #getResultSetValue(java.sql.ResultSet, int)} for unknown types. + *

    Note that the returned value may not be assignable to the specified + * required type, in case of an unknown type. Calling code needs to deal + * with this case appropriately, e.g. throwing a corresponding exception. + * @param rs is the ResultSet holding the data + * @param index is the column index + * @param requiredType the required value type (may be {@code null}) + * @return the value object (possibly not of the specified required type, + * with further conversion steps necessary) + * @throws SQLException if thrown by the JDBC API + * @see #getResultSetValue(ResultSet, int) + */ + @Nullable + public static Object getResultSetValue(ResultSet rs, int index, @Nullable Class requiredType) throws SQLException { + if (requiredType == null) { + return getResultSetValue(rs, index); + } + + Object value; + + // Explicitly extract typed value, as far as possible. + if (String.class == requiredType) { + return rs.getString(index); + } + else if (boolean.class == requiredType || Boolean.class == requiredType) { + value = rs.getBoolean(index); + } + else if (byte.class == requiredType || Byte.class == requiredType) { + value = rs.getByte(index); + } + else if (short.class == requiredType || Short.class == requiredType) { + value = rs.getShort(index); + } + else if (int.class == requiredType || Integer.class == requiredType) { + value = rs.getInt(index); + } + else if (long.class == requiredType || Long.class == requiredType) { + value = rs.getLong(index); + } + else if (float.class == requiredType || Float.class == requiredType) { + value = rs.getFloat(index); + } + else if (double.class == requiredType || Double.class == requiredType || + Number.class == requiredType) { + value = rs.getDouble(index); + } + else if (BigDecimal.class == requiredType) { + return rs.getBigDecimal(index); + } + else if (java.sql.Date.class == requiredType) { + return rs.getDate(index); + } + else if (java.sql.Time.class == requiredType) { + return rs.getTime(index); + } + else if (java.sql.Timestamp.class == requiredType || java.util.Date.class == requiredType) { + return rs.getTimestamp(index); + } + else if (byte[].class == requiredType) { + return rs.getBytes(index); + } + else if (Blob.class == requiredType) { + return rs.getBlob(index); + } + else if (Clob.class == requiredType) { + return rs.getClob(index); + } + else if (requiredType.isEnum()) { + // Enums can either be represented through a String or an enum index value: + // leave enum type conversion up to the caller (e.g. a ConversionService) + // but make sure that we return nothing other than a String or an Integer. + Object obj = rs.getObject(index); + if (obj instanceof String) { + return obj; + } + else if (obj instanceof Number) { + // Defensively convert any Number to an Integer (as needed by our + // ConversionService's IntegerToEnumConverterFactory) for use as index + return NumberUtils.convertNumberToTargetClass((Number) obj, Integer.class); + } + else { + // e.g. on Postgres: getObject returns a PGObject but we need a String + return rs.getString(index); + } + } + + else { + // Some unknown type desired -> rely on getObject. + try { + return rs.getObject(index, requiredType); + } + catch (AbstractMethodError err) { + logger.debug("JDBC driver does not implement JDBC 4.1 'getObject(int, Class)' method", err); + } + catch (SQLFeatureNotSupportedException ex) { + logger.debug("JDBC driver does not support JDBC 4.1 'getObject(int, Class)' method", ex); + } + catch (SQLException ex) { + logger.debug("JDBC driver has limited support for JDBC 4.1 'getObject(int, Class)' method", ex); + } + + // Corresponding SQL types for JSR-310 / Joda-Time types, left up + // to the caller to convert them (e.g. through a ConversionService). + String typeName = requiredType.getSimpleName(); + if ("LocalDate".equals(typeName)) { + return rs.getDate(index); + } + else if ("LocalTime".equals(typeName)) { + return rs.getTime(index); + } + else if ("LocalDateTime".equals(typeName)) { + return rs.getTimestamp(index); + } + + // Fall back to getObject without type specification, again + // left up to the caller to convert the value if necessary. + return getResultSetValue(rs, index); + } + + // Perform was-null check if necessary (for results that the JDBC driver returns as primitives). + return (rs.wasNull() ? null : value); + } + + /** + * Retrieve a JDBC column value from a ResultSet, using the most appropriate + * value type. The returned value should be a detached value object, not having + * any ties to the active ResultSet: in particular, it should not be a Blob or + * Clob object but rather a byte array or String representation, respectively. + *

    Uses the {@code getObject(index)} method, but includes additional "hacks" + * to get around Oracle 10g returning a non-standard object for its TIMESTAMP + * datatype and a {@code java.sql.Date} for DATE columns leaving out the + * time portion: These columns will explicitly be extracted as standard + * {@code java.sql.Timestamp} object. + * @param rs is the ResultSet holding the data + * @param index is the column index + * @return the value object + * @throws SQLException if thrown by the JDBC API + * @see java.sql.Blob + * @see java.sql.Clob + * @see java.sql.Timestamp + */ + @Nullable + public static Object getResultSetValue(ResultSet rs, int index) throws SQLException { + Object obj = rs.getObject(index); + String className = null; + if (obj != null) { + className = obj.getClass().getName(); + } + if (obj instanceof Blob) { + Blob blob = (Blob) obj; + obj = blob.getBytes(1, (int) blob.length()); + } + else if (obj instanceof Clob) { + Clob clob = (Clob) obj; + obj = clob.getSubString(1, (int) clob.length()); + } + else if ("oracle.sql.TIMESTAMP".equals(className) || "oracle.sql.TIMESTAMPTZ".equals(className)) { + obj = rs.getTimestamp(index); + } + else if (className != null && className.startsWith("oracle.sql.DATE")) { + String metaDataClassName = rs.getMetaData().getColumnClassName(index); + if ("java.sql.Timestamp".equals(metaDataClassName) || "oracle.sql.TIMESTAMP".equals(metaDataClassName)) { + obj = rs.getTimestamp(index); + } + else { + obj = rs.getDate(index); + } + } + else if (obj instanceof java.sql.Date) { + if ("java.sql.Timestamp".equals(rs.getMetaData().getColumnClassName(index))) { + obj = rs.getTimestamp(index); + } + } + return obj; + } + + /** + * Extract database meta-data via the given DatabaseMetaDataCallback. + *

    This method will open a connection to the database and retrieve its meta-data. + * Since this method is called before the exception translation feature is configured + * for a DataSource, this method can not rely on SQLException translation itself. + *

    Any exceptions will be wrapped in a MetaDataAccessException. This is a checked + * exception and any calling code should catch and handle this exception. You can just + * log the error and hope for the best, but there is probably a more serious error that + * will reappear when you try to access the database again. + * @param dataSource the DataSource to extract meta-data for + * @param action callback that will do the actual work + * @return object containing the extracted information, as returned by + * the DatabaseMetaDataCallback's {@code processMetaData} method + * @throws MetaDataAccessException if meta-data access failed + * @see java.sql.DatabaseMetaData + */ + public static T extractDatabaseMetaData(DataSource dataSource, DatabaseMetaDataCallback action) + throws MetaDataAccessException { + + Connection con = null; + try { + con = DataSourceUtils.getConnection(dataSource); + DatabaseMetaData metaData; + try { + metaData = con.getMetaData(); + } + catch (SQLException ex) { + if (DataSourceUtils.isConnectionTransactional(con, dataSource)) { + // Probably a closed thread-bound Connection - retry against fresh Connection + DataSourceUtils.releaseConnection(con, dataSource); + con = null; + logger.debug("Failed to obtain DatabaseMetaData from transactional Connection - " + + "retrying against fresh Connection", ex); + con = dataSource.getConnection(); + metaData = con.getMetaData(); + } + else { + throw ex; + } + } + if (metaData == null) { + // should only happen in test environments + throw new MetaDataAccessException("DatabaseMetaData returned by Connection [" + con + "] was null"); + } + return action.processMetaData(metaData); + } + catch (CannotGetJdbcConnectionException ex) { + throw new MetaDataAccessException("Could not get Connection for extracting meta-data", ex); + } + catch (SQLException ex) { + throw new MetaDataAccessException("Error while extracting DatabaseMetaData", ex); + } + catch (AbstractMethodError err) { + throw new MetaDataAccessException( + "JDBC DatabaseMetaData method not implemented by JDBC driver - upgrade your driver", err); + } + finally { + DataSourceUtils.releaseConnection(con, dataSource); + } + } + + /** + * Call the specified method on DatabaseMetaData for the given DataSource, + * and extract the invocation result. + * @param dataSource the DataSource to extract meta-data for + * @param metaDataMethodName the name of the DatabaseMetaData method to call + * @return the object returned by the specified DatabaseMetaData method + * @throws MetaDataAccessException if we couldn't access the DatabaseMetaData + * or failed to invoke the specified method + * @see java.sql.DatabaseMetaData + * @deprecated as of 5.2.9, in favor of + * {@link #extractDatabaseMetaData(DataSource, DatabaseMetaDataCallback)} + * with a lambda expression or method reference and a generically typed result + */ + @Deprecated + @SuppressWarnings("unchecked") + public static T extractDatabaseMetaData(DataSource dataSource, final String metaDataMethodName) + throws MetaDataAccessException { + + return (T) extractDatabaseMetaData(dataSource, + dbmd -> { + try { + return DatabaseMetaData.class.getMethod(metaDataMethodName).invoke(dbmd); + } + catch (NoSuchMethodException ex) { + throw new MetaDataAccessException("No method named '" + metaDataMethodName + + "' found on DatabaseMetaData instance [" + dbmd + "]", ex); + } + catch (IllegalAccessException ex) { + throw new MetaDataAccessException( + "Could not access DatabaseMetaData method '" + metaDataMethodName + "'", ex); + } + catch (InvocationTargetException ex) { + if (ex.getTargetException() instanceof SQLException) { + throw (SQLException) ex.getTargetException(); + } + throw new MetaDataAccessException( + "Invocation of DatabaseMetaData method '" + metaDataMethodName + "' failed", ex); + } + }); + } + + /** + * Return whether the given JDBC driver supports JDBC 2.0 batch updates. + *

    Typically invoked right before execution of a given set of statements: + * to decide whether the set of SQL statements should be executed through + * the JDBC 2.0 batch mechanism or simply in a traditional one-by-one fashion. + *

    Logs a warning if the "supportsBatchUpdates" methods throws an exception + * and simply returns {@code false} in that case. + * @param con the Connection to check + * @return whether JDBC 2.0 batch updates are supported + * @see java.sql.DatabaseMetaData#supportsBatchUpdates() + */ + public static boolean supportsBatchUpdates(Connection con) { + try { + DatabaseMetaData dbmd = con.getMetaData(); + if (dbmd != null) { + if (dbmd.supportsBatchUpdates()) { + logger.debug("JDBC driver supports batch updates"); + return true; + } + else { + logger.debug("JDBC driver does not support batch updates"); + } + } + } + catch (SQLException ex) { + logger.debug("JDBC driver 'supportsBatchUpdates' method threw exception", ex); + } + return false; + } + + /** + * Extract a common name for the target database in use even if + * various drivers/platforms provide varying names at runtime. + * @param source the name as provided in database meta-data + * @return the common name to be used (e.g. "DB2" or "Sybase") + */ + @Nullable + public static String commonDatabaseName(@Nullable String source) { + String name = source; + if (source != null && source.startsWith("DB2")) { + name = "DB2"; + } + else if ("MariaDB".equals(source)) { + name = "MySQL"; + } + else if ("Sybase SQL Server".equals(source) || + "Adaptive Server Enterprise".equals(source) || + "ASE".equals(source) || + "sql server".equalsIgnoreCase(source) ) { + name = "Sybase"; + } + return name; + } + + /** + * Check whether the given SQL type is numeric. + * @param sqlType the SQL type to be checked + * @return whether the type is numeric + */ + public static boolean isNumeric(int sqlType) { + return (Types.BIT == sqlType || Types.BIGINT == sqlType || Types.DECIMAL == sqlType || + Types.DOUBLE == sqlType || Types.FLOAT == sqlType || Types.INTEGER == sqlType || + Types.NUMERIC == sqlType || Types.REAL == sqlType || Types.SMALLINT == sqlType || + Types.TINYINT == sqlType); + } + + /** + * Resolve the standard type name for the given SQL type, if possible. + * @param sqlType the SQL type to resolve + * @return the corresponding constant name in {@link java.sql.Types} + * (e.g. "VARCHAR"/"NUMERIC"), or {@code null} if not resolvable + * @since 5.2 + */ + @Nullable + public static String resolveTypeName(int sqlType) { + return typeNames.get(sqlType); + } + + /** + * Determine the column name to use. The column name is determined based on a + * lookup using ResultSetMetaData. + *

    This method implementation takes into account recent clarifications + * expressed in the JDBC 4.0 specification: + *

    columnLabel - the label for the column specified with the SQL AS clause. + * If the SQL AS clause was not specified, then the label is the name of the column. + * @param resultSetMetaData the current meta-data to use + * @param columnIndex the index of the column for the look up + * @return the column name to use + * @throws SQLException in case of lookup failure + */ + public static String lookupColumnName(ResultSetMetaData resultSetMetaData, int columnIndex) throws SQLException { + String name = resultSetMetaData.getColumnLabel(columnIndex); + if (!StringUtils.hasLength(name)) { + name = resultSetMetaData.getColumnName(columnIndex); + } + return name; + } + + /** + * Convert a column name with underscores to the corresponding property name using "camel case". + * A name like "customer_number" would match a "customerNumber" property name. + * @param name the column name to be converted + * @return the name using "camel case" + */ + public static String convertUnderscoreNameToPropertyName(@Nullable String name) { + StringBuilder result = new StringBuilder(); + boolean nextIsUpper = false; + if (name != null && name.length() > 0) { + if (name.length() > 1 && name.charAt(1) == '_') { + result.append(Character.toUpperCase(name.charAt(0))); + } + else { + result.append(Character.toLowerCase(name.charAt(0))); + } + for (int i = 1; i < name.length(); i++) { + char c = name.charAt(i); + if (c == '_') { + nextIsUpper = true; + } + else { + if (nextIsUpper) { + result.append(Character.toUpperCase(c)); + nextIsUpper = false; + } + else { + result.append(Character.toLowerCase(c)); + } + } + } + } + return result.toString(); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/KeyHolder.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/KeyHolder.java new file mode 100644 index 0000000..e022341 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/KeyHolder.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import java.util.List; +import java.util.Map; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.lang.Nullable; + +/** + * Interface for retrieving keys, typically used for auto-generated keys + * as potentially returned by JDBC insert statements. + * + *

    Implementations of this interface can hold any number of keys. + * In the general case, the keys are returned as a List containing one Map + * for each row of keys. + * + *

    Most applications only use one key per row and process only one row at a + * time in an insert statement. In these cases, just call {@link #getKey() getKey} + * or {@link #getKeyAs(Class) getKeyAs} to retrieve the key. The value returned + * by {@code getKey} is a {@link Number}, which is the usual type for auto-generated + * keys. For any other auto-generated key type, use {@code getKeyAs} instead. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @author Slawomir Dymitrow + * @since 1.1 + * @see org.springframework.jdbc.core.JdbcTemplate + * @see org.springframework.jdbc.object.SqlUpdate + */ +public interface KeyHolder { + + /** + * Retrieve the first item from the first map, assuming that there is just + * one item and just one map, and that the item is a number. + * This is the typical case: a single, numeric generated key. + *

    Keys are held in a List of Maps, where each item in the list represents + * the keys for each row. If there are multiple columns, then the Map will have + * multiple entries as well. If this method encounters multiple entries in + * either the map or the list meaning that multiple keys were returned, + * then an InvalidDataAccessApiUsageException is thrown. + * @return the generated key as a number + * @throws InvalidDataAccessApiUsageException if multiple keys are encountered + * @see #getKeyAs(Class) + */ + @Nullable + Number getKey() throws InvalidDataAccessApiUsageException; + + /** + * Retrieve the first item from the first map, assuming that there is just + * one item and just one map, and that the item is an instance of specified type. + * This is a common case: a single generated key of the specified type. + *

    Keys are held in a List of Maps, where each item in the list represents + * the keys for each row. If there are multiple columns, then the Map will have + * multiple entries as well. If this method encounters multiple entries in + * either the map or the list meaning that multiple keys were returned, + * then an InvalidDataAccessApiUsageException is thrown. + * @param keyType the type of the auto-generated key + * @return the generated key as an instance of specified type + * @throws InvalidDataAccessApiUsageException if multiple keys are encountered + * @since 5.3 + * @see #getKey() + */ + @Nullable + T getKeyAs(Class keyType) throws InvalidDataAccessApiUsageException; + + /** + * Retrieve the first map of keys. + *

    If there are multiple entries in the list (meaning that multiple rows + * had keys returned), then an InvalidDataAccessApiUsageException is thrown. + * @return the Map of generated keys for a single row + * @throws InvalidDataAccessApiUsageException if keys for multiple rows are encountered + */ + @Nullable + Map getKeys() throws InvalidDataAccessApiUsageException; + + /** + * Return a reference to the List that contains the keys. + *

    Can be used for extracting keys for multiple rows (an unusual case), + * and also for adding new maps of keys. + * @return the List for the generated keys, with each entry representing + * an individual row through a Map of column names and key values + */ + List> getKeyList(); + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/MetaDataAccessException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/MetaDataAccessException.java new file mode 100644 index 0000000..21895c8 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/MetaDataAccessException.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import org.springframework.core.NestedCheckedException; + +/** + * Exception indicating that something went wrong during JDBC meta-data lookup. + * + *

    This is a checked exception since we want it to be caught, logged and + * handled rather than cause the application to fail. Failure to read JDBC + * meta-data is usually not a fatal problem. + * + * @author Thomas Risberg + * @since 1.0.1 + */ +@SuppressWarnings("serial") +public class MetaDataAccessException extends NestedCheckedException { + + /** + * Constructor for MetaDataAccessException. + * @param msg the detail message + */ + public MetaDataAccessException(String msg) { + super(msg); + } + + /** + * Constructor for MetaDataAccessException. + * @param msg the detail message + * @param cause the root cause from the data access API in use + */ + public MetaDataAccessException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java new file mode 100644 index 0000000..f50380f --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java @@ -0,0 +1,413 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import java.lang.reflect.Constructor; +import java.sql.BatchUpdateException; +import java.sql.SQLException; +import java.util.Arrays; + +import javax.sql.DataSource; + +import org.springframework.dao.CannotAcquireLockException; +import org.springframework.dao.CannotSerializeTransactionException; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.DeadlockLoserDataAccessException; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.dao.PermissionDeniedDataAccessException; +import org.springframework.dao.TransientDataAccessResourceException; +import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.jdbc.InvalidResultSetAccessException; +import org.springframework.lang.Nullable; +import org.springframework.util.function.SingletonSupplier; +import org.springframework.util.function.SupplierUtils; + +/** + * Implementation of {@link SQLExceptionTranslator} that analyzes vendor-specific error codes. + * More precise than an implementation based on SQL state, but heavily vendor-specific. + * + *

    This class applies the following matching rules: + *

      + *
    • Try custom translation implemented by any subclass. Note that this class is + * concrete and is typically used itself, in which case this rule doesn't apply. + *
    • Apply error code matching. Error codes are obtained from the SQLErrorCodesFactory + * by default. This factory loads a "sql-error-codes.xml" file from the class path, + * defining error code mappings for database names from database meta-data. + *
    • Fallback to a fallback translator. {@link SQLStateSQLExceptionTranslator} is the + * default fallback translator, analyzing the exception's SQL state only. On Java 6 + * which introduces its own {@code SQLException} subclass hierarchy, we will + * use {@link SQLExceptionSubclassTranslator} by default, which in turns falls back + * to Spring's own SQL state translation when not encountering specific subclasses. + *
    + * + *

    The configuration file named "sql-error-codes.xml" is by default read from + * this package. It can be overridden through a file of the same name in the root + * of the class path (e.g. in the "/WEB-INF/classes" directory), as long as the + * Spring JDBC package is loaded from the same ClassLoader. + * + * @author Rod Johnson + * @author Thomas Risberg + * @author Juergen Hoeller + * @see SQLErrorCodesFactory + * @see SQLStateSQLExceptionTranslator + */ +public class SQLErrorCodeSQLExceptionTranslator extends AbstractFallbackSQLExceptionTranslator { + + private static final int MESSAGE_ONLY_CONSTRUCTOR = 1; + private static final int MESSAGE_THROWABLE_CONSTRUCTOR = 2; + private static final int MESSAGE_SQLEX_CONSTRUCTOR = 3; + private static final int MESSAGE_SQL_THROWABLE_CONSTRUCTOR = 4; + private static final int MESSAGE_SQL_SQLEX_CONSTRUCTOR = 5; + + + /** Error codes used by this translator. */ + @Nullable + private SingletonSupplier sqlErrorCodes; + + + /** + * Constructor for use as a JavaBean. + * The SqlErrorCodes or DataSource property must be set. + */ + public SQLErrorCodeSQLExceptionTranslator() { + setFallbackTranslator(new SQLExceptionSubclassTranslator()); + } + + /** + * Create an SQL error code translator for the given DataSource. + * Invoking this constructor will cause a Connection to be obtained + * from the DataSource to get the meta-data. + * @param dataSource the DataSource to use to find meta-data and establish + * which error codes are usable + * @see SQLErrorCodesFactory + */ + public SQLErrorCodeSQLExceptionTranslator(DataSource dataSource) { + this(); + setDataSource(dataSource); + } + + /** + * Create an SQL error code translator for the given database product name. + * Invoking this constructor will avoid obtaining a Connection from the + * DataSource to get the meta-data. + * @param dbName the database product name that identifies the error codes entry + * @see SQLErrorCodesFactory + * @see java.sql.DatabaseMetaData#getDatabaseProductName() + */ + public SQLErrorCodeSQLExceptionTranslator(String dbName) { + this(); + setDatabaseProductName(dbName); + } + + /** + * Create an SQLErrorCode translator given these error codes. + * Does not require a database meta-data lookup to be performed using a connection. + * @param sec error codes + */ + public SQLErrorCodeSQLExceptionTranslator(SQLErrorCodes sec) { + this(); + this.sqlErrorCodes = SingletonSupplier.of(sec); + } + + + /** + * Set the DataSource for this translator. + *

    Setting this property will cause a Connection to be obtained from + * the DataSource to get the meta-data. + * @param dataSource the DataSource to use to find meta-data and establish + * which error codes are usable + * @see SQLErrorCodesFactory#getErrorCodes(javax.sql.DataSource) + * @see java.sql.DatabaseMetaData#getDatabaseProductName() + */ + public void setDataSource(DataSource dataSource) { + this.sqlErrorCodes = + SingletonSupplier.of(() -> SQLErrorCodesFactory.getInstance().resolveErrorCodes(dataSource)); + this.sqlErrorCodes.get(); // try early initialization - otherwise the supplier will retry later + } + + /** + * Set the database product name for this translator. + *

    Setting this property will avoid obtaining a Connection from the DataSource + * to get the meta-data. + * @param dbName the database product name that identifies the error codes entry + * @see SQLErrorCodesFactory#getErrorCodes(String) + * @see java.sql.DatabaseMetaData#getDatabaseProductName() + */ + public void setDatabaseProductName(String dbName) { + this.sqlErrorCodes = SingletonSupplier.of(SQLErrorCodesFactory.getInstance().getErrorCodes(dbName)); + } + + /** + * Set custom error codes to be used for translation. + * @param sec custom error codes to use + */ + public void setSqlErrorCodes(@Nullable SQLErrorCodes sec) { + this.sqlErrorCodes = SingletonSupplier.ofNullable(sec); + } + + /** + * Return the error codes used by this translator. + * Usually determined via a DataSource. + * @see #setDataSource + */ + @Nullable + public SQLErrorCodes getSqlErrorCodes() { + return SupplierUtils.resolve(this.sqlErrorCodes); + } + + + @Override + @Nullable + protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) { + SQLException sqlEx = ex; + if (sqlEx instanceof BatchUpdateException && sqlEx.getNextException() != null) { + SQLException nestedSqlEx = sqlEx.getNextException(); + if (nestedSqlEx.getErrorCode() > 0 || nestedSqlEx.getSQLState() != null) { + sqlEx = nestedSqlEx; + } + } + + // First, try custom translation from overridden method. + DataAccessException dae = customTranslate(task, sql, sqlEx); + if (dae != null) { + return dae; + } + + // Next, try the custom SQLException translator, if available. + SQLErrorCodes sqlErrorCodes = getSqlErrorCodes(); + if (sqlErrorCodes != null) { + SQLExceptionTranslator customTranslator = sqlErrorCodes.getCustomSqlExceptionTranslator(); + if (customTranslator != null) { + DataAccessException customDex = customTranslator.translate(task, sql, sqlEx); + if (customDex != null) { + return customDex; + } + } + } + + // Check SQLErrorCodes with corresponding error code, if available. + if (sqlErrorCodes != null) { + String errorCode; + if (sqlErrorCodes.isUseSqlStateForTranslation()) { + errorCode = sqlEx.getSQLState(); + } + else { + // Try to find SQLException with actual error code, looping through the causes. + // E.g. applicable to java.sql.DataTruncation as of JDK 1.6. + SQLException current = sqlEx; + while (current.getErrorCode() == 0 && current.getCause() instanceof SQLException) { + current = (SQLException) current.getCause(); + } + errorCode = Integer.toString(current.getErrorCode()); + } + + if (errorCode != null) { + // Look for defined custom translations first. + CustomSQLErrorCodesTranslation[] customTranslations = sqlErrorCodes.getCustomTranslations(); + if (customTranslations != null) { + for (CustomSQLErrorCodesTranslation customTranslation : customTranslations) { + if (Arrays.binarySearch(customTranslation.getErrorCodes(), errorCode) >= 0 && + customTranslation.getExceptionClass() != null) { + DataAccessException customException = createCustomException( + task, sql, sqlEx, customTranslation.getExceptionClass()); + if (customException != null) { + logTranslation(task, sql, sqlEx, true); + return customException; + } + } + } + } + // Next, look for grouped error codes. + if (Arrays.binarySearch(sqlErrorCodes.getBadSqlGrammarCodes(), errorCode) >= 0) { + logTranslation(task, sql, sqlEx, false); + return new BadSqlGrammarException(task, (sql != null ? sql : ""), sqlEx); + } + else if (Arrays.binarySearch(sqlErrorCodes.getInvalidResultSetAccessCodes(), errorCode) >= 0) { + logTranslation(task, sql, sqlEx, false); + return new InvalidResultSetAccessException(task, (sql != null ? sql : ""), sqlEx); + } + else if (Arrays.binarySearch(sqlErrorCodes.getDuplicateKeyCodes(), errorCode) >= 0) { + logTranslation(task, sql, sqlEx, false); + return new DuplicateKeyException(buildMessage(task, sql, sqlEx), sqlEx); + } + else if (Arrays.binarySearch(sqlErrorCodes.getDataIntegrityViolationCodes(), errorCode) >= 0) { + logTranslation(task, sql, sqlEx, false); + return new DataIntegrityViolationException(buildMessage(task, sql, sqlEx), sqlEx); + } + else if (Arrays.binarySearch(sqlErrorCodes.getPermissionDeniedCodes(), errorCode) >= 0) { + logTranslation(task, sql, sqlEx, false); + return new PermissionDeniedDataAccessException(buildMessage(task, sql, sqlEx), sqlEx); + } + else if (Arrays.binarySearch(sqlErrorCodes.getDataAccessResourceFailureCodes(), errorCode) >= 0) { + logTranslation(task, sql, sqlEx, false); + return new DataAccessResourceFailureException(buildMessage(task, sql, sqlEx), sqlEx); + } + else if (Arrays.binarySearch(sqlErrorCodes.getTransientDataAccessResourceCodes(), errorCode) >= 0) { + logTranslation(task, sql, sqlEx, false); + return new TransientDataAccessResourceException(buildMessage(task, sql, sqlEx), sqlEx); + } + else if (Arrays.binarySearch(sqlErrorCodes.getCannotAcquireLockCodes(), errorCode) >= 0) { + logTranslation(task, sql, sqlEx, false); + return new CannotAcquireLockException(buildMessage(task, sql, sqlEx), sqlEx); + } + else if (Arrays.binarySearch(sqlErrorCodes.getDeadlockLoserCodes(), errorCode) >= 0) { + logTranslation(task, sql, sqlEx, false); + return new DeadlockLoserDataAccessException(buildMessage(task, sql, sqlEx), sqlEx); + } + else if (Arrays.binarySearch(sqlErrorCodes.getCannotSerializeTransactionCodes(), errorCode) >= 0) { + logTranslation(task, sql, sqlEx, false); + return new CannotSerializeTransactionException(buildMessage(task, sql, sqlEx), sqlEx); + } + } + } + + // We couldn't identify it more precisely - let's hand it over to the SQLState fallback translator. + if (logger.isDebugEnabled()) { + String codes; + if (sqlErrorCodes != null && sqlErrorCodes.isUseSqlStateForTranslation()) { + codes = "SQL state '" + sqlEx.getSQLState() + "', error code '" + sqlEx.getErrorCode(); + } + else { + codes = "Error code '" + sqlEx.getErrorCode() + "'"; + } + logger.debug("Unable to translate SQLException with " + codes + ", will now try the fallback translator"); + } + + return null; + } + + /** + * Subclasses can override this method to attempt a custom mapping from + * {@link SQLException} to {@link DataAccessException}. + * @param task readable text describing the task being attempted + * @param sql the SQL query or update that caused the problem (may be {@code null}) + * @param sqlEx the offending SQLException + * @return {@code null} if no custom translation applies, otherwise a {@link DataAccessException} + * resulting from custom translation. This exception should include the {@code sqlEx} parameter + * as a nested root cause. This implementation always returns {@code null}, meaning that the + * translator always falls back to the default error codes. + */ + @Nullable + protected DataAccessException customTranslate(String task, @Nullable String sql, SQLException sqlEx) { + return null; + } + + /** + * Create a custom {@link DataAccessException}, based on a given exception + * class from a {@link CustomSQLErrorCodesTranslation} definition. + * @param task readable text describing the task being attempted + * @param sql the SQL query or update that caused the problem (may be {@code null}) + * @param sqlEx the offending SQLException + * @param exceptionClass the exception class to use, as defined in the + * {@link CustomSQLErrorCodesTranslation} definition + * @return {@code null} if the custom exception could not be created, otherwise + * the resulting {@link DataAccessException}. This exception should include the + * {@code sqlEx} parameter as a nested root cause. + * @see CustomSQLErrorCodesTranslation#setExceptionClass + */ + @Nullable + protected DataAccessException createCustomException( + String task, @Nullable String sql, SQLException sqlEx, Class exceptionClass) { + + // Find appropriate constructor for the given exception class + try { + int constructorType = 0; + Constructor[] constructors = exceptionClass.getConstructors(); + for (Constructor constructor : constructors) { + Class[] parameterTypes = constructor.getParameterTypes(); + if (parameterTypes.length == 1 && String.class == parameterTypes[0] && + constructorType < MESSAGE_ONLY_CONSTRUCTOR) { + constructorType = MESSAGE_ONLY_CONSTRUCTOR; + } + if (parameterTypes.length == 2 && String.class == parameterTypes[0] && + Throwable.class == parameterTypes[1] && + constructorType < MESSAGE_THROWABLE_CONSTRUCTOR) { + constructorType = MESSAGE_THROWABLE_CONSTRUCTOR; + } + if (parameterTypes.length == 2 && String.class == parameterTypes[0] && + SQLException.class == parameterTypes[1] && + constructorType < MESSAGE_SQLEX_CONSTRUCTOR) { + constructorType = MESSAGE_SQLEX_CONSTRUCTOR; + } + if (parameterTypes.length == 3 && String.class == parameterTypes[0] && + String.class == parameterTypes[1] && Throwable.class == parameterTypes[2] && + constructorType < MESSAGE_SQL_THROWABLE_CONSTRUCTOR) { + constructorType = MESSAGE_SQL_THROWABLE_CONSTRUCTOR; + } + if (parameterTypes.length == 3 && String.class == parameterTypes[0] && + String.class == parameterTypes[1] && SQLException.class == parameterTypes[2] && + constructorType < MESSAGE_SQL_SQLEX_CONSTRUCTOR) { + constructorType = MESSAGE_SQL_SQLEX_CONSTRUCTOR; + } + } + + // invoke constructor + Constructor exceptionConstructor; + switch (constructorType) { + case MESSAGE_SQL_SQLEX_CONSTRUCTOR: + Class[] messageAndSqlAndSqlExArgsClass = new Class[] {String.class, String.class, SQLException.class}; + Object[] messageAndSqlAndSqlExArgs = new Object[] {task, sql, sqlEx}; + exceptionConstructor = exceptionClass.getConstructor(messageAndSqlAndSqlExArgsClass); + return (DataAccessException) exceptionConstructor.newInstance(messageAndSqlAndSqlExArgs); + case MESSAGE_SQL_THROWABLE_CONSTRUCTOR: + Class[] messageAndSqlAndThrowableArgsClass = new Class[] {String.class, String.class, Throwable.class}; + Object[] messageAndSqlAndThrowableArgs = new Object[] {task, sql, sqlEx}; + exceptionConstructor = exceptionClass.getConstructor(messageAndSqlAndThrowableArgsClass); + return (DataAccessException) exceptionConstructor.newInstance(messageAndSqlAndThrowableArgs); + case MESSAGE_SQLEX_CONSTRUCTOR: + Class[] messageAndSqlExArgsClass = new Class[] {String.class, SQLException.class}; + Object[] messageAndSqlExArgs = new Object[] {task + ": " + sqlEx.getMessage(), sqlEx}; + exceptionConstructor = exceptionClass.getConstructor(messageAndSqlExArgsClass); + return (DataAccessException) exceptionConstructor.newInstance(messageAndSqlExArgs); + case MESSAGE_THROWABLE_CONSTRUCTOR: + Class[] messageAndThrowableArgsClass = new Class[] {String.class, Throwable.class}; + Object[] messageAndThrowableArgs = new Object[] {task + ": " + sqlEx.getMessage(), sqlEx}; + exceptionConstructor = exceptionClass.getConstructor(messageAndThrowableArgsClass); + return (DataAccessException)exceptionConstructor.newInstance(messageAndThrowableArgs); + case MESSAGE_ONLY_CONSTRUCTOR: + Class[] messageOnlyArgsClass = new Class[] {String.class}; + Object[] messageOnlyArgs = new Object[] {task + ": " + sqlEx.getMessage()}; + exceptionConstructor = exceptionClass.getConstructor(messageOnlyArgsClass); + return (DataAccessException) exceptionConstructor.newInstance(messageOnlyArgs); + default: + if (logger.isWarnEnabled()) { + logger.warn("Unable to find appropriate constructor of custom exception class [" + + exceptionClass.getName() + "]"); + } + return null; + } + } + catch (Throwable ex) { + if (logger.isWarnEnabled()) { + logger.warn("Unable to instantiate custom exception class [" + exceptionClass.getName() + "]", ex); + } + return null; + } + } + + private void logTranslation(String task, @Nullable String sql, SQLException sqlEx, boolean custom) { + if (logger.isDebugEnabled()) { + String intro = custom ? "Custom translation of" : "Translating"; + logger.debug(intro + " SQLException with SQL state '" + sqlEx.getSQLState() + + "', error code '" + sqlEx.getErrorCode() + "', message [" + sqlEx.getMessage() + "]" + + (sql != null ? "; SQL was [" + sql + "]": "") + " for task [" + task + "]"); + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodes.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodes.java new file mode 100644 index 0000000..5d4498c --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodes.java @@ -0,0 +1,222 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * JavaBean for holding JDBC error codes for a particular database. + * Instances of this class are normally loaded through a bean factory. + * + *

    Used by Spring's {@link SQLErrorCodeSQLExceptionTranslator}. + * The file "sql-error-codes.xml" in this package contains default + * {@code SQLErrorCodes} instances for various databases. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @see SQLErrorCodesFactory + * @see SQLErrorCodeSQLExceptionTranslator + */ +public class SQLErrorCodes { + + @Nullable + private String[] databaseProductNames; + + private boolean useSqlStateForTranslation = false; + + private String[] badSqlGrammarCodes = new String[0]; + + private String[] invalidResultSetAccessCodes = new String[0]; + + private String[] duplicateKeyCodes = new String[0]; + + private String[] dataIntegrityViolationCodes = new String[0]; + + private String[] permissionDeniedCodes = new String[0]; + + private String[] dataAccessResourceFailureCodes = new String[0]; + + private String[] transientDataAccessResourceCodes = new String[0]; + + private String[] cannotAcquireLockCodes = new String[0]; + + private String[] deadlockLoserCodes = new String[0]; + + private String[] cannotSerializeTransactionCodes = new String[0]; + + @Nullable + private CustomSQLErrorCodesTranslation[] customTranslations; + + @Nullable + private SQLExceptionTranslator customSqlExceptionTranslator; + + + /** + * Set this property if the database name contains spaces, + * in which case we can not use the bean name for lookup. + */ + public void setDatabaseProductName(@Nullable String databaseProductName) { + this.databaseProductNames = new String[] {databaseProductName}; + } + + @Nullable + public String getDatabaseProductName() { + return (this.databaseProductNames != null && this.databaseProductNames.length > 0 ? + this.databaseProductNames[0] : null); + } + + /** + * Set this property to specify multiple database names that contains spaces, + * in which case we can not use bean names for lookup. + */ + public void setDatabaseProductNames(@Nullable String... databaseProductNames) { + this.databaseProductNames = databaseProductNames; + } + + @Nullable + public String[] getDatabaseProductNames() { + return this.databaseProductNames; + } + + /** + * Set this property to true for databases that do not provide an error code + * but that do provide SQL State (this includes PostgreSQL). + */ + public void setUseSqlStateForTranslation(boolean useStateCodeForTranslation) { + this.useSqlStateForTranslation = useStateCodeForTranslation; + } + + public boolean isUseSqlStateForTranslation() { + return this.useSqlStateForTranslation; + } + + public void setBadSqlGrammarCodes(String... badSqlGrammarCodes) { + this.badSqlGrammarCodes = StringUtils.sortStringArray(badSqlGrammarCodes); + } + + public String[] getBadSqlGrammarCodes() { + return this.badSqlGrammarCodes; + } + + public void setInvalidResultSetAccessCodes(String... invalidResultSetAccessCodes) { + this.invalidResultSetAccessCodes = StringUtils.sortStringArray(invalidResultSetAccessCodes); + } + + public String[] getInvalidResultSetAccessCodes() { + return this.invalidResultSetAccessCodes; + } + + public String[] getDuplicateKeyCodes() { + return this.duplicateKeyCodes; + } + + public void setDuplicateKeyCodes(String... duplicateKeyCodes) { + this.duplicateKeyCodes = duplicateKeyCodes; + } + + public void setDataIntegrityViolationCodes(String... dataIntegrityViolationCodes) { + this.dataIntegrityViolationCodes = StringUtils.sortStringArray(dataIntegrityViolationCodes); + } + + public String[] getDataIntegrityViolationCodes() { + return this.dataIntegrityViolationCodes; + } + + public void setPermissionDeniedCodes(String... permissionDeniedCodes) { + this.permissionDeniedCodes = StringUtils.sortStringArray(permissionDeniedCodes); + } + + public String[] getPermissionDeniedCodes() { + return this.permissionDeniedCodes; + } + + public void setDataAccessResourceFailureCodes(String... dataAccessResourceFailureCodes) { + this.dataAccessResourceFailureCodes = StringUtils.sortStringArray(dataAccessResourceFailureCodes); + } + + public String[] getDataAccessResourceFailureCodes() { + return this.dataAccessResourceFailureCodes; + } + + public void setTransientDataAccessResourceCodes(String... transientDataAccessResourceCodes) { + this.transientDataAccessResourceCodes = StringUtils.sortStringArray(transientDataAccessResourceCodes); + } + + public String[] getTransientDataAccessResourceCodes() { + return this.transientDataAccessResourceCodes; + } + + public void setCannotAcquireLockCodes(String... cannotAcquireLockCodes) { + this.cannotAcquireLockCodes = StringUtils.sortStringArray(cannotAcquireLockCodes); + } + + public String[] getCannotAcquireLockCodes() { + return this.cannotAcquireLockCodes; + } + + public void setDeadlockLoserCodes(String... deadlockLoserCodes) { + this.deadlockLoserCodes = StringUtils.sortStringArray(deadlockLoserCodes); + } + + public String[] getDeadlockLoserCodes() { + return this.deadlockLoserCodes; + } + + public void setCannotSerializeTransactionCodes(String... cannotSerializeTransactionCodes) { + this.cannotSerializeTransactionCodes = StringUtils.sortStringArray(cannotSerializeTransactionCodes); + } + + public String[] getCannotSerializeTransactionCodes() { + return this.cannotSerializeTransactionCodes; + } + + public void setCustomTranslations(CustomSQLErrorCodesTranslation... customTranslations) { + this.customTranslations = customTranslations; + } + + @Nullable + public CustomSQLErrorCodesTranslation[] getCustomTranslations() { + return this.customTranslations; + } + + public void setCustomSqlExceptionTranslatorClass(@Nullable Class customTranslatorClass) { + if (customTranslatorClass != null) { + try { + this.customSqlExceptionTranslator = + ReflectionUtils.accessibleConstructor(customTranslatorClass).newInstance(); + } + catch (Throwable ex) { + throw new IllegalStateException("Unable to instantiate custom translator", ex); + } + } + else { + this.customSqlExceptionTranslator = null; + } + } + + public void setCustomSqlExceptionTranslator(@Nullable SQLExceptionTranslator customSqlExceptionTranslator) { + this.customSqlExceptionTranslator = customSqlExceptionTranslator; + } + + @Nullable + public SQLExceptionTranslator getCustomSqlExceptionTranslator() { + return this.customSqlExceptionTranslator; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodesFactory.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodesFactory.java new file mode 100644 index 0000000..2b78fa7 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodesFactory.java @@ -0,0 +1,318 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import java.sql.DatabaseMetaData; +import java.util.Collections; +import java.util.Map; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.PatternMatchUtils; +import org.springframework.util.StringUtils; + +/** + * Factory for creating {@link SQLErrorCodes} based on the + * "databaseProductName" taken from the {@link java.sql.DatabaseMetaData}. + * + *

    Returns {@code SQLErrorCodes} populated with vendor codes + * defined in a configuration file named "sql-error-codes.xml". + * Reads the default file in this package if not overridden by a file in + * the root of the class path (for example in the "/WEB-INF/classes" directory). + * + * @author Thomas Risberg + * @author Rod Johnson + * @author Juergen Hoeller + * @see java.sql.DatabaseMetaData#getDatabaseProductName() + */ +public class SQLErrorCodesFactory { + + /** + * The name of custom SQL error codes file, loading from the root + * of the class path (e.g. from the "/WEB-INF/classes" directory). + */ + public static final String SQL_ERROR_CODE_OVERRIDE_PATH = "sql-error-codes.xml"; + + /** + * The name of default SQL error code files, loading from the class path. + */ + public static final String SQL_ERROR_CODE_DEFAULT_PATH = "org/springframework/jdbc/support/sql-error-codes.xml"; + + + private static final Log logger = LogFactory.getLog(SQLErrorCodesFactory.class); + + /** + * Keep track of a single instance so we can return it to classes that request it. + */ + private static final SQLErrorCodesFactory instance = new SQLErrorCodesFactory(); + + + /** + * Return the singleton instance. + */ + public static SQLErrorCodesFactory getInstance() { + return instance; + } + + + /** + * Map to hold error codes for all databases defined in the config file. + * Key is the database product name, value is the SQLErrorCodes instance. + */ + private final Map errorCodesMap; + + /** + * Map to cache the SQLErrorCodes instance per DataSource. + */ + private final Map dataSourceCache = new ConcurrentReferenceHashMap<>(16); + + + /** + * Create a new instance of the {@link SQLErrorCodesFactory} class. + *

    Not public to enforce Singleton design pattern. Would be private + * except to allow testing via overriding the + * {@link #loadResource(String)} method. + *

    Do not subclass in application code. + * @see #loadResource(String) + */ + protected SQLErrorCodesFactory() { + Map errorCodes; + + try { + DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + lbf.setBeanClassLoader(getClass().getClassLoader()); + XmlBeanDefinitionReader bdr = new XmlBeanDefinitionReader(lbf); + + // Load default SQL error codes. + Resource resource = loadResource(SQL_ERROR_CODE_DEFAULT_PATH); + if (resource != null && resource.exists()) { + bdr.loadBeanDefinitions(resource); + } + else { + logger.info("Default sql-error-codes.xml not found (should be included in spring-jdbc jar)"); + } + + // Load custom SQL error codes, overriding defaults. + resource = loadResource(SQL_ERROR_CODE_OVERRIDE_PATH); + if (resource != null && resource.exists()) { + bdr.loadBeanDefinitions(resource); + logger.debug("Found custom sql-error-codes.xml file at the root of the classpath"); + } + + // Check all beans of type SQLErrorCodes. + errorCodes = lbf.getBeansOfType(SQLErrorCodes.class, true, false); + if (logger.isTraceEnabled()) { + logger.trace("SQLErrorCodes loaded: " + errorCodes.keySet()); + } + } + catch (BeansException ex) { + logger.warn("Error loading SQL error codes from config file", ex); + errorCodes = Collections.emptyMap(); + } + + this.errorCodesMap = errorCodes; + } + + /** + * Load the given resource from the class path. + *

    Not to be overridden by application developers, who should obtain + * instances of this class from the static {@link #getInstance()} method. + *

    Protected for testability. + * @param path resource path; either a custom path or one of either + * {@link #SQL_ERROR_CODE_DEFAULT_PATH} or + * {@link #SQL_ERROR_CODE_OVERRIDE_PATH}. + * @return the resource, or {@code null} if the resource wasn't found + * @see #getInstance + */ + @Nullable + protected Resource loadResource(String path) { + return new ClassPathResource(path, getClass().getClassLoader()); + } + + + /** + * Return the {@link SQLErrorCodes} instance for the given database. + *

    No need for a database meta-data lookup. + * @param databaseName the database name (must not be {@code null}) + * @return the {@code SQLErrorCodes} instance for the given database + * (never {@code null}; potentially empty) + * @throws IllegalArgumentException if the supplied database name is {@code null} + */ + public SQLErrorCodes getErrorCodes(String databaseName) { + Assert.notNull(databaseName, "Database product name must not be null"); + + SQLErrorCodes sec = this.errorCodesMap.get(databaseName); + if (sec == null) { + for (SQLErrorCodes candidate : this.errorCodesMap.values()) { + if (PatternMatchUtils.simpleMatch(candidate.getDatabaseProductNames(), databaseName)) { + sec = candidate; + break; + } + } + } + if (sec != null) { + checkCustomTranslatorRegistry(databaseName, sec); + if (logger.isDebugEnabled()) { + logger.debug("SQL error codes for '" + databaseName + "' found"); + } + return sec; + } + + // Could not find the database among the defined ones. + if (logger.isDebugEnabled()) { + logger.debug("SQL error codes for '" + databaseName + "' not found"); + } + return new SQLErrorCodes(); + } + + /** + * Return {@link SQLErrorCodes} for the given {@link DataSource}, + * evaluating "databaseProductName" from the + * {@link java.sql.DatabaseMetaData}, or an empty error codes + * instance if no {@code SQLErrorCodes} were found. + * @param dataSource the {@code DataSource} identifying the database + * @return the corresponding {@code SQLErrorCodes} object + * (never {@code null}; potentially empty) + * @see java.sql.DatabaseMetaData#getDatabaseProductName() + */ + public SQLErrorCodes getErrorCodes(DataSource dataSource) { + SQLErrorCodes sec = resolveErrorCodes(dataSource); + return (sec != null ? sec : new SQLErrorCodes()); + } + + /** + * Return {@link SQLErrorCodes} for the given {@link DataSource}, + * evaluating "databaseProductName" from the + * {@link java.sql.DatabaseMetaData}, or {@code null} if case + * of a JDBC meta-data access problem. + * @param dataSource the {@code DataSource} identifying the database + * @return the corresponding {@code SQLErrorCodes} object, + * or {@code null} in case of a JDBC meta-data access problem + * @since 5.2.9 + * @see java.sql.DatabaseMetaData#getDatabaseProductName() + */ + @Nullable + public SQLErrorCodes resolveErrorCodes(DataSource dataSource) { + Assert.notNull(dataSource, "DataSource must not be null"); + if (logger.isDebugEnabled()) { + logger.debug("Looking up default SQLErrorCodes for DataSource [" + identify(dataSource) + "]"); + } + + // Try efficient lock-free access for existing cache entry + SQLErrorCodes sec = this.dataSourceCache.get(dataSource); + if (sec == null) { + synchronized (this.dataSourceCache) { + // Double-check within full dataSourceCache lock + sec = this.dataSourceCache.get(dataSource); + if (sec == null) { + // We could not find it - got to look it up. + try { + String name = JdbcUtils.extractDatabaseMetaData(dataSource, + DatabaseMetaData::getDatabaseProductName); + if (StringUtils.hasLength(name)) { + return registerDatabase(dataSource, name); + } + } + catch (MetaDataAccessException ex) { + logger.warn("Error while extracting database name", ex); + } + return null; + } + } + } + + if (logger.isDebugEnabled()) { + logger.debug("SQLErrorCodes found in cache for DataSource [" + identify(dataSource) + "]"); + } + + return sec; + } + + /** + * Associate the specified database name with the given {@link DataSource}. + * @param dataSource the {@code DataSource} identifying the database + * @param databaseName the corresponding database name as stated in the error codes + * definition file (must not be {@code null}) + * @return the corresponding {@code SQLErrorCodes} object (never {@code null}) + * @see #unregisterDatabase(DataSource) + */ + public SQLErrorCodes registerDatabase(DataSource dataSource, String databaseName) { + SQLErrorCodes sec = getErrorCodes(databaseName); + if (logger.isDebugEnabled()) { + logger.debug("Caching SQL error codes for DataSource [" + identify(dataSource) + + "]: database product name is '" + databaseName + "'"); + } + this.dataSourceCache.put(dataSource, sec); + return sec; + } + + /** + * Clear the cache for the specified {@link DataSource}, if registered. + * @param dataSource the {@code DataSource} identifying the database + * @return the corresponding {@code SQLErrorCodes} object that got removed, + * or {@code null} if not registered + * @since 4.3.5 + * @see #registerDatabase(DataSource, String) + */ + @Nullable + public SQLErrorCodes unregisterDatabase(DataSource dataSource) { + return this.dataSourceCache.remove(dataSource); + } + + /** + * Build an identification String for the given {@link DataSource}, + * primarily for logging purposes. + * @param dataSource the {@code DataSource} to introspect + * @return the identification String + */ + private String identify(DataSource dataSource) { + return dataSource.getClass().getName() + '@' + Integer.toHexString(dataSource.hashCode()); + } + + /** + * Check the {@link CustomSQLExceptionTranslatorRegistry} for any entries. + */ + private void checkCustomTranslatorRegistry(String databaseName, SQLErrorCodes errorCodes) { + SQLExceptionTranslator customTranslator = + CustomSQLExceptionTranslatorRegistry.getInstance().findTranslatorForDatabase(databaseName); + if (customTranslator != null) { + if (errorCodes.getCustomSqlExceptionTranslator() != null && logger.isDebugEnabled()) { + logger.debug("Overriding already defined custom translator '" + + errorCodes.getCustomSqlExceptionTranslator().getClass().getSimpleName() + + " with '" + customTranslator.getClass().getSimpleName() + + "' found in the CustomSQLExceptionTranslatorRegistry for database '" + databaseName + "'"); + } + else if (logger.isTraceEnabled()) { + logger.trace("Using custom translator '" + customTranslator.getClass().getSimpleName() + + "' found in the CustomSQLExceptionTranslatorRegistry for database '" + databaseName + "'"); + } + errorCodes.setCustomSqlExceptionTranslator(customTranslator); + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java new file mode 100644 index 0000000..3532753 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import java.sql.SQLDataException; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLIntegrityConstraintViolationException; +import java.sql.SQLInvalidAuthorizationSpecException; +import java.sql.SQLNonTransientConnectionException; +import java.sql.SQLNonTransientException; +import java.sql.SQLRecoverableException; +import java.sql.SQLSyntaxErrorException; +import java.sql.SQLTimeoutException; +import java.sql.SQLTransactionRollbackException; +import java.sql.SQLTransientConnectionException; +import java.sql.SQLTransientException; + +import org.springframework.dao.ConcurrencyFailureException; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.dao.PermissionDeniedDataAccessException; +import org.springframework.dao.QueryTimeoutException; +import org.springframework.dao.RecoverableDataAccessException; +import org.springframework.dao.TransientDataAccessResourceException; +import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.lang.Nullable; + +/** + * {@link SQLExceptionTranslator} implementation which analyzes the specific + * {@link java.sql.SQLException} subclass thrown by the JDBC driver. + * + *

    Falls back to a standard {@link SQLStateSQLExceptionTranslator} if the JDBC + * driver does not actually expose JDBC 4 compliant {@code SQLException} subclasses. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.5 + * @see java.sql.SQLTransientException + * @see java.sql.SQLTransientException + * @see java.sql.SQLRecoverableException + */ +public class SQLExceptionSubclassTranslator extends AbstractFallbackSQLExceptionTranslator { + + public SQLExceptionSubclassTranslator() { + setFallbackTranslator(new SQLStateSQLExceptionTranslator()); + } + + @Override + @Nullable + protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) { + if (ex instanceof SQLTransientException) { + if (ex instanceof SQLTransientConnectionException) { + return new TransientDataAccessResourceException(buildMessage(task, sql, ex), ex); + } + else if (ex instanceof SQLTransactionRollbackException) { + return new ConcurrencyFailureException(buildMessage(task, sql, ex), ex); + } + else if (ex instanceof SQLTimeoutException) { + return new QueryTimeoutException(buildMessage(task, sql, ex), ex); + } + } + else if (ex instanceof SQLNonTransientException) { + if (ex instanceof SQLNonTransientConnectionException) { + return new DataAccessResourceFailureException(buildMessage(task, sql, ex), ex); + } + else if (ex instanceof SQLDataException) { + return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex); + } + else if (ex instanceof SQLIntegrityConstraintViolationException) { + return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex); + } + else if (ex instanceof SQLInvalidAuthorizationSpecException) { + return new PermissionDeniedDataAccessException(buildMessage(task, sql, ex), ex); + } + else if (ex instanceof SQLSyntaxErrorException) { + return new BadSqlGrammarException(task, (sql != null ? sql : ""), ex); + } + else if (ex instanceof SQLFeatureNotSupportedException) { + return new InvalidDataAccessApiUsageException(buildMessage(task, sql, ex), ex); + } + } + else if (ex instanceof SQLRecoverableException) { + return new RecoverableDataAccessException(buildMessage(task, sql, ex), ex); + } + + // Fallback to Spring's own SQL state translation... + return null; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionTranslator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionTranslator.java new file mode 100644 index 0000000..349ea1d --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionTranslator.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import java.sql.SQLException; + +import org.springframework.dao.DataAccessException; +import org.springframework.lang.Nullable; + +/** + * Strategy interface for translating between {@link SQLException SQLExceptions} + * and Spring's data access strategy-agnostic {@link DataAccessException} + * hierarchy. + * + *

    Implementations can be generic (for example, using + * {@link java.sql.SQLException#getSQLState() SQLState} codes for JDBC) or wholly + * proprietary (for example, using Oracle error codes) for greater precision. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @see org.springframework.dao.DataAccessException + */ +@FunctionalInterface +public interface SQLExceptionTranslator { + + /** + * Translate the given {@link SQLException} into a generic {@link DataAccessException}. + *

    The returned DataAccessException is supposed to contain the original + * {@code SQLException} as root cause. However, client code may not generally + * rely on this due to DataAccessExceptions possibly being caused by other resource + * APIs as well. That said, a {@code getRootCause() instanceof SQLException} + * check (and subsequent cast) is considered reliable when expecting JDBC-based + * access to have happened. + * @param task readable text describing the task being attempted + * @param sql the SQL query or update that caused the problem (if known) + * @param ex the offending {@code SQLException} + * @return the DataAccessException wrapping the {@code SQLException}, + * or {@code null} if no specific translation could be applied + * @see org.springframework.dao.DataAccessException#getRootCause() + */ + @Nullable + DataAccessException translate(String task, @Nullable String sql, SQLException ex); + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslator.java new file mode 100644 index 0000000..4585993 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslator.java @@ -0,0 +1,147 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import java.sql.SQLException; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.dao.ConcurrencyFailureException; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.QueryTimeoutException; +import org.springframework.dao.TransientDataAccessResourceException; +import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.lang.Nullable; + +/** + * {@link SQLExceptionTranslator} implementation that analyzes the SQL state in + * the {@link SQLException} based on the first two digits (the SQL state "class"). + * Detects standard SQL state values and well-known vendor-specific SQL states. + * + *

    Not able to diagnose all problems, but is portable between databases and + * does not require special initialization (no database vendor detection, etc.). + * For more precise translation, consider {@link SQLErrorCodeSQLExceptionTranslator}. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Thomas Risberg + * @see java.sql.SQLException#getSQLState() + * @see SQLErrorCodeSQLExceptionTranslator + */ +public class SQLStateSQLExceptionTranslator extends AbstractFallbackSQLExceptionTranslator { + + private static final Set BAD_SQL_GRAMMAR_CODES = new HashSet<>(8); + + private static final Set DATA_INTEGRITY_VIOLATION_CODES = new HashSet<>(8); + + private static final Set DATA_ACCESS_RESOURCE_FAILURE_CODES = new HashSet<>(8); + + private static final Set TRANSIENT_DATA_ACCESS_RESOURCE_CODES = new HashSet<>(8); + + private static final Set CONCURRENCY_FAILURE_CODES = new HashSet<>(4); + + + static { + BAD_SQL_GRAMMAR_CODES.add("07"); // Dynamic SQL error + BAD_SQL_GRAMMAR_CODES.add("21"); // Cardinality violation + BAD_SQL_GRAMMAR_CODES.add("2A"); // Syntax error direct SQL + BAD_SQL_GRAMMAR_CODES.add("37"); // Syntax error dynamic SQL + BAD_SQL_GRAMMAR_CODES.add("42"); // General SQL syntax error + BAD_SQL_GRAMMAR_CODES.add("65"); // Oracle: unknown identifier + + DATA_INTEGRITY_VIOLATION_CODES.add("01"); // Data truncation + DATA_INTEGRITY_VIOLATION_CODES.add("02"); // No data found + DATA_INTEGRITY_VIOLATION_CODES.add("22"); // Value out of range + DATA_INTEGRITY_VIOLATION_CODES.add("23"); // Integrity constraint violation + DATA_INTEGRITY_VIOLATION_CODES.add("27"); // Triggered data change violation + DATA_INTEGRITY_VIOLATION_CODES.add("44"); // With check violation + + DATA_ACCESS_RESOURCE_FAILURE_CODES.add("08"); // Connection exception + DATA_ACCESS_RESOURCE_FAILURE_CODES.add("53"); // PostgreSQL: insufficient resources (e.g. disk full) + DATA_ACCESS_RESOURCE_FAILURE_CODES.add("54"); // PostgreSQL: program limit exceeded (e.g. statement too complex) + DATA_ACCESS_RESOURCE_FAILURE_CODES.add("57"); // DB2: out-of-memory exception / database not started + DATA_ACCESS_RESOURCE_FAILURE_CODES.add("58"); // DB2: unexpected system error + + TRANSIENT_DATA_ACCESS_RESOURCE_CODES.add("JW"); // Sybase: internal I/O error + TRANSIENT_DATA_ACCESS_RESOURCE_CODES.add("JZ"); // Sybase: unexpected I/O error + TRANSIENT_DATA_ACCESS_RESOURCE_CODES.add("S1"); // DB2: communication failure + + CONCURRENCY_FAILURE_CODES.add("40"); // Transaction rollback + CONCURRENCY_FAILURE_CODES.add("61"); // Oracle: deadlock + } + + + @Override + @Nullable + protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) { + // First, the getSQLState check... + String sqlState = getSqlState(ex); + if (sqlState != null && sqlState.length() >= 2) { + String classCode = sqlState.substring(0, 2); + if (logger.isDebugEnabled()) { + logger.debug("Extracted SQL state class '" + classCode + "' from value '" + sqlState + "'"); + } + if (BAD_SQL_GRAMMAR_CODES.contains(classCode)) { + return new BadSqlGrammarException(task, (sql != null ? sql : ""), ex); + } + else if (DATA_INTEGRITY_VIOLATION_CODES.contains(classCode)) { + return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex); + } + else if (DATA_ACCESS_RESOURCE_FAILURE_CODES.contains(classCode)) { + return new DataAccessResourceFailureException(buildMessage(task, sql, ex), ex); + } + else if (TRANSIENT_DATA_ACCESS_RESOURCE_CODES.contains(classCode)) { + return new TransientDataAccessResourceException(buildMessage(task, sql, ex), ex); + } + else if (CONCURRENCY_FAILURE_CODES.contains(classCode)) { + return new ConcurrencyFailureException(buildMessage(task, sql, ex), ex); + } + } + + // For MySQL: exception class name indicating a timeout? + // (since MySQL doesn't throw the JDBC 4 SQLTimeoutException) + if (ex.getClass().getName().contains("Timeout")) { + return new QueryTimeoutException(buildMessage(task, sql, ex), ex); + } + + // Couldn't resolve anything proper - resort to UncategorizedSQLException. + return null; + } + + /** + * Gets the SQL state code from the supplied {@link SQLException exception}. + *

    Some JDBC drivers nest the actual exception from a batched update, so we + * might need to dig down into the nested exception. + * @param ex the exception from which the {@link SQLException#getSQLState() SQL state} + * is to be extracted + * @return the SQL state code + */ + @Nullable + private String getSqlState(SQLException ex) { + String sqlState = ex.getSQLState(); + if (sqlState == null) { + SQLException nestedEx = ex.getNextException(); + if (nestedEx != null) { + sqlState = nestedEx.getSQLState(); + } + } + return sqlState; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SqlValue.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SqlValue.java new file mode 100644 index 0000000..ec9a368 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SqlValue.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * Simple interface for complex types to be set as statement parameters. + * + *

    Implementations perform the actual work of setting the actual values. They must + * implement the callback method {@code setValue} which can throw SQLExceptions + * that will be caught and translated by the calling code. This callback method has + * access to the underlying Connection via the given PreparedStatement object, if that + * should be needed to create any database-specific objects. + * + * @author Juergen Hoeller + * @since 2.5.6 + * @see org.springframework.jdbc.core.SqlTypeValue + * @see org.springframework.jdbc.core.DisposableSqlTypeValue + */ +public interface SqlValue { + + /** + * Set the value on the given PreparedStatement. + * @param ps the PreparedStatement to work on + * @param paramIndex the index of the parameter for which we need to set the value + * @throws SQLException if an SQLException is encountered while setting parameter values + */ + void setValue(PreparedStatement ps, int paramIndex) throws SQLException; + + /** + * Clean up resources held by this value object. + */ + void cleanup(); + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractColumnMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractColumnMaxValueIncrementer.java new file mode 100644 index 0000000..67615dc --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractColumnMaxValueIncrementer.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.incrementer; + +import javax.sql.DataSource; + +import org.springframework.util.Assert; + +/** + * Abstract base class for {@link DataFieldMaxValueIncrementer} implementations that use + * a column in a custom sequence table. Subclasses need to provide the specific handling + * of that table in their {@link #getNextKey()} implementation. + * + * @author Juergen Hoeller + * @since 2.5.3 + */ +public abstract class AbstractColumnMaxValueIncrementer extends AbstractDataFieldMaxValueIncrementer { + + /** The name of the column for this sequence. */ + private String columnName; + + /** The number of keys buffered in a cache. */ + private int cacheSize = 1; + + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + * @see #setColumnName + */ + public AbstractColumnMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + * @param columnName the name of the column in the sequence table to use + */ + public AbstractColumnMaxValueIncrementer(DataSource dataSource, String incrementerName, String columnName) { + super(dataSource, incrementerName); + Assert.notNull(columnName, "Column name must not be null"); + this.columnName = columnName; + } + + + /** + * Set the name of the column in the sequence table. + */ + public void setColumnName(String columnName) { + this.columnName = columnName; + } + + /** + * Return the name of the column in the sequence table. + */ + public String getColumnName() { + return this.columnName; + } + + /** + * Set the number of buffered keys. + */ + public void setCacheSize(int cacheSize) { + this.cacheSize = cacheSize; + } + + /** + * Return the number of buffered keys. + */ + public int getCacheSize() { + return this.cacheSize; + } + + @Override + public void afterPropertiesSet() { + super.afterPropertiesSet(); + if (this.columnName == null) { + throw new IllegalArgumentException("Property 'columnName' is required"); + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractDataFieldMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractDataFieldMaxValueIncrementer.java new file mode 100644 index 0000000..bc96bcf --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractDataFieldMaxValueIncrementer.java @@ -0,0 +1,154 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.incrementer; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.dao.DataAccessException; +import org.springframework.util.Assert; + +/** + * Base implementation of {@link DataFieldMaxValueIncrementer} that delegates + * to a single {@link #getNextKey} template method that returns a {@code long}. + * Uses longs for String values, padding with zeroes if required. + * + * @author Dmitriy Kopylenko + * @author Juergen Hoeller + * @author Jean-Pierre Pawlak + * @author Juergen Hoeller + */ +public abstract class AbstractDataFieldMaxValueIncrementer implements DataFieldMaxValueIncrementer, InitializingBean { + + private DataSource dataSource; + + /** The name of the sequence/table containing the sequence. */ + private String incrementerName; + + /** The length to which a string result should be pre-pended with zeroes. */ + protected int paddingLength = 0; + + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + */ + public AbstractDataFieldMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + */ + public AbstractDataFieldMaxValueIncrementer(DataSource dataSource, String incrementerName) { + Assert.notNull(dataSource, "DataSource must not be null"); + Assert.notNull(incrementerName, "Incrementer name must not be null"); + this.dataSource = dataSource; + this.incrementerName = incrementerName; + } + + + /** + * Set the data source to retrieve the value from. + */ + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + } + + /** + * Return the data source to retrieve the value from. + */ + public DataSource getDataSource() { + return this.dataSource; + } + + /** + * Set the name of the sequence/table. + */ + public void setIncrementerName(String incrementerName) { + this.incrementerName = incrementerName; + } + + /** + * Return the name of the sequence/table. + */ + public String getIncrementerName() { + return this.incrementerName; + } + + /** + * Set the padding length, i.e. the length to which a string result + * should be pre-pended with zeroes. + */ + public void setPaddingLength(int paddingLength) { + this.paddingLength = paddingLength; + } + + /** + * Return the padding length for String values. + */ + public int getPaddingLength() { + return this.paddingLength; + } + + @Override + public void afterPropertiesSet() { + if (this.dataSource == null) { + throw new IllegalArgumentException("Property 'dataSource' is required"); + } + if (this.incrementerName == null) { + throw new IllegalArgumentException("Property 'incrementerName' is required"); + } + } + + + @Override + public int nextIntValue() throws DataAccessException { + return (int) getNextKey(); + } + + @Override + public long nextLongValue() throws DataAccessException { + return getNextKey(); + } + + @Override + public String nextStringValue() throws DataAccessException { + String s = Long.toString(getNextKey()); + int len = s.length(); + if (len < this.paddingLength) { + StringBuilder sb = new StringBuilder(this.paddingLength); + for (int i = 0; i < this.paddingLength - len; i++) { + sb.append('0'); + } + sb.append(s); + s = sb.toString(); + } + return s; + } + + + /** + * Determine the next key to use, as a long. + * @return the key to use as a long. It will eventually be converted later + * in another format by the public concrete methods of this class. + */ + protected abstract long getNextKey(); + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractIdentityColumnMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractIdentityColumnMaxValueIncrementer.java new file mode 100644 index 0000000..0bb4445 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractIdentityColumnMaxValueIncrementer.java @@ -0,0 +1,164 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.incrementer; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.jdbc.support.JdbcUtils; + +/** + * Abstract base class for {@link DataFieldMaxValueIncrementer} implementations + * which are based on identity columns in a sequence-like table. + * + * @author Juergen Hoeller + * @author Thomas Risberg + * @since 4.1.2 + */ +public abstract class AbstractIdentityColumnMaxValueIncrementer extends AbstractColumnMaxValueIncrementer { + + private boolean deleteSpecificValues = false; + + /** The current cache of values. */ + private long[] valueCache; + + /** The next id to serve from the value cache. */ + private int nextValueIndex = -1; + + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + * @see #setColumnName + */ + public AbstractIdentityColumnMaxValueIncrementer() { + } + + public AbstractIdentityColumnMaxValueIncrementer(DataSource dataSource, String incrementerName, String columnName) { + super(dataSource, incrementerName, columnName); + } + + + /** + * Specify whether to delete the entire range below the current maximum key value + * ({@code false} - the default), or the specifically generated values ({@code true}). + * The former mode will use a where range clause whereas the latter will use an in + * clause starting with the lowest value minus 1, just preserving the maximum value. + */ + public void setDeleteSpecificValues(boolean deleteSpecificValues) { + this.deleteSpecificValues = deleteSpecificValues; + } + + /** + * Return whether to delete the entire range below the current maximum key value + * ({@code false} - the default), or the specifically generated values ({@code true}). + */ + public boolean isDeleteSpecificValues() { + return this.deleteSpecificValues; + } + + + @Override + protected synchronized long getNextKey() throws DataAccessException { + if (this.nextValueIndex < 0 || this.nextValueIndex >= getCacheSize()) { + /* + * Need to use straight JDBC code because we need to make sure that the insert and select + * are performed on the same connection (otherwise we can't be sure that @@identity + * returns the correct value) + */ + Connection con = DataSourceUtils.getConnection(getDataSource()); + Statement stmt = null; + try { + stmt = con.createStatement(); + DataSourceUtils.applyTransactionTimeout(stmt, getDataSource()); + this.valueCache = new long[getCacheSize()]; + this.nextValueIndex = 0; + for (int i = 0; i < getCacheSize(); i++) { + stmt.executeUpdate(getIncrementStatement()); + ResultSet rs = stmt.executeQuery(getIdentityStatement()); + try { + if (!rs.next()) { + throw new DataAccessResourceFailureException("Identity statement failed after inserting"); + } + this.valueCache[i] = rs.getLong(1); + } + finally { + JdbcUtils.closeResultSet(rs); + } + } + stmt.executeUpdate(getDeleteStatement(this.valueCache)); + } + catch (SQLException ex) { + throw new DataAccessResourceFailureException("Could not increment identity", ex); + } + finally { + JdbcUtils.closeStatement(stmt); + DataSourceUtils.releaseConnection(con, getDataSource()); + } + } + return this.valueCache[this.nextValueIndex++]; + } + + + /** + * Statement to use to increment the "sequence" value. + * @return the SQL statement to use + */ + protected abstract String getIncrementStatement(); + + /** + * Statement to use to obtain the current identity value. + * @return the SQL statement to use + */ + protected abstract String getIdentityStatement(); + + /** + * Statement to use to clean up "sequence" values. + *

    The default implementation either deletes the entire range below + * the current maximum value, or the specifically generated values + * (starting with the lowest minus 1, just preserving the maximum value) + * - according to the {@link #isDeleteSpecificValues()} setting. + * @param values the currently generated key values + * (the number of values corresponds to {@link #getCacheSize()}) + * @return the SQL statement to use + */ + protected String getDeleteStatement(long[] values) { + StringBuilder sb = new StringBuilder(64); + sb.append("delete from ").append(getIncrementerName()).append(" where ").append(getColumnName()); + if (isDeleteSpecificValues()) { + sb.append(" in (").append(values[0] - 1); + for (int i = 0; i < values.length - 1; i++) { + sb.append(", ").append(values[i]); + } + sb.append(")"); + } + else { + long maxValue = values[values.length - 1]; + sb.append(" < ").append(maxValue); + } + return sb.toString(); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractSequenceMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractSequenceMaxValueIncrementer.java new file mode 100644 index 0000000..4909ffe --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractSequenceMaxValueIncrementer.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.incrementer; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.jdbc.support.JdbcUtils; + +/** + * Abstract base class for {@link DataFieldMaxValueIncrementer} implementations that use + * a database sequence. Subclasses need to provide the database-specific SQL to use. + * + * @author Juergen Hoeller + * @since 26.02.2004 + * @see #getSequenceQuery + */ +public abstract class AbstractSequenceMaxValueIncrementer extends AbstractDataFieldMaxValueIncrementer { + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + */ + public AbstractSequenceMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + */ + public AbstractSequenceMaxValueIncrementer(DataSource dataSource, String incrementerName) { + super(dataSource, incrementerName); + } + + + /** + * Executes the SQL as specified by {@link #getSequenceQuery()}. + */ + @Override + protected long getNextKey() throws DataAccessException { + Connection con = DataSourceUtils.getConnection(getDataSource()); + Statement stmt = null; + ResultSet rs = null; + try { + stmt = con.createStatement(); + DataSourceUtils.applyTransactionTimeout(stmt, getDataSource()); + rs = stmt.executeQuery(getSequenceQuery()); + if (rs.next()) { + return rs.getLong(1); + } + else { + throw new DataAccessResourceFailureException("Sequence query did not return a result"); + } + } + catch (SQLException ex) { + throw new DataAccessResourceFailureException("Could not obtain sequence value", ex); + } + finally { + JdbcUtils.closeResultSet(rs); + JdbcUtils.closeStatement(stmt); + DataSourceUtils.releaseConnection(con, getDataSource()); + } + } + + /** + * Return the database-specific query to use for retrieving a sequence value. + *

    The provided SQL is supposed to result in a single row with a single + * column that allows for extracting a {@code long} value. + */ + protected abstract String getSequenceQuery(); + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/DB2MainframeSequenceMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/DB2MainframeSequenceMaxValueIncrementer.java new file mode 100644 index 0000000..de88a53 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/DB2MainframeSequenceMaxValueIncrementer.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2008 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.incrementer; + +import javax.sql.DataSource; + +/** + * {@link DataFieldMaxValueIncrementer} that retrieves the next value + * of a given sequence on DB2 for the mainframe (z/OS, DB2/390, DB2/400). + * + *

    Thanks to Jens Eickmeyer for the suggestion! + * + * @author Juergen Hoeller + * @since 2.5.3 + * @deprecated in favor of the differently named {@link Db2MainframeMaxValueIncrementer} + */ +@Deprecated +public class DB2MainframeSequenceMaxValueIncrementer extends AbstractSequenceMaxValueIncrementer { + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + */ + public DB2MainframeSequenceMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + */ + public DB2MainframeSequenceMaxValueIncrementer(DataSource dataSource, String incrementerName) { + super(dataSource, incrementerName); + } + + + @Override + protected String getSequenceQuery() { + return "select next value for " + getIncrementerName() + " from sysibm.sysdummy1"; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/DB2SequenceMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/DB2SequenceMaxValueIncrementer.java new file mode 100644 index 0000000..bf6e1c9 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/DB2SequenceMaxValueIncrementer.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2008 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.incrementer; + +import javax.sql.DataSource; + +/** + * {@link DataFieldMaxValueIncrementer} that retrieves the next value + * of a given sequence on DB2 LUW (for Linux, Unix and Windows). + * + *

    Thanks to Mark MacMahon for the suggestion! + * + * @author Juergen Hoeller + * @since 1.1.3 + * @deprecated in favor of the specifically named {@link Db2LuwMaxValueIncrementer} + */ +@Deprecated +public class DB2SequenceMaxValueIncrementer extends Db2LuwMaxValueIncrementer { + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + */ + public DB2SequenceMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + */ + public DB2SequenceMaxValueIncrementer(DataSource dataSource, String incrementerName) { + super(dataSource, incrementerName); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/DataFieldMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/DataFieldMaxValueIncrementer.java new file mode 100644 index 0000000..fa9a08a --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/DataFieldMaxValueIncrementer.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2007 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.incrementer; + +import org.springframework.dao.DataAccessException; + +/** + * Interface that defines contract of incrementing any data store field's + * maximum value. Works much like a sequence number generator. + * + *

    Typical implementations may use standard SQL, native RDBMS sequences + * or Stored Procedures to do the job. + * + * @author Dmitriy Kopylenko + * @author Jean-Pierre Pawlak + * @author Juergen Hoeller + */ +public interface DataFieldMaxValueIncrementer { + + /** + * Increment the data store field's max value as int. + * @return int next data store value such as max + 1 + * @throws org.springframework.dao.DataAccessException in case of errors + */ + int nextIntValue() throws DataAccessException; + + /** + * Increment the data store field's max value as long. + * @return int next data store value such as max + 1 + * @throws org.springframework.dao.DataAccessException in case of errors + */ + long nextLongValue() throws DataAccessException; + + /** + * Increment the data store field's max value as String. + * @return next data store value such as max + 1 + * @throws org.springframework.dao.DataAccessException in case of errors + */ + String nextStringValue() throws DataAccessException; + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/Db2LuwMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/Db2LuwMaxValueIncrementer.java new file mode 100644 index 0000000..c982765 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/Db2LuwMaxValueIncrementer.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.incrementer; + +import javax.sql.DataSource; + +/** + * {@link DataFieldMaxValueIncrementer} that retrieves the next value + * of a given sequence on DB2 LUW (for Linux, Unix and Windows). + * + *

    Thanks to Mark MacMahon for the suggestion! + * + * @author Juergen Hoeller + * @since 4.3.15 + * @see Db2MainframeMaxValueIncrementer + */ +public class Db2LuwMaxValueIncrementer extends AbstractSequenceMaxValueIncrementer { + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + */ + public Db2LuwMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + */ + public Db2LuwMaxValueIncrementer(DataSource dataSource, String incrementerName) { + super(dataSource, incrementerName); + } + + + @Override + protected String getSequenceQuery() { + return "values nextval for " + getIncrementerName(); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/Db2MainframeMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/Db2MainframeMaxValueIncrementer.java new file mode 100644 index 0000000..143ec25 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/Db2MainframeMaxValueIncrementer.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.incrementer; + +import javax.sql.DataSource; + +/** + * {@link DataFieldMaxValueIncrementer} that retrieves the next value + * of a given sequence on DB2 for the mainframe (z/OS, DB2/390, DB2/400). + * + *

    Thanks to Jens Eickmeyer for the suggestion! + * + * @author Juergen Hoeller + * @since 4.3.15 + * @see Db2LuwMaxValueIncrementer + */ +public class Db2MainframeMaxValueIncrementer extends AbstractSequenceMaxValueIncrementer { + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + */ + public Db2MainframeMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + */ + public Db2MainframeMaxValueIncrementer(DataSource dataSource, String incrementerName) { + super(dataSource, incrementerName); + } + + + @Override + protected String getSequenceQuery() { + return "select next value for " + getIncrementerName() + " from sysibm.sysdummy1"; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/DerbyMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/DerbyMaxValueIncrementer.java new file mode 100644 index 0000000..af3ffc1 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/DerbyMaxValueIncrementer.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.incrementer; + +import javax.sql.DataSource; + +/** + * {@link DataFieldMaxValueIncrementer} that increments the maximum value of a given Derby table + * with the equivalent of an auto-increment column. Note: If you use this class, your Derby key + * column should NOT be defined as an IDENTITY column, as the sequence table does the job. + * + *

    The sequence is kept in a table. There should be one sequence table per + * table that needs an auto-generated key. + * + *

    Derby requires an additional column to be used for the insert since it is impossible + * to insert a null into the identity column and have the value generated. This is solved by + * providing the name of a dummy column that also must be created in the sequence table. + * + *

    Example: + * + *

    create table tab (id int not null primary key, text varchar(100));
    + * create table tab_sequence (value int generated always as identity, dummy char(1));
    + * insert into tab_sequence (dummy) values(null);
    + * + * If "cacheSize" is set, the intermediate values are served without querying the + * database. If the server or your application is stopped or crashes or a transaction + * is rolled back, the unused values will never be served. The maximum hole size in + * numbering is consequently the value of cacheSize. + * + * HINT: Since Derby supports the JDBC 3.0 {@code getGeneratedKeys} method, + * it is recommended to use IDENTITY columns directly in the tables and then utilizing + * a {@link org.springframework.jdbc.support.KeyHolder} when calling the with the + * {@code update(PreparedStatementCreator psc, KeyHolder generatedKeyHolder)} + * method of the {@link org.springframework.jdbc.core.JdbcTemplate}. + * + *

    Thanks to Endre Stolsvik for the suggestion! + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.5 + */ +public class DerbyMaxValueIncrementer extends AbstractIdentityColumnMaxValueIncrementer { + + /** The default for dummy name. */ + private static final String DEFAULT_DUMMY_NAME = "dummy"; + + /** The name of the dummy column used for inserts. */ + private String dummyName = DEFAULT_DUMMY_NAME; + + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + * @see #setColumnName + */ + public DerbyMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + * @param columnName the name of the column in the sequence table to use + */ + public DerbyMaxValueIncrementer(DataSource dataSource, String incrementerName, String columnName) { + super(dataSource, incrementerName, columnName); + this.dummyName = DEFAULT_DUMMY_NAME; + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + * @param columnName the name of the column in the sequence table to use + * @param dummyName the name of the dummy column used for inserts + */ + public DerbyMaxValueIncrementer(DataSource dataSource, String incrementerName, String columnName, String dummyName) { + super(dataSource, incrementerName, columnName); + this.dummyName = dummyName; + } + + + /** + * Set the name of the dummy column. + */ + public void setDummyName(String dummyName) { + this.dummyName = dummyName; + } + + /** + * Return the name of the dummy column. + */ + public String getDummyName() { + return this.dummyName; + } + + + @Override + protected String getIncrementStatement() { + return "insert into " + getIncrementerName() + " (" + getDummyName() + ") values(null)"; + } + + @Override + protected String getIdentityStatement() { + return "select IDENTITY_VAL_LOCAL() from " + getIncrementerName(); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/H2SequenceMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/H2SequenceMaxValueIncrementer.java new file mode 100644 index 0000000..7b39ada --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/H2SequenceMaxValueIncrementer.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.incrementer; + +import javax.sql.DataSource; + +/** + * {@link DataFieldMaxValueIncrementer} that retrieves the next value + * of a given H2 sequence. + * + * @author Thomas Risberg + * @since 2.5 + */ +public class H2SequenceMaxValueIncrementer extends AbstractSequenceMaxValueIncrementer { + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + */ + public H2SequenceMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + */ + public H2SequenceMaxValueIncrementer(DataSource dataSource, String incrementerName) { + super(dataSource, incrementerName); + } + + + @Override + protected String getSequenceQuery() { + return "select " + getIncrementerName() + ".nextval from dual"; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/HanaSequenceMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/HanaSequenceMaxValueIncrementer.java new file mode 100644 index 0000000..f88da25 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/HanaSequenceMaxValueIncrementer.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.incrementer; + +import javax.sql.DataSource; + +/** + * {@link DataFieldMaxValueIncrementer} that retrieves the next value + * of a given SAP HANA sequence. + * + * @author Jonathan Bregler + * @author Juergen Hoeller + * @since 4.3.15 + */ +public class HanaSequenceMaxValueIncrementer extends AbstractSequenceMaxValueIncrementer { + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + */ + public HanaSequenceMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + */ + public HanaSequenceMaxValueIncrementer(DataSource dataSource, String incrementerName) { + super(dataSource, incrementerName); + } + + + @Override + protected String getSequenceQuery() { + return "select " + getIncrementerName() + ".nextval from dummy"; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/HsqlMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/HsqlMaxValueIncrementer.java new file mode 100644 index 0000000..bdbbc93 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/HsqlMaxValueIncrementer.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.incrementer; + +import javax.sql.DataSource; + +/** + * {@link DataFieldMaxValueIncrementer} that increments the maximum value of a given HSQL table + * with the equivalent of an auto-increment column. Note: If you use this class, your HSQL + * key column should NOT be auto-increment, as the sequence table does the job. + * + *

    The sequence is kept in a table. There should be one sequence table per + * table that needs an auto-generated key. + * + *

    Example: + * + *

    create table tab (id int not null primary key, text varchar(100));
    + * create table tab_sequence (value identity);
    + * insert into tab_sequence values(0);
    + * + * If "cacheSize" is set, the intermediate values are served without querying the + * database. If the server or your application is stopped or crashes or a transaction + * is rolled back, the unused values will never be served. The maximum hole size in + * numbering is consequently the value of cacheSize. + * + *

    NOTE: HSQL now supports sequences and you should consider using them instead: + * {@link HsqlSequenceMaxValueIncrementer} + * + * @author Jean-Pierre Pawlak + * @author Thomas Risberg + * @author Juergen Hoeller + * @see HsqlSequenceMaxValueIncrementer + */ +public class HsqlMaxValueIncrementer extends AbstractIdentityColumnMaxValueIncrementer { + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + * @see #setColumnName + */ + public HsqlMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + * @param columnName the name of the column in the sequence table to use + */ + public HsqlMaxValueIncrementer(DataSource dataSource, String incrementerName, String columnName) { + super(dataSource, incrementerName, columnName); + } + + + @Override + protected String getIncrementStatement() { + return "insert into " + getIncrementerName() + " values(null)"; + } + + @Override + protected String getIdentityStatement() { + return "select max(identity()) from " + getIncrementerName(); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/HsqlSequenceMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/HsqlSequenceMaxValueIncrementer.java new file mode 100644 index 0000000..d139049 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/HsqlSequenceMaxValueIncrementer.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.incrementer; + +import javax.sql.DataSource; + +/** + * {@link DataFieldMaxValueIncrementer} that retrieves the next value + * of a given HSQL sequence. + * + *

    Thanks to Guillaume Bilodeau for the suggestion! + * + *

    NOTE: This is an alternative to using a regular table to support + * generating unique keys that was necessary in previous versions of HSQL. + * + * @author Thomas Risberg + * @since 2.5 + * @see HsqlMaxValueIncrementer + */ +public class HsqlSequenceMaxValueIncrementer extends AbstractSequenceMaxValueIncrementer { + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + */ + public HsqlSequenceMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + */ + public HsqlSequenceMaxValueIncrementer(DataSource dataSource, String incrementerName) { + super(dataSource, incrementerName); + } + + + @Override + protected String getSequenceQuery() { + return "call next value for " + getIncrementerName(); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java new file mode 100644 index 0000000..cf6d0f0 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/MySQLMaxValueIncrementer.java @@ -0,0 +1,194 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.incrementer; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.jdbc.support.JdbcUtils; + +/** + * {@link DataFieldMaxValueIncrementer} that increments the maximum value of a given MySQL table + * with the equivalent of an auto-increment column. Note: If you use this class, your MySQL + * key column should NOT be auto-increment, as the sequence table does the job. + * + *

    The sequence is kept in a table; there should be one sequence table per + * table that needs an auto-generated key. The storage engine used by the sequence table + * can be MYISAM or INNODB since the sequences are allocated using a separate connection + * without being affected by any other transactions that might be in progress. + * + *

    Example: + * + *

    create table tab (id int unsigned not null primary key, text varchar(100));
    + * create table tab_sequence (value int not null);
    + * insert into tab_sequence values(0);
    + * + * If "cacheSize" is set, the intermediate values are served without querying the + * database. If the server or your application is stopped or crashes or a transaction + * is rolled back, the unused values will never be served. The maximum hole size in + * numbering is consequently the value of cacheSize. + * + *

    It is possible to avoid acquiring a new connection for the incrementer by setting the + * "useNewConnection" property to false. In this case you MUST use a non-transactional + * storage engine like MYISAM when defining the incrementer table. + * + * @author Jean-Pierre Pawlak + * @author Thomas Risberg + * @author Juergen Hoeller + */ +public class MySQLMaxValueIncrementer extends AbstractColumnMaxValueIncrementer { + + /** The SQL string for retrieving the new sequence value. */ + private static final String VALUE_SQL = "select last_insert_id()"; + + /** The next id to serve. */ + private long nextId = 0; + + /** The max id to serve. */ + private long maxId = 0; + + /** Whether or not to use a new connection for the incrementer. */ + private boolean useNewConnection = true; + + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + * @see #setColumnName + */ + public MySQLMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence table to use + * @param columnName the name of the column in the sequence table to use + */ + public MySQLMaxValueIncrementer(DataSource dataSource, String incrementerName, String columnName) { + super(dataSource, incrementerName, columnName); + } + + + /** + * Set whether to use a new connection for the incrementer. + *

    {@code true} is necessary to support transactional storage engines, + * using an isolated separate transaction for the increment operation. + * {@code false} is sufficient if the storage engine of the sequence table + * is non-transactional (like MYISAM), avoiding the effort of acquiring an + * extra {@code Connection} for the increment operation. + *

    Default is {@code true} since Spring Framework 5.0. + * @since 4.3.6 + * @see DataSource#getConnection() + */ + public void setUseNewConnection(boolean useNewConnection) { + this.useNewConnection = useNewConnection; + } + + + @Override + protected synchronized long getNextKey() throws DataAccessException { + if (this.maxId == this.nextId) { + /* + * If useNewConnection is true, then we obtain a non-managed connection so our modifications + * are handled in a separate transaction. If it is false, then we use the current transaction's + * connection relying on the use of a non-transactional storage engine like MYISAM for the + * incrementer table. We also use straight JDBC code because we need to make sure that the insert + * and select are performed on the same connection (otherwise we can't be sure that last_insert_id() + * returned the correct value). + */ + Connection con = null; + Statement stmt = null; + boolean mustRestoreAutoCommit = false; + try { + if (this.useNewConnection) { + con = getDataSource().getConnection(); + if (con.getAutoCommit()) { + mustRestoreAutoCommit = true; + con.setAutoCommit(false); + } + } + else { + con = DataSourceUtils.getConnection(getDataSource()); + } + stmt = con.createStatement(); + if (!this.useNewConnection) { + DataSourceUtils.applyTransactionTimeout(stmt, getDataSource()); + } + // Increment the sequence column... + String columnName = getColumnName(); + try { + stmt.executeUpdate("update " + getIncrementerName() + " set " + columnName + + " = last_insert_id(" + columnName + " + " + getCacheSize() + ")"); + } + catch (SQLException ex) { + throw new DataAccessResourceFailureException("Could not increment " + columnName + " for " + + getIncrementerName() + " sequence table", ex); + } + // Retrieve the new max of the sequence column... + ResultSet rs = stmt.executeQuery(VALUE_SQL); + try { + if (!rs.next()) { + throw new DataAccessResourceFailureException("last_insert_id() failed after executing an update"); + } + this.maxId = rs.getLong(1); + } + finally { + JdbcUtils.closeResultSet(rs); + } + this.nextId = this.maxId - getCacheSize() + 1; + } + catch (SQLException ex) { + throw new DataAccessResourceFailureException("Could not obtain last_insert_id()", ex); + } + finally { + JdbcUtils.closeStatement(stmt); + if (con != null) { + if (this.useNewConnection) { + try { + con.commit(); + if (mustRestoreAutoCommit) { + con.setAutoCommit(true); + } + } + catch (SQLException ignore) { + throw new DataAccessResourceFailureException( + "Unable to commit new sequence value changes for " + getIncrementerName()); + } + JdbcUtils.closeConnection(con); + } + else { + DataSourceUtils.releaseConnection(con, getDataSource()); + } + } + } + } + else { + this.nextId++; + } + return this.nextId; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/OracleSequenceMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/OracleSequenceMaxValueIncrementer.java new file mode 100644 index 0000000..6568e48 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/OracleSequenceMaxValueIncrementer.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.incrementer; + +import javax.sql.DataSource; + +/** + * {@link DataFieldMaxValueIncrementer} that retrieves the next value + * of a given Oracle sequence. + * + * @author Dmitriy Kopylenko + * @author Thomas Risberg + * @author Juergen Hoeller + */ +public class OracleSequenceMaxValueIncrementer extends AbstractSequenceMaxValueIncrementer { + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + */ + public OracleSequenceMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + */ + public OracleSequenceMaxValueIncrementer(DataSource dataSource, String incrementerName) { + super(dataSource, incrementerName); + } + + + @Override + protected String getSequenceQuery() { + return "select " + getIncrementerName() + ".nextval from dual"; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/PostgreSQLSequenceMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/PostgreSQLSequenceMaxValueIncrementer.java new file mode 100644 index 0000000..0d02917 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/PostgreSQLSequenceMaxValueIncrementer.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.incrementer; + +import javax.sql.DataSource; + +/** + * {@link DataFieldMaxValueIncrementer} that retrieves the next value + * of a given PostgreSQL sequence. + * + *

    Thanks to Tomislav Urban for the suggestion! + * + * @author Juergen Hoeller + * @deprecated in favor of the differently named {@link PostgresSequenceMaxValueIncrementer} + */ +@Deprecated +public class PostgreSQLSequenceMaxValueIncrementer extends PostgresSequenceMaxValueIncrementer { + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + */ + public PostgreSQLSequenceMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + */ + public PostgreSQLSequenceMaxValueIncrementer(DataSource dataSource, String incrementerName) { + super(dataSource, incrementerName); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/PostgresSequenceMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/PostgresSequenceMaxValueIncrementer.java new file mode 100644 index 0000000..589ca91 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/PostgresSequenceMaxValueIncrementer.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.incrementer; + +import javax.sql.DataSource; + +/** + * {@link DataFieldMaxValueIncrementer} that retrieves the next value + * of a given PostgreSQL sequence. + * + *

    Thanks to Tomislav Urban for the suggestion! + * + * @author Juergen Hoeller + * @since 4.3.15 + */ +public class PostgresSequenceMaxValueIncrementer extends AbstractSequenceMaxValueIncrementer { + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + */ + public PostgresSequenceMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + */ + public PostgresSequenceMaxValueIncrementer(DataSource dataSource, String incrementerName) { + super(dataSource, incrementerName); + } + + + @Override + protected String getSequenceQuery() { + return "select nextval('" + getIncrementerName() + "')"; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/SqlServerMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/SqlServerMaxValueIncrementer.java new file mode 100644 index 0000000..016e78c --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/SqlServerMaxValueIncrementer.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.incrementer; + +import javax.sql.DataSource; + +/** + * {@link DataFieldMaxValueIncrementer} that increments the maximum value of a given SQL Server table + * with the equivalent of an auto-increment column. Note: If you use this class, your table key + * column should NOT be defined as an IDENTITY column, as the sequence table does the job. + * + *

    This class is intended to be used with Microsoft SQL Server. + * + *

    The sequence is kept in a table. There should be one sequence table per + * table that needs an auto-generated key. + * + *

    Example: + * + *

    create table tab (id int not null primary key, text varchar(100))
    + * create table tab_sequence (id bigint identity)
    + * insert into tab_sequence default values
    + * + * If "cacheSize" is set, the intermediate values are served without querying the + * database. If the server or your application is stopped or crashes or a transaction + * is rolled back, the unused values will never be served. The maximum hole size in + * numbering is consequently the value of cacheSize. + * + * HINT: Since Microsoft SQL Server supports the JDBC 3.0 {@code getGeneratedKeys} + * method, it is recommended to use IDENTITY columns directly in the tables and then using a + * {@link org.springframework.jdbc.core.simple.SimpleJdbcInsert} or utilizing + * a {@link org.springframework.jdbc.support.KeyHolder} when calling the with the + * {@code update(PreparedStatementCreator psc, KeyHolder generatedKeyHolder)} + * method of the {@link org.springframework.jdbc.core.JdbcTemplate}. + * + *

    Thanks to Preben Nilsson for the suggestion! + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.5.5 + */ +public class SqlServerMaxValueIncrementer extends AbstractIdentityColumnMaxValueIncrementer { + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + * @see #setColumnName + */ + public SqlServerMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + * @param columnName the name of the column in the sequence table to use + */ + public SqlServerMaxValueIncrementer(DataSource dataSource, String incrementerName, String columnName) { + super(dataSource, incrementerName, columnName); + } + + + @Override + protected String getIncrementStatement() { + return "insert into " + getIncrementerName() + " default values"; + } + + @Override + protected String getIdentityStatement() { + return "select @@identity"; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/SybaseAnywhereMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/SybaseAnywhereMaxValueIncrementer.java new file mode 100644 index 0000000..f970ac0 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/SybaseAnywhereMaxValueIncrementer.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.incrementer; + +import javax.sql.DataSource; + +/** + * {@link DataFieldMaxValueIncrementer} that increments the maximum value of a given Sybase table + * with the equivalent of an auto-increment column. Note: If you use this class, your table key + * column should NOT be defined as an IDENTITY column, as the sequence table does the job. + * + *

    This class is intended to be used with Sybase Anywhere. + * + *

    The sequence is kept in a table. There should be one sequence table per + * table that needs an auto-generated key. + * + *

    Example: + * + *

    create table tab (id int not null primary key, text varchar(100))
    + * create table tab_sequence (id bigint identity)
    + * insert into tab_sequence values(DEFAULT)
    + * + * If "cacheSize" is set, the intermediate values are served without querying the + * database. If the server or your application is stopped or crashes or a transaction + * is rolled back, the unused values will never be served. The maximum hole size in + * numbering is consequently the value of cacheSize. + * + * HINT: Since Sybase Anywhere supports the JDBC 3.0 {@code getGeneratedKeys} + * method, it is recommended to use IDENTITY columns directly in the tables and then + * using a {@link org.springframework.jdbc.core.simple.SimpleJdbcInsert} or utilizing + * a {@link org.springframework.jdbc.support.KeyHolder} when calling the with the + * {@code update(PreparedStatementCreator psc, KeyHolder generatedKeyHolder)} + * method of the {@link org.springframework.jdbc.core.JdbcTemplate}. + * + *

    Thanks to Tarald Saxi Stormark for the suggestion! + * + * @author Thomas Risberg + * @since 3.0.5 + */ +public class SybaseAnywhereMaxValueIncrementer extends SybaseMaxValueIncrementer { + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + * @see #setColumnName + */ + public SybaseAnywhereMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + * @param columnName the name of the column in the sequence table to use + */ + public SybaseAnywhereMaxValueIncrementer(DataSource dataSource, String incrementerName, String columnName) { + super(dataSource, incrementerName, columnName); + } + + + @Override + protected String getIncrementStatement() { + return "insert into " + getIncrementerName() + " values(DEFAULT)"; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/SybaseMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/SybaseMaxValueIncrementer.java new file mode 100644 index 0000000..f1d462e --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/SybaseMaxValueIncrementer.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.incrementer; + +import javax.sql.DataSource; + +/** + * {@link DataFieldMaxValueIncrementer} that increments the maximum value of a given Sybase table + * with the equivalent of an auto-increment column. Note: If you use this class, your table key + * column should NOT be defined as an IDENTITY column, as the sequence table does the job. + * + *

    This class is intended to be used with Sybase Adaptive Server. + * + *

    The sequence is kept in a table. There should be one sequence table per + * table that needs an auto-generated key. + * + *

    Example: + * + *

    create table tab (id int not null primary key, text varchar(100))
    + * create table tab_sequence (id bigint identity)
    + * insert into tab_sequence values()
    + * + * If "cacheSize" is set, the intermediate values are served without querying the + * database. If the server or your application is stopped or crashes or a transaction + * is rolled back, the unused values will never be served. The maximum hole size in + * numbering is consequently the value of cacheSize. + * + * HINT: Since Sybase Adaptive Server supports the JDBC 3.0 {@code getGeneratedKeys} + * method, it is recommended to use IDENTITY columns directly in the tables and then + * using a {@link org.springframework.jdbc.core.simple.SimpleJdbcInsert} or utilizing + * a {@link org.springframework.jdbc.support.KeyHolder} when calling the with the + * {@code update(PreparedStatementCreator psc, KeyHolder generatedKeyHolder)} + * method of the {@link org.springframework.jdbc.core.JdbcTemplate}. + * + *

    Thanks to Yinwei Liu for the suggestion! + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.5.5 + */ +public class SybaseMaxValueIncrementer extends AbstractIdentityColumnMaxValueIncrementer { + + /** + * Default constructor for bean property style usage. + * @see #setDataSource + * @see #setIncrementerName + * @see #setColumnName + */ + public SybaseMaxValueIncrementer() { + } + + /** + * Convenience constructor. + * @param dataSource the DataSource to use + * @param incrementerName the name of the sequence/table to use + * @param columnName the name of the column in the sequence table to use + */ + public SybaseMaxValueIncrementer(DataSource dataSource, String incrementerName, String columnName) { + super(dataSource, incrementerName, columnName); + } + + + @Override + protected String getIncrementStatement() { + return "insert into " + getIncrementerName() + " values()"; + } + + @Override + protected String getIdentityStatement() { + return "select @@identity"; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/package-info.java new file mode 100644 index 0000000..93364ca --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/package-info.java @@ -0,0 +1,7 @@ +/** + * Provides a support framework for incrementing database table values + * via sequences, with implementations for various databases. + * + *

    Can be used independently, for example in custom JDBC access code. + */ +package org.springframework.jdbc.support.incrementer; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/AbstractLobHandler.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/AbstractLobHandler.java new file mode 100644 index 0000000..8e1dd93 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/AbstractLobHandler.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.lob; + +import java.io.InputStream; +import java.io.Reader; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.springframework.lang.Nullable; + +/** + * Abstract base class for {@link LobHandler} implementations. + * + *

    Implements all accessor methods for column names through a column lookup + * and delegating to the corresponding accessor that takes a column index. + * + * @author Juergen Hoeller + * @since 1.2 + * @see java.sql.ResultSet#findColumn + */ +public abstract class AbstractLobHandler implements LobHandler { + + @Override + @Nullable + public byte[] getBlobAsBytes(ResultSet rs, String columnName) throws SQLException { + return getBlobAsBytes(rs, rs.findColumn(columnName)); + } + + @Override + @Nullable + public InputStream getBlobAsBinaryStream(ResultSet rs, String columnName) throws SQLException { + return getBlobAsBinaryStream(rs, rs.findColumn(columnName)); + } + + @Override + @Nullable + public String getClobAsString(ResultSet rs, String columnName) throws SQLException { + return getClobAsString(rs, rs.findColumn(columnName)); + } + + @Override + @Nullable + public InputStream getClobAsAsciiStream(ResultSet rs, String columnName) throws SQLException { + return getClobAsAsciiStream(rs, rs.findColumn(columnName)); + } + + @Override + public Reader getClobAsCharacterStream(ResultSet rs, String columnName) throws SQLException { + return getClobAsCharacterStream(rs, rs.findColumn(columnName)); + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/DefaultLobHandler.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/DefaultLobHandler.java new file mode 100644 index 0000000..e5cc443 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/DefaultLobHandler.java @@ -0,0 +1,404 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.lob; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.lang.Nullable; + +/** + * Default implementation of the {@link LobHandler} interface. + * Invokes the direct accessor methods that {@code java.sql.ResultSet} + * and {@code java.sql.PreparedStatement} offer. + * + *

    By default, incoming streams are going to be passed to the appropriate + * {@code setBinary/Ascii/CharacterStream} method on the JDBC driver's + * {@link PreparedStatement}. If the specified content length is negative, + * this handler will use the JDBC 4.0 variants of the set-stream methods + * without a length parameter; otherwise, it will pass the specified length + * on to the driver. + * + *

    This LobHandler should work for any JDBC driver that is JDBC compliant + * in terms of the spec's suggestions regarding simple BLOB and CLOB handling. + * This does not apply to Oracle 9i's drivers at all; as of Oracle 10g, + * it does work but may still come with LOB size limitations. Consider using + * recent Oracle drivers even when working against an older database server. + * See the {@link LobHandler} javadoc for the full set of recommendations. + * + *

    Some JDBC drivers require values with a BLOB/CLOB target column to be + * explicitly set through the JDBC {@code setBlob} / {@code setClob} API: + * for example, PostgreSQL's driver. Switch the {@link #setWrapAsLob "wrapAsLob"} + * property to "true" when operating against such a driver. + * + *

    On JDBC 4.0, this LobHandler also supports streaming the BLOB/CLOB content + * via the {@code setBlob} / {@code setClob} variants that take a stream + * argument directly. Consider switching the {@link #setStreamAsLob "streamAsLob"} + * property to "true" when operating against a fully compliant JDBC 4.0 driver. + * + *

    Finally, this LobHandler also supports the creation of temporary BLOB/CLOB + * objects. Consider switching the {@link #setCreateTemporaryLob "createTemporaryLob"} + * property to "true" when "streamAsLob" happens to run into LOB size limitations. + * + *

    See the {@link LobHandler} interface javadoc for a summary of recommendations. + * + * @author Juergen Hoeller + * @since 04.12.2003 + * @see java.sql.ResultSet#getBytes + * @see java.sql.ResultSet#getBinaryStream + * @see java.sql.ResultSet#getString + * @see java.sql.ResultSet#getAsciiStream + * @see java.sql.ResultSet#getCharacterStream + * @see java.sql.PreparedStatement#setBytes + * @see java.sql.PreparedStatement#setBinaryStream + * @see java.sql.PreparedStatement#setString + * @see java.sql.PreparedStatement#setAsciiStream + * @see java.sql.PreparedStatement#setCharacterStream + */ +public class DefaultLobHandler extends AbstractLobHandler { + + protected final Log logger = LogFactory.getLog(getClass()); + + private boolean wrapAsLob = false; + + private boolean streamAsLob = false; + + private boolean createTemporaryLob = false; + + + /** + * Specify whether to submit a byte array / String to the JDBC driver + * wrapped in a JDBC Blob / Clob object, using the JDBC {@code setBlob} / + * {@code setClob} method with a Blob / Clob argument. + *

    Default is "false", using the common JDBC 2.0 {@code setBinaryStream} + * / {@code setCharacterStream} method for setting the content. Switch this + * to "true" for explicit Blob / Clob wrapping against JDBC drivers that + * are known to require such wrapping (e.g. PostgreSQL's for access to OID + * columns, whereas BYTEA columns need to be accessed the standard way). + *

    This setting affects byte array / String arguments as well as stream + * arguments, unless {@link #setStreamAsLob "streamAsLob"} overrides this + * handling to use JDBC 4.0's new explicit streaming support (if available). + * @see java.sql.PreparedStatement#setBlob(int, java.sql.Blob) + * @see java.sql.PreparedStatement#setClob(int, java.sql.Clob) + */ + public void setWrapAsLob(boolean wrapAsLob) { + this.wrapAsLob = wrapAsLob; + } + + /** + * Specify whether to submit a binary stream / character stream to the JDBC + * driver as explicit LOB content, using the JDBC 4.0 {@code setBlob} / + * {@code setClob} method with a stream argument. + *

    Default is "false", using the common JDBC 2.0 {@code setBinaryStream} + * / {@code setCharacterStream} method for setting the content. + * Switch this to "true" for explicit JDBC 4.0 streaming, provided that your + * JDBC driver actually supports those JDBC 4.0 operations (e.g. Derby's). + *

    This setting affects stream arguments as well as byte array / String + * arguments, requiring JDBC 4.0 support. For supporting LOB content against + * JDBC 3.0, check out the {@link #setWrapAsLob "wrapAsLob"} setting. + * @see java.sql.PreparedStatement#setBlob(int, java.io.InputStream, long) + * @see java.sql.PreparedStatement#setClob(int, java.io.Reader, long) + */ + public void setStreamAsLob(boolean streamAsLob) { + this.streamAsLob = streamAsLob; + } + + /** + * Specify whether to copy a byte array / String into a temporary JDBC + * Blob / Clob object created through the JDBC 4.0 {@code createBlob} / + * {@code createClob} methods. + *

    Default is "false", using the common JDBC 2.0 {@code setBinaryStream} + * / {@code setCharacterStream} method for setting the content. Switch this + * to "true" for explicit Blob / Clob creation using JDBC 4.0. + *

    This setting affects stream arguments as well as byte array / String + * arguments, requiring JDBC 4.0 support. For supporting LOB content against + * JDBC 3.0, check out the {@link #setWrapAsLob "wrapAsLob"} setting. + * @see java.sql.Connection#createBlob() + * @see java.sql.Connection#createClob() + */ + public void setCreateTemporaryLob(boolean createTemporaryLob) { + this.createTemporaryLob = createTemporaryLob; + } + + + @Override + @Nullable + public byte[] getBlobAsBytes(ResultSet rs, int columnIndex) throws SQLException { + logger.debug("Returning BLOB as bytes"); + if (this.wrapAsLob) { + Blob blob = rs.getBlob(columnIndex); + return blob.getBytes(1, (int) blob.length()); + } + else { + return rs.getBytes(columnIndex); + } + } + + @Override + @Nullable + public InputStream getBlobAsBinaryStream(ResultSet rs, int columnIndex) throws SQLException { + logger.debug("Returning BLOB as binary stream"); + if (this.wrapAsLob) { + Blob blob = rs.getBlob(columnIndex); + return blob.getBinaryStream(); + } + else { + return rs.getBinaryStream(columnIndex); + } + } + + @Override + @Nullable + public String getClobAsString(ResultSet rs, int columnIndex) throws SQLException { + logger.debug("Returning CLOB as string"); + if (this.wrapAsLob) { + Clob clob = rs.getClob(columnIndex); + return clob.getSubString(1, (int) clob.length()); + } + else { + return rs.getString(columnIndex); + } + } + + @Override + public InputStream getClobAsAsciiStream(ResultSet rs, int columnIndex) throws SQLException { + logger.debug("Returning CLOB as ASCII stream"); + if (this.wrapAsLob) { + Clob clob = rs.getClob(columnIndex); + return clob.getAsciiStream(); + } + else { + return rs.getAsciiStream(columnIndex); + } + } + + @Override + public Reader getClobAsCharacterStream(ResultSet rs, int columnIndex) throws SQLException { + logger.debug("Returning CLOB as character stream"); + if (this.wrapAsLob) { + Clob clob = rs.getClob(columnIndex); + return clob.getCharacterStream(); + } + else { + return rs.getCharacterStream(columnIndex); + } + } + + @Override + public LobCreator getLobCreator() { + return (this.createTemporaryLob ? new TemporaryLobCreator() : new DefaultLobCreator()); + } + + + /** + * Default LobCreator implementation as an inner class. + * Can be subclassed in DefaultLobHandler extensions. + */ + protected class DefaultLobCreator implements LobCreator { + + @Override + public void setBlobAsBytes(PreparedStatement ps, int paramIndex, @Nullable byte[] content) + throws SQLException { + + if (streamAsLob) { + if (content != null) { + ps.setBlob(paramIndex, new ByteArrayInputStream(content), content.length); + } + else { + ps.setBlob(paramIndex, (Blob) null); + } + } + else if (wrapAsLob) { + if (content != null) { + ps.setBlob(paramIndex, new PassThroughBlob(content)); + } + else { + ps.setBlob(paramIndex, (Blob) null); + } + } + else { + ps.setBytes(paramIndex, content); + } + if (logger.isDebugEnabled()) { + logger.debug(content != null ? "Set bytes for BLOB with length " + content.length : + "Set BLOB to null"); + } + } + + @Override + public void setBlobAsBinaryStream( + PreparedStatement ps, int paramIndex, @Nullable InputStream binaryStream, int contentLength) + throws SQLException { + + if (streamAsLob) { + if (binaryStream != null) { + if (contentLength >= 0) { + ps.setBlob(paramIndex, binaryStream, contentLength); + } + else { + ps.setBlob(paramIndex, binaryStream); + } + } + else { + ps.setBlob(paramIndex, (Blob) null); + } + } + else if (wrapAsLob) { + if (binaryStream != null) { + ps.setBlob(paramIndex, new PassThroughBlob(binaryStream, contentLength)); + } + else { + ps.setBlob(paramIndex, (Blob) null); + } + } + else if (contentLength >= 0) { + ps.setBinaryStream(paramIndex, binaryStream, contentLength); + } + else { + ps.setBinaryStream(paramIndex, binaryStream); + } + if (logger.isDebugEnabled()) { + logger.debug(binaryStream != null ? "Set binary stream for BLOB with length " + contentLength : + "Set BLOB to null"); + } + } + + @Override + public void setClobAsString(PreparedStatement ps, int paramIndex, @Nullable String content) + throws SQLException { + + if (streamAsLob) { + if (content != null) { + ps.setClob(paramIndex, new StringReader(content), content.length()); + } + else { + ps.setClob(paramIndex, (Clob) null); + } + } + else if (wrapAsLob) { + if (content != null) { + ps.setClob(paramIndex, new PassThroughClob(content)); + } + else { + ps.setClob(paramIndex, (Clob) null); + } + } + else { + ps.setString(paramIndex, content); + } + if (logger.isDebugEnabled()) { + logger.debug(content != null ? "Set string for CLOB with length " + content.length() : + "Set CLOB to null"); + } + } + + @Override + public void setClobAsAsciiStream( + PreparedStatement ps, int paramIndex, @Nullable InputStream asciiStream, int contentLength) + throws SQLException { + + if (streamAsLob) { + if (asciiStream != null) { + Reader reader = new InputStreamReader(asciiStream, StandardCharsets.US_ASCII); + if (contentLength >= 0) { + ps.setClob(paramIndex, reader, contentLength); + } + else { + ps.setClob(paramIndex, reader); + } + } + else { + ps.setClob(paramIndex, (Clob) null); + } + } + else if (wrapAsLob) { + if (asciiStream != null) { + ps.setClob(paramIndex, new PassThroughClob(asciiStream, contentLength)); + } + else { + ps.setClob(paramIndex, (Clob) null); + } + } + else if (contentLength >= 0) { + ps.setAsciiStream(paramIndex, asciiStream, contentLength); + } + else { + ps.setAsciiStream(paramIndex, asciiStream); + } + if (logger.isDebugEnabled()) { + logger.debug(asciiStream != null ? "Set ASCII stream for CLOB with length " + contentLength : + "Set CLOB to null"); + } + } + + @Override + public void setClobAsCharacterStream( + PreparedStatement ps, int paramIndex, @Nullable Reader characterStream, int contentLength) + throws SQLException { + + if (streamAsLob) { + if (characterStream != null) { + if (contentLength >= 0) { + ps.setClob(paramIndex, characterStream, contentLength); + } + else { + ps.setClob(paramIndex, characterStream); + } + } + else { + ps.setClob(paramIndex, (Clob) null); + } + } + else if (wrapAsLob) { + if (characterStream != null) { + ps.setClob(paramIndex, new PassThroughClob(characterStream, contentLength)); + } + else { + ps.setClob(paramIndex, (Clob) null); + } + } + else if (contentLength >= 0) { + ps.setCharacterStream(paramIndex, characterStream, contentLength); + } + else { + ps.setCharacterStream(paramIndex, characterStream); + } + if (logger.isDebugEnabled()) { + logger.debug(characterStream != null ? "Set character stream for CLOB with length " + contentLength : + "Set CLOB to null"); + } + } + + @Override + public void close() { + // nothing to do when not creating temporary LOBs + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/LobCreator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/LobCreator.java new file mode 100644 index 0000000..e8edd9c --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/LobCreator.java @@ -0,0 +1,142 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.lob; + +import java.io.Closeable; +import java.io.InputStream; +import java.io.Reader; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.springframework.lang.Nullable; + +/** + * Interface that abstracts potentially database-specific creation of large binary + * fields and large text fields. Does not work with {@code java.sql.Blob} + * and {@code java.sql.Clob} instances in the API, as some JDBC drivers + * do not support these types as such. + * + *

    The LOB creation part is where {@link LobHandler} implementations usually + * differ. Possible strategies include usage of + * {@code PreparedStatement.setBinaryStream/setCharacterStream} but also + * {@code PreparedStatement.setBlob/setClob} with either a stream argument + * (requires JDBC 4.0) or {@code java.sql.Blob/Clob} wrapper objects. + * + *

    A LobCreator represents a session for creating BLOBs: It is not + * thread-safe and needs to be instantiated for each statement execution or for + * each transaction. Each LobCreator needs to be closed after completion. + * + *

    For convenient working with a PreparedStatement and a LobCreator, + * consider using {@link org.springframework.jdbc.core.JdbcTemplate} with an + *{@link org.springframework.jdbc.core.support.AbstractLobCreatingPreparedStatementCallback} + * implementation. See the latter's javadoc for details. + * + * @author Juergen Hoeller + * @since 04.12.2003 + * @see #close() + * @see LobHandler#getLobCreator() + * @see DefaultLobHandler.DefaultLobCreator + * @see java.sql.PreparedStatement#setBlob + * @see java.sql.PreparedStatement#setClob + * @see java.sql.PreparedStatement#setBytes + * @see java.sql.PreparedStatement#setBinaryStream + * @see java.sql.PreparedStatement#setString + * @see java.sql.PreparedStatement#setAsciiStream + * @see java.sql.PreparedStatement#setCharacterStream + */ +public interface LobCreator extends Closeable { + + /** + * Set the given content as bytes on the given statement, using the given + * parameter index. Might simply invoke {@code PreparedStatement.setBytes} + * or create a Blob instance for it, depending on the database and driver. + * @param ps the PreparedStatement to the set the content on + * @param paramIndex the parameter index to use + * @param content the content as byte array, or {@code null} for SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.PreparedStatement#setBytes + */ + void setBlobAsBytes(PreparedStatement ps, int paramIndex, @Nullable byte[] content) + throws SQLException; + + /** + * Set the given content as binary stream on the given statement, using the given + * parameter index. Might simply invoke {@code PreparedStatement.setBinaryStream} + * or create a Blob instance for it, depending on the database and driver. + * @param ps the PreparedStatement to the set the content on + * @param paramIndex the parameter index to use + * @param contentStream the content as binary stream, or {@code null} for SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.PreparedStatement#setBinaryStream + */ + void setBlobAsBinaryStream( + PreparedStatement ps, int paramIndex, @Nullable InputStream contentStream, int contentLength) + throws SQLException; + + /** + * Set the given content as String on the given statement, using the given + * parameter index. Might simply invoke {@code PreparedStatement.setString} + * or create a Clob instance for it, depending on the database and driver. + * @param ps the PreparedStatement to the set the content on + * @param paramIndex the parameter index to use + * @param content the content as String, or {@code null} for SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.PreparedStatement#setBytes + */ + void setClobAsString(PreparedStatement ps, int paramIndex, @Nullable String content) + throws SQLException; + + /** + * Set the given content as ASCII stream on the given statement, using the given + * parameter index. Might simply invoke {@code PreparedStatement.setAsciiStream} + * or create a Clob instance for it, depending on the database and driver. + * @param ps the PreparedStatement to the set the content on + * @param paramIndex the parameter index to use + * @param asciiStream the content as ASCII stream, or {@code null} for SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.PreparedStatement#setAsciiStream + */ + void setClobAsAsciiStream( + PreparedStatement ps, int paramIndex, @Nullable InputStream asciiStream, int contentLength) + throws SQLException; + + /** + * Set the given content as character stream on the given statement, using the given + * parameter index. Might simply invoke {@code PreparedStatement.setCharacterStream} + * or create a Clob instance for it, depending on the database and driver. + * @param ps the PreparedStatement to the set the content on + * @param paramIndex the parameter index to use + * @param characterStream the content as character stream, or {@code null} for SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.PreparedStatement#setCharacterStream + */ + void setClobAsCharacterStream( + PreparedStatement ps, int paramIndex, @Nullable Reader characterStream, int contentLength) + throws SQLException; + + /** + * Close this LobCreator session and free its temporarily created BLOBs and CLOBs. + * Will not need to do anything if using PreparedStatement's standard methods, + * but might be necessary to free database resources if using proprietary means. + *

    NOTE: Needs to be invoked after the involved PreparedStatements have + * been executed or the affected O/R mapping sessions have been flushed. + * Otherwise, the database resources for the temporary BLOBs might stay allocated. + */ + @Override + void close(); + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/LobHandler.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/LobHandler.java new file mode 100644 index 0000000..8bacfea --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/LobHandler.java @@ -0,0 +1,215 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.lob; + +import java.io.InputStream; +import java.io.Reader; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.springframework.lang.Nullable; + +/** + * Abstraction for handling large binary fields and large text fields in + * specific databases, no matter if represented as simple types or Large OBjects. + * + *

    Provides accessor methods for BLOBs and CLOBs, and acts as factory for + * LobCreator instances, to be used as sessions for creating BLOBs or CLOBs. + * LobCreators are typically instantiated for each statement execution or for + * each transaction; they are not thread-safe because they might track + * allocated database resources in order to free them after execution. + * + *

    Most databases/drivers should be able to work with {@link DefaultLobHandler}, + * which by default delegates to JDBC's direct accessor methods, avoiding the + * {@code java.sql.Blob} and {@code java.sql.Clob} API completely. + * {@link DefaultLobHandler} can also be configured to access LOBs using + * {@code PreparedStatement.setBlob/setClob} (e.g. for PostgreSQL), through + * setting the {@link DefaultLobHandler#setWrapAsLob "wrapAsLob"} property. + * + *

    Of course, you need to declare different field types for each database. + * In Oracle, any binary content needs to go into a BLOB, and all character content + * beyond 4000 bytes needs to go into a CLOB. In MySQL, there is no notion of a + * CLOB type but rather a LONGTEXT type that behaves like a VARCHAR. For complete + * portability, use a LobHandler for fields that might typically require LOBs on + * some database because of the field size (take Oracle's numbers as a guideline). + * + *

    Summarizing the recommended options (for actual LOB fields): + *

      + *
    • JDBC 4.0 driver (including Oracle 11g driver): Use {@link DefaultLobHandler}, + * potentially with {@code streamAsLob=true} if your database driver requires that + * hint when populating a LOB field. Fall back to {@code createTemporaryLob=true} + * if you happen to run into LOB size limitations with your (Oracle) database setup. + *
    • Oracle 10g driver: Use {@link DefaultLobHandler} with standard setup. + * On Oracle 10.1, set the "SetBigStringTryClob" connection property; as of Oracle 10.2, + * DefaultLobHandler should work with standard setup out of the box. + *
    • PostgreSQL: Configure {@link DefaultLobHandler} with {@code wrapAsLob=true}, + * and use that LobHandler to access OID columns (but not BYTEA) in your database tables. + *
    • For all other database drivers (and for non-LOB fields that might potentially + * turn into LOBs on some databases): Simply use a plain {@link DefaultLobHandler}. + *
    + * + * @author Juergen Hoeller + * @since 23.12.2003 + * @see DefaultLobHandler + * @see java.sql.ResultSet#getBlob + * @see java.sql.ResultSet#getClob + * @see java.sql.ResultSet#getBytes + * @see java.sql.ResultSet#getBinaryStream + * @see java.sql.ResultSet#getString + * @see java.sql.ResultSet#getAsciiStream + * @see java.sql.ResultSet#getCharacterStream + */ +public interface LobHandler { + + /** + * Retrieve the given column as bytes from the given ResultSet. + * Might simply invoke {@code ResultSet.getBytes} or work with + * {@code ResultSet.getBlob}, depending on the database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnName the column name to use + * @return the content as byte array, or {@code null} in case of SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getBytes + */ + @Nullable + byte[] getBlobAsBytes(ResultSet rs, String columnName) throws SQLException; + + /** + * Retrieve the given column as bytes from the given ResultSet. + * Might simply invoke {@code ResultSet.getBytes} or work with + * {@code ResultSet.getBlob}, depending on the database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnIndex the column index to use + * @return the content as byte array, or {@code null} in case of SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getBytes + */ + @Nullable + byte[] getBlobAsBytes(ResultSet rs, int columnIndex) throws SQLException; + + /** + * Retrieve the given column as binary stream from the given ResultSet. + * Might simply invoke {@code ResultSet.getBinaryStream} or work with + * {@code ResultSet.getBlob}, depending on the database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnName the column name to use + * @return the content as binary stream, or {@code null} in case of SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getBinaryStream + */ + @Nullable + InputStream getBlobAsBinaryStream(ResultSet rs, String columnName) throws SQLException; + + /** + * Retrieve the given column as binary stream from the given ResultSet. + * Might simply invoke {@code ResultSet.getBinaryStream} or work with + * {@code ResultSet.getBlob}, depending on the database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnIndex the column index to use + * @return the content as binary stream, or {@code null} in case of SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getBinaryStream + */ + @Nullable + InputStream getBlobAsBinaryStream(ResultSet rs, int columnIndex) throws SQLException; + + /** + * Retrieve the given column as String from the given ResultSet. + * Might simply invoke {@code ResultSet.getString} or work with + * {@code ResultSet.getClob}, depending on the database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnName the column name to use + * @return the content as String, or {@code null} in case of SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getString + */ + @Nullable + String getClobAsString(ResultSet rs, String columnName) throws SQLException; + + /** + * Retrieve the given column as String from the given ResultSet. + * Might simply invoke {@code ResultSet.getString} or work with + * {@code ResultSet.getClob}, depending on the database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnIndex the column index to use + * @return the content as String, or {@code null} in case of SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getString + */ + @Nullable + String getClobAsString(ResultSet rs, int columnIndex) throws SQLException; + + /** + * Retrieve the given column as ASCII stream from the given ResultSet. + * Might simply invoke {@code ResultSet.getAsciiStream} or work with + * {@code ResultSet.getClob}, depending on the database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnName the column name to use + * @return the content as ASCII stream, or {@code null} in case of SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getAsciiStream + */ + @Nullable + InputStream getClobAsAsciiStream(ResultSet rs, String columnName) throws SQLException; + + /** + * Retrieve the given column as ASCII stream from the given ResultSet. + * Might simply invoke {@code ResultSet.getAsciiStream} or work with + * {@code ResultSet.getClob}, depending on the database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnIndex the column index to use + * @return the content as ASCII stream, or {@code null} in case of SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getAsciiStream + */ + @Nullable + InputStream getClobAsAsciiStream(ResultSet rs, int columnIndex) throws SQLException; + + /** + * Retrieve the given column as character stream from the given ResultSet. + * Might simply invoke {@code ResultSet.getCharacterStream} or work with + * {@code ResultSet.getClob}, depending on the database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnName the column name to use + * @return the content as character stream + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getCharacterStream + */ + Reader getClobAsCharacterStream(ResultSet rs, String columnName) throws SQLException; + + /** + * Retrieve the given column as character stream from the given ResultSet. + * Might simply invoke {@code ResultSet.getCharacterStream} or work with + * {@code ResultSet.getClob}, depending on the database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnIndex the column index to use + * @return the content as character stream + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getCharacterStream + */ + Reader getClobAsCharacterStream(ResultSet rs, int columnIndex) throws SQLException; + + /** + * Create a new {@link LobCreator} instance, i.e. a session for creating BLOBs + * and CLOBs. Needs to be closed after the created LOBs are not needed anymore - + * typically after statement execution or transaction completion. + * @return the new LobCreator instance + * @see LobCreator#close() + */ + LobCreator getLobCreator(); + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/PassThroughBlob.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/PassThroughBlob.java new file mode 100644 index 0000000..ad50eeb --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/PassThroughBlob.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.lob; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.sql.Blob; +import java.sql.SQLException; + +import org.springframework.lang.Nullable; +import org.springframework.util.StreamUtils; + +/** + * Simple JDBC {@link Blob} adapter that exposes a given byte array or binary stream. + * Optionally used by {@link DefaultLobHandler}. + * + * @author Juergen Hoeller + * @since 2.5.3 + */ +class PassThroughBlob implements Blob { + + @Nullable + private byte[] content; + + @Nullable + private InputStream binaryStream; + + private long contentLength; + + + public PassThroughBlob(byte[] content) { + this.content = content; + this.contentLength = content.length; + } + + public PassThroughBlob(InputStream binaryStream, long contentLength) { + this.binaryStream = binaryStream; + this.contentLength = contentLength; + } + + + @Override + public long length() throws SQLException { + return this.contentLength; + } + + @Override + public InputStream getBinaryStream() throws SQLException { + if (this.content != null) { + return new ByteArrayInputStream(this.content); + } + else { + return (this.binaryStream != null ? this.binaryStream : StreamUtils.emptyInput()); + } + } + + + @Override + public InputStream getBinaryStream(long pos, long length) throws SQLException { + throw new UnsupportedOperationException(); + } + + @Override + public OutputStream setBinaryStream(long pos) throws SQLException { + throw new UnsupportedOperationException(); + } + + @Override + public byte[] getBytes(long pos, int length) throws SQLException { + throw new UnsupportedOperationException(); + } + + @Override + public int setBytes(long pos, byte[] bytes) throws SQLException { + throw new UnsupportedOperationException(); + } + + @Override + public int setBytes(long pos, byte[] bytes, int offset, int len) throws SQLException { + throw new UnsupportedOperationException(); + } + + @Override + public long position(byte[] pattern, long start) throws SQLException { + throw new UnsupportedOperationException(); + } + + @Override + public long position(Blob pattern, long start) throws SQLException { + throw new UnsupportedOperationException(); + } + + @Override + public void truncate(long len) throws SQLException { + throw new UnsupportedOperationException(); + } + + @Override + public void free() throws SQLException { + // no-op + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/PassThroughClob.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/PassThroughClob.java new file mode 100644 index 0000000..0f4d25a --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/PassThroughClob.java @@ -0,0 +1,162 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.lob; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.Reader; +import java.io.StringReader; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.sql.Clob; +import java.sql.SQLException; + +import org.springframework.lang.Nullable; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StreamUtils; + +/** + * Simple JDBC {@link Clob} adapter that exposes a given String or character stream. + * Optionally used by {@link DefaultLobHandler}. + * + * @author Juergen Hoeller + * @since 2.5.3 + */ +class PassThroughClob implements Clob { + + @Nullable + private String content; + + @Nullable + private Reader characterStream; + + @Nullable + private InputStream asciiStream; + + private long contentLength; + + + public PassThroughClob(String content) { + this.content = content; + this.contentLength = content.length(); + } + + public PassThroughClob(Reader characterStream, long contentLength) { + this.characterStream = characterStream; + this.contentLength = contentLength; + } + + public PassThroughClob(InputStream asciiStream, long contentLength) { + this.asciiStream = asciiStream; + this.contentLength = contentLength; + } + + + @Override + public long length() throws SQLException { + return this.contentLength; + } + + @Override + public Reader getCharacterStream() throws SQLException { + if (this.content != null) { + return new StringReader(this.content); + } + else if (this.characterStream != null) { + return this.characterStream; + } + else { + return new InputStreamReader( + (this.asciiStream != null ? this.asciiStream : StreamUtils.emptyInput()), + StandardCharsets.US_ASCII); + } + } + + @Override + public InputStream getAsciiStream() throws SQLException { + try { + if (this.content != null) { + return new ByteArrayInputStream(this.content.getBytes(StandardCharsets.US_ASCII)); + } + else if (this.characterStream != null) { + String tempContent = FileCopyUtils.copyToString(this.characterStream); + return new ByteArrayInputStream(tempContent.getBytes(StandardCharsets.US_ASCII)); + } + else { + return (this.asciiStream != null ? this.asciiStream : StreamUtils.emptyInput()); + } + } + catch (IOException ex) { + throw new SQLException("Failed to read stream content: " + ex); + } + } + + + @Override + public Reader getCharacterStream(long pos, long length) throws SQLException { + throw new UnsupportedOperationException(); + } + + @Override + public Writer setCharacterStream(long pos) throws SQLException { + throw new UnsupportedOperationException(); + } + + @Override + public OutputStream setAsciiStream(long pos) throws SQLException { + throw new UnsupportedOperationException(); + } + + @Override + public String getSubString(long pos, int length) throws SQLException { + throw new UnsupportedOperationException(); + } + + @Override + public int setString(long pos, String str) throws SQLException { + throw new UnsupportedOperationException(); + } + + @Override + public int setString(long pos, String str, int offset, int len) throws SQLException { + throw new UnsupportedOperationException(); + } + + @Override + public long position(String searchstr, long start) throws SQLException { + throw new UnsupportedOperationException(); + } + + @Override + public long position(Clob searchstr, long start) throws SQLException { + throw new UnsupportedOperationException(); + } + + @Override + public void truncate(long len) throws SQLException { + throw new UnsupportedOperationException(); + } + + @Override + public void free() throws SQLException { + // no-op + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/TemporaryLobCreator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/TemporaryLobCreator.java new file mode 100644 index 0000000..052ae0d --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/TemporaryLobCreator.java @@ -0,0 +1,200 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.lob; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.lang.Nullable; +import org.springframework.util.FileCopyUtils; + +/** + * {@link LobCreator} implementation based on temporary LOBs, + * using JDBC 4.0's {@link java.sql.Connection#createBlob()} / + * {@link java.sql.Connection#createClob()} mechanism. + * + *

    Used by DefaultLobHandler's {@link DefaultLobHandler#setCreateTemporaryLob} mode. + * Can also be used directly to reuse the tracking and freeing of temporary LOBs. + * + * @author Juergen Hoeller + * @since 3.2.2 + * @see DefaultLobHandler#setCreateTemporaryLob + * @see java.sql.Connection#createBlob() + * @see java.sql.Connection#createClob() + */ +public class TemporaryLobCreator implements LobCreator { + + protected static final Log logger = LogFactory.getLog(TemporaryLobCreator.class); + + private final Set temporaryBlobs = new LinkedHashSet<>(1); + + private final Set temporaryClobs = new LinkedHashSet<>(1); + + + @Override + public void setBlobAsBytes(PreparedStatement ps, int paramIndex, @Nullable byte[] content) + throws SQLException { + + if (content != null) { + Blob blob = ps.getConnection().createBlob(); + blob.setBytes(1, content); + this.temporaryBlobs.add(blob); + ps.setBlob(paramIndex, blob); + } + else { + ps.setBlob(paramIndex, (Blob) null); + } + + if (logger.isDebugEnabled()) { + logger.debug(content != null ? "Copied bytes into temporary BLOB with length " + content.length : + "Set BLOB to null"); + } + } + + @Override + public void setBlobAsBinaryStream( + PreparedStatement ps, int paramIndex, @Nullable InputStream binaryStream, int contentLength) + throws SQLException { + + if (binaryStream != null) { + Blob blob = ps.getConnection().createBlob(); + try { + FileCopyUtils.copy(binaryStream, blob.setBinaryStream(1)); + } + catch (IOException ex) { + throw new DataAccessResourceFailureException("Could not copy into LOB stream", ex); + } + this.temporaryBlobs.add(blob); + ps.setBlob(paramIndex, blob); + } + else { + ps.setBlob(paramIndex, (Blob) null); + } + + if (logger.isDebugEnabled()) { + logger.debug(binaryStream != null ? + "Copied binary stream into temporary BLOB with length " + contentLength : + "Set BLOB to null"); + } + } + + @Override + public void setClobAsString(PreparedStatement ps, int paramIndex, @Nullable String content) + throws SQLException { + + if (content != null) { + Clob clob = ps.getConnection().createClob(); + clob.setString(1, content); + this.temporaryClobs.add(clob); + ps.setClob(paramIndex, clob); + } + else { + ps.setClob(paramIndex, (Clob) null); + } + + if (logger.isDebugEnabled()) { + logger.debug(content != null ? "Copied string into temporary CLOB with length " + content.length() : + "Set CLOB to null"); + } + } + + @Override + public void setClobAsAsciiStream( + PreparedStatement ps, int paramIndex, @Nullable InputStream asciiStream, int contentLength) + throws SQLException { + + if (asciiStream != null) { + Clob clob = ps.getConnection().createClob(); + try { + FileCopyUtils.copy(asciiStream, clob.setAsciiStream(1)); + } + catch (IOException ex) { + throw new DataAccessResourceFailureException("Could not copy into LOB stream", ex); + } + this.temporaryClobs.add(clob); + ps.setClob(paramIndex, clob); + } + else { + ps.setClob(paramIndex, (Clob) null); + } + + if (logger.isDebugEnabled()) { + logger.debug(asciiStream != null ? + "Copied ASCII stream into temporary CLOB with length " + contentLength : + "Set CLOB to null"); + } + } + + @Override + public void setClobAsCharacterStream( + PreparedStatement ps, int paramIndex, @Nullable Reader characterStream, int contentLength) + throws SQLException { + + if (characterStream != null) { + Clob clob = ps.getConnection().createClob(); + try { + FileCopyUtils.copy(characterStream, clob.setCharacterStream(1)); + } + catch (IOException ex) { + throw new DataAccessResourceFailureException("Could not copy into LOB stream", ex); + } + this.temporaryClobs.add(clob); + ps.setClob(paramIndex, clob); + } + else { + ps.setClob(paramIndex, (Clob) null); + } + + if (logger.isDebugEnabled()) { + logger.debug(characterStream != null ? + "Copied character stream into temporary CLOB with length " + contentLength : + "Set CLOB to null"); + } + } + + @Override + public void close() { + for (Blob blob : this.temporaryBlobs) { + try { + blob.free(); + } + catch (SQLException ex) { + logger.warn("Could not free BLOB", ex); + } + } + for (Clob clob : this.temporaryClobs) { + try { + clob.free(); + } + catch (SQLException ex) { + logger.warn("Could not free CLOB", ex); + } + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/package-info.java new file mode 100644 index 0000000..a07f8da --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/package-info.java @@ -0,0 +1,10 @@ +/** + * Provides a strategy interface for Large OBject handling, + * as well as a customizable default implementation. + */ +@NonNullApi +@NonNullFields +package org.springframework.jdbc.support.lob; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/package-info.java new file mode 100644 index 0000000..bd86887 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/package-info.java @@ -0,0 +1,14 @@ +/** + * Support classes for the JDBC framework, used by the classes in the + * jdbc.core and jdbc.object packages. Provides a translator from + * SQLExceptions Spring's generic DataAccessExceptions. + * + *

    Can be used independently, for example in custom JDBC access code, + * or in JDBC-based O/R mapping layers. + */ +@NonNullApi +@NonNullFields +package org.springframework.jdbc.support; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSet.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSet.java new file mode 100644 index 0000000..c399afa --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSet.java @@ -0,0 +1,768 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.rowset; + +import java.math.BigDecimal; +import java.sql.Date; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Collections; +import java.util.Map; + +import org.springframework.jdbc.InvalidResultSetAccessException; +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; + +/** + * The default implementation of Spring's {@link SqlRowSet} interface, wrapping a + * {@link java.sql.ResultSet}, catching any {@link SQLException SQLExceptions} and + * translating them to a corresponding Spring {@link InvalidResultSetAccessException}. + * + *

    The passed-in ResultSet should already be disconnected if the SqlRowSet is supposed + * to be usable in a disconnected fashion. This means that you will usually pass in a + * {@code javax.sql.rowset.CachedRowSet}, which implements the ResultSet interface. + * + *

    Note: Since JDBC 4.0, it has been clarified that any methods using a String to identify + * the column should be using the column label. The column label is assigned using the ALIAS + * keyword in the SQL query string. When the query doesn't use an ALIAS, the default label is + * the column name. Most JDBC ResultSet implementations follow this new pattern but there are + * exceptions such as the {@code com.sun.rowset.CachedRowSetImpl} class which only uses + * the column name, ignoring any column labels. As of Spring 3.0.5, ResultSetWrappingSqlRowSet + * will translate column labels to the correct column index to provide better support for the + * {@code com.sun.rowset.CachedRowSetImpl} which is the default implementation used by + * {@link org.springframework.jdbc.core.JdbcTemplate} when working with RowSets. + * + *

    Note: This class implements the {@code java.io.Serializable} marker interface + * through the SqlRowSet interface, but is only actually serializable if the disconnected + * ResultSet/RowSet contained in it is serializable. Most CachedRowSet implementations + * are actually serializable, so this should usually work out. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 1.2 + * @see java.sql.ResultSet + * @see javax.sql.rowset.CachedRowSet + * @see org.springframework.jdbc.core.JdbcTemplate#queryForRowSet + */ +public class ResultSetWrappingSqlRowSet implements SqlRowSet { + + /** use serialVersionUID from Spring 1.2 for interoperability. */ + private static final long serialVersionUID = -4688694393146734764L; + + + private final ResultSet resultSet; + + private final SqlRowSetMetaData rowSetMetaData; + + private final Map columnLabelMap; + + + /** + * Create a new ResultSetWrappingSqlRowSet for the given ResultSet. + * @param resultSet a disconnected ResultSet to wrap + * (usually a {@code javax.sql.rowset.CachedRowSet}) + * @throws InvalidResultSetAccessException if extracting + * the ResultSetMetaData failed + * @see javax.sql.rowset.CachedRowSet + * @see java.sql.ResultSet#getMetaData + * @see ResultSetWrappingSqlRowSetMetaData + */ + public ResultSetWrappingSqlRowSet(ResultSet resultSet) throws InvalidResultSetAccessException { + this.resultSet = resultSet; + try { + this.rowSetMetaData = new ResultSetWrappingSqlRowSetMetaData(resultSet.getMetaData()); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + try { + ResultSetMetaData rsmd = resultSet.getMetaData(); + if (rsmd != null) { + int columnCount = rsmd.getColumnCount(); + this.columnLabelMap = CollectionUtils.newHashMap(columnCount); + for (int i = 1; i <= columnCount; i++) { + String key = rsmd.getColumnLabel(i); + // Make sure to preserve first matching column for any given name, + // as defined in ResultSet's type-level javadoc (lines 81 to 83). + if (!this.columnLabelMap.containsKey(key)) { + this.columnLabelMap.put(key, i); + } + } + } + else { + this.columnLabelMap = Collections.emptyMap(); + } + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + + } + + + /** + * Return the underlying ResultSet + * (usually a {@code javax.sql.rowset.CachedRowSet}). + * @see javax.sql.rowset.CachedRowSet + */ + public final ResultSet getResultSet() { + return this.resultSet; + } + + /** + * @see java.sql.ResultSetMetaData#getCatalogName(int) + */ + @Override + public final SqlRowSetMetaData getMetaData() { + return this.rowSetMetaData; + } + + /** + * @see java.sql.ResultSet#findColumn(String) + */ + @Override + public int findColumn(String columnLabel) throws InvalidResultSetAccessException { + Integer columnIndex = this.columnLabelMap.get(columnLabel); + if (columnIndex != null) { + return columnIndex; + } + else { + try { + return this.resultSet.findColumn(columnLabel); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + } + + + // RowSet methods for extracting data values + + /** + * @see java.sql.ResultSet#getBigDecimal(int) + */ + @Override + @Nullable + public BigDecimal getBigDecimal(int columnIndex) throws InvalidResultSetAccessException { + try { + return this.resultSet.getBigDecimal(columnIndex); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getBigDecimal(String) + */ + @Override + @Nullable + public BigDecimal getBigDecimal(String columnLabel) throws InvalidResultSetAccessException { + return getBigDecimal(findColumn(columnLabel)); + } + + /** + * @see java.sql.ResultSet#getBoolean(int) + */ + @Override + public boolean getBoolean(int columnIndex) throws InvalidResultSetAccessException { + try { + return this.resultSet.getBoolean(columnIndex); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getBoolean(String) + */ + @Override + public boolean getBoolean(String columnLabel) throws InvalidResultSetAccessException { + return getBoolean(findColumn(columnLabel)); + } + + /** + * @see java.sql.ResultSet#getByte(int) + */ + @Override + public byte getByte(int columnIndex) throws InvalidResultSetAccessException { + try { + return this.resultSet.getByte(columnIndex); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getByte(String) + */ + @Override + public byte getByte(String columnLabel) throws InvalidResultSetAccessException { + return getByte(findColumn(columnLabel)); + } + + /** + * @see java.sql.ResultSet#getDate(int) + */ + @Override + @Nullable + public Date getDate(int columnIndex) throws InvalidResultSetAccessException { + try { + return this.resultSet.getDate(columnIndex); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getDate(String) + */ + @Override + @Nullable + public Date getDate(String columnLabel) throws InvalidResultSetAccessException { + return getDate(findColumn(columnLabel)); + } + + /** + * @see java.sql.ResultSet#getDate(int, Calendar) + */ + @Override + @Nullable + public Date getDate(int columnIndex, Calendar cal) throws InvalidResultSetAccessException { + try { + return this.resultSet.getDate(columnIndex, cal); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getDate(String, Calendar) + */ + @Override + @Nullable + public Date getDate(String columnLabel, Calendar cal) throws InvalidResultSetAccessException { + return getDate(findColumn(columnLabel), cal); + } + + /** + * @see java.sql.ResultSet#getDouble(int) + */ + @Override + public double getDouble(int columnIndex) throws InvalidResultSetAccessException { + try { + return this.resultSet.getDouble(columnIndex); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getDouble(String) + */ + @Override + public double getDouble(String columnLabel) throws InvalidResultSetAccessException { + return getDouble(findColumn(columnLabel)); + } + + /** + * @see java.sql.ResultSet#getFloat(int) + */ + @Override + public float getFloat(int columnIndex) throws InvalidResultSetAccessException { + try { + return this.resultSet.getFloat(columnIndex); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getFloat(String) + */ + @Override + public float getFloat(String columnLabel) throws InvalidResultSetAccessException { + return getFloat(findColumn(columnLabel)); + } + + /** + * @see java.sql.ResultSet#getInt(int) + */ + @Override + public int getInt(int columnIndex) throws InvalidResultSetAccessException { + try { + return this.resultSet.getInt(columnIndex); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getInt(String) + */ + @Override + public int getInt(String columnLabel) throws InvalidResultSetAccessException { + return getInt(findColumn(columnLabel)); + } + + /** + * @see java.sql.ResultSet#getLong(int) + */ + @Override + public long getLong(int columnIndex) throws InvalidResultSetAccessException { + try { + return this.resultSet.getLong(columnIndex); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getLong(String) + */ + @Override + public long getLong(String columnLabel) throws InvalidResultSetAccessException { + return getLong(findColumn(columnLabel)); + } + + /** + * @see java.sql.ResultSet#getNString(int) + */ + @Override + @Nullable + public String getNString(int columnIndex) throws InvalidResultSetAccessException { + try { + return this.resultSet.getNString(columnIndex); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getNString(String) + */ + @Override + @Nullable + public String getNString(String columnLabel) throws InvalidResultSetAccessException { + return getNString(findColumn(columnLabel)); + } + + /** + * @see java.sql.ResultSet#getObject(int) + */ + @Override + @Nullable + public Object getObject(int columnIndex) throws InvalidResultSetAccessException { + try { + return this.resultSet.getObject(columnIndex); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getObject(String) + */ + @Override + @Nullable + public Object getObject(String columnLabel) throws InvalidResultSetAccessException { + return getObject(findColumn(columnLabel)); + } + + /** + * @see java.sql.ResultSet#getObject(int, Map) + */ + @Override + @Nullable + public Object getObject(int columnIndex, Map> map) throws InvalidResultSetAccessException { + try { + return this.resultSet.getObject(columnIndex, map); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getObject(String, Map) + */ + @Override + @Nullable + public Object getObject(String columnLabel, Map> map) throws InvalidResultSetAccessException { + return getObject(findColumn(columnLabel), map); + } + + /** + * @see java.sql.ResultSet#getObject(int, Class) + */ + @Override + @Nullable + public T getObject(int columnIndex, Class type) throws InvalidResultSetAccessException { + try { + return this.resultSet.getObject(columnIndex, type); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getObject(String, Class) + */ + @Override + @Nullable + public T getObject(String columnLabel, Class type) throws InvalidResultSetAccessException { + return getObject(findColumn(columnLabel), type); + } + + /** + * @see java.sql.ResultSet#getShort(int) + */ + @Override + public short getShort(int columnIndex) throws InvalidResultSetAccessException { + try { + return this.resultSet.getShort(columnIndex); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getShort(String) + */ + @Override + public short getShort(String columnLabel) throws InvalidResultSetAccessException { + return getShort(findColumn(columnLabel)); + } + + /** + * @see java.sql.ResultSet#getString(int) + */ + @Override + @Nullable + public String getString(int columnIndex) throws InvalidResultSetAccessException { + try { + return this.resultSet.getString(columnIndex); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getString(String) + */ + @Override + @Nullable + public String getString(String columnLabel) throws InvalidResultSetAccessException { + return getString(findColumn(columnLabel)); + } + + /** + * @see java.sql.ResultSet#getTime(int) + */ + @Override + @Nullable + public Time getTime(int columnIndex) throws InvalidResultSetAccessException { + try { + return this.resultSet.getTime(columnIndex); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getTime(String) + */ + @Override + @Nullable + public Time getTime(String columnLabel) throws InvalidResultSetAccessException { + return getTime(findColumn(columnLabel)); + } + + /** + * @see java.sql.ResultSet#getTime(int, Calendar) + */ + @Override + @Nullable + public Time getTime(int columnIndex, Calendar cal) throws InvalidResultSetAccessException { + try { + return this.resultSet.getTime(columnIndex, cal); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getTime(String, Calendar) + */ + @Override + @Nullable + public Time getTime(String columnLabel, Calendar cal) throws InvalidResultSetAccessException { + return getTime(findColumn(columnLabel), cal); + } + + /** + * @see java.sql.ResultSet#getTimestamp(int) + */ + @Override + @Nullable + public Timestamp getTimestamp(int columnIndex) throws InvalidResultSetAccessException { + try { + return this.resultSet.getTimestamp(columnIndex); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getTimestamp(String) + */ + @Override + @Nullable + public Timestamp getTimestamp(String columnLabel) throws InvalidResultSetAccessException { + return getTimestamp(findColumn(columnLabel)); + } + + /** + * @see java.sql.ResultSet#getTimestamp(int, Calendar) + */ + @Override + @Nullable + public Timestamp getTimestamp(int columnIndex, Calendar cal) throws InvalidResultSetAccessException { + try { + return this.resultSet.getTimestamp(columnIndex, cal); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getTimestamp(String, Calendar) + */ + @Override + @Nullable + public Timestamp getTimestamp(String columnLabel, Calendar cal) throws InvalidResultSetAccessException { + return getTimestamp(findColumn(columnLabel), cal); + } + + + // RowSet navigation methods + + /** + * @see java.sql.ResultSet#absolute(int) + */ + @Override + public boolean absolute(int row) throws InvalidResultSetAccessException { + try { + return this.resultSet.absolute(row); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#afterLast() + */ + @Override + public void afterLast() throws InvalidResultSetAccessException { + try { + this.resultSet.afterLast(); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#beforeFirst() + */ + @Override + public void beforeFirst() throws InvalidResultSetAccessException { + try { + this.resultSet.beforeFirst(); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#first() + */ + @Override + public boolean first() throws InvalidResultSetAccessException { + try { + return this.resultSet.first(); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#getRow() + */ + @Override + public int getRow() throws InvalidResultSetAccessException { + try { + return this.resultSet.getRow(); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#isAfterLast() + */ + @Override + public boolean isAfterLast() throws InvalidResultSetAccessException { + try { + return this.resultSet.isAfterLast(); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#isBeforeFirst() + */ + @Override + public boolean isBeforeFirst() throws InvalidResultSetAccessException { + try { + return this.resultSet.isBeforeFirst(); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#isFirst() + */ + @Override + public boolean isFirst() throws InvalidResultSetAccessException { + try { + return this.resultSet.isFirst(); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#isLast() + */ + @Override + public boolean isLast() throws InvalidResultSetAccessException { + try { + return this.resultSet.isLast(); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#last() + */ + @Override + public boolean last() throws InvalidResultSetAccessException { + try { + return this.resultSet.last(); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#next() + */ + @Override + public boolean next() throws InvalidResultSetAccessException { + try { + return this.resultSet.next(); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#previous() + */ + @Override + public boolean previous() throws InvalidResultSetAccessException { + try { + return this.resultSet.previous(); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#relative(int) + */ + @Override + public boolean relative(int rows) throws InvalidResultSetAccessException { + try { + return this.resultSet.relative(rows); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + /** + * @see java.sql.ResultSet#wasNull() + */ + @Override + public boolean wasNull() throws InvalidResultSetAccessException { + try { + return this.resultSet.wasNull(); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSetMetaData.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSetMetaData.java new file mode 100644 index 0000000..88ad094 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSetMetaData.java @@ -0,0 +1,220 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.rowset; + +import java.sql.ResultSetMetaData; +import java.sql.SQLException; + +import org.springframework.jdbc.InvalidResultSetAccessException; +import org.springframework.lang.Nullable; + +/** + * The default implementation of Spring's {@link SqlRowSetMetaData} interface, wrapping a + * {@link java.sql.ResultSetMetaData} instance, catching any {@link SQLException SQLExceptions} + * and translating them to a corresponding Spring {@link InvalidResultSetAccessException}. + * + *

    Used by {@link ResultSetWrappingSqlRowSet}. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 1.2 + * @see ResultSetWrappingSqlRowSet#getMetaData() + */ +public class ResultSetWrappingSqlRowSetMetaData implements SqlRowSetMetaData { + + private final ResultSetMetaData resultSetMetaData; + + @Nullable + private String[] columnNames; + + + /** + * Create a new ResultSetWrappingSqlRowSetMetaData object + * for the given ResultSetMetaData instance. + * @param resultSetMetaData a disconnected ResultSetMetaData instance + * to wrap (usually a {@code javax.sql.RowSetMetaData} instance) + * @see java.sql.ResultSet#getMetaData + * @see javax.sql.RowSetMetaData + * @see ResultSetWrappingSqlRowSet#getMetaData + */ + public ResultSetWrappingSqlRowSetMetaData(ResultSetMetaData resultSetMetaData) { + this.resultSetMetaData = resultSetMetaData; + } + + + @Override + public String getCatalogName(int column) throws InvalidResultSetAccessException { + try { + return this.resultSetMetaData.getCatalogName(column); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + @Override + public String getColumnClassName(int column) throws InvalidResultSetAccessException { + try { + return this.resultSetMetaData.getColumnClassName(column); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + @Override + public int getColumnCount() throws InvalidResultSetAccessException { + try { + return this.resultSetMetaData.getColumnCount(); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + @Override + public String[] getColumnNames() throws InvalidResultSetAccessException { + if (this.columnNames == null) { + this.columnNames = new String[getColumnCount()]; + for (int i = 0; i < getColumnCount(); i++) { + this.columnNames[i] = getColumnName(i + 1); + } + } + return this.columnNames; + } + + @Override + public int getColumnDisplaySize(int column) throws InvalidResultSetAccessException { + try { + return this.resultSetMetaData.getColumnDisplaySize(column); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + @Override + public String getColumnLabel(int column) throws InvalidResultSetAccessException { + try { + return this.resultSetMetaData.getColumnLabel(column); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + @Override + public String getColumnName(int column) throws InvalidResultSetAccessException { + try { + return this.resultSetMetaData.getColumnName(column); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + @Override + public int getColumnType(int column) throws InvalidResultSetAccessException { + try { + return this.resultSetMetaData.getColumnType(column); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + @Override + public String getColumnTypeName(int column) throws InvalidResultSetAccessException { + try { + return this.resultSetMetaData.getColumnTypeName(column); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + @Override + public int getPrecision(int column) throws InvalidResultSetAccessException { + try { + return this.resultSetMetaData.getPrecision(column); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + @Override + public int getScale(int column) throws InvalidResultSetAccessException { + try { + return this.resultSetMetaData.getScale(column); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + @Override + public String getSchemaName(int column) throws InvalidResultSetAccessException { + try { + return this.resultSetMetaData.getSchemaName(column); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + @Override + public String getTableName(int column) throws InvalidResultSetAccessException { + try { + return this.resultSetMetaData.getTableName(column); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + @Override + public boolean isCaseSensitive(int column) throws InvalidResultSetAccessException { + try { + return this.resultSetMetaData.isCaseSensitive(column); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + @Override + public boolean isCurrency(int column) throws InvalidResultSetAccessException { + try { + return this.resultSetMetaData.isCurrency(column); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + + @Override + public boolean isSigned(int column) throws InvalidResultSetAccessException { + try { + return this.resultSetMetaData.isSigned(column); + } + catch (SQLException se) { + throw new InvalidResultSetAccessException(se); + } + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/SqlRowSet.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/SqlRowSet.java new file mode 100644 index 0000000..cb49fd0 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/SqlRowSet.java @@ -0,0 +1,519 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.rowset; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.sql.Date; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Map; + +import org.springframework.jdbc.InvalidResultSetAccessException; +import org.springframework.lang.Nullable; + +/** + * Mirror interface for {@link javax.sql.RowSet}, representing a disconnected variant of + * {@link java.sql.ResultSet} data. + * + *

    The main difference to the standard JDBC RowSet is that a {@link java.sql.SQLException} + * is never thrown here. This allows an SqlRowSet to be used without having to deal with + * checked exceptions. An SqlRowSet will throw Spring's {@link InvalidResultSetAccessException} + * instead (when appropriate). + * + *

    Note: This interface extends the {@code java.io.Serializable} marker interface. + * Implementations, which typically hold disconnected data, are encouraged to be actually + * serializable (as far as possible). + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 1.2 + * @see javax.sql.RowSet + * @see java.sql.ResultSet + * @see org.springframework.jdbc.InvalidResultSetAccessException + * @see org.springframework.jdbc.core.JdbcTemplate#queryForRowSet + */ +public interface SqlRowSet extends Serializable { + + /** + * Retrieve the meta-data, i.e. number, types and properties + * for the columns of this row set. + * @return a corresponding SqlRowSetMetaData instance + * @see java.sql.ResultSet#getMetaData() + */ + SqlRowSetMetaData getMetaData(); + + /** + * Map the given column label to its column index. + * @param columnLabel the name of the column + * @return the column index for the given column label + * @see java.sql.ResultSet#findColumn(String) + */ + int findColumn(String columnLabel) throws InvalidResultSetAccessException; + + + // RowSet methods for extracting data values + + /** + * Retrieve the value of the indicated column in the current row as a BigDecimal object. + * @param columnIndex the column index + * @return an BigDecimal object representing the column value + * @see java.sql.ResultSet#getBigDecimal(int) + */ + @Nullable + BigDecimal getBigDecimal(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as a BigDecimal object. + * @param columnLabel the column label + * @return an BigDecimal object representing the column value + * @see java.sql.ResultSet#getBigDecimal(String) + */ + @Nullable + BigDecimal getBigDecimal(String columnLabel) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as a boolean. + * @param columnIndex the column index + * @return a boolean representing the column value + * @see java.sql.ResultSet#getBoolean(int) + */ + boolean getBoolean(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as a boolean. + * @param columnLabel the column label + * @return a boolean representing the column value + * @see java.sql.ResultSet#getBoolean(String) + */ + boolean getBoolean(String columnLabel) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as a byte. + * @param columnIndex the column index + * @return a byte representing the column value + * @see java.sql.ResultSet#getByte(int) + */ + byte getByte(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as a byte. + * @param columnLabel the column label + * @return a byte representing the column value + * @see java.sql.ResultSet#getByte(String) + */ + byte getByte(String columnLabel) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as a Date object. + * @param columnIndex the column index + * @return a Date object representing the column value + * @see java.sql.ResultSet#getDate(int) + */ + @Nullable + Date getDate(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as a Date object. + * @param columnLabel the column label + * @return a Date object representing the column value + * @see java.sql.ResultSet#getDate(String) + */ + @Nullable + Date getDate(String columnLabel) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as a Date object. + * @param columnIndex the column index + * @param cal the Calendar to use in constructing the Date + * @return a Date object representing the column value + * @see java.sql.ResultSet#getDate(int, Calendar) + */ + @Nullable + Date getDate(int columnIndex, Calendar cal) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as a Date object. + * @param columnLabel the column label + * @param cal the Calendar to use in constructing the Date + * @return a Date object representing the column value + * @see java.sql.ResultSet#getDate(String, Calendar) + */ + @Nullable + Date getDate(String columnLabel, Calendar cal) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as a Double object. + * @param columnIndex the column index + * @return a Double object representing the column value + * @see java.sql.ResultSet#getDouble(int) + */ + double getDouble(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as a Double object. + * @param columnLabel the column label + * @return a Double object representing the column value + * @see java.sql.ResultSet#getDouble(String) + */ + double getDouble(String columnLabel) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as a float. + * @param columnIndex the column index + * @return a float representing the column value + * @see java.sql.ResultSet#getFloat(int) + */ + float getFloat(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as a float. + * @param columnLabel the column label + * @return a float representing the column value + * @see java.sql.ResultSet#getFloat(String) + */ + float getFloat(String columnLabel) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as an int. + * @param columnIndex the column index + * @return an int representing the column value + * @see java.sql.ResultSet#getInt(int) + */ + int getInt(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as an int. + * @param columnLabel the column label + * @return an int representing the column value + * @see java.sql.ResultSet#getInt(String) + */ + int getInt(String columnLabel) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as a long. + * @param columnIndex the column index + * @return a long representing the column value + * @see java.sql.ResultSet#getLong(int) + */ + long getLong(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as a long. + * @param columnLabel the column label + * @return a long representing the column value + * @see java.sql.ResultSet#getLong(String) + */ + long getLong(String columnLabel) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as a String + * (for NCHAR, NVARCHAR, LONGNVARCHAR columns). + * @param columnIndex the column index + * @return a String representing the column value + * @since 4.1.3 + * @see java.sql.ResultSet#getNString(int) + */ + @Nullable + String getNString(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as a String + * (for NCHAR, NVARCHAR, LONGNVARCHAR columns). + * @param columnLabel the column label + * @return a String representing the column value + * @since 4.1.3 + * @see java.sql.ResultSet#getNString(String) + */ + @Nullable + String getNString(String columnLabel) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as an Object. + * @param columnIndex the column index + * @return a Object representing the column value + * @see java.sql.ResultSet#getObject(int) + */ + @Nullable + Object getObject(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as an Object. + * @param columnLabel the column label + * @return a Object representing the column value + * @see java.sql.ResultSet#getObject(String) + */ + @Nullable + Object getObject(String columnLabel) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as an Object. + * @param columnIndex the column index + * @param map a Map object containing the mapping from SQL types to Java types + * @return a Object representing the column value + * @see java.sql.ResultSet#getObject(int, Map) + */ + @Nullable + Object getObject(int columnIndex, Map> map) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as an Object. + * @param columnLabel the column label + * @param map a Map object containing the mapping from SQL types to Java types + * @return a Object representing the column value + * @see java.sql.ResultSet#getObject(String, Map) + */ + @Nullable + Object getObject(String columnLabel, Map> map) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as an Object. + * @param columnIndex the column index + * @param type the Java type to convert the designated column to + * @return a Object representing the column value + * @since 4.1.3 + * @see java.sql.ResultSet#getObject(int, Class) + */ + @Nullable + T getObject(int columnIndex, Class type) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as an Object. + * @param columnLabel the column label + * @param type the Java type to convert the designated column to + * @return a Object representing the column value + * @since 4.1.3 + * @see java.sql.ResultSet#getObject(String, Class) + */ + @Nullable + T getObject(String columnLabel, Class type) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as a short. + * @param columnIndex the column index + * @return a short representing the column value + * @see java.sql.ResultSet#getShort(int) + */ + short getShort(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as a short. + * @param columnLabel the column label + * @return a short representing the column value + * @see java.sql.ResultSet#getShort(String) + */ + short getShort(String columnLabel) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as a String. + * @param columnIndex the column index + * @return a String representing the column value + * @see java.sql.ResultSet#getString(int) + */ + @Nullable + String getString(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as a String. + * @param columnLabel the column label + * @return a String representing the column value + * @see java.sql.ResultSet#getString(String) + */ + @Nullable + String getString(String columnLabel) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as a Time object. + * @param columnIndex the column index + * @return a Time object representing the column value + * @see java.sql.ResultSet#getTime(int) + */ + @Nullable + Time getTime(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as a Time object. + * @param columnLabel the column label + * @return a Time object representing the column value + * @see java.sql.ResultSet#getTime(String) + */ + @Nullable + Time getTime(String columnLabel) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as a Time object. + * @param columnIndex the column index + * @param cal the Calendar to use in constructing the Date + * @return a Time object representing the column value + * @see java.sql.ResultSet#getTime(int, Calendar) + */ + @Nullable + Time getTime(int columnIndex, Calendar cal) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as a Time object. + * @param columnLabel the column label + * @param cal the Calendar to use in constructing the Date + * @return a Time object representing the column value + * @see java.sql.ResultSet#getTime(String, Calendar) + */ + @Nullable + Time getTime(String columnLabel, Calendar cal) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as a Timestamp object. + * @param columnIndex the column index + * @return a Timestamp object representing the column value + * @see java.sql.ResultSet#getTimestamp(int) + */ + @Nullable + Timestamp getTimestamp(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as a Timestamp object. + * @param columnLabel the column label + * @return a Timestamp object representing the column value + * @see java.sql.ResultSet#getTimestamp(String) + */ + @Nullable + Timestamp getTimestamp(String columnLabel) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as a Timestamp object. + * @param columnIndex the column index + * @param cal the Calendar to use in constructing the Date + * @return a Timestamp object representing the column value + * @see java.sql.ResultSet#getTimestamp(int, Calendar) + */ + @Nullable + Timestamp getTimestamp(int columnIndex, Calendar cal) throws InvalidResultSetAccessException; + + /** + * Retrieve the value of the indicated column in the current row as a Timestamp object. + * @param columnLabel the column label + * @param cal the Calendar to use in constructing the Date + * @return a Timestamp object representing the column value + * @see java.sql.ResultSet#getTimestamp(String, Calendar) + */ + @Nullable + Timestamp getTimestamp(String columnLabel, Calendar cal) throws InvalidResultSetAccessException; + + + // RowSet navigation methods + + /** + * Move the cursor to the given row number in the row set, just after the last row. + * @param row the number of the row where the cursor should move + * @return {@code true} if the cursor is on the row set, {@code false} otherwise + * @see java.sql.ResultSet#absolute(int) + */ + boolean absolute(int row) throws InvalidResultSetAccessException; + + /** + * Move the cursor to the end of this row set. + * @see java.sql.ResultSet#afterLast() + */ + void afterLast() throws InvalidResultSetAccessException; + + /** + * Move the cursor to the front of this row set, just before the first row. + * @see java.sql.ResultSet#beforeFirst() + */ + void beforeFirst() throws InvalidResultSetAccessException; + + /** + * Move the cursor to the first row of this row set. + * @return {@code true} if the cursor is on a valid row, {@code false} otherwise + * @see java.sql.ResultSet#first() + */ + boolean first() throws InvalidResultSetAccessException; + + /** + * Retrieve the current row number. + * @return the current row number + * @see java.sql.ResultSet#getRow() + */ + int getRow() throws InvalidResultSetAccessException; + + /** + * Retrieve whether the cursor is after the last row of this row set. + * @return {@code true} if the cursor is after the last row, {@code false} otherwise + * @see java.sql.ResultSet#isAfterLast() + */ + boolean isAfterLast() throws InvalidResultSetAccessException; + + /** + * Retrieve whether the cursor is before the first row of this row set. + * @return {@code true} if the cursor is before the first row, {@code false} otherwise + * @see java.sql.ResultSet#isBeforeFirst() + */ + boolean isBeforeFirst() throws InvalidResultSetAccessException; + + /** + * Retrieve whether the cursor is on the first row of this row set. + * @return {@code true} if the cursor is after the first row, {@code false} otherwise + * @see java.sql.ResultSet#isFirst() + */ + boolean isFirst() throws InvalidResultSetAccessException; + + /** + * Retrieve whether the cursor is on the last row of this row set. + * @return {@code true} if the cursor is after the last row, {@code false} otherwise + * @see java.sql.ResultSet#isLast() + */ + boolean isLast() throws InvalidResultSetAccessException; + + /** + * Move the cursor to the last row of this row set. + * @return {@code true} if the cursor is on a valid row, {@code false} otherwise + * @see java.sql.ResultSet#last() + */ + boolean last() throws InvalidResultSetAccessException; + + /** + * Move the cursor to the next row. + * @return {@code true} if the new row is valid, {@code false} if there are no more rows + * @see java.sql.ResultSet#next() + */ + boolean next() throws InvalidResultSetAccessException; + + /** + * Move the cursor to the previous row. + * @return {@code true} if the new row is valid, {@code false} if it is off the row set + * @see java.sql.ResultSet#previous() + */ + boolean previous() throws InvalidResultSetAccessException; + + /** + * Move the cursor a relative number of rows, either positive or negative. + * @return {@code true} if the cursor is on a row, {@code false} otherwise + * @see java.sql.ResultSet#relative(int) + */ + boolean relative(int rows) throws InvalidResultSetAccessException; + + /** + * Report whether the last column read had a value of SQL {@code NULL}. + *

    Note that you must first call one of the getter methods and then + * call the {@code wasNull()} method. + * @return {@code true} if the most recent column retrieved was + * SQL {@code NULL}, {@code false} otherwise + * @see java.sql.ResultSet#wasNull() + */ + boolean wasNull() throws InvalidResultSetAccessException; + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/SqlRowSetMetaData.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/SqlRowSetMetaData.java new file mode 100644 index 0000000..15a5f01 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/SqlRowSetMetaData.java @@ -0,0 +1,169 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.rowset; + +import org.springframework.jdbc.InvalidResultSetAccessException; + +/** + * Metadata interface for Spring's {@link SqlRowSet}, analogous to JDBC's + * {@link java.sql.ResultSetMetaData}. + * + *

    The main difference to the standard JDBC ResultSetMetaData is that a + * {@link java.sql.SQLException} is never thrown here. This allows + * SqlRowSetMetaData to be used without having to deal with checked exceptions. + * SqlRowSetMetaData will throw Spring's {@link InvalidResultSetAccessException} + * instead (when appropriate). + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 1.2 + * @see SqlRowSet#getMetaData() + * @see java.sql.ResultSetMetaData + * @see org.springframework.jdbc.InvalidResultSetAccessException + */ +public interface SqlRowSetMetaData { + + /** + * Retrieve the catalog name of the table that served as the source for the + * specified column. + * @param columnIndex the index of the column + * @return the catalog name + * @see java.sql.ResultSetMetaData#getCatalogName(int) + */ + String getCatalogName(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieve the fully qualified class that the specified column will be mapped to. + * @param columnIndex the index of the column + * @return the class name as a String + * @see java.sql.ResultSetMetaData#getColumnClassName(int) + */ + String getColumnClassName(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieve the number of columns in the RowSet. + * @return the number of columns + * @see java.sql.ResultSetMetaData#getColumnCount() + */ + int getColumnCount() throws InvalidResultSetAccessException; + + /** + * Return the column names of the table that the result set represents. + * @return the column names + */ + String[] getColumnNames() throws InvalidResultSetAccessException; + + /** + * Retrieve the maximum width of the designated column. + * @param columnIndex the index of the column + * @return the width of the column + * @see java.sql.ResultSetMetaData#getColumnDisplaySize(int) + */ + int getColumnDisplaySize(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieve the suggested column title for the column specified. + * @param columnIndex the index of the column + * @return the column title + * @see java.sql.ResultSetMetaData#getColumnLabel(int) + */ + String getColumnLabel(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieve the column name for the indicated column. + * @param columnIndex the index of the column + * @return the column name + * @see java.sql.ResultSetMetaData#getColumnName(int) + */ + String getColumnName(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieve the SQL type code for the indicated column. + * @param columnIndex the index of the column + * @return the SQL type code + * @see java.sql.ResultSetMetaData#getColumnType(int) + * @see java.sql.Types + */ + int getColumnType(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieve the DBMS-specific type name for the indicated column. + * @param columnIndex the index of the column + * @return the type name + * @see java.sql.ResultSetMetaData#getColumnTypeName(int) + */ + String getColumnTypeName(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieve the precision for the indicated column. + * @param columnIndex the index of the column + * @return the precision + * @see java.sql.ResultSetMetaData#getPrecision(int) + */ + int getPrecision(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieve the scale of the indicated column. + * @param columnIndex the index of the column + * @return the scale + * @see java.sql.ResultSetMetaData#getScale(int) + */ + int getScale(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieve the schema name of the table that served as the source for the + * specified column. + * @param columnIndex the index of the column + * @return the schema name + * @see java.sql.ResultSetMetaData#getSchemaName(int) + */ + String getSchemaName(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Retrieve the name of the table that served as the source for the + * specified column. + * @param columnIndex the index of the column + * @return the name of the table + * @see java.sql.ResultSetMetaData#getTableName(int) + */ + String getTableName(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Indicate whether the case of the designated column is significant. + * @param columnIndex the index of the column + * @return true if the case sensitive, false otherwise + * @see java.sql.ResultSetMetaData#isCaseSensitive(int) + */ + boolean isCaseSensitive(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Indicate whether the designated column contains a currency value. + * @param columnIndex the index of the column + * @return true if the value is a currency value, false otherwise + * @see java.sql.ResultSetMetaData#isCurrency(int) + */ + boolean isCurrency(int columnIndex) throws InvalidResultSetAccessException; + + /** + * Indicate whether the designated column contains a signed number. + * @param columnIndex the index of the column + * @return true if the column contains a signed number, false otherwise + * @see java.sql.ResultSetMetaData#isSigned(int) + */ + boolean isSigned(int columnIndex) throws InvalidResultSetAccessException; + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/package-info.java new file mode 100644 index 0000000..d38a51b --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/package-info.java @@ -0,0 +1,10 @@ +/** + * Provides a convenient holder for disconnected result sets. + * Supported by JdbcTemplate, but can be used independently too. + */ +@NonNullApi +@NonNullFields +package org.springframework.jdbc.support.rowset; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/Jdbc4SqlXmlHandler.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/Jdbc4SqlXmlHandler.java new file mode 100644 index 0000000..a79fb27 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/Jdbc4SqlXmlHandler.java @@ -0,0 +1,213 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.xml; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLXML; + +import javax.xml.transform.Result; +import javax.xml.transform.Source; +import javax.xml.transform.dom.DOMResult; +import javax.xml.transform.dom.DOMSource; + +import org.w3c.dom.Document; + +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.lang.Nullable; + +/** + * Default implementation of the {@link SqlXmlHandler} interface. + * Provides database-specific implementations for storing and + * retrieving XML documents to and from fields in a database, + * relying on the JDBC 4.0 {@code java.sql.SQLXML} facility. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @since 2.5.6 + * @see java.sql.SQLXML + * @see java.sql.ResultSet#getSQLXML + * @see java.sql.PreparedStatement#setSQLXML + */ +public class Jdbc4SqlXmlHandler implements SqlXmlHandler { + + //------------------------------------------------------------------------- + // Convenience methods for accessing XML content + //------------------------------------------------------------------------- + + @Override + @Nullable + public String getXmlAsString(ResultSet rs, String columnName) throws SQLException { + SQLXML xmlObject = rs.getSQLXML(columnName); + return (xmlObject != null ? xmlObject.getString() : null); + } + + @Override + @Nullable + public String getXmlAsString(ResultSet rs, int columnIndex) throws SQLException { + SQLXML xmlObject = rs.getSQLXML(columnIndex); + return (xmlObject != null ? xmlObject.getString() : null); + } + + @Override + @Nullable + public InputStream getXmlAsBinaryStream(ResultSet rs, String columnName) throws SQLException { + SQLXML xmlObject = rs.getSQLXML(columnName); + return (xmlObject != null ? xmlObject.getBinaryStream() : null); + } + + @Override + @Nullable + public InputStream getXmlAsBinaryStream(ResultSet rs, int columnIndex) throws SQLException { + SQLXML xmlObject = rs.getSQLXML(columnIndex); + return (xmlObject != null ? xmlObject.getBinaryStream() : null); + } + + @Override + @Nullable + public Reader getXmlAsCharacterStream(ResultSet rs, String columnName) throws SQLException { + SQLXML xmlObject = rs.getSQLXML(columnName); + return (xmlObject != null ? xmlObject.getCharacterStream() : null); + } + + @Override + @Nullable + public Reader getXmlAsCharacterStream(ResultSet rs, int columnIndex) throws SQLException { + SQLXML xmlObject = rs.getSQLXML(columnIndex); + return (xmlObject != null ? xmlObject.getCharacterStream() : null); + } + + @Override + @Nullable + public Source getXmlAsSource(ResultSet rs, String columnName, @Nullable Class sourceClass) + throws SQLException { + + SQLXML xmlObject = rs.getSQLXML(columnName); + if (xmlObject == null) { + return null; + } + return (sourceClass != null ? xmlObject.getSource(sourceClass) : xmlObject.getSource(DOMSource.class)); + } + + @Override + @Nullable + public Source getXmlAsSource(ResultSet rs, int columnIndex, @Nullable Class sourceClass) + throws SQLException { + + SQLXML xmlObject = rs.getSQLXML(columnIndex); + if (xmlObject == null) { + return null; + } + return (sourceClass != null ? xmlObject.getSource(sourceClass) : xmlObject.getSource(DOMSource.class)); + } + + + //------------------------------------------------------------------------- + // Convenience methods for building XML content + //------------------------------------------------------------------------- + + @Override + public SqlXmlValue newSqlXmlValue(final String value) { + return new AbstractJdbc4SqlXmlValue() { + @Override + protected void provideXml(SQLXML xmlObject) throws SQLException, IOException { + xmlObject.setString(value); + } + }; + } + + @Override + public SqlXmlValue newSqlXmlValue(final XmlBinaryStreamProvider provider) { + return new AbstractJdbc4SqlXmlValue() { + @Override + protected void provideXml(SQLXML xmlObject) throws SQLException, IOException { + provider.provideXml(xmlObject.setBinaryStream()); + } + }; + } + + @Override + public SqlXmlValue newSqlXmlValue(final XmlCharacterStreamProvider provider) { + return new AbstractJdbc4SqlXmlValue() { + @Override + protected void provideXml(SQLXML xmlObject) throws SQLException, IOException { + provider.provideXml(xmlObject.setCharacterStream()); + } + }; + } + + @Override + public SqlXmlValue newSqlXmlValue(final Class resultClass, final XmlResultProvider provider) { + return new AbstractJdbc4SqlXmlValue() { + @Override + protected void provideXml(SQLXML xmlObject) throws SQLException, IOException { + provider.provideXml(xmlObject.setResult(resultClass)); + } + }; + } + + @Override + public SqlXmlValue newSqlXmlValue(final Document document) { + return new AbstractJdbc4SqlXmlValue() { + @Override + protected void provideXml(SQLXML xmlObject) throws SQLException, IOException { + xmlObject.setResult(DOMResult.class).setNode(document); + } + }; + } + + + /** + * Internal base class for {@link SqlXmlValue} implementations. + */ + private abstract static class AbstractJdbc4SqlXmlValue implements SqlXmlValue { + + @Nullable + private SQLXML xmlObject; + + @Override + public void setValue(PreparedStatement ps, int paramIndex) throws SQLException { + this.xmlObject = ps.getConnection().createSQLXML(); + try { + provideXml(this.xmlObject); + } + catch (IOException ex) { + throw new DataAccessResourceFailureException("Failure encountered while providing XML", ex); + } + ps.setSQLXML(paramIndex, this.xmlObject); + } + + @Override + public void cleanup() { + if (this.xmlObject != null) { + try { + this.xmlObject.free(); + } + catch (SQLException ex) { + throw new DataAccessResourceFailureException("Could not free SQLXML object", ex); + } + } + } + + protected abstract void provideXml(SQLXML xmlObject) throws SQLException, IOException; + } + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlFeatureNotImplementedException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlFeatureNotImplementedException.java new file mode 100644 index 0000000..443f4bf --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlFeatureNotImplementedException.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.xml; + +import org.springframework.dao.InvalidDataAccessApiUsageException; + +/** + * Exception thrown when the underlying implementation does not support the + * requested feature of the API. + * + * @author Thomas Risberg + * @since 2.5.5 + */ +@SuppressWarnings("serial") +public class SqlXmlFeatureNotImplementedException extends InvalidDataAccessApiUsageException { + + /** + * Constructor for SqlXmlFeatureNotImplementedException. + * @param msg the detail message + */ + public SqlXmlFeatureNotImplementedException(String msg) { + super(msg); + } + + /** + * Constructor for SqlXmlFeatureNotImplementedException. + * @param msg the detail message + * @param cause the root cause from the data access API in use + */ + public SqlXmlFeatureNotImplementedException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlHandler.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlHandler.java new file mode 100644 index 0000000..e4f7318 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlHandler.java @@ -0,0 +1,232 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.xml; + +import java.io.InputStream; +import java.io.Reader; +import java.sql.ResultSet; +import java.sql.SQLException; + +import javax.xml.transform.Result; +import javax.xml.transform.Source; + +import org.w3c.dom.Document; + +import org.springframework.lang.Nullable; + +/** + * Abstraction for handling XML fields in specific databases. Its main purpose + * is to isolate database-specific handling of XML stored in the database. + * + *

    JDBC 4.0 introduces the new data type {@code java.sql.SQLXML} + * but most databases and their drivers currently rely on database-specific + * data types and features. + * + *

    Provides accessor methods for XML fields and acts as factory for + * {@link SqlXmlValue} instances. + * + * @author Thomas Risberg + * @since 2.5.5 + * @see Jdbc4SqlXmlHandler + * @see java.sql.SQLXML + * @see java.sql.ResultSet#getSQLXML + * @see java.sql.PreparedStatement#setSQLXML + */ +public interface SqlXmlHandler { + + //------------------------------------------------------------------------- + // Convenience methods for accessing XML content + //------------------------------------------------------------------------- + + /** + * Retrieve the given column as String from the given ResultSet. + *

    Might simply invoke {@code ResultSet.getString} or work with + * {@code SQLXML} or database-specific classes depending on the + * database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnName the column name to use + * @return the content as String, or {@code null} in case of SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getString + * @see java.sql.ResultSet#getSQLXML + */ + @Nullable + String getXmlAsString(ResultSet rs, String columnName) throws SQLException; + + /** + * Retrieve the given column as String from the given ResultSet. + *

    Might simply invoke {@code ResultSet.getString} or work with + * {@code SQLXML} or database-specific classes depending on the + * database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnIndex the column index to use + * @return the content as String, or {@code null} in case of SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getString + * @see java.sql.ResultSet#getSQLXML + */ + @Nullable + String getXmlAsString(ResultSet rs, int columnIndex) throws SQLException; + + /** + * Retrieve the given column as binary stream from the given ResultSet. + *

    Might simply invoke {@code ResultSet.getAsciiStream} or work with + * {@code SQLXML} or database-specific classes depending on the + * database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnName the column name to use + * @return the content as a binary stream, or {@code null} in case of SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getSQLXML + * @see java.sql.SQLXML#getBinaryStream + */ + @Nullable + InputStream getXmlAsBinaryStream(ResultSet rs, String columnName) throws SQLException; + + /** + * Retrieve the given column as binary stream from the given ResultSet. + *

    Might simply invoke {@code ResultSet.getAsciiStream} or work with + * {@code SQLXML} or database-specific classes depending on the + * database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnIndex the column index to use + * @return the content as binary stream, or {@code null} in case of SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getSQLXML + * @see java.sql.SQLXML#getBinaryStream + */ + @Nullable + InputStream getXmlAsBinaryStream(ResultSet rs, int columnIndex) throws SQLException; + + /** + * Retrieve the given column as character stream from the given ResultSet. + *

    Might simply invoke {@code ResultSet.getCharacterStream} or work with + * {@code SQLXML} or database-specific classes depending on the + * database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnName the column name to use + * @return the content as character stream, or {@code null} in case of SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getSQLXML + * @see java.sql.SQLXML#getCharacterStream + */ + @Nullable + Reader getXmlAsCharacterStream(ResultSet rs, String columnName) throws SQLException; + + /** + * Retrieve the given column as character stream from the given ResultSet. + *

    Might simply invoke {@code ResultSet.getCharacterStream} or work with + * {@code SQLXML} or database-specific classes depending on the + * database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnIndex the column index to use + * @return the content as character stream, or {@code null} in case of SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getSQLXML + * @see java.sql.SQLXML#getCharacterStream + */ + @Nullable + Reader getXmlAsCharacterStream(ResultSet rs, int columnIndex) throws SQLException; + + /** + * Retrieve the given column as Source implemented using the specified source class + * from the given ResultSet. + *

    Might work with {@code SQLXML} or database-specific classes depending + * on the database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnName the column name to use + * @param sourceClass the implementation class to be used + * @return the content as character stream, or {@code null} in case of SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getSQLXML + * @see java.sql.SQLXML#getSource + */ + @Nullable + Source getXmlAsSource(ResultSet rs, String columnName, @Nullable Class sourceClass) throws SQLException; + + /** + * Retrieve the given column as Source implemented using the specified source class + * from the given ResultSet. + *

    Might work with {@code SQLXML} or database-specific classes depending + * on the database and driver. + * @param rs the ResultSet to retrieve the content from + * @param columnIndex the column index to use + * @param sourceClass the implementation class to be used + * @return the content as character stream, or {@code null} in case of SQL NULL + * @throws SQLException if thrown by JDBC methods + * @see java.sql.ResultSet#getSQLXML + * @see java.sql.SQLXML#getSource + */ + @Nullable + Source getXmlAsSource(ResultSet rs, int columnIndex, @Nullable Class sourceClass) throws SQLException; + + + //------------------------------------------------------------------------- + // Convenience methods for building XML content + //------------------------------------------------------------------------- + + /** + * Create a {@code SqlXmlValue} instance for the given XML data, + * as supported by the underlying JDBC driver. + * @param value the XML String value providing XML data + * @return the implementation specific instance + * @see SqlXmlValue + * @see java.sql.SQLXML#setString(String) + */ + SqlXmlValue newSqlXmlValue(String value); + + /** + * Create a {@code SqlXmlValue} instance for the given XML data, + * as supported by the underlying JDBC driver. + * @param provider the {@code XmlBinaryStreamProvider} providing XML data + * @return the implementation specific instance + * @see SqlXmlValue + * @see java.sql.SQLXML#setBinaryStream() + */ + SqlXmlValue newSqlXmlValue(XmlBinaryStreamProvider provider); + + /** + * Create a {@code SqlXmlValue} instance for the given XML data, + * as supported by the underlying JDBC driver. + * @param provider the {@code XmlCharacterStreamProvider} providing XML data + * @return the implementation specific instance + * @see SqlXmlValue + * @see java.sql.SQLXML#setCharacterStream() + */ + SqlXmlValue newSqlXmlValue(XmlCharacterStreamProvider provider); + + /** + * Create a {@code SqlXmlValue} instance for the given XML data, + * as supported by the underlying JDBC driver. + * @param resultClass the Result implementation class to be used + * @param provider the {@code XmlResultProvider} that will provide the XML data + * @return the implementation specific instance + * @see SqlXmlValue + * @see java.sql.SQLXML#setResult(Class) + */ + SqlXmlValue newSqlXmlValue(Class resultClass, XmlResultProvider provider); + + /** + * Create a {@code SqlXmlValue} instance for the given XML data, + * as supported by the underlying JDBC driver. + * @param doc the XML Document to be used + * @return the implementation specific instance + * @see SqlXmlValue + */ + SqlXmlValue newSqlXmlValue(Document doc); + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlValue.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlValue.java new file mode 100644 index 0000000..3d45d46 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlValue.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2008 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.xml; + +import org.springframework.jdbc.support.SqlValue; + +/** + * Subinterface of {@link org.springframework.jdbc.support.SqlValue} + * that supports passing in XML data to specified column and adds a + * cleanup callback, to be invoked after the value has been set and + * the corresponding statement has been executed. + * + * @author Thomas Risberg + * @since 2.5.5 + * @see org.springframework.jdbc.support.SqlValue + */ +public interface SqlXmlValue extends SqlValue { + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlBinaryStreamProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlBinaryStreamProvider.java new file mode 100644 index 0000000..1926b9d --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlBinaryStreamProvider.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.xml; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Interface defining handling involved with providing {@code OutputStream} + * data for XML input. + * + * @author Thomas Risberg + * @since 2.5.5 + * @see java.io.OutputStream + */ +public interface XmlBinaryStreamProvider { + + /** + * Implementations must implement this method to provide the XML content + * for the {@code OutputStream}. + * @param outputStream the {@code OutputStream} object being used to provide the XML input + * @throws IOException if an I/O error occurs while providing the XML + */ + void provideXml(OutputStream outputStream) throws IOException; + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlCharacterStreamProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlCharacterStreamProvider.java new file mode 100644 index 0000000..8b2bf69 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlCharacterStreamProvider.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.xml; + +import java.io.IOException; +import java.io.Writer; + +/** + * Interface defining handling involved with providing {@code Writer} + * data for XML input. + * + * @author Thomas Risberg + * @since 2.5.5 + * @see java.io.Writer + */ +public interface XmlCharacterStreamProvider { + + /** + * Implementations must implement this method to provide the XML content + * for the {@code Writer}. + * @param writer the {@code Writer} object being used to provide the XML input + * @throws IOException if an I/O error occurs while providing the XML + */ + void provideXml(Writer writer) throws IOException; + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlResultProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlResultProvider.java new file mode 100644 index 0000000..0c3eb05 --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/XmlResultProvider.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.xml; + +import javax.xml.transform.Result; + +/** + * Interface defining handling involved with providing {@code Result} + * data for XML input. + * + * @author Thomas Risberg + * @since 2.5.5 + * @see javax.xml.transform.Result + */ +public interface XmlResultProvider { + + /** + * Implementations must implement this method to provide the XML content + * for the {@code Result}. Implementations will vary depending on + * the {@code Result} implementation used. + * @param result the {@code Result} object being used to provide the XML input + */ + void provideXml(Result result); + +} diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/package-info.java new file mode 100644 index 0000000..24e863b --- /dev/null +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/package-info.java @@ -0,0 +1,9 @@ +/** + * Abstraction for handling fields of SQLXML data type. + */ +@NonNullApi +@NonNullFields +package org.springframework.jdbc.support.xml; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-jdbc/src/main/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensions.kt b/spring-jdbc/src/main/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensions.kt new file mode 100644 index 0000000..ff97b51 --- /dev/null +++ b/spring-jdbc/src/main/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensions.kt @@ -0,0 +1,121 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core + +import java.sql.ResultSet + +/** + * Extension for [JdbcOperations.queryForObject] providing a `queryForObject("...")` variant. + * + * @author Mario Arias + * @since 5.0 + */ +inline fun JdbcOperations.queryForObject(sql: String): T = + queryForObject(sql, T::class.java) as T + +/** + * Extensions for [JdbcOperations.queryForObject] providing a RowMapper-like function + * variant: `queryForObject("...", arg1, argN){ rs, i -> }`. + * + * @author Mario Arias + * @since 5.0 + */ +inline fun JdbcOperations.queryForObject(sql: String, vararg args: Any, crossinline function: (ResultSet, Int) -> T): T = + queryForObject(sql, RowMapper { resultSet, i -> function(resultSet, i) }, *args) as T + +/** + * Extension for [JdbcOperations.queryForObject] providing a + * `queryForObject("...", arrayOf(arg1, argN), intArray(type1, typeN))` variant. + * + * @author Mario Arias + * @since 5.0 + */ +inline fun JdbcOperations.queryForObject(sql: String, args: Array, argTypes: IntArray): T? = + queryForObject(sql, args, argTypes, T::class.java) as T + +/** + * Extension for [JdbcOperations.queryForObject] providing a + * `queryForObject("...", arrayOf(arg1, argN))` variant. + * + * @author Mario Arias + * @since 5.0 + */ +inline fun JdbcOperations.queryForObject(sql: String, args: Array): T? = + queryForObject(sql, T::class.java, args) as T + +/** + * Extension for [JdbcOperations.queryForList] providing a `queryForList("...")` variant. + * + * @author Mario Arias + * @since 5.0 + */ +@Suppress("EXTENSION_SHADOWED_BY_MEMBER") +inline fun JdbcOperations.queryForList(sql: String): List = + queryForList(sql, T::class.java) + +/** + * Extension for [JdbcOperations.queryForList] providing a + * `queryForList("...", arrayOf(arg1, argN), intArray(type1, typeN))` variant. + * + * @author Mario Arias + * @since 5.0 + */ +@Suppress("EXTENSION_SHADOWED_BY_MEMBER") +inline fun JdbcOperations.queryForList(sql: String, args: Array, + argTypes: IntArray): List = + queryForList(sql, args, argTypes, T::class.java) + +/** + * Extension for [JdbcOperations.queryForList] providing a + * `queryForList("...", arrayOf(arg1, argN))` variant. + * + * @author Mario Arias + * @since 5.0 + */ +inline fun JdbcOperations.queryForList(sql: String, args: Array): List = + queryForList(sql, T::class.java, args) + +/** + * Extension for [JdbcOperations.query] providing a ResultSetExtractor-like function + * variant: `query("...", arg1, argN){ rs -> }`. + * + * @author Mario Arias + * @since 5.0 + */ +inline fun JdbcOperations.query(sql: String, vararg args: Any, + crossinline function: (ResultSet) -> T): T = + query(sql, ResultSetExtractor { function(it) }, *args) as T + +/** + * Extension for [JdbcOperations.query] providing a RowCallbackHandler-like function + * variant: `query("...", arg1, argN){ rs -> }`. + * + * @author Mario Arias + * @since 5.0 + */ +fun JdbcOperations.query(sql: String, vararg args: Any, function: (ResultSet) -> Unit): Unit = + query(sql, RowCallbackHandler { function(it) }, *args) + +/** + * Extensions for [JdbcOperations.query] providing a RowMapper-like function variant: + * `query("...", arg1, argN){ rs, i -> }`. + * + * @author Mario Arias + * @since 5.0 + */ +fun JdbcOperations.query(sql: String, vararg args: Any, function: (ResultSet, Int) -> T): List = + query(sql, RowMapper { rs, i -> function(rs, i) }, *args) diff --git a/spring-jdbc/src/main/kotlin/org/springframework/jdbc/core/namedparam/MapSqlParameterSourceExtensions.kt b/spring-jdbc/src/main/kotlin/org/springframework/jdbc/core/namedparam/MapSqlParameterSourceExtensions.kt new file mode 100644 index 0000000..c5d74de --- /dev/null +++ b/spring-jdbc/src/main/kotlin/org/springframework/jdbc/core/namedparam/MapSqlParameterSourceExtensions.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.namedparam + +/** + * Extension for [MapSqlParameterSource.addValue] providing Array like setter. + * + * ```kotlin + * source["age"] = 3 + * ``` + * @author Mario Arias + * @since 5.0 + * + */ +operator fun MapSqlParameterSource.set(paramName: String, value: Any) { + this.addValue(paramName, value) +} + +/** + * Extension for [MapSqlParameterSource.addValue] providing Array like setter. + * + * ```kotlin + * source["age", JDBCType.INTEGER.vendorTypeNumber] = 3 + * ``` + * @author Mario Arias + * @since 5.0 + * + */ +operator fun MapSqlParameterSource.set(paramName: String, sqlType: Int, value: Any) { + this.addValue(paramName, value, sqlType) +} + +/** + * Extension for [MapSqlParameterSource.addValue] providing Array like setter + * + * ```kotlin + * source["age", JDBCType.INTEGER.vendorTypeNumber, "INT"] = 3 + * ``` + * @author Mario Arias + * @since 5.0 + * + */ +operator fun MapSqlParameterSource.set(paramName: String, sqlType: Int, typeName: String, value: Any) { + this.addValue(paramName, value, sqlType, typeName) +} diff --git a/spring-jdbc/src/main/resources/org/springframework/jdbc/config/spring-jdbc.gif b/spring-jdbc/src/main/resources/org/springframework/jdbc/config/spring-jdbc.gif new file mode 100644 index 0000000..20ed1f9 Binary files /dev/null and b/spring-jdbc/src/main/resources/org/springframework/jdbc/config/spring-jdbc.gif differ diff --git a/spring-jdbc/src/main/resources/org/springframework/jdbc/config/spring-jdbc.xsd b/spring-jdbc/src/main/resources/org/springframework/jdbc/config/spring-jdbc.xsd new file mode 100644 index 0000000..06d253f --- /dev/null +++ b/spring-jdbc/src/main/resources/org/springframework/jdbc/config/spring-jdbc.xsd @@ -0,0 +1,223 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + If set to "true", a pseudo-random unique name will be generated for the embedded + database, overriding any implicit name provided via the 'id' attribute or any + explicit name provided via the 'database-name' attribute. + Note that this is not the bean name but rather the name of the embedded database + as used in the JDBC connection URL for the database. + + + + + + + + + + + + + + + + + + + + + elements. + ]]> + + + + + + + + + + + + + + + + + + + + + + Is this bean "enabled", meaning the scripts will be executed? + Defaults to true but can be used to switch on and off script execution + depending on the environment. + + + + + + + Should failed SQL statements be ignored during execution? + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-jdbc/src/main/resources/org/springframework/jdbc/support/sql-error-codes.xml b/spring-jdbc/src/main/resources/org/springframework/jdbc/support/sql-error-codes.xml new file mode 100644 index 0000000..f0ed657 --- /dev/null +++ b/spring-jdbc/src/main/resources/org/springframework/jdbc/support/sql-error-codes.xml @@ -0,0 +1,299 @@ + + + + + + + + + DB2* + + + -007,-029,-097,-104,-109,-115,-128,-199,-204,-206,-301,-408,-441,-491 + + + -803 + + + -407,-530,-531,-532,-543,-544,-545,-603,-667 + + + -904,-971 + + + -1035,-1218,-30080,-30081 + + + -911,-913 + + + + + + Apache Derby + + + true + + + 42802,42821,42X01,42X02,42X03,42X04,42X05,42X06,42X07,42X08 + + + 23505 + + + 22001,22005,23502,23503,23513,X0Y32 + + + 04501,08004,42Y07 + + + 40XL1 + + + 40001 + + + + + + 42000,42001,42101,42102,42111,42112,42121,42122,42132 + + + 23001,23505 + + + 22001,22003,22012,22018,22025,23000,23002,23003,23502,23503,23506,23507,23513 + + + 90046,90100,90117,90121,90126 + + + 50200 + + + + + + + + SAP HANA + SAP DB + + + + + 257,259,260,261,262,263,264,267,268,269,270,271,272,273,275,276,277,278, + 278,279,280,281,282,283,284,285,286,288,289,290,294,295,296,297,299,308,309, + 313,315,316,318,319,320,321,322,323,324,328,329,330,333,335,336,337,338,340, + 343,350,351,352,362,368 + + + + 10,258 + + + 301 + + + 461,462 + + + -813,-709,-708,1024,1025,1026,1027,1029,1030,1031 + + + -11210,582,587,588,594 + + + 131 + + + 138,143 + + + 133 + + + + + + HSQL Database Engine + + + -22,-28 + + + -104 + + + -9 + + + -80 + + + + + + Informix Dynamic Server + + + -201,-217,-696 + + + -239,-268,-6017 + + + -692,-11030 + + + + + + Microsoft SQL Server + + + 156,170,207,208,209 + + + 229 + + + 2601,2627 + + + 544,8114,8115 + + + 4060 + + + 1222 + + + 1205 + + + + + + + MySQL + MariaDB + + + + 1054,1064,1146 + + + 1062 + + + 630,839,840,893,1169,1215,1216,1217,1364,1451,1452,1557 + + + 1 + + + 1205,3572 + + + 1213 + + + + + + 900,903,904,917,936,942,17006,6550 + + + 17003 + + + 1 + + + 1400,1722,2291,2292 + + + 17002,17447 + + + 54,30006 + + + 8177 + + + 60 + + + + + + true + + + 03000,42000,42601,42602,42622,42804,42P01 + + + 23505 + + + 23000,23502,23503,23514 + + + 53000,53100,53200,53300 + + + 55P03 + + + 40001 + + + 40P01 + + + + + + + Sybase SQL Server + Adaptive Server Enterprise + ASE + SQL Server + sql server + + + + 101,102,103,104,105,106,107,108,109,110,111,112,113,116,120,121,123,207,208,213,257,512 + + + 2601,2615,2626 + + + 233,511,515,530,546,547,2615,2714 + + + 921,1105 + + + 12205 + + + 1205 + + + + diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/Customer.java b/spring-jdbc/src/test/java/org/springframework/jdbc/Customer.java new file mode 100644 index 0000000..e02991a --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/Customer.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2007 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc; + +/** + * @author Juergen Hoeller + */ +public class Customer { + + private int id; + + private String forename; + + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getForename() { + return forename; + } + + public void setForename(String forename) { + this.forename = forename; + } + + + @Override + public String toString() { + return "Customer: id=" + id + "; forename=" + forename; + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/config/InitializeDatabaseIntegrationTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/config/InitializeDatabaseIntegrationTests.java new file mode 100644 index 0000000..e346409 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/config/InitializeDatabaseIntegrationTests.java @@ -0,0 +1,142 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.config; + +import java.util.List; +import java.util.Map; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.jdbc.core.JdbcTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Dave Syer + */ +public class InitializeDatabaseIntegrationTests { + + private String enabled; + + private ClassPathXmlApplicationContext context; + + + @BeforeEach + public void init() { + enabled = System.setProperty("ENABLED", "true"); + } + + @AfterEach + public void after() { + if (enabled != null) { + System.setProperty("ENABLED", enabled); + } + else { + System.clearProperty("ENABLED"); + } + if (context != null) { + context.close(); + } + } + + + @Test + public void testCreateEmbeddedDatabase() throws Exception { + context = new ClassPathXmlApplicationContext("org/springframework/jdbc/config/jdbc-initialize-config.xml"); + assertCorrectSetup(context.getBean("dataSource", DataSource.class)); + } + + @Test + public void testDisableCreateEmbeddedDatabase() throws Exception { + System.setProperty("ENABLED", "false"); + context = new ClassPathXmlApplicationContext("org/springframework/jdbc/config/jdbc-initialize-config.xml"); + assertThatExceptionOfType(BadSqlGrammarException.class).isThrownBy(() -> + assertCorrectSetup(context.getBean("dataSource", DataSource.class))); + } + + @Test + public void testIgnoreFailedDrops() throws Exception { + context = new ClassPathXmlApplicationContext("org/springframework/jdbc/config/jdbc-initialize-fail-config.xml"); + assertCorrectSetup(context.getBean("dataSource", DataSource.class)); + } + + @Test + public void testScriptNameWithPattern() throws Exception { + context = new ClassPathXmlApplicationContext("org/springframework/jdbc/config/jdbc-initialize-pattern-config.xml"); + DataSource dataSource = context.getBean("dataSource", DataSource.class); + assertCorrectSetup(dataSource); + JdbcTemplate t = new JdbcTemplate(dataSource); + assertThat(t.queryForObject("select name from T_TEST", String.class)).isEqualTo("Dave"); + } + + @Test + public void testScriptNameWithPlaceholder() throws Exception { + context = new ClassPathXmlApplicationContext("org/springframework/jdbc/config/jdbc-initialize-placeholder-config.xml"); + DataSource dataSource = context.getBean("dataSource", DataSource.class); + assertCorrectSetup(dataSource); + } + + @Test + public void testScriptNameWithExpressions() throws Exception { + context = new ClassPathXmlApplicationContext("org/springframework/jdbc/config/jdbc-initialize-expression-config.xml"); + DataSource dataSource = context.getBean("dataSource", DataSource.class); + assertCorrectSetup(dataSource); + } + + @Test + public void testCacheInitialization() throws Exception { + context = new ClassPathXmlApplicationContext("org/springframework/jdbc/config/jdbc-initialize-cache-config.xml"); + assertCorrectSetup(context.getBean("dataSource", DataSource.class)); + CacheData cache = context.getBean(CacheData.class); + assertThat(cache.getCachedData().size()).isEqualTo(1); + } + + private void assertCorrectSetup(DataSource dataSource) { + JdbcTemplate jt = new JdbcTemplate(dataSource); + assertThat(jt.queryForObject("select count(*) from T_TEST", Integer.class).intValue()).isEqualTo(1); + } + + + public static class CacheData implements InitializingBean { + + private JdbcTemplate jdbcTemplate; + + private List> cache; + + public void setDataSource(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + public List> getCachedData() { + return cache; + } + + @Override + public void afterPropertiesSet() throws Exception { + cache = jdbcTemplate.queryForList("SELECT * FROM T_TEST"); + } + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/config/JdbcNamespaceIntegrationTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/config/JdbcNamespaceIntegrationTests.java new file mode 100644 index 0000000..ec9c5fc --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/config/JdbcNamespaceIntegrationTests.java @@ -0,0 +1,204 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.config; + +import java.util.function.Predicate; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.PropertyValue; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.testfixture.EnabledForTestGroups; +import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.AbstractDriverBasedDataSource; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseFactoryBean; +import org.springframework.jdbc.datasource.init.DataSourceInitializer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; +import static org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseFactory.DEFAULT_DATABASE_NAME; + +/** + * @author Dave Syer + * @author Juergen Hoeller + * @author Chris Beams + * @author Sam Brannen + * @author Stephane Nicoll + */ +class JdbcNamespaceIntegrationTests { + + @Test + @EnabledForTestGroups(LONG_RUNNING) + void createEmbeddedDatabase() throws Exception { + assertCorrectSetup("jdbc-config.xml", "dataSource", "h2DataSource", "derbyDataSource"); + } + + @Test + @EnabledForTestGroups(LONG_RUNNING) + void createEmbeddedDatabaseAgain() throws Exception { + // If Derby isn't cleaned up properly this will fail... + assertCorrectSetup("jdbc-config.xml", "derbyDataSource"); + } + + @Test + void createWithResourcePattern() throws Exception { + assertCorrectSetup("jdbc-config-pattern.xml", "dataSource"); + } + + @Test + void createWithAnonymousDataSourceAndDefaultDatabaseName() throws Exception { + assertCorrectSetupForSingleDataSource("jdbc-config-db-name-default-and-anonymous-datasource.xml", + url -> url.endsWith(DEFAULT_DATABASE_NAME)); + } + + @Test + void createWithImplicitDatabaseName() throws Exception { + assertCorrectSetupForSingleDataSource("jdbc-config-db-name-implicit.xml", url -> url.endsWith("dataSource")); + } + + @Test + void createWithExplicitDatabaseName() throws Exception { + assertCorrectSetupForSingleDataSource("jdbc-config-db-name-explicit.xml", url -> url.endsWith("customDbName")); + } + + @Test + void createWithGeneratedDatabaseName() throws Exception { + Predicate urlPredicate = url -> url.startsWith("jdbc:hsqldb:mem:"); + urlPredicate.and(url -> !url.endsWith("dataSource")); + urlPredicate.and(url -> !url.endsWith("shouldBeOverriddenByGeneratedName")); + assertCorrectSetupForSingleDataSource("jdbc-config-db-name-generated.xml", urlPredicate); + } + + @Test + void createWithEndings() throws Exception { + assertCorrectSetupAndCloseContext("jdbc-initialize-endings-config.xml", 2, "dataSource"); + } + + @Test + void createWithEndingsNested() throws Exception { + assertCorrectSetupAndCloseContext("jdbc-initialize-endings-nested-config.xml", 2, "dataSource"); + } + + @Test + void createAndDestroy() throws Exception { + try (ClassPathXmlApplicationContext context = context("jdbc-destroy-config.xml")) { + DataSource dataSource = context.getBean(DataSource.class); + JdbcTemplate template = new JdbcTemplate(dataSource); + assertNumRowsInTestTable(template, 1); + context.getBean(DataSourceInitializer.class).destroy(); + // Table has been dropped + assertThatExceptionOfType(BadSqlGrammarException.class).isThrownBy(() -> + assertNumRowsInTestTable(template, 1)); + } + } + + @Test + void createAndDestroyNestedWithHsql() throws Exception { + try (ClassPathXmlApplicationContext context = context("jdbc-destroy-nested-config.xml")) { + DataSource dataSource = context.getBean(DataSource.class); + JdbcTemplate template = new JdbcTemplate(dataSource); + assertNumRowsInTestTable(template, 1); + context.getBean(EmbeddedDatabaseFactoryBean.class).destroy(); + // Table has been dropped + assertThatExceptionOfType(BadSqlGrammarException.class).isThrownBy(() -> + assertNumRowsInTestTable(template, 1)); + } + } + + @Test + void createAndDestroyNestedWithH2() throws Exception { + try (ClassPathXmlApplicationContext context = context("jdbc-destroy-nested-config-h2.xml")) { + DataSource dataSource = context.getBean(DataSource.class); + JdbcTemplate template = new JdbcTemplate(dataSource); + assertNumRowsInTestTable(template, 1); + context.getBean(EmbeddedDatabaseFactoryBean.class).destroy(); + // Table has been dropped + assertThatExceptionOfType(BadSqlGrammarException.class).isThrownBy(() -> + assertNumRowsInTestTable(template, 1)); + } + } + + @Test + void multipleDataSourcesHaveDifferentDatabaseNames() throws Exception { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(factory).loadBeanDefinitions(new ClassPathResource( + "jdbc-config-multiple-datasources.xml", getClass())); + assertBeanPropertyValueOf("databaseName", "firstDataSource", factory); + assertBeanPropertyValueOf("databaseName", "secondDataSource", factory); + } + + @Test + void initializeWithCustomSeparator() throws Exception { + assertCorrectSetupAndCloseContext("jdbc-initialize-custom-separator.xml", 2, "dataSource"); + } + + @Test + void embeddedWithCustomSeparator() throws Exception { + assertCorrectSetupAndCloseContext("jdbc-config-custom-separator.xml", 2, "dataSource"); + } + + private ClassPathXmlApplicationContext context(String file) { + return new ClassPathXmlApplicationContext(file, getClass()); + } + + private void assertBeanPropertyValueOf(String propertyName, String expected, DefaultListableBeanFactory factory) { + BeanDefinition bean = factory.getBeanDefinition(expected); + PropertyValue value = bean.getPropertyValues().getPropertyValue(propertyName); + assertThat(value).isNotNull(); + assertThat(value.getValue().toString()).isEqualTo(expected); + } + + private void assertNumRowsInTestTable(JdbcTemplate template, int count) { + assertThat(template.queryForObject("select count(*) from T_TEST", Integer.class).intValue()).isEqualTo(count); + } + + private void assertCorrectSetup(String file, String... dataSources) { + assertCorrectSetupAndCloseContext(file, 1, dataSources); + } + + private void assertCorrectSetupAndCloseContext(String file, int count, String... dataSources) { + try (ConfigurableApplicationContext context = context(file)) { + for (String dataSourceName : dataSources) { + DataSource dataSource = context.getBean(dataSourceName, DataSource.class); + assertNumRowsInTestTable(new JdbcTemplate(dataSource), count); + assertThat(dataSource instanceof AbstractDriverBasedDataSource).isTrue(); + AbstractDriverBasedDataSource adbDataSource = (AbstractDriverBasedDataSource) dataSource; + assertThat(adbDataSource.getUrl()).contains(dataSourceName); + } + } + } + + private void assertCorrectSetupForSingleDataSource(String file, Predicate urlPredicate) { + try (ConfigurableApplicationContext context = context(file)) { + DataSource dataSource = context.getBean(DataSource.class); + assertNumRowsInTestTable(new JdbcTemplate(dataSource), 1); + assertThat(dataSource instanceof AbstractDriverBasedDataSource).isTrue(); + AbstractDriverBasedDataSource adbDataSource = (AbstractDriverBasedDataSource) dataSource; + assertThat(urlPredicate.test(adbDataSource.getUrl())).isTrue(); + } + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java new file mode 100644 index 0000000..93716e5 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/AbstractRowMapperTests.java @@ -0,0 +1,168 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.Statement; +import java.sql.Timestamp; +import java.util.Date; + +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.PropertyAccessorFactory; +import org.springframework.jdbc.core.test.ConcretePerson; +import org.springframework.jdbc.core.test.ConstructorPerson; +import org.springframework.jdbc.core.test.DatePerson; +import org.springframework.jdbc.core.test.Person; +import org.springframework.jdbc.core.test.SpacePerson; +import org.springframework.jdbc.datasource.SingleConnectionDataSource; +import org.springframework.jdbc.support.SQLStateSQLExceptionTranslator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Mock object based abstract class for RowMapper tests. + * Initializes mock objects and verifies results. + * + * @author Thomas Risberg + */ +public abstract class AbstractRowMapperTests { + + protected void verifyPerson(Person person) { + assertThat(person.getName()).isEqualTo("Bubba"); + assertThat(person.getAge()).isEqualTo(22L); + assertThat(person.getBirth_date()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); + assertThat(person.getBalance()).isEqualTo(new BigDecimal("1234.56")); + verifyPersonViaBeanWrapper(person); + } + + protected void verifyPerson(ConcretePerson person) { + assertThat(person.getName()).isEqualTo("Bubba"); + assertThat(person.getAge()).isEqualTo(22L); + assertThat(person.getBirth_date()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); + assertThat(person.getBalance()).isEqualTo(new BigDecimal("1234.56")); + verifyPersonViaBeanWrapper(person); + } + + protected void verifyPerson(SpacePerson person) { + assertThat(person.getLastName()).isEqualTo("Bubba"); + assertThat(person.getAge()).isEqualTo(22L); + assertThat(person.getBirthDate()).isEqualTo(new Timestamp(1221222L).toLocalDateTime()); + assertThat(person.getBalance()).isEqualTo(new BigDecimal("1234.56")); + } + + protected void verifyPerson(DatePerson person) { + assertThat(person.getLastName()).isEqualTo("Bubba"); + assertThat(person.getAge()).isEqualTo(22L); + assertThat(person.getBirthDate()).isEqualTo(new java.sql.Date(1221222L).toLocalDate()); + assertThat(person.getBalance()).isEqualTo(new BigDecimal("1234.56")); + } + + protected void verifyPerson(ConstructorPerson person) { + assertThat(person.name()).isEqualTo("Bubba"); + assertThat(person.age()).isEqualTo(22L); + assertThat(person.birth_date()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); + assertThat(person.balance()).isEqualTo(new BigDecimal("1234.56")); + verifyPersonViaBeanWrapper(person); + } + + private void verifyPersonViaBeanWrapper(Object person) { + BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(person); + assertThat(bw.getPropertyValue("name")).isEqualTo("Bubba"); + assertThat(bw.getPropertyValue("age")).isEqualTo(22L); + assertThat((Date) bw.getPropertyValue("birth_date")).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L)); + assertThat(bw.getPropertyValue("balance")).isEqualTo(new BigDecimal("1234.56")); + } + + + protected enum MockType {ONE, TWO, THREE}; + + + protected static class Mock { + + private Connection connection; + + private ResultSetMetaData resultSetMetaData; + + private ResultSet resultSet; + + private Statement statement; + + private JdbcTemplate jdbcTemplate; + + public Mock() throws Exception { + this(MockType.ONE); + } + + @SuppressWarnings("unchecked") + public Mock(MockType type) throws Exception { + connection = mock(Connection.class); + statement = mock(Statement.class); + resultSet = mock(ResultSet.class); + resultSetMetaData = mock(ResultSetMetaData.class); + + given(connection.createStatement()).willReturn(statement); + given(statement.executeQuery(anyString())).willReturn(resultSet); + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + + given(resultSet.next()).willReturn(true, false); + given(resultSet.getString(1)).willReturn("Bubba"); + given(resultSet.getLong(2)).willReturn(22L); + given(resultSet.getTimestamp(3)).willReturn(new Timestamp(1221222L)); + given(resultSet.getObject(anyInt(), any(Class.class))).willThrow(new SQLFeatureNotSupportedException()); + given(resultSet.getDate(3)).willReturn(new java.sql.Date(1221222L)); + given(resultSet.getBigDecimal(4)).willReturn(new BigDecimal("1234.56")); + given(resultSet.wasNull()).willReturn(type == MockType.TWO); + + given(resultSetMetaData.getColumnCount()).willReturn(4); + given(resultSetMetaData.getColumnLabel(1)).willReturn( + type == MockType.THREE ? "Last Name" : "name"); + given(resultSetMetaData.getColumnLabel(2)).willReturn("age"); + given(resultSetMetaData.getColumnLabel(3)).willReturn("birth_date"); + given(resultSetMetaData.getColumnLabel(4)).willReturn("balance"); + + given(resultSet.findColumn("name")).willReturn(1); + given(resultSet.findColumn("age")).willReturn(2); + given(resultSet.findColumn("birth_date")).willReturn(3); + given(resultSet.findColumn("balance")).willReturn(4); + + jdbcTemplate = new JdbcTemplate(); + jdbcTemplate.setDataSource(new SingleConnectionDataSource(connection, false)); + jdbcTemplate.setExceptionTranslator(new SQLStateSQLExceptionTranslator()); + jdbcTemplate.afterPropertiesSet(); + } + + public JdbcTemplate getJdbcTemplate() { + return jdbcTemplate; + } + + public void verifyClosed() throws Exception { + verify(resultSet).close(); + verify(statement).close(); + } + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java new file mode 100644 index 0000000..6e1f84a --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/BeanPropertyRowMapperTests.java @@ -0,0 +1,137 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.TypeMismatchException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.jdbc.core.test.ConcretePerson; +import org.springframework.jdbc.core.test.DatePerson; +import org.springframework.jdbc.core.test.ExtendedPerson; +import org.springframework.jdbc.core.test.Person; +import org.springframework.jdbc.core.test.SpacePerson; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Thomas Risberg + * @author Juergen Hoeller + */ +public class BeanPropertyRowMapperTests extends AbstractRowMapperTests { + + @Test + @SuppressWarnings({"unchecked", "rawtypes"}) + public void testOverridingDifferentClassDefinedForMapping() { + BeanPropertyRowMapper mapper = new BeanPropertyRowMapper(Person.class); + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() -> + mapper.setMappedClass(Long.class)); + } + + @Test + public void testOverridingSameClassDefinedForMapping() { + BeanPropertyRowMapper mapper = new BeanPropertyRowMapper<>(Person.class); + mapper.setMappedClass(Person.class); + } + + @Test + public void testStaticQueryWithRowMapper() throws Exception { + Mock mock = new Mock(); + List result = mock.getJdbcTemplate().query( + "select name, age, birth_date, balance from people", + new BeanPropertyRowMapper<>(Person.class)); + assertThat(result.size()).isEqualTo(1); + verifyPerson(result.get(0)); + mock.verifyClosed(); + } + + @Test + public void testMappingWithInheritance() throws Exception { + Mock mock = new Mock(); + List result = mock.getJdbcTemplate().query( + "select name, age, birth_date, balance from people", + new BeanPropertyRowMapper<>(ConcretePerson.class)); + assertThat(result.size()).isEqualTo(1); + verifyPerson(result.get(0)); + mock.verifyClosed(); + } + + @Test + public void testMappingWithNoUnpopulatedFieldsFound() throws Exception { + Mock mock = new Mock(); + List result = mock.getJdbcTemplate().query( + "select name, age, birth_date, balance from people", + new BeanPropertyRowMapper<>(ConcretePerson.class, true)); + assertThat(result.size()).isEqualTo(1); + verifyPerson(result.get(0)); + mock.verifyClosed(); + } + + @Test + public void testMappingWithUnpopulatedFieldsNotChecked() throws Exception { + Mock mock = new Mock(); + List result = mock.getJdbcTemplate().query( + "select name, age, birth_date, balance from people", + new BeanPropertyRowMapper<>(ExtendedPerson.class)); + assertThat(result.size()).isEqualTo(1); + ExtendedPerson bean = result.get(0); + verifyPerson(bean); + mock.verifyClosed(); + } + + @Test + public void testMappingWithUnpopulatedFieldsNotAccepted() throws Exception { + Mock mock = new Mock(); + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() -> + mock.getJdbcTemplate().query("select name, age, birth_date, balance from people", + new BeanPropertyRowMapper<>(ExtendedPerson.class, true))); + } + + @Test + public void testMappingNullValue() throws Exception { + BeanPropertyRowMapper mapper = new BeanPropertyRowMapper<>(Person.class); + Mock mock = new Mock(MockType.TWO); + assertThatExceptionOfType(TypeMismatchException.class).isThrownBy(() -> + mock.getJdbcTemplate().query("select name, null as age, birth_date, balance from people", mapper)); + } + + @Test + public void testQueryWithSpaceInColumnNameAndLocalDateTime() throws Exception { + Mock mock = new Mock(MockType.THREE); + List result = mock.getJdbcTemplate().query( + "select last_name as \"Last Name\", age, birth_date, balance from people", + new BeanPropertyRowMapper<>(SpacePerson.class)); + assertThat(result.size()).isEqualTo(1); + verifyPerson(result.get(0)); + mock.verifyClosed(); + } + + @Test + public void testQueryWithSpaceInColumnNameAndLocalDate() throws Exception { + Mock mock = new Mock(MockType.THREE); + List result = mock.getJdbcTemplate().query( + "select last_name as \"Last Name\", age, birth_date, balance from people", + new BeanPropertyRowMapper<>(DatePerson.class)); + assertThat(result.size()).isEqualTo(1); + verifyPerson(result.get(0)); + mock.verifyClosed(); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java new file mode 100644 index 0000000..bc2cae0 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/DataClassRowMapperTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.jdbc.core.test.ConstructorPerson; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + * @since 5.3 + */ +public class DataClassRowMapperTests extends AbstractRowMapperTests { + + @Test + public void testStaticQueryWithDataClass() throws Exception { + Mock mock = new Mock(); + List result = mock.getJdbcTemplate().query( + "select name, age, birth_date, balance from people", + new DataClassRowMapper<>(ConstructorPerson.class)); + assertThat(result.size()).isEqualTo(1); + verifyPerson(result.get(0)); + + mock.verifyClosed(); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/JdbcTemplateQueryTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/JdbcTemplateQueryTests.java new file mode 100644 index 0000000..337f5f3 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/JdbcTemplateQueryTests.java @@ -0,0 +1,423 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.dao.IncorrectResultSizeDataAccessException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author Juergen Hoeller + * @author Phillip Webb + * @author Rob Winch + * @since 19.12.2004 + */ +public class JdbcTemplateQueryTests { + + private Connection connection; + + private DataSource dataSource; + + private Statement statement; + + private PreparedStatement preparedStatement; + + private ResultSet resultSet; + + private ResultSetMetaData resultSetMetaData; + + private JdbcTemplate template; + + + @BeforeEach + public void setUp() throws Exception { + this.connection = mock(Connection.class); + this.dataSource = mock(DataSource.class); + this.statement = mock(Statement.class); + this.preparedStatement = mock(PreparedStatement.class); + this.resultSet = mock(ResultSet.class); + this.resultSetMetaData = mock(ResultSetMetaData.class); + this.template = new JdbcTemplate(this.dataSource); + given(this.dataSource.getConnection()).willReturn(this.connection); + given(this.resultSet.getMetaData()).willReturn(this.resultSetMetaData); + given(this.resultSetMetaData.getColumnCount()).willReturn(1); + given(this.resultSetMetaData.getColumnLabel(1)).willReturn("age"); + given(this.connection.createStatement()).willReturn(this.statement); + given(this.connection.prepareStatement(anyString())).willReturn(this.preparedStatement); + given(this.preparedStatement.executeQuery()).willReturn(this.resultSet); + given(this.statement.executeQuery(anyString())).willReturn(this.resultSet); + } + + + @Test + public void testQueryForList() throws Exception { + String sql = "SELECT AGE FROM CUSTMR WHERE ID < 3"; + given(this.resultSet.next()).willReturn(true, true, false); + given(this.resultSet.getObject(1)).willReturn(11, 12); + List> li = this.template.queryForList(sql); + assertThat(li.size()).as("All rows returned").isEqualTo(2); + assertThat(((Integer) li.get(0).get("age")).intValue()).as("First row is Integer").isEqualTo(11); + assertThat(((Integer) li.get(1).get("age")).intValue()).as("Second row is Integer").isEqualTo(12); + verify(this.resultSet).close(); + verify(this.statement).close(); + } + + @Test + public void testQueryForListWithEmptyResult() throws Exception { + String sql = "SELECT AGE FROM CUSTMR WHERE ID < 3"; + given(this.resultSet.next()).willReturn(false); + List> li = this.template.queryForList(sql); + assertThat(li.size()).as("All rows returned").isEqualTo(0); + verify(this.resultSet).close(); + verify(this.statement).close(); + } + + @Test + public void testQueryForListWithSingleRowAndColumn() throws Exception { + String sql = "SELECT AGE FROM CUSTMR WHERE ID < 3"; + given(this.resultSet.next()).willReturn(true, false); + given(this.resultSet.getObject(1)).willReturn(11); + List> li = this.template.queryForList(sql); + assertThat(li.size()).as("All rows returned").isEqualTo(1); + assertThat(((Integer) li.get(0).get("age")).intValue()).as("First row is Integer").isEqualTo(11); + verify(this.resultSet).close(); + verify(this.statement).close(); + } + + @Test + public void testQueryForListWithIntegerElement() throws Exception { + String sql = "SELECT AGE FROM CUSTMR WHERE ID < 3"; + given(this.resultSet.next()).willReturn(true, false); + given(this.resultSet.getInt(1)).willReturn(11); + List li = this.template.queryForList(sql, Integer.class); + assertThat(li.size()).as("All rows returned").isEqualTo(1); + assertThat(li.get(0).intValue()).as("Element is Integer").isEqualTo(11); + verify(this.resultSet).close(); + verify(this.statement).close(); + } + + @Test + public void testQueryForMapWithSingleRowAndColumn() throws Exception { + String sql = "SELECT AGE FROM CUSTMR WHERE ID < 3"; + given(this.resultSet.next()).willReturn(true, false); + given(this.resultSet.getObject(1)).willReturn(11); + Map map = this.template.queryForMap(sql); + assertThat(((Integer) map.get("age")).intValue()).as("Wow is Integer").isEqualTo(11); + verify(this.resultSet).close(); + verify(this.statement).close(); + } + + @Test + public void testQueryForObjectThrowsIncorrectResultSizeForMoreThanOneRow() throws Exception { + String sql = "select pass from t_account where first_name='Alef'"; + given(this.resultSet.next()).willReturn(true, true, false); + given(this.resultSet.getString(1)).willReturn("pass"); + assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class).isThrownBy(() -> + this.template.queryForObject(sql, String.class)); + verify(this.resultSet).close(); + verify(this.statement).close(); + } + + @Test + public void testQueryForObjectWithRowMapper() throws Exception { + String sql = "SELECT AGE FROM CUSTMR WHERE ID = 3"; + given(this.resultSet.next()).willReturn(true, false); + given(this.resultSet.getInt(1)).willReturn(22); + Object o = this.template.queryForObject(sql, new RowMapper() { + @Override + public Integer mapRow(ResultSet rs, int rowNum) throws SQLException { + return rs.getInt(1); + } + }); + assertThat(o instanceof Integer).as("Correct result type").isTrue(); + verify(this.resultSet).close(); + verify(this.statement).close(); + } + + @Test + public void testQueryForStreamWithRowMapper() throws Exception { + String sql = "SELECT AGE FROM CUSTMR WHERE ID = 3"; + given(this.resultSet.next()).willReturn(true, false); + given(this.resultSet.getInt(1)).willReturn(22); + AtomicInteger count = new AtomicInteger(); + try (Stream s = this.template.queryForStream(sql, (rs, rowNum) -> rs.getInt(1))) { + s.forEach(val -> { + count.incrementAndGet(); + assertThat(val).isEqualTo(22); + }); + } + assertThat(count.get()).isEqualTo(1); + verify(this.resultSet).close(); + verify(this.statement).close(); + } + + @Test + public void testQueryForObjectWithString() throws Exception { + String sql = "SELECT AGE FROM CUSTMR WHERE ID = 3"; + given(this.resultSet.next()).willReturn(true, false); + given(this.resultSet.getString(1)).willReturn("myvalue"); + assertThat(this.template.queryForObject(sql, String.class)).isEqualTo("myvalue"); + verify(this.resultSet).close(); + verify(this.statement).close(); + } + + @Test + public void testQueryForObjectWithBigInteger() throws Exception { + String sql = "SELECT AGE FROM CUSTMR WHERE ID = 3"; + given(this.resultSet.next()).willReturn(true, false); + given(this.resultSet.getObject(1, BigInteger.class)).willReturn(new BigInteger("22")); + assertThat(this.template.queryForObject(sql, BigInteger.class)).isEqualTo(new BigInteger("22")); + verify(this.resultSet).close(); + verify(this.statement).close(); + } + + @Test + public void testQueryForObjectWithBigDecimal() throws Exception { + String sql = "SELECT AGE FROM CUSTMR WHERE ID = 3"; + given(this.resultSet.next()).willReturn(true, false); + given(this.resultSet.getBigDecimal(1)).willReturn(new BigDecimal("22.5")); + assertThat(this.template.queryForObject(sql, BigDecimal.class)).isEqualTo(new BigDecimal("22.5")); + verify(this.resultSet).close(); + verify(this.statement).close(); + } + + @Test + public void testQueryForObjectWithInteger() throws Exception { + String sql = "SELECT AGE FROM CUSTMR WHERE ID = 3"; + given(this.resultSet.next()).willReturn(true, false); + given(this.resultSet.getInt(1)).willReturn(22); + assertThat(this.template.queryForObject(sql, Integer.class)).isEqualTo(Integer.valueOf(22)); + verify(this.resultSet).close(); + verify(this.statement).close(); + } + + @Test + public void testQueryForObjectWithIntegerAndNull() throws Exception { + String sql = "SELECT AGE FROM CUSTMR WHERE ID = 3"; + given(this.resultSet.next()).willReturn(true, false); + given(this.resultSet.getInt(1)).willReturn(0); + given(this.resultSet.wasNull()).willReturn(true); + assertThat(this.template.queryForObject(sql, Integer.class)).isNull(); + verify(this.resultSet).close(); + verify(this.statement).close(); + } + + @Test + public void testQueryForInt() throws Exception { + String sql = "SELECT AGE FROM CUSTMR WHERE ID = 3"; + given(this.resultSet.next()).willReturn(true, false); + given(this.resultSet.getInt(1)).willReturn(22); + int i = this.template.queryForObject(sql, Integer.class).intValue(); + assertThat(i).as("Return of an int").isEqualTo(22); + verify(this.resultSet).close(); + verify(this.statement).close(); + } + + @Test + public void testQueryForIntPrimitive() throws Exception { + String sql = "SELECT AGE FROM CUSTMR WHERE ID = 3"; + given(this.resultSet.next()).willReturn(true, false); + given(this.resultSet.getInt(1)).willReturn(22); + int i = this.template.queryForObject(sql, int.class); + assertThat(i).as("Return of an int").isEqualTo(22); + verify(this.resultSet).close(); + verify(this.statement).close(); + } + + @Test + public void testQueryForLong() throws Exception { + String sql = "SELECT AGE FROM CUSTMR WHERE ID = 3"; + given(this.resultSet.next()).willReturn(true, false); + given(this.resultSet.getLong(1)).willReturn(87L); + long l = this.template.queryForObject(sql, Long.class).longValue(); + assertThat(l).as("Return of a long").isEqualTo(87); + verify(this.resultSet).close(); + verify(this.statement).close(); + } + + @Test + public void testQueryForLongPrimitive() throws Exception { + String sql = "SELECT AGE FROM CUSTMR WHERE ID = 3"; + given(this.resultSet.next()).willReturn(true, false); + given(this.resultSet.getLong(1)).willReturn(87L); + long l = this.template.queryForObject(sql, long.class); + assertThat(l).as("Return of a long").isEqualTo(87); + verify(this.resultSet).close(); + verify(this.statement).close(); + } + + @Test + public void testQueryForListWithArgs() throws Exception { + doTestQueryForListWithArgs("SELECT AGE FROM CUSTMR WHERE ID < ?"); + } + + @Test + public void testQueryForListIsNotConfusedByNamedParameterPrefix() throws Exception { + doTestQueryForListWithArgs("SELECT AGE FROM PREFIX:CUSTMR WHERE ID < ?"); + } + + private void doTestQueryForListWithArgs(String sql) throws Exception { + given(this.resultSet.next()).willReturn(true, true, false); + given(this.resultSet.getObject(1)).willReturn(11, 12); + List> li = this.template.queryForList(sql, 3); + assertThat(li.size()).as("All rows returned").isEqualTo(2); + assertThat(((Integer) li.get(0).get("age")).intValue()).as("First row is Integer").isEqualTo(11); + assertThat(((Integer) li.get(1).get("age")).intValue()).as("Second row is Integer").isEqualTo(12); + verify(this.preparedStatement).setObject(1, 3); + verify(this.resultSet).close(); + verify(this.preparedStatement).close(); + } + + @Test + public void testQueryForListWithArgsAndEmptyResult() throws Exception { + String sql = "SELECT AGE FROM CUSTMR WHERE ID < ?"; + given(this.resultSet.next()).willReturn(false); + List> li = this.template.queryForList(sql, 3); + assertThat(li.size()).as("All rows returned").isEqualTo(0); + verify(this.preparedStatement).setObject(1, 3); + verify(this.resultSet).close(); + verify(this.preparedStatement).close(); + } + + @Test + public void testQueryForListWithArgsAndSingleRowAndColumn() throws Exception { + String sql = "SELECT AGE FROM CUSTMR WHERE ID < ?"; + given(this.resultSet.next()).willReturn(true, false); + given(this.resultSet.getObject(1)).willReturn(11); + List> li = this.template.queryForList(sql, 3); + assertThat(li.size()).as("All rows returned").isEqualTo(1); + assertThat(((Integer) li.get(0).get("age")).intValue()).as("First row is Integer").isEqualTo(11); + verify(this.preparedStatement).setObject(1, 3); + verify(this.resultSet).close(); + verify(this.preparedStatement).close(); + } + + @Test + public void testQueryForListWithArgsAndIntegerElementAndSingleRowAndColumn() throws Exception { + String sql = "SELECT AGE FROM CUSTMR WHERE ID < ?"; + given(this.resultSet.next()).willReturn(true, false); + given(this.resultSet.getInt(1)).willReturn(11); + List li = this.template.queryForList(sql, Integer.class, 3); + assertThat(li.size()).as("All rows returned").isEqualTo(1); + assertThat(li.get(0).intValue()).as("First row is Integer").isEqualTo(11); + verify(this.preparedStatement).setObject(1, 3); + verify(this.resultSet).close(); + verify(this.preparedStatement).close(); + } + + @Test + public void testQueryForMapWithArgsAndSingleRowAndColumn() throws Exception { + String sql = "SELECT AGE FROM CUSTMR WHERE ID < ?"; + given(this.resultSet.next()).willReturn(true, false); + given(this.resultSet.getObject(1)).willReturn(11); + Map map = this.template.queryForMap(sql, 3); + assertThat(((Integer) map.get("age")).intValue()).as("Row is Integer").isEqualTo(11); + verify(this.preparedStatement).setObject(1, 3); + verify(this.resultSet).close(); + verify(this.preparedStatement).close(); + } + + @Test + public void testQueryForObjectWithArgsAndRowMapper() throws Exception { + String sql = "SELECT AGE FROM CUSTMR WHERE ID = ?"; + given(this.resultSet.next()).willReturn(true, false); + given(this.resultSet.getInt(1)).willReturn(22); + Object o = this.template.queryForObject(sql, (rs, rowNum) -> rs.getInt(1), 3); + assertThat(o instanceof Integer).as("Correct result type").isTrue(); + verify(this.preparedStatement).setObject(1, 3); + verify(this.resultSet).close(); + verify(this.preparedStatement).close(); + } + + @Test + public void testQueryForStreamWithArgsAndRowMapper() throws Exception { + String sql = "SELECT AGE FROM CUSTMR WHERE ID = ?"; + given(this.resultSet.next()).willReturn(true, false); + given(this.resultSet.getInt(1)).willReturn(22); + AtomicInteger count = new AtomicInteger(); + try (Stream s = this.template.queryForStream(sql, (rs, rowNum) -> rs.getInt(1), 3)) { + s.forEach(val -> { + count.incrementAndGet(); + assertThat(val).isEqualTo(22); + }); + } + assertThat(count.get()).isEqualTo(1); + verify(this.preparedStatement).setObject(1, 3); + verify(this.resultSet).close(); + verify(this.preparedStatement).close(); + } + + @Test + public void testQueryForObjectWithArgsAndInteger() throws Exception { + String sql = "SELECT AGE FROM CUSTMR WHERE ID = ?"; + given(this.resultSet.next()).willReturn(true, false); + given(this.resultSet.getInt(1)).willReturn(22); + Object o = this.template.queryForObject(sql, Integer.class, 3); + assertThat(o instanceof Integer).as("Correct result type").isTrue(); + verify(this.preparedStatement).setObject(1, 3); + verify(this.resultSet).close(); + verify(this.preparedStatement).close(); + } + + @Test + public void testQueryForIntWithArgs() throws Exception { + String sql = "SELECT AGE FROM CUSTMR WHERE ID = ?"; + given(this.resultSet.next()).willReturn(true, false); + given(this.resultSet.getInt(1)).willReturn(22); + int i = this.template.queryForObject(sql, Integer.class, 3).intValue(); + assertThat(i).as("Return of an int").isEqualTo(22); + verify(this.preparedStatement).setObject(1, 3); + verify(this.resultSet).close(); + verify(this.preparedStatement).close(); + } + + @Test + public void testQueryForLongWithArgs() throws Exception { + String sql = "SELECT AGE FROM CUSTMR WHERE ID = ?"; + given(this.resultSet.next()).willReturn(true, false); + given(this.resultSet.getLong(1)).willReturn(87L); + long l = this.template.queryForObject(sql, Long.class, 3).longValue(); + assertThat(l).as("Return of a long").isEqualTo(87); + verify(this.preparedStatement).setObject(1, 3); + verify(this.resultSet).close(); + verify(this.preparedStatement).close(); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/JdbcTemplateTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/JdbcTemplateTests.java new file mode 100644 index 0000000..339bec5 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/JdbcTemplateTests.java @@ -0,0 +1,1133 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.BatchUpdateException; +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.Statement; +import java.sql.Types; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.jdbc.CannotGetJdbcConnectionException; +import org.springframework.jdbc.SQLWarningException; +import org.springframework.jdbc.UncategorizedSQLException; +import org.springframework.jdbc.core.support.AbstractInterruptibleBatchPreparedStatementSetter; +import org.springframework.jdbc.datasource.ConnectionProxy; +import org.springframework.jdbc.datasource.SingleConnectionDataSource; +import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator; +import org.springframework.jdbc.support.SQLStateSQLExceptionTranslator; +import org.springframework.util.LinkedCaseInsensitiveMap; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Mock object based tests for JdbcTemplate. + * + * @author Rod Johnson + * @author Thomas Risberg + * @author Juergen Hoeller + * @author Phillip Webb + */ +public class JdbcTemplateTests { + + private Connection connection; + + private DataSource dataSource; + + private PreparedStatement preparedStatement; + + private Statement statement; + + private ResultSet resultSet; + + private JdbcTemplate template; + + private CallableStatement callableStatement; + + + @BeforeEach + public void setup() throws Exception { + this.connection = mock(Connection.class); + this.dataSource = mock(DataSource.class); + this.preparedStatement = mock(PreparedStatement.class); + this.statement = mock(Statement.class); + this.resultSet = mock(ResultSet.class); + this.template = new JdbcTemplate(this.dataSource); + this.callableStatement = mock(CallableStatement.class); + given(this.dataSource.getConnection()).willReturn(this.connection); + given(this.connection.prepareStatement(anyString())).willReturn(this.preparedStatement); + given(this.preparedStatement.executeQuery()).willReturn(this.resultSet); + given(this.preparedStatement.executeQuery(anyString())).willReturn(this.resultSet); + given(this.preparedStatement.getConnection()).willReturn(this.connection); + given(this.statement.getConnection()).willReturn(this.connection); + given(this.statement.executeQuery(anyString())).willReturn(this.resultSet); + given(this.connection.prepareCall(anyString())).willReturn(this.callableStatement); + given(this.callableStatement.getResultSet()).willReturn(this.resultSet); + } + + + @Test + public void testBeanProperties() throws Exception { + assertThat(this.template.getDataSource() == this.dataSource).as("datasource ok").isTrue(); + assertThat(this.template.isIgnoreWarnings()).as("ignores warnings by default").isTrue(); + this.template.setIgnoreWarnings(false); + boolean condition = !this.template.isIgnoreWarnings(); + assertThat(condition).as("can set NOT to ignore warnings").isTrue(); + } + + @Test + public void testUpdateCount() throws Exception { + final String sql = "UPDATE INVOICE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?"; + int idParam = 11111; + given(this.preparedStatement.executeUpdate()).willReturn(1); + Dispatcher d = new Dispatcher(idParam, sql); + int rowsAffected = this.template.update(d); + assertThat(rowsAffected == 1).as("1 update affected 1 row").isTrue(); + verify(this.preparedStatement).setInt(1, idParam); + verify(this.preparedStatement).close(); + verify(this.connection).close(); + } + + @Test + public void testBogusUpdate() throws Exception { + final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?"; + final int idParam = 6666; + + // It's because Integers aren't canonical + SQLException sqlException = new SQLException("bad update"); + given(this.preparedStatement.executeUpdate()).willThrow(sqlException); + + Dispatcher d = new Dispatcher(idParam, sql); + assertThatExceptionOfType(UncategorizedSQLException.class).isThrownBy(() -> + this.template.update(d)) + .withCause(sqlException); + verify(this.preparedStatement).setInt(1, idParam); + verify(this.preparedStatement).close(); + verify(this.connection, atLeastOnce()).close(); + } + + @Test + public void testStringsWithStaticSql() throws Exception { + doTestStrings(null, null, null, null, (template, sql, rch) -> template.query(sql, rch)); + } + + @Test + public void testStringsWithStaticSqlAndFetchSizeAndMaxRows() throws Exception { + doTestStrings(10, 20, 30, null, (template, sql, rch) -> template.query(sql, rch)); + } + + @Test + public void testStringsWithEmptyPreparedStatementSetter() throws Exception { + doTestStrings(null, null, null, null, (template, sql, rch) -> + template.query(sql, (PreparedStatementSetter) null, rch)); + } + + @Test + public void testStringsWithPreparedStatementSetter() throws Exception { + final Integer argument = 99; + doTestStrings(null, null, null, argument, (template, sql, rch) -> + template.query(sql, ps -> ps.setObject(1, argument), rch)); + } + + @Test + @SuppressWarnings("deprecation") + public void testStringsWithEmptyPreparedStatementArgs() throws Exception { + doTestStrings(null, null, null, null, + (template, sql, rch) -> template.query(sql, (Object[]) null, rch)); + } + + @Test + @SuppressWarnings("deprecation") + public void testStringsWithPreparedStatementArgs() throws Exception { + final Integer argument = 99; + doTestStrings(null, null, null, argument, + (template, sql, rch) -> template.query(sql, new Object[] {argument}, rch)); + } + + private void doTestStrings(Integer fetchSize, Integer maxRows, Integer queryTimeout, + Object argument, JdbcTemplateCallback jdbcTemplateCallback) throws Exception { + + String sql = "SELECT FORENAME FROM CUSTMR"; + String[] results = {"rod", "gary", " portia"}; + + class StringHandler implements RowCallbackHandler { + private List list = new ArrayList<>(); + @Override + public void processRow(ResultSet rs) throws SQLException { + this.list.add(rs.getString(1)); + } + public String[] getStrings() { + return StringUtils.toStringArray(this.list); + } + } + + given(this.resultSet.next()).willReturn(true, true, true, false); + given(this.resultSet.getString(1)).willReturn(results[0], results[1], results[2]); + given(this.connection.createStatement()).willReturn(this.preparedStatement); + + StringHandler sh = new StringHandler(); + JdbcTemplate template = new JdbcTemplate(); + template.setDataSource(this.dataSource); + if (fetchSize != null) { + template.setFetchSize(fetchSize.intValue()); + } + if (maxRows != null) { + template.setMaxRows(maxRows.intValue()); + } + if (queryTimeout != null) { + template.setQueryTimeout(queryTimeout.intValue()); + } + jdbcTemplateCallback.doInJdbcTemplate(template, sql, sh); + + // Match + String[] forenames = sh.getStrings(); + assertThat(forenames.length == results.length).as("same length").isTrue(); + for (int i = 0; i < forenames.length; i++) { + assertThat(forenames[i].equals(results[i])).as("Row " + i + " matches").isTrue(); + } + + if (fetchSize != null) { + verify(this.preparedStatement).setFetchSize(fetchSize.intValue()); + } + if (maxRows != null) { + verify(this.preparedStatement).setMaxRows(maxRows.intValue()); + } + if (queryTimeout != null) { + verify(this.preparedStatement).setQueryTimeout(queryTimeout.intValue()); + } + if (argument != null) { + verify(this.preparedStatement).setObject(1, argument); + } + verify(this.resultSet).close(); + verify(this.preparedStatement).close(); + verify(this.connection).close(); + } + + @Test + public void testLeaveConnectionOpenOnRequest() throws Exception { + String sql = "SELECT ID, FORENAME FROM CUSTMR WHERE ID < 3"; + + given(this.resultSet.next()).willReturn(false); + given(this.connection.isClosed()).willReturn(false); + given(this.connection.createStatement()).willReturn(this.preparedStatement); + // if close is called entire test will fail + willThrow(new RuntimeException()).given(this.connection).close(); + + SingleConnectionDataSource scf = new SingleConnectionDataSource(this.dataSource.getConnection(), false); + this.template = new JdbcTemplate(scf, false); + RowCountCallbackHandler rcch = new RowCountCallbackHandler(); + this.template.query(sql, rcch); + + verify(this.resultSet).close(); + verify(this.preparedStatement).close(); + } + + @Test + public void testConnectionCallback() throws Exception { + String result = this.template.execute(new ConnectionCallback() { + @Override + public String doInConnection(Connection con) { + assertThat(con instanceof ConnectionProxy).isTrue(); + assertThat(((ConnectionProxy) con).getTargetConnection()).isSameAs(JdbcTemplateTests.this.connection); + return "test"; + } + }); + assertThat(result).isEqualTo("test"); + } + + @Test + public void testConnectionCallbackWithStatementSettings() throws Exception { + String result = this.template.execute(new ConnectionCallback() { + @Override + public String doInConnection(Connection con) throws SQLException { + PreparedStatement ps = con.prepareStatement("some SQL"); + ps.setFetchSize(10); + ps.setMaxRows(20); + ps.close(); + return "test"; + } + }); + + assertThat(result).isEqualTo("test"); + verify(this.preparedStatement).setFetchSize(10); + verify(this.preparedStatement).setMaxRows(20); + verify(this.preparedStatement).close(); + verify(this.connection).close(); + } + + @Test + public void testCloseConnectionOnRequest() throws Exception { + String sql = "SELECT ID, FORENAME FROM CUSTMR WHERE ID < 3"; + + given(this.resultSet.next()).willReturn(false); + given(this.connection.createStatement()).willReturn(this.preparedStatement); + + RowCountCallbackHandler rcch = new RowCountCallbackHandler(); + this.template.query(sql, rcch); + + verify(this.resultSet).close(); + verify(this.preparedStatement).close(); + verify(this.connection).close(); + } + + /** + * Test that we see a runtime exception come back. + */ + @Test + public void testExceptionComesBack() throws Exception { + final String sql = "SELECT ID FROM CUSTMR"; + final RuntimeException runtimeException = new RuntimeException("Expected"); + + given(this.resultSet.next()).willReturn(true); + given(this.connection.createStatement()).willReturn(this.preparedStatement); + + try { + assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> + this.template.query(sql, (RowCallbackHandler) rs -> { + throw runtimeException; + })) + .withMessage(runtimeException.getMessage()); + } + finally { + verify(this.resultSet).close(); + verify(this.preparedStatement).close(); + verify(this.connection).close(); + } + } + + /** + * Test update with static SQL. + */ + @Test + public void testSqlUpdate() throws Exception { + final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = 4"; + int rowsAffected = 33; + + given(this.statement.executeUpdate(sql)).willReturn(rowsAffected); + given(this.connection.createStatement()).willReturn(this.statement); + + int actualRowsAffected = this.template.update(sql); + assertThat(actualRowsAffected == rowsAffected).as("Actual rows affected is correct").isTrue(); + verify(this.statement).close(); + verify(this.connection).close(); + } + + /** + * Test update with dynamic SQL. + */ + @Test + public void testSqlUpdateWithArguments() throws Exception { + final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ? and PR = ?"; + int rowsAffected = 33; + given(this.preparedStatement.executeUpdate()).willReturn(rowsAffected); + + int actualRowsAffected = this.template.update(sql, + 4, new SqlParameterValue(Types.NUMERIC, 2, Float.valueOf(1.4142f))); + assertThat(actualRowsAffected == rowsAffected).as("Actual rows affected is correct").isTrue(); + verify(this.preparedStatement).setObject(1, 4); + verify(this.preparedStatement).setObject(2, Float.valueOf(1.4142f), Types.NUMERIC, 2); + verify(this.preparedStatement).close(); + verify(this.connection).close(); + } + + @Test + public void testSqlUpdateEncountersSqlException() throws Exception { + SQLException sqlException = new SQLException("bad update"); + final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = 4"; + + given(this.statement.executeUpdate(sql)).willThrow(sqlException); + given(this.connection.createStatement()).willReturn(this.statement); + + assertThatExceptionOfType(DataAccessException.class).isThrownBy(() -> + this.template.update(sql)) + .withCause(sqlException); + verify(this.statement).close(); + verify(this.connection, atLeastOnce()).close(); + } + + @Test + public void testSqlUpdateWithThreadConnection() throws Exception { + final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = 4"; + int rowsAffected = 33; + + given(this.statement.executeUpdate(sql)).willReturn(rowsAffected); + given(this.connection.createStatement()).willReturn(this.statement); + + int actualRowsAffected = this.template.update(sql); + assertThat(actualRowsAffected == rowsAffected).as("Actual rows affected is correct").isTrue(); + + verify(this.statement).close(); + verify(this.connection).close(); + } + + @Test + public void testBatchUpdate() throws Exception { + final String[] sql = {"UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = 1", + "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = 2"}; + + given(this.statement.executeBatch()).willReturn(new int[] {1, 1}); + mockDatabaseMetaData(true); + given(this.connection.createStatement()).willReturn(this.statement); + + JdbcTemplate template = new JdbcTemplate(this.dataSource, false); + + int[] actualRowsAffected = template.batchUpdate(sql); + assertThat(actualRowsAffected.length == 2).as("executed 2 updates").isTrue(); + + verify(this.statement).addBatch(sql[0]); + verify(this.statement).addBatch(sql[1]); + verify(this.statement).close(); + verify(this.connection, atLeastOnce()).close(); + } + + @Test + public void testBatchUpdateWithBatchFailure() throws Exception { + final String[] sql = {"A", "B", "C", "D"}; + given(this.statement.executeBatch()).willThrow( + new BatchUpdateException(new int[] {1, Statement.EXECUTE_FAILED, 1, Statement.EXECUTE_FAILED})); + mockDatabaseMetaData(true); + given(this.connection.createStatement()).willReturn(this.statement); + + JdbcTemplate template = new JdbcTemplate(this.dataSource, false); + try { + template.batchUpdate(sql); + } + catch (UncategorizedSQLException ex) { + assertThat(ex.getSql()).isEqualTo("B; D"); + } + } + + @Test + public void testBatchUpdateWithNoBatchSupport() throws Exception { + final String[] sql = {"UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = 1", + "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = 2"}; + + given(this.statement.execute(sql[0])).willReturn(false); + given(this.statement.getUpdateCount()).willReturn(1, 1); + given(this.statement.execute(sql[1])).willReturn(false); + + mockDatabaseMetaData(false); + given(this.connection.createStatement()).willReturn(this.statement); + + JdbcTemplate template = new JdbcTemplate(this.dataSource, false); + + int[] actualRowsAffected = template.batchUpdate(sql); + assertThat(actualRowsAffected.length == 2).as("executed 2 updates").isTrue(); + + verify(this.statement, never()).addBatch(anyString()); + verify(this.statement).close(); + verify(this.connection, atLeastOnce()).close(); + } + + @Test + public void testBatchUpdateWithNoBatchSupportAndSelect() throws Exception { + final String[] sql = {"UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = 1", + "SELECT * FROM NOSUCHTABLE"}; + + given(this.statement.execute(sql[0])).willReturn(false); + given(this.statement.getUpdateCount()).willReturn(1); + given(this.statement.execute(sql[1])).willReturn(true); + mockDatabaseMetaData(false); + given(this.connection.createStatement()).willReturn(this.statement); + + JdbcTemplate template = new JdbcTemplate(this.dataSource, false); + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() -> + template.batchUpdate(sql)); + verify(this.statement, never()).addBatch(anyString()); + verify(this.statement).close(); + verify(this.connection, atLeastOnce()).close(); + } + + @Test + public void testBatchUpdateWithPreparedStatement() throws Exception { + final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?"; + final int[] ids = new int[] {100, 200}; + final int[] rowsAffected = new int[] {1, 2}; + + given(this.preparedStatement.executeBatch()).willReturn(rowsAffected); + mockDatabaseMetaData(true); + + BatchPreparedStatementSetter setter = new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + ps.setInt(1, ids[i]); + } + @Override + public int getBatchSize() { + return ids.length; + } + }; + + JdbcTemplate template = new JdbcTemplate(this.dataSource, false); + + int[] actualRowsAffected = template.batchUpdate(sql, setter); + assertThat(actualRowsAffected.length == 2).as("executed 2 updates").isTrue(); + assertThat(actualRowsAffected[0]).isEqualTo(rowsAffected[0]); + assertThat(actualRowsAffected[1]).isEqualTo(rowsAffected[1]); + + verify(this.preparedStatement, times(2)).addBatch(); + verify(this.preparedStatement).setInt(1, ids[0]); + verify(this.preparedStatement).setInt(1, ids[1]); + verify(this.preparedStatement).close(); + verify(this.connection, atLeastOnce()).close(); + } + + @Test + public void testInterruptibleBatchUpdate() throws Exception { + final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?"; + final int[] ids = new int[] {100, 200}; + final int[] rowsAffected = new int[] {1, 2}; + + given(this.preparedStatement.executeBatch()).willReturn(rowsAffected); + mockDatabaseMetaData(true); + + BatchPreparedStatementSetter setter = + new InterruptibleBatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + if (i < ids.length) { + ps.setInt(1, ids[i]); + } + } + @Override + public int getBatchSize() { + return 1000; + } + @Override + public boolean isBatchExhausted(int i) { + return (i >= ids.length); + } + }; + + JdbcTemplate template = new JdbcTemplate(this.dataSource, false); + + int[] actualRowsAffected = template.batchUpdate(sql, setter); + assertThat(actualRowsAffected.length == 2).as("executed 2 updates").isTrue(); + assertThat(actualRowsAffected[0]).isEqualTo(rowsAffected[0]); + assertThat(actualRowsAffected[1]).isEqualTo(rowsAffected[1]); + + verify(this.preparedStatement, times(2)).addBatch(); + verify(this.preparedStatement).setInt(1, ids[0]); + verify(this.preparedStatement).setInt(1, ids[1]); + verify(this.preparedStatement).close(); + verify(this.connection, atLeastOnce()).close(); + } + + @Test + public void testInterruptibleBatchUpdateWithBaseClass() throws Exception { + final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?"; + final int[] ids = new int[] {100, 200}; + final int[] rowsAffected = new int[] {1, 2}; + + given(this.preparedStatement.executeBatch()).willReturn(rowsAffected); + mockDatabaseMetaData(true); + + BatchPreparedStatementSetter setter = + new AbstractInterruptibleBatchPreparedStatementSetter() { + @Override + protected boolean setValuesIfAvailable(PreparedStatement ps, int i) throws SQLException { + if (i < ids.length) { + ps.setInt(1, ids[i]); + return true; + } + else { + return false; + } + } + }; + + JdbcTemplate template = new JdbcTemplate(this.dataSource, false); + + int[] actualRowsAffected = template.batchUpdate(sql, setter); + assertThat(actualRowsAffected.length == 2).as("executed 2 updates").isTrue(); + assertThat(actualRowsAffected[0]).isEqualTo(rowsAffected[0]); + assertThat(actualRowsAffected[1]).isEqualTo(rowsAffected[1]); + + verify(this.preparedStatement, times(2)).addBatch(); + verify(this.preparedStatement).setInt(1, ids[0]); + verify(this.preparedStatement).setInt(1, ids[1]); + verify(this.preparedStatement).close(); + verify(this.connection, atLeastOnce()).close(); + } + + @Test + public void testInterruptibleBatchUpdateWithBaseClassAndNoBatchSupport() throws Exception { + final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?"; + final int[] ids = new int[] {100, 200}; + final int[] rowsAffected = new int[] {1, 2}; + + given(this.preparedStatement.executeUpdate()).willReturn(rowsAffected[0], rowsAffected[1]); + mockDatabaseMetaData(false); + + BatchPreparedStatementSetter setter = + new AbstractInterruptibleBatchPreparedStatementSetter() { + @Override + protected boolean setValuesIfAvailable(PreparedStatement ps, int i) throws SQLException { + if (i < ids.length) { + ps.setInt(1, ids[i]); + return true; + } + else { + return false; + } + } + }; + + JdbcTemplate template = new JdbcTemplate(this.dataSource, false); + + int[] actualRowsAffected = template.batchUpdate(sql, setter); + assertThat(actualRowsAffected.length == 2).as("executed 2 updates").isTrue(); + assertThat(actualRowsAffected[0]).isEqualTo(rowsAffected[0]); + assertThat(actualRowsAffected[1]).isEqualTo(rowsAffected[1]); + + verify(this.preparedStatement, never()).addBatch(); + verify(this.preparedStatement).setInt(1, ids[0]); + verify(this.preparedStatement).setInt(1, ids[1]); + verify(this.preparedStatement).close(); + verify(this.connection, atLeastOnce()).close(); + } + + @Test + public void testBatchUpdateWithPreparedStatementAndNoBatchSupport() throws Exception { + final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?"; + final int[] ids = new int[] {100, 200}; + final int[] rowsAffected = new int[] {1, 2}; + + given(this.preparedStatement.executeUpdate()).willReturn(rowsAffected[0], rowsAffected[1]); + + BatchPreparedStatementSetter setter = new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + ps.setInt(1, ids[i]); + } + @Override + public int getBatchSize() { + return ids.length; + } + }; + + int[] actualRowsAffected = this.template.batchUpdate(sql, setter); + assertThat(actualRowsAffected.length == 2).as("executed 2 updates").isTrue(); + assertThat(actualRowsAffected[0]).isEqualTo(rowsAffected[0]); + assertThat(actualRowsAffected[1]).isEqualTo(rowsAffected[1]); + + verify(this.preparedStatement, never()).addBatch(); + verify(this.preparedStatement).setInt(1, ids[0]); + verify(this.preparedStatement).setInt(1, ids[1]); + verify(this.preparedStatement).close(); + verify(this.connection).close(); + } + + @Test + public void testBatchUpdateFails() throws Exception { + final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?"; + final int[] ids = new int[] {100, 200}; + SQLException sqlException = new SQLException(); + + given(this.preparedStatement.executeBatch()).willThrow(sqlException); + mockDatabaseMetaData(true); + + BatchPreparedStatementSetter setter = new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + ps.setInt(1, ids[i]); + } + @Override + public int getBatchSize() { + return ids.length; + } + }; + + try { + assertThatExceptionOfType(DataAccessException.class).isThrownBy(() -> + this.template.batchUpdate(sql, setter)) + .withCause(sqlException); + } + finally { + verify(this.preparedStatement, times(2)).addBatch(); + verify(this.preparedStatement).setInt(1, ids[0]); + verify(this.preparedStatement).setInt(1, ids[1]); + verify(this.preparedStatement).close(); + verify(this.connection, atLeastOnce()).close(); + } + } + + @Test + public void testBatchUpdateWithEmptyList() throws Exception { + final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?"; + JdbcTemplate template = new JdbcTemplate(this.dataSource, false); + + int[] actualRowsAffected = template.batchUpdate(sql, Collections.emptyList()); + assertThat(actualRowsAffected.length == 0).as("executed 0 updates").isTrue(); + } + + @Test + public void testBatchUpdateWithListOfObjectArrays() throws Exception { + final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?"; + final List ids = new ArrayList<>(2); + ids.add(new Object[] {100}); + ids.add(new Object[] {200}); + final int[] rowsAffected = new int[] {1, 2}; + + given(this.preparedStatement.executeBatch()).willReturn(rowsAffected); + mockDatabaseMetaData(true); + JdbcTemplate template = new JdbcTemplate(this.dataSource, false); + + int[] actualRowsAffected = template.batchUpdate(sql, ids); + assertThat(actualRowsAffected.length == 2).as("executed 2 updates").isTrue(); + assertThat(actualRowsAffected[0]).isEqualTo(rowsAffected[0]); + assertThat(actualRowsAffected[1]).isEqualTo(rowsAffected[1]); + + verify(this.preparedStatement, times(2)).addBatch(); + verify(this.preparedStatement).setObject(1, 100); + verify(this.preparedStatement).setObject(1, 200); + verify(this.preparedStatement).close(); + verify(this.connection, atLeastOnce()).close(); + } + + @Test + public void testBatchUpdateWithListOfObjectArraysPlusTypeInfo() throws Exception { + final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?"; + final List ids = new ArrayList<>(2); + ids.add(new Object[] {100}); + ids.add(new Object[] {200}); + final int[] sqlTypes = new int[] {Types.NUMERIC}; + final int[] rowsAffected = new int[] {1, 2}; + + given(this.preparedStatement.executeBatch()).willReturn(rowsAffected); + mockDatabaseMetaData(true); + this.template = new JdbcTemplate(this.dataSource, false); + + int[] actualRowsAffected = this.template.batchUpdate(sql, ids, sqlTypes); + assertThat(actualRowsAffected.length == 2).as("executed 2 updates").isTrue(); + assertThat(actualRowsAffected[0]).isEqualTo(rowsAffected[0]); + assertThat(actualRowsAffected[1]).isEqualTo(rowsAffected[1]); + verify(this.preparedStatement, times(2)).addBatch(); + verify(this.preparedStatement).setObject(1, 100, sqlTypes[0]); + verify(this.preparedStatement).setObject(1, 200, sqlTypes[0]); + verify(this.preparedStatement).close(); + verify(this.connection, atLeastOnce()).close(); + } + + @Test + public void testBatchUpdateWithCollectionOfObjects() throws Exception { + final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?"; + final List ids = Arrays.asList(100, 200, 300); + final int[] rowsAffected1 = new int[] {1, 2}; + final int[] rowsAffected2 = new int[] {3}; + + given(this.preparedStatement.executeBatch()).willReturn(rowsAffected1, rowsAffected2); + mockDatabaseMetaData(true); + + ParameterizedPreparedStatementSetter setter = (ps, argument) -> ps.setInt(1, argument.intValue()); + JdbcTemplate template = new JdbcTemplate(this.dataSource, false); + + int[][] actualRowsAffected = template.batchUpdate(sql, ids, 2, setter); + assertThat(actualRowsAffected[0].length).as("executed 2 updates").isEqualTo(2); + assertThat(actualRowsAffected[0][0]).isEqualTo(rowsAffected1[0]); + assertThat(actualRowsAffected[0][1]).isEqualTo(rowsAffected1[1]); + assertThat(actualRowsAffected[1][0]).isEqualTo(rowsAffected2[0]); + + verify(this.preparedStatement, times(3)).addBatch(); + verify(this.preparedStatement).setInt(1, ids.get(0)); + verify(this.preparedStatement).setInt(1, ids.get(1)); + verify(this.preparedStatement).setInt(1, ids.get(2)); + verify(this.preparedStatement).close(); + verify(this.connection, atLeastOnce()).close(); + } + + @Test + public void testCouldNotGetConnectionForOperationOrExceptionTranslator() throws SQLException { + SQLException sqlException = new SQLException("foo", "07xxx"); + this.dataSource = mock(DataSource.class); + given(this.dataSource.getConnection()).willThrow(sqlException); + JdbcTemplate template = new JdbcTemplate(this.dataSource, false); + RowCountCallbackHandler rcch = new RowCountCallbackHandler(); + + assertThatExceptionOfType(CannotGetJdbcConnectionException.class).isThrownBy(() -> + template.query("SELECT ID, FORENAME FROM CUSTMR WHERE ID < 3", rcch)) + .withCause(sqlException); + } + + @Test + public void testCouldNotGetConnectionForOperationWithLazyExceptionTranslator() throws SQLException { + SQLException sqlException = new SQLException("foo", "07xxx"); + this.dataSource = mock(DataSource.class); + given(this.dataSource.getConnection()).willThrow(sqlException); + this.template = new JdbcTemplate(); + this.template.setDataSource(this.dataSource); + this.template.afterPropertiesSet(); + RowCountCallbackHandler rcch = new RowCountCallbackHandler(); + + assertThatExceptionOfType(CannotGetJdbcConnectionException.class).isThrownBy(() -> + this.template.query("SELECT ID, FORENAME FROM CUSTMR WHERE ID < 3", rcch)) + .withCause(sqlException); + } + + @Test + public void testCouldNotGetConnectionInOperationWithExceptionTranslatorInitializedViaBeanProperty() + throws SQLException { + + doTestCouldNotGetConnectionInOperationWithExceptionTranslatorInitialized(true); + } + + @Test + public void testCouldNotGetConnectionInOperationWithExceptionTranslatorInitializedInAfterPropertiesSet() + throws SQLException { + + doTestCouldNotGetConnectionInOperationWithExceptionTranslatorInitialized(false); + } + + /** + * If beanProperty is true, initialize via exception translator bean property; + * if false, use afterPropertiesSet(). + */ + private void doTestCouldNotGetConnectionInOperationWithExceptionTranslatorInitialized(boolean beanProperty) + throws SQLException { + + SQLException sqlException = new SQLException("foo", "07xxx"); + this.dataSource = mock(DataSource.class); + given(this.dataSource.getConnection()).willThrow(sqlException); + this.template = new JdbcTemplate(); + this.template.setDataSource(this.dataSource); + this.template.setLazyInit(false); + if (beanProperty) { + // This will get a connection. + this.template.setExceptionTranslator(new SQLErrorCodeSQLExceptionTranslator(this.dataSource)); + } + else { + // This will cause creation of default SQL translator. + this.template.afterPropertiesSet(); + } + RowCountCallbackHandler rcch = new RowCountCallbackHandler(); + assertThatExceptionOfType(CannotGetJdbcConnectionException.class).isThrownBy(() -> + this.template.query("SELECT ID, FORENAME FROM CUSTMR WHERE ID < 3", rcch)) + .withCause(sqlException); + } + + @Test + public void testPreparedStatementSetterSucceeds() throws Exception { + final String sql = "UPDATE FOO SET NAME=? WHERE ID = 1"; + final String name = "Gary"; + int expectedRowsUpdated = 1; + + given(this.preparedStatement.executeUpdate()).willReturn(expectedRowsUpdated); + + PreparedStatementSetter pss = ps -> ps.setString(1, name); + int actualRowsUpdated = new JdbcTemplate(this.dataSource).update(sql, pss); + assertThat(expectedRowsUpdated).as("updated correct # of rows").isEqualTo(actualRowsUpdated); + verify(this.preparedStatement).setString(1, name); + verify(this.preparedStatement).close(); + verify(this.connection).close(); + } + + @Test + public void testPreparedStatementSetterFails() throws Exception { + final String sql = "UPDATE FOO SET NAME=? WHERE ID = 1"; + final String name = "Gary"; + SQLException sqlException = new SQLException(); + given(this.preparedStatement.executeUpdate()).willThrow(sqlException); + + PreparedStatementSetter pss = ps -> ps.setString(1, name); + assertThatExceptionOfType(DataAccessException.class).isThrownBy(() -> + new JdbcTemplate(this.dataSource).update(sql, pss)) + .withCause(sqlException); + verify(this.preparedStatement).setString(1, name); + verify(this.preparedStatement).close(); + verify(this.connection, atLeastOnce()).close(); + } + + @Test + public void testCouldNotClose() throws Exception { + SQLException sqlException = new SQLException("bar"); + given(this.connection.createStatement()).willReturn(this.statement); + given(this.resultSet.next()).willReturn(false); + willThrow(sqlException).given(this.resultSet).close(); + willThrow(sqlException).given(this.statement).close(); + willThrow(sqlException).given(this.connection).close(); + + RowCountCallbackHandler rcch = new RowCountCallbackHandler(); + this.template.query("SELECT ID, FORENAME FROM CUSTMR WHERE ID < 3", rcch); + verify(this.connection).close(); + } + + /** + * Mock objects allow us to produce warnings at will + */ + @Test + public void testFatalWarning() throws Exception { + String sql = "SELECT forename from custmr"; + SQLWarning warnings = new SQLWarning("My warning"); + + given(this.resultSet.next()).willReturn(false); + given(this.preparedStatement.getWarnings()).willReturn(warnings); + given(this.connection.createStatement()).willReturn(this.preparedStatement); + + JdbcTemplate t = new JdbcTemplate(this.dataSource); + t.setIgnoreWarnings(false); + + ResultSetExtractor extractor = rs -> rs.getByte(1); + assertThatExceptionOfType(SQLWarningException.class).isThrownBy(() -> + t.query(sql, extractor)) + .withCause(warnings); + verify(this.resultSet).close(); + verify(this.preparedStatement).close(); + verify(this.connection).close(); + } + + @Test + public void testIgnoredWarning() throws Exception { + String sql = "SELECT forename from custmr"; + SQLWarning warnings = new SQLWarning("My warning"); + + given(this.resultSet.next()).willReturn(false); + given(this.connection.createStatement()).willReturn(this.preparedStatement); + given(this.preparedStatement.getWarnings()).willReturn(warnings); + + // Too long: truncation + + this.template.setIgnoreWarnings(true); + RowCallbackHandler rch = rs -> rs.getByte(1); + this.template.query(sql, rch); + + verify(this.resultSet).close(); + verify(this.preparedStatement).close(); + verify(this.connection).close(); + } + + @Test + public void testSQLErrorCodeTranslation() throws Exception { + final SQLException sqlException = new SQLException("I have a known problem", "99999", 1054); + final String sql = "SELECT ID FROM CUSTOMER"; + + given(this.resultSet.next()).willReturn(true); + mockDatabaseMetaData(false); + given(this.connection.createStatement()).willReturn(this.preparedStatement); + + assertThatExceptionOfType(BadSqlGrammarException.class).isThrownBy(() -> + this.template.query(sql, (RowCallbackHandler) rs -> { + throw sqlException; + })) + .withCause(sqlException); + verify(this.resultSet).close(); + verify(this.preparedStatement).close(); + verify(this.connection, atLeastOnce()).close(); + } + + @Test + public void testSQLErrorCodeTranslationWithSpecifiedDbName() throws Exception { + final SQLException sqlException = new SQLException("I have a known problem", "99999", 1054); + final String sql = "SELECT ID FROM CUSTOMER"; + + given(this.resultSet.next()).willReturn(true); + given(this.connection.createStatement()).willReturn(this.preparedStatement); + + JdbcTemplate template = new JdbcTemplate(); + template.setDataSource(this.dataSource); + template.setDatabaseProductName("MySQL"); + template.afterPropertiesSet(); + + assertThatExceptionOfType(BadSqlGrammarException.class).isThrownBy(() -> + template.query(sql, (RowCallbackHandler) rs -> { + throw sqlException; + })) + .withCause(sqlException); + verify(this.resultSet).close(); + verify(this.preparedStatement).close(); + verify(this.connection).close(); + } + + /** + * Test that we see an SQLException translated using Error Code. + * If we provide the SQLExceptionTranslator, we shouldn't use a connection + * to get the metadata + */ + @Test + public void testUseCustomSQLErrorCodeTranslator() throws Exception { + // Bad SQL state + final SQLException sqlException = new SQLException("I have a known problem", "07000", 1054); + final String sql = "SELECT ID FROM CUSTOMER"; + + given(this.resultSet.next()).willReturn(true); + given(this.connection.createStatement()).willReturn(this.preparedStatement); + + JdbcTemplate template = new JdbcTemplate(); + template.setDataSource(this.dataSource); + // Set custom exception translator + template.setExceptionTranslator(new SQLStateSQLExceptionTranslator()); + template.afterPropertiesSet(); + + assertThatExceptionOfType(BadSqlGrammarException.class).isThrownBy(() -> + template.query(sql, (RowCallbackHandler) rs -> { + throw sqlException; + })) + .withCause(sqlException); + verify(this.resultSet).close(); + verify(this.preparedStatement).close(); + verify(this.connection).close(); + } + + @Test + public void testStaticResultSetClosed() throws Exception { + ResultSet resultSet2 = mock(ResultSet.class); + reset(this.preparedStatement); + given(this.preparedStatement.executeQuery()).willReturn(resultSet2); + given(this.connection.createStatement()).willReturn(this.statement); + + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() -> + this.template.query("my query", (ResultSetExtractor) rs -> { + throw new InvalidDataAccessApiUsageException(""); + })); + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() -> + this.template.query(con -> con.prepareStatement("my query"), (ResultSetExtractor) rs2 -> { + throw new InvalidDataAccessApiUsageException(""); + })); + + verify(this.resultSet).close(); + verify(resultSet2).close(); + verify(this.preparedStatement).close(); + verify(this.connection, atLeastOnce()).close(); + } + + @Test + public void testExecuteClosed() throws Exception { + given(this.resultSet.next()).willReturn(true); + given(this.callableStatement.execute()).willReturn(true); + given(this.callableStatement.getUpdateCount()).willReturn(-1); + + SqlParameter param = new SqlReturnResultSet("", (RowCallbackHandler) rs -> { + throw new InvalidDataAccessApiUsageException(""); + }); + + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() -> + this.template.call(conn -> conn.prepareCall("my query"), Collections.singletonList(param))); + verify(this.resultSet).close(); + verify(this.callableStatement).close(); + verify(this.connection).close(); + } + + @Test + public void testCaseInsensitiveResultsMap() throws Exception { + given(this.callableStatement.execute()).willReturn(false); + given(this.callableStatement.getUpdateCount()).willReturn(-1); + given(this.callableStatement.getObject(1)).willReturn("X"); + + boolean condition = !this.template.isResultsMapCaseInsensitive(); + assertThat(condition).as("default should have been NOT case insensitive").isTrue(); + + this.template.setResultsMapCaseInsensitive(true); + assertThat(this.template.isResultsMapCaseInsensitive()).as("now it should have been set to case insensitive").isTrue(); + + Map out = this.template.call( + conn -> conn.prepareCall("my query"), Collections.singletonList(new SqlOutParameter("a", 12))); + + assertThat(out).isInstanceOf(LinkedCaseInsensitiveMap.class); + assertThat(out.get("A")).as("we should have gotten the result with upper case").isNotNull(); + assertThat(out.get("a")).as("we should have gotten the result with lower case").isNotNull(); + verify(this.callableStatement).close(); + verify(this.connection).close(); + } + + @Test // SPR-16578 + public void testEquallyNamedColumn() throws SQLException { + given(this.connection.createStatement()).willReturn(this.statement); + + ResultSetMetaData metaData = mock(ResultSetMetaData.class); + given(metaData.getColumnCount()).willReturn(2); + given(metaData.getColumnLabel(1)).willReturn("x"); + given(metaData.getColumnLabel(2)).willReturn("X"); + given(this.resultSet.getMetaData()).willReturn(metaData); + + given(this.resultSet.next()).willReturn(true, false); + given(this.resultSet.getObject(1)).willReturn("first value"); + given(this.resultSet.getObject(2)).willReturn("second value"); + + Map map = this.template.queryForMap("my query"); + assertThat(map.size()).isEqualTo(1); + assertThat(map.get("x")).isEqualTo("first value"); + } + + + private void mockDatabaseMetaData(boolean supportsBatchUpdates) throws SQLException { + DatabaseMetaData databaseMetaData = mock(DatabaseMetaData.class); + given(databaseMetaData.getDatabaseProductName()).willReturn("MySQL"); + given(databaseMetaData.supportsBatchUpdates()).willReturn(supportsBatchUpdates); + given(this.connection.getMetaData()).willReturn(databaseMetaData); + } + + + private interface JdbcTemplateCallback { + + void doInJdbcTemplate(JdbcTemplate template, String sql, RowCallbackHandler rch); + } + + + private static class Dispatcher implements PreparedStatementCreator, SqlProvider { + + private int id; + + private String sql; + + public Dispatcher(int id, String sql) { + this.id = id; + this.sql = sql; + } + + @Override + public PreparedStatement createPreparedStatement(Connection connection) throws SQLException { + PreparedStatement ps = connection.prepareStatement(this.sql); + ps.setInt(1, this.id); + return ps; + } + + @Override + public String getSql() { + return this.sql; + } + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/RowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/RowMapperTests.java new file mode 100644 index 0000000..22896c2 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/RowMapperTests.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Types; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.jdbc.datasource.SingleConnectionDataSource; +import org.springframework.jdbc.support.SQLStateSQLExceptionTranslator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author Juergen Hoeller + * @author Sam Brannen + * @since 02.08.2004 + */ +public class RowMapperTests { + + private final Connection connection = mock(Connection.class); + + private final Statement statement = mock(Statement.class); + + private final PreparedStatement preparedStatement = mock(PreparedStatement.class); + + private final ResultSet resultSet = mock(ResultSet.class); + + private final JdbcTemplate template = new JdbcTemplate(); + + private final RowMapper testRowMapper = + (rs, rowNum) -> new TestBean(rs.getString(1), rs.getInt(2)); + + private List result; + + @BeforeEach + public void setUp() throws SQLException { + given(connection.createStatement()).willReturn(statement); + given(connection.prepareStatement(anyString())).willReturn(preparedStatement); + given(statement.executeQuery(anyString())).willReturn(resultSet); + given(preparedStatement.executeQuery()).willReturn(resultSet); + given(resultSet.next()).willReturn(true, true, false); + given(resultSet.getString(1)).willReturn("tb1", "tb2"); + given(resultSet.getInt(2)).willReturn(1, 2); + + template.setDataSource(new SingleConnectionDataSource(connection, false)); + template.setExceptionTranslator(new SQLStateSQLExceptionTranslator()); + template.afterPropertiesSet(); + } + + @AfterEach + public void verifyClosed() throws Exception { + verify(resultSet).close(); + } + + @AfterEach + public void verifyResults() { + assertThat(result).isNotNull(); + assertThat(result.size()).isEqualTo(2); + TestBean testBean1 = result.get(0); + TestBean testBean2 = result.get(1); + assertThat(testBean1.getName()).isEqualTo("tb1"); + assertThat(testBean2.getName()).isEqualTo("tb2"); + assertThat(testBean1.getAge()).isEqualTo(1); + assertThat(testBean2.getAge()).isEqualTo(2); + } + + @Test + public void staticQueryWithRowMapper() throws SQLException { + result = template.query("some SQL", testRowMapper); + verify(statement).close(); + } + + @Test + public void preparedStatementCreatorWithRowMapper() throws SQLException { + result = template.query(con -> preparedStatement, testRowMapper); + verify(preparedStatement).close(); + } + + @Test + public void preparedStatementSetterWithRowMapper() throws SQLException { + result = template.query("some SQL", ps -> ps.setString(1, "test"), testRowMapper); + verify(preparedStatement).setString(1, "test"); + verify(preparedStatement).close(); + } + + @Test + @SuppressWarnings("deprecation") + public void queryWithArgsAndRowMapper() throws SQLException { + result = template.query("some SQL", new Object[] { "test1", "test2" }, testRowMapper); + preparedStatement.setString(1, "test1"); + preparedStatement.setString(2, "test2"); + preparedStatement.close(); + } + + @Test + public void queryWithArgsAndTypesAndRowMapper() throws SQLException { + result = template.query("some SQL", + new Object[] { "test1", "test2" }, + new int[] { Types.VARCHAR, Types.VARCHAR }, + testRowMapper); + verify(preparedStatement).setString(1, "test1"); + verify(preparedStatement).setString(2, "test2"); + verify(preparedStatement).close(); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/SimpleRowCountCallbackHandler.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/SimpleRowCountCallbackHandler.java new file mode 100644 index 0000000..d59fbb5 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/SimpleRowCountCallbackHandler.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Simple row count callback handler for testing purposes. + * Does not call any JDBC methods on the given ResultSet. + * + * @author Juergen Hoeller + * @since 2.0 + */ +public class SimpleRowCountCallbackHandler implements RowCallbackHandler { + + private int count; + + + @Override + public void processRow(ResultSet rs) throws SQLException { + count++; + } + + public int getCount() { + return count; + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/SingleColumnRowMapperTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/SingleColumnRowMapperTests.java new file mode 100644 index 0000000..24f8413 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/SingleColumnRowMapperTests.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.Timestamp; +import java.time.LocalDateTime; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.dao.TypeMismatchDataAccessException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link SingleColumnRowMapper}. + * + * @author Kazuki Shimizu + * @since 5.0.4 + */ +public class SingleColumnRowMapperTests { + + @Test // SPR-16483 + public void useDefaultConversionService() throws SQLException { + Timestamp timestamp = new Timestamp(0); + + SingleColumnRowMapper rowMapper = SingleColumnRowMapper.newInstance(LocalDateTime.class); + + ResultSet resultSet = mock(ResultSet.class); + ResultSetMetaData metaData = mock(ResultSetMetaData.class); + given(metaData.getColumnCount()).willReturn(1); + given(resultSet.getMetaData()).willReturn(metaData); + given(resultSet.getObject(1, LocalDateTime.class)) + .willThrow(new SQLFeatureNotSupportedException()); + given(resultSet.getTimestamp(1)).willReturn(timestamp); + + LocalDateTime actualLocalDateTime = rowMapper.mapRow(resultSet, 1); + + assertThat(actualLocalDateTime).isEqualTo(timestamp.toLocalDateTime()); + } + + @Test // SPR-16483 + public void useCustomConversionService() throws SQLException { + Timestamp timestamp = new Timestamp(0); + + DefaultConversionService myConversionService = new DefaultConversionService(); + myConversionService.addConverter(Timestamp.class, MyLocalDateTime.class, + source -> new MyLocalDateTime(source.toLocalDateTime())); + SingleColumnRowMapper rowMapper = + SingleColumnRowMapper.newInstance(MyLocalDateTime.class, myConversionService); + + ResultSet resultSet = mock(ResultSet.class); + ResultSetMetaData metaData = mock(ResultSetMetaData.class); + given(metaData.getColumnCount()).willReturn(1); + given(resultSet.getMetaData()).willReturn(metaData); + given(resultSet.getObject(1, MyLocalDateTime.class)) + .willThrow(new SQLFeatureNotSupportedException()); + given(resultSet.getObject(1)).willReturn(timestamp); + + MyLocalDateTime actualMyLocalDateTime = rowMapper.mapRow(resultSet, 1); + + assertThat(actualMyLocalDateTime).isNotNull(); + assertThat(actualMyLocalDateTime.value).isEqualTo(timestamp.toLocalDateTime()); + } + + @Test // SPR-16483 + public void doesNotUseConversionService() throws SQLException { + SingleColumnRowMapper rowMapper = + SingleColumnRowMapper.newInstance(LocalDateTime.class, null); + + ResultSet resultSet = mock(ResultSet.class); + ResultSetMetaData metaData = mock(ResultSetMetaData.class); + given(metaData.getColumnCount()).willReturn(1); + given(resultSet.getMetaData()).willReturn(metaData); + given(resultSet.getObject(1, LocalDateTime.class)) + .willThrow(new SQLFeatureNotSupportedException()); + given(resultSet.getTimestamp(1)).willReturn(new Timestamp(0)); + assertThatExceptionOfType(TypeMismatchDataAccessException.class).isThrownBy(() -> + rowMapper.mapRow(resultSet, 1)); + } + + + private static class MyLocalDateTime { + + private final LocalDateTime value; + + public MyLocalDateTime(LocalDateTime value) { + this.value = value; + } + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/StatementCreatorUtilsTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/StatementCreatorUtilsTests.java new file mode 100644 index 0000000..7588a75 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/StatementCreatorUtilsTests.java @@ -0,0 +1,238 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ParameterMetaData; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; +import java.util.GregorianCalendar; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +/** + * @author Juergen Hoeller + * @since 31.08.2004 + */ +public class StatementCreatorUtilsTests { + + private PreparedStatement preparedStatement; + + + @BeforeEach + public void setUp() { + preparedStatement = mock(PreparedStatement.class); + } + + + @Test + public void testSetParameterValueWithNullAndType() throws SQLException { + StatementCreatorUtils.setParameterValue(preparedStatement, 1, Types.VARCHAR, null, null); + verify(preparedStatement).setNull(1, Types.VARCHAR); + } + + @Test + public void testSetParameterValueWithNullAndTypeName() throws SQLException { + StatementCreatorUtils.setParameterValue(preparedStatement, 1, Types.VARCHAR, "mytype", null); + verify(preparedStatement).setNull(1, Types.VARCHAR, "mytype"); + } + + @Test + public void testSetParameterValueWithNullAndUnknownType() throws SQLException { + StatementCreatorUtils.shouldIgnoreGetParameterType = true; + Connection con = mock(Connection.class); + DatabaseMetaData dbmd = mock(DatabaseMetaData.class); + given(preparedStatement.getConnection()).willReturn(con); + given(dbmd.getDatabaseProductName()).willReturn("Oracle"); + given(dbmd.getDriverName()).willReturn("Oracle Driver"); + given(con.getMetaData()).willReturn(dbmd); + StatementCreatorUtils.setParameterValue(preparedStatement, 1, SqlTypeValue.TYPE_UNKNOWN, null, null); + verify(preparedStatement).setNull(1, Types.NULL); + StatementCreatorUtils.shouldIgnoreGetParameterType = false; + } + + @Test + public void testSetParameterValueWithNullAndUnknownTypeOnInformix() throws SQLException { + StatementCreatorUtils.shouldIgnoreGetParameterType = true; + Connection con = mock(Connection.class); + DatabaseMetaData dbmd = mock(DatabaseMetaData.class); + given(preparedStatement.getConnection()).willReturn(con); + given(con.getMetaData()).willReturn(dbmd); + given(dbmd.getDatabaseProductName()).willReturn("Informix Dynamic Server"); + given(dbmd.getDriverName()).willReturn("Informix Driver"); + StatementCreatorUtils.setParameterValue(preparedStatement, 1, SqlTypeValue.TYPE_UNKNOWN, null, null); + verify(dbmd).getDatabaseProductName(); + verify(dbmd).getDriverName(); + verify(preparedStatement).setObject(1, null); + StatementCreatorUtils.shouldIgnoreGetParameterType = false; + } + + @Test + public void testSetParameterValueWithNullAndUnknownTypeOnDerbyEmbedded() throws SQLException { + StatementCreatorUtils.shouldIgnoreGetParameterType = true; + Connection con = mock(Connection.class); + DatabaseMetaData dbmd = mock(DatabaseMetaData.class); + given(preparedStatement.getConnection()).willReturn(con); + given(con.getMetaData()).willReturn(dbmd); + given(dbmd.getDatabaseProductName()).willReturn("Apache Derby"); + given(dbmd.getDriverName()).willReturn("Apache Derby Embedded Driver"); + StatementCreatorUtils.setParameterValue(preparedStatement, 1, SqlTypeValue.TYPE_UNKNOWN, null, null); + verify(dbmd).getDatabaseProductName(); + verify(dbmd).getDriverName(); + verify(preparedStatement).setNull(1, Types.VARCHAR); + StatementCreatorUtils.shouldIgnoreGetParameterType = false; + } + + @Test + public void testSetParameterValueWithNullAndGetParameterTypeWorking() throws SQLException { + ParameterMetaData pmd = mock(ParameterMetaData.class); + given(preparedStatement.getParameterMetaData()).willReturn(pmd); + given(pmd.getParameterType(1)).willReturn(Types.SMALLINT); + StatementCreatorUtils.setParameterValue(preparedStatement, 1, SqlTypeValue.TYPE_UNKNOWN, null, null); + verify(pmd).getParameterType(1); + verify(preparedStatement, never()).getConnection(); + verify(preparedStatement).setNull(1, Types.SMALLINT); + } + + @Test + public void testSetParameterValueWithString() throws SQLException { + StatementCreatorUtils.setParameterValue(preparedStatement, 1, Types.VARCHAR, null, "test"); + verify(preparedStatement).setString(1, "test"); + } + + @Test + public void testSetParameterValueWithStringAndSpecialType() throws SQLException { + StatementCreatorUtils.setParameterValue(preparedStatement, 1, Types.CHAR, null, "test"); + verify(preparedStatement).setObject(1, "test", Types.CHAR); + } + + @Test public void testSetParameterValueWithStringAndUnknownType() throws SQLException { + StatementCreatorUtils.setParameterValue(preparedStatement, 1, SqlTypeValue.TYPE_UNKNOWN, null, "test"); + verify(preparedStatement).setString(1, "test"); + } + + @Test + public void testSetParameterValueWithSqlDate() throws SQLException { + java.sql.Date date = new java.sql.Date(1000); + StatementCreatorUtils.setParameterValue(preparedStatement, 1, Types.DATE, null, date); + verify(preparedStatement).setDate(1, date); + } + + @Test + public void testSetParameterValueWithDateAndUtilDate() throws SQLException { + java.util.Date date = new java.util.Date(1000); + StatementCreatorUtils.setParameterValue(preparedStatement, 1, Types.DATE, null, date); + verify(preparedStatement).setDate(1, new java.sql.Date(1000)); + } + + @Test + public void testSetParameterValueWithDateAndCalendar() throws SQLException { + java.util.Calendar cal = new GregorianCalendar(); + StatementCreatorUtils.setParameterValue(preparedStatement, 1, Types.DATE, null, cal); + verify(preparedStatement).setDate(1, new java.sql.Date(cal.getTime().getTime()), cal); + } + + @Test + public void testSetParameterValueWithSqlTime() throws SQLException { + java.sql.Time time = new java.sql.Time(1000); + StatementCreatorUtils.setParameterValue(preparedStatement, 1, Types.TIME, null, time); + verify(preparedStatement).setTime(1, time); + } + + @Test + public void testSetParameterValueWithTimeAndUtilDate() throws SQLException { + java.util.Date date = new java.util.Date(1000); + StatementCreatorUtils.setParameterValue(preparedStatement, 1, Types.TIME, null, date); + verify(preparedStatement).setTime(1, new java.sql.Time(1000)); + } + + @Test + public void testSetParameterValueWithTimeAndCalendar() throws SQLException { + java.util.Calendar cal = new GregorianCalendar(); + StatementCreatorUtils.setParameterValue(preparedStatement, 1, Types.TIME, null, cal); + verify(preparedStatement).setTime(1, new java.sql.Time(cal.getTime().getTime()), cal); + } + + @Test + public void testSetParameterValueWithSqlTimestamp() throws SQLException { + java.sql.Timestamp timestamp = new java.sql.Timestamp(1000); + StatementCreatorUtils.setParameterValue(preparedStatement, 1, Types.TIMESTAMP, null, timestamp); + verify(preparedStatement).setTimestamp(1, timestamp); + } + + @Test + public void testSetParameterValueWithTimestampAndUtilDate() throws SQLException { + java.util.Date date = new java.util.Date(1000); + StatementCreatorUtils.setParameterValue(preparedStatement, 1, Types.TIMESTAMP, null, date); + verify(preparedStatement).setTimestamp(1, new java.sql.Timestamp(1000)); + } + + @Test + public void testSetParameterValueWithTimestampAndCalendar() throws SQLException { + java.util.Calendar cal = new GregorianCalendar(); + StatementCreatorUtils.setParameterValue(preparedStatement, 1, Types.TIMESTAMP, null, cal); + verify(preparedStatement).setTimestamp(1, new java.sql.Timestamp(cal.getTime().getTime()), cal); + } + + @Test + public void testSetParameterValueWithDateAndUnknownType() throws SQLException { + java.util.Date date = new java.util.Date(1000); + StatementCreatorUtils.setParameterValue(preparedStatement, 1, SqlTypeValue.TYPE_UNKNOWN, null, date); + verify(preparedStatement).setTimestamp(1, new java.sql.Timestamp(1000)); + } + + @Test + public void testSetParameterValueWithCalendarAndUnknownType() throws SQLException { + java.util.Calendar cal = new GregorianCalendar(); + StatementCreatorUtils.setParameterValue(preparedStatement, 1, SqlTypeValue.TYPE_UNKNOWN, null, cal); + verify(preparedStatement).setTimestamp(1, new java.sql.Timestamp(cal.getTime().getTime()), cal); + } + + @Test // SPR-8571 + public void testSetParameterValueWithStringAndVendorSpecificType() throws SQLException { + Connection con = mock(Connection.class); + DatabaseMetaData dbmd = mock(DatabaseMetaData.class); + given(preparedStatement.getConnection()).willReturn(con); + given(dbmd.getDatabaseProductName()).willReturn("Oracle"); + given(con.getMetaData()).willReturn(dbmd); + StatementCreatorUtils.setParameterValue(preparedStatement, 1, Types.OTHER, null, "test"); + verify(preparedStatement).setString(1, "test"); + } + + @Test // SPR-8571 + public void testSetParameterValueWithNullAndVendorSpecificType() throws SQLException { + StatementCreatorUtils.shouldIgnoreGetParameterType = true; + Connection con = mock(Connection.class); + DatabaseMetaData dbmd = mock(DatabaseMetaData.class); + given(preparedStatement.getConnection()).willReturn(con); + given(dbmd.getDatabaseProductName()).willReturn("Oracle"); + given(dbmd.getDriverName()).willReturn("Oracle Driver"); + given(con.getMetaData()).willReturn(dbmd); + StatementCreatorUtils.setParameterValue(preparedStatement, 1, Types.OTHER, null, null); + verify(preparedStatement).setNull(1, Types.NULL); + StatementCreatorUtils.shouldIgnoreGetParameterType = false; + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/BeanPropertySqlParameterSourceTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/BeanPropertySqlParameterSourceTests.java new file mode 100644 index 0000000..7abd2fc --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/BeanPropertySqlParameterSourceTests.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.namedparam; + +import java.sql.Types; +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Rick Evans + * @author Juergen Hoeller + * @author Arjen Poutsma + * @author Juergen Hoeller + */ +public class BeanPropertySqlParameterSourceTests { + + @Test + public void withNullBeanPassedToCtor() { + assertThatIllegalArgumentException().isThrownBy(() -> + new BeanPropertySqlParameterSource(null)); + } + + @Test + public void getValueWhereTheUnderlyingBeanHasNoSuchProperty() { + BeanPropertySqlParameterSource source = new BeanPropertySqlParameterSource(new TestBean()); + assertThatIllegalArgumentException().isThrownBy(() -> + source.getValue("thisPropertyDoesNotExist")); + } + + @Test + public void successfulPropertyAccess() { + BeanPropertySqlParameterSource source = new BeanPropertySqlParameterSource(new TestBean("tb", 99)); + assertThat(Arrays.asList(source.getReadablePropertyNames()).contains("name")).isTrue(); + assertThat(Arrays.asList(source.getReadablePropertyNames()).contains("age")).isTrue(); + assertThat(source.getValue("name")).isEqualTo("tb"); + assertThat(source.getValue("age")).isEqualTo(99); + assertThat(source.getSqlType("name")).isEqualTo(Types.VARCHAR); + assertThat(source.getSqlType("age")).isEqualTo(Types.INTEGER); + } + + @Test + public void successfulPropertyAccessWithOverriddenSqlType() { + BeanPropertySqlParameterSource source = new BeanPropertySqlParameterSource(new TestBean("tb", 99)); + source.registerSqlType("age", Types.NUMERIC); + assertThat(source.getValue("name")).isEqualTo("tb"); + assertThat(source.getValue("age")).isEqualTo(99); + assertThat(source.getSqlType("name")).isEqualTo(Types.VARCHAR); + assertThat(source.getSqlType("age")).isEqualTo(Types.NUMERIC); + } + + @Test + public void hasValueWhereTheUnderlyingBeanHasNoSuchProperty() { + BeanPropertySqlParameterSource source = new BeanPropertySqlParameterSource(new TestBean()); + assertThat(source.hasValue("thisPropertyDoesNotExist")).isFalse(); + } + + @Test + public void getValueWhereTheUnderlyingBeanPropertyIsNotReadable() { + BeanPropertySqlParameterSource source = new BeanPropertySqlParameterSource(new NoReadableProperties()); + assertThatIllegalArgumentException().isThrownBy(() -> + source.getValue("noOp")); + } + + @Test + public void hasValueWhereTheUnderlyingBeanPropertyIsNotReadable() { + BeanPropertySqlParameterSource source = new BeanPropertySqlParameterSource(new NoReadableProperties()); + assertThat(source.hasValue("noOp")).isFalse(); + } + + @Test + public void toStringShowsParameterDetails() { + BeanPropertySqlParameterSource source = new BeanPropertySqlParameterSource(new TestBean("tb", 99)); + assertThat(source.toString()) + .startsWith("BeanPropertySqlParameterSource {") + .contains("name=tb (type:VARCHAR)") + .contains("age=99 (type:INTEGER)") + .endsWith("}"); + } + + @Test + public void toStringShowsCustomSqlType() { + BeanPropertySqlParameterSource source = new BeanPropertySqlParameterSource(new TestBean("tb", 99)); + source.registerSqlType("name", Integer.MAX_VALUE); + assertThat(source.toString()) + .startsWith("BeanPropertySqlParameterSource {") + .contains("name=tb (type:" + Integer.MAX_VALUE + ")") + .contains("age=99 (type:INTEGER)") + .endsWith("}"); + } + + @Test + public void toStringDoesNotShowTypeUnknown() { + BeanPropertySqlParameterSource source = new BeanPropertySqlParameterSource(new TestBean("tb", 99)); + assertThat(source.toString()) + .startsWith("BeanPropertySqlParameterSource {") + .contains("beanFactory=null") + .doesNotContain("beanFactory=null (type:") + .endsWith("}"); + } + + + @SuppressWarnings("unused") + private static final class NoReadableProperties { + + public void setNoOp(String noOp) { + } + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/MapSqlParameterSourceTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/MapSqlParameterSourceTests.java new file mode 100644 index 0000000..2159ab1 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/MapSqlParameterSourceTests.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.namedparam; + +import java.sql.Types; + +import org.junit.jupiter.api.Test; + +import org.springframework.jdbc.core.SqlParameterValue; +import org.springframework.jdbc.support.JdbcUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Rick Evans + * @author Arjen Poutsma + * @author Juergen Hoeller + */ +public class MapSqlParameterSourceTests { + + @Test + public void nullParameterValuesPassedToCtorIsOk() { + new MapSqlParameterSource(null); + } + + @Test + public void getValueChokesIfParameterIsNotPresent() { + MapSqlParameterSource source = new MapSqlParameterSource(); + assertThatIllegalArgumentException().isThrownBy(() -> + source.getValue("pechorin was right!")); + } + + @Test + public void sqlParameterValueRegistersSqlType() { + MapSqlParameterSource msps = new MapSqlParameterSource("FOO", new SqlParameterValue(Types.NUMERIC, "Foo")); + assertThat(msps.getSqlType("FOO")).as("Correct SQL Type not registered").isEqualTo(2); + MapSqlParameterSource msps2 = new MapSqlParameterSource(); + msps2.addValues(msps.getValues()); + assertThat(msps2.getSqlType("FOO")).as("Correct SQL Type not registered").isEqualTo(2); + } + + @Test + public void toStringShowsParameterDetails() { + MapSqlParameterSource source = new MapSqlParameterSource("FOO", new SqlParameterValue(Types.NUMERIC, "Foo")); + assertThat(source.toString()).isEqualTo("MapSqlParameterSource {FOO=Foo (type:NUMERIC)}"); + } + + @Test + public void toStringShowsCustomSqlType() { + MapSqlParameterSource source = new MapSqlParameterSource("FOO", new SqlParameterValue(Integer.MAX_VALUE, "Foo")); + assertThat(source.toString()).isEqualTo(("MapSqlParameterSource {FOO=Foo (type:" + Integer.MAX_VALUE + ")}")); + } + + @Test + public void toStringDoesNotShowTypeUnknown() { + MapSqlParameterSource source = new MapSqlParameterSource("FOO", new SqlParameterValue(JdbcUtils.TYPE_UNKNOWN, "Foo")); + assertThat(source.toString()).isEqualTo("MapSqlParameterSource {FOO=Foo}"); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplateTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplateTests.java new file mode 100644 index 0000000..31fa105 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplateTests.java @@ -0,0 +1,589 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.namedparam; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; + +import org.springframework.jdbc.Customer; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.PreparedStatementCallback; +import org.springframework.jdbc.core.SqlParameterValue; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author Rick Evans + * @author Juergen Hoeller + * @author Chris Beams + * @author Nikita Khateev + * @author Fedor Bobin + */ +public class NamedParameterJdbcTemplateTests { + + private static final String SELECT_NAMED_PARAMETERS = + "select id, forename from custmr where id = :id and country = :country"; + private static final String SELECT_NAMED_PARAMETERS_PARSED = + "select id, forename from custmr where id = ? and country = ?"; + private static final String SELECT_NO_PARAMETERS = + "select id, forename from custmr"; + + private static final String UPDATE_NAMED_PARAMETERS = + "update seat_status set booking_id = null where performance_id = :perfId and price_band_id = :priceId"; + private static final String UPDATE_NAMED_PARAMETERS_PARSED = + "update seat_status set booking_id = null where performance_id = ? and price_band_id = ?"; + + private static final String UPDATE_ARRAY_PARAMETERS = + "update customer set type = array[:typeIds] where id = :id"; + private static final String UPDATE_ARRAY_PARAMETERS_PARSED = + "update customer set type = array[?, ?, ?] where id = ?"; + + private static final String[] COLUMN_NAMES = new String[] {"id", "forename"}; + + + private Connection connection; + + private DataSource dataSource; + + private PreparedStatement preparedStatement; + + private ResultSet resultSet; + + private DatabaseMetaData databaseMetaData; + + private Map params = new HashMap<>(); + + private NamedParameterJdbcTemplate namedParameterTemplate; + + + @BeforeEach + public void setup() throws Exception { + connection = mock(Connection.class); + dataSource = mock(DataSource.class); + preparedStatement = mock(PreparedStatement.class); + resultSet = mock(ResultSet.class); + namedParameterTemplate = new NamedParameterJdbcTemplate(dataSource); + databaseMetaData = mock(DatabaseMetaData.class); + given(dataSource.getConnection()).willReturn(connection); + given(connection.prepareStatement(anyString())).willReturn(preparedStatement); + given(preparedStatement.getConnection()).willReturn(connection); + given(preparedStatement.executeQuery()).willReturn(resultSet); + given(databaseMetaData.getDatabaseProductName()).willReturn("MySQL"); + given(databaseMetaData.supportsBatchUpdates()).willReturn(true); + } + + + @Test + public void testNullDataSourceProvidedToCtor() { + assertThatIllegalArgumentException().isThrownBy(() -> + new NamedParameterJdbcTemplate((DataSource) null)); + } + + @Test + public void testNullJdbcTemplateProvidedToCtor() { + assertThatIllegalArgumentException().isThrownBy(() -> + new NamedParameterJdbcTemplate((JdbcOperations) null)); + } + + @Test + public void testTemplateConfiguration() { + assertThat(namedParameterTemplate.getJdbcTemplate().getDataSource()).isSameAs(dataSource); + } + + @Test + public void testExecute() throws SQLException { + given(preparedStatement.executeUpdate()).willReturn(1); + + params.put("perfId", 1); + params.put("priceId", 1); + Object result = namedParameterTemplate.execute(UPDATE_NAMED_PARAMETERS, params, + (PreparedStatementCallback) ps -> { + assertThat(ps).isEqualTo(preparedStatement); + ps.executeUpdate(); + return "result"; + }); + + assertThat(result).isEqualTo("result"); + verify(connection).prepareStatement(UPDATE_NAMED_PARAMETERS_PARSED); + verify(preparedStatement).setObject(1, 1); + verify(preparedStatement).setObject(2, 1); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Disabled("SPR-16340") + @Test + public void testExecuteArray() throws SQLException { + given(preparedStatement.executeUpdate()).willReturn(1); + + List typeIds = Arrays.asList(1, 2, 3); + + params.put("typeIds", typeIds); + params.put("id", 1); + Object result = namedParameterTemplate.execute(UPDATE_ARRAY_PARAMETERS, params, + (PreparedStatementCallback) ps -> { + assertThat(ps).isEqualTo(preparedStatement); + ps.executeUpdate(); + return "result"; + }); + + assertThat(result).isEqualTo("result"); + verify(connection).prepareStatement(UPDATE_ARRAY_PARAMETERS_PARSED); + verify(preparedStatement).setObject(1, 1); + verify(preparedStatement).setObject(2, 2); + verify(preparedStatement).setObject(3, 3); + verify(preparedStatement).setObject(4, 1); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testExecuteWithTypedParameters() throws SQLException { + given(preparedStatement.executeUpdate()).willReturn(1); + + params.put("perfId", new SqlParameterValue(Types.DECIMAL, 1)); + params.put("priceId", new SqlParameterValue(Types.INTEGER, 1)); + Object result = namedParameterTemplate.execute(UPDATE_NAMED_PARAMETERS, params, + (PreparedStatementCallback) ps -> { + assertThat(ps).isEqualTo(preparedStatement); + ps.executeUpdate(); + return "result"; + }); + + assertThat(result).isEqualTo("result"); + verify(connection).prepareStatement(UPDATE_NAMED_PARAMETERS_PARSED); + verify(preparedStatement).setObject(1, 1, Types.DECIMAL); + verify(preparedStatement).setObject(2, 1, Types.INTEGER); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testExecuteNoParameters() throws SQLException { + given(preparedStatement.executeUpdate()).willReturn(1); + + Object result = namedParameterTemplate.execute(SELECT_NO_PARAMETERS, + (PreparedStatementCallback) ps -> { + assertThat(ps).isEqualTo(preparedStatement); + ps.executeQuery(); + return "result"; + }); + + assertThat(result).isEqualTo("result"); + verify(connection).prepareStatement(SELECT_NO_PARAMETERS); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testQueryWithResultSetExtractor() throws SQLException { + given(resultSet.next()).willReturn(true); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + + params.put("id", new SqlParameterValue(Types.DECIMAL, 1)); + params.put("country", "UK"); + Customer cust = namedParameterTemplate.query(SELECT_NAMED_PARAMETERS, params, + rs -> { + rs.next(); + Customer cust1 = new Customer(); + cust1.setId(rs.getInt(COLUMN_NAMES[0])); + cust1.setForename(rs.getString(COLUMN_NAMES[1])); + return cust1; + }); + + assertThat(cust.getId() == 1).as("Customer id was assigned correctly").isTrue(); + assertThat(cust.getForename().equals("rod")).as("Customer forename was assigned correctly").isTrue(); + verify(connection).prepareStatement(SELECT_NAMED_PARAMETERS_PARSED); + verify(preparedStatement).setObject(1, 1, Types.DECIMAL); + verify(preparedStatement).setString(2, "UK"); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testQueryWithResultSetExtractorNoParameters() throws SQLException { + given(resultSet.next()).willReturn(true); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + + Customer cust = namedParameterTemplate.query(SELECT_NO_PARAMETERS, + rs -> { + rs.next(); + Customer cust1 = new Customer(); + cust1.setId(rs.getInt(COLUMN_NAMES[0])); + cust1.setForename(rs.getString(COLUMN_NAMES[1])); + return cust1; + }); + + assertThat(cust.getId() == 1).as("Customer id was assigned correctly").isTrue(); + assertThat(cust.getForename().equals("rod")).as("Customer forename was assigned correctly").isTrue(); + verify(connection).prepareStatement(SELECT_NO_PARAMETERS); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testQueryWithRowCallbackHandler() throws SQLException { + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + + params.put("id", new SqlParameterValue(Types.DECIMAL, 1)); + params.put("country", "UK"); + final List customers = new ArrayList<>(); + namedParameterTemplate.query(SELECT_NAMED_PARAMETERS, params, rs -> { + Customer cust = new Customer(); + cust.setId(rs.getInt(COLUMN_NAMES[0])); + cust.setForename(rs.getString(COLUMN_NAMES[1])); + customers.add(cust); + }); + + assertThat(customers.size()).isEqualTo(1); + assertThat(customers.get(0).getId() == 1).as("Customer id was assigned correctly").isTrue(); + assertThat(customers.get(0).getForename().equals("rod")).as("Customer forename was assigned correctly").isTrue(); + verify(connection).prepareStatement(SELECT_NAMED_PARAMETERS_PARSED); + verify(preparedStatement).setObject(1, 1, Types.DECIMAL); + verify(preparedStatement).setString(2, "UK"); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testQueryWithRowCallbackHandlerNoParameters() throws SQLException { + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + + final List customers = new ArrayList<>(); + namedParameterTemplate.query(SELECT_NO_PARAMETERS, rs -> { + Customer cust = new Customer(); + cust.setId(rs.getInt(COLUMN_NAMES[0])); + cust.setForename(rs.getString(COLUMN_NAMES[1])); + customers.add(cust); + }); + + assertThat(customers.size()).isEqualTo(1); + assertThat(customers.get(0).getId() == 1).as("Customer id was assigned correctly").isTrue(); + assertThat(customers.get(0).getForename().equals("rod")).as("Customer forename was assigned correctly").isTrue(); + verify(connection).prepareStatement(SELECT_NO_PARAMETERS); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testQueryWithRowMapper() throws SQLException { + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + + params.put("id", new SqlParameterValue(Types.DECIMAL, 1)); + params.put("country", "UK"); + List customers = namedParameterTemplate.query(SELECT_NAMED_PARAMETERS, params, + (rs, rownum) -> { + Customer cust = new Customer(); + cust.setId(rs.getInt(COLUMN_NAMES[0])); + cust.setForename(rs.getString(COLUMN_NAMES[1])); + return cust; + }); + + assertThat(customers.size()).isEqualTo(1); + assertThat(customers.get(0).getId() == 1).as("Customer id was assigned correctly").isTrue(); + assertThat(customers.get(0).getForename().equals("rod")).as("Customer forename was assigned correctly").isTrue(); + verify(connection).prepareStatement(SELECT_NAMED_PARAMETERS_PARSED); + verify(preparedStatement).setObject(1, 1, Types.DECIMAL); + verify(preparedStatement).setString(2, "UK"); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testQueryWithRowMapperNoParameters() throws SQLException { + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + + List customers = namedParameterTemplate.query(SELECT_NO_PARAMETERS, + (rs, rownum) -> { + Customer cust = new Customer(); + cust.setId(rs.getInt(COLUMN_NAMES[0])); + cust.setForename(rs.getString(COLUMN_NAMES[1])); + return cust; + }); + + assertThat(customers.size()).isEqualTo(1); + assertThat(customers.get(0).getId() == 1).as("Customer id was assigned correctly").isTrue(); + assertThat(customers.get(0).getForename().equals("rod")).as("Customer forename was assigned correctly").isTrue(); + verify(connection).prepareStatement(SELECT_NO_PARAMETERS); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testQueryForObjectWithRowMapper() throws SQLException { + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + + params.put("id", new SqlParameterValue(Types.DECIMAL, 1)); + params.put("country", "UK"); + + Customer cust = namedParameterTemplate.queryForObject(SELECT_NAMED_PARAMETERS, params, + (rs, rownum) -> { + Customer cust1 = new Customer(); + cust1.setId(rs.getInt(COLUMN_NAMES[0])); + cust1.setForename(rs.getString(COLUMN_NAMES[1])); + return cust1; + }); + + assertThat(cust.getId() == 1).as("Customer id was assigned correctly").isTrue(); + assertThat(cust.getForename().equals("rod")).as("Customer forename was assigned correctly").isTrue(); + verify(connection).prepareStatement(SELECT_NAMED_PARAMETERS_PARSED); + verify(preparedStatement).setObject(1, 1, Types.DECIMAL); + verify(preparedStatement).setString(2, "UK"); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testQueryForStreamWithRowMapper() throws SQLException { + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + + params.put("id", new SqlParameterValue(Types.DECIMAL, 1)); + params.put("country", "UK"); + AtomicInteger count = new AtomicInteger(); + + try (Stream s = namedParameterTemplate.queryForStream(SELECT_NAMED_PARAMETERS, params, + (rs, rownum) -> { + Customer cust1 = new Customer(); + cust1.setId(rs.getInt(COLUMN_NAMES[0])); + cust1.setForename(rs.getString(COLUMN_NAMES[1])); + return cust1; + })) { + s.forEach(cust -> { + count.incrementAndGet(); + assertThat(cust.getId() == 1).as("Customer id was assigned correctly").isTrue(); + assertThat(cust.getForename().equals("rod")).as("Customer forename was assigned correctly").isTrue(); + }); + } + + assertThat(count.get()).isEqualTo(1); + verify(connection).prepareStatement(SELECT_NAMED_PARAMETERS_PARSED); + verify(preparedStatement).setObject(1, 1, Types.DECIMAL); + verify(preparedStatement).setString(2, "UK"); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testUpdate() throws SQLException { + given(preparedStatement.executeUpdate()).willReturn(1); + + params.put("perfId", 1); + params.put("priceId", 1); + int rowsAffected = namedParameterTemplate.update(UPDATE_NAMED_PARAMETERS, params); + + assertThat(rowsAffected).isEqualTo(1); + verify(connection).prepareStatement(UPDATE_NAMED_PARAMETERS_PARSED); + verify(preparedStatement).setObject(1, 1); + verify(preparedStatement).setObject(2, 1); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testUpdateWithTypedParameters() throws SQLException { + given(preparedStatement.executeUpdate()).willReturn(1); + + params.put("perfId", new SqlParameterValue(Types.DECIMAL, 1)); + params.put("priceId", new SqlParameterValue(Types.INTEGER, 1)); + int rowsAffected = namedParameterTemplate.update(UPDATE_NAMED_PARAMETERS, params); + + assertThat(rowsAffected).isEqualTo(1); + verify(connection).prepareStatement(UPDATE_NAMED_PARAMETERS_PARSED); + verify(preparedStatement).setObject(1, 1, Types.DECIMAL); + verify(preparedStatement).setObject(2, 1, Types.INTEGER); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testBatchUpdateWithPlainMap() throws Exception { + @SuppressWarnings("unchecked") + final Map[] ids = new Map[2]; + ids[0] = Collections.singletonMap("id", 100); + ids[1] = Collections.singletonMap("id", 200); + final int[] rowsAffected = new int[] {1, 2}; + + given(preparedStatement.executeBatch()).willReturn(rowsAffected); + given(connection.getMetaData()).willReturn(databaseMetaData); + namedParameterTemplate = new NamedParameterJdbcTemplate(new JdbcTemplate(dataSource, false)); + + int[] actualRowsAffected = namedParameterTemplate.batchUpdate( + "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = :id", ids); + assertThat(actualRowsAffected.length == 2).as("executed 2 updates").isTrue(); + assertThat(actualRowsAffected[0]).isEqualTo(rowsAffected[0]); + assertThat(actualRowsAffected[1]).isEqualTo(rowsAffected[1]); + verify(connection).prepareStatement("UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?"); + verify(preparedStatement).setObject(1, 100); + verify(preparedStatement).setObject(1, 200); + verify(preparedStatement, times(2)).addBatch(); + verify(preparedStatement, atLeastOnce()).close(); + verify(connection, atLeastOnce()).close(); + } + + @Test + public void testBatchUpdateWithEmptyMap() throws Exception { + @SuppressWarnings("unchecked") + final Map[] ids = new Map[0]; + namedParameterTemplate = new NamedParameterJdbcTemplate(new JdbcTemplate(dataSource, false)); + + int[] actualRowsAffected = namedParameterTemplate.batchUpdate( + "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = :id", ids); + assertThat(actualRowsAffected.length == 0).as("executed 0 updates").isTrue(); + } + + @Test + public void testBatchUpdateWithSqlParameterSource() throws Exception { + SqlParameterSource[] ids = new SqlParameterSource[2]; + ids[0] = new MapSqlParameterSource("id", 100); + ids[1] = new MapSqlParameterSource("id", 200); + final int[] rowsAffected = new int[] {1, 2}; + + given(preparedStatement.executeBatch()).willReturn(rowsAffected); + given(connection.getMetaData()).willReturn(databaseMetaData); + namedParameterTemplate = new NamedParameterJdbcTemplate(new JdbcTemplate(dataSource, false)); + + int[] actualRowsAffected = namedParameterTemplate.batchUpdate( + "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = :id", ids); + assertThat(actualRowsAffected.length == 2).as("executed 2 updates").isTrue(); + assertThat(actualRowsAffected[0]).isEqualTo(rowsAffected[0]); + assertThat(actualRowsAffected[1]).isEqualTo(rowsAffected[1]); + verify(connection).prepareStatement("UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?"); + verify(preparedStatement).setObject(1, 100); + verify(preparedStatement).setObject(1, 200); + verify(preparedStatement, times(2)).addBatch(); + verify(preparedStatement, atLeastOnce()).close(); + verify(connection, atLeastOnce()).close(); + } + + @Test + public void testBatchUpdateWithInClause() throws Exception { + @SuppressWarnings("unchecked") + Map[] parameters = new Map[3]; + parameters[0] = Collections.singletonMap("ids", Arrays.asList(1, 2)); + parameters[1] = Collections.singletonMap("ids", Arrays.asList("3", "4")); + parameters[2] = Collections.singletonMap("ids", (Iterable) () -> Arrays.asList(5, 6).iterator()); + + final int[] rowsAffected = new int[] {1, 2, 3}; + given(preparedStatement.executeBatch()).willReturn(rowsAffected); + given(connection.getMetaData()).willReturn(databaseMetaData); + + JdbcTemplate template = new JdbcTemplate(dataSource, false); + namedParameterTemplate = new NamedParameterJdbcTemplate(template); + + int[] actualRowsAffected = namedParameterTemplate.batchUpdate( + "delete sometable where id in (:ids)", + parameters + ); + + assertThat(actualRowsAffected.length).as("executed 3 updates").isEqualTo(3); + + InOrder inOrder = inOrder(preparedStatement); + + inOrder.verify(preparedStatement).setObject(1, 1); + inOrder.verify(preparedStatement).setObject(2, 2); + inOrder.verify(preparedStatement).addBatch(); + + inOrder.verify(preparedStatement).setString(1, "3"); + inOrder.verify(preparedStatement).setString(2, "4"); + inOrder.verify(preparedStatement).addBatch(); + + inOrder.verify(preparedStatement).setObject(1, 5); + inOrder.verify(preparedStatement).setObject(2, 6); + inOrder.verify(preparedStatement).addBatch(); + + inOrder.verify(preparedStatement, atLeastOnce()).close(); + verify(connection, atLeastOnce()).close(); + } + + @Test + public void testBatchUpdateWithSqlParameterSourcePlusTypeInfo() throws Exception { + SqlParameterSource[] ids = new SqlParameterSource[3]; + ids[0] = new MapSqlParameterSource().addValue("id", null, Types.NULL); + ids[1] = new MapSqlParameterSource().addValue("id", 100, Types.NUMERIC); + ids[2] = new MapSqlParameterSource().addValue("id", 200, Types.NUMERIC); + final int[] rowsAffected = new int[] {1, 2, 3}; + + given(preparedStatement.executeBatch()).willReturn(rowsAffected); + given(connection.getMetaData()).willReturn(databaseMetaData); + namedParameterTemplate = new NamedParameterJdbcTemplate(new JdbcTemplate(dataSource, false)); + + int[] actualRowsAffected = namedParameterTemplate.batchUpdate( + "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = :id", ids); + assertThat(actualRowsAffected.length == 3).as("executed 3 updates").isTrue(); + assertThat(actualRowsAffected[0]).isEqualTo(rowsAffected[0]); + assertThat(actualRowsAffected[1]).isEqualTo(rowsAffected[1]); + assertThat(actualRowsAffected[2]).isEqualTo(rowsAffected[2]); + verify(connection).prepareStatement("UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?"); + verify(preparedStatement).setNull(1, Types.NULL); + verify(preparedStatement).setObject(1, 100, Types.NUMERIC); + verify(preparedStatement).setObject(1, 200, Types.NUMERIC); + verify(preparedStatement, times(3)).addBatch(); + verify(preparedStatement, atLeastOnce()).close(); + verify(connection, atLeastOnce()).close(); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterQueryTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterQueryTests.java new file mode 100644 index 0000000..25ccf1e --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterQueryTests.java @@ -0,0 +1,338 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.namedparam; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Types; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.jdbc.core.RowMapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author Thomas Risberg + * @author Phillip Webb + */ +public class NamedParameterQueryTests { + + private DataSource dataSource; + + private Connection connection; + + private PreparedStatement preparedStatement; + + private ResultSet resultSet; + + private ResultSetMetaData resultSetMetaData; + + private NamedParameterJdbcTemplate template; + + + @BeforeEach + public void setup() throws Exception { + connection = mock(Connection.class); + dataSource = mock(DataSource.class); + preparedStatement = mock(PreparedStatement.class); + resultSet = mock(ResultSet.class); + resultSetMetaData = mock(ResultSetMetaData.class); + template = new NamedParameterJdbcTemplate(dataSource); + given(dataSource.getConnection()).willReturn(connection); + given(resultSetMetaData.getColumnCount()).willReturn(1); + given(resultSetMetaData.getColumnLabel(1)).willReturn("age"); + given(connection.prepareStatement(anyString())).willReturn(preparedStatement); + given(preparedStatement.executeQuery()).willReturn(resultSet); + } + + @AfterEach + public void verifyClose() throws Exception { + verify(preparedStatement).close(); + verify(resultSet).close(); + verify(connection).close(); + } + + + @Test + public void testQueryForListWithParamMap() throws Exception { + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, true, false); + given(resultSet.getObject(1)).willReturn(11, 12); + + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("id", 3); + List> li = template.queryForList( + "SELECT AGE FROM CUSTMR WHERE ID < :id", params); + + assertThat(li.size()).as("All rows returned").isEqualTo(2); + assertThat(((Integer) li.get(0).get("age")).intValue()).as("First row is Integer").isEqualTo(11); + assertThat(((Integer) li.get(1).get("age")).intValue()).as("Second row is Integer").isEqualTo(12); + + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID < ?"); + verify(preparedStatement).setObject(1, 3); + } + + @Test + public void testQueryForListWithParamMapAndEmptyResult() throws Exception { + given(resultSet.next()).willReturn(false); + + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("id", 3); + List> li = template.queryForList( + "SELECT AGE FROM CUSTMR WHERE ID < :id", params); + + assertThat(li.size()).as("All rows returned").isEqualTo(0); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID < ?"); + verify(preparedStatement).setObject(1, 3); + } + + @Test + public void testQueryForListWithParamMapAndSingleRowAndColumn() throws Exception { + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, false); + given(resultSet.getObject(1)).willReturn(11); + + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("id", 3); + List> li = template.queryForList( + "SELECT AGE FROM CUSTMR WHERE ID < :id", params); + + assertThat(li.size()).as("All rows returned").isEqualTo(1); + assertThat(((Integer) li.get(0).get("age")).intValue()).as("First row is Integer").isEqualTo(11); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID < ?"); + verify(preparedStatement).setObject(1, 3); + } + + @Test + public void testQueryForListWithParamMapAndIntegerElementAndSingleRowAndColumn() + throws Exception { + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt(1)).willReturn(11); + + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("id", 3); + List li = template.queryForList("SELECT AGE FROM CUSTMR WHERE ID < :id", + params, Integer.class); + + assertThat(li.size()).as("All rows returned").isEqualTo(1); + assertThat(li.get(0).intValue()).as("First row is Integer").isEqualTo(11); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID < ?"); + verify(preparedStatement).setObject(1, 3); + } + + @Test + public void testQueryForMapWithParamMapAndSingleRowAndColumn() throws Exception { + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, false); + given(resultSet.getObject(1)).willReturn(11); + + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("id", 3); + Map map = template.queryForMap("SELECT AGE FROM CUSTMR WHERE ID < :id", params); + + assertThat(((Integer) map.get("age")).intValue()).as("Row is Integer").isEqualTo(11); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID < ?"); + verify(preparedStatement).setObject(1, 3); + } + + @Test + public void testQueryForObjectWithParamMapAndRowMapper() throws Exception { + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt(1)).willReturn(22); + + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("id", 3); + Object o = template.queryForObject("SELECT AGE FROM CUSTMR WHERE ID = :id", + params, new RowMapper() { + @Override + public Object mapRow(ResultSet rs, int rowNum) throws SQLException { + return rs.getInt(1); + } + }); + + boolean condition = o instanceof Integer; + assertThat(condition).as("Correct result type").isTrue(); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?"); + verify(preparedStatement).setObject(1, 3); + } + + @Test + public void testQueryForObjectWithMapAndInteger() throws Exception { + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt(1)).willReturn(22); + + Map params = new HashMap<>(); + params.put("id", 3); + Object o = template.queryForObject("SELECT AGE FROM CUSTMR WHERE ID = :id", + params, Integer.class); + + boolean condition = o instanceof Integer; + assertThat(condition).as("Correct result type").isTrue(); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?"); + verify(preparedStatement).setObject(1, 3); + } + + @Test + public void testQueryForObjectWithParamMapAndInteger() throws Exception { + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt(1)).willReturn(22); + + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("id", 3); + Object o = template.queryForObject("SELECT AGE FROM CUSTMR WHERE ID = :id", + params, Integer.class); + + boolean condition = o instanceof Integer; + assertThat(condition).as("Correct result type").isTrue(); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?"); + verify(preparedStatement).setObject(1, 3); + } + + @Test + public void testQueryForObjectWithParamMapAndList() throws Exception { + String sql = "SELECT AGE FROM CUSTMR WHERE ID IN (:ids)"; + String sqlToUse = "SELECT AGE FROM CUSTMR WHERE ID IN (?, ?)"; + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt(1)).willReturn(22); + + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("ids", Arrays.asList(3, 4)); + Object o = template.queryForObject(sql, params, Integer.class); + + boolean condition = o instanceof Integer; + assertThat(condition).as("Correct result type").isTrue(); + verify(connection).prepareStatement(sqlToUse); + verify(preparedStatement).setObject(1, 3); + } + + @Test + public void testQueryForObjectWithParamMapAndListOfExpressionLists() throws Exception { + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt(1)).willReturn(22); + + MapSqlParameterSource params = new MapSqlParameterSource(); + List l1 = new ArrayList<>(); + l1.add(new Object[] {3, "Rod"}); + l1.add(new Object[] {4, "Juergen"}); + params.addValue("multiExpressionList", l1); + Object o = template.queryForObject( + "SELECT AGE FROM CUSTMR WHERE (ID, NAME) IN (:multiExpressionList)", + params, Integer.class); + + boolean condition = o instanceof Integer; + assertThat(condition).as("Correct result type").isTrue(); + verify(connection).prepareStatement( + "SELECT AGE FROM CUSTMR WHERE (ID, NAME) IN ((?, ?), (?, ?))"); + verify(preparedStatement).setObject(1, 3); + } + + @Test + public void testQueryForIntWithParamMap() throws Exception { + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt(1)).willReturn(22); + + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("id", 3); + int i = template.queryForObject("SELECT AGE FROM CUSTMR WHERE ID = :id", params, Integer.class).intValue(); + + assertThat(i).as("Return of an int").isEqualTo(22); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?"); + verify(preparedStatement).setObject(1, 3); + } + + @Test + public void testQueryForLongWithParamBean() throws Exception { + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, false); + given(resultSet.getLong(1)).willReturn(87L); + + BeanPropertySqlParameterSource params = new BeanPropertySqlParameterSource(new ParameterBean(3)); + long l = template.queryForObject("SELECT AGE FROM CUSTMR WHERE ID = :id", params, Long.class).longValue(); + + assertThat(l).as("Return of a long").isEqualTo(87); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?"); + verify(preparedStatement).setObject(1, 3, Types.INTEGER); + } + + @Test + public void testQueryForLongWithParamBeanWithCollection() throws Exception { + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, false); + given(resultSet.getLong(1)).willReturn(87L); + + BeanPropertySqlParameterSource params = new BeanPropertySqlParameterSource(new ParameterCollectionBean(3, 5)); + long l = template.queryForObject("SELECT AGE FROM CUSTMR WHERE ID IN (:ids)", params, Long.class).longValue(); + + assertThat(l).as("Return of a long").isEqualTo(87); + verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID IN (?, ?)"); + verify(preparedStatement).setObject(1, 3); + verify(preparedStatement).setObject(2, 5); + } + + + static class ParameterBean { + + private final int id; + + public ParameterBean(int id) { + this.id = id; + } + + public int getId() { + return id; + } + } + + + static class ParameterCollectionBean { + + private final Collection ids; + + public ParameterCollectionBean(Integer... ids) { + this.ids = Arrays.asList(ids); + } + + public Collection getIds() { + return ids; + } + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterUtilsTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterUtilsTests.java new file mode 100644 index 0000000..685d2f3 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterUtilsTests.java @@ -0,0 +1,306 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.namedparam; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.dao.InvalidDataAccessApiUsageException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Thomas Risberg + * @author Juergen Hoeller + * @author Rick Evans + * @author Artur Geraschenko + */ +public class NamedParameterUtilsTests { + + @Test + public void parseSql() { + String sql = "xxx :a yyyy :b :c :a zzzzz"; + ParsedSql psql = NamedParameterUtils.parseSqlStatement(sql); + assertThat(NamedParameterUtils.substituteNamedParameters(psql, null)).isEqualTo("xxx ? yyyy ? ? ? zzzzz"); + assertThat(psql.getParameterNames().get(0)).isEqualTo("a"); + assertThat(psql.getParameterNames().get(2)).isEqualTo("c"); + assertThat(psql.getParameterNames().get(3)).isEqualTo("a"); + assertThat(psql.getTotalParameterCount()).isEqualTo(4); + assertThat(psql.getNamedParameterCount()).isEqualTo(3); + + String sql2 = "xxx &a yyyy ? zzzzz"; + ParsedSql psql2 = NamedParameterUtils.parseSqlStatement(sql2); + assertThat(NamedParameterUtils.substituteNamedParameters(psql2, null)).isEqualTo("xxx ? yyyy ? zzzzz"); + assertThat(psql2.getParameterNames().get(0)).isEqualTo("a"); + assertThat(psql2.getTotalParameterCount()).isEqualTo(2); + assertThat(psql2.getNamedParameterCount()).isEqualTo(1); + + String sql3 = "xxx &ä+:ö" + '\t' + ":ü%10 yyyy ? zzzzz"; + ParsedSql psql3 = NamedParameterUtils.parseSqlStatement(sql3); + assertThat(psql3.getParameterNames().get(0)).isEqualTo("ä"); + assertThat(psql3.getParameterNames().get(1)).isEqualTo("ö"); + assertThat(psql3.getParameterNames().get(2)).isEqualTo("ü"); + } + + @Test + public void substituteNamedParameters() { + MapSqlParameterSource namedParams = new MapSqlParameterSource(); + namedParams.addValue("a", "a").addValue("b", "b").addValue("c", "c"); + assertThat(NamedParameterUtils.substituteNamedParameters("xxx :a :b :c", namedParams)).isEqualTo("xxx ? ? ?"); + assertThat(NamedParameterUtils.substituteNamedParameters("xxx :a :b :c xx :a :a", namedParams)).isEqualTo("xxx ? ? ? xx ? ?"); + } + + @Test + public void convertParamMapToArray() { + Map paramMap = new HashMap<>(); + paramMap.put("a", "a"); + paramMap.put("b", "b"); + paramMap.put("c", "c"); + assertThat(NamedParameterUtils.buildValueArray("xxx :a :b :c", paramMap).length).isSameAs(3); + assertThat(NamedParameterUtils.buildValueArray("xxx :a :b :c xx :a :b", paramMap).length).isSameAs(5); + assertThat(NamedParameterUtils.buildValueArray("xxx :a :a :a xx :a :a", paramMap).length).isSameAs(5); + assertThat(NamedParameterUtils.buildValueArray("xxx :a :b :c xx :a :b", paramMap)[4]).isEqualTo("b"); + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).as("mixed named parameters and ? placeholders").isThrownBy(() -> + NamedParameterUtils.buildValueArray("xxx :a :b ?", paramMap)); + } + + @Test + public void convertTypeMapToArray() { + MapSqlParameterSource namedParams = new MapSqlParameterSource(); + namedParams.addValue("a", "a", 1).addValue("b", "b", 2).addValue("c", "c", 3); + assertThat(NamedParameterUtils + .buildSqlTypeArray(NamedParameterUtils.parseSqlStatement("xxx :a :b :c"), namedParams).length).isSameAs(3); + assertThat(NamedParameterUtils + .buildSqlTypeArray(NamedParameterUtils.parseSqlStatement("xxx :a :b :c xx :a :b"), namedParams).length).isSameAs(5); + assertThat(NamedParameterUtils + .buildSqlTypeArray(NamedParameterUtils.parseSqlStatement("xxx :a :a :a xx :a :a"), namedParams).length).isSameAs(5); + assertThat(NamedParameterUtils + .buildSqlTypeArray(NamedParameterUtils.parseSqlStatement("xxx :a :b :c xx :a :b"), namedParams)[4]).isEqualTo(2); + } + + @Test + public void convertTypeMapToSqlParameterList() { + MapSqlParameterSource namedParams = new MapSqlParameterSource(); + namedParams.addValue("a", "a", 1).addValue("b", "b", 2).addValue("c", "c", 3, "SQL_TYPE"); + assertThat(NamedParameterUtils + .buildSqlParameterList(NamedParameterUtils.parseSqlStatement("xxx :a :b :c"), namedParams).size()).isSameAs(3); + assertThat(NamedParameterUtils + .buildSqlParameterList(NamedParameterUtils.parseSqlStatement("xxx :a :b :c xx :a :b"), namedParams).size()).isSameAs(5); + assertThat(NamedParameterUtils + .buildSqlParameterList(NamedParameterUtils.parseSqlStatement("xxx :a :a :a xx :a :a"), namedParams).size()).isSameAs(5); + assertThat(NamedParameterUtils + .buildSqlParameterList(NamedParameterUtils.parseSqlStatement("xxx :a :b :c xx :a :b"), namedParams).get(4).getSqlType()).isEqualTo(2); + assertThat(NamedParameterUtils + .buildSqlParameterList(NamedParameterUtils.parseSqlStatement("xxx :a :b :c"), namedParams).get(2).getTypeName()).isEqualTo("SQL_TYPE"); + } + + @Test + public void buildValueArrayWithMissingParameterValue() { + String sql = "select count(0) from foo where id = :id"; + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() -> + NamedParameterUtils.buildValueArray(sql, Collections.emptyMap())); + } + + @Test + public void substituteNamedParametersWithStringContainingQuotes() { + String expectedSql = "select 'first name' from artists where id = ? and quote = 'exsqueeze me?'"; + String sql = "select 'first name' from artists where id = :id and quote = 'exsqueeze me?'"; + String newSql = NamedParameterUtils.substituteNamedParameters(sql, new MapSqlParameterSource()); + assertThat(newSql).isEqualTo(expectedSql); + } + + @Test + public void testParseSqlStatementWithStringContainingQuotes() { + String expectedSql = "select 'first name' from artists where id = ? and quote = 'exsqueeze me?'"; + String sql = "select 'first name' from artists where id = :id and quote = 'exsqueeze me?'"; + ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql); + assertThat(NamedParameterUtils.substituteNamedParameters(parsedSql, null)).isEqualTo(expectedSql); + } + + @Test // SPR-4789 + public void parseSqlContainingComments() { + String sql1 = "/*+ HINT */ xxx /* comment ? */ :a yyyy :b :c :a zzzzz -- :xx XX\n"; + ParsedSql psql1 = NamedParameterUtils.parseSqlStatement(sql1); + assertThat(NamedParameterUtils.substituteNamedParameters(psql1, null)).isEqualTo("/*+ HINT */ xxx /* comment ? */ ? yyyy ? ? ? zzzzz -- :xx XX\n"); + MapSqlParameterSource paramMap = new MapSqlParameterSource(); + paramMap.addValue("a", "a"); + paramMap.addValue("b", "b"); + paramMap.addValue("c", "c"); + Object[] params = NamedParameterUtils.buildValueArray(psql1, paramMap, null); + assertThat(params.length).isEqualTo(4); + assertThat(params[0]).isEqualTo("a"); + assertThat(params[1]).isEqualTo("b"); + assertThat(params[2]).isEqualTo("c"); + assertThat(params[3]).isEqualTo("a"); + + String sql2 = "/*+ HINT */ xxx /* comment ? */ :a yyyy :b :c :a zzzzz -- :xx XX"; + ParsedSql psql2 = NamedParameterUtils.parseSqlStatement(sql2); + assertThat(NamedParameterUtils.substituteNamedParameters(psql2, null)).isEqualTo("/*+ HINT */ xxx /* comment ? */ ? yyyy ? ? ? zzzzz -- :xx XX"); + + String sql3 = "/*+ HINT */ xxx /* comment ? */ :a yyyy :b :c :a zzzzz /* :xx XX*"; + ParsedSql psql3 = NamedParameterUtils.parseSqlStatement(sql3); + assertThat(NamedParameterUtils.substituteNamedParameters(psql3, null)).isEqualTo("/*+ HINT */ xxx /* comment ? */ ? yyyy ? ? ? zzzzz /* :xx XX*"); + + String sql4 = "/*+ HINT */ xxx /* comment :a ? */ :a yyyy :b :c :a zzzzz /* :xx XX*"; + ParsedSql psql4 = NamedParameterUtils.parseSqlStatement(sql4); + Map parameters = Collections.singletonMap("a", "0"); + assertThat(NamedParameterUtils.substituteNamedParameters(psql4, new MapSqlParameterSource(parameters))).isEqualTo("/*+ HINT */ xxx /* comment :a ? */ ? yyyy ? ? ? zzzzz /* :xx XX*"); + } + + @Test // SPR-4612 + public void parseSqlStatementWithPostgresCasting() { + String expectedSql = "select 'first name' from artists where id = ? and birth_date=?::timestamp"; + String sql = "select 'first name' from artists where id = :id and birth_date=:birthDate::timestamp"; + ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql); + assertThat(NamedParameterUtils.substituteNamedParameters(parsedSql, null)).isEqualTo(expectedSql); + } + + @Test // SPR-13582 + public void parseSqlStatementWithPostgresContainedOperator() { + String expectedSql = "select 'first name' from artists where info->'stat'->'albums' = ?? ? and '[\"1\",\"2\",\"3\"]'::jsonb ?? '4'"; + String sql = "select 'first name' from artists where info->'stat'->'albums' = ?? :album and '[\"1\",\"2\",\"3\"]'::jsonb ?? '4'"; + ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql); + assertThat(parsedSql.getTotalParameterCount()).isEqualTo(1); + assertThat(NamedParameterUtils.substituteNamedParameters(parsedSql, null)).isEqualTo(expectedSql); + } + + @Test // SPR-15382 + public void parseSqlStatementWithPostgresAnyArrayStringsExistsOperator() { + String expectedSql = "select '[\"3\", \"11\"]'::jsonb ?| '{1,3,11,12,17}'::text[]"; + String sql = "select '[\"3\", \"11\"]'::jsonb ?| '{1,3,11,12,17}'::text[]"; + + ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql); + assertThat(parsedSql.getTotalParameterCount()).isEqualTo(0); + assertThat(NamedParameterUtils.substituteNamedParameters(parsedSql, null)).isEqualTo(expectedSql); + } + + @Test // SPR-15382 + public void parseSqlStatementWithPostgresAllArrayStringsExistsOperator() { + String expectedSql = "select '[\"3\", \"11\"]'::jsonb ?& '{1,3,11,12,17}'::text[] AND ? = 'Back in Black'"; + String sql = "select '[\"3\", \"11\"]'::jsonb ?& '{1,3,11,12,17}'::text[] AND :album = 'Back in Black'"; + + ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql); + assertThat(parsedSql.getTotalParameterCount()).isEqualTo(1); + assertThat(NamedParameterUtils.substituteNamedParameters(parsedSql, null)).isEqualTo(expectedSql); + } + + @Test // SPR-7476 + public void parseSqlStatementWithEscapedColon() { + String expectedSql = "select '0\\:0' as a, foo from bar where baz < DATE(? 23:59:59) and baz = ?"; + String sql = "select '0\\:0' as a, foo from bar where baz < DATE(:p1 23\\:59\\:59) and baz = :p2"; + + ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql); + assertThat(parsedSql.getParameterNames().size()).isEqualTo(2); + assertThat(parsedSql.getParameterNames().get(0)).isEqualTo("p1"); + assertThat(parsedSql.getParameterNames().get(1)).isEqualTo("p2"); + String finalSql = NamedParameterUtils.substituteNamedParameters(parsedSql, null); + assertThat(finalSql).isEqualTo(expectedSql); + } + + @Test // SPR-7476 + public void parseSqlStatementWithBracketDelimitedParameterNames() { + String expectedSql = "select foo from bar where baz = b??z"; + String sql = "select foo from bar where baz = b:{p1}:{p2}z"; + + ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql); + assertThat(parsedSql.getParameterNames().size()).isEqualTo(2); + assertThat(parsedSql.getParameterNames().get(0)).isEqualTo("p1"); + assertThat(parsedSql.getParameterNames().get(1)).isEqualTo("p2"); + String finalSql = NamedParameterUtils.substituteNamedParameters(parsedSql, null); + assertThat(finalSql).isEqualTo(expectedSql); + } + + @Test // SPR-7476 + public void parseSqlStatementWithEmptyBracketsOrBracketsInQuotes() { + String expectedSql = "select foo from bar where baz = b:{}z"; + String sql = "select foo from bar where baz = b:{}z"; + ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql); + assertThat(parsedSql.getParameterNames().size()).isEqualTo(0); + String finalSql = NamedParameterUtils.substituteNamedParameters(parsedSql, null); + assertThat(finalSql).isEqualTo(expectedSql); + + String expectedSql2 = "select foo from bar where baz = 'b:{p1}z'"; + String sql2 = "select foo from bar where baz = 'b:{p1}z'"; + + ParsedSql parsedSql2 = NamedParameterUtils.parseSqlStatement(sql2); + assertThat(parsedSql2.getParameterNames().size()).isEqualTo(0); + String finalSql2 = NamedParameterUtils.substituteNamedParameters(parsedSql2, null); + assertThat(finalSql2).isEqualTo(expectedSql2); + } + + @Test + public void parseSqlStatementWithSingleLetterInBrackets() { + String expectedSql = "select foo from bar where baz = b?z"; + String sql = "select foo from bar where baz = b:{p}z"; + + ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql); + assertThat(parsedSql.getParameterNames().size()).isEqualTo(1); + assertThat(parsedSql.getParameterNames().get(0)).isEqualTo("p"); + String finalSql = NamedParameterUtils.substituteNamedParameters(parsedSql, null); + assertThat(finalSql).isEqualTo(expectedSql); + } + + @Test // SPR-2544 + public void parseSqlStatementWithLogicalAnd() { + String expectedSql = "xxx & yyyy"; + ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(expectedSql); + assertThat(NamedParameterUtils.substituteNamedParameters(parsedSql, null)).isEqualTo(expectedSql); + } + + @Test // SPR-2544 + public void substituteNamedParametersWithLogicalAnd() { + String expectedSql = "xxx & yyyy"; + String newSql = NamedParameterUtils.substituteNamedParameters(expectedSql, new MapSqlParameterSource()); + assertThat(newSql).isEqualTo(expectedSql); + } + + @Test // SPR-3173 + public void variableAssignmentOperator() { + String expectedSql = "x := 1"; + String newSql = NamedParameterUtils.substituteNamedParameters(expectedSql, new MapSqlParameterSource()); + assertThat(newSql).isEqualTo(expectedSql); + } + + @Test // SPR-8280 + public void parseSqlStatementWithQuotedSingleQuote() { + String sql = "SELECT ':foo'':doo', :xxx FROM DUAL"; + ParsedSql psql = NamedParameterUtils.parseSqlStatement(sql); + assertThat(psql.getTotalParameterCount()).isEqualTo(1); + assertThat(psql.getParameterNames().get(0)).isEqualTo("xxx"); + } + + @Test + public void parseSqlStatementWithQuotesAndCommentBefore() { + String sql = "SELECT /*:doo*/':foo', :xxx FROM DUAL"; + ParsedSql psql = NamedParameterUtils.parseSqlStatement(sql); + assertThat(psql.getTotalParameterCount()).isEqualTo(1); + assertThat(psql.getParameterNames().get(0)).isEqualTo("xxx"); + } + + @Test + public void parseSqlStatementWithQuotesAndCommentAfter() { + String sql2 = "SELECT ':foo'/*:doo*/, :xxx FROM DUAL"; + ParsedSql psql2 = NamedParameterUtils.parseSqlStatement(sql2); + assertThat(psql2.getTotalParameterCount()).isEqualTo(1); + assertThat(psql2.getParameterNames().get(0)).isEqualTo("xxx"); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/CallMetaDataContextTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/CallMetaDataContextTests.java new file mode 100644 index 0000000..260b85f --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/CallMetaDataContextTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.simple; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.Types; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.jdbc.core.SqlInOutParameter; +import org.springframework.jdbc.core.SqlOutParameter; +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.jdbc.core.metadata.CallMetaDataContext; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Mock object based tests for CallMetaDataContext. + * + * @author Thomas Risberg + */ +public class CallMetaDataContextTests { + + private DataSource dataSource; + + private Connection connection; + + private DatabaseMetaData databaseMetaData; + + private CallMetaDataContext context = new CallMetaDataContext(); + + + @BeforeEach + public void setUp() throws Exception { + connection = mock(Connection.class); + databaseMetaData = mock(DatabaseMetaData.class); + given(connection.getMetaData()).willReturn(databaseMetaData); + dataSource = mock(DataSource.class); + given(dataSource.getConnection()).willReturn(connection); + } + + @AfterEach + public void verifyClosed() throws Exception { + verify(connection).close(); + } + + + @Test + public void testMatchParameterValuesAndSqlInOutParameters() throws Exception { + final String TABLE = "customers"; + final String USER = "me"; + given(databaseMetaData.getDatabaseProductName()).willReturn("MyDB"); + given(databaseMetaData.getUserName()).willReturn(USER); + given(databaseMetaData.storesLowerCaseIdentifiers()).willReturn(true); + + List parameters = new ArrayList<>(); + parameters.add(new SqlParameter("id", Types.NUMERIC)); + parameters.add(new SqlInOutParameter("name", Types.NUMERIC)); + parameters.add(new SqlOutParameter("customer_no", Types.NUMERIC)); + + MapSqlParameterSource parameterSource = new MapSqlParameterSource(); + parameterSource.addValue("id", 1); + parameterSource.addValue("name", "Sven"); + parameterSource.addValue("customer_no", "12345XYZ"); + + context.setProcedureName(TABLE); + context.initializeMetaData(dataSource); + context.processParameters(parameters); + + Map inParameters = context.matchInParameterValuesWithCallParameters(parameterSource); + assertThat(inParameters.size()).as("Wrong number of matched in parameter values").isEqualTo(2); + assertThat(inParameters.containsKey("id")).as("in parameter value missing").isTrue(); + assertThat(inParameters.containsKey("name")).as("in out parameter value missing").isTrue(); + boolean condition = !inParameters.containsKey("customer_no"); + assertThat(condition).as("out parameter value matched").isTrue(); + + List names = context.getOutParameterNames(); + assertThat(names.size()).as("Wrong number of out parameters").isEqualTo(2); + + List callParameters = context.getCallParameters(); + assertThat(callParameters.size()).as("Wrong number of call parameters").isEqualTo(3); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcCallTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcCallTests.java new file mode 100644 index 0000000..51a9bca --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcCallTests.java @@ -0,0 +1,317 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.simple; + +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.jdbc.core.SqlOutParameter; +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link SimpleJdbcCall}. + * + * @author Thomas Risberg + * @author Kiril Nugmanov + */ +public class SimpleJdbcCallTests { + + private Connection connection; + + private DatabaseMetaData databaseMetaData; + + private DataSource dataSource; + + private CallableStatement callableStatement; + + + @BeforeEach + public void setUp() throws Exception { + connection = mock(Connection.class); + databaseMetaData = mock(DatabaseMetaData.class); + dataSource = mock(DataSource.class); + callableStatement = mock(CallableStatement.class); + given(connection.getMetaData()).willReturn(databaseMetaData); + given(dataSource.getConnection()).willReturn(connection); + } + + + @Test + public void testNoSuchStoredProcedure() throws Exception { + final String NO_SUCH_PROC = "x"; + SQLException sqlException = new SQLException("Syntax error or access violation exception", "42000"); + given(databaseMetaData.getDatabaseProductName()).willReturn("MyDB"); + given(databaseMetaData.getDatabaseProductName()).willReturn("MyDB"); + given(databaseMetaData.getUserName()).willReturn("me"); + given(databaseMetaData.storesLowerCaseIdentifiers()).willReturn(true); + given(callableStatement.execute()).willThrow(sqlException); + given(connection.prepareCall("{call " + NO_SUCH_PROC + "()}")).willReturn(callableStatement); + SimpleJdbcCall sproc = new SimpleJdbcCall(dataSource).withProcedureName(NO_SUCH_PROC); + try { + assertThatExceptionOfType(BadSqlGrammarException.class).isThrownBy(() -> + sproc.execute()) + .withCause(sqlException); + } + finally { + verify(callableStatement).close(); + verify(connection, atLeastOnce()).close(); + } + } + + @Test + public void testUnnamedParameterHandling() throws Exception { + final String MY_PROC = "my_proc"; + SimpleJdbcCall sproc = new SimpleJdbcCall(dataSource).withProcedureName(MY_PROC); + // Shouldn't succeed in adding unnamed parameter + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() -> + sproc.addDeclaredParameter(new SqlParameter(1))); + } + + @Test + public void testAddInvoiceProcWithoutMetaDataUsingMapParamSource() throws Exception { + initializeAddInvoiceWithoutMetaData(false); + SimpleJdbcCall adder = new SimpleJdbcCall(dataSource).withProcedureName("add_invoice"); + adder.declareParameters( + new SqlParameter("amount", Types.INTEGER), + new SqlParameter("custid", Types.INTEGER), + new SqlOutParameter("newid", Types.INTEGER)); + Number newId = adder.executeObject(Number.class, new MapSqlParameterSource(). + addValue("amount", 1103). + addValue("custid", 3)); + assertThat(newId.intValue()).isEqualTo(4); + verifyAddInvoiceWithoutMetaData(false); + verify(connection, atLeastOnce()).close(); + } + + @Test + public void testAddInvoiceProcWithoutMetaDataUsingArrayParams() throws Exception { + initializeAddInvoiceWithoutMetaData(false); + SimpleJdbcCall adder = new SimpleJdbcCall(dataSource).withProcedureName("add_invoice"); + adder.declareParameters( + new SqlParameter("amount", Types.INTEGER), + new SqlParameter("custid", Types.INTEGER), + new SqlOutParameter("newid", Types.INTEGER)); + Number newId = adder.executeObject(Number.class, 1103, 3); + assertThat(newId.intValue()).isEqualTo(4); + verifyAddInvoiceWithoutMetaData(false); + verify(connection, atLeastOnce()).close(); + } + + @Test + public void testAddInvoiceProcWithMetaDataUsingMapParamSource() throws Exception { + initializeAddInvoiceWithMetaData(false); + SimpleJdbcCall adder = new SimpleJdbcCall(dataSource).withProcedureName("add_invoice"); + Number newId = adder.executeObject(Number.class, new MapSqlParameterSource() + .addValue("amount", 1103) + .addValue("custid", 3)); + assertThat(newId.intValue()).isEqualTo(4); + verifyAddInvoiceWithMetaData(false); + verify(connection, atLeastOnce()).close(); + } + + @Test + public void testAddInvoiceProcWithMetaDataUsingArrayParams() throws Exception { + initializeAddInvoiceWithMetaData(false); + SimpleJdbcCall adder = new SimpleJdbcCall(dataSource).withProcedureName("add_invoice"); + Number newId = adder.executeObject(Number.class, 1103, 3); + assertThat(newId.intValue()).isEqualTo(4); + verifyAddInvoiceWithMetaData(false); + verify(connection, atLeastOnce()).close(); + } + + @Test + public void testAddInvoiceFuncWithoutMetaDataUsingMapParamSource() throws Exception { + initializeAddInvoiceWithoutMetaData(true); + SimpleJdbcCall adder = new SimpleJdbcCall(dataSource).withFunctionName("add_invoice"); + adder.declareParameters( + new SqlOutParameter("return", Types.INTEGER), + new SqlParameter("amount", Types.INTEGER), + new SqlParameter("custid", Types.INTEGER)); + Number newId = adder.executeFunction(Number.class, new MapSqlParameterSource() + .addValue("amount", 1103) + .addValue("custid", 3)); + assertThat(newId.intValue()).isEqualTo(4); + verifyAddInvoiceWithoutMetaData(true); + verify(connection, atLeastOnce()).close(); + } + + @Test + public void testAddInvoiceFuncWithoutMetaDataUsingArrayParams() throws Exception { + initializeAddInvoiceWithoutMetaData(true); + SimpleJdbcCall adder = new SimpleJdbcCall(dataSource).withFunctionName("add_invoice"); + adder.declareParameters( + new SqlOutParameter("return", Types.INTEGER), + new SqlParameter("amount", Types.INTEGER), + new SqlParameter("custid", Types.INTEGER)); + Number newId = adder.executeFunction(Number.class, 1103, 3); + assertThat(newId.intValue()).isEqualTo(4); + verifyAddInvoiceWithoutMetaData(true); + verify(connection, atLeastOnce()).close(); + } + + @Test + public void testAddInvoiceFuncWithMetaDataUsingMapParamSource() throws Exception { + initializeAddInvoiceWithMetaData(true); + SimpleJdbcCall adder = new SimpleJdbcCall(dataSource).withFunctionName("add_invoice"); + Number newId = adder.executeFunction(Number.class, new MapSqlParameterSource() + .addValue("amount", 1103) + .addValue("custid", 3)); + assertThat(newId.intValue()).isEqualTo(4); + verifyAddInvoiceWithMetaData(true); + verify(connection, atLeastOnce()).close(); + + } + + @Test + public void testAddInvoiceFuncWithMetaDataUsingArrayParams() throws Exception { + initializeAddInvoiceWithMetaData(true); + SimpleJdbcCall adder = new SimpleJdbcCall(dataSource).withFunctionName("add_invoice"); + Number newId = adder.executeFunction(Number.class, 1103, 3); + assertThat(newId.intValue()).isEqualTo(4); + verifyAddInvoiceWithMetaData(true); + verify(connection, atLeastOnce()).close(); + + } + + @Test + public void testCorrectFunctionStatement() throws Exception { + initializeAddInvoiceWithMetaData(true); + SimpleJdbcCall adder = new SimpleJdbcCall(dataSource).withFunctionName("add_invoice"); + adder.compile(); + verifyStatement(adder, "{? = call ADD_INVOICE(?, ?)}"); + } + + @Test + public void testCorrectFunctionStatementNamed() throws Exception { + initializeAddInvoiceWithMetaData(true); + SimpleJdbcCall adder = new SimpleJdbcCall(dataSource).withNamedBinding().withFunctionName("add_invoice"); + adder.compile(); + verifyStatement(adder, "{? = call ADD_INVOICE(AMOUNT => ?, CUSTID => ?)}"); + } + + @Test + public void testCorrectProcedureStatementNamed() throws Exception { + initializeAddInvoiceWithMetaData(false); + SimpleJdbcCall adder = new SimpleJdbcCall(dataSource).withNamedBinding().withProcedureName("add_invoice"); + adder.compile(); + verifyStatement(adder, "{call ADD_INVOICE(AMOUNT => ?, CUSTID => ?, NEWID => ?)}"); + } + + + private void verifyStatement(SimpleJdbcCall adder, String expected) { + assertThat(adder.getCallString()).as("Incorrect call statement").isEqualTo(expected); + } + + private void initializeAddInvoiceWithoutMetaData(boolean isFunction) throws SQLException { + given(databaseMetaData.getDatabaseProductName()).willReturn("MyDB"); + given(databaseMetaData.getUserName()).willReturn("me"); + given(databaseMetaData.storesLowerCaseIdentifiers()).willReturn(true); + given(callableStatement.execute()).willReturn(false); + given(callableStatement.getUpdateCount()).willReturn(-1); + if (isFunction) { + given(callableStatement.getObject(1)).willReturn(4L); + given(connection.prepareCall("{? = call add_invoice(?, ?)}") + ).willReturn(callableStatement); + } + else { + given(callableStatement.getObject(3)).willReturn(4L); + given(connection.prepareCall("{call add_invoice(?, ?, ?)}") + ).willReturn(callableStatement); + } + } + + private void verifyAddInvoiceWithoutMetaData(boolean isFunction) throws SQLException { + if (isFunction) { + verify(callableStatement).registerOutParameter(1, 4); + verify(callableStatement).setObject(2, 1103, 4); + verify(callableStatement).setObject(3, 3, 4); + } + else { + verify(callableStatement).setObject(1, 1103, 4); + verify(callableStatement).setObject(2, 3, 4); + verify(callableStatement).registerOutParameter(3, 4); + } + verify(callableStatement).close(); + } + + private void initializeAddInvoiceWithMetaData(boolean isFunction) throws SQLException { + ResultSet proceduresResultSet = mock(ResultSet.class); + ResultSet procedureColumnsResultSet = mock(ResultSet.class); + given(databaseMetaData.getDatabaseProductName()).willReturn("Oracle"); + given(databaseMetaData.getUserName()).willReturn("ME"); + given(databaseMetaData.storesUpperCaseIdentifiers()).willReturn(true); + given(databaseMetaData.getProcedures("", "ME", "ADD_INVOICE")).willReturn(proceduresResultSet); + given(databaseMetaData.getProcedureColumns("", "ME", "ADD_INVOICE", null)).willReturn(procedureColumnsResultSet); + + given(proceduresResultSet.next()).willReturn(true, false); + given(proceduresResultSet.getString("PROCEDURE_NAME")).willReturn("add_invoice"); + + given(procedureColumnsResultSet.next()).willReturn(true, true, true, false); + given(procedureColumnsResultSet.getInt("DATA_TYPE")).willReturn(4); + if (isFunction) { + given(procedureColumnsResultSet.getString("COLUMN_NAME")).willReturn(null,"amount", "custid"); + given(procedureColumnsResultSet.getInt("COLUMN_TYPE")).willReturn(5, 1, 1); + given(connection.prepareCall("{? = call ADD_INVOICE(?, ?)}")).willReturn(callableStatement); + given(callableStatement.getObject(1)).willReturn(4L); + } + else { + given(procedureColumnsResultSet.getString("COLUMN_NAME")).willReturn("amount", "custid", "newid"); + given(procedureColumnsResultSet.getInt("COLUMN_TYPE")).willReturn(1, 1, 4); + given(connection.prepareCall("{call ADD_INVOICE(?, ?, ?)}")).willReturn(callableStatement); + given(callableStatement.getObject(3)).willReturn(4L); + } + given(callableStatement.getUpdateCount()).willReturn(-1); + } + + private void verifyAddInvoiceWithMetaData(boolean isFunction) throws SQLException { + ResultSet proceduresResultSet = databaseMetaData.getProcedures("", "ME", "ADD_INVOICE"); + ResultSet procedureColumnsResultSet = databaseMetaData.getProcedureColumns("", "ME", "ADD_INVOICE", null); + if (isFunction) { + verify(callableStatement).registerOutParameter(1, 4); + verify(callableStatement).setObject(2, 1103, 4); + verify(callableStatement).setObject(3, 3, 4); + } + else { + verify(callableStatement).setObject(1, 1103, 4); + verify(callableStatement).setObject(2, 3, 4); + verify(callableStatement).registerOutParameter(3, 4); + } + verify(callableStatement).close(); + verify(proceduresResultSet).close(); + verify(procedureColumnsResultSet).close(); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcInsertTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcInsertTests.java new file mode 100644 index 0000000..ec399d3 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcInsertTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.simple; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.util.HashMap; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.dao.InvalidDataAccessApiUsageException; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Mock object based tests for SimpleJdbcInsert. + * + * @author Thomas Risberg + */ +public class SimpleJdbcInsertTests { + + private Connection connection; + + private DatabaseMetaData databaseMetaData; + + private DataSource dataSource; + + + @BeforeEach + public void setUp() throws Exception { + connection = mock(Connection.class); + databaseMetaData = mock(DatabaseMetaData.class); + dataSource = mock(DataSource.class); + given(connection.getMetaData()).willReturn(databaseMetaData); + given(dataSource.getConnection()).willReturn(connection); + } + + @AfterEach + public void verifyClosed() throws Exception { + verify(connection).close(); + } + + + @Test + public void testNoSuchTable() throws Exception { + ResultSet resultSet = mock(ResultSet.class); + given(resultSet.next()).willReturn(false); + given(databaseMetaData.getDatabaseProductName()).willReturn("MyDB"); + given(databaseMetaData.getDatabaseProductName()).willReturn("MyDB"); + given(databaseMetaData.getDatabaseProductVersion()).willReturn("1.0"); + given(databaseMetaData.getUserName()).willReturn("me"); + given(databaseMetaData.storesLowerCaseIdentifiers()).willReturn(true); + given(databaseMetaData.getTables(null, null, "x", null)).willReturn(resultSet); + + SimpleJdbcInsert insert = new SimpleJdbcInsert(dataSource).withTableName("x"); + // Shouldn't succeed in inserting into table which doesn't exist + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() -> + insert.execute(new HashMap<>())); + verify(resultSet).close(); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/TableMetaDataContextTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/TableMetaDataContextTests.java new file mode 100644 index 0000000..f49e69e --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/TableMetaDataContextTests.java @@ -0,0 +1,163 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.simple; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.Types; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.jdbc.core.SqlParameterValue; +import org.springframework.jdbc.core.metadata.TableMetaDataContext; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Mock object based tests for TableMetaDataContext. + * + * @author Thomas Risberg + */ +public class TableMetaDataContextTests { + + private Connection connection; + + private DataSource dataSource; + + private DatabaseMetaData databaseMetaData; + + private TableMetaDataContext context = new TableMetaDataContext(); + + + @BeforeEach + public void setUp() throws Exception { + connection = mock(Connection.class); + dataSource = mock(DataSource.class); + databaseMetaData = mock(DatabaseMetaData.class); + given(connection.getMetaData()).willReturn(databaseMetaData); + given(dataSource.getConnection()).willReturn(connection); + } + + + @Test + public void testMatchInParametersAndSqlTypeInfoWrapping() throws Exception { + final String TABLE = "customers"; + final String USER = "me"; + + ResultSet metaDataResultSet = mock(ResultSet.class); + given(metaDataResultSet.next()).willReturn(true, false); + given(metaDataResultSet.getString("TABLE_SCHEM")).willReturn(USER); + given(metaDataResultSet.getString("TABLE_NAME")).willReturn(TABLE); + given(metaDataResultSet.getString("TABLE_TYPE")).willReturn("TABLE"); + + ResultSet columnsResultSet = mock(ResultSet.class); + given(columnsResultSet.next()).willReturn( + true, true, true, true, false); + given(columnsResultSet.getString("COLUMN_NAME")).willReturn( + "id", "name", "customersince", "version"); + given(columnsResultSet.getInt("DATA_TYPE")).willReturn( + Types.INTEGER, Types.VARCHAR, Types.DATE, Types.NUMERIC); + given(columnsResultSet.getBoolean("NULLABLE")).willReturn( + false, true, true, false); + + given(databaseMetaData.getDatabaseProductName()).willReturn("MyDB"); + given(databaseMetaData.getDatabaseProductName()).willReturn("1.0"); + given(databaseMetaData.getUserName()).willReturn(USER); + given(databaseMetaData.storesLowerCaseIdentifiers()).willReturn(true); + given(databaseMetaData.getTables(null, null, TABLE, null)).willReturn(metaDataResultSet); + given(databaseMetaData.getColumns(null, USER, TABLE, null)).willReturn(columnsResultSet); + + MapSqlParameterSource map = new MapSqlParameterSource(); + map.addValue("id", 1); + map.addValue("name", "Sven"); + map.addValue("customersince", new Date()); + map.addValue("version", 0); + map.registerSqlType("customersince", Types.DATE); + map.registerSqlType("version", Types.NUMERIC); + + context.setTableName(TABLE); + context.processMetaData(dataSource, new ArrayList<>(), new String[] {}); + + List values = context.matchInParameterValuesWithInsertColumns(map); + + assertThat(values.size()).as("wrong number of parameters: ").isEqualTo(4); + boolean condition3 = values.get(0) instanceof Number; + assertThat(condition3).as("id not wrapped with type info").isTrue(); + boolean condition2 = values.get(1) instanceof String; + assertThat(condition2).as("name not wrapped with type info").isTrue(); + boolean condition1 = values.get(2) instanceof SqlParameterValue; + assertThat(condition1).as("date wrapped with type info").isTrue(); + boolean condition = values.get(3) instanceof SqlParameterValue; + assertThat(condition).as("version wrapped with type info").isTrue(); + verify(metaDataResultSet, atLeastOnce()).next(); + verify(columnsResultSet, atLeastOnce()).next(); + verify(metaDataResultSet).close(); + verify(columnsResultSet).close(); + } + + @Test + public void testTableWithSingleColumnGeneratedKey() throws Exception { + final String TABLE = "customers"; + final String USER = "me"; + + ResultSet metaDataResultSet = mock(ResultSet.class); + given(metaDataResultSet.next()).willReturn(true, false); + given(metaDataResultSet.getString("TABLE_SCHEM")).willReturn(USER); + given(metaDataResultSet.getString("TABLE_NAME")).willReturn(TABLE); + given(metaDataResultSet.getString("TABLE_TYPE")).willReturn("TABLE"); + + ResultSet columnsResultSet = mock(ResultSet.class); + given(columnsResultSet.next()).willReturn(true, false); + given(columnsResultSet.getString("COLUMN_NAME")).willReturn("id"); + given(columnsResultSet.getInt("DATA_TYPE")).willReturn(Types.INTEGER); + given(columnsResultSet.getBoolean("NULLABLE")).willReturn(false); + + given(databaseMetaData.getDatabaseProductName()).willReturn("MyDB"); + given(databaseMetaData.getDatabaseProductName()).willReturn("1.0"); + given(databaseMetaData.getUserName()).willReturn(USER); + given(databaseMetaData.storesLowerCaseIdentifiers()).willReturn(true); + given(databaseMetaData.getTables(null, null, TABLE, null)).willReturn(metaDataResultSet); + given(databaseMetaData.getColumns(null, USER, TABLE, null)).willReturn(columnsResultSet); + + MapSqlParameterSource map = new MapSqlParameterSource(); + String[] keyCols = new String[] { "id" }; + context.setTableName(TABLE); + context.processMetaData(dataSource, new ArrayList<>(), keyCols); + List values = context.matchInParameterValuesWithInsertColumns(map); + String insertString = context.createInsertString(keyCols); + + assertThat(values.size()).as("wrong number of parameters: ").isEqualTo(0); + assertThat(insertString).as("empty insert not generated correctly").isEqualTo("INSERT INTO customers () VALUES()"); + verify(metaDataResultSet, atLeastOnce()).next(); + verify(columnsResultSet, atLeastOnce()).next(); + verify(metaDataResultSet).close(); + verify(columnsResultSet).close(); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/JdbcBeanDefinitionReaderTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/JdbcBeanDefinitionReaderTests.java new file mode 100644 index 0000000..f6452be --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/JdbcBeanDefinitionReaderTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.support; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.testfixture.beans.TestBean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author Rod Johnson + */ +class JdbcBeanDefinitionReaderTests { + + @Test + @SuppressWarnings("deprecation") + void readBeanDefinitionFromMockedDataSource() throws Exception { + String sql = "SELECT NAME AS NAME, PROPERTY AS PROPERTY, VALUE AS VALUE FROM T"; + + Connection connection = mock(Connection.class); + DataSource dataSource = mock(DataSource.class); + given(dataSource.getConnection()).willReturn(connection); + + ResultSet resultSet = mock(ResultSet.class); + given(resultSet.next()).willReturn(true, true, false); + given(resultSet.getString(1)).willReturn("one", "one"); + given(resultSet.getString(2)).willReturn("(class)", "age"); + given(resultSet.getString(3)).willReturn("org.springframework.beans.testfixture.beans.TestBean", "53"); + + Statement statement = mock(Statement.class); + given(statement.executeQuery(sql)).willReturn(resultSet); + given(connection.createStatement()).willReturn(statement); + + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + JdbcBeanDefinitionReader reader = new JdbcBeanDefinitionReader(bf); + reader.setDataSource(dataSource); + reader.loadBeanDefinitions(sql); + assertThat(bf.getBeanDefinitionCount()).as("Incorrect number of bean definitions").isEqualTo(1); + TestBean tb = (TestBean) bf.getBean("one"); + assertThat(tb.getAge()).as("Age in TestBean was wrong.").isEqualTo(53); + + verify(resultSet).close(); + verify(statement).close(); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/JdbcDaoSupportTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/JdbcDaoSupportTests.java new file mode 100644 index 0000000..8f95cf8 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/JdbcDaoSupportTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.support; + +import java.util.ArrayList; +import java.util.List; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; + +import org.springframework.jdbc.core.JdbcTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * @author Juergen Hoeller + * @since 30.07.2003 + */ +public class JdbcDaoSupportTests { + + @Test + public void testJdbcDaoSupportWithDataSource() throws Exception { + DataSource ds = mock(DataSource.class); + final List test = new ArrayList<>(); + JdbcDaoSupport dao = new JdbcDaoSupport() { + @Override + protected void initDao() { + test.add("test"); + } + }; + dao.setDataSource(ds); + dao.afterPropertiesSet(); + assertThat(dao.getDataSource()).as("Correct DataSource").isEqualTo(ds); + assertThat(dao.getJdbcTemplate().getDataSource()).as("Correct JdbcTemplate").isEqualTo(ds); + assertThat(test.size()).as("initDao called").isEqualTo(1); + } + + @Test + public void testJdbcDaoSupportWithJdbcTemplate() throws Exception { + JdbcTemplate template = new JdbcTemplate(); + final List test = new ArrayList<>(); + JdbcDaoSupport dao = new JdbcDaoSupport() { + @Override + protected void initDao() { + test.add("test"); + } + }; + dao.setJdbcTemplate(template); + dao.afterPropertiesSet(); + assertThat(template).as("Correct JdbcTemplate").isEqualTo(dao.getJdbcTemplate()); + assertThat(test.size()).as("initDao called").isEqualTo(1); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/LobSupportTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/LobSupportTests.java new file mode 100644 index 0000000..d0ba6ed --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/LobSupportTests.java @@ -0,0 +1,128 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.support; + +import java.io.IOException; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.junit.jupiter.api.Test; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.jdbc.LobRetrievalFailureException; +import org.springframework.jdbc.support.lob.LobCreator; +import org.springframework.jdbc.support.lob.LobHandler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author Alef Arendsen + */ +public class LobSupportTests { + + @Test + public void testCreatingPreparedStatementCallback() throws SQLException { + LobHandler handler = mock(LobHandler.class); + LobCreator creator = mock(LobCreator.class); + PreparedStatement ps = mock(PreparedStatement.class); + + given(handler.getLobCreator()).willReturn(creator); + given(ps.executeUpdate()).willReturn(3); + + class SetValuesCalled { + boolean b = false; + } + + final SetValuesCalled svc = new SetValuesCalled(); + + AbstractLobCreatingPreparedStatementCallback psc = new AbstractLobCreatingPreparedStatementCallback( + handler) { + @Override + protected void setValues(PreparedStatement ps, LobCreator lobCreator) + throws SQLException, DataAccessException { + svc.b = true; + } + }; + + assertThat(psc.doInPreparedStatement(ps)).isEqualTo(Integer.valueOf(3)); + assertThat(svc.b).isTrue(); + verify(creator).close(); + verify(handler).getLobCreator(); + verify(ps).executeUpdate(); + } + + @Test + public void testAbstractLobStreamingResultSetExtractorNoRows() throws SQLException { + ResultSet rset = mock(ResultSet.class); + AbstractLobStreamingResultSetExtractor lobRse = getResultSetExtractor(false); + assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class).isThrownBy(() -> + lobRse.extractData(rset)); + verify(rset).next(); + } + + @Test + public void testAbstractLobStreamingResultSetExtractorOneRow() throws SQLException { + ResultSet rset = mock(ResultSet.class); + given(rset.next()).willReturn(true, false); + AbstractLobStreamingResultSetExtractor lobRse = getResultSetExtractor(false); + lobRse.extractData(rset); + verify(rset).clearWarnings(); + } + + @Test + public void testAbstractLobStreamingResultSetExtractorMultipleRows() + throws SQLException { + ResultSet rset = mock(ResultSet.class); + given(rset.next()).willReturn(true, true, false); + AbstractLobStreamingResultSetExtractor lobRse = getResultSetExtractor(false); + assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class).isThrownBy(() -> + lobRse.extractData(rset)); + verify(rset).clearWarnings(); + } + + @Test + public void testAbstractLobStreamingResultSetExtractorCorrectException() + throws SQLException { + ResultSet rset = mock(ResultSet.class); + given(rset.next()).willReturn(true); + AbstractLobStreamingResultSetExtractor lobRse = getResultSetExtractor(true); + assertThatExceptionOfType(LobRetrievalFailureException.class).isThrownBy(() -> + lobRse.extractData(rset)); + } + + private AbstractLobStreamingResultSetExtractor getResultSetExtractor(final boolean ex) { + AbstractLobStreamingResultSetExtractor lobRse = new AbstractLobStreamingResultSetExtractor() { + + @Override + protected void streamData(ResultSet rs) throws SQLException, IOException { + if (ex) { + throw new IOException(); + } + else { + rs.clearWarnings(); + } + } + }; + return lobRse; + } +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/SqlLobValueTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/SqlLobValueTests.java new file mode 100644 index 0000000..87d45d0 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/support/SqlLobValueTests.java @@ -0,0 +1,191 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.support; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import org.springframework.jdbc.support.lob.LobCreator; +import org.springframework.jdbc.support.lob.LobHandler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +/** + * Test cases for the SQL LOB value: + * + * BLOB: + * 1. Types.BLOB: setBlobAsBytes (byte[]) + * 2. String: setBlobAsBytes (byte[]) + * 3. else: IllegalArgumentException + * + * CLOB: + * 4. String or NULL: setClobAsString (String) + * 5. InputStream: setClobAsAsciiStream (InputStream) + * 6. Reader: setClobAsCharacterStream (Reader) + * 7. else: IllegalArgumentException + * + * @author Alef Arendsen + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class SqlLobValueTests { + + @Mock + private PreparedStatement preparedStatement; + + @Mock + private LobHandler handler; + + @Mock + private LobCreator creator; + + @Captor + private ArgumentCaptor inputStreamCaptor; + + @BeforeEach + void setUp() { + given(handler.getLobCreator()).willReturn(creator); + } + + @Test + void test1() throws SQLException { + byte[] testBytes = "Bla".getBytes(); + SqlLobValue lob = new SqlLobValue(testBytes, handler); + lob.setTypeValue(preparedStatement, 1, Types.BLOB, "test"); + verify(creator).setBlobAsBytes(preparedStatement, 1, testBytes); + } + + @Test + void test2() throws SQLException { + String testString = "Bla"; + SqlLobValue lob = new SqlLobValue(testString, handler); + lob.setTypeValue(preparedStatement, 1, Types.BLOB, "test"); + verify(creator).setBlobAsBytes(preparedStatement, 1, testString.getBytes()); + } + + @Test + void test3() throws SQLException { + SqlLobValue lob = new SqlLobValue(new InputStreamReader(new ByteArrayInputStream("Bla".getBytes())), 12); + assertThatIllegalArgumentException().isThrownBy(() -> + lob.setTypeValue(preparedStatement, 1, Types.BLOB, "test")); + } + + @Test + void test4() throws SQLException { + String testContent = "Bla"; + SqlLobValue lob = new SqlLobValue(testContent, handler); + lob.setTypeValue(preparedStatement, 1, Types.CLOB, "test"); + verify(creator).setClobAsString(preparedStatement, 1, testContent); + } + + @Test + void test5() throws Exception { + byte[] testContent = "Bla".getBytes(); + SqlLobValue lob = new SqlLobValue(new ByteArrayInputStream(testContent), 3, handler); + lob.setTypeValue(preparedStatement, 1, Types.CLOB, "test"); + verify(creator).setClobAsAsciiStream(eq(preparedStatement), eq(1), inputStreamCaptor.capture(), eq(3)); + byte[] bytes = new byte[3]; + inputStreamCaptor.getValue().read(bytes); + assertThat(bytes).isEqualTo(testContent); + } + + @Test + void test6() throws SQLException { + byte[] testContent = "Bla".getBytes(); + ByteArrayInputStream bais = new ByteArrayInputStream(testContent); + InputStreamReader reader = new InputStreamReader(bais); + SqlLobValue lob = new SqlLobValue(reader, 3, handler); + lob.setTypeValue(preparedStatement, 1, Types.CLOB, "test"); + verify(creator).setClobAsCharacterStream(eq(preparedStatement), eq(1), eq(reader), eq(3)); + } + + @Test + void test7() throws SQLException { + SqlLobValue lob = new SqlLobValue("bla".getBytes()); + assertThatIllegalArgumentException().isThrownBy(() -> + lob.setTypeValue(preparedStatement, 1, Types.CLOB, "test")); + } + + @Test + void testOtherConstructors() throws SQLException { + // a bit BS, but we need to test them, as long as they don't throw exceptions + + SqlLobValue lob = new SqlLobValue("bla"); + lob.setTypeValue(preparedStatement, 1, Types.CLOB, "test"); + + SqlLobValue lob2 = new SqlLobValue("bla".getBytes()); + assertThatIllegalArgumentException().isThrownBy(() -> + lob2.setTypeValue(preparedStatement, 1, Types.CLOB, "test")); + + lob = new SqlLobValue(new ByteArrayInputStream("bla".getBytes()), 3); + lob.setTypeValue(preparedStatement, 1, Types.CLOB, "test"); + + lob = new SqlLobValue(new InputStreamReader(new ByteArrayInputStream( + "bla".getBytes())), 3); + lob.setTypeValue(preparedStatement, 1, Types.CLOB, "test"); + + // same for BLOB + lob = new SqlLobValue("bla"); + lob.setTypeValue(preparedStatement, 1, Types.BLOB, "test"); + + lob = new SqlLobValue("bla".getBytes()); + lob.setTypeValue(preparedStatement, 1, Types.BLOB, "test"); + + lob = new SqlLobValue(new ByteArrayInputStream("bla".getBytes()), 3); + lob.setTypeValue(preparedStatement, 1, Types.BLOB, "test"); + + SqlLobValue lob3 = new SqlLobValue(new InputStreamReader(new ByteArrayInputStream( + "bla".getBytes())), 3); + assertThatIllegalArgumentException().isThrownBy(() -> + lob3.setTypeValue(preparedStatement, 1, Types.BLOB, "test")); + } + + @Test + void testCorrectCleanup() throws SQLException { + SqlLobValue lob = new SqlLobValue("Bla", handler); + lob.setTypeValue(preparedStatement, 1, Types.CLOB, "test"); + lob.cleanup(); + verify(creator).setClobAsString(preparedStatement, 1, "Bla"); + verify(creator).close(); + } + + @Test + void testOtherSqlType() throws SQLException { + SqlLobValue lob = new SqlLobValue("Bla", handler); + assertThatIllegalArgumentException().isThrownBy(() -> + lob.setTypeValue(preparedStatement, 1, Types.SMALLINT, "test")); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/AbstractPerson.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/AbstractPerson.java new file mode 100644 index 0000000..f2698d3 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/AbstractPerson.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.test; + +import java.util.Date; + +/** + * @author Thomas Risberg + */ +public abstract class AbstractPerson { + + private String name; + + private long age; + + private Date birth_date; + + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public long getAge() { + return age; + } + + public void setAge(long age) { + this.age = age; + } + + public Date getBirth_date() { + return birth_date; + } + + public void setBirth_date(Date birth_date) { + this.birth_date = birth_date; + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConcretePerson.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConcretePerson.java new file mode 100644 index 0000000..69eb185 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConcretePerson.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.test; + +import java.math.BigDecimal; + +/** + * @author Thomas Risberg + */ +public class ConcretePerson extends AbstractPerson { + + private BigDecimal balance; + + + public BigDecimal getBalance() { + return balance; + } + + public void setBalance(BigDecimal balance) { + this.balance = balance; + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java new file mode 100644 index 0000000..0e15987 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ConstructorPerson.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.test; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * @author Juergen Hoeller + */ +public class ConstructorPerson { + + private String name; + + private long age; + + private java.util.Date birth_date; + + private BigDecimal balance; + + + public ConstructorPerson(String name, long age, Date birth_date, BigDecimal balance) { + this.name = name; + this.age = age; + this.birth_date = birth_date; + this.balance = balance; + } + + + public String name() { + return name; + } + + public long age() { + return age; + } + + public Date birth_date() { + return birth_date; + } + + public BigDecimal balance() { + return balance; + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/DatePerson.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/DatePerson.java new file mode 100644 index 0000000..7dc9e81 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/DatePerson.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.test; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * @author Juergen Hoeller + */ +public class DatePerson { + + private String lastName; + + private long age; + + private LocalDate birthDate; + + private BigDecimal balance; + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public long getAge() { + return age; + } + + public void setAge(long age) { + this.age = age; + } + + public LocalDate getBirthDate() { + return birthDate; + } + + public void setBirthDate(LocalDate birthDate) { + this.birthDate = birthDate; + } + + public BigDecimal getBalance() { + return balance; + } + + public void setBalance(BigDecimal balance) { + this.balance = balance; + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ExtendedPerson.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ExtendedPerson.java new file mode 100644 index 0000000..6e22dfc --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/ExtendedPerson.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.test; + +/** + * @author Juergen Hoeller + */ +public class ExtendedPerson extends ConcretePerson { + + private Object someField; + + + public Object getSomeField() { + return someField; + } + + public void setSomeField(Object someField) { + this.someField = someField; + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/Person.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/Person.java new file mode 100644 index 0000000..e1d9149 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/Person.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.test; + +import java.math.BigDecimal; + +/** + * @author Thomas Risberg + */ +public class Person { + + private String name; + + private long age; + + private java.util.Date birth_date; + + private BigDecimal balance; + + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public long getAge() { + return age; + } + + public void setAge(long age) { + this.age = age; + } + + public java.util.Date getBirth_date() { + return birth_date; + } + + public void setBirth_date(java.util.Date birth_date) { + this.birth_date = birth_date; + } + + public BigDecimal getBalance() { + return balance; + } + + public void setBalance(BigDecimal balance) { + this.balance = balance; + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/SpacePerson.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/SpacePerson.java new file mode 100644 index 0000000..8dc8875 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/test/SpacePerson.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2016 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.test; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * @author Thomas Risberg + */ +public class SpacePerson { + + private String lastName; + + private long age; + + private LocalDateTime birthDate; + + private BigDecimal balance; + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public long getAge() { + return age; + } + + public void setAge(long age) { + this.age = age; + } + + public LocalDateTime getBirthDate() { + return birthDate; + } + + public void setBirthDate(LocalDateTime birthDate) { + this.birthDate = birthDate; + } + + public BigDecimal getBalance() { + return balance; + } + + public void setBalance(BigDecimal balanace) { + this.balance = balanace; + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceJtaTransactionTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceJtaTransactionTests.java new file mode 100644 index 0000000..13e87b6 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceJtaTransactionTests.java @@ -0,0 +1,746 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.sql.DataSource; +import javax.transaction.RollbackException; +import javax.transaction.Status; +import javax.transaction.SystemException; +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; +import javax.transaction.UserTransaction; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.StaticListableBeanFactory; +import org.springframework.jdbc.datasource.lookup.BeanFactoryDataSourceLookup; +import org.springframework.jdbc.datasource.lookup.IsolationLevelDataSourceRouter; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.jta.JtaTransactionManager; +import org.springframework.transaction.jta.JtaTransactionObject; +import org.springframework.transaction.support.TransactionCallbackWithoutResult; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.transaction.support.TransactionTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author Juergen Hoeller + * @since 17.10.2005 + */ +public class DataSourceJtaTransactionTests { + + private Connection connection; + private DataSource dataSource; + private UserTransaction userTransaction; + private TransactionManager transactionManager; + private Transaction transaction; + + @BeforeEach + public void setup() throws Exception { + connection =mock(Connection.class); + dataSource = mock(DataSource.class); + userTransaction = mock(UserTransaction.class); + transactionManager = mock(TransactionManager.class); + transaction = mock(Transaction.class); + given(dataSource.getConnection()).willReturn(connection); + } + + @AfterEach + public void verifyTransactionSynchronizationManagerState() { + assertThat(TransactionSynchronizationManager.getResourceMap().isEmpty()).isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); + assertThat(TransactionSynchronizationManager.getCurrentTransactionName()).isNull(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.getCurrentTransactionIsolationLevel()).isNull(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isFalse(); + } + + @Test + public void testJtaTransactionCommit() throws Exception { + doTestJtaTransaction(false); + } + + @Test + public void testJtaTransactionRollback() throws Exception { + doTestJtaTransaction(true); + } + + private void doTestJtaTransaction(final boolean rollback) throws Exception { + if (rollback) { + given(userTransaction.getStatus()).willReturn( + Status.STATUS_NO_TRANSACTION,Status.STATUS_ACTIVE); + } + else { + given(userTransaction.getStatus()).willReturn( + Status.STATUS_NO_TRANSACTION, Status.STATUS_ACTIVE, Status.STATUS_ACTIVE); + } + + JtaTransactionManager ptm = new JtaTransactionManager(userTransaction); + TransactionTemplate tt = new TransactionTemplate(ptm); + boolean condition3 = !TransactionSynchronizationManager.hasResource(dataSource); + assertThat(condition3).as("Hasn't thread connection").isTrue(); + boolean condition2 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition2).as("JTA synchronizations not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + boolean condition = !TransactionSynchronizationManager.hasResource(dataSource); + assertThat(condition).as("Hasn't thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("JTA synchronizations active").isTrue(); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + + Connection c = DataSourceUtils.getConnection(dataSource); + assertThat(TransactionSynchronizationManager.hasResource(dataSource)).as("Has thread connection").isTrue(); + DataSourceUtils.releaseConnection(c, dataSource); + + c = DataSourceUtils.getConnection(dataSource); + assertThat(TransactionSynchronizationManager.hasResource(dataSource)).as("Has thread connection").isTrue(); + DataSourceUtils.releaseConnection(c, dataSource); + + if (rollback) { + status.setRollbackOnly(); + } + } + }); + + boolean condition1 = !TransactionSynchronizationManager.hasResource(dataSource); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + boolean condition = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition).as("JTA synchronizations not active").isTrue(); + verify(userTransaction).begin(); + if (rollback) { + verify(userTransaction).rollback(); + } + verify(connection).close(); + } + + @Test + public void testJtaTransactionCommitWithPropagationRequiresNew() throws Exception { + doTestJtaTransactionWithPropagationRequiresNew(false, false, false, false); + } + + @Test + public void testJtaTransactionCommitWithPropagationRequiresNewWithAccessAfterResume() throws Exception { + doTestJtaTransactionWithPropagationRequiresNew(false, false, true, false); + } + + @Test + public void testJtaTransactionCommitWithPropagationRequiresNewWithOpenOuterConnection() throws Exception { + doTestJtaTransactionWithPropagationRequiresNew(false, true, false, false); + } + + @Test + public void testJtaTransactionCommitWithPropagationRequiresNewWithOpenOuterConnectionAccessed() throws Exception { + doTestJtaTransactionWithPropagationRequiresNew(false, true, true, false); + } + + @Test + public void testJtaTransactionCommitWithPropagationRequiresNewWithTransactionAwareDataSource() throws Exception { + doTestJtaTransactionWithPropagationRequiresNew(false, false, true, true); + } + + @Test + public void testJtaTransactionRollbackWithPropagationRequiresNew() throws Exception { + doTestJtaTransactionWithPropagationRequiresNew(true, false, false, false); + } + + @Test + public void testJtaTransactionRollbackWithPropagationRequiresNewWithAccessAfterResume() throws Exception { + doTestJtaTransactionWithPropagationRequiresNew(true, false, true, false); + } + + @Test + public void testJtaTransactionRollbackWithPropagationRequiresNewWithOpenOuterConnection() throws Exception { + doTestJtaTransactionWithPropagationRequiresNew(true, true, false, false); + } + + @Test + public void testJtaTransactionRollbackWithPropagationRequiresNewWithOpenOuterConnectionAccessed() throws Exception { + doTestJtaTransactionWithPropagationRequiresNew(true, true, true, false); + } + + @Test + public void testJtaTransactionRollbackWithPropagationRequiresNewWithTransactionAwareDataSource() throws Exception { + doTestJtaTransactionWithPropagationRequiresNew(true, false, true, true); + } + + private void doTestJtaTransactionWithPropagationRequiresNew( + final boolean rollback, final boolean openOuterConnection, final boolean accessAfterResume, + final boolean useTransactionAwareDataSource) throws Exception { + + given(transactionManager.suspend()).willReturn(transaction); + if (rollback) { + given(userTransaction.getStatus()).willReturn(Status.STATUS_NO_TRANSACTION, + Status.STATUS_ACTIVE); + } + else { + given(userTransaction.getStatus()).willReturn(Status.STATUS_NO_TRANSACTION, + Status.STATUS_ACTIVE, Status.STATUS_ACTIVE); + } + + given(connection.isReadOnly()).willReturn(true); + + final DataSource dsToUse = useTransactionAwareDataSource ? + new TransactionAwareDataSourceProxy(dataSource) : dataSource; + + JtaTransactionManager ptm = new JtaTransactionManager(userTransaction, transactionManager); + final TransactionTemplate tt = new TransactionTemplate(ptm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + boolean condition3 = !TransactionSynchronizationManager.hasResource(dsToUse); + assertThat(condition3).as("Hasn't thread connection").isTrue(); + boolean condition2 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition2).as("JTA synchronizations not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + boolean condition = !TransactionSynchronizationManager.hasResource(dsToUse); + assertThat(condition).as("Hasn't thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("JTA synchronizations active").isTrue(); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + + Connection c = DataSourceUtils.getConnection(dsToUse); + try { + assertThat(TransactionSynchronizationManager.hasResource(dsToUse)).as("Has thread connection").isTrue(); + c.isReadOnly(); + DataSourceUtils.releaseConnection(c, dsToUse); + + c = DataSourceUtils.getConnection(dsToUse); + assertThat(TransactionSynchronizationManager.hasResource(dsToUse)).as("Has thread connection").isTrue(); + if (!openOuterConnection) { + DataSourceUtils.releaseConnection(c, dsToUse); + } + } + catch (SQLException ex) { + } + + for (int i = 0; i < 5; i++) { + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + boolean condition = !TransactionSynchronizationManager.hasResource(dsToUse); + assertThat(condition).as("Hasn't thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("JTA synchronizations active").isTrue(); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + + try { + Connection c = DataSourceUtils.getConnection(dsToUse); + c.isReadOnly(); + assertThat(TransactionSynchronizationManager.hasResource(dsToUse)).as("Has thread connection").isTrue(); + DataSourceUtils.releaseConnection(c, dsToUse); + + c = DataSourceUtils.getConnection(dsToUse); + assertThat(TransactionSynchronizationManager.hasResource(dsToUse)).as("Has thread connection").isTrue(); + DataSourceUtils.releaseConnection(c, dsToUse); + } + catch (SQLException ex) { + } + } + }); + + } + + if (rollback) { + status.setRollbackOnly(); + } + + if (accessAfterResume) { + try { + if (!openOuterConnection) { + c = DataSourceUtils.getConnection(dsToUse); + } + assertThat(TransactionSynchronizationManager.hasResource(dsToUse)).as("Has thread connection").isTrue(); + c.isReadOnly(); + DataSourceUtils.releaseConnection(c, dsToUse); + + c = DataSourceUtils.getConnection(dsToUse); + assertThat(TransactionSynchronizationManager.hasResource(dsToUse)).as("Has thread connection").isTrue(); + DataSourceUtils.releaseConnection(c, dsToUse); + } + catch (SQLException ex) { + } + } + + else { + if (openOuterConnection) { + DataSourceUtils.releaseConnection(c, dsToUse); + } + } + } + }); + + boolean condition1 = !TransactionSynchronizationManager.hasResource(dsToUse); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + boolean condition = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition).as("JTA synchronizations not active").isTrue(); + verify(userTransaction, times(6)).begin(); + verify(transactionManager, times(5)).resume(transaction); + if (rollback) { + verify(userTransaction, times(5)).commit(); + verify(userTransaction).rollback(); + } + else { + verify(userTransaction, times(6)).commit(); + } + if (accessAfterResume && !openOuterConnection) { + verify(connection, times(7)).close(); + } + else { + verify(connection, times(6)).close(); + } + } + + @Test + public void testJtaTransactionCommitWithPropagationRequiredWithinSupports() throws Exception { + doTestJtaTransactionCommitWithNewTransactionWithinEmptyTransaction(false, false); + } + + @Test + public void testJtaTransactionCommitWithPropagationRequiredWithinNotSupported() throws Exception { + doTestJtaTransactionCommitWithNewTransactionWithinEmptyTransaction(false, true); + } + + @Test + public void testJtaTransactionCommitWithPropagationRequiresNewWithinSupports() throws Exception { + doTestJtaTransactionCommitWithNewTransactionWithinEmptyTransaction(true, false); + } + + @Test + public void testJtaTransactionCommitWithPropagationRequiresNewWithinNotSupported() throws Exception { + doTestJtaTransactionCommitWithNewTransactionWithinEmptyTransaction(true, true); + } + + private void doTestJtaTransactionCommitWithNewTransactionWithinEmptyTransaction( + final boolean requiresNew, boolean notSupported) throws Exception { + + if (notSupported) { + given(userTransaction.getStatus()).willReturn( + Status.STATUS_ACTIVE, + Status.STATUS_NO_TRANSACTION, + Status.STATUS_ACTIVE, + Status.STATUS_ACTIVE); + given(transactionManager.suspend()).willReturn(transaction); + } + else { + given(userTransaction.getStatus()).willReturn( + Status.STATUS_NO_TRANSACTION, + Status.STATUS_NO_TRANSACTION, + Status.STATUS_ACTIVE, + Status.STATUS_ACTIVE); + } + + final DataSource dataSource = mock(DataSource.class); + final Connection connection1 = mock(Connection.class); + final Connection connection2 = mock(Connection.class); + given(dataSource.getConnection()).willReturn(connection1, connection2); + + final JtaTransactionManager ptm = new JtaTransactionManager(userTransaction, transactionManager); + TransactionTemplate tt = new TransactionTemplate(ptm); + tt.setPropagationBehavior(notSupported ? + TransactionDefinition.PROPAGATION_NOT_SUPPORTED : TransactionDefinition.PROPAGATION_SUPPORTS); + + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isFalse(); + assertThat(DataSourceUtils.getConnection(dataSource)).isSameAs(connection1); + assertThat(DataSourceUtils.getConnection(dataSource)).isSameAs(connection1); + + TransactionTemplate tt2 = new TransactionTemplate(ptm); + tt2.setPropagationBehavior(requiresNew ? + TransactionDefinition.PROPAGATION_REQUIRES_NEW : TransactionDefinition.PROPAGATION_REQUIRED); + tt2.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + assertThat(DataSourceUtils.getConnection(dataSource)).isSameAs(connection2); + assertThat(DataSourceUtils.getConnection(dataSource)).isSameAs(connection2); + } + }); + + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isFalse(); + assertThat(DataSourceUtils.getConnection(dataSource)).isSameAs(connection1); + } + }); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); + verify(userTransaction).begin(); + verify(userTransaction).commit(); + if (notSupported) { + verify(transactionManager).resume(transaction); + } + verify(connection2).close(); + verify(connection1).close(); + } + + @Test + public void testJtaTransactionCommitWithPropagationRequiresNewAndSuspendException() throws Exception { + doTestJtaTransactionWithPropagationRequiresNewAndBeginException(true, false, false); + } + + @Test + public void testJtaTransactionCommitWithPropagationRequiresNewWithOpenOuterConnectionAndSuspendException() throws Exception { + doTestJtaTransactionWithPropagationRequiresNewAndBeginException(true, true, false); + } + + @Test + public void testJtaTransactionCommitWithPropagationRequiresNewWithTransactionAwareDataSourceAndSuspendException() throws Exception { + doTestJtaTransactionWithPropagationRequiresNewAndBeginException(true, false, true); + } + + @Test + public void testJtaTransactionCommitWithPropagationRequiresNewWithOpenOuterConnectionAndTransactionAwareDataSourceAndSuspendException() throws Exception { + doTestJtaTransactionWithPropagationRequiresNewAndBeginException(true, true, true); + } + + @Test + public void testJtaTransactionCommitWithPropagationRequiresNewAndBeginException() throws Exception { + doTestJtaTransactionWithPropagationRequiresNewAndBeginException(false, false, false); + } + + @Test + public void testJtaTransactionCommitWithPropagationRequiresNewWithOpenOuterConnectionAndBeginException() throws Exception { + doTestJtaTransactionWithPropagationRequiresNewAndBeginException(false, true, false); + } + + @Test + public void testJtaTransactionCommitWithPropagationRequiresNewWithOpenOuterConnectionAndTransactionAwareDataSourceAndBeginException() throws Exception { + doTestJtaTransactionWithPropagationRequiresNewAndBeginException(false, true, true); + } + + @Test + public void testJtaTransactionCommitWithPropagationRequiresNewWithTransactionAwareDataSourceAndBeginException() throws Exception { + doTestJtaTransactionWithPropagationRequiresNewAndBeginException(false, false, true); + } + + private void doTestJtaTransactionWithPropagationRequiresNewAndBeginException(boolean suspendException, + final boolean openOuterConnection, final boolean useTransactionAwareDataSource) throws Exception { + + given(userTransaction.getStatus()).willReturn( + Status.STATUS_NO_TRANSACTION, + Status.STATUS_ACTIVE, + Status.STATUS_ACTIVE); + if (suspendException) { + given(transactionManager.suspend()).willThrow(new SystemException()); + } + else { + given(transactionManager.suspend()).willReturn(transaction); + willThrow(new SystemException()).given(userTransaction).begin(); + } + + given(connection.isReadOnly()).willReturn(true); + + final DataSource dsToUse = useTransactionAwareDataSource ? + new TransactionAwareDataSourceProxy(dataSource) : dataSource; + if (dsToUse instanceof TransactionAwareDataSourceProxy) { + ((TransactionAwareDataSourceProxy) dsToUse).setReobtainTransactionalConnections(true); + } + + JtaTransactionManager ptm = new JtaTransactionManager(userTransaction, transactionManager); + final TransactionTemplate tt = new TransactionTemplate(ptm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + boolean condition3 = !TransactionSynchronizationManager.hasResource(dsToUse); + assertThat(condition3).as("Hasn't thread connection").isTrue(); + boolean condition2 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition2).as("JTA synchronizations not active").isTrue(); + + assertThatExceptionOfType(TransactionException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + boolean condition = !TransactionSynchronizationManager.hasResource(dsToUse); + assertThat(condition).as("Hasn't thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("JTA synchronizations active").isTrue(); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + + Connection c = DataSourceUtils.getConnection(dsToUse); + try { + assertThat(TransactionSynchronizationManager.hasResource(dsToUse)).as("Has thread connection").isTrue(); + c.isReadOnly(); + DataSourceUtils.releaseConnection(c, dsToUse); + + c = DataSourceUtils.getConnection(dsToUse); + assertThat(TransactionSynchronizationManager.hasResource(dsToUse)).as("Has thread connection").isTrue(); + if (!openOuterConnection) { + DataSourceUtils.releaseConnection(c, dsToUse); + } + } + catch (SQLException ex) { + } + + try { + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + boolean condition = !TransactionSynchronizationManager.hasResource(dsToUse); + assertThat(condition).as("Hasn't thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("JTA synchronizations active").isTrue(); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + + Connection c = DataSourceUtils.getConnection(dsToUse); + assertThat(TransactionSynchronizationManager.hasResource(dsToUse)).as("Has thread connection").isTrue(); + DataSourceUtils.releaseConnection(c, dsToUse); + + c = DataSourceUtils.getConnection(dsToUse); + assertThat(TransactionSynchronizationManager.hasResource(dsToUse)).as("Has thread connection").isTrue(); + DataSourceUtils.releaseConnection(c, dsToUse); + } + }); + } + finally { + if (openOuterConnection) { + try { + c.isReadOnly(); + DataSourceUtils.releaseConnection(c, dsToUse); + } + catch (SQLException ex) { + } + } + } + } + })); + + boolean condition1 = !TransactionSynchronizationManager.hasResource(dsToUse); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + boolean condition = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition).as("JTA synchronizations not active").isTrue(); + + verify(userTransaction).begin(); + if (suspendException) { + verify(userTransaction).rollback(); + } + + if (suspendException) { + verify(connection, atLeastOnce()).close(); + } + else { + verify(connection, never()).close(); + } + } + + @Test + public void testJtaTransactionWithConnectionHolderStillBound() throws Exception { + @SuppressWarnings("serial") + JtaTransactionManager ptm = new JtaTransactionManager(userTransaction) { + + @Override + protected void doRegisterAfterCompletionWithJtaTransaction( + JtaTransactionObject txObject, + final List synchronizations) + throws RollbackException, SystemException { + Thread async = new Thread() { + @Override + public void run() { + invokeAfterCompletion(synchronizations, TransactionSynchronization.STATUS_COMMITTED); + } + }; + async.start(); + try { + async.join(); + } + catch (InterruptedException ex) { + ex.printStackTrace(); + } + } + }; + TransactionTemplate tt = new TransactionTemplate(ptm); + boolean condition2 = !TransactionSynchronizationManager.hasResource(dataSource); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("JTA synchronizations not active").isTrue(); + + given(userTransaction.getStatus()).willReturn(Status.STATUS_ACTIVE); + for (int i = 0; i < 3; i++) { + final boolean releaseCon = (i != 1); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("JTA synchronizations active").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Is existing transaction").isTrue(); + + Connection c = DataSourceUtils.getConnection(dataSource); + assertThat(TransactionSynchronizationManager.hasResource(dataSource)).as("Has thread connection").isTrue(); + DataSourceUtils.releaseConnection(c, dataSource); + + c = DataSourceUtils.getConnection(dataSource); + assertThat(TransactionSynchronizationManager.hasResource(dataSource)).as("Has thread connection").isTrue(); + if (releaseCon) { + DataSourceUtils.releaseConnection(c, dataSource); + } + } + }); + + if (!releaseCon) { + assertThat(TransactionSynchronizationManager.hasResource(dataSource)).as("Still has connection holder").isTrue(); + } + else { + boolean condition = !TransactionSynchronizationManager.hasResource(dataSource); + assertThat(condition).as("Hasn't thread connection").isTrue(); + } + boolean condition = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition).as("JTA synchronizations not active").isTrue(); + } + verify(connection, times(3)).close(); + } + + @Test + public void testJtaTransactionWithIsolationLevelDataSourceAdapter() throws Exception { + given(userTransaction.getStatus()).willReturn( + Status.STATUS_NO_TRANSACTION, + Status.STATUS_ACTIVE, + Status.STATUS_ACTIVE, + Status.STATUS_NO_TRANSACTION, + Status.STATUS_ACTIVE, + Status.STATUS_ACTIVE); + + final IsolationLevelDataSourceAdapter dsToUse = new IsolationLevelDataSourceAdapter(); + dsToUse.setTargetDataSource(dataSource); + dsToUse.afterPropertiesSet(); + + JtaTransactionManager ptm = new JtaTransactionManager(userTransaction); + ptm.setAllowCustomIsolationLevels(true); + + TransactionTemplate tt = new TransactionTemplate(ptm); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + Connection c = DataSourceUtils.getConnection(dsToUse); + assertThat(TransactionSynchronizationManager.hasResource(dsToUse)).as("Has thread connection").isTrue(); + assertThat(c).isSameAs(connection); + DataSourceUtils.releaseConnection(c, dsToUse); + } + }); + + tt.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ); + tt.setReadOnly(true); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + Connection c = DataSourceUtils.getConnection(dsToUse); + assertThat(TransactionSynchronizationManager.hasResource(dsToUse)).as("Has thread connection").isTrue(); + assertThat(c).isSameAs(connection); + DataSourceUtils.releaseConnection(c, dsToUse); + } + }); + + verify(userTransaction, times(2)).begin(); + verify(userTransaction, times(2)).commit(); + verify(connection).setReadOnly(true); + verify(connection).setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ); + verify(connection, times(2)).close(); + } + + @Test + public void testJtaTransactionWithIsolationLevelDataSourceRouter() throws Exception { + doTestJtaTransactionWithIsolationLevelDataSourceRouter(false); + } + + @Test + public void testJtaTransactionWithIsolationLevelDataSourceRouterWithDataSourceLookup() throws Exception { + doTestJtaTransactionWithIsolationLevelDataSourceRouter(true); + } + + private void doTestJtaTransactionWithIsolationLevelDataSourceRouter(boolean dataSourceLookup) throws Exception { +given( userTransaction.getStatus()).willReturn(Status.STATUS_NO_TRANSACTION, Status.STATUS_ACTIVE, Status.STATUS_ACTIVE, Status.STATUS_NO_TRANSACTION, Status.STATUS_ACTIVE, Status.STATUS_ACTIVE); + + final DataSource dataSource1 = mock(DataSource.class); + final Connection connection1 = mock(Connection.class); + given(dataSource1.getConnection()).willReturn(connection1); + + final DataSource dataSource2 = mock(DataSource.class); + final Connection connection2 = mock(Connection.class); + given(dataSource2.getConnection()).willReturn(connection2); + + final IsolationLevelDataSourceRouter dsToUse = new IsolationLevelDataSourceRouter(); + Map targetDataSources = new HashMap<>(); + if (dataSourceLookup) { + targetDataSources.put("ISOLATION_REPEATABLE_READ", "ds2"); + dsToUse.setDefaultTargetDataSource("ds1"); + StaticListableBeanFactory beanFactory = new StaticListableBeanFactory(); + beanFactory.addBean("ds1", dataSource1); + beanFactory.addBean("ds2", dataSource2); + dsToUse.setDataSourceLookup(new BeanFactoryDataSourceLookup(beanFactory)); + } + else { + targetDataSources.put("ISOLATION_REPEATABLE_READ", dataSource2); + dsToUse.setDefaultTargetDataSource(dataSource1); + } + dsToUse.setTargetDataSources(targetDataSources); + dsToUse.afterPropertiesSet(); + + JtaTransactionManager ptm = new JtaTransactionManager(userTransaction); + ptm.setAllowCustomIsolationLevels(true); + + TransactionTemplate tt = new TransactionTemplate(ptm); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + Connection c = DataSourceUtils.getConnection(dsToUse); + assertThat(TransactionSynchronizationManager.hasResource(dsToUse)).as("Has thread connection").isTrue(); + assertThat(c).isSameAs(connection1); + DataSourceUtils.releaseConnection(c, dsToUse); + } + }); + + tt.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + Connection c = DataSourceUtils.getConnection(dsToUse); + assertThat(TransactionSynchronizationManager.hasResource(dsToUse)).as("Has thread connection").isTrue(); + assertThat(c).isSameAs(connection2); + DataSourceUtils.releaseConnection(c, dsToUse); + } + }); + + verify(userTransaction, times(2)).begin(); + verify(userTransaction, times(2)).commit(); + verify(connection1).close(); + verify(connection2).close(); + } +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java new file mode 100644 index 0000000..b4c12bf --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java @@ -0,0 +1,1746 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Savepoint; +import java.sql.Statement; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.InOrder; + +import org.springframework.core.testfixture.EnabledForTestGroups; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.jdbc.UncategorizedSQLException; +import org.springframework.transaction.CannotCreateTransactionException; +import org.springframework.transaction.IllegalTransactionStateException; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.TransactionSystemException; +import org.springframework.transaction.TransactionTimedOutException; +import org.springframework.transaction.UnexpectedRollbackException; +import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.springframework.transaction.support.TransactionCallbackWithoutResult; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.transaction.support.TransactionTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; + +/** + * @author Juergen Hoeller + * @since 04.07.2003 + * @see org.springframework.jdbc.support.JdbcTransactionManagerTests + */ +public class DataSourceTransactionManagerTests { + + private DataSource ds; + + private Connection con; + + private DataSourceTransactionManager tm; + + + @BeforeEach + public void setup() throws Exception { + ds = mock(DataSource.class); + con = mock(Connection.class); + given(ds.getConnection()).willReturn(con); + tm = new DataSourceTransactionManager(ds); + } + + @AfterEach + public void verifyTransactionSynchronizationManagerState() { + assertThat(TransactionSynchronizationManager.getResourceMap().isEmpty()).isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isFalse(); + } + + + @Test + public void testTransactionCommitWithAutoCommitTrue() throws Exception { + doTestTransactionCommitRestoringAutoCommit(true, false, false); + } + + @Test + public void testTransactionCommitWithAutoCommitFalse() throws Exception { + doTestTransactionCommitRestoringAutoCommit(false, false, false); + } + + @Test + public void testTransactionCommitWithAutoCommitTrueAndLazyConnection() throws Exception { + doTestTransactionCommitRestoringAutoCommit(true, true, false); + } + + @Test + public void testTransactionCommitWithAutoCommitFalseAndLazyConnection() throws Exception { + doTestTransactionCommitRestoringAutoCommit(false, true, false); + } + + @Test + public void testTransactionCommitWithAutoCommitTrueAndLazyConnectionAndStatementCreated() throws Exception { + doTestTransactionCommitRestoringAutoCommit(true, true, true); + } + + @Test + public void testTransactionCommitWithAutoCommitFalseAndLazyConnectionAndStatementCreated() throws Exception { + doTestTransactionCommitRestoringAutoCommit(false, true, true); + } + + private void doTestTransactionCommitRestoringAutoCommit( + boolean autoCommit, boolean lazyConnection, final boolean createStatement) throws Exception { + + if (lazyConnection) { + given(con.getAutoCommit()).willReturn(autoCommit); + given(con.getTransactionIsolation()).willReturn(Connection.TRANSACTION_READ_COMMITTED); + given(con.getWarnings()).willThrow(new SQLException()); + } + + if (!lazyConnection || createStatement) { + given(con.getAutoCommit()).willReturn(autoCommit); + } + + final DataSource dsToUse = (lazyConnection ? new LazyConnectionDataSourceProxy(ds) : ds); + tm = new DataSourceTransactionManager(dsToUse); + TransactionTemplate tt = new TransactionTemplate(tm); + boolean condition3 = !TransactionSynchronizationManager.hasResource(dsToUse); + assertThat(condition3).as("Hasn't thread connection").isTrue(); + boolean condition2 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition2).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(dsToUse)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + Connection tCon = DataSourceUtils.getConnection(dsToUse); + try { + if (createStatement) { + tCon.createStatement(); + } + else { + tCon.getWarnings(); + tCon.clearWarnings(); + } + } + catch (SQLException ex) { + throw new UncategorizedSQLException("", "", ex); + } + } + }); + + boolean condition1 = !TransactionSynchronizationManager.hasResource(dsToUse); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + boolean condition = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition).as("Synchronization not active").isTrue(); + + if (autoCommit && (!lazyConnection || createStatement)) { + InOrder ordered = inOrder(con); + ordered.verify(con).setAutoCommit(false); + ordered.verify(con).commit(); + ordered.verify(con).setAutoCommit(true); + } + if (createStatement) { + verify(con, times(2)).close(); + } + else { + verify(con).close(); + } + } + + @Test + public void testTransactionRollbackWithAutoCommitTrue() throws Exception { + doTestTransactionRollbackRestoringAutoCommit(true, false, false); + } + + @Test + public void testTransactionRollbackWithAutoCommitFalse() throws Exception { + doTestTransactionRollbackRestoringAutoCommit(false, false, false); + } + + @Test + public void testTransactionRollbackWithAutoCommitTrueAndLazyConnection() throws Exception { + doTestTransactionRollbackRestoringAutoCommit(true, true, false); + } + + @Test + public void testTransactionRollbackWithAutoCommitFalseAndLazyConnection() throws Exception { + doTestTransactionRollbackRestoringAutoCommit(false, true, false); + } + + @Test + public void testTransactionRollbackWithAutoCommitTrueAndLazyConnectionAndCreateStatement() throws Exception { + doTestTransactionRollbackRestoringAutoCommit(true, true, true); + } + + @Test + public void testTransactionRollbackWithAutoCommitFalseAndLazyConnectionAndCreateStatement() throws Exception { + doTestTransactionRollbackRestoringAutoCommit(false, true, true); + } + + private void doTestTransactionRollbackRestoringAutoCommit( + boolean autoCommit, boolean lazyConnection, final boolean createStatement) throws Exception { + + if (lazyConnection) { + given(con.getAutoCommit()).willReturn(autoCommit); + given(con.getTransactionIsolation()).willReturn(Connection.TRANSACTION_READ_COMMITTED); + } + + if (!lazyConnection || createStatement) { + given(con.getAutoCommit()).willReturn(autoCommit); + } + + final DataSource dsToUse = (lazyConnection ? new LazyConnectionDataSourceProxy(ds) : ds); + tm = new DataSourceTransactionManager(dsToUse); + TransactionTemplate tt = new TransactionTemplate(tm); + boolean condition3 = !TransactionSynchronizationManager.hasResource(dsToUse); + assertThat(condition3).as("Hasn't thread connection").isTrue(); + boolean condition2 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition2).as("Synchronization not active").isTrue(); + + final RuntimeException ex = new RuntimeException("Application exception"); + assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(dsToUse)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + Connection con = DataSourceUtils.getConnection(dsToUse); + if (createStatement) { + try { + con.createStatement(); + } + catch (SQLException ex) { + throw new UncategorizedSQLException("", "", ex); + } + } + throw ex; + } + })) + .isEqualTo(ex); + + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + boolean condition = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition).as("Synchronization not active").isTrue(); + + if (autoCommit && (!lazyConnection || createStatement)) { + InOrder ordered = inOrder(con); + ordered.verify(con).setAutoCommit(false); + ordered.verify(con).rollback(); + ordered.verify(con).setAutoCommit(true); + } + if (createStatement) { + verify(con, times(2)).close(); + } + else { + verify(con).close(); + } + } + + @Test + public void testTransactionRollbackOnly() throws Exception { + tm.setTransactionSynchronization(DataSourceTransactionManager.SYNCHRONIZATION_NEVER); + TransactionTemplate tt = new TransactionTemplate(tm); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + ConnectionHolder conHolder = new ConnectionHolder(con, true); + TransactionSynchronizationManager.bindResource(ds, conHolder); + final RuntimeException ex = new RuntimeException("Application exception"); + try { + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Is existing transaction").isTrue(); + throw ex; + } + }); + fail("Should have thrown RuntimeException"); + } + catch (RuntimeException ex2) { + // expected + boolean condition = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition).as("Synchronization not active").isTrue(); + assertThat(ex2).as("Correct exception thrown").isEqualTo(ex); + } + finally { + TransactionSynchronizationManager.unbindResource(ds); + } + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + } + + @Test + public void testParticipatingTransactionWithRollbackOnly() throws Exception { + doTestParticipatingTransactionWithRollbackOnly(false); + } + + @Test + public void testParticipatingTransactionWithRollbackOnlyAndFailEarly() throws Exception { + doTestParticipatingTransactionWithRollbackOnly(true); + } + + private void doTestParticipatingTransactionWithRollbackOnly(boolean failEarly) throws Exception { + given(con.isReadOnly()).willReturn(false); + if (failEarly) { + tm.setFailEarlyOnGlobalRollbackOnly(true); + } + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + TransactionStatus ts = tm.getTransaction(new DefaultTransactionDefinition()); + TestTransactionSynchronization synch = + new TestTransactionSynchronization(ds, TransactionSynchronization.STATUS_ROLLED_BACK); + TransactionSynchronizationManager.registerSynchronization(synch); + + boolean outerTransactionBoundaryReached = false; + try { + assertThat(ts.isNewTransaction()).as("Is new transaction").isTrue(); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + boolean condition1 = !status.isNewTransaction(); + assertThat(condition1).as("Is existing transaction").isTrue(); + assertThat(status.isRollbackOnly()).as("Is not rollback-only").isFalse(); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Is existing transaction").isTrue(); + status.setRollbackOnly(); + } + }); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Is existing transaction").isTrue(); + assertThat(status.isRollbackOnly()).as("Is rollback-only").isTrue(); + } + }); + + outerTransactionBoundaryReached = true; + tm.commit(ts); + + fail("Should have thrown UnexpectedRollbackException"); + } + catch (UnexpectedRollbackException ex) { + // expected + if (!outerTransactionBoundaryReached) { + tm.rollback(ts); + } + if (failEarly) { + assertThat(outerTransactionBoundaryReached).isFalse(); + } + else { + assertThat(outerTransactionBoundaryReached).isTrue(); + } + } + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + assertThat(synch.beforeCommitCalled).isFalse(); + assertThat(synch.beforeCompletionCalled).isTrue(); + assertThat(synch.afterCommitCalled).isFalse(); + assertThat(synch.afterCompletionCalled).isTrue(); + verify(con).rollback(); + verify(con).close(); + } + + @Test + public void testParticipatingTransactionWithIncompatibleIsolationLevel() throws Exception { + tm.setValidateExistingTransaction(true); + + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + assertThatExceptionOfType(IllegalTransactionStateException.class).isThrownBy(() -> { + final TransactionTemplate tt = new TransactionTemplate(tm); + final TransactionTemplate tt2 = new TransactionTemplate(tm); + tt2.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isRollbackOnly()).as("Is not rollback-only").isFalse(); + tt2.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + status.setRollbackOnly(); + } + }); + assertThat(status.isRollbackOnly()).as("Is rollback-only").isTrue(); + } + }); + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(); + verify(con).close(); + } + + @Test + public void testParticipatingTransactionWithIncompatibleReadOnly() throws Exception { + willThrow(new SQLException("read-only not supported")).given(con).setReadOnly(true); + tm.setValidateExistingTransaction(true); + + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + assertThatExceptionOfType(IllegalTransactionStateException.class).isThrownBy(() -> { + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setReadOnly(true); + final TransactionTemplate tt2 = new TransactionTemplate(tm); + tt2.setReadOnly(false); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isRollbackOnly()).as("Is not rollback-only").isFalse(); + tt2.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + status.setRollbackOnly(); + } + }); + assertThat(status.isRollbackOnly()).as("Is rollback-only").isTrue(); + } + }); + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(); + verify(con).close(); + } + + @Test + public void testParticipatingTransactionWithTransactionStartedFromSynch() throws Exception { + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + + final TestTransactionSynchronization synch = + new TestTransactionSynchronization(ds, TransactionSynchronization.STATUS_COMMITTED) { + @Override + protected void doAfterCompletion(int status) { + super.doAfterCompletion(status); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + } + }); + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {}); + } + }; + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + TransactionSynchronizationManager.registerSynchronization(synch); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + assertThat(synch.beforeCommitCalled).isTrue(); + assertThat(synch.beforeCompletionCalled).isTrue(); + assertThat(synch.afterCommitCalled).isTrue(); + assertThat(synch.afterCompletionCalled).isTrue(); + boolean condition3 = synch.afterCompletionException instanceof IllegalStateException; + assertThat(condition3).isTrue(); + verify(con, times(2)).commit(); + verify(con, times(2)).close(); + } + + @Test + public void testParticipatingTransactionWithDifferentConnectionObtainedFromSynch() throws Exception { + DataSource ds2 = mock(DataSource.class); + final Connection con2 = mock(Connection.class); + given(ds2.getConnection()).willReturn(con2); + + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + final TransactionTemplate tt = new TransactionTemplate(tm); + + final TestTransactionSynchronization synch = + new TestTransactionSynchronization(ds, TransactionSynchronization.STATUS_COMMITTED) { + @Override + protected void doAfterCompletion(int status) { + super.doAfterCompletion(status); + Connection con = DataSourceUtils.getConnection(ds2); + DataSourceUtils.releaseConnection(con, ds2); + } + }; + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + TransactionSynchronizationManager.registerSynchronization(synch); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + assertThat(synch.beforeCommitCalled).isTrue(); + assertThat(synch.beforeCompletionCalled).isTrue(); + assertThat(synch.afterCommitCalled).isTrue(); + assertThat(synch.afterCompletionCalled).isTrue(); + assertThat(synch.afterCompletionException).isNull(); + verify(con).commit(); + verify(con).close(); + verify(con2).close(); + } + + @Test + public void testParticipatingTransactionWithRollbackOnlyAndInnerSynch() throws Exception { + tm.setTransactionSynchronization(DataSourceTransactionManager.SYNCHRONIZATION_NEVER); + DataSourceTransactionManager tm2 = new DataSourceTransactionManager(ds); + // tm has no synch enabled (used at outer level), tm2 has synch enabled (inner level) + + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + TransactionStatus ts = tm.getTransaction(new DefaultTransactionDefinition()); + final TestTransactionSynchronization synch = + new TestTransactionSynchronization(ds, TransactionSynchronization.STATUS_UNKNOWN); + + assertThatExceptionOfType(UnexpectedRollbackException.class).isThrownBy(() -> { + assertThat(ts.isNewTransaction()).as("Is new transaction").isTrue(); + final TransactionTemplate tt = new TransactionTemplate(tm2); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + boolean condition1 = !status.isNewTransaction(); + assertThat(condition1).as("Is existing transaction").isTrue(); + assertThat(status.isRollbackOnly()).as("Is not rollback-only").isFalse(); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Is existing transaction").isTrue(); + status.setRollbackOnly(); + } + }); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Is existing transaction").isTrue(); + assertThat(status.isRollbackOnly()).as("Is rollback-only").isTrue(); + TransactionSynchronizationManager.registerSynchronization(synch); + } + }); + + tm.commit(ts); + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + assertThat(synch.beforeCommitCalled).isFalse(); + assertThat(synch.beforeCompletionCalled).isTrue(); + assertThat(synch.afterCommitCalled).isFalse(); + assertThat(synch.afterCompletionCalled).isTrue(); + verify(con).rollback(); + verify(con).close(); + } + + @Test + public void testPropagationRequiresNewWithExistingTransaction() throws Exception { + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + status.setRollbackOnly(); + } + }); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(); + verify(con).commit(); + verify(con, times(2)).close(); + } + + @Test + public void testPropagationRequiresNewWithExistingTransactionAndUnrelatedDataSource() throws Exception { + Connection con2 = mock(Connection.class); + final DataSource ds2 = mock(DataSource.class); + given(ds2.getConnection()).willReturn(con2); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + + PlatformTransactionManager tm2 = new DataSourceTransactionManager(ds2); + final TransactionTemplate tt2 = new TransactionTemplate(tm2); + tt2.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + + boolean condition4 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition4).as("Hasn't thread connection").isTrue(); + boolean condition3 = !TransactionSynchronizationManager.hasResource(ds2); + assertThat(condition3).as("Hasn't thread connection").isTrue(); + boolean condition2 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition2).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + tt2.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + status.setRollbackOnly(); + } + }); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + } + }); + + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + boolean condition = !TransactionSynchronizationManager.hasResource(ds2); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).commit(); + verify(con).close(); + verify(con2).rollback(); + verify(con2).close(); + } + + @Test + public void testPropagationRequiresNewWithExistingTransactionAndUnrelatedFailingDataSource() throws Exception { + final DataSource ds2 = mock(DataSource.class); + SQLException failure = new SQLException(); + given(ds2.getConnection()).willThrow(failure); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + + DataSourceTransactionManager tm2 = new DataSourceTransactionManager(ds2); + tm2.setTransactionSynchronization(DataSourceTransactionManager.SYNCHRONIZATION_NEVER); + final TransactionTemplate tt2 = new TransactionTemplate(tm2); + tt2.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + + boolean condition4 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition4).as("Hasn't thread connection").isTrue(); + boolean condition3 = !TransactionSynchronizationManager.hasResource(ds2); + assertThat(condition3).as("Hasn't thread connection").isTrue(); + boolean condition2 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition2).as("Synchronization not active").isTrue(); + + assertThatExceptionOfType(CannotCreateTransactionException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + tt2.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + status.setRollbackOnly(); + } + }); + } + })).withCause(failure); + + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + boolean condition = !TransactionSynchronizationManager.hasResource(ds2); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(); + verify(con).close(); + } + + @Test + public void testPropagationNotSupportedWithExistingTransaction() throws Exception { + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NOT_SUPPORTED); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Isn't new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isFalse(); + status.setRollbackOnly(); + } + }); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).commit(); + verify(con).close(); + } + + @Test + public void testPropagationNeverWithExistingTransaction() throws Exception { + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + assertThatExceptionOfType(IllegalTransactionStateException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NEVER); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + fail("Should have thrown IllegalTransactionStateException"); + } + }); + fail("Should have thrown IllegalTransactionStateException"); + } + })); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(); + verify(con).close(); + } + + @Test + public void testPropagationSupportsAndRequiresNew() throws Exception { + TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + TransactionTemplate tt2 = new TransactionTemplate(tm); + tt2.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + tt2.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(DataSourceUtils.getConnection(ds)).isSameAs(con); + assertThat(DataSourceUtils.getConnection(ds)).isSameAs(con); + } + }); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).commit(); + verify(con).close(); + } + + @Test + public void testPropagationSupportsAndRequiresNewWithEarlyAccess() throws Exception { + final Connection con1 = mock(Connection.class); + final Connection con2 = mock(Connection.class); + given(ds.getConnection()).willReturn(con1, con2); + + final + TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + assertThat(DataSourceUtils.getConnection(ds)).isSameAs(con1); + assertThat(DataSourceUtils.getConnection(ds)).isSameAs(con1); + TransactionTemplate tt2 = new TransactionTemplate(tm); + tt2.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + tt2.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(DataSourceUtils.getConnection(ds)).isSameAs(con2); + assertThat(DataSourceUtils.getConnection(ds)).isSameAs(con2); + } + }); + assertThat(DataSourceUtils.getConnection(ds)).isSameAs(con1); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con1).close(); + verify(con2).commit(); + verify(con2).close(); + } + + @Test + public void testTransactionWithIsolationAndReadOnly() throws Exception { + given(con.getTransactionIsolation()).willReturn(Connection.TRANSACTION_READ_COMMITTED); + given(con.getAutoCommit()).willReturn(true); + + TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + tt.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE); + tt.setReadOnly(true); + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isTrue(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + // something transactional + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + InOrder ordered = inOrder(con); + ordered.verify(con).setReadOnly(true); + ordered.verify(con).setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); + ordered.verify(con).setAutoCommit(false); + ordered.verify(con).commit(); + ordered.verify(con).setAutoCommit(true); + ordered.verify(con).setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); + ordered.verify(con).setReadOnly(false); + verify(con).close(); + } + + @Test + public void testTransactionWithEnforceReadOnly() throws Exception { + tm.setEnforceReadOnly(true); + + given(con.getAutoCommit()).willReturn(true); + Statement stmt = mock(Statement.class); + given(con.createStatement()).willReturn(stmt); + + TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + tt.setReadOnly(true); + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isTrue(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + // something transactional + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + InOrder ordered = inOrder(con, stmt); + ordered.verify(con).setReadOnly(true); + ordered.verify(con).setAutoCommit(false); + ordered.verify(stmt).executeUpdate("SET TRANSACTION READ ONLY"); + ordered.verify(stmt).close(); + ordered.verify(con).commit(); + ordered.verify(con).setAutoCommit(true); + ordered.verify(con).setReadOnly(false); + ordered.verify(con).close(); + } + + @ParameterizedTest(name = "transaction with {0} second timeout") + @ValueSource(ints = {1, 10}) + @EnabledForTestGroups(LONG_RUNNING) + public void transactionWithTimeout(int timeout) throws Exception { + PreparedStatement ps = mock(PreparedStatement.class); + given(con.getAutoCommit()).willReturn(true); + given(con.prepareStatement("some SQL statement")).willReturn(ps); + + TransactionTemplate tt = new TransactionTemplate(tm); + tt.setTimeout(timeout); + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + + try { + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + try { + Thread.sleep(1500); + } + catch (InterruptedException ex) { + } + try { + Connection con = DataSourceUtils.getConnection(ds); + PreparedStatement ps = con.prepareStatement("some SQL statement"); + DataSourceUtils.applyTransactionTimeout(ps, ds); + } + catch (SQLException ex) { + throw new DataAccessResourceFailureException("", ex); + } + } + }); + if (timeout <= 1) { + fail("Should have thrown TransactionTimedOutException"); + } + } + catch (TransactionTimedOutException ex) { + if (timeout <= 1) { + // expected + } + else { + throw ex; + } + } + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + if (timeout > 1) { + verify(ps).setQueryTimeout(timeout - 1); + verify(con).commit(); + } + else { + verify(con).rollback(); + } + InOrder ordered = inOrder(con); + ordered.verify(con).setAutoCommit(false); + ordered.verify(con).setAutoCommit(true); + verify(con).close(); + } + + @Test + public void testTransactionAwareDataSourceProxy() throws Exception { + given(con.getAutoCommit()).willReturn(true); + given(con.getWarnings()).willThrow(new SQLException()); + + TransactionTemplate tt = new TransactionTemplate(tm); + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + // something transactional + assertThat(DataSourceUtils.getConnection(ds)).isEqualTo(con); + TransactionAwareDataSourceProxy dsProxy = new TransactionAwareDataSourceProxy(ds); + try { + Connection tCon = dsProxy.getConnection(); + tCon.getWarnings(); + tCon.clearWarnings(); + assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con); + // should be ignored + dsProxy.getConnection().close(); + } + catch (SQLException ex) { + throw new UncategorizedSQLException("", "", ex); + } + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + InOrder ordered = inOrder(con); + ordered.verify(con).setAutoCommit(false); + ordered.verify(con).commit(); + ordered.verify(con).setAutoCommit(true); + verify(con).close(); + } + + @Test + public void testTransactionAwareDataSourceProxyWithSuspension() throws Exception { + given(con.getAutoCommit()).willReturn(true); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW); + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + // something transactional + assertThat(DataSourceUtils.getConnection(ds)).isEqualTo(con); + final TransactionAwareDataSourceProxy dsProxy = new TransactionAwareDataSourceProxy(ds); + try { + assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con); + // should be ignored + dsProxy.getConnection().close(); + } + catch (SQLException ex) { + throw new UncategorizedSQLException("", "", ex); + } + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + // something transactional + assertThat(DataSourceUtils.getConnection(ds)).isEqualTo(con); + try { + assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con); + // should be ignored + dsProxy.getConnection().close(); + } + catch (SQLException ex) { + throw new UncategorizedSQLException("", "", ex); + } + } + }); + + try { + assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con); + // should be ignored + dsProxy.getConnection().close(); + } + catch (SQLException ex) { + throw new UncategorizedSQLException("", "", ex); + } + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + InOrder ordered = inOrder(con); + ordered.verify(con).setAutoCommit(false); + ordered.verify(con).commit(); + ordered.verify(con).setAutoCommit(true); + verify(con, times(2)).close(); + } + + @Test + public void testTransactionAwareDataSourceProxyWithSuspensionAndReobtaining() throws Exception { + given(con.getAutoCommit()).willReturn(true); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW); + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + // something transactional + assertThat(DataSourceUtils.getConnection(ds)).isEqualTo(con); + final TransactionAwareDataSourceProxy dsProxy = new TransactionAwareDataSourceProxy(ds); + dsProxy.setReobtainTransactionalConnections(true); + try { + assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con); + // should be ignored + dsProxy.getConnection().close(); + } + catch (SQLException ex) { + throw new UncategorizedSQLException("", "", ex); + } + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + // something transactional + assertThat(DataSourceUtils.getConnection(ds)).isEqualTo(con); + try { + assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con); + // should be ignored + dsProxy.getConnection().close(); + } + catch (SQLException ex) { + throw new UncategorizedSQLException("", "", ex); + } + } + }); + + try { + assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con); + // should be ignored + dsProxy.getConnection().close(); + } + catch (SQLException ex) { + throw new UncategorizedSQLException("", "", ex); + } + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + InOrder ordered = inOrder(con); + ordered.verify(con).setAutoCommit(false); + ordered.verify(con).commit(); + ordered.verify(con).setAutoCommit(true); + verify(con, times(2)).close(); + } + + /** + * Test behavior if the first operation on a connection (getAutoCommit) throws SQLException. + */ + @Test + public void testTransactionWithExceptionOnBegin() throws Exception { + willThrow(new SQLException("Cannot begin")).given(con).getAutoCommit(); + + TransactionTemplate tt = new TransactionTemplate(tm); + assertThatExceptionOfType(CannotCreateTransactionException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + // something transactional + } + })); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).close(); + } + + @Test + public void testTransactionWithExceptionOnCommit() throws Exception { + willThrow(new SQLException("Cannot commit")).given(con).commit(); + + TransactionTemplate tt = new TransactionTemplate(tm); + assertThatExceptionOfType(TransactionSystemException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + // something transactional + } + })); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).close(); + } + + @Test + public void testTransactionWithExceptionOnCommitAndRollbackOnCommitFailure() throws Exception { + willThrow(new SQLException("Cannot commit")).given(con).commit(); + + tm.setRollbackOnCommitFailure(true); + TransactionTemplate tt = new TransactionTemplate(tm); + assertThatExceptionOfType(TransactionSystemException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + // something transactional + } + })); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(); + verify(con).close(); + } + + @Test + public void testTransactionWithExceptionOnRollback() throws Exception { + given(con.getAutoCommit()).willReturn(true); + willThrow(new SQLException("Cannot rollback")).given(con).rollback(); + + TransactionTemplate tt = new TransactionTemplate(tm); + assertThatExceptionOfType(TransactionSystemException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + status.setRollbackOnly(); + } + })); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + InOrder ordered = inOrder(con); + ordered.verify(con).setAutoCommit(false); + ordered.verify(con).rollback(); + ordered.verify(con).setAutoCommit(true); + verify(con).close(); + } + + @Test + public void testTransactionWithPropagationSupports() throws Exception { + TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Is not new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isFalse(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + } + + @Test + public void testTransactionWithPropagationNotSupported() throws Exception { + TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NOT_SUPPORTED); + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Is not new transaction").isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + } + + @Test + public void testTransactionWithPropagationNever() throws Exception { + TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NEVER); + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Is not new transaction").isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + } + + @Test + public void testExistingTransactionWithPropagationNested() throws Exception { + doTestExistingTransactionWithPropagationNested(1); + } + + @Test + public void testExistingTransactionWithPropagationNestedTwice() throws Exception { + doTestExistingTransactionWithPropagationNested(2); + } + + private void doTestExistingTransactionWithPropagationNested(final int count) throws Exception { + DatabaseMetaData md = mock(DatabaseMetaData.class); + Savepoint sp = mock(Savepoint.class); + + given(md.supportsSavepoints()).willReturn(true); + given(con.getMetaData()).willReturn(md); + for (int i = 1; i <= count; i++) { + given(con.setSavepoint(ConnectionHolder.SAVEPOINT_NAME_PREFIX + i)).willReturn(sp); + } + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + boolean condition1 = !status.hasSavepoint(); + assertThat(condition1).as("Isn't nested transaction").isTrue(); + for (int i = 0; i < count; i++) { + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Isn't new transaction").isTrue(); + assertThat(status.hasSavepoint()).as("Is nested transaction").isTrue(); + } + }); + } + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + boolean condition = !status.hasSavepoint(); + assertThat(condition).as("Isn't nested transaction").isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con, times(count)).releaseSavepoint(sp); + verify(con).commit(); + verify(con).close(); + } + + @Test + public void testExistingTransactionWithPropagationNestedAndRollback() throws Exception { + DatabaseMetaData md = mock(DatabaseMetaData.class); + Savepoint sp = mock(Savepoint.class); + + given(md.supportsSavepoints()).willReturn(true); + given(con.getMetaData()).willReturn(md); + given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + boolean condition1 = !status.hasSavepoint(); + assertThat(condition1).as("Isn't nested transaction").isTrue(); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Isn't new transaction").isTrue(); + assertThat(status.hasSavepoint()).as("Is nested transaction").isTrue(); + status.setRollbackOnly(); + } + }); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + boolean condition = !status.hasSavepoint(); + assertThat(condition).as("Isn't nested transaction").isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(sp); + verify(con).releaseSavepoint(sp); + verify(con).commit(); + verify(con).close(); + } + + @Test + public void testExistingTransactionWithPropagationNestedAndRequiredRollback() throws Exception { + DatabaseMetaData md = mock(DatabaseMetaData.class); + Savepoint sp = mock(Savepoint.class); + + given(md.supportsSavepoints()).willReturn(true); + given(con.getMetaData()).willReturn(md); + given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + boolean condition1 = !status.hasSavepoint(); + assertThat(condition1).as("Isn't nested transaction").isTrue(); + assertThatIllegalStateException().isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Isn't new transaction").isTrue(); + assertThat(status.hasSavepoint()).as("Is nested transaction").isTrue(); + TransactionTemplate ntt = new TransactionTemplate(tm); + ntt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + boolean condition1 = !status.isNewTransaction(); + assertThat(condition1).as("Isn't new transaction").isTrue(); + boolean condition = !status.hasSavepoint(); + assertThat(condition).as("Is regular transaction").isTrue(); + throw new IllegalStateException(); + } + }); + } + })); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + boolean condition = !status.hasSavepoint(); + assertThat(condition).as("Isn't nested transaction").isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(sp); + verify(con).releaseSavepoint(sp); + verify(con).commit(); + verify(con).close(); + } + + @Test + public void testExistingTransactionWithPropagationNestedAndRequiredRollbackOnly() throws Exception { + DatabaseMetaData md = mock(DatabaseMetaData.class); + Savepoint sp = mock(Savepoint.class); + + given(md.supportsSavepoints()).willReturn(true); + given(con.getMetaData()).willReturn(md); + given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + boolean condition1 = !status.hasSavepoint(); + assertThat(condition1).as("Isn't nested transaction").isTrue(); + assertThatExceptionOfType(UnexpectedRollbackException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Isn't new transaction").isTrue(); + assertThat(status.hasSavepoint()).as("Is nested transaction").isTrue(); + TransactionTemplate ntt = new TransactionTemplate(tm); + ntt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + boolean condition1 = !status.isNewTransaction(); + assertThat(condition1).as("Isn't new transaction").isTrue(); + boolean condition = !status.hasSavepoint(); + assertThat(condition).as("Is regular transaction").isTrue(); + status.setRollbackOnly(); + } + }); + } + })); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + boolean condition = !status.hasSavepoint(); + assertThat(condition).as("Isn't nested transaction").isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(sp); + verify(con).releaseSavepoint(sp); + verify(con).commit(); + verify(con).close(); + } + + @Test + public void testExistingTransactionWithManualSavepoint() throws Exception { + DatabaseMetaData md = mock(DatabaseMetaData.class); + Savepoint sp = mock(Savepoint.class); + + given(md.supportsSavepoints()).willReturn(true); + given(con.getMetaData()).willReturn(md); + given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + Object savepoint = status.createSavepoint(); + status.releaseSavepoint(savepoint); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).releaseSavepoint(sp); + verify(con).commit(); + verify(con).close(); + verify(ds).getConnection(); + } + + @Test + public void testExistingTransactionWithManualSavepointAndRollback() throws Exception { + DatabaseMetaData md = mock(DatabaseMetaData.class); + Savepoint sp = mock(Savepoint.class); + + given(md.supportsSavepoints()).willReturn(true); + given(con.getMetaData()).willReturn(md); + given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + Object savepoint = status.createSavepoint(); + status.rollbackToSavepoint(savepoint); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(sp); + verify(con).commit(); + verify(con).close(); + } + + @Test + public void testTransactionWithPropagationNested() throws Exception { + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).commit(); + verify(con).close(); + } + + @Test + public void testTransactionWithPropagationNestedAndRollback() throws Exception { + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + status.setRollbackOnly(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(); + verify(con).close(); + } + + + private static class TestTransactionSynchronization implements TransactionSynchronization { + + private DataSource dataSource; + + private int status; + + public boolean beforeCommitCalled; + + public boolean beforeCompletionCalled; + + public boolean afterCommitCalled; + + public boolean afterCompletionCalled; + + public Throwable afterCompletionException; + + public TestTransactionSynchronization(DataSource dataSource, int status) { + this.dataSource = dataSource; + this.status = status; + } + + @Override + public void suspend() { + } + + @Override + public void resume() { + } + + @Override + public void flush() { + } + + @Override + public void beforeCommit(boolean readOnly) { + if (this.status != TransactionSynchronization.STATUS_COMMITTED) { + fail("Should never be called"); + } + assertThat(this.beforeCommitCalled).isFalse(); + this.beforeCommitCalled = true; + } + + @Override + public void beforeCompletion() { + assertThat(this.beforeCompletionCalled).isFalse(); + this.beforeCompletionCalled = true; + } + + @Override + public void afterCommit() { + if (this.status != TransactionSynchronization.STATUS_COMMITTED) { + fail("Should never be called"); + } + assertThat(this.afterCommitCalled).isFalse(); + this.afterCommitCalled = true; + } + + @Override + public void afterCompletion(int status) { + try { + doAfterCompletion(status); + } + catch (Throwable ex) { + this.afterCompletionException = ex; + } + } + + protected void doAfterCompletion(int status) { + assertThat(this.afterCompletionCalled).isFalse(); + this.afterCompletionCalled = true; + assertThat(status == this.status).isTrue(); + assertThat(TransactionSynchronizationManager.hasResource(this.dataSource)).isTrue(); + } + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DelegatingDataSourceTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DelegatingDataSourceTests.java new file mode 100644 index 0000000..59ce02c --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DelegatingDataSourceTests.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource; + +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; +import java.sql.Connection; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link DelegatingDataSource}. + * + * @author Phillip Webb + */ +public class DelegatingDataSourceTests { + + private final DataSource delegate = mock(DataSource.class); + + private DelegatingDataSource dataSource = new DelegatingDataSource(delegate); + + @Test + public void shouldDelegateGetConnection() throws Exception { + Connection connection = mock(Connection.class); + given(delegate.getConnection()).willReturn(connection); + assertThat(dataSource.getConnection()).isEqualTo(connection); + } + + @Test + public void shouldDelegateGetConnectionWithUsernameAndPassword() throws Exception { + Connection connection = mock(Connection.class); + String username = "username"; + String password = "password"; + given(delegate.getConnection(username, password)).willReturn(connection); + assertThat(dataSource.getConnection(username, password)).isEqualTo(connection); + } + + @Test + public void shouldDelegateGetLogWriter() throws Exception { + PrintWriter writer = new PrintWriter(new ByteArrayOutputStream()); + given(delegate.getLogWriter()).willReturn(writer); + assertThat(dataSource.getLogWriter()).isEqualTo(writer); + } + + @Test + public void shouldDelegateSetLogWriter() throws Exception { + PrintWriter writer = new PrintWriter(new ByteArrayOutputStream()); + dataSource.setLogWriter(writer); + verify(delegate).setLogWriter(writer); + } + + @Test + public void shouldDelegateGetLoginTimeout() throws Exception { + int timeout = 123; + given(delegate.getLoginTimeout()).willReturn(timeout); + assertThat(dataSource.getLoginTimeout()).isEqualTo(timeout); + } + + @Test + public void shouldDelegateSetLoginTimeoutWithSeconds() throws Exception { + int timeout = 123; + dataSource.setLoginTimeout(timeout); + verify(delegate).setLoginTimeout(timeout); + } + + @Test + public void shouldDelegateUnwrapWithoutImplementing() throws Exception { + ExampleWrapper wrapper = mock(ExampleWrapper.class); + given(delegate.unwrap(ExampleWrapper.class)).willReturn(wrapper); + assertThat(dataSource.unwrap(ExampleWrapper.class)).isEqualTo(wrapper); + } + + @Test + public void shouldDelegateUnwrapImplementing() throws Exception { + dataSource = new DelegatingDataSourceWithWrapper(); + assertThat(dataSource.unwrap(ExampleWrapper.class)).isSameAs(dataSource); + } + + @Test + public void shouldDelegateIsWrapperForWithoutImplementing() throws Exception { + given(delegate.isWrapperFor(ExampleWrapper.class)).willReturn(true); + assertThat(dataSource.isWrapperFor(ExampleWrapper.class)).isTrue(); + } + + @Test + public void shouldDelegateIsWrapperForImplementing() throws Exception { + dataSource = new DelegatingDataSourceWithWrapper(); + assertThat(dataSource.isWrapperFor(ExampleWrapper.class)).isTrue(); + } + + public static interface ExampleWrapper { + } + + private static class DelegatingDataSourceWithWrapper extends DelegatingDataSource + implements ExampleWrapper { + } +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DriverManagerDataSourceTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DriverManagerDataSourceTests.java new file mode 100644 index 0000000..3b5ecbe --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DriverManagerDataSourceTests.java @@ -0,0 +1,146 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource; + +import java.sql.Connection; +import java.util.Properties; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.Mockito.mock; + +/** + * @author Rod Johnson + */ +public class DriverManagerDataSourceTests { + + private Connection connection = mock(Connection.class); + + @Test + public void testStandardUsage() throws Exception { + final String jdbcUrl = "url"; + final String uname = "uname"; + final String pwd = "pwd"; + + class TestDriverManagerDataSource extends DriverManagerDataSource { + @Override + protected Connection getConnectionFromDriverManager(String url, Properties props) { + assertThat(url).isEqualTo(jdbcUrl); + assertThat(props.getProperty("user")).isEqualTo(uname); + assertThat(props.getProperty("password")).isEqualTo(pwd); + return connection; + } + } + + DriverManagerDataSource ds = new TestDriverManagerDataSource(); + //ds.setDriverClassName("foobar"); + ds.setUrl(jdbcUrl); + ds.setUsername(uname); + ds.setPassword(pwd); + + Connection actualCon = ds.getConnection(); + assertThat(actualCon == connection).isTrue(); + + assertThat(ds.getUrl().equals(jdbcUrl)).isTrue(); + assertThat(ds.getPassword().equals(pwd)).isTrue(); + assertThat(ds.getUsername().equals(uname)).isTrue(); + } + + @Test + public void testUsageWithConnectionProperties() throws Exception { + final String jdbcUrl = "url"; + + final Properties connProps = new Properties(); + connProps.setProperty("myProp", "myValue"); + connProps.setProperty("yourProp", "yourValue"); + connProps.setProperty("user", "uname"); + connProps.setProperty("password", "pwd"); + + class TestDriverManagerDataSource extends DriverManagerDataSource { + @Override + protected Connection getConnectionFromDriverManager(String url, Properties props) { + assertThat(url).isEqualTo(jdbcUrl); + assertThat(props.getProperty("user")).isEqualTo("uname"); + assertThat(props.getProperty("password")).isEqualTo("pwd"); + assertThat(props.getProperty("myProp")).isEqualTo("myValue"); + assertThat(props.getProperty("yourProp")).isEqualTo("yourValue"); + return connection; + } + } + + DriverManagerDataSource ds = new TestDriverManagerDataSource(); + //ds.setDriverClassName("foobar"); + ds.setUrl(jdbcUrl); + ds.setConnectionProperties(connProps); + + Connection actualCon = ds.getConnection(); + assertThat(actualCon == connection).isTrue(); + + assertThat(ds.getUrl().equals(jdbcUrl)).isTrue(); + } + + @Test + public void testUsageWithConnectionPropertiesAndUserCredentials() throws Exception { + final String jdbcUrl = "url"; + final String uname = "uname"; + final String pwd = "pwd"; + + final Properties connProps = new Properties(); + connProps.setProperty("myProp", "myValue"); + connProps.setProperty("yourProp", "yourValue"); + connProps.setProperty("user", "uname2"); + connProps.setProperty("password", "pwd2"); + + class TestDriverManagerDataSource extends DriverManagerDataSource { + @Override + protected Connection getConnectionFromDriverManager(String url, Properties props) { + assertThat(url).isEqualTo(jdbcUrl); + assertThat(props.getProperty("user")).isEqualTo(uname); + assertThat(props.getProperty("password")).isEqualTo(pwd); + assertThat(props.getProperty("myProp")).isEqualTo("myValue"); + assertThat(props.getProperty("yourProp")).isEqualTo("yourValue"); + return connection; + } + } + + DriverManagerDataSource ds = new TestDriverManagerDataSource(); + //ds.setDriverClassName("foobar"); + ds.setUrl(jdbcUrl); + ds.setUsername(uname); + ds.setPassword(pwd); + ds.setConnectionProperties(connProps); + + Connection actualCon = ds.getConnection(); + assertThat(actualCon == connection).isTrue(); + + assertThat(ds.getUrl().equals(jdbcUrl)).isTrue(); + assertThat(ds.getPassword().equals(pwd)).isTrue(); + assertThat(ds.getUsername().equals(uname)).isTrue(); + } + + @Test + public void testInvalidClassName() throws Exception { + String bogusClassName = "foobar"; + DriverManagerDataSource ds = new DriverManagerDataSource(); + assertThatIllegalStateException().isThrownBy(() -> + ds.setDriverClassName(bogusClassName)) + .withCauseInstanceOf(ClassNotFoundException.class); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/TestDataSourceWrapper.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/TestDataSourceWrapper.java new file mode 100644 index 0000000..c12160c --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/TestDataSourceWrapper.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource; + +import java.sql.Connection; +import java.sql.SQLException; + +import javax.sql.DataSource; + +public class TestDataSourceWrapper extends AbstractDataSource { + + private DataSource target; + + public void setTarget(DataSource target) { + this.target = target; + } + + @Override + public Connection getConnection() throws SQLException { + return target.getConnection(); + } + + @Override + public Connection getConnection(String username, String password) throws SQLException { + return target.getConnection(username, password); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/UserCredentialsDataSourceAdapterTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/UserCredentialsDataSourceAdapterTests.java new file mode 100644 index 0000000..ff047cc --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/UserCredentialsDataSourceAdapterTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource; + +import java.sql.Connection; +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * @author Juergen Hoeller + * @since 28.05.2004 + */ +public class UserCredentialsDataSourceAdapterTests { + + @Test + public void testStaticCredentials() throws SQLException { + DataSource dataSource = mock(DataSource.class); + Connection connection = mock(Connection.class); + given(dataSource.getConnection("user", "pw")).willReturn(connection); + + UserCredentialsDataSourceAdapter adapter = new UserCredentialsDataSourceAdapter(); + adapter.setTargetDataSource(dataSource); + adapter.setUsername("user"); + adapter.setPassword("pw"); + assertThat(adapter.getConnection()).isEqualTo(connection); + } + + @Test + public void testNoCredentials() throws SQLException { + DataSource dataSource = mock(DataSource.class); + Connection connection = mock(Connection.class); + given(dataSource.getConnection()).willReturn(connection); + UserCredentialsDataSourceAdapter adapter = new UserCredentialsDataSourceAdapter(); + adapter.setTargetDataSource(dataSource); + assertThat(adapter.getConnection()).isEqualTo(connection); + } + + @Test + public void testThreadBoundCredentials() throws SQLException { + DataSource dataSource = mock(DataSource.class); + Connection connection = mock(Connection.class); + given(dataSource.getConnection("user", "pw")).willReturn(connection); + + UserCredentialsDataSourceAdapter adapter = new UserCredentialsDataSourceAdapter(); + adapter.setTargetDataSource(dataSource); + + adapter.setCredentialsForCurrentThread("user", "pw"); + try { + assertThat(adapter.getConnection()).isEqualTo(connection); + } + finally { + adapter.removeCredentialsFromCurrentThread(); + } + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilderTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilderTests.java new file mode 100644 index 0000000..d4be256 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilderTests.java @@ -0,0 +1,188 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.embedded; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.ClassRelativeResourceLoader; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.init.CannotReadScriptException; +import org.springframework.jdbc.datasource.init.ScriptStatementFailedException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType.DERBY; +import static org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType.H2; + +/** + * Integration tests for {@link EmbeddedDatabaseBuilder}. + * + * @author Keith Donald + * @author Sam Brannen + */ +public class EmbeddedDatabaseBuilderTests { + + private final EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder(new ClassRelativeResourceLoader( + getClass())); + + + @Test + public void addDefaultScripts() throws Exception { + doTwice(() -> { + EmbeddedDatabase db = new EmbeddedDatabaseBuilder()// + .addDefaultScripts()// + .build(); + assertDatabaseCreatedAndShutdown(db); + }); + } + + @Test + public void addScriptWithBogusFileName() { + assertThatExceptionOfType(CannotReadScriptException.class).isThrownBy( + new EmbeddedDatabaseBuilder().addScript("bogus.sql")::build); + } + + @Test + public void addScript() throws Exception { + doTwice(() -> { + EmbeddedDatabase db = builder// + .addScript("db-schema.sql")// + .addScript("db-test-data.sql")// + .build(); + assertDatabaseCreatedAndShutdown(db); + }); + } + + @Test + public void addScripts() throws Exception { + doTwice(() -> { + EmbeddedDatabase db = builder// + .addScripts("db-schema.sql", "db-test-data.sql")// + .build(); + assertDatabaseCreatedAndShutdown(db); + }); + } + + @Test + public void addScriptsWithDefaultCommentPrefix() throws Exception { + doTwice(() -> { + EmbeddedDatabase db = builder// + .addScripts("db-schema-comments.sql", "db-test-data.sql")// + .build(); + assertDatabaseCreatedAndShutdown(db); + }); + } + + @Test + public void addScriptsWithCustomCommentPrefix() throws Exception { + doTwice(() -> { + EmbeddedDatabase db = builder// + .addScripts("db-schema-custom-comments.sql", "db-test-data.sql")// + .setCommentPrefix("~")// + .build(); + assertDatabaseCreatedAndShutdown(db); + }); + } + + @Test + public void addScriptsWithCustomBlockComments() throws Exception { + doTwice(() -> { + EmbeddedDatabase db = builder// + .addScripts("db-schema-block-comments.sql", "db-test-data.sql")// + .setBlockCommentStartDelimiter("{*")// + .setBlockCommentEndDelimiter("*}")// + .build(); + assertDatabaseCreatedAndShutdown(db); + }); + } + + @Test + public void setTypeToH2() throws Exception { + doTwice(() -> { + EmbeddedDatabase db = builder// + .setType(H2)// + .addScripts("db-schema.sql", "db-test-data.sql")// + .build(); + assertDatabaseCreatedAndShutdown(db); + }); + } + + @Test + public void setTypeToDerbyAndIgnoreFailedDrops() throws Exception { + doTwice(() -> { + EmbeddedDatabase db = builder// + .setType(DERBY)// + .ignoreFailedDrops(true)// + .addScripts("db-schema-derby-with-drop.sql", "db-test-data.sql").build(); + assertDatabaseCreatedAndShutdown(db); + }); + } + + @Test + public void createSameSchemaTwiceWithoutUniqueDbNames() throws Exception { + EmbeddedDatabase db1 = new EmbeddedDatabaseBuilder(new ClassRelativeResourceLoader(getClass())) + .addScripts("db-schema-without-dropping.sql").build(); + try { + assertThatExceptionOfType(ScriptStatementFailedException.class).isThrownBy(() -> + new EmbeddedDatabaseBuilder(new ClassRelativeResourceLoader(getClass())).addScripts("db-schema-without-dropping.sql").build()); + } + finally { + db1.shutdown(); + } + } + + @Test + public void createSameSchemaTwiceWithGeneratedUniqueDbNames() throws Exception { + EmbeddedDatabase db1 = new EmbeddedDatabaseBuilder(new ClassRelativeResourceLoader(getClass()))// + .addScripts("db-schema-without-dropping.sql", "db-test-data.sql")// + .generateUniqueName(true)// + .build(); + + JdbcTemplate template1 = new JdbcTemplate(db1); + assertNumRowsInTestTable(template1, 1); + template1.update("insert into T_TEST (NAME) values ('Sam')"); + assertNumRowsInTestTable(template1, 2); + + EmbeddedDatabase db2 = new EmbeddedDatabaseBuilder(new ClassRelativeResourceLoader(getClass()))// + .addScripts("db-schema-without-dropping.sql", "db-test-data.sql")// + .generateUniqueName(true)// + .build(); + assertDatabaseCreated(db2); + + db1.shutdown(); + db2.shutdown(); + } + + private void doTwice(Runnable test) { + test.run(); + test.run(); + } + + private void assertNumRowsInTestTable(JdbcTemplate template, int count) { + assertThat(template.queryForObject("select count(*) from T_TEST", Integer.class).intValue()).isEqualTo(count); + } + + private void assertDatabaseCreated(EmbeddedDatabase db) { + assertNumRowsInTestTable(new JdbcTemplate(db), 1); + } + + private void assertDatabaseCreatedAndShutdown(EmbeddedDatabase db) { + assertDatabaseCreated(db); + db.shutdown(); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryBeanTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryBeanTests.java new file mode 100644 index 0000000..f9dc0c3 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryBeanTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.embedded; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.ClassRelativeResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Keith Donald + */ +public class EmbeddedDatabaseFactoryBeanTests { + + private final ClassRelativeResourceLoader resourceLoader = new ClassRelativeResourceLoader(getClass()); + + + Resource resource(String path) { + return resourceLoader.getResource(path); + } + + @Test + public void testFactoryBeanLifecycle() throws Exception { + EmbeddedDatabaseFactoryBean bean = new EmbeddedDatabaseFactoryBean(); + ResourceDatabasePopulator populator = new ResourceDatabasePopulator(resource("db-schema.sql"), + resource("db-test-data.sql")); + bean.setDatabasePopulator(populator); + bean.afterPropertiesSet(); + DataSource ds = bean.getObject(); + JdbcTemplate template = new JdbcTemplate(ds); + assertThat(template.queryForObject("select NAME from T_TEST", String.class)).isEqualTo("Keith"); + bean.destroy(); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryTests.java new file mode 100644 index 0000000..7f7b7fd --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.embedded; + +import java.sql.Connection; + +import org.junit.jupiter.api.Test; + +import org.springframework.jdbc.datasource.init.DatabasePopulator; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Keith Donald + */ +public class EmbeddedDatabaseFactoryTests { + + private EmbeddedDatabaseFactory factory = new EmbeddedDatabaseFactory(); + + + @Test + public void testGetDataSource() { + StubDatabasePopulator populator = new StubDatabasePopulator(); + factory.setDatabasePopulator(populator); + EmbeddedDatabase db = factory.getDatabase(); + assertThat(populator.populateCalled).isTrue(); + db.shutdown(); + } + + + private static class StubDatabasePopulator implements DatabasePopulator { + + private boolean populateCalled; + + @Override + public void populate(Connection connection) { + this.populateCalled = true; + } + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/AbstractDatabaseInitializationTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/AbstractDatabaseInitializationTests.java new file mode 100644 index 0000000..80c523b --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/AbstractDatabaseInitializationTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.init; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +import org.springframework.core.io.ClassRelativeResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import static org.assertj.core.api.Assertions.assertThat; + + + +/** + * Abstract base class for integration tests involving database initialization. + * + * @author Sam Brannen + * @since 4.0.3 + */ +public abstract class AbstractDatabaseInitializationTests { + + private final ClassRelativeResourceLoader resourceLoader = new ClassRelativeResourceLoader(getClass()); + + EmbeddedDatabase db; + + JdbcTemplate jdbcTemplate; + + + @BeforeEach + public void setUp() { + db = new EmbeddedDatabaseBuilder().setType(getEmbeddedDatabaseType()).build(); + jdbcTemplate = new JdbcTemplate(db); + } + + @AfterEach + public void shutDown() { + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.clear(); + TransactionSynchronizationManager.unbindResource(db); + } + db.shutdown(); + } + + abstract EmbeddedDatabaseType getEmbeddedDatabaseType(); + + Resource resource(String path) { + return resourceLoader.getResource(path); + } + + Resource defaultSchema() { + return resource("db-schema.sql"); + } + + Resource usersSchema() { + return resource("users-schema.sql"); + } + + void assertUsersDatabaseCreated(String... lastNames) { + for (String lastName : lastNames) { + String sql = "select count(0) from users where last_name = ?"; + Integer result = jdbcTemplate.queryForObject(sql, Integer.class, lastName); + assertThat(result).as("user with last name [" + lastName + "]").isEqualTo(1); + } + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/AbstractDatabasePopulatorTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/AbstractDatabasePopulatorTests.java new file mode 100644 index 0000000..ff8401f --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/AbstractDatabasePopulatorTests.java @@ -0,0 +1,197 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.init; + +import java.sql.Connection; +import java.sql.SQLException; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Abstract base class for integration tests for {@link ResourceDatabasePopulator} + * and {@link DatabasePopulatorUtils}. + * + * @author Dave Syer + * @author Sam Brannen + * @author Oliver Gierke + */ +abstract class AbstractDatabasePopulatorTests extends AbstractDatabaseInitializationTests { + + private static final String COUNT_DAVE_SQL = "select COUNT(NAME) from T_TEST where NAME='Dave'"; + + private static final String COUNT_KEITH_SQL = "select COUNT(NAME) from T_TEST where NAME='Keith'"; + + protected final ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator(); + + + @Test + void scriptWithSingleLineCommentsAndFailedDrop() throws Exception { + databasePopulator.addScript(resource("db-schema-failed-drop-comments.sql")); + databasePopulator.addScript(resource("db-test-data.sql")); + databasePopulator.setIgnoreFailedDrops(true); + DatabasePopulatorUtils.execute(databasePopulator, db); + assertTestDatabaseCreated(); + } + + @Test + void scriptWithStandardEscapedLiteral() throws Exception { + databasePopulator.addScript(defaultSchema()); + databasePopulator.addScript(resource("db-test-data-escaped-literal.sql")); + DatabasePopulatorUtils.execute(databasePopulator, db); + assertTestDatabaseCreated("'Keith'"); + } + + @Test + void scriptWithMySqlEscapedLiteral() throws Exception { + databasePopulator.addScript(defaultSchema()); + databasePopulator.addScript(resource("db-test-data-mysql-escaped-literal.sql")); + DatabasePopulatorUtils.execute(databasePopulator, db); + assertTestDatabaseCreated("\\$Keith\\$"); + } + + @Test + void scriptWithMultipleStatements() throws Exception { + databasePopulator.addScript(defaultSchema()); + databasePopulator.addScript(resource("db-test-data-multiple.sql")); + DatabasePopulatorUtils.execute(databasePopulator, db); + assertThat(jdbcTemplate.queryForObject(COUNT_KEITH_SQL, Integer.class)).isEqualTo(1); + assertThat(jdbcTemplate.queryForObject(COUNT_DAVE_SQL, Integer.class)).isEqualTo(1); + } + + @Test + void scriptWithMultipleStatementsAndLongSeparator() throws Exception { + databasePopulator.addScript(defaultSchema()); + databasePopulator.addScript(resource("db-test-data-endings.sql")); + databasePopulator.setSeparator("@@"); + DatabasePopulatorUtils.execute(databasePopulator, db); + assertThat(jdbcTemplate.queryForObject(COUNT_KEITH_SQL, Integer.class)).isEqualTo(1); + assertThat(jdbcTemplate.queryForObject(COUNT_DAVE_SQL, Integer.class)).isEqualTo(1); + } + + @Test + void scriptWithMultipleStatementsAndWhitespaceSeparator() throws Exception { + databasePopulator.addScript(defaultSchema()); + databasePopulator.addScript(resource("db-test-data-whitespace.sql")); + databasePopulator.setSeparator("/\n"); + DatabasePopulatorUtils.execute(databasePopulator, db); + assertThat(jdbcTemplate.queryForObject(COUNT_KEITH_SQL, Integer.class)).isEqualTo(1); + assertThat(jdbcTemplate.queryForObject(COUNT_DAVE_SQL, Integer.class)).isEqualTo(1); + } + + @Test + void scriptWithMultipleStatementsAndNewlineSeparator() throws Exception { + databasePopulator.addScript(defaultSchema()); + databasePopulator.addScript(resource("db-test-data-newline.sql")); + DatabasePopulatorUtils.execute(databasePopulator, db); + assertThat(jdbcTemplate.queryForObject(COUNT_KEITH_SQL, Integer.class)).isEqualTo(1); + assertThat(jdbcTemplate.queryForObject(COUNT_DAVE_SQL, Integer.class)).isEqualTo(1); + } + + @Test + void scriptWithMultipleStatementsAndMultipleNewlineSeparator() throws Exception { + databasePopulator.addScript(defaultSchema()); + databasePopulator.addScript(resource("db-test-data-multi-newline.sql")); + databasePopulator.setSeparator("\n\n"); + DatabasePopulatorUtils.execute(databasePopulator, db); + assertThat(jdbcTemplate.queryForObject(COUNT_KEITH_SQL, Integer.class)).isEqualTo(1); + assertThat(jdbcTemplate.queryForObject(COUNT_DAVE_SQL, Integer.class)).isEqualTo(1); + } + + @Test + void scriptWithEolBetweenTokens() throws Exception { + databasePopulator.addScript(usersSchema()); + databasePopulator.addScript(resource("users-data.sql")); + DatabasePopulatorUtils.execute(databasePopulator, db); + assertUsersDatabaseCreated("Brannen"); + } + + @Test + void scriptWithCommentsWithinStatements() throws Exception { + databasePopulator.addScript(usersSchema()); + databasePopulator.addScript(resource("users-data-with-comments.sql")); + DatabasePopulatorUtils.execute(databasePopulator, db); + assertUsersDatabaseCreated("Brannen", "Hoeller"); + } + + @Test + void scriptWithoutStatementSeparator() throws Exception { + databasePopulator.setSeparator(ScriptUtils.EOF_STATEMENT_SEPARATOR); + databasePopulator.addScript(resource("drop-users-schema.sql")); + databasePopulator.addScript(resource("users-schema-without-separator.sql")); + databasePopulator.addScript(resource("users-data-without-separator.sql")); + DatabasePopulatorUtils.execute(databasePopulator, db); + + assertUsersDatabaseCreated("Brannen"); + } + + @Test + void constructorWithMultipleScriptResources() throws Exception { + final ResourceDatabasePopulator populator = new ResourceDatabasePopulator(usersSchema(), + resource("users-data-with-comments.sql")); + DatabasePopulatorUtils.execute(populator, db); + assertUsersDatabaseCreated("Brannen", "Hoeller"); + } + + @Test + void scriptWithSelectStatements() throws Exception { + databasePopulator.addScript(defaultSchema()); + databasePopulator.addScript(resource("db-test-data-select.sql")); + DatabasePopulatorUtils.execute(databasePopulator, db); + assertThat(jdbcTemplate.queryForObject(COUNT_KEITH_SQL, Integer.class)).isEqualTo(1); + assertThat(jdbcTemplate.queryForObject(COUNT_DAVE_SQL, Integer.class)).isEqualTo(1); + } + + /** + * See SPR-9457 + */ + @Test + void usesBoundConnectionIfAvailable() throws SQLException { + TransactionSynchronizationManager.initSynchronization(); + Connection connection = DataSourceUtils.getConnection(db); + DatabasePopulator populator = mock(DatabasePopulator.class); + DatabasePopulatorUtils.execute(populator, db); + verify(populator).populate(connection); + } + + /** + * See SPR-9781 + */ + @Test + @Timeout(1) + void executesHugeScriptInReasonableTime() throws SQLException { + databasePopulator.addScript(defaultSchema()); + databasePopulator.addScript(resource("db-test-data-huge.sql")); + DatabasePopulatorUtils.execute(databasePopulator, db); + } + + private void assertTestDatabaseCreated() { + assertTestDatabaseCreated("Keith"); + } + + private void assertTestDatabaseCreated(String name) { + assertThat(jdbcTemplate.queryForObject("select NAME from T_TEST", String.class)).isEqualTo(name); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/CompositeDatabasePopulatorTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/CompositeDatabasePopulatorTests.java new file mode 100644 index 0000000..d24c7df --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/CompositeDatabasePopulatorTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.init; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Unit tests for {@link CompositeDatabasePopulator}. + * + * @author Kazuki Shimizu + * @author Juergen Hoeller + * @since 4.3 + */ +public class CompositeDatabasePopulatorTests { + + private final Connection mockedConnection = mock(Connection.class); + + private final DatabasePopulator mockedDatabasePopulator1 = mock(DatabasePopulator.class); + + private final DatabasePopulator mockedDatabasePopulator2 = mock(DatabasePopulator.class); + + + @Test + public void addPopulators() throws SQLException { + CompositeDatabasePopulator populator = new CompositeDatabasePopulator(); + populator.addPopulators(mockedDatabasePopulator1, mockedDatabasePopulator2); + populator.populate(mockedConnection); + verify(mockedDatabasePopulator1,times(1)).populate(mockedConnection); + verify(mockedDatabasePopulator2, times(1)).populate(mockedConnection); + } + + @Test + public void setPopulatorsWithMultiple() throws SQLException { + CompositeDatabasePopulator populator = new CompositeDatabasePopulator(); + populator.setPopulators(mockedDatabasePopulator1, mockedDatabasePopulator2); // multiple + populator.populate(mockedConnection); + verify(mockedDatabasePopulator1, times(1)).populate(mockedConnection); + verify(mockedDatabasePopulator2, times(1)).populate(mockedConnection); + } + + @Test + public void setPopulatorsForOverride() throws SQLException { + CompositeDatabasePopulator populator = new CompositeDatabasePopulator(); + populator.setPopulators(mockedDatabasePopulator1); + populator.setPopulators(mockedDatabasePopulator2); // override + populator.populate(mockedConnection); + verify(mockedDatabasePopulator1, times(0)).populate(mockedConnection); + verify(mockedDatabasePopulator2, times(1)).populate(mockedConnection); + } + + @Test + public void constructWithVarargs() throws SQLException { + CompositeDatabasePopulator populator = + new CompositeDatabasePopulator(mockedDatabasePopulator1, mockedDatabasePopulator2); + populator.populate(mockedConnection); + verify(mockedDatabasePopulator1, times(1)).populate(mockedConnection); + verify(mockedDatabasePopulator2, times(1)).populate(mockedConnection); + } + + @Test + public void constructWithCollection() throws SQLException { + Set populators = new LinkedHashSet<>(); + populators.add(mockedDatabasePopulator1); + populators.add(mockedDatabasePopulator2); + CompositeDatabasePopulator populator = new CompositeDatabasePopulator(populators); + populator.populate(mockedConnection); + verify(mockedDatabasePopulator1, times(1)).populate(mockedConnection); + verify(mockedDatabasePopulator2, times(1)).populate(mockedConnection); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/H2DatabasePopulatorTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/H2DatabasePopulatorTests.java new file mode 100644 index 0000000..e61ebab --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/H2DatabasePopulatorTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.init; + +import org.junit.jupiter.api.Test; + +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Sam Brannen + * @since 4.0.3 + */ +class H2DatabasePopulatorTests extends AbstractDatabasePopulatorTests { + + @Override + protected EmbeddedDatabaseType getEmbeddedDatabaseType() { + return EmbeddedDatabaseType.H2; + } + + /** + * https://jira.spring.io/browse/SPR-15896 + * + * @since 5.0 + */ + @Test + void scriptWithH2Alias() throws Exception { + databasePopulator.addScript(usersSchema()); + databasePopulator.addScript(resource("db-test-data-h2-alias.sql")); + // Set statement separator to double newline so that ";" is not + // considered a statement separator within the source code of the + // aliased function 'REVERSE'. + databasePopulator.setSeparator("\n\n"); + DatabasePopulatorUtils.execute(databasePopulator, db); + String sql = "select REVERSE(first_name) from users where last_name='Brannen'"; + assertThat(jdbcTemplate.queryForObject(sql, String.class)).isEqualTo("maS"); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/HsqlDatabasePopulatorTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/HsqlDatabasePopulatorTests.java new file mode 100644 index 0000000..0b43cd7 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/HsqlDatabasePopulatorTests.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2014 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.init; + +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; + +/** + * @author Sam Brannen + * @since 4.0.3 + */ +public class HsqlDatabasePopulatorTests extends AbstractDatabasePopulatorTests { + + @Override + protected EmbeddedDatabaseType getEmbeddedDatabaseType() { + return EmbeddedDatabaseType.HSQL; + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ResourceDatabasePopulatorTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ResourceDatabasePopulatorTests.java new file mode 100644 index 0000000..deca141 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ResourceDatabasePopulatorTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.init; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.core.io.Resource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Unit tests for {@link ResourceDatabasePopulator}. + * + * @author Sam Brannen + * @since 4.1 + * @see AbstractDatabasePopulatorTests + */ +public class ResourceDatabasePopulatorTests { + + private static final Resource script1 = Mockito.mock(Resource.class); + private static final Resource script2 = Mockito.mock(Resource.class); + private static final Resource script3 = Mockito.mock(Resource.class); + + + @Test + public void constructWithNullResource() { + assertThatIllegalArgumentException().isThrownBy(() -> + new ResourceDatabasePopulator((Resource) null)); + } + + @Test + public void constructWithNullResourceArray() { + assertThatIllegalArgumentException().isThrownBy(() -> + new ResourceDatabasePopulator((Resource[]) null)); + } + + @Test + public void constructWithResource() { + ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator(script1); + assertThat(databasePopulator.scripts.size()).isEqualTo(1); + } + + @Test + public void constructWithMultipleResources() { + ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator(script1, script2); + assertThat(databasePopulator.scripts.size()).isEqualTo(2); + } + + @Test + public void constructWithMultipleResourcesAndThenAddScript() { + ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator(script1, script2); + assertThat(databasePopulator.scripts.size()).isEqualTo(2); + + databasePopulator.addScript(script3); + assertThat(databasePopulator.scripts.size()).isEqualTo(3); + } + + @Test + public void addScriptsWithNullResource() { + ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator(); + assertThatIllegalArgumentException().isThrownBy(() -> + databasePopulator.addScripts((Resource) null)); + } + + @Test + public void addScriptsWithNullResourceArray() { + ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator(); + assertThatIllegalArgumentException().isThrownBy(() -> + databasePopulator.addScripts((Resource[]) null)); + } + + @Test + public void setScriptsWithNullResource() { + ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator(); + assertThatIllegalArgumentException().isThrownBy(() -> + databasePopulator.setScripts((Resource) null)); + } + + @Test + public void setScriptsWithNullResourceArray() { + ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator(); + assertThatIllegalArgumentException().isThrownBy(() -> + databasePopulator.setScripts((Resource[]) null)); + } + + @Test + public void setScriptsAndThenAddScript() { + ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator(); + assertThat(databasePopulator.scripts.size()).isEqualTo(0); + + databasePopulator.setScripts(script1, script2); + assertThat(databasePopulator.scripts.size()).isEqualTo(2); + + databasePopulator.addScript(script3); + assertThat(databasePopulator.scripts.size()).isEqualTo(3); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ScriptUtilsIntegrationTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ScriptUtilsIntegrationTests.java new file mode 100644 index 0000000..541611a --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ScriptUtilsIntegrationTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.init; + +import java.sql.SQLException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; + +import static org.springframework.jdbc.datasource.init.ScriptUtils.executeSqlScript; + +/** + * Integration tests for {@link ScriptUtils}. + * + * @author Sam Brannen + * @since 4.0.3 + * @see ScriptUtilsUnitTests + */ +public class ScriptUtilsIntegrationTests extends AbstractDatabaseInitializationTests { + + @Override + protected EmbeddedDatabaseType getEmbeddedDatabaseType() { + return EmbeddedDatabaseType.HSQL; + } + + @BeforeEach + public void setUpSchema() throws SQLException { + executeSqlScript(db.getConnection(), usersSchema()); + } + + @Test + public void executeSqlScriptContainingMultiLineComments() throws SQLException { + executeSqlScript(db.getConnection(), resource("test-data-with-multi-line-comments.sql")); + assertUsersDatabaseCreated("Hoeller", "Brannen"); + } + + /** + * @since 4.2 + */ + @Test + public void executeSqlScriptContainingSingleQuotesNestedInsideDoubleQuotes() throws SQLException { + executeSqlScript(db.getConnection(), resource("users-data-with-single-quotes-nested-in-double-quotes.sql")); + assertUsersDatabaseCreated("Hoeller", "Brannen"); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ScriptUtilsUnitTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ScriptUtilsUnitTests.java new file mode 100644 index 0000000..f57adec --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ScriptUtilsUnitTests.java @@ -0,0 +1,186 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.init; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.support.EncodedResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.jdbc.datasource.init.ScriptUtils.DEFAULT_BLOCK_COMMENT_END_DELIMITER; +import static org.springframework.jdbc.datasource.init.ScriptUtils.DEFAULT_BLOCK_COMMENT_START_DELIMITER; +import static org.springframework.jdbc.datasource.init.ScriptUtils.DEFAULT_COMMENT_PREFIXES; +import static org.springframework.jdbc.datasource.init.ScriptUtils.DEFAULT_STATEMENT_SEPARATOR; +import static org.springframework.jdbc.datasource.init.ScriptUtils.containsSqlScriptDelimiters; +import static org.springframework.jdbc.datasource.init.ScriptUtils.splitSqlScript; + +/** + * Unit tests for {@link ScriptUtils}. + * + * @author Thomas Risberg + * @author Sam Brannen + * @author Phillip Webb + * @author Chris Baldwin + * @author Nicolas Debeissat + * @since 4.0.3 + * @see ScriptUtilsIntegrationTests + */ +public class ScriptUtilsUnitTests { + + @Test + public void splitSqlScriptDelimitedWithSemicolon() { + String rawStatement1 = "insert into customer (id, name)\nvalues (1, 'Rod ; Johnson'), (2, 'Adrian \n Collier')"; + String cleanedStatement1 = "insert into customer (id, name) values (1, 'Rod ; Johnson'), (2, 'Adrian \n Collier')"; + String rawStatement2 = "insert into orders(id, order_date, customer_id)\nvalues (1, '2008-01-02', 2)"; + String cleanedStatement2 = "insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2)"; + String rawStatement3 = "insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2)"; + String cleanedStatement3 = "insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2)"; + char delim = ';'; + String script = rawStatement1 + delim + rawStatement2 + delim + rawStatement3 + delim; + List statements = new ArrayList<>(); + splitSqlScript(script, delim, statements); + assertThat(statements).containsExactly(cleanedStatement1, cleanedStatement2, cleanedStatement3); + } + + @Test + public void splitSqlScriptDelimitedWithNewLine() { + String statement1 = "insert into customer (id, name) values (1, 'Rod ; Johnson'), (2, 'Adrian \n Collier')"; + String statement2 = "insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2)"; + String statement3 = "insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2)"; + char delim = '\n'; + String script = statement1 + delim + statement2 + delim + statement3 + delim; + List statements = new ArrayList<>(); + splitSqlScript(script, delim, statements); + assertThat(statements).containsExactly(statement1, statement2, statement3); + } + + @Test + public void splitSqlScriptDelimitedWithNewLineButDefaultDelimiterSpecified() { + String statement1 = "do something"; + String statement2 = "do something else"; + char delim = '\n'; + String script = statement1 + delim + statement2 + delim; + List statements = new ArrayList<>(); + splitSqlScript(script, DEFAULT_STATEMENT_SEPARATOR, statements); + assertThat(statements).as("stripped but not split statements").containsExactly(script.replace('\n', ' ')); + } + + @Test // SPR-13218 + public void splitScriptWithSingleQuotesNestedInsideDoubleQuotes() throws Exception { + String statement1 = "select '1' as \"Dogbert's owner's\" from dual"; + String statement2 = "select '2' as \"Dilbert's\" from dual"; + char delim = ';'; + String script = statement1 + delim + statement2 + delim; + List statements = new ArrayList<>(); + splitSqlScript(script, ';', statements); + assertThat(statements).containsExactly(statement1, statement2); + } + + @Test // SPR-11560 + public void readAndSplitScriptWithMultipleNewlinesAsSeparator() throws Exception { + String script = readScript("db-test-data-multi-newline.sql"); + List statements = new ArrayList<>(); + splitSqlScript(script, "\n\n", statements); + String statement1 = "insert into T_TEST (NAME) values ('Keith')"; + String statement2 = "insert into T_TEST (NAME) values ('Dave')"; + assertThat(statements).containsExactly(statement1, statement2); + } + + @Test + public void readAndSplitScriptContainingComments() throws Exception { + String script = readScript("test-data-with-comments.sql"); + splitScriptContainingComments(script, DEFAULT_COMMENT_PREFIXES); + } + + @Test + public void readAndSplitScriptContainingCommentsWithWindowsLineEnding() throws Exception { + String script = readScript("test-data-with-comments.sql").replaceAll("\n", "\r\n"); + splitScriptContainingComments(script, DEFAULT_COMMENT_PREFIXES); + } + + @Test + public void readAndSplitScriptContainingCommentsWithMultiplePrefixes() throws Exception { + String script = readScript("test-data-with-multi-prefix-comments.sql"); + splitScriptContainingComments(script, "--", "#", "^"); + } + + private void splitScriptContainingComments(String script, String... commentPrefixes) throws Exception { + List statements = new ArrayList<>(); + splitSqlScript(null, script, ";", commentPrefixes, DEFAULT_BLOCK_COMMENT_START_DELIMITER, + DEFAULT_BLOCK_COMMENT_END_DELIMITER, statements); + String statement1 = "insert into customer (id, name) values (1, 'Rod; Johnson'), (2, 'Adrian Collier')"; + String statement2 = "insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2)"; + String statement3 = "insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2)"; + // Statement 4 addresses the error described in SPR-9982. + String statement4 = "INSERT INTO persons( person_id , name) VALUES( 1 , 'Name' )"; + assertThat(statements).containsExactly(statement1, statement2, statement3, statement4); + } + + @Test // SPR-10330 + public void readAndSplitScriptContainingCommentsWithLeadingTabs() throws Exception { + String script = readScript("test-data-with-comments-and-leading-tabs.sql"); + List statements = new ArrayList<>(); + splitSqlScript(script, ';', statements); + String statement1 = "insert into customer (id, name) values (1, 'Sam Brannen')"; + String statement2 = "insert into orders(id, order_date, customer_id) values (1, '2013-06-08', 1)"; + String statement3 = "insert into orders(id, order_date, customer_id) values (2, '2013-06-08', 1)"; + assertThat(statements).containsExactly(statement1, statement2, statement3); + } + + @Test // SPR-9531 + public void readAndSplitScriptContainingMultiLineComments() throws Exception { + String script = readScript("test-data-with-multi-line-comments.sql"); + List statements = new ArrayList<>(); + splitSqlScript(script, ';', statements); + String statement1 = "INSERT INTO users(first_name, last_name) VALUES('Juergen', 'Hoeller')"; + String statement2 = "INSERT INTO users(first_name, last_name) VALUES( 'Sam' , 'Brannen' )"; + assertThat(statements).containsExactly(statement1, statement2); + } + + @Test + public void readAndSplitScriptContainingMultiLineNestedComments() throws Exception { + String script = readScript("test-data-with-multi-line-nested-comments.sql"); + List statements = new ArrayList<>(); + splitSqlScript(script, ';', statements); + String statement1 = "INSERT INTO users(first_name, last_name) VALUES('Juergen', 'Hoeller')"; + String statement2 = "INSERT INTO users(first_name, last_name) VALUES( 'Sam' , 'Brannen' )"; + assertThat(statements).containsExactly(statement1, statement2); + } + + @Test + public void containsDelimiters() { + assertThat(containsSqlScriptDelimiters("select 1\n select ';'", ";")).isFalse(); + assertThat(containsSqlScriptDelimiters("select 1; select 2", ";")).isTrue(); + assertThat(containsSqlScriptDelimiters("select 1; select '\\n\n';", "\n")).isFalse(); + assertThat(containsSqlScriptDelimiters("select 1\n select 2", "\n")).isTrue(); + assertThat(containsSqlScriptDelimiters("select 1\n select 2", "\n\n")).isFalse(); + assertThat(containsSqlScriptDelimiters("select 1\n\n select 2", "\n\n")).isTrue(); + // MySQL style escapes '\\' + assertThat(containsSqlScriptDelimiters("insert into users(first_name, last_name)\nvalues('a\\\\', 'b;')", ";")).isFalse(); + assertThat(containsSqlScriptDelimiters("insert into users(first_name, last_name)\nvalues('Charles', 'd\\'Artagnan'); select 1;", ";")).isTrue(); + } + + private String readScript(String path) throws Exception { + EncodedResource resource = new EncodedResource(new ClassPathResource(path, getClass())); + return ScriptUtils.readScript(resource); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/lookup/BeanFactoryDataSourceLookupTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/lookup/BeanFactoryDataSourceLookupTests.java new file mode 100644 index 0000000..5c6a203 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/lookup/BeanFactoryDataSourceLookupTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.lookup; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanNotOfRequiredTypeException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * @author Rick Evans + * @author Juergen Hoeller + * @author Chris Beams + */ +public class BeanFactoryDataSourceLookupTests { + + private static final String DATASOURCE_BEAN_NAME = "dataSource"; + + + @Test + public void testLookupSunnyDay() { + BeanFactory beanFactory = mock(BeanFactory.class); + + StubDataSource expectedDataSource = new StubDataSource(); + given(beanFactory.getBean(DATASOURCE_BEAN_NAME, DataSource.class)).willReturn(expectedDataSource); + + BeanFactoryDataSourceLookup lookup = new BeanFactoryDataSourceLookup(); + lookup.setBeanFactory(beanFactory); + DataSource dataSource = lookup.getDataSource(DATASOURCE_BEAN_NAME); + assertThat(dataSource).as("A DataSourceLookup implementation must *never* return null from " + + "getDataSource(): this one obviously (and incorrectly) is").isNotNull(); + assertThat(dataSource).isSameAs(expectedDataSource); + } + + @Test + public void testLookupWhereBeanFactoryYieldsNonDataSourceType() throws Exception { + final BeanFactory beanFactory = mock(BeanFactory.class); + + given(beanFactory.getBean(DATASOURCE_BEAN_NAME, DataSource.class)).willThrow( + new BeanNotOfRequiredTypeException(DATASOURCE_BEAN_NAME, + DataSource.class, String.class)); + + BeanFactoryDataSourceLookup lookup = new BeanFactoryDataSourceLookup(beanFactory); + assertThatExceptionOfType(DataSourceLookupFailureException.class).isThrownBy(() -> + lookup.getDataSource(DATASOURCE_BEAN_NAME)); + } + + @Test + public void testLookupWhereBeanFactoryHasNotBeenSupplied() throws Exception { + BeanFactoryDataSourceLookup lookup = new BeanFactoryDataSourceLookup(); + assertThatIllegalStateException().isThrownBy(() -> + lookup.getDataSource(DATASOURCE_BEAN_NAME)); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/lookup/JndiDataSourceLookupTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/lookup/JndiDataSourceLookupTests.java new file mode 100644 index 0000000..741a100 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/lookup/JndiDataSourceLookupTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.lookup; + +import javax.naming.NamingException; +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Rick Evans + * @author Chris Beams + */ +public class JndiDataSourceLookupTests { + + private static final String DATA_SOURCE_NAME = "Love is like a stove, burns you when it's hot"; + + @Test + public void testSunnyDay() throws Exception { + final DataSource expectedDataSource = new StubDataSource(); + JndiDataSourceLookup lookup = new JndiDataSourceLookup() { + @Override + protected T lookup(String jndiName, Class requiredType) { + assertThat(jndiName).isEqualTo(DATA_SOURCE_NAME); + return requiredType.cast(expectedDataSource); + } + }; + DataSource dataSource = lookup.getDataSource(DATA_SOURCE_NAME); + assertThat(dataSource).as("A DataSourceLookup implementation must *never* return null from getDataSource(): this one obviously (and incorrectly) is").isNotNull(); + assertThat(dataSource).isSameAs(expectedDataSource); + } + + @Test + public void testNoDataSourceAtJndiLocation() throws Exception { + JndiDataSourceLookup lookup = new JndiDataSourceLookup() { + @Override + protected T lookup(String jndiName, Class requiredType) throws NamingException { + assertThat(jndiName).isEqualTo(DATA_SOURCE_NAME); + throw new NamingException(); + } + }; + assertThatExceptionOfType(DataSourceLookupFailureException.class).isThrownBy(() -> + lookup.getDataSource(DATA_SOURCE_NAME)); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/lookup/MapDataSourceLookupTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/lookup/MapDataSourceLookupTests.java new file mode 100644 index 0000000..39b7b6d --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/lookup/MapDataSourceLookupTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.lookup; + +import java.util.HashMap; +import java.util.Map; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Rick Evans + * @author Chris Beams + */ +public class MapDataSourceLookupTests { + + private static final String DATA_SOURCE_NAME = "dataSource"; + + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void getDataSourcesReturnsUnmodifiableMap() throws Exception { + MapDataSourceLookup lookup = new MapDataSourceLookup(); + Map dataSources = lookup.getDataSources(); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + dataSources.put("", "")); + } + + @Test + public void lookupSunnyDay() throws Exception { + Map dataSources = new HashMap<>(); + StubDataSource expectedDataSource = new StubDataSource(); + dataSources.put(DATA_SOURCE_NAME, expectedDataSource); + MapDataSourceLookup lookup = new MapDataSourceLookup(); + lookup.setDataSources(dataSources); + DataSource dataSource = lookup.getDataSource(DATA_SOURCE_NAME); + assertThat(dataSource).as("A DataSourceLookup implementation must *never* return null from getDataSource(): this one obviously (and incorrectly) is").isNotNull(); + assertThat(dataSource).isSameAs(expectedDataSource); + } + + @Test + public void setDataSourcesIsAnIdempotentOperation() throws Exception { + Map dataSources = new HashMap<>(); + StubDataSource expectedDataSource = new StubDataSource(); + dataSources.put(DATA_SOURCE_NAME, expectedDataSource); + MapDataSourceLookup lookup = new MapDataSourceLookup(); + lookup.setDataSources(dataSources); + lookup.setDataSources(null); // must be idempotent (i.e. the following lookup must still work); + DataSource dataSource = lookup.getDataSource(DATA_SOURCE_NAME); + assertThat(dataSource).as("A DataSourceLookup implementation must *never* return null from getDataSource(): this one obviously (and incorrectly) is").isNotNull(); + assertThat(dataSource).isSameAs(expectedDataSource); + } + + @Test + public void addingDataSourcePermitsOverride() throws Exception { + Map dataSources = new HashMap<>(); + StubDataSource overridenDataSource = new StubDataSource(); + StubDataSource expectedDataSource = new StubDataSource(); + dataSources.put(DATA_SOURCE_NAME, overridenDataSource); + MapDataSourceLookup lookup = new MapDataSourceLookup(); + lookup.setDataSources(dataSources); + lookup.addDataSource(DATA_SOURCE_NAME, expectedDataSource); // must override existing entry + DataSource dataSource = lookup.getDataSource(DATA_SOURCE_NAME); + assertThat(dataSource).as("A DataSourceLookup implementation must *never* return null from getDataSource(): this one obviously (and incorrectly) is").isNotNull(); + assertThat(dataSource).isSameAs(expectedDataSource); + } + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void getDataSourceWhereSuppliedMapHasNonDataSourceTypeUnderSpecifiedKey() throws Exception { + Map dataSources = new HashMap(); + dataSources.put(DATA_SOURCE_NAME, new Object()); + MapDataSourceLookup lookup = new MapDataSourceLookup(dataSources); + + assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> + lookup.getDataSource(DATA_SOURCE_NAME)); + } + + @Test + public void getDataSourceWhereSuppliedMapHasNoEntryForSpecifiedKey() throws Exception { + MapDataSourceLookup lookup = new MapDataSourceLookup(); + + assertThatExceptionOfType(DataSourceLookupFailureException.class).isThrownBy(() -> + lookup.getDataSource(DATA_SOURCE_NAME)); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/lookup/StubDataSource.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/lookup/StubDataSource.java new file mode 100644 index 0000000..9db40f2 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/lookup/StubDataSource.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.datasource.lookup; + +import java.sql.Connection; +import java.sql.SQLException; + +import org.springframework.jdbc.datasource.AbstractDataSource; + +/** + * Stub, do-nothing DataSource implementation. + * + *

    All methods throw {@link UnsupportedOperationException}. + * + * @author Rick Evans + */ +class StubDataSource extends AbstractDataSource { + + @Override + public Connection getConnection() throws SQLException { + throw new UnsupportedOperationException(); + } + + @Override + public Connection getConnection(String username, String password) throws SQLException { + throw new UnsupportedOperationException(); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/object/BatchSqlUpdateTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/object/BatchSqlUpdateTests.java new file mode 100644 index 0000000..6ea55b5 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/object/BatchSqlUpdateTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.object; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.Types; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; + +import org.springframework.jdbc.core.SqlParameter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author Juergen Hoeller + * @since 22.02.2005 + */ +public class BatchSqlUpdateTests { + + @Test + public void testBatchUpdateWithExplicitFlush() throws Exception { + doTestBatchUpdate(false); + } + + @Test + public void testBatchUpdateWithFlushThroughBatchSize() throws Exception { + doTestBatchUpdate(true); + } + + private void doTestBatchUpdate(boolean flushThroughBatchSize) throws Exception { + final String sql = "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?"; + final int[] ids = new int[] { 100, 200 }; + final int[] rowsAffected = new int[] { 1, 2 }; + + Connection connection = mock(Connection.class); + DataSource dataSource = mock(DataSource.class); + given(dataSource.getConnection()).willReturn(connection); + PreparedStatement preparedStatement = mock(PreparedStatement.class); + given(preparedStatement.getConnection()).willReturn(connection); + given(preparedStatement.executeBatch()).willReturn(rowsAffected); + + DatabaseMetaData mockDatabaseMetaData = mock(DatabaseMetaData.class); + given(mockDatabaseMetaData.supportsBatchUpdates()).willReturn(true); + given(connection.prepareStatement(sql)).willReturn(preparedStatement); + given(connection.getMetaData()).willReturn(mockDatabaseMetaData); + + BatchSqlUpdate update = new BatchSqlUpdate(dataSource, sql); + update.declareParameter(new SqlParameter(Types.INTEGER)); + if (flushThroughBatchSize) { + update.setBatchSize(2); + } + + update.update(ids[0]); + update.update(ids[1]); + + if (flushThroughBatchSize) { + assertThat(update.getQueueCount()).isEqualTo(0); + assertThat(update.getRowsAffected().length).isEqualTo(2); + } + else { + assertThat(update.getQueueCount()).isEqualTo(2); + assertThat(update.getRowsAffected().length).isEqualTo(0); + } + + int[] actualRowsAffected = update.flush(); + assertThat(update.getQueueCount()).isEqualTo(0); + + if (flushThroughBatchSize) { + assertThat(actualRowsAffected.length == 0).as("flush did not execute updates").isTrue(); + } + else { + assertThat(actualRowsAffected.length == 2).as("executed 2 updates").isTrue(); + assertThat(actualRowsAffected[0]).isEqualTo(rowsAffected[0]); + assertThat(actualRowsAffected[1]).isEqualTo(rowsAffected[1]); + } + + actualRowsAffected = update.getRowsAffected(); + assertThat(actualRowsAffected.length == 2).as("executed 2 updates").isTrue(); + assertThat(actualRowsAffected[0]).isEqualTo(rowsAffected[0]); + assertThat(actualRowsAffected[1]).isEqualTo(rowsAffected[1]); + + update.reset(); + assertThat(update.getRowsAffected().length).isEqualTo(0); + + verify(preparedStatement).setObject(1, ids[0], Types.INTEGER); + verify(preparedStatement).setObject(1, ids[1], Types.INTEGER); + verify(preparedStatement, times(2)).addBatch(); + verify(preparedStatement).close(); + } +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/object/CustomerMapper.java b/spring-jdbc/src/test/java/org/springframework/jdbc/object/CustomerMapper.java new file mode 100644 index 0000000..87251de --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/object/CustomerMapper.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.object; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.springframework.jdbc.Customer; +import org.springframework.jdbc.core.RowMapper; + +public class CustomerMapper implements RowMapper { + + private static final String[] COLUMN_NAMES = new String[] {"id", "forename"}; + + @Override + public Customer mapRow(ResultSet rs, int rownum) throws SQLException { + Customer cust = new Customer(); + cust.setId(rs.getInt(COLUMN_NAMES[0])); + cust.setForename(rs.getString(COLUMN_NAMES[1])); + return cust; + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/object/GenericSqlQueryTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/object/GenericSqlQueryTests.java new file mode 100644 index 0000000..62ee268 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/object/GenericSqlQueryTests.java @@ -0,0 +1,124 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.object; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.core.io.ClassPathResource; +import org.springframework.jdbc.Customer; +import org.springframework.jdbc.datasource.TestDataSourceWrapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author Thomas Risberg + * @author Juergen Hoeller + */ +public class GenericSqlQueryTests { + + private static final String SELECT_ID_FORENAME_NAMED_PARAMETERS_PARSED = + "select id, forename from custmr where id = ? and country = ?"; + + private DefaultListableBeanFactory beanFactory; + + private Connection connection; + + private PreparedStatement preparedStatement; + + private ResultSet resultSet; + + + @BeforeEach + public void setUp() throws Exception { + this.beanFactory = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(this.beanFactory).loadBeanDefinitions( + new ClassPathResource("org/springframework/jdbc/object/GenericSqlQueryTests-context.xml")); + DataSource dataSource = mock(DataSource.class); + this.connection = mock(Connection.class); + this.preparedStatement = mock(PreparedStatement.class); + this.resultSet = mock(ResultSet.class); + given(dataSource.getConnection()).willReturn(connection); + TestDataSourceWrapper testDataSource = (TestDataSourceWrapper) beanFactory.getBean("dataSource"); + testDataSource.setTarget(dataSource); + } + + @Test + public void testCustomerQueryWithPlaceholders() throws SQLException { + SqlQuery query = (SqlQuery) beanFactory.getBean("queryWithPlaceholders"); + doTestCustomerQuery(query, false); + } + + @Test + public void testCustomerQueryWithNamedParameters() throws SQLException { + SqlQuery query = (SqlQuery) beanFactory.getBean("queryWithNamedParameters"); + doTestCustomerQuery(query, true); + } + + @Test + public void testCustomerQueryWithRowMapperInstance() throws SQLException { + SqlQuery query = (SqlQuery) beanFactory.getBean("queryWithRowMapperBean"); + doTestCustomerQuery(query, true); + } + + private void doTestCustomerQuery(SqlQuery query, boolean namedParameters) throws SQLException { + given(resultSet.next()).willReturn(true); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + given(resultSet.next()).willReturn(true, false); + given(preparedStatement.executeQuery()).willReturn(resultSet); + given(connection.prepareStatement(SELECT_ID_FORENAME_NAMED_PARAMETERS_PARSED)).willReturn(preparedStatement); + + List queryResults; + if (namedParameters) { + Map params = new HashMap<>(2); + params.put("id", 1); + params.put("country", "UK"); + queryResults = query.executeByNamedParam(params); + } + else { + Object[] params = new Object[] {1, "UK"}; + queryResults = query.execute(params); + } + assertThat(queryResults.size() == 1).as("Customer was returned correctly").isTrue(); + Customer cust = (Customer) queryResults.get(0); + assertThat(cust.getId() == 1).as("Customer id was assigned correctly").isTrue(); + assertThat(cust.getForename().equals("rod")).as("Customer forename was assigned correctly").isTrue(); + + verify(resultSet).close(); + verify(preparedStatement).setObject(1, 1, Types.INTEGER); + verify(preparedStatement).setString(2, "UK"); + verify(preparedStatement).close(); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/object/GenericStoredProcedureTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/object/GenericStoredProcedureTests.java new file mode 100644 index 0000000..149c792 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/object/GenericStoredProcedureTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.object; + +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.Types; +import java.util.HashMap; +import java.util.Map; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.core.io.ClassPathResource; +import org.springframework.jdbc.datasource.TestDataSourceWrapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author Thomas Risberg + */ +public class GenericStoredProcedureTests { + + @Test + public void testAddInvoices() throws Exception { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + new XmlBeanDefinitionReader(bf).loadBeanDefinitions( + new ClassPathResource("org/springframework/jdbc/object/GenericStoredProcedureTests-context.xml")); + Connection connection = mock(Connection.class); + DataSource dataSource = mock(DataSource.class); + given(dataSource.getConnection()).willReturn(connection); + CallableStatement callableStatement = mock(CallableStatement.class); + TestDataSourceWrapper testDataSource = (TestDataSourceWrapper) bf.getBean("dataSource"); + testDataSource.setTarget(dataSource); + + given(callableStatement.execute()).willReturn(false); + given(callableStatement.getUpdateCount()).willReturn(-1); + given(callableStatement.getObject(3)).willReturn(4); + + given(connection.prepareCall("{call " + "add_invoice" + "(?, ?, ?)}")).willReturn(callableStatement); + + StoredProcedure adder = (StoredProcedure) bf.getBean("genericProcedure"); + Map in = new HashMap<>(2); + in.put("amount", 1106); + in.put("custid", 3); + Map out = adder.execute(in); + Integer id = (Integer) out.get("newid"); + assertThat(id.intValue()).isEqualTo(4); + + verify(callableStatement).setObject(1, 1106, Types.INTEGER); + verify(callableStatement).setObject(2, 3, Types.INTEGER); + verify(callableStatement).registerOutParameter(3, Types.INTEGER); + verify(callableStatement).close(); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/object/RdbmsOperationTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/object/RdbmsOperationTests.java new file mode 100644 index 0000000..c91d7bd --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/object/RdbmsOperationTests.java @@ -0,0 +1,171 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.object; + +import java.sql.Types; +import java.util.HashMap; +import java.util.Map; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.SqlInOutParameter; +import org.springframework.jdbc.core.SqlOutParameter; +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.jdbc.datasource.DriverManagerDataSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Trevor Cook + * @author Juergen Hoeller + * @author Sam Brannen + */ +public class RdbmsOperationTests { + + private final TestRdbmsOperation operation = new TestRdbmsOperation(); + + + @Test + public void emptySql() { + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy( + operation::compile); + } + + @Test + public void setTypeAfterCompile() { + operation.setDataSource(new DriverManagerDataSource()); + operation.setSql("select * from mytable"); + operation.compile(); + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() -> + operation.setTypes(new int[] { Types.INTEGER })); + } + + @Test + public void declareParameterAfterCompile() { + operation.setDataSource(new DriverManagerDataSource()); + operation.setSql("select * from mytable"); + operation.compile(); + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() -> + operation.declareParameter(new SqlParameter(Types.INTEGER))); + } + + @Test + public void tooFewParameters() { + operation.setSql("select * from mytable"); + operation.setTypes(new int[] { Types.INTEGER }); + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() -> + operation.validateParameters((Object[]) null)); + } + + @Test + public void tooFewMapParameters() { + operation.setSql("select * from mytable"); + operation.setTypes(new int[] { Types.INTEGER }); + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() -> + operation.validateNamedParameters((Map) null)); + } + + @Test + public void operationConfiguredViaJdbcTemplateMustGetDataSource() throws Exception { + operation.setSql("foo"); + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() -> + operation.compile()) + .withMessageContaining("ataSource"); + } + + @Test + public void tooManyParameters() { + operation.setSql("select * from mytable"); + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() -> + operation.validateParameters(new Object[] { 1, 2 })); + } + + @Test + public void unspecifiedMapParameters() { + operation.setSql("select * from mytable"); + Map params = new HashMap<>(); + params.put("col1", "value"); + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() -> + operation.validateNamedParameters(params)); + } + + @Test + public void compileTwice() { + operation.setDataSource(new DriverManagerDataSource()); + operation.setSql("select * from mytable"); + operation.setTypes(null); + operation.compile(); + operation.compile(); + } + + @Test + public void emptyDataSource() { + SqlOperation operation = new SqlOperation() {}; + operation.setSql("select * from mytable"); + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy( + operation::compile); + } + + @Test + public void parameterPropagation() { + SqlOperation operation = new SqlOperation() {}; + DataSource ds = new DriverManagerDataSource(); + operation.setDataSource(ds); + operation.setFetchSize(10); + operation.setMaxRows(20); + JdbcTemplate jt = operation.getJdbcTemplate(); + assertThat(jt.getDataSource()).isEqualTo(ds); + assertThat(jt.getFetchSize()).isEqualTo(10); + assertThat(jt.getMaxRows()).isEqualTo(20); + } + + @Test + public void validateInOutParameter() { + operation.setDataSource(new DriverManagerDataSource()); + operation.setSql("DUMMY_PROC"); + operation.declareParameter(new SqlOutParameter("DUMMY_OUT_PARAM", Types.VARCHAR)); + operation.declareParameter(new SqlInOutParameter("DUMMY_IN_OUT_PARAM", Types.VARCHAR)); + operation.validateParameters(new Object[] {"DUMMY_VALUE1", "DUMMY_VALUE2"}); + } + + @Test + public void parametersSetWithList() { + DataSource ds = new DriverManagerDataSource(); + operation.setDataSource(ds); + operation.setSql("select * from mytable where one = ? and two = ?"); + operation.setParameters(new SqlParameter[] { + new SqlParameter("one", Types.NUMERIC), + new SqlParameter("two", Types.NUMERIC)}); + operation.afterPropertiesSet(); + operation.validateParameters(new Object[] { 1, "2" }); + assertThat(operation.getDeclaredParameters().size()).isEqualTo(2); + } + + + private static class TestRdbmsOperation extends RdbmsOperation { + + @Override + protected void compileInternal() { + } + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/object/SqlQueryTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/object/SqlQueryTests.java new file mode 100644 index 0000000..e6c95ee --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/object/SqlQueryTests.java @@ -0,0 +1,768 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.object; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.jdbc.Customer; +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author Trevor Cook + * @author Thomas Risberg + * @author Juergen Hoeller + */ +public class SqlQueryTests { + + //FIXME inline? + private static final String SELECT_ID = + "select id from custmr"; + private static final String SELECT_ID_WHERE = + "select id from custmr where forename = ? and id = ?"; + private static final String SELECT_FORENAME = + "select forename from custmr"; + private static final String SELECT_FORENAME_EMPTY = + "select forename from custmr WHERE 1 = 2"; + private static final String SELECT_ID_FORENAME_WHERE = + "select id, forename from prefix:custmr where forename = ?"; + private static final String SELECT_ID_FORENAME_NAMED_PARAMETERS = + "select id, forename from custmr where id = :id and country = :country"; + private static final String SELECT_ID_FORENAME_NAMED_PARAMETERS_PARSED = + "select id, forename from custmr where id = ? and country = ?"; + private static final String SELECT_ID_FORENAME_WHERE_ID_IN_LIST_1 = + "select id, forename from custmr where id in (?, ?)"; + private static final String SELECT_ID_FORENAME_WHERE_ID_IN_LIST_2 = + "select id, forename from custmr where id in (:ids)"; + private static final String SELECT_ID_FORENAME_WHERE_ID_REUSED_1 = + "select id, forename from custmr where id = ? or id = ?)"; + private static final String SELECT_ID_FORENAME_WHERE_ID_REUSED_2 = + "select id, forename from custmr where id = :id1 or id = :id1)"; + private static final String SELECT_ID_FORENAME_WHERE_ID = + "select id, forename from custmr where id <= ?"; + + private static final String[] COLUMN_NAMES = new String[] {"id", "forename"}; + private static final int[] COLUMN_TYPES = new int[] {Types.INTEGER, Types.VARCHAR}; + + private Connection connection; + private DataSource dataSource; + private PreparedStatement preparedStatement; + private ResultSet resultSet; + + + @BeforeEach + public void setUp() throws Exception { + this.connection = mock(Connection.class); + this.dataSource = mock(DataSource.class); + this.preparedStatement = mock(PreparedStatement.class); + this.resultSet = mock(ResultSet.class); + given(this.dataSource.getConnection()).willReturn(this.connection); + given(this.connection.prepareStatement(anyString())).willReturn(this.preparedStatement); + given(preparedStatement.executeQuery()).willReturn(resultSet); + } + + @Test + public void testQueryWithoutParams() throws SQLException { + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt(1)).willReturn(1); + + SqlQuery query = new MappingSqlQueryWithParameters() { + @Override + protected Integer mapRow(ResultSet rs, int rownum, @Nullable Object[] params, @Nullable Map context) + throws SQLException { + assertThat(params == null).as("params were null").isTrue(); + assertThat(context == null).as("context was null").isTrue(); + return rs.getInt(1); + } + }; + query.setDataSource(dataSource); + query.setSql(SELECT_ID); + query.compile(); + List list = query.execute(); + + assertThat(list).isEqualTo(Arrays.asList(1)); + verify(connection).prepareStatement(SELECT_ID); + verify(resultSet).close(); + verify(preparedStatement).close(); + } + + @Test + public void testQueryWithoutEnoughParams() { + MappingSqlQuery query = new MappingSqlQuery() { + @Override + protected Integer mapRow(ResultSet rs, int rownum) throws SQLException { + return rs.getInt(1); + } + }; + query.setDataSource(dataSource); + query.setSql(SELECT_ID_WHERE); + query.declareParameter(new SqlParameter(COLUMN_NAMES[0], COLUMN_TYPES[0])); + query.declareParameter(new SqlParameter(COLUMN_NAMES[1], COLUMN_TYPES[1])); + query.compile(); + + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy( + query::execute); + } + + @Test + public void testQueryWithMissingMapParams() { + MappingSqlQuery query = new MappingSqlQuery() { + @Override + protected Integer mapRow(ResultSet rs, int rownum) throws SQLException { + return rs.getInt(1); + } + }; + query.setDataSource(dataSource); + query.setSql(SELECT_ID_WHERE); + query.declareParameter(new SqlParameter(COLUMN_NAMES[0], COLUMN_TYPES[0])); + query.declareParameter(new SqlParameter(COLUMN_NAMES[1], COLUMN_TYPES[1])); + query.compile(); + + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() -> + query.executeByNamedParam(Collections.singletonMap(COLUMN_NAMES[0], "value"))); + } + + @Test + public void testStringQueryWithResults() throws Exception { + String[] dbResults = new String[] { "alpha", "beta", "charlie" }; + given(resultSet.next()).willReturn(true, true, true, false); + given(resultSet.getString(1)).willReturn(dbResults[0], dbResults[1], dbResults[2]); + StringQuery query = new StringQuery(dataSource, SELECT_FORENAME); + query.setRowsExpected(3); + String[] results = query.run(); + assertThat(results).isEqualTo(dbResults); + verify(connection).prepareStatement(SELECT_FORENAME); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testStringQueryWithoutResults() throws SQLException { + given(resultSet.next()).willReturn(false); + StringQuery query = new StringQuery(dataSource, SELECT_FORENAME_EMPTY); + String[] results = query.run(); + assertThat(results).isEqualTo(new String[0]); + verify(connection).prepareStatement(SELECT_FORENAME_EMPTY); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testFindCustomerIntInt() throws SQLException { + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + + class CustomerQuery extends MappingSqlQuery { + + public CustomerQuery(DataSource ds) { + super(ds, SELECT_ID_WHERE); + declareParameter(new SqlParameter(Types.NUMERIC)); + declareParameter(new SqlParameter(Types.NUMERIC)); + compile(); + } + + @Override + protected Customer mapRow(ResultSet rs, int rownum) throws SQLException { + Customer cust = new Customer(); + cust.setId(rs.getInt(COLUMN_NAMES[0])); + cust.setForename(rs.getString(COLUMN_NAMES[1])); + return cust; + } + + public Customer findCustomer(int id, int otherNum) { + return findObject(id, otherNum); + } + } + + CustomerQuery query = new CustomerQuery(dataSource); + Customer cust = query.findCustomer(1, 1); + + assertThat(cust.getId() == 1).as("Customer id was assigned correctly").isTrue(); + assertThat(cust.getForename().equals("rod")).as("Customer forename was assigned correctly").isTrue(); + verify(preparedStatement).setObject(1, 1, Types.NUMERIC); + verify(preparedStatement).setObject(2, 1, Types.NUMERIC); + verify(connection).prepareStatement(SELECT_ID_WHERE); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testFindCustomerString() throws SQLException { + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + + class CustomerQuery extends MappingSqlQuery { + + public CustomerQuery(DataSource ds) { + super(ds, SELECT_ID_FORENAME_WHERE); + declareParameter(new SqlParameter(Types.VARCHAR)); + compile(); + } + + @Override + protected Customer mapRow(ResultSet rs, int rownum) throws SQLException { + Customer cust = new Customer(); + cust.setId(rs.getInt(COLUMN_NAMES[0])); + cust.setForename(rs.getString(COLUMN_NAMES[1])); + return cust; + } + + public Customer findCustomer(String id) { + return findObject(id); + } + } + + CustomerQuery query = new CustomerQuery(dataSource); + Customer cust = query.findCustomer("rod"); + + assertThat(cust.getId() == 1).as("Customer id was assigned correctly").isTrue(); + assertThat(cust.getForename().equals("rod")).as("Customer forename was assigned correctly").isTrue(); + verify(preparedStatement).setString(1, "rod"); + verify(connection).prepareStatement(SELECT_ID_FORENAME_WHERE); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testFindCustomerMixed() throws SQLException { + reset(connection); + PreparedStatement preparedStatement2 = mock(PreparedStatement.class); + ResultSet resultSet2 = mock(ResultSet.class); + given(preparedStatement2.executeQuery()).willReturn(resultSet2); + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + given(resultSet2.next()).willReturn(false); + given(connection.prepareStatement(SELECT_ID_WHERE)).willReturn(preparedStatement, preparedStatement2); + + class CustomerQuery extends MappingSqlQuery { + + public CustomerQuery(DataSource ds) { + super(ds, SELECT_ID_WHERE); + declareParameter(new SqlParameter(COLUMN_NAMES[0], COLUMN_TYPES[0])); + declareParameter(new SqlParameter(COLUMN_NAMES[1], COLUMN_TYPES[1])); + compile(); + } + + @Override + protected Customer mapRow(ResultSet rs, int rownum) throws SQLException { + Customer cust = new Customer(); + cust.setId(rs.getInt(COLUMN_NAMES[0])); + cust.setForename(rs.getString(COLUMN_NAMES[1])); + return cust; + } + + public Customer findCustomer(int id, String name) { + return findObject(new Object[] { id, name }); + } + } + + CustomerQuery query = new CustomerQuery(dataSource); + + Customer cust1 = query.findCustomer(1, "rod"); + assertThat(cust1 != null).as("Found customer").isTrue(); + assertThat(cust1.getId() == 1).as("Customer id was assigned correctly").isTrue(); + + Customer cust2 = query.findCustomer(1, "Roger"); + assertThat(cust2 == null).as("No customer found").isTrue(); + + verify(preparedStatement).setObject(1, 1, Types.INTEGER); + verify(preparedStatement).setString(2, "rod"); + verify(preparedStatement2).setObject(1, 1, Types.INTEGER); + verify(preparedStatement2).setString(2, "Roger"); + verify(resultSet).close(); + verify(resultSet2).close(); + verify(preparedStatement).close(); + verify(preparedStatement2).close(); + verify(connection, times(2)).close(); + } + + @Test + public void testFindTooManyCustomers() throws SQLException { + given(resultSet.next()).willReturn(true, true, false); + given(resultSet.getInt("id")).willReturn(1, 2); + given(resultSet.getString("forename")).willReturn("rod", "rod"); + + class CustomerQuery extends MappingSqlQuery { + + public CustomerQuery(DataSource ds) { + super(ds, SELECT_ID_FORENAME_WHERE); + declareParameter(new SqlParameter(Types.VARCHAR)); + compile(); + } + + @Override + protected Customer mapRow(ResultSet rs, int rownum) throws SQLException { + Customer cust = new Customer(); + cust.setId(rs.getInt(COLUMN_NAMES[0])); + cust.setForename(rs.getString(COLUMN_NAMES[1])); + return cust; + } + + public Customer findCustomer(String id) { + return findObject(id); + } + } + + CustomerQuery query = new CustomerQuery(dataSource); + assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class).isThrownBy(() -> + query.findCustomer("rod")); + verify(preparedStatement).setString(1, "rod"); + verify(connection).prepareStatement(SELECT_ID_FORENAME_WHERE); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testListCustomersIntInt() throws SQLException { + given(resultSet.next()).willReturn(true, true, false); + given(resultSet.getInt("id")).willReturn(1, 2); + given(resultSet.getString("forename")).willReturn("rod", "dave"); + + class CustomerQuery extends MappingSqlQuery { + + public CustomerQuery(DataSource ds) { + super(ds, SELECT_ID_WHERE); + declareParameter(new SqlParameter(Types.NUMERIC)); + declareParameter(new SqlParameter(Types.NUMERIC)); + compile(); + } + + @Override + protected Customer mapRow(ResultSet rs, int rownum) throws SQLException { + Customer cust = new Customer(); + cust.setId(rs.getInt(COLUMN_NAMES[0])); + cust.setForename(rs.getString(COLUMN_NAMES[1])); + return cust; + } + } + + CustomerQuery query = new CustomerQuery(dataSource); + List list = query.execute(1, 1); + assertThat(list.size() == 2).as("2 results in list").isTrue(); + assertThat(list.get(0).getForename()).isEqualTo("rod"); + assertThat(list.get(1).getForename()).isEqualTo("dave"); + verify(preparedStatement).setObject(1, 1, Types.NUMERIC); + verify(preparedStatement).setObject(2, 1, Types.NUMERIC); + verify(connection).prepareStatement(SELECT_ID_WHERE); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testListCustomersString() throws SQLException { + given(resultSet.next()).willReturn(true, true, false); + given(resultSet.getInt("id")).willReturn(1, 2); + given(resultSet.getString("forename")).willReturn("rod", "dave"); + + class CustomerQuery extends MappingSqlQuery { + + public CustomerQuery(DataSource ds) { + super(ds, SELECT_ID_FORENAME_WHERE); + declareParameter(new SqlParameter(Types.VARCHAR)); + compile(); + } + + @Override + protected Customer mapRow(ResultSet rs, int rownum) throws SQLException { + Customer cust = new Customer(); + cust.setId(rs.getInt(COLUMN_NAMES[0])); + cust.setForename(rs.getString(COLUMN_NAMES[1])); + return cust; + } + } + + CustomerQuery query = new CustomerQuery(dataSource); + List list = query.execute("one"); + assertThat(list.size() == 2).as("2 results in list").isTrue(); + assertThat(list.get(0).getForename()).isEqualTo("rod"); + assertThat(list.get(1).getForename()).isEqualTo("dave"); + verify(preparedStatement).setString(1, "one"); + verify(connection).prepareStatement(SELECT_ID_FORENAME_WHERE); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testFancyCustomerQuery() throws SQLException { + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + + given(connection.prepareStatement(SELECT_ID_FORENAME_WHERE, + ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY) + ).willReturn(preparedStatement); + + class CustomerQuery extends MappingSqlQuery { + + public CustomerQuery(DataSource ds) { + super(ds, SELECT_ID_FORENAME_WHERE); + setResultSetType(ResultSet.TYPE_SCROLL_SENSITIVE); + declareParameter(new SqlParameter(Types.NUMERIC)); + compile(); + } + + @Override + protected Customer mapRow(ResultSet rs, int rownum) throws SQLException { + Customer cust = new Customer(); + cust.setId(rs.getInt(COLUMN_NAMES[0])); + cust.setForename(rs.getString(COLUMN_NAMES[1])); + return cust; + } + + public Customer findCustomer(int id) { + return findObject(id); + } + } + + CustomerQuery query = new CustomerQuery(dataSource); + Customer cust = query.findCustomer(1); + assertThat(cust.getId() == 1).as("Customer id was assigned correctly").isTrue(); + assertThat(cust.getForename().equals("rod")).as("Customer forename was assigned correctly").isTrue(); + verify(preparedStatement).setObject(1, 1, Types.NUMERIC); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testUnnamedParameterDeclarationWithNamedParameterQuery() + throws SQLException { + class CustomerQuery extends MappingSqlQuery { + + public CustomerQuery(DataSource ds) { + super(ds, SELECT_ID_FORENAME_WHERE); + setResultSetType(ResultSet.TYPE_SCROLL_SENSITIVE); + declareParameter(new SqlParameter(Types.NUMERIC)); + compile(); + } + + @Override + protected Customer mapRow(ResultSet rs, int rownum) throws SQLException { + Customer cust = new Customer(); + cust.setId(rs.getInt(COLUMN_NAMES[0])); + cust.setForename(rs.getString(COLUMN_NAMES[1])); + return cust; + } + + public Customer findCustomer(int id) { + Map params = new HashMap<>(); + params.put("id", id); + return executeByNamedParam(params).get(0); + } + } + + // Query should not succeed since parameter declaration did not specify parameter name + CustomerQuery query = new CustomerQuery(dataSource); + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() -> + query.findCustomer(1)); + } + + @Test + public void testNamedParameterCustomerQueryWithUnnamedDeclarations() + throws SQLException { + doTestNamedParameterCustomerQuery(false); + } + + @Test + public void testNamedParameterCustomerQueryWithNamedDeclarations() + throws SQLException { + doTestNamedParameterCustomerQuery(true); + } + + private void doTestNamedParameterCustomerQuery(final boolean namedDeclarations) + throws SQLException { + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + given(connection.prepareStatement(SELECT_ID_FORENAME_NAMED_PARAMETERS_PARSED, + ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY) + ).willReturn(preparedStatement); + + class CustomerQuery extends MappingSqlQuery { + + public CustomerQuery(DataSource ds) { + super(ds, SELECT_ID_FORENAME_NAMED_PARAMETERS); + setResultSetType(ResultSet.TYPE_SCROLL_SENSITIVE); + if (namedDeclarations) { + declareParameter(new SqlParameter("country", Types.VARCHAR)); + declareParameter(new SqlParameter("id", Types.NUMERIC)); + } + else { + declareParameter(new SqlParameter(Types.NUMERIC)); + declareParameter(new SqlParameter(Types.VARCHAR)); + } + compile(); + } + + @Override + protected Customer mapRow(ResultSet rs, int rownum) throws SQLException { + Customer cust = new Customer(); + cust.setId(rs.getInt(COLUMN_NAMES[0])); + cust.setForename(rs.getString(COLUMN_NAMES[1])); + return cust; + } + + public Customer findCustomer(int id, String country) { + Map params = new HashMap<>(); + params.put("id", id); + params.put("country", country); + return executeByNamedParam(params).get(0); + } + } + + CustomerQuery query = new CustomerQuery(dataSource); + Customer cust = query.findCustomer(1, "UK"); + assertThat(cust.getId() == 1).as("Customer id was assigned correctly").isTrue(); + assertThat(cust.getForename().equals("rod")).as("Customer forename was assigned correctly").isTrue(); + verify(preparedStatement).setObject(1, 1, Types.NUMERIC); + verify(preparedStatement).setString(2, "UK"); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testNamedParameterInListQuery() throws SQLException { + given(resultSet.next()).willReturn(true, true, false); + given(resultSet.getInt("id")).willReturn(1, 2); + given(resultSet.getString("forename")).willReturn("rod", "juergen"); + + given(connection.prepareStatement(SELECT_ID_FORENAME_WHERE_ID_IN_LIST_1, + ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY) + ).willReturn(preparedStatement); + + class CustomerQuery extends MappingSqlQuery { + + public CustomerQuery(DataSource ds) { + super(ds, SELECT_ID_FORENAME_WHERE_ID_IN_LIST_2); + setResultSetType(ResultSet.TYPE_SCROLL_SENSITIVE); + declareParameter(new SqlParameter("ids", Types.NUMERIC)); + compile(); + } + + @Override + protected Customer mapRow(ResultSet rs, int rownum) throws SQLException { + Customer cust = new Customer(); + cust.setId(rs.getInt(COLUMN_NAMES[0])); + cust.setForename(rs.getString(COLUMN_NAMES[1])); + return cust; + } + + public List findCustomers(List ids) { + Map params = new HashMap<>(); + params.put("ids", ids); + return executeByNamedParam(params); + } + } + + CustomerQuery query = new CustomerQuery(dataSource); + List ids = new ArrayList<>(); + ids.add(1); + ids.add(2); + List cust = query.findCustomers(ids); + + assertThat(cust.size()).as("We got two customers back").isEqualTo(2); + assertThat(1).as("First customer id was assigned correctly").isEqualTo(cust.get(0).getId()); + assertThat("rod").as("First customer forename was assigned correctly").isEqualTo(cust.get(0).getForename()); + assertThat(2).as("Second customer id was assigned correctly").isEqualTo(cust.get(1).getId()); + assertThat("juergen").as("Second customer forename was assigned correctly").isEqualTo(cust.get(1).getForename()); + verify(preparedStatement).setObject(1, 1, Types.NUMERIC); + verify(preparedStatement).setObject(2, 2, Types.NUMERIC); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testNamedParameterQueryReusingParameter() throws SQLException { + given(resultSet.next()).willReturn(true, true, false); + given(resultSet.getInt("id")).willReturn(1, 2); + given(resultSet.getString("forename")).willReturn("rod", "juergen"); + + given(connection.prepareStatement(SELECT_ID_FORENAME_WHERE_ID_REUSED_1, + ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY)).willReturn(preparedStatement) +; + + class CustomerQuery extends MappingSqlQuery { + + public CustomerQuery(DataSource ds) { + super(ds, SELECT_ID_FORENAME_WHERE_ID_REUSED_2); + setResultSetType(ResultSet.TYPE_SCROLL_SENSITIVE); + declareParameter(new SqlParameter("id1", Types.NUMERIC)); + compile(); + } + + @Override + protected Customer mapRow(ResultSet rs, int rownum) throws SQLException { + Customer cust = new Customer(); + cust.setId(rs.getInt(COLUMN_NAMES[0])); + cust.setForename(rs.getString(COLUMN_NAMES[1])); + return cust; + } + + public List findCustomers(Integer id) { + Map params = new HashMap<>(); + params.put("id1", id); + return executeByNamedParam(params); + } + } + + CustomerQuery query = new CustomerQuery(dataSource); + List cust = query.findCustomers(1); + + assertThat(cust.size()).as("We got two customers back").isEqualTo(2); + assertThat(1).as("First customer id was assigned correctly").isEqualTo(cust.get(0).getId()); + assertThat("rod").as("First customer forename was assigned correctly").isEqualTo(cust.get(0).getForename()); + assertThat(2).as("Second customer id was assigned correctly").isEqualTo(cust.get(1).getId()); + assertThat("juergen").as("Second customer forename was assigned correctly").isEqualTo(cust.get(1).getForename()); + + verify(preparedStatement).setObject(1, 1, Types.NUMERIC); + verify(preparedStatement).setObject(2, 1, Types.NUMERIC); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + @Test + public void testNamedParameterUsingInvalidQuestionMarkPlaceHolders() + throws SQLException { + given( + connection.prepareStatement(SELECT_ID_FORENAME_WHERE_ID_REUSED_1, + ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY)).willReturn(preparedStatement); + + class CustomerQuery extends MappingSqlQuery { + + public CustomerQuery(DataSource ds) { + super(ds, SELECT_ID_FORENAME_WHERE_ID_REUSED_1); + setResultSetType(ResultSet.TYPE_SCROLL_SENSITIVE); + declareParameter(new SqlParameter("id1", Types.NUMERIC)); + compile(); + } + + @Override + protected Customer mapRow(ResultSet rs, int rownum) throws SQLException { + Customer cust = new Customer(); + cust.setId(rs.getInt(COLUMN_NAMES[0])); + cust.setForename(rs.getString(COLUMN_NAMES[1])); + return cust; + } + + public List findCustomers(Integer id1) { + Map params = new HashMap<>(); + params.put("id1", id1); + return executeByNamedParam(params); + } + } + + CustomerQuery query = new CustomerQuery(dataSource); + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() -> + query.findCustomers(1)); + } + + @Test + public void testUpdateCustomers() throws SQLException { + given(resultSet.next()).willReturn(true, true, false); + given(resultSet.getInt("id")).willReturn(1, 2); + given(connection.prepareStatement(SELECT_ID_FORENAME_WHERE_ID, + ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE) + ).willReturn(preparedStatement); + + class CustomerUpdateQuery extends UpdatableSqlQuery { + + public CustomerUpdateQuery(DataSource ds) { + super(ds, SELECT_ID_FORENAME_WHERE_ID); + declareParameter(new SqlParameter(Types.NUMERIC)); + compile(); + } + + @Override + protected Customer updateRow(ResultSet rs, int rownum, @Nullable Map context) + throws SQLException { + rs.updateString(2, "" + context.get(rs.getInt(COLUMN_NAMES[0]))); + return null; + } + } + + CustomerUpdateQuery query = new CustomerUpdateQuery(dataSource); + Map values = new HashMap<>(2); + values.put(1, "Rod"); + values.put(2, "Thomas"); + query.execute(2, values); + verify(resultSet).updateString(2, "Rod"); + verify(resultSet).updateString(2, "Thomas"); + verify(resultSet, times(2)).updateRow(); + verify(preparedStatement).setObject(1, 2, Types.NUMERIC); + verify(resultSet).close(); + verify(preparedStatement).close(); + verify(connection).close(); + } + + private static class StringQuery extends MappingSqlQuery { + + public StringQuery(DataSource ds, String sql) { + super(ds, sql); + compile(); + } + + @Override + protected String mapRow(ResultSet rs, int rownum) throws SQLException { + return rs.getString(1); + } + + public String[] run() { + return StringUtils.toStringArray(execute()); + } + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/object/SqlUpdateTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/object/SqlUpdateTests.java new file mode 100644 index 0000000..852d855 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/object/SqlUpdateTests.java @@ -0,0 +1,438 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.object; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Types; +import java.util.HashMap; +import java.util.Map; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.jdbc.JdbcUpdateAffectedIncorrectNumberOfRowsException; +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author Trevor Cook + * @author Thomas Risberg + * @author Juergen Hoeller + */ +public class SqlUpdateTests { + + private static final String UPDATE = + "update seat_status set booking_id = null"; + + private static final String UPDATE_INT = + "update seat_status set booking_id = null where performance_id = ?"; + + private static final String UPDATE_INT_INT = + "update seat_status set booking_id = null where performance_id = ? and price_band_id = ?"; + + private static final String UPDATE_NAMED_PARAMETERS = + "update seat_status set booking_id = null where performance_id = :perfId and price_band_id = :priceId"; + + private static final String UPDATE_STRING = + "update seat_status set booking_id = null where name = ?"; + + private static final String UPDATE_OBJECTS = + "update seat_status set booking_id = null where performance_id = ? and price_band_id = ? and name = ? and confirmed = ?"; + + private static final String INSERT_GENERATE_KEYS = + "insert into show (name) values(?)"; + + private DataSource dataSource; + + private Connection connection; + + private PreparedStatement preparedStatement; + + private ResultSet resultSet; + + private ResultSetMetaData resultSetMetaData; + + + @BeforeEach + public void setUp() throws Exception { + dataSource = mock(DataSource.class); + connection = mock(Connection.class); + preparedStatement = mock(PreparedStatement.class); + resultSet = mock(ResultSet.class); + resultSetMetaData = mock(ResultSetMetaData.class); + given(dataSource.getConnection()).willReturn(connection); + } + + @AfterEach + public void verifyClosed() throws Exception { + verify(preparedStatement).close(); + verify(connection).close(); + } + + + @Test + public void testUpdate() throws SQLException { + given(preparedStatement.executeUpdate()).willReturn(1); + given(connection.prepareStatement(UPDATE)).willReturn(preparedStatement); + + Updater pc = new Updater(); + int rowsAffected = pc.run(); + + assertThat(rowsAffected).isEqualTo(1); + } + + @Test + public void testUpdateInt() throws SQLException { + given(preparedStatement.executeUpdate()).willReturn(1); + given(connection.prepareStatement(UPDATE_INT)).willReturn(preparedStatement); + + IntUpdater pc = new IntUpdater(); + int rowsAffected = pc.run(1); + + assertThat(rowsAffected).isEqualTo(1); + verify(preparedStatement).setObject(1, 1, Types.NUMERIC); + } + + @Test + public void testUpdateIntInt() throws SQLException { + given(preparedStatement.executeUpdate()).willReturn(1); + given(connection.prepareStatement(UPDATE_INT_INT)).willReturn(preparedStatement); + + IntIntUpdater pc = new IntIntUpdater(); + int rowsAffected = pc.run(1, 1); + + assertThat(rowsAffected).isEqualTo(1); + verify(preparedStatement).setObject(1, 1, Types.NUMERIC); + verify(preparedStatement).setObject(2, 1, Types.NUMERIC); + } + + @Test + public void testNamedParameterUpdateWithUnnamedDeclarations() throws SQLException { + doTestNamedParameterUpdate(false); + } + + @Test + public void testNamedParameterUpdateWithNamedDeclarations() throws SQLException { + doTestNamedParameterUpdate(true); + } + + private void doTestNamedParameterUpdate(final boolean namedDeclarations) + throws SQLException { + given(preparedStatement.executeUpdate()).willReturn(1); + given(connection.prepareStatement(UPDATE_INT_INT)).willReturn(preparedStatement); + + class NamedParameterUpdater extends SqlUpdate { + public NamedParameterUpdater() { + setSql(UPDATE_NAMED_PARAMETERS); + setDataSource(dataSource); + if (namedDeclarations) { + declareParameter(new SqlParameter("priceId", Types.DECIMAL)); + declareParameter(new SqlParameter("perfId", Types.NUMERIC)); + } + else { + declareParameter(new SqlParameter(Types.NUMERIC)); + declareParameter(new SqlParameter(Types.DECIMAL)); + } + compile(); + } + + public int run(int performanceId, int type) { + Map params = new HashMap<>(); + params.put("perfId", performanceId); + params.put("priceId", type); + return updateByNamedParam(params); + } + } + + NamedParameterUpdater pc = new NamedParameterUpdater(); + int rowsAffected = pc.run(1, 1); + assertThat(rowsAffected).isEqualTo(1); + verify(preparedStatement).setObject(1, 1, Types.NUMERIC); + verify(preparedStatement).setObject(2, 1, Types.DECIMAL); + } + + @Test + public void testUpdateString() throws SQLException { + given(preparedStatement.executeUpdate()).willReturn(1); + given(connection.prepareStatement(UPDATE_STRING)).willReturn(preparedStatement); + + StringUpdater pc = new StringUpdater(); + int rowsAffected = pc.run("rod"); + + assertThat(rowsAffected).isEqualTo(1); + verify(preparedStatement).setString(1, "rod"); + } + + @Test + public void testUpdateMixed() throws SQLException { + given(preparedStatement.executeUpdate()).willReturn(1); + given(connection.prepareStatement(UPDATE_OBJECTS)).willReturn(preparedStatement); + + MixedUpdater pc = new MixedUpdater(); + int rowsAffected = pc.run(1, 1, "rod", true); + + assertThat(rowsAffected).isEqualTo(1); + verify(preparedStatement).setObject(1, 1, Types.NUMERIC); + verify(preparedStatement).setObject(2, 1, Types.NUMERIC, 2); + verify(preparedStatement).setString(3, "rod"); + verify(preparedStatement).setBoolean(4, Boolean.TRUE); + } + + @Test + public void testUpdateAndGeneratedKeys() throws SQLException { + given(resultSetMetaData.getColumnCount()).willReturn(1); + given(resultSetMetaData.getColumnLabel(1)).willReturn("1"); + given(resultSet.getMetaData()).willReturn(resultSetMetaData); + given(resultSet.next()).willReturn(true, false); + given(resultSet.getObject(1)).willReturn(11); + given(preparedStatement.executeUpdate()).willReturn(1); + given(preparedStatement.getGeneratedKeys()).willReturn(resultSet); + given(connection.prepareStatement(INSERT_GENERATE_KEYS, + PreparedStatement.RETURN_GENERATED_KEYS) + ).willReturn(preparedStatement); + + GeneratedKeysUpdater pc = new GeneratedKeysUpdater(); + KeyHolder generatedKeyHolder = new GeneratedKeyHolder(); + int rowsAffected = pc.run("rod", generatedKeyHolder); + + assertThat(rowsAffected).isEqualTo(1); + assertThat(generatedKeyHolder.getKeyList().size()).isEqualTo(1); + assertThat(generatedKeyHolder.getKey().intValue()).isEqualTo(11); + verify(preparedStatement).setString(1, "rod"); + verify(resultSet).close(); + } + + @Test + public void testUpdateConstructor() throws SQLException { + given(preparedStatement.executeUpdate()).willReturn(1); + given(connection.prepareStatement(UPDATE_OBJECTS)).willReturn(preparedStatement); + ConstructorUpdater pc = new ConstructorUpdater(); + + int rowsAffected = pc.run(1, 1, "rod", true); + + assertThat(rowsAffected).isEqualTo(1); + verify(preparedStatement).setObject(1, 1, Types.NUMERIC); + verify(preparedStatement).setObject(2, 1, Types.NUMERIC); + verify(preparedStatement).setString(3, "rod"); + verify(preparedStatement).setBoolean(4, Boolean.TRUE); + } + + @Test + public void testUnderMaxRows() throws SQLException { + given(preparedStatement.executeUpdate()).willReturn(3); + given(connection.prepareStatement(UPDATE)).willReturn(preparedStatement); + + MaxRowsUpdater pc = new MaxRowsUpdater(); + + int rowsAffected = pc.run(); + assertThat(rowsAffected).isEqualTo(3); + } + + @Test + public void testMaxRows() throws SQLException { + given(preparedStatement.executeUpdate()).willReturn(5); + given(connection.prepareStatement(UPDATE)).willReturn(preparedStatement); + + MaxRowsUpdater pc = new MaxRowsUpdater(); + int rowsAffected = pc.run(); + + assertThat(rowsAffected).isEqualTo(5); + } + + @Test + public void testOverMaxRows() throws SQLException { + given(preparedStatement.executeUpdate()).willReturn(8); + given(connection.prepareStatement(UPDATE)).willReturn(preparedStatement); + + MaxRowsUpdater pc = new MaxRowsUpdater(); + + assertThatExceptionOfType(JdbcUpdateAffectedIncorrectNumberOfRowsException.class).isThrownBy( + pc::run); + } + + @Test + public void testRequiredRows() throws SQLException { + given(preparedStatement.executeUpdate()).willReturn(3); + given(connection.prepareStatement(UPDATE)).willReturn(preparedStatement); + + RequiredRowsUpdater pc = new RequiredRowsUpdater(); + int rowsAffected = pc.run(); + + assertThat(rowsAffected).isEqualTo(3); + } + + @Test + public void testNotRequiredRows() throws SQLException { + given(preparedStatement.executeUpdate()).willReturn(2); + given(connection.prepareStatement(UPDATE)).willReturn(preparedStatement); + RequiredRowsUpdater pc = new RequiredRowsUpdater(); + assertThatExceptionOfType(JdbcUpdateAffectedIncorrectNumberOfRowsException.class).isThrownBy( + pc::run); + } + + private class Updater extends SqlUpdate { + + public Updater() { + setSql(UPDATE); + setDataSource(dataSource); + compile(); + } + + public int run() { + return update(); + } + } + + + private class IntUpdater extends SqlUpdate { + + public IntUpdater() { + setSql(UPDATE_INT); + setDataSource(dataSource); + declareParameter(new SqlParameter(Types.NUMERIC)); + compile(); + } + + public int run(int performanceId) { + return update(performanceId); + } + } + + + private class IntIntUpdater extends SqlUpdate { + + public IntIntUpdater() { + setSql(UPDATE_INT_INT); + setDataSource(dataSource); + declareParameter(new SqlParameter(Types.NUMERIC)); + declareParameter(new SqlParameter(Types.NUMERIC)); + compile(); + } + + public int run(int performanceId, int type) { + return update(performanceId, type); + } + } + + + private class StringUpdater extends SqlUpdate { + + public StringUpdater() { + setSql(UPDATE_STRING); + setDataSource(dataSource); + declareParameter(new SqlParameter(Types.VARCHAR)); + compile(); + } + + public int run(String name) { + return update(name); + } + } + + + private class MixedUpdater extends SqlUpdate { + + public MixedUpdater() { + setSql(UPDATE_OBJECTS); + setDataSource(dataSource); + declareParameter(new SqlParameter(Types.NUMERIC)); + declareParameter(new SqlParameter(Types.NUMERIC, 2)); + declareParameter(new SqlParameter(Types.VARCHAR)); + declareParameter(new SqlParameter(Types.BOOLEAN)); + compile(); + } + + public int run(int performanceId, int type, String name, boolean confirmed) { + return update(performanceId, type, name, confirmed); + } + } + + + private class GeneratedKeysUpdater extends SqlUpdate { + + public GeneratedKeysUpdater() { + setSql(INSERT_GENERATE_KEYS); + setDataSource(dataSource); + declareParameter(new SqlParameter(Types.VARCHAR)); + setReturnGeneratedKeys(true); + compile(); + } + + public int run(String name, KeyHolder generatedKeyHolder) { + return update(new Object[] {name}, generatedKeyHolder); + } + } + + + private class ConstructorUpdater extends SqlUpdate { + + public ConstructorUpdater() { + super(dataSource, UPDATE_OBJECTS, + new int[] {Types.NUMERIC, Types.NUMERIC, Types.VARCHAR, Types.BOOLEAN }); + compile(); + } + + public int run(int performanceId, int type, String name, boolean confirmed) { + return update(performanceId, type, name, confirmed); + } + } + + + private class MaxRowsUpdater extends SqlUpdate { + + public MaxRowsUpdater() { + setSql(UPDATE); + setDataSource(dataSource); + setMaxRowsAffected(5); + compile(); + } + + public int run() { + return update(); + } + } + + + private class RequiredRowsUpdater extends SqlUpdate { + + public RequiredRowsUpdater() { + setSql(UPDATE); + setDataSource(dataSource); + setRequiredRowsAffected(3); + compile(); + } + + public int run() { + return update(); + } + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/object/StoredProcedureTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/object/StoredProcedureTests.java new file mode 100644 index 0000000..ee7b13e --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/object/StoredProcedureTests.java @@ -0,0 +1,715 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.object; + +import java.math.BigDecimal; +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Types; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.jdbc.core.CallableStatementCreator; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.ParameterMapper; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.SimpleRowCountCallbackHandler; +import org.springframework.jdbc.core.SqlOutParameter; +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.jdbc.core.SqlReturnResultSet; +import org.springframework.jdbc.core.support.AbstractSqlTypeValue; +import org.springframework.jdbc.datasource.ConnectionHolder; +import org.springframework.jdbc.support.SQLExceptionTranslator; +import org.springframework.jdbc.support.SQLStateSQLExceptionTranslator; +import org.springframework.lang.Nullable; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.startsWith; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +/** + * @author Thomas Risberg + * @author Trevor Cook + * @author Rod Johnson + */ +public class StoredProcedureTests { + + private DataSource dataSource; + private Connection connection; + private CallableStatement callableStatement; + + private boolean verifyClosedAfter = true; + + @BeforeEach + public void setup() throws Exception { + dataSource = mock(DataSource.class); + connection = mock(Connection.class); + callableStatement = mock(CallableStatement.class); + given(dataSource.getConnection()).willReturn(connection); + given(callableStatement.getConnection()).willReturn(connection); + } + + @AfterEach + public void verifyClosed() throws Exception { + if (verifyClosedAfter) { + verify(callableStatement).close(); + verify(connection, atLeastOnce()).close(); + } + } + + @Test + public void testNoSuchStoredProcedure() throws Exception { + SQLException sqlException = new SQLException( + "Syntax error or access violation exception", "42000"); + given(callableStatement.execute()).willThrow(sqlException); + given(connection.prepareCall("{call " + NoSuchStoredProcedure.SQL + "()}")).willReturn( + callableStatement); + + NoSuchStoredProcedure sproc = new NoSuchStoredProcedure(dataSource); + assertThatExceptionOfType(BadSqlGrammarException.class).isThrownBy( + sproc::execute); + } + + private void testAddInvoice(final int amount, final int custid) throws Exception { + AddInvoice adder = new AddInvoice(dataSource); + int id = adder.execute(amount, custid); + assertThat(id).isEqualTo(4); + } + + private void testAddInvoiceUsingObjectArray(final int amount, final int custid) + throws Exception { + AddInvoiceUsingObjectArray adder = new AddInvoiceUsingObjectArray(dataSource); + int id = adder.execute(amount, custid); + assertThat(id).isEqualTo(5); + } + + @Test + public void testAddInvoices() throws Exception { + given(callableStatement.execute()).willReturn(false); + given(callableStatement.getUpdateCount()).willReturn(-1); + given(callableStatement.getObject(3)).willReturn(4); + given(connection.prepareCall("{call " + AddInvoice.SQL + "(?, ?, ?)}") + ).willReturn(callableStatement); + testAddInvoice(1106, 3); + verify(callableStatement).setObject(1, 1106, Types.INTEGER); + verify(callableStatement).setObject(2, 3, Types.INTEGER); + verify(callableStatement).registerOutParameter(3, Types.INTEGER); + } + + @Test + public void testAddInvoicesUsingObjectArray() throws Exception { + given(callableStatement.execute()).willReturn(false); + given(callableStatement.getUpdateCount()).willReturn(-1); + given(callableStatement.getObject(3)).willReturn(5); + given(connection.prepareCall("{call " + AddInvoice.SQL + "(?, ?, ?)}") + ).willReturn(callableStatement); + testAddInvoiceUsingObjectArray(1106, 4); + verify(callableStatement).setObject(1, 1106, Types.INTEGER); + verify(callableStatement).setObject(2, 4, Types.INTEGER); + verify(callableStatement).registerOutParameter(3, Types.INTEGER); + } + + @Test + public void testAddInvoicesWithinTransaction() throws Exception { + given(callableStatement.execute()).willReturn(false); + given(callableStatement.getUpdateCount()).willReturn(-1); + given(callableStatement.getObject(3)).willReturn(4); + given(connection.prepareCall("{call " + AddInvoice.SQL + "(?, ?, ?)}") + ).willReturn(callableStatement); + TransactionSynchronizationManager.bindResource(dataSource, new ConnectionHolder(connection)); + try { + testAddInvoice(1106, 3); + verify(callableStatement).setObject(1, 1106, Types.INTEGER); + verify(callableStatement).setObject(2, 3, Types.INTEGER); + verify(callableStatement).registerOutParameter(3, Types.INTEGER); + verify(connection, never()).close(); + } + finally { + TransactionSynchronizationManager.unbindResource(dataSource); + connection.close(); + } + } + + /** + * Confirm no connection was used to get metadata. Does not use superclass replay + * mechanism. + */ + @Test + public void testStoredProcedureConfiguredViaJdbcTemplateWithCustomExceptionTranslator() + throws Exception { + given(callableStatement.execute()).willReturn(false); + given(callableStatement.getUpdateCount()).willReturn(-1); + given(callableStatement.getObject(2)).willReturn(5); + given(connection.prepareCall("{call " + StoredProcedureConfiguredViaJdbcTemplate.SQL + "(?, ?)}") + ).willReturn(callableStatement); + + class TestJdbcTemplate extends JdbcTemplate { + + int calls; + + @Override + public Map call(CallableStatementCreator csc, + List declaredParameters) throws DataAccessException { + calls++; + return super.call(csc, declaredParameters); + } + } + TestJdbcTemplate t = new TestJdbcTemplate(); + t.setDataSource(dataSource); + // Will fail without the following, because we're not able to get a connection + // from the DataSource here if we need to create an ExceptionTranslator + t.setExceptionTranslator(new SQLStateSQLExceptionTranslator()); + StoredProcedureConfiguredViaJdbcTemplate sp = new StoredProcedureConfiguredViaJdbcTemplate(t); + + assertThat(sp.execute(11)).isEqualTo(5); + assertThat(t.calls).isEqualTo(1); + + verify(callableStatement).setObject(1, 11, Types.INTEGER); + verify(callableStatement).registerOutParameter(2, Types.INTEGER); + } + + /** + * Confirm our JdbcTemplate is used + */ + @Test + public void testStoredProcedureConfiguredViaJdbcTemplate() throws Exception { + given(callableStatement.execute()).willReturn(false); + given(callableStatement.getUpdateCount()).willReturn(-1); + given(callableStatement.getObject(2)).willReturn(4); + given(connection.prepareCall("{call " + StoredProcedureConfiguredViaJdbcTemplate.SQL + "(?, ?)}") + ).willReturn(callableStatement); + JdbcTemplate t = new JdbcTemplate(); + t.setDataSource(dataSource); + StoredProcedureConfiguredViaJdbcTemplate sp = new StoredProcedureConfiguredViaJdbcTemplate(t); + assertThat(sp.execute(1106)).isEqualTo(4); + verify(callableStatement).setObject(1, 1106, Types.INTEGER); + verify(callableStatement).registerOutParameter(2, Types.INTEGER); + } + + @Test + public void testNullArg() throws Exception { + given(callableStatement.execute()).willReturn(false); + given(callableStatement.getUpdateCount()).willReturn(-1); + given(connection.prepareCall("{call " + NullArg.SQL + "(?)}")).willReturn(callableStatement); + NullArg na = new NullArg(dataSource); + na.execute((String) null); + callableStatement.setNull(1, Types.VARCHAR); + } + + @Test + public void testUnnamedParameter() throws Exception { + this.verifyClosedAfter = false; + // Shouldn't succeed in creating stored procedure with unnamed parameter + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() -> + new UnnamedParameterStoredProcedure(dataSource)); + } + + @Test + public void testMissingParameter() throws Exception { + this.verifyClosedAfter = false; + MissingParameterStoredProcedure mp = new MissingParameterStoredProcedure(dataSource); + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy( + mp::execute); + } + + @Test + public void testStoredProcedureExceptionTranslator() throws Exception { + SQLException sqlException = new SQLException( + "Syntax error or access violation exception", "42000"); + given(callableStatement.execute()).willThrow(sqlException); + given(connection.prepareCall("{call " + StoredProcedureExceptionTranslator.SQL + "()}") + ).willReturn(callableStatement); + StoredProcedureExceptionTranslator sproc = new StoredProcedureExceptionTranslator(dataSource); + assertThatExceptionOfType(CustomDataException.class).isThrownBy( + sproc::execute); + } + + @Test + public void testStoredProcedureWithResultSet() throws Exception { + ResultSet resultSet = mock(ResultSet.class); + given(resultSet.next()).willReturn(true, true, false); + given(callableStatement.execute()).willReturn(true); + given(callableStatement.getUpdateCount()).willReturn(-1); + given(callableStatement.getResultSet()).willReturn(resultSet); + given(callableStatement.getUpdateCount()).willReturn(-1); + given(connection.prepareCall("{call " + StoredProcedureWithResultSet.SQL + "()}") + ).willReturn(callableStatement); + StoredProcedureWithResultSet sproc = new StoredProcedureWithResultSet(dataSource); + sproc.execute(); + assertThat(sproc.getCount()).isEqualTo(2); + verify(resultSet).close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testStoredProcedureWithResultSetMapped() throws Exception { + ResultSet resultSet = mock(ResultSet.class); + given(resultSet.next()).willReturn(true, true, false); + given(resultSet.getString(2)).willReturn("Foo", "Bar"); + given(callableStatement.execute()).willReturn(true); + given(callableStatement.getUpdateCount()).willReturn(-1); + given(callableStatement.getResultSet()).willReturn(resultSet); + given(callableStatement.getMoreResults()).willReturn(false); + given(callableStatement.getUpdateCount()).willReturn(-1); + given(connection.prepareCall("{call " + StoredProcedureWithResultSetMapped.SQL + "()}") + ).willReturn(callableStatement); + StoredProcedureWithResultSetMapped sproc = new StoredProcedureWithResultSetMapped(dataSource); + Map res = sproc.execute(); + List rs = (List) res.get("rs"); + assertThat(rs.size()).isEqualTo(2); + assertThat(rs.get(0)).isEqualTo("Foo"); + assertThat(rs.get(1)).isEqualTo("Bar"); + verify(resultSet).close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testStoredProcedureWithUndeclaredResults() throws Exception { + ResultSet resultSet1 = mock(ResultSet.class); + given(resultSet1.next()).willReturn(true, true, false); + given(resultSet1.getString(2)).willReturn("Foo", "Bar"); + + ResultSetMetaData resultSetMetaData = mock(ResultSetMetaData.class); + given(resultSetMetaData.getColumnCount()).willReturn(2); + given(resultSetMetaData.getColumnLabel(1)).willReturn("spam"); + given(resultSetMetaData.getColumnLabel(2)).willReturn("eggs"); + + ResultSet resultSet2 = mock(ResultSet.class); + given(resultSet2.getMetaData()).willReturn(resultSetMetaData); + given(resultSet2.next()).willReturn(true, false); + given(resultSet2.getObject(1)).willReturn("Spam"); + given(resultSet2.getObject(2)).willReturn("Eggs"); + + given(callableStatement.execute()).willReturn(true); + given(callableStatement.getUpdateCount()).willReturn(-1); + given(callableStatement.getResultSet()).willReturn(resultSet1, resultSet2); + given(callableStatement.getMoreResults()).willReturn(true, false, false); + given(callableStatement.getUpdateCount()).willReturn(-1, -1, 0, -1); + given(connection.prepareCall("{call " + StoredProcedureWithResultSetMapped.SQL + "()}") + ).willReturn(callableStatement); + + StoredProcedureWithResultSetMapped sproc = new StoredProcedureWithResultSetMapped(dataSource); + Map res = sproc.execute(); + + assertThat(res.size()).as("incorrect number of returns").isEqualTo(3); + + List rs1 = (List) res.get("rs"); + assertThat(rs1.size()).isEqualTo(2); + assertThat(rs1.get(0)).isEqualTo("Foo"); + assertThat(rs1.get(1)).isEqualTo("Bar"); + + List rs2 = (List) res.get("#result-set-2"); + assertThat(rs2.size()).isEqualTo(1); + Object o2 = rs2.get(0); + boolean condition = o2 instanceof Map; + assertThat(condition).as("wron type returned for result set 2").isTrue(); + Map m2 = (Map) o2; + assertThat(m2.get("spam")).isEqualTo("Spam"); + assertThat(m2.get("eggs")).isEqualTo("Eggs"); + + Number n = (Number) res.get("#update-count-1"); + assertThat(n.intValue()).as("wrong update count").isEqualTo(0); + verify(resultSet1).close(); + verify(resultSet2).close(); + } + + @Test + public void testStoredProcedureSkippingResultsProcessing() throws Exception { + given(callableStatement.execute()).willReturn(true); + given(callableStatement.getUpdateCount()).willReturn(-1); + given(connection.prepareCall("{call " + StoredProcedureWithResultSetMapped.SQL + "()}") + ).willReturn(callableStatement); + JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); + jdbcTemplate.setSkipResultsProcessing(true); + StoredProcedureWithResultSetMapped sproc = new StoredProcedureWithResultSetMapped( + jdbcTemplate); + Map res = sproc.execute(); + assertThat(res.size()).as("incorrect number of returns").isEqualTo(0); + } + + @Test + @SuppressWarnings("unchecked") + public void testStoredProcedureSkippingUndeclaredResults() throws Exception { + ResultSet resultSet = mock(ResultSet.class); + given(resultSet.next()).willReturn(true, true, false); + given(resultSet.getString(2)).willReturn("Foo", "Bar"); + given(callableStatement.execute()).willReturn(true); + given(callableStatement.getUpdateCount()).willReturn(-1); + given(callableStatement.getResultSet()).willReturn(resultSet); + given(callableStatement.getMoreResults()).willReturn(true, false); + given(callableStatement.getUpdateCount()).willReturn(-1, -1); + given(connection.prepareCall("{call " + StoredProcedureWithResultSetMapped.SQL + "()}") + ).willReturn(callableStatement); + + JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); + jdbcTemplate.setSkipUndeclaredResults(true); + StoredProcedureWithResultSetMapped sproc = new StoredProcedureWithResultSetMapped( + jdbcTemplate); + Map res = sproc.execute(); + + assertThat(res.size()).as("incorrect number of returns").isEqualTo(1); + List rs1 = (List) res.get("rs"); + assertThat(rs1.size()).isEqualTo(2); + assertThat(rs1.get(0)).isEqualTo("Foo"); + assertThat(rs1.get(1)).isEqualTo("Bar"); + verify(resultSet).close(); + } + + @Test + public void testParameterMapper() throws Exception { + given(callableStatement.execute()).willReturn(false); + given(callableStatement.getUpdateCount()).willReturn(-1); + given(callableStatement.getObject(2)).willReturn("OK"); + given(connection.prepareCall("{call " + ParameterMapperStoredProcedure.SQL + "(?, ?)}") + ).willReturn(callableStatement); + + ParameterMapperStoredProcedure pmsp = new ParameterMapperStoredProcedure(dataSource); + Map out = pmsp.executeTest(); + assertThat(out.get("out")).isEqualTo("OK"); + + verify(callableStatement).setString(eq(1), startsWith("Mock for Connection")); + verify(callableStatement).registerOutParameter(2, Types.VARCHAR); + } + + @Test + public void testSqlTypeValue() throws Exception { + int[] testVal = new int[] { 1, 2 }; + given(callableStatement.execute()).willReturn(false); + given(callableStatement.getUpdateCount()).willReturn(-1); + given(callableStatement.getObject(2)).willReturn("OK"); + given(connection.prepareCall("{call " + SqlTypeValueStoredProcedure.SQL + "(?, ?)}") + ).willReturn(callableStatement); + + SqlTypeValueStoredProcedure stvsp = new SqlTypeValueStoredProcedure(dataSource); + Map out = stvsp.executeTest(testVal); + assertThat(out.get("out")).isEqualTo("OK"); + verify(callableStatement).setObject(1, testVal, Types.ARRAY); + verify(callableStatement).registerOutParameter(2, Types.VARCHAR); + } + + @Test + public void testNumericWithScale() throws Exception { + given(callableStatement.execute()).willReturn(false); + given(callableStatement.getUpdateCount()).willReturn(-1); + given(callableStatement.getObject(1)).willReturn(new BigDecimal("12345.6789")); + given(connection.prepareCall("{call " + NumericWithScaleStoredProcedure.SQL + "(?)}") + ).willReturn(callableStatement); + NumericWithScaleStoredProcedure nwssp = new NumericWithScaleStoredProcedure(dataSource); + Map out = nwssp.executeTest(); + assertThat(out.get("out")).isEqualTo(new BigDecimal("12345.6789")); + verify(callableStatement).registerOutParameter(1, Types.DECIMAL, 4); + } + + private static class StoredProcedureConfiguredViaJdbcTemplate extends StoredProcedure { + + public static final String SQL = "configured_via_jt"; + + public StoredProcedureConfiguredViaJdbcTemplate(JdbcTemplate t) { + setJdbcTemplate(t); + setSql(SQL); + declareParameter(new SqlParameter("intIn", Types.INTEGER)); + declareParameter(new SqlOutParameter("intOut", Types.INTEGER)); + compile(); + } + + public int execute(int intIn) { + Map in = new HashMap<>(); + in.put("intIn", intIn); + Map out = execute(in); + return ((Number) out.get("intOut")).intValue(); + } + } + + private static class AddInvoice extends StoredProcedure { + + public static final String SQL = "add_invoice"; + + public AddInvoice(DataSource ds) { + setDataSource(ds); + setSql(SQL); + declareParameter(new SqlParameter("amount", Types.INTEGER)); + declareParameter(new SqlParameter("custid", Types.INTEGER)); + declareParameter(new SqlOutParameter("newid", Types.INTEGER)); + compile(); + } + + public int execute(int amount, int custid) { + Map in = new HashMap<>(); + in.put("amount", amount); + in.put("custid", custid); + Map out = execute(in); + return ((Number) out.get("newid")).intValue(); + } + } + + private static class AddInvoiceUsingObjectArray extends StoredProcedure { + + public static final String SQL = "add_invoice"; + + public AddInvoiceUsingObjectArray(DataSource ds) { + setDataSource(ds); + setSql(SQL); + declareParameter(new SqlParameter("amount", Types.INTEGER)); + declareParameter(new SqlParameter("custid", Types.INTEGER)); + declareParameter(new SqlOutParameter("newid", Types.INTEGER)); + compile(); + } + + public int execute(int amount, int custid) { + Map out = execute(new Object[] { amount, custid }); + return ((Number) out.get("newid")).intValue(); + } + } + + private static class NullArg extends StoredProcedure { + + public static final String SQL = "takes_null"; + + public NullArg(DataSource ds) { + setDataSource(ds); + setSql(SQL); + declareParameter(new SqlParameter("ptest", Types.VARCHAR)); + compile(); + } + + public void execute(String s) { + Map in = new HashMap<>(); + in.put("ptest", s); + execute(in); + } + } + + private static class NoSuchStoredProcedure extends StoredProcedure { + + public static final String SQL = "no_sproc_with_this_name"; + + public NoSuchStoredProcedure(DataSource ds) { + setDataSource(ds); + setSql(SQL); + compile(); + } + + public void execute() { + execute(new HashMap<>()); + } + } + + private static class UnnamedParameterStoredProcedure extends StoredProcedure { + + public UnnamedParameterStoredProcedure(DataSource ds) { + super(ds, "unnamed_parameter_sp"); + declareParameter(new SqlParameter(Types.INTEGER)); + compile(); + } + + } + + private static class MissingParameterStoredProcedure extends StoredProcedure { + + public MissingParameterStoredProcedure(DataSource ds) { + setDataSource(ds); + setSql("takes_string"); + declareParameter(new SqlParameter("mystring", Types.VARCHAR)); + compile(); + } + + public void execute() { + execute(new HashMap<>()); + } + } + + private static class StoredProcedureWithResultSet extends StoredProcedure { + + public static final String SQL = "sproc_with_result_set"; + + private final SimpleRowCountCallbackHandler handler = new SimpleRowCountCallbackHandler(); + + public StoredProcedureWithResultSet(DataSource ds) { + setDataSource(ds); + setSql(SQL); + declareParameter(new SqlReturnResultSet("rs", this.handler)); + compile(); + } + + public void execute() { + execute(new HashMap<>()); + } + + public int getCount() { + return this.handler.getCount(); + } + } + + private static class StoredProcedureWithResultSetMapped extends StoredProcedure { + + public static final String SQL = "sproc_with_result_set"; + + public StoredProcedureWithResultSetMapped(DataSource ds) { + setDataSource(ds); + setSql(SQL); + declareParameter(new SqlReturnResultSet("rs", new RowMapperImpl())); + compile(); + } + + public StoredProcedureWithResultSetMapped(JdbcTemplate jt) { + setJdbcTemplate(jt); + setSql(SQL); + declareParameter(new SqlReturnResultSet("rs", new RowMapperImpl())); + compile(); + } + + public Map execute() { + return execute(new HashMap<>()); + } + + private static class RowMapperImpl implements RowMapper { + + @Override + public String mapRow(ResultSet rs, int rowNum) throws SQLException { + return rs.getString(2); + } + } + } + + private static class ParameterMapperStoredProcedure extends StoredProcedure { + + public static final String SQL = "parameter_mapper_sp"; + + public ParameterMapperStoredProcedure(DataSource ds) { + setDataSource(ds); + setSql(SQL); + declareParameter(new SqlParameter("in", Types.VARCHAR)); + declareParameter(new SqlOutParameter("out", Types.VARCHAR)); + compile(); + } + + public Map executeTest() { + return execute(new TestParameterMapper()); + } + + private static class TestParameterMapper implements ParameterMapper { + + private TestParameterMapper() { + } + + @Override + public Map createMap(Connection con) throws SQLException { + Map inParms = new HashMap<>(); + String testValue = con.toString(); + inParms.put("in", testValue); + return inParms; + } + } + } + + private static class SqlTypeValueStoredProcedure extends StoredProcedure { + + public static final String SQL = "sql_type_value_sp"; + + public SqlTypeValueStoredProcedure(DataSource ds) { + setDataSource(ds); + setSql(SQL); + declareParameter(new SqlParameter("in", Types.ARRAY, "NUMBERS")); + declareParameter(new SqlOutParameter("out", Types.VARCHAR)); + compile(); + } + + public Map executeTest(final int[] inValue) { + Map in = new HashMap<>(); + in.put("in", new AbstractSqlTypeValue() { + @Override + public Object createTypeValue(Connection con, int type, String typeName) { + // assertEquals(Connection.class, con.getClass()); + // assertEquals(Types.ARRAY, type); + // assertEquals("NUMBER", typeName); + return inValue; + } + }); + return execute(in); + } + } + + private static class NumericWithScaleStoredProcedure extends StoredProcedure { + + public static final String SQL = "numeric_with_scale_sp"; + + public NumericWithScaleStoredProcedure(DataSource ds) { + setDataSource(ds); + setSql(SQL); + declareParameter(new SqlOutParameter("out", Types.DECIMAL, 4)); + compile(); + } + + public Map executeTest() { + return execute(new HashMap<>()); + } + } + + private static class StoredProcedureExceptionTranslator extends StoredProcedure { + + public static final String SQL = "no_sproc_with_this_name"; + + public StoredProcedureExceptionTranslator(DataSource ds) { + setDataSource(ds); + setSql(SQL); + getJdbcTemplate().setExceptionTranslator(new SQLExceptionTranslator() { + @Override + public DataAccessException translate(String task, @Nullable String sql, SQLException ex) { + return new CustomDataException(sql, ex); + } + }); + compile(); + } + + public void execute() { + execute(new HashMap<>()); + } + } + + @SuppressWarnings("serial") + private static class CustomDataException extends DataAccessException { + + public CustomDataException(String s) { + super(s); + } + + public CustomDataException(String s, Throwable ex) { + super(s, ex); + } + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/CustomErrorCodeException.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/CustomErrorCodeException.java new file mode 100644 index 0000000..d1824d0 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/CustomErrorCodeException.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import org.springframework.dao.DataAccessException; + +/** + * @author Thomas Risberg + */ +@SuppressWarnings("serial") +public class CustomErrorCodeException extends DataAccessException { + + public CustomErrorCodeException(String msg) { + super(msg); + } + + public CustomErrorCodeException(String msg, Throwable ex) { + super(msg, ex); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistrarTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistrarTests.java new file mode 100644 index 0000000..4eb3947 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistrarTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import java.sql.SQLException; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.TransientDataAccessResourceException; +import org.springframework.jdbc.BadSqlGrammarException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for custom {@link SQLExceptionTranslator}. + * + * @author Thomas Risberg + */ +public class CustomSQLExceptionTranslatorRegistrarTests { + + @Test + @SuppressWarnings("resource") + public void customErrorCodeTranslation() { + new ClassPathXmlApplicationContext("test-custom-translators-context.xml", + CustomSQLExceptionTranslatorRegistrarTests.class); + + SQLErrorCodes codes = SQLErrorCodesFactory.getInstance().getErrorCodes("H2"); + SQLErrorCodeSQLExceptionTranslator sext = new SQLErrorCodeSQLExceptionTranslator(); + sext.setSqlErrorCodes(codes); + + DataAccessException exFor4200 = sext.doTranslate("", "", new SQLException("Ouch", "42000", 42000)); + assertThat(exFor4200).as("Should have been translated").isNotNull(); + assertThat(BadSqlGrammarException.class.isAssignableFrom(exFor4200.getClass())).as("Should have been instance of BadSqlGrammarException").isTrue(); + + DataAccessException exFor2 = sext.doTranslate("", "", new SQLException("Ouch", "42000", 2)); + assertThat(exFor2).as("Should have been translated").isNotNull(); + assertThat(TransientDataAccessResourceException.class.isAssignableFrom(exFor2.getClass())).as("Should have been instance of TransientDataAccessResourceException").isTrue(); + + DataAccessException exFor3 = sext.doTranslate("", "", new SQLException("Ouch", "42000", 3)); + assertThat(exFor3).as("Should not have been translated").isNull(); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/CustomSqlExceptionTranslator.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/CustomSqlExceptionTranslator.java new file mode 100644 index 0000000..1a5d833 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/CustomSqlExceptionTranslator.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2017 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import java.sql.SQLException; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.TransientDataAccessResourceException; +import org.springframework.lang.Nullable; + +/** + * Custom SQLException translation for testing. + * + * @author Thomas Risberg + */ +public class CustomSqlExceptionTranslator implements SQLExceptionTranslator { + + @Override + public DataAccessException translate(String task, @Nullable String sql, SQLException ex) { + if (ex.getErrorCode() == 2) { + return new TransientDataAccessResourceException("Custom", ex); + } + return null; + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java new file mode 100644 index 0000000..d2e3594 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/DataFieldMaxValueIncrementerTests.java @@ -0,0 +1,209 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; + +import org.springframework.jdbc.support.incrementer.HanaSequenceMaxValueIncrementer; +import org.springframework.jdbc.support.incrementer.HsqlMaxValueIncrementer; +import org.springframework.jdbc.support.incrementer.MySQLMaxValueIncrementer; +import org.springframework.jdbc.support.incrementer.OracleSequenceMaxValueIncrementer; +import org.springframework.jdbc.support.incrementer.PostgresSequenceMaxValueIncrementer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author Juergen Hoeller + * @since 27.02.2004 + */ +public class DataFieldMaxValueIncrementerTests { + + private final DataSource dataSource = mock(DataSource.class); + + private final Connection connection = mock(Connection.class); + + private final Statement statement = mock(Statement.class); + + private final ResultSet resultSet = mock(ResultSet.class); + + + @Test + public void testHanaSequenceMaxValueIncrementer() throws SQLException { + given(dataSource.getConnection()).willReturn(connection); + given(connection.createStatement()).willReturn(statement); + given(statement.executeQuery("select myseq.nextval from dummy")).willReturn(resultSet); + given(resultSet.next()).willReturn(true); + given(resultSet.getLong(1)).willReturn(10L, 12L); + + HanaSequenceMaxValueIncrementer incrementer = new HanaSequenceMaxValueIncrementer(); + incrementer.setDataSource(dataSource); + incrementer.setIncrementerName("myseq"); + incrementer.setPaddingLength(2); + incrementer.afterPropertiesSet(); + + assertThat(incrementer.nextLongValue()).isEqualTo(10); + assertThat(incrementer.nextStringValue()).isEqualTo("12"); + + verify(resultSet, times(2)).close(); + verify(statement, times(2)).close(); + verify(connection, times(2)).close(); + } + + @Test + public void testHsqlMaxValueIncrementer() throws SQLException { + given(dataSource.getConnection()).willReturn(connection); + given(connection.createStatement()).willReturn(statement); + given(statement.executeQuery("select max(identity()) from myseq")).willReturn(resultSet); + given(resultSet.next()).willReturn(true); + given(resultSet.getLong(1)).willReturn(0L, 1L, 2L, 3L, 4L, 5L); + + HsqlMaxValueIncrementer incrementer = new HsqlMaxValueIncrementer(); + incrementer.setDataSource(dataSource); + incrementer.setIncrementerName("myseq"); + incrementer.setColumnName("seq"); + incrementer.setCacheSize(3); + incrementer.setPaddingLength(3); + incrementer.afterPropertiesSet(); + + assertThat(incrementer.nextIntValue()).isEqualTo(0); + assertThat(incrementer.nextLongValue()).isEqualTo(1); + assertThat(incrementer.nextStringValue()).isEqualTo("002"); + assertThat(incrementer.nextIntValue()).isEqualTo(3); + assertThat(incrementer.nextLongValue()).isEqualTo(4); + + verify(statement, times(6)).executeUpdate("insert into myseq values(null)"); + verify(statement).executeUpdate("delete from myseq where seq < 2"); + verify(statement).executeUpdate("delete from myseq where seq < 5"); + verify(resultSet, times(6)).close(); + verify(statement, times(2)).close(); + verify(connection, times(2)).close(); + } + + @Test + public void testHsqlMaxValueIncrementerWithDeleteSpecificValues() throws SQLException { + given(dataSource.getConnection()).willReturn(connection); + given(connection.createStatement()).willReturn(statement); + given(statement.executeQuery("select max(identity()) from myseq")).willReturn(resultSet); + given(resultSet.next()).willReturn(true); + given(resultSet.getLong(1)).willReturn(0L, 1L, 2L, 3L, 4L, 5L); + + HsqlMaxValueIncrementer incrementer = new HsqlMaxValueIncrementer(); + incrementer.setDataSource(dataSource); + incrementer.setIncrementerName("myseq"); + incrementer.setColumnName("seq"); + incrementer.setCacheSize(3); + incrementer.setPaddingLength(3); + incrementer.setDeleteSpecificValues(true); + incrementer.afterPropertiesSet(); + + assertThat(incrementer.nextIntValue()).isEqualTo(0); + assertThat(incrementer.nextLongValue()).isEqualTo(1); + assertThat(incrementer.nextStringValue()).isEqualTo("002"); + assertThat(incrementer.nextIntValue()).isEqualTo(3); + assertThat(incrementer.nextLongValue()).isEqualTo(4); + + verify(statement, times(6)).executeUpdate("insert into myseq values(null)"); + verify(statement).executeUpdate("delete from myseq where seq in (-1, 0, 1)"); + verify(statement).executeUpdate("delete from myseq where seq in (2, 3, 4)"); + verify(resultSet, times(6)).close(); + verify(statement, times(2)).close(); + verify(connection, times(2)).close(); + } + + @Test + public void testMySQLMaxValueIncrementer() throws SQLException { + given(dataSource.getConnection()).willReturn(connection); + given(connection.createStatement()).willReturn(statement); + given(statement.executeQuery("select last_insert_id()")).willReturn(resultSet); + given(resultSet.next()).willReturn(true); + given(resultSet.getLong(1)).willReturn(2L, 4L); + + MySQLMaxValueIncrementer incrementer = new MySQLMaxValueIncrementer(); + incrementer.setDataSource(dataSource); + incrementer.setIncrementerName("myseq"); + incrementer.setColumnName("seq"); + incrementer.setCacheSize(2); + incrementer.setPaddingLength(1); + incrementer.afterPropertiesSet(); + + assertThat(incrementer.nextIntValue()).isEqualTo(1); + assertThat(incrementer.nextLongValue()).isEqualTo(2); + assertThat(incrementer.nextStringValue()).isEqualTo("3"); + assertThat(incrementer.nextLongValue()).isEqualTo(4); + + verify(statement, times(2)).executeUpdate("update myseq set seq = last_insert_id(seq + 2)"); + verify(resultSet, times(2)).close(); + verify(statement, times(2)).close(); + verify(connection, times(2)).close(); + } + + @Test + public void testOracleSequenceMaxValueIncrementer() throws SQLException { + given(dataSource.getConnection()).willReturn(connection); + given(connection.createStatement()).willReturn(statement); + given(statement.executeQuery("select myseq.nextval from dual")).willReturn(resultSet); + given(resultSet.next()).willReturn(true); + given(resultSet.getLong(1)).willReturn(10L, 12L); + + OracleSequenceMaxValueIncrementer incrementer = new OracleSequenceMaxValueIncrementer(); + incrementer.setDataSource(dataSource); + incrementer.setIncrementerName("myseq"); + incrementer.setPaddingLength(2); + incrementer.afterPropertiesSet(); + + assertThat(incrementer.nextLongValue()).isEqualTo(10); + assertThat(incrementer.nextStringValue()).isEqualTo("12"); + + verify(resultSet, times(2)).close(); + verify(statement, times(2)).close(); + verify(connection, times(2)).close(); + } + + @Test + public void testPostgresSequenceMaxValueIncrementer() throws SQLException { + given(dataSource.getConnection()).willReturn(connection); + given(connection.createStatement()).willReturn(statement); + given(statement.executeQuery("select nextval('myseq')")).willReturn(resultSet); + given(resultSet.next()).willReturn(true); + given(resultSet.getLong(1)).willReturn(10L, 12L); + + PostgresSequenceMaxValueIncrementer incrementer = new PostgresSequenceMaxValueIncrementer(); + incrementer.setDataSource(dataSource); + incrementer.setIncrementerName("myseq"); + incrementer.setPaddingLength(5); + incrementer.afterPropertiesSet(); + + assertThat(incrementer.nextStringValue()).isEqualTo("00010"); + assertThat(incrementer.nextIntValue()).isEqualTo(12); + + verify(resultSet, times(2)).close(); + verify(statement, times(2)).close(); + verify(connection, times(2)).close(); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/DatabaseStartupValidatorTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/DatabaseStartupValidatorTests.java new file mode 100644 index 0000000..498d754 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/DatabaseStartupValidatorTests.java @@ -0,0 +1,138 @@ +/* + * Copyright 2003-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.jdbc.CannotGetJdbcConnectionException; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Mock object based tests for {@code DatabaseStartupValidator}. + * + * @author Marten Deinum + */ +class DatabaseStartupValidatorTests { + + private final DataSource dataSource = mock(DataSource.class); + + private final Connection connection = mock(Connection.class); + + private final DatabaseStartupValidator validator = new DatabaseStartupValidator(); + + + @BeforeEach + void setUp() throws Exception { + given(dataSource.getConnection()).willReturn(connection); + validator.setDataSource(dataSource); + validator.setTimeout(3); // ensure tests don't accidentally run too long + } + + @Test + void exceededTimeoutThrowsException() { + validator.setTimeout(1); + assertThatExceptionOfType(CannotGetJdbcConnectionException.class) + .isThrownBy(validator::afterPropertiesSet); + } + + @Test + void properSetupForDataSource() { + validator.setDataSource(null); + + assertThatIllegalArgumentException().isThrownBy(validator::afterPropertiesSet); + } + + @Test + void shouldUseJdbc4IsValidByDefault() throws Exception { + given(connection.isValid(1)).willReturn(true); + + validator.afterPropertiesSet(); + + verify(connection, times(1)).isValid(1); + verify(connection, times(1)).close(); + } + + @Test + void shouldCallValidatonTwiceWhenNotValid() throws Exception { + given(connection.isValid(1)).willReturn(false, true); + + validator.afterPropertiesSet(); + + verify(connection, times(2)).isValid(1); + verify(connection, times(2)).close(); + } + + @Test + void shouldCallValidatonTwiceInCaseOfException() throws Exception { + given(connection.isValid(1)).willThrow(new SQLException("Test")).willReturn(true); + + validator.afterPropertiesSet(); + + verify(connection, times(2)).isValid(1); + verify(connection, times(2)).close(); + } + + @Test + @SuppressWarnings("deprecation") + void useValidationQueryInsteadOfIsValid() throws Exception { + String validationQuery = "SELECT NOW() FROM DUAL"; + Statement statement = mock(Statement.class); + given(connection.createStatement()).willReturn(statement); + given(statement.execute(validationQuery)).willReturn(true); + + validator.setValidationQuery(validationQuery); + validator.afterPropertiesSet(); + + verify(connection, times(1)).createStatement(); + verify(statement, times(1)).execute(validationQuery); + verify(connection, times(1)).close(); + verify(statement, times(1)).close(); + } + + @Test + @SuppressWarnings("deprecation") + void shouldExecuteValidatonTwiceOnError() throws Exception { + String validationQuery = "SELECT NOW() FROM DUAL"; + Statement statement = mock(Statement.class); + given(connection.createStatement()).willReturn(statement); + given(statement.execute(validationQuery)) + .willThrow(new SQLException("Test")) + .willReturn(true); + + validator.setValidationQuery(validationQuery); + validator.afterPropertiesSet(); + + verify(connection, times(2)).createStatement(); + verify(statement, times(2)).execute(validationQuery); + verify(connection, times(2)).close(); + verify(statement, times(2)).close(); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/DefaultLobHandlerTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/DefaultLobHandlerTests.java new file mode 100644 index 0000000..6343ffa --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/DefaultLobHandlerTests.java @@ -0,0 +1,138 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.junit.jupiter.api.Test; + +import org.springframework.jdbc.support.lob.DefaultLobHandler; +import org.springframework.jdbc.support.lob.LobCreator; +import org.springframework.jdbc.support.lob.LobHandler; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author Juergen Hoeller + * @since 17.12.2003 + */ +public class DefaultLobHandlerTests { + + private ResultSet rs = mock(ResultSet.class); + + private PreparedStatement ps = mock(PreparedStatement.class); + + private LobHandler lobHandler = new DefaultLobHandler(); + + private LobCreator lobCreator = lobHandler.getLobCreator(); + + + @Test + public void testGetBlobAsBytes() throws SQLException { + lobHandler.getBlobAsBytes(rs, 1); + verify(rs).getBytes(1); + } + + @Test + public void testGetBlobAsBinaryStream() throws SQLException { + lobHandler.getBlobAsBinaryStream(rs, 1); + verify(rs).getBinaryStream(1); + } + + @Test + public void testGetClobAsString() throws SQLException { + lobHandler.getClobAsString(rs, 1); + verify(rs).getString(1); + } + + @Test + public void testGetClobAsAsciiStream() throws SQLException { + lobHandler.getClobAsAsciiStream(rs, 1); + verify(rs).getAsciiStream(1); + } + + @Test + public void testGetClobAsCharacterStream() throws SQLException { + lobHandler.getClobAsCharacterStream(rs, 1); + verify(rs).getCharacterStream(1); + } + + @Test + public void testSetBlobAsBytes() throws SQLException { + byte[] content = "testContent".getBytes(); + lobCreator.setBlobAsBytes(ps, 1, content); + verify(ps).setBytes(1, content); + } + + @Test + public void testSetBlobAsBinaryStream() throws SQLException, IOException { + InputStream bis = new ByteArrayInputStream("testContent".getBytes()); + lobCreator.setBlobAsBinaryStream(ps, 1, bis, 11); + verify(ps).setBinaryStream(1, bis, 11); + } + + @Test + public void testSetBlobAsBinaryStreamWithoutLength() throws SQLException, IOException { + InputStream bis = new ByteArrayInputStream("testContent".getBytes()); + lobCreator.setBlobAsBinaryStream(ps, 1, bis, -1); + verify(ps).setBinaryStream(1, bis); + } + + @Test + public void testSetClobAsString() throws SQLException, IOException { + String content = "testContent"; + lobCreator.setClobAsString(ps, 1, content); + verify(ps).setString(1, content); + } + + @Test + public void testSetClobAsAsciiStream() throws SQLException, IOException { + InputStream bis = new ByteArrayInputStream("testContent".getBytes()); + lobCreator.setClobAsAsciiStream(ps, 1, bis, 11); + verify(ps).setAsciiStream(1, bis, 11); + } + + @Test + public void testSetClobAsAsciiStreamWithoutLength() throws SQLException, IOException { + InputStream bis = new ByteArrayInputStream("testContent".getBytes()); + lobCreator.setClobAsAsciiStream(ps, 1, bis, -1); + verify(ps).setAsciiStream(1, bis); + } + + @Test + public void testSetClobAsCharacterStream() throws SQLException, IOException { + Reader str = new StringReader("testContent"); + lobCreator.setClobAsCharacterStream(ps, 1, str, 11); + verify(ps).setCharacterStream(1, str, 11); + } + + @Test + public void testSetClobAsCharacterStreamWithoutLength() throws SQLException, IOException { + Reader str = new StringReader("testContent"); + lobCreator.setClobAsCharacterStream(ps, 1, str, -1); + verify(ps).setCharacterStream(1, str); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/JdbcTransactionManagerTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/JdbcTransactionManagerTests.java new file mode 100644 index 0000000..7890fc7 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/JdbcTransactionManagerTests.java @@ -0,0 +1,1836 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Savepoint; +import java.sql.Statement; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.InOrder; + +import org.springframework.core.testfixture.EnabledForTestGroups; +import org.springframework.dao.ConcurrencyFailureException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.jdbc.UncategorizedSQLException; +import org.springframework.jdbc.datasource.ConnectionHolder; +import org.springframework.jdbc.datasource.ConnectionProxy; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy; +import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; +import org.springframework.transaction.CannotCreateTransactionException; +import org.springframework.transaction.IllegalTransactionStateException; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.TransactionSystemException; +import org.springframework.transaction.TransactionTimedOutException; +import org.springframework.transaction.UnexpectedRollbackException; +import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.springframework.transaction.support.TransactionCallbackWithoutResult; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.transaction.support.TransactionTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; + +/** + * @author Juergen Hoeller + * @since 5.3 + * @see org.springframework.jdbc.datasource.DataSourceTransactionManagerTests + */ +public class JdbcTransactionManagerTests { + + private DataSource ds; + + private Connection con; + + private JdbcTransactionManager tm; + + + @BeforeEach + public void setup() throws Exception { + ds = mock(DataSource.class); + con = mock(Connection.class); + given(ds.getConnection()).willReturn(con); + tm = new JdbcTransactionManager(ds); + } + + @AfterEach + public void verifyTransactionSynchronizationManagerState() { + assertThat(TransactionSynchronizationManager.getResourceMap().isEmpty()).isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isFalse(); + } + + + @Test + public void testTransactionCommitWithAutoCommitTrue() throws Exception { + doTestTransactionCommitRestoringAutoCommit(true, false, false); + } + + @Test + public void testTransactionCommitWithAutoCommitFalse() throws Exception { + doTestTransactionCommitRestoringAutoCommit(false, false, false); + } + + @Test + public void testTransactionCommitWithAutoCommitTrueAndLazyConnection() throws Exception { + doTestTransactionCommitRestoringAutoCommit(true, true, false); + } + + @Test + public void testTransactionCommitWithAutoCommitFalseAndLazyConnection() throws Exception { + doTestTransactionCommitRestoringAutoCommit(false, true, false); + } + + @Test + public void testTransactionCommitWithAutoCommitTrueAndLazyConnectionAndStatementCreated() throws Exception { + doTestTransactionCommitRestoringAutoCommit(true, true, true); + } + + @Test + public void testTransactionCommitWithAutoCommitFalseAndLazyConnectionAndStatementCreated() throws Exception { + doTestTransactionCommitRestoringAutoCommit(false, true, true); + } + + private void doTestTransactionCommitRestoringAutoCommit( + boolean autoCommit, boolean lazyConnection, final boolean createStatement) throws Exception { + + if (lazyConnection) { + given(con.getAutoCommit()).willReturn(autoCommit); + given(con.getTransactionIsolation()).willReturn(Connection.TRANSACTION_READ_COMMITTED); + given(con.getWarnings()).willThrow(new SQLException()); + } + + if (!lazyConnection || createStatement) { + given(con.getAutoCommit()).willReturn(autoCommit); + } + + final DataSource dsToUse = (lazyConnection ? new LazyConnectionDataSourceProxy(ds) : ds); + tm = new JdbcTransactionManager(dsToUse); + TransactionTemplate tt = new TransactionTemplate(tm); + boolean condition3 = !TransactionSynchronizationManager.hasResource(dsToUse); + assertThat(condition3).as("Hasn't thread connection").isTrue(); + boolean condition2 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition2).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(dsToUse)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + Connection tCon = DataSourceUtils.getConnection(dsToUse); + try { + if (createStatement) { + tCon.createStatement(); + } + else { + tCon.getWarnings(); + tCon.clearWarnings(); + } + } + catch (SQLException ex) { + throw new UncategorizedSQLException("", "", ex); + } + } + }); + + boolean condition1 = !TransactionSynchronizationManager.hasResource(dsToUse); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + boolean condition = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition).as("Synchronization not active").isTrue(); + + if (autoCommit && (!lazyConnection || createStatement)) { + InOrder ordered = inOrder(con); + ordered.verify(con).setAutoCommit(false); + ordered.verify(con).commit(); + ordered.verify(con).setAutoCommit(true); + } + if (createStatement) { + verify(con, times(2)).close(); + } + else { + verify(con).close(); + } + } + + @Test + public void testTransactionRollbackWithAutoCommitTrue() throws Exception { + doTestTransactionRollbackRestoringAutoCommit(true, false, false); + } + + @Test + public void testTransactionRollbackWithAutoCommitFalse() throws Exception { + doTestTransactionRollbackRestoringAutoCommit(false, false, false); + } + + @Test + public void testTransactionRollbackWithAutoCommitTrueAndLazyConnection() throws Exception { + doTestTransactionRollbackRestoringAutoCommit(true, true, false); + } + + @Test + public void testTransactionRollbackWithAutoCommitFalseAndLazyConnection() throws Exception { + doTestTransactionRollbackRestoringAutoCommit(false, true, false); + } + + @Test + public void testTransactionRollbackWithAutoCommitTrueAndLazyConnectionAndCreateStatement() throws Exception { + doTestTransactionRollbackRestoringAutoCommit(true, true, true); + } + + @Test + public void testTransactionRollbackWithAutoCommitFalseAndLazyConnectionAndCreateStatement() throws Exception { + doTestTransactionRollbackRestoringAutoCommit(false, true, true); + } + + private void doTestTransactionRollbackRestoringAutoCommit( + boolean autoCommit, boolean lazyConnection, final boolean createStatement) throws Exception { + + if (lazyConnection) { + given(con.getAutoCommit()).willReturn(autoCommit); + given(con.getTransactionIsolation()).willReturn(Connection.TRANSACTION_READ_COMMITTED); + } + + if (!lazyConnection || createStatement) { + given(con.getAutoCommit()).willReturn(autoCommit); + } + + final DataSource dsToUse = (lazyConnection ? new LazyConnectionDataSourceProxy(ds) : ds); + tm = new JdbcTransactionManager(dsToUse); + TransactionTemplate tt = new TransactionTemplate(tm); + boolean condition3 = !TransactionSynchronizationManager.hasResource(dsToUse); + assertThat(condition3).as("Hasn't thread connection").isTrue(); + boolean condition2 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition2).as("Synchronization not active").isTrue(); + + final RuntimeException ex = new RuntimeException("Application exception"); + assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(dsToUse)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + Connection con = DataSourceUtils.getConnection(dsToUse); + if (createStatement) { + try { + con.createStatement(); + } + catch (SQLException ex) { + throw new UncategorizedSQLException("", "", ex); + } + } + throw ex; + } + })) + .isEqualTo(ex); + + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + boolean condition = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition).as("Synchronization not active").isTrue(); + + if (autoCommit && (!lazyConnection || createStatement)) { + InOrder ordered = inOrder(con); + ordered.verify(con).setAutoCommit(false); + ordered.verify(con).rollback(); + ordered.verify(con).setAutoCommit(true); + } + if (createStatement) { + verify(con, times(2)).close(); + } + else { + verify(con).close(); + } + } + + @Test + public void testTransactionRollbackOnly() throws Exception { + tm.setTransactionSynchronization(JdbcTransactionManager.SYNCHRONIZATION_NEVER); + TransactionTemplate tt = new TransactionTemplate(tm); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + ConnectionHolder conHolder = new ConnectionHolder(con, true); + TransactionSynchronizationManager.bindResource(ds, conHolder); + final RuntimeException ex = new RuntimeException("Application exception"); + try { + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Is existing transaction").isTrue(); + throw ex; + } + }); + fail("Should have thrown RuntimeException"); + } + catch (RuntimeException ex2) { + // expected + boolean condition = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition).as("Synchronization not active").isTrue(); + assertThat(ex2).as("Correct exception thrown").isEqualTo(ex); + } + finally { + TransactionSynchronizationManager.unbindResource(ds); + } + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + } + + @Test + public void testParticipatingTransactionWithRollbackOnly() throws Exception { + doTestParticipatingTransactionWithRollbackOnly(false); + } + + @Test + public void testParticipatingTransactionWithRollbackOnlyAndFailEarly() throws Exception { + doTestParticipatingTransactionWithRollbackOnly(true); + } + + private void doTestParticipatingTransactionWithRollbackOnly(boolean failEarly) throws Exception { + given(con.isReadOnly()).willReturn(false); + if (failEarly) { + tm.setFailEarlyOnGlobalRollbackOnly(true); + } + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + TransactionStatus ts = tm.getTransaction(new DefaultTransactionDefinition()); + TestTransactionSynchronization synch = + new TestTransactionSynchronization(ds, TransactionSynchronization.STATUS_ROLLED_BACK); + TransactionSynchronizationManager.registerSynchronization(synch); + + boolean outerTransactionBoundaryReached = false; + try { + assertThat(ts.isNewTransaction()).as("Is new transaction").isTrue(); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + boolean condition1 = !status.isNewTransaction(); + assertThat(condition1).as("Is existing transaction").isTrue(); + assertThat(status.isRollbackOnly()).as("Is not rollback-only").isFalse(); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Is existing transaction").isTrue(); + status.setRollbackOnly(); + } + }); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Is existing transaction").isTrue(); + assertThat(status.isRollbackOnly()).as("Is rollback-only").isTrue(); + } + }); + + outerTransactionBoundaryReached = true; + tm.commit(ts); + + fail("Should have thrown UnexpectedRollbackException"); + } + catch (UnexpectedRollbackException ex) { + // expected + if (!outerTransactionBoundaryReached) { + tm.rollback(ts); + } + if (failEarly) { + assertThat(outerTransactionBoundaryReached).isFalse(); + } + else { + assertThat(outerTransactionBoundaryReached).isTrue(); + } + } + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + assertThat(synch.beforeCommitCalled).isFalse(); + assertThat(synch.beforeCompletionCalled).isTrue(); + assertThat(synch.afterCommitCalled).isFalse(); + assertThat(synch.afterCompletionCalled).isTrue(); + verify(con).rollback(); + verify(con).close(); + } + + @Test + public void testParticipatingTransactionWithIncompatibleIsolationLevel() throws Exception { + tm.setValidateExistingTransaction(true); + + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + assertThatExceptionOfType(IllegalTransactionStateException.class).isThrownBy(() -> { + final TransactionTemplate tt = new TransactionTemplate(tm); + final TransactionTemplate tt2 = new TransactionTemplate(tm); + tt2.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isRollbackOnly()).as("Is not rollback-only").isFalse(); + tt2.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + status.setRollbackOnly(); + } + }); + assertThat(status.isRollbackOnly()).as("Is rollback-only").isTrue(); + } + }); + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(); + verify(con).close(); + } + + @Test + public void testParticipatingTransactionWithIncompatibleReadOnly() throws Exception { + willThrow(new SQLException("read-only not supported")).given(con).setReadOnly(true); + tm.setValidateExistingTransaction(true); + + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + assertThatExceptionOfType(IllegalTransactionStateException.class).isThrownBy(() -> { + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setReadOnly(true); + final TransactionTemplate tt2 = new TransactionTemplate(tm); + tt2.setReadOnly(false); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isRollbackOnly()).as("Is not rollback-only").isFalse(); + tt2.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + status.setRollbackOnly(); + } + }); + assertThat(status.isRollbackOnly()).as("Is rollback-only").isTrue(); + } + }); + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(); + verify(con).close(); + } + + @Test + public void testParticipatingTransactionWithTransactionStartedFromSynch() throws Exception { + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + + final TestTransactionSynchronization synch = + new TestTransactionSynchronization(ds, TransactionSynchronization.STATUS_COMMITTED) { + @Override + protected void doAfterCompletion(int status) { + super.doAfterCompletion(status); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + } + }); + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {}); + } + }; + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + TransactionSynchronizationManager.registerSynchronization(synch); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + assertThat(synch.beforeCommitCalled).isTrue(); + assertThat(synch.beforeCompletionCalled).isTrue(); + assertThat(synch.afterCommitCalled).isTrue(); + assertThat(synch.afterCompletionCalled).isTrue(); + boolean condition3 = synch.afterCompletionException instanceof IllegalStateException; + assertThat(condition3).isTrue(); + verify(con, times(2)).commit(); + verify(con, times(2)).close(); + } + + @Test + public void testParticipatingTransactionWithDifferentConnectionObtainedFromSynch() throws Exception { + DataSource ds2 = mock(DataSource.class); + final Connection con2 = mock(Connection.class); + given(ds2.getConnection()).willReturn(con2); + + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + final TransactionTemplate tt = new TransactionTemplate(tm); + + final TestTransactionSynchronization synch = + new TestTransactionSynchronization(ds, TransactionSynchronization.STATUS_COMMITTED) { + @Override + protected void doAfterCompletion(int status) { + super.doAfterCompletion(status); + Connection con = DataSourceUtils.getConnection(ds2); + DataSourceUtils.releaseConnection(con, ds2); + } + }; + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + TransactionSynchronizationManager.registerSynchronization(synch); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + assertThat(synch.beforeCommitCalled).isTrue(); + assertThat(synch.beforeCompletionCalled).isTrue(); + assertThat(synch.afterCommitCalled).isTrue(); + assertThat(synch.afterCompletionCalled).isTrue(); + assertThat(synch.afterCompletionException).isNull(); + verify(con).commit(); + verify(con).close(); + verify(con2).close(); + } + + @Test + public void testParticipatingTransactionWithRollbackOnlyAndInnerSynch() throws Exception { + tm.setTransactionSynchronization(JdbcTransactionManager.SYNCHRONIZATION_NEVER); + JdbcTransactionManager tm2 = new JdbcTransactionManager(ds); + // tm has no synch enabled (used at outer level), tm2 has synch enabled (inner level) + + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + TransactionStatus ts = tm.getTransaction(new DefaultTransactionDefinition()); + final TestTransactionSynchronization synch = + new TestTransactionSynchronization(ds, TransactionSynchronization.STATUS_UNKNOWN); + + assertThatExceptionOfType(UnexpectedRollbackException.class).isThrownBy(() -> { + assertThat(ts.isNewTransaction()).as("Is new transaction").isTrue(); + final TransactionTemplate tt = new TransactionTemplate(tm2); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + boolean condition1 = !status.isNewTransaction(); + assertThat(condition1).as("Is existing transaction").isTrue(); + assertThat(status.isRollbackOnly()).as("Is not rollback-only").isFalse(); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Is existing transaction").isTrue(); + status.setRollbackOnly(); + } + }); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Is existing transaction").isTrue(); + assertThat(status.isRollbackOnly()).as("Is rollback-only").isTrue(); + TransactionSynchronizationManager.registerSynchronization(synch); + } + }); + + tm.commit(ts); + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + assertThat(synch.beforeCommitCalled).isFalse(); + assertThat(synch.beforeCompletionCalled).isTrue(); + assertThat(synch.afterCommitCalled).isFalse(); + assertThat(synch.afterCompletionCalled).isTrue(); + verify(con).rollback(); + verify(con).close(); + } + + @Test + public void testPropagationRequiresNewWithExistingTransaction() throws Exception { + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + status.setRollbackOnly(); + } + }); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(); + verify(con).commit(); + verify(con, times(2)).close(); + } + + @Test + public void testPropagationRequiresNewWithExistingTransactionAndUnrelatedDataSource() throws Exception { + Connection con2 = mock(Connection.class); + final DataSource ds2 = mock(DataSource.class); + given(ds2.getConnection()).willReturn(con2); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + + PlatformTransactionManager tm2 = new JdbcTransactionManager(ds2); + final TransactionTemplate tt2 = new TransactionTemplate(tm2); + tt2.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + + boolean condition4 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition4).as("Hasn't thread connection").isTrue(); + boolean condition3 = !TransactionSynchronizationManager.hasResource(ds2); + assertThat(condition3).as("Hasn't thread connection").isTrue(); + boolean condition2 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition2).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + tt2.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + status.setRollbackOnly(); + } + }); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + } + }); + + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + boolean condition = !TransactionSynchronizationManager.hasResource(ds2); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).commit(); + verify(con).close(); + verify(con2).rollback(); + verify(con2).close(); + } + + @Test + public void testPropagationRequiresNewWithExistingTransactionAndUnrelatedFailingDataSource() throws Exception { + final DataSource ds2 = mock(DataSource.class); + SQLException failure = new SQLException(); + given(ds2.getConnection()).willThrow(failure); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + + JdbcTransactionManager tm2 = new JdbcTransactionManager(ds2); + tm2.setTransactionSynchronization(JdbcTransactionManager.SYNCHRONIZATION_NEVER); + final TransactionTemplate tt2 = new TransactionTemplate(tm2); + tt2.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + + boolean condition4 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition4).as("Hasn't thread connection").isTrue(); + boolean condition3 = !TransactionSynchronizationManager.hasResource(ds2); + assertThat(condition3).as("Hasn't thread connection").isTrue(); + boolean condition2 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition2).as("Synchronization not active").isTrue(); + + assertThatExceptionOfType(CannotCreateTransactionException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + tt2.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + status.setRollbackOnly(); + } + }); + } + })).withCause(failure); + + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + boolean condition = !TransactionSynchronizationManager.hasResource(ds2); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(); + verify(con).close(); + } + + @Test + public void testPropagationNotSupportedWithExistingTransaction() throws Exception { + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NOT_SUPPORTED); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Isn't new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isFalse(); + status.setRollbackOnly(); + } + }); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).commit(); + verify(con).close(); + } + + @Test + public void testPropagationNeverWithExistingTransaction() throws Exception { + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + assertThatExceptionOfType(IllegalTransactionStateException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NEVER); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + fail("Should have thrown IllegalTransactionStateException"); + } + }); + fail("Should have thrown IllegalTransactionStateException"); + } + })); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(); + verify(con).close(); + } + + @Test + public void testPropagationSupportsAndRequiresNew() throws Exception { + TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + TransactionTemplate tt2 = new TransactionTemplate(tm); + tt2.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + tt2.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(DataSourceUtils.getConnection(ds)).isSameAs(con); + assertThat(DataSourceUtils.getConnection(ds)).isSameAs(con); + } + }); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).commit(); + verify(con).close(); + } + + @Test + public void testPropagationSupportsAndRequiresNewWithEarlyAccess() throws Exception { + final Connection con1 = mock(Connection.class); + final Connection con2 = mock(Connection.class); + given(ds.getConnection()).willReturn(con1, con2); + + final + TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + assertThat(DataSourceUtils.getConnection(ds)).isSameAs(con1); + assertThat(DataSourceUtils.getConnection(ds)).isSameAs(con1); + TransactionTemplate tt2 = new TransactionTemplate(tm); + tt2.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + tt2.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + assertThat(DataSourceUtils.getConnection(ds)).isSameAs(con2); + assertThat(DataSourceUtils.getConnection(ds)).isSameAs(con2); + } + }); + assertThat(DataSourceUtils.getConnection(ds)).isSameAs(con1); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con1).close(); + verify(con2).commit(); + verify(con2).close(); + } + + @Test + public void testTransactionWithIsolationAndReadOnly() throws Exception { + given(con.getTransactionIsolation()).willReturn(Connection.TRANSACTION_READ_COMMITTED); + given(con.getAutoCommit()).willReturn(true); + + TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + tt.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE); + tt.setReadOnly(true); + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isTrue(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + // something transactional + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + InOrder ordered = inOrder(con); + ordered.verify(con).setReadOnly(true); + ordered.verify(con).setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); + ordered.verify(con).setAutoCommit(false); + ordered.verify(con).commit(); + ordered.verify(con).setAutoCommit(true); + ordered.verify(con).setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); + ordered.verify(con).setReadOnly(false); + verify(con).close(); + } + + @Test + public void testTransactionWithEnforceReadOnly() throws Exception { + tm.setEnforceReadOnly(true); + + given(con.getAutoCommit()).willReturn(true); + Statement stmt = mock(Statement.class); + given(con.createStatement()).willReturn(stmt); + + TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + tt.setReadOnly(true); + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isTrue(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isTrue(); + // something transactional + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + InOrder ordered = inOrder(con, stmt); + ordered.verify(con).setReadOnly(true); + ordered.verify(con).setAutoCommit(false); + ordered.verify(stmt).executeUpdate("SET TRANSACTION READ ONLY"); + ordered.verify(stmt).close(); + ordered.verify(con).commit(); + ordered.verify(con).setAutoCommit(true); + ordered.verify(con).setReadOnly(false); + ordered.verify(con).close(); + } + + @ParameterizedTest(name = "transaction with {0} second timeout") + @ValueSource(ints = {1, 10}) + @EnabledForTestGroups(LONG_RUNNING) + public void transactionWithTimeout(int timeout) throws Exception { + PreparedStatement ps = mock(PreparedStatement.class); + given(con.getAutoCommit()).willReturn(true); + given(con.prepareStatement("some SQL statement")).willReturn(ps); + + TransactionTemplate tt = new TransactionTemplate(tm); + tt.setTimeout(timeout); + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + + try { + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + try { + Thread.sleep(1500); + } + catch (InterruptedException ex) { + } + try { + Connection con = DataSourceUtils.getConnection(ds); + PreparedStatement ps = con.prepareStatement("some SQL statement"); + DataSourceUtils.applyTransactionTimeout(ps, ds); + } + catch (SQLException ex) { + throw new DataAccessResourceFailureException("", ex); + } + } + }); + if (timeout <= 1) { + fail("Should have thrown TransactionTimedOutException"); + } + } + catch (TransactionTimedOutException ex) { + if (timeout <= 1) { + // expected + } + else { + throw ex; + } + } + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + if (timeout > 1) { + verify(ps).setQueryTimeout(timeout - 1); + verify(con).commit(); + } + else { + verify(con).rollback(); + } + InOrder ordered = inOrder(con); + ordered.verify(con).setAutoCommit(false); + ordered.verify(con).setAutoCommit(true); + verify(con).close(); + } + + @Test + public void testTransactionAwareDataSourceProxy() throws Exception { + given(con.getAutoCommit()).willReturn(true); + given(con.getWarnings()).willThrow(new SQLException()); + + TransactionTemplate tt = new TransactionTemplate(tm); + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + // something transactional + assertThat(DataSourceUtils.getConnection(ds)).isEqualTo(con); + TransactionAwareDataSourceProxy dsProxy = new TransactionAwareDataSourceProxy(ds); + try { + Connection tCon = dsProxy.getConnection(); + tCon.getWarnings(); + tCon.clearWarnings(); + assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con); + // should be ignored + dsProxy.getConnection().close(); + } + catch (SQLException ex) { + throw new UncategorizedSQLException("", "", ex); + } + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + InOrder ordered = inOrder(con); + ordered.verify(con).setAutoCommit(false); + ordered.verify(con).commit(); + ordered.verify(con).setAutoCommit(true); + verify(con).close(); + } + + @Test + public void testTransactionAwareDataSourceProxyWithSuspension() throws Exception { + given(con.getAutoCommit()).willReturn(true); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW); + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + // something transactional + assertThat(DataSourceUtils.getConnection(ds)).isEqualTo(con); + final TransactionAwareDataSourceProxy dsProxy = new TransactionAwareDataSourceProxy(ds); + try { + assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con); + // should be ignored + dsProxy.getConnection().close(); + } + catch (SQLException ex) { + throw new UncategorizedSQLException("", "", ex); + } + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + // something transactional + assertThat(DataSourceUtils.getConnection(ds)).isEqualTo(con); + try { + assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con); + // should be ignored + dsProxy.getConnection().close(); + } + catch (SQLException ex) { + throw new UncategorizedSQLException("", "", ex); + } + } + }); + + try { + assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con); + // should be ignored + dsProxy.getConnection().close(); + } + catch (SQLException ex) { + throw new UncategorizedSQLException("", "", ex); + } + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + InOrder ordered = inOrder(con); + ordered.verify(con).setAutoCommit(false); + ordered.verify(con).commit(); + ordered.verify(con).setAutoCommit(true); + verify(con, times(2)).close(); + } + + @Test + public void testTransactionAwareDataSourceProxyWithSuspensionAndReobtaining() throws Exception { + given(con.getAutoCommit()).willReturn(true); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRES_NEW); + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + // something transactional + assertThat(DataSourceUtils.getConnection(ds)).isEqualTo(con); + final TransactionAwareDataSourceProxy dsProxy = new TransactionAwareDataSourceProxy(ds); + dsProxy.setReobtainTransactionalConnections(true); + try { + assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con); + // should be ignored + dsProxy.getConnection().close(); + } + catch (SQLException ex) { + throw new UncategorizedSQLException("", "", ex); + } + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + // something transactional + assertThat(DataSourceUtils.getConnection(ds)).isEqualTo(con); + try { + assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con); + // should be ignored + dsProxy.getConnection().close(); + } + catch (SQLException ex) { + throw new UncategorizedSQLException("", "", ex); + } + } + }); + + try { + assertThat(((ConnectionProxy) dsProxy.getConnection()).getTargetConnection()).isEqualTo(con); + // should be ignored + dsProxy.getConnection().close(); + } + catch (SQLException ex) { + throw new UncategorizedSQLException("", "", ex); + } + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + InOrder ordered = inOrder(con); + ordered.verify(con).setAutoCommit(false); + ordered.verify(con).commit(); + ordered.verify(con).setAutoCommit(true); + verify(con, times(2)).close(); + } + + /** + * Test behavior if the first operation on a connection (getAutoCommit) throws SQLException. + */ + @Test + public void testTransactionWithExceptionOnBegin() throws Exception { + willThrow(new SQLException("Cannot begin")).given(con).getAutoCommit(); + + TransactionTemplate tt = new TransactionTemplate(tm); + assertThatExceptionOfType(CannotCreateTransactionException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + // something transactional + } + })); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).close(); + } + + @Test + public void testTransactionWithExceptionOnCommit() throws Exception { + willThrow(new SQLException("Cannot commit")).given(con).commit(); + + TransactionTemplate tt = new TransactionTemplate(tm); + assertThatExceptionOfType(TransactionSystemException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + // something transactional + } + })); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).close(); + } + + @Test + public void testTransactionWithDataAccessExceptionOnCommit() throws Exception { + willThrow(new SQLException("Cannot commit")).given(con).commit(); + tm.setExceptionTranslator((task, sql, ex) -> new ConcurrencyFailureException(task)); + + TransactionTemplate tt = new TransactionTemplate(tm); + assertThatExceptionOfType(ConcurrencyFailureException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + // something transactional + } + })); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).close(); + } + + @Test + public void testTransactionWithDataAccessExceptionOnCommitFromLazyExceptionTranslator() throws Exception { + willThrow(new SQLException("Cannot commit", "40")).given(con).commit(); + + TransactionTemplate tt = new TransactionTemplate(tm); + assertThatExceptionOfType(ConcurrencyFailureException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + // something transactional + } + })); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).close(); + } + + @Test + public void testTransactionWithExceptionOnCommitAndRollbackOnCommitFailure() throws Exception { + willThrow(new SQLException("Cannot commit")).given(con).commit(); + + tm.setRollbackOnCommitFailure(true); + TransactionTemplate tt = new TransactionTemplate(tm); + assertThatExceptionOfType(TransactionSystemException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + // something transactional + } + })); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(); + verify(con).close(); + } + + @Test + public void testTransactionWithExceptionOnRollback() throws Exception { + given(con.getAutoCommit()).willReturn(true); + willThrow(new SQLException("Cannot rollback")).given(con).rollback(); + + TransactionTemplate tt = new TransactionTemplate(tm); + assertThatExceptionOfType(TransactionSystemException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + status.setRollbackOnly(); + } + })); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + InOrder ordered = inOrder(con); + ordered.verify(con).setAutoCommit(false); + ordered.verify(con).rollback(); + ordered.verify(con).setAutoCommit(true); + verify(con).close(); + } + + @Test + public void testTransactionWithDataAccessExceptionOnRollback() throws Exception { + given(con.getAutoCommit()).willReturn(true); + willThrow(new SQLException("Cannot rollback")).given(con).rollback(); + tm.setExceptionTranslator((task, sql, ex) -> new ConcurrencyFailureException(task)); + + TransactionTemplate tt = new TransactionTemplate(tm); + assertThatExceptionOfType(ConcurrencyFailureException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + status.setRollbackOnly(); + } + })); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + InOrder ordered = inOrder(con); + ordered.verify(con).setAutoCommit(false); + ordered.verify(con).rollback(); + ordered.verify(con).setAutoCommit(true); + verify(con).close(); + } + + @Test + public void testTransactionWithDataAccessExceptionOnRollbackFromLazyExceptionTranslator() throws Exception { + given(con.getAutoCommit()).willReturn(true); + willThrow(new SQLException("Cannot rollback", "40")).given(con).rollback(); + + TransactionTemplate tt = new TransactionTemplate(tm); + assertThatExceptionOfType(ConcurrencyFailureException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + status.setRollbackOnly(); + } + })); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + InOrder ordered = inOrder(con); + ordered.verify(con).setAutoCommit(false); + ordered.verify(con).rollback(); + ordered.verify(con).setAutoCommit(true); + verify(con).close(); + } + + @Test + public void testTransactionWithPropagationSupports() throws Exception { + TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Is not new transaction").isTrue(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isFalse(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + } + + @Test + public void testTransactionWithPropagationNotSupported() throws Exception { + TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NOT_SUPPORTED); + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Is not new transaction").isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + } + + @Test + public void testTransactionWithPropagationNever() throws Exception { + TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NEVER); + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + boolean condition1 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition1).as("Hasn't thread connection").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Is not new transaction").isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + } + + @Test + public void testExistingTransactionWithPropagationNested() throws Exception { + doTestExistingTransactionWithPropagationNested(1); + } + + @Test + public void testExistingTransactionWithPropagationNestedTwice() throws Exception { + doTestExistingTransactionWithPropagationNested(2); + } + + private void doTestExistingTransactionWithPropagationNested(final int count) throws Exception { + DatabaseMetaData md = mock(DatabaseMetaData.class); + Savepoint sp = mock(Savepoint.class); + + given(md.supportsSavepoints()).willReturn(true); + given(con.getMetaData()).willReturn(md); + for (int i = 1; i <= count; i++) { + given(con.setSavepoint(ConnectionHolder.SAVEPOINT_NAME_PREFIX + i)).willReturn(sp); + } + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + boolean condition1 = !status.hasSavepoint(); + assertThat(condition1).as("Isn't nested transaction").isTrue(); + for (int i = 0; i < count; i++) { + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Isn't new transaction").isTrue(); + assertThat(status.hasSavepoint()).as("Is nested transaction").isTrue(); + } + }); + } + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + boolean condition = !status.hasSavepoint(); + assertThat(condition).as("Isn't nested transaction").isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con, times(count)).releaseSavepoint(sp); + verify(con).commit(); + verify(con).close(); + } + + @Test + public void testExistingTransactionWithPropagationNestedAndRollback() throws Exception { + DatabaseMetaData md = mock(DatabaseMetaData.class); + Savepoint sp = mock(Savepoint.class); + + given(md.supportsSavepoints()).willReturn(true); + given(con.getMetaData()).willReturn(md); + given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + boolean condition1 = !status.hasSavepoint(); + assertThat(condition1).as("Isn't nested transaction").isTrue(); + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Isn't new transaction").isTrue(); + assertThat(status.hasSavepoint()).as("Is nested transaction").isTrue(); + status.setRollbackOnly(); + } + }); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + boolean condition = !status.hasSavepoint(); + assertThat(condition).as("Isn't nested transaction").isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(sp); + verify(con).releaseSavepoint(sp); + verify(con).commit(); + verify(con).close(); + } + + @Test + public void testExistingTransactionWithPropagationNestedAndRequiredRollback() throws Exception { + DatabaseMetaData md = mock(DatabaseMetaData.class); + Savepoint sp = mock(Savepoint.class); + + given(md.supportsSavepoints()).willReturn(true); + given(con.getMetaData()).willReturn(md); + given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + boolean condition1 = !status.hasSavepoint(); + assertThat(condition1).as("Isn't nested transaction").isTrue(); + assertThatIllegalStateException().isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Isn't new transaction").isTrue(); + assertThat(status.hasSavepoint()).as("Is nested transaction").isTrue(); + TransactionTemplate ntt = new TransactionTemplate(tm); + ntt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + boolean condition1 = !status.isNewTransaction(); + assertThat(condition1).as("Isn't new transaction").isTrue(); + boolean condition = !status.hasSavepoint(); + assertThat(condition).as("Is regular transaction").isTrue(); + throw new IllegalStateException(); + } + }); + } + })); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + boolean condition = !status.hasSavepoint(); + assertThat(condition).as("Isn't nested transaction").isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(sp); + verify(con).releaseSavepoint(sp); + verify(con).commit(); + verify(con).close(); + } + + @Test + public void testExistingTransactionWithPropagationNestedAndRequiredRollbackOnly() throws Exception { + DatabaseMetaData md = mock(DatabaseMetaData.class); + Savepoint sp = mock(Savepoint.class); + + given(md.supportsSavepoints()).willReturn(true); + given(con.getMetaData()).willReturn(md); + given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + boolean condition1 = !status.hasSavepoint(); + assertThat(condition1).as("Isn't nested transaction").isTrue(); + assertThatExceptionOfType(UnexpectedRollbackException.class).isThrownBy(() -> + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + boolean condition = !status.isNewTransaction(); + assertThat(condition).as("Isn't new transaction").isTrue(); + assertThat(status.hasSavepoint()).as("Is nested transaction").isTrue(); + TransactionTemplate ntt = new TransactionTemplate(tm); + ntt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(TransactionSynchronizationManager.hasResource(ds)).as("Has thread connection").isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).as("Synchronization active").isTrue(); + boolean condition1 = !status.isNewTransaction(); + assertThat(condition1).as("Isn't new transaction").isTrue(); + boolean condition = !status.hasSavepoint(); + assertThat(condition).as("Is regular transaction").isTrue(); + status.setRollbackOnly(); + } + }); + } + })); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + boolean condition = !status.hasSavepoint(); + assertThat(condition).as("Isn't nested transaction").isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(sp); + verify(con).releaseSavepoint(sp); + verify(con).commit(); + verify(con).close(); + } + + @Test + public void testExistingTransactionWithManualSavepoint() throws Exception { + DatabaseMetaData md = mock(DatabaseMetaData.class); + Savepoint sp = mock(Savepoint.class); + + given(md.supportsSavepoints()).willReturn(true); + given(con.getMetaData()).willReturn(md); + given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + Object savepoint = status.createSavepoint(); + status.releaseSavepoint(savepoint); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).releaseSavepoint(sp); + verify(con).commit(); + verify(con).close(); + verify(ds).getConnection(); + } + + @Test + public void testExistingTransactionWithManualSavepointAndRollback() throws Exception { + DatabaseMetaData md = mock(DatabaseMetaData.class); + Savepoint sp = mock(Savepoint.class); + + given(md.supportsSavepoints()).willReturn(true); + given(con.getMetaData()).willReturn(md); + given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp); + + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + Object savepoint = status.createSavepoint(); + status.rollbackToSavepoint(savepoint); + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(sp); + verify(con).commit(); + verify(con).close(); + } + + @Test + public void testTransactionWithPropagationNested() throws Exception { + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).commit(); + verify(con).close(); + } + + @Test + public void testTransactionWithPropagationNestedAndRollback() throws Exception { + final TransactionTemplate tt = new TransactionTemplate(tm); + tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED); + boolean condition2 = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition2).as("Hasn't thread connection").isTrue(); + boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive(); + assertThat(condition1).as("Synchronization not active").isTrue(); + + tt.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException { + assertThat(status.isNewTransaction()).as("Is new transaction").isTrue(); + status.setRollbackOnly(); + } + }); + + boolean condition = !TransactionSynchronizationManager.hasResource(ds); + assertThat(condition).as("Hasn't thread connection").isTrue(); + verify(con).rollback(); + verify(con).close(); + } + + + private static class TestTransactionSynchronization implements TransactionSynchronization { + + private DataSource dataSource; + + private int status; + + public boolean beforeCommitCalled; + + public boolean beforeCompletionCalled; + + public boolean afterCommitCalled; + + public boolean afterCompletionCalled; + + public Throwable afterCompletionException; + + public TestTransactionSynchronization(DataSource dataSource, int status) { + this.dataSource = dataSource; + this.status = status; + } + + @Override + public void suspend() { + } + + @Override + public void resume() { + } + + @Override + public void flush() { + } + + @Override + public void beforeCommit(boolean readOnly) { + if (this.status != TransactionSynchronization.STATUS_COMMITTED) { + fail("Should never be called"); + } + assertThat(this.beforeCommitCalled).isFalse(); + this.beforeCommitCalled = true; + } + + @Override + public void beforeCompletion() { + assertThat(this.beforeCompletionCalled).isFalse(); + this.beforeCompletionCalled = true; + } + + @Override + public void afterCommit() { + if (this.status != TransactionSynchronization.STATUS_COMMITTED) { + fail("Should never be called"); + } + assertThat(this.afterCommitCalled).isFalse(); + this.afterCommitCalled = true; + } + + @Override + public void afterCompletion(int status) { + try { + doAfterCompletion(status); + } + catch (Throwable ex) { + this.afterCompletionException = ex; + } + } + + protected void doAfterCompletion(int status) { + assertThat(this.afterCompletionCalled).isFalse(); + this.afterCompletionCalled = true; + assertThat(status == this.status).isTrue(); + assertThat(TransactionSynchronizationManager.hasResource(this.dataSource)).isTrue(); + } + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/JdbcUtilsTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/JdbcUtilsTests.java new file mode 100644 index 0000000..7005193 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/JdbcUtilsTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import java.sql.Types; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link JdbcUtils}. + * + * @author Thomas Risberg + * @author Juergen Hoeller + */ +public class JdbcUtilsTests { + + @Test + public void commonDatabaseName() { + assertThat(JdbcUtils.commonDatabaseName("Oracle")).isEqualTo("Oracle"); + assertThat(JdbcUtils.commonDatabaseName("DB2-for-Spring")).isEqualTo("DB2"); + assertThat(JdbcUtils.commonDatabaseName("Sybase SQL Server")).isEqualTo("Sybase"); + assertThat(JdbcUtils.commonDatabaseName("Adaptive Server Enterprise")).isEqualTo("Sybase"); + assertThat(JdbcUtils.commonDatabaseName("MySQL")).isEqualTo("MySQL"); + } + + @Test + public void resolveTypeName() { + assertThat(JdbcUtils.resolveTypeName(Types.VARCHAR)).isEqualTo("VARCHAR"); + assertThat(JdbcUtils.resolveTypeName(Types.NUMERIC)).isEqualTo("NUMERIC"); + assertThat(JdbcUtils.resolveTypeName(Types.INTEGER)).isEqualTo("INTEGER"); + assertThat(JdbcUtils.resolveTypeName(JdbcUtils.TYPE_UNKNOWN)).isNull(); + } + + @Test + public void convertUnderscoreNameToPropertyName() { + assertThat(JdbcUtils.convertUnderscoreNameToPropertyName("MY_NAME")).isEqualTo("myName"); + assertThat(JdbcUtils.convertUnderscoreNameToPropertyName("yOUR_nAME")).isEqualTo("yourName"); + assertThat(JdbcUtils.convertUnderscoreNameToPropertyName("a_name")).isEqualTo("AName"); + assertThat(JdbcUtils.convertUnderscoreNameToPropertyName("someone_elses_name")).isEqualTo("someoneElsesName"); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/KeyHolderTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/KeyHolderTests.java new file mode 100644 index 0000000..c128b8f --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/KeyHolderTests.java @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.dao.InvalidDataAccessApiUsageException; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link KeyHolder} and {@link GeneratedKeyHolder}. + * + * @author Thomas Risberg + * @author Sam Brannen + * @since July 18, 2004 + */ +class KeyHolderTests { + + private final KeyHolder kh = new GeneratedKeyHolder(); + + + @Test + void getKeyForSingleNumericKey() { + kh.getKeyList().add(singletonMap("key", 1)); + + assertThat(kh.getKey()).as("single key should be returned").isEqualTo(1); + } + + @Test + void getKeyForSingleNonNumericKey() { + kh.getKeyList().add(singletonMap("key", "ABC")); + + assertThatExceptionOfType(DataRetrievalFailureException.class) + .isThrownBy(() -> kh.getKey()) + .withMessage("The generated key type is not supported. Unable to cast [java.lang.String] to [java.lang.Number]."); + } + + @Test + void getKeyWithNoKeysInMap() { + kh.getKeyList().add(emptyMap()); + + assertThatExceptionOfType(DataRetrievalFailureException.class) + .isThrownBy(() -> kh.getKey()) + .withMessageStartingWith("Unable to retrieve the generated key."); + } + + @Test + void getKeyWithMultipleKeysInMap() { + @SuppressWarnings("serial") + Map m = new HashMap() {{ + put("key", 1); + put("seq", 2); + }}; + kh.getKeyList().add(m); + + assertThat(kh.getKeys()).as("two keys should be in the map").hasSize(2); + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) + .isThrownBy(() -> kh.getKey()) + .withMessageStartingWith("The getKey method should only be used when a single key is returned."); + } + + @Test + void getKeyAsStringForSingleKey() { + kh.getKeyList().add(singletonMap("key", "ABC")); + + assertThat(kh.getKeyAs(String.class)).as("single key should be returned").isEqualTo("ABC"); + } + + @Test + void getKeyAsWrongType() { + kh.getKeyList().add(singletonMap("key", "ABC")); + + assertThatExceptionOfType(DataRetrievalFailureException.class) + .isThrownBy(() -> kh.getKeyAs(Integer.class)) + .withMessage("The generated key type is not supported. Unable to cast [java.lang.String] to [java.lang.Integer]."); + } + + @Test + void getKeyAsIntegerWithNullValue() { + kh.getKeyList().add(singletonMap("key", null)); + + assertThatExceptionOfType(DataRetrievalFailureException.class) + .isThrownBy(() -> kh.getKeyAs(Integer.class)) + .withMessage("The generated key type is not supported. Unable to cast [null] to [java.lang.Integer]."); + } + + @Test + void getKeysWithMultipleKeyRows() { + @SuppressWarnings("serial") + Map m = new HashMap() {{ + put("key", 1); + put("seq", 2); + }}; + kh.getKeyList().addAll(asList(m, m)); + + assertThat(kh.getKeyList()).as("two rows should be in the list").hasSize(2); + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) + .isThrownBy(() -> kh.getKeys()) + .withMessageStartingWith("The getKeys method should only be used when keys for a single row are returned."); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslatorTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslatorTests.java new file mode 100644 index 0000000..5cdfb7e --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslatorTests.java @@ -0,0 +1,210 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import java.sql.BatchUpdateException; +import java.sql.Connection; +import java.sql.DataTruncation; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.dao.CannotAcquireLockException; +import org.springframework.dao.CannotSerializeTransactionException; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.DeadlockLoserDataAccessException; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.jdbc.InvalidResultSetAccessException; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author Rod Johnson + * @author Juergen Hoeller + * @author Sam Brannen + */ +public class SQLErrorCodeSQLExceptionTranslatorTests { + + private static SQLErrorCodes ERROR_CODES = new SQLErrorCodes(); + static { + ERROR_CODES.setBadSqlGrammarCodes("1", "2"); + ERROR_CODES.setInvalidResultSetAccessCodes("3", "4"); + ERROR_CODES.setDuplicateKeyCodes("10"); + ERROR_CODES.setDataAccessResourceFailureCodes("5"); + ERROR_CODES.setDataIntegrityViolationCodes("6"); + ERROR_CODES.setCannotAcquireLockCodes("7"); + ERROR_CODES.setDeadlockLoserCodes("8"); + ERROR_CODES.setCannotSerializeTransactionCodes("9"); + } + + + @Test + public void errorCodeTranslation() { + SQLExceptionTranslator sext = new SQLErrorCodeSQLExceptionTranslator(ERROR_CODES); + + SQLException badSqlEx = new SQLException("", "", 1); + BadSqlGrammarException bsgex = (BadSqlGrammarException) sext.translate("task", "SQL", badSqlEx); + assertThat(bsgex.getSql()).isEqualTo("SQL"); + assertThat((Object) bsgex.getSQLException()).isEqualTo(badSqlEx); + + SQLException invResEx = new SQLException("", "", 4); + InvalidResultSetAccessException irsex = (InvalidResultSetAccessException) sext.translate("task", "SQL", invResEx); + assertThat(irsex.getSql()).isEqualTo("SQL"); + assertThat((Object) irsex.getSQLException()).isEqualTo(invResEx); + + checkTranslation(sext, 5, DataAccessResourceFailureException.class); + checkTranslation(sext, 6, DataIntegrityViolationException.class); + checkTranslation(sext, 7, CannotAcquireLockException.class); + checkTranslation(sext, 8, DeadlockLoserDataAccessException.class); + checkTranslation(sext, 9, CannotSerializeTransactionException.class); + checkTranslation(sext, 10, DuplicateKeyException.class); + + SQLException dupKeyEx = new SQLException("", "", 10); + DataAccessException dksex = sext.translate("task", "SQL", dupKeyEx); + assertThat(DataIntegrityViolationException.class.isInstance(dksex)).as("Not instance of DataIntegrityViolationException").isTrue(); + + // Test fallback. We assume that no database will ever return this error code, + // but 07xxx will be bad grammar picked up by the fallback SQLState translator + SQLException sex = new SQLException("", "07xxx", 666666666); + BadSqlGrammarException bsgex2 = (BadSqlGrammarException) sext.translate("task", "SQL2", sex); + assertThat(bsgex2.getSql()).isEqualTo("SQL2"); + assertThat((Object) bsgex2.getSQLException()).isEqualTo(sex); + } + + private void checkTranslation(SQLExceptionTranslator sext, int errorCode, Class exClass) { + SQLException sex = new SQLException("", "", errorCode); + DataAccessException ex = sext.translate("", "", sex); + assertThat(exClass.isInstance(ex)).isTrue(); + assertThat(ex.getCause() == sex).isTrue(); + } + + @Test + public void batchExceptionTranslation() { + SQLExceptionTranslator sext = new SQLErrorCodeSQLExceptionTranslator(ERROR_CODES); + + SQLException badSqlEx = new SQLException("", "", 1); + BatchUpdateException batchUpdateEx = new BatchUpdateException(); + batchUpdateEx.setNextException(badSqlEx); + BadSqlGrammarException bsgex = (BadSqlGrammarException) sext.translate("task", "SQL", batchUpdateEx); + assertThat(bsgex.getSql()).isEqualTo("SQL"); + assertThat((Object) bsgex.getSQLException()).isEqualTo(badSqlEx); + } + + @Test + public void dataTruncationTranslation() { + SQLExceptionTranslator sext = new SQLErrorCodeSQLExceptionTranslator(ERROR_CODES); + + SQLException dataAccessEx = new SQLException("", "", 5); + DataTruncation dataTruncation = new DataTruncation(1, true, true, 1, 1, dataAccessEx); + DataAccessResourceFailureException daex = (DataAccessResourceFailureException) sext.translate("task", "SQL", dataTruncation); + assertThat(daex.getCause()).isEqualTo(dataTruncation); + } + + @SuppressWarnings("serial") + @Test + public void customTranslateMethodTranslation() { + final String TASK = "TASK"; + final String SQL = "SQL SELECT *"; + final DataAccessException customDex = new DataAccessException("") {}; + + final SQLException badSqlEx = new SQLException("", "", 1); + SQLException intVioEx = new SQLException("", "", 6); + + SQLErrorCodeSQLExceptionTranslator sext = new SQLErrorCodeSQLExceptionTranslator() { + @Override + @Nullable + protected DataAccessException customTranslate(String task, @Nullable String sql, SQLException sqlex) { + assertThat(task).isEqualTo(TASK); + assertThat(sql).isEqualTo(SQL); + return (sqlex == badSqlEx) ? customDex : null; + } + }; + sext.setSqlErrorCodes(ERROR_CODES); + + // Shouldn't custom translate this + assertThat(sext.translate(TASK, SQL, badSqlEx)).isEqualTo(customDex); + DataIntegrityViolationException diex = (DataIntegrityViolationException) sext.translate(TASK, SQL, intVioEx); + assertThat(diex.getCause()).isEqualTo(intVioEx); + } + + @Test + public void customExceptionTranslation() { + final String TASK = "TASK"; + final String SQL = "SQL SELECT *"; + final SQLErrorCodes customErrorCodes = new SQLErrorCodes(); + final CustomSQLErrorCodesTranslation customTranslation = new CustomSQLErrorCodesTranslation(); + + customErrorCodes.setBadSqlGrammarCodes("1", "2"); + customErrorCodes.setDataIntegrityViolationCodes("3", "4"); + customTranslation.setErrorCodes("1"); + customTranslation.setExceptionClass(CustomErrorCodeException.class); + customErrorCodes.setCustomTranslations(customTranslation); + + SQLErrorCodeSQLExceptionTranslator sext = new SQLErrorCodeSQLExceptionTranslator(customErrorCodes); + + // Should custom translate this + SQLException badSqlEx = new SQLException("", "", 1); + assertThat(sext.translate(TASK, SQL, badSqlEx).getClass()).isEqualTo(CustomErrorCodeException.class); + assertThat(sext.translate(TASK, SQL, badSqlEx).getCause()).isEqualTo(badSqlEx); + + // Shouldn't custom translate this + SQLException invResEx = new SQLException("", "", 3); + DataIntegrityViolationException diex = (DataIntegrityViolationException) sext.translate(TASK, SQL, invResEx); + assertThat(diex.getCause()).isEqualTo(invResEx); + + // Shouldn't custom translate this - invalid class + assertThatIllegalArgumentException().isThrownBy(() -> + customTranslation.setExceptionClass(String.class)); + } + + @Test + public void dataSourceInitialization() throws Exception { + SQLException connectionException = new SQLException(); + SQLException duplicateKeyException = new SQLException("test", "", 1); + + DataSource dataSource = mock(DataSource.class); + given(dataSource.getConnection()).willThrow(connectionException); + + SQLErrorCodeSQLExceptionTranslator sext = new SQLErrorCodeSQLExceptionTranslator(dataSource); + assertThat(sext.translate("test", null, duplicateKeyException)).isNull(); + + DatabaseMetaData databaseMetaData = mock(DatabaseMetaData.class); + given(databaseMetaData.getDatabaseProductName()).willReturn("Oracle"); + + Connection connection = mock(Connection.class); + given(connection.getMetaData()).willReturn(databaseMetaData); + + Mockito.reset(dataSource); + given(dataSource.getConnection()).willReturn(connection); + assertThat(sext.translate("test", null, duplicateKeyException)).isInstanceOf(DuplicateKeyException.class); + + verify(connection).close(); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLErrorCodesFactoryTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLErrorCodesFactoryTests.java new file mode 100644 index 0000000..7ebf726 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLErrorCodesFactoryTests.java @@ -0,0 +1,378 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.util.Arrays; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; + +/** + * Tests for SQLErrorCodes loading. + * + * @author Rod Johnson + * @author Thomas Risberg + * @author Stephane Nicoll + * @author Juergen Hoeller + */ +public class SQLErrorCodesFactoryTests { + + /** + * Check that a default instance returns empty error codes for an unknown database. + */ + @Test + public void testDefaultInstanceWithNoSuchDatabase() { + SQLErrorCodes sec = SQLErrorCodesFactory.getInstance().getErrorCodes("xx"); + assertThat(sec.getBadSqlGrammarCodes().length == 0).isTrue(); + assertThat(sec.getDataIntegrityViolationCodes().length == 0).isTrue(); + } + + /** + * Check that a known database produces recognizable codes. + */ + @Test + public void testDefaultInstanceWithOracle() { + SQLErrorCodes sec = SQLErrorCodesFactory.getInstance().getErrorCodes("Oracle"); + assertIsOracle(sec); + } + + private void assertIsOracle(SQLErrorCodes sec) { + assertThat(sec.getBadSqlGrammarCodes().length > 0).isTrue(); + assertThat(sec.getDataIntegrityViolationCodes().length > 0).isTrue(); + // These had better be a Bad SQL Grammar code + assertThat(Arrays.binarySearch(sec.getBadSqlGrammarCodes(), "942") >= 0).isTrue(); + assertThat(Arrays.binarySearch(sec.getBadSqlGrammarCodes(), "6550") >= 0).isTrue(); + // This had better NOT be + assertThat(Arrays.binarySearch(sec.getBadSqlGrammarCodes(), "9xx42") >= 0).isFalse(); + } + + private void assertIsSQLServer(SQLErrorCodes sec) { + assertThat(sec.getDatabaseProductName()).isEqualTo("Microsoft SQL Server"); + + assertThat(sec.getBadSqlGrammarCodes().length > 0).isTrue(); + + assertThat(Arrays.binarySearch(sec.getBadSqlGrammarCodes(), "156") >= 0).isTrue(); + assertThat(Arrays.binarySearch(sec.getBadSqlGrammarCodes(), "170") >= 0).isTrue(); + assertThat(Arrays.binarySearch(sec.getBadSqlGrammarCodes(), "207") >= 0).isTrue(); + assertThat(Arrays.binarySearch(sec.getBadSqlGrammarCodes(), "208") >= 0).isTrue(); + assertThat(Arrays.binarySearch(sec.getBadSqlGrammarCodes(), "209") >= 0).isTrue(); + assertThat(Arrays.binarySearch(sec.getBadSqlGrammarCodes(), "9xx42") >= 0).isFalse(); + + assertThat(sec.getPermissionDeniedCodes().length > 0).isTrue(); + assertThat(Arrays.binarySearch(sec.getPermissionDeniedCodes(), "229") >= 0).isTrue(); + + assertThat(sec.getDuplicateKeyCodes().length > 0).isTrue(); + assertThat(Arrays.binarySearch(sec.getDuplicateKeyCodes(), "2601") >= 0).isTrue(); + assertThat(Arrays.binarySearch(sec.getDuplicateKeyCodes(), "2627") >= 0).isTrue(); + + assertThat(sec.getDataIntegrityViolationCodes().length > 0).isTrue(); + assertThat(Arrays.binarySearch(sec.getDataIntegrityViolationCodes(), "544") >= 0).isTrue(); + assertThat(Arrays.binarySearch(sec.getDataIntegrityViolationCodes(), "8114") >= 0).isTrue(); + assertThat(Arrays.binarySearch(sec.getDataIntegrityViolationCodes(), "8115") >= 0).isTrue(); + + assertThat(sec.getDataAccessResourceFailureCodes().length > 0).isTrue(); + assertThat(Arrays.binarySearch(sec.getDataAccessResourceFailureCodes(), "4060") >= 0).isTrue(); + + assertThat(sec.getCannotAcquireLockCodes().length > 0).isTrue(); + assertThat(Arrays.binarySearch(sec.getCannotAcquireLockCodes(), "1222") >= 0).isTrue(); + + assertThat(sec.getDeadlockLoserCodes().length > 0).isTrue(); + assertThat(Arrays.binarySearch(sec.getDeadlockLoserCodes(), "1205") >= 0).isTrue(); + } + + private void assertIsHsql(SQLErrorCodes sec) { + assertThat(sec.getBadSqlGrammarCodes().length > 0).isTrue(); + assertThat(sec.getDataIntegrityViolationCodes().length > 0).isTrue(); + // This had better be a Bad SQL Grammar code + assertThat(Arrays.binarySearch(sec.getBadSqlGrammarCodes(), "-22") >= 0).isTrue(); + // This had better NOT be + assertThat(Arrays.binarySearch(sec.getBadSqlGrammarCodes(), "-9") >= 0).isFalse(); + } + + private void assertIsDB2(SQLErrorCodes sec) { + assertThat(sec.getBadSqlGrammarCodes().length > 0).isTrue(); + assertThat(sec.getDataIntegrityViolationCodes().length > 0).isTrue(); + + assertThat(Arrays.binarySearch(sec.getBadSqlGrammarCodes(), "942") >= 0).isFalse(); + // This had better NOT be + assertThat(Arrays.binarySearch(sec.getBadSqlGrammarCodes(), "-204") >= 0).isTrue(); + } + + private void assertIsHana(SQLErrorCodes sec) { + assertThat(sec.getBadSqlGrammarCodes().length > 0).isTrue(); + assertThat(sec.getDataIntegrityViolationCodes().length > 0).isTrue(); + + assertThat(Arrays.binarySearch(sec.getBadSqlGrammarCodes(), "368") >= 0).isTrue(); + assertThat(Arrays.binarySearch(sec.getPermissionDeniedCodes(), "10") >= 0).isTrue(); + assertThat(Arrays.binarySearch(sec.getDuplicateKeyCodes(), "301") >= 0).isTrue(); + assertThat(Arrays.binarySearch(sec.getDataIntegrityViolationCodes(), "461") >= 0).isTrue(); + assertThat(Arrays.binarySearch(sec.getDataAccessResourceFailureCodes(), "-813") >=0).isTrue(); + assertThat(Arrays.binarySearch(sec.getInvalidResultSetAccessCodes(), "582") >=0).isTrue(); + assertThat(Arrays.binarySearch(sec.getCannotAcquireLockCodes(), "131") >= 0).isTrue(); + assertThat(Arrays.binarySearch(sec.getCannotSerializeTransactionCodes(), "138") >= 0).isTrue(); + assertThat(Arrays.binarySearch(sec.getDeadlockLoserCodes(), "133") >= 0).isTrue(); + + } + + @Test + public void testLookupOrder() { + class TestSQLErrorCodesFactory extends SQLErrorCodesFactory { + private int lookups = 0; + @Override + protected Resource loadResource(String path) { + ++lookups; + if (lookups == 1) { + assertThat(path).isEqualTo(SQLErrorCodesFactory.SQL_ERROR_CODE_DEFAULT_PATH); + return null; + } + else { + // Should have only one more lookup + assertThat(lookups).isEqualTo(2); + assertThat(path).isEqualTo(SQLErrorCodesFactory.SQL_ERROR_CODE_OVERRIDE_PATH); + return null; + } + } + } + + // Should have failed to load without error + TestSQLErrorCodesFactory sf = new TestSQLErrorCodesFactory(); + assertThat(sf.getErrorCodes("XX").getBadSqlGrammarCodes().length == 0).isTrue(); + assertThat(sf.getErrorCodes("Oracle").getDataIntegrityViolationCodes().length == 0).isTrue(); + } + + /** + * Check that user defined error codes take precedence. + */ + @Test + public void testFindUserDefinedCodes() { + class TestSQLErrorCodesFactory extends SQLErrorCodesFactory { + @Override + protected Resource loadResource(String path) { + if (SQLErrorCodesFactory.SQL_ERROR_CODE_OVERRIDE_PATH.equals(path)) { + return new ClassPathResource("test-error-codes.xml", SQLErrorCodesFactoryTests.class); + } + return null; + } + } + + // Should have loaded without error + TestSQLErrorCodesFactory sf = new TestSQLErrorCodesFactory(); + assertThat(sf.getErrorCodes("XX").getBadSqlGrammarCodes().length == 0).isTrue(); + assertThat(sf.getErrorCodes("Oracle").getBadSqlGrammarCodes().length).isEqualTo(2); + assertThat(sf.getErrorCodes("Oracle").getBadSqlGrammarCodes()[0]).isEqualTo("1"); + assertThat(sf.getErrorCodes("Oracle").getBadSqlGrammarCodes()[1]).isEqualTo("2"); + } + + @Test + public void testInvalidUserDefinedCodeFormat() { + class TestSQLErrorCodesFactory extends SQLErrorCodesFactory { + @Override + protected Resource loadResource(String path) { + if (SQLErrorCodesFactory.SQL_ERROR_CODE_OVERRIDE_PATH.equals(path)) { + // Guaranteed to be on the classpath, but most certainly NOT XML + return new ClassPathResource("SQLExceptionTranslator.class", SQLErrorCodesFactoryTests.class); + } + return null; + } + } + + // Should have failed to load without error + TestSQLErrorCodesFactory sf = new TestSQLErrorCodesFactory(); + assertThat(sf.getErrorCodes("XX").getBadSqlGrammarCodes().length == 0).isTrue(); + assertThat(sf.getErrorCodes("Oracle").getBadSqlGrammarCodes().length).isEqualTo(0); + } + + /** + * Check that custom error codes take precedence. + */ + @Test + public void testFindCustomCodes() { + class TestSQLErrorCodesFactory extends SQLErrorCodesFactory { + @Override + protected Resource loadResource(String path) { + if (SQLErrorCodesFactory.SQL_ERROR_CODE_OVERRIDE_PATH.equals(path)) { + return new ClassPathResource("custom-error-codes.xml", SQLErrorCodesFactoryTests.class); + } + return null; + } + } + + // Should have loaded without error + TestSQLErrorCodesFactory sf = new TestSQLErrorCodesFactory(); + assertThat(sf.getErrorCodes("Oracle").getCustomTranslations().length).isEqualTo(1); + CustomSQLErrorCodesTranslation translation = + sf.getErrorCodes("Oracle").getCustomTranslations()[0]; + assertThat(translation.getExceptionClass()).isEqualTo(CustomErrorCodeException.class); + assertThat(translation.getErrorCodes().length).isEqualTo(1); + } + + @Test + public void testDataSourceWithNullMetadata() throws Exception { + Connection connection = mock(Connection.class); + DataSource dataSource = mock(DataSource.class); + given(dataSource.getConnection()).willReturn(connection); + + SQLErrorCodes sec = SQLErrorCodesFactory.getInstance().getErrorCodes(dataSource); + assertIsEmpty(sec); + verify(connection).close(); + + reset(connection); + sec = SQLErrorCodesFactory.getInstance().resolveErrorCodes(dataSource); + assertThat(sec).isNull(); + verify(connection).close(); + } + + @Test + public void testGetFromDataSourceWithSQLException() throws Exception { + SQLException expectedSQLException = new SQLException(); + + DataSource dataSource = mock(DataSource.class); + given(dataSource.getConnection()).willThrow(expectedSQLException); + + SQLErrorCodes sec = SQLErrorCodesFactory.getInstance().getErrorCodes(dataSource); + assertIsEmpty(sec); + + sec = SQLErrorCodesFactory.getInstance().resolveErrorCodes(dataSource); + assertThat(sec).isNull(); + } + + private SQLErrorCodes getErrorCodesFromDataSource(String productName, SQLErrorCodesFactory factory) throws Exception { + DatabaseMetaData databaseMetaData = mock(DatabaseMetaData.class); + given(databaseMetaData.getDatabaseProductName()).willReturn(productName); + + Connection connection = mock(Connection.class); + given(connection.getMetaData()).willReturn(databaseMetaData); + + DataSource dataSource = mock(DataSource.class); + given(dataSource.getConnection()).willReturn(connection); + + SQLErrorCodesFactory secf = (factory != null ? factory : SQLErrorCodesFactory.getInstance()); + SQLErrorCodes sec = secf.getErrorCodes(dataSource); + + SQLErrorCodes sec2 = secf.getErrorCodes(dataSource); + assertThat(sec).as("Cached per DataSource").isSameAs(sec2); + + verify(connection).close(); + return sec; + } + + @Test + public void testSQLServerRecognizedFromMetadata() throws Exception { + SQLErrorCodes sec = getErrorCodesFromDataSource("MS-SQL", null); + assertIsSQLServer(sec); + } + + @Test + public void testOracleRecognizedFromMetadata() throws Exception { + SQLErrorCodes sec = getErrorCodesFromDataSource("Oracle", null); + assertIsOracle(sec); + } + + @Test + public void testHsqlRecognizedFromMetadata() throws Exception { + SQLErrorCodes sec = getErrorCodesFromDataSource("HSQL Database Engine", null); + assertIsHsql(sec); + } + + @Test + public void testDB2RecognizedFromMetadata() throws Exception { + SQLErrorCodes sec = getErrorCodesFromDataSource("DB2", null); + assertIsDB2(sec); + sec = getErrorCodesFromDataSource("DB2/", null); + assertIsDB2(sec); + sec = getErrorCodesFromDataSource("DB-2", null); + assertIsEmpty(sec); + } + + @Test + public void testHanaIsRecognizedFromMetadata() throws Exception { + SQLErrorCodes sec = getErrorCodesFromDataSource("SAP DB", null); + assertIsHana(sec); + } + + /** + * Check that wild card database name works. + */ + @Test + public void testWildCardNameRecognized() throws Exception { + class WildcardSQLErrorCodesFactory extends SQLErrorCodesFactory { + @Override + protected Resource loadResource(String path) { + if (SQLErrorCodesFactory.SQL_ERROR_CODE_OVERRIDE_PATH.equals(path)) { + return new ClassPathResource("wildcard-error-codes.xml", SQLErrorCodesFactoryTests.class); + } + return null; + } + } + + WildcardSQLErrorCodesFactory factory = new WildcardSQLErrorCodesFactory(); + SQLErrorCodes sec = getErrorCodesFromDataSource("DB2", factory); + assertIsDB2(sec); + sec = getErrorCodesFromDataSource("DB2 UDB for Xxxxx", factory); + assertIsDB2(sec); + + sec = getErrorCodesFromDataSource("DB3", factory); + assertIsDB2(sec); + sec = getErrorCodesFromDataSource("DB3/", factory); + assertIsDB2(sec); + sec = getErrorCodesFromDataSource("/DB3", factory); + assertIsDB2(sec); + sec = getErrorCodesFromDataSource("/DB3", factory); + assertIsDB2(sec); + sec = getErrorCodesFromDataSource("/DB3/", factory); + assertIsDB2(sec); + sec = getErrorCodesFromDataSource("DB-3", factory); + assertIsEmpty(sec); + + sec = getErrorCodesFromDataSource("DB1", factory); + assertIsDB2(sec); + sec = getErrorCodesFromDataSource("DB1/", factory); + assertIsDB2(sec); + sec = getErrorCodesFromDataSource("/DB1", factory); + assertIsEmpty(sec); + sec = getErrorCodesFromDataSource("/DB1/", factory); + assertIsEmpty(sec); + + sec = getErrorCodesFromDataSource("DB0", factory); + assertIsDB2(sec); + sec = getErrorCodesFromDataSource("/DB0", factory); + assertIsDB2(sec); + sec = getErrorCodesFromDataSource("DB0/", factory); + assertIsEmpty(sec); + sec = getErrorCodesFromDataSource("/DB0/", factory); + assertIsEmpty(sec); + } + + private void assertIsEmpty(SQLErrorCodes sec) { + assertThat(sec.getBadSqlGrammarCodes().length).isEqualTo(0); + assertThat(sec.getDataIntegrityViolationCodes().length).isEqualTo(0); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionCustomTranslatorTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionCustomTranslatorTests.java new file mode 100644 index 0000000..8d46c0f --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionCustomTranslatorTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import java.sql.SQLException; + +import org.junit.jupiter.api.Test; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.TransientDataAccessResourceException; +import org.springframework.jdbc.BadSqlGrammarException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for custom SQLException translation. + * + * @author Thomas Risberg + * @author Sam Brannen + */ +public class SQLExceptionCustomTranslatorTests { + + private static SQLErrorCodes ERROR_CODES = new SQLErrorCodes(); + + static { + ERROR_CODES.setBadSqlGrammarCodes(new String[] { "1" }); + ERROR_CODES.setDataAccessResourceFailureCodes(new String[] { "2" }); + ERROR_CODES.setCustomSqlExceptionTranslatorClass(CustomSqlExceptionTranslator.class); + } + + private final SQLExceptionTranslator sext = new SQLErrorCodeSQLExceptionTranslator(ERROR_CODES); + + + @Test + public void badSqlGrammarException() { + SQLException badSqlGrammarExceptionEx = SQLExceptionSubclassFactory.newSQLDataException("", "", 1); + DataAccessException dae = sext.translate("task", "SQL", badSqlGrammarExceptionEx); + assertThat(dae.getCause()).isEqualTo(badSqlGrammarExceptionEx); + assertThat(dae).isInstanceOf(BadSqlGrammarException.class); + } + + @Test + public void dataAccessResourceException() { + SQLException dataAccessResourceEx = SQLExceptionSubclassFactory.newSQLDataException("", "", 2); + DataAccessException dae = sext.translate("task", "SQL", dataAccessResourceEx); + assertThat(dae.getCause()).isEqualTo(dataAccessResourceEx); + assertThat(dae).isInstanceOf(TransientDataAccessResourceException.class); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassFactory.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassFactory.java new file mode 100644 index 0000000..7a6d99d --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassFactory.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2008 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import java.sql.SQLDataException; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLIntegrityConstraintViolationException; +import java.sql.SQLInvalidAuthorizationSpecException; +import java.sql.SQLNonTransientConnectionException; +import java.sql.SQLRecoverableException; +import java.sql.SQLSyntaxErrorException; +import java.sql.SQLTimeoutException; +import java.sql.SQLTransactionRollbackException; +import java.sql.SQLTransientConnectionException; + +/** + * Class to generate Java 6 SQLException subclasses for testing purposes. + * + * @author Thomas Risberg + */ +public class SQLExceptionSubclassFactory { + + public static SQLException newSQLDataException(String reason, String SQLState, int vendorCode) { + return new SQLDataException(reason, SQLState, vendorCode); + } + + public static SQLException newSQLFeatureNotSupportedException(String reason, String SQLState, int vendorCode) { + return new SQLFeatureNotSupportedException(reason, SQLState, vendorCode); + } + + public static SQLException newSQLIntegrityConstraintViolationException(String reason, String SQLState, int vendorCode) { + return new SQLIntegrityConstraintViolationException(reason, SQLState, vendorCode); + } + + public static SQLException newSQLInvalidAuthorizationSpecException(String reason, String SQLState, int vendorCode) { + return new SQLInvalidAuthorizationSpecException(reason, SQLState, vendorCode); + } + + public static SQLException newSQLNonTransientConnectionException(String reason, String SQLState, int vendorCode) { + return new SQLNonTransientConnectionException(reason, SQLState, vendorCode); + } + + public static SQLException newSQLSyntaxErrorException(String reason, String SQLState, int vendorCode) { + return new SQLSyntaxErrorException(reason, SQLState, vendorCode); + } + + public static SQLException newSQLTransactionRollbackException(String reason, String SQLState, int vendorCode) { + return new SQLTransactionRollbackException(reason, SQLState, vendorCode); + } + + public static SQLException newSQLTransientConnectionException(String reason, String SQLState, int vendorCode) { + return new SQLTransientConnectionException(reason, SQLState, vendorCode); + } + + public static SQLException newSQLTimeoutException(String reason, String SQLState, int vendorCode) { + return new SQLTimeoutException(reason, SQLState, vendorCode); + } + + public static SQLException newSQLRecoverableException(String reason, String SQLState, int vendorCode) { + return new SQLRecoverableException(reason, SQLState, vendorCode); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslatorTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslatorTests.java new file mode 100644 index 0000000..f599303 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslatorTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import java.sql.SQLException; + +import org.junit.jupiter.api.Test; + +import org.springframework.dao.ConcurrencyFailureException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.dao.PermissionDeniedDataAccessException; +import org.springframework.dao.QueryTimeoutException; +import org.springframework.dao.RecoverableDataAccessException; +import org.springframework.dao.TransientDataAccessResourceException; +import org.springframework.jdbc.BadSqlGrammarException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Thomas Risberg + */ +public class SQLExceptionSubclassTranslatorTests { + + private static SQLErrorCodes ERROR_CODES = new SQLErrorCodes(); + + static { + ERROR_CODES.setBadSqlGrammarCodes("1"); + } + + + @Test + public void errorCodeTranslation() { + SQLExceptionTranslator sext = new SQLErrorCodeSQLExceptionTranslator(ERROR_CODES); + + SQLException dataIntegrityViolationEx = SQLExceptionSubclassFactory.newSQLDataException("", "", 0); + DataIntegrityViolationException divex = (DataIntegrityViolationException) sext.translate("task", "SQL", dataIntegrityViolationEx); + assertThat(divex.getCause()).isEqualTo(dataIntegrityViolationEx); + + SQLException featureNotSupEx = SQLExceptionSubclassFactory.newSQLFeatureNotSupportedException("", "", 0); + InvalidDataAccessApiUsageException idaex = (InvalidDataAccessApiUsageException) sext.translate("task", "SQL", featureNotSupEx); + assertThat(idaex.getCause()).isEqualTo(featureNotSupEx); + + SQLException dataIntegrityViolationEx2 = SQLExceptionSubclassFactory.newSQLIntegrityConstraintViolationException("", "", 0); + DataIntegrityViolationException divex2 = (DataIntegrityViolationException) sext.translate("task", "SQL", dataIntegrityViolationEx2); + assertThat(divex2.getCause()).isEqualTo(dataIntegrityViolationEx2); + + SQLException permissionDeniedEx = SQLExceptionSubclassFactory.newSQLInvalidAuthorizationSpecException("", "", 0); + PermissionDeniedDataAccessException pdaex = (PermissionDeniedDataAccessException) sext.translate("task", "SQL", permissionDeniedEx); + assertThat(pdaex.getCause()).isEqualTo(permissionDeniedEx); + + SQLException dataAccessResourceEx = SQLExceptionSubclassFactory.newSQLNonTransientConnectionException("", "", 0); + DataAccessResourceFailureException darex = (DataAccessResourceFailureException) sext.translate("task", "SQL", dataAccessResourceEx); + assertThat(darex.getCause()).isEqualTo(dataAccessResourceEx); + + SQLException badSqlEx2 = SQLExceptionSubclassFactory.newSQLSyntaxErrorException("", "", 0); + BadSqlGrammarException bsgex2 = (BadSqlGrammarException) sext.translate("task", "SQL2", badSqlEx2); + assertThat(bsgex2.getSql()).isEqualTo("SQL2"); + assertThat((Object) bsgex2.getSQLException()).isEqualTo(badSqlEx2); + + SQLException tranRollbackEx = SQLExceptionSubclassFactory.newSQLTransactionRollbackException("", "", 0); + ConcurrencyFailureException cfex = (ConcurrencyFailureException) sext.translate("task", "SQL", tranRollbackEx); + assertThat(cfex.getCause()).isEqualTo(tranRollbackEx); + + SQLException transientConnEx = SQLExceptionSubclassFactory.newSQLTransientConnectionException("", "", 0); + TransientDataAccessResourceException tdarex = (TransientDataAccessResourceException) sext.translate("task", "SQL", transientConnEx); + assertThat(tdarex.getCause()).isEqualTo(transientConnEx); + + SQLException transientConnEx2 = SQLExceptionSubclassFactory.newSQLTimeoutException("", "", 0); + QueryTimeoutException tdarex2 = (QueryTimeoutException) sext.translate("task", "SQL", transientConnEx2); + assertThat(tdarex2.getCause()).isEqualTo(transientConnEx2); + + SQLException recoverableEx = SQLExceptionSubclassFactory.newSQLRecoverableException("", "", 0); + RecoverableDataAccessException rdaex2 = (RecoverableDataAccessException) sext.translate("task", "SQL", recoverableEx); + assertThat(rdaex2.getCause()).isEqualTo(recoverableEx); + + // Test classic error code translation. We should move there next if the exception we pass in is not one + // of the new sub-classes. + SQLException sexEct = new SQLException("", "", 1); + BadSqlGrammarException bsgEct = (BadSqlGrammarException) sext.translate("task", "SQL-ECT", sexEct); + assertThat(bsgEct.getSql()).isEqualTo("SQL-ECT"); + assertThat((Object) bsgEct.getSQLException()).isEqualTo(sexEct); + + // Test fallback. We assume that no database will ever return this error code, + // but 07xxx will be bad grammar picked up by the fallback SQLState translator + SQLException sexFbt = new SQLException("", "07xxx", 666666666); + BadSqlGrammarException bsgFbt = (BadSqlGrammarException) sext.translate("task", "SQL-FBT", sexFbt); + assertThat(bsgFbt.getSql()).isEqualTo("SQL-FBT"); + assertThat((Object) bsgFbt.getSQLException()).isEqualTo(sexFbt); + // and 08xxx will be data resource failure (non-transient) picked up by the fallback SQLState translator + SQLException sexFbt2 = new SQLException("", "08xxx", 666666666); + DataAccessResourceFailureException darfFbt = (DataAccessResourceFailureException) sext.translate("task", "SQL-FBT2", sexFbt2); + assertThat(darfFbt.getCause()).isEqualTo(sexFbt2); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateExceptionTranslatorTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateExceptionTranslatorTests.java new file mode 100644 index 0000000..608e0b5 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateExceptionTranslatorTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import java.sql.SQLException; + +import org.junit.jupiter.api.Test; + +import org.springframework.jdbc.BadSqlGrammarException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rod Johnson + * @since 13-Jan-03 + */ +public class SQLStateExceptionTranslatorTests { + + private static final String sql = "SELECT FOO FROM BAR"; + + private final SQLStateSQLExceptionTranslator trans = new SQLStateSQLExceptionTranslator(); + + // ALSO CHECK CHAIN of SQLExceptions!? + // also allow chain of translators? default if can't do specific? + + @Test + public void badSqlGrammar() { + SQLException sex = new SQLException("Message", "42001", 1); + try { + throw this.trans.translate("task", sql, sex); + } + catch (BadSqlGrammarException ex) { + // OK + assertThat(sql.equals(ex.getSql())).as("SQL is correct").isTrue(); + assertThat(sex.equals(ex.getSQLException())).as("Exception matches").isTrue(); + } + } + + @Test + public void invalidSqlStateCode() { + SQLException sex = new SQLException("Message", "NO SUCH CODE", 1); + assertThat(this.trans.translate("task", sql, sex)).isNull(); + } + + /** + * PostgreSQL can return null. + * SAP DB can apparently return empty SQL code. + * Bug 729170 + */ + @Test + public void malformedSqlStateCodes() { + SQLException sex = new SQLException("Message", null, 1); + assertThat(this.trans.translate("task", sql, sex)).isNull(); + + sex = new SQLException("Message", "", 1); + assertThat(this.trans.translate("task", sql, sex)).isNull(); + + // One char's not allowed + sex = new SQLException("Message", "I", 1); + assertThat(this.trans.translate("task", sql, sex)).isNull(); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslatorTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslatorTests.java new file mode 100644 index 0000000..98baf1a --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslatorTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support; + +import java.sql.SQLException; + +import org.junit.jupiter.api.Test; + +import org.springframework.dao.ConcurrencyFailureException; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.TransientDataAccessResourceException; +import org.springframework.jdbc.BadSqlGrammarException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Rick Evans + * @author Juergen Hoeller + * @author Chris Beams + */ +public class SQLStateSQLExceptionTranslatorTests { + + private static final String REASON = "The game is afoot!"; + + private static final String TASK = "Counting sheep... yawn."; + + private static final String SQL = "select count(0) from t_sheep where over_fence = ... yawn... 1"; + + + @Test + public void testTranslateNullException() { + assertThatIllegalArgumentException().isThrownBy(() -> + new SQLStateSQLExceptionTranslator().translate("", "", null)); + } + + @Test + public void testTranslateBadSqlGrammar() { + doTest("07", BadSqlGrammarException.class); + } + + @Test + public void testTranslateDataIntegrityViolation() { + doTest("23", DataIntegrityViolationException.class); + } + + @Test + public void testTranslateDataAccessResourceFailure() { + doTest("53", DataAccessResourceFailureException.class); + } + + @Test + public void testTranslateTransientDataAccessResourceFailure() { + doTest("S1", TransientDataAccessResourceException.class); + } + + @Test + public void testTranslateConcurrencyFailure() { + doTest("40", ConcurrencyFailureException.class); + } + + @Test + public void testTranslateUncategorized() { + assertThat(new SQLStateSQLExceptionTranslator().translate("", "", new SQLException(REASON, "00000000"))).isNull(); + } + + + private void doTest(String sqlState, Class dataAccessExceptionType) { + SQLException ex = new SQLException(REASON, sqlState); + SQLExceptionTranslator translator = new SQLStateSQLExceptionTranslator(); + DataAccessException dax = translator.translate(TASK, SQL, ex); + assertThat(dax).as("Specific translation must not result in a null DataAccessException being returned.").isNotNull(); + assertThat(dax.getClass()).as("Wrong DataAccessException type returned as the result of the translation").isEqualTo(dataAccessExceptionType); + assertThat(dax.getCause()).as("The original SQLException must be preserved in the translated DataAccessException").isNotNull(); + assertThat(dax.getCause()).as("The exact same original SQLException must be preserved in the translated DataAccessException").isSameAs(ex); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/rowset/ResultSetWrappingRowSetTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/rowset/ResultSetWrappingRowSetTests.java new file mode 100644 index 0000000..06ca494 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/rowset/ResultSetWrappingRowSetTests.java @@ -0,0 +1,223 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.support.rowset; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.sql.Date; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Time; +import java.sql.Timestamp; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.jdbc.InvalidResultSetAccessException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * @author Thomas Risberg + */ +public class ResultSetWrappingRowSetTests { + + private ResultSet resultSet; + + private ResultSetWrappingSqlRowSet rowSet; + + + @BeforeEach + public void setup() throws Exception { + resultSet = mock(ResultSet.class); + rowSet = new ResultSetWrappingSqlRowSet(resultSet); + } + + + @Test + public void testGetBigDecimalInt() throws Exception { + Method rset = ResultSet.class.getDeclaredMethod("getBigDecimal", int.class); + Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getBigDecimal", int.class); + doTest(rset, rowset, 1, BigDecimal.ONE); + } + + @Test + public void testGetBigDecimalString() throws Exception { + Method rset = ResultSet.class.getDeclaredMethod("getBigDecimal", int.class); + Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getBigDecimal", String.class); + doTest(rset, rowset, "test", BigDecimal.ONE); + } + + @Test + public void testGetStringInt() throws Exception { + Method rset = ResultSet.class.getDeclaredMethod("getString", int.class); + Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getString", int.class); + doTest(rset, rowset, 1, "test"); + } + + @Test + public void testGetStringString() throws Exception { + Method rset = ResultSet.class.getDeclaredMethod("getString", int.class); + Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getString", String.class); + doTest(rset, rowset, "test", "test"); + } + + @Test + public void testGetTimestampInt() throws Exception { + Method rset = ResultSet.class.getDeclaredMethod("getTimestamp", int.class); + Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getTimestamp", int.class); + doTest(rset, rowset, 1, new Timestamp(1234L)); + } + + @Test + public void testGetTimestampString() throws Exception { + Method rset = ResultSet.class.getDeclaredMethod("getTimestamp", int.class); + Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getTimestamp", String.class); + doTest(rset, rowset, "test", new Timestamp(1234L)); + } + + @Test + public void testGetDateInt() throws Exception { + Method rset = ResultSet.class.getDeclaredMethod("getDate", int.class); + Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getDate", int.class); + doTest(rset, rowset, 1, new Date(1234L)); + } + + @Test + public void testGetDateString() throws Exception { + Method rset = ResultSet.class.getDeclaredMethod("getDate", int.class); + Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getDate", String.class); + doTest(rset, rowset, "test", new Date(1234L)); + } + + @Test + public void testGetTimeInt() throws Exception { + Method rset = ResultSet.class.getDeclaredMethod("getTime", int.class); + Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getTime", int.class); + doTest(rset, rowset, 1, new Time(1234L)); + } + + @Test + public void testGetTimeString() throws Exception { + Method rset = ResultSet.class.getDeclaredMethod("getTime", int.class); + Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getTime", String.class); + doTest(rset, rowset, "test", new Time(1234L)); + } + + @Test + public void testGetObjectInt() throws Exception { + Method rset = ResultSet.class.getDeclaredMethod("getObject", int.class); + Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getObject", int.class); + doTest(rset, rowset, 1, new Object()); + } + + @Test + public void testGetObjectString() throws Exception { + Method rset = ResultSet.class.getDeclaredMethod("getObject", int.class); + Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getObject", String.class); + doTest(rset, rowset, "test", new Object()); + } + + @Test + public void testGetIntInt() throws Exception { + Method rset = ResultSet.class.getDeclaredMethod("getInt", int.class); + Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getInt", int.class); + doTest(rset, rowset, 1, 1); + } + + @Test + public void testGetIntString() throws Exception { + Method rset = ResultSet.class.getDeclaredMethod("getInt", int.class); + Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getInt", String.class); + doTest(rset, rowset, "test", 1); + } + + @Test + public void testGetFloatInt() throws Exception { + Method rset = ResultSet.class.getDeclaredMethod("getFloat", int.class); + Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getFloat", int.class); + doTest(rset, rowset, 1, 1.0f); + } + + @Test + public void testGetFloatString() throws Exception { + Method rset = ResultSet.class.getDeclaredMethod("getFloat", int.class); + Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getFloat", String.class); + doTest(rset, rowset, "test", 1.0f); + } + + @Test + public void testGetDoubleInt() throws Exception { + Method rset = ResultSet.class.getDeclaredMethod("getDouble", int.class); + Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getDouble", int.class); + doTest(rset, rowset, 1, 1.0d); + } + + @Test + public void testGetDoubleString() throws Exception { + Method rset = ResultSet.class.getDeclaredMethod("getDouble", int.class); + Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getDouble", String.class); + doTest(rset, rowset, "test", 1.0d); + } + + @Test + public void testGetLongInt() throws Exception { + Method rset = ResultSet.class.getDeclaredMethod("getLong", int.class); + Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getLong", int.class); + doTest(rset, rowset, 1, 1L); + } + + @Test + public void testGetLongString() throws Exception { + Method rset = ResultSet.class.getDeclaredMethod("getLong", int.class); + Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getLong", String.class); + doTest(rset, rowset, "test", 1L); + } + + @Test + public void testGetBooleanInt() throws Exception { + Method rset = ResultSet.class.getDeclaredMethod("getBoolean", int.class); + Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getBoolean", int.class); + doTest(rset, rowset, 1, true); + } + + @Test + public void testGetBooleanString() throws Exception { + Method rset = ResultSet.class.getDeclaredMethod("getBoolean", int.class); + Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getBoolean", String.class); + doTest(rset, rowset, "test", true); + } + + private void doTest(Method rsetMethod, Method rowsetMethod, Object arg, Object ret) throws Exception { + if (arg instanceof String) { + given(resultSet.findColumn((String) arg)).willReturn(1); + given(rsetMethod.invoke(resultSet, 1)).willReturn(ret).willThrow(new SQLException("test")); + } + else { + given(rsetMethod.invoke(resultSet, arg)).willReturn(ret).willThrow(new SQLException("test")); + } + rowsetMethod.invoke(rowSet, arg); + assertThatExceptionOfType(InvocationTargetException.class).isThrownBy(() -> + rowsetMethod.invoke(rowSet, arg)). + satisfies(ex -> assertThat(ex.getTargetException()).isExactlyInstanceOf(InvalidResultSetAccessException.class)); + } + +} diff --git a/spring-jdbc/src/test/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensionsTests.kt b/spring-jdbc/src/test/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensionsTests.kt new file mode 100644 index 0000000..8c1f39f --- /dev/null +++ b/spring-jdbc/src/test/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensionsTests.kt @@ -0,0 +1,141 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core + +import java.sql.* + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +/** + * Mock object based tests for [JdbcOperations] Kotlin extensions + * + * @author Mario Arias + * @author Sebastien Deleuze + */ +class JdbcOperationsExtensionsTests { + + val template = mockk() + + val sql = "select age from customer where id = 3" + + @Test + fun `queryForObject with reified type parameters`() { + every { template.queryForObject(sql, any>()) } returns 2 + assertThat(template.queryForObject(sql)).isEqualTo(2) + verify { template.queryForObject(sql, any>()) } + } + + @Test + fun `queryForObject with RowMapper-like function`() { + every { template.queryForObject(sql, any>(), any()) } returns 2 + assertThat(template.queryForObject(sql, 3) { rs: ResultSet, _: Int -> rs.getInt(1) }).isEqualTo(2) + verify { template.queryForObject(eq(sql), any>(), eq(3)) } + } + + @Test // gh-22682 + fun `queryForObject with nullable RowMapper-like function`() { + every { template.queryForObject(sql, any>(), 3) } returns null + assertThat(template.queryForObject(sql, 3) { _, _ -> null }).isNull() + verify { template.queryForObject(eq(sql), any>(), eq(3)) } + } + + @Test + fun `queryForObject with reified type parameters and argTypes`() { + val args = arrayOf(3) + val argTypes = intArrayOf(JDBCType.INTEGER.vendorTypeNumber) + every { template.queryForObject(sql, args, argTypes, any>()) } returns 2 + assertThat(template.queryForObject(sql, args, argTypes)).isEqualTo(2) + verify { template.queryForObject(sql, args, argTypes, any>()) } + } + + @Test + fun `queryForObject with reified type parameters and args`() { + val args = arrayOf(3) + every { template.queryForObject(sql, any>(), args) } returns 2 + assertThat(template.queryForObject(sql, args)).isEqualTo(2) + verify { template.queryForObject(sql, any>(), args) } + } + + @Test + fun `queryForList with reified type parameters`() { + val list = listOf(1, 2, 3) + every { template.queryForList(sql, any>()) } returns list + assertThat(template.queryForList(sql)).isEqualTo(list) + verify { template.queryForList(sql, any>()) } + } + + @Test + fun `queryForList with reified type parameters and argTypes`() { + val list = listOf(1, 2, 3) + val args = arrayOf(3) + val argTypes = intArrayOf(JDBCType.INTEGER.vendorTypeNumber) + every { template.queryForList(sql, args, argTypes, any>()) } returns list + assertThat(template.queryForList(sql, args, argTypes)).isEqualTo(list) + verify { template.queryForList(sql, args, argTypes, any>()) } + } + + @Test + fun `queryForList with reified type parameters and args`() { + val list = listOf(1, 2, 3) + val args = arrayOf(3) + every { template.queryForList(sql, any>(), args) } returns list + template.queryForList(sql, args) + verify { template.queryForList(sql, any>(), args) } + } + + @Test + fun `query with ResultSetExtractor-like function`() { + every { template.query(eq(sql), any>(), eq(3)) } returns 2 + assertThat(template.query(sql, 3) { rs -> + rs.next() + rs.getInt(1) + }).isEqualTo(2) + verify { template.query(eq(sql), any>(), eq(3)) } + } + + @Test // gh-22682 + fun `query with nullable ResultSetExtractor-like function`() { + every { template.query(eq(sql), any>(), eq(3)) } returns null + assertThat(template.query(sql, 3) { _ -> null }).isNull() + verify { template.query(eq(sql), any>(), eq(3)) } + } + + @Suppress("RemoveExplicitTypeArguments") + @Test + fun `query with RowCallbackHandler-like function`() { + every { template.query(sql, ofType(), 3) } returns Unit + template.query(sql, 3) { rs -> + assertThat(rs.getInt(1)).isEqualTo(22) + } + verify { template.query(sql, ofType(), 3) } + } + + @Test + fun `query with RowMapper-like function`() { + val list = mutableListOf(1, 2, 3) + every { template.query(sql, ofType>(), 3) } returns list + assertThat(template.query(sql, 3) { rs, _ -> + rs.getInt(1) + }).isEqualTo(list) + verify { template.query(sql, ofType>(), 3) } + } + +} diff --git a/spring-jdbc/src/test/kotlin/org/springframework/jdbc/core/namedparam/MapSqlParameterSourceExtensionsTests.kt b/spring-jdbc/src/test/kotlin/org/springframework/jdbc/core/namedparam/MapSqlParameterSourceExtensionsTests.kt new file mode 100644 index 0000000..add5017 --- /dev/null +++ b/spring-jdbc/src/test/kotlin/org/springframework/jdbc/core/namedparam/MapSqlParameterSourceExtensionsTests.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.jdbc.core.namedparam + +import org.assertj.core.api.Assertions.assertThat +import java.sql.JDBCType + +import org.junit.jupiter.api.Test + +/** + * Tests for [MapSqlParameterSource] Kotlin extensions. + * + * @author Mario Arias + */ +class MapSqlParameterSourceExtensionsTests { + + @Test + fun `setter with value`() { + val source = MapSqlParameterSource() + source["foo"] = 2 + assertThat(source.getValue("foo")).isEqualTo(2) + } + + @Test + fun `setter with value and type`() { + val source = MapSqlParameterSource() + source["foo", JDBCType.INTEGER.vendorTypeNumber] = 2 + assertThat(source.getValue("foo")).isEqualTo(2) + assertThat(source.getSqlType("foo")).isEqualTo(JDBCType.INTEGER.vendorTypeNumber) + } + + @Test + fun `setter with value, type and type name`() { + val source = MapSqlParameterSource() + source["foo", JDBCType.INTEGER.vendorTypeNumber, "INT"] = 2 + assertThat(source.getValue("foo")).isEqualTo(2) + assertThat(source.getSqlType("foo")).isEqualTo(JDBCType.INTEGER.vendorTypeNumber) + assertThat(source.getTypeName("foo")).isEqualTo("INT") + } + +} diff --git a/spring-jdbc/src/test/resources/data.sql b/spring-jdbc/src/test/resources/data.sql new file mode 100644 index 0000000..51de08a --- /dev/null +++ b/spring-jdbc/src/test/resources/data.sql @@ -0,0 +1 @@ +insert into T_TEST (NAME) values ('Keith'); \ No newline at end of file diff --git a/spring-jdbc/src/test/resources/log4j2-test.xml b/spring-jdbc/src/test/resources/log4j2-test.xml new file mode 100644 index 0000000..d23e1b2 --- /dev/null +++ b/spring-jdbc/src/test/resources/log4j2-test.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/spring-jdbc/src/test/resources/org/springframework/jdbc/config/db-drops.sql b/spring-jdbc/src/test/resources/org/springframework/jdbc/config/db-drops.sql new file mode 100644 index 0000000..a19743f --- /dev/null +++ b/spring-jdbc/src/test/resources/org/springframework/jdbc/config/db-drops.sql @@ -0,0 +1 @@ +drop table T_TEST; diff --git a/spring-jdbc/src/test/resources/org/springframework/jdbc/config/db-schema-derby.sql b/spring-jdbc/src/test/resources/org/springframework/jdbc/config/db-schema-derby.sql new file mode 100644 index 0000000..9b2e14d --- /dev/null +++ b/spring-jdbc/src/test/resources/org/springframework/jdbc/config/db-schema-derby.sql @@ -0,0 +1 @@ +create table T_TEST (NAME varchar(50) not null); \ No newline at end of file diff --git a/spring-jdbc/src/test/resources/org/springframework/jdbc/config/db-schema.sql b/spring-jdbc/src/test/resources/org/springframework/jdbc/config/db-schema.sql new file mode 100644 index 0000000..73d0feb --- /dev/null +++ b/spring-jdbc/src/test/resources/org/springframework/jdbc/config/db-schema.sql @@ -0,0 +1,3 @@ +drop table T_TEST if exists; + +create table T_TEST (NAME varchar(50) not null); \ No newline at end of file diff --git a/spring-jdbc/src/test/resources/org/springframework/jdbc/config/db-test-data-endings.sql b/spring-jdbc/src/test/resources/org/springframework/jdbc/config/db-test-data-endings.sql new file mode 100644 index 0000000..3830391 --- /dev/null +++ b/spring-jdbc/src/test/resources/org/springframework/jdbc/config/db-test-data-endings.sql @@ -0,0 +1,2 @@ +insert into T_TEST (NAME) values ('Keith')@@ +insert into T_TEST (NAME) values ('Dave')@@ \ No newline at end of file diff --git a/spring-jdbc/src/test/resources/org/springframework/jdbc/config/db-test-data.sql b/spring-jdbc/src/test/resources/org/springframework/jdbc/config/db-test-data.sql new file mode 100644 index 0000000..51de08a --- /dev/null +++ b/spring-jdbc/src/test/resources/org/springframework/jdbc/config/db-test-data.sql @@ -0,0 +1 @@ +insert into T_TEST (NAME) values ('Keith'); \ No newline at end of file diff --git a/spring-jdbc/src/test/resources/org/springframework/jdbc/config/db-update-data.sql b/spring-jdbc/src/test/resources/org/springframework/jdbc/config/db-update-data.sql new file mode 100644 index 0000000..db1bd5e --- /dev/null +++ b/spring-jdbc/src/test/resources/org/springframework/jdbc/config/db-update-data.sql @@ -0,0 +1 @@ +update T_TEST set NAME='Dave' where name='Keith'; \ No newline at end of file diff --git a/spring-jdbc/src/test/resources/org/springframework/jdbc/config/jdbc-config-custom-separator.xml b/spring-jdbc/src/test/resources/org/springframework/jdbc/config/jdbc-config-custom-separator.xml new file mode 100644 index 0000000..de3b95a --- /dev/null +++ b/spring-jdbc/src/test/resources/org/springframework/jdbc/config/jdbc-config-custom-separator.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/spring-jdbc/src/test/resources/org/springframework/jdbc/config/jdbc-config-db-name-default-and-anonymous-datasource.xml b/spring-jdbc/src/test/resources/org/springframework/jdbc/config/jdbc-config-db-name-default-and-anonymous-datasource.xml new file mode 100644 index 0000000..06f7c12 --- /dev/null +++ b/spring-jdbc/src/test/resources/org/springframework/jdbc/config/jdbc-config-db-name-default-and-anonymous-datasource.xml @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/spring-jdbc/src/test/resources/org/springframework/jdbc/config/jdbc-config-db-name-explicit.xml b/spring-jdbc/src/test/resources/org/springframework/jdbc/config/jdbc-config-db-name-explicit.xml new file mode 100644 index 0000000..e378d39 --- /dev/null +++ b/spring-jdbc/src/test/resources/org/springframework/jdbc/config/jdbc-config-db-name-explicit.xml @@ -0,0 +1,12 @@ + + + + + "; + request.setQueryString(xssQueryString); + tag.doStartTag(); + assertThat(getOutput()).isEqualTo(("
    ")); + } + + @Test + public void get() throws Exception { + this.tag.setMethod("get"); + + this.tag.doStartTag(); + this.tag.doEndTag(); + this.tag.doFinally(); + + String output = getOutput(); + String formOutput = getFormTag(output); + String inputOutput = getInputTag(output); + + assertContainsAttribute(formOutput, "method", "get"); + assertThat(inputOutput).isEqualTo(""); + } + + @Test + public void post() throws Exception { + this.tag.setMethod("post"); + + this.tag.doStartTag(); + this.tag.doEndTag(); + this.tag.doFinally(); + + String output = getOutput(); + String formOutput = getFormTag(output); + String inputOutput = getInputTag(output); + + assertContainsAttribute(formOutput, "method", "post"); + assertThat(inputOutput).isEqualTo(""); + } + + @Test + public void put() throws Exception { + this.tag.setMethod("put"); + + this.tag.doStartTag(); + this.tag.doEndTag(); + this.tag.doFinally(); + + String output = getOutput(); + String formOutput = getFormTag(output); + String inputOutput = getInputTag(output); + + assertContainsAttribute(formOutput, "method", "post"); + assertContainsAttribute(inputOutput, "name", "_method"); + assertContainsAttribute(inputOutput, "value", "put"); + assertContainsAttribute(inputOutput, "type", "hidden"); + } + + @Test + public void delete() throws Exception { + this.tag.setMethod("delete"); + + this.tag.doStartTag(); + this.tag.doEndTag(); + this.tag.doFinally(); + + String output = getOutput(); + String formOutput = getFormTag(output); + String inputOutput = getInputTag(output); + + assertContainsAttribute(formOutput, "method", "post"); + assertContainsAttribute(inputOutput, "name", "_method"); + assertContainsAttribute(inputOutput, "value", "delete"); + assertContainsAttribute(inputOutput, "type", "hidden"); + } + + @Test + public void customMethodParameter() throws Exception { + this.tag.setMethod("put"); + this.tag.setMethodParam("methodParameter"); + + this.tag.doStartTag(); + this.tag.doEndTag(); + this.tag.doFinally(); + + String output = getOutput(); + String formOutput = getFormTag(output); + String inputOutput = getInputTag(output); + + assertContainsAttribute(formOutput, "method", "post"); + assertContainsAttribute(inputOutput, "name", "methodParameter"); + assertContainsAttribute(inputOutput, "value", "put"); + assertContainsAttribute(inputOutput, "type", "hidden"); + } + + @Test + public void clearAttributesOnFinally() throws Exception { + this.tag.setModelAttribute("model"); + getPageContext().setAttribute("model", "foo bar"); + assertThat(getPageContext().getAttribute(FormTag.MODEL_ATTRIBUTE_VARIABLE_NAME, PageContext.REQUEST_SCOPE)).isNull(); + this.tag.doStartTag(); + assertThat(getPageContext().getAttribute(FormTag.MODEL_ATTRIBUTE_VARIABLE_NAME, PageContext.REQUEST_SCOPE)).isNotNull(); + this.tag.doFinally(); + assertThat(getPageContext().getAttribute(FormTag.MODEL_ATTRIBUTE_VARIABLE_NAME, PageContext.REQUEST_SCOPE)).isNull(); + } + + @Test + public void requestDataValueProcessorHooks() throws Exception { + String action = "/my/form?foo=bar"; + RequestDataValueProcessor processor = getMockRequestDataValueProcessor(); + given(processor.processAction(this.request, action, "post")).willReturn(action); + given(processor.getExtraHiddenFields(this.request)).willReturn(Collections.singletonMap("key", "value")); + + this.tag.doStartTag(); + this.tag.doEndTag(); + this.tag.doFinally(); + + String output = getOutput(); + + assertThat(getInputTag(output)).isEqualTo("
    \n\n
    "); + assertFormTagOpened(output); + assertFormTagClosed(output); + } + + @Test + public void defaultActionEncoded() throws Exception { + + this.request.setRequestURI("/a b c"); + request.setQueryString(""); + + this.tag.doStartTag(); + this.tag.doEndTag(); + this.tag.doFinally(); + + String output = getOutput(); + String formOutput = getFormTag(output); + + assertContainsAttribute(formOutput, "action", "/a%20b%20c"); + } + + private String getFormTag(String output) { + int inputStart = output.indexOf("<", 1); + int inputEnd = output.lastIndexOf(">", output.length() - 2); + return output.substring(0, inputStart) + output.substring(inputEnd + 1); + } + + private String getInputTag(String output) { + int inputStart = output.indexOf("<", 1); + int inputEnd = output.lastIndexOf(">", output.length() - 2); + return output.substring(inputStart, inputEnd + 1); + } + + + private static void assertFormTagOpened(String output) { + assertThat(output.startsWith("")).isTrue(); + } + +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/form/HiddenInputTagTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/form/HiddenInputTagTests.java new file mode 100644 index 0000000..29c95bd --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/tags/form/HiddenInputTagTests.java @@ -0,0 +1,139 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://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.springframework.web.servlet.tags.form; + +import javax.servlet.jsp.JspException; +import javax.servlet.jsp.tagext.Tag; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.validation.BeanPropertyBindingResult; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Rob Harrop + */ +public class HiddenInputTagTests extends AbstractFormTagTests { + + private HiddenInputTag tag; + + private TestBean bean; + + @Override + @SuppressWarnings("serial") + protected void onSetUp() { + this.tag = new HiddenInputTag() { + @Override + protected TagWriter createTagWriter() { + return new TagWriter(getWriter()); + } + }; + this.tag.setPageContext(getPageContext()); + } + + @Test + public void render() throws Exception { + this.tag.setPath("name"); + int result = this.tag.doStartTag(); + assertThat(result).isEqualTo(Tag.SKIP_BODY); + + String output = getOutput(); + + assertTagOpened(output); + assertTagClosed(output); + + assertContainsAttribute(output, "type", "hidden"); + assertContainsAttribute(output, "value", "Sally Greenwood"); + assertAttributeNotPresent(output, "disabled"); + } + + @Test + public void withCustomBinder() throws Exception { + this.tag.setPath("myFloat"); + + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(this.bean, COMMAND_NAME); + errors.getPropertyAccessor().registerCustomEditor(Float.class, new SimpleFloatEditor()); + exposeBindingResult(errors); + + assertThat(this.tag.doStartTag()).isEqualTo(Tag.SKIP_BODY); + + String output = getOutput(); + + assertTagOpened(output); + assertTagClosed(output); + + assertContainsAttribute(output, "type", "hidden"); + assertContainsAttribute(output, "value", "12.34f"); + } + + @Test + public void dynamicTypeAttribute() throws JspException { + assertThatIllegalArgumentException().isThrownBy(() -> + this.tag.setDynamicAttribute(null, "type", "email")) + .withMessage("Attribute type=\"email\" is not allowed"); + } + + @Test + public void disabledTrue() throws Exception { + this.tag.setDisabled(true); + + this.tag.doStartTag(); + this.tag.doEndTag(); + + String output = getOutput(); + assertTagOpened(output); + assertTagClosed(output); + + assertContainsAttribute(output, "disabled", "disabled"); + } + + // SPR-8661 + + @Test + public void disabledFalse() throws Exception { + this.tag.setDisabled(false); + + this.tag.doStartTag(); + this.tag.doEndTag(); + + String output = getOutput(); + assertTagOpened(output); + assertTagClosed(output); + + assertAttributeNotPresent(output, "disabled"); + } + + private void assertTagClosed(String output) { + assertThat(output.endsWith("/>")).isTrue(); + } + + private void assertTagOpened(String output) { + assertThat(output.startsWith(" + this.tag.setDynamicAttribute(null, "type", "radio")) + .withMessage("Attribute type=\"radio\" is not allowed"); + } + + @Test + public void dynamicTypeCheckboxAttribute() throws JspException { + assertThatIllegalArgumentException().isThrownBy(() -> + this.tag.setDynamicAttribute(null, "type", "checkbox")) + .withMessage("Attribute type=\"checkbox\" is not allowed"); + } + + protected final void assertTagClosed(String output) { + assertThat(output.endsWith("/>")).as("Tag not closed properly").isTrue(); + } + + protected final void assertTagOpened(String output) { + assertThat(output.startsWith(" + assertAttributeNotPresent(output, "name"); + // id attribute is supported, but we don't want it + assertAttributeNotPresent(output, "id"); + assertThat(output.startsWith("